浏览代码

前端独立新版本

Gogs 1 月之前
当前提交
d2ecbfae44
共有 100 个文件被更改,包括 8175 次插入0 次删除
  1. 7 0
      .env.example
  2. 43 0
      .gitignore
  3. 12 0
      .prettierrc.base.json
  4. 87 0
      README.md
  5. 3 0
      apps/factory-app/eslint.config.js
  6. 13 0
      apps/factory-app/index.html
  7. 43 0
      apps/factory-app/package.json
  8. 19 0
      apps/factory-app/src/App.vue
  9. 16 0
      apps/factory-app/src/main.ts
  10. 112 0
      apps/factory-app/src/router/index.ts
  11. 126 0
      apps/factory-app/src/stores/auth.ts
  12. 33 0
      apps/factory-app/src/styles/main.css
  13. 120 0
      apps/factory-app/src/views/ChangePasswordView.vue
  14. 373 0
      apps/factory-app/src/views/CutBatchesView.vue
  15. 134 0
      apps/factory-app/src/views/DashboardView.vue
  16. 150 0
      apps/factory-app/src/views/LoginView.vue
  17. 23 0
      apps/factory-app/src/views/NotFoundView.vue
  18. 234 0
      apps/factory-app/src/views/PricesView.vue
  19. 260 0
      apps/factory-app/src/views/ProcessesView.vue
  20. 283 0
      apps/factory-app/src/views/RecordsView.vue
  21. 252 0
      apps/factory-app/src/views/SalaryView.vue
  22. 337 0
      apps/factory-app/src/views/UsersView.vue
  23. 388 0
      apps/factory-app/src/views/WorkOrdersView.vue
  24. 18 0
      apps/factory-app/tailwind.config.js
  25. 33 0
      apps/factory-app/tsconfig.json
  26. 10 0
      apps/factory-app/tsconfig.node.json
  27. 39 0
      apps/factory-app/vite.config.ts
  28. 3 0
      apps/platform-app/eslint.config.js
  29. 13 0
      apps/platform-app/index.html
  30. 41 0
      apps/platform-app/package.json
  31. 19 0
      apps/platform-app/src/App.vue
  32. 20 0
      apps/platform-app/src/main.ts
  33. 69 0
      apps/platform-app/src/router/index.ts
  34. 104 0
      apps/platform-app/src/stores/auth.ts
  35. 38 0
      apps/platform-app/src/styles/main.css
  36. 186 0
      apps/platform-app/src/views/ChangePasswordView.vue
  37. 177 0
      apps/platform-app/src/views/DashboardView.vue
  38. 358 0
      apps/platform-app/src/views/FactoriesView.vue
  39. 107 0
      apps/platform-app/src/views/LoginView.vue
  40. 30 0
      apps/platform-app/src/views/NotFoundView.vue
  41. 502 0
      apps/platform-app/src/views/UsersView.vue
  42. 19 0
      apps/platform-app/tailwind.config.js
  43. 36 0
      apps/platform-app/tsconfig.json
  44. 10 0
      apps/platform-app/tsconfig.node.json
  45. 39 0
      apps/platform-app/vite.config.ts
  46. 12 0
      apps/worker-app/index.html
  47. 42 0
      apps/worker-app/package.json
  48. 19 0
      apps/worker-app/src/App.vue
  49. 99 0
      apps/worker-app/src/components/WorkerLayout.vue
  50. 16 0
      apps/worker-app/src/main.ts
  51. 95 0
      apps/worker-app/src/router/index.ts
  52. 130 0
      apps/worker-app/src/stores/auth.ts
  53. 66 0
      apps/worker-app/src/styles/main.css
  54. 92 0
      apps/worker-app/src/views/ChangePasswordView.vue
  55. 161 0
      apps/worker-app/src/views/HomeView.vue
  56. 163 0
      apps/worker-app/src/views/LoginView.vue
  57. 23 0
      apps/worker-app/src/views/NotFoundView.vue
  58. 90 0
      apps/worker-app/src/views/ProfileView.vue
  59. 116 0
      apps/worker-app/src/views/RecordsView.vue
  60. 82 0
      apps/worker-app/src/views/SalaryView.vue
  61. 191 0
      apps/worker-app/src/views/ScanView.vue
  62. 140 0
      apps/worker-app/src/views/SubmitView.vue
  63. 18 0
      apps/worker-app/tailwind.config.js
  64. 30 0
      apps/worker-app/tsconfig.json
  65. 10 0
      apps/worker-app/tsconfig.node.json
  66. 38 0
      apps/worker-app/vite.config.ts
  67. 72 0
      deploy/deploy.ps1
  68. 68 0
      deploy/nginx.conf
  69. 78 0
      eslint.config.base.js
  70. 37 0
      package.json
  71. 18 0
      packages/api-client/package.json
  72. 80 0
      packages/api-client/src/client.ts
  73. 33 0
      packages/api-client/src/endpoints/auth.ts
  74. 68 0
      packages/api-client/src/endpoints/factory.ts
  75. 39 0
      packages/api-client/src/endpoints/process.ts
  76. 61 0
      packages/api-client/src/endpoints/record.ts
  77. 66 0
      packages/api-client/src/endpoints/salary.ts
  78. 56 0
      packages/api-client/src/endpoints/stats.ts
  79. 54 0
      packages/api-client/src/endpoints/user.ts
  80. 64 0
      packages/api-client/src/endpoints/workOrder.ts
  81. 13 0
      packages/api-client/src/index.ts
  82. 12 0
      packages/api-client/tsconfig.json
  83. 28 0
      packages/shared-components/package.json
  84. 31 0
      packages/shared-components/src/common/AppToast.vue
  85. 7 0
      packages/shared-components/src/index.ts
  86. 163 0
      packages/shared-components/src/layout/SidebarLayout.vue
  87. 5 0
      packages/shared-components/src/shims-vue.d.ts
  88. 12 0
      packages/shared-components/tsconfig.json
  89. 17 0
      packages/shared-utils/package.json
  90. 79 0
      packages/shared-utils/src/errorHandler.ts
  91. 57 0
      packages/shared-utils/src/format.ts
  92. 7 0
      packages/shared-utils/src/index.ts
  93. 66 0
      packages/shared-utils/src/permission.ts
  94. 112 0
      packages/shared-utils/src/storage.ts
  95. 71 0
      packages/shared-utils/src/validate.ts
  96. 11 0
      packages/shared-utils/tsconfig.json
  97. 14 0
      packages/types/package.json
  98. 27 0
      packages/types/src/api.ts
  99. 10 0
      packages/types/src/index.ts
  100. 67 0
      packages/types/src/models.ts

+ 7 - 0
.env.example

@@ -0,0 +1,7 @@
+# 智裁云前端环境变量配置示例
+
+# API基础地址(开发环境)
+VITE_API_BASE_URL=http://localhost:8080
+
+# API基础地址(生产环境)
+# VITE_API_BASE_URL=https://api.smartcut.example.com

+ 43 - 0
.gitignore

@@ -0,0 +1,43 @@
+# Dependencies
+node_modules
+.pnpm-store
+pnpm-lock.yaml
+
+# Build outputs
+dist
+*.local
+
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+
+# Editor directories and files
+.vscode
+.idea
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+.DS_Store
+
+# Testing
+coverage
+*.lcov
+
+# Environment files
+.env
+.env.local
+.env.*.local
+
+# Cache
+.cache
+.temp
+*.tsbuildinfo
+
+# TypeScript
+*.tsbuildinfo

+ 12 - 0
.prettierrc.base.json

@@ -0,0 +1,12 @@
+{
+  "semi": true,
+  "trailingComma": "es5",
+  "singleQuote": true,
+  "printWidth": 100,
+  "tabWidth": 2,
+  "useTabs": false,
+  "bracketSpacing": true,
+  "arrowParens": "avoid",
+  "endOfLine": "auto",
+  "vueIndentScriptAndStyle": false
+}

+ 87 - 0
README.md

@@ -0,0 +1,87 @@
+# 智裁云前端项目
+
+基于 Vue 3 + TypeScript + Vite 的现代化前端项目(Monorepo架构)
+
+## 项目结构
+
+```
+smartcut-frontend/
+├── apps/                          # 应用目录
+│   ├── platform-app/              # 系统管理后台
+│   ├── factory-app/               # 工厂管理后台
+│   └── worker-app/                # 工人端
+├── packages/                      # 共享包目录
+│   ├── types/                     # TypeScript类型定义
+│   ├── api-client/                # 统一API封装
+│   ├── shared-utils/              # 共享工具函数
+│   └── shared-components/         # 共享组件库
+└── pnpm-workspace.yaml            # pnpm workspace配置
+```
+
+## 技术栈
+
+- **前端框架**: Vue 3 + Composition API
+- **UI组件库**: Ant Design Vue 4.x
+- **状态管理**: Pinia 2.x
+- **路由管理**: Vue Router 4.x
+- **构建工具**: Vite 5.x
+- **CSS方案**: TailwindCSS + Ant Design Vue
+- **HTTP客户端**: Axios
+- **代码规范**: ESLint + Prettier
+- **类型安全**: TypeScript 5.3+
+
+## 开发指南
+
+### 安装依赖
+
+```bash
+pnpm install
+```
+
+### 开发模式
+
+```bash
+# 系统管理后台
+pnpm dev:platform
+
+# 工厂管理后台
+pnpm dev:factory
+
+# 工人端
+pnpm dev:worker
+```
+
+### 构建生产版本
+
+```bash
+# 构建单个应用
+pnpm build:platform
+pnpm build:factory
+pnpm build:worker
+
+# 构建所有应用
+pnpm build:all
+```
+
+### 代码规范
+
+```bash
+# 代码检查
+pnpm lint
+
+# 代码格式化
+pnpm format
+```
+
+## 关键特性
+
+- ✅ **完全重写前端**: 拒绝向后兼容,现代化架构
+- ✅ **Monorepo统一管理**: 共享代码、统一依赖
+- ✅ **TypeScript类型安全**: 全栈类型约束
+- ✅ **Query参数传递factory_id**: 废弃路径参数模式
+- ✅ **独立域名部署**: 前端完全独立
+- ✅ **响应式设计**: PC + 移动端统一适配
+
+## 参考文档
+
+详细设计方案请参考: `../smartcut/.trae/documents/前后端分离设计方案.md`

+ 3 - 0
apps/factory-app/eslint.config.js

@@ -0,0 +1,3 @@
+const baseConfig = require('../../eslint.config.base.js');
+
+module.exports = [...baseConfig];

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

@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+  <head>
+    <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" />
+    <title>智裁云 - 工厂管理后台</title>
+  </head>
+  <body>
+    <div id="app"></div>
+    <script type="module" src="/src/main.ts"></script>
+  </body>
+</html>

+ 43 - 0
apps/factory-app/package.json

@@ -0,0 +1,43 @@
+{
+  "name": "factory-app",
+  "version": "1.0.0",
+  "private": true,
+  "type": "module",
+  "scripts": {
+    "dev": "vite",
+    "build": "vite build",
+    "type-check": "vue-tsc --noEmit",
+    "preview": "vite preview",
+    "lint": "eslint . --fix",
+    "format": "prettier --write src/"
+  },
+  "dependencies": {
+    "vue": "^3.4.0",
+    "vue-router": "^4.2.0",
+    "pinia": "^2.1.0",
+    "ant-design-vue": "^4.0.0",
+    "axios": "^1.6.0",
+    "@ant-design/icons-vue": "^7.0.0",
+    "echarts": "^5.4.0",
+    "dayjs": "^1.11.0",
+    "@smartcut/types": "workspace:*",
+    "@smartcut/api-client": "workspace:*",
+    "@smartcut/shared-utils": "workspace:*",
+    "@smartcut/shared-components": "workspace:*"
+  },
+  "devDependencies": {
+    "@vitejs/plugin-vue": "^5.0.0",
+    "typescript": "^5.3.0",
+    "vite": "^5.0.0",
+    "vue-tsc": "^1.8.0",
+    "eslint": "^9.0.0",
+    "@eslint/js": "^9.0.0",
+    "typescript-eslint": "^8.0.0",
+    "eslint-plugin-vue": "^9.30.0",
+    "vue-eslint-parser": "^9.4.0",
+    "prettier": "^3.2.0",
+    "tailwindcss": "^3.4.0",
+    "autoprefixer": "^10.4.0",
+    "postcss": "^8.4.0"
+  }
+}

+ 19 - 0
apps/factory-app/src/App.vue

@@ -0,0 +1,19 @@
+<template>
+  <a-config-provider :locale="zhCN">
+    <router-view />
+  </a-config-provider>
+</template>
+
+<script setup lang="ts">
+import zhCN from 'ant-design-vue/es/locale/zh_CN';
+</script>
+
+<style>
+#app {
+  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,
+    'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
+    'Noto Color Emoji';
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
+</style>

+ 16 - 0
apps/factory-app/src/main.ts

@@ -0,0 +1,16 @@
+import { createApp } from 'vue';
+import { createPinia } from 'pinia';
+import Antd from 'ant-design-vue';
+import 'ant-design-vue/dist/reset.css';
+import App from './App.vue';
+import router from './router';
+
+import './styles/main.css';
+
+const app = createApp(App);
+
+app.use(createPinia());
+app.use(router);
+app.use(Antd);
+
+app.mount('#app');

+ 112 - 0
apps/factory-app/src/router/index.ts

@@ -0,0 +1,112 @@
+// 工厂管理后台路由配置(完全重写:标准SPA路由)
+
+import { createRouter, createWebHistory } from 'vue-router';
+import { useAuthStore } from '@/stores/auth';
+
+const routes = [
+  {
+    path: '/login',
+    name: 'Login',
+    component: () => import('@/views/LoginView.vue'),
+    meta: { requiresAuth: false }
+  },
+  {
+    path: '/',
+    name: 'Dashboard',
+    component: () => import('@/views/DashboardView.vue'),
+    meta: { requiresAuth: true }
+  },
+  {
+    path: '/work-orders',
+    name: 'WorkOrders',
+    component: () => import('@/views/WorkOrdersView.vue'),
+    meta: { requiresAuth: true }
+  },
+  {
+    path: '/processes',
+    name: 'Processes',
+    component: () => import('@/views/ProcessesView.vue'),
+    meta: { requiresAuth: true }
+  },
+  {
+    path: '/records',
+    name: 'Records',
+    component: () => import('@/views/RecordsView.vue'),
+    meta: { requiresAuth: true }
+  },
+  {
+    path: '/cut-batches',
+    name: 'CutBatches',
+    component: () => import('@/views/CutBatchesView.vue'),
+    meta: { requiresAuth: true }
+  },
+  {
+    path: '/prices',
+    name: 'Prices',
+    component: () => import('@/views/PricesView.vue'),
+    meta: { requiresAuth: true }
+  },
+  {
+    path: '/salary',
+    name: 'Salary',
+    component: () => import('@/views/SalaryView.vue'),
+    meta: { requiresAuth: true }
+  },
+  {
+    path: '/users',
+    name: 'Users',
+    component: () => import('@/views/UsersView.vue'),
+    meta: { requiresAuth: true }
+  },
+  {
+    path: '/change-password',
+    name: 'ChangePassword',
+    component: () => import('@/views/ChangePasswordView.vue'),
+    meta: { requiresAuth: true }
+  },
+  {
+    path: '/:pathMatch(.*)*',
+    name: 'NotFound',
+    component: () => import('@/views/NotFoundView.vue'),
+    meta: { requiresAuth: false }
+  }
+];
+
+const router = createRouter({
+  history: createWebHistory(),
+  routes
+});
+
+// 路由守卫:认证检查 + factory_id验证
+router.beforeEach((to, from, next) => {
+  const authStore = useAuthStore();
+
+  // 需要认证的路由
+  if (to.meta.requiresAuth) {
+    if (!authStore.isLoggedIn) {
+      // 未登录时跳转登录页,携带redirect和factory参数
+      next({
+        name: 'Login',
+        query: {
+          redirect: to.fullPath,
+          factory: authStore.currentFactoryId || ''
+        }
+      });
+    } else if (!authStore.currentFactoryId) {
+      // 有Token但无factory_id,跳转登录重新选择工厂
+      next({
+        name: 'Login',
+        query: { redirect: to.fullPath }
+      });
+    } else {
+      next();
+    }
+  } else if (to.name === 'Login' && authStore.isLoggedIn) {
+    // 已登录时访问登录页,跳转首页
+    next({ name: 'Dashboard' });
+  } else {
+    next();
+  }
+});
+
+export default router;

+ 126 - 0
apps/factory-app/src/stores/auth.ts

@@ -0,0 +1,126 @@
+// 工厂管理后台认证状态管理
+
+import { defineStore } from 'pinia';
+import { ref, computed } from 'vue';
+import type { User } from '@smartcut/types';
+import { AuthApi, createApiClient } from '@smartcut/api-client';
+import {
+  STORAGE_KEYS,
+  setSessionStorage,
+  getSessionStorage,
+  removeSessionStorage
+} from '@smartcut/shared-utils';
+
+export const useAuthStore = defineStore('auth', () => {
+  // 状态定义
+  const token = ref<string | null>(getSessionStorage<string>(STORAGE_KEYS.FACTORY_TOKEN));
+  const user = ref<User | null>(getSessionStorage<User>(STORAGE_KEYS.FACTORY_USER));
+  const currentFactoryId = ref<string | null>(
+    getSessionStorage<string>(STORAGE_KEYS.CURRENT_FACTORY_ID)
+  );
+  const isLoggedIn = computed(() => !!token.value && !!user.value && !!currentFactoryId.value);
+
+  // 创建API客户端(factory_id通过Query参数传递)
+  const apiClient = createApiClient({
+    baseURL: window.location.origin,
+    tokenKey: STORAGE_KEYS.FACTORY_TOKEN
+  });
+
+  const authApi = new AuthApi(apiClient);
+
+  /**
+   * 工厂管理员登录(完全重写:Query参数传递factory_id)
+   */
+  async function login(username: string, password: string, factoryId: string) {
+    try {
+      const response = await authApi.login({
+        username,
+        password,
+        factory_id: factoryId // 新方案:登录接口接收factory_id参数
+      });
+
+      // 存储Token、用户信息、factory_id到SessionStorage
+      token.value = response.token;
+      user.value = response.user as User;
+      currentFactoryId.value = factoryId;
+
+      setSessionStorage(STORAGE_KEYS.FACTORY_TOKEN, response.token);
+      setSessionStorage(STORAGE_KEYS.FACTORY_USER, response.user);
+      setSessionStorage(STORAGE_KEYS.CURRENT_FACTORY_ID, factoryId);
+
+      return response;
+    } catch (error) {
+      console.error('登录失败:', error);
+      throw error;
+    }
+  }
+
+  /**
+   * 登出
+   */
+  function logout() {
+    token.value = null;
+    user.value = null;
+    currentFactoryId.value = null;
+
+    removeSessionStorage(STORAGE_KEYS.FACTORY_TOKEN);
+    removeSessionStorage(STORAGE_KEYS.FACTORY_USER);
+    removeSessionStorage(STORAGE_KEYS.CURRENT_FACTORY_ID);
+  }
+
+  /**
+   * 获取用户信息
+   */
+  async function fetchProfile() {
+    if (!token.value || !currentFactoryId.value) return;
+
+    try {
+      const profile = await authApi.getProfile();
+      user.value = profile as User;
+      setSessionStorage(STORAGE_KEYS.FACTORY_USER, profile);
+    } catch (error) {
+      console.error('获取用户信息失败:', error);
+      logout();
+    }
+  }
+
+  /**
+   * 修改密码
+   */
+  async function changePassword(oldPassword: string, newPassword: string) {
+    try {
+      await authApi.changePassword({
+        old_password: oldPassword,
+        new_password: newPassword
+      });
+
+      if (user.value) {
+        user.value.must_change_password = false;
+        setSessionStorage(STORAGE_KEYS.FACTORY_USER, user.value);
+      }
+    } catch (error) {
+      console.error('修改密码失败:', error);
+      throw error;
+    }
+  }
+
+  /**
+   * 从URL Query参数设置factory_id
+   */
+  function setFactoryIdFromQuery(factoryId: string) {
+    currentFactoryId.value = factoryId;
+    setSessionStorage(STORAGE_KEYS.CURRENT_FACTORY_ID, factoryId);
+  }
+
+  return {
+    token,
+    user,
+    currentFactoryId,
+    isLoggedIn,
+    login,
+    logout,
+    fetchProfile,
+    changePassword,
+    setFactoryIdFromQuery
+  };
+});

+ 33 - 0
apps/factory-app/src/styles/main.css

@@ -0,0 +1,33 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+:root {
+  --ant-primary-color: #1e3a5f;
+  --ant-primary-color-hover: #2a4a6f;
+  --ant-primary-color-active: #15304f;
+}
+
+.ant-btn-primary {
+  background-color: #1e3a5f;
+}
+
+.ant-btn-primary:hover {
+  background-color: #2a4a6f;
+}
+
+.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;
+}

+ 120 - 0
apps/factory-app/src/views/ChangePasswordView.vue

