Przeglądaj źródła

style(admin): 商品管理页企业级视觉重构

- 标题区主操作唯一化:新增商品置为唯一 primary,批量操作降为次按钮
- 筛选区改为「标签+控件」结构,去除 el-card 内套 filter-bar 的卡中卡阴影
- 数值列右对齐+等宽数字+售价千分位;状态标签标准化为浅底深字+同色点
- 操作列由 280px 缩减为 176px 并降为文本按钮,主色覆盖率收敛至 5% 内
- 新增批量选中提示条;弹窗区块标题/圆角/分割线统一;表格高度改为响应式

Co-Authored-By: WorkBuddy <workbuddy@tencent.com>
liaoxg 5 dni temu
rodzic
commit
dbb4b8c95b
1 zmienionych plików z 563 dodań i 236 usunięć
  1. 563 236
      cfc-web/src/views/admin/ProductManage.vue

+ 563 - 236
cfc-web/src/views/admin/ProductManage.vue

@@ -1,235 +1,313 @@
 <template>
-  <div class="product-manage admin-page">
-    <el-card>
-      <div slot="header" class="admin-page-header">
-        <span class="admin-page-title">商品管理</span>
-        <el-button type="success" size="mini" icon="el-icon-plus" @click="$router.push('/product-edit')">新增商品</el-button>
-        <el-dropdown trigger="hover" @command="handleBatchCmd">
-          <el-button size="mini" type="primary">
-            批量<i class="el-icon-arrow-down el-icon--right"></i>
+  <div class="product-manage">
+    <el-card class="pm-card" shadow="never">
+      <!-- 页面标题区:唯一主操作「新增商品」,其余降为次按钮 -->
+      <div class="pm-header">
+        <div class="pm-header__left">
+          <h1 class="pm-title">商品管理</h1>
+          <span class="pm-subtitle">共 {{ pagination.total }} 个商品</span>
+        </div>
+        <div class="pm-header__right">
+          <el-dropdown trigger="click" @command="handleBatchCmd">
+            <el-button size="small">
+              批量操作<i class="el-icon-arrow-down el-icon--right"></i>
+            </el-button>
+            <el-dropdown-menu slot="dropdown">
+              <el-dropdown-item command="batchOnShelf" icon="el-icon-upload2">批量上架</el-dropdown-item>
+              <el-dropdown-item command="batchOffShelf" icon="el-icon-download">批量下架</el-dropdown-item>
+            </el-dropdown-menu>
+          </el-dropdown>
+          <el-button size="small" type="primary" icon="el-icon-plus" @click="$router.push('/product-edit')">
+            新增商品
           </el-button>
-          <el-dropdown-menu slot="dropdown">
-            <el-dropdown-item command="batchOnShelf">批量上架</el-dropdown-item>
-            <el-dropdown-item command="batchOffShelf">批量下架</el-dropdown-item>
-          </el-dropdown-menu>
-        </el-dropdown>
+        </div>
       </div>
 
-      <div class="filter-bar" style="margin-bottom:12px;">
-        <el-input
-          v-model="filters.keyword"
-          placeholder="搜索商品名称"
-          prefix-icon="el-icon-search"
-          clearable
-          style="width:200px;"
-          @clear="loadList"
-          @keyup.enter.native="loadList"
-        />
-        <el-select v-model="filters.status" placeholder="商品状态" @change="loadList" clearable style="width:130px;">
-          <el-option label="全部状态" value="" />
-          <el-option label="待审核" value="pending" />
-          <el-option label="待上架" value="approved" />
-          <el-option label="已上架" value="on_shelf" />
-          <el-option label="已下架" value="off_shelf" />
-          <el-option label="已拒绝" value="rejected" />
-        </el-select>
-        <el-select v-model="filters.productType" placeholder="商品类型" @change="loadList" clearable style="width:140px;">
-          <el-option label="全部类型" value="" />
-          <el-option label="实物商品" value="physical" />
-          <el-option label="虚拟商品" value="virtual" />
-          <el-option label="优惠券" value="coupon" />
-          <el-option label="测评商品" value="assessment" />
-        </el-select>
-        <el-button type="primary" size="mini" @click="loadList">查询</el-button>
-        <el-button size="mini" @click="resetFilters">重置</el-button>
+      <!-- 筛选区:标签 + 控件结构,操作按钮居右 -->
+      <div class="pm-filter">
+        <div class="pm-filter__fields">
+          <div class="pm-field">
+            <label class="pm-field__label">关键词</label>
+            <el-input
+              v-model="filters.keyword"
+              placeholder="搜索商品名称"
+              prefix-icon="el-icon-search"
+              size="small"
+              clearable
+              class="pm-field__control pm-field__control--search"
+              @clear="loadList"
+              @keyup.enter.native="loadList"
+            />
+          </div>
+          <div class="pm-field">
+            <label class="pm-field__label">商品状态</label>
+            <el-select v-model="filters.status" placeholder="全部状态" size="small" clearable class="pm-field__control" @change="loadList">
+              <el-option label="全部状态" value="" />
+              <el-option label="待审核" value="pending" />
+              <el-option label="待上架" value="approved" />
+              <el-option label="已上架" value="on_shelf" />
+              <el-option label="已下架" value="off_shelf" />
+              <el-option label="已拒绝" value="rejected" />
+            </el-select>
+          </div>
+          <div class="pm-field">
+            <label class="pm-field__label">商品类型</label>
+            <el-select v-model="filters.productType" placeholder="全部类型" size="small" clearable class="pm-field__control" @change="loadList">
+              <el-option label="全部类型" value="" />
+              <el-option label="实物商品" value="physical" />
+              <el-option label="虚拟商品" value="virtual" />
+              <el-option label="优惠券" value="coupon" />
+              <el-option label="测评商品" value="assessment" />
+            </el-select>
+          </div>
+        </div>
+        <div class="pm-filter__actions">
+          <el-button size="small" @click="resetFilters">重置</el-button>
+          <el-button size="small" type="primary" @click="loadList">查询</el-button>
+        </div>
       </div>
 
