491 lines
14 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>