@@ -0,0 +1,120 @@
+<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>
+</template>
+
+<script setup lang="ts">
+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();
+
+const formState = reactive({
+  old_password: '',
+  new_password: '',
+  confirm_password: ''
+});
+
+const formRules = {
+  old_password: [{ required: true, message: '请输入旧密码', trigger: 'blur' }],
+  new_password: [
+    { required: true, message: '请输入新密码', trigger: 'blur' },
+    {
+      validator: (_rule: any, value: string) => {
+        if (!validatePasswordStrength(value)) return Promise.reject('密码强度不足');
+        return Promise.resolve();
+      },
+      trigger: 'blur'
+    }
+  ],
+  confirm_password: [
+    { required: true, message: '请确认新密码', trigger: 'blur' },
+    {
+      validator: (_rule: any, value: string) => {
+        if (value !== formState.new_password) return Promise.reject('两次输入的密码不一致');
+        return Promise.resolve();
+      },
+      trigger: 'blur'
+    }
+  ]
+};
+
+async function handleSubmit() {
+  try {
+    submitting.value = true;
+    await authStore.changePassword(formState.old_password, formState.new_password);
+    message.success('密码修改成功');
+    if (authStore.user?.must_change_password) {
+      router.push('/');
+    } else {
+      resetForm();
+    }
+  } catch (error: any) {
+    message.error(error.response?.data?.msg || '修改密码失败');
+  } finally {
+    submitting.value = false;
+  }
+}
+
+function resetForm() {
+  formRef.value?.resetFields();
+  formState.old_password = '';
+  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>

+ 373 - 0
apps/factory-app/src/views/CutBatchesView.vue

@@ -0,0 +1,373 @@
+<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>
+</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 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,
+  tokenKey: STORAGE_KEYS.FACTORY_TOKEN
+});
+
+const workOrderApi = new WorkOrderApi(apiClient);
+
+const batches = ref<CutBatch[]>([]);
+const workOrders = ref<WorkOrder[]>([]);
+const bundles = ref<CutBundle[]>([]);
+const loading = ref(false);
+const searchKeyword = ref('');
+
+const columns = [
+  { title: 'ID', dataIndex: 'id', key: 'id', width: 80 },
+  { title: '批次号', dataIndex: 'batch_no', key: 'batch_no', width: 150 },
+  { title: '工单号', dataIndex: 'order_no', key: 'order_no', width: 150 },
+  { title: '颜色', dataIndex: 'color', key: 'color', width: 100 },
+  { title: '总数量', dataIndex: 'total_qty', key: 'total_qty', width: 100 },
+  { title: '完成数量', dataIndex: 'completed_qty', key: 'completed_qty', width: 100 },
+  { title: '状态', dataIndex: 'status', key: 'status', width: 100, slots: { customRender: 'status' } },
+  { title: '操作', key: 'action', width: 250, slots: { customRender: 'action' } }
+];
+
+const bundleColumns = [
+  { title: 'ID', dataIndex: 'id', key: 'id', width: 80 },
+  { title: '扎号', dataIndex: 'bundle_no', key: 'bundle_no', width: 120 },
+  { title: '尺码', dataIndex: 'size', key: 'size', width: 100 },
+  { title: '数量', dataIndex: 'quantity', key: 'quantity', width: 100 },
+  { title: '操作', key: 'action', width: 150, slots: { customRender: 'action' } }
+];
+
+const modalVisible = ref(false);
+const modalTitle = ref('创建批次');
+const isEdit = ref(false);
+const submitting = ref(false);
+const formRef = ref();
+const editingId = ref<number | null>(null);
+
+const formState = reactive({
+  work_order_id: null as number | null,
+  batch_no: '',
+  color: '',
+  total_qty: 1
+});
+
+const formRules = {
+  work_order_id: [{ required: true, message: '请选择工单', trigger: 'change' }],
+  batch_no: [{ required: true, message: '请输入批次号', trigger: 'blur' }],
+  total_qty: [{ required: true, message: '请输入总数量', trigger: 'blur' }]
+};
+
+const bundlesModalVisible = ref(false);
+const currentBatch = ref<CutBatch | null>(null);
+const addBundleModalVisible = ref(false);
+const addingBundle = ref(false);
+const newBundle = reactive({ size: '', quantity: 1 });
+
+const bundleQRVisible = ref(false);
+const bundleQRUrl = ref('');
+const currentBundle = ref<CutBundle | null>(null);
+
+function getStatusColor(status: string) {
+  const colors: Record<string, string> = {
+    pending: 'default',
+    in_progress: 'blue',
+    completed: 'green'
+  };
+  return colors[status] || 'default';
+}
+
+function getStatusText(status: string) {
+  const texts: Record<string, string> = {
+    pending: '待开始',
+    in_progress: '进行中',
+    completed: '已完成'
+  };
+  return texts[status] || status;
+}
+
+async function loadBatches() {
+  loading.value = true;
+  try {
+    const response = await apiClient.get('/cut-batches', { params: { keyword: searchKeyword.value } });
+    batches.value = response.data.data.list || [];
+  } catch (error) {
+    message.error('加载批次列表失败');
+  } finally {
+    loading.value = false;
+  }
+}
+
+async function loadWorkOrders() {
+  try {
+    const result = await workOrderApi.getWorkOrders({ page: 1, page_size: 100 });
+    workOrders.value = result.list;
+  } catch (error) {
+    // ignore
+  }
+}
+
+function showCreateModal() {
+  modalTitle.value = '创建批次';
+  isEdit.value = false;
+  modalVisible.value = true;
+  formRef.value?.resetFields();
+  formState.work_order_id = null;
+  formState.batch_no = '';
+  formState.color = '';
+  formState.total_qty = 1;
+}
+
+function showEditModal(record: CutBatch) {
+  modalTitle.value = '编辑批次';
+  isEdit.value = true;
+  editingId.value = record.id;
+  modalVisible.value = true;
+  formState.work_order_id = record.work_order_id;
+  formState.batch_no = record.batch_no;
+  formState.color = record.color;
+  formState.total_qty = record.total_qty;
+}
+
+async function handleSubmit() {
+  try {
+    await formRef.value.validate();
+    submitting.value = true;
+    if (isEdit.value && editingId.value) {
+      await apiClient.put(`/cut-batches/${editingId.value}`, {
+        color: formState.color,
+        total_qty: formState.total_qty
+      });
+      message.success('批次已更新');
+    } else {
+      await apiClient.post('/cut-batches', {
+        work_order_id: formState.work_order_id,
+        batch_no: formState.batch_no,
+        color: formState.color,
+        total_qty: formState.total_qty
+      });
+      message.success('批次已创建');
+    }
+    modalVisible.value = false;
+    loadBatches();
+  } catch (error: any) {
+    message.error(error.response?.data?.msg || '操作失败');
+  } finally {
+    submitting.value = false;
+  }
+}
+
+async function deleteBatch(id: number) {
+  try {
+    await apiClient.delete(`/cut-batches/${id}`, { headers: { 'X-Confirm': 'true' } });
+    message.success('批次已删除');
+    loadBatches();
+  } catch (error) {
+    message.error('删除失败');
+  }
+}
+
+async function showBundlesModal(record: CutBatch) {
+  currentBatch.value = record;
+  bundlesModalVisible.value = true;
+  try {
+    const response = await apiClient.get(`/cut-batches/${record.id}/bundles`);
+    bundles.value = response.data.data || [];
+  } catch (error) {
+    message.error('加载扎号列表失败');
+  }
+}
+
+function showAddBundleModal() {
+  addBundleModalVisible.value = true;
+  newBundle.size = '';
+  newBundle.quantity = 1;
+}
+
+async function handleAddBundle() {
+  if (!currentBatch.value) return;
+  if (!newBundle.size) {
+    message.error('请输入尺码');
+    return;
+  }
+  addingBundle.value = true;
+  try {
+    await apiClient.post(`/cut-batches/${currentBatch.value.id}/bundles`, {
+      size: newBundle.size,
+      quantity: newBundle.quantity
+    });
+    message.success('扎号已添加');
+    addBundleModalVisible.value = false;
+    showBundlesModal(currentBatch.value);
+  } catch (error: any) {
+    message.error(error.response?.data?.msg || '添加失败');
+  } finally {
+    addingBundle.value = false;
+  }
+}
+
+async function deleteBundle(id: number) {
+  try {
+    await apiClient.delete(`/cut-batches/bundles/${id}`);
+    message.success('扎号已删除');
+    if (currentBatch.value) showBundlesModal(currentBatch.value);
+  } catch (error) {
+    message.error('删除失败');
+  }
+}
+
+async function showBundleQRCode(bundle: CutBundle) {
+  currentBundle.value = bundle;
+  try {
+    const response = await apiClient.get(`/cut-batches/bundles/${bundle.id}/qrcode`, { responseType: 'blob' });
+    bundleQRUrl.value = URL.createObjectURL(response.data);
+    bundleQRVisible.value = true;
+  } catch (error) {
+    message.error('生成二维码失败');
+  }
+}
+
+async function generateAllQRCodes() {
+  if (!currentBatch.value) return;
+  try {
+    const response = await apiClient.get(`/cut-batches/${currentBatch.value.id}/qrcodes`, { responseType: 'blob' });
+    const url = URL.createObjectURL(response.data);
+    const a = document.createElement('a');
+    a.href = url;
+    a.download = `batch-${currentBatch.value.batch_no}-qrcodes.zip`;
+    a.click();
+    URL.revokeObjectURL(url);
+    message.success('二维码已下载');
+  } catch (error) {
+    message.error('批量生成二维码失败');
+  }
+}
+
+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>

+ 134 - 0
apps/factory-app/src/views/DashboardView.vue

@@ -0,0 +1,134 @@
+<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>
+
+      <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>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, 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 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,
+  today_output: 0,
+  worker_count: 0,
+  active_orders: 0
+});
+
+const apiClient = createApiClient({
+  baseURL: window.location.origin,
+  tokenKey: STORAGE_KEYS.FACTORY_TOKEN
+});
+
+const statsApi = new StatsApi(apiClient);
+
+onMounted(async () => {
+  try {
+    dashboardData.value = await statsApi.getDashboard();
+  } 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>

+ 150 - 0
apps/factory-app/src/views/LoginView.vue

@@ -0,0 +1,150 @@
+<template>
+  <div class="login-page">
+    <a-card class="login-card" title="智裁云 - 工厂管理后台">
+      <!-- 工厂选择 -->
+      <a-form-item v-if="!factoryId" label="选择工厂">
+        <a-select
+          v-model:value="selectedFactory"
+          placeholder="请选择工厂"
+          size="large"
+          :loading="loadingFactories"
+        >
+          <a-select-option v-for="f in factories" :key="f.id" :value="f.id">
+            {{ f.name }}
+          </a-select-option>
+        </a-select>
+      </a-form-item>
+
+      <a-form
+        :model="formState"
+        :rules="rules"
+        @finish="handleLogin"
+        layout="vertical"
+      >
+        <a-form-item label="用户名" name="username">
+          <a-input
+            v-model:value="formState.username"
+            placeholder="请输入用户名"
+            size="large"
+          />
+        </a-form-item>
+
+        <a-form-item label="密码" name="password">
+          <a-input-password
+            v-model:value="formState.password"
+            placeholder="请输入密码"
+            size="large"
+          />
+        </a-form-item>
+
+        <a-form-item>
+          <a-button
+            type="primary"
+            html-type="submit"
+            size="large"
+            block
+            :loading="loading"
+            :disabled="!factoryId && !selectedFactory"
+          >
+            登录
+          </a-button>
+        </a-form-item>
+      </a-form>
+    </a-card>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive, onMounted } from 'vue';
+import { useRouter, useRoute } from 'vue-router';
+import { message } from 'ant-design-vue';
+import { useAuthStore } from '@/stores/auth';
+import { FactoryApi, createApiClient } from '@smartcut/api-client';
+import { STORAGE_KEYS } from '@smartcut/shared-utils';
+import type { Factory } from '@smartcut/types';
+
+const router = useRouter();
+const route = useRoute();
+const authStore = useAuthStore();
+
+const loading = ref(false);
+const loadingFactories = ref(false);
+const factories = ref<Factory[]>([]);
+const selectedFactory = ref('');
+
+// 从Query参数获取factory_id
+const factoryId = ref(route.query.factory as string);
+
+const formState = reactive({
+  username: '',
+  password: ''
+});
+
+const rules = {
+  username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
+  password: [{ required: true, message: '请输入密码', trigger: 'blur' }]
+};
+
+// 加载工厂列表(如果没有factory参数)
+onMounted(async () => {
+  if (!factoryId.value) {
+    loadingFactories.value = true;
+    try {
+      const apiClient = createApiClient({
+        baseURL: window.location.origin,
+        tokenKey: STORAGE_KEYS.PLATFORM_TOKEN
+      });
+      const factoryApi = new FactoryApi(apiClient);
+      const result = await factoryApi.listFactories();
+      factories.value = result.list.filter(f => f.status === 1);
+    } catch (error) {
+      message.error('加载工厂列表失败');
+    } finally {
+      loadingFactories.value = false;
+    }
+  } else {
+    // 存储factory_id到SessionStorage
+    authStore.setFactoryIdFromQuery(factoryId.value);
+  }
+});
+
+async function handleLogin() {
+  const targetFactoryId = factoryId.value || selectedFactory.value;
+
+  if (!targetFactoryId) {
+    message.error('请选择工厂');
+    return;
+  }
+
+  loading.value = true;
+
+  try {
+    await authStore.login(formState.username, formState.password, targetFactoryId);
+    message.success('登录成功');
+
+    const redirect = route.query.redirect as string;
+    router.push(redirect || '/');
+  } catch (error: any) {
+    const errorMsg = error.response?.data?.msg || '登录失败';
+    message.error(errorMsg);
+  } finally {
+    loading.value = false;
+  }
+}
+</script>
+
+<style scoped lang="postcss">
+.login-page {
+  height: 100vh;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  background-color: #f0f2f5;
+}
+
+.login-card {
+  width: 400px;
+  border-radius: 8px;
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+}
+</style>

+ 23 - 0
apps/factory-app/src/views/NotFoundView.vue

@@ -0,0 +1,23 @@
+<template>
+  <div class="not-found-page">
+    <a-result status="404" title="404" sub-title="抱歉,您访问的页面不存在">
+      <template #extra>
+        <a-button type="primary" @click="router.push('/')">返回首页</a-button>
+      </template>
+    </a-result>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { useRouter } from 'vue-router';
+const router = useRouter();
+</script>
+
+<style scoped lang="postcss">
+.not-found-page {
+  height: 100vh;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
+</style>

+ 234 - 0
apps/factory-app/src/views/PricesView.vue

@@ -0,0 +1,234 @@
+<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 }}
+            </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>
+</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 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,
+  tokenKey: STORAGE_KEYS.FACTORY_TOKEN
+});
+
+const workOrderApi = new WorkOrderApi(apiClient);
+const processApi = new ProcessApi(apiClient);
+
+const prices = ref<PriceTemplate[]>([]);
+const workOrders = ref<WorkOrder[]>([]);
+const processes = ref<Process[]>([]);
+const loading = ref(false);
+const filterWorkOrderId = ref<number | null>(null);
+
+const columns = [
+  { title: 'ID', dataIndex: 'id', key: 'id', width: 80 },
+  { title: '款号', dataIndex: 'style', key: 'style', width: 120 },
+  { title: '工序', dataIndex: 'process_name', key: 'process_name', width: 150 },
+  { title: '工价', key: 'price', width: 100, slots: { customRender: 'price' } },
+  { title: '备注', dataIndex: 'remark', key: 'remark', width: 200 },
+  { title: '操作', key: 'action', width: 150, slots: { customRender: 'action' } }
+];
+
+const modalVisible = ref(false);
+const modalTitle = ref('设置工价');
+const isEdit = ref(false);
+const submitting = ref(false);
+const formRef = ref();
+const editingId = ref<number | null>(null);
+
+const formState = reactive({
+  style: '',
+  process_id: null as number | null,
+  price: 0,
+  remark: ''
+});
+
+const formRules = {
+  style: [{ required: true, message: '请输入款号', trigger: 'blur' }],
+  process_id: [{ required: true, message: '请选择工序', trigger: 'change' }],
+  price: [{ required: true, message: '请输入工价', trigger: 'blur' }]
+};
+
+async function loadPrices() {
+  loading.value = true;
+  try {
+    const params: any = {};
+    if (filterWorkOrderId.value) params.work_order_id = filterWorkOrderId.value;
+    const response = await apiClient.get('/prices', { params });
+    prices.value = response.data.data || [];
+  } catch (error) {
+    message.error('加载工价列表失败');
+  } finally {
+    loading.value = false;
+  }
+}
+
+async function loadWorkOrders() {
+  try {
+    const result = await workOrderApi.getWorkOrders({ page: 1, page_size: 100 });
+    workOrders.value = result.list;
+  } catch (error) {
+    // ignore
+  }
+}
+
+async function loadProcesses() {
+  try {
+    processes.value = await processApi.getProcesses();
+  } catch (error) {
+    // ignore
+  }
+}
+
+function showCreateModal() {
+  modalTitle.value = '设置工价';
+  isEdit.value = false;
+  modalVisible.value = true;
+  formRef.value?.resetFields();
+  formState.style = '';
+  formState.process_id = null;
+  formState.price = 0;
+  formState.remark = '';
+}
+
+function showEditModal(record: PriceTemplate) {
+  modalTitle.value = '编辑工价';
+  isEdit.value = true;
+  editingId.value = record.id;
+  modalVisible.value = true;
+  formState.style = record.style;
+  formState.process_id = record.process_id;
+  formState.price = record.price;
+  formState.remark = record.remark;
+}
+
+async function handleSubmit() {
+  try {
+    await formRef.value.validate();
+    submitting.value = true;
+    if (isEdit.value && editingId.value) {
+      await apiClient.put(`/prices/${editingId.value}`, {
+        process_id: formState.process_id,
+        price: formState.price,
+        remark: formState.remark
+      });
+      message.success('工价已更新');
+    } else {
+      await apiClient.post('/prices', {
+        style: formState.style,
+        process_id: formState.process_id,
+        price: formState.price,
+        remark: formState.remark
+      });
+      message.success('工价已创建');
+    }
+    modalVisible.value = false;
+    loadPrices();
+  } catch (error: any) {
+    message.error(error.response?.data?.msg || '操作失败');
+  } finally {
+    submitting.value = false;
+  }
+}
+
+async function deletePrice(id: number) {
+  try {
+    await apiClient.delete(`/prices/${id}`);
+    message.success('工价已删除');
+    loadPrices();
+  } catch (error) {
+    message.error('删除失败');
+  }
+}
+
+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>

+ 260 - 0
apps/factory-app/src/views/ProcessesView.vue

@@ -0,0 +1,260 @@
+<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)">
+                编辑
+              </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-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>
+</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 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,
+  tokenKey: STORAGE_KEYS.FACTORY_TOKEN
+});
+
+const processApi = new ProcessApi(apiClient);
+
+const processes = ref<Process[]>([]);
+const loading = ref(false);
+
+const columns = [
+  { title: 'ID', dataIndex: 'id', key: 'id', width: 80 },
+  { title: '工序名称', dataIndex: 'name', key: 'name', width: 200 },
+  { title: '工序代码', dataIndex: 'code', key: 'code', width: 150 },
+  { title: '排序', dataIndex: 'sort_order', key: 'sort_order', width: 100 },
+  { title: '状态', dataIndex: 'status', key: 'status', width: 100, slots: { customRender: 'status' } },
+  { title: '操作', key: 'action', fixed: 'right', width: 150, slots: { customRender: 'action' } }
+];
+
+const modalVisible = ref(false);
+const modalTitle = ref('创建工序');
+const isEdit = ref(false);
+const submitting = ref(false);
+const formRef = ref();
+const editingProcessId = ref<number | null>(null);
+
+const formState = reactive({
+  name: '',
+  code: '',
+  sort_order: 1,
+  status: 1
+});
+
+const formRules = {
+  name: [{ required: true, message: '请输入工序名称', trigger: 'blur' }],
+  code: [
+    { required: true, message: '请输入工序代码', trigger: 'blur' },
+    { pattern: /^[a-zA-Z0-9_-]+$/, message: '工序代码只能包含字母、数字、下划线或横线', trigger: 'blur' }
+  ]
+};
+
+async function loadProcesses() {
+  loading.value = true;
+  try {
+    processes.value = await processApi.getProcesses();
+  } catch (error) {
+    message.error('加载工序列表失败');
+  } finally {
+    loading.value = false;
+  }
+}
+
+function showCreateModal() {
+  modalTitle.value = '创建工序';
+  isEdit.value = false;
+  modalVisible.value = true;
+  resetForm();
+}
+
+function showEditModal(record: Process) {
+  modalTitle.value = '编辑工序';
+  isEdit.value = true;
+  editingProcessId.value = record.id;
+  modalVisible.value = true;
+
+  formState.name = record.name;
+  formState.code = record.code;
+  formState.sort_order = record.sort_order;
+  formState.status = record.status;
+}
+
+async function handleSubmit() {
+  try {
+    await formRef.value.validate();
+    submitting.value = true;
+
+    if (isEdit.value && editingProcessId.value) {
+      await processApi.updateProcess(editingProcessId.value, {
+        name: formState.name,
+        code: formState.code,
+        sort_order: formState.sort_order,
+        status: formState.status
+      });
+      message.success('工序已更新');
+    } else {
+      await processApi.createProcess({
+        name: formState.name,
+        code: formState.code,
+        sort_order: formState.sort_order
+      });
+      message.success('工序已创建');
+    }
+
+    modalVisible.value = false;
+    loadProcesses();
+  } catch (error: any) {
+    const errorMsg = error.response?.data?.msg || '操作失败';
+    message.error(errorMsg);
+  } finally {
+    submitting.value = false;
+  }
+}
+
+function resetForm() {
+  formRef.value?.resetFields();
+  formState.name = '';
+  formState.code = '';
+  formState.sort_order = 1;
+  formState.status = 1;
+  editingProcessId.value = null;
+}
+
+async function deleteProcess(id: number) {
+  try {
+    await processApi.deleteProcess(id);
+    message.success('工序已删除');
+    loadProcesses();
+  } catch (error) {
+    message.error('删除工序失败');
+  }
+}
+
+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>

+ 283 - 0
apps/factory-app/src/views/RecordsView.vue

@@ -0,0 +1,283 @@
+<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"
+      >
+        <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>
+  </SidebarLayout>
+</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 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,
+  tokenKey: STORAGE_KEYS.FACTORY_TOKEN
+});
+
+const recordApi = new RecordApi(apiClient);
+const processApi = new ProcessApi(apiClient);
+
+const records = ref<PieceRecord[]>([]);
+const processes = ref<Process[]>([]);
+const loading = ref(false);
+const searchKeyword = ref('');
+const filterDate = ref<dayjs.Dayjs | null>(null);
+
+const pagination = reactive({
+  current: 1,
+  pageSize: 10,
+  total: 0,
+  showSizeChanger: true,
+  showTotal: (total: number) => `共 ${total} 条`
+});
+
+const columns = [
+  { title: 'ID', dataIndex: 'id', key: 'id', width: 80 },
+  { title: '员工', dataIndex: 'user_name', key: 'user_name', width: 150 },
+  { title: '工单号', dataIndex: 'order_no', key: 'order_no', width: 150 },
+  { title: '工序', dataIndex: 'process_name', key: 'process_name', width: 150 },
+  { title: '数量', dataIndex: 'quantity', key: 'quantity', width: 100 },
+  { title: '单价', dataIndex: 'price', key: 'price', width: 100 },
+  { title: '金额', key: 'amount', width: 100, slots: { customRender: 'amount' } },
+  { title: '日期', dataIndex: 'record_date', key: 'record_date', width: 120 },
+  { title: '状态', dataIndex: 'status', key: 'status', width: 100, slots: { customRender: 'status' } },
+  { title: '操作', key: 'action', fixed: 'right', width: 150, slots: { customRender: 'action' } }
+];
+
+const scanModalVisible = ref(false);
+const scanning = ref(false);
+const scanFormRef = ref();
+
+const scanState = reactive({
+  qr_code: '',
+  process_id: null as number | null,
+  quantity: 1
+});
+
+const scanRules = {
+  qr_code: [{ required: true, message: '请输入二维码内容', trigger: 'blur' }],
+  process_id: [{ required: true, message: '请选择工序', trigger: 'change' }],
+  quantity: [{ required: true, message: '请输入数量', trigger: 'blur' }]
+};
+
+async function loadRecords() {
+  loading.value = true;
+  try {
+    const result = await recordApi.getRecords({
+      page: pagination.current,
+      page_size: pagination.pageSize,
+      date: filterDate.value ? formatDate(filterDate.value.toDate()) : undefined,
+      keyword: searchKeyword.value
+    });
+    records.value = result.list;
+    pagination.total = result.total;
+  } catch (error) {
+    message.error('加载计件记录失败');
+  } finally {
+    loading.value = false;
+  }
+}
+
+async function loadProcesses() {
+  try {
+    processes.value = await processApi.getProcesses();
+  } catch (error) {
+    message.error('加载工序列表失败');
+  }
+}
+
+function handleTableChange(pag: any) {
+  pagination.current = pag.current;
+  pagination.pageSize = pag.pageSize;
+  loadRecords();
+}
+
+function showScanModal() {
+  scanModalVisible.value = true;
+  resetScanForm();
+}
+
+async function handleScanSubmit() {
+  try {
+    await scanFormRef.value.validate();
+    scanning.value = true;
+
+    await recordApi.scanPiece({
+      qr_code: scanState.qr_code,
+      process_id: scanState.process_id!,
+      quantity: scanState.quantity
+    });
+
+    message.success('计件成功');
+    scanModalVisible.value = false;
+    loadRecords();
+  } catch (error: any) {
+    const errorMsg = error.response?.data?.msg || '计件失败';
+    message.error(errorMsg);
+  } finally {
+    scanning.value = false;
+  }
+}
+
+function resetScanForm() {
+  scanFormRef.value?.resetFields();
+  scanState.qr_code = '';
+  scanState.process_id = null;
+  scanState.quantity = 1;
+}
+
+async function toggleRecordStatus(record: PieceRecord) {
+  try {
+    if (record.status === 'normal') {
+      await recordApi.revertRecord(record.id);
+      message.success('记录已退回');
+    } else {
+      await recordApi.unrevertRecord(record.id);
+      message.success('已撤销退回');
+    }
+    loadRecords();
+  } catch (error) {
+    message.error('操作失败');
+  }
+}
+
+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>

+ 252 - 0
apps/factory-app/src/views/SalaryView.vue

