浏览代码

工厂管理后台第一版

Gogs 1 月之前
父节点
当前提交
ff71f06fed

+ 20 - 8
apps/factory-app/src/App.vue

@@ -1,19 +1,18 @@
 <template>
   <a-config-provider :locale="zhCN">
-    <SidebarLayout
+    <BasicLayout
       v-if="showLayout"
       :userName="authStore.user?.name"
-      :currentPath="route.path"
       @logout="handleLogout"
     >
       <router-view />
-    </SidebarLayout>
+    </BasicLayout>
     <router-view v-else />
   </a-config-provider>
 </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';
@@ -21,11 +20,13 @@ import {
   DashboardOutlined,
   FileOutlined,
   AppstoreOutlined,
+  ScissorOutlined,
   UnorderedListOutlined,
+  DollarOutlined,
   PayCircleOutlined,
   UserOutlined
 } from '@ant-design/icons-vue';
-import { SidebarLayout } from '@smartcut/shared-components';
+import BasicLayout from '@/layouts/BasicLayout.vue';
 import { useAuthStore } from '@/stores/auth';
 import { useLayoutStore } from '@/stores/layout';
 
@@ -34,6 +35,10 @@ const route = useRoute();
 const authStore = useAuthStore();
 const layoutStore = useLayoutStore();
 
+const handleResize = () => {
+  layoutStore.setMobile(window.innerWidth < 768);
+};
+
 // 初始化布局配置
 onMounted(() => {
   layoutStore.init({
@@ -41,12 +46,19 @@ onMounted(() => {
       { key: 'dashboard', label: '主页看板', icon: DashboardOutlined, path: '/' },
       { key: 'work-orders', label: '工单管理', icon: FileOutlined, path: '/work-orders' },
       { key: 'processes', label: '工序管理', icon: AppstoreOutlined, path: '/processes' },
+      { key: 'cut-batches', label: '裁床批次', icon: ScissorOutlined, path: '/cut-batches' },
       { key: 'records', label: '计件记录', icon: UnorderedListOutlined, path: '/records' },
+      { key: 'prices', label: '工价设置', icon: DollarOutlined, path: '/prices' },
       { key: 'salary', label: '工资管理', icon: PayCircleOutlined, path: '/salary' },
       { key: 'users', label: '用户管理', icon: UserOutlined, path: '/users' }
-    ],
-    theme: 'dark'
+    ]
   });
+  handleResize();
+  window.addEventListener('resize', handleResize);
+});
+
+onUnmounted(() => {
+  window.removeEventListener('resize', handleResize);
 });
 
 // 根据路由 meta 判断是否显示布局
@@ -70,4 +82,4 @@ async function handleLogout() {
   -webkit-font-smoothing: antialiased;
   -moz-osx-font-smoothing: grayscale;
 }
-</style>
+</style>

+ 27 - 0
apps/factory-app/src/components/Breadcrumb.vue

@@ -0,0 +1,27 @@
+<template>
+  <a-breadcrumb class="breadcrumb-nav">
+    <a-breadcrumb-item v-for="(item, index) in breadcrumbs" :key="index">
+      <span :class="{ 'font-semibold text-gray-800': index === breadcrumbs.length - 1 }">
+        {{ item }}
+      </span>
+    </a-breadcrumb-item>
+  </a-breadcrumb>
+</template>
+
+<script setup lang="ts">
+import { computed } from 'vue';
+import { useRoute } from 'vue-router';
+
+const route = useRoute();
+
+const breadcrumbs = computed(() => {
+  const breadcrumb = route.meta.breadcrumb as string[] | undefined;
+  return breadcrumb || ['首页'];
+});
+</script>
+
+<style scoped>
+.breadcrumb-nav {
+  font-size: 14px;
+}
+</style>

+ 55 - 0
apps/factory-app/src/components/HeaderBar.vue

@@ -0,0 +1,55 @@
+<template>
+  <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-2 md:mr-6">
+        智裁云
+      </h1>
+      <!-- 折叠按钮 -->
+      <a-button
+        type="text"
+        @click="$emit('toggle')"
+        class="flex items-center justify-center hover:bg-gray-100 transition-colors duration-200"
+      >
+        <MenuUnfoldOutlined v-if="collapsed" class="text-lg text-gray-600" />
+        <MenuFoldOutlined v-else class="text-lg text-gray-600" />
+      </a-button>
+    </div>
+
+    <!-- 中间:面包屑导航(移动端隐藏) -->
+    <div class="flex-1 hidden md:flex justify-center">
+      <Breadcrumb />
+    </div>
+
+    <!-- 右侧:通知 + 用户信息 -->
+    <div class="flex items-center space-x-4">
+      <!-- 通知图标 -->
+      <a-badge :count="0" :dot="false">
+        <BellOutlined class="text-xl text-gray-600 cursor-pointer hover:text-blue-500 transition-colors duration-200" />
+      </a-badge>
+
+      <!-- 分隔线 -->
+      <div class="h-6 w-px bg-gray-200"></div>
+
+      <!-- 用户信息 -->
+      <UserMenu :userName="userName" @logout="$emit('logout')" />
+    </div>
+  </header>
+</template>
+
+<script setup lang="ts">
+import { MenuFoldOutlined, MenuUnfoldOutlined, BellOutlined } from '@ant-design/icons-vue';
+import Breadcrumb from './Breadcrumb.vue';
+import UserMenu from './UserMenu.vue';
+
+defineProps<{
+  collapsed: boolean;
+  userName?: string;
+}>();
+
+defineEmits<{
+  toggle: [];
+  logout: [];
+}>();
+</script>

+ 67 - 0
apps/factory-app/src/components/SideMenu.vue

@@ -0,0 +1,67 @@
+<template>
+  <aside
+    class="bg-white border-r border-gray-200 transition-all duration-300 flex-shrink-0"
+    :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-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)
+        }"
+      >
+        <!-- 激活状态左侧高亮条 -->
+        <span v-if="isActive(item.path)" class="absolute left-0 top-0 bottom-0 w-1 bg-blue-500"></span>
+        <!-- 图标 -->
+        <component :is="item.icon" class="text-xl flex-shrink-0" />
+        <!-- 文字(折叠时隐藏) -->
+        <span v-if="!collapsed" class="ml-3 font-medium">{{ item.label }}</span>
+      </router-link>
+    </nav>
+  </aside>
+</template>
+
+<script setup lang="ts">
+import { useRoute } from 'vue-router';
+import type { Component } from 'vue';
+
+interface MenuItem {
+  key: string;
+  label: string;
+  icon: Component;
+  path: string;
+}
+
+defineProps<{
+  menuItems: MenuItem[];
+  collapsed: boolean;
+}>();
+
+const route = useRoute();
+
+function isActive(path: string): boolean {
+  if (path === '/') {
+    return route.path === '/';
+  }
+  return route.path.startsWith(path);
+}
+</script>
+
+<style scoped>
+.menu-item.active {
+  background-color: #eff6ff;
+  color: #2563eb;
+}
+
+.menu-item {
+  text-decoration: none;
+}
+
+.menu-item:hover {
+  text-decoration: none;
+}
+</style>

+ 47 - 0
apps/factory-app/src/components/UserMenu.vue

