feat(gerp): 新增子表单控件,员工档案展示新字段
CrudFormPanel 支持 type=subform(明细子表可增删行,明细数组随主表提交);新增 SubFormTable 组件;types 增加 SubFormConfig;UserDetail 档案面板按后端下发选项展示(布尔显示是/否、多选顿号分隔、部门名走关联标签、联系方式明细单独成块,类型名取后端选项)。
This commit is contained in:
1 parent
f806106c88
commit
6bb1edba88
4 files changed
+264
-14
No files matched your search
@@ -5,6 +5,7 @@ import type { FormInstance, FormRules } from 'element-plus'
|
||||
import type { ColumnSchema } from '@/types/api'
|
||||
import EntityPicker from './EntityPicker.vue'
|
||||
import MultiEntityPicker from './MultiEntityPicker.vue'
|
||||
import SubFormTable from './SubFormTable.vue'
|
||||
import RichTextEditor from './RichTextEditor.vue'
|
||||
import { get } from '@/api/request'
|
||||
|
||||
@@ -173,6 +174,13 @@ watch(
|
||||
:disabled="col.disabled"
|
||||
@update:label="(label: string) => onMultiLabel(col.field, label)"
|
||||
/>
|
||||
<!-- subform:子表单(明细子表,如员工档案的「联系方式」),明细行随主表一起提交 -->
|
||||
<SubFormTable
|
||||
v-else-if="col.type === 'subform'"
|
||||
v-model="formModel[col.field]"
|
||||
:config="col.subform"
|
||||
:disabled="col.disabled"
|
||||
/>
|
||||
<!-- entityPicker:EntityPicker 组件(支持级联:dependsOn 字段值 + dependCompany 公司值) -->
|
||||
<EntityPicker
|
||||
v-else-if="col.type === 'entityPicker' && col.entityPicker && !col.multiValue"
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* SubFormTable 子表单(明细子表)
|
||||
*
|
||||
* 用于 ColumnSchema.type === 'subform' 的列:把「主表一条记录 → N 条明细」渲染成
|
||||
* 可增删行的明细区(如员工档案的「联系方式」)。
|
||||
*
|
||||
* 值模型:v-model 绑定的就是明细行数组(挂在主表 formModel[field] 上),
|
||||
* 随主表一起提交;后端保存主表后按该数组整体重写子表。
|
||||
*
|
||||
* 明细行字段定义由后端下发(col.subform.fields),此处只负责渲染与增删,
|
||||
* 不额外定义任何业务字段。
|
||||
*/
|
||||
import { computed } from 'vue'
|
||||
import type { ColumnSchema, SubFormConfig } from '@/types/api'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
/** 明细行数组(v-model) */
|
||||
modelValue?: Record<string, unknown>[]
|
||||
/** 子表单配置(标题/按钮文案/明细行字段定义) */
|
||||
config?: SubFormConfig
|
||||
/** 是否禁用(禁用时不能增删改) */
|
||||
disabled?: boolean
|
||||
}>(), {
|
||||
modelValue: () => [],
|
||||
config: undefined,
|
||||
disabled: false,
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', rows: Record<string, unknown>[]): void
|
||||
}>()
|
||||
|
||||
/** 实际可渲染的明细行字段(跳过 hideInForm/hidden:如 ContactSort 由后端按下标重排) */
|
||||
const fields = computed<ColumnSchema[]>(() =>
|
||||
(props.config?.fields || []).filter(
|
||||
(f) => f.hideInForm !== true && f.type !== 'hidden' && f.type !== 'divider' && f.type !== 'html',
|
||||
),
|
||||
)
|
||||
|
||||
/** 明细行(始终是数组,避免 undefined 导致表格报错) */
|
||||
const rows = computed<Record<string, unknown>[]>(() => props.modelValue || [])
|
||||
|
||||
/** 更新明细行(整体替换:新增/删除/改值都走这里,父组件 formModel 同步) */
|
||||
function commit(next: Record<string, unknown>[]): void {
|
||||
emit('update:modelValue', next)
|
||||
}
|
||||
|
||||
/** 新增一行:按字段默认值初始化(select 取首项,其余空) */
|
||||
function addRow(): void {
|
||||
const row: Record<string, unknown> = {}
|
||||
for (const f of fields.value) {
|
||||
row[f.field] = f.type === 'select' ? (f.options?.[0]?.value ?? '') : ''
|
||||
}
|
||||
commit([...rows.value, row])
|
||||
}
|
||||
|
||||
/** 删除第 index 行 */
|
||||
function removeRow(index: number): void {
|
||||
const next = rows.value.slice()
|
||||
next.splice(index, 1)
|
||||
commit(next)
|
||||
}
|
||||
|
||||
/** 单元格取值变化:只改对应行的对应字段 */
|
||||
function updateCell(index: number, field: string, value: unknown): void {
|
||||
const next = rows.value.slice()
|
||||
next[index] = { ...next[index], [field]: value }
|
||||
commit(next)
|
||||
}
|
||||
|
||||
/** 单元格宽度:select 稍宽,数字/日期窄一点 */
|
||||
function colWidth(field: ColumnSchema): number {
|
||||
if (field.type === 'select') return 160
|
||||
if (field.type === 'number' || field.type === 'date' || field.type === 'datetime') return 140
|
||||
return 220
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="sub-form-table">
|
||||
<div class="sub-form-head">
|
||||
<span class="sub-form-title">{{ config?.title || '明细' }}({{ rows.length }})</span>
|
||||
<el-button type="primary" plain size="small" :disabled="disabled" @click="addRow">
|
||||
<el-icon><Plus /></el-icon>{{ config?.addText || '添加一行' }}
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<el-table :data="rows" size="small" border class="sub-form-body">
|
||||
<el-table-column
|
||||
v-for="f in fields"
|
||||
:key="f.field"
|
||||
:label="f.title"
|
||||
:min-width="colWidth(f)"
|
||||
>
|
||||
<template #default="{ row, $index }">
|
||||
<el-select
|
||||
v-if="f.type === 'select'"
|
||||
:model-value="row[f.field]"
|
||||
:placeholder="f.placeholder || `选择${f.title}`"
|
||||
:disabled="disabled"
|
||||
style="width: 100%"
|
||||
@update:model-value="(v: unknown) => updateCell($index, f.field, v)"
|
||||
>
|
||||
<el-option
|
||||
v-for="opt in f.options || []"
|
||||
:key="String(opt.value)"
|
||||
:value="opt.value"
|
||||
:label="opt.label"
|
||||
/>
|
||||
</el-select>
|
||||
<el-input-number
|
||||
v-else-if="f.type === 'number'"
|
||||
:model-value="row[f.field] as number"
|
||||
:disabled="disabled"
|
||||
controls-position="right"
|
||||
style="width: 100%"
|
||||
@update:model-value="(v: number | undefined) => updateCell($index, f.field, v)"
|
||||
/>
|
||||
<el-input
|
||||
v-else
|
||||
:model-value="String(row[f.field] ?? '')"
|
||||
:placeholder="f.placeholder || `请输入${f.title}`"
|
||||
:disabled="disabled"
|
||||
@update:model-value="(v: string) => updateCell($index, f.field, v)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="操作" width="80" align="center">
|
||||
<template #default="{ $index }">
|
||||
<el-button link type="danger" :disabled="disabled" @click="removeRow($index)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 空态:表格自带空数据展示,这里给更明确的引导 -->
|
||||
<template #empty>
|
||||
<span class="sub-form-empty">暂无明细,点「{{ config?.addText || '添加一行' }}」新增</span>
|
||||
</template>
|
||||
</el-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.sub-form-table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sub-form-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.sub-form-title {
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.sub-form-body {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sub-form-empty {
|
||||
color: var(--el-text-color-placeholder);
|
||||
font-size: 13px;
|
||||
}
|
||||
</style>
|
||||
+23
-1
@@ -107,6 +107,24 @@ export interface EntityPickerConfig {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 子表单配置(ColumnSchema.type === 'subform')
|
||||
*
|
||||
* 语义:主表一条记录对应 N 条明细(如员工档案 → 联系方式)。表单里渲染成一个
|
||||
* 「可增删行的明细区」,明细行数组挂在主表表单的 `formModel[该列 field]` 上,
|
||||
* 随主表一起提交(后端在保存主表后整体重写子表)。
|
||||
*/
|
||||
export interface SubFormConfig {
|
||||
/** 子表名(对应后端 table,便于调试/扩展) */
|
||||
table?: string
|
||||
/** 区块标题(默认取列 title) */
|
||||
title?: string
|
||||
/** 「添加」按钮文案 */
|
||||
addText?: string
|
||||
/** 明细行字段定义(复用列结构:field/title/type/options/required/placeholder) */
|
||||
fields: ColumnSchema[]
|
||||
}
|
||||
|
||||
/**
|
||||
* 后端 FormAPI Columns 返回的列定义
|
||||
* 兼容旧 GERP TableEngine 的 column schema,前端用 vxe-table / FormKit 渲染
|
||||
@@ -128,7 +146,7 @@ export interface ColumnSchema {
|
||||
* 控件/渲染类型
|
||||
* 渲染 + 编辑共用:text/string/number/textarea/select/date/datetime/time/
|
||||
* boolean/switch/checkbox/radio/tags/password/email/tel/phone/url/color/
|
||||
* file/image/hidden/entityPicker/readonly/divider/html/status/uuid
|
||||
* file/image/hidden/entityPicker/subform/readonly/divider/html/status/uuid
|
||||
*/
|
||||
type?: string
|
||||
|
||||
@@ -223,6 +241,10 @@ export interface ColumnSchema {
|
||||
/** 表单项下方说明文字 */
|
||||
description?: string
|
||||
|
||||
// === 子表单(明细子表) ===
|
||||
/** type=subform 时的配置(如员工档案的「联系方式」明细行) */
|
||||
subform?: SubFormConfig
|
||||
|
||||
// === 实体选择器 ===
|
||||
/** type=entityPicker 时的配置 */
|
||||
entityPicker?: EntityPickerConfig
|
||||
|
||||
@@ -208,8 +208,9 @@ async function handleSubmit(): Promise<void> {
|
||||
/** eHRM 档案管理菜单路由(清单 MenuLink /ehrm/profile 规范化后的 Path) */
|
||||
const EHRM_PROFILE_PATH = '/ehrm/profile'
|
||||
|
||||
/** 档案只读展示时排除的列:主键与纯 UUID 引用列(员工=本页用户,部门已有 DeptName 展示) */
|
||||
const PROFILE_HIDDEN_FIELDS = ['EmployeeProfileUUID', 'UserUUID', 'DeptUUID', 'TenantUUID', 'CompanyUUID']
|
||||
/** 档案只读展示时排除的列:主键与纯 UUID 引用列
|
||||
*(员工=本页用户;部门用 DeptUUID 的关联标签展示为「部门名称」;联系方式子表单独成块) */
|
||||
const PROFILE_HIDDEN_FIELDS = ['EmployeeProfileUUID', 'UserUUID', 'TenantUUID', 'CompanyUUID']
|
||||
|
||||
const profileLoading = ref(false)
|
||||
/** 该员工的档案记录(一人一份,最多一条) */
|
||||
@@ -258,22 +259,47 @@ const profileFormColumns = computed(() =>
|
||||
),
|
||||
)
|
||||
|
||||
/** 只读展示值:实体标签(*_label)优先 → 选项映射 → 日期格式化 → 原值 */
|
||||
/** 只读展示值:实体标签(*_label)优先 → 选项映射 → 布尔/多选/日期 → 原值 */
|
||||
function profileValue(col: ColumnSchema): string {
|
||||
const row = profileRecord.value
|
||||
if (!row) return '-'
|
||||
const label = row[`${col.field}_label`]
|
||||
if (label) return String(label)
|
||||
const raw = row[col.field]
|
||||
// 布尔意愿字段(switch/boolean)先判:false 也是有效值,不能当空处理
|
||||
if (col.type === 'switch' || col.type === 'boolean') return raw ? '是' : '否'
|
||||
if (raw === null || raw === undefined || raw === '') return '-'
|
||||
if (col.options?.length) {
|
||||
const hit = col.options.find((o) => String(o.value) === String(raw))
|
||||
if (hit) return hit.label
|
||||
}
|
||||
// 多选(tags):逗号串 → 顿号分隔展示
|
||||
if (col.type === 'tags') {
|
||||
const items = String(raw).split(col.tagsSeparator || ',').map((s) => s.trim()).filter(Boolean)
|
||||
return items.length ? items.join('、') : '-'
|
||||
}
|
||||
if (col.type === 'date' || col.type === 'datetime') return formatTime(String(raw))
|
||||
return String(raw)
|
||||
}
|
||||
|
||||
/** 档案联系方式明细(子表 Contacts:后端随档案一并返回,按 ContactSort 排序) */
|
||||
const profileContacts = computed<Record<string, any>[]>(() => {
|
||||
const rows = profileRecord.value?.Contacts
|
||||
return Array.isArray(rows) ? rows : []
|
||||
})
|
||||
|
||||
/** 联系方式类型选项:来自后端下发的子表列定义,前端不另写一份映射 */
|
||||
const contactTypeOptions = computed(() => {
|
||||
const col = profileColumns.value.find((c) => c.subform?.table === 'EmployeeContact')
|
||||
return col?.subform?.fields.find((f) => f.field === 'ContactType')?.options || []
|
||||
})
|
||||
|
||||
/** 联系方式类型值 → 中文名 */
|
||||
function contactTypeLabel(value: unknown): string {
|
||||
const hit = contactTypeOptions.value.find((o) => String(o.value) === String(value))
|
||||
return hit ? hit.label : String(value ?? '-')
|
||||
}
|
||||
|
||||
/** 加载该员工的档案(列表按 UserUUID 过滤,取首条) */
|
||||
async function loadProfile(): Promise<void> {
|
||||
if (!userUUID.value) {
|
||||
@@ -522,16 +548,29 @@ watch(userUUID, () => {
|
||||
</template>
|
||||
|
||||
<div v-loading="profileLoading" class="profile-panel">
|
||||
<!-- 已有档案:键值只读展示(不可更改) -->
|
||||
<el-descriptions v-if="profileRecord" :column="3" border class="profile-desc">
|
||||
<el-descriptions-item
|
||||
v-for="col in profileDisplayColumns"
|
||||
:key="col.field"
|
||||
:label="col.title"
|
||||
>
|
||||
{{ profileValue(col) }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<!-- 已有档案:键值只读展示(不可更改)+ 联系方式明细 -->
|
||||
<template v-if="profileRecord">
|
||||
<el-descriptions :column="3" border class="profile-desc">
|
||||
<el-descriptions-item
|
||||
v-for="col in profileDisplayColumns"
|
||||
:key="col.field"
|
||||
:label="col.title"
|
||||
>
|
||||
{{ profileValue(col) }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<!-- 联系方式明细(EmployeeContact 子表):只读列出,类型名用后端下发的选项 -->
|
||||
<div v-if="profileContacts.length" class="profile-contacts">
|
||||
<div class="profile-contacts-title">联系方式</div>
|
||||
<el-table :data="profileContacts" size="small" border>
|
||||
<el-table-column label="类型" width="160">
|
||||
<template #default="{ row }">{{ contactTypeLabel(row.ContactType) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="联系方式" prop="ContactValue" />
|
||||
</el-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 无档案 + 有 eHRM 修改权限:居中「点击填写」 -->
|
||||
<div v-else-if="canEditProfile" class="profile-empty">
|
||||
@@ -727,6 +766,18 @@ watch(userUUID, () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 联系方式明细(子表):与档案键值表保持上下间距
|
||||
.profile-contacts {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.profile-contacts-title {
|
||||
margin-bottom: 6px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
|
||||
// 无档案空态:居中「点击填写」
|
||||
.profile-empty {
|
||||
display: flex;
|
||||
|
||||
Reference in new issue
Block a user