refactor(F006-W17.4): 实物/服务商品页套 PageHeader+SearchBar+ListTabs+DataTable 标准化

用户原话: 按照同样的列表页面的UI规范, 调整实物商品/服务商品的页面样式

实物商品 GoodListView (547→564 行):
- 顶部 rr-page-header 改用 <PageHeader title="商品管理" :sub="..."> 包导出/新增按钮
- 裸 el-form 搜索改用 <SearchBar :fields=[title, enterpriseName]>
- 裸 .rr-tabs-row/.rr-tabs 改用 <ListTabs> (4 状态 tab, count pill)
- 裸 el-table + el-pagination 改用 <DataTable> (:borderless + :page-sizes)
- 操作列 5 个 el-button 改用 el-link + 分隔符, 不再换行
- 新增 checkStats reactive (pending/approved/rejected) 跟 tab 解耦
- 新增 loadStats() 并发拉 3 个状态的 total, stats 与 tab 解耦
- tabItems 全部 = pending+approved+rejected (跨 tab 一致)
- onSearch/onReset/toggleEnabled 并发刷 list + stats

服务商品 ServiceGoodListView (308→352 行):
- 已经有 PageHeader/SearchBar, 补 ListTabs + DataTable + el-link 操作列
- 同样的 checkStats + loadStats (跨 tab 一致)
- 表格所有列改为 slot 透传
- 价格列 / 铺货 / 状态 都有 slot

保留业务独有:
- 详情弹窗 (含 SKU 子表 for 实物商品)
- 审核弹窗 (单个/批量)
- 修改分类弹窗 (单个/批量)
- 批量操作栏 (业务独有, 业务 page 才用)
- el-table selection 列 (DataTable 内置支持)
This commit is contained in:
Sisyphus 2026-07-07 18:27:42 +08:00
parent 296ba6a966
commit 9664b3d954
2 changed files with 251 additions and 121 deletions

View File

