Files
zkxw/web-admin/src/views/project-management/ManagementPage.vue
T
zlwuandCursor e895daf8fc feat(project): 项目编号支持新增编辑自由填写
项目管理弹窗增加项目编号输入;后端保存改为接收客户端编号并校验租户内唯一,不再自动生成。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-11-05 14:41:41 +08:00

434 lines
16 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>
<div class="page-container"
><section class="panel">
<div class="filter-block"
><el-form class="filter-form" label-position="top" @submit.prevent="search">
<div class="filter-grid">
<el-form-item label="关键词"
><el-input v-model="query.keyword" clearable placeholder="名称 / 编号"
/></el-form-item>
<el-form-item v-if="isTeam" label="负责人"
><el-select v-model="query.leaderUserId" filterable clearable
><el-option
v-for="user in refs.users"
:key="user.id"
:value="user.id"
:label="user.name" /></el-select
></el-form-item>
<el-form-item v-if="isTeam" label="启用状态"
><el-select v-model="query.enabled" clearable
><el-option label="启用" :value="true" /><el-option
label="停用"
:value="false" /></el-select
></el-form-item>
<el-form-item v-else label="项目组"
><el-select v-model="query.teamId" filterable clearable
><el-option
v-for="team in teams"
:key="team.id"
:value="team.id"
:label="team.teamName" /></el-select
></el-form-item>
<el-form-item v-if="!isTeam" label="状态"
><el-select v-model="query.status" clearable
><el-option
v-for="(label, value) in statuses"
:key="value"
:value="value"
:label="label" /></el-select
></el-form-item> </div
><div class="filter-actions-row"
><div class="filter-actions-end">
<el-button @click="reset"
><el-icon><RefreshLeft /></el-icon>重置</el-button
>
<el-button type="primary" @click="search"
><el-icon><Search /></el-icon>搜索</el-button
>
</div></div
>
</el-form></div
>
<div class="list-toolbar"
><div class="list-toolbar-row"
><el-button v-if="canEdit" type="primary" @click="edit()"
><el-icon><Plus /></el-icon>新增{{ isTeam ? '项目组' : '项目' }}</el-button
></div
></div
>
<div class="list-body list-wrap"
><el-table :data="rows" v-loading="loading" class="list-table" stripe>
<el-table-column
type="index"
label="序号"
min-width="68"
align="center"
header-align="center"
:index="(index) => (query.page - 1) * query.pageSize + index + 1"
/>
<el-table-column
:prop="isTeam ? 'teamName' : 'projectName'"
:label="isTeam ? '项目组' : '项目名称'"
min-width="180"
/>
<el-table-column v-if="!isTeam" prop="projectNo" label="项目编号" min-width="160" />
<el-table-column v-if="!isTeam" prop="teamName" label="所属项目组" min-width="160" />
<el-table-column prop="leaderName" label="负责人" min-width="120" />
<el-table-column v-if="isTeam" label="成员" min-width="180" show-overflow-tooltip
><template #default="{ row }">{{
joinChildNames(row.members)
}}</template></el-table-column
>
<el-table-column v-if="isTeam" label="负责医院" min-width="200" show-overflow-tooltip
><template #default="{ row }">{{
joinChildNames(row.hospitals)
}}</template></el-table-column
>
<el-table-column label="状态" min-width="110"
><template #default="{ row }"
><el-tag
:type="
isTeam
? row.enabled
? 'success'
: 'info'
: row.status === 'active'
? 'success'
: row.status === 'disabled'
? 'info'
: 'warning'
"
>{{ isTeam ? (row.enabled ? '启用' : '停用') : statuses[row.status] }}</el-tag
></template
></el-table-column
>
<el-table-column prop="remark" label="备注" min-width="180" show-overflow-tooltip />
<el-table-column label="操作" fixed="right" min-width="260"
><template #default="{ row }">
<el-button class="btn-op-item" @click="edit(row, true)"
><el-icon><View /></el-icon>查看</el-button
>
<el-button v-if="canEdit" class="btn-op-item" @click="edit(row)"
><el-icon><Edit /></el-icon>编辑</el-button
>
<el-button v-if="canEdit" class="btn-op-item" @click="remove(row)"
><el-icon><Delete /></el-icon>删除</el-button
>
</template></el-table-column
> </el-table
><div class="pagination list-pager-bar"
><el-pagination
v-model:current-page="query.page"
v-model:page-size="query.pageSize"
:total="total"
layout="total, sizes, prev, pager, next"
@current-change="load"
@size-change="search" /></div
></div>
<el-dialog
v-model="visible"
:title="`${readonly ? '查看' : editingId ? '编辑' : '新增'}${isTeam ? '项目组' : '项目'}`"
:width="isTeam && !readonly ? '960px' : '720px'"
class="dialog-form-layout"
append-to-body
modal-class="protocol-dialog-mask"
:close-on-click-modal="false"
>
<el-form
ref="formRef"
:model="form"
label-position="top"
:disabled="readonly"
scroll-to-error
><div class="form-grid-2">
<el-form-item
:label="isTeam ? '项目组名称' : '项目名称'"
:prop="isTeam ? 'teamName' : 'projectName'"
:rules="[{ required: true, message: '请输入名称' }]"
><el-input v-if="isTeam" v-model="form.teamName" /><el-input
v-else
v-model="form.projectName"
/></el-form-item>
<el-form-item
v-if="!isTeam"
label="项目编号"
prop="projectNo"
:rules="[{ required: true, message: '请输入项目编号' }]"
><el-input v-model="form.projectNo" maxlength="64" show-word-limit
/></el-form-item>
<el-form-item
v-if="!isTeam"
label="所属项目组"
prop="teamId"
:rules="[{ required: true, message: '请选择项目组' }]"
><el-select v-model="form.teamId" filterable @change="onTeamChange"
><el-option
v-for="team in teams"
:key="team.id"
:value="team.id"
:label="team.teamName" /></el-select
></el-form-item>
<el-form-item
label="负责人"
prop="leaderUserId"
:rules="[{ required: true, message: '请选择负责人' }]"
><el-select v-model="form.leaderUserId" filterable clearable
><el-option
v-for="user in refs.users"
:key="user.id"
:value="user.id"
:label="user.name" /></el-select
></el-form-item>
<template v-if="isTeam">
<el-form-item label="成员" :class="{ 'form-col-span-2': !readonly }"
><el-select v-model="memberIds" multiple filterable
><el-option
v-for="user in refs.users"
:key="user.id"
:value="user.id"
:label="user.name" /></el-select
></el-form-item>
<el-form-item label="负责地区(信息记录)" :class="{ 'form-col-span-2': !readonly }"
><el-select v-model="regionIds" multiple filterable
><el-option
v-for="region in refs.regions"
:key="region.id"
:value="region.id"
:label="region.name" /></el-select
></el-form-item>
<el-form-item label="负责医院(信息记录)" :class="{ 'form-col-span-2': !readonly }"
><el-select v-model="hospitalIds" multiple filterable
><el-option
v-for="hospital in refs.hospitals"
:key="hospital.id"
:value="hospital.id"
:label="hospital.name" /></el-select
></el-form-item>
<el-form-item label="启用" :class="{ 'form-col-span-2': !readonly }"
><el-switch v-model="form.enabled"
/></el-form-item>
<div class="form-tip form-col-span-2"
>地区、医院仅记录项目组职责,不改变业务地点候选或数据权限。</div
>
</template>
<template v-else>
<el-form-item
label="业务类型"
prop="bizTypes"
:rules="[{ required: true, type: 'array', min: 1, message: '请选择业务类型' }]"
><el-select v-model="form.bizTypes" multiple
><el-option
v-for="(label, key) in projectBizLabels"
:key="key"
:value="key"
:label="label" /></el-select
></el-form-item>
<el-form-item label="开始日期"
><el-date-picker
v-model="form.startDate"
value-format="YYYY-MM-DD"
style="width: 100%"
/></el-form-item>
<el-form-item label="结束日期"
><el-date-picker
v-model="form.endDate"
value-format="YYYY-MM-DD"
style="width: 100%"
/></el-form-item>
<el-form-item label="状态"
><el-select v-model="form.status"
><el-option
v-for="(label, key) in statuses"
:key="key"
:value="key"
:label="label" /></el-select
></el-form-item>
</template>
<el-form-item label="备注" class="form-col-span-2"
><el-input v-model="form.remark" type="textarea" :rows="3"
/></el-form-item> </div
></el-form>
<template #footer
><el-button @click="visible = false"
><el-icon><Close /></el-icon>关闭</el-button
><el-button v-if="!readonly" type="primary" :loading="saving" @click="save"
><el-icon><Check /></el-icon>保存</el-button
></template
>
</el-dialog>
</section></div
>
</template>
<script setup lang="ts">
import { joinChildNames, managementPayload } from '@/utils/project-management'
import { useMenuPerm } from '@/hooks/core/useMenuPerm'
import {
Plus,
Search,
RefreshLeft,
View,
Edit,
Delete,
Check,
Close
} from '@element-plus/icons-vue'
import {
bizLabels,
fetchManagementList,
fetchManagementDetail,
saveManagement,
deleteManagement,
fetchProjectReferences,
type ProjectRecord
} from '@/api/project-management'
const projectBizLabels = Object.fromEntries(
Object.entries(bizLabels).filter(([key]) => key !== 'presign')
)
const props = defineProps<{ kind: 'teams' | 'projects' }>()
const isTeam = computed(() => props.kind === 'teams')
const { hasMenuPerm } = useMenuPerm()
const canEdit = computed(() =>
hasMenuPerm(isTeam.value ? 'project:team:edit' : 'project:project:edit')
)
const statuses: Record<string, string> = {
pending: '待启动',
active: '进行中',
completed: '已完成',
disabled: '停用'
}
const query = reactive({
keyword: '',
leaderUserId: '',
enabled: undefined as boolean | undefined,
teamId: '',
status: '',
page: 1,
pageSize: 20
})
const rows = ref<ProjectRecord[]>([]),
teams = ref<ProjectRecord[]>([]),
total = ref(0),
loading = ref(false),
visible = ref(false),
saving = ref(false),
readonly = ref(false),
editingId = ref(''),
formRef = ref()
const refs = reactive<{
users: ProjectRecord[]
regions: ProjectRecord[]
hospitals: ProjectRecord[]
}>({ users: [], regions: [], hospitals: [] })
const form = ref<ProjectRecord>({}),
memberIds = ref<string[]>([]),
regionIds = ref<string[]>([]),
hospitalIds = ref<string[]>([])
async function load() {
loading.value = true
try {
const result = await fetchManagementList(props.kind, {
...query,
enabled: typeof query.enabled === 'boolean' ? query.enabled : undefined
})
rows.value = result.list
total.value = result.total
} finally {
loading.value = false
}
}
function search() {
query.page = 1
load()
}
function reset() {
Object.assign(query, {
keyword: '',
leaderUserId: '',
enabled: undefined,
teamId: '',
status: ''
})
search()
}
function onTeamChange(teamId: string) {
if (readonly.value || isTeam.value) return
const team = teams.value.find((item) => item.id === teamId)
if (team?.leaderUserId) form.value.leaderUserId = team.leaderUserId
}
async function edit(row?: ProjectRecord, view = false) {
readonly.value = view
editingId.value = row?.id || ''
form.value = row
? await fetchManagementDetail(props.kind, row.id)
: {
teamName: '',
projectName: '',
projectNo: '',
leaderUserId: '',
enabled: true,
status: 'pending',
bizTypes: [],
remark: ''
}
memberIds.value = (form.value.members || []).map((x: ProjectRecord) => x.userId)
regionIds.value = (form.value.regions || []).map((x: ProjectRecord) => x.regionId)
hospitalIds.value = (form.value.hospitals || []).map((x: ProjectRecord) => x.hospitalId)
visible.value = true
}
function children(key: string, field: string, ids: string[]) {
return ids.map((id) => ({
...(form.value[key] || []).find((x: ProjectRecord) => x[field] === id),
[field]: id
}))
}
async function save() {
await formRef.value.validate()
if (form.value.startDate && form.value.endDate && form.value.startDate > form.value.endDate) {
ElMessage.warning('结束日期不能早于开始日期')
return
}
saving.value = true
try {
const data = { ...form.value }
if (isTeam.value)
Object.assign(data, {
members: children('members', 'userId', memberIds.value),
regions: children('regions', 'regionId', regionIds.value),
hospitals: children('hospitals', 'hospitalId', hospitalIds.value)
})
await saveManagement(
props.kind,
managementPayload(props.kind, data),
editingId.value || undefined
)
visible.value = false
ElMessage.success('保存成功')
await load()
} finally {
saving.value = false
}
}
async function remove(row: ProjectRecord) {
await ElMessageBox.confirm('确认删除?已被业务引用的项目或项目组请改为停用。', '删除确认', {
type: 'warning'
})
await deleteManagement(props.kind, row.id)
await load()
}
onMounted(async () => {
await load()
Object.assign(refs, await fetchProjectReferences())
if (!isTeam.value) {
let page = 1
let teamTotal = Infinity
do {
const result = await fetchManagementList('teams', { page, pageSize: 100 })
teamTotal = result.total
teams.value.push(...result.list)
if (teams.value.length >= result.total || !result.list.length) break
page++
} while (teams.value.length < teamTotal)
}
})
</script>