<script setup lang="ts">
|
import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue'
|
import { useRoute, useRouter } from 'vue-router'
|
import { load as loadAMapApi } from '@amap/amap-jsapi-loader'
|
import { Minus, Picture, Plus, Refresh, Search } from '@element-plus/icons-vue'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
import type { FormInstance, FormRules } from 'element-plus'
|
import {
|
createCoupon as createCouponApi,
|
checkMerchantLoginAccount,
|
createMerchant,
|
createProduct,
|
getAdminMerchantDetail,
|
deleteCoupon as deleteCouponApi,
|
deleteProduct as deleteProductApi,
|
getAdminCouponDetail,
|
getAdminCoupons,
|
getAdminProducts,
|
updateCoupon,
|
updateCouponStatus,
|
updateMerchant,
|
updateProduct,
|
updateProductStatus,
|
uploadAdminImage,
|
uploadAdminPdf,
|
} from '../api/admin'
|
import AdminRichTextEditor from '../components/AdminRichTextEditor.vue'
|
import type { Coupon, CouponPayload, MerchantPayload, Product, ProductPayload } from '../types/admin'
|
|
interface BusinessHour {
|
id: number
|
value: string[]
|
}
|
|
interface MerchantRightConfig {
|
level: string
|
enabled: boolean
|
tag: string
|
detail: string
|
storeConsume: boolean
|
discount: string
|
governmentSubsidy: boolean
|
consumeDesc: string
|
consumeEnabled: boolean
|
freeVoucherEnabled?: boolean
|
freeVoucherTimes?: string
|
freeVoucherCycle?: string
|
freeVoucherSubsidy?: boolean
|
consumeType?: string
|
consumeDiscount?: string
|
consumeDiscountSubsidy?: boolean
|
consumeCoupon?: string
|
consumeCouponSubsidy?: boolean
|
parkingType?: 'free' | 'paid'
|
parkingScope?: 'all' | 'selected'
|
externalLinkType?: string
|
externalMiniProgramShortLink?: string
|
externalMiniProgramPath?: string
|
externalWebUrl?: string
|
}
|
|
const route = useRoute()
|
const router = useRouter()
|
const saving = ref(false)
|
const merchantFormRef = ref<FormInstance>()
|
const accountFormRef = ref<FormInstance>()
|
const rightsSaving = ref(false)
|
const accountChecking = ref(false)
|
const accountTaken = ref(false)
|
const merchantHasLoginAccount = ref(false)
|
const imageInputRef = ref<HTMLInputElement>()
|
const imageUploading = ref(false)
|
const imageUploadTarget = ref<'merchant' | 'productCover' | 'productImage' | 'approvalWindow'>('merchant')
|
const merchantCoverPreviewFailed = ref(false)
|
const productCoverPreviewFailed = ref(false)
|
const routeTab = typeof route.query.tab === 'string' ? route.query.tab : 'settings'
|
const activeTab = ref(['settings', 'rights', 'products', 'coupons'].includes(routeTab) ? routeTab : 'settings')
|
const couponMode = ref<'list' | 'edit'>('list')
|
const hasValidMerchantId = computed(() => /^\d+$/.test(queryString('merchantId')))
|
const isEditMode = computed(() => route.query.mode === 'edit' && hasValidMerchantId.value)
|
const hasExistingMerchant = computed(() => isEditMode.value)
|
const normalizedMerchantType = computed(() => String(form.merchantType || '').trim())
|
const isFoodMerchant = computed(() => normalizedMerchantType.value === '餐饮服务')
|
const isApartmentMerchant = computed(() => normalizedMerchantType.value === '公寓租赁')
|
const isHotelMerchant = computed(() => normalizedMerchantType.value === '酒店服务')
|
const isCarWashMerchant = computed(() => normalizedMerchantType.value === '自助洗车')
|
const isHealthCheckMerchant = computed(() => normalizedMerchantType.value === '免费体检')
|
const isParkingMerchant = computed(() => normalizedMerchantType.value === '停车服务')
|
const isChildcareMerchant = computed(() => normalizedMerchantType.value === '子女托管')
|
const isFinancialMerchant = computed(() => normalizedMerchantType.value === '金融服务')
|
const isMovieMerchant = computed(() => normalizedMerchantType.value === '观影服务')
|
const isSpouseEmploymentMerchant = computed(() => normalizedMerchantType.value === '配偶推荐就业')
|
const isApprovalMerchant = computed(() => normalizedMerchantType.value === '行政审批')
|
const isServiceMerchant = computed(() => isFoodMerchant.value || normalizedMerchantType.value === '景区服务')
|
const supportsStoreConsumption = computed(() => [
|
'酒店服务', '景区服务', '餐饮服务', '口腔治疗保健', '免费体检',
|
].includes(normalizedMerchantType.value) || (isCarWashMerchant.value && form.miniProgramJumpType === 'normal'))
|
const merchantAppLoginOptionalTypes = [
|
'购房优惠', '购车服务', '家电服务', '数码服务', '文化服务', '电信服务', '口腔治疗保健', '加油服务',
|
]
|
const simplifiedMerchantSettingsTypes = [
|
'购房优惠', '购车服务', '家电服务', '数码服务', '文化服务', '电信服务', '口腔治疗保健', '加油服务',
|
]
|
const usesSimplifiedMerchantSettings = computed(() => simplifiedMerchantSettingsTypes.includes(normalizedMerchantType.value))
|
const settingsAndRightsOnlyMerchantTypes = [
|
'加油服务', '购房优惠', '购车服务', '家电服务', '数码服务', '文化服务', '电信服务',
|
]
|
const usesSettingsAndRightsOnlyTabs = computed(() => settingsAndRightsOnlyMerchantTypes.includes(normalizedMerchantType.value))
|
const supportsMerchantAppLogin = computed(() => (
|
isServiceMerchant.value
|
|| isApartmentMerchant.value
|
|| isCarWashMerchant.value
|
|| isHealthCheckMerchant.value
|
|| isParkingMerchant.value
|
|| isChildcareMerchant.value
|
|| isFinancialMerchant.value
|
|| isMovieMerchant.value
|
|| isSpouseEmploymentMerchant.value
|
|| isApprovalMerchant.value
|
|| merchantAppLoginOptionalTypes.includes(normalizedMerchantType.value)
|
))
|
const requiresMerchantOperator = computed(() => (!isEditMode.value || !merchantHasLoginAccount.value) && (
|
normalizedMerchantType.value === '景区服务'
|
|| (supportsMerchantAppLogin.value && Number(form.canLoginMerchantApp) === 1)
|
))
|
function carWashJumpMeta() {
|
if (!isCarWashMerchant.value) return {}
|
if (form.miniProgramJumpType !== 'miniprogram') {
|
return {
|
externalLinkType: 'normal',
|
externalMiniProgramShortLink: '',
|
externalMiniProgramPath: '',
|
externalWebUrl: '',
|
}
|
}
|
return {
|
externalLinkType: 'miniProgram',
|
externalMiniProgramShortLink: String(form.miniProgramJumpUrl || '').trim(),
|
externalMiniProgramPath: '',
|
externalWebUrl: '',
|
}
|
}
|
|
function syncCarWashJumpMeta() {
|
if (!isCarWashMerchant.value || rightsConfigs.length === 0) return
|
const meta = carWashJumpMeta()
|
rightsConfigs.forEach((item, index) => {
|
if (index === 0) {
|
Object.assign(item, meta)
|
return
|
}
|
item.externalLinkType = ''
|
item.externalMiniProgramShortLink = ''
|
item.externalMiniProgramPath = ''
|
item.externalWebUrl = ''
|
})
|
}
|
|
function queryString(key: string, fallback = '') {
|
const value = route.query[key]
|
return typeof value === 'string' && value ? value : fallback
|
}
|
|
function defaultMerchantRecommend() {
|
const merchantType = queryString('merchantType')
|
return merchantType === '餐饮服务' || merchantType === '景区服务' ? 1 : 0
|
}
|
|
function applyNewMerchantDefaults() {
|
if (isEditMode.value) return
|
|
const merchantType = queryString('merchantType')
|
form.merchantType = merchantType
|
form.recommend = merchantType === '餐饮服务' || merchantType === '景区服务' ? 1 : 0
|
form.sortNo = undefined
|
}
|
|
const tabs = computed(() => {
|
if (usesSettingsAndRightsOnlyTabs.value) {
|
return [
|
{ label: '商家设置', value: 'settings' },
|
{ label: '商家权益', value: 'rights' },
|
]
|
}
|
if (isApartmentMerchant.value) {
|
return [
|
{ label: '商家设置', value: 'settings' },
|
{ label: '商家权益', value: 'rights' },
|
]
|
}
|
if (isHealthCheckMerchant.value) {
|
return [
|
{ label: '商家设置', value: 'settings' },
|
{ label: '商家权益', value: 'rights' },
|
{ label: '团单', value: 'products' },
|
]
|
}
|
if (isParkingMerchant.value || isChildcareMerchant.value || isFinancialMerchant.value || isMovieMerchant.value || isSpouseEmploymentMerchant.value || isApprovalMerchant.value) {
|
return [
|
{ label: '商家设置', value: 'settings' },
|
{ label: '商家权益', value: 'rights' },
|
]
|
}
|
if (isCarWashMerchant.value) {
|
if (form.miniProgramJumpType === 'normal') {
|
return [
|
{ label: '商家设置', value: 'settings' },
|
{ label: '商家权益', value: 'rights' },
|
{ label: '团单', value: 'products' },
|
{ label: '优惠券', value: 'coupons' },
|
]
|
}
|
return [
|
{ label: '商家设置', value: 'settings' },
|
{ label: '商家权益', value: 'rights' },
|
]
|
}
|
return [
|
{ label: '商家设置', value: 'settings' },
|
{ label: '商家权益', value: 'rights' },
|
{ label: '团单', value: 'products' },
|
{ label: '优惠券', value: 'coupons' },
|
]
|
})
|
|
const form = reactive<MerchantPayload & { createdAt: string }>({
|
merchantName: '',
|
merchantType: queryString('merchantType', ''),
|
contactMobile: '',
|
address: '',
|
coverUrl: '',
|
createdAt: '',
|
businessStatus: '',
|
longitude: undefined,
|
latitude: undefined,
|
canLoginMerchantApp: 1,
|
groupBuyingEnabled: 0,
|
usageInstructions: '',
|
rightsText: '',
|
miniProgramJumpType: 'miniprogram',
|
miniProgramJumpUrl: '',
|
serviceLabel: '',
|
recommend: defaultMerchantRecommend(),
|
sortNo: undefined,
|
status: 1,
|
businessHours: [],
|
})
|
const merchantRules = computed<FormRules>(() => ({
|
merchantName: [{ required: true, message: '请输入商家名称', trigger: 'blur' }],
|
contactMobile: [{ required: true, message: '请输入联系电话', trigger: 'blur' }],
|
address: isApartmentMerchant.value ? [] : [{ required: true, message: '请输入商家地址', trigger: 'blur' }],
|
coverUrl: [{ required: true, message: '请上传商家封面图', trigger: 'change' }],
|
sortNo: [{
|
validator: (_rule, value, callback) => {
|
const text = String(value ?? '').trim()
|
if (!text) return callback(new Error('请输入排序'))
|
if (!/^\d+$/.test(text)) return callback(new Error('排序只能输入非负整数'))
|
callback()
|
},
|
trigger: 'blur',
|
}],
|
}))
|
|
const accountForm = reactive({
|
loginAccount: '',
|
loginPassword: '',
|
confirmPassword: '',
|
})
|
const accountRules = computed<FormRules>(() => ({
|
loginAccount: [{
|
validator: (_rule, value, callback) => {
|
if (!requiresMerchantOperator.value) return callback()
|
if (!String(value || '').trim()) return callback(new Error('请输入登录账号'))
|
if (!/^[A-Za-z0-9]{1,32}$/.test(String(value).trim())) return callback(new Error('登录账号仅支持英文和数字,长度不超过32位'))
|
callback()
|
},
|
trigger: 'blur',
|
}],
|
loginPassword: [{
|
validator: (_rule, value, callback) => {
|
const updatingPassword = isEditMode.value && String(value || '').length > 0
|
if (!requiresMerchantOperator.value && !updatingPassword) return callback()
|
if (!value) return callback(new Error('请输入登录密码'))
|
if (String(value).length < 6 || String(value).length > 64) return callback(new Error('登录密码长度须为6至64位'))
|
callback()
|
},
|
trigger: 'blur',
|
}],
|
confirmPassword: [{
|
validator: (_rule, value, callback) => {
|
const needsConfirmation = requiresMerchantOperator.value || (isEditMode.value && accountForm.loginPassword.length > 0)
|
if (!needsConfirmation) return callback()
|
if (!value) return callback(new Error('请输入确认密码'))
|
if (value !== accountForm.loginPassword) return callback(new Error('两次输入的密码不一致'))
|
callback()
|
},
|
trigger: 'blur',
|
}],
|
}))
|
|
function normalizeLoginAccount() {
|
accountForm.loginAccount = accountForm.loginAccount.replace(/[^A-Za-z0-9]/g, '').slice(0, 32)
|
accountTaken.value = false
|
}
|
|
async function checkLoginAccountAvailable() {
|
if (!requiresMerchantOperator.value) return true
|
if (!/^[A-Za-z0-9]{1,32}$/.test(accountForm.loginAccount)) return false
|
accountChecking.value = true
|
try {
|
const result = await checkMerchantLoginAccount(accountForm.loginAccount)
|
accountTaken.value = !result.available
|
if (accountTaken.value) ElMessage.warning('登录账号已存在,请更换')
|
return result.available
|
} finally {
|
accountChecking.value = false
|
}
|
}
|
|
function formatDisplayTime(value?: string) {
|
return value ? value.replace('T', ' ') : ''
|
}
|
|
const productFilters = reactive({
|
productName: '',
|
status: undefined as number | undefined,
|
createdTime: [] as string[],
|
})
|
|
const productLoading = ref(false)
|
const productMode = ref<'list' | 'form'>('list')
|
const productSaving = ref(false)
|
const productFormRef = ref<FormInstance>()
|
const editingProductId = ref<number | string>()
|
|
const productPagination = reactive({
|
page: 1,
|
size: 10,
|
total: 0,
|
})
|
|
const productForm = reactive<ProductPayload & { createdAt?: string, updatedAt?: string }>({
|
merchantId: queryString('merchantId'),
|
productType: 'GROUP',
|
productName: '',
|
coverUrl: '',
|
salePrice: undefined,
|
stock: undefined,
|
detailText: '',
|
benefitDesc: '',
|
status: 1,
|
sortNo: 0,
|
productImages: [],
|
})
|
const productImages = ref<string[]>([])
|
const productRules = computed<FormRules>(() => ({
|
productName: [{ required: true, message: '请输入商品名称', trigger: 'blur' }],
|
coverUrl: [{ required: true, message: '请上传商品封面图', trigger: 'change' }],
|
salePrice: [{
|
validator: (_rule, value, callback) => {
|
if (isHealthCheckMerchant.value) return callback()
|
const amountText = String(value ?? '').trim()
|
return /^\d+(?:\.\d{1,2})?$/.test(amountText) && Number(amountText) >= 0.01
|
? callback()
|
: callback(new Error('商品金额必须为不小于0.01的正数,最多两位小数'))
|
},
|
trigger: 'blur',
|
}],
|
productImages: [{
|
validator: (_rule, _value, callback) => isHealthCheckMerchant.value || productImages.value.length > 0
|
? callback()
|
: callback(new Error('请至少添加一张商品图片')),
|
trigger: 'change',
|
}],
|
detailText: [{
|
validator: (_rule, value, callback) => isHealthCheckMerchant.value || String(value || '').replace(/<[^>]*>/g, '').replace(/ /g, '').trim()
|
? callback()
|
: callback(new Error('请输入商品详情')),
|
trigger: 'change',
|
}],
|
}))
|
|
interface HealthCheckItem {
|
name: string
|
meaning: string
|
gender: string[]
|
}
|
const healthCheckItems = ref<HealthCheckItem[]>([])
|
|
function addHealthCheckItem() {
|
healthCheckItems.value.push({ name: '', meaning: '', gender: ['男'] })
|
}
|
|
function removeHealthCheckItem(index: number) {
|
healthCheckItems.value.splice(index, 1)
|
}
|
|
const productNoticeForm = reactive({
|
validPeriod: '',
|
useMethod: '',
|
availableTime: '',
|
otherRules: '',
|
})
|
const productValidPeriodRange = ref<string[]>([])
|
const productAvailableTimeRange = ref<string[]>([])
|
|
const productRights = reactive([
|
{
|
level: '卓越人才卡',
|
enabled: true,
|
free: true,
|
limitCount: '',
|
discount: '',
|
exceedDiscount: '',
|
resetCycle: '每天',
|
subsidy: true,
|
exceedSubsidy: true,
|
},
|
{
|
level: '杰出人才卡',
|
enabled: true,
|
free: false,
|
limitCount: '',
|
discount: '',
|
exceedDiscount: '',
|
resetCycle: '每天',
|
subsidy: true,
|
exceedSubsidy: true,
|
},
|
{
|
level: '菁英人才卡',
|
enabled: true,
|
free: false,
|
limitCount: '',
|
discount: '',
|
exceedDiscount: '',
|
resetCycle: '每天',
|
subsidy: true,
|
exceedSubsidy: true,
|
},
|
])
|
const productExceedDiscountErrors = reactive<Record<string, string>>({})
|
|
function defaultProductRights() {
|
return [
|
{
|
level: '卓越人才卡',
|
enabled: true,
|
free: true,
|
limitCount: '',
|
discount: '',
|
exceedDiscount: '',
|
resetCycle: '每天',
|
subsidy: true,
|
exceedSubsidy: true,
|
},
|
{
|
level: '杰出人才卡',
|
enabled: true,
|
free: false,
|
limitCount: '',
|
discount: '',
|
exceedDiscount: '',
|
resetCycle: '每天',
|
subsidy: true,
|
exceedSubsidy: true,
|
},
|
{
|
level: '菁英人才卡',
|
enabled: true,
|
free: false,
|
limitCount: '',
|
discount: '',
|
exceedDiscount: '',
|
resetCycle: '每天',
|
subsidy: true,
|
exceedSubsidy: true,
|
},
|
]
|
}
|
|
const couponFilters = reactive({
|
couponName: '',
|
status: undefined as number | undefined,
|
createdTime: [] as string[],
|
})
|
|
const couponLoading = ref(false)
|
const couponSaving = ref(false)
|
const editingCouponId = ref<number | string>()
|
|
const couponPagination = reactive({
|
page: 1,
|
size: 10,
|
total: 0,
|
})
|
|
const couponEditForm = reactive({
|
couponName: '',
|
useRequirement: 'full',
|
fullAmount: '',
|
couponAmount: '',
|
talentLevels: ['卓越人才卡', '杰出人才卡', '菁英人才卡'],
|
validTime: [] as string[],
|
productScope: 'selected',
|
storeConsume: true,
|
governmentSubsidy: true,
|
status: 1,
|
})
|
const couponFormRef = ref<FormInstance>()
|
const couponRules = computed<FormRules>(() => ({
|
couponName: [{ required: true, message: '请输入优惠券名称', trigger: 'blur' }],
|
couponAmount: [{
|
validator: (_rule, value, callback) => {
|
const amount = Number(value)
|
return value !== '' && Number.isFinite(amount) && amount >= 0.1 && amount <= 9999.9
|
? callback()
|
: callback(new Error('优惠券金额必须在0.1-9999.9元之间'))
|
},
|
trigger: 'blur',
|
}],
|
fullAmount: [{
|
validator: (_rule, value, callback) => couponEditForm.useRequirement !== 'full' || String(value || '').trim()
|
? callback()
|
: callback(new Error('请输入使用门槛')),
|
trigger: 'blur',
|
}],
|
validTime: [{ required: true, message: '请选择优惠券有效期', trigger: 'change' }],
|
productScope: [{
|
validator: (_rule, value, callback) => value !== 'selected' || selectedCouponProductIds.value.length > 0
|
? callback()
|
: callback(new Error('请至少选择一个可用商品')),
|
trigger: 'change',
|
}],
|
}))
|
|
const merchantProducts = ref<Product[]>([])
|
const merchantCoupons = ref<Coupon[]>([])
|
const couponProducts = ref<Product[]>([])
|
const selectedCouponProductIds = ref<Array<number | string>>([])
|
const couponLevelQuantities = reactive<Record<string, number>>({})
|
const couponTalentOptions = ['卓越人才卡', '杰出人才卡', '菁英人才卡']
|
|
const businessHours = reactive<BusinessHour[]>([{ id: 1, value: [] }])
|
|
function defaultMerchantRights(): MerchantRightConfig[] {
|
const currentType = queryString('merchantType', '') || normalizedMerchantType.value
|
const supportsDefaultStoreConsumption = ['餐饮服务', '景区服务', '口腔治疗保健', '免费体检'].includes(currentType)
|
|| (currentType === '自助洗车' && form.miniProgramJumpType === 'normal')
|
const isHotel = currentType === '酒店服务'
|
const isCarWash = currentType === '自助洗车'
|
const isHealthCheck = currentType === '免费体检'
|
const isParking = currentType === '停车服务'
|
const isChildcare = currentType === '子女托管'
|
const isFinancial = currentType === '金融服务'
|
const isMovie = currentType === '观影服务'
|
const isSpouseEmployment = currentType === '配偶推荐就业'
|
const isApproval = currentType === '行政审批'
|
|
if (isHealthCheck) {
|
return [
|
{
|
level: '卓越人才卡',
|
enabled: true,
|
tag: '',
|
detail: '',
|
storeConsume: true,
|
discount: '',
|
governmentSubsidy: true,
|
consumeDesc: '',
|
consumeEnabled: true,
|
},
|
{
|
level: '杰出人才卡',
|
enabled: true,
|
tag: '',
|
detail: '',
|
storeConsume: true,
|
discount: '',
|
governmentSubsidy: true,
|
consumeDesc: '',
|
consumeEnabled: true,
|
},
|
{
|
level: '菁英人才卡',
|
enabled: true,
|
tag: '',
|
detail: '',
|
storeConsume: true,
|
discount: '',
|
governmentSubsidy: true,
|
consumeDesc: '',
|
consumeEnabled: true,
|
},
|
]
|
}
|
|
if (isFinancial) {
|
return [
|
{
|
level: '卓越人才卡',
|
enabled: true,
|
tag: '',
|
detail: '',
|
storeConsume: false,
|
discount: '',
|
governmentSubsidy: false,
|
consumeDesc: '',
|
consumeEnabled: false,
|
},
|
{
|
level: '杰出人才卡',
|
enabled: true,
|
tag: '',
|
detail: '',
|
storeConsume: false,
|
discount: '',
|
governmentSubsidy: false,
|
consumeDesc: '',
|
consumeEnabled: false,
|
},
|
{
|
level: '菁英人才卡',
|
enabled: true,
|
tag: '',
|
detail: '',
|
storeConsume: false,
|
discount: '',
|
governmentSubsidy: false,
|
consumeDesc: '',
|
consumeEnabled: false,
|
},
|
]
|
}
|
|
if (isMovie) {
|
return [
|
{
|
level: '卓越人才卡',
|
enabled: true,
|
tag: '',
|
detail: '',
|
storeConsume: false,
|
discount: '',
|
governmentSubsidy: false,
|
consumeDesc: '',
|
consumeEnabled: false,
|
},
|
{
|
level: '杰出人才卡',
|
enabled: true,
|
tag: '',
|
detail: '',
|
storeConsume: false,
|
discount: '',
|
governmentSubsidy: false,
|
consumeDesc: '',
|
consumeEnabled: false,
|
},
|
{
|
level: '菁英人才卡',
|
enabled: true,
|
tag: '',
|
detail: '',
|
storeConsume: false,
|
discount: '',
|
governmentSubsidy: false,
|
consumeDesc: '',
|
consumeEnabled: false,
|
},
|
]
|
}
|
|
if (isSpouseEmployment) {
|
return [
|
{
|
level: '卓越人才卡',
|
enabled: true,
|
tag: '',
|
detail: '',
|
storeConsume: false,
|
discount: '',
|
governmentSubsidy: false,
|
consumeDesc: '',
|
consumeEnabled: false,
|
},
|
{
|
level: '杰出人才卡',
|
enabled: true,
|
tag: '',
|
detail: '',
|
storeConsume: false,
|
discount: '',
|
governmentSubsidy: false,
|
consumeDesc: '',
|
consumeEnabled: false,
|
},
|
{
|
level: '菁英人才卡',
|
enabled: true,
|
tag: '',
|
detail: '',
|
storeConsume: false,
|
discount: '',
|
governmentSubsidy: false,
|
consumeDesc: '',
|
consumeEnabled: false,
|
},
|
]
|
}
|
|
if (isApproval) {
|
return []
|
}
|
|
if (isCarWash && form.miniProgramJumpType === 'miniprogram') {
|
return [
|
{
|
level: '卓越人才卡',
|
enabled: true,
|
tag: '',
|
detail: '',
|
storeConsume: false,
|
discount: '',
|
governmentSubsidy: false,
|
consumeDesc: '',
|
consumeEnabled: false,
|
},
|
{
|
level: '杰出人才卡',
|
enabled: true,
|
tag: '',
|
detail: '',
|
storeConsume: false,
|
discount: '',
|
governmentSubsidy: false,
|
consumeDesc: '',
|
consumeEnabled: false,
|
},
|
{
|
level: '菁英人才卡',
|
enabled: true,
|
tag: '',
|
detail: '',
|
storeConsume: false,
|
discount: '',
|
governmentSubsidy: false,
|
consumeDesc: '',
|
consumeEnabled: false,
|
},
|
]
|
}
|
|
if (isChildcare) {
|
return [
|
{
|
level: '卓越人才卡',
|
enabled: true,
|
tag: '',
|
detail: '',
|
storeConsume: false,
|
discount: '',
|
governmentSubsidy: false,
|
consumeDesc: '',
|
consumeEnabled: false,
|
},
|
{
|
level: '杰出人才卡',
|
enabled: true,
|
tag: '',
|
detail: '',
|
storeConsume: false,
|
discount: '',
|
governmentSubsidy: false,
|
consumeDesc: '',
|
consumeEnabled: false,
|
},
|
{
|
level: '菁英人才卡',
|
enabled: true,
|
tag: '',
|
detail: '',
|
storeConsume: false,
|
discount: '',
|
governmentSubsidy: false,
|
consumeDesc: '',
|
consumeEnabled: false,
|
},
|
]
|
}
|
|
if (isParking) {
|
return [
|
{
|
level: '卓越人才卡',
|
enabled: true,
|
tag: '',
|
detail: '',
|
storeConsume: false,
|
discount: '',
|
governmentSubsidy: false,
|
consumeDesc: '',
|
consumeEnabled: false,
|
parkingType: 'free',
|
parkingScope: 'all',
|
},
|
{
|
level: '杰出人才卡',
|
enabled: true,
|
tag: '',
|
detail: '',
|
storeConsume: false,
|
discount: '',
|
governmentSubsidy: false,
|
consumeDesc: '',
|
consumeEnabled: false,
|
parkingType: 'paid',
|
},
|
{
|
level: '菁英人才卡',
|
enabled: true,
|
tag: '',
|
detail: '',
|
storeConsume: false,
|
discount: '',
|
governmentSubsidy: false,
|
consumeDesc: '',
|
consumeEnabled: false,
|
parkingType: 'paid',
|
},
|
]
|
}
|
|
if (isHotel) {
|
return [
|
{
|
level: '卓越人才卡(外地)',
|
enabled: true,
|
tag: '',
|
detail: '',
|
storeConsume: true,
|
discount: '',
|
governmentSubsidy: true,
|
consumeDesc: '',
|
consumeEnabled: true,
|
freeVoucherEnabled: true,
|
freeVoucherTimes: '',
|
freeVoucherCycle: '每天',
|
freeVoucherSubsidy: true,
|
consumeType: '折扣',
|
consumeDiscount: '',
|
consumeDiscountSubsidy: true,
|
consumeCoupon: '',
|
consumeCouponSubsidy: true,
|
},
|
{
|
level: '卓越人才卡(本地)',
|
enabled: true,
|
tag: '',
|
detail: '',
|
storeConsume: true,
|
discount: '',
|
governmentSubsidy: true,
|
consumeDesc: '',
|
consumeEnabled: true,
|
freeVoucherEnabled: true,
|
freeVoucherTimes: '',
|
freeVoucherCycle: '每天',
|
freeVoucherSubsidy: true,
|
consumeType: '折扣',
|
consumeDiscount: '',
|
consumeDiscountSubsidy: true,
|
consumeCoupon: '',
|
consumeCouponSubsidy: true,
|
},
|
{
|
level: '杰出人才卡',
|
enabled: true,
|
tag: '',
|
detail: '',
|
storeConsume: true,
|
discount: '',
|
governmentSubsidy: true,
|
consumeDesc: '',
|
consumeEnabled: true,
|
freeVoucherEnabled: true,
|
freeVoucherTimes: '',
|
freeVoucherCycle: '每天',
|
freeVoucherSubsidy: true,
|
consumeType: '折扣',
|
consumeDiscount: '',
|
consumeDiscountSubsidy: true,
|
consumeCoupon: '',
|
consumeCouponSubsidy: true,
|
},
|
{
|
level: '菁英人才卡',
|
enabled: true,
|
tag: '',
|
detail: '',
|
storeConsume: true,
|
discount: '',
|
governmentSubsidy: true,
|
consumeDesc: '',
|
consumeEnabled: true,
|
freeVoucherEnabled: true,
|
freeVoucherTimes: '',
|
freeVoucherCycle: '每天',
|
freeVoucherSubsidy: true,
|
consumeType: '折扣',
|
consumeDiscount: '',
|
consumeDiscountSubsidy: true,
|
consumeCoupon: '',
|
consumeCouponSubsidy: true,
|
},
|
]
|
}
|
|
return [
|
{
|
level: '卓越人才卡',
|
enabled: true,
|
tag: '',
|
detail: '',
|
storeConsume: supportsDefaultStoreConsumption,
|
discount: '',
|
governmentSubsidy: supportsDefaultStoreConsumption,
|
consumeDesc: '',
|
consumeEnabled: supportsDefaultStoreConsumption,
|
},
|
{
|
level: '杰出人才卡',
|
enabled: true,
|
tag: '',
|
detail: '',
|
storeConsume: supportsDefaultStoreConsumption,
|
discount: '',
|
governmentSubsidy: supportsDefaultStoreConsumption,
|
consumeDesc: '',
|
consumeEnabled: supportsDefaultStoreConsumption,
|
},
|
{
|
level: '菁英人才卡',
|
enabled: true,
|
tag: '',
|
detail: '',
|
storeConsume: supportsDefaultStoreConsumption,
|
discount: '',
|
governmentSubsidy: supportsDefaultStoreConsumption,
|
consumeDesc: '',
|
consumeEnabled: supportsDefaultStoreConsumption,
|
},
|
{
|
level: '普通游客',
|
enabled: true,
|
tag: '',
|
detail: '',
|
storeConsume: false,
|
discount: '',
|
governmentSubsidy: false,
|
consumeDesc: '',
|
consumeEnabled: false,
|
},
|
]
|
}
|
|
const rightsConfigs = reactive<MerchantRightConfig[]>(defaultMerchantRights())
|
|
const rightExampleImage =
|
'https://images.unsplash.com/photo-1559847844-5315695dadae?auto=format&fit=crop&w=640&q=80'
|
const showRightsExamples = false
|
|
const rightExampleLines = [
|
'营业时间10:00-22:00',
|
'提供包间、免费停车、WiFi等服务',
|
'菜品以家常菜、特色川菜为主,具体以门店实际为准',
|
]
|
|
const locationDialogVisible = ref(false)
|
const pendingLongitude = ref('')
|
const pendingLatitude = ref('')
|
const locationSearch = ref('')
|
const locationSearching = ref(false)
|
const locationSearchResults = ref<Array<{ name: string; longitude: number; latitude: number }>>([])
|
const mapElement = ref<HTMLElement>()
|
let locationMap: any
|
let locationMarker: any
|
let amapApi: any
|
|
async function loadAMap() {
|
if (amapApi) return amapApi
|
const key = import.meta.env.VITE_AMAP_KEY?.trim()
|
const securityJsCode = import.meta.env.VITE_AMAP_SECURITY_CODE?.trim()
|
if (!key || !securityJsCode) throw new Error('AMap credentials are not configured')
|
;(window as any)._AMapSecurityConfig = { securityJsCode }
|
amapApi = await loadAMapApi({
|
key,
|
version: '2.0',
|
plugins: ['AMap.PlaceSearch', 'AMap.Scale', 'AMap.ToolBar'],
|
})
|
return amapApi
|
}
|
|
interface ParkingAddress {
|
name: string
|
address: string
|
longitude?: number
|
latitude?: number
|
}
|
interface SchoolAddress {
|
name: string
|
address: string
|
longitude?: number
|
latitude?: number
|
detail?: string
|
}
|
interface ApprovalItem {
|
name: string
|
content: string
|
address: string
|
phone: string
|
talentLevels: string[]
|
windowInfoUrl?: string
|
isNew?: boolean
|
isEditing?: boolean
|
}
|
|
const parkingAddresses = ref<ParkingAddress[]>([])
|
const activeParkingAddress = ref<ParkingAddress>()
|
const schoolAddresses = ref<SchoolAddress[]>([])
|
const activeSchoolAddress = ref<SchoolAddress>()
|
const schoolDialogVisible = ref(false)
|
const editingSchoolIndex = ref(-1)
|
const editingSchool = reactive<SchoolAddress>({ name: '', address: '', detail: '' })
|
const schoolFormRef = ref<FormInstance>()
|
const schoolRules: FormRules = {
|
name: [{ required: true, message: '请输入学校名称', trigger: 'blur' }],
|
address: [{ required: true, message: '请输入地址', trigger: 'blur' }],
|
}
|
const approvalItems = ref<ApprovalItem[]>([])
|
const approvalUploadIndex = ref(-1)
|
|
function addParkingAddress() {
|
parkingAddresses.value.push({ name: '', address: '' })
|
}
|
|
function removeParkingAddress(index: number) {
|
parkingAddresses.value.splice(index, 1)
|
}
|
|
async function markParkingLocation(row: ParkingAddress) {
|
activeSchoolAddress.value = undefined
|
activeParkingAddress.value = row
|
pendingLongitude.value = String(row.longitude || '')
|
pendingLatitude.value = String(row.latitude || '')
|
locationDialogVisible.value = true
|
locationSearch.value = row.address || ''
|
await initializeLocationMap()
|
}
|
|
function openSchoolDialog(index: number) {
|
editingSchoolIndex.value = index
|
if (index >= 0) {
|
Object.assign(editingSchool, schoolAddresses.value[index])
|
} else {
|
Object.assign(editingSchool, { name: '', address: '', detail: '', longitude: undefined, latitude: undefined })
|
}
|
schoolDialogVisible.value = true
|
}
|
|
async function saveSchool() {
|
const valid = await schoolFormRef.value?.validate().catch(() => false)
|
if (!valid) return
|
// if (!editingSchool.longitude || !editingSchool.latitude) { ElMessage.warning('请完成定位标点'); return }
|
|
if (editingSchoolIndex.value >= 0) {
|
Object.assign(schoolAddresses.value[editingSchoolIndex.value], editingSchool)
|
} else {
|
schoolAddresses.value.push({ ...editingSchool })
|
}
|
schoolDialogVisible.value = false
|
}
|
|
function removeSchool(index: number) {
|
schoolAddresses.value.splice(index, 1)
|
}
|
|
async function markSchoolLocation() {
|
activeParkingAddress.value = undefined
|
activeSchoolAddress.value = editingSchool
|
pendingLongitude.value = String(editingSchool.longitude || '')
|
pendingLatitude.value = String(editingSchool.latitude || '')
|
locationDialogVisible.value = true
|
locationSearch.value = editingSchool.address || ''
|
await initializeLocationMap()
|
}
|
|
function addApprovalRow() {
|
approvalItems.value.push({
|
name: '',
|
content: '',
|
address: '',
|
phone: '',
|
talentLevels: [],
|
windowInfoUrl: '',
|
isNew: true,
|
isEditing: true,
|
})
|
}
|
|
function saveApprovalRow(index: number) {
|
const item = approvalItems.value[index]
|
if (!item) return
|
if (!item.name.trim()) { ElMessage.warning('请输入名称'); return }
|
if (!item.content.trim()) { ElMessage.warning('请输入权益内容'); return }
|
if (!item.address.trim()) { ElMessage.warning('请输入地址'); return }
|
if (!item.phone.trim()) { ElMessage.warning('请输入电话'); return }
|
if (!item.talentLevels.length) { ElMessage.warning('请至少选择一个适用人才'); return }
|
item.isNew = false
|
item.isEditing = false
|
ElMessage.success('当前行已保存')
|
}
|
|
function editApprovalRow(index: number) {
|
const item = approvalItems.value[index]
|
if (!item) return
|
item.isEditing = true
|
}
|
|
function removeApproval(index: number) {
|
approvalItems.value.splice(index, 1)
|
}
|
|
function uploadApprovalWindow(index: number) {
|
approvalUploadIndex.value = index
|
changeCover('approvalWindow')
|
}
|
|
function addBusinessHour() {
|
businessHours.push({ id: Date.now(), value: [] })
|
}
|
|
function removeBusinessHour(index: number) {
|
if (businessHours.length <= 1) return
|
businessHours.splice(index, 1)
|
}
|
|
function businessHoursPayload() {
|
return businessHours
|
.map((item) => ({ startTime: item.value[0], endTime: item.value[1] }))
|
.filter((item) => item.startTime && item.endTime && item.startTime !== item.endTime)
|
}
|
|
function resetBusinessHours(hours?: Array<{ startTime: string; endTime: string }>) {
|
const normalized = (hours || []).filter((item) => item.startTime && item.endTime)
|
businessHours.splice(0, businessHours.length, ...(normalized.length > 0
|
? normalized.map((item, index) => ({ id: Date.now() + index, value: [item.startTime, item.endTime] }))
|
: [{ id: Date.now(), value: [] }]))
|
}
|
|
function selectedCoordinates() {
|
const longitudeText = pendingLongitude.value.trim()
|
const latitudeText = pendingLatitude.value.trim()
|
if (!longitudeText || !latitudeText) return undefined
|
const longitude = Number(longitudeText)
|
const latitude = Number(latitudeText)
|
return Number.isFinite(longitude) && Number.isFinite(latitude) ? { longitude, latitude } : undefined
|
}
|
|
function setMapMarker(latitude: number, longitude: number, center = true) {
|
pendingLongitude.value = longitude.toFixed(6)
|
pendingLatitude.value = latitude.toFixed(6)
|
if (!locationMap) return
|
const position = [longitude, latitude]
|
if (!locationMarker) {
|
locationMarker = new amapApi.Marker({ position, anchor: 'bottom-center' })
|
locationMap.add(locationMarker)
|
} else {
|
locationMarker.setPosition(position)
|
}
|
if (center) locationMap.setZoomAndCenter(Math.max(locationMap.getZoom(), 16), position)
|
}
|
|
async function initializeLocationMap() {
|
await nextTick()
|
if (!mapElement.value || locationMap) return
|
try {
|
const AMap = await loadAMap()
|
const selected = selectedCoordinates()
|
const initial = selected || { longitude: 101.718637, latitude: 26.582347 }
|
locationMap = new AMap.Map(mapElement.value, {
|
viewMode: '2D',
|
zoom: selected ? 16 : 13,
|
center: [initial.longitude, initial.latitude],
|
resizeEnable: true,
|
})
|
locationMap.addControl(new AMap.Scale())
|
locationMap.addControl(new AMap.ToolBar({ position: { right: '10px', top: '10px' } }))
|
locationMap.on('click', (event: any) => setMapMarker(event.lnglat.getLat(), event.lnglat.getLng(), false))
|
if (selected) setMapMarker(initial.latitude, initial.longitude, false)
|
window.setTimeout(() => locationMap?.resize(), 120)
|
} catch (error) {
|
console.error('Failed to initialize AMap', error)
|
ElMessage.error('高德地图加载失败,请检查地图配置或网络后重试')
|
}
|
}
|
|
async function searchMapLocation() {
|
const keyword = locationSearch.value.trim()
|
if (!keyword) { ElMessage.warning('请输入地址或地点关键词'); return }
|
locationSearching.value = true
|
try {
|
const AMap = await loadAMap()
|
const placeSearch = new AMap.PlaceSearch({ city: '攀枝花', citylimit: false, pageSize: 6, pageIndex: 1, extensions: 'base' })
|
const records = await new Promise<any[]>((resolve, reject) => {
|
placeSearch.search(keyword, (status: string, result: any) => {
|
if (status === 'complete') resolve(result?.poiList?.pois || [])
|
else if (status === 'no_data') resolve([])
|
else reject(new Error(result?.info || 'place search failed'))
|
})
|
})
|
locationSearchResults.value = records.map((item) => {
|
const longitude = Number(item.location?.getLng?.() ?? item.location?.lng)
|
const latitude = Number(item.location?.getLat?.() ?? item.location?.lat)
|
const address = [item.name, item.address].filter(Boolean).join(' - ')
|
return { name: address, longitude, latitude }
|
}).filter((item) => Number.isFinite(item.longitude) && Number.isFinite(item.latitude))
|
if (!locationSearchResults.value.length) ElMessage.info('未找到匹配地点,请调整关键词或直接点击地图标点')
|
} catch (error) {
|
console.error('Failed to search AMap places', error)
|
ElMessage.error('地点搜索失败,请检查网络后重试')
|
} finally { locationSearching.value = false }
|
}
|
|
function selectSearchLocation(item: { name: string; longitude: number; latitude: number }) {
|
setMapMarker(item.latitude, item.longitude)
|
if (!form.address?.trim()) form.address = item.name
|
locationSearchResults.value = []
|
}
|
|
function closeLocationMap() {
|
locationMap?.destroy()
|
locationMap = undefined
|
locationMarker = undefined
|
locationSearchResults.value = []
|
}
|
|
async function markLocation() {
|
activeParkingAddress.value = undefined
|
activeSchoolAddress.value = undefined
|
pendingLongitude.value = String(form.longitude || '')
|
pendingLatitude.value = String(form.latitude || '')
|
locationDialogVisible.value = true
|
locationSearch.value = form.address || ''
|
await initializeLocationMap()
|
}
|
|
function saveLocation() {
|
const longitude = Number(pendingLongitude.value)
|
const latitude = Number(pendingLatitude.value)
|
if (!Number.isFinite(longitude) || longitude < -180 || longitude > 180 || !Number.isFinite(latitude) || latitude < -90 || latitude > 90) {
|
ElMessage.warning('请输入有效的经纬度')
|
return
|
}
|
if (activeParkingAddress.value) {
|
activeParkingAddress.value.longitude = longitude
|
activeParkingAddress.value.latitude = latitude
|
} else if (activeSchoolAddress.value) {
|
activeSchoolAddress.value.longitude = longitude
|
activeSchoolAddress.value.latitude = latitude
|
} else {
|
form.longitude = longitude
|
form.latitude = latitude
|
}
|
locationDialogVisible.value = false
|
closeLocationMap()
|
ElMessage.success('地图位置已标记')
|
}
|
|
function changeCover(target: 'merchant' | 'productCover' | 'productImage' | 'approvalWindow') {
|
imageUploadTarget.value = target
|
if (imageInputRef.value) imageInputRef.value.accept = target === 'approvalWindow' ? 'application/pdf,.pdf' : 'image/jpeg,image/png,image/webp'
|
imageInputRef.value?.click()
|
}
|
|
function resetMerchantCoverPreview() {
|
merchantCoverPreviewFailed.value = false
|
}
|
|
function resetProductCoverPreview() {
|
productCoverPreviewFailed.value = false
|
}
|
|
function removeProductImage(index: number) {
|
productImages.value.splice(index, 1)
|
}
|
|
async function uploadSelectedImage(event: Event) {
|
const input = event.target as HTMLInputElement
|
const file = input.files?.[0]
|
input.value = ''
|
if (!file) return
|
|
if (imageUploadTarget.value === 'approvalWindow') {
|
const isPdf = file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')
|
if (!isPdf) {
|
ElMessage.warning('窗口信息只能上传 PDF 文件')
|
return
|
}
|
if (file.size > 20 * 1024 * 1024) {
|
ElMessage.warning('PDF 文件大小不能超过 20 MB')
|
return
|
}
|
imageUploading.value = true
|
try {
|
const result = await uploadAdminPdf(file)
|
if (approvalUploadIndex.value >= 0) approvalItems.value[approvalUploadIndex.value].windowInfoUrl = result.url
|
ElMessage.success('窗口信息上传成功')
|
} catch {
|
// The shared HTTP interceptor displays the upload error.
|
} finally {
|
approvalUploadIndex.value = -1
|
imageUploading.value = false
|
}
|
return
|
}
|
|
const acceptedTypes = ['image/jpeg', 'image/png', 'image/webp']
|
if (!acceptedTypes.includes(file.type)) {
|
ElMessage.warning('请选择 JPG、PNG 或 WEBP 格式的图片')
|
return
|
}
|
if (file.size > 10 * 1024 * 1024) {
|
ElMessage.warning('图片大小不能超过 10 MB')
|
return
|
}
|
|
imageUploading.value = true
|
try {
|
const result = await uploadAdminImage(file)
|
if (imageUploadTarget.value === 'merchant') {
|
form.coverUrl = result.url
|
resetMerchantCoverPreview()
|
merchantFormRef.value?.validateField('coverUrl').catch(() => undefined)
|
} else if (imageUploadTarget.value === 'productCover') {
|
productForm.coverUrl = result.url
|
resetProductCoverPreview()
|
productFormRef.value?.validateField('coverUrl').catch(() => undefined)
|
} else if (!productImages.value.includes(result.url)) {
|
productImages.value.push(result.url)
|
}
|
ElMessage.success('图片上传成功')
|
} catch {
|
// The shared HTTP interceptor displays the upload error.
|
} finally {
|
approvalUploadIndex.value = -1
|
imageUploading.value = false
|
}
|
}
|
|
function resetMerchantRights(config: unknown) {
|
const savedRights = parseJsonValue<MerchantRightConfig[]>(config, [])
|
if (isCarWashMerchant.value) {
|
const savedCarWashJump = Array.isArray(savedRights)
|
? savedRights.find((entry) => entry?.externalLinkType || entry?.externalMiniProgramShortLink || entry?.externalMiniProgramPath || entry?.externalWebUrl)
|
: undefined
|
if (savedCarWashJump) {
|
form.miniProgramJumpType = savedCarWashJump.externalLinkType === 'miniProgram' || String(savedCarWashJump.externalMiniProgramShortLink || '').trim()
|
? 'miniprogram'
|
: 'normal'
|
form.miniProgramJumpUrl = String(savedCarWashJump.externalMiniProgramShortLink || '').trim()
|
} else {
|
form.miniProgramJumpType = 'normal'
|
form.miniProgramJumpUrl = ''
|
}
|
}
|
rightsConfigs.splice(0, rightsConfigs.length, ...defaultMerchantRights().map((item) => {
|
const saved = Array.isArray(savedRights) ? savedRights.find((entry) => entry?.level === item.level) : undefined
|
return {
|
...item,
|
...(saved || {}),
|
consumeEnabled: item.consumeEnabled
|
}
|
}))
|
syncCarWashJumpMeta()
|
}
|
|
function resetCarWashJumpFromMerchant(merchant: Record<string, any>) {
|
if (!isCarWashMerchant.value) return
|
const savedRights = parseJsonValue<MerchantRightConfig[]>(merchant.merchantRightsConfig, [])
|
const hasNewCarWashJump = Array.isArray(savedRights)
|
&& savedRights.some((entry) => entry?.externalLinkType || entry?.externalMiniProgramShortLink || entry?.externalMiniProgramPath || entry?.externalWebUrl)
|
if (hasNewCarWashJump) return
|
const type = String(merchant.carWashExternalLinkType || '').trim()
|
const shortLink = String(merchant.carWashExternalMiniProgramShortLink || '').trim()
|
if (type === 'miniProgram' && shortLink) {
|
form.miniProgramJumpType = 'miniprogram'
|
form.miniProgramJumpUrl = shortLink
|
syncCarWashJumpMeta()
|
}
|
}
|
|
function serializeMerchantRightsConfig() {
|
return rightsConfigs.map((item, index) => {
|
const payload = { ...item }
|
if (!supportsStoreConsumption.value) {
|
payload.storeConsume = false
|
payload.discount = ''
|
payload.governmentSubsidy = false
|
payload.consumeDesc = ''
|
payload.consumeEnabled = false
|
payload.consumeType = ''
|
payload.consumeDiscount = ''
|
payload.consumeDiscountSubsidy = false
|
payload.consumeCoupon = ''
|
payload.consumeCouponSubsidy = false
|
}
|
if (isCarWashMerchant.value) {
|
if (index === 0) {
|
Object.assign(payload, carWashJumpMeta())
|
} else {
|
payload.externalLinkType = ''
|
payload.externalMiniProgramShortLink = ''
|
payload.externalMiniProgramPath = ''
|
payload.externalWebUrl = ''
|
}
|
}
|
return payload
|
})
|
}
|
|
async function saveRights() {
|
const merchantId = currentMerchantId()
|
if (!merchantId) {
|
ElMessage.warning('请先保存商家设置')
|
return
|
}
|
|
if (isParkingMerchant.value) {
|
if (parkingAddresses.value.length === 0) {
|
ElMessage.warning('请至少添加一个停车场')
|
return
|
}
|
const names = parkingAddresses.value.map(p => p.name?.trim()).filter(Boolean)
|
if (names.length !== parkingAddresses.value.length) {
|
ElMessage.warning('停车场名称不能为空')
|
return
|
}
|
if (new Set(names).size !== names.length) {
|
ElMessage.warning('停车场名称不能重复')
|
return
|
}
|
const addresses = parkingAddresses.value.map(p => p.address?.trim()).filter(Boolean)
|
if (addresses.length !== parkingAddresses.value.length) {
|
ElMessage.warning('停车场地址不能为空')
|
return
|
}
|
const unlocated = parkingAddresses.value.filter(p => !p.latitude || !p.longitude)
|
if (unlocated.length > 0) {
|
ElMessage.warning('请完成所有停车场的标点')
|
return
|
}
|
}
|
|
if (isChildcareMerchant.value && schoolAddresses.value.length > 0) {
|
const names = schoolAddresses.value.map(s => s.name?.trim()).filter(Boolean)
|
if (names.length !== schoolAddresses.value.length) {
|
ElMessage.warning('学校名称不能为空')
|
return
|
}
|
const addresses = schoolAddresses.value.map(s => s.address?.trim()).filter(Boolean)
|
if (addresses.length !== schoolAddresses.value.length) {
|
ElMessage.warning('学校地址不能为空')
|
return
|
}
|
const unlocated = schoolAddresses.value.filter(s => !s.latitude || !s.longitude)
|
if (unlocated.length > 0) {
|
ElMessage.warning('请完成所有学校地址的标点')
|
return
|
}
|
}
|
|
if (isApprovalMerchant.value && approvalItems.value.length > 0) {
|
const invalidItem = approvalItems.value.find(item => (
|
!item.name?.trim()
|
|| !item.content?.trim()
|
|| !item.address?.trim()
|
|| !item.phone?.trim()
|
|| !item.talentLevels?.length
|
))
|
if (invalidItem) {
|
ElMessage.warning('请完善行政审批权益表格信息')
|
return
|
}
|
}
|
|
if (isCarWashMerchant.value && form.miniProgramJumpType === 'miniprogram' && !String(form.miniProgramJumpUrl || '').trim()) {
|
ElMessage.warning('请输入小程序跳转链接')
|
return
|
}
|
|
rightsSaving.value = true
|
try {
|
await updateMerchant(merchantId, {
|
merchantName: form.merchantName,
|
merchantType: form.merchantType,
|
contactName: form.contactName,
|
contactMobile: form.contactMobile,
|
address: form.address,
|
coverUrl: form.coverUrl,
|
businessStatus: form.businessStatus,
|
longitude: Number(form.longitude || 0) || undefined,
|
latitude: Number(form.latitude || 0) || undefined,
|
canLoginMerchantApp: Number(form.canLoginMerchantApp ?? 1),
|
groupBuyingEnabled: form.groupBuyingEnabled,
|
usageInstructions: isParkingMerchant.value
|
? JSON.stringify(parkingAddresses.value)
|
: isChildcareMerchant.value
|
? JSON.stringify(schoolAddresses.value)
|
: isApprovalMerchant.value
|
? JSON.stringify(approvalItems.value)
|
: form.usageInstructions,
|
rightsText: form.rightsText,
|
merchantRightsConfig: serializeMerchantRightsConfig(),
|
serviceLabel: form.serviceLabel,
|
businessHours: businessHoursPayload(),
|
recommend: form.recommend,
|
sortNo: Number(form.sortNo || 0),
|
status: form.status,
|
})
|
ElMessage.success('商家权益已保存')
|
} finally {
|
rightsSaving.value = false
|
}
|
}
|
|
function currentMerchantId() {
|
return queryString('merchantId')
|
}
|
|
function productAuditStatusText(status?: number) {
|
if (status === 2) return '审核通过'
|
if (status === 3) return '已驳回'
|
return '待审核'
|
}
|
|
function canToggleProductStatus(row: Product) {
|
return row.auditStatus === 2
|
}
|
|
function productToggleTip(row: Product) {
|
return canToggleProductStatus(row) ? '' : '审核通过后可上下架'
|
}
|
|
function productAmountText(row: Product) {
|
if (isHotelMerchant.value) {
|
const min = row.salePrice
|
const max = row.originalPrice
|
if (min !== undefined && max !== undefined) {
|
return `${Number(min).toFixed(2).replace(/\.00$/, '')} - ${Number(max).toFixed(2).replace(/\.00$/, '')}`
|
}
|
}
|
const amount = row.salePrice ?? row.originalPrice
|
if (amount === undefined || amount === null) return ''
|
return Number(amount).toFixed(2).replace(/\.00$/, '')
|
}
|
|
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 rangeValues(value: string, pattern: RegExp) {
|
return (value.match(pattern) || []).slice(0, 2)
|
}
|
|
function rangeText(value: string[]) {
|
return value.length === 2 && value[0] && value[1] ? `${value[0]} 至 ${value[1]}` : ''
|
}
|
|
function resetProductRights(config?: Product['rightsConfig']) {
|
const next = parseJsonValue(config, defaultProductRights())
|
productRights.splice(0, productRights.length, ...defaultProductRights().map((item) => {
|
const saved = Array.isArray(next) ? next.find((entry) => entry.level === item.level) : undefined
|
return { ...item, ...(saved || {}) }
|
}))
|
}
|
|
function resetProductForm(row?: Product) {
|
editingProductId.value = row?.id
|
const notice = parseJsonValue<Record<string, string>>(row?.purchaseNotice, {})
|
productForm.merchantId = row?.merchantId || currentMerchantId()
|
productForm.productType = row?.productType || 'GROUP'
|
productForm.productName = row?.productName || ''
|
productForm.coverUrl = row?.coverUrl || ''
|
const savedImages = parseJsonValue<unknown>(row?.productImages, [])
|
productImages.value = Array.isArray(savedImages)
|
? savedImages.filter((image): image is string => typeof image === 'string' && image.length > 0)
|
: []
|
productForm.salePrice = isHealthCheckMerchant.value ? undefined : row?.salePrice
|
productForm.originalPrice = isHealthCheckMerchant.value ? undefined : row?.originalPrice
|
productForm.stock = row?.stock === -1 ? undefined : row?.stock
|
productForm.detailText = row?.detailText || ''
|
productForm.benefitDesc = row?.benefitDesc || ''
|
productForm.status = row?.status ?? 1
|
productForm.sortNo = row?.sortNo ?? 0
|
productForm.createdAt = row?.createdAt || ''
|
productForm.updatedAt = row?.submitTime || row?.createdAt || ''
|
productNoticeForm.validPeriod = notice.validPeriod || ''
|
productNoticeForm.useMethod = notice.useMethod || ''
|
productNoticeForm.availableTime = notice.availableTime || ''
|
productNoticeForm.otherRules = notice.otherRules || notice.otherRule || ''
|
productValidPeriodRange.value = rangeValues(productNoticeForm.validPeriod, /\d{4}-\d{2}-\d{2}/g)
|
productAvailableTimeRange.value = rangeValues(productNoticeForm.availableTime, /\d{2}:\d{2}/g)
|
if (isHealthCheckMerchant.value) {
|
const parsed = parseJsonValue<HealthCheckItem[]>(row?.detailText, [])
|
healthCheckItems.value = parsed.length ? parsed : [{ name: '', meaning: '', gender: ['男'] }]
|
}
|
resetProductRights(row?.rightsConfig)
|
}
|
|
async function loadProducts() {
|
const merchantId = currentMerchantId()
|
if (!merchantId) {
|
merchantProducts.value = []
|
productPagination.total = 0
|
return
|
}
|
|
productLoading.value = true
|
try {
|
const result = await getAdminProducts({
|
page: productPagination.page,
|
size: productPagination.size,
|
merchantId,
|
keyword: productFilters.productName || undefined,
|
status: productFilters.status,
|
createdStart: productFilters.createdTime?.[0],
|
createdEnd: productFilters.createdTime?.[1],
|
})
|
merchantProducts.value = result.records
|
productPagination.total = result.total
|
} finally {
|
productLoading.value = false
|
}
|
}
|
|
function searchProducts() {
|
productPagination.page = 1
|
loadProducts()
|
}
|
|
function resetProductFilters() {
|
productFilters.productName = ''
|
productFilters.status = undefined
|
productFilters.createdTime = []
|
searchProducts()
|
}
|
|
function editProduct(row: Product) {
|
resetProductForm(row)
|
productMode.value = 'form'
|
}
|
|
function openProductCreate() {
|
// if (!currentMerchantId()) {
|
// ElMessage.warning('请先保存商家信息')
|
// return
|
// }
|
resetProductForm()
|
productMode.value = 'form'
|
}
|
|
function cancelProductForm() {
|
productMode.value = 'list'
|
}
|
|
function validExceedDiscount(value: unknown) {
|
const text = String(value ?? '').trim()
|
if (!/^\d+(?:\.\d)?$/.test(text)) return false
|
const amount = Number(text)
|
return amount > 0 && amount <= 10
|
}
|
|
function validateProductExceedDiscount(item: { level: string, exceedDiscount: unknown }) {
|
const key = item.level
|
if (validExceedDiscount(item.exceedDiscount)) {
|
delete productExceedDiscountErrors[key]
|
return true
|
}
|
productExceedDiscountErrors[key] = '请输入大于 0 且不超过 10 的一位小数'
|
return false
|
}
|
|
function validFreeLimitCount(value: unknown) {
|
const text = String(value ?? '').trim()
|
if (!/^\d+$/.test(text)) return false
|
const count = Number(text)
|
return count >= 1 && count <= 999
|
}
|
|
async function saveProduct() {
|
const valid = await productFormRef.value?.validate().catch(() => false)
|
if (!valid) return
|
if (isHealthCheckMerchant.value) {
|
if (!productValidPeriodRange.value || productValidPeriodRange.value.length !== 2) {
|
ElMessage.warning('请选择可预约时间')
|
return
|
}
|
if (healthCheckItems.value.length === 0) {
|
ElMessage.warning('请至少添加一项检查项目')
|
return
|
}
|
for (const item of healthCheckItems.value) {
|
if (!item.name?.trim() || !item.meaning?.trim() || item.gender.length === 0) {
|
ElMessage.warning('请完善检查项目信息(名称、意义、适用性别均不能为空)')
|
return
|
}
|
}
|
} else if (!isHotelMerchant.value) {
|
if (productRights.some((item) => item.free && !validFreeLimitCount(item.limitCount))) {
|
ElMessage.warning('免费次数请输入 1 至 999 的整数,999 表示不限次数')
|
return
|
}
|
if (productRights.some((item) => item.free && !validateProductExceedDiscount(item))) {
|
return
|
}
|
}
|
productSaving.value = true
|
try {
|
const payload = {
|
...productForm,
|
merchantId: currentMerchantId(),
|
detailText: isHealthCheckMerchant.value ? JSON.stringify(healthCheckItems.value) : productForm.detailText,
|
salePrice: isHealthCheckMerchant.value || productForm.salePrice === undefined ? undefined : Number(productForm.salePrice),
|
originalPrice: isHealthCheckMerchant.value || productForm.originalPrice === undefined ? undefined : Number(productForm.originalPrice),
|
stock: productForm.stock === undefined ? undefined : Number(productForm.stock),
|
sortNo: productForm.sortNo === undefined ? 0 : Number(productForm.sortNo),
|
purchaseNotice: {
|
...productNoticeForm,
|
validPeriod: rangeText(productValidPeriodRange.value),
|
availableTime: rangeText(productAvailableTimeRange.value),
|
},
|
productImages: [...productImages.value],
|
rightsConfig: isHealthCheckMerchant.value || isHotelMerchant.value
|
? []
|
: productRights.map((item) => ({ ...item })),
|
}
|
if (editingProductId.value) {
|
await updateProduct(editingProductId.value, payload)
|
} else {
|
await createProduct(payload)
|
}
|
ElMessage.success('团单已提交审核')
|
productMode.value = 'list'
|
await loadProducts()
|
} finally {
|
productSaving.value = false
|
}
|
}
|
|
async function offlineProduct(row: Product) {
|
if (!canToggleProductStatus(row)) {
|
ElMessage.warning('审核通过后才可以上下架')
|
return
|
}
|
const nextStatus = row.status === 0 ? 1 : 0
|
await updateProductStatus(row.id, nextStatus)
|
ElMessage.success(nextStatus === 0 ? '团单已下架' : '团单已上架')
|
await loadProducts()
|
}
|
|
async function deleteProduct(row: Product) {
|
await ElMessageBox.confirm('确认删除该团单?', '删除团单', {
|
confirmButtonText: '确定',
|
cancelButtonText: '取消',
|
type: 'warning',
|
})
|
await deleteProductApi(row.id)
|
ElMessage.success('团单已删除')
|
await loadProducts()
|
}
|
|
function couponConditionText(row: Coupon) {
|
if (row.conditionType === 'NONE') return '不限'
|
const amount = row.conditionAmount === undefined || row.conditionAmount === null ? '' : Number(row.conditionAmount).toString()
|
return amount ? `满${amount}元可用` : '满'
|
}
|
|
function couponAmountText(row: Coupon) {
|
if (row.couponAmount === undefined || row.couponAmount === null || Number(row.couponAmount) === 0) return ''
|
return Number(row.couponAmount).toFixed(2).replace(/\.00$/, '')
|
}
|
|
function couponTalentText(row: Coupon) {
|
return row.talentLevels || ''
|
}
|
|
function couponProductText(row: Coupon) {
|
if (row.productScope === 'ALL') return '不限'
|
return row.productName || ''
|
}
|
|
function couponExpiredText(row: Coupon) {
|
if (row.validStart && row.validEnd) return `${row.validStart} 至 ${row.validEnd}`
|
return row.validEnd || row.validStart || ''
|
}
|
|
function couponStatusText(row: Coupon) {
|
return row.status === 0 ? '停用' : '启用'
|
}
|
|
async function loadCoupons() {
|
const merchantId = currentMerchantId()
|
if (!merchantId) {
|
merchantCoupons.value = []
|
couponPagination.total = 0
|
return
|
}
|
|
couponLoading.value = true
|
try {
|
const result = await getAdminCoupons({
|
page: couponPagination.page,
|
size: couponPagination.size,
|
merchantId,
|
keyword: couponFilters.couponName || undefined,
|
status: couponFilters.status,
|
createdStart: couponFilters.createdTime?.[0],
|
createdEnd: couponFilters.createdTime?.[1],
|
})
|
merchantCoupons.value = result.records
|
couponPagination.total = result.total
|
} finally {
|
couponLoading.value = false
|
}
|
}
|
|
async function loadCouponProducts() {
|
const merchantId = currentMerchantId()
|
if (!merchantId) {
|
couponProducts.value = []
|
return
|
}
|
const result = await getAdminProducts({ page: 1, size: 100, merchantId })
|
couponProducts.value = result.records
|
}
|
|
function searchCoupons() {
|
couponPagination.page = 1
|
loadCoupons()
|
}
|
|
function resetCouponFilters() {
|
couponFilters.couponName = ''
|
couponFilters.status = undefined
|
couponFilters.createdTime = []
|
searchCoupons()
|
}
|
|
function resetCouponForm() {
|
editingCouponId.value = undefined
|
couponEditForm.couponName = ''
|
couponEditForm.useRequirement = 'full'
|
couponEditForm.fullAmount = ''
|
couponEditForm.couponAmount = ''
|
couponEditForm.talentLevels = ['卓越人才卡', '杰出人才卡', '菁英人才卡']
|
couponEditForm.validTime = []
|
couponEditForm.productScope = 'selected'
|
couponEditForm.storeConsume = true
|
couponEditForm.governmentSubsidy = true
|
couponEditForm.status = 1
|
selectedCouponProductIds.value = []
|
Object.keys(couponLevelQuantities).forEach((key) => delete couponLevelQuantities[key])
|
couponEditForm.talentLevels.forEach((level) => { couponLevelQuantities[level] = 1 })
|
}
|
|
function parseTalentLevels(value?: string) {
|
return value ? value.split(',').filter(Boolean) : []
|
}
|
|
async function editCoupon(row: Coupon) {
|
const detail = await getAdminCouponDetail(row.id)
|
editingCouponId.value = detail.id
|
couponEditForm.couponName = detail.couponName || ''
|
couponEditForm.useRequirement = detail.conditionType === 'NONE' ? 'none' : 'full'
|
couponEditForm.fullAmount =
|
detail.conditionAmount === undefined || detail.conditionAmount === null ? '' : String(detail.conditionAmount)
|
couponEditForm.couponAmount =
|
detail.couponAmount === undefined || detail.couponAmount === null ? '' : String(detail.couponAmount)
|
couponEditForm.talentLevels = parseTalentLevels(detail.talentLevels)
|
Object.keys(couponLevelQuantities).forEach((key) => delete couponLevelQuantities[key])
|
let savedQuantities: Record<string, number> = {}
|
try { savedQuantities = typeof detail.talentLevelQuantities === 'string' ? JSON.parse(detail.talentLevelQuantities) : (detail.talentLevelQuantities || {}) } catch { savedQuantities = {} }
|
couponEditForm.talentLevels.forEach((level) => { couponLevelQuantities[level] = Number(savedQuantities[level] || 1) })
|
couponEditForm.validTime = detail.validStart && detail.validEnd ? [detail.validStart, detail.validEnd] : []
|
couponEditForm.productScope = detail.productScope === 'ALL' ? 'all' : 'selected'
|
couponEditForm.storeConsume = detail.storeConsume === true || detail.storeConsume === 1
|
couponEditForm.governmentSubsidy = detail.governmentSubsidy === true || detail.governmentSubsidy === 1
|
couponEditForm.status = detail.status ?? 1
|
selectedCouponProductIds.value = detail.productIds || []
|
await loadCouponProducts()
|
couponMode.value = 'edit'
|
}
|
|
async function openCouponCreate() {
|
resetCouponForm()
|
await loadCouponProducts()
|
couponMode.value = 'edit'
|
}
|
|
async function disableCoupon(row: Coupon) {
|
const nextStatus = row.status === 0 ? 1 : 0
|
await updateCouponStatus(row.id, nextStatus)
|
ElMessage.success(nextStatus === 0 ? '优惠券已停用' : '优惠券已启用')
|
await loadCoupons()
|
}
|
|
async function deleteCoupon(row: Coupon) {
|
await ElMessageBox.confirm('确认删除该优惠券?', '删除优惠券', {
|
confirmButtonText: '确定',
|
cancelButtonText: '取消',
|
type: 'warning',
|
})
|
await deleteCouponApi(row.id)
|
ElMessage.success('优惠券已删除')
|
await loadCoupons()
|
}
|
|
function couponProductSelected(row: Product) {
|
return selectedCouponProductIds.value.some((id) => String(id) === String(row.id))
|
}
|
|
function toggleCouponProduct(row: Product, checked: boolean) {
|
if (checked) {
|
if (!couponProductSelected(row)) {
|
selectedCouponProductIds.value.push(row.id)
|
}
|
couponFormRef.value?.validateField('productScope').catch(() => undefined)
|
return
|
}
|
selectedCouponProductIds.value = selectedCouponProductIds.value.filter((id) => String(id) !== String(row.id))
|
couponFormRef.value?.validateField('productScope').catch(() => undefined)
|
}
|
|
function toggleCouponTalentLevel(level: string, enabled: boolean) {
|
if (enabled) {
|
if (!couponEditForm.talentLevels.includes(level)) couponEditForm.talentLevels.push(level)
|
if (!couponLevelQuantities[level]) couponLevelQuantities[level] = 1
|
return
|
}
|
couponEditForm.talentLevels = couponEditForm.talentLevels.filter((item) => item !== level)
|
}
|
|
function couponPayload(): CouponPayload {
|
return {
|
merchantId: currentMerchantId(),
|
couponName: couponEditForm.couponName,
|
conditionType: couponEditForm.useRequirement === 'none' ? 'NONE' : 'FULL',
|
conditionAmount:
|
couponEditForm.useRequirement === 'none' || couponEditForm.fullAmount === ''
|
? undefined
|
: Number(couponEditForm.fullAmount),
|
couponAmount: couponEditForm.couponAmount === '' ? undefined : Number(couponEditForm.couponAmount),
|
talentLevels: couponEditForm.talentLevels,
|
talentLevelQuantities: Object.fromEntries(couponEditForm.talentLevels.map((level) => [level, Number(couponLevelQuantities[level] || 1)])),
|
validStart: couponEditForm.validTime?.[0],
|
validEnd: couponEditForm.validTime?.[1],
|
productScope: couponEditForm.productScope === 'all' ? 'ALL' : 'SELECTED',
|
productIds: couponEditForm.productScope === 'all' ? [] : selectedCouponProductIds.value,
|
storeConsume: couponEditForm.storeConsume,
|
governmentSubsidy: couponEditForm.governmentSubsidy,
|
status: couponEditForm.status,
|
}
|
}
|
|
async function saveCoupon() {
|
const valid = await couponFormRef.value?.validate().catch(() => false)
|
if (!valid) return
|
couponSaving.value = true
|
try {
|
const payload = couponPayload()
|
if (editingCouponId.value) {
|
await updateCoupon(editingCouponId.value, payload)
|
} else {
|
await createCouponApi(payload)
|
}
|
} finally {
|
couponSaving.value = false
|
}
|
ElMessage.success('优惠券已保存')
|
couponMode.value = 'list'
|
await loadCoupons()
|
}
|
|
function cancelCoupon() {
|
couponMode.value = 'list'
|
}
|
|
async function saveMerchant() {
|
const merchantValid = await merchantFormRef.value?.validate().catch(() => false)
|
if (!merchantValid) return
|
if (isServiceMerchant.value && rightsConfigs.some((item) => item.storeConsume && !validExceedDiscount(item.discount))) {
|
ElMessage.warning('折扣请输入大于 0 且不超过 10 的一位小数')
|
return
|
}
|
const updatingMerchantPassword = isEditMode.value && accountForm.loginPassword.length > 0
|
const accountValid = await accountFormRef.value?.validate().catch(() => false)
|
if (!accountValid) return
|
if (requiresMerchantOperator.value) {
|
if (!(await checkLoginAccountAvailable())) return
|
}
|
|
if (isParkingMerchant.value && parkingAddresses.value.length > 0) {
|
const names = parkingAddresses.value.map(p => p.name?.trim()).filter(Boolean)
|
if (names.length !== parkingAddresses.value.length) {
|
ElMessage.warning('停车场名称不能为空')
|
return
|
}
|
if (new Set(names).size !== names.length) {
|
ElMessage.warning('停车场名称不能重复')
|
return
|
}
|
const addresses = parkingAddresses.value.map(p => p.address?.trim()).filter(Boolean)
|
if (addresses.length !== parkingAddresses.value.length) {
|
ElMessage.warning('停车场地址不能为空')
|
return
|
}
|
const unlocated = parkingAddresses.value.filter(p => !p.latitude || !p.longitude)
|
if (unlocated.length > 0) {
|
ElMessage.warning('请完成所有停车场的标点')
|
return
|
}
|
}
|
|
if (isChildcareMerchant.value && schoolAddresses.value.length > 0) {
|
const names = schoolAddresses.value.map(s => s.name?.trim()).filter(Boolean)
|
if (names.length !== schoolAddresses.value.length) {
|
ElMessage.warning('学校名称不能为空')
|
return
|
}
|
const addresses = schoolAddresses.value.map(s => s.address?.trim()).filter(Boolean)
|
if (addresses.length !== schoolAddresses.value.length) {
|
ElMessage.warning('学校地址不能为空')
|
return
|
}
|
const unlocated = schoolAddresses.value.filter(s => !s.latitude || !s.longitude)
|
if (unlocated.length > 0) {
|
ElMessage.warning('请完成所有学校地址的标点')
|
return
|
}
|
}
|
|
if (isApprovalMerchant.value && approvalItems.value.length > 0) {
|
const invalidItem = approvalItems.value.find(item => (
|
!item.name?.trim()
|
|| !item.content?.trim()
|
|| !item.address?.trim()
|
|| !item.phone?.trim()
|
|| !item.talentLevels?.length
|
))
|
if (invalidItem) {
|
ElMessage.warning('请完善行政审批权益表格信息')
|
return
|
}
|
}
|
|
saving.value = true
|
try {
|
const payload = {
|
merchantName: form.merchantName,
|
merchantType: form.merchantType,
|
contactName: form.contactName,
|
contactMobile: form.contactMobile,
|
address: form.address,
|
coverUrl: form.coverUrl,
|
businessStatus: form.businessStatus,
|
longitude: Number(form.longitude || 0) || undefined,
|
latitude: Number(form.latitude || 0) || undefined,
|
canLoginMerchantApp: requiresMerchantOperator.value ? 1 : Number(form.canLoginMerchantApp ?? 1),
|
loginAccount: !isEditMode.value || !merchantHasLoginAccount.value ? accountForm.loginAccount : undefined,
|
loginPassword: !isEditMode.value || updatingMerchantPassword || !merchantHasLoginAccount.value ? accountForm.loginPassword : undefined,
|
groupBuyingEnabled: form.groupBuyingEnabled,
|
usageInstructions: isParkingMerchant.value
|
? JSON.stringify(parkingAddresses.value)
|
: isChildcareMerchant.value
|
? JSON.stringify(schoolAddresses.value)
|
: isApprovalMerchant.value
|
? JSON.stringify(approvalItems.value)
|
: form.usageInstructions,
|
rightsText: form.rightsText,
|
merchantRightsConfig: serializeMerchantRightsConfig(),
|
serviceLabel: form.serviceLabel,
|
businessHours: businessHoursPayload(),
|
recommend: form.recommend,
|
sortNo: Number(form.sortNo || 0),
|
status: form.status,
|
}
|
|
if (isEditMode.value) {
|
await updateMerchant(queryString('merchantId'), payload)
|
ElMessage.success('商家已更新')
|
} else {
|
await createMerchant(payload)
|
ElMessage.success('商家已创建')
|
}
|
router.push('/merchants')
|
} finally {
|
saving.value = false
|
}
|
}
|
|
watch(activeTab, (value) => {
|
if (value === 'products') {
|
loadProducts()
|
}
|
if (value === 'coupons') {
|
couponMode.value = 'list'
|
loadCoupons()
|
}
|
})
|
|
watch(tabs, (availableTabs) => {
|
if (!availableTabs.some((item) => item.value === activeTab.value)) {
|
activeTab.value = 'settings'
|
}
|
}, { immediate: true })
|
|
watch(() => form.coverUrl, resetMerchantCoverPreview)
|
watch(() => productForm.coverUrl, resetProductCoverPreview)
|
watch(productValidPeriodRange, (value) => {
|
productNoticeForm.validPeriod = rangeText(value)
|
})
|
watch(productAvailableTimeRange, (value) => {
|
productNoticeForm.availableTime = rangeText(value)
|
})
|
watch(productImages, (value) => {
|
productForm.productImages = [...value]
|
productFormRef.value?.validateField('productImages').catch(() => undefined)
|
}, { deep: true })
|
watch(businessHours, () => {
|
form.businessHours = businessHoursPayload()
|
}, { deep: true, immediate: true })
|
|
watch(() => form.miniProgramJumpType, () => {
|
if (isCarWashMerchant.value) {
|
resetMerchantRights(serializeMerchantRightsConfig())
|
}
|
})
|
|
watch(() => form.miniProgramJumpUrl, () => {
|
if (isCarWashMerchant.value) syncCarWashJumpMeta()
|
})
|
|
// The create page is reused when the service type changes, so reset these
|
// create-only defaults from the current route instead of retaining old values.
|
watch(() => [route.query.mode, route.query.merchantType], applyNewMerchantDefaults, { immediate: true })
|
|
onMounted(() => {
|
if (isEditMode.value && queryString('merchantId')) {
|
getAdminMerchantDetail(queryString('merchantId')).then((merchant) => {
|
Object.assign(form, merchant)
|
form.createdAt = formatDisplayTime(merchant.createdAt)
|
form.groupBuyingEnabled = Number(merchant.groupBuyingEnabled ?? 1)
|
form.longitude = Number(merchant.longitude ?? 0)
|
form.latitude = Number(merchant.latitude ?? 0)
|
form.canLoginMerchantApp = Number(merchant.canLoginMerchantApp ?? 1)
|
form.recommend = Number(merchant.isRecommend ?? 0)
|
accountForm.loginAccount = merchant.loginAccount || ''
|
merchantHasLoginAccount.value = Boolean(String(merchant.loginAccount || '').trim())
|
if (isParkingMerchant.value) {
|
parkingAddresses.value = parseJsonValue<ParkingAddress[]>(merchant.usageInstructions, [])
|
} else if (isChildcareMerchant.value) {
|
schoolAddresses.value = parseJsonValue<SchoolAddress[]>(merchant.usageInstructions, [])
|
} else if (isApprovalMerchant.value) {
|
approvalItems.value = parseJsonValue<ApprovalItem[]>(merchant.usageInstructions, [])
|
}
|
resetBusinessHours(merchant.businessHours)
|
resetMerchantRights(merchant.merchantRightsConfig)
|
resetCarWashJumpFromMerchant(merchant as Record<string, any>)
|
})
|
}
|
if (activeTab.value === 'products') {
|
loadProducts()
|
}
|
if (activeTab.value === 'coupons') {
|
loadCoupons()
|
}
|
})
|
</script>
|
|
<template>
|
<div class="merchant-create-page">
|
<input
|
ref="imageInputRef"
|
type="file"
|
accept="image/jpeg,image/png,image/webp"
|
hidden
|
@change="uploadSelectedImage"
|
/>
|
<el-radio-group v-model="activeTab" class="merchant-create-tabs">
|
<el-radio-button v-for="item in tabs" :key="item.value" :label="item.value">
|
{{ item.label }}
|
</el-radio-button>
|
</el-radio-group>
|
|
<template v-if="activeTab === 'settings'">
|
<section class="merchant-create-panel">
|
<h3>商家信息</h3>
|
|
<el-form ref="merchantFormRef" :model="form" :rules="merchantRules" class="merchant-settings-form" label-width="58px" label-position="left">
|
<div class="merchant-create-content" :class="{ 'service-merchant-content': isServiceMerchant || isApartmentMerchant || isHotelMerchant || isCarWashMerchant || isHealthCheckMerchant || isParkingMerchant || isChildcareMerchant || isFinancialMerchant || isMovieMerchant || isSpouseEmploymentMerchant || isApprovalMerchant }">
|
<div class="merchant-create-left" :style="isApartmentMerchant || isFoodMerchant || isHotelMerchant || isCarWashMerchant || isHealthCheckMerchant || isParkingMerchant || isChildcareMerchant || isFinancialMerchant || isMovieMerchant || isSpouseEmploymentMerchant || isApprovalMerchant ? 'flex: 1' : ''">
|
<el-form-item class="merchant-form-row three-columns" label="商家名称" prop="merchantName" required>
|
<el-input v-model="form.merchantName" />
|
</el-form-item>
|
|
<el-form-item v-if="!isApartmentMerchant" class="merchant-form-row address-row" :label="isCarWashMerchant || isHealthCheckMerchant || isFinancialMerchant || isMovieMerchant || isSpouseEmploymentMerchant || isApprovalMerchant ? '服务点' : '地址'" prop="address" required>
|
<el-input v-model="form.address" />
|
<el-button type="primary" @click="markLocation">地图标点</el-button>
|
</el-form-item>
|
|
<el-form-item class="merchant-cover-row" label="封面图" prop="coverUrl" required>
|
<div class="merchant-cover-preview">
|
<img
|
v-if="form.coverUrl && !merchantCoverPreviewFailed"
|
:src="form.coverUrl"
|
alt="商家封面"
|
@error="merchantCoverPreviewFailed = true"
|
/>
|
<el-icon v-else><Picture /></el-icon>
|
</div>
|
<el-button type="primary" :loading="imageUploading" @click="changeCover('merchant')">更换封面</el-button>
|
</el-form-item>
|
</div>
|
|
<div v-if="!isServiceMerchant && !isApartmentMerchant && !isHotelMerchant && !isCarWashMerchant && !isHealthCheckMerchant && !isParkingMerchant && !isChildcareMerchant && !isFinancialMerchant && !isMovieMerchant && !isSpouseEmploymentMerchant && !isApprovalMerchant" class="merchant-create-middle">
|
<el-form-item class="merchant-form-row" label="联系电话" prop="contactMobile" required>
|
<el-input v-model="form.contactMobile" />
|
</el-form-item>
|
|
<template v-if="!isApartmentMerchant && !usesSimplifiedMerchantSettings">
|
<div class="merchant-form-row recommend-row">
|
<label>开启团购</label>
|
<el-switch v-model="form.groupBuyingEnabled" :active-value="1" :inactive-value="0" />
|
</div>
|
|
<div class="merchant-form-row">
|
<label>服务标签</label>
|
<el-input v-model="form.serviceLabel" placeholder="如:到店消费" />
|
</div>
|
|
<div class="merchant-form-row">
|
<label>经纬度</label>
|
<el-input v-model="form.longitude" placeholder="经度" />
|
<el-input v-model="form.latitude" placeholder="纬度" />
|
</div>
|
</template>
|
</div>
|
|
<div class="merchant-create-right" :style="isApartmentMerchant || isFoodMerchant || isHotelMerchant || isCarWashMerchant || isHealthCheckMerchant || isParkingMerchant || isChildcareMerchant || isFinancialMerchant || isMovieMerchant || isSpouseEmploymentMerchant || isApprovalMerchant ? 'flex: 1' : ''">
|
<el-form-item v-if="isServiceMerchant || isApartmentMerchant || isHotelMerchant || isCarWashMerchant || isHealthCheckMerchant || isParkingMerchant || isChildcareMerchant || isFinancialMerchant || isMovieMerchant || isSpouseEmploymentMerchant || isApprovalMerchant" class="merchant-form-row service-contact-row" label="联系电话" prop="contactMobile" required>
|
<el-input v-model="form.contactMobile" placeholder="请输入联系电话" />
|
</el-form-item>
|
|
<div v-if="hasExistingMerchant" class="merchant-form-row">
|
<label>创建时间</label>
|
<el-input v-model="form.createdAt" readonly />
|
</div>
|
|
<el-form-item v-if="!isApprovalMerchant" class="merchant-hours-block" label="营业时间" prop="businessHours">
|
<div class="merchant-hours-list">
|
<div v-for="(item, index) in businessHours" :key="item.id" class="merchant-hour-row">
|
<el-time-picker
|
v-model="item.value"
|
is-range
|
class="merchant-time-range-picker"
|
format="HH:mm"
|
value-format="HH:mm"
|
range-separator="至"
|
start-placeholder="开始时间"
|
end-placeholder="结束时间"
|
/>
|
<button class="merchant-hour-action" type="button" @click="removeBusinessHour(index)">
|
<el-icon><Minus /></el-icon>
|
</button>
|
<button
|
v-if="index === businessHours.length - 1"
|
class="merchant-hour-action"
|
type="button"
|
@click="addBusinessHour"
|
>
|
<el-icon><Plus /></el-icon>
|
</button>
|
</div>
|
</div>
|
</el-form-item>
|
|
<div class="merchant-form-row recommend-row">
|
<label>是否推荐</label>
|
<el-switch v-model="form.recommend" :active-value="1" :inactive-value="0" />
|
</div>
|
|
<el-form-item class="merchant-form-row" label="排序" prop="sortNo" required>
|
<el-input v-model="form.sortNo" placeholder="请输入" />
|
</el-form-item>
|
</div>
|
</div>
|
</el-form>
|
</section>
|
|
<section v-if="!isServiceMerchant && !isApartmentMerchant && !isHotelMerchant && !isCarWashMerchant && !isHealthCheckMerchant && !isParkingMerchant && !isChildcareMerchant && !isFinancialMerchant && !isMovieMerchant && !isSpouseEmploymentMerchant && !isApprovalMerchant && !usesSimplifiedMerchantSettings" class="merchant-create-panel">
|
<h3>小程序详情配置</h3>
|
<div class="merchant-form-row">
|
<label>使用说明</label>
|
<el-input v-model="form.usageInstructions" type="textarea" :rows="3" />
|
</div>
|
<div class="merchant-form-row">
|
<label>权益文案</label>
|
<el-input v-model="form.rightsText" type="textarea" :rows="3" />
|
</div>
|
</section>
|
|
<section class="merchant-account-panel">
|
<h3>账户设置</h3>
|
<el-form ref="accountFormRef" class="merchant-account-form" :model="accountForm" :rules="accountRules" label-width="72px" label-position="left">
|
<el-form-item v-if="supportsMerchantAppLogin" label="登录商户端" label-width="100px">
|
<el-switch v-model="form.canLoginMerchantApp" :active-value="1" :inactive-value="0" />
|
</el-form-item>
|
<el-form-item class="merchant-account-field" label="登录账号" prop="loginAccount" :required="requiresMerchantOperator" :error="accountTaken ? '登录账号已存在,请更换' : ''">
|
<el-input v-model="accountForm.loginAccount" maxlength="32" :disabled="isEditMode && merchantHasLoginAccount" :placeholder="isEditMode && !merchantHasLoginAccount ? '请输入登录账号' : (isEditMode ? '已配置登录账号' : '仅支持英文和数字')" @input="normalizeLoginAccount" @blur="checkLoginAccountAvailable" />
|
</el-form-item>
|
<el-form-item class="merchant-account-field" label="登录密码" prop="loginPassword" :required="requiresMerchantOperator || (isEditMode && Boolean(accountForm.loginPassword))">
|
<el-input v-model="accountForm.loginPassword" type="password" show-password :placeholder="isEditMode ? '留空则不修改,6至64位' : ''" />
|
</el-form-item>
|
<el-form-item class="merchant-account-field" label="确认密码" prop="confirmPassword" :required="requiresMerchantOperator || (isEditMode && Boolean(accountForm.loginPassword))">
|
<el-input v-model="accountForm.confirmPassword" type="password" show-password :placeholder="isEditMode ? '再次输入新密码' : ''" />
|
</el-form-item>
|
</el-form>
|
</section>
|
|
<el-button type="primary" class="merchant-create-save" :loading="saving" @click="saveMerchant">保存</el-button>
|
</template>
|
|
<template v-else-if="activeTab === 'rights'">
|
<section class="merchant-rights-panel" :class="{ 'movie-merchant-rights-panel': isMovieMerchant }">
|
<h3 v-if="isMovieMerchant" class="merchant-rights-section-title">商家权益</h3>
|
|
<div v-if="!isApartmentMerchant && !isCarWashMerchant && !isHealthCheckMerchant && !isParkingMerchant && !isChildcareMerchant && !isFinancialMerchant && !isMovieMerchant && !isSpouseEmploymentMerchant && !isApprovalMerchant" class="merchant-rights-top-row">
|
<strong>团单服务</strong>
|
<el-switch v-model="form.groupBuyingEnabled" :active-value="1" :inactive-value="0" />
|
</div>
|
|
<div v-if="isCarWashMerchant" class="merchant-rights-top-row carwash-miniprogram-row">
|
<el-radio-group v-model="form.miniProgramJumpType" class="carwash-jump-radio">
|
<el-radio label="miniprogram">小程序跳转</el-radio>
|
<el-radio label="normal">普通</el-radio>
|
</el-radio-group>
|
<div v-if="form.miniProgramJumpType === 'miniprogram'" class="carwash-jump-input-row">
|
<span>小程序跳转链接</span>
|
<el-input v-model="form.miniProgramJumpUrl" placeholder="请输入" />
|
</div>
|
</div>
|
|
<div v-if="!isApprovalMerchant" v-for="item in rightsConfigs" :key="item.level" class="merchant-rights-card" :class="{ 'movie-rights-card': isMovieMerchant }">
|
<div class="merchant-rights-level-row">
|
<strong>{{ item.level }}</strong>
|
<el-switch v-model="item.enabled" />
|
<span>标签</span>
|
<el-input v-model="item.tag" placeholder="请输入" />
|
</div>
|
|
<div class="merchant-rights-detail-row">
|
<label>权益详情描述</label>
|
<div class="merchant-rights-detail-content">
|
<el-input v-model="item.detail" type="textarea" :rows="4" resize="none" placeholder="请输入" />
|
<div v-if="isParkingMerchant" class="merchant-rights-parking-radio" style="margin-top: 16px;">
|
<el-radio-group v-model="item.parkingType">
|
<el-radio label="free">免费</el-radio>
|
<el-radio label="paid">付费</el-radio>
|
</el-radio-group>
|
</div>
|
<div v-if="isParkingMerchant && item.parkingType === 'free'" style="margin-top: 16px;">
|
<el-radio-group v-model="item.parkingScope" style="display: flex; flex-direction: column; align-items: flex-start; gap: 12px;">
|
<el-radio label="all" style="margin-right: 0;">停车场全部可选</el-radio>
|
<el-radio label="selected" style="margin-right: 0;">停车场只选其一</el-radio>
|
</el-radio-group>
|
</div>
|
</div>
|
<div class="merchant-rights-example" v-if="showRightsExamples && isMovieMerchant">
|
<span>示例:</span>
|
<div class="merchant-rights-phone-preview">
|
<div class="phone-preview-hero">
|
<img :src="form.coverUrl || rightExampleImage" alt="权益示例" />
|
<span class="phone-preview-back">‹</span>
|
<span class="phone-preview-more">•••</span>
|
</div>
|
<div class="phone-preview-body">
|
<div class="phone-preview-title-row">
|
<strong>{{ form.merchantName || '影城服务' }}</strong>
|
<div class="phone-preview-coupon">
|
<span>券</span>
|
</div>
|
</div>
|
<div class="phone-preview-tag">{{ item.tag || '人才卡专享' }}</div>
|
<div class="phone-preview-address">
|
<span>⌖</span>
|
<p>{{ form.address || '请填写商家地址' }}</p>
|
<i>➤</i>
|
<i>☎</i>
|
</div>
|
<div class="phone-preview-tabs">
|
<span>消费购买</span>
|
<strong>权益详情</strong>
|
</div>
|
<div class="phone-preview-detail">
|
<p>{{ item.detail || '请输入权益详情描述' }}</p>
|
</div>
|
</div>
|
</div>
|
</div>
|
<div class="merchant-rights-example" v-else-if="showRightsExamples && (isFinancialMerchant || isSpouseEmploymentMerchant)">
|
<span>示例:</span>
|
<div class="merchant-rights-phone-preview">
|
<div class="phone-preview-hero">
|
<img :src="rightExampleImage" alt="权益示例" />
|
<span class="phone-preview-back">‹</span>
|
<span class="phone-preview-more">•••</span>
|
</div>
|
<div class="phone-preview-body">
|
<div class="phone-preview-title-row">
|
<strong>{{ item.level }}</strong>
|
</div>
|
<div class="phone-preview-detail">
|
<p>{{ item.detail || '请输入权益详情描述' }}</p>
|
</div>
|
</div>
|
</div>
|
</div>
|
<div class="merchant-rights-example" v-else-if="showRightsExamples && !isParkingMerchant && (!isCarWashMerchant || form.miniProgramJumpType === 'normal')">
|
<span>示例:</span>
|
<div class="merchant-rights-phone-preview">
|
<div class="phone-preview-hero">
|
<img :src="rightExampleImage" alt="权益示例" />
|
<span class="phone-preview-back">‹</span>
|
<span class="phone-preview-more">•••</span>
|
</div>
|
<div class="phone-preview-body">
|
<div class="phone-preview-title-row">
|
<strong>{{ form.merchantName || '百杏厨房(中环天地店)' }}</strong>
|
<div class="phone-preview-coupon">
|
<span>券</span>
|
<b>8.8折</b>
|
</div>
|
</div>
|
<div class="phone-preview-tag">到店消费 | 卓越卡可用</div>
|
<div class="phone-preview-address">
|
<span>⌖</span>
|
<p>{{ form.address || '四川省攀枝花市东区炳三区新福路20号中环天地负二层' }}</p>
|
<i>➤</i>
|
<i>☎</i>
|
</div>
|
<div class="phone-preview-tabs">
|
<span>消费购买</span>
|
<strong>权益详情</strong>
|
</div>
|
<div class="phone-preview-detail">
|
<p v-for="line in rightExampleLines" :key="line">{{ line }}</p>
|
</div>
|
</div>
|
</div>
|
</div>
|
</div>
|
|
<template v-if="supportsStoreConsumption && item.consumeEnabled">
|
<template v-if="isHotelMerchant">
|
<div class="merchant-consume-row hotel-voucher-row">
|
<strong>免费券</strong>
|
<el-switch v-model="item.freeVoucherEnabled" />
|
<el-input v-model="item.freeVoucherTimes" placeholder="请输入" class="hotel-times-input" />
|
<span>次</span>
|
<span class="hotel-margin-left">重置周期</span>
|
<el-select v-model="item.freeVoucherCycle" class="hotel-cycle-select">
|
<el-option label="每天" value="每天" />
|
<el-option label="每周" value="每周" />
|
<el-option label="每月" value="每月" />
|
<el-option label="每季" value="每季" />
|
<el-option label="每年" value="每年" />
|
</el-select>
|
<strong class="hotel-margin-left">是否政府补贴</strong>
|
<el-switch v-model="item.freeVoucherSubsidy" />
|
</div>
|
|
<div class="merchant-consume-row hotel-consume-row" >
|
<strong>到店消费</strong>
|
<el-switch v-model="item.storeConsume" />
|
<div class="hotel-consume-options">
|
<div class="hotel-consume-option">
|
<el-radio v-model="item.consumeType" label="折扣">折扣</el-radio>
|
<el-input v-model="item.consumeDiscount" placeholder="请输入" />
|
<span>折</span>
|
<strong class="hotel-margin-left">是否政府补贴</strong>
|
<el-switch v-model="item.consumeDiscountSubsidy" />
|
</div>
|
<div class="hotel-consume-option">
|
<el-radio v-model="item.consumeType" label="优惠">优惠</el-radio>
|
<el-input v-model="item.consumeCoupon" placeholder="请输入" />
|
<span>元</span>
|
<strong class="hotel-margin-left">是否政府补贴</strong>
|
<el-switch v-model="item.consumeCouponSubsidy" />
|
</div>
|
</div>
|
</div>
|
</template>
|
<template v-else>
|
<div class="merchant-consume-row">
|
<strong>到店消费</strong>
|
<el-switch v-model="item.storeConsume" />
|
<span>折扣</span>
|
<el-input v-model="item.discount" placeholder="请输入" />
|
<span>折</span>
|
<strong>是否政府补贴</strong>
|
<el-switch v-model="item.governmentSubsidy" />
|
</div>
|
</template>
|
|
<div v-if="!isParkingMerchant" class="merchant-rights-detail-row consume-desc-row">
|
<label>到店消费使用描述</label>
|
<el-input v-model="item.consumeDesc" type="textarea" :rows="4" resize="none" placeholder="请输入" />
|
<div v-if="showRightsExamples" class="merchant-rights-example">
|
<span>示例:</span>
|
<div class="merchant-rights-phone-preview">
|
<div class="phone-preview-hero">
|
<img :src="rightExampleImage" alt="到店消费示例" />
|
<span class="phone-preview-back">‹</span>
|
<span class="phone-preview-more">•••</span>
|
</div>
|
<div class="phone-preview-body">
|
<div class="phone-preview-title-row">
|
<strong>百杏厨房(中环天地店)</strong>
|
<div class="phone-preview-coupon">
|
<span>券</span>
|
<b>8.8折</b>
|
</div>
|
</div>
|
<div class="phone-preview-tag">到店消费 | 卓越卡可用</div>
|
<div class="phone-preview-address">
|
<span>⌖</span>
|
<p>四川省攀枝花市东区炳三区新福路20号中环天地负二层</p>
|
<i>➤</i>
|
<i>☎</i>
|
</div>
|
<div class="phone-preview-tabs">
|
<span>消费购买</span>
|
<strong>权益详情</strong>
|
</div>
|
<div class="phone-preview-detail">
|
<p v-for="line in rightExampleLines" :key="line">{{ line }}</p>
|
</div>
|
</div>
|
</div>
|
</div>
|
</div>
|
<div v-if="isChildcareMerchant" class="merchant-rights-detail-row consume-desc-row">
|
<label>到店消费使用描述</label>
|
<el-input v-model="item.consumeDesc" type="textarea" :rows="4" resize="none" placeholder="请输入" />
|
</div>
|
</template>
|
|
<div v-if="isParkingMerchant" class="merchant-rights-detail-row consume-desc-row">
|
<label>到店消费使用描述</label>
|
<el-input v-model="item.consumeDesc" type="textarea" :rows="4" resize="none" placeholder="请输入" />
|
</div>
|
</div>
|
|
<div v-if="isApprovalMerchant" class="merchant-rights-parking-address-panel" style="padding: 0 32px;">
|
<div class="parking-address-header">商家权益</div>
|
<div class="parking-address-table-wrapper">
|
<el-table :data="approvalItems" border>
|
<el-table-column label="名称" min-width="120">
|
<template #default="{ row }">
|
<el-input v-if="row.isNew || row.isEditing" v-model="row.name" placeholder="请输入" />
|
<span v-else>{{ row.name }}</span>
|
</template>
|
</el-table-column>
|
<el-table-column label="权益内容" min-width="220">
|
<template #default="{ row }">
|
<el-input v-if="row.isNew || row.isEditing" v-model="row.content" type="textarea" :rows="3" resize="none" placeholder="请输入" />
|
<span v-else>{{ row.content }}</span>
|
</template>
|
</el-table-column>
|
<el-table-column label="地址" min-width="140">
|
<template #default="{ row }">
|
<el-input v-if="row.isNew || row.isEditing" v-model="row.address" placeholder="请输入" />
|
<span v-else>{{ row.address }}</span>
|
</template>
|
</el-table-column>
|
<el-table-column label="电话" min-width="100">
|
<template #default="{ row }">
|
<el-input v-if="row.isNew || row.isEditing" v-model="row.phone" placeholder="请输入" />
|
<span v-else>{{ row.phone }}</span>
|
</template>
|
</el-table-column>
|
<el-table-column label="适用人才" min-width="180">
|
<template #default="{ row }">
|
<el-checkbox-group v-model="row.talentLevels" :disabled="!row.isNew && !row.isEditing">
|
<el-checkbox label="卓越人才卡" />
|
<el-checkbox label="杰出人才卡" />
|
<el-checkbox label="菁英人才卡" />
|
</el-checkbox-group>
|
</template>
|
</el-table-column>
|
<el-table-column label="操作" width="160" fixed="right" align="center">
|
<template #default="{ $index, row }">
|
<el-button v-if="row.isNew || row.isEditing" link type="primary" @click="saveApprovalRow($index)">保存</el-button>
|
<el-button v-else link type="primary" @click="editApprovalRow($index)">编辑</el-button>
|
<el-button link type="danger" @click="removeApproval($index)">删除</el-button>
|
<el-button link type="primary" @click="uploadApprovalWindow($index)">{{ row.windowInfoUrl ? '重新上传窗口信息' : '上传窗口信息' }}</el-button>
|
</template>
|
</el-table-column>
|
</el-table>
|
<div style="text-align: right; margin-top: 16px;">
|
<el-button plain @click="addApprovalRow">新增</el-button>
|
</div>
|
</div>
|
</div>
|
</section>
|
|
<section v-if="isParkingMerchant" class="merchant-parking-address-panel">
|
<h3 class="merchant-parking-address-title">停车场地址</h3>
|
<el-table :data="parkingAddresses" border>
|
<el-table-column label="名称">
|
<template #default="{ row }"><el-input v-model="row.name" placeholder="请输入" /></template>
|
</el-table-column>
|
<el-table-column label="地址">
|
<template #default="{ row }"><el-input v-model="row.address" placeholder="请输入" /></template>
|
</el-table-column>
|
<el-table-column label="定位标点" width="120" align="center">
|
<template #default="{ row }">
|
<el-button type="primary" size="small" @click="markParkingLocation(row)">{{ row.latitude ? '已标点' : '未标点' }}</el-button>
|
</template>
|
</el-table-column>
|
<el-table-column label="操作" width="100" fixed="right" align="center">
|
<template #default="{ $index }">
|
<el-button link type="danger" @click="removeParkingAddress($index)">删除</el-button>
|
</template>
|
</el-table-column>
|
</el-table>
|
<div class="merchant-parking-address-actions">
|
<el-button plain @click="addParkingAddress">新增</el-button>
|
</div>
|
</section>
|
|
<section v-if="isChildcareMerchant" class="merchant-school-address-panel">
|
<h3 class="merchant-school-address-title">学校地址</h3>
|
<el-table :data="schoolAddresses" border>
|
<el-table-column label="名称" prop="name" />
|
<el-table-column label="地址" prop="address" />
|
<el-table-column label="操作" width="120" fixed="right" align="center">
|
<template #default="{ $index }">
|
<el-button link type="danger" @click="removeSchool($index)">删除</el-button>
|
<el-button link type="primary" @click="openSchoolDialog($index)">详情</el-button>
|
</template>
|
</el-table-column>
|
</el-table>
|
<div class="merchant-school-address-actions">
|
<el-button plain @click="openSchoolDialog(-1)">新增</el-button>
|
</div>
|
</section>
|
|
<el-button type="primary" class="merchant-create-save" :loading="rightsSaving" @click="saveRights">保存</el-button>
|
</template>
|
|
<template v-else-if="activeTab === 'products'">
|
<template v-if="productMode === 'list'">
|
<section class="merchant-products-panel">
|
<h3>查询表格</h3>
|
|
<div class="merchant-products-filter">
|
<label>商品名称</label>
|
<el-input v-model="productFilters.productName" placeholder="请输入商品名称" clearable />
|
|
<label>状态</label>
|
<el-select v-model="productFilters.status" placeholder="全部" clearable>
|
<el-option label="全部" :value="undefined" />
|
<el-option label="待审核" :value="1" />
|
<el-option label="审核通过" :value="2" />
|
<el-option label="已驳回" :value="3" />
|
</el-select>
|
|
<label>创建时间</label>
|
<el-date-picker
|
v-model="productFilters.createdTime"
|
class="product-created-time-picker"
|
type="daterange"
|
start-placeholder="开始日期"
|
end-placeholder="结束日期"
|
range-separator="至"
|
value-format="YYYY-MM-DD"
|
/>
|
|
<div class="merchant-products-actions">
|
<el-button type="primary" :icon="Search" @click="searchProducts">查询</el-button>
|
<el-button :icon="Refresh" @click="resetProductFilters">重置</el-button>
|
</div>
|
</div>
|
|
<div class="merchant-products-toolbar">
|
<el-button type="primary" :icon="Plus" @click="openProductCreate">新增团单</el-button>
|
</div>
|
|
<el-table
|
v-loading="productLoading"
|
:data="merchantProducts"
|
border
|
class="merchant-products-table"
|
empty-text="暂无团单数据"
|
>
|
<el-table-column prop="id" label="id" min-width="90" />
|
<el-table-column prop="productName" label="商品名称" min-width="90" />
|
<el-table-column label="封面图" min-width="90">
|
<template #default="{ row }">
|
<span>{{ row.coverUrl ? '已上传' : '' }}</span>
|
</template>
|
</el-table-column>
|
<el-table-column label="商品金额" min-width="90">
|
<template #default="{ row }">{{ productAmountText(row) }}</template>
|
</el-table-column>
|
<el-table-column v-if="!isHotelMerchant && !isHealthCheckMerchant" prop="salesCount" label="销量" min-width="90">
|
<template #default="{ row }">{{ row.salesCount || '' }}</template>
|
</el-table-column>
|
<el-table-column label="修改时间" min-width="90">
|
<template #default="{ row }">{{ formatDisplayTime(row.submitTime || row.createdAt) }}</template>
|
</el-table-column>
|
<el-table-column label="状态" min-width="90">
|
<template #default="{ row }">{{ productAuditStatusText(row.auditStatus) }}</template>
|
</el-table-column>
|
<el-table-column label="操作" width="164" fixed="right" align="center">
|
<template #default="{ row }">
|
<div class="merchant-products-operation">
|
<el-button link @click="editProduct(row)">编辑</el-button>
|
<el-tooltip :content="productToggleTip(row)" :disabled="canToggleProductStatus(row)" placement="top">
|
<span class="merchant-products-operation-item">
|
<el-button
|
link
|
:disabled="!canToggleProductStatus(row)"
|
@click="offlineProduct(row)"
|
>
|
{{ row.status === 0 ? '上架' : '下架' }}
|
</el-button>
|
</span>
|
</el-tooltip>
|
<el-button link @click="deleteProduct(row)">删除</el-button>
|
</div>
|
</template>
|
</el-table-column>
|
</el-table>
|
|
<div class="merchant-products-real-pagination">
|
<el-pagination
|
v-model:current-page="productPagination.page"
|
v-model:page-size="productPagination.size"
|
background
|
layout="total, sizes, prev, pager, next"
|
:page-sizes="[10, 20, 50]"
|
:total="productPagination.total"
|
@change="loadProducts"
|
/>
|
</div>
|
</section>
|
</template>
|
|
<template v-else>
|
<el-form ref="productFormRef" :model="productForm" :rules="productRules" class="merchant-product-edit-form" label-width="68px" label-position="left">
|
<section class="merchant-product-form-panel">
|
<h3>商品信息</h3>
|
|
<div class="product-form-grid">
|
<div class="product-info-left">
|
<el-form-item class="product-form-row" label="商品名称" prop="productName" required>
|
<el-input v-model="productForm.productName" placeholder="请输入商品名称" />
|
</el-form-item>
|
|
<el-form-item class="product-image-row" label="封面图" prop="coverUrl" required>
|
<div class="product-cover-preview">
|
<img
|
v-if="productForm.coverUrl && !productCoverPreviewFailed"
|
:src="productForm.coverUrl"
|
alt="封面图"
|
@error="productCoverPreviewFailed = true"
|
/>
|
<el-icon v-else><Picture /></el-icon>
|
<span v-if="productForm.coverUrl && !productCoverPreviewFailed" class="product-image-zoom">⌕</span>
|
</div>
|
<el-button type="primary" :loading="imageUploading" @click="changeCover('productCover')">更换封面</el-button>
|
</el-form-item>
|
|
<el-form-item v-if="!isHealthCheckMerchant" class="product-image-row compact" label="商品图片" prop="productImages" required>
|
<div class="product-image-list">
|
<div v-for="(image, index) in productImages" :key="image" class="product-thumb-preview">
|
<img :src="image" alt="商品图片" />
|
<button type="button" class="product-image-remove" @click="removeProductImage(index)">×</button>
|
</div>
|
<div v-if="productImages.length === 0" class="product-thumb-preview">
|
<el-icon><Picture /></el-icon>
|
</div>
|
</div>
|
<el-button type="primary" :loading="imageUploading" @click="changeCover('productImage')">添加图片</el-button>
|
</el-form-item>
|
|
<div v-if="isHealthCheckMerchant" class="product-form-row health-check-time-row" style="margin-top: 24px;">
|
<label>可预约时间</label>
|
<el-date-picker
|
v-model="productValidPeriodRange"
|
type="daterange"
|
start-placeholder="开始日期"
|
end-placeholder="结束日期"
|
range-separator="—"
|
format="YYYY/MM/DD"
|
value-format="YYYY-MM-DD"
|
/>
|
</div>
|
</div>
|
|
<div v-if="!isHealthCheckMerchant" class="product-info-middle">
|
<el-form-item class="product-form-row amount" :class="{ 'hotel-amount': isHotelMerchant }" label="商品金额" prop="salePrice" required>
|
<template v-if="isHotelMerchant">
|
<el-input-number
|
v-model="productForm.salePrice"
|
:min="0.01"
|
:step="0.01"
|
:precision="2"
|
:controls="false"
|
placeholder=""
|
/>
|
<span class="amount-separator">-</span>
|
<el-input-number
|
v-model="productForm.originalPrice"
|
:min="0.01"
|
:step="0.01"
|
:precision="2"
|
:controls="false"
|
placeholder=""
|
/>
|
</template>
|
<template v-else>
|
<el-input-number
|
v-model="productForm.salePrice"
|
:min="0.01"
|
:step="0.01"
|
:precision="2"
|
:controls="false"
|
placeholder="请输入"
|
/>
|
</template>
|
<span>元</span>
|
</el-form-item>
|
</div>
|
|
</div>
|
|
<el-form-item v-if="!isHealthCheckMerchant" class="product-detail-row" label="商品详情" prop="detailText" required>
|
<AdminRichTextEditor :model-value="productForm.detailText || ''" class="merchant-product-rich-editor" placeholder="请输入商品详情" :height="260" @update:model-value="value => { productForm.detailText = value; productFormRef?.validateField('detailText') }" />
|
</el-form-item>
|
|
<div v-if="isHealthCheckMerchant" class="product-detail-row health-check-table-row">
|
<label>商品详情</label>
|
<div style="flex: 1; display: flex; flex-direction: column; gap: 12px;">
|
<el-table :data="healthCheckItems" border>
|
<el-table-column label="检查项目">
|
<template #default="{ row }"><el-input v-model="row.name" placeholder="请输入" /></template>
|
</el-table-column>
|
<el-table-column label="项目意义">
|
<template #default="{ row }"><el-input v-model="row.meaning" placeholder="请输入" /></template>
|
</el-table-column>
|
<el-table-column label="男/女" width="160">
|
<template #default="{ row }">
|
<el-checkbox-group v-model="row.gender">
|
<el-checkbox label="男">男</el-checkbox>
|
<el-checkbox label="女">女</el-checkbox>
|
</el-checkbox-group>
|
</template>
|
</el-table-column>
|
<el-table-column label="操作" width="80" fixed="right" align="center">
|
<template #default="{ $index }">
|
<el-button link type="primary" @click="removeHealthCheckItem($index)">删除</el-button>
|
</template>
|
</el-table-column>
|
</el-table>
|
<div style="text-align: right;">
|
<el-button type="primary" @click="addHealthCheckItem">新增</el-button>
|
</div>
|
</div>
|
</div>
|
</section>
|
</el-form>
|
|
<section v-if="!isHealthCheckMerchant" class="merchant-product-form-panel notice">
|
<h3>购买须知设置(非必填)</h3>
|
<div class="product-notice-grid">
|
<div class="product-notice-field">
|
<label>有效期</label>
|
<el-date-picker
|
v-model="productValidPeriodRange"
|
type="daterange"
|
start-placeholder="开始日期"
|
end-placeholder="结束日期"
|
range-separator="至"
|
value-format="YYYY-MM-DD"
|
/>
|
</div>
|
<div class="product-notice-field">
|
<label>使用方式</label>
|
<el-input v-model="productNoticeForm.useMethod" placeholder="请输入" />
|
</div>
|
<div class="product-notice-field">
|
<label>可用时间</label>
|
<el-time-picker
|
v-model="productAvailableTimeRange"
|
is-range
|
class="merchant-time-range-picker"
|
start-placeholder="开始时间"
|
end-placeholder="结束时间"
|
range-separator="至"
|
value-format="HH:mm"
|
format="HH:mm"
|
/>
|
</div>
|
<div class="product-notice-field">
|
<label>其他规则</label>
|
<el-input v-model="productNoticeForm.otherRules" placeholder="请输入" />
|
</div>
|
</div>
|
</section>
|
|
<section v-if="!isHotelMerchant && !isHealthCheckMerchant" class="merchant-product-form-panel rights">
|
<h3>人才权益</h3>
|
<div class="product-rights-list">
|
<div v-for="item in productRights" :key="item.level" class="product-right-row">
|
<div class="product-right-main" :class="{ free: item.free }">
|
<strong class="right-level">{{ item.level }}</strong>
|
<el-switch v-model="item.enabled" class="right-enabled" />
|
<span class="right-free-label">是否免费</span>
|
<el-switch v-model="item.free" class="right-free-switch" />
|
<template v-if="item.free">
|
<div class="right-limit-field">
|
<span class="product-right-tip">设置成999不限次数</span>
|
<el-input-number
|
v-model="item.limitCount"
|
:min="1"
|
:max="999"
|
:step="1"
|
:precision="0"
|
:controls="false"
|
placeholder="请输入"
|
/>
|
</div>
|
<span class="right-unit">次</span>
|
<span class="right-reset-label">重置周期</span>
|
<el-select v-model="item.resetCycle" class="right-reset-select">
|
<el-option label="每天" value="每天" />
|
<el-option label="每周" value="每周" />
|
<el-option label="每月" value="每月" />
|
<el-option label="每年" value="每年" />
|
</el-select>
|
</template>
|
<template v-else>
|
<div class="right-discount-control">
|
<span class="right-discount-label">折扣</span>
|
<el-input v-model="item.discount" class="right-discount-input" placeholder="请输入" />
|
<span class="right-unit">折</span>
|
</div>
|
</template>
|
<span class="right-subsidy-label">是否政府补贴</span>
|
<el-switch v-model="item.subsidy" class="right-subsidy-switch" />
|
</div>
|
<div v-if="item.free" class="product-right-sub">
|
<span class="right-exceed-label">超过次数</span>
|
<div class="right-exceed-discount-field">
|
<el-input-number
|
v-model="item.exceedDiscount"
|
class="right-exceed-discount-input"
|
:min="0.1"
|
:max="10"
|
:step="0.1"
|
:precision="1"
|
:controls="false"
|
placeholder="请输入"
|
@change="validateProductExceedDiscount(item)"
|
@blur="validateProductExceedDiscount(item)"
|
/>
|
<span v-if="productExceedDiscountErrors[item.level]" class="field-error">{{ productExceedDiscountErrors[item.level] }}</span>
|
</div>
|
<span class="right-exceed-unit">折</span>
|
<span class="right-exceed-subsidy-label">是否政府补贴</span>
|
<el-switch v-model="item.exceedSubsidy" class="right-exceed-subsidy-switch" />
|
</div>
|
</div>
|
</div>
|
</section>
|
|
<div class="product-form-actions">
|
<el-button type="primary" :loading="productSaving" @click="saveProduct">保存</el-button>
|
<el-button @click="cancelProductForm">取消</el-button>
|
</div>
|
</template>
|
</template>
|
|
<template v-else-if="activeTab === 'coupons'">
|
<section class="merchant-coupons-panel">
|
<template v-if="couponMode === 'list'">
|
<h3>查询表格</h3>
|
|
<div class="merchant-coupons-filter">
|
<label>优惠券名称</label>
|
<el-input v-model="couponFilters.couponName" placeholder="请输入优惠券名称" clearable />
|
|
<label>状态</label>
|
<el-select v-model="couponFilters.status" placeholder="全部" clearable>
|
<el-option label="全部" :value="undefined" />
|
<el-option label="启用" :value="1" />
|
<el-option label="停用" :value="0" />
|
</el-select>
|
|
<label>创建时间</label>
|
<el-date-picker
|
v-model="couponFilters.createdTime"
|
class="coupon-created-time-picker"
|
type="daterange"
|
start-placeholder="开始日期"
|
end-placeholder="结束日期"
|
range-separator="至"
|
value-format="YYYY-MM-DD"
|
/>
|
|
<div class="merchant-coupons-actions">
|
<el-button type="primary" :icon="Search" @click="searchCoupons">查询</el-button>
|
<el-button :icon="Refresh" @click="resetCouponFilters">重置</el-button>
|
</div>
|
</div>
|
|
<div class="merchant-coupons-toolbar">
|
<el-button type="primary" :icon="Plus" @click="openCouponCreate">新增优惠券</el-button>
|
</div>
|
|
<el-table
|
v-loading="couponLoading"
|
:data="merchantCoupons"
|
border
|
class="merchant-coupons-table"
|
:show-header="true"
|
empty-text="暂无优惠券数据"
|
>
|
<el-table-column prop="id" label="id" min-width="80" />
|
<el-table-column prop="couponName" label="优惠券名称" min-width="90" />
|
<el-table-column label="适用商品" min-width="90">
|
<template #default="{ row }">{{ couponProductText(row) }}</template>
|
</el-table-column>
|
<el-table-column label="条件" min-width="90">
|
<template #default="{ row }">{{ couponConditionText(row) }}</template>
|
</el-table-column>
|
<el-table-column label="优惠券金额" min-width="90">
|
<template #default="{ row }">{{ couponAmountText(row) }}</template>
|
</el-table-column>
|
<el-table-column label="可用人才" min-width="90">
|
<template #default="{ row }">{{ couponTalentText(row) }}</template>
|
</el-table-column>
|
<el-table-column label="到期时间" min-width="90">
|
<template #default="{ row }">{{ couponExpiredText(row) }}</template>
|
</el-table-column>
|
<el-table-column prop="useCount" label="使用次数" min-width="90" />
|
<el-table-column label="状态" min-width="90">
|
<template #default="{ row }">{{ couponStatusText(row) }}</template>
|
</el-table-column>
|
<el-table-column label="操作" width="164" fixed="right" align="center">
|
<template #default="{ row }">
|
<div class="merchant-coupons-operation">
|
<el-button link @click="editCoupon(row)">编辑</el-button>
|
<el-button link @click="disableCoupon(row)">{{ row.status === 0 ? '启用' : '停用' }}</el-button>
|
<el-button link @click="deleteCoupon(row)">删除</el-button>
|
</div>
|
</template>
|
</el-table-column>
|
</el-table>
|
|
<div class="merchant-coupons-real-pagination">
|
<el-pagination
|
v-model:current-page="couponPagination.page"
|
v-model:page-size="couponPagination.size"
|
background
|
layout="total, sizes, prev, pager, next"
|
:page-sizes="[10, 20, 50]"
|
:total="couponPagination.total"
|
@change="loadCoupons"
|
/>
|
</div>
|
</template>
|
|
<template v-else>
|
<el-form ref="couponFormRef" :model="couponEditForm" :rules="couponRules" class="merchant-coupon-edit-form" label-width="72px" label-position="left">
|
<el-form-item class="coupon-edit-row" label="优惠券名称" prop="couponName" required>
|
<el-input v-model="couponEditForm.couponName" placeholder="请输入优惠券名称" />
|
</el-form-item>
|
|
<el-form-item class="coupon-edit-row coupon-amount-row" label="优惠券金额" prop="couponAmount" required>
|
<div class="coupon-amount-control">
|
<el-input v-model="couponEditForm.couponAmount" inputmode="decimal" maxlength="8" placeholder="请输入优惠券金额" />
|
<span>元</span>
|
</div>
|
</el-form-item>
|
|
<el-form-item class="coupon-edit-row coupon-requirement-row" label="使用要求" prop="fullAmount" :required="couponEditForm.useRequirement === 'full'">
|
<el-radio-group v-model="couponEditForm.useRequirement">
|
<el-radio label="full" class="coupon-full-radio">满</el-radio>
|
<el-input v-model="couponEditForm.fullAmount" class="coupon-full-amount">
|
<template #append>元可用</template>
|
</el-input>
|
<el-radio label="none" class="coupon-none-radio">不限</el-radio>
|
</el-radio-group>
|
</el-form-item>
|
|
<div class="coupon-edit-row">
|
<label>可用人才</label>
|
<el-checkbox-group v-model="couponEditForm.talentLevels" class="coupon-talent-checkboxes" style="display: none">
|
<el-checkbox label="卓越人才卡">卓越人才卡</el-checkbox>
|
<el-checkbox label="杰出人才卡">杰出人才卡</el-checkbox>
|
<el-checkbox label="菁英人才卡">菁英人才卡</el-checkbox>
|
</el-checkbox-group>
|
<div class="coupon-talent-rows" style="display: grid; gap: 22px; padding: 4px 0">
|
<div v-for="level in couponTalentOptions" :key="level" class="coupon-talent-row" style="display: flex; align-items: center; gap: 18px; min-height: 32px">
|
<strong style="width: 90px; font-weight: 500">{{ level }}</strong>
|
<el-switch :model-value="couponEditForm.talentLevels.includes(level)" @change="(enabled: boolean) => toggleCouponTalentLevel(level, enabled)" />
|
<el-input v-model="couponLevelQuantities[level]" :disabled="!couponEditForm.talentLevels.includes(level)" placeholder="请输入" style="width: 108px" />
|
<span>张</span>
|
</div>
|
</div>
|
<div v-for="level in couponEditForm.talentLevels" :key="level" class="coupon-level-quantity" style="display: none">
|
<span>{{ level }}</span>
|
<el-input-number v-model="couponLevelQuantities[level]" :min="1" :max="99" :precision="0" :step="1" controls-position="right" />
|
<span>张</span>
|
</div>
|
</div>
|
|
<el-form-item class="coupon-edit-row" label="有效期" prop="validTime" required>
|
<el-date-picker
|
v-model="couponEditForm.validTime"
|
class="coupon-valid-time-picker"
|
type="daterange"
|
start-placeholder="开始日期"
|
end-placeholder="结束日期"
|
range-separator="至"
|
value-format="YYYY-MM-DD"
|
/>
|
</el-form-item>
|
|
<el-form-item class="coupon-edit-row coupon-product-scope-row" label="可用商品" prop="productScope" :required="couponEditForm.productScope === 'selected'">
|
<el-radio-group v-model="couponEditForm.productScope">
|
<el-radio label="all">不限</el-radio>
|
<el-radio label="selected">选择</el-radio>
|
<strong>已选择{{ selectedCouponProductIds.length }}件商品</strong>
|
</el-radio-group>
|
</el-form-item>
|
|
<div class="coupon-product-select-table">
|
<el-table :data="couponProducts" border class="coupon-edit-product-table" :show-header="true">
|
<el-table-column width="98" align="center">
|
<template #default="{ row }">
|
<el-checkbox
|
:model-value="couponProductSelected(row)"
|
@change="(checked: boolean) => toggleCouponProduct(row, checked)"
|
/>
|
</template>
|
</el-table-column>
|
<el-table-column prop="id" label="id" min-width="98" />
|
<el-table-column prop="productName" label="商品名称" min-width="98" />
|
<el-table-column label="封面图" min-width="98">
|
<template #default="{ row }">{{ row.coverUrl ? '已上传' : '' }}</template>
|
</el-table-column>
|
<el-table-column label="商品金额" min-width="98">
|
<template #default="{ row }">{{ productAmountText(row) }}</template>
|
</el-table-column>
|
<el-table-column prop="salesCount" label="销量" min-width="98" />
|
<el-table-column label="修改时间" min-width="98">
|
<template #default="{ row }">{{ formatDisplayTime(row.submitTime || row.createdAt) }}</template>
|
</el-table-column>
|
<el-table-column label="状态" min-width="98">
|
<template #default="{ row }">{{ productAuditStatusText(row.auditStatus) }}</template>
|
</el-table-column>
|
</el-table>
|
</div>
|
|
<div class="coupon-edit-row coupon-switch-row">
|
<label>是否到店消费可用</label>
|
<el-switch v-model="couponEditForm.storeConsume" />
|
</div>
|
|
<div class="coupon-edit-row coupon-switch-row">
|
<label>是否政府补贴</label>
|
<el-switch v-model="couponEditForm.governmentSubsidy" />
|
</div>
|
|
<div class="coupon-edit-actions">
|
<el-button type="primary" :loading="couponSaving" @click="saveCoupon">保存</el-button>
|
<el-button @click="cancelCoupon">取消</el-button>
|
</div>
|
</el-form>
|
</template>
|
</section>
|
</template>
|
|
<section v-else class="merchant-create-panel merchant-create-empty-panel" />
|
|
<el-dialog v-model="schoolDialogVisible" :title="editingSchoolIndex >= 0 ? '编辑学校' : '新增学校'" width="820px" destroy-on-close :close-on-click-modal="false">
|
<el-form ref="schoolFormRef" :model="editingSchool" :rules="schoolRules" label-width="100px" style="padding: 20px 20px 0;">
|
<el-form-item label="学校名称" prop="name" required>
|
<el-input v-model="editingSchool.name" placeholder="请输入" style="width: 320px" />
|
</el-form-item>
|
<el-form-item label="地址" prop="address" required>
|
<el-input v-model="editingSchool.address" placeholder="请输入" style="width: 320px" />
|
</el-form-item>
|
<el-form-item label="定位点">
|
<el-button type="primary" size="small" @click="markSchoolLocation">{{ editingSchool.latitude ? '已标点' : '未标点' }}</el-button>
|
</el-form-item>
|
<el-form-item label="学校详情">
|
<AdminRichTextEditor :model-value="editingSchool.detail || ''" placeholder="请输入学校详情" @update:model-value="value => { editingSchool.detail = value }" />
|
</el-form-item>
|
</el-form>
|
<template #footer>
|
<el-button @click="schoolDialogVisible = false">取消</el-button>
|
<el-button type="primary" @click="saveSchool">保存</el-button>
|
</template>
|
</el-dialog>
|
|
<el-dialog v-model="locationDialogVisible" title="选择地址" width="820px" destroy-on-close :close-on-click-modal="false" @closed="closeLocationMap">
|
<div class="merchant-location-picker">
|
<div class="merchant-map-search"><el-input v-model="locationSearch" placeholder="搜索商家地址或地点" clearable @keyup.enter="searchMapLocation" /><el-button type="primary" :loading="locationSearching" @click="searchMapLocation">搜索</el-button></div>
|
<div v-if="locationSearchResults.length" class="merchant-map-results"><button v-for="item in locationSearchResults" :key="`${item.longitude}-${item.latitude}`" type="button" @click="selectSearchLocation(item)">{{ item.name }}</button></div>
|
<div ref="mapElement" class="merchant-location-map" />
|
<div class="merchant-location-coordinates">已选坐标:{{ pendingLongitude || '-' }}, {{ pendingLatitude || '-' }}<span>可搜索地点或直接点击地图放置标点</span></div>
|
</div>
|
<template #footer><el-button @click="locationDialogVisible = false">取消</el-button><el-button type="primary" @click="saveLocation">确认</el-button></template>
|
</el-dialog>
|
</div>
|
</template>
|