| | |
| | | deleteProduct as deleteProductApi, |
| | | getAdminCouponDetail, |
| | | getAdminCoupons, |
| | | getAdminProducts, |
| | | getAdminMerchantProducts, |
| | | updateCoupon, |
| | | updateCouponStatus, |
| | | updateMerchant, |
| | |
| | | const merchantHasLoginAccount = ref(false) |
| | | const imageInputRef = ref<HTMLInputElement>() |
| | | const imageUploading = ref(false) |
| | | const imageUploadTarget = ref<'merchant' | 'productCover' | 'productImage' | 'approvalWindow'>('merchant') |
| | | const imageUploadTarget = ref<'merchant' | 'productCover' | 'productImage' | 'approvalWindow' | 'fuelHero'>('merchant') |
| | | const merchantCoverPreviewFailed = ref(false) |
| | | const productCoverPreviewFailed = ref(false) |
| | | const routeTab = typeof route.query.tab === 'string' ? route.query.tab : 'settings' |
| | |
| | | const hasValidMerchantId = computed(() => /^\d+$/.test(queryString('merchantId'))) |
| | | const isEditMode = computed(() => route.query.mode === 'edit' && hasValidMerchantId.value) |
| | | const hasExistingMerchant = computed(() => isEditMode.value) |
| | | const merchantDetailLoaded = ref(!isEditMode.value) |
| | | const originalMerchantLoginAccount = ref('') |
| | | 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 isFuelMerchant = computed(() => normalizedMerchantType.value === '加油服务') |
| | | const isChildcareMerchant = computed(() => normalizedMerchantType.value === '子女托管') |
| | | const isFinancialMerchant = computed(() => normalizedMerchantType.value === '金融服务') |
| | | const merchantTypeAliases: Record<string, string> = { |
| | | '餐饮服务': 'food', food: 'food', |
| | | '景区服务': 'scenic', scenic: 'scenic', |
| | | '酒店服务': 'hotel', hotel: 'hotel', |
| | | '公寓租赁': 'apartment', apartment: 'apartment', |
| | | '自助洗车': 'carWash', '自助洗车服务': 'carWash', carWash: 'carWash', |
| | | '免费体检': 'medical', medical: 'medical', |
| | | '停车服务': 'parking', parking: 'parking', |
| | | '加油服务': 'fuel', fuel: 'fuel', |
| | | '子女托管': 'children', children: 'children', |
| | | '学生托付': 'children', '学生托管': 'children', '子女托管服务': 'children', |
| | | '金融服务': 'finance', finance: 'finance', |
| | | '配偶推荐就业': 'employment', employment: 'employment', |
| | | '行政审批': 'approval', approval: 'approval', |
| | | '观影服务': 'cinema', cinema: 'cinema', theatre: 'cinema', '电影院': 'cinema', '影院': 'cinema', '影城': 'cinema', '剧院': 'cinema', |
| | | '购房优惠': 'housing', housing: 'housing', |
| | | '购车服务': 'car', car: 'car', |
| | | '家电服务': 'appliance', appliance: 'appliance', |
| | | '数码服务': 'digital', digital: 'digital', |
| | | '文化服务': 'culture', culture: 'culture', book: 'culture', |
| | | '电信服务': 'telecom', telecom: 'telecom', |
| | | '口腔治疗保健': 'dental', dental: 'dental', |
| | | } |
| | | const merchantTypeCode = computed(() => { |
| | | const raw = String(form.merchantTypeCode || normalizedMerchantType.value).trim() |
| | | return merchantTypeAliases[raw] || raw |
| | | }) |
| | | const isFoodMerchant = computed(() => merchantTypeCode.value === 'food') |
| | | const isApartmentMerchant = computed(() => merchantTypeCode.value === 'apartment') |
| | | const isHotelMerchant = computed(() => merchantTypeCode.value === 'hotel') |
| | | const isCarWashMerchant = computed(() => merchantTypeCode.value === 'carWash') |
| | | const isHealthCheckMerchant = computed(() => merchantTypeCode.value === 'medical') |
| | | const isParkingMerchant = computed(() => merchantTypeCode.value === 'parking') |
| | | const isFuelMerchant = computed(() => merchantTypeCode.value === 'fuel') |
| | | const isChildcareMerchant = computed(() => merchantTypeCode.value === 'children') |
| | | const isFinancialMerchant = computed(() => merchantTypeCode.value === 'finance') |
| | | const isMovieMerchant = computed(() => [ |
| | | '观影服务', 'cinema', 'theatre', '电影院', '影院', '影城', '剧院', |
| | | ].includes(normalizedMerchantType.value)) |
| | | const isSpouseEmploymentMerchant = computed(() => normalizedMerchantType.value === '配偶推荐就业') |
| | | const isApprovalMerchant = computed(() => normalizedMerchantType.value === '行政审批') |
| | | const isServiceMerchant = computed(() => isFoodMerchant.value || normalizedMerchantType.value === '景区服务') |
| | | 'cinema', |
| | | ].includes(merchantTypeCode.value)) |
| | | const isSpouseEmploymentMerchant = computed(() => merchantTypeCode.value === 'employment') |
| | | const isApprovalMerchant = computed(() => merchantTypeCode.value === 'approval') |
| | | const isBookMerchant = computed(() => merchantTypeCode.value === 'culture') |
| | | const couponWithoutProducts = computed(() => isBookMerchant.value || isChildcareMerchant.value) |
| | | const couponStoreConsumeOnly = computed(() => couponWithoutProducts.value || isHotelMerchant.value) |
| | | const isServiceMerchant = computed(() => isFoodMerchant.value || merchantTypeCode.value === 'scenic') |
| | | const supportsStoreConsumption = computed(() => [ |
| | | '酒店服务', '景区服务', '餐饮服务', '口腔治疗保健', '免费体检', |
| | | ].includes(normalizedMerchantType.value) || (isCarWashMerchant.value && form.miniProgramJumpType === 'normal')) |
| | | 'hotel', 'scenic', 'food', 'dental', 'culture', 'children', |
| | | ].includes(merchantTypeCode.value) || (isCarWashMerchant.value && form.miniProgramJumpType === 'normal')) |
| | | const merchantAppLoginOptionalTypes = [ |
| | | '购房优惠', '购车服务', '家电服务', '数码服务', '文化服务', '电信服务', '口腔治疗保健', '加油服务', |
| | | 'housing', 'car', 'appliance', 'digital', 'culture', 'telecom', 'dental', 'fuel', |
| | | ] |
| | | const simplifiedMerchantSettingsTypes = [ |
| | | '购房优惠', '购车服务', '家电服务', '数码服务', '文化服务', '电信服务', '口腔治疗保健', '加油服务', |
| | | 'housing', 'car', 'appliance', 'digital', 'culture', 'telecom', 'dental', 'fuel', |
| | | ] |
| | | const usesSimplifiedMerchantSettings = computed(() => simplifiedMerchantSettingsTypes.includes(normalizedMerchantType.value)) |
| | | const usesSimplifiedMerchantSettings = computed(() => simplifiedMerchantSettingsTypes.includes(merchantTypeCode.value)) |
| | | const settingsAndRightsOnlyMerchantTypes = [ |
| | | '加油服务', '购房优惠', '购车服务', '家电服务', '数码服务', '文化服务', '电信服务', |
| | | 'fuel', 'housing', 'car', 'appliance', 'digital', 'culture', 'telecom', |
| | | ] |
| | | const usesSettingsAndRightsOnlyTabs = computed(() => settingsAndRightsOnlyMerchantTypes.includes(normalizedMerchantType.value)) |
| | | const usesSettingsAndRightsOnlyTabs = computed(() => settingsAndRightsOnlyMerchantTypes.includes(merchantTypeCode.value)) |
| | | const supportsMerchantAppLogin = computed(() => ( |
| | | isServiceMerchant.value |
| | | || isApartmentMerchant.value |
| | |
| | | || isMovieMerchant.value |
| | | || isSpouseEmploymentMerchant.value |
| | | || isApprovalMerchant.value |
| | | || merchantAppLoginOptionalTypes.includes(normalizedMerchantType.value) |
| | | || merchantAppLoginOptionalTypes.includes(merchantTypeCode.value) |
| | | )) |
| | | const requiresMerchantOperator = computed(() => (!isEditMode.value || !merchantHasLoginAccount.value) && ( |
| | | normalizedMerchantType.value === '景区服务' |
| | | const merchantOperatorEnabled = computed(() => ( |
| | | merchantTypeCode.value === 'scenic' |
| | | || (supportsMerchantAppLogin.value && Number(form.canLoginMerchantApp) === 1) |
| | | )) |
| | | const requiresMerchantOperator = computed(() => (!isEditMode.value || !merchantHasLoginAccount.value) && merchantOperatorEnabled.value) |
| | | function carWashJumpMeta() { |
| | | if (!isCarWashMerchant.value) return {} |
| | | if (form.miniProgramJumpType !== 'miniprogram') { |
| | |
| | | |
| | | function defaultMerchantRecommend() { |
| | | const merchantType = queryString('merchantType') |
| | | return merchantType === '餐饮服务' || merchantType === '景区服务' ? 1 : 0 |
| | | const merchantTypeCode = merchantTypeAliases[merchantType] || merchantType |
| | | return merchantTypeCode === 'food' || merchantTypeCode === 'scenic' ? 1 : 0 |
| | | } |
| | | |
| | | function applyNewMerchantDefaults() { |
| | |
| | | |
| | | const merchantType = queryString('merchantType') |
| | | form.merchantType = merchantType |
| | | form.recommend = merchantType === '餐饮服务' || merchantType === '景区服务' ? 1 : 0 |
| | | form.merchantTypeCode = '' |
| | | const merchantTypeCode = merchantTypeAliases[merchantType] || merchantType |
| | | form.recommend = merchantTypeCode === 'food' || merchantTypeCode === 'scenic' ? 1 : 0 |
| | | form.sortNo = undefined |
| | | } |
| | | |
| | | function requiresRightsTag() { |
| | | return !(isCarWashMerchant.value && form.miniProgramJumpType === 'miniprogram') && !isApprovalMerchant.value |
| | | } |
| | | |
| | | function hasMissingRightsTag() { |
| | | return requiresRightsTag() && rightsConfigs.some((item) => !String(item.tag || '').trim()) |
| | | } |
| | | |
| | | const tabs = computed(() => { |
| | | if (couponWithoutProducts.value) { |
| | | return [ |
| | | { label: '商家设置', value: 'settings' }, |
| | | { label: '商家权益', value: 'rights' }, |
| | | { label: '优惠券', value: 'coupons' }, |
| | | ] |
| | | } |
| | | if (usesSettingsAndRightsOnlyTabs.value) { |
| | | return [ |
| | | { label: '商家设置', value: 'settings' }, |
| | |
| | | { label: '团单', value: 'products' }, |
| | | ] |
| | | } |
| | | if (isParkingMerchant.value || isChildcareMerchant.value || isFinancialMerchant.value || isMovieMerchant.value || isSpouseEmploymentMerchant.value || isApprovalMerchant.value) { |
| | | if (isParkingMerchant.value || isFinancialMerchant.value || isMovieMerchant.value || isSpouseEmploymentMerchant.value || isApprovalMerchant.value) { |
| | | return [ |
| | | { label: '商家设置', value: 'settings' }, |
| | | { label: '商家权益', value: 'rights' }, |
| | |
| | | ] |
| | | }) |
| | | |
| | | const form = reactive<MerchantPayload & { createdAt: string }>({ |
| | | const form = reactive<MerchantPayload & { createdAt: string; merchantTypeCode?: string }>({ |
| | | merchantName: '', |
| | | merchantType: queryString('merchantType', ''), |
| | | contactMobile: '', |
| | |
| | | const merchantRules = computed<FormRules>(() => ({ |
| | | merchantName: [{ required: true, message: '请输入商家名称', trigger: 'blur' }], |
| | | contactMobile: [{ required: true, message: '请输入联系电话', trigger: 'blur' }], |
| | | address: isApartmentMerchant.value ? [] : [{ required: true, message: '请输入商家地址', trigger: 'blur' }], |
| | | address: isApartmentMerchant.value ? [] : [{ required: true, message: '请输入商家地址', trigger: ['blur', 'change'] }], |
| | | coverUrl: [{ required: true, message: '请上传商家封面图', trigger: 'change' }], |
| | | sortNo: [{ |
| | | validator: (_rule, value, callback) => { |
| | |
| | | loginPassword: '', |
| | | confirmPassword: '', |
| | | }) |
| | | // 编辑已有登录账号的商家时,密码可以留空(留空表示不修改),此时不标必填、也不校验。 |
| | | const passwordFieldsRequired = computed(() => requiresMerchantOperator.value || (isEditMode.value && accountForm.loginPassword.length > 0)) |
| | | const accountRules = computed<FormRules>(() => ({ |
| | | loginAccount: [{ |
| | | // 规则里必须显式声明 required:否则 el-form-item 上的 required 会让 Element Plus |
| | | // 追加一条只有 required 的空规则,报出框架默认的英文 "loginAccount is required"。 |
| | | required: true, |
| | | validator: (_rule, value, callback) => { |
| | | if (!requiresMerchantOperator.value) return callback() |
| | | if (!String(value || '').trim()) return callback(new Error('请输入登录账号')) |
| | |
| | | trigger: 'blur', |
| | | }], |
| | | loginPassword: [{ |
| | | required: true, |
| | | validator: (_rule, value, callback) => { |
| | | const updatingPassword = isEditMode.value && String(value || '').length > 0 |
| | | if (!requiresMerchantOperator.value && !updatingPassword) return callback() |
| | |
| | | trigger: 'blur', |
| | | }], |
| | | confirmPassword: [{ |
| | | required: true, |
| | | validator: (_rule, value, callback) => { |
| | | const needsConfirmation = requiresMerchantOperator.value || (isEditMode.value && accountForm.loginPassword.length > 0) |
| | | if (!needsConfirmation) return callback() |
| | |
| | | } |
| | | |
| | | async function checkLoginAccountAvailable() { |
| | | if (!requiresMerchantOperator.value) return true |
| | | if (!/^[A-Za-z0-9]{1,32}$/.test(accountForm.loginAccount)) return false |
| | | // Loading an edit record programmatically updates and may blur this field. |
| | | // Do not validate until its current operator account has been identified. |
| | | if (isEditMode.value && !merchantDetailLoaded.value) return true |
| | | const normalizedAccount = accountForm.loginAccount.trim() |
| | | if (isEditMode.value && originalMerchantLoginAccount.value !== '' |
| | | && normalizedAccount === originalMerchantLoginAccount.value) { |
| | | accountTaken.value = false |
| | | return true |
| | | } |
| | | // An existing merchant keeps its owner account; it must not be checked |
| | | // against itself, and any stale validation message should be cleared. |
| | | if (!requiresMerchantOperator.value) { |
| | | accountTaken.value = false |
| | | return true |
| | | } |
| | | if (!/^[A-Za-z0-9]{1,32}$/.test(normalizedAccount)) return false |
| | | accountChecking.value = true |
| | | try { |
| | | const result = await checkMerchantLoginAccount(accountForm.loginAccount) |
| | | const currentMerchantId = isEditMode.value ? queryString('merchantId') : undefined |
| | | const result = await checkMerchantLoginAccount(normalizedAccount, currentMerchantId) |
| | | accountTaken.value = !result.available |
| | | if (accountTaken.value) ElMessage.warning('登录账号已存在,请更换') |
| | | return result.available |
| | |
| | | }], |
| | | validTime: [{ required: true, message: '请选择优惠券有效期', trigger: 'change' }], |
| | | productScope: [{ |
| | | validator: (_rule, value, callback) => value !== 'selected' || selectedCouponProductIds.value.length > 0 |
| | | validator: (_rule, value, callback) => couponStoreConsumeOnly.value || value !== 'selected' || selectedCouponProductIds.value.length > 0 |
| | | ? callback() |
| | | : callback(new Error('请至少选择一个可用商品')), |
| | | trigger: 'change', |
| | |
| | | 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 === '行政审批' |
| | | const currentType = merchantTypeCode.value |
| | | const supportsDefaultStoreConsumption = ['food', 'scenic', 'dental', 'culture', 'children'].includes(currentType) |
| | | || (currentType === 'carWash' && form.miniProgramJumpType === 'normal') |
| | | const isHotel = currentType === 'hotel' |
| | | const isCarWash = currentType === 'carWash' |
| | | const isHealthCheck = currentType === 'medical' |
| | | const isParking = currentType === 'parking' |
| | | const isChildcare = currentType === 'children' |
| | | const isFinancial = currentType === 'finance' |
| | | const isMovie = currentType === 'cinema' |
| | | const isSpouseEmployment = currentType === 'employment' |
| | | const isApproval = currentType === 'approval' |
| | | |
| | | if (isHealthCheck) { |
| | | return [ |
| | |
| | | enabled: true, |
| | | tag: '', |
| | | detail: '', |
| | | storeConsume: true, |
| | | storeConsume: false, |
| | | discount: '', |
| | | governmentSubsidy: true, |
| | | governmentSubsidy: false, |
| | | consumeDesc: '', |
| | | consumeEnabled: true, |
| | | consumeEnabled: false, |
| | | }, |
| | | { |
| | | level: '杰出人才卡', |
| | | enabled: true, |
| | | tag: '', |
| | | detail: '', |
| | | storeConsume: true, |
| | | storeConsume: false, |
| | | discount: '', |
| | | governmentSubsidy: true, |
| | | governmentSubsidy: false, |
| | | consumeDesc: '', |
| | | consumeEnabled: true, |
| | | consumeEnabled: false, |
| | | }, |
| | | { |
| | | level: '菁英人才卡', |
| | | enabled: true, |
| | | tag: '', |
| | | detail: '', |
| | | storeConsume: true, |
| | | storeConsume: false, |
| | | discount: '', |
| | | governmentSubsidy: true, |
| | | governmentSubsidy: false, |
| | | consumeDesc: '', |
| | | consumeEnabled: true, |
| | | consumeEnabled: false, |
| | | }, |
| | | ] |
| | | } |
| | |
| | | enabled: true, |
| | | tag: '', |
| | | detail: '', |
| | | storeConsume: false, |
| | | storeConsume: true, |
| | | discount: '', |
| | | governmentSubsidy: false, |
| | | governmentSubsidy: true, |
| | | consumeDesc: '', |
| | | consumeEnabled: false, |
| | | consumeEnabled: true, |
| | | }, |
| | | { |
| | | level: '杰出人才卡', |
| | | enabled: true, |
| | | tag: '', |
| | | detail: '', |
| | | storeConsume: false, |
| | | storeConsume: true, |
| | | discount: '', |
| | | governmentSubsidy: false, |
| | | governmentSubsidy: true, |
| | | consumeDesc: '', |
| | | consumeEnabled: false, |
| | | consumeEnabled: true, |
| | | }, |
| | | { |
| | | level: '菁英人才卡', |
| | | enabled: true, |
| | | tag: '', |
| | | detail: '', |
| | | storeConsume: false, |
| | | storeConsume: true, |
| | | discount: '', |
| | | governmentSubsidy: false, |
| | | governmentSubsidy: true, |
| | | consumeDesc: '', |
| | | consumeEnabled: false, |
| | | consumeEnabled: true, |
| | | }, |
| | | ] |
| | | } |
| | |
| | | const parkingAddresses = ref<ParkingAddress[]>([]) |
| | | const activeParkingAddress = ref<ParkingAddress>() |
| | | const fuelStores = ref<FuelStore[]>([]) |
| | | // 加油服务详情页轮播图(后台维护,保存时整表替换)。 |
| | | const fuelHeroImages = ref<Array<{ id?: number | string; imageUrl: string; enabled: boolean }>>([]) |
| | | // 可配置详情页轮播图的商家类型:加油服务、子女托管、行政审批。 |
| | | const supportsHeroImages = computed(() => isFuelMerchant.value || isChildcareMerchant.value || isApprovalMerchant.value) |
| | | const merchantHeroImageTip = computed(() => isFuelMerchant.value ? '加油服务' : (isChildcareMerchant.value ? '子女托管' : '行政审批')) |
| | | const activeFuelStore = ref<FuelStore>() |
| | | const schoolAddresses = ref<SchoolAddress[]>([]) |
| | | const activeSchoolAddress = ref<SchoolAddress>() |
| | |
| | | return new Promise((resolve) => { |
| | | const geolocation = window.navigator?.geolocation |
| | | if (!geolocation) { |
| | | ElMessage.info('当前浏览器不支持定位,请直接搜索或点击地图标记') |
| | | console.info('browser geolocation is unavailable, fall back to the default map center') |
| | | resolve(undefined) |
| | | return |
| | | } |
| | | const hostname = window.location.hostname |
| | | // 浏览器只允许在安全上下文(HTTPS 或 localhost)里定位,局域网 IP + HTTP 必然被拒。 |
| | | // 定位只是"把地图居中"的便利功能,这里静默跳过,不再弹提示打断选点流程。 |
| | | if (!window.isSecureContext && hostname !== 'localhost' && hostname !== '127.0.0.1') { |
| | | ElMessage.warning('浏览器定位需要使用 localhost 或 HTTPS 地址访问') |
| | | console.info('browser geolocation requires a secure context; keeping the default map center') |
| | | resolve(undefined) |
| | | return |
| | | } |
| | |
| | | resolve(Number.isFinite(longitude) && Number.isFinite(latitude) ? { longitude, latitude } : undefined) |
| | | }, |
| | | (error) => { |
| | | if (error.code === error.PERMISSION_DENIED) ElMessage.info('未获得浏览器定位权限,请直接搜索或点击地图标记') |
| | | console.info(`browser geolocation unavailable: ${error.code}`) |
| | | resolve(undefined) |
| | | }, |
| | | { enableHighAccuracy: true, timeout: 10000, maximumAge: 0 }, |
| | |
| | | activeParkingAddress.value.address = normalizedAddress |
| | | } else if (activeSchoolAddress.value) { |
| | | activeSchoolAddress.value.address = normalizedAddress |
| | | // 地图选点/反查地址是程序写入,不会触发 blur/change,旧的"请输入地址"提示会一直留着。 |
| | | schoolFormRef.value?.clearValidate('address') |
| | | } else if (activeFuelStore.value) { |
| | | activeFuelStore.value.address = normalizedAddress |
| | | } else { |
| | | form.address = normalizedAddress |
| | | // 同理:选完地址立刻清掉"请输入商家地址",避免看起来像没选成功。 |
| | | merchantFormRef.value?.clearValidate('address') |
| | | } |
| | | locationSearch.value = normalizedAddress |
| | | } |
| | |
| | | ElMessage.success('地图位置已标记') |
| | | } |
| | | |
| | | function changeCover(target: 'merchant' | 'productCover' | 'productImage' | 'approvalWindow') { |
| | | function changeCover(target: 'merchant' | 'productCover' | 'productImage' | 'approvalWindow' | 'fuelHero') { |
| | | imageUploadTarget.value = target |
| | | if (imageInputRef.value) imageInputRef.value.accept = target === 'approvalWindow' ? 'application/pdf,.pdf' : 'image/jpeg,image/png,image/webp' |
| | | imageInputRef.value?.click() |
| | |
| | | productForm.coverUrl = result.url |
| | | resetProductCoverPreview() |
| | | productFormRef.value?.validateField('coverUrl').catch(() => undefined) |
| | | } else if (imageUploadTarget.value === 'fuelHero') { |
| | | if (!fuelHeroImages.value.some((image) => image.imageUrl === result.url)) { |
| | | fuelHeroImages.value.push({ imageUrl: result.url, enabled: true }) |
| | | } |
| | | } else if (!productImages.value.includes(result.url)) { |
| | | productImages.value.push(result.url) |
| | | } |
| | |
| | | } |
| | | rightsConfigs.splice(0, rightsConfigs.length, ...defaultMerchantRights().map((item) => { |
| | | const saved = Array.isArray(savedRights) ? savedRights.find((entry) => entry?.level === item.level) : undefined |
| | | return { |
| | | const merged = { |
| | | ...item, |
| | | ...(saved || {}), |
| | | consumeEnabled: item.consumeEnabled |
| | | } |
| | | if (isParkingMerchant.value && merged.parkingType === 'paid') merged.consumeEnabled = true |
| | | if (supportsStoreConsumption.value && merged.level !== '普通游客') merged.consumeEnabled = true |
| | | return merged |
| | | })) |
| | | syncCarWashJumpMeta() |
| | | } |
| | |
| | | const payload = { ...item } |
| | | const supportsItemStoreConsumption = supportsStoreConsumption.value |
| | | || (isParkingMerchant.value && item.parkingType === 'paid') |
| | | if (isParkingMerchant.value && item.parkingType === 'paid') { |
| | | payload.consumeEnabled = true |
| | | } |
| | | if (!supportsItemStoreConsumption) { |
| | | payload.storeConsume = false |
| | | payload.discount = '' |
| | |
| | | return |
| | | } |
| | | |
| | | if (hasMissingRightsTag()) { |
| | | ElMessage.warning('请填写各个人才卡等级的标签') |
| | | return |
| | | } |
| | | |
| | | rightsSaving.value = true |
| | | try { |
| | | await updateMerchant(merchantId, { |
| | |
| | | canLoginMerchantApp: Number(form.canLoginMerchantApp ?? 1), |
| | | groupBuyingEnabled: form.groupBuyingEnabled, |
| | | applicableStores: isFuelMerchant.value ? fuelStores.value : undefined, |
| | | heroImages: supportsHeroImages.value ? fuelHeroImages.value : undefined, |
| | | usageInstructions: isParkingMerchant.value |
| | | ? JSON.stringify(parkingAddresses.value) |
| | | : isChildcareMerchant.value |
| | |
| | | const amount = row.salePrice ?? row.originalPrice |
| | | if (amount === undefined || amount === null) return '' |
| | | return Number(amount).toFixed(2).replace(/\.00$/, '') |
| | | } |
| | | |
| | | // 销量是计数:0 也要显示成 0,不能因为 0 变空白让人以为没统计出来。 |
| | | function salesCountText(row: Product) { |
| | | const count = row.salesCount |
| | | return count === undefined || count === null ? 0 : count |
| | | } |
| | | |
| | | function parseJsonValue<T>(value: unknown, fallback: T): T { |
| | |
| | | |
| | | productLoading.value = true |
| | | try { |
| | | const result = await getAdminProducts({ |
| | | const result = await getAdminMerchantProducts(merchantId, { |
| | | page: productPagination.page, |
| | | size: productPagination.size, |
| | | merchantId, |
| | | keyword: productFilters.productName || undefined, |
| | | status: productFilters.status, |
| | | createdStart: productFilters.createdTime?.[0], |
| | |
| | | } |
| | | |
| | | async function loadCouponProducts() { |
| | | if (couponStoreConsumeOnly.value) { |
| | | couponProducts.value = [] |
| | | selectedCouponProductIds.value = [] |
| | | return |
| | | } |
| | | const merchantId = currentMerchantId() |
| | | if (!merchantId) { |
| | | couponProducts.value = [] |
| | | return |
| | | } |
| | | const result = await getAdminProducts({ page: 1, size: 100, merchantId }) |
| | | const result = await getAdminMerchantProducts(merchantId, { page: 1, size: 100 }) |
| | | couponProducts.value = result.records |
| | | } |
| | | |
| | |
| | | couponEditForm.couponAmount = '' |
| | | couponEditForm.talentLevels = ['卓越人才卡', '杰出人才卡', '菁英人才卡'] |
| | | couponEditForm.validTime = [] |
| | | couponEditForm.productScope = 'selected' |
| | | couponEditForm.productScope = couponStoreConsumeOnly.value ? 'all' : 'selected' |
| | | couponEditForm.storeConsume = true |
| | | couponEditForm.governmentSubsidy = true |
| | | couponEditForm.status = 1 |
| | |
| | | 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.productScope = couponStoreConsumeOnly.value || 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 |
| | |
| | | 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, |
| | | productScope: couponStoreConsumeOnly.value || couponEditForm.productScope === 'all' ? 'ALL' : 'SELECTED', |
| | | productIds: couponStoreConsumeOnly.value || couponEditForm.productScope === 'all' ? [] : selectedCouponProductIds.value, |
| | | storeConsume: couponStoreConsumeOnly.value || couponEditForm.storeConsume, |
| | | governmentSubsidy: couponEditForm.governmentSubsidy, |
| | | status: couponEditForm.status, |
| | | } |
| | |
| | | loginPassword: !isEditMode.value || updatingMerchantPassword || !merchantHasLoginAccount.value ? accountForm.loginPassword : undefined, |
| | | groupBuyingEnabled: form.groupBuyingEnabled, |
| | | applicableStores: isFuelMerchant.value ? fuelStores.value : undefined, |
| | | heroImages: supportsHeroImages.value ? fuelHeroImages.value : undefined, |
| | | usageInstructions: isParkingMerchant.value |
| | | ? JSON.stringify(parkingAddresses.value) |
| | | : isChildcareMerchant.value |
| | |
| | | if (isCarWashMerchant.value) syncCarWashJumpMeta() |
| | | }) |
| | | |
| | | watch(requiresMerchantOperator, (required) => { |
| | | if (!required) accountTaken.value = false |
| | | }) |
| | | |
| | | // 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 }) |
| | |
| | | form.canLoginMerchantApp = Number(merchant.canLoginMerchantApp ?? 1) |
| | | form.recommend = Number(merchant.isRecommend ?? 0) |
| | | accountForm.loginAccount = merchant.loginAccount || '' |
| | | originalMerchantLoginAccount.value = String(merchant.loginAccount || '').trim() |
| | | merchantHasLoginAccount.value = Boolean(String(merchant.loginAccount || '').trim()) |
| | | accountTaken.value = false |
| | | if (isParkingMerchant.value) { |
| | | parkingAddresses.value = parseJsonValue<ParkingAddress[]>(merchant.usageInstructions, []) |
| | | } else if (isFuelMerchant.value) { |
| | |
| | | } else if (isApprovalMerchant.value) { |
| | | approvalItems.value = parseJsonValue<ApprovalItem[]>(merchant.usageInstructions, []) |
| | | } |
| | | // 详情页轮播图(加油 / 子女托管 / 行政审批)统一从 heroImages 读。 |
| | | if (supportsHeroImages.value) { |
| | | fuelHeroImages.value = Array.isArray(merchant.heroImages) |
| | | ? merchant.heroImages.map((image: any) => ({ |
| | | id: image.id, |
| | | imageUrl: String(image.imageUrl ?? image.image_url ?? ''), |
| | | enabled: Number(image.enabled ?? 1) === 1, |
| | | })).filter((image: any) => image.imageUrl !== '') |
| | | : [] |
| | | } |
| | | resetBusinessHours(merchant.businessHours) |
| | | resetMerchantRights(merchant.merchantRightsConfig) |
| | | resetCarWashJumpFromMerchant(merchant as Record<string, any>) |
| | | }).finally(() => { |
| | | merchantDetailLoaded.value = true |
| | | }) |
| | | } |
| | | if (activeTab.value === 'products') { |
| | |
| | | </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="如:到店消费" /> |
| | |
| | | <el-form-item v-if="supportsMerchantAppLogin" class="merchant-account-login-toggle" 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-form-item class="merchant-account-field" label="登录账号" prop="loginAccount" :required="merchantOperatorEnabled" :error="requiresMerchantOperator && 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-form-item class="merchant-account-field" label="登录密码" prop="loginPassword" :required="passwordFieldsRequired"> |
| | | <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-form-item class="merchant-account-field" label="确认密码" prop="confirmPassword" :required="passwordFieldsRequired"> |
| | | <el-input v-model="accountForm.confirmPassword" type="password" show-password :placeholder="isEditMode ? '再次输入新密码' : ''" /> |
| | | </el-form-item> |
| | | </el-form> |
| | |
| | | <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"> |
| | |
| | | <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="请输入" /> |
| | | <template v-if="requiresRightsTag()"> |
| | | <span><i class="merchant-rights-required">*</i>标签</span> |
| | | <el-input v-model="item.tag" placeholder="请输入" /> |
| | | </template> |
| | | </div> |
| | | |
| | | <div class="merchant-rights-detail-row"> |
| | |
| | | <span>券</span> |
| | | </div> |
| | | </div> |
| | | <div class="phone-preview-tag">{{ item.tag || '人才卡专享' }}</div> |
| | | <div class="phone-preview-tag">{{ item.tag || '请填写标签' }}</div> |
| | | <div class="phone-preview-address"> |
| | | <span>⌖</span> |
| | | <p>{{ form.address || '请填写商家地址' }}</p> |
| | |
| | | <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> |
| | | <span class="hotel-reset-label">重置周期</span> |
| | | <el-select v-model="item.freeVoucherCycle" class="hotel-cycle-select"> |
| | | <el-option label="每天" value="每天" /> |
| | | <el-option label="每周" value="每周" /> |
| | |
| | | </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 class="merchant-parking-address-actions"><el-button plain @click="addFuelStore">新增</el-button></div> |
| | | </section> |
| | | |
| | | <section v-if="supportsHeroImages" class="merchant-parking-address-panel"> |
| | | <h3 class="merchant-parking-address-title">详情页图片</h3> |
| | | <p class="merchant-fuel-hero-tip">显示在人才端{{ merchantHeroImageTip }}详情页顶部,按顺序展示、只展示已启用的图片;不配置时自动使用商家封面图。推荐 750 × 730 px。</p> |
| | | <el-table :data="fuelHeroImages" border> |
| | | <el-table-column label="图片" width="140" align="center"> |
| | | <template #default="{ row }"> |
| | | <img v-if="row.imageUrl" class="merchant-fuel-hero-preview" :src="row.imageUrl" alt="详情页图片" /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="启用" width="120" align="center"> |
| | | <template #default="{ row }"><el-switch v-model="row.enabled" /></template> |
| | | </el-table-column> |
| | | <el-table-column label="操作" width="100" fixed="right" align="center"> |
| | | <template #default="{ $index }"><el-button link type="danger" @click="fuelHeroImages.splice($index, 1)">删除</el-button></template> |
| | | </el-table-column> |
| | | </el-table> |
| | | <div class="merchant-parking-address-actions"> |
| | | <el-button plain :loading="imageUploading" @click="changeCover('fuelHero')">上传图片</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> |
| | |
| | | <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> |
| | | <template #default="{ row }">{{ salesCountText(row) }}</template> |
| | | </el-table-column> |
| | | <el-table-column label="修改时间" min-width="90"> |
| | | <template #default="{ row }">{{ formatDisplayTime(row.submitTime || row.createdAt) }}</template> |
| | |
| | | /> |
| | | </el-form-item> |
| | | |
| | | <el-form-item class="coupon-edit-row coupon-product-scope-row" label="可用商品" prop="productScope" :required="couponEditForm.productScope === 'selected'"> |
| | | <div v-if="couponStoreConsumeOnly" class="coupon-edit-row coupon-switch-row"> |
| | | <label>适用范围</label> |
| | | <span>到店消费</span> |
| | | </div> |
| | | |
| | | <el-form-item v-else 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> |
| | |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | |
| | | <div class="coupon-product-select-table"> |
| | | <div v-if="!couponStoreConsumeOnly" 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-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 prop="salesCount" label="销量" min-width="98"> |
| | | <template #default="{ row }">{{ salesCountText(row) }}</template> |
| | | </el-table-column> |
| | | <el-table-column label="修改时间" min-width="98"> |
| | | <template #default="{ row }">{{ formatDisplayTime(row.submitTime || row.createdAt) }}</template> |
| | | </el-table-column> |
| | |
| | | </el-table> |
| | | </div> |
| | | |
| | | <div class="coupon-edit-row coupon-switch-row"> |
| | | <div v-if="!couponStoreConsumeOnly" class="coupon-edit-row coupon-switch-row"> |
| | | <label>是否到店消费可用</label> |
| | | <el-switch v-model="couponEditForm.storeConsume" /> |
| | | </div> |