@@ -0,0 +1,47 @@
+<template>
+  <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-0 sm:mr-2">
+        {{ userName?.charAt(0) || '?' }}
+      </a-avatar>
+      <!-- 用户名和角色(移动端隐藏) -->
+      <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 hidden sm:inline" />
+    </div>
+    <template #overlay>
+      <a-menu class="!rounded-lg !shadow-lg !min-w-[160px]">
+        <a-menu-item key="password" class="!hover:bg-gray-50">
+          <router-link to="/change-password" class="flex items-center">
+            <LockOutlined class="mr-2" />
+            修改密码
+          </router-link>
+        </a-menu-item>
+        <a-menu-divider />
+        <a-menu-item key="logout" @click="handleLogout" class="!hover:bg-red-50 !text-red-600">
+          <LogoutOutlined class="mr-2" />
+          退出登录
+        </a-menu-item>
+      </a-menu>
+    </template>
+  </a-dropdown>
+</template>
+
+<script setup lang="ts">
+import { DownOutlined, LockOutlined, LogoutOutlined } from '@ant-design/icons-vue';
+
+defineProps<{
+  userName?: string;
+}>();
+
+const emit = defineEmits<{
+  logout: [];
+}>();
+
+function handleLogout() {
+  emit('logout');
+}
+</script>

+ 74 - 0
apps/factory-app/src/layouts/BasicLayout.vue

@@ -0,0 +1,74 @@
+<template>
+  <div class="h-screen flex flex-col overflow-hidden">
+    <!-- Header 顶部导航栏(全宽) -->
+    <HeaderBar
+      :collapsed="layoutStore.collapsed"
+      :userName="userName"
+      @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 />
+      </main>
+    </div>
+  </div>
+</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';
+
+defineProps<{
+  userName?: string;
+}>();
+
+defineEmits<{
+  logout: [];
+}>();
+
+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>

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

@@ -14,55 +14,55 @@ const routes = [
     path: '/',
     name: 'Dashboard',
     component: () => import('@/views/DashboardView.vue'),
-    meta: { requiresAuth: true, showLayout: true }
+    meta: { requiresAuth: true, showLayout: true, breadcrumb: ['首页', '主页看板'] }
   },
   {
     path: '/work-orders',
     name: 'WorkOrders',
     component: () => import('@/views/WorkOrdersView.vue'),
-    meta: { requiresAuth: true, showLayout: true }
+    meta: { requiresAuth: true, showLayout: true, breadcrumb: ['首页', '工单管理'] }
   },
   {
     path: '/processes',
     name: 'Processes',
     component: () => import('@/views/ProcessesView.vue'),
-    meta: { requiresAuth: true, showLayout: true }
+    meta: { requiresAuth: true, showLayout: true, breadcrumb: ['首页', '工序管理'] }
   },
   {
     path: '/records',
     name: 'Records',
     component: () => import('@/views/RecordsView.vue'),
-    meta: { requiresAuth: true, showLayout: true }
+    meta: { requiresAuth: true, showLayout: true, breadcrumb: ['首页', '计件记录'] }
   },
   {
     path: '/cut-batches',
     name: 'CutBatches',
     component: () => import('@/views/CutBatchesView.vue'),
-    meta: { requiresAuth: true, showLayout: true }
+    meta: { requiresAuth: true, showLayout: true, breadcrumb: ['首页', '裁床批次'] }
   },
   {
     path: '/prices',
     name: 'Prices',
     component: () => import('@/views/PricesView.vue'),
-    meta: { requiresAuth: true, showLayout: true }
+    meta: { requiresAuth: true, showLayout: true, breadcrumb: ['首页', '工价设置'] }
   },
   {
     path: '/salary',
     name: 'Salary',
     component: () => import('@/views/SalaryView.vue'),
-    meta: { requiresAuth: true, showLayout: true }
+    meta: { requiresAuth: true, showLayout: true, breadcrumb: ['首页', '工资管理'] }
   },
   {
     path: '/users',
     name: 'Users',
     component: () => import('@/views/UsersView.vue'),
-    meta: { requiresAuth: true, showLayout: true }
+    meta: { requiresAuth: true, showLayout: true, breadcrumb: ['首页', '用户管理'] }
   },
   {
     path: '/change-password',
     name: 'ChangePassword',
     component: () => import('@/views/ChangePasswordView.vue'),
-    meta: { requiresAuth: true, showLayout: true }
+    meta: { requiresAuth: true, showLayout: true, breadcrumb: ['首页', '修改密码'] }
   },
   {
     path: '/:pathMatch(.*)*',

+ 18 - 6
apps/factory-app/src/stores/layout.ts

@@ -4,30 +4,42 @@ import type { Component } from 'vue';
 export interface MenuItem {
   key: string;
   label: string;
-  icon: Component; // 图标组件引用
+  icon: Component;
   path: string;
 }
 
 export interface LayoutConfig {
   menuItems: MenuItem[];
-  theme?: 'light' | 'dark';
 }
 
 export const useLayoutStore = defineStore('layout', {
   state: () => ({
     menuItems: [] as MenuItem[],
-    theme: 'dark' as 'light' | 'dark',
-    collapsed: false
+    collapsed: false,
+    isMobile: false,
+    drawerOpen: false
   }),
 
   actions: {
     init(config: LayoutConfig) {
       this.menuItems = config.menuItems;
-      if (config.theme) this.theme = config.theme;
     },
 
     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;
     }
   }
-});
+});

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

@@ -19,4 +19,21 @@
 
 .ant-btn-primary:active {
   background-color: #15304f;
+}
+
+.stat-card {
+  position: relative;
+  overflow: hidden;
+}
+
+.stat-card::before {
+  content: '';
+  position: absolute;
+  top: 0;
+  right: 0;
+  width: 100px;
+  height: 100px;
+  background: radial-gradient(circle, rgba(255, 255, 255, 0.1) 0%, transparent 70%);
+  transform: translate(30%, -30%);
+  pointer-events: none;
 }

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

@@ -1,6 +1,6 @@
 <template>
-  <div>
-    <a-card title="修改密码">
+  <div class="p-4 md:p-8">
+    <a-card class="max-w-2xl mx-auto rounded-xl shadow-md !border-0" title="修改密码">
       <a-form ref="formRef" :model="formState" :rules="formRules" layout="vertical" @finish="handleSubmit">
         <a-form-item label="旧密码" name="old_password">
           <a-input-password v-model:value="formState.old_password" placeholder="请输入旧密码" size="large" />

+ 77 - 15
apps/factory-app/src/views/CutBatchesView.vue

@@ -1,31 +1,86 @@
 <template>
-  <div>
-    <a-card>
-      <a-space>
-        <a-input-search v-model:value="searchKeyword" placeholder="搜索批次号" style="width: 300px" @search="loadBatches" />
-        <a-button type="primary" @click="showCreateModal">创建批次</a-button>
+  <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" wrap>
+        <a-input-search v-model:value="searchKeyword" placeholder="搜索批次号" class="w-full sm:w-80" @search="loadBatches" />
+        <a-button type="primary" @click="showCreateModal" class="shadow-sm hover:shadow-md transition-shadow">
+          <PlusOutlined class="mr-2" />
+          创建批次
+        </a-button>
       </a-space>
     </a-card>
 
-    <a-card class="mt-4" title="裁床批次列表">
+    <!-- 桌面端:裁床批次列表表格 -->
+    <a-card v-if="!layoutStore.isMobile" class="rounded-xl shadow-md !border-0" title="裁床批次列表">
       <a-table :columns="columns" :dataSource="batches" :loading="loading" rowKey="id">
         <template #status="{ record }">
-          <a-tag :color="getStatusColor(record.status)">{{ getStatusText(record.status) }}</a-tag>
+          <a-tag :color="getStatusColor(record.status)" class="!rounded-full">{{ getStatusText(record.status) }}</a-tag>
         </template>
         <template #action="{ record }">
           <a-space>
