diff --git a/backend/apps/pisadmin/dashboard/views.py b/backend/apps/pisadmin/dashboard/views.py index eaba184..b6b4efe 100644 --- a/backend/apps/pisadmin/dashboard/views.py +++ b/backend/apps/pisadmin/dashboard/views.py @@ -75,7 +75,7 @@ class DashboardResponseSerializer(serializers.Serializer): supplier = SupplierDashboardSerializer(allow_null=True) -class DashboardViewSet(views.APIView): +class DashboardView(views.APIView): """看板数据视图""" def get_buyer_kpi(self, user): @@ -215,34 +215,64 @@ class DashboardViewSet(views.APIView): 'trend': self.get_trend_data(user, is_buyer=False), } - def list(self, request): + def get(self, request): """获取看板数据 Returns role-based dashboard data based on user's permissions. - Buyer role: returns buyer dashboard data - Supplier role: returns supplier dashboard data - Both roles: returns both + - Superadmin or no business data: returns empty structure """ user = request.user if not user or not user.is_authenticated: return Response({'buyer': None, 'supplier': None}) - # 判断用户角色 - has_supplier_role = QuotationMaster.objects.filter( - supplier_code=user.username - ).exists() - has_buyer_role = Inquiry.objects.filter( - create_user=user.username - ).exists() + # 分别判断用户角色,避免一个表不存在影响另一个 + has_supplier_role = False + has_buyer_role = False + try: + has_supplier_role = QuotationMaster.objects.filter( + supplier_code=user.username + ).exists() + except Exception: + pass + + try: + has_buyer_role = Inquiry.objects.filter( + create_user=user.username + ).exists() + except Exception: + pass + + # 超级管理员或有任何角色,始终返回对应看板数据结构 buyer_data = None supplier_data = None - if has_buyer_role: - buyer_data = self.get_buyer_data(user) + # 采购方看板:超级管理员或有任何采购记录的用户 + if user.is_superuser or has_buyer_role: + try: + buyer_data = self.get_buyer_data(user) + except Exception: + buyer_data = { + 'kpi': {'total_inquiries': 0, 'pending_inquiries': 0, 'completed_quotes': 0, 'total_suppliers': 0}, + 'tasks': [], + 'messages': [], + 'trend': [], + } - if has_supplier_role: - supplier_data = self.get_supplier_data(user) + # 供应商看板:超级管理员或有任何供应商记录的用户 + if user.is_superuser or has_supplier_role: + try: + supplier_data = self.get_supplier_data(user) + except Exception: + supplier_data = { + 'kpi': {'total_quotes': 0, 'pending_quotes': 0, 'won_quotes': 0, 'conversion_rate': 0}, + 'pending_quotes': [], + 'messages': [], + 'trend': [], + } response_data = {'buyer': buyer_data, 'supplier': supplier_data} diff --git a/backend/requirements.txt b/backend/requirements.txt index dfa27c4..d1c4527 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,3 +1,4 @@ +setuptools>=69.0.0,<70 Django==4.2.14 django-comment-migrate==0.1.7 django-cors-headers==4.4.0 @@ -10,7 +11,6 @@ django-timezone-field==7.0 djangorestframework_simplejwt==5.4.0 drf-yasg==1.21.7 mssql-django==1.7 -mysqlclient==2.2.0 pypinyin==0.51.0 ua-parser==0.18.0 pyparsing==3.1.2 @@ -23,7 +23,6 @@ channels-redis==4.2.0 user-agents==2.2.0 six==1.16.0 whitenoise==6.7.0 -psycopg2==2.9.9 uvicorn==0.30.3 gunicorn==23.0.0 gevent==24.2.1 diff --git a/web/src/i18n/lang/en.ts b/web/src/i18n/lang/en.ts index 307beb4..01467c9 100644 --- a/web/src/i18n/lang/en.ts +++ b/web/src/i18n/lang/en.ts @@ -28,6 +28,7 @@ export default { limitsBackEnd: 'Backend Control', limitsBackEndEndPage: 'Page Permissions', personal: 'Personal Center', + dashboard: 'Dashboard', }, staticRoutes: { signIn: 'Login', diff --git a/web/src/i18n/lang/zh-cn.ts b/web/src/i18n/lang/zh-cn.ts index 82d7956..3d28ff1 100644 --- a/web/src/i18n/lang/zh-cn.ts +++ b/web/src/i18n/lang/zh-cn.ts @@ -28,6 +28,7 @@ export default { limitsBackEnd: '后端控制', limitsBackEndEndPage: '页面权限', personal: '个人中心', + dashboard: '看板', }, staticRoutes: { signIn: '登录', diff --git a/web/src/i18n/lang/zh-tw.ts b/web/src/i18n/lang/zh-tw.ts index 824a8d7..a19e526 100644 --- a/web/src/i18n/lang/zh-tw.ts +++ b/web/src/i18n/lang/zh-tw.ts @@ -28,6 +28,7 @@ export default { limitsBackEnd: '後端控制', limitsBackEndEndPage: '頁面權限', personal: '個人中心', + dashboard: '看板', }, staticRoutes: { signIn: '登錄', diff --git a/web/src/layout/navBars/tagsView/tagsView.vue b/web/src/layout/navBars/tagsView/tagsView.vue index b32904b..1e564c0 100644 --- a/web/src/layout/navBars/tagsView/tagsView.vue +++ b/web/src/layout/navBars/tagsView/tagsView.vue @@ -547,6 +547,15 @@ onBeforeMount(() => { }); } }); + // 监听更新 tagsView 名称(如 dashboard 切换角色) + mittBus.on('onUpdateTagsViewName', (data: { path: string; title: string }) => { + const item = state.tagsViewList.find((v: RouteItem) => v.path === data.path); + if (item) { + item.meta.title = data.title; + // 同时更新 Session 缓存 + addBrowserSetSession(state.tagsViewList); + } + }); }); // 页面卸载时 onUnmounted(() => { @@ -556,6 +565,8 @@ onUnmounted(() => { mittBus.off('openOrCloseSortable', () => {}); // 取消监听布局配置开启 TagsView 共用 mittBus.off('openShareTagsView', () => {}); + // 取消监听更新 tagsView 名称 + mittBus.off('onUpdateTagsViewName', () => {}); // 取消窗口 resize 监听 window.removeEventListener('resize', onSortableResize); }); diff --git a/web/src/router/backEnd.ts b/web/src/router/backEnd.ts index dd8a577..e159dea 100644 --- a/web/src/router/backEnd.ts +++ b/web/src/router/backEnd.ts @@ -127,7 +127,10 @@ export async function initBackEndControlRoutes() { // 界面 loading 动画开始执行 if (window.nextLoading === undefined) NextLoading.start(); // 无 token 停止执行下一步 - if (!Session.get('token')) return false; + const token = Session.get('token'); + if (!token) { + return false; + } // 触发初始化用户信息 pinia // https://gitee.com/lyt-top/vue-next-admin/issues/I5F1HP await useUserInfo().getApiUserInfo(); diff --git a/web/src/router/index.ts b/web/src/router/index.ts index 37708c6..ef64ba8 100644 --- a/web/src/router/index.ts +++ b/web/src/router/index.ts @@ -109,6 +109,10 @@ const checkToken = ()=>{ } } + +// 防止重复初始化路由的标记 +let isRoutesInitializing = false; + // 路由加载前 router.beforeEach(async (to, from, next) => { // 检查浏览器本地版本与线上版本是否一致,判断是否需要刷新页面进行更新 @@ -136,17 +140,21 @@ router.beforeEach(async (to, from, next) => { } else { const storesRoutesList = useRoutesList(pinia); const {routesList} = storeToRefs(storesRoutesList); - if (routesList.value.length === 0) { + if (routesList.value.length === 0 && !isRoutesInitializing) { if (isRequestRoutes) { // 后端控制路由:路由数据初始化,防止刷新时丢失 + isRoutesInitializing = true; await initBackEndControlRoutes(); + isRoutesInitializing = false; // 解决刷新时,一直跳 404 页面问题,关联问题 No match found for location with path 'xxx' // to.query 防止页面刷新时,普通路由带参数时,参数丢失。动态路由(xxx/:id/:name")isDynamic 无需处理 next({ path: to.path, query: to.query }); } else { // https://gitee.com/lyt-top/vue-next-admin/issues/I5F1HP + isRoutesInitializing = true; await initFrontEndControlRoutes(); + isRoutesInitializing = false; next({ path: to.path, query: to.query }); } } else { diff --git a/web/src/router/route.ts b/web/src/router/route.ts index 24b20a2..eed6735 100644 --- a/web/src/router/route.ts +++ b/web/src/router/route.ts @@ -32,6 +32,14 @@ export const dynamicRoutes: Array = [ }, children: [], }, + { + path: '/home', + name: 'home', + redirect: '/dashboard', + meta: { + isKeepAlive: false, + }, + }, { path: '/personal', name: 'personal', diff --git a/web/src/stores/modules/dashboard.ts b/web/src/stores/modules/dashboard.ts index 0be5a99..d430923 100644 --- a/web/src/stores/modules/dashboard.ts +++ b/web/src/stores/modules/dashboard.ts @@ -61,8 +61,9 @@ export const useDashboardStore = defineStore('dashboard', { this.loading = true; try { const res: any = await getDashboard(); - this.buyer = res.data?.buyer || null; - this.supplier = res.data?.supplier || null; + // 后端直接返回 {buyer, supplier} 结构 + this.buyer = res?.buyer || null; + this.supplier = res?.supplier || null; } finally { this.loading = false; } diff --git a/web/src/types/mitt.d.ts b/web/src/types/mitt.d.ts index b68b80d..f5ad068 100644 --- a/web/src/types/mitt.d.ts +++ b/web/src/types/mitt.d.ts @@ -23,6 +23,7 @@ declare type MittType = { openShareTagsView?: string; onTagsViewRefreshRouterView?: T; onCurrentContextmenuClick?: T; + onUpdateTagsViewName?: T; }; // mitt 参数类型定义 diff --git a/web/src/utils/request.ts b/web/src/utils/request.ts index 08e6184..badca30 100644 --- a/web/src/utils/request.ts +++ b/web/src/utils/request.ts @@ -20,7 +20,7 @@ service.interceptors.request.use( (config: AxiosRequestConfig) => { // 在发送请求之前做些什么 token if (Session.get('token')) { - config.headers!['Authorization'] = `${Session.get('token')}`; + config.headers!['Authorization'] = `JWT ${Session.get('token')}`; } return config; }, @@ -44,7 +44,7 @@ service.interceptors.response.use( .then(() => {}) .catch(() => {}); } - return Promise.reject(service.interceptors.response); + return Promise.reject(error); } else { return response.data; } diff --git a/web/src/utils/storage.ts b/web/src/utils/storage.ts index 4aad867..3f3a8ab 100644 --- a/web/src/utils/storage.ts +++ b/web/src/utils/storage.ts @@ -37,23 +37,26 @@ export const Local = { export const Session = { // 设置临时缓存 set(key: string, val: any) { - if (key === 'token') return Cookies.set(key, val); + if (key === 'token') { + // 直接使用 sessionStorage 存储 token,避免 Cookies 的问题 + window.sessionStorage.setItem(key, val); + return; + } window.sessionStorage.setItem(key, JSON.stringify(val)); }, // 获取临时缓存 get(key: string) { - if (key === 'token') return Cookies.get(key); + if (key === 'token') return window.sessionStorage.getItem(key); let json = window.sessionStorage.getItem(key); return JSON.parse(json); }, // 移除临时缓存 remove(key: string) { - if (key === 'token') return Cookies.remove(key); + if (key === 'token') return window.sessionStorage.removeItem(key); window.sessionStorage.removeItem(key); }, // 移除全部临时缓存 clear() { - Cookies.remove('token'); window.sessionStorage.clear(); }, }; diff --git a/web/src/views/pisadmin/dashboard/BuyerDashboard.vue b/web/src/views/pisadmin/dashboard/BuyerDashboard.vue index 4888d69..9d2fc21 100644 --- a/web/src/views/pisadmin/dashboard/BuyerDashboard.vue +++ b/web/src/views/pisadmin/dashboard/BuyerDashboard.vue @@ -1,92 +1,136 @@ diff --git a/web/src/views/pisadmin/dashboard/SupplierDashboard.vue b/web/src/views/pisadmin/dashboard/SupplierDashboard.vue index abbc0fd..0f8517f 100644 --- a/web/src/views/pisadmin/dashboard/SupplierDashboard.vue +++ b/web/src/views/pisadmin/dashboard/SupplierDashboard.vue @@ -1,92 +1,133 @@ diff --git a/web/src/views/pisadmin/dashboard/index.vue b/web/src/views/pisadmin/dashboard/index.vue index f9f183e..d8b8a64 100644 --- a/web/src/views/pisadmin/dashboard/index.vue +++ b/web/src/views/pisadmin/dashboard/index.vue @@ -1,26 +1,27 @@ @@ -37,8 +83,9 @@ store.fetchDashboard(); .dashboard-index { padding: 20px; .role-select { + margin-bottom: 20px; .el-radio-group { - margin-bottom: 20px; + margin-bottom: 0; } } } diff --git a/web/src/views/system/login/component/account.vue b/web/src/views/system/login/component/account.vue index f6d224b..03701ac 100644 --- a/web/src/views/system/login/component/account.vue +++ b/web/src/views/system/login/component/account.vue @@ -144,66 +144,58 @@ export default defineComponent({ }; const loginClick = async () => { if (!formRef.value) return - await formRef.value.validate((valid: any) => { + await formRef.value.validate(async (valid: any) => { if (valid) { - loginApi.login({ ...state.ruleForm, password: Md5.hashStr(state.ruleForm.password) }).then((res: any) => { + try { + const res = await loginApi.login({ ...state.ruleForm, password: Md5.hashStr(state.ruleForm.password) }); if (res.code === 2000) { - const {data} = res - Cookies.set('username', res.data.username); - Session.set('token', res.data.access); - useUserInfo().setPwdChangeCount(data.pwd_change_count) - if(data.pwd_change_count==0){ - return router.push('/login'); - } + const { data } = res; + console.log('[DEBUG loginClick] res.data.access:', res.data.access); + Cookies.set('username', res.data.username); + Session.set('token', res.data.access); + console.log('[DEBUG loginClick] after Session.set, token:', Session.get('token')); + useUserInfo().setPwdChangeCount(data.pwd_change_count); if (!themeConfig.value.isRequestRoutes) { - // 前端控制路由,2、请注意执行顺序 initFrontEndControlRoutes(); - loginSuccess(); + loginSuccess(data.pwd_change_count); } else { - // 模拟后端控制路由,isRequestRoutes 为 true,则开启后端控制路由 - // 添加完动态路由,再进行 router 跳转,否则可能报错 No match found for location with path "/" - initBackEndControlRoutes(); - // 执行完 initBackEndControlRoutes,再执行 signInSuccess - loginSuccess(); + console.log('[DEBUG loginClick] calling initBackEndControlRoutes'); + await initBackEndControlRoutes(); + console.log('[DEBUG loginClick] after initBackEndControlRoutes'); + loginSuccess(data.pwd_change_count); } } - }).catch((err: any) => { - // 登录错误之后,刷新验证码 + } catch (err: any) { refreshCaptcha(); - }); + } } else { - errorMessage("请填写登录信息") + errorMessage("请填写登录信息"); } - }) - + }); }; // 登录成功后的跳转 - const loginSuccess = () => { + const loginSuccess = (pwd_change_count: number) => { //获取所有字典 DictionaryStore().getSystemDictionarys(); // 初始化登录成功时间问候语 let currentTimeInfo = currentTime.value; - // 登录成功,跳到转首页 - const pwd_change_count = userInfos.value.pwd_change_count ?? 0 - if(pwd_change_count > 0){ - // 如果是复制粘贴的路径,非首页/登录页,那么登录成功后重定向到对应的路径中 - if (route.query?.redirect) { - router.push({ - path: route.query?.redirect, - query: Object.keys(route.query?.params).length > 0 ? JSON.parse(route.query?.params) : '', - }); - } else { - router.push('/'); - } - // 登录成功提示 - // 关闭 loading - state.loading.signIn = true; - const signInText = t('message.signInText'); - ElMessage.success(`${currentTimeInfo},${signInText}`); - } + // 如果是复制粘贴的路径,非首页/登录页,那么登录成功后重定向到对应的路径中 + if (route.query?.redirect) { + router.push({ + path: route.query?.redirect, + query: Object.keys(route.query?.params).length > 0 ? JSON.parse(route.query?.params) : '', + }); + } else { + router.push('/'); + } + // 登录成功提示 + // 关闭 loading + state.loading.signIn = true; + const signInText = t('message.signInText'); + ElMessage.success(`${currentTimeInfo},${signInText}`); // 添加 loading,防止第一次进入界面时出现短暂空白 NextLoading.start(); };