From 2eb2d1a43bee8d40a188deb86632739228a65cc7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E9=83=AD=E5=BA=86=E6=B3=B0?=
<12369755+htcloud1@user.noreply.gitee.com>
Date: Sat, 22 Aug 2026 20:16:46 +0800
Subject: [PATCH] =?UTF-8?q?feat(front):=20=E9=A1=B9=E7=9B=AE/=E4=BC=9A?=
=?UTF-8?q?=E8=AE=AE=E7=AE=A1=E7=90=86=20admin=20=E7=AB=AF=E8=A1=A5?=
=?UTF-8?q?=E9=BD=90=20+=20=E5=88=A0=E9=99=A4=E6=8C=89=E9=92=AE=20+=20?=
=?UTF-8?q?=E7=BA=A7=E8=81=94=20UI=20+=20=E8=A7=92=E8=89=B2=E5=8A=B3?=
=?UTF-8?q?=E5=8A=A1=E7=BB=84=E4=BB=B6?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
admin 会议管理:
- 新建 admin/AdminMeetings.vue (查看/删除/批量删除, 面包屑 首页/项目管理/会议管理)
- AdminLayout.vue 加 /admin/meetings 菜单
- router/index.js 加 admin-meetings + admin-meetings-detail 路由
- MeetingDetail.vue admin 删除按钮改 admin+manager 双角色可触发 (onAdminDelete 跳回按角色)
manager 会议管理:
- Meetings.vue 加批量删除 + 删除按钮 + 删除确认文案 (6 张表级联)
- MeetingDetail.vue 删除按钮 v-if 改 canDeleteMeeting = isAdmin || isManager
manager 项目管理:
- Projects.vue 加批量删除 + 删除项 + doDelete 改走 cascade (5 张项目表 + 会议链)
- ProjectsNew.vue style-alignment: page-card 居左 (max-width 1200px, 无 margin auto), form-actions 包在 el-form 内
- Projects.vue + Meetings.vue + AdminMeetings.vue 删页内冗余删除提示
store + 组件:
- store/user.js 加 isAdmin getter
- components/ProjectRoleSelect.vue 新建 (项目级角色劳务下拉, 接 roleLabor)
---
ry-vue3/src/components/ProjectRoleSelect.vue | 153 +++++++++++++
ry-vue3/src/layout/AdminLayout.vue | 1 +
ry-vue3/src/router/index.js | 2 +
ry-vue3/src/store/user.js | 3 +-
ry-vue3/src/views/admin/AdminMeetings.vue | 212 ++++++++++++++++++
.../views/manager/ManagerProjectDetail.vue | 11 +-
ry-vue3/src/views/manager/MeetingDetail.vue | 75 ++++++-
ry-vue3/src/views/manager/Meetings.vue | 45 +++-
ry-vue3/src/views/manager/Projects.vue | 50 ++++-
ry-vue3/src/views/manager/ProjectsNew.vue | 61 +++--
10 files changed, 582 insertions(+), 31 deletions(-)
create mode 100644 ry-vue3/src/components/ProjectRoleSelect.vue
create mode 100644 ry-vue3/src/views/admin/AdminMeetings.vue
diff --git a/ry-vue3/src/components/ProjectRoleSelect.vue b/ry-vue3/src/components/ProjectRoleSelect.vue
new file mode 100644
index 0000000..4cdc7a7
--- /dev/null
+++ b/ry-vue3/src/components/ProjectRoleSelect.vue
@@ -0,0 +1,153 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ry-vue3/src/layout/AdminLayout.vue b/ry-vue3/src/layout/AdminLayout.vue
index 3960952..01c1393 100644
--- a/ry-vue3/src/layout/AdminLayout.vue
+++ b/ry-vue3/src/layout/AdminLayout.vue
@@ -163,6 +163,7 @@ const MENU = {
{ path: '/admin/users', title: '用户管理', icon: User },
{ path: '/admin/roles', title: '角色管理', icon: Setting },
{ path: '/admin/projects', title: '项目管理', icon: Document },
+ { path: '/admin/meetings', title: '会议管理', icon: Calendar },
{ path: '/admin/library', title: '资料库管理', icon: Box, children: [
{ path: '/admin/experts', title: '专家管理', icon: UserFilled },
{ path: '/admin/sponsor-orgs', title: '支持单位管理', icon: Star },
diff --git a/ry-vue3/src/router/index.js b/ry-vue3/src/router/index.js
index b09a8e2..57fe73c 100644
--- a/ry-vue3/src/router/index.js
+++ b/ry-vue3/src/router/index.js
@@ -26,6 +26,8 @@ const routes = [
{ path: 'users', name: 'admin-users', component: () => import('@/views/admin/Users.vue'), meta: { title: '用户管理' } },
{ path: 'roles', name: 'admin-roles', component: () => import('@/views/admin/Roles.vue'), meta: { title: '角色管理' } },
{ path: 'projects', name: 'admin-projects', component: () => import('@/views/manager/Projects.vue'), meta: { title: '项目管理' } },
+ { path: 'meetings', name: 'admin-meetings', component: () => import('@/views/admin/AdminMeetings.vue'), meta: { title: '会议管理' } },
+ { path: 'meetings/detail/:meetingId', name: 'admin-meetings-detail', component: () => import('@/views/manager/MeetingDetail.vue'), meta: { title: '会议详情' } },
{ path: 'department', name: 'admin-department', component: () => import('@/views/admin/Department.vue'), meta: { title: '科室管理' } },
{ path: 'title', name: 'admin-title', component: () => import('@/views/admin/Title.vue'), meta: { title: '职称管理' } },
{ path: 'experts', name: 'admin-experts', component: () => import('@/views/expert/Experts.vue'), meta: { title: '专家管理' } },
diff --git a/ry-vue3/src/store/user.js b/ry-vue3/src/store/user.js
index 9f193a7..ed478c5 100644
--- a/ry-vue3/src/store/user.js
+++ b/ry-vue3/src/store/user.js
@@ -18,7 +18,8 @@ export const useUserStore = defineStore('user', {
accountType: (s) => s.user?.accountType || 'MAIN',
parentUserId: (s) => s.user?.parentUserId || null,
isMain: (s) => (s.user?.accountType || 'MAIN') === 'MAIN',
- isSub: (s) => s.user?.accountType === 'SUB'
+ isSub: (s) => s.user?.accountType === 'SUB',
+ isAdmin: (s) => s.user?.role === 'admin'
},
actions: {
setLogin(token, user) {
diff --git a/ry-vue3/src/views/admin/AdminMeetings.vue b/ry-vue3/src/views/admin/AdminMeetings.vue
new file mode 100644
index 0000000..8aae464
--- /dev/null
+++ b/ry-vue3/src/views/admin/AdminMeetings.vue
@@ -0,0 +1,212 @@
+
+
+
首页 / 项目管理 / 会议管理
+
+
+
+
+
+
+
+
+
+ 至
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 查找
+ 重置
+
+
+
+
+
+ 批量删除
+ 已选 {{ selectedIds.length }} 条
+
+
+
+
+
+
+
+
+
+
+ {{ fmtTime(row.startTime) }}
+
+
+ {{ fmtTime(row.endTime) }}
+
+
+
+ {{ row.periodNo ? '第 ' + row.periodNo + ' 期' : '-' }}
+
+
+
+ {{ row.currentStage }}
+
+
+
+
+ 查看
+ 删除
+
+
+
+
+
+
+
+
+
+
diff --git a/ry-vue3/src/views/manager/ManagerProjectDetail.vue b/ry-vue3/src/views/manager/ManagerProjectDetail.vue
index 5a103c2..afc66b2 100644
--- a/ry-vue3/src/views/manager/ManagerProjectDetail.vue
+++ b/ry-vue3/src/views/manager/ManagerProjectDetail.vue
@@ -100,12 +100,9 @@ const projectId = ref('')
const row = ref({})
const loading = ref(false)
-// 角色劳务 (从 biz_project.role_labor JSON 字段读)
+// 角色劳务 (从 biz_project.role_labor JSON 字段读; 空数组 → 模板 "暂无角色" 行兜底)
const form = reactive({
- roleRows: [
- { role: '主席', customName: '', amount: 5000 },
- { role: '主持', customName: '', amount: 3000 }
- ]
+ roleRows: []
})
// 监督员 (从 biz_project.monitors JSON 字段读)
const monitors = ref([])
@@ -147,11 +144,11 @@ async function loadProject() {
const resp = await bizGet('project', projectId.value)
const data = (resp && resp.data) || resp || {}
row.value = data || {}
- // 角色劳务 (JSON 字段, 缺则用默认)
+ // 角色劳务 (JSON 字段, 缺/空 → form.roleRows 保持空数组, 模板"暂无角色"兜底)
try {
const rl = JSON.parse(row.value.roleLabor || '[]')
if (Array.isArray(rl) && rl.length) form.roleRows = rl
- } catch (e) { /* keep default */ }
+ } catch (e) { /* keep empty */ }
// 监督员 (JSON 字段)
try {
const mn = JSON.parse(row.value.monitors || '[]')
diff --git a/ry-vue3/src/views/manager/MeetingDetail.vue b/ry-vue3/src/views/manager/MeetingDetail.vue
index dce58d3..2213182 100644
--- a/ry-vue3/src/views/manager/MeetingDetail.vue
+++ b/ry-vue3/src/views/manager/MeetingDetail.vue
@@ -6,7 +6,11 @@
-
会议详情
+
+ 会议详情
+
+ 删除会议
+
@@ -365,7 +369,7 @@
-
+
@@ -440,6 +444,7 @@ import OssFileUploader from '@/components/OssFileUploader.vue'
import ImportResultDialog from '@/components/ImportResultDialog.vue'
import DoctorTitleSelect from '@/components/DoctorTitleSelect.vue'
import DoctorDeptSelect from '@/components/DoctorDeptSelect.vue'
+import ProjectRoleSelect from '@/components/ProjectRoleSelect.vue'
import { ElMessageBox } from 'element-plus'
import { Upload } from '@element-plus/icons-vue'
@@ -461,6 +466,10 @@ const busy = ref({ submitMaterial: false, submitVoucher: false })
const currentUserId = computed(() => userStore.user?.userId)
const currentRole = computed(() => userStore.role)
const isManager = computed(() => currentRole.value === 'manager')
+const isAdmin = computed(() => currentRole.value === 'admin')
+/** 软删除会议: admin / manager 可调 (后端 BizMeetingController.DELETE 强校验) */
+const canDeleteMeeting = computed(() => isAdmin.value || isManager.value)
+const deleting = ref(false)
// ===================== 工具 =====================
function pad(n) { return String(n).padStart(2, '0') }
@@ -612,6 +621,9 @@ async function loadMaterials() {
const attendeeRows = ref([])
const attendeeLoading = ref(false)
+/** 项目级角色 (来自 biz_project.role_labor JSON 解析), 用于参会人 dialog 角色下拉 */
+const projectRoles = ref([])
+
/** 参会人 dialog 状态 (新增/编辑共用) */
const attendeeDialog = ref({
show: false,
@@ -832,13 +844,37 @@ async function load() {
try {
const resp = await bizGet('meeting', meetingId.value)
row.value = (resp && (resp.data || resp)) || {}
- await Promise.all([loadMaterials(), loadStaff(), loadTrail(), loadAttendees()])
+ await Promise.all([loadMaterials(), loadStaff(), loadTrail(), loadAttendees(), loadProjectRoles()])
} catch (e) {
console.error('[meeting-detail] load failed', e)
row.value = {}
} finally { loading.value = false }
}
+/**
+ * 加载项目级角色 (biz_project.role_labor JSON)
+ * 失败/为空 → projectRoles 留空, ProjectRoleSelect 会显示空下拉 + 虚拟"其他"逃生口
+ */
+async function loadProjectRoles() {
+ const pid = row.value.projectId
+ if (!pid) { projectRoles.value = []; return }
+ try {
+ const resp = await bizGet('project', pid)
+ const p = (resp && (resp.data?.data || resp.data)) || resp
+ const rl = p && p.roleLabor
+ if (typeof rl === 'string') {
+ try { projectRoles.value = JSON.parse(rl) || [] } catch (e) { projectRoles.value = [] }
+ } else if (Array.isArray(rl)) {
+ projectRoles.value = rl
+ } else {
+ projectRoles.value = []
+ }
+ } catch (e) {
+ console.warn('[meeting-detail] loadProjectRoles failed', e)
+ projectRoles.value = []
+ }
+}
+
// ===================== 保存 (材料) =====================
async function onSave() {
if (saving.value) return
@@ -1062,8 +1098,36 @@ async function confirmAudit() {
function onDownloadTemplate() { ElMessage.info('模板下载 - 接口待对接') }
function onPackUpload() { ElMessage.info('打包上传 - 接口待对接') }
+// ===================== admin / manager 删除会议 (软删) =====================
+/**
+ * 后端 BizMeetingController.DELETE 强校验 role in (admin, manager).
+ * 详情页删除后跳回当前角色对应的会议列表 (避免停在已删会议的空页).
+ */
+async function onAdminDelete() {
+ try {
+ await ElMessageBox.confirm(
+ `确定要删除会议「${row.value.meetingName || ''}」吗?\n删除为软删除, 数据保留审计追溯, 医生/参会人将不再看到该会议.`,
+ '删除确认', { type: 'warning' }
+ )
+ } catch { return }
+ deleting.value = true
+ try {
+ await request.delete(`/business/meeting/${meetingId.value}`)
+ ElMessage.success('删除成功')
+ router.push(isAdmin.value ? '/admin/meetings' : '/manager/meetings')
+ } catch (e) {
+ ElMessage.error(e?.msg || '删除失败')
+ } finally {
+ deleting.value = false
+ }
+}
+
// ===================== 导航 =====================
-function goBack() { router.push('/manager/meetings') }
+function goBack() {
+ // 角色感知返回: admin 跳 admin 列表, manager 跳 manager 列表
+ const target = isAdmin.value ? '/admin/meetings' : '/manager/meetings'
+ router.push(target)
+}
// ===================== 初始化 =====================
onMounted(load)
@@ -1073,7 +1137,8 @@ onMounted(load)
.page-card { background: #fff; padding: 16px 20px; border-radius: 6px; border: 1px solid #f0f0f0; width: 100%; }
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
.breadcrumb .current { color: #262626; font-weight: 500; }
-.page-title { font-size: 22px; font-weight: 600; color: #1a1a1a; margin-bottom: 16px; }
+.page-title { font-size: 22px; font-weight: 600; color: #1a1a1a; margin-bottom: 16px; display: flex; justify-content: space-between; align-items: center; }
+.admin-delete-btn { font-size: 13px; }
.card { background: #fff; border: 1px solid #f0f0f0; border-radius: 8px; padding: 20px 24px; margin-bottom: 16px; }
.section-title { font-size: 16px; font-weight: 600; color: #1a1a1a; margin-bottom: 16px; padding-left: 10px; border-left: 3px solid var(--brand-primary); }
.text-muted { color: #8c8c8c; font-size: 13px; }
diff --git a/ry-vue3/src/views/manager/Meetings.vue b/ry-vue3/src/views/manager/Meetings.vue
index 1f83425..3e7c411 100644
--- a/ry-vue3/src/views/manager/Meetings.vue
+++ b/ry-vue3/src/views/manager/Meetings.vue
@@ -39,6 +39,7 @@
批量提交
+ 批量删除
已选 {{ selectedIds.length }} 条
@@ -65,12 +66,13 @@
-
+
查看
修改
提交
复制
+ 删除
@@ -94,7 +96,7 @@
diff --git a/ry-vue3/src/views/manager/Projects.vue b/ry-vue3/src/views/manager/Projects.vue
index 2777a5a..8aa8064 100644
--- a/ry-vue3/src/views/manager/Projects.vue
+++ b/ry-vue3/src/views/manager/Projects.vue
@@ -78,6 +78,9 @@
批量开通{{ selection.length }}
+
+ 批量删除{{ selection.length }}
+
@@ -131,6 +134,7 @@
导出报名专家
开通
删除公告
+ 删除项目
@@ -269,7 +273,13 @@ function onAction(cmd, row) {
else if (cmd === 'exportExperts') exportExperts(row)
else if (cmd === 'activate') openActivate(row)
else if (cmd === 'delAnn') openDeleteAnnouncement(row)
+ else if (cmd === 'delete') doDelete(row)
}
+// 仅 admin / manager 可见删除按钮 (前端 v-if, 后端 controller 再兜底 role 校验)
+const canDelete = computed(() => {
+ const r = userStore.role
+ return r === 'admin' || r === 'manager'
+})
function doCreateMeeting(row) { router.push(`/manager/meetings/new?projectId=${row.projectId}`) }
function doAssign(row) {
// 跳转独立子页面 /manager/projects/assign?projectId=X
@@ -277,18 +287,54 @@ function doAssign(row) {
}
function doEdit(row) { router.push(`/manager/projects/edit/${row.projectId}`) }
-// 删除项目(按需补充,原型无但需要)
+// 删除项目 (软删除 + 级联置 5 张项目表 + 项目下所有会议 is_deleted=1)
+// 后端 BizProjectController.remove 已强制 role in (admin, manager), 前端只做可见性 v-if
async function doDelete(row) {
try {
- await ElMessageBox.confirm(`确认删除项目"${row.projectName}"?\n项目编号: ${row.projectNo}\n\n删除后无法恢复!`, '删除确认', { type: 'error', confirmButtonText: '确认删除', cancelButtonText: '取消' })
+ await ElMessageBox.confirm(
+ `确认删除项目"${row.projectName}"?\n项目编号: ${row.projectNo}\n\n` +
+ `此操作将级联软删除:\n` +
+ `· 项目主表 + 方案 + 执行方分配 + 支持方分配 + 评分 (5 张表)\n` +
+ `· 项目下所有会议 + 会议附件 + 参会人 + 监督员 + 执行人 + 审计日志 (6 张表)\n\n` +
+ `数据保留在库, 不再展示给前台用户, 审计追溯可查。`,
+ '删除确认',
+ { type: 'error', confirmButtonText: '确认软删除', cancelButtonText: '取消' }
+ )
await bizDelete('project', row.projectId)
ElMessage.success('已删除')
+ // 从 selection 里移除已删行, 避免批量按钮残留
+ selection.value = selection.value.filter(r => r.projectId !== row.projectId)
load()
} catch (e) {
if (e !== 'cancel' && e !== 'close') ElMessage.error(e?.msg || '删除失败')
}
}
+// 批量软删除 (admin/manager)
+async function onBatchDelete() {
+ if (!selection.value.length) return ElMessage.warning('请先勾选项目')
+ try {
+ await ElMessageBox.confirm(
+ `确认批量软删除选中的 ${selection.value.length} 个项目?\n\n` +
+ `此操作将级联软删除每个项目下的:\n` +
+ `· 5 张项目表 (主表/方案/分配/支持方/评分)\n` +
+ `· 6 张会议表 (会议本身 + 附件 + 参会人 + 监督员 + 执行人 + 审计日志)\n\n` +
+ `数据保留, 不再展示给前台用户。`,
+ '批量删除确认',
+ { type: 'error', confirmButtonText: `确认删除 ${selection.value.length} 个`, cancelButtonText: '取消' }
+ )
+ } catch { return }
+ const ids = selection.value.map(r => r.projectId)
+ try {
+ await bizDelete('project', ids)
+ ElMessage.success(`已批量软删除 ${ids.length} 个项目`)
+ selection.value = []
+ load()
+ } catch (e) {
+ ElMessage.error(e?.msg || '批量删除失败')
+ }
+}
+
function reset() { q.value = { sponsorAdminUserIds:[], execAdminUserIds:[], projectNo:'', projectName:'', projectForm:'', startTime:'', endTime:'', isFinished:'', isSettled:'', managerScore:'' }; page.pageNum = 1; load() }
// 导出
diff --git a/ry-vue3/src/views/manager/ProjectsNew.vue b/ry-vue3/src/views/manager/ProjectsNew.vue
index b78d235..ac6271f 100644
--- a/ry-vue3/src/views/manager/ProjectsNew.vue
+++ b/ry-vue3/src/views/manager/ProjectsNew.vue
@@ -1,5 +1,5 @@
-
+
首页 / 项目管理 / {{ isEdit ? '编辑项目' : '新建项目' }}
@@ -121,14 +121,14 @@
/>
-
-
-
- 取消
- 保存并发布公告
- 保存
-
+
+
+ 取消
+ 保存并发布公告
+ 保存
+
+
@@ -226,6 +226,12 @@ async function loadProject() {
form.endTime = p.endTime || ''
form.leadUserId = p.leadUserId || null
form.isBidProject = p.isBidProject || 'N'
+ // 角色劳务反显: biz_project.role_labor JSON 数组 [{role, customName, amount}]
+ // 空数组/解析失败 → 保留 form 默认 5 行 (主席/主持/讲者/点评/评审/讨论)
+ try {
+ const rl = (typeof p.roleLabor === 'string') ? JSON.parse(p.roleLabor || '[]') : p.roleLabor
+ if (Array.isArray(rl) && rl.length) form.roleRows = rl
+ } catch (e) { /* keep default */ }
// 公告文件反显: 优先 publicityFiles JSON 数组, 否则兼容旧字段
const files = []
if (Array.isArray(p.publicityFiles)) files.push(...p.publicityFiles)
@@ -305,6 +311,16 @@ async function submit(mode = 'save') {
leadUserId: form.leadUserId,
isBidProject: form.isBidProject
}
+ // 角色劳务: 过滤空 role, 序列化为 JSON 字符串存到 biz_project.role_labor (TEXT 列)
+ // "其他" 角色把 customName 作为 role 显示名 (与 ManagerProjectDetail 渲染一致)
+ const roleRowsPayload = form.roleRows
+ .filter(r => r.role && String(r.role).trim() !== '')
+ .map(r => ({
+ role: r.role,
+ customName: r.role === '其他' ? (r.customName || '') : '',
+ amount: Number(r.amount) || 0
+ }))
+ payload.roleLabor = JSON.stringify(roleRowsPayload)
// 公告文件以 publicityFiles JSON 数组存储 (替代旧 invitationUrl/supportLetterUrl/publishUrl 独立列)
payload.publicityFiles = JSON.stringify(uploaded.map(n => ({
type: n.key,
@@ -339,8 +355,6 @@ async function submit(mode = 'save') {
// bizAdd 返回 { code, msg, data: { projectId: '...' } } 或 直接 data
savedProjectId = r?.data?.projectId || r?.projectId || r?.data
}
- const roleText = form.roleRows.filter(r => r.role).map(r => `${r.role === '其他' ? (r.customName || '其他') : r.role}=${r.amount}`).join(', ')
- if (roleText) console.info('[new-project.roleLabor]', form.projectNo, roleText)
const successText = isEdit
? (mode === 'publish' ? '更新并发布公告成功' : '更新成功')
: (mode === 'publish' ? '保存并发布公告成功' : '保存成功')
@@ -355,8 +369,14 @@ async function submit(mode = 'save') {
\ No newline at end of file