shinnosuke
2026-08-19 2b20145159c4c4eefc73fd449093e639812a64df
又又更新
31个文件已修改
298 ■■■■■ 已修改文件
talent-card-admin/src/layouts/AdminLayout.vue 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/main.ts 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/router/index.ts 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/style.css 60 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/views/AppointmentManagementView.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/views/BusinessManagementView.vue 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/views/ChildCardApplicationDetailView.vue 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/views/ChildCardView.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/views/FamilyCardApplicationDetailView.vue 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/views/FamilyCardView.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/views/FuelServiceView.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/views/HotelReservationView.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/views/HotelVerificationView.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/views/MedicalApplicationView.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/views/MedicalReservationView.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/views/MerchantCreateView.vue 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/views/MerchantView.vue 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/views/MovieTicketView.vue 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/views/OrderView.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/views/ParkingPlateView.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/views/ProductAuditDetailView.vue 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/views/ProductView.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/views/RefundRecordView.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/views/RoleFormView.vue 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/views/SpouseEmploymentView.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/views/StaffManagementView.vue 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/views/TalentListView.vue 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/views/UpgradeApplicationDetailView.vue 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/java/com/talentcard/server/business/admin/AdminManagementService.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/java/com/talentcard/server/business/admin/auth/AdminAuthInterceptor.java 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/java/com/talentcard/server/business/admin/auth/AdminAuthService.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/layouts/AdminLayout.vue
@@ -18,6 +18,7 @@
import { getAdminPendingNotices } from '../api/admin'
import type { AdminPendingNotice } from '../api/admin'
import { useAppStore } from '../stores/app'
import { hasAdminPermission } from '../utils/permissions'
interface MenuItem {
  path?: string
@@ -71,7 +72,7 @@
]
const financeMenus: MenuItem[] = [
  { path: '/merchant-settlement', label: '商户结算审核' },
  { path: '/merchant-settlement', label: '提现管理' },
]
const orderMenus: MenuItem[] = [
@@ -125,7 +126,7 @@
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))
  return !required || hasAdminPermission(required, appStore)
}
const visibleMenuGroups = computed(() => menuGroups
  .map((group) => ({ ...group, items: group.items.filter((item) => canAccess(item.path)) }))
talent-card-admin/src/main.ts
@@ -6,5 +6,16 @@
import './style.css'
import App from './App.vue'
import router from './router'
import { hasAdminPermission } from './utils/permissions'
createApp(App).use(createPinia()).use(router).use(ElementPlus, { locale: zhCn }).mount('#app')
const app = createApp(App)
app.use(createPinia()).use(router).use(ElementPlus, { locale: zhCn })
app.directive('permission', {
  mounted(el, binding) {
    if (!hasAdminPermission(String(binding.value))) el.style.display = 'none'
  },
  updated(el, binding) {
    el.style.display = hasAdminPermission(String(binding.value)) ? '' : 'none'
  },
})
app.mount('#app')
talent-card-admin/src/router/index.ts
@@ -1,6 +1,7 @@
import { createRouter, createWebHistory } from 'vue-router'
import { useAppStore } from '../stores/app'
import AdminLayout from '../layouts/AdminLayout.vue'
import { hasAdminPermission, requiredRoutePermission } from '../utils/permissions'
const familyCardView = () => import('../views/FamilyCardView.vue')
const businessManagementView = () => import('../views/BusinessManagementView.vue')
@@ -110,7 +111,7 @@
          path: 'merchant-settlement',
          name: 'merchant-settlement',
          component: () => import('../views/MerchantSettlementView.vue'),
          meta: { title: '商户结算审核' },
          meta: { title: '提现管理' },
        },
        {
          path: 'merchant-onboardings/:id',
@@ -348,7 +349,11 @@
router.beforeEach((to) => {
  const appStore = useAppStore()
  if (!to.meta.public && !appStore.token) {
    return '/login'
    return { path: '/login', query: { redirect: to.fullPath } }
  }
  if (!to.meta.public) {
    const required = requiredRoutePermission(to.path, to.query as unknown as Record<string, unknown>)
    if (required && !hasAdminPermission(required, appStore)) return '/dashboard'
  }
  return true
})
talent-card-admin/src/style.css
@@ -2032,15 +2032,39 @@
.coupon-edit-row {
  display: grid;
  grid-template-columns: 72px minmax(0, 1fr);
  gap: 14px;
  align-items: center;
  grid-template-columns: 82px minmax(0, 1fr);
  gap: 6px;
  align-items: start;
  margin-bottom: 18px;
}
.coupon-edit-row > label {
  line-height: 18px;
  height: 30px;
  box-sizing: border-box;
  padding: 0 0 0 12px;
  line-height: 30px;
  justify-content: flex-end;
  white-space: nowrap;
}
.merchant-coupon-edit-form .coupon-edit-row .el-form-item__label {
  width: 82px !important;
  max-width: none;
  height: 30px;
  box-sizing: border-box;
  padding: 0 !important;
  overflow: visible;
  flex: 0 0 82px;
  line-height: 30px;
  white-space: nowrap !important;
  word-break: keep-all;
  overflow-wrap: normal;
}
.coupon-edit-row .el-form-item__content {
  min-width: 0;
  min-height: 30px;
  margin-left: 0 !important;
}
.merchant-coupon-edit-form .el-input {
@@ -2050,16 +2074,23 @@
.coupon-amount-control {
  display: flex;
  align-items: center;
  gap: 8px;
  gap: 12px;
}
.coupon-amount-control .el-input-number {
  width: 200px;
.coupon-amount-control .el-input {
  width: 184px;
}
.merchant-coupon-edit-form .coupon-amount-control .el-input__wrapper {
  min-height: 30px;
  padding: 1px 11px;
  background: #f5f6f8;
  box-shadow: none;
}
.coupon-amount-control > span {
  color: #334155;
  line-height: 28px;
  line-height: 30px;
  white-space: nowrap;
}
@@ -2085,8 +2116,10 @@
.coupon-requirement-row .el-radio-group {
  display: flex;
  align-items: center;
  flex-wrap: nowrap;
  gap: 0;
  height: 28px;
  white-space: nowrap;
}
.coupon-requirement-row .el-radio {
@@ -2190,12 +2223,19 @@
}
.coupon-switch-row {
  grid-template-columns: 72px 44px;
  grid-template-columns: 130px 44px;
  gap: 6px;
  margin-bottom: 18px;
}
.coupon-switch-row > label {
  white-space: normal;
  width: 130px;
  box-sizing: border-box;
  padding: 0 0 0 20px;
  line-height: 30px;
  justify-content: flex-start;
  white-space: nowrap;
  word-break: keep-all;
}
.coupon-switch-row .el-switch {
talent-card-admin/src/views/AppointmentManagementView.vue
@@ -44,7 +44,7 @@
        <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.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 :icon="Download" @click="download">导出</el-button></div>
        <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>
      </el-form>
      <el-table v-loading="loading" :data="rows" border empty-text="暂无预约申请数据">
        <el-table-column prop="applicationNo" label="申请单号" min-width="130" />
talent-card-admin/src/views/BusinessManagementView.vue
@@ -202,18 +202,18 @@
        </div>
      </el-form>
      <div class="business-fill-tools">
        <el-button v-if="!isUpgrade" type="primary" :icon="Plus" @click="openCreate">新建</el-button>
        <el-button :icon="Download" @click="download">下载</el-button>
        <el-button v-if="!isUpgrade" v-permission="isRole ? 'admin.roles.create' : 'admin.staff.create'" type="primary" :icon="Plus" @click="openCreate">新建</el-button>
        <el-button v-permission="isUpgrade ? 'admin.upgrade-applications.export' : isRole ? 'admin.roles.export' : 'admin.staff.export'" :icon="Download" @click="download">下载</el-button>
      </div>
      <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 && !(isRole && isPlatformAdminRole(row))" link type="primary" :icon="Edit" @click="openEdit(row)">编辑</el-button>
            <el-button v-permission="isUpgrade ? 'admin.upgrade-applications.view' : isRole ? 'admin.roles.view' : 'admin.staff.view'" link type="primary" :icon="View" @click="view(row)">查看</el-button>
            <el-button v-if="!isUpgrade && !(isRole && isPlatformAdminRole(row))" v-permission="isRole ? 'admin.roles.edit' : 'admin.staff.edit'" 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>
              <el-button v-permission="'admin.roles.disable'" link type="primary" @click="changeRoleStatus(row)">{{ Number(row.status) === 1 ? '停用' : '启用' }}</el-button>
              <el-button v-permission="'admin.roles.delete'" link type="danger" @click="removeRole(row)">删除</el-button>
            </template>
          </template>
        </el-table-column>
talent-card-admin/src/views/ChildCardApplicationDetailView.vue
@@ -124,8 +124,8 @@
    </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>
      <el-button v-permission="'admin.child-cards.audit'" type="primary" @click="approveApplication">审核通过</el-button>
      <el-button v-permission="'admin.child-cards.audit'" type="primary" @click="rejectApplication">审核不通过</el-button>
    </div>
  </div>
</template>
talent-card-admin/src/views/ChildCardView.vue
@@ -163,7 +163,7 @@
      </el-form>
      <div class="family-card-table-tools">
        <el-button :icon="Download" @click="download">下载</el-button>
          <el-button v-permission="'admin.child-cards.export'" :icon="Download" @click="download">下载</el-button>
      </div>
      <el-table v-loading="loading" :data="rows" class="family-card-table" :empty-text="emptyText">
talent-card-admin/src/views/FamilyCardApplicationDetailView.vue
@@ -163,8 +163,8 @@
    </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>
      <el-button v-permission="'admin.family-card-applications.audit'" type="primary" @click="approveApplication">通过</el-button>
      <el-button v-permission="'admin.family-card-applications.audit'" type="primary" @click="rejectApplication">拒绝</el-button>
    </div>
  </div>
</template>
talent-card-admin/src/views/FamilyCardView.vue
@@ -212,7 +212,7 @@
      </el-form>
      <div class="family-card-table-tools">
        <el-button :icon="Download" @click="download">{{ isManagement ? '下载' : '导出' }}</el-button>
        <el-button v-permission="isManagement ? 'admin.family-cards.export' : 'admin.family-card-applications.export'" :icon="Download" @click="download">{{ isManagement ? '下载' : '导出' }}</el-button>
      </div>
      <el-table v-if="isManagement" v-loading="loading" :data="rows" class="family-card-table" :empty-text="emptyText">
talent-card-admin/src/views/FuelServiceView.vue
@@ -132,7 +132,7 @@
      </el-form>
      <div class="fuel-service-table-tools">
        <el-button :icon="Download" @click="download">下载</el-button>
        <el-button v-permission="'admin.fuel-services.export'" :icon="Download" @click="download">下载</el-button>
      </div>
      <el-table
talent-card-admin/src/views/HotelReservationView.vue
@@ -133,7 +133,7 @@
      </el-form>
      <div class="hotel-reservation-table-tools">
        <el-button :icon="Download" @click="download">下载</el-button>
        <el-button v-permission="'admin.hotel-reservations.export'" :icon="Download" @click="download">下载</el-button>
      </div>
      <el-table
talent-card-admin/src/views/HotelVerificationView.vue
@@ -126,7 +126,7 @@
      </el-form>
      <div class="order-table-tools">
        <el-button :icon="Download" @click="download">下载</el-button>
        <el-button v-permission="'admin.hotel-free-verifications.export'" :icon="Download" @click="download">下载</el-button>
      </div>
      <el-table
talent-card-admin/src/views/MedicalApplicationView.vue
@@ -123,7 +123,7 @@
      </el-form>
      <div class="medical-reservation-table-tools">
        <el-button :icon="Download" @click="download">下载</el-button>
        <el-button v-permission="'admin.medical-applications.export'" :icon="Download" @click="download">下载</el-button>
      </div>
      <el-table
talent-card-admin/src/views/MedicalReservationView.vue
@@ -127,7 +127,7 @@
      </el-form>
      <div class="medical-reservation-table-tools">
        <el-button :icon="Download" @click="download">下载</el-button>
        <el-button v-permission="'admin.medical-reservations.export'" :icon="Download" @click="download">下载</el-button>
      </div>
      <el-table
talent-card-admin/src/views/MerchantCreateView.vue
@@ -3092,7 +3092,10 @@
            </el-form-item>
            <el-form-item class="coupon-edit-row coupon-amount-row" label="优惠券金额" prop="couponAmount" required>
              <div class="coupon-amount-control"><el-input-number v-model="couponEditForm.couponAmount" :min="0.01" :precision="2" :step="1" controls-position="right" placeholder="请输入优惠券金额" /><span>元</span></div>
              <div class="coupon-amount-control">
                <el-input v-model="couponEditForm.couponAmount" inputmode="decimal" maxlength="8" placeholder="请输入优惠券金额" />
                <span>元</span>
              </div>
            </el-form-item>
            <el-form-item class="coupon-edit-row coupon-requirement-row" label="使用要求" prop="fullAmount" :required="couponEditForm.useRequirement === 'full'">
talent-card-admin/src/views/MerchantView.vue
@@ -291,7 +291,7 @@
      </el-form>
      <div class="prototype-table-tools">
        <el-button type="primary" :icon="Plus" @click="openCreate">新建</el-button>
          <el-button v-permission="'admin.merchants.create'" type="primary" :icon="Plus" @click="openCreate">新建</el-button>
      </div>
      <el-table
@@ -332,11 +332,11 @@
        </el-table-column>
        <el-table-column label="操作" width="174" fixed="right" align="center">
          <template #default="{ row }">
            <el-button link :type="row.status === 0 ? 'success' : 'danger'" @click="toggleStatus(row)">
            <el-button v-permission="'admin.merchants.disable'" link :type="row.status === 0 ? 'success' : 'danger'" @click="toggleStatus(row)">
              {{ row.status === 0 ? '启用' : '禁用' }}
            </el-button>
            <el-button link type="primary" :icon="Goods" @click="openProducts(row)">团购管理</el-button>
            <el-button link type="primary" :icon="Edit" @click="openEdit(row)">编辑</el-button>
            <el-button v-permission="'admin.merchants.edit'" link type="primary" :icon="Edit" @click="openEdit(row)">编辑</el-button>
          </template>
        </el-table-column>
      </el-table>
talent-card-admin/src/views/MovieTicketView.vue
@@ -437,7 +437,7 @@
        <div class="movie-ticket-package-tools">
          <el-button type="primary" :icon="Plus" @click="openCreate">新增</el-button>
          <el-button :icon="Download" @click="download">下载</el-button>
          <el-button v-permission="'admin.movie-tickets.export'" :icon="Download" @click="download">下载</el-button>
        </div>
        <el-table v-loading="loading" :data="rows" border class="movie-ticket-package-table" empty-text="暂无票包">
@@ -488,7 +488,7 @@
        <div class="movie-ticket-record-tools">
          <el-button type="primary" @click="openSupplement">补录发放</el-button>
          <el-button :icon="Download" @click="download">下载</el-button>
          <el-button v-permission="'admin.movie-tickets.export'" :icon="Download" @click="download">下载</el-button>
        </div>
        <el-table v-loading="loading" :data="rows" border class="movie-ticket-record-table" empty-text="暂无领取记录">
@@ -528,7 +528,7 @@
          </div>
        </el-form>
        <div class="movie-ticket-package-tools">
          <el-button type="primary" @click="openImport(selectedPackage || {})">导入码</el-button>
          <el-button v-permission="'admin.movie-tickets.import'" type="primary" @click="openImport(selectedPackage || {})">导入码</el-button>
          <el-button @click="openGenerate(selectedPackage || {})">批量生成</el-button>
          <span />
        </div>
talent-card-admin/src/views/OrderView.vue
@@ -203,7 +203,7 @@
      </el-form>
      <div class="order-table-tools">
        <el-button :icon="Download" @click="download">下载</el-button>
        <el-button v-permission="'admin.orders.export'" :icon="Download" @click="download">下载</el-button>
      </div>
      <el-table v-loading="loading" :data="orders" border class="order-list-table" empty-text="暂无订单数据">
talent-card-admin/src/views/ParkingPlateView.vue
@@ -34,7 +34,7 @@
        <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 :icon="Download" @click="download">导出</el-button></div>
        <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>
      </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>
talent-card-admin/src/views/ProductAuditDetailView.vue
@@ -313,8 +313,8 @@
    </section>
    <div v-if="isPendingAudit" class="audit-action-row">
      <el-button type="primary" :loading="auditing" @click="submitAudit(2)">审核通过</el-button>
      <el-button type="primary" :loading="auditing" @click="submitAudit(3)">审核不通过</el-button>
      <el-button v-permission="'admin.products.audit'" type="primary" :loading="auditing" @click="submitAudit(2)">审核通过</el-button>
      <el-button v-permission="'admin.products.audit'" type="primary" :loading="auditing" @click="submitAudit(3)">审核不通过</el-button>
    </div>
    <div v-else class="audit-result-row">
      <span>审核结果:{{ auditResultText }}</span>
talent-card-admin/src/views/ProductView.vue
@@ -210,7 +210,7 @@
        </el-table-column>
        <el-table-column label="操作" width="180" fixed="right" align="center">
          <template #default="{ row }">
            <el-button link type="primary" @click="viewDetail(row)">查看</el-button>
            <el-button v-permission="'admin.products.view'" link type="primary" @click="viewDetail(row)">查看</el-button>
          </template>
        </el-table-column>
      </el-table>
talent-card-admin/src/views/RefundRecordView.vue
@@ -174,7 +174,7 @@
      </el-form>
      <div class="order-table-tools">
        <el-button :icon="Download" @click="download">下载</el-button>
        <el-button v-permission="'admin.refunds.export'" :icon="Download" @click="download">下载</el-button>
      </div>
      <el-table v-loading="loading" :data="rows" border class="order-list-table refund-record-table" empty-text="暂无退款记录">
talent-card-admin/src/views/RoleFormView.vue
@@ -13,11 +13,11 @@
  {
    name: '持卡管理',
    pages: [
      { code: 'talents', name: '持卡人管理', actions: ['查看', '新增', '编辑', '启用/停用'] },
      { code: 'family-card-applications', name: '家属卡申请', actions: ['查看', '审核'] },
      { code: 'upgrade-applications', name: '升级申请', actions: ['查看', '审核'] },
      { code: 'family-cards', name: '家属卡管理', actions: ['查看', '启用/停用'] },
      { code: 'child-cards', name: '子女管理', actions: ['查看', '审核'] },
      { code: 'talents', name: '持卡人管理', actions: ['查看', '新增', '编辑', '启用/停用', '批量导入', '下载'] },
      { code: 'family-card-applications', name: '家属卡申请', actions: ['查看', '审核', '下载'] },
      { code: 'upgrade-applications', name: '升级申请', actions: ['查看', '审核', '下载'] },
      { code: 'family-cards', name: '家属卡管理', actions: ['查看', '启用/停用', '下载'] },
      { code: 'child-cards', name: '子女管理', actions: ['查看', '审核', '下载'] },
    ],
  },
  {
@@ -30,32 +30,32 @@
  {
    name: '订单管理',
    pages: [
      { code: 'orders', name: '订单列表', actions: ['查看'] },
      { code: 'refunds', name: '退款记录', actions: ['查看'] },
      { code: 'hotel-free-verifications', name: '酒店免费核销记录', actions: ['查看'] },
      { code: 'orders', name: '订单列表', actions: ['查看', '下载'] },
      { code: 'refunds', name: '退款记录', actions: ['查看', '下载'] },
      { code: 'hotel-free-verifications', name: '酒店免费核销记录', actions: ['查看', '下载'] },
    ],
  },
  {
    name: '预约管理',
    pages: [
      { code: 'appointment-management', name: '预约服务管理', actions: ['查看', '审核'] },
      { code: 'medical-reservations', name: '体检预约管理', actions: ['查看', '审核'] },
      { code: 'medical-applications', name: '就医申请管理', actions: ['查看', '审核'] },
      { code: 'parking-plates', name: '停车车牌管理', actions: ['查看', '审核'] },
      { code: 'appointment-management', name: '预约服务管理', actions: ['查看', '审核', '下载'] },
      { code: 'medical-reservations', name: '体检预约管理', actions: ['查看', '审核', '下载'] },
      { code: 'medical-applications', name: '就医申请管理', actions: ['查看', '审核', '下载'] },
      { code: 'parking-plates', name: '停车车牌管理', actions: ['查看', '审核', '下载'] },
      { code: 'car-wash-benefits', name: '洗车权益管理', actions: ['查看', '审核'] },
      { code: 'fuel-services', name: '加油服务管理', actions: ['查看', '审核'] },
      { code: 'spouse-employment', name: '配偶就业管理', actions: ['查看', '审核'] },
      { code: 'hotel-reservations', name: '酒店预约管理', actions: ['查看', '审核'] },
      { code: 'fuel-services', name: '加油服务管理', actions: ['查看', '审核', '下载'] },
      { code: 'spouse-employment', name: '配偶就业管理', actions: ['查看', '审核', '下载'] },
      { code: 'hotel-reservations', name: '酒店预约管理', actions: ['查看', '审核', '下载'] },
    ],
  },
  { name: '财务管理', pages: [{ code: 'merchant-settlement', name: '商户结算审核', actions: ['查看', '审核'] }] },
  { name: '财务管理', pages: [{ code: 'merchant-settlement', name: '提现管理', actions: ['查看', '审核'] }] },
  {
    name: '运营管理',
    pages: [
      { code: 'equity-images', name: '权益图片管理', actions: ['查看', '编辑'] },
      { code: 'news', name: '人才资讯管理', actions: ['查看', '新增', '编辑', '删除'] },
      { code: 'policies', name: '人才政策管理', actions: ['查看', '新增', '编辑', '删除'] },
      { code: 'movie-tickets', name: '电影票管理', actions: ['查看', '新增', '编辑', '审核'] },
      { code: 'movie-tickets', name: '电影票管理', actions: ['查看', '新增', '编辑', '审核', '批量导入', '下载'] },
    ],
  },
  {
@@ -63,8 +63,8 @@
    pages: [
      { code: 'settings', name: '平台设置', actions: ['查看', '编辑'] },
      { code: 'protocol-settings', name: '协议设置', actions: ['查看', '编辑'] },
      { code: 'roles', name: '角色管理', actions: ['查看', '新增', '编辑', '删除'] },
      { code: 'staff', name: '员工管理', actions: ['查看', '新增', '编辑', '启用/停用', '删除', '重置密码'] },
      { code: 'roles', name: '角色管理', actions: ['查看', '新增', '编辑', '启用/停用', '删除', '下载'] },
      { code: 'staff', name: '员工管理', actions: ['查看', '新增', '编辑', '启用/停用', '删除', '重置密码', '下载'] },
    ],
  },
]
@@ -81,6 +81,8 @@
  '删除': 'delete',
  '重置密码': 'reset',
  '审核': 'audit',
  '批量导入': 'import',
  '下载': 'export',
} as Record<string, string>)[action] || 'view'}`
const pageCodes = (page: Page) => page.actions.map((action) => code(page.code, action))
const moduleCodes = (module: Module) => module.pages.flatMap(pageCodes)
@@ -94,5 +96,5 @@
async function save() { const valid = await formRef.value?.validate().catch(() => false); if (!valid) return; saving.value = true; try { const payload = { roleName: form.roleName.trim(), roleCode: '', remark: form.remark.trim(), status: form.status, permissionCodes: form.permissionCodes }; if (id.value) await updateAdminRole(id.value, payload); else await createAdminRole(payload); ElMessage.success('保存成功'); router.replace('/roles') } finally { saving.value = false } }
onMounted(load)
</script>
<template><div class="page-stack role-form"><section class="panel"><div class="panel-header"><h3>{{ id ? '编辑角色' : '新增角色' }}</h3></div><el-form ref="formRef" class="base" :model="form" :rules="rules" label-position="top"><el-form-item label="角色名称" prop="roleName"><el-input v-model="form.roleName" placeholder="请输入" maxlength="64" /></el-form-item><el-form-item label="备注"><el-input v-model="form.remark" placeholder="请输入" maxlength="255" /></el-form-item></el-form><div class="permission-title">操作权限</div><div class="matrix"><div class="line header"><span>板块</span><span>页面</span><span>权限</span></div><template v-for="module in modules" :key="module.name"><div v-for="(page, index) in module.pages" :key="page.code" class="line"><span v-if="index === 0" class="module" :style="{ gridRow: `span ${module.pages.length}` }"><el-checkbox :model-value="checked(moduleCodes(module))" :indeterminate="partial(moduleCodes(module))" @click.stop @change="toggleModule(module, Boolean($event))">{{ module.name }}</el-checkbox></span><span><el-checkbox :model-value="checked(pageCodes(page))" :indeterminate="partial(pageCodes(page))" @click.stop @change="togglePage(page, Boolean($event))">{{ page.name }}</el-checkbox></span><span class="actions"><el-checkbox v-for="action in page.actions" :key="action" :model-value="form.permissionCodes.includes(code(page.code, action))" @click.stop @change="toggle([code(page.code, action)], Boolean($event))">{{ action }}</el-checkbox></span></div></template></div><div class="buttons"><el-button type="primary" :loading="saving" @click="save">保存</el-button><el-button @click="router.back()">返回</el-button></div></section></div></template>
<style scoped>.base{display:grid;grid-template-columns:360px minmax(400px,1fr);gap:28px;padding:16px 20px}.permission-title{padding:14px 20px;border-top:8px solid #f4f6f8;font-size:16px;font-weight:600}.matrix{margin:0 20px;border:1px solid #dfe4ea}.line{display:grid;grid-template-columns:150px 190px 1fr;min-height:44px;border-bottom:1px solid #e7ebf0}.line:last-child{border-bottom:0}.line>span{display:flex;align-items:center;padding:7px 18px;border-right:1px solid #e7ebf0}.line>span:last-child{border-right:0}.header{min-height:36px;background:#fafbfd;font-weight:600}.header span{justify-content:center}.actions{gap:26px;flex-wrap:wrap}.buttons{padding:20px;display:flex;gap:14px}.buttons .el-button{min-width:78px}@media(max-width:850px){.base{grid-template-columns:1fr}.line{grid-template-columns:120px 150px 1fr}.actions{gap:12px}}</style>
<template><div class="page-stack role-form"><section class="panel"><div class="panel-header"><h3>{{ id ? '编辑角色' : '新增角色' }}</h3></div><el-form ref="formRef" class="base" :model="form" :rules="rules" label-position="top"><el-form-item label="角色名称" prop="roleName"><el-input v-model="form.roleName" placeholder="请输入" maxlength="64" /></el-form-item><el-form-item label="备注"><el-input v-model="form.remark" placeholder="请输入" maxlength="255" /></el-form-item></el-form><div class="permission-title">操作权限</div><div class="matrix"><div class="line header"><span>板块</span><span>页面</span><span>权限</span></div><template v-for="module in modules" :key="module.name"><div class="module-group"><div v-for="(page, index) in module.pages" :key="page.code" class="line"><span v-if="index === 0" class="module" :style="{ gridRow: `span ${module.pages.length}` }"><el-checkbox :model-value="checked(moduleCodes(module))" :indeterminate="partial(moduleCodes(module))" @click.stop @change="toggleModule(module, Boolean($event))">{{ module.name }}</el-checkbox></span><span><el-checkbox :model-value="checked(pageCodes(page))" :indeterminate="partial(pageCodes(page))" @click.stop @change="togglePage(page, Boolean($event))">{{ page.name }}</el-checkbox></span><span class="actions"><el-checkbox v-for="action in page.actions" :key="action" :model-value="form.permissionCodes.includes(code(page.code, action))" @click.stop @change="toggle([code(page.code, action)], Boolean($event))">{{ action }}</el-checkbox></span></div></div></template></div><div class="buttons"><el-button type="primary" :loading="saving" @click="save">保存</el-button><el-button @click="router.back()">返回</el-button></div></section></div></template>
<style scoped>.base{display:grid;grid-template-columns:360px minmax(400px,1fr);gap:28px;padding:16px 20px}.permission-title{padding:14px 20px;border-top:8px solid #f4f6f8;font-size:16px;font-weight:600}.matrix{display:grid;grid-template-columns:180px 220px minmax(0,1fr);margin:0 20px;border:1px solid #dfe4ea}.module-group{display:contents}.line{display:contents}.line>span{display:flex;align-items:center;min-height:44px;padding:7px 18px;border-right:1px solid #e7ebf0;border-bottom:1px solid #e7ebf0}.line>span:last-child{border-right:0}.module{align-self:stretch}.header{display:contents}.header span{display:flex;align-items:center;justify-content:center;min-height:36px;background:#fafbfd;font-weight:600;border-right:1px solid #e7ebf0;border-bottom:1px solid #e7ebf0}.header span:last-child{border-right:0}.actions{display:flex;align-items:center;gap:26px;flex-wrap:nowrap;white-space:nowrap;overflow:auto}.buttons{padding:20px;display:flex;gap:14px}.buttons .el-button{min-width:78px}@media(max-width:850px){.base{grid-template-columns:1fr}.matrix{grid-template-columns:130px 170px minmax(360px,1fr)}.actions{gap:12px}}</style>
talent-card-admin/src/views/SpouseEmploymentView.vue
@@ -119,7 +119,7 @@
      </el-form>
      <div class="spouse-employment-table-tools">
        <el-button :icon="Download" @click="download">下载</el-button>
        <el-button v-permission="'admin.spouse-employment.export'" :icon="Download" @click="download">下载</el-button>
      </div>
      <el-table
talent-card-admin/src/views/StaffManagementView.vue
@@ -248,7 +248,7 @@
        ></el-form
      >
      <div class="tools">
        <el-button type="primary" :icon="Plus" @click="create">新增</el-button>
        <el-button v-permission="'admin.staff.create'" type="primary" :icon="Plus" @click="create">新增</el-button>
      </div>
      <el-table v-loading="loading" :data="pagedRows" stripe class="table"
        ><el-table-column
@@ -289,16 +289,16 @@
          }}</template></el-table-column
        ><el-table-column label="操作" width="225" fixed="right"
          ><template #default="{ row }"
            ><el-button v-if="!isDefaultAdmin(row)" link type="primary" @click="edit(row)">编辑</el-button
            ><el-button
            ><el-button v-if="!isDefaultAdmin(row)" v-permission="'admin.staff.edit'" link type="primary" @click="edit(row)">编辑</el-button
            ><el-button v-permission="'admin.staff.disable'"
              v-if="!isDefaultAdmin(row)"
              link
              type="primary"
              @click="changeStatus(row)"
              >{{ row.status === 1 ? "禁用" : "启用" }}</el-button
            ><el-button link type="primary" @click="password(row)"
            ><el-button v-permission="'admin.staff.reset'" link type="primary" @click="password(row)"
              >重置密码</el-button
            ><el-button
            ><el-button v-permission="'admin.staff.delete'"
              v-if="!isDefaultAdmin(row)"
              link
              type="danger"
talent-card-admin/src/views/TalentListView.vue
@@ -236,10 +236,10 @@
      <div class="prototype-table-tools">
        <div class="prototype-table-tools-left">
          <el-button type="primary" :icon="Plus" @click="openCreate">新建</el-button>
          <el-button :icon="Upload" @click="openImport">批量导入</el-button>
          <el-button v-permission="'admin.talents.create'" type="primary" :icon="Plus" @click="openCreate">新建</el-button>
          <el-button v-permission="'admin.talents.import'" :icon="Upload" @click="openImport">批量导入</el-button>
        </div>
        <el-button :icon="Download" :loading="exporting" @click="download">下载</el-button>
        <el-button v-permission="'admin.talents.export'" :icon="Download" :loading="exporting" @click="download">下载</el-button>
      </div>
      <el-table v-loading="loading" :data="talents" border class="prototype-table" empty-text="暂无持卡人数据">
@@ -267,7 +267,7 @@
        <el-table-column label="操作" width="150" fixed="right" align="center">
          <template #default="{ row }">
            <el-button link type="primary" @click="openDetail(row)">查看</el-button>
            <el-button link type="primary" @click="changeStatus(row)">{{ row.status === 1 ? '停用' : '启用' }}</el-button>
          <el-button v-permission="'admin.talents.disable'" link type="primary" @click="changeStatus(row)">{{ row.status === 1 ? '停用' : '启用' }}</el-button>
          </template>
        </el-table-column>
      </el-table>
talent-card-admin/src/views/UpgradeApplicationDetailView.vue
@@ -109,8 +109,8 @@
    </section>
    <section v-if="detail" class="upgrade-detail-actions">
      <template v-if="pending">
        <el-button type="primary" :loading="processing" @click="approve">处理</el-button>
        <el-button type="danger" :loading="processing" @click="reject">审核不通过</el-button>
        <el-button v-permission="'admin.upgrade-applications.audit'" type="primary" :loading="processing" @click="approve">处理</el-button>
        <el-button v-permission="'admin.upgrade-applications.audit'" type="danger" :loading="processing" @click="reject">审核不通过</el-button>
      </template>
      <span v-else>审核结果:{{ text(detail.status) }}<template v-if="detail.auditRemark">({{ detail.auditRemark }})</template></span>
    </section>
talent-card-server/src/main/java/com/talentcard/server/business/admin/AdminManagementService.java
@@ -24,7 +24,7 @@
    public List<Map<String, Object>> roles() { return rows("select id, role_name, role_code, remark, permission_codes, status, created_at from admin_role order by id"); }
    @Transactional public Map<String, Object> createRole(AdminRoleRequest request) { String code = roleCode(request.roleCode()); jdbc.update("insert into admin_role(role_name,role_code,remark,permission_codes,status) values (?,?,?,?,?)", request.roleName().trim(), code, request.remark(), permissions(request.permissionCodes()), request.status()); return one("select id, role_name, role_code, remark, permission_codes, status, created_at from admin_role where id=last_insert_id()"); }
    @Transactional public Map<String, Object> updateRole(Long id, AdminRoleRequest request) { if ("platform_admin".equals(existingRoleCode(id))) throw new BusinessException(ErrorCode.CONFLICT, "超级管理员角色不可编辑"); String code = request.roleCode() == null || request.roleCode().isBlank() ? existingRoleCode(id) : request.roleCode().trim(); if (jdbc.update("update admin_role set role_name=?,role_code=?,remark=?,permission_codes=?,status=? where id=?", request.roleName().trim(), code, request.remark(), permissions(request.permissionCodes()), request.status(), id) == 0) throw new BusinessException(ErrorCode.NOT_FOUND, "Role does not exist"); return one("select id, role_name, role_code, remark, permission_codes, status, created_at from admin_role where id=?", id); }
    @Transactional public Map<String, Object> updateRole(Long id, AdminRoleRequest request) { if ("platform_admin".equals(existingRoleCode(id))) throw new BusinessException(ErrorCode.CONFLICT, "超级管理员角色不可编辑"); String oldCode = existingRoleCode(id); String code = request.roleCode() == null || request.roleCode().isBlank() ? oldCode : request.roleCode().trim(); if (jdbc.update("update admin_role set role_name=?,role_code=?,remark=?,permission_codes=?,status=? where id=?", request.roleName().trim(), code, request.remark(), permissions(request.permissionCodes()), request.status(), id) == 0) throw new BusinessException(ErrorCode.NOT_FOUND, "Role does not exist"); jdbc.update("update admin_session s join admin_operator o on o.id=s.operator_id set s.revoked_at=now() where s.revoked_at is null and o.role_code in (?, ?)", oldCode, code); return one("select id, role_name, role_code, remark, permission_codes, status, created_at from admin_role where id=?", id); }
    @Transactional public Map<String, Object> updateRoleStatus(Long id, Byte status) { String code = existingRoleCode(id); if (status == 0 && "platform_admin".equals(code)) throw new BusinessException(ErrorCode.CONFLICT, "平台管理员角色不可禁用"); jdbc.update("update admin_role set status=? where id=?", status, id); return one("select id, role_name, role_code, remark, permission_codes, status, created_at from admin_role where id=?", id); }
    @Transactional public Map<String, Object> deleteRole(Long id) { String code = existingRoleCode(id); if ("platform_admin".equals(code)) throw new BusinessException(ErrorCode.CONFLICT, "平台管理员角色不可删除"); if (Boolean.TRUE.equals(jdbc.queryForObject("select exists(select 1 from admin_operator where role_code=? and deleted=0)", Boolean.class, code))) throw new BusinessException(ErrorCode.CONFLICT, "该角色仍有员工使用,不能删除"); jdbc.update("delete from admin_role where id=?", id); return Map.of("id", id, "deleted", true); }
talent-card-server/src/main/java/com/talentcard/server/business/admin/auth/AdminAuthInterceptor.java
@@ -5,6 +5,8 @@
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.http.HttpStatus;
@Component
public class AdminAuthInterceptor implements HandlerInterceptor {
@@ -22,6 +24,61 @@
        Long operatorId = service.authenticate(request.getHeader(HttpHeaders.AUTHORIZATION));
        request.setAttribute(OPERATOR_ID_ATTRIBUTE, operatorId);
        request.setAttribute(OPERATOR_NAME_ATTRIBUTE, service.operatorName(operatorId));
        String permission = permission(request.getMethod(), request.getRequestURI());
        if (!permission.isBlank() && !service.hasPermission(operatorId, permission)) {
            throw new ResponseStatusException(HttpStatus.FORBIDDEN, "当前账号没有该操作权限");
        }
        return true;
    }
    private String permission(String method, String uri) {
        if (uri.startsWith("/api/admin/management/roles")) {
            return managementPermission(method, uri, "roles");
        }
        if (uri.startsWith("/api/admin/management/staff")) {
            return managementPermission(method, uri, "staff");
        }
        String page = page(uri);
        if (page.isBlank()) return "";
        String action = "GET".equalsIgnoreCase(method) ? "view" : "edit";
        if ("POST".equalsIgnoreCase(method)) action = "create";
        if ("DELETE".equalsIgnoreCase(method)) action = "delete";
        if ("PATCH".equalsIgnoreCase(method)) {
            action = uri.endsWith("/audit") || uri.endsWith("/status") ? (uri.endsWith("/audit") ? "audit" : "disable") : "edit";
        }
        return "admin." + page + "." + action;
    }
    private String page(String uri) {
        if (uri.startsWith("/api/admin/business/merchants")) return "merchants";
        if (uri.startsWith("/api/admin/business/products")) return "products";
        if (uri.startsWith("/api/admin/business/news")) return "news";
        if (uri.startsWith("/api/admin/business/policies")) return "policies";
        if (uri.startsWith("/api/admin/business/coupons")) return "benefit-coupons";
        if (uri.startsWith("/api/admin/dependent-card-applies")) return "family-card-applications";
        if (uri.startsWith("/api/admin/cards/child-applications")) return "child-cards";
        if (uri.startsWith("/api/admin/service-appointment-applications")) return "appointment-management";
        if (uri.startsWith("/api/admin/medical-reservations")) return "medical-reservations";
        if (uri.startsWith("/api/admin/parking-plate-submissions")) return "parking-plates";
        if (uri.startsWith("/api/admin/car-wash-benefits") || uri.startsWith("/api/admin/car-wash-merchant-jump-configs")) return "car-wash-benefits";
        if (uri.startsWith("/api/admin/fuel-services")) return "fuel-services";
        if (uri.startsWith("/api/admin/spouse-employment")) return "spouse-employment";
        if (uri.startsWith("/api/admin/hotel-reservations")) return "hotel-reservations";
        if (uri.startsWith("/api/admin/merchant-settlement")) return "merchant-settlement";
        if (uri.startsWith("/api/admin/ticket-packages")) return "movie-tickets";
        if (uri.startsWith("/api/admin/display-services")) return "display-services";
        if (uri.startsWith("/api/admin/equity-images")) return "equity-images";
        return "";
    }
    private String managementPermission(String method, String uri, String page) {
        String action;
        if ("GET".equalsIgnoreCase(method)) action = "view";
        else if ("POST".equalsIgnoreCase(method)) action = "create";
        else if ("DELETE".equalsIgnoreCase(method)) action = "delete";
        else if ("PUT".equalsIgnoreCase(method)) action = "edit";
        else if ("PATCH".equalsIgnoreCase(method)) action = uri.endsWith("/status") ? "disable" : "reset";
        else return "";
        return "admin." + page + "." + action;
    }
}
talent-card-server/src/main/java/com/talentcard/server/business/admin/auth/AdminAuthService.java
@@ -109,6 +109,25 @@
        return displayName.isBlank() ? "管理员" : displayName;
    }
    @Transactional(readOnly = true)
    public boolean hasPermission(Long operatorId, String permission) {
        List<Map<String, Object>> result = rows("""
                select o.role_code, r.permission_codes
                from admin_operator o left join admin_role r on r.role_code = o.role_code
                where o.id = ? and o.deleted = 0
                limit 1
                """, operatorId);
        if (result.isEmpty()) return false;
        Map<String, Object> operator = result.get(0);
        if ("platform_admin".equals(String.valueOf(operator.get("roleCode")))) return true;
        String raw = String.valueOf(operator.getOrDefault("permissionCodes", ""));
        try {
            return JSON.parseArray(raw, String.class).contains(permission);
        } catch (Exception ignored) {
            return false;
        }
    }
    private Map<String, Object> issueSession(Map<String, Object> operator) {
        String accessToken = token();
        LocalDateTime now = LocalDateTime.now();