<script setup lang="ts">
|
import { computed, onMounted, reactive, ref } from 'vue'
|
import { useRouter } from 'vue-router'
|
import { Edit, Goods, Plus, Refresh, Search } from '@element-plus/icons-vue'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
import type { FormInstance, FormRules } from 'element-plus'
|
import { getAdminMerchants, updateMerchant, updateMerchantStatus } from '../api/admin'
|
import type { Merchant, MerchantPayload } from '../types/admin'
|
|
const router = useRouter()
|
const loading = ref(false)
|
const saving = ref(false)
|
const formRef = ref<FormInstance>()
|
const typeDialogVisible = ref(false)
|
const drawerVisible = ref(false)
|
const editingId = ref<number | string>()
|
const merchants = ref<Merchant[]>([])
|
const selectedCreateType = ref('餐饮服务')
|
|
const createMerchantTypes = [
|
'免费体检',
|
'配偶推荐就业',
|
'金融服务',
|
'行政审批',
|
'子女托管',
|
'餐饮服务',
|
'酒店服务',
|
'公寓租赁',
|
'加油服务',
|
'停车服务',
|
'自助洗车',
|
'景区服务',
|
'购房优惠',
|
'购车服务',
|
'家电服务',
|
'数码服务',
|
'文化服务',
|
'电信服务',
|
'口腔治疗保健',
|
'观影服务',
|
]
|
const merchantTypes = [
|
'免费体检',
|
'配偶推荐就业',
|
'金融服务',
|
'行政审批',
|
'子女托管',
|
'餐饮服务',
|
'酒店服务',
|
'公寓租赁',
|
'加油服务',
|
'停车服务',
|
'自助洗车',
|
'景区服务',
|
'购房优惠',
|
'购车服务',
|
'家电服务',
|
'数码服务',
|
'文化服务',
|
'电信服务',
|
'口腔治疗保健',
|
'观影服务',
|
]
|
|
const filters = reactive({
|
keyword: '',
|
contactMobile: '',
|
type: '',
|
status: undefined as number | undefined,
|
recommend: undefined as number | undefined,
|
createdTime: [] as string[],
|
})
|
|
const pagination = reactive({
|
page: 1,
|
size: 10,
|
total: 0,
|
})
|
|
const emptyForm = (): MerchantPayload => ({
|
merchantName: '',
|
merchantType: '',
|
contactName: '',
|
contactMobile: '',
|
address: '',
|
logoUrl: '',
|
coverUrl: '',
|
businessStatus: '',
|
recommend: 0,
|
sortNo: 0,
|
status: 1,
|
})
|
|
const form = reactive<MerchantPayload>(emptyForm())
|
const rules: FormRules = {
|
merchantName: [{ required: true, message: '请输入商家名称', trigger: 'blur' }],
|
}
|
const drawerTitle = computed(() => (editingId.value ? '编辑商家' : '新建商家'))
|
|
const merchantTypeAliases: Record<string, string> = {
|
'餐饮服务': 'food', food: 'food',
|
'景区服务': 'scenic', scenic: 'scenic',
|
'酒店服务': 'hotel', hotel: 'hotel',
|
'公寓租赁': 'apartment', apartment: 'apartment',
|
'自助洗车': 'carWash', '自助洗车服务': 'carWash', carWash: 'carWash',
|
'免费体检': 'medical', medical: 'medical', healthCheck: 'medical',
|
'停车服务': 'parking', parking: 'parking',
|
'加油服务': 'fuel', fuel: 'fuel',
|
'子女托管': 'children', children: 'children',
|
'学生托付': 'children', '学生托管': 'children', '子女托管服务': 'children',
|
'金融服务': 'finance', finance: 'finance',
|
'配偶推荐就业': 'employment', employment: 'employment',
|
'行政审批': 'approval', approval: 'approval',
|
'观影服务': 'cinema', cinema: 'cinema', theatre: 'cinema', '电影院': 'cinema', '影院': 'cinema', '影城': 'cinema', '剧院': 'cinema',
|
'购房优惠': 'housing', housing: 'housing',
|
'购车服务': 'car', car: 'car',
|
'家电服务': 'appliance', appliance: 'appliance',
|
'数码服务': 'digital', digital: 'digital',
|
'文化服务': 'culture', culture: 'culture', book: 'culture',
|
'电信服务': 'telecom', telecom: 'telecom',
|
'口腔治疗保健': 'dental', dental: 'dental',
|
}
|
const productTabTypeCodes = new Set(['food', 'scenic', 'hotel', 'dental', 'medical'])
|
|
const merchantTypeDisplayNames: Record<string, string> = {
|
food: '餐饮服务',
|
scenic: '景区服务',
|
hotel: '酒店服务',
|
apartment: '公寓租赁',
|
carWash: '自助洗车',
|
medical: '免费体检',
|
parking: '停车服务',
|
fuel: '加油服务',
|
children: '子女托管',
|
finance: '金融服务',
|
employment: '配偶推荐就业',
|
approval: '行政审批',
|
cinema: '观影服务',
|
housing: '购房优惠',
|
car: '购车服务',
|
appliance: '家电服务',
|
digital: '数码服务',
|
culture: '文化服务',
|
telecom: '电信服务',
|
dental: '口腔治疗保健',
|
}
|
|
function resolveMerchantTypeCode(row: Merchant) {
|
const raw = String(row.merchantTypeCode || row.merchantType || '').trim()
|
return merchantTypeAliases[raw] || raw
|
}
|
|
function displayMerchantType(row: Merchant) {
|
const code = resolveMerchantTypeCode(row)
|
return merchantTypeDisplayNames[code] || displayDash(row.merchantType)
|
}
|
|
function parseMerchantRights(config: Merchant['merchantRightsConfig']) {
|
if (Array.isArray(config)) return config
|
if (typeof config !== 'string' || !config.trim()) return []
|
try {
|
const parsed = JSON.parse(config)
|
return Array.isArray(parsed) ? parsed : []
|
} catch {
|
return []
|
}
|
}
|
|
function isCarWashMiniProgramJump(row: Merchant) {
|
const linkType = String(row.carWashExternalLinkType || '').trim()
|
if (linkType === 'miniProgram') return true
|
if (linkType === 'normal') return false
|
const jump = parseMerchantRights(row.merchantRightsConfig).find((entry) => (
|
entry?.externalLinkType
|
|| entry?.externalMiniProgramShortLink
|
|| entry?.externalMiniProgramPath
|
|| entry?.externalWebUrl
|
))
|
if (!jump) return false
|
return jump.externalLinkType === 'miniProgram' || Boolean(String(jump.externalMiniProgramShortLink || '').trim())
|
}
|
|
function supportsGroupBuying(row: Merchant) {
|
const code = resolveMerchantTypeCode(row)
|
if (code === 'carWash') return !isCarWashMiniProgramJump(row)
|
return productTabTypeCodes.has(code)
|
}
|
|
function resetForm() {
|
Object.assign(form, emptyForm())
|
editingId.value = undefined
|
}
|
|
function statusText(status?: number) {
|
return status === 0 ? '禁用' : '正常'
|
}
|
|
function recommendText(recommend?: number) {
|
return recommend === 1 ? '是' : '-'
|
}
|
|
function displayDash(value?: string | number) {
|
if (value === undefined || value === null || value === '') return '-'
|
return value
|
}
|
|
function formatDisplayTime(value?: string) {
|
return value ? value.replace('T', ' ') : '-'
|
}
|
|
async function loadData() {
|
loading.value = true
|
try {
|
const result = await getAdminMerchants({
|
page: pagination.page,
|
size: pagination.size,
|
keyword: filters.keyword || undefined,
|
contactMobile: filters.contactMobile || undefined,
|
type: filters.type || undefined,
|
status: filters.status,
|
recommend: filters.recommend,
|
createdStart: filters.createdTime?.[0],
|
createdEnd: filters.createdTime?.[1],
|
})
|
merchants.value = result.records
|
pagination.total = result.total
|
} catch {
|
merchants.value = []
|
pagination.total = 0
|
} finally {
|
loading.value = false
|
}
|
}
|
|
function search() {
|
pagination.page = 1
|
loadData()
|
}
|
|
function resetFilters() {
|
filters.keyword = ''
|
filters.contactMobile = ''
|
filters.type = ''
|
filters.status = undefined
|
filters.recommend = undefined
|
filters.createdTime = []
|
search()
|
}
|
|
function openCreate() {
|
selectedCreateType.value = createMerchantTypes[0]
|
typeDialogVisible.value = true
|
}
|
|
function nextCreateStep() {
|
typeDialogVisible.value = false
|
router.push({
|
path: '/merchants/new',
|
// Explicitly mark this as a create route so a previously opened edit page
|
// cannot leave its mode or data in the reused route component.
|
query: { mode: 'create', merchantType: selectedCreateType.value },
|
})
|
}
|
|
function openEdit(row: Merchant) {
|
router.push({
|
path: '/merchants/new',
|
query: {
|
mode: 'edit',
|
merchantId: row.id,
|
merchantName: row.merchantName,
|
merchantType: row.merchantType || '',
|
contactMobile: row.contactMobile || '',
|
address: row.address || '',
|
coverUrl: row.coverUrl || '',
|
createdAt: row.createdAt || '',
|
businessStatus: row.businessStatus || '',
|
recommend: row.isRecommend ?? 0,
|
sortNo: row.sortNo ?? 0,
|
status: row.status ?? 1,
|
},
|
})
|
}
|
|
async function submit() {
|
const valid = await formRef.value?.validate().catch(() => false)
|
if (!valid) return
|
saving.value = true
|
try {
|
if (!editingId.value) return
|
await updateMerchant(editingId.value, form)
|
ElMessage.success('商家已更新')
|
drawerVisible.value = false
|
await loadData()
|
} finally {
|
saving.value = false
|
}
|
}
|
|
async function toggleStatus(row: Merchant) {
|
const nextStatus = row.status === 0 ? 1 : 0
|
await ElMessageBox.confirm(`确认${nextStatus === 0 ? '禁用' : '启用'}该商家?`, '状态变更')
|
await updateMerchantStatus(row.id, nextStatus)
|
ElMessage.success('状态已更新')
|
await loadData()
|
}
|
|
function openProducts(row: Merchant) {
|
router.push({
|
path: '/merchants/new',
|
query: {
|
mode: 'edit',
|
tab: 'products',
|
merchantId: row.id,
|
merchantName: row.merchantName,
|
merchantType: row.merchantType || '',
|
contactMobile: row.contactMobile || '',
|
address: row.address || '',
|
coverUrl: row.coverUrl || '',
|
createdAt: row.createdAt || '',
|
businessStatus: row.businessStatus || '',
|
recommend: row.isRecommend ?? 0,
|
sortNo: row.sortNo ?? 0,
|
status: row.status ?? 1,
|
},
|
})
|
}
|
|
onMounted(loadData)
|
</script>
|
|
<template>
|
<div class="page-stack merchant-prototype-page">
|
<section class="panel">
|
<div class="panel-header">
|
<h3>查询表格</h3>
|
</div>
|
|
<el-form class="merchant-filter-form" :model="filters" label-width="74px">
|
<el-form-item label="商家">
|
<el-input v-model="filters.keyword" placeholder="请输入商家名称" clearable @keyup.enter="search" />
|
</el-form-item>
|
<el-form-item label="联系电话">
|
<el-input v-model="filters.contactMobile" placeholder="请输入商家联系电话" clearable />
|
</el-form-item>
|
<el-form-item label="创建时间">
|
<el-date-picker
|
v-model="filters.createdTime"
|
type="daterange"
|
start-placeholder="开始日期"
|
end-placeholder="结束日期"
|
range-separator="至"
|
value-format="YYYY-MM-DD"
|
/>
|
</el-form-item>
|
<el-form-item label="商家类型">
|
<el-select v-model="filters.type" placeholder="全部" clearable filterable>
|
<el-option label="全部" value="" />
|
<el-option v-for="type in merchantTypes" :key="type" :label="type" :value="type" />
|
</el-select>
|
</el-form-item>
|
<el-form-item label="状态">
|
<el-select v-model="filters.status" placeholder="全部" clearable>
|
<el-option label="全部" :value="undefined" />
|
<el-option label="正常" :value="1" />
|
<el-option label="禁用" :value="0" />
|
</el-select>
|
</el-form-item>
|
<el-form-item label="是否推荐">
|
<el-select v-model="filters.recommend" placeholder="全部" clearable>
|
<el-option label="全部" :value="undefined" />
|
<el-option label="是" :value="1" />
|
<el-option label="否" :value="0" />
|
</el-select>
|
</el-form-item>
|
<div class="merchant-filter-actions">
|
<el-button type="primary" :icon="Search" @click="search">查询</el-button>
|
<el-button :icon="Refresh" @click="resetFilters">重置</el-button>
|
</div>
|
</el-form>
|
|
<div class="prototype-table-tools">
|
<el-button v-permission="'admin.merchants.create'" type="primary" :icon="Plus" @click="openCreate">新建</el-button>
|
</div>
|
|
<el-table
|
v-loading="loading"
|
:data="merchants"
|
border
|
class="merchant-management-table"
|
empty-text="暂无商家数据"
|
>
|
<el-table-column type="index" label="编号" width="54" align="center" />
|
<el-table-column prop="merchantName" label="商家名称" min-width="150" show-overflow-tooltip />
|
<el-table-column prop="contactMobile" label="联系方式" width="118">
|
<template #default="{ row }">{{ displayDash(row.contactMobile) }}</template>
|
</el-table-column>
|
<el-table-column prop="address" label="地址" min-width="220" show-overflow-tooltip>
|
<template #default="{ row }">{{ displayDash(row.address) }}</template>
|
</el-table-column>
|
<el-table-column prop="merchantType" label="商家类型" width="112">
|
<template #default="{ row }">{{ displayMerchantType(row) }}</template>
|
</el-table-column>
|
<el-table-column prop="orderCount" label="订单总数" width="76" align="center">
|
<template #default="{ row }">{{ displayDash(row.orderCount) }}</template>
|
</el-table-column>
|
<el-table-column prop="reservationCount" label="预约总数" width="76" align="center">
|
<template #default="{ row }">{{ displayDash(row.reservationCount) }}</template>
|
</el-table-column>
|
<el-table-column label="创建时间" width="140" align="center">
|
<template #default="{ row }">{{ formatDisplayTime(row.createdAt) }}</template>
|
</el-table-column>
|
<el-table-column label="是否推荐" width="82" align="center">
|
<template #default="{ row }">{{ recommendText(row.isRecommend) }}</template>
|
</el-table-column>
|
<el-table-column label="营业状态" width="82" align="center">
|
<template #default="{ row }">{{ displayDash(row.businessStatus) }}</template>
|
</el-table-column>
|
<el-table-column label="状态" width="68" align="center">
|
<template #default="{ row }">{{ statusText(row.status) }}</template>
|
</el-table-column>
|
<el-table-column label="操作" width="220" fixed="right" align="center">
|
<template #default="{ row }">
|
<div class="merchant-action-cell">
|
<div>
|
<el-button v-permission="'admin.merchants.disable'" link :type="row.status === 0 ? 'success' : 'danger'" @click="toggleStatus(row)">
|
{{ row.status === 0 ? '启用' : '禁用' }}
|
</el-button>
|
</div>
|
<div>
|
<el-button v-permission="'admin.merchants.edit'" link type="primary" :icon="Edit" @click="openEdit(row)">编辑</el-button>
|
</div>
|
<div>
|
<el-button v-if="supportsGroupBuying(row)" link type="primary" :icon="Goods" @click="openProducts(row)">团购管理</el-button>
|
</div>
|
</div>
|
</template>
|
</el-table-column>
|
</el-table>
|
|
<div class="pagination-row">
|
<el-pagination
|
v-model:current-page="pagination.page"
|
v-model:page-size="pagination.size"
|
background
|
layout="total, sizes, prev, pager, next"
|
:page-sizes="[10, 20, 50]"
|
:total="pagination.total"
|
@change="loadData"
|
/>
|
</div>
|
</section>
|
|
<el-dialog v-model="typeDialogVisible" title="选择商家类型" width="220px" class="merchant-type-dialog" align-center :close-on-click-modal="false">
|
<el-select v-model="selectedCreateType" class="merchant-type-select" filterable placeholder="请输入商家类型">
|
<el-option v-for="type in createMerchantTypes" :key="type" :label="type" :value="type" />
|
</el-select>
|
<el-button type="primary" class="next-step-button" @click="nextCreateStep">下一步</el-button>
|
</el-dialog>
|
|
<el-drawer v-model="drawerVisible" :title="drawerTitle" size="520px" @closed="resetForm">
|
<el-form ref="formRef" :model="form" :rules="rules" label-width="96px" class="drawer-form">
|
<el-form-item label="商家名称" prop="merchantName" required>
|
<el-input v-model="form.merchantName" placeholder="请输入商家名称" />
|
</el-form-item>
|
<el-form-item label="商家类型">
|
<el-select v-model="form.merchantType" allow-create filterable placeholder="请选择或输入类型">
|
<el-option v-for="type in merchantTypes" :key="type" :label="type" :value="type" />
|
</el-select>
|
</el-form-item>
|
<el-form-item label="联系人">
|
<el-input v-model="form.contactName" placeholder="请输入联系人" />
|
</el-form-item>
|
<el-form-item label="联系电话">
|
<el-input v-model="form.contactMobile" placeholder="请输入联系电话" />
|
</el-form-item>
|
<el-form-item label="地址">
|
<el-input v-model="form.address" type="textarea" :rows="2" placeholder="请输入地址" />
|
</el-form-item>
|
<el-form-item label="营业状态">
|
<el-select v-model="form.businessStatus" clearable placeholder="请选择营业状态">
|
<el-option label="营业中" value="营业中" />
|
<el-option label="休息中" value="休息中" />
|
</el-select>
|
</el-form-item>
|
<el-form-item label="Logo">
|
<el-input v-model="form.logoUrl" placeholder="图片 URL" />
|
</el-form-item>
|
<el-form-item label="封面图">
|
<el-input v-model="form.coverUrl" placeholder="图片 URL" />
|
</el-form-item>
|
<el-form-item label="排序">
|
<el-input-number v-model="form.sortNo" :min="0" :max="9999" />
|
</el-form-item>
|
<el-form-item label="是否推荐">
|
<el-switch v-model="form.recommend" :active-value="1" :inactive-value="0" />
|
</el-form-item>
|
<el-form-item label="状态">
|
<el-radio-group v-model="form.status">
|
<el-radio-button :label="1">正常</el-radio-button>
|
<el-radio-button :label="0">禁用</el-radio-button>
|
</el-radio-group>
|
</el-form-item>
|
</el-form>
|
<template #footer>
|
<el-button @click="drawerVisible = false">取消</el-button>
|
<el-button type="primary" :loading="saving" @click="submit">保存</el-button>
|
</template>
|
</el-drawer>
|
</div>
|
</template>
|