Эх сурвалжийг харах

feat: 实现P0优先级核心页面

- 新增仓库管理页面(WarehouseView)
- 新增库存变动记录页面(InventoryLogView)
- 新增物流商配置页面(LogisticsProviderView)
- 新增运费模板页面(ShippingTemplateView)
- 新增收款管理页面(PaymentView)
- 新增备货计划页面(ReplenishmentPlanView)
- 新增工单管理页面(TicketView)
- 更新路由和菜单配置
- 添加finance用户角色类型
docker 2 сар өмнө
parent
commit
5882822ecc

+ 31 - 0
src/router/menu.ts

@@ -58,9 +58,40 @@ export const menuGroups: MenuGroup[] = [
     title: '库存与履约',
     items: [
       { key: 'inventory-overview', title: '库存总览', path: '/inventory/overview', roles: ['admin', 'manager', 'operator', 'procurement', 'warehouse'] },
+      { key: 'inventory-logs', title: '库存变动记录', path: '/inventory/logs', roles: ['admin', 'manager', 'operator', 'warehouse'] },
+      { key: 'warehouse-manage', title: '仓库管理', path: '/warehouse/manage', roles: ['admin', 'manager'] },
       { key: 'shipping-work', title: '发货作业', path: '/inventory/shipping', roles: ['admin', 'manager', 'warehouse'] }
     ]
   },
