feat: complete phase 02 RBAC and data scope recovery

This commit is contained in:
2026-09-10 10:58:43 +08:00
parent b46b48eb33
commit 5b51d5b3f1
115 changed files with 3608 additions and 1135 deletions
+5 -5
View File
@@ -38,22 +38,22 @@
<el-icon><Document /></el-icon><template #title>审计日志</template>
</el-menu-item>
<!-- V1.1 新增菜单 -->
<el-menu-item v-if="hasPerm('banner.manage')" index="/banner">
<el-menu-item v-if="hasPerm('banner.view')" index="/banner">
<el-icon><Promotion /></el-icon><template #title>Banner管理</template>
</el-menu-item>
<el-menu-item v-if="hasPerm('content.manage')" index="/content">
<el-menu-item v-if="hasPerm('content.view')" index="/content">
<el-icon><FolderOpened /></el-icon><template #title>内容管理</template>
</el-menu-item>
<el-menu-item v-if="hasPerm('service_location.manage')" index="/service-station">
<el-menu-item v-if="hasPerm('service_location.view')" index="/service-station">
<el-icon><Location /></el-icon><template #title>善缘服务站</template>
</el-menu-item>
<el-menu-item v-if="hasPerm('service_appointment.manage')" index="/service-appointment">
<el-menu-item v-if="hasPerm('service_appointment.view')" index="/service-appointment">
<el-icon><Memo /></el-icon><template #title>服务预约</template>
</el-menu-item>
<el-menu-item v-if="hasPerm('service_program.manage')" index="/service-program">
<el-icon><Service /></el-icon><template #title>专项服务项目</template>
</el-menu-item>
<el-menu-item v-if="hasPerm('counseling.manage')" index="/counseling">
<el-menu-item v-if="hasPerm('counseling.service.view')" index="/counseling">
<el-icon><ChatDotRound /></el-icon><template #title>婚恋咨询</template>
</el-menu-item>
<el-menu-item v-if="hasPerm('referral.manage')" index="/referral">
@@ -69,8 +69,8 @@
</el-select>
</el-form-item>
<el-form-item label="排序"><el-input-number v-model="form.sort_order" :min="0" /></el-form-item>
<el-form-item label="展示开始"><el-date-picker v-model="form.startAt" type="datetime" /></el-form-item>
<el-form-item label="展示结束"><el-date-picker v-model="form.endAt" type="datetime" /></el-form-item>
<el-form-item label="展示开始"><el-date-picker v-model="form.start_at" type="datetime" /></el-form-item>
<el-form-item label="展示结束"><el-date-picker v-model="form.end_at" type="datetime" /></el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
@@ -78,7 +78,7 @@
</template>
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue'
import { ref, onMounted } from 'vue'
import { get, post, put, del } from '@/api/client'
import { ElMessage } from 'element-plus'
@@ -3,7 +3,7 @@
<el-card>
<template #header><div class="card-header"><span>公益婚恋咨询管理</span></div></template>
<el-tabs v-model="activeTab">
<el-tab-pane label="待分配队列" name="queue">
<el-tab-pane v-if="canAssign" label="待分配队列" name="queue">
<el-table :data="pendingQueue" stripe v-loading="loading">
<el-table-column prop="userId" label="用户ID" width="120" />
<el-table-column prop="serviceType" label="服务类型" width="150">
@@ -36,10 +36,10 @@
</el-table-column>
<el-table-column label="操作" width="200">
<template #default="{ row }">
<el-button v-if="row.status==='assigned'" size="small" @click="scheduleCounseling(row)">排期</el-button>
<el-button v-if="row.status==='scheduled'" size="small" type="success" @click="startCounseling(row)">开始</el-button>
<el-button v-if="row.status==='in_progress'" size="small" type="primary" @click="showFollowup(row)">跟进</el-button>
<el-button v-if="row.status==='in_progress'" size="small" type="warning" @click="completeCounseling(row)">完成</el-button>
<el-button v-if="canProcess && row.status==='assigned'" size="small" @click="scheduleCounseling(row)">排期</el-button>
<el-button v-if="canProcess && row.status==='scheduled'" size="small" type="success" @click="startCounseling(row)">开始</el-button>
<el-button v-if="canCreateNote && row.status==='in_progress'" size="small" type="primary" @click="showFollowup(row)">跟进</el-button>
<el-button v-if="canProcess && row.status==='in_progress'" size="small" type="warning" @click="completeCounseling(row)">完成</el-button>
</template>
</el-table-column>
</el-table>
@@ -78,11 +78,16 @@
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { ref, computed, onMounted } from 'vue'
import { get, post } from '@/api/client'
import { ElMessage } from 'element-plus'
import { useAuthStore } from '@/stores/auth'
const activeTab = ref('queue')
const authStore = useAuthStore()
const canAssign = computed(() => authStore.permissions.includes('counseling.service.assign'))
const canProcess = computed(() => authStore.permissions.includes('counseling.service.process'))
const canCreateNote = computed(() => authStore.permissions.includes('counseling.note.create'))
const activeTab = ref(canAssign.value ? 'queue' : 'assigned')
const loading = ref(false)
const pendingQueue = ref<any[]>([])
const assignedServices = ref<any[]>([])
@@ -116,38 +121,38 @@ function formatDateTime(d: string) { return d ? new Date(d).toLocaleString('zh-C
async function loadQueue() {
loading.value = true
try { const res: any = await get('/operator/counseling/services/queue'); pendingQueue.value = res?.data || [] } finally { loading.value = false }
try { const res: any = await get('/counseling/services/operator/queue'); pendingQueue.value = res?.data || [] } finally { loading.value = false }
}
async function loadAssigned() {
loading.value = true
try { const res: any = await get('/operator/counseling/services/operator/assigned'); assignedServices.value = res?.data || [] } finally { loading.value = false }
try { const res: any = await get('/counseling/services/operator/assigned'); assignedServices.value = res?.data || [] } finally { loading.value = false }
}
async function openAssign(row: any) { currentId.value = row.id; assignForm.value.counselorId = ''; assignDialogVisible.value = true }
async function doAssign() {
try { await post(`/operator/counseling/services/${currentId.value}/assign`, { counselorId: assignForm.value.counselorId }); ElMessage.success('分配成功'); assignDialogVisible.value = false; loadQueue() }
try { await post(`/counseling/services/operator/${currentId.value}/assign`, { counselorId: assignForm.value.counselorId }); ElMessage.success('分配成功'); assignDialogVisible.value = false; loadQueue() }
catch (e: any) { ElMessage.error(e?.response?.data?.message || '操作失败') }
}
async function scheduleCounseling(row: any) {
const now = new Date(); now.setHours(now.getHours() + 2)
const end = new Date(now); end.setHours(end.getHours() + 1)
try { await post(`/operator/counseling/services/${row.id}/schedule`, { startAt: now.toISOString(), endAt: end.toISOString() }); ElMessage.success('排期成功'); loadAssigned() }
try { await post(`/counseling/services/operator/${row.id}/schedule`, { startAt: now.toISOString(), endAt: end.toISOString() }); ElMessage.success('排期成功'); loadAssigned() }
catch (e: any) { ElMessage.error(e?.response?.data?.message || '操作失败') }
}
async function startCounseling(row: any) {
try { await post(`/operator/counseling/services/${row.id}/start`, {}); ElMessage.success('已开始'); loadAssigned() }
try { await post(`/counseling/services/operator/${row.id}/start`, {}); ElMessage.success('已开始'); loadAssigned() }
catch (e: any) { ElMessage.error(e?.response?.data?.message || '操作失败') }
}
function showFollowup(row: any) { currentId.value = row.id; followupForm.value = { type: 'phone_call', content: '' }; followupDialogVisible.value = true }
async function doFollowup() {
try { await post(`/operator/counseling/services/${currentId.value}/followup`, { followupType: followupForm.value.type, content: followupForm.value.content }); ElMessage.success('跟进记录已保存'); followupDialogVisible.value = false; loadAssigned() }
}
try { await post(`/counseling/services/operator/${currentId.value}/followup`, { followupType: followupForm.value.type, content: followupForm.value.content }); ElMessage.success('跟进记录已保存'); followupDialogVisible.value = false; loadAssigned() }
catch (e: any) { ElMessage.error(e?.response?.data?.message || '操作失败') }
}
async function completeCounseling(row: any) {
try { await post(, {}); ElMessage.success('已完成'); loadAssigned() }
try { await post(`/counseling/services/operator/${row.id}/complete`, {}); ElMessage.success('已完成'); loadAssigned() }
catch (e: any) { ElMessage.error(e?.response?.data?.message || '操作失败') }
}
onMounted(() => { loadQueue(); loadAssigned() })
onMounted(() => { if (canAssign.value) loadQueue(); loadAssigned() })
</script>
<style scoped>
@@ -1,7 +1,7 @@
<template>
<div class="service-program">
<el-card>
<template #header><div class="card-header"><span>专项公益服务项目</span><el-button type="primary" size="small" @click="openCreate">新建项目</el-button></div></template>
<template #header><div class="card-header"><span>专项公益服务项目</span><el-button type="primary" size="small" disabled>新建项目(待开放)</el-button></div></template>
<el-table :data="programs" stripe v-loading="loading">
<el-table-column prop="name" label="项目名称" />
<el-table-column prop="status" label="状态" width="100">
@@ -46,7 +46,7 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { get, put } from '@/api/client'
import { get, post, put } from '@/api/client'
import { ElMessage } from 'element-plus'
const loading = ref(false)
+5 -24
View File
@@ -3,26 +3,6 @@ import type { RouteRecordRaw } from 'vue-router'
import { ElMessage } from 'element-plus'
import { useAuthStore } from '@/stores/auth'
// 路由权限映射:path prefix → 需要的最小权限码
const ROUTE_PERMISSIONS: Record<string, string[]> = {
'/users/detail': ['user.sensitive.view'],
'/operators': ['permission.manage'],
'/organizations': ['organization.manage'],
'/analytics': ['analytics.view'],
'/audit': ['user.view'],
'/review': ['review.view'],
'/risk/reports': ['risk.report.view'],
'/appeals': ['risk.report.process'],
'/activities/interests': ['activity.review_registration'],
'/banner': ['banner.manage'],
'/content': ['content.manage'],
'/service-station': ['service_location.manage'],
'/service-appointment': ['service_appointment.manage'],
'/service-program': ['service_program.manage'],
'/counseling': ['counseling.manage'],
'/referral': ['referral.manage'],
}
function hasPermission(requiredPerms: string[]): boolean {
if (!requiredPerms.length) return true
const userPerms = useAuthStore().permissions || []
@@ -48,11 +28,12 @@ const routes: RouteRecordRaw[] = [
{ path: 'analytics', name: 'Analytics', component: () => import('@/modules/analytics/AnalyticsView.vue'), meta: { title: '数据统计', permissions: ['analytics.view'] } },
{ path: 'audit', name: 'AuditLog', component: () => import('@/modules/audit/AuditLogView.vue'), meta: { title: '操作日志', permissions: ['user.view'] } },
// V1.1 新增模块
{ path: 'banner', name: 'BannerManagement', component: () => import('@/modules/banner/BannerManagementView.vue'), meta: { title: 'Banner管理', permissions: ['banner.manage'] } },
{ path: 'content', name: 'ContentManagement', component: () => import('@/modules/content/ContentManagementView.vue'), meta: { title: '内容管理', permissions: ['content.manage'] } },
{ path: 'service-station', name: 'ServiceStation', component: () => import('@/modules/service-station/ServiceStationView.vue'), meta: { title: '善缘服务站', permissions: ['service_location.manage'] } },
{ path: 'service-appointment', name: 'ServiceAppointment', component: () => import('@/modules/service-appointment/ServiceAppointmentView.vue'), meta: { title: '服务预约', permissions: ['service_appointment.manage'] } },
{ path: 'banner', name: 'BannerManagement', component: () => import('@/modules/banner/BannerManagementView.vue'), meta: { title: 'Banner管理', permissions: ['banner.view'] } },
{ path: 'content', name: 'ContentManagement', component: () => import('@/modules/content/ContentManagementView.vue'), meta: { title: '内容管理', permissions: ['content.view'] } },
{ path: 'service-station', name: 'ServiceStation', component: () => import('@/modules/service-station/ServiceStationView.vue'), meta: { title: '善缘服务站', permissions: ['service_location.view'] } },
{ path: 'service-appointment', name: 'ServiceAppointment', component: () => import('@/modules/service-appointment/ServiceAppointmentView.vue'), meta: { title: '服务预约', permissions: ['service_appointment.view'] } },
{ path: 'service-program', name: 'ServiceProgram', component: () => import('@/modules/service-program/ServiceProgramView.vue'), meta: { title: '专项服务项目', permissions: ['service_program.manage'] } },
{ path: 'counseling', name: 'Counseling', component: () => import('@/modules/counseling/CounselingView.vue'), meta: { title: '婚恋咨询', permissions: ['counseling.service.view'] } },
{ path: 'referral', name: 'ReferralManagement', component: () => import('@/modules/referral/ReferralManagementView.vue'), meta: { title: '转介管理', permissions: ['referral.manage'] } },
],
},
+3 -3
View File
@@ -22,15 +22,15 @@ export const useAuthStore = defineStore('auth', () => {
async function login(username: string, password: string) {
const res: any = await post('/auth/operator/login', { username, password })
if (res.success) {
token.value = res.data.token
token.value = res.data.accessToken
operator.value = {
operatorId: res.data.operatorId,
name: res.data.name,
username: res.data.username,
permissions: res.data.permissions || [],
organizationId: null,
organizationId: res.data.organizationId ?? null,
}
localStorage.setItem('accessToken', res.data.token)
localStorage.setItem('accessToken', res.data.accessToken)
localStorage.setItem('operator', JSON.stringify(operator.value))
return res.data
}