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:
-11
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -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 精确匹配 (可选)
|
||||||
|
|||||||
+1
-1
@@ -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")
|
||||||
|
|||||||
+1
-1
@@ -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 角色类型
|
||||||
|
|||||||
@@ -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] || '/')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 },
|
||||||
|
|||||||
@@ -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: '工作台' } },
|
||||||
|
|||||||
@@ -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: '' })
|
||||||
|
|||||||
@@ -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([])
|
||||||
|
|||||||
@@ -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 }))
|
||||||
|
|||||||
@@ -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: '支持方'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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') }
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
@@ -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'],
|
||||||
|
|||||||
Reference in New Issue
Block a user