项目管理弹窗增加项目编号输入;后端保存改为接收客户端编号并校验租户内唯一,不再自动生成。 Co-authored-by: Cursor <cursoragent@cursor.com>
434 lines
16 KiB
Vue
434 lines
16 KiB
Vue
<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>
|