20260722222510
This commit is contained in:
1 parent
d04fad24ed
commit
59b3ccfb1c
36 files changed
+2753
-3064
No files matched your search
@@ -0,0 +1,115 @@
|
||||
<script setup>
|
||||
import { computed, nextTick, ref } from "vue";
|
||||
import { Plus, Trash2 } from "@lucide/vue";
|
||||
import FmsTable from "@/components/fms-table/FmsTable.vue";
|
||||
|
||||
const props = defineProps({
|
||||
title: { type: String, required: true },
|
||||
columns: { type: Array, required: true },
|
||||
addText: { type: String, default: "新增" },
|
||||
canAdd: { type: Boolean, default: true },
|
||||
canDelete: { type: Boolean, default: true },
|
||||
height: { type: [String, Number], default: null },
|
||||
});
|
||||
const rows = defineModel("rows", { type: Array, default: () => [] });
|
||||
const emit = defineEmits(["add", "remove"]);
|
||||
const activeRowKey = ref(null);
|
||||
const selectedRowKeys = ref([]);
|
||||
const rowSelection = computed(() => ({
|
||||
selectedRowKeys: selectedRowKeys.value,
|
||||
onChange: (keys) => {
|
||||
selectedRowKeys.value = keys;
|
||||
},
|
||||
}));
|
||||
|
||||
function editRow(_event, row) {
|
||||
activeRowKey.value = String(row.b_id);
|
||||
}
|
||||
|
||||
function removeSelected() {
|
||||
const selected = new Set(selectedRowKeys.value.map(String));
|
||||
const removed = rows.value.filter((row) => selected.has(String(row.b_id)));
|
||||
rows.value = rows.value.filter((row) => !selected.has(String(row.b_id)));
|
||||
if (selected.has(String(activeRowKey.value))) activeRowKey.value = null;
|
||||
selectedRowKeys.value = [];
|
||||
emit("remove", removed);
|
||||
}
|
||||
|
||||
async function addRow() {
|
||||
emit("add");
|
||||
await nextTick();
|
||||
const row = rows.value.at(-1);
|
||||
if (row?.b_id != null) activeRowKey.value = String(row.b_id);
|
||||
}
|
||||
|
||||
defineExpose({ editRow: (row) => (activeRowKey.value = String(row.b_id)) });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="metadata-table-panel" :style="height ? null : { height: '100%' }">
|
||||
<header class="panel-toolbar">
|
||||
<strong>{{ title }}</strong>
|
||||
<div class="panel-actions">
|
||||
<slot name="actions" />
|
||||
<a-button v-if="canAdd" type="primary" @click="addRow"> <Plus />{{ addText }} </a-button>
|
||||
<a-button
|
||||
v-if="canDelete"
|
||||
danger
|
||||
:disabled="!selectedRowKeys.length"
|
||||
@click="removeSelected"
|
||||
>
|
||||
<Trash2 />删除
|
||||
</a-button>
|
||||
</div>
|
||||
</header>
|
||||
<div
|
||||
class="table-body"
|
||||
:style="
|
||||
height
|
||||
? { flex: 'none', height: typeof height === 'number' ? `${height}px` : height }
|
||||
: null
|
||||
"
|
||||
>
|
||||
<FmsTable
|
||||
:columns="props.columns"
|
||||
:col-resizable="false"
|
||||
:data-source="rows"
|
||||
row-key="b_id"
|
||||
:row-selection="canDelete ? rowSelection : null"
|
||||
:active-row-key="activeRowKey"
|
||||
@row-click="editRow"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.metadata-table-panel {
|
||||
display: flex;
|
||||
min-height: 0;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.panel-toolbar,
|
||||
.panel-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.panel-toolbar {
|
||||
min-height: 40px;
|
||||
flex: none;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.panel-actions {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.table-body {
|
||||
flex: 1;
|
||||
min-height: 180px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,165 +0,0 @@
|
||||
<script setup>
|
||||
import ModuleTranslationEditor from "./ModuleTranslationEditor.vue";
|
||||
|
||||
const props = defineProps({
|
||||
directoryParentOptions: { type: Array, default: () => [] },
|
||||
parentOptions: { type: Array, default: () => [] },
|
||||
});
|
||||
const emit = defineEmits(["update-module-code"]);
|
||||
const module = defineModel("module", { type: Object, default: null });
|
||||
const translations = defineModel("translations", { type: Object, default: () => ({}) });
|
||||
|
||||
function updateRoute(value) {
|
||||
module.value.b_route = value || null;
|
||||
if (!String(value || "").trim()) module.value.b_canmenu = 0;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section v-if="module" class="module-basic-info">
|
||||
<a-form v-if="module.b_type === 'directory'" layout="vertical" class="directory-form">
|
||||
<div class="directory-grid">
|
||||
<a-form-item label="模块编码" required>
|
||||
<a-input :value="module.b_code" @update:value="emit('update-module-code', $event)" />
|
||||
</a-form-item>
|
||||
<a-form-item label="模块名称" required>
|
||||
<a-input v-model:value="module.b_name" />
|
||||
</a-form-item>
|
||||
<a-form-item label="上级模块">
|
||||
<a-select
|
||||
v-model:value="module.b_parent_id"
|
||||
allow-clear
|
||||
show-search
|
||||
option-filter-prop="label"
|
||||
:options="directoryParentOptions.filter((item) => item.value !== module.b_id)"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="多语言键">
|
||||
<div class="i18n-editor-row">
|
||||
<a-input v-model:value="module.b_i18n" />
|
||||
<ModuleTranslationEditor
|
||||
v-model:translations="translations"
|
||||
:i18n-key="module.b_i18n"
|
||||
:default-name="module.b_name"
|
||||
:disabled="!module.b_i18n?.trim()"
|
||||
/>
|
||||
</div>
|
||||
</a-form-item>
|
||||
<a-form-item label="显示顺序">
|
||||
<a-input-number v-model:value="module.b_xh" :min="0" />
|
||||
</a-form-item>
|
||||
</div>
|
||||
</a-form>
|
||||
|
||||
<a-form v-else layout="vertical" class="basic-form">
|
||||
<div class="form-grid">
|
||||
<a-form-item label="模块编码" required>
|
||||
<a-input :value="module.b_code" @update:value="emit('update-module-code', $event)" />
|
||||
</a-form-item>
|
||||
<a-form-item label="模块名称" required>
|
||||
<a-input v-model:value="module.b_name" />
|
||||
</a-form-item>
|
||||
<a-form-item label="上级模块">
|
||||
<a-select
|
||||
v-model:value="module.b_parent_id"
|
||||
allow-clear
|
||||
show-search
|
||||
option-filter-prop="label"
|
||||
:options="parentOptions"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="页面路由(可选)">
|
||||
<a-input
|
||||
:value="module.b_route"
|
||||
placeholder="留空时作为数据模块使用"
|
||||
@update:value="updateRoute"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="多语言键">
|
||||
<div class="i18n-editor-row">
|
||||
<a-input v-model:value="module.b_i18n" />
|
||||
<ModuleTranslationEditor
|
||||
v-model:translations="translations"
|
||||
:i18n-key="module.b_i18n"
|
||||
:default-name="module.b_name"
|
||||
:disabled="!module.b_i18n?.trim()"
|
||||
/>
|
||||
</div>
|
||||
</a-form-item>
|
||||
<a-form-item label="查询表或视图">
|
||||
<a-input v-model:value="module.b_viewtable" />
|
||||
</a-form-item>
|
||||
<a-form-item label="保存表">
|
||||
<a-input v-model:value="module.b_savetable" />
|
||||
</a-form-item>
|
||||
<a-form-item label="主键字段">
|
||||
<a-input v-model:value="module.b_keyfield" />
|
||||
</a-form-item>
|
||||
<a-form-item label="排序条件">
|
||||
<a-input v-model:value="module.b_orderfield" />
|
||||
</a-form-item>
|
||||
<a-form-item label="显示顺序">
|
||||
<a-input-number v-model:value="module.b_xh" :min="0" />
|
||||
</a-form-item>
|
||||
</div>
|
||||
</a-form>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.module-basic-info {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
padding-top: 10px 0;
|
||||
}
|
||||
|
||||
.directory-form {
|
||||
max-width: 760px;
|
||||
}
|
||||
|
||||
.basic-form {
|
||||
max-width: 1040px;
|
||||
padding-top: 2px;
|
||||
}
|
||||
|
||||
.directory-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 0 10px;
|
||||
}
|
||||
|
||||
.form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 0 10px;
|
||||
}
|
||||
|
||||
.i18n-editor-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.i18n-editor-row :deep(.ant-input) {
|
||||
min-width: 0;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
:deep(.ant-form-item) {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
@media (max-width: 980px) {
|
||||
.form-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.directory-grid,
|
||||
.form-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,65 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
parentOptions: { type: Array, default: () => [] },
|
||||
routeOptions: { type: Array, default: () => [] },
|
||||
});
|
||||
const module = defineModel("module", { type: Object, required: true });
|
||||
function setBoolean(field, checked) {
|
||||
module.value[field] = checked ? 1 : 0;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-form layout="vertical" class="basic-form">
|
||||
<a-form-item label="模块类型">
|
||||
<a-segmented
|
||||
v-model:value="module.b_module_type"
|
||||
:options="[
|
||||
{ label: '目录', value: 'directory' },
|
||||
{ label: '页面', value: 'page' },
|
||||
{ label: '数据', value: 'data' },
|
||||
]"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="模块编码"
|
||||
><a-input v-model:value="module.b_code" :maxlength="50"
|
||||
/></a-form-item>
|
||||
<a-form-item label="默认名称"
|
||||
><a-input v-model:value="module.b_name" :maxlength="100"
|
||||
/></a-form-item>
|
||||
<a-form-item label="多语言键"
|
||||
><a-input v-model:value="module.b_i18n" :maxlength="150"
|
||||
/></a-form-item>
|
||||
<a-form-item label="上级模块">
|
||||
<a-select v-model:value="module.b_parent_id" allow-clear :options="parentOptions" />
|
||||
</a-form-item>
|
||||
<a-form-item v-if="module.b_module_type === 'page'" label="已注册页面">
|
||||
<a-select v-model:value="module.b_route" :options="routeOptions" />
|
||||
</a-form-item>
|
||||
<template v-if="module.b_module_type === 'data'">
|
||||
<a-form-item label="查询视图"><a-input v-model:value="module.b_viewtable" /></a-form-item>
|
||||
<a-form-item label="保存表"><a-input v-model:value="module.b_savetable" /></a-form-item>
|
||||
<a-form-item label="主键字段"><a-input v-model:value="module.b_keyfield" /></a-form-item>
|
||||
<a-form-item label="默认排序"><a-input v-model:value="module.b_orderfield" /></a-form-item>
|
||||
</template>
|
||||
<a-form-item label="同级顺序"
|
||||
><a-input-number v-model:value="module.b_xh" :min="0"
|
||||
/></a-form-item>
|
||||
<a-form-item label="启用">
|
||||
<a-switch :checked="module.b_canuse === 1" @change="setBoolean('b_canuse', $event)" />
|
||||
</a-form-item>
|
||||
<a-form-item v-if="module.b_module_type !== 'data'" label="显示菜单">
|
||||
<a-switch :checked="module.b_canmenu === 1" @change="setBoolean('b_canmenu', $event)" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.basic-form {
|
||||
display: grid;
|
||||
max-width: 1080px;
|
||||
grid-template-columns: repeat(3, minmax(180px, 1fr));
|
||||
gap: 0 16px;
|
||||
padding-top: 14px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,131 @@
|
||||
<script setup>
|
||||
import { computed } from "vue";
|
||||
import { createTempId } from "@/utils/tempId";
|
||||
import MetadataTablePanel from "./MetadataTablePanel.vue";
|
||||
|
||||
const props = defineProps({ fields: { type: Array, default: () => [] } });
|
||||
const groups = defineModel("groups", { type: Array, default: () => [] });
|
||||
const edits = defineModel("edits", { type: Array, default: () => [] });
|
||||
const fieldOptions = computed(() =>
|
||||
props.fields.map((item) => ({ value: item.b_id, label: `${item.b_name} (${item.b_field})` })),
|
||||
);
|
||||
const groupOptions = computed(() =>
|
||||
groups.value.map((item) => ({ value: item.b_code, label: item.b_title })),
|
||||
);
|
||||
const groupColumns = [
|
||||
{ dataIndex: "b_code", title: "分组编码", width: 150, editor: { maxlength: 100 } },
|
||||
{ dataIndex: "b_title", title: "默认标题", width: 180, editor: { maxlength: 100 } },
|
||||
{ dataIndex: "b_i18n", title: "标题多语言键", width: 250, editor: { maxlength: 150 } },
|
||||
{ dataIndex: "b_xh", title: "顺序", width: 80, editor: { type: "number", min: 0 } },
|
||||
{ dataIndex: "b_canuse", title: "启用", width: 70, align: "center", editor: "checkbox" },
|
||||
];
|
||||
const editColumns = computed(() => [
|
||||
{
|
||||
dataIndex: "b_field_id",
|
||||
title: "字段",
|
||||
width: 190,
|
||||
editor: { type: "select", options: fieldOptions.value },
|
||||
},
|
||||
{
|
||||
dataIndex: "b_editor",
|
||||
title: "编辑器",
|
||||
width: 110,
|
||||
editor: {
|
||||
type: "select",
|
||||
options: ["input", "textarea", "select", "date", "switch"].map((value) => ({
|
||||
value,
|
||||
label: value,
|
||||
})),
|
||||
},
|
||||
},
|
||||
{
|
||||
dataIndex: "b_edit_group",
|
||||
title: "表单分组",
|
||||
width: 150,
|
||||
editor: { type: "select", options: groupOptions.value },
|
||||
},
|
||||
{ dataIndex: "b_placeholder", title: "输入提示", width: 180, editor: { maxlength: 200 } },
|
||||
{ dataIndex: "b_required", title: "必填", width: 65, align: "center", editor: "checkbox" },
|
||||
{ dataIndex: "b_readonly", title: "只读", width: 65, align: "center", editor: "checkbox" },
|
||||
{ dataIndex: "b_disabled", title: "禁用", width: 65, align: "center", editor: "checkbox" },
|
||||
{ dataIndex: "b_canform", title: "Form", width: 65, align: "center", editor: "checkbox" },
|
||||
{ dataIndex: "b_edit_xh", title: "顺序", width: 75, editor: { type: "number", min: 0 } },
|
||||
{
|
||||
dataIndex: "b_edit_span",
|
||||
title: "栅格",
|
||||
width: 75,
|
||||
editor: { type: "number", min: 1, max: 24 },
|
||||
},
|
||||
{ dataIndex: "b_editor_options", title: "编辑器 JSON", width: 260, editor: {} },
|
||||
]);
|
||||
|
||||
function addGroup() {
|
||||
groups.value.push({
|
||||
b_id: createTempId(),
|
||||
b_code: `group_${groups.value.length + 1}`,
|
||||
b_title: "新分组",
|
||||
b_i18n: null,
|
||||
b_xh: Math.max(0, ...groups.value.map((item) => Number(item.b_xh) || 0)) + 10,
|
||||
b_canuse: 1,
|
||||
});
|
||||
}
|
||||
|
||||
function addEdit() {
|
||||
const used = new Set(edits.value.map((item) => String(item.b_field_id)));
|
||||
const field = props.fields.find((item) => !used.has(String(item.b_id)));
|
||||
if (!field) return;
|
||||
edits.value.push({
|
||||
b_id: createTempId(),
|
||||
b_field_id: field.b_id,
|
||||
b_editor: "input",
|
||||
b_edit_format: null,
|
||||
b_editor_options: null,
|
||||
b_required: 0,
|
||||
b_readonly: 0,
|
||||
b_disabled: 0,
|
||||
b_placeholder: null,
|
||||
b_canform: 1,
|
||||
b_edit_group: null,
|
||||
b_edit_xh: Math.max(0, ...edits.value.map((item) => Number(item.b_edit_xh) || 0)) + 10,
|
||||
b_edit_span: 24,
|
||||
});
|
||||
}
|
||||
|
||||
function handleGroupRemove(removed) {
|
||||
const removedCodes = new Set(removed.map((item) => item.b_code));
|
||||
edits.value.forEach((edit) => {
|
||||
if (removedCodes.has(edit.b_edit_group)) edit.b_edit_group = null;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="edit-panels">
|
||||
<MetadataTablePanel
|
||||
v-model:rows="groups"
|
||||
title="表单分组"
|
||||
add-text="新增分组"
|
||||
:columns="groupColumns"
|
||||
height="220px"
|
||||
@add="addGroup"
|
||||
@remove="handleGroupRemove"
|
||||
/>
|
||||
<MetadataTablePanel
|
||||
v-model:rows="edits"
|
||||
title="编辑字段"
|
||||
add-text="新增字段"
|
||||
:columns="editColumns"
|
||||
@add="addEdit"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.edit-panels {
|
||||
display: grid;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
grid-template-rows: auto minmax(260px, 1fr);
|
||||
gap: 12px;
|
||||
}
|
||||
</style>
|
||||
-213
@@ -1,213 +0,0 @@
|
||||
<script setup>
|
||||
import { ref } from "vue";
|
||||
import { App } from "antdv-next";
|
||||
import { RefreshCw } from "@lucide/vue";
|
||||
import FmsTable from "@/components/fms-table/FmsTable.vue";
|
||||
import { describeApi } from "@/services/api";
|
||||
import { createTempId } from "@/utils/tempId";
|
||||
import { createI18nCellRenderer } from "./createI18nCellRenderer";
|
||||
import ModuleTranslationEditor from "./ModuleTranslationEditor.vue";
|
||||
|
||||
const props = defineProps({
|
||||
moduleId: { type: [String, Number], default: "" },
|
||||
moduleCode: { type: String, default: "" },
|
||||
// 字段来源:模块配置的查询表或视图,为空时无法同步。
|
||||
viewTable: { type: String, default: "" },
|
||||
});
|
||||
|
||||
const fields = defineModel("fields", { type: Array, default: () => [] });
|
||||
const translations = defineModel("translations", { type: Object, default: () => ({}) });
|
||||
const { message } = App.useApp();
|
||||
// 当前编辑行 key;点击某行进入单行编辑,再次点击退出。
|
||||
const activeRowKey = ref(null);
|
||||
const syncing = ref(false);
|
||||
const translationEditorRef = ref(null);
|
||||
|
||||
const renderI18nCell = createI18nCellRenderer({
|
||||
activeRowKey,
|
||||
translationEditorRef,
|
||||
});
|
||||
|
||||
function onRowClick(_ev, row) {
|
||||
const key = String(row.b_id);
|
||||
activeRowKey.value = activeRowKey.value === key ? null : key;
|
||||
}
|
||||
|
||||
function mapType(type) {
|
||||
const t = String(type || "").toLowerCase();
|
||||
if (t.includes("bit")) return "boolean";
|
||||
if (/(tinyint|smallint|int|bigint|decimal|numeric|float|real|money|smallmoney)/.test(t)) {
|
||||
return "number";
|
||||
}
|
||||
if (/(datetime|smalldatetime|date|time)/.test(t)) return "date";
|
||||
if (/(text|ntext)/.test(t)) return "text";
|
||||
return "string";
|
||||
}
|
||||
|
||||
// 从视图同步字段:保留已有配置,追加视图新增列,移除视图已不存在的列。
|
||||
async function syncFields() {
|
||||
const source = props.viewTable;
|
||||
if (!source) {
|
||||
message.warning("请先在基本信息中配置查询表或视图");
|
||||
return;
|
||||
}
|
||||
syncing.value = true;
|
||||
try {
|
||||
const columns = (await describeApi(source)).data || [];
|
||||
if (!columns.length) {
|
||||
message.warning("该视图没有可识别的列");
|
||||
return;
|
||||
}
|
||||
|
||||
const existingByField = new Map(fields.value.map((item) => [item.b_field, item]));
|
||||
const merged = [];
|
||||
let maxXh = 0;
|
||||
fields.value.forEach((item) => {
|
||||
if (columns.some((col) => col.name === item.b_field)) {
|
||||
merged.push(item);
|
||||
maxXh = Math.max(maxXh, Number(item.b_xh) || 0);
|
||||
}
|
||||
});
|
||||
for (const col of columns) {
|
||||
if (existingByField.has(col.name)) continue;
|
||||
maxXh += 10;
|
||||
merged.push({
|
||||
b_id: createTempId(),
|
||||
b_module_id: props.moduleId,
|
||||
b_field: col.name,
|
||||
b_name: col.name,
|
||||
b_i18n: props.moduleCode ? `field.${props.moduleCode}.${col.name}` : null,
|
||||
b_type: mapType(col.type),
|
||||
b_canlist: 1,
|
||||
b_canform: 1,
|
||||
b_must: 0,
|
||||
b_canquery: 0,
|
||||
b_readonly: 0,
|
||||
b_width: 120,
|
||||
b_value: null,
|
||||
b_xh: maxXh,
|
||||
b_canuse: 1,
|
||||
});
|
||||
}
|
||||
fields.value = merged;
|
||||
message.success(`已同步 ${columns.length} 个字段`);
|
||||
} catch (error) {
|
||||
message.error(error.message || "字段同步失败");
|
||||
} finally {
|
||||
syncing.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
const columns = ref([
|
||||
{
|
||||
dataIndex: "b_i18n",
|
||||
title: "多语言键",
|
||||
width: 250,
|
||||
customCell: renderI18nCell,
|
||||
},
|
||||
{ dataIndex: "b_field", title: "字段", width: 150, editor: { maxlength: 50 } },
|
||||
{ dataIndex: "b_name", title: "名称", width: 150, editor: { maxlength: 100 } },
|
||||
{
|
||||
dataIndex: "b_type",
|
||||
title: "类型",
|
||||
width: 120,
|
||||
editor: {
|
||||
type: "select",
|
||||
options: ["string", "text", "number", "select", "date", "boolean"].map((value) => ({
|
||||
value,
|
||||
label: value,
|
||||
})),
|
||||
},
|
||||
},
|
||||
{
|
||||
dataIndex: "b_canlist",
|
||||
title: "列表",
|
||||
width: 64,
|
||||
align: "center",
|
||||
headerAlign: "center",
|
||||
editor: "checkbox",
|
||||
},
|
||||
{
|
||||
dataIndex: "b_canform",
|
||||
title: "表单",
|
||||
width: 64,
|
||||
align: "center",
|
||||
headerAlign: "center",
|
||||
editor: {
|
||||
type: "checkbox",
|
||||
onChange: (value, row) => {
|
||||
if (value === 0) row.b_must = 0;
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
dataIndex: "b_must",
|
||||
title: "必填",
|
||||
width: 64,
|
||||
align: "center",
|
||||
headerAlign: "center",
|
||||
editor: { type: "checkbox", disabled: (row) => row.b_canform !== 1 },
|
||||
},
|
||||
{
|
||||
dataIndex: "b_canquery",
|
||||
title: "查询",
|
||||
width: 64,
|
||||
align: "center",
|
||||
headerAlign: "center",
|
||||
editor: "checkbox",
|
||||
},
|
||||
{
|
||||
dataIndex: "b_readonly",
|
||||
title: "只读",
|
||||
width: 64,
|
||||
align: "center",
|
||||
headerAlign: "center",
|
||||
editor: "checkbox",
|
||||
},
|
||||
{ dataIndex: "b_width", title: "宽度", width: 90, editor: { type: "number", min: 1 } },
|
||||
{ dataIndex: "b_xh", title: "顺序", width: 80, editor: { type: "number", min: 0 } },
|
||||
]);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="table-pane">
|
||||
<div class="toolbar">
|
||||
<a-button type="primary" :loading="syncing" @click="syncFields">
|
||||
<template #icon><RefreshCw /></template>
|
||||
同步字段
|
||||
</a-button>
|
||||
</div>
|
||||
<FmsTable
|
||||
class="sub-table"
|
||||
row-key="b_id"
|
||||
v-model:columns="columns"
|
||||
:data-source="fields"
|
||||
:active-row-key="activeRowKey"
|
||||
@row-click="onRowClick"
|
||||
/>
|
||||
<ModuleTranslationEditor ref="translationEditorRef" v-model:translations="translations" />
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.table-pane {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex: none;
|
||||
padding: 10px 0;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.sub-table {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,52 @@
|
||||
<script setup>
|
||||
import { computed } from "vue";
|
||||
import { RefreshCw } from "@lucide/vue";
|
||||
import MetadataTablePanel from "./MetadataTablePanel.vue";
|
||||
|
||||
defineProps({ syncing: Boolean });
|
||||
defineEmits(["sync"]);
|
||||
const fields = defineModel("fields", { type: Array, default: () => [] });
|
||||
const columns = computed(() => [
|
||||
{ dataIndex: "b_field", title: "字段", width: 150 },
|
||||
{ dataIndex: "b_name", title: "默认名称", width: 160, editor: { maxlength: 100 } },
|
||||
{ dataIndex: "b_i18n", title: "多语言键", width: 240, editor: { maxlength: 150 } },
|
||||
{
|
||||
dataIndex: "b_field_type",
|
||||
title: "字段类型",
|
||||
width: 130,
|
||||
editor: {
|
||||
type: "select",
|
||||
options: [
|
||||
"string",
|
||||
"text",
|
||||
"number",
|
||||
"date",
|
||||
"datetime",
|
||||
"boolean",
|
||||
"select_id",
|
||||
"select_input",
|
||||
].map((value) => ({ value, label: value })),
|
||||
},
|
||||
},
|
||||
{ dataIndex: "b_dict_code", title: "字典编码", width: 150, editor: { maxlength: 100 } },
|
||||
{ dataIndex: "b_value", title: "默认值", width: 160, editor: { maxlength: 500 } },
|
||||
{ dataIndex: "b_remark", title: "说明", width: 220, editor: { maxlength: 500 } },
|
||||
{ dataIndex: "b_canuse", title: "启用", width: 70, align: "center", editor: "checkbox" },
|
||||
]);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MetadataTablePanel
|
||||
v-model:rows="fields"
|
||||
title="字段定义"
|
||||
:columns="columns"
|
||||
:can-add="false"
|
||||
:can-delete="false"
|
||||
>
|
||||
<template #actions>
|
||||
<a-button type="primary" :loading="syncing" @click="$emit('sync')">
|
||||
<RefreshCw />从视图同步
|
||||
</a-button>
|
||||
</template>
|
||||
</MetadataTablePanel>
|
||||
</template>
|
||||
@@ -0,0 +1,142 @@
|
||||
<script setup>
|
||||
import { computed } from "vue";
|
||||
import { createTempId } from "@/utils/tempId";
|
||||
import MetadataTablePanel from "./MetadataTablePanel.vue";
|
||||
|
||||
const props = defineProps({ fields: { type: Array, default: () => [] } });
|
||||
const nodes = defineModel("nodes", { type: Array, default: () => [] });
|
||||
const fieldOptions = computed(() =>
|
||||
props.fields.map((item) => ({ value: item.b_id, label: `${item.b_name} (${item.b_field})` })),
|
||||
);
|
||||
const groupOptions = computed(() => [
|
||||
{ value: null, label: "顶层" },
|
||||
...nodes.value
|
||||
.filter((item) => item.b_kind === "group")
|
||||
.map((item) => ({ value: item.b_id, label: item.b_title || "未命名分组" })),
|
||||
]);
|
||||
const columns = computed(() => [
|
||||
{
|
||||
dataIndex: "b_kind",
|
||||
title: "节点类型",
|
||||
width: 100,
|
||||
editor: {
|
||||
type: "select",
|
||||
options: [
|
||||
{ value: "group", label: "分组" },
|
||||
{ value: "field", label: "字段" },
|
||||
],
|
||||
onChange: (value, row) => {
|
||||
if (value === "group") {
|
||||
row.b_field_id = null;
|
||||
row.b_width = null;
|
||||
row.b_display_type = null;
|
||||
row.b_canedit = 0;
|
||||
} else {
|
||||
row.b_display_type ||= "text";
|
||||
row.b_width ||= 120;
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
dataIndex: "b_field_id",
|
||||
title: "关联字段",
|
||||
width: 190,
|
||||
editor: {
|
||||
type: "select",
|
||||
options: fieldOptions.value,
|
||||
disabled: (row) => row.b_kind === "group",
|
||||
},
|
||||
},
|
||||
{
|
||||
dataIndex: "b_parent_id",
|
||||
title: "上级分组",
|
||||
width: 150,
|
||||
editor: {
|
||||
type: "select",
|
||||
options: (row) =>
|
||||
groupOptions.value.filter((item) => String(item.value) !== String(row.b_id)),
|
||||
},
|
||||
},
|
||||
{ dataIndex: "b_title", title: "分组/列标题", width: 170, editor: { maxlength: 100 } },
|
||||
{ dataIndex: "b_i18n", title: "标题多语言键", width: 230, editor: { maxlength: 150 } },
|
||||
{ dataIndex: "b_visible", title: "显示", width: 65, align: "center", editor: "checkbox" },
|
||||
{ dataIndex: "b_xh", title: "顺序", width: 75, editor: { type: "number", min: 0 } },
|
||||
{
|
||||
dataIndex: "b_width",
|
||||
title: "列宽",
|
||||
width: 80,
|
||||
editor: { type: "number", min: 40, disabled: (row) => row.b_kind === "group" },
|
||||
},
|
||||
{
|
||||
dataIndex: "b_display_type",
|
||||
title: "显示类型",
|
||||
width: 120,
|
||||
editor: {
|
||||
type: "select",
|
||||
options: ["text", "money", "date", "datetime", "tag"].map((value) => ({
|
||||
value,
|
||||
label: value,
|
||||
})),
|
||||
disabled: (row) => row.b_kind === "group",
|
||||
},
|
||||
},
|
||||
{
|
||||
dataIndex: "b_sortable",
|
||||
title: "排序",
|
||||
width: 65,
|
||||
align: "center",
|
||||
editor: { type: "checkbox", disabled: (row) => row.b_kind === "group" },
|
||||
},
|
||||
{
|
||||
dataIndex: "b_canedit",
|
||||
title: "表内编辑",
|
||||
width: 80,
|
||||
align: "center",
|
||||
editor: { type: "checkbox", disabled: (row) => row.b_kind === "group" },
|
||||
},
|
||||
]);
|
||||
|
||||
function addNode() {
|
||||
nodes.value.push({
|
||||
b_id: createTempId(),
|
||||
b_parent_id: null,
|
||||
b_kind: "field",
|
||||
b_field_id: null,
|
||||
b_title: null,
|
||||
b_i18n: null,
|
||||
b_visible: 1,
|
||||
b_xh: Math.max(0, ...nodes.value.map((item) => Number(item.b_xh) || 0)) + 10,
|
||||
b_width: 120,
|
||||
b_sortable: 0,
|
||||
b_display_type: "text",
|
||||
b_canedit: 0,
|
||||
});
|
||||
}
|
||||
|
||||
function handleRemove(removed) {
|
||||
const removedIds = new Set(removed.map((item) => String(item.b_id)));
|
||||
let changed = true;
|
||||
while (changed) {
|
||||
changed = false;
|
||||
nodes.value.forEach((node) => {
|
||||
if (removedIds.has(String(node.b_parent_id)) && !removedIds.has(String(node.b_id))) {
|
||||
removedIds.add(String(node.b_id));
|
||||
changed = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
nodes.value = nodes.value.filter((node) => !removedIds.has(String(node.b_id)));
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MetadataTablePanel
|
||||
v-model:rows="nodes"
|
||||
title="列表列与多级表头"
|
||||
add-text="新增节点"
|
||||
:columns="columns"
|
||||
@add="addNode"
|
||||
@remove="handleRemove"
|
||||
/>
|
||||
</template>
|
||||
@@ -1,146 +0,0 @@
|
||||
<script setup>
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { Plus, Trash2 } from "@lucide/vue";
|
||||
import FmsTable from "@/components/fms-table/FmsTable.vue";
|
||||
import { createTempId } from "@/utils/tempId";
|
||||
import { createI18nCellRenderer } from "./createI18nCellRenderer";
|
||||
import ModuleTranslationEditor from "./ModuleTranslationEditor.vue";
|
||||
|
||||
const props = defineProps({
|
||||
moduleId: { type: [String, Number], default: "" },
|
||||
moduleCode: { type: String, default: "" },
|
||||
});
|
||||
|
||||
const powers = defineModel("powers", { type: Array, default: () => [] });
|
||||
const translations = defineModel("translations", { type: Object, default: () => ({}) });
|
||||
const selectedRowKeys = ref([]);
|
||||
// 当前编辑行 key;点击某行进入单行编辑,再次点击退出。
|
||||
const activeRowKey = ref(null);
|
||||
const translationEditorRef = ref(null);
|
||||
|
||||
const renderI18nCell = createI18nCellRenderer({
|
||||
activeRowKey,
|
||||
translationEditorRef,
|
||||
});
|
||||
|
||||
function onRowClick(_ev, row) {
|
||||
const key = String(row.b_id);
|
||||
activeRowKey.value = activeRowKey.value === key ? null : key;
|
||||
}
|
||||
|
||||
const rowSelection = computed(() => ({
|
||||
selectedRowKeys: selectedRowKeys.value,
|
||||
onChange: (keys) => {
|
||||
selectedRowKeys.value = keys;
|
||||
},
|
||||
}));
|
||||
|
||||
watch(
|
||||
() => powers.value.map((item) => String(item.b_id)),
|
||||
(keys) => {
|
||||
const available = new Set(keys);
|
||||
selectedRowKeys.value = selectedRowKeys.value.filter((key) => available.has(String(key)));
|
||||
},
|
||||
);
|
||||
|
||||
function nextPowerCode() {
|
||||
const existing = new Set(powers.value.map((item) => item.b_code));
|
||||
let index = 1;
|
||||
while (existing.has(`new_${index}`)) index += 1;
|
||||
return `new_${index}`;
|
||||
}
|
||||
|
||||
function onAdd() {
|
||||
if (!props.moduleId) return;
|
||||
const code = nextPowerCode();
|
||||
powers.value = [
|
||||
...powers.value,
|
||||
{
|
||||
b_id: createTempId(),
|
||||
b_module_id: props.moduleId,
|
||||
b_code: code,
|
||||
b_name: "新权限",
|
||||
b_i18n: props.moduleCode ? `power.${props.moduleCode}.${code}` : null,
|
||||
b_canuse: 1,
|
||||
b_xh: (powers.value.length + 1) * 10,
|
||||
},
|
||||
];
|
||||
activeRowKey.value = powers.value[powers.value.length - 1].b_id;
|
||||
}
|
||||
|
||||
function onBatchDelete() {
|
||||
const selected = new Set(selectedRowKeys.value.map(String));
|
||||
powers.value = powers.value.filter((item) => !selected.has(String(item.b_id)));
|
||||
selectedRowKeys.value = [];
|
||||
if (activeRowKey.value && selected.has(String(activeRowKey.value))) {
|
||||
activeRowKey.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
const columns = ref([
|
||||
{
|
||||
dataIndex: "b_i18n",
|
||||
title: "多语言键",
|
||||
width: 250,
|
||||
customCell: renderI18nCell,
|
||||
},
|
||||
{ dataIndex: "b_code", title: "权限代码", width: 180, editor: { maxlength: 50 } },
|
||||
{ dataIndex: "b_name", title: "名称", width: 180, editor: { maxlength: 100 } },
|
||||
{
|
||||
dataIndex: "b_canuse",
|
||||
title: "启用",
|
||||
width: 74,
|
||||
align: "center",
|
||||
headerAlign: "center",
|
||||
editor: "checkbox",
|
||||
},
|
||||
{ dataIndex: "b_xh", title: "顺序", width: 90, editor: { type: "number", min: 0 } },
|
||||
]);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="table-pane">
|
||||
<div class="toolbar">
|
||||
<a-button type="primary" @click="onAdd">
|
||||
<template #icon><Plus /></template>
|
||||
新增
|
||||
</a-button>
|
||||
<a-button danger :disabled="!selectedRowKeys.length" @click="onBatchDelete">
|
||||
<template #icon><Trash2 /></template>
|
||||
批量删除
|
||||
</a-button>
|
||||
</div>
|
||||
<FmsTable
|
||||
class="sub-table"
|
||||
row-key="b_id"
|
||||
v-model:columns="columns"
|
||||
:data-source="powers"
|
||||
:row-selection="rowSelection"
|
||||
:active-row-key="activeRowKey"
|
||||
@row-click="onRowClick"
|
||||
/>
|
||||
<ModuleTranslationEditor ref="translationEditorRef" v-model:translations="translations" />
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.table-pane {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex: none;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.sub-table {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,34 @@
|
||||
<script setup>
|
||||
import { createTempId } from "@/utils/tempId";
|
||||
import MetadataTablePanel from "./MetadataTablePanel.vue";
|
||||
|
||||
const powers = defineModel("powers", { type: Array, default: () => [] });
|
||||
const columns = [
|
||||
{ dataIndex: "b_code", title: "操作编码", width: 150, editor: { maxlength: 50 } },
|
||||
{ dataIndex: "b_name", title: "默认名称", width: 180, editor: { maxlength: 100 } },
|
||||
{ dataIndex: "b_i18n", title: "多语言键", width: 260, editor: { maxlength: 150 } },
|
||||
{ dataIndex: "b_xh", title: "顺序", width: 80, editor: { type: "number", min: 0 } },
|
||||
{ dataIndex: "b_canuse", title: "启用", width: 70, align: "center", editor: "checkbox" },
|
||||
];
|
||||
|
||||
function addPower() {
|
||||
powers.value.push({
|
||||
b_id: createTempId(),
|
||||
b_code: "",
|
||||
b_name: "新操作",
|
||||
b_i18n: null,
|
||||
b_xh: Math.max(0, ...powers.value.map((item) => Number(item.b_xh) || 0)) + 10,
|
||||
b_canuse: 1,
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MetadataTablePanel
|
||||
v-model:rows="powers"
|
||||
title="模块操作权限"
|
||||
add-text="新增操作"
|
||||
:columns="columns"
|
||||
@add="addPower"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,65 @@
|
||||
<script setup>
|
||||
import { computed } from "vue";
|
||||
import { createTempId } from "@/utils/tempId";
|
||||
import MetadataTablePanel from "./MetadataTablePanel.vue";
|
||||
|
||||
const props = defineProps({ fields: { type: Array, default: () => [] } });
|
||||
const queries = defineModel("queries", { type: Array, default: () => [] });
|
||||
const fieldOptions = computed(() =>
|
||||
props.fields.map((item) => ({ value: item.b_id, label: `${item.b_name} (${item.b_field})` })),
|
||||
);
|
||||
const columns = computed(() => [
|
||||
{
|
||||
dataIndex: "b_field_id",
|
||||
title: "字段",
|
||||
width: 210,
|
||||
editor: { type: "select", options: fieldOptions.value },
|
||||
},
|
||||
{
|
||||
dataIndex: "b_component",
|
||||
title: "查询组件",
|
||||
width: 130,
|
||||
editor: {
|
||||
type: "select",
|
||||
options: ["input", "select", "date", "date-range"].map((value) => ({ value, label: value })),
|
||||
},
|
||||
},
|
||||
{
|
||||
dataIndex: "b_operator",
|
||||
title: "操作符",
|
||||
width: 100,
|
||||
editor: {
|
||||
type: "select",
|
||||
options: ["=", "like", "between", "in"].map((value) => ({ value, label: value })),
|
||||
},
|
||||
},
|
||||
{ dataIndex: "b_default_value", title: "默认值", width: 220, editor: { maxlength: 500 } },
|
||||
{ dataIndex: "b_xh", title: "顺序", width: 80, editor: { type: "number", min: 0 } },
|
||||
{ dataIndex: "b_canuse", title: "启用", width: 70, align: "center", editor: "checkbox" },
|
||||
]);
|
||||
|
||||
function addQuery() {
|
||||
const used = new Set(queries.value.map((item) => String(item.b_field_id)));
|
||||
const field = props.fields.find((item) => !used.has(String(item.b_id)));
|
||||
if (!field) return;
|
||||
queries.value.push({
|
||||
b_id: createTempId(),
|
||||
b_field_id: field.b_id,
|
||||
b_component: "input",
|
||||
b_operator: "like",
|
||||
b_default_value: null,
|
||||
b_xh: Math.max(0, ...queries.value.map((item) => Number(item.b_xh) || 0)) + 10,
|
||||
b_canuse: 1,
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MetadataTablePanel
|
||||
v-model:rows="queries"
|
||||
title="列表查询条件"
|
||||
add-text="新增条件"
|
||||
:columns="columns"
|
||||
@add="addQuery"
|
||||
/>
|
||||
</template>
|
||||
@@ -1,87 +0,0 @@
|
||||
<script setup>
|
||||
import { computed, reactive } from "vue";
|
||||
import { App } from "antdv-next";
|
||||
import { Languages } from "@lucide/vue";
|
||||
import { I18N_LOCALES, isValidI18nKey } from "@/utils/i18n";
|
||||
|
||||
const props = defineProps({
|
||||
i18nKey: { type: String, default: "" },
|
||||
disabled: { type: Boolean, default: false },
|
||||
// 默认语言(由 b_name 承载),不在此编辑器中维护,避免两份中文名互相脱节。
|
||||
defaultLocale: { type: String, default: "zh-CN" },
|
||||
// 默认语言对应的名称(如 b_name 的中文名),仅展示用,帮助辨识所属对象。
|
||||
defaultName: { type: String, default: "" },
|
||||
});
|
||||
const translations = defineModel("translations", { type: Object, default: () => ({}) });
|
||||
const { message } = App.useApp();
|
||||
const dialog = reactive({ open: false, key: "", name: "", values: {} });
|
||||
|
||||
// 仅维护非默认语言;默认语言(zh-CN)由对应实体的 b_name 字段承载。
|
||||
const editableLocales = computed(() =>
|
||||
I18N_LOCALES.filter((locale) => locale.value !== props.defaultLocale),
|
||||
);
|
||||
const defaultLocaleLabel = computed(
|
||||
() =>
|
||||
I18N_LOCALES.find((locale) => locale.value === props.defaultLocale)?.label ||
|
||||
props.defaultLocale,
|
||||
);
|
||||
|
||||
// 支持两种打开方式:
|
||||
// 1) 自带按钮点击:使用 props.i18nKey / props.defaultName
|
||||
// 2) 表格内按行打开:传入 key / name(如字段、权限的某行)
|
||||
function openEditor(forceKey, forceName) {
|
||||
const key = String(forceKey ?? props.i18nKey).trim();
|
||||
const name = forceName ?? props.defaultName;
|
||||
if (!isValidI18nKey(key)) {
|
||||
message.warning("请先填写正确的多语言键");
|
||||
return;
|
||||
}
|
||||
dialog.key = key;
|
||||
dialog.name = name || "";
|
||||
dialog.values = Object.fromEntries(
|
||||
editableLocales.value.map((locale) => [
|
||||
locale.value,
|
||||
translations.value[key]?.[locale.value] || "",
|
||||
]),
|
||||
);
|
||||
dialog.open = true;
|
||||
}
|
||||
|
||||
defineExpose({ open: openEditor });
|
||||
|
||||
function saveTranslations() {
|
||||
// 整条 key 只写非默认语言,顺带清掉历史残留的默认语言翻译,杜绝覆盖 b_name。
|
||||
translations.value = {
|
||||
...translations.value,
|
||||
[dialog.key]: { ...dialog.values },
|
||||
};
|
||||
dialog.open = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-button v-if="i18nKey" :disabled="disabled" @click="openEditor">
|
||||
<template #icon><Languages /></template>
|
||||
多语言
|
||||
</a-button>
|
||||
|
||||
<a-modal
|
||||
v-model:open="dialog.open"
|
||||
title="更新多语言"
|
||||
ok-text="更新"
|
||||
cancel-text="取消"
|
||||
@ok="saveTranslations"
|
||||
>
|
||||
<a-form layout="vertical">
|
||||
<a-form-item label="多语言键">
|
||||
<a-input :value="dialog.key" readonly />
|
||||
</a-form-item>
|
||||
<a-form-item :label="`${defaultLocaleLabel}(默认语言)`">
|
||||
<a-input :value="dialog.name" readonly />
|
||||
</a-form-item>
|
||||
<a-form-item v-for="locale in editableLocales" :key="locale.value" :label="locale.label">
|
||||
<a-input v-model:value="dialog.values[locale.value]" :maxlength="500" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</template>
|
||||
@@ -0,0 +1,44 @@
|
||||
<script setup>
|
||||
import { computed } from "vue";
|
||||
import { createTempId } from "@/utils/tempId";
|
||||
import { I18N_LOCALES } from "@/utils/i18n";
|
||||
import MetadataTablePanel from "./MetadataTablePanel.vue";
|
||||
|
||||
const props = defineProps({ keys: { type: Array, default: () => [] } });
|
||||
const translations = defineModel("translations", { type: Array, default: () => [] });
|
||||
const columns = computed(() => [
|
||||
{
|
||||
dataIndex: "b_key",
|
||||
title: "多语言键",
|
||||
width: 300,
|
||||
editor: { type: "select", options: props.keys.map((value) => ({ value, label: value })) },
|
||||
},
|
||||
{
|
||||
dataIndex: "b_locale",
|
||||
title: "语言",
|
||||
width: 150,
|
||||
editor: { type: "select", options: I18N_LOCALES },
|
||||
},
|
||||
{ dataIndex: "b_value", title: "翻译内容", width: 380, editor: { maxlength: 500 } },
|
||||
]);
|
||||
|
||||
function addTranslation() {
|
||||
translations.value.push({
|
||||
b_id: createTempId(),
|
||||
b_key: props.keys[0] || "",
|
||||
b_locale: "en-US",
|
||||
b_value: "",
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MetadataTablePanel
|
||||
v-model:rows="translations"
|
||||
title="模块关联翻译"
|
||||
add-text="新增翻译"
|
||||
:columns="columns"
|
||||
:can-delete="false"
|
||||
@add="addTranslation"
|
||||
/>
|
||||
</template>
|
||||
@@ -64,8 +64,9 @@ const contextMenu = computed(() => {
|
||||
const node = contextNode.value;
|
||||
return {
|
||||
items: [
|
||||
{ key: "module", label: "新增模块", disabled: !props.canCreate },
|
||||
{ key: "directory", label: "新增目录", disabled: !props.canCreate },
|
||||
{ key: "page", label: "新增页面", disabled: !props.canCreate },
|
||||
{ key: "data", label: "新增数据模块", disabled: !props.canCreate },
|
||||
{ type: "divider" },
|
||||
{
|
||||
key: "delete",
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
import { h } from "vue";
|
||||
import { Input } from "antdv-next";
|
||||
import { Languages } from "@lucide/vue";
|
||||
|
||||
const cellStyle = {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "4px",
|
||||
width: "100%",
|
||||
};
|
||||
const inputStyle = { flex: "1 1 auto", minWidth: 0 };
|
||||
const textStyle = {
|
||||
flex: "1 1 auto",
|
||||
minWidth: 0,
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
};
|
||||
const iconStyle = {
|
||||
display: "inline-flex",
|
||||
flex: "none",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
width: "24px",
|
||||
height: "24px",
|
||||
cursor: "pointer",
|
||||
color: "var(--ant-color-primary, #1677ff)",
|
||||
};
|
||||
|
||||
export function createI18nCellRenderer({ activeRowKey, translationEditorRef }) {
|
||||
return function renderI18nCell({ row }) {
|
||||
const editing = activeRowKey.value != null && String(row.b_id) === String(activeRowKey.value);
|
||||
|
||||
return h("div", { style: cellStyle }, [
|
||||
editing
|
||||
? h("div", { style: inputStyle }, [
|
||||
h(Input, {
|
||||
value: row.b_i18n,
|
||||
size: "small",
|
||||
maxlength: 150,
|
||||
allowClear: true,
|
||||
style: { width: "100%" },
|
||||
onClick: (event) => event.stopPropagation(),
|
||||
"onUpdate:value": (value) => {
|
||||
row.b_i18n = value;
|
||||
},
|
||||
}),
|
||||
])
|
||||
: h("span", { style: textStyle }, row.b_i18n ?? ""),
|
||||
h(
|
||||
"span",
|
||||
{
|
||||
style: iconStyle,
|
||||
title: "编辑翻译",
|
||||
onClick: (event) => {
|
||||
event.stopPropagation();
|
||||
translationEditorRef.value?.open(row.b_i18n, row.b_name);
|
||||
},
|
||||
},
|
||||
[h(Languages, { size: 16 })],
|
||||
),
|
||||
]);
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user