add 项目管理

This commit is contained in:
Asoka.Wang 2025-06-19 20:15:27 +08:00
parent 09eb888093
commit c7a5299466
4 changed files with 436 additions and 125 deletions

View File

@ -1,6 +1,7 @@
import { ServiceResponse } from './response';
import { ServiceRequestPage } from './pageInput'
import { PageResponse } from './pageResponse'
import { ProjectUser, ProjectPrincipal } from './projectUserType'
// 过滤条件
export interface ProjectFilter {
@ -30,8 +31,18 @@ export interface ProjectDto {
status?: boolean
/** 创建时间 */
createdTime?: string | null
/** 创建人姓名 */
createdUserName?: string | null
/** 修改时间 */
modifiedTime?: string | null
/** 项目成员 */
projectUsers?: ProjectUser[]
/** 项目负责人 */
projectPrincipals?: ProjectPrincipal[]
}
/** 项目添加和更新输入接口 */
@ -48,11 +59,17 @@ export interface ProjectAddInputAndUpdateInput {
principalId?: number
/** 状态 */
status?: boolean
/** 项目成员 */
projectUsers?: ProjectUser[]
/** 项目负责人 */
projectPrincipals?: ProjectPrincipal[]
}
// API 类型定义
export type ProjectPageInput = ServiceRequestPage<ProjectFilter>;
export type ProjectPageResponse = PageResponse<ProjectDto>;
export type ProjectPageResponse = ServiceResponse<PageResponse<ProjectDto>>;
export type ProjectOutput = ServiceResponse<ProjectDto>;
export type ProjectAddInput = ProjectAddInputAndUpdateInput;
export type ProjectUpdateInput = ProjectAddInputAndUpdateInput;

View File

@ -0,0 +1,61 @@
/**
*
*/
/**
*
*/
export interface ProjectUser {
/** ID */
id?: number;
/** 项目ID */
projectId?: number;
/** 所属组ID */
groupId?: number;
/** 用户ID */
userId: number;
/** 用户过期时间 */
limitToDate?: string;
/** 上传权限 */
upload: boolean;
/** 撤回上传权限 */
revokeUpload: boolean;
/** 审核权限 */
review: boolean;
/** 撤回审核权限 */
revokeReview: boolean;
/** 签名权限 */
signature: boolean;
/** 撤回签名权限 */
revokeSignature: boolean;
/** 审核(二)权限 */
verify: boolean;
/** 撤回审核(二)权限 */
revokeVerify: boolean;
}
/**
*
*/
export interface ProjectPrincipal {
/** ID */
id?: number;
/** 项目ID */
projectId?: number;
/** 用户ID */
userId: number;
}

View File

@ -1,43 +1,128 @@
<template>
<el-dialog v-model="dialogVisible" :title="title" width="600px" append-to-body class="project-form-dialog">
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px" class="project-form">
<el-form-item label="部门" prop="departmentId">
<el-select v-model="form.departmentId" placeholder="请选择部门" style="width: 100%" filterable>
<el-option v-for="item in state.departmentOptions" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item label="项目编号" prop="projectNo">
<el-input v-model="form.projectNo" placeholder="请输入项目编号" />
</el-form-item>
<el-form-item label="项目名称" prop="projectName">
<el-input v-model="form.projectName" placeholder="请输入项目名称" />
</el-form-item>
<el-form-item label="责任人" prop="principalId">
<el-select v-model="form.principalId" placeholder="请选择责任人" style="width: 100%" filterable>
<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>
<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 prop="userName" label="用户名称" min-width="120" />
<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>
<el-button @click="OnCancel" size="default"> </el-button>
<el-button type="primary" :loading="state.sureLoading" @click="submitForm" size="default"> </el-button>
<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 } from 'vue'
import { ref, reactive, onMounted, getCurrentInstance, defineAsyncComponent } from 'vue'
import { ElMessage } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
import { ProjectApi } from '../../../../api/admin/ProjectApi'
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<{
@ -46,42 +131,51 @@ const props = defineProps<{
const dialogVisible = ref(false)
const formRef = ref<FormInstance>()
const form = reactive<ProjectAddInputAndUpdateInput>({
departmentId: undefined,
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: '',
principalId: undefined,
principalIds: [],
projectUsers: [],
status: true
})
const state = reactive({
sureLoading: false,
departmentOptions: [] as Array<{ id: number; name: string }>,
principalOptions: [] as Array<{ id: number; name: string }>
})
const rules = reactive<FormRules>({
departmentId: [{ required: true, message: '请选择部门', trigger: 'change' }],
projectNo: [{ required: true, message: '请输入项目编号', trigger: 'blur' }],
projectName: [{ required: true, message: '请输入项目名称', trigger: 'blur' }],
principalId: [{ required: true, message: '请选择责任人', trigger: 'change' }]
projectName: [{ required: true, message: '请输入项目名称', trigger: 'blur' }]
})
/** 获取部门列表 */
const getDepartmentOptions = async () => {
try {
const res = await new OrgApi().getList()
if (res?.success && res.data) {
state.departmentOptions = res.data.map((item: any) => ({
id: item.id,
name: item.name
}))
}
} catch (error) {
console.error('获取部门列表失败:', error)
}
}
/** 获取负责人列表 */
const getPrincipalOptions = async () => {
try {
@ -101,30 +195,55 @@ const getPrincipalOptions = async () => {
}
const open = async (id?: number) => {
proxy.$modal.loading()
// loading
dialogVisible.value = true
// IDloading
if (id && id > 0) {
const res = await new ProjectApi().get({ id }, { loading: true })
if (res?.success && res.data) {
const formData = res.data as unknown as ProjectAddInputAndUpdateInput
Object.assign(form, formData)
state.sureLoading = true
try {
const res = await new ProjectApi().get({ id }, { loading: false })
if (res?.success && res.data) {
const formData = res.data as any
form.principalIds = Array.isArray(res.data.projectPrincipals)
? res.data.projectPrincipals.map((item: any) => item.userId)
: []
// TODO
console.log("form.principalIds", form.principalIds)
form.projectUsers = Array.isArray(formData.projectUsers) ? formData.projectUsers : []
form.projectUsers.forEach((item: any) => {
item.userName = state.principalOptions.find((a) => a.id === item.userId)?.name || ''
})
Object.assign(form, {
id: formData.id,
projectNo: formData.projectNo || '',
projectName: formData.projectName || '',
principalIds: formData.projectPrincipalIds || [],
projectUsers: formData.projectUsers || [],
status: formData.status !== undefined ? formData.status : true
})
}
} catch (error) {
console.error('获取项目详情失败:', error)
} finally {
state.sureLoading = false
}
} else {
//
Object.assign(form, {
departmentId: undefined,
projectNo: '',
projectName: '',
principalId: undefined,
principalIds: [],
projectUsers: [],
status: true
})
}
proxy.$modal.closeLoading()
dialogVisible.value = true
await Promise.all([
getDepartmentOptions(),
getPrincipalOptions()
])
if (state.principalOptions.length === 0) {
await getPrincipalOptions()
}
}
/** 取消 */
@ -141,7 +260,16 @@ const submitForm = async () => {
state.sureLoading = true
try {
const api = new ProjectApi()
const res = form.id ? await api.update(form) : await api.add(form)
// 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
@ -159,9 +287,53 @@ const submitForm = async () => {
})
}
onMounted(() => {
getDepartmentOptions()
getPrincipalOptions()
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)
}
onMounted(async () => {
//
await Promise.all([
getPrincipalOptions()
])
})
defineExpose({
@ -170,15 +342,105 @@ defineExpose({
</script>
<style lang="scss" scoped>
.project-form-dialog {
:deep(.el-dialog__body) {
padding: 20px 30px;
.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;
}
}
.project-form {
.el-form-item {
margin-bottom: 22px;
@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>

View File

@ -1,37 +1,27 @@
<template>
<my-layout>
<el-card class="box-card">
<template #header>
<div class="card-header">
<el-form :inline="true" :model="state.filter" class="demo-form-inline">
<el-form-item label="关键字">
<el-input v-model="state.filter.keyWord" placeholder="请输入关键字" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="部门">
<el-select v-model="state.filter.departmentId" placeholder="请选择部门" clearable filterable>
<el-option v-for="item in state.departmentOptions" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleQuery">查询</el-button>
<el-button @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-button v-auth="'api:admin:project:add'" type="primary" @click="handleAdd">新增</el-button>
</div>
</template>
<!-- 查询区 -->
<el-card class="my-query-box mt8" shadow="never" :body-style="{ paddingBottom: '0' }">
<el-form :inline="true" label-width="auto" :model="state.filter" class="demo-form-inline" @submit.stop.prevent>
<el-form-item label="关键字">
<el-input v-model="state.filter.keyWord" placeholder="请输入关键字" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="ele-Search" @click="handleQuery"> 查询 </el-button>
<el-button v-auth="'api:admin:project:add'" type="primary" icon="ele-Plus" @click="handleAdd"> 新增 </el-button>
</el-form-item>
</el-form>
</el-card>
<el-table v-loading="loading" :data="state.projectList" style="width: 100%">
<el-table-column type="index" label="序号" width="60" />
<!-- 表格区 -->
<el-card class="my-fill mt8" shadow="never">
<div class="my-tools-box mb8 my-flex my-flex-between">
<!-- 可添加工具栏内容如批量操作等 -->
</div>
<el-table v-loading="loading" :data="state.projectList" style="width: 100%" border row-key="projectNo">
<el-table-column prop="projectNo" label="项目编号" />
<el-table-column prop="projectName" label="项目名称" />
<el-table-column prop="departmentName" label="部门" />
<el-table-column prop="principalName" label="责任人" />
<el-table-column prop="status" label="状态" width="80">
<template #default="{ row }">
<el-tag :type="row.status ? 'success' : 'danger'">{{ row.status ? '启用' : '禁用' }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="createdUserName" label="创建人" />
<el-table-column prop="createdTime" label="创建时间" width="180" />
<el-table-column label="操作" width="180" fixed="right" header-align="center" align="center">
<template #default="{ row }">
@ -72,14 +62,12 @@ const state = reactive({
total: 0,
filter: {
keyWord: '',
departmentId: undefined as number | undefined
},
pageInput: {
currentPage: 1,
pageSize: 20,
} as ProjectPageInput,
projectList: [] as Array<ProjectDto>,
departmentOptions: [] as Array<{ id: number; name: string }>
projectList: [] as Array<ProjectDto>
})
/** 查询列表 */
@ -88,8 +76,8 @@ const getList = async () => {
state.pageInput.filter = state.filter
try {
const res = await new ProjectApi().getPage(state.pageInput)
state.projectList = res.list || []
state.total = res.total || 0
state.projectList = res.data?.list || []
state.total = res.data?.total || 0
} catch (error) {
console.error('获取列表失败:', error)
} finally {
@ -97,21 +85,6 @@ const getList = async () => {
}
}
/** 获取部门列表 */
const getDepartmentOptions = async () => {
try {
const res = await new OrgApi().getList()
if (res?.success && res.data) {
state.departmentOptions = res.data.map((item: any) => ({
id: item.id,
name: item.name
}))
}
} catch (error) {
console.error('获取部门列表失败:', error)
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
state.pageInput.currentPage = 1
@ -122,7 +95,6 @@ const handleQuery = () => {
const resetQuery = () => {
state.filter = {
keyWord: '',
departmentId: undefined
}
handleQuery()
}
@ -172,7 +144,6 @@ const handleCurrentChange = (val: number) => {
onMounted(() => {
getList()
getDepartmentOptions()
eventBus.on('refreshProject', getList)
})
</script>