@@ -0,0 +1,252 @@
+<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"
+      >
+        <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"
+        >
+          <template #amount="{ record }">
+            ¥{{ record.total_amount.toFixed(2) }}
+          </template>
+        </a-table>
+      </a-modal>
+    </div>
+  </SidebarLayout>
+</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 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,
+  tokenKey: STORAGE_KEYS.FACTORY_TOKEN
+});
+
+const salaryApi = new SalaryApi(apiClient);
+
+const salaryPeriods = ref<SalaryPeriod[]>([]);
+const loading = ref(false);
+
+const columns = [
+  { title: '工资周期', dataIndex: 'period', key: 'period', width: 150 },
+  { title: '状态', dataIndex: 'status', key: 'status', width: 120, slots: { customRender: 'status' } },
+  { title: '创建时间', dataIndex: 'created_at', key: 'created_at', width: 200 },
+  { title: '操作', key: 'action', width: 300, slots: { customRender: 'action' } }
+];
+
+const detailColumns = [
+  { title: '员工', dataIndex: 'user_name', key: 'user_name', width: 150 },
+  { title: '总数量', dataIndex: 'total_quantity', key: 'total_quantity', width: 120 },
+  { title: '记录数', dataIndex: 'records_count', key: 'records_count', width: 100 },
+  { title: '工资金额', key: 'amount', width: 150, slots: { customRender: 'amount' } }
+];
+
+const generateModalVisible = ref(false);
+const generating = ref(false);
+const generatePeriod = ref<dayjs.Dayjs | null>(null);
+
+const detailModalVisible = ref(false);
+const currentPeriod = ref('');
+const salaryDetails = ref<SalaryDetailItem[]>([]);
+const detailLoading = ref(false);
+
+async function loadSalaryList() {
+  loading.value = true;
+  try {
+    salaryPeriods.value = await salaryApi.getSalaryList();
+  } catch (error) {
+    message.error('加载工资列表失败');
+  } finally {
+    loading.value = false;
+  }
+}
+
+function showGenerateModal() {
+  generateModalVisible.value = true;
+  generatePeriod.value = dayjs();
+}
+
+async function handleGenerate() {
+  if (!generatePeriod.value) {
+    message.error('请选择工资周期');
+    return;
+  }
+
+  generating.value = true;
+  try {
+    const period = generatePeriod.value.format('YYYY-MM');
+    await salaryApi.generateSalary({ period });
+    message.success('工资已生成');
+    generateModalVisible.value = false;
+    loadSalaryList();
+  } catch (error: any) {
+    message.error(error.response?.data?.msg || '生成工资失败');
+  } finally {
+    generating.value = false;
+  }
+}
+
+async function showDetailModal(period: string) {
+  currentPeriod.value = period;
+  detailModalVisible.value = true;
+  detailLoading.value = true;
+  try {
+    salaryDetails.value = await salaryApi.getSalaryDetail(period);
+  } catch (error) {
+    message.error('加载工资明细失败');
+  } finally {
+    detailLoading.value = false;
+  }
+}
+
+async function lockSalary(period: string) {
+  try {
+    await salaryApi.lockSalary(period);
+    message.success('工资已锁定');
+    loadSalaryList();
+  } catch (error) {
+    message.error('锁定失败');
+  }
+}
+
+async function unlockSalary(period: string) {
+  try {
+    await salaryApi.unlockSalary(period);
+    message.success('工资已解锁');
+    loadSalaryList();
+  } catch (error) {
+    message.error('解锁失败');
+  }
+}
+
+async function deleteSalary(period: string) {
+  try {
+    await salaryApi.deleteSalary(period);
+    message.success('工资周期已删除');
+    loadSalaryList();
+  } catch (error) {
+    message.error('删除失败');
+  }
+}
+
+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>

+ 337 - 0
apps/factory-app/src/views/UsersView.vue

@@ -0,0 +1,337 @@
+<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>
+
+      <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-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>
+  </SidebarLayout>
+</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 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,
+  tokenKey: STORAGE_KEYS.FACTORY_TOKEN
+});
+
+const userApi = new UserApi(apiClient);
+
+const users = ref<User[]>([]);
+const loading = ref(false);
+const searchKeyword = ref('');
+
+const pagination = reactive({
+  current: 1,
+  pageSize: 10,
+  total: 0,
+  showSizeChanger: true,
+  showTotal: (total: number) => `共 ${total} 条`
+});
+
+const columns = [
+  { title: 'ID', dataIndex: 'id', key: 'id', width: 80 },
+  { title: '用户名', dataIndex: 'username', key: 'username', width: 150 },
+  { title: '姓名', dataIndex: 'name', key: 'name', width: 150 },
+  { title: '手机号', dataIndex: 'phone', key: 'phone', width: 150 },
+  { title: '角色', dataIndex: 'role', key: 'role', width: 120, slots: { customRender: 'role' } },
+  { title: '状态', dataIndex: 'status', key: 'status', width: 100, slots: { customRender: 'status' } },
+  { title: '操作', key: 'action', width: 250, slots: { customRender: 'action' } }
+];
+
+const modalVisible = ref(false);
+const modalTitle = ref('创建用户');
+const isEdit = ref(false);
+const submitting = ref(false);
+const formRef = ref();
+const editingId = ref<number | null>(null);
+
+const formState = reactive({
+  username: '',
+  name: '',
+  phone: '',
+  role: 'worker' as 'factory_admin' | 'worker',
+  password: '',
+  status: 1
+});
+
+const formRules = {
+  username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
+  name: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
+  phone: [
+    { required: true, message: '请输入手机号', trigger: 'blur' },
+    { pattern: /^1[3-9]\d{9}$/, message: '手机号格式不正确', trigger: 'blur' }
+  ],
+  password: [
+    { required: true, message: '请输入密码', trigger: 'blur' },
+    {
+      validator: (_rule: any, value: string) => {
+        if (!validatePasswordStrength(value)) return Promise.reject('密码强度不足');
+        return Promise.resolve();
+      },
+      trigger: 'blur'
+    }
+  ]
+};
+
+const resetPwdModalVisible = ref(false);
+const resetting = ref(false);
+const resetFormRef = ref();
+const resetUserId = ref<number | null>(null);
+const resetState = reactive({ password: '' });
+const resetRules = {
+  password: [
+    { required: true, message: '请输入新密码', trigger: 'blur' },
+    {
+      validator: (_rule: any, value: string) => {
+        if (!validatePasswordStrength(value)) return Promise.reject('密码强度不足');
+        return Promise.resolve();
+      },
+      trigger: 'blur'
+    }
+  ]
+};
+
+async function loadUsers() {
+  loading.value = true;
+  try {
+    const result = await userApi.getUsers({
+      page: pagination.current,
+      page_size: pagination.pageSize,
+      keyword: searchKeyword.value
+    });
+    users.value = result.list;
+    pagination.total = result.total;
+  } catch (error) {
+    message.error('加载用户列表失败');
+  } finally {
+    loading.value = false;
+  }
+}
+
+function handleTableChange(pag: any) {
+  pagination.current = pag.current;
+  pagination.pageSize = pag.pageSize;
+  loadUsers();
+}
+
+function showCreateModal() {
+  modalTitle.value = '创建用户';
+  isEdit.value = false;
+  modalVisible.value = true;
+  resetForm();
+}
+
+function showEditModal(record: User) {
+  modalTitle.value = '编辑用户';
+  isEdit.value = true;
+  editingId.value = record.id;
+  modalVisible.value = true;
+  formState.username = record.username;
+  formState.name = record.name;
+  formState.phone = record.phone;
+  formState.role = record.role;
+  formState.status = record.status;
+}
+
+async function handleSubmit() {
+  try {
+    await formRef.value.validate();
+    submitting.value = true;
+    if (isEdit.value && editingId.value) {
+      await userApi.updateUser(editingId.value, {
+        name: formState.name,
+        phone: formState.phone,
+        status: formState.status
+      });
+      message.success('用户已更新');
+    } else {
+      await userApi.createUser({
+        username: formState.username,
+        name: formState.name,
+        phone: formState.phone,
+        role: formState.role,
+        password: formState.password
+      });
+      message.success('用户已创建');
+    }
+    modalVisible.value = false;
+    loadUsers();
+  } catch (error: any) {
+    message.error(error.response?.data?.msg || '操作失败');
+  } finally {
+    submitting.value = false;
+  }
+}
+
+function resetForm() {
+  formRef.value?.resetFields();
+  formState.username = '';
+  formState.name = '';
+  formState.phone = '';
+  formState.role = 'worker';
+  formState.password = '';
+  formState.status = 1;
+  editingId.value = null;
+}
+
+function showResetPasswordModal(record: User) {
+  resetUserId.value = record.id;
+  resetPwdModalVisible.value = true;
+  resetFormRef.value?.resetFields();
+}
+
+async function handleResetPassword() {
+  try {
+    await resetFormRef.value.validate();
+    resetting.value = true;
+    await userApi.resetPassword(resetUserId.value!, { password: resetState.password });
+    message.success('密码已重置');
+    resetPwdModalVisible.value = false;
+  } catch (error) {
+    message.error('重置密码失败');
+  } finally {
+    resetting.value = false;
+  }
+}
+
+async function deleteUser(id: number) {
+  try {
+    await userApi.deleteUser(id);
+    message.success('用户已删除');
+    loadUsers();
+  } catch (error) {
+    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; }
+</style>

+ 388 - 0
apps/factory-app/src/views/WorkOrdersView.vue

@@ -0,0 +1,388 @@
+<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"
+        >
+          <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"
+        >
+          <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="qr-code-container">
+          <img :src="qrCodeUrl" alt="工单二维码" style="width: 100%" />
+          <p class="qr-code-tip">请使用手机扫描此二维码进行计件</p>
+        </div>
+      </a-modal>
+    </div>
+  </SidebarLayout>
+</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 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,
+  tokenKey: STORAGE_KEYS.FACTORY_TOKEN
+});
+
+const workOrderApi = new WorkOrderApi(apiClient);
+
+const workOrders = ref<WorkOrder[]>([]);
+const loading = ref(false);
+const searchKeyword = ref('');
+const filterStatus = ref('');
+
+const pagination = reactive({
+  current: 1,
+  pageSize: 10,
+  total: 0,
+  showSizeChanger: true,
+  showTotal: (total: number) => `共 ${total} 条`
+});
+
+const columns = [
+  { title: 'ID', dataIndex: 'id', key: 'id', width: 80 },
+  { title: '工单号', dataIndex: 'order_no', key: 'order_no', width: 150 },
+  { title: '款号', dataIndex: 'style', key: 'style', width: 120 },
+  { title: '款号名称', dataIndex: 'style_name', key: 'style_name', width: 200 },
+  { title: '总数量', dataIndex: 'total_qty', key: 'total_qty', width: 100 },
+  { title: '完成数量', dataIndex: 'completed_qty', key: 'completed_qty', width: 100 },
+  { title: '进度', key: 'progress', width: 150, slots: { customRender: 'progress' } },
+  { title: '状态', dataIndex: 'status', key: 'status', width: 100, slots: { customRender: 'status' } },
+  { title: '操作', key: 'action', fixed: 'right', width: 200, slots: { customRender: 'action' } }
+];
+
+const modalVisible = ref(false);
+const modalTitle = ref('创建工单');
+const isEdit = ref(false);
+const submitting = ref(false);
+const formRef = ref();
+const editingOrderId = ref<number | null>(null);
+
+const formState = reactive({
+  order_no: '',
+  style: '',
+  style_name: '',
+  total_qty: 1,
+  remark: ''
+});
+
+const formRules = {
+  order_no: [{ required: true, message: '请输入工单号', trigger: 'blur' }],
+  style: [
+    { required: true, message: '请输入款号', trigger: 'blur' },
+    { pattern: /^[a-zA-Z0-9_-]+$/, message: '款号只能包含字母、数字、下划线或横线', trigger: 'blur' }
+  ],
+  style_name: [{ required: true, message: '请输入款号名称', trigger: 'blur' }],
+  total_qty: [{ required: true, message: '请输入总数量', trigger: 'blur' }]
+};
+
+const qrCodeModalVisible = ref(false);
+const qrCodeUrl = ref('');
+
+function getStatusColor(status: string) {
+  const colors: Record<string, string> = {
+    pending: 'default',
+    in_progress: 'blue',
+    completed: 'green',
+    cancelled: 'red'
+  };
+  return colors[status] || 'default';
+}
+
+function getStatusText(status: string) {
+  const texts: Record<string, string> = {
+    pending: '待开始',
+    in_progress: '进行中',
+    completed: '已完成',
+    cancelled: '已取消'
+  };
+  return texts[status] || status;
+}
+
+async function loadWorkOrders() {
+  loading.value = true;
+  try {
+    const result = await workOrderApi.getWorkOrders({
+      page: pagination.current,
+      page_size: pagination.pageSize,
+      status: filterStatus.value,
+      keyword: searchKeyword.value
+    });
+    workOrders.value = result.list;
+    pagination.total = result.total;
+  } catch (error) {
+    message.error('加载工单列表失败');
+  } finally {
+    loading.value = false;
+  }
+}
+
+function handleTableChange(pag: any) {
+  pagination.current = pag.current;
+  pagination.pageSize = pag.pageSize;
+  loadWorkOrders();
+}
+
+function showCreateModal() {
+  modalTitle.value = '创建工单';
+  isEdit.value = false;
+  modalVisible.value = true;
+  resetForm();
+}
+
+function showEditModal(record: WorkOrder) {
+  modalTitle.value = '编辑工单';
+  isEdit.value = true;
+  editingOrderId.value = record.id;
+  modalVisible.value = true;
+
+  formState.order_no = record.order_no;
+  formState.style = record.style;
+  formState.style_name = record.style_name;
+  formState.total_qty = record.total_qty;
+  formState.remark = record.remark;
+}
+
+async function handleSubmit() {
+  try {
+    await formRef.value.validate();
+    submitting.value = true;
+
+    if (isEdit.value && editingOrderId.value) {
+      await workOrderApi.updateWorkOrder(editingOrderId.value, {
+        style_name: formState.style_name,
+        total_qty: formState.total_qty,
+        remark: formState.remark
+      });
+      message.success('工单已更新');
+    } else {
+      await workOrderApi.createWorkOrder({
+        order_no: formState.order_no,
+        style: formState.style,
+        style_name: formState.style_name,
+        total_qty: formState.total_qty,
+        remark: formState.remark
+      });
+      message.success('工单已创建');
+    }
+
+    modalVisible.value = false;
+    loadWorkOrders();
+  } catch (error: any) {
+    const errorMsg = error.response?.data?.msg || '操作失败';
+    message.error(errorMsg);
+  } finally {
+    submitting.value = false;
+  }
+}
+
+function resetForm() {
+  formRef.value?.resetFields();
+  formState.order_no = '';
+  formState.style = '';
+  formState.style_name = '';
+  formState.total_qty = 1;
+  formState.remark = '';
+  editingOrderId.value = null;
+}
+
+async function generateQRCode(id: number) {
+  try {
+    const blob = await workOrderApi.getWorkOrderQRCode(id);
+    qrCodeUrl.value = URL.createObjectURL(blob);
+    qrCodeModalVisible.value = true;
+  } catch (error) {
+    message.error('生成二维码失败');
+  }
+}
+
+async function deleteWorkOrder(id: number) {
+  try {
+    await workOrderApi.deleteWorkOrder(id);
+    message.success('工单已删除');
+    loadWorkOrders();
+  } catch (error) {
+    message.error('删除工单失败');
+  }
+}
+
+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>

+ 18 - 0
apps/factory-app/tailwind.config.js

@@ -0,0 +1,18 @@
+module.exports = {
+  content: [
+    './index.html',
+    './src/**/*.{vue,js,ts,jsx,tsx}',
+    '../../packages/shared-components/src/**/*.{vue,js,ts,jsx,tsx}'
+  ],
+  theme: {
+    extend: {
+      colors: {
+        primary: '#1e3a5f',
+        accent: '#f5a623'
+      }
+    }
+  },
+  corePlugins: {
+    preflight: false
+  }
+};

+ 33 - 0
apps/factory-app/tsconfig.json

@@ -0,0 +1,33 @@
+{
+  "extends": "../../tsconfig.base.json",
+  "compilerOptions": {
+    "target": "ES2020",
+    "useDefineForClassFields": true,
+    "module": "ESNext",
+    "lib": ["ES2020", "DOM", "DOM.Iterable"],
+    "skipLibCheck": true,
+
+    "moduleResolution": "bundler",
+    "allowImportingTsExtensions": true,
+    "resolveJsonModule": true,
+    "isolatedModules": true,
+    "noEmit": true,
+    "jsx": "preserve",
+
+    "strict": true,
+    "noUnusedLocals": true,
+    "noUnusedParameters": true,
+    "noFallthroughCasesInSwitch": true,
+
+    "baseUrl": ".",
+    "paths": {
+      "@/*": ["src/*"],
+      "@smartcut/types": ["../../packages/types/src"],
+      "@smartcut/api-client": ["../../packages/api-client/src"],
+      "@smartcut/shared-utils": ["../../packages/shared-utils/src"],
+      "@smartcut/shared-components": ["../../packages/shared-components/src"]
+    }
+  },
+  "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"],
+  "references": [{ "path": "./tsconfig.node.json" }]
+}

+ 10 - 0
apps/factory-app/tsconfig.node.json

@@ -0,0 +1,10 @@
+{
+  "compilerOptions": {
+    "composite": true,
+    "skipLibCheck": true,
+    "module": "ESNext",
+    "moduleResolution": "bundler",
+    "allowSyntheticDefaultImports": true
+  },
+  "include": ["vite.config.ts"]
+}

+ 39 - 0
apps/factory-app/vite.config.ts

@@ -0,0 +1,39 @@
+import { defineConfig } from 'vite';
+import vue from '@vitejs/plugin-vue';
+import { resolve } from 'path';
+
+export default defineConfig({
+  plugins: [vue()],
+  resolve: {
+    alias: {
+      '@': resolve(__dirname, 'src'),
+      '@smartcut/types': resolve(__dirname, '../../packages/types/src'),
+      '@smartcut/api-client': resolve(__dirname, '../../packages/api-client/src'),
+      '@smartcut/shared-utils': resolve(__dirname, '../../packages/shared-utils/src'),
+      '@smartcut/shared-components': resolve(__dirname, '../../packages/shared-components/src')
+    }
+  },
+  server: {
+    port: 3001,
+    proxy: {
+      '/api': {
+        target: 'http://localhost:8080',
+        changeOrigin: true
+      }
+    }
+  },
+  build: {
+    outDir: 'dist',
+    sourcemap: false,
+    rollupOptions: {
+      output: {
+        manualChunks: {
+          'vue-vendor': ['vue', 'vue-router', 'pinia'],
+          'antd-vendor': ['ant-design-vue', '@ant-design/icons-vue'],
+          'echarts-vendor': ['echarts'],
+          'shared-vendor': ['axios']
+        }
+      }
+    }
+  }
+});

+ 3 - 0
apps/platform-app/eslint.config.js

@@ -0,0 +1,3 @@
+const baseConfig = require('../../eslint.config.base.js');
+
+module.exports = [...baseConfig];

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

@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+  <head>
+    <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" />
+    <title>智裁云 - 系统管理后台</title>
+  </head>
+  <body>
+    <div id="app"></div>
+    <script type="module" src="/src/main.ts"></script>
+  </body>
+</html>

+ 41 - 0
apps/platform-app/package.json

@@ -0,0 +1,41 @@
+{
+  "name": "platform-app",
+  "version": "1.0.0",
+  "private": true,
+  "type": "module",
+  "scripts": {
+    "dev": "vite",
+    "build": "vite build",
+    "type-check": "vue-tsc --noEmit",
+    "preview": "vite preview",
+    "lint": "eslint . --fix",
+    "format": "prettier --write src/"
+  },
+  "dependencies": {
+    "vue": "^3.4.0",
+    "vue-router": "^4.2.0",
+    "pinia": "^2.1.0",
+    "ant-design-vue": "^4.0.0",
+    "axios": "^1.6.0",
+    "@ant-design/icons-vue": "^7.0.0",
+    "@smartcut/types": "workspace:*",
+    "@smartcut/api-client": "workspace:*",
+    "@smartcut/shared-utils": "workspace:*",
+    "@smartcut/shared-components": "workspace:*"
+  },
+  "devDependencies": {
+    "@vitejs/plugin-vue": "^5.0.0",
+    "typescript": "^5.3.0",
+    "vite": "^5.0.0",
+    "vue-tsc": "^1.8.0",
+    "eslint": "^9.0.0",
+    "@eslint/js": "^9.0.0",
+    "typescript-eslint": "^8.0.0",
+    "eslint-plugin-vue": "^9.30.0",
+    "vue-eslint-parser": "^9.4.0",
+    "prettier": "^3.2.0",
+    "tailwindcss": "^3.4.0",
+    "autoprefixer": "^10.4.0",
+    "postcss": "^8.4.0"
+  }
+}

+ 19 - 0
apps/platform-app/src/App.vue

@@ -0,0 +1,19 @@
+<template>
+  <a-config-provider :locale="zhCN">
+    <router-view />
+  </a-config-provider>
+</template>
+
+<script setup lang="ts">
+import zhCN from 'ant-design-vue/es/locale/zh_CN';
+</script>
+
+<style>
+#app {
+  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,
+    'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
+    'Noto Color Emoji';
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
+</style>

+ 20 - 0
apps/platform-app/src/main.ts

@@ -0,0 +1,20 @@
+import { createApp } from 'vue';
+import { createPinia } from 'pinia';
+import Antd from 'ant-design-vue';
+import 'ant-design-vue/dist/reset.css';
+import App from './App.vue';
+import router from './router';
+
+// TailwindCSS
+import './styles/main.css';
+
+// 创建应用
+const app = createApp(App);
+
+// 注册插件
+app.use(createPinia());
+app.use(router);
+app.use(Antd);
+
+// 挂载应用
+app.mount('#app');

+ 69 - 0
apps/platform-app/src/router/index.ts

@@ -0,0 +1,69 @@
+// 系统管理后台路由配置
+
+import { createRouter, createWebHistory } from 'vue-router';
+import { useAuthStore } from '@/stores/auth';
+
+const routes = [
+  {
+    path: '/login',
+    name: 'Login',
+    component: () => import('@/views/LoginView.vue'),
+    meta: { requiresAuth: false }
+  },
+  {
+    path: '/',
+    name: 'Dashboard',
+    component: () => import('@/views/DashboardView.vue'),
+    meta: { requiresAuth: true }
+  },
+  {
+    path: '/factories',
+    name: 'Factories',
+    component: () => import('@/views/FactoriesView.vue'),
+    meta: { requiresAuth: true }
+  },
+  {
+    path: '/users',
+    name: 'Users',
+    component: () => import('@/views/UsersView.vue'),
+    meta: { requiresAuth: true }
+  },
+  {
+    path: '/change-password',
+    name: 'ChangePassword',
+    component: () => import('@/views/ChangePasswordView.vue'),
+    meta: { requiresAuth: true }
+  },
+  {
+    path: '/:pathMatch(.*)*',
+    name: 'NotFound',
+    component: () => import('@/views/NotFoundView.vue'),
+    meta: { requiresAuth: false }
+  }
+];
+
+const router = createRouter({
+  history: createWebHistory(),
+  routes
+});
+
+// 路由守卫:认证检查
+router.beforeEach((to, from, next) => {
+  const authStore = useAuthStore();
+
+  // 需要认证的路由
+  if (to.meta.requiresAuth && !authStore.isLoggedIn) {
+    // 未登录时跳转登录页,携带redirect参数
+    next({
+      name: 'Login',
+      query: { redirect: to.fullPath }
+    });
+  } else if (to.name === 'Login' && authStore.isLoggedIn) {
+    // 已登录时访问登录页,跳转首页
+    next({ name: 'Dashboard' });
+  } else {
+    next();
+  }
+});
+
+export default router;

+ 104 - 0
apps/platform-app/src/stores/auth.ts

