Jelajahi Sumber

框架调整优化

Gogs 1 bulan lalu
induk
melakukan
57ae2f1c01
54 mengubah file dengan 1169 tambahan dan 1598 penghapusan
  1. 2 1
      .gitignore
  2. 3 0
      apps/factory-app/index.html
  3. 1 1
      apps/factory-app/package.json
  4. 55 1
      apps/factory-app/src/App.vue
  5. 9 0
      apps/factory-app/src/apiConfig.ts
  6. 12 12
      apps/factory-app/src/router/index.ts
  7. 2 1
      apps/factory-app/src/stores/auth.ts
  8. 33 0
      apps/factory-app/src/stores/layout.ts
  9. 1 12
      apps/factory-app/src/styles/main.css
  10. 23 51
      apps/factory-app/src/views/ChangePasswordView.vue
  11. 88 123
      apps/factory-app/src/views/CutBatchesView.vue
  12. 53 101
      apps/factory-app/src/views/DashboardView.vue
  13. 2 1
      apps/factory-app/src/views/LoginView.vue
  14. 51 83
      apps/factory-app/src/views/PricesView.vue
  15. 88 124
      apps/factory-app/src/views/ProcessesView.vue
  16. 99 135
      apps/factory-app/src/views/RecordsView.vue
  17. 94 126
      apps/factory-app/src/views/SalaryView.vue
  18. 95 127
      apps/factory-app/src/views/UsersView.vue
  19. 142 186
      apps/factory-app/src/views/WorkOrdersView.vue
  20. 9 0
      apps/factory-app/src/vite-env.d.ts
  21. 1 1
      apps/factory-app/vite.config.ts
  22. 3 0
      apps/platform-app/index.html
  23. 1 1
      apps/platform-app/package.json
  24. 45 1
      apps/platform-app/src/App.vue
  25. 9 0
      apps/platform-app/src/apiConfig.ts
  26. 7 7
      apps/platform-app/src/router/index.ts
  27. 2 1
      apps/platform-app/src/stores/auth.ts
  28. 33 0
      apps/platform-app/src/stores/layout.ts
  29. 1 17
      apps/platform-app/src/styles/main.css
  30. 6 55
      apps/platform-app/src/views/ChangePasswordView.vue
  31. 65 145
      apps/platform-app/src/views/DashboardView.vue
  32. 9 60
      apps/platform-app/src/views/FactoriesView.vue
  33. 15 71
      apps/platform-app/src/views/UsersView.vue
  34. 9 0
      apps/platform-app/src/vite-env.d.ts
  35. 1 1
      apps/platform-app/vite.config.ts
  36. 3 0
      apps/worker-app/index.html
  37. 1 1
      apps/worker-app/package.json
  38. 9 0
      apps/worker-app/src/apiConfig.ts
  39. 1 1
      apps/worker-app/src/router/index.ts
  40. 2 1
      apps/worker-app/src/stores/auth.ts
  41. 2 2
      apps/worker-app/src/views/HomeView.vue
  42. 2 1
      apps/worker-app/src/views/LoginView.vue
  43. 2 4
      apps/worker-app/src/views/RecordsView.vue
  44. 2 4
      apps/worker-app/src/views/SalaryView.vue
  45. 2 4
      apps/worker-app/src/views/ScanView.vue
  46. 3 6
      apps/worker-app/src/views/SubmitView.vue
  47. 9 0
      apps/worker-app/src/vite-env.d.ts
  48. 1 1
      apps/worker-app/vite.config.ts
  49. 2 0
      packages/shared-components/package.json
  50. 1 4
      packages/shared-components/src/common/AppToast.vue
  51. 54 122
      packages/shared-components/src/layout/SidebarLayout.vue
  52. 1 1
      packages/shared-components/tsconfig.json
  53. 1 0
      packages/types/src/record.ts
  54. 2 1
      packages/types/src/user.ts

+ 2 - 1
.gitignore

@@ -40,4 +40,5 @@ coverage
 *.tsbuildinfo
 
 # TypeScript
-*.tsbuildinfo
+*.tsbuildinfo
+/.trae/documents

+ 3 - 0
apps/factory-app/index.html

@@ -4,6 +4,9 @@
     <meta charset="UTF-8" />
     <link rel="icon" type="image/svg+xml" href="/vite.svg" />
     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
+    <meta http-equiv="Pragma" content="no-cache" />
+    <meta http-equiv="Expires" content="0" />
     <title>智裁云 - 工厂管理后台</title>
   </head>
   <body>

+ 1 - 1
apps/factory-app/package.json

@@ -5,7 +5,7 @@
   "type": "module",
   "scripts": {
     "dev": "vite",
-    "build": "vite build",
+    "build": "vue-tsc --noEmit && vite build",
     "type-check": "vue-tsc --noEmit",
     "preview": "vite preview",
     "lint": "eslint . --fix",

+ 55 - 1
apps/factory-app/src/App.vue

@@ -1,11 +1,65 @@
 <template>
   <a-config-provider :locale="zhCN">
-    <router-view />
+    <SidebarLayout
+      v-if="showLayout"
+      :userName="authStore.user?.name"
+      :currentPath="route.path"
+      @logout="handleLogout"
+    >
+      <router-view />
+    </SidebarLayout>
+    <router-view v-else />
   </a-config-provider>
 </template>
 
 <script setup lang="ts">
+import { computed, onMounted } from 'vue';
+import { useRouter, useRoute } from 'vue-router';
+import { message } from 'ant-design-vue';
 import zhCN from 'ant-design-vue/es/locale/zh_CN';
+import {
+  DashboardOutlined,
+  FileOutlined,
+  AppstoreOutlined,
+  UnorderedListOutlined,
+  PayCircleOutlined,
+  UserOutlined
+} from '@ant-design/icons-vue';
+import { SidebarLayout } from '@smartcut/shared-components';
+import { useAuthStore } from '@/stores/auth';
+import { useLayoutStore } from '@/stores/layout';
+
+const router = useRouter();
+const route = useRoute();
+const authStore = useAuthStore();
+const layoutStore = useLayoutStore();
+
+// 初始化布局配置
+onMounted(() => {
+  layoutStore.init({
+    menuItems: [
+      { key: 'dashboard', label: '主页看板', icon: DashboardOutlined, path: '/' },
+      { key: 'work-orders', label: '工单管理', icon: FileOutlined, path: '/work-orders' },
+      { key: 'processes', label: '工序管理', icon: AppstoreOutlined, path: '/processes' },
+      { key: 'records', label: '计件记录', icon: UnorderedListOutlined, path: '/records' },
+      { key: 'salary', label: '工资管理', icon: PayCircleOutlined, path: '/salary' },
+      { key: 'users', label: '用户管理', icon: UserOutlined, path: '/users' }
+    ],
+    theme: 'dark'
+  });
+});
+
+// 根据路由 meta 判断是否显示布局
+const showLayout = computed(() => {
+  return route.meta.showLayout !== false;
+});
+
+// 登出处理
+function handleLogout() {
+  authStore.logout();
+  message.success('已退出登录');
+  router.push('/login');
+}
 </script>
 
 <style>

+ 9 - 0
apps/factory-app/src/apiConfig.ts

@@ -0,0 +1,9 @@
+// API配置
+
+/**
+ * 获取API基础地址
+ * 前后端分离架构,返回独立的后端API地址
+ */
+export function getApiBaseUrl(): string {
+  return import.meta.env.VITE_API_BASE_URL;
+}

+ 12 - 12
apps/factory-app/src/router/index.ts

@@ -8,67 +8,67 @@ const routes = [
     path: '/login',
     name: 'Login',
     component: () => import('@/views/LoginView.vue'),
-    meta: { requiresAuth: false }
+    meta: { requiresAuth: false, showLayout: false }
   },
   {
     path: '/',
     name: 'Dashboard',
     component: () => import('@/views/DashboardView.vue'),
-    meta: { requiresAuth: true }
+    meta: { requiresAuth: true, showLayout: true }
   },
   {
     path: '/work-orders',
     name: 'WorkOrders',
     component: () => import('@/views/WorkOrdersView.vue'),
-    meta: { requiresAuth: true }
+    meta: { requiresAuth: true, showLayout: true }
   },
   {
     path: '/processes',
     name: 'Processes',
     component: () => import('@/views/ProcessesView.vue'),
-    meta: { requiresAuth: true }
+    meta: { requiresAuth: true, showLayout: true }
   },
   {
     path: '/records',
     name: 'Records',
     component: () => import('@/views/RecordsView.vue'),
-    meta: { requiresAuth: true }
+    meta: { requiresAuth: true, showLayout: true }
   },
   {
     path: '/cut-batches',
     name: 'CutBatches',
     component: () => import('@/views/CutBatchesView.vue'),
-    meta: { requiresAuth: true }
+    meta: { requiresAuth: true, showLayout: true }
   },
   {
     path: '/prices',
     name: 'Prices',
     component: () => import('@/views/PricesView.vue'),
-    meta: { requiresAuth: true }
+    meta: { requiresAuth: true, showLayout: true }
   },
   {
     path: '/salary',
     name: 'Salary',
     component: () => import('@/views/SalaryView.vue'),
-    meta: { requiresAuth: true }
+    meta: { requiresAuth: true, showLayout: true }
   },
   {
     path: '/users',
     name: 'Users',
     component: () => import('@/views/UsersView.vue'),
-    meta: { requiresAuth: true }
+    meta: { requiresAuth: true, showLayout: true }
   },
   {
     path: '/change-password',
     name: 'ChangePassword',
     component: () => import('@/views/ChangePasswordView.vue'),
-    meta: { requiresAuth: true }
+    meta: { requiresAuth: true, showLayout: true }
   },
   {
     path: '/:pathMatch(.*)*',
     name: 'NotFound',
     component: () => import('@/views/NotFoundView.vue'),
-    meta: { requiresAuth: false }
+    meta: { requiresAuth: false, showLayout: false }
   }
 ];
 
@@ -78,7 +78,7 @@ const router = createRouter({
 });
 
 // 路由守卫:认证检查 + factory_id验证
