| src/App.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/components.d.ts | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/layouts/tabbar.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/pages.json | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/pages/appointment/index.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/pages/index/index.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/static/appointment/bg.png | 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/static/home/chevron_left.png | 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/static/home/chevron_right.png | 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/static/home/download.png | 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/static/home/left.png | 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/static/home/right.png | 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/static/home/share.png | 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/static/home/成都喜望科技有限公司.png | 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/uni-pages.d.ts | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 |
src/App.vue
@@ -10,7 +10,13 @@ .page-wraper { min-height: calc(100vh - var(--window-top)); box-sizing: border-box; background: #f9f9f9; background: #F6FBFF; } page { background-color: #F6FBFF; --wot-color-white: #F6FBFF; --wot-color-border-light: #E5E5E5; } .wot-theme-dark.page-wraper { src/components.d.ts
@@ -12,11 +12,19 @@ GlobalMessage: typeof import('./components/GlobalMessage.vue')['default'] GlobalToast: typeof import('./components/GlobalToast.vue')['default'] PrivacyPopup: typeof import('./components/PrivacyPopup.vue')['default'] WdButton: typeof import('wot-design-uni/components/wd-button/wd-button.vue')['default'] WdCell: typeof import('wot-design-uni/components/wd-cell/wd-cell.vue')['default'] WdConfigProvider: typeof import('wot-design-uni/components/wd-config-provider/wd-config-provider.vue')['default'] WdDatetimePicker: typeof import('wot-design-uni/components/wd-datetime-picker/wd-datetime-picker.vue')['default'] WdForm: typeof import('wot-design-uni/components/wd-form/wd-form.vue')['default'] WdGap: typeof import('wot-design-uni/components/wd-gap/wd-gap.vue')['default'] WdInput: typeof import('wot-design-uni/components/wd-input/wd-input.vue')['default'] WdMessageBox: typeof import('wot-design-uni/components/wd-message-box/wd-message-box.vue')['default'] WdNotify: typeof import('wot-design-uni/components/wd-notify/wd-notify.vue')['default'] WdPicker: typeof import('wot-design-uni/components/wd-picker/wd-picker.vue')['default'] WdPopup: typeof import('wot-design-uni/components/wd-popup/wd-popup.vue')['default'] WdRadio: typeof import('wot-design-uni/components/wd-radio/wd-radio.vue')['default'] WdRadioGroup: typeof import('wot-design-uni/components/wd-radio-group/wd-radio-group.vue')['default'] WdTabbar: typeof import('wot-design-uni/components/wd-tabbar/wd-tabbar.vue')['default'] WdTabbarItem: typeof import('wot-design-uni/components/wd-tabbar-item/wd-tabbar-item.vue')['default'] WdToast: typeof import('wot-design-uni/components/wd-toast/wd-toast.vue')['default'] src/layouts/tabbar.vue
@@ -36,7 +36,7 @@ <slot /> <wd-gap safe-area-bottom height="var(--wot-tabbar-height, 50px)" /> <wd-tabbar :model-value="activeTabbar.name" active-color="#0068FF" inactive-color="#1A1A1A" bordered safe-area-inset-bottom fixed :model-value="activeTabbar.name" active-color="#0068FF" inactive-color="#1A1A1A" bordered safe-area-inset-bottom fixed custom-class="custom-tabbar-bg" @change="handleTabbarChange" > <wd-tabbar-item @@ -49,3 +49,11 @@ </wd-tabbar-item> </wd-tabbar> </template> <style lang="scss"> .custom-tabbar-bg { border-radius: 20rpx 20rpx 0 0; overflow: hidden; border-top: 2rpx solid var(--wot-color-border-light); } </style> src/pages.json
@@ -24,6 +24,15 @@ } }, { "path": "pages/appointment/index", "type": "page", "name": "appointment", "layout": "default", "style": { "navigationStyle": "custom" } }, { "path": "pages/mine/index", "type": "page", "name": "mine", src/pages/appointment/index.vue
New file @@ -0,0 +1,239 @@ <script setup lang="ts"> import { reactive, ref } from 'vue' definePage({ name: 'appointment', layout: 'default', style: { navigationBarTitleText: '预约', navigationBarBackgroundColor: '#F6FBFF', }, }) const router = useRouter() // 表单数据 const formData = reactive({ interviewee: '', reason: '', dateType: 'single', // single 单日 | multi 多日 visitTimeSingle: '', visitTimeMultiStart: '', visitTimeMultiEnd: '', entryType: 'person', // person 人员 | vehicle 车辆 visitors: [] as any[], // 访客列表 }) // UI 选项 const reasonColumns = ['拜访', '会议', '邀约', '合作', '其他'] // 提交 loading 状态 const isSubmitting = ref(false) // 校验与提交 function handleSubmit() { // 简单的前端校验 if (!formData.interviewee) { return uni.showToast({ title: '请输入被访人', icon: 'none' }) } if (!formData.reason) { return uni.showToast({ title: '请选择拜访事由', icon: 'none' }) } if (formData.dateType === 'single' && !formData.visitTimeSingle) { return uni.showToast({ title: '请选择拜访时间', icon: 'none' }) } if (formData.dateType === 'multi' && (!formData.visitTimeMultiStart || !formData.visitTimeMultiEnd)) { return uni.showToast({ title: '请选择完整的拜访时间', icon: 'none' }) } if (!formData.entryType) { return uni.showToast({ title: '请选择进场类型', icon: 'none' }) } if (formData.visitors.length === 0) { return uni.showToast({ title: '请选择来访人员', icon: 'none' }) } isSubmitting.value = true uni.showLoading({ title: '提交中...' }) // 模拟接口请求 setTimeout(() => { isSubmitting.value = false uni.hideLoading() uni.showToast({ title: '预约成功', icon: 'success', duration: 2000, }) setTimeout(() => { router.back() }, 2000) }, 1500) } // 模拟选择来访人员 function handleSelectVisitor() { // 这里可以跳转到选人页面,为了演示直接 Mock formData.visitors.push({ name: `访客${formData.visitors.length + 1}` }) } </script> <template> <view class="min-h-screen bg-[#F6FBFF] pb-[200rpx]"> <!-- 顶部背景点缀 (与首页风格类似) --> <view class="absolute left-0 top-0 z-0 h-[400rpx] w-full" style="background: linear-gradient(180deg, #D1E6FE 0%, #F6FBFF 100%);" /> <view class="relative z-10 px-[32rpx] pt-[32rpx]"> <!-- 拜访信息模块 --> <view class="mb-[16rpx]"> <text class="text-[32rpx] text-[#1A1A1A] font-bold"> 拜访信息 </text> </view> <view class="mb-[48rpx] rounded-[16rpx] bg-white p-[8rpx] shadow-sm"> <wd-form :model="formData"> <!-- 被访人 --> <wd-input v-model="formData.interviewee" label="被访人" placeholder="请输入要接见谁" required align="right" /> <!-- 拜访事由 --> <wd-picker v-model="formData.reason" label="拜访事由" :columns="reasonColumns" placeholder="请选择拜访事由" required align="right" /> <!-- 拜访时间类型切换 --> <wd-cell title="拜访时间" required custom-class="cell-with-switch"> <view class="flex justify-end"> <view class="h-[64rpx] w-[180rpx] flex rounded-full bg-[#F5F5F5] p-[4rpx]"> <view class="flex flex-1 items-center justify-center rounded-full text-[24rpx] transition-all" :class="formData.dateType === 'single' ? 'bg-[#0068FF] text-white' : 'text-[#666666]'" @click="formData.dateType = 'single'" > 单日 </view> <view class="flex flex-1 items-center justify-center rounded-full text-[24rpx] transition-all" :class="formData.dateType === 'multi' ? 'bg-[#0068FF] text-white' : 'text-[#666666]'" @click="formData.dateType = 'multi'" > 多日 </view> </view> </view> </wd-cell> <!-- 单日时间选择 --> <wd-datetime-picker v-if="formData.dateType === 'single'" v-model="formData.visitTimeSingle" type="datetime" placeholder="请选择拜访时间" align="right" /> <!-- 多日时间选择 (由于 wd-datetime-picker 没有 range,这里用两个 picker 或一个特定交互代替) --> <template v-if="formData.dateType === 'multi'"> <wd-datetime-picker v-model="formData.visitTimeMultiStart" type="datetime" label="开始时间" placeholder="请选择开始时间" align="right" /> <wd-datetime-picker v-model="formData.visitTimeMultiEnd" type="datetime" label="结束时间" placeholder="请选择结束时间" align="right" /> </template> </wd-form> </view> <!-- 来访人信息模块 --> <view class="mb-[16rpx] flex items-end"> <text class="text-[32rpx] text-[#1A1A1A] font-bold"> 来访人信息 </text> <text class="ml-[16rpx] text-[24rpx] text-[#999999]"> 选择您及同行人员 </text> </view> <view class="mb-[24rpx] rounded-[16rpx] bg-white p-[8rpx] shadow-sm"> <wd-cell title="进场类型" custom-class="cell-with-switch"> <view class="flex justify-end"> <wd-radio-group v-model="formData.entryType" inline shape="dot"> <wd-radio value="person"> 人员进场 </wd-radio> <wd-radio value="vehicle"> 车辆进场 </wd-radio> </wd-radio-group> </view> </wd-cell> </view> <view class="mb-[48rpx] rounded-[16rpx] bg-white p-[32rpx] shadow-sm"> <view class="mb-[32rpx] flex items-center"> <text class="text-[28rpx] text-[#1A1A1A]"> 来访人员 </text> <text class="ml-[8rpx] text-[24rpx] text-[#FF4D4F]"> (必填) </text> </view> <view class="flex flex-col items-center"> <view v-if="formData.visitors.length > 0" class="mb-[24rpx] w-full flex flex-wrap gap-[16rpx]"> <view v-for="(v, index) in formData.visitors" :key="index" class="rounded-full bg-[#F5F5F5] px-[24rpx] py-[8rpx] text-[24rpx]"> {{ v.name }} </view> </view> <view class="h-[72rpx] w-[280rpx] flex items-center justify-center border border-[#0068FF] rounded-full border-solid bg-[#F0F6FF]" @click="handleSelectVisitor" > <text class="mb-[4rpx] text-[36rpx] text-[#0068FF] leading-none"> + </text> <text class="ml-[8rpx] text-[28rpx] text-[#0068FF]"> 选择来访人员 </text> </view> </view> </view> <!-- 提交按钮 --> <view class="mt-[64rpx]"> <wd-button type="primary" size="large" block custom-class="submit-btn" :loading="isSubmitting" @click="handleSubmit"> 提交 </wd-button> </view> </view> </view> </template> <style lang="scss" scoped> :deep(.cell-with-switch .wd-cell__value) { flex: 1; } :deep(.submit-btn) { height: 96rpx !important; border-radius: 48rpx !important; font-size: 32rpx !important; background: #0068FF !important; } </style> src/pages/index/index.vue
@@ -6,6 +6,111 @@ // 弹窗显示状态 const showLoginPopup = ref(false) // 登录状态 const isLoggedIn = ref(true) // 访客码数据 const visitorList = ref([ { id: 1, name: '王敏', department: '管理部', area: 'AAAAA', time: '2023-10-10 11:30至2023-10-15 18:30', qrCode: '/static/home/成都喜望科技有限公司.png', visitorName: '王琦', }, { id: 2, name: '李华', department: '技术部', area: 'BBBBB', time: '2023-11-01 09:00至2023-11-01 18:00', qrCode: '/static/home/成都喜望科技有限公司.png', visitorName: '张三', }, ]) const currentVisitorIndex = ref(0) const router = useRouter() // 切换访客码 function handlePrevVisitor() { if (currentVisitorIndex.value > 0) { currentVisitorIndex.value-- } } function handleNextVisitor() { if (currentVisitorIndex.value < visitorList.value.length - 1) { currentVisitorIndex.value++ } } function onSwiperChange(e: any) { currentVisitorIndex.value = e.detail.current } // 下载访客码 function handleDownloadVisitorCode(qrCodeUrl: string) { // 小程序环境中获取图片信息并保存到相册 uni.showLoading({ title: '保存中...' }) // 这里如果是本地路径如 /static/xxx,可以直接用。如果后续换成网络图,需要先 uni.downloadFile // 为了演示,统一走 uni.getImageInfo 以校验文件存在并保存 uni.getImageInfo({ src: qrCodeUrl, success: (image) => { uni.saveImageToPhotosAlbum({ filePath: image.path, success: () => { uni.hideLoading() uni.showToast({ title: '保存成功', icon: 'success', }) }, fail: (err) => { uni.hideLoading() // 拒绝授权后会走 fail if (err.errMsg === 'saveImageToPhotosAlbum:fail auth deny') { uni.showModal({ title: '提示', content: '需要您授权保存相册', success(res) { if (res.confirm) { uni.openSetting({ success(settingdata) { if (settingdata.authSetting['scope.writePhotosAlbum']) { uni.showToast({ title: '授权成功,请重新点击下载', icon: 'none' }) } else { uni.showToast({ title: '授权失败,无法保存', icon: 'none' }) } }, }) } }, }) } else { uni.showToast({ title: '保存失败', icon: 'none', }) } }, }) }, fail: () => { uni.hideLoading() uni.showToast({ title: '获取图片失败', icon: 'none', }) }, }) } // 触发弹窗显示 function handleShowLoginPopup() { showLoginPopup.value = true @@ -22,6 +127,7 @@ if (e.detail.errMsg === 'getPhoneNumber:ok') { // 用户同意授权,这里可以拿到 e.detail.code,发送给后端换取手机号 console.log('获取手机号成功:', e.detail) isLoggedIn.value = true // 模拟登录成功状态 uni.showToast({ title: '登录成功', icon: 'success', @@ -47,12 +153,12 @@ </script> <template> <view class="relative bg-[#F8F9FA]"> <view class="relative"> <!-- 背景图 --> <image src="/static/home/bg.png" class="absolute left-0 top-0 z-0 h-[1624rpx] w-full" mode="scaleToFill" /> <!-- 内容区 --> <view class="relative z-10" :style="{ paddingTop: `${safeAreaInsets?.top || 44}px` }"> <view class="relative z-4" :style="{ paddingTop: `${safeAreaInsets?.top || 44}px` }"> <!-- 导航栏标题 --> <view class="mb-[64rpx] h-[88rpx] flex items-center px-[32rpx]"> <text class="text-[36rpx] text-white font-medium"> @@ -61,14 +167,19 @@ </view> <!-- 欢迎使用区 --> <view class="mb-[36rpx] flex items-center pl-[26rpx] pr-[22rpx]" @click="handleShowLoginPopup"> <view class="mb-[36rpx] flex items-center pl-[26rpx] pr-[22rpx]" @click="!isLoggedIn && handleShowLoginPopup()"> <image src="/static/home/hi.png" class="mr-[24rpx] h-[96rpx] w-[212rpx]" mode="aspectFit" /> <view class="welcome-title flex items-center text-[56rpx] text-white font-[600]"> <template v-if="!isLoggedIn"> <text>欢迎使用,</text> <view class="login-text relative"> 请登录 <view class="login-text__underline absolute bottom-0 left-0 h-[4rpx] w-full bg-white" /> </view> </template> <template v-else> <text>王丽</text> </template> </view> </view> @@ -123,6 +234,7 @@ <view class="h-[240rpx] w-[344rpx] flex flex-1 flex-col items-center justify-center rounded-[20rpx]" style="background: linear-gradient(180deg, #DEF4FF 0%, #FFFFFF 100%);border: 1rpx solid #fff;box-shadow: 0 4rpx 10rpx 0 rgba(0, 104, 255, 0.16)" @click="() => { router.push({ name: 'appointment' }) }" > <image src="/static/home/appointment.png" class="mb-[12rpx] h-[96rpx] w-[96rpx]" /> <text class="text-[28rpx] text-[#1a1a1a]"> @@ -148,6 +260,100 @@ </view> </view> </view> <!-- 登录后:我的访客码区域 --> <view v-if="isLoggedIn" class="mt-[32rpx] px-[24rpx]"> <!-- 头部标题与切换 --> <view class="mb-[20rpx] flex items-center justify-between"> <text class="text-[34rpx] text-[#1a1a1a]"> 我的访客码 </text> <view class="flex items-center"> <image src="/static/home/chevron_left.png" class="h-[40rpx] w-[40rpx] pr-[24rpx]" @click="handlePrevVisitor" /> <text class="text-[28rpx] text-[#999999] line-height-[40rpx]"> {{ currentVisitorIndex + 1 }}/{{ visitorList.length }} </text> <image src="/static/home/chevron_right.png" class="h-[40rpx] w-[40rpx] pl-[24rpx]" @click="handleNextVisitor" /> </view> </view> <!-- 访客码卡片轮播 --> <swiper class="min-h-[840rpx] w-full" :current="currentVisitorIndex" style="box-shadow: 0 6rpx 10rpx 0 rgba(0, 104, 255, 0.16);" @change="onSwiperChange" > <swiper-item v-for="(item) in visitorList" :key="item.id"> <view class="h-full w-full flex flex-col rounded-[12rpx] bg-white py-[24rpx]" style="box-sizing: border-box;overflow: hidden;" > <!-- 顶部信息 --> <view class="flex flex-col items-center"> <text class="mb-[16rpx] text-[28rpx] text-[#1a1a1a]"> {{ item.name }}({{ item.department }}) | 可访问区域{{ item.area }} </text> <view class="mb-[28rpx] mt-[16rpx] h-[48rpx] w-full flex items-center justify-center" style="background: linear-gradient(90deg, rgba(0,104,255,0) 0%, rgba(0,104,255) 50%, rgba(0,104,255,0) 100%);" > <text class="text-[24rpx] text-white"> {{ item.time }} </text> </view> </view> <!-- 二维码区 --> <view class="relative flex items-center justify-center px-[64rpx]"> <image src="/static/home/left.png" class="absolute left-[32rpx] h-[48rpx] w-[24rpx]" @click="handlePrevVisitor" /> <image :src="item.qrCode" class="h-[448rpx] w-[448rpx]" mode="aspectFit" /> <image src="/static/home/right.png" class="absolute right-[32rpx] h-[48rpx] w-[24rpx]" @click="handleNextVisitor" /> </view> <!-- 访客名称 --> <view class="mt-[20rpx] flex justify-center pb-[36rpx]"> <text class="text-[24rpx] text-[rgba(26,26,26,0.5)]"> {{ item.visitorName }}的访客码 </text> </view> <!-- 底部操作 --> <view class="h-[112rpx] flex items-center justify-center gap-[32rpx] pt-[28rpx]" style="border-top: 2rpx dashed #D8D8D8;border-radius: 12rpx;"> <view class="w-[144rpx] flex flex-col items-center justify-center" @click="handleDownloadVisitorCode(item.qrCode)"> <image src="/static/home/download.png" class="mb-[8rpx] h-[40rpx] w-[40rpx]" /> <text class="text-[24rpx] text-[#1a1a1a]"> 下载访客码 </text> </view> <button class="m-0 w-[144rpx] flex flex-col items-center justify-center border-none bg-transparent p-0 line-height-normal after:border-none" open-type="share" > <image src="/static/home/share.png" class="mb-[8rpx] h-[40rpx] w-[40rpx]" /> <text class="text-[24rpx] text-[#1a1a1a]"> 分享好友 </text> </button> </view> </view> </swiper-item> </swiper> </view> <!-- 备案号 (登录后显示在底部) --> <view v-if="isLoggedIn" class="mt-[48rpx] flex justify-center pb-[32rpx]"> <text class="text-[24rpx] text-[#999999]"> 备案号:2435345342 </text> </view> </view> <!-- 悬浮流程图标 --> src/static/appointment/bg.png
src/static/home/chevron_left.png
src/static/home/chevron_right.png
src/static/home/download.png
src/static/home/left.png
src/static/home/right.png
src/static/home/share.png
src/static/home/成都喜望科技有限公司.png
src/uni-pages.d.ts
@@ -5,6 +5,7 @@ type _LocationUrl = "/pages/index/index" | "/pages/appointment/index" | "/pages/mine/index"; interface NavigateToOptions {