20260721223033
This commit is contained in:
1 parent
bab8e2972f
commit
e9f099f044
26 files changed
+1687
-349
No files matched your search
@@ -1,11 +1,13 @@
|
||||
<script setup>
|
||||
import ModuleTranslationEditor from "./ModuleTranslationEditor.vue";
|
||||
|
||||
const props = defineProps({
|
||||
directoryParentOptions: { type: Array, default: () => [] },
|
||||
isNew: Boolean,
|
||||
parentOptions: { type: Array, default: () => [] },
|
||||
});
|
||||
const emit = defineEmits(["update-module-id"]);
|
||||
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;
|
||||
@@ -18,7 +20,7 @@ function updateRoute(value) {
|
||||
<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_id" disabled />
|
||||
<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" />
|
||||
@@ -33,7 +35,14 @@ function updateRoute(value) {
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="多语言键">
|
||||
<a-input v-model:value="module.b_i18n" />
|
||||
<div class="i18n-editor-row">
|
||||
<a-input v-model:value="module.b_i18n" />
|
||||
<ModuleTranslationEditor
|
||||
v-model:translations="translations"
|
||||
:i18n-key="module.b_i18n"
|
||||
:disabled="!module.b_i18n?.trim()"
|
||||
/>
|
||||
</div>
|
||||
</a-form-item>
|
||||
<a-form-item label="显示顺序">
|
||||
<a-input-number v-model:value="module.b_xh" :min="0" />
|
||||
@@ -44,11 +53,7 @@ function updateRoute(value) {
|
||||
<a-form v-else layout="vertical" class="basic-form">
|
||||
<div class="form-grid">
|
||||
<a-form-item label="模块编码" required>
|
||||
<a-input
|
||||
:value="module.b_id"
|
||||
:disabled="!isNew"
|
||||
@update:value="emit('update-module-id', $event)"
|
||||
/>
|
||||
<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" />
|
||||
@@ -70,7 +75,14 @@ function updateRoute(value) {
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="多语言键">
|
||||
<a-input v-model:value="module.b_i18n" />
|
||||
<div class="i18n-editor-row">
|
||||
<a-input v-model:value="module.b_i18n" />
|
||||
<ModuleTranslationEditor
|
||||
v-model:translations="translations"
|
||||
:i18n-key="module.b_i18n"
|
||||
:disabled="!module.b_i18n?.trim()"
|
||||
/>
|
||||
</div>
|
||||
</a-form-item>
|
||||
<a-form-item label="查询表或视图">
|
||||
<a-input v-model:value="module.b_viewtable" />
|
||||
@@ -81,12 +93,9 @@ function updateRoute(value) {
|
||||
<a-form-item label="主键字段">
|
||||
<a-input v-model:value="module.b_keyfield" />
|
||||
</a-form-item>
|
||||
<a-form-item label="排序字段">
|
||||
<a-form-item label="排序条件">
|
||||
<a-input v-model:value="module.b_orderfield" />
|
||||
</a-form-item>
|
||||
<a-form-item label="排序方向">
|
||||
<a-segmented v-model:value="module.b_ordertype" :options="['ASC', 'DESC']" />
|
||||
</a-form-item>
|
||||
<a-form-item label="显示顺序">
|
||||
<a-input-number v-model:value="module.b_xh" :min="0" />
|
||||
</a-form-item>
|
||||
@@ -100,7 +109,7 @@ function updateRoute(value) {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
padding-top: 10px;
|
||||
padding-top: 10px 0;
|
||||
}
|
||||
|
||||
.directory-form {
|
||||
@@ -124,6 +133,17 @@ function updateRoute(value) {
|
||||
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;
|
||||
}
|
||||
|
||||
+156
-13
@@ -1,29 +1,165 @@
|
||||
<script setup>
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { App } from "antdv-next";
|
||||
import { Plus, Trash2 } from "@lucide/vue";
|
||||
import FmsTable from "@/components/FmsTable.vue";
|
||||
import { nextIdApi } from "@/services/api";
|
||||
|
||||
const props = defineProps({
|
||||
moduleId: { type: [String, Number], default: "" },
|
||||
moduleCode: { type: String, default: "" },
|
||||
});
|
||||
|
||||
const fields = defineModel("fields", { type: Array, default: () => [] });
|
||||
const { message } = App.useApp();
|
||||
const adding = ref(false);
|
||||
const selectedRowKeys = ref([]);
|
||||
|
||||
const columns = [
|
||||
{ dataIndex: "b_field", title: "字段", width: 150 },
|
||||
{ dataIndex: "b_name", title: "名称", width: 160 },
|
||||
{ dataIndex: "b_type", title: "类型", width: 120 },
|
||||
{ dataIndex: "b_canlist", title: "列表", width: 64, align: "center", headerAlign: "center" },
|
||||
{ dataIndex: "b_canform", title: "表单", width: 64, align: "center", headerAlign: "center" },
|
||||
{ dataIndex: "b_must", title: "必填", width: 64, align: "center", headerAlign: "center" },
|
||||
{ dataIndex: "b_canquery", title: "查询", width: 64, align: "center", headerAlign: "center" },
|
||||
{ dataIndex: "b_readonly", title: "只读", width: 64, align: "center", headerAlign: "center" },
|
||||
{ dataIndex: "b_width", title: "宽度", width: 90 },
|
||||
{ dataIndex: "b_xh", title: "顺序", width: 80 },
|
||||
];
|
||||
const rowSelection = computed(() => ({
|
||||
selectedRowKeys: selectedRowKeys.value,
|
||||
onChange: (keys) => {
|
||||
selectedRowKeys.value = keys;
|
||||
},
|
||||
}));
|
||||
|
||||
watch(
|
||||
() => fields.value.map((item) => String(item.b_id)),
|
||||
(keys) => {
|
||||
const available = new Set(keys);
|
||||
selectedRowKeys.value = selectedRowKeys.value.filter((key) => available.has(String(key)));
|
||||
},
|
||||
);
|
||||
|
||||
function nextFieldCode() {
|
||||
const existing = new Set(fields.value.map((item) => item.b_field));
|
||||
let index = 1;
|
||||
while (existing.has(`b_new_${index}`)) index += 1;
|
||||
return `b_new_${index}`;
|
||||
}
|
||||
|
||||
async function onAdd() {
|
||||
if (!props.moduleId || adding.value) return;
|
||||
adding.value = true;
|
||||
try {
|
||||
const code = nextFieldCode();
|
||||
const id = String((await nextIdApi()).data);
|
||||
fields.value = [
|
||||
...fields.value,
|
||||
{
|
||||
b_id: id,
|
||||
b_module_id: props.moduleId,
|
||||
b_field: code,
|
||||
b_name: "新字段",
|
||||
b_i18n: props.moduleCode ? `field.${props.moduleCode}.${code}` : null,
|
||||
b_type: "string",
|
||||
b_canlist: 1,
|
||||
b_canform: 1,
|
||||
b_must: 0,
|
||||
b_canquery: 0,
|
||||
b_readonly: 0,
|
||||
b_width: 120,
|
||||
b_value: null,
|
||||
b_xh: (fields.value.length + 1) * 10,
|
||||
b_canuse: 1,
|
||||
},
|
||||
];
|
||||
} catch (error) {
|
||||
message.error(error.message || "字段创建失败");
|
||||
} finally {
|
||||
adding.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function onBatchDelete() {
|
||||
const selected = new Set(selectedRowKeys.value.map(String));
|
||||
fields.value = fields.value.filter((item) => !selected.has(String(item.b_id)));
|
||||
selectedRowKeys.value = [];
|
||||
}
|
||||
|
||||
const columns = ref([
|
||||
{ dataIndex: "b_field", title: "字段", width: 150, editor: { maxlength: 50 } },
|
||||
{ dataIndex: "b_name", title: "名称", width: 150, editor: { maxlength: 100 } },
|
||||
{ dataIndex: "b_i18n", title: "多语言键", width: 230, editor: { maxlength: 150 } },
|
||||
{
|
||||
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="adding" @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"
|
||||
:columns="columns"
|
||||
v-model:columns="columns"
|
||||
:data-source="fields"
|
||||
:row-selection="rowSelection"
|
||||
/>
|
||||
</section>
|
||||
</template>
|
||||
@@ -37,6 +173,13 @@ const columns = [
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex: none;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.sub-table {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
|
||||
@@ -1,24 +1,107 @@
|
||||
<script setup>
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { App } from "antdv-next";
|
||||
import { Plus, Trash2 } from "@lucide/vue";
|
||||
import FmsTable from "@/components/FmsTable.vue";
|
||||
import { nextIdApi } from "@/services/api";
|
||||
|
||||
const props = defineProps({
|
||||
moduleId: { type: [String, Number], default: "" },
|
||||
moduleCode: { type: String, default: "" },
|
||||
});
|
||||
|
||||
const powers = defineModel("powers", { type: Array, default: () => [] });
|
||||
const { message } = App.useApp();
|
||||
const adding = ref(false);
|
||||
const selectedRowKeys = ref([]);
|
||||
|
||||
const columns = [
|
||||
{ dataIndex: "b_code", title: "权限代码", width: 180 },
|
||||
{ dataIndex: "b_name", title: "名称", width: 200 },
|
||||
{ dataIndex: "b_i18n", title: "多语言键", width: 200 },
|
||||
{ dataIndex: "b_canuse", title: "启用", width: 74, align: "center", headerAlign: "center" },
|
||||
{ dataIndex: "b_xh", title: "顺序", width: 90 },
|
||||
];
|
||||
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}`;
|
||||
}
|
||||
|
||||
async function onAdd() {
|
||||
if (!props.moduleId || adding.value) return;
|
||||
adding.value = true;
|
||||
try {
|
||||
const code = nextPowerCode();
|
||||
const id = String((await nextIdApi()).data);
|
||||
powers.value = [
|
||||
...powers.value,
|
||||
{
|
||||
b_id: id,
|
||||
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,
|
||||
},
|
||||
];
|
||||
} catch (error) {
|
||||
message.error(error.message || "权限创建失败");
|
||||
} finally {
|
||||
adding.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function onBatchDelete() {
|
||||
const selected = new Set(selectedRowKeys.value.map(String));
|
||||
powers.value = powers.value.filter((item) => !selected.has(String(item.b_id)));
|
||||
selectedRowKeys.value = [];
|
||||
}
|
||||
|
||||
const columns = ref([
|
||||
{ dataIndex: "b_code", title: "权限代码", width: 180, editor: { maxlength: 50 } },
|
||||
{ dataIndex: "b_name", title: "名称", width: 180, editor: { maxlength: 100 } },
|
||||
{ dataIndex: "b_i18n", title: "多语言键", width: 240, editor: { maxlength: 150 } },
|
||||
{
|
||||
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" :loading="adding" @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"
|
||||
:columns="columns"
|
||||
v-model:columns="columns"
|
||||
:data-source="powers"
|
||||
:row-selection="rowSelection"
|
||||
/>
|
||||
</section>
|
||||
</template>
|
||||
@@ -32,6 +115,13 @@ const columns = [
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex: none;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.sub-table {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
<script setup>
|
||||
import { 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 },
|
||||
});
|
||||
const translations = defineModel("translations", { type: Object, default: () => ({}) });
|
||||
const { message } = App.useApp();
|
||||
const dialog = reactive({ open: false, key: "", values: {} });
|
||||
|
||||
function openEditor() {
|
||||
const key = props.i18nKey.trim();
|
||||
if (!isValidI18nKey(key)) {
|
||||
message.warning("请先填写正确的多语言键");
|
||||
return;
|
||||
}
|
||||
dialog.key = key;
|
||||
dialog.values = Object.fromEntries(
|
||||
I18N_LOCALES.map((locale) => [locale.value, translations.value[key]?.[locale.value] || ""]),
|
||||
);
|
||||
dialog.open = true;
|
||||
}
|
||||
|
||||
function saveTranslations() {
|
||||
translations.value = {
|
||||
...translations.value,
|
||||
[dialog.key]: { ...dialog.values },
|
||||
};
|
||||
dialog.open = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-button :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" disabled />
|
||||
</a-form-item>
|
||||
<a-form-item v-for="locale in I18N_LOCALES" :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>
|
||||
@@ -1,57 +0,0 @@
|
||||
<script setup>
|
||||
import { computed } from "vue";
|
||||
import FmsTable from "@/components/FmsTable.vue";
|
||||
|
||||
const LOCALES = [
|
||||
{ value: "zh-CN", label: "简体中文" },
|
||||
{ value: "en-US", label: "English" },
|
||||
{ value: "zh-TW", label: "繁體中文" },
|
||||
];
|
||||
|
||||
const props = defineProps({
|
||||
keys: { type: Array, default: () => [] },
|
||||
});
|
||||
const translations = defineModel("translations", { type: Object, default: () => ({}) });
|
||||
|
||||
const rows = computed(() =>
|
||||
props.keys.map((key) => {
|
||||
const entry = translations.value[key] || {};
|
||||
const row = { key };
|
||||
LOCALES.forEach((locale) => {
|
||||
row[locale.value] = entry[locale.value];
|
||||
});
|
||||
return row;
|
||||
}),
|
||||
);
|
||||
|
||||
const columns = [
|
||||
{ dataIndex: "key", title: "多语言键", width: 280, fixed: "left" },
|
||||
...LOCALES.map((locale) => ({ dataIndex: locale.value, title: locale.label, width: 220 })),
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="translation-pane">
|
||||
<FmsTable
|
||||
class="sub-table"
|
||||
row-key="key"
|
||||
:columns="columns"
|
||||
:data-source="rows"
|
||||
/>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.translation-pane {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sub-table {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -5,6 +5,7 @@ import FmsTree from "@/components/FmsTree.vue";
|
||||
|
||||
const props = defineProps({
|
||||
modules: { type: Array, required: true },
|
||||
protectedKeys: { type: Array, default: () => [] },
|
||||
selectedKey: { type: String, default: "" },
|
||||
loading: Boolean,
|
||||
canCreate: Boolean,
|
||||
@@ -16,15 +17,16 @@ const contextNode = ref(null);
|
||||
|
||||
const treeData = computed(() => {
|
||||
const keyword = searchText.value.trim().toLowerCase();
|
||||
const codeById = new Map(props.modules.map((item) => [item.b_id, item.b_code]));
|
||||
const visible = keyword
|
||||
? new Set(
|
||||
props.modules
|
||||
.filter(
|
||||
(item) =>
|
||||
String(item.b_id).toLowerCase().includes(keyword) ||
|
||||
String(item.b_code).toLowerCase().includes(keyword) ||
|
||||
String(item.b_name).toLowerCase().includes(keyword),
|
||||
)
|
||||
.flatMap((item) => [item.b_id, item.b_parent_id]),
|
||||
.flatMap((item) => [item.b_code, codeById.get(item.b_parent_id)].filter(Boolean)),
|
||||
)
|
||||
: null;
|
||||
const children = new Map();
|
||||
@@ -35,11 +37,11 @@ const treeData = computed(() => {
|
||||
});
|
||||
const build = (parent) =>
|
||||
(children.get(parent) || [])
|
||||
.filter((item) => !visible || visible.has(item.b_id))
|
||||
.filter((item) => !visible || visible.has(item.b_code))
|
||||
.map((item) => {
|
||||
const nodeChildren = build(item.b_id);
|
||||
return {
|
||||
key: item.b_id,
|
||||
key: item.b_code,
|
||||
title: item.b_name,
|
||||
children: nodeChildren,
|
||||
isLeaf: nodeChildren.length === 0,
|
||||
@@ -69,7 +71,7 @@ const contextMenu = computed(() => {
|
||||
key: "delete",
|
||||
label: "删除",
|
||||
danger: true,
|
||||
disabled: !node || !props.canDelete || ["M_SYSTEM", "M_MODULE"].includes(node.key),
|
||||
disabled: !node || !props.canDelete || props.protectedKeys.includes(String(node.key)),
|
||||
},
|
||||
],
|
||||
onClick: ({ key }) => {
|
||||
|
||||
Reference in new issue
Block a user