| | |
| | | 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 |
| | |
| | | ] |
| | | |
| | | const financeMenus: MenuItem[] = [ |
| | | { path: '/merchant-settlement', label: '商户结算审核' }, |
| | | { path: '/merchant-settlement', label: '提现管理' }, |
| | | ] |
| | | |
| | | const orderMenus: MenuItem[] = [ |
| | |
| | | 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)) })) |
| | |
| | | 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') |
| | |
| | | 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') |
| | |
| | | path: 'merchant-settlement', |
| | | name: 'merchant-settlement', |
| | | component: () => import('../views/MerchantSettlementView.vue'), |
| | | meta: { title: '商户结算审核' }, |
| | | meta: { title: '提现管理' }, |
| | | }, |
| | | { |
| | | path: 'merchant-onboardings/:id', |
| | |
| | | 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 |
| | | }) |
| | |
| | | |
| | | .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 { |
| | |
| | | .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; |
| | | } |
| | | |
| | |
| | | .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 { |
| | |
| | | } |
| | | |
| | | .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 { |
| | |
| | | <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" /> |
| | |
| | | </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> |
| | |
| | | </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> |
| | |
| | | </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"> |
| | |
| | | </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> |
| | |
| | | </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"> |
| | |
| | | </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 |
| | |
| | | </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 |
| | |
| | | </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 |
| | |
| | | </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 |
| | |
| | | </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 |
| | |
| | | </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'"> |
| | |
| | | </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 |
| | |
| | | </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> |
| | |
| | | |
| | | <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="暂无票包"> |
| | |
| | | |
| | | <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="暂无领取记录"> |
| | |
| | | </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> |
| | |
| | | </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="暂无订单数据"> |
| | |
| | | <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> |
| | |
| | | </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> |
| | |
| | | </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> |
| | |
| | | </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="暂无退款记录"> |
| | |
| | | { |
| | | 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: ['查看', '审核', '下载'] }, |
| | | ], |
| | | }, |
| | | { |
| | |
| | | { |
| | | 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: ['查看', '新增', '编辑', '审核', '批量导入', '下载'] }, |
| | | ], |
| | | }, |
| | | { |
| | |
| | | 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: ['查看', '新增', '编辑', '启用/停用', '删除', '重置密码', '下载'] }, |
| | | ], |
| | | }, |
| | | ] |
| | |
| | | '删除': '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) |
| | |
| | | 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> |
| | |
| | | </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 |
| | |
| | | ></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 |
| | |
| | | }}</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" |
| | |
| | | |
| | | <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="暂无持卡人数据"> |
| | |
| | | <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> |
| | |
| | | </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> |
| | |
| | | |
| | | 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); } |
| | | |
| | |
| | | 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 { |
| | |
| | | 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; |
| | | } |
| | | } |
| | |
| | | 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(); |