| | |
| | | |
| | | 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[] = [] |
| | |
| | | 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[] : [] |
| | |
| | | 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) |
| | |
| | | <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> |
| | |
| | | |
| | | 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() == '') { |
| | |
| | | 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 |
| | | } |
| | |
| | | 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' |
| | | } |
| | |
| | | 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 ['商家管理', '团单审核', '详情'] |
| | | } |
| | |
| | | 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'), |
| | |
| | | } |
| | | |
| | | .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 { |
| | |
| | | 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 { |
| | |
| | | |
| | | .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 { |
| | |
| | | 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 { |
| | |
| | | width: 100%; |
| | | } |
| | | |
| | | .product-notice-field .merchant-time-range-picker { |
| | | width: 100%; |
| | | } |
| | | |
| | | .product-rights-list { |
| | | display: grid; |
| | | gap: 28px; |
| New file |
| | |
| | | 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]) : '' |
| | | } |
| | |
| | | router.push(`/upgrade-applications/${row.id}`) |
| | | return |
| | | } |
| | | if (isRole.value) { |
| | | router.push(`/roles/${row.id}/view`) |
| | | return |
| | | } |
| | | activeRow.value = row |
| | | dialogVisible.value = true |
| | | } |
| | |
| | | <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="状态"> |
| | |
| | | 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: [{ |
| | |
| | | <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="至" |
| | |
| | | <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> |
| | |
| | | </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> |
| | |
| | | <el-time-picker |
| | | v-model="productAvailableTimeRange" |
| | | is-range |
| | | class="merchant-time-range-picker" |
| | | start-placeholder="开始时间" |
| | | end-placeholder="结束时间" |
| | | range-separator="至" |
| | |
| | | <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> |
| | |
| | | /> |
| | | </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> |
| | |
| | | </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> |
| | |
| | | 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) }, |
| | |
| | | <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"> |
| | |
| | | <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' |
| | |
| | | ] |
| | | 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' }] } |
| | |
| | | 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> |
| | |
| | | .filters { |
| | | margin: 14px 20px 0; |
| | | padding: 16px; |
| | | border-bottom: 1px solid #d9dee8; |
| | | } |
| | | .filters :deep(.el-input), |
| | | .filters :deep(.el-select) { |
| | |
| | | .tools { |
| | | display: flex; |
| | | justify-content: flex-end; |
| | | padding: 10px 20px 14px; |
| | | padding: 18px 20px 14px; |
| | | } |
| | | .table { |
| | | margin: 0 20px; |