-            <a-button size="small" @click="showBundlesModal(record)">查看扎号</a-button>
-            <a-button size="small" @click="showEditModal(record)">编辑</a-button>
+            <a-button size="small" @click="showBundlesModal(record)" class="hover:shadow-sm transition-shadow">
+              <EyeOutlined class="mr-1" />
+              查看扎号
+            </a-button>
+            <a-button size="small" @click="showEditModal(record)" class="hover:shadow-sm transition-shadow">
+              <EditOutlined class="mr-1" />
+              编辑
+            </a-button>
             <a-popconfirm title="确定删除此批次吗?" @confirm="deleteBatch(record.id)">
-              <a-button size="small" danger>删除</a-button>
+              <a-button size="small" danger class="hover:shadow-sm transition-shadow">
+                <DeleteOutlined class="mr-1" />
+                删除
+              </a-button>
             </a-popconfirm>
           </a-space>
         </template>
       </a-table>
     </a-card>
 
+    <!-- 移动端:批次卡片列表 -->
+    <div v-else class="space-y-3">
+      <a-spin :spinning="loading">
+        <a-empty v-if="!loading && batches.length === 0" description="暂无批次数据" class="py-8" />
+        <a-card
+          v-for="item in batches"
+          :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.batch_no }}</span>
+            <a-tag :color="getStatusColor(item.status)" class="!rounded-full !mr-0 flex-shrink-0">
+              {{ getStatusText(item.status) }}
+            </a-tag>
+          </div>
+          <div class="text-sm text-gray-500 space-y-1 mb-3">
+            <div>工单号:{{ item.order_no }}</div>
+            <div>颜色:{{ item.color }}</div>
+            <div>数量:{{ item.completed_qty }} / {{ item.total_qty }}</div>
+          </div>
+          <div class="flex items-center gap-2 pt-3 border-t border-gray-100">
+            <a-button size="small" @click="showBundlesModal(item)">
+              <EyeOutlined class="mr-1" />
+              查看扎号
+            </a-button>
+            <a-button size="small" @click="showEditModal(item)">
+              <EditOutlined class="mr-1" />
+              编辑
+            </a-button>
+            <a-popconfirm title="确定删除此批次吗?" @confirm="deleteBatch(item.id)">
+              <a-button size="small" danger>
+                <DeleteOutlined class="mr-1" />
+                删除
+              </a-button>
+            </a-popconfirm>
+          </div>
+        </a-card>
+      </a-spin>
+    </div>
+
     <!-- 创建/编辑批次模态框 -->
-    <a-modal v-model:open="modalVisible" :title="modalTitle" @ok="handleSubmit" :confirmLoading="submitting" width="600px">
+    <a-modal v-model:open="modalVisible" :title="modalTitle" @ok="handleSubmit" :confirmLoading="submitting" :width="modalWidth" class="!rounded-xl">
       <a-form ref="formRef" :model="formState" :rules="formRules" layout="vertical">
         <a-form-item label="工单" name="work_order_id" v-if="!isEdit">
           <a-select v-model:value="formState.work_order_id" placeholder="请选择工单">
@@ -47,7 +102,7 @@
     </a-modal>
 
     <!-- 扎号管理模态框 -->
-    <a-modal v-model:open="bundlesModalVisible" :title="`扎号管理 - ${currentBatch?.batch_no}`" width="900px" :footer="null">
+    <a-modal v-model:open="bundlesModalVisible" :title="`扎号管理 - ${currentBatch?.batch_no}`" :width="bundlesModalWidth" :footer="null" class="!rounded-xl">
       <a-space class="mb-4">
         <a-button type="primary" @click="showAddBundleModal">添加扎号</a-button>
         <a-button @click="generateAllQRCodes">批量生成二维码</a-button>
@@ -65,7 +120,7 @@
     </a-modal>
 
     <!-- 添加扎号模态框 -->
-    <a-modal v-model:open="addBundleModalVisible" title="添加扎号" @ok="handleAddBundle" :confirmLoading="addingBundle">
+    <a-modal v-model:open="addBundleModalVisible" title="添加扎号" @ok="handleAddBundle" :confirmLoading="addingBundle" :width="modalWidth" class="!rounded-xl">
       <a-form layout="vertical">
         <a-form-item label="尺码" required>
           <a-input v-model:value="newBundle.size" placeholder="如:S/M/L/XL" />
@@ -77,7 +132,7 @@
     </a-modal>
 
     <!-- 单个扎号二维码展示 -->
-    <a-modal v-model:open="bundleQRVisible" title="扎号二维码" :footer="null" width="400px">
+    <a-modal v-model:open="bundleQRVisible" title="扎号二维码" :footer="null" :width="qrModalWidth" class="!rounded-xl">
       <div class="text-center">
         <img :src="bundleQRUrl" alt="扎号二维码" style="width: 100%" />
         <p class="mt-4 text-gray-600 text-sm">扎号: {{ currentBundle?.bundle_no }}</p>
@@ -87,11 +142,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, EyeOutlined } from '@ant-design/icons-vue';
 import { WorkOrderApi, createApiClient } from '@smartcut/api-client';
 import { STORAGE_KEYS } from '@smartcut/shared-utils';
 import { getApiBaseUrl } from '@/apiConfig';