@@ -0,0 +1,104 @@
+// 系统管理后台认证状态管理
+
+import { defineStore } from 'pinia';
+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';
+
+export const useAuthStore = defineStore('auth', () => {
+  // 状态定义
+  const token = ref<string | null>(getSessionStorage<string>(STORAGE_KEYS.PLATFORM_TOKEN));
+  const user = ref<SystemUser | null>(getSessionStorage<SystemUser>(STORAGE_KEYS.PLATFORM_USER));
+  const isLoggedIn = computed(() => !!token.value && !!user.value);
+
+  // 创建API客户端
+  const apiClient = createApiClient({
+    baseURL: window.location.origin,
+    tokenKey: STORAGE_KEYS.PLATFORM_TOKEN
+  });
+
+  const authApi = new AuthApi(apiClient);
+
+  /**
+   * 系统管理员登录
+   */
+  async function login(username: string, password: string) {
+    try {
+      const response = await authApi.login({
+        username,
+        password
+      });
+
+      // 存储Token和用户信息到SessionStorage
+      token.value = response.token;
+      user.value = response.user as SystemUser;
+
+      setSessionStorage(STORAGE_KEYS.PLATFORM_TOKEN, response.token);
+      setSessionStorage(STORAGE_KEYS.PLATFORM_USER, response.user);
+
+      return response;
+    } catch (error) {
+      console.error('登录失败:', error);
+      throw error;
+    }
+  }
+
+  /**
+   * 登出
+   */
+  function logout() {
+    token.value = null;
+    user.value = null;
+
+    removeSessionStorage(STORAGE_KEYS.PLATFORM_TOKEN);
+    removeSessionStorage(STORAGE_KEYS.PLATFORM_USER);
+  }
+
+  /**
+   * 获取用户信息
+   */
+  async function fetchProfile() {
+    if (!token.value) return;
+
+    try {
+      const profile = await authApi.getProfile();
+      user.value = profile as SystemUser;
+      setSessionStorage(STORAGE_KEYS.PLATFORM_USER, profile);
+    } catch (error) {
+      console.error('获取用户信息失败:', error);
+      logout();
+    }
+  }
+
+  /**
+   * 修改密码
+   */
+  async function changePassword(oldPassword: string, newPassword: string) {
+    try {
+      await authApi.changePassword({
+        old_password: oldPassword,
+        new_password: newPassword
+      });
+
+      // 如果成功修改密码,清除must_change_password标记
+      if (user.value) {
+        user.value.must_change_password = false;
+        setSessionStorage(STORAGE_KEYS.PLATFORM_USER, user.value);
+      }
+    } catch (error) {
+      console.error('修改密码失败:', error);
+      throw error;
+    }
+  }
+
+  return {
+    token,
+    user,
+    isLoggedIn,
+    login,
+    logout,
+    fetchProfile,
+    changePassword
+  };
+});

+ 38 - 0
apps/platform-app/src/styles/main.css

@@ -0,0 +1,38 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+/* 自定义主题色 */
+:root {
+  --ant-primary-color: #1e3a5f;
+  --ant-primary-color-hover: #2a4a6f;
+  --ant-primary-color-active: #15304f;
+}
+
+/* 全局样式调整 */
+.ant-btn-primary {
+  background-color: #1e3a5f;
+}
+
+.ant-btn-primary:hover {
+  background-color: #2a4a6f;
+}
+
+.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;
+}

+ 186 - 0
apps/platform-app/src/views/ChangePasswordView.vue