-      <div class="table-scroll-wrap-sm">
-        <el-table :max-height="tableHeight" :data="list" v-loading="loading" border stripe @selection-change="handleSelectionChange">
-      <el-table-column type="selection" width="60" />
-      <el-table-column prop="id" label="ID" width="70" />
-      <el-table-column label="商品图片" width="80">
-        <template slot-scope="{ row }">
-          <img
-            v-if="row.coverImage"
-            :src="row.coverImage"
-            class="product-thumb"
-            @click="previewImage(row.coverImage)"
-          />
-          <span v-else style="color:#999">-</span>
-        </template>
-      </el-table-column>
-      <el-table-column prop="name" label="商品名称" min-width="150" show-overflow-tooltip>
-        <template slot="header">
-          <span class="sort-header" @click.stop="onSortClick('name', $event)" @dblclick.stop="onSortToggle('name')">商品名称<span v-if="sortIndex('name') !== null" class="sort-index" :class="sortClass('name') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('name') }}</span>{{ sortIcon('name') }}</span>
-        </template>
-      </el-table-column>
-
-      <el-table-column label="类型" width="90">
-        <template slot-scope="{ row }">
-          {{ typeLabel(row.productType) }}
-        </template>
-      </el-table-column>
-
-      <el-table-column label="售价(元)" width="90">
-        <template slot-scope="{ row }">
-          {{ formatPriceWithSymbol(row.price || 0) }}
-        </template>
-      </el-table-column>
-      <el-table-column prop="stock" label="库存" width="80">
-        <template slot-scope="{ row }">
-          <span :class="{ 'stock-warning': row.stock !== null && row.stock < 10 }">
-            {{ row.stock }}
-          </span>
-        </template>
-        <template slot="header">
-          <span class="sort-header" @click.stop="onSortClick('stock', $event)" @dblclick.stop="onSortToggle('stock')">库存<span v-if="sortIndex('stock') !== null" class="sort-index" :class="sortClass('stock') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('stock') }}</span>{{ sortIcon('stock') }}</span>
-        </template>
-      </el-table-column>
-      <el-table-column prop="salesCount" label="销量" width="70">
-        <template slot="header">
-          <span class="sort-header" @click.stop="onSortClick('salesCount', $event)" @dblclick.stop="onSortToggle('salesCount')">销量<span v-if="sortIndex('salesCount') !== null" class="sort-index" :class="sortClass('salesCount') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('salesCount') }}</span>{{ sortIcon('salesCount') }}</span>
-        </template>
-      </el-table-column>
-
-      <el-table-column label="供应商" width="120" show-overflow-tooltip>
-        <template slot-scope="{ row }">
-          {{ row.vendorName || '-' }}
-        </template>
-      </el-table-column>
-
-      <el-table-column label="状态" width="90">
-        <template slot-scope="{ row }">
-          <el-tag :type="statusType(row.status)" size="mini">{{ statusLabel(row.status) }}</el-tag>
-        </template>
-      </el-table-column>
-      <el-table-column prop="sortOrder" label="排序" width="80">
-        <template slot-scope="{ row }">
-          {{ row.sortOrder != null ? row.sortOrder : '-' }}
-        </template>
-        <template slot="header">
-          <span class="sort-header" @click.stop="onSortClick('sortOrder', $event)" @dblclick.stop="onSortToggle('sortOrder')">排序<span v-if="sortIndex('sortOrder') !== null" class="sort-index" :class="sortClass('sortOrder') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('sortOrder') }}</span>{{ sortIcon('sortOrder') }}</span>
-        </template>
-      </el-table-column>
-      <el-table-column label="最低可见级别" width="140">
-        <template slot-scope="{ row }">
-          <span v-if="row.minVisibleLevel">{{ levelLabel(row.minVisibleLevel) }}</span>
-          <span v-else style="color:#999">全部</span>
-        </template>
-      </el-table-column>
-      <el-table-column prop="createdAt" label="创建时间" width="160">
-        <template slot-scope="{ row }">
-          {{ formatTime(row.createdAt) }}
-        </template>
-          <template slot="header">
-            <span class="sort-header" @click.stop="onSortClick('createdAt', $event)" @dblclick.stop="onSortToggle('createdAt')">创建时间<span v-if="sortIndex('createdAt') !== null" class="sort-index" :class="sortClass('createdAt') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('createdAt') }}</span>{{ sortIcon('createdAt') }}</span>
-
-          </template></el-table-column>
-      <el-table-column label="操作" width="280" fixed="right">
-        <template slot-scope="{ row }">
-          <el-button size="mini" type="primary" @click="$router.push('/product-edit?id=' + row.id)">编辑</el-button>
-          <el-button size="mini" type="info" @click="showInventoryDialog(row)">库存</el-button>
-          <el-dropdown trigger="hover" @command="(cmd) => handleActionCmd(row, cmd)">
-            <el-button size="mini">
-              更多<i class="el-icon-arrow-down el-icon--right"></i>
-            </el-button>
-            <el-dropdown-menu slot="dropdown">
-              <el-dropdown-item command="copy" icon="el-icon-document-copy">复制</el-dropdown-item>
-              <el-dropdown-item v-if="row.status === 'pending'" command="approve" icon="el-icon-circle-check">通过</el-dropdown-item>
-              <el-dropdown-item v-if="row.status === 'pending'" command="reject" icon="el-icon-circle-close">拒绝</el-dropdown-item>
-              <el-dropdown-item v-if="row.status === 'on_shelf'" command="offShelf" icon="el-icon-download">下架</el-dropdown-item>
-              <el-dropdown-item v-if="row.status === 'off_shelf'" command="onShelf" icon="el-icon-upload2">上架</el-dropdown-item>
-            </el-dropdown-menu>
-          </el-dropdown>
-        </template>
-      </el-table-column>
+      <!-- 批量选择提示条:仅在选择后出现,避免常态占位 -->
+      <div v-if="selectedIds.length" class="pm-batch-bar">
+        <span class="pm-batch-bar__text">已选 <b>{{ selectedIds.length }}</b> 个商品</span>
+        <div class="pm-batch-bar__actions">
+          <el-button type="text" size="small" @click="handleBatchShelve(true)">批量上架</el-button>
+          <el-divider direction="vertical"></el-divider>
+          <el-button type="text" size="small" @click="handleBatchShelve(false)">批量下架</el-button>
+          <el-divider direction="vertical"></el-divider>
+          <el-button type="text" size="small" class="pm-text-muted" @click="clearSelection">清空选择</el-button>
+        </div>
+      </div>
+
+      <!-- 表格区:数值右对齐等宽、状态标准化标签、操作列降为文本按钮 -->
+      <div class="pm-table-wrap">
+        <el-table
+          ref="table"
+          :max-height="tableHeight"
+          :data="list"
+          v-loading="loading"
+          border
+          size="small"
+          @selection-change="handleSelectionChange"
+        >
+          <el-table-column type="selection" width="48" align="center" />
+          <el-table-column prop="id" label="ID" width="72" align="right" class-name="pm-num" />
+          <el-table-column label="图片" width="72" align="center">
+            <template slot-scope="{ row }">
+              <img
+                v-if="row.coverImage"
+                :src="row.coverImage"
+                class="pm-thumb"
+                @click="previewImage(row.coverImage)"
+              />
+              <span v-else class="pm-placeholder">—</span>
+            </template>
+          </el-table-column>
+          <el-table-column prop="name" label="商品名称" min-width="180" show-overflow-tooltip>
+            <template slot="header">
+              <span
+                class="pm-sort-header"
+                title="单击切换排序,双击取消"
+                @click.stop="onSortClick('name', $event)"
+                @dblclick.stop="onSortToggle('name')"
+              >
+                商品名称<span v-if="sortIndex('name') !== null" class="pm-sort-index">{{ sortIndex('name') }}</span>{{ sortIcon('name') }}
+              </span>
+            </template>
+          </el-table-column>
+          <el-table-column label="类型" width="96">
+            <template slot-scope="{ row }">{{ typeLabel(row.productType) }}</template>
+          </el-table-column>
+          <el-table-column label="售价(元)" width="112" align="right" header-align="right" class-name="pm-num">
+            <template slot-scope="{ row }">{{ formatPriceWithSymbol(row.price || 0) }}</template>
+          </el-table-column>
+          <el-table-column prop="stock" label="库存" width="88" align="right" header-align="right" class-name="pm-num">
+            <template slot-scope="{ row }">
+              <span :class="{ 'pm-stock-warning': row.stock !== null && row.stock < 10 }">{{ row.stock }}</span>
+            </template>
+            <template slot="header">
+              <span
+                class="pm-sort-header"
+                title="单击切换排序,双击取消"
+                @click.stop="onSortClick('stock', $event)"
+                @dblclick.stop="onSortToggle('stock')"
+              >
+                库存<span v-if="sortIndex('stock') !== null" class="pm-sort-index">{{ sortIndex('stock') }}</span>{{ sortIcon('stock') }}
+              </span>
+            </template>
+          </el-table-column>
+          <el-table-column prop="salesCount" label="销量" width="88" align="right" header-align="right" class-name="pm-num">
+            <template slot="header">
+              <span
+                class="pm-sort-header"
+                title="单击切换排序,双击取消"
+                @click.stop="onSortClick('salesCount', $event)"
+                @dblclick.stop="onSortToggle('salesCount')"
+              >
+                销量<span v-if="sortIndex('salesCount') !== null" class="pm-sort-index">{{ sortIndex('salesCount') }}</span>{{ sortIcon('salesCount') }}
+              </span>
+            </template>
+          </el-table-column>
+          <el-table-column label="供应商" width="140" show-overflow-tooltip>
+            <template slot-scope="{ row }">
+              <span :class="{ 'pm-placeholder': !row.vendorName }">{{ row.vendorName || '—' }}</span>
+            </template>
+          </el-table-column>
+          <el-table-column label="状态" width="96">
+            <template slot-scope="{ row }">
+              <span class="pm-status" :class="'pm-status--' + row.status">
+                <i class="pm-status__dot"></i>{{ statusLabel(row.status) }}
+              </span>
+            </template>
+          </el-table-column>
+          <el-table-column prop="sortOrder" label="排序" width="88" align="right" header-align="right" class-name="pm-num">
+            <template slot-scope="{ row }">{{ row.sortOrder != null ? row.sortOrder : '—' }}</template>
+            <template slot="header">
+              <span
+                class="pm-sort-header"
+                title="单击切换排序,双击取消"
+                @click.stop="onSortClick('sortOrder', $event)"
+                @dblclick.stop="onSortToggle('sortOrder')"
+              >
+                排序<span v-if="sortIndex('sortOrder') !== null" class="pm-sort-index">{{ sortIndex('sortOrder') }}</span>{{ sortIcon('sortOrder') }}
+              </span>
+            </template>
+          </el-table-column>
+          <el-table-column label="最低可见级别" width="128">
+            <template slot-scope="{ row }">
+              <span :class="{ 'pm-placeholder': !row.minVisibleLevel }">{{ levelLabel(row.minVisibleLevel) }}</span>
+            </template>
+          </el-table-column>
+          <el-table-column prop="createdAt" label="创建时间" width="160">
+            <template slot-scope="{ row }">{{ formatTime(row.createdAt) }}</template>
+            <template slot="header">
+              <span
+                class="pm-sort-header"
+                title="单击切换排序,双击取消"
+                @click.stop="onSortClick('createdAt', $event)"
+                @dblclick.stop="onSortToggle('createdAt')"
+              >
+                创建时间<span v-if="sortIndex('createdAt') !== null" class="pm-sort-index">{{ sortIndex('createdAt') }}</span>{{ sortIcon('createdAt') }}
+              </span>
+            </template>
+          </el-table-column>
+          <el-table-column label="操作" width="176" fixed="right" align="left">
+            <template slot-scope="{ row }">
+              <el-button type="text" size="small" @click="$router.push('/product-edit?id=' + row.id)">编辑</el-button>
+              <el-button type="text" size="small" @click="showInventoryDialog(row)">库存</el-button>
+              <el-dropdown trigger="click" @command="(cmd) => handleActionCmd(row, cmd)">
+                <el-button type="text" size="small">
+                  更多<i class="el-icon-arrow-down el-icon--right"></i>
+                </el-button>
+                <el-dropdown-menu slot="dropdown">
+                  <el-dropdown-item command="copy" icon="el-icon-document-copy">复制</el-dropdown-item>
+                  <el-dropdown-item v-if="row.status === 'pending'" command="approve" icon="el-icon-circle-check">通过</el-dropdown-item>
+                  <el-dropdown-item v-if="row.status === 'pending'" command="reject" icon="el-icon-circle-close" divided>拒绝</el-dropdown-item>
+                  <el-dropdown-item v-if="row.status === 'on_shelf'" command="offShelf" icon="el-icon-download">下架</el-dropdown-item>
+                  <el-dropdown-item v-if="row.status === 'off_shelf'" command="onShelf" icon="el-icon-upload2">上架</el-dropdown-item>
+                </el-dropdown-menu>
+              </el-dropdown>
+            </template>
+          </el-table-column>
         </el-table>
