refactor(meetings): MeetingDetail + MeetingNew 挪到公共目录 views/meetings/

会议管理 3 个公共组件统一放在 views/meetings/ 下:
- Meetings.vue          (列表, 上轮 Option C 已挪)
- MeetingDetail.vue     ← 本次挪
- MeetingNew.vue        ← 本次挪

理由:
- 3 个组件都是 admin/manager 共用 (路由前缀对称指过去)
- 目录名 'meetings' 精确表达 '会议公共组件' 语义
- 避免 grep 'manager/Meeting*' 误以为是 manager 专属

router/index.js 4 个 import 路径同步更新:
- admin-meetings-detail / admin-meetings-new
- manager-meetings-detail / manager-meetings-new

git mv 保留文件历史 (rename detection), 无新增代码逻辑改动.
This commit is contained in:
郭庆泰
2026-08-22 20:22:46 +08:00
parent 996ac13994
commit e68476eeeb
3 changed files with 4 additions and 4 deletions
+279
View File
@@ -0,0 +1,279 @@
<template>
<div class="page-card manager-meeting-new">
<!-- 面包屑: 标题随 mode 变 (新建/修改/复制) -->
<div class="breadcrumb">首页 / 会议管理 / {{ pageTitle }}</div>
<!-- el-form 包裹 + label-width 110px (跟原型一致) -->
<el-form ref="formRef" :model="form" :rules="rules" label-width="110px">
<!-- 2 列布局 (跟原型 .form-grid 一致: grid-auto-flow: column 让左 6 右 5 对齐) -->
<div class="form-grid">
<!-- 左列 6 项只读快照 (来自 biz_project 派生) -->
<el-form-item label="项目编号">
<span class="form-value code">{{ snap.projectNo }}</span>
</el-form-item>
<el-form-item label="项目名称">
<span class="form-value">{{ snap.projectName }}</span>
</el-form-item>
<el-form-item label="总场次/总期数">
<span class="form-value">{{ snap.totalSessions }}</span>
</el-form-item>
<el-form-item label="支持单位">
<span class="form-value">{{ snap.sponsorOrgName }}</span>
</el-form-item>
<el-form-item label="创建时间">
<span class="form-value">{{ snap.createTime }}</span>
</el-form-item>
<el-form-item label="创建人员">
<span class="form-value">{{ snap.createUserName }}</span>
</el-form-item>
<!-- 右列 5 项: 1 只读 (会议名称 = 项目名) + 4 输入 -->
<el-form-item label="会议名称">
<span class="form-value">{{ form.meetingName }}</span>
</el-form-item>
<el-form-item label="期数" prop="periodNo">
<el-input v-model="form.periodNo" placeholder="请填写第几期" />
</el-form-item>
<el-form-item label="会议开始时间" prop="startTime">
<el-date-picker v-model="form.startTime" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" placeholder="选择开始时间" />
</el-form-item>
<el-form-item label="会议结束时间" prop="endTime">
<el-date-picker v-model="form.endTime" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" placeholder="选择结束时间" />
</el-form-item>
<el-form-item label="备注">
<el-input v-model="form.remark" placeholder="请输入备注" />
</el-form-item>
</div>
<!-- 底部按钮 (skill: 用 el-form-item 包裹, 不另起 div.form-actions) -->
<el-form-item class="form-actions">
<el-button type="primary" :loading="submitting" @click="onSave">保存</el-button>
<el-button @click="goBack">取消</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script setup>
import { ref, reactive, computed, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useUserStore } from '@/store/modules/user'
import { bizAdd, bizGet, bizUpdate } from '@/api/public'
import { ElMessage } from 'element-plus'
const route = useRoute()
const router = useRouter()
// ========== 角色感知 (admin/manager 共用此新建页, 按 URL 第 1 段识别) ==========
const userStore = useUserStore()
const roleSegment = computed(() => route.path.split('/')[1]) // 'admin' | 'manager'
const isAdmin = computed(() => roleSegment.value === 'admin')
const meetingsBase = computed(() => `/${roleSegment.value}/meetings`)
const projectsBase = computed(() => `/${roleSegment.value}/projects`)
// ========== mode 判定 ==========
// 3 种模式:
// new: ?projectId=xxx → POST /business/meeting (新建)
// edit: ?meetingId=xxx → PUT /business/meeting/{id} (修改)
// copy: ?meetingId=xxx&mode=copy → POST /business/meeting (复制)
// 修改/复制 URL 同时带 projectId 让左列快照能立即加载 (不必先等 bizGet meeting)
const meetingId = route.query.meetingId || ''
const urlProjectId = route.query.projectId || ''
const mode = computed(() => {
if (meetingId && route.query.mode === 'copy') return 'copy'
if (meetingId) return 'edit'
return 'new'
})
const pageTitle = computed(() => ({ new: '新建会议', edit: '修改会议', copy: '复制会议' }[mode.value]))
const formRef = ref(null)
const submitting = ref(false)
// ========== 项目只读快照 (左列 6 项) ==========
const snap = reactive({
projectNo: '',
projectName: '',
totalSessions: '',
sponsorOrgName: '',
createTime: '',
createUserName: ''
})
// ========== 表单 (右列 5 项: meetingName 只读 + 4 输入) ==========
const form = reactive({
projectId: urlProjectId ? Number(urlProjectId) : null,
projectNo: '',
projectName: '',
meetingName: '',
periodNo: '',
startTime: '',
endTime: '',
remark: ''
})
const rules = {
periodNo: [{ required: true, message: '请填写第几期', trigger: 'blur' }],
startTime: [{ required: true, message: '请选择会议开始时间', trigger: 'change' }],
endTime: [{ required: true, message: '请选择会议结束时间', trigger: 'change' }]
}
// ========== 加载项目快照 ==========
async function loadProjectSnapshot(pid) {
if (!pid) return
try {
const r = await bizGet('project', pid)
const d = (r && r.data) || {}
snap.projectNo = d.projectNo || ''
snap.projectName = d.projectName || ''
snap.totalSessions = d.totalSessions ?? ''
snap.sponsorOrgName = d.sponsorOrgName || ''
snap.createTime = d.createTime || ''
snap.createUserName = d.createUserName || d.createBy || ''
} catch (e) { /* 静默, 不阻塞主流程 */ }
}
// ========== 加载会议详情 (edit/copy 模式用) ==========
async function loadMeeting(mid) {
if (!mid) return
const r = await bizGet('meeting', mid)
const d = (r && r.data) || {}
form.meetingId = d.meetingId
form.projectId = d.projectId ?? null
form.projectNo = d.projectNo || ''
form.projectName = d.projectName || ''
form.meetingName = d.meetingName || ''
form.periodNo = d.periodNo || ''
form.startTime = d.startTime || ''
form.endTime = d.endTime || ''
form.remark = d.remark || ''
// 左列快照 projectNo/projectName 用 meeting 冗余字段 (即使 project_id 为 NULL 也能显示)
snap.projectNo = d.projectNo || ''
snap.projectName = d.projectName || ''
// 总场次/支持单位/创建时间/创建人员 优先用 URL projectId, 其次 meeting.projectId
const pid = urlProjectId || d.projectId
if (pid) {
await loadProjectSnapshot(pid)
}
}
// ========== 保存 ==========
async function onSave() {
try { await formRef.value.validate() } catch (e) { return }
submitting.value = true
try {
const payload = {
projectId: form.projectId,
projectNo: form.projectNo,
projectName: form.projectName,
meetingName: form.meetingName,
periodNo: form.periodNo,
startTime: form.startTime,
endTime: form.endTime,
remark: form.remark
}
if (mode.value === 'edit') {
// 修改: 走 bizUpdate, 带 meetingId (后端 WHERE 用, 不能 delete)
payload.meetingId = form.meetingId
// __silentError: 拦截器不自动 toast, 错误统一由下方 catch 处理 (避免双 toast)
await bizUpdate('meeting', payload, { __silentError: true })
ElMessage.success('修改成功')
} else {
// 新建 / 复制: 走 bizAdd
await bizAdd('meeting', payload, { __silentError: true })
ElMessage.success(mode.value === 'copy' ? '复制成功' : '会议创建成功')
}
goBack()
} catch (e) {
ElMessage.error(e?.msg || (mode.value === 'edit' ? '修改失败' : '保存失败'))
} finally {
submitting.value = false
}
}
function goBack() {
// 跳回上游列表页 (按角色):
// new (从 Projects 来) → /{role}/projects
// edit/copy (从 Meetings 来) → /{role}/meetings
if (mode.value === 'new') {
router.push(projectsBase.value)
} else {
router.push(meetingsBase.value)
}
}
onMounted(async () => {
if (mode.value === 'edit') {
await loadMeeting(meetingId)
} else if (mode.value === 'copy') {
// 复制: 加载会议详情, name 加 " 副本", 期数 reset 让用户重填
await loadMeeting(meetingId)
form.meetingName = (form.meetingName || '') + ' 副本'
form.periodNo = '' // 复制后让用户重新填第几期
form.meetingId = null // 防御: 不带源 meetingId 走 bizAdd
} else {
// 新建: 加载项目快照, meetingName 默认 = 项目名
if (urlProjectId) {
await loadProjectSnapshot(urlProjectId)
form.projectNo = snap.projectNo
form.projectName = snap.projectName
form.meetingName = snap.projectName
}
}
})
</script>
<style scoped>
/* === 整页面板 (跟 Accounts.vue 一致: max-width: 1200px, 不居中) === */
.manager-meeting-new {
padding: 16px;
max-width: 1200px;
}
/* 面包屑 (跟 executor/People.vue 一致) */
.breadcrumb {
font-size: 13px;
color: #8c8c8c;
margin-bottom: 24px;
}
/* === 2 列布局 (跟原型 .form-grid 一致, grid-auto-flow: column 让左 6 右 5 对齐) === */
.form-grid {
display: grid;
grid-template-columns: 1fr 1fr;
grid-auto-flow: column;
grid-template-rows: repeat(6, auto);
gap: 20px 32px;
max-width: 900px;
}
/* grid 内的 el-form-item 不再加默认 margin-bottom (由 grid 控制间距) */
.form-grid :deep(.el-form-item) {
margin-bottom: 0;
}
/* 只读值样式 */
.form-value {
font-size: 14px;
color: #333;
line-height: 32px;
}
.form-value.code {
font-family: ui-monospace, "Courier New", monospace;
color: var(--brand-primary);
}
/* 底部按钮 (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>