@@ -0,0 +1,186 @@
+<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="请输入新密码(至少8位,包含大小写字母、数字、特殊字符中3类)"
+              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>
+</template>
+
+<script setup lang="ts">
+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();
+
+const formState = reactive({
+  old_password: '',
+  new_password: '',
+  confirm_password: ''
+});
+
+const formRules = {
+  old_password: [
+    { required: true, message: '请输入旧密码', trigger: 'blur' }
+  ],
+  new_password: [
+    { required: true, message: '请输入新密码', trigger: 'blur' },
+    {
+      validator: (rule: any, value: string) => {
+        if (!validatePasswordStrength(value)) {
+          return Promise.reject('密码强度不足');
+        }
+        return Promise.resolve();
+      },
+      trigger: 'blur'
+    }
+  ],
+  confirm_password: [
+    { required: true, message: '请确认新密码', trigger: 'blur' },
+    {
+      validator: (rule: any, value: string) => {
+        if (value !== formState.new_password) {
+          return Promise.reject('两次输入的密码不一致');
+        }
+        return Promise.resolve();
+      },
+      trigger: 'blur'
+    }
+  ]
+};
+
+// 提交表单
+async function handleSubmit() {
+  try {
+    submitting.value = true;
+
+    await authStore.changePassword(formState.old_password, formState.new_password);
+    
+    message.success('密码修改成功');
+    
+    // 如果是强制修改密码场景,修改成功后跳转首页
+    if (authStore.user?.must_change_password) {
+      router.push('/');
+    } else {
+      resetForm();
+    }
+  } catch (error: any) {
+    const errorMsg = error.response?.data?.msg || '修改密码失败';
+    message.error(errorMsg);
+  } finally {
+    submitting.value = false;
+  }
+}
+
+// 重置表单
+function resetForm() {
+  formRef.value?.resetFields();
+  formState.old_password = '';
+  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>

+ 177 - 0
apps/platform-app/src/views/DashboardView.vue

@@ -0,0 +1,177 @@
+<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>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, 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 { STORAGE_KEYS } 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 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,
+  user_count: 0,
+  log_count: 0
+});
+
+// API客户端
+const apiClient = createApiClient({
+  baseURL: window.location.origin,
+  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; // 暂时硬编码
+  } 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>

+ 358 - 0
apps/platform-app/src/views/FactoriesView.vue

@@ -0,0 +1,358 @@
+<template>
+  <SidebarLayout
+    :menuItems="menuItems"
+    :userName="authStore.user?.name"
+    :activeKey="'factories'"
+    @logout="handleLogout"
+  >
+    <div class="factories-page">
+      <!-- 头部工具栏 -->
+      <a-card>
+        <a-space>
+          <a-input-search
+            v-model:value="searchKeyword"
+            placeholder="搜索工厂名称"
+            style="width: 300px"
+            @search="loadFactories"
+          />
+          <a-button type="primary" @click="showCreateModal">
+            创建工厂
+          </a-button>
+        </a-space>
+      </a-card>
+
+      <!-- 工厂列表表格 -->
+      <a-card class="mt-4">
+        <a-table
+          :columns="columns"
+          :dataSource="factories"
+          :loading="loading"
+          :pagination="pagination"
+          @change="handleTableChange"
+          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-button
+                size="small"
+                :type="record.status === 1 ? 'default' : 'primary'"
+                @click="toggleFactoryStatus(record)"
+              >
+                {{ record.status === 1 ? '禁用' : '启用' }}
+              </a-button>
+              <a-popconfirm
+                title="确定删除此工厂吗?此操作将永久删除工厂数据库文件!"
+                ok-text="确定"
+                cancel-text="取消"
+                @confirm="deleteFactory(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-form
+          ref="formRef"
+          :model="formState"
+          :rules="formRules"
+          layout="vertical"
+        >
+          <a-form-item label="工厂ID" name="id" v-if="!isEdit">
+            <a-input
+              v-model:value="formState.id"
+              placeholder="请输入工厂ID(字母或数字)"
+            />
+          </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="db_path" v-if="!isEdit">
+            <a-input
+              v-model:value="formState.db_path"
+              placeholder="可选,默认自动生成"
+            />
+          </a-form-item>
+        </a-form>
+      </a-modal>
+    </div>
+  </SidebarLayout>
+</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 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,
+  tokenKey: STORAGE_KEYS.PLATFORM_TOKEN
+});
+
+const factoryApi = new FactoryApi(apiClient);
+
+// 工厂列表数据
+const factories = ref<Factory[]>([]);
+const loading = ref(false);
+const searchKeyword = ref('');
+
+const pagination = reactive({
+  current: 1,
+  pageSize: 10,
+  total: 0,
+  showSizeChanger: true,
+  showTotal: (total: number) => `共 ${total} 条`
+});
+
+// 表格列定义
+const columns = [
+  {
+    title: '工厂ID',
+    dataIndex: 'id',
+    key: 'id',
+    width: 120
+  },
+  {
+    title: '工厂名称',
+    dataIndex: 'name',
+    key: 'name',
+    width: 200
+  },
+  {
+    title: '状态',
+    dataIndex: 'status',
+    key: 'status',
+    width: 100,
+    slots: { customRender: 'status' }
+  },
+  {
+    title: '创建时间',
+    dataIndex: 'created_at',
+    key: 'created_at',
+    width: 180
+  },
+  {
+    title: '操作',
+    key: 'action',
+    fixed: 'right',
+    width: 200,
+    slots: { customRender: 'action' }
+  }
+];
+
+// 模态框
+const modalVisible = ref(false);
+const modalTitle = ref('创建工厂');
+const isEdit = ref(false);
+const submitting = ref(false);
+const formRef = ref();
+
+const formState = reactive({
+  id: '',
+  name: '',
+  db_path: ''
+});
+
+const formRules = {
+  id: [
+    { required: true, message: '请输入工厂ID', trigger: 'blur' },
+    { pattern: /^[a-zA-Z0-9_-]+$/, message: '工厂ID只能包含字母、数字、下划线或横线', trigger: 'blur' }
+  ],
+  name: [
+    { required: true, message: '请输入工厂名称', trigger: 'blur' }
+  ]
+};
+
+// 加载工厂列表
+async function loadFactories() {
+  loading.value = true;
+  try {
+    const result = await factoryApi.listFactories();
+    factories.value = result.list;
+    pagination.total = result.total;
+  } catch (error) {
+    message.error('加载工厂列表失败');
+  } finally {
+    loading.value = false;
+  }
+}
+
+// 表格分页/筛选变化
+function handleTableChange(pag: any) {
+  pagination.current = pag.current;
+  pagination.pageSize = pag.pageSize;
+  loadFactories();
+}
+
+// 显示创建模态框
+function showCreateModal() {
+  modalTitle.value = '创建工厂';
+  isEdit.value = false;
+  modalVisible.value = true;
+  resetForm();
+}
+
+// 显示编辑模态框
+function showEditModal(record: Factory) {
+  modalTitle.value = '编辑工厂';
+  isEdit.value = true;
+  modalVisible.value = true;
+  
+  // 填充表单数据
+  formState.id = record.id;
+  formState.name = record.name;
+  formState.db_path = record.db_path;
+}
+
+// 提交表单
+async function handleSubmit() {
+  try {
+    await formRef.value.validate();
+    submitting.value = true;
+
+    if (isEdit.value) {
+      // 编辑工厂
+      await factoryApi.updateFactory(formState.id, {
+        name: formState.name
+      });
+      message.success('工厂已更新');
+    } else {
+      // 创建工厂
+      await factoryApi.createFactory({
+        id: formState.id,
+        name: formState.name,
+        db_path: formState.db_path
+      });
+      message.success('工厂已创建');
+    }
+
+    modalVisible.value = false;
+    loadFactories();
+  } catch (error) {
+    if (error.response?.data?.msg) {
+      message.error(error.response.data.msg);
+    } else {
+      message.error(isEdit.value ? '更新工厂失败' : '创建工厂失败');
+    }
+  } finally {
+    submitting.value = false;
+  }
+}
+
+// 重置表单
+function resetForm() {
+  formRef.value?.resetFields();
+  formState.id = '';
+  formState.name = '';
+  formState.db_path = '';
+}
+
+// 切换工厂状态(启用/禁用)
+async function toggleFactoryStatus(record: Factory) {
+  try {
+    if (record.status === 1) {
+      // 禁用工厂
+      await factoryApi.deleteFactory(record.id, false);
+      message.success('工厂已禁用');
+    } else {
+      // 启用工厂
+      await factoryApi.enableFactory(record.id);
+      message.success('工厂已启用');
+    }
+    loadFactories();
+  } catch (error) {
+    message.error('操作失败');
+  }
+}
+
+// 删除工厂(永久删除)
+async function deleteFactory(id: string) {
+  try {
+    await factoryApi.deleteFactory(id, true);
+    message.success('工厂已永久删除');
+    loadFactories();
+  } catch (error) {
+    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>

+ 107 - 0
apps/platform-app/src/views/LoginView.vue

@@ -0,0 +1,107 @@
+<template>
+  <div class="login-page">
+    <a-card class="login-card" title="智裁云 - 系统管理后台">
+      <a-form
+        :model="formState"
+        :rules="rules"
+        @finish="handleLogin"
+        layout="vertical"
+      >
+        <a-form-item label="用户名" name="username">
+          <a-input
+            v-model:value="formState.username"
+            placeholder="请输入用户名"
+            size="large"
+          />
+        </a-form-item>
+
+        <a-form-item label="密码" name="password">
+          <a-input-password
+            v-model:value="formState.password"
+            placeholder="请输入密码"
+            size="large"
+          />
+        </a-form-item>
+
+        <a-form-item>
+          <a-button
+            type="primary"
+            html-type="submit"
+            size="large"
+            block
+            :loading="loading"
+          >
+            登录
+          </a-button>
+        </a-form-item>
+      </a-form>
+    </a-card>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive } from 'vue';
+import { useRouter, useRoute } from 'vue-router';
+import { message } from 'ant-design-vue';
+import { useAuthStore } from '@/stores/auth';
+
+const router = useRouter();
+const route = useRoute();
+const authStore = useAuthStore();
+
+const loading = ref(false);
+const formState = reactive({
+  username: '',
+  password: ''
+});
+
+const rules = {
+  username: [
+    { required: true, message: '请输入用户名', trigger: 'blur' }
+  ],
+  password: [
+    { required: true, message: '请输入密码', trigger: 'blur' }
+  ]
+};
+
+async function handleLogin() {
+  loading.value = true;
+
+  try {
+    await authStore.login(formState.username, formState.password);
+    message.success('登录成功');
+
+    // 跳转到目标页面或首页
+    const redirect = route.query.redirect as string;
+    router.push(redirect || '/');
+  } catch (error: any) {
+    const errorMsg = error.response?.data?.msg || '登录失败,请检查用户名和密码';
+    message.error(errorMsg);
+  } finally {
+    loading.value = false;
+  }
+}
+</script>
+
+<style scoped lang="postcss">
+.login-page {
+  height: 100vh;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  background-color: #f0f2f5;
+}
+
+.login-card {
+  width: 400px;
+  border-radius: 8px;
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+}
+
+.login-card .ant-card-head-title {
+  text-align: center;
+  font-size: 18px;
+  font-weight: bold;
+  color: #1e3a5f;
+}
+</style>

+ 30 - 0
apps/platform-app/src/views/NotFoundView.vue

@@ -0,0 +1,30 @@
+<template>
+  <div class="not-found-page">
+    <a-result
+      status="404"
+      title="404"
+      sub-title="抱歉,您访问的页面不存在"
+    >
+      <template #extra>
+        <a-button type="primary" @click="router.push('/')">
+          返回首页
+        </a-button>
+      </template>
+    </a-result>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { useRouter } from 'vue-router';
+
+const router = useRouter();
+</script>
+
+<style scoped lang="postcss">
+.not-found-page {
+  height: 100vh;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
+</style>

+ 502 - 0
apps/platform-app/src/views/UsersView.vue

@@ -0,0 +1,502 @@
+<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>
+
+      <!-- 用户列表表格 -->
+      <a-card class="mt-4">
+        <a-table
+          :columns="columns"
+          :dataSource="users"
+          :loading="loading"
+          :pagination="pagination"
+          @change="handleTableChange"
+          rowKey="id"
+        >
+          <!-- 状态列 -->
+          <template #status="{ record }">
+            <a-tag :color="record.status === 1 ? 'green' : 'red'">
+              {{ record.status === 1 ? '正常' : '禁用' }}
+            </a-tag>
+          </template>
+
+          <!-- 必须修改密码列 -->
+          <template #must_change_password="{ record }">
+            <a-tag :color="record.must_change_password ? 'orange' : 'blue'">
+              {{ record.must_change_password ? '需要修改' : '已修改' }}
+            </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="确定删除此用户吗?"
+                ok-text="确定"
+                cancel-text="取消"
+                @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-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="请输入用户名(2-32位)"
+            />
+          </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="password" v-if="!isEdit">
+            <a-input-password
+              v-model:value="formState.password"
+              placeholder="请输入密码(至少8位,包含大小写字母、数字、特殊字符中3类)"
+            />
+            <div class="password-tips">
+              密码强度要求:至少8位,包含大小写字母、数字、特殊字符中3类
+            </div>
+          </a-form-item>
+
+          <a-form-item label="状态" name="status">
+            <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="resetPasswordModalVisible"
+        title="重置密码"
+        @ok="handleResetPassword"
+        @cancel="resetPasswordForm"
+        :confirmLoading="resettingPassword"
+      >
+        <a-form
+          ref="resetPasswordFormRef"
+          :model="resetPasswordState"
+          :rules="resetPasswordRules"
+          layout="vertical"
+        >
+          <a-form-item label="新密码" name="password">
+            <a-input-password
+              v-model:value="resetPasswordState.password"
+              placeholder="请输入新密码"
+            />
+            <div class="password-tips">
+              密码强度要求:至少8位,包含大小写字母、数字、特殊字符中3类
+            </div>
+          </a-form-item>
+        </a-form>
+      </a-modal>
+    </div>
+  </SidebarLayout>
+</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 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,
+  tokenKey: STORAGE_KEYS.PLATFORM_TOKEN
+});
+
+const userApi = new UserApi(apiClient);
+
+// 用户列表数据
+const users = ref<SystemUser[]>([]);
+const loading = ref(false);
+const searchKeyword = ref('');
+
+const pagination = reactive({
+  current: 1,
+  pageSize: 10,
+  total: 0,
+  showSizeChanger: true,
+  showTotal: (total: number) => `共 ${total} 条`
+});
+
+// 表格列定义
+const columns = [
+  {
+    title: 'ID',
+    dataIndex: 'id',
+    key: 'id',
+    width: 80
+  },
+  {
+    title: '用户名',
+    dataIndex: 'username',
+    key: 'username',
+    width: 150
+  },
+  {
+    title: '姓名',
+    dataIndex: 'name',
+    key: 'name',
+    width: 150
+  },
+  {
+    title: '手机号',
+    dataIndex: 'phone',
+    key: 'phone',
+    width: 150
+  },
+  {
+    title: '状态',
+    dataIndex: 'status',
+    key: 'status',
+    width: 100,
+    slots: { customRender: 'status' }
+  },
+  {
+    title: '修改密码',
+    dataIndex: 'must_change_password',
+    key: 'must_change_password',
+    width: 120,
+    slots: { customRender: 'must_change_password' }
+  },
+  {
+    title: '创建时间',
+    dataIndex: 'created_at',
+    key: 'created_at',
+    width: 180
+  },
+  {
+    title: '操作',
+    key: 'action',
+    fixed: 'right',
+    width: 200,
+    slots: { customRender: 'action' }
+  }
+];
+
+// 模态框
+const modalVisible = ref(false);
+const modalTitle = ref('创建用户');
+const isEdit = ref(false);
+const submitting = ref(false);
+const formRef = ref();
+const editingUserId = ref<number | null>(null);
+
+const formState = reactive({
+  username: '',
+  name: '',
+  phone: '',
+  password: '',
+  status: 1
+});
+
+const formRules = {
+  username: [
+    { required: true, message: '请输入用户名', trigger: 'blur' },
+    { min: 2, max: 32, message: '用户名长度2-32位', trigger: 'blur' }
+  ],
+  name: [
+    { required: true, message: '请输入姓名', trigger: 'blur' }
+  ],
+  phone: [
+    { required: true, message: '请输入手机号', trigger: 'blur' },
+    { pattern: /^1[3-9]\d{9}$/, message: '手机号格式不正确', trigger: 'blur' }
+  ],
+  password: [
+    { required: true, message: '请输入密码', trigger: 'blur' },
+    {
+      validator: (rule: any, value: string) => {
+        if (!validatePasswordStrength(value)) {
+          return Promise.reject('密码强度不足');
+        }
+        return Promise.resolve();
+      },
+      trigger: 'blur'
+    }
+  ]
+};
+
+// 重置密码模态框
+const resetPasswordModalVisible = ref(false);
+const resettingPassword = ref(false);
+const resetPasswordFormRef = ref();
+const resetPasswordUserId = ref<number | null>(null);
+
+const resetPasswordState = reactive({
+  password: ''
+});
+
+const resetPasswordRules = {
+  password: [
+    { required: true, message: '请输入新密码', trigger: 'blur' },
+    {
+      validator: (rule: any, value: string) => {
+        if (!validatePasswordStrength(value)) {
+          return Promise.reject('密码强度不足');
+        }
+        return Promise.resolve();
+      },
+      trigger: 'blur'
+    }
+  ]
+};
+
+// 加载用户列表
+async function loadUsers() {
+  loading.value = true;
+  try {
+    const result = await userApi.getUsers({
+      page: pagination.current,
+      page_size: pagination.pageSize,
+      keyword: searchKeyword.value
+    });
+    users.value = result.list;
+    pagination.total = result.total;
+  } catch (error) {
+    message.error('加载用户列表失败');
+  } finally {
+    loading.value = false;
+  }
+}
+
+// 表格分页变化
+function handleTableChange(pag: any) {
+  pagination.current = pag.current;
+  pagination.pageSize = pag.pageSize;
+  loadUsers();
+}
+
+// 显示创建模态框
+function showCreateModal() {
+  modalTitle.value = '创建用户';
+  isEdit.value = false;
+  modalVisible.value = true;
+  resetForm();
+}
+
+// 显示编辑模态框
+function showEditModal(record: SystemUser) {
+  modalTitle.value = '编辑用户';
+  isEdit.value = true;
+  editingUserId.value = record.id;
+  modalVisible.value = true;
+  
+  // 填充表单数据
+  formState.username = record.username;
+  formState.name = record.name;
+  formState.phone = record.phone;
+  formState.status = record.status;
+}
+
+// 提交表单
+async function handleSubmit() {
+  try {
+    await formRef.value.validate();
+    submitting.value = true;
+
+    if (isEdit.value && editingUserId.value) {
+      // 编辑用户
+      await userApi.updateUser(editingUserId.value, {
+        name: formState.name,
+        phone: formState.phone,
+        status: formState.status
+      });
+      message.success('用户已更新');
+    } else {
+      // 创建用户
+      await userApi.createUser({
+        username: formState.username,
+        name: formState.name,
+        phone: formState.phone,
+        password: formState.password
+      });
+      message.success('用户已创建');
+    }
+
+    modalVisible.value = false;
+    loadUsers();
+  } catch (error) {
+    if (error.response?.data?.msg) {
+      message.error(error.response.data.msg);
+    } else {
+      message.error(isEdit.value ? '更新用户失败' : '创建用户失败');
+    }
+  } finally {
+    submitting.value = false;
+  }
+}
+
+// 重置表单
+function resetForm() {
+  formRef.value?.resetFields();
+  formState.username = '';
+  formState.name = '';
+  formState.phone = '';
+  formState.password = '';
+  formState.status = 1;
+  editingUserId.value = null;
+}
+
+// 显示重置密码模态框
+function showResetPasswordModal(record: SystemUser) {
+  resetPasswordUserId.value = record.id;
+  resetPasswordModalVisible.value = true;
+  resetPasswordForm();
+}
+
+// 重置密码
+async function handleResetPassword() {
+  try {
+    await resetPasswordFormRef.value.validate();
+    resettingPassword.value = true;
+
+    await userApi.resetPassword(resetPasswordUserId.value!, {
+      password: resetPasswordState.password
+    });
+
+    message.success('密码已重置');
+    resetPasswordModalVisible.value = false;
+    loadUsers();
+  } catch (error) {
+    message.error('重置密码失败');
+  } finally {
+    resettingPassword.value = false;
+  }
+}
+
+// 重置密码表单
+function resetPasswordForm() {
+  resetPasswordFormRef.value?.resetFields();
+  resetPasswordState.password = '';
+  resetPasswordUserId.value = null;
+}
+
+// 删除用户
+async function deleteUser(id: number) {
+  try {
+    await userApi.deleteUser(id);
+    message.success('用户已删除');
+    loadUsers();
+  } catch (error) {
+    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>

+ 19 - 0
apps/platform-app/tailwind.config.js

@@ -0,0 +1,19 @@
+module.exports = {
+  content: [
+    './index.html',
+    './src/**/*.{vue,js,ts,jsx,tsx}',
+    '../../packages/shared-components/src/**/*.{vue,js,ts,jsx,tsx}'
+  ],
+  theme: {
+    extend: {
+      colors: {
+        primary: '#1e3a5f',
+        accent: '#f5a623'
+      }
+    }
+  },
+  // 与Ant Design Vue兼容配置
+  corePlugins: {
+    preflight: false // 禁用TailwindCSS的基础样式重置,避免与Ant Design冲突
+  }
+};

+ 36 - 0
apps/platform-app/tsconfig.json

@@ -0,0 +1,36 @@
+{
+  "extends": "../../tsconfig.base.json",
+  "compilerOptions": {
+    "target": "ES2020",
+    "useDefineForClassFields": true,
+    "module": "ESNext",
+    "lib": ["ES2020", "DOM", "DOM.Iterable"],
+    "skipLibCheck": true,
+
+    /* Bundler mode */
+    "moduleResolution": "bundler",
+    "allowImportingTsExtensions": true,
+    "resolveJsonModule": true,
+    "isolatedModules": true,
+    "noEmit": true,
+    "jsx": "preserve",
+
+    /* Linting */
+    "strict": true,
+    "noUnusedLocals": true,
+    "noUnusedParameters": true,
+    "noFallthroughCasesInSwitch": true,
+
+    /* Path aliases */
+    "baseUrl": ".",
+    "paths": {
+      "@/*": ["src/*"],
+      "@smartcut/types": ["../../packages/types/src"],
+      "@smartcut/api-client": ["../../packages/api-client/src"],
+      "@smartcut/shared-utils": ["../../packages/shared-utils/src"],
+      "@smartcut/shared-components": ["../../packages/shared-components/src"]
+    }
+  },
+  "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"],
+  "references": [{ "path": "./tsconfig.node.json" }]
+}

+ 10 - 0
apps/platform-app/tsconfig.node.json

@@ -0,0 +1,10 @@
+{
+  "compilerOptions": {
+    "composite": true,
+    "skipLibCheck": true,
+    "module": "ESNext",
+    "moduleResolution": "bundler",
+    "allowSyntheticDefaultImports": true
+  },
+  "include": ["vite.config.ts"]
+}

+ 39 - 0
apps/platform-app/vite.config.ts

@@ -0,0 +1,39 @@
+import { defineConfig } from 'vite';
+import vue from '@vitejs/plugin-vue';
+import { resolve } from 'path';
+
+// https://vitejs.dev/config/
+export default defineConfig({
+  plugins: [vue()],
+  resolve: {
+    alias: {
+      '@': resolve(__dirname, 'src'),
+      '@smartcut/types': resolve(__dirname, '../../packages/types/src'),
+      '@smartcut/api-client': resolve(__dirname, '../../packages/api-client/src'),
+      '@smartcut/shared-utils': resolve(__dirname, '../../packages/shared-utils/src'),
+      '@smartcut/shared-components': resolve(__dirname, '../../packages/shared-components/src')
+    }
+  },
+  server: {
+    port: 3000,
+    proxy: {
+      '/api': {
+        target: 'http://localhost:8080',
+        changeOrigin: true
+      }
+    }
+  },
+  build: {
+    outDir: 'dist',
+    sourcemap: false,
+    rollupOptions: {
+      output: {
+        manualChunks: {
+          'vue-vendor': ['vue', 'vue-router', 'pinia'],
+          'antd-vendor': ['ant-design-vue', '@ant-design/icons-vue'],
+          'shared-vendor': ['axios']
+        }
+      }
+    }
+  }
+});

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

@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
+    <title>智裁云 - 工人端</title>
+  </head>
+  <body>
+    <div id="app"></div>
+    <script type="module" src="/src/main.ts"></script>
+  </body>
+</html>

+ 42 - 0
apps/worker-app/package.json

@@ -0,0 +1,42 @@
+{
+  "name": "worker-app",
+  "version": "1.0.0",
+  "private": true,
+  "type": "module",
+  "scripts": {
+    "dev": "vite",
+    "build": "vite build",
+    "type-check": "vue-tsc --noEmit",
+    "preview": "vite preview",
+    "lint": "eslint . --fix",
+    "format": "prettier --write src/"
+  },
+  "dependencies": {
+    "vue": "^3.4.0",
+    "vue-router": "^4.2.0",
+    "pinia": "^2.1.0",
+    "ant-design-vue": "^4.0.0",
+    "axios": "^1.6.0",
+    "@ant-design/icons-vue": "^7.0.0",
+    "dayjs": "^1.11.0",
+    "@smartcut/types": "workspace:*",
+    "@smartcut/api-client": "workspace:*",
+    "@smartcut/shared-utils": "workspace:*",
+    "@smartcut/shared-components": "workspace:*"
+  },
+  "devDependencies": {
+    "@vitejs/plugin-vue": "^5.0.0",
+    "typescript": "^5.3.0",
+    "vite": "^5.0.0",
+    "vue-tsc": "^1.8.0",
+    "eslint": "^9.0.0",
+    "@eslint/js": "^9.0.0",
+    "typescript-eslint": "^8.0.0",
+    "eslint-plugin-vue": "^9.30.0",
+    "vue-eslint-parser": "^9.4.0",
+    "prettier": "^3.2.0",
+    "tailwindcss": "^3.4.0",
+    "autoprefixer": "^10.4.0",
+    "postcss": "^8.4.0"
+  }
+}

+ 19 - 0
apps/worker-app/src/App.vue

@@ -0,0 +1,19 @@
+<template>
+  <a-config-provider :locale="zhCN">
+    <router-view />
+  </a-config-provider>
+</template>
+
+<script setup lang="ts">
+import zhCN from 'ant-design-vue/es/locale/zh_CN';
+</script>
+
+<style>
+#app {
+  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,
+    'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
+    'Noto Color Emoji';
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
+</style>

+ 99 - 0
apps/worker-app/src/components/WorkerLayout.vue

@@ -0,0 +1,99 @@
+<template>
+  <div class="worker-layout">
+    <!-- 头部 -->
+    <div class="worker-header">
+      <div class="header-title">{{ pageTitle }}</div>
+      <div class="header-user" @click="router.push('/profile')">
+        <a-avatar size="small">{{ authStore.user?.name?.charAt(0) }}</a-avatar>
+      </div>
+    </div>
+
+    <!-- 内容区 -->
+    <div class="worker-content">
+      <slot />
+    </div>
+
+    <!-- 底部Tab栏 -->
+    <div class="worker-tabbar">
+      <div
+        v-for="tab in tabs"
+        :key="tab.key"
+        class="worker-tabbar-item"
+        :class="{ active: activeTab === tab.key }"
+        @click="router.push(tab.path)"
+      >
+        <component :is="tab.icon" class="icon" />
+        <span>{{ tab.label }}</span>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { computed } from 'vue';
+import { useRouter, useRoute } from 'vue-router';
+import {
+  HomeOutlined,
+  ScanOutlined,
+  UnorderedListOutlined,
+  PayCircleOutlined,
+  UserOutlined
+} from '@ant-design/icons-vue';
+import { useAuthStore } from '@/stores/auth';
+
+const router = useRouter();
+const route = useRoute();
+const authStore = useAuthStore();
+
+const tabs = [
+  { key: 'home', label: '首页', icon: HomeOutlined, path: '/' },
+  { key: 'scan', label: '扫码', icon: ScanOutlined, path: '/scan' },
+  { key: 'records', label: '记录', icon: UnorderedListOutlined, path: '/records' },
+  { key: 'salary', label: '工资', icon: PayCircleOutlined, path: '/salary' },
+  { key: 'profile', label: '我的', icon: UserOutlined, path: '/profile' }
+];
+
+const activeTab = computed(() => {
+  const path = route.path;
+  if (path === '/') return 'home';
+  if (path.startsWith('/scan')) return 'scan';
+  if (path.startsWith('/records')) return 'records';
+  if (path.startsWith('/salary')) return 'salary';
+  if (path.startsWith('/profile')) return 'profile';
+  return 'home';
+});
+
+const pageTitle = computed(() => {
+  const tab = tabs.find(t => t.key === activeTab.value);
+  return tab?.label || '智裁云';
+});
+</script>
+
+<style scoped lang="postcss">
+.worker-header {
+  position: sticky;
+  top: 0;
+  z-index: 100;
+  height: 48px;
+  background-color: #1e3a5f;
+  color: #fff;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 0 16px;
+}
+
+.header-title {
+  font-size: 16px;
+  font-weight: 500;
+}
+
+.header-user {
+  cursor: pointer;
+}
+
+.worker-content {
+  padding: 16px;
+  min-height: calc(100vh - 48px - 56px);
+}
+</style>

+ 16 - 0
apps/worker-app/src/main.ts

@@ -0,0 +1,16 @@
+import { createApp } from 'vue';
+import { createPinia } from 'pinia';
+import Antd from 'ant-design-vue';
+import 'ant-design-vue/dist/reset.css';
+import App from './App.vue';
+import router from './router';
+
+import './styles/main.css';
+
+const app = createApp(App);
+
+app.use(createPinia());
+app.use(router);
+app.use(Antd);
+
+app.mount('#app');

+ 95 - 0
apps/worker-app/src/router/index.ts

@@ -0,0 +1,95 @@
+// 工人端路由配置
+
+import { createRouter, createWebHistory } from 'vue-router';
+import { useAuthStore } from '@/stores/auth';
+
+const routes = [
+  {
+    path: '/login',
+    name: 'Login',
+    component: () => import('@/views/LoginView.vue'),
+    meta: { requiresAuth: false }
+  },
+  {
+    path: '/',
+    name: 'Home',
+    component: () => import('@/views/HomeView.vue'),
+    meta: { requiresAuth: true }
+  },
+  {
+    path: '/scan',
+    name: 'Scan',
+    component: () => import('@/views/ScanView.vue'),
+    meta: { requiresAuth: true }
+  },
+  {
+    path: '/submit',
+    name: 'Submit',
+    component: () => import('@/views/SubmitView.vue'),
+    meta: { requiresAuth: true }
+  },
+  {
+    path: '/records',
+    name: 'Records',
+    component: () => import('@/views/RecordsView.vue'),
+    meta: { requiresAuth: true }
+  },
+  {
+    path: '/salary',
+    name: 'Salary',
+    component: () => import('@/views/SalaryView.vue'),
+    meta: { requiresAuth: true }
+  },
+  {
+    path: '/profile',
+    name: 'Profile',
+    component: () => import('@/views/ProfileView.vue'),
+    meta: { requiresAuth: true }
+  },
+  {
+    path: '/change-password',
+    name: 'ChangePassword',
+    component: () => import('@/views/ChangePasswordView.vue'),
+    meta: { requiresAuth: true }
+  },
+  {
+    path: '/:pathMatch(.*)*',
+    name: 'NotFound',
+    component: () => import('@/views/NotFoundView.vue'),
+    meta: { requiresAuth: false }
+  }
+];
+
+const router = createRouter({
+  history: createWebHistory(),
+  routes
+});
+
+router.beforeEach((to, from, next) => {
+  const authStore = useAuthStore();
+
+  if (to.meta.requiresAuth) {
+    if (!authStore.isLoggedIn) {
+      next({
+        name: 'Login',
+        query: {
+          redirect: to.fullPath,
+          factory: authStore.currentFactoryId || ''
+        }
+      });
+    } else if (!authStore.currentFactoryId) {
+      next({
+        name: 'Login',
+        query: { redirect: to.fullPath }
+      });
+    } else {
+      next();
+    }
+  } else if (to.name === 'Login' && authStore.isLoggedIn) {
+    next({ name: 'Home' });
+  } else {
+    next();
+  }
+});
+
+export default router;

+ 130 - 0
apps/worker-app/src/stores/auth.ts

@@ -0,0 +1,130 @@
+// 工人端认证状态管理
+
+import { defineStore } from 'pinia';
+import { ref, computed } from 'vue';
+import type { User } from '@smartcut/types';
+import { AuthApi, createApiClient } from '@smartcut/api-client';
+import {
+  STORAGE_KEYS,
+  setSessionStorage,
+  getSessionStorage,
+  removeSessionStorage
+} from '@smartcut/shared-utils';
+
+export const useAuthStore = defineStore('auth', () => {
+  const token = ref<string | null>(getSessionStorage<string>(STORAGE_KEYS.WORKER_TOKEN));
+  const user = ref<User | null>(getSessionStorage<User>(STORAGE_KEYS.WORKER_USER));
+  const currentFactoryId = ref<string | null>(
+    getSessionStorage<string>(STORAGE_KEYS.CURRENT_FACTORY_ID)
+  );
+  const isLoggedIn = computed(() => !!token.value && !!user.value && !!currentFactoryId.value);
+
+  const apiClient = createApiClient({
+    baseURL: window.location.origin,
+    tokenKey: STORAGE_KEYS.WORKER_TOKEN
+  });
+
+  const authApi = new AuthApi(apiClient);
+
+  async function login(username: string, password: string, factoryId: string) {
+    try {
+      const response = await authApi.login({
+        username,
+        password,
+        factory_id: factoryId
+      });
+
+      token.value = response.token;
+      user.value = response.user as User;
+      currentFactoryId.value = factoryId;
+
+      setSessionStorage(STORAGE_KEYS.WORKER_TOKEN, response.token);
+      setSessionStorage(STORAGE_KEYS.WORKER_USER, response.user);
+      setSessionStorage(STORAGE_KEYS.CURRENT_FACTORY_ID, factoryId);
+
+      return response;
+    } catch (error) {
+      console.error('登录失败:', error);
+      throw error;
+    }
+  }
+
+  async function loginByPhone(phone: string, password: string, factoryId: string) {
+    try {
+      const response = await authApi.login({
+        phone,
+        password,
+        factory_id: factoryId
+      });
+
+      token.value = response.token;
+      user.value = response.user as User;
+      currentFactoryId.value = factoryId;
+
+      setSessionStorage(STORAGE_KEYS.WORKER_TOKEN, response.token);
+      setSessionStorage(STORAGE_KEYS.WORKER_USER, response.user);
+      setSessionStorage(STORAGE_KEYS.CURRENT_FACTORY_ID, factoryId);
+
+      return response;
+    } catch (error) {
+      console.error('登录失败:', error);
+      throw error;
+    }
+  }
+
+  function logout() {
+    token.value = null;
+    user.value = null;
+    currentFactoryId.value = null;
+
+    removeSessionStorage(STORAGE_KEYS.WORKER_TOKEN);
+    removeSessionStorage(STORAGE_KEYS.WORKER_USER);
+    removeSessionStorage(STORAGE_KEYS.CURRENT_FACTORY_ID);
+  }
+
+  async function fetchProfile() {
+    if (!token.value || !currentFactoryId.value) return;
+    try {
+      const profile = await authApi.getProfile();
+      user.value = profile as User;
+      setSessionStorage(STORAGE_KEYS.WORKER_USER, profile);
+    } catch (error) {
+      console.error('获取用户信息失败:', error);
+      logout();
+    }
+  }
+
+  async function changePassword(oldPassword: string, newPassword: string) {
+    try {
+      await authApi.changePassword({
+        old_password: oldPassword,
+        new_password: newPassword
+      });
+      if (user.value) {
+        user.value.must_change_password = false;
+        setSessionStorage(STORAGE_KEYS.WORKER_USER, user.value);
+      }
+    } catch (error) {
+      console.error('修改密码失败:', error);
+      throw error;
+    }
+  }
+
+  function setFactoryIdFromQuery(factoryId: string) {
+    currentFactoryId.value = factoryId;
+    setSessionStorage(STORAGE_KEYS.CURRENT_FACTORY_ID, factoryId);
+  }
+
+  return {
+    token,
+    user,
+    currentFactoryId,
+    isLoggedIn,
+    login,
+    loginByPhone,
+    logout,
+    fetchProfile,
+    changePassword,
+    setFactoryIdFromQuery
+  };
+});

+ 66 - 0
apps/worker-app/src/styles/main.css

@@ -0,0 +1,66 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+:root {
+  --ant-primary-color: #1e3a5f;
+  --ant-primary-color-hover: #2a4a6f;
+  --ant-primary-color-active: #15304f;
+}
+
+.ant-btn-primary {
+  background-color: #1e3a5f;
+}
+
+.ant-btn-primary:hover {
+  background-color: #2a4a6f;
+}
+
+.ant-btn-primary:active {
+  background-color: #15304f;
+}
+
+/* 移动端优化 */
+body {
+  margin: 0;
+  padding: 0;
+  background-color: #f0f2f5;
+}
+
+.worker-layout {
+  min-height: 100vh;
+  padding-bottom: 60px;
+}
+
+.worker-tabbar {
+  position: fixed;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  height: 56px;
+  background-color: #fff;
+  border-top: 1px solid #e8e8e8;
+  display: flex;
+  z-index: 1000;
+}
+
+.worker-tabbar-item {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  color: #666;
+  font-size: 12px;
+  cursor: pointer;
+  transition: color 0.3s;
+}
+
+.worker-tabbar-item.active {
+  color: #1e3a5f;
+}
+
+.worker-tabbar-item .icon {
+  font-size: 20px;
+  margin-bottom: 2px;
+}

+ 92 - 0
apps/worker-app/src/views/ChangePasswordView.vue

@@ -0,0 +1,92 @@
+<template>
+  <WorkerLayout>
+    <div class="change-password-view">
+      <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-button type="primary" html-type="submit" size="large" block :loading="submitting">
+            提交
+          </a-button>
+        </a-form>
+      </a-card>
+    </div>
+  </WorkerLayout>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive } 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 { validatePasswordStrength } from '@smartcut/shared-utils';
+
+const router = useRouter();
+const authStore = useAuthStore();
+
+const submitting = ref(false);
+const formRef = ref();
+
+const formState = reactive({
+  old_password: '',
+  new_password: '',
+  confirm_password: ''
+});
+
+const formRules = {
+  old_password: [{ required: true, message: '请输入旧密码', trigger: 'blur' }],
+  new_password: [
+    { required: true, message: '请输入新密码', trigger: 'blur' },
+    {
+      validator: (_rule: any, value: string) => {
+        if (!validatePasswordStrength(value)) return Promise.reject('密码强度不足');
+        return Promise.resolve();
+      },
+      trigger: 'blur'
+    }
+  ],
+  confirm_password: [
+    { required: true, message: '请确认新密码', trigger: 'blur' },
+    {
+      validator: (_rule: any, value: string) => {
+        if (value !== formState.new_password) return Promise.reject('两次输入的密码不一致');
+        return Promise.resolve();
+      },
+      trigger: 'blur'
+    }
+  ]
+};
+
+async function handleSubmit() {
+  try {
+    submitting.value = true;
+    await authStore.changePassword(formState.old_password, formState.new_password);
+    message.success('密码修改成功');
+    router.push('/profile');
+  } catch (error: any) {
+    message.error(error.response?.data?.msg || '修改密码失败');
+  } finally {
+    submitting.value = false;
+  }
+}
+</script>
+
+<style scoped lang="postcss">
+.change-password-view { padding: 0; }
+.password-tips { font-size: 12px; color: #999; margin-top: 8px; }
+</style>

+ 161 - 0
apps/worker-app/src/views/HomeView.vue

@@ -0,0 +1,161 @@
+<template>
+  <WorkerLayout>
+    <div class="home-view">
+      <!-- 欢迎卡片 -->
+      <a-card class="welcome-card">
+        <div class="welcome-text">
+          <h2>你好,{{ authStore.user?.name }}</h2>
+          <p>{{ today }}</p>
+        </div>
+      </a-card>
+
+      <!-- 今日数据 -->
+      <a-row :gutter="12" class="mt-3">
+        <a-col :span="12">
+          <a-card>
+            <a-statistic title="今日产量" :value="dashboardData.today_quantity" suffix="件" />
+          </a-card>
+        </a-col>
+        <a-col :span="12">
+          <a-card>
+            <a-statistic
+              title="今日工资"
+              :value="dashboardData.today_amount"
+              :precision="2"
+              prefix="¥"
+            />
+          </a-card>
+        </a-col>
+      </a-row>
+
+      <!-- 本月数据 -->
+      <a-row :gutter="12" class="mt-3">
+        <a-col :span="12">
+          <a-card>
+            <a-statistic title="本月产量" :value="dashboardData.month_quantity" suffix="件" />
+          </a-card>
+        </a-col>
+        <a-col :span="12">
+          <a-card>
+            <a-statistic
+              title="本月工资"
+              :value="dashboardData.month_amount"
+              :precision="2"
+              prefix="¥"
+            />
+          </a-card>
+        </a-col>
+      </a-row>
+
+      <!-- 快捷操作 -->
+      <a-card title="快捷操作" class="mt-3">
+        <a-row :gutter="12">
+          <a-col :span="12">
+            <a-button type="primary" size="large" block @click="router.push('/scan')">
+              扫码计件
+            </a-button>
+          </a-col>
+          <a-col :span="12">
+            <a-button size="large" block @click="router.push('/submit')">
+              手动计件
+            </a-button>
+          </a-col>
+        </a-row>
+      </a-card>
+
+      <!-- 最近记录 -->
+      <a-card title="最近记录" class="mt-3" v-if="recentRecords.length > 0">
+        <a-list :data-source="recentRecords" size="small">
+          <template #renderItem="{ item }">
+            <a-list-item>
+              <a-list-item-meta>
+                <template #title>
+                  {{ item.process_name }} - {{ item.quantity }}件
+                </template>
+                <template #description>
+                  ¥{{ item.amount.toFixed(2) }} | {{ item.record_date }}
+                </template>
+              </a-list-item-meta>
+            </a-list-item>
+          </template>
+        </a-list>
+      </a-card>
+    </div>
+  </WorkerLayout>
+</template>
+
+<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 type { WorkerDashboardData, PieceRecord } from '@smartcut/types';
+
+const router = useRouter();
+const authStore = useAuthStore();
+
+const today = formatDate(new Date());
+
+const dashboardData = ref<WorkerDashboardData>({
+  today_quantity: 0,
+  today_amount: 0,
+  month_quantity: 0,
+  month_amount: 0
+});
+
+const recentRecords = ref<PieceRecord[]>([]);
+
+const apiClient = createApiClient({
+  baseURL: window.location.origin,
+  tokenKey: STORAGE_KEYS.WORKER_TOKEN
+});
+
+const statsApi = new StatsApi(apiClient);
+const recordApi = new RecordApi(apiClient);
+
+onMounted(async () => {
+  try {
+    dashboardData.value = await statsApi.getWorkerDashboard();
+  } catch (error) {
+    // ignore
+  }
+
+  try {
+    const result = await recordApi.getWorkerRecords({ page: 1, page_size: 5 });
+    recentRecords.value = result.list;
+  } catch (error) {
+    // ignore
+  }
+});
+</script>
+
+<style scoped lang="postcss">
+.welcome-card {
+  background: linear-gradient(135deg, #1e3a5f 0%, #2a4a6f 100%);
+  color: #fff;
+  border: none;
+}
+
+.welcome-card :deep(.ant-card-body) {
+  padding: 20px;
+}
+
+.welcome-text h2 {
+  color: #fff;
+  margin: 0;
+  font-size: 20px;
+}
+
+.welcome-text p {
+  color: rgba(255, 255, 255, 0.8);
+  margin: 8px 0 0 0;
+  font-size: 14px;
+}
+
+.mt-3 {
+  margin-top: 12px;
+}
+</style>

+ 163 - 0
apps/worker-app/src/views/LoginView.vue

@@ -0,0 +1,163 @@
+<template>
+  <div class="login-page">
+    <div class="login-header">
+      <h1>智裁云</h1>
+      <p>工人端</p>
+    </div>
+
+    <a-card class="login-card">
+      <!-- 工厂选择 -->
+      <a-form-item v-if="!factoryId" label="选择工厂">
+        <a-select
+          v-model:value="selectedFactory"
+          placeholder="请选择工厂"
+          size="large"
+          :loading="loadingFactories"
+        >
+          <a-select-option v-for="f in factories" :key="f.id" :value="f.id">
+            {{ f.name }}
+          </a-select-option>
+        </a-select>
+      </a-form-item>
+
+      <!-- 登录方式切换 -->
+      <a-radio-group v-model:value="loginType" class="login-type">
+        <a-radio value="username">用户名登录</a-radio>
+        <a-radio value="phone">手机号登录</a-radio>
+      </a-radio-group>
+
+      <a-form :model="formState" @finish="handleLogin" layout="vertical">
+        <a-form-item v-if="loginType === 'username'" label="用户名" name="username" :rules="[{ required: true, message: '请输入用户名' }]">
+          <a-input v-model:value="formState.username" placeholder="请输入用户名" size="large" />
+        </a-form-item>
+
+        <a-form-item v-else label="手机号" name="phone" :rules="[{ required: true, message: '请输入手机号' }, { pattern: /^1[3-9]\d{9}$/, message: '手机号格式不正确' }]">
+          <a-input v-model:value="formState.phone" placeholder="请输入手机号" size="large" />
+        </a-form-item>
+
+        <a-form-item label="密码" name="password" :rules="[{ required: true, message: '请输入密码' }]">
+          <a-input-password v-model:value="formState.password" placeholder="请输入密码" size="large" />
+        </a-form-item>
+
+        <a-form-item>
+          <a-button
+            type="primary"
+            html-type="submit"
+            size="large"
+            block
+            :loading="loading"
+            :disabled="!factoryId && !selectedFactory"
+          >
+            登录
+          </a-button>
+        </a-form-item>
+      </a-form>
+    </a-card>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive, onMounted } from 'vue';
+import { useRouter, useRoute } from 'vue-router';
+import { message } from 'ant-design-vue';
+import { useAuthStore } from '@/stores/auth';
+import { FactoryApi, createApiClient } from '@smartcut/api-client';
+import { STORAGE_KEYS } from '@smartcut/shared-utils';
+import type { Factory } from '@smartcut/types';
+
+const router = useRouter();
+const route = useRoute();
+const authStore = useAuthStore();
+
+const loading = ref(false);
+const loadingFactories = ref(false);
+const factories = ref<Factory[]>([]);
+const selectedFactory = ref('');
+const factoryId = ref(route.query.factory as string);
+const loginType = ref<'username' | 'phone'>('username');
+
+const formState = reactive({
+  username: '',
+  phone: '',
+  password: ''
+});
+
+onMounted(async () => {
+  if (!factoryId.value) {
+    loadingFactories.value = true;
+    try {
+      const apiClient = createApiClient({
+        baseURL: window.location.origin,
+        tokenKey: STORAGE_KEYS.PLATFORM_TOKEN
+      });
+      const factoryApi = new FactoryApi(apiClient);
+      const result = await factoryApi.listFactories();
+      factories.value = result.list.filter(f => f.status === 1);
+    } catch (error) {
+      message.error('加载工厂列表失败');
+    } finally {
+      loadingFactories.value = false;
+    }
+  } else {
+    authStore.setFactoryIdFromQuery(factoryId.value);
+  }
+});
+
+async function handleLogin() {
+  const targetFactoryId = factoryId.value || selectedFactory.value;
+  if (!targetFactoryId) {
+    message.error('请选择工厂');
+    return;
+  }
+
+  loading.value = true;
+  try {
+    if (loginType.value === 'username') {
+      await authStore.login(formState.username, formState.password, targetFactoryId);
+    } else {
+      await authStore.loginByPhone(formState.phone, formState.password, targetFactoryId);
+    }
+    message.success('登录成功');
+    const redirect = route.query.redirect as string;
+    router.push(redirect || '/');
+  } catch (error: any) {
+    message.error(error.response?.data?.msg || '登录失败');
+  } finally {
+    loading.value = false;
+  }
+}
+</script>
+
+<style scoped lang="postcss">
+.login-page {
+  min-height: 100vh;
+  background-color: #f0f2f5;
+  padding: 40px 20px;
+}
+
+.login-header {
+  text-align: center;
+  margin-bottom: 32px;
+}
+
+.login-header h1 {
+  font-size: 32px;
+  color: #1e3a5f;
+  margin: 0;
+}
+
+.login-header p {
+  font-size: 16px;
+  color: #666;
+  margin-top: 8px;
+}
+
+.login-card {
+  border-radius: 12px;
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+}
+
+.login-type {
+  margin-bottom: 16px;
+}
+</style>

+ 23 - 0
apps/worker-app/src/views/NotFoundView.vue

@@ -0,0 +1,23 @@
+<template>
+  <div class="not-found-page">
+    <a-result status="404" title="404" sub-title="页面不存在">
+      <template #extra>
+        <a-button type="primary" @click="router.push('/')">返回首页</a-button>
+      </template>
+    </a-result>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { useRouter } from 'vue-router';
+const router = useRouter();
+</script>
+
+<style scoped lang="postcss">
+.not-found-page {
+  min-height: 100vh;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
+</style>

+ 90 - 0
apps/worker-app/src/views/ProfileView.vue

@@ -0,0 +1,90 @@
+<template>
+  <WorkerLayout>
+    <div class="profile-view">
+      <!-- 用户信息卡片 -->
+      <a-card>
+        <div class="user-info">
+          <a-avatar size="large" class="user-avatar">
+            {{ authStore.user?.name?.charAt(0) }}
+          </a-avatar>
+          <div class="user-detail">
+            <h3>{{ authStore.user?.name }}</h3>
+            <p>{{ authStore.user?.phone }}</p>
+            <a-tag color="blue">工人</a-tag>
+          </div>
+        </div>
+      </a-card>
+
+      <!-- 功能菜单 -->
+      <a-card class="mt-3">
+        <a-list>
+          <a-list-item @click="router.push('/change-password')">
+            <a-list-item-meta>
+              <template #avatar><KeyOutlined /></template>
+              <template #title>修改密码</template>
+            </a-list-item-meta>
+            <template #action><RightOutlined /></template>
+          </a-list-item>
+
+          <a-list-item v-if="authStore.user?.must_change_password">
+            <a-list-item-meta>
+              <template #avatar><WarningOutlined /></template>
+              <template #title>
+                <a-badge status="warning" text="需要修改密码" />
+              </template>
+            </a-list-item-meta>
+          </a-list-item>
+        </a-list>
+      </a-card>
+
+      <!-- 退出登录 -->
+      <a-card class="mt-3">
+        <a-button danger size="large" block @click="handleLogout">
+          退出登录
+        </a-button>
+      </a-card>
+    </div>
+  </WorkerLayout>
+</template>
+
+<script setup lang="ts">
+import { useRouter } from 'vue-router';
+import { message } from 'ant-design-vue';
+import { KeyOutlined, RightOutlined, WarningOutlined } from '@ant-design/icons-vue';
+import { useAuthStore } from '@/stores/auth';
+import WorkerLayout from '@/components/WorkerLayout.vue';
+
+const router = useRouter();
+const authStore = useAuthStore();
+
+function handleLogout() {
+  authStore.logout();
+  message.success('已退出登录');
+  router.push('/login');
+}
+</script>
+
+<style scoped lang="postcss">
+.profile-view { padding: 0; }
+.user-info {
+  display: flex;
+  align-items: center;
+  gap: 16px;
+}
+.user-avatar {
+  background-color: #1e3a5f;
+  font-size: 24px;
+}
+.user-detail h3 {
+  margin: 0;
+  font-size: 18px;
+}
+.user-detail p {
+  margin: 4px 0;
+  color: #666;
+  font-size: 14px;
+}
+.mt-3 {
+  margin-top: 12px;
+}
+</style>

+ 116 - 0
apps/worker-app/src/views/RecordsView.vue

@@ -0,0 +1,116 @@
+<template>
+  <WorkerLayout>
+    <div class="records-view">
+      <a-card title="我的计件记录">
+        <!-- 日期筛选 -->
+        <a-date-picker
+          v-model:value="filterDate"
+          placeholder="筛选日期"
+          @change="loadRecords"
+          style="width: 100%; margin-bottom: 12px"
+          size="large"
+        />
+
+        <!-- 记录列表 -->
+        <a-list
+          :data-source="records"
+          :loading="loading"
+          :pagination="pagination"
+          @change="handlePageChange"
+        >
+          <template #renderItem="{ item }">
+            <a-list-item>
+              <a-list-item-meta>
+                <template #title>
+                  <a-space>
+                    <span>{{ item.process_name }}</span>
+                    <a-tag :color="item.status === 'normal' ? 'green' : 'red'">
+                      {{ item.status === 'normal' ? '正常' : '已退回' }}
+                    </a-tag>
+                  </a-space>
+                </template>
+                <template #description>
+                  {{ item.order_no }} - {{ item.style_name }}<br/>
+                  数量:{{ item.quantity }}件 | 单价:¥{{ item.price.toFixed(2) }} | 金额:¥{{ item.amount.toFixed(2) }}<br/>
+                  日期:{{ item.record_date }}
+                </template>
+              </a-list-item-meta>
+            </a-list-item>
+          </template>
+
+          <template #header>
+            <div v-if="records.length === 0 && !loading" class="empty-text">
+              暂无记录
+            </div>
+          </template>
+        </a-list>
+      </a-card>
+    </div>
+  </WorkerLayout>
+</template>
+
+<script setup lang="ts">
+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 type { PieceRecord } from '@smartcut/types';
+
+const authStore = useAuthStore();
+
+const records = ref<PieceRecord[]>([]);
+const loading = ref(false);
+const filterDate = ref<dayjs.Dayjs | null>(null);
+
+const pagination = reactive({
+  current: 1,
+  pageSize: 10,
+  total: 0,
+  showSizeChanger: false
+});
+
+const apiClient = createApiClient({
+  baseURL: window.location.origin,
+  tokenKey: STORAGE_KEYS.WORKER_TOKEN
+});
+
+const recordApi = new RecordApi(apiClient);
+
+async function loadRecords() {
+  loading.value = true;
+  try {
+    const result = await recordApi.getWorkerRecords({
+      page: pagination.current,
+      page_size: pagination.pageSize,
+      date: filterDate.value ? formatDate(filterDate.value.toDate()) : undefined
+    });
+    records.value = result.list;
+    pagination.total = result.total;
+  } catch (error) {
+    message.error('加载记录失败');
+  } finally {
+    loading.value = false;
+  }
+}
+
+function handlePageChange(page: number) {
+  pagination.current = page;
+  loadRecords();
+}
+
+onMounted(() => {
+  loadRecords();
+});
+</script>
+
+<style scoped lang="postcss">
+.records-view { padding: 0; }
+.empty-text {
+  text-align: center;
+  color: #999;
+  padding: 20px;
+}
+</style>

+ 82 - 0
apps/worker-app/src/views/SalaryView.vue

@@ -0,0 +1,82 @@
+<template>
+  <WorkerLayout>
+    <div class="salary-view">
+      <a-card title="我的工资">
+        <!-- 工资列表 -->
+        <a-list
+          :data-source="salaries"
+          :loading="loading"
+        >
+          <template #renderItem="{ item }">
+            <a-list-item>
+              <a-list-item-meta>
+                <template #title>
+                  {{ item.period }}
+                </template>
+                <template #description>
+                  数量:{{ item.total_quantity }}件 | 记录:{{ item.records_count }}条
+                </template>
+              </a-list-item-meta>
+              <div class="salary-amount">
+                ¥{{ item.total_amount.toFixed(2) }}
+              </div>
+            </a-list-item>
+          </template>
+
+          <template #header>
+            <div v-if="salaries.length === 0 && !loading" class="empty-text">
+              暂无工资记录
+            </div>
+          </template>
+        </a-list>
+      </a-card>
+    </div>
+  </WorkerLayout>
+</template>
+
+<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 type { WorkerSalary } from '@smartcut/types';
+
+const authStore = useAuthStore();
+
+const salaries = ref<WorkerSalary[]>([]);
+const loading = ref(false);
+
+const apiClient = createApiClient({
+  baseURL: window.location.origin,
+  tokenKey: STORAGE_KEYS.WORKER_TOKEN
+});
+
+const salaryApi = new SalaryApi(apiClient);
+
+onMounted(async () => {
+  loading.value = true;
+  try {
+    salaries.value = await salaryApi.getWorkerSalary();
+  } catch (error) {
+    message.error('加载工资数据失败');
+  } finally {
+    loading.value = false;
+  }
+});
+</script>
+
+<style scoped lang="postcss">
+.salary-view { padding: 0; }
+.salary-amount {
+  font-size: 18px;
+  font-weight: bold;
+  color: #1e3a5f;
+}
+.empty-text {
+  text-align: center;
+  color: #999;
+  padding: 20px;
+}
+</style>

+ 191 - 0
apps/worker-app/src/views/ScanView.vue

@@ -0,0 +1,191 @@
+<template>
+  <WorkerLayout>
+    <div class="scan-view">
+      <a-card title="扫码计件">
+        <!-- 扫码区域 -->
+        <div class="scan-area">
+          <a-button
+            type="primary"
+            size="large"
+            block
+            @click="startScan"
+            :loading="scanning"
+            v-if="!scanning"
+          >
+            <template #icon><ScanOutlined /></template>
+            开始扫描
+          </a-button>
+
+          <div v-if="scanning" class="scanning">
+            <a-spin tip="正在扫描..." />
+            <a-button @click="stopScan" class="mt-3">停止扫描</a-button>
+          </div>
+        </div>
+
+        <!-- 手动输入二维码 -->
+        <a-divider>或手动输入</a-divider>
+        <a-input
+          v-model:value="qrCode"
+          placeholder="请输入二维码内容"
+          size="large"
+          @pressEnter="handleQrCode"
+        />
+      </a-card>
+
+      <!-- 计件表单 -->
+      <a-card title="计件信息" class="mt-3" v-if="bundleInfo">
+        <a-descriptions :column="1" size="small">
+          <a-descriptions-item label="扎号">{{ bundleInfo.bundle_no }}</a-descriptions-item>
+          <a-descriptions-item label="尺码">{{ bundleInfo.size }}</a-descriptions-item>
+          <a-descriptions-item label="数量">{{ bundleInfo.quantity }}</a-descriptions-item>
+          <a-descriptions-item label="工单">{{ bundleInfo.order_no }}</a-descriptions-item>
+          <a-descriptions-item label="款号">{{ bundleInfo.style_name }}</a-descriptions-item>
+        </a-descriptions>
+
+        <a-form layout="vertical" class="mt-3">
+          <a-form-item label="工序" required>
+            <a-select v-model:value="processId" placeholder="请选择工序" size="large">
+              <a-select-option v-for="p in processes" :key="p.id" :value="p.id">
+                {{ p.name }}
+              </a-select-option>
+            </a-select>
+          </a-form-item>
+
+          <a-form-item label="数量" required>
+            <a-input-number
+              v-model:value="quantity"
+              :min="1"
+              :max="bundleInfo.quantity"
+              size="large"
+              style="width: 100%"
+            />
+          </a-form-item>
+
+          <a-button
+            type="primary"
+            size="large"
+            block
+            @click="handleSubmit"
+            :loading="submitting"
+          >
+            确认计件
+          </a-button>
+        </a-form>
+      </a-card>
+    </div>
+  </WorkerLayout>
+</template>
+
+<script setup lang="ts">
+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 type { Process } from '@smartcut/types';
+
+const authStore = useAuthStore();
+
+const scanning = ref(false);
+const qrCode = ref('');
+const bundleInfo = ref<any>(null);
+const processes = ref<Process[]>([]);
+const processId = ref<number | null>(null);
+const quantity = ref(1);
+const submitting = ref(false);
+
+const apiClient = createApiClient({
+  baseURL: window.location.origin,
+  tokenKey: STORAGE_KEYS.WORKER_TOKEN
+});
+
+const recordApi = new RecordApi(apiClient);
+const processApi = new ProcessApi(apiClient);
+
+onMounted(async () => {
+  try {
+    processes.value = await processApi.getProcesses();
+  } catch (error) {
+    message.error('加载工序列表失败');
+  }
+});
+
+async function startScan() {
+  scanning.value = true;
+  // 实际项目中这里调用摄像头扫码API
+  // 由于浏览器限制,需要HTTPS环境
+  message.info('扫码功能需要在HTTPS环境下使用,请手动输入二维码');
+  scanning.value = false;
+}
+
+function stopScan() {
+  scanning.value = false;
+}
+
+async function handleQrCode() {
+  if (!qrCode.value) {
+    message.error('请输入二维码内容');
+    return;
+  }
+
+  try {
+    // 解析二维码获取扎号信息
+    const response = await apiClient.get('/worker/bundle-info', {
+      params: { qr_code: qrCode.value }
+    });
+    bundleInfo.value = response.data.data;
+    quantity.value = bundleInfo.value.quantity;
+    message.success('已识别扎号');
+  } catch (error: any) {
+    message.error(error.response?.data?.msg || '二维码无效');
+  }
+}
+
+async function handleSubmit() {
+  if (!processId.value) {
+    message.error('请选择工序');
+    return;
+  }
+  if (!bundleInfo.value) {
+    message.error('请先扫描或输入二维码');
+    return;
+  }
+
+  submitting.value = true;
+  try {
+    await recordApi.scanPiece({
+      qr_code: qrCode.value,
+      process_id: processId.value,
+      quantity: quantity.value
+    });
+    message.success('计件成功');
+
+    // 重置表单
+    qrCode.value = '';
+    bundleInfo.value = null;
+    processId.value = null;
+    quantity.value = 1;
+  } catch (error: any) {
+    message.error(error.response?.data?.msg || '计件失败');
+  } finally {
+    submitting.value = false;
+  }
+}
+</script>
+
+<style scoped lang="postcss">
+.scan-view { padding: 0; }
+.scan-area {
+  text-align: center;
+  padding: 20px 0;
+}
+.scanning {
+  text-align: center;
+  padding: 40px 0;
+}
+.mt-3 {
+  margin-top: 12px;
+}
+</style>

+ 140 - 0
apps/worker-app/src/views/SubmitView.vue

@@ -0,0 +1,140 @@
+<template>
+  <WorkerLayout>
+    <div class="submit-view">
+      <a-card title="手动计件">
+        <a-form layout="vertical">
+          <a-form-item label="工单" required>
+            <a-select
+              v-model:value="workOrderId"
+              placeholder="请选择工单"
+              size="large"
+              show-search
+              :filter-option="filterOption"
+            >
+              <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="工序" required>
+            <a-select v-model:value="processId" placeholder="请选择工序" size="large">
+              <a-select-option v-for="p in processes" :key="p.id" :value="p.id">
+                {{ p.name }}
+              </a-select-option>
+            </a-select>
+          </a-form-item>
+
+          <a-form-item label="数量" required>
+            <a-input-number
+              v-model:value="quantity"
+              :min="1"
+              :max="1000"
+              size="large"
+              style="width: 100%"
+              placeholder="请输入数量"
+            />
+          </a-form-item>
+
+          <a-form-item>
+            <a-button
+              type="primary"
+              size="large"
+              block
+              @click="handleSubmit"
+              :loading="submitting"
+            >
+              提交计件
+            </a-button>
+          </a-form-item>
+        </a-form>
+      </a-card>
+    </div>
+  </WorkerLayout>
+</template>
+
+<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 { STORAGE_KEYS } from '@smartcut/shared-utils';
+import type { WorkOrder, Process } from '@smartcut/types';
+
+const authStore = useAuthStore();
+
+const workOrders = ref<WorkOrder[]>([]);
+const processes = ref<Process[]>([]);
+const workOrderId = ref<number | null>(null);
+const processId = ref<number | null>(null);
+const quantity = ref(1);
+const submitting = ref(false);
+
+const apiClient = createApiClient({
+  baseURL: window.location.origin,
+  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;
+}
+
+onMounted(async () => {
+  try {
+    const result = await workOrderApi.getWorkOrders({
+      page: 1,
+      page_size: 100,
+      status: 'in_progress'
+    });
+    workOrders.value = result.list;
+  } catch (error) {
+    message.error('加载工单失败');
+  }
+
+  try {
+    processes.value = await processApi.getProcesses();
+  } catch (error) {
+    message.error('加载工序失败');
+  }
+});
+
+async function handleSubmit() {
+  if (!workOrderId.value) {
+    message.error('请选择工单');
+    return;
+  }
+  if (!processId.value) {
+    message.error('请选择工序');
+    return;
+  }
+
+  submitting.value = true;
+  try {
+    // 手动计件:通过worker端API提交
+    await apiClient.post('/worker/submit', {
+      work_order_id: workOrderId.value,
+      process_id: processId.value,
+      quantity: quantity.value
+    });
+    message.success('计件成功');
+
+    // 重置表单
+    workOrderId.value = null;
+    processId.value = null;
+    quantity.value = 1;
+  } catch (error: any) {
+    message.error(error.response?.data?.msg || '计件失败');
+  } finally {
+    submitting.value = false;
+  }
+}
+</script>
+
+<style scoped lang="postcss">
+.submit-view { padding: 0; }
+</style>

+ 18 - 0
apps/worker-app/tailwind.config.js

@@ -0,0 +1,18 @@
+module.exports = {
+  content: [
+    './index.html',
+    './src/**/*.{vue,js,ts,jsx,tsx}',
+    '../../packages/shared-components/src/**/*.{vue,js,ts,jsx,tsx}'
+  ],
+  theme: {
+    extend: {
+      colors: {
+        primary: '#1e3a5f',
+        accent: '#f5a623'
+      }
+    }
+  },
+  corePlugins: {
+    preflight: false
+  }
+};

+ 30 - 0
apps/worker-app/tsconfig.json

@@ -0,0 +1,30 @@
+{
+  "extends": "../../tsconfig.base.json",
+  "compilerOptions": {
+    "target": "ES2020",
+    "useDefineForClassFields": true,
+    "module": "ESNext",
+    "lib": ["ES2020", "DOM", "DOM.Iterable"],
+    "skipLibCheck": true,
+    "moduleResolution": "bundler",
+    "allowImportingTsExtensions": true,
+    "resolveJsonModule": true,
+    "isolatedModules": true,
+    "noEmit": true,
+    "jsx": "preserve",
+    "strict": true,
+    "noUnusedLocals": true,
+    "noUnusedParameters": true,
+    "noFallthroughCasesInSwitch": true,
+    "baseUrl": ".",
+    "paths": {
+      "@/*": ["src/*"],
+      "@smartcut/types": ["../../packages/types/src"],
+      "@smartcut/api-client": ["../../packages/api-client/src"],
+      "@smartcut/shared-utils": ["../../packages/shared-utils/src"],
+      "@smartcut/shared-components": ["../../packages/shared-components/src"]
+    }
+  },
+  "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"],
+  "references": [{ "path": "./tsconfig.node.json" }]
+}

+ 10 - 0
apps/worker-app/tsconfig.node.json

@@ -0,0 +1,10 @@
+{
+  "compilerOptions": {
+    "composite": true,
+    "skipLibCheck": true,
+    "module": "ESNext",
+    "moduleResolution": "bundler",
+    "allowSyntheticDefaultImports": true
+  },
+  "include": ["vite.config.ts"]
+}

+ 38 - 0
apps/worker-app/vite.config.ts

@@ -0,0 +1,38 @@
+import { defineConfig } from 'vite';
+import vue from '@vitejs/plugin-vue';
+import { resolve } from 'path';
+
+export default defineConfig({
+  plugins: [vue()],
+  resolve: {
+    alias: {
+      '@': resolve(__dirname, 'src'),
+      '@smartcut/types': resolve(__dirname, '../../packages/types/src'),
+      '@smartcut/api-client': resolve(__dirname, '../../packages/api-client/src'),
+      '@smartcut/shared-utils': resolve(__dirname, '../../packages/shared-utils/src'),
+      '@smartcut/shared-components': resolve(__dirname, '../../packages/shared-components/src')
+    }
+  },
+  server: {
+    port: 3002,
+    proxy: {
+      '/api': {
+        target: 'http://localhost:8080',
+        changeOrigin: true
+      }
+    }
+  },
+  build: {
+    outDir: 'dist',
+    sourcemap: false,
+    rollupOptions: {
+      output: {
+        manualChunks: {
+          'vue-vendor': ['vue', 'vue-router', 'pinia'],
+          'antd-vendor': ['ant-design-vue', '@ant-design/icons-vue'],
+          'shared-vendor': ['axios']
+        }
+      }
+    }
+  }
+});

+ 72 - 0
deploy/deploy.ps1

@@ -0,0 +1,72 @@
+# 智裁云前端部署脚本(PowerShell)
+# 使用方法: .\deploy.ps1 -Env production
+
+param(
+    [Parameter(Mandatory=$false)]
+    [ValidateSet("development", "production")]
+    [string]$Env = "production"
+)
+
+Write-Host "开始构建智裁云前端项目..." -ForegroundColor Green
+
+# 检查pnpm是否安装
+if (!(Get-Command pnpm -ErrorAction SilentlyContinue)) {
+    Write-Host "错误: pnpm未安装,请先运行 npm install -g pnpm" -ForegroundColor Red
+    exit 1
+}
+
+# 安装依赖
+Write-Host "安装依赖..." -ForegroundColor Yellow
+pnpm install
+
+# 构建所有应用
+Write-Host "构建所有应用..." -ForegroundColor Yellow
+pnpm build:all
+
+# 检查构建产物
+$apps = @("platform-app", "factory-app", "worker-app")
+foreach ($app in $apps) {
+    $distPath = "apps\$app\dist"
+    if (Test-Path $distPath) {
+        Write-Host "✓ $app 构建成功" -ForegroundColor Green
+    } else {
+        Write-Host "✗ $app 构建失败" -ForegroundColor Red
+        exit 1
+    }
+}
+
+# 创建部署目录
+$deployDir = "dist"
+if (Test-Path $deployDir) {
+    Remove-Item $deployDir -Recurse -Force
+}
+New-Item -ItemType Directory -Path $deployDir -Force | Out-Null
+
+# 复制构建产物
+foreach ($app in $apps) {
+    $srcPath = "apps\$app\dist"
+    $destPath = "$deployDir\$app"
+    Copy-Item -Path $srcPath -Destination $destPath -Recurse
+    Write-Host "✓ 已复制 $app 到 $destPath" -ForegroundColor Green
+}
+
+# 复制Nginx配置
+Copy-Item -Path "deploy\nginx.conf" -Destination "$deployDir\nginx.conf"
+Write-Host "✓ 已复制Nginx配置" -ForegroundColor Green
+
+Write-Host ""
+Write-Host "==========================================" -ForegroundColor Cyan
+Write-Host "构建完成!" -ForegroundColor Green
+Write-Host "==========================================" -ForegroundColor Cyan
+Write-Host ""
+Write-Host "部署目录: $deployPath" -ForegroundColor Yellow
+Write-Host ""
+Write-Host "部署步骤:" -ForegroundColor Yellow
+Write-Host "1. 将 $deployDir 目录上传到服务器" -ForegroundColor White
+Write-Host "2. 复制 nginx.conf 到 /etc/nginx/conf.d/" -ForegroundColor White
+Write-Host "3. 重启Nginx: sudo nginx -s reload" -ForegroundColor White
+Write-Host ""
+Write-Host "应用访问地址:" -ForegroundColor Yellow
+Write-Host "  系统管理后台: https://smartcut.example.com/platform" -ForegroundColor White
+Write-Host "  工厂管理后台: https://smartcut.example.com/factory" -ForegroundColor White
+Write-Host "  工人端:       https://smartcut.example.com/worker" -ForegroundColor White

+ 68 - 0
deploy/nginx.conf

@@ -0,0 +1,68 @@
+# 智裁云前端Nginx部署配置示例
+# 方案:同一域名子路径部署
+
+server {
+    listen 80;
+    server_name smartcut.example.com;
+
+    # 系统管理后台
+    location /platform {
+        alias /var/www/smartcut/dist/platform-app;
+        try_files $uri $uri/ /platform/index.html;
+
+        location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
+            expires 1y;
+            add_header Cache-Control "public, immutable";
+        }
+    }
+
+    # 工厂管理后台
+    location /factory {
+        alias /var/www/smartcut/dist/factory-app;
+        try_files $uri $uri/ /factory/index.html;
+
+        location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
+            expires 1y;
+            add_header Cache-Control "public, immutable";
+        }
+    }
+
+    # 工人端
+    location /worker {
+        alias /var/www/smartcut/dist/worker-app;
+        try_files $uri $uri/ /worker/index.html;
+
+        location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
+            expires 1y;
+            add_header Cache-Control "public, immutable";
+        }
+    }
+
+    # 后端API代理
+    location /api/ {
+        proxy_pass http://127.0.0.1:8080;
+        proxy_set_header Host $host;
+        proxy_set_header X-Real-IP $remote_addr;
+        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+        proxy_set_header X-Forwarded-Proto $scheme;
+    }
+
+    # 健康检查
+    location /health {
+        proxy_pass http://127.0.0.1:8080;
+    }
+
+    # 监控端点
+    location /metrics {
+        proxy_pass http://127.0.0.1:8080;
+        # 建议添加IP白名单限制
+        # allow 10.0.0.0/8;
+        # deny all;
+    }
+
+    # Gzip压缩
+    gzip on;
+    gzip_vary on;
+    gzip_min_length 1024;
+    gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
+}