+  {
+    key: 'logistics',
+    title: '物流中心',
+    items: [
+      { key: 'logistics-providers', title: '物流商配置', path: '/logistics/providers', roles: ['admin', 'manager', 'operator'] },
+      { key: 'shipping-templates', title: '运费模板', path: '/logistics/templates', roles: ['admin', 'manager', 'operator'] }
+    ]
+  },
+  {
+    key: 'finance',
+    title: '财务中心',
+    items: [
+      { key: 'finance-payments', title: '收款管理', path: '/finance/payments', roles: ['admin', 'manager', 'finance'] }
+    ]
+  },
+  {
+    key: 'procurement',
+    title: '采购中心',
+    items: [
+      { key: 'replenishment-plan', title: '备货计划', path: '/procurement/replenishment', roles: ['admin', 'manager', 'procurement'] }
+    ]
+  },
+  {
+    key: 'crm',
+    title: '客服中心',
+    items: [
+      { key: 'crm-tickets', title: '工单管理', path: '/crm/tickets', roles: ['admin', 'manager', 'customer_service'] }
+    ]
+  },
   {
     key: 'system',
     title: '系统与权限',

+ 42 - 0
src/router/routes.ts

@@ -107,6 +107,48 @@ export const routes: RouteRecordRaw[] = [
         component: () => import('@/views/inventory/ShippingWorkView.vue'),
         meta: { title: '发货作业', pageKey: 'shipping-work', roles: ['admin', 'manager', 'warehouse'] }
       },
+      {
+        path: '/inventory/logs',
+        name: 'inventory-logs',
+        component: () => import('@/views/warehouse/InventoryLogView.vue'),
+        meta: { title: '库存变动记录', pageKey: 'inventory-logs', roles: ['admin', 'manager', 'operator', 'warehouse'] }
+      },
+      {
+        path: '/warehouse/manage',
+        name: 'warehouse-manage',
+        component: () => import('@/views/warehouse/WarehouseView.vue'),
+        meta: { title: '仓库管理', pageKey: 'warehouse-manage', roles: ['admin', 'manager'] }
+      },
+      {
+        path: '/logistics/providers',
+        name: 'logistics-providers',
+        component: () => import('@/views/logistics/LogisticsProviderView.vue'),
+        meta: { title: '物流商配置', pageKey: 'logistics-providers', roles: ['admin', 'manager', 'operator'] }
+      },
+      {
+        path: '/logistics/templates',
+        name: 'shipping-templates',
+        component: () => import('@/views/logistics/ShippingTemplateView.vue'),
+        meta: { title: '运费模板', pageKey: 'shipping-templates', roles: ['admin', 'manager', 'operator'] }
+      },
+      {
+        path: '/finance/payments',
+        name: 'finance-payments',
+        component: () => import('@/views/finance/PaymentView.vue'),
+        meta: { title: '收款管理', pageKey: 'finance-payments', roles: ['admin', 'manager', 'finance'] }
+      },
+      {
+        path: '/procurement/replenishment',
+        name: 'replenishment-plan',
+        component: () => import('@/views/procurement/ReplenishmentPlanView.vue'),
+        meta: { title: '备货计划', pageKey: 'replenishment-plan', roles: ['admin', 'manager', 'procurement'] }
+      },
+      {
+        path: '/crm/tickets',
+        name: 'crm-tickets',
+        component: () => import('@/views/crm/TicketView.vue'),
+        meta: { title: '工单管理', pageKey: 'crm-tickets', roles: ['admin', 'manager', 'customer_service'] }
+      },
       {
         path: '/system/roles',
         name: 'role-permission',

+ 1 - 1
src/types/page.ts

@@ -393,7 +393,7 @@ export interface ApiKeyItem {
 }
 
 /* ───── User ───── */
-export type UserRole = 'admin' | 'customer_service' | 'manager' | 'operator' | 'procurement' | 'warehouse';
+export type UserRole = 'admin' | 'customer_service' | 'finance' | 'manager' | 'operator' | 'procurement' | 'warehouse';
 
 export interface UserProfile {
   avatar: string;

+ 387 - 0
src/views/crm/TicketView.vue

@@ -0,0 +1,387 @@
+<template>
+  <div class="app-page">
+    <section class="glass-card section-card">
+      <el-form :model="filters" inline class="filter-form">
+        <el-form-item label="工单号">
+          <el-input v-model="filters.ticketNo" placeholder="工单号" clearable style="width:170px" @keyup.enter="loadData" />
+        </el-form-item>
+        <el-form-item label="工单类型">
+          <el-select v-model="filters.type" placeholder="全部" clearable style="width:130px">
+            <el-option label="咨询" value="咨询" />
+            <el-option label="投诉" value="投诉" />
+            <el-option label="售后" value="售后" />
+            <el-option label="其他" value="其他" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="优先级">
+          <el-select v-model="filters.priority" placeholder="全部" clearable style="width:120px">
+            <el-option label="紧急" value="紧急" />
+            <el-option label="高" value="高" />
+            <el-option label="中" value="中" />
+            <el-option label="低" value="低" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="状态">
+          <el-select v-model="filters.status" placeholder="全部" clearable style="width:130px">
+            <el-option label="待处理" value="待处理" />
+            <el-option label="处理中" value="处理中" />
+            <el-option label="待确认" value="待确认" />
+            <el-option label="已完结" value="已完结" />
+          </el-select>
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" @click="loadData">查询</el-button>
+          <el-button @click="resetFilters">重置</el-button>
+        </el-form-item>
+      </el-form>
+    </section>
+
+    <section class="glass-card section-card" style="padding:12px 24px">
+      <div class="table-toolbar" style="margin-bottom:0">
+        <div class="chip-list">
+          <el-button type="primary" @click="openCreate">新建工单</el-button>
+        </div>
+        <el-button @click="loadData">刷新</el-button>
+      </div>
+    </section>
+
+    <section class="glass-card section-card">
+      <el-table :data="filteredItems" stripe style="width:100%" v-loading="loading">
+        <el-table-column prop="ticketNo" label="工单号" width="170" />
+        <el-table-column prop="title" label="标题" min-width="200" show-overflow-tooltip />
+        <el-table-column prop="type" label="类型" width="80">
+          <template #default="{ row }">
+            <el-tag :type="typeTag(row.type)" size="small">{{ row.type }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column prop="priority" label="优先级" width="80">
+          <template #default="{ row }">
+            <el-tag :type="priorityTag(row.priority)" size="small">{{ row.priority }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column prop="status" label="状态" width="90">
+          <template #default="{ row }">
+            <el-tag :type="statusTag(row.status)" size="small">{{ row.status }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column prop="creator" label="创建人" width="100" />
+        <el-table-column prop="handler" label="处理人" width="100" />
+        <el-table-column prop="relatedOrder" label="关联订单" width="140">
+          <template #default="{ row }">
+            <el-button v-if="row.relatedOrder" link type="primary">{{ row.relatedOrder }}</el-button>
+            <span v-else>-</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="createdAt" label="创建时间" width="160" />
+        <el-table-column prop="updateTime" label="更新时间" width="160" />
+        <el-table-column label="操作" width="160" fixed="right">
+          <template #default="{ row }">
+            <el-button link type="primary" @click="openDetail(row)">处理</el-button>
+            <el-button link type="primary" @click="assignHandler(row)" v-if="row.status === '待处理'">分配</el-button>
+          </template>
+        </el-table-column>
+        <template #empty>
+          <el-empty description="暂无数据" />
+        </template>
+      </el-table>
+    </section>
+
+    <el-dialog v-model="createVisible" title="新建工单" width="560px" destroy-on-close>
+      <el-form :model="createForm" label-width="100px">
+        <el-form-item label="工单类型" required>
+          <el-select v-model="createForm.type" placeholder="选择类型" style="width:100%">
+            <el-option label="咨询" value="咨询" />
+            <el-option label="投诉" value="投诉" />
+            <el-option label="售后" value="售后" />
+            <el-option label="其他" value="其他" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="优先级" required>
+          <el-select v-model="createForm.priority" placeholder="选择优先级" style="width:100%">
+            <el-option label="紧急" value="紧急" />
+            <el-option label="高" value="高" />
+            <el-option label="中" value="中" />
+            <el-option label="低" value="低" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="工单标题" required>
+          <el-input v-model="createForm.title" placeholder="请输入工单标题" />
+        </el-form-item>
+        <el-form-item label="关联订单">
+          <el-input v-model="createForm.relatedOrder" placeholder="关联订单号(可选)" />
+        </el-form-item>
+        <el-form-item label="问题描述" required>
+          <el-input v-model="createForm.content" type="textarea" :rows="4" placeholder="请详细描述问题" />
+        </el-form-item>
+        <el-form-item label="附件">
+          <el-upload action="#" :auto-upload="false" :limit="3">
+            <el-button size="small">上传附件</el-button>
+          </el-upload>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="createVisible = false">取消</el-button>
+        <el-button type="primary" @click="confirmCreate">提交工单</el-button>
+      </template>
+    </el-dialog>
+
+    <el-drawer v-model="detailVisible" title="工单处理" size="600px">
+      <template v-if="detailItem">
+        <el-descriptions :column="2" border style="margin-bottom:16px">
+          <el-descriptions-item label="工单号">{{ detailItem.ticketNo }}</el-descriptions-item>
+          <el-descriptions-item label="状态">
+            <el-tag :type="statusTag(detailItem.status)">{{ detailItem.status }}</el-tag>
+          </el-descriptions-item>
+          <el-descriptions-item label="类型">
+            <el-tag :type="typeTag(detailItem.type)">{{ detailItem.type }}</el-tag>
+          </el-descriptions-item>
+          <el-descriptions-item label="优先级">
+            <el-tag :type="priorityTag(detailItem.priority)">{{ detailItem.priority }}</el-tag>
+          </el-descriptions-item>
+          <el-descriptions-item label="创建人">{{ detailItem.creator }}</el-descriptions-item>
+          <el-descriptions-item label="处理人">{{ detailItem.handler }}</el-descriptions-item>
+          <el-descriptions-item label="关联订单" :span="2">
+            <el-button v-if="detailItem.relatedOrder" link type="primary">{{ detailItem.relatedOrder }}</el-button>
+            <span v-else>-</span>
+          </el-descriptions-item>
+          <el-descriptions-item label="创建时间" :span="2">{{ detailItem.createdAt }}</el-descriptions-item>
+        </el-descriptions>
+
+        <h4 style="margin:0 0 12px">问题描述</h4>
+        <div class="glass-card" style="padding:12px;margin-bottom:16px">
+          <p style="margin:0">{{ detailItem.content }}</p>
+        </div>
+
+        <h4 style="margin:0 0 12px">处理记录</h4>
+        <el-timeline>
+          <el-timeline-item v-for="(log, idx) in detailItem.logs" :key="idx" :timestamp="log.time" :type="log.type">
+            <strong>{{ log.operator }}</strong>
+            <div style="color:var(--cb-text-soft);margin-top:4px">{{ log.action }}</div>
+          </el-timeline-item>
+        </el-timeline>
+
+        <div v-if="detailItem.status !== '已完结'" style="margin-top:16px">
+          <h4 style="margin:0 0 12px">处理操作</h4>
+          <el-form label-position="top">
+            <el-form-item label="回复内容">
+              <el-input v-model="replyContent" type="textarea" :rows="3" placeholder="输入回复内容" />
+            </el-form-item>
+            <el-form-item>
+              <div class="chip-list">
+                <el-button type="primary" @click="submitReply">提交回复</el-button>
+                <el-button v-if="detailItem.status === '处理中'" type="success" @click="requestConfirm">请求确认</el-button>
+                <el-button v-if="detailItem.status === '待确认'" type="success" @click="closeTicket">完结工单</el-button>
+              </div>
+            </el-form-item>
+          </el-form>
+        </div>
+      </template>
+    </el-drawer>
+
+    <el-dialog v-model="assignVisible" title="分配处理人" width="400px">
+      <el-form label-position="top">
+        <el-form-item label="选择处理人">
+          <el-select v-model="assignHandlerId" placeholder="选择处理人" style="width:100%">
+            <el-option label="客服组 / 张丽" value="张丽" />
+            <el-option label="客服组 / 李梅" value="李梅" />
+            <el-option label="运营组 / 王磊" value="王磊" />
+            <el-option label="运营组 / 陈欣" value="陈欣" />
+          </el-select>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="assignVisible = false">取消</el-button>
+        <el-button type="primary" @click="confirmAssign">确认分配</el-button>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { computed, onMounted, reactive, ref } from 'vue';
+import { ElMessage, ElMessageBox } from 'element-plus';
+
+interface TicketItem {
+  ticketNo: string;
+  title: string;
+  type: string;
+  priority: string;
+  status: string;
+  creator: string;
+  handler: string;
+  relatedOrder: string;
+  content: string;
+  createdAt: string;
+  updateTime: string;
+  logs: { time: string; operator: string; action: string; type: string }[];
+}
+
+const items = ref<TicketItem[]>([
+  { ticketNo: 'TK-20260420-001', title: '订单发货延迟咨询', type: '咨询', priority: '中', status: '待处理', creator: 'Olivia Zhang', handler: '-', relatedOrder: 'OMS-20260419-0012', content: '您好,我有一笔订单已经等待发货3天了,请问是什么原因导致的延迟?订单号:OMS-20260419-0012', createdAt: '2026-04-20 09:30:00', updateTime: '2026-04-20 09:30:00', logs: [] },
+  { ticketNo: 'TK-20260420-002', title: '商品破损投诉', type: '投诉', priority: '高', status: '处理中', creator: 'Noah Smith', handler: '张丽', relatedOrder: 'OMS-20260418-0008', content: '收到的行李箱角部有凹陷,包装也有破损,请求退货退款处理', createdAt: '2026-04-20 08:15:00', updateTime: '2026-04-20 10:20:00', logs: [{ time: '2026-04-20 10:20:00', operator: '张丽', action: '已联系物流公司核实,等待回复', type: 'primary' }] },
+  { ticketNo: 'TK-20260419-003', title: '退换货申请', type: '售后', priority: '紧急', status: '待确认', creator: 'Emma Wilson', handler: '李梅', relatedOrder: 'OMS-20260418-0015', content: '背包拉链损坏,无法正常使用,申请换货', createdAt: '2026-04-19 16:45:00', updateTime: '2026-04-20 11:30:00', logs: [{ time: '2026-04-19 17:00:00', operator: '李梅', action: '已审核通过,等待用户寄回商品', type: 'success' }, { time: '2026-04-20 11:30:00', operator: '系统', action: '用户已填写退货物流单号', type: 'info' }] },
+  { ticketNo: 'TK-20260419-004', title: '发票开具咨询', type: '咨询', priority: '低', status: '已完结', creator: 'Liam Chen', handler: '张丽', relatedOrder: 'OMS-20260419-0022', content: '请问如何申请开具增值税发票?', createdAt: '2026-04-19 14:20:00', updateTime: '2026-04-19 15:00:00', logs: [{ time: '2026-04-19 14:30:00', operator: '张丽', action: '已发送发票申请指引', type: 'primary' }, { time: '2026-04-19 15:00:00', operator: '系统', action: '用户确认问题已解决,工单完结', type: 'success' }] },
+  { ticketNo: 'TK-20260418-005', title: '账户异常问题', type: '其他', priority: '中', status: '待处理', creator: 'Sophie Brown', handler: '-', relatedOrder: '', content: '无法登录账户,提示密码错误但我已经确认密码正确', createdAt: '2026-04-18 11:30:00', updateTime: '2026-04-18 11:30:00', logs: [] }
+]);
+
+const loading = ref(false);
+const createVisible = ref(false);
+const detailVisible = ref(false);
+const assignVisible = ref(false);
+const detailItem = ref<TicketItem | null>(null);
+const assignHandlerId = ref('');
+const replyContent = ref('');
+
+const filters = ref({ ticketNo: '', type: '', priority: '', status: '' });
+
+const createForm = reactive({
+  type: '',
+  priority: '',
+  title: '',
+  relatedOrder: '',
+  content: ''
+});
+
+const filteredItems = computed(() => {
+  return items.value.filter(item => {
+    if (filters.value.ticketNo && !item.ticketNo.includes(filters.value.ticketNo)) return false;
+    if (filters.value.type && item.type !== filters.value.type) return false;
+    if (filters.value.priority && item.priority !== filters.value.priority) return false;
+    if (filters.value.status && item.status !== filters.value.status) return false;
+    return true;
+  });
+});
+
+const typeTag = (type: string) => {
+  const map: Record<string, string> = { '咨询': '', '投诉': 'danger', '售后': 'warning', '其他': 'info' };
+  return map[type] || '';
+};
+
+const priorityTag = (priority: string) => {
+  const map: Record<string, string> = { '紧急': 'danger', '高': 'warning', '中': '', '低': 'info' };
+  return map[priority] || '';
+};
+
+const statusTag = (status: string) => {
+  const map: Record<string, string> = { '待处理': 'warning', '处理中': 'primary', '待确认': '', '已完结': 'success' };
+  return map[status] || '';
+};
+
+const loadData = () => {
+  loading.value = true;
+  setTimeout(() => { loading.value = false; }, 300);
+};
+
+const resetFilters = () => {
+  filters.value = { ticketNo: '', type: '', priority: '', status: '' };
+};
+
+const openCreate = () => {
+  Object.assign(createForm, { type: '', priority: '', title: '', relatedOrder: '', content: '' });
+  createVisible.value = true;
+};
+
+const confirmCreate = async () => {
+  if (!createForm.type || !createForm.priority || !createForm.title || !createForm.content) {
+    ElMessage.warning('请填写必填项');
+    return;
+  }
+  const newTicket: TicketItem = {
+    ticketNo: `TK-${new Date().toISOString().slice(0, 10).replace(/-/g, '')}-${String(items.value.length + 1).padStart(3, '0')}`,
+    ...createForm,
+    status: '待处理',
+    creator: '当前用户',
+    handler: '-',
+    createdAt: new Date().toLocaleString(),
+    updateTime: new Date().toLocaleString(),
+    logs: []
+  };
+  items.value.unshift(newTicket);
+  createVisible.value = false;
+  ElMessage.success('工单已创建');
+};
+
+const openDetail = (row: TicketItem) => {
+  detailItem.value = row;
+  replyContent.value = '';
+  detailVisible.value = true;
+};
+
+const assignHandler = (row: TicketItem) => {
+  detailItem.value = row;
+  assignHandlerId.value = '';
+  assignVisible.value = true;
+};
+
+const confirmAssign = async () => {
+  if (!assignHandlerId.value) {
+    ElMessage.warning('请选择处理人');
+    return;
+  }
+  const idx = items.value.findIndex(t => t.ticketNo === detailItem.value!.ticketNo);
+  if (idx !== -1) {
+    items.value[idx].handler = assignHandlerId.value;
+    items.value[idx].status = '处理中';
+    items.value[idx].updateTime = new Date().toLocaleString();
+  }
+  assignVisible.value = false;
+  ElMessage.success(`已分配给 ${assignHandlerId.value}`);
+};
+
+const submitReply = async () => {
+  if (!replyContent.value) {
+    ElMessage.warning('请输入回复内容');
+    return;
+  }
+  const idx = items.value.findIndex(t => t.ticketNo === detailItem.value!.ticketNo);
+  if (idx !== -1) {
+    items.value[idx].logs.push({
+      time: new Date().toLocaleString(),
+      operator: '当前用户',
+      action: replyContent.value,
+      type: 'primary'
+    });
+    items.value[idx].updateTime = new Date().toLocaleString();
+    detailItem.value = items.value[idx];
+  }
+  replyContent.value = '';
+  ElMessage.success('回复已提交');
+};
+
+const requestConfirm = async () => {
+  const idx = items.value.findIndex(t => t.ticketNo === detailItem.value!.ticketNo);
+  if (idx !== -1) {
+    items.value[idx].status = '待确认';
+    items.value[idx].logs.push({
+      time: new Date().toLocaleString(),
+      operator: '系统',
+      action: '已提交确认请求,等待用户确认',
+      type: 'info'
+    });
+    detailItem.value = items.value[idx];
+  }
+  ElMessage.success('已提交确认请求');
+};
+
+const closeTicket = async () => {
+  await ElMessageBox.confirm('确认完结此工单?', '完结确认');
+  const idx = items.value.findIndex(t => t.ticketNo === detailItem.value!.ticketNo);
+  if (idx !== -1) {
+    items.value[idx].status = '已完结';
+    items.value[idx].logs.push({
+      time: new Date().toLocaleString(),
+      operator: '系统',
+      action: '工单已完结',
+      type: 'success'
+    });
+    detailItem.value = items.value[idx];
+  }
+  ElMessage.success('工单已完结');
+};
+
+onMounted(loadData);
+</script>
+
+<style scoped>
+.filter-form :deep(.el-form-item) { margin-bottom: 0; }
+</style>

+ 222 - 0
src/views/finance/PaymentView.vue

@@ -0,0 +1,222 @@
+<template>
+  <div class="app-page">
+    <section class="glass-card section-card">
+      <el-form :model="filters" inline class="filter-form">
+        <el-form-item label="收款单号">
+          <el-input v-model="filters.paymentNo" placeholder="收款单号" clearable style="width:170px" @keyup.enter="loadData" />
+        </el-form-item>
+        <el-form-item label="渠道订单号">
+          <el-input v-model="filters.channelOrderNo" placeholder="渠道订单号" clearable style="width:180px" />
+        </el-form-item>
+        <el-form-item label="渠道">
+          <el-select v-model="filters.channel" placeholder="全部渠道" clearable style="width:140px">
+            <el-option label="Shopify" value="Shopify" />
+            <el-option label="TikTok Shop" value="TikTok Shop" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="对账状态">
+          <el-select v-model="filters.reconcileStatus" placeholder="全部" clearable style="width:130px">
+            <el-option label="待对账" value="待对账" />
+            <el-option label="已确认" value="已确认" />
+            <el-option label="有差异" value="有差异" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="收款时间">
+          <el-date-picker v-model="filters.dateRange" type="daterange" range-separator="至" start-placeholder="开始" end-placeholder="结束" style="width:240px" />
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" @click="loadData">查询</el-button>
+          <el-button @click="resetFilters">重置</el-button>
+        </el-form-item>
+      </el-form>
+    </section>
+
+    <section class="glass-card section-card" style="padding:12px 24px">
+      <div class="table-toolbar" style="margin-bottom:0">
+        <div class="chip-list">
+          <el-button type="primary" @click="openReconcile">对账确认</el-button>
+          <el-button @click="doExport">导出</el-button>
+        </div>
+        <el-select v-model="summaryView" placeholder="汇总视图" clearable style="width:180px">
+          <el-option label="按渠道汇总" value="channel" />
+          <el-option label="按店铺汇总" value="shop" />
+          <el-option label="按时间汇总" value="time" />
+        </el-select>
+      </div>
+    </section>
+
+    <section class="glass-card section-card">
+      <el-table :data="filteredItems" stripe style="width:100%" v-loading="loading" @selection-change="onSelection">
+        <el-table-column type="selection" width="45" />
+        <el-table-column prop="paymentNo" label="收款单号" width="170" />
+        <el-table-column prop="channelOrderNo" label="渠道订单号" width="170" />
+        <el-table-column prop="channel" label="渠道" width="120" />
+        <el-table-column prop="shopName" label="店铺" width="140" />
+        <el-table-column prop="currency" label="币种" width="70" />
+        <el-table-column prop="amount" label="收款金额" width="110">
+          <template #default="{ row }">
+            <span style="font-weight:600">{{ row.currency }} {{ row.amount }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="payMethod" label="支付方式" width="100" />
+        <el-table-column prop="payTime" label="收款时间" width="160" />
+        <el-table-column prop="reconcileStatus" label="对账状态" width="100">
+          <template #default="{ row }">
+            <el-tag :type="reconcileTag(row.reconcileStatus)" size="small">{{ row.reconcileStatus }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column prop="remark" label="备注" min-width="120" show-overflow-tooltip />
+        <el-table-column label="操作" width="100" fixed="right">
+          <template #default="{ row }">
+            <el-button link type="primary" @click="openDetail(row)">详情</el-button>
+          </template>
+        </el-table-column>
+        <template #empty>
+          <el-empty description="暂无数据" />
+        </template>
+      </el-table>
+    </section>
+
+    <section class="glass-card section-card" style="padding:16px">
+      <h4 style="margin:0 0 12px">收款汇总</h4>
+      <div class="stat-grid" style="grid-template-columns:repeat(4, 1fr)">
+        <article class="stat-card">
+          <div class="stat-card__label">今日收款</div>
+          <div class="stat-card__value" style="font-size:24px;color:var(--cb-primary)">$12,450.00</div>
+        </article>
+        <article class="stat-card">
+          <div class="stat-card__label">本周收款</div>
+          <div class="stat-card__value" style="font-size:24px">$86,230.00</div>
+        </article>
+        <article class="stat-card">
+          <div class="stat-card__label">本月收款</div>
+          <div class="stat-card__value" style="font-size:24px">$328,500.00</div>
+        </article>
+        <article class="stat-card">
+          <div class="stat-card__label">待对账</div>
+          <div class="stat-card__value" style="font-size:24px;color:var(--cb-accent)">$15,680.00</div>
+        </article>
+      </div>
+    </section>
+
+    <el-dialog v-model="detailVisible" title="收款详情" width="560px">
+      <el-descriptions :column="1" border v-if="detailItem">
+        <el-descriptions-item label="收款单号">{{ detailItem.paymentNo }}</el-descriptions-item>
+        <el-descriptions-item label="渠道订单号">{{ detailItem.channelOrderNo }}</el-descriptions-item>
+        <el-descriptions-item label="渠道">{{ detailItem.channel }}</el-descriptions-item>
+        <el-descriptions-item label="店铺">{{ detailItem.shopName }}</el-descriptions-item>
+        <el-descriptions-item label="币种">{{ detailItem.currency }}</el-descriptions-item>
+        <el-descriptions-item label="收款金额">{{ detailItem.currency }} {{ detailItem.amount }}</el-descriptions-item>
+        <el-descriptions-item label="支付方式">{{ detailItem.payMethod }}</el-descriptions-item>
+        <el-descriptions-item label="收款时间">{{ detailItem.payTime }}</el-descriptions-item>
+        <el-descriptions-item label="对账状态">
+          <el-tag :type="reconcileTag(detailItem.reconcileStatus)">{{ detailItem.reconcileStatus }}</el-tag>
+        </el-descriptions-item>
+        <el-descriptions-item label="交易流水号">{{ detailItem.transactionNo }}</el-descriptions-item>
+        <el-descriptions-item label="手续费">{{ detailItem.currency }} {{ detailItem.fee }}</el-descriptions-item>
+        <el-descriptions-item label="备注">{{ detailItem.remark || '-' }}</el-descriptions-item>
+      </el-descriptions>
+      <template #footer>
+        <el-button @click="detailVisible = false">关闭</el-button>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { computed, onMounted, ref } from 'vue';
+import { ElMessage, ElMessageBox } from 'element-plus';
+
+interface PaymentItem {
+  id: string;
+  paymentNo: string;
+  channelOrderNo: string;
+  channel: string;
+  shopName: string;
+  currency: string;
+  amount: string;
+  payMethod: string;
+  payTime: string;
+  reconcileStatus: string;
+  transactionNo: string;
+  fee: string;
+  remark: string;
+}
+
+const items = ref<PaymentItem[]>([
+  { id: 'P001', paymentNo: 'PAY-20260420-001', channelOrderNo: 'SHOP-US-20260420-0123', channel: 'Shopify', shopName: 'NomadPeak US', currency: 'USD', amount: '128.00', payMethod: 'Credit Card', payTime: '2026-04-20 10:30:15', reconcileStatus: '待对账', transactionNo: 'txn_3N4K8M9L2', fee: '3.84', remark: '' },
+  { id: 'P002', paymentNo: 'PAY-20260420-002', channelOrderNo: 'SHOP-JP-20260420-0456', channel: 'Shopify', shopName: 'UrbanTrail JP', currency: 'JPY', amount: '8900', payMethod: 'PayPay', payTime: '2026-04-20 09:45:22', reconcileStatus: '已确认', transactionNo: 'txn_4P5Q9R8S3', fee: '267', remark: '' },
+  { id: 'P003', paymentNo: 'PAY-20260419-003', channelOrderNo: 'TT-UK-20260419-0789', channel: 'TikTok Shop', shopName: 'AeroDry UK', currency: 'GBP', amount: '65.00', payMethod: 'TikTok Pay', payTime: '2026-04-19 21:09:33', reconcileStatus: '已确认', transactionNo: 'txn_5T6U7V9W4', fee: '1.95', remark: '' },
+  { id: 'P004', paymentNo: 'PAY-20260419-004', channelOrderNo: 'SHOP-US-20260419-0321', channel: 'Shopify', shopName: 'NomadPeak US', currency: 'USD', amount: '259.00', payMethod: 'PayPal', payTime: '2026-04-19 18:20:45', reconcileStatus: '已确认', transactionNo: 'txn_6X7Y8Z9A5', fee: '7.77', remark: '' },
+  { id: 'P005', paymentNo: 'PAY-20260419-005', channelOrderNo: 'TT-UK-20260419-0654', channel: 'TikTok Shop', shopName: 'AeroDry UK', currency: 'GBP', amount: '156.00', payMethod: 'Credit Card', payTime: '2026-04-19 16:35:08', reconcileStatus: '有差异', transactionNo: 'txn_7B8C9D0E6', fee: '4.68', remark: '金额差异 $2.00' },
+  { id: 'P006', paymentNo: 'PAY-20260418-006', channelOrderNo: 'SHOP-JP-20260418-0987', channel: 'Shopify', shopName: 'UrbanTrail JP', currency: 'JPY', amount: '12500', payMethod: 'Credit Card', payTime: '2026-04-18 14:12:30', reconcileStatus: '已确认', transactionNo: 'txn_8F9G0H1I7', fee: '375', remark: '' }
+]);
+
+const loading = ref(false);
+const selected = ref<PaymentItem[]>([]);
+const summaryView = ref('');
+const detailVisible = ref(false);
+const detailItem = ref<PaymentItem | null>(null);
+
+const filters = ref({
+  paymentNo: '',
+  channelOrderNo: '',
+  channel: '',
+  reconcileStatus: '',
+  dateRange: [] as string[]
+});
+
+const filteredItems = computed(() => {
+  return items.value.filter(item => {
+    if (filters.value.paymentNo && !item.paymentNo.includes(filters.value.paymentNo)) return false;
+    if (filters.value.channelOrderNo && !item.channelOrderNo.includes(filters.value.channelOrderNo)) return false;
+    if (filters.value.channel && item.channel !== filters.value.channel) return false;
+    if (filters.value.reconcileStatus && item.reconcileStatus !== filters.value.reconcileStatus) return false;
+    return true;
+  });
+});
+
+const reconcileTag = (status: string) => {
+  const map: Record<string, string> = { '待对账': 'warning', '已确认': 'success', '有差异': 'danger' };
+  return map[status] || '';
+};
+
+const loadData = () => {
+  loading.value = true;
+  setTimeout(() => { loading.value = false; }, 300);
+};
+
+const resetFilters = () => {
+  filters.value = { paymentNo: '', channelOrderNo: '', channel: '', reconcileStatus: '', dateRange: [] };
+};
+
+const onSelection = (rows: PaymentItem[]) => { selected.value = rows; };
+
+const openDetail = (row: PaymentItem) => {
+  detailItem.value = row;
+  detailVisible.value = true;
+};
+
+const openReconcile = async () => {
+  if (!selected.value.length) {
+    ElMessage.warning('请先选择要对账的收款记录');
+    return;
+  }
+  await ElMessageBox.confirm(`确认对选中的 ${selected.value.length} 条记录进行对账确认?`, '对账确认');
+  for (const item of selected.value) {
+    const idx = items.value.findIndex(p => p.id === item.id);
+    if (idx !== -1) items.value[idx].reconcileStatus = '已确认';
+  }
+  ElMessage.success('对账已完成');
+};
+
+const doExport = () => {
+  ElMessage.info('导出开始,完成后将自动下载');
+};
+
+onMounted(loadData);
+</script>
+
+<style scoped>
+.filter-form :deep(.el-form-item) { margin-bottom: 0; }
+</style>

+ 368 - 0
src/views/logistics/LogisticsProviderView.vue

@@ -0,0 +1,368 @@
+<template>
+  <div class="app-page">
+    <section class="glass-card section-card">
+      <el-form :model="filters" inline class="filter-form">
+        <el-form-item label="物流商名称">
+          <el-input v-model="filters.name" placeholder="物流商名称" clearable style="width:160px" @keyup.enter="loadData" />
+        </el-form-item>
+        <el-form-item label="承运渠道">
+          <el-select v-model="filters.channel" placeholder="全部" clearable style="width:140px">
+            <el-option label="DHL" value="DHL" />
+            <el-option label="FedEx" value="FedEx" />
+            <el-option label="UPS" value="UPS" />
+            <el-option label="顺丰" value="顺丰" />
+            <el-option label="云途" value="云途" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="状态">
+          <el-select v-model="filters.status" placeholder="全部" clearable style="width:120px">
+            <el-option label="启用" value="启用" />
+            <el-option label="停用" value="停用" />
+          </el-select>
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" @click="loadData">查询</el-button>
+          <el-button @click="resetFilters">重置</el-button>
+        </el-form-item>
+      </el-form>
+    </section>
+
+    <section class="glass-card section-card" style="padding:12px 24px">
+      <div class="table-toolbar" style="margin-bottom:0">
+        <div class="chip-list">
+          <el-button type="primary" @click="openDialog()">新建物流商</el-button>
+        </div>
+        <el-button @click="loadData">刷新</el-button>
+      </div>
+    </section>
+
+    <section class="glass-card section-card">
+      <el-table :data="filteredItems" stripe style="width:100%" v-loading="loading">
+        <el-table-column prop="name" label="物流商名称" min-width="140" />
+        <el-table-column prop="code" label="代码" width="100" />
+        <el-table-column prop="channels" label="承运渠道" min-width="180">
+          <template #default="{ row }">
+            <el-tag v-for="c in row.channels" :key="c" size="small" style="margin-right:4px">{{ c }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column prop="settlementType" label="结算方式" width="100" />
+        <el-table-column prop="avgDays" label="平均时效(天)" width="110" />
+        <el-table-column prop="trackingUrl" label="追踪URL" min-width="200" show-overflow-tooltip />
+        <el-table-column prop="status" label="状态" width="80">
+          <template #default="{ row }">
+            <el-tag :type="row.status === '启用' ? 'success' : 'danger'" size="small">{{ row.status }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column prop="updatedAt" label="更新时间" width="160" />
+        <el-table-column label="操作" width="200" fixed="right">
+          <template #default="{ row }">
+            <el-button link type="primary" @click="openDialog(row)">编辑</el-button>
+            <el-button link type="primary" @click="openTemplate(row)">运费模板</el-button>
+            <el-button link :type="row.status === '启用' ? 'danger' : 'primary'" @click="toggleStatus(row)">{{ row.status === '启用' ? '停用' : '启用' }}</el-button>
+          </template>
+        </el-table-column>
+        <template #empty>
+          <el-empty description="暂无数据" />
+        </template>
+      </el-table>
+    </section>
+
+    <el-dialog v-model="dialogVisible" :title="editingItem ? '编辑物流商' : '新建物流商'" width="560px" destroy-on-close>
+      <el-form :model="formData" label-width="110px">
+        <el-form-item label="物流商名称" required>
+          <el-input v-model="formData.name" placeholder="请输入物流商名称" />
+        </el-form-item>
+        <el-form-item label="物流商代码" required>
+          <el-input v-model="formData.code" placeholder="如 DHL/FedEx" style="width:100%" />
+        </el-form-item>
+        <el-form-item label="承运渠道" required>
+          <el-checkbox-group v-model="formData.channels">
+            <el-checkbox label="DHL" />
+            <el-checkbox label="FedEx" />
+            <el-checkbox label="UPS" />
+            <el-checkbox label="顺丰" />
+            <el-checkbox label="云途" />
+            <el-checkbox label="燕文" />
+            <el-checkbox label="4PX" />
+          </el-checkbox-group>
+        </el-form-item>
+        <el-form-item label="结算方式" required>
+          <el-select v-model="formData.settlementType" placeholder="选择结算方式" style="width:100%">
+            <el-option label="按重量" value="按重量" />
+            <el-option label="按件数" value="按件数" />
+            <el-option label="按体积" value="按体积" />
+            <el-option label="包月" value="包月" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="平均时效(天)">
+          <el-input-number v-model="formData.avgDays" :min="1" :max="60" style="width:100%" />
+        </el-form-item>
+        <el-form-item label="追踪URL模板">
+          <el-input v-model="formData.trackingUrl" placeholder="https://track.xxx.com/{trackingNo}" />
+        </el-form-item>
+        <el-form-item label="联系人">
+          <el-input v-model="formData.contact" placeholder="联系人" />
+        </el-form-item>
+        <el-form-item label="联系电话">
+          <el-input v-model="formData.phone" placeholder="电话" />
+        </el-form-item>
+        <el-form-item label="备注">
+          <el-input v-model="formData.remark" type="textarea" :rows="2" placeholder="备注信息" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="dialogVisible = false">取消</el-button>
+        <el-button type="primary" @click="saveProvider">保存</el-button>
+      </template>
+    </el-dialog>
+
+    <el-dialog v-model="templateDialogVisible" :title="`运费模板 - ${currentProvider?.name}`" width="700px" destroy-on-close>
+      <div style="margin-bottom:16px">
+        <el-button type="primary" size="small" @click="openTemplateDialog()">新建模板</el-button>
+      </div>
+      <el-table :data="templateList" stripe size="small">
+        <el-table-column prop="name" label="模板名称" min-width="140" />
+        <el-table-column prop="calcType" label="计费方式" width="100">
+          <template #default="{ row }">
+            <el-tag size="small">{{ row.calcType }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column prop="firstWeight" label="首重" width="80" />
+        <el-table-column prop="firstPrice" label="首费" width="80" />
+        <el-table-column prop="continueWeight" label="续重" width="80" />
+        <el-table-column prop="continuePrice" label="续费" width="80" />
+        <el-table-column prop="regions" label="适用地区" min-width="120" show-overflow-tooltip />
+        <el-table-column prop="status" label="状态" width="70">
+          <template #default="{ row }">
+            <el-tag :type="row.status === '启用' ? 'success' : 'danger'" size="small">{{ row.status }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" width="100" fixed="right">
+          <template #default="{ row }">
+            <el-button link type="primary" size="small" @click="editTemplate(row)">编辑</el-button>
+            <el-button link type="danger" size="small" @click="deleteTemplate(row)">删除</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+    </el-dialog>
+
+    <el-dialog v-model="templateFormVisible" :title="editingTemplate ? '编辑模板' : '新建模板'" width="500px" destroy-on-close>
+      <el-form :model="templateForm" label-width="100px">
+        <el-form-item label="模板名称" required>
+          <el-input v-model="templateForm.name" placeholder="模板名称" />
+        </el-form-item>
+        <el-form-item label="计费方式" required>
+          <el-select v-model="templateForm.calcType" placeholder="选择计费方式" style="width:100%">
+            <el-option label="按重量" value="按重量" />
+            <el-option label="按件数" value="按件数" />
+            <el-option label="按体积" value="按体积" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="首重/首件">
+          <el-input-number v-model="templateForm.firstWeight" :min="0" style="width:48%" /> 
+          <span style="margin:0 8px">kg/件</span>
+        </el-form-item>
+        <el-form-item label="首费">
+          <el-input-number v-model="templateForm.firstPrice" :min="0" :precision="2" style="width:100%" />
+        </el-form-item>
+        <el-form-item label="续重/续件">
+          <el-input-number v-model="templateForm.continueWeight" :min="0" style="width:48%" />
+          <span style="margin:0 8px">kg/件</span>
+        </el-form-item>
+        <el-form-item label="续费">
+          <el-input-number v-model="templateForm.continuePrice" :min="0" :precision="2" style="width:100%" />
+        </el-form-item>
+        <el-form-item label="适用地区">
+          <el-input v-model="templateForm.regions" type="textarea" :rows="2" placeholder="美国,加拿大,英国" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="templateFormVisible = false">取消</el-button>
+        <el-button type="primary" @click="saveTemplate">保存</el-button>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { computed, onMounted, reactive, ref } from 'vue';
+import { ElMessage, ElMessageBox } from 'element-plus';
+
+interface LogisticsProvider {
+  id: string;
+  name: string;
+  code: string;
+  channels: string[];
+  settlementType: string;
+  avgDays: number;
+  trackingUrl: string;
+  contact: string;
+  phone: string;
+  status: string;
+  updatedAt: string;
+  remark?: string;
+}
+
+interface ShippingTemplate {
+  id: string;
+  name: string;
+  calcType: string;
+  firstWeight: number;
+  firstPrice: number;
+  continueWeight: number;
+  continuePrice: number;
+  regions: string;
+  status: string;
+}
+
+const items = ref<LogisticsProvider[]>([
+  { id: 'LP001', name: 'DHL国际快递', code: 'DHL', channels: ['DHL'], settlementType: '按重量', avgDays: 5, trackingUrl: 'https://www.dhl.com/track?tracking-id={trackingNo}', contact: 'John', phone: '+1-800-225-5345', status: '启用', updatedAt: '2026-04-15 10:30' },
+  { id: 'LP002', name: 'FedEx联邦快递', code: 'FedEx', channels: ['FedEx'], settlementType: '按重量', avgDays: 6, trackingUrl: 'https://www.fedex.com/fedextrack/?trknbr={trackingNo}', contact: 'Mary', phone: '+1-800-463-3339', status: '启用', updatedAt: '2026-04-14 14:20' },
+  { id: 'LP003', name: '顺丰速运', code: 'SF', channels: ['顺丰'], settlementType: '按重量', avgDays: 3, trackingUrl: 'https://www.sf-express.com/sf-service-owf/web/en/querytools/track/{trackingNo}', contact: '王强', phone: '95338', status: '启用', updatedAt: '2026-04-10 09:00' },
+  { id: 'LP004', name: '云途物流', code: 'YTO', channels: ['云途'], settlementType: '按重量', avgDays: 8, trackingUrl: 'https://www.yuntrack.com/track?trackingNo={trackingNo}', contact: 'Tom', phone: '400-800-6060', status: '停用', updatedAt: '2026-03-28 16:45' }
+]);
+
+const templates = ref<ShippingTemplate[]>([
+  { id: 'T001', name: 'DHL-美国专线', calcType: '按重量', firstWeight: 0.5, firstPrice: 120, continueWeight: 0.5, continuePrice: 35, regions: '美国,加拿大', status: '启用' },
+  { id: 'T002', name: 'DHL-欧洲专线', calcType: '按重量', firstWeight: 0.5, firstPrice: 150, continueWeight: 0.5, continuePrice: 45, regions: '英国,德国,法国,意大利', status: '启用' },
+  { id: 'T003', name: 'FedEx-全球优先', calcType: '按重量', firstWeight: 1, firstPrice: 200, continueWeight: 1, continuePrice: 50, regions: '全球', status: '启用' }
+]);
+
+const loading = ref(false);
+const dialogVisible = ref(false);
+const templateDialogVisible = ref(false);
+const templateFormVisible = ref(false);
+const editingItem = ref<LogisticsProvider | null>(null);
+const currentProvider = ref<LogisticsProvider | null>(null);
+const editingTemplate = ref<ShippingTemplate | null>(null);
+
+const filters = ref({ name: '', channel: '', status: '' });
+
+const formData = reactive({
+  name: '',
+  code: '',
+  channels: [] as string[],
+  settlementType: '',
+  avgDays: 5,
+  trackingUrl: '',
+  contact: '',
+  phone: '',
+  remark: ''
+});
+
+const templateForm = reactive({
+  name: '',
+  calcType: '按重量',
+  firstWeight: 0.5,
+  firstPrice: 0,
+  continueWeight: 0.5,
+  continuePrice: 0,
+  regions: ''
+});
+
+const templateList = computed(() => {
+  if (!currentProvider.value) return [];
+  return templates.value;
+});
+
+const filteredItems = computed(() => {
+  return items.value.filter(item => {
+    if (filters.value.name && !item.name.includes(filters.value.name)) return false;
+    if (filters.value.channel && !item.channels.includes(filters.value.channel)) return false;
+    if (filters.value.status && item.status !== filters.value.status) return false;
+    return true;
+  });
+});
+
+const loadData = () => {
+  loading.value = true;
+  setTimeout(() => { loading.value = false; }, 300);
+};
+
+const resetFilters = () => {
+  filters.value = { name: '', channel: '', status: '' };
+};
+
+const openDialog = (item?: LogisticsProvider) => {
+  editingItem.value = item || null;
+  if (item) {
+    Object.assign(formData, { name: item.name, code: item.code, channels: [...item.channels], settlementType: item.settlementType, avgDays: item.avgDays, trackingUrl: item.trackingUrl, contact: item.contact, phone: item.phone, remark: item.remark || '' });
+  } else {
+    Object.assign(formData, { name: '', code: '', channels: [], settlementType: '', avgDays: 5, trackingUrl: '', contact: '', phone: '', remark: '' });
+  }
+  dialogVisible.value = true;
+};
+
+const saveProvider = () => {
+  if (!formData.name || !formData.code || !formData.channels.length) {
+    ElMessage.warning('请填写必填项');
+    return;
+  }
+  if (editingItem.value) {
+    const idx = items.value.findIndex(i => i.id === editingItem.value!.id);
+    if (idx !== -1) items.value[idx] = { ...items.value[idx], ...formData, updatedAt: new Date().toLocaleString() };
+    ElMessage.success('物流商已更新');
+  } else {
+    items.value.push({ id: `LP${String(items.value.length + 1).padStart(3, '0')}`, ...formData, status: '启用', updatedAt: new Date().toLocaleString() });
+    ElMessage.success('物流商已创建');
+  }
+  dialogVisible.value = false;
+};
+
+const toggleStatus = async (row: LogisticsProvider) => {
+  const action = row.status === '启用' ? '停用' : '启用';
+  await ElMessageBox.confirm(`确认${action}「${row.name}」?`, `${action}确认`);
+  const idx = items.value.findIndex(i => i.id === row.id);
+  if (idx !== -1) {
+    items.value[idx].status = row.status === '启用' ? '停用' : '启用';
+    items.value[idx].updatedAt = new Date().toLocaleString();
+  }
+  ElMessage.success(`物流商已${action}`);
+};
+
+const openTemplate = (row: LogisticsProvider) => {
+  currentProvider.value = row;
+  templateDialogVisible.value = true;
+};
+
+const openTemplateDialog = () => {
+  editingTemplate.value = null;
+  Object.assign(templateForm, { name: '', calcType: '按重量', firstWeight: 0.5, firstPrice: 0, continueWeight: 0.5, continuePrice: 0, regions: '' });
+  templateFormVisible.value = true;
+};
+
+const editTemplate = (row: ShippingTemplate) => {
+  editingTemplate.value = row;
+  Object.assign(templateForm, { ...row });
+  templateFormVisible.value = true;
+};
+
+const saveTemplate = () => {
+  if (!templateForm.name) {
+    ElMessage.warning('请填写模板名称');
+    return;
+  }
+  if (editingTemplate.value) {
+    const idx = templates.value.findIndex(t => t.id === editingTemplate.value!.id);
+    if (idx !== -1) templates.value[idx] = { ...templates.value[idx], ...templateForm };
+    ElMessage.success('模板已更新');
+  } else {
+    templates.value.push({ id: `T${String(templates.value.length + 1).padStart(3, '0')}`, ...templateForm, status: '启用' });
+    ElMessage.success('模板已创建');
+  }
+  templateFormVisible.value = false;
+};
+
+const deleteTemplate = async (row: ShippingTemplate) => {
+  await ElMessageBox.confirm('确认删除此模板?', '删除确认');
+  const idx = templates.value.findIndex(t => t.id === row.id);
+  if (idx !== -1) templates.value.splice(idx, 1);
+  ElMessage.success('模板已删除');
+};
+
+onMounted(loadData);
+</script>
+
+<style scoped>
+.filter-form :deep(.el-form-item) { margin-bottom: 0; }
+</style>

+ 252 - 0
src/views/logistics/ShippingTemplateView.vue

@@ -0,0 +1,252 @@
+<template>
+  <div class="app-page">
+    <section class="glass-card section-card">
+      <el-form :model="filters" inline class="filter-form">
+        <el-form-item label="模板名称">
+          <el-input v-model="filters.name" placeholder="模板名称" clearable style="width:160px" @keyup.enter="loadData" />
+        </el-form-item>
+        <el-form-item label="计费方式">
+          <el-select v-model="filters.calcType" placeholder="全部" clearable style="width:130px">
+            <el-option label="按重量" value="按重量" />
+            <el-option label="按件数" value="按件数" />
+            <el-option label="按体积" value="按体积" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="状态">
+          <el-select v-model="filters.status" placeholder="全部" clearable style="width:120px">
+            <el-option label="启用" value="启用" />
+            <el-option label="停用" value="停用" />
+          </el-select>
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" @click="loadData">查询</el-button>
+          <el-button @click="resetFilters">重置</el-button>
+        </el-form-item>
+      </el-form>
+    </section>
+
+    <section class="glass-card section-card" style="padding:12px 24px">
+      <div class="table-toolbar" style="margin-bottom:0">
+        <div class="chip-list">
+          <el-button type="primary" @click="openDialog()">新建模板</el-button>
+        </div>
+        <el-button @click="loadData">刷新</el-button>
+      </div>
+    </section>
+
+    <section class="glass-card section-card">
+      <el-table :data="filteredItems" stripe style="width:100%" v-loading="loading">
+        <el-table-column prop="name" label="模板名称" min-width="160" />
+        <el-table-column prop="logisticsProvider" label="物流商" width="140" />
+        <el-table-column prop="calcType" label="计费方式" width="100">
+          <template #default="{ row }">
+            <el-tag size="small">{{ row.calcType }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="首费规则" min-width="180">
+          <template #default="{ row }">
+            {{ row.firstWeight }}{{ row.calcType === '按重量' ? 'kg' : '件' }}内 ${{ row.firstPrice }}
+          </template>
+        </el-table-column>
+        <el-table-column label="续费规则" min-width="180">
+          <template #default="{ row }">
+            每{{ row.continueWeight }}{{ row.calcType === '按重量' ? 'kg' : '件' }} +${{ row.continuePrice }}
+          </template>
+        </el-table-column>
+        <el-table-column prop="regions" label="适用地区" min-width="200" show-overflow-tooltip />
+        <el-table-column prop="status" label="状态" width="80">
+          <template #default="{ row }">
+            <el-tag :type="row.status === '启用' ? 'success' : 'danger'" size="small">{{ row.status }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column prop="updatedAt" label="更新时间" width="160" />
+        <el-table-column label="操作" width="180" fixed="right">
+          <template #default="{ row }">
+            <el-button link type="primary" @click="openDialog(row)">编辑</el-button>
+            <el-button link :type="row.status === '启用' ? 'danger' : 'primary'" @click="toggleStatus(row)">{{ row.status === '启用' ? '停用' : '启用' }}</el-button>
+            <el-button link type="primary" @click="doCopy(row)">复制</el-button>
+          </template>
+        </el-table-column>
+        <template #empty>
+          <el-empty description="暂无数据" />
+        </template>
+      </el-table>
+    </section>
+
+    <el-dialog v-model="dialogVisible" :title="editingItem ? '编辑运费模板' : '新建运费模板'" width="560px" destroy-on-close>
+      <el-form :model="formData" label-width="110px">
+        <el-form-item label="模板名称" required>
+          <el-input v-model="formData.name" placeholder="请输入模板名称" />
+        </el-form-item>
+        <el-form-item label="物流商" required>
+          <el-select v-model="formData.logisticsProvider" placeholder="选择物流商" style="width:100%">
+            <el-option label="DHL" value="DHL" />
+            <el-option label="FedEx" value="FedEx" />
+            <el-option label="UPS" value="UPS" />
+            <el-option label="顺丰" value="顺丰" />
+            <el-option label="云途" value="云途" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="计费方式" required>
+          <el-select v-model="formData.calcType" placeholder="选择计费方式" style="width:100%">
+            <el-option label="按重量" value="按重量" />
+            <el-option label="按件数" value="按件数" />
+            <el-option label="按体积" value="按体积" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="首重/首件">
+          <el-input-number v-model="formData.firstWeight" :min="0" :precision="2" style="width:100%" />
+        </el-form-item>
+        <el-form-item label="首费">
+          <el-input-number v-model="formData.firstPrice" :min="0" :precision="2" style="width:100%" />
+        </el-form-item>
+        <el-form-item label="续重/续件">
+          <el-input-number v-model="formData.continueWeight" :min="0" :precision="2" style="width:100%" />
+        </el-form-item>
+        <el-form-item label="续费">
+          <el-input-number v-model="formData.continuePrice" :min="0" :precision="2" style="width:100%" />
+        </el-form-item>
+        <el-form-item label="偏远地区附加费">
+          <el-input-number v-model="formData.remoteSurcharge" :min="0" :precision="2" style="width:100%" />
+        </el-form-item>
+        <el-form-item label="适用地区" required>
+          <el-select v-model="formData.regions" multiple placeholder="选择适用地区" style="width:100%">
+            <el-option label="美国" value="美国" />
+            <el-option label="加拿大" value="加拿大" />
+            <el-option label="英国" value="英国" />
+            <el-option label="德国" value="德国" />
+            <el-option label="法国" value="法国" />
+            <el-option label="意大利" value="意大利" />
+            <el-option label="西班牙" value="西班牙" />
+            <el-option label="澳大利亚" value="澳大利亚" />
+            <el-option label="日本" value="日本" />
+            <el-option label="韩国" value="韩国" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="备注">
+          <el-input v-model="formData.remark" type="textarea" :rows="2" placeholder="备注信息" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="dialogVisible = false">取消</el-button>
+        <el-button type="primary" @click="saveTemplate">保存</el-button>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { computed, onMounted, reactive, ref } from 'vue';
+import { ElMessage, ElMessageBox } from 'element-plus';
+
+interface ShippingTemplate {
+  id: string;
+  name: string;
+  logisticsProvider: string;
+  calcType: string;
+  firstWeight: number;
+  firstPrice: number;
+  continueWeight: number;
+  continuePrice: number;
+  remoteSurcharge: number;
+  regions: string[];
+  status: string;
+  updatedAt: string;
+}
+
+const items = ref<ShippingTemplate[]>([
+  { id: 'ST001', name: 'DHL-美国标准', logisticsProvider: 'DHL', calcType: '按重量', firstWeight: 0.5, firstPrice: 120, continueWeight: 0.5, continuePrice: 35, remoteSurcharge: 25, regions: ['美国', '加拿大'], status: '启用', updatedAt: '2026-04-15 10:30' },
+  { id: 'ST002', name: 'DHL-欧洲优先', logisticsProvider: 'DHL', calcType: '按重量', firstWeight: 0.5, firstPrice: 150, continueWeight: 0.5, continuePrice: 45, remoteSurcharge: 30, regions: ['英国', '德国', '法国', '意大利', '西班牙'], status: '启用', updatedAt: '2026-04-14 14:20' },
+  { id: 'ST003', name: 'FedEx-全球特快', logisticsProvider: 'FedEx', calcType: '按重量', firstWeight: 1, firstPrice: 200, continueWeight: 1, continuePrice: 50, remoteSurcharge: 40, regions: ['美国', '加拿大', '英国', '德国', '澳大利亚', '日本'], status: '启用', updatedAt: '2026-04-10 09:00' },
+  { id: 'ST004', name: '顺丰-中国大陆', logisticsProvider: '顺丰', calcType: '按重量', firstWeight: 1, firstPrice: 15, continueWeight: 1, continuePrice: 5, remoteSurcharge: 0, regions: ['中国大陆'], status: '启用', updatedAt: '2026-04-08 16:45' },
+  { id: 'ST005', name: '云途-欧洲专线', logisticsProvider: '云途', calcType: '按重量', firstWeight: 0.5, firstPrice: 85, continueWeight: 0.5, continuePrice: 25, remoteSurcharge: 20, regions: ['英国', '德国', '法国', '意大利'], status: '停用', updatedAt: '2026-03-28 11:30' }
+]);
+
+const loading = ref(false);
+const dialogVisible = ref(false);
+const editingItem = ref<ShippingTemplate | null>(null);
+
+const filters = ref({ name: '', calcType: '', status: '' });
+
+const formData = reactive({
+  name: '',
+  logisticsProvider: '',
+  calcType: '按重量',
+  firstWeight: 0.5,
+  firstPrice: 0,
+  continueWeight: 0.5,
+  continuePrice: 0,
+  remoteSurcharge: 0,
+  regions: [] as string[],
+  remark: ''
+});
+
+const filteredItems = computed(() => {
+  return items.value.filter(item => {
+    if (filters.value.name && !item.name.includes(filters.value.name)) return false;
+    if (filters.value.calcType && item.calcType !== filters.value.calcType) return false;
+    if (filters.value.status && item.status !== filters.value.status) return false;
+    return true;
+  });
+});
+
+const loadData = () => {
+  loading.value = true;
+  setTimeout(() => { loading.value = false; }, 300);
+};
+
+const resetFilters = () => {
+  filters.value = { name: '', calcType: '', status: '' };
+};
+
+const openDialog = (item?: ShippingTemplate) => {
+  editingItem.value = item || null;
+  if (item) {
+    Object.assign(formData, { ...item });
+  } else {
+    Object.assign(formData, { name: '', logisticsProvider: '', calcType: '按重量', firstWeight: 0.5, firstPrice: 0, continueWeight: 0.5, continuePrice: 0, remoteSurcharge: 0, regions: [], remark: '' });
+  }
+  dialogVisible.value = true;
+};
+
+const saveTemplate = () => {
+  if (!formData.name || !formData.logisticsProvider || !formData.regions.length) {
+    ElMessage.warning('请填写必填项');
+    return;
+  }
+  if (editingItem.value) {
+    const idx = items.value.findIndex(i => i.id === editingItem.value!.id);
+    if (idx !== -1) items.value[idx] = { ...items.value[idx], ...formData, updatedAt: new Date().toLocaleString() };
+    ElMessage.success('模板已更新');
+  } else {
+    items.value.push({ id: `ST${String(items.value.length + 1).padStart(3, '0')}`, ...formData, status: '启用', updatedAt: new Date().toLocaleString() });
+    ElMessage.success('模板已创建');
+  }
+  dialogVisible.value = false;
+};
+
+const toggleStatus = async (row: ShippingTemplate) => {
+  const action = row.status === '启用' ? '停用' : '启用';
+  await ElMessageBox.confirm(`确认${action}「${row.name}」?`, `${action}确认`);
+  const idx = items.value.findIndex(i => i.id === row.id);
+  if (idx !== -1) {
+    items.value[idx].status = row.status === '启用' ? '停用' : '启用';
+    items.value[idx].updatedAt = new Date().toLocaleString();
+  }
+  ElMessage.success(`模板已${action}`);
+};
+
+const doCopy = (row: ShippingTemplate) => {
+  ElMessageBox.confirm(`确认复制「${row.name}」模板?`, '复制模板').then(() => {
+    const copy = { ...row, id: `ST${String(items.value.length + 1).padStart(3, '0')}`, name: `${row.name} (副本)`, status: '停用', updatedAt: new Date().toLocaleString() };
+    items.value.push(copy);
+    ElMessage.success('模板已复制');
+  }).catch(() => {});
+};
+
+onMounted(loadData);
+</script>
+
+<style scoped>
+.filter-form :deep(.el-form-item) { margin-bottom: 0; }
+</style>

+ 301 - 0
src/views/procurement/ReplenishmentPlanView.vue

@@ -0,0 +1,301 @@
+<template>
+  <div class="app-page">
+    <section class="glass-card section-card">
+      <el-form :model="filters" inline class="filter-form">
+        <el-form-item label="SKU">
+          <el-input v-model="filters.sku" placeholder="SKU编号" clearable style="width:160px" @keyup.enter="loadData" />
+        </el-form-item>
+        <el-form-item label="仓库">
+          <el-select v-model="filters.warehouse" placeholder="全部仓库" clearable style="width:140px">
+            <el-option v-for="w in warehouses" :key="w" :label="w" :value="w" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="预警状态">
+          <el-select v-model="filters.warningStatus" placeholder="全部" clearable style="width:130px">
+            <el-option label="正常" value="正常" />
+            <el-option label="库存不足" value="库存不足" />
+            <el-option label="即将缺货" value="即将缺货" />
+          </el-select>
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" @click="loadData">查询</el-button>
+          <el-button @click="resetFilters">重置</el-button>
+        </el-form-item>
+      </el-form>
+    </section>
+
+    <section class="glass-card section-card" style="padding:12px 24px">
+      <div class="table-toolbar" style="margin-bottom:0">
+        <div class="chip-list">
+          <el-button type="primary" @click="openGeneratePO">生成采购单</el-button>
+          <el-button @click="loadData">刷新</el-button>
+        </div>
+        <el-select v-model="autoRefresh" placeholder="自动刷新" clearable style="width:140px">
+          <el-option label="每5分钟" value="5" />
+          <el-option label="每15分钟" value="15" />
+          <el-option label="每30分钟" value="30" />
+        </el-select>
+      </div>
+    </section>
+
+    <section class="glass-card section-card">
+      <el-table :data="filteredItems" stripe style="width:100%" v-loading="loading" @selection-change="onSelection">
+        <el-table-column type="selection" width="45" />
+        <el-table-column prop="sku" label="SKU" width="160" />
+        <el-table-column prop="productTitle" label="商品名称" min-width="220" show-overflow-tooltip />
+        <el-table-column prop="warehouse" label="仓库" width="120" />
+        <el-table-column prop="dailySales" label="日均销量" width="90" align="center" />
+        <el-table-column prop="available" label="可用库存" width="90" align="center">
+          <template #default="{ row }">
+            <span :class="stockClass(row.available, row.safeStock)">{{ row.available }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="locked" label="锁定库存" width="90" align="center" />
+        <el-table-column prop="inbound" label="在途库存" width="90" align="center" />
+        <el-table-column prop="safeStock" label="安全库存" width="90" align="center" />
+        <el-table-column prop="suggestQty" label="建议补货量" width="110" align="center">
+          <template #default="{ row }">
+            <el-tag type="warning" size="small">{{ row.suggestQty }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column prop="supplier" label="建议供应商" width="130" />
+        <el-table-column prop="leadTime" label="交期(天)" width="90" align="center" />
+        <el-table-column prop="warningStatus" label="预警状态" width="100">
+          <template #default="{ row }">
+            <el-tag :type="warningTag(row.warningStatus)" size="small">{{ row.warningStatus }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" width="120" fixed="right">
+          <template #default="{ row }">
+            <el-button link type="primary" @click="openDetail(row)">详情</el-button>
+            <el-button link type="primary" @click="ignoreWarning(row)">忽略</el-button>
+          </template>
+        </el-table-column>
+        <template #empty>
+          <el-empty description="暂无数据" />
+        </template>
+      </el-table>
+    </section>
+
+    <section class="glass-card section-card" style="padding:16px">
+      <h4 style="margin:0 0 12px">备货统计</h4>
+      <div class="stat-grid" style="grid-template-columns:repeat(5, 1fr)">
+        <article class="stat-card">
+          <div class="stat-card__label">待补货SKU</div>
+          <div class="stat-card__value" style="font-size:24px;color:var(--cb-accent)">12</div>
+        </article>
+        <article class="stat-card">
+          <div class="stat-card__label">建议补货总量</div>
+          <div class="stat-card__value" style="font-size:24px">2,450</div>
+        </article>
+        <article class="stat-card">
+          <div class="stat-card__label">预计采购金额</div>
+          <div class="stat-card__value" style="font-size:24px">$48,600</div>
+        </article>
+        <article class="stat-card">
+          <div class="stat-card__label">库存不足SKU</div>
+          <div class="stat-card__value" style="font-size:24px;color:var(--cb-danger)">5</div>
+        </article>
+        <article class="stat-card">
+          <div class="stat-card__label">覆盖天数</div>
+          <div class="stat-card__value" style="font-size:24px">18天</div>
+        </article>
+      </div>
+    </section>
+
+    <el-dialog v-model="detailVisible" title="备货详情" width="600px">
+      <el-descriptions :column="2" border v-if="detailItem">
+        <el-descriptions-item label="SKU">{{ detailItem.sku }}</el-descriptions-item>
+        <el-descriptions-item label="商品名称">{{ detailItem.productTitle }}</el-descriptions-item>
+        <el-descriptions-item label="仓库">{{ detailItem.warehouse }}</el-descriptions-item>
+        <el-descriptions-item label="预警状态">
+          <el-tag :type="warningTag(detailItem.warningStatus)">{{ detailItem.warningStatus }}</el-tag>
+        </el-descriptions-item>
+        <el-descriptions-item label="日均销量">{{ detailItem.dailySales }}</el-descriptions-item>
+        <el-descriptions-item label="安全库存">{{ detailItem.safeStock }}</el-descriptions-item>
+        <el-descriptions-item label="可用库存">{{ detailItem.available }}</el-descriptions-item>
+        <el-descriptions-item label="锁定库存">{{ detailItem.locked }}</el-descriptions-item>
+        <el-descriptions-item label="在途库存">{{ detailItem.inbound }}</el-descriptions-item>
+        <el-descriptions-item label="建议补货量">{{ detailItem.suggestQty }}</el-descriptions-item>
+        <el-descriptions-item label="建议供应商">{{ detailItem.supplier }}</el-descriptions-item>
+        <el-descriptions-item label="标准交期">{{ detailItem.leadTime }}天</el-descriptions-item>
+        <el-descriptions-item label="覆盖天数" :span="2">{{ detailItem.coverDays }}天</el-descriptions-item>
+      </el-descriptions>
+      <template #footer>
+        <el-button @click="detailVisible = false">关闭</el-button>
+        <el-button type="primary" @click="generatePO(detailItem!)">生成采购单</el-button>
+      </template>
+    </el-dialog>
+
+    <el-dialog v-model="poDialogVisible" title="生成采购单" width="560px" destroy-on-close>
+      <el-form :model="poForm" label-width="100px">
+        <el-form-item label="供应商" required>
+          <el-select v-model="poForm.supplier" placeholder="选择供应商" style="width:100%">
+            <el-option v-for="s in suppliers" :key="s" :label="s" :value="s" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="仓库" required>
+          <el-select v-model="poForm.warehouse" placeholder="选择仓库" style="width:100%">
+            <el-option v-for="w in warehouses" :key="w" :label="w" :value="w" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="商品明细">
+          <el-table :data="selectedItems" stripe size="small">
+            <el-table-column prop="sku" label="SKU" width="140" />
+            <el-table-column prop="productTitle" label="商品" min-width="120" />
+            <el-table-column prop="suggestQty" label="建议数量" width="90" />
+            <el-table-column label="采购数量" width="120">
+              <template #default="{ row }">
+                <el-input-number v-model="row.poQty" :min="1" size="small" style="width:100%" />
+              </template>
+            </el-table-column>
+          </el-table>
+        </el-form-item>
+        <el-form-item label="预计交期">
+          <el-date-picker v-model="poForm.expectedDate" type="date" placeholder="选择日期" style="width:100%" />
+        </el-form-item>
+        <el-form-item label="备注">
+          <el-input v-model="poForm.remark" type="textarea" :rows="2" placeholder="备注信息" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="poDialogVisible = false">取消</el-button>
+        <el-button type="primary" @click="confirmGeneratePO">确认生成</el-button>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { computed, onMounted, reactive, ref } from 'vue';
+import { ElMessage, ElMessageBox } from 'element-plus';
+
+interface ReplenishmentItem {
+  sku: string;
+  productTitle: string;
+  warehouse: string;
+  dailySales: number;
+  available: number;
+  locked: number;
+  inbound: number;
+  safeStock: number;
+  suggestQty: number;
+  supplier: string;
+  leadTime: number;
+  warningStatus: string;
+  coverDays: number;
+}
+
+const items = ref<ReplenishmentItem[]>([
+  { sku: 'SKU-LUGG-20-BLK', productTitle: 'TravelFlex Carry-On / Black', warehouse: '深圳南山仓', dailySales: 8, available: 15, locked: 5, inbound: 0, safeStock: 50, suggestQty: 80, supplier: '深圳鼎力供应商', leadTime: 7, warningStatus: '库存不足', coverDays: 2 },
+  { sku: 'SKU-BAG-ML-BRW', productTitle: 'Classic Leather Tote / Brown', warehouse: '义乌商贸仓', dailySales: 5, available: 45, locked: 3, inbound: 30, safeStock: 40, suggestQty: 25, supplier: '义乌恒达皮具', leadTime: 10, warningStatus: '正常', coverDays: 9 },
+  { sku: 'SKU-SPRT-YGA-BLU', productTitle: 'Yoga Mat Pro / Blue', warehouse: '深圳南山仓', dailySales: 12, available: 20, locked: 8, inbound: 100, safeStock: 60, suggestQty: 60, supplier: '深圳鼎力供应商', leadTime: 7, warningStatus: '即将缺货', coverDays: 2 },
+  { sku: 'SKU-LUGG-28-NVY', productTitle: 'TravelFlex Large Check-In / Navy', warehouse: '洛杉矶海外仓', dailySales: 3, available: 8, locked: 2, inbound: 0, safeStock: 20, suggestQty: 30, supplier: '美国西部仓', leadTime: 3, warningStatus: '库存不足', coverDays: 3 },
+  { sku: 'SKU-BAG-BPK-OLV', productTitle: 'Urban Backpack / Olive', warehouse: '义乌商贸仓', dailySales: 6, available: 55, locked: 4, inbound: 0, safeStock: 50, suggestQty: 35, supplier: '义乌恒达皮具', leadTime: 10, warningStatus: '正常', coverDays: 9 },
+  { sku: 'SKU-TOWEL-SET-MIX', productTitle: 'AeroDry Towel Set', warehouse: '深圳南山仓', dailySales: 10, available: 30, locked: 10, inbound: 50, safeStock: 80, suggestQty: 80, supplier: '深圳鼎力供应商', leadTime: 7, warningStatus: '正常', coverDays: 3 }
+]);
+
+const loading = ref(false);
+const selected = ref<ReplenishmentItem[]>([]);
+const detailVisible = ref(false);
+const poDialogVisible = ref(false);
+const detailItem = ref<ReplenishmentItem | null>(null);
+const autoRefresh = ref('');
+const warehouses = ['深圳南山仓', '义乌商贸仓', '洛杉矶海外仓'];
+const suppliers = ['深圳鼎力供应商', '义乌恒达皮具', '美国西部仓'];
+
+const filters = ref({ sku: '', warehouse: '', warningStatus: '' });
+
+const poForm = reactive({
+  supplier: '',
+  warehouse: '',
+  expectedDate: '',
+  remark: ''
+});
+
+const selectedItems = ref<Array<ReplenishmentItem & { poQty: number }>>([]);
+
+const filteredItems = computed(() => {
+  return items.value.filter(item => {
+    if (filters.value.sku && !item.sku.includes(filters.value.sku) && !item.productTitle.includes(filters.value.sku)) return false;
+    if (filters.value.warehouse && item.warehouse !== filters.value.warehouse) return false;
+    if (filters.value.warningStatus && item.warningStatus !== filters.value.warningStatus) return false;
+    return true;
+  });
+});
+
+const stockClass = (available: number, safeStock: number) => {
+  if (available === 0) return 'text-danger';
+  if (available < safeStock) return 'text-warning';
+  return '';
+};
+
+const warningTag = (status: string) => {
+  const map: Record<string, string> = { '正常': 'success', '库存不足': 'danger', '即将缺货': 'warning' };
+  return map[status] || '';
+};
+
+const loadData = () => {
+  loading.value = true;
+  setTimeout(() => { loading.value = false; }, 300);
+};
+
+const resetFilters = () => {
+  filters.value = { sku: '', warehouse: '', warningStatus: '' };
+};
+
+const onSelection = (rows: ReplenishmentItem[]) => { selected.value = rows; };
+
+const openDetail = (row: ReplenishmentItem) => {
+  detailItem.value = row;
+  detailVisible.value = true;
+};
+
+const ignoreWarning = async (row: ReplenishmentItem) => {
+  await ElMessageBox.confirm('确认忽略此备货建议?', '忽略确认');
+  const idx = items.value.findIndex(i => i.sku === row.sku);
+  if (idx !== -1) items.value.splice(idx, 1);
+  ElMessage.success('已忽略');
+};
+
+const openGeneratePO = async () => {
+  if (!selected.value.length) {
+    ElMessage.warning('请先选择要生成采购单的商品');
+    return;
+  }
+  selectedItems.value = selected.value.map(i => ({ ...i, poQty: i.suggestQty }));
+  poForm.supplier = '';
+  poForm.warehouse = '';
+  poForm.expectedDate = '';
+  poForm.remark = '';
+  poDialogVisible.value = true;
+};
+
+const generatePO = (row: ReplenishmentItem) => {
+  detailVisible.value = false;
+  selectedItems.value = [{ ...row, poQty: row.suggestQty }];
+  poForm.supplier = row.supplier;
+  poForm.warehouse = row.warehouse;
+  poForm.expectedDate = '';
+  poForm.remark = '';
+  poDialogVisible.value = true;
+};
+
+const confirmGeneratePO = async () => {
+  if (!poForm.supplier || !poForm.warehouse) {
+    ElMessage.warning('请填写供应商和仓库');
+    return;
+  }
+  await ElMessageBox.confirm('确认生成采购单?', '生成确认');
+  ElMessage.success('采购单已生成');
+  poDialogVisible.value = false;
+};
+
+onMounted(loadData);
+</script>
+
+<style scoped>
+.filter-form :deep(.el-form-item) { margin-bottom: 0; }
+.text-danger { color: var(--cb-danger); font-weight: 600; }
+.text-warning { color: var(--cb-accent); font-weight: 600; }
+</style>

+ 160 - 0
src/views/warehouse/InventoryLogView.vue

@@ -0,0 +1,160 @@
+<template>
+  <div class="app-page">
+    <section class="glass-card section-card">
+      <el-form :model="filters" inline class="filter-form">
+        <el-form-item label="SKU">
+          <el-input v-model="filters.sku" placeholder="SKU编号" clearable style="width:160px" @keyup.enter="loadData" />
+        </el-form-item>
+        <el-form-item label="仓库">
+          <el-select v-model="filters.warehouse" placeholder="全部仓库" clearable style="width:140px">
+            <el-option v-for="w in warehouses" :key="w" :label="w" :value="w" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="变动类型">
+          <el-select v-model="filters.changeType" placeholder="全部" clearable style="width:130px">
+            <el-option label="入库" value="入库" />
+            <el-option label="出库" value="出库" />
+            <el-option label="调拨" value="调拨" />
+            <el-option label="盘点" value="盘点" />
+            <el-option label="调整" value="调整" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="时间范围">
+          <el-date-picker v-model="filters.dateRange" type="daterange" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" style="width:240px" />
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" @click="loadData">查询</el-button>
+          <el-button @click="resetFilters">重置</el-button>
+        </el-form-item>
+      </el-form>
+    </section>
+
+    <section class="glass-card section-card" style="padding:12px 24px">
+      <div class="table-toolbar" style="margin-bottom:0">
+        <div class="chip-list">
+          <el-button @click="doExport">导出记录</el-button>
+        </div>
+        <el-button @click="loadData">刷新</el-button>
+      </div>
+    </section>
+
+    <section class="glass-card section-card">
+      <el-table :data="filteredItems" stripe style="width:100%" v-loading="loading">
+        <el-table-column prop="time" label="变动时间" width="160" />
+        <el-table-column prop="sku" label="SKU" width="160" />
+        <el-table-column prop="productTitle" label="商品名称" min-width="200" show-overflow-tooltip />
+        <el-table-column prop="warehouse" label="仓库" width="120" />
+        <el-table-column prop="changeType" label="变动类型" width="90">
+          <template #default="{ row }">
+            <el-tag :type="changeTypeTag(row.changeType)" size="small">{{ row.changeType }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column prop="changeQty" label="变动数量" width="100">
+          <template #default="{ row }">
+            <span :class="row.changeQty > 0 ? 'text-success' : 'text-danger'">
+              {{ row.changeQty > 0 ? '+' : '' }}{{ row.changeQty }}
+            </span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="beforeQty" label="变动前" width="80" />
+        <el-table-column prop="afterQty" label="变动后" width="80" />
+        <el-table-column prop="source" label="来源" width="120" show-overflow-tooltip />
+        <el-table-column prop="relatedOrder" label="关联单据" width="140">
+          <template #default="{ row }">
+            <el-button v-if="row.relatedOrder" link type="primary">{{ row.relatedOrder }}</el-button>
+            <span v-else>-</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="operator" label="操作人" width="100" />
+        <el-table-column prop="remark" label="备注" min-width="120" show-overflow-tooltip />
+        <template #empty>
+          <el-empty description="暂无数据" />
+        </template>
+      </el-table>
+      <div style="display:flex;justify-content:flex-end;margin-top:16px" v-if="total > 0">
+        <el-pagination v-model:current-page="page" v-model:page-size="pageSize" :total="total" :page-sizes="[10, 20, 50]" layout="total, sizes, prev, pager, next" @size-change="loadData" @current-change="loadData" />
+      </div>
+    </section>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { computed, onMounted, ref } from 'vue';
+import { ElMessage } from 'element-plus';
+
+interface InventoryLogItem {
+  id: string;
+  time: string;
+  sku: string;
+  productTitle: string;
+  warehouse: string;
+  changeType: string;
+  changeQty: number;
+  beforeQty: number;
+  afterQty: number;
+  source: string;
+  relatedOrder: string;
+  operator: string;
+  remark: string;
+}
+
+const items = ref<InventoryLogItem[]>([
+  { id: 'L001', time: '2026-04-20 10:30', sku: 'SKU-LUGG-20-BLK', productTitle: 'TravelFlex Carry-On / Black', warehouse: '深圳南山仓', changeType: '入库', changeQty: 100, beforeQty: 50, afterQty: 150, source: '采购入库', relatedOrder: 'PO-20260420-001', operator: '张明', remark: '采购到货' },
+  { id: 'L002', time: '2026-04-20 09:15', sku: 'SKU-BAG-ML-BRW', productTitle: 'Classic Leather Tote / Brown', warehouse: '义乌商贸仓', changeType: '出库', changeQty: -5, beforeQty: 30, afterQty: 25, source: '订单出库', relatedOrder: 'OMS-20260419-0012', operator: '李华', remark: '订单发货' },
+  { id: 'L003', time: '2026-04-19 16:45', sku: 'SKU-SPRT-YGA-BLU', productTitle: 'Yoga Mat Pro / Blue', warehouse: '深圳南山仓', changeType: '调拨', changeQty: -20, beforeQty: 80, afterQty: 60, source: '仓库调拨', relatedOrder: 'TR-20260419-001', operator: '王磊', remark: '调拨至义乌仓' },
+  { id: 'L004', time: '2026-04-19 14:20', sku: 'SKU-LUGG-28-NVY', productTitle: 'TravelFlex Large Check-In / Navy', warehouse: '洛杉矶海外仓', changeType: '入库', changeQty: 50, beforeQty: 20, afterQty: 70, source: '海外仓收货', relatedOrder: 'TR-20260418-002', operator: 'Jack', remark: '海运到货' },
+  { id: 'L005', time: '2026-04-19 11:00', sku: 'SKU-BAG-BPK-OLV', productTitle: 'Urban Backpack / Olive', warehouse: '义乌商贸仓', changeType: '盘点', changeQty: -2, beforeQty: 45, afterQty: 43, source: '库存盘点', relatedOrder: '', operator: '李华', remark: '盘点差异' },
+  { id: 'L006', time: '2026-04-18 17:30', sku: 'SKU-TOWEL-SET-MIX', productTitle: 'AeroDry Towel Set', warehouse: '深圳南山仓', changeType: '调整', changeQty: 10, beforeQty: 100, afterQty: 110, source: '库存调整', relatedOrder: '', operator: '张明', remark: '系统录入更正' },
+  { id: 'L007', time: '2026-04-18 15:00', sku: 'SKU-LUGG-20-BLK', productTitle: 'TravelFlex Carry-On / Black', warehouse: '义乌商贸仓', changeType: '出库', changeQty: -8, beforeQty: 40, afterQty: 32, source: '订单出库', relatedOrder: 'OMS-20260418-0015', operator: '李华', remark: '' },
+  { id: 'L008', time: '2026-04-18 10:45', sku: 'SKU-SPRT-BTL-GRN', productTitle: 'Sports Bottle 750ml / Green', warehouse: '洛杉矶海外仓', changeType: '出库', changeQty: -12, beforeQty: 60, afterQty: 48, source: '订单出库', relatedOrder: 'OMS-20260419-0022', operator: 'Jack', remark: '' }
+]);
+
+const loading = ref(false);
+const page = ref(1);
+const pageSize = ref(10);
+const warehouses = ['深圳南山仓', '义乌商贸仓', '洛杉矶海外仓', '伦敦海外仓'];
+
+const filters = ref({
+  sku: '',
+  warehouse: '',
+  changeType: '',
+  dateRange: [] as string[]
+});
+
+const filteredItems = computed(() => {
+  return items.value.filter(item => {
+    if (filters.value.sku && !item.sku.includes(filters.value.sku) && !item.productTitle.includes(filters.value.sku)) return false;
+    if (filters.value.warehouse && item.warehouse !== filters.value.warehouse) return false;
+    if (filters.value.changeType && item.changeType !== filters.value.changeType) return false;
+    return true;
+  });
+});
+
+const total = computed(() => filteredItems.value.length);
+
+const changeTypeTag = (type: string) => {
+  const map: Record<string, string> = { '入库': 'success', '出库': 'danger', '调拨': 'warning', '盘点': 'info', '调整': '' };
+  return map[type] || '';
+};
+
+const loadData = () => {
+  loading.value = true;
+  setTimeout(() => { loading.value = false; }, 300);
+};
+
+const resetFilters = () => {
+  filters.value = { sku: '', warehouse: '', changeType: '', dateRange: [] };
+};
+
+const doExport = () => {
+  ElMessage.info('导出开始,完成后将自动下载');
+};
+
+onMounted(loadData);
+</script>
+
+<style scoped>
+.filter-form :deep(.el-form-item) { margin-bottom: 0; }
+.text-success { color: var(--cb-success); font-weight: 600; }
+.text-danger { color: var(--cb-danger); font-weight: 600; }
+</style>

+ 301 - 0
src/views/warehouse/WarehouseView.vue

@@ -0,0 +1,301 @@
+<template>
+  <div class="app-page">
+    <section class="glass-card section-card">
+      <el-form :model="filters" inline class="filter-form">
+        <el-form-item label="仓库名称">
+          <el-input v-model="filters.name" placeholder="仓库名称" clearable style="width:160px" @keyup.enter="loadData" />
+        </el-form-item>
+        <el-form-item label="仓库类型">
+          <el-select v-model="filters.type" placeholder="全部" clearable style="width:140px">
+            <el-option label="自有仓" value="自有仓" />
+            <el-option label="第三方仓" value="第三方仓" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="状态">
+          <el-select v-model="filters.status" placeholder="全部" clearable style="width:120px">
+            <el-option label="启用" value="启用" />
+            <el-option label="停用" value="停用" />
+          </el-select>
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" @click="loadData">查询</el-button>
+          <el-button @click="resetFilters">重置</el-button>
+        </el-form-item>
+      </el-form>
+    </section>
+
+    <section class="glass-card section-card" style="padding:12px 24px">
+      <div class="table-toolbar" style="margin-bottom:0">
+        <div class="chip-list">
+          <el-button type="primary" @click="openDialog()">新建仓库</el-button>
+        </div>
+        <el-button @click="loadData">刷新</el-button>
+      </div>
+    </section>
+
+    <section class="glass-card section-card">
+      <el-table :data="filteredItems" stripe style="width:100%" v-loading="loading">
+        <el-table-column prop="name" label="仓库名称" min-width="140" />
+        <el-table-column prop="type" label="仓库类型" width="110">
+          <template #default="{ row }">
+            <el-tag :type="row.type === '自有仓' ? 'success' : 'warning'" size="small">{{ row.type }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column prop="address" label="地址" min-width="220" show-overflow-tooltip />
+        <el-table-column prop="contact" label="联系人" width="100" />
+        <el-table-column prop="phone" label="联系电话" width="130" />
+        <el-table-column prop="area" label="面积(m²)" width="90" />
+        <el-table-column prop="status" label="状态" width="80">
+          <template #default="{ row }">
+            <el-tag :type="row.status === '启用' ? 'success' : 'danger'" size="small">{{ row.status }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column prop="updatedAt" label="更新时间" width="160" />
+        <el-table-column label="操作" width="180" fixed="right">
+          <template #default="{ row }">
+            <el-button link type="primary" @click="openDialog(row)">编辑</el-button>
+            <el-button link :type="row.status === '启用' ? 'danger' : 'primary'" @click="toggleStatus(row)">{{ row.status === '启用' ? '停用' : '启用' }}</el-button>
+            <el-button link type="primary" @click="openTransfer(row)">调拨</el-button>
+          </template>
+        </el-table-column>
+        <template #empty>
+          <el-empty description="暂无数据" />
+        </template>
+      </el-table>
+    </section>
+
+    <el-dialog v-model="dialogVisible" :title="editingItem ? '编辑仓库' : '新建仓库'" width="560px" destroy-on-close>
+      <el-form :model="formData" label-width="100px">
+        <el-form-item label="仓库名称" required>
+          <el-input v-model="formData.name" placeholder="请输入仓库名称" />
+        </el-form-item>
+        <el-form-item label="仓库类型" required>
+          <el-select v-model="formData.type" placeholder="选择仓库类型" style="width:100%">
+            <el-option label="自有仓" value="自有仓" />
+            <el-option label="第三方仓" value="第三方仓" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="所在地区" required>
+          <el-cascader v-model="formData.region" :options="regionOptions" placeholder="选择省/市/区" style="width:100%" />
+        </el-form-item>
+        <el-form-item label="详细地址" required>
+          <el-input v-model="formData.address" placeholder="详细地址" />
+        </el-form-item>
+        <el-form-item label="联系人">
+          <el-input v-model="formData.contact" placeholder="联系人姓名" />
+        </el-form-item>
+        <el-form-item label="联系电话">
+          <el-input v-model="formData.phone" placeholder="手机号或座机" />
+        </el-form-item>
+        <el-form-item label="仓库面积(m²)">
+          <el-input-number v-model="formData.area" :min="0" style="width:100%" />
+        </el-form-item>
+        <el-form-item label="负责人">
+          <el-input v-model="formData.manager" placeholder="仓库负责人" />
+        </el-form-item>
+        <el-form-item label="备注">
+          <el-input v-model="formData.remark" type="textarea" :rows="2" placeholder="备注信息" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="dialogVisible = false">取消</el-button>
+        <el-button type="primary" @click="saveWarehouse">保存</el-button>
+      </template>
+    </el-dialog>
+
+    <el-dialog v-model="transferDialogVisible" title="仓库调拨" width="500px" destroy-on-close>
+      <el-form :model="transferData" label-width="100px">
+        <el-form-item label="调出仓库">
+          <el-input :value="currentWarehouse?.name" disabled />
+        </el-form-item>
+        <el-form-item label="调入仓库" required>
+          <el-select v-model="transferData.targetWarehouse" placeholder="选择调入仓库" style="width:100%">
+            <el-option v-for="w in warehouseOptions" :key="w.id" :label="w.name" :value="w.id" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="调拨商品" required>
+          <el-select v-model="transferData.sku" placeholder="选择SKU" style="width:100%" filterable>
+            <el-option v-for="s in skuOptions" :key="s.sku" :label="`${s.sku} - ${s.title}`" :value="s.sku" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="调拨数量" required>
+          <el-input-number v-model="transferData.qty" :min="1" style="width:100%" />
+        </el-form-item>
+        <el-form-item label="调拨原因">
+          <el-input v-model="transferData.reason" placeholder="调拨原因" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="transferDialogVisible = false">取消</el-button>
+        <el-button type="primary" @click="submitTransfer">提交调拨</el-button>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { computed, onMounted, reactive, ref } from 'vue';
+import { ElMessage, ElMessageBox } from 'element-plus';
+import type { WarehouseItem } from '@/types/page';
+
+interface WarehouseItem {
+  id: string;
+  name: string;
+  type: string;
+  address: string;
+  contact: string;
+  phone: string;
+  area: number;
+  manager: string;
+  status: string;
+  updatedAt: string;
+}
+
+const items = ref<WarehouseItem[]>([
+  { id: 'W001', name: '深圳南山仓', type: '自有仓', address: '广东省深圳市南山区科技园南路88号', contact: '张明', phone: '138-0013-8000', area: 1200, manager: '张明', status: '启用', updatedAt: '2026-04-15 10:30' },
+  { id: 'W002', name: '义乌商贸仓', type: '第三方仓', address: '浙江省义乌市稠城街道国际商贸城', contact: '李华', phone: '139-5739-5739', area: 2500, manager: '李华', status: '启用', updatedAt: '2026-04-18 14:20' },
+  { id: 'W003', name: '洛杉矶海外仓', type: '第三方仓', address: '1500 S Alamed St, Los Angeles, CA 90015', contact: 'Jack', phone: '+1-213-555-8800', area: 3500, manager: 'Jack', status: '启用', updatedAt: '2026-04-10 09:00' },
+  { id: 'W004', name: '伦敦海外仓', type: '第三方仓', address: '45 Industrial Estate, London E16 2LY', contact: 'Tom', phone: '+44-20-7946-0000', area: 2000, manager: 'Tom', status: '停用', updatedAt: '2026-03-28 16:45' }
+]);
+
+const loading = ref(false);
+const dialogVisible = ref(false);
+const transferDialogVisible = ref(false);
+const editingItem = ref<WarehouseItem | null>(null);
+const currentWarehouse = ref<WarehouseItem | null>(null);
+
+const filters = ref({ name: '', type: '', status: '' });
+
+const formData = reactive({
+  name: '',
+  type: '',
+  region: [] as string[],
+  address: '',
+  contact: '',
+  phone: '',
+  area: 0,
+  manager: '',
+  remark: ''
+});
+
+const transferData = reactive({
+  targetWarehouse: '',
+  sku: '',
+  qty: 1,
+  reason: ''
+});
+
+const regionOptions = [
+  { value: 'guangdong', label: '广东省', children: [{ value: 'shenzhen', label: '深圳市' }, { value: 'guangzhou', label: '广州市' }] },
+  { value: 'zhejiang', label: '浙江省', children: [{ value: 'hangzhou', label: '杭州市' }, { value: 'yiwu', label: '义乌市' }] },
+  { value: 'jiangsu', label: '江苏省', children: [{ value: 'nanjing', label: '南京市' }, { value: 'suzhou', label: '苏州市' }] }
+];
+
+const warehouseOptions = computed(() => items.value.filter(w => w.status === '启用' && w.id !== currentWarehouse.value?.id));
+
+const skuOptions = [
+  { sku: 'SKU-LUGG-20-BLK', title: 'TravelFlex Carry-On / Black' },
+  { sku: 'SKU-BAG-ML-BRW', title: 'Classic Leather Tote / Brown' },
+  { sku: 'SKU-SPRT-YGA-BLU', title: 'Yoga Mat Pro / Blue' }
+];
+
+const filteredItems = computed(() => {
+  return items.value.filter(item => {
+    if (filters.value.name && !item.name.includes(filters.value.name)) return false;
+    if (filters.value.type && item.type !== filters.value.type) return false;
+    if (filters.value.status && item.status !== filters.value.status) return false;
+    return true;
+  });
+});
+
+const loadData = () => {
+  loading.value = true;
+  setTimeout(() => { loading.value = false; }, 300);
+};
+
+const resetFilters = () => {
+  filters.value = { name: '', type: '', status: '' };
+};
+
+const openDialog = (item?: WarehouseItem) => {
+  editingItem.value = item || null;
+  if (item) {
+    Object.assign(formData, {
+      name: item.name,
+      type: item.type,
+      region: [],
+      address: item.address,
+      contact: item.contact,
+      phone: item.phone,
+      area: item.area,
+      manager: item.manager,
+      remark: ''
+    });
+  } else {
+    Object.assign(formData, { name: '', type: '', region: [], address: '', contact: '', phone: '', area: 0, manager: '', remark: '' });
+  }
+  dialogVisible.value = true;
+};
+
+const saveWarehouse = () => {
+  if (!formData.name || !formData.type || !formData.address) {
+    ElMessage.warning('请填写必填项');
+    return;
+  }
+  if (editingItem.value) {
+    const idx = items.value.findIndex(w => w.id === editingItem.value!.id);
+    if (idx !== -1) {
+      items.value[idx] = { ...items.value[idx], ...formData, address: formData.region.join('') + formData.address, updatedAt: new Date().toLocaleString() };
+    }
+    ElMessage.success('仓库已更新');
+  } else {
+    items.value.push({
+      id: `W${String(items.value.length + 1).padStart(3, '0')}`,
+      name: formData.name,
+      type: formData.type,
+      address: formData.region.join('') + formData.address,
+      contact: formData.contact,
+      phone: formData.phone,
+      area: formData.area,
+      manager: formData.manager,
+      status: '启用',
+      updatedAt: new Date().toLocaleString()
+    });
+    ElMessage.success('仓库已创建');
+  }
+  dialogVisible.value = false;
+};
+
+const toggleStatus = async (row: WarehouseItem) => {
+  const action = row.status === '启用' ? '停用' : '启用';
+  await ElMessageBox.confirm(`确认${action}仓库「${row.name}」?`, `${action}确认`);
+  const idx = items.value.findIndex(w => w.id === row.id);
+  if (idx !== -1) {
+    items.value[idx].status = row.status === '启用' ? '停用' : '启用';
+    items.value[idx].updatedAt = new Date().toLocaleString();
+  }
+  ElMessage.success(`仓库已${action}`);
+};
+
+const openTransfer = (row: WarehouseItem) => {
+  currentWarehouse.value = row;
+  Object.assign(transferData, { targetWarehouse: '', sku: '', qty: 1, reason: '' });
+  transferDialogVisible.value = true;
+};
+
+const submitTransfer = async () => {
+  if (!transferData.targetWarehouse || !transferData.sku || !transferData.qty) {
+    ElMessage.warning('请填写完整调拨信息');
+    return;
+  }
+  await ElMessageBox.confirm('确认提交调拨单?', '调拨确认');
+  ElMessage.success('调拨单已提交');
+  transferDialogVisible.value = false;
+};
+
+onMounted(loadData);
+</script>
+
+<style scoped>
+.filter-form :deep(.el-form-item) { margin-bottom: 0; }
+</style>