longz
2026-04-28 bfa7d3b23e526809b04299d9c2719c3449aba3be
小程序图片压缩
2个文件已添加
75个文件已修改
580 ■■■■■ 已修改文件
frontend-admin/src/layouts/MainLayout.vue 78 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yuyueMiniProgram/src/api/core/handlers.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yuyueMiniProgram/src/auto-imports.d.ts 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yuyueMiniProgram/src/components.d.ts 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yuyueMiniProgram/src/components/MiniLoginPrompt.vue 162 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yuyueMiniProgram/src/layouts/tabbar.vue 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yuyueMiniProgram/src/main.ts 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yuyueMiniProgram/src/pages/index/index.vue 94 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yuyueMiniProgram/src/pages/mine/index.vue 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yuyueMiniProgram/src/router/index.ts 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yuyueMiniProgram/src/static/appointment/bg.png 补丁 | 查看 | 原始文档 | blame | 历史
yuyueMiniProgram/src/static/appointment/company.png 补丁 | 查看 | 原始文档 | blame | 历史
yuyueMiniProgram/src/static/appointment/delete.png 补丁 | 查看 | 原始文档 | blame | 历史
yuyueMiniProgram/src/static/appointment/idCard.png 补丁 | 查看 | 原始文档 | blame | 历史
yuyueMiniProgram/src/static/appointment/man.png 补丁 | 查看 | 原始文档 | blame | 历史
yuyueMiniProgram/src/static/appointment/plate.png 补丁 | 查看 | 原始文档 | blame | 历史
yuyueMiniProgram/src/static/appointment/woman.png 补丁 | 查看 | 原始文档 | blame | 历史
yuyueMiniProgram/src/static/choosePerson/add.png 补丁 | 查看 | 原始文档 | blame | 历史
yuyueMiniProgram/src/static/choosePerson/bg@2x.png 补丁 | 查看 | 原始文档 | blame | 历史
yuyueMiniProgram/src/static/choosePerson/edit.png 补丁 | 查看 | 原始文档 | blame | 历史
yuyueMiniProgram/src/static/choosePerson/noData.png 补丁 | 查看 | 原始文档 | blame | 历史
yuyueMiniProgram/src/static/home/appointment.png 补丁 | 查看 | 原始文档 | blame | 历史
yuyueMiniProgram/src/static/home/bg.png 补丁 | 查看 | 原始文档 | blame | 历史
yuyueMiniProgram/src/static/home/chevron_left.png 补丁 | 查看 | 原始文档 | blame | 历史
yuyueMiniProgram/src/static/home/chevron_right.png 补丁 | 查看 | 原始文档 | blame | 历史
yuyueMiniProgram/src/static/home/download.png 补丁 | 查看 | 原始文档 | blame | 历史
yuyueMiniProgram/src/static/home/hi.png 补丁 | 查看 | 原始文档 | blame | 历史
yuyueMiniProgram/src/static/home/left.png 补丁 | 查看 | 原始文档 | blame | 历史
yuyueMiniProgram/src/static/home/process.png 补丁 | 查看 | 原始文档 | blame | 历史
yuyueMiniProgram/src/static/home/record.png 补丁 | 查看 | 原始文档 | blame | 历史
yuyueMiniProgram/src/static/home/right.png 补丁 | 查看 | 原始文档 | blame | 历史
yuyueMiniProgram/src/static/home/share.png 补丁 | 查看 | 原始文档 | blame | 历史
yuyueMiniProgram/src/static/home/tips.png 补丁 | 查看 | 原始文档 | blame | 历史
yuyueMiniProgram/src/static/home/toDo.png 补丁 | 查看 | 原始文档 | blame | 历史
yuyueMiniProgram/src/static/home/toDo_bg.png 补丁 | 查看 | 原始文档 | blame | 历史
yuyueMiniProgram/src/static/home/visitor.png 补丁 | 查看 | 原始文档 | blame | 历史
yuyueMiniProgram/src/static/home/wait.png 补丁 | 查看 | 原始文档 | blame | 历史
yuyueMiniProgram/src/static/home/wait_bg.png 补丁 | 查看 | 原始文档 | blame | 历史
yuyueMiniProgram/src/static/home/成都喜望科技有限公司.png 补丁 | 查看 | 原始文档 | blame | 历史
yuyueMiniProgram/src/static/mine/aboutUs.png 补丁 | 查看 | 原始文档 | blame | 历史
yuyueMiniProgram/src/static/mine/bg.png 补丁 | 查看 | 原始文档 | blame | 历史
yuyueMiniProgram/src/static/mine/bgmask.png 补丁 | 查看 | 原始文档 | blame | 历史
yuyueMiniProgram/src/static/mine/change.png 补丁 | 查看 | 原始文档 | blame | 历史
yuyueMiniProgram/src/static/mine/edit.png 补丁 | 查看 | 原始文档 | blame | 历史
yuyueMiniProgram/src/static/mine/header.png 补丁 | 查看 | 原始文档 | blame | 历史
yuyueMiniProgram/src/static/mine/logout.png 补丁 | 查看 | 原始文档 | blame | 历史
yuyueMiniProgram/src/static/mine/record.png 补丁 | 查看 | 原始文档 | blame | 历史
yuyueMiniProgram/src/static/mine/review.png 补丁 | 查看 | 原始文档 | blame | 历史
yuyueMiniProgram/src/static/review/Pending.png 补丁 | 查看 | 原始文档 | blame | 历史
yuyueMiniProgram/src/static/review/add.png 补丁 | 查看 | 原始文档 | blame | 历史
yuyueMiniProgram/src/static/review/applay.png 补丁 | 查看 | 原始文档 | blame | 历史
yuyueMiniProgram/src/static/review/arrow.png 补丁 | 查看 | 原始文档 | blame | 历史
yuyueMiniProgram/src/static/review/bg-grey.png 补丁 | 查看 | 原始文档 | blame | 历史
yuyueMiniProgram/src/static/review/bg-t.png 补丁 | 查看 | 原始文档 | blame | 历史
yuyueMiniProgram/src/static/review/cancel.png 补丁 | 查看 | 原始文档 | blame | 历史
yuyueMiniProgram/src/static/review/cancelIcon.png 补丁 | 查看 | 原始文档 | blame | 历史
yuyueMiniProgram/src/static/review/cancelReview.png 补丁 | 查看 | 原始文档 | blame | 历史
yuyueMiniProgram/src/static/review/chapter.png 补丁 | 查看 | 原始文档 | blame | 历史
yuyueMiniProgram/src/static/review/circle.png 补丁 | 查看 | 原始文档 | blame | 历史
yuyueMiniProgram/src/static/review/close.png 补丁 | 查看 | 原始文档 | blame | 历史
yuyueMiniProgram/src/static/review/close@2x.png 补丁 | 查看 | 原始文档 | blame | 历史
yuyueMiniProgram/src/static/review/contentbg.png 补丁 | 查看 | 原始文档 | blame | 历史
yuyueMiniProgram/src/static/review/fail.png 补丁 | 查看 | 原始文档 | blame | 历史
yuyueMiniProgram/src/static/review/greenpass.png 补丁 | 查看 | 原始文档 | blame | 历史
yuyueMiniProgram/src/static/review/pass.png 补丁 | 查看 | 原始文档 | blame | 历史
yuyueMiniProgram/src/static/review/passReview.png 补丁 | 查看 | 原始文档 | blame | 历史
yuyueMiniProgram/src/static/review/reduce.png 补丁 | 查看 | 原始文档 | blame | 历史
yuyueMiniProgram/src/static/review/refesh.png 补丁 | 查看 | 原始文档 | blame | 历史
yuyueMiniProgram/src/static/review/refueReview.png 补丁 | 查看 | 原始文档 | blame | 历史
yuyueMiniProgram/src/static/review/review.png 补丁 | 查看 | 原始文档 | blame | 历史
yuyueMiniProgram/src/static/review/success.png 补丁 | 查看 | 原始文档 | blame | 历史
yuyueMiniProgram/src/static/review/underReview.png 补丁 | 查看 | 原始文档 | blame | 历史
yuyueMiniProgram/src/static/viewCode/Download@2x.png 补丁 | 查看 | 原始文档 | blame | 历史
yuyueMiniProgram/src/static/viewCode/bg@2x.png 补丁 | 查看 | 原始文档 | blame | 历史
yuyueMiniProgram/src/store/auth.ts 67 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yuyueMiniProgram/src/types/auth.ts 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yuyueMiniProgram/src/utils/mini-login.ts 75 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
frontend-admin/src/layouts/MainLayout.vue
@@ -1,6 +1,6 @@
<template>
  <el-container class="layout">
    <el-aside width="248px" class="side-wrap">
    <el-aside width="15.98vw" class="side-wrap">
      <div class="brand-block">
        <div class="brand-icon"></div>
        <div class="brand-name">运维运营管理平台</div>
