491 lines
14 KiB
Vue
491 lines
14 KiB
Vue
<template>
|
||
<el-dialog
|
||
v-model="dialogVisible"
|
||
destroy-on-close
|
||
:title="title"
|
||
draggable
|
||
:close-on-click-modal="false"
|
||
:close-on-press-escape="false"
|
||
width="1400px"
|
||
>
|
||
<div class="project-form-container">
|
||
<!-- 左侧:项目基础信息 -->
|
||
<div class="left-panel">
|
||
<h4 class="panel-title">基础信息</h4>
|
||
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px" size="default">
|
||
<el-form-item label="项目编号" prop="projectNo">
|
||
<el-input v-model="form.projectNo" placeholder="请输入项目编号" clearable />
|
||
</el-form-item>
|
||
<el-form-item label="项目名称" prop="projectName">
|
||
<el-input v-model="form.projectName" placeholder="请输入项目名称" clearable />
|
||
</el-form-item>
|
||
<el-form-item label="责任人" prop="principalIds">
|
||
<el-select v-model="form.principalIds" multiple placeholder="请选择责任人" style="width: 100%" filterable clearable>
|
||
<el-option v-for="item in state.principalOptions" :key="item.id" :label="item.name" :value="item.id" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="状态" prop="status">
|
||
<el-switch v-model="form.status" />
|
||
</el-form-item>
|
||
</el-form>
|
||
</div>
|
||
|
||
<!-- 右侧:项目人员列表 -->
|
||
<div class="right-panel">
|
||
<h4 class="panel-title">人员列表</h4>
|
||
<div class="user-actions">
|
||
<el-button type="primary" size="default" @click="handleAddUser">添加人员</el-button>
|
||
</div>
|
||
<el-table
|
||
:data="form.projectUsers"
|
||
style="width: 100%"
|
||
size="default"
|
||
border
|
||
max-height="500"
|
||
>
|
||
<el-table-column label="用户名称" min-width="120">
|
||
<template #default="{ row }">
|
||
<span>{{ row.userName || getUserName(row.userId) }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="审核权限" width="180">
|
||
<template #default="{ row }">
|
||
<div class="permission-grid">
|
||
<el-checkbox v-model="row.upload" size="default">上传</el-checkbox>
|
||
<el-checkbox v-model="row.verify" size="default">审核</el-checkbox>
|
||
<el-checkbox v-model="row.review" size="default">下游审核</el-checkbox>
|
||
<el-checkbox v-model="row.signature" size="default">签名</el-checkbox>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="撤回权限" width="230">
|
||
<template #default="{ row }">
|
||
<div class="permission-grid">
|
||
<el-checkbox v-model="row.revokeUpload" size="default">撤回上传</el-checkbox>
|
||
<el-checkbox v-model="row.revokeVerify" size="default">撤回审核</el-checkbox>
|
||
<el-checkbox v-model="row.revokeReview" size="default">撤回下游审核</el-checkbox>
|
||
<el-checkbox v-model="row.revokeSignature" size="default">撤回签名</el-checkbox>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="limitToDate" label="有效日期" min-width="120">
|
||
<template #default="{ row }">
|
||
<el-date-picker
|
||
v-model="row.limitToDate"
|
||
type="date"
|
||
placeholder="有效日期"
|
||
size="default"
|
||
style="width: 100%"
|
||
format="YYYY-MM-DD"
|
||
value-format="YYYY-MM-DD"
|
||
/>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作" width="80" fixed="right">
|
||
<template #default="{ $index }">
|
||
<el-button
|
||
type="danger"
|
||
size="default"
|
||
text
|
||
@click="handleRemoveUser($index)"
|
||
>删除</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</div>
|
||
</div>
|
||
|
||
<template #footer>
|
||
<span class="dialog-footer">
|
||
<el-button @click="OnCancel" size="default">取 消</el-button>
|
||
<el-button type="primary" :loading="state.sureLoading" @click="submitForm" size="default">确 定</el-button>
|
||
</span>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<user-select
|
||
ref="userSelectRef"
|
||
:title="`添加【${form.projectName}】人员`"
|
||
multiple
|
||
:sure-loading="state.sureLoading"
|
||
@sure="onSureUser"
|
||
></user-select>
|
||
</template>
|
||
|
||
<script setup lang="ts" name="admin/project/form">
|
||
import { ref, reactive, onMounted, getCurrentInstance, defineAsyncComponent, watch } from 'vue'
|
||
import { ElMessage } from 'element-plus'
|
||
import type { FormInstance, FormRules } from 'element-plus'
|
||
import { ProjectApi } from '/@/api/admin/ProjectApi'
|
||
import { OrgApi } from '/@/api/admin/Org'
|
||
import { UserApi } from '/@/api/admin/User'
|
||
import { ProjectAddInputAndUpdateInput } from '/@/api/types/projectType'
|
||
import { ProjectUser } from '/@/api/types/projectUserType'
|
||
import {
|
||
UserGetPageOutput,
|
||
} from '/@/api/admin/data-contracts'
|
||
import eventBus from '/@/utils/mitt'
|
||
|
||
const UserSelect = defineAsyncComponent(() => import('/@/views/admin/user/components/user-select.vue'))
|
||
const { proxy } = getCurrentInstance() as any
|
||
|
||
const props = defineProps<{
|
||
title: string
|
||
}>()
|
||
|
||
const dialogVisible = ref(false)
|
||
const formRef = ref<FormInstance>()
|
||
const userSelectRef = ref()
|
||
|
||
// 扩展项目表单接口
|
||
interface ProjectFormData {
|
||
id?: number
|
||
projectNo: string
|
||
projectName: string
|
||
principalIds: number[]
|
||
projectUsers: Array<{
|
||
id?: number
|
||
userId: number
|
||
userName?: string
|
||
groupId?: number
|
||
groupName?: string
|
||
limitToDate?: string
|
||
upload: boolean
|
||
revokeUpload: boolean
|
||
review: boolean
|
||
revokeReview: boolean
|
||
signature: boolean
|
||
revokeSignature: boolean
|
||
verify: boolean
|
||
revokeVerify: boolean
|
||
}>
|
||
status: boolean
|
||
}
|
||
|
||
const form = reactive<ProjectFormData>({
|
||
projectNo: '',
|
||
projectName: '',
|
||
principalIds: [],
|
||
projectUsers: [],
|
||
status: true
|
||
})
|
||
|
||
const state = reactive({
|
||
sureLoading: false,
|
||
principalOptions: [] as Array<{ id: number; name: string }>
|
||
})
|
||
|
||
const rules = reactive<FormRules>({
|
||
projectNo: [{ required: true, message: '请输入项目编号', trigger: 'blur' }],
|
||
projectName: [{ required: true, message: '请输入项目名称', trigger: 'blur' }],
|
||
principalIds: [
|
||
{
|
||
required: true,
|
||
message: '请选择责任人',
|
||
trigger: 'change',
|
||
validator: (rule: any, value: any, callback: any) => {
|
||
if (!Array.isArray(value) || value.length === 0) {
|
||
callback(new Error('请选择责任人'))
|
||
} else {
|
||
callback()
|
||
}
|
||
}
|
||
}
|
||
]
|
||
})
|
||
|
||
/** 获取负责人列表 */
|
||
const getPrincipalOptions = async () => {
|
||
try {
|
||
const res = await new UserApi().getPage({
|
||
currentPage: 1,
|
||
pageSize: 1000,
|
||
filter: {}
|
||
})
|
||
|
||
state.principalOptions = res.data?.list?.map((item: any) => ({
|
||
id: item.id,
|
||
name: item.name
|
||
})) || []
|
||
|
||
|
||
} catch (error) {
|
||
console.error('获取用户列表失败:', error)
|
||
}
|
||
}
|
||
|
||
const getUserName = (userId: number) => {
|
||
return state.principalOptions.find((a) => a.id === userId)?.name || ''
|
||
}
|
||
|
||
const open = async (id?: number) => {
|
||
// 先打开弹窗,避免loading遮罩造成的刷新感
|
||
dialogVisible.value = true
|
||
|
||
// 如果是编辑模式且有ID,才显示loading
|
||
if (id && id > 0) {
|
||
state.sureLoading = true
|
||
try {
|
||
const res = await new ProjectApi().get({ id }, { loading: false })
|
||
if (res?.success && res.data) {
|
||
const formData = res.data as any
|
||
|
||
// 处理责任人数据
|
||
let principalIds: number[] = []
|
||
if (Array.isArray(res.data.projectPrincipals)) {
|
||
principalIds = res.data.projectPrincipals.map((item: any) => item.userId).filter((id: any) => typeof id === 'number')
|
||
}
|
||
|
||
// 处理项目用户数据
|
||
let projectUsers = []
|
||
if (Array.isArray(formData.projectUsers)) {
|
||
projectUsers = formData.projectUsers
|
||
}
|
||
|
||
// 重新赋值整个表单对象,避免响应式问题
|
||
Object.assign(form, {
|
||
id: formData.id,
|
||
projectNo: formData.projectNo || '',
|
||
projectName: formData.projectName || '',
|
||
principalIds: [...principalIds], // 使用展开运算符创建新数组
|
||
projectUsers: [...projectUsers], // 使用展开运算符创建新数组
|
||
status: formData.status !== undefined ? formData.status : true
|
||
})
|
||
|
||
console.log('编辑数据加载:', {
|
||
principalIds: form.principalIds,
|
||
projectUsers: form.projectUsers
|
||
})
|
||
}
|
||
} catch (error) {
|
||
console.error('获取项目详情失败:', error)
|
||
ElMessage.error('获取项目详情失败')
|
||
} finally {
|
||
state.sureLoading = false
|
||
}
|
||
} else {
|
||
// 新增模式,直接重置表单
|
||
Object.assign(form, {
|
||
id: undefined,
|
||
projectNo: '',
|
||
projectName: '',
|
||
principalIds: [],
|
||
projectUsers: [],
|
||
status: true
|
||
})
|
||
}
|
||
|
||
// 确保选项数据已加载
|
||
if (state.principalOptions.length === 0) {
|
||
await getPrincipalOptions()
|
||
}
|
||
}
|
||
|
||
/** 取消 */
|
||
const OnCancel = () => {
|
||
dialogVisible.value = false
|
||
formRef.value?.resetFields()
|
||
}
|
||
|
||
/** 提交表单 */
|
||
const submitForm = async () => {
|
||
if (!formRef.value) return
|
||
await formRef.value.validate(async (valid) => {
|
||
if (valid) {
|
||
state.sureLoading = true
|
||
try {
|
||
const api = new ProjectApi()
|
||
// 转换数据格式以匹配后端API
|
||
const submitData = {
|
||
id: form.id,
|
||
projectNo: form.projectNo,
|
||
projectName: form.projectName,
|
||
status: form.status,
|
||
projectPrincipals: form.principalIds.map((id: number) => ({ userId: id })),
|
||
projectUsers: form.projectUsers.length > 0 ? form.projectUsers : []
|
||
}
|
||
const res = form.id ? await api.update(submitData as any) : await api.add(submitData as any)
|
||
if (res.success) {
|
||
ElMessage.success(form.id ? '修改成功' : '新增成功')
|
||
dialogVisible.value = false
|
||
eventBus.emit('refreshProject')
|
||
}
|
||
} catch (error) {
|
||
console.error('提交失败:', error)
|
||
ElMessage.error('操作失败,请重试')
|
||
} finally {
|
||
state.sureLoading = false
|
||
}
|
||
} else {
|
||
ElMessage.warning('存在未填写的必填项')
|
||
}
|
||
})
|
||
}
|
||
|
||
const handleAddUser = () => {
|
||
userSelectRef.value.open()
|
||
}
|
||
|
||
const onSureUser = async (users: UserGetPageOutput[]) => {
|
||
if (!(users?.length > 0)) {
|
||
userSelectRef.value.close()
|
||
return
|
||
}
|
||
|
||
state.sureLoading = true
|
||
// 过滤掉 id 为 undefined 的用户,保证 userId 类型安全
|
||
const validUsers = users?.filter((u) => typeof u.id === 'number') ?? []
|
||
|
||
// 避免重复添加
|
||
const existingUserIds = form.projectUsers.map((u) => u.userId)
|
||
const newUsers = validUsers.filter((u) => !existingUserIds.includes(u.id as number))
|
||
|
||
const newUserList = newUsers.map((u) => ({
|
||
userId: u.id as number, // 明确断言为 number,避免类型错误
|
||
userName: u.userName ?? '',
|
||
groupName: '',
|
||
limitToDate: '',
|
||
upload: false,
|
||
revokeUpload: false,
|
||
review: false,
|
||
revokeReview: false,
|
||
signature: false,
|
||
revokeSignature: false,
|
||
verify: false,
|
||
revokeVerify: false
|
||
}))
|
||
form.projectUsers.push(...newUserList)
|
||
state.sureLoading = false
|
||
userSelectRef.value.close()
|
||
}
|
||
|
||
/** 删除用户 */
|
||
const handleRemoveUser = (index: number) => {
|
||
form.projectUsers.splice(index, 1)
|
||
}
|
||
|
||
// 监听 principalIds 变化,用于调试
|
||
watch(() => form.principalIds, (newVal, oldVal) => {
|
||
console.log('principalIds 变化:', {
|
||
新值: newVal,
|
||
旧值: oldVal,
|
||
类型: Array.isArray(newVal) ? 'Array' : typeof newVal,
|
||
长度: Array.isArray(newVal) ? newVal.length : 'N/A'
|
||
})
|
||
}, { deep: true })
|
||
|
||
onMounted(async () => {
|
||
// 组件挂载时就加载选项数据,避免弹窗打开时的延迟
|
||
await Promise.all([
|
||
getPrincipalOptions()
|
||
])
|
||
})
|
||
|
||
defineExpose({
|
||
open
|
||
})
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.project-form-container {
|
||
display: flex;
|
||
gap: 20px;
|
||
min-height: 650px;
|
||
}
|
||
|
||
.left-panel {
|
||
flex: 0 0 350px;
|
||
padding: 20px;
|
||
border: 1px solid #e4e7ed;
|
||
border-radius: 6px;
|
||
background-color: #fafafa;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.right-panel {
|
||
flex: 1;
|
||
padding: 20px;
|
||
border: 1px solid #e4e7ed;
|
||
border-radius: 6px;
|
||
background-color: #fafafa;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.panel-title {
|
||
margin: 0 0 20px 0;
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: #303133;
|
||
padding-bottom: 10px;
|
||
border-bottom: 2px solid #409eff;
|
||
}
|
||
|
||
.user-actions {
|
||
margin-bottom: 16px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
:deep(.el-table) {
|
||
border-radius: 6px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
:deep(.el-table .el-table__body-wrapper) {
|
||
overflow-y: auto;
|
||
scrollbar-width: thin;
|
||
scrollbar-color: #c1c1c1 #f1f1f1;
|
||
}
|
||
|
||
:deep(.el-table .el-table__body-wrapper::-webkit-scrollbar) {
|
||
width: 8px;
|
||
}
|
||
|
||
:deep(.el-table .el-table__body-wrapper::-webkit-scrollbar-track) {
|
||
background: #f1f1f1;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
:deep(.el-table .el-table__body-wrapper::-webkit-scrollbar-thumb) {
|
||
background: #c1c1c1;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
:deep(.el-table .el-table__body-wrapper::-webkit-scrollbar-thumb:hover) {
|
||
background: #a0a0a0;
|
||
}
|
||
|
||
.permission-grid {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 8px;
|
||
|
||
.el-checkbox {
|
||
margin-right: 0;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
:deep(.el-dialog) {
|
||
height: 95vh;
|
||
width: 95vw !important;
|
||
}
|
||
|
||
.project-form-container {
|
||
flex-direction: column;
|
||
gap: 16px;
|
||
}
|
||
|
||
.left-panel,
|
||
.right-panel {
|
||
flex: none;
|
||
padding: 16px;
|
||
}
|
||
|
||
.permission-grid {
|
||
grid-template-columns: 1fr;
|
||
gap: 4px;
|
||
}
|
||
}
|
||
</style> |