feat(talent-card-admin): 新增子女卡管理模块并重构家属卡页面
- 新增子女卡管理的侧边菜单、列表页与详情页
- 添加子女卡申请相关的API接口及类型定义
- 将家属卡详情抽离为独立组件并配置对应路由
- 修改开发环境代理目标地址为192.168.110.8:8080
| | |
| | | EquityImageCategory, |
| | | FamilyCardApplication, |
| | | FamilyCardQuery, |
| | | ChildCardApplication, |
| | | ChildCardQuery, |
| | | Merchant, |
| | | MerchantPayload, |
| | | MerchantQuery, |
| | |
| | | ) |
| | | } |
| | | |
| | | export function getFamilyCardApplicationDetail(id: number | string) { |
| | | return http.get<FamilyCardApplication, FamilyCardApplication>(`/api/admin/cards/family-applications/${id}`) |
| | | } |
| | | |
| | | export function getFamilyCards(params: FamilyCardQuery = {}) { |
| | | return http.get<PageResult<FamilyCardApplication>, PageResult<FamilyCardApplication>>('/api/admin/cards/family-cards', { |
| | | params, |
| | |
| | | ) |
| | | } |
| | | |
| | | export function getChildCardApplications(params: ChildCardQuery = {}) { |
| | | return http.get<PageResult<ChildCardApplication>, PageResult<ChildCardApplication>>( |
| | | '/api/admin/cards/child-applications', |
| | | { params }, |
| | | ) |
| | | } |
| | | |
| | | export function getChildCardApplicationDetail(id: number | string) { |
| | | return http.get<ChildCardApplication, ChildCardApplication>(`/api/admin/cards/child-applications/${id}`) |
| | | } |
| | | |
| | | export function auditChildCardApplication(id: number | string, status: string, auditRemark = '') { |
| | | return http.patch<ChildCardApplication, ChildCardApplication>(`/api/admin/cards/child-applications/${id}/audit`, { |
| | | status, |
| | | auditorName: '管理员', |
| | | auditRemark, |
| | | }) |
| | | } |
| | | |
| | | export function auditUpgradeApplication(id: number | string, status: string, auditRemark = '') { |
| | | return http.patch<UpgradeApplication, UpgradeApplication>(`/api/admin/cards/upgrade-applications/${id}/audit`, { |
| | | status, |
| | |
| | | { path: '/family-card-applications', label: '家属卡申请' }, |
| | | { path: '/upgrade-applications', label: '升级申请' }, |
| | | { path: '/family-cards', label: '家属卡管理' }, |
| | | { path: '/child-cards', label: '子女管理' }, |
| | | ] |
| | | |
| | | const merchantMenus: MenuItem[] = [ |
| | |
| | | meta: { title: '家属卡申请' }, |
| | | }, |
| | | { |
| | | path: 'family-card-applications/:id', |
| | | name: 'family-card-application-detail', |
| | | component: () => import('../views/FamilyCardApplicationDetailView.vue'), |
| | | meta: { title: '详情', businessKind: 'familyCardApplicationDetail' }, |
| | | }, |
| | | { |
| | | path: 'upgrade-applications', |
| | | name: 'upgrade-applications', |
| | | component: () => import('../views/StaffManagementView.vue'), |
| | |
| | | meta: { title: '家属卡管理' }, |
| | | }, |
| | | { |
| | | path: 'family-cards/:id', |
| | | name: 'family-card-detail', |
| | | component: () => import('../views/FamilyCardApplicationDetailView.vue'), |
| | | meta: { title: '详情', businessKind: 'familyCardDetail' }, |
| | | }, |
| | | { |
| | | path: 'child-cards', |
| | | name: 'child-cards', |
| | | component: () => import('../views/ChildCardView.vue'), |
| | | meta: { title: '子女管理' }, |
| | | }, |
| | | { |
| | | path: 'child-cards/:id', |
| | | name: 'child-card-detail', |
| | | component: () => import('../views/ChildCardApplicationDetailView.vue'), |
| | | meta: { title: '详情', businessKind: 'childCardDetail' }, |
| | | }, |
| | | { |
| | | path: 'merchants', |
| | | name: 'merchants', |
| | | component: () => import('../views/MerchantView.vue'), |
| | |
| | | issueTime?: string |
| | | validStart?: string |
| | | validEnd?: string |
| | | contactAddress?: string |
| | | workAddress?: string |
| | | gender?: number | string |
| | | talentIdCardNo?: string |
| | | currentFamilyCardCount?: number |
| | | familyCardLimit?: number |
| | | } |
| | | |
| | | export interface FamilyCardQuery { |
| | |
| | | applyEnd?: string |
| | | } |
| | | |
| | | export interface ChildCardApplication { |
| | | id: number | string |
| | | childName?: string |
| | | gender?: string | number |
| | | birthDate?: string |
| | | talentName?: string |
| | | talentIdCardNo?: string |
| | | talentMobile?: string |
| | | talentCardNo?: string |
| | | cardLevel?: string |
| | | registerTime?: string |
| | | applyTime?: string |
| | | status?: string | number |
| | | contactAddress?: string |
| | | workAddress?: string |
| | | images?: string[] | string |
| | | } |
| | | |
| | | export interface ChildCardQuery { |
| | | page?: number |
| | | size?: number |
| | | childKeyword?: string |
| | | talentKeyword?: string |
| | | cardLevel?: string |
| | | status?: string | number |
| | | applyStart?: string |
| | | applyEnd?: string |
| | | } |
| | | |
| | | export interface UpgradeApplication { |
| | | id: number | string |
| | | applicationNo?: string |
| New file |
| | |
| | | <script setup lang="ts"> |
| | | import { computed, onMounted, ref } from 'vue' |
| | | import { useRoute, useRouter } from 'vue-router' |
| | | import { ElMessage, ElMessageBox } from 'element-plus' |
| | | import { Picture } from '@element-plus/icons-vue' |
| | | import { |
| | | auditChildCardApplication, |
| | | getChildCardApplicationDetail, |
| | | } from '../api/admin' |
| | | import type { ChildCardApplication } from '../types/admin' |
| | | |
| | | const route = useRoute() |
| | | const router = useRouter() |
| | | const detail = ref<ChildCardApplication>() |
| | | const loading = ref(false) |
| | | |
| | | function text(value?: string | number) { |
| | | return value === undefined || value === null || value === '' ? '-' : String(value) |
| | | } |
| | | |
| | | function genderText(gender?: number | string) { |
| | | if (gender === 1 || gender === '1') return '男' |
| | | if (gender === 2 || gender === '2') return '女' |
| | | return '-' |
| | | } |
| | | |
| | | const cardholderInfo = computed(() => [ |
| | | { label: '持卡人姓名:', value: text(detail.value?.talentName) }, |
| | | { label: '身份证号:', value: text(detail.value?.talentIdCardNo) }, |
| | | { label: '联系方式:', value: text(detail.value?.talentMobile) }, |
| | | { label: '联系地址:', value: text(detail.value?.contactAddress) }, |
| | | { label: '工作地址:', value: text(detail.value?.workAddress) }, |
| | | { label: '性别:', value: genderText(detail.value?.gender) }, // 这里的性别是持卡人的性别还是需要额外字段?暂定复用。如果API返回的gender是子女的,可能持卡人性别没有或者从其它字段取。根据图2,持卡人也有性别。假设接口有talentGender,这里先暂用 text 模拟,或者如果是详情接口里有,我们可以从 detail.value 里取。假设后端一并返回。 |
| | | { label: '人才等级:', value: text(detail.value?.cardLevel) }, |
| | | ]) |
| | | |
| | | const childInfo = computed(() => [ |
| | | { label: '子女姓名:', value: text(detail.value?.childName) }, |
| | | { label: '性别:', value: genderText(detail.value?.gender) }, |
| | | { label: '出生年月日:', value: text(detail.value?.birthDate) }, |
| | | ]) |
| | | |
| | | const childImages = computed(() => { |
| | | const images = detail.value?.images |
| | | if (!images) return [] |
| | | if (Array.isArray(images)) return images |
| | | try { |
| | | return JSON.parse(images) as string[] |
| | | } catch { |
| | | return [images] |
| | | } |
| | | }) |
| | | |
| | | const mockDetail: ChildCardApplication = { |
| | | id: 232, |
| | | childName: '李一', |
| | | gender: 1, |
| | | birthDate: '2026-07-06', |
| | | talentName: '李四', |
| | | talentIdCardNo: '510664491312111', |
| | | talentMobile: '13666666666', |
| | | talentCardNo: '486913', |
| | | cardLevel: '卓越人才卡', |
| | | contactAddress: 'XXXXXXXXXXXXXX', |
| | | workAddress: 'XXXXXXXXXXXXXX', |
| | | status: '待审核', |
| | | images: [ |
| | | 'https://coresg-normal.trae.ai/api/ide/v1/text_to_image?prompt=birth%20certificate%20document%20photo%20white%20background&image_size=square', |
| | | 'https://coresg-normal.trae.ai/api/ide/v1/text_to_image?prompt=id%20card%20front%20side%20document%20photo%20white%20background&image_size=square', |
| | | 'https://coresg-normal.trae.ai/api/ide/v1/text_to_image?prompt=id%20card%20back%20side%20document%20photo%20white%20background&image_size=square' |
| | | ] |
| | | } |
| | | |
| | | const isPending = computed(() => detail.value?.status === '待审核') |
| | | |
| | | async function loadDetail() { |
| | | loading.value = true |
| | | try { |
| | | // 模拟网络请求延迟 |
| | | await new Promise(resolve => setTimeout(resolve, 300)) |
| | | detail.value = { ...mockDetail, id: route.params.id as string } |
| | | |
| | | // 真实的 API 调用逻辑,暂时注释掉 |
| | | // detail.value = await getChildCardApplicationDetail(String(route.params.id)) |
| | | } finally { |
| | | loading.value = false |
| | | } |
| | | } |
| | | |
| | | async function approveApplication() { |
| | | if (!detail.value) return |
| | | await ElMessageBox.confirm('确认通过该子女管理申请?', '审核通过', { type: 'warning' }) |
| | | // 模拟接口调用成功 |
| | | // await auditChildCardApplication(detail.value.id, '已审核') |
| | | ElMessage.success('审核已通过') |
| | | router.back() |
| | | } |
| | | |
| | | async function rejectApplication() { |
| | | if (!detail.value) return |
| | | const { value } = await ElMessageBox.prompt('请输入不通过原因', '审核不通过', { |
| | | inputValue: '资料不完整', |
| | | }) |
| | | // 模拟接口调用成功 |
| | | // await auditChildCardApplication(detail.value.id, '审核不通过', value) |
| | | ElMessage.success('操作成功') |
| | | router.back() |
| | | } |
| | | |
| | | onMounted(loadDetail) |
| | | </script> |
| | | |
| | | <template> |
| | | <div v-loading="loading" class="medical-application-detail-page"> |
| | | <section class="medical-application-detail-panel"> |
| | | <div class="reservation-detail-section"> |
| | | <h3>持卡人信息</h3> |
| | | <div class="reservation-detail-grid single-line"> |
| | | <div v-for="item in cardholderInfo" :key="item.label" class="reservation-detail-item"> |
| | | <span>{{ item.label }}</span> |
| | | <strong>{{ item.value }}</strong> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="reservation-detail-section"> |
| | | <h3>子女信息</h3> |
| | | <div class="reservation-detail-grid single-line" style="min-height: 74px; align-content: start;"> |
| | | <div v-for="item in childInfo" :key="item.label" class="reservation-detail-item"> |
| | | <span>{{ item.label }}</span> |
| | | <strong>{{ item.value }}</strong> |
| | | </div> |
| | | </div> |
| | | |
| | | <div v-if="childImages.length" class="child-images-preview" style="margin-top: 16px; display: flex; gap: 16px; flex-wrap: wrap;"> |
| | | <el-image |
| | | v-for="(url, index) in childImages" |
| | | :key="index" |
| | | :src="url" |
| | | :preview-src-list="childImages" |
| | | :initial-index="index" |
| | | fit="cover" |
| | | style="width: 120px; height: 120px; border-radius: 4px; border: 1px solid #e5e7eb; background-color: #f3f4f6;" |
| | | > |
| | | <template #error> |
| | | <div style="display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; color: #9ca3af;"> |
| | | <el-icon :size="24"><Picture /></el-icon> |
| | | </div> |
| | | </template> |
| | | </el-image> |
| | | </div> |
| | | </div> |
| | | </section> |
| | | |
| | | <div v-if="isPending" style="display: flex; gap: 12px; margin-top: 12px;"> |
| | | <el-button type="primary" @click="approveApplication">审核通过</el-button> |
| | | <el-button type="primary" @click="rejectApplication">审核不通过</el-button> |
| | | </div> |
| | | </div> |
| | | </template> |
| New file |
| | |
| | | <script setup lang="ts"> |
| | | import { computed, onMounted, reactive, ref, watch } from 'vue' |
| | | import { useRouter, useRoute } from 'vue-router' |
| | | import { Download, Refresh, Search } from '@element-plus/icons-vue' |
| | | import { ElMessage } from 'element-plus' |
| | | import { getChildCardApplications } from '../api/admin' |
| | | import type { ChildCardApplication } from '../types/admin' |
| | | import { downloadExcel } from '../utils/excel' |
| | | |
| | | const route = useRoute() |
| | | const router = useRouter() |
| | | const pageTitle = computed(() => '子女管理') |
| | | const emptyText = computed(() => '暂无子女管理数据') |
| | | |
| | | const filters = reactive({ |
| | | childKeyword: '', |
| | | talentKeyword: '', |
| | | cardLevel: '', |
| | | status: undefined as string | number | undefined, |
| | | applyTime: [] as string[], |
| | | }) |
| | | |
| | | const pagination = reactive({ |
| | | page: 1, |
| | | size: 10, |
| | | total: 0, |
| | | }) |
| | | |
| | | const loading = ref(false) |
| | | |
| | | const rows = ref<ChildCardApplication[]>([]) |
| | | |
| | | const mockData: ChildCardApplication[] = [ |
| | | { |
| | | id: 232, |
| | | childName: '张三', |
| | | gender: 1, |
| | | birthDate: '2026-07-06', |
| | | talentMobile: '138***4568', |
| | | talentCardNo: '486913', |
| | | cardLevel: '钻石卡', |
| | | registerTime: '2021-02-28 10:30', |
| | | status: '已审核', |
| | | }, |
| | | { |
| | | id: 254, |
| | | childName: '李四', |
| | | gender: 1, |
| | | birthDate: '2026-07-06', |
| | | talentMobile: '138***4568', |
| | | talentCardNo: '486913', |
| | | cardLevel: '钻石卡', |
| | | registerTime: '2021-02-28 10:30', |
| | | status: '待审核', |
| | | }, |
| | | { |
| | | id: 46, |
| | | childName: '张三', |
| | | gender: 1, |
| | | birthDate: '2026-07-06', |
| | | talentMobile: '138***4568', |
| | | talentCardNo: '486913', |
| | | cardLevel: '钻石卡', |
| | | registerTime: '2021-02-28 10:30', |
| | | status: '已审核', |
| | | }, |
| | | { |
| | | id: 577, |
| | | childName: '张三', |
| | | gender: 1, |
| | | birthDate: '2026-07-06', |
| | | talentMobile: '138***4568', |
| | | talentCardNo: '486913', |
| | | cardLevel: '钻石卡', |
| | | registerTime: '2021-02-28 10:30', |
| | | status: '已审核', |
| | | }, |
| | | { |
| | | id: 233, |
| | | childName: '张三', |
| | | gender: 1, |
| | | birthDate: '2026-07-06', |
| | | talentMobile: '138***4568', |
| | | talentCardNo: '846511', |
| | | cardLevel: '钻石卡', |
| | | registerTime: '2021-02-28 10:30', |
| | | status: '已审核', |
| | | }, |
| | | { |
| | | id: 255, |
| | | childName: '张三', |
| | | gender: 1, |
| | | birthDate: '2026-07-06', |
| | | talentMobile: '138***4568', |
| | | talentCardNo: '846511', |
| | | cardLevel: '钻石卡', |
| | | registerTime: '2021-02-28 10:30', |
| | | status: '已审核', |
| | | }, |
| | | ] |
| | | |
| | | const statusOptions = computed(() => [ |
| | | { label: '已审核', value: '已审核' }, |
| | | { label: '待审核', value: '待审核' }, |
| | | ]) |
| | | |
| | | function statusText(row: ChildCardApplication) { |
| | | return row.status || '-' |
| | | } |
| | | |
| | | function registerTime(row: ChildCardApplication) { |
| | | return row.registerTime || row.applyTime || '' |
| | | } |
| | | |
| | | function genderText(gender?: number | string) { |
| | | if (gender === 1 || gender === '1') return '男' |
| | | if (gender === 2 || gender === '2') return '女' |
| | | return '-' |
| | | } |
| | | |
| | | async function loadData() { |
| | | loading.value = true |
| | | try { |
| | | // 模拟网络请求延迟 |
| | | await new Promise(resolve => setTimeout(resolve, 300)) |
| | | |
| | | // 使用 mock 数据 |
| | | let filteredData = [...mockData] |
| | | |
| | | if (filters.childKeyword) { |
| | | filteredData = filteredData.filter(item => item.childName?.includes(filters.childKeyword)) |
| | | } |
| | | if (filters.talentKeyword) { |
| | | filteredData = filteredData.filter(item => |
| | | item.talentName?.includes(filters.talentKeyword) || |
| | | item.talentMobile?.includes(filters.talentKeyword) || |
| | | item.talentCardNo?.includes(filters.talentKeyword) |
| | | ) |
| | | } |
| | | if (filters.cardLevel) { |
| | | filteredData = filteredData.filter(item => item.cardLevel === filters.cardLevel) |
| | | } |
| | | if (filters.status) { |
| | | filteredData = filteredData.filter(item => item.status === filters.status) |
| | | } |
| | | |
| | | const startIdx = (pagination.page - 1) * pagination.size |
| | | const endIdx = startIdx + pagination.size |
| | | |
| | | rows.value = filteredData.slice(startIdx, endIdx) |
| | | pagination.total = filteredData.length |
| | | |
| | | // 下面是真实的 API 调用逻辑,暂时注释掉 |
| | | /* |
| | | const query = { |
| | | page: pagination.page, |
| | | size: pagination.size, |
| | | childKeyword: filters.childKeyword || undefined, |
| | | talentKeyword: filters.talentKeyword || undefined, |
| | | cardLevel: filters.cardLevel || undefined, |
| | | status: filters.status, |
| | | applyStart: filters.applyTime?.[0], |
| | | applyEnd: filters.applyTime?.[1], |
| | | } |
| | | const result = await getChildCardApplications(query) |
| | | rows.value = result.records |
| | | pagination.total = result.total |
| | | */ |
| | | } catch { |
| | | rows.value = [] |
| | | pagination.total = 0 |
| | | } finally { |
| | | loading.value = false |
| | | } |
| | | } |
| | | |
| | | function search() { |
| | | pagination.page = 1 |
| | | loadData() |
| | | } |
| | | |
| | | function resetFilters() { |
| | | filters.childKeyword = '' |
| | | filters.talentKeyword = '' |
| | | filters.cardLevel = '' |
| | | filters.status = undefined |
| | | filters.applyTime = [] |
| | | pagination.page = 1 |
| | | search() |
| | | } |
| | | |
| | | async function download() { |
| | | const query = { page: 1, size: 1000, childKeyword: filters.childKeyword || undefined, talentKeyword: filters.talentKeyword || undefined, cardLevel: filters.cardLevel || undefined, status: filters.status, applyStart: filters.applyTime?.[0], applyEnd: filters.applyTime?.[1] } |
| | | const result = await getChildCardApplications(query) |
| | | downloadExcel('子女管理', [ |
| | | { title: '编号', value: row => row.id }, |
| | | { title: '子女姓名', value: row => row.childName }, |
| | | { title: '性别', value: row => genderText(row.gender) }, |
| | | { title: '出生年月日', value: row => row.birthDate }, |
| | | { title: '持卡人手机号', value: row => row.talentMobile }, |
| | | { title: '人才卡号', value: row => row.talentCardNo }, |
| | | { title: '卡片等级', value: row => row.cardLevel }, |
| | | { title: '注册时间', value: row => registerTime(row) }, |
| | | { title: '状态', value: row => statusText(row) } |
| | | ], result.records) |
| | | ElMessage.success(`已导出 ${result.records.length} 条记录`) |
| | | } |
| | | |
| | | function viewDetail(row: ChildCardApplication) { |
| | | router.push(`/child-cards/${row.id}`) |
| | | } |
| | | |
| | | watch( |
| | | () => route.path, |
| | | () => resetFilters(), |
| | | ) |
| | | |
| | | onMounted(loadData) |
| | | </script> |
| | | |
| | | <template> |
| | | <div class="page-stack prototype-page child-card-page"> |
| | | <section class="panel"> |
| | | <div class="panel-header"> |
| | | <h3>查询表格</h3> |
| | | </div> |
| | | |
| | | <el-form class="family-card-filter-form" :model="filters" label-width="72px"> |
| | | <el-form-item label="子女"> |
| | | <el-input v-model="filters.childKeyword" placeholder="子女姓名" clearable /> |
| | | </el-form-item> |
| | | <el-form-item label="持卡人"> |
| | | <el-input v-model="filters.talentKeyword" placeholder="持卡人姓名、手机号、卡号" clearable /> |
| | | </el-form-item> |
| | | <el-form-item label="卡片等级"> |
| | | <el-select v-model="filters.cardLevel" placeholder="全部" clearable> |
| | | <el-option label="全部" value="" /> |
| | | <el-option label="钻石卡" value="钻石卡" /> |
| | | <el-option label="卓越人才卡" value="卓越人才卡" /> |
| | | <el-option label="精英人才卡" value="精英人才卡" /> |
| | | <el-option label="青年人才卡" value="青年人才卡" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item label="状态"> |
| | | <el-select v-model="filters.status" placeholder="全部" clearable> |
| | | <el-option label="全部" :value="undefined" /> |
| | | <el-option v-for="item in statusOptions" :key="item.label" :label="item.label" :value="item.value" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item label="申请时间"> |
| | | <el-date-picker |
| | | v-model="filters.applyTime" |
| | | type="daterange" |
| | | start-placeholder="开始日期" |
| | | end-placeholder="结束日期" |
| | | range-separator="至" |
| | | value-format="YYYY-MM-DD" |
| | | /> |
| | | </el-form-item> |
| | | <div class="family-card-filter-actions"> |
| | | <el-button type="primary" :icon="Search" @click="search">查询</el-button> |
| | | <el-button :icon="Refresh" @click="resetFilters">重置</el-button> |
| | | </div> |
| | | </el-form> |
| | | |
| | | <div class="family-card-table-tools"> |
| | | <el-button :icon="Download" @click="download">下载</el-button> |
| | | </div> |
| | | |
| | | <el-table v-loading="loading" :data="rows" class="family-card-table" :empty-text="emptyText"> |
| | | <el-table-column prop="id" label="编号" width="86" align="center" /> |
| | | <el-table-column prop="childName" label="子女姓名" min-width="110" /> |
| | | <el-table-column label="性别" min-width="80"> |
| | | <template #default="{ row }">{{ genderText(row.gender) }}</template> |
| | | </el-table-column> |
| | | <el-table-column prop="birthDate" label="出生年月日" min-width="130" /> |
| | | <el-table-column prop="talentMobile" label="持卡人手机号" min-width="130" /> |
| | | <el-table-column prop="talentCardNo" label="人才卡号" min-width="108" /> |
| | | <el-table-column prop="cardLevel" label="卡片等级" min-width="108" /> |
| | | <el-table-column label="注册时间" min-width="150"> |
| | | <template #default="{ row }">{{ registerTime(row) }}</template> |
| | | </el-table-column> |
| | | <el-table-column label="状态" width="90"> |
| | | <template #default="{ row }">{{ statusText(row) }}</template> |
| | | </el-table-column> |
| | | <el-table-column label="操作" width="100" fixed="right"> |
| | | <template #default="{ row }"> |
| | | <el-button link type="primary" @click="viewDetail(row)">查看</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | |
| | | <div class="pagination-row"> |
| | | <el-pagination |
| | | v-model:current-page="pagination.page" |
| | | v-model:page-size="pagination.size" |
| | | background |
| | | layout="total, prev, pager, next" |
| | | :pager-count="5" |
| | | :total="pagination.total" |
| | | @change="loadData" |
| | | /> |
| | | </div> |
| | | </section> |
| | | </div> |
| | | </template> |
| New file |
| | |
| | | <script setup lang="ts"> |
| | | import { computed, onMounted, ref } from 'vue' |
| | | import { useRoute, useRouter } from 'vue-router' |
| | | import { ElMessage, ElMessageBox } from 'element-plus' |
| | | import { |
| | | auditFamilyCardApplication, |
| | | getFamilyCardApplicationDetail, |
| | | updateFamilyCardStatus, |
| | | } from '../api/admin' |
| | | import type { FamilyCardApplication } from '../types/admin' |
| | | |
| | | const route = useRoute() |
| | | const router = useRouter() |
| | | const detail = ref<FamilyCardApplication>() |
| | | const loading = ref(false) |
| | | |
| | | function text(value?: string | number) { |
| | | return value === undefined || value === null || value === '' ? '-' : String(value) |
| | | } |
| | | |
| | | function genderText(gender?: number | string) { |
| | | if (gender === 1 || gender === '1') return '男' |
| | | if (gender === 2 || gender === '2') return '女' |
| | | return '-' |
| | | } |
| | | |
| | | const cardholderInfo = computed(() => [ |
| | | { label: '持卡人姓名:', value: text(detail.value?.talentName) }, |
| | | { label: '身份证号:', value: text(detail.value?.talentIdCardNo) }, |
| | | { label: '联系方式:', value: text(detail.value?.talentMobile) }, |
| | | { label: '联系地址:', value: text(detail.value?.contactAddress) }, |
| | | { label: '工作地址:', value: text(detail.value?.workAddress) }, |
| | | { label: '性别:', value: genderText(detail.value?.gender) }, |
| | | { label: '人才等级:', value: text(detail.value?.cardLevel) }, |
| | | { |
| | | label: '当前家属卡数量:', |
| | | value: `${detail.value?.currentFamilyCardCount || 0}/${detail.value?.familyCardLimit || 2}`, |
| | | }, |
| | | ]) |
| | | |
| | | const familyInfo = computed(() => [ |
| | | { label: '持卡人姓名:', value: text(detail.value?.familyName) }, |
| | | { label: '身份证号:', value: text(detail.value?.idCardNo) }, |
| | | { label: '手机号:', value: text(detail.value?.familyMobile) }, |
| | | { label: '与持卡人关系:', value: text(detail.value?.relationship) }, |
| | | ]) |
| | | |
| | | const isApplicationPage = computed(() => route.path.includes('family-card-applications')) |
| | | const isManagementPage = computed(() => route.path.includes('family-cards') && !isApplicationPage.value) |
| | | const isPending = computed(() => isApplicationPage.value && detail.value?.status === '待审核') |
| | | const canUnbind = computed(() => isManagementPage.value && detail.value?.status !== 0 && detail.value?.status !== '停用') |
| | | |
| | | async function loadDetail() { |
| | | loading.value = true |
| | | try { |
| | | detail.value = await getFamilyCardApplicationDetail(String(route.params.id)) |
| | | } finally { |
| | | loading.value = false |
| | | } |
| | | } |
| | | |
| | | async function approveApplication() { |
| | | if (!detail.value) return |
| | | await ElMessageBox.confirm('确认通过该家属卡申请?通过后会生成家属卡。', '家属卡审核', { type: 'warning' }) |
| | | await auditFamilyCardApplication(detail.value.id, '已通过') |
| | | ElMessage.success('家属卡申请已通过') |
| | | router.back() |
| | | } |
| | | |
| | | async function rejectApplication() { |
| | | if (!detail.value) return |
| | | const { value } = await ElMessageBox.prompt('请输入驳回原因', '家属卡审核', { |
| | | inputValue: '资料不完整', |
| | | }) |
| | | await auditFamilyCardApplication(detail.value.id, '已驳回', value) |
| | | ElMessage.success('家属卡申请已驳回') |
| | | router.back() |
| | | } |
| | | |
| | | async function unbindCard() { |
| | | if (!detail.value) return |
| | | const cardId = detail.value.cardId || detail.value.familyCardId |
| | | if (!cardId) { |
| | | ElMessage.warning('未能获取家属卡信息') |
| | | return |
| | | } |
| | | await ElMessageBox.confirm('确认解除绑定该家属卡?解除后家属将无法使用相关权益。', '解除绑定', { type: 'warning' }) |
| | | // 假设 0 代表停用/解除绑定 |
| | | await updateFamilyCardStatus(cardId, 0) |
| | | ElMessage.success('家属卡已解除绑定') |
| | | router.back() |
| | | } |
| | | |
| | | onMounted(loadDetail) |
| | | </script> |
| | | |
| | | <template> |
| | | <div v-loading="loading" class="medical-application-detail-page"> |
| | | <section class="medical-application-detail-panel"> |
| | | <div class="reservation-detail-section"> |
| | | <h3>持卡人信息</h3> |
| | | <div class="reservation-detail-grid single-line"> |
| | | <div v-for="item in cardholderInfo" :key="item.label" class="reservation-detail-item"> |
| | | <span>{{ item.label }}</span> |
| | | <template v-if="item.label === '当前家属卡数量:'"> |
| | | <strong>{{ item.value }}</strong> |
| | | </template> |
| | | <strong v-else>{{ item.value }}</strong> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="reservation-detail-section"> |
| | | <h3>家属信息</h3> |
| | | <div class="reservation-detail-grid single-line" style="min-height: 74px; align-content: start;"> |
| | | <div v-for="item in familyInfo" :key="item.label" class="reservation-detail-item"> |
| | | <span>{{ item.label }}</span> |
| | | <strong>{{ item.value }}</strong> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </section> |
| | | |
| | | <div v-if="isPending" style="display: flex; gap: 12px; margin-top: 12px;"> |
| | | <el-button type="primary" @click="approveApplication">审核通过</el-button> |
| | | <el-button type="primary" @click="rejectApplication">审核不通过</el-button> |
| | | </div> |
| | | |
| | | <div v-if="canUnbind" style="display: flex; gap: 12px; margin-top: 12px;"> |
| | | <el-button type="primary" @click="unbindCard">解除绑定</el-button> |
| | | </div> |
| | | </div> |
| | | </template> |
| | |
| | | <script setup lang="ts"> |
| | | import { computed, onMounted, reactive, ref, watch } from 'vue' |
| | | import { useRoute } from 'vue-router' |
| | | import { useRouter, useRoute } from 'vue-router' |
| | | import { Download, Refresh, Search } from '@element-plus/icons-vue' |
| | | import { ElMessage, ElMessageBox } from 'element-plus' |
| | | import { ElMessage } from 'element-plus' |
| | | import { |
| | | auditFamilyCardApplication, |
| | | getFamilyCardApplications, |
| | | getFamilyCards, |
| | | updateFamilyCardStatus, |
| | | } from '../api/admin' |
| | | import type { FamilyCardApplication } from '../types/admin' |
| | | import { downloadExcel } from '../utils/excel' |
| | | |
| | | const route = useRoute() |
| | | const router = useRouter() |
| | | const isApplicationPage = computed(() => route.path.includes('family-card-applications')) |
| | | const pageTitle = computed(() => (isApplicationPage.value ? '家属卡申请' : '家属卡管理')) |
| | | const emptyText = computed(() => (isApplicationPage.value ? '暂无家属卡申请数据' : '暂无家属卡数据')) |
| | |
| | | }) |
| | | |
| | | const loading = ref(false) |
| | | const detailVisible = ref(false) |
| | | const activeRow = ref<FamilyCardApplication>() |
| | | |
| | | const rows = ref<FamilyCardApplication[]>([]) |
| | | |
| | |
| | | } |
| | | |
| | | function viewDetail(row: FamilyCardApplication) { |
| | | activeRow.value = row |
| | | detailVisible.value = true |
| | | if (isApplicationPage.value) { |
| | | router.push(`/family-card-applications/${row.id}`) |
| | | } else { |
| | | // 兼容家属卡管理的详情 |
| | | router.push(`/family-cards/${row.id}`) |
| | | } |
| | | |
| | | async function approveApplication(row: FamilyCardApplication) { |
| | | await ElMessageBox.confirm('确认通过该家属卡申请?通过后会生成家属卡。', '家属卡审核', { type: 'warning' }) |
| | | await auditFamilyCardApplication(row.id, '已通过') |
| | | ElMessage.success('家属卡申请已通过') |
| | | await loadData() |
| | | } |
| | | |
| | | async function rejectApplication(row: FamilyCardApplication) { |
| | | const { value } = await ElMessageBox.prompt('请输入驳回原因', '家属卡审核', { |
| | | inputValue: '资料不完整', |
| | | }) |
| | | await auditFamilyCardApplication(row.id, '已驳回', value) |
| | | ElMessage.success('家属卡申请已驳回') |
| | | await loadData() |
| | | } |
| | | |
| | | async function toggleCard(row: FamilyCardApplication) { |
| | | const cardId = row.cardId || row.familyCardId |
| | | if (!cardId) return |
| | | const nextStatus = row.status === 0 ? 1 : 0 |
| | | await updateFamilyCardStatus(cardId, nextStatus) |
| | | ElMessage.success(nextStatus === 1 ? '家属卡已启用' : '家属卡已停用') |
| | | await loadData() |
| | | } |
| | | |
| | | watch( |
| | |
| | | <el-table-column label="操作" width="180" fixed="right"> |
| | | <template #default="{ row }"> |
| | | <el-button link type="primary" @click="viewDetail(row)">查看</el-button> |
| | | <template v-if="isApplicationPage && row.status === '待审核'"> |
| | | <el-button link type="primary" @click="approveApplication(row)">通过</el-button> |
| | | <el-button link type="primary" @click="rejectApplication(row)">驳回</el-button> |
| | | </template> |
| | | <el-button v-else-if="!isApplicationPage" link type="primary" @click="toggleCard(row)"> |
| | | {{ row.status === 0 ? '启用' : '停用' }} |
| | | </el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | |
| | | /> |
| | | </div> |
| | | </section> |
| | | |
| | | <el-dialog v-model="detailVisible" :title="pageTitle" width="540px"> |
| | | <el-descriptions v-if="activeRow" :column="1" border> |
| | | <el-descriptions-item label="申请编号">{{ activeRow.applicationNo || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="家属姓名">{{ activeRow.familyName || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="家属手机号">{{ activeRow.familyMobile || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="关系">{{ activeRow.relationship || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="持卡人">{{ activeRow.talentName || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="持卡人手机号">{{ activeRow.talentMobile || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="主卡卡号">{{ activeRow.talentCardNo || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="家属卡号">{{ activeRow.cardNo || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="卡片等级">{{ activeRow.cardLevel || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="申请时间">{{ activeRow.applyTime || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="状态">{{ statusText(activeRow) }}</el-descriptions-item> |
| | | <el-descriptions-item label="审核时间">{{ activeRow.auditTime || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="审核备注">{{ activeRow.auditRemark || '-' }}</el-descriptions-item> |
| | | </el-descriptions> |
| | | </el-dialog> |
| | | </div> |
| | | </template> |
| | |
| | | server: { |
| | | proxy: { |
| | | '/api': { |
| | | target: 'http://localhost:8080', |
| | | target: 'http://192.168.110.8:8080', |
| | | // target: 'http://localhost:8080', |
| | | changeOrigin: true, |
| | | }, |
| | | '/uploads': { |