+ 78 - 0
eslint.config.base.js

@@ -0,0 +1,78 @@
+// Eslint 9.x flat config
+const js = require('@eslint/js');
+const tseslint = require('typescript-eslint');
+const vuePlugin = require('eslint-plugin-vue');
+const vueParser = require('vue-eslint-parser');
+
+module.exports = [
+  js.configs.recommended,
+  ...tseslint.configs.recommended,
+  ...vuePlugin.configs['flat/vue3-recommended'],
+  {
+    languageOptions: {
+      ecmaVersion: 'latest',
+      sourceType: 'module',
+      parser: vueParser,
+      parserOptions: {
+        parser: tseslint.parser,
+        ecmaVersion: 'latest',
+        sourceType: 'module'
+      },
+      globals: {
+        // Browser globals
+        window: 'readonly',
+        document: 'readonly',
+        navigator: 'readonly',
+        console: 'readonly',
+        localStorage: 'readonly',
+        sessionStorage: 'readonly',
+        fetch: 'readonly',
+        // Node globals
+        process: 'readonly',
+        module: 'readonly',
+        require: 'readonly',
+        __dirname: 'readonly',
+        __filename: 'readonly',
+        // ES2021 globals
+        Promise: 'readonly',
+        Symbol: 'readonly',
+        Map: 'readonly',
+        Set: 'readonly',
+        Array: 'readonly',
+        Object: 'readonly',
+        String: 'readonly',
+        Number: 'readonly',
+        Boolean: 'readonly',
+        Date: 'readonly',
+        Error: 'readonly',
+        setTimeout: 'readonly',
+        clearTimeout: 'readonly',
+        setInterval: 'readonly',
+        clearInterval: 'readonly'
+      }
+    },
+    rules: {
+      // TypeScript rules
+      '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
+      '@typescript-eslint/no-explicit-any': 'warn',
+      '@typescript-eslint/explicit-module-boundary-types': 'off',
+
+      // Vue rules
+      'vue/multi-word-component-names': 'off',
+      'vue/no-v-html': 'warn',
+      'vue/require-default-prop': 'off',
+      'vue/require-explicit-emits': 'warn',
+
+      // General rules
+      'no-console': 'warn',
+      'no-debugger': 'warn'
+    }
+  },
+  {
+    ignores: [
+      '**/node_modules/**',
+      '**/dist/**',
+      '**/*.d.ts'
+    ]
+  }
+];