-    </div>
-
-    <el-pagination
-      @size-change="handleSizeChange"
-      @current-change="handlePageChange"
-      :current-page="pagination.page"
-      :page-sizes="[10, 20, 50]"
-      :page-size="pagination.size"
-      :total="pagination.total"
-      layout="total, sizes, prev, pager, next, jumper"
-      class="pagination-wrap"
-    />
-
-    <el-dialog title="拒绝原因" :visible.sync="rejectDialogVisible" width="420px">
-      <el-input v-model="rejectReason" type="textarea" :rows="3" placeholder="请输入拒绝原因" />
+      </div>
+
+      <!-- 分页:右侧对齐,总数前置 -->
+      <div class="pm-pagination">
+        <el-pagination
+          @size-change="handleSizeChange"
+          @current-change="handlePageChange"
+          :current-page="pagination.page"
+          :page-sizes="[10, 20, 50]"
+          :page-size="pagination.size"
+          :total="pagination.total"
+          layout="total, sizes, prev, pager, next, jumper"
+          background
+        />
+      </div>
+    </el-card>
+
+    <!-- 拒绝原因弹窗 -->
+    <el-dialog title="拒绝原因" :visible.sync="rejectDialogVisible" width="460px" class="pm-dialog">
+      <el-input v-model="rejectReason" type="textarea" :rows="3" maxlength="200" show-word-limit placeholder="请输入拒绝原因(将同步给商品提交方)" />
       <span slot="footer">
