shinnosuke
2026-08-25 b10362d06187a654bb71c9dcf1c51ddb6c7bcd36
fix: align admin management and miniapp merchant data
1个文件已添加
23个文件已修改
242 ■■■■■ 已修改文件
rencaika/pages/merchant/merchant.uvue 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rencaika/pages/shopping-benefits/shopping-benefits.uvue 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/api/admin.ts 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/style.css 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/utils/cardLevels.ts 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/views/AppointmentManagementView.vue 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/views/BusinessManagementView.vue 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/views/ChildCardView.vue 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/views/FamilyCardView.vue 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/views/FuelServiceView.vue 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/views/HotelReservationView.vue 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/views/HotelVerificationView.vue 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/views/MedicalApplicationView.vue 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/views/MedicalReservationView.vue 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/views/MerchantCreateView.vue 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/views/MovieTicketRecordView.vue 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/views/MovieTicketView.vue 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/views/OrderView.vue 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/views/ParkingPlateView.vue 73 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/views/RefundRecordView.vue 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/views/SpouseEmploymentView.vue 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/views/TalentListView.vue 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/java/com/talentcard/server/business/portal/PortalService.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/java/com/talentcard/server/service/ServiceAppointmentAdminService.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rencaika/pages/merchant/merchant.uvue
@@ -619,9 +619,16 @@
            applyMedicalRightsConfig(data)
            // 普通自助洗车复用餐饮商家的团单展示;外链洗车不会进入此通用详情页。
            const isNormalCarWash = merchantType.value == 'carWash'
            groupBuyingEnabled.value = isNormalCarWash
            groupBuyingEnabled.value = merchantType.value == 'culture'
                ? false
                : isNormalCarWash
                ? true
                : (data == null || data.groupBuyingEnabled == null ? groupBuyingEnabled.value : data.groupBuyingEnabled === true || Number(data.groupBuyingEnabled) == 1)
            if (merchantType.value == 'culture') {
                const cultureStoreConsume = data != null && (data.storeConsume === true || Number(data.storeConsume) == 1) && (data.consumeEnabled === true || Number(data.consumeEnabled) == 1)
                merchantServiceLabel.value = cultureStoreConsume ? '到店消费' : ''
                merchantPrimaryTag.value = merchantHasCoupon.value ? '券' : '权益'
            }
            isHotelMerchant.value = merchantType.value == 'hotel'
            merchantConsumeDiscount.value = ''
            if (isHotelMerchant.value) {
rencaika/pages/shopping-benefits/shopping-benefits.uvue
@@ -10,7 +10,7 @@
                <view v-if="initialLoading" class="service-empty"><view class="loading-spinner"></view><text>正在加载购物服务</text></view>
                <view v-else-if="initialError" class="service-empty error-state"><text>购物服务加载失败</text><text class="retry-text" @tap="retryInitialLoad">重新加载</text></view>
                <view v-else-if="hasLoaded && visibleServices.length == 0" class="service-empty"><text>暂无相关购物服务</text></view>
                <view v-else-if="hasLoaded" class="service-list"><view v-for="item in visibleServices" :key="item.id" class="service-item" :class="item.available ? '' : 'is-unavailable'" @tap="openMerchant(item)"><image class="service-image" :src="item.image" mode="aspectFill" @error="replaceImage(item)"></image><view class="service-info"><text class="service-name">{{item.name}}</text><text v-if="item.discount != ''" class="service-discount">{{item.discount}}</text><text v-else-if="!item.available" class="service-discount">{{item.unavailableReason}}</text><view v-if="item.address != ''" class="address-row"><image src="/static/benefits-assets/map-pin.svg" mode="aspectFit"></image><text>{{item.address}}</text></view></view></view></view>
                <view v-else-if="hasLoaded" class="service-list"><view v-for="item in visibleServices" :key="item.id" class="service-item" :class="item.available ? '' : 'is-unavailable'" @tap="openMerchant(item)"><image class="service-image" :src="item.image" mode="aspectFill" @error="replaceImage(item)"></image><view class="service-info"><text class="service-name">{{item.name}}</text><view v-if="item.tags.length > 0" class="service-tags"><text v-for="tag in item.tags" :key="tag" :class="tag == '券' ? 'coupon-tag' : 'service-tag'">{{tag}}</text></view><text v-else-if="!item.available" class="service-discount">{{item.unavailableReason}}</text><view v-if="item.address != ''" class="address-row"><image src="/static/benefits-assets/map-pin.svg" mode="aspectFit"></image><text>{{item.address}}</text></view></view></view></view>
                <view v-if="loadingMore" class="loading-row"><view class="loading-spinner"></view><text>正在加载</text></view><view v-else-if="loadMoreError" class="loading-row load-error" @tap="retryLoadMore"><text>加载失败,点击重试</text></view><view v-else-if="hasLoaded && !hasMore && visibleServices.length > 0" class="no-more"><view></view><text>已经到底了</text><view></view></view>
            </view><view class="bottom-space"></view>
        </scroll-view>
@@ -72,7 +72,16 @@
        getShoppingTopicMerchants(activeTab.value, talentId.value, currentPage.value + 1, 4, (data: any) => {
            if (requestVersion != listRequestVersion.value) return
            const records: any[] = data != null && data.records instanceof Array ? data.records as any[] : []; const next = append ? visibleServices.value.slice() : []
            for (let index = 0; index < records.length; index++) { const item = records[index]; next.push({ id: '' + item.id, merchantType: item.merchantType != null ? item.merchantType as string : '', name: item.merchantName != null && item.merchantName != '' ? item.merchantName as string : '商家名称未配置', discount: item.discountText != null ? item.discountText as string : '', address: item.address != null ? item.address as string : '', image: item.coverUrl != null && item.coverUrl != '' ? resolveApiUrl(item.coverUrl as string) : fallbackImage, available: item.available !== false, unavailableReason: item.unavailableReason != null && item.unavailableReason != '' ? item.unavailableReason as string : '该服务当前不可用' }) }
            for (let index = 0; index < records.length; index++) {
                const item = records[index]
                const merchantType = item.merchantType != null ? item.merchantType as string : ''
                const tags: string[] = []
                if (item.couponAvailable === true || Number(item.couponAvailable) == 1) tags.push('券')
                const configuredTag = item.discountText != null ? String(item.discountText).trim() : ''
                if (merchantType == 'culture' && (item.storeConsumeAvailable === true || Number(item.storeConsumeAvailable) == 1)) tags.push('到店消费')
                if (merchantType != 'culture' && configuredTag != '' && tags.indexOf(configuredTag) < 0) tags.push(configuredTag)
                next.push({ id: '' + item.id, merchantType: merchantType, name: item.merchantName != null && item.merchantName != '' ? item.merchantName as string : '商家名称未配置', discount: configuredTag, tags: tags, address: item.address != null ? item.address as string : '', image: item.coverUrl != null && item.coverUrl != '' ? resolveApiUrl(item.coverUrl as string) : fallbackImage, available: item.available !== false, unavailableReason: item.unavailableReason != null && item.unavailableReason != '' ? item.unavailableReason as string : '该服务当前不可用' })
            }
            visibleServices.value = next; currentPage.value++; hasMore.value = data != null && data.total != null ? next.length < data.total : false; initialLoading.value = false; loadingMore.value = false; hasLoaded.value = true
        }, () => { if (requestVersion != listRequestVersion.value) return; initialLoading.value = false; loadingMore.value = false; if (append) loadMoreError.value = true; else initialError.value = true })
    }
