Gogs пре 1 месец
родитељ
комит
bdfe186883

+ 23 - 8
apps/platform-app/src/views/DashboardView.vue

@@ -92,7 +92,7 @@ import { ref, onMounted } from 'vue';
 import { useRouter } from 'vue-router';
 import { message } from 'ant-design-vue';
 import { ShopOutlined, CheckCircleOutlined, UserOutlined, FileTextOutlined } from '@ant-design/icons-vue';
-import { FactoryApi, createApiClient } from '@smartcut/api-client';
+import { FactoryApi, UserApi, AuditLogApi, createApiClient } from '@smartcut/api-client';
 import { STORAGE_KEYS } from '@smartcut/shared-utils';
 import { getApiBaseUrl } from '@/apiConfig';
 
@@ -111,15 +111,30 @@ const apiClient = createApiClient({
 });
 
 const factoryApi = new FactoryApi(apiClient);
+const userApi = new UserApi(apiClient);
+const auditLogApi = new AuditLogApi(apiClient);
 
 onMounted(async () => {
-  try {
-    const factoryRes = await factoryApi.listFactories();
-    dashboardData.value.factory_count = factoryRes.total;
-    dashboardData.value.enabled_factory_count = factoryRes.list.filter(f => f.status === 1).length;
-    dashboardData.value.user_count = 5;
-    dashboardData.value.log_count = 100;
-  } catch (error) {
+  // 并行获取三组数据,任一失败不影响其他卡片
+  const [factoryRes, userRes, logRes] = await Promise.allSettled([
+    factoryApi.listFactories(),
+    userApi.getUsers({ page: 1, page_size: 1 }),
+    auditLogApi.getAuditLogs({ page: 1, page_size: 1 })
+  ]);
+
+  if (factoryRes.status === 'fulfilled') {
+    dashboardData.value.factory_count = factoryRes.value.total;
+    dashboardData.value.enabled_factory_count = factoryRes.value.list.filter(f => f.status === 1).length;
+  }
+  if (userRes.status === 'fulfilled') {
+    dashboardData.value.user_count = userRes.value.total;
+  }
+  if (logRes.status === 'fulfilled') {
+    dashboardData.value.log_count = logRes.value.total;
+  }
+
+  // 三组全部失败时才提示错误
+  if (factoryRes.status === 'rejected' && userRes.status === 'rejected' && logRes.status === 'rejected') {
     message.error('加载看板数据失败');
   }
 });

+ 29 - 5
apps/worker-app/src/router/index.ts

@@ -65,28 +65,52 @@ const router = createRouter({
   routes
 });
 
+// 构建redirect路径(剔除factory参数,避免与外层factory重复;登录后守卫会自动重新注入)
+function buildRedirect(to: { path: string; query: Record<string, any> }): string {
+  const restQuery = { ...to.query };
+  delete restQuery.factory;
+  const keys = Object.keys(restQuery);
+  if (keys.length === 0) return to.path;
+  const qs = keys.map(k => `${k}=${encodeURIComponent(String(restQuery[k]))}`).join('&');
+  return `${to.path}?${qs}`;
+}
+
+// 路由守卫:认证检查 + factory_id验证 + 自动注入factory参数
 router.beforeEach((to, _from, next) => {
   const authStore = useAuthStore();
+  const factoryId = authStore.currentFactoryId;
 
   if (to.meta.requiresAuth) {
     if (!authStore.isLoggedIn) {
+      // 未登录:跳转登录页,携带redirect和factory参数
+      // factory优先取store(已登录过的工厂),回退取URL query(支持深链直达 /?factory=xxx)
+      const factoryForLogin = factoryId || (to.query.factory as string) || '';
       next({
         name: 'Login',
         query: {
-          redirect: to.fullPath,
-          factory: authStore.currentFactoryId || ''
+          redirect: buildRedirect(to),
+          ...(factoryForLogin ? { factory: factoryForLogin } : {})
         }
       });
-    } else if (!authStore.currentFactoryId) {
+    } else if (!factoryId) {
+      // 有Token但无factory:跳转登录重新选择工厂(同样回退URL query)
+      const factoryForLogin = (to.query.factory as string) || '';
       next({
         name: 'Login',
-        query: { redirect: to.fullPath }
+        query: {
+          redirect: buildRedirect(to),
+          ...(factoryForLogin ? { factory: factoryForLogin } : {})
+        }
       });
+    } else if (to.query.factory !== factoryId) {
+      // 已登录:确保URL携带正确的factory参数(自动注入,用户无感,支持深链/新标签页)
+      next({ ...to, query: { ...to.query, factory: factoryId } });
     } else {
       next();
     }
   } else if (to.name === 'Login' && authStore.isLoggedIn) {
-    next({ name: 'Home' });
+    // 已登录时访问登录页,跳转首页,携带factory
+    next({ name: 'Home', query: { factory: factoryId || '' } });
   } else {
     next();
   }

+ 1 - 3
apps/worker-app/src/stores/auth.ts

@@ -80,11 +80,9 @@ export const useAuthStore = defineStore('auth', () => {
     } catch (e) { /* 忽略 */ }
     token.value = null;
     user.value = null;
-    currentFactoryId.value = null;
-
+    // 保留 currentFactoryId,便于重新登录同一工厂
     removeSessionStorage(STORAGE_KEYS.WORKER_TOKEN);
     removeSessionStorage(STORAGE_KEYS.WORKER_USER);
-    removeSessionStorage(STORAGE_KEYS.CURRENT_FACTORY_ID);
   }
 
   async function fetchProfile() {

+ 8 - 3
apps/worker-app/src/views/LoginView.vue

@@ -62,7 +62,7 @@ import { useRouter, useRoute } from 'vue-router';
 import { message } from 'ant-design-vue';
 import { useAuthStore } from '@/stores/auth';
 import { FactoryApi, createApiClient } from '@smartcut/api-client';
-import { STORAGE_KEYS } from '@smartcut/shared-utils';
+import { STORAGE_KEYS, getSessionStorage } from '@smartcut/shared-utils';
 import { getApiBaseUrl } from '@/apiConfig';
 import type { Factory } from '@smartcut/types';
 
@@ -74,7 +74,12 @@ const loading = ref(false);
 const loadingFactories = ref(false);
 const factories = ref<Factory[]>([]);
 const selectedFactory = ref('');
-const factoryId = ref(route.query.factory as string);
+// 从 Query 参数获取 factory_id,回退到 SessionStorage
+const factoryId = ref(
+  (route.query.factory as string) ||
+  getSessionStorage<string>(STORAGE_KEYS.CURRENT_FACTORY_ID) ||
+  ''
+);
 const loginType = ref<'username' | 'phone'>('username');
 
 const formState = reactive({
@@ -124,7 +129,7 @@ async function handleLogin() {
     const safeRedirect = redirect && redirect.startsWith('/') && !redirect.startsWith('//') ? redirect : '/';
     router.push(safeRedirect);
   } catch (error: any) {
-    message.error(error.response?.data?.msg || '登录失败');
+    message.error(error.response?.data?.msg || error.message || '登录失败');
   } finally {
     loading.value = false;
   }

+ 4 - 1
apps/worker-app/src/views/ProfileView.vue

@@ -60,7 +60,10 @@ const authStore = useAuthStore();
 async function handleLogout() {
   await authStore.logout();
   message.success('已退出登录');
-  router.push('/login');
+  router.push({
+    name: 'Login',
+    query: { factory: authStore.currentFactoryId || '' }
+  });
 }
 </script>
 

+ 16 - 0
packages/api-client/src/endpoints/auditLog.ts

@@ -0,0 +1,16 @@
+// 审计日志API端点(系统级)
+
+import type { AxiosInstance } from 'axios';
+import type { AuditLog, ApiResponse, PaginatedResponse } from '@smartcut/types';
+
+export class AuditLogApi {
+  constructor(private client: AxiosInstance) {}
+
+  /**
+   * 获取审计日志列表
+   */
+  async getAuditLogs(params?: { page?: number; page_size?: number; action?: string; target_type?: string; actor_user_id?: number; factory_id?: string; success?: number; start?: string; end?: string }): Promise<PaginatedResponse<AuditLog>> {
+    const response = await this.client.get<ApiResponse<PaginatedResponse<AuditLog>>>('/audit-logs', { params });
+    return response.data.data;
+  }
+}

+ 1 - 0
packages/api-client/src/index.ts

@@ -9,5 +9,6 @@ export { ProcessApi } from './endpoints/process';
 export { RecordApi } from './endpoints/record';
 export { SalaryApi } from './endpoints/salary';
 export { StatsApi } from './endpoints/stats';
+export { AuditLogApi } from './endpoints/auditLog';
 
 export type { ApiClientConfig } from './client';

+ 15 - 0
packages/types/src/models.ts

@@ -66,4 +66,19 @@ export interface PriceTemplate {
   process_name?: string; // 关联工序名(JOIN 查询时返回)
   created_at: string;
   updated_at: string;
+}
+
+// 审计日志(系统级,存储于 system.db)
+export interface AuditLog {
+  id: number;
+  actor_user_id: number;
+  actor_role: string;
+  factory_id: string;
+  action: string;
+  target_type: string;
+  target_id: string;
+  success: number; // 1=成功, 0=失败
+  client_ip: string;
+  detail: string;
+  created_at: string;
 }

+ 0 - 2
packages/types/src/stats.ts

@@ -6,8 +6,6 @@ export interface DashboardData {
   today_output: number;
   worker_count: number;
   active_orders: number;
-  output_change?: string;
-  today_change?: string;
 }
 
 // 生产进度数据