-router.beforeEach((to, from, next) => {
+router.beforeEach((to, _from, next) => {
   const authStore = useAuthStore();
 
   // 需要认证的路由

+ 2 - 1
apps/factory-app/src/stores/auth.ts

@@ -10,6 +10,7 @@ import {
   getSessionStorage,
   removeSessionStorage
 } from '@smartcut/shared-utils';
+import { getApiBaseUrl } from '@/apiConfig';
 
 export const useAuthStore = defineStore('auth', () => {
   // 状态定义
@@ -22,7 +23,7 @@ export const useAuthStore = defineStore('auth', () => {
 
   // 创建API客户端(factory_id通过Query参数传递)
   const apiClient = createApiClient({
-    baseURL: window.location.origin,
+    baseURL: getApiBaseUrl(),
     tokenKey: STORAGE_KEYS.FACTORY_TOKEN
   });
 

+ 33 - 0
apps/factory-app/src/stores/layout.ts

@@ -0,0 +1,33 @@
+import { defineStore } from 'pinia';
+import type { Component } from 'vue';
+
+export interface MenuItem {
+  key: string;
+  label: string;
+  icon: Component; // 图标组件引用
+  path: string;
+}
+
+export interface LayoutConfig {
+  menuItems: MenuItem[];
+  theme?: 'light' | 'dark';
+}
+
+export const useLayoutStore = defineStore('layout', {
+  state: () => ({
+    menuItems: [] as MenuItem[],
+    theme: 'dark' as 'light' | 'dark',
+    collapsed: false
+  }),
+
+  actions: {
+    init(config: LayoutConfig) {
+      this.menuItems = config.menuItems;
+      if (config.theme) this.theme = config.theme;
+    },
+
+    toggleCollapsed() {
+      this.collapsed = !this.collapsed;
+    }
+  }
+});

+ 1 - 12
apps/factory-app/src/styles/main.css

@@ -2,6 +2,7 @@
 @tailwind components;
 @tailwind utilities;
 
+/* Ant Design 主题定制 */
 :root {
   --ant-primary-color: #1e3a5f;
   --ant-primary-color-hover: #2a4a6f;
@@ -18,16 +19,4 @@
 
 .ant-btn-primary:active {
   background-color: #15304f;
-}
-
-.ant-layout-sider {
-  background-color: #001529 !important;
-}
-
-.ant-card {
-  border-radius: 8px;
-}
-
-.ant-table-wrapper {
-  border-radius: 8px;
 }

+ 23 - 51
apps/factory-app/src/views/ChangePasswordView.vue

@@ -1,33 +1,26 @@
 <template>
-  <SidebarLayout
-    :menuItems="menuItems"
-    :userName="authStore.user?.name"
-    :activeKey="'change-password'"
-    @logout="handleLogout"
-  >
-    <div class="change-password-page">
-      <a-card title="修改密码">
-        <a-form ref="formRef" :model="formState" :rules="formRules" layout="vertical" @finish="handleSubmit">
-          <a-form-item label="旧密码" name="old_password">
-            <a-input-password v-model:value="formState.old_password" placeholder="请输入旧密码" size="large" />
-          </a-form-item>
-          <a-form-item label="新密码" name="new_password">
-            <a-input-password v-model:value="formState.new_password" placeholder="请输入新密码" size="large" />
-            <div class="password-tips">密码强度要求:至少8位,包含大小写字母、数字、特殊字符中3类</div>
-          </a-form-item>
-          <a-form-item label="确认密码" name="confirm_password">
-            <a-input-password v-model:value="formState.confirm_password" placeholder="请再次输入新密码" size="large" />
-          </a-form-item>
-          <a-form-item>
-            <a-space>
-              <a-button type="primary" html-type="submit" size="large" :loading="submitting">提交</a-button>
-              <a-button size="large" @click="resetForm">重置</a-button>
-            </a-space>
-          </a-form-item>
-        </a-form>
-      </a-card>
-    </div>
-  </SidebarLayout>
+  <div>
+    <a-card title="修改密码">
+      <a-form ref="formRef" :model="formState" :rules="formRules" layout="vertical" @finish="handleSubmit">
+        <a-form-item label="旧密码" name="old_password">
+          <a-input-password v-model:value="formState.old_password" placeholder="请输入旧密码" size="large" />
+        </a-form-item>
+        <a-form-item label="新密码" name="new_password">
+          <a-input-password v-model:value="formState.new_password" placeholder="请输入新密码" size="large" />
+          <div class="text-xs text-gray-400 mt-2">密码强度要求:至少8位,包含大小写字母、数字、特殊字符中3类</div>
+        </a-form-item>
+        <a-form-item label="确认密码" name="confirm_password">
+          <a-input-password v-model:value="formState.confirm_password" placeholder="请再次输入新密码" size="large" />
+        </a-form-item>
+        <a-form-item>
+          <a-space>
+            <a-button type="primary" html-type="submit" size="large" :loading="submitting">提交</a-button>
+            <a-button size="large" @click="resetForm">重置</a-button>
+          </a-space>
+        </a-form-item>
+      </a-form>
+    </a-card>
+  </div>
 </template>
 
 <script setup lang="ts">
@@ -35,21 +28,11 @@ import { ref, reactive } from 'vue';
 import { useRouter } from 'vue-router';
 import { message } from 'ant-design-vue';
 import { useAuthStore } from '@/stores/auth';
-import { SidebarLayout } from '@smartcut/shared-components';
 import { validatePasswordStrength } from '@smartcut/shared-utils';
 
 const router = useRouter();
 const authStore = useAuthStore();
 
-const menuItems = [
-  { key: 'dashboard', label: '主页看板', icon: 'DashboardOutlined', path: '/' },
-  { key: 'work-orders', label: '工单管理', icon: 'FileOutlined', path: '/work-orders' },
-  { key: 'processes', label: '工序管理', icon: 'AppstoreOutlined', path: '/processes' },
-  { key: 'records', label: '计件记录', icon: 'UnorderedListOutlined', path: '/records' },
-  { key: 'salary', label: '工资管理', icon: 'PayCircleOutlined', path: '/salary' },
-  { key: 'users', label: '用户管理', icon: 'UserOutlined', path: '/users' }
-];
-
 const submitting = ref(false);
 const formRef = ref();
 
@@ -106,15 +89,4 @@ function resetForm() {
   formState.new_password = '';
   formState.confirm_password = '';
 }
-
-function handleLogout() {
-  authStore.logout();
-  message.success('已退出登录');
-  router.push('/login');
-}
-</script>
-
-<style scoped lang="postcss">
-.change-password-page { padding: 0; }
-.password-tips { font-size: 12px; color: #999; margin-top: 8px; }
-</style>
+</script>

+ 88 - 123
apps/factory-app/src/views/CutBatchesView.vue

@@ -1,122 +1,101 @@
 <template>
-  <SidebarLayout
-    :menuItems="menuItems"
-    :userName="authStore.user?.name"
-    :activeKey="'cut-batches'"
-    @logout="handleLogout"
-  >
-    <div class="cut-batches-page">
-      <a-card>
-        <a-space>
-          <a-input-search v-model:value="searchKeyword" placeholder="搜索批次号" style="width: 300px" @search="loadBatches" />
-          <a-button type="primary" @click="showCreateModal">创建批次</a-button>
-        </a-space>
-      </a-card>
-
-      <a-card class="mt-4" title="裁床批次列表">
-        <a-table :columns="columns" :dataSource="batches" :loading="loading" rowKey="id">
-          <template #status="{ record }">
-            <a-tag :color="getStatusColor(record.status)">{{ getStatusText(record.status) }}</a-tag>
-          </template>
-          <template #action="{ record }">
-            <a-space>
-              <a-button size="small" @click="showBundlesModal(record)">查看扎号</a-button>
-              <a-button size="small" @click="showEditModal(record)">编辑</a-button>
-              <a-popconfirm title="确定删除此批次吗?" @confirm="deleteBatch(record.id)">
-                <a-button size="small" danger>删除</a-button>
-              </a-popconfirm>
-            </a-space>
-          </template>
-        </a-table>
-      </a-card>
-
-      <!-- 创建/编辑批次模态框 -->
-      <a-modal v-model:open="modalVisible" :title="modalTitle" @ok="handleSubmit" :confirmLoading="submitting" width="600px">
-        <a-form ref="formRef" :model="formState" :rules="formRules" layout="vertical">
-          <a-form-item label="工单" name="work_order_id" v-if="!isEdit">
-            <a-select v-model:value="formState.work_order_id" placeholder="请选择工单">
-              <a-select-option v-for="wo in workOrders" :key="wo.id" :value="wo.id">
-                {{ wo.order_no }} - {{ wo.style_name }}
-              </a-select-option>
-            </a-select>
-          </a-form-item>
-          <a-form-item label="批次号" name="batch_no" v-if="!isEdit">
-            <a-input v-model:value="formState.batch_no" placeholder="请输入批次号" />
-          </a-form-item>
-          <a-form-item label="颜色" name="color">
-            <a-input v-model:value="formState.color" placeholder="请输入颜色" />
-          </a-form-item>
-          <a-form-item label="总数量" name="total_qty">
-            <a-input-number v-model:value="formState.total_qty" :min="1" :max="100000" style="width: 100%" />
-          </a-form-item>
-        </a-form>
-      </a-modal>
-
-      <!-- 扎号管理模态框 -->
-      <a-modal v-model:open="bundlesModalVisible" :title="`扎号管理 - ${currentBatch?.batch_no}`" width="900px" :footer="null">
-        <a-space class="mb-4">
-          <a-button type="primary" @click="showAddBundleModal">添加扎号</a-button>
-          <a-button @click="generateAllQRCodes">批量生成二维码</a-button>
-        </a-space>
-        <a-table :columns="bundleColumns" :dataSource="bundles" rowKey="id" size="small">
-          <template #action="{ record }">
-            <a-space>
-              <a-button size="small" @click="showBundleQRCode(record)">二维码</a-button>
-              <a-popconfirm title="删除此扎号?" @confirm="deleteBundle(record.id)">
-                <a-button size="small" danger>删除</a-button>
-              </a-popconfirm>
-            </a-space>
-          </template>
-        </a-table>
-      </a-modal>
-
-      <!-- 添加扎号模态框 -->
-      <a-modal v-model:open="addBundleModalVisible" title="添加扎号" @ok="handleAddBundle" :confirmLoading="addingBundle">
-        <a-form layout="vertical">
-          <a-form-item label="尺码" required>
-            <a-input v-model:value="newBundle.size" placeholder="如:S/M/L/XL" />
-          </a-form-item>
-          <a-form-item label="数量" required>
-            <a-input-number v-model:value="newBundle.quantity" :min="1" :max="10000" style="width: 100%" />
-          </a-form-item>
-        </a-form>
-      </a-modal>
-
-      <!-- 单个扎号二维码展示 -->
-      <a-modal v-model:open="bundleQRVisible" title="扎号二维码" :footer="null" width="400px">
-        <div class="qr-code-container">
-          <img :src="bundleQRUrl" alt="扎号二维码" style="width: 100%" />
-          <p class="qr-code-tip">扎号: {{ currentBundle?.bundle_no }}</p>
-        </div>
-      </a-modal>
-    </div>
-  </SidebarLayout>
+  <div>
+    <a-card>
+      <a-space>
+        <a-input-search v-model:value="searchKeyword" placeholder="搜索批次号" style="width: 300px" @search="loadBatches" />
+        <a-button type="primary" @click="showCreateModal">创建批次</a-button>
+      </a-space>
+    </a-card>
+
+    <a-card class="mt-4" title="裁床批次列表">
+      <a-table :columns="columns" :dataSource="batches" :loading="loading" rowKey="id">
+        <template #status="{ record }">
+          <a-tag :color="getStatusColor(record.status)">{{ getStatusText(record.status) }}</a-tag>
+        </template>
+        <template #action="{ record }">
+          <a-space>
+            <a-button size="small" @click="showBundlesModal(record)">查看扎号</a-button>
+            <a-button size="small" @click="showEditModal(record)">编辑</a-button>
+            <a-popconfirm title="确定删除此批次吗?" @confirm="deleteBatch(record.id)">
+              <a-button size="small" danger>删除</a-button>
+            </a-popconfirm>
+          </a-space>
+        </template>
+      </a-table>
+    </a-card>
+
+    <!-- 创建/编辑批次模态框 -->
+    <a-modal v-model:open="modalVisible" :title="modalTitle" @ok="handleSubmit" :confirmLoading="submitting" width="600px">
+      <a-form ref="formRef" :model="formState" :rules="formRules" layout="vertical">
+        <a-form-item label="工单" name="work_order_id" v-if="!isEdit">
+          <a-select v-model:value="formState.work_order_id" placeholder="请选择工单">
+            <a-select-option v-for="wo in workOrders" :key="wo.id" :value="wo.id">
+              {{ wo.order_no }} - {{ wo.style_name }}
+            </a-select-option>
+          </a-select>
+        </a-form-item>
+        <a-form-item label="批次号" name="batch_no" v-if="!isEdit">
+          <a-input v-model:value="formState.batch_no" placeholder="请输入批次号" />
+        </a-form-item>
+        <a-form-item label="颜色" name="color">
+          <a-input v-model:value="formState.color" placeholder="请输入颜色" />
+        </a-form-item>
+        <a-form-item label="总数量" name="total_qty">
+          <a-input-number v-model:value="formState.total_qty" :min="1" :max="100000" style="width: 100%" />
+        </a-form-item>
+      </a-form>
+    </a-modal>
+
+    <!-- 扎号管理模态框 -->
+    <a-modal v-model:open="bundlesModalVisible" :title="`扎号管理 - ${currentBatch?.batch_no}`" width="900px" :footer="null">
+      <a-space class="mb-4">
+        <a-button type="primary" @click="showAddBundleModal">添加扎号</a-button>
+        <a-button @click="generateAllQRCodes">批量生成二维码</a-button>
+      </a-space>
+      <a-table :columns="bundleColumns" :dataSource="bundles" rowKey="id" size="small">
+        <template #action="{ record }">
+          <a-space>
+            <a-button size="small" @click="showBundleQRCode(record)">二维码</a-button>
+            <a-popconfirm title="删除此扎号?" @confirm="deleteBundle(record.id)">
+              <a-button size="small" danger>删除</a-button>
+            </a-popconfirm>
+          </a-space>
+        </template>
+      </a-table>
+    </a-modal>
+
+    <!-- 添加扎号模态框 -->
+    <a-modal v-model:open="addBundleModalVisible" title="添加扎号" @ok="handleAddBundle" :confirmLoading="addingBundle">
+      <a-form layout="vertical">
+        <a-form-item label="尺码" required>
+          <a-input v-model:value="newBundle.size" placeholder="如:S/M/L/XL" />
+        </a-form-item>
+        <a-form-item label="数量" required>
+          <a-input-number v-model:value="newBundle.quantity" :min="1" :max="10000" style="width: 100%" />
+        </a-form-item>
+      </a-form>
+    </a-modal>
+
+    <!-- 单个扎号二维码展示 -->
+    <a-modal v-model:open="bundleQRVisible" title="扎号二维码" :footer="null" width="400px">
+      <div class="text-center">
+        <img :src="bundleQRUrl" alt="扎号二维码" style="width: 100%" />
+        <p class="mt-4 text-gray-600 text-sm">扎号: {{ currentBundle?.bundle_no }}</p>
+      </div>
+    </a-modal>
+  </div>
 </template>
 
 <script setup lang="ts">
 import { ref, reactive, onMounted } from 'vue';
-import { useRouter } from 'vue-router';
 import { message } from 'ant-design-vue';
-import { useAuthStore } from '@/stores/auth';
-import { SidebarLayout } from '@smartcut/shared-components';
 import { WorkOrderApi, createApiClient } from '@smartcut/api-client';
 import { STORAGE_KEYS } from '@smartcut/shared-utils';
+import { getApiBaseUrl } from '@/apiConfig';
 import type { WorkOrder, CutBatch, CutBundle } from '@smartcut/types';
 
-const router = useRouter();
-const authStore = useAuthStore();
-
-const menuItems = [
-  { key: 'dashboard', label: '主页看板', icon: 'DashboardOutlined', path: '/' },
-  { key: 'work-orders', label: '工单管理', icon: 'FileOutlined', path: '/work-orders' },
-  { key: 'processes', label: '工序管理', icon: 'AppstoreOutlined', path: '/processes' },
-  { key: 'records', label: '计件记录', icon: 'UnorderedListOutlined', path: '/records' },
-  { key: 'salary', label: '工资管理', icon: 'PayCircleOutlined', path: '/salary' },
-  { key: 'users', label: '用户管理', icon: 'UserOutlined', path: '/users' }
-];
-
 const apiClient = createApiClient({
-  baseURL: window.location.origin,
+  baseURL: getApiBaseUrl(),
   tokenKey: STORAGE_KEYS.FACTORY_TOKEN
 });
 
@@ -352,22 +331,8 @@ async function generateAllQRCodes() {
   }
 }
 
-function handleLogout() {
-  authStore.logout();
-  message.success('已退出登录');
-  router.push('/login');
-}
-
 onMounted(() => {
   loadBatches();
   loadWorkOrders();
 });
-</script>
-
-<style scoped lang="postcss">
-.cut-batches-page { padding: 0; }
-.mt-4 { margin-top: 16px; }
-.mb-4 { margin-bottom: 16px; }
-.qr-code-container { text-align: center; }
-.qr-code-tip { margin-top: 16px; color: #666; font-size: 14px; }
-</style>
+</script>

+ 53 - 101
apps/factory-app/src/views/DashboardView.vue

@@ -1,95 +1,66 @@
 <template>
-  <SidebarLayout
-    :menuItems="menuItems"
-    :userName="authStore.user?.name"
-    :activeKey="activeMenuKey"
-    @logout="handleLogout"
-  >
-    <div class="dashboard">
-      <a-row :gutter="16">
-        <a-col :span="6">
-          <a-card>
-            <a-statistic title="总产量" :value="dashboardData.total_output || 0">
-              <template #suffix><span class="unit">件</span></template>
-            </a-statistic>
-          </a-card>
-        </a-col>
-        <a-col :span="6">
-          <a-card>
-            <a-statistic
-              title="今日产量"
-              :value="dashboardData.today_output || 0"
-              :valueStyle="{ color: '#3f8600' }"
-            >
-              <template #suffix><span class="unit">件</span></template>
-            </a-statistic>
-          </a-card>
-        </a-col>
-        <a-col :span="6">
-          <a-card>
-            <a-statistic title="员工数" :value="dashboardData.worker_count || 0">
-              <template #suffix><span class="unit">人</span></template>
-            </a-statistic>
-          </a-card>
-        </a-col>
-        <a-col :span="6">
-          <a-card>
-            <a-statistic title="活跃工单" :value="dashboardData.active_orders || 0">
-              <template #suffix><span class="unit">个</span></template>
-            </a-statistic>
-          </a-card>
-        </a-col>
-      </a-row>
+  <div>
+    <a-row :gutter="16">
+      <a-col :span="6">
+        <a-card>
+          <a-statistic title="总产量" :value="dashboardData.total_output || 0">
+            <template #suffix><span class="text-sm text-gray-500">件</span></template>
+          </a-statistic>
+        </a-card>
+      </a-col>
+      <a-col :span="6">
+        <a-card>
+          <a-statistic
+            title="今日产量"
+            :value="dashboardData.today_output || 0"
+            :valueStyle="{ color: '#3f8600' }"
+          >
+            <template #suffix><span class="text-sm text-gray-500">件</span></template>
+          </a-statistic>
+        </a-card>
+      </a-col>
+      <a-col :span="6">
+        <a-card>
+          <a-statistic title="员工数" :value="dashboardData.worker_count || 0">
+            <template #suffix><span class="text-sm text-gray-500">人</span></template>
+          </a-statistic>
+        </a-card>
+      </a-col>
+      <a-col :span="6">
+        <a-card>
+          <a-statistic title="活跃工单" :value="dashboardData.active_orders || 0">
+            <template #suffix><span class="text-sm text-gray-500">个</span></template>
+          </a-statistic>
+        </a-card>
+      </a-col>
+    </a-row>
 
-      <a-card title="快捷操作" class="mt-4">
-        <a-space>
-          <a-button type="primary" @click="router.push('/work-orders')">
-            工单管理
-          </a-button>
-          <a-button @click="router.push('/records')">
-            计件记录
-          </a-button>
-          <a-button @click="router.push('/salary')">
-            工资管理
-          </a-button>
-        </a-space>
-      </a-card>
-    </div>
-  </SidebarLayout>
+    <a-card title="快捷操作" class="mt-4">
+      <a-space>
+        <a-button type="primary" @click="router.push('/work-orders')">
+          工单管理
+        </a-button>
+        <a-button @click="router.push('/records')">
+          计件记录
+        </a-button>
+        <a-button @click="router.push('/salary')">
+          工资管理
+        </a-button>
+      </a-space>
+    </a-card>
+  </div>
 </template>
 
 <script setup lang="ts">
-import { ref, computed, onMounted } from 'vue';
+import { ref, onMounted } from 'vue';
 import { useRouter } from 'vue-router';
 import { message } from 'ant-design-vue';
-import { useAuthStore } from '@/stores/auth';
-import { SidebarLayout } from '@smartcut/shared-components';
 import { StatsApi, createApiClient } from '@smartcut/api-client';
 import { STORAGE_KEYS } from '@smartcut/shared-utils';
+import { getApiBaseUrl } from '@/apiConfig';
 import type { DashboardData } from '@smartcut/types';
 
 const router = useRouter();
-const authStore = useAuthStore();
-
-const menuItems = [
-  { key: 'dashboard', label: '主页看板', icon: 'DashboardOutlined', path: '/' },
-  { key: 'work-orders', label: '工单管理', icon: 'FileOutlined', path: '/work-orders' },
-  { key: 'processes', label: '工序管理', icon: 'AppstoreOutlined', path: '/processes' },
-  { key: 'records', label: '计件记录', icon: 'UnorderedListOutlined', path: '/records' },
-  { key: 'salary', label: '工资管理', icon: 'PayCircleOutlined', path: '/salary' },
-  { key: 'users', label: '用户管理', icon: 'UserOutlined', path: '/users' }
-];
-
-const activeMenuKey = computed(() => {
-  const path = router.currentRoute.value.path;
-  if (path === '/') return 'dashboard';
-  if (path.startsWith('/work-orders')) return 'work-orders';
-  if (path.startsWith('/processes')) return 'processes';
-  if (path.startsWith('/records')) return 'records';
-  if (path.startsWith('/salary')) return 'salary';
-  if (path.startsWith('/users')) return 'users';
-  return 'dashboard';
-});
 
 const dashboardData = ref<DashboardData>({
   total_output: 0,
@@ -99,7 +70,7 @@ const dashboardData = ref<DashboardData>({
 });
 
 const apiClient = createApiClient({
-  baseURL: window.location.origin,
+  baseURL: getApiBaseUrl(),
   tokenKey: STORAGE_KEYS.FACTORY_TOKEN
 });
 
@@ -112,23 +83,4 @@ onMounted(async () => {
     message.error('加载看板数据失败');
   }
 });
-
-function handleLogout() {
-  authStore.logout();
-  message.success('已退出登录');
-  router.push('/login');
-}
-</script>
-
-<style scoped lang="postcss">
-.dashboard {
-  padding: 0;
-}
-.unit {
-  font-size: 14px;
-  color: #999;
-}
-.mt-4 {
-  margin-top: 16px;
-}
-</style>
+</script>

+ 2 - 1
apps/factory-app/src/views/LoginView.vue

@@ -61,6 +61,7 @@ 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 { getApiBaseUrl } from '@/apiConfig';
 import type { Factory } from '@smartcut/types';
 
 const router = useRouter();
@@ -91,7 +92,7 @@ onMounted(async () => {
     loadingFactories.value = true;
     try {
       const apiClient = createApiClient({
-        baseURL: window.location.origin,
+        baseURL: getApiBaseUrl(),
         tokenKey: STORAGE_KEYS.PLATFORM_TOKEN
       });
       const factoryApi = new FactoryApi(apiClient);

+ 51 - 83
apps/factory-app/src/views/PricesView.vue

@@ -1,87 +1,66 @@
 <template>
-  <SidebarLayout
-    :menuItems="menuItems"
-    :userName="authStore.user?.name"
-    :activeKey="'prices'"
-    @logout="handleLogout"
-  >
-    <div class="prices-page">
-      <a-card>
-        <a-space>
-          <a-select v-model:value="filterWorkOrderId" placeholder="筛选工单" style="width: 250px" allowClear @change="loadPrices">
-            <a-select-option v-for="wo in workOrders" :key="wo.id" :value="wo.id">
-              {{ wo.order_no }} - {{ wo.style_name }}
+  <div>
+    <a-card>
+      <a-space>
+        <a-select v-model:value="filterWorkOrderId" placeholder="筛选工单" style="width: 250px" allowClear @change="loadPrices">
+          <a-select-option v-for="wo in workOrders" :key="wo.id" :value="wo.id">
+            {{ wo.order_no }} - {{ wo.style_name }}
+          </a-select-option>
+        </a-select>
+        <a-button type="primary" @click="showCreateModal">设置工价</a-button>
+      </a-space>
+    </a-card>
+
+    <a-card class="mt-4" title="工价列表">
+      <a-table :columns="columns" :dataSource="prices" :loading="loading" rowKey="id">
+        <template #price="{ record }">
+          ¥{{ record.price.toFixed(2) }}
+        </template>
+        <template #action="{ record }">
+          <a-space>
+            <a-button size="small" @click="showEditModal(record)">编辑</a-button>
+            <a-popconfirm title="删除此工价?" @confirm="deletePrice(record.id)">
+              <a-button size="small" danger>删除</a-button>
+            </a-popconfirm>
+          </a-space>
+        </template>
+      </a-table>
+    </a-card>
+
+    <!-- 创建/编辑工价模态框 -->
+    <a-modal v-model:open="modalVisible" :title="modalTitle" @ok="handleSubmit" :confirmLoading="submitting">
+      <a-form ref="formRef" :model="formState" :rules="formRules" layout="vertical">
+        <a-form-item label="款号" name="style" v-if="!isEdit">
+          <a-input v-model:value="formState.style" placeholder="请输入款号" />
+        </a-form-item>
+        <a-form-item label="工序" name="process_id">
+          <a-select v-model:value="formState.process_id" placeholder="请选择工序">
+            <a-select-option v-for="p in processes" :key="p.id" :value="p.id">
+              {{ p.name }} ({{ p.code }})
             </a-select-option>
           </a-select>
-          <a-button type="primary" @click="showCreateModal">设置工价</a-button>
-        </a-space>
-      </a-card>
-
-      <a-card class="mt-4" title="工价列表">
-        <a-table :columns="columns" :dataSource="prices" :loading="loading" rowKey="id">
-          <template #price="{ record }">
-            ¥{{ record.price.toFixed(2) }}
-          </template>
-          <template #action="{ record }">
-            <a-space>
-              <a-button size="small" @click="showEditModal(record)">编辑</a-button>
-              <a-popconfirm title="删除此工价?" @confirm="deletePrice(record.id)">
-                <a-button size="small" danger>删除</a-button>
-              </a-popconfirm>
-            </a-space>
-          </template>
-        </a-table>
-      </a-card>
-
-      <!-- 创建/编辑工价模态框 -->
-      <a-modal v-model:open="modalVisible" :title="modalTitle" @ok="handleSubmit" :confirmLoading="submitting">
-        <a-form ref="formRef" :model="formState" :rules="formRules" layout="vertical">
-          <a-form-item label="款号" name="style" v-if="!isEdit">
-            <a-input v-model:value="formState.style" placeholder="请输入款号" />
-          </a-form-item>
-          <a-form-item label="工序" name="process_id">
-            <a-select v-model:value="formState.process_id" placeholder="请选择工序">
-              <a-select-option v-for="p in processes" :key="p.id" :value="p.id">
-                {{ p.name }} ({{ p.code }})
-              </a-select-option>
-            </a-select>
-          </a-form-item>
-          <a-form-item label="工价(元)" name="price">
-            <a-input-number v-model:value="formState.price" :min="0" :max="10000" :step="0.1" style="width: 100%" />
-          </a-form-item>
-          <a-form-item label="备注" name="remark">
-            <a-textarea v-model:value="formState.remark" :rows="2" placeholder="可选" />
-          </a-form-item>
-        </a-form>
-      </a-modal>
-    </div>
-  </SidebarLayout>
+        </a-form-item>
+        <a-form-item label="工价(元)" name="price">
+          <a-input-number v-model:value="formState.price" :min="0" :max="10000" :step="0.1" style="width: 100%" />
+        </a-form-item>
+        <a-form-item label="备注" name="remark">
+          <a-textarea v-model:value="formState.remark" :rows="2" placeholder="可选" />
+        </a-form-item>
+      </a-form>
+    </a-modal>
+  </div>
 </template>
 
 <script setup lang="ts">
 import { ref, reactive, onMounted } from 'vue';
-import { useRouter } from 'vue-router';
 import { message } from 'ant-design-vue';
-import { useAuthStore } from '@/stores/auth';
-import { SidebarLayout } from '@smartcut/shared-components';
 import { WorkOrderApi, ProcessApi, createApiClient } from '@smartcut/api-client';
 import { STORAGE_KEYS } from '@smartcut/shared-utils';
+import { getApiBaseUrl } from '@/apiConfig';
 import type { WorkOrder, Process, PriceTemplate } from '@smartcut/types';
 
-const router = useRouter();
-const authStore = useAuthStore();
-
-const menuItems = [
-  { key: 'dashboard', label: '主页看板', icon: 'DashboardOutlined', path: '/' },
-  { key: 'work-orders', label: '工单管理', icon: 'FileOutlined', path: '/work-orders' },
-  { key: 'processes', label: '工序管理', icon: 'AppstoreOutlined', path: '/processes' },
-  { key: 'records', label: '计件记录', icon: 'UnorderedListOutlined', path: '/records' },
-  { key: 'salary', label: '工资管理', icon: 'PayCircleOutlined', path: '/salary' },
-  { key: 'users', label: '用户管理', icon: 'UserOutlined', path: '/users' }
-];
-
 const apiClient = createApiClient({
-  baseURL: window.location.origin,
+  baseURL: getApiBaseUrl(),
   tokenKey: STORAGE_KEYS.FACTORY_TOKEN
 });
 
@@ -215,20 +194,9 @@ async function deletePrice(id: number) {
   }
 }
 
-function handleLogout() {
-  authStore.logout();
-  message.success('已退出登录');
-  router.push('/login');
-}
-
 onMounted(() => {
   loadPrices();
   loadWorkOrders();
   loadProcesses();
 });
-</script>
-
-<style scoped lang="postcss">
-.prices-page { padding: 0; }
-.mt-4 { margin-top: 16px; }
-</style>
+</script>

+ 88 - 124
apps/factory-app/src/views/ProcessesView.vue

@@ -1,124 +1,103 @@
 <template>
-  <SidebarLayout
-    :menuItems="menuItems"
-    :userName="authStore.user?.name"
-    :activeKey="'processes'"
-    @logout="handleLogout"
-  >
-    <div class="processes-page">
-      <a-card>
-        <a-button type="primary" @click="showCreateModal">
-          创建工序
-        </a-button>
-      </a-card>
-
-      <a-card class="mt-4">
-        <a-table
-          :columns="columns"
-          :dataSource="processes"
-          :loading="loading"
-          rowKey="id"
-        >
-          <template #status="{ record }">
-            <a-tag :color="record.status === 1 ? 'green' : 'red'">
-              {{ record.status === 1 ? '启用' : '禁用' }}
-            </a-tag>
-          </template>
-
-          <template #action="{ record }">
-            <a-space>
-              <a-button size="small" @click="showEditModal(record)">
-                编辑
+  <div>
+    <a-card>
+      <a-button type="primary" @click="showCreateModal">
+        创建工序
+      </a-button>
+    </a-card>
+
+    <a-card class="mt-4">
+      <a-table
+        :columns="columns"
+        :dataSource="processes"
+        :loading="loading"
+        rowKey="id"
+      >
+        <template #status="{ record }">
+          <a-tag :color="record.status === 1 ? 'green' : 'red'">
+            {{ record.status === 1 ? '启用' : '禁用' }}
+          </a-tag>
+        </template>
+
+        <template #action="{ record }">
+          <a-space>
+            <a-button size="small" @click="showEditModal(record)">
+              编辑
+            </a-button>
+            <a-popconfirm
+              title="确定删除此工序吗?"
+              ok-text="确定"
+              cancel-text="取消"
+              @confirm="deleteProcess(record.id)"
+            >
+              <a-button size="small" danger>
+                删除
               </a-button>
-              <a-popconfirm
-                title="确定删除此工序吗?"
-                ok-text="确定"
-                cancel-text="取消"
-                @confirm="deleteProcess(record.id)"
-              >
-                <a-button size="small" danger>
-                  删除
-                </a-button>
-              </a-popconfirm>
-            </a-space>
-          </template>
-        </a-table>
-      </a-card>
-
-      <!-- 创建/编辑工序模态框 -->
-      <a-modal
-        v-model:open="modalVisible"
-        :title="modalTitle"
-        @ok="handleSubmit"
-        @cancel="resetForm"
-        :confirmLoading="submitting"
+            </a-popconfirm>
+          </a-space>
+        </template>
+      </a-table>
+    </a-card>
+
+    <!-- 创建/编辑工序模态框 -->
+    <a-modal
+      v-model:open="modalVisible"
+      :title="modalTitle"
+      @ok="handleSubmit"
+      @cancel="resetForm"
+      :confirmLoading="submitting"
+    >
+      <a-form
+        ref="formRef"
+        :model="formState"
+        :rules="formRules"
+        layout="vertical"
       >
-        <a-form
-          ref="formRef"
-          :model="formState"
-          :rules="formRules"
-          layout="vertical"
-        >
-          <a-form-item label="工序名称" name="name">
-            <a-input
-              v-model:value="formState.name"
-              placeholder="请输入工序名称"
-            />
-          </a-form-item>
-
-          <a-form-item label="工序代码" name="code">
-            <a-input
-              v-model:value="formState.code"
-              placeholder="请输入工序代码(字母或数字)"
-            />
-          </a-form-item>
-
-          <a-form-item label="排序" name="sort_order">
-            <a-input-number
-              v-model:value="formState.sort_order"
-              :min="1"
-              :max="1000"
-              placeholder="请输入排序号"
-              style="width: 100%"
-            />
-          </a-form-item>
-
-          <a-form-item label="状态" name="status" v-if="isEdit">
-            <a-radio-group v-model:value="formState.status">
-              <a-radio :value="1">启用</a-radio>
-              <a-radio :value="0">禁用</a-radio>
-            </a-radio-group>
-          </a-form-item>
-        </a-form>
-      </a-modal>
-    </div>
-  </SidebarLayout>
+        <a-form-item label="工序名称" name="name">
+          <a-input
+            v-model:value="formState.name"
+            placeholder="请输入工序名称"
+          />
+        </a-form-item>
+
+        <a-form-item label="工序代码" name="code">
+          <a-input
+            v-model:value="formState.code"
+            placeholder="请输入工序代码(字母或数字)"
+          />
+        </a-form-item>
+
+        <a-form-item label="排序" name="sort_order">
+          <a-input-number
+            v-model:value="formState.sort_order"
+            :min="1"
+            :max="1000"
+            placeholder="请输入排序号"
+            style="width: 100%"
+          />
+        </a-form-item>
+
+        <a-form-item label="状态" name="status" v-if="isEdit">
+          <a-radio-group v-model:value="formState.status">
+            <a-radio :value="1">启用</a-radio>
+            <a-radio :value="0">禁用</a-radio>
+          </a-radio-group>
+        </a-form-item>
+      </a-form>
+    </a-modal>
+  </div>
 </template>
 
 <script setup lang="ts">
 import { ref, reactive, onMounted } from 'vue';
-import { useRouter } from 'vue-router';
 import { message } from 'ant-design-vue';
-import { useAuthStore } from '@/stores/auth';
-import { SidebarLayout } from '@smartcut/shared-components';
 import { ProcessApi, createApiClient } from '@smartcut/api-client';
 import { STORAGE_KEYS } from '@smartcut/shared-utils';
+import { getApiBaseUrl } from '@/apiConfig';
 import type { Process } from '@smartcut/types';
 
-const router = useRouter();
-const authStore = useAuthStore();
-
-const menuItems = [
-  { key: 'dashboard', label: '主页看板', icon: 'DashboardOutlined', path: '/' },
-  { key: 'work-orders', label: '工单管理', icon: 'FileOutlined', path: '/work-orders' },
-  { key: 'processes', label: '工序管理', icon: 'AppstoreOutlined', path: '/processes' },
-  { key: 'records', label: '计件记录', icon: 'UnorderedListOutlined', path: '/records' },
-  { key: 'salary', label: '工资管理', icon: 'PayCircleOutlined', path: '/salary' },
-  { key: 'users', label: '用户管理', icon: 'UserOutlined', path: '/users' }
-];
-
 const apiClient = createApiClient({
-  baseURL: window.location.origin,
+  baseURL: getApiBaseUrl(),
   tokenKey: STORAGE_KEYS.FACTORY_TOKEN
 });
 
@@ -239,22 +218,7 @@ async function deleteProcess(id: number) {
   }
 }
 
-function handleLogout() {
-  authStore.logout();
-  message.success('已退出登录');
-  router.push('/login');
-}
-
 onMounted(() => {
   loadProcesses();
 });
-</script>
-
-<style scoped lang="postcss">
-.processes-page {
-  padding: 0;
-}
-.mt-4 {
-  margin-top: 16px;
-}
-</style>
+</script>

+ 99 - 135
apps/factory-app/src/views/RecordsView.vue

@@ -1,136 +1,115 @@
 <template>
-  <SidebarLayout
-    :menuItems="menuItems"
-    :userName="authStore.user?.name"
-    :activeKey="'records'"
-    @logout="handleLogout"
-  >
-    <div class="records-page">
-      <a-card>
-        <a-space>
-          <a-input-search
-            v-model:value="searchKeyword"
-            placeholder="搜索员工姓名"
-            style="width: 300px"
-            @search="loadRecords"
-          />
-          <a-date-picker
-            v-model:value="filterDate"
-            placeholder="筛选日期"
-            @change="loadRecords"
-          />
-          <a-button type="primary" @click="showScanModal">
-            扫码计件
-          </a-button>
-        </a-space>
-      </a-card>
-
-      <a-card class="mt-4">
-        <a-table
-          :columns="columns"
-          :dataSource="records"
-          :loading="loading"
-          :pagination="pagination"
-          @change="handleTableChange"
-          rowKey="id"
-        >
-          <template #status="{ record }">
-            <a-tag :color="record.status === 'normal' ? 'green' : 'red'">
-              {{ record.status === 'normal' ? '正常' : '已退回' }}
-            </a-tag>
-          </template>
-
-          <template #amount="{ record }">
-            ¥{{ record.amount.toFixed(2) }}
-          </template>
-
-          <template #action="{ record }">
-            <a-space>
-              <a-button
-                size="small"
-                :type="record.status === 'normal' ? 'default' : 'primary'"
-                @click="toggleRecordStatus(record)"
-              >
-                {{ record.status === 'normal' ? '退回' : '撤销退回' }}
-              </a-button>
-            </a-space>
-          </template>
-        </a-table>
-      </a-card>
-
-      <!-- 扫码计件模态框 -->
-      <a-modal
-        v-model:open="scanModalVisible"
-        title="扫码计件"
-        @ok="handleScanSubmit"
-        @cancel="resetScanForm"
-        :confirmLoading="scanning"
-        width="500px"
+  <div>
+    <a-card>
+      <a-space>
+        <a-input-search
+          v-model:value="searchKeyword"
+          placeholder="搜索员工姓名"
+          style="width: 300px"
+          @search="loadRecords"
+        />
+        <a-date-picker
+          v-model:value="filterDate"
+          placeholder="筛选日期"
+          @change="loadRecords"
+        />
+        <a-button type="primary" @click="showScanModal">
+          扫码计件
+        </a-button>
+      </a-space>
+    </a-card>
+
+    <a-card class="mt-4">
+      <a-table
+        :columns="columns"
+        :dataSource="records"
+        :loading="loading"
+        :pagination="pagination"
+        @change="handleTableChange"
+        rowKey="id"
       >
-        <a-form
-          ref="scanFormRef"
-          :model="scanState"
-          :rules="scanRules"
-          layout="vertical"
-        >
-          <a-form-item label="二维码" name="qr_code">
-            <a-input
-              v-model:value="scanState.qr_code"
-              placeholder="请输入或扫描二维码内容"
-            />
-          </a-form-item>
-
-          <a-form-item label="工序" name="process_id">
-            <a-select
-              v-model:value="scanState.process_id"
-              placeholder="请选择工序"
+        <template #status="{ record }">
+          <a-tag :color="record.status === 'normal' ? 'green' : 'red'">
+            {{ record.status === 'normal' ? '正常' : '已退回' }}
+          </a-tag>
+        </template>
+
+        <template #amount="{ record }">
+          ¥{{ record.amount.toFixed(2) }}
+        </template>
+
+        <template #action="{ record }">
+          <a-space>
+            <a-button
+              size="small"
+              :type="record.status === 'normal' ? 'default' : 'primary'"
+              @click="toggleRecordStatus(record)"
             >
-              <a-select-option v-for="p in processes" :key="p.id" :value="p.id">
-                {{ p.name }} ({{ p.code }})
-              </a-select-option>
-            </a-select>
-          </a-form-item>
-
-          <a-form-item label="数量" name="quantity">
-            <a-input-number
-              v-model:value="scanState.quantity"
-              :min="1"
-              :max="1000"
-              placeholder="请输入数量"
-              style="width: 100%"
-            />
-          </a-form-item>
-        </a-form>
-      </a-modal>
-    </div>
-  </SidebarLayout>
+              {{ record.status === 'normal' ? '退回' : '撤销退回' }}
+            </a-button>
+          </a-space>
+        </template>
+      </a-table>
+    </a-card>
+
+    <!-- 扫码计件模态框 -->
+    <a-modal
+      v-model:open="scanModalVisible"
+      title="扫码计件"
+      @ok="handleScanSubmit"
+      @cancel="resetScanForm"
+      :confirmLoading="scanning"
+      width="500px"
+    >
+      <a-form
+        ref="scanFormRef"
+        :model="scanState"
+        :rules="scanRules"
+        layout="vertical"
+      >
+        <a-form-item label="二维码" name="qr_code">
+          <a-input
+            v-model:value="scanState.qr_code"
+            placeholder="请输入或扫描二维码内容"
+          />
+        </a-form-item>
+
+        <a-form-item label="工序" name="process_id">
+          <a-select
+            v-model:value="scanState.process_id"
+            placeholder="请选择工序"
+          >
+            <a-select-option v-for="p in processes" :key="p.id" :value="p.id">
+              {{ p.name }} ({{ p.code }})
+            </a-select-option>
+          </a-select>
+        </a-form-item>
+
+        <a-form-item label="数量" name="quantity">
+          <a-input-number
+            v-model:value="scanState.quantity"
+            :min="1"
+            :max="1000"
+            placeholder="请输入数量"
+            style="width: 100%"
+          />
+        </a-form-item>
+      </a-form>
+    </a-modal>
+  </div>
 </template>
 
 <script setup lang="ts">
 import { ref, reactive, onMounted } from 'vue';
-import { useRouter } from 'vue-router';
 import { message } from 'ant-design-vue';
-import { useAuthStore } from '@/stores/auth';
-import { SidebarLayout } from '@smartcut/shared-components';
 import { RecordApi, ProcessApi, createApiClient } from '@smartcut/api-client';
 import { STORAGE_KEYS, formatDate } from '@smartcut/shared-utils';
+import { getApiBaseUrl } from '@/apiConfig';
 import type { PieceRecord, Process } from '@smartcut/types';
 import dayjs from 'dayjs';
 
-const router = useRouter();
-const authStore = useAuthStore();
-
-const menuItems = [
-  { key: 'dashboard', label: '主页看板', icon: 'DashboardOutlined', path: '/' },
-  { key: 'work-orders', label: '工单管理', icon: 'FileOutlined', path: '/work-orders' },
-  { key: 'processes', label: '工序管理', icon: 'AppstoreOutlined', path: '/processes' },
-  { key: 'records', label: '计件记录', icon: 'UnorderedListOutlined', path: '/records' },
-  { key: 'salary', label: '工资管理', icon: 'PayCircleOutlined', path: '/salary' },
-  { key: 'users', label: '用户管理', icon: 'UserOutlined', path: '/users' }
-];
-
 const apiClient = createApiClient({
-  baseURL: window.location.origin,
+  baseURL: getApiBaseUrl(),
   tokenKey: STORAGE_KEYS.FACTORY_TOKEN
 });
 
@@ -261,23 +240,8 @@ async function toggleRecordStatus(record: PieceRecord) {
   }
 }
 
-function handleLogout() {
-  authStore.logout();
-  message.success('已退出登录');
-  router.push('/login');
-}
-
 onMounted(() => {
   loadRecords();
   loadProcesses();
 });
-</script>
-
-<style scoped lang="postcss">
-.records-page {
-  padding: 0;
-}
-.mt-4 {
-  margin-top: 16px;
-}
-</style>
+</script>

+ 94 - 126
apps/factory-app/src/views/SalaryView.vue

@@ -1,130 +1,109 @@
 <template>
-  <SidebarLayout
-    :menuItems="menuItems"
-    :userName="authStore.user?.name"
-    :activeKey="'salary'"
-    @logout="handleLogout"
-  >
-    <div class="salary-page">
-      <a-card>
-        <a-space>
-          <a-button type="primary" @click="showGenerateModal">
-            生成工资
-          </a-button>
-          <a-button @click="loadSalaryList">刷新</a-button>
-        </a-space>
-      </a-card>
-
-      <a-card class="mt-4" title="工资周期列表">
-        <a-table
-          :columns="columns"
-          :dataSource="salaryPeriods"
-          :loading="loading"
-          rowKey="period"
-        >
-          <template #status="{ record }">
-            <a-tag :color="record.status === 'locked' ? 'red' : 'green'">
-              {{ record.status === 'locked' ? '已锁定' : '待锁定' }}
-            </a-tag>
-          </template>
-
-          <template #action="{ record }">
-            <a-space>
-              <a-button size="small" @click="showDetailModal(record.period)">
-                查看明细
-              </a-button>
-              <a-button
-                v-if="record.status === 'pending'"
-                size="small"
-                type="primary"
-                @click="lockSalary(record.period)"
-              >
-                锁定
-              </a-button>
-              <a-button
-                v-else
-                size="small"
-                @click="unlockSalary(record.period)"
-              >
-                解锁
-              </a-button>
-              <a-popconfirm
-                title="确定删除此工资周期吗?"
-                @confirm="deleteSalary(record.period)"
-              >
-                <a-button size="small" danger>删除</a-button>
-              </a-popconfirm>
-            </a-space>
-          </template>
-        </a-table>
-      </a-card>
-
-      <!-- 生成工资模态框 -->
-      <a-modal
-        v-model:open="generateModalVisible"
-        title="生成工资"
-        @ok="handleGenerate"
-        :confirmLoading="generating"
+  <div>
+    <a-card>
+      <a-space>
+        <a-button type="primary" @click="showGenerateModal">
+          生成工资
+        </a-button>
+        <a-button @click="loadSalaryList">刷新</a-button>
+      </a-space>
+    </a-card>
+
+    <a-card class="mt-4" title="工资周期列表">
+      <a-table
+        :columns="columns"
+        :dataSource="salaryPeriods"
+        :loading="loading"
+        rowKey="period"
       >
-        <a-form layout="vertical">
-          <a-form-item label="工资周期" required>
-            <a-month-picker
-              v-model:value="generatePeriod"
-              placeholder="请选择工资周期"
-              style="width: 100%"
-            />
-          </a-form-item>
-        </a-form>
-      </a-modal>
-
-      <!-- 工资明细模态框 -->
-      <a-modal
-        v-model:open="detailModalVisible"
-        :title="`工资明细 - ${currentPeriod}`"
-        width="800px"
-        :footer="null"
+        <template #status="{ record }">
+          <a-tag :color="record.status === 'locked' ? 'red' : 'green'">
+            {{ record.status === 'locked' ? '已锁定' : '待锁定' }}
+          </a-tag>
+        </template>
+
+        <template #action="{ record }">
+          <a-space>
+            <a-button size="small" @click="showDetailModal(record.period)">
+              查看明细
+            </a-button>
+            <a-button
+              v-if="record.status === 'pending'"
+              size="small"
+              type="primary"
+              @click="lockSalary(record.period)"
+            >
+              锁定
+            </a-button>
+            <a-button
+              v-else
+              size="small"
+              @click="unlockSalary(record.period)"
+            >
+              解锁
+            </a-button>
+            <a-popconfirm
+              title="确定删除此工资周期吗?"
+              @confirm="deleteSalary(record.period)"
+            >
+              <a-button size="small" danger>删除</a-button>
+            </a-popconfirm>
+          </a-space>
+        </template>
+      </a-table>
+    </a-card>
+
+    <!-- 生成工资模态框 -->
+    <a-modal
+      v-model:open="generateModalVisible"
+      title="生成工资"
+      @ok="handleGenerate"
+      :confirmLoading="generating"
+    >
+      <a-form layout="vertical">
+        <a-form-item label="工资周期" required>
+          <a-month-picker
+            v-model:value="generatePeriod"
+            placeholder="请选择工资周期"
+            style="width: 100%"
+          />
+        </a-form-item>
+      </a-form>
+    </a-modal>
+
+    <!-- 工资明细模态框 -->
+    <a-modal
+      v-model:open="detailModalVisible"
+      :title="`工资明细 - ${currentPeriod}`"
+      width="800px"
+      :footer="null"
+    >
+      <a-table
+        :columns="detailColumns"
+        :dataSource="salaryDetails"
+        :loading="detailLoading"
+        rowKey="user_id"
+        :pagination="false"
       >
-        <a-table
-          :columns="detailColumns"
-          :dataSource="salaryDetails"
-          :loading="detailLoading"
-          rowKey="user_id"
-          :pagination="false"
-        >
-          <template #amount="{ record }">
-            ¥{{ record.total_amount.toFixed(2) }}
-          </template>
-        </a-table>
-      </a-modal>
-    </div>
-  </SidebarLayout>
+        <template #amount="{ record }">
+          ¥{{ record.total_amount.toFixed(2) }}
+        </template>
+      </a-table>
+    </a-modal>
+  </div>
 </template>
 
 <script setup lang="ts">
 import { ref, onMounted } from 'vue';
-import { useRouter } from 'vue-router';
 import { message } from 'ant-design-vue';
 import dayjs from 'dayjs';
-import { useAuthStore } from '@/stores/auth';
-import { SidebarLayout } from '@smartcut/shared-components';
 import { SalaryApi, createApiClient } from '@smartcut/api-client';
-import { STORAGE_KEYS, formatMoney } from '@smartcut/shared-utils';
+import { STORAGE_KEYS } from '@smartcut/shared-utils';
+import { getApiBaseUrl } from '@/apiConfig';
 import type { SalaryPeriod, SalaryDetailItem } from '@smartcut/types';
 
-const router = useRouter();
-const authStore = useAuthStore();
-
-const menuItems = [
-  { key: 'dashboard', label: '主页看板', icon: 'DashboardOutlined', path: '/' },
-  { key: 'work-orders', label: '工单管理', icon: 'FileOutlined', path: '/work-orders' },
-  { key: 'processes', label: '工序管理', icon: 'AppstoreOutlined', path: '/processes' },
-  { key: 'records', label: '计件记录', icon: 'UnorderedListOutlined', path: '/records' },
-  { key: 'salary', label: '工资管理', icon: 'PayCircleOutlined', path: '/salary' },
-  { key: 'users', label: '用户管理', icon: 'UserOutlined', path: '/users' }
-];
-
 const apiClient = createApiClient({
-  baseURL: window.location.origin,
+  baseURL: getApiBaseUrl(),
   tokenKey: STORAGE_KEYS.FACTORY_TOKEN
 });
 
@@ -235,18 +214,7 @@ async function deleteSalary(period: string) {
   }
 }
 
-function handleLogout() {
-  authStore.logout();
-  message.success('已退出登录');
-  router.push('/login');
-}
-
 onMounted(() => {
   loadSalaryList();
 });
-</script>
-
-<style scoped lang="postcss">
-.salary-page { padding: 0; }
-.mt-4 { margin-top: 16px; }
-</style>
+</script>

+ 95 - 127
apps/factory-app/src/views/UsersView.vue

@@ -1,132 +1,111 @@
 <template>
-  <SidebarLayout
-    :menuItems="menuItems"
-    :userName="authStore.user?.name"
-    :activeKey="'users'"
-    @logout="handleLogout"
-  >
-    <div class="users-page">
-      <a-card>
-        <a-space>
-          <a-input-search
-            v-model:value="searchKeyword"
-            placeholder="搜索用户名或姓名"
-            style="width: 300px"
-            @search="loadUsers"
-          />
-          <a-button type="primary" @click="showCreateModal">创建用户</a-button>
-        </a-space>
-      </a-card>
+  <div>
+    <a-card>
+      <a-space>
+        <a-input-search
+          v-model:value="searchKeyword"
+          placeholder="搜索用户名或姓名"
+          style="width: 300px"
+          @search="loadUsers"
+        />
+        <a-button type="primary" @click="showCreateModal">创建用户</a-button>
+      </a-space>
+    </a-card>
 
-      <a-card class="mt-4">
-        <a-table
-          :columns="columns"
-          :dataSource="users"
-          :loading="loading"
-          :pagination="pagination"
-          @change="handleTableChange"
-          rowKey="id"
-        >
-          <template #role="{ record }">
-            <a-tag :color="record.role === 'factory_admin' ? 'blue' : 'default'">
-              {{ record.role === 'factory_admin' ? '工厂管理员' : '工人' }}
-            </a-tag>
-          </template>
-          <template #status="{ record }">
-            <a-tag :color="record.status === 1 ? 'green' : 'red'">
-              {{ record.status === 1 ? '正常' : '禁用' }}
-            </a-tag>
-          </template>
-          <template #action="{ record }">
-            <a-space>
-              <a-button size="small" @click="showEditModal(record)">编辑</a-button>
-              <a-button size="small" @click="showResetPasswordModal(record)">重置密码</a-button>
-              <a-popconfirm title="确定删除此用户吗?" @confirm="deleteUser(record.id)">
-                <a-button size="small" danger>删除</a-button>
-              </a-popconfirm>
-            </a-space>
-          </template>
-        </a-table>
-      </a-card>
-
-      <!-- 创建/编辑用户模态框 -->
-      <a-modal
-        v-model:open="modalVisible"
-        :title="modalTitle"
-        @ok="handleSubmit"
-        @cancel="resetForm"
-        :confirmLoading="submitting"
-        width="600px"
+    <a-card class="mt-4">
+      <a-table
+        :columns="columns"
+        :dataSource="users"
+        :loading="loading"
+        :pagination="pagination"
+        @change="handleTableChange"
+        rowKey="id"
       >
-        <a-form ref="formRef" :model="formState" :rules="formRules" layout="vertical">
-          <a-form-item label="用户名" name="username" v-if="!isEdit">
-            <a-input v-model:value="formState.username" placeholder="请输入用户名" />
-          </a-form-item>
-          <a-form-item label="姓名" name="name">
-            <a-input v-model:value="formState.name" placeholder="请输入姓名" />
-          </a-form-item>
-          <a-form-item label="手机号" name="phone">
-            <a-input v-model:value="formState.phone" placeholder="请输入手机号" />
-          </a-form-item>
-          <a-form-item label="角色" name="role" v-if="!isEdit">
-            <a-radio-group v-model:value="formState.role">
-              <a-radio value="factory_admin">工厂管理员</a-radio>
-              <a-radio value="worker">工人</a-radio>
-            </a-radio-group>
-          </a-form-item>
-          <a-form-item label="密码" name="password" v-if="!isEdit">
-            <a-input-password v-model:value="formState.password" placeholder="请输入密码" />
-          </a-form-item>
-          <a-form-item label="状态" name="status" v-if="isEdit">
-            <a-radio-group v-model:value="formState.status">
-              <a-radio :value="1">正常</a-radio>
-              <a-radio :value="0">禁用</a-radio>
-            </a-radio-group>
-          </a-form-item>
-        </a-form>
-      </a-modal>
+        <template #role="{ record }">
+          <a-tag :color="record.role === 'factory_admin' ? 'blue' : 'default'">
+            {{ record.role === 'factory_admin' ? '工厂管理员' : '工人' }}
+          </a-tag>
+        </template>
+        <template #status="{ record }">
+          <a-tag :color="record.status === 1 ? 'green' : 'red'">
+            {{ record.status === 1 ? '正常' : '禁用' }}
+          </a-tag>
+        </template>
+        <template #action="{ record }">
+          <a-space>
+            <a-button size="small" @click="showEditModal(record)">编辑</a-button>
+            <a-button size="small" @click="showResetPasswordModal(record)">重置密码</a-button>
+            <a-popconfirm title="确定删除此用户吗?" @confirm="deleteUser(record.id)">
+              <a-button size="small" danger>删除</a-button>
+            </a-popconfirm>
+          </a-space>
+        </template>
+      </a-table>
+    </a-card>
 
-      <!-- 重置密码模态框 -->
-      <a-modal
-        v-model:open="resetPwdModalVisible"
-        title="重置密码"
-        @ok="handleResetPassword"
-        :confirmLoading="resetting"
-      >
-        <a-form ref="resetFormRef" :model="resetState" :rules="resetRules" layout="vertical">
-          <a-form-item label="新密码" name="password">
-            <a-input-password v-model:value="resetState.password" placeholder="请输入新密码" />
-          </a-form-item>
-        </a-form>
-      </a-modal>
-    </div>
-  </SidebarLayout>
+    <!-- 创建/编辑用户模态框 -->
+    <a-modal
+      v-model:open="modalVisible"
+      :title="modalTitle"
+      @ok="handleSubmit"
+      @cancel="resetForm"
+      :confirmLoading="submitting"
+      width="600px"
+    >
+      <a-form ref="formRef" :model="formState" :rules="formRules" layout="vertical">
+        <a-form-item label="用户名" name="username" v-if="!isEdit">
+          <a-input v-model:value="formState.username" placeholder="请输入用户名" />
+        </a-form-item>
+        <a-form-item label="姓名" name="name">
+          <a-input v-model:value="formState.name" placeholder="请输入姓名" />
+        </a-form-item>
+        <a-form-item label="手机号" name="phone">
+          <a-input v-model:value="formState.phone" placeholder="请输入手机号" />
+        </a-form-item>
+        <a-form-item label="角色" name="role" v-if="!isEdit">
+          <a-radio-group v-model:value="formState.role">
+            <a-radio value="factory_admin">工厂管理员</a-radio>
+            <a-radio value="worker">工人</a-radio>
+          </a-radio-group>
+        </a-form-item>
+        <a-form-item label="密码" name="password" v-if="!isEdit">
+          <a-input-password v-model:value="formState.password" placeholder="请输入密码" />
+        </a-form-item>
+        <a-form-item label="状态" name="status" v-if="isEdit">
+          <a-radio-group v-model:value="formState.status">
+            <a-radio :value="1">正常</a-radio>
+            <a-radio :value="0">禁用</a-radio>
+          </a-radio-group>
+        </a-form-item>
+      </a-form>
+    </a-modal>
+
+    <!-- 重置密码模态框 -->
+    <a-modal
+      v-model:open="resetPwdModalVisible"
+      title="重置密码"
+      @ok="handleResetPassword"
+      :confirmLoading="resetting"
+    >
+      <a-form ref="resetFormRef" :model="resetState" :rules="resetRules" layout="vertical">
+        <a-form-item label="新密码" name="password">
+          <a-input-password v-model:value="resetState.password" placeholder="请输入新密码" />
+        </a-form-item>
+      </a-form>
+    </a-modal>
+  </div>
 </template>
 
 <script setup lang="ts">
 import { ref, reactive, onMounted } from 'vue';
-import { useRouter } from 'vue-router';
 import { message } from 'ant-design-vue';
-import { useAuthStore } from '@/stores/auth';
-import { SidebarLayout } from '@smartcut/shared-components';
 import { UserApi, createApiClient } from '@smartcut/api-client';
 import { STORAGE_KEYS, validatePasswordStrength } from '@smartcut/shared-utils';
+import { getApiBaseUrl } from '@/apiConfig';
 import type { User } from '@smartcut/types';
 
-const router = useRouter();
-const authStore = useAuthStore();
-
-const menuItems = [
-  { key: 'dashboard', label: '主页看板', icon: 'DashboardOutlined', path: '/' },
-  { key: 'work-orders', label: '工单管理', icon: 'FileOutlined', path: '/work-orders' },
-  { key: 'processes', label: '工序管理', icon: 'AppstoreOutlined', path: '/processes' },
-  { key: 'records', label: '计件记录', icon: 'UnorderedListOutlined', path: '/records' },
-  { key: 'salary', label: '工资管理', icon: 'PayCircleOutlined', path: '/salary' },
-  { key: 'users', label: '用户管理', icon: 'UserOutlined', path: '/users' }
-];
-
 const apiClient = createApiClient({
-  baseURL: window.location.origin,
+  baseURL: getApiBaseUrl(),
   tokenKey: STORAGE_KEYS.FACTORY_TOKEN
 });
 
@@ -320,18 +299,7 @@ async function deleteUser(id: number) {
   }
 }
 
-function handleLogout() {
-  authStore.logout();
-  message.success('已退出登录');
-  router.push('/login');
-}
-
 onMounted(() => {
   loadUsers();
 });
-</script>
-
-<style scoped lang="postcss">
-.users-page { padding: 0; }
-.mt-4 { margin-top: 16px; }
-</style>
+</script>

+ 142 - 186
apps/factory-app/src/views/WorkOrdersView.vue

@@ -1,179 +1,158 @@
 <template>
-  <SidebarLayout
-    :menuItems="menuItems"
-    :userName="authStore.user?.name"
-    :activeKey="'work-orders'"
-    @logout="handleLogout"
-  >
-    <div class="work-orders-page">
-      <a-card>
-        <a-space>
-          <a-input-search
-            v-model:value="searchKeyword"
-            placeholder="搜索款号或工单号"
-            style="width: 300px"
-            @search="loadWorkOrders"
-          />
-          <a-select
-            v-model:value="filterStatus"
-            placeholder="状态筛选"
-            style="width: 150px"
-            allowClear
-            @change="loadWorkOrders"
-          >
-            <a-select-option value="pending">待开始</a-select-option>
-            <a-select-option value="in_progress">进行中</a-select-option>
-            <a-select-option value="completed">已完成</a-select-option>
-            <a-select-option value="cancelled">已取消</a-select-option>
-          </a-select>
-          <a-button type="primary" @click="showCreateModal">
-            创建工单
-          </a-button>
-        </a-space>
-      </a-card>
-
-      <a-card class="mt-4">
-        <a-table
-          :columns="columns"
-          :dataSource="workOrders"
-          :loading="loading"
-          :pagination="pagination"
-          @change="handleTableChange"
-          rowKey="id"
+  <div>
+    <a-card>
+      <a-space>
+        <a-input-search
+          v-model:value="searchKeyword"
+          placeholder="搜索款号或工单号"
+          style="width: 300px"
+          @search="loadWorkOrders"
+        />
+        <a-select
+          v-model:value="filterStatus"
+          placeholder="状态筛选"
+          style="width: 150px"
+          allowClear
+          @change="loadWorkOrders"
         >
-          <template #status="{ record }">
-            <a-tag :color="getStatusColor(record.status)">
-              {{ getStatusText(record.status) }}
-            </a-tag>
-          </template>
-
-          <template #progress="{ record }">
-            <a-progress
-              :percent="Math.round((record.completed_qty / record.total_qty) * 100)"
-              :status="record.status === 'completed' ? 'success' : 'active'"
-            />
-          </template>
-
-          <template #action="{ record }">
-            <a-space>
-              <a-button size="small" @click="showEditModal(record)">
-                编辑
-              </a-button>
-              <a-button size="small" type="default" @click="generateQRCode(record.id)">
-                生成二维码
-              </a-button>
-              <a-popconfirm
-                title="确定删除此工单吗?"
-                ok-text="确定"
-                cancel-text="取消"
-                @confirm="deleteWorkOrder(record.id)"
-              >
-                <a-button size="small" danger>
-                  删除
-                </a-button>
-              </a-popconfirm>
-            </a-space>
-          </template>
-        </a-table>
-      </a-card>
-
-      <!-- 创建/编辑工单模态框 -->
-      <a-modal
-        v-model:open="modalVisible"
-        :title="modalTitle"
-        @ok="handleSubmit"
-        @cancel="resetForm"
-        :confirmLoading="submitting"
-        width="600px"
+          <a-select-option value="pending">待开始</a-select-option>
+          <a-select-option value="in_progress">进行中</a-select-option>
+          <a-select-option value="completed">已完成</a-select-option>
+          <a-select-option value="cancelled">已取消</a-select-option>
+        </a-select>
+        <a-button type="primary" @click="showCreateModal">
+          创建工单
+        </a-button>
+      </a-space>
+    </a-card>
+
+    <a-card class="mt-4">
+      <a-table
+        :columns="columns"
+        :dataSource="workOrders"
+        :loading="loading"
+        :pagination="pagination"
+        @change="handleTableChange"
+        rowKey="id"
       >
-        <a-form
-          ref="formRef"
-          :model="formState"
-          :rules="formRules"
-          layout="vertical"
-        >
-          <a-form-item label="工单号" name="order_no" v-if="!isEdit">
-            <a-input
-              v-model:value="formState.order_no"
-              placeholder="请输入工单号"
-            />
-          </a-form-item>
-
-          <a-form-item label="款号" name="style" v-if="!isEdit">
-            <a-input
-              v-model:value="formState.style"
-              placeholder="请输入款号(字母或数字)"
-            />
-          </a-form-item>
-
-          <a-form-item label="款号名称" name="style_name">
-            <a-input
-              v-model:value="formState.style_name"
-              placeholder="请输入款号名称"
-            />
-          </a-form-item>
-
-          <a-form-item label="总数量" name="total_qty">
-            <a-input-number
-              v-model:value="formState.total_qty"
-              :min="1"
-              :max="100000"
-              placeholder="请输入总数量"
-              style="width: 100%"
-            />
-          </a-form-item>
-
-          <a-form-item label="备注" name="remark">
-            <a-textarea
-              v-model:value="formState.remark"
-              placeholder="可选备注信息"
-              :rows="3"
-            />
-          </a-form-item>
-        </a-form>
-      </a-modal>
-
-      <!-- 二维码展示模态框 -->
-      <a-modal
-        v-model:open="qrCodeModalVisible"
-        title="工单二维码"
-        @cancel="qrCodeModalVisible = false"
-        :footer="null"
-        width="400px"
+        <template #status="{ record }">
+          <a-tag :color="getStatusColor(record.status)">
+            {{ getStatusText(record.status) }}
+          </a-tag>
+        </template>
+
+        <template #progress="{ record }">
+          <a-progress
+            :percent="Math.round((record.completed_qty / record.total_qty) * 100)"
+            :status="record.status === 'completed' ? 'success' : 'active'"
+          />
+        </template>
+
+        <template #action="{ record }">
+          <a-space>
+            <a-button size="small" @click="showEditModal(record)">
+              编辑
+            </a-button>
+            <a-button size="small" type="default" @click="generateQRCode(record.id)">
+              生成二维码
+            </a-button>
+            <a-popconfirm
+              title="确定删除此工单吗?"
+              ok-text="确定"
+              cancel-text="取消"
+              @confirm="deleteWorkOrder(record.id)"
+            >
+              <a-button size="small" danger>
+                删除
+              </a-button>
+            </a-popconfirm>
+          </a-space>
+        </template>
+      </a-table>
+    </a-card>
+
+    <!-- 创建/编辑工单模态框 -->
+    <a-modal
+      v-model:open="modalVisible"
+      :title="modalTitle"
+      @ok="handleSubmit"
+      @cancel="resetForm"
+      :confirmLoading="submitting"
+      width="600px"
+    >
+      <a-form
+        ref="formRef"
+        :model="formState"
+        :rules="formRules"
+        layout="vertical"
       >
-        <div class="qr-code-container">
-          <img :src="qrCodeUrl" alt="工单二维码" style="width: 100%" />
-          <p class="qr-code-tip">请使用手机扫描此二维码进行计件</p>
-        </div>
-      </a-modal>
-    </div>
-  </SidebarLayout>
+        <a-form-item label="工单号" name="order_no" v-if="!isEdit">
+          <a-input
+            v-model:value="formState.order_no"
+            placeholder="请输入工单号"
+          />
+        </a-form-item>
+
+        <a-form-item label="款号" name="style" v-if="!isEdit">
+          <a-input
+            v-model:value="formState.style"
+            placeholder="请输入款号(字母或数字)"
+          />
+        </a-form-item>
+
+        <a-form-item label="款号名称" name="style_name">
+          <a-input
+            v-model:value="formState.style_name"
+            placeholder="请输入款号名称"
+          />
+        </a-form-item>
+
+        <a-form-item label="总数量" name="total_qty">
+          <a-input-number
+            v-model:value="formState.total_qty"
+            :min="1"
+            :max="100000"
+            placeholder="请输入总数量"
+            style="width: 100%"
+          />
+        </a-form-item>
+
+        <a-form-item label="备注" name="remark">
+          <a-textarea
+            v-model:value="formState.remark"
+            placeholder="可选备注信息"
+            :rows="3"
+          />
+        </a-form-item>
+      </a-form>
+    </a-modal>
+
+    <!-- 二维码展示模态框 -->
+    <a-modal
+      v-model:open="qrCodeModalVisible"
+      title="工单二维码"
+      @cancel="qrCodeModalVisible = false"
+      :footer="null"
+      width="400px"
+    >
+      <div class="text-center">
+        <img :src="qrCodeUrl" alt="工单二维码" style="width: 100%" />
+        <p class="mt-4 text-gray-600 text-sm">请使用手机扫描此二维码进行计件</p>
+      </div>
+    </a-modal>
+  </div>
 </template>
 
 <script setup lang="ts">
 import { ref, reactive, onMounted } from 'vue';
-import { useRouter } from 'vue-router';
 import { message } from 'ant-design-vue';
-import { useAuthStore } from '@/stores/auth';
-import { SidebarLayout } from '@smartcut/shared-components';
 import { WorkOrderApi, createApiClient } from '@smartcut/api-client';
 import { STORAGE_KEYS } from '@smartcut/shared-utils';
+import { getApiBaseUrl } from '@/apiConfig';
 import type { WorkOrder } from '@smartcut/types';
 
-const router = useRouter();
-const authStore = useAuthStore();
-
-const menuItems = [
-  { key: 'dashboard', label: '主页看板', icon: 'DashboardOutlined', path: '/' },
-  { key: 'work-orders', label: '工单管理', icon: 'FileOutlined', path: '/work-orders' },
-  { key: 'processes', label: '工序管理', icon: 'AppstoreOutlined', path: '/processes' },
-  { key: 'records', label: '计件记录', icon: 'UnorderedListOutlined', path: '/records' },
-  { key: 'salary', label: '工资管理', icon: 'PayCircleOutlined', path: '/salary' },
-  { key: 'users', label: '用户管理', icon: 'UserOutlined', path: '/users' }
-];
-
 const apiClient = createApiClient({
-  baseURL: window.location.origin,
+  baseURL: getApiBaseUrl(),
   tokenKey: STORAGE_KEYS.FACTORY_TOKEN
 });
 
@@ -359,30 +338,7 @@ async function deleteWorkOrder(id: number) {
   }
 }
 
-function handleLogout() {
-  authStore.logout();
-  message.success('已退出登录');
-  router.push('/login');
-}
-
 onMounted(() => {
   loadWorkOrders();
 });
-</script>
-
-<style scoped lang="postcss">
-.work-orders-page {
-  padding: 0;
-}
-.mt-4 {
-  margin-top: 16px;
-}
-.qr-code-container {
-  text-align: center;
-}
-.qr-code-tip {
-  margin-top: 16px;
-  color: #666;
-  font-size: 14px;
-}
-</style>
+</script>

+ 9 - 0
apps/factory-app/src/vite-env.d.ts

@@ -0,0 +1,9 @@
+/// <reference types="vite/client" />
+
+interface ImportMetaEnv {
+  readonly VITE_API_BASE_URL: string
+}
+
+interface ImportMeta {
+  readonly env: ImportMetaEnv
+}

+ 1 - 1
apps/factory-app/vite.config.ts

@@ -17,7 +17,7 @@ export default defineConfig({
     port: 3001,
     proxy: {
       '/api': {
-        target: 'http://localhost:8080',
+        target: 'https://smartcut.51zj.cc/api/v1',
         changeOrigin: true
       }
     }

+ 3 - 0
apps/platform-app/index.html

@@ -4,6 +4,9 @@
     <meta charset="UTF-8" />
     <link rel="icon" type="image/svg+xml" href="/vite.svg" />
     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
+    <meta http-equiv="Pragma" content="no-cache" />
+    <meta http-equiv="Expires" content="0" />
     <title>智裁云 - 系统管理后台</title>
   </head>
   <body>

+ 1 - 1
apps/platform-app/package.json

@@ -5,7 +5,7 @@
   "type": "module",
   "scripts": {
     "dev": "vite",
-    "build": "vite build",
+    "build": "vue-tsc --noEmit && vite build",
     "type-check": "vue-tsc --noEmit",
     "preview": "vite preview",
     "lint": "eslint . --fix",

+ 45 - 1
apps/platform-app/src/App.vue

@@ -1,11 +1,55 @@
 <template>
   <a-config-provider :locale="zhCN">
-    <router-view />
+    <SidebarLayout
+      v-if="showLayout"
+      :userName="authStore.user?.name"
+      :currentPath="route.path"
+      @logout="handleLogout"
+    >
+      <router-view />
+    </SidebarLayout>
+    <router-view v-else />
   </a-config-provider>
 </template>
 
 <script setup lang="ts">
+import { computed, onMounted } from 'vue';
+import { useRouter, useRoute } from 'vue-router';
+import { message } from 'ant-design-vue';
 import zhCN from 'ant-design-vue/es/locale/zh_CN';
+import { DashboardOutlined, ShopOutlined, UserOutlined } from '@ant-design/icons-vue';
+import { SidebarLayout } from '@smartcut/shared-components';
+import { useAuthStore } from '@/stores/auth';
+import { useLayoutStore } from '@/stores/layout';
+
+const router = useRouter();
+const route = useRoute();
+const authStore = useAuthStore();
+const layoutStore = useLayoutStore();
+
+// 初始化布局配置
+onMounted(() => {
+  layoutStore.init({
+    menuItems: [
+      { key: 'dashboard', label: '主页看板', icon: DashboardOutlined, path: '/' },
+      { key: 'factories', label: '工厂管理', icon: ShopOutlined, path: '/factories' },
+      { key: 'users', label: '系统用户', icon: UserOutlined, path: '/users' }
+    ],
+    theme: 'dark'
+  });
+});
+
+// 根据路由 meta 判断是否显示布局
+const showLayout = computed(() => {
+  return route.meta.showLayout !== false;
+});
+
+// 登出处理
+function handleLogout() {
+  authStore.logout();
+  message.success('已退出登录');
+  router.push('/login');
+}
 </script>
 
 <style>

+ 9 - 0
apps/platform-app/src/apiConfig.ts

@@ -0,0 +1,9 @@
+// API配置
+
+/**
+ * 获取API基础地址
+ * 前后端分离架构,返回独立的后端API地址
+ */
+export function getApiBaseUrl(): string {
+  return import.meta.env.VITE_API_BASE_URL;
+}

+ 7 - 7
apps/platform-app/src/router/index.ts

@@ -8,37 +8,37 @@ const routes = [
     path: '/login',
     name: 'Login',
     component: () => import('@/views/LoginView.vue'),
-    meta: { requiresAuth: false }
+    meta: { requiresAuth: false, showLayout: false }
   },
   {
     path: '/',
     name: 'Dashboard',
     component: () => import('@/views/DashboardView.vue'),
-    meta: { requiresAuth: true }
+    meta: { requiresAuth: true, showLayout: true }
   },
   {
     path: '/factories',
     name: 'Factories',
     component: () => import('@/views/FactoriesView.vue'),
-    meta: { requiresAuth: true }
+    meta: { requiresAuth: true, showLayout: true }
   },
   {
     path: '/users',
     name: 'Users',
     component: () => import('@/views/UsersView.vue'),
-    meta: { requiresAuth: true }
+    meta: { requiresAuth: true, showLayout: true }
   },
   {
     path: '/change-password',
     name: 'ChangePassword',
     component: () => import('@/views/ChangePasswordView.vue'),
-    meta: { requiresAuth: true }
+    meta: { requiresAuth: true, showLayout: true }
   },
   {
     path: '/:pathMatch(.*)*',
     name: 'NotFound',
     component: () => import('@/views/NotFoundView.vue'),
-    meta: { requiresAuth: false }
+    meta: { requiresAuth: false, showLayout: false }
   }
 ];
 
@@ -48,7 +48,7 @@ const router = createRouter({
 });
 
 // 路由守卫:认证检查
-router.beforeEach((to, from, next) => {
+router.beforeEach((to, _from, next) => {
   const authStore = useAuthStore();
 
   // 需要认证的路由

+ 2 - 1
apps/platform-app/src/stores/auth.ts

@@ -5,6 +5,7 @@ import { ref, computed } from 'vue';
 import type { SystemUser } from '@smartcut/types';
 import { AuthApi, createApiClient } from '@smartcut/api-client';
 import { STORAGE_KEYS, setSessionStorage, getSessionStorage, removeSessionStorage } from '@smartcut/shared-utils';
+import { getApiBaseUrl } from '@/apiConfig';
 
 export const useAuthStore = defineStore('auth', () => {
   // 状态定义
@@ -14,7 +15,7 @@ export const useAuthStore = defineStore('auth', () => {
 
   // 创建API客户端
   const apiClient = createApiClient({
-    baseURL: window.location.origin,
+    baseURL: getApiBaseUrl(),
     tokenKey: STORAGE_KEYS.PLATFORM_TOKEN
   });
 

+ 33 - 0
apps/platform-app/src/stores/layout.ts

@@ -0,0 +1,33 @@
+import { defineStore } from 'pinia';
+import type { Component } from 'vue';
+
+export interface MenuItem {
+  key: string;
+  label: string;
+  icon: Component; // 图标组件引用
+  path: string;
+}
+
+export interface LayoutConfig {
+  menuItems: MenuItem[];
+  theme?: 'light' | 'dark';
+}
+
+export const useLayoutStore = defineStore('layout', {
+  state: () => ({
+    menuItems: [] as MenuItem[],
+    theme: 'dark' as 'light' | 'dark',
+    collapsed: false
+  }),
+
+  actions: {
+    init(config: LayoutConfig) {
+      this.menuItems = config.menuItems;
+      if (config.theme) this.theme = config.theme;
+    },
+
+    toggleCollapsed() {
+      this.collapsed = !this.collapsed;
+    }
+  }
+});

+ 1 - 17
apps/platform-app/src/styles/main.css

@@ -2,14 +2,13 @@
 @tailwind components;
 @tailwind utilities;
 
-/* 自定义主题色 */
+/* Ant Design 主题定制 */
 :root {
   --ant-primary-color: #1e3a5f;
   --ant-primary-color-hover: #2a4a6f;
   --ant-primary-color-active: #15304f;
 }
 
-/* 全局样式调整 */
 .ant-btn-primary {
   background-color: #1e3a5f;
 }
@@ -20,19 +19,4 @@
 
 .ant-btn-primary:active {
   background-color: #15304f;
-}
-
-/* 侧边栏样式 */
-.ant-layout-sider {
-  background-color: #001529 !important;
-}
-
-/* 卡片样式 */
-.ant-card {
-  border-radius: 8px;
-}
-
-/* 表格样式 */
-.ant-table-wrapper {
-  border-radius: 8px;
 }

+ 6 - 55
apps/platform-app/src/views/ChangePasswordView.vue

@@ -1,11 +1,5 @@
 <template>
-  <SidebarLayout
-    :menuItems="menuItems"
-    :userName="authStore.user?.name"
-    :activeKey="'change-password'"
-    @logout="handleLogout"
-  >
-    <div class="change-password-page">
+  <div>
       <a-card title="修改密码">
         <a-form
           ref="formRef"
@@ -28,7 +22,7 @@
               placeholder="请输入新密码(至少8位,包含大小写字母、数字、特殊字符中3类)"
               size="large"
             />
-            <div class="password-tips">
+            <div class="text-xs text-gray-500 mt-2">
               密码强度要求:至少8位,包含大小写字母、数字、特殊字符中3类
             </div>
           </a-form-item>
@@ -58,8 +52,7 @@
           </a-form-item>
         </a-form>
       </a-card>
-    </div>
-  </SidebarLayout>
+  </div>
 </template>
 
 <script setup lang="ts">
@@ -67,34 +60,11 @@ import { ref, reactive } from 'vue';
 import { useRouter } from 'vue-router';
 import { message } from 'ant-design-vue';
 import { useAuthStore } from '@/stores/auth';
-import { SidebarLayout } from '@smartcut/shared-components';
 import { validatePasswordStrength } from '@smartcut/shared-utils';
 
 const router = useRouter();
 const authStore = useAuthStore();
 
-// 菜单项
-const menuItems = [
-  {
-    key: 'dashboard',
-    label: '主页看板',
-    icon: 'DashboardOutlined',
-    path: '/'
-  },
-  {
-    key: 'factories',
-    label: '工厂管理',
-    icon: 'ShopOutlined',
-    path: '/factories'
-  },
-  {
-    key: 'users',
-    label: '系统用户',
-    icon: 'UserOutlined',
-    path: '/users'
-  }
-];
-
 const submitting = ref(false);
 const formRef = ref();
 
@@ -111,7 +81,7 @@ const formRules = {
   new_password: [
     { required: true, message: '请输入新密码', trigger: 'blur' },
     {
-      validator: (rule: any, value: string) => {
+      validator: (_rule: any, value: string) => {
         if (!validatePasswordStrength(value)) {
           return Promise.reject('密码强度不足');
         }
@@ -123,7 +93,7 @@ const formRules = {
   confirm_password: [
     { required: true, message: '请确认新密码', trigger: 'blur' },
     {
-      validator: (rule: any, value: string) => {
+      validator: (_rule: any, value: string) => {
         if (value !== formState.new_password) {
           return Promise.reject('两次输入的密码不一致');
         }
@@ -164,23 +134,4 @@ function resetForm() {
   formState.new_password = '';
   formState.confirm_password = '';
 }
-
-// 登出处理
-function handleLogout() {
-  authStore.logout();
-  message.success('已退出登录');
-  router.push('/login');
-}
-</script>
-
-<style scoped lang="postcss">
-.change-password-page {
-  padding: 0;
-}
-
-.password-tips {
-  font-size: 12px;
-  color: #999;
-  margin-top: 8px;
-}
-</style>
+</script>

+ 65 - 145
apps/platform-app/src/views/DashboardView.vue

@@ -1,126 +1,74 @@
 <template>
-  <SidebarLayout
-    :menuItems="menuItems"
-    :userName="authStore.user?.name"
-    :activeKey="activeMenuKey"
-    @logout="handleLogout"
-  >
-    <!-- 主页看板内容 -->
-    <div class="dashboard">
-      <a-row :gutter="16">
-        <!-- 统计卡片 -->
-        <a-col :span="6">
-          <a-card>
-            <a-statistic
-              title="工厂总数"
-              :value="dashboardData.factory_count || 0"
-            >
-              <template #suffix>
-                <span class="unit">家</span>
-              </template>
-            </a-statistic>
-          </a-card>
-        </a-col>
-
-        <a-col :span="6">
-          <a-card>
-            <a-statistic
-              title="启用工厂"
-              :value="dashboardData.enabled_factory_count || 0"
-              :valueStyle="{ color: '#3f8600' }"
-            >
-              <template #suffix>
-                <span class="unit">家</span>
-              </template>
-            </a-statistic>
-          </a-card>
-        </a-col>
-
-        <a-col :span="6">
-          <a-card>
-            <a-statistic
-              title="系统用户"
-              :value="dashboardData.user_count || 0"
-            >
-              <template #suffix>
-                <span class="unit">人</span>
-              </template>
-            </a-statistic>
-          </a-card>
-        </a-col>
-
-        <a-col :span="6">
-          <a-card>
-            <a-statistic
-              title="审计日志"
-              :value="dashboardData.log_count || 0"
-            >
-              <template #suffix>
-                <span class="unit">条</span>
-              </template>
-            </a-statistic>
-          </a-card>
-        </a-col>
-      </a-row>
-
-      <!-- 快捷操作 -->
-      <a-card title="快捷操作" class="mt-4">
-        <a-space>
-          <a-button type="primary" @click="router.push('/factories')">
-            管理工厂
-          </a-button>
-          <a-button @click="router.push('/users')">
-            系统用户
-          </a-button>
-        </a-space>
-      </a-card>
-    </div>
-  </SidebarLayout>
+  <div class="p-6">
+    <a-row :gutter="16">
+      <a-col :span="6">
+        <a-card>
+          <a-statistic title="工厂总数" :value="dashboardData.factory_count || 0">
+            <template #suffix>
+              <span class="text-sm text-gray-500">家</span>
+            </template>
+          </a-statistic>
+        </a-card>
+      </a-col>
+
+      <a-col :span="6">
+        <a-card>
+          <a-statistic
+            title="启用工厂"
+            :value="dashboardData.enabled_factory_count || 0"
+            :valueStyle="{ color: '#3f8600' }"
+          >
+            <template #suffix>
+              <span class="text-sm text-gray-500">家</span>
+            </template>
+          </a-statistic>
+        </a-card>
+      </a-col>
+
+      <a-col :span="6">
+        <a-card>
+          <a-statistic title="系统用户" :value="dashboardData.user_count || 0">
+            <template #suffix>
+              <span class="text-sm text-gray-500">人</span>
+            </template>
+          </a-statistic>
+        </a-card>
+      </a-col>
+
+      <a-col :span="6">
+        <a-card>
+          <a-statistic title="审计日志" :value="dashboardData.log_count || 0">
+            <template #suffix>
+              <span class="text-sm text-gray-500">条</span>
+            </template>
+          </a-statistic>
+        </a-card>
+      </a-col>
+    </a-row>
+
+    <a-card title="快捷操作" class="mt-4">
+      <a-space>
+        <a-button type="primary" @click="router.push('/factories')">
+          管理工厂
+        </a-button>
+        <a-button @click="router.push('/users')">
+          系统用户
+        </a-button>
+      </a-space>
+    </a-card>
+  </div>
 </template>
 
 <script setup lang="ts">
-import { ref, computed, onMounted } from 'vue';
+import { ref, onMounted } from 'vue';
 import { useRouter } from 'vue-router';
 import { message } from 'ant-design-vue';
-import { useAuthStore } from '@/stores/auth';
-import { SidebarLayout } from '@smartcut/shared-components';
-import { FactoryApi, UserApi, createApiClient } from '@smartcut/api-client';
+import { FactoryApi, createApiClient } from '@smartcut/api-client';
 import { STORAGE_KEYS } from '@smartcut/shared-utils';
+import { getApiBaseUrl } from '@/apiConfig';
 
 const router = useRouter();
-const authStore = useAuthStore();
 
-// 菜单项
-const menuItems = [
-  {
-    key: 'dashboard',
-    label: '主页看板',
-    icon: 'DashboardOutlined',
-    path: '/'
-  },
-  {
-    key: 'factories',
-    label: '工厂管理',
-    icon: 'ShopOutlined',
-    path: '/factories'
-  },
-  {
-    key: 'users',
-    label: '系统用户',
-    icon: 'UserOutlined',
-    path: '/users'
-  }
-];
-
-const activeMenuKey = computed(() => {
-  const path = router.currentRoute.value.path;
-  if (path === '/') return 'dashboard';
-  if (path.startsWith('/factories')) return 'factories';
-  if (path.startsWith('/users')) return 'users';
-  return 'dashboard';
-});
-
-// 看板数据
 const dashboardData = ref({
   factory_count: 0,
   enabled_factory_count: 0,
@@ -128,50 +76,22 @@ const dashboardData = ref({
   log_count: 0
 });
 
-// API客户端
 const apiClient = createApiClient({
-  baseURL: window.location.origin,
+  baseURL: getApiBaseUrl(),
   tokenKey: STORAGE_KEYS.PLATFORM_TOKEN
 });
 
 const factoryApi = new FactoryApi(apiClient);
-const userApi = new UserApi(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; // 暂时硬编码
+    dashboardData.value.user_count = 5;
+    dashboardData.value.log_count = 100;
   } catch (error) {
     message.error('加载看板数据失败');
   }
 });
-
-// 登出处理
-function handleLogout() {
-  authStore.logout();
-  message.success('已退出登录');
-  router.push('/login');
-}
-</script>
-
-<style scoped lang="postcss">
-.dashboard {
-  padding: 0;
-}
-
-.unit {
-  font-size: 14px;
-  color: #999;
-}
-
-.mt-4 {
-  margin-top: 16px;
-}
-</style>
+</script>

+ 9 - 60
apps/platform-app/src/views/FactoriesView.vue

@@ -1,11 +1,5 @@
 <template>
-  <SidebarLayout
-    :menuItems="menuItems"
-    :userName="authStore.user?.name"
-    :activeKey="'factories'"
-    @logout="handleLogout"
-  >
-    <div class="factories-page">
+  <div>
       <!-- 头部工具栏 -->
       <a-card>
         <a-space>
@@ -102,48 +96,20 @@
           </a-form-item>
         </a-form>
       </a-modal>
-    </div>
-  </SidebarLayout>
+  </div>
 </template>
 
 <script setup lang="ts">
 import { ref, reactive, onMounted } from 'vue';
-import { useRouter } from 'vue-router';
 import { message } from 'ant-design-vue';
-import { useAuthStore } from '@/stores/auth';
-import { SidebarLayout } from '@smartcut/shared-components';
 import { FactoryApi, createApiClient } from '@smartcut/api-client';
 import { STORAGE_KEYS } from '@smartcut/shared-utils';
+import { getApiBaseUrl } from '@/apiConfig';
 import type { Factory } from '@smartcut/types';
 
-const router = useRouter();
-const authStore = useAuthStore();
-
-// 菜单项
-const menuItems = [
-  {
-    key: 'dashboard',
-    label: '主页看板',
-    icon: 'DashboardOutlined',
-    path: '/'
-  },
-  {
-    key: 'factories',
-    label: '工厂管理',
-    icon: 'ShopOutlined',
-    path: '/factories'
-  },
-  {
-    key: 'users',
-    label: '系统用户',
-    icon: 'UserOutlined',
-    path: '/users'
-  }
-];
-
 // API客户端
 const apiClient = createApiClient({
-  baseURL: window.location.origin,
+  baseURL: getApiBaseUrl(),
   tokenKey: STORAGE_KEYS.PLATFORM_TOKEN
 });
 
@@ -228,7 +194,7 @@ async function loadFactories() {
     const result = await factoryApi.listFactories();
     factories.value = result.list;
     pagination.total = result.total;
-  } catch (error) {
+  } catch (error: any) {
     message.error('加载工厂列表失败');
   } finally {
     loading.value = false;
@@ -286,7 +252,7 @@ async function handleSubmit() {
 
     modalVisible.value = false;
     loadFactories();
-  } catch (error) {
+  } catch (error: any) {
     if (error.response?.data?.msg) {
       message.error(error.response.data.msg);
     } else {
@@ -318,7 +284,7 @@ async function toggleFactoryStatus(record: Factory) {
       message.success('工厂已启用');
     }
     loadFactories();
-  } catch (error) {
+  } catch (error: any) {
     message.error('操作失败');
   }
 }
@@ -329,30 +295,13 @@ async function deleteFactory(id: string) {
     await factoryApi.deleteFactory(id, true);
     message.success('工厂已永久删除');
     loadFactories();
-  } catch (error) {
+  } catch (error: any) {
     message.error('删除工厂失败');
   }
 }
 
-// 登出处理
-function handleLogout() {
-  authStore.logout();
-  message.success('已退出登录');
-  router.push('/login');
-}
-
 // 初始化加载
 onMounted(() => {
   loadFactories();
 });
-</script>
-
-<style scoped lang="postcss">
-.factories-page {
-  padding: 0;
-}
-
-.mt-4 {
-  margin-top: 16px;
-}
-</style>
+</script>

+ 15 - 71
apps/platform-app/src/views/UsersView.vue

@@ -1,11 +1,5 @@
 <template>
-  <SidebarLayout
-    :menuItems="menuItems"
-    :userName="authStore.user?.name"
-    :activeKey="'users'"
-    @logout="handleLogout"
-  >
-    <div class="users-page">
+  <div>
       <!-- 头部工具栏 -->
       <a-card>
         <a-space>
@@ -110,7 +104,7 @@
               v-model:value="formState.password"
               placeholder="请输入密码(至少8位,包含大小写字母、数字、特殊字符中3类)"
             />
-            <div class="password-tips">
+            <div class="text-xs text-gray-500 mt-2">
               密码强度要求:至少8位,包含大小写字母、数字、特殊字符中3类
             </div>
           </a-form-item>
@@ -143,54 +137,26 @@
               v-model:value="resetPasswordState.password"
               placeholder="请输入新密码"
             />
-            <div class="password-tips">
+            <div class="text-xs text-gray-500 mt-2">
               密码强度要求:至少8位,包含大小写字母、数字、特殊字符中3类
             </div>
           </a-form-item>
         </a-form>
       </a-modal>
-    </div>
-  </SidebarLayout>
+  </div>
 </template>
 
 <script setup lang="ts">
 import { ref, reactive, onMounted } from 'vue';
-import { useRouter } from 'vue-router';
 import { message } from 'ant-design-vue';
-import { useAuthStore } from '@/stores/auth';
-import { SidebarLayout } from '@smartcut/shared-components';
 import { UserApi, createApiClient } from '@smartcut/api-client';
 import { STORAGE_KEYS, validatePasswordStrength } from '@smartcut/shared-utils';
+import { getApiBaseUrl } from '@/apiConfig';
 import type { SystemUser } from '@smartcut/types';
 
-const router = useRouter();
-const authStore = useAuthStore();
-
-// 菜单项
-const menuItems = [
-  {
-    key: 'dashboard',
-    label: '主页看板',
-    icon: 'DashboardOutlined',
-    path: '/'
-  },
-  {
-    key: 'factories',
-    label: '工厂管理',
-    icon: 'ShopOutlined',
-    path: '/factories'
-  },
-  {
-    key: 'users',
-    label: '系统用户',
-    icon: 'UserOutlined',
-    path: '/users'
-  }
-];
-
 // API客户端
 const apiClient = createApiClient({
-  baseURL: window.location.origin,
+  baseURL: getApiBaseUrl(),
   tokenKey: STORAGE_KEYS.PLATFORM_TOKEN
 });
 
@@ -295,7 +261,7 @@ const formRules = {
   password: [
     { required: true, message: '请输入密码', trigger: 'blur' },
     {
-      validator: (rule: any, value: string) => {
+      validator: (_rule: any, value: string) => {
         if (!validatePasswordStrength(value)) {
           return Promise.reject('密码强度不足');
         }
@@ -320,7 +286,7 @@ const resetPasswordRules = {
   password: [
     { required: true, message: '请输入新密码', trigger: 'blur' },
     {
-      validator: (rule: any, value: string) => {
+      validator: (_rule: any, value: string) => {
         if (!validatePasswordStrength(value)) {
           return Promise.reject('密码强度不足');
         }
@@ -342,7 +308,7 @@ async function loadUsers() {
     });
     users.value = result.list;
     pagination.total = result.total;
-  } catch (error) {
+  } catch (error: any) {
     message.error('加载用户列表失败');
   } finally {
     loading.value = false;
@@ -398,14 +364,15 @@ async function handleSubmit() {
         username: formState.username,
         name: formState.name,
         phone: formState.phone,
-        password: formState.password
+        password: formState.password,
+        role: 'platform_admin'
       });
       message.success('用户已创建');
     }
 
     modalVisible.value = false;
     loadUsers();
-  } catch (error) {
+  } catch (error: any) {
     if (error.response?.data?.msg) {
       message.error(error.response.data.msg);
     } else {
@@ -447,7 +414,7 @@ async function handleResetPassword() {
     message.success('密码已重置');
     resetPasswordModalVisible.value = false;
     loadUsers();
-  } catch (error) {
+  } catch (error: any) {
     message.error('重置密码失败');
   } finally {
     resettingPassword.value = false;
@@ -467,36 +434,13 @@ async function deleteUser(id: number) {
     await userApi.deleteUser(id);
     message.success('用户已删除');
     loadUsers();
-  } catch (error) {
+  } catch (error: any) {
     message.error('删除用户失败');
   }
 }
 
-// 登出处理
-function handleLogout() {
-  authStore.logout();
-  message.success('已退出登录');
-  router.push('/login');
-}
-
 // 初始化加载
 onMounted(() => {
   loadUsers();
 });
-</script>
-
-<style scoped lang="postcss">
-.users-page {
-  padding: 0;
-}
-
-.mt-4 {
-  margin-top: 16px;
-}
-
-.password-tips {
-  font-size: 12px;
-  color: #999;
-  margin-top: 8px;
-}
-</style>
+</script>

+ 9 - 0
apps/platform-app/src/vite-env.d.ts

@@ -0,0 +1,9 @@
+/// <reference types="vite/client" />
+
+interface ImportMetaEnv {
+  readonly VITE_API_BASE_URL: string
+}
+
+interface ImportMeta {
+  readonly env: ImportMetaEnv
+}

+ 1 - 1
apps/platform-app/vite.config.ts

@@ -18,7 +18,7 @@ export default defineConfig({
     port: 3000,
     proxy: {
       '/api': {
-        target: 'http://localhost:8080',
+        target: 'https://smartcut.51zj.cc',
         changeOrigin: true
       }
     }

+ 3 - 0
apps/worker-app/index.html

@@ -3,6 +3,9 @@
   <head>
     <meta charset="UTF-8" />
     <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
+    <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
+    <meta http-equiv="Pragma" content="no-cache" />
+    <meta http-equiv="Expires" content="0" />
     <title>智裁云 - 工人端</title>
   </head>
   <body>

+ 1 - 1
apps/worker-app/package.json

@@ -5,7 +5,7 @@
   "type": "module",
   "scripts": {
     "dev": "vite",
-    "build": "vite build",
+    "build": "vue-tsc --noEmit && vite build",
     "type-check": "vue-tsc --noEmit",
     "preview": "vite preview",
     "lint": "eslint . --fix",

+ 9 - 0
apps/worker-app/src/apiConfig.ts

@@ -0,0 +1,9 @@
+// API配置
+
+/**
+ * 获取API基础地址
+ * 前后端分离架构,返回独立的后端API地址
+ */
+export function getApiBaseUrl(): string {
+  return import.meta.env.VITE_API_BASE_URL;
+}

+ 1 - 1
apps/worker-app/src/router/index.ts

@@ -65,7 +65,7 @@ const router = createRouter({
   routes
 });
 
-router.beforeEach((to, from, next) => {
+router.beforeEach((to, _from, next) => {
   const authStore = useAuthStore();
 
   if (to.meta.requiresAuth) {

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

@@ -10,6 +10,7 @@ import {
   getSessionStorage,
   removeSessionStorage
 } from '@smartcut/shared-utils';
+import { getApiBaseUrl } from '@/apiConfig';
 
 export const useAuthStore = defineStore('auth', () => {
   const token = ref<string | null>(getSessionStorage<string>(STORAGE_KEYS.WORKER_TOKEN));
@@ -20,7 +21,7 @@ export const useAuthStore = defineStore('auth', () => {
   const isLoggedIn = computed(() => !!token.value && !!user.value && !!currentFactoryId.value);
 
   const apiClient = createApiClient({
-    baseURL: window.location.origin,
+    baseURL: getApiBaseUrl(),
     tokenKey: STORAGE_KEYS.WORKER_TOKEN
   });
 

+ 2 - 2
apps/worker-app/src/views/HomeView.vue

@@ -87,11 +87,11 @@
 <script setup lang="ts">
 import { ref, onMounted } from 'vue';
 import { useRouter } from 'vue-router';
-import { message } from 'ant-design-vue';
 import { useAuthStore } from '@/stores/auth';
 import WorkerLayout from '@/components/WorkerLayout.vue';
 import { StatsApi, RecordApi, createApiClient } from '@smartcut/api-client';
 import { STORAGE_KEYS, formatDate } from '@smartcut/shared-utils';
+import { getApiBaseUrl } from '@/apiConfig';
 import type { WorkerDashboardData, PieceRecord } from '@smartcut/types';
 
 const router = useRouter();
@@ -109,7 +109,7 @@ const dashboardData = ref<WorkerDashboardData>({
 const recentRecords = ref<PieceRecord[]>([]);
 
 const apiClient = createApiClient({
-  baseURL: window.location.origin,
+  baseURL: getApiBaseUrl(),
   tokenKey: STORAGE_KEYS.WORKER_TOKEN
 });
 

+ 2 - 1
apps/worker-app/src/views/LoginView.vue

@@ -63,6 +63,7 @@ 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 { getApiBaseUrl } from '@/apiConfig';
 import type { Factory } from '@smartcut/types';
 
 const router = useRouter();
@@ -87,7 +88,7 @@ onMounted(async () => {
     loadingFactories.value = true;
     try {
       const apiClient = createApiClient({
-        baseURL: window.location.origin,
+        baseURL: getApiBaseUrl(),
         tokenKey: STORAGE_KEYS.PLATFORM_TOKEN
       });
       const factoryApi = new FactoryApi(apiClient);

+ 2 - 4
apps/worker-app/src/views/RecordsView.vue

@@ -53,14 +53,12 @@
 import { ref, reactive, onMounted } from 'vue';
 import { message } from 'ant-design-vue';
 import dayjs from 'dayjs';
-import { useAuthStore } from '@/stores/auth';
 import WorkerLayout from '@/components/WorkerLayout.vue';
 import { RecordApi, createApiClient } from '@smartcut/api-client';
 import { STORAGE_KEYS, formatDate } from '@smartcut/shared-utils';
+import { getApiBaseUrl } from '@/apiConfig';
 import type { PieceRecord } from '@smartcut/types';
 
-const authStore = useAuthStore();
-
 const records = ref<PieceRecord[]>([]);
 const loading = ref(false);
 const filterDate = ref<dayjs.Dayjs | null>(null);
@@ -73,7 +71,7 @@ const pagination = reactive({
 });
 
 const apiClient = createApiClient({
-  baseURL: window.location.origin,
+  baseURL: getApiBaseUrl(),
   tokenKey: STORAGE_KEYS.WORKER_TOKEN
 });
 

+ 2 - 4
apps/worker-app/src/views/SalaryView.vue

@@ -37,19 +37,17 @@
 <script setup lang="ts">
 import { ref, onMounted } from 'vue';
 import { message } from 'ant-design-vue';
-import { useAuthStore } from '@/stores/auth';
 import WorkerLayout from '@/components/WorkerLayout.vue';
 import { SalaryApi, createApiClient } from '@smartcut/api-client';
 import { STORAGE_KEYS } from '@smartcut/shared-utils';
+import { getApiBaseUrl } from '@/apiConfig';
 import type { WorkerSalary } from '@smartcut/types';
 
-const authStore = useAuthStore();
-
 const salaries = ref<WorkerSalary[]>([]);
 const loading = ref(false);
 
 const apiClient = createApiClient({
-  baseURL: window.location.origin,
+  baseURL: getApiBaseUrl(),
   tokenKey: STORAGE_KEYS.WORKER_TOKEN
 });
 

+ 2 - 4
apps/worker-app/src/views/ScanView.vue

@@ -80,14 +80,12 @@
 import { ref, onMounted } from 'vue';
 import { message } from 'ant-design-vue';
 import { ScanOutlined } from '@ant-design/icons-vue';
-import { useAuthStore } from '@/stores/auth';
 import WorkerLayout from '@/components/WorkerLayout.vue';
 import { RecordApi, ProcessApi, createApiClient } from '@smartcut/api-client';
 import { STORAGE_KEYS } from '@smartcut/shared-utils';
+import { getApiBaseUrl } from '@/apiConfig';
 import type { Process } from '@smartcut/types';
 
-const authStore = useAuthStore();
-
 const scanning = ref(false);
 const qrCode = ref('');
 const bundleInfo = ref<any>(null);
@@ -97,7 +95,7 @@ const quantity = ref(1);
 const submitting = ref(false);
 
 const apiClient = createApiClient({
-  baseURL: window.location.origin,
+  baseURL: getApiBaseUrl(),
   tokenKey: STORAGE_KEYS.WORKER_TOKEN
 });
 

+ 3 - 6
apps/worker-app/src/views/SubmitView.vue

@@ -56,14 +56,12 @@
 <script setup lang="ts">
 import { ref, onMounted } from 'vue';
 import { message } from 'ant-design-vue';
-import { useAuthStore } from '@/stores/auth';
 import WorkerLayout from '@/components/WorkerLayout.vue';
-import { WorkOrderApi, ProcessApi, RecordApi, createApiClient } from '@smartcut/api-client';
+import { WorkOrderApi, ProcessApi, createApiClient } from '@smartcut/api-client';
 import { STORAGE_KEYS } from '@smartcut/shared-utils';
+import { getApiBaseUrl } from '@/apiConfig';
 import type { WorkOrder, Process } from '@smartcut/types';
 
-const authStore = useAuthStore();
-
 const workOrders = ref<WorkOrder[]>([]);
 const processes = ref<Process[]>([]);
 const workOrderId = ref<number | null>(null);
@@ -72,13 +70,12 @@ const quantity = ref(1);
 const submitting = ref(false);
 
 const apiClient = createApiClient({
-  baseURL: window.location.origin,
+  baseURL: getApiBaseUrl(),
   tokenKey: STORAGE_KEYS.WORKER_TOKEN
 });
 
 const workOrderApi = new WorkOrderApi(apiClient);
 const processApi = new ProcessApi(apiClient);
-const recordApi = new RecordApi(apiClient);
 
 function filterOption(input: string, option: any) {
   return option.children[0].children.toLowerCase().indexOf(input.toLowerCase()) >= 0;

+ 9 - 0
apps/worker-app/src/vite-env.d.ts

@@ -0,0 +1,9 @@
+/// <reference types="vite/client" />
+
+interface ImportMetaEnv {
+  readonly VITE_API_BASE_URL: string
+}
+
+interface ImportMeta {
+  readonly env: ImportMetaEnv
+}

+ 1 - 1
apps/worker-app/vite.config.ts

@@ -17,7 +17,7 @@ export default defineConfig({
     port: 3002,
     proxy: {
       '/api': {
-        target: 'http://localhost:8080',
+        target: 'https://smartcut.51zj.cc',
         changeOrigin: true
       }
     }

+ 2 - 0
packages/shared-components/package.json

@@ -10,6 +10,7 @@
   },
   "dependencies": {
     "vue": "^3.4.0",
+    "vue-router": "^4.2.0",
     "ant-design-vue": "^4.0.0",
     "@ant-design/icons-vue": "^7.0.0",
     "echarts": "^5.4.0",
@@ -23,6 +24,7 @@
   },
   "peerDependencies": {
     "vue": "^3.4.0",
+    "vue-router": "^4.2.0",
     "ant-design-vue": "^4.0.0"
   }
 }

+ 1 - 4
packages/shared-components/src/common/AppToast.vue

@@ -7,7 +7,6 @@
 </template>
 
 <script setup lang="ts">
-import { ref } from 'vue';
 import { message } from 'ant-design-vue';
 
 const props = defineProps<{
@@ -15,10 +14,8 @@ const props = defineProps<{
   duration?: number;
 }>();
 
-const [messageApi, contextHolder] = message.useMessage();
-
 function show(content: string) {
-  messageApi.open({
+  message.open({
     content,
     type: props.type || 'success',
     duration: props.duration || 3

+ 54 - 122
packages/shared-components/src/layout/SidebarLayout.vue

@@ -1,22 +1,26 @@
 <template>
-  <div class="sidebar-layout">
+  <a-layout class="h-screen">
     <!-- 侧边栏 -->
-    <a-layout-sider v-model:collapsed="collapsed" :trigger="null" collapsible class="sidebar">
-      <div class="logo">
-        <h1 v-if="!collapsed">智裁云</h1>
+    <a-layout-sider
+      v-model:collapsed="layout.collapsed"
+      :trigger="null"
+      collapsible
+      class="!bg-slate-900"
+    >
+      <div class="h-16 flex items-center justify-center text-white font-bold text-xl">
+        <h1 v-if="!layout.collapsed">智裁云</h1>
         <h1 v-else>裁</h1>
       </div>
 
-      <!-- 菜单 -->
       <a-menu
         v-model:selectedKeys="selectedKeys"
         mode="inline"
-        :theme="theme"
-        class="menu"
+        :theme="layout.theme"
+        class="border-r-0"
       >
-        <a-menu-item v-for="item in menuItems" :key="item.key">
-          <router-link :to="item.path">
-            <component :is="item.icon" />
+        <a-menu-item v-for="item in layout.menuItems" :key="item.key">
+          <router-link :to="item.path" class="flex items-center">
+            <component :is="item.icon" class="mr-2" />
             <span>{{ item.label }}</span>
           </router-link>
         </a-menu-item>
@@ -24,140 +28,68 @@
     </a-layout-sider>
 
     <!-- 主内容区 -->
-    <a-layout class="main-layout">
+    <a-layout class="bg-gray-100">
       <!-- 头部 -->
-      <a-layout-header class="header">
-        <div class="header-left">
-          <a-button
-            type="text"
-            @click="collapsed = !collapsed"
-            class="trigger-btn"
-          >
-            <MenuFoldOutlined v-if="!collapsed" />
-            <MenuUnfoldOutlined v-else />
-          </a-button>
-        </div>
-
-        <div class="header-right">
-          <!-- 用户信息 -->
-          <a-dropdown>
-            <a-avatar :src="userAvatar" class="user-avatar">
-              {{ userName?.charAt(0) }}
-            </a-avatar>
-            <template #overlay>
-              <a-menu>
-                <a-menu-item key="profile">
-                  <router-link to="/profile">个人信息</router-link>
-                </a-menu-item>
-                <a-menu-item key="password">
-                  <router-link to="/change-password">修改密码</router-link>
-                </a-menu-item>
-                <a-menu-divider />
-                <a-menu-item key="logout" @click="handleLogout">
-                  退出登录
-                </a-menu-item>
-              </a-menu>
-            </template>
-          </a-dropdown>
-        </div>
+      <a-layout-header class="bg-white px-6 flex justify-between items-center shadow-sm">
+        <a-button type="text" @click="layout.toggleCollapsed()" class="text-lg px-6">
+          <MenuFoldOutlined v-if="!layout.collapsed" />
+          <MenuUnfoldOutlined v-else />
+        </a-button>
+
+        <!-- 用户下拉菜单 -->
+        <a-dropdown>
+          <a-avatar class="cursor-pointer">
+            {{ userName?.charAt(0) }}
+          </a-avatar>
+          <template #overlay>
+            <a-menu>
+              <a-menu-item key="password">
+                <router-link to="/change-password">修改密码</router-link>
+              </a-menu-item>
+              <a-menu-divider />
+              <a-menu-item key="logout" @click="handleLogout">
+                退出登录
+              </a-menu-item>
+            </a-menu>
+          </template>
+        </a-dropdown>
       </a-layout-header>
 
       <!-- 内容区 -->
-      <a-layout-content class="content">
+      <a-layout-content class="m-6 bg-white rounded-lg min-h-80 overflow-auto">
         <slot />
       </a-layout-content>
     </a-layout>
-  </div>
+  </a-layout>
 </template>
 
 <script setup lang="ts">
-import { ref, computed } from 'vue';
+import { computed, getCurrentInstance } from 'vue';
 import { MenuFoldOutlined, MenuUnfoldOutlined } from '@ant-design/icons-vue';
+import { useLayoutStore } from '@/stores/layout';
 
-interface MenuItem {
-  key: string;
-  label: string;
-  icon: any;
-  path: string;
-}
+const layout = useLayoutStore();
 
-const props = defineProps<{
-  menuItems: MenuItem[];
+// Props 接收用户信息和当前路径
+defineProps<{
   userName?: string;
   userAvatar?: string;
-  theme?: 'light' | 'dark';
-  activeKey?: string;
+  currentPath?: string;
 }>();
 
 const emit = defineEmits<{
   logout: [];
 }>();
 
-const collapsed = ref(false);
-const selectedKeys = computed(() => [props.activeKey || '']);
+// activeKey 自动从路径计算
+const selectedKeys = computed(() => {
+  const props = getCurrentInstance()?.props as any;
+  const path = props?.currentPath || '/';
+  const item = layout.menuItems.find(m => path.startsWith(m.path));
+  return item ? [item.key] : [];
+});
 
 function handleLogout() {
   emit('logout');
 }
-</script>
-
-<style scoped lang="postcss">
-.sidebar-layout {
-  height: 100vh;
-}
-
-.sidebar {
-  background-color: #001529;
-}
-
-.logo {
-  height: 64px;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  color: #fff;
-  font-size: 20px;
-  font-weight: bold;
-}
-
-.menu {
-  border-right: none;
-}
-
-.main-layout {
-  background-color: #f0f2f5;
-}
-
-.header {
-  background-color: #fff;
-  padding: 0 24px;
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
-}
-
-.trigger-btn {
-  font-size: 18px;
-  line-height: 64px;
-  padding: 0 24px;
-  cursor: pointer;
-  transition: color 0.3s;
-}
-
-.trigger-btn:hover {
-  color: #1890ff;
-}
-
-.user-avatar {
-  cursor: pointer;
-}
-
-.content {
-  margin: 24px;
-  padding: 24px;
-  background-color: #fff;
-  min-height: 360px;
-  overflow-y: auto;
-}
-</style>
+</script>

+ 1 - 1
packages/shared-components/tsconfig.json

@@ -8,5 +8,5 @@
       "@smartcut/shared-components": ["./src"]
     }
   },
-  "include": ["src/**/*.ts", "src/**/*.vue"]
+  "include": ["src/**/*.ts", "src/**/*.vue", "src/**/*.d.ts"]
 }

+ 1 - 0
packages/types/src/record.ts

@@ -31,6 +31,7 @@ export interface RecordQueryParams {
   user_id?: number;
   date?: string;
   work_order_id?: number;
+  keyword?: string; // 搜索关键词(员工姓名)
 }
 
 // 工人端计件记录查询参数

+ 2 - 1
packages/types/src/user.ts

@@ -27,10 +27,11 @@ export interface SystemUser {
 
 // 用户创建请求
 export interface CreateUserRequest {
+  username?: string; // 用户名(可选,后端可能自动生成)
   name: string;
   phone: string;
   password: string;
-  role: 'factory_admin' | 'worker';
+  role: 'factory_admin' | 'worker' | 'platform_admin';
 }
 
 // 用户更新请求