fix: restore features overwritten by merge
| | |
| | | </template> |
| | | |
| | | <script setup lang="uts"> |
| | | import { getCarWashMerchantDetail, getTalentCardProfile } from '../../services/portal.uts' |
| | | import { getAppHome, getCarWashMerchantDetail, getTalentCardProfile } from '../../services/portal.uts' |
| | | import { getApiBaseUrl } from '../../config/api.uts' |
| | | import { safeBack } from '../../utils/navigation.uts' |
| | | |
| | | type PageState = 'loading' | 'ready' | 'error' | 'invalid' |
| | |
| | | merchantId.value = options != null && options['merchantId'] != null ? options['merchantId'] as string : '' |
| | | talentId.value = options != null && options['talentId'] != null ? options['talentId'] as string : '' |
| | | talentCardId.value = options != null && options['talentCardId'] != null ? options['talentCardId'] as string : '' |
| | | if (source.value == 'homeMiniProgram') uni.setNavigationBarTitle({ title: '首页服务' }) |
| | | if (!isValidSource()) { |
| | | showInvalid('服务地址无效', '请从已配置的服务入口重新进入') |
| | | return |
| | |
| | | }) |
| | | |
| | | const isSafeWebUrl = (value: string): boolean => /^https:\/\/[A-Za-z0-9.-]+(?::\d+)?(?:\/|$)/.test(value) |
| | | const isValidSource = (): boolean => (source.value == 'carWash' && /^\d+$/.test(merchantId.value)) || (source.value == 'advisor' && /^\d+$/.test(talentId.value)) |
| | | const isSafeHomeLinkUrl = (value: string): boolean => { |
| | | if (isSafeWebUrl(value)) return true |
| | | const apiHost = getApiBaseUrl().replace(/^https?:\/\//, '').split('/')[0].split(':')[0] |
| | | if (apiHost == '') return false |
| | | const escapedHost = apiHost.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') |
| | | return new RegExp('^http://' + escapedHost + '(?::\\d+)?(?:/|$)').test(value) |
| | | } |
| | | const isValidSource = (): boolean => (source.value == 'carWash' && /^\d+$/.test(merchantId.value)) || (source.value == 'advisor' && /^\d+$/.test(talentId.value)) || source.value == 'homeMiniProgram' |
| | | const loadExternalService = () => { |
| | | if (!isValidSource()) { |
| | | showInvalid('服务地址无效', '请从已配置的服务入口重新进入') |
| | |
| | | stateTitle.value = '正在打开服务' |
| | | stateDescription.value = '请稍候' |
| | | if (source.value == 'advisor') { loadAdvisorService(); return } |
| | | if (source.value == 'homeMiniProgram') { loadHomeMiniProgram(); return } |
| | | getCarWashMerchantDetail(merchantId.value, talentCardId.value, (data: any) => { |
| | | if (data == null || data.available !== true) { |
| | | showInvalid('服务暂不可用', data != null && data.unavailableReason != null ? data.unavailableReason as string : '请稍后重试') |
| | |
| | | const url = data.externalLinkType == 'web' && data.externalWebUrl != null ? data.externalWebUrl as string : '' |
| | | if (!isSafeWebUrl(url)) { |
| | | showInvalid('服务暂不可用', '服务地址暂未配置或未通过安全校验') |
| | | return |
| | | } |
| | | targetUrl.value = url |
| | | pageState.value = 'ready' |
| | | }, () => { |
| | | pageState.value = 'error' |
| | | stateTitle.value = '服务加载失败' |
| | | stateDescription.value = '请检查网络后重新加载' |
| | | }) |
| | | } |
| | | const loadHomeMiniProgram = () => { |
| | | getAppHome((data: any) => { |
| | | const url = data != null && data.miniProgramUrl != null ? data.miniProgramUrl as string : '' |
| | | if (!isSafeHomeLinkUrl(url)) { |
| | | showInvalid('首页跳转链接不可用', '请在后台配置有效链接;生产环境请使用 HTTPS') |
| | | return |
| | | } |
| | | targetUrl.value = url |
| | |
| | | stateTitle.value = '网页加载失败' |
| | | stateDescription.value = '请稍后重新加载或返回服务详情' |
| | | } |
| | | const goBack = () => safeBack(source.value == 'advisor' ? '/pages/card/card?preview=member&talentId=' + encodeURIComponent(talentId.value) : '/pages/car-wash-merchant/car-wash-merchant?merchantId=' + encodeURIComponent(merchantId.value)) |
| | | const goBack = () => safeBack(source.value == 'advisor' ? '/pages/card/card?preview=member&talentId=' + encodeURIComponent(talentId.value) : (source.value == 'homeMiniProgram' ? '/pages/index/index' : '/pages/car-wash-merchant/car-wash-merchant?merchantId=' + encodeURIComponent(merchantId.value))) |
| | | </script> |
| | | |
| | | <style> |
| | |
| | | const route = useRoute() |
| | | const router = useRouter() |
| | | const appStore = useAppStore() |
| | | const avatarText = computed(() => (appStore.user.name || 'A').trim().slice(0, 1).toUpperCase()) |
| | | |
| | | interface HeaderNotice { |
| | | id: number |
| | |
| | | { index: 'operation-management', label: '运营管理', icon: Operation, items: operationMenus }, |
| | | { index: 'system-management', label: '系统管理', icon: Setting, items: systemMenus }, |
| | | ] |
| | | |
| | | // Keep the navigation in sync with the permissions returned at login. Newer |
| | | // routes without a dedicated permission code intentionally remain visible. |
| | | const routePermissions: Record<string, string> = { |
| | | '/talents': 'admin.talents.view', '/family-card-applications': 'admin.family-cards.view', '/upgrade-applications': 'admin.talents.view', '/family-cards': 'admin.family-cards.view', '/child-cards': 'admin.family-cards.view', |
| | | '/merchants': 'admin.merchants.view', '/products': 'admin.products.view', '/orders': 'admin.orders.view', '/refund-records': 'admin.refunds.view', '/hotel-free-verifications': 'admin.orders.view', |
| | | '/medical-reservations': 'admin.medical-reservations.view', '/medical-applications': 'admin.medical-reservations.view', '/parking-plates': 'admin.parking-plates.view', '/fuel-services': 'admin.parking-plates.view', '/spouse-employment': 'admin.parking-plates.view', '/hotel-reservations': 'admin.medical-reservations.view', |
| | | '/merchant-settlement': 'admin.merchants.view', '/equity-images': 'admin.equity-images.view', '/talent-news-management': 'admin.news.view', '/talent-policy-management': 'admin.news.view', '/movie-tickets': 'admin.news.view', |
| | | '/system-settings': 'admin.settings.view', '/protocol-settings': 'admin.settings.view', '/roles': 'admin.roles.view', '/staff': 'admin.staff.view', |
| | | } |
| | | const canAccess = (path?: string) => { |
| | | if (!path || appStore.user.role === '平台管理') return true |
| | | const required = routePermissions[path] |
| | | return !required || Boolean(appStore.user.permissionCodes?.includes('*') || appStore.user.permissionCodes?.includes(required)) |
| | | } |
| | | const visibleMenuGroups = computed(() => menuGroups |
| | | .map((group) => ({ ...group, items: group.items.filter((item) => canAccess(item.path)) })) |
| | | .filter((group) => group.items.length > 0)) |
| | | |
| | | const defaultOpeneds = computed(() => { |
| | | const currentGroup = menuGroups.find((item) => findMenuLabel(item.items, activeMenu.value)) |
| | |
| | | <el-tooltip content="退出登录" placement="bottom"> |
| | | <el-button :icon="SwitchButton" circle @click="logout" /> |
| | | </el-tooltip> |
| | | <div class="admin-avatar" title="平台管理员" aria-label="平台管理员头像">A</div> |
| | | <div class="admin-avatar" :title="appStore.user.name" :aria-label="`${appStore.user.name}头像`">{{ avatarText }}</div> |
| | | </div> |
| | | </el-header> |
| | | |
| | |
| | | <span>首页</span> |
| | | </el-menu-item> |
| | | |
| | | <el-sub-menu v-for="group in menuGroups" :key="group.index" :index="group.index"> |
| | | <el-sub-menu v-for="group in visibleMenuGroups" :key="group.index" :index="group.index"> |
| | | <template #title> |
| | | <el-icon> |
| | | <component :is="group.icon" /> |
| | |
| | | |
| | | const familyCardView = () => import('../views/FamilyCardView.vue') |
| | | const businessManagementView = () => import('../views/BusinessManagementView.vue') |
| | | const cardPlaceholderView = () => import('../views/CardPlaceholderView.vue') |
| | | |
| | | const router = createRouter({ |
| | | history: createWebHistory(), |
| | |
| | | { |
| | | path: 'appointment-management', |
| | | name: 'appointment-management', |
| | | component: () => import('../views/AppointmentManagementView.vue'), |
| | | component: cardPlaceholderView, |
| | | meta: { title: '预约服务管理', businessKind: 'appointmentManagement' }, |
| | | }, |
| | | { |
| | |
| | | { |
| | | path: 'car-wash-benefits', |
| | | name: 'car-wash-benefits', |
| | | component: () => import('../views/CarWashBenefitView.vue'), |
| | | component: cardPlaceholderView, |
| | | meta: { title: '洗车权益管理', businessKind: 'carWashBenefit' }, |
| | | }, |
| | | { |
| | |
| | | { |
| | | path: 'benefit-coupons', |
| | | name: 'benefit-coupons', |
| | | component: () => import('../views/BenefitCouponManagementView.vue'), |
| | | component: cardPlaceholderView, |
| | | meta: { title: '优惠券管理', businessKind: 'benefitCoupons' }, |
| | | }, |
| | | { |
| | | path: 'display-services', |
| | | name: 'display-services', |
| | | component: () => import('../views/DisplayServiceManagementView.vue'), |
| | | component: cardPlaceholderView, |
| | | meta: { title: '展示类服务管理', businessKind: 'displayServices' }, |
| | | }, |
| | | { |
| | |
| | | export interface FamilyCardApplication { |
| | | id: number | string |
| | | applicationNo?: string |
| | | applyNo?: string |
| | | talentId?: number | string |
| | | talentName?: string |
| | | talentMobile?: string |
| | | talentCardNo?: string |
| | | cardLevel?: string |
| | | familyName?: string |
| | | dependentName?: string |
| | | familyMobile?: string |
| | | dependentMobile?: string |
| | | relationship?: string |
| | | relation?: string |
| | | idCardNo?: string |
| | | dependentIdCardNo?: string |
| | | applyTime?: string |
| | | createdAt?: string |
| | | status?: string | number |
| | | statusText?: string |
| | | auditTime?: string |
| | | auditorName?: string |
| | | auditBy?: string |
| | | auditRemark?: string |
| | | proofFiles?: string | string[] |
| | | familyTalentId?: number | string |
| | | familyCardId?: number | string |
| | | cardId?: number | string |
| | |
| | | import { computed, onMounted, reactive, ref, watch } from 'vue' |
| | | import { useRoute, useRouter } from 'vue-router' |
| | | import { Download, Edit, Plus, Refresh, Search, View } from '@element-plus/icons-vue' |
| | | import { ElMessage } from 'element-plus' |
| | | import { ElMessage, ElMessageBox } from 'element-plus' |
| | | import type { FormInstance, FormRules } from 'element-plus' |
| | | import { |
| | | createAdminRole, |
| | | createAdminStaff, |
| | | deleteAdminRole, |
| | | getAdminRoles, |
| | | getAdminStaff, |
| | | getUpgradeApplications, |
| | | updateAdminRole, |
| | | updateAdminRoleStatus, |
| | | updateAdminStaff, |
| | | } from '../api/admin' |
| | | import type { AdminRole, AdminStaff } from '../types/admin' |
| | |
| | | const rows = ref<Record<string, unknown>[]>([]) |
| | | const dialogVisible = ref(false) |
| | | const editVisible = ref(false) |
| | | const formRef = ref<FormInstance>() |
| | | const activeRow = ref<Record<string, unknown>>() |
| | | const editingId = ref<number | string>() |
| | | const filters = reactive({ keyword: '', cardLevel: '', status: '', timeRange: [] as string[] }) |
| | | const pagination = reactive({ page: 1, size: 10, total: 0 }) |
| | | const form = reactive({ name: '', code: '', password: '', remark: '', status: 1 }) |
| | | const accountPattern = /^[A-Za-z0-9]+$/ |
| | | const rules = computed<FormRules>(() => ({ |
| | | name: [{ required: true, message: isRole.value ? '请输入角色名称' : '请输入员工姓名', trigger: 'blur' }], |
| | | code: [ |
| | | { required: true, message: isRole.value ? '请输入角色编码' : '请输入登录账号', trigger: 'blur' }, |
| | | { |
| | | validator: (_rule, value, callback) => !isRole.value && value && !accountPattern.test(String(value).trim()) |
| | | ? callback(new Error('登录账号仅支持英文、数字或英文+数字')) |
| | | : callback(), |
| | | trigger: 'blur', |
| | | }, |
| | | ], |
| | | password: [{ |
| | | validator: (_rule, value, callback) => !isRole.value && !editingId.value && !value |
| | | ? callback(new Error('请设置登录密码')) |
| | | : callback(), |
| | | trigger: 'blur', |
| | | }], |
| | | })) |
| | | |
| | | const filteredRows = computed(() => isRole.value |
| | | ? rows.value.filter((row) => (!filters.keyword || String(row.roleName || '').includes(filters.keyword)) && (!filters.status || String(row.status) === filters.status)) |
| | | : rows.value) |
| | | const pagedRows = computed(() => isUpgrade.value |
| | | ? rows.value |
| | | : filteredRows.value.slice((pagination.page - 1) * pagination.size, pagination.page * pagination.size)) |
| | | const pageTotal = computed(() => isRole.value ? filteredRows.value.length : pagination.total) |
| | | |
| | | const columns = computed(() => isUpgrade.value |
| | | ? [['applicationNo', '申请编号'], ['talentName', '持卡人名称'], ['talentMobile', '持卡人手机号'], ['targetLevel', '卡片等级'], ['applyTime', '申请时间'], ['status', '状态']] |
| | | : isRole.value |
| | | ? [['roleCode', '角色编码'], ['roleName', '角色名称'], ['remark', '备注'], ['statusText', '状态'], ['createdAt', '创建时间']] |
| | | ? [['serialNo', '序号'], ['roleName', '角色名称'], ['remark', '备注'], ['statusText', '状态'], ['createdAt', '创建时间']] |
| | | : [['account', '登录账号'], ['displayName', '员工姓名'], ['roleCode', '所属角色'], ['statusText', '状态'], ['createdAt', '创建时间']]) |
| | | |
| | | function statusText(value: unknown) { return Number(value) === 1 ? '启用' : '停用' } |
| | | function formatDateTime(value: unknown) { |
| | | return typeof value === 'string' && value ? value.replace('T', ' ').slice(0, 19) : '' |
| | | } |
| | | function normalizeRole(row: AdminRole) { return { ...row, createdAt: formatDateTime(row.createdAt), statusText: statusText(row.status) } } |
| | | function normalizeRole(row: AdminRole, index: number) { return { ...row, serialNo: index + 1, createdAt: formatDateTime(row.createdAt), statusText: statusText(row.status) } } |
| | | function normalizeStaff(row: AdminStaff) { return { ...row, createdAt: formatDateTime(row.createdAt), statusText: statusText(row.status) } } |
| | | function normalizeUpgrade(row: Record<string, unknown>) { return { ...row, applyTime: formatDateTime(row.applyTime) } } |
| | | function isPlatformAdminRole(row: Record<string, unknown>) { return String(row.roleCode || '') === 'platform_admin' } |
| | | |
| | | async function loadData() { |
| | | loading.value = true |
| | |
| | | |
| | | function search() { pagination.page = 1; loadData() } |
| | | function reset() { filters.keyword = ''; filters.cardLevel = ''; filters.status = ''; filters.timeRange = []; search() } |
| | | function changePage() { if (isUpgrade.value) loadData() } |
| | | function view(row: Record<string, unknown>) { |
| | | if (isUpgrade.value) { |
| | | router.push(`/upgrade-applications/${row.id}`) |
| | |
| | | } |
| | | |
| | | async function save() { |
| | | if (!form.name.trim() || !form.code.trim()) { ElMessage.warning('请填写名称和编码/账号'); return } |
| | | const valid = await formRef.value?.validate().catch(() => false) |
| | | if (!valid) return |
| | | if (isRole.value) { |
| | | const payload = { roleName: form.name.trim(), roleCode: form.code.trim(), remark: form.remark.trim(), status: form.status } |
| | | if (editingId.value) await updateAdminRole(editingId.value, payload); else await createAdminRole(payload) |
| | |
| | | ElMessage.success('保存成功') |
| | | editVisible.value = false |
| | | loadData() |
| | | } |
| | | |
| | | async function changeRoleStatus(row: Record<string, unknown>) { |
| | | const status = Number(row.status) === 1 ? 0 : 1 |
| | | await updateAdminRoleStatus(row.id as number | string, status) |
| | | ElMessage.success(status === 1 ? '已启用' : '已停用') |
| | | await loadData() |
| | | } |
| | | |
| | | async function removeRole(row: Record<string, unknown>) { |
| | | await ElMessageBox.confirm(`确认删除角色“${row.roleName}”?`, '删除角色', { type: 'warning' }) |
| | | await deleteAdminRole(row.id as number | string) |
| | | ElMessage.success('已删除') |
| | | await loadData() |
| | | } |
| | | |
| | | async function download() { |
| | |
| | | <el-button v-if="!isUpgrade" type="primary" :icon="Plus" @click="openCreate">新建</el-button> |
| | | <el-button :icon="Download" @click="download">下载</el-button> |
| | | </div> |
| | | <el-table v-loading="loading" :data="rows" border class="business-fill-table" :empty-text="`暂无${title}数据`"> |
| | | <el-table v-loading="loading" :data="pagedRows" border class="business-fill-table" :empty-text="`暂无${title}数据`"> |
| | | <el-table-column v-for="[key, label] in columns" :key="key" :prop="key" :label="label" min-width="130" show-overflow-tooltip /> |
| | | <el-table-column label="操作" width="150" fixed="right" align="center"> |
| | | <template #default="{ row }"> |
| | | <el-button link type="primary" :icon="View" @click="view(row)">查看</el-button> |
| | | <el-button v-if="!isUpgrade" link type="primary" :icon="Edit" @click="openEdit(row)">编辑</el-button> |
| | | <template v-if="isRole && !isPlatformAdminRole(row)"> |
| | | <el-button link type="primary" @click="changeRoleStatus(row)">{{ Number(row.status) === 1 ? '停用' : '启用' }}</el-button> |
| | | <el-button link type="danger" @click="removeRole(row)">删除</el-button> |
| | | </template> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | <div class="pagination-row"> |
| | | <el-pagination v-if="isUpgrade" v-model:current-page="pagination.page" v-model:page-size="pagination.size" background layout="total, prev, pager, next" :total="pagination.total" @change="loadData" /> |
| | | <el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.size" background layout="total, sizes, prev, pager, next" :page-sizes="[10, 20, 50]" :total="pageTotal" @change="changePage" /> |
| | | </div> |
| | | </section> |
| | | <el-dialog v-model="dialogVisible" :title="title" width="520px"> |
| | | <el-dialog v-model="dialogVisible" :title="title" width="520px" :close-on-click-modal="false"> |
| | | <el-descriptions v-if="activeRow" :column="1" border> |
| | | <el-descriptions-item v-for="[key, label] in columns" :key="key" :label="label">{{ activeRow[key] || '-' }}</el-descriptions-item> |
| | | </el-descriptions> |
| | | </el-dialog> |
| | | <el-dialog v-model="editVisible" :title="`编辑${title}`" width="480px" destroy-on-close> |
| | | <el-form label-width="92px"> |
| | | <el-form-item :label="isRole ? '角色名称' : '员工姓名'"><el-input v-model="form.name" /></el-form-item> |
| | | <el-form-item :label="isRole ? '角色编码' : '登录账号'"><el-input v-model="form.code" /></el-form-item> |
| | | <el-form-item v-if="!isRole" label="登录密码"><el-input v-model="form.password" type="password" :placeholder="editingId ? '留空则不修改' : '请输入登录密码'" /></el-form-item> |
| | | <el-dialog v-model="editVisible" :title="`编辑${title}`" width="480px" destroy-on-close :close-on-click-modal="false"> |
| | | <el-form ref="formRef" :model="form" :rules="rules" label-width="92px"> |
| | | <el-form-item :label="isRole ? '角色名称' : '员工姓名'" prop="name"><el-input v-model="form.name" /></el-form-item> |
| | | <el-form-item :label="isRole ? '角色编码' : '登录账号'" prop="code"><el-input v-model="form.code" :placeholder="isRole ? '' : '仅支持英文和数字'" maxlength="32" /></el-form-item> |
| | | <el-form-item v-if="!isRole" label="登录密码" prop="password"><el-input v-model="form.password" type="password" :placeholder="editingId ? '留空则不修改' : '请输入登录密码'" /></el-form-item> |
| | | <el-form-item v-if="isRole" label="备注"><el-input v-model="form.remark" /></el-form-item> |
| | | <el-form-item label="状态"><el-switch v-model="form.status" :active-value="1" :inactive-value="0" /></el-form-item> |
| | | </el-form> |
| | |
| | | return text(status) |
| | | } |
| | | |
| | | function formatFiles(value?: string | string[] | null) { |
| | | if (!value) return [] as string[] |
| | | if (Array.isArray(value)) return value.filter(Boolean) |
| | | const raw = String(value).trim() |
| | | if (!raw) return [] as string[] |
| | | try { |
| | | const parsed = JSON.parse(raw) |
| | | if (Array.isArray(parsed)) return parsed.filter(Boolean).map(String) |
| | | } catch { |
| | | // ignore |
| | | } |
| | | return raw.split(/[;,\n]/).map(item => item.trim()).filter(Boolean) |
| | | } |
| | | |
| | | function familyName() { |
| | | return detail.value?.familyName || detail.value?.dependentName || '-' |
| | | } |
| | |
| | | { label: '审核备注:', value: text(detail.value?.auditRemark) }, |
| | | ]) |
| | | |
| | | const proofFiles = computed(() => formatFiles(detail.value?.proofFiles as unknown as string | string[] | null)) |
| | | const isPending = computed(() => detail.value?.status === 0 || detail.value?.status === '0' || detail.value?.statusText === '待审核') |
| | | |
| | | async function loadDetail() { |
| New file |
| | |
| | | package com.talentcard.server.response; |
| | | |
| | | import java.time.LocalDateTime; |
| | | |
| | | import com.talentcard.server.entity.DependentCardApply; |
| | | import io.swagger.v3.oas.annotations.media.Schema; |
| | | |
| | | @Schema(name = "DependentCardApplyDetailResponse", description = "家属附属卡申请详情") |
| | | public record DependentCardApplyDetailResponse( |
| | | Long id, |
| | | String applyNo, |
| | | String dependentName, |
| | | String dependentIdCardNo, |
| | | String dependentMobile, |
| | | String relation, |
| | | Integer status, |
| | | String statusText, |
| | | String auditRemark, |
| | | LocalDateTime applyTime |
| | | ) { |
| | | public static DependentCardApplyDetailResponse from(DependentCardApply apply) { |
| | | return new DependentCardApplyDetailResponse( |
| | | apply.getId(), |
| | | apply.getApplyNo(), |
| | | apply.getDependentName(), |
| | | apply.getDependentIdCardNo(), |
| | | apply.getDependentMobile(), |
| | | apply.getRelation(), |
| | | apply.getStatus(), |
| | | statusText(apply.getStatus()), |
| | | apply.getAuditRemark(), |
| | | apply.getApplyTime() |
| | | ); |
| | | } |
| | | |
| | | private static String statusText(Integer status) { |
| | | if (status == null) return "未知"; |
| | | return switch (status) { |
| | | case DependentCardApply.STATUS_PENDING_REVIEW -> "待审核"; |
| | | case DependentCardApply.STATUS_APPROVED -> "已通过"; |
| | | case DependentCardApply.STATUS_REJECTED -> "已驳回"; |
| | | case DependentCardApply.STATUS_CANCELLED -> "已取消"; |
| | | default -> "未知"; |
| | | }; |
| | | } |
| | | } |