zjk
2026-08-05 8a185cf7c56f786b64c25c99028ac101ab37ac51
feat(talent-card-admin): 新增子女卡管理模块并重构家属卡页面

- 新增子女卡管理的侧边菜单、列表页与详情页
- 添加子女卡申请相关的API接口及类型定义
- 将家属卡详情抽离为独立组件并配置对应路由
- 修改开发环境代理目标地址为192.168.110.8:8080
3个文件已添加
6个文件已修改
754 ■■■■■ 已修改文件
talent-card-admin/src/api/admin.ts 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/layouts/AdminLayout.vue 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/router/index.ts 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/types/admin.ts 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/views/ChildCardApplicationDetailView.vue 160 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/views/ChildCardView.vue 306 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/views/FamilyCardApplicationDetailView.vue 133 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/views/FamilyCardView.vue 67 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/vite.config.ts 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/api/admin.ts
@@ -8,6 +8,8 @@
  EquityImageCategory,
  FamilyCardApplication,
  FamilyCardQuery,
  ChildCardApplication,
  ChildCardQuery,
  Merchant,
  MerchantPayload,
  MerchantQuery,
@@ -324,6 +326,10 @@
  )
}
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,
@@ -351,6 +357,25 @@
  )
}
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,
talent-card-admin/src/layouts/AdminLayout.vue
@@ -74,6 +74,7 @@
  { path: '/family-card-applications', label: '家属卡申请' },
  { path: '/upgrade-applications', label: '升级申请' },
  { path: '/family-cards', label: '家属卡管理' },
  { path: '/child-cards', label: '子女管理' },
]
const merchantMenus: MenuItem[] = [
talent-card-admin/src/router/index.ts
@@ -50,6 +50,12 @@
          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'),
@@ -62,6 +68,24 @@
          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'),
talent-card-admin/src/types/admin.ts
@@ -536,6 +536,12 @@
  issueTime?: string
  validStart?: string
  validEnd?: string
  contactAddress?: string
  workAddress?: string
  gender?: number | string
  talentIdCardNo?: string
  currentFamilyCardCount?: number
  familyCardLimit?: number
}
export interface FamilyCardQuery {
@@ -549,6 +555,35 @@
  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
talent-card-admin/src/views/ChildCardApplicationDetailView.vue
New file
@@ -0,0 +1,160 @@
<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>
talent-card-admin/src/views/ChildCardView.vue
New file
@@ -0,0 +1,306 @@
<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>
talent-card-admin/src/views/FamilyCardApplicationDetailView.vue
New file
@@ -0,0 +1,133 @@
<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>
talent-card-admin/src/views/FamilyCardView.vue
@@ -1,18 +1,17 @@
<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 ? '暂无家属卡申请数据' : '暂无家属卡数据'))
@@ -32,8 +31,6 @@
})
const loading = ref(false)
const detailVisible = ref(false)
const activeRow = ref<FamilyCardApplication>()
const rows = ref<FamilyCardApplication[]>([])
@@ -106,33 +103,12 @@
}
function viewDetail(row: FamilyCardApplication) {
  activeRow.value = row
  detailVisible.value = true
}
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()
  if (isApplicationPage.value) {
    router.push(`/family-card-applications/${row.id}`)
  } else {
    // 兼容家属卡管理的详情
    router.push(`/family-cards/${row.id}`)
  }
}
watch(
@@ -209,13 +185,6 @@
        <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>
@@ -232,23 +201,5 @@
        />
      </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>
talent-card-admin/vite.config.ts
@@ -7,7 +7,8 @@
  server: {
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        target: 'http://192.168.110.8:8080',
        // target: 'http://localhost:8080',
        changeOrigin: true,
      },
      '/uploads': {