shinnosuke
2026-08-11 a375ecc695efe1739871e9d9d0bf981454aa8061
fix: restore features overwritten by merge
1个文件已添加
6个文件已修改
208 ■■■■■ 已修改文件
rencaika/pages/external-webview/external-webview.uvue 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/layouts/AdminLayout.vue 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/router/index.ts 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/types/admin.ts 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/views/BusinessManagementView.vue 76 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/views/FamilyCardApplicationDetailView.vue 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/java/com/talentcard/server/response/DependentCardApplyDetailResponse.java 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rencaika/pages/external-webview/external-webview.uvue
@@ -12,7 +12,8 @@
</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'
@@ -30,6 +31,7 @@
        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
@@ -38,7 +40,14 @@
    })
    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('服务地址无效', '请从已配置的服务入口重新进入')
@@ -48,6 +57,7 @@
        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 : '请稍后重试')
@@ -56,6 +66,21 @@
            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
@@ -93,7 +118,7 @@
        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>
talent-card-admin/src/layouts/AdminLayout.vue
@@ -34,6 +34,7 @@
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
@@ -128,6 +129,24 @@
  { 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))
@@ -288,7 +307,7 @@
        <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>
@@ -306,7 +325,7 @@
            <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" />
talent-card-admin/src/router/index.ts
@@ -4,6 +4,7 @@
const familyCardView = () => import('../views/FamilyCardView.vue')
const businessManagementView = () => import('../views/BusinessManagementView.vue')
const cardPlaceholderView = () => import('../views/CardPlaceholderView.vue')
const router = createRouter({
  history: createWebHistory(),
@@ -154,7 +155,7 @@
        {
          path: 'appointment-management',
          name: 'appointment-management',
          component: () => import('../views/AppointmentManagementView.vue'),
          component: cardPlaceholderView,
          meta: { title: '预约服务管理', businessKind: 'appointmentManagement' },
        },
        {
@@ -196,7 +197,7 @@
        {
          path: 'car-wash-benefits',
          name: 'car-wash-benefits',
          component: () => import('../views/CarWashBenefitView.vue'),
          component: cardPlaceholderView,
          meta: { title: '洗车权益管理', businessKind: 'carWashBenefit' },
        },
        {
@@ -238,13 +239,13 @@
        {
          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' },
        },
        {
talent-card-admin/src/types/admin.ts
@@ -560,21 +560,29 @@
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
talent-card-admin/src/views/BusinessManagementView.vue
@@ -2,14 +2,17 @@
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'
@@ -25,25 +28,54 @@
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
@@ -64,6 +96,7 @@
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}`)
@@ -88,7 +121,8 @@
}
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)
@@ -100,6 +134,20 @@
  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() {
@@ -157,29 +205,33 @@
        <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>
talent-card-admin/src/views/FamilyCardApplicationDetailView.vue
@@ -29,20 +29,6 @@
  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 || '-'
}
@@ -86,7 +72,6 @@
  { 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() {
talent-card-server/src/main/java/com/talentcard/server/response/DependentCardApplyDetailResponse.java
New file
@@ -0,0 +1,46 @@
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 -> "未知";
        };
    }
}