Files
GERP/src/views/company/WorkgroupDetail.vue
T
TermiNexus 43a03857d1 refactor(company): 公司/部门/工作组编辑改为右侧抽屉
- 三个详情页的编辑 el-dialog 统一改为 el-drawer(direction=rtl, size=600)

- 保留原表单与 footer 按钮,destroy-on-close 语义不变
2026-09-29 12:01:02 +08:00

355 lines
12 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.
<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>