20260720215810
This commit is contained in:
1 parent
94207c77ff
commit
fd161dd302
9 files changed
+1242
-1102
No files matched your search
@@ -0,0 +1,197 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from "vue";
|
||||||
|
|
||||||
|
defineOptions({ inheritAttrs: false });
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
bordered: { type: Boolean, default: true },
|
||||||
|
scroll: { type: Object, default: () => ({}) },
|
||||||
|
scrollX: { type: [Boolean, Number, String], default: "max-content" },
|
||||||
|
});
|
||||||
|
|
||||||
|
const tableAreaRef = ref(null);
|
||||||
|
const bodyHeight = ref(240);
|
||||||
|
const mergedScroll = computed(() => ({
|
||||||
|
...props.scroll,
|
||||||
|
x: props.scrollX,
|
||||||
|
y: bodyHeight.value,
|
||||||
|
}));
|
||||||
|
|
||||||
|
let resizeObserver;
|
||||||
|
let mutationObserver;
|
||||||
|
let frameId = 0;
|
||||||
|
const observedElements = new Set();
|
||||||
|
|
||||||
|
function getOuterHeight(element) {
|
||||||
|
if (!element) return 0;
|
||||||
|
|
||||||
|
const style = getComputedStyle(element);
|
||||||
|
const marginTop = Number.parseFloat(style.marginTop) || 0;
|
||||||
|
const marginBottom = Number.parseFloat(style.marginBottom) || 0;
|
||||||
|
|
||||||
|
return element.getBoundingClientRect().height + marginTop + marginBottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clipsOverflowY(element) {
|
||||||
|
const { overflow, overflowY } = getComputedStyle(element);
|
||||||
|
return [overflow, overflowY].some((value) => /^(auto|clip|hidden|scroll)$/.test(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAvailableHeight(area) {
|
||||||
|
const areaTop = area.getBoundingClientRect().top;
|
||||||
|
let boundaryBottom = Number.POSITIVE_INFINITY;
|
||||||
|
let ancestor = area.parentElement;
|
||||||
|
|
||||||
|
while (ancestor && ancestor !== document.documentElement) {
|
||||||
|
if (clipsOverflowY(ancestor)) {
|
||||||
|
const rect = ancestor.getBoundingClientRect();
|
||||||
|
boundaryBottom = Math.min(boundaryBottom, rect.top + ancestor.clientTop + ancestor.clientHeight);
|
||||||
|
}
|
||||||
|
ancestor = ancestor.parentElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Number.isFinite(boundaryBottom) && boundaryBottom > areaTop) {
|
||||||
|
return Math.floor(boundaryBottom - areaTop);
|
||||||
|
}
|
||||||
|
return area.clientHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateBodyHeight() {
|
||||||
|
const area = tableAreaRef.value;
|
||||||
|
if (!area?.clientHeight) return;
|
||||||
|
|
||||||
|
const headerHeight = getOuterHeight(
|
||||||
|
area.querySelector(".ant-table-header") || area.querySelector(".ant-table-thead"),
|
||||||
|
);
|
||||||
|
const paginationHeight = getOuterHeight(area.querySelector(".ant-table-pagination"));
|
||||||
|
const titleHeight = getOuterHeight(area.querySelector(".ant-table-title"));
|
||||||
|
const footerHeight = getOuterHeight(area.querySelector(".ant-table-footer"));
|
||||||
|
|
||||||
|
const nextHeight = Math.max(
|
||||||
|
1,
|
||||||
|
getAvailableHeight(area) - headerHeight - paginationHeight - titleHeight - footerHeight,
|
||||||
|
);
|
||||||
|
if (nextHeight !== bodyHeight.value) bodyHeight.value = nextHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleHeightUpdate() {
|
||||||
|
cancelAnimationFrame(frameId);
|
||||||
|
frameId = requestAnimationFrame(updateBodyHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
function observe(element) {
|
||||||
|
if (!element || observedElements.has(element)) return;
|
||||||
|
resizeObserver?.observe(element);
|
||||||
|
observedElements.add(element);
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncObservedElements() {
|
||||||
|
const area = tableAreaRef.value;
|
||||||
|
if (!area) return;
|
||||||
|
|
||||||
|
let ancestor = area.parentElement;
|
||||||
|
while (ancestor && ancestor !== document.documentElement) {
|
||||||
|
if (ancestor === area.parentElement || clipsOverflowY(ancestor)) observe(ancestor);
|
||||||
|
ancestor = ancestor.parentElement;
|
||||||
|
}
|
||||||
|
observe(area.querySelector(".ant-table-header"));
|
||||||
|
observe(area.querySelector(".ant-table-pagination"));
|
||||||
|
observe(area.querySelector(".ant-table-title"));
|
||||||
|
observe(area.querySelector(".ant-table-footer"));
|
||||||
|
scheduleHeightUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
resizeObserver = new ResizeObserver(scheduleHeightUpdate);
|
||||||
|
mutationObserver = new MutationObserver(() => {
|
||||||
|
nextTick(syncObservedElements);
|
||||||
|
});
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
syncObservedElements();
|
||||||
|
mutationObserver.observe(tableAreaRef.value, { childList: true, subtree: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
cancelAnimationFrame(frameId);
|
||||||
|
resizeObserver?.disconnect();
|
||||||
|
mutationObserver?.disconnect();
|
||||||
|
observedElements.clear();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
ref="tableAreaRef"
|
||||||
|
class="auto-height-table"
|
||||||
|
:style="{ '--auto-table-body-height': bodyHeight + 'px' }"
|
||||||
|
>
|
||||||
|
<a-table v-bind="$attrs" :bordered="bordered" :scroll="mergedScroll" size="small">
|
||||||
|
<template v-for="(_, name) in $slots" :key="name" #[name]="slotProps">
|
||||||
|
<slot :name="name" v-bind="slotProps || {}" />
|
||||||
|
</template>
|
||||||
|
</a-table>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.auto-height-table {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-table-wrapper),
|
||||||
|
:deep(.ant-spin-nested-loading),
|
||||||
|
:deep(.ant-spin-container),
|
||||||
|
:deep(.ant-table),
|
||||||
|
:deep(.ant-table-container) {
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-table-wrapper) {
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-spin-nested-loading),
|
||||||
|
:deep(.ant-spin-container),
|
||||||
|
:deep(.ant-table),
|
||||||
|
:deep(.ant-table-container) {
|
||||||
|
display: flex;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-table-header),
|
||||||
|
:deep(.ant-table-pagination) {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-table-body) {
|
||||||
|
height: var(--auto-table-body-height);
|
||||||
|
min-height: 0;
|
||||||
|
max-height: var(--auto-table-body-height) !important;
|
||||||
|
flex: 0 0 var(--auto-table-body-height);
|
||||||
|
overflow-y: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* scroll.y 模式下 antdv 不会渲染 body 底部外边框;且直接加在滚动容器 .ant-table-body
|
||||||
|
上时,边框会被内部滚动内容的最后一行盖住、或被祖先 overflow:hidden 裁掉,表现为
|
||||||
|
“看不到/被遮挡”。所以把底边框挂在不滚动的 .ant-table-container 上,并强制 border-box
|
||||||
|
让边框计入盒子高度,避免溢出裁切。 */
|
||||||
|
::deep(.ant-table-container),
|
||||||
|
::deep(.ant-table-body) {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
::deep(.ant-table-bordered .ant-table-container) {
|
||||||
|
border-bottom: 1px solid var(--ant-color-border-secondary, #f0f0f0);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -8,6 +8,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -20,7 +22,6 @@ body,
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
margin: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
|
|||||||
@@ -1,63 +1,37 @@
|
|||||||
<script setup lang="js">
|
<script setup lang="js">
|
||||||
import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue'
|
import AutoHeightTable from "../components/AutoHeightTable.vue";
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{
|
{
|
||||||
title: 'Name (all screens)',
|
title: "Name (all screens)",
|
||||||
dataIndex: 'name',
|
dataIndex: "name",
|
||||||
key: 'name',
|
key: "name",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Age (medium screen or bigger)',
|
title: "Age (medium screen or bigger)",
|
||||||
dataIndex: 'age',
|
dataIndex: "age",
|
||||||
key: 'age',
|
key: "age",
|
||||||
responsive: ['md'],
|
responsive: ["md"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Address (large screen or bigger)',
|
title: "Address (large screen or bigger)",
|
||||||
dataIndex: 'address',
|
dataIndex: "address",
|
||||||
key: 'address',
|
key: "address",
|
||||||
responsive: ['lg'],
|
responsive: ["lg"],
|
||||||
},
|
},
|
||||||
]
|
];
|
||||||
|
|
||||||
const dataSource = Array.from({ length: 30 }, (_, i) => ({
|
const dataSource = Array.from({ length: 30 }, (_, i) => ({
|
||||||
key: String(i + 1),
|
key: String(i + 1),
|
||||||
name: 'John Brown',
|
name: "John Brown",
|
||||||
age: 32,
|
age: 32,
|
||||||
address: 'New York No. 1 Lake Park',
|
address: "New York No. 1 Lake Park",
|
||||||
}))
|
}));
|
||||||
|
|
||||||
const tableWrap = ref(null)
|
|
||||||
const scrollY = ref(0)
|
|
||||||
let ro = null
|
|
||||||
|
|
||||||
function updateScrollY() {
|
|
||||||
const el = tableWrap.value
|
|
||||||
if (!el) return
|
|
||||||
const thead = el.querySelector('.ant-table-thead')
|
|
||||||
const pagination = el.querySelector('.ant-table-pagination')
|
|
||||||
const headerH = thead ? thead.offsetHeight : 0
|
|
||||||
const pagH = pagination ? pagination.offsetHeight + 16 : 0
|
|
||||||
scrollY.value = Math.max(0, el.clientHeight - headerH - pagH)
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
await nextTick()
|
|
||||||
updateScrollY()
|
|
||||||
ro = new ResizeObserver(updateScrollY)
|
|
||||||
if (tableWrap.value) ro.observe(tableWrap.value)
|
|
||||||
})
|
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
if (ro) ro.disconnect()
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="dashboard h-full flex flex-col gap-3 p-4">
|
<div class="dashboard h-full min-h-0 flex flex-col gap-3 overflow-hidden p-4">
|
||||||
<!-- Toolbar -->
|
<div class="toolbar flex flex-none items-center justify-between">
|
||||||
<div class="toolbar flex-none flex items-center justify-between">
|
|
||||||
<span class="text-base font-semibold">Dashboard</span>
|
<span class="text-base font-semibold">Dashboard</span>
|
||||||
<a-space>
|
<a-space>
|
||||||
<a-button type="primary">新增</a-button>
|
<a-button type="primary">新增</a-button>
|
||||||
@@ -66,21 +40,12 @@ onBeforeUnmount(() => {
|
|||||||
</a-space>
|
</a-space>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Table fills the remaining space -->
|
<AutoHeightTable :columns="columns" :data-source="dataSource">
|
||||||
<div ref="tableWrap" class="table-area flex-1 min-h-0 overflow-hidden">
|
<template #bodyCell="{ column, text }">
|
||||||
<a-table
|
<template v-if="column.key === 'name'">
|
||||||
:columns="columns"
|
<a>{{ text }}</a>
|
||||||
:data-source="dataSource"
|
|
||||||
:scroll="{ y: scrollY, x: 'max-content' }"
|
|
||||||
bordered
|
|
||||||
size="small"
|
|
||||||
>
|
|
||||||
<template #bodyCell="{ column, text }">
|
|
||||||
<template v-if="column.key === 'name'">
|
|
||||||
<a>{{ text }}</a>
|
|
||||||
</template>
|
|
||||||
</template>
|
</template>
|
||||||
</a-table>
|
</template>
|
||||||
</div>
|
</AutoHeightTable>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,143 @@
|
|||||||
|
<script setup>
|
||||||
|
const props = defineProps({
|
||||||
|
directoryParentOptions: { type: Array, default: () => [] },
|
||||||
|
isNew: Boolean,
|
||||||
|
parentOptions: { type: Array, default: () => [] },
|
||||||
|
});
|
||||||
|
const emit = defineEmits(["update-module-id"]);
|
||||||
|
const module = defineModel("module", { type: Object, default: null });
|
||||||
|
|
||||||
|
function updateRoute(value) {
|
||||||
|
module.value.b_route = value || null;
|
||||||
|
if (!String(value || "").trim()) module.value.b_canmenu = 0;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section v-if="module" class="module-basic-info">
|
||||||
|
<a-form v-if="module.b_type === 'directory'" layout="vertical" class="directory-form">
|
||||||
|
<div class="directory-grid">
|
||||||
|
<a-form-item label="模块编码" required>
|
||||||
|
<a-input :value="module.b_id" disabled />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="模块名称" required>
|
||||||
|
<a-input v-model:value="module.b_name" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="上级模块">
|
||||||
|
<a-select
|
||||||
|
v-model:value="module.b_parent_id"
|
||||||
|
allow-clear
|
||||||
|
show-search
|
||||||
|
option-filter-prop="label"
|
||||||
|
:options="directoryParentOptions.filter((item) => item.value !== module.b_id)"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="多语言键">
|
||||||
|
<a-input v-model:value="module.b_i18n" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="显示顺序">
|
||||||
|
<a-input-number v-model:value="module.b_xh" :min="0" />
|
||||||
|
</a-form-item>
|
||||||
|
</div>
|
||||||
|
</a-form>
|
||||||
|
|
||||||
|
<a-form v-else layout="vertical" class="basic-form">
|
||||||
|
<div class="form-grid">
|
||||||
|
<a-form-item label="模块编码" required>
|
||||||
|
<a-input
|
||||||
|
:value="module.b_id"
|
||||||
|
:disabled="!isNew"
|
||||||
|
@update:value="emit('update-module-id', $event)"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="模块名称" required>
|
||||||
|
<a-input v-model:value="module.b_name" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="上级模块">
|
||||||
|
<a-select
|
||||||
|
v-model:value="module.b_parent_id"
|
||||||
|
allow-clear
|
||||||
|
show-search
|
||||||
|
option-filter-prop="label"
|
||||||
|
:options="parentOptions"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="页面路由(可选)">
|
||||||
|
<a-input
|
||||||
|
:value="module.b_route"
|
||||||
|
placeholder="留空时作为数据模块使用"
|
||||||
|
@update:value="updateRoute"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="多语言键">
|
||||||
|
<a-input v-model:value="module.b_i18n" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="查询表或视图">
|
||||||
|
<a-input v-model:value="module.b_viewtable" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="保存表">
|
||||||
|
<a-input v-model:value="module.b_savetable" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="主键字段">
|
||||||
|
<a-input v-model:value="module.b_keyfield" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="排序字段">
|
||||||
|
<a-input v-model:value="module.b_orderfield" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="排序方向">
|
||||||
|
<a-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>
|
||||||
|
</div>
|
||||||
|
</a-form>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.module-basic-info {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: auto;
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.directory-form {
|
||||||
|
max-width: 760px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.basic-form {
|
||||||
|
max-width: 1040px;
|
||||||
|
padding-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.directory-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
gap: 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-form-item) {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 980px) {
|
||||||
|
.form-grid {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 720px) {
|
||||||
|
.directory-grid,
|
||||||
|
.form-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
+248
@@ -0,0 +1,248 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed, ref } from "vue";
|
||||||
|
import { Languages, Plus, Trash2 } from "@lucide/vue";
|
||||||
|
import { LOCALES } from "../../../services/moduleManagement";
|
||||||
|
import AutoHeightTable from "../../../components/AutoHeightTable.vue";
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
canSave: Boolean,
|
||||||
|
});
|
||||||
|
const emit = defineEmits(["add"]);
|
||||||
|
const fields = defineModel("fields", { type: Array, default: () => [] });
|
||||||
|
const translations = defineModel("translations", { type: Object, default: () => ({}) });
|
||||||
|
|
||||||
|
const editingFieldId = ref("");
|
||||||
|
const selectedFieldKeys = ref([]);
|
||||||
|
const columns = [
|
||||||
|
{ title: "字段", key: "b_field", width: 150 },
|
||||||
|
{ title: "名称", key: "b_name", width: 150 },
|
||||||
|
{ title: "类型", key: "b_type", width: 120 },
|
||||||
|
{ title: "列表", key: "b_canlist", width: 64, align: "center" },
|
||||||
|
{ title: "表单", key: "b_canform", width: 64, align: "center" },
|
||||||
|
{ title: "必填", key: "b_must", width: 64, align: "center" },
|
||||||
|
{ title: "查询", key: "b_canquery", width: 64, align: "center" },
|
||||||
|
{ title: "只读", key: "b_readonly", width: 64, align: "center" },
|
||||||
|
{ title: "宽度", key: "b_width", width: 90 },
|
||||||
|
{ title: "顺序", key: "b_xh", width: 80 },
|
||||||
|
{ title: "操作", key: "operation", width: 58, fixed: "right" },
|
||||||
|
];
|
||||||
|
const fieldTypes = [
|
||||||
|
["string", "单行文本"],
|
||||||
|
["text", "多行文本"],
|
||||||
|
["number", "数字"],
|
||||||
|
["select", "选择"],
|
||||||
|
["date", "日期"],
|
||||||
|
["boolean", "布尔"],
|
||||||
|
].map(([value, label]) => ({ value, label }));
|
||||||
|
const rowSelection = computed(() => ({
|
||||||
|
selectedRowKeys: selectedFieldKeys.value,
|
||||||
|
onChange: (keys) => {
|
||||||
|
selectedFieldKeys.value = keys;
|
||||||
|
},
|
||||||
|
columnWidth: 40,
|
||||||
|
}));
|
||||||
|
|
||||||
|
function translationEntry(key) {
|
||||||
|
translations.value[key] ||= {};
|
||||||
|
return translations.value[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
function setFieldTranslation(record, locale, value) {
|
||||||
|
translationEntry(record.b_i18n)[locale] = value;
|
||||||
|
if (locale === "zh-CN") record.b_name = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fieldTranslationValue(record, locale) {
|
||||||
|
return translationEntry(record.b_i18n)[locale] ?? (locale === "zh-CN" ? record.b_name : "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function fieldTypeLabel(value) {
|
||||||
|
return fieldTypes.find((item) => item.value === value)?.label || value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setBoolean(record, field, checked) {
|
||||||
|
record[field] = checked ? 1 : 0;
|
||||||
|
if (field === "b_canform" && !checked) record.b_must = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fieldRow(record) {
|
||||||
|
return {
|
||||||
|
onClick: () => {
|
||||||
|
editingFieldId.value = String(record.b_id);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function leaveEdit(event) {
|
||||||
|
if (!event.target.closest(".ant-table-row")) editingFieldId.value = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeField(record) {
|
||||||
|
fields.value = fields.value.filter((item) => String(item.b_id) !== String(record.b_id));
|
||||||
|
selectedFieldKeys.value = selectedFieldKeys.value.filter(
|
||||||
|
(key) => String(key) !== String(record.b_id),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteSelectedFields() {
|
||||||
|
const ids = new Set(selectedFieldKeys.value.map(String));
|
||||||
|
fields.value = fields.value.filter((item) => !ids.has(String(item.b_id)));
|
||||||
|
selectedFieldKeys.value = [];
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section class="table-pane" @click="leaveEdit">
|
||||||
|
<div class="table-toolbar">
|
||||||
|
<a-button type="primary" ghost :disabled="!canSave" @click="emit('add')">
|
||||||
|
<Plus :size="15" />新增字段
|
||||||
|
</a-button>
|
||||||
|
<a-popconfirm title="确定删除选中的字段吗?" @confirm="deleteSelectedFields">
|
||||||
|
<a-button danger :disabled="!canSave || !selectedFieldKeys.length">
|
||||||
|
<Trash2 :size="15" />批量删除 ({{ selectedFieldKeys.length }})
|
||||||
|
</a-button>
|
||||||
|
</a-popconfirm>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<AutoHeightTable
|
||||||
|
:columns="columns"
|
||||||
|
:data-source="fields"
|
||||||
|
:pagination="false"
|
||||||
|
:row-class-name="(record) => (String(record.b_id) === editingFieldId ? 'editing-row' : '')"
|
||||||
|
:row-selection="rowSelection"
|
||||||
|
row-key="b_id"
|
||||||
|
@row="fieldRow"
|
||||||
|
>
|
||||||
|
<template #bodyCell="{ column, record }">
|
||||||
|
<a-tooltip v-if="column.key === 'operation'" title="删除字段">
|
||||||
|
<a-button type="text" danger @click.stop="removeField(record)">
|
||||||
|
<Trash2 :size="15" />
|
||||||
|
</a-button>
|
||||||
|
</a-tooltip>
|
||||||
|
|
||||||
|
<template v-else-if="String(record.b_id) === editingFieldId">
|
||||||
|
<a-input v-if="column.key === 'b_field'" v-model:value="record.b_field" />
|
||||||
|
<div v-else-if="column.key === 'b_name'" class="field-name-editor">
|
||||||
|
<a-input
|
||||||
|
:value="record.b_name"
|
||||||
|
@update:value="setFieldTranslation(record, 'zh-CN', $event)"
|
||||||
|
/>
|
||||||
|
<a-popover trigger="click" placement="bottomRight">
|
||||||
|
<template #content>
|
||||||
|
<a-space direction="vertical" size="small" style="width: 260px">
|
||||||
|
<div v-for="locale in LOCALES" :key="locale.value">
|
||||||
|
<div class="locale-label">{{ locale.label }}</div>
|
||||||
|
<a-input
|
||||||
|
:value="fieldTranslationValue(record, locale.value)"
|
||||||
|
@update:value="setFieldTranslation(record, locale.value, $event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</a-space>
|
||||||
|
</template>
|
||||||
|
<a-button type="text" class="i18n-button" @click.stop>
|
||||||
|
<Languages :size="15" />
|
||||||
|
</a-button>
|
||||||
|
</a-popover>
|
||||||
|
</div>
|
||||||
|
<a-select
|
||||||
|
v-else-if="column.key === 'b_type'"
|
||||||
|
v-model:value="record.b_type"
|
||||||
|
:options="fieldTypes"
|
||||||
|
/>
|
||||||
|
<a-switch
|
||||||
|
v-else-if="
|
||||||
|
['b_canlist', 'b_canform', 'b_must', 'b_canquery', 'b_readonly'].includes(column.key)
|
||||||
|
"
|
||||||
|
size="small"
|
||||||
|
:checked="record[column.key] === 1"
|
||||||
|
:disabled="column.key === 'b_must' && record.b_canform === 0"
|
||||||
|
@change="setBoolean(record, column.key, $event)"
|
||||||
|
/>
|
||||||
|
<a-input-number
|
||||||
|
v-else-if="column.key === 'b_width'"
|
||||||
|
v-model:value="record.b_width"
|
||||||
|
:min="40"
|
||||||
|
/>
|
||||||
|
<a-input-number v-else-if="column.key === 'b_xh'" v-model:value="record.b_xh" :min="0" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<span v-if="column.key === 'b_field'" class="cell-code">{{ record.b_field }}</span>
|
||||||
|
<span v-else-if="column.key === 'b_name'" class="cell-name">
|
||||||
|
{{ record.b_name }}<Languages v-if="record.b_i18n" :size="13" />
|
||||||
|
</span>
|
||||||
|
<span v-else-if="column.key === 'b_type'">{{ fieldTypeLabel(record.b_type) }}</span>
|
||||||
|
<a-switch
|
||||||
|
v-else-if="
|
||||||
|
['b_canlist', 'b_canform', 'b_must', 'b_canquery', 'b_readonly'].includes(column.key)
|
||||||
|
"
|
||||||
|
size="small"
|
||||||
|
:checked="record[column.key] === 1"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
<span v-else-if="column.key === 'b_width'">{{ record.b_width || "-" }}</span>
|
||||||
|
<span v-else-if="column.key === 'b_xh'">{{ record.b_xh }}</span>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</AutoHeightTable>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.table-pane {
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-toolbar {
|
||||||
|
display: flex;
|
||||||
|
min-height: 40px;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-name-editor,
|
||||||
|
.cell-name {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-name-editor .ant-input {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.i18n-button {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
padding-inline: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell-name svg {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
color: #8c8c8c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell-code {
|
||||||
|
color: #475569;
|
||||||
|
font-family: Consolas, monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.editing-row > td) {
|
||||||
|
background: #f0f7ff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-table-row) {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-table-cell .ant-input-number),
|
||||||
|
:deep(.ant-table-cell .ant-select) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed, ref } from "vue";
|
||||||
|
import { Plus, Trash2 } from "@lucide/vue";
|
||||||
|
import AutoHeightTable from "../../../components/AutoHeightTable.vue";
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
canSave: Boolean,
|
||||||
|
});
|
||||||
|
const emit = defineEmits(["add"]);
|
||||||
|
const powers = defineModel("powers", { type: Array, default: () => [] });
|
||||||
|
|
||||||
|
const editingPowerId = ref("");
|
||||||
|
const selectedPowerKeys = ref([]);
|
||||||
|
const columns = [
|
||||||
|
{ title: "权限代码", key: "b_code", width: 180 },
|
||||||
|
{ title: "名称", key: "b_name", width: 200 },
|
||||||
|
{ title: "多语言键", key: "b_i18n" },
|
||||||
|
{ title: "启用", key: "b_canuse", width: 74, align: "center" },
|
||||||
|
{ title: "顺序", key: "b_xh", width: 90 },
|
||||||
|
{ title: "操作", key: "operation", width: 58 },
|
||||||
|
];
|
||||||
|
const rowSelection = computed(() => ({
|
||||||
|
selectedRowKeys: selectedPowerKeys.value,
|
||||||
|
onChange: (keys) => {
|
||||||
|
selectedPowerKeys.value = keys;
|
||||||
|
},
|
||||||
|
columnWidth: 40,
|
||||||
|
}));
|
||||||
|
|
||||||
|
function setBoolean(record, field, checked) {
|
||||||
|
record[field] = checked ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function powerRow(record) {
|
||||||
|
return {
|
||||||
|
onClick: () => {
|
||||||
|
editingPowerId.value = String(record.b_id);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function leaveEdit(event) {
|
||||||
|
if (!event.target.closest(".ant-table-row")) editingPowerId.value = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function removePower(record) {
|
||||||
|
powers.value = powers.value.filter((item) => String(item.b_id) !== String(record.b_id));
|
||||||
|
selectedPowerKeys.value = selectedPowerKeys.value.filter(
|
||||||
|
(key) => String(key) !== String(record.b_id),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteSelectedPowers() {
|
||||||
|
const ids = new Set(selectedPowerKeys.value.map(String));
|
||||||
|
powers.value = powers.value.filter((item) => !ids.has(String(item.b_id)));
|
||||||
|
selectedPowerKeys.value = [];
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section class="table-pane" @click="leaveEdit">
|
||||||
|
<div class="table-toolbar">
|
||||||
|
<a-button type="primary" ghost :disabled="!canSave" @click="emit('add')">
|
||||||
|
<Plus :size="15" />新增权限
|
||||||
|
</a-button>
|
||||||
|
<a-popconfirm title="确定删除选中的权限吗?" @confirm="deleteSelectedPowers">
|
||||||
|
<a-button danger :disabled="!canSave || !selectedPowerKeys.length">
|
||||||
|
<Trash2 :size="15" />批量删除 ({{ selectedPowerKeys.length }})
|
||||||
|
</a-button>
|
||||||
|
</a-popconfirm>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<AutoHeightTable
|
||||||
|
:columns="columns"
|
||||||
|
:data-source="powers"
|
||||||
|
:pagination="false"
|
||||||
|
:row-class-name="(record) => (String(record.b_id) === editingPowerId ? 'editing-row' : '')"
|
||||||
|
:row-selection="rowSelection"
|
||||||
|
row-key="b_id"
|
||||||
|
@row="powerRow"
|
||||||
|
>
|
||||||
|
<template #bodyCell="{ column, record }">
|
||||||
|
<a-tooltip v-if="column.key === 'operation'" title="删除权限">
|
||||||
|
<a-button type="text" danger @click.stop="removePower(record)">
|
||||||
|
<Trash2 :size="15" />
|
||||||
|
</a-button>
|
||||||
|
</a-tooltip>
|
||||||
|
|
||||||
|
<template v-else-if="String(record.b_id) === editingPowerId">
|
||||||
|
<a-input v-if="column.key === 'b_code'" v-model:value="record.b_code" />
|
||||||
|
<a-input v-else-if="column.key === 'b_name'" v-model:value="record.b_name" />
|
||||||
|
<a-input v-else-if="column.key === 'b_i18n'" v-model:value="record.b_i18n" />
|
||||||
|
<a-switch
|
||||||
|
v-else-if="column.key === 'b_canuse'"
|
||||||
|
size="small"
|
||||||
|
:checked="record.b_canuse === 1"
|
||||||
|
@change="setBoolean(record, 'b_canuse', $event)"
|
||||||
|
/>
|
||||||
|
<a-input-number v-else-if="column.key === 'b_xh'" v-model:value="record.b_xh" :min="0" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<span v-if="column.key === 'b_code'" class="cell-code">{{ record.b_code }}</span>
|
||||||
|
<span v-else-if="column.key === 'b_name'">{{ record.b_name }}</span>
|
||||||
|
<span v-else-if="column.key === 'b_i18n'" class="cell-code">
|
||||||
|
{{ record.b_i18n || "-" }}
|
||||||
|
</span>
|
||||||
|
<a-switch
|
||||||
|
v-else-if="column.key === 'b_canuse'"
|
||||||
|
size="small"
|
||||||
|
:checked="record.b_canuse === 1"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
<span v-else-if="column.key === 'b_xh'">{{ record.b_xh }}</span>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</AutoHeightTable>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.table-pane {
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-toolbar {
|
||||||
|
display: flex;
|
||||||
|
min-height: 40px;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell-code {
|
||||||
|
color: #475569;
|
||||||
|
font-family: Consolas, monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.editing-row > td) {
|
||||||
|
background: #f0f7ff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-table-row) {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-table-cell .ant-input-number) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed, ref } from "vue";
|
||||||
|
import { Plus, Trash2 } from "@lucide/vue";
|
||||||
|
import { App } from "antdv-next";
|
||||||
|
import { LOCALES } from "../../../services/moduleManagement";
|
||||||
|
import AutoHeightTable from "../../../components/AutoHeightTable.vue";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
canSave: Boolean,
|
||||||
|
keys: { type: Array, default: () => [] },
|
||||||
|
});
|
||||||
|
const customKeys = defineModel("customKeys", { type: Array, default: () => [] });
|
||||||
|
const translations = defineModel("translations", { type: Object, default: () => ({}) });
|
||||||
|
const { message } = App.useApp();
|
||||||
|
|
||||||
|
const newI18nKey = ref("");
|
||||||
|
const showI18nInput = ref(false);
|
||||||
|
const columns = [
|
||||||
|
{ title: "多语言键", key: "key", width: 280, fixed: "left" },
|
||||||
|
...LOCALES.map((locale) => ({
|
||||||
|
title: locale.label,
|
||||||
|
key: locale.value,
|
||||||
|
width: 220,
|
||||||
|
})),
|
||||||
|
{ title: "操作", key: "operation", width: 58, fixed: "right" },
|
||||||
|
];
|
||||||
|
const rows = computed(() =>
|
||||||
|
props.keys.map((key) => ({
|
||||||
|
key,
|
||||||
|
isCustom: customKeys.value.includes(key),
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
function translationEntry(key) {
|
||||||
|
translations.value[key] ||= {};
|
||||||
|
return translations.value[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
function setTranslation(key, locale, value) {
|
||||||
|
translationEntry(key)[locale] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addCustomKey() {
|
||||||
|
const key = newI18nKey.value.trim();
|
||||||
|
if (!key) return;
|
||||||
|
if (props.keys.includes(key)) {
|
||||||
|
message.warning("多语言键已存在");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
customKeys.value = [...customKeys.value, key];
|
||||||
|
translationEntry(key);
|
||||||
|
newI18nKey.value = "";
|
||||||
|
showI18nInput.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeCustomKey(key) {
|
||||||
|
customKeys.value = customKeys.value.filter((item) => item !== key);
|
||||||
|
delete translations.value[key];
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section class="translation-pane">
|
||||||
|
<div class="table-toolbar">
|
||||||
|
<template v-if="showI18nInput">
|
||||||
|
<a-input
|
||||||
|
v-model:value="newI18nKey"
|
||||||
|
placeholder="输入多语言键名,回车确认"
|
||||||
|
size="small"
|
||||||
|
class="new-key-input"
|
||||||
|
@press-enter="addCustomKey"
|
||||||
|
/>
|
||||||
|
<a-button
|
||||||
|
type="primary"
|
||||||
|
ghost
|
||||||
|
size="small"
|
||||||
|
:disabled="!newI18nKey.trim()"
|
||||||
|
@click="addCustomKey"
|
||||||
|
>
|
||||||
|
<Plus :size="15" />添加
|
||||||
|
</a-button>
|
||||||
|
<a-button size="small" @click="showI18nInput = false">取消</a-button>
|
||||||
|
</template>
|
||||||
|
<a-button v-else type="primary" ghost :disabled="!canSave" @click="showI18nInput = true">
|
||||||
|
<Plus :size="15" />新增多语言键
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<AutoHeightTable :columns="columns" :data-source="rows" :pagination="false" row-key="key">
|
||||||
|
<template #bodyCell="{ column, record }">
|
||||||
|
<code v-if="column.key === 'key'" class="translation-key">{{ record.key }}</code>
|
||||||
|
<a-input
|
||||||
|
v-else-if="LOCALES.some((locale) => locale.value === column.key)"
|
||||||
|
:value="translationEntry(record.key)[column.key]"
|
||||||
|
size="small"
|
||||||
|
:placeholder="column.key === 'zh-CN' ? '必填' : '可选'"
|
||||||
|
@update:value="setTranslation(record.key, column.key, $event)"
|
||||||
|
/>
|
||||||
|
<a-tooltip v-else-if="column.key === 'operation' && record.isCustom" title="删除多语言键">
|
||||||
|
<a-button type="text" danger @click="removeCustomKey(record.key)">
|
||||||
|
<Trash2 :size="15" />
|
||||||
|
</a-button>
|
||||||
|
</a-tooltip>
|
||||||
|
</template>
|
||||||
|
<template #emptyText>
|
||||||
|
<a-empty description="当前模块没有多语言键" />
|
||||||
|
</template>
|
||||||
|
</AutoHeightTable>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.translation-pane {
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-toolbar {
|
||||||
|
display: flex;
|
||||||
|
min-height: 40px;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-key-input {
|
||||||
|
width: 260px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.translation-key {
|
||||||
|
color: #374151;
|
||||||
|
font-family: Consolas, monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,22 +1,17 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref } from "vue";
|
import { computed, ref } from "vue";
|
||||||
import { FileText, Folder, Plus, RefreshCw, Search } from "@lucide/vue";
|
import { RefreshCw, Search } from "@lucide/vue";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
modules: { type: Array, required: true },
|
modules: { type: Array, required: true },
|
||||||
selectedKey: { type: String, default: "" },
|
selectedKey: { type: String, default: "" },
|
||||||
loading: Boolean,
|
loading: Boolean,
|
||||||
canCreate: Boolean,
|
canCreate: Boolean,
|
||||||
|
canDelete: Boolean,
|
||||||
});
|
});
|
||||||
const emit = defineEmits(["select", "add", "refresh"]);
|
const emit = defineEmits(["select", "add", "delete", "refresh"]);
|
||||||
const searchText = ref("");
|
const searchText = ref("");
|
||||||
const addMenu = {
|
const contextNode = ref(null);
|
||||||
items: [
|
|
||||||
{ key: "module", label: "新增模块" },
|
|
||||||
{ key: "directory", label: "新增目录" },
|
|
||||||
],
|
|
||||||
onClick: ({ key }) => emit("add", key),
|
|
||||||
};
|
|
||||||
|
|
||||||
const treeData = computed(() => {
|
const treeData = computed(() => {
|
||||||
const keyword = searchText.value.trim().toLowerCase();
|
const keyword = searchText.value.trim().toLowerCase();
|
||||||
@@ -25,8 +20,8 @@ const treeData = computed(() => {
|
|||||||
props.modules
|
props.modules
|
||||||
.filter(
|
.filter(
|
||||||
(item) =>
|
(item) =>
|
||||||
item.b_id.toLowerCase().includes(keyword) ||
|
String(item.b_id).toLowerCase().includes(keyword) ||
|
||||||
item.b_name.toLowerCase().includes(keyword),
|
String(item.b_name).toLowerCase().includes(keyword),
|
||||||
)
|
)
|
||||||
.flatMap((item) => [item.b_id, item.b_parent_id]),
|
.flatMap((item) => [item.b_id, item.b_parent_id]),
|
||||||
)
|
)
|
||||||
@@ -43,16 +38,46 @@ const treeData = computed(() => {
|
|||||||
.map((item) => ({
|
.map((item) => ({
|
||||||
key: item.b_id,
|
key: item.b_id,
|
||||||
title: item.b_name,
|
title: item.b_name,
|
||||||
code: item.b_id,
|
|
||||||
type: item.b_type,
|
|
||||||
children: build(item.b_id),
|
children: build(item.b_id),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return build("__root__");
|
return build("__root__");
|
||||||
});
|
});
|
||||||
|
|
||||||
function onSelect(keys) {
|
function onSelect(keys) {
|
||||||
if (keys[0]) emit("select", keys[0]);
|
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: "module", label: "新增模块", disabled: !props.canCreate },
|
||||||
|
{ key: "page", label: "新增页面", disabled: !props.canCreate },
|
||||||
|
{ type: "divider" },
|
||||||
|
{
|
||||||
|
key: "delete",
|
||||||
|
label: "删除",
|
||||||
|
danger: true,
|
||||||
|
disabled: !node || !props.canDelete || ["M_SYSTEM", "M_MODULE"].includes(node.key),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
onClick: ({ key }) => {
|
||||||
|
if (!node) return;
|
||||||
|
if (key === "delete") {
|
||||||
|
emit("delete", node.key);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
emit("add", key, node.key);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -62,43 +87,29 @@ function onSelect(keys) {
|
|||||||
<strong>模块</strong>
|
<strong>模块</strong>
|
||||||
<span>{{ modules.length }}</span>
|
<span>{{ modules.length }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="tree-header-actions">
|
<a-tooltip title="刷新">
|
||||||
<a-tooltip title="新增">
|
<a-button type="text" size="small" :loading="loading" @click="emit('refresh')">
|
||||||
<a-dropdown :menu="addMenu" :trigger="['click']" :disabled="!canCreate">
|
<RefreshCw :size="16" />
|
||||||
<a-button type="text" size="small" :disabled="!canCreate">
|
</a-button>
|
||||||
<Plus :size="16" />
|
</a-tooltip>
|
||||||
</a-button>
|
|
||||||
</a-dropdown>
|
|
||||||
</a-tooltip>
|
|
||||||
<a-tooltip title="刷新">
|
|
||||||
<a-button type="text" size="small" :loading="loading" @click="emit('refresh')">
|
|
||||||
<RefreshCw :size="16" />
|
|
||||||
</a-button>
|
|
||||||
</a-tooltip>
|
|
||||||
</div>
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<a-input v-model:value="searchText" allow-clear placeholder="搜索名称或代码">
|
<a-input v-model:value="searchText" allow-clear size="small" placeholder="搜索名称或编码">
|
||||||
<template #prefix><Search :size="15" /></template>
|
<template #prefix><Search :size="15" /></template>
|
||||||
</a-input>
|
</a-input>
|
||||||
|
|
||||||
<a-spin :spinning="loading">
|
<a-spin :spinning="loading" class="tree-spin">
|
||||||
<a-tree
|
<a-dropdown :menu="contextMenu" :trigger="['contextmenu']">
|
||||||
block-node
|
<a-tree
|
||||||
default-expand-all
|
block-node
|
||||||
:tree-data="treeData"
|
default-expand-all
|
||||||
:selected-keys="selectedKey ? [selectedKey] : []"
|
show-line
|
||||||
@select="onSelect"
|
:tree-data="treeData"
|
||||||
>
|
:selected-keys="selectedKey ? [selectedKey] : []"
|
||||||
<template #title="node">
|
@right-click="onRightClick"
|
||||||
<div class="tree-title">
|
@select="onSelect"
|
||||||
<Folder v-if="node.type === 'directory'" :size="15" />
|
/>
|
||||||
<FileText v-else :size="15" />
|
</a-dropdown>
|
||||||
<span>{{ node.title }}</span>
|
|
||||||
<small>{{ node.code }}</small>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</a-tree>
|
|
||||||
</a-spin>
|
</a-spin>
|
||||||
</aside>
|
</aside>
|
||||||
</template>
|
</template>
|
||||||
@@ -113,45 +124,34 @@ function onSelect(keys) {
|
|||||||
background: #fff;
|
background: #fff;
|
||||||
padding: 10px 8px;
|
padding: 10px 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
header,
|
header,
|
||||||
header > div,
|
header > div {
|
||||||
.tree-title {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
header {
|
header {
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
header > div {
|
header > div {
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
header span {
|
header span {
|
||||||
color: #8c8c8c;
|
color: #8c8c8c;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
.tree-header-actions {
|
|
||||||
gap: 4px;
|
.tree-spin {
|
||||||
}
|
|
||||||
.tree-title {
|
|
||||||
min-width: 0;
|
|
||||||
gap: 7px;
|
|
||||||
}
|
|
||||||
.tree-title span {
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
.tree-title small {
|
|
||||||
margin-left: auto;
|
|
||||||
color: #9ca3af;
|
|
||||||
font-size: 10px;
|
|
||||||
}
|
|
||||||
:deep(.ant-spin-nested-loading) {
|
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
flex: 1;
|
flex: 1 1 auto;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
:deep(.ant-tree) {
|
|
||||||
background: transparent;
|
:deep(.ant-spin-nested-loading),
|
||||||
|
:deep(.ant-spin-container) {
|
||||||
|
min-height: 0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
Reference in new issue
Block a user