@@ -11,9 +11,9 @@
        unique-opened
        router
        class="side-menu"
        background-color="#2f63d4"
        background-color="#245ECC"
        text-color="#dce8ff"
        active-text-color="#2f63d4"
        active-text-color="#245ECC"
      >
        <el-menu-item v-if="auth.hasPermission('dashboard:page')" index="/dashboard" class="home-item">
          <el-icon><House /></el-icon>
@@ -100,22 +100,19 @@
    <el-container class="main-shell">
      <div class="top-wrap">
        <el-header class="header">
          <div class="header-left">
            <div class="header-title">
              <span class="header-dot"></span>
              <span>运维运营管理平台</span>
            </div>
          </div>
          <div class="header-spacer"></div>
          <div class="header-right">
            <el-button class="menu-trigger" text>
              <el-icon><Operation /></el-icon>
            </el-button>
            <div class="user-box">
              <el-avatar v-if="auth.avatarUrl" :size="28" :src="auth.avatarUrl" class="hdr-av" />
              <el-avatar v-else :size="28" class="hdr-av">{{ nameInitial }}</el-avatar>
              <el-avatar v-if="auth.avatarUrl" :size="30" :src="auth.avatarUrl" class="hdr-av" />
              <el-avatar v-else :size="30" class="hdr-av">{{ nameInitial }}</el-avatar>
              <span class="user-name">{{ auth.displayName }}</span>
              <el-button class="logout-trigger" text @click="confirmLogout">
                <el-icon><SwitchButton /></el-icon>
              </el-button>
            </div>
            <el-button type="primary" link @click="logout">退出</el-button>
          </div>
        </el-header>
