Browse Source

全面检查修改一次

Gogs 1 month ago
parent
commit
5648051357
38 changed files with 606 additions and 410 deletions
  1. 14 28
      apps/factory-app/src/router/index.ts
  2. 8 7
      apps/factory-app/src/stores/auth.ts
  3. 3 4
      apps/factory-app/src/views/LoginView.vue
  4. 3 3
      apps/factory-app/src/views/PricesView.vue
  5. 21 5
      apps/factory-app/src/views/RecordsView.vue
  6. 90 34
      apps/factory-app/src/views/SalaryView.vue
  7. 2 2
      apps/factory-app/src/views/StatsView.vue
  8. 2 2
      apps/factory-app/src/views/WorkOrdersView.vue
  9. 3 0
      apps/platform-app/src/router/index.ts
  10. 2 2
      apps/platform-app/src/stores/auth.ts
  11. 3 3
      apps/platform-app/src/views/DashboardView.vue
  12. 2 2
      apps/platform-app/src/views/FactoriesView.vue
  13. 2 2
      apps/platform-app/src/views/UsersView.vue
  14. 18 16
      apps/worker-app/package.json
  15. 13 27
      apps/worker-app/src/router/index.ts
  16. 17 10
      apps/worker-app/src/stores/auth.ts
  17. 2 2
      apps/worker-app/src/views/HomeView.vue
  18. 3 4
      apps/worker-app/src/views/LoginView.vue
  19. 78 1
      apps/worker-app/src/views/ProfileView.vue
  20. 2 2
      apps/worker-app/src/views/RecordsView.vue
  21. 6 3
      apps/worker-app/src/views/SalaryView.vue
  22. 44 16
      apps/worker-app/src/views/ScanView.vue
  23. 3 2
      apps/worker-app/src/views/SubmitView.vue
  24. 0 72
      deploy/deploy.ps1
  25. 0 126
      deploy/nginx.conf
  26. 3 23
      packages/api-client/src/client.ts
  27. 1 1
      packages/api-client/src/endpoints/auditLog.ts
  28. 6 3
      packages/api-client/src/endpoints/auth.ts
  29. 9 1
      packages/api-client/src/endpoints/factory.ts
  30. 28 0
      packages/api-client/src/endpoints/record.ts
  31. 3 3
      packages/api-client/src/endpoints/salary.ts
  32. 42 0
      packages/api-client/src/endpoints/systemAuth.ts
  33. 68 0
      packages/api-client/src/endpoints/systemFactory.ts
  34. 54 0
      packages/api-client/src/endpoints/systemUser.ts
  35. 3 0
      packages/api-client/src/index.ts
  36. 6 0
      packages/types/src/models.ts
  37. 26 0
      packages/types/src/record.ts
  38. 16 4
      packages/types/src/salary.ts

+ 14 - 28
apps/factory-app/src/router/index.ts

@@ -1,7 +1,8 @@
-// 工厂管理后台路由配置(完全重写:标准SPA路由)
+// 工厂管理后台路由配置
 
 
 import { createRouter, createWebHistory } from 'vue-router';
 import { createRouter, createWebHistory } from 'vue-router';
 import { useAuthStore } from '@/stores/auth';
 import { useAuthStore } from '@/stores/auth';