+ 37 - 0
package.json

@@ -0,0 +1,37 @@
+{
+  "name": "smartcut-frontend",
+  "version": "1.0.0",
+  "private": true,
+  "description": "智裁云前端项目(Monorepo架构)",
+  "scripts": {
+    "dev:platform": "pnpm --filter platform-app dev",
+    "dev:factory": "pnpm --filter factory-app dev",
+    "dev:worker": "pnpm --filter worker-app dev",
+    "build:platform": "pnpm --filter platform-app build",
+    "build:factory": "pnpm --filter factory-app build",
+    "build:worker": "pnpm --filter worker-app build",
+    "build:all": "pnpm -r build",
+    "lint": "pnpm -r lint",
+    "format": "pnpm -r format",
+    "clean": "pnpm -r clean"
+  },
+  "devDependencies": {
+    "@types/node": "^20.0.0",
+    "typescript": "^5.3.0",
+    "eslint": "^9.0.0",
+    "@eslint/js": "^9.0.0",
+    "typescript-eslint": "^8.0.0",
+    "eslint-plugin-vue": "^9.30.0",
+    "vue-eslint-parser": "^9.4.0",
+    "prettier": "^3.2.0",
+    "vite": "^5.0.0",
+    "@vitejs/plugin-vue": "^5.0.0",
+    "tailwindcss": "^3.4.0",
+    "autoprefixer": "^10.4.0",
+    "postcss": "^8.4.0"
+  },
+  "engines": {
+    "node": ">=18.0.0",
+    "pnpm": ">=8.0.0"
+  }
+}

+ 18 - 0
packages/api-client/package.json

@@ -0,0 +1,18 @@
+{
+  "name": "@smartcut/api-client",
+  "version": "1.0.0",
+  "description": "智裁云统一API封装客户端",
+  "main": "./src/index.ts",
+  "types": "./src/index.ts",
+  "scripts": {
+    "build": "tsc --noEmit",
+    "clean": "rimraf dist"
+  },
+  "dependencies": {
+    "axios": "^1.6.0",
+    "@smartcut/types": "workspace:*"
+  },
+  "devDependencies": {
+    "typescript": "^5.3.0"
+  }
+}

+ 80 - 0
packages/api-client/src/client.ts

@@ -0,0 +1,80 @@
+// Axios客户端实例(完全重写:Query参数传递factory_id)
+
+import axios from 'axios';
+import type { AxiosInstance } from 'axios';
+import { getSessionStorage, removeSessionStorage, setSessionStorage, STORAGE_KEYS } from '@smartcut/shared-utils';
+
+export interface ApiClientConfig {
+  baseURL: string;
+  tokenKey: string; // localStorage中的token键名
+}
+
+/**
+ * 创建API客户端实例
+ * 关键改变:factory_id通过Query参数传递,废弃路径参数模式
+ */
+export function createApiClient(config: ApiClientConfig): AxiosInstance {
+  const instance = axios.create({
+    baseURL: config.baseURL,
+    timeout: 30000,
+    headers: {
+      'Content-Type': 'application/json'
+    }
+  });
+
+  // 请求拦截器:注入JWT Token + factory_id(全新实现)
+  instance.interceptors.request.use(
+    (requestConfig) => {
+      // 1. 注入JWT Token
+      const token = getSessionStorage<string>(config.tokenKey);
+      if (token) {
+        requestConfig.headers.Authorization = `Bearer ${token}`;
+      }
+
+      // 2. 工厂ID处理(完全重写:废弃路径参数模式)
+      // 从SessionStorage读取当前工厂ID(登录时存储)
+      const factoryId = getSessionStorage<string>(STORAGE_KEYS.CURRENT_FACTORY_ID);
+
+      if (factoryId) {
+        // 新方案:通过Query参数传递factory_id
+        if (requestConfig.params) {
+          requestConfig.params.factory_id = factoryId;
+        } else {
+          requestConfig.params = { factory_id: factoryId };
+        }
+
+        // POST/PUT请求也可在请求体中传递
+        if (requestConfig.data && typeof requestConfig.data === 'object' && requestConfig.method !== 'GET') {
+          requestConfig.data.factory_id = factoryId;
+        }
+      }
+
+      return requestConfig;
+    },
+    (error) => Promise.reject(error)
+  );
+
+  // 响应拦截器:Token续期 + 401处理(保持不变)
+  instance.interceptors.response.use(
+    (response) => {
+      const newToken = response.headers['x-new-token'];
+      if (newToken) {
+        setSessionStorage(config.tokenKey, newToken);
+      }
+      return response;
+    },
+    (error) => {
+      if (error.response?.status === 401) {
+        // 401时清除Token和factory_id
+        removeSessionStorage(config.tokenKey);
+        removeSessionStorage(STORAGE_KEYS.CURRENT_FACTORY_ID);
+
+        // 跳转登录页面(需根据应用类型判断)
+        window.location.href = '/login';
+      }
+      return Promise.reject(error);
+    }
+  );
+
+  return instance;
+}

+ 33 - 0
packages/api-client/src/endpoints/auth.ts

@@ -0,0 +1,33 @@
+// 认证API端点
+
+import type { AxiosInstance } from 'axios';
+import type { LoginRequest, LoginResponse, User, SystemUser, ChangePasswordRequest } from '@smartcut/types';
+import type { ApiResponse } from '@smartcut/types';
+
+export class AuthApi {
+  constructor(private client: AxiosInstance) {}
+
+  /**
+   * 用户登录
+   * 新增factory_id参数(Query参数传递)
+   */
+  async login(data: LoginRequest): Promise<LoginResponse> {
+    const response = await this.client.post<ApiResponse<LoginResponse>>('/auth/login', data);
+    return response.data.data;
+  }
+
+  /**
+   * 获取当前用户信息
+   */
+  async getProfile(): Promise<User | SystemUser> {
+    const response = await this.client.get<ApiResponse<User | SystemUser>>('/auth/profile');
+    return response.data.data;
+  }
+
+  /**
+   * 修改密码
+   */
+  async changePassword(data: ChangePasswordRequest): Promise<void> {
+    await this.client.put<ApiResponse>('/auth/password', data);
+  }
+}

+ 68 - 0
packages/api-client/src/endpoints/factory.ts

@@ -0,0 +1,68 @@
+// 工厂管理API端点(系统级)
+
+import type { AxiosInstance } from 'axios';
+import type { Factory, ApiResponse, PaginatedResponse } from '@smartcut/types';
+
+export interface CreateFactoryRequest {
+  id: string;
+  name: string;
+  db_path?: string;
+}
+
+export interface UpdateFactoryRequest {
+  name: string;
+  db_path?: string;
+}
+
+export class FactoryApi {
+  constructor(private client: AxiosInstance) {}
+
+  /**
+   * 获取工厂列表
+   */
+  async listFactories(): Promise<PaginatedResponse<Factory>> {
+    const response = await this.client.get<ApiResponse<PaginatedResponse<Factory>>>('/factories');
+    return response.data.data;
+  }
+
+  /**
+   * 创建工厂
+   */
+  async createFactory(data: CreateFactoryRequest): Promise<Factory> {
+    const response = await this.client.post<ApiResponse<Factory>>('/factories', data);
+    return response.data.data;
+  }
+
+  /**
+   * 获取工厂详情
+   */
+  async getFactory(id: string): Promise<Factory> {
+    const response = await this.client.get<ApiResponse<Factory>>(`/factories/${id}`);
+    return response.data.data;
+  }
+
+  /**
+   * 更新工厂
+   */
+  async updateFactory(id: string, data: UpdateFactoryRequest): Promise<Factory> {
+    const response = await this.client.put<ApiResponse<Factory>>(`/factories/${id}`, data);
+    return response.data.data;
+  }
+
+  /**
+   * 删除工厂(软删除或永久删除)
+   */
+  async deleteFactory(id: string, permanent = false): Promise<void> {
+    await this.client.delete<ApiResponse>(`/factories/${id}`, {
+      params: { permanent },
+      headers: { 'X-Confirm': 'true' }
+    });
+  }
+
+  /**
+   * 启用工厂
+   */
+  async enableFactory(id: string): Promise<void> {
+    await this.client.post<ApiResponse>(`/factories/${id}/enable`);
+  }
+}

+ 39 - 0
packages/api-client/src/endpoints/process.ts

@@ -0,0 +1,39 @@
+// 工序管理API端点
+
+import type { AxiosInstance } from 'axios';
+import type { Process, CreateProcessRequest, UpdateProcessRequest, ApiResponse } from '@smartcut/types';
+
+export class ProcessApi {
+  constructor(private client: AxiosInstance) {}
+
+  /**
+   * 获取工序列表
+   */
+  async getProcesses(): Promise<Process[]> {
+    const response = await this.client.get<ApiResponse<Process[]>>('/processes');
+    return response.data.data;
+  }
+
+  /**
+   * 创建工序
+   */
+  async createProcess(data: CreateProcessRequest): Promise<Process> {
+    const response = await this.client.post<ApiResponse<Process>>('/processes', data);
+    return response.data.data;
+  }
+
+  /**
+   * 更新工序
+   */
+  async updateProcess(id: number, data: UpdateProcessRequest): Promise<Process> {
+    const response = await this.client.put<ApiResponse<Process>>(`/processes/${id}`, data);
+    return response.data.data;
+  }
+
+  /**
+   * 删除工序
+   */
+  async deleteProcess(id: number): Promise<void> {
+    await this.client.delete<ApiResponse>(`/processes/${id}`);
+  }
+}

+ 61 - 0
packages/api-client/src/endpoints/record.ts

@@ -0,0 +1,61 @@
+// 计件记录API端点
+
+import type { AxiosInstance } from 'axios';
+import type {
+  PieceRecord,
+  ScanRequest,
+  RecordQueryParams,
+  WorkerRecordQueryParams,
+  ApiResponse,
+  PaginatedResponse
+} from '@smartcut/types';
+
+export class RecordApi {
+  constructor(private client: AxiosInstance) {}
+
+  /**
+   * 扫码计件
+   */
+  async scanPiece(data: ScanRequest): Promise<PieceRecord> {
+    const response = await this.client.post<ApiResponse<PieceRecord>>('/records/scan', data);
+    return response.data.data;
+  }
+
+  /**
+   * 获取计件记录列表(管理员)
+   */
+  async getRecords(params?: RecordQueryParams): Promise<PaginatedResponse<PieceRecord>> {
+    const response = await this.client.get<ApiResponse<PaginatedResponse<PieceRecord>>>('/records', { params });
+    return response.data.data;
+  }
+
+  /**
+   * 获取计件记录详情
+   */
+  async getRecord(id: number): Promise<PieceRecord> {
+    const response = await this.client.get<ApiResponse<PieceRecord>>(`/records/${id}`);
+    return response.data.data;
+  }
+
+  /**
+   * 退回计件记录
+   */
+  async revertRecord(id: number): Promise<void> {
+    await this.client.delete<ApiResponse>(`/records/${id}`);
+  }
+
+  /**
+   * 撤销退回
+   */
+  async unrevertRecord(id: number): Promise<void> {
+    await this.client.post<ApiResponse>(`/records/${id}/unrevert`);
+  }
+
+  /**
+   * 工人端:获取个人计件记录
+   */
+  async getWorkerRecords(params?: WorkerRecordQueryParams): Promise<PaginatedResponse<PieceRecord>> {
+    const response = await this.client.get<ApiResponse<PaginatedResponse<PieceRecord>>>('/worker/records', { params });
+    return response.data.data;
+  }
+}

+ 66 - 0
packages/api-client/src/endpoints/salary.ts

@@ -0,0 +1,66 @@
+// 工资管理API端点
+
+import type { AxiosInstance } from 'axios';
+import type {
+  SalaryPeriod,
+  SalaryDetailItem,
+  WorkerSalary,
+  GenerateSalaryRequest,
+  ApiResponse
+} from '@smartcut/types';
+
+export class SalaryApi {
+  constructor(private client: AxiosInstance) {}
+
+  /**
+   * 生成工资
+   */
+  async generateSalary(data: GenerateSalaryRequest): Promise<void> {
+    await this.client.post<ApiResponse>('/salary/generate', data);
+  }
+
+  /**
+   * 获取工资周期列表
+   */
+  async getSalaryList(): Promise<SalaryPeriod[]> {
+    const response = await this.client.get<ApiResponse<SalaryPeriod[]>>('/salary/list');
+    return response.data.data;
+  }
+
+  /**
+   * 获取工资明细
+   */
+  async getSalaryDetail(period: string): Promise<SalaryDetailItem[]> {
+    const response = await this.client.get<ApiResponse<SalaryDetailItem[]>>(`/salary/detail/${period}`);
+    return response.data.data;
+  }
+
+  /**
+   * 锁定工资周期
+   */
+  async lockSalary(period: string): Promise<void> {
+    await this.client.post<ApiResponse>(`/salary/lock/${period}`);
+  }
+
+  /**
+   * 解锁工资周期
+   */
+  async unlockSalary(period: string): Promise<void> {
+    await this.client.post<ApiResponse>(`/salary/unlock/${period}`);
+  }
+
+  /**
+   * 删除工资周期
+   */
+  async deleteSalary(period: string): Promise<void> {
+    await this.client.delete<ApiResponse>(`/salary/${period}`);
+  }
+
+  /**
+   * 工人端:获取个人工资
+   */
+  async getWorkerSalary(): Promise<WorkerSalary[]> {
+    const response = await this.client.get<ApiResponse<WorkerSalary[]>>('/worker/salary');
+    return response.data.data;
+  }
+}

+ 56 - 0
packages/api-client/src/endpoints/stats.ts

@@ -0,0 +1,56 @@
+// 数据统计API端点
+
+import type { AxiosInstance } from 'axios';
+import type {
+  DashboardData,
+  ProductionData,
+  RankingData,
+  ProgressMatrixData,
+  WorkerDashboardData,
+  StatsQueryParams,
+  ApiResponse
+} from '@smartcut/types';
+
+export class StatsApi {
+  constructor(private client: AxiosInstance) {}
+
+  /**
+   * 管理看板
+   */
+  async getDashboard(): Promise<DashboardData> {
+    const response = await this.client.get<ApiResponse<DashboardData>>('/stats/dashboard');
+    return response.data.data;
+  }
+
+  /**
+   * 生产进度
+   */
+  async getProduction(): Promise<ProductionData[]> {
+    const response = await this.client.get<ApiResponse<ProductionData[]>>('/stats/production');
+    return response.data.data;
+  }
+
+  /**
+   * 员工排行
+   */
+  async getRanking(params?: StatsQueryParams): Promise<RankingData[]> {
+    const response = await this.client.get<ApiResponse<RankingData[]>>('/stats/ranking', { params });
+    return response.data.data;
+  }
+
+  /**
+   * 生产进度矩阵
+   */
+  async getProgressMatrix(params?: StatsQueryParams): Promise<ProgressMatrixData[]> {
+    const response = await this.client.get<ApiResponse<ProgressMatrixData[]>>('/stats/progress', { params });
+    return response.data.data;
+  }
+
+  /**
+   * 工人看板
+   */
+  async getWorkerDashboard(): Promise<WorkerDashboardData> {
+    const response = await this.client.get<ApiResponse<WorkerDashboardData>>('/worker/dashboard');
+    return response.data.data;
+  }
+}

+ 54 - 0
packages/api-client/src/endpoints/user.ts

@@ -0,0 +1,54 @@
+// 用户管理API端点
+
+import type { AxiosInstance } from 'axios';
+import type { User, CreateUserRequest, UpdateUserRequest, ResetPasswordRequest, ApiResponse, PaginatedResponse } from '@smartcut/types';
+
+export class UserApi {
+  constructor(private client: AxiosInstance) {}
+
+  /**
+   * 获取用户列表
+   */
+  async getUsers(params?: { page?: number; page_size?: number; role?: string; keyword?: string }): Promise<PaginatedResponse<User>> {
+    const response = await this.client.get<ApiResponse<PaginatedResponse<User>>>('/users', { params });
+    return response.data.data;
+  }
+
+  /**
+   * 创建用户
+   */
+  async createUser(data: CreateUserRequest): Promise<User> {
+    const response = await this.client.post<ApiResponse<User>>('/users', data);
+    return response.data.data;
+  }
+
+  /**
+   * 获取用户详情
+   */
+  async getUser(id: number): Promise<User> {
+    const response = await this.client.get<ApiResponse<User>>(`/users/${id}`);
+    return response.data.data;
+  }
+
+  /**
+   * 更新用户
+   */
+  async updateUser(id: number, data: UpdateUserRequest): Promise<User> {
+    const response = await this.client.put<ApiResponse<User>>(`/users/${id}`, data);
+    return response.data.data;
+  }
+
+  /**
+   * 删除用户
+   */
+  async deleteUser(id: number): Promise<void> {
+    await this.client.delete<ApiResponse>(`/users/${id}`);
+  }
+
+  /**
+   * 重置用户密码
+   */
+  async resetPassword(id: number, data: ResetPasswordRequest): Promise<void> {
+    await this.client.post<ApiResponse>(`/users/${id}/reset-password`, data);
+  }
+}