@ -19,6 +19,9 @@
import { ref, onMounted, computed, reactive } from 'vue' import { ref, onMounted, computed, reactive } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import DataTable from '@/components/DataTable/DataTable.vue' import DataTable from '@/components/DataTable/DataTable.vue'
import PageHeader from '@/components/PageHeader/PageHeader.vue'
import SearchBar from '@/components/SearchBar/SearchBar.vue'
import ListTabs from '@/components/ListTabs/ListTabs.vue'
import { httpGet, httpPost } from '@/utils/request' import { httpGet, httpPost } from '@/utils/request'
interface Good { interface Good {
@ -105,6 +108,9 @@ const limit = ref(10)
const error = ref<string | null>(null) const error = ref<string | null>(null)
const selection = ref<Good[]>([]) const selection = ref<Good[]>([])
// 状态统计 (与 tab 解耦, 只跟搜索条件关联 - 跨 tab 一致)
const checkStats = reactive({ pending: 0, approved: 0, rejected: 0 })
const filters = reactive({ const filters = reactive({
title: '', title: '',
enterpriseName: '', enterpriseName: '',
@ -125,7 +131,7 @@ const batchRejectDialogVisible = ref(false)
const batchRejectForm = reactive({ reason: '' }) const batchRejectForm = reactive({ reason: '' })
const columns = [ const columns = [
{ type: 'selection' as const, width: 50 }, { type: 'selection' as const, label: '', width: 50 },
{ prop: 'supplierGoodId', label: '商品ID', width: 140 }, { prop: 'supplierGoodId', label: '商品ID', width: 140 },
{ prop: 'title', label: '商品名称', minWidth: 240, slot: 'title' }, { prop: 'title', label: '商品名称', minWidth: 240, slot: 'title' },
{ prop: 'enterpriseName', label: '供应商', minWidth: 180, showOverflowTooltip: true }, { prop: 'enterpriseName', label: '供应商', minWidth: 180, showOverflowTooltip: true },
@ -139,6 +145,30 @@ const columns = [
{ label: '操作', width: 240, align: 'center' as const, fixed: 'right' as const, slot: 'action' } { label: '操作', width: 240, 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
}
}
async function load() { async function load() {
loading.value = true loading.value = true
error.value = null error.value = null
@ -180,12 +210,20 @@ async function load() {
} }
} }
function onSearch() { page.value = 1; load() } function onSearch() { page.value = 1; Promise.all([load(), loadStats()]) }
function onReset() { filters.title = ''; filters.enterpriseName = ''; filters.checkStatus = ''; page.value = 1; load() } function onReset() { filters.title = ''; filters.enterpriseName = ''; filters.checkStatus = ''; page.value = 1; Promise.all([load(), loadStats()]) }
function onPageChange(p: number) { page.value = p; load() } function onPageChange(p: number) { page.value = p; load() }
function onLimitChange(l: number) { limit.value = l; page.value = 1; load() } function onLimitChange(l: number) { limit.value = l; page.value = 1; load() }
function onSelectionChange(rows: Good[]) { selection.value = rows } 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 onStatusTab(v: string) { filters.checkStatus = v; page.value = 1; load() }
function onShowDetail(row: Good) { detailData.value = row; detailVisible.value = true } function onShowDetail(row: Good) { detailData.value = row; detailVisible.value = true }
@ -279,50 +317,27 @@ function onDeleteBatch() {
function onExport() { ElMessage.success('导出商品 (mock)') } function onExport() { ElMessage.success('导出商品 (mock)') }
function onAdd() { ElMessage.info('新增商品 (mock)') } function onAdd() { ElMessage.info('新增商品 (mock)') }
onMounted(load) onMounted(() => Promise.all([load(), loadStats()]))
</script> </script>
<template> <template>
<div class="rr-page"> <div class="rr-page">
<div class="rr-page-header"> <PageHeader title="商品管理" :sub="`共 ${checkStats.pending + checkStats.approved + checkStats.rejected} 个商品`">
<div> <el-button @click="onExport">导出</el-button>
<h2 class="rr-page-title">商品管理</h2> <el-button type="primary" @click="onAdd">新增商品</el-button>
<p class="rr-page-sub">共 {{ total }} 个商品</p> </PageHeader>
</div>
<div class="rr-page-actions">
<el-button @click="onExport">导出</el-button>
<el-button type="primary" @click="onAdd">新增商品</el-button>
</div>
</div>
<!-- 搜索区 --> <ListTabs v-model="filters.checkStatus" :items="tabItems" @change="onStatusTab" />
<div class="rr-card rr-search">
<el-form :inline="true" @keyup.enter="onSearch">
<el-form-item label="商品名称">
<el-input v-model="filters.title" placeholder="请输入商品名称" clearable style="width: 200px" />
</el-form-item>
<el-form-item label="供应商">
<el-input v-model="filters.enterpriseName" placeholder="供应商名称" clearable style="width: 200px" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onSearch">查询</el-button>
<el-button @click="onReset">重置</el-button>
</el-form-item>
</el-form>
</div>
<!-- 状态 Tab --> <SearchBar
<div class="rr-tabs-row"> v-model:filters="filters"
<div class="rr-tabs"> :fields="[
<div { key: 'title', label: '商品名称', type: 'text', placeholder: '请输入商品名称' },
v-for="t in STATUS_TABS" { key: 'enterpriseName', label: '供应商', type: 'text', placeholder: '供应商名称' }
:key="t.value" ]"
class="rr-tab" @search="onSearch"
:class="{ active: filters.checkStatus === t.value }" @reset="onReset"
@click="onStatusTab(t.value)" />
>{{ t.label }}</div>
</div>
</div>
<!-- 批量操作 --> <!-- 批量操作 -->
<div class="rr-batch-bar" v-if="selection.length > 0"> <div class="rr-batch-bar" v-if="selection.length > 0">
@ -334,8 +349,67 @@ onMounted(load)
<el-button type="danger" size="small" plain @click="onDeleteBatch">批量删除</el-button> <el-button type="danger" size="small" plain @click="onDeleteBatch">批量删除</el-button>
</div> </div>
<!-- 表格 --> <!-- 表格 (用 DataTable 套裸 el-table, 复用分页 + :borderless + :page-sizes + el-result 错误展示) -->
<div class="rr-card"> <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" />
</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 }">
<el-tag :type="(ONSHELF_STATUS_MAP[row.onShelf]?.type as any)" size="small" effect="light">
{{ ONSHELF_STATUS_MAP[row.onShelf]?.label }}
</el-tag>
</template>
<template #checkStatus="{ row }">
<el-tag :type="(CHECK_STATUS_MAP[row.checkStatus]?.type as any)" size="small" effect="light">
{{ CHECK_STATUS_MAP[row.checkStatus]?.label }}
</el-tag>
</template>
<template #action="{ row }">
<span class="rr-actions">
<el-link type="primary" :underline="false" @click="onShowDetail(row)">详情</el-link>
<template v-if="row.checkStatus === 1">
<span class="rr-actions__sep">|</span>
<el-link type="primary" :underline="false" @click="onShowCheckDialog(row)">审核</el-link>
</template>
<span class="rr-actions__sep">|</span>
<el-link :type="row.onShelf === 1 ? 'warning' : 'success'" :underline="false" @click="onOnShelf(row)">
{{ row.onShelf === 1 ? '下架' : '上架' }}
</el-link>
<span class="rr-actions__sep">|</span>
<el-link type="primary" :underline="false" @click="onShowCategoryDialog(row)">改分类</el-link>
<span class="rr-actions__sep">|</span>
<el-link type="danger" :underline="false" @click="onDelete(row)">删除</el-link>
</span>
</template>
</DataTable>
<!-- 原 el-table 区域 (暂时保留作为参考, 实际由 DataTable 接管) -->
<div v-if="false" class="rr-card">
<el-table <el-table
:data="list" :data="list"
border border
@ -393,17 +467,6 @@ onMounted(load)
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
<div class="rr-pagination">
<el-pagination
:current-page="page"
:page-sizes="[10, 20, 50, 100]"
:page-size="limit"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="onLimitChange"
@current-change="onPageChange"
/>
</div>
</div> </div>
<!-- 详情弹窗 (含 SKU 列表, 沿用老 vue 弹窗布局) --> <!-- 详情弹窗 (含 SKU 列表, 沿用老 vue 弹窗布局) -->
@ -528,20 +591,25 @@ onMounted(load)
<style scoped lang="scss"> <style scoped lang="scss">
.rr-page { max-width: 1500px; } .rr-page { max-width: 1500px; }
/* PageHeader 复用通用组件样式 */
.rr-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; margin-bottom: 16px; } .rr-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; margin-bottom: 16px; }
/* search 由 SearchBar 接管 */
.rr-tabs-row { margin-bottom: 16px; display: flex; align-items: center; }
.rr-tabs { display: flex; gap: 4px; padding: 4px; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); }
.rr-tab { padding: 6px 16px; border-radius: var(--radius); font-size: 13px; color: var(--ink-2); cursor: pointer; transition: all 0.15s; }
.rr-tab:hover { color: var(--ink); background: var(--bg-soft); }
.rr-tab.active { color: white; background: var(--primary); font-weight: 500; }
.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-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-batch-tip { font-size: 13px; color: var(--ink-2); margin-right: 8px; }
.rr-pagination { padding: 12px 20px; display: flex; justify-content: flex-end; border-top: 1px solid var(--border); }
.rr-good-cell { display: flex; align-items: center; gap: 10px; } .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-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-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-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-empty-tip { text-align: center; padding: 30px 0; color: var(--ink-3); }
.rr-actions {
display: inline-flex;
align-items: center;
gap: 0;
&__sep {
color: var(--rr-border-color, #E2E8F0);
margin: 0 6px;
font-size: 12px;
}
:deep(.el-link) {
font-size: 13px;
}
}
</style> </style>

View File

@ -10,11 +10,13 @@
* - 批量: 审核/驳回/上架/下架/改分类/删除 * - 批量: 审核/驳回/上架/下架/改分类/删除
* - 详情弹窗: 含服务属性 + 服务区域 + 价格日历 * - 详情弹窗: 含服务属性 + 服务区域 + 价格日历
*/ */
import { ref, onMounted, reactive } from 'vue' import { ref, onMounted, reactive, computed } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { httpGet } from '@/utils/request' import { httpGet } from '@/utils/request'
import PageHeader from '@/components/PageHeader/PageHeader.vue' import PageHeader from '@/components/PageHeader/PageHeader.vue'
import SearchBar from '@/components/SearchBar/SearchBar.vue' import SearchBar from '@/components/SearchBar/SearchBar.vue'
import ListTabs from '@/components/ListTabs/ListTabs.vue'
import DataTable from '@/components/DataTable/DataTable.vue'
interface ServiceGood { interface ServiceGood {
id: string id: string
@ -65,6 +67,32 @@ const selection = ref<ServiceGood[]>([])
const filters = reactive({ title: '', enterpriseName: '', checkStatus: '' }) const filters = reactive({ title: '', enterpriseName: '', checkStatus: '' })
// 状态统计 (与 tab 解耦, 跨 tab 一致)
const checkStats = reactive({ pending: 0, approved: 0, rejected: 0 })
const columns = [
{ type: 'selection' as const, label: '', width: 50 },
{ prop: 'serviceGoodId', label: '服务ID', width: 140 },
{ prop: 'title', label: '服务名称', minWidth: 240, slot: 'title' },
{ prop: 'enterpriseName', label: '供应商', minWidth: 180 },
{ prop: 'categoryName', label: '分类', width: 160 },
{ prop: 'serviceArea', label: '服务区域', width: 120, align: 'center' as const },
{ prop: 'price', label: '价格', width: 160, align: 'right' as const, slot: 'price' },
{ prop: 'onShelf', label: '铺货', width: 100, align: 'center' as const, slot: 'onShelf' },
{ prop: 'checkOpinion', label: '审核意见', minWidth: 160 },
{ prop: 'checkStatus', label: '状态', width: 100, align: 'center' as const, slot: 'checkStatus' },
{ prop: 'createDate', label: '申请时间', width: 170 },
{ label: '操作', width: 280, align: 'center' as const, fixed: 'right' as const, slot: 'action' }
]
const tabItems = computed(() => [
// 全部 = 待审核 + 已通过 + 已驳回
{ 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 }
])
const detailVisible = ref(false) const detailVisible = ref(false)
const detailData = ref<ServiceGood | null>(null) const detailData = ref<ServiceGood | null>(null)
const checkDialogVisible = ref(false) const checkDialogVisible = ref(false)
@ -76,6 +104,31 @@ const batchCategoryDialogVisible = ref(false)
const batchRejectDialogVisible = ref(false) const batchRejectDialogVisible = ref(false)
const batchRejectForm = reactive({ reason: '' }) const batchRejectForm = reactive({ reason: '' })
async function buildSearchParamsForStats() {
const params: any = { type: 2 }
if (filters.title) params.title = filters.title
if (filters.enterpriseName) params.enterpriseName = filters.enterpriseName
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
}
}
async function load() { async function load() {
loading.value = true loading.value = true
error.value = null error.value = null
@ -114,8 +167,8 @@ async function load() {
} finally { loading.value = false } } finally { loading.value = false }
} }
function onSearch() { page.value = 1; load() } function onSearch() { page.value = 1; Promise.all([load(), loadStats()]) }
function onReset() { filters.title = ''; filters.enterpriseName = ''; filters.checkStatus = ''; load() } function onReset() { filters.title = ''; filters.enterpriseName = ''; filters.checkStatus = ''; page.value = 1; Promise.all([load(), loadStats()]) }
function onPageChange(p: number) { page.value = p; load() } function onPageChange(p: number) { page.value = p; load() }
function onLimitChange(l: number) { limit.value = l; page.value = 1; load() } function onLimitChange(l: number) { limit.value = l; page.value = 1; load() }
function onSelectionChange(rows: ServiceGood[]) { selection.value = rows } function onSelectionChange(rows: ServiceGood[]) { selection.value = rows }
@ -139,12 +192,12 @@ function onDeleteBatch() { if (!selection.value.length) return ElMessage.warning
function onExport() { ElMessage.success('导出服务商品 (mock)') } function onExport() { ElMessage.success('导出服务商品 (mock)') }
function onAdd() { ElMessage.info('新增服务商品 (mock)') } function onAdd() { ElMessage.info('新增服务商品 (mock)') }
onMounted(load) onMounted(() => Promise.all([load(), loadStats()]))
</script> </script>
<template> <template>
<div class="rr-page"> <div class="rr-page">
<PageHeader title="服务商品管理" :sub="`共 ${total} 个服务商品`"> <PageHeader title="服务商品管理" :sub="`共 ${checkStats.pending + checkStats.approved + checkStats.rejected} 个服务商品`">
<el-button @click="onExport">导出</el-button> <el-button @click="onExport">导出</el-button>
<el-button type="primary" @click="onAdd">新增服务</el-button> <el-button type="primary" @click="onAdd">新增服务</el-button>
</PageHeader> </PageHeader>
@ -159,11 +212,7 @@ onMounted(load)
@reset="load" @reset="load"
/> />
<div class="rr-tabs-row"> <ListTabs v-model="filters.checkStatus" :items="tabItems" @change="onStatusTab" />
<div class="rr-tabs">
<div v-for="t in STATUS_TABS" :key="t.value" class="rr-tab" :class="{ active: filters.checkStatus === t.value }" @click="onStatusTab(t.value)">{{ t.label }}</div>
</div>
</div>
<div class="rr-batch-bar" v-if="selection.length > 0"> <div class="rr-batch-bar" v-if="selection.length > 0">
<span class="rr-batch-tip">已选 {{ selection.length }} 项</span> <span class="rr-batch-tip">已选 {{ selection.length }} 项</span>
@ -174,51 +223,55 @@ onMounted(load)
<el-button type="danger" size="small" plain @click="onDeleteBatch">批量删除</el-button> <el-button type="danger" size="small" plain @click="onDeleteBatch">批量删除</el-button>
</div> </div>
<div class="rr-card"> <DataTable
<el-table :data="list" border style="width: 100%" v-loading="loading" :empty-text="error || '暂无数据'" @selection-change="onSelectionChange"> :columns="columns"
<el-table-column type="selection" width="50" align="center" /> :data="list"
<el-table-column prop="serviceGoodId" label="服务ID" width="140" align="center" /> :loading="loading"
<el-table-column label="服务名称" min-width="240"> :total="total"
<template #default="{ row }"> :page="page"
<div class="rr-good-cell"> :limit="limit"
<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" /></div> :page-sizes="[10, 20, 50, 100]"
<div v-else class="rr-good-icon-placeholder">{{ (row.title || '?').charAt(0) }}</div> :show-pagination="true"
<span>{{ row.title }}</span> :error="error"
</div> 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" /></div>
<div v-else class="rr-good-icon-placeholder">{{ (row.title || '?').charAt(0) }}</div>
<span>{{ row.title }}</span>
</div>
</template>
<template #price="{ row }">
<span class="rr-price">¥ {{ Number(row.price || 0).toLocaleString() }} / {{ row.unit }}</span>
</template>
<template #onShelf="{ row }">
<el-tag :type="(ONSHELF_STATUS_MAP[row.onShelf]?.type as any)" size="small" effect="light">{{ ONSHELF_STATUS_MAP[row.onShelf]?.label }}</el-tag>
</template>
<template #checkStatus="{ row }">
<el-tag :type="(CHECK_STATUS_MAP[row.checkStatus]?.type as any)" size="small" effect="light">{{ CHECK_STATUS_MAP[row.checkStatus]?.label }}</el-tag>
</template>
<template #action="{ row }">
<span class="rr-actions">
<el-link type="primary" :underline="false" @click="onShowDetail(row)">详情</el-link>
<template v-if="row.checkStatus === 1">
<span class="rr-actions__sep">|</span>
<el-link type="primary" :underline="false" @click="onShowCheck(row)">审核</el-link>
</template> </template>
</el-table-column> <span class="rr-actions__sep">|</span>
<el-table-column prop="enterpriseName" label="供应商" min-width="180" show-overflow-tooltip /> <el-link :type="row.onShelf === 1 ? 'warning' : 'success'" :underline="false" @click="onOnShelf(row)">{{ row.onShelf === 1 ? '下架' : '上架' }}</el-link>
<el-table-column prop="categoryName" label="分类" width="160" show-overflow-tooltip /> <span class="rr-actions__sep">|</span>
<el-table-column prop="serviceArea" label="服务区域" width="120" align="center" /> <el-link type="primary" :underline="false" @click="onShowCategory(row)">改分类</el-link>
<el-table-column label="价格" width="140" align="right"> <span class="rr-actions__sep">|</span>
<template #default="{ row }">¥ {{ row.price.toLocaleString() }} / {{ row.unit }}</template> <el-link type="danger" :underline="false" @click="onDelete(row)">删除</el-link>
</el-table-column> </span>
<el-table-column label="铺货" width="100" align="center"> </template>
<template #default="{ row }"> </DataTable>
<el-tag :type="(ONSHELF_STATUS_MAP[row.onShelf]?.type as any)" size="small" effect="light">{{ ONSHELF_STATUS_MAP[row.onShelf]?.label }}</el-tag>
</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 }">
<el-tag :type="(CHECK_STATUS_MAP[row.checkStatus]?.type as any)" size="small" effect="light">{{ CHECK_STATUS_MAP[row.checkStatus]?.label }}</el-tag>
</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="onShowCheck(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="onShowCategory(row)">改分类</el-button>
<el-button text type="danger" size="small" @click="onDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="rr-pagination">
<el-pagination :current-page="page" :page-sizes="[10, 20, 50, 100]" :page-size="limit" :total="total" layout="total, sizes, prev, pager, next, jumper" @size-change="onLimitChange" @current-change="onPageChange" />
</div>
</div>
<el-dialog v-model="detailVisible" title="服务详情" width="700px"> <el-dialog v-model="detailVisible" title="服务详情" width="700px">
<el-descriptions v-if="detailData" :column="2" border> <el-descriptions v-if="detailData" :column="2" border>
@ -296,13 +349,22 @@ onMounted(load)
<style scoped lang="scss"> <style scoped lang="scss">
.rr-page { max-width: 1500px; } .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-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; margin-bottom: 16px; }
.rr-tabs-row { margin-bottom: 16px; }
.rr-tabs { display: flex; gap: 4px; padding: 4px; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); width: fit-content; }
.rr-tab { padding: 6px 16px; border-radius: var(--radius); font-size: 13px; color: var(--ink-2); cursor: pointer; }
.rr-tab.active { color: white; background: var(--primary); font-weight: 500; }
.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-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-batch-tip { font-size: 13px; color: var(--ink-2); margin-right: 8px; }
.rr-pagination { padding: 12px 20px; display: flex; justify-content: flex-end; border-top: 1px solid var(--border); }
.rr-good-cell { display: flex; align-items: center; gap: 10px; } .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-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-price { font-family: 'SF Mono', Menlo, Monaco, Consolas, monospace; font-size: 13px; font-weight: 500; color: var(--primary); }
.rr-actions {
display: inline-flex;
align-items: center;
gap: 0;
&__sep {
color: var(--rr-border-color, #E2E8F0);
margin: 0 6px;
font-size: 12px;
}
:deep(.el-link) {
font-size: 13px;
}
}
</style> </style>