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:
@@ -0,0 +1,153 @@
|
||||
<template>
|
||||
<!--
|
||||
项目级角色下拉控件 (带"其他"逃生口)
|
||||
|
||||
数据来源: 父组件传入 props.roles (来自 biz_project.role_labor JSON, 形如 [{role, customName, amount}])
|
||||
- role === '其他' → 显示 customName 作为 label
|
||||
- role !== '其他' → 显示 role 作为 label
|
||||
|
||||
行为契约 (前端控件自管理, 父组件无感知):
|
||||
- 标准项目角色: 用户从下拉选 → v-model = 该角色名
|
||||
- 选 "其他" (控件自己虚拟追加, 不来自项目定义): 下方出现 el-input → v-model = input 内容
|
||||
- 回填: modelValue 不在项目角色列表里 → 自动按 "其他 + input" 显示, input 预填原值
|
||||
|
||||
用法:
|
||||
<project-role-select v-model="attendeeDialog.form.laborForm" :roles="projectRoles" />
|
||||
-->
|
||||
<div class="project-role-select">
|
||||
<el-select
|
||||
v-model="selectedKey"
|
||||
:placeholder="placeholder"
|
||||
:disabled="disabled"
|
||||
:clearable="clearable"
|
||||
:filterable="filterable"
|
||||
style="width: 100%"
|
||||
@change="handleSelectChange"
|
||||
@clear="handleClear"
|
||||
>
|
||||
<el-option
|
||||
v-for="o in displayOptions"
|
||||
:key="o.key"
|
||||
:label="o.label"
|
||||
:value="o.key"
|
||||
/>
|
||||
</el-select>
|
||||
<el-input
|
||||
v-if="showOtherInput"
|
||||
v-model="otherText"
|
||||
:placeholder="otherPlaceholder"
|
||||
class="other-input"
|
||||
maxlength="50"
|
||||
@input="emitValue"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: String, default: '' },
|
||||
/** 项目级角色数组: [{role, customName, amount}] (来自 biz_project.role_labor JSON 解析) */
|
||||
roles: { type: Array, default: () => [] },
|
||||
placeholder: { type: String, default: '请选择角色' },
|
||||
otherPlaceholder: { type: String, default: '请输入角色名' },
|
||||
disabled: { type: Boolean, default: false },
|
||||
clearable: { type: Boolean, default: true },
|
||||
filterable: { type: Boolean, default: true }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'change'])
|
||||
|
||||
// "其他" 虚拟项的内部 key — 不暴露给父组件, 仅用于区分 selectedKey 状态
|
||||
const OTHER_KEY = '__other_role__'
|
||||
|
||||
// 把 props.roles 投影成 {key, label} 数组
|
||||
// - role === '其他' → label = customName (项目里实际填的"自定义角色名")
|
||||
// - role !== '其他' → label = role
|
||||
const projectOptions = computed(() => {
|
||||
return (props.roles || [])
|
||||
.filter(r => r && (r.role || r.customName))
|
||||
.map(r => {
|
||||
if (r.role === '其他') {
|
||||
const customName = (r.customName || '').trim()
|
||||
return { key: customName, label: customName || '其他' }
|
||||
}
|
||||
return { key: r.role, label: r.role }
|
||||
})
|
||||
// 同 key 去重 (防御性: 项目里手填了两个相同 role)
|
||||
.filter((o, i, arr) => arr.findIndex(x => x.key === o.key) === i)
|
||||
// 去掉与 OTHER_KEY 冲突的 (理论上不会, 但防御)
|
||||
.filter(o => o.key !== OTHER_KEY)
|
||||
})
|
||||
|
||||
const selectedKey = ref(props.modelValue || null)
|
||||
const otherText = ref(props.modelValue || '')
|
||||
|
||||
const showOtherInput = computed(() => selectedKey.value === OTHER_KEY)
|
||||
|
||||
// 给 el-select 渲染的 options = 项目角色 + 末尾虚拟"其他"
|
||||
const displayOptions = computed(() => [
|
||||
...projectOptions.value,
|
||||
{ key: OTHER_KEY, label: '其他' }
|
||||
])
|
||||
|
||||
function isValueInOptions(v) {
|
||||
if (v == null || v === '') return false
|
||||
return projectOptions.value.some(o => o.key === v)
|
||||
}
|
||||
|
||||
function syncFromModelValue(v) {
|
||||
if (v == null || v === '') {
|
||||
selectedKey.value = null
|
||||
otherText.value = ''
|
||||
return
|
||||
}
|
||||
if (isValueInOptions(v)) {
|
||||
selectedKey.value = v
|
||||
otherText.value = ''
|
||||
} else {
|
||||
// 不在项目角色列表里 → 走 "其他" 路径
|
||||
selectedKey.value = OTHER_KEY
|
||||
otherText.value = v
|
||||
}
|
||||
}
|
||||
|
||||
function emitValue() {
|
||||
let v
|
||||
if (selectedKey.value === OTHER_KEY) {
|
||||
v = otherText.value || ''
|
||||
} else if (selectedKey.value == null) {
|
||||
v = ''
|
||||
} else {
|
||||
v = selectedKey.value
|
||||
}
|
||||
emit('update:modelValue', v)
|
||||
emit('change', v)
|
||||
}
|
||||
|
||||
function handleSelectChange(key) {
|
||||
if (key === OTHER_KEY && selectedKey.value !== OTHER_KEY) {
|
||||
otherText.value = ''
|
||||
}
|
||||
selectedKey.value = key
|
||||
emitValue()
|
||||
}
|
||||
|
||||
function handleClear() {
|
||||
selectedKey.value = null
|
||||
otherText.value = ''
|
||||
emitValue()
|
||||
}
|
||||
|
||||
// 初始化 + 响应外部变化
|
||||
syncFromModelValue(props.modelValue)
|
||||
watch(() => props.modelValue, v => syncFromModelValue(v))
|
||||
// 项目角色列表变化后重做一次同步 (例如 load() 后异步拿到 roles)
|
||||
watch(() => props.roles, () => syncFromModelValue(props.modelValue), { deep: true })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.project-role-select { display: flex; flex-direction: column; gap: 6px; width: 100%; }
|
||||
.other-input { width: 100%; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user