@@ -161,7 +158,8 @@
<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from "vue";
import { useRoute, useRouter } from "vue-router";
import { Document, Grid, House, Operation, Setting, User, UserFilled, Wallet } from "@element-plus/icons-vue";
import { ElMessageBox } from "element-plus";
import { Document, Grid, House, Operation, Setting, SwitchButton, User, UserFilled, Wallet } from "@element-plus/icons-vue";
import { useAuthStore } from "../stores/auth";
import http from "../api/http";
@@ -175,6 +173,10 @@
const router = useRouter();
const auth = useAuthStore();
const active = computed(() => route.path);
const nameInitial = computed(() => {
  const n = auth.displayName;
  return n && n.length ? n.slice(0, 1) : "?";
});
const defaultOpeneds = computed(() => {
  const path = route.path;
  if (path.startsWith("/system/")) return ["system"];
@@ -210,11 +212,6 @@
  "/region/local-units": "属地单位管理",
};
const hiddenTabRouteNames = new Set(["staff-group-detail", "role-detail"]);
const nameInitial = computed(() => {
  const n = auth.displayName;
  return n && n.length ? n.slice(0, 1) : "?";
});
function resolveRouteTitle() {
  if (typeof route.meta.title === "string" && route.meta.title) {
    return route.meta.title;
@@ -342,6 +339,19 @@
  auth.clear();
  router.push("/login");
}
async function confirmLogout() {
  try {
    await ElMessageBox.confirm("确认退出当前账号吗?", "退出确认", {
      confirmButtonText: "确认",
      cancelButtonText: "取消",
      type: "warning",
    });
  } catch {
    return;
  }
  await logout();
}
</script>
<style scoped>
@@ -356,7 +366,7 @@
}
.side-wrap {
  background: linear-gradient(180deg, #2f63d4 0%, #2d5fce 100%);
  background: #245ECC;
  color: #fff;
  padding: 0 10px 12px;
  box-sizing: border-box;
@@ -411,7 +421,7 @@
.side-menu :deep(.el-menu-item.is-active) {
  background: #ffffff !important;
  color: #2f63d4 !important;
  color: #245ECC !important;
}
.side-menu :deep(.el-sub-menu .el-menu-item) {
@@ -449,34 +459,23 @@
  display: flex;
  align-items: center;
  justify-content: space-between;
  height: 44px;
  width: 100%;
  height: 60px;
  width: 84vw;
  padding: 0 12px 0 14px;
  background: #ffffff;
  border-bottom: 1px solid #e7edf8;
  box-sizing: border-box;
}
.header-left,
.header-spacer,
.header-right,
.user-box,
.header-title {
.user-box {
  display: flex;
  align-items: center;
}
.header-title {
  gap: 10px;
  color: #3d5d9e;
  font-size: 14px;
  font-weight: 600;
}
.header-dot {
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: linear-gradient(135deg, #7ca9ff, #4f7dff);
.header-spacer {
  flex: 1 1 auto;
}
.header-right {
@@ -491,6 +490,11 @@
  gap: 8px;
}
.logout-trigger {
  color: #5e6f95;
  padding: 0;
}
.user-name {
  color: #47587c;
  font-size: 13px;
yuyueMiniProgram/src/api/core/handlers.ts
@@ -38,8 +38,6 @@
  clearMiniAuthSession()
  if (!redirectingToLogin) {
    redirectingToLogin = true
    const globalToast = useGlobalToast()
    globalToast.error({ msg: message || '登录已过期,请重新登录!', duration: 1200 })
    const timer = setTimeout(() => {
      clearTimeout(timer)
      redirectingToLogin = false
yuyueMiniProgram/src/auto-imports.d.ts
@@ -9,8 +9,10 @@
  const $$userConfigMap: typeof import('./api/index')['$$userConfigMap']
  const Apis: typeof import('./api/index')['Apis']
  const CommonUtil: typeof import('wot-design-uni')['CommonUtil']
  const EMPLOYEE_SUBSCRIBE_TEMPLATE_IDS: typeof import('./utils/mini-login')['EMPLOYEE_SUBSCRIBE_TEMPLATE_IDS']
  const EffectScope: typeof import('vue')['EffectScope']
  const MINI_AUTH_STORAGE_KEY: typeof import('./store/auth')['MINI_AUTH_STORAGE_KEY']
  const MINI_PHONE_AUTH_REQUIRED: typeof import('./utils/mini-login')['MINI_PHONE_AUTH_REQUIRED']
  const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate']
  const alovaInstance: typeof import('./api/index')['alovaInstance']
  const api: typeof import('./api/index')['default']
@@ -67,13 +69,17 @@
  const fetchVisitReasons: typeof import('./api/lookup')['fetchVisitReasons']
  const fetchVisitRecords: typeof import('./api/appointment')['fetchVisitRecords']
  const fetchVisitorDetail: typeof import('./api/visitor')['fetchVisitorDetail']
  const finishMiniLogin: typeof import('./utils/mini-login')['finishMiniLogin']
  const getActivePinia: typeof import('pinia')['getActivePinia']
  const getApiErrorCode: typeof import('./utils/mini-login')['getApiErrorCode']
  const getCurrentInstance: typeof import('vue')['getCurrentInstance']
  const getCurrentPath: typeof import('./utils/index')['getCurrentPath']
  const getCurrentScope: typeof import('vue')['getCurrentScope']
  const getPersistedAuthState: typeof import('./store/auth')['getPersistedAuthState']
  const getPersistedToken: typeof import('./store/auth')['getPersistedToken']
  const getWxLoginCode: typeof import('./utils/mini-login')['getWxLoginCode']
  const h: typeof import('vue')['h']
  const hideMiniLoginPrompt: typeof import('./store/auth')['hideMiniLoginPrompt']
  const ignorableWatch: typeof import('@vueuse/core')['ignorableWatch']
  const inject: typeof import('vue')['inject']
  const injectLocal: typeof import('@vueuse/core')['injectLocal']
@@ -153,6 +159,7 @@
  const refDefault: typeof import('@vueuse/core')['refDefault']
  const refThrottled: typeof import('@vueuse/core')['refThrottled']
  const refWithControl: typeof import('@vueuse/core')['refWithControl']
  const requestEmployeeSubscribeAuthorization: typeof import('./utils/mini-login')['requestEmployeeSubscribeAuthorization']
  const requestMiniSubscribeMessages: typeof import('./utils/subscribeMessage')['requestMiniSubscribeMessages']
  const resolveComponent: typeof import('vue')['resolveComponent']
  const resolveRef: typeof import('@vueuse/core')['resolveRef']
@@ -161,9 +168,11 @@
  const searchVisitedUsersByExactName: typeof import('./api/lookup')['searchVisitedUsersByExactName']
  const setActivePinia: typeof import('pinia')['setActivePinia']
  const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix']
  const setMiniPhoneAuthorizeRequired: typeof import('./store/auth')['setMiniPhoneAuthorizeRequired']
  const shallowReactive: typeof import('vue')['shallowReactive']
  const shallowReadonly: typeof import('vue')['shallowReadonly']
  const shallowRef: typeof import('vue')['shallowRef']
  const showMiniLoginPrompt: typeof import('./store/auth')['showMiniLoginPrompt']
  const storeToRefs: typeof import('pinia')['storeToRefs']
  const submitGeneralApproval: typeof import('./api/appointment')['submitGeneralApproval']
  const submitHandlerApproval: typeof import('./api/appointment')['submitHandlerApproval']
@@ -401,8 +410,10 @@
    readonly $$userConfigMap: UnwrapRef<typeof import('./api/index')['$$userConfigMap']>
    readonly Apis: UnwrapRef<typeof import('./api/index')['Apis']>
    readonly CommonUtil: UnwrapRef<typeof import('wot-design-uni')['CommonUtil']>
    readonly EMPLOYEE_SUBSCRIBE_TEMPLATE_IDS: UnwrapRef<typeof import('./utils/mini-login')['EMPLOYEE_SUBSCRIBE_TEMPLATE_IDS']>
    readonly EffectScope: UnwrapRef<typeof import('vue')['EffectScope']>
    readonly MINI_AUTH_STORAGE_KEY: UnwrapRef<typeof import('./store/auth')['MINI_AUTH_STORAGE_KEY']>
    readonly MINI_PHONE_AUTH_REQUIRED: UnwrapRef<typeof import('./utils/mini-login')['MINI_PHONE_AUTH_REQUIRED']>
    readonly acceptHMRUpdate: UnwrapRef<typeof import('pinia')['acceptHMRUpdate']>
    readonly alovaInstance: UnwrapRef<typeof import('./api/index')['alovaInstance']>
    readonly api: UnwrapRef<typeof import('./api/index')['default']>
@@ -458,13 +469,17 @@
    readonly fetchVisitReasons: UnwrapRef<typeof import('./api/lookup')['fetchVisitReasons']>
    readonly fetchVisitRecords: UnwrapRef<typeof import('./api/appointment')['fetchVisitRecords']>
    readonly fetchVisitorDetail: UnwrapRef<typeof import('./api/visitor')['fetchVisitorDetail']>
    readonly finishMiniLogin: UnwrapRef<typeof import('./utils/mini-login')['finishMiniLogin']>
    readonly getActivePinia: UnwrapRef<typeof import('pinia')['getActivePinia']>
    readonly getApiErrorCode: UnwrapRef<typeof import('./utils/mini-login')['getApiErrorCode']>
    readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']>
    readonly getCurrentPath: UnwrapRef<typeof import('./utils/index')['getCurrentPath']>
    readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']>
    readonly getPersistedAuthState: UnwrapRef<typeof import('./store/auth')['getPersistedAuthState']>
    readonly getPersistedToken: UnwrapRef<typeof import('./store/auth')['getPersistedToken']>
    readonly getWxLoginCode: UnwrapRef<typeof import('./utils/mini-login')['getWxLoginCode']>
    readonly h: UnwrapRef<typeof import('vue')['h']>
    readonly hideMiniLoginPrompt: UnwrapRef<typeof import('./store/auth')['hideMiniLoginPrompt']>
    readonly ignorableWatch: UnwrapRef<typeof import('@vueuse/core')['ignorableWatch']>
    readonly inject: UnwrapRef<typeof import('vue')['inject']>
    readonly injectLocal: UnwrapRef<typeof import('@vueuse/core')['injectLocal']>
@@ -543,6 +558,7 @@
    readonly refDefault: UnwrapRef<typeof import('@vueuse/core')['refDefault']>
    readonly refThrottled: UnwrapRef<typeof import('@vueuse/core')['refThrottled']>
    readonly refWithControl: UnwrapRef<typeof import('@vueuse/core')['refWithControl']>
    readonly requestEmployeeSubscribeAuthorization: UnwrapRef<typeof import('./utils/mini-login')['requestEmployeeSubscribeAuthorization']>
    readonly resolveComponent: UnwrapRef<typeof import('vue')['resolveComponent']>
    readonly resolveRef: UnwrapRef<typeof import('@vueuse/core')['resolveRef']>
    readonly resolveUnref: UnwrapRef<typeof import('@vueuse/core')['resolveUnref']>
@@ -550,9 +566,11 @@
    readonly searchVisitedUsersByExactName: UnwrapRef<typeof import('./api/lookup')['searchVisitedUsersByExactName']>
    readonly setActivePinia: UnwrapRef<typeof import('pinia')['setActivePinia']>
    readonly setMapStoreSuffix: UnwrapRef<typeof import('pinia')['setMapStoreSuffix']>
    readonly setMiniPhoneAuthorizeRequired: UnwrapRef<typeof import('./store/auth')['setMiniPhoneAuthorizeRequired']>
    readonly shallowReactive: UnwrapRef<typeof import('vue')['shallowReactive']>
    readonly shallowReadonly: UnwrapRef<typeof import('vue')['shallowReadonly']>
    readonly shallowRef: UnwrapRef<typeof import('vue')['shallowRef']>
    readonly showMiniLoginPrompt: UnwrapRef<typeof import('./store/auth')['showMiniLoginPrompt']>
    readonly storeToRefs: UnwrapRef<typeof import('pinia')['storeToRefs']>
    readonly submitGeneralApproval: UnwrapRef<typeof import('./api/appointment')['submitGeneralApproval']>
    readonly submitHandlerApproval: UnwrapRef<typeof import('./api/appointment')['submitHandlerApproval']>
yuyueMiniProgram/src/components.d.ts
@@ -11,6 +11,7 @@
    GlobalLoading: typeof import('./components/GlobalLoading.vue')['default']
    GlobalMessage: typeof import('./components/GlobalMessage.vue')['default']
    GlobalToast: typeof import('./components/GlobalToast.vue')['default']
    MiniLoginPrompt: typeof import('./components/MiniLoginPrompt.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']
yuyueMiniProgram/src/components/MiniLoginPrompt.vue
New file
@@ -0,0 +1,162 @@
<script setup lang="ts">
import { loginByWx } from '@/api/auth'
import { useMiniAuthStore } from '@/store/auth'
import { finishMiniLogin, getApiErrorCode, getWxLoginCode, MINI_PHONE_AUTH_REQUIRED } from '@/utils/mini-login'
import { normalizeProfileDisplayName } from '@/types/auth'
const emit = defineEmits<{
  (e: 'login-success'): void
}>()
const authStore = useMiniAuthStore()
const router = useRouter()
const loginSubmitting = ref(false)
const visible = computed(() => authStore.loginPromptVisible)
const needPhoneAuthorize = computed(() => authStore.needPhoneAuthorize)
const message = computed(() => {
  if (needPhoneAuthorize.value) {
    return '首次登录需授权手机号完成身份识别'
  }
  return authStore.authPromptMessage || '您还未登录,请先登录'
})
function hideLoginPrompt() {
  authStore.hideLoginPrompt()
}
function shouldForceEditProfile() {
  return !normalizeProfileDisplayName(authStore.profile?.displayName)
}
function openProfileEditorIfNeeded() {
  if (!shouldForceEditProfile()) {
    return false
  }
  uni.showToast({
    title: '请先完善个人资料',
    icon: 'none',
  })
  router.push({ name: 'profile' })
  return true
}
async function handleStartLogin() {
  if (loginSubmitting.value) {
    return
  }
  loginSubmitting.value = true
  try {
    const wxCode = await getWxLoginCode()
    const loginData = await loginByWx({ wxCode })
    await finishMiniLogin(loginData)
    hideLoginPrompt()
    uni.$emit('mini-login-success')
    emit('login-success')
    if (openProfileEditorIfNeeded()) {
      return
    }
    uni.showToast({
      title: '登录成功',
      icon: 'success',
    })
  }
  catch (error) {
    if (getApiErrorCode(error) === MINI_PHONE_AUTH_REQUIRED) {
      authStore.setNeedPhoneAuthorize(true)
      return
    }
    uni.showToast({
      title: error instanceof Error ? error.message : '登录失败',
      icon: 'none',
    })
  }
  finally {
    loginSubmitting.value = false
  }
}
async function handlePhoneAuthorize(event: any) {
  if (loginSubmitting.value) {
    return
  }
  const phoneCode = event?.detail?.code
  if (!phoneCode) {
    uni.showToast({
      title: '未获取到手机号授权',
      icon: 'none',
    })
    return
  }
  loginSubmitting.value = true
  try {
    const wxCode = await getWxLoginCode()
    const loginData = await loginByWx({ wxCode, phoneCode })
    await finishMiniLogin(loginData)
    hideLoginPrompt()
    uni.$emit('mini-login-success')
    emit('login-success')
    if (openProfileEditorIfNeeded()) {
      return
    }
    uni.showToast({
      title: '登录成功',
      icon: 'success',
    })
  }
  catch (error) {
    uni.showToast({
      title: error instanceof Error ? error.message : '登录失败',
      icon: 'none',
    })
  }
  finally {
    loginSubmitting.value = false
  }
}
</script>
<template>
  <view v-if="visible" class="fixed inset-0 z-40 bg-[rgba(0,0,0,0.55)]" @click="!needPhoneAuthorize && hideLoginPrompt()" />
  <view
    v-if="visible"
    class="fixed left-[50%] top-[50%] z-50 w-[520rpx] translate-x-[-50%] translate-y-[-50%] rounded-[28rpx] bg-white px-[40rpx] pb-[36rpx] pt-[28rpx]"
  >
    <view class="mb-[16rpx] text-center text-[34rpx] text-[#1A1A1A] font-medium">
      提示
    </view>
    <view class="flex flex-col items-center">
      <image src="/static/home/tips.png" class="mb-[24rpx] h-[144rpx] w-[144rpx]" mode="aspectFit" />
      <text class="mb-[44rpx] text-[28rpx] text-[#4A4A4A]">
        {{ message }}
      </text>
    </view>
    <view class="flex items-center justify-between gap-[20rpx]">
      <view
        class="h-[80rpx] flex flex-1 items-center justify-center rounded-full bg-[#F2F2F2] text-[30rpx] text-[#666666]"
        @click="hideLoginPrompt"
      >
        再想想
      </view>
      <button
        v-if="needPhoneAuthorize"
        class="m-0 h-[80rpx] flex flex-1 items-center justify-center rounded-full border-none bg-[#0C6FFF] p-0 text-[30rpx] text-white after:border-none"
        :disabled="loginSubmitting"
        open-type="getPhoneNumber"
        @getphonenumber="handlePhoneAuthorize"
      >
        {{ loginSubmitting ? '授权中...' : '授权手机号' }}
      </button>
      <view
        v-else
        class="h-[80rpx] flex flex-1 items-center justify-center rounded-full bg-[#0C6FFF] text-[30rpx] text-white"
        @click="handleStartLogin"
      >
        {{ loginSubmitting ? '登录中...' : '去登录' }}
      </view>
    </view>
  </view>
</template>
yuyueMiniProgram/src/layouts/tabbar.vue
@@ -2,10 +2,19 @@
const router = useRouter()
const route = useRoute()
const authStore = useMiniAuthStore()
const { activeTabbar, getTabbarItemValue, setTabbarItemActive, tabbarList } = useTabbar()
function handleTabbarChange({ value }: { value: string }) {
  if (value === 'mine' && !authStore.isLoggedIn) {
    authStore.showLoginPrompt('您还未登录,请先登录')
    setTabbarItemActive('home')
    if (route.name !== 'home') {
      router.replaceAll({ name: 'home' })
    }
    return
  }
  setTabbarItemActive(value)
  router.pushTab({ name: value })
}
@@ -34,6 +43,7 @@
<template>
  <slot />
  <MiniLoginPrompt />
  <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 custom-class="custom-tabbar-bg"
yuyueMiniProgram/src/main.ts
@@ -3,6 +3,45 @@
import router from './router'
import 'uno.css'
if (typeof globalThis.TextEncoder === 'undefined') {
  class MiniTextEncoder {
    encode(input = '') {
      const bytes: number[] = []
      for (const char of input) {
        const codePoint = char.codePointAt(0) ?? 0
        if (codePoint <= 0x7F) {
          bytes.push(codePoint)
          continue
        }
        if (codePoint <= 0x7FF) {
          bytes.push(
            0xC0 | (codePoint >> 6),
            0x80 | (codePoint & 0x3F),
          )
          continue
        }
        if (codePoint <= 0xFFFF) {
          bytes.push(
            0xE0 | (codePoint >> 12),
            0x80 | ((codePoint >> 6) & 0x3F),
            0x80 | (codePoint & 0x3F),
          )
          continue
        }
        bytes.push(
          0xF0 | (codePoint >> 18),
          0x80 | ((codePoint >> 12) & 0x3F),
          0x80 | ((codePoint >> 6) & 0x3F),
          0x80 | (codePoint & 0x3F),
        )
      }
      return new Uint8Array(bytes)
    }
  }
  globalThis.TextEncoder = MiniTextEncoder as typeof TextEncoder
}
const pinia = createPinia()
pinia.use(persistPlugin)
export function createApp() {
yuyueMiniProgram/src/pages/index/index.vue
@@ -3,6 +3,7 @@
import { fetchMyApprovalTodos, fetchMyVisitorCodes, fetchVisitRecords } from '@/api/appointment'
import { useMiniAuthStore } from '@/store/auth'
import type { MiniMyVisitorCodeOrderItem, MiniMyVisitorCodeTicketItem } from '@/types/appointment'
import { getApiErrorCode } from '@/utils/mini-login'
interface VisitorCodeDisplayTicket extends MiniMyVisitorCodeTicketItem {
  qrCode: string
@@ -27,9 +28,12 @@
const visitRecordCount = ref(0)
const profile = computed(() => authStore.profile)
const isLoggedIn = computed(() => authStore.isLoggedIn)
const isEmployee = computed(() => profile.value?.userType === 'EMPLOYEE')
const showEmployeeDashboard = computed(() => !isLoggedIn.value || isEmployee.value)
const welcomeName = computed(() => profile.value?.displayName || '欢迎使用,请登录')
const userRoleText = computed(() => {
  if (!profile.value) {
  if (!profile.value || !isLoggedIn.value) {
    return ''
  }
  if (profile.value.userType === 'EMPLOYEE') {
@@ -38,7 +42,7 @@
  return '普通访客'
})
const secondaryText = computed(() => {
  if (!profile.value) {
  if (!profile.value || !isLoggedIn.value) {
    return ''
  }
  if (profile.value.userType === 'EMPLOYEE') {
@@ -46,6 +50,8 @@
  }
  return profile.value.companyName || ''
})
const approvalTodoDisplay = computed(() => isLoggedIn.value ? String(approvalTodoCount.value) : '-')
const visitRecordDisplay = computed(() => isLoggedIn.value ? String(visitRecordCount.value) : '-')
const currentOrder = computed(() => visitorOrders.value[currentOrderIndex.value] || null)
const currentTicket = computed(() => {
  if (!currentOrder.value) {
@@ -102,6 +108,10 @@
  return order.visitTimeText || '暂无有效期'
}
function isUnauthorizedError(error: unknown) {
  return getApiErrorCode(error) === 401 || getApiErrorCode(error) === 403
}
function formatDateTime(value?: string | null) {
  if (!value) {
    return ''
@@ -121,6 +131,13 @@
}
async function loadVisitorCodes() {
  if (!isLoggedIn.value) {
    visitorOrders.value = []
    ticketIndexMap.value = {}
    currentOrderIndex.value = 0
    loadingVisitorCodes.value = false
    return
  }
  loadingVisitorCodes.value = true
  const currentToken = qrRenderToken.value + 1
  qrRenderToken.value = currentToken
@@ -145,6 +162,9 @@
  catch (error) {
    visitorOrders.value = []
    ticketIndexMap.value = {}
    if (isUnauthorizedError(error)) {
      return
    }
    uni.showToast({
      title: error instanceof Error ? error.message : '加载访客码失败',
      icon: 'none',
@@ -158,7 +178,7 @@
}
async function loadEmployeeDashboardCounts() {
  if (!isEmployee.value) {
  if (!isLoggedIn.value || !isEmployee.value) {
    approvalTodoCount.value = 0
    visitRecordCount.value = 0
    return
@@ -335,14 +355,50 @@
  })
}
function showLoginPrompt(message?: string) {
  authStore.showLoginPrompt(message)
}
function hideLoginPrompt() {
  authStore.hideLoginPrompt()
}
function ensureLoggedIn(action?: () => void, message?: string) {
  if (isLoggedIn.value) {
    action?.()
    return true
  }
  showLoginPrompt(message)
  return false
}
async function reloadHomeData() {
  if (!isLoggedIn.value) {
    visitorOrders.value = []
    ticketIndexMap.value = {}
    currentOrderIndex.value = 0
    approvalTodoCount.value = 0
    visitRecordCount.value = 0
    return
  }
  await Promise.all([
    loadVisitorCodes(),
    loadEmployeeDashboardCounts(),
  ])
}
function handleOpenApprovalTodo() {
  ensureLoggedIn(() => {
  uni.navigateTo({ url: '/pages/reviewList/index' })
  }, '您还未登录,请先登录')
}
function handleOpenVisitRecords() {
  ensureLoggedIn(() => {
  uni.navigateTo({
    url: '/pages/reception/index',
  })
  }, '您还未登录,请先登录')
}
function buildVisitorCodeSharePath(orderUuid?: string | null) {
@@ -361,8 +417,15 @@
onShow(() => {
  authStore.restoreSession()
  void loadVisitorCodes()
  void loadEmployeeDashboardCounts()
  void reloadHomeData()
})
onMounted(() => {
  uni.$on('mini-login-success', reloadHomeData)
})
onUnmounted(() => {
  uni.$off('mini-login-success', reloadHomeData)
})
definePage({
@@ -385,19 +448,19 @@
        </text>
      </view>
      <view class="mb-[36rpx] flex items-center pl-[26rpx] pr-[22rpx]">
      <view class="mb-[36rpx] flex items-center pl-[26rpx] pr-[22rpx]" @click="!isLoggedIn && showLoginPrompt('您还未登录,请先登录')">
        <image src="/static/home/hi.png" class="mr-[24rpx] h-[96rpx] w-[212rpx]" mode="aspectFit" />
        <view class="flex flex-col">
          <view class="welcome-title flex items-center text-[56rpx] text-white font-[600]">
            <text>{{ profile?.displayName || '用户' }}</text>
            <text>{{ welcomeName }}</text>
          </view>
          <text class="mt-[8rpx] text-[24rpx] text-white/90">
          <text v-if="isLoggedIn && userRoleText" class="mt-[8rpx] text-[24rpx] text-white/90">
            {{ userRoleText }}<template v-if="secondaryText"> · {{ secondaryText }}</template>
          </text>
        </view>
      </view>
      <view v-if="isEmployee" class="mb-[32rpx] w-full pl-[26rpx] pr-[22rpx]" style="box-sizing: border-box;">
      <view v-if="showEmployeeDashboard" class="mb-[32rpx] w-full pl-[26rpx] pr-[22rpx]" style="box-sizing: border-box;">
        <view
          class="rounded-[16rpx] p-[24rpx] pb-[26rpx]"
          style="background: linear-gradient(180deg, #CCE4FE 0%, #FAFCFD 40%, #FDFDFD 100%); box-shadow: 0 4rpx 8rpx 0 rgba(0, 104, 255, 0.2);border: 1rpx solid #fff;"
@@ -416,7 +479,7 @@
                  待办
                </text>
                <text class="text-[32rpx] text-[#056DFF] font-bold">
                  {{ approvalTodoCount }}
                  {{ approvalTodoDisplay }}
                </text>
              </view>
            </view>
@@ -433,7 +496,7 @@
                  待接待
                </text>
                <text class="text-[32rpx] text-[#F86911] font-bold">
                  {{ visitRecordCount }}
                  {{ visitRecordDisplay }}
                </text>
              </view>
            </view>
@@ -445,7 +508,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' }) }"
          @click="ensureLoggedIn(() => { router.push({ name: 'appointment' }) })"
        >
          <image src="/static/home/appointment.png" class="mb-[12rpx] h-[96rpx] w-[96rpx]" />
          <text class="text-[28rpx] text-[#1a1a1a]">
@@ -456,7 +519,7 @@
          <view
            class="flex flex-1 items-center rounded-[20rpx] px-[40rpx]"
            style="background: linear-gradient(180deg, #FFF7E0 0%, #FFFFFF 100%);box-shadow: 0 4rpx 10rpx 0 rgba(0, 104, 255, 0.16)"
            @click="() => { router.push({ name: 'record' }) }"
            @click="ensureLoggedIn(() => { router.push({ name: 'record' }) })"
          >
            <image src="/static/home/record.png" class="mr-[16rpx] h-[64rpx] w-[64rpx]" />
            <text class="text-[28rpx] text-[#000000]">
@@ -466,7 +529,7 @@
          <view
            class="flex flex-1 items-center rounded-[20rpx] px-[40rpx]"
            style="background: linear-gradient(180deg, #E0FAEE 0%, #FFFFFF 100%);border: 1rpx solid #fff;box-shadow: 0 4rpx 10rpx 0 rgba(0, 104, 255, 0.16)"
            @click="uni.navigateTo({ url: '/pages/choosePerson/index?mode=manage' })"
            @click="ensureLoggedIn(() => { uni.navigateTo({ url: '/pages/choosePerson/index?mode=manage' }) })"
          >
            <image src="/static/home/visitor.png" class="mr-[16rpx] h-[64rpx] w-[64rpx]" />
            <text class="text-[28rpx] text-[#000000]">
@@ -476,7 +539,7 @@
        </view>
      </view>
      <view class="mt-[32rpx] px-[24rpx]">
      <view v-if="isLoggedIn" class="mt-[32rpx] px-[24rpx]">
        <view class="mb-[20rpx] flex items-center justify-between">
          <text class="text-[34rpx] text-[#1a1a1a]">
            我的访客码
@@ -610,6 +673,7 @@
    </view>
    <image
      v-if="isLoggedIn"
      src="/static/home/process.png"
      class="fixed right-[-42rpx] z-5 h-[128rpx] w-[128rpx]"
      :style="{ bottom: `${parkFlowBottomPx}px` }"
yuyueMiniProgram/src/pages/mine/index.vue
@@ -24,6 +24,7 @@
const navBarHeight = 44
const profile = computed(() => authStore.profile)
const isLoggedIn = computed(() => authStore.isLoggedIn)
const userRole = computed<'employee' | 'normal'>(() => profile.value?.userType === 'EMPLOYEE' ? 'employee' : 'normal')
const roleLabel = computed(() => {
  if (userRole.value === 'employee') {
@@ -49,6 +50,10 @@
const visibleMenus = computed(() => menuList.value.filter(item => item.showFor.includes(userRole.value)))
function handleEditProfile() {
  if (!isLoggedIn.value) {
    authStore.showLoginPrompt('您还未登录,请先登录')
    return
  }
  uni.navigateTo({ url: '/pages/profile/index' })
}
@@ -61,11 +66,15 @@
  }
  finally {
    authStore.clearSession()
    router.replaceAll({ name: 'login' })
    router.replaceAll({ name: 'home' })
  }
}
function handleMenuClick(item: MenuItem) {
  if (!isLoggedIn.value) {
    authStore.showLoginPrompt('您还未登录,请先登录')
    return
  }
  if (item.title === '退出登录') {
    void handleLogout()
    return
yuyueMiniProgram/src/router/index.ts
@@ -2,7 +2,7 @@
import { pages, subPackages } from 'virtual:uni-pages'
import { fetchMiniMe } from '@/api/auth'
import { setUnauthorizedRedirectHandler } from './authRedirect'
import { clearMiniAuthSession, getPersistedAuthState, updateMiniAuthProfile } from '@/store/auth'
import { clearMiniAuthSession, getPersistedAuthState, showMiniLoginPrompt, updateMiniAuthProfile } from '@/store/auth'
import { normalizeMeProfile } from '@/types/auth'
function generateRoutes() {
@@ -27,10 +27,11 @@
})
setUnauthorizedRedirectHandler(() => {
  router.replaceAll({ name: 'login' })
  showMiniLoginPrompt('登录已失效,请重新登录')
  router.replaceAll({ name: 'home' })
})
const PUBLIC_ROUTE_NAMES = new Set(['login', 'visitorCode'])
const PUBLIC_ROUTE_NAMES = new Set(['home', 'login', 'visitorCode'])
const HOME_ROUTE_NAME = 'home'
function isPublicRoute(name?: string) {
@@ -48,7 +49,8 @@
      next()
      return
    }
    next({ name: 'login' })
    showMiniLoginPrompt('您还未登录,请先登录')
    next({ name: HOME_ROUTE_NAME })
    return
  }
@@ -64,16 +66,17 @@
    catch (error) {
      console.warn('mini auth guard fetch /me failed', error)
      clearMiniAuthSession()
      showMiniLoginPrompt('登录已失效,请重新登录')
      return false
    }
  }
  return Promise.resolve(ensureProfile()).then((ok) => {
    if (!ok) {
      next({ name: 'login' })
      next({ name: HOME_ROUTE_NAME })
      return
    }
    if (publicRoute) {
    if (to.name === 'login') {
      next({ name: HOME_ROUTE_NAME })
      return
    }
yuyueMiniProgram/src/static/appointment/bg.png

yuyueMiniProgram/src/static/appointment/company.png

yuyueMiniProgram/src/static/appointment/delete.png

yuyueMiniProgram/src/static/appointment/idCard.png

yuyueMiniProgram/src/static/appointment/man.png

yuyueMiniProgram/src/static/appointment/plate.png

yuyueMiniProgram/src/static/appointment/woman.png

yuyueMiniProgram/src/static/choosePerson/add.png

yuyueMiniProgram/src/static/choosePerson/bg@2x.png

yuyueMiniProgram/src/static/choosePerson/edit.png

yuyueMiniProgram/src/static/choosePerson/noData.png

yuyueMiniProgram/src/static/home/appointment.png

yuyueMiniProgram/src/static/home/bg.png

yuyueMiniProgram/src/static/home/chevron_left.png

yuyueMiniProgram/src/static/home/chevron_right.png

yuyueMiniProgram/src/static/home/download.png

yuyueMiniProgram/src/static/home/hi.png

yuyueMiniProgram/src/static/home/left.png

yuyueMiniProgram/src/static/home/process.png

yuyueMiniProgram/src/static/home/record.png

yuyueMiniProgram/src/static/home/right.png

yuyueMiniProgram/src/static/home/share.png

yuyueMiniProgram/src/static/home/tips.png

yuyueMiniProgram/src/static/home/toDo.png

yuyueMiniProgram/src/static/home/toDo_bg.png

yuyueMiniProgram/src/static/home/visitor.png

yuyueMiniProgram/src/static/home/wait.png

yuyueMiniProgram/src/static/home/wait_bg.png

yuyueMiniProgram/src/static/home/成都喜望科技有限公司.png

yuyueMiniProgram/src/static/mine/aboutUs.png

yuyueMiniProgram/src/static/mine/bg.png

yuyueMiniProgram/src/static/mine/bgmask.png

yuyueMiniProgram/src/static/mine/change.png

yuyueMiniProgram/src/static/mine/edit.png

yuyueMiniProgram/src/static/mine/header.png

yuyueMiniProgram/src/static/mine/logout.png

yuyueMiniProgram/src/static/mine/record.png

yuyueMiniProgram/src/static/mine/review.png

yuyueMiniProgram/src/static/review/Pending.png

yuyueMiniProgram/src/static/review/add.png

yuyueMiniProgram/src/static/review/applay.png

yuyueMiniProgram/src/static/review/arrow.png

yuyueMiniProgram/src/static/review/bg-grey.png

yuyueMiniProgram/src/static/review/bg-t.png

yuyueMiniProgram/src/static/review/cancel.png

yuyueMiniProgram/src/static/review/cancelIcon.png

yuyueMiniProgram/src/static/review/cancelReview.png

yuyueMiniProgram/src/static/review/chapter.png

yuyueMiniProgram/src/static/review/circle.png

yuyueMiniProgram/src/static/review/close.png

yuyueMiniProgram/src/static/review/close@2x.png

yuyueMiniProgram/src/static/review/contentbg.png

yuyueMiniProgram/src/static/review/fail.png

yuyueMiniProgram/src/static/review/greenpass.png

yuyueMiniProgram/src/static/review/pass.png

yuyueMiniProgram/src/static/review/passReview.png

yuyueMiniProgram/src/static/review/reduce.png

yuyueMiniProgram/src/static/review/refesh.png

yuyueMiniProgram/src/static/review/refueReview.png

yuyueMiniProgram/src/static/review/review.png

yuyueMiniProgram/src/static/review/success.png

yuyueMiniProgram/src/static/review/underReview.png

yuyueMiniProgram/src/static/viewCode/Download@2x.png

yuyueMiniProgram/src/static/viewCode/bg@2x.png

yuyueMiniProgram/src/store/auth.ts
@@ -8,10 +8,26 @@
  profile: MiniUserProfile | null
}
export interface MiniAuthUiState {
  loginPromptVisible: boolean
  needPhoneAuthorize: boolean
  authPromptMessage: string
}
type MiniAuthStoreState = MiniAuthState & MiniAuthUiState
function createInitialState(): MiniAuthState {
  return {
    token: '',
    profile: null,
  }
}
function createInitialUiState(): MiniAuthUiState {
  return {
    loginPromptVisible: false,
    needPhoneAuthorize: false,
    authPromptMessage: '',
  }
}
@@ -38,7 +54,7 @@
  uni.setStorageSync(MINI_AUTH_STORAGE_KEY, state)
}
function patchActiveStore(state: MiniAuthState) {
function patchActiveStore(state: Partial<MiniAuthStoreState>) {
  const pinia = getActivePinia()
  if (!pinia) {
    return
@@ -50,7 +66,10 @@
export function clearMiniAuthSession() {
  const state = createInitialState()
  uni.removeStorageSync(MINI_AUTH_STORAGE_KEY)
  patchActiveStore(state)
  patchActiveStore({
    ...state,
    ...createInitialUiState(),
  })
}
export function updateMiniAuthProfile(profile: MiniUserProfile) {
@@ -62,8 +81,32 @@
  patchActiveStore(state)
}
export function showMiniLoginPrompt(message = '') {
  patchActiveStore({
    loginPromptVisible: true,
    authPromptMessage: message,
  })
}
export function hideMiniLoginPrompt() {
  patchActiveStore({
    loginPromptVisible: false,
    authPromptMessage: '',
    needPhoneAuthorize: false,
  })
}
export function setMiniPhoneAuthorizeRequired(required: boolean) {
  patchActiveStore({
    needPhoneAuthorize: required,
  })
}
export const useMiniAuthStore = defineStore('miniAuth', {
  state: (): MiniAuthState => createInitialState(),
  state: (): MiniAuthStoreState => ({
    ...createInitialState(),
    ...createInitialUiState(),
  }),
  getters: {
    isLoggedIn: state => Boolean(state.token),
  },
@@ -71,6 +114,9 @@
    setSession(payload: MiniAuthState) {
      this.token = payload.token
      this.profile = payload.profile
      this.loginPromptVisible = false
      this.needPhoneAuthorize = false
      this.authPromptMessage = ''
      writePersistedAuthState({
        token: payload.token,
        profile: payload.profile,
@@ -86,6 +132,9 @@
    clearSession() {
      this.token = ''
      this.profile = null
      this.loginPromptVisible = false
      this.needPhoneAuthorize = false
      this.authPromptMessage = ''
      uni.removeStorageSync(MINI_AUTH_STORAGE_KEY)
    },
    restoreSession() {
@@ -94,5 +143,17 @@
      this.profile = state.profile
      return state
    },
    showLoginPrompt(message = '') {
      this.loginPromptVisible = true
      this.authPromptMessage = message
    },
    hideLoginPrompt() {
      this.loginPromptVisible = false
      this.needPhoneAuthorize = false
      this.authPromptMessage = ''
    },
    setNeedPhoneAuthorize(required: boolean) {
      this.needPhoneAuthorize = required
    },
  },
})
yuyueMiniProgram/src/types/auth.ts
@@ -43,6 +43,10 @@
  avatarUrl?: string | null
}
export function normalizeProfileDisplayName(value?: string | null) {
  return (value || '').trim()
}
export function normalizeLoginProfile(data: MiniLoginResponse): MiniUserProfile {
  return {
    mpUserId: data.mpUserId,
@@ -51,7 +55,7 @@
    deptName: data.deptName ?? null,
    deptLeaderFlag: Boolean(data.deptLeader),
    avatarUrl: data.avatarUrl ?? null,
    displayName: data.displayName,
    displayName: normalizeProfileDisplayName(data.displayName),
    mobile: null,
    gender: null,
    companyName: data.companyName ?? null,
@@ -66,7 +70,7 @@
    deptName: data.deptName ?? null,
    deptLeaderFlag: Boolean(data.deptLeaderFlag),
    avatarUrl: data.avatarUrl ?? null,
    displayName: data.nickName,
    displayName: normalizeProfileDisplayName(data.nickName),
    mobile: data.mobile ?? null,
    gender: data.gender ?? null,
    companyName: data.companyName ?? null,
yuyueMiniProgram/src/utils/mini-login.ts
New file
@@ -0,0 +1,75 @@
import type { ApiError } from '@/api/core/handlers'
import { fetchMiniMe, loginByWx } from '@/api/auth'
import { normalizeLoginProfile, normalizeMeProfile } from '@/types/auth'
import { useMiniAuthStore } from '@/store/auth'
export const EMPLOYEE_SUBSCRIBE_TEMPLATE_IDS = [
  'BX49OBHvRcoYiCw0ZLqx04BiWQ9KaEQwuVth_Lh_M2A',
  'GC1S3T2qHCbz8ow130u7elRczuqW6Z8q54mKRCYE-xo',
]
export const MINI_PHONE_AUTH_REQUIRED = 10008
export function getApiErrorCode(error: unknown) {
  return typeof error === 'object' && error != null && 'code' in error
    ? Number((error as ApiError).code)
    : NaN
}
export async function getWxLoginCode() {
  // #ifdef MP-WEIXIN
  return await new Promise<string>((resolve, reject) => {
    uni.login({
      provider: 'weixin',
      success: (res) => {
        if (res.code) {
          resolve(res.code)
          return
        }
        reject(new Error('获取微信登录凭证失败'))
      },
      fail: () => reject(new Error('获取微信登录凭证失败')),
    })
  })
  // #endif
  // #ifndef MP-WEIXIN
  throw new Error('当前环境暂不支持微信授权登录')
  // #endif
}
export async function requestEmployeeSubscribeAuthorization(userType?: string | null) {
  if (userType !== 'EMPLOYEE') {
    return
  }
  // #ifdef MP-WEIXIN
  try {
    await new Promise<void>((resolve) => {
      uni.requestSubscribeMessage({
        tmplIds: EMPLOYEE_SUBSCRIBE_TEMPLATE_IDS,
        complete: () => resolve(),
      })
    })
  }
  catch (error) {
    console.warn('request employee subscribe message failed', error)
  }
  // #endif
}
export async function finishMiniLogin(loginData: Awaited<ReturnType<typeof loginByWx>>) {
  const authStore = useMiniAuthStore()
  authStore.setSession({
    token: loginData.token,
    profile: normalizeLoginProfile(loginData),
  })
  try {
    const profile = await fetchMiniMe(loginData.token)
    authStore.setProfile(normalizeMeProfile(profile))
  }
  catch (error) {
    console.warn('load mini profile failed after login', error)
  }
  await requestEmployeeSubscribeAuthorization(authStore.profile?.userType)
}