-        <el-button @click="rejectDialogVisible = false">取消</el-button>
-        <el-button type="danger" @click="confirmReject">确认拒绝</el-button>
+        <el-button size="small" @click="rejectDialogVisible = false">取消</el-button>
+        <el-button size="small" type="danger" @click="confirmReject">确认拒绝</el-button>
       </span>
     </el-dialog>
 
-    <el-dialog title="商品详情" :visible.sync="detailDialogVisible" width="600px">
-      <el-descriptions :column="2" border v-if="detail">
+    <!-- 商品详情弹窗 -->
+    <el-dialog title="商品详情" :visible.sync="detailDialogVisible" width="720px" class="pm-dialog">
+      <el-descriptions :column="2" border size="small" v-if="detail">
         <el-descriptions-item label="商品名称">{{ detail.name }}</el-descriptions-item>
         <el-descriptions-item label="商品类型">{{ typeLabel(detail.productType) }}</el-descriptions-item>
         <el-descriptions-item label="售价">{{ formatPriceWithSymbol(detail.price || 0) }}</el-descriptions-item>
         <el-descriptions-item label="库存">{{ detail.stock }}</el-descriptions-item>
         <el-descriptions-item label="销量">{{ detail.salesCount || 0 }}</el-descriptions-item>
-        <el-descriptions-item label="供应商">{{ detail.vendorName || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="供应商">{{ detail.vendorName || '—' }}</el-descriptions-item>
         <el-descriptions-item label="状态">
-          <el-tag :type="statusType(detail.status)" size="mini">{{ statusLabel(detail.status) }}</el-tag>
-        </el-descriptions-item>
-        <el-descriptions-item label="最低可见级别">
-          {{ detail.minVisibleLevel ? levelLabel(detail.minVisibleLevel) : '全部' }}
+          <span class="pm-status" :class="'pm-status--' + detail.status">
+            <i class="pm-status__dot"></i>{{ statusLabel(detail.status) }}
+          </span>
         </el-descriptions-item>
-        <el-descriptions-item label="拒绝原因" v-if="detail.rejectReason">{{ detail.rejectReason }}</el-descriptions-item>
-        <el-descriptions-item label="商品描述" :span="2">{{ detail.description || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="最低可见级别">{{ detail.minVisibleLevel ? levelLabel(detail.minVisibleLevel) : '全部' }}</el-descriptions-item>
+        <el-descriptions-item label="拒绝原因" v-if="detail.rejectReason" :span="2">{{ detail.rejectReason }}</el-descriptions-item>
+        <el-descriptions-item label="商品描述" :span="2">{{ detail.description || '—' }}</el-descriptions-item>
         <el-descriptions-item label="创建时间" :span="2">{{ formatTime(detail.createdAt) }}</el-descriptions-item>
       </el-descriptions>
-      <div v-else style="text-align:center;color:#999;">加载中...</div>
+      <div v-else class="pm-empty">加载中…</div>
     </el-dialog>
-  <!-- 库存弹窗 -->
-    <el-dialog title="库存管理" :visible.sync="inventoryDialogVisible" width="800px">
+
+    <!-- 库存弹窗 -->
+    <el-dialog title="库存管理" :visible.sync="inventoryDialogVisible" width="860px" class="pm-dialog">
       <div v-loading="inventoryLoading">
-        <el-descriptions :column="2" border v-if="inventoryData">
+        <el-descriptions :column="3" border size="small" v-if="inventoryData">
           <el-descriptions-item label="商品名称">{{ inventoryData.productName }}</el-descriptions-item>
           <el-descriptions-item label="当前库存">{{ inventoryData.stock }}</el-descriptions-item>
           <el-descriptions-item label="预警下限">{{ inventoryData.minStockAlert != null ? inventoryData.minStockAlert : 10 }}</el-descriptions-item>
         </el-descriptions>
 
-        <h4 style="margin:16px 0 8px;">SKU 库存</h4>
-        <el-table :data="inventoryData.skus" border stripe v-if="inventoryData && inventoryData.skus && inventoryData.skus.length">
-          <el-table-column prop="id" label="SKU ID" width="70" />
-          <el-table-column prop="specs" label="规格" min-width="150" />
-          <el-table-column prop="stock" label="库存" width="70" />
-          <el-table-column prop="minStockAlert" label="预警下限" width="70" />
-        </el-table>
-        <div v-else-if="inventoryData" style="color:#999;padding:8px;">无SKU数据</div>
+        <div class="pm-section">
+          <div class="pm-section__title">SKU 库存</div>
+          <el-table :data="inventoryData && inventoryData.skus || []" border size="small" empty-text="暂无 SKU 数据">
+            <el-table-column prop="id" label="SKU ID" width="88" align="right" class-name="pm-num" />
+            <el-table-column prop="specs" label="规格" min-width="180" show-overflow-tooltip />
+            <el-table-column prop="stock" label="库存" width="88" align="right" class-name="pm-num" />
+            <el-table-column prop="minStockAlert" label="预警下限" width="96" align="right" class-name="pm-num" />
+          </el-table>
+        </div>
 
-        <h4 style="margin:16px 0 8px;">最近 10 条流水</h4>
-        <el-table :data="inventoryData.recentTransactions" border stripe v-if="inventoryData && inventoryData.recentTransactions && inventoryData.recentTransactions.length">
-          <el-table-column label="类型" width="80">
-            <template slot-scope="{ row }">
-              <el-tag :type="row.type === 'inbound' ? 'success' : row.type === 'outbound' ? 'danger' : 'warning'" size="mini">
-                {{ {'inbound':'入库','outbound':'出库','adjustment':'调账','counting':'盘点','refund':'退款入库'}[row.type] || row.type }}
-              </el-tag>
-            </template>
-          </el-table-column>
-          <el-table-column label="数量" width="70">
-            <template slot-scope="{ row }">
-              <span :style="{ color: row.direction === 1 ? '#67c23a' : '#f56c6c' }">{{ row.direction === 1 ? '+' : '-' }}{{ row.quantity }}</span>
-            </template>
-          </el-table-column>
-          <el-table-column prop="beforeStock" label="变动前" width="70" />
-          <el-table-column prop="afterStock" label="变动后" width="70" />
-          <el-table-column prop="remark" label="备注" min-width="150" />
-          <el-table-column prop="createdAt" label="时间" width="160" />
-        </el-table>
-        <div v-else style="color:#999;padding:8px;">暂无流水记录</div>
+        <div class="pm-section">
+          <div class="pm-section__title">最近 10 条流水</div>
+          <el-table :data="inventoryData && inventoryData.recentTransactions || []" border size="small" empty-text="暂无流水记录">
+            <el-table-column label="类型" width="96">
+              <template slot-scope="{ row }">
+                <span class="pm-status" :class="'pm-status--' + (row.type === 'inbound' || row.type === 'refund' ? 'on_shelf' : row.type === 'outbound' ? 'rejected' : 'pending')">
+                  <i class="pm-status__dot"></i>{{ txTypeLabel(row.type) }}
+                </span>
+              </template>
+            </el-table-column>
+            <el-table-column label="数量" width="96" align="right" class-name="pm-num">
+              <template slot-scope="{ row }">
+                <span :class="row.direction === 1 ? 'pm-qty-in' : 'pm-qty-out'">
+                  {{ row.direction === 1 ? '+' : '−' }}{{ row.quantity }}
+                </span>
+              </template>
+            </el-table-column>
+            <el-table-column prop="beforeStock" label="变动前" width="88" align="right" class-name="pm-num" />
+            <el-table-column prop="afterStock" label="变动后" width="88" align="right" class-name="pm-num" />
+            <el-table-column prop="remark" label="备注" min-width="160" show-overflow-tooltip />
+            <el-table-column label="时间" width="160">
+              <template slot-scope="{ row }">{{ formatTime(row.createdAt) }}</template>
+            </el-table-column>
+          </el-table>
+        </div>
       </div>
       <span slot="footer">
-        <el-button @click="inventoryDialogVisible = false">关闭</el-button>
-        <el-button type="primary" @click="$router.push('/inventory')">进入库存管理</el-button>
+        <el-button size="small" @click="inventoryDialogVisible = false">关闭</el-button>
+        <el-button size="small" type="primary" @click="$router.push('/inventory')">进入库存管理</el-button>
       </span>
     </el-dialog>
-  </el-card>
   </div>
 </template>
 
@@ -240,15 +318,11 @@ import { getProductList, reviewProduct, shelveProduct, batchShelveProduct, copyP
 export default {
   mixins: [SortMixin],
   name: 'ProductManage',
-  computed: {
-    tableHeight() {
-      return window.innerHeight - 300
-    }
-  },
   data() {
     return {
       list: [],
       loading: false,
+      tableHeight: 480,
       filters: {
         keyword: '',
         status: '',
@@ -269,13 +343,23 @@ export default {
       inventoryDialogVisible: false,
       inventoryLoading: false,
       inventoryData: null,
-      selectedIds: []  // 批量操作选中的商品ID集合
+      selectedIds: [] // 批量操作选中的商品ID集合
     }
   },
   mounted() {
+    this.calcTableHeight()
+    window.addEventListener('resize', this.calcTableHeight)
     this.loadList()
   },
+  beforeDestroy() {
+    window.removeEventListener('resize', this.calcTableHeight)
+  },
   methods: {
+    calcTableHeight() {
+      // 页面留白 24*2 + 卡片内边距 24*2 + 头部 32 + 筛选区 56 + 分页 56 ≈ 基准 300
+      const offset = this.selectedIds.length ? 336 : 300
+      this.tableHeight = Math.max(320, window.innerHeight - offset)
+    },
     async loadList() {
       this.loading = true
       try {
@@ -306,16 +390,6 @@ export default {
       this.pagination.page = page
       this.loadList()
     },
-    statusType(status) {
-      const map = {
-        pending: 'warning',
-        approved: 'success',
-        on_shelf: 'success',
-        off_shelf: 'info',
-        rejected: 'danger'
-      }
-      return map[status] || 'info'
-    },
     statusLabel(status) {
       const map = {
         pending: '待审核',
@@ -326,6 +400,10 @@ export default {
       }
       return map[status] || status
     },
+    txTypeLabel(type) {
+      const map = { inbound: '入库', outbound: '出库', adjustment: '调账', counting: '盘点', refund: '退款入库' }
+      return map[type] || type
+    },
     typeLabel(type) {
       const map = { physical: '实物商品', virtual: '虚拟商品', coupon: '优惠券', assessment: '测评商品' }
       return map[type] || type
@@ -336,12 +414,13 @@ export default {
       return map[code] || code
     },
     formatTime(t) {
-      if (!t) return '-'
+      if (!t) return '—'
       return t.replace ? t.replace('T', ' ').substring(0, 19) : t
     },
     formatPriceWithSymbol(price) {
-      if (price === 0) return '免费'
-      return '¥' + (price / 100).toFixed(2)
+      if (!price) return '免费'
+      const v = (price / 100).toFixed(2)
+      return '¥' + v.replace(/\B(?=(\d{3})+(?!\d))/g, ',')
     },
     async handleReview(row, action) {
       try {
@@ -435,7 +514,7 @@ export default {
         await this.$confirm('确认' + (shelve ? '批量上架' : '批量下架') + '选中的 ' + this.selectedIds.length + ' 个商品?', '提示')
         await batchShelveProduct(this.selectedIds, shelve)
         this.$message.success(shelve ? '批量上架成功' : '批量下架成功')
-        this.selectedIds = []
+        this.clearSelection()
         this.loadList()
       } catch (e) {
         if (e !== 'cancel') this.$message.error('操作失败')
@@ -443,42 +522,290 @@ export default {
     },
     handleSelectionChange(rows) {
       this.selectedIds = rows.map(r => r.id)
+      this.calcTableHeight()
+    },
+    clearSelection() {
+      this.selectedIds = []
+      if (this.$refs.table) this.$refs.table.clearSelection()
+      this.calcTableHeight()
     }
   }
 }
 </script>
 
 <style scoped>
+/* ==========================================================
+   商品管理页 · 企业级视觉规范
+   间距 8 基准 / 主色仅 1 处主操作 / 状态色仅承载语义
+   ========================================================== */
 .product-manage {
-  padding: 20px;
+  padding: 24px;
+}
+
+.pm-card {
+  border: 1px solid #f0f0f0;
+  border-radius: 8px;
+  box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.03);
+}
+
+.pm-card /deep/ .el-card__body {
+  padding: 24px;
+}
+
+/* ---------- 页面标题区 ---------- */
+.pm-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  margin-bottom: 24px;
+}
+
+.pm-header__left {
+  display: flex;
+  align-items: baseline;
+  gap: 12px;
+}
+
+.pm-title {
+  margin: 0;
+  font-size: 20px;
+  line-height: 1.4;
+  font-weight: 600;
+  color: rgba(0, 0, 0, 0.88);
+}
+
+.pm-subtitle {
+  font-size: 12px;
+  color: rgba(0, 0, 0, 0.45);
+}
+
+.pm-header__right {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+/* ---------- 筛选区:白底 + 下分割线,不用阴影 ---------- */
+.pm-filter {
+  display: flex;
+  align-items: flex-end;
+  justify-content: space-between;
+  flex-wrap: wrap;
+  gap: 16px;
+  padding-bottom: 16px;
+  margin-bottom: 16px;
+  border-bottom: 1px solid #f0f0f0;
+}
+
+.pm-filter__fields {
+  display: flex;
+  align-items: center;
+  flex-wrap: wrap;
+  gap: 16px;
+}
+
+.pm-field {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.pm-field__label {
+  font-size: 14px;
+  color: rgba(0, 0, 0, 0.65);
+  white-space: nowrap;
+}
+
+.pm-field__control {
+  width: 150px;
+}
+
+.pm-field__control--search {
+  width: 220px;
+}
+
+.pm-filter__actions {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+/* ---------- 批量选择提示条 ---------- */
+.pm-batch-bar {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  height: 40px;
+  padding: 0 16px;
+  margin-bottom: 12px;
+  background: #e6f4ff;
+  border: 1px solid #91caff;
+  border-radius: 6px;
+}
+
+.pm-batch-bar__text {
+  font-size: 14px;
+  color: rgba(0, 0, 0, 0.65);
+}
+
+.pm-batch-bar__text b {
+  color: #0958d9;
+  font-weight: 600;
+}
+
+.pm-batch-bar__actions {
+  display: flex;
+  align-items: center;
+}
+
+/* ---------- 表格 ---------- */
+.pm-table-wrap /deep/ .el-table {
+  font-size: 14px;
+  border-radius: 6px;
+}
+
+.pm-table-wrap /deep/ .el-table th {
+  height: 46px;
+  padding: 0;
+  background: #fafafa;
+  color: rgba(0, 0, 0, 0.88);
+  font-weight: 500;
+}
+
+.pm-table-wrap /deep/ .el-table td {
+  padding: 12px 0;
+}
+
+/* 数值列:右对齐 + 等宽数字 */
+.pm-table-wrap /deep/ .pm-num {
+  font-variant-numeric: tabular-nums;
+  font-feature-settings: 'tnum';
 }
-.product-thumb {
-  width: 50px;
-  height: 50px;
+
+.pm-thumb {
+  width: 40px;
+  height: 40px;
   object-fit: cover;
   border-radius: 4px;
+  border: 1px solid #f0f0f0;
   cursor: pointer;
+  display: block;
 }
-.stock-warning {
-  color: #f56c6c;
-  font-weight: bold;
+
+.pm-placeholder {
+  color: rgba(0, 0, 0, 0.25);
 }
-.header-search {
-  width: 180px;
+
+.pm-stock-warning {
+  color: #cf1322;
+  font-weight: 600;
 }
 
-.sort-header {
+/* 排序表头 */
+.pm-sort-header {
   cursor: pointer;
   user-select: none;
+  font-size: 14px;
   font-weight: 500;
-  font-size: 12px;
 }
-.sort-header:hover {
-  color: #409eff;
+
+.pm-sort-header:hover {
+  color: #1677ff;
 }
-.sort-tag {
-  margin-left: 8px;
-  cursor: pointer;
+
+.pm-sort-index {
+  display: inline-block;
+  min-width: 14px;
+  height: 14px;
+  line-height: 14px;
+  margin-left: 4px;
+  padding: 0 3px;
+  font-size: 11px;
+  font-weight: 400;
+  text-align: center;
+  color: #1677ff;
+  background: #e6f4ff;
+  border-radius: 2px;
+}
+
+/* ---------- 标准化状态标签(浅底 + 深字 + 同色点) ---------- */
+.pm-status {
+  display: inline-flex;
+  align-items: center;
+  gap: 6px;
+  padding: 2px 8px;
+  font-size: 12px;
+  line-height: 20px;
+  border-radius: 2px;
   white-space: nowrap;
 }
+
+.pm-status__dot {
+  width: 6px;
+  height: 6px;
+  border-radius: 50%;
+  background: currentColor;
+}
+
+.pm-status--pending { background: #fffbe6; color: #d48806; }
+.pm-status--approved { background: #e6f4ff; color: #0958d9; }
+.pm-status--on_shelf { background: #f6ffed; color: #389e0d; }
+.pm-status--off_shelf { background: #f5f5f5; color: rgba(0, 0, 0, 0.65); }
+.pm-status--rejected { background: #fff1f0; color: #cf1322; }
+
+/* ---------- 分页 ---------- */
+.pm-pagination {
+  display: flex;
+  justify-content: flex-end;
+  padding-top: 16px;
+}
+
+/* ---------- 弹窗内区块 ---------- */
+.pm-dialog /deep/ .el-dialog__header {
+  padding: 16px 24px;
+  border-bottom: 1px solid #f0f0f0;
+}
+
+.pm-dialog /deep/ .el-dialog__title {
+  font-size: 16px;
+  font-weight: 500;
+  color: rgba(0, 0, 0, 0.88);
+}
+
+.pm-dialog /deep/ .el-dialog__body {
+  padding: 24px;
+}
+
+.pm-dialog /deep/ .el-dialog__footer {
+  padding: 12px 24px;
+  border-top: 1px solid #f0f0f0;
+}
+
+.pm-section {
+  margin-top: 24px;
+}
+
+.pm-section__title {
+  font-size: 14px;
+  font-weight: 500;
+  color: rgba(0, 0, 0, 0.88);
+  padding-left: 8px;
+  border-left: 3px solid #1677ff;
+  margin-bottom: 12px;
+  line-height: 1.2;
+}
+
+.pm-qty-in { color: #389e0d; font-variant-numeric: tabular-nums; }
+.pm-qty-out { color: #cf1322; font-variant-numeric: tabular-nums; }
+
+.pm-empty {
+  padding: 32px 0;
+  text-align: center;
+  color: rgba(0, 0, 0, 0.45);
+  font-size: 14px;
+}
+
+.pm-text-muted {
+  color: rgba(0, 0, 0, 0.45);
+}
 </style>