Files
workspace/code/g3soft-erp/web/src/views/module/module-management/ModuleBasicPanel.vue
T
2026-10-15 17:31:35 +08:00

246 lines
9.8 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">
/**
* 模块基础信息面板(s_module)。
*
* 数据契约:v-model:module 绑定父级持有的「编辑草稿」(选中模块的 s_module 行副本),
* 面板就地修改字段(与原型一致);保存编排在父级,本面板只管 UI 与校验。
* 编码(b_id 即主键):仅未落库草稿可改,改动时同步派生多语言键 module.xxx;
* 类型切换会清掉不适用字段(module 无数据来源;virtual 无保存表)。
*
* 布局:三列栅格,一行三个字段;不分组(与菜单基础信息同款理由)——
* 分组标题只是把十来个字段切成几段,字段本身并没有多到需要分段,去掉反而更紧凑。
* 多语言键(b_i18n)不在界面上显示 —— 它由编码派生、用户改不了,
* 占一个字段位只是噪音(派生逻辑照旧,见 onCodeInput)。
*
* 启用态(b_canuse)不在本表单里:它在左树行尾勾选(点一下即时写库)与卡头徽标展示。
* 表单里再放一个开关会与之语义冲突 —— 树里那个立即生效,表单这个要等「保存」,
* 同一字段两种口径用户没法预期;且草稿态启停本身是伪需求(启停是运维动作,不是配置内容)。
*
* 校验规则(随输入实时提示;保存前由父级调 validate() 统一跑一遍):
* - 编码:必填、2-50 位、小写字母开头、只含小写字母/数字/下划线、禁止负数临时编码;
* - 名称 / 类型必填;data 的查询来源(表或 SQL)、主键字段、保存表必填。
*/
import { computed, ref } from 'vue'
import { G3Form, G3FormItem, G3Input, G3Select } from '@g3soft/ui'
import type { FormRule } from '@g3soft/ui'
import { buildModuleI18nKey, type ModuleFieldRow, type ModuleNode } from './utils'
const moduleModel = defineModel<ModuleNode | null>('module', { default: null })
const props = withDefaults(
defineProps<{
/** 本模块的字段清单(s_field 最小投影):「数据范围字段」下拉的候选 */
fields?: ModuleFieldRow[]
/** 编码只读:已保存模块主键即编码,创建后不可修改 */
codeReadonly?: boolean
}>(),
{ fields: () => [], codeReadonly: false },
)
const typeOptions = [
{ label: '模块', value: 'module' },
{ label: '数据模块', value: 'data' },
{ label: '虚拟模块', value: 'virtual' },
]
const isData = computed(() => moduleModel.value?.b_module_type === 'data')
const isVirtual = computed(() => moduleModel.value?.b_module_type === 'virtual')
/** data / virtual 拥有查询来源与字段定义 */
const hasDataSource = computed(() => isData.value || isVirtual.value)
/** 数据范围字段候选(本模块字段),空 b_field 的行直接剔除 */
const scopeFieldOptions = computed(() =>
props.fields
.filter((field) => field.b_field != null && String(field.b_field) !== '')
.map((field) => ({
label: String(field.b_name || field.b_field),
value: String(field.b_field),
})),
)
const formRules = computed<Record<string, FormRule[]>>(() => ({
b_id: [
{ required: true, message: '请填写模块编码' },
{
validator: (value) => {
const code = String(value ?? '').trim()
if (!code) return '编码不能为空'
if (/^-\d+$/.test(code)) return '新建模块编码不能以负数临时值保存'
if (code.length < 2 || code.length > 50) return '编码长度需为 2-50 个字符'
return /^[a-z][a-z0-9_]*$/.test(code)
? true
: '编码须以小写字母开头,且只能包含小写字母、数字和下划线'
},
},
],
b_name: [{ required: true, message: '请填写模块名称' }],
b_module_type: [{ required: true, message: '请选择模块类型' }],
b_view_table: isData.value
? [
{
validator: (value) => {
if (String(value ?? '').trim()) return true
if (String(moduleModel.value?.b_query_sql ?? '').trim()) return true
return '请填写查询表/视图,或配置查询 SQL'
},
},
]
: [],
b_key_field: isData.value ? [{ required: true, message: '请填写主键字段' }] : [],
b_save_table: isData.value ? [{ required: true, message: '请填写保存表' }] : [],
}))
const formRef = ref<InstanceType<typeof G3Form> | null>(null)
/** 校验全表:返回 [{ name, errors }],空数组即通过(供保存前调用) */
async function validate() {
return (await formRef.value?.validateAll()) ?? []
}
defineExpose({ validate })
/** 就地写草稿字段:空字符串统一收敛成 null(数据库可空列的约定) */
function setField(field: string, value: unknown) {
const draft = moduleModel.value as Record<string, unknown> | null
if (!draft) return
draft[field] = value === '' ? null : value
}
/** 编码变化:始终以完整编码重新派生多语言键 */
function onCodeInput(value: string | number) {
setField('b_id', value)
setField('b_i18n', buildModuleI18nKey(String(value ?? '')))
}
/** 切换类型:清掉不适用字段(module 无数据来源;virtual 无保存表),并刷新多语言键 */
function onModuleTypeChange(value: unknown) {
const type = String(value ?? '')
setField('b_module_type', type)
if (type === 'module') {
setField('b_view_table', null)
setField('b_save_table', null)
setField('b_key_field', null)
setField('b_scope_field', null)
setField('b_order_sql', null)
setField('b_query_sql', null)
} else if (type === 'virtual') {
setField('b_save_table', null)
}
setField('b_i18n', buildModuleI18nKey(String(moduleModel.value?.b_id ?? '')))
}
</script>
<template>
<div v-if="moduleModel" class="mm-basic">
<G3Form
ref="formRef"
:model="moduleModel"
:rules="formRules"
:columns="3"
max-width="720px"
label-width="96px"
>
<G3FormItem label="模块编码" name="b_id" required>
<G3Input
:model-value="moduleModel.b_id ?? ''"
:disabled="codeReadonly"
@update:model-value="onCodeInput"
/>
</G3FormItem>
<G3FormItem label="模块名称" name="b_name" required>
<G3Input
:model-value="moduleModel.b_name ?? ''"
@update:model-value="(value) => setField('b_name', value)"
/>
</G3FormItem>
<G3FormItem label="模块类型" name="b_module_type" required>
<G3Select
:model-value="moduleModel.b_module_type ?? ''"
:options="typeOptions"
@update:model-value="onModuleTypeChange"
/>
</G3FormItem>
<!-- 数据来源:data / virtual 才有 -->
<G3FormItem v-if="hasDataSource" label="查询表/视图" name="b_view_table" :required="isData">
<G3Input
:model-value="moduleModel.b_view_table ?? ''"
@update:model-value="(value) => setField('b_view_table', value)"
/>
</G3FormItem>
<G3FormItem v-if="hasDataSource" label="主键字段" name="b_key_field" :required="isData">
<G3Input
:model-value="moduleModel.b_key_field ?? ''"
@update:model-value="(value) => setField('b_key_field', value)"
/>
</G3FormItem>
<!-- 数据范围字段:记录归属人字段(如录入人)。
用户数据范围面板只选「看到哪一档」,用哪个字段判在这里定 ——
同一个模块的所有用户必须用同一字段,否则同一份数据对不同用户含义不同 -->
<G3FormItem v-if="hasDataSource" label="数据范围字段" name="b_scope_field">
<G3Select
:model-value="moduleModel.b_scope_field ?? ''"
:options="scopeFieldOptions"
clearable
title="记录归属人字段:「仅本人」按它比对,「本部门 / 本部门及下级」由该用户反查部门;留空表示本模块不按字段判范围"
@update:model-value="(value) => setField('b_scope_field', value)"
/>
</G3FormItem>
<G3FormItem v-if="isData" label="保存表" name="b_save_table" required>
<G3Input
:model-value="moduleModel.b_save_table ?? ''"
@update:model-value="(value) => setField('b_save_table', value)"
/>
</G3FormItem>
<G3FormItem v-if="hasDataSource" label="默认排序 SQL" name="b_order_sql">
<G3Input
:model-value="moduleModel.b_order_sql ?? ''"
@update:model-value="(value) => setField('b_order_sql', value)"
/>
</G3FormItem>
<!-- 查询 SQL / 扩展配置:内容较长,整行铺满 -->
<G3FormItem v-if="hasDataSource" label="查询 SQL" name="b_query_sql" :span="3">
<G3Input
type="textarea"
:rows="3"
:model-value="moduleModel.b_query_sql ?? ''"
@update:model-value="(value) => setField('b_query_sql', value)"
/>
</G3FormItem>
<G3FormItem v-if="hasDataSource" label="扩展配置 JSON" name="b_config_json" :span="3">
<G3Input
type="textarea"
:rows="2"
:model-value="moduleModel.b_config_json ?? ''"
@update:model-value="(value) => setField('b_config_json', value)"
/>
</G3FormItem>
<G3FormItem label="排序号" name="b_xh">
<G3Input
type="number"
:model-value="moduleModel.b_xh ?? ''"
@update:model-value="(value) => setField('b_xh', value)"
/>
</G3FormItem>
<G3FormItem label="描述" name="b_bz" :span="3">
<G3Input
type="textarea"
:rows="2"
:model-value="moduleModel.b_bz ?? ''"
@update:model-value="(value) => setField('b_bz', value)"
/>
</G3FormItem>
</G3Form>
</div>
</template>
<style lang="scss" scoped>
/* 面板内容自带内边距(骨架的 pane 只负责滚动,不设 padding) */
.mm-basic {
padding: 4px 0 16px;
}
</style>