shinnosuke
2026-08-20 a9ee1bf668342e210dcf470f72a35594fe1e4c3d
又又更新
1个文件已添加
12个文件已修改
236 ■■■■■ 已修改文件
rencaika/pages/card-upgrade/card-upgrade.uvue 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rencaika/pages/medical-booking/medical-booking.uvue 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/layouts/AdminLayout.vue 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/router/index.ts 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/style.css 78 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/utils/permissions.ts 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/views/BusinessManagementView.vue 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/views/MerchantCreateView.vue 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/views/MerchantOnboardingDetailView.vue 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/views/MerchantView.vue 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/views/OrderView.vue 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/views/RoleFormView.vue 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/views/StaffManagementView.vue 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rencaika/pages/card-upgrade/card-upgrade.uvue
@@ -107,6 +107,7 @@
    const draftKey = (): string => 'card-upgrade-draft-' + talentId.value
    const newHonor = (id: number): any => ({ id: id, name: '', date: '', proof: '', proofFileId: '', uploadState: '', progress: 0, error: '', abort: null })
    const isApprovedApplication = (status: any): boolean => status == 'approved' || status == '已通过'
    const saveDraft = () => {
        if (talentId.value == '' || isSubmitted.value) return
        const honors: any[] = []
@@ -156,6 +157,11 @@
    const loadUpgrade = () => {
        pageLoading.value = true
        pageLoadFailed.value = false
        isSubmitted.value = false
        submittedStatus.value = ''
        rejectReason.value = ''
        honorForms.value = [newHonor(1)]
        nextFormId.value = 2
        getCardUpgrade(talentId.value, (data: any) => {
            pageLoading.value = false
            targetLevels.value = data != null && data.targetCardLevels instanceof Array ? data.targetCardLevels as string[] : []
@@ -170,9 +176,9 @@
                    submittedStatus.value = application.status as string
                    return
                }
                if (application.status == 'approved') {
                    honorForms.value = [newHonor(1)]
                    nextFormId.value = 2
                if (isApprovedApplication(application.status)) {
                    // 已通过的申请资料属于上一轮升级,不能作为下一次升级的草稿回显。
                    uni.removeStorageSync(draftKey())
                    return
                }
                applyReturnedHonors(honors)
rencaika/pages/medical-booking/medical-booking.uvue
@@ -20,7 +20,7 @@
                <view class="field-card"><input class="field-input" v-model="userMobile" type="number" maxlength="11" placeholder="请输入手机号" placeholder-class="field-placeholder" /></view>
                <text class="field-label">身份证号</text>
                <view class="field-card"><input class="field-input" v-model="userIdCard" type="idcard" maxlength="18" placeholder="请输入身份证号" placeholder-class="field-placeholder" /></view>
                <view class="field-card"><input class="field-input" :value="userIdCard" type="idcard" maxlength="18" placeholder="请输入身份证号" placeholder-class="field-placeholder" @input="handleIdCardInput" /></view>
                <text class="field-label">预约医院</text>
                <view class="field-card"><text class="field-text">{{hospitalName}}</text></view>
@@ -80,6 +80,10 @@
    const goBack = () => safeBack('/pages/premium-benefits/premium-benefits')
    const changeExpectDate = (event: any) => { expectDate.value = event.detail.value as string }
    const handleIdCardInput = (event: any) => {
        const raw = event != null && event.detail != null && event.detail.value != null ? event.detail.value as string : ''
        userIdCard.value = raw.toUpperCase().replace(/[^0-9A-Z]/g, '').slice(0, 18)
    }
    const submitBooking = () => {
        if (isSubmitting.value) return
        if (userName.value.trim() == '') {
@@ -90,7 +94,7 @@
            uni.showToast({ title: '请输入正确的 11 位手机号', icon: 'none' })
            return
        }
        if (!/^[0-9]{17}[0-9Xx]$/.test(userIdCard.value.trim())) {
        if (!/^[0-9]{17}[0-9X]$/.test(userIdCard.value.trim())) {
            uni.showToast({ title: '请输入正确的 18 位身份证号', icon: 'none' })
            return
        }
talent-card-admin/src/layouts/AdminLayout.vue
@@ -151,6 +151,9 @@
  if (route.path === '/merchants/new') {
    return '/merchants'
  }
  if (route.path === '/roles/new' || /^\/roles\/[^/]+(?:\/view)?$/.test(route.path)) {
    return '/roles'
  }
  if (/^\/products\/[^/]+$/.test(route.path)) {
    return '/products'
  }
@@ -237,6 +240,15 @@
  if (route.name === 'merchant-create') {
    return ['商家管理', '商家查询', route.query.mode === 'edit' ? '编辑' : '新建']
  }
  if (route.name === 'role-create') {
    return ['系统管理', '角色管理', '新增']
  }
  if (route.name === 'role-edit') {
    return ['系统管理', '角色管理', '编辑']
  }
  if (route.name === 'role-detail') {
    return ['系统管理', '角色管理', '查看']
  }
  if (route.name === 'product-audit-detail') {
    return ['商家管理', '团单审核', '详情']
  }
talent-card-admin/src/router/index.ts
@@ -330,6 +330,12 @@
          meta: { title: '新增角色' },
        },
        {
          path: 'roles/:id/view',
          name: 'role-detail',
          component: () => import('../views/RoleFormView.vue'),
          meta: { title: '查看角色' },
        },
        {
          path: 'roles/:id',
          name: 'role-edit',
          component: () => import('../views/RoleFormView.vue'),
talent-card-admin/src/style.css
@@ -603,7 +603,11 @@
}
.merchant-form-row.address-row {
  grid-template-columns: 58px minmax(0, 1fr) 72px;
  grid-template-columns: 68px minmax(0, 1fr) 88px;
}
.merchant-form-row.three-columns {
  grid-template-columns: 68px minmax(0, 1fr);
}
.merchant-form-row.address-row .el-input {
@@ -611,20 +615,50 @@
  min-width: 0;
}
.merchant-form-row.address-row .el-form-item__content {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 16px;
  margin-left: 0 !important;
}
.merchant-form-row.address-row .el-button,
.merchant-cover-row .el-button {
  width: 72px;
  width: 88px;
  height: 32px;
  padding: 0 12px;
  box-sizing: border-box;
}
.merchant-cover-row .el-button {
  margin-left: 0;
}
.merchant-cover-row .el-form-item__content {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 16px;
  margin-left: 0 !important;
}
.merchant-form-row.address-row .el-button {
  flex: 0 0 72px;
  flex: 0 0 auto;
  white-space: nowrap;
}
.merchant-cover-row {
  grid-template-columns: 58px 268px 92px;
  grid-template-columns: 68px 268px 92px;
  align-items: end;
  margin-top: 6px;
}
.merchant-cover-row .el-form-item__label {
  width: 68px !important;
  padding-right: 0;
  white-space: nowrap;
  line-height: 28px;
}
.merchant-cover-preview {
@@ -657,13 +691,13 @@
.merchant-hour-row {
  display: grid;
  grid-template-columns: 210px 20px 20px;
  grid-template-columns: 278px 20px 20px;
  gap: 10px;
  align-items: center;
}
.merchant-hour-row .el-date-editor {
  width: 210px;
.merchant-hour-row .merchant-time-range-picker {
  width: 278px;
}
.merchant-hour-action {
@@ -699,9 +733,35 @@
  margin-bottom: 18px;
}
.merchant-account-form .merchant-account-field {
  display: flex;
  align-items: center;
}
.merchant-account-form .merchant-account-field > .el-form-item__label {
  flex: 0 0 92px;
  width: 92px !important;
  padding-right: 12px;
  white-space: nowrap !important;
  line-height: 32px;
}
.merchant-account-form .merchant-account-field > .el-form-item__content {
  min-width: 0;
  margin-left: 0 !important;
}
.merchant-account-form .merchant-account-field.is-required:not(.is-no-asterisk) .el-form-item__label::before {
  content: "*";
  margin-right: 4px;
  color: var(--el-color-danger);
}
.merchant-account-form .el-form-item__label {
  width: 92px !important;
  color: #334155;
  font-size: 13px;
  white-space: nowrap;
}
.merchant-create-save {
@@ -1820,6 +1880,10 @@
  width: 100%;
}
.product-notice-field .merchant-time-range-picker {
  width: 100%;
}
.product-rights-list {
  display: grid;
  gap: 28px;
talent-card-admin/src/utils/permissions.ts
New file
@@ -0,0 +1,55 @@
import { useAppStore } from '../stores/app'
export type AdminPermissionAction = 'view' | 'create' | 'edit' | 'disable' | 'delete' | 'audit' | 'reset'
export function hasAdminPermission(permission: string, store = useAppStore()) {
  return store.user.permissionCodes?.includes('*') === true || store.user.permissionCodes?.includes(permission) === true
}
export function pagePermission(page: string, action: AdminPermissionAction = 'view') {
  return `admin.${page}.${action}`
}
// More specific routes must be checked before their list/detail parent routes.
export function requiredRoutePermission(path: string, query?: Record<string, unknown>) {
  if (path === '/dashboard') return ''
  if (path === '/roles/new') return pagePermission('roles', 'create')
  if (/^\/roles\/[^/]+\/view$/.test(path)) return pagePermission('roles', 'view')
  if (/^\/roles\/[^/]+$/.test(path)) return pagePermission('roles', 'edit')
  if (path === '/staff') return pagePermission('staff', 'view')
  if (path === '/talents/new') return pagePermission('talents', 'create')
  if (/^\/talents\/[^/]+$/.test(path)) return pagePermission('talents', 'view')
  if (path === '/merchants/new') return pagePermission('merchants', query?.mode === 'edit' ? 'edit' : 'create')
  if (/^\/merchant-onboardings\/[^/]+$/.test(path)) return pagePermission('merchant-settlement', 'view')
  if (/^\/products\/[^/]+$/.test(path)) return pagePermission('products', 'view')
  if (/^\/orders\/[^/]+$/.test(path)) return pagePermission('orders', 'view')
  if (/^\/upgrade-applications\/[^/]+$/.test(path)) return pagePermission('upgrade-applications', 'view')
  if (/^\/family-card-applications\/[^/]+$/.test(path)) return pagePermission('family-card-applications', 'view')
  if (/^\/child-cards\/[^/]+$/.test(path)) return pagePermission('child-cards', 'view')
  if (/^\/family-cards\/[^/]+$/.test(path)) return pagePermission('family-cards', 'view')
  if (/^\/medical-reservations\/[^/]+$/.test(path)) return pagePermission('medical-reservations', 'view')
  if (/^\/medical-applications\/[^/]+$/.test(path)) return pagePermission('medical-applications', 'view')
  if (/^\/parking-plates\/[^/]+$/.test(path)) return pagePermission('parking-plates', 'view')
  if (/^\/fuel-services\/[^/]+$/.test(path)) return pagePermission('fuel-services', 'view')
  if (/^\/spouse-employment\/[^/]+$/.test(path)) return pagePermission('spouse-employment', 'view')
  if (/^\/hotel-reservations\/[^/]+$/.test(path)) return pagePermission('hotel-reservations', 'view')
  if (path === '/talent-news') return pagePermission('news', 'create')
  if (/^\/talent-news-categories\/[^/]+$/.test(path)) return pagePermission('news', 'edit')
  if (path === '/talent-policies') return pagePermission('policies', 'create')
  if (/^\/talent-policy-categories\/[^/]+$/.test(path)) return pagePermission('policies', 'edit')
  if (path === '/talent-news-management') return pagePermission('news', 'view')
  if (path === '/talent-policy-management') return pagePermission('policies', 'view')
  const listPages: Record<string, string> = {
    '/talents': 'talents', '/family-card-applications': 'family-card-applications', '/upgrade-applications': 'upgrade-applications',
    '/family-cards': 'family-cards', '/child-cards': 'child-cards', '/merchants': 'merchants', '/products': 'products',
    '/orders': 'orders', '/refund-records': 'refunds', '/hotel-free-verifications': 'hotel-free-verifications',
    '/appointment-management': 'appointment-management', '/medical-reservations': 'medical-reservations', '/medical-applications': 'medical-applications',
    '/parking-plates': 'parking-plates', '/car-wash-benefits': 'car-wash-benefits', '/fuel-services': 'fuel-services',
    '/spouse-employment': 'spouse-employment', '/hotel-reservations': 'hotel-reservations', '/benefit-coupons': 'benefit-coupons',
    '/display-services': 'display-services', '/equity-images': 'equity-images', '/movie-tickets': 'movie-tickets',
    '/movie-ticket-records': 'movie-tickets', '/merchant-settlement': 'merchant-settlement', '/system-settings': 'settings',
    '/protocol-settings': 'protocol-settings', '/roles': 'roles', '/staff': 'staff',
  }
  return listPages[path] ? pagePermission(listPages[path]) : ''
}
talent-card-admin/src/views/BusinessManagementView.vue
@@ -102,6 +102,10 @@
    router.push(`/upgrade-applications/${row.id}`)
    return
  }
  if (isRole.value) {
    router.push(`/roles/${row.id}/view`)
    return
  }
  activeRow.value = row
  dialogVisible.value = true
}
@@ -178,9 +182,10 @@
        <el-form-item v-if="isUpgrade" label="卡片等级">
          <el-select v-model="filters.cardLevel" clearable placeholder="全部">
            <el-option label="全部" value="" />
            <el-option label="卓越卡" value="EXCELLENT" />
            <el-option label="杰出卡" value="OUTSTANDING" />
            <el-option label="菁英卡" value="ELITE" />
            <el-option label="卓越人才卡" value="卓越人才卡" />
            <el-option label="杰出人才卡" value="杰出人才卡" />
            <el-option label="菁英人才卡" value="菁英人才卡" />
            <el-option label="普通游客" value="普通游客" />
          </el-select>
        </el-form-item>
        <el-form-item label="状态">
talent-card-admin/src/views/MerchantCreateView.vue
@@ -357,9 +357,13 @@
  productName: [{ required: true, message: '请输入商品名称', trigger: 'blur' }],
  coverUrl: [{ required: true, message: '请上传商品封面图', trigger: 'change' }],
  salePrice: [{
    validator: (_rule, value, callback) => isHealthCheckMerchant.value || (Number.isFinite(Number(value)) && Number(value) > 0)
      ? callback()
      : callback(new Error('请输入大于 0 的商品金额')),
    validator: (_rule, value, callback) => {
      if (isHealthCheckMerchant.value) return callback()
      const amountText = String(value ?? '').trim()
      return /^\d+(?:\.\d{1,2})?$/.test(amountText) && Number(amountText) >= 0.01
        ? callback()
        : callback(new Error('商品金额必须为不小于0.01的正数,最多两位小数'))
    },
    trigger: 'blur',
  }],
  productImages: [{
@@ -2275,7 +2279,7 @@
                  <el-time-picker
                    v-model="item.value"
                    is-range
                    arrow-control
                    class="merchant-time-range-picker"
                    format="HH:mm"
                    value-format="HH:mm"
                    range-separator="至"
@@ -2328,13 +2332,13 @@
          <el-form-item v-if="supportsMerchantAppLogin" label="登录商户端" label-width="100px">
            <el-switch v-model="form.canLoginMerchantApp" :active-value="1" :inactive-value="0" />
          </el-form-item>
          <el-form-item label="登录账号" prop="loginAccount" :required="requiresMerchantOperator" :error="accountTaken ? '登录账号已存在,请更换' : ''">
          <el-form-item class="merchant-account-field" label="登录账号" prop="loginAccount" :required="requiresMerchantOperator" :error="accountTaken ? '登录账号已存在,请更换' : ''">
            <el-input v-model="accountForm.loginAccount" maxlength="32" :disabled="isEditMode && merchantHasLoginAccount" :placeholder="isEditMode && !merchantHasLoginAccount ? '请输入登录账号' : (isEditMode ? '已配置登录账号' : '仅支持英文和数字')" @input="normalizeLoginAccount" @blur="checkLoginAccountAvailable" />
          </el-form-item>
          <el-form-item label="登录密码" prop="loginPassword" :required="requiresMerchantOperator || (isEditMode && Boolean(accountForm.loginPassword))">
          <el-form-item class="merchant-account-field" label="登录密码" prop="loginPassword" :required="requiresMerchantOperator || (isEditMode && Boolean(accountForm.loginPassword))">
            <el-input v-model="accountForm.loginPassword" type="password" show-password :placeholder="isEditMode ? '留空则不修改,6至64位' : ''" />
          </el-form-item>
          <el-form-item label="确认密码" prop="confirmPassword" :required="requiresMerchantOperator || (isEditMode && Boolean(accountForm.loginPassword))">
          <el-form-item class="merchant-account-field" label="确认密码" prop="confirmPassword" :required="requiresMerchantOperator || (isEditMode && Boolean(accountForm.loginPassword))">
            <el-input v-model="accountForm.confirmPassword" type="password" show-password :placeholder="isEditMode ? '再次输入新密码' : ''" />
          </el-form-item>
        </el-form>
@@ -2858,17 +2862,6 @@
              </el-form-item>
            </div>
            <div class="product-info-right" v-if="isEditMode && (isHotelMerchant || isHealthCheckMerchant)">
              <div class="product-form-row">
                <label>创建时间</label>
                <span>{{ formatDisplayTime(productForm.createdAt) }}</span>
              </div>
              <div class="product-form-row">
                <label>修改时间</label>
                <span>{{ formatDisplayTime(productForm.updatedAt) }}</span>
              </div>
            </div>
          </div>
          <el-form-item v-if="!isHealthCheckMerchant" class="product-detail-row" label="商品详情" prop="detailText" required>
@@ -2930,6 +2923,7 @@
              <el-time-picker
                v-model="productAvailableTimeRange"
                is-range
                class="merchant-time-range-picker"
                start-placeholder="开始时间"
                end-placeholder="结束时间"
                range-separator="至"
talent-card-admin/src/views/MerchantOnboardingDetailView.vue
@@ -63,14 +63,13 @@
      <div v-if="!isIndividual(detail.subjectType)" class="detail-grid">
        <div class="detail-field"><span>统一社会信用代码</span><strong>{{ text(detail.creditCode) }}</strong></div>
        <div class="detail-field"><span>营业执照有效期类型</span><strong>{{ validityTypeLabel(detail.licenseValidityType) }}</strong></div>
        <div class="detail-field placeholder-field"></div>
        <div class="detail-field"><span>证照有效期起始日期</span><strong>{{ dateText(detail.licenseBeginDate) }}</strong></div>
        <div class="detail-field"><span>证照有效期结束日期</span><strong>{{ dateText(detail.licenseEndDate) }}</strong></div>
        <div class="detail-field"><span>注册地址(省、市、区)</span><strong>{{ text(detail.registeredRegion) }}</strong></div>
        <div class="detail-field"><span>详细注册地址</span><strong>{{ text(detail.registeredAddress) }}</strong></div>
        <div class="detail-field"><span>法人姓名</span><strong>{{ text(detail.subjectName) }}</strong></div>
        <div class="detail-field"><span>法人证件类型</span><strong>身份证</strong></div>
        <div class="detail-field"><span>法人证件号码</span><strong>{{ text(detail.subjectIdNoMasked) }}</strong></div>
        <div class="detail-field"><span>法人证件号码</span><strong>{{ text(detail.subjectIdNo || detail.subjectIdNoMasked) }}</strong></div>
        <div class="detail-field"><span>法人证件有效期类型</span><strong>{{ validityTypeLabel(detail.subjectCertValidityType) }}</strong></div>
        <div class="detail-field"><span>法人证件有效期开始日期</span><strong>{{ dateText(detail.subjectCertBeginDate) }}</strong></div>
        <div class="detail-field"><span>法人证件有效期截止日期</span><strong>{{ dateText(detail.subjectCertEndDate) }}</strong></div>
talent-card-admin/src/views/MerchantView.vue
@@ -265,7 +265,7 @@
          />
        </el-form-item>
        <el-form-item label="商家类型">
          <el-select v-model="filters.type" placeholder="全部" clearable>
          <el-select v-model="filters.type" placeholder="全部" clearable filterable>
            <el-option label="全部" value="" />
            <el-option v-for="type in merchantTypes" :key="type" :label="type" :value="type" />
          </el-select>
@@ -355,7 +355,7 @@
    </section>
    <el-dialog v-model="typeDialogVisible" title="选择商家类型" width="220px" class="merchant-type-dialog" align-center :close-on-click-modal="false">
      <el-select v-model="selectedCreateType" class="merchant-type-select">
      <el-select v-model="selectedCreateType" class="merchant-type-select" filterable placeholder="请输入商家类型">
        <el-option v-for="type in createMerchantTypes" :key="type" :label="type" :value="type" />
      </el-select>
      <el-button type="primary" class="next-step-button" @click="nextCreateStep">下一步</el-button>
talent-card-admin/src/views/OrderView.vue
@@ -114,7 +114,8 @@
  downloadExcel(activeTab.value === 'GROUP' ? '团购订单' : '到店消费订单', [
    { title: '订单号', value: row => row.orderNo }, { title: '持卡人', value: row => row.talentName },
    { title: '手机号', value: row => row.talentMobile }, { title: '人才等级', value: row => row.talentLevel },
    { title: '商家', value: row => row.merchantName }, { title: '商品/权益', value: row => row.productName || row.benefitName },
    { title: '商家', value: row => row.merchantName },
    ...(activeTab.value === 'GROUP' ? [{ title: '团购名称', value: (row: Order) => row.productName || row.benefitName }] : []),
    { title: '订单金额(元)', value: row => moneyText(row.orderAmount) }, { title: '补贴金额(元)', value: row => moneyText(row.subsidyAmount) },
    { title: '下单时间', value: row => formatDateTime(row.createdAt) }, { title: '支付时间', value: row => paymentTime(row) },
    { title: '订单状态', value: row => orderStatusText(row) },
@@ -213,7 +214,7 @@
        <el-table-column prop="talentLevel" label="人才等级" width="104" />
        <el-table-column prop="merchantName" label="商家名称" width="118" show-overflow-tooltip />
        <el-table-column label="商家类型" width="86"><template #default="{ row }">{{ merchantTypeText(row.merchantType) }}</template></el-table-column>
        <el-table-column :label="itemNameLabel" min-width="100" show-overflow-tooltip>
        <el-table-column v-if="isGroupOrder" :label="itemNameLabel" min-width="100" show-overflow-tooltip>
          <template #default="{ row }">{{ row.benefitName || row.productName || '-' }}</template>
        </el-table-column>
        <el-table-column :label="amountLabel" width="82" align="center">
talent-card-admin/src/views/RoleFormView.vue
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed, onMounted, reactive, ref } from 'vue'
import { computed, reactive, ref, watch } from 'vue'
import { ElMessage } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
import { useRoute, useRouter } from 'vue-router'
@@ -70,6 +70,7 @@
]
const route = useRoute(), router = useRouter(), saving = ref(false)
const id = computed(() => route.params.id ? String(route.params.id) : '')
const isViewMode = computed(() => route.name === 'role-detail')
const form = reactive({ roleName: '', remark: '', status: 1, permissionCodes: [] as string[] })
const formRef = ref<FormInstance>()
const rules: FormRules = { roleName: [{ required: true, message: '请输入角色名称', trigger: 'blur' }] }
@@ -93,8 +94,8 @@
const togglePage = (page: Page, enabled: boolean) => toggle(pageCodes(page), enabled)
const parse = (value: AdminRole['permissionCodes']) => { if (Array.isArray(value)) return value; try { const result = JSON.parse(value || '[]'); return Array.isArray(result) ? result : [] } catch { return [] } }
async function load() { if (!id.value) return; const role = (await getAdminRoles()).find((item) => String(item.id) === id.value); if (!role) { ElMessage.error('角色不存在'); router.replace('/roles'); return }; Object.assign(form, { roleName: role.roleName, remark: role.remark || '', status: role.status, permissionCodes: parse(role.permissionCodes) }) }
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)
async function save() { if (isViewMode.value) return; 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 } }
watch(() => route.fullPath, load, { immediate: true })
</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 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>
<template><div class="page-stack role-form"><section class="panel"><div class="panel-header"><h3>{{ isViewMode ? '查看角色' : 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" :readonly="isViewMode" placeholder="请输入" maxlength="64" /></el-form-item><el-form-item label="备注"><el-input v-model="form.remark" :readonly="isViewMode" 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 :disabled="isViewMode" :model-value="checked(moduleCodes(module))" :indeterminate="partial(moduleCodes(module))" @click.stop @change="toggleModule(module, Boolean($event))">{{ module.name }}</el-checkbox></span><span><el-checkbox :disabled="isViewMode" :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" :disabled="isViewMode" :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 v-if="!isViewMode" 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/StaffManagementView.vue
@@ -387,6 +387,7 @@
.filters {
  margin: 14px 20px 0;
  padding: 16px;
  border-bottom: 1px solid #d9dee8;
}
.filters :deep(.el-input),
.filters :deep(.el-select) {
@@ -395,7 +396,7 @@
.tools {
  display: flex;
  justify-content: flex-end;
  padding: 10px 20px 14px;
  padding: 18px 20px 14px;
}
.table {
  margin: 0 20px;