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:
parent
296ba6a966
commit
9664b3d954
@ -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>
|
|
||||||
<h2 class="rr-page-title">商品管理</h2>
|
|
||||||
<p class="rr-page-sub">共 {{ total }} 个商品</p>
|
|
||||||
</div>
|
|
||||||
<div class="rr-page-actions">
|
|
||||||
<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>
|
||||||
</div>
|
</PageHeader>
|
||||||
</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>
|
||||||
|
|||||||
@ -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"
|
||||||
|
: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 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-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>
|
<div v-else class="rr-good-icon-placeholder">{{ (row.title || '?').charAt(0) }}</div>
|
||||||
<span>{{ row.title }}</span>
|
<span>{{ row.title }}</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
<template #price="{ row }">
|
||||||
<el-table-column prop="enterpriseName" label="供应商" min-width="180" show-overflow-tooltip />
|
<span class="rr-price">¥ {{ Number(row.price || 0).toLocaleString() }} / {{ row.unit }}</span>
|
||||||
<el-table-column prop="categoryName" label="分类" width="160" show-overflow-tooltip />
|
</template>
|
||||||
<el-table-column prop="serviceArea" label="服务区域" width="120" align="center" />
|
<template #onShelf="{ row }">
|
||||||
<el-table-column label="价格" width="140" align="right">
|
|
||||||
<template #default="{ row }">¥ {{ row.price.toLocaleString() }} / {{ row.unit }}</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="铺货" width="100" align="center">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<el-tag :type="(ONSHELF_STATUS_MAP[row.onShelf]?.type as any)" size="small" effect="light">{{ ONSHELF_STATUS_MAP[row.onShelf]?.label }}</el-tag>
|
<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>
|
||||||
</el-table-column>
|
<template #checkStatus="{ row }">
|
||||||
<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>
|
<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>
|
||||||
</el-table-column>
|
<template #action="{ row }">
|
||||||
<el-table-column label="申请时间" prop="createDate" width="170" />
|
<span class="rr-actions">
|
||||||
<el-table-column label="操作" width="280" align="center" fixed="right">
|
<el-link type="primary" :underline="false" @click="onShowDetail(row)">详情</el-link>
|
||||||
<template #default="{ row }">
|
<template v-if="row.checkStatus === 1">
|
||||||
<el-button text type="primary" size="small" @click="onShowDetail(row)">详情</el-button>
|
<span class="rr-actions__sep">|</span>
|
||||||
<el-button v-if="row.checkStatus === 1" text type="primary" size="small" @click="onShowCheck(row)">审核</el-button>
|
<el-link type="primary" :underline="false" @click="onShowCheck(row)">审核</el-link>
|
||||||
<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>
|
</template>
|
||||||
</el-table-column>
|
<span class="rr-actions__sep">|</span>
|
||||||
</el-table>
|
<el-link :type="row.onShelf === 1 ? 'warning' : 'success'" :underline="false" @click="onOnShelf(row)">{{ row.onShelf === 1 ? '下架' : '上架' }}</el-link>
|
||||||
<div class="rr-pagination">
|
<span class="rr-actions__sep">|</span>
|
||||||
<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" />
|
<el-link type="primary" :underline="false" @click="onShowCategory(row)">改分类</el-link>
|
||||||
</div>
|
<span class="rr-actions__sep">|</span>
|
||||||
</div>
|
<el-link type="danger" :underline="false" @click="onDelete(row)">删除</el-link>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</DataTable>
|
||||||
|
|
||||||
<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>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user