feat(front): 项目/会议管理 admin 端补齐 + 删除按钮 + 级联 UI + 角色劳务组件

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)
This commit is contained in:
郭庆泰
2026-08-22 20:16:46 +08:00
parent 0eb5da6ea8
commit 2eb2d1a43b
10 changed files with 582 additions and 31 deletions
+47 -14
View File
@@ -1,5 +1,5 @@
<template>
<div class="page-card">
<div class="page-card manager-projects-new">
<!-- 面包屑 -->
<div class="breadcrumb">
首页 / 项目管理 / <span class="current">{{ isEdit ? '编辑项目' : '新建项目' }}</span>
@@ -121,14 +121,14 @@
/>
</div>
</div>
</el-form>
<!-- 底部按钮 -->
<div class="form-actions">
<el-button @click="confirmCancel">取消</el-button>
<el-button type="warning" :loading="submitting" @click="submit('publish')">保存并发布公告</el-button>
<el-button type="primary" :loading="submitting" @click="submit('save')">保存</el-button>
</div>
<!-- 底部按钮 (skill: el-form-item 包裹, 不另起 div.form-actions) -->
<el-form-item class="form-actions">
<el-button @click="confirmCancel">取消</el-button>
<el-button type="warning" :loading="submitting" @click="submit('publish')">保存并发布公告</el-button>
<el-button type="primary" :loading="submitting" @click="submit('save')">保存</el-button>
</el-form-item>
</el-form>
</div>
</template>
@@ -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') {
</script>
<style scoped>
/* 整页面板 (与 People.vue 风格一致, 1 层) */
.page-card { background: #fff; padding: 16px 20px; border-radius: 6px; border: 1px solid #f0f0f0; }
/* 整页面板 (与 ManagerProjectDetail.vue 风格一致: 新建/编辑页 max-width 1200px, 居左, 不居中) */
.manager-projects-new {
background: #fff;
padding: 16px 20px;
border-radius: 6px;
border: 1px solid #f0f0f0;
max-width: 1200px;
}
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
.breadcrumb .current { color: #262626; font-weight: 500; }
@@ -399,6 +419,19 @@ async function submit(mode = 'save') {
.notice-uploader :deep(.file-icon) { font-size: 18px; }
.notice-uploader :deep(.file-icon svg) { width: 18px; height: 18px; }
/* 底部按钮 */
.form-actions { display: flex; justify-content: center; gap: 16px; padding: 20px 0 4px; border-top: 1px solid #f0f0f0; margin-top: 8px; }
/* 底部按钮 (skill: 用 el-form-item 包裹, 不用 div.form-actions) */
.form-actions {
margin-top: 24px;
padding-top: 24px;
border-top: 1px solid #f0f0f0;
}
/* 按钮所在 form-item 不显示 label 列 (skill form-actions 包裹的标准做法) */
.form-actions :deep(.el-form-item__label) {
display: none;
}
.form-actions :deep(.el-form-item__content) {
margin-left: 0 !important;
display: flex;
gap: 12px;
}
</style>