246 lines
9.8 KiB
Vue
246 lines
9.8 KiB
Vue
<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>
|