This commit is contained in:
oneao committed 2026-10-14 17:25:47 +08:00
1 parent 15e0ae5b67
commit e434374db5
71 files changed
+5726 -329

No files matched your search

@@ -0,0 +1,253 @@
<script setup lang="ts">
/**
* 模块基础信息面板(s_module)。
*
* 数据契约:v-model:module 绑定父级持有的「编辑草稿」(选中模块的 s_module 行副本),
* 面板就地修改字段(与原型一致);保存编排在父级,本面板只管 UI 与校验。
* 编码(b_id 即主键):仅未落库草稿可改,改动时同步派生多语言键 module.xxx;
* 类型切换会清掉不适用字段(module 无数据来源;virtual 无保存表)。
*
* 校验规则(随输入实时提示;保存前由父级调 validate() 统一跑一遍):
* - 编码:必填、2-50 位、小写字母开头、只含小写字母/数字/下划线、禁止负数临时编码;
* - 名称 / 类型必填;data 的查询来源(表或 SQL)、主键字段、保存表必填。
*/
import { computed, ref } from 'vue'
import { G3Form, G3FormGroup, G3FormItem, G3Input, G3Select, G3Switch } 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"
>
<G3FormGroup title="标识与归属">
<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>
<G3FormItem label="多语言键" name="b_i18n" :span="2">
<G3Input :model-value="moduleModel.b_i18n ?? ''" readonly />
</G3FormItem>
</G3FormGroup>
<G3FormGroup v-if="hasDataSource" title="数据来源">
<G3FormItem 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 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 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 label="默认排序 SQL" name="b_order_sql" :span="isData ? 1 : 3">
<G3Input
:model-value="moduleModel.b_order_sql ?? ''"
@update:model-value="(value) => setField('b_order_sql', value)"
/>
</G3FormItem>
</G3FormGroup>
<G3FormGroup v-if="hasDataSource" title="查询与扩展">
<G3FormItem 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 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>
</G3FormGroup>
<G3FormGroup title="行为设置">
<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_canuse">
<G3Switch
:model-value="Number(moduleModel.b_canuse) === 1"
@update:model-value="(value) => setField('b_canuse', value ? 1 : 0)"
/>
</G3FormItem>
</G3FormGroup>
<G3FormGroup title="更多">
<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>
</G3FormGroup>
</G3Form>
</div>
</template>
<style lang="scss" scoped>
/* 面板内容自带内边距(骨架的 pane 只负责滚动,不设 padding) */
.mm-basic {
padding: 4px 0 16px;
}
</style>