diff --git a/src/App.vue b/src/App.vue index 3036e35..24903c9 100644 --- a/src/App.vue +++ b/src/App.vue @@ -14,6 +14,7 @@ import { checkStorageCompatibility } from './utils/storage' import { mittBus } from '@/utils/sys' import appLogo from '@/assets/img/common/logox.png' + const router = useRouter() const userStore = useUserStore() const { language, accessToken } = storeToRefs(userStore) @@ -178,6 +179,22 @@ if (userStore.isLogin) { try { const data = await UserService.getUserInfo() + const { systemsid } = useUserStore() + // 排除移动端电子签名页,不做签名校验 + const currentPath = router.currentRoute.value?.path || '' + const isOnMobileSignature = currentPath.startsWith('/auth/signature/mobile') + if (!isOnMobileSignature && (data.user.sign_img == null || data.user.sign_img == '')) { + ElMessageBox.alert('请前往个人中心完成签名操作!', '提醒', { + confirmButtonText: '确定', + callback: () => { + if (systemsid == '1') { + router.push('/admin/user/user_info') + } else { + router.push('/system/user-center') + } + } + }) + } userStore.setUserInfo(data) } catch (error) { console.error('获取用户信息失败', error) diff --git a/src/api/adminApi.ts b/src/api/adminApi.ts index 0eea85d..080d181 100644 --- a/src/api/adminApi.ts +++ b/src/api/adminApi.ts @@ -242,4 +242,54 @@ export class manageService { params }) } + //审批记录 + static stockapprovalrecord(params: any) { + return request.get({ + url: '/admin/approval/records/list', + params + }) + } + //审批通过/拒绝 + static stockapprovalpass(params: any) { + return request.post({ + url: '/admin/approval/records/action', + params + }) + } + //审批记录 + static stockapprovallogs(params: any) { + return request.get({ + url: '/admin/approval/records/logs', + params + }) + } + //备货单 + static getStockList(params: Api.Common.PaginatingParams) { + return request.get({ + url: '/admin/stock/order/list', + params + }) + } + //添加备货单 + static addStock(params: any) { + return request.post({ + url: '/admin/stock/order/store', + params + // showErrorMessage: false // 不显示错误消息 + }) + } + //详情备货单 + static getStockDetail(params: any) { + return request.get({ + url: '/admin/stock/order/show', + params + }) + } + //打印 + static printStock(params: any) { + return request.get({ + url: '/admin/stock/order/print', + params + }) + } } diff --git a/src/api/commonApi.ts b/src/api/commonApi.ts index a359043..2c812fe 100644 --- a/src/api/commonApi.ts +++ b/src/api/commonApi.ts @@ -88,6 +88,13 @@ export class commonService { params }) } + //字典项下拉框 + static getDictItemSelect(params: any) { + return request.get({ + url: '/common/preset/dictItem', + params + }) + } //字典分类下拉框 static getDictTypeSelect() { return request.get({ @@ -118,4 +125,11 @@ export class commonService { url: '/common/preset/users' }) } + //签名 编辑 + static signatureEdit(params: any) { + return request.post({ + url: '/admin/user/update/sign', + params + }) + } } diff --git a/src/api/crmApi.ts b/src/api/crmApi.ts index cf871d8..bfb8dde 100644 --- a/src/api/crmApi.ts +++ b/src/api/crmApi.ts @@ -236,4 +236,26 @@ export class CrmService { params }) } + //审批记录 + static stockapprovalrecord(params: any) { + return request.get({ + url: '/crm/approval/records/list', + params + }) + } + //审批通过/拒绝 + static stockapprovalpass(params: any) { + return request.post({ + url: '/crm/approval/records/action', + params + }) + } + //审批记录 + static stockapprovallogs(params: any) { + return request.get({ + url: '/crm/approval/records/logs', + params + }) + } + } diff --git a/src/composables/usePdfPreview.ts b/src/composables/usePdfPreview.ts index 1da68d4..61f151f 100644 --- a/src/composables/usePdfPreview.ts +++ b/src/composables/usePdfPreview.ts @@ -1,19 +1,42 @@ import { ref, reactive } from 'vue' export const usePdfPreview = () => { - const state = reactive({ visible: false, url: '', name: '', blobUrl: '' }) + const state = reactive({ + visible: false, + url: '', + name: '', + blobUrl: '', + images: [] as string[] + }) const frameRef = ref(null) + // 动态加载 pdf.js(优先本地依赖,失败回退 CDN) + const loadPdfJs = async (): Promise => { + // 统一使用 CDN,避免本地依赖缺失导致打包时报错 + const ensure = (src: string) => + new Promise((resolve, reject) => { + const s = document.createElement('script') + s.src = src + s.onload = () => resolve() + s.onerror = () => reject(new Error('load script failed')) + document.head.appendChild(s) + }) + await ensure('https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js') + await ensure('https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js') + return (window as any).pdfjsLib + } + const open = (url: string, name = '') => { if (!url) return state.url = url state.name = name state.visible = true + state.images = [] } const openWithResponse = (res: any) => { - const url = res?.data?.file_url || res?.file_url - const name = res?.data?.file_name || res?.file_name || '' + const url = res?.data?.file_url_full || res?.file_url_full + const name = res?.data?.file_url || res?.file_url || '' open(url, name) } @@ -32,7 +55,7 @@ export const usePdfPreview = () => { if (!state.url) return const filename = state.name || inferFilenameFromUrl(state.url) try { - const resp = await fetch(state.url, { credentials: 'include' }) + const resp = await fetch(state.url, { credentials: 'omit' }) const blob = await resp.blob() const url = window.URL.createObjectURL(blob) const link = document.createElement('a') @@ -42,7 +65,7 @@ export const usePdfPreview = () => { link.click() document.body.removeChild(link) window.URL.revokeObjectURL(url) - } catch (err) { + } catch { window.open(state.url, '_blank') } } @@ -51,15 +74,42 @@ export const usePdfPreview = () => { const preparePreview = async () => { if (!state.url) return try { - const resp = await fetch(state.url, { credentials: 'include' }) + const resp = await fetch(state.url, { credentials: 'omit' }) const blob = await resp.blob() state.blobUrl && window.URL.revokeObjectURL(state.blobUrl) state.blobUrl = window.URL.createObjectURL(blob) - } catch (e) { + } catch { state.blobUrl = '' } } + // 生成图片以便无需浏览器 PDF 插件即可展示 + const prepareImages = async () => { + if (!state.url) return + state.images = [] + try { + const resp = await fetch(state.url, { credentials: 'omit' }) + const arrayBuffer = await resp.arrayBuffer() + const pdfjsLib = await loadPdfJs() + const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise + const images: string[] = [] + for (let p = 1; p <= pdf.numPages; p++) { + const page = await pdf.getPage(p) + const viewport = page.getViewport({ scale: 1.5 }) + const canvas = document.createElement('canvas') + const context = canvas.getContext('2d') + if (!context) continue + canvas.width = viewport.width + canvas.height = viewport.height + await page.render({ canvasContext: context, viewport }).promise + images.push(canvas.toDataURL('image/png')) + } + state.images = images + } catch { + state.images = [] + } + } + const print = () => { const frame = frameRef.value if (frame && frame.contentWindow) { @@ -75,7 +125,16 @@ export const usePdfPreview = () => { } } - return { pdf: state, frameRef, open, openWithResponse, download, print, preparePreview } + return { + pdf: state, + frameRef, + open, + openWithResponse, + download, + print, + preparePreview, + prepareImages + } } diff --git a/src/composables/useUploadHeaders.ts b/src/composables/useUploadHeaders.ts index be4edca..8450304 100644 --- a/src/composables/useUploadHeaders.ts +++ b/src/composables/useUploadHeaders.ts @@ -4,10 +4,10 @@ import { useUserStore } from '@/store/modules/user' export function useUploadHeaders() { const userStore = useUserStore() const uploadHeaders = computed(() => ({ - 'Fdesk-Language': userStore.language, - 'Fdesk-Corp-Abbr': userStore.companyabbr, - 'Fdesk-System-Code': 'fdesk', - 'Fdesk-Login-Type': userStore.loginType, + 'Fdesk-Language': userStore.language || 'zh', + 'Fdesk-Corp-Abbr': userStore.companyabbr || 'default', + 'Fdesk-System-Code': userStore.systemsabbr || 'fdesk', + 'Fdesk-Login-Type': userStore.loginType || 'upwd', 'Fdesk-Auth-Token': 'Bearer ' + userStore.accessToken })) return { uploadHeaders } diff --git a/src/router/guards/beforeEach.ts b/src/router/guards/beforeEach.ts index 2e27d11..dd5be70 100644 --- a/src/router/guards/beforeEach.ts +++ b/src/router/guards/beforeEach.ts @@ -59,27 +59,65 @@ async function handleRouteGuard( // 设置系统主题 setSystemTheme(to) - // 处理登录状态 - if (!(await handleLoginStatus(to, userStore, next))) { - return + // 特殊处理电子签名页面 - 优先处理,避免登录检查 + if (to.path === '/auth/signature/mobile') { + // 设置认证信息 + if (to.query.accessToken || to.query.token) { + const token = (to.query.accessToken as string) || (to.query.token as string) + if (token) { + userStore.setToken(token) + userStore.setLoginStatus(true) + } + } + + // 设置其他必要的参数到userStore + if (to.query.companyid && to.query.companyabbr) { + userStore.setCompany(to.query.companyid as string, to.query.companyabbr as string) + } + + if (to.query.systemsid && to.query.systemsabbr) { + userStore.setSystems(to.query.systemsid as string, to.query.systemsabbr as string) + } + + if (to.query.loginType) { + userStore.setLoginType(to.query.loginType as 'feishu' | 'upwd') + } + + if (to.matched.length > 0) { + setWorktab(to) + setPageTitle(to) + next() + return + } else { + // 如果路由没有匹配,直接跳转到404 + next(RoutesAlias.Exception404) + return + } } - // 处理动态路由注册 - if (!isRouteRegistered.value && userStore.isLogin) { + // 处理其他页面的登录状态(跳过电子签名页面) + if (to.path !== '/auth/signature/mobile') { + if (!(await handleLoginStatus(to, userStore, next))) { + return + } + } + + // 处理动态路由注册(跳过电子签名页面) + if (to.path !== '/auth/signature/mobile' && !isRouteRegistered.value && userStore.isLogin) { await handleDynamicRoutes(to, router, next) return } - // 处理已知的匹配路由 - if (to.matched.length > 0) { + // 处理已知的匹配路由(跳过电子签名页面) + if (to.path !== '/auth/signature/mobile' && to.matched.length > 0) { setWorktab(to) setPageTitle(to) next() return } - // 尝试刷新路由重新注册 - if (userStore.isLogin) { + // 尝试刷新路由重新注册(跳过电子签名页面) + if (to.path !== '/auth/signature/mobile' && userStore.isLogin) { isRouteRegistered.value = false await handleDynamicRoutes(to, router, next) return @@ -97,6 +135,7 @@ async function handleLoginStatus( userStore: ReturnType, next: NavigationGuardNext ): Promise { + // 检查其他页面的登录状态 if (!userStore.isLogin && to.path !== RoutesAlias.Login && !to.meta.noLogin) { userStore.logOut() next({ @@ -105,6 +144,7 @@ async function handleLoginStatus( }) return false } + return true } @@ -200,22 +240,17 @@ async function registerAndStoreMenu( */ function handleMenuError(error: unknown): void { console.error('菜单处理失败:', error) - // 清理所有状态 const userStore = useUserStore() const menuStore = useMenuStore() const worktabStore = useWorktabStore() - // 清空菜单和工作台 menuStore.clearMenuList() worktabStore.clearAll() - // 重置路由注册状态 isRouteRegistered.value = false - // 登出用户 userStore.logOut() - throw error instanceof Error ? error : new Error('获取菜单列表失败,请重新登录') } @@ -251,20 +286,21 @@ function isValidMenuList(menuList: AppRouteRecord[]): boolean { */ export function resetRouterState(router: Router): void { isRouteRegistered.value = false - + // 清理所有动态注册的路由(不仅仅是带有dynamic标记的) const routesToRemove: string[] = [] router.getRoutes().forEach((route) => { // 排除静态路由和登录页面 - if (route.name && - route.name !== 'Login' && - route.name !== 'Exception404' && - route.name !== 'Exception500' && - !route.path.startsWith('/exception/')) { + if ( + route.name && + route.name !== 'Login' && + route.name !== 'Exception404' && + route.name !== 'Exception500' && + !route.path.startsWith('/exception/') + ) { routesToRemove.push(route.name as string) } }) - // 移除动态路由 routesToRemove.forEach((routeName) => { try { @@ -273,11 +309,9 @@ export function resetRouterState(router: Router): void { console.warn(`移除路由失败: ${routeName}`, error) } }) - // 清空菜单数据 const menuStore = useMenuStore() menuStore.clearMenuList() - // 清空工作台标签页 const worktabStore = useWorktabStore() worktabStore.clearAll() diff --git a/src/router/routes/asyncRoutes.ts b/src/router/routes/asyncRoutes.ts index 086015c..9138c0b 100644 --- a/src/router/routes/asyncRoutes.ts +++ b/src/router/routes/asyncRoutes.ts @@ -133,5 +133,42 @@ export const asyncRoutes: AppRouteRecord[] = [ title: '审批流程添加', isHide: true } + }, + { + path: '/crmpc/approval/auth_detil', + name: 'approvalDetil', + component: 'crmpc/approval/auth_detil', + meta: { + title: '审核详情', + isHide: true + } + }, + // 管理端备货单相关路由 + { + path: '/admin/stock/stocking_list_admin', + name: 'AdminStockingList', + component: 'admin/stock/stocking_list_admin', + meta: { + title: '备货单列表', + isHide: true + } + }, + { + path: '/admin/stock/show_admin', + name: 'AdminStockingShow', + component: 'admin/stock/show_admin', + meta: { + title: '备货单详情', + isHide: true + } + }, + { + path: '/admin/stock/final_show_admin', + name: 'AdminStockingFinalShow', + component: 'admin/stock/final_show_admin', + meta: { + title: '最终备货单详情', + isHide: true + } } ] diff --git a/src/router/utils/registerRoutes.ts b/src/router/utils/registerRoutes.ts index a6c9195..e98f981 100644 --- a/src/router/utils/registerRoutes.ts +++ b/src/router/utils/registerRoutes.ts @@ -130,15 +130,21 @@ function loadComponent(componentPath: string, routeName: string): () => Promise< } // 构建可能的路径 - const fullPath = `../../views${componentPath}.vue` - const fullPathWithIndex = `../../views${componentPath}/index.vue` + const fullPathNoSlash = `../../views${componentPath}.vue` + const fullPathWithIndexNoSlash = `../../views${componentPath}/index.vue` + const fullPathWithSlash = `../../views/${componentPath}.vue` + const fullPathWithIndexSlash = `../../views/${componentPath}/index.vue` - // 先尝试直接路径,再尝试添加/index的路径 - const module = modules[fullPath] || modules[fullPathWithIndex] + // 依次尝试四种可能路径(兼容是否带斜杠的情况) + const module = + modules[fullPathNoSlash] || + modules[fullPathWithIndexNoSlash] || + modules[fullPathWithSlash] || + modules[fullPathWithIndexSlash] if (!module) { console.error( - `[路由错误] 未找到组件:${routeName},尝试过的路径: ${fullPath} 和 ${fullPathWithIndex}` + `[路由错误] 未找到组件:${routeName},尝试过的路径: ${fullPathNoSlash}、${fullPathWithIndexNoSlash}、${fullPathWithSlash} 和 ${fullPathWithIndexSlash}` ) return () => Promise.resolve({ diff --git a/src/store/modules/user copy.ts b/src/store/modules/user copy.ts new file mode 100644 index 0000000..9e53132 --- /dev/null +++ b/src/store/modules/user copy.ts @@ -0,0 +1,212 @@ +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' +import { LanguageEnum } from '@/enums/appEnum' +import { router } from '@/router' +import { useSettingStore } from './setting' +import { useWorktabStore } from './worktab' +import { AppRouteRecord } from '@/types/router' +import { setPageTitle } from '@/router/utils/utils' +import { resetRouterState } from '@/router/guards/beforeEach' +import { RoutesAlias } from '@/router/routesAlias' +import { UserService } from '@/api/usersApi' // 新增导入 +let refreshTimeout: ReturnType | null = null // 定时器变量 + +// 用户 +export const useUserStore = defineStore( + 'userStore', + () => { + const language = ref(LanguageEnum.ZH) + const isLogin = ref(false) + const isLock = ref(false) + const lockPassword = ref('') + const info = ref>({}) + const searchHistory = ref([]) + const accessToken = ref('') + const refreshToken = ref('') + const companyid = ref('') + const companyabbr = ref('') + const systemsid = ref('') + const systemsabbr = ref('') + const home_page = ref('') + const loginType = ref<'feishu' | 'upwd'>('upwd') + const tokenExpireAt = ref(0) + + const getUserInfo = computed(() => info.value) + const getSettingState = computed(() => useSettingStore().$state) + const getWorktabState = computed(() => useWorktabStore().$state) + + const setUserInfo = (newInfo: Api.User.UserInfo) => { + info.value = newInfo + } + + const setLoginStatus = (status: boolean) => { + isLogin.value = status + } + + const setLanguage = (lang: LanguageEnum) => { + setPageTitle(router.currentRoute.value) + language.value = lang + } + const setLoginType = (type: 'feishu' | 'upwd') => { + loginType.value = type + } + const setSearchHistory = (list: AppRouteRecord[]) => { + searchHistory.value = list + } + + const setLockStatus = (status: boolean) => { + isLock.value = status + } + + const setLockPassword = (password: string) => { + lockPassword.value = password + } + + const setToken = (newAccessToken: string, newRefreshToken?: string, expiresIn?: number) => { + accessToken.value = newAccessToken + if (newRefreshToken) { + refreshToken.value = newRefreshToken + } + if (expiresIn) { + const expireAt = Date.now() + expiresIn * 1000 + tokenExpireAt.value = expireAt + localStorage.setItem('token_expire_at', String(expireAt)) + // 检查是否需要重新设置定时器 + const currentLeftSeconds = Math.floor((expireAt - Date.now()) / 1000) + if (currentLeftSeconds > 0) { + scheduleTokenRefresh(currentLeftSeconds) + } + } + } + const setCompany = (id: string, abbr: string) => { + companyid.value = id + companyabbr.value = abbr + } + const setSystems = (id: string, abbr: string) => { + systemsid.value = id + systemsabbr.value = abbr + } + // 设置首页 + const sethome_page = (page: string) => { + home_page.value = page + } + function restoreTokenTimer() { + const expireAt = Number(localStorage.getItem('token_expire_at') || '0') + if (accessToken.value && expireAt > Date.now()) { + const leftSeconds = Math.floor((expireAt - Date.now()) / 1000) + scheduleTokenRefresh(leftSeconds) + } + } + + let isScheduled = false + + function scheduleTokenRefresh(expiresIn: number) { + if (refreshTimeout) { + clearTimeout(refreshTimeout) + refreshTimeout = null + } + + // 提前1分钟刷新 + const refreshTime = (expiresIn - 60) * 1000 + + if (refreshTime > 0) { + if (!isScheduled) { + isScheduled = true + refreshTimeout = setTimeout(() => { + refreshTokenAction().finally(() => { + isScheduled = false // 重置标志位 + }) + }, refreshTime) + } else { + console.log('scheduleTokenRefresh is already scheduled, skipping...') + } + } else { + console.error('Invalid refreshTime:', refreshTime) + } + } + + async function refreshTokenAction() { + try { + const { data } = await UserService.refreshToken({ refresh_token: refreshToken.value }) + // 动态计算剩余时间 + const currentTime = Date.now() + const expireAt = currentTime + data.expires_in * 1000 + const leftSeconds = Math.floor((expireAt - currentTime) / 1000) + setToken(data.access_token, data.refresh_token, leftSeconds) + } catch (e) { + console.error('Token refresh failed:', e) + logOut() + } + } + + const logOut = () => { + // 清空所有本地缓存 + localStorage.clear() + sessionStorage.clear() + systemsabbr.value = '' + systemsid.value = '' + companyabbr.value = '' + companyid.value = '' + home_page.value = '' + info.value = {} + isLogin.value = false + isLock.value = false + lockPassword.value = '' + accessToken.value = '' + refreshToken.value = '' + if (refreshTimeout) clearTimeout(refreshTimeout) // 清理定时器 + // 清理工作台标签页 + useWorktabStore().clearAll() + // 清理sessionStorage中的用户信息缓存 + sessionStorage.removeItem('lastUserInfo') + sessionStorage.removeItem('iframeRoutes') + // 重置路由状态 + resetRouterState(router) + // 跳转到登录页 + router.push(RoutesAlias.Login) + } + + return { + language, + isLogin, + isLock, + lockPassword, + info, + companyid, + companyabbr, + systemsid, + systemsabbr, + home_page, + searchHistory, + loginType, + accessToken, + refreshToken, + tokenExpireAt, + getUserInfo, + getSettingState, + getWorktabState, + setUserInfo, + setLoginStatus, + setLanguage, + setSearchHistory, + setLockStatus, + setLockPassword, + setToken, + setCompany, + setSystems, + setLoginType, + sethome_page, + logOut, + restoreTokenTimer + } + }, + { + persist: { + key: 'user', + storage: localStorage, + afterRestore: (ctx: { store: { restoreTokenTimer: () => void } }) => { + ctx.store.restoreTokenTimer() + } + } as any + } +) diff --git a/src/utils/storage/storage.ts b/src/utils/storage/storage.ts index 6b91467..e027419 100644 --- a/src/utils/storage/storage.ts +++ b/src/utils/storage/storage.ts @@ -98,10 +98,10 @@ class StorageCompatibilityManager { } /** - * 检查是否在登录页面 + * 检查是否在登录页面或电子签名页面 */ private isOnLoginPage(): boolean { - return location.href.includes(RoutesAlias.Login) + return location.href.includes(RoutesAlias.Login) || location.href.includes('/auth/signature/mobile') } /** diff --git a/src/views/admin/approval/approval_list.vue b/src/views/admin/approval/approval_list.vue index 479993a..97ac8cf 100644 --- a/src/views/admin/approval/approval_list.vue +++ b/src/views/admin/approval/approval_list.vue @@ -34,17 +34,17 @@ @current-change="handleCurrentChange" >