feat: 整体废弃 leader 业务角色

leader 角色 (项目负责人) 业务上已不再使用, 整体下线。仅保留 sys_dept.leader 字段 (部门负责人, RuoYi 标准字段, 与角色无关)。

前端清理 (15 文件):
- 路由: 删除 /leader 块 + 4 子路由 (router L56-61)
- 菜单: 删除 AdminLayout 中 leader 菜单 (5 行)
- 视图: 删除整个 views/leader/ 目录 (Home/Projects/Meetings/Account.vue 共 ~32KB)
- Login.vue: 删默认 role='leader', 删 leader 角色选项, 删 autoRole 的 leader 分支, 删 roleHome 的 leader 项
- PortalShell.vue + 5 个 portal/* (Home/Publicity/ArticleView/PublicityDetail/SpecialPlanDetail): admin fallback '/leader/home' → '/admin/workbench', 删 leader 项, 同步更新 admin 入口 (/leader/home → /admin/workbench)
- 5 处 ROLE_LABEL 字典删 leader: '项目负责人' (AdminLayout/Account/Roles/Users/Workbench)
- Roles.vue 注释同步

后端清理 (7 文件):
- BizDashboardController: 删除 /leader dashboard 端点 (整个方法)
- BizPersonController: BUSINESS_MAIN_ROLES 删 leader
- BizExecutorController/BizProjectPlanController/ISysUserService/SysUserExtendVo/SysUser: 注释同步删 leader

测试:
- test_20rounds.py: 删 leader 路由断言 + view_map + role_home + role_view 共 5 处

未做 / 后续:
- DB: sys_user.role_type='leader' 现存账号是测试账号, 用户确认可忽略, 后续手动清理
- 文档: proto/html/leader.html 等原型文件保留 (历史)
- _self/*.md 笔记提及 leader 不动 (笔记)
This commit is contained in:
郭庆泰
2026-08-20 22:02:24 +08:00
parent af1ee0a5f7
commit 44a020da68
25 changed files with 29 additions and 857 deletions
@@ -42,15 +42,4 @@ public class BizDashboardController extends BaseController {
return success(map); return success(map);
} }
@GetMapping("/leader")
public AjaxResult leaderDashboard() {
Map<String, Object> map = new HashMap<>();
List<BizProject> projects = bizProjectService.selectList(new BizProject());
List<BizMeeting> meetings = bizMeetingService.selectList(new BizMeeting());
map.put("totalProjects", projects.size());
map.put("totalMeetings", meetings.size());
map.put("finishedProjects", projects.stream().filter(p -> "1".equals(p.getIsFinished())).count());
map.put("finishedMeetings", meetings.stream().filter(m -> "已结题".equals(m.getCurrentStage())).count());
return success(map);
}
} }
@@ -26,7 +26,7 @@ public class BizExecutorController extends BaseController
private JdbcTemplate jdbcTemplate; private JdbcTemplate jdbcTemplate;
/** /**
* 查询指定角色用户列表 (支持 executor / sponsor / manager / leader / doctor / admin) * 查询指定角色用户列表 (支持 executor / sponsor / manager / doctor / admin)
* @param roleType 角色类型 (默认 'executor') * @param roleType 角色类型 (默认 'executor')
* @param userName 用户名模糊搜索 (可选) * @param userName 用户名模糊搜索 (可选)
* @param userId 精确匹配 (可选) * @param userId 精确匹配 (可选)
@@ -37,7 +37,7 @@ public class BizPersonController extends BaseController
* (sys_user.parent_user_id → sys_user.user_id → biz_person.user_id 链路过滤) * (sys_user.parent_user_id → sys_user.user_id → biz_person.user_id 链路过滤)
*/ */
private static final java.util.Set<String> BUSINESS_MAIN_ROLES = java.util.Set.of( private static final java.util.Set<String> BUSINESS_MAIN_ROLES = java.util.Set.of(
"executor", "sponsor", "doctor", "manager", "leader" "executor", "sponsor", "doctor", "manager"
); );
@GetMapping("/list") @GetMapping("/list")
@@ -17,7 +17,7 @@ import com.ruoyi.business.service.IBizProjectPlanService;
* *
* 角色权限: * 角色权限:
* - doctor: 只看自己投的稿 (submitter_id = 当前用户); 新建/编辑强制 submitter_id 写自己, status 默认 '0' * - doctor: 只看自己投的稿 (submitter_id = 当前用户); 新建/编辑强制 submitter_id 写自己, status 默认 '0'
* - manager / leader / sponsor / admin: 全部可见, 不强制 submitter * - manager / sponsor / admin: 全部可见, 不强制 submitter
*/ */
@RestController @RestController
@RequestMapping("/business/projectPlan") @RequestMapping("/business/projectPlan")
@@ -86,7 +86,7 @@ public class SysUser extends BaseEntity
private Long[] postIds; private Long[] postIds;
/** 角色ID */ /** 角色ID */
private Long roleId; private Long roleId;
/** 业务角色 (admin / leader / manager / doctor / executor / sponsor), DB 列 sys_user.role_type */ /** 业务角色 (admin / manager / doctor / executor / sponsor), DB 列 sys_user.role_type */
private String roleType; private String roleType;
public SysUser() public SysUser()
{ {
@@ -13,7 +13,7 @@ public class SysUserExtendVo extends SysUser
{ {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
/** 业务角色 (admin / leader / manager / doctor / executor / sponsor) */ /** 业务角色 (admin / manager / doctor / executor / sponsor) */
private String roleType; private String roleType;
public String getRoleType() public String getRoleType()
@@ -223,7 +223,7 @@ public interface ISysUserService
public boolean isPhoneRegistered(String phone); public boolean isPhoneRegistered(String phone);
/** /**
* 更新 sys_user.role_type (业务角色: admin/leader/manager/doctor/executor/sponsor) * 更新 sys_user.role_type (业务角色: admin/manager/doctor/executor/sponsor)
* *
* @param userId 用户ID * @param userId 用户ID
* @param roleType 角色类型 * @param roleType 角色类型
+1 -1
View File
@@ -126,7 +126,7 @@ async function onUserCmd(cmd) {
if (cmd === 'logout') return goLogout() if (cmd === 'logout') return goLogout()
if (cmd === 'account') { if (cmd === 'account') {
const r = (userStore.user?.role || '') const r = (userStore.user?.role || '')
const map = { admin: '/admin/workbench', leader: '/leader/home', manager: '/manager/workbench', doctor: '/doctor/home', executor: '/executor/meetings', sponsor: '/sponsor/home' } const map = { admin: '/admin/workbench', manager: '/manager/workbench', doctor: '/doctor/home', executor: '/executor/meetings', sponsor: '/sponsor/home' }
router.push(map[r] || '/') router.push(map[r] || '/')
} }
} }
+1 -7
View File
@@ -66,7 +66,7 @@ const router = useRouter()
const store = useUserStore() const store = useUserStore()
const role = computed(() => route.meta?.role || store.role) const role = computed(() => route.meta?.role || store.role)
const ROLE_LABEL = { leader: '项目负责人', manager: '合规人员', admin: '后台管理员', doctor: '评审专家', executor: '执行人', sponsor: '支持方' } const ROLE_LABEL = { manager: '合规人员', admin: '后台管理员', doctor: '评审专家', executor: '执行人', sponsor: '支持方' }
const roleLabel = computed(() => ROLE_LABEL[role.value] || '工作台') const roleLabel = computed(() => ROLE_LABEL[role.value] || '工作台')
const MENU = { const MENU = {
@@ -90,12 +90,6 @@ const MENU = {
]}, ]},
{ path: '/admin/account', title: '账号信息', icon: User } { path: '/admin/account', title: '账号信息', icon: User }
], ],
leader: [
{ path: '/leader/home', title: '工作台', icon: House },
{ path: '/leader/projects', title: '项目列表', icon: Document },
{ path: '/leader/meetings', title: '会议列表', icon: Calendar },
{ path: '/leader/account', title: '账号信息', icon: User }
],
manager: [ manager: [
{ path: '/manager/workbench', title: '工作台', icon: House }, { path: '/manager/workbench', title: '工作台', icon: House },
{ path: '/manager/plans', title: '项目策划方案', icon: EditPen }, { path: '/manager/plans', title: '项目策划方案', icon: EditPen },
-8
View File
@@ -53,14 +53,6 @@ const routes = [
{ path: 'account', name: 'admin-account', component: () => import('@/views/admin/Account.vue'), meta: { title: '账号信息' } } { path: 'account', name: 'admin-account', component: () => import('@/views/admin/Account.vue'), meta: { title: '账号信息' } }
] ]
}, },
{ path: '/leader', component: AdminLayout, meta: { role: 'leader' }, children: [
{ path: '', redirect: { name: 'leader-home' } },
{ path: 'home', name: 'leader-home', component: () => import('@/views/leader/Home.vue'), meta: { title: '工作台' } },
{ path: 'projects', name: 'leader-projects', component: () => import('@/views/leader/Projects.vue'), meta: { title: '项目列表' } },
{ path: 'meetings', name: 'leader-meetings', component: () => import('@/views/leader/Meetings.vue'), meta: { title: '会议列表' } },
{ path: 'account', name: 'leader-account', component: () => import('@/views/leader/Account.vue'), meta: { title: '账号信息' } }
]
},
{ path: '/manager', component: AdminLayout, meta: { role: 'manager' }, children: [ { path: '/manager', component: AdminLayout, meta: { role: 'manager' }, children: [
{ path: '', redirect: { name: 'manager-workbench' } }, { path: '', redirect: { name: 'manager-workbench' } },
{ path: 'workbench', name: 'manager-workbench', component: () => import('@/views/manager/Workbench.vue'), meta: { title: '工作台' } }, { path: 'workbench', name: 'manager-workbench', component: () => import('@/views/manager/Workbench.vue'), meta: { title: '工作台' } },
+1 -1
View File
@@ -50,7 +50,7 @@ const store = useUserStore()
const tab = ref('profile') const tab = ref('profile')
const loading = ref(false) const loading = ref(false)
const profile = ref({}) const profile = ref({})
const ROLE_LABEL = { admin: '后台管理员', leader: '项目负责人', manager: '合规人员', doctor: '评审专家', executor: '执行人', sponsor: '支持方' } const ROLE_LABEL = { admin: '后台管理员', manager: '合规人员', doctor: '评审专家', executor: '执行人', sponsor: '支持方' }
const roleLabel = computed(() => ROLE_LABEL[store.role] || store.role || '后台管理员') const roleLabel = computed(() => ROLE_LABEL[store.role] || store.role || '后台管理员')
const form = reactive({ nickName: '', phonenumber: '', email: '' }) const form = reactive({ nickName: '', phonenumber: '', email: '' })
+2 -2
View File
@@ -46,10 +46,10 @@ import request from '@/utils/request'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
const ROLE_LABEL = { const ROLE_LABEL = {
admin: '后台管理员', leader: '项目负责人', manager: '合规人员', admin: '后台管理员', manager: '合规人员',
doctor: '评审专家', executor: '执行人', sponsor: '支持方' doctor: '评审专家', executor: '执行人', sponsor: '支持方'
} }
// 卡片展示的角色 (隐藏 admin / leader) // 卡片展示的角色 (隐藏 admin)
const VISIBLE_ROLES = ['manager', 'doctor', 'executor', 'sponsor'] const VISIBLE_ROLES = ['manager', 'doctor', 'executor', 'sponsor']
const roleStats = ref([]) const roleStats = ref([])
+1 -1
View File
@@ -107,7 +107,7 @@ import { useUserStore } from '@/store/user'
const userStore = useUserStore() const userStore = useUserStore()
const ROLE_LABEL = { const ROLE_LABEL = {
admin: '后台管理员', leader: '项目负责人', manager: '合规人员', admin: '后台管理员', manager: '合规人员',
doctor: '评审专家', executor: '执行人', sponsor: '支持方' doctor: '评审专家', executor: '执行人', sponsor: '支持方'
} }
const roleOptions = Object.entries(ROLE_LABEL).map(([value, label]) => ({ value, label })) const roleOptions = Object.entries(ROLE_LABEL).map(([value, label]) => ({ value, label }))
+1 -1
View File
@@ -52,7 +52,7 @@ const stats = ref({
const roleStats = ref([]) const roleStats = ref([])
const ROLE_LABEL = { const ROLE_LABEL = {
admin: '后台管理员', leader: '项目负责人', manager: '合规人员', admin: '后台管理员', manager: '合规人员',
doctor: '评审专家', executor: '执行人', sponsor: '支持方' doctor: '评审专家', executor: '执行人', sponsor: '支持方'
} }
+3 -6
View File
@@ -189,7 +189,7 @@ const router = useRouter()
const route = useRoute() const route = useRoute()
const userStore = useUserStore() const userStore = useUserStore()
const form = reactive({ username: '', password: '', code: '', uuid: '', role: 'leader' }) const form = reactive({ username: '', password: '', code: '', uuid: '', role: 'admin' })
const captchaImg = ref('') const captchaImg = ref('')
const captchaEnabled = ref(true) const captchaEnabled = ref(true)
const loading = ref(false) const loading = ref(false)
@@ -210,7 +210,6 @@ const registerTypes = [
const userTypes = [ const userTypes = [
{ value: 'admin', name: '系统管理员', desc: '系统配置、用户管理、权限控制' }, { value: 'admin', name: '系统管理员', desc: '系统配置、用户管理、权限控制' },
{ value: 'leader', name: '学会领导', desc: '查看全局数据、审批重要事项' },
{ value: 'manager', name: '项目经理', desc: '项目管理、方案审核、过程监督' }, { value: 'manager', name: '项目经理', desc: '项目管理、方案审核、过程监督' },
{ value: 'doctor', name: '评审专家', desc: '项目评审、评分反馈、查看任务' }, { value: 'doctor', name: '评审专家', desc: '项目评审、评分反馈、查看任务' },
{ value: 'executor', name: '执行单位', desc: '会议组织、人员安排、劳务结算' }, { value: 'executor', name: '执行单位', desc: '会议组织、人员安排、劳务结算' },
@@ -286,7 +285,7 @@ async function afterLogin(token, displayName, fallbackRole) {
}) })
} }
ElMessage.success(`欢迎,${displayName}${userTypes.find(u => u.value === role)?.name || role}`) ElMessage.success(`欢迎,${displayName}${userTypes.find(u => u.value === role)?.name || role}`)
// 没拿到角色就跳回登录页 (之前 fallback 到 /leader/home 会让子账号误入 leader) // 没拿到角色就跳回登录页
if (!role || !roleHome[role]) return router.replace({ name: 'login' }) if (!role || !roleHome[role]) return router.replace({ name: 'login' })
// 带 redirect 回跳 (401/守卫带过来的原页面), 否则跳角色首页 // 带 redirect 回跳 (401/守卫带过来的原页面), 否则跳角色首页
const redirect = route.query.redirect const redirect = route.query.redirect
@@ -356,18 +355,16 @@ async function onSmsLoginSubmit() {
function autoRole(username) { function autoRole(username) {
const u = (username || '').toLowerCase() const u = (username || '').toLowerCase()
if (u === 'admin' || u === 'ry') return 'admin' if (u === 'admin' || u === 'ry') return 'admin'
if (u.startsWith('leader')) return 'leader'
if (u.startsWith('manager')) return 'manager' if (u.startsWith('manager')) return 'manager'
if (u.startsWith('doctor')) return 'doctor' if (u.startsWith('doctor')) return 'doctor'
if (u.startsWith('executor')) return 'executor' if (u.startsWith('executor')) return 'executor'
if (u.startsWith('sponsor')) return 'sponsor' if (u.startsWith('sponsor')) return 'sponsor'
// 不匹配返回空串, 让 /getInfo 决定 (避免名字不规范时误进 leader) // 不匹配返回空串, 让 /getInfo 决定
return '' return ''
} }
const roleHome = { const roleHome = {
admin: '/admin/workbench', admin: '/admin/workbench',
leader: '/leader/home',
manager: '/manager/workbench', manager: '/manager/workbench',
doctor: '/doctor/home', doctor: '/doctor/home',
executor: '/executor/meetings', executor: '/executor/meetings',
-80
View File
@@ -1,80 +0,0 @@
<template>
<!--
原型: 账号信息.html
SIDEBAR: ['项目列表', '人员管理', '会议管理']
列头 (top 285-335, left): 姓名
说明: 项目主管的个人账号信息页, 含手机号/修改密码
-->
<div class="leader-account">
<p class="page-sub">查看与维护您的账号信息</p>
<div class="page-card">
<el-form :model="form" label-width="120px" style="max-width:560px">
<el-form-item label="账号">
<el-input :model-value="store.user?.userName" disabled />
</el-form-item>
<el-form-item label="姓名">
<el-input :model-value="store.user?.nickName || store.user?.userName" disabled />
</el-form-item>
<el-form-item label="角色">
<el-input :model-value="roleLabel" disabled />
</el-form-item>
<el-form-item label="手机号">
<el-input v-model="form.phone" placeholder="请输入手机号" />
</el-form-item>
<el-divider>修改密码</el-divider>
<el-form-item label="原密码">
<el-input v-model="form.oldPwd" type="password" show-password placeholder="请输入原密码" />
</el-form-item>
<el-form-item label="新密码">
<el-input v-model="form.newPwd" type="password" show-password placeholder="请输入新密码" />
</el-form-item>
<el-form-item label="确认密码">
<el-input v-model="form.confirmPwd" type="password" show-password placeholder="请再次输入新密码" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onSave">保存</el-button>
<el-button @click="onCancel">取消</el-button>
</el-form-item>
</el-form>
</div>
</div>
</template>
<script setup>
import { reactive, computed } from 'vue'
import { useUserStore } from '@/store/user'
import { ElMessage } from 'element-plus'
const store = useUserStore()
const roleLabel = computed(() => {
const r = store.user?.role
if (r === 'leader') return '项目主管'
if (r === 'sponsor') return '支持方'
if (r === 'expert') return '专家'
if (r === 'executor') return '执行方'
return r || '-'
})
const form = reactive({
phone: store.user?.phonenumber || '',
oldPwd: '', newPwd: '', confirmPwd: ''
})
function onSave() {
if (form.newPwd && form.newPwd !== form.confirmPwd) {
ElMessage.error('两次输入密码不一致'); return
}
ElMessage.success('已保存')
}
function onCancel() { form.oldPwd = ''; form.newPwd = ''; form.confirmPwd = '' }
</script>
<style scoped>
.leader-account { padding: 16px; }
.page-sub { font-size: 13px; color: #909399; margin-bottom: 16px; }
.page-card { background: #fff; border-radius: 4px; padding: 24px; border: 1px solid #ebeef5; }
</style>
-154
View File
@@ -1,154 +0,0 @@
<template>
<!--
原型: 首页.html
SIDEBAR: ['项目列表', '人员管理', '会议管理']
筛选区 (top 165-205, left): 项目列表, 项目总数量, 已结算数量, 已结题数量
列头 (top 285-335, left): 首页
-->
<div class="leader-home">
<!-- Row 1: 项目总数量 / 已结算数量 / 已结题数量 -->
<div class="stats-grid stats-grid-3">
<router-link class="stat-card" to="/leader/projects?isFinished=0">
<div class="stat-label">项目总数量</div>
<div class="stat-value">{{ stats.totalProjects }}</div>
<div class="stat-desc">查看全部项目</div>
</router-link>
<router-link class="stat-card" to="/leader/projects?isSettled=Y">
<div class="stat-label">已结算数量</div>
<div class="stat-value">{{ stats.settledProjects }}</div>
<div class="stat-desc">查看已结算项目</div>
</router-link>
<router-link class="stat-card" to="/leader/projects?isFinished=1">
<div class="stat-label">已结题数量</div>
<div class="stat-value">{{ stats.completedProjects }}</div>
<div class="stat-desc">查看已结题项目</div>
</router-link>
</div>
<!-- Row 2: 未执行会议 / 已执行会议 -->
<div class="stats-grid stats-grid-2">
<router-link class="stat-card" to="/leader/meetings?currentStage=未执行">
<div class="stat-label">未执行会议</div>
<div class="stat-value">{{ stats.pendingMeetings }}</div>
<div class="stat-desc">查看未执行会议</div>
</router-link>
<router-link class="stat-card" to="/leader/meetings?currentStage=已执行">
<div class="stat-label">已执行会议</div>
<div class="stat-value">{{ stats.executedMeetings }}</div>
<div class="stat-desc">查看已执行会议</div>
</router-link>
</div>
<!-- 消息通知 -->
<section class="section">
<h2 class="section-title">消息通知<a class="more" href="javascript:void(0)">更多 </a></h2>
<ul class="notice-list">
<li class="notice-item" v-for="n in notices" :key="n.title" :class="{ read: n.read }">
<div class="dot"></div>
<div class="body">
<div class="title">{{ n.title }}</div>
<div class="text">{{ n.text }}</div>
</div>
<span class="time">{{ n.time }}</span>
</li>
<li v-if="notices.length === 0" class="empty">暂无消息</li>
</ul>
</section>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { bizList, listMyMessages } from '@/api/public'
const stats = ref({
totalProjects: 0, settledProjects: 0, completedProjects: 0,
pendingMeetings: 0, executedMeetings: 0
})
const notices = ref([])
async function loadStats() {
try {
const ps = await bizList('project', { pageNum: 1, pageSize: 1 })
stats.value.totalProjects = ps.total || 0
const ms = await bizList('meeting', { pageNum: 1, pageSize: 1 })
stats.value.executedMeetings = ms.total || 0
} catch (e) {
console.warn('loadStats failed', e)
}
}
async function loadNotices() {
try {
const r = await listMyMessages({ limit: 50 }); const list = (r.data && r.data.rows) || r.rows || []
notices.value = list.map(n => ({
id: n.id,
title: n.title,
text: n.text,
time: n.time,
read: n.read
}))
} catch (e) {
console.warn('loadNotices failed', e)
}
}
onMounted(() => {
loadStats()
loadNotices()
})
</script>
<style scoped>
.leader-home { padding: 16px; }
.stats-grid { display: grid; gap: 16px; margin-bottom: 16px; }
.stats-grid-3 { grid-template-columns: repeat(3, 1fr); }
.stats-grid-2 { grid-template-columns: repeat(2, 1fr); }
.stat-card {
background: #fff;
border-radius: 4px;
padding: 24px;
border: 1px solid #f0f0f0;
cursor: pointer;
text-decoration: none;
color: inherit;
display: block;
}
.stat-label { font-size: 14px; color: #8c8c8c; margin-bottom: 16px; }
.stat-value { font-size: 32px; font-weight: 600; color: #1a1a1a; line-height: 1.2; margin-bottom: 8px; }
.stat-desc { font-size: 12px; color: #bfbfbf; }
.section { background: #fff; border-radius: 4px; padding: 20px 24px; border: 1px solid #f0f0f0; }
.section-title {
font-size: 15px; font-weight: 600; color: #1a1a1a;
margin-bottom: 16px; padding-left: 10px;
border-left: 3px solid var(--brand-primary);
display: flex; justify-content: space-between;
}
.more { font-size: 12px; color: var(--brand-primary); text-decoration: none; }
.notice-list { list-style: none; border-top: 1px solid #f0f0f0; }
.notice-item {
padding: 12px 0; border-bottom: 1px solid #f0f0f0;
display: flex; align-items: center; gap: 12px; cursor: pointer;
}
.notice-item:last-child { border-bottom: none; }
.notice-item .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--brand-primary); flex-shrink: 0; }
.notice-item.read .dot { background: transparent; border: 1px solid #d9d9d9; }
.notice-item .body { flex: 1; min-width: 0; }
.notice-item .title { font-size: 13px; color: #1a1a1a; }
.notice-item.read .title { color: #8c8c8c; }
.notice-item .text {
font-size: 12px; color: #595959;
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; margin-top: 2px;
}
.notice-item .time { font-size: 12px; color: #bfbfbf; flex-shrink: 0; }
.empty { padding: 24px; text-align: center; color: #909399; }
@media (max-width: 768px) {
.stats-grid-3 { grid-template-columns: repeat(2, 1fr); }
.stats-grid-2 { grid-template-columns: 1fr; }
}
</style>
-287
View File
@@ -1,287 +0,0 @@
<!--
原型: 会议列表.html (执行方 / leader)
SIDEBAR: ['项目列表', '人员管理', '会议列表']
列头: 项目编号 / 会议名称 / 系列会主题 / 期数 / 当前阶段 / 是否结算 / 操作
操作列 ( left 顺序): 查看 / 查看监察意见 / 确认无误
: 支持方状态机 待监管确认无误(监管通过)待结算; 待监管存在需整改的问题待整改
-->
<template>
<div class="leader-meetings">
<p class="page-sub">查看您参与的会议</p>
<!-- ========== 筛选区 ========== -->
<div class="filter-card">
<el-form :inline="true" :model="q" size="default">
<el-form-item label="项目编号">
<el-input v-model="q.projectNo" clearable placeholder="项目编号" style="width:160px" />
</el-form-item>
<el-form-item label="会议名称">
<el-input v-model="q.meetingName" clearable placeholder="会议名称" style="width:180px" />
</el-form-item>
<el-form-item label="是否系列会">
<el-select v-model="q.isSeries" clearable placeholder="全部" style="width:110px">
<el-option label="是" value="1" />
<el-option label="否" value="0" />
</el-select>
</el-form-item>
<el-form-item label="是否结算">
<el-select v-model="q.isSettled" clearable placeholder="全部" style="width:110px">
<el-option label="已结算" value="1" />
<el-option label="未结算" value="0" />
</el-select>
</el-form-item>
<el-form-item label="期数">
<el-input v-model="q.periodNo" clearable placeholder="期数" style="width:90px" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="load">查找</el-button>
<el-button @click="reset">重置</el-button>
</el-form-item>
</el-form>
</div>
<!-- ========== 表格区 (无批量按钮,按原型) ========== -->
<div class="page-card">
<el-table :data="rows" border stripe v-loading="loading">
<el-table-column prop="projectNo" label="项目编号" min-width="130" />
<el-table-column prop="meetingName" label="会议名称" min-width="200" show-overflow-tooltip />
<el-table-column prop="seriesTopic" label="系列会主题" min-width="180" show-overflow-tooltip />
<el-table-column prop="periodNo" label="期数" width="80" align="center" />
<el-table-column prop="currentStage" label="当前阶段" width="100" align="center" />
<el-table-column label="是否结算" width="90" align="center">
<template #default="{ row }">
<el-tag :type="row.isSettled === 'Y' ? 'success' : 'info'" size="small">
{{ row.isSettled === 'Y' ? '已结算' : '未结算' }}
</el-tag>
</template>
</el-table-column>
<!-- 操作列按原型 left 顺序: 查看 / 查看监察意见 / 确认无误 -->
<el-table-column label="操作" width="280" fixed="right">
<template #default="{ row }">
<el-button size="small" link type="primary" @click="openDetail(row)">查看</el-button>
<el-button size="small" link type="primary" @click="openOpinion(row)">
{{ hasOpinion(row) ? '查看监察意见' : '监察意见' }}
</el-button>
<el-button size="small" link type="primary"
:disabled="!canConfirm(row)"
@click="onConfirm(row)">确认无误</el-button>
</template>
</el-table-column>
</el-table>
<div class="pager">
<el-pagination v-model:current-page="page.pageNum" v-model:page-size="page.pageSize"
:total="page.total" :page-sizes="[10,20,50]" layout="total, sizes, prev, pager, next, jumper"
@current-change="load" @size-change="load" />
</div>
</div>
<!-- ========== Dialog: 详情 ========== -->
<el-dialog v-model="detailOpen" title="会议详情" width="600px">
<el-descriptions :column="2" border>
<el-descriptions-item label="会议名称" :span="2">{{ detail.meetingName }}</el-descriptions-item>
<el-descriptions-item label="项目名称" :span="2">{{ detail.projectName || detail.projectNo }}</el-descriptions-item>
<el-descriptions-item label="期数">{{ detail.periodNo }}</el-descriptions-item>
<el-descriptions-item label="当前阶段">{{ detail.currentStage }}</el-descriptions-item>
<el-descriptions-item label="开始时间" :span="2">{{ detail.startTime || '-' }}</el-descriptions-item>
<el-descriptions-item label="结束时间" :span="2">{{ detail.endTime || '-' }}</el-descriptions-item>
<el-descriptions-item label="状态" :span="2">
<el-tag size="small">{{ detail.supervisionStatus || detail.currentStage || '-' }}</el-tag>
</el-descriptions-item>
</el-descriptions>
<template #footer>
<el-button @click="detailOpen = false">关闭</el-button>
</template>
</el-dialog>
<!-- ========== Dialog: 监察意见 (原型 u9911 panel) ========== -->
<el-dialog v-model="opinionOpen" title="监察意见" width="560px">
<el-form :model="opinionForm" label-width="100px">
<el-form-item label="意见选择" required>
<el-radio-group v-model="opinionForm.rating">
<el-radio value="确认无误">确认无误</el-radio>
<el-radio value="存在需整改的问题">存在需整改的问题</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item v-if="opinionForm.rating === '存在需整改的问题'" label="详细意见" required>
<el-input v-model="opinionForm.content" type="textarea" :rows="6" :maxlength="500" show-word-limit
placeholder="请输入整改意见 (最多 500 字)" />
</el-form-item>
<p class="dialog-hint">*3 个工作日之内如果未填写, 则默认为确认无误</p>
</el-form>
<template #footer>
<el-button @click="opinionOpen = false">取消</el-button>
<el-button type="primary" :loading="opinionSubmitting" @click="submitOpinion">保存</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { bizList, bizUpdate } from '@/api/public'
import { ElMessage, ElMessageBox } from 'element-plus'
const rows = ref([])
const loading = ref(false)
const q = reactive({
projectNo: '',
meetingName: '',
isSeries: '',
isSettled: '',
periodNo: ''
})
const page = reactive({ pageNum: 1, pageSize: 10, total: 0 })
const detail = ref({})
const detailOpen = ref(false)
const opinionOpen = ref(false)
const opinionSubmitting = ref(false)
const opinionForm = reactive({
meetingId: null,
rating: '确认无误',
content: ''
})
async function load() {
loading.value = true
try {
const r = await bizList('meeting', { ...q, pageNum: page.pageNum, pageSize: page.pageSize })
rows.value = (r.data && r.data.rows) || r.rows || []
page.total = (r.data && r.data.total) || r.total || 0
} catch (e) {
rows.value = []
page.total = 0
} finally {
loading.value = false
}
}
function reset() {
Object.assign(q, { projectNo: '', meetingName: '', isSeries: '', isSettled: '', periodNo: '' })
page.pageNum = 1
load()
}
function openDetail(row) {
detail.value = row
detailOpen.value = true
}
// 是否已填写过监察意见 — 用于按钮文案切换
function hasOpinion(row) {
return !!(row && (row.supervisionRating || row.supervisionOpinion))
}
// 仅"待监管/已执行未传材料"阶段允许填写监察意见
function canOpinion(row) {
if (!row) return false
return ['待监管', '已执行未传材料'].includes(row.currentStage)
}
// 仅"待监管"阶段允许确认无误 (已填写监察意见为"确认无误" 或 3 天自动)
function canConfirm(row) {
if (!row) return false
return row.currentStage === '待监管'
}
function openOpinion(row) {
opinionForm.meetingId = row.meetingId
opinionForm.rating = row.supervisionRating || '确认无误'
opinionForm.content = row.supervisionOpinion || ''
opinionOpen.value = true
}
async function submitOpinion() {
if (!opinionForm.meetingId) return
if (!opinionForm.rating) {
ElMessage.warning('请选择意见')
return
}
if (opinionForm.rating === '存在需整改的问题' && !opinionForm.content.trim()) {
ElMessage.warning('请填写详细意见')
return
}
opinionSubmitting.value = true
try {
// 选择"存在需整改的问题" → 阶段变为"待整改"
// 选择"确认无误" → 阶段变为"监管通过" (原型: 监管通过默认只显示 1 个工作日)
const stage = opinionForm.rating === '存在需整改的问题' ? '待整改' : '监管通过'
await bizUpdate('meeting', {
meetingId: opinionForm.meetingId,
supervisionRating: opinionForm.rating,
supervisionOpinion: opinionForm.content,
supervisionStatus: opinionForm.rating,
currentStage: stage
})
ElMessage.success('监察意见已保存')
opinionOpen.value = false
load()
} catch (e) {
ElMessage.error(e?.msg || '保存失败')
} finally {
opinionSubmitting.value = false
}
}
// 确认无误 — 直接更新状态为"监管通过", 1 个工作日后自动变为"待结算"
async function onConfirm(row) {
try {
await ElMessageBox.confirm(`确定将会议「${row.meetingName || ''}」确认为监管通过吗?`, '确认无误', {
type: 'warning',
confirmButtonText: '确认无误',
cancelButtonText: '取消'
})
} catch {
return
}
try {
await bizUpdate('meeting', {
meetingId: row.meetingId,
supervisionRating: '确认无误',
supervisionStatus: '监管通过',
currentStage: '监管通过'
})
ElMessage.success('已确认无误')
load()
} catch (e) {
ElMessage.error(e?.msg || '操作失败')
}
}
onMounted(load)
</script>
<style scoped>
.leader-meetings { padding: 16px; }
.page-sub { font-size: 13px; color: #909399; margin-bottom: 16px; }
.filter-card,
.page-card { background: #fff; border-radius: 4px; padding: 16px; border: 1px solid #ebeef5; margin-bottom: 12px; }
.pager { display: flex; justify-content: flex-end; margin-top: 12px; }
.dialog-hint { color: #909399; font-size: 12px; margin: 8px 0 0; }
/* 按钮: var(--brand-primary) 单色蓝, 4px 方角, 无渐变 / 无 translateY */
.leader-meetings :deep(.el-button) {
border-radius: 4px;
background-image: none;
}
.leader-meetings :deep(.el-button--primary) {
--el-button-bg-color: var(--brand-primary);
--el-button-border-color: var(--brand-primary);
--el-button-hover-bg-color: var(--brand-primary-deep);
--el-button-hover-border-color: var(--brand-primary-deep);
--el-button-active-bg-color: var(--brand-primary-darker);
--el-button-active-border-color: var(--brand-primary-darker);
transform: none;
}
.leader-meetings :deep(.el-button--primary:hover),
.leader-meetings :deep(.el-button--primary:focus) {
transform: none;
}
.leader-meetings :deep(.el-button.is-link) {
background-image: none;
transform: none;
}
</style>
-275
View File
@@ -1,275 +0,0 @@
<template>
<!--
原型: 项目列表_1.html
SIDEBAR: ['项目列表', '人员管理', '会议列表']
批量按钮 (top ~220, left): 项目分配 / 批量评价
列头 (top 285-335, left): 项目编号 / 项目名称 / 总场次/总期数 / 已执行 / 未执行 / 总金额 /
可用金额 / 已支付劳务费 / 已支付会务费 / 执行单位评价支持方 /
是否结题 / 项目开始时间 / 项目结束时间 / 操作
操作列 (top 352, left): 查看 / 项目评价 / 分配
提示:
- 支持方管理员分配时只能分配监察员支持方领导方可分配执行人员
- 项目评价: 4 维度评分标准与供应商系统一致
-->
<div class="leader-projects">
<p class="page-sub">查看您负责执行的项目</p>
<div class="filter-card">
<el-form :inline="true" :model="q" size="default">
<el-form-item label="项目编号">
<el-input v-model="q.projectNo" clearable placeholder="项目编号" style="width:160px" />
</el-form-item>
<el-form-item label="项目名称">
<el-input v-model="q.projectName" clearable placeholder="项目名称" style="width:180px" />
</el-form-item>
<el-form-item label="项目形式">
<el-select v-model="q.projectForm" placeholder="全部" clearable style="width:120px">
<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="是否结题">
<el-select v-model="q.isFinished" placeholder="全部" clearable style="width:100px">
<el-option label="已结题" value="1" />
<el-option label="未结题" value="0" />
</el-select>
</el-form-item>
<el-form-item label="是否结算">
<el-select v-model="q.isSettled" placeholder="全部" clearable style="width:100px">
<el-option label="已结算" value="Y" />
<el-option label="未结算" value="N" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="load">查找</el-button>
<el-button @click="reset">重置</el-button>
</el-form-item>
</el-form>
</div>
<div class="page-card">
<!-- 批量按钮 (按原型 left 顺序): 项目分配 / 批量评价 -->
<div class="batch-bar">
<el-button type="primary" @click="openBatchAssign">项目分配</el-button>
<!-- 评价入口仅 manager / sponsor 可见, leader 不可评分 -->
<span class="hint">已选 {{ selected.length }} </span>
</div>
<el-table :data="rows" border stripe v-loading="loading" @selection-change="onSelect">
<el-table-column type="selection" width="48" />
<el-table-column prop="projectNo" label="项目编号" min-width="140" fixed>
<template #default="{ row }">
<el-link type="primary" :underline="false" @click="openDetail(row)">{{ row.projectNo }}</el-link>
</template>
</el-table-column>
<el-table-column prop="projectName" label="项目名称" min-width="200" show-overflow-tooltip />
<el-table-column prop="totalSessions" label="总场次/总期数" width="120" align="center" />
<el-table-column prop="doneSessions" label="已执行" width="80" align="center" />
<el-table-column prop="todoSessions" label="未执行" width="80" align="center" />
<el-table-column label="总金额" width="120" align="right">
<template #default="{ row }">¥{{ formatMoney(row.totalAmount) }}</template>
</el-table-column>
<el-table-column label="可用金额" width="120" align="right">
<template #default="{ row }">¥{{ formatMoney(row.availableAmount) }}</template>
</el-table-column>
<el-table-column label="已支付劳务费" width="120" align="right">
<template #default="{ row }">¥{{ formatMoney(row.paidLaborAmount) }}</template>
</el-table-column>
<el-table-column label="已支付会务费" width="120" align="right">
<template #default="{ row }">¥{{ formatMoney(row.paidMeetingAmount) }}</template>
</el-table-column>
<el-table-column prop="managerScore" label="执行单位评价(合规)" width="150" align="center" />
<el-table-column label="是否结题" width="90" align="center">
<template #default="{ row }">
<el-tag :type="row.isFinished === '1' ? 'success' : 'info'" size="small">
{{ row.isFinished === '1' ? '已结题' : '未结题' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="startTime" label="项目开始时间" min-width="160" />
<el-table-column prop="endTime" label="项目结束时间" min-width="160" />
<!-- 操作列按原型 left 顺序: 查看 / 分配 -->
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<el-button size="small" link type="primary" @click="openDetail(row)">查看</el-button>
<el-button size="small" link type="primary" @click="openAssign(row)">分配</el-button>
</template>
</el-table-column>
</el-table>
<div class="pager">
<el-pagination v-model:current-page="page.pageNum" v-model:page-size="page.pageSize"
:total="page.total" :page-sizes="[10,20,50]" layout="total, sizes, prev, pager, next, jumper"
@current-change="load" @size-change="load" />
</div>
</div>
<!-- 详情 -->
<el-dialog v-model="detailOpen" title="项目详情" width="640px">
<el-descriptions :column="2" border>
<el-descriptions-item label="项目编号">{{ detail.projectNo }}</el-descriptions-item>
<el-descriptions-item label="项目形式">{{ detail.projectForm }}</el-descriptions-item>
<el-descriptions-item label="项目名称" :span="2">{{ detail.projectName }}</el-descriptions-item>
<el-descriptions-item label="总场次/总期数">{{ detail.totalSessions || 0 }}</el-descriptions-item>
<el-descriptions-item label="已执行/未执行">{{ detail.doneSessions || 0 }} / {{ detail.todoSessions || 0 }}</el-descriptions-item>
<el-descriptions-item label="总金额">¥{{ formatMoney(detail.totalAmount) }}</el-descriptions-item>
<el-descriptions-item label="可用金额">¥{{ formatMoney(detail.availableAmount) }}</el-descriptions-item>
<el-descriptions-item label="已支付劳务费">¥{{ formatMoney(detail.paidLaborAmount) }}</el-descriptions-item>
<el-descriptions-item label="已支付会务费">¥{{ formatMoney(detail.paidMeetingAmount) }}</el-descriptions-item>
<el-descriptions-item label="执行单位评价(合规)">{{ detail.managerScore || '-' }}</el-descriptions-item>
<el-descriptions-item label="是否结题">
<el-tag :type="detail.isFinished === '1' ? 'success' : 'info'">
{{ detail.isFinished === '1' ? '已结题' : '未结题' }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="项目开始时间">{{ detail.startTime || '-' }}</el-descriptions-item>
<el-descriptions-item label="项目结束时间">{{ detail.endTime || '-' }}</el-descriptions-item>
</el-descriptions>
<template #footer><el-button @click="detailOpen=false">关闭</el-button></template>
</el-dialog>
<!-- 分配 dialog -->
<el-dialog v-model="assignOpen" :title="assignMode==='batch' ? '批量项目分配' : '项目分配'" width="520px">
<el-form :model="assignForm" label-width="100px">
<el-form-item v-if="assignMode==='single'" label="项目编号 / 名称">
<span>{{ assignForm.projectNo }} / {{ assignForm.projectName }}</span>
</el-form-item>
<el-form-item v-else label="分配项目">
<span> {{ assignForm.projectIds.length }} 个项目</span>
</el-form-item>
<el-form-item label="执行人员">
<el-select v-model="assignForm.executorId" filterable placeholder="请选择执行人员" style="width:100%">
<el-option v-for="p in personList" :key="p.personId" :label="p.name" :value="p.personId" />
</el-select>
</el-form-item>
<el-form-item label="分配备注">
<el-input v-model="assignForm.remark" type="textarea" rows="3" placeholder="可选" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="assignOpen = false">取消</el-button>
<el-button type="primary" @click="submitAssign">确认分配</el-button>
</template>
</el-dialog>
<!-- 项目评价 dialog 已移除: leader 不可评分 -->
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { bizList, bizUpdate, bizAdd } from '@/api/public'
import { ElMessage, ElMessageBox } from 'element-plus'
const q = reactive({ projectNo: '', projectName: '', projectForm: '', isFinished: '', isSettled: '' })
const rows = ref([])
const loading = ref(false)
const page = reactive({ pageNum: 1, pageSize: 10, total: 0 })
const selected = ref([])
const detail = ref({})
const detailOpen = ref(false)
// 分配
const assignOpen = ref(false)
const assignMode = ref('single') // 'single' | 'batch'
const assignForm = reactive({
projectId: null, projectNo: '', projectName: '',
projectIds: [], executorId: '', remark: ''
})
const personList = ref([])
// 评价相关状态已移除: leader 不可评分
function formatMoney(v) { return Number(v || 0).toFixed(2) }
async function load() {
loading.value = true
try {
const r = await bizList('project', { ...q, pageNum: page.pageNum, pageSize: page.pageSize })
rows.value = (r.data && r.data.rows) || r.rows || []
page.total = (r.data && r.data.total) || r.total || 0
} catch (e) { rows.value = []; page.total = 0 }
finally { loading.value = false }
}
async function loadPersons() {
try {
const r = await bizList('person', { pageNum: 1, pageSize: 100 })
personList.value = (r.data && r.data.rows) || r.rows || []
} catch (e) { personList.value = [] }
}
function reset() {
Object.assign(q, { projectNo: '', projectName: '', projectForm: '', isFinished: '', isSettled: '' })
page.pageNum = 1
load()
}
function onSelect(arr) { selected.value = arr }
function openDetail(row) { detail.value = row; detailOpen.value = true }
// ─── 分配 ───────────────────────────────────────────────
function openAssign(row) {
assignMode.value = 'single'
Object.assign(assignForm, {
projectId: row.projectId,
projectNo: row.projectNo,
projectName: row.projectName,
projectIds: [],
executorId: '',
remark: ''
})
assignOpen.value = true
}
function openBatchAssign() {
if (!selected.value.length) { ElMessage.warning('请先勾选项目'); return }
assignMode.value = 'batch'
Object.assign(assignForm, {
projectId: null, projectNo: '', projectName: '',
projectIds: selected.value.map(r => r.projectId),
executorId: '',
remark: ''
})
assignOpen.value = true
}
async function submitAssign() {
if (!assignForm.executorId) { ElMessage.warning('请选择执行人员'); return }
const ids = assignMode.value === 'single'
? [assignForm.projectId]
: assignForm.projectIds
let ok = 0
for (const pid of ids) {
try {
await bizUpdate('project', {
projectId: pid,
executorId: assignForm.executorId,
assignRemark: assignForm.remark
})
ok++
} catch (e) { /* 跳过失败项 */ }
}
ElMessage.success(`已分配 ${ok}`)
assignOpen.value = false
load()
}
// 项目评价相关函数已移除: leader 不可评分
onMounted(() => { load(); loadPersons() })
</script>
<style scoped>
.leader-projects { padding: 16px; }
.page-sub { font-size: 13px; color: #909399; margin-bottom: 16px; }
.filter-card, .page-card { background: #fff; border-radius: 4px; padding: 16px; border: 1px solid #ebeef5; margin-bottom: 12px; }
.batch-bar { margin-bottom: 12px; display: flex; gap: 8px; align-items: center; }
.hint { color: #909399; font-size: 12px; margin-left: 8px; }
.pager { display: flex; justify-content: flex-end; margin-top: 12px; }
.score-row { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
.dialog-hint { color: #909399; font-size: 12px; margin: -8px 0 0; }
</style>
+2 -2
View File
@@ -147,8 +147,8 @@ async function onUserCmd(cmd) {
if (cmd === 'logout') return goLogout() if (cmd === 'logout') return goLogout()
if (cmd === 'account') { if (cmd === 'account') {
const r = (userStore.user?.role || '') const r = (userStore.user?.role || '')
const map = { admin: '/leader/home', leader: '/leader/home', manager: '/manager/workbench', doctor: '/doctor/home', executor: '/executor/meetings', sponsor: '/sponsor/home' } const map = { admin: '/admin/workbench', manager: '/manager/workbench', doctor: '/doctor/home', executor: '/executor/meetings', sponsor: '/sponsor/home' }
router.push(map[r] || '/leader/home') router.push(map[r] || '/admin/workbench')
} }
} }
+2 -2
View File
@@ -247,8 +247,8 @@ async function onUserCmd(cmd) {
if (cmd === 'logout') return goLogout() if (cmd === 'logout') return goLogout()
if (cmd === 'account') { if (cmd === 'account') {
const r = (userStore.user?.role || '') const r = (userStore.user?.role || '')
const map = { admin: '/leader/home', leader: '/leader/home', manager: '/manager/workbench', doctor: '/doctor/home', executor: '/executor/meetings', sponsor: '/sponsor/home' } const map = { admin: '/admin/workbench', manager: '/manager/workbench', doctor: '/doctor/home', executor: '/executor/meetings', sponsor: '/sponsor/home' }
router.push(map[r] || '/leader/home') router.push(map[r] || '/admin/workbench')
} }
} }
function goPublicity() { router.push('/publicity') } function goPublicity() { router.push('/publicity') }
+2 -2
View File
@@ -240,8 +240,8 @@ async function onUserCmd(cmd) {
if (cmd === 'logout') return goLogout() if (cmd === 'logout') return goLogout()
if (cmd === 'account') { if (cmd === 'account') {
const r = (userStore.user?.role || '') const r = (userStore.user?.role || '')
const map = { admin: '/leader/home', leader: '/leader/home', manager: '/manager/workbench', doctor: '/doctor/home', executor: '/executor/meetings', sponsor: '/sponsor/home' } const map = { admin: '/admin/workbench', manager: '/manager/workbench', doctor: '/doctor/home', executor: '/executor/meetings', sponsor: '/sponsor/home' }
router.push(map[r] || '/leader/home') router.push(map[r] || '/admin/workbench')
} }
} }
function goDetail(n) { function goDetail(n) {
+3 -3
View File
@@ -243,7 +243,7 @@ const loggedIn = computed(() => !!userStore.token)
const userName = computed(() => userStore.user?.userName || '用户') const userName = computed(() => userStore.user?.userName || '用户')
// 意向按钮按角色显隐: // 意向按钮按角色显隐:
// sponsor (含 MAIN/SUB) → 仅支持意向; executor (含 MAIN/SUB) → 仅执行意向 // sponsor (含 MAIN/SUB) → 仅支持意向; executor (含 MAIN/SUB) → 仅执行意向
// admin / leader / manager / doctor → 两个都隐藏 // admin / manager / doctor → 两个都隐藏
// 匿名 → 两个都显示 (走 guestDialog) // 匿名 → 两个都显示 (走 guestDialog)
const canShowSupportBtn = computed(() => { const canShowSupportBtn = computed(() => {
const r = userStore.user?.role || '' const r = userStore.user?.role || ''
@@ -392,8 +392,8 @@ async function onUserCmd(cmd) {
if (cmd === 'logout') return goLogout() if (cmd === 'logout') return goLogout()
if (cmd === 'account') { if (cmd === 'account') {
const r = (userStore.user?.role || '') const r = (userStore.user?.role || '')
const map = { admin: '/leader/home', leader: '/leader/home', manager: '/manager/workbench', doctor: '/doctor/home', executor: '/executor/meetings', sponsor: '/sponsor/home' } const map = { admin: '/admin/workbench', manager: '/manager/workbench', doctor: '/doctor/home', executor: '/executor/meetings', sponsor: '/sponsor/home' }
router.push(map[r] || '/leader/home') router.push(map[r] || '/admin/workbench')
} }
} }
@@ -185,8 +185,8 @@ async function onUserCmd(cmd) {
if (cmd === 'logout') return goLogout() if (cmd === 'logout') return goLogout()
if (cmd === 'account') { if (cmd === 'account') {
const r = (userStore.user?.role || '') const r = (userStore.user?.role || '')
const map = { admin: '/leader/home', leader: '/leader/home', manager: '/manager/workbench', doctor: '/doctor/home', executor: '/executor/meetings', sponsor: '/sponsor/home' } const map = { admin: '/admin/workbench', manager: '/manager/workbench', doctor: '/doctor/home', executor: '/executor/meetings', sponsor: '/sponsor/home' }
router.push(map[r] || '/leader/home') router.push(map[r] || '/admin/workbench')
} }
} }
+2 -6
View File
@@ -85,7 +85,6 @@ def test_round(n):
# 7. 前端路由覆盖 # 7. 前端路由覆盖
expected_routes = [ expected_routes = [
'/', '/publicity', '/login', '/register-expert', '/register-supplier', '/register-sponsor', '/', '/publicity', '/login', '/register-expert', '/register-supplier', '/register-sponsor',
'/leader/home', '/leader/projects', '/leader/meetings', '/leader/account',
'/manager/workbench', '/manager/plans', '/manager/projects', '/manager/meetings', '/manager/workbench', '/manager/plans', '/manager/projects', '/manager/meetings',
'/manager/experts', '/manager/exec-units', '/manager/support-units', '/manager/experts', '/manager/exec-units', '/manager/support-units',
'/manager/support-intent', '/manager/exec-intent', '/manager/accounts', '/manager/support-intent', '/manager/exec-intent', '/manager/accounts',
@@ -98,7 +97,6 @@ def test_round(n):
for r in expected_routes: for r in expected_routes:
path_only = r.lstrip('/') path_only = r.lstrip('/')
# 路由 path 可能有/也可能没有(顶级 parent vs child),name 也可作为 path 替代 # 路由 path 可能有/也可能没有(顶级 parent vs child),name 也可作为 path 替代
# 期望 router 中存在 path: 'leader/home' 或 name: 'leader-home'
name_guess = path_only.replace('/', '-') name_guess = path_only.replace('/', '-')
if not any(p in router_file for p in [ if not any(p in router_file for p in [
f"path: '{path_only}'", f"path: '/{path_only}'", f"path: '{path_only}'", f"path: '/{path_only}'",
@@ -188,7 +186,6 @@ def test_round(n):
# ============ 新增: 角色页 field 覆盖率 ============ # ============ 新增: 角色页 field 覆盖率 ============
view_map = { view_map = {
# 角色: [(view文件, 原型HTML, 必须出现的字段)] # 角色: [(view文件, 原型HTML, 必须出现的字段)]
'leader': ('Home.vue', 'leader-home.html', ['项目总数量','已结算数量','已结题数量','未执行会议','已执行会议','消息通知']),
'manager': ('Workbench.vue', 'workbench.html', ['项目总数量','已结算的会议','已结题的项目','待结算的会议','未执行会议','待审核的专家']), 'manager': ('Workbench.vue', 'workbench.html', ['项目总数量','已结算的会议','已结题的项目','待结算的会议','未执行会议','待审核的专家']),
'doctor': ('Review.vue', None, ['项目评审','评审意见','评分']), 'doctor': ('Review.vue', None, ['项目评审','评审意见','评分']),
'executor': ('Meetings.vue', None, ['会议管理']), 'executor': ('Meetings.vue', None, ['会议管理']),
@@ -236,7 +233,7 @@ def test_round(n):
# ============ 新增: 角色路由自动跳转 ============ # ============ 新增: 角色路由自动跳转 ============
login_js = Path('/home/john/ry8080/ry-vue3/src/views/auth/Login.vue').read_text() login_js = Path('/home/john/ry8080/ry-vue3/src/views/auth/Login.vue').read_text()
check('登录按角色路由', 'leader' in login_js and 'manager' in login_js, '') check('登录按角色路由', 'manager' in login_js, '')
# ============ 新增: 业务表结构校验 ============ # ============ 新增: 业务表结构校验 ============
try: try:
@@ -282,7 +279,7 @@ def test_round(n):
check('流程5-认证端点', 'msg' in r, f"code={r.get('code')}") check('流程5-认证端点', 'msg' in r, f"code={r.get('code')}")
# ============ 新增: 角色权限检查(每个角色 home 可访问)============ # ============ 新增: 角色权限检查(每个角色 home 可访问)============
for role_home in ['/leader/home','/manager/workbench','/doctor/review','/executor/meetings','/sponsor/home']: for role_home in ['/manager/workbench','/doctor/review','/executor/meetings','/sponsor/home']:
path_only = role_home.lstrip('/') path_only = role_home.lstrip('/')
name_guess = path_only.replace('/', '-') name_guess = path_only.replace('/', '-')
check(f'角色 home: {role_home}', any(p in router_file for p in [ check(f'角色 home: {role_home}', any(p in router_file for p in [
@@ -301,7 +298,6 @@ def test_round(n):
# ============ 新增: 角色 × view 映射完整性 ============ # ============ 新增: 角色 × view 映射完整性 ============
expected_role_views = { expected_role_views = {
'leader': ['Home.vue','Projects.vue','Meetings.vue','Account.vue'],
'manager': ['Workbench.vue','Plans.vue','Projects.vue','Meetings.vue','Experts.vue','ExecUnits.vue','SupportUnits.vue','SupportIntent.vue','ExecIntent.vue','Accounts.vue'], 'manager': ['Workbench.vue','Plans.vue','Projects.vue','Meetings.vue','Experts.vue','ExecUnits.vue','SupportUnits.vue','SupportIntent.vue','ExecIntent.vue','Accounts.vue'],
'doctor': ['Review.vue','Score.vue'], 'doctor': ['Review.vue','Score.vue'],
'executor': ['Meetings.vue','Labor.vue'], 'executor': ['Meetings.vue','Labor.vue'],