| | |
| | | return http.get<ServiceApplication, ServiceApplication>(`/api/admin/service-appointment-applications/${id}`) |
| | | } |
| | | |
| | | export function updateServiceApplicationStatus(id: number | string, status: string, handlerName = '管理员') { |
| | | export function updateServiceApplicationStatus( |
| | | id: number | string, |
| | | status: string, |
| | | handlerName = '管理员', |
| | | remark = '', |
| | | rejectReason = '', |
| | | ) { |
| | | return http.patch<ServiceApplication, ServiceApplication>(`/api/admin/service-appointment-applications/${id}/status`, { |
| | | status, |
| | | handlerName, |
| | | remark, |
| | | rejectReason, |
| | | }) |
| | | } |
| | | |
| | | export function exportServiceApplications(params: ServiceApplicationQuery = {}) { |
| | | return http.get<Blob, Blob>('/api/admin/service-appointment-applications/export', { |
| | | params, |
| | | responseType: 'blob', |
| | | }) |
| | | } |
| | | |
| | |
| | | return http.get<PageResult<AnyRecord>, PageResult<AnyRecord>>('/api/admin/display-services', { params }) |
| | | } |
| | | |
| | | export function getDisplayServiceDetail(serviceId: number | string) { |
| | | return http.get<AnyRecord, AnyRecord>(`/api/admin/display-services/${serviceId}`) |
| | | } |
| | | |
| | | export function getDisplayServiceCategories() { |
| | | return http.get<AnyRecord[], AnyRecord[]>('/api/admin/display-services/categories') |
| | | } |
| | |
| | | gap: 16px 52px; |
| | | min-height: 82px; |
| | | padding: 18px; |
| | | background: #f2f3f5; |
| | | background: transparent; |
| | | font-size: 13px; |
| | | } |
| | | |
| | |
| | | gap: 34px; |
| | | min-height: 136px; |
| | | padding: 8px 24px 18px; |
| | | background: #f2f3f5; |
| | | background: transparent; |
| | | } |
| | | |
| | | .talent-card-stack { |
| | |
| | | .service-staff-panel { |
| | | min-height: 54px; |
| | | padding: 14px 18px; |
| | | background: #f2f3f5; |
| | | background: transparent; |
| | | } |
| | | |
| | | .service-staff-panel .el-select { |
| | |
| | | .identity-edit-card { |
| | | min-height: 92px; |
| | | padding: 16px 14px; |
| | | background: #f2f3f5; |
| | | background: transparent; |
| | | } |
| | | |
| | | .identity-edit-card .el-form { |
| | | .identity-edit-form { |
| | | display: grid; |
| | | grid-template-columns: repeat(3, minmax(220px, 1fr)); |
| | | gap: 12px 50px; |
| | |
| | | .identity-edit-card .el-input, |
| | | .identity-edit-card .el-select { |
| | | width: 180px; |
| | | } |
| | | |
| | | /* 新增页与详情页采用同样的整行信息面板,避免被表单标签列压缩。 */ |
| | | .talent-create-page .create-card-form-item, |
| | | .talent-create-page .service-staff-form-item { |
| | | margin: 0; |
| | | } |
| | | |
| | | .talent-create-page .create-card-form-item > .el-form-item__content, |
| | | .talent-create-page .service-staff-form-item > .el-form-item__content { |
| | | display: block; |
| | | margin-left: 0 !important; |
| | | } |
| | | |
| | | .talent-create-page .create-card-form-item .card-manage-panel, |
| | | .talent-create-page .service-staff-form-item .service-staff-panel { |
| | | width: 100%; |
| | | box-sizing: border-box; |
| | | } |
| | | |
| | | .talent-create-page .service-staff-form-item .service-staff-panel { |
| | | min-height: 54px; |
| | | } |
| | | |
| | | .create-card-preview { |
| | |
| | | grid-template-columns: repeat(2, minmax(0, 1fr)); |
| | | } |
| | | |
| | | .identity-edit-card .el-form { |
| | | .identity-edit-form { |
| | | grid-template-columns: repeat(2, minmax(220px, 1fr)); |
| | | } |
| | | |
| | |
| | | width: min(100%, 220px); |
| | | } |
| | | |
| | | .identity-edit-card .el-form { |
| | | .identity-edit-form { |
| | | grid-template-columns: 1fr; |
| | | } |
| | | } |
| | |
| | | XLSX.utils.book_append_sheet(workbook, sheet, '数据列表') |
| | | XLSX.writeFile(workbook, `${filename}.xlsx`) |
| | | } |
| | | |
| | | export function saveBlob(blob: Blob, filename: string) { |
| | | const url = URL.createObjectURL(blob) |
| | | const link = document.createElement('a') |
| | | link.href = url |
| | | link.download = filename |
| | | document.body.appendChild(link) |
| | | link.click() |
| | | link.remove() |
| | | URL.revokeObjectURL(url) |
| | | } |
| | |
| | | const isSpouseEmploymentMerchant = computed(() => normalizedMerchantType.value === '配偶推荐就业') |
| | | const isApprovalMerchant = computed(() => normalizedMerchantType.value === '行政审批') |
| | | const isServiceMerchant = computed(() => isFoodMerchant.value || normalizedMerchantType.value === '景区服务') |
| | | const requiresFoodMerchantSettings = computed(() => !isEditMode.value && isFoodMerchant.value) |
| | | const supportsStoreConsumption = computed(() => [ |
| | | '酒店服务', '景区服务', '餐饮服务', '口腔治疗保健', '免费体检', |
| | | ].includes(normalizedMerchantType.value) || (isCarWashMerchant.value && form.miniProgramJumpType === 'normal')) |
| | |
| | | contactMobile: [{ required: true, message: '请输入联系电话', trigger: 'blur' }], |
| | | address: isApartmentMerchant.value ? [] : [{ required: true, message: '请输入商家地址', trigger: 'blur' }], |
| | | coverUrl: [{ required: true, message: '请上传商家封面图', trigger: 'change' }], |
| | | businessHours: [{ |
| | | validator: (_rule, _value, callback) => isApprovalMerchant.value || hasValidBusinessHours() |
| | | ? callback() |
| | | : callback(new Error('请选择有效的营业时间')), |
| | | trigger: 'change', |
| | | }], |
| | | sortNo: [{ |
| | | validator: (_rule, value, callback) => { |
| | | const text = String(value ?? '').trim() |
| | |
| | | businessHours.splice(index, 1) |
| | | } |
| | | |
| | | function hasValidBusinessHours() { |
| | | return businessHours.length > 0 && businessHours.every((item) => { |
| | | const [startTime, endTime] = item.value |
| | | return Boolean(startTime && endTime && startTime !== endTime) |
| | | }) |
| | | } |
| | | |
| | | function businessHoursPayload() { |
| | | return businessHours.map((item) => ({ startTime: item.value[0], endTime: item.value[1] })) |
| | | 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 }>) { |
| | |
| | | <el-input v-model="form.createdAt" readonly /> |
| | | </div> |
| | | |
| | | <el-form-item v-if="!isApprovalMerchant" class="merchant-hours-block" label="营业时间" prop="businessHours" required> |
| | | <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 |
| | |
| | | { pattern: /^[0-9A-Z]{18}$/, message: '身份证号须为18位数字或大写英文字母', trigger: 'blur' }, |
| | | ], |
| | | contactAddress: [{ required: true, message: '请输入联系地址', trigger: 'blur' }], |
| | | workAddress: [{ required: true, message: '请输入工作地址', trigger: 'blur' }], |
| | | workUnit: [{ required: true, message: '请输入工作单位', trigger: 'blur' }], |
| | | gender: [{ required: true, message: '请选择性别', trigger: 'change' }], |
| | | talentCardNo: [{ |
| | | validator: (_rule, value, callback) => talentLevelConfirmed.value && String(value || '').trim() |
| | |
| | | |
| | | saving.value = true |
| | | try { |
| | | const talent = await createTalent(form) |
| | | // 后端仍保留 workAddress 历史字段;新建时同步写入,保证新旧端均可回显。 |
| | | const talent = await createTalent({ ...form, workAddress: form.workUnit }) |
| | | await updateTalentServiceStaff(talent.id, serviceStaff.value) |
| | | ElMessage.success('持卡人已保存') |
| | | router.push('/talents') |
| | |
| | | <template> |
| | | <div class="talent-detail-page talent-create-page"> |
| | | <el-form ref="formRef" :model="form" :rules="rules" label-width="104px"> |
| | | <section class="detail-block create-mobile-summary"> |
| | | <section class="detail-block user-summary create-mobile-summary"> |
| | | <el-form-item label="手机号码:" prop="mobile" required> |
| | | <el-input |
| | | v-model="form.mobile" |
| | |
| | | <el-form-item label="联系地址:" prop="contactAddress" required> |
| | | <el-input v-model="form.contactAddress" placeholder="请输入" /> |
| | | </el-form-item> |
| | | <el-form-item label="工作地址:" prop="workAddress" required> |
| | | <el-input v-model="form.workAddress" placeholder="请输入" /> |
| | | <el-form-item label="工作单位:" prop="workUnit" required> |
| | | <el-input v-model="form.workUnit" placeholder="请输入工作单位" /> |
| | | </el-form-item> |
| | | <el-form-item label="性别:" prop="gender" required> |
| | | <el-select v-model="form.gender" placeholder="选择"> |
| | |
| | | |
| | | <section class="detail-block"> |
| | | <h3>人才卡管理</h3> |
| | | <el-form-item label="人才等级/卡号" prop="talentCardNo" required> |
| | | <el-form-item prop="talentCardNo" required class="create-card-form-item" label-width="0"> |
| | | <div class="card-manage-panel"> |
| | | <div class="talent-card-stack"> |
| | | <button type="button" class="talent-card-preview create-card-preview" @click="openCardDialog"> |
| | |
| | | |
| | | <section class="detail-block"> |
| | | <h3>专属服务人员</h3> |
| | | <el-form-item label="专属服务人员" prop="serviceStaff" class="service-staff-form-item" required> |
| | | <el-form-item prop="serviceStaff" class="service-staff-form-item" required label-width="0"> |
| | | <div class="service-staff-panel"> |
| | | <el-select v-model="serviceStaff" clearable placeholder="请选择专属服务人员" @change="formRef?.validateField('serviceStaff')"> |
| | | <el-option |
| | |
| | | name: talent.value?.name || '', |
| | | idCardNo: talent.value?.idCardNo || '', |
| | | gender: talent.value?.gender, |
| | | workUnit: talent.value?.workUnit || '', |
| | | workAddress: talent.value?.workAddress || '', |
| | | workUnit: talent.value?.workUnit || talent.value?.workAddress || '', |
| | | workAddress: talent.value?.workAddress || talent.value?.workUnit || '', |
| | | contactAddress: talent.value?.contactAddress || '', |
| | | } |
| | | certificationError.value = '' |
| | |
| | | if (!talent.value?.id) return |
| | | certificationSaving.value = true |
| | | try { |
| | | talent.value = await updateTalentCertification(talent.value.id, form) |
| | | // 统一以 workUnit 为准,同时同步历史 workAddress 字段。 |
| | | talent.value = await updateTalentCertification(talent.value.id, { ...form, workAddress: form.workUnit }) |
| | | certificationDialogVisible.value = false |
| | | ElMessage.success('认证信息已更新') |
| | | } finally { |
| | |
| | | <strong>{{ mask(talent?.idCardNo, 6, 4) }}</strong> |
| | | </div> |
| | | <div> |
| | | <span>工作地址:</span> |
| | | <strong>{{ talent?.workAddress || talent?.workUnit || '-' }}</strong> |
| | | <span>工作单位:</span> |
| | | <strong>{{ talent?.workUnit || talent?.workAddress || '-' }}</strong> |
| | | </div> |
| | | <div> |
| | | <span>联系地址:</span> |
| | |
| | | <label> |
| | | <span>工作单位</span> |
| | | <el-input v-model="certificationForm.workUnit" maxlength="128" placeholder="请输入工作单位" /> |
| | | </label> |
| | | <label> |
| | | <span>工作地址</span> |
| | | <el-input v-model="certificationForm.workAddress" maxlength="255" placeholder="请输入工作地址" /> |
| | | </label> |
| | | <label> |
| | | <span>联系地址</span> |
| | |
| | | if (!Set.of("景区服务", "餐饮服务").contains(request.merchantType())) { |
| | | return; |
| | | } |
| | | if (request.businessHours() == null || request.businessHours().isEmpty() |
| | | || request.businessHours().stream().anyMatch(item -> item.startTime() == null || item.endTime() == null |
| | | || item.startTime().isBlank() || item.endTime().isBlank() || item.startTime().equals(item.endTime()))) { |
| | | throw new BusinessException(ErrorCode.BAD_REQUEST, "请选择有效的营业时间"); |
| | | } |
| | | if (request.loginAccount() == null || !request.loginAccount().trim().matches("^[A-Za-z0-9]{1,32}$")) { |
| | | throw new BusinessException(ErrorCode.BAD_REQUEST, "商户登录账号只能包含英文字母和数字"); |
| | | } |
| | |
| | | |
| | | private void syncMerchantBusinessHours(Long merchantId, Long storeId, List<MerchantBusinessHourRequest> businessHours) { |
| | | if (businessHours == null) return; |
| | | jdbcTemplate.update("delete from merchant_business_hours where merchant_id = ? and store_id = ?", merchantId, storeId); |
| | | if (businessHours.isEmpty()) { |
| | | throw new BusinessException(ErrorCode.BAD_REQUEST, "请至少配置一个营业时间段"); |
| | | return; |
| | | } |
| | | if (businessHours.stream().anyMatch(item -> item.startTime().equals(item.endTime()))) { |
| | | throw new BusinessException(ErrorCode.BAD_REQUEST, "营业开始时间和结束时间不能相同"); |
| | | } |
| | | jdbcTemplate.update("delete from merchant_business_hours where merchant_id = ? and store_id = ?", merchantId, storeId); |
| | | for (int index = 0; index < businessHours.size(); index++) { |
| | | MerchantBusinessHourRequest item = businessHours.get(index); |
| | | jdbcTemplate.update(""" |
| | |
| | | where s.status = 1 and s.deleted = 0 and h.merchant_id in (""" + placeholders + ")", merchantIds.toArray())); |
| | | LocalTime now = LocalTime.now(ZoneId.of("Asia/Shanghai")); |
| | | Set<Long> openMerchantIds = new HashSet<>(); |
| | | Set<Long> merchantsWithBusinessHours = new HashSet<>(); |
| | | for (Map<String, Object> period : periods) { |
| | | Long merchantId = Long.valueOf(String.valueOf(period.get("merchantId"))); |
| | | merchantsWithBusinessHours.add(merchantId); |
| | | if (isWithinBusinessPeriod(now, String.valueOf(period.get("startTime")), String.valueOf(period.get("endTime")))) { |
| | | openMerchantIds.add(Long.valueOf(String.valueOf(period.get("merchantId")))); |
| | | openMerchantIds.add(merchantId); |
| | | } |
| | | } |
| | | for (Map<String, Object> merchant : merchants) { |
| | | Long merchantId = Long.valueOf(String.valueOf(merchant.get("id"))); |
| | | boolean enabled = "1".equals(String.valueOf(merchant.get("status"))); |
| | | merchant.put("businessStatus", enabled && openMerchantIds.contains(merchantId) ? "营业中" : "休息中"); |
| | | boolean open = !merchantsWithBusinessHours.contains(merchantId) || openMerchantIds.contains(merchantId); |
| | | merchant.put("businessStatus", enabled && open ? "营业中" : "休息中"); |
| | | } |
| | | } |
| | | |
| | |
| | | if (!Long.valueOf(1).equals(number(merchant.get("status"))) || "closed".equals(businessStatus(merchant.get("businessStatus")))) { |
| | | return false; |
| | | } |
| | | Long configuredPeriods = jdbc.queryForObject(""" |
| | | select count(*) |
| | | from merchant_business_hours |
| | | where merchant_id = ? and store_id = ? |
| | | """, Long.class, merchant.get("id"), storeId); |
| | | if (configuredPeriods == null || configuredPeriods == 0) { |
| | | return true; |
| | | } |
| | | Long activePeriods = jdbc.queryForObject(""" |
| | | select count(*) |
| | | from merchant_business_hours |