+import { isFactoryAdmin, needsPasswordChange } from '@smartcut/shared-utils';
 
 
 const routes = [
 const routes = [
   {
   {
@@ -89,17 +90,7 @@ const router = createRouter({
   routes
   routes
 });
 });
 
 
-// 构建redirect路径(剔除factory参数,避免与外层factory重复;登录后守卫会自动重新注入)
-function buildRedirect(to: { path: string; query: Record<string, any> }): string {
-  const restQuery = { ...to.query };
-  delete restQuery.factory;
-  const keys = Object.keys(restQuery);
-  if (keys.length === 0) return to.path;
-  const qs = keys.map(k => `${k}=${encodeURIComponent(String(restQuery[k]))}`).join('&');
-  return `${to.path}?${qs}`;
-}
-
-// 路由守卫:认证检查 + factory_id验证 + 自动注入factory参数
+// 路由守卫:认证检查(factory_id 由后端从 Token 提取,无需在 URL 中维护)
 router.beforeEach((to, _from, next) => {
 router.beforeEach((to, _from, next) => {
   const authStore = useAuthStore();
   const authStore = useAuthStore();
   const factoryId = authStore.currentFactoryId;
   const factoryId = authStore.currentFactoryId;
@@ -107,35 +98,30 @@ router.beforeEach((to, _from, next) => {
   // 需要认证的路由
   // 需要认证的路由
   if (to.meta.requiresAuth) {
   if (to.meta.requiresAuth) {
     if (!authStore.isLoggedIn) {
     if (!authStore.isLoggedIn) {
-      // 未登录时跳转登录页,携带redirect和factory参数
-      // factory优先取store(已登录过的工厂),回退取URL query(支持深链直达 /?factory=xxx)
+      // 未登录:跳转登录页,携带 factory 参数
       const factoryForLogin = factoryId || (to.query.factory as string) || '';
       const factoryForLogin = factoryId || (to.query.factory as string) || '';
       next({
       next({
         name: 'Login',
         name: 'Login',
-        query: {
-          redirect: buildRedirect(to),
-          ...(factoryForLogin ? { factory: factoryForLogin } : {})
-        }
+        query: factoryForLogin ? { factory: factoryForLogin } : undefined
       });
       });
     } else if (!factoryId) {
     } else if (!factoryId) {
-      // 有Token但无factory_id,跳转登录重新选择工厂(同样回退URL query)
+      // 有 Token 但无 factory_id,跳转登录重新选择工厂
       const factoryForLogin = (to.query.factory as string) || '';
       const factoryForLogin = (to.query.factory as string) || '';
       next({
       next({
         name: 'Login',
         name: 'Login',
-        query: {
-          redirect: buildRedirect(to),
-          ...(factoryForLogin ? { factory: factoryForLogin } : {})
-        }
+        query: factoryForLogin ? { factory: factoryForLogin } : undefined
       });
       });
-    } else if (to.query.factory !== factoryId) {
-      // 已登录:确保URL携带正确的factory参数(自动注入,用户无感,支持深链/新标签页)
-      next({ ...to, query: { ...to.query, factory: factoryId } });
+    } else if (authStore.user && !isFactoryAdmin(authStore.user)) {
+      authStore.logout();
+      next({ name: 'Login' });
+    } else if (authStore.user && needsPasswordChange(authStore.user) && to.name !== 'ChangePassword') {
+      next({ name: 'ChangePassword' });
     } else {
     } else {
       next();
       next();
     }
     }
   } else if (to.name === 'Login' && authStore.isLoggedIn) {
   } else if (to.name === 'Login' && authStore.isLoggedIn) {
-    // 已登录时访问登录页,跳转首页,携带factory
-    next({ name: 'Dashboard', query: { factory: factoryId || '' } });
+    // 已登录时访问登录页,跳转首页
+    next({ name: 'Dashboard' });
   } else {
   } else {
     next();
     next();
   }
   }

+ 8 - 7
apps/factory-app/src/stores/auth.ts

@@ -21,7 +21,7 @@ export const useAuthStore = defineStore('auth', () => {
   );
   );
   const isLoggedIn = computed(() => !!token.value && !!user.value && !!currentFactoryId.value);
   const isLoggedIn = computed(() => !!token.value && !!user.value && !!currentFactoryId.value);
 
 
-  // 创建API客户端(factory_id通过Query参数传递)
+  // 创建API客户端(factory_id 由后端从 Token 自动提取)
   const apiClient = createApiClient({
   const apiClient = createApiClient({
     baseURL: getApiBaseUrl(),
     baseURL: getApiBaseUrl(),
     tokenKey: STORAGE_KEYS.FACTORY_TOKEN
     tokenKey: STORAGE_KEYS.FACTORY_TOKEN
@@ -30,15 +30,16 @@ export const useAuthStore = defineStore('auth', () => {
   const authApi = new AuthApi(apiClient);
   const authApi = new AuthApi(apiClient);
 
 
   /**
   /**
-   * 工厂管理员登录(完全重写:Query参数传递factory_id)
+   * 工厂管理员登录
+   * factoryId 通过 Query 参数传递给后端
    */
    */
   async function login(username: string, password: string, factoryId: string) {
   async function login(username: string, password: string, factoryId: string) {
     try {
     try {
-      const response = await authApi.login({
-        username,
-        password,
-        factory_id: factoryId // 新方案:登录接口接收factory_id参数
-      });
+      // 登录时传递 factoryId 作为 Query 参数
+      const response = await authApi.login(
+        { username, password },
+        factoryId
+      );
 
 
       // 存储Token、用户信息、factory_id到SessionStorage
       // 存储Token、用户信息、factory_id到SessionStorage
       token.value = response.token;
       token.value = response.token;

+ 3 - 4
apps/factory-app/src/views/LoginView.vue

@@ -78,7 +78,7 @@ import { useAuthStore } from '@/stores/auth';
 import { FactoryApi, createApiClient } from '@smartcut/api-client';
 import { FactoryApi, createApiClient } from '@smartcut/api-client';
 import { STORAGE_KEYS, getSessionStorage } from '@smartcut/shared-utils';
 import { STORAGE_KEYS, getSessionStorage } from '@smartcut/shared-utils';
 import { getApiBaseUrl } from '@/apiConfig';
 import { getApiBaseUrl } from '@/apiConfig';
-import type { Factory } from '@smartcut/types';
+import type { PublicFactory } from '@smartcut/types';
 
 
 const router = useRouter();
 const router = useRouter();
 const route = useRoute();
 const route = useRoute();
@@ -86,7 +86,7 @@ const authStore = useAuthStore();
 
 
 const loading = ref(false);
 const loading = ref(false);
 const loadingFactories = ref(false);
 const loadingFactories = ref(false);
-const factories = ref<Factory[]>([]);
+const factories = ref<PublicFactory[]>([]);
 const selectedFactory = ref('');
 const selectedFactory = ref('');
 
 
 // 从Query参数获取factory_id,回退到SessionStorage
 // 从Query参数获取factory_id,回退到SessionStorage
@@ -116,8 +116,7 @@ onMounted(async () => {
         tokenKey: STORAGE_KEYS.PLATFORM_TOKEN
         tokenKey: STORAGE_KEYS.PLATFORM_TOKEN
       });
       });
       const factoryApi = new FactoryApi(apiClient);
       const factoryApi = new FactoryApi(apiClient);
-      const result = await factoryApi.listFactories();
-      factories.value = result.list.filter(f => f.status === 1);
+      factories.value = await factoryApi.listPublicFactories();
     } catch (error) {
     } catch (error) {
       message.error('加载工厂列表失败');
       message.error('加载工厂列表失败');
     } finally {
     } finally {

+ 3 - 3
apps/factory-app/src/views/PricesView.vue

@@ -34,7 +34,7 @@
     <a-card v-if="!layoutStore.isMobile" class="rounded-xl shadow-md !border-0" title="工价列表">
     <a-card v-if="!layoutStore.isMobile" class="rounded-xl shadow-md !border-0" title="工价列表">
       <a-table :columns="columns" :dataSource="prices" :loading="loading" rowKey="id">
       <a-table :columns="columns" :dataSource="prices" :loading="loading" rowKey="id">
         <template #price="{ record }">
         <template #price="{ record }">
-          ¥{{ record.price.toFixed(2) }}
+          ¥{{ formatMoney(record.price) }}
         </template>
         </template>
         <template #action="{ record }">
         <template #action="{ record }">
           <a-space>
           <a-space>
@@ -65,7 +65,7 @@
         >
         >
           <div class="flex items-start justify-between gap-2 mb-2">
           <div class="flex items-start justify-between gap-2 mb-2">
             <span class="font-medium text-gray-900 break-all">{{ item.style }}</span>
             <span class="font-medium text-gray-900 break-all">{{ item.style }}</span>
-            <span class="text-blue-600 font-bold flex-shrink-0">¥{{ item.price.toFixed(2) }}</span>
+            <span class="text-blue-600 font-bold flex-shrink-0">¥{{ formatMoney(item.price) }}</span>
           </div>
           </div>
           <div class="text-sm text-gray-500 space-y-1 mb-3">
           <div class="text-sm text-gray-500 space-y-1 mb-3">
             <div>工序:{{ item.process_name }}</div>
             <div>工序:{{ item.process_name }}</div>
@@ -116,7 +116,7 @@ import { ref, reactive, onMounted, computed } from 'vue';
 import { message } from 'ant-design-vue';
 import { message } from 'ant-design-vue';
 import { PlusOutlined, EditOutlined, DeleteOutlined, ThunderboltOutlined } from '@ant-design/icons-vue';
 import { PlusOutlined, EditOutlined, DeleteOutlined, ThunderboltOutlined } from '@ant-design/icons-vue';
 import { WorkOrderApi, ProcessApi, createApiClient } from '@smartcut/api-client';
 import { WorkOrderApi, ProcessApi, createApiClient } from '@smartcut/api-client';
-import { STORAGE_KEYS } from '@smartcut/shared-utils';
+import { STORAGE_KEYS, formatMoney } from '@smartcut/shared-utils';
 import { getApiBaseUrl } from '@/apiConfig';
 import { getApiBaseUrl } from '@/apiConfig';
 import { useLayoutStore } from '@/stores/layout';
 import { useLayoutStore } from '@/stores/layout';
 import type { WorkOrder, Process, PriceTemplate } from '@smartcut/types';
 import type { WorkOrder, Process, PriceTemplate } from '@smartcut/types';

+ 21 - 5
apps/factory-app/src/views/RecordsView.vue

@@ -14,6 +14,16 @@
           placeholder="筛选日期"
           placeholder="筛选日期"
           @change="loadRecords"
           @change="loadRecords"
         />
         />
+        <a-select
+          v-model:value="filterStatus"
+          placeholder="状态筛选"
+          class="w-full sm:w-40"
+          allowClear
+          @change="loadRecords"
+        >
+          <a-select-option value="normal">正常</a-select-option>
+          <a-select-option value="reverted">已退回</a-select-option>
+        </a-select>
         <a-button type="primary" @click="showScanModal" class="shadow-sm hover:shadow-md transition-shadow">
         <a-button type="primary" @click="showScanModal" class="shadow-sm hover:shadow-md transition-shadow">
           <ScanOutlined class="mr-2" />
           <ScanOutlined class="mr-2" />
           扫码计件
           扫码计件
@@ -37,8 +47,12 @@
           </a-tag>
           </a-tag>
         </template>
         </template>
 
 
+        <template #price="{ record }">
+          ¥{{ formatMoney(record.price) }}
+        </template>
+
         <template #amount="{ record }">
         <template #amount="{ record }">
-          ¥{{ record.amount.toFixed(2) }}
+          ¥{{ formatMoney(record.amount) }}
         </template>
         </template>
 
 
         <template #action="{ record }">
         <template #action="{ record }">
@@ -76,7 +90,7 @@
             <div>工单号:{{ item.order_no }}</div>
             <div>工单号:{{ item.order_no }}</div>
             <div>工序:{{ item.process_name }}</div>
             <div>工序:{{ item.process_name }}</div>
             <div>数量:{{ item.quantity }}</div>
             <div>数量:{{ item.quantity }}</div>
-            <div>金额:¥{{ item.amount.toFixed(2) }}</div>
+            <div>金额:¥{{ formatMoney(item.amount) }}</div>
             <div>日期:{{ item.record_date }}</div>
             <div>日期:{{ item.record_date }}</div>
           </div>
           </div>
           <div class="flex items-center gap-2 pt-3 border-t border-gray-100">
           <div class="flex items-center gap-2 pt-3 border-t border-gray-100">
@@ -155,7 +169,7 @@ import { ref, reactive, onMounted, computed } from 'vue';
 import { message } from 'ant-design-vue';
 import { message } from 'ant-design-vue';
 import { ScanOutlined } from '@ant-design/icons-vue';
 import { ScanOutlined } from '@ant-design/icons-vue';
 import { RecordApi, ProcessApi, createApiClient } from '@smartcut/api-client';
 import { RecordApi, ProcessApi, createApiClient } from '@smartcut/api-client';
-import { STORAGE_KEYS, formatDate } from '@smartcut/shared-utils';
+import { STORAGE_KEYS, formatDate, formatMoney } from '@smartcut/shared-utils';
 import { getApiBaseUrl } from '@/apiConfig';
 import { getApiBaseUrl } from '@/apiConfig';
 import { useLayoutStore } from '@/stores/layout';
 import { useLayoutStore } from '@/stores/layout';
 import type { PieceRecord, Process } from '@smartcut/types';
 import type { PieceRecord, Process } from '@smartcut/types';
@@ -177,6 +191,7 @@ const processes = ref<Process[]>([]);
 const loading = ref(false);
 const loading = ref(false);
 const searchKeyword = ref('');
 const searchKeyword = ref('');
 const filterDate = ref<dayjs.Dayjs | null>(null);
 const filterDate = ref<dayjs.Dayjs | null>(null);
+const filterStatus = ref<string | undefined>(undefined);
 
 
 const pagination = reactive({
 const pagination = reactive({
   current: 1,
   current: 1,
@@ -192,7 +207,7 @@ const columns = [
   { title: '工单号', dataIndex: 'order_no', key: 'order_no', width: 150 },
   { title: '工单号', dataIndex: 'order_no', key: 'order_no', width: 150 },
   { title: '工序', dataIndex: 'process_name', key: 'process_name', width: 150 },
   { title: '工序', dataIndex: 'process_name', key: 'process_name', width: 150 },
   { title: '数量', dataIndex: 'quantity', key: 'quantity', width: 100 },
   { title: '数量', dataIndex: 'quantity', key: 'quantity', width: 100 },
-  { title: '单价', dataIndex: 'price', key: 'price', width: 100 },
+  { title: '单价', key: 'price', width: 100, slots: { customRender: 'price' } },
   { title: '金额', key: 'amount', width: 100, slots: { customRender: 'amount' } },
   { title: '金额', key: 'amount', width: 100, slots: { customRender: 'amount' } },
   { title: '日期', dataIndex: 'record_date', key: 'record_date', width: 120 },
   { title: '日期', dataIndex: 'record_date', key: 'record_date', width: 120 },
   { title: '状态', dataIndex: 'status', key: 'status', width: 100, slots: { customRender: 'status' } },
   { title: '状态', dataIndex: 'status', key: 'status', width: 100, slots: { customRender: 'status' } },
@@ -222,7 +237,8 @@ async function loadRecords() {
       page: pagination.current,
       page: pagination.current,
       page_size: pagination.pageSize,
       page_size: pagination.pageSize,
       date: filterDate.value ? formatDate(filterDate.value.toDate()) : undefined,
       date: filterDate.value ? formatDate(filterDate.value.toDate()) : undefined,
-      keyword: searchKeyword.value
+      keyword: searchKeyword.value,
+      status: filterStatus.value
     });
     });
     records.value = result.list;
     records.value = result.list;
     pagination.total = result.total;
     pagination.total = result.total;

+ 90 - 34
apps/factory-app/src/views/SalaryView.vue

@@ -34,25 +34,37 @@
               <EyeOutlined class="mr-1" />
               <EyeOutlined class="mr-1" />
               查看明细
               查看明细
             </a-button>
             </a-button>
-            <a-button
+            <a-popconfirm
               v-if="record.status === 'pending'"
               v-if="record.status === 'pending'"
-              size="small"
-              type="primary"
-              @click="lockSalary(record.period)"
-              class="hover:shadow-sm transition-shadow"
+              title="锁定后该月将无法继续计件和退回记录,确定锁定?"
+              ok-text="确定锁定"
+              cancel-text="取消"
+              @confirm="lockSalary(record.period)"
             >
             >
-              <LockOutlined class="mr-1" />
-              锁定
-            </a-button>
-            <a-button
+              <a-button
+                size="small"
+                type="primary"
+                class="hover:shadow-sm transition-shadow"
+              >
+                <LockOutlined class="mr-1" />
+                锁定
+              </a-button>
+            </a-popconfirm>
+            <a-popconfirm
               v-else
               v-else
-              size="small"
-              @click="unlockSalary(record.period)"
-              class="hover:shadow-sm transition-shadow"
+              title="解锁后该月可继续计件和修改记录,确定解锁?"
+              ok-text="确定解锁"
+              cancel-text="取消"
+              @confirm="unlockSalary(record.period)"
             >
             >
-              <UnlockOutlined class="mr-1" />
-              解锁
-            </a-button>
+              <a-button
+                size="small"
+                class="hover:shadow-sm transition-shadow"
+              >
+                <UnlockOutlined class="mr-1" />
+                解锁
+              </a-button>
+            </a-popconfirm>
             <a-popconfirm
             <a-popconfirm
               title="确定删除此工资周期吗?"
               title="确定删除此工资周期吗?"
               @confirm="deleteSalary(record.period)"
               @confirm="deleteSalary(record.period)"
@@ -91,23 +103,30 @@
               <EyeOutlined class="mr-1" />
               <EyeOutlined class="mr-1" />
               明细
               明细
             </a-button>
             </a-button>
-            <a-button
+            <a-popconfirm
               v-if="item.status === 'pending'"
               v-if="item.status === 'pending'"
-              size="small"
-              type="primary"
-              @click="lockSalary(item.period)"
+              title="锁定后该月将无法继续计件和退回记录,确定锁定?"
+              ok-text="确定锁定"
+              cancel-text="取消"
+              @confirm="lockSalary(item.period)"
             >
             >
-              <LockOutlined class="mr-1" />
-              锁定
-            </a-button>
-            <a-button
+              <a-button size="small" type="primary">
+                <LockOutlined class="mr-1" />
+                锁定
+              </a-button>
+            </a-popconfirm>
+            <a-popconfirm
               v-else
               v-else
-              size="small"
-              @click="unlockSalary(item.period)"
+              title="解锁后该月可继续计件和修改记录,确定解锁?"
+              ok-text="确定解锁"
+              cancel-text="取消"
+              @confirm="unlockSalary(item.period)"
             >
             >
-              <UnlockOutlined class="mr-1" />
-              解锁
-            </a-button>
+              <a-button size="small">
+                <UnlockOutlined class="mr-1" />
+                解锁
+              </a-button>
+            </a-popconfirm>
             <a-popconfirm title="确定删除此工资周期吗?" @confirm="deleteSalary(item.period)">
             <a-popconfirm title="确定删除此工资周期吗?" @confirm="deleteSalary(item.period)">
               <a-button size="small" danger>
               <a-button size="small" danger>
                 <DeleteOutlined class="mr-1" />
                 <DeleteOutlined class="mr-1" />
@@ -134,6 +153,7 @@
             v-model:value="generatePeriod"
             v-model:value="generatePeriod"
             placeholder="请选择工资周期"
             placeholder="请选择工资周期"
             style="width: 100%"
             style="width: 100%"
+            :disabled-date="disabledFutureMonth"
           />
           />
         </a-form-item>
         </a-form-item>
       </a-form>
       </a-form>
@@ -147,15 +167,22 @@
       :footer="null"
       :footer="null"
       class="!rounded-xl"
       class="!rounded-xl"
     >
     >
+      <div class="mb-3 text-lg font-bold text-blue-600">
+        工资总额: ¥{{ formatMoney(salaryGrandTotal) }}
+        <a-tag :color="salaryStatus === 'locked' ? 'red' : 'green'" class="ml-3">
+          {{ salaryStatus === 'locked' ? '已锁定' : '待锁定' }}
+        </a-tag>
+      </div>
       <a-table
       <a-table
         :columns="detailColumns"
         :columns="detailColumns"
         :dataSource="salaryDetails"
         :dataSource="salaryDetails"
         :loading="detailLoading"
         :loading="detailLoading"
         rowKey="user_id"
         rowKey="user_id"
-        :pagination="false"
+        :pagination="detailPagination"
+        @change="handleDetailPageChange"
       >
       >
         <template #amount="{ record }">
         <template #amount="{ record }">
-          ¥{{ record.total_amount.toFixed(2) }}
+          ¥{{ formatMoney(record.total_amount) }}
         </template>
         </template>
       </a-table>
       </a-table>
     </a-modal>
     </a-modal>
@@ -163,12 +190,12 @@
 </template>
 </template>
 
 
 <script setup lang="ts">
 <script setup lang="ts">
-import { ref, onMounted, computed } from 'vue';
+import { ref, reactive, onMounted, computed } from 'vue';
 import { message } from 'ant-design-vue';
 import { message } from 'ant-design-vue';
 import { PlusOutlined, ReloadOutlined, EyeOutlined, LockOutlined, UnlockOutlined, DeleteOutlined } from '@ant-design/icons-vue';
 import { PlusOutlined, ReloadOutlined, EyeOutlined, LockOutlined, UnlockOutlined, DeleteOutlined } from '@ant-design/icons-vue';
 import dayjs from 'dayjs';
 import dayjs from 'dayjs';
 import { SalaryApi, createApiClient } from '@smartcut/api-client';
 import { SalaryApi, createApiClient } from '@smartcut/api-client';
-import { STORAGE_KEYS } from '@smartcut/shared-utils';
+import { STORAGE_KEYS, formatMoney } from '@smartcut/shared-utils';
 import { getApiBaseUrl } from '@/apiConfig';
 import { getApiBaseUrl } from '@/apiConfig';
 import { useLayoutStore } from '@/stores/layout';
 import { useLayoutStore } from '@/stores/layout';
 import type { SalaryPeriod, SalaryDetailItem } from '@smartcut/types';
 import type { SalaryPeriod, SalaryDetailItem } from '@smartcut/types';
@@ -197,7 +224,7 @@ const columns = [
 const detailColumns = [
 const detailColumns = [
   { title: '员工', dataIndex: 'user_name', key: 'user_name', width: 150 },
   { title: '员工', dataIndex: 'user_name', key: 'user_name', width: 150 },
   { title: '总数量', dataIndex: 'total_quantity', key: 'total_quantity', width: 120 },
   { title: '总数量', dataIndex: 'total_quantity', key: 'total_quantity', width: 120 },
-  { title: '记录数', dataIndex: 'records_count', key: 'records_count', width: 100 },
+  { title: '记录数', dataIndex: 'record_count', key: 'record_count', width: 100 },
   { title: '工资金额', key: 'amount', width: 150, slots: { customRender: 'amount' } }
   { title: '工资金额', key: 'amount', width: 150, slots: { customRender: 'amount' } }
 ];
 ];
 
 
@@ -209,6 +236,13 @@ const detailModalVisible = ref(false);
 const currentPeriod = ref('');
 const currentPeriod = ref('');
 const salaryDetails = ref<SalaryDetailItem[]>([]);
 const salaryDetails = ref<SalaryDetailItem[]>([]);
 const detailLoading = ref(false);
 const detailLoading = ref(false);
+const salaryGrandTotal = ref(0);
+const salaryStatus = ref('');
+const detailPagination = reactive({
+  current: 1,
+  pageSize: 20,
+  total: 0
+});
 
 
 async function loadSalaryList() {
 async function loadSalaryList() {
   loading.value = true;
   loading.value = true;
@@ -226,6 +260,10 @@ function showGenerateModal() {
   generatePeriod.value = dayjs();
   generatePeriod.value = dayjs();
 }
 }
 
 
+function disabledFutureMonth(current: dayjs.Dayjs): boolean {
+  return current && current > dayjs().endOf('month');
+}
+
 async function handleGenerate() {
 async function handleGenerate() {
   if (!generatePeriod.value) {
   if (!generatePeriod.value) {
     message.error('请选择工资周期');
     message.error('请选择工资周期');
@@ -249,9 +287,21 @@ async function handleGenerate() {
 async function showDetailModal(period: string) {
 async function showDetailModal(period: string) {
   currentPeriod.value = period;
   currentPeriod.value = period;
   detailModalVisible.value = true;
   detailModalVisible.value = true;
+  detailPagination.current = 1;
+  await loadSalaryDetail();
+}
+
+async function loadSalaryDetail() {
   detailLoading.value = true;
   detailLoading.value = true;
   try {
   try {
-    salaryDetails.value = await salaryApi.getSalaryDetail(period);
+    const res = await salaryApi.getSalaryDetail(currentPeriod.value, {
+      page: detailPagination.current,
+      page_size: detailPagination.pageSize
+    });
+    salaryDetails.value = res.details;
+    salaryGrandTotal.value = res.grand_total;
+    salaryStatus.value = res.status;
+    detailPagination.total = res.total;
   } catch (error) {
   } catch (error) {
     message.error('加载工资明细失败');
     message.error('加载工资明细失败');
   } finally {
   } finally {
@@ -259,6 +309,12 @@ async function showDetailModal(period: string) {
   }
   }
 }
 }
 
 
+function handleDetailPageChange(pag: any) {
+  detailPagination.current = pag.current;
+  detailPagination.pageSize = pag.pageSize;
+  loadSalaryDetail();
+}
+
 async function lockSalary(period: string) {
 async function lockSalary(period: string) {
   try {
   try {
     await salaryApi.lockSalary(period);
     await salaryApi.lockSalary(period);

+ 2 - 2
apps/factory-app/src/views/StatsView.vue

@@ -43,7 +43,7 @@
               </a-tag>
               </a-tag>
             </template>
             </template>
             <template #amount="{ record }">
             <template #amount="{ record }">
-              ¥{{ record.total_amount.toFixed(2) }}
+              ¥{{ formatMoney(record.total_amount) }}
             </template>
             </template>
           </a-table>
           </a-table>
         </a-tab-pane>
         </a-tab-pane>
@@ -89,7 +89,7 @@
 import { ref, onMounted } from 'vue';
 import { ref, onMounted } from 'vue';
 import { message } from 'ant-design-vue';
 import { message } from 'ant-design-vue';
 import { StatsApi, createApiClient } from '@smartcut/api-client';
 import { StatsApi, createApiClient } from '@smartcut/api-client';
-import { STORAGE_KEYS } from '@smartcut/shared-utils';
+import { STORAGE_KEYS, formatMoney } from '@smartcut/shared-utils';
 import { getApiBaseUrl } from '@/apiConfig';
 import { getApiBaseUrl } from '@/apiConfig';
 import type { ProductionData, RankingData, ProgressMatrixData } from '@smartcut/types';
 import type { ProductionData, RankingData, ProgressMatrixData } from '@smartcut/types';
 
 

+ 2 - 2
apps/factory-app/src/views/WorkOrdersView.vue

@@ -46,7 +46,7 @@
 
 
         <template #progress="{ record }">
         <template #progress="{ record }">
           <a-progress
           <a-progress
-            :percent="Math.round((record.completed_qty / record.total_qty) * 100)"
+            :percent="record.total_qty > 0 ? Math.round((record.completed_qty / record.total_qty) * 100) : 0"
             :status="record.status === 'completed' ? 'success' : 'active'"
             :status="record.status === 'completed' ? 'success' : 'active'"
           />
           />
         </template>
         </template>
@@ -99,7 +99,7 @@
             <div>数量:{{ item.completed_qty }} / {{ item.total_qty }}</div>
             <div>数量:{{ item.completed_qty }} / {{ item.total_qty }}</div>
           </div>
           </div>
           <a-progress
           <a-progress
-            :percent="Math.round((item.completed_qty / item.total_qty) * 100)"
+            :percent="item.total_qty > 0 ? Math.round((item.completed_qty / item.total_qty) * 100) : 0"
             :status="item.status === 'completed' ? 'success' : 'active'"
             :status="item.status === 'completed' ? 'success' : 'active'"
             size="small"
             size="small"
             class="mb-3"
             class="mb-3"

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

@@ -2,6 +2,7 @@
 
 
 import { createRouter, createWebHistory } from 'vue-router';
 import { createRouter, createWebHistory } from 'vue-router';
 import { useAuthStore } from '@/stores/auth';
 import { useAuthStore } from '@/stores/auth';
+import { needsPasswordChange } from '@smartcut/shared-utils';
 
 
 const routes = [
 const routes = [
   {
   {
@@ -67,6 +68,8 @@ router.beforeEach((to, _from, next) => {
   } else if (to.name === 'Login' && authStore.isLoggedIn) {
   } else if (to.name === 'Login' && authStore.isLoggedIn) {
     // 已登录时访问登录页,跳转首页
     // 已登录时访问登录页,跳转首页
     next({ name: 'Dashboard' });
     next({ name: 'Dashboard' });
+  } else if (authStore.isLoggedIn && authStore.user && needsPasswordChange(authStore.user) && to.name !== 'ChangePassword') {
+    next({ name: 'ChangePassword' });
   } else {
   } else {
     next();
     next();
   }
   }

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

@@ -3,7 +3,7 @@
 import { defineStore } from 'pinia';
 import { defineStore } from 'pinia';
 import { ref, computed } from 'vue';
 import { ref, computed } from 'vue';
 import type { SystemUser } from '@smartcut/types';
 import type { SystemUser } from '@smartcut/types';
-import { AuthApi, createApiClient } from '@smartcut/api-client';
+import { SystemAuthApi, createApiClient } from '@smartcut/api-client';
 import { STORAGE_KEYS, setSessionStorage, getSessionStorage, removeSessionStorage } from '@smartcut/shared-utils';
 import { STORAGE_KEYS, setSessionStorage, getSessionStorage, removeSessionStorage } from '@smartcut/shared-utils';
 import { getApiBaseUrl } from '@/apiConfig';
 import { getApiBaseUrl } from '@/apiConfig';
 
 
@@ -19,7 +19,7 @@ export const useAuthStore = defineStore('auth', () => {
     tokenKey: STORAGE_KEYS.PLATFORM_TOKEN
     tokenKey: STORAGE_KEYS.PLATFORM_TOKEN
   });
   });
 
 
-  const authApi = new AuthApi(apiClient);
+  const authApi = new SystemAuthApi(apiClient);
 
 
   /**
   /**
    * 系统管理员登录
    * 系统管理员登录

+ 3 - 3
apps/platform-app/src/views/DashboardView.vue

@@ -92,7 +92,7 @@ import { ref, onMounted } from 'vue';
 import { useRouter } from 'vue-router';
 import { useRouter } from 'vue-router';
 import { message } from 'ant-design-vue';
 import { message } from 'ant-design-vue';
 import { ShopOutlined, CheckCircleOutlined, UserOutlined, FileTextOutlined } from '@ant-design/icons-vue';
 import { ShopOutlined, CheckCircleOutlined, UserOutlined, FileTextOutlined } from '@ant-design/icons-vue';
-import { FactoryApi, UserApi, AuditLogApi, createApiClient } from '@smartcut/api-client';
+import { SystemFactoryApi, SystemUserApi, AuditLogApi, createApiClient } from '@smartcut/api-client';
 import { STORAGE_KEYS } from '@smartcut/shared-utils';
 import { STORAGE_KEYS } from '@smartcut/shared-utils';
 import { getApiBaseUrl } from '@/apiConfig';
 import { getApiBaseUrl } from '@/apiConfig';
 
 
@@ -110,8 +110,8 @@ const apiClient = createApiClient({
   tokenKey: STORAGE_KEYS.PLATFORM_TOKEN
   tokenKey: STORAGE_KEYS.PLATFORM_TOKEN
 });
 });
 
 
-const factoryApi = new FactoryApi(apiClient);
-const userApi = new UserApi(apiClient);
+const factoryApi = new SystemFactoryApi(apiClient);
+const userApi = new SystemUserApi(apiClient);
 const auditLogApi = new AuditLogApi(apiClient);
 const auditLogApi = new AuditLogApi(apiClient);
 
 
 onMounted(async () => {
 onMounted(async () => {

+ 2 - 2
apps/platform-app/src/views/FactoriesView.vue

@@ -171,7 +171,7 @@
 import { ref, reactive, onMounted, computed } from 'vue';
 import { ref, reactive, onMounted, computed } from 'vue';
 import { message } from 'ant-design-vue';
 import { message } from 'ant-design-vue';
 import { PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons-vue';
 import { PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons-vue';
-import { FactoryApi, createApiClient } from '@smartcut/api-client';
+import { SystemFactoryApi, createApiClient } from '@smartcut/api-client';
 import { STORAGE_KEYS } from '@smartcut/shared-utils';
 import { STORAGE_KEYS } from '@smartcut/shared-utils';
 import { getApiBaseUrl } from '@/apiConfig';
 import { getApiBaseUrl } from '@/apiConfig';
 import { useLayoutStore } from '@/stores/layout';
 import { useLayoutStore } from '@/stores/layout';
@@ -183,7 +183,7 @@ const apiClient = createApiClient({
   tokenKey: STORAGE_KEYS.PLATFORM_TOKEN
   tokenKey: STORAGE_KEYS.PLATFORM_TOKEN
 });
 });
 
 
-const factoryApi = new FactoryApi(apiClient);
+const factoryApi = new SystemFactoryApi(apiClient);
 
 
 const layoutStore = useLayoutStore();
 const layoutStore = useLayoutStore();
 const modalWidth = computed(() => layoutStore.isMobile ? '90%' : undefined);
 const modalWidth = computed(() => layoutStore.isMobile ? '90%' : undefined);

+ 2 - 2
apps/platform-app/src/views/UsersView.vue

@@ -224,7 +224,7 @@
 import { ref, reactive, onMounted, computed } from 'vue';
 import { ref, reactive, onMounted, computed } from 'vue';
 import { message } from 'ant-design-vue';
 import { message } from 'ant-design-vue';
 import { PlusOutlined, EditOutlined, KeyOutlined, DeleteOutlined } from '@ant-design/icons-vue';
 import { PlusOutlined, EditOutlined, KeyOutlined, DeleteOutlined } from '@ant-design/icons-vue';
-import { UserApi, createApiClient } from '@smartcut/api-client';
+import { SystemUserApi, createApiClient } from '@smartcut/api-client';
 import { STORAGE_KEYS, validatePasswordStrength } from '@smartcut/shared-utils';
 import { STORAGE_KEYS, validatePasswordStrength } from '@smartcut/shared-utils';
 import { getApiBaseUrl } from '@/apiConfig';
 import { getApiBaseUrl } from '@/apiConfig';
 import { useLayoutStore } from '@/stores/layout';
 import { useLayoutStore } from '@/stores/layout';
@@ -236,7 +236,7 @@ const apiClient = createApiClient({
   tokenKey: STORAGE_KEYS.PLATFORM_TOKEN
   tokenKey: STORAGE_KEYS.PLATFORM_TOKEN
 });
 });
 
 
-const userApi = new UserApi(apiClient);
+const userApi = new SystemUserApi(apiClient);
 
 
 const layoutStore = useLayoutStore();
 const layoutStore = useLayoutStore();
 const modalWidth = computed(() => layoutStore.isMobile ? '90%' : 600);
 const modalWidth = computed(() => layoutStore.isMobile ? '90%' : 600);

+ 18 - 16
apps/worker-app/package.json

@@ -12,31 +12,33 @@
     "format": "prettier --write src/"
     "format": "prettier --write src/"
   },
   },
   "dependencies": {
   "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",
     "@ant-design/icons-vue": "^7.0.0",
-    "dayjs": "^1.11.0",
-    "@smartcut/types": "workspace:*",
     "@smartcut/api-client": "workspace:*",
     "@smartcut/api-client": "workspace:*",
+    "@smartcut/shared-components": "workspace:*",
     "@smartcut/shared-utils": "workspace:*",
     "@smartcut/shared-utils": "workspace:*",
-    "@smartcut/shared-components": "workspace:*"
+    "@smartcut/types": "workspace:*",
+    "@zxing/browser": "^0.2.1",
+    "@zxing/library": "^0.23.0",
+    "ant-design-vue": "^4.0.0",
+    "axios": "^1.6.0",
+    "dayjs": "^1.11.0",
+    "pinia": "^2.1.0",
+    "vue": "^3.4.0",
+    "vue-router": "^4.2.0"
   },
   },
   "devDependencies": {
   "devDependencies": {
+    "@eslint/js": "^9.0.0",
     "@vitejs/plugin-vue": "^5.0.0",
     "@vitejs/plugin-vue": "^5.0.0",
-    "typescript": "^5.3.0",
-    "vite": "^5.0.0",
-    "vue-tsc": "^1.8.0",
+    "autoprefixer": "^10.4.0",
     "eslint": "^9.0.0",
     "eslint": "^9.0.0",
-    "@eslint/js": "^9.0.0",
-    "typescript-eslint": "^8.0.0",
     "eslint-plugin-vue": "^9.30.0",
     "eslint-plugin-vue": "^9.30.0",
-    "vue-eslint-parser": "^9.4.0",
+    "postcss": "^8.4.0",
     "prettier": "^3.2.0",
     "prettier": "^3.2.0",
     "tailwindcss": "^3.4.0",
     "tailwindcss": "^3.4.0",
-    "autoprefixer": "^10.4.0",
-    "postcss": "^8.4.0"
+    "typescript": "^5.3.0",
+    "typescript-eslint": "^8.0.0",
+    "vite": "^5.0.0",
+    "vue-eslint-parser": "^9.4.0",
+    "vue-tsc": "^1.8.0"
   }
   }
 }
 }

+ 13 - 27
apps/worker-app/src/router/index.ts

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

+ 17 - 10
apps/worker-app/src/stores/auth.ts

@@ -20,6 +20,7 @@ export const useAuthStore = defineStore('auth', () => {
   );
   );
   const isLoggedIn = computed(() => !!token.value && !!user.value && !!currentFactoryId.value);
   const isLoggedIn = computed(() => !!token.value && !!user.value && !!currentFactoryId.value);
 
 
+  // 创建API客户端(factory_id 由后端从 Token 自动提取)
   const apiClient = createApiClient({
   const apiClient = createApiClient({
     baseURL: getApiBaseUrl(),
     baseURL: getApiBaseUrl(),
     tokenKey: STORAGE_KEYS.WORKER_TOKEN
     tokenKey: STORAGE_KEYS.WORKER_TOKEN
@@ -27,13 +28,16 @@ export const useAuthStore = defineStore('auth', () => {
 
 
   const authApi = new AuthApi(apiClient);
   const authApi = new AuthApi(apiClient);
 
 
+  /**
+   * 工人登录
+   * factoryId 通过 Query 参数传递给后端
+   */
   async function login(username: string, password: string, factoryId: string) {
   async function login(username: string, password: string, factoryId: string) {
     try {
     try {
-      const response = await authApi.login({
-        username,
-        password,
-        factory_id: factoryId
-      });
+      const response = await authApi.login(
+        { username, password },
+        factoryId
+      );
 
 
       token.value = response.token;
       token.value = response.token;
       user.value = response.user as User;
       user.value = response.user as User;
@@ -50,13 +54,16 @@ export const useAuthStore = defineStore('auth', () => {
     }
     }
   }
   }
 
 
+  /**
+   * 通过手机号登录
+   * factoryId 通过 Query 参数传递给后端
+   */
   async function loginByPhone(phone: string, password: string, factoryId: string) {
   async function loginByPhone(phone: string, password: string, factoryId: string) {
     try {
     try {
-      const response = await authApi.login({
-        phone,
-        password,
-        factory_id: factoryId
-      });
+      const response = await authApi.login(
+        { phone, password },
+        factoryId
+      );
 
 
       token.value = response.token;
       token.value = response.token;
       user.value = response.user as User;
       user.value = response.user as User;

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

@@ -73,7 +73,7 @@
                   {{ item.process_name }} - {{ item.quantity }}件
                   {{ item.process_name }} - {{ item.quantity }}件
                 </template>
                 </template>
                 <template #description>
                 <template #description>
-                  ¥{{ item.amount.toFixed(2) }} | {{ item.record_date }}
+                  ¥{{ formatMoney(item.amount) }} | {{ item.record_date }}
                 </template>
                 </template>
               </a-list-item-meta>
               </a-list-item-meta>
             </a-list-item>
             </a-list-item>
@@ -90,7 +90,7 @@ import { useRouter } from 'vue-router';
 import { useAuthStore } from '@/stores/auth';
 import { useAuthStore } from '@/stores/auth';
 import WorkerLayout from '@/components/WorkerLayout.vue';
 import WorkerLayout from '@/components/WorkerLayout.vue';
 import { StatsApi, RecordApi, createApiClient } from '@smartcut/api-client';
 import { StatsApi, RecordApi, createApiClient } from '@smartcut/api-client';
-import { STORAGE_KEYS, formatDate } from '@smartcut/shared-utils';
+import { STORAGE_KEYS, formatDate, formatMoney } from '@smartcut/shared-utils';
 import { getApiBaseUrl } from '@/apiConfig';
 import { getApiBaseUrl } from '@/apiConfig';
 import type { WorkerDashboardData, PieceRecord } from '@smartcut/types';
 import type { WorkerDashboardData, PieceRecord } from '@smartcut/types';
 
 

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

@@ -64,7 +64,7 @@ import { useAuthStore } from '@/stores/auth';
 import { FactoryApi, createApiClient } from '@smartcut/api-client';
 import { FactoryApi, createApiClient } from '@smartcut/api-client';
 import { STORAGE_KEYS, getSessionStorage } from '@smartcut/shared-utils';
 import { STORAGE_KEYS, getSessionStorage } from '@smartcut/shared-utils';
 import { getApiBaseUrl } from '@/apiConfig';
 import { getApiBaseUrl } from '@/apiConfig';
-import type { Factory } from '@smartcut/types';
+import type { PublicFactory } from '@smartcut/types';
 
 
 const router = useRouter();
 const router = useRouter();
 const route = useRoute();
 const route = useRoute();
@@ -72,7 +72,7 @@ const authStore = useAuthStore();
 
 
 const loading = ref(false);
 const loading = ref(false);
 const loadingFactories = ref(false);
 const loadingFactories = ref(false);
-const factories = ref<Factory[]>([]);
+const factories = ref<PublicFactory[]>([]);
 const selectedFactory = ref('');
 const selectedFactory = ref('');
 // 从 Query 参数获取 factory_id,回退到 SessionStorage
 // 从 Query 参数获取 factory_id,回退到 SessionStorage
 const factoryId = ref(
 const factoryId = ref(
@@ -97,8 +97,7 @@ onMounted(async () => {
         tokenKey: STORAGE_KEYS.PLATFORM_TOKEN
         tokenKey: STORAGE_KEYS.PLATFORM_TOKEN
       });
       });
       const factoryApi = new FactoryApi(apiClient);
       const factoryApi = new FactoryApi(apiClient);
-      const result = await factoryApi.listFactories();
-      factories.value = result.list.filter(f => f.status === 1);
+      factories.value = await factoryApi.listPublicFactories();
     } catch (error) {
     } catch (error) {
       message.error('加载工厂列表失败');
       message.error('加载工厂列表失败');
     } finally {
     } finally {

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

@@ -15,6 +15,29 @@
         </div>
         </div>
       </a-card>
       </a-card>
 
 
+      <!-- 编辑个人信息 -->
+      <a-card class="mt-3" title="个人信息">
+        <a-form v-if="editing" layout="vertical">
+          <a-form-item label="姓名">
+            <a-input v-model:value="formState.name" placeholder="请输入姓名" size="large" :maxlength="32" />
+          </a-form-item>
+          <a-form-item label="手机号">
+            <a-input v-model:value="formState.phone" placeholder="请输入手机号" size="large" :maxlength="20" />
+          </a-form-item>
+          <a-space>
+            <a-button type="primary" :loading="saving" @click="handleSave">保存</a-button>
+            <a-button @click="cancelEdit">取消</a-button>
+          </a-space>
+        </a-form>
+        <div v-else>
+          <a-descriptions :column="1" size="small">
+            <a-descriptions-item label="姓名">{{ authStore.user?.name }}</a-descriptions-item>
+            <a-descriptions-item label="手机号">{{ authStore.user?.phone }}</a-descriptions-item>
+          </a-descriptions>
+          <a-button type="primary" class="mt-3" @click="startEdit">编辑</a-button>
+        </div>
+      </a-card>
+
       <!-- 功能菜单 -->
       <!-- 功能菜单 -->
       <a-card class="mt-3">
       <a-card class="mt-3">
         <a-list>
         <a-list>
@@ -48,15 +71,69 @@
 </template>
 </template>
 
 
 <script setup lang="ts">
 <script setup lang="ts">
+import { ref, reactive } from 'vue';
 import { useRouter } from 'vue-router';
 import { useRouter } from 'vue-router';
 import { message } from 'ant-design-vue';
 import { message } from 'ant-design-vue';
 import { KeyOutlined, RightOutlined, WarningOutlined } from '@ant-design/icons-vue';
 import { KeyOutlined, RightOutlined, WarningOutlined } from '@ant-design/icons-vue';
+import { RecordApi, createApiClient } from '@smartcut/api-client';
+import { STORAGE_KEYS, setSessionStorage } from '@smartcut/shared-utils';
+import { getApiBaseUrl } from '@/apiConfig';
 import { useAuthStore } from '@/stores/auth';
 import { useAuthStore } from '@/stores/auth';
 import WorkerLayout from '@/components/WorkerLayout.vue';
 import WorkerLayout from '@/components/WorkerLayout.vue';
 
 
 const router = useRouter();
 const router = useRouter();
 const authStore = useAuthStore();
 const authStore = useAuthStore();
 
 
+const apiClient = createApiClient({
+  baseURL: getApiBaseUrl(),
+  tokenKey: STORAGE_KEYS.WORKER_TOKEN
+});
+const recordApi = new RecordApi(apiClient);
+
+const editing = ref(false);
+const saving = ref(false);
+const formState = reactive({
+  name: '',
+  phone: ''
+});
+
+function startEdit() {
+  formState.name = authStore.user?.name || '';
+  formState.phone = authStore.user?.phone || '';
+  editing.value = true;
+}
+
+function cancelEdit() {
+  editing.value = false;
+}
+
+async function handleSave() {
+  saving.value = true;
+  try {
+    const res = await recordApi.updateWorkerProfile({
+      name: formState.name,
+      phone: formState.phone
+    });
+    if (authStore.user) {
+      authStore.user.name = formState.name;
+      authStore.user.phone = formState.phone;
+      setSessionStorage(STORAGE_KEYS.WORKER_USER, authStore.user);
+    }
+    editing.value = false;
+    if (res.must_relogin) {
+      message.success('手机号已变更,请重新登录');
+      await authStore.logout();
+      router.push({ name: 'Login', query: { factory: authStore.currentFactoryId || '' } });
+    } else {
+      message.success('更新成功');
+    }
+  } catch (error: any) {
+    message.error(error.response?.data?.msg || '更新失败');
+  } finally {
+    saving.value = false;
+  }
+}
+
 async function handleLogout() {
 async function handleLogout() {
   await authStore.logout();
   await authStore.logout();
   message.success('已退出登录');
   message.success('已退出登录');
@@ -90,4 +167,4 @@ async function handleLogout() {
 .mt-3 {
 .mt-3 {
   margin-top: 12px;
   margin-top: 12px;
 }
 }
-</style>
+</style>

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

@@ -31,7 +31,7 @@
                 </template>
                 </template>
                 <template #description>
                 <template #description>
                   {{ item.order_no }} - {{ item.style_name }}<br/>
                   {{ item.order_no }} - {{ item.style_name }}<br/>
-                  数量:{{ item.quantity }}件 | 单价:¥{{ item.price.toFixed(2) }} | 金额:¥{{ item.amount.toFixed(2) }}<br/>
+                  数量:{{ item.quantity }}件 | 单价:¥{{ formatMoney(item.price) }} | 金额:¥{{ formatMoney(item.amount) }}<br/>
                   日期:{{ item.record_date }}
                   日期:{{ item.record_date }}
                 </template>
                 </template>
               </a-list-item-meta>
               </a-list-item-meta>
@@ -55,7 +55,7 @@ import { message } from 'ant-design-vue';
 import dayjs from 'dayjs';
 import dayjs from 'dayjs';
 import WorkerLayout from '@/components/WorkerLayout.vue';
 import WorkerLayout from '@/components/WorkerLayout.vue';
 import { RecordApi, createApiClient } from '@smartcut/api-client';
 import { RecordApi, createApiClient } from '@smartcut/api-client';
-import { STORAGE_KEYS, formatDate } from '@smartcut/shared-utils';
+import { STORAGE_KEYS, formatDate, formatMoney } from '@smartcut/shared-utils';
 import { getApiBaseUrl } from '@/apiConfig';
 import { getApiBaseUrl } from '@/apiConfig';
 import type { PieceRecord } from '@smartcut/types';
 import type { PieceRecord } from '@smartcut/types';
 
 

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

@@ -14,11 +14,14 @@
                   {{ item.period }}
                   {{ item.period }}
                 </template>
                 </template>
                 <template #description>
                 <template #description>
-                  数量:{{ item.total_quantity }}件 | 记录:{{ item.records_count }}条
+                  数量:{{ item.quantity }}件 | 记录:{{ item.record_count }}条
                 </template>
                 </template>
               </a-list-item-meta>
               </a-list-item-meta>
               <div class="salary-amount">
               <div class="salary-amount">
-                ¥{{ item.total_amount.toFixed(2) }}
+                ¥{{ formatMoney(item.amount) }}
+                <a-tag :color="item.status === 'locked' ? 'red' : 'green'" class="ml-2">
+                  {{ item.status === 'locked' ? '已锁定' : '待锁定' }}
+                </a-tag>
               </div>
               </div>
             </a-list-item>
             </a-list-item>
           </template>
           </template>
@@ -39,7 +42,7 @@ import { ref, onMounted } from 'vue';
 import { message } from 'ant-design-vue';
 import { message } from 'ant-design-vue';
 import WorkerLayout from '@/components/WorkerLayout.vue';
 import WorkerLayout from '@/components/WorkerLayout.vue';
 import { SalaryApi, createApiClient } from '@smartcut/api-client';
 import { SalaryApi, createApiClient } from '@smartcut/api-client';
-import { STORAGE_KEYS } from '@smartcut/shared-utils';
+import { STORAGE_KEYS, formatMoney } from '@smartcut/shared-utils';
 import { getApiBaseUrl } from '@/apiConfig';
 import { getApiBaseUrl } from '@/apiConfig';
 import type { WorkerSalary } from '@smartcut/types';
 import type { WorkerSalary } from '@smartcut/types';
 
 

+ 44 - 16
apps/worker-app/src/views/ScanView.vue

@@ -16,10 +16,12 @@
             开始扫描
             开始扫描
           </a-button>
           </a-button>
 
 
-          <div v-if="scanning" class="scanning">
-            <a-spin tip="正在扫描..." />
-            <a-button @click="stopScan" class="mt-3">停止扫描</a-button>
+          <div v-if="scanning" class="camera-area">
+            <video ref="videoRef" autoplay playsinline class="w-full rounded-lg" />
+            <div class="text-center text-gray-400 mt-2">将二维码对准摄像头</div>
+            <a-button @click="stopScan" class="mt-3" block>停止扫描</a-button>
           </div>
           </div>
+          <div v-if="scanError" class="text-red-500 mt-2 text-center">{{ scanError }}</div>
         </div>
         </div>
 
 
         <!-- 手动输入二维码 -->
         <!-- 手动输入二维码 -->
@@ -77,22 +79,27 @@
 </template>
 </template>
 
 
 <script setup lang="ts">
 <script setup lang="ts">
-import { ref, onMounted } from 'vue';
+import { ref, onMounted, onUnmounted } from 'vue';
 import { message } from 'ant-design-vue';
 import { message } from 'ant-design-vue';
 import { ScanOutlined } from '@ant-design/icons-vue';
 import { ScanOutlined } from '@ant-design/icons-vue';
+import { BrowserMultiFormatReader } from '@zxing/browser';
 import WorkerLayout from '@/components/WorkerLayout.vue';
 import WorkerLayout from '@/components/WorkerLayout.vue';
 import { RecordApi, ProcessApi, createApiClient } from '@smartcut/api-client';
 import { RecordApi, ProcessApi, createApiClient } from '@smartcut/api-client';
 import { STORAGE_KEYS } from '@smartcut/shared-utils';
 import { STORAGE_KEYS } from '@smartcut/shared-utils';
 import { getApiBaseUrl } from '@/apiConfig';
 import { getApiBaseUrl } from '@/apiConfig';
-import type { Process } from '@smartcut/types';
+import type { Process, BundleInfo } from '@smartcut/types';
 
 
 const scanning = ref(false);
 const scanning = ref(false);
 const qrCode = ref('');
 const qrCode = ref('');
-const bundleInfo = ref<any>(null);
+const bundleInfo = ref<BundleInfo | null>(null);
 const processes = ref<Process[]>([]);
 const processes = ref<Process[]>([]);
 const processId = ref<number | null>(null);
 const processId = ref<number | null>(null);
 const quantity = ref(1);
 const quantity = ref(1);
 const submitting = ref(false);
 const submitting = ref(false);
+const reader = ref<BrowserMultiFormatReader | null>(null);
+const videoRef = ref<HTMLVideoElement | null>(null);
+const scanError = ref('');
+const controls = ref<{ stop: () => void } | null>(null);
 
 
 const apiClient = createApiClient({
 const apiClient = createApiClient({
   baseURL: getApiBaseUrl(),
   baseURL: getApiBaseUrl(),
@@ -110,15 +117,39 @@ onMounted(async () => {
   }
   }
 });
 });
 
 
+onUnmounted(() => {
+  stopScan();
+});
+
 async function startScan() {
 async function startScan() {
   scanning.value = true;
   scanning.value = true;
-  // 实际项目中这里调用摄像头扫码API
-  // 由于浏览器限制,需要HTTPS环境
-  message.info('扫码功能需要在HTTPS环境下使用,请手动输入二维码');
-  scanning.value = false;
+  scanError.value = '';
+  try {
+    if (!reader.value) {
+      reader.value = new BrowserMultiFormatReader();
+    }
+    controls.value = await reader.value.decodeFromConstraints(
+      { video: { facingMode: 'environment' } },
+      videoRef.value!,
+      (result) => {
+        if (result) {
+          qrCode.value = result.getText();
+          stopScan();
+          handleQrCode();
+        }
+      }
+    );
+  } catch (e: any) {
+    scanError.value = '无法访问摄像头,请手动输入二维码';
+    scanning.value = false;
+  }
 }
 }
 
 
 function stopScan() {
 function stopScan() {
+  if (controls.value) {
+    controls.value.stop();
+    controls.value = null;
+  }
   scanning.value = false;
   scanning.value = false;
 }
 }
 
 
@@ -130,10 +161,7 @@ async function handleQrCode() {
 
 
   try {
   try {
     // 解析二维码获取扎号信息
     // 解析二维码获取扎号信息
-    const response = await apiClient.get('/worker/bundle-info', {
-      params: { qr_code: qrCode.value }
-    });
-    bundleInfo.value = response.data.data;
+    bundleInfo.value = await recordApi.getBundleInfo(qrCode.value);
     quantity.value = bundleInfo.value.quantity;
     quantity.value = bundleInfo.value.quantity;
     message.success('已识别扎号');
     message.success('已识别扎号');
   } catch (error: any) {
   } catch (error: any) {
@@ -179,9 +207,9 @@ async function handleSubmit() {
   text-align: center;
   text-align: center;
   padding: 20px 0;
   padding: 20px 0;
 }
 }
-.scanning {
+.camera-area {
   text-align: center;
   text-align: center;
-  padding: 40px 0;
+  padding: 20px 0;
 }
 }
 .mt-3 {
 .mt-3 {
   margin-top: 12px;
   margin-top: 12px;

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

@@ -57,7 +57,7 @@
 import { ref, onMounted } from 'vue';
 import { ref, onMounted } from 'vue';
 import { message } from 'ant-design-vue';
 import { message } from 'ant-design-vue';
 import WorkerLayout from '@/components/WorkerLayout.vue';
 import WorkerLayout from '@/components/WorkerLayout.vue';
-import { WorkOrderApi, ProcessApi, createApiClient } from '@smartcut/api-client';
+import { WorkOrderApi, ProcessApi, RecordApi, createApiClient } from '@smartcut/api-client';
 import { STORAGE_KEYS } from '@smartcut/shared-utils';
 import { STORAGE_KEYS } from '@smartcut/shared-utils';
 import { getApiBaseUrl } from '@/apiConfig';
 import { getApiBaseUrl } from '@/apiConfig';
 import type { WorkOrder, Process } from '@smartcut/types';
 import type { WorkOrder, Process } from '@smartcut/types';
@@ -76,6 +76,7 @@ const apiClient = createApiClient({
 
 
 const workOrderApi = new WorkOrderApi(apiClient);
 const workOrderApi = new WorkOrderApi(apiClient);
 const processApi = new ProcessApi(apiClient);
 const processApi = new ProcessApi(apiClient);
+const recordApi = new RecordApi(apiClient);
 
 
 function filterOption(input: string, option: any) {
 function filterOption(input: string, option: any) {
   return option.children[0].children.toLowerCase().indexOf(input.toLowerCase()) >= 0;
   return option.children[0].children.toLowerCase().indexOf(input.toLowerCase()) >= 0;
@@ -113,7 +114,7 @@ async function handleSubmit() {
   submitting.value = true;
   submitting.value = true;
   try {
   try {
     // 手动计件:通过worker端API提交
     // 手动计件:通过worker端API提交
-    await apiClient.post('/worker/submit', {
+    await recordApi.submitPiece({
       work_order_id: workOrderId.value,
       work_order_id: workOrderId.value,
       process_id: processId.value,
       process_id: processId.value,
       quantity: quantity.value
       quantity: quantity.value

+ 0 - 72
deploy/deploy.ps1

@@ -1,72 +0,0 @@
-# 智裁云前端部署脚本(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

+ 0 - 126
deploy/nginx.conf

@@ -1,126 +0,0 @@
-# 智裁云前端 Nginx 部署配置(独立子域名方案)
-# 三个 Vue 应用分别部署在独立子域名,各自 server 块含 SPA history 回退
-# 部署后需 reload: sudo nginx -t && sudo nginx -s reload
-# 注意:SSL 证书路径需根据生产服务器实际路径调整
-
-# ==================== 系统管理后台 ====================
-server {
-    listen 443 ssl http2;
-    server_name platform.smartcut.51zj.cc;
-
-    ssl_certificate     /etc/nginx/ssl/51zj.cc.crt;
-    ssl_certificate_key /etc/nginx/ssl/51zj.cc.key;
-
-    root /var/www/smartcut/dist/platform-app;
-    index index.html;
-
-    # SPA history 回退:未匹配的路径一律返回 index.html,交由前端路由处理
-    location / {
-        try_files $uri $uri/ /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; }
-
-    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;
-}
-
-# ==================== 工厂管理后台 ====================
-server {
-    listen 443 ssl http2;
-    server_name factory.smartcut.51zj.cc;
-
-    ssl_certificate     /etc/nginx/ssl/51zj.cc.crt;
-    ssl_certificate_key /etc/nginx/ssl/51zj.cc.key;
-
-    root /var/www/smartcut/dist/factory-app;
-    index index.html;
-
-    location / {
-        try_files $uri $uri/ /index.html;
-    }
-
-    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
-        expires 1y;
-        add_header Cache-Control "public, immutable";
-    }
-
-    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; }
-
-    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;
-}
-
-# ==================== 工人端 ====================
-server {
-    listen 443 ssl http2;
-    server_name worker.smartcut.51zj.cc;
-
-    ssl_certificate     /etc/nginx/ssl/51zj.cc.crt;
-    ssl_certificate_key /etc/nginx/ssl/51zj.cc.key;
-
-    root /var/www/smartcut/dist/worker-app;
-    index index.html;
-
-    location / {
-        try_files $uri $uri/ /index.html;
-    }
-
-    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
-        expires 1y;
-        add_header Cache-Control "public, immutable";
-    }
-
-    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; }
-
-    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;
-}
-
-# ==================== HTTP → HTTPS 重定向 ====================
-server {
-    listen 80;
-    server_name platform.smartcut.51zj.cc factory.smartcut.51zj.cc worker.smartcut.51zj.cc;
-    return 301 https://$host$request_uri;
-}

+ 3 - 23
packages/api-client/src/client.ts

@@ -1,4 +1,4 @@
-// Axios客户端实例(完全重写:Query参数传递factory_id)
+// Axios客户端实例(仅注入JWT Token,factory_id由后端从Token提取)
 
 
 import axios from 'axios';
 import axios from 'axios';
 import type { AxiosInstance } from 'axios';
 import type { AxiosInstance } from 'axios';
@@ -11,7 +11,7 @@ export interface ApiClientConfig {
 
 
 /**
 /**
  * 创建API客户端实例
  * 创建API客户端实例
- * 关键改变:factory_id通过Query参数传递,废弃路径参数模式
+ * factory_id 不再由前端传递,后端通过 JWT Token 自动提取
  */
  */
 export function createApiClient(config: ApiClientConfig): AxiosInstance {
 export function createApiClient(config: ApiClientConfig): AxiosInstance {
   const instance = axios.create({
   const instance = axios.create({
@@ -22,33 +22,13 @@ export function createApiClient(config: ApiClientConfig): AxiosInstance {
     }
     }
   });
   });
 
 
-  // 请求拦截器:注入JWT Token + factory_id(全新实现)
+  // 请求拦截器:仅注入JWT Token
   instance.interceptors.request.use(
   instance.interceptors.request.use(
     (requestConfig) => {
     (requestConfig) => {
-      // 1. 注入JWT Token
       const token = getSessionStorage<string>(config.tokenKey);
       const token = getSessionStorage<string>(config.tokenKey);
       if (token) {
       if (token) {
         requestConfig.headers.Authorization = `Bearer ${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;
       return requestConfig;
     },
     },
     (error) => Promise.reject(error)
     (error) => Promise.reject(error)

+ 1 - 1
packages/api-client/src/endpoints/auditLog.ts

@@ -10,7 +10,7 @@ export class AuditLogApi {
    * 获取审计日志列表
    * 获取审计日志列表
    */
    */
   async getAuditLogs(params?: { page?: number; page_size?: number; action?: string; target_type?: string; actor_user_id?: number; factory_id?: string; success?: number; start?: string; end?: string }): Promise<PaginatedResponse<AuditLog>> {
   async getAuditLogs(params?: { page?: number; page_size?: number; action?: string; target_type?: string; actor_user_id?: number; factory_id?: string; success?: number; start?: string; end?: string }): Promise<PaginatedResponse<AuditLog>> {
-    const response = await this.client.get<ApiResponse<PaginatedResponse<AuditLog>>>('/audit-logs', { params });
+    const response = await this.client.get<ApiResponse<PaginatedResponse<AuditLog>>>('/sys/audit-logs', { params });
     return response.data.data;
     return response.data.data;
   }
   }
 }
 }

+ 6 - 3
packages/api-client/src/endpoints/auth.ts

@@ -9,10 +9,13 @@ export class AuthApi {
 
 
   /**
   /**
    * 用户登录
    * 用户登录
-   * 新增factory_id参数(Query参数传递)
+   * factory_id 通过 Query 参数传递(登录请求必须传递)
    */
    */
-  async login(data: LoginRequest): Promise<LoginResponse> {
-    const response = await this.client.post<ApiResponse<LoginResponse>>('/auth/login', data);
+  async login(data: LoginRequest, factoryId: string): Promise<LoginResponse> {
+    const response = await this.client.post<ApiResponse<LoginResponse>>(
+      `/auth/login?factory_id=${encodeURIComponent(factoryId)}`,
+      data
+    );
     if (!response.data.data) {
     if (!response.data.data) {
       throw new Error(response.data.msg || '登录失败');
       throw new Error(response.data.msg || '登录失败');
     }
     }

+ 9 - 1
packages/api-client/src/endpoints/factory.ts

@@ -1,7 +1,7 @@
 // 工厂管理API端点(系统级)
 // 工厂管理API端点(系统级)
 
 
 import type { AxiosInstance } from 'axios';
 import type { AxiosInstance } from 'axios';
-import type { Factory, ApiResponse, PaginatedResponse } from '@smartcut/types';
+import type { Factory, PublicFactory, ApiResponse, PaginatedResponse } from '@smartcut/types';
 
 
 export interface CreateFactoryRequest {
 export interface CreateFactoryRequest {
   id: string;
   id: string;
@@ -65,4 +65,12 @@ export class FactoryApi {
   async enableFactory(id: string): Promise<void> {
   async enableFactory(id: string): Promise<void> {
     await this.client.post<ApiResponse>(`/factories/${id}/enable`);
     await this.client.post<ApiResponse>(`/factories/${id}/enable`);
   }
   }
+
+  /**
+   * 获取公开工厂列表(无需鉴权,仅启用的工厂)
+   */
+  async listPublicFactories(): Promise<PublicFactory[]> {
+    const response = await this.client.get<ApiResponse<{ list: PublicFactory[]; total: number }>>('/public/factories');
+    return response.data.data.list;
+  }
 }
 }

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

@@ -6,6 +6,8 @@ import type {
   ScanRequest,
   ScanRequest,
   RecordQueryParams,
   RecordQueryParams,
   WorkerRecordQueryParams,
   WorkerRecordQueryParams,
+  BundleInfo,
+  ScanResult,
   ApiResponse,
   ApiResponse,
   PaginatedResponse
   PaginatedResponse
 } from '@smartcut/types';
 } from '@smartcut/types';
@@ -58,4 +60,30 @@ export class RecordApi {
     const response = await this.client.get<ApiResponse<PaginatedResponse<PieceRecord>>>('/worker/records', { params });
     const response = await this.client.get<ApiResponse<PaginatedResponse<PieceRecord>>>('/worker/records', { params });
     return response.data.data;
     return response.data.data;
   }
   }
+
+  /**
+   * 工人端:查询扎号详情(扫码预览)
+   */
+  async getBundleInfo(qrCode: string): Promise<BundleInfo> {
+    const response = await this.client.get<ApiResponse<BundleInfo>>('/worker/bundle-info', {
+      params: { qr_code: qrCode }
+    });
+    return response.data.data;
+  }
+
+  /**
+   * 工人端:手动计件(工单级别)
+   */
+  async submitPiece(data: { work_order_id: number; process_id: number; quantity: number }): Promise<ScanResult> {
+    const response = await this.client.post<ApiResponse<ScanResult>>('/worker/submit', data);
+    return response.data.data;
+  }
+
+  /**
+   * 工人端:更新个人信息(姓名/手机号)
+   */
+  async updateWorkerProfile(data: { name?: string; phone?: string }): Promise<{ must_relogin?: boolean }> {
+    const response = await this.client.put<ApiResponse<{ must_relogin?: boolean } | null>>('/worker/profile', data);
+    return response.data.data || {};
+  }
 }
 }

+ 3 - 3
packages/api-client/src/endpoints/salary.ts

@@ -3,7 +3,7 @@
 import type { AxiosInstance } from 'axios';
 import type { AxiosInstance } from 'axios';
 import type {
 import type {
   SalaryPeriod,
   SalaryPeriod,
-  SalaryDetailItem,
+  SalaryDetailResponse,
   WorkerSalary,
   WorkerSalary,
   GenerateSalaryRequest,
   GenerateSalaryRequest,
   ApiResponse
   ApiResponse
@@ -30,8 +30,8 @@ export class SalaryApi {
   /**
   /**
    * 获取工资明细
    * 获取工资明细
    */
    */
-  async getSalaryDetail(period: string): Promise<SalaryDetailItem[]> {
-    const response = await this.client.get<ApiResponse<SalaryDetailItem[]>>(`/salary/detail/${period}`);
+  async getSalaryDetail(period: string, params?: { page?: number; page_size?: number }): Promise<SalaryDetailResponse> {
+    const response = await this.client.get<ApiResponse<SalaryDetailResponse>>(`/salary/detail/${period}`, { params });
     return response.data.data;
     return response.data.data;
   }
   }
 
 

+ 42 - 0
packages/api-client/src/endpoints/systemAuth.ts

@@ -0,0 +1,42 @@
+// 系统管理员认证API端点(系统级,路径前缀 /sys)
+
+import type { AxiosInstance } from 'axios';
+import type { LoginResponse, SystemUser, ChangePasswordRequest } from '@smartcut/types';
+import type { ApiResponse } from '@smartcut/types';
+
+export class SystemAuthApi {
+  constructor(private client: AxiosInstance) {}
+
+  /**
+   * 系统管理员登录(无需 factory_id)
+   */
+  async login(data: { username: string; password: string }): Promise<LoginResponse> {
+    const response = await this.client.post<ApiResponse<LoginResponse>>('/sys/auth/login', data);
+    if (!response.data.data) {
+      throw new Error(response.data.msg || '登录失败');
+    }
+    return response.data.data;
+  }
+
+  /**
+   * 获取当前系统管理员信息
+   */
+  async getProfile(): Promise<SystemUser> {
+    const response = await this.client.get<ApiResponse<SystemUser>>('/sys/auth/profile');
+    return response.data.data;
+  }
+
+  /**
+   * 修改密码
+   */
+  async changePassword(data: ChangePasswordRequest): Promise<void> {
+    await this.client.put<ApiResponse>('/sys/auth/password', data);
+  }
+
+  /**
+   * 登出(撤销后端 jti)
+   */
+  async logout(): Promise<void> {
+    await this.client.post<ApiResponse>('/sys/auth/logout');
+  }
+}

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

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

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

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

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

@@ -11,5 +11,8 @@ export { SalaryApi } from './endpoints/salary';
 export { StatsApi } from './endpoints/stats';
 export { StatsApi } from './endpoints/stats';
 export { AuditLogApi } from './endpoints/auditLog';
 export { AuditLogApi } from './endpoints/auditLog';
 export { BackupApi } from './endpoints/backup';
 export { BackupApi } from './endpoints/backup';
+export { SystemAuthApi } from './endpoints/systemAuth';
+export { SystemFactoryApi } from './endpoints/systemFactory';
+export { SystemUserApi } from './endpoints/systemUser';
 
 
 export type { ApiClientConfig } from './client';
 export type { ApiClientConfig } from './client';

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

@@ -29,6 +29,12 @@ export interface Factory {
   updated_at: string;
   updated_at: string;
 }
 }
 
 
+// 公开工厂信息(不含敏感字段,供登录页使用)
+export interface PublicFactory {
+  id: string;
+  name: string;
+}
+
 // 裁床批次类型
 // 裁床批次类型
 export interface CutBatch {
 export interface CutBatch {
   id: number;
   id: number;

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

@@ -36,6 +36,7 @@ export interface RecordQueryParams {
   date?: string;
   date?: string;
   work_order_id?: number;
   work_order_id?: number;
   keyword?: string; // 搜索关键词(员工姓名)
   keyword?: string; // 搜索关键词(员工姓名)
+  status?: string; // 状态筛选(normal/reverted)
 }
 }
 
 
 // 工人端计件记录查询参数
 // 工人端计件记录查询参数
@@ -43,4 +44,29 @@ export interface WorkerRecordQueryParams {
   page?: number;
   page?: number;
   page_size?: number;
   page_size?: number;
   date?: string;
   date?: string;
+}
+
+// 扎号详情(扫码预览,不写计件记录)
+export interface BundleInfo {
+  bundle_no: string;
+  size: string;
+  quantity: number;
+  order_no: string;
+  style_name: string;
+  batch_id: number;
+  bundle_id: number;
+  work_order_id: number;
+}
+
+// 扫码/手动计件结果
+export interface ScanResult {
+  id: number;
+  work_order: string;
+  style: string;
+  quantity: number;
+  price: number;
+  amount: number;
+  record_date: string;
+  price_missing: boolean;
+  clamped: boolean;
 }
 }

+ 16 - 4
packages/types/src/salary.ts

@@ -13,15 +13,27 @@ export interface SalaryDetailItem {
   user_name: string;
   user_name: string;
   total_amount: number;
   total_amount: number;
   total_quantity: number;
   total_quantity: number;
-  records_count: number;
+  record_count: number;
+}
+
+// 工资明细响应(包含明细列表和汇总信息)
+export interface SalaryDetailResponse {
+  details: SalaryDetailItem[];
+  grand_total: number;
+  status: string;
+  period: string;
+  total: number;
+  page: number;
+  page_size: number;
 }
 }
 
 
 // 工人生成的工资数据
 // 工人生成的工资数据
 export interface WorkerSalary {
 export interface WorkerSalary {
   period: string;
   period: string;
-  total_amount: number;
-  total_quantity: number;
-  records_count: number;
+  amount: number;
+  quantity: number;
+  record_count: number;
+  status: string;
 }
 }
 
 
 // 生成工资请求
 // 生成工资请求