- 三个详情页的编辑 el-dialog 统一改为 el-drawer(direction=rtl, size=600) - 保留原表单与 footer 按钮,destroy-on-close 语义不变
355 lines
12 KiB
Vue
355 lines
12 KiB
Vue
<script setup lang="ts">
|
||
/**
|
||
* 工作组详情页 WorkgroupDetail
|
||
*
|
||
* 迁移自 TempLate/GERP/pages/Company/workgroup-detail.html
|
||
*
|
||
* 业务结构:
|
||
* 1. 基本信息(InfoGrid):工作组名称 / 英文名称 / 所属公司 / 工作组介绍
|
||
* 2. 工作组角色子表(CrudTable: WorkgroupRole,按 WorkgroupUUID 过滤)
|
||
* - 列:RoleUUID(entityPicker Role) / RoleType(0组员/1组长) / CreateTime
|
||
* 3. actions:编辑按钮(动态编辑弹窗)
|
||
*
|
||
* 迁移要点:
|
||
* - server / admin_verify 由 request.ts 拦截器统一注入
|
||
* - 主表单条加载:formApi.list('Workgroup', { WorkgroupUUID, pageSize: 1 })
|
||
* - UUID 解析:所属公司 CompanyUUID → CompanyName,使用 useResolveUUIDs composable(带缓存)
|
||
* - 原生 buildApiUrl 无 admin_verify 分支(本页未启用特权模式),拦截器按 URL 自动处理
|
||
*/
|
||
import { ref, reactive, computed, onMounted, watch } from 'vue'
|
||
import { useRoute, useRouter } from 'vue-router'
|
||
import { ElMessage } from 'element-plus'
|
||
import DetailPage from '@/components/DetailPage.vue'
|
||
import InfoGrid from '@/components/InfoGrid.vue'
|
||
import CrudTable from '@/components/CrudTable.vue'
|
||
import EntityPicker from '@/components/EntityPicker.vue'
|
||
import BatchPositionDialog from '@/components/BatchPositionDialog.vue'
|
||
import { formApi } from '@/api/form'
|
||
import { useResolveUUIDs } from '@/composables/useResolveUUIDs'
|
||
import { setPageTitle } from '@/utils/serverScope'
|
||
import type { ColumnSchema } from '@/types/api'
|
||
|
||
const route = useRoute()
|
||
const router = useRouter()
|
||
const { resolveUUIDs } = useResolveUUIDs()
|
||
|
||
// 路由参数:工作组 UUID(computed 响应式:同一 tab 复用且 query 变化时自动更新)
|
||
const workgroupUUID = computed(() => (route.query.WorkgroupUUID as string) || '')
|
||
|
||
// === 状态 ===
|
||
const loading = ref(false)
|
||
const data = ref<Record<string, any> | null>(null)
|
||
// 所属公司名称(UUID 解析结果)
|
||
const companyName = ref('')
|
||
|
||
// 编辑弹窗状态
|
||
const editDialogVisible = ref(false)
|
||
const editSubmitting = ref(false)
|
||
const columns = ref<ColumnSchema[]>([])
|
||
const editForm = reactive<Record<string, any>>({})
|
||
|
||
// 主键字段
|
||
const PK_FIELD = 'WorkgroupUUID'
|
||
|
||
// tags 表单:逗号分隔字符串 ↔ 多选数组互转(工作组标签 WorkgroupTags)
|
||
function splitTags(value: unknown, separator = ','): string[] {
|
||
if (!value) return []
|
||
return String(value).split(separator).map((s) => s.trim()).filter(Boolean)
|
||
}
|
||
function joinTags(values: string[], separator = ','): string {
|
||
return (values || []).filter(Boolean).join(separator)
|
||
}
|
||
|
||
// 页面标题
|
||
const title = computed(() => data.value?.WorkgroupName || '工作组详情')
|
||
|
||
// === 数据加载 ===
|
||
|
||
async function loadData(): Promise<void> {
|
||
if (!workgroupUUID.value) {
|
||
// 无 WorkgroupUUID 参数:不弹警告(首次无参进入/菜单误入均静默),显示空态引导
|
||
data.value = null
|
||
setPageTitle('工作组详情')
|
||
return
|
||
}
|
||
loading.value = true
|
||
try {
|
||
const result = await formApi.list('Workgroup', { [PK_FIELD]: workgroupUUID.value, pageSize: 1 })
|
||
const list: any[] = Array.isArray(result) ? result : (result as any)?.list || []
|
||
const row = list.find((w) => w[PK_FIELD] === workgroupUUID.value) || list[0]
|
||
if (!row) {
|
||
ElMessage.error('未找到工作组数据')
|
||
return
|
||
}
|
||
data.value = row
|
||
setPageTitle(row.WorkgroupName || '工作组详情')
|
||
// 解析所属公司 UUID → 名称
|
||
await resolveCompany(row.CompanyUUID)
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
/** 解析 CompanyUUID → CompanyName */
|
||
async function resolveCompany(companyUUID?: string): Promise<void> {
|
||
if (!companyUUID) {
|
||
companyName.value = ''
|
||
return
|
||
}
|
||
try {
|
||
const resolved = await resolveUUIDs({ company: { table: 'Company', uuids: [companyUUID] } })
|
||
companyName.value = resolved.company?.[companyUUID] || companyUUID
|
||
} catch {
|
||
companyName.value = companyUUID
|
||
}
|
||
}
|
||
|
||
async function loadColumns(): Promise<void> {
|
||
try {
|
||
columns.value = await formApi.getColumns('Workgroup')
|
||
} catch {
|
||
// 列定义加载失败不阻塞展示
|
||
}
|
||
}
|
||
|
||
// === 编辑弹窗 ===
|
||
|
||
const formColumns = computed(() =>
|
||
columns.value.filter(
|
||
(c) =>
|
||
c.editable !== false &&
|
||
!c.hideInForm &&
|
||
c.type !== 'hidden' &&
|
||
c.type !== 'divider' &&
|
||
c.type !== 'html' &&
|
||
c.field !== PK_FIELD,
|
||
),
|
||
)
|
||
|
||
function handleEdit(): void {
|
||
if (!data.value) return
|
||
Object.keys(editForm).forEach((k) => delete editForm[k])
|
||
Object.assign(editForm, data.value)
|
||
editDialogVisible.value = true
|
||
}
|
||
|
||
async function handleSubmit(): Promise<void> {
|
||
editSubmitting.value = true
|
||
try {
|
||
await formApi.update('Workgroup', editForm[PK_FIELD], editForm)
|
||
ElMessage.success('保存成功')
|
||
editDialogVisible.value = false
|
||
await loadData()
|
||
} finally {
|
||
editSubmitting.value = false
|
||
}
|
||
}
|
||
|
||
// === 子表跳转 ===
|
||
|
||
/** UserRole 子表查看 → 用户详情 */
|
||
function goUserDetail(row: Record<string, any>): void {
|
||
router.push({ path: '/system/user-detail', query: { UserUUID: row.UserUUID || '' } })
|
||
}
|
||
|
||
// === 批量调整职务 ===
|
||
const wgTableRef = ref()
|
||
const batchDialogVisible = ref(false)
|
||
const batchPreselected = ref<Array<{ pk: string; label: string }>>([])
|
||
|
||
/** 打开批量职务弹窗:有选中行 → 应用到选中行;无选中 → 弹窗内选工作组成员 */
|
||
function openBatchDialog(): void {
|
||
const rows: Array<Record<string, any>> = wgTableRef.value?.selectedRows ?? []
|
||
batchPreselected.value = rows.map((r) => ({
|
||
pk: String(r.UserRoleWorkgroupUUID ?? ''),
|
||
label:
|
||
`${String((r.RoleNameCover || r.UserRoleUUID_label) ?? '')}${r.UserUUID_label ? `(${String(r.UserUUID_label)})` : ''}` ||
|
||
String(r.UserRoleUUID ?? ''),
|
||
}))
|
||
batchDialogVisible.value = true
|
||
}
|
||
|
||
onMounted(async () => {
|
||
await Promise.all([loadData(), loadColumns()])
|
||
})
|
||
|
||
// 同一 tab 复用(v-show 常驻)时 query 变化:重新加载(防展示旧工作组数据 / 缺参报错)
|
||
// immediate 使首次挂载也走统一逻辑(与 onMounted 的 loadData 幂等);
|
||
// 空值场景静默显示空态(loadData 内处理),不再弹警告
|
||
watch(workgroupUUID, () => {
|
||
void loadData()
|
||
}, { immediate: true })
|
||
</script>
|
||
|
||
<template>
|
||
<DetailPage :title="title" :loading="loading">
|
||
<template #actions>
|
||
<el-button type="primary" @click="handleEdit">
|
||
<el-icon><Edit /></el-icon>编辑
|
||
</el-button>
|
||
</template>
|
||
|
||
<!-- 无 WorkgroupUUID 参数或数据未加载成功:空态引导 -->
|
||
<el-empty
|
||
v-if="!data"
|
||
description="工作组数据未加载成功,请确认登录状态后从工作组列表进入详情"
|
||
:image-size="80"
|
||
/>
|
||
|
||
<template v-else>
|
||
<!-- 基本信息 -->
|
||
<InfoGrid title="基本信息" :columns="2">
|
||
<el-descriptions-item label="工作组名称">{{ data?.WorkgroupName || '-' }}</el-descriptions-item>
|
||
<el-descriptions-item label="英文名称">{{ data?.WorkgroupEnglishName || '-' }}</el-descriptions-item>
|
||
<el-descriptions-item label="所属公司">
|
||
{{ companyName || data?.CompanyUUID || '-' }}
|
||
</el-descriptions-item>
|
||
<el-descriptions-item label="工作组介绍">{{ data?.WorkgroupIntroduction || '-' }}</el-descriptions-item>
|
||
</InfoGrid>
|
||
|
||
<!-- 工作组员工子表(UserRoleWorkgroup 表:用户-角色对 ↔ 工作组多对多) -->
|
||
<el-card shadow="never" class="detail-card">
|
||
<template #header>
|
||
<div class="detail-card-header">
|
||
<el-icon><Avatar /></el-icon>
|
||
<span>工作组员工</span>
|
||
</div>
|
||
</template>
|
||
<CrudTable
|
||
v-if="workgroupUUID"
|
||
ref="wgTableRef"
|
||
table="UserRoleWorkgroup"
|
||
title="工作组员工"
|
||
:query="{ WorkgroupUUID: workgroupUUID, CompanyUUID: data?.CompanyUUID || '' }"
|
||
:pk-field="'UserRoleWorkgroupUUID'"
|
||
selectable
|
||
pageable
|
||
:max-height="400"
|
||
:on-view="goUserDetail"
|
||
>
|
||
<template #toolbar="{ refresh }">
|
||
<el-button type="warning" @click="openBatchDialog">
|
||
<el-icon><Edit /></el-icon>批量调整职务
|
||
</el-button>
|
||
<BatchPositionDialog
|
||
v-model:visible="batchDialogVisible"
|
||
table="UserRoleWorkgroup"
|
||
:context="{ WorkgroupUUID: workgroupUUID, CompanyUUID: data?.CompanyUUID || '' }"
|
||
:preselected="batchPreselected"
|
||
title="工作组员工批量调整职务"
|
||
@success="refresh"
|
||
/>
|
||
</template>
|
||
</CrudTable>
|
||
</el-card>
|
||
</template>
|
||
|
||
<!-- 编辑抽屉 -->
|
||
<el-drawer
|
||
v-model="editDialogVisible"
|
||
title="编辑工作组"
|
||
direction="rtl"
|
||
:size="600"
|
||
destroy-on-close
|
||
>
|
||
<el-form :model="editForm" label-width="120px" label-position="right">
|
||
<el-form-item
|
||
v-for="col in formColumns"
|
||
:key="col.field"
|
||
:label="col.title"
|
||
:required="col.required"
|
||
>
|
||
<EntityPicker
|
||
v-if="col.type === 'entityPicker' && col.entityPicker"
|
||
v-model="editForm[col.field]"
|
||
:config="col.entityPicker"
|
||
:existing-label="editForm[col.field + '_label']"
|
||
:placeholder="col.placeholder"
|
||
:disabled="col.disabled"
|
||
@update:label="(label: string) => (editForm[col.field + '_label'] = label)"
|
||
/>
|
||
<el-input
|
||
v-else-if="!col.type || col.type === 'text' || col.type === 'string'"
|
||
v-model="editForm[col.field]"
|
||
:placeholder="col.placeholder"
|
||
:maxlength="col.maxLength"
|
||
:disabled="col.disabled"
|
||
/>
|
||
<el-input
|
||
v-else-if="col.type === 'textarea'"
|
||
v-model="editForm[col.field]"
|
||
type="textarea"
|
||
:rows="col.rows || 3"
|
||
:disabled="col.disabled"
|
||
/>
|
||
<el-input-number
|
||
v-else-if="col.type === 'number'"
|
||
v-model="editForm[col.field]"
|
||
:min="col.min"
|
||
:max="col.max"
|
||
:step="col.step"
|
||
:disabled="col.disabled"
|
||
/>
|
||
<el-select
|
||
v-else-if="col.type === 'select'"
|
||
v-model="editForm[col.field]"
|
||
:placeholder="col.placeholder"
|
||
:disabled="col.disabled"
|
||
style="width: 100%"
|
||
>
|
||
<el-option
|
||
v-for="opt in col.options"
|
||
:key="String(opt.value)"
|
||
:label="opt.label"
|
||
:value="opt.value"
|
||
/>
|
||
</el-select>
|
||
<!-- tags:多选标签(预设可选项 + 允许自定义新增),逗号分隔存储 -->
|
||
<el-select
|
||
v-else-if="col.type === 'tags'"
|
||
:model-value="splitTags(editForm[col.field], col.tagsSeparator)"
|
||
@update:model-value="(v: string[]) => (editForm[col.field] = joinTags(v, col.tagsSeparator))"
|
||
:placeholder="col.placeholder"
|
||
:disabled="col.disabled"
|
||
multiple
|
||
filterable
|
||
allow-create
|
||
default-first-option
|
||
style="width: 100%"
|
||
>
|
||
<el-option
|
||
v-for="opt in col.options ?? []"
|
||
:key="String(opt.value)"
|
||
:label="opt.label"
|
||
:value="opt.value"
|
||
/>
|
||
</el-select>
|
||
<el-date-picker
|
||
v-else-if="col.type === 'date' || col.type === 'datetime'"
|
||
v-model="editForm[col.field]"
|
||
:type="col.type as 'date' | 'datetime'"
|
||
:placeholder="col.placeholder"
|
||
style="width: 100%"
|
||
/>
|
||
<el-switch
|
||
v-else-if="col.type === 'switch' || col.type === 'boolean'"
|
||
v-model="editForm[col.field]"
|
||
:disabled="col.disabled"
|
||
/>
|
||
<el-input
|
||
v-else
|
||
:model-value="String(editForm[col.field] ?? '')"
|
||
readonly
|
||
/>
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button @click="editDialogVisible = false">取消</el-button>
|
||
<el-button type="primary" :loading="editSubmitting" @click="handleSubmit">保存</el-button>
|
||
</template>
|
||
</el-drawer>
|
||
</DetailPage>
|
||
</template>
|
||
|
||
<style scoped lang="scss"></style>
|