+ 64 - 0
packages/api-client/src/endpoints/workOrder.ts

@@ -0,0 +1,64 @@
+// 工单管理API端点
+
+import type { AxiosInstance } from 'axios';
+import type {
+  WorkOrder,
+  CreateWorkOrderRequest,
+  UpdateWorkOrderRequest,
+  WorkOrderQueryParams,
+  ApiResponse,
+  PaginatedResponse
+} from '@smartcut/types';
+
+export class WorkOrderApi {
+  constructor(private client: AxiosInstance) {}
+
+  /**
+   * 获取工单列表
+   */
+  async getWorkOrders(params?: WorkOrderQueryParams): Promise<PaginatedResponse<WorkOrder>> {
+    const response = await this.client.get<ApiResponse<PaginatedResponse<WorkOrder>>>('/work-orders', { params });
+    return response.data.data;
+  }
+
+  /**
+   * 创建工单
+   */
+  async createWorkOrder(data: CreateWorkOrderRequest): Promise<WorkOrder> {
+    const response = await this.client.post<ApiResponse<WorkOrder>>('/work-orders', data);
+    return response.data.data;
+  }
+
+  /**
+   * 获取工单详情
+   */
+  async getWorkOrder(id: number): Promise<WorkOrder> {
+    const response = await this.client.get<ApiResponse<WorkOrder>>(`/work-orders/${id}`);
+    return response.data.data;
+  }
+
+  /**
+   * 更新工单(PATCH语义)
+   */
+  async updateWorkOrder(id: number, data: UpdateWorkOrderRequest): Promise<WorkOrder> {
+    const response = await this.client.put<ApiResponse<WorkOrder>>(`/work-orders/${id}`, data);
+    return response.data.data;
+  }
+
+  /**
+   * 删除工单
+   */
+  async deleteWorkOrder(id: number): Promise<void> {
+    await this.client.delete<ApiResponse>(`/work-orders/${id}`);
+  }
+
+  /**
+   * 生成工单二维码
+   */
+  async getWorkOrderQRCode(id: number): Promise<Blob> {
+    const response = await this.client.get(`/work-orders/${id}/qrcode`, {
+      responseType: 'blob'
+    });
+    return response.data;
+  }
+}

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

@@ -0,0 +1,13 @@
+// 智裁云API客户端导出
+
+export { createApiClient } from './client';
+export { AuthApi } from './endpoints/auth';
+export { FactoryApi } from './endpoints/factory';
+export { WorkOrderApi } from './endpoints/workOrder';
+export { UserApi } from './endpoints/user';
+export { ProcessApi } from './endpoints/process';
+export { RecordApi } from './endpoints/record';
+export { SalaryApi } from './endpoints/salary';
+export { StatsApi } from './endpoints/stats';
+
+export type { ApiClientConfig } from './client';

+ 12 - 0
packages/api-client/tsconfig.json

@@ -0,0 +1,12 @@
+{
+  "extends": "../../tsconfig.base.json",
+  "compilerOptions": {
+    "baseUrl": ".",
+    "paths": {
+      "@smartcut/types": ["../types/src"],
+      "@smartcut/shared-utils": ["../shared-utils/src"],
+      "@smartcut/api-client": ["./src"]
+    }
+  },
+  "include": ["src/**/*.ts"]
+}

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

@@ -0,0 +1,28 @@
+{
+  "name": "@smartcut/shared-components",
+  "version": "1.0.0",
+  "description": "智裁云共享组件库",
+  "main": "./src/index.ts",
+  "types": "./src/index.ts",
+  "scripts": {
+    "build": "tsc --noEmit",
+    "clean": "rimraf dist"
+  },
+  "dependencies": {
+    "vue": "^3.4.0",
+    "ant-design-vue": "^4.0.0",
+    "@ant-design/icons-vue": "^7.0.0",
+    "echarts": "^5.4.0",
+    "@smartcut/types": "workspace:*",
+    "@smartcut/shared-utils": "workspace:*"
+  },
+  "devDependencies": {
+    "typescript": "^5.3.0",
+    "vite": "^5.0.0",
+    "@vitejs/plugin-vue": "^5.0.0"
+  },
+  "peerDependencies": {
+    "vue": "^3.4.0",
+    "ant-design-vue": "^4.0.0"
+  }
+}

+ 31 - 0
packages/shared-components/src/common/AppToast.vue

@@ -0,0 +1,31 @@
+<template>
+  <a-message
+    :content="message"
+    :type="type"
+    :duration="duration"
+  />
+</template>
+
+<script setup lang="ts">
+import { ref } from 'vue';
+import { message } from 'ant-design-vue';
+
+const props = defineProps<{
+  type?: 'success' | 'error' | 'warning' | 'info';
+  duration?: number;
+}>();
+
+const [messageApi, contextHolder] = message.useMessage();
+
+function show(content: string) {
+  messageApi.open({
+    content,
+    type: props.type || 'success',
+    duration: props.duration || 3
+  });
+}
+
+defineExpose({
+  show
+});
+</script>

+ 7 - 0
packages/shared-components/src/index.ts

@@ -0,0 +1,7 @@
+// 智裁云共享组件导出
+
+// Layout components
+export { default as SidebarLayout } from './layout/SidebarLayout.vue';
+
+// Common components
+export { default as AppToast } from './common/AppToast.vue';

+ 163 - 0
packages/shared-components/src/layout/SidebarLayout.vue

@@ -0,0 +1,163 @@
+<template>
+  <div class="sidebar-layout">
+    <!-- 侧边栏 -->
+    <a-layout-sider v-model:collapsed="collapsed" :trigger="null" collapsible class="sidebar">
+      <div class="logo">
+        <h1 v-if="!collapsed">智裁云</h1>
+        <h1 v-else>裁</h1>
+      </div>
+
+      <!-- 菜单 -->
+      <a-menu
+        v-model:selectedKeys="selectedKeys"
+        mode="inline"
+        :theme="theme"
+        class="menu"
+      >
+        <a-menu-item v-for="item in menuItems" :key="item.key">
+          <router-link :to="item.path">
+            <component :is="item.icon" />
+            <span>{{ item.label }}</span>
+          </router-link>
+        </a-menu-item>
+      </a-menu>
+    </a-layout-sider>
+
+    <!-- 主内容区 -->
+    <a-layout class="main-layout">
+      <!-- 头部 -->
+      <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>
+
+      <!-- 内容区 -->
+      <a-layout-content class="content">
+        <slot />
+      </a-layout-content>
+    </a-layout>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, computed } from 'vue';
+import { MenuFoldOutlined, MenuUnfoldOutlined } from '@ant-design/icons-vue';
+
+interface MenuItem {
+  key: string;
+  label: string;
+  icon: any;
+  path: string;
+}
+
+const props = defineProps<{
+  menuItems: MenuItem[];
+  userName?: string;
+  userAvatar?: string;
+  theme?: 'light' | 'dark';
+  activeKey?: string;
+}>();
+
+const emit = defineEmits<{
+  logout: [];
+}>();
+
+const collapsed = ref(false);
+const selectedKeys = computed(() => [props.activeKey || '']);
+
+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>

+ 5 - 0
packages/shared-components/src/shims-vue.d.ts

@@ -0,0 +1,5 @@
+declare module '*.vue' {
+  import type { DefineComponent } from 'vue'
+  const component: DefineComponent<Record<string, unknown>, Record<string, unknown>, unknown>
+  export default component
+}

+ 12 - 0
packages/shared-components/tsconfig.json

@@ -0,0 +1,12 @@
+{
+  "extends": "../../tsconfig.base.json",
+  "compilerOptions": {
+    "baseUrl": ".",
+    "paths": {
+      "@smartcut/types": ["../types/src"],
+      "@smartcut/shared-utils": ["../shared-utils/src"],
+      "@smartcut/shared-components": ["./src"]
+    }
+  },
+  "include": ["src/**/*.ts", "src/**/*.vue"]
+}

+ 17 - 0
packages/shared-utils/package.json

@@ -0,0 +1,17 @@
+{
+  "name": "@smartcut/shared-utils",
+  "version": "1.0.0",
+  "description": "智裁云共享工具函数库",
+  "main": "./src/index.ts",
+  "types": "./src/index.ts",
+  "scripts": {
+    "build": "tsc --noEmit",
+    "clean": "rimraf dist"
+  },
+  "dependencies": {
+    "@smartcut/types": "workspace:*"
+  },
+  "devDependencies": {
+    "typescript": "^5.3.0"
+  }
+}

+ 79 - 0
packages/shared-utils/src/errorHandler.ts

@@ -0,0 +1,79 @@
+// 错误处理工具函数
+
+import type { ApiResponse } from '@smartcut/types';
+
+/**
+ * 检查API响应是否成功
+ */
+export function isApiSuccess(response: ApiResponse): boolean {
+  return response.code === 0;
+}
+
+/**
+ * 获取API错误消息
+ */
+export function getApiErrorMessage(response: ApiResponse): string {
+  return response.msg || '请求失败';
+}
+
+/**
+ * 处理网络错误
+ */
+export function handleNetworkError(error: any): string {
+  if (error.response) {
+    // 后端返回错误
+    const status = error.response.status;
+    switch (status) {
+      case 400:
+        return '请求参数错误';
+      case 401:
+        return '未认证或Token过期';
+      case 403:
+        return '权限不足';
+      case 404:
+        return '资源不存在';
+      case 500:
+        return '服务器内部错误';
+      default:
+        return `请求失败: ${status}`;
+    }
+  } else if (error.request) {
+    // 网络错误
+    return '网络错误,请检查网络连接';
+  } else {
+    // 其他错误
+    return error.message || '未知错误';
+  }
+}
+
+/**
+ * 判断是否为敏感错误(需要脱敏处理)
+ */
+export function isSensitiveError(error: any): boolean {
+  const message = error.response?.data?.msg || error.message;
+  if (!message) return false;
+
+  // 包含"失败"关键词的错误需要脱敏
+  const sensitiveKeywords = ['失败', '错误', '异常'];
+  return sensitiveKeywords.some(keyword => message.includes(keyword));
+}
+
+/**
+ * 获取脱敏后的错误消息
+ */
+export function getSafeErrorMessage(error: any): string {
+  if (isSensitiveError(error)) {
+    return '操作失败,请稍后重试';
+  }
+  return handleNetworkError(error);
+}
+
+/**
+ * XSS防护:HTML转义
+ */
+export function escapeHtml(text: string | null | undefined): string {
+  if (text === null || text === undefined) return '';
+  const div = document.createElement('div');
+  div.textContent = text;
+  return div.innerHTML;
+}

+ 57 - 0
packages/shared-utils/src/format.ts

@@ -0,0 +1,57 @@
+// 格式化工具函数
+
+/**
+ * 格式化日期 YYYY-MM-DD
+ */
+export function formatDate(dateStr: string | Date | null | undefined): string {
+  if (!dateStr) return '-';
+  const d = new Date(dateStr);
+  if (isNaN(d.getTime())) return String(dateStr);
+  const year = d.getFullYear();
+  const month = String(d.getMonth() + 1).padStart(2, '0');
+  const day = String(d.getDate()).padStart(2, '0');
+  return `${year}-${month}-${day}`;
+}
+
+/**
+ * 格式化日期时间 YYYY-MM-DD HH:mm
+ */
+export function formatDateTime(dateStr: string | Date | null | undefined): string {
+  if (!dateStr) return '-';
+  const d = new Date(dateStr);
+  if (isNaN(d.getTime())) return String(dateStr);
+  const year = d.getFullYear();
+  const month = String(d.getMonth() + 1).padStart(2, '0');
+  const day = String(d.getDate()).padStart(2, '0');
+  const hour = String(d.getHours()).padStart(2, '0');
+  const minute = String(d.getMinutes()).padStart(2, '0');
+  return `${year}-${month}-${day} ${hour}:${minute}`;
+}
+
+/**
+ * 格式化金额(带千分位分隔符,保留2位小数)
+ */
+export function formatMoney(amount: number | null | undefined): string {
+  if (amount == null || isNaN(amount)) return '0.00';
+  return Number(amount).toLocaleString('zh-CN', {
+    minimumFractionDigits: 2,
+    maximumFractionDigits: 2
+  });
+}
+
+/**
+ * 格式化数量(带千分位分隔符,无小数)
+ */
+export function formatNumber(num: number | null | undefined): string {
+  if (num == null || isNaN(num)) return '0';
+  return Number(num).toLocaleString('zh-CN');
+}
+
+/**
+ * 格式化百分比
+ */
+export function formatPercent(value: number, total: number): string {
+  if (total === 0) return '0%';
+  const percent = (value / total) * 100;
+  return `${percent.toFixed(1)}%`;
+}

+ 7 - 0
packages/shared-utils/src/index.ts

@@ -0,0 +1,7 @@
+// 智裁云共享工具函数导出
+
+export * from './format';
+export * from './validate';
+export * from './permission';
+export * from './storage';
+export * from './errorHandler';

+ 66 - 0
packages/shared-utils/src/permission.ts

@@ -0,0 +1,66 @@
+// 权限检查工具函数
+
+import type { User, SystemUser } from '@smartcut/types';
+
+/**
+ * 检查是否为系统管理员
+ */
+export function isSystemAdmin(user: SystemUser | User): boolean {
+  return (user as SystemUser).username !== undefined && !(user as User).role;
+}
+
+/**
+ * 检查是否为工厂管理员
+ */
+export function isFactoryAdmin(user: User): boolean {
+  return user.role === 'factory_admin';
+}
+
+/**
+ * 检查是否为工人
+ */
+export function isWorker(user: User): boolean {
+  return user.role === 'worker';
+}
+
+/**
+ * 检查用户状态是否为启用
+ */
+export function isUserEnabled(user: User | SystemUser): boolean {
+  return user.status === 1;
+}
+
+/**
+ * 检查是否需要强制修改密码
+ */
+export function needsPasswordChange(user: User | SystemUser): boolean {
+  return user.must_change_password === true;
+}
+
+/**
+ * 检查是否有权限访问工厂资源
+ */
+export function canAccessFactory(user: User, _factoryId: string): boolean {
+  return isUserEnabled(user);
+}
+
+/**
+ * 检查是否有权限管理用户
+ */
+export function canManageUsers(user: User): boolean {
+  return isFactoryAdmin(user);
+}
+
+/**
+ * 检查是否有权限创建工单
+ */
+export function canCreateWorkOrder(user: User): boolean {
+  return isFactoryAdmin(user);
+}
+
+/**
+ * 检查是否有权限管理工资
+ */
+export function canManageSalary(user: User): boolean {
+  return isFactoryAdmin(user);
+}

+ 112 - 0
packages/shared-utils/src/storage.ts

@@ -0,0 +1,112 @@
+// localStorage和sessionStorage封装
+
+/**
+ * localStorage存储键常量
+ */
+export const STORAGE_KEYS = {
+  PLATFORM_TOKEN: 'platform_token',
+  PLATFORM_USER: 'platform_user',
+  FACTORY_TOKEN: 'factory_token',
+  FACTORY_USER: 'factory_user',
+  FACTORY_REDIRECT_URL: 'factory_redirect_url',
+  WORKER_TOKEN: 'worker_token',
+  WORKER_USER: 'worker_user',
+  WORKER_REDIRECT_URL: 'worker_redirect_url',
+  CURRENT_FACTORY_ID: 'current_factory_id' // 新增:当前工厂ID(SessionStorage)
+};
+
+/**
+ * 安全存储到localStorage
+ */
+export function setLocalStorage(key: string, value: any): void {
+  try {
+    const serialized = JSON.stringify(value);
+    localStorage.setItem(key, serialized);
+  } catch (error) {
+    console.error('localStorage存储失败:', error);
+  }
+}
+
+/**
+ * 从localStorage安全读取
+ */
+export function getLocalStorage<T = any>(key: string): T | null {
+  try {
+    const serialized = localStorage.getItem(key);
+    if (!serialized) return null;
+    return JSON.parse(serialized) as T;
+  } catch (error) {
+    console.error('localStorage读取失败:', error);
+    return null;
+  }
+}
+
+/**
+ * 从localStorage删除
+ */
+export function removeLocalStorage(key: string): void {
+  try {
+    localStorage.removeItem(key);
+  } catch (error) {
+    console.error('localStorage删除失败:', error);
+  }
+}
+
+/**
+ * 清空localStorage(谨慎使用)
+ */
+export function clearLocalStorage(): void {
+  try {
+    localStorage.clear();
+  } catch (error) {
+    console.error('localStorage清空失败:', error);
+  }
+}
+
+/**
+ * 安全存储到sessionStorage
+ */
+export function setSessionStorage(key: string, value: any): void {
+  try {
+    const serialized = JSON.stringify(value);
+    sessionStorage.setItem(key, serialized);
+  } catch (error) {
+    console.error('sessionStorage存储失败:', error);
+  }
+}
+
+/**
+ * 从sessionStorage安全读取
+ */
+export function getSessionStorage<T = any>(key: string): T | null {
+  try {
+    const serialized = sessionStorage.getItem(key);
+    if (!serialized) return null;
+    return JSON.parse(serialized) as T;
+  } catch (error) {
+    console.error('sessionStorage读取失败:', error);
+    return null;
+  }
+}
+
+/**
+ * 从sessionStorage删除
+ */
+export function removeSessionStorage(key: string): void {
+  try {
+    sessionStorage.removeItem(key);
+  } catch (error) {
+    console.error('sessionStorage删除失败:', error);
+  }
+}
+
+/**
+ * 清空sessionStorage
+ */
+export function clearSessionStorage(): void {
+  try {
+    sessionStorage.clear();
+  } catch (error) {
+    console.error('sessionStorage清空失败:', error);
+  }
+}

+ 71 - 0
packages/shared-utils/src/validate.ts

@@ -0,0 +1,71 @@
+// 验证工具函数
+
+/**
+ * 验证手机号格式
+ */
+export function validatePhone(phone: string): boolean {
+  const phoneRegex = /^1[3-9]\d{9}$/;
+  return phoneRegex.test(phone);
+}
+
+/**
+ * 验证用户名格式(2-32位,仅字母/数字/下划线/中文)
+ */
+export function validateUsername(username: string): boolean {
+  const usernameRegex = /^[a-zA-Z0-9_\u4e00-\u9fa5]{2,32}$/;
+  return usernameRegex.test(username);
+}
+
+/**
+ * 验证密码强度(至少8位,包含大小写字母、数字、特殊字符中3类)
+ */
+export function validatePasswordStrength(password: string): boolean {
+  if (password.length < 8) return false;
+
+  const hasUpper = /[A-Z]/.test(password);
+  const hasLower = /[a-z]/.test(password);
+  const hasNumber = /[0-9]/.test(password);
+  const hasSpecial = /[!@#$%^&*(),.?":{}|<>]/.test(password);
+
+  const typeCount = [hasUpper, hasLower, hasNumber, hasSpecial].filter(Boolean).length;
+  return typeCount >= 3;
+}
+
+/**
+ * 验证日期格式 YYYY-MM-DD
+ */
+export function validateDate(dateStr: string): boolean {
+  const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
+  if (!dateRegex.test(dateStr)) return false;
+
+  const date = new Date(dateStr);
+  return !isNaN(date.getTime());
+}
+
+/**
+ * 验证工资周期格式 YYYY-MM
+ */
+export function validateSalaryPeriod(period: string): boolean {
+  const periodRegex = /^\d{4}-\d{2}$/;
+  if (!periodRegex.test(period)) return false;
+
+  const [year, month] = period.split('-');
+  const yearNum = parseInt(year);
+  const monthNum = parseInt(month);
+
+  return yearNum >= 2020 && yearNum <= 2100 && monthNum >= 1 && monthNum <= 12;
+}
+
+/**
+ * 验证数量范围
+ */
+export function validateQuantity(quantity: number, min = 1, max = 100000): boolean {
+  return quantity >= min && quantity <= max;
+}
+
+/**
+ * 验证金额范围
+ */
+export function validatePrice(price: number, min = 0, max = 10000): boolean {
+  return price >= min && price <= max;
+}

+ 11 - 0
packages/shared-utils/tsconfig.json

@@ -0,0 +1,11 @@
+{
+  "extends": "../../tsconfig.base.json",
+  "compilerOptions": {
+    "baseUrl": ".",
+    "paths": {
+      "@smartcut/types": ["../types/src"],
+      "@smartcut/shared-utils": ["./src"]
+    }
+  },
+  "include": ["src/**/*.ts"]
+}

+ 14 - 0
packages/types/package.json

@@ -0,0 +1,14 @@
+{
+  "name": "@smartcut/types",
+  "version": "1.0.0",
+  "description": "智裁云TypeScript类型定义",
+  "main": "./src/index.ts",
+  "types": "./src/index.ts",
+  "scripts": {
+    "build": "tsc --noEmit",
+    "clean": "rimraf dist"
+  },
+  "devDependencies": {
+    "typescript": "^5.3.0"
+  }
+}

+ 27 - 0
packages/types/src/api.ts

@@ -0,0 +1,27 @@
+// API响应通用类型
+import type { User, SystemUser } from './user';
+
+export interface ApiResponse<T = any> {
+  code: number;
+  msg: string;
+  data: T;
+}
+
+export interface PaginatedResponse<T> {
+  list: T[];
+  total: number;
+  page: number;
+  page_size: number;
+}
+
+export interface LoginRequest {
+  username?: string;
+  phone?: string;
+  password: string;
+  factory_id?: string; // 新增:工厂ID参数(Query参数传递模式)
+}
+
+export interface LoginResponse {
+  token: string;
+  user: User | SystemUser;
+}

+ 10 - 0
packages/types/src/index.ts

@@ -0,0 +1,10 @@
+// 智裁云TypeScript类型定义导出
+
+export * from './api';
+export * from './models';
+export * from './user';
+export * from './workOrder';
+export * from './process';
+export * from './record';
+export * from './salary';
+export * from './stats';

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

@@ -0,0 +1,67 @@
+// 基础数据模型类型
+
+// 用户角色常量
+export const RoleSystemAdmin = 'system_admin';
+export const RoleFactoryAdmin = 'factory_admin';
+export const RoleWorker = 'worker';
+
+// 工单/批次状态常量
+export const StatusPending = 'pending';
+export const StatusInProgress = 'in_progress';
+export const StatusCompleted = 'completed';
+export const StatusCancelled = 'cancelled';
+
+// 计件记录状态常量
+export const RecordStatusNormal = 'normal';
+export const RecordStatusReverted = 'reverted';
+
+// 工资周期状态常量
+export const SalaryStatusPending = 'pending';
+export const SalaryStatusLocked = 'locked';
+
+// 工厂类型
+export interface Factory {
+  id: string;
+  name: string;
+  db_path: string;
+  status: number; // 1=启用, 0=禁用
+  created_at: string;
+  updated_at: string;
+}
+
+// 裁床批次类型
+export interface CutBatch {
+  id: number;
+  work_order_id: number;
+  batch_no: string;
+  color: string;
+  total_qty: number;
+  completed_qty: number;
+  status: string;
+  created_at: string;
+  updated_at: string;
+}
+
+// 扎号(分扎)类型
+export interface CutBundle {
+  id: number;
+  batch_id: number;
+  bundle_no: string;
+  size: string;
+  quantity: number;
+  qr_code: string;
+  status: string;
+  created_at: string;
+  updated_at: string;
+}
+
+// 工价模板类型
+export interface PriceTemplate {
+  id: number;
+  style: string;
+  process_id: number;
+  price: number;
+  remark: string;
+  created_at: string;
+  updated_at: string;
+}

部分文件因为文件数量过多而无法显示