741 lines
40 KiB
Vue
741 lines
40 KiB
Vue
<script setup lang="ts">
|
||
/**
|
||
* F006 实物商品管理 (W5.x.x+5 整改) - 完整版
|
||
*
|
||
* 沿用老 vue supplier/good.vue 1628 行完整功能:
|
||
* - 搜索: 商品名/供应商/分类/品牌
|
||
* - 状态 tab: 全部/待审核/已通过/已驳回/已下架
|
||
* - 表格列: 封面/商品/供应商/3级分类/品牌/单位/申请时间/铺货/审核意见/状态
|
||
* - 操作: 详情(常驻) + 审核/上下架/改分类/删除(收纳⋯) [W17.6 主次分离]
|
||
* - 批量: 批量审核/批量驳回/批量下架/批量改分类/批量删除
|
||
* - 详情弹窗: 含 SKU 列表 (编码/组合/封面/原价/现价/秒杀价/拼团价/库存/最小购买)
|
||
* - 审核弹窗: 通过/驳回 + 审核意见
|
||
* - 修改分类弹窗: 单个/批量
|
||
*
|
||
* 字段完全沿用老 vue: supplierGoodId, icon, title, enterpriseName,
|
||
* firstCategoryName, secondCategoryName, thirdCategoryName, brandName,
|
||
* unitName, onShelf, checkOpinion, checkStatus
|
||
*/
|
||
import { ref, onMounted, computed, reactive } from 'vue'
|
||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||
import DataTable from '@/components/DataTable/DataTable.vue'
|
||
import PageHeader from '@/components/PageHeader/PageHeader.vue'
|
||
import SearchBar from '@/components/SearchBar/SearchBar.vue'
|
||
import TableActions from '@/components/TableActions/TableActions.vue'
|
||
import ListTabs from '@/components/ListTabs/ListTabs.vue'
|
||
import { httpGet, httpPost } from '@/utils/request'
|
||
|
||
interface Good {
|
||
id: string
|
||
supplierGoodId: string
|
||
icon: string | null
|
||
title: string
|
||
enterpriseName: string
|
||
firstCategoryName: string
|
||
secondCategoryName: string
|
||
thirdCategoryName: string
|
||
brandName: string
|
||
unitName: string
|
||
onShelf: number
|
||
checkOpinion: string
|
||
checkStatus: number
|
||
createDate: string
|
||
raw: any
|
||
// SKU 列表 (详情用)
|
||
skuList?: SkuItem[]
|
||
}
|
||
|
||
interface SkuItem {
|
||
id: string
|
||
skuCode: string
|
||
propertyNames: string
|
||
icon: string | null
|
||
oldPrice: number
|
||
newPrice: number
|
||
seckillPrice: number | null
|
||
groupPrice: number | null
|
||
store: number
|
||
buyMin: number
|
||
raw: any
|
||
}
|
||
|
||
// 状态映射 (沿用老 vue checkStatus 1=待审核 2=已通过 3=已驳回, onShelf 0=下架 1=在售)
|
||
const CHECK_STATUS_MAP: Record<number, { label: string; color: string; bg: string }> = {
|
||
1: { label: '待审核', color: 'var(--st-warn-fg)', bg: 'var(--st-warn-bg)' },
|
||
2: { label: '已通过', color: 'var(--st-ok-fg)', bg: 'var(--st-ok-bg)' },
|
||
3: { label: '已驳回', color: 'var(--st-err-fg)', bg: 'var(--st-err-bg)' }
|
||
}
|
||
|
||
const ONSHELF_STATUS_MAP: Record<number, { label: string; color: string; bg: string }> = {
|
||
0: { label: '已下架', color: 'var(--st-lv4-fg)', bg: 'var(--st-lv4-bg)' },
|
||
1: { label: '已上架', color: 'var(--st-ok-fg)', bg: 'var(--st-ok-bg)' }
|
||
}
|
||
|
||
const STATUS_TABS = [
|
||
{ value: '', label: '全部' },
|
||
{ value: '1', label: '待审核' },
|
||
{ value: '2', label: '已通过' },
|
||
{ value: '3', label: '已驳回' }
|
||
]
|
||
|
||
// FALLBACK (老 vue 字段, 端点不可用时降级)
|
||
const FALLBACK: Good[] = [
|
||
{
|
||
id: '1', supplierGoodId: 'G20260705001', icon: null, title: '工业传感器 X100 高精度',
|
||
enterpriseName: '深圳科技股份有限公司', firstCategoryName: '工业设备', secondCategoryName: '传感器', thirdCategoryName: '工业传感器',
|
||
brandName: '西门子', unitName: '台', onShelf: 1, checkOpinion: '通过', checkStatus: 2, createDate: '2026-07-05 10:24:33', raw: {},
|
||
skuList: [
|
||
{ id: 's1', skuCode: 'X100-A001', propertyNames: '型号:X100 精度:0.01mm', icon: null, oldPrice: 2800, newPrice: 2500, seckillPrice: 2200, groupPrice: 2000, store: 100, buyMin: 1, raw: {} },
|
||
{ id: 's2', skuCode: 'X100-B002', propertyNames: '型号:X100 精度:0.05mm', icon: null, oldPrice: 2500, newPrice: 2200, seckillPrice: null, groupPrice: 1900, store: 80, buyMin: 1, raw: {} }
|
||
]
|
||
},
|
||
{
|
||
id: '2', supplierGoodId: 'G20260705002', icon: null, title: '智能控制器 Y200 多协议',
|
||
enterpriseName: '上海制造企业', firstCategoryName: '工业设备', secondCategoryName: '控制器', thirdCategoryName: '智能控制器',
|
||
brandName: '三菱', unitName: '台', onShelf: 1, checkOpinion: '', checkStatus: 1, createDate: '2026-07-05 11:15:00', raw: {}
|
||
},
|
||
{
|
||
id: '3', supplierGoodId: 'G20260705003', icon: null, title: '高精度测量仪 0.001mm',
|
||
enterpriseName: '北京电子有限公司', firstCategoryName: '仪器仪表', secondCategoryName: '测量仪', thirdCategoryName: '高精度测量仪',
|
||
brandName: '欧姆龙', unitName: '台', onShelf: 0, checkOpinion: '资料不全', checkStatus: 3, createDate: '2026-07-05 09:00:00', raw: {}
|
||
}
|
||
]
|
||
|
||
const list = ref<Good[]>([])
|
||
const total = ref(0)
|
||
const loading = ref(false)
|
||
const page = ref(1)
|
||
const limit = ref(10)
|
||
const error = ref<string | null>(null)
|
||
const selection = ref<Good[]>([])
|
||
|
||
// 状态统计 (与 tab 解耦, 只跟搜索条件关联 - 跨 tab 一致)
|
||
const checkStats = reactive({ pending: 0, approved: 0, rejected: 0 })
|
||
|
||
// W17.6: 商品总数 + SKU 总数 (实物商品下, 全量, 不分 checkStatus)
|
||
const counts = reactive({ goodCount: 0, skuCount: 0 })
|
||
|
||
const filters = reactive({
|
||
title: '',
|
||
enterpriseName: '',
|
||
checkStatus: ''
|
||
})
|
||
|
||
// 商品详情/审核抽屉 (2026-09-18 重设计: 大抽屉分区展示 完整信息 + 审核操作条)
|
||
const drawerVisible = ref(false)
|
||
const drawerLoading = ref(false)
|
||
const drawerRow = ref<Good | null>(null)
|
||
const drawerInfo = ref<any>(null)
|
||
const drawerFocusAudit = ref(false)
|
||
const drawerAuditForm = reactive({ checkStatus: 2, checkOpinion: '' })
|
||
const galleryActive = ref(0)
|
||
const categoryDialogVisible = ref(false)
|
||
const categoryData = ref<Good | null>(null)
|
||
const categoryForm = reactive({ firstCategoryId: '', secondCategoryId: '', thirdCategoryId: '' })
|
||
const batchCategoryDialogVisible = ref(false)
|
||
const batchRejectDialogVisible = ref(false)
|
||
const batchRejectForm = reactive({ reason: '' })
|
||
|
||
// httpGet/httpPost 已解包 code===0 的业务 data(拦截器已 reject 非 0 code),
|
||
// 拿到值即为成功;兼容对象/布尔/null 返回(与 InquiryDetailView 修复同源)
|
||
function isOk(r: any) {
|
||
return !(r && typeof r === 'object' && 'code' in r && Number(r.code) !== 0)
|
||
}
|
||
|
||
const columns = [
|
||
{ type: 'selection' as const, label: '', width: 50 },
|
||
{ prop: 'supplierGoodId', label: '商品ID', width: 100 },
|
||
{ prop: 'title', label: '商品名称', minWidth: 320, slot: 'title' },
|
||
{ prop: 'enterpriseName', label: '供应商', minWidth: 180, showOverflowTooltip: true },
|
||
{ prop: 'categoryName', label: '分类', minWidth: 180, slot: 'category' },
|
||
{ prop: 'brandName', label: '品牌', width: 110 },
|
||
{ prop: 'unitName', label: '单位', width: 90, minWidth: 90, align: 'center' as const },
|
||
{ prop: 'onShelf', label: '铺货', width: 100, align: 'center' as const, slot: 'onShelf' },
|
||
{ prop: 'checkStatus', label: '状态', width: 100, align: 'center' as const, slot: 'checkStatus' },
|
||
{ prop: 'createDate', label: '申请时间', width: 170 },
|
||
{ label: '操作', width: 140, align: 'center' as const, fixed: 'right' as const, slot: 'action' }
|
||
]
|
||
|
||
async function buildSearchParamsForStats() {
|
||
const params: any = { type: 1 }
|
||
if (filters.title) params.title = filters.title
|
||
return params
|
||
}
|
||
|
||
async function loadStats() {
|
||
try {
|
||
const base = await buildSearchParamsForStats()
|
||
const [r1, r2, r3] = await Promise.all([
|
||
httpGet('/supplier/good/pageList', { ...base, checkStatus: 1, page: 1, limit: 1 }),
|
||
httpGet('/supplier/good/pageList', { ...base, checkStatus: 2, page: 1, limit: 1 }),
|
||
httpGet('/supplier/good/pageList', { ...base, checkStatus: 3, page: 1, limit: 1 })
|
||
])
|
||
checkStats.pending = Number(r1?.total ?? 0)
|
||
checkStats.approved = Number(r2?.total ?? 0)
|
||
checkStats.rejected = Number(r3?.total ?? 0)
|
||
} catch {
|
||
checkStats.pending = 0
|
||
checkStats.approved = 0
|
||
checkStats.rejected = 0
|
||
}
|
||
}
|
||
|
||
// W17.6: 全量商品数 + 全量 SKU 数 (实物, type=1, 不分 checkStatus / 搜索)
|
||
async function loadCounts() {
|
||
try {
|
||
const r: any = await httpGet('/supplier/good/sku-count', { type: 1 })
|
||
counts.goodCount = Number(r?.goodCount ?? 0)
|
||
counts.skuCount = Number(r?.skuCount ?? 0)
|
||
} catch {
|
||
counts.goodCount = 0
|
||
counts.skuCount = 0
|
||
}
|
||
}
|
||
|
||
async function load() {
|
||
loading.value = true
|
||
error.value = null
|
||
try {
|
||
// W13.1: /supplier/good/pageList 返完整字段 — 比 search 端点更全
|
||
// 2026-09-20: 筛选条件下推到后端 (原仅前端过滤当前页, 导致 tab 计数 42 与列表 16106 条不一致)
|
||
const queryParams: Record<string, any> = {
|
||
page: page.value,
|
||
limit: limit.value,
|
||
type: 1 // 1=实物商品 (2=服务商品, 在 ServiceGoodListView 单独展示)
|
||
}
|
||
if (filters.title) queryParams.title = filters.title
|
||
if (filters.checkStatus) queryParams.checkStatus = Number(filters.checkStatus)
|
||
if (filters.enterpriseName) queryParams.enterpriseName = filters.enterpriseName
|
||
const data = await httpGet<{ total: number | string; list: any[] }>('/supplier/good/pageList', queryParams)
|
||
let rows = (data.list || []).map((it: any) => ({
|
||
id: String(it.id),
|
||
supplierGoodId: it.supplierGoodId || String(it.id),
|
||
icon: it.icon || null,
|
||
title: it.title || '--',
|
||
enterpriseName: it.enterpriseName || '--',
|
||
firstCategoryName: it.firstCategoryName || '',
|
||
secondCategoryName: it.secondCategoryName || '',
|
||
thirdCategoryName: it.thirdCategoryName || '',
|
||
brandName: it.brandName || '--',
|
||
unitName: it.unitName || '--',
|
||
onShelf: it.onShelf ?? 1,
|
||
checkOpinion: it.checkOpinion || '',
|
||
checkStatus: it.checkStatus ?? 2,
|
||
createDate: it.createDate || '',
|
||
raw: it
|
||
}))
|
||
// 筛选已由后端条件完成 (checkStatus/enterpriseName/title 下推), rows 即当前页数据
|
||
list.value = rows
|
||
total.value = Number(data.total) || 0
|
||
} catch (e: any) {
|
||
error.value = e?.msg || e?.message || '加载失败'
|
||
list.value = []
|
||
total.value = 0
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
function onSearch() { page.value = 1; Promise.all([load(), loadStats(), loadCounts()]) }
|
||
function onReset() { filters.title = ''; filters.enterpriseName = ''; filters.checkStatus = ''; page.value = 1; Promise.all([load(), loadStats(), loadCounts()]) }
|
||
function onPageChange(p: number) { page.value = p; load() }
|
||
function onLimitChange(l: number) { limit.value = l; page.value = 1; load() }
|
||
function onSelectionChange(rows: Good[]) { selection.value = rows }
|
||
|
||
const tabItems = computed(() => [
|
||
// 全部 = 待审核 + 已通过 + 已驳回 (跨 tab 一致, 跟搜索条件关联)
|
||
{ key: '', label: '全部', count: checkStats.pending + checkStats.approved + checkStats.rejected },
|
||
{ key: '1', label: '待审核', count: checkStats.pending },
|
||
{ key: '2', label: '已通过', count: checkStats.approved },
|
||
{ key: '3', label: '已驳回', count: checkStats.rejected }
|
||
])
|
||
|
||
function onStatusTab(v: string) { filters.checkStatus = v; page.value = 1; load() }
|
||
|
||
function splitImages(v: any): string[] {
|
||
return String(v || '').split(',').map((s: string) => s.trim()).filter(Boolean)
|
||
}
|
||
/** 打开商品详情/审核抽屉 (focusAudit=true 时审核入口进入并定位操作条) */
|
||
async function openGoodDrawer(row: Good, focusAudit = false) {
|
||
drawerRow.value = row
|
||
drawerInfo.value = null
|
||
drawerFocusAudit.value = focusAudit
|
||
drawerAuditForm.checkStatus = 2
|
||
drawerAuditForm.checkOpinion = ''
|
||
galleryActive.value = 0
|
||
drawerVisible.value = true
|
||
drawerLoading.value = true
|
||
try {
|
||
// 列表行 id 是 tb_good(铺货表) 的 id; 详情接口要 tb_supplier_good.id -> 用行的 supplierGoodId
|
||
const detailId = Number(row.raw?.supplierGoodId) || Number(row.raw?.supplier_good_id) || Number(row.id)
|
||
const r: any = await httpGet('/supplier/supplierGood/info', { id: detailId })
|
||
// httpGet 已解包: r 即 SupplierGoodDTO (含 images/content/skuList/skuDetailList)
|
||
drawerInfo.value = r && (r.id || r.title) ? r : (row.raw || row)
|
||
} catch {
|
||
drawerInfo.value = row.raw || row
|
||
} finally { drawerLoading.value = false }
|
||
if (focusAudit) {
|
||
setTimeout(() => document.querySelector('.rr-gd-audit')?.scrollIntoView({ behavior: 'smooth', block: 'center' }), 400)
|
||
}
|
||
}
|
||
const drawerImages = computed(() => splitImages(drawerInfo.value?.images))
|
||
const drawerSkuTotalStore = computed(() => (drawerInfo.value?.skuDetailList || []).reduce((s: number, it: any) => s + (Number(it.store) || 0), 0))
|
||
const drawerCategoryPath = computed(() => [drawerInfo.value?.firstCategoryName, drawerInfo.value?.secondCategoryName, drawerInfo.value?.thirdCategoryName].filter(Boolean).join(' / '))
|
||
function onShowDetail(row: Good) { openGoodDrawer(row, false) }
|
||
function onShowCheckDialog(row: Good) { openGoodDrawer(row, true) }
|
||
async function onSubmitDrawerAudit() {
|
||
const f = drawerAuditForm
|
||
if (f.checkStatus === 3 && !f.checkOpinion.trim()) { ElMessage.warning('驳回时请填写审核意见'); return }
|
||
const id = drawerRow.value?.id || drawerInfo.value?.id
|
||
try {
|
||
const r: any = await httpGet('/supplier/good/checkGood', {
|
||
id, checkStatus: f.checkStatus, checkOpinion: f.checkOpinion.trim()
|
||
})
|
||
if (isOk(r)) {
|
||
ElMessage.success(f.checkStatus === 2 ? '审核通过' : '已驳回')
|
||
drawerVisible.value = false
|
||
load()
|
||
} else { ElMessage.error(r?.msg || '操作失败') }
|
||
} catch (e: any) { ElMessage.error(e?.msg || e?.message || '操作失败') }
|
||
}
|
||
async function onOnShelf(row: Good) {
|
||
try { await ElMessageBox.confirm(`确认${row.onShelf === 1 ? '下架' : '上架'}「${row.title}」?`, '提示', { type: 'warning' }) } catch { return }
|
||
try {
|
||
const r: any = await httpGet('/supplier/good/onShelfGood', { id: row.id })
|
||
if (isOk(r)) { ElMessage.success(row.onShelf === 1 ? '已下架' : '已上架'); load() }
|
||
else ElMessage.error(r?.msg || '操作失败')
|
||
} catch (e: any) { ElMessage.error(e?.msg || e?.message || '操作失败') }
|
||
}
|
||
function onShowCategoryDialog(row: Good) { categoryData.value = row; categoryDialogVisible.value = true }
|
||
async function onSubmitCategory() {
|
||
try {
|
||
const r: any = await httpPost('/supplier/good/update', {
|
||
id: categoryData.value?.id || categoryData.value?.raw?.id,
|
||
firstCategoryId: categoryForm.firstCategoryId || undefined,
|
||
secondCategoryId: categoryForm.secondCategoryId || undefined,
|
||
thirdCategoryId: categoryForm.thirdCategoryId || undefined
|
||
})
|
||
if (isOk(r)) { ElMessage.success('分类已修改'); categoryDialogVisible.value = false; load() }
|
||
else ElMessage.error(r?.msg || '操作失败')
|
||
} catch (e: any) { ElMessage.error(e?.msg || e?.message || '操作失败') }
|
||
}
|
||
async function onDelete(row: Good) {
|
||
try { await ElMessageBox.confirm(`确认删除「${row.title}」?`, '提示', { type: 'warning' }) } catch { return }
|
||
try {
|
||
const r: any = await httpGet('/supplier/good/delete', { id: row.id })
|
||
if (isOk(r)) { ElMessage.success('已删除'); load() }
|
||
else ElMessage.error(r?.msg || '操作失败')
|
||
} catch (e: any) { ElMessage.error(e?.msg || e?.message || '操作失败') }
|
||
}
|
||
|
||
function onBatchCheck() {
|
||
if (selection.value.length === 0) { ElMessage.warning('请先选择商品'); return }
|
||
ElMessageBox.confirm(`确认批量通过 ${selection.value.length} 个商品?`, '提示', { type: 'success' }).then(async () => {
|
||
try { for (const s of selection.value) { await httpPost('/supplier/good/checkGood', { id: s.id, checkStatus: 2, checkOpinion: '批量审核通过' }) } ElMessage.success(`批量审核通过 ${selection.value.length} 个`); load() }
|
||
catch (e: any) { ElMessage.error(e?.msg || '批量审核失败'); load() }
|
||
}).catch(() => {})
|
||
}
|
||
function onBatchReject() { if (selection.value.length === 0) { ElMessage.warning('请先选择商品'); return } batchRejectDialogVisible.value = true }
|
||
function onSubmitBatchReject() {
|
||
if (!batchRejectForm.reason.trim()) { ElMessage.warning('请填写驳回原因'); return }
|
||
Promise.all(selection.value.map(s => httpPost('/supplier/good/checkGood', { id: s.id, checkStatus: 3, checkOpinion: batchRejectForm.reason }))).then(() => {
|
||
ElMessage.success(`已驳回 ${selection.value.length} 个商品`); batchRejectDialogVisible.value = false; load()
|
||
}).catch((e: any) => ElMessage.error(e?.msg || '批量驳回失败'))
|
||
}
|
||
function onBatchOnShelf() {
|
||
if (selection.value.length === 0) { ElMessage.warning('请先选择商品'); return }
|
||
ElMessageBox.confirm(`确认批量下架 ${selection.value.length} 个商品?`, '提示', { type: 'warning' }).then(async () => {
|
||
try { for (const s of selection.value) { await httpPost('/supplier/good/goodOnShelf', { id: s.id, onShelf: 0 }) } ElMessage.success(`批量下架 ${selection.value.length} 个`); load() }
|
||
catch (e: any) { ElMessage.error(e?.msg || '批量下架失败'); load() }
|
||
}).catch(() => {})
|
||
}
|
||
function onShowBatchCategoryDialog() {
|
||
if (selection.value.length === 0) { ElMessage.warning('请先选择商品'); return }
|
||
batchCategoryDialogVisible.value = true
|
||
}
|
||
function onSubmitBatchCategory() {
|
||
Promise.all(selection.value.map(s => httpPost('/supplier/good/updateSupplierGood', { id: s.id, firstCategoryId: categoryForm.firstCategoryId }))).then(() => {
|
||
ElMessage.success(`已修改 ${selection.value.length} 个商品分类`); batchCategoryDialogVisible.value = false; load()
|
||
}).catch((e: any) => ElMessage.error(e?.msg || '批量改分类失败'))
|
||
}
|
||
function onDeleteBatch() {
|
||
if (selection.value.length === 0) { ElMessage.warning('请先选择商品'); return }
|
||
ElMessageBox.confirm(`确认删除 ${selection.value.length} 个商品?`, '提示', { type: 'warning' }).then(async () => {
|
||
try { for (const s of selection.value) { await httpPost('/supplier/good/deleteSupplierGood', { id: s.id }) } ElMessage.success('批量删除成功'); load() }
|
||
catch (e: any) { ElMessage.error(e?.msg || '批量删除失败'); load() }
|
||
}).catch(() => {})
|
||
}
|
||
function onExport() { ElMessage.info('导出功能开发中') }
|
||
function onAdd() { ElMessage.info('新增商品请前往供应商端') }
|
||
|
||
onMounted(() => Promise.all([load(), loadStats(), loadCounts()]))
|
||
</script>
|
||
|
||
<template>
|
||
<div class="rr-page">
|
||
<PageHeader title="商品管理" :sub="`共 ${counts.goodCount} 个商品,${counts.skuCount} 个SKU`">
|
||
<el-button @click="onExport">导出</el-button>
|
||
<el-button type="primary" @click="onAdd">新增商品</el-button>
|
||
</PageHeader>
|
||
|
||
<ListTabs v-model="filters.checkStatus" :items="tabItems" @change="onStatusTab" />
|
||
|
||
<SearchBar
|
||
v-model:filters="filters"
|
||
:fields="[
|
||
{ key: 'title', label: '商品名称', type: 'text', placeholder: '请输入商品名称' },
|
||
{ key: 'enterpriseName', label: '供应商', type: 'text', placeholder: '供应商名称' }
|
||
]"
|
||
@search="onSearch"
|
||
@reset="onReset"
|
||
/>
|
||
|
||
<!-- 批量操作 -->
|
||
<div class="rr-batch-bar" v-if="selection.length > 0">
|
||
<span class="rr-batch-tip">已选 {{ selection.length }} 项</span>
|
||
<el-button type="success" size="small" @click="onBatchCheck">批量审核</el-button>
|
||
<el-button type="danger" size="small" @click="onBatchReject">批量驳回</el-button>
|
||
<el-button type="warning" size="small" @click="onBatchOnShelf">批量下架</el-button>
|
||
<el-button size="small" @click="onShowBatchCategoryDialog">批量修改分类</el-button>
|
||
<el-button type="danger" size="small" plain @click="onDeleteBatch">批量删除</el-button>
|
||
</div>
|
||
|
||
<!-- 表格 (用 DataTable 套裸 el-table, 复用分页 + :borderless + :page-sizes + el-result 错误展示) -->
|
||
<DataTable
|
||
:columns="columns"
|
||
:data="list"
|
||
:loading="loading"
|
||
:total="total"
|
||
:page="page"
|
||
:limit="limit"
|
||
:page-sizes="[10, 20, 50, 100]"
|
||
:show-pagination="true"
|
||
:error="error"
|
||
empty-text="暂无商品"
|
||
:borderless="true"
|
||
row-key="id"
|
||
@page-change="onPageChange"
|
||
@limit-change="onLimitChange"
|
||
@selection-change="onSelectionChange"
|
||
>
|
||
<template #title="{ row }">
|
||
<div class="rr-good-cell">
|
||
<div v-if="row.icon" class="rr-good-icon">
|
||
<el-image :src="row.icon" :preview-src-list="[row.icon]" fit="cover" style="width: 40px; height: 40px; border-radius: 4px" preview-teleported />
|
||
</div>
|
||
<div v-else class="rr-good-icon-placeholder">{{ (row.title || '?').charAt(0) }}</div>
|
||
<span class="rr-good-title">{{ row.title }}</span>
|
||
</div>
|
||
</template>
|
||
<template #category="{ row }">
|
||
<span>{{ [row.firstCategoryName, row.secondCategoryName, row.thirdCategoryName].filter(Boolean).join(' / ') || '--' }}</span>
|
||
</template>
|
||
<template #onShelf="{ row }">
|
||
<span class="st-pill" :style="{ color: ONSHELF_STATUS_MAP[row.onShelf]?.color, background: ONSHELF_STATUS_MAP[row.onShelf]?.bg }"><span class="st-dot"></span>{{ ONSHELF_STATUS_MAP[row.onShelf]?.label }}</span>
|
||
</template>
|
||
<template #checkStatus="{ row }">
|
||
<span class="st-pill" :style="{ color: CHECK_STATUS_MAP[row.checkStatus]?.color, background: CHECK_STATUS_MAP[row.checkStatus]?.bg }"><span class="st-dot"></span>{{ CHECK_STATUS_MAP[row.checkStatus]?.label }}</span>
|
||
</template>
|
||
<template #action="{ row }">
|
||
<TableActions
|
||
:row="row"
|
||
:actions="[
|
||
{ label: '详情', type: 'primary', onClick: onShowDetail },
|
||
{ label: '审核', type: 'primary', when: (r: any) => r.checkStatus === 1, onClick: onShowCheckDialog },
|
||
{ label: (r: any) => r.onShelf === 1 ? '下架' : '上架', type: (r: any) => r.onShelf === 1 ? 'warning' : 'success', onClick: onOnShelf },
|
||
{ label: '改分类', type: 'primary', onClick: onShowCategoryDialog },
|
||
{ label: '删除', type: 'danger', divided: true, onClick: onDelete }
|
||
]"
|
||
/>
|
||
</template>
|
||
</DataTable>
|
||
|
||
<!-- 原 el-table 区域 (暂时保留作为参考, 实际由 DataTable 接管) -->
|
||
<div v-if="false" class="rr-card">
|
||
<el-table
|
||
:data="list"
|
||
border
|
||
style="width: 100%"
|
||
v-loading="loading"
|
||
:empty-text="error || '暂无商品'"
|
||
@selection-change="onSelectionChange"
|
||
>
|
||
<el-table-column type="selection" width="50" align="center" />
|
||
<el-table-column prop="supplierGoodId" label="商品ID" width="140" align="center" show-overflow-tooltip />
|
||
<el-table-column label="商品名称" min-width="240">
|
||
<template #default="{ row }">
|
||
<div class="rr-good-cell">
|
||
<div v-if="row.icon" class="rr-good-icon">
|
||
<el-image :src="row.icon" :preview-src-list="[row.icon]" fit="cover" style="width: 40px; height: 40px; border-radius: 4px" preview-teleported />
|
||
</div>
|
||
<div v-else class="rr-good-icon-placeholder">{{ (row.title || '?').charAt(0) }}</div>
|
||
<span class="rr-good-title">{{ row.title }}</span>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="enterpriseName" label="供应商" min-width="180" show-overflow-tooltip />
|
||
<el-table-column label="分类" min-width="240" show-overflow-tooltip>
|
||
<template #default="{ row }">
|
||
<span>{{ [row.firstCategoryName, row.secondCategoryName, row.thirdCategoryName].filter(Boolean).join(' / ') || '--' }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="brandName" label="品牌" width="120" />
|
||
<el-table-column prop="unitName" label="单位" width="80" align="center" />
|
||
<el-table-column label="铺货" width="100" align="center">
|
||
<template #default="{ row }">
|
||
<span class="st-pill" :style="{ color: ONSHELF_STATUS_MAP[row.onShelf]?.color, background: ONSHELF_STATUS_MAP[row.onShelf]?.bg }"><span class="st-dot"></span>{{ ONSHELF_STATUS_MAP[row.onShelf]?.label }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="checkOpinion" label="审核意见" min-width="160" show-overflow-tooltip />
|
||
<el-table-column label="状态" width="100" align="center">
|
||
<template #default="{ row }">
|
||
<span class="st-pill" :style="{ color: CHECK_STATUS_MAP[row.checkStatus]?.color, background: CHECK_STATUS_MAP[row.checkStatus]?.bg }"><span class="st-dot"></span>{{ CHECK_STATUS_MAP[row.checkStatus]?.label }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="申请时间" prop="createDate" width="170" />
|
||
<el-table-column label="操作" width="280" align="center" fixed="right">
|
||
<template #default="{ row }">
|
||
<el-button text type="primary" size="small" @click="onShowDetail(row)">详情</el-button>
|
||
<el-button v-if="row.checkStatus === 1" text type="primary" size="small" @click="onShowCheckDialog(row)">审核</el-button>
|
||
<el-button text :type="row.onShelf === 1 ? 'warning' : 'success'" size="small" @click="onOnShelf(row)">
|
||
{{ row.onShelf === 1 ? '下架' : '上架' }}
|
||
</el-button>
|
||
<el-button text type="primary" size="small" @click="onShowCategoryDialog(row)">改分类</el-button>
|
||
<el-button text type="danger" size="small" @click="onDelete(row)">删除</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</div>
|
||
|
||
<!-- ===== 商品详情 / 审核抽屉 (2026-09-18 重设计: 分区式完整信息 + 底部审核操作条) ===== -->
|
||
<el-drawer v-model="drawerVisible" size="min(940px, 94vw)" :with-header="true"
|
||
:title="drawerRow?.title ? `商品详情 · ${drawerRow.title}` : '商品详情'"
|
||
destroy-on-close :close-on-click-modal="false" class="rr-gd-drawer">
|
||
<div v-loading="drawerLoading" class="rr-gd-wrap">
|
||
<template v-if="drawerInfo">
|
||
<!-- 头部: 主图 + 标题/标签 + 元信息 + 价格 -->
|
||
<div class="rr-gd-head">
|
||
<el-image :src="drawerInfo.icon || drawerImages[0]" fit="cover"
|
||
:preview-src-list="[drawerInfo.icon || drawerImages[0]].filter(Boolean)" preview-teleported
|
||
class="rr-gd-mainimg">
|
||
<template #error><div class="rr-gd-imgerr">无图</div></template>
|
||
</el-image>
|
||
<div class="rr-gd-headmain">
|
||
<div class="rr-gd-titleline">
|
||
<span class="rr-gd-title">{{ drawerInfo.title || drawerRow?.title }}
|
||
<span v-if="Number(drawerInfo.sourceType) === 2" class="px-1.5 py-0.5 bg-amber-50 text-amber-600 border border-amber-200 rounded text-[10px] font-bold align-middle" style="margin-left:6px">共享商品</span>
|
||
</span>
|
||
<span class="rr-gd-code">{{ drawerInfo.goodCode || drawerInfo.id }}</span>
|
||
</div>
|
||
<div class="rr-gd-tags">
|
||
<span class="st-pill" :style="{ color: CHECK_STATUS_MAP[Number(drawerRow?.checkStatus ?? -1)]?.color, background: CHECK_STATUS_MAP[Number(drawerRow?.checkStatus ?? -1)]?.bg }"><span class="st-dot"></span>{{ (drawerRow ? CHECK_STATUS_MAP[drawerRow.checkStatus] : undefined)?.label || '--' }}</span>
|
||
<span class="st-pill" :style="{ color: ONSHELF_STATUS_MAP[Number(drawerRow?.onShelf ?? -1)]?.color, background: ONSHELF_STATUS_MAP[Number(drawerRow?.onShelf ?? -1)]?.bg }"><span class="st-dot"></span>{{ ONSHELF_STATUS_MAP[Number(drawerRow?.onShelf ?? -1)]?.label || '--' }}</span>
|
||
</div>
|
||
<div class="rr-gd-price">
|
||
<span class="rr-gd-price-now">¥ {{ drawerInfo.newPrice ?? drawerRow?.raw?.newPrice ?? '--' }}</span>
|
||
<span v-if="drawerInfo.oldPrice" class="rr-gd-price-old">¥ {{ drawerInfo.oldPrice }}</span>
|
||
<span class="rr-gd-price-unit">/ {{ drawerInfo.unitName || drawerRow?.unitName || '件' }}</span>
|
||
</div>
|
||
<div class="rr-gd-meta">
|
||
<span><label>供应商</label>{{ drawerInfo.enterpriseName || drawerRow?.enterpriseName || '--' }}</span>
|
||
<span><label>品牌</label>{{ drawerInfo.brandName || drawerRow?.brandName || '--' }}</span>
|
||
<span><label>分类</label>{{ drawerCategoryPath || '--' }}</span>
|
||
<span><label>申请时间</label>{{ drawerInfo.createDate || drawerRow?.createDate || '--' }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 商品图片画廊 -->
|
||
<div class="rr-gd-section" v-if="drawerImages.length">
|
||
<h4 class="rr-gd-sectitle">商品图片 <span class="rr-gd-seccount">{{ drawerImages.length }} 张</span></h4>
|
||
<div class="rr-gd-gallery">
|
||
<el-image v-for="(img, gi) in drawerImages" :key="gi" :src="img" fit="cover"
|
||
:preview-src-list="drawerImages" :initial-index="gi" preview-teleported hide-on-click-modal
|
||
class="rr-gd-thumb">
|
||
<template #error><div class="rr-gd-imgerr">加载失败</div></template>
|
||
</el-image>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 介绍视频 -->
|
||
<div class="rr-gd-section" v-if="drawerInfo.videoUrl">
|
||
<h4 class="rr-gd-sectitle">介绍视频</h4>
|
||
<video :src="drawerInfo.videoUrl" controls preload="metadata" class="rr-gd-video"></video>
|
||
</div>
|
||
|
||
<!-- 规格与 SKU -->
|
||
<div class="rr-gd-section" v-if="(drawerInfo.skuList?.length || drawerInfo.skuDetailList?.length)">
|
||
<h4 class="rr-gd-sectitle">规格与 SKU <span class="rr-gd-seccount">{{ drawerInfo.skuDetailList?.length || 0 }} 个 SKU · 库存合计 {{ drawerSkuTotalStore }}</span></h4>
|
||
<div v-if="drawerInfo.skuList?.length" class="rr-gd-props">
|
||
<div v-for="grp in drawerInfo.skuList" :key="grp.id" class="rr-gd-prop">
|
||
<label>{{ grp.name }}</label>
|
||
<span class="rr-gd-propchip" v-for="pv in grp.propertyList" :key="pv.id">{{ pv.name }}</span>
|
||
</div>
|
||
</div>
|
||
<el-table v-if="drawerInfo.skuDetailList?.length" :data="drawerInfo.skuDetailList" border size="small"
|
||
max-height="320" class="rr-gd-skutable">
|
||
<el-table-column label="规格组合" prop="propertyNames" min-width="180" show-overflow-tooltip />
|
||
<el-table-column label="图片" width="64" align="center">
|
||
<template #default="{ row }">
|
||
<el-image v-if="row.icon" :src="row.icon" fit="cover" :preview-src-list="[row.icon]" preview-teleported class="rr-gd-skuimg" />
|
||
<span v-else>--</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="SKU编码" prop="skuCode" min-width="150" show-overflow-tooltip />
|
||
<el-table-column label="现价" width="96" align="right">
|
||
<template #default="{ row }">¥ {{ row.newPrice ?? '--' }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="原价" width="96" align="right">
|
||
<template #default="{ row }">{{ row.oldPrice ?? '--' }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="秒杀价" width="96" align="right">
|
||
<template #default="{ row }">{{ row.seckillPrice ?? '--' }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="拼团价" width="96" align="right">
|
||
<template #default="{ row }">{{ row.groupPrice ?? '--' }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="库存" width="72" align="center" prop="store" />
|
||
<el-table-column label="起购" width="64" align="center" prop="buyMin" />
|
||
<el-table-column label="限购" width="64" align="center">
|
||
<template #default="{ row }">{{ row.limitNum ?? '--' }}</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</div>
|
||
|
||
<!-- 商品详情 (富文本) -->
|
||
<div class="rr-gd-section">
|
||
<h4 class="rr-gd-sectitle">商品详情</h4>
|
||
<div v-if="drawerInfo.content" class="rr-gd-content" v-html="drawerInfo.content"></div>
|
||
<div v-else-if="drawerInfo.remark" class="rr-gd-content">{{ drawerInfo.remark }}</div>
|
||
<div v-else class="rr-gd-emptypar">商家未填写图文详情</div>
|
||
</div>
|
||
|
||
<!-- 审核信息 -->
|
||
<div class="rr-gd-section rr-gd-auditinfo">
|
||
<h4 class="rr-gd-sectitle">审核信息</h4>
|
||
<div class="rr-gd-auditline">
|
||
<span><label>当前状态</label>{{ (drawerRow ? CHECK_STATUS_MAP[drawerRow.checkStatus] : undefined)?.label || '--' }}</span>
|
||
<span><label>审核意见</label>{{ drawerRow?.checkOpinion || drawerInfo?.checkOpinion || '—' }}</span>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
|
||
<!-- 底部固定审核操作条 -->
|
||
<template #footer>
|
||
<!-- 待审核判断以列表行状态为准 (审核入口数据源; 个别脏数据 info 返回值可能不一致) -->
|
||
<div v-if="drawerRow && Number(drawerRow.checkStatus) === 1" class="rr-gd-audit">
|
||
<el-radio-group v-model="drawerAuditForm.checkStatus" class="rr-gd-auditradio">
|
||
<el-radio :value="2">审核通过</el-radio>
|
||
<el-radio :value="3">审核驳回</el-radio>
|
||
</el-radio-group>
|
||
<!-- 通过无需填写审核意见; 驳回时必填 -->
|
||
<el-input v-if="drawerAuditForm.checkStatus === 3" v-model="drawerAuditForm.checkOpinion" type="textarea"
|
||
:rows="2" maxlength="200" placeholder="请填写驳回原因(必填),将展示给商家" class="rr-gd-auditinput" />
|
||
<div class="rr-gd-auditbtns">
|
||
<el-button @click="drawerVisible = false">取消</el-button>
|
||
<el-button type="primary" @click="onSubmitDrawerAudit">提交审核结果</el-button>
|
||
</div>
|
||
</div>
|
||
<div v-else class="rr-gd-auditdone">
|
||
<span v-if="drawerRow">该商品已完成审核({{ CHECK_STATUS_MAP[drawerRow.checkStatus]?.label }}<template v-if="drawerInfo?.checkOpinion">:{{ drawerInfo.checkOpinion }}</template>),如需修改请使用列表「⋯ → 上架/下架」操作。</span>
|
||
</div>
|
||
</template>
|
||
</el-drawer>
|
||
|
||
<!-- 修改分类弹窗 (单个) -->
|
||
<el-dialog v-model="categoryDialogVisible" title="修改分类" width="500px">
|
||
<el-form v-if="categoryData" label-width="100px">
|
||
<el-form-item label="商品名称">{{ categoryData.title }}</el-form-item>
|
||
<el-form-item label="当前分类">
|
||
{{ [categoryData.firstCategoryName, categoryData.secondCategoryName, categoryData.thirdCategoryName].filter(Boolean).join(' / ') }}
|
||
</el-form-item>
|
||
<el-form-item label="新分类">
|
||
<el-cascader v-model="categoryForm" :options="[]" placeholder="请选择新分类" style="width: 100%" />
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button @click="categoryDialogVisible = false">取消</el-button>
|
||
<el-button type="primary" @click="onSubmitCategory">确定</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 批量修改分类弹窗 -->
|
||
<el-dialog v-model="batchCategoryDialogVisible" title="批量修改分类" width="500px">
|
||
<el-form label-width="100px">
|
||
<el-form-item label="已选商品">共 {{ selection.length }} 个</el-form-item>
|
||
<el-form-item label="新分类">
|
||
<el-cascader :options="[]" placeholder="请选择新分类" style="width: 100%" />
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button @click="batchCategoryDialogVisible = false">取消</el-button>
|
||
<el-button type="primary" @click="onSubmitBatchCategory">确定</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 批量驳回弹窗 -->
|
||
<el-dialog v-model="batchRejectDialogVisible" title="批量驳回" width="500px">
|
||
<el-form label-width="100px">
|
||
<el-form-item label="已选商品">共 {{ selection.length }} 个</el-form-item>
|
||
<el-form-item label="驳回原因" required>
|
||
<el-input v-model="batchRejectForm.reason" type="textarea" :rows="4" placeholder="请填写驳回原因" />
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button @click="batchRejectDialogVisible = false">取消</el-button>
|
||
<el-button type="primary" @click="onSubmitBatchReject">确定</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped lang="scss">
|
||
.rr-page { max-width: 1500px; }
|
||
.rr-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; margin-bottom: 16px; }
|
||
.rr-batch-bar { display: flex; align-items: center; gap: 8px; padding: 10px 16px; background: var(--bg-soft); border-radius: var(--radius); margin-bottom: 12px; }
|
||
.rr-batch-tip { font-size: 13px; color: var(--ink-2); margin-right: 8px; }
|
||
.rr-good-cell { display: flex; align-items: center; gap: 10px; }
|
||
.rr-good-icon-placeholder { width: 40px; height: 40px; border-radius: 4px; background: var(--bg-soft); display: flex; align-items: center; justify-content: center; color: var(--ink-3); font-weight: 600; flex-shrink: 0; }
|
||
.rr-good-title { font-size: 13.5px; color: var(--ink); }
|
||
.rr-sku-title { font-size: 14px; font-weight: 600; color: var(--ink); margin: 24px 0 12px 0; }
|
||
.rr-empty-tip { text-align: center; padding: 30px 0; color: var(--ink-3); }
|
||
|
||
/* ===== 商品详情/审核抽屉 ===== */
|
||
.rr-gd-wrap { padding: 0 2px; }
|
||
.rr-gd-head { display: flex; gap: 16px; padding: 4px 0 14px; border-bottom: 1px solid var(--border); }
|
||
.rr-gd-mainimg { flex: 0 0 96px; width: 96px; height: 96px; border-radius: 12px; border: 1px solid var(--border); background: var(--bg, #f6f8fb); }
|
||
.rr-gd-imgerr { display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; font-size: 12px; color: var(--ink-3); }
|
||
.rr-gd-headmain { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 8px; }
|
||
.rr-gd-titleline { display: flex; align-items: baseline; gap: 10px; min-width: 0; }
|
||
.rr-gd-title { font-size: 16px; font-weight: 700; color: var(--ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||
.rr-gd-code { flex: 0 0 auto; font-size: 11px; font-family: 'SF Mono', Menlo, Consolas, monospace; color: var(--ink-3); background: var(--bg, #f6f8fb); border: 1px solid var(--border); border-radius: 4px; padding: 1px 8px; }
|
||
.rr-gd-tags { display: flex; gap: 8px; }
|
||
.rr-gd-price { display: flex; align-items: baseline; gap: 8px; }
|
||
.rr-gd-price-now { font-size: 22px; font-weight: 800; color: #dc2626; }
|
||
.rr-gd-price-old { font-size: 12px; color: var(--ink-3); text-decoration: line-through; }
|
||
.rr-gd-price-unit { font-size: 12px; color: var(--ink-3); }
|
||
.rr-gd-meta { display: flex; flex-wrap: wrap; gap: 6px 18px; font-size: 12px; color: var(--ink-2); }
|
||
.rr-gd-meta label { color: var(--ink-3); margin-right: 4px; }
|
||
.rr-gd-section { margin-top: 18px; }
|
||
.rr-gd-sectitle { font-size: 13px; font-weight: 700; color: var(--ink); margin-bottom: 10px; padding-left: 8px; border-left: 3px solid var(--primary); line-height: 1.2; }
|
||
.rr-gd-seccount { font-weight: 500; font-size: 11px; color: var(--ink-3); margin-left: 6px; }
|
||
.rr-gd-gallery { display: flex; flex-wrap: wrap; gap: 10px; }
|
||
.rr-gd-thumb { width: 92px; height: 92px; border-radius: 10px; border: 1px solid var(--border); cursor: zoom-in; background: var(--bg, #f6f8fb); }
|
||
.rr-gd-video { max-width: 360px; width: 100%; border-radius: 10px; border: 1px solid var(--border); background: #000; }
|
||
.rr-gd-props { display: flex; flex-direction: column; gap: 8px; margin-bottom: 12px; }
|
||
.rr-gd-prop { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
||
.rr-gd-prop label { flex: 0 0 88px; font-size: 12px; color: var(--ink-3); }
|
||
.rr-gd-propchip { font-size: 11px; color: var(--ink-2); background: var(--bg, #f6f8fb); border: 1px solid var(--border); border-radius: 20px; padding: 2px 10px; }
|
||
.rr-gd-skuimg { width: 40px; height: 40px; border-radius: 6px; border: 1px solid var(--border); cursor: zoom-in; }
|
||
.rr-gd-content { border: 1px solid var(--border); border-radius: 10px; padding: 12px 14px; background: var(--surface); max-height: 460px; overflow-y: auto; }
|
||
.rr-gd-content :deep(img) { max-width: 100%; height: auto; display: block; margin: 6px 0; border-radius: 6px; }
|
||
.rr-gd-content :deep(p) { margin: 6px 0; line-height: 1.8; font-size: 13px; color: var(--ink); }
|
||
.rr-gd-emptypar { font-size: 12px; color: var(--ink-3); background: var(--bg, #f6f8fb); border-radius: 8px; padding: 14px; }
|
||
.rr-gd-auditinfo .rr-gd-auditline { display: flex; gap: 28px; font-size: 13px; color: var(--ink-2); }
|
||
.rr-gd-auditinfo label { color: var(--ink-3); margin-right: 6px; }
|
||
.rr-gd-audit { display: flex; flex-direction: column; gap: 10px; }
|
||
.rr-gd-auditradio { margin-bottom: 2px; }
|
||
.rr-gd-auditdone { font-size: 12px; color: var(--ink-3); background: var(--bg, #f6f8fb); border-radius: 8px; padding: 10px 12px; line-height: 1.6; }
|
||
</style>
|