Browse Source

Update: 添加售后订单和供应商能力视图,优化多个页面功能

docker 2 tháng trước cách đây
mục cha
commit
38d3d81df6

+ 4 - 1
.claude/settings.local.json

@@ -1,7 +1,10 @@
 {
   "permissions": {
     "allow": [
-      "Bash(ls:*)"
+      "Bash(ls:*)",
+      "Bash(wc:*)",
+      "Bash(npx vue-tsc:*)",
+      "Bash(npm run:*)"
     ]
   }
 }

+ 2 - 2
src/router/routes.ts

@@ -68,7 +68,7 @@ export const routes: RouteRecordRaw[] = [
       {
         path: '/order/after-sale',
         name: 'order-after-sale',
-        component: () => import('@/views/shared/SpecOnlyPage.vue'),
+        component: () => import('@/views/order/OrderAfterSaleView.vue'),
         meta: { title: '售后处理', pageKey: 'order-after-sale', roles: ['admin', 'manager', 'operator', 'customer_service'] }
       },
       {
@@ -86,7 +86,7 @@ export const routes: RouteRecordRaw[] = [
       {
         path: '/supplier/capability',
         name: 'supply-capability',
-        component: () => import('@/views/shared/SpecOnlyPage.vue'),
+        component: () => import('@/views/supplier/SupplyCapabilityView.vue'),
         meta: { title: '供货能力配置', pageKey: 'supply-capability', roles: ['admin', 'manager', 'procurement'] }
       },
       {

+ 328 - 21
src/views/channel/ChannelConfigView.vue

@@ -1,5 +1,6 @@
 <template>
   <div class="app-page">
+    <!-- 页面头部 -->
     <section class="glass-card page-hero">
       <div class="page-hero__meta">
         <span class="page-hero__eyebrow">Adapter</span>
@@ -12,39 +13,345 @@
       </div>
     </section>
 
-    <section class="glass-card section-card">
-      <div class="table-toolbar">
-        <div class="chip-list">
-          <el-button type="primary">新增店铺授权</el-button>
-          <el-button>测试连接</el-button>
-          <el-button>查看同步日志</el-button>
-        </div>
+    <!-- 操作栏 -->
+    <section class="glass-card section-card" style="padding:12px 24px">
+      <div class="table-toolbar" style="margin-bottom:0">
+        <el-button type="primary" @click="openCreateDrawer">新增店铺授权</el-button>
       </div>
+    </section>
+
+    <!-- 渠道卡片网格 -->
+    <section class="channel-card-grid">
+      <article
+        v-for="item in items"
+        :key="item.id"
+        class="glass-card channel-card"
+        :class="{ 'channel-card--error': item.errorMessage }"
+      >
+        <div class="channel-card__header">
+          <div class="channel-card__identity">
+            <h3>{{ item.channel }}</h3>
+            <span class="muted">{{ item.shopName }}</span>
+          </div>
+          <div class="channel-card__badges">
+            <el-tag :type="item.tokenStatus === '已授权' ? 'success' : 'danger'" size="small">
+              {{ item.tokenStatus }}
+            </el-tag>
+            <el-tag
+              :type="item.syncStatus === '同步中' ? 'primary' : item.syncStatus === '已暂停' ? 'info' : 'warning'"
+              size="small"
+            >
+              {{ item.syncStatus }}
+            </el-tag>
+          </div>
+        </div>
 
-      <el-table :data="items" stripe style="width: 100%">
-        <el-table-column prop="channel" label="渠道" width="130" />
-        <el-table-column prop="shopName" label="店铺名称" min-width="220" />
-        <el-table-column prop="tokenStatus" label="授权状态" width="120" />
-        <el-table-column prop="syncStatus" label="同步状态" width="120" />
-        <el-table-column prop="lastSyncAt" label="最近同步时间" width="170" />
-        <el-table-column prop="errorMessage" label="异常提示" min-width="220" />
-      </el-table>
+        <div class="channel-card__body">
+          <div class="channel-card__info-row">
+            <span class="muted">最近同步</span>
+            <span>{{ item.lastSyncAt || '--' }}</span>
+          </div>
+          <div v-if="item.errorMessage" class="channel-card__error">
+            <el-icon><WarningFilled /></el-icon>
+            <span>{{ item.errorMessage }}</span>
+          </div>
+        </div>
+
+        <div class="channel-card__actions">
+          <el-button link type="primary" @click="openEditDrawer(item)">编辑配置</el-button>
+          <el-button link type="primary" @click="testConnection(item)">测试连接</el-button>
+          <el-button link type="primary" @click="reauthorize(item)">重新授权</el-button>
+          <el-button
+            link
+            :type="item.syncEnabled ? 'danger' : 'success'"
+            @click="toggleSync(item)"
+          >
+            {{ item.syncEnabled ? '停用同步' : '启用同步' }}
+          </el-button>
+        </div>
+      </article>
     </section>
 
-    <SpecPageRenderer page-key="channel-config" />
+    <!-- 配置抽屉 -->
+    <el-drawer
+      v-model="drawerVisible"
+      :title="isEdit ? '编辑渠道配置' : '新增店铺授权'"
+      size="520px"
+      destroy-on-close
+    >
+      <el-form
+        ref="formRef"
+        :model="formData"
+        :rules="formRules"
+        label-width="120px"
+        label-position="top"
+      >
+        <el-form-item label="店铺名称" prop="shopName">
+          <el-input v-model="formData.shopName" placeholder="请输入店铺名称" />
+        </el-form-item>
+        <el-form-item label="App Key" prop="appKey">
+          <el-input v-model="formData.appKey" placeholder="请输入 App Key" />
+        </el-form-item>
+        <el-form-item label="App Secret" prop="appSecret">
+          <el-input v-model="formData.appSecret" placeholder="请输入 App Secret" show-password />
+        </el-form-item>
+        <el-form-item label="Access Token" prop="accessToken">
+          <el-input v-model="formData.accessToken" placeholder="请输入 Access Token" show-password />
+        </el-form-item>
+        <el-form-item label="Webhook URL">
+          <div style="display:flex;gap:8px;width:100%">
+            <el-input v-model="formData.webhookUrl" readonly />
+            <el-button @click="copyWebhookUrl">复制</el-button>
+          </div>
+        </el-form-item>
+        <el-form-item label="Webhook Secret">
+          <el-input v-model="formData.webhookSecret" placeholder="请输入 Webhook Secret" show-password />
+        </el-form-item>
+        <el-form-item label="启用同步">
+          <el-switch v-model="formData.syncEnabled" />
+        </el-form-item>
+        <el-form-item label="默认仓库">
+          <el-select v-model="formData.defaultWarehouse" placeholder="请选择仓库" style="width:100%">
+            <el-option label="华东仓" value="华东仓" />
+            <el-option label="华南仓" value="华南仓" />
+            <el-option label="海外仓-US" value="海外仓-US" />
+            <el-option label="海外仓-JP" value="海外仓-JP" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="备注">
+          <el-input v-model="formData.remark" type="textarea" :rows="3" placeholder="可选备注" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="drawerVisible = false">取消</el-button>
+        <el-button type="primary" :loading="submitting" @click="handleSubmit">保存</el-button>
+      </template>
+    </el-drawer>
   </div>
 </template>
 
 <script setup lang="ts">
-import { onMounted, ref } from 'vue';
+import { onMounted, ref, reactive } from 'vue';
+import { ElMessage, ElMessageBox } from 'element-plus';
+import { WarningFilled } from '@element-plus/icons-vue';
+import type { FormInstance, FormRules } from 'element-plus';
 import { api } from '@/api/services';
-import SpecPageRenderer from '@/components/SpecPageRenderer.vue';
 import type { ChannelItem } from '@/types/page';
 
 const items = ref<ChannelItem[]>([]);
+const drawerVisible = ref(false);
+const isEdit = ref(false);
+const editingId = ref('');
+const submitting = ref(false);
+const formRef = ref<FormInstance>();
 
-onMounted(async () => {
-  const response = await api.getChannels();
-  items.value = response.items;
+const defaultForm = (): {
+  shopName: string;
+  appKey: string;
+  appSecret: string;
+  accessToken: string;
+  webhookUrl: string;
+  webhookSecret: string;
+  syncEnabled: boolean;
+  defaultWarehouse: string;
+  remark: string;
+} => ({
+  shopName: '',
+  appKey: '',
+  appSecret: '',
+  accessToken: '',
+  webhookUrl: '',
+  webhookSecret: '',
+  syncEnabled: false,
+  defaultWarehouse: '',
+  remark: ''
 });
+
+const formData = reactive(defaultForm());
+
+const formRules: FormRules = {
+  shopName: [{ required: true, message: '请输入店铺名称', trigger: 'blur' }],
+  appKey: [{ required: true, message: '请输入 App Key', trigger: 'blur' }],
+  appSecret: [{ required: true, message: '请输入 App Secret', trigger: 'blur' }],
+  accessToken: [{ required: true, message: '请输入 Access Token', trigger: 'blur' }]
+};
+
+const loadData = async () => {
+  const res = await api.getChannels();
+  items.value = res.items;
+};
+
+const resetForm = () => {
+  Object.assign(formData, defaultForm());
+  isEdit.value = false;
+  editingId.value = '';
+};
+
+const openCreateDrawer = () => {
+  resetForm();
+  drawerVisible.value = true;
+};
+
+const openEditDrawer = (item: ChannelItem) => {
+  resetForm();
+  isEdit.value = true;
+  editingId.value = item.id;
+  Object.assign(formData, {
+    shopName: item.shopName || '',
+    appKey: item.appKey || '',
+    appSecret: item.appSecret || '',
+    accessToken: item.accessToken || '',
+    webhookUrl: item.webhookUrl || '',
+    webhookSecret: item.webhookSecret || '',
+    syncEnabled: item.syncEnabled ?? false,
+    defaultWarehouse: item.defaultWarehouse || '',
+    remark: item.remark || ''
+  });
+  drawerVisible.value = true;
+};
+
+const handleSubmit = async () => {
+  await formRef.value?.validate();
+  submitting.value = true;
+  try {
+    if (isEdit.value) {
+      await api.updateChannel(editingId.value, { ...formData });
+      ElMessage.success('配置已更新');
+    } else {
+      await api.createChannel({ ...formData });
+      ElMessage.success('渠道已创建');
+    }
+    drawerVisible.value = false;
+    loadData();
+  } finally {
+    submitting.value = false;
+  }
+};
+
+const testConnection = async (item: ChannelItem) => {
+  try {
+    await ElMessageBox.confirm(
+      `即将测试「${item.shopName}」的连接是否正常,测试通过后才能启用同步。`,
+      '测试连接',
+      { confirmButtonText: '开始测试', cancelButtonText: '取消', type: 'info' }
+    );
+    // Simulate test - in real app this would call an API
+    ElMessage.success(`「${item.shopName}」连接测试成功`);
+  } catch {
+    // cancelled
+  }
+};
+
+const reauthorize = async (item: ChannelItem) => {
+  try {
+    await ElMessageBox.confirm(
+      `重新授权「${item.shopName}」将跳转到渠道授权页面,请确认。`,
+      '重新授权',
+      { confirmButtonText: '确认授权', cancelButtonText: '取消', type: 'warning' }
+    );
+    ElMessage.info('正在跳转授权页面...');
+  } catch {
+    // cancelled
+  }
+};
+
+const toggleSync = async (item: ChannelItem) => {
+  const action = item.syncEnabled ? '停用' : '启用';
+  try {
+    await ElMessageBox.confirm(
+      `确认${action}「${item.shopName}」的同步?${item.syncEnabled ? '停用后订单和商品将停止同步。' : '启用前请确保连接测试已通过。'}`,
+      `${action}同步`,
+      { confirmButtonText: '确认', cancelButtonText: '取消', type: 'warning' }
+    );
+    await api.updateChannel(item.id, { syncEnabled: !item.syncEnabled });
+    ElMessage.success(`已${action}同步`);
+    loadData();
+  } catch {
+    // cancelled
+  }
+};
+
+const copyWebhookUrl = () => {
+  if (!formData.webhookUrl) {
+    ElMessage.warning('暂无 Webhook URL 可复制');
+    return;
+  }
+  navigator.clipboard?.writeText(formData.webhookUrl).then(() => {
+    ElMessage.success('已复制到剪贴板');
+  }).catch(() => {
+    ElMessage.error('复制失败,请手动复制');
+  });
+};
+
+onMounted(loadData);
 </script>
+
+<style scoped>
+.channel-card-grid {
+  display: grid;
+  grid-template-columns: repeat(auto-fill, minmax(420px, 1fr));
+  gap: 20px;
+}
+
+.channel-card {
+  padding: 20px 24px;
+  display: flex;
+  flex-direction: column;
+  gap: 14px;
+}
+
+.channel-card--error {
+  border-left: 3px solid var(--el-color-danger);
+}
+
+.channel-card__header {
+  display: flex;
+  justify-content: space-between;
+  align-items: flex-start;
+}
+
+.channel-card__identity h3 {
+  margin: 0;
+  font-size: 16px;
+}
+
+.channel-card__badges {
+  display: flex;
+  gap: 6px;
+}
+
+.channel-card__body {
+  display: flex;
+  flex-direction: column;
+  gap: 6px;
+}
+
+.channel-card__info-row {
+  display: flex;
+  justify-content: space-between;
+  font-size: 13px;
+}
+
+.channel-card__error {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  color: var(--el-color-danger);
+  font-size: 13px;
+  background: rgba(245, 108, 108, 0.06);
+  padding: 6px 10px;
+  border-radius: 4px;
+}
+
+.channel-card__actions {
+  display: flex;
+  gap: 4px;
+  border-top: 1px solid var(--el-border-color-lighter);
+  padding-top: 12px;
+}
+
+.muted {
+  color: var(--cb-text-soft);
+  font-size: 13px;
+}
+</style>

+ 74 - 7
src/views/dashboard/ReportDashboardView.vue

@@ -12,14 +12,22 @@
       </div>
     </section>
 
+    <!-- 统计卡片 -->
     <section class="stat-grid">
-      <article v-for="item in overview?.stats" :key="item.label" class="glass-card stat-card">
-        <div class="stat-card__label">{{ item.label }}</div>
-        <div class="stat-card__value">{{ item.value }}</div>
-        <div class="stat-card__trend">{{ item.trend }}</div>
+      <article
+        v-for="stat in overview?.stats"
+        :key="stat.label"
+        class="glass-card stat-card"
+        :class="{ 'stat-card--clickable': isClickableStat(stat.label) }"
+        @click="navigateFromStat(stat.label)"
+      >
+        <div class="stat-card__label">{{ stat.label }}</div>
+        <div class="stat-card__value">{{ stat.value }}</div>
+        <div class="stat-card__trend" :class="trendClass(stat.trend)">{{ stat.trend }}</div>
       </article>
     </section>
 
+    <!-- 预警 + 流程建议 -->
     <section class="page-grid page-grid--two">
       <article class="glass-card section-card">
         <div class="section-card__title">
@@ -46,18 +54,36 @@
         <ul class="tight-list">
           <li v-for="tip in overview?.workflowTips" :key="tip">{{ tip }}</li>
         </ul>
+
+        <!-- 快捷导航 -->
+        <div style="margin-top:24px;border-top:1px solid var(--el-border-color-lighter);padding-top:16px">
+          <h4 style="margin-bottom:12px">快捷导航</h4>
+          <div class="quick-nav">
+            <el-button type="warning" @click="$router.push('/inventory/overview')">
+              低库存商品 → 库存总览
+            </el-button>
+            <el-button type="danger" @click="$router.push('/order/list')">
+              异常订单 → 订单列表
+            </el-button>
+            <el-button @click="$router.push('/shipping/work')">
+              待发货 → 发货作业
+            </el-button>
+            <el-button @click="$router.push('/report/center')">
+              查看报表 → 报表中心
+            </el-button>
+          </div>
+        </div>
       </article>
     </section>
-
-    <SpecPageRenderer page-key="report-dashboard" />
   </div>
 </template>
 
 <script setup lang="ts">
 import { onMounted, ref } from 'vue';
+import { useRouter } from 'vue-router';
 import { api, type DashboardOverviewResponse } from '@/api/services';
-import SpecPageRenderer from '@/components/SpecPageRenderer.vue';
 
+const router = useRouter();
 const overview = ref<DashboardOverviewResponse>();
 
 const timelineType = (level: string) => {
@@ -66,7 +92,48 @@ const timelineType = (level: string) => {
   return 'primary';
 };
 
+const trendClass = (trend: string) => {
+  if (!trend) return '';
+  if (trend.includes('↑') || trend.includes('+')) return 'trend-up';
+  if (trend.includes('↓') || trend.includes('-')) return 'trend-down';
+  return '';
+};
+
+const isClickableStat = (label: string) => {
+  return label === '低库存商品数' || label === '异常订单数';
+};
+
+const navigateFromStat = (label: string) => {
+  if (label === '低库存商品数') {
+    router.push('/inventory/overview');
+  } else if (label === '异常订单数') {
+    router.push('/order/list');
+  }
+};
+
 onMounted(async () => {
   overview.value = await api.getDashboardOverview();
 });
 </script>
+
+<style scoped>
+.stat-card--clickable {
+  cursor: pointer;
+  transition: transform 0.15s ease, box-shadow 0.15s ease;
+}
+
+.stat-card--clickable:hover {
+  transform: translateY(-2px);
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+}
+
+.trend-up { color: var(--el-color-success); }
+.trend-down { color: var(--el-color-danger); }
+.muted { color: var(--cb-text-soft); font-size: 13px; }
+
+.quick-nav {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 10px;
+}
+</style>

+ 263 - 10
src/views/inventory/InventoryOverviewView.vue

@@ -1,5 +1,6 @@
 <template>
   <div class="app-page">
+    <!-- 页面头部 -->
     <section class="glass-card page-hero">
       <div class="page-hero__meta">
         <span class="page-hero__eyebrow">Inventory</span>
@@ -12,38 +13,290 @@
       </div>
     </section>
 
+    <!-- 筛选区 -->
     <section class="glass-card section-card">
-      <el-table :data="items" stripe style="width: 100%">
+      <el-form :model="filters" inline class="filter-form">
+        <el-form-item label="SKU">
+          <el-input v-model="filters.sku" placeholder="请输入 SKU" clearable style="width:180px" @keyup.enter="loadData" />
+        </el-form-item>
+        <el-form-item label="仓库">
+          <el-select v-model="filters.warehouse" placeholder="全部仓库" clearable style="width:140px">
+            <el-option label="华东仓" value="华东仓" />
+            <el-option label="华南仓" value="华南仓" />
+            <el-option label="海外仓-US" value="海外仓-US" />
+            <el-option label="海外仓-JP" value="海外仓-JP" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="渠道">
+          <el-select v-model="filters.channel" placeholder="全部渠道" clearable style="width:140px">
+            <el-option label="Shopify US" value="Shopify US" />
+            <el-option label="Shopify JP" value="Shopify JP" />
+            <el-option label="TikTok UK" value="TikTok UK" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="预警状态">
+          <el-select v-model="filters.warningStatus" placeholder="全部" clearable style="width:120px">
+            <el-option label="正常" value="正常" />
+            <el-option label="低于安全库存" value="低于安全库存" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="库存状态">
+          <el-select v-model="filters.inventoryStatus" 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="openAdjustDialog">手动调整</el-button>
+          <el-button @click="createReplenishment">创建补货建议</el-button>
+          <el-button @click="doExport">导出</el-button>
+        </div>
+      </div>
+    </section>
+
+    <!-- 库存表格 -->
+    <section class="glass-card section-card">
+      <el-table
+        :data="filteredItems"
+        stripe
+        style="width:100%"
+        :row-class-name="rowClass"
+      >
         <el-table-column prop="sku" label="SKU" width="180" />
         <el-table-column prop="productTitle" label="商品标题" min-width="250" />
         <el-table-column prop="warehouse" label="仓库" width="110" />
-        <el-table-column prop="available" label="可用库存" width="110" />
+        <el-table-column label="可用库存" width="110">
+          <template #default="{ row }">
+            <el-button link type="primary" @click="openDetailDrawer(row)">{{ row.available }}</el-button>
+          </template>
+        </el-table-column>
         <el-table-column prop="locked" label="锁定库存" width="110" />
         <el-table-column prop="inbound" label="在途库存" width="110" />
         <el-table-column prop="safeStock" label="安全库存" width="110" />
         <el-table-column label="预警状态" width="140">
           <template #default="{ row }">
-            <el-tag :type="row.warningStatus === '正常' ? 'success' : 'danger'">{{ row.warningStatus }}</el-tag>
+            <el-tag :type="row.warningStatus === '正常' ? 'success' : 'danger'" size="small">
+              {{ row.warningStatus }}
+            </el-tag>
           </template>
         </el-table-column>
         <el-table-column prop="lastChangeAt" label="最近变动时间" width="170" />
+        <el-table-column label="操作" width="140" fixed="right">
+          <template #default="{ row }">
+            <el-button link type="primary" @click="openDetailDrawer(row)">明细</el-button>
+            <el-button link type="primary" @click="openAdjustFor(row)">调整</el-button>
+          </template>
+        </el-table-column>
       </el-table>
     </section>
 
-    <SpecPageRenderer page-key="inventory-overview" />
+    <!-- 库存明细抽屉 -->
+    <el-drawer
+      v-model="detailVisible"
+      :title="`库存明细 — ${detailItem?.sku || ''}`"
+      size="560px"
+      destroy-on-close
+    >
+      <template v-if="detailItem">
+        <el-descriptions :column="2" border size="small" style="margin-bottom:20px">
+          <el-descriptions-item label="商品标题" :span="2">{{ detailItem.productTitle }}</el-descriptions-item>
+          <el-descriptions-item label="仓库">{{ detailItem.warehouse }}</el-descriptions-item>
+          <el-descriptions-item label="预警状态">
+            <el-tag :type="detailItem.warningStatus === '正常' ? 'success' : 'danger'" size="small">
+              {{ detailItem.warningStatus }}
+            </el-tag>
+          </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.safeStock }}</el-descriptions-item>
+        </el-descriptions>
+
+        <h4 style="margin-bottom:12px">库存变动记录</h4>
+        <el-table :data="logs" stripe size="small">
+          <el-table-column prop="time" label="时间" width="160" />
+          <el-table-column prop="source" label="来源" width="100" />
+          <el-table-column prop="quantity" label="变动数量" width="100">
+            <template #default="{ row }">
+              <span :style="{ color: row.quantity > 0 ? 'var(--el-color-success)' : 'var(--el-color-danger)' }">
+                {{ row.quantity > 0 ? '+' : '' }}{{ row.quantity }}
+              </span>
+            </template>
+          </el-table-column>
+          <el-table-column prop="afterQty" label="变动后数量" width="110" />
+          <el-table-column prop="relatedOrder" label="关联单号" min-width="140" />
+          <el-table-column prop="operator" label="操作人" width="100" />
+        </el-table>
+      </template>
+    </el-drawer>
+
+    <!-- 手动调整对话框 -->
+    <el-dialog
+      v-model="adjustVisible"
+      title="手动库存调整"
+      width="480px"
+      destroy-on-close
+    >
+      <el-form ref="adjustFormRef" :model="adjustForm" :rules="adjustRules" label-width="100px">
+        <el-form-item label="SKU">
+          <el-input :model-value="adjustTarget?.sku" disabled />
+        </el-form-item>
+        <el-form-item label="仓库">
+          <el-input :model-value="adjustTarget?.warehouse" disabled />
+        </el-form-item>
+        <el-form-item label="当前库存">
+          <el-input :model-value="String(adjustTarget?.available ?? '')" disabled />
+        </el-form-item>
+        <el-form-item label="调整数量" prop="quantity">
+          <el-input-number v-model="adjustForm.quantity" :step="1" style="width:100%" />
+          <div class="muted" style="margin-top:4px">正数为增加,负数为扣减</div>
+        </el-form-item>
+        <el-form-item label="调整原因" prop="reason">
+          <el-input v-model="adjustForm.reason" type="textarea" :rows="3" placeholder="请输入调整原因(必填)" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="adjustVisible = false">取消</el-button>
+        <el-button type="primary" :loading="adjusting" @click="submitAdjust">确认调整</el-button>
+      </template>
+    </el-dialog>
   </div>
 </template>
 
 <script setup lang="ts">
-import { onMounted, ref } from 'vue';
+import { computed, onMounted, ref, reactive } from 'vue';
+import { ElMessage, ElMessageBox } from 'element-plus';
+import type { FormInstance, FormRules } from 'element-plus';
 import { api } from '@/api/services';
-import SpecPageRenderer from '@/components/SpecPageRenderer.vue';
-import type { InventoryItem } from '@/types/page';
+import type { InventoryItem, InventoryLogItem } from '@/types/page';
 
 const items = ref<InventoryItem[]>([]);
+const logs = ref<InventoryLogItem[]>([]);
+
+const filters = ref({
+  sku: '',
+  warehouse: '',
+  channel: '',
+  warningStatus: '',
+  inventoryStatus: ''
+});
 
-onMounted(async () => {
-  const response = await api.getInventory();
-  items.value = response.items;
+const filteredItems = computed(() => {
+  return items.value.filter((item) => {
+    if (filters.value.sku && !item.sku.toLowerCase().includes(filters.value.sku.toLowerCase())) return false;
+    if (filters.value.warehouse && item.warehouse !== filters.value.warehouse) return false;
+    if (filters.value.warningStatus && item.warningStatus !== filters.value.warningStatus) return false;
+    if (filters.value.inventoryStatus) {
+      if (filters.value.inventoryStatus === '有库存' && item.available <= 0) return false;
+      if (filters.value.inventoryStatus === '零库存' && item.available !== 0) return false;
+      if (filters.value.inventoryStatus === '负库存' && item.available >= 0) return false;
+    }
+    return true;
+  });
 });
+
+const rowClass = ({ row }: { row: InventoryItem }) => {
+  return row.warningStatus !== '正常' ? 'warning-row' : '';
+};
+
+/* ---- 数据加载 ---- */
+const loadData = async () => {
+  const res = await api.getInventory();
+  items.value = res.items;
+};
+
+const resetFilters = () => {
+  filters.value = { sku: '', warehouse: '', channel: '', warningStatus: '', inventoryStatus: '' };
+};
+
+/* ---- 库存明细抽屉 ---- */
+const detailVisible = ref(false);
+const detailItem = ref<InventoryItem | null>(null);
+
+const openDetailDrawer = async (item: InventoryItem) => {
+  detailItem.value = item;
+  detailVisible.value = true;
+  const logRes = await api.getInventoryLogs();
+  logs.value = logRes.items;
+};
+
+/* ---- 手动调整 ---- */
+const adjustVisible = ref(false);
+const adjustTarget = ref<InventoryItem | null>(null);
+const adjusting = ref(false);
+const adjustFormRef = ref<FormInstance>();
+
+const adjustForm = reactive({ quantity: 0, reason: '' });
+
+const adjustRules: FormRules = {
+  quantity: [{ required: true, message: '请输入调整数量', trigger: 'blur' }],
+  reason: [{ required: true, message: '请输入调整原因', trigger: 'blur' }]
+};
+
+const openAdjustDialog = () => {
+  adjustTarget.value = null;
+  adjustForm.quantity = 0;
+  adjustForm.reason = '';
+  adjustVisible.value = true;
+};
+
+const openAdjustFor = (item: InventoryItem) => {
+  adjustTarget.value = item;
+  adjustForm.quantity = 0;
+  adjustForm.reason = '';
+  adjustVisible.value = true;
+};
+
+const submitAdjust = async () => {
+  await adjustFormRef.value?.validate();
+  if (!adjustTarget.value) {
+    ElMessage.warning('请选择要调整的库存行');
+    return;
+  }
+  adjusting.value = true;
+  try {
+    await api.updateInventory(adjustTarget.value.id, {
+      available: adjustTarget.value.available + adjustForm.quantity
+    } as Partial<InventoryItem>);
+    ElMessage.success('库存已调整');
+    adjustVisible.value = false;
+    loadData();
+  } finally {
+    adjusting.value = false;
+  }
+};
+
+/* ---- 补货建议 ---- */
+const createReplenishment = () => {
+  const lowStockItems = filteredItems.value.filter((i) => i.warningStatus !== '正常');
+  if (lowStockItems.length === 0) {
+    ElMessage.info('当前无低于安全库存的商品');
+    return;
+  }
+  ElMessage.success(`已为 ${lowStockItems.length} 个低于安全库存的 SKU 创建补货建议`);
+};
+
+/* ---- 导出 ---- */
+const doExport = () => {
+  ElMessage.info('导出已开始,完成后将自动下载');
+};
+
+onMounted(loadData);
 </script>
+
+<style scoped>
+.filter-form :deep(.el-form-item) { margin-bottom: 0; }
+:deep(.warning-row) { background-color: rgba(245, 108, 108, 0.06) !important; }
+.muted { color: var(--cb-text-soft); font-size: 13px; }
+</style>

+ 276 - 11
src/views/inventory/ShippingWorkView.vue

@@ -1,5 +1,6 @@
 <template>
   <div class="app-page">
+    <!-- 页面头部 -->
     <section class="glass-card page-hero">
       <div class="page-hero__meta">
         <span class="page-hero__eyebrow">Fulfillment</span>
@@ -12,33 +13,297 @@
       </div>
     </section>
 
+    <!-- 筛选区 -->
     <section class="glass-card section-card">
-      <el-table :data="items" stripe style="width: 100%">
+      <el-form :model="filters" inline class="filter-form">
+        <el-form-item label="发货单号">
+          <el-input v-model="filters.shipmentNo" placeholder="请输入发货单号" clearable style="width:170px" @keyup.enter="loadData" />
+        </el-form-item>
+        <el-form-item label="订单号">
+          <el-input v-model="filters.orderNo" placeholder="请输入订单号" clearable style="width:170px" @keyup.enter="loadData" />
+        </el-form-item>
+        <el-form-item label="仓库">
+          <el-select v-model="filters.warehouse" placeholder="全部仓库" clearable style="width:140px">
+            <el-option label="华东仓" value="华东仓" />
+            <el-option label="华南仓" value="华南仓" />
+            <el-option label="海外仓-US" value="海外仓-US" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="发货状态">
+          <el-select v-model="filters.shippingStatus" 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.returnStatus" 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.createTimeRange"
+            type="daterange"
+            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="openTrackingDialog(null)">批量录入运单号</el-button>
+        </div>
+      </div>
+    </section>
+
+    <!-- 发货表格 -->
+    <section class="glass-card section-card">
+      <el-table :data="filteredItems" stripe style="width:100%">
         <el-table-column prop="shipmentNo" label="发货单号" width="170" />
         <el-table-column prop="orderNo" label="订单号" width="180" />
         <el-table-column prop="warehouse" label="仓库" width="110" />
-        <el-table-column prop="packageCount" label="包裹数" width="90" />
-        <el-table-column prop="carrier" label="物流公司" width="120" />
-        <el-table-column prop="shippingStatus" label="发货状态" width="110" />
-        <el-table-column prop="returnStatus" label="回传状态" width="110" />
+        <el-table-column prop="skuCount" label="SKU 数量" width="100" />
+        <el-table-column prop="expectedQty" label="预期数量" width="100" />
+        <el-table-column prop="actualQty" label="实际数量" width="100" />
+        <el-table-column prop="carrier" label="物流公司" width="120">
+          <template #default="{ row }">
+            {{ row.carrier || '--' }}
+          </template>
+        </el-table-column>
+        <el-table-column prop="trackingNo" label="运单号" width="160">
+          <template #default="{ row }">
+            {{ row.trackingNo || '--' }}
+          </template>
+        </el-table-column>
+        <el-table-column label="发货状态" width="110">
+          <template #default="{ row }">
+            <el-tag :type="shippingStatusType(row.shippingStatus)" size="small">
+              {{ row.shippingStatus }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="回传状态" width="110">
+          <template #default="{ row }">
+            <el-tag :type="returnStatusType(row.returnStatus)" size="small">
+              {{ row.returnStatus }}
+            </el-tag>
+          </template>
+        </el-table-column>
         <el-table-column prop="createdAt" label="创建时间" width="170" />
+        <el-table-column label="操作" width="220" fixed="right">
+          <template #default="{ row }">
+            <el-button link type="primary" @click="openTrackingDialog(row)">录入运单</el-button>
+            <el-button
+              link
+              type="primary"
+              :disabled="row.shippingStatus === '已发货' || !row.trackingNo"
+              @click="confirmShipment(row)"
+            >
+              确认发货
+            </el-button>
+            <el-button
+              v-if="row.returnStatus === '回传失败'"
+              link
+              type="warning"
+              @click="retryReturn(row)"
+            >
+              重试回传
+            </el-button>
+            <el-button link type="primary" @click="printLabel(row)">打印面单</el-button>
+          </template>
+        </el-table-column>
       </el-table>
     </section>
 
-    <SpecPageRenderer page-key="shipping-work" />
+    <!-- 录入运单号对话框 -->
+    <el-dialog
+      v-model="trackingVisible"
+      :title="trackingTarget ? `录入运单号 — ${trackingTarget.shipmentNo}` : '批量录入运单号'"
+      width="500px"
+      destroy-on-close
+    >
+      <el-form ref="trackingFormRef" :model="trackingForm" :rules="trackingRules" label-width="100px">
+        <el-form-item v-if="trackingTarget" label="发货单号">
+          <el-input :model-value="trackingTarget.shipmentNo" disabled />
+        </el-form-item>
+        <el-form-item label="物流公司" prop="carrier">
+          <el-select v-model="trackingForm.carrier" placeholder="请选择物流公司" style="width:100%">
+            <el-option label="顺丰速运" value="顺丰速运" />
+            <el-option label="中通快递" value="中通快递" />
+            <el-option label="圆通速递" value="圆通速递" />
+            <el-option label="DHL" value="DHL" />
+            <el-option label="FedEx" value="FedEx" />
+            <el-option label="UPS" value="UPS" />
+            <el-option label="Japan Post" value="Japan Post" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="运单号" prop="trackingNo">
+          <el-input v-model="trackingForm.trackingNo" placeholder="请输入运单号" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="trackingVisible = false">取消</el-button>
+        <el-button type="primary" :loading="submittingTracking" @click="submitTracking">确认录入</el-button>
+      </template>
+    </el-dialog>
   </div>
 </template>
 
 <script setup lang="ts">
-import { onMounted, ref } from 'vue';
+import { computed, onMounted, ref, reactive } from 'vue';
+import { ElMessage, ElMessageBox } from 'element-plus';
+import type { FormInstance, FormRules } from 'element-plus';
 import { api } from '@/api/services';
-import SpecPageRenderer from '@/components/SpecPageRenderer.vue';
 import type { ShippingItem } from '@/types/page';
 
 const items = ref<ShippingItem[]>([]);
+const submittingTracking = ref(false);
+const trackingVisible = ref(false);
+const trackingTarget = ref<ShippingItem | null>(null);
+const trackingFormRef = ref<FormInstance>();
 
-onMounted(async () => {
-  const response = await api.getShippingOrders();
-  items.value = response.items;
+const trackingForm = reactive({ carrier: '', trackingNo: '' });
+
+const trackingRules: FormRules = {
+  carrier: [{ required: true, message: '请选择物流公司', trigger: 'change' }],
+  trackingNo: [{ required: true, message: '请输入运单号', trigger: 'blur' }]
+};
+
+const filters = ref({
+  shipmentNo: '',
+  orderNo: '',
+  warehouse: '',
+  shippingStatus: '',
+  returnStatus: '',
+  createTimeRange: null as [Date, Date] | null
 });
+
+const filteredItems = computed(() => {
+  return items.value.filter((item) => {
+    if (filters.value.shipmentNo && !item.shipmentNo.includes(filters.value.shipmentNo)) return false;
+    if (filters.value.orderNo && !item.orderNo.includes(filters.value.orderNo)) return false;
+    if (filters.value.warehouse && item.warehouse !== filters.value.warehouse) return false;
+    if (filters.value.shippingStatus && item.shippingStatus !== filters.value.shippingStatus) return false;
+    if (filters.value.returnStatus && item.returnStatus !== filters.value.returnStatus) return false;
+    return true;
+  });
+});
+
+const shippingStatusType = (status: string) => {
+  const map: Record<string, string> = { '待拣货': 'info', '待发货': 'warning', '已发货': 'success' };
+  return map[status] || '';
+};
+
+const returnStatusType = (status: string) => {
+  const map: Record<string, string> = { '未回传': 'info', '已回传': 'success', '回传失败': 'danger' };
+  return map[status] || '';
+};
+
+const loadData = async () => {
+  const res = await api.getShippingOrders();
+  items.value = res.items;
+};
+
+const resetFilters = () => {
+  filters.value = {
+    shipmentNo: '',
+    orderNo: '',
+    warehouse: '',
+    shippingStatus: '',
+    returnStatus: '',
+    createTimeRange: null
+  };
+};
+
+const openTrackingDialog = (item: ShippingItem | null) => {
+  trackingTarget.value = item;
+  trackingForm.carrier = item?.carrier || '';
+  trackingForm.trackingNo = item?.trackingNo || '';
+  trackingVisible.value = true;
+};
+
+const submitTracking = async () => {
+  await trackingFormRef.value?.validate();
+  if (!trackingTarget.value) {
+    ElMessage.warning('请从列表中选择需要录入运单号的发货单');
+    return;
+  }
+  submittingTracking.value = true;
+  try {
+    await api.updateShippingOrder(trackingTarget.value.id, {
+      carrier: trackingForm.carrier,
+      trackingNo: trackingForm.trackingNo,
+      shippingStatus: '待发货'
+    } as Partial<ShippingItem>);
+    ElMessage.success('运单号已录入');
+    trackingVisible.value = false;
+    loadData();
+  } finally {
+    submittingTracking.value = false;
+  }
+};
+
+const confirmShipment = async (item: ShippingItem) => {
+  if (!item.carrier || !item.trackingNo) {
+    ElMessage.warning('请先录入物流公司和运单号后再确认发货');
+    return;
+  }
+  try {
+    await ElMessageBox.confirm(
+      `确认发货单「${item.shipmentNo}」已发出?`,
+      '确认发货',
+      { confirmButtonText: '确认', cancelButtonText: '取消', type: 'warning' }
+    );
+    await api.updateShippingOrder(item.id, {
+      shippingStatus: '已发货'
+    } as Partial<ShippingItem>);
+    ElMessage.success('已确认发货');
+    loadData();
+  } catch {
+    // cancelled
+  }
+};
+
+const retryReturn = async (item: ShippingItem) => {
+  try {
+    await ElMessageBox.confirm(
+      `重试回传发货单「${item.shipmentNo}」的物流信息?`,
+      '重试回传',
+      { confirmButtonText: '重试', cancelButtonText: '取消', type: 'info' }
+    );
+    await api.updateShippingOrder(item.id, {
+      returnStatus: '已回传'
+    } as Partial<ShippingItem>);
+    ElMessage.success('回传成功');
+    loadData();
+  } catch {
+    // cancelled
+  }
+};
+
+const printLabel = (item: ShippingItem) => {
+  if (!item.trackingNo) {
+    ElMessage.warning('请先录入运单号');
+    return;
+  }
+  ElMessage.info(`正在生成「${item.shipmentNo}」的面单,请稍候...`);
+};
+
+onMounted(loadData);
 </script>
+
+<style scoped>
+.filter-form :deep(.el-form-item) { margin-bottom: 0; }
+</style>

+ 183 - 0
src/views/order/OrderAfterSaleView.vue

@@ -0,0 +1,183 @@
+<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.afterSaleNo" placeholder="AS-xxx" clearable style="width:170px" @keyup.enter="loadData" />
+        </el-form-item>
+        <el-form-item label="订单号">
+          <el-input v-model="filters.orderNo" placeholder="OMS-xxx" clearable style="width:180px" />
+        </el-form-item>
+        <el-form-item label="售后类型">
+          <el-select v-model="filters.type" placeholder="全部" clearable style="width:110px">
+            <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.auditStatus" placeholder="全部" clearable style="width:110px">
+            <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">
+      <el-table :data="filteredItems" stripe style="width:100%">
+        <el-table-column prop="afterSaleNo" label="售后单号" width="180" />
+        <el-table-column prop="orderNo" label="订单号" width="190">
+          <template #default="{ row }">
+            <el-button link type="primary" @click="$router.push(`/order/detail?id=${row.orderNo}`)">{{ row.orderNo }}</el-button>
+          </template>
+        </el-table-column>
+        <el-table-column prop="buyer" label="买家" width="130" />
+        <el-table-column prop="type" label="类型" width="100">
+          <template #default="{ row }">
+            <el-tag :type="row.type === '退款' ? 'danger' : row.type === '换货' ? 'warning' : ''" size="small">{{ row.type }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column prop="amount" label="申请金额" width="100" />
+        <el-table-column prop="reason" label="原因" min-width="150" />
+        <el-table-column prop="auditStatus" label="审核" width="90">
+          <template #default="{ row }">
+            <el-tag :type="row.auditStatus === '已通过' ? 'success' : row.auditStatus === '已拒绝' ? 'danger' : 'warning'" size="small">{{ row.auditStatus }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column prop="refundStatus" label="退款状态" width="100">
+          <template #default="{ row }">
+            <el-tag :type="row.refundStatus === '已退款' ? 'success' : row.refundStatus === '补发中' ? 'warning' : 'info'" size="small">{{ row.refundStatus }}</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 }">
+            <template v-if="row.auditStatus === '待审核'">
+              <el-button link type="primary" @click="openAudit(row, 'approve')">通过</el-button>
+              <el-button link type="danger" @click="openAudit(row, 'reject')">拒绝</el-button>
+            </template>
+            <el-button link type="primary" v-if="row.auditStatus === '已通过' && row.type === '退款' && row.refundStatus === '未退款'" @click="doRefund(row)">发起退款</el-button>
+            <el-button link @click="openDetail(row)">详情</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+    </section>
+
+    <!-- 审核弹窗 -->
+    <el-dialog v-model="auditDialog" :title="auditAction === 'approve' ? '审核通过' : '审核拒绝'" width="500px">
+      <el-descriptions :column="1" border style="margin-bottom:16px">
+        <el-descriptions-item label="售后单号">{{ auditItem?.afterSaleNo }}</el-descriptions-item>
+        <el-descriptions-item label="售后类型">{{ auditItem?.type }}</el-descriptions-item>
+        <el-descriptions-item label="申请金额">{{ auditItem?.amount }}</el-descriptions-item>
+        <el-descriptions-item label="申请原因">{{ auditItem?.reason }}</el-descriptions-item>
+      </el-descriptions>
+      <el-form label-position="top">
+        <el-form-item :label="auditAction === 'reject' ? '拒绝原因(必填)' : '审核备注'">
+          <el-input v-model="auditRemark" type="textarea" :rows="3" :placeholder="auditAction === 'reject' ? '请填写拒绝原因' : '可选'" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="auditDialog = false">取消</el-button>
+        <el-button :type="auditAction === 'approve' ? 'primary' : 'danger'" @click="confirmAudit">确认</el-button>
+      </template>
+    </el-dialog>
+
+    <!-- 详情抽屉 -->
+    <el-drawer v-model="detailDrawer" title="售后详情" size="480px">
+      <template v-if="detailItem">
+        <el-descriptions :column="1" border>
+          <el-descriptions-item label="售后单号">{{ detailItem.afterSaleNo }}</el-descriptions-item>
+          <el-descriptions-item label="关联订单">{{ detailItem.orderNo }}</el-descriptions-item>
+          <el-descriptions-item label="买家">{{ detailItem.buyer }}</el-descriptions-item>
+          <el-descriptions-item label="售后类型">{{ detailItem.type }}</el-descriptions-item>
+          <el-descriptions-item label="申请金额">{{ detailItem.amount }}</el-descriptions-item>
+          <el-descriptions-item label="原因">{{ detailItem.reason }}</el-descriptions-item>
+          <el-descriptions-item label="审核状态">{{ detailItem.auditStatus }}</el-descriptions-item>
+          <el-descriptions-item label="退款状态">{{ detailItem.refundStatus }}</el-descriptions-item>
+          <el-descriptions-item label="更新时间">{{ detailItem.updatedAt }}</el-descriptions-item>
+        </el-descriptions>
+      </template>
+    </el-drawer>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { computed, onMounted, ref } from 'vue';
+import { ElMessage, ElMessageBox } from 'element-plus';
+import { api } from '@/api/services';
+import type { AfterSaleItem } from '@/types/page';
+
+const items = ref<AfterSaleItem[]>([]);
+const auditDialog = ref(false);
+const detailDrawer = ref(false);
+const auditAction = ref<'approve' | 'reject'>('approve');
+const auditItem = ref<AfterSaleItem | null>(null);
+const detailItem = ref<AfterSaleItem | null>(null);
+const auditRemark = ref('');
+
+const filters = ref({ afterSaleNo: '', orderNo: '', type: '', auditStatus: '' });
+
+const filteredItems = computed(() => {
+  return items.value.filter((item) => {
+    if (filters.value.afterSaleNo && !item.afterSaleNo.includes(filters.value.afterSaleNo)) return false;
+    if (filters.value.orderNo && !item.orderNo.includes(filters.value.orderNo)) return false;
+    if (filters.value.type && item.type !== filters.value.type) return false;
+    if (filters.value.auditStatus && item.auditStatus !== filters.value.auditStatus) return false;
+    return true;
+  });
+});
+
+const loadData = async () => {
+  const res = await api.getAfterSales();
+  items.value = res.items;
+};
+
+const resetFilters = () => {
+  filters.value = { afterSaleNo: '', orderNo: '', type: '', auditStatus: '' };
+};
+
+const openAudit = (item: AfterSaleItem, action: 'approve' | 'reject') => {
+  auditItem.value = item;
+  auditAction.value = action;
+  auditRemark.value = '';
+  auditDialog.value = true;
+};
+
+const confirmAudit = async () => {
+  if (auditAction.value === 'reject' && !auditRemark.value) {
+    ElMessage.warning('拒绝售后必须填写原因');
+    return;
+  }
+  const newStatus = auditAction.value === 'approve' ? '已通过' : '已拒绝';
+  await api.updateAfterSale(auditItem.value!.id, { auditStatus: newStatus } as Partial<AfterSaleItem>);
+  auditDialog.value = false;
+  ElMessage.success(`已${auditAction.value === 'approve' ? '通过' : '拒绝'}审核`);
+  loadData();
+};
+
+const doRefund = async (row: AfterSaleItem) => {
+  await ElMessageBox.confirm(`确认对 ${row.afterSaleNo} 发起退款 ${row.amount}?`);
+  await api.updateAfterSale(row.id, { refundStatus: '已退款' } as Partial<AfterSaleItem>);
+  ElMessage.success('退款已发起');
+  loadData();
+};
+
+const openDetail = (item: AfterSaleItem) => {
+  detailItem.value = item;
+  detailDrawer.value = true;
+};
+
+onMounted(loadData);
+</script>
+
+<style scoped>
+.filter-form :deep(.el-form-item) { margin-bottom: 0; }
+</style>

+ 1 - 1
src/views/order/OrderDetailView.vue

@@ -11,7 +11,7 @@
       <div class="stat-grid" style="margin-top:16px">
         <article class="stat-card">
           <div class="stat-card__label">订单状态</div>
-          <div class="stat-card__value" style="font-size:22px">{{ statusLabel(order.orderStatus) }}</div>
+          <div class="stat-card__value" style="font-size:22px">{{ statusLabel(order.orderStatus ?? '') }}</div>
         </article>
         <article class="stat-card">
           <div class="stat-card__label">支付金额</div>

+ 251 - 18
src/views/report/ReportCenterView.vue

@@ -1,5 +1,6 @@
 <template>
   <div class="app-page">
+    <!-- 页面头部 -->
     <section class="glass-card page-hero">
       <div class="page-hero__meta">
         <span class="page-hero__eyebrow">BI Center</span>
@@ -12,46 +13,278 @@
       </div>
     </section>
 
+    <!-- 报表类型与筛选 -->
     <section class="glass-card section-card">
       <div class="table-toolbar">
-        <el-form inline>
+        <el-form inline class="filter-form">
           <el-form-item label="报表类型">
-            <el-select model-value="渠道销售日报" style="width: 170px">
-              <el-option label="渠道销售日报" value="渠道销售日报" />
+            <el-select v-model="reportType" placeholder="请选择报表类型" style="width:170px" @change="onReportTypeChange">
+              <el-option label="渠道报表" value="channel" />
+              <el-option label="SKU 报表" value="sku" />
+              <el-option label="履约报表" value="fulfillment" />
+              <el-option label="自定义报表" value="custom" />
             </el-select>
           </el-form-item>
+
+          <!-- 时间范围:所有类型都有 -->
           <el-form-item label="时间范围">
-            <el-date-picker type="daterange" start-placeholder="开始日期" end-placeholder="结束日期" />
+            <el-date-picker
+              v-model="filters.timeRange"
+              type="daterange"
+              start-placeholder="开始日期"
+              end-placeholder="结束日期"
+              style="width:240px"
+            />
           </el-form-item>
+
+          <!-- 渠道报表筛选 -->
+          <template v-if="reportType === 'channel'">
+            <el-form-item label="渠道">
+              <el-select v-model="filters.channel" placeholder="全部渠道" clearable style="width:140px">
+                <el-option label="Shopify US" value="Shopify US" />
+                <el-option label="Shopify JP" value="Shopify JP" />
+                <el-option label="TikTok UK" value="TikTok UK" />
+              </el-select>
+            </el-form-item>
+            <el-form-item label="店铺">
+              <el-select v-model="filters.shop" placeholder="全部店铺" clearable style="width:140px">
+                <el-option label="旗舰店" value="旗舰店" />
+                <el-option label="折扣店" value="折扣店" />
+              </el-select>
+            </el-form-item>
+          </template>
+
+          <!-- SKU 报表筛选 -->
+          <template v-if="reportType === 'sku'">
+            <el-form-item label="SKU">
+              <el-input v-model="filters.sku" placeholder="请输入 SKU" clearable style="width:160px" />
+            </el-form-item>
+            <el-form-item label="供应商">
+              <el-select v-model="filters.supplier" placeholder="全部供应商" clearable style="width:140px">
+                <el-option label="供应商 A" value="供应商 A" />
+                <el-option label="供应商 B" value="供应商 B" />
+              </el-select>
+            </el-form-item>
+          </template>
+
+          <!-- 履约报表筛选 -->
+          <template v-if="reportType === 'fulfillment'">
+            <el-form-item label="仓库">
+              <el-select v-model="filters.warehouse" placeholder="全部仓库" clearable style="width:140px">
+                <el-option label="华东仓" value="华东仓" />
+                <el-option label="华南仓" value="华南仓" />
+                <el-option label="海外仓-US" value="海外仓-US" />
+              </el-select>
+            </el-form-item>
+            <el-form-item label="国家">
+              <el-select v-model="filters.country" placeholder="全部国家" clearable style="width:140px">
+                <el-option label="美国" value="US" />
+                <el-option label="日本" value="JP" />
+                <el-option label="英国" value="UK" />
+              </el-select>
+            </el-form-item>
+          </template>
         </el-form>
+
         <div class="chip-list">
-          <el-button type="primary">生成报表</el-button>
-          <el-button>导出 Excel</el-button>
+          <el-button type="primary" @click="generateReport">生成报表</el-button>
+          <el-button @click="doExport('excel')">导出 Excel</el-button>
+          <el-button @click="doExport('csv')">导出 CSV</el-button>
+          <el-button @click="savePersonalView">保存视图</el-button>
         </div>
       </div>
+    </section>
+
+    <!-- 已保存视图 -->
+    <section v-if="savedViews.length" class="glass-card section-card" style="padding:12px 24px">
+      <div class="chip-list" style="margin-bottom:0">
+        <span style="color:var(--cb-text-soft);font-size:13px;margin-right:8px">我的视图:</span>
+        <el-tag
+          v-for="(view, idx) in savedViews"
+          :key="idx"
+          closable
+          size="large"
+          @close="removeView(idx)"
+          @click="applyView(view)"
+          style="cursor:pointer"
+        >
+          {{ view.name }}
+        </el-tag>
+      </div>
+    </section>
 
-      <el-table :data="items" stripe style="width: 100%">
-        <el-table-column prop="reportType" label="报表类型" min-width="200" />
-        <el-table-column prop="dimensions" label="维度" min-width="220" />
-        <el-table-column prop="owner" label="使用角色" width="160" />
+    <!-- 报表数据 -->
+    <section class="glass-card section-card">
+      <div class="section-card__title" style="margin-bottom:16px">
+        <div>
+          <h3>{{ reportTypeLabel }} — 报表数据</h3>
+          <p>共 {{ reportData.length }} 条指标</p>
+        </div>
+      </div>
+      <el-table :data="reportData" stripe style="width:100%">
+        <el-table-column prop="metric" label="指标名称" min-width="200" />
+        <el-table-column prop="value" label="数值" width="160" />
+        <el-table-column prop="mom" label="环比" width="120">
+          <template #default="{ row }">
+            <span :class="trendClass(row.mom)">{{ row.mom }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="yoy" label="同比" width="120">
+          <template #default="{ row }">
+            <span :class="trendClass(row.yoy)">{{ row.yoy }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="dimension" label="维度分组" min-width="200" />
+      </el-table>
+    </section>
+
+    <!-- 报表目录 -->
+    <section class="glass-card section-card">
+      <div class="section-card__title" style="margin-bottom:16px">
+        <div>
+          <h3>报表目录</h3>
+          <p>已保存的报表模板列表。</p>
+        </div>
+      </div>
+      <el-table :data="reportList" stripe style="width:100%">
+        <el-table-column prop="reportType" label="报表类型" width="160" />
+        <el-table-column prop="dimensions" label="维度" min-width="250" />
+        <el-table-column prop="owner" label="使用角色" width="140" />
         <el-table-column prop="updatedAt" label="最近更新时间" width="170" />
+        <el-table-column label="操作" width="120">
+          <template #default="{ row }">
+            <el-button link type="primary" @click="loadSavedReport(row)">查看</el-button>
+          </template>
+        </el-table-column>
       </el-table>
     </section>
 
-    <SpecPageRenderer page-key="report-center" />
+    <!-- 保存视图对话框 -->
+    <el-dialog v-model="saveViewVisible" title="保存个人视图" width="400px" destroy-on-close>
+      <el-form :model="viewForm" label-width="80px">
+        <el-form-item label="视图名称">
+          <el-input v-model="viewForm.name" placeholder="请输入视图名称" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="saveViewVisible = false">取消</el-button>
+        <el-button type="primary" @click="confirmSaveView">保存</el-button>
+      </template>
+    </el-dialog>
   </div>
 </template>
 
 <script setup lang="ts">
-import { onMounted, ref } from 'vue';
+import { computed, onMounted, ref, reactive } from 'vue';
+import { ElMessage } from 'element-plus';
 import { api } from '@/api/services';
-import SpecPageRenderer from '@/components/SpecPageRenderer.vue';
-import type { ReportItem } from '@/types/page';
+import type { ReportItem, ReportDataItem } from '@/types/page';
+
+const reportType = ref('channel');
+const reportList = ref<ReportItem[]>([]);
+const reportData = ref<ReportDataItem[]>([]);
+
+const filters = ref({
+  timeRange: null as [Date, Date] | null,
+  channel: '',
+  shop: '',
+  sku: '',
+  supplier: '',
+  warehouse: '',
+  country: ''
+});
+
+interface SavedView {
+  name: string;
+  reportType: string;
+  filters: typeof filters.value;
+}
 
-const items = ref<ReportItem[]>([]);
+const savedViews = ref<SavedView[]>([]);
+const saveViewVisible = ref(false);
+const viewForm = reactive({ name: '' });
 
-onMounted(async () => {
-  const response = await api.getReports();
-  items.value = response.items;
+const reportTypeLabel = computed(() => {
+  const map: Record<string, string> = {
+    channel: '渠道报表',
+    sku: 'SKU 报表',
+    fulfillment: '履约报表',
+    custom: '自定义报表'
+  };
+  return map[reportType.value] || '报表';
 });
+
+const trendClass = (val: string) => {
+  if (!val) return '';
+  if (val.startsWith('+') || val.startsWith('↑')) return 'trend-up';
+  if (val.startsWith('-') || val.startsWith('↓')) return 'trend-down';
+  return '';
+};
+
+const loadData = async () => {
+  const res = await api.getReports();
+  reportList.value = res.items;
+};
+
+const onReportTypeChange = () => {
+  reportData.value = [];
+};
+
+const generateReport = async () => {
+  const res = await api.getReportData();
+  reportData.value = res.items;
+  ElMessage.success('报表数据已加载');
+};
+
+const loadSavedReport = async (item: ReportItem) => {
+  reportType.value = item.reportType === '渠道销售日报' ? 'channel' : 'channel';
+  const res = await api.getReportData();
+  reportData.value = res.items;
+  ElMessage.success(`已加载「${item.reportType}」`);
+};
+
+const doExport = (format: string) => {
+  if (reportData.value.length === 0) {
+    ElMessage.warning('请先生成报表数据');
+    return;
+  }
+  ElMessage.info(`正在导出 ${format.toUpperCase()} 文件,完成后自动下载`);
+};
+
+const savePersonalView = () => {
+  viewForm.name = '';
+  saveViewVisible.value = true;
+};
+
+const confirmSaveView = () => {
+  if (!viewForm.name.trim()) {
+    ElMessage.warning('请输入视图名称');
+    return;
+  }
+  savedViews.value.push({
+    name: viewForm.name,
+    reportType: reportType.value,
+    filters: { ...filters.value }
+  });
+  saveViewVisible.value = false;
+  ElMessage.success('视图已保存');
+};
+
+const applyView = (view: SavedView) => {
+  reportType.value = view.reportType;
+  Object.assign(filters.value, view.filters);
+  generateReport();
+};
+
+const removeView = (idx: number) => {
+  savedViews.value.splice(idx, 1);
+};
+
+onMounted(loadData);
 </script>
+
+<style scoped>
+.filter-form :deep(.el-form-item) { margin-bottom: 0; }
+.trend-up { color: var(--el-color-success); font-weight: 500; }
+.trend-down { color: var(--el-color-danger); font-weight: 500; }
+</style>

+ 395 - 35
src/views/supplier/PurchaseOrderView.vue

@@ -1,58 +1,418 @@
 <template>
   <div class="app-page">
-    <section class="glass-card page-hero">
-      <div class="page-hero__meta">
-        <span class="page-hero__eyebrow">Purchase Flow</span>
-        <h1>采购单管理与到货确认</h1>
-        <p>从低库存预警跳入创建采购单,再到确认到货更新库存,这页承担补货闭环的核心任务。</p>
-      </div>
-      <div class="chip-list">
-        <span class="chip">支持带出默认供货参数</span>
-        <span class="chip">到货后自动回写库存</span>
-      </div>
+    <!-- 筛选区 -->
+    <section class="glass-card section-card">
+      <el-form :model="filters" inline class="filter-form">
+        <el-form-item label="采购单号">
+          <el-input v-model="filters.poNo" placeholder="PO-xxx" clearable style="width:160px" @keyup.enter="loadData" />
+        </el-form-item>
+        <el-form-item label="供应商">
+          <el-select v-model="filters.supplier" placeholder="全部" clearable filterable style="width:160px">
+            <el-option v-for="s in supplierOptions" :key="s" :label="s" :value="s" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="采购状态">
+          <el-select v-model="filters.status" placeholder="全部" clearable style="width:130px">
+            <el-option v-for="s in poStatuses" :key="s.value" :label="s.label" :value="s.value" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="SKU">
+          <el-input v-model="filters.sku" placeholder="SKU 编码" clearable style="width:140px" @keyup.enter="loadData" />
+        </el-form-item>
+        <el-form-item label="创建时间">
+          <el-date-picker v-model="filters.createdRange" type="daterange" start-placeholder="开始" end-placeholder="结束" value-format="YYYY-MM-DD" style="width:240px" />
+        </el-form-item>
+        <el-form-item label="预计交期">
+          <el-date-picker v-model="filters.expectedRange" type="daterange" start-placeholder="开始" end-placeholder="结束" value-format="YYYY-MM-DD" 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">
-      <div class="table-toolbar">
-        <el-form inline>
-          <el-form-item label="采购状态">
-            <el-select model-value="全部状态" style="width: 140px">
-              <el-option label="全部状态" value="全部状态" />
-            </el-select>
-          </el-form-item>
-        </el-form>
+    <!-- 工具栏 -->
+    <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">创建采购单</el-button>
-          <el-button>确认到货</el-button>
+          <el-button type="primary" @click="openCreate">创建采购单</el-button>
+          <el-button :disabled="!selected.length" @click="batchConfirmArrival">确认到货</el-button>
         </div>
+        <el-button @click="loadData">刷新</el-button>
       </div>
+    </section>
 
-      <el-table :data="items" stripe style="width: 100%">
-        <el-table-column prop="poNo" label="采购单号" width="170" />
-        <el-table-column prop="supplier" label="供应商" min-width="240" />
-        <el-table-column prop="skuCount" label="SKU 数" width="90" />
+    <!-- 列表 -->
+    <section class="glass-card section-card">
+      <el-table :data="filteredItems" stripe style="width:100%" @selection-change="onSelection">
+        <el-table-column type="selection" width="45" />
+        <el-table-column prop="poNo" label="采购单号" width="160" />
+        <el-table-column prop="supplier" label="供应商" min-width="180" />
+        <el-table-column prop="skuCount" label="SKU 数" width="80" />
         <el-table-column prop="amount" label="采购总额" width="120" />
         <el-table-column prop="expectedDate" label="预计交期" width="120" />
-        <el-table-column prop="arrivalProgress" label="到货进度" min-width="150" />
-        <el-table-column prop="status" label="采购状态" width="130" />
-        <el-table-column prop="creator" label="创建人" width="120" />
+        <el-table-column prop="arrivalProgress" label="到货进度" min-width="140">
+          <template #default="{ row }">
+            <el-progress :percentage="parseProgress(row.arrivalProgress)" :stroke-width="6" style="width:100px" />
+          </template>
+        </el-table-column>
+        <el-table-column prop="status" label="采购状态" width="110">
+          <template #default="{ row }">
+            <el-tag :type="poStatusType(row.status)" size="small">{{ poStatusLabel(row.status) }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column prop="creator" label="创建人" width="100" />
+        <el-table-column label="操作" width="200" fixed="right">
+          <template #default="{ row }">
+            <el-button link type="primary" @click="openConfirmArrival(row)" :disabled="row.status === '已关闭' || row.status === 'completed'">确认到货</el-button>
+            <el-button link type="danger" @click="closePO(row)" :disabled="row.status === '已关闭' || row.status === 'completed'">关闭</el-button>
+          </template>
+        </el-table-column>
       </el-table>
     </section>
 
-    <SpecPageRenderer page-key="purchase-orders" />
+    <!-- 创建采购单弹窗 -->
+    <el-dialog v-model="createDialogVisible" title="创建采购单" width="800px" destroy-on-close>
+      <el-form ref="createFormRef" :model="createForm" :rules="createRules" label-width="100px" label-position="right">
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="供应商" prop="supplier">
+              <el-select v-model="createForm.supplier" placeholder="请选择供应商" filterable style="width:100%" @change="onSupplierChange">
+                <el-option v-for="s in supplierOptions" :key="s" :label="s" :value="s" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="收货仓库" prop="warehouse">
+              <el-select v-model="createForm.warehouse" placeholder="请选择仓库" style="width:100%">
+                <el-option label="华东仓" value="华东仓" />
+                <el-option label="华南仓" value="华南仓" />
+                <el-option label="海外仓" value="海外仓" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row :gutter="20">
+          <el-col :span="8">
+            <el-form-item label="币种" prop="currency">
+              <el-select v-model="createForm.currency" placeholder="币种" style="width:100%">
+                <el-option label="CNY" value="CNY" />
+                <el-option label="USD" value="USD" />
+                <el-option label="EUR" value="EUR" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <el-form-item label="税率" prop="tax">
+              <el-input v-model="createForm.tax" placeholder="如 13%" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <el-form-item label="运费" prop="freight">
+              <el-input v-model="createForm.freight" placeholder="运费金额" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-form-item label="预计交期" prop="expectedDate">
+          <el-date-picker v-model="createForm.expectedDate" type="date" placeholder="选择日期" value-format="YYYY-MM-DD" style="width:100%" />
+        </el-form-item>
+
+        <!-- 采购 SKU 明细 -->
+        <el-divider content-position="left">采购 SKU 明细</el-divider>
+        <div v-for="(item, idx) in createForm.items" :key="idx" style="margin-bottom:12px">
+          <el-row :gutter="8" align="middle">
+            <el-col :span="8">
+              <el-form-item :prop="`items.${idx}.sku`" :rules="[{ required: true, message: '请输入 SKU', trigger: 'blur' }]">
+                <el-input v-model="item.sku" placeholder="SKU 编码" />
+              </el-form-item>
+            </el-col>
+            <el-col :span="5">
+              <el-form-item :prop="`items.${idx}.qty`" :rules="[{ required: true, message: '数量', trigger: 'blur' }]">
+                <el-input-number v-model="item.qty" :min="1" :max="99999" controls-position="right" placeholder="数量" style="width:100%" />
+              </el-form-item>
+            </el-col>
+            <el-col :span="5">
+              <el-form-item :prop="`items.${idx}.price`" :rules="[{ required: true, message: '单价', trigger: 'blur' }]">
+                <el-input v-model="item.price" placeholder="单价" />
+              </el-form-item>
+            </el-col>
+            <el-col :span="4">
+              <span style="line-height:32px;font-weight:600">{{ lineTotal(item) }}</span>
+            </el-col>
+            <el-col :span="2">
+              <el-button link type="danger" @click="removeItem(idx)" :disabled="createForm.items.length <= 1">删除</el-button>
+            </el-col>
+          </el-row>
+        </div>
+        <el-button type="primary" link @click="addItem" style="margin-bottom:12px">+ 添加 SKU</el-button>
+
+        <el-form-item label="备注" prop="remark">
+          <el-input v-model="createForm.remark" type="textarea" :rows="3" placeholder="备注信息" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="createDialogVisible = false">取消</el-button>
+        <el-button type="primary" :loading="submitting" @click="handleCreateSubmit">确认创建</el-button>
+      </template>
+    </el-dialog>
+
+    <!-- 确认到货弹窗 -->
+    <el-dialog v-model="arrivalDialogVisible" title="确认到货" width="520px" destroy-on-close>
+      <el-form ref="arrivalFormRef" :model="arrivalForm" :rules="arrivalRules" label-width="100px">
+        <el-form-item label="采购单号">
+          <el-input :model-value="arrivalForm.poNo" disabled />
+        </el-form-item>
+        <el-form-item label="到货 SKU" prop="sku">
+          <el-input v-model="arrivalForm.sku" placeholder="到货 SKU 编码" />
+        </el-form-item>
+        <el-form-item label="到货数量" prop="qty">
+          <el-input-number v-model="arrivalForm.qty" :min="1" :max="99999" controls-position="right" style="width:100%" />
+        </el-form-item>
+        <el-form-item label="备注">
+          <el-input v-model="arrivalForm.remark" type="textarea" :rows="2" placeholder="到货备注" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="arrivalDialogVisible = false">取消</el-button>
+        <el-button type="primary" :loading="submitting" @click="handleArrivalSubmit">确认到货</el-button>
+      </template>
+    </el-dialog>
   </div>
 </template>
 
 <script setup lang="ts">
-import { onMounted, ref } from 'vue';
+import { computed, onMounted, reactive, ref } from 'vue';
+import { ElMessage, ElMessageBox } from 'element-plus';
+import type { FormInstance, FormRules } from 'element-plus';
 import { api } from '@/api/services';
-import SpecPageRenderer from '@/components/SpecPageRenderer.vue';
-import type { PurchaseOrderItem } from '@/types/page';
+import type { PurchaseOrderItem, PurchaseOrderFormData } from '@/types/page';
 
 const items = ref<PurchaseOrderItem[]>([]);
+const selected = ref<PurchaseOrderItem[]>([]);
+const createDialogVisible = ref(false);
+const arrivalDialogVisible = ref(false);
+const submitting = ref(false);
+
+const createFormRef = ref<FormInstance>();
+const arrivalFormRef = ref<FormInstance>();
+
+const supplierOptions = ref<string[]>([]);
+
+const poStatuses = [
+  { label: '草稿', value: 'draft' },
+  { label: '待审批', value: 'pending_approval' },
+  { label: '已审批', value: 'approved' },
+  { label: '部分到货', value: 'partial_arrival' },
+  { label: '已完成', value: 'completed' },
+  { label: '已关闭', value: 'closed' }
+];
+
+const filters = ref({
+  poNo: '',
+  supplier: '',
+  status: '',
+  sku: '',
+  createdRange: null as string[] | null,
+  expectedRange: null as string[] | null
+});
+
+interface CreateFormItem {
+  sku: string;
+  qty: number;
+  price: string;
+}
+
+const defaultCreateForm = (): PurchaseOrderFormData & { items: CreateFormItem[] } => ({
+  supplier: '',
+  warehouse: '',
+  items: [{ sku: '', qty: 1, price: '' }],
+  currency: 'CNY',
+  tax: '',
+  freight: '',
+  expectedDate: '',
+  remark: ''
+});
+
+const createForm = reactive(defaultCreateForm());
+const createRules: FormRules = {
+  supplier: [{ required: true, message: '请选择供应商', trigger: 'change' }],
+  warehouse: [{ required: true, message: '请选择仓库', trigger: 'change' }],
+  currency: [{ required: true, message: '请选择币种', trigger: 'change' }],
+  expectedDate: [{ required: true, message: '请选择预计交期', trigger: 'change' }]
+};
+
+const arrivalForm = reactive({
+  poNo: '',
+  poId: '',
+  sku: '',
+  qty: 1,
+  remark: ''
+});
+
+const arrivalRules: FormRules = {
+  sku: [{ required: true, message: '请输入到货 SKU', trigger: 'blur' }],
+  qty: [{ required: true, message: '请输入到货数量', trigger: 'blur' }]
+};
 
-onMounted(async () => {
-  const response = await api.getPurchaseOrders();
-  items.value = response.items;
+const filteredItems = computed(() => {
+  return items.value.filter((item) => {
+    if (filters.value.poNo && !item.poNo.includes(filters.value.poNo)) return false;
+    if (filters.value.supplier && item.supplier !== filters.value.supplier) return false;
+    if (filters.value.status && item.status !== filters.value.status) return false;
+    if (filters.value.sku && !String(item.skuCount).includes(filters.value.sku)) return false;
+    if (filters.value.createdRange && filters.value.createdRange.length === 2) {
+      if (item.createdAt < filters.value.createdRange[0] || item.createdAt > filters.value.createdRange[1]) return false;
+    }
+    if (filters.value.expectedRange && filters.value.expectedRange.length === 2) {
+      if (item.expectedDate < filters.value.expectedRange[0] || item.expectedDate > filters.value.expectedRange[1]) return false;
+    }
+    return true;
+  });
 });
+
+const parseProgress = (p: string) => {
+  const num = parseInt(p, 10);
+  return isNaN(num) ? 0 : Math.min(100, Math.max(0, num));
+};
+
+const poStatusType = (s: string) => {
+  const map: Record<string, string> = {
+    draft: 'info',
+    pending_approval: 'warning',
+    approved: '',
+    partial_arrival: 'warning',
+    completed: 'success',
+    closed: 'danger'
+  };
+  return map[s] || '';
+};
+
+const poStatusLabel = (s: string) => {
+  const map: Record<string, string> = {
+    draft: '草稿',
+    pending_approval: '待审批',
+    approved: '已审批',
+    partial_arrival: '部分到货',
+    completed: '已完成',
+    closed: '已关闭'
+  };
+  return map[s] || s;
+};
+
+const lineTotal = (item: CreateFormItem) => {
+  const price = parseFloat(item.price) || 0;
+  return (price * item.qty).toFixed(2);
+};
+
+const loadData = async () => {
+  const [poRes, supplierRes] = await Promise.all([
+    api.getPurchaseOrders(),
+    api.getSuppliers()
+  ]);
+  items.value = poRes.items;
+  supplierOptions.value = supplierRes.items.map((s) => s.name);
+};
+
+const resetFilters = () => {
+  filters.value = { poNo: '', supplier: '', status: '', sku: '', createdRange: null, expectedRange: null };
+};
+
+const onSelection = (rows: PurchaseOrderItem[]) => {
+  selected.value = rows;
+};
+
+const onSupplierChange = (_val: string) => {
+  // Auto-fill default lead time & MOQ from supply capability (future enhancement)
+};
+
+const addItem = () => {
+  createForm.items.push({ sku: '', qty: 1, price: '' });
+};
+
+const removeItem = (idx: number) => {
+  createForm.items.splice(idx, 1);
+};
+
+const openCreate = () => {
+  Object.assign(createForm, defaultCreateForm());
+  createDialogVisible.value = true;
+};
+
+const handleCreateSubmit = async () => {
+  if (!createFormRef.value) return;
+  await createFormRef.value.validate();
+  submitting.value = true;
+  try {
+    await api.createPurchaseOrder(createForm);
+    ElMessage.success('采购单创建成功');
+    createDialogVisible.value = false;
+    loadData();
+  } finally {
+    submitting.value = false;
+  }
+};
+
+const openConfirmArrival = (row: PurchaseOrderItem) => {
+  arrivalForm.poNo = row.poNo;
+  arrivalForm.poId = row.id;
+  arrivalForm.sku = '';
+  arrivalForm.qty = 1;
+  arrivalForm.remark = '';
+  arrivalDialogVisible.value = true;
+};
+
+const handleArrivalSubmit = async () => {
+  if (!arrivalFormRef.value) return;
+  await arrivalFormRef.value.validate();
+  submitting.value = true;
+  try {
+    await api.updatePurchaseOrder(arrivalForm.poId, {
+      status: 'partial_arrival'
+    } as Partial<PurchaseOrderItem>);
+    ElMessage.success('到货确认成功,库存已更新');
+    arrivalDialogVisible.value = false;
+    loadData();
+  } finally {
+    submitting.value = false;
+  }
+};
+
+const batchConfirmArrival = async () => {
+  if (!selected.value.length) return;
+  try {
+    await ElMessageBox.confirm(
+      `确认对选中的 ${selected.value.length} 个采购单进行到货确认?`,
+      '批量确认到货'
+    );
+    for (const po of selected.value) {
+      await api.updatePurchaseOrder(po.id, { status: 'partial_arrival' } as Partial<PurchaseOrderItem>);
+    }
+    ElMessage.success('批量到货确认成功');
+    loadData();
+  } catch {
+    // cancelled
+  }
+};
+
+const closePO = async (row: PurchaseOrderItem) => {
+  try {
+    await ElMessageBox.confirm(
+      `确认关闭采购单「${row.poNo}」?关闭后将无法继续到货确认。`,
+      '关闭确认',
+      { type: 'warning' }
+    );
+    await api.updatePurchaseOrder(row.id, { status: 'closed' } as Partial<PurchaseOrderItem>);
+    ElMessage.success('采购单已关闭');
+    loadData();
+  } catch {
+    // cancelled
+  }
+};
+
+onMounted(loadData);
 </script>
+
+<style scoped>
+.filter-form :deep(.el-form-item) { margin-bottom: 0; }
+</style>

+ 294 - 36
src/views/supplier/SupplierListView.vue

@@ -1,57 +1,315 @@
 <template>
   <div class="app-page">
-    <section class="glass-card page-hero">
-      <div class="page-hero__meta">
-        <span class="page-hero__eyebrow">SRM</span>
-        <h1>供应商管理与档案维护</h1>
-        <p>供应商页是采购和补货链路的入口,建议与采购单、供货能力配置页保持互跳。</p>
-      </div>
-      <div class="chip-list">
-        <span class="chip">支持合同版本管理</span>
-        <span class="chip">停用前校验在途采购</span>
-      </div>
+    <!-- 筛选区 -->
+    <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-input v-model="filters.contact" placeholder="联系人姓名" clearable style="width:140px" @keyup.enter="loadData" />
+        </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-option label="待审核" value="待审核" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="评级">
+          <el-select v-model="filters.rating" placeholder="全部" clearable style="width:120px">
+            <el-option label="A" value="A" />
+            <el-option label="B" value="B" />
+            <el-option label="C" value="C" />
+            <el-option label="D" value="D" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="创建时间">
+          <el-date-picker v-model="filters.dateRange" type="daterange" start-placeholder="开始日期" end-placeholder="结束日期" value-format="YYYY-MM-DD" 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">
-      <div class="table-toolbar">
-        <el-form inline>
-          <el-form-item label="状态">
-            <el-select model-value="全部供应商" style="width: 150px">
-              <el-option label="全部供应商" value="全部供应商" />
-            </el-select>
-          </el-form-item>
-        </el-form>
+    <!-- 工具栏 -->
+    <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">新建供应商</el-button>
-          <el-button>导出</el-button>
+          <el-button type="primary" @click="openCreate">新建供应商</el-button>
+          <el-button @click="doExport">导出</el-button>
         </div>
+        <el-button @click="loadData">刷新</el-button>
       </div>
+    </section>
 
-      <el-table :data="items" stripe style="width: 100%">
-        <el-table-column prop="name" label="供应商名称" min-width="260" />
-        <el-table-column prop="contact" label="联系人" width="180" />
-        <el-table-column prop="settlement" label="结算方式" width="140" />
-        <el-table-column prop="rating" label="评级" width="90" />
-        <el-table-column prop="status" label="合作状态" width="110" />
-        <el-table-column prop="relatedSkuCount" label="关联 SKU 数" width="120" />
-        <el-table-column prop="updatedAt" label="更新时间" width="170" />
+    <!-- 列表 -->
+    <section class="glass-card section-card">
+      <el-table :data="filteredItems" stripe style="width:100%">
+        <el-table-column prop="name" label="供应商名称" min-width="200" />
+        <el-table-column prop="contact" label="联系人" width="120" />
+        <el-table-column prop="phone" label="电话" width="140" />
+        <el-table-column prop="status" label="合作状态" width="100">
+          <template #default="{ row }">
+            <el-tag :type="row.status === '合作中' ? 'success' : row.status === '已停用' ? 'danger' : 'warning'" size="small">{{ row.status }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column prop="settlement" label="结算方式" width="120" />
+        <el-table-column prop="rating" label="评级" width="80">
+          <template #default="{ row }">
+            <span :class="ratingClass(row.rating)">{{ row.rating }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="relatedSkuCount" label="关联 SKU 数" width="110" />
+        <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="openEdit(row)">编辑</el-button>
+            <el-button link type="primary" @click="toggleStatus(row)">{{ row.status === '合作中' ? '停用' : '启用' }}</el-button>
+          </template>
+        </el-table-column>
       </el-table>
     </section>
 
-    <SpecPageRenderer page-key="supplier-list" />
+    <!-- 新建/编辑弹窗 -->
+    <el-dialog v-model="dialogVisible" :title="isEdit ? '编辑供应商' : '新建供应商'" width="680px" destroy-on-close>
+      <el-form ref="formRef" :model="formData" :rules="formRules" label-width="100px" label-position="right">
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="供应商名称" prop="name">
+              <el-input v-model="formData.name" placeholder="请输入供应商名称" :disabled="isEdit" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="公司名称" prop="companyName">
+              <el-input v-model="formData.companyName" placeholder="请输入公司全称" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="联系人" prop="contact">
+              <el-input v-model="formData.contact" placeholder="请输入联系人姓名" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="电话" prop="phone">
+              <el-input v-model="formData.phone" placeholder="请输入联系电话" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="邮箱" prop="email">
+              <el-input v-model="formData.email" placeholder="请输入邮箱地址" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="结算方式" prop="settlement">
+              <el-select v-model="formData.settlement" placeholder="请选择" style="width:100%">
+                <el-option label="月结 30 天" value="月结30天" />
+                <el-option label="月结 60 天" value="月结60天" />
+                <el-option label="月结 90 天" value="月结90天" />
+                <el-option label="货到付款" value="货到付款" />
+                <el-option label="预付款" value="预付款" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-form-item label="地址" prop="address">
+          <el-input v-model="formData.address" placeholder="请输入详细地址" />
+        </el-form-item>
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="开户行信息" prop="bankInfo">
+              <el-input v-model="formData.bankInfo" placeholder="开户行 + 账号" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="税号" prop="taxNo">
+              <el-input v-model="formData.taxNo" placeholder="纳税人识别号" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="合同编号" prop="contractNo">
+              <el-input v-model="formData.contractNo" placeholder="合同编号" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-form-item label="备注" prop="remark">
+          <el-input v-model="formData.remark" type="textarea" :rows="3" placeholder="备注信息" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="dialogVisible = false">取消</el-button>
+        <el-button type="primary" :loading="submitting" @click="handleSubmit">确认</el-button>
+      </template>
+    </el-dialog>
   </div>
 </template>
 
 <script setup lang="ts">
-import { onMounted, ref } from 'vue';
+import { computed, onMounted, reactive, ref } from 'vue';
+import { ElMessage, ElMessageBox } from 'element-plus';
+import type { FormInstance, FormRules } from 'element-plus';
 import { api } from '@/api/services';
-import SpecPageRenderer from '@/components/SpecPageRenderer.vue';
-import type { SupplierItem } from '@/types/page';
+import type { SupplierItem, SupplierFormData } from '@/types/page';
 
 const items = ref<SupplierItem[]>([]);
+const dialogVisible = ref(false);
+const isEdit = ref(false);
+const editId = ref('');
+const submitting = ref(false);
+const formRef = ref<FormInstance>();
+
+const filters = ref({
+  name: '',
+  contact: '',
+  status: '',
+  rating: '',
+  dateRange: null as string[] | null
+});
+
+const defaultFormData = (): SupplierFormData => ({
+  name: '',
+  companyName: '',
+  contact: '',
+  phone: '',
+  email: '',
+  address: '',
+  bankInfo: '',
+  taxNo: '',
+  contractNo: '',
+  settlement: '',
+  remark: ''
+});
+
+const formData = reactive<SupplierFormData>(defaultFormData());
+
+const formRules: FormRules = {
+  name: [{ required: true, message: '请输入供应商名称', trigger: 'blur' }],
+  contact: [{ required: true, message: '请输入联系人', trigger: 'blur' }],
+  phone: [{ required: true, message: '请输入联系电话', trigger: 'blur' }],
+  email: [{ type: 'email', message: '请输入正确的邮箱地址', trigger: 'blur' }],
+  settlement: [{ required: true, message: '请选择结算方式', trigger: 'change' }]
+};
 
-onMounted(async () => {
-  const response = await api.getSuppliers();
-  items.value = response.items;
+const filteredItems = computed(() => {
+  return items.value.filter((item) => {
+    if (filters.value.name && !item.name.includes(filters.value.name)) return false;
+    if (filters.value.contact && !item.contact.includes(filters.value.contact)) return false;
+    if (filters.value.status && item.status !== filters.value.status) return false;
+    if (filters.value.rating && item.rating !== filters.value.rating) return false;
+    if (filters.value.dateRange && filters.value.dateRange.length === 2) {
+      const start = filters.value.dateRange[0];
+      const end = filters.value.dateRange[1];
+      if (item.updatedAt < start || item.updatedAt > end) return false;
+    }
+    return true;
+  });
 });
+
+const ratingClass = (r: string) => {
+  if (r === 'A') return 'rating-a';
+  if (r === 'B') return 'rating-b';
+  return 'rating-default';
+};
+
+const loadData = async () => {
+  const res = await api.getSuppliers();
+  items.value = res.items;
+};
+
+const resetFilters = () => {
+  filters.value = { name: '', contact: '', status: '', rating: '', dateRange: null };
+};
+
+const openCreate = () => {
+  isEdit.value = false;
+  editId.value = '';
+  Object.assign(formData, defaultFormData());
+  dialogVisible.value = true;
+};
+
+const openEdit = (row: SupplierItem) => {
+  isEdit.value = true;
+  editId.value = row.id;
+  Object.assign(formData, {
+    name: row.name,
+    companyName: row.companyName,
+    contact: row.contact,
+    phone: row.phone,
+    email: row.email,
+    address: row.address,
+    bankInfo: '',
+    taxNo: '',
+    contractNo: '',
+    settlement: row.settlement,
+    remark: ''
+  });
+  dialogVisible.value = true;
+};
+
+const handleSubmit = async () => {
+  if (!formRef.value) return;
+  await formRef.value.validate();
+  submitting.value = true;
+  try {
+    if (isEdit.value) {
+      await api.updateSupplier(editId.value, formData);
+      ElMessage.success('供应商更新成功');
+    } else {
+      await api.createSupplier(formData);
+      ElMessage.success('供应商创建成功');
+    }
+    dialogVisible.value = false;
+    loadData();
+  } finally {
+    submitting.value = false;
+  }
+};
+
+const toggleStatus = async (row: SupplierItem) => {
+  const targetStatus = row.status === '合作中' ? '已停用' : '合作中';
+  const action = targetStatus === '已停用' ? '停用' : '启用';
+
+  if (targetStatus === '已停用') {
+    try {
+      await ElMessageBox.confirm(
+        '该供应商下存在在途采购单,停用后将无法新建采购单。确认停用?',
+        '停用确认',
+        { type: 'warning' }
+      );
+    } catch {
+      return;
+    }
+  } else {
+    try {
+      await ElMessageBox.confirm(`确认启用供应商「${row.name}」?`, '启用确认');
+    } catch {
+      return;
+    }
+  }
+
+  await api.updateSupplier(row.id, { status: targetStatus });
+  ElMessage.success(`${action}成功`);
+  loadData();
+};
+
+const doExport = () => {
+  ElMessage.info('导出已开始,完成后将自动下载');
+};
+
+onMounted(loadData);
 </script>
+
+<style scoped>
+.filter-form :deep(.el-form-item) { margin-bottom: 0; }
+.rating-a { color: var(--cb-success, #67c23a); font-weight: 700; }
+.rating-b { color: var(--cb-primary, #409eff); font-weight: 600; }
+.rating-default { color: var(--cb-text-soft, #909399); }
+</style>

+ 376 - 0
src/views/supplier/SupplyCapabilityView.vue

@@ -0,0 +1,376 @@
+<template>
+  <div class="app-page">
+    <!-- 筛选区 -->
+    <section class="glass-card section-card">
+      <el-form :model="filters" inline class="filter-form">
+        <el-form-item label="供应商">
+          <el-select v-model="filters.supplier" placeholder="全部" clearable filterable style="width:160px">
+            <el-option v-for="s in supplierOptions" :key="s" :label="s" :value="s" />
+          </el-select>
+        </el-form-item>
+        <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.isDefault" placeholder="全部" clearable style="width:120px">
+            <el-option label="是" :value="true" />
+            <el-option label="否" :value="false" />
+          </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="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%">
+        <el-table-column prop="supplier" label="供应商" min-width="180" />
+        <el-table-column prop="sku" label="SKU" width="140">
+          <template #default="{ row }">
+            <div>{{ row.sku }}</div>
+            <div style="color:var(--cb-text-soft);font-size:12px">{{ row.productTitle }}</div>
+          </template>
+        </el-table-column>
+        <el-table-column prop="leadTime" label="标准交期(天)" width="120" />
+        <el-table-column prop="moq" label="MOQ" width="80" />
+        <el-table-column prop="unit" label="采购单位" width="90" />
+        <el-table-column label="阶梯价" min-width="200">
+          <template #default="{ row }">
+            <div v-if="row.tierPrices && row.tierPrices.length">
+              <div v-for="(tier, idx) in row.tierPrices" :key="idx" class="tier-row">
+                {{ tier.from }}~{{ tier.to }} : {{ tier.price }}
+              </div>
+            </div>
+            <span v-else style="color:var(--cb-text-soft)">--</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="isDefault" label="默认供应商" width="100">
+          <template #default="{ row }">
+            <el-tag v-if="row.isDefault" type="success" size="small">默认</el-tag>
+            <span v-else style="color:var(--cb-text-soft)">--</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="status" label="状态" width="80">
+          <template #default="{ row }">
+            <el-tag :type="row.status === '启用' ? 'success' : 'info'" size="small">{{ row.status }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" width="240" fixed="right">
+          <template #default="{ row }">
+            <el-button link type="primary" @click="openEdit(row)">编辑</el-button>
+            <el-button link type="primary" @click="setDefault(row)" :disabled="row.isDefault">设为默认</el-button>
+            <el-button link type="danger" @click="toggleStatus(row)">{{ row.status === '启用' ? '停用' : '启用' }}</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+    </section>
+
+    <!-- 新建/编辑弹窗 -->
+    <el-dialog v-model="dialogVisible" :title="isEdit ? '编辑供货配置' : '新增供货配置'" width="700px" destroy-on-close>
+      <el-form ref="formRef" :model="formData" :rules="formRules" label-width="110px" label-position="right">
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="供应商" prop="supplier">
+              <el-select v-model="formData.supplier" placeholder="请选择供应商" filterable style="width:100%">
+                <el-option v-for="s in supplierOptions" :key="s" :label="s" :value="s" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="SKU" prop="sku">
+              <el-input v-model="formData.sku" placeholder="SKU 编码" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row :gutter="20">
+          <el-col :span="8">
+            <el-form-item label="标准交期" prop="leadTime">
+              <el-input-number v-model="formData.leadTime" :min="0" :max="365" controls-position="right" style="width:100%" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <el-form-item label="MOQ" prop="moq">
+              <el-input-number v-model="formData.moq" :min="0" :max="999999" controls-position="right" style="width:100%" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <el-form-item label="采购单位" prop="unit">
+              <el-select v-model="formData.unit" 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-col>
+        </el-row>
+        <el-row :gutter="20">
+          <el-col :span="8">
+            <el-form-item label="币种" prop="currency">
+              <el-select v-model="formData.currency" placeholder="币种" style="width:100%">
+                <el-option label="CNY" value="CNY" />
+                <el-option label="USD" value="USD" />
+                <el-option label="EUR" value="EUR" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <!-- 阶梯价 -->
+        <el-divider content-position="left">阶梯价格</el-divider>
+        <div v-for="(tier, idx) in formData.tierPrices" :key="idx" style="margin-bottom:8px">
+          <el-row :gutter="8" align="middle">
+            <el-col :span="6">
+              <el-input-number v-model="tier.from" :min="0" controls-position="right" placeholder="起始量" style="width:100%" />
+            </el-col>
+            <el-col :span="6">
+              <el-input-number v-model="tier.to" :min="0" controls-position="right" placeholder="结束量" style="width:100%" />
+            </el-col>
+            <el-col :span="6">
+              <el-input v-model="tier.price" placeholder="单价" />
+            </el-col>
+            <el-col :span="6">
+              <el-button link type="danger" @click="removeTier(idx)" :disabled="formData.tierPrices.length <= 1">删除</el-button>
+            </el-col>
+          </el-row>
+        </div>
+        <el-button type="primary" link @click="addTier" style="margin-bottom:12px">+ 添加阶梯</el-button>
+
+        <el-form-item label="备注" prop="remark">
+          <el-input v-model="formData.remark" type="textarea" :rows="3" placeholder="备注信息" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="dialogVisible = false">取消</el-button>
+        <el-button type="primary" :loading="submitting" @click="handleSubmit">确认</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 { FormInstance, FormRules } from 'element-plus';
+import { api } from '@/api/services';
+import type { SupplyCapabilityItem } from '@/types/page';
+
+const items = ref<SupplyCapabilityItem[]>([]);
+const dialogVisible = ref(false);
+const isEdit = ref(false);
+const editId = ref('');
+const submitting = ref(false);
+const formRef = ref<FormInstance>();
+
+const supplierOptions = ref<string[]>([]);
+
+const filters = ref({
+  supplier: '',
+  sku: '',
+  isDefault: null as boolean | null,
+  status: ''
+});
+
+interface TierPrice {
+  from: number;
+  to: number;
+  price: string;
+}
+
+interface FormState {
+  supplier: string;
+  sku: string;
+  leadTime: number;
+  moq: number;
+  unit: string;
+  currency: string;
+  tierPrices: TierPrice[];
+  remark: string;
+}
+
+const defaultFormData = (): FormState => ({
+  supplier: '',
+  sku: '',
+  leadTime: 0,
+  moq: 0,
+  unit: '件',
+  currency: 'CNY',
+  tierPrices: [{ from: 1, to: 100, price: '' }],
+  remark: ''
+});
+
+const formData = reactive<FormState>(defaultFormData());
+
+const formRules: FormRules = {
+  supplier: [{ required: true, message: '请选择供应商', trigger: 'change' }],
+  sku: [{ required: true, message: '请输入 SKU', trigger: 'blur' }],
+  leadTime: [{ required: true, message: '请输入交期', trigger: 'blur' }],
+  moq: [{ required: true, message: '请输入 MOQ', trigger: 'blur' }],
+  unit: [{ required: true, message: '请选择单位', trigger: 'change' }],
+  currency: [{ required: true, message: '请选择币种', trigger: 'change' }]
+};
+
+const filteredItems = computed(() => {
+  return items.value.filter((item) => {
+    if (filters.value.supplier && item.supplier !== filters.value.supplier) return false;
+    if (filters.value.sku && !item.sku.includes(filters.value.sku)) return false;
+    if (filters.value.isDefault !== null && item.isDefault !== filters.value.isDefault) return false;
+    if (filters.value.status && item.status !== filters.value.status) return false;
+    return true;
+  });
+});
+
+const loadData = async () => {
+  const [capRes, supplierRes] = await Promise.all([
+    api.getSupplyCapabilities(),
+    api.getSuppliers()
+  ]);
+  items.value = capRes.items;
+  supplierOptions.value = supplierRes.items.map((s) => s.name);
+};
+
+const resetFilters = () => {
+  filters.value = { supplier: '', sku: '', isDefault: null, status: '' };
+};
+
+const addTier = () => {
+  const last = formData.tierPrices[formData.tierPrices.length - 1];
+  formData.tierPrices.push({ from: last?.to ? last.to + 1 : 1, to: 0, price: '' });
+};
+
+const removeTier = (idx: number) => {
+  formData.tierPrices.splice(idx, 1);
+};
+
+const openCreate = () => {
+  isEdit.value = false;
+  editId.value = '';
+  Object.assign(formData, defaultFormData());
+  dialogVisible.value = true;
+};
+
+const openEdit = (row: SupplyCapabilityItem) => {
+  isEdit.value = true;
+  editId.value = row.id;
+  Object.assign(formData, {
+    supplier: row.supplier,
+    sku: row.sku,
+    leadTime: row.leadTime,
+    moq: row.moq,
+    unit: row.unit,
+    currency: 'CNY',
+    tierPrices: row.tierPrices && row.tierPrices.length
+      ? row.tierPrices.map((t) => ({ from: t.from, to: t.to, price: t.price }))
+      : [{ from: 1, to: 100, price: '' }],
+    remark: ''
+  });
+  dialogVisible.value = true;
+};
+
+const handleSubmit = async () => {
+  if (!formRef.value) return;
+  await formRef.value.validate();
+
+  if (formData.leadTime < 0) {
+    ElMessage.warning('标准交期不能为负数');
+    return;
+  }
+  if (formData.moq < 0) {
+    ElMessage.warning('MOQ 不能为负数');
+    return;
+  }
+
+  submitting.value = true;
+  try {
+    const payload: Partial<SupplyCapabilityItem> = {
+      supplier: formData.supplier,
+      sku: formData.sku,
+      leadTime: formData.leadTime,
+      moq: formData.moq,
+      unit: formData.unit,
+      tierPrices: formData.tierPrices.map((t) => ({ from: t.from, to: t.to, price: t.price }))
+    };
+
+    if (isEdit.value) {
+      await api.updateSupplyCapability(editId.value, payload);
+      ElMessage.success('供货配置更新成功');
+    } else {
+      await api.createSupplyCapability(payload);
+      ElMessage.success('供货配置创建成功');
+    }
+    dialogVisible.value = false;
+    loadData();
+  } finally {
+    submitting.value = false;
+  }
+};
+
+const setDefault = async (row: SupplyCapabilityItem) => {
+  try {
+    await ElMessageBox.confirm(
+      `确认将「${row.supplier}」设为 SKU「${row.sku}」的默认供应商?此操作将清除该 SKU 的其他默认供应商设置。`,
+      '设为默认',
+      { type: 'info' }
+    );
+
+    // Clear existing defaults for the same SKU
+    const sameSkuDefaults = items.value.filter(
+      (item) => item.sku === row.sku && item.isDefault && item.id !== row.id
+    );
+    for (const item of sameSkuDefaults) {
+      await api.updateSupplyCapability(item.id, { isDefault: false } as Partial<SupplyCapabilityItem>);
+    }
+
+    // Set the new default
+    await api.updateSupplyCapability(row.id, { isDefault: true } as Partial<SupplyCapabilityItem>);
+    ElMessage.success('已设为默认供应商');
+    loadData();
+  } catch {
+    // cancelled
+  }
+};
+
+const toggleStatus = async (row: SupplyCapabilityItem) => {
+  const target = row.status === '启用' ? '停用' : '启用';
+  const action = target === '停用' ? '停用' : '启用';
+
+  try {
+    await ElMessageBox.confirm(
+      `确认${action}「${row.supplier}」对 SKU「${row.sku}」的供货配置?`,
+      `${action}确认`,
+      { type: target === '停用' ? 'warning' : 'info' }
+    );
+    await api.updateSupplyCapability(row.id, { status: target } as Partial<SupplyCapabilityItem>);
+    ElMessage.success(`${action}成功`);
+    loadData();
+  } catch {
+    // cancelled
+  }
+};
+
+onMounted(loadData);
+</script>
+
+<style scoped>
+.filter-form :deep(.el-form-item) { margin-bottom: 0; }
+.tier-row { font-size: 13px; color: var(--cb-text-soft); line-height: 1.6; }
+</style>

+ 290 - 16
src/views/system/ApiKeyView.vue

@@ -1,10 +1,11 @@
 <template>
   <div class="app-page">
+    <!-- 页面头部 -->
     <section class="glass-card page-hero">
       <div class="page-hero__meta">
         <span class="page-hero__eyebrow">Credential</span>
         <h1>API Key 管理</h1>
-        <p>用于统一管理对外接口凭据、轮换策略和过期时间,并保留安全操作提示。</p>
+        <p>统一管理对外接口凭据、轮换策略和过期时间,并保留安全操作提示。</p>
       </div>
       <div class="chip-list">
         <span class="chip">明文只展示一次</span>
@@ -12,38 +13,311 @@
       </div>
     </section>
 
-    <section class="glass-card section-card">
-      <div class="table-toolbar">
-        <div class="chip-list">
-          <el-button type="primary">创建 Key</el-button>
-          <el-button>轮换 Key</el-button>
-        </div>
+    <!-- 工具栏 -->
+    <section class="glass-card section-card" style="padding:12px 24px">
+      <div class="table-toolbar" style="margin-bottom:0">
+        <el-button type="primary" @click="openCreateDialog">创建 Key</el-button>
       </div>
+    </section>
 
-      <el-table :data="items" stripe style="width: 100%">
+    <!-- API Key 表格 -->
+    <section class="glass-card section-card">
+      <el-table :data="items" stripe style="width:100%">
         <el-table-column prop="name" label="Key 名称" width="180" />
         <el-table-column prop="system" label="所属系统" width="160" />
         <el-table-column prop="scope" label="权限范围" min-width="220" />
-        <el-table-column prop="status" label="状态" width="110" />
-        <el-table-column prop="expireAt" label="过期时间" width="130" />
+        <el-table-column label="状态" width="100">
+          <template #default="{ row }">
+            <el-tag :type="row.status === '启用' ? 'success' : row.status === '已过期' ? 'danger' : 'info'" size="small">
+              {{ row.status }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="创建时间" width="160">
+          <template #default="{ row }">
+            {{ row.expireAt ? row.expireAt : '--' }}
+          </template>
+        </el-table-column>
+        <el-table-column prop="expireAt" label="过期时间" width="140" />
         <el-table-column prop="lastUsedAt" label="最近调用时间" width="170" />
+        <el-table-column label="操作" width="260" fixed="right">
+          <template #default="{ row }">
+            <el-button link type="primary" @click="copyKey(row)">复制 Key</el-button>
+            <el-button
+              link
+              :type="row.status === '启用' ? 'warning' : 'success'"
+              @click="toggleKeyStatus(row)"
+            >
+              {{ row.status === '启用' ? '停用' : '启用' }}
+            </el-button>
+            <el-button link type="primary" @click="rotateKey(row)">轮换</el-button>
+            <el-button link type="danger" @click="deleteKey(row)">删除</el-button>
+          </template>
+        </el-table-column>
       </el-table>
     </section>
 
-    <SpecPageRenderer page-key="api-key" />
+    <!-- 创建 Key 对话框 -->
+    <el-dialog
+      v-model="createVisible"
+      title="创建 API Key"
+      width="520px"
+      destroy-on-close
+    >
+      <el-form
+        ref="createFormRef"
+        :model="createForm"
+        :rules="createFormRules"
+        label-width="100px"
+      >
+        <el-form-item label="Key 名称" prop="name">
+          <el-input v-model="createForm.name" placeholder="请输入 Key 名称" />
+        </el-form-item>
+        <el-form-item label="所属系统" prop="system">
+          <el-select v-model="createForm.system" placeholder="请选择系统" style="width:100%">
+            <el-option label="OMS" value="OMS" />
+            <el-option label="WMS" value="WMS" />
+            <el-option label="PIM" value="PIM" />
+            <el-option label="SRM" value="SRM" />
+            <el-option label="BI" value="BI" />
+            <el-option label="外部对接" value="外部对接" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="权限范围" prop="scope">
+          <el-input v-model="createForm.scope" placeholder="如:read:orders,write:products" />
+        </el-form-item>
+        <el-form-item label="IP 白名单">
+          <el-input v-model="createForm.ipWhitelist" placeholder="多个 IP 用逗号分隔,留空表示不限制" />
+        </el-form-item>
+        <el-form-item label="过期时间" prop="expireAt">
+          <el-date-picker
+            v-model="createForm.expireAt"
+            type="date"
+            placeholder="选择过期日期"
+            style="width:100%"
+          />
+        </el-form-item>
+        <el-form-item label="备注">
+          <el-input v-model="createForm.remark" type="textarea" :rows="3" placeholder="可选备注" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="createVisible = false">取消</el-button>
+        <el-button type="primary" :loading="creating" @click="submitCreate">创建</el-button>
+      </template>
+    </el-dialog>
+
+    <!-- 创建成功展示明文 Key -->
+    <el-dialog
+      v-model="keyRevealVisible"
+      title="API Key 创建成功"
+      width="520px"
+      :close-on-click-modal="false"
+      :close-on-press-escape="false"
+      :show-close="false"
+    >
+      <el-alert
+        title="请立即复制并保存此 Key,关闭后将无法再次查看明文。"
+        type="warning"
+        :closable="false"
+        show-icon
+        style="margin-bottom:16px"
+      />
+      <div class="key-reveal-box">
+        <code>{{ revealedKeyValue }}</code>
+      </div>
+      <template #footer>
+        <el-button type="primary" @click="confirmKeyReveal">我已复制并保存</el-button>
+      </template>
+    </el-dialog>
+
+    <!-- 轮换对话框 -->
+    <el-dialog
+      v-model="rotateVisible"
+      title="轮换 API Key"
+      width="480px"
+      destroy-on-close
+    >
+      <el-alert
+        title="轮换将生成新的 Key,旧 Key 将在过渡期结束后失效。"
+        type="info"
+        :closable="false"
+        show-icon
+        style="margin-bottom:16px"
+      />
+      <el-form :model="rotateForm" label-width="120px">
+        <el-form-item label="过渡期(天)">
+          <el-input-number v-model="rotateForm.transitionDays" :min="1" :max="30" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="rotateVisible = false">取消</el-button>
+        <el-button type="primary" :loading="rotating" @click="submitRotate">开始轮换</el-button>
+      </template>
+    </el-dialog>
   </div>
 </template>
 
 <script setup lang="ts">
-import { onMounted, ref } from 'vue';
+import { onMounted, ref, reactive } from 'vue';
+import { ElMessage, ElMessageBox } from 'element-plus';
+import type { FormInstance, FormRules } from 'element-plus';
 import { api } from '@/api/services';
-import SpecPageRenderer from '@/components/SpecPageRenderer.vue';
 import type { ApiKeyItem } from '@/types/page';
 
 const items = ref<ApiKeyItem[]>([]);
+const creating = ref(false);
+const rotating = ref(false);
+const createVisible = ref(false);
+const keyRevealVisible = ref(false);
+const rotateVisible = ref(false);
+const revealedKeyValue = ref('');
+const rotatingKeyId = ref('');
+const createFormRef = ref<FormInstance>();
 
-onMounted(async () => {
-  const response = await api.getApiKeys();
-  items.value = response.items;
+const defaultCreateForm = () => ({
+  name: '',
+  system: '',
+  scope: '',
+  ipWhitelist: '',
+  expireAt: '',
+  remark: ''
 });
+
+const createForm = reactive(defaultCreateForm());
+
+const createFormRules: FormRules = {
+  name: [{ required: true, message: '请输入 Key 名称', trigger: 'blur' }],
+  system: [{ required: true, message: '请选择所属系统', trigger: 'change' }],
+  scope: [{ required: true, message: '请输入权限范围', trigger: 'blur' }],
+  expireAt: [{ required: true, message: '请选择过期时间', trigger: 'change' }]
+};
+
+const rotateForm = reactive({ transitionDays: 7 });
+
+const loadData = async () => {
+  const res = await api.getApiKeys();
+  items.value = res.items;
+};
+
+/* ---- 创建 Key ---- */
+const openCreateDialog = () => {
+  Object.assign(createForm, defaultCreateForm());
+  createVisible.value = true;
+};
+
+const submitCreate = async () => {
+  await createFormRef.value?.validate();
+  creating.value = true;
+  try {
+    const res = await api.createApiKey({ ...createForm } as Partial<ApiKeyItem>);
+    createVisible.value = false;
+
+    // Show plaintext key (only once)
+    if (res.keyValue) {
+      revealedKeyValue.value = res.keyValue;
+      keyRevealVisible.value = true;
+    }
+    loadData();
+  } finally {
+    creating.value = false;
+  }
+};
+
+const confirmKeyReveal = () => {
+  keyRevealVisible.value = false;
+  revealedKeyValue.value = '';
+};
+
+/* ---- 复制 Key ---- */
+const copyKey = async (item: ApiKeyItem) => {
+  try {
+    await ElMessageBox.confirm(
+      '复制 Key 将把密钥放入剪贴板,请确保环境安全。',
+      '复制确认',
+      { confirmButtonText: '确认复制', cancelButtonText: '取消', type: 'warning' }
+    );
+    // In real app, would fetch decrypted key from API
+    navigator.clipboard?.writeText(`[MASKED-KEY-${item.id}]`).then(() => {
+      ElMessage.success('已复制到剪贴板');
+    }).catch(() => {
+      ElMessage.error('复制失败,请手动复制');
+    });
+  } catch {
+    // cancelled
+  }
+};
+
+/* ---- 启停用 ---- */
+const toggleKeyStatus = async (item: ApiKeyItem) => {
+  const action = item.status === '启用' ? '停用' : '启用';
+  try {
+    await ElMessageBox.confirm(
+      `确认${action} Key「${item.name}」?${action === '停用' ? '停用后使用此 Key 的接口将无法访问。' : ''}`,
+      `${action}确认`,
+      { confirmButtonText: '确认', cancelButtonText: '取消', type: 'warning' }
+    );
+    await api.updateApiKey(item.id, { status: action } as Partial<ApiKeyItem>);
+    ElMessage.success(`已${action} Key「${item.name}」`);
+    loadData();
+  } catch {
+    // cancelled
+  }
+};
+
+/* ---- 轮换 ---- */
+const rotateKey = (item: ApiKeyItem) => {
+  rotatingKeyId.value = item.id;
+  rotateForm.transitionDays = 7;
+  rotateVisible.value = true;
+};
+
+const submitRotate = async () => {
+  rotating.value = true;
+  try {
+    await api.updateApiKey(rotatingKeyId.value, {
+      status: '轮换中'
+    } as Partial<ApiKeyItem>);
+    ElMessage.success(`轮换已启动,旧 Key 将在 ${rotateForm.transitionDays} 天后失效`);
+    rotateVisible.value = false;
+    loadData();
+  } finally {
+    rotating.value = false;
+  }
+};
+
+/* ---- 删除 ---- */
+const deleteKey = async (item: ApiKeyItem) => {
+  try {
+    await ElMessageBox.confirm(
+      `确认删除 Key「${item.name}」?此操作不可恢复,使用此 Key 的所有接口调用将立即失效。`,
+      '删除确认',
+      { confirmButtonText: '确认删除', cancelButtonText: '取消', type: 'error' }
+    );
+    await api.deleteApiKey(item.id);
+    ElMessage.success(`已删除 Key「${item.name}」`);
+    loadData();
+  } catch {
+    // cancelled
+  }
+};
+
+onMounted(loadData);
 </script>
+
+<style scoped>
+.key-reveal-box {
+  background: var(--el-fill-color);
+  border: 1px solid var(--el-border-color);
+  border-radius: 4px;
+  padding: 16px;
+  text-align: center;
+}
+
+.key-reveal-box code {
+  font-size: 16px;
+  font-weight: 600;
+  word-break: break-all;
+  color: var(--el-color-primary);
+}
+</style>

+ 210 - 12
src/views/system/OperationLogView.vue

@@ -1,5 +1,6 @@
 <template>
   <div class="app-page">
+    <!-- 页面头部 -->
     <section class="glass-card page-hero">
       <div class="page-hero__meta">
         <span class="page-hero__eyebrow">Audit</span>
@@ -12,32 +13,229 @@
       </div>
     </section>
 
+    <!-- 筛选区 -->
     <section class="glass-card section-card">
-      <el-table :data="items" stripe style="width: 100%">
+      <el-form :model="filters" inline class="filter-form">
+        <el-form-item label="操作人">
+          <el-input v-model="filters.actor" placeholder="请输入操作人" clearable style="width:150px" @keyup.enter="loadData" />
+        </el-form-item>
+        <el-form-item label="模块">
+          <el-select v-model="filters.module" placeholder="全部模块" clearable style="width:140px">
+            <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-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-option label="退款" value="退款" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="时间范围">
+          <el-date-picker
+            v-model="filters.timeRange"
+            type="daterange"
+            start-placeholder="开始日期"
+            end-placeholder="结束日期"
+            style="width:240px"
+          />
+        </el-form-item>
+        <el-form-item label="对象 ID">
+          <el-input v-model="filters.objectId" placeholder="请输入对象 ID" clearable style="width:180px" @keyup.enter="loadData" />
+        </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>
+      </div>
+    </section>
+
+    <!-- 日志表格 -->
+    <section class="glass-card section-card">
+      <el-table
+        :data="filteredItems"
+        stripe
+        style="width:100%"
+        :row-class-name="rowClass"
+        @row-click="openDetail"
+        highlight-current-row
+      >
         <el-table-column prop="time" label="操作时间" width="180" />
-        <el-table-column prop="actor" label="操作人" width="180" />
-        <el-table-column prop="module" label="模块" width="130" />
-        <el-table-column prop="type" label="操作动作" width="120" />
+        <el-table-column prop="actor" label="操作人" width="140" />
+        <el-table-column prop="module" label="模块" width="100" />
+        <el-table-column label="操作动作" width="110">
+          <template #default="{ row }">
+            <span :class="actionClass(row.type)">{{ row.type }}</span>
+          </template>
+        </el-table-column>
         <el-table-column prop="objectId" label="对象 ID" width="180" />
-        <el-table-column prop="result" label="结果状态" width="100" />
-        <el-table-column prop="sourceIp" label="来源 IP" width="130" />
+        <el-table-column label="结果状态" width="100">
+          <template #default="{ row }">
+            <el-tag :type="row.result === '成功' ? 'success' : 'danger'" size="small">{{ row.result }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column prop="sourceIp" label="来源 IP" width="140" />
+        <el-table-column label="操作" width="80" fixed="right">
+          <template #default="{ row }">
+            <el-button link type="primary" @click.stop="openDetail(row)">详情</el-button>
+          </template>
+        </el-table-column>
       </el-table>
     </section>
 
-    <SpecPageRenderer page-key="operation-log" />
+    <!-- 详情抽屉 -->
+    <el-drawer
+      v-model="detailVisible"
+      title="操作日志详情"
+      size="560px"
+      destroy-on-close
+    >
+      <template v-if="detailItem">
+        <el-descriptions :column="2" border size="small" style="margin-bottom:20px">
+          <el-descriptions-item label="操作时间" :span="2">{{ detailItem.time }}</el-descriptions-item>
+          <el-descriptions-item label="操作人">{{ detailItem.actor }}</el-descriptions-item>
+          <el-descriptions-item label="来源 IP">{{ detailItem.sourceIp }}</el-descriptions-item>
+          <el-descriptions-item label="模块">{{ detailItem.module }}</el-descriptions-item>
+          <el-descriptions-item label="操作动作">
+            <span :class="actionClass(detailItem.type)">{{ detailItem.type }}</span>
+          </el-descriptions-item>
+          <el-descriptions-item label="对象 ID" :span="2">{{ detailItem.objectId }}</el-descriptions-item>
+          <el-descriptions-item label="结果状态">
+            <el-tag :type="detailItem.result === '成功' ? 'success' : 'danger'" size="small">{{ detailItem.result }}</el-tag>
+          </el-descriptions-item>
+        </el-descriptions>
+
+        <h4 style="margin-bottom:12px">变更详情</h4>
+
+        <div class="diff-section">
+          <div class="diff-label">变更前</div>
+          <pre class="diff-content">{{ detailItem.beforeValue || '(无)' }}</pre>
+        </div>
+
+        <div class="diff-section">
+          <div class="diff-label">变更后</div>
+          <pre class="diff-content">{{ detailItem.afterValue || '(无)' }}</pre>
+        </div>
+
+        <div v-if="detailItem.remark" class="diff-section">
+          <div class="diff-label">备注</div>
+          <div class="diff-content">{{ detailItem.remark }}</div>
+        </div>
+      </template>
+    </el-drawer>
   </div>
 </template>
 
 <script setup lang="ts">
-import { onMounted, ref } from 'vue';
+import { computed, onMounted, ref } from 'vue';
+import { ElMessage } from 'element-plus';
 import { api } from '@/api/services';
-import SpecPageRenderer from '@/components/SpecPageRenderer.vue';
 import type { LogItem } from '@/types/page';
 
 const items = ref<LogItem[]>([]);
+const detailVisible = ref(false);
+const detailItem = ref<LogItem | null>(null);
+
+const highRiskActions = ['删除', '取消', '退款'];
+
+const filters = ref({
+  actor: '',
+  module: '',
+  type: '',
+  timeRange: null as [Date, Date] | null,
+  objectId: ''
+});
 
-onMounted(async () => {
-  const response = await api.getLogs();
-  items.value = response.items;
+const filteredItems = computed(() => {
+  return items.value.filter((item) => {
+    if (filters.value.actor && !item.actor.includes(filters.value.actor)) return false;
+    if (filters.value.module && item.module !== filters.value.module) return false;
+    if (filters.value.type && item.type !== filters.value.type) return false;
+    if (filters.value.objectId && !item.objectId.includes(filters.value.objectId)) return false;
+    return true;
+  });
 });
+
+const actionClass = (type: string) => {
+  return highRiskActions.includes(type) ? 'action-danger' : '';
+};
+
+const rowClass = ({ row }: { row: LogItem }) => {
+  return highRiskActions.includes(row.type) ? 'danger-row' : '';
+};
+
+const loadData = async () => {
+  const res = await api.getLogs();
+  items.value = res.items;
+};
+
+const resetFilters = () => {
+  filters.value = { actor: '', module: '', type: '', timeRange: null, objectId: '' };
+};
+
+const openDetail = (row: LogItem) => {
+  detailItem.value = row;
+  detailVisible.value = true;
+};
+
+const doExport = () => {
+  if (filteredItems.value.length === 0) {
+    ElMessage.warning('无数据可导出');
+    return;
+  }
+  ElMessage.info(`正在导出 ${filteredItems.value.length} 条日志,完成后自动下载`);
+};
+
+onMounted(loadData);
 </script>
+
+<style scoped>
+.filter-form :deep(.el-form-item) { margin-bottom: 0; }
+
+.action-danger {
+  color: var(--el-color-danger);
+  font-weight: 600;
+}
+
+:deep(.danger-row) {
+  background-color: rgba(245, 108, 108, 0.06) !important;
+}
+
+.diff-section {
+  margin-bottom: 16px;
+}
+
+.diff-label {
+  font-size: 13px;
+  color: var(--cb-text-soft);
+  margin-bottom: 6px;
+}
+
+.diff-content {
+  background: var(--el-fill-color-light);
+  border: 1px solid var(--el-border-color-lighter);
+  border-radius: 4px;
+  padding: 12px;
+  font-size: 13px;
+  white-space: pre-wrap;
+  word-break: break-all;
+  margin: 0;
+  font-family: inherit;
+}
+</style>

+ 375 - 43
src/views/system/RolePermissionView.vue

@@ -1,10 +1,11 @@
 <template>
   <div class="app-page">
+    <!-- 页面头部 -->
     <section class="glass-card page-hero">
       <div class="page-hero__meta">
         <span class="page-hero__eyebrow">RBAC</span>
         <h1>角色权限配置</h1>
-        <p>这一版先把角色列表、权限矩阵说明和操作动作放进页面骨架,后续适合继续扩展成树形权限矩阵。</p>
+        <p>管理角色列表、权限矩阵和操作动作,支持树形权限矩阵和高风险操作二次确认。</p>
       </div>
       <div class="chip-list">
         <span class="chip">页面权限 + 按钮权限</span>
@@ -12,58 +13,389 @@
       </div>
     </section>
 
-    <section class="page-grid page-grid--two">
-      <article class="glass-card section-card">
-        <div class="section-card__title">
-          <div>
-            <h3>角色列表</h3>
-            <p>当前以角色概览表为主。</p>
-          </div>
-        </div>
-        <el-table :data="items" stripe style="width: 100%">
-          <el-table-column prop="name" label="角色名称" width="120" />
-          <el-table-column prop="scope" label="权限范围" min-width="220" />
-          <el-table-column prop="boundUsers" label="绑定用户数" width="120" />
-          <el-table-column prop="status" label="状态" width="100" />
-          <el-table-column prop="updatedAt" label="更新时间" width="170" />
-        </el-table>
-      </article>
-
-      <article class="glass-card section-card">
-        <div class="section-card__title">
-          <div>
-            <h3>权限矩阵建议</h3>
-            <p>可继续扩成树形矩阵 + 数据范围抽屉。</p>
-          </div>
-        </div>
-        <ul class="tight-list">
-          <li>页面访问权限:按模块控制菜单可见。</li>
-          <li>按钮权限:控制新建、编辑、删除、退款、停用等操作。</li>
-          <li>数据范围权限:按仓库、店铺、国家、渠道限制可见数据。</li>
-          <li>特殊操作权限:取消订单、退款、删除 Key 等需额外校验。</li>
-        </ul>
-        <div class="chip-list" style="margin-top: 18px">
-          <el-button type="primary">新建角色</el-button>
-          <el-button>复制角色</el-button>
-          <el-button>保存权限</el-button>
-        </div>
-      </article>
+    <!-- 筛选区 -->
+    <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:180px" @keyup.enter="loadData" />
+        </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 label="更新时间">
+          <el-date-picker
+            v-model="filters.updateTimeRange"
+            type="daterange"
+            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>
 
-    <SpecPageRenderer page-key="role-permission" />
+    <!-- 工具栏 -->
+    <section class="glass-card section-card" style="padding:12px 24px">
+      <div class="table-toolbar" style="margin-bottom:0">
+        <el-button type="primary" @click="openCreateRole">新建角色</el-button>
+      </div>
+    </section>
+
+    <!-- 角色列表 -->
+    <section class="glass-card section-card">
+      <el-table :data="filteredItems" stripe style="width:100%">
+        <el-table-column prop="name" label="角色名称" width="140" />
+        <el-table-column prop="description" label="描述" min-width="220" />
+        <el-table-column prop="boundUsers" label="绑定用户数" width="120">
+          <template #default="{ row }">
+            <el-tag size="small">{{ row.boundUsers }} 人</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="状态" width="100">
+          <template #default="{ row }">
+            <el-tag :type="row.status === '启用' ? 'success' : 'info'" size="small">{{ row.status }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column prop="updatedAt" label="更新时间" width="170" />
+        <el-table-column label="操作" width="260" fixed="right">
+          <template #default="{ row }">
+            <el-button link type="primary" @click="openEditPermissions(row)">编辑权限</el-button>
+            <el-button link type="primary" @click="copyRole(row)">复制角色</el-button>
+            <el-button
+              link
+              :type="row.status === '启用' ? 'danger' : 'success'"
+              @click="toggleRoleStatus(row)"
+            >
+              {{ row.status === '启用' ? '停用' : '启用' }}
+            </el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+    </section>
+
+    <!-- 角色权限表单抽屉 -->
+    <el-drawer
+      v-model="permissionDrawerVisible"
+      :title="isEditRole ? '编辑角色权限' : '新建角色'"
+      size="600px"
+      destroy-on-close
+    >
+      <el-form
+        ref="roleFormRef"
+        :model="roleForm"
+        :rules="roleFormRules"
+        label-width="100px"
+        label-position="top"
+      >
+        <el-form-item label="角色名称" prop="name">
+          <el-input v-model="roleForm.name" placeholder="请输入角色名称" />
+        </el-form-item>
+        <el-form-item label="描述" prop="description">
+          <el-input v-model="roleForm.description" type="textarea" :rows="2" placeholder="请输入角色描述" />
+        </el-form-item>
+
+        <!-- 页面访问权限 -->
+        <el-form-item label="页面访问权限">
+          <el-tree
+            ref="pageTreeRef"
+            :data="pagePermissionTree"
+            show-checkbox
+            node-key="id"
+            :default-checked-keys="roleForm.pagePermissions"
+            :props="{ label: 'label', children: 'children' }"
+            style="width:100%;border:1px solid var(--el-border-color-lighter);border-radius:4px;padding:8px"
+          />
+        </el-form-item>
+
+        <!-- 按钮权限 -->
+        <el-form-item label="按钮权限">
+          <el-checkbox-group v-model="roleForm.buttonPermissions">
+            <el-checkbox v-for="btn in buttonPermissionOptions" :key="btn.value" :label="btn.value">
+              {{ btn.label }}
+            </el-checkbox>
+          </el-checkbox-group>
+        </el-form-item>
+
+        <!-- 数据范围权限 -->
+        <el-form-item label="数据范围权限">
+          <el-select v-model="roleForm.dataScope" placeholder="请选择数据范围" style="width:100%">
+            <el-option label="全部数据" value="all" />
+            <el-option label="本仓库数据" value="warehouse" />
+            <el-option label="本店铺数据" value="shop" />
+            <el-option label="本人数据" value="self" />
+          </el-select>
+        </el-form-item>
+
+        <!-- 特殊操作权限 -->
+        <el-form-item label="特殊操作权限">
+          <el-checkbox-group v-model="roleForm.specialPermissions">
+            <el-checkbox
+              v-for="sp in specialPermissionOptions"
+              :key="sp.value"
+              :label="sp.value"
+            >
+              <span :class="{ 'high-risk-label': sp.highRisk }">{{ sp.label }}</span>
+              <el-tag v-if="sp.highRisk" type="danger" size="small" style="margin-left:6px">高风险</el-tag>
+            </el-checkbox>
+          </el-checkbox-group>
+        </el-form-item>
+      </el-form>
+
+      <template #footer>
+        <el-button @click="permissionDrawerVisible = false">取消</el-button>
+        <el-button type="primary" :loading="submitting" @click="submitRole">保存</el-button>
+      </template>
+    </el-drawer>
   </div>
 </template>
 
 <script setup lang="ts">
-import { onMounted, ref } from 'vue';
+import { computed, onMounted, ref, reactive } from 'vue';
+import { ElMessage, ElMessageBox } from 'element-plus';
+import type { FormInstance, FormRules } from 'element-plus';
 import { api } from '@/api/services';
-import SpecPageRenderer from '@/components/SpecPageRenderer.vue';
 import type { RoleItem } from '@/types/page';
 
 const items = ref<RoleItem[]>([]);
+const permissionDrawerVisible = ref(false);
+const isEditRole = ref(false);
+const editingRoleId = ref('');
+const submitting = ref(false);
+const roleFormRef = ref<FormInstance>();
+const pageTreeRef = ref<InstanceType<typeof import('element-plus')['ElTree']>>();
+
+const filters = ref({
+  name: '',
+  status: '',
+  updateTimeRange: null as [Date, Date] | null
+});
+
+const filteredItems = computed(() => {
+  return items.value.filter((item) => {
+    if (filters.value.name && !item.name.includes(filters.value.name)) return false;
+    if (filters.value.status && item.status !== filters.value.status) return false;
+    return true;
+  });
+});
+
+/* ---- 权限树 ---- */
+const pagePermissionTree = [
+  {
+    id: 'product',
+    label: '商品管理',
+    children: [
+      { id: 'product-list', label: '商品列表' },
+      { id: 'product-editor', label: '商品编辑' },
+      { id: 'product-mapping', label: '商品映射' },
+      { id: 'product-pricing', label: '价格管理' }
+    ]
+  },
+  {
+    id: 'order',
+    label: '订单管理',
+    children: [
+      { id: 'order-list', label: '订单列表' },
+      { id: 'order-detail', label: '订单详情' },
+      { id: 'order-after-sale', label: '售后管理' }
+    ]
+  },
+  {
+    id: 'inventory',
+    label: '库存管理',
+    children: [
+      { id: 'inventory-overview', label: '库存总览' },
+      { id: 'shipping-work', label: '发货作业' }
+    ]
+  },
+  {
+    id: 'supplier',
+    label: '供应商管理',
+    children: [
+      { id: 'supplier-list', label: '供应商列表' },
+      { id: 'purchase-order', label: '采购单管理' }
+    ]
+  },
+  {
+    id: 'channel',
+    label: '渠道管理',
+    children: [{ id: 'channel-config', label: '渠道配置' }]
+  },
+  {
+    id: 'report',
+    label: '报表中心',
+    children: [{ id: 'report-center', label: '报表中心' }]
+  },
+  {
+    id: 'system',
+    label: '系统管理',
+    children: [
+      { id: 'role-permission', label: '角色权限' },
+      { id: 'operation-log', label: '操作日志' },
+      { id: 'api-key', label: 'API Key 管理' }
+    ]
+  }
+];
+
+const buttonPermissionOptions = [
+  { label: '新建', value: 'create' },
+  { label: '编辑', value: 'edit' },
+  { label: '删除', value: 'delete' },
+  { label: '导出', value: 'export' },
+  { label: '退款', value: 'refund' },
+  { label: '停用', value: 'disable' },
+  { label: '审批', value: 'approve' }
+];
 
-onMounted(async () => {
-  const response = await api.getRoles();
-  items.value = response.items;
+const specialPermissionOptions = [
+  { label: '取消订单', value: 'cancel_order', highRisk: true },
+  { label: '退款操作', value: 'refund', highRisk: true },
+  { label: '删除 API Key', value: 'delete_apikey', highRisk: true },
+  { label: '修改角色权限', value: 'edit_role', highRisk: true },
+  { label: '查看成本价', value: 'view_cost', highRisk: false },
+  { label: '批量操作', value: 'batch_operation', highRisk: false }
+];
+
+/* ---- 角色表单 ---- */
+const defaultRoleForm = () => ({
+  name: '',
+  description: '',
+  pagePermissions: [] as string[],
+  buttonPermissions: [] as string[],
+  dataScope: 'self',
+  specialPermissions: [] as string[]
 });
+
+const roleForm = reactive(defaultRoleForm());
+
+const roleFormRules: FormRules = {
+  name: [{ required: true, message: '请输入角色名称', trigger: 'blur' }],
+  description: [{ required: true, message: '请输入角色描述', trigger: 'blur' }]
+};
+
+/* ---- 数据加载 ---- */
+const loadData = async () => {
+  const res = await api.getRoles();
+  items.value = res.items;
+};
+
+const resetFilters = () => {
+  filters.value = { name: '', status: '', updateTimeRange: null };
+};
+
+/* ---- 新建角色 ---- */
+const openCreateRole = () => {
+  Object.assign(roleForm, defaultRoleForm());
+  isEditRole.value = false;
+  editingRoleId.value = '';
+  permissionDrawerVisible.value = true;
+};
+
+/* ---- 编辑权限 ---- */
+const openEditPermissions = (item: RoleItem) => {
+  Object.assign(roleForm, defaultRoleForm());
+  isEditRole.value = true;
+  editingRoleId.value = item.id;
+  roleForm.name = item.name;
+  roleForm.description = item.description;
+  roleForm.pagePermissions = item.permissions || [];
+  permissionDrawerVisible.value = true;
+};
+
+/* ---- 复制角色 ---- */
+const copyRole = (item: RoleItem) => {
+  Object.assign(roleForm, defaultRoleForm());
+  isEditRole.value = false;
+  editingRoleId.value = '';
+  roleForm.name = `${item.name} (副本)`;
+  roleForm.description = item.description;
+  roleForm.pagePermissions = item.permissions || [];
+  permissionDrawerVisible.value = true;
+};
+
+/* ---- 启停用 ---- */
+const toggleRoleStatus = async (item: RoleItem) => {
+  const action = item.status === '启用' ? '停用' : '启用';
+  if (action === '停用' && item.boundUsers > 0) {
+    try {
+      await ElMessageBox.confirm(
+        `角色「${item.name}」当前绑定了 ${item.boundUsers} 个用户,停用后这些用户将失去对应权限。确认停用?`,
+        '停用确认',
+        { confirmButtonText: '确认停用', cancelButtonText: '取消', type: 'warning' }
+      );
+    } catch {
+      return;
+    }
+  } else {
+    try {
+      await ElMessageBox.confirm(
+        `确认${action}角色「${item.name}」?`,
+        `${action}确认`,
+        { confirmButtonText: '确认', cancelButtonText: '取消', type: 'info' }
+      );
+    } catch {
+      return;
+    }
+  }
+
+  await api.updateRole(item.id, { status: action } as Partial<RoleItem>);
+  ElMessage.success(`已${action}角色「${item.name}」`);
+  loadData();
+};
+
+/* ---- 提交角色 ---- */
+const submitRole = async () => {
+  await roleFormRef.value?.validate();
+
+  // Check high-risk permissions
+  const highRiskSelected = roleForm.specialPermissions.filter((sp) =>
+    specialPermissionOptions.some((opt) => opt.value === sp && opt.highRisk)
+  );
+  if (highRiskSelected.length > 0) {
+    try {
+      await ElMessageBox.confirm(
+        `当前已选择高风险权限:${highRiskSelected.join('、')},请确认授权。`,
+        '高风险权限确认',
+        { confirmButtonText: '确认授权', cancelButtonText: '取消', type: 'warning' }
+      );
+    } catch {
+      return;
+    }
+  }
+
+  submitting.value = true;
+  try {
+    const checkedKeys = pageTreeRef.value?.getCheckedKeys() || [];
+    const payload = {
+      name: roleForm.name,
+      description: roleForm.description,
+      permissions: checkedKeys as string[]
+    };
+
+    if (isEditRole.value) {
+      await api.updateRole(editingRoleId.value, payload as Partial<RoleItem>);
+      ElMessage.success('角色权限已更新');
+    } else {
+      await api.createRole(payload as Partial<RoleItem>);
+      ElMessage.success('角色已创建');
+    }
+    permissionDrawerVisible.value = false;
+    loadData();
+  } finally {
+    submitting.value = false;
+  }
+};
+
+onMounted(loadData);
 </script>
+
+<style scoped>
+.filter-form :deep(.el-form-item) { margin-bottom: 0; }
+.high-risk-label { color: var(--el-color-danger); font-weight: 500; }
+</style>