feat: adapt fuel stores and medical product audit details
| | |
| | | const stores: any[] = data != null && data.applicableStores instanceof Array ? data.applicableStores as any[] : [] |
| | | applicableStores.value = stores.map((item: any) => ({ |
| | | id: '' + item.id, |
| | | name: item.storeName != null ? item.storeName as string : '', |
| | | name: item.storeName != null ? item.storeName as string : (item.name != null ? item.name as string : ''), |
| | | address: item.address != null ? item.address as string : '', |
| | | latitude: item.latitude != null ? Number(item.latitude) : 0, |
| | | longitude: item.longitude != null ? Number(item.longitude) : 0, |
| | | canNavigate: item.canNavigate === true || Number(item.canNavigate) == 1 |
| | | canNavigate: (item.canNavigate === true || Number(item.canNavigate) == 1) |
| | | && Number(item.latitude) > 0 && Number(item.longitude) > 0 |
| | | })) |
| | | merchantLoading.value = false |
| | | listLoading.value = false |
| | |
| | | height: auto; |
| | | } |
| | | |
| | | .health-check-audit-table { |
| | | width: calc(100% - 14px); |
| | | margin-left: 14px; |
| | | } |
| | | |
| | | .merchant-product-rich-editor { |
| | | flex: 1; |
| | | width: 100%; |
| | |
| | | canLoginMerchantApp?: number |
| | | loginAccount?: string |
| | | groupBuyingEnabled?: number |
| | | applicableStores?: Array<{ |
| | | id?: number | string |
| | | name?: string |
| | | storeName?: string |
| | | address?: string |
| | | longitude?: number |
| | | latitude?: number |
| | | canNavigate?: boolean | number |
| | | }> |
| | | usageInstructions?: string |
| | | rightsText?: string |
| | | miniProgramJumpType?: string |
| | |
| | | loginAccount?: string |
| | | loginPassword?: string |
| | | groupBuyingEnabled?: number |
| | | applicableStores?: Array<{ |
| | | id?: number | string |
| | | name?: string |
| | | storeName?: string |
| | | address?: string |
| | | longitude?: number |
| | | latitude?: number |
| | | }> |
| | | usageInstructions?: string |
| | | rightsText?: string |
| | | miniProgramJumpType?: string |
| | |
| | | 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 isMovieMerchant = computed(() => [ |
| | |
| | | longitude?: number |
| | | latitude?: number |
| | | } |
| | | interface FuelStore { |
| | | id?: number | string |
| | | name: string |
| | | address: string |
| | | longitude?: number |
| | | latitude?: number |
| | | } |
| | | interface SchoolAddress { |
| | | name: string |
| | | address: string |
| | |
| | | |
| | | const parkingAddresses = ref<ParkingAddress[]>([]) |
| | | const activeParkingAddress = ref<ParkingAddress>() |
| | | const fuelStores = ref<FuelStore[]>([]) |
| | | const activeFuelStore = ref<FuelStore>() |
| | | const schoolAddresses = ref<SchoolAddress[]>([]) |
| | | const activeSchoolAddress = ref<SchoolAddress>() |
| | | const schoolDialogVisible = ref(false) |
| | |
| | | parkingAddresses.value.splice(index, 1) |
| | | } |
| | | |
| | | function addFuelStore() { |
| | | fuelStores.value.push({ name: '', address: '' }) |
| | | } |
| | | |
| | | function removeFuelStore(index: number) { |
| | | fuelStores.value.splice(index, 1) |
| | | } |
| | | |
| | | async function markFuelStoreLocation(row: FuelStore) { |
| | | activeParkingAddress.value = undefined |
| | | activeSchoolAddress.value = undefined |
| | | activeFuelStore.value = row |
| | | pendingLongitude.value = String(row.longitude || '') |
| | | pendingLatitude.value = String(row.latitude || '') |
| | | locationDialogVisible.value = true |
| | | locationSearch.value = row.address || '' |
| | | await initializeLocationMap() |
| | | } |
| | | |
| | | async function markParkingLocation(row: ParkingAddress) { |
| | | activeFuelStore.value = undefined |
| | | activeSchoolAddress.value = undefined |
| | | activeParkingAddress.value = row |
| | | pendingLongitude.value = String(row.longitude || '') |
| | |
| | | } |
| | | |
| | | async function markSchoolLocation() { |
| | | activeFuelStore.value = undefined |
| | | activeParkingAddress.value = undefined |
| | | activeSchoolAddress.value = editingSchool |
| | | pendingLongitude.value = String(editingSchool.longitude || '') |
| | |
| | | async function markLocation() { |
| | | activeParkingAddress.value = undefined |
| | | activeSchoolAddress.value = undefined |
| | | activeFuelStore.value = undefined |
| | | pendingLongitude.value = String(form.longitude || '') |
| | | pendingLatitude.value = String(form.latitude || '') |
| | | locationDialogVisible.value = true |
| | |
| | | } else if (activeSchoolAddress.value) { |
| | | activeSchoolAddress.value.longitude = longitude |
| | | activeSchoolAddress.value.latitude = latitude |
| | | } else if (activeFuelStore.value) { |
| | | activeFuelStore.value.longitude = longitude |
| | | activeFuelStore.value.latitude = latitude |
| | | } else { |
| | | form.longitude = longitude |
| | | form.latitude = latitude |
| | |
| | | } |
| | | } |
| | | |
| | | if (isFuelMerchant.value) { |
| | | if (fuelStores.value.length === 0) { |
| | | ElMessage.warning('请至少添加一个适用门店') |
| | | return |
| | | } |
| | | const names = fuelStores.value.map(store => store.name?.trim()).filter(Boolean) |
| | | if (names.length !== fuelStores.value.length) { |
| | | ElMessage.warning('适用门店名称不能为空') |
| | | return |
| | | } |
| | | if (new Set(names).size !== names.length) { |
| | | ElMessage.warning('适用门店名称不能重复') |
| | | return |
| | | } |
| | | const addresses = fuelStores.value.map(store => store.address?.trim()).filter(Boolean) |
| | | if (addresses.length !== fuelStores.value.length) { |
| | | ElMessage.warning('适用门店地址不能为空') |
| | | return |
| | | } |
| | | const unlocated = fuelStores.value.filter(store => !store.latitude || !store.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) { |
| | |
| | | latitude: Number(form.latitude || 0) || undefined, |
| | | canLoginMerchantApp: Number(form.canLoginMerchantApp ?? 1), |
| | | groupBuyingEnabled: form.groupBuyingEnabled, |
| | | applicableStores: isFuelMerchant.value ? fuelStores.value : undefined, |
| | | usageInstructions: isParkingMerchant.value |
| | | ? JSON.stringify(parkingAddresses.value) |
| | | : isChildcareMerchant.value |
| | |
| | | } |
| | | } |
| | | |
| | | if (isFuelMerchant.value && fuelStores.value.length > 0) { |
| | | const names = fuelStores.value.map(store => store.name?.trim()).filter(Boolean) |
| | | if (names.length !== fuelStores.value.length) { |
| | | ElMessage.warning('适用门店名称不能为空') |
| | | return |
| | | } |
| | | if (new Set(names).size !== names.length) { |
| | | ElMessage.warning('适用门店名称不能重复') |
| | | return |
| | | } |
| | | const addresses = fuelStores.value.map(store => store.address?.trim()).filter(Boolean) |
| | | if (addresses.length !== fuelStores.value.length) { |
| | | ElMessage.warning('适用门店地址不能为空') |
| | | return |
| | | } |
| | | const unlocated = fuelStores.value.filter(store => !store.latitude || !store.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) { |
| | |
| | | loginAccount: !isEditMode.value || !merchantHasLoginAccount.value ? accountForm.loginAccount : undefined, |
| | | loginPassword: !isEditMode.value || updatingMerchantPassword || !merchantHasLoginAccount.value ? accountForm.loginPassword : undefined, |
| | | groupBuyingEnabled: form.groupBuyingEnabled, |
| | | applicableStores: isFuelMerchant.value ? fuelStores.value : undefined, |
| | | usageInstructions: isParkingMerchant.value |
| | | ? JSON.stringify(parkingAddresses.value) |
| | | : isChildcareMerchant.value |
| | |
| | | merchantHasLoginAccount.value = Boolean(String(merchant.loginAccount || '').trim()) |
| | | if (isParkingMerchant.value) { |
| | | parkingAddresses.value = parseJsonValue<ParkingAddress[]>(merchant.usageInstructions, []) |
| | | } else if (isFuelMerchant.value) { |
| | | fuelStores.value = Array.isArray(merchant.applicableStores) |
| | | ? merchant.applicableStores.map((store: any) => ({ |
| | | id: store.id, |
| | | name: String(store.storeName ?? store.name ?? ''), |
| | | address: String(store.address ?? ''), |
| | | longitude: store.longitude == null ? undefined : Number(store.longitude), |
| | | latitude: store.latitude == null ? undefined : Number(store.latitude), |
| | | })) |
| | | : [] |
| | | } else if (isChildcareMerchant.value) { |
| | | schoolAddresses.value = parseJsonValue<SchoolAddress[]>(merchant.usageInstructions, []) |
| | | } else if (isApprovalMerchant.value) { |
| | |
| | | </div> |
| | | </section> |
| | | |
| | | <section v-if="isFuelMerchant" class="merchant-parking-address-panel"> |
| | | <h3 class="merchant-parking-address-title">适用门店</h3> |
| | | <el-table :data="fuelStores" border> |
| | | <el-table-column label="名称" min-width="180"> |
| | | <template #default="{ row }"><el-input v-model="row.name" placeholder="请输入" /></template> |
| | | </el-table-column> |
| | | <el-table-column label="地址" min-width="260"> |
| | | <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="markFuelStoreLocation(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="removeFuelStore($index)">删除</el-button></template> |
| | | </el-table-column> |
| | | </el-table> |
| | | <div class="merchant-parking-address-actions"><el-button plain @click="addFuelStore">新增</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> |
| | |
| | | subsidy: boolean |
| | | } |
| | | |
| | | interface HealthCheckItem { |
| | | name: string |
| | | meaning: string |
| | | gender: string[] |
| | | } |
| | | |
| | | type LegacyBenefitConfig = |
| | | | { rows?: Array<Partial<BenefitRow> & { levelName?: string; governmentSubsidy?: boolean }> } |
| | | | Array<Partial<BenefitRow>> |
| | |
| | | availableTime: '', |
| | | otherRules: '', |
| | | }) |
| | | const healthCheckItems = ref<HealthCheckItem[]>([]) |
| | | |
| | | const benefitRows = reactive<BenefitRow[]>([ |
| | | { level: '卓越人才卡', enabled: true, free: true, limitCount: '', discount: '', exceedDiscount: '', resetCycle: '每天', subsidy: true }, |
| | |
| | | const isHotelProduct = computed(() => { |
| | | const merchantType = product.value?.merchantType || '' |
| | | return merchantType === '酒店服务' || merchantType === '酒店' |
| | | }) |
| | | const isHealthCheckProduct = computed(() => { |
| | | const productData = product.value as (Product & Record<string, unknown>) | undefined |
| | | const values = [ |
| | | productData?.merchantType, |
| | | productData?.merchantTypeCode, |
| | | productData?.merchant_type, |
| | | productData?.merchant_type_code, |
| | | productData?.productType, |
| | | ].map(value => String(value || '').trim().toLowerCase().replace(/[\s_-]+/g, '')).filter(Boolean) |
| | | return values.some(value => [ |
| | | '医院服务', '医院', '医疗服务', '医疗', '免费体检', '体检服务', '医疗体检', |
| | | 'hospital', 'medical', 'healthcheck', 'medicalservice', |
| | | ].includes(value)) |
| | | }) |
| | | const hideTalentBenefitPanel = computed(() => { |
| | | const productData = product.value as (Product & Record<string, unknown>) | undefined |
| | |
| | | return merchantTypes.some(type => hiddenTypes.includes(type)) || ['hotel', 'hospital', 'medical', 'healthcheck'].includes(productType) |
| | | }) |
| | | const productAmountText = computed(() => { |
| | | if (isHealthCheckProduct.value) return '' |
| | | const salePrice = product.value?.salePrice |
| | | const originalPrice = product.value?.originalPrice |
| | | if (isHotelProduct.value && salePrice !== undefined && originalPrice !== undefined) { |
| | | return `${amountText(salePrice)} - ${amountText(originalPrice)}` |
| | | } |
| | | return amountText(salePrice ?? originalPrice) || '158' |
| | | }) |
| | | const healthCheckPeriodText = computed(() => { |
| | | const directValue = [product.value?.validStartTime, product.value?.validEndTime].filter(Boolean).join(' 至 ') |
| | | if (directValue) return directValue |
| | | const savedNotice = parseJsonValue<Record<string, unknown>>(product.value?.purchaseNotice, {}) |
| | | return String(savedNotice.validPeriod || savedNotice.valid_period || savedNotice.validity || '') |
| | | }) |
| | | const formatDateTime = (value?: string) => value ? value.replace('T', ' ') : '' |
| | | const createdAtText = computed(() => formatDateTime(product.value?.createdAt) || '2026-06-18 11:34:25') |
| | |
| | | return coverUrl ? [coverUrl] : [fallbackImage] |
| | | } |
| | | |
| | | function parseHealthCheckItems(value?: Product['detailText']) { |
| | | healthCheckItems.value = [] |
| | | const parsed = parseJsonValue<unknown>(value, []) |
| | | if (!Array.isArray(parsed)) return |
| | | healthCheckItems.value = parsed.map((item: any) => ({ |
| | | name: String(item?.name || ''), |
| | | meaning: String(item?.meaning || ''), |
| | | gender: Array.isArray(item?.gender) ? item.gender.map((entry: unknown) => String(entry)) : [], |
| | | })) |
| | | } |
| | | |
| | | function parseBenefits(value?: Product['rightsConfig'], legacyValue?: string) { |
| | | benefitRows.forEach((row) => { |
| | | row.enabled = true |
| | |
| | | return |
| | | } finally { |
| | | galleryImages.value = parseProductImages(product.value?.productImages, product.value?.coverUrl) |
| | | parseHealthCheckItems(product.value?.detailText) |
| | | parseNotice(product.value?.purchaseNotice, product.value) |
| | | parseBenefits(product.value?.rightsConfig, product.value?.benefitDesc) |
| | | loading.value = false |
| | |
| | | <label>商品名称</label> |
| | | <el-input :model-value="productNameText" readonly /> |
| | | </div> |
| | | <div class="detail-field detail-money-field"> |
| | | <div v-if="!isHealthCheckProduct" class="detail-field detail-money-field"> |
| | | <label>商品金额</label> |
| | | <el-input :model-value="productAmountText" readonly /> |
| | | <span>元</span> |
| | | </div> |
| | | <div v-else class="detail-field detail-period-field"> |
| | | <label>可预约时间</label> |
| | | <el-input :model-value="healthCheckPeriodText" placeholder="未配置" readonly /> |
| | | </div> |
| | | <div class="detail-time-pair"> |
| | | <label>创建时间</label> |
| | |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="audit-gallery-row"> |
| | | <div v-if="!isHealthCheckProduct" class="audit-gallery-row"> |
| | | <label>商品图片</label> |
| | | <div class="audit-thumb-list"> |
| | | <div v-for="image in imageList" :key="image" class="audit-thumb-box"> |
| | |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="audit-textarea-row"> |
| | | <div v-if="!isHealthCheckProduct" class="audit-textarea-row"> |
| | | <label>商品详情</label> |
| | | <div class="audit-rich-text-preview" v-html="product?.detailText || ''"></div> |
| | | </div> |
| | | |
| | | <div class="purchase-notice-block"> |
| | | <div v-else class="audit-textarea-row health-check-audit-row"> |
| | | <label>商品详情</label> |
| | | <el-table :data="healthCheckItems" border class="health-check-audit-table"> |
| | | <el-table-column prop="name" label="检查项目" min-width="220" /> |
| | | <el-table-column prop="meaning" label="项目意义" min-width="320" /> |
| | | <el-table-column label="男/女" width="180"> |
| | | <template #default="{ row }">{{ row.gender.join('、') }}</template> |
| | | </el-table-column> |
| | | <template #empty>暂无体检项目</template> |
| | | </el-table> |
| | | </div> |
| | | |
| | | <div v-if="!isHealthCheckProduct" class="purchase-notice-block"> |
| | | <p>购买须知设置(非必填)</p> |
| | | <div class="purchase-notice-grid"> |
| | | <div> |
| | |
| | | merchant.logo_url, merchant.cover_url, merchant.address, merchant.business_status, merchant.longitude, |
| | | merchant.latitude, merchant.can_login_merchant_app, merchant.group_buying_enabled, merchant.usage_instructions, |
| | | merchant.rights_text, merchant.merchant_rights_config, merchant.service_label, merchant.is_recommend, |
| | | merchant.sort_no, merchant.status, merchant.created_at, merchant.updated_at, |
| | | merchant.sort_no, merchant.status, merchant.created_at, merchant.updated_at, merchant.merchant_type_code, |
| | | s.external_link_type as car_wash_external_link_type, |
| | | s.external_mini_program_app_id as car_wash_external_mini_program_app_id, |
| | | s.external_mini_program_path as car_wash_external_mini_program_path, |
| | |
| | | where merchant_id = ? |
| | | order by sort_no, id |
| | | """, id))); |
| | | if ("fuel".equals(String.valueOf(result.get("merchantTypeCode")))) { |
| | | result.put("applicableStores", DatabaseRows.normalize(jdbcTemplate.queryForList(""" |
| | | select id, store_name, address, longitude, latitude |
| | | from transport_fuel_store |
| | | where merchant_id = ? and deleted = 0 and enabled = 1 |
| | | order by sort_no asc, id asc |
| | | """, id))); |
| | | } |
| | | return result; |
| | | } |
| | | |
| | |
| | | Long id = jdbcTemplate.queryForObject("select last_insert_id()", Long.class); |
| | | ensureParkingBenefit(id, merchantType.code()); |
| | | ensureFuelBenefit(id, merchantType.code(), request.serviceLabel()); |
| | | syncFuelStores(id, merchantType.code(), request.applicableStores()); |
| | | syncPremiumTopicMerchant(id, merchantType, request.sortNo()); |
| | | Long storeId = createDefaultStore(id, request.merchantName(), request.address()); |
| | | syncMerchantBusinessHours(id, storeId, request.businessHours()); |
| | |
| | | ensureUpdated(updated); |
| | | ensureParkingBenefit(id, merchantType.code()); |
| | | ensureFuelBenefit(id, merchantType.code(), request.serviceLabel()); |
| | | syncFuelStores(id, merchantType.code(), request.applicableStores()); |
| | | syncPremiumTopicMerchant(id, merchantType, request.sortNo()); |
| | | Long storeId = findOrCreateDefaultStore(id, request.merchantName(), request.address()); |
| | | syncMerchantBusinessHours(id, storeId, request.businessHours()); |
| | |
| | | """, merchantId, discountText, merchantId); |
| | | } |
| | | |
| | | private void syncFuelStores(Long merchantId, String merchantType, List<Map<String, Object>> stores) { |
| | | if (!"fuel".equals(merchantType) || stores == null) return; |
| | | List<Map<String, Object>> normalized = new ArrayList<>(); |
| | | Set<String> names = new HashSet<>(); |
| | | for (Map<String, Object> store : stores) { |
| | | if (store == null) continue; |
| | | Object nameValue = store.containsKey("storeName") ? store.get("storeName") : store.get("name"); |
| | | String name = nameValue == null ? "" : String.valueOf(nameValue).trim(); |
| | | Object addressValue = store.get("address"); |
| | | String address = addressValue == null ? "" : String.valueOf(addressValue).trim(); |
| | | if (name.isBlank() || address.isBlank()) throw new BusinessException(ErrorCode.BAD_REQUEST, "请完善适用门店名称和地址"); |
| | | if (!names.add(name)) throw new BusinessException(ErrorCode.BAD_REQUEST, "适用门店名称不能重复"); |
| | | Double longitude = numberValue(store.get("longitude")); |
| | | Double latitude = numberValue(store.get("latitude")); |
| | | if (longitude == null || longitude < -180 || longitude > 180 || latitude == null || latitude < -90 || latitude > 90 |
| | | || (longitude == 0 && latitude == 0)) { |
| | | throw new BusinessException(ErrorCode.BAD_REQUEST, "请完成所有适用门店的标点"); |
| | | } |
| | | Map<String, Object> item = new LinkedHashMap<>(); |
| | | item.put("name", name); |
| | | item.put("address", address); |
| | | item.put("longitude", longitude); |
| | | item.put("latitude", latitude); |
| | | normalized.add(item); |
| | | } |
| | | jdbcTemplate.update("delete from transport_fuel_store where merchant_id = ?", merchantId); |
| | | for (int index = 0; index < normalized.size(); index++) { |
| | | Map<String, Object> store = normalized.get(index); |
| | | jdbcTemplate.update(""" |
| | | insert into transport_fuel_store(merchant_id, store_name, address, longitude, latitude, sort_no, enabled, deleted) |
| | | values (?, ?, ?, ?, ?, ?, 1, 0) |
| | | """, merchantId, store.get("name"), store.get("address"), store.get("longitude"), store.get("latitude"), (index + 1) * 10); |
| | | } |
| | | } |
| | | |
| | | private Double numberValue(Object value) { |
| | | if (value instanceof Number number) return number.doubleValue(); |
| | | try { return value == null ? null : Double.valueOf(String.valueOf(value)); } |
| | | catch (NumberFormatException ignored) { return null; } |
| | | } |
| | | |
| | | private void syncPremiumTopicMerchant(Long merchantId, MerchantType merchantType, Integer sortNo) { |
| | | if (merchantType.premiumCategoryKey() == null || merchantType.premiumBusinessType() == null) { |
| | | jdbcTemplate.update("update premium_topic_merchant set deleted = 1 where merchant_id = ? and deleted = 0", merchantId); |
| | |
| | | Double longitude, |
| | | Double latitude, |
| | | Byte groupBuyingEnabled, |
| | | List<Map<String, Object>> applicableStores, |
| | | String usageInstructions, |
| | | String rightsText, |
| | | List<Map<String, Object>> merchantRightsConfig, |
| | |
| | | Double longitude, |
| | | Double latitude, |
| | | Byte groupBuyingEnabled, |
| | | List<Map<String, Object>> applicableStores, |
| | | String usageInstructions, |
| | | String rightsText, |
| | | List<Map<String, Object>> merchantRightsConfig, |