20260723173317
This commit is contained in:
1 parent
59b3ccfb1c
commit
5f73b54802
33 files changed
+3064
-2108
No files matched your search
@@ -14,9 +14,7 @@ const openKeys = ref(["system"]);
|
||||
const effectiveCollapsed = computed(() => appStore.sidebarCollapsed);
|
||||
const menuItems = computed(() => {
|
||||
const systemPages = [
|
||||
{ key: "/system/metadata", label: "元数据配置", moduleCode: "m_metadata" },
|
||||
{ key: "/system/i18n", label: "多语言管理", moduleCode: "m_i18n" },
|
||||
{ key: "/system/authorization", label: "用户授权", moduleCode: "m_authorization" },
|
||||
{ key: "/system/module-management", label: "模块管理", moduleCode: "m_metadata" },
|
||||
].filter((item) => permissionStore.canModule(item.moduleCode));
|
||||
const items = [{ key: "/dashboard", icon: menuIcon(House), label: "工作台" }];
|
||||
if (systemPages.length)
|
||||
|
||||
@@ -175,7 +175,7 @@ function toggleMaximize() {
|
||||
<ArrowLeft />
|
||||
</button>
|
||||
</a-tooltip>
|
||||
<span class="tab-action__divider" />
|
||||
|
||||
<a-tooltip title="刷新当前页" placement="bottom">
|
||||
<button
|
||||
type="button"
|
||||
@@ -186,7 +186,7 @@ function toggleMaximize() {
|
||||
<RotateCw />
|
||||
</button>
|
||||
</a-tooltip>
|
||||
<span class="tab-action__divider" />
|
||||
|
||||
<a-tooltip title="返回控制台" placement="bottom">
|
||||
<button
|
||||
type="button"
|
||||
@@ -357,7 +357,7 @@ function toggleMaximize() {
|
||||
.tab-nav-button {
|
||||
display: grid;
|
||||
flex: none;
|
||||
width: 26px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
padding: 0;
|
||||
place-items: center;
|
||||
@@ -386,23 +386,21 @@ function toggleMaximize() {
|
||||
.tab-actions {
|
||||
display: flex;
|
||||
flex: none;
|
||||
height: 28px;
|
||||
align-items: center;
|
||||
border: 1px solid var(--fms-border);
|
||||
border-radius: 6px;
|
||||
background: var(--fms-surface);
|
||||
overflow: hidden;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.tab-action {
|
||||
display: grid;
|
||||
width: 30px;
|
||||
height: 100%;
|
||||
flex: none;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
padding: 0;
|
||||
place-items: center;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
border-radius: 4px;
|
||||
color: var(--fms-text-secondary);
|
||||
background: var(--tabs-idle-bg);
|
||||
cursor: pointer;
|
||||
transition:
|
||||
color 0.15s ease,
|
||||
@@ -414,19 +412,8 @@ function toggleMaximize() {
|
||||
}
|
||||
|
||||
.lucide {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.tab-action__divider {
|
||||
flex: none;
|
||||
width: 1px;
|
||||
height: 16px;
|
||||
background: var(--fms-border);
|
||||
}
|
||||
|
||||
.tab-maximize {
|
||||
margin-left: 2px;
|
||||
}
|
||||
</style>
|
||||
@@ -25,38 +25,16 @@ const routes = [
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "system/metadata",
|
||||
name: "system-metadata",
|
||||
path: "system/module-management",
|
||||
name: "system-module-management",
|
||||
component: () => import("@/views/module-management/index.vue"),
|
||||
meta: {
|
||||
title: "元数据配置",
|
||||
title: "模块管理",
|
||||
section: "系统管理",
|
||||
menuKey: "/system/metadata",
|
||||
menuKey: "/system/module-management",
|
||||
moduleCode: "m_metadata",
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "system/authorization",
|
||||
name: "system-authorization",
|
||||
component: () => import("@/views/authorization-management/index.vue"),
|
||||
meta: {
|
||||
title: "用户授权",
|
||||
section: "系统管理",
|
||||
menuKey: "/system/authorization",
|
||||
moduleCode: "m_authorization",
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "system/i18n",
|
||||
name: "system-i18n",
|
||||
component: () => import("@/views/i18n-management/index.vue"),
|
||||
meta: {
|
||||
title: "多语言管理",
|
||||
section: "系统管理",
|
||||
menuKey: "/system/i18n",
|
||||
moduleCode: "m_i18n",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -89,11 +89,12 @@ export function saveObjectApi(requests) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取下一个雪花 ID。
|
||||
* @returns {Promise<ApiResult<string>>} 取号响应,雪花 ID 使用字符串避免精度丢失。
|
||||
* 获取下一个雪花 ID;传入 count 可批量获取指定数量的 ID(返回数组)。
|
||||
* @param {number} [count=1] 需要获取的 ID 数量,缺省 1。
|
||||
* @returns {Promise<ApiResult<string|string[]>>} count 为 1 时 data 为单个 ID 字符串,大于 1 时为 ID 字符串数组。
|
||||
*/
|
||||
export function nextIdApi() {
|
||||
return get("/data/nextid");
|
||||
export function nextIdApi(count = 1) {
|
||||
return get("/data/nextid", { params: { count } });
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -25,11 +25,13 @@ export async function allocateTemporaryIds(rowGroups) {
|
||||
rowGroups
|
||||
.flat()
|
||||
.map((row) => String(row?.b_id ?? ""))
|
||||
.filter((id) => /^-\d+$/.test(id)),
|
||||
.filter((id) => /^-\d+$/.test(id))
|
||||
),
|
||||
];
|
||||
const generated = await Promise.all(temporaryIds.map(() => nextIdApi()));
|
||||
const mapping = new Map(temporaryIds.map((id, index) => [id, String(generated[index].data)]));
|
||||
if (!temporaryIds.length) return new Map();
|
||||
const res = await nextIdApi(temporaryIds.length);
|
||||
const ids = Array.isArray(res.data) ? res.data : [res.data];
|
||||
const mapping = new Map(temporaryIds.map((id, index) => [id, String(ids[index])]));
|
||||
rowGroups.flat().forEach((row) => {
|
||||
const id = mapping.get(String(row?.b_id));
|
||||
if (id) row.b_id = id;
|
||||
|
||||
@@ -1,300 +0,0 @@
|
||||
<script setup>
|
||||
import { computed, h, onMounted, ref } from "vue";
|
||||
import { App, Checkbox } from "antdv-next";
|
||||
import { RefreshCw, Save, ShieldCheck } from "@lucide/vue";
|
||||
import FmsTable from "@/components/fms-table/FmsTable.vue";
|
||||
import { loadDataApi, saveObjectApi } from "@/services/api";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import { allocateTemporaryIds, diffRows, tableChange } from "@/utils/dataChanges";
|
||||
import { createTempId } from "@/utils/tempId";
|
||||
|
||||
const { message } = App.useApp();
|
||||
const authStore = useAuthStore();
|
||||
const loading = ref(false);
|
||||
const saving = ref(false);
|
||||
const users = ref([]);
|
||||
const modules = ref([]);
|
||||
const powers = ref([]);
|
||||
const selectedUserId = ref();
|
||||
const moduleIds = ref([]);
|
||||
const powerIds = ref([]);
|
||||
const originalModules = ref([]);
|
||||
const originalPowers = ref([]);
|
||||
const userOptions = computed(() =>
|
||||
users.value.map((item) => ({
|
||||
value: item.b_id,
|
||||
label: `${item.b_name} (${item.b_account})`,
|
||||
})),
|
||||
);
|
||||
const moduleRows = computed(() =>
|
||||
modules.value.filter((item) => item.b_module_type !== "directory"),
|
||||
);
|
||||
const powersByModule = computed(() => {
|
||||
const result = new Map();
|
||||
powers.value.forEach((power) => {
|
||||
const key = String(power.b_module_id);
|
||||
if (!result.has(key)) result.set(key, []);
|
||||
result.get(key).push(power);
|
||||
});
|
||||
return result;
|
||||
});
|
||||
const moduleIdSet = computed(() => new Set(moduleIds.value.map(String)));
|
||||
const powerIdSet = computed(() => new Set(powerIds.value.map(String)));
|
||||
const columns = computed(() => [
|
||||
{ dataIndex: "b_name", title: "模块", width: 220 },
|
||||
{ dataIndex: "b_code", title: "模块编码", width: 160 },
|
||||
{
|
||||
dataIndex: "__moduleAccess",
|
||||
title: "访问",
|
||||
width: 80,
|
||||
align: "center",
|
||||
customCell: ({ row }) =>
|
||||
h(Checkbox, {
|
||||
checked: moduleIdSet.value.has(String(row.b_id)),
|
||||
"onUpdate:checked": (checked) => toggleModule(row, checked),
|
||||
}),
|
||||
},
|
||||
{
|
||||
dataIndex: "__powers",
|
||||
title: "操作权限",
|
||||
width: 520,
|
||||
customCell: ({ row }) =>
|
||||
h(
|
||||
"div",
|
||||
{ class: "power-options" },
|
||||
(powersByModule.value.get(String(row.b_id)) || []).map((power) =>
|
||||
h(
|
||||
Checkbox,
|
||||
{
|
||||
checked: powerIdSet.value.has(String(power.b_id)),
|
||||
"onUpdate:checked": (checked) => togglePower(row, power, checked),
|
||||
},
|
||||
() => power.b_name,
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
]);
|
||||
|
||||
async function loadBase() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const [userResult, moduleResult, powerResult] = await Promise.all([
|
||||
loadDataApi("b_user", "b_canuse = 1", "b_name ASC", [
|
||||
"b_id",
|
||||
"b_account",
|
||||
"b_name",
|
||||
"b_canuse",
|
||||
]),
|
||||
loadDataApi("b_module", "b_canuse = 1", "b_xh ASC"),
|
||||
loadDataApi("b_module_power", "b_canuse = 1", "b_xh ASC"),
|
||||
]);
|
||||
users.value = userResult.data || [];
|
||||
modules.value = moduleResult.data || [];
|
||||
powers.value = powerResult.data || [];
|
||||
if (!selectedUserId.value && users.value.length) selectedUserId.value = users.value[0].b_id;
|
||||
await loadPermissions();
|
||||
} catch (error) {
|
||||
message.error(error.message || "授权数据加载失败");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadPermissions() {
|
||||
if (!selectedUserId.value) return;
|
||||
const userId = String(selectedUserId.value);
|
||||
if (!/^\d+$/.test(userId)) return message.warning("用户 ID 格式不正确");
|
||||
try {
|
||||
const [moduleResult, powerResult] = await Promise.all([
|
||||
loadDataApi("b_user_module", `b_user_id = ${userId}`, "b_id ASC"),
|
||||
loadDataApi("b_user_power", `b_user_id = ${userId}`, "b_id ASC"),
|
||||
]);
|
||||
originalModules.value = moduleResult.data || [];
|
||||
originalPowers.value = powerResult.data || [];
|
||||
moduleIds.value = originalModules.value.map((item) => item.b_module_id);
|
||||
powerIds.value = originalPowers.value.map((item) => item.b_power_id);
|
||||
} catch (error) {
|
||||
message.error(error.message || "用户权限加载失败");
|
||||
}
|
||||
}
|
||||
|
||||
function toggleModule(module, checked) {
|
||||
const values = new Set(moduleIds.value.map(String));
|
||||
if (checked) values.add(String(module.b_id));
|
||||
else {
|
||||
values.delete(String(module.b_id));
|
||||
const modulePowerIds = new Set(
|
||||
(powersByModule.value.get(String(module.b_id)) || []).map((item) => String(item.b_id)),
|
||||
);
|
||||
powerIds.value = powerIds.value.filter((value) => !modulePowerIds.has(String(value)));
|
||||
}
|
||||
moduleIds.value = [...values];
|
||||
}
|
||||
|
||||
function togglePower(module, power, checked) {
|
||||
if (checked && !moduleIdSet.value.has(String(module.b_id))) {
|
||||
moduleIds.value = [...moduleIds.value, module.b_id];
|
||||
}
|
||||
const values = new Set(powerIds.value.map(String));
|
||||
if (checked) values.add(String(power.b_id));
|
||||
else values.delete(String(power.b_id));
|
||||
powerIds.value = [...values];
|
||||
}
|
||||
|
||||
async function save() {
|
||||
if (!selectedUserId.value) return;
|
||||
saving.value = true;
|
||||
try {
|
||||
const currentModules = moduleIds.value.map((moduleId) => {
|
||||
const old = originalModules.value.find(
|
||||
(item) => String(item.b_module_id) === String(moduleId),
|
||||
);
|
||||
return (
|
||||
old || {
|
||||
b_id: createTempId(),
|
||||
b_user_id: selectedUserId.value,
|
||||
b_module_id: moduleId,
|
||||
b_inputuser_id: authStore.user.id,
|
||||
b_inputdatetime: new Date().toISOString().slice(0, 19).replace("T", " "),
|
||||
}
|
||||
);
|
||||
});
|
||||
const currentPowers = powerIds.value.map((powerId) => {
|
||||
const old = originalPowers.value.find((item) => String(item.b_power_id) === String(powerId));
|
||||
return (
|
||||
old || {
|
||||
b_id: createTempId(),
|
||||
b_user_id: selectedUserId.value,
|
||||
b_power_id: powerId,
|
||||
b_inputuser_id: authStore.user.id,
|
||||
b_inputdatetime: new Date().toISOString().slice(0, 19).replace("T", " "),
|
||||
}
|
||||
);
|
||||
});
|
||||
await allocateTemporaryIds([currentModules, currentPowers]);
|
||||
const moduleChanges = diffRows(currentModules, originalModules.value);
|
||||
const powerChanges = diffRows(currentPowers, originalPowers.value);
|
||||
await saveObjectApi([
|
||||
tableChange("b_user_power", powerChanges),
|
||||
tableChange("b_user_module", moduleChanges),
|
||||
]);
|
||||
message.success("用户授权已保存");
|
||||
await loadPermissions();
|
||||
} catch (error) {
|
||||
message.error(error.message || "授权保存失败");
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadBase);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="authorization-page">
|
||||
<header class="page-header">
|
||||
<div class="page-title">
|
||||
<h1><ShieldCheck />用户授权</h1>
|
||||
<span>模块访问与操作权限</span>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<a-button :loading="loading" @click="loadBase"><RefreshCw />刷新</a-button>
|
||||
<a-button type="primary" :loading="saving" :disabled="!selectedUserId" @click="save">
|
||||
<Save />保存授权
|
||||
</a-button>
|
||||
</div>
|
||||
</header>
|
||||
<div class="user-bar">
|
||||
<span>用户</span>
|
||||
<a-select
|
||||
v-model:value="selectedUserId"
|
||||
show-search
|
||||
:options="userOptions"
|
||||
@change="loadPermissions"
|
||||
/>
|
||||
</div>
|
||||
<div class="table-body">
|
||||
<a-spin :spinning="loading">
|
||||
<FmsTable
|
||||
:columns="columns"
|
||||
:data-source="moduleRows"
|
||||
row-key="b_id"
|
||||
:col-resizable="false"
|
||||
/>
|
||||
</a-spin>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.authorization-page {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--fms-border);
|
||||
background: var(--fms-surface);
|
||||
}
|
||||
|
||||
.page-header,
|
||||
.page-title,
|
||||
.page-title h1,
|
||||
.header-actions,
|
||||
.user-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
flex: none;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid var(--fms-border);
|
||||
}
|
||||
|
||||
.page-title,
|
||||
.page-title h1,
|
||||
.header-actions,
|
||||
.user-bar {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.page-title h1 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.page-title span {
|
||||
color: var(--fms-text-secondary);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.user-bar {
|
||||
flex: none;
|
||||
|
||||
.ant-select {
|
||||
width: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
.table-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
|
||||
:deep(.ant-spin-nested-loading),
|
||||
:deep(.ant-spin-container) {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:deep(.power-options) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,193 +0,0 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import { App } from "antdv-next";
|
||||
import { Languages, Plus, RefreshCw, Save, Trash2 } from "@lucide/vue";
|
||||
import FmsTable from "@/components/fms-table/FmsTable.vue";
|
||||
import { loadDataApi, saveObjectApi } from "@/services/api";
|
||||
import { allocateTemporaryIds, cloneData, diffRows, tableChange } from "@/utils/dataChanges";
|
||||
import { I18N_LOCALES } from "@/utils/i18n";
|
||||
import { createTempId } from "@/utils/tempId";
|
||||
|
||||
const { message } = App.useApp();
|
||||
const loading = ref(false);
|
||||
const saving = ref(false);
|
||||
const keyword = ref("");
|
||||
const rows = ref([]);
|
||||
const original = ref([]);
|
||||
const activeRowKey = ref(null);
|
||||
const selectedRowKeys = ref([]);
|
||||
const rowSelection = computed(() => ({
|
||||
selectedRowKeys: selectedRowKeys.value,
|
||||
onChange: (keys) => {
|
||||
selectedRowKeys.value = keys;
|
||||
},
|
||||
}));
|
||||
const columns = [
|
||||
{ dataIndex: "b_key", title: "多语言键", width: 320, editor: { maxlength: 150 } },
|
||||
{
|
||||
dataIndex: "b_locale",
|
||||
title: "语言",
|
||||
width: 150,
|
||||
editor: { type: "select", options: I18N_LOCALES },
|
||||
},
|
||||
{ dataIndex: "b_value", title: "翻译内容", width: 420, editor: { maxlength: 500 } },
|
||||
];
|
||||
const filteredRows = computed(() => {
|
||||
const value = keyword.value.trim().toLowerCase();
|
||||
if (!value) return rows.value;
|
||||
return rows.value.filter((row) =>
|
||||
`${row.b_key} ${row.b_locale} ${row.b_value}`.toLowerCase().includes(value),
|
||||
);
|
||||
});
|
||||
|
||||
async function load() {
|
||||
loading.value = true;
|
||||
try {
|
||||
rows.value = (await loadDataApi("b_i18n", null, "b_key ASC")).data || [];
|
||||
original.value = cloneData(rows.value);
|
||||
selectedRowKeys.value = [];
|
||||
activeRowKey.value = null;
|
||||
} catch (error) {
|
||||
message.error(error.message || "多语言加载失败");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function add() {
|
||||
const row = { b_id: createTempId(), b_key: "", b_locale: "en-US", b_value: "" };
|
||||
rows.value.unshift(row);
|
||||
activeRowKey.value = row.b_id;
|
||||
}
|
||||
|
||||
function removeSelected() {
|
||||
const selected = new Set(selectedRowKeys.value.map(String));
|
||||
rows.value = rows.value.filter((row) => !selected.has(String(row.b_id)));
|
||||
selectedRowKeys.value = [];
|
||||
activeRowKey.value = null;
|
||||
}
|
||||
|
||||
async function save() {
|
||||
if (rows.value.some((row) => !row.b_key?.trim() || !row.b_locale || !row.b_value?.trim())) {
|
||||
return message.warning("多语言键、语言和翻译内容不能为空");
|
||||
}
|
||||
const compound = rows.value.map((row) => `${row.b_key}\0${row.b_locale}`);
|
||||
if (new Set(compound).size !== compound.length) return message.warning("多语言键和语言不能重复");
|
||||
saving.value = true;
|
||||
try {
|
||||
await allocateTemporaryIds([rows.value]);
|
||||
const changes = diffRows(rows.value, original.value);
|
||||
if (!changes.inserts.length && !changes.updates.length && !changes.deletes.length) {
|
||||
return message.info("没有需要保存的修改");
|
||||
}
|
||||
await saveObjectApi([tableChange("b_i18n", changes)]);
|
||||
message.success("多语言已保存");
|
||||
await load();
|
||||
} catch (error) {
|
||||
message.error(error.message || "保存失败");
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(load);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="i18n-page">
|
||||
<header class="page-header">
|
||||
<div class="page-title">
|
||||
<h1><Languages />多语言管理</h1>
|
||||
<span>{{ rows.length }} 条翻译</span>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<a-button :loading="loading" @click="load"><RefreshCw />刷新</a-button>
|
||||
<a-button @click="add"><Plus />新增</a-button>
|
||||
<a-button danger :disabled="!selectedRowKeys.length" @click="removeSelected">
|
||||
<Trash2 />删除
|
||||
</a-button>
|
||||
<a-button type="primary" :loading="saving" @click="save"><Save />保存全部</a-button>
|
||||
</div>
|
||||
</header>
|
||||
<a-input-search
|
||||
v-model:value="keyword"
|
||||
allow-clear
|
||||
placeholder="搜索多语言键、语言或内容"
|
||||
class="search"
|
||||
/>
|
||||
<div class="table-body">
|
||||
<a-spin :spinning="loading">
|
||||
<FmsTable
|
||||
:columns="columns"
|
||||
:data-source="filteredRows"
|
||||
row-key="b_id"
|
||||
:col-resizable="false"
|
||||
:row-selection="rowSelection"
|
||||
:active-row-key="activeRowKey"
|
||||
@row-click="(_event, row) => (activeRowKey = row.b_id)"
|
||||
/>
|
||||
</a-spin>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.i18n-page {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--fms-border);
|
||||
background: var(--fms-surface);
|
||||
}
|
||||
|
||||
.page-header,
|
||||
.page-title,
|
||||
.page-title h1,
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
flex: none;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid var(--fms-border);
|
||||
}
|
||||
|
||||
.page-title,
|
||||
.page-title h1,
|
||||
.header-actions {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.page-title h1 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.page-title span {
|
||||
color: var(--fms-text-secondary);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.search {
|
||||
width: 360px;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.table-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
|
||||
:deep(.ant-spin-nested-loading),
|
||||
:deep(.ant-spin-container) {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,256 @@
|
||||
<script setup>
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { message } from "antdv-next";
|
||||
import FmsTree from "@/components/FmsTree.vue";
|
||||
import { saveObjectApi } from "@/services/api";
|
||||
import { cloneData, diffRows, allocateTemporaryIds, tableChange, hasChanges } from "@/utils/dataChanges";
|
||||
import { createTempId } from "@/utils/tempId";
|
||||
|
||||
const props = defineProps({
|
||||
visible: { type: Boolean, default: false },
|
||||
moduleId: { type: String, required: true },
|
||||
groups: { type: Array, default: () => [] },
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:visible", "update"]);
|
||||
|
||||
const localGroups = ref([]);
|
||||
const selectedGroupId = ref(null);
|
||||
const editForm = ref(null);
|
||||
|
||||
watch(
|
||||
() => props.groups,
|
||||
(val) => {
|
||||
localGroups.value = cloneData(val || []);
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
const treeData = computed(() => {
|
||||
return buildTreeData(localGroups.value);
|
||||
});
|
||||
|
||||
function buildTreeData(items) {
|
||||
const map = new Map();
|
||||
const roots = [];
|
||||
items.forEach((item) => {
|
||||
item.key = String(item.b_id);
|
||||
map.set(item.key, item);
|
||||
});
|
||||
items.forEach((item) => {
|
||||
if (item.b_parent_id && map.has(String(item.b_parent_id))) {
|
||||
const parent = map.get(String(item.b_parent_id));
|
||||
if (!parent.children) parent.children = [];
|
||||
parent.children.push(item);
|
||||
} else {
|
||||
roots.push(item);
|
||||
}
|
||||
});
|
||||
return roots.map((r) => ({
|
||||
key: r.key,
|
||||
title: r.b_title || `分组-${r.b_id}`,
|
||||
children: r.children?.map((c) => ({
|
||||
key: c.key,
|
||||
title: c.b_title || `分组-${c.b_id}`,
|
||||
children: c.children?.map((gc) => ({
|
||||
key: gc.key,
|
||||
title: gc.b_title || `分组-${gc.b_id}`,
|
||||
})),
|
||||
})) || [],
|
||||
}));
|
||||
}
|
||||
|
||||
const selectedGroup = computed(() =>
|
||||
localGroups.value.find((g) => String(g.b_id) === selectedGroupId.value),
|
||||
);
|
||||
|
||||
function onTreeSelect(keys) {
|
||||
if (keys.length > 0) {
|
||||
selectedGroupId.value = String(keys[0]);
|
||||
const g = selectedGroup.value;
|
||||
editForm.value = g ? { ...g } : null;
|
||||
} else {
|
||||
selectedGroupId.value = null;
|
||||
editForm.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
function addGroup(parentId) {
|
||||
const newGroup = {
|
||||
b_id: createTempId(),
|
||||
b_module_id: props.moduleId,
|
||||
b_parent_id: parentId || null,
|
||||
b_title: "",
|
||||
b_i18n: null,
|
||||
b_xh: localGroups.value.length * 10,
|
||||
b_canuse: 1,
|
||||
};
|
||||
localGroups.value.push(newGroup);
|
||||
selectedGroupId.value = String(newGroup.b_id);
|
||||
editForm.value = { ...newGroup };
|
||||
}
|
||||
|
||||
function deleteGroup() {
|
||||
if (!selectedGroupId.value) return;
|
||||
localGroups.value = localGroups.value.filter((g) => String(g.b_id) !== selectedGroupId.value);
|
||||
selectedGroupId.value = null;
|
||||
editForm.value = null;
|
||||
}
|
||||
|
||||
async function saveAllGroups() {
|
||||
const original = cloneData(props.groups || []);
|
||||
const changes = diffRows(localGroups.value, original);
|
||||
const req = tableChange("b_module_field_group", changes);
|
||||
if (!hasChanges(req)) {
|
||||
message.info("没有需要保存的变更");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await allocateTemporaryIds([req.inserts || [], req.updates || [], req.deletes || []]);
|
||||
await saveObjectApi([req]);
|
||||
emit("update", cloneData(localGroups.value));
|
||||
message.success("分组已保存");
|
||||
emit("update:visible", false);
|
||||
} catch (e) {
|
||||
message.error("保存失败:" + (e.message || e));
|
||||
}
|
||||
}
|
||||
|
||||
function onClose() {
|
||||
emit("update:visible", false);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-modal
|
||||
:open="visible"
|
||||
title="分组管理"
|
||||
width="650px"
|
||||
@cancel="onClose"
|
||||
@ok="saveAllGroups"
|
||||
>
|
||||
<div class="group-manager">
|
||||
<div class="group-sidebar">
|
||||
<div class="group-sidebar-header">
|
||||
<span>分组树</span>
|
||||
<a-button size="small" type="primary" @click="addGroup(null)">新增</a-button>
|
||||
</div>
|
||||
<FmsTree
|
||||
v-if="treeData.length > 0"
|
||||
class="group-tree"
|
||||
:tree-data="treeData"
|
||||
:field-names="{ title: 'title', key: 'key', children: 'children' }"
|
||||
:selected-keys="selectedGroupId ? [selectedGroupId] : []"
|
||||
@select="onTreeSelect"
|
||||
>
|
||||
<template #title="{ key, title: nodeTitle }">
|
||||
<span class="group-tree-node">
|
||||
<span>{{ nodeTitle }}</span>
|
||||
<span class="group-tree-node-actions">
|
||||
<a-button size="small" type="text" @click.stop="addGroup(key)">+</a-button>
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
</FmsTree>
|
||||
<a-empty v-else description="暂无分组" />
|
||||
</div>
|
||||
<div class="group-detail" v-if="editForm">
|
||||
<a-form layout="vertical">
|
||||
<a-form-item label="分组标题" required>
|
||||
<a-input
|
||||
:value="editForm.b_title"
|
||||
@update:value="(v) => editForm.b_title = v"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="多语言键">
|
||||
<a-input
|
||||
:value="editForm.b_i18n"
|
||||
@update:value="(v) => editForm.b_i18n = v || null"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="排序号">
|
||||
<a-input-number
|
||||
:value="editForm.b_xh"
|
||||
:min="0"
|
||||
@update:value="(v) => editForm.b_xh = v"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="启用">
|
||||
<a-switch
|
||||
:checked="editForm.b_canuse === 1"
|
||||
@update:checked="(v) => editForm.b_canuse = v ? 1 : 0"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button danger size="small" @click="deleteGroup">删除此分组</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
<a-empty v-else description="选择左侧分组节点编辑" class="group-detail-empty" />
|
||||
</div>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.group-manager {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
min-height: 350px;
|
||||
}
|
||||
|
||||
.group-sidebar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: none;
|
||||
width: 240px;
|
||||
border: 1px solid var(--fms-border);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.group-sidebar-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex: none;
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid var(--fms-border);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.group-tree {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.group-tree-node {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.group-tree-node-actions {
|
||||
display: none;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.group-tree-node:hover .group-tree-node-actions {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.group-detail {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.group-detail-empty {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
@@ -1,115 +0,0 @@
|
||||
<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,65 +1,174 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
parentOptions: { type: Array, default: () => [] },
|
||||
routeOptions: { type: Array, default: () => [] },
|
||||
import { computed, ref, watch } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
moduleId: { type: String, required: true },
|
||||
data: { type: Object, default: null },
|
||||
loading: { type: Boolean, default: false },
|
||||
});
|
||||
const module = defineModel("module", { type: Object, required: true });
|
||||
function setBoolean(field, checked) {
|
||||
module.value[field] = checked ? 1 : 0;
|
||||
|
||||
const emit = defineEmits(["update"]);
|
||||
|
||||
const formData = ref(null);
|
||||
|
||||
watch(
|
||||
() => props.data,
|
||||
(val) => {
|
||||
formData.value = val ? { ...val } : null;
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
const isDirectory = computed(() => formData.value?.b_module_type === "directory");
|
||||
const isPage = computed(() => formData.value?.b_module_type === "page");
|
||||
const isData = computed(() => formData.value?.b_module_type === "data");
|
||||
|
||||
function emitUpdate() {
|
||||
if (formData.value) emit("update", { ...formData.value });
|
||||
}
|
||||
|
||||
function setField(field, value) {
|
||||
if (!formData.value) return;
|
||||
formData.value[field] = value === "" ? null : value;
|
||||
emitUpdate();
|
||||
}
|
||||
</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>
|
||||
<div class="basic-panel" v-if="formData">
|
||||
<div class="panel-body">
|
||||
<a-form layout="vertical" class="basic-form">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="8">
|
||||
<a-form-item label="模块编码" required>
|
||||
<a-input
|
||||
:value="formData.b_code"
|
||||
placeholder="如 m_sea"
|
||||
@update:value="(v) => setField('b_code', v)"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item label="模块名称" required>
|
||||
<a-input
|
||||
:value="formData.b_name"
|
||||
placeholder="模块显示名称"
|
||||
@update:value="(v) => setField('b_name', v)"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item label="模块类型" required>
|
||||
<a-select
|
||||
:value="formData.b_module_type"
|
||||
:options="[
|
||||
{ label: '目录', value: 'directory' },
|
||||
{ label: '页面', value: 'page' },
|
||||
{ label: '数据', value: 'data' },
|
||||
]"
|
||||
@update:value="(v) => { formData.b_module_type = v; emitUpdate(); }"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="8">
|
||||
<a-form-item label="上级模块">
|
||||
<a-input :value="formData.b_parent_id" disabled placeholder="通过左侧树选中后自动设置" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item label="多语言键">
|
||||
<a-input
|
||||
:value="formData.b_i18n"
|
||||
placeholder="如 module.m_sea"
|
||||
@update:value="(v) => setField('b_i18n', v)"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="4">
|
||||
<a-form-item label="排序号">
|
||||
<a-input-number
|
||||
:value="formData.b_xh"
|
||||
:min="0"
|
||||
@update:value="(v) => setField('b_xh', v)"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 页面/数据 专用字段 -->
|
||||
<template v-if="!isDirectory">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="8" v-if="isPage">
|
||||
<a-form-item label="页面路由" required>
|
||||
<a-input
|
||||
:value="formData.b_route"
|
||||
placeholder="如 /sea/list"
|
||||
@update:value="(v) => setField('b_route', v)"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8" v-if="isData">
|
||||
<a-form-item label="查询视图" required>
|
||||
<a-input
|
||||
:value="formData.b_viewtable"
|
||||
placeholder="如 v_sea"
|
||||
@update:value="(v) => setField('b_viewtable', v)"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8" v-if="isData">
|
||||
<a-form-item label="保存表">
|
||||
<a-input
|
||||
:value="formData.b_savetable"
|
||||
placeholder="如 d_sea"
|
||||
@update:value="(v) => setField('b_savetable', v)"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row :gutter="16" v-if="isData">
|
||||
<a-col :span="8">
|
||||
<a-form-item label="主键字段">
|
||||
<a-input
|
||||
:value="formData.b_keyfield"
|
||||
placeholder="如 b_id"
|
||||
@update:value="(v) => setField('b_keyfield', v)"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item label="默认排序">
|
||||
<a-input
|
||||
:value="formData.b_orderfield"
|
||||
placeholder="如 b_xh ASC"
|
||||
@update:value="(v) => setField('b_orderfield', v)"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</template>
|
||||
</a-form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.basic-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding: 16px 20px;
|
||||
}
|
||||
|
||||
.basic-form {
|
||||
display: grid;
|
||||
max-width: 1080px;
|
||||
grid-template-columns: repeat(3, minmax(180px, 1fr));
|
||||
gap: 0 16px;
|
||||
padding-top: 14px;
|
||||
max-width: 960px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,163 @@
|
||||
<script setup>
|
||||
import { computed, ref, watch } from "vue";
|
||||
import FmsTable from "@/components/fms-table/FmsTable.vue";
|
||||
import { createTempId } from "@/utils/tempId";
|
||||
import FieldGroupManager from "./FieldGroupManager.vue";
|
||||
import { createDeleteCell, createSelectCell, createToggleCell } from "./cellRenderers";
|
||||
|
||||
const props = defineProps({
|
||||
moduleId: { type: String, required: true },
|
||||
editConfig: { type: Array, default: () => [] },
|
||||
fields: { type: Array, default: () => [] },
|
||||
groups: { type: Array, default: () => [] },
|
||||
loading: { type: Boolean, default: false },
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update", "updateGroups"]);
|
||||
|
||||
const localEdit = ref([]);
|
||||
const groupManagerVisible = ref(false);
|
||||
|
||||
watch(
|
||||
() => props.editConfig,
|
||||
(val) => {
|
||||
localEdit.value = val?.map((r) => ({ ...r })) || [];
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
function emitUpdate() {
|
||||
emit("update", localEdit.value.map((r) => ({ ...r })));
|
||||
}
|
||||
|
||||
function setField(row, field, value) {
|
||||
row[field] = value === undefined || value === null ? null : value;
|
||||
emitUpdate();
|
||||
}
|
||||
|
||||
const availableFields = computed(() =>
|
||||
(props.fields || [])
|
||||
.filter((f) => f.b_canuse === 1)
|
||||
.map((f) => ({ label: f.b_name || f.b_field, value: String(f.b_id) })),
|
||||
);
|
||||
|
||||
const groupOptions = computed(() =>
|
||||
(props.groups || [])
|
||||
.filter((g) => g.b_canuse === 1)
|
||||
.map((g) => ({ label: g.b_title || `分组-${g.b_id}`, value: String(g.b_id) })),
|
||||
);
|
||||
|
||||
function addRow() {
|
||||
localEdit.value.push({
|
||||
b_id: createTempId(),
|
||||
b_module_id: props.moduleId,
|
||||
b_field_id: null,
|
||||
b_required: 0,
|
||||
b_readonly: 0,
|
||||
b_disabled: 0,
|
||||
b_group_id: null,
|
||||
b_xh: localEdit.value.length * 10,
|
||||
});
|
||||
emitUpdate();
|
||||
}
|
||||
|
||||
function deleteRow(record) {
|
||||
localEdit.value = localEdit.value.filter((r) => r.b_id !== record.b_id);
|
||||
emitUpdate();
|
||||
}
|
||||
|
||||
function makeSwitchCol(field, title, width) {
|
||||
return {
|
||||
dataIndex: field,
|
||||
title,
|
||||
width: width || 60,
|
||||
align: "center",
|
||||
customCell: createToggleCell(field, (row) => setField(row, field, row[field] === 1 ? 0 : 1)),
|
||||
};
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{
|
||||
dataIndex: "b_field_id",
|
||||
title: "字段",
|
||||
width: 160,
|
||||
customCell: createSelectCell({
|
||||
value: "b_field_id",
|
||||
getOptions: () => availableFields.value,
|
||||
onChange: (row, v) => setField(row, "b_field_id", v),
|
||||
placeholder: "选择字段",
|
||||
}),
|
||||
},
|
||||
makeSwitchCol("b_required", "必填"),
|
||||
makeSwitchCol("b_readonly", "只读"),
|
||||
makeSwitchCol("b_disabled", "禁用"),
|
||||
{
|
||||
dataIndex: "b_group_id",
|
||||
title: "编辑分组",
|
||||
width: 160,
|
||||
customCell: createSelectCell({
|
||||
value: "b_group_id",
|
||||
getOptions: () => groupOptions.value,
|
||||
onChange: (row, v) => setField(row, "b_group_id", v),
|
||||
placeholder: "选择分组",
|
||||
}),
|
||||
},
|
||||
{ dataIndex: "b_xh", title: "排序号", width: 80 },
|
||||
{
|
||||
dataIndex: "actions",
|
||||
title: "操作",
|
||||
width: 60,
|
||||
align: "center",
|
||||
customCell: createDeleteCell((row) => deleteRow(row)),
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="edit-config-panel">
|
||||
<div class="panel-toolbar">
|
||||
<a-space>
|
||||
<a-button type="primary" @click="addRow">新增行</a-button>
|
||||
<a-button @click="groupManagerVisible = true">分组管理</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<FmsTable :columns="columns" :data-source="localEdit" row-key="b_id" />
|
||||
</div>
|
||||
<FieldGroupManager
|
||||
v-model:visible="groupManagerVisible"
|
||||
:module-id="moduleId"
|
||||
:groups="groups"
|
||||
@update="(v) => emit('updateGroups', v)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.edit-config-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.panel-toolbar {
|
||||
display: flex;
|
||||
flex: none;
|
||||
gap: 8px;
|
||||
padding: 12px 20px;
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
padding: 16px 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.panel-body > :deep(.fms-table) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -1,131 +0,0 @@
|
||||
<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>
|
||||
@@ -1,52 +1,116 @@
|
||||
<script setup>
|
||||
import { computed } from "vue";
|
||||
import { RefreshCw } from "@lucide/vue";
|
||||
import MetadataTablePanel from "./MetadataTablePanel.vue";
|
||||
import { ref, watch } from "vue";
|
||||
import FmsTable from "@/components/fms-table/FmsTable.vue";
|
||||
import { createSelectCell, createToggleCell } from "./cellRenderers";
|
||||
|
||||
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 })),
|
||||
},
|
||||
const FIELD_TYPES = [
|
||||
{ label: "输入框", value: "input" },
|
||||
{ label: "文本域", value: "textarea" },
|
||||
{ label: "数字", value: "number" },
|
||||
{ label: "金额", value: "money" },
|
||||
{ label: "日期", value: "date" },
|
||||
{ label: "日期时间", value: "datetime" },
|
||||
{ label: "开关", value: "switch" },
|
||||
{ label: "下拉选择", value: "select" },
|
||||
];
|
||||
|
||||
const props = defineProps({
|
||||
moduleId: { type: String, required: true },
|
||||
fields: { type: Array, default: () => [] },
|
||||
viewTable: { type: String, default: null },
|
||||
loading: { type: Boolean, default: false },
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update", "sync"]);
|
||||
|
||||
const localFields = ref([]);
|
||||
|
||||
watch(
|
||||
() => props.fields,
|
||||
(val) => {
|
||||
localFields.value = val?.map((r) => ({ ...r })) || [];
|
||||
},
|
||||
{ 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" },
|
||||
]);
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
function emitUpdate() {
|
||||
emit("update", localFields.value.map((r) => ({ ...r })));
|
||||
}
|
||||
|
||||
function setField(row, field, value) {
|
||||
row[field] = value === "" ? null : value;
|
||||
emitUpdate();
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{ dataIndex: "b_field", title: "字段名", width: 140 },
|
||||
{ dataIndex: "b_name", title: "字段名称", width: 140 },
|
||||
{
|
||||
dataIndex: "b_type",
|
||||
title: "控件类型",
|
||||
width: 130,
|
||||
customCell: createSelectCell({
|
||||
value: "b_type",
|
||||
getOptions: () => FIELD_TYPES,
|
||||
onChange: (row, v) => setField(row, "b_type", v),
|
||||
}),
|
||||
},
|
||||
{ dataIndex: "b_default_value", title: "默认值", width: 120 },
|
||||
{ dataIndex: "b_i18n", title: "多语言键", width: 140 },
|
||||
{
|
||||
dataIndex: "b_canuse",
|
||||
title: "启用",
|
||||
width: 70,
|
||||
align: "center",
|
||||
customCell: createToggleCell("b_canuse", (row) => setField(row, "b_canuse", row.b_canuse === 1 ? 0 : 1)),
|
||||
},
|
||||
];
|
||||
</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>
|
||||
<div class="fields-panel">
|
||||
<div class="panel-toolbar">
|
||||
<a-space>
|
||||
<a-button
|
||||
v-if="viewTable"
|
||||
type="primary"
|
||||
@click="emit('sync')"
|
||||
>
|
||||
同步字段 ({{ viewTable }})
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<FmsTable :columns="columns" :data-source="localFields" row-key="b_id" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.fields-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.panel-toolbar {
|
||||
display: flex;
|
||||
flex: none;
|
||||
gap: 8px;
|
||||
padding: 12px 20px;
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
padding: 16px 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.panel-body > :deep(.fms-table) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,140 @@
|
||||
<script setup>
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { message } from "antdv-next";
|
||||
import FmsTable from "@/components/fms-table/FmsTable.vue";
|
||||
import { loadDataApi, saveObjectApi } from "@/services/api";
|
||||
import { cloneData, diffRows, allocateTemporaryIds, tableChange, hasChanges } from "@/utils/dataChanges";
|
||||
import { createTempId } from "@/utils/tempId";
|
||||
import { createDeleteCell } from "./cellRenderers";
|
||||
|
||||
const props = defineProps({
|
||||
moduleId: { type: String, required: true },
|
||||
loading: { type: Boolean, default: false },
|
||||
});
|
||||
|
||||
const i18nData = ref([]);
|
||||
const originals = ref([]);
|
||||
const isLoading = ref(false);
|
||||
const isSaving = ref(false);
|
||||
const searchKey = ref("");
|
||||
|
||||
async function loadI18n() {
|
||||
isLoading.value = true;
|
||||
try {
|
||||
const res = await loadDataApi("b_i18n", null, "b_key ASC, b_locale ASC");
|
||||
originals.value = res.data || [];
|
||||
i18nData.value = cloneData(originals.value);
|
||||
} catch (e) {
|
||||
message.error("加载多语言失败:" + (e.message || e));
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => props.moduleId, loadI18n, { immediate: true });
|
||||
|
||||
function addRow() {
|
||||
i18nData.value.push({
|
||||
b_id: createTempId(),
|
||||
b_key: "",
|
||||
b_locale: "zh-CN",
|
||||
b_value: "",
|
||||
});
|
||||
}
|
||||
|
||||
function deleteRow(record) {
|
||||
i18nData.value = i18nData.value.filter((r) => r.b_id !== record.b_id);
|
||||
}
|
||||
|
||||
async function save() {
|
||||
const changes = diffRows(i18nData.value, originals.value);
|
||||
const req = tableChange("b_i18n", changes);
|
||||
if (!hasChanges(req)) {
|
||||
message.info("没有需要保存的变更");
|
||||
return;
|
||||
}
|
||||
isSaving.value = true;
|
||||
try {
|
||||
await allocateTemporaryIds([req.inserts || [], req.updates || [], req.deletes || []]);
|
||||
await saveObjectApi([req]);
|
||||
originals.value = cloneData(i18nData.value);
|
||||
message.success("保存成功");
|
||||
} catch (e) {
|
||||
message.error("保存失败:" + (e.message || e));
|
||||
} finally {
|
||||
isSaving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
const displayedData = computed(() => {
|
||||
if (!searchKey.value) return i18nData.value;
|
||||
const kw = searchKey.value.toLowerCase();
|
||||
return i18nData.value.filter(
|
||||
(r) =>
|
||||
(r.b_key || "").toLowerCase().includes(kw) ||
|
||||
(r.b_value || "").toLowerCase().includes(kw),
|
||||
);
|
||||
});
|
||||
|
||||
const columns = [
|
||||
{ dataIndex: "b_key", title: "键", width: 200 },
|
||||
{ dataIndex: "b_locale", title: "语言", width: 100 },
|
||||
{ dataIndex: "b_value", title: "翻译文本", width: 300 },
|
||||
{
|
||||
dataIndex: "actions",
|
||||
title: "操作",
|
||||
width: 60,
|
||||
align: "center",
|
||||
customCell: createDeleteCell((row) => deleteRow(row)),
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="i18n-panel">
|
||||
<div class="panel-toolbar">
|
||||
<a-space>
|
||||
<a-input-search
|
||||
v-model:value="searchKey"
|
||||
placeholder="搜索键或值"
|
||||
style="width: 200px"
|
||||
allow-clear
|
||||
/>
|
||||
<a-button type="primary" @click="addRow">新增翻译</a-button>
|
||||
<a-button :loading="isSaving" @click="save">保存</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<FmsTable :columns="columns" :data-source="displayedData" row-key="b_id" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.i18n-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.panel-toolbar {
|
||||
display: flex;
|
||||
flex: none;
|
||||
gap: 8px;
|
||||
padding: 12px 20px;
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
padding: 16px 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.panel-body > :deep(.fms-table) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,168 @@
|
||||
<script setup>
|
||||
import { computed, ref, watch } from "vue";
|
||||
import FmsTable from "@/components/fms-table/FmsTable.vue";
|
||||
import { createTempId } from "@/utils/tempId";
|
||||
import FieldGroupManager from "./FieldGroupManager.vue";
|
||||
import { createDeleteCell, createSelectCell, createToggleCell } from "./cellRenderers";
|
||||
|
||||
const props = defineProps({
|
||||
moduleId: { type: String, required: true },
|
||||
listConfig: { type: Array, default: () => [] },
|
||||
fields: { type: Array, default: () => [] },
|
||||
groups: { type: Array, default: () => [] },
|
||||
loading: { type: Boolean, default: false },
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update", "updateGroups"]);
|
||||
|
||||
const localList = ref([]);
|
||||
const groupManagerVisible = ref(false);
|
||||
|
||||
watch(
|
||||
() => props.listConfig,
|
||||
(val) => {
|
||||
localList.value = val?.map((r) => ({ ...r })) || [];
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
function emitUpdate() {
|
||||
emit("update", localList.value.map((r) => ({ ...r })));
|
||||
}
|
||||
|
||||
function setField(row, field, value) {
|
||||
row[field] = value === undefined || value === null ? null : value;
|
||||
emitUpdate();
|
||||
}
|
||||
|
||||
const availableFields = computed(() =>
|
||||
(props.fields || [])
|
||||
.filter((f) => f.b_canuse === 1)
|
||||
.map((f) => ({ label: f.b_name || f.b_field, value: String(f.b_id) })),
|
||||
);
|
||||
|
||||
const groupOptions = computed(() =>
|
||||
(props.groups || [])
|
||||
.filter((g) => g.b_canuse === 1)
|
||||
.map((g) => ({ label: g.b_title || `分组-${g.b_id}`, value: String(g.b_id) })),
|
||||
);
|
||||
|
||||
function addRow() {
|
||||
localList.value.push({
|
||||
b_id: createTempId(),
|
||||
b_module_id: props.moduleId,
|
||||
b_field_id: null,
|
||||
b_group_id: null,
|
||||
b_title: null,
|
||||
b_i18n: null,
|
||||
b_visible: 1,
|
||||
b_xh: localList.value.length * 10,
|
||||
b_width: null,
|
||||
b_sortable: 0,
|
||||
});
|
||||
emitUpdate();
|
||||
}
|
||||
|
||||
function deleteRow(record) {
|
||||
localList.value = localList.value.filter((r) => r.b_id !== record.b_id);
|
||||
emitUpdate();
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{
|
||||
dataIndex: "b_field_id",
|
||||
title: "字段",
|
||||
width: 160,
|
||||
customCell: createSelectCell({
|
||||
value: "b_field_id",
|
||||
getOptions: () => availableFields.value,
|
||||
onChange: (row, v) => setField(row, "b_field_id", v),
|
||||
placeholder: "选择字段",
|
||||
}),
|
||||
},
|
||||
{ dataIndex: "b_title", title: "列标题", width: 120 },
|
||||
{ dataIndex: "b_width", title: "列宽", width: 80 },
|
||||
{
|
||||
dataIndex: "b_group_id",
|
||||
title: "分组",
|
||||
width: 140,
|
||||
customCell: createSelectCell({
|
||||
value: "b_group_id",
|
||||
getOptions: () => groupOptions.value,
|
||||
onChange: (row, v) => setField(row, "b_group_id", v),
|
||||
placeholder: "选择分组",
|
||||
}),
|
||||
},
|
||||
{
|
||||
dataIndex: "b_visible",
|
||||
title: "显示",
|
||||
width: 60,
|
||||
align: "center",
|
||||
customCell: createToggleCell("b_visible", (row) => setField(row, "b_visible", row.b_visible === 1 ? 0 : 1)),
|
||||
},
|
||||
{
|
||||
dataIndex: "b_sortable",
|
||||
title: "排序",
|
||||
width: 60,
|
||||
align: "center",
|
||||
customCell: createToggleCell("b_sortable", (row) => setField(row, "b_sortable", row.b_sortable === 1 ? 0 : 1)),
|
||||
},
|
||||
{ dataIndex: "b_xh", title: "排序号", width: 80 },
|
||||
{
|
||||
dataIndex: "actions",
|
||||
title: "操作",
|
||||
width: 60,
|
||||
align: "center",
|
||||
customCell: createDeleteCell((row) => deleteRow(row)),
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="list-config-panel">
|
||||
<div class="panel-toolbar">
|
||||
<a-space>
|
||||
<a-button type="primary" @click="addRow">新增列</a-button>
|
||||
<a-button @click="groupManagerVisible = true">分组管理</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<FmsTable :columns="columns" :data-source="localList" row-key="b_id" />
|
||||
</div>
|
||||
<FieldGroupManager
|
||||
v-model:visible="groupManagerVisible"
|
||||
:module-id="moduleId"
|
||||
:groups="groups"
|
||||
@update="(v) => emit('updateGroups', v)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.list-config-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.panel-toolbar {
|
||||
display: flex;
|
||||
flex: none;
|
||||
gap: 8px;
|
||||
padding: 12px 20px;
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
padding: 16px 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.panel-body > :deep(.fms-table) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -1,142 +0,0 @@
|
||||
<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,34 +1,112 @@
|
||||
<script setup>
|
||||
import { ref, watch } from "vue";
|
||||
import FmsTable from "@/components/fms-table/FmsTable.vue";
|
||||
import { createTempId } from "@/utils/tempId";
|
||||
import MetadataTablePanel from "./MetadataTablePanel.vue";
|
||||
import { createDeleteCell, createToggleCell } from "./cellRenderers";
|
||||
|
||||
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" },
|
||||
];
|
||||
const props = defineProps({
|
||||
moduleId: { type: String, required: true },
|
||||
powers: { type: Array, default: () => [] },
|
||||
loading: { type: Boolean, default: false },
|
||||
});
|
||||
|
||||
function addPower() {
|
||||
powers.value.push({
|
||||
const emit = defineEmits(["update"]);
|
||||
|
||||
const localPowers = ref([]);
|
||||
|
||||
watch(
|
||||
() => props.powers,
|
||||
(val) => {
|
||||
localPowers.value = val?.map((r) => ({ ...r })) || [];
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
function emitUpdate() {
|
||||
emit("update", localPowers.value.map((r) => ({ ...r })));
|
||||
}
|
||||
|
||||
function addRow() {
|
||||
localPowers.value.push({
|
||||
b_id: createTempId(),
|
||||
b_module_id: props.moduleId,
|
||||
b_code: "",
|
||||
b_name: "新操作",
|
||||
b_name: "",
|
||||
b_i18n: null,
|
||||
b_xh: Math.max(0, ...powers.value.map((item) => Number(item.b_xh) || 0)) + 10,
|
||||
b_xh: localPowers.value.length * 10,
|
||||
b_canuse: 1,
|
||||
});
|
||||
emitUpdate();
|
||||
}
|
||||
|
||||
function deleteRow(record) {
|
||||
localPowers.value = localPowers.value.filter((r) => r.b_id !== record.b_id);
|
||||
emitUpdate();
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{ dataIndex: "b_code", title: "操作编码", width: 160 },
|
||||
{ dataIndex: "b_name", title: "操作名称", width: 160 },
|
||||
{ dataIndex: "b_i18n", title: "多语言键", width: 160 },
|
||||
{ dataIndex: "b_xh", title: "排序", width: 80 },
|
||||
{
|
||||
dataIndex: "b_canuse",
|
||||
title: "启用",
|
||||
width: 70,
|
||||
align: "center",
|
||||
customCell: createToggleCell("b_canuse", (row) => {
|
||||
row.b_canuse = row.b_canuse === 1 ? 0 : 1;
|
||||
emitUpdate();
|
||||
}),
|
||||
},
|
||||
{
|
||||
dataIndex: "actions",
|
||||
title: "操作",
|
||||
width: 60,
|
||||
align: "center",
|
||||
customCell: createDeleteCell((row) => deleteRow(row)),
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MetadataTablePanel
|
||||
v-model:rows="powers"
|
||||
title="模块操作权限"
|
||||
add-text="新增操作"
|
||||
:columns="columns"
|
||||
@add="addPower"
|
||||
/>
|
||||
<div class="powers-panel">
|
||||
<div class="panel-toolbar">
|
||||
<a-space>
|
||||
<a-button type="primary" @click="addRow">新增权限</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<FmsTable :columns="columns" :data-source="localPowers" row-key="b_id" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.powers-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.panel-toolbar {
|
||||
display: flex;
|
||||
flex: none;
|
||||
gap: 8px;
|
||||
padding: 12px 20px;
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
padding: 16px 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.panel-body > :deep(.fms-table) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,165 @@
|
||||
<script setup>
|
||||
import { computed, ref, watch } from "vue";
|
||||
import FmsTable from "@/components/fms-table/FmsTable.vue";
|
||||
import { createTempId } from "@/utils/tempId";
|
||||
import { createDeleteCell, createSelectCell, createToggleCell } from "./cellRenderers";
|
||||
|
||||
const COMPONENT_OPTIONS = [
|
||||
{ label: "输入框", value: "input" },
|
||||
{ label: "下拉选择", value: "select" },
|
||||
{ label: "日期选择", value: "date-picker" },
|
||||
{ label: "数字范围", value: "number-range" },
|
||||
];
|
||||
|
||||
const OPERATOR_OPTIONS = [
|
||||
{ label: "等于 (=)", value: "=" },
|
||||
{ label: "模糊 (like)", value: "like" },
|
||||
{ label: "范围 (between)", value: "between" },
|
||||
{ label: "包含 (in)", value: "in" },
|
||||
];
|
||||
|
||||
const props = defineProps({
|
||||
moduleId: { type: String, required: true },
|
||||
queryConfig: { type: Array, default: () => [] },
|
||||
fields: { type: Array, default: () => [] },
|
||||
loading: { type: Boolean, default: false },
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update"]);
|
||||
|
||||
const localQuery = ref([]);
|
||||
|
||||
watch(
|
||||
() => props.queryConfig,
|
||||
(val) => {
|
||||
localQuery.value = val?.map((r) => ({ ...r })) || [];
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
function emitUpdate() {
|
||||
emit("update", localQuery.value.map((r) => ({ ...r })));
|
||||
}
|
||||
|
||||
function setField(row, field, value) {
|
||||
row[field] = value === undefined || value === null ? null : value;
|
||||
emitUpdate();
|
||||
}
|
||||
|
||||
const availableFields = computed(() =>
|
||||
(props.fields || [])
|
||||
.filter((f) => f.b_canuse === 1)
|
||||
.map((f) => ({ label: f.b_name || f.b_field, value: String(f.b_id) })),
|
||||
);
|
||||
|
||||
function addRow() {
|
||||
localQuery.value.push({
|
||||
b_id: createTempId(),
|
||||
b_module_id: props.moduleId,
|
||||
b_field_id: null,
|
||||
b_component: "input",
|
||||
b_operator: "=",
|
||||
b_default_value: null,
|
||||
b_xh: localQuery.value.length * 10,
|
||||
b_canuse: 1,
|
||||
});
|
||||
emitUpdate();
|
||||
}
|
||||
|
||||
function deleteRow(record) {
|
||||
localQuery.value = localQuery.value.filter((r) => r.b_id !== record.b_id);
|
||||
emitUpdate();
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{
|
||||
dataIndex: "b_field_id",
|
||||
title: "字段",
|
||||
width: 160,
|
||||
customCell: createSelectCell({
|
||||
value: "b_field_id",
|
||||
getOptions: () => availableFields.value,
|
||||
onChange: (row, v) => setField(row, "b_field_id", v),
|
||||
placeholder: "选择字段",
|
||||
}),
|
||||
},
|
||||
{
|
||||
dataIndex: "b_component",
|
||||
title: "查询组件",
|
||||
width: 140,
|
||||
customCell: createSelectCell({
|
||||
value: "b_component",
|
||||
getOptions: () => COMPONENT_OPTIONS,
|
||||
onChange: (row, v) => setField(row, "b_component", v),
|
||||
}),
|
||||
},
|
||||
{
|
||||
dataIndex: "b_operator",
|
||||
title: "操作符",
|
||||
width: 140,
|
||||
customCell: createSelectCell({
|
||||
value: "b_operator",
|
||||
getOptions: () => OPERATOR_OPTIONS,
|
||||
onChange: (row, v) => setField(row, "b_operator", v),
|
||||
}),
|
||||
},
|
||||
{ dataIndex: "b_default_value", title: "默认查询值", width: 140 },
|
||||
{
|
||||
dataIndex: "b_canuse",
|
||||
title: "启用",
|
||||
width: 60,
|
||||
align: "center",
|
||||
customCell: createToggleCell("b_canuse", (row) => setField(row, "b_canuse", row.b_canuse === 1 ? 0 : 1)),
|
||||
},
|
||||
{ dataIndex: "b_xh", title: "排序号", width: 80 },
|
||||
{
|
||||
dataIndex: "actions",
|
||||
title: "操作",
|
||||
width: 60,
|
||||
align: "center",
|
||||
customCell: createDeleteCell((row) => deleteRow(row)),
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="query-config-panel">
|
||||
<div class="panel-toolbar">
|
||||
<a-space>
|
||||
<a-button type="primary" @click="addRow">新增查询条件</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<FmsTable :columns="columns" :data-source="localQuery" row-key="b_id" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.query-config-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.panel-toolbar {
|
||||
display: flex;
|
||||
flex: none;
|
||||
gap: 8px;
|
||||
padding: 12px 20px;
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
padding: 16px 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.panel-body > :deep(.fms-table) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -1,65 +0,0 @@
|
||||
<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,44 +0,0 @@
|
||||
<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>
|
||||
@@ -1,164 +0,0 @@
|
||||
<script setup>
|
||||
import { computed, ref } from "vue";
|
||||
import { RefreshCw, Search } from "@lucide/vue";
|
||||
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,
|
||||
canDelete: Boolean,
|
||||
});
|
||||
const emit = defineEmits(["select", "add", "delete", "refresh"]);
|
||||
const searchText = ref("");
|
||||
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_code).toLowerCase().includes(keyword) ||
|
||||
String(item.b_name).toLowerCase().includes(keyword),
|
||||
)
|
||||
.flatMap((item) => [item.b_code, codeById.get(item.b_parent_id)].filter(Boolean)),
|
||||
)
|
||||
: null;
|
||||
const children = new Map();
|
||||
props.modules.forEach((item) => {
|
||||
const parent = item.b_parent_id || "__root__";
|
||||
if (!children.has(parent)) children.set(parent, []);
|
||||
children.get(parent).push(item);
|
||||
});
|
||||
const build = (parent) =>
|
||||
(children.get(parent) || [])
|
||||
.filter((item) => !visible || visible.has(item.b_code))
|
||||
.map((item) => {
|
||||
const nodeChildren = build(item.b_id);
|
||||
return {
|
||||
key: item.b_code,
|
||||
title: item.b_name,
|
||||
children: nodeChildren,
|
||||
isLeaf: nodeChildren.length === 0,
|
||||
};
|
||||
});
|
||||
|
||||
return build("__root__");
|
||||
});
|
||||
|
||||
function onSelect(keys) {
|
||||
if (keys[0]) emit("select", keys[0]);
|
||||
}
|
||||
|
||||
function onRightClick({ node }) {
|
||||
contextNode.value = node;
|
||||
emit("select", node.key);
|
||||
}
|
||||
|
||||
const contextMenu = computed(() => {
|
||||
const node = contextNode.value;
|
||||
return {
|
||||
items: [
|
||||
{ key: "directory", label: "新增目录", disabled: !props.canCreate },
|
||||
{ key: "page", label: "新增页面", disabled: !props.canCreate },
|
||||
{ key: "data", label: "新增数据模块", disabled: !props.canCreate },
|
||||
{ type: "divider" },
|
||||
{
|
||||
key: "delete",
|
||||
label: "删除",
|
||||
danger: true,
|
||||
disabled: !node || !props.canDelete || props.protectedKeys.includes(String(node.key)),
|
||||
},
|
||||
],
|
||||
onClick: ({ key }) => {
|
||||
if (!node) return;
|
||||
if (key === "delete") {
|
||||
emit("delete", node.key);
|
||||
return;
|
||||
}
|
||||
|
||||
emit("add", key, node.key);
|
||||
},
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<aside class="module-tree-panel">
|
||||
<header>
|
||||
<div>
|
||||
<strong>模块</strong>
|
||||
<span>{{ modules.length }}</span>
|
||||
</div>
|
||||
<a-tooltip title="刷新">
|
||||
<a-button type="text" size="small" :loading="loading" @click="emit('refresh')">
|
||||
<RefreshCw :size="16" />
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
</header>
|
||||
|
||||
<a-input v-model:value="searchText" allow-clear placeholder="搜索名称或编码">
|
||||
<template #prefix><Search :size="15" /></template>
|
||||
</a-input>
|
||||
|
||||
<a-spin :spinning="loading" class="tree-spin">
|
||||
<a-dropdown :menu="contextMenu" :trigger="['contextmenu']">
|
||||
<FmsTree
|
||||
block-node
|
||||
default-expand-all
|
||||
:tree-data="treeData"
|
||||
:selected-keys="selectedKey ? [selectedKey] : []"
|
||||
@right-click="onRightClick"
|
||||
@select="onSelect"
|
||||
/>
|
||||
</a-dropdown>
|
||||
</a-spin>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.module-tree-panel {
|
||||
display: flex;
|
||||
min-height: 0;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
border-right: 1px solid #e5e7eb;
|
||||
background: #fff;
|
||||
padding: 10px 8px;
|
||||
}
|
||||
|
||||
header,
|
||||
header > div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
header {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
header > div {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
header span {
|
||||
color: #8c8c8c;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.tree-spin {
|
||||
min-height: 0;
|
||||
flex: 1 1 auto;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
:deep(.ant-spin-nested-loading),
|
||||
:deep(.ant-spin-container) {
|
||||
min-height: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,64 @@
|
||||
import { h } from "vue";
|
||||
import { Button, Popconfirm, Select } from "antdv-next";
|
||||
|
||||
// 单元格渲染器统一采用 stk-table-vue 的 customCell 写法:
|
||||
// customCell 会被当作函数式组件调用(props 含 row/col/rowIndex/cellValue),
|
||||
// 必须返回 h(...) 生成的 VNode,返回普通对象会渲染成 [object Object]。
|
||||
|
||||
// 删除操作单元格
|
||||
export function createDeleteCell(onDelete) {
|
||||
return (props) =>
|
||||
h(
|
||||
Popconfirm,
|
||||
{
|
||||
title: "确定删除该记录?",
|
||||
okText: "删除",
|
||||
okButtonProps: { danger: true },
|
||||
onConfirm: () => onDelete(props.row),
|
||||
},
|
||||
{
|
||||
default: () =>
|
||||
h(
|
||||
Button,
|
||||
{ type: "link", size: "small", danger: true, onClick: (e) => e.stopPropagation() },
|
||||
"删除",
|
||||
),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// 是否(0/1)切换单元格
|
||||
export function createToggleCell(field, onChange) {
|
||||
return (props) => {
|
||||
const checked = props.row[field] === 1;
|
||||
return h(
|
||||
"span",
|
||||
{
|
||||
style: { cursor: "pointer" },
|
||||
onClick: (e) => {
|
||||
e.stopPropagation();
|
||||
onChange(props.row, field);
|
||||
},
|
||||
},
|
||||
checked ? "是" : "否",
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
// 下拉选择单元格。getOptions 返回最新选项(保持响应式)。
|
||||
export function createSelectCell({ value, getOptions, onChange, placeholder, allowClear }) {
|
||||
return (props) =>
|
||||
h(Select, {
|
||||
value:
|
||||
props.row[value] != null && props.row[value] !== ""
|
||||
? String(props.row[value])
|
||||
: undefined,
|
||||
size: "small",
|
||||
style: { width: "100%" },
|
||||
options: getOptions(),
|
||||
allowClear: allowClear !== false,
|
||||
placeholder: placeholder || "请选择",
|
||||
"onUpdate:value": (v) => onChange(props.row, v),
|
||||
onClick: (e) => e.stopPropagation(),
|
||||
});
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user