| | |
| | | <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> |
| | |
| | | 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> |
| | |
| | | <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> |
| | | |
| | |
| | | <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"; |
| | | |
| | |
| | | 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"]; |
| | |
| | | "/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; |
| | |
| | | auth.clear(); |
| | | router.push("/login"); |
| | | } |
| | | |
| | | async function confirmLogout() { |
| | | try { |
| | | await ElMessageBox.confirm("确认退出当前账号吗?", "退出确认", { |
| | | confirmButtonText: "确认", |
| | | cancelButtonText: "取消", |
| | | type: "warning", |
| | | }); |
| | | } catch { |
| | | return; |
| | | } |
| | | await logout(); |
| | | } |
| | | </script> |
| | | |
| | | <style scoped> |
| | |
| | | } |
| | | |
| | | .side-wrap { |
| | | background: linear-gradient(180deg, #2f63d4 0%, #2d5fce 100%); |
| | | background: #245ECC; |
| | | color: #fff; |
| | | padding: 0 10px 12px; |
| | | box-sizing: border-box; |
| | |
| | | |
| | | .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) { |
| | |
| | | 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 { |
| | |
| | | gap: 8px; |
| | | } |
| | | |
| | | .logout-trigger { |
| | | color: #5e6f95; |
| | | padding: 0; |
| | | } |
| | | |
| | | .user-name { |
| | | color: #47587c; |
| | | font-size: 13px; |
| | |
| | | clearMiniAuthSession() |
| | | if (!redirectingToLogin) { |
| | | redirectingToLogin = true |
| | | const globalToast = useGlobalToast() |
| | | globalToast.error({ msg: message || '登录已过期,请重新登录!', duration: 1200 }) |
| | | const timer = setTimeout(() => { |
| | | clearTimeout(timer) |
| | | redirectingToLogin = false |
| | |
| | | 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'] |
| | |
| | | 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'] |
| | |
| | | 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'] |
| | |
| | | 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'] |
| | |
| | | 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']> |
| | |
| | | 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']> |
| | |
| | | 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']> |
| | |
| | | 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']> |
| | |
| | | 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'] |
| New file |
| | |
| | | <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> |
| | |
| | | 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 }) |
| | | } |
| | |
| | | |
| | | <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" |
| | |
| | | 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() { |
| | |
| | | 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 |
| | |
| | | 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') { |
| | |
| | | return '普通访客' |
| | | }) |
| | | const secondaryText = computed(() => { |
| | | if (!profile.value) { |
| | | if (!profile.value || !isLoggedIn.value) { |
| | | return '' |
| | | } |
| | | if (profile.value.userType === 'EMPLOYEE') { |
| | |
| | | } |
| | | 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) { |
| | |
| | | return order.visitTimeText || '暂无有效期' |
| | | } |
| | | |
| | | function isUnauthorizedError(error: unknown) { |
| | | return getApiErrorCode(error) === 401 || getApiErrorCode(error) === 403 |
| | | } |
| | | |
| | | function formatDateTime(value?: string | null) { |
| | | if (!value) { |
| | | return '' |
| | |
| | | } |
| | | |
| | | 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 |
| | |
| | | catch (error) { |
| | | visitorOrders.value = [] |
| | | ticketIndexMap.value = {} |
| | | if (isUnauthorizedError(error)) { |
| | | return |
| | | } |
| | | uni.showToast({ |
| | | title: error instanceof Error ? error.message : '加载访客码失败', |
| | | icon: 'none', |
| | |
| | | } |
| | | |
| | | async function loadEmployeeDashboardCounts() { |
| | | if (!isEmployee.value) { |
| | | if (!isLoggedIn.value || !isEmployee.value) { |
| | | approvalTodoCount.value = 0 |
| | | visitRecordCount.value = 0 |
| | | return |
| | |
| | | }) |
| | | } |
| | | |
| | | 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() { |
| | | uni.navigateTo({ url: '/pages/reviewList/index' }) |
| | | ensureLoggedIn(() => { |
| | | uni.navigateTo({ url: '/pages/reviewList/index' }) |
| | | }, '您还未登录,请先登录') |
| | | } |
| | | |
| | | function handleOpenVisitRecords() { |
| | | uni.navigateTo({ |
| | | url: '/pages/reception/index', |
| | | }) |
| | | ensureLoggedIn(() => { |
| | | uni.navigateTo({ |
| | | url: '/pages/reception/index', |
| | | }) |
| | | }, '您还未登录,请先登录') |
| | | } |
| | | |
| | | function buildVisitorCodeSharePath(orderUuid?: string | null) { |
| | |
| | | |
| | | onShow(() => { |
| | | authStore.restoreSession() |
| | | void loadVisitorCodes() |
| | | void loadEmployeeDashboardCounts() |
| | | void reloadHomeData() |
| | | }) |
| | | |
| | | onMounted(() => { |
| | | uni.$on('mini-login-success', reloadHomeData) |
| | | }) |
| | | |
| | | onUnmounted(() => { |
| | | uni.$off('mini-login-success', reloadHomeData) |
| | | }) |
| | | |
| | | definePage({ |
| | |
| | | </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;" |
| | |
| | | 待办 |
| | | </text> |
| | | <text class="text-[32rpx] text-[#056DFF] font-bold"> |
| | | {{ approvalTodoCount }} |
| | | {{ approvalTodoDisplay }} |
| | | </text> |
| | | </view> |
| | | </view> |
| | |
| | | 待接待 |
| | | </text> |
| | | <text class="text-[32rpx] text-[#F86911] font-bold"> |
| | | {{ visitRecordCount }} |
| | | {{ visitRecordDisplay }} |
| | | </text> |
| | | </view> |
| | | </view> |
| | |
| | | <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]"> |
| | |
| | | <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]"> |
| | |
| | | <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]"> |
| | |
| | | </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]"> |
| | | 我的访客码 |
| | |
| | | </view> |
| | | |
| | | <image |
| | | v-if="isLoggedIn" |
| | | src="/static/home/process.png" |
| | | class="fixed right-[-42rpx] z-5 h-[128rpx] w-[128rpx]" |
| | | :style="{ bottom: `${parkFlowBottomPx}px` }" |
| | |
| | | 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') { |
| | |
| | | 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' }) |
| | | } |
| | | |
| | |
| | | } |
| | | 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 |
| | |
| | | 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() { |
| | |
| | | }) |
| | | |
| | | 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) { |
| | |
| | | next() |
| | | return |
| | | } |
| | | next({ name: 'login' }) |
| | | showMiniLoginPrompt('您还未登录,请先登录') |
| | | next({ name: HOME_ROUTE_NAME }) |
| | | return |
| | | } |
| | | |
| | |
| | | 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 |
| | | } |
| | |
| | | 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: '', |
| | | } |
| | | } |
| | | |
| | |
| | | uni.setStorageSync(MINI_AUTH_STORAGE_KEY, state) |
| | | } |
| | | |
| | | function patchActiveStore(state: MiniAuthState) { |
| | | function patchActiveStore(state: Partial<MiniAuthStoreState>) { |
| | | const pinia = getActivePinia() |
| | | if (!pinia) { |
| | | return |
| | |
| | | export function clearMiniAuthSession() { |
| | | const state = createInitialState() |
| | | uni.removeStorageSync(MINI_AUTH_STORAGE_KEY) |
| | | patchActiveStore(state) |
| | | patchActiveStore({ |
| | | ...state, |
| | | ...createInitialUiState(), |
| | | }) |
| | | } |
| | | |
| | | export function updateMiniAuthProfile(profile: MiniUserProfile) { |
| | |
| | | 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), |
| | | }, |
| | |
| | | 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, |
| | |
| | | clearSession() { |
| | | this.token = '' |
| | | this.profile = null |
| | | this.loginPromptVisible = false |
| | | this.needPhoneAuthorize = false |
| | | this.authPromptMessage = '' |
| | | uni.removeStorageSync(MINI_AUTH_STORAGE_KEY) |
| | | }, |
| | | restoreSession() { |
| | |
| | | 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 |
| | | }, |
| | | }, |
| | | }) |
| | |
| | | avatarUrl?: string | null |
| | | } |
| | | |
| | | export function normalizeProfileDisplayName(value?: string | null) { |
| | | return (value || '').trim() |
| | | } |
| | | |
| | | export function normalizeLoginProfile(data: MiniLoginResponse): MiniUserProfile { |
| | | return { |
| | | mpUserId: data.mpUserId, |
| | |
| | | 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, |
| | |
| | | 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, |
| New file |
| | |
| | | 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) |
| | | } |