Просмотр исходного кода

feat(web): 限时特价管理端页面 + 路由 + API 封装

Sisyphus Agent 4 дней назад
Родитель
Сommit
ed52d2224f
3 измененных файлов с 778 добавлено и 0 удалено
  1. 20 0
      cfc-web/src/api/admin.js
  2. 6 0
      cfc-web/src/router/index.js
  3. 752 0
      cfc-web/src/views/admin/SpecialPriceManage.vue

+ 20 - 0
cfc-web/src/api/admin.js

@@ -1998,6 +1998,26 @@ export function getProductStock(productId) {
   })
 }
 
+// ========== 限时特价(扫码闪购)API ==========
+export function getSpecialPriceList(params) {
+  return request({ url: '/api/admin/special-price/list', method: 'post', data: params || {} })
+}
+export function createSpecialPrice(data) {
+  return request({ url: '/api/admin/special-price/create', method: 'post', data })
+}
+export function updateSpecialPrice(data) {
+  return request({ url: '/api/admin/special-price/update', method: 'post', data })
+}
+export function setSpecialPriceStatus(data) {
+  return request({ url: '/api/admin/special-price/status', method: 'post', data })
+}
+export function deleteSpecialPrice(id) {
+  return request({ url: '/api/admin/special-price/delete', method: 'post', data: { id } })
+}
+export function getSpecialPriceQrcode(id) {
+  return request({ url: '/api/admin/special-price/qrcode', method: 'post', data: { id } })
+}
+
 export function saveBundleItems(bundleProductId, items) {
   return request({
     url: '/api/admin/inventory/bundle/items',

+ 6 - 0
cfc-web/src/router/index.js

@@ -276,6 +276,12 @@ const routes = [
         component: () => import('@/views/admin/ProductManage.vue'),
         meta: { title: '商品管理', perm: 'commerce:products,commerce:self' }
       },
+      {
+        path: 'special-price',
+        name: 'SpecialPriceManage',
+        component: () => import('@/views/admin/SpecialPriceManage.vue'),
+        meta: { title: '限时特价', perm: 'commerce:products' }
+      },
       {
         path: 'inventory',
         name: 'InventoryManage',

+ 752 - 0
cfc-web/src/views/admin/SpecialPriceManage.vue

@@ -0,0 +1,752 @@
+<template>
+  <div class="special-price-manage admin-page">
+    <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-button size="small" type="primary" icon="el-icon-plus" @click="openCreate">新建活动</el-button>
+        </div>
+      </div>
+
+      <!-- 筛选区 -->
+      <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.runStatus" placeholder="全部状态" size="small" clearable class="pm-field__control" @change="loadList">
+              <el-option label="全部状态" value="" />
+              <el-option label="进行中" value="ACTIVE" />
+              <el-option label="未开始" value="NOT_STARTED" />
+              <el-option label="已售罄" value="SOLD_OUT" />
+              <el-option label="已结束" value="ENDED" />
+              <el-option label="草稿" value="draft" />
+              <el-option label="已停用" value="disabled" />
+            </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="pm-table-wrap">
+        <div class="adm-table-wrap">
+          <el-table
+            :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 prop="name" label="活动名" min-width="140" show-overflow-tooltip />
+            <el-table-column label="商品" min-width="180" show-overflow-tooltip>
+              <template slot-scope="{ row }">
+                <span class="pm-product-link" @click="goProduct(row)">{{ row.productName || '—' }}</span>
+                <span class="pm-product-id">#{{ row.productId }}</span>
+              </template>
+            </el-table-column>
+            <el-table-column label="原价 / 特价" width="150">
+              <template slot-scope="{ row }">
+                <div class="pm-price">
+                  <span class="pm-price__original">{{ formatPriceWithSymbol(row.productPrice) }}</span>
+                  <span
+                    class="pm-price__special"
+                    :class="{ 'pm-price__special--discount': row.productPrice && row.specialPrice && row.specialPrice < row.productPrice }"
+                  >{{ formatPriceWithSymbol(row.specialPrice) }}</span>
+                </div>
+              </template>
+            </el-table-column>
+            <el-table-column label="时间窗口" width="230">
+              <template slot-scope="{ row }">
+                <div class="pm-time-window">
+                  <el-tag size="mini" type="info">{{ formatTime(row.startTime) }}</el-tag>
+                  <span class="pm-time-window__sep">至</span>
+                  <el-tag size="mini" type="info">{{ formatTime(row.endTime) }}</el-tag>
+                </div>
+              </template>
+            </el-table-column>
+            <el-table-column label="已售 / 剩余" width="140">
+              <template slot-scope="{ row }">
+                <div class="pm-quota">
+                  <div class="pm-quota__bar">
+                    <div class="pm-quota__fill" :style="{ width: quotaPercent(row) + '%' }"></div>
+                  </div>
+                  <span class="pm-quota__text">{{ row.soldQuantity || 0 }} / {{ Math.max((row.totalQuantity || 0) - (row.soldQuantity || 0), 0) }}</span>
+                </div>
+              </template>
+            </el-table-column>
+            <el-table-column prop="perUserLimit" label="每人限购" width="90" align="right" class-name="pm-num">
+              <template slot-scope="{ row }">{{ row.perUserLimit || '—' }}</template>
+            </el-table-column>
+            <el-table-column label="券开关" width="88">
+              <template slot-scope="{ row }">
+                <el-tag :type="row.allowCoupon === 1 ? 'success' : 'danger'" size="mini">{{ row.allowCoupon === 1 ? '可用券' : '禁券' }}</el-tag>
+              </template>
+            </el-table-column>
+            <el-table-column label="运行状态" width="96">
+              <template slot-scope="{ row }">
+                <span class="pm-status" :class="'pm-status--' + row._runStatus">
+                  <i class="pm-status__dot"></i>{{ runStatusLabel(row._runStatus) }}
+                </span>
+              </template>
+            </el-table-column>
+            <el-table-column label="操作" width="232" fixed="right" align="left">
+              <template slot-scope="{ row }">
+                <el-button v-if="row._runStatus === 'draft' || row._runStatus === 'disabled'" type="text" size="small" @click="openEdit(row)">编辑</el-button>
+                <el-button v-if="row._runStatus === 'draft' || row._runStatus === 'disabled'" type="text" size="small" class="pm-text-success" @click="toggleStatus(row, 'enable')">启用</el-button>
+                <el-button v-if="row._runStatus === 'active' || row._runStatus === 'not_started' || row._runStatus === 'sold_out'" type="text" size="small" class="pm-text-warning" @click="toggleStatus(row, 'disable')">停用</el-button>
+                <el-button type="text" size="small" @click="showQrcode(row)">二维码</el-button>
+                <el-button type="text" size="small" class="pm-text-danger" @click="handleDelete(row)">删除</el-button>
+              </template>
+            </el-table-column>
+          </el-table>
+        </div>
+      </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"
+          layout="total, sizes, prev, pager, next, jumper"
+          :total="pagination.total"
+        />
+      </div>
+    </el-card>
+
+    <!-- 新建 / 编辑弹窗 -->
+    <el-dialog :title="form.id ? '编辑特价活动' : '新建特价活动'" :visible.sync="dialogVisible" width="560px" @closed="resetForm">
+      <el-form label-width="96px">
+        <el-form-item label="活动名称" required>
+          <el-input v-model="form.name" placeholder="如:开学季限时特价" maxlength="60" />
+        </el-form-item>
+        <el-form-item label="商品" required>
+          <el-select
+            v-model="form.productId"
+            filterable
+            remote
+            reserve-keyword
+            :remote-method="searchProducts"
+            :loading="productSearchLoading"
+            placeholder="输入关键词搜索商品"
+            style="width: 100%;"
+            @change="onProductSelected"
+          >
+            <el-option v-for="p in productOptions" :key="p.id" :label="(p.name || '') + ' #' + p.id" :value="p.id" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="特价(元)" required>
+          <el-input-number v-model="form.specialPriceYuan" :min="0.01" :precision="2" :step="1" controls-position="right" style="width: 200px;" @change="onSpecialPriceYuanChange" />
+          <span class="pm-form-hint">商品原价:{{ formatPriceWithSymbol(form.productPrice) }},特价须低于原价</span>
+        </el-form-item>
+        <el-form-item label="起止时间" required>
+          <el-date-picker
+            v-model="form.timeRange"
+            type="datetimerange"
+            range-separator="至"
+            start-placeholder="开始时间"
+            end-placeholder="结束时间"
+            value-format="yyyy-MM-dd HH:mm:ss"
+            style="width: 100%;"
+          />
+        </el-form-item>
+        <el-form-item label="总供应量" required>
+          <el-input-number v-model="form.totalQuantity" :min="1" :step="1" controls-position="right" style="width: 200px;" />
+        </el-form-item>
+        <el-form-item label="每人限购" required>
+          <el-input-number v-model="form.perUserLimit" :min="1" :step="1" controls-position="right" style="width: 200px;" />
+        </el-form-item>
+        <el-form-item label="允许优惠券">
+          <el-switch v-model="form.allowCoupon" :active-value="1" :inactive-value="0" />
+        </el-form-item>
+      </el-form>
+      <div slot="footer">
+        <el-button @click="dialogVisible = false">取消</el-button>
+        <el-button @click="handleSave">保存</el-button>
+        <el-button v-if="!form.id" type="primary" @click="handleSaveAndEnable">保存并启用</el-button>
+      </div>
+      <div v-if="form.id && (form._runStatus === 'draft' || form._runStatus === 'disabled')" class="pm-dialog-tip">
+        保存后可在列表中对活动执行「启用」
+      </div>
+    </el-dialog>
+
+    <!-- 二维码弹窗 -->
+    <el-dialog title="特价活动小程序码" :visible.sync="qrDialogVisible" width="420px">
+      <div v-loading="qrLoading" class="pm-qr-wrap">
+        <img v-if="qrData && qrData.qrCodeBase64" :src="qrData.qrCodeBase64" class="pm-qr-img" alt="特价小程序码" />
+        <div v-else-if="!qrLoading" class="pm-qr-empty">{{ qrData && qrData.note ? qrData.note : '二维码未生成' }}</div>
+        <div v-if="qrData && qrData.scene" class="pm-qr-scene">scene:{{ qrData.scene }}</div>
+      </div>
+      <div slot="footer">
+        <a v-if="qrData && qrData.qrCodeBase64" :href="qrData.qrCodeBase64" download="special-price-qrcode.png" class="pm-qr-download">下载二维码</a>
+        <el-button @click="qrDialogVisible = false">关闭</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import {
+  getSpecialPriceList,
+  createSpecialPrice,
+  updateSpecialPrice,
+  setSpecialPriceStatus,
+  deleteSpecialPrice,
+  getSpecialPriceQrcode,
+  getProductList
+} from '@/api/admin'
+
+export default {
+  name: 'SpecialPriceManage',
+  data() {
+    return {
+      list: [],
+      loading: false,
+      filters: {
+        keyword: '',
+        runStatus: ''
+      },
+      pagination: {
+        page: 1,
+        size: 20,
+        total: 0
+      },
+      selectedIds: [],
+      dialogVisible: false,
+      form: this.emptyForm(),
+      productOptions: [],
+      productSearchLoading: false,
+      qrDialogVisible: false,
+      qrLoading: false,
+      qrData: null
+    }
+  },
+  mounted() {
+    this.loadList()
+  },
+  methods: {
+    emptyForm() {
+      return {
+        id: null,
+        name: '',
+        productId: null,
+        productPrice: 0,
+        specialPriceYuan: 0,
+        specialPrice: 0,
+        timeRange: [],
+        totalQuantity: 1,
+        perUserLimit: 1,
+        allowCoupon: 0,
+        _runStatus: null
+      }
+    },
+    async loadList() {
+      this.loading = true
+      try {
+        const params = {
+          page: this.pagination.page,
+          size: this.pagination.size,
+          keyword: this.filters.keyword || null,
+          runStatus: this.filters.runStatus || null
+        }
+        const res = await getSpecialPriceList(params)
+        const records = (res.data && res.data.records) || []
+        this.list = records.map(r => Object.assign({}, r, { _runStatus: this.runStatusOf(r) }))
+        this.pagination.total = (res.data && res.data.total) || 0
+      } catch (e) {
+        this.$message.error(e.message || '加载失败')
+      } finally {
+        this.loading = false
+      }
+    },
+    resetFilters() {
+      this.filters.keyword = ''
+      this.filters.runStatus = ''
+      this.pagination.page = 1
+      this.loadList()
+    },
+    handleSizeChange(size) {
+      this.pagination.size = size
+      this.pagination.page = 1
+      this.loadList()
+    },
+    handlePageChange(page) {
+      this.pagination.page = page
+      this.loadList()
+    },
+    handleSelectionChange(selection) {
+      this.selectedIds = selection.map(s => s.id)
+    },
+    // 运行状态推导:镜像后端 SQL(listWithProductName 的 runStatusCondition)
+    runStatusOf(row) {
+      if (row.status === 'draft') return 'draft'
+      if (row.status === 'disabled') return 'disabled'
+      const now = Date.now()
+      const start = this.parseTs(row.startTime)
+      const end = this.parseTs(row.endTime)
+      if (start && now < start) return 'not_started'
+      if (end && now > end) return 'ended'
+      if ((row.soldQuantity || 0) >= (row.totalQuantity || 0)) return 'sold_out'
+      return 'active'
+    },
+    parseTs(v) {
+      if (!v) return 0
+      if (typeof v === 'number') return v
+      const t = Date.parse(String(v).replace(' ', 'T'))
+      return isNaN(t) ? 0 : t
+    },
+    runStatusLabel(status) {
+      const map = {
+        active: '进行中',
+        not_started: '未开始',
+        sold_out: '已售罄',
+        ended: '已结束',
+        draft: '草稿',
+        disabled: '已停用'
+      }
+      return map[status] || status
+    },
+    quotaPercent(row) {
+      if (!row.totalQuantity) return 0
+      return Math.min(100, Math.round((row.soldQuantity || 0) * 100 / row.totalQuantity))
+    },
+    formatPriceWithSymbol(price) {
+      if (!price) return '免费'
+      return '¥' + (price / 100).toFixed(2)
+    },
+    goProduct(row) {
+      this.$router.push('/product-edit?id=' + row.productId)
+    },
+    // ---------------- 新建/编辑 ----------------
+    openCreate() {
+      this.form = this.emptyForm()
+      this.productOptions = []
+      this.dialogVisible = true
+    },
+    openEdit(row) {
+      this.form = {
+        id: row.id,
+        name: row.name || '',
+        productId: row.productId || null,
+        productPrice: row.productPrice || 0,
+        specialPriceYuan: row.specialPrice ? row.specialPrice / 100 : 0,
+        specialPrice: row.specialPrice || 0,
+        timeRange: (row.startTime && row.endTime) ? [this.formatTime(row.startTime), this.formatTime(row.endTime)] : [],
+        totalQuantity: row.totalQuantity || 1,
+        perUserLimit: row.perUserLimit || 1,
+        allowCoupon: row.allowCoupon === 1 ? 1 : 0,
+        _runStatus: row._runStatus
+      }
+      // 预置当前商品为唯一选项,避免编辑态回显空
+      this.productOptions = [{ id: row.productId, name: row.productName, price: row.productPrice }]
+      this.dialogVisible = true
+    },
+    resetForm() {
+      this.form = this.emptyForm()
+    },
+    formatTime(t) {
+      return t ? t.replace('T', ' ').substring(0, 19) : '-'
+    },
+    async searchProducts(keyword) {
+      if (!keyword) return
+      this.productSearchLoading = true
+      try {
+        const res = await getProductList({ keyword: keyword.trim(), page: 1, size: 20 })
+        this.productOptions = (res.data && res.data.records) || []
+      } catch (e) {
+        this.$message.error('商品搜索失败')
+      } finally {
+        this.productSearchLoading = false
+      }
+    },
+    onProductSelected(id) {
+      const p = this.productOptions.find(x => x.id === id)
+      if (p) this.form.productPrice = p.price || 0
+    },
+    onSpecialPriceYuanChange(val) {
+      this.form.specialPrice = Math.round((val || 0) * 100)
+    },
+    validateForm() {
+      const f = this.form
+      if (!f.name || !f.name.trim()) return '请填写活动名称'
+      if (!f.productId) return '请选择商品'
+      if (!f.specialPrice || f.specialPrice <= 0) return '请填写特价金额'
+      if (f.productPrice && f.specialPrice >= f.productPrice) return '特价必须低于商品原价'
+      if (!f.timeRange || f.timeRange.length !== 2) return '请选择起止时间'
+      if (f.timeRange[0] >= f.timeRange[1]) return '开始时间必须早于结束时间'
+      if (!f.totalQuantity || f.totalQuantity < 1) return '总供应量必须大于等于1'
+      if (!f.perUserLimit || f.perUserLimit < 1) return '每人限购必须大于等于1'
+      return null
+    },
+    buildPayload() {
+      const f = this.form
+      return {
+        name: f.name.trim(),
+        productId: f.productId,
+        specialPrice: f.specialPrice,
+        startTime: f.timeRange[0],
+        endTime: f.timeRange[1],
+        totalQuantity: f.totalQuantity,
+        perUserLimit: f.perUserLimit,
+        allowCoupon: f.allowCoupon === 1 ? 1 : 0
+      }
+    },
+    async handleSave() {
+      const err = this.validateForm()
+      if (err) return this.$message.warning(err)
+      const payload = this.buildPayload()
+      try {
+        if (this.form.id) {
+          await updateSpecialPrice(Object.assign({ id: this.form.id }, payload))
+          this.$message.success('修改成功')
+        } else {
+          await createSpecialPrice(payload)
+          this.$message.success('创建成功')
+        }
+        this.dialogVisible = false
+        this.loadList()
+      } catch (e) {
+        this.$message.error(e.message || '保存失败')
+      }
+    },
+    async handleSaveAndEnable() {
+      const err = this.validateForm()
+      if (err) return this.$message.warning(err)
+      const payload = this.buildPayload()
+      try {
+        let id = this.form.id
+        if (id) {
+          await updateSpecialPrice(Object.assign({ id }, payload))
+        } else {
+          const res = await createSpecialPrice(payload)
+          id = (res.data && res.data.id) || id
+        }
+        if (!id) throw new Error('活动创建失败')
+        await setSpecialPriceStatus({ id, action: 'enable' })
+        this.$message.success('已创建并启用')
+        this.dialogVisible = false
+        this.loadList()
+      } catch (e) {
+        this.$message.error(e.message || '操作失败')
+      }
+    },
+    // ---------------- 状态 / 删除 ----------------
+    async toggleStatus(row, action) {
+      try {
+        await this.$confirm(action === 'enable' ? '确认启用该特价活动?启用后按时间窗生效(同商品进行中活动互斥)。' : '确认停用该特价活动?停用后扫码进入将回落原价。', '提示')
+        await setSpecialPriceStatus({ id: row.id, action })
+        this.$message.success(action === 'enable' ? '已启用' : '已停用')
+        this.loadList()
+      } catch (e) {
+        if (e !== 'cancel') this.$message.error(e.message || '操作失败')
+      }
+    },
+    async handleDelete(row) {
+      try {
+        await this.$confirm('确认删除该特价活动?若已有特价订单则无法删除。', '提示', { type: 'warning' })
+        await deleteSpecialPrice(row.id)
+        this.$message.success('删除成功')
+        this.loadList()
+      } catch (e) {
+        if (e !== 'cancel') this.$message.error(e.message || '删除失败')
+      }
+    },
+    // ---------------- 二维码 ----------------
+    async showQrcode(row) {
+      this.qrDialogVisible = true
+      this.qrLoading = true
+      this.qrData = null
+      try {
+        const res = await getSpecialPriceQrcode(row.id)
+        this.qrData = res.data || null
+      } catch (e) {
+        this.$message.error(e.message || '二维码生成失败')
+      } finally {
+        this.qrLoading = false
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.pm-card /deep/ .el-card__body {
+  padding: 24px;
+  display: flex;
+  flex-direction: column;
+  min-height: calc(100vh - 60px - 48px);
+}
+
+/* ---------- 页面标题区 ---------- */
+.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-table-wrap {
+  flex: 1;
+  min-height: 0;
+  display: flex;
+  flex-direction: column;
+}
+.pm-table-wrap /deep/ .el-table {
+  flex: 1;
+  min-height: 0;
+  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';
+}
+
+/* ---------- 价格列 ---------- */
+.pm-price {
+  display: flex;
+  flex-direction: column;
+  gap: 2px;
+}
+.pm-price__original {
+  font-size: 12px;
+  color: rgba(0, 0, 0, 0.45);
+  text-decoration: line-through;
+}
+.pm-price__special {
+  font-size: 14px;
+  font-weight: 600;
+  color: rgba(0, 0, 0, 0.88);
+}
+.pm-price__special--discount {
+  color: #cf1322;
+}
+
+/* ---------- 时间窗口 ---------- */
+.pm-time-window {
+  display: inline-flex;
+  align-items: center;
+  gap: 4px;
+  white-space: nowrap;
+}
+.pm-time-window__sep {
+  font-size: 12px;
+  color: rgba(0, 0, 0, 0.45);
+}
+
+/* ---------- 库存进度条 ---------- */
+.pm-quota {
+  display: flex;
+  flex-direction: column;
+  gap: 4px;
+}
+.pm-quota__bar {
+  height: 6px;
+  width: 100%;
+  min-width: 60px;
+  background: #f0f0f0;
+  border-radius: 3px;
+  overflow: hidden;
+}
+.pm-quota__fill {
+  height: 100%;
+  background: linear-gradient(90deg, #ff7a45, #ff4d4f);
+  border-radius: 3px;
+}
+.pm-quota__text {
+  font-size: 12px;
+  color: rgba(0, 0, 0, 0.65);
+  font-variant-numeric: tabular-nums;
+}
+
+/* ---------- 商品链接 ---------- */
+.pm-product-link {
+  color: #1677ff;
+  cursor: pointer;
+}
+.pm-product-link:hover {
+  text-decoration: underline;
+}
+.pm-product-id {
+  margin-left: 4px;
+  font-size: 12px;
+  color: rgba(0, 0, 0, 0.45);
+}
+
+/* ---------- 状态标签 ---------- */
+.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--active { background: #f6ffed; color: #389e0d; }
+.pm-status--not_started { background: #e6f4ff; color: #0958d9; }
+.pm-status--sold_out { background: #fff1f0; color: #cf1322; }
+.pm-status--ended { background: #f5f5f5; color: rgba(0, 0, 0, 0.65); }
+.pm-status--draft { background: #fffbe6; color: #d48806; }
+.pm-status--disabled { background: #f5f5f5; color: rgba(0, 0, 0, 0.65); }
+
+/* ---------- 操作按钮颜色 ---------- */
+.pm-text-success { color: #389e0d; }
+.pm-text-warning { color: #d48806; }
+.pm-text-danger { color: #cf1322; }
+
+/* ---------- 分页 ---------- */
+.pm-pagination {
+  display: flex;
+  justify-content: flex-end;
+  margin-top: 16px;
+}
+
+/* ---------- 弹窗 ---------- */
+.pm-form-hint {
+  margin-left: 12px;
+  font-size: 12px;
+  color: rgba(0, 0, 0, 0.45);
+}
+.pm-dialog-tip {
+  margin-top: -12px;
+  padding: 8px 12px;
+  font-size: 12px;
+  color: #d48806;
+  background: #fffbe6;
+  border: 1px solid #ffe58f;
+  border-radius: 4px;
+}
+
+/* ---------- 二维码 ---------- */
+.pm-qr-wrap {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  gap: 12px;
+  min-height: 200px;
+  justify-content: center;
+}
+.pm-qr-img {
+  width: 240px;
+  height: 240px;
+  border: 1px solid #f0f0f0;
+  border-radius: 6px;
+}
+.pm-qr-empty {
+  color: rgba(0, 0, 0, 0.45);
+  font-size: 14px;
+}
+.pm-qr-scene {
+  font-size: 12px;
+  color: rgba(0, 0, 0, 0.65);
+  word-break: break-all;
+}
+.pm-qr-download {
+  float: left;
+  line-height: 32px;
+  color: #1677ff;
+}
+</style>