@@ -84,6 +93,10 @@
            uni.navigateTo({ url: '/pages/merchant/merchant?merchantId=' + encodeURIComponent(item.id) + '&merchantType=dental&name=' + encodeURIComponent(item.name) + '&image=' + encodeURIComponent(item.image) + '&address=' + encodeURIComponent(item.address) + '&discount=' + encodeURIComponent(item.discount) })
            return
        }
        if (item.merchantType == 'culture') {
            uni.navigateTo({ url: '/pages/merchant/merchant?merchantId=' + encodeURIComponent(item.id) + '&merchantType=culture&name=' + encodeURIComponent(item.name) + '&image=' + encodeURIComponent(item.image) + '&address=' + encodeURIComponent(item.address) + '&discount=' + encodeURIComponent(item.discount) })
            return
        }
        uni.navigateTo({ url: '/pages/shopping-merchant/shopping-merchant?merchantId=' + encodeURIComponent(item.id) })
    }
</script>
@@ -93,5 +106,5 @@
    .page,.page-scroll { width:100%; height:100vh; background-color:#FFFFFF; }.custom-nav { position:fixed; z-index:20; left:0; right:0; top:0; padding-bottom:20rpx; }.nav-bg { position:absolute; left:0; right:0; top:0; bottom:0; opacity:0; background-image:linear-gradient(180deg,#F26A38,#EE542B); transition:opacity 280ms ease-out; }.custom-nav.is-solid .nav-bg { opacity:1; }.safe-space,.nav-row { position:relative; z-index:1; }.safe-space { height:28px; }.nav-row { display:flex; box-sizing:border-box; padding-left:24rpx; flex-direction:row; align-items:center; justify-content:space-between; }.back-button { display:flex; width:54rpx; height:54rpx; position:relative; z-index:2; border-radius:50%; background-color:rgba(45,45,45,.52); justify-content:center; align-items:center; }.custom-nav.is-solid .back-button { background-color:transparent; }.back-button image { width:38rpx; height:38rpx; }.nav-title { position:absolute; z-index:1; left:0; right:0; color:#FFFFFF; font-size:31rpx; font-weight:500; text-align:center; text-shadow:0 2rpx 8rpx rgba(0,0,0,.35); }.capsule-space { height:100%; flex-shrink:0; }
    .hero { height:730rpx; position:relative; overflow:visible; background-color:#1C315E; }.hero-image { position:absolute; left:0; top:0; width:100%; height:730rpx; }.hero-top-shade { position:absolute; left:0; right:0; top:0; height:245rpx; background-image:linear-gradient(180deg,rgba(0,0,0,.62),rgba(0,0,0,0)); }.hero-bottom-shade { position:absolute; left:0; right:0; bottom:0; height:230rpx; background-image:linear-gradient(180deg,rgba(0,0,0,0),rgba(0,0,0,.42)); }.scene-name { position:absolute; z-index:3; left:38rpx; top:318rpx; color:#FFFFFF; font-family:'FoodCalligraphy'; font-size:48rpx; line-height:66rpx; text-shadow:0 3rpx 10rpx rgba(0,0,0,.55); }.scene-scroll { position:absolute; z-index:4; left:0; right:0; top:469rpx; width:100%; height:150rpx; white-space:nowrap; }.scene-row { display:flex; height:150rpx; flex-direction:row; align-items:center; }.scene-pad { width:322rpx; height:1rpx; flex-shrink:0; }.scene-item { display:flex; width:106rpx; height:106rpx; margin-right:31rpx; box-sizing:border-box; overflow:hidden; position:relative; z-index:1; border:3rpx solid rgba(255,255,255,.9); border-radius:8rpx; flex-shrink:0; transition:transform 180ms ease-out; }.scene-item image { width:100%; height:100%; }.scene-item.active { z-index:3; border-width:4rpx; border-color:#FFD329; transform:scale(1.28); box-shadow:0 8rpx 18rpx rgba(0,0,0,.28); }
    .topic-error { display:flex; height:76rpx; padding:0 31rpx; background-color:#FFF8F4; flex-direction:row; justify-content:center; align-items:center; }.topic-error text { color:#A48A7A; font-size:22rpx; }.topic-error text + text { margin-left:22rpx; color:#F26A38; }.topic-summary { position:relative; z-index:4; margin-top:-66rpx; }.intro-card { margin:0 31rpx; box-sizing:border-box; padding:25rpx 31rpx; border-radius:26rpx; background-image:linear-gradient(90deg,#FF5320,#FF8A22); box-shadow:0 10rpx 22rpx rgba(255,104,28,.24); }.intro-card text { color:#FFFFFF; font-size:23rpx; line-height:34rpx; }.service-section { padding:32rpx 31rpx 0; }.tab-scroll { width:100%; height:78rpx; white-space:nowrap; }.service-tabs { display:flex; height:72rpx; flex-direction:row; align-items:flex-start; }.service-tab { display:flex; width:144rpx; margin-right:42rpx; flex-direction:column; align-items:center; flex-shrink:0; }.service-tab text { color:#8A8D92; font-size:29rpx; font-weight:600; }.service-tab.active text { color:#363636; font-weight:700; }.tab-line { width:0; height:7rpx; margin-top:12rpx; border-radius:4rpx; background-image:linear-gradient(90deg,#FF4D36,#FFBF3A); transition:width 180ms ease-out; }.service-tab.active .tab-line { width:61rpx; }
    .service-list { padding-top:10rpx; }.service-item { display:flex; min-height:215rpx; margin-bottom:22rpx; flex-direction:row; align-items:flex-start; }.service-item.is-unavailable { opacity:.55; }.service-image { width:184rpx; height:184rpx; border-radius:17rpx; flex-shrink:0; background-color:#EEEEEE; }.service-info { display:flex; min-width:0; min-height:184rpx; flex:1; box-sizing:border-box; padding:4rpx 0 4rpx 24rpx; flex-direction:column; justify-content:space-between; }.service-name { color:#292929; font-size:29rpx; line-height:40rpx; font-weight:700; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }.service-discount { align-self:flex-start; max-width:400rpx; padding:6rpx 9rpx; border-radius:6rpx; background-color:#FFF5E7; color:#FF7C2B; font-size:21rpx; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }.address-row { display:flex; min-width:0; box-sizing:border-box; padding:7rpx 10rpx; border-radius:7rpx; background-image:linear-gradient(90deg,rgba(239,240,242,.98),rgba(242,243,245,.76) 58%,rgba(255,255,255,0)); flex-direction:row; align-items:flex-start; }.address-row image { width:27rpx; height:27rpx; margin-right:8rpx; flex-shrink:0; }.address-row text { min-width:0; flex:1; color:#7E8490; font-size:21rpx; line-height:31rpx; max-height:62rpx; overflow:hidden; }.service-empty { display:flex; height:310rpx; justify-content:center; align-items:center; }.service-empty text { color:#A5A8AD; font-size:25rpx; }.error-state { flex-direction:column; }.error-state .retry-text { margin-top:22rpx; color:#F26A38; }.loading-row,.no-more { display:flex; height:90rpx; flex-direction:row; justify-content:center; align-items:center; }.loading-spinner { width:28rpx; height:28rpx; margin-right:13rpx; box-sizing:border-box; border:3rpx solid #FFD6C4; border-top-color:#FF6C2B; border-radius:50%; animation:shopping-loading 720ms linear infinite; }.loading-row text { color:#989DA5; font-size:22rpx; }.load-error text { color:#F26A38; }.no-more view { width:70rpx; height:1rpx; background-color:#E7E7E7; }.no-more text { margin:0 18rpx; color:#B1B3B7; font-size:21rpx; }.bottom-space { height:calc(30rpx + env(safe-area-inset-bottom)); }@keyframes shopping-loading { from { transform:rotate(0deg); } to { transform:rotate(360deg); } }
    .service-list { padding-top:10rpx; }.service-item { display:flex; min-height:215rpx; margin-bottom:22rpx; flex-direction:row; align-items:flex-start; }.service-item.is-unavailable { opacity:.55; }.service-image { width:184rpx; height:184rpx; border-radius:17rpx; flex-shrink:0; background-color:#EEEEEE; }.service-info { display:flex; min-width:0; min-height:184rpx; flex:1; box-sizing:border-box; padding:4rpx 0 4rpx 24rpx; flex-direction:column; justify-content:space-between; }.service-name { color:#292929; font-size:29rpx; line-height:40rpx; font-weight:700; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }.service-discount { align-self:flex-start; max-width:400rpx; padding:6rpx 9rpx; border-radius:6rpx; background-color:#FFF5E7; color:#FF7C2B; font-size:21rpx; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }.service-tags { display:flex; min-height:38rpx; flex-direction:row; align-items:center; }.service-tags text { margin-right:9rpx; padding:5rpx 9rpx; border-radius:5rpx; font-size:21rpx; line-height:28rpx; white-space:nowrap; }.coupon-tag { background-color:#FFF0EC; color:#FF5D37; }.service-tag { background-color:#FFF5E7; color:#FF7C2B; }.address-row { display:flex; min-width:0; box-sizing:border-box; padding:7rpx 10rpx; border-radius:7rpx; background-image:linear-gradient(90deg,rgba(239,240,242,.98),rgba(242,243,245,.76) 58%,rgba(255,255,255,0)); flex-direction:row; align-items:flex-start; }.address-row image { width:27rpx; height:27rpx; margin-right:8rpx; flex-shrink:0; }.address-row text { min-width:0; flex:1; color:#7E8490; font-size:21rpx; line-height:31rpx; max-height:62rpx; overflow:hidden; }.service-empty { display:flex; height:310rpx; justify-content:center; align-items:center; }.service-empty text { color:#A5A8AD; font-size:25rpx; }.error-state { flex-direction:column; }.error-state .retry-text { margin-top:22rpx; color:#F26A38; }.loading-row,.no-more { display:flex; height:90rpx; flex-direction:row; justify-content:center; align-items:center; }.loading-spinner { width:28rpx; height:28rpx; margin-right:13rpx; box-sizing:border-box; border:3rpx solid #FFD6C4; border-top-color:#FF6C2B; border-radius:50%; animation:shopping-loading 720ms linear infinite; }.loading-row text { color:#989DA5; font-size:22rpx; }.load-error text { color:#F26A38; }.no-more view { width:70rpx; height:1rpx; background-color:#E7E7E7; }.no-more text { margin:0 18rpx; color:#B1B3B7; font-size:21rpx; }.bottom-space { height:calc(30rpx + env(safe-area-inset-bottom)); }@keyframes shopping-loading { from { transform:rotate(0deg); } to { transform:rotate(360deg); } }
</style>
talent-card-admin/src/api/admin.ts
@@ -333,8 +333,13 @@
}
export function getServiceApplications(params: ServiceApplicationQuery = {}) {
  const requestParams: Record<string, unknown> = { ...params }
  if (params.talentKeyword && !requestParams.keyword) {
    requestParams.keyword = params.talentKeyword
  }
  delete requestParams.talentKeyword
  return http.get<PageResult<ServiceApplication>, PageResult<ServiceApplication>>('/api/admin/service-appointment-applications', {
    params,
    params: requestParams,
  })
}
talent-card-admin/src/style.css
@@ -2972,8 +2972,8 @@
.parking-plate-filter-form {
  display: grid;
  grid-template-columns: minmax(250px, 1.05fr) minmax(250px, 1.05fr) minmax(240px, 1fr) auto;
  gap: 14px 36px;
  grid-template-columns: repeat(3, minmax(180px, 360px)) minmax(74px, 1fr);
  gap: 14px 24px;
  align-items: start;
  padding-bottom: 18px;
  border-bottom: 1px solid #d9dee8;
@@ -2993,6 +2993,7 @@
  grid-row: 1 / span 2;
  grid-column: 4;
  display: grid;
  justify-content: end;
  gap: 10px;
  align-content: start;
  min-width: 74px;
@@ -3801,7 +3802,7 @@
.movie-ticket-create-row {
  display: grid;
  grid-template-columns: 58px minmax(180px, 1fr) 36px 180px 86px;
  grid-template-columns: 58px minmax(180px, 600px) 36px 180px 86px;
  gap: 12px;
  align-items: center;
  color: #111827;
@@ -3819,7 +3820,7 @@
.movie-ticket-benefit-row {
  display: grid;
  grid-template-columns: 86px 40px 56px 80px 20px 56px 90px;
  grid-template-columns: 86px 40px 56px 180px 14px 56px 108px;
  gap: 12px;
  align-items: center;
  color: #111827;
@@ -3830,9 +3831,14 @@
  font-weight: 700;
}
.movie-ticket-benefit-row .el-input,
.movie-ticket-benefit-row .el-input-number,
.movie-ticket-benefit-row .el-select {
  width: 100%;
  min-width: 0;
}
.movie-ticket-benefit-row > span {
  white-space: nowrap;
}
.movie-ticket-panel-header {
talent-card-admin/src/utils/cardLevels.ts
New file
@@ -0,0 +1,6 @@
export const TALENT_CARD_LEVELS: readonly string[] = [
  '卓越人才卡',
  '杰出人才卡',
  '菁英人才卡',
  '普通游客',
]
talent-card-admin/src/views/AppointmentManagementView.vue
@@ -3,6 +3,7 @@
import { Download, Refresh, Search } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import { exportServiceApplications, getServiceApplications, updateServiceApplicationStatus } from '../api/admin'
import { TALENT_CARD_LEVELS } from '../utils/cardLevels'
import { saveBlob } from '../utils/excel'
const loading = ref(false)
@@ -18,7 +19,7 @@
  { label: '配偶就业', value: 'SPOUSE_EMPLOYMENT' },
  { label: '学生托管', value: 'CHILD_ENTRUSTMENT' },
]
const cardLevels = ['卓越人才卡', '杰出人才卡', '菁英人才卡', '青年人才卡', 'EXCELLENT', 'OUTSTANDING', 'ELITE']
const cardLevels = TALENT_CARD_LEVELS
const statusOptions = ['待处理', '已处理', '已驳回', '待审核', '审核通过', '审核不通过', '已取消']
function queryParams(size = pagination.size) {
@@ -41,7 +42,7 @@
        <el-form-item label="预约类型"><el-select v-model="filters.applicationType" clearable placeholder="全部"><el-option v-for="item in appointmentTypes" :key="item.value" :label="item.label" :value="item.value" /></el-select></el-form-item>
        <el-form-item label="关键词"><el-input v-model="filters.keyword" clearable placeholder="姓名、手机号、卡号、申请编号" /></el-form-item>
        <el-form-item label="服务机构"><el-input v-model="filters.serviceKeyword" clearable placeholder="医院、酒店或机构名称" /></el-form-item>
        <el-form-item label="卡片等级"><el-select v-model="filters.cardLevel" clearable placeholder="全部"><el-option v-for="level in cardLevels" :key="level" :label="level" :value="level" /></el-select></el-form-item>
        <el-form-item label="卡片等级"><el-select v-model="filters.cardLevel" clearable placeholder="全部"><el-option label="全部" value="" /><el-option v-for="level in cardLevels" :key="level" :label="level" :value="level" /></el-select></el-form-item>
        <el-form-item label="状态"><el-select v-model="filters.status" clearable placeholder="全部"><el-option v-for="status in statusOptions" :key="status" :label="status" :value="status" /></el-select></el-form-item>
        <el-form-item label="提交时间"><el-date-picker v-model="filters.submittedTime" type="daterange" value-format="YYYY-MM-DD" start-placeholder="开始日期" end-placeholder="结束日期" /></el-form-item>
        <div class="toolbar-actions"><el-button type="primary" :icon="Search" @click="search">查询</el-button><el-button :icon="Refresh" @click="resetFilters">重置</el-button><el-button v-permission="'admin.appointment-management.export'" :icon="Download" @click="download">导出</el-button></div>
talent-card-admin/src/views/BusinessManagementView.vue
@@ -16,6 +16,7 @@
  updateAdminStaff,
} from '../api/admin'
import type { AdminRole, AdminStaff } from '../types/admin'
import { TALENT_CARD_LEVELS } from '../utils/cardLevels'
import { downloadExcel } from '../utils/excel'
const route = useRoute()
@@ -182,10 +183,7 @@
        <el-form-item v-if="isUpgrade" label="卡片等级">
          <el-select v-model="filters.cardLevel" clearable placeholder="全部">
            <el-option label="全部" value="" />
            <el-option label="卓越人才卡" value="卓越人才卡" />
            <el-option label="杰出人才卡" value="杰出人才卡" />
            <el-option label="菁英人才卡" value="菁英人才卡" />
            <el-option label="普通游客" value="普通游客" />
            <el-option v-for="level in TALENT_CARD_LEVELS" :key="level" :label="level" :value="level" />
          </el-select>
        </el-form-item>
        <el-form-item label="状态">
talent-card-admin/src/views/ChildCardView.vue
@@ -5,6 +5,7 @@
import { ElMessage } from 'element-plus'
import { getChildCardApplications } from '../api/admin'
import type { ChildCardApplication } from '../types/admin'
import { TALENT_CARD_LEVELS } from '../utils/cardLevels'
import { downloadExcel } from '../utils/excel'
const route = useRoute()
@@ -134,10 +135,7 @@
        <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-option v-for="level in TALENT_CARD_LEVELS" :key="level" :label="level" :value="level" />
          </el-select>
        </el-form-item>
        <el-form-item label="状态">
talent-card-admin/src/views/FamilyCardView.vue
@@ -5,6 +5,7 @@
import { ElMessage } from 'element-plus'
import { getFamilyCardApplications } from '../api/admin'
import type { FamilyCardApplication } from '../types/admin'
import { TALENT_CARD_LEVELS } from '../utils/cardLevels'
import { downloadExcel } from '../utils/excel'
const props = defineProps<{ mode?: 'application' | 'management' }>()
@@ -42,7 +43,7 @@
]
const statusOptions = computed(() => isManagement.value ? managementStatusOptions : applicationStatusOptions)
const cardLevels = ['卓越人才卡', '杰出人才卡', '菁英人才卡', '普通游客']
const cardLevels = TALENT_CARD_LEVELS
function statusText(row: FamilyCardApplication) {
  if (isManagement.value) {
talent-card-admin/src/views/FuelServiceView.vue
@@ -5,13 +5,14 @@
import { ElMessage } from 'element-plus'
import { getServiceApplications } from '../api/admin'
import type { ServiceApplication } from '../types/admin'
import { TALENT_CARD_LEVELS } from '../utils/cardLevels'
import { downloadExcel } from '../utils/excel'
const loading = ref(false)
const router = useRouter()
const rows = ref<ServiceApplication[]>([])
const cardLevels = ['卓越人才卡', '杰出人才卡', '菁英人才卡', '青年人才卡']
const cardLevels = TALENT_CARD_LEVELS
const statusOptions = ['待处理', '已处理', '已驳回']
const submissionTypes = ['新增', '修改']
talent-card-admin/src/views/HotelReservationView.vue
@@ -5,13 +5,14 @@
import { ElMessage } from 'element-plus'
import { getServiceApplications } from '../api/admin'
import type { ServiceApplication } from '../types/admin'
import { TALENT_CARD_LEVELS } from '../utils/cardLevels'
import { downloadExcel } from '../utils/excel'
const loading = ref(false)
const router = useRouter()
const rows = ref<ServiceApplication[]>([])
const cardLevels = ['卓越人才卡', '杰出人才卡', '菁英人才卡', '青年人才卡']
const cardLevels = TALENT_CARD_LEVELS
const subsidyOptions = ['是', '否']
const statusOptions = ['待处理', '已处理', '已取消']
talent-card-admin/src/views/HotelVerificationView.vue
@@ -4,12 +4,13 @@
import { ElMessage } from 'element-plus'
import { getVerificationRecords } from '../api/admin'
import type { VerificationRecord } from '../types/admin'
import { TALENT_CARD_LEVELS } from '../utils/cardLevels'
import { downloadExcel } from '../utils/excel'
const loading = ref(false)
const rows = ref<VerificationRecord[]>([])
const cardLevels = ['钻石卡', '卓越人才卡', '精英人才卡', '青年人才卡']
const cardLevels = TALENT_CARD_LEVELS
const filters = reactive({
  talentKeyword: '',
talent-card-admin/src/views/MedicalApplicationView.vue
@@ -5,13 +5,14 @@
import { ElMessage } from 'element-plus'
import { getServiceApplications } from '../api/admin'
import type { ServiceApplication } from '../types/admin'
import { TALENT_CARD_LEVELS } from '../utils/cardLevels'
import { downloadExcel } from '../utils/excel'
const loading = ref(false)
const router = useRouter()
const rows = ref<ServiceApplication[]>([])
const cardLevels = ['卓越人才卡', '杰出人才卡', '菁英人才卡', '青年人才卡']
const cardLevels = TALENT_CARD_LEVELS
const statusOptions = ['待处理', '已处理', '已取消']
const filters = reactive({
talent-card-admin/src/views/MedicalReservationView.vue
@@ -5,13 +5,14 @@
import { ElMessage } from 'element-plus'
import { getServiceApplications } from '../api/admin'
import type { ServiceApplication } from '../types/admin'
import { TALENT_CARD_LEVELS } from '../utils/cardLevels'
import { downloadExcel } from '../utils/excel'
const loading = ref(false)
const router = useRouter()
const rows = ref<ServiceApplication[]>([])
const cardLevels = ['卓越人才卡', '杰出人才卡', '菁英人才卡', '青年人才卡']
const cardLevels = TALENT_CARD_LEVELS
const statusOptions = ['待处理', '已处理', '已取消']
const filters = reactive({
talent-card-admin/src/views/MerchantCreateView.vue
@@ -120,9 +120,10 @@
].includes(merchantTypeCode.value))
const isSpouseEmploymentMerchant = computed(() => merchantTypeCode.value === 'employment')
const isApprovalMerchant = computed(() => merchantTypeCode.value === 'approval')
const isBookMerchant = computed(() => merchantTypeCode.value === 'culture')
const isServiceMerchant = computed(() => isFoodMerchant.value || merchantTypeCode.value === 'scenic')
const supportsStoreConsumption = computed(() => [
  'hotel', 'scenic', 'food', 'dental', 'medical',
  'hotel', 'scenic', 'food', 'dental', 'medical', 'culture',
].includes(merchantTypeCode.value) || (isCarWashMerchant.value && form.miniProgramJumpType === 'normal'))
const merchantAppLoginOptionalTypes = [
  'housing', 'car', 'appliance', 'digital', 'culture', 'telecom', 'dental', 'fuel',
@@ -210,6 +211,13 @@
}
const tabs = computed(() => {
  if (isBookMerchant.value) {
    return [
      { label: '商家设置', value: 'settings' },
      { label: '商家权益', value: 'rights' },
      { label: '优惠券', value: 'coupons' },
    ]
  }
  if (usesSettingsAndRightsOnlyTabs.value) {
    return [
      { label: '商家设置', value: 'settings' },
@@ -561,7 +569,7 @@
  }],
  validTime: [{ required: true, message: '请选择优惠券有效期', trigger: 'change' }],
  productScope: [{
    validator: (_rule, value, callback) => value !== 'selected' || selectedCouponProductIds.value.length > 0
    validator: (_rule, value, callback) => isBookMerchant.value || value !== 'selected' || selectedCouponProductIds.value.length > 0
      ? callback()
      : callback(new Error('请至少选择一个可用商品')),
    trigger: 'change',
@@ -2046,6 +2054,11 @@
}
async function loadCouponProducts() {
  if (isBookMerchant.value) {
    couponProducts.value = []
    selectedCouponProductIds.value = []
    return
  }
  const merchantId = currentMerchantId()
  if (!merchantId) {
    couponProducts.value = []
@@ -2075,7 +2088,7 @@
  couponEditForm.couponAmount = ''
  couponEditForm.talentLevels = ['卓越人才卡', '杰出人才卡', '菁英人才卡']
  couponEditForm.validTime = []
  couponEditForm.productScope = 'selected'
  couponEditForm.productScope = isBookMerchant.value ? 'all' : 'selected'
  couponEditForm.storeConsume = true
  couponEditForm.governmentSubsidy = true
  couponEditForm.status = 1
@@ -2103,7 +2116,7 @@
  try { savedQuantities = typeof detail.talentLevelQuantities === 'string' ? JSON.parse(detail.talentLevelQuantities) : (detail.talentLevelQuantities || {}) } catch { savedQuantities = {} }
  couponEditForm.talentLevels.forEach((level) => { couponLevelQuantities[level] = Number(savedQuantities[level] || 1) })
  couponEditForm.validTime = detail.validStart && detail.validEnd ? [detail.validStart, detail.validEnd] : []
  couponEditForm.productScope = detail.productScope === 'ALL' ? 'all' : 'selected'
  couponEditForm.productScope = isBookMerchant.value || detail.productScope === 'ALL' ? 'all' : 'selected'
  couponEditForm.storeConsume = detail.storeConsume === true || detail.storeConsume === 1
  couponEditForm.governmentSubsidy = detail.governmentSubsidy === true || detail.governmentSubsidy === 1
  couponEditForm.status = detail.status ?? 1
@@ -2175,9 +2188,9 @@
    talentLevelQuantities: Object.fromEntries(couponEditForm.talentLevels.map((level) => [level, Number(couponLevelQuantities[level] || 1)])),
    validStart: couponEditForm.validTime?.[0],
    validEnd: couponEditForm.validTime?.[1],
    productScope: couponEditForm.productScope === 'all' ? 'ALL' : 'SELECTED',
    productIds: couponEditForm.productScope === 'all' ? [] : selectedCouponProductIds.value,
    storeConsume: couponEditForm.storeConsume,
    productScope: isBookMerchant.value || couponEditForm.productScope === 'all' ? 'ALL' : 'SELECTED',
    productIds: isBookMerchant.value || couponEditForm.productScope === 'all' ? [] : selectedCouponProductIds.value,
    storeConsume: isBookMerchant.value || couponEditForm.storeConsume,
    governmentSubsidy: couponEditForm.governmentSubsidy,
    status: couponEditForm.status,
  }
@@ -3429,7 +3442,12 @@
              />
            </el-form-item>
            <el-form-item class="coupon-edit-row coupon-product-scope-row" label="可用商品" prop="productScope" :required="couponEditForm.productScope === 'selected'">
            <div v-if="isBookMerchant" class="coupon-edit-row coupon-switch-row">
              <label>适用范围</label>
              <span>到店消费</span>
            </div>
            <el-form-item v-else class="coupon-edit-row coupon-product-scope-row" label="可用商品" prop="productScope" :required="couponEditForm.productScope === 'selected'">
              <el-radio-group v-model="couponEditForm.productScope">
                <el-radio label="all">不限</el-radio>
                <el-radio label="selected">选择</el-radio>
@@ -3437,7 +3455,7 @@
              </el-radio-group>
            </el-form-item>
            <div class="coupon-product-select-table">
            <div v-if="!isBookMerchant" class="coupon-product-select-table">
              <el-table :data="couponProducts" border class="coupon-edit-product-table" :show-header="true">
                <el-table-column width="98" align="center">
                  <template #default="{ row }">
@@ -3465,7 +3483,7 @@
              </el-table>
            </div>
            <div class="coupon-edit-row coupon-switch-row">
            <div v-if="!isBookMerchant" class="coupon-edit-row coupon-switch-row">
              <label>是否到店消费可用</label>
              <el-switch v-model="couponEditForm.storeConsume" />
            </div>
talent-card-admin/src/views/MovieTicketRecordView.vue
@@ -4,12 +4,13 @@
import { ElMessage } from 'element-plus'
import { getMovieTicketRecords } from '../api/admin'
import type { MovieTicketRecord } from '../types/admin'
import { TALENT_CARD_LEVELS } from '../utils/cardLevels'
import { downloadExcel } from '../utils/excel'
const loading = ref(false)
const rows = ref<MovieTicketRecord[]>([])
const cardLevels = ['卓越人才卡', '杰出人才卡', '菁英人才卡', '青年人才卡']
const cardLevels = TALENT_CARD_LEVELS
const filters = reactive({
  talentKeyword: '',
talent-card-admin/src/views/MovieTicketView.vue
@@ -15,6 +15,7 @@
  updateTicketPackageStatus,
  voidTicketPackageCode,
} from '../api/admin'
import { TALENT_CARD_LEVELS } from '../utils/cardLevels'
import { downloadExcel } from '../utils/excel'
type ViewMode = 'list' | 'claims' | 'form' | 'codes'
@@ -32,7 +33,8 @@
const activePackage = ref<Record<string, any> | null>(null)
const selectedPackage = ref<Record<string, any> | null>(null)
const cardLevels = ['卓越人才卡', '菁英人才卡', '杰出人才卡']
const cardLevels = TALENT_CARD_LEVELS
const ticketRuleLevels = TALENT_CARD_LEVELS.filter(level => level !== '普通游客')
const resetCycles = ['每天', '每周', '每月', '每年']
const packageStatuses = [{ label: '全部', value: '' }, { label: '正常', value: 1 }, { label: '禁用', value: 0 }]
const codeStatuses = [
@@ -81,7 +83,7 @@
  usageText: '',
  status: 1,
  sortNo: 0,
  levelRules: cardLevels.map(level => ({ talentLevel: level, enabled: 1, claimableCount: undefined as number | undefined, resetCycle: '每天' })),
  levelRules: ticketRuleLevels.map(level => ({ talentLevel: level, enabled: 1, claimableCount: undefined as number | undefined, resetCycle: '每天' })),
})
const codeForm = reactive({ batchNo: '', codesText: '', remark: '' })
@@ -247,7 +249,7 @@
    sortNo: Number(row?.sortNo || 0),
  })
  const existingRules = Array.isArray(row?.levelRules) ? row.levelRules : []
  form.levelRules = cardLevels.map(level => {
  form.levelRules = ticketRuleLevels.map(level => {
    const found = existingRules.find((item: Record<string, any>) => item.talentLevel === level)
    return {
      talentLevel: level,
talent-card-admin/src/views/OrderView.vue
@@ -5,6 +5,7 @@
import { ElMessage } from 'element-plus'
import { getOrders } from '../api/admin'
import type { Order } from '../types/admin'
import { TALENT_CARD_LEVELS } from '../utils/cardLevels'
import { downloadExcel } from '../utils/excel'
const loading = ref(false)
@@ -12,7 +13,7 @@
const orders = ref<Order[]>([])
const router = useRouter()
const cardLevels = ['卓越人才卡', '杰出人才卡', '菁英人才卡', '普通游客']
const cardLevels = TALENT_CARD_LEVELS
const filters = reactive({
  talentKeyword: '',
talent-card-admin/src/views/ParkingPlateView.vue
@@ -3,56 +3,69 @@
import { useRouter } from 'vue-router'
import { Download, Refresh, Search } from '@element-plus/icons-vue'
import { getParkingPlateSubmissions } from '../api/admin'
import { TALENT_CARD_LEVELS } from '../utils/cardLevels'
import { downloadExcel } from '../utils/excel'
const loading = ref(false)
const router = useRouter()
const rows = ref<Record<string, any>[]>([])
const filters = reactive({ keyword: '', serviceKeyword: '', plateNumber: '', cardLevel: '', status: '', submissionType: '', submittedTime: [] as string[] })
const filters = reactive({ keyword: '', serviceKeyword: '', cardLevel: '', status: '', submissionType: '', submittedTime: [] as string[] })
const pagination = reactive({ page: 1, size: 10, total: 0 })
const cardLevels = ['卓越人才卡', '杰出人才卡', '菁英人才卡', '青年人才卡', 'EXCELLENT', 'OUTSTANDING', 'ELITE']
const statusOptions = ['待处理', '已处理', '已驳回', '已提交商家', '已录入']
const cardLevels = TALENT_CARD_LEVELS
const statusOptions = ['待处理', '已处理', '已驳回']
const submissionTypes = ['新增', '修改']
function text(value: unknown) { return value === undefined || value === null || value === '' ? '-' : String(value) }
function formatDate(value: unknown) { return text(value).replace('T', ' ') }
function statusText(value: unknown) {
  const label = text(value)
  return label === '已拒绝' ? '已驳回' : label
  const label = text(value).toUpperCase()
  if (['SUBMITTED', 'PENDING', '待处理', '待审核'].includes(label)) return '待处理'
  if (['ACTIVE', 'PROCESSED', 'PASSED', 'APPROVED', '已处理', '处理完成', '通过', '已通过'].includes(label)) return '已处理'
  if (['REJECTED', 'REFUSED', 'DENIED', '已拒绝', '拒绝', '驳回', '已驳回'].includes(label)) return '已驳回'
  if (['CANCELLED', 'CANCELED', '已取消', '取消'].includes(label)) return '已取消'
  return text(value)
}
function queryParams(size = pagination.size) { return { page: pagination.page, size, keyword: filters.keyword || undefined, serviceKeyword: filters.serviceKeyword || undefined, plateNumber: filters.plateNumber || undefined, cardLevel: filters.cardLevel || undefined, status: filters.status || undefined, submissionType: filters.submissionType || undefined, submittedStart: filters.submittedTime?.[0], submittedEnd: filters.submittedTime?.[1] } }
function queryParams(size = pagination.size) { return { page: pagination.page, size, keyword: filters.keyword || undefined, serviceKeyword: filters.serviceKeyword || undefined, cardLevel: filters.cardLevel || undefined, status: filters.status || undefined, submissionType: filters.submissionType || undefined, submittedStart: filters.submittedTime?.[0], submittedEnd: filters.submittedTime?.[1] } }
async function loadData() { loading.value = true; try { const result = await getParkingPlateSubmissions(queryParams()); rows.value = result.records || []; pagination.total = result.total || 0 } finally { loading.value = false } }
function search() { pagination.page = 1; loadData() }
function resetFilters() { Object.assign(filters, { keyword: '', serviceKeyword: '', plateNumber: '', cardLevel: '', status: '', submissionType: '', submittedTime: [] }); search() }
function resetFilters() { Object.assign(filters, { keyword: '', serviceKeyword: '', cardLevel: '', status: '', submissionType: '', submittedTime: [] }); search() }
function viewDetail(row: Record<string, any>) { router.push(`/parking-plates/${row.id}`) }
async function download() { const result = await getParkingPlateSubmissions({ ...queryParams(1000), page: 1 }); downloadExcel('停车车牌管理', [{ title: '提交单号', value: row => text(row.submissionNo || row.applicationNo) }, { title: '持卡人', value: row => text(row.talentName) }, { title: '手机号', value: row => text(row.talentMobile) }, { title: '车牌号', value: row => text(row.plateNumber || row.plateNo) }, { title: '停车场', value: row => text(row.parkingLotName || row.parkingLot || row.serviceName) }, { title: '提交类型', value: row => text(row.submissionType) }, { title: '提交时间', value: row => formatDate(row.submittedAt || row.createdAt) }, { title: '状态', value: row => statusText(row.statusName || row.status) }], result.records || []) }
async function download() { const result = await getParkingPlateSubmissions({ ...queryParams(1000), page: 1 }); downloadExcel('停车车牌管理', [{ title: '提交单号', value: row => text(row.registrationNo || row.submissionNo || row.applicationNo) }, { title: '持卡人名称', value: row => text(row.talentName) }, { title: '持卡人电话', value: row => text(row.talentMobile) }, { title: '人才等级', value: row => text(row.cardLevel || row.talentLevel) }, { title: '车牌号', value: row => text(row.plateNumber || row.plateNo) }, { title: '停车场', value: row => text(row.parkingLotName || row.parkingLot || row.serviceName) }, { title: '提交时间', value: row => formatDate(row.submittedAt || row.createdAt) }, { title: '提交类型', value: row => text(row.submissionType) }, { title: '状态', value: row => statusText(row.statusName || row.status) }], result.records || []) }
onMounted(loadData)
</script>
<template>
  <div class="page-stack prototype-page">
    <div class="toolbar"><div><h2>停车车牌管理</h2><p>查看人才提交的车牌号,支持筛选、处理和导出给商家录入。</p></div><el-button :icon="Refresh" :loading="loading" @click="loadData">刷新</el-button></div>
  <div class="page-stack prototype-page parking-plate-page">
    <section class="panel">
      <el-form class="filter-form" :model="filters" label-width="84px">
        <el-form-item label="关键词"><el-input v-model="filters.keyword" clearable placeholder="持卡人、手机号、卡号、提交单号" /></el-form-item>
        <el-form-item label="车牌号"><el-input v-model="filters.plateNumber" clearable placeholder="请输入车牌号" /></el-form-item>
        <el-form-item label="停车场"><el-input v-model="filters.serviceKeyword" clearable placeholder="商家或停车场" /></el-form-item>
        <el-form-item label="卡片等级"><el-select v-model="filters.cardLevel" clearable placeholder="全部"><el-option v-for="level in cardLevels" :key="level" :label="level" :value="level" /></el-select></el-form-item>
        <el-form-item label="提交类型"><el-select v-model="filters.submissionType" clearable placeholder="全部"><el-option v-for="item in submissionTypes" :key="item" :label="item" :value="item" /></el-select></el-form-item>
        <el-form-item label="状态"><el-select v-model="filters.status" clearable placeholder="全部"><el-option v-for="status in statusOptions" :key="status" :label="status" :value="status" /></el-select></el-form-item>
        <el-form-item label="提交时间"><el-date-picker v-model="filters.submittedTime" type="daterange" value-format="YYYY-MM-DD" start-placeholder="开始日期" end-placeholder="结束日期" /></el-form-item>
        <div class="toolbar-actions"><el-button type="primary" :icon="Search" @click="search">查询</el-button><el-button :icon="Refresh" @click="resetFilters">重置</el-button><el-button v-permission="'admin.parking-plates.export'" :icon="Download" @click="download">导出</el-button></div>
      <div class="panel-header">
        <h3>查询表格</h3>
      </div>
      <el-form class="parking-plate-filter-form" :model="filters" label-width="72px">
        <el-form-item label="持卡人"><el-input v-model="filters.keyword" clearable placeholder="持卡人姓名、手机号、卡号" /></el-form-item>
        <el-form-item label="停车场"><el-input v-model="filters.serviceKeyword" clearable placeholder="停车场名称" /></el-form-item>
        <el-form-item label="卡片等级"><el-select v-model="filters.cardLevel" clearable placeholder="全部"><el-option label="全部" value="" /><el-option v-for="level in cardLevels" :key="level" :label="level" :value="level" /></el-select></el-form-item>
        <el-form-item label="提交时间"><el-date-picker v-model="filters.submittedTime" type="daterange" value-format="YYYY-MM-DD" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" /></el-form-item>
        <el-form-item label="状态"><el-select v-model="filters.status" clearable placeholder="全部"><el-option label="全部" value="" /><el-option v-for="status in statusOptions" :key="status" :label="status" :value="status" /></el-select></el-form-item>
        <el-form-item label="提交类型"><el-select v-model="filters.submissionType" clearable placeholder="全部"><el-option label="全部" value="" /><el-option v-for="item in submissionTypes" :key="item" :label="item" :value="item" /></el-select></el-form-item>
        <div class="parking-plate-filter-actions"><el-button type="primary" :icon="Search" @click="search">查询</el-button><el-button :icon="Refresh" @click="resetFilters">重置</el-button></div>
      </el-form>
      <el-table v-loading="loading" :data="rows" border empty-text="暂无车牌提交数据">
        <el-table-column label="提交单号" min-width="130"><template #default="{ row }">{{ text(row.submissionNo || row.applicationNo) }}</template></el-table-column>
        <el-table-column prop="talentName" label="持卡人" min-width="110" />
        <el-table-column prop="talentMobile" label="手机号" min-width="120" />
        <el-table-column label="车牌号" min-width="120"><template #default="{ row }">{{ text(row.plateNumber || row.plateNo) }}</template></el-table-column>
        <el-table-column label="停车场" min-width="160" show-overflow-tooltip><template #default="{ row }">{{ text(row.parkingLotName || row.parkingLot || row.serviceName) }}</template></el-table-column>
        <el-table-column prop="submissionType" label="提交类型" min-width="100" />
        <el-table-column label="提交时间" min-width="170"><template #default="{ row }">{{ formatDate(row.submittedAt || row.createdAt) }}</template></el-table-column>
        <el-table-column label="状态" min-width="110"><template #default="{ row }">{{ statusText(row.statusName || row.status) }}</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>
      <div class="parking-plate-table-tools">
        <el-button v-permission="'admin.parking-plates.export'" :icon="Download" @click="download">下载</el-button>
      </div>
      <el-table v-loading="loading" :data="rows" border class="parking-plate-table" empty-text="暂无车牌提交数据">
        <el-table-column label="提交单号" min-width="116" align="center"><template #default="{ row }">{{ text(row.registrationNo || row.submissionNo || row.applicationNo) }}</template></el-table-column>
        <el-table-column prop="talentName" label="持卡人名称" min-width="106" align="center" />
        <el-table-column prop="talentMobile" label="持卡人电话" min-width="118" align="center" />
        <el-table-column label="人才等级" min-width="106" align="center"><template #default="{ row }">{{ text(row.cardLevel || row.talentLevel) }}</template></el-table-column>
        <el-table-column label="车牌号" min-width="104" align="center"><template #default="{ row }">{{ text(row.plateNumber || row.plateNo) }}</template></el-table-column>
        <el-table-column label="停车场" min-width="144" align="center" show-overflow-tooltip><template #default="{ row }">{{ text(row.parkingLotName || row.parkingLot || row.serviceName) }}</template></el-table-column>
        <el-table-column label="提交时间" min-width="134" align="center"><template #default="{ row }">{{ formatDate(row.submittedAt || row.createdAt) }}</template></el-table-column>
        <el-table-column prop="submissionType" label="提交类型" min-width="96" align="center" />
        <el-table-column label="状态" min-width="90" align="center"><template #default="{ row }">{{ statusText(row.statusName || row.status) }}</template></el-table-column>
        <el-table-column label="操作" width="86" fixed="right" align="center"><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" :total="pagination.total" @change="loadData" /></div>
      <div class="pagination-row"><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]" :pager-count="5" :total="pagination.total" @change="loadData" /></div>
    </section>
  </div>
</template>
talent-card-admin/src/views/RefundRecordView.vue
@@ -5,6 +5,7 @@
import { ElMessage } from 'element-plus'
import { getRefundRecords } from '../api/admin'
import type { RefundRecord } from '../types/admin'
import { TALENT_CARD_LEVELS } from '../utils/cardLevels'
import { downloadExcel } from '../utils/excel'
const activeTab = ref<'GROUP' | 'STORE'>('GROUP')
@@ -12,7 +13,7 @@
const loading = ref(false)
const rows = ref<RefundRecord[]>([])
const cardLevels = ['卓越人才卡', '杰出人才卡', '菁英人才卡', '普通游客']
const cardLevels = TALENT_CARD_LEVELS
const itemNameLabel = computed(() => (activeTab.value === 'GROUP' ? '团购名称' : '消费项目'))
const filters = reactive({
talent-card-admin/src/views/SpouseEmploymentView.vue
@@ -5,13 +5,14 @@
import { ElMessage } from 'element-plus'
import { getServiceApplications } from '../api/admin'
import type { ServiceApplication } from '../types/admin'
import { TALENT_CARD_LEVELS } from '../utils/cardLevels'
import { downloadExcel } from '../utils/excel'
const loading = ref(false)
const router = useRouter()
const rows = ref<ServiceApplication[]>([])
const cardLevels = ['卓越人才卡', '杰出人才卡', '菁英人才卡', '青年人才卡']
const cardLevels = TALENT_CARD_LEVELS
const statusOptions = ['待处理', '已处理', '已驳回']
const filters = reactive({
talent-card-admin/src/views/TalentListView.vue
@@ -5,6 +5,7 @@
import { ElMessage, ElMessageBox } from 'element-plus'
import { downloadTalentImportTemplate, exportTalents, getTalents, importTalents, updateTalentStatus } from '../api/admin'
import type { Talent, TalentImportResult } from '../types/admin'
import { TALENT_CARD_LEVELS } from '../utils/cardLevels'
const router = useRouter()
const loading = ref(false)
@@ -15,7 +16,7 @@
const importResult = ref<TalentImportResult>()
const talents = ref<Talent[]>([])
const levels = ['卓越人才卡', '杰出人才卡', '菁英人才卡', '普通游客']
const levels = TALENT_CARD_LEVELS
const filters = reactive({
  keyword: '',
talent-card-server/src/main/java/com/talentcard/server/business/portal/PortalService.java
@@ -525,6 +525,11 @@
            List<Map<String, Object>> rights = merchantRightsConfigs(row.get("merchantRightsConfig"));
            Map<String, Object> configuredRight = matchingMerchantRight(rights, cardLevel, sourceAreaType, true);
            Map<String, Object> levelRight = matchingMerchantRight(rights, cardLevel, sourceAreaType, false);
            Long merchantId = row.get("id") instanceof Number number ? number.longValue() : null;
            item.put("couponAvailable", merchantId != null && hasActiveMerchantCoupon(merchantId));
            item.put("storeConsumeAvailable", configuredRight != null
                    && booleanValue(configuredRight.get("storeConsume"))
                    && booleanValue(configuredRight.get("consumeEnabled")));
            if (configuredRight != null) {
                item.put("discountText", textOrDefault(configuredRight.get("tag"), textOrDefault(item.get("serviceLabel"), "")));
                item.put("rightsAvailable", true);
talent-card-server/src/main/java/com/talentcard/server/service/ServiceAppointmentAdminService.java
@@ -258,6 +258,10 @@
        result.put("appointmentTime", first(application.getExpectedTime(), "以专员通知为准"));
        result.put("applicantName", participantName(application));
        result.put("applicantMobile", sensitiveDataCrypto.decrypt(first(application.getApplicantMobile(), application.getSpouseMobile(), application.getTalentMobile())));
        if (ServiceAppointmentApplicationService.TYPE_SPOUSE.equals(application.getApplicationType())) {
            result.put("spouseName", first(application.getSpouseName(), application.getApplicantName()));
            result.put("spouseMobile", sensitiveDataCrypto.decrypt(first(application.getSpouseMobile(), application.getApplicantMobile())));
        }
        result.put("submittedAt", application.getSubmittedAt());
        result.put("handledAt", application.getHandledAt());
        result.put("handlerName", application.getHandlerName());
@@ -294,6 +298,13 @@
        spouse.put("inInstitution", first(application.getSpouseInInstitution()));
        spouse.put("workExperience", first(application.getSpouseWorkExperience()));
        result.put("spouse", spouse);
        result.put("spouseIdCardNo", spouse.get("idCardNo"));
        result.put("spousePoliticalStatus", spouse.get("politicalStatus"));
        result.put("spouseEducation", spouse.get("education"));
        result.put("spouseSchoolMajor", spouse.get("schoolMajor"));
        result.put("spouseWorkUnit", spouse.get("workUnit"));
        result.put("spouseInInstitution", spouse.get("inInstitution"));
        result.put("spouseWorkExperience", spouse.get("workExperience"));
        Map<String, Object> child = new LinkedHashMap<>();
        child.put("id", application.getChildId());