Sfoglia il codice sorgente

系统管理后台第一版

Gogs 1 mese fa
parent
commit
aa4f81854c

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

@@ -128,7 +128,7 @@ async function handleLogin() {
     const safeRedirect = redirect && redirect.startsWith('/') && !redirect.startsWith('//') ? redirect : '/';
     router.push(safeRedirect);
   } catch (error: any) {
-    const errorMsg = error.response?.data?.msg || '登录失败';
+    const errorMsg = error.response?.data?.msg || error.message || '登录失败';
     message.error(errorMsg);
   } finally {
     loading.value = false;

+ 13 - 3
apps/platform-app/src/App.vue

@@ -12,7 +12,7 @@
 </template>
 
 <script setup lang="ts">
-import { computed, onMounted } from 'vue';
+import { computed, onMounted, onUnmounted } from 'vue';
 import { useRouter, useRoute } from 'vue-router';
 import { message } from 'ant-design-vue';
 import zhCN from 'ant-design-vue/es/locale/zh_CN';
@@ -26,6 +26,10 @@ const route = useRoute();
 const authStore = useAuthStore();
 const layoutStore = useLayoutStore();
 
+const handleResize = () => {
+  layoutStore.setMobile(window.innerWidth < 768);
+};
+
 // 初始化布局配置
 onMounted(() => {
   layoutStore.init({
@@ -35,6 +39,12 @@ onMounted(() => {
       { key: 'users', label: '系统用户', icon: UserOutlined, path: '/users' }
     ]
   });
+  handleResize();
+  window.addEventListener('resize', handleResize);
+});
+
+onUnmounted(() => {
+  window.removeEventListener('resize', handleResize);
 });
 
 // 根据路由 meta 判断是否显示布局
@@ -43,8 +53,8 @@ const showLayout = computed(() => {
 });
 
 // 登出处理
-async function handleLogout() {
-  await authStore.logout();
+function handleLogout() {
+  authStore.logout();
   message.success('已退出登录');
   router.push('/login');
 }

+ 4 - 4
apps/platform-app/src/components/HeaderBar.vue

@@ -1,9 +1,9 @@
 <template>
-  <header class="bg-white shadow-sm h-16 flex items-center justify-between px-6 flex-shrink-0 border-b border-gray-100">
+  <header class="bg-white shadow-sm h-16 flex items-center justify-between px-4 md:px-6 flex-shrink-0 border-b border-gray-100">
     <!-- 左侧:Logo + 折叠按钮 -->
     <div class="flex items-center">
       <!-- 品牌Logo -->
-      <h1 class="text-xl font-bold bg-gradient-to-r from-blue-500 to-blue-700 bg-clip-text text-transparent mr-6">
+      <h1 class="text-xl font-bold bg-gradient-to-r from-blue-500 to-blue-700 bg-clip-text text-transparent mr-2 md:mr-6">
         智裁云
       </h1>
       <!-- 折叠按钮 -->
@@ -17,8 +17,8 @@
       </a-button>
     </div>
 
-    <!-- 中间:面包屑导航 -->
-    <div class="flex-1 flex justify-center">
+    <!-- 中间:面包屑导航(移动端隐藏) -->
+    <div class="flex-1 hidden md:flex justify-center">
       <Breadcrumb />
     </div>
 

+ 10 - 2
apps/platform-app/src/components/SideMenu.vue

@@ -1,14 +1,14 @@
 <template>
   <aside
     class="bg-white border-r border-gray-200 transition-all duration-300 flex-shrink-0"
-    :style="{ width: collapsed ? '64px' : '256px' }"
+    :style="{ width: collapsed ? '64px' : '200px' }"
   >
     <nav class="py-4">
       <router-link
         v-for="item in menuItems"
         :key="item.key"
         :to="item.path"
-        class="menu-item flex items-center px-6 py-3 transition-all duration-200 relative"
+        class="menu-item flex items-center px-4 py-3 transition-all duration-200 relative no-underline"
         :class="{
           'active': isActive(item.path),
           'text-gray-600 hover:bg-gray-50 hover:text-gray-900': !isActive(item.path)
@@ -56,4 +56,12 @@ function isActive(path: string): boolean {
   background-color: #eff6ff;
   color: #2563eb;
 }
+
+.menu-item {
+  text-decoration: none;
+}
+
+.menu-item:hover {
+  text-decoration: none;
+}
 </style>

+ 4 - 4
apps/platform-app/src/components/UserMenu.vue

@@ -2,15 +2,15 @@
   <a-dropdown :trigger="['click']">
     <div class="flex items-center cursor-pointer px-3 py-1.5 rounded-lg hover:bg-gray-100 transition-colors duration-200">
       <!-- 头像 -->
-      <a-avatar class="bg-gradient-to-br from-blue-500 to-blue-600 text-white font-bold mr-2">
+      <a-avatar class="bg-gradient-to-br from-blue-500 to-blue-600 text-white font-bold mr-0 sm:mr-2">
         {{ userName?.charAt(0) || '?' }}
       </a-avatar>
-      <!-- 用户名和角色 -->
-      <div class="flex flex-col">
+      <!-- 用户名和角色(移动端隐藏) -->
+      <div class="flex flex-col hidden sm:flex">
         <span class="text-sm font-medium text-gray-800">{{ userName || '未登录' }}</span>
         <span class="text-xs text-gray-500">管理员</span>
       </div>
-      <DownOutlined class="ml-2 text-gray-400 text-xs" />
+      <DownOutlined class="ml-2 text-gray-400 text-xs hidden sm:inline" />
     </div>
     <template #overlay>
       <a-menu class="!rounded-lg !shadow-lg !min-w-[160px]">

+ 35 - 2
apps/platform-app/src/layouts/BasicLayout.vue

@@ -4,18 +4,35 @@
     <HeaderBar
       :collapsed="layoutStore.collapsed"
       :userName="userName"
-      @toggle="layoutStore.toggleCollapsed()"
+      @toggle="handleToggle"
       @logout="$emit('logout')"
     />
 
     <!-- 下方区域:侧边栏 + 主内容区 -->
     <div class="flex-1 flex overflow-hidden">
-      <!-- 侧边栏 -->
+      <!-- 桌面端侧边栏 -->
       <SideMenu
+        v-if="!layoutStore.isMobile"
         :menuItems="layoutStore.menuItems"
         :collapsed="layoutStore.collapsed"
       />
 
+      <!-- 移动端抽屉侧边栏 -->
+      <a-drawer
+        v-else
+        :open="layoutStore.drawerOpen"
+        placement="left"
+        :closable="false"
+        :body-style="{ padding: '0' }"
+        width="220px"
+        @close="layoutStore.closeDrawer()"
+      >
+        <SideMenu
+          :menuItems="layoutStore.menuItems"
+          :collapsed="false"
+        />
+      </a-drawer>
+
       <!-- 主内容区 -->
       <main class="flex-1 overflow-auto bg-gray-50">
         <slot />
@@ -25,6 +42,8 @@
 </template>
 
 <script setup lang="ts">
+import { watch } from 'vue';
+import { useRoute } from 'vue-router';
 import HeaderBar from '@/components/HeaderBar.vue';
 import SideMenu from '@/components/SideMenu.vue';
 import { useLayoutStore } from '@/stores/layout';
@@ -38,4 +57,18 @@ defineEmits<{
 }>();
 
 const layoutStore = useLayoutStore();
+const route = useRoute();
+
+function handleToggle() {
+  if (layoutStore.isMobile) {
+    layoutStore.toggleDrawer();
+  } else {
+    layoutStore.toggleCollapsed();
+  }
+}
+
+// 移动端路由切换后自动关闭抽屉
+watch(() => route.path, () => {
+  if (layoutStore.isMobile) layoutStore.closeDrawer();
+});
 </script>

+ 16 - 1
apps/platform-app/src/stores/layout.ts

@@ -15,7 +15,9 @@ export interface LayoutConfig {
 export const useLayoutStore = defineStore('layout', {
   state: () => ({
     menuItems: [] as MenuItem[],
-    collapsed: false
+    collapsed: false,
+    isMobile: false,
+    drawerOpen: false
   }),
 
   actions: {
@@ -25,6 +27,19 @@ export const useLayoutStore = defineStore('layout', {
 
     toggleCollapsed() {
       this.collapsed = !this.collapsed;
+    },
+
+    setMobile(value: boolean) {
+      this.isMobile = value;
+      if (!value) this.drawerOpen = false;
+    },
+
+    toggleDrawer() {
+      this.drawerOpen = !this.drawerOpen;
+    },
+
+    closeDrawer() {
+      this.drawerOpen = false;
     }
   }
 });

+ 1 - 1
apps/platform-app/src/views/ChangePasswordView.vue

@@ -1,5 +1,5 @@
 <template>
-  <div class="p-8">
+  <div class="p-4 md:p-8">
       <a-card class="max-w-2xl mx-auto rounded-xl shadow-md !border-0">
         <template #title>
           <div class="flex items-center">

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

@@ -1,9 +1,9 @@
 <template>
-  <div class="p-8">
+  <div class="p-4 md:p-8">
     <!-- 统计卡片区域 -->
-    <a-row :gutter="24" class="mb-8">
+    <a-row :gutter="[16, 16]" class="mb-4 md:mb-8">
       <!-- 工厂总数卡片 -->
-      <a-col :span="6">
+      <a-col :xs="24" :sm="12" :lg="6">
         <a-card class="stat-card bg-gradient-to-br from-blue-500 to-blue-600 text-white rounded-xl shadow-lg hover:shadow-xl hover:scale-105 transition-all duration-300 !border-0">
           <div class="flex items-center justify-between">
             <div>
@@ -17,7 +17,7 @@
       </a-col>
 
       <!-- 启用工厂卡片 -->
-      <a-col :span="6">
+      <a-col :xs="24" :sm="12" :lg="6">
         <a-card class="stat-card bg-gradient-to-br from-green-500 to-green-600 text-white rounded-xl shadow-lg hover:shadow-xl hover:scale-105 transition-all duration-300 !border-0">
           <div class="flex items-center justify-between">
             <div>
@@ -31,7 +31,7 @@
       </a-col>
 
       <!-- 系统用户卡片 -->
-      <a-col :span="6">
+      <a-col :xs="24" :sm="12" :lg="6">
         <a-card class="stat-card bg-gradient-to-br from-purple-500 to-purple-600 text-white rounded-xl shadow-lg hover:shadow-xl hover:scale-105 transition-all duration-300 !border-0">
           <div class="flex items-center justify-between">
             <div>
@@ -45,7 +45,7 @@
       </a-col>
 
       <!-- 审计日志卡片 -->
-      <a-col :span="6">
+      <a-col :xs="24" :sm="12" :lg="6">
         <a-card class="stat-card bg-gradient-to-br from-orange-500 to-orange-600 text-white rounded-xl shadow-lg hover:shadow-xl hover:scale-105 transition-all duration-300 !border-0">
           <div class="flex items-center justify-between">
             <div>

+ 77 - 6
apps/platform-app/src/views/FactoriesView.vue

@@ -1,12 +1,12 @@
 <template>
-  <div class="p-8">
+  <div class="p-4 md:p-8">
       <!-- 头部工具栏 -->
       <a-card class="mb-6 bg-gray-50 rounded-lg shadow-sm !border-0">
-        <a-space size="large">
+        <a-space size="large" wrap>
           <a-input-search
             v-model:value="searchKeyword"
             placeholder="搜索工厂名称..."
-            class="w-80"
+            class="w-full sm:w-80"
             @search="loadFactories"
           />
           <a-button type="primary" @click="showCreateModal" class="shadow-sm hover:shadow-md transition-shadow">
@@ -16,8 +16,8 @@
         </a-space>
       </a-card>
 
-      <!-- 工厂列表表格 -->
-      <a-card class="rounded-xl shadow-md !border-0">
+      <!-- 桌面端:工厂列表表格 -->
+      <a-card v-if="!layoutStore.isMobile" class="rounded-xl shadow-md !border-0">
         <a-table
           :columns="columns"
           :dataSource="factories"
@@ -25,6 +25,7 @@
           :pagination="pagination"
           @change="handleTableChange"
           rowKey="id"
+          :scroll="{ x: 800 }"
         >
           <!-- 状态列 -->
           <template #status="{ record }">
@@ -64,6 +65,64 @@
         </a-table>
       </a-card>
 
+      <!-- 移动端:工厂卡片列表 -->
+      <div v-else class="space-y-3">
+        <a-spin :spinning="loading">
+          <a-empty v-if="!loading && factories.length === 0" description="暂无工厂数据" class="py-8" />
+          <a-card
+            v-for="item in factories"
+            :key="item.id"
+            class="rounded-xl shadow-sm !border-0"
+            size="small"
+          >
+            <div class="flex items-start justify-between gap-2 mb-2">
+              <span class="font-medium text-gray-900 break-all">{{ item.name }}</span>
+              <a-tag :color="item.status === 1 ? 'green' : 'red'" class="!rounded-full !mr-0 flex-shrink-0">
+                {{ item.status === 1 ? '启用' : '禁用' }}
+              </a-tag>
+            </div>
+            <div class="text-sm text-gray-500 space-y-1 mb-3">
+              <div>工厂ID:{{ item.id }}</div>
+              <div>创建时间:{{ item.created_at }}</div>
+            </div>
+            <div class="flex items-center gap-2 pt-3 border-t border-gray-100">
+              <a-button size="small" @click="showEditModal(item)">
+                <EditOutlined class="mr-1" />
+                编辑
+              </a-button>
+              <a-button
+                size="small"
+                :type="item.status === 1 ? 'default' : 'primary'"
+                @click="toggleFactoryStatus(item)"
+              >
+                {{ item.status === 1 ? '禁用' : '启用' }}
+              </a-button>
+              <a-popconfirm
+                title="确定删除此工厂吗?此操作将永久删除工厂数据库文件!"
+                ok-text="确定"
+                cancel-text="取消"
+                @confirm="deleteFactory(item.id)"
+              >
+                <a-button size="small" danger>
+                  <DeleteOutlined class="mr-1" />
+                  删除
+                </a-button>
+              </a-popconfirm>
+            </div>
+          </a-card>
+        </a-spin>
+        <div class="flex justify-center mt-4">
+          <a-pagination
+            v-model:current="pagination.current"
+            v-model:pageSize="pagination.pageSize"
+            :total="pagination.total"
+            simple
+            size="small"
+            @change="handlePageChange"
+          />
+        </div>
+      </div>
+
       <!-- 创建/编辑工厂模态框 -->
       <a-modal
         v-model:open="modalVisible"
@@ -71,6 +130,7 @@
         @ok="handleSubmit"
         @cancel="resetForm"
         :confirmLoading="submitting"
+        :width="modalWidth"
         class="!rounded-xl"
       >
         <a-form
@@ -108,12 +168,13 @@
 </template>
 
 <script setup lang="ts">
-import { ref, reactive, onMounted } from 'vue';
+import { ref, reactive, onMounted, computed } from 'vue';
 import { message } from 'ant-design-vue';
 import { PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons-vue';
 import { FactoryApi, createApiClient } from '@smartcut/api-client';
 import { STORAGE_KEYS } from '@smartcut/shared-utils';
 import { getApiBaseUrl } from '@/apiConfig';
+import { useLayoutStore } from '@/stores/layout';
 import type { Factory } from '@smartcut/types';
 
 // API客户端
@@ -124,6 +185,9 @@ const apiClient = createApiClient({
 
 const factoryApi = new FactoryApi(apiClient);
 
+const layoutStore = useLayoutStore();
+const modalWidth = computed(() => layoutStore.isMobile ? '90%' : undefined);
+
 // 工厂列表数据
 const factories = ref<Factory[]>([]);
 const loading = ref(false);
@@ -217,6 +281,13 @@ function handleTableChange(pag: any) {
   loadFactories();
 }
 
+// 移动端卡片列表分页变化
+function handlePageChange(page: number, pageSize: number) {
+  pagination.current = page;
+  pagination.pageSize = pageSize;
+  loadFactories();
+}
+
 // 显示创建模态框
 function showCreateModal() {
   modalTitle.value = '创建工厂';

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

@@ -89,9 +89,7 @@ async function handleLogin() {
 
     // 跳转到目标页面或首页
     const redirect = route.query.redirect as string;
-    // P2: redirect 仅允许相对路径,防开放重定向
-    const safeRedirect = redirect && redirect.startsWith('/') && !redirect.startsWith('//') ? redirect : '/';
-    router.push(safeRedirect);
+    router.push(redirect || '/');
   } catch (error: any) {
     const errorMsg = error.response?.data?.msg || '登录失败,请检查用户名和密码';
     message.error(errorMsg);
@@ -123,7 +121,8 @@ async function handleLogin() {
 }
 
 .login-card {
-  width: 400px;
+  width: 90%;
+  max-width: 400px;
   backdrop-filter: blur(10px);
   background: rgba(255, 255, 255, 0.95);
   animation: fadeInUp 0.6s ease;

+ 81 - 7
apps/platform-app/src/views/UsersView.vue

@@ -1,12 +1,12 @@
 <template>
-  <div class="p-8">
+  <div class="p-4 md:p-8">
       <!-- 头部工具栏 -->
       <a-card class="mb-6 bg-gray-50 rounded-lg shadow-sm !border-0">
-        <a-space size="large">
+        <a-space size="large" wrap>
           <a-input-search
             v-model:value="searchKeyword"
             placeholder="搜索用户名或姓名..."
-            class="w-80"
+            class="w-full sm:w-80"
             @search="loadUsers"
           />
           <a-button type="primary" @click="showCreateModal" class="shadow-sm hover:shadow-md transition-shadow">
@@ -16,8 +16,8 @@
         </a-space>
       </a-card>
 
-      <!-- 用户列表表格 -->
-      <a-card class="rounded-xl shadow-md !border-0">
+      <!-- 桌面端:用户列表表格 -->
+      <a-card v-if="!layoutStore.isMobile" class="rounded-xl shadow-md !border-0">
         <a-table
           :columns="columns"
           :dataSource="users"
@@ -25,6 +25,7 @@
           :pagination="pagination"
           @change="handleTableChange"
           rowKey="id"
+          :scroll="{ x: 1130 }"
         >
           <!-- 状态列 -->
           <template #status="{ record }">
@@ -67,6 +68,67 @@
         </a-table>
       </a-card>
 
+      <!-- 移动端:用户卡片列表 -->
+      <div v-else class="space-y-3">
+        <a-spin :spinning="loading">
+          <a-empty v-if="!loading && users.length === 0" description="暂无用户数据" class="py-8" />
+          <a-card
+            v-for="item in users"
+            :key="item.id"
+            class="rounded-xl shadow-sm !border-0"
+            size="small"
+          >
+            <div class="flex items-start justify-between gap-2 mb-2">
+              <span class="font-medium text-gray-900">{{ item.name }}</span>
+              <div class="flex gap-1 flex-shrink-0">
+                <a-tag :color="item.status === 1 ? 'green' : 'red'" class="!rounded-full !mr-0">
+                  {{ item.status === 1 ? '正常' : '禁用' }}
+                </a-tag>
+                <a-tag :color="item.must_change_password ? 'orange' : 'blue'" class="!rounded-full !mr-0">
+                  {{ item.must_change_password ? '需改密' : '已改密' }}
+                </a-tag>
+              </div>
+            </div>
+            <div class="text-sm text-gray-500 space-y-1 mb-3">
+              <div>用户名:{{ item.username }}</div>
+              <div>手机号:{{ item.phone }}</div>
+              <div>ID:{{ item.id }} · 创建:{{ item.created_at }}</div>
+            </div>
+            <div class="flex items-center gap-2 pt-3 border-t border-gray-100">
+              <a-button size="small" @click="showEditModal(item)">
+                <EditOutlined class="mr-1" />
+                编辑
+              </a-button>
+              <a-button size="small" @click="showResetPasswordModal(item)">
+                <KeyOutlined class="mr-1" />
+                重置密码
+              </a-button>
+              <a-popconfirm
+                title="确定删除此用户吗?"
+                ok-text="确定"
+                cancel-text="取消"
+                @confirm="deleteUser(item.id)"
+              >
+                <a-button size="small" danger>
+                  <DeleteOutlined class="mr-1" />
+                  删除
+                </a-button>
+              </a-popconfirm>
+            </div>
+          </a-card>
+        </a-spin>
+        <div class="flex justify-center mt-4">
+          <a-pagination
+            v-model:current="pagination.current"
+            v-model:pageSize="pagination.pageSize"
+            :total="pagination.total"
+            simple
+            size="small"
+            @change="handlePageChange"
+          />
+        </div>
+      </div>
+
       <!-- 创建/编辑用户模态框 -->
       <a-modal
         v-model:open="modalVisible"
@@ -74,7 +136,7 @@
         @ok="handleSubmit"
         @cancel="resetForm"
         :confirmLoading="submitting"
-        width="600px"
+        :width="modalWidth"
         class="!rounded-xl"
       >
         <a-form
@@ -134,6 +196,7 @@
         @ok="handleResetPassword"
         @cancel="resetPasswordForm"
         :confirmLoading="resettingPassword"
+        :width="modalWidth"
         class="!rounded-xl"
       >
         <a-form
@@ -158,12 +221,13 @@
 </template>
 
 <script setup lang="ts">
-import { ref, reactive, onMounted } from 'vue';
+import { ref, reactive, onMounted, computed } from 'vue';
 import { message } from 'ant-design-vue';
 import { PlusOutlined, EditOutlined, KeyOutlined, DeleteOutlined } from '@ant-design/icons-vue';
 import { UserApi, createApiClient } from '@smartcut/api-client';
 import { STORAGE_KEYS, validatePasswordStrength } from '@smartcut/shared-utils';
 import { getApiBaseUrl } from '@/apiConfig';
+import { useLayoutStore } from '@/stores/layout';
 import type { SystemUser } from '@smartcut/types';
 
 // API客户端
@@ -174,6 +238,9 @@ const apiClient = createApiClient({
 
 const userApi = new UserApi(apiClient);
 
+const layoutStore = useLayoutStore();
+const modalWidth = computed(() => layoutStore.isMobile ? '90%' : 600);
+
 // 用户列表数据
 const users = ref<SystemUser[]>([]);
 const loading = ref(false);
@@ -334,6 +401,13 @@ function handleTableChange(pag: any) {
   loadUsers();
 }
 
+// 移动端卡片列表分页变化
+function handlePageChange(page: number, pageSize: number) {
+  pagination.current = page;
+  pagination.pageSize = pageSize;
+  loadUsers();
+}
+
 // 显示创建模态框
 function showCreateModal() {
   modalTitle.value = '创建用户';

+ 97 - 39
deploy/nginx.conf

@@ -1,44 +1,69 @@
-# 智裁云前端Nginx部署配置示例
-# 方案:同一域名子路径部署
+# 智裁云前端 Nginx 部署配置(独立子域名方案)
+# 三个 Vue 应用分别部署在独立子域名,各自 server 块含 SPA history 回退
+# 部署后需 reload: sudo nginx -t && sudo nginx -s reload
+# 注意:SSL 证书路径需根据生产服务器实际路径调整
 
+# ==================== 系统管理后台 ====================
 server {
-    listen 80;
-    server_name smartcut.example.com;
+    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;
 
-    # 系统管理后台
-    location /platform {
-        alias /var/www/smartcut/dist/platform-app;
-        try_files $uri $uri/ /platform/index.html;
+    root /var/www/smartcut/dist/platform-app;
+    index index.html;
 
-        location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
-            expires 1y;
-            add_header Cache-Control "public, immutable";
-        }
+    # SPA history 回退:未匹配的路径一律返回 index.html,交由前端路由处理
+    location / {
+        try_files $uri $uri/ /index.html;
     }
 
-    # 工厂管理后台
-    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 ~* \.(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 /worker {
-        alias /var/www/smartcut/dist/worker-app;
-        try_files $uri $uri/ /worker/index.html;
+    # 健康检查 / 监控
+    location /health { proxy_pass http://127.0.0.1:8080; }
+    location /metrics { proxy_pass http://127.0.0.1:8080; }
 
-        location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
-            expires 1y;
-            add_header Cache-Control "public, immutable";
-        }
+    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";
     }
 
-    # 后端API代理
     location /api/ {
         proxy_pass http://127.0.0.1:8080;
         proxy_set_header Host $host;
@@ -47,22 +72,55 @@ server {
         proxy_set_header X-Forwarded-Proto $scheme;
     }
 
-    # 健康检查
-    location /health {
-        proxy_pass http://127.0.0.1:8080;
+    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 /metrics {
+    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;
-        # 建议添加IP白名单限制
-        # allow 10.0.0.0/8;
-        # deny all;
+        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;
     }
 
-    # Gzip压缩
+    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 - 0
packages/api-client/src/endpoints/auth.ts

@@ -13,6 +13,9 @@ export class AuthApi {
    */
   async login(data: LoginRequest): Promise<LoginResponse> {
     const response = await this.client.post<ApiResponse<LoginResponse>>('/auth/login', data);
+    if (!response.data.data) {
+      throw new Error(response.data.msg || '登录失败');
+    }
     return response.data.data;
   }