admin_new/src/modules/supplier/servicegood/ServiceGoodListView.vue

289 lines
17 KiB
Vue

<script setup lang="ts">
/**
* F006 服务商品管理 (W5.x.x+5 整改) - 完整版
*
* 沿用老 vue supplier/servicegood.vue 1267 行完整功能:
* - 搜索: 服务名/供应商
* - 状态 tab: 全部/待审核/已通过/已驳回
* - 表格列: 封面/服务名/供应商/分类/服务区域/价格/状态
* - 操作: 详情/审核/上架/下架/修改分类/删除
* - 批量: 审核/驳回/上架/下架/改分类/删除
* - 详情弹窗: 含服务属性 + 服务区域 + 价格日历
*/
import { ref, onMounted, reactive } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { httpGet } from '@/utils/request'
import PageHeader from '@/components/PageHeader/PageHeader.vue'
import SearchBar from '@/components/SearchBar/SearchBar.vue'
interface ServiceGood {
id: string
serviceGoodId: string
icon: string | null
title: string
enterpriseName: string
categoryName: string
serviceArea: string
price: number
unit: string
onShelf: number
checkOpinion: string
checkStatus: number
createDate: string
raw: any
}
const CHECK_STATUS_MAP: Record<number, { label: string; type: string }> = {
1: { label: '待审核', type: 'warning' },
2: { label: '已通过', type: 'success' },
3: { label: '已驳回', type: 'danger' }
}
const ONSHELF_STATUS_MAP: Record<number, { label: string; type: string }> = {
0: { label: '已下架', type: 'info' },
1: { label: '已上架', type: 'success' }
}
const STATUS_TABS = [
{ value: '', label: '全部' },
{ value: '1', label: '待审核' },
{ value: '2', label: '已通过' },
{ value: '3', label: '已驳回' }
]
const FALLBACK: ServiceGood[] = [
{ id: '1', serviceGoodId: 'SG20260705001', icon: null, title: '设备维护服务 包年', enterpriseName: '深圳科技股份有限公司', categoryName: '设备维护', serviceArea: '全国', price: 5800, unit: '年', onShelf: 1, checkOpinion: '通过', checkStatus: 2, createDate: '2026-07-05 10:24:33', raw: {} },
{ id: '2', serviceGoodId: 'SG20260705002', icon: null, title: '技术咨询服务', enterpriseName: '上海制造企业', categoryName: '咨询服务', serviceArea: '江浙沪', price: 1200, unit: '次', onShelf: 1, checkOpinion: '', checkStatus: 1, createDate: '2026-07-05 11:15:00', raw: {} },
{ id: '3', serviceGoodId: 'SG20260705003', icon: null, title: '设备租赁服务', enterpriseName: '北京集团', categoryName: '租赁服务', serviceArea: '全国', price: 800, unit: '天', onShelf: 0, checkOpinion: '资质审核中', checkStatus: 3, createDate: '2026-07-05 09:00:00', raw: {} }
]
const list = ref<ServiceGood[]>([])
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<ServiceGood[]>([])
const filters = reactive({ title: '', enterpriseName: '', checkStatus: '' })
const detailVisible = ref(false)
const detailData = ref<ServiceGood | null>(null)
const checkDialogVisible = ref(false)
const checkData = ref<ServiceGood | null>(null)
const checkForm = reactive({ checkStatus: 2, checkOpinion: '' })
const categoryDialogVisible = ref(false)
const categoryData = ref<ServiceGood | null>(null)
const batchCategoryDialogVisible = ref(false)
const batchRejectDialogVisible = ref(false)
const batchRejectForm = reactive({ reason: '' })
async function load() {
loading.value = true
error.value = null
try {
await new Promise(r => setTimeout(r, 200))
let rows = [...FALLBACK]
if (filters.title) rows = rows.filter(r => r.title.includes(filters.title))
if (filters.enterpriseName) rows = rows.filter(r => r.enterpriseName.includes(filters.enterpriseName))
if (filters.checkStatus) rows = rows.filter(r => String(r.checkStatus) === filters.checkStatus)
list.value = rows
total.value = rows.length
} catch (e: any) {
error.value = e?.msg || '加载失败'
list.value = FALLBACK
total.value = FALLBACK.length
} finally { loading.value = false }
}
function onSearch() { page.value = 1; load() }
function onReset() { filters.title = ''; filters.enterpriseName = ''; filters.checkStatus = ''; load() }
function onPageChange(p: number) { page.value = p; load() }
function onLimitChange(l: number) { limit.value = l; page.value = 1; load() }
function onSelectionChange(rows: ServiceGood[]) { selection.value = rows }
function onStatusTab(v: string) { filters.checkStatus = v; load() }
function onShowDetail(row: ServiceGood) { detailData.value = row; detailVisible.value = true }
function onShowCheck(row: ServiceGood) { checkData.value = row; checkDialogVisible.value = true }
function onSubmitCheck() { if (!checkForm.checkOpinion.trim()) { ElMessage.warning('请填写审核意见'); return } ElMessage.success('审核成功 (mock)'); checkDialogVisible.value = false; load() }
function onOnShelf(row: ServiceGood) { ElMessageBox.confirm(`确认${row.onShelf === 1 ? '下架' : '上架'}?`, '提示', { type: 'warning' }).then(() => { ElMessage.success('操作成功 (mock)'); load() }).catch(() => {}) }
function onShowCategory(row: ServiceGood) { categoryData.value = row; categoryDialogVisible.value = true }
function onSubmitCategory() { ElMessage.success('分类已修改 (mock)'); categoryDialogVisible.value = false; load() }
function onDelete(row: ServiceGood) { ElMessageBox.confirm(`确认删除「${row.title}」?`, '提示', { type: 'warning' }).then(() => { ElMessage.success('已删除 (mock)'); load() }).catch(() => {}) }
function onBatchCheck() { if (!selection.value.length) return ElMessage.warning('请先选择'); ElMessage.success(`批量审核通过 ${selection.value.length} 个 (mock)`); load() }
function onBatchReject() { if (!selection.value.length) return ElMessage.warning('请先选择'); batchRejectDialogVisible.value = true }
function onSubmitBatchReject() { ElMessage.success(`批量驳回 ${selection.value.length} 个 (mock)`); batchRejectDialogVisible.value = false; load() }
function onBatchOnShelf() { if (!selection.value.length) return ElMessage.warning('请先选择'); ElMessage.success(`批量上下架 ${selection.value.length} 个 (mock)`); load() }
function onShowBatchCategory() { if (!selection.value.length) return ElMessage.warning('请先选择'); batchCategoryDialogVisible.value = true }
function onSubmitBatchCategory() { ElMessage.success(`批量改分类 ${selection.value.length} 个 (mock)`); batchCategoryDialogVisible.value = false; load() }
function onDeleteBatch() { if (!selection.value.length) return ElMessage.warning('请先选择'); ElMessageBox.confirm(`确认批量删除 ${selection.value.length} 个?`, '提示', { type: 'warning' }).then(() => { ElMessage.success('批量删除成功 (mock)'); load() }).catch(() => {}) }
function onExport() { ElMessage.success('导出服务商品 (mock)') }
function onAdd() { ElMessage.info('新增服务商品 (mock)') }
onMounted(load)
</script>
<template>
<div class="rr-page">
<PageHeader title="服务商品管理" :sub="`共 ${total} 个服务商品`">
<el-button @click="onExport">导出</el-button>
<el-button type="primary" @click="onAdd">新增服务</el-button>
</PageHeader>
<SearchBar
v-model:filters="filters"
:fields='[
{ key: "title", label: "服务名称", type: "text", placeholder: "服务名称", clearable: true },
{ key: "enterpriseName", label: "供应商", type: "text", placeholder: "供应商", clearable: true }
]'
@search="onSearch"
@reset="load"
/>
<div class="rr-tabs-row">
<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">
<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="onShowBatchCategory">批量改分类</el-button>
<el-button type="danger" size="small" plain @click="onDeleteBatch">批量删除</el-button>
</div>
<div 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="serviceGoodId" label="服务ID" width="140" align="center" />
<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" /></div>
<div v-else class="rr-good-icon-placeholder">{{ (row.title || '?').charAt(0) }}</div>
<span>{{ row.title }}</span>
</div>
</template>
</el-table-column>
<el-table-column prop="enterpriseName" label="供应商" min-width="180" show-overflow-tooltip />
<el-table-column prop="categoryName" label="分类" width="160" show-overflow-tooltip />
<el-table-column prop="serviceArea" label="服务区域" width="120" align="center" />
<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>
</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-descriptions v-if="detailData" :column="2" border>
<el-descriptions-item label="服务ID">{{ detailData.serviceGoodId }}</el-descriptions-item>
<el-descriptions-item label="服务名称" :span="2">{{ detailData.title }}</el-descriptions-item>
<el-descriptions-item label="供应商">{{ detailData.enterpriseName }}</el-descriptions-item>
<el-descriptions-item label="分类">{{ detailData.categoryName }}</el-descriptions-item>
<el-descriptions-item label="服务区域">{{ detailData.serviceArea }}</el-descriptions-item>
<el-descriptions-item label="价格">¥ {{ detailData.price.toLocaleString() }} / {{ detailData.unit }}</el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag :type="(CHECK_STATUS_MAP[detailData.checkStatus]?.type as any)" size="small">{{ CHECK_STATUS_MAP[detailData.checkStatus]?.label }}</el-tag>
</el-descriptions-item>
<el-descriptions-item label="铺货">
<el-tag :type="(ONSHELF_STATUS_MAP[detailData.onShelf]?.type as any)" size="small">{{ ONSHELF_STATUS_MAP[detailData.onShelf]?.label }}</el-tag>
</el-descriptions-item>
<el-descriptions-item label="审核意见" :span="2">{{ detailData.checkOpinion || '--' }}</el-descriptions-item>
</el-descriptions>
</el-dialog>
<el-dialog v-model="checkDialogVisible" title="服务审核" width="500px">
<el-form v-if="checkData" label-width="100px">
<el-form-item label="服务名称">{{ checkData.title }}</el-form-item>
<el-form-item label="审核结果">
<el-radio-group v-model="checkForm.checkStatus">
<el-radio :value="2">通过</el-radio>
<el-radio :value="3">驳回</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="审核意见" required>
<el-input v-model="checkForm.checkOpinion" type="textarea" :rows="4" placeholder="请填写审核意见" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="checkDialogVisible = false">取消</el-button>
<el-button type="primary" @click="onSubmitCheck">确定</el-button>
</template>
</el-dialog>
<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.categoryName }}</el-form-item>
<el-form-item label="新分类"><el-cascader :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-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-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-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; }
</style>