+import { useLayoutStore } from '@/stores/layout';
 import type { WorkOrder, CutBatch, CutBundle } from '@smartcut/types';
 
 const apiClient = createApiClient({
@@ -101,6 +158,11 @@ const apiClient = createApiClient({
 
 const workOrderApi = new WorkOrderApi(apiClient);
 
+const layoutStore = useLayoutStore();
+const modalWidth = computed(() => layoutStore.isMobile ? '90%' : 600);
+const bundlesModalWidth = computed(() => layoutStore.isMobile ? '90%' : 900);
+const qrModalWidth = computed(() => layoutStore.isMobile ? '90%' : '400px');
+
 const batches = ref<CutBatch[]>([]);
 const workOrders = ref<WorkOrder[]>([]);
 const bundles = ref<CutBundle[]>([]);

+ 77 - 32
apps/factory-app/src/views/DashboardView.vue

@@ -1,49 +1,93 @@
 <template>
-  <div>
-    <a-row :gutter="16">
-      <a-col :span="6">
-        <a-card>
-          <a-statistic title="总产量" :value="dashboardData.total_output || 0">
-            <template #suffix><span class="text-sm text-gray-500">件</span></template>
-          </a-statistic>
+  <div class="p-4 md:p-8">
+    <!-- 统计卡片区域 -->
+    <a-row :gutter="[16, 16]" class="mb-4 md:mb-8">
+      <!-- 总产量卡片 -->
+      <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>
+              <div class="text-sm opacity-80 mb-2">总产量</div>
+              <div class="text-4xl font-bold">{{ dashboardData.total_output || 0 }}</div>
+              <div class="text-sm mt-2 opacity-70">件</div>
+            </div>
+            <ProfileOutlined class="text-5xl opacity-80" />
+          </div>
         </a-card>
       </a-col>
-      <a-col :span="6">
-        <a-card>
-          <a-statistic
-            title="今日产量"
-            :value="dashboardData.today_output || 0"
-            :valueStyle="{ color: '#3f8600' }"
-          >
-            <template #suffix><span class="text-sm text-gray-500">件</span></template>
-          </a-statistic>
+
+      <!-- 今日产量卡片 -->
+      <a-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>
+              <div class="text-sm opacity-80 mb-2">今日产量</div>
+              <div class="text-4xl font-bold">{{ dashboardData.today_output || 0 }}</div>
+              <div class="text-sm mt-2 opacity-70">件</div>
+            </div>
+            <CheckCircleOutlined class="text-5xl opacity-80" />
+          </div>
         </a-card>
       </a-col>
-      <a-col :span="6">
-        <a-card>
-          <a-statistic title="员工数" :value="dashboardData.worker_count || 0">
-            <template #suffix><span class="text-sm text-gray-500">人</span></template>
-          </a-statistic>
+
+      <!-- 员工数卡片 -->
+      <a-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>
+              <div class="text-sm opacity-80 mb-2">员工数</div>
+              <div class="text-4xl font-bold">{{ dashboardData.worker_count || 0 }}</div>
+              <div class="text-sm mt-2 opacity-70">人</div>
+            </div>
+            <UserOutlined class="text-5xl opacity-80" />
+          </div>
         </a-card>
       </a-col>
-      <a-col :span="6">
-        <a-card>
-          <a-statistic title="活跃工单" :value="dashboardData.active_orders || 0">
-            <template #suffix><span class="text-sm text-gray-500">个</span></template>
-          </a-statistic>
+
+      <!-- 活跃工单卡片 -->
+      <a-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>
+              <div class="text-sm opacity-80 mb-2">活跃工单</div>
+              <div class="text-4xl font-bold">{{ dashboardData.active_orders || 0 }}</div>
+              <div class="text-sm mt-2 opacity-70">个</div>
+            </div>
+            <FileOutlined class="text-5xl opacity-80" />
+          </div>
         </a-card>
       </a-col>
     </a-row>
 
-    <a-card title="快捷操作" class="mt-4">
-      <a-space>
-        <a-button type="primary" @click="router.push('/work-orders')">
+    <!-- 快捷操作卡片 -->
+    <a-card class="rounded-xl shadow-md !border-0" title="快捷操作">
+      <template #extra>
+        <span class="text-gray-400 text-sm">常用功能快速访问</span>
+      </template>
+      <a-space size="large" wrap>
+        <a-button
+          type="primary"
+          size="large"
+          @click="router.push('/work-orders')"
+          class="shadow-md hover:shadow-lg transition-shadow"
+        >
+          <FileOutlined class="mr-2" />
           工单管理
         </a-button>
-        <a-button @click="router.push('/records')">
+        <a-button
+          size="large"
+          @click="router.push('/records')"
+          class="shadow-sm hover:shadow-md transition-shadow"
+        >
+          <UnorderedListOutlined class="mr-2" />
           计件记录
         </a-button>
-        <a-button @click="router.push('/salary')">
+        <a-button
+          size="large"
+          @click="router.push('/salary')"
+          class="shadow-sm hover:shadow-md transition-shadow"
+        >
+          <PayCircleOutlined class="mr-2" />
           工资管理
         </a-button>
       </a-space>
@@ -55,6 +99,7 @@
 import { ref, onMounted } from 'vue';
 import { useRouter } from 'vue-router';
 import { message } from 'ant-design-vue';
+import { ProfileOutlined, CheckCircleOutlined, UserOutlined, FileOutlined, UnorderedListOutlined, PayCircleOutlined } from '@ant-design/icons-vue';
 import { StatsApi, createApiClient } from '@smartcut/api-client';
 import { STORAGE_KEYS } from '@smartcut/shared-utils';
 import { getApiBaseUrl } from '@/apiConfig';
@@ -83,4 +128,4 @@ onMounted(async () => {
     message.error('加载看板数据失败');
   }
 });
-</script>
+</script>

+ 68 - 23
apps/factory-app/src/views/LoginView.vue

@@ -1,19 +1,14 @@
 <template>
   <div class="login-page">
-    <a-card class="login-card" title="智裁云 - 工厂管理后台">
-      <!-- 工厂选择 -->
-      <a-form-item v-if="!factoryId" label="选择工厂">
-        <a-select
-          v-model:value="selectedFactory"
-          placeholder="请选择工厂"
-          size="large"
-          :loading="loadingFactories"
-        >
-          <a-select-option v-for="f in factories" :key="f.id" :value="f.id">
-            {{ f.name }}
-          </a-select-option>
-        </a-select>
-      </a-form-item>
+    <a-card class="login-card rounded-xl shadow-2xl !border-0" :bodyStyle="{ padding: '32px' }">
+      <template #title>
+        <div class="text-center py-4">
+          <h1 class="text-3xl font-bold bg-gradient-to-r from-blue-500 to-blue-700 bg-clip-text text-transparent mb-2">
+            智裁云
+          </h1>
+          <p class="text-gray-500 text-sm">工厂管理后台</p>
+        </div>
+      </template>
 
       <a-form
         :model="formState"
@@ -21,12 +16,28 @@
         @finish="handleLogin"
         layout="vertical"
       >
+        <!-- 工厂选择(无 factory 参数时显示) -->
+        <a-form-item v-if="!factoryId" label="选择工厂" name="factory">
+          <a-select
+            v-model:value="selectedFactory"
+            placeholder="请选择工厂"
+            size="large"
+            :loading="loadingFactories"
+          >
+            <a-select-option v-for="f in factories" :key="f.id" :value="f.id">
+              {{ f.name }}
+            </a-select-option>
+          </a-select>
+        </a-form-item>
+
         <a-form-item label="用户名" name="username">
           <a-input
             v-model:value="formState.username"
             placeholder="请输入用户名"
             size="large"
-          />
+          >
+            <template #prefix><UserOutlined class="text-gray-400" /></template>
+          </a-input>
         </a-form-item>
 
         <a-form-item label="密码" name="password">
@@ -34,10 +45,12 @@
             v-model:value="formState.password"
             placeholder="请输入密码"
             size="large"
-          />
+          >
+            <template #prefix><LockOutlined class="text-gray-400" /></template>
+          </a-input-password>
         </a-form-item>
 
-        <a-form-item>
+        <a-form-item class="mt-6">
           <a-button
             type="primary"
             html-type="submit"
@@ -45,7 +58,9 @@
             block
             :loading="loading"
             :disabled="!factoryId && !selectedFactory"
+            class="shadow-lg hover:shadow-xl transition-all duration-300"
           >
+            <LoginOutlined class="mr-2" />
             登录
           </a-button>
         </a-form-item>
@@ -58,6 +73,7 @@
 import { ref, reactive, onMounted } from 'vue';
 import { useRouter, useRoute } from 'vue-router';
 import { message } from 'ant-design-vue';
+import { UserOutlined, LockOutlined, LoginOutlined } from '@ant-design/icons-vue';
 import { useAuthStore } from '@/stores/auth';
 import { FactoryApi, createApiClient } from '@smartcut/api-client';
 import { STORAGE_KEYS } from '@smartcut/shared-utils';
@@ -136,18 +152,47 @@ async function handleLogin() {
 }
 </script>
 
-<style scoped lang="postcss">
+<style scoped>
 .login-page {
   height: 100vh;
   display: flex;
   justify-content: center;
   align-items: center;
-  background-color: #f0f2f5;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  position: relative;
+}
+
+.login-page::before {
+  content: '';
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><circle cx="50" cy="50" r="40" fill="rgba(255,255,255,0.1)"/></svg>') repeat;
+  opacity: 0.3;
 }
 
 .login-card {
-  width: 400px;
-  border-radius: 8px;
-  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+  width: 90%;
+  max-width: 400px;
+  backdrop-filter: blur(10px);
+  background: rgba(255, 255, 255, 0.95);
+  animation: fadeInUp 0.6s ease;
+}
+
+@keyframes fadeInUp {
+  from {
+    opacity: 0;
+    transform: translateY(30px);
+  }
+  to {
+    opacity: 1;
+    transform: translateY(0);
+  }
+}
+
+.login-card :deep(.ant-card-head) {
+  border-bottom: none;
 }
-</style>
+</style>

+ 60 - 10
apps/factory-app/src/views/PricesView.vue

@@ -1,34 +1,79 @@
 <template>
-  <div>
-    <a-card>
-      <a-space>
-        <a-select v-model:value="filterWorkOrderId" placeholder="筛选工单" style="width: 250px" allowClear @change="loadPrices">
+  <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" wrap>
+        <a-select v-model:value="filterWorkOrderId" placeholder="筛选工单" class="w-full sm:w-64" allowClear @change="loadPrices">
           <a-select-option v-for="wo in workOrders" :key="wo.id" :value="wo.id">
             {{ wo.order_no }} - {{ wo.style_name }}
           </a-select-option>
         </a-select>
-        <a-button type="primary" @click="showCreateModal">设置工价</a-button>
+        <a-button type="primary" @click="showCreateModal" class="shadow-sm hover:shadow-md transition-shadow">
+          <PlusOutlined class="mr-2" />
+          设置工价
+        </a-button>
       </a-space>
     </a-card>
 
-    <a-card class="mt-4" 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">
         <template #price="{ record }">
           ¥{{ record.price.toFixed(2) }}
         </template>
         <template #action="{ record }">
           <a-space>
-            <a-button size="small" @click="showEditModal(record)">编辑</a-button>
+            <a-button size="small" @click="showEditModal(record)" class="hover:shadow-sm transition-shadow">
+              <EditOutlined class="mr-1" />
+              编辑
+            </a-button>
             <a-popconfirm title="删除此工价?" @confirm="deletePrice(record.id)">
-              <a-button size="small" danger>删除</a-button>
+              <a-button size="small" danger class="hover:shadow-sm transition-shadow">
+                <DeleteOutlined class="mr-1" />
+                删除
+              </a-button>
             </a-popconfirm>
           </a-space>
         </template>
       </a-table>
     </a-card>
 
+    <!-- 移动端:工价卡片列表 -->
+    <div v-else class="space-y-3">
+      <a-spin :spinning="loading">
+        <a-empty v-if="!loading && prices.length === 0" description="暂无工价数据" class="py-8" />
+        <a-card
+          v-for="item in prices"
+          :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.style }}</span>
+            <span class="text-blue-600 font-bold flex-shrink-0">¥{{ item.price.toFixed(2) }}</span>
+          </div>
+          <div class="text-sm text-gray-500 space-y-1 mb-3">
+            <div>工序:{{ item.process_name }}</div>
+            <div v-if="item.remark">备注:{{ item.remark }}</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-popconfirm title="删除此工价?" @confirm="deletePrice(item.id)">
+              <a-button size="small" danger>
+                <DeleteOutlined class="mr-1" />
+                删除
+              </a-button>
+            </a-popconfirm>
+          </div>
+        </a-card>
+      </a-spin>
+    </div>
+
     <!-- 创建/编辑工价模态框 -->
-    <a-modal v-model:open="modalVisible" :title="modalTitle" @ok="handleSubmit" :confirmLoading="submitting">
+    <a-modal v-model:open="modalVisible" :title="modalTitle" @ok="handleSubmit" :confirmLoading="submitting" :width="modalWidth" class="!rounded-xl">
       <a-form ref="formRef" :model="formState" :rules="formRules" layout="vertical">
         <a-form-item label="款号" name="style" v-if="!isEdit">
           <a-input v-model:value="formState.style" placeholder="请输入款号" />
@@ -52,11 +97,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 { WorkOrderApi, ProcessApi, createApiClient } from '@smartcut/api-client';
 import { STORAGE_KEYS } from '@smartcut/shared-utils';
 import { getApiBaseUrl } from '@/apiConfig';
+import { useLayoutStore } from '@/stores/layout';
 import type { WorkOrder, Process, PriceTemplate } from '@smartcut/types';
 
 const apiClient = createApiClient({
@@ -67,6 +114,9 @@ const apiClient = createApiClient({
 const workOrderApi = new WorkOrderApi(apiClient);
 const processApi = new ProcessApi(apiClient);
 
+const layoutStore = useLayoutStore();
+const modalWidth = computed(() => layoutStore.isMobile ? '90%' : 600);
+
 const prices = ref<PriceTemplate[]>([]);
 const workOrders = ref<WorkOrder[]>([]);
 const processes = ref<Process[]>([]);

+ 65 - 10
apps/factory-app/src/views/ProcessesView.vue

@@ -1,12 +1,17 @@
 <template>
-  <div>
-    <a-card>
-      <a-button type="primary" @click="showCreateModal">
-        创建工序
-      </a-button>
+  <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" wrap>
+        <a-button type="primary" @click="showCreateModal" class="shadow-sm hover:shadow-md transition-shadow">
+          <PlusOutlined class="mr-2" />
+          创建工序
+        </a-button>
+      </a-space>
     </a-card>
 
-    <a-card class="mt-4">
+    <!-- 桌面端:工序列表表格 -->
+    <a-card v-if="!layoutStore.isMobile" class="rounded-xl shadow-md !border-0">
       <a-table
         :columns="columns"
         :dataSource="processes"
@@ -14,14 +19,15 @@
         rowKey="id"
       >
         <template #status="{ record }">
-          <a-tag :color="record.status === 1 ? 'green' : 'red'">
+          <a-tag :color="record.status === 1 ? 'green' : 'red'" class="!rounded-full">
             {{ record.status === 1 ? '启用' : '禁用' }}
           </a-tag>
         </template>
 
         <template #action="{ record }">
           <a-space>
-            <a-button size="small" @click="showEditModal(record)">
+            <a-button size="small" @click="showEditModal(record)" class="hover:shadow-sm transition-shadow">
+              <EditOutlined class="mr-1" />
               编辑
             </a-button>
             <a-popconfirm
@@ -30,7 +36,8 @@
               cancel-text="取消"
               @confirm="deleteProcess(record.id)"
             >
-              <a-button size="small" danger>
+              <a-button size="small" danger class="hover:shadow-sm transition-shadow">
+                <DeleteOutlined class="mr-1" />
                 删除
               </a-button>
             </a-popconfirm>
@@ -39,6 +46,47 @@
       </a-table>
     </a-card>
 
+    <!-- 移动端:工序卡片列表 -->
+    <div v-else class="space-y-3">
+      <a-spin :spinning="loading">
+        <a-empty v-if="!loading && processes.length === 0" description="暂无工序数据" class="py-8" />
+        <a-card
+          v-for="item in processes"
+          :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>工序代码:{{ item.code }}</div>
+            <div>排序:{{ item.sort_order }}</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-popconfirm
+              title="确定删除此工序吗?"
+              ok-text="确定"
+              cancel-text="取消"
+              @confirm="deleteProcess(item.id)"
+            >
+              <a-button size="small" danger>
+                <DeleteOutlined class="mr-1" />
+                删除
+              </a-button>
+            </a-popconfirm>
+          </div>
+        </a-card>
+      </a-spin>
+    </div>
+
     <!-- 创建/编辑工序模态框 -->
     <a-modal
       v-model:open="modalVisible"
@@ -46,6 +94,8 @@
       @ok="handleSubmit"
       @cancel="resetForm"
       :confirmLoading="submitting"
+      :width="modalWidth"
+      class="!rounded-xl"
     >
       <a-form
         ref="formRef"
@@ -89,11 +139,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 { ProcessApi, createApiClient } from '@smartcut/api-client';
 import { STORAGE_KEYS } from '@smartcut/shared-utils';
 import { getApiBaseUrl } from '@/apiConfig';
+import { useLayoutStore } from '@/stores/layout';
 import type { Process } from '@smartcut/types';
 
 const apiClient = createApiClient({
@@ -103,6 +155,9 @@ const apiClient = createApiClient({
 
 const processApi = new ProcessApi(apiClient);
 
+const layoutStore = useLayoutStore();
+const modalWidth = computed(() => layoutStore.isMobile ? '90%' : undefined);
+
 const processes = ref<Process[]>([]);
 const loading = ref(false);
 

+ 72 - 9
apps/factory-app/src/views/RecordsView.vue

@@ -1,11 +1,12 @@
 <template>
-  <div>
-    <a-card>
-      <a-space>
+  <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" wrap>
         <a-input-search
           v-model:value="searchKeyword"
           placeholder="搜索员工姓名"
-          style="width: 300px"
+          class="w-full sm:w-80"
           @search="loadRecords"
         />
         <a-date-picker
@@ -13,13 +14,15 @@
           placeholder="筛选日期"
           @change="loadRecords"
         />
-        <a-button type="primary" @click="showScanModal">
+        <a-button type="primary" @click="showScanModal" class="shadow-sm hover:shadow-md transition-shadow">
+          <ScanOutlined class="mr-2" />
           扫码计件
         </a-button>
       </a-space>
     </a-card>
 
-    <a-card class="mt-4">
+    <!-- 桌面端:计件记录表格 -->
+    <a-card v-if="!layoutStore.isMobile" class="rounded-xl shadow-md !border-0">
       <a-table
         :columns="columns"
         :dataSource="records"
@@ -29,7 +32,7 @@
         rowKey="id"
       >
         <template #status="{ record }">
-          <a-tag :color="record.status === 'normal' ? 'green' : 'red'">
+          <a-tag :color="record.status === 'normal' ? 'green' : 'red'" class="!rounded-full">
             {{ record.status === 'normal' ? '正常' : '已退回' }}
           </a-tag>
         </template>
@@ -44,6 +47,7 @@
               size="small"
               :type="record.status === 'normal' ? 'default' : 'primary'"
               @click="toggleRecordStatus(record)"
+              class="hover:shadow-sm transition-shadow"
             >
               {{ record.status === 'normal' ? '退回' : '撤销退回' }}
             </a-button>
@@ -52,6 +56,52 @@
       </a-table>
     </a-card>
 
+    <!-- 移动端:记录卡片列表 -->
+    <div v-else class="space-y-3">
+      <a-spin :spinning="loading">
+        <a-empty v-if="!loading && records.length === 0" description="暂无计件记录" class="py-8" />
+        <a-card
+          v-for="item in records"
+          :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.user_name }}</span>
+            <a-tag :color="item.status === 'normal' ? 'green' : 'red'" class="!rounded-full !mr-0 flex-shrink-0">
+              {{ item.status === 'normal' ? '正常' : '已退回' }}
+            </a-tag>
+          </div>
+          <div class="text-sm text-gray-500 space-y-1 mb-3">
+            <div>工单号:{{ item.order_no }}</div>
+            <div>工序:{{ item.process_name }}</div>
+            <div>数量:{{ item.quantity }}</div>
+            <div>金额:¥{{ item.amount.toFixed(2) }}</div>
+            <div>日期:{{ item.record_date }}</div>
+          </div>
+          <div class="flex items-center gap-2 pt-3 border-t border-gray-100">
+            <a-button
+              size="small"
+              :type="item.status === 'normal' ? 'default' : 'primary'"
+              @click="toggleRecordStatus(item)"
+            >
+              {{ item.status === 'normal' ? '退回' : '撤销退回' }}
+            </a-button>
+          </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="scanModalVisible"
@@ -59,7 +109,8 @@
       @ok="handleScanSubmit"
       @cancel="resetScanForm"
       :confirmLoading="scanning"
-      width="500px"
+      :width="modalWidth"
+      class="!rounded-xl"
     >
       <a-form
         ref="scanFormRef"
@@ -100,11 +151,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 { ScanOutlined } from '@ant-design/icons-vue';
 import { RecordApi, ProcessApi, createApiClient } from '@smartcut/api-client';
 import { STORAGE_KEYS, formatDate } from '@smartcut/shared-utils';
 import { getApiBaseUrl } from '@/apiConfig';
+import { useLayoutStore } from '@/stores/layout';
 import type { PieceRecord, Process } from '@smartcut/types';
 import dayjs from 'dayjs';
 
@@ -116,6 +169,9 @@ const apiClient = createApiClient({
 const recordApi = new RecordApi(apiClient);
 const processApi = new ProcessApi(apiClient);
 
+const layoutStore = useLayoutStore();
+const modalWidth = computed(() => layoutStore.isMobile ? '90%' : 500);
+
 const records = ref<PieceRecord[]>([]);
 const processes = ref<Process[]>([]);
 const loading = ref(false);
@@ -191,6 +247,13 @@ function handleTableChange(pag: any) {
   loadRecords();
 }
 
+// 移动端卡片列表分页变化
+function handlePageChange(page: number, pageSize: number) {
+  pagination.current = page;
+  pagination.pageSize = pageSize;
+  loadRecords();
+}
+
 function showScanModal() {
   scanModalVisible.value = true;
   resetScanForm();

+ 86 - 11
apps/factory-app/src/views/SalaryView.vue

@@ -1,15 +1,21 @@
 <template>
-  <div>
-    <a-card>
-      <a-space>
-        <a-button type="primary" @click="showGenerateModal">
+  <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" wrap>
+        <a-button type="primary" @click="showGenerateModal" class="shadow-sm hover:shadow-md transition-shadow">
+          <PlusOutlined class="mr-2" />
           生成工资
         </a-button>
-        <a-button @click="loadSalaryList">刷新</a-button>
+        <a-button @click="loadSalaryList" class="shadow-sm hover:shadow-md transition-shadow">
+          <ReloadOutlined class="mr-2" />
+          刷新
+        </a-button>
       </a-space>
     </a-card>
 
-    <a-card class="mt-4" title="工资周期列表">
+    <!-- 桌面端:工资周期列表表格 -->
+    <a-card v-if="!layoutStore.isMobile" class="rounded-xl shadow-md !border-0" title="工资周期列表">
       <a-table
         :columns="columns"
         :dataSource="salaryPeriods"
@@ -17,14 +23,15 @@
         rowKey="period"
       >
         <template #status="{ record }">
-          <a-tag :color="record.status === 'locked' ? 'red' : 'green'">
+          <a-tag :color="record.status === 'locked' ? 'red' : 'green'" class="!rounded-full">
             {{ record.status === 'locked' ? '已锁定' : '待锁定' }}
           </a-tag>
         </template>
 
         <template #action="{ record }">
           <a-space>
-            <a-button size="small" @click="showDetailModal(record.period)">
+            <a-button size="small" @click="showDetailModal(record.period)" class="hover:shadow-sm transition-shadow">
+              <EyeOutlined class="mr-1" />
               查看明细
             </a-button>
             <a-button
@@ -32,33 +39,94 @@
               size="small"
               type="primary"
               @click="lockSalary(record.period)"
+              class="hover:shadow-sm transition-shadow"
             >
+              <LockOutlined class="mr-1" />
               锁定
             </a-button>
             <a-button
               v-else
               size="small"
               @click="unlockSalary(record.period)"
+              class="hover:shadow-sm transition-shadow"
             >
+              <UnlockOutlined class="mr-1" />
               解锁
             </a-button>
             <a-popconfirm
               title="确定删除此工资周期吗?"
               @confirm="deleteSalary(record.period)"
             >
-              <a-button size="small" danger>删除</a-button>
+              <a-button size="small" danger class="hover:shadow-sm transition-shadow">
+                <DeleteOutlined class="mr-1" />
+                删除
+              </a-button>
             </a-popconfirm>
           </a-space>
         </template>
       </a-table>
     </a-card>
 
+    <!-- 移动端:工资周期卡片列表 -->
+    <div v-else class="space-y-3">
+      <a-spin :spinning="loading">
+        <a-empty v-if="!loading && salaryPeriods.length === 0" description="暂无工资周期" class="py-8" />
+        <a-card
+          v-for="item in salaryPeriods"
+          :key="item.period"
+          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.period }}</span>
+            <a-tag :color="item.status === 'locked' ? 'red' : 'green'" class="!rounded-full !mr-0 flex-shrink-0">
+              {{ item.status === 'locked' ? '已锁定' : '待锁定' }}
+            </a-tag>
+          </div>
+          <div class="text-sm text-gray-500 space-y-1 mb-3">
+            <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="showDetailModal(item.period)">
+              <EyeOutlined class="mr-1" />
+              明细
+            </a-button>
+            <a-button
+              v-if="item.status === 'pending'"
+              size="small"
+              type="primary"
+              @click="lockSalary(item.period)"
+            >
+              <LockOutlined class="mr-1" />
+              锁定
+            </a-button>
+            <a-button
+              v-else
+              size="small"
+              @click="unlockSalary(item.period)"
+            >
+              <UnlockOutlined class="mr-1" />
+              解锁
+            </a-button>
+            <a-popconfirm title="确定删除此工资周期吗?" @confirm="deleteSalary(item.period)">
+              <a-button size="small" danger>
+                <DeleteOutlined class="mr-1" />
+                删除
+              </a-button>
+            </a-popconfirm>
+          </div>
+        </a-card>
+      </a-spin>
+    </div>
+
     <!-- 生成工资模态框 -->
     <a-modal
       v-model:open="generateModalVisible"
       title="生成工资"
       @ok="handleGenerate"
       :confirmLoading="generating"
+      :width="modalWidth"
+      class="!rounded-xl"
     >
       <a-form layout="vertical">
         <a-form-item label="工资周期" required>
@@ -75,8 +143,9 @@
     <a-modal
       v-model:open="detailModalVisible"
       :title="`工资明细 - ${currentPeriod}`"
-      width="800px"
+      :width="detailModalWidth"
       :footer="null"
+      class="!rounded-xl"
     >
       <a-table
         :columns="detailColumns"
@@ -94,12 +163,14 @@
 </template>
 
 <script setup lang="ts">
-import { ref, onMounted } from 'vue';
+import { ref, onMounted, computed } from 'vue';
 import { message } from 'ant-design-vue';
+import { PlusOutlined, ReloadOutlined, EyeOutlined, LockOutlined, UnlockOutlined, DeleteOutlined } from '@ant-design/icons-vue';
 import dayjs from 'dayjs';
 import { SalaryApi, createApiClient } from '@smartcut/api-client';
 import { STORAGE_KEYS } from '@smartcut/shared-utils';
 import { getApiBaseUrl } from '@/apiConfig';
+import { useLayoutStore } from '@/stores/layout';
 import type { SalaryPeriod, SalaryDetailItem } from '@smartcut/types';
 
 const apiClient = createApiClient({
@@ -109,6 +180,10 @@ const apiClient = createApiClient({
 
 const salaryApi = new SalaryApi(apiClient);
 
+const layoutStore = useLayoutStore();
+const modalWidth = computed(() => layoutStore.isMobile ? '90%' : undefined);
+const detailModalWidth = computed(() => layoutStore.isMobile ? '90%' : 800);
+
 const salaryPeriods = ref<SalaryPeriod[]>([]);
 const loading = ref(false);
 

+ 99 - 13
apps/factory-app/src/views/UsersView.vue

@@ -1,18 +1,23 @@
 <template>
-  <div>
-    <a-card>
-      <a-space>
+  <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" wrap>
         <a-input-search
           v-model:value="searchKeyword"
           placeholder="搜索用户名或姓名"
-          style="width: 300px"
+          class="w-full sm:w-80"
           @search="loadUsers"
         />
-        <a-button type="primary" @click="showCreateModal">创建用户</a-button>
+        <a-button type="primary" @click="showCreateModal" class="shadow-sm hover:shadow-md transition-shadow">
+          <PlusOutlined class="mr-2" />
+          创建用户
+        </a-button>
       </a-space>
     </a-card>
 
-    <a-card class="mt-4">
+    <!-- 桌面端:用户列表表格 -->
+    <a-card v-if="!layoutStore.isMobile" class="rounded-xl shadow-md !border-0">
       <a-table
         :columns="columns"
         :dataSource="users"
@@ -22,27 +27,93 @@
         rowKey="id"
       >
         <template #role="{ record }">
-          <a-tag :color="record.role === 'factory_admin' ? 'blue' : 'default'">
+          <a-tag :color="record.role === 'factory_admin' ? 'blue' : 'default'" class="!rounded-full">
             {{ record.role === 'factory_admin' ? '工厂管理员' : '工人' }}
           </a-tag>
         </template>
         <template #status="{ record }">
-          <a-tag :color="record.status === 1 ? 'green' : 'red'">
+          <a-tag :color="record.status === 1 ? 'green' : 'red'" class="!rounded-full">
             {{ record.status === 1 ? '正常' : '禁用' }}
           </a-tag>
         </template>
         <template #action="{ record }">
           <a-space>
-            <a-button size="small" @click="showEditModal(record)">编辑</a-button>
-            <a-button size="small" @click="showResetPasswordModal(record)">重置密码</a-button>
+            <a-button size="small" @click="showEditModal(record)" class="hover:shadow-sm transition-shadow">
+              <EditOutlined class="mr-1" />
+              编辑
+            </a-button>
+            <a-button size="small" @click="showResetPasswordModal(record)" class="hover:shadow-sm transition-shadow">
+              <KeyOutlined class="mr-1" />
+              重置密码
+            </a-button>
             <a-popconfirm title="确定删除此用户吗?" @confirm="deleteUser(record.id)">
-              <a-button size="small" danger>删除</a-button>
+              <a-button size="small" danger class="hover:shadow-sm transition-shadow">
+                <DeleteOutlined class="mr-1" />
+                删除
+              </a-button>
             </a-popconfirm>
           </a-space>
         </template>
       </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">
+            <div>
+              <span class="font-medium text-gray-900 break-all">{{ item.name }}</span>
+              <span class="text-xs text-gray-400 ml-2">{{ item.username }}</span>
+            </div>
+            <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-2">
+            <div>手机号:{{ item.phone }}</div>
+          </div>
+          <div class="mb-3">
+            <a-tag :color="item.role === 'factory_admin' ? 'blue' : 'default'" class="!rounded-full">
+              {{ item.role === 'factory_admin' ? '工厂管理员' : '工人' }}
+            </a-tag>
+          </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="确定删除此用户吗?" @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"
@@ -50,7 +121,8 @@
       @ok="handleSubmit"
       @cancel="resetForm"
       :confirmLoading="submitting"
-      width="600px"
+      :width="modalWidth"
+      class="!rounded-xl"
     >
       <a-form ref="formRef" :model="formState" :rules="formRules" layout="vertical">
         <a-form-item label="用户名" name="username" v-if="!isEdit">
@@ -86,6 +158,8 @@
       title="重置密码"
       @ok="handleResetPassword"
       :confirmLoading="resetting"
+      :width="modalWidth"
+      class="!rounded-xl"
     >
       <a-form ref="resetFormRef" :model="resetState" :rules="resetRules" layout="vertical">
         <a-form-item label="新密码" name="password">
@@ -97,11 +171,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, KeyOutlined } 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 { User } from '@smartcut/types';
 
 const apiClient = createApiClient({
@@ -111,6 +187,9 @@ const apiClient = createApiClient({
 
 const userApi = new UserApi(apiClient);
 
+const layoutStore = useLayoutStore();
+const modalWidth = computed(() => layoutStore.isMobile ? '90%' : 600);
+
 const users = ref<User[]>([]);
 const loading = ref(false);
 const searchKeyword = ref('');
@@ -209,6 +288,13 @@ function handleTableChange(pag: any) {
   loadUsers();
 }
 
+// 移动端卡片列表分页变化
+function handlePageChange(page: number, pageSize: number) {
+  pagination.current = page;
+  pagination.pageSize = pageSize;
+  loadUsers();
+}
+
 function showCreateModal() {
   modalTitle.value = '创建用户';
   isEdit.value = false;

+ 97 - 14
apps/factory-app/src/views/WorkOrdersView.vue

@@ -1,17 +1,18 @@
 <template>
-  <div>
-    <a-card>
-      <a-space>
+  <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" wrap>
         <a-input-search
           v-model:value="searchKeyword"
           placeholder="搜索款号或工单号"
-          style="width: 300px"
+          class="w-full sm:w-80"
           @search="loadWorkOrders"
         />
         <a-select
           v-model:value="filterStatus"
           placeholder="状态筛选"
-          style="width: 150px"
+          class="w-full sm:w-40"
           allowClear
           @change="loadWorkOrders"
         >
@@ -20,13 +21,15 @@
           <a-select-option value="completed">已完成</a-select-option>
           <a-select-option value="cancelled">已取消</a-select-option>
         </a-select>
-        <a-button type="primary" @click="showCreateModal">
+        <a-button type="primary" @click="showCreateModal" class="shadow-sm hover:shadow-md transition-shadow">
+          <PlusOutlined class="mr-2" />
           创建工单
         </a-button>
       </a-space>
     </a-card>
 
-    <a-card class="mt-4">
+    <!-- 桌面端:工单列表表格 -->
+    <a-card v-if="!layoutStore.isMobile" class="rounded-xl shadow-md !border-0">
       <a-table
         :columns="columns"
         :dataSource="workOrders"
@@ -36,7 +39,7 @@
         rowKey="id"
       >
         <template #status="{ record }">
-          <a-tag :color="getStatusColor(record.status)">
+          <a-tag :color="getStatusColor(record.status)" class="!rounded-full">
             {{ getStatusText(record.status) }}
           </a-tag>
         </template>
@@ -50,10 +53,12 @@
 
         <template #action="{ record }">
           <a-space>
-            <a-button size="small" @click="showEditModal(record)">
+            <a-button size="small" @click="showEditModal(record)" class="hover:shadow-sm transition-shadow">
+              <EditOutlined class="mr-1" />
               编辑
             </a-button>
-            <a-button size="small" type="default" @click="generateQRCode(record.id)">
+            <a-button size="small" type="default" @click="generateQRCode(record.id)" class="hover:shadow-sm transition-shadow">
+              <QrcodeOutlined class="mr-1" />
               生成二维码
             </a-button>
             <a-popconfirm
@@ -62,7 +67,8 @@
               cancel-text="取消"
               @confirm="deleteWorkOrder(record.id)"
             >
-              <a-button size="small" danger>
+              <a-button size="small" danger class="hover:shadow-sm transition-shadow">
+                <DeleteOutlined class="mr-1" />
                 删除
               </a-button>
             </a-popconfirm>
@@ -71,6 +77,68 @@
       </a-table>
     </a-card>
 
+    <!-- 移动端:工单卡片列表 -->
+    <div v-else class="space-y-3">
+      <a-spin :spinning="loading">
+        <a-empty v-if="!loading && workOrders.length === 0" description="暂无工单数据" class="py-8" />
+        <a-card
+          v-for="item in workOrders"
+          :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.order_no }}</span>
+            <a-tag :color="getStatusColor(item.status)" class="!rounded-full !mr-0 flex-shrink-0">
+              {{ getStatusText(item.status) }}
+            </a-tag>
+          </div>
+          <div class="text-sm text-gray-500 space-y-1 mb-2">
+            <div>款号:{{ item.style }}</div>
+            <div>款号名称:{{ item.style_name }}</div>
+            <div>数量:{{ item.completed_qty }} / {{ item.total_qty }}</div>
+          </div>
+          <a-progress
+            :percent="Math.round((item.completed_qty / item.total_qty) * 100)"
+            :status="item.status === 'completed' ? 'success' : 'active'"
+            size="small"
+            class="mb-3"
+          />
+          <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="generateQRCode(item.id)">
+              <QrcodeOutlined class="mr-1" />
+              二维码
+            </a-button>
+            <a-popconfirm
+              title="确定删除此工单吗?"
+              ok-text="确定"
+              cancel-text="取消"
+              @confirm="deleteWorkOrder(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"
@@ -78,7 +146,8 @@
       @ok="handleSubmit"
       @cancel="resetForm"
       :confirmLoading="submitting"
-      width="600px"
+      :width="modalWidth"
+      class="!rounded-xl"
     >
       <a-form
         ref="formRef"
@@ -133,7 +202,8 @@
       title="工单二维码"
       @cancel="qrCodeModalVisible = false"
       :footer="null"
-      width="400px"
+      :width="qrModalWidth"
+      class="!rounded-xl"
     >
       <div class="text-center">
         <img :src="qrCodeUrl" alt="工单二维码" style="width: 100%" />
@@ -144,11 +214,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, QrcodeOutlined } from '@ant-design/icons-vue';
 import { WorkOrderApi, createApiClient } from '@smartcut/api-client';
 import { STORAGE_KEYS } from '@smartcut/shared-utils';
 import { getApiBaseUrl } from '@/apiConfig';
+import { useLayoutStore } from '@/stores/layout';
 import type { WorkOrder } from '@smartcut/types';
 
 const apiClient = createApiClient({
@@ -158,6 +230,10 @@ const apiClient = createApiClient({
 
 const workOrderApi = new WorkOrderApi(apiClient);
 
+const layoutStore = useLayoutStore();
+const modalWidth = computed(() => layoutStore.isMobile ? '90%' : 600);
+const qrModalWidth = computed(() => layoutStore.isMobile ? '90%' : '400px');
+
 const workOrders = ref<WorkOrder[]>([]);
 const loading = ref(false);
 const searchKeyword = ref('');
@@ -255,6 +331,13 @@ function handleTableChange(pag: any) {
   loadWorkOrders();
 }
 
+// 移动端卡片列表分页变化
+function handlePageChange(page: number, pageSize: number) {
+  pagination.current = page;
+  pagination.pageSize = pageSize;
+  loadWorkOrders();
+}
+
 function showCreateModal() {
   modalTitle.value = '创建工单';
   isEdit.value = false;

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

@@ -38,6 +38,7 @@ export interface CutBatch {
   total_qty: number;
   completed_qty: number;
   status: string;
+  order_no?: string; // 关联工单号(JOIN 查询时返回)
   created_at: string;
   updated_at: string;
 }
@@ -62,6 +63,7 @@ export interface PriceTemplate {
   process_id: number;
   price: number;
   remark: string;
+  process_name?: string; // 关联工序名(JOIN 查询时返回)
   created_at: string;
   updated_at: string;
 }

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

@@ -12,6 +12,10 @@ export interface PieceRecord {
   status: 'normal' | 'reverted';
   batch_id?: number;
   bundle_id?: number;
+  // 关联字段(后端 JOIN 查询时返回)
+  user_name?: string;
+  order_no?: string;
+  process_name?: string;
   created_at: string;
   updated_at: string;
 }