<script setup lang="ts">
|
import { computed, onMounted, reactive, ref } from 'vue'
|
import { useRoute, useRouter } from 'vue-router'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ZoomIn } from '@element-plus/icons-vue'
|
import { auditProduct, getAdminProductDetail } from '../api/admin'
|
import type { Product } from '../types/admin'
|
|
const fallbackImage =
|
'https://images.unsplash.com/photo-1569718212165-3a8278d5f624?auto=format&fit=crop&w=640&q=80'
|
|
interface BenefitRow {
|
level: string
|
enabled: boolean
|
free: boolean
|
limitCount: string
|
discount: string
|
exceedDiscount: string
|
resetCycle: string
|
subsidy: boolean
|
}
|
|
type LegacyBenefitConfig =
|
| { rows?: Array<Partial<BenefitRow> & { levelName?: string; governmentSubsidy?: boolean }> }
|
| Array<Partial<BenefitRow>>
|
|
const route = useRoute()
|
const router = useRouter()
|
const loading = ref(false)
|
const auditing = ref(false)
|
const product = ref<Product>()
|
const galleryImages = ref<string[]>([])
|
const previewVisible = ref(false)
|
const previewUrl = ref('')
|
|
const notice = reactive({
|
validPeriod: '',
|
useMethod: '',
|
availableTime: '',
|
otherRules: '',
|
})
|
|
const benefitRows = reactive<BenefitRow[]>([
|
{ level: '卓越人才卡', enabled: true, free: true, limitCount: '', discount: '', exceedDiscount: '', resetCycle: '每天', subsidy: true },
|
{ level: '杰出人才卡', enabled: true, free: false, limitCount: '', discount: '', exceedDiscount: '', resetCycle: '每天', subsidy: true },
|
{ level: '菁英人才卡', enabled: true, free: false, limitCount: '', discount: '', exceedDiscount: '', resetCycle: '每天', subsidy: true },
|
])
|
|
const productImage = computed(() => product.value?.coverUrl || fallbackImage)
|
const imageList = computed(() => (galleryImages.value.length ? galleryImages.value : [productImage.value]))
|
const productNameText = computed(() => product.value?.productName || '百杏厨房(万象城店)')
|
const isHotelProduct = computed(() => {
|
const merchantType = product.value?.merchantType || ''
|
return merchantType === '酒店服务' || merchantType === '酒店'
|
})
|
const hideTalentBenefitPanel = computed(() => {
|
const productData = product.value as (Product & Record<string, unknown>) | undefined
|
const merchantTypes = [
|
productData?.merchantType,
|
productData?.merchantTypeCode,
|
productData?.merchant_type,
|
productData?.merchant_type_code,
|
]
|
.map(value => String(value || '').trim().toLowerCase().replace(/[\s_-]+/g, ''))
|
.filter(Boolean)
|
const productType = String(productData?.productType || '').trim().toLowerCase().replace(/[\s_-]+/g, '')
|
const hiddenTypes = [
|
'酒店服务', '酒店', 'hotel',
|
'医院服务', '医院', '医疗服务', '医疗', '免费体检', '体检服务', '医疗体检',
|
'hospital', 'medical', 'healthcheck', 'medicalservice',
|
]
|
return merchantTypes.some(type => hiddenTypes.includes(type)) || ['hotel', 'hospital', 'medical', 'healthcheck'].includes(productType)
|
})
|
const productAmountText = computed(() => {
|
const salePrice = product.value?.salePrice
|
const originalPrice = product.value?.originalPrice
|
if (isHotelProduct.value && salePrice !== undefined && originalPrice !== undefined) {
|
return `${amountText(salePrice)} - ${amountText(originalPrice)}`
|
}
|
return amountText(salePrice ?? originalPrice) || '158'
|
})
|
const formatDateTime = (value?: string) => value ? value.replace('T', ' ') : ''
|
const createdAtText = computed(() => formatDateTime(product.value?.createdAt) || '2026-06-18 11:34:25')
|
const submitTimeText = computed(() => formatDateTime(product.value?.submitTime || product.value?.createdAt) || '2026-06-18 11:34:25')
|
const isPendingAudit = computed(() => product.value?.auditStatus === 1)
|
const auditResultText = computed(() => {
|
if (product.value?.auditStatus === 2) return '审核通过'
|
if (product.value?.auditStatus === 3) return '已驳回'
|
return '待审核'
|
})
|
|
function amountText(value?: number) {
|
if (value === undefined || value === null) return ''
|
return Number(value).toFixed(2).replace(/\.00$/, '')
|
}
|
|
function parseNotice(value?: string | Record<string, unknown>, source?: Product) {
|
notice.validPeriod = ''
|
notice.useMethod = ''
|
notice.availableTime = ''
|
notice.otherRules = ''
|
const fallback = source as (Product & Record<string, unknown>) | undefined
|
if (!value) {
|
notice.validPeriod = [fallback?.validStartTime, fallback?.validEndTime].filter(Boolean).join(' 至 ')
|
notice.useMethod = String(fallback?.usageInstructions || fallback?.usage || '')
|
notice.availableTime = String(fallback?.availableHours || fallback?.availableTime || '')
|
notice.otherRules = String(fallback?.otherRules || fallback?.rules || '')
|
return
|
}
|
|
try {
|
const parsed = typeof value === 'string' ? JSON.parse(value) : value
|
notice.validPeriod = parsed.validPeriod || parsed.valid_period || parsed.validity || ''
|
notice.useMethod = parsed.useMethod || parsed.use_method || parsed.usage || parsed.usageInstructions || parsed.usage_instructions || ''
|
notice.availableTime = parsed.availableTime || parsed.available_time || parsed.availableHours || parsed.available_hours || ''
|
notice.otherRules = parsed.otherRules || parsed.other_rules || parsed.otherRule || parsed.other_rule || parsed.rules || ''
|
notice.validPeriod ||= [fallback?.validStartTime, fallback?.validEndTime].filter(Boolean).join(' 至 ')
|
notice.useMethod ||= String(fallback?.usageInstructions || fallback?.usage || '')
|
notice.availableTime ||= String(fallback?.availableHours || fallback?.availableTime || '')
|
notice.otherRules ||= String(fallback?.otherRules || fallback?.rules || '')
|
} catch {
|
notice.otherRules = typeof value === 'string' ? value : ''
|
}
|
}
|
|
function parseJsonValue<T>(value: unknown, fallback: T): T {
|
if (!value) return fallback
|
if (typeof value !== 'string') return value as T
|
try {
|
return JSON.parse(value) as T
|
} catch {
|
return fallback
|
}
|
}
|
|
function parseProductImages(value?: Product['productImages'], coverUrl?: string) {
|
const images = parseJsonValue<string[]>(value, [])
|
if (Array.isArray(images) && images.length) return images
|
return coverUrl ? [coverUrl] : [fallbackImage]
|
}
|
|
function parseBenefits(value?: Product['rightsConfig'], legacyValue?: string) {
|
benefitRows.forEach((row) => {
|
row.enabled = true
|
row.free = row.level === '卓越人才卡'
|
row.limitCount = ''
|
row.discount = ''
|
row.exceedDiscount = ''
|
row.resetCycle = '每天'
|
row.subsidy = true
|
})
|
|
const parsed = parseJsonValue(value, undefined)
|
const legacy = parseJsonValue<LegacyBenefitConfig | undefined>(legacyValue, undefined)
|
const rows = Array.isArray(parsed)
|
? parsed
|
: Array.isArray(legacy)
|
? legacy
|
: legacy?.rows
|
if (!Array.isArray(rows)) return
|
rows.forEach((item: Partial<BenefitRow> & { levelName?: string; governmentSubsidy?: boolean }) => {
|
const target = benefitRows.find((row) => row.level === item.level || row.level === item.levelName)
|
if (!target) return
|
target.enabled = item.enabled ?? target.enabled
|
target.free = item.free ?? target.free
|
target.limitCount = item.limitCount ?? target.limitCount
|
target.discount = item.discount ?? target.discount
|
target.exceedDiscount = item.exceedDiscount ?? target.exceedDiscount
|
target.resetCycle = item.resetCycle ?? target.resetCycle
|
target.subsidy = item.subsidy ?? item.governmentSubsidy ?? target.subsidy
|
})
|
}
|
|
async function loadData() {
|
loading.value = true
|
try {
|
const id = String(route.params.id)
|
product.value = await getAdminProductDetail(id)
|
} catch {
|
ElMessage.error('团单详情加载失败')
|
router.push('/products')
|
return
|
} finally {
|
galleryImages.value = parseProductImages(product.value?.productImages, product.value?.coverUrl)
|
parseNotice(product.value?.purchaseNotice, product.value)
|
parseBenefits(product.value?.rightsConfig, product.value?.benefitDesc)
|
loading.value = false
|
}
|
}
|
|
async function submitAudit(status: 2 | 3) {
|
const id = product.value?.id
|
if (!id) return
|
|
let remark = ''
|
if (status === 3) {
|
const result = await ElMessageBox.prompt('请输入审核不通过原因', '审核不通过', {
|
inputPlaceholder: '请输入',
|
confirmButtonText: '确定',
|
cancelButtonText: '取消',
|
})
|
remark = result.value || ''
|
} else {
|
await ElMessageBox.confirm('确认审核通过该团单?', '审核通过', {
|
confirmButtonText: '确定',
|
cancelButtonText: '取消',
|
type: 'warning',
|
})
|
}
|
|
auditing.value = true
|
try {
|
if (String(id) !== 'demo') {
|
product.value = await auditProduct(id, status, remark)
|
}
|
ElMessage.success(status === 2 ? '审核已通过' : '审核已驳回')
|
router.push('/products')
|
} finally {
|
auditing.value = false
|
}
|
}
|
|
function previewImage(image: string) {
|
previewUrl.value = image
|
previewVisible.value = true
|
}
|
|
onMounted(loadData)
|
</script>
|
|
<template>
|
<div v-loading="loading" class="product-audit-detail-page">
|
<section class="audit-detail-panel product-info-panel">
|
<h3>商品信息</h3>
|
|
<div class="product-basic-grid">
|
<div class="detail-field">
|
<label>商品名称</label>
|
<el-input :model-value="productNameText" readonly />
|
</div>
|
<div class="detail-field detail-money-field">
|
<label>商品金额</label>
|
<el-input :model-value="productAmountText" readonly />
|
<span>元</span>
|
</div>
|
<div class="detail-time-pair">
|
<label>创建时间</label>
|
<strong>{{ createdAtText }}</strong>
|
<label>提交修改时间</label>
|
<strong>{{ submitTimeText }}</strong>
|
</div>
|
</div>
|
|
<div class="audit-image-row">
|
<label>封面图</label>
|
<div class="audit-cover-box">
|
<img :src="productImage" alt="商品封面" />
|
</div>
|
</div>
|
|
<div class="audit-gallery-row">
|
<label>商品图片</label>
|
<div class="audit-thumb-list">
|
<div v-for="image in imageList" :key="image" class="audit-thumb-box">
|
<img :src="image" alt="商品图片" />
|
<button class="thumb-preview" type="button" @click="previewImage(image)">
|
<el-icon><ZoomIn /></el-icon>
|
</button>
|
</div>
|
</div>
|
</div>
|
|
<div class="audit-textarea-row">
|
<label>商品详情</label>
|
<div class="audit-rich-text-preview" v-html="product?.detailText || ''"></div>
|
</div>
|
|
<div class="purchase-notice-block">
|
<p>购买须知设置(非必填)</p>
|
<div class="purchase-notice-grid">
|
<div>
|
<label>有效期</label>
|
<el-input v-model="notice.validPeriod" placeholder="请输入" readonly />
|
</div>
|
<div>
|
<label>使用方式</label>
|
<el-input v-model="notice.useMethod" placeholder="请输入" readonly />
|
</div>
|
<div>
|
<label>可用时间</label>
|
<el-input v-model="notice.availableTime" placeholder="请输入" readonly />
|
</div>
|
<div>
|
<label>其他规则</label>
|
<el-input v-model="notice.otherRules" placeholder="请输入" readonly />
|
</div>
|
</div>
|
</div>
|
</section>
|
<el-dialog v-model="previewVisible" title="图片预览" width="760px" destroy-on-close :close-on-click-modal="false">
|
<img v-if="previewUrl" :src="previewUrl" alt="商品图片预览" class="product-audit-preview-image" />
|
</el-dialog>
|
|
<section v-if="!hideTalentBenefitPanel" class="audit-detail-panel talent-benefit-panel">
|
<h3>人才权益</h3>
|
<div class="benefit-config-grid">
|
<div v-for="row in benefitRows" :key="row.level" class="benefit-config-row">
|
<strong>{{ row.level }}</strong>
|
<el-switch v-model="row.enabled" disabled />
|
<span>是否免费</span>
|
<el-switch v-model="row.free" disabled />
|
<template v-if="row.free">
|
<span>免费次数</span>
|
<el-input v-model="row.limitCount" placeholder="请输入" disabled />
|
<span>次</span>
|
<span>重置周期</span>
|
<el-input v-model="row.resetCycle" disabled />
|
<span>超过次数</span>
|
<el-input v-model="row.exceedDiscount" placeholder="请输入" disabled />
|
<span>折</span>
|
</template>
|
<template v-else>
|
<span>折扣</span>
|
<el-input v-model="row.discount" placeholder="请输入" disabled />
|
<span>折</span>
|
</template>
|
<span>是否政府补贴</span>
|
<el-switch v-model="row.subsidy" disabled />
|
</div>
|
</div>
|
</section>
|
|
<div v-if="isPendingAudit" class="audit-action-row">
|
<el-button v-permission="'admin.products.audit'" type="primary" :loading="auditing" @click="submitAudit(2)">审核通过</el-button>
|
<el-button v-permission="'admin.products.audit'" type="primary" :loading="auditing" @click="submitAudit(3)">审核不通过</el-button>
|
</div>
|
<div v-else class="audit-result-row">
|
<span>审核结果:{{ auditResultText }}</span>
|
<span v-if="product?.auditRemark">驳回原因:{{ product.auditRemark }}</span>
|
</div>
|
</div>
|
</template>
|