zjk
2026-04-09 06b96c68b0b0811eb310ee23cba7a332d3737e75
feat: 首页未登录,预约页
9个文件已添加
6个文件已修改
487 ■■■■■ 已修改文件
src/App.vue 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components.d.ts 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layouts/tabbar.vue 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages.json 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/appointment/index.vue 239 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/index/index.vue 212 ●●●●● 补丁 | 查看 | 原始文档 | 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 1 ●●●●● 补丁 | 查看 | 原始文档 | 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 {