20260812221200
This commit is contained in:
1 parent
87b2d3c482
commit
4bd730f649
47 files changed
+6286
-1881
No files matched your search
+53
-39
@@ -6,10 +6,7 @@ import { Input } from "@/components/ui/input";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import FmsTree from "@/components/FmsTree.vue";
|
||||
import {
|
||||
loadDataApi,
|
||||
saveObjectApi,
|
||||
} from "@/services/api";
|
||||
import { loadDataApi, saveObjectApi } from "@/services/api";
|
||||
import {
|
||||
diffRows,
|
||||
tableChange,
|
||||
@@ -57,15 +54,18 @@ function rebuildTree() {
|
||||
groupTree.value = buildGroupTree(props.groups);
|
||||
}
|
||||
|
||||
watch(() => props.open, (v) => {
|
||||
if (v) {
|
||||
rebuildTree();
|
||||
const all = props.groups.map((g) => String(g.b_id));
|
||||
expandedKeys.value = all;
|
||||
dirty.value = false;
|
||||
selectedGroupId.value = null;
|
||||
}
|
||||
});
|
||||
watch(
|
||||
() => props.open,
|
||||
(v) => {
|
||||
if (v) {
|
||||
rebuildTree();
|
||||
const all = props.groups.map((g) => String(g.b_id));
|
||||
expandedKeys.value = all;
|
||||
dirty.value = false;
|
||||
selectedGroupId.value = null;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const selectedGroup = ref<Record<string, unknown> | null>(null);
|
||||
|
||||
@@ -74,8 +74,7 @@ watch(selectedGroupId, (id) => {
|
||||
selectedGroup.value = null;
|
||||
return;
|
||||
}
|
||||
selectedGroup.value =
|
||||
props.groups.find((g) => String(g.b_id) === id) || null;
|
||||
selectedGroup.value = props.groups.find((g) => String(g.b_id) === id) || null;
|
||||
});
|
||||
|
||||
function findNode(key: string): Record<string, unknown> | null {
|
||||
@@ -160,7 +159,12 @@ async function deleteGroup(groupId: string) {
|
||||
toast.error("该分组已被列表配置或编辑配置引用,请先取消引用。");
|
||||
return;
|
||||
}
|
||||
const ok = await confirm({ title: "删除分组", description: "确定删除该分组?", confirmText: "删除", danger: true });
|
||||
const ok = await confirm({
|
||||
title: "删除分组",
|
||||
description: "确定删除该分组?",
|
||||
confirmText: "删除",
|
||||
danger: true,
|
||||
});
|
||||
if (!ok) return;
|
||||
|
||||
const walk = (nodes: Record<string, unknown>[]): boolean => {
|
||||
@@ -223,14 +227,13 @@ async function close() {
|
||||
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<div
|
||||
v-if="open"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center"
|
||||
>
|
||||
<div v-if="open" class="fixed inset-0 z-50 flex items-center justify-center">
|
||||
<!-- Backdrop -->
|
||||
<div class="fixed inset-0 bg-black/50 backdrop-blur-sm" @click="close" />
|
||||
<!-- Dialog -->
|
||||
<div class="relative z-50 w-[720px] max-h-[80vh] flex flex-col rounded-lg border bg-card shadow-xl">
|
||||
<div
|
||||
class="relative z-50 w-[720px] max-h-[80vh] flex flex-col rounded-lg border bg-card shadow-xl"
|
||||
>
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between border-b px-5 py-3">
|
||||
<div class="flex items-center gap-2">
|
||||
@@ -242,7 +245,14 @@ async function close() {
|
||||
class="rounded p-0.5 text-muted-foreground transition-colors hover:text-foreground"
|
||||
@click="close"
|
||||
>
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path d="M18 6L6 18M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
@@ -268,39 +278,43 @@ async function close() {
|
||||
<div class="space-y-4">
|
||||
<div class="space-y-2">
|
||||
<Label class="text-[13px]">分组名称</Label>
|
||||
<Input
|
||||
v-model="(selectedGroup.b_name as string)"
|
||||
class="h-8 text-[13px]"
|
||||
@input="dirty = true"
|
||||
/>
|
||||
<Input v-model="selectedGroup.b_name as string" @input="dirty = true" />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label class="text-[13px]">多语言键</Label>
|
||||
<Input
|
||||
v-model="(selectedGroup.b_i18n as string)"
|
||||
class="h-8 text-[13px]"
|
||||
@input="dirty = true"
|
||||
/>
|
||||
<Input v-model="selectedGroup.b_i18n as string" @input="dirty = true" />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label class="text-[13px]">排序号</Label>
|
||||
<Input
|
||||
:model-value="String(selectedGroup.b_xh ?? 0)"
|
||||
type="number"
|
||||
class="h-8 text-[13px]"
|
||||
@update:model-value="(v) => { (selectedGroup.b_xh as number) = Number(v); dirty = true }"
|
||||
@update:model-value="
|
||||
(v) => {
|
||||
(selectedGroup.b_xh as number) = Number(v);
|
||||
dirty = true;
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Switch
|
||||
:checked="Number(selectedGroup.b_canuse) === 1"
|
||||
@update:checked="(v) => { (selectedGroup.b_canuse as number) = v ? 1 : 0; dirty = true }"
|
||||
@update:checked="
|
||||
(v) => {
|
||||
(selectedGroup.b_canuse as number) = v ? 1 : 0;
|
||||
dirty = true;
|
||||
}
|
||||
"
|
||||
/>
|
||||
<Label class="text-[13px]">启用</Label>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else class="flex items-center justify-center h-full text-sm text-muted-foreground">
|
||||
<div
|
||||
v-else
|
||||
class="flex items-center justify-center h-full text-sm text-muted-foreground"
|
||||
>
|
||||
请从左侧选择分组
|
||||
</div>
|
||||
</div>
|
||||
@@ -308,13 +322,13 @@ async function close() {
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="flex items-center justify-between border-t px-5 py-2.5">
|
||||
<Button variant="outline" size="sm" class="h-7 text-[12px]" @click="addGroup(null)">
|
||||
<Button variant="outline" @click="addGroup(null)">
|
||||
<Plus :size="13" class="mr-1" />
|
||||
添加分组
|
||||
</Button>
|
||||
<div class="flex items-center gap-2">
|
||||
<Button variant="outline" size="sm" class="h-7 text-[12px]" @click="close">取消</Button>
|
||||
<Button size="sm" class="h-7 text-[12px]" :disabled="saving" @click="saveGroups">
|
||||
<Button variant="outline" @click="close">取消</Button>
|
||||
<Button :disabled="saving" @click="saveGroups">
|
||||
{{ saving ? "保存中…" : "保存" }}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
+54
-51
@@ -21,45 +21,46 @@ const locales = ref<string[]>([]);
|
||||
const loading = ref(false);
|
||||
const saving = ref(false);
|
||||
|
||||
watch(
|
||||
() => props.open,
|
||||
async (v) => {
|
||||
if (!v) return;
|
||||
loading.value = true;
|
||||
try {
|
||||
const key = props.i18nKey;
|
||||
if (!key) {
|
||||
translations.value = {};
|
||||
return;
|
||||
}
|
||||
// Load language types
|
||||
const langRes = await loadDataApi(
|
||||
"b_i18n_type",
|
||||
"b_canuse = 1",
|
||||
"b_default DESC, b_xh ASC, b_id ASC",
|
||||
);
|
||||
const langs = (langRes.data || []) as { b_code: string; b_default: number }[];
|
||||
locales.value = langs.map((l) => l.b_code);
|
||||
|
||||
// Load existing translations
|
||||
const i18nRes = await loadDataApi("b_i18n", `b_key = '${key}'`);
|
||||
const existing = (i18nRes.data || []) as { b_locale: string; b_value: string }[];
|
||||
|
||||
watch(() => props.open, async (v) => {
|
||||
if (!v) return;
|
||||
loading.value = true;
|
||||
try {
|
||||
const key = props.i18nKey;
|
||||
if (!key) {
|
||||
translations.value = {};
|
||||
return;
|
||||
const values: Record<string, string> = {};
|
||||
for (const lang of locales.value) {
|
||||
const found = existing.find((e) => e.b_locale === lang);
|
||||
values[lang] = found?.b_value ?? "";
|
||||
}
|
||||
// Set default value for primary locale
|
||||
const primary = langs.find((l) => Number(l.b_default) === 1);
|
||||
if (primary && !values[primary.b_code]) {
|
||||
values[primary.b_code] = props.defaultValue;
|
||||
}
|
||||
translations.value = values;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
// Load language types
|
||||
const langRes = await loadDataApi(
|
||||
"b_i18n_type",
|
||||
"b_canuse = 1",
|
||||
"b_default DESC, b_xh ASC, b_id ASC",
|
||||
);
|
||||
const langs = (langRes.data || []) as { b_code: string; b_default: number }[];
|
||||
locales.value = langs.map((l) => l.b_code);
|
||||
|
||||
// Load existing translations
|
||||
const i18nRes = await loadDataApi("b_i18n", `b_key = '${key}'`);
|
||||
const existing = (i18nRes.data || []) as { b_locale: string; b_value: string }[];
|
||||
|
||||
const values: Record<string, string> = {};
|
||||
for (const lang of locales.value) {
|
||||
const found = existing.find((e) => e.b_locale === lang);
|
||||
values[lang] = found?.b_value ?? "";
|
||||
}
|
||||
// Set default value for primary locale
|
||||
const primary = langs.find((l) => Number(l.b_default) === 1);
|
||||
if (primary && !values[primary.b_code]) {
|
||||
values[primary.b_code] = props.defaultValue;
|
||||
}
|
||||
translations.value = values;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
async function save() {
|
||||
saving.value = true;
|
||||
@@ -77,9 +78,7 @@ async function save() {
|
||||
});
|
||||
}
|
||||
if (inserts.length) {
|
||||
await saveObjectApi([
|
||||
{ table: "b_i18n", key_field: "b_key", inserts },
|
||||
]);
|
||||
await saveObjectApi([{ table: "b_i18n", key_field: "b_key", inserts }]);
|
||||
}
|
||||
emit("saved", key);
|
||||
emit("update:open", false);
|
||||
@@ -97,7 +96,9 @@ function close() {
|
||||
<Teleport to="body">
|
||||
<div v-if="open" class="fixed inset-0 z-50 flex items-center justify-center">
|
||||
<div class="fixed inset-0 bg-black/50 backdrop-blur-sm" @click="close" />
|
||||
<div class="relative z-50 w-[480px] max-h-[70vh] flex flex-col rounded-lg border bg-card shadow-xl">
|
||||
<div
|
||||
class="relative z-50 w-[480px] max-h-[70vh] flex flex-col rounded-lg border bg-card shadow-xl"
|
||||
>
|
||||
<div class="flex items-center justify-between border-b px-5 py-3">
|
||||
<span class="text-[13px] font-semibold">多语言快速设置</span>
|
||||
<button
|
||||
@@ -105,7 +106,14 @@ function close() {
|
||||
class="rounded p-0.5 text-muted-foreground transition-colors hover:text-foreground"
|
||||
@click="close"
|
||||
>
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path d="M18 6L6 18M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
@@ -113,22 +121,17 @@ function close() {
|
||||
<div class="flex-1 overflow-auto p-5 space-y-3">
|
||||
<div class="space-y-1.5">
|
||||
<Label class="text-[12px]">翻译键</Label>
|
||||
<Input :model-value="i18nKey" class="h-8 text-[13px]" disabled />
|
||||
</div>
|
||||
<div v-if="loading" class="text-center text-sm text-muted-foreground py-6">
|
||||
加载中…
|
||||
<Input :model-value="i18nKey" disabled />
|
||||
</div>
|
||||
<div v-if="loading" class="text-center text-sm text-muted-foreground py-6">加载中…</div>
|
||||
<div v-else v-for="locale in locales" :key="locale" class="space-y-1.5">
|
||||
<Label class="text-[12px]">{{ locale }}</Label>
|
||||
<Input
|
||||
v-model="translations[locale]"
|
||||
class="h-8 text-[13px]"
|
||||
/>
|
||||
<Input v-model="translations[locale]" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-end gap-2 border-t px-5 py-2.5">
|
||||
<Button variant="outline" size="sm" class="h-7 text-[12px]" @click="close">取消</Button>
|
||||
<Button size="sm" class="h-7 text-[12px]" :disabled="saving" @click="save">
|
||||
<Button variant="outline" @click="close">取消</Button>
|
||||
<Button :disabled="saving" @click="save">
|
||||
{{ saving ? "保存中…" : "保存" }}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
+156
-20
@@ -3,42 +3,178 @@ import { computed } from "vue";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { createAutoCodeDraft } from "../common";
|
||||
|
||||
const props = defineProps<{ moduleId: string; autoCode: Record<string, unknown>[]; fields: Record<string, unknown>[]; loading?: boolean }>();
|
||||
const props = defineProps<{
|
||||
moduleId: string;
|
||||
autoCode: Record<string, unknown>[];
|
||||
fields: Record<string, unknown>[];
|
||||
loading?: boolean;
|
||||
}>();
|
||||
const emit = defineEmits<{ (e: "update", rows: Record<string, unknown>[]): void }>();
|
||||
const rule = computed(() => props.autoCode?.length ? props.autoCode[0] : null);
|
||||
const rule = computed(() => (props.autoCode?.length ? props.autoCode[0] : null));
|
||||
const enabled = computed(() => Number(rule.value?.b_canuse) === 1);
|
||||
const FMT = ["yyyy","yy","yyyyMM","yyyy-MM","yyyyMMdd","yyyy-MM-dd","yyyy/MM/dd","yyyyMMddHHmm","yyyy-MM-dd HH:mm","yyyy/MM/dd HH:mm"];
|
||||
const AVF = computed(() => (props.fields || []).filter((f) => Number(f.b_canuse) === 1 && f.b_field !== "b_id" && (f.b_type === "input" || f.b_type === "textarea")));
|
||||
const RST = [{ value: "none", label: "不重置" },{ value: "year", label: "按年" },{ value: "month", label: "按月" },{ value: "day", label: "按日" }];
|
||||
const preview = computed(() => { if (!rule.value || !enabled.value) return ""; const p = String(rule.value.b_prefix || ""), sep = String(rule.value.b_separator || ""), fmt = String(rule.value.b_date_format || ""), w = Number(rule.value.b_sequence_width || 5), sv = Number(rule.value.b_start_value || 1); const d = new Date(), Y = String(d.getFullYear()), y = Y.slice(2), M = String(d.getMonth()+1).padStart(2,"0"), D = String(d.getDate()).padStart(2,"0"); const ds = fmt.replace("yyyy",Y).replace("yy",y).replace("MM",M).replace("dd",D); return `${p}${ds}${sep}${String(sv).padStart(w,"0")}`; });
|
||||
function setEn(v: boolean) { if (v && !rule.value) { emit("update", [createAutoCodeDraft(props.moduleId, props.fields as any, true)]); } else if (rule.value) { emit("update", [{ ...rule.value, b_canuse: v ? 1 : 0 }]); } }
|
||||
function up(key: string, value: unknown) { if (!rule.value) return; emit("update", [{ ...rule.value, [key]: value }]); }
|
||||
const FMT = [
|
||||
"yyyy",
|
||||
"yy",
|
||||
"yyyyMM",
|
||||
"yyyy-MM",
|
||||
"yyyyMMdd",
|
||||
"yyyy-MM-dd",
|
||||
"yyyy/MM/dd",
|
||||
"yyyyMMddHHmm",
|
||||
"yyyy-MM-dd HH:mm",
|
||||
"yyyy/MM/dd HH:mm",
|
||||
];
|
||||
const AVF = computed(() =>
|
||||
(props.fields || []).filter(
|
||||
(f) =>
|
||||
Number(f.b_canuse) === 1 &&
|
||||
f.b_field !== "b_id" &&
|
||||
(f.b_type === "input" || f.b_type === "textarea"),
|
||||
),
|
||||
);
|
||||
const RST = [
|
||||
{ value: "none", label: "不重置" },
|
||||
{ value: "year", label: "按年" },
|
||||
{ value: "month", label: "按月" },
|
||||
{ value: "day", label: "按日" },
|
||||
];
|
||||
const preview = computed(() => {
|
||||
if (!rule.value || !enabled.value) return "";
|
||||
const p = String(rule.value.b_prefix || ""),
|
||||
sep = String(rule.value.b_separator || ""),
|
||||
fmt = String(rule.value.b_date_format || ""),
|
||||
w = Number(rule.value.b_sequence_width || 5),
|
||||
sv = Number(rule.value.b_start_value || 1);
|
||||
const d = new Date(),
|
||||
Y = String(d.getFullYear()),
|
||||
y = Y.slice(2),
|
||||
M = String(d.getMonth() + 1).padStart(2, "0"),
|
||||
D = String(d.getDate()).padStart(2, "0");
|
||||
const ds = fmt.replace("yyyy", Y).replace("yy", y).replace("MM", M).replace("dd", D);
|
||||
return `${p}${ds}${sep}${String(sv).padStart(w, "0")}`;
|
||||
});
|
||||
function setEn(v: boolean) {
|
||||
if (v && !rule.value) {
|
||||
emit("update", [createAutoCodeDraft(props.moduleId, props.fields as any, true)]);
|
||||
} else if (rule.value) {
|
||||
emit("update", [{ ...rule.value, b_canuse: v ? 1 : 0 }]);
|
||||
}
|
||||
}
|
||||
function up(key: string, value: unknown) {
|
||||
if (!rule.value) return;
|
||||
emit("update", [{ ...rule.value, [key]: value }]);
|
||||
}
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export default { name: "ModuleAutoCodePanel" };
|
||||
</script>
|
||||
<script lang="ts">export default { name: "ModuleAutoCodePanel" };</script>
|
||||
|
||||
<template>
|
||||
<div class="h-full min-h-0 overflow-y-auto space-y-5">
|
||||
<div class="flex items-center justify-between rounded-lg border p-3">
|
||||
<div>
|
||||
<div class="text-[13px] font-semibold">自动编码</div>
|
||||
<div class="text-[12px] text-muted-foreground mt-0.5">开启后将按照配置规则自动生成唯一编号</div>
|
||||
<div class="text-[12px] text-muted-foreground mt-0.5">
|
||||
开启后将按照配置规则自动生成唯一编号
|
||||
</div>
|
||||
</div>
|
||||
<Switch :checked="enabled" :disabled="loading" @update:checked="setEn" />
|
||||
</div>
|
||||
<template v-if="rule">
|
||||
<div class="grid w-[640px] max-w-full grid-cols-2 gap-x-6 gap-y-4" :class="enabled ? '' : 'pointer-events-none opacity-50'">
|
||||
<div class="space-y-1.5"><Label class="text-[12px]">业务编号字段</Label><Select :model-value="String(rule.b_field_id ?? '')" :disabled="!enabled || loading" @update:model-value="(v: string) => up('b_field_id', v)"><SelectTrigger class="h-8 text-[13px]"><SelectValue placeholder="请选择" /></SelectTrigger><SelectContent><SelectItem v-for="f in AVF" :key="String(f.b_id)" :value="String(f.b_id)">{{ String(f.b_field) }}</SelectItem></SelectContent></Select></div>
|
||||
<div class="space-y-1.5"><Label class="text-[12px]">固定前缀</Label><Input :model-value="String(rule.b_prefix ?? '')" class="h-8 text-[13px]" placeholder="例如: ORD" :disabled="!enabled || loading" @update:model-value="(v: string) => up('b_prefix', v)" /></div>
|
||||
<div class="space-y-1.5"><Label class="text-[12px]">日期格式</Label><Select :model-value="String(rule.b_date_format ?? 'yyyyMM')" :disabled="!enabled || loading" @update:model-value="(v: string) => up('b_date_format', v)"><SelectTrigger class="h-8 text-[13px]"><SelectValue /></SelectTrigger><SelectContent><SelectItem v-for="f in FMT" :key="f" :value="f">{{ f }}</SelectItem></SelectContent></Select></div>
|
||||
<div class="space-y-1.5"><Label class="text-[12px]">分隔符</Label><Input :model-value="String(rule.b_separator ?? '-')" class="h-8 text-[13px]" placeholder="-" :disabled="!enabled || loading" @update:model-value="(v: string) => up('b_separator', v)" /></div>
|
||||
<div class="space-y-1.5"><Label class="text-[12px]">流水号位数 (1-18)</Label><Input :model-value="String(rule.b_sequence_width ?? 5)" type="number" class="h-8 text-[13px]" :disabled="!enabled || loading" @update:model-value="(v: string) => up('b_sequence_width', Number(v))" /></div>
|
||||
<div class="space-y-1.5"><Label class="text-[12px]">起始值</Label><Input :model-value="String(rule.b_start_value ?? 1)" type="number" class="h-8 text-[13px]" :disabled="!enabled || loading" @update:model-value="(v: string) => up('b_start_value', Number(v))" /></div>
|
||||
<div class="space-y-1.5"><Label class="text-[12px]">重置周期</Label><Select :model-value="String(rule.b_reset_type ?? 'month')" :disabled="!enabled || loading" @update:model-value="(v: string) => up('b_reset_type', v)"><SelectTrigger class="h-8 text-[13px]"><SelectValue /></SelectTrigger><SelectContent><SelectItem v-for="r in RST" :key="r.value" :value="r.value">{{ r.label }}</SelectItem></SelectContent></Select></div>
|
||||
<div
|
||||
class="grid w-[640px] max-w-full grid-cols-2 gap-x-6 gap-y-4"
|
||||
:class="enabled ? '' : 'pointer-events-none opacity-50'"
|
||||
>
|
||||
<div class="space-y-1.5">
|
||||
<Label class="text-[12px]">业务编号字段</Label
|
||||
><Select
|
||||
:model-value="String(rule.b_field_id ?? '')"
|
||||
:disabled="!enabled || loading"
|
||||
@update:model-value="(v: string) => up('b_field_id', v)"
|
||||
><SelectTrigger><SelectValue placeholder="请选择" /></SelectTrigger
|
||||
><SelectContent
|
||||
><SelectItem v-for="f in AVF" :key="String(f.b_id)" :value="String(f.b_id)">{{
|
||||
String(f.b_field)
|
||||
}}</SelectItem></SelectContent
|
||||
></Select
|
||||
>
|
||||
</div>
|
||||
<div class="space-y-1.5">
|
||||
<Label class="text-[12px]">固定前缀</Label
|
||||
><Input
|
||||
:model-value="String(rule.b_prefix ?? '')"
|
||||
placeholder="例如: ORD"
|
||||
:disabled="!enabled || loading"
|
||||
@update:model-value="(v: string) => up('b_prefix', v)"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-1.5">
|
||||
<Label class="text-[12px]">日期格式</Label
|
||||
><Select
|
||||
:model-value="String(rule.b_date_format ?? 'yyyyMM')"
|
||||
:disabled="!enabled || loading"
|
||||
@update:model-value="(v: string) => up('b_date_format', v)"
|
||||
><SelectTrigger><SelectValue /></SelectTrigger
|
||||
><SelectContent
|
||||
><SelectItem v-for="f in FMT" :key="f" :value="f">{{ f }}</SelectItem></SelectContent
|
||||
></Select
|
||||
>
|
||||
</div>
|
||||
<div class="space-y-1.5">
|
||||
<Label class="text-[12px]">分隔符</Label
|
||||
><Input
|
||||
:model-value="String(rule.b_separator ?? '-')"
|
||||
placeholder="-"
|
||||
:disabled="!enabled || loading"
|
||||
@update:model-value="(v: string) => up('b_separator', v)"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-1.5">
|
||||
<Label class="text-[12px]">流水号位数 (1-18)</Label
|
||||
><Input
|
||||
:model-value="String(rule.b_sequence_width ?? 5)"
|
||||
type="number"
|
||||
:disabled="!enabled || loading"
|
||||
@update:model-value="(v: string) => up('b_sequence_width', Number(v))"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-1.5">
|
||||
<Label class="text-[12px]">起始值</Label
|
||||
><Input
|
||||
:model-value="String(rule.b_start_value ?? 1)"
|
||||
type="number"
|
||||
:disabled="!enabled || loading"
|
||||
@update:model-value="(v: string) => up('b_start_value', Number(v))"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-1.5">
|
||||
<Label class="text-[12px]">重置周期</Label
|
||||
><Select
|
||||
:model-value="String(rule.b_reset_type ?? 'month')"
|
||||
:disabled="!enabled || loading"
|
||||
@update:model-value="(v: string) => up('b_reset_type', v)"
|
||||
><SelectTrigger><SelectValue /></SelectTrigger
|
||||
><SelectContent
|
||||
><SelectItem v-for="r in RST" :key="r.value" :value="r.value">{{
|
||||
r.label
|
||||
}}</SelectItem></SelectContent
|
||||
></Select
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="enabled" class="rounded-lg border bg-muted/20 p-3">
|
||||
<span class="text-[12px] text-muted-foreground mr-2">预览</span
|
||||
><code class="text-[14px] font-medium">{{ preview }}</code>
|
||||
</div>
|
||||
<div v-if="enabled" class="rounded-lg border bg-muted/20 p-3"><span class="text-[12px] text-muted-foreground mr-2">预览</span><code class="text-[14px] font-medium">{{ preview }}</code></div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
+170
-20
@@ -3,19 +3,66 @@ import { Globe } from "@lucide/vue";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import I18nQuickSetModal from "./I18nQuickSetModal.vue";
|
||||
import { MODULE_TYPE_LABELS } from "@/types/module";
|
||||
import { buildModuleI18nKey } from "@/utils/i18n";
|
||||
|
||||
const props = defineProps<{ moduleId: string; data: Record<string, unknown> | null; parentName?: string; loading?: boolean }>();
|
||||
const props = defineProps<{
|
||||
moduleId: string;
|
||||
data: Record<string, unknown> | null;
|
||||
parentName?: string;
|
||||
loading?: boolean;
|
||||
}>();
|
||||
const emit = defineEmits<{ (e: "update", data: Record<string, unknown>): void }>();
|
||||
|
||||
const moduleTypeOptions = Object.entries(MODULE_TYPE_LABELS).map(([v, l]) => ({ value: v, label: l }));
|
||||
const iconList = ["Folder","FileText","Database","Settings","Users","Globe","Home","Layout","Package","Shield","BarChart","PieChart","List","Grid","Calendar","Clock","Map","Mail","MessageSquare","Search","Code","Terminal","Book","Zap","Star","Heart","Award","Trophy","Briefcase","ShoppingCart"];
|
||||
const moduleTypeOptions = Object.entries(MODULE_TYPE_LABELS).map(([v, l]) => ({
|
||||
value: v,
|
||||
label: l,
|
||||
}));
|
||||
const iconList = [
|
||||
"Folder",
|
||||
"FileText",
|
||||
"Database",
|
||||
"Settings",
|
||||
"Users",
|
||||
"Globe",
|
||||
"Home",
|
||||
"Layout",
|
||||
"Package",
|
||||
"Shield",
|
||||
"BarChart",
|
||||
"PieChart",
|
||||
"List",
|
||||
"Grid",
|
||||
"Calendar",
|
||||
"Clock",
|
||||
"Map",
|
||||
"Mail",
|
||||
"MessageSquare",
|
||||
"Search",
|
||||
"Code",
|
||||
"Terminal",
|
||||
"Book",
|
||||
"Zap",
|
||||
"Star",
|
||||
"Heart",
|
||||
"Award",
|
||||
"Trophy",
|
||||
"Briefcase",
|
||||
"ShoppingCart",
|
||||
];
|
||||
|
||||
const i18nModalOpen = ref(false);
|
||||
function openI18nModal() { i18nModalOpen.value = true; }
|
||||
function openI18nModal() {
|
||||
i18nModalOpen.value = true;
|
||||
}
|
||||
function updateField(key: string, value: unknown) {
|
||||
if (!props.data) return;
|
||||
const u: Record<string, unknown> = { ...props.data, [key]: value };
|
||||
@@ -23,37 +70,140 @@ function updateField(key: string, value: unknown) {
|
||||
emit("update", u);
|
||||
}
|
||||
</script>
|
||||
<script lang="ts">import { ref } from "vue"; export default { name: "ModuleBasicPanel" };</script>
|
||||
<script lang="ts">
|
||||
import { ref } from "vue";
|
||||
export default { name: "ModuleBasicPanel" };
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-full min-h-0 overflow-y-auto space-y-5">
|
||||
<h3 class="text-[13px] font-semibold">基本信息</h3>
|
||||
<div class="grid w-[640px] max-w-full grid-cols-2 gap-x-6 gap-y-4">
|
||||
<!-- 编码 -->
|
||||
<div class="space-y-1.5"><Label class="text-[12px]">编码 <span class="text-destructive">*</span></Label><Input :model-value="String(data?.b_code ?? '')" class="h-8 text-[13px]" :disabled="loading" @update:model-value="(v: string) => updateField('b_code', v)" /></div>
|
||||
<div class="space-y-1.5">
|
||||
<Label class="text-[12px]">编码 <span class="text-destructive">*</span></Label
|
||||
><Input
|
||||
:model-value="String(data?.b_code ?? '')"
|
||||
:disabled="loading"
|
||||
@update:model-value="(v: string) => updateField('b_code', v)"
|
||||
/>
|
||||
</div>
|
||||
<!-- 名称 -->
|
||||
<div class="space-y-1.5"><Label class="text-[12px]">名称 <span class="text-destructive">*</span></Label><div class="flex items-center gap-1"><Input :model-value="String(data?.b_name ?? '')" class="h-8 flex-1 text-[13px]" :disabled="loading" @update:model-value="(v: string) => updateField('b_name', v)" /><Button variant="ghost" size="icon-xs" :disabled="loading" @click="openI18nModal"><Globe :size="14" /></Button></div></div>
|
||||
<div class="space-y-1.5">
|
||||
<Label class="text-[12px]">名称 <span class="text-destructive">*</span></Label>
|
||||
<div class="flex items-center gap-1">
|
||||
<Input
|
||||
:model-value="String(data?.b_name ?? '')"
|
||||
class="flex-1"
|
||||
:disabled="loading"
|
||||
@update:model-value="(v: string) => updateField('b_name', v)"
|
||||
/><Button variant="ghost" :disabled="loading" @click="openI18nModal"
|
||||
><Globe :size="14"
|
||||
/></Button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 类型 -->
|
||||
<div class="space-y-1.5"><Label class="text-[12px]">模块类型</Label><Select :model-value="String(data?.b_module_type ?? '')" :disabled="loading" @update:model-value="(v: string) => updateField('b_module_type', v)"><SelectTrigger class="h-8 text-[13px]"><SelectValue /></SelectTrigger><SelectContent><SelectItem v-for="o in moduleTypeOptions" :key="o.value" :value="o.value">{{ o.label }}</SelectItem></SelectContent></Select></div>
|
||||
<div class="space-y-1.5">
|
||||
<Label class="text-[12px]">模块类型</Label
|
||||
><Select
|
||||
:model-value="String(data?.b_module_type ?? '')"
|
||||
:disabled="loading"
|
||||
@update:model-value="(v: string) => updateField('b_module_type', v)"
|
||||
><SelectTrigger><SelectValue /></SelectTrigger
|
||||
><SelectContent
|
||||
><SelectItem v-for="o in moduleTypeOptions" :key="o.value" :value="o.value">{{
|
||||
o.label
|
||||
}}</SelectItem></SelectContent
|
||||
></Select
|
||||
>
|
||||
</div>
|
||||
<!-- 上级 -->
|
||||
<div class="space-y-1.5"><Label class="text-[12px]">上级模块</Label><Input :model-value="parentName ?? ''" class="h-8 text-[13px]" disabled /></div>
|
||||
<div class="space-y-1.5">
|
||||
<Label class="text-[12px]">上级模块</Label
|
||||
><Input :model-value="parentName ?? ''" disabled />
|
||||
</div>
|
||||
<!-- 排序 -->
|
||||
<div class="space-y-1.5"><Label class="text-[12px]">排序号</Label><Input :model-value="String(data?.b_xh ?? 0)" type="number" class="h-8 text-[13px]" :disabled="loading" @update:model-value="(v: string) => updateField('b_xh', Number(v))" /></div>
|
||||
<div class="space-y-1.5">
|
||||
<Label class="text-[12px]">排序号</Label
|
||||
><Input
|
||||
:model-value="String(data?.b_xh ?? 0)"
|
||||
type="number"
|
||||
:disabled="loading"
|
||||
@update:model-value="(v: string) => updateField('b_xh', Number(v))"
|
||||
/>
|
||||
</div>
|
||||
<!-- 图标 -->
|
||||
<div class="space-y-1.5"><Label class="text-[12px]">图标</Label><Select :model-value="String(data?.b_icon ?? '') || 'none'" :disabled="loading" @update:model-value="(v: string) => updateField('b_icon', v === 'none' ? '' : v)"><SelectTrigger class="h-8 text-[13px]"><SelectValue /></SelectTrigger><SelectContent><SelectItem value="none">(无)</SelectItem><SelectItem v-for="i in iconList" :key="i" :value="i">{{ i }}</SelectItem></SelectContent></Select></div>
|
||||
<div class="space-y-1.5">
|
||||
<Label class="text-[12px]">图标</Label
|
||||
><Select
|
||||
:model-value="String(data?.b_icon ?? '') || 'none'"
|
||||
:disabled="loading"
|
||||
@update:model-value="(v: string) => updateField('b_icon', v === 'none' ? '' : v)"
|
||||
><SelectTrigger><SelectValue /></SelectTrigger
|
||||
><SelectContent
|
||||
><SelectItem value="none">(无)</SelectItem
|
||||
><SelectItem v-for="i in iconList" :key="i" :value="i">{{
|
||||
i
|
||||
}}</SelectItem></SelectContent
|
||||
></Select
|
||||
>
|
||||
</div>
|
||||
<!-- 路由 -->
|
||||
<div v-if="data?.b_module_type === 'page'" class="space-y-1.5"><Label class="text-[12px]">页面路由 <span class="text-destructive">*</span></Label><Input :model-value="String(data?.b_route ?? '')" class="h-8 text-[13px]" :disabled="loading" @update:model-value="(v: string) => updateField('b_route', v)" /></div>
|
||||
<div v-if="data?.b_module_type === 'page'" class="space-y-1.5">
|
||||
<Label class="text-[12px]">页面路由 <span class="text-destructive">*</span></Label
|
||||
><Input
|
||||
:model-value="String(data?.b_route ?? '')"
|
||||
:disabled="loading"
|
||||
@update:model-value="(v: string) => updateField('b_route', v)"
|
||||
/>
|
||||
</div>
|
||||
<!-- 视图 -->
|
||||
<div v-if="data?.b_module_type === 'data'" class="space-y-1.5"><Label class="text-[12px]">查询视图 <span class="text-destructive">*</span></Label><Input :model-value="String(data?.b_viewtable ?? '')" class="h-8 text-[13px]" :disabled="loading" @update:model-value="(v: string) => updateField('b_viewtable', v)" /></div>
|
||||
<div v-if="data?.b_module_type === 'data'" class="space-y-1.5">
|
||||
<Label class="text-[12px]">查询视图 <span class="text-destructive">*</span></Label
|
||||
><Input
|
||||
:model-value="String(data?.b_viewtable ?? '')"
|
||||
:disabled="loading"
|
||||
@update:model-value="(v: string) => updateField('b_viewtable', v)"
|
||||
/>
|
||||
</div>
|
||||
<!-- 保存表 -->
|
||||
<div v-if="data?.b_module_type === 'data'" class="space-y-1.5"><Label class="text-[12px]">保存表</Label><Input :model-value="String(data?.b_savetable ?? '')" class="h-8 text-[13px]" :disabled="loading" @update:model-value="(v: string) => updateField('b_savetable', v)" /></div>
|
||||
<div v-if="data?.b_module_type === 'data'" class="space-y-1.5">
|
||||
<Label class="text-[12px]">保存表</Label
|
||||
><Input
|
||||
:model-value="String(data?.b_savetable ?? '')"
|
||||
:disabled="loading"
|
||||
@update:model-value="(v: string) => updateField('b_savetable', v)"
|
||||
/>
|
||||
</div>
|
||||
<!-- 主键 -->
|
||||
<div v-if="data?.b_module_type === 'data'" class="space-y-1.5"><Label class="text-[12px]">主键字段</Label><Input :model-value="String(data?.b_keyfield ?? '')" class="h-8 text-[13px]" :disabled="loading" @update:model-value="(v: string) => updateField('b_keyfield', v)" /></div>
|
||||
<div v-if="data?.b_module_type === 'data'" class="space-y-1.5">
|
||||
<Label class="text-[12px]">主键字段</Label
|
||||
><Input
|
||||
:model-value="String(data?.b_keyfield ?? '')"
|
||||
:disabled="loading"
|
||||
@update:model-value="(v: string) => updateField('b_keyfield', v)"
|
||||
/>
|
||||
</div>
|
||||
<!-- 排序字段 -->
|
||||
<div v-if="data?.b_module_type === 'data'" class="space-y-1.5"><Label class="text-[12px]">默认排序字段</Label><Input :model-value="String(data?.b_orderfield ?? '')" class="h-8 text-[13px]" :disabled="loading" @update:model-value="(v: string) => updateField('b_orderfield', v)" /></div>
|
||||
<div v-if="data?.b_module_type === 'data'" class="space-y-1.5">
|
||||
<Label class="text-[12px]">默认排序字段</Label
|
||||
><Input
|
||||
:model-value="String(data?.b_orderfield ?? '')"
|
||||
:disabled="loading"
|
||||
@update:model-value="(v: string) => updateField('b_orderfield', v)"
|
||||
/>
|
||||
</div>
|
||||
<!-- 多语言键 -->
|
||||
<div class="space-y-1.5"><Label class="text-[12px]">多语言键</Label><Input :model-value="String(data?.b_i18n ?? '')" class="h-8 text-[13px]" disabled /></div>
|
||||
<div class="space-y-1.5">
|
||||
<Label class="text-[12px]">多语言键</Label
|
||||
><Input :model-value="String(data?.b_i18n ?? '')" disabled />
|
||||
</div>
|
||||
</div>
|
||||
<I18nQuickSetModal v-if="data" :open="i18nModalOpen" :i18n-key="String(data.b_i18n ?? '')" :default-value="String(data.b_name || data.b_code || '')" @update:open="(v: boolean) => i18nModalOpen = v" />
|
||||
<I18nQuickSetModal
|
||||
v-if="data"
|
||||
:open="i18nModalOpen"
|
||||
:i18n-key="String(data.b_i18n ?? '')"
|
||||
:default-value="String(data.b_name || data.b_code || '')"
|
||||
@update:open="(v: boolean) => (i18nModalOpen = v)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -46,10 +46,7 @@ function toggle() {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-show="!collapsed"
|
||||
class="p-4"
|
||||
>
|
||||
<div v-show="!collapsed" class="p-4">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+192
-29
@@ -1,67 +1,230 @@
|
||||
<script setup lang="ts">
|
||||
import { h, ref } from "vue";
|
||||
import { FolderTree, ArrowRightLeft } from "@lucide/vue";
|
||||
import { computed, h, ref } from "vue";
|
||||
import { FolderTree, ArrowRightLeft, Search } from "@lucide/vue";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import FmsTable from "@/components/fms-table/FmsTable.vue";
|
||||
import FieldGroupManager from "./FieldGroupManager.vue";
|
||||
|
||||
const props = defineProps<{ moduleId: string; editConfig: Record<string, unknown>[]; listConfig: Record<string, unknown>[]; queryConfig: Record<string, unknown>[]; fields: Record<string, unknown>[]; groups: Record<string, unknown>[]; referencedGroupIds?: string[]; loading?: boolean }>();
|
||||
const emit = defineEmits<{ (e: "update", rows: Record<string, unknown>[]): void; (e: "groups-saved", groups: Record<string, unknown>[], mapping: Map<string, string>): void; (e: "sync-list", rows: Record<string, unknown>[]): void; (e: "sync-query", rows: Record<string, unknown>[]): void }>();
|
||||
const props = defineProps<{
|
||||
moduleId: string;
|
||||
editConfig: Record<string, unknown>[];
|
||||
listConfig: Record<string, unknown>[];
|
||||
queryConfig: Record<string, unknown>[];
|
||||
fields: Record<string, unknown>[];
|
||||
groups: Record<string, unknown>[];
|
||||
referencedGroupIds?: string[];
|
||||
loading?: boolean;
|
||||
}>();
|
||||
const emit = defineEmits<{
|
||||
(e: "update", rows: Record<string, unknown>[]): void;
|
||||
(e: "groups-saved", groups: Record<string, unknown>[], mapping: Map<string, string>): void;
|
||||
(e: "sync-list", rows: Record<string, unknown>[]): void;
|
||||
(e: "sync-query", rows: Record<string, unknown>[]): void;
|
||||
}>();
|
||||
|
||||
const gmOpen = ref(false);
|
||||
function fname(id: unknown) { const f = props.fields.find((x) => String(x.b_id) === String(id)); return f ? String(f.b_field || "") : String(id || ""); }
|
||||
function gname(id: unknown) { const g = props.groups.find((x) => String(x.b_id) === String(id)); return g ? String(g.b_name || g.b_i18n || "") : ""; }
|
||||
function up(idx: number, key: string, value: unknown) { const u = [...props.editConfig]; u[idx] = { ...u[idx], [key]: value }; emit("update", u); }
|
||||
function onCellChange(row: Record<string, unknown>, dataIndex: string, value: unknown) { const idx = props.editConfig.findIndex((x) => String(x.b_id) === String(row.b_id)); if (idx >= 0) up(idx, dataIndex, value); }
|
||||
|
||||
const searchFields = [
|
||||
{ value: "_field", label: "字段名" },
|
||||
{ value: "_group", label: "分组" },
|
||||
];
|
||||
const searchField = ref(searchFields[0].value);
|
||||
const searchText = ref("");
|
||||
const filteredEditConfig = computed(() => {
|
||||
if (!searchText.value.trim()) return props.editConfig;
|
||||
const kw = searchText.value.trim().toLowerCase();
|
||||
return props.editConfig.filter((r) => {
|
||||
if (searchField.value === "_field") return fname(r.b_field_id).toLowerCase().includes(kw);
|
||||
return gname(r.b_group_id).toLowerCase().includes(kw);
|
||||
});
|
||||
});
|
||||
|
||||
function fname(id: unknown) {
|
||||
const f = props.fields.find((x) => String(x.b_id) === String(id));
|
||||
return f ? String(f.b_field || "") : String(id || "");
|
||||
}
|
||||
function gname(id: unknown) {
|
||||
const g = props.groups.find((x) => String(x.b_id) === String(id));
|
||||
return g ? String(g.b_name || g.b_i18n || "") : "";
|
||||
}
|
||||
function up(idx: number, key: string, value: unknown) {
|
||||
const u = [...props.editConfig];
|
||||
u[idx] = { ...u[idx], [key]: value };
|
||||
emit("update", u);
|
||||
}
|
||||
function onCellChange(row: Record<string, unknown>, dataIndex: string, value: unknown) {
|
||||
const idx = props.editConfig.findIndex((x) => String(x.b_id) === String(row.b_id));
|
||||
if (idx >= 0) up(idx, dataIndex, value);
|
||||
}
|
||||
function syncList() {
|
||||
const e = props.editConfig.filter((r) => Number(r.b_visible) === 1);
|
||||
const lid = new Set(props.listConfig.map((r) => String(r.b_field_id)));
|
||||
const nr = e.filter((r) => !lid.has(String(r.b_field_id))).map((r) => ({ b_id: (r as any).b_id, b_module_id: props.moduleId, b_field_id: r.b_field_id, b_width: 100, b_group_id: r.b_group_id || null, b_visible: 1, b_xh: r.b_xh || 0 }));
|
||||
const nr = e
|
||||
.filter((r) => !lid.has(String(r.b_field_id)))
|
||||
.map((r) => ({
|
||||
b_id: (r as any).b_id,
|
||||
b_module_id: props.moduleId,
|
||||
b_field_id: r.b_field_id,
|
||||
b_width: 100,
|
||||
b_group_id: r.b_group_id || null,
|
||||
b_visible: 1,
|
||||
b_xh: r.b_xh || 0,
|
||||
}));
|
||||
if (nr.length) emit("sync-list", [...props.listConfig, ...nr]);
|
||||
}
|
||||
function syncQuery() {
|
||||
const e = props.editConfig.filter((r) => Number(r.b_visible) === 1);
|
||||
const qid = new Set(props.queryConfig.map((r) => String(r.b_field_id)));
|
||||
const nr = e.filter((r) => !qid.has(String(r.b_field_id))).map((r) => ({ b_id: (r as any).b_id, b_module_id: props.moduleId, b_field_id: r.b_field_id, b_component: "input", b_operator: "=", b_default_value: null, b_xh: r.b_xh || 0, b_canuse: 1 }));
|
||||
const nr = e
|
||||
.filter((r) => !qid.has(String(r.b_field_id)))
|
||||
.map((r) => ({
|
||||
b_id: (r as any).b_id,
|
||||
b_module_id: props.moduleId,
|
||||
b_field_id: r.b_field_id,
|
||||
b_component: "input",
|
||||
b_operator: "=",
|
||||
b_default_value: null,
|
||||
b_xh: r.b_xh || 0,
|
||||
b_canuse: 1,
|
||||
}));
|
||||
if (nr.length) emit("sync-query", [...props.queryConfig, ...nr]);
|
||||
}
|
||||
function onRowOrderChange(rows: Record<string, unknown>[]) { const reordered = rows.map((x, i) => ({ ...x, b_xh: (i + 1) * 10 })); emit("update", reordered); }
|
||||
function onRowOrderChange(rows: Record<string, unknown>[]) {
|
||||
const reordered = rows.map((x, i) => ({ ...x, b_xh: (i + 1) * 10 }));
|
||||
emit("update", reordered);
|
||||
}
|
||||
|
||||
function fieldCell(row: Record<string, unknown>) { return h("span", { class: "text-[13px]" }, fname(row.b_field_id)); }
|
||||
function groupCell(row: Record<string, unknown>) { return h("span", { class: "text-[13px] text-muted-foreground" }, gname(row.b_group_id)); }
|
||||
function fieldCell(row: Record<string, unknown>) {
|
||||
return h("span", { class: "text-[13px]" }, fname(row.b_field_id));
|
||||
}
|
||||
function groupCell(row: Record<string, unknown>) {
|
||||
return h("span", { class: "text-[13px] text-muted-foreground" }, gname(row.b_group_id));
|
||||
}
|
||||
|
||||
const columns = ref([
|
||||
{ dataIndex: "b_field_id", title: "字段", width: 130, customCell: ({ row }: { row: Record<string, unknown> }) => fieldCell(row) },
|
||||
{ dataIndex: "b_visible", title: "显示", width: 60, editor: { type: "checkbox", onChange: (v: number, row: Record<string, unknown>) => onCellChange(row, "b_visible", v) } },
|
||||
{ dataIndex: "b_required", title: "必填", width: 60, editor: { type: "checkbox", onChange: (v: number, row: Record<string, unknown>) => onCellChange(row, "b_required", v) } },
|
||||
{ dataIndex: "b_readonly", title: "只读", width: 60, editor: { type: "checkbox", onChange: (v: number, row: Record<string, unknown>) => onCellChange(row, "b_readonly", v) } },
|
||||
{ dataIndex: "b_disabled", title: "禁用", width: 60, editor: { type: "checkbox", onChange: (v: number, row: Record<string, unknown>) => onCellChange(row, "b_disabled", v) } },
|
||||
{ dataIndex: "b_group_id", title: "分组", width: 100, customCell: ({ row }: { row: Record<string, unknown> }) => groupCell(row) },
|
||||
{ dataIndex: "b_xh", title: "排序", width: 70, editor: { type: "input", inputType: "number", onChange: (v: string, row: Record<string, unknown>) => onCellChange(row, "b_xh", Number(v)) } },
|
||||
{
|
||||
dataIndex: "b_field_id",
|
||||
title: "字段",
|
||||
width: 130,
|
||||
customCell: ({ row }: { row: Record<string, unknown> }) => fieldCell(row),
|
||||
},
|
||||
{
|
||||
dataIndex: "b_visible",
|
||||
title: "显示",
|
||||
width: 60,
|
||||
editor: {
|
||||
type: "checkbox",
|
||||
onChange: (v: number, row: Record<string, unknown>) => onCellChange(row, "b_visible", v),
|
||||
},
|
||||
},
|
||||
{
|
||||
dataIndex: "b_required",
|
||||
title: "必填",
|
||||
width: 60,
|
||||
editor: {
|
||||
type: "checkbox",
|
||||
onChange: (v: number, row: Record<string, unknown>) => onCellChange(row, "b_required", v),
|
||||
},
|
||||
},
|
||||
{
|
||||
dataIndex: "b_readonly",
|
||||
title: "只读",
|
||||
width: 60,
|
||||
editor: {
|
||||
type: "checkbox",
|
||||
onChange: (v: number, row: Record<string, unknown>) => onCellChange(row, "b_readonly", v),
|
||||
},
|
||||
},
|
||||
{
|
||||
dataIndex: "b_disabled",
|
||||
title: "禁用",
|
||||
width: 60,
|
||||
editor: {
|
||||
type: "checkbox",
|
||||
onChange: (v: number, row: Record<string, unknown>) => onCellChange(row, "b_disabled", v),
|
||||
},
|
||||
},
|
||||
{
|
||||
dataIndex: "b_group_id",
|
||||
title: "分组",
|
||||
width: 100,
|
||||
customCell: ({ row }: { row: Record<string, unknown> }) => groupCell(row),
|
||||
},
|
||||
{
|
||||
dataIndex: "b_xh",
|
||||
title: "排序",
|
||||
width: 70,
|
||||
editor: {
|
||||
type: "input",
|
||||
inputType: "number",
|
||||
onChange: (v: string, row: Record<string, unknown>) => onCellChange(row, "b_xh", Number(v)),
|
||||
},
|
||||
},
|
||||
]);
|
||||
</script>
|
||||
<script lang="ts">export default { name: "ModuleEditConfigPanel" };</script>
|
||||
<script lang="ts">
|
||||
export default { name: "ModuleEditConfigPanel" };
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-[13px] font-semibold">编辑配置</h3>
|
||||
<div class="flex h-full min-h-0 flex-col gap-4">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div class="flex min-w-0 flex-1 items-center rounded-md border border-input shadow-sm">
|
||||
<Select v-model="searchField">
|
||||
<SelectTrigger class="w-[110px] shrink-0 border-0 shadow-none rounded-r-none focus:ring-0"
|
||||
><SelectValue
|
||||
/></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem v-for="opt in searchFields" :key="opt.value" :value="opt.value">{{
|
||||
opt.label
|
||||
}}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div class="w-px self-stretch bg-border" />
|
||||
<Input
|
||||
v-model="searchText"
|
||||
class="flex-1 border-0 shadow-none rounded-none focus-visible:ring-0"
|
||||
placeholder="搜索..."
|
||||
/>
|
||||
<Button variant="ghost" size="icon" class="rounded-l-none"><Search :size="16" /></Button>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Button variant="outline" size="sm" class="h-7 text-[12px]" :disabled="loading" @click="gmOpen = true"><FolderTree :size="12" class="mr-1" />分组</Button>
|
||||
<Button variant="outline" size="sm" class="h-7 text-[12px]" :disabled="loading" @click="syncList"><ArrowRightLeft :size="12" class="mr-1" />同步列表</Button>
|
||||
<Button variant="outline" size="sm" class="h-7 text-[12px]" :disabled="loading" @click="syncQuery"><ArrowRightLeft :size="12" class="mr-1" />同步查询</Button>
|
||||
<Button variant="outline" :disabled="loading" @click="gmOpen = true"
|
||||
><FolderTree :size="12" class="mr-1" />分组</Button
|
||||
>
|
||||
<Button variant="outline" :disabled="loading" @click="syncList"
|
||||
><ArrowRightLeft :size="12" class="mr-1" />同步列表</Button
|
||||
>
|
||||
<Button variant="outline" :disabled="loading" @click="syncQuery"
|
||||
><ArrowRightLeft :size="12" class="mr-1" />同步查询</Button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<FmsTable
|
||||
class="min-h-0 flex-1"
|
||||
v-model:columns="columns"
|
||||
:data-source="editConfig"
|
||||
:data-source="filteredEditConfig"
|
||||
row-key="b_id"
|
||||
:editable="!loading"
|
||||
:virtual="false"
|
||||
:row-draggable="!loading"
|
||||
@row-order-change="onRowOrderChange"
|
||||
/>
|
||||
<FieldGroupManager :open="gmOpen" :module-id="moduleId" :groups="groups" :referenced-group-ids="referencedGroupIds" @update:open="(v: boolean) => gmOpen = v" @saved="(g, m) => emit('groups-saved', g, m)" />
|
||||
<FieldGroupManager
|
||||
:open="gmOpen"
|
||||
:module-id="moduleId"
|
||||
:groups="groups"
|
||||
:referenced-group-ids="referencedGroupIds"
|
||||
@update:open="(v: boolean) => (gmOpen = v)"
|
||||
@saved="(g, m) => emit('groups-saved', g, m)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
+155
-27
@@ -1,19 +1,61 @@
|
||||
<script setup lang="ts">
|
||||
import { h, ref } from "vue";
|
||||
import { Globe } from "@lucide/vue";
|
||||
import { computed, h, ref } from "vue";
|
||||
import { Globe, Search } from "@lucide/vue";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import FmsTable from "@/components/fms-table/FmsTable.vue";
|
||||
import I18nQuickSetModal from "./I18nQuickSetModal.vue";
|
||||
import { buildModuleItemI18nKey } from "@/utils/i18n";
|
||||
|
||||
const props = defineProps<{ moduleId: string; moduleCode: string; fields: Record<string, unknown>[]; loading?: boolean }>();
|
||||
const emit = defineEmits<{ (e: "update", fields: Record<string, unknown>[]): void; (e: "sync-fields"): void }>();
|
||||
const props = defineProps<{
|
||||
moduleId: string;
|
||||
moduleCode: string;
|
||||
fields: Record<string, unknown>[];
|
||||
loading?: boolean;
|
||||
}>();
|
||||
const emit = defineEmits<{
|
||||
(e: "update", fields: Record<string, unknown>[]): void;
|
||||
(e: "sync-fields"): void;
|
||||
}>();
|
||||
|
||||
const i18nModalOpen = ref(false); const i18nTargetKey = ref(""); const i18nDefaultName = ref("");
|
||||
function openI18nModal(f: Record<string, unknown>) { i18nTargetKey.value = String(f.b_i18n || ""); i18nDefaultName.value = String(f.b_name || f.b_field || ""); i18nModalOpen.value = true; }
|
||||
const i18nModalOpen = ref(false);
|
||||
const i18nTargetKey = ref("");
|
||||
const i18nDefaultName = ref("");
|
||||
|
||||
const searchFields = [
|
||||
{ value: "b_field", label: "字段名" },
|
||||
{ value: "b_name", label: "字段名称" },
|
||||
];
|
||||
const searchField = ref(searchFields[0].value);
|
||||
const searchText = ref("");
|
||||
const filteredFields = computed(() => {
|
||||
if (!searchText.value.trim()) return props.fields;
|
||||
const kw = searchText.value.trim().toLowerCase();
|
||||
const sf = searchField.value;
|
||||
return props.fields.filter((f) =>
|
||||
String(f[sf] ?? "")
|
||||
.toLowerCase()
|
||||
.includes(kw),
|
||||
);
|
||||
});
|
||||
|
||||
function openI18nModal(f: Record<string, unknown>) {
|
||||
i18nTargetKey.value = String(f.b_i18n || "");
|
||||
i18nDefaultName.value = String(f.b_name || f.b_field || "");
|
||||
i18nModalOpen.value = true;
|
||||
}
|
||||
function updateField(idx: number, key: string, value: unknown) {
|
||||
const u = [...props.fields]; u[idx] = { ...u[idx], [key]: value };
|
||||
if (key === "b_field") u[idx].b_i18n = buildModuleItemI18nKey("field", props.moduleCode, String(value));
|
||||
const u = [...props.fields];
|
||||
u[idx] = { ...u[idx], [key]: value };
|
||||
if (key === "b_field")
|
||||
u[idx].b_i18n = buildModuleItemI18nKey("field", props.moduleCode, String(value));
|
||||
emit("update", u);
|
||||
}
|
||||
function onCellChange(row: Record<string, unknown>, dataIndex: string, value: unknown) {
|
||||
@@ -21,44 +63,130 @@ function onCellChange(row: Record<string, unknown>, dataIndex: string, value: un
|
||||
if (idx >= 0) updateField(idx, dataIndex, value);
|
||||
}
|
||||
|
||||
const CONTROL_TYPES = [{ value: "input", label: "文本" },{ value: "textarea", label: "多行文本" },{ value: "number", label: "数字" },{ value: "money", label: "金额" },{ value: "date", label: "日期" },{ value: "datetime", label: "日期时间" },{ value: "checkbox", label: "复选框" },{ value: "select", label: "下拉选择" }];
|
||||
const CONTROL_TYPES = [
|
||||
{ value: "input", label: "文本" },
|
||||
{ value: "textarea", label: "多行文本" },
|
||||
{ value: "number", label: "数字" },
|
||||
{ value: "money", label: "金额" },
|
||||
{ value: "date", label: "日期" },
|
||||
{ value: "datetime", label: "日期时间" },
|
||||
{ value: "checkbox", label: "复选框" },
|
||||
{ value: "select", label: "下拉选择" },
|
||||
];
|
||||
|
||||
function i18nCell(row: Record<string, unknown>) {
|
||||
return h("div", { class: "flex items-center gap-1" }, [
|
||||
h("span", { class: "flex-1 truncate text-[12px] text-muted-foreground" }, String(row.b_i18n ?? "")),
|
||||
h("button", {
|
||||
type: "button",
|
||||
class: "inline-flex h-6 w-6 shrink-0 items-center justify-center rounded text-muted-foreground hover:bg-accent hover:text-foreground",
|
||||
title: "设置多语言",
|
||||
onClick: (e: MouseEvent) => { e.stopPropagation(); openI18nModal(row); },
|
||||
}, [h(Globe, { size: 12 })]),
|
||||
h(
|
||||
"span",
|
||||
{ class: "flex-1 truncate text-[12px] text-muted-foreground" },
|
||||
String(row.b_i18n ?? ""),
|
||||
),
|
||||
h(
|
||||
"button",
|
||||
{
|
||||
type: "button",
|
||||
class:
|
||||
"inline-flex h-6 w-6 shrink-0 items-center justify-center rounded text-muted-foreground hover:bg-accent hover:text-foreground",
|
||||
title: "设置多语言",
|
||||
onClick: (e: MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
openI18nModal(row);
|
||||
},
|
||||
},
|
||||
[h(Globe, { size: 12 })],
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
const columns = ref([
|
||||
{ dataIndex: "b_field", title: "字段名", width: 130 },
|
||||
{ dataIndex: "b_name", title: "字段名称", width: 120, editor: { type: "input", onChange: (v: string, row: Record<string, unknown>) => onCellChange(row, "b_name", v) } },
|
||||
{ dataIndex: "b_type", title: "控件类型", width: 110, editor: { type: "select", options: CONTROL_TYPES, allowClear: false, onChange: (v: string, row: Record<string, unknown>) => onCellChange(row, "b_type", v) } },
|
||||
{ dataIndex: "b_default_value", title: "默认值", width: 110, editor: { type: "input", onChange: (v: string, row: Record<string, unknown>) => onCellChange(row, "b_default_value", v) } },
|
||||
{ dataIndex: "b_i18n", title: "多语言键", width: 170, customCell: ({ row }: { row: Record<string, unknown> }) => i18nCell(row) },
|
||||
{ dataIndex: "b_canuse", title: "启用", width: 60, editor: { type: "checkbox", onChange: (v: number, row: Record<string, unknown>) => onCellChange(row, "b_canuse", v) } },
|
||||
{
|
||||
dataIndex: "b_name",
|
||||
title: "字段名称",
|
||||
width: 120,
|
||||
editor: {
|
||||
type: "input",
|
||||
onChange: (v: string, row: Record<string, unknown>) => onCellChange(row, "b_name", v),
|
||||
},
|
||||
},
|
||||
{
|
||||
dataIndex: "b_type",
|
||||
title: "控件类型",
|
||||
width: 110,
|
||||
editor: {
|
||||
type: "select",
|
||||
options: CONTROL_TYPES,
|
||||
allowClear: false,
|
||||
onChange: (v: string, row: Record<string, unknown>) => onCellChange(row, "b_type", v),
|
||||
},
|
||||
},
|
||||
{
|
||||
dataIndex: "b_default_value",
|
||||
title: "默认值",
|
||||
width: 110,
|
||||
editor: {
|
||||
type: "input",
|
||||
onChange: (v: string, row: Record<string, unknown>) =>
|
||||
onCellChange(row, "b_default_value", v),
|
||||
},
|
||||
},
|
||||
{
|
||||
dataIndex: "b_i18n",
|
||||
title: "多语言键",
|
||||
width: 170,
|
||||
customCell: ({ row }: { row: Record<string, unknown> }) => i18nCell(row),
|
||||
},
|
||||
{
|
||||
dataIndex: "b_canuse",
|
||||
title: "启用",
|
||||
width: 60,
|
||||
editor: {
|
||||
type: "checkbox",
|
||||
onChange: (v: number, row: Record<string, unknown>) => onCellChange(row, "b_canuse", v),
|
||||
},
|
||||
},
|
||||
]);
|
||||
</script>
|
||||
<script lang="ts">export default { name: "ModuleFieldsPanel" };</script>
|
||||
<script lang="ts">
|
||||
export default { name: "ModuleFieldsPanel" };
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex h-full min-h-0 flex-col gap-4">
|
||||
<div class="flex shrink-0 items-center justify-between">
|
||||
<h3 class="text-[13px] font-semibold">字段定义</h3>
|
||||
<Button variant="outline" size="sm" class="h-7 text-[12px]" :disabled="loading" @click="emit('sync-fields')">同步字段</Button>
|
||||
<div class="flex shrink-0 items-center justify-between gap-3">
|
||||
<div class="flex min-w-0 flex-1 items-center rounded-md border border-input shadow-sm">
|
||||
<Select v-model="searchField">
|
||||
<SelectTrigger class="w-[110px] shrink-0 border-0 shadow-none rounded-r-none focus:ring-0"
|
||||
><SelectValue
|
||||
/></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem v-for="opt in searchFields" :key="opt.value" :value="opt.value">{{
|
||||
opt.label
|
||||
}}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div class="w-px self-stretch bg-border" />
|
||||
<Input
|
||||
v-model="searchText"
|
||||
class="flex-1 border-0 shadow-none rounded-none focus-visible:ring-0"
|
||||
placeholder="搜索..."
|
||||
/>
|
||||
<Button variant="ghost" size="icon" class="rounded-l-none"><Search :size="16" /></Button>
|
||||
</div>
|
||||
<Button variant="outline" :disabled="loading" @click="emit('sync-fields')">同步字段</Button>
|
||||
</div>
|
||||
<FmsTable
|
||||
v-model:columns="columns"
|
||||
:data-source="fields"
|
||||
:data-source="filteredFields"
|
||||
row-key="b_id"
|
||||
:editable="!loading"
|
||||
:virtual="false"
|
||||
/>
|
||||
<I18nQuickSetModal :open="i18nModalOpen" :i18n-key="i18nTargetKey" :default-value="i18nDefaultName" @update:open="(v: boolean) => i18nModalOpen = v" />
|
||||
<I18nQuickSetModal
|
||||
:open="i18nModalOpen"
|
||||
:i18n-key="i18nTargetKey"
|
||||
:default-value="i18nDefaultName"
|
||||
@update:open="(v: boolean) => (i18nModalOpen = v)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
+79
-24
@@ -3,6 +3,13 @@ import { ref, watch, computed, h } from "vue";
|
||||
import { Plus, Trash2, Search } from "@lucide/vue";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import FmsTable from "@/components/fms-table/FmsTable.vue";
|
||||
import { loadDataApi } from "@/services/api";
|
||||
import { cloneData } from "@/utils/dataChanges";
|
||||
@@ -13,13 +20,31 @@ const locales = ref<string[]>([]);
|
||||
const dirty = ref(false);
|
||||
const savedSnapshot = ref<I18nRow[]>([]);
|
||||
const pivotRows = ref<I18nRow[]>([]);
|
||||
|
||||
const searchFields = [
|
||||
{ value: "b_key", label: "翻译键" },
|
||||
{ value: "_type", label: "类型" },
|
||||
{ value: "_value", label: "翻译内容" },
|
||||
];
|
||||
const searchField = ref(searchFields[0].value);
|
||||
const search = ref("");
|
||||
const selectedRowKeys = ref<string[]>([]);
|
||||
|
||||
const filteredRows = computed(() => {
|
||||
const kw = search.value.trim().toLowerCase();
|
||||
if (!kw) return pivotRows.value;
|
||||
return pivotRows.value.filter((r) => String(r.b_key).toLowerCase().includes(kw));
|
||||
return pivotRows.value.filter((r) => {
|
||||
if (searchField.value === "b_key") return String(r.b_key).toLowerCase().includes(kw);
|
||||
if (searchField.value === "_type") {
|
||||
const t = isCtrl(r) ? "系统" : "自定义";
|
||||
return t.includes(kw);
|
||||
}
|
||||
return Object.values(r).some((v) =>
|
||||
String(v ?? "")
|
||||
.toLowerCase()
|
||||
.includes(kw),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
function isCtrl(row: I18nRow) {
|
||||
@@ -138,11 +163,13 @@ async function buildSaveRequest() {
|
||||
const k = `${fl.b_key}:${fl.b_locale}`;
|
||||
const ex = exMap.get(k);
|
||||
if (!ex) inserts.push(fl);
|
||||
else if (String(ex.b_value) !== String(fl.b_value)) updates.push({ ...ex, b_value: fl.b_value });
|
||||
else if (String(ex.b_value) !== String(fl.b_value))
|
||||
updates.push({ ...ex, b_value: fl.b_value });
|
||||
}
|
||||
const curKL = new Set(flat.map((r) => `${r.b_key}:${r.b_locale}`));
|
||||
for (const ex of existing) {
|
||||
if (!curKL.has(`${ex.b_key}:${ex.b_locale}`)) deletes.push({ b_key: ex.b_key, b_locale: ex.b_locale });
|
||||
if (!curKL.has(`${ex.b_key}:${ex.b_locale}`))
|
||||
deletes.push({ b_key: ex.b_key, b_locale: ex.b_locale });
|
||||
}
|
||||
if (!inserts.length && !updates.length && !deletes.length) return null;
|
||||
const req: Record<string, unknown> = { table: "b_i18n", key_field: "b_key" };
|
||||
@@ -156,9 +183,16 @@ function markSaved() {
|
||||
savedSnapshot.value = cloneData(pivotRows.value);
|
||||
dirty.value = false;
|
||||
}
|
||||
function mergeDefaults(entries: { key: string; locale: string; value: unknown; managedId?: string }[]) {
|
||||
function mergeDefaults(
|
||||
entries: { key: string; locale: string; value: unknown; managedId?: string }[],
|
||||
) {
|
||||
const byKey = new Map(pivotRows.value.map((r) => [String(r.b_key), r]));
|
||||
const existM = new Set(pivotRows.value.map((r) => r._managedId).filter(Boolean).map(String));
|
||||
const existM = new Set(
|
||||
pivotRows.value
|
||||
.map((r) => r._managedId)
|
||||
.filter(Boolean)
|
||||
.map(String),
|
||||
);
|
||||
for (const e of entries) {
|
||||
if (!e.key) continue;
|
||||
const ex = byKey.get(e.key);
|
||||
@@ -200,10 +234,21 @@ function removeByManagedIds(managedIds: Set<string>) {
|
||||
pivotRows.value = pivotRows.value.filter((r) => !r._managedId || !managedIds.has(r._managedId));
|
||||
dirty.value = true;
|
||||
}
|
||||
defineExpose({ reconcileManagedRows, buildSaveRequest, markSaved, mergeDefaults, refreshKey, removeByManagedIds });
|
||||
defineExpose({
|
||||
reconcileManagedRows,
|
||||
buildSaveRequest,
|
||||
markSaved,
|
||||
mergeDefaults,
|
||||
refreshKey,
|
||||
removeByManagedIds,
|
||||
});
|
||||
|
||||
function addNewRow() {
|
||||
pivotRows.value.push({ _id: "i18n_" + Math.random().toString(36).slice(2, 10), b_key: "", _localeIds: {} });
|
||||
pivotRows.value.push({
|
||||
_id: "i18n_" + Math.random().toString(36).slice(2, 10),
|
||||
b_key: "",
|
||||
_localeIds: {},
|
||||
});
|
||||
dirty.value = true;
|
||||
}
|
||||
function delSel() {
|
||||
@@ -215,7 +260,11 @@ function delSel() {
|
||||
|
||||
async function loadLocales() {
|
||||
try {
|
||||
const r = await loadDataApi("b_i18n_type", "b_canuse = 1", "b_default DESC, b_xh ASC, b_id ASC");
|
||||
const r = await loadDataApi(
|
||||
"b_i18n_type",
|
||||
"b_canuse = 1",
|
||||
"b_default DESC, b_xh ASC, b_id ASC",
|
||||
);
|
||||
locales.value = ((r.data || []) as { b_code: string }[]).map((l) => l.b_code);
|
||||
} catch {
|
||||
locales.value = [];
|
||||
@@ -234,29 +283,35 @@ export default { name: "ModuleI18nPanel" };
|
||||
|
||||
<template>
|
||||
<div class="flex h-full min-h-0 flex-col gap-4">
|
||||
<div class="flex shrink-0 items-center justify-between">
|
||||
<h3 class="text-[13px] font-semibold">多语言</h3>
|
||||
<div class="flex shrink-0 items-center justify-between gap-3">
|
||||
<div class="flex min-w-0 flex-1 items-center rounded-md border border-input shadow-sm">
|
||||
<Select v-model="searchField">
|
||||
<SelectTrigger class="w-[110px] shrink-0 border-0 shadow-none rounded-r-none focus:ring-0"
|
||||
><SelectValue
|
||||
/></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem v-for="opt in searchFields" :key="opt.value" :value="opt.value">{{
|
||||
opt.label
|
||||
}}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div class="w-px self-stretch bg-border" />
|
||||
<Input
|
||||
v-model="search"
|
||||
class="flex-1 border-0 shadow-none rounded-none focus-visible:ring-0"
|
||||
placeholder="搜索..."
|
||||
/>
|
||||
<Button variant="ghost" size="icon" class="rounded-l-none"><Search :size="16" /></Button>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Button
|
||||
v-if="selectedRowKeys.length"
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
class="h-7 text-[12px]"
|
||||
@click="delSel"
|
||||
>
|
||||
<Button v-if="selectedRowKeys.length" variant="destructive" @click="delSel">
|
||||
<Trash2 :size="12" class="mr-1" />批量删除
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" class="h-7 text-[12px]" :disabled="loading" @click="addNewRow">
|
||||
<Button variant="outline" :disabled="loading" @click="addNewRow">
|
||||
<Plus :size="12" class="mr-1" />新增翻译
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3 shrink-0">
|
||||
<div class="relative w-60">
|
||||
<Search :size="14" class="absolute left-2.5 top-1/2 -translate-y-1/2 text-muted-foreground" />
|
||||
<Input v-model="search" class="h-8 pl-8 text-[13px]" placeholder="搜索翻译键…" />
|
||||
</div>
|
||||
</div>
|
||||
<FmsTable
|
||||
class="min-h-0 flex-1"
|
||||
v-model:columns="columns"
|
||||
|
||||
+177
-26
@@ -1,65 +1,216 @@
|
||||
<script setup lang="ts">
|
||||
import { h, ref } from "vue";
|
||||
import { FolderTree, ArrowRightLeft } from "@lucide/vue";
|
||||
import { computed, h, ref } from "vue";
|
||||
import { FolderTree, ArrowRightLeft, Search } from "@lucide/vue";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import FmsTable from "@/components/fms-table/FmsTable.vue";
|
||||
import FieldGroupManager from "./FieldGroupManager.vue";
|
||||
|
||||
const props = defineProps<{ moduleId: string; listConfig: Record<string, unknown>[]; editConfig: Record<string, unknown>[]; queryConfig: Record<string, unknown>[]; fields: Record<string, unknown>[]; groups: Record<string, unknown>[]; referencedGroupIds?: string[]; loading?: boolean }>();
|
||||
const emit = defineEmits<{ (e: "update", rows: Record<string, unknown>[]): void; (e: "groups-saved", groups: Record<string, unknown>[], mapping: Map<string, string>): void; (e: "sync-edit", rows: Record<string, unknown>[]): void; (e: "sync-query", rows: Record<string, unknown>[]): void }>();
|
||||
const props = defineProps<{
|
||||
moduleId: string;
|
||||
listConfig: Record<string, unknown>[];
|
||||
editConfig: Record<string, unknown>[];
|
||||
queryConfig: Record<string, unknown>[];
|
||||
fields: Record<string, unknown>[];
|
||||
groups: Record<string, unknown>[];
|
||||
referencedGroupIds?: string[];
|
||||
loading?: boolean;
|
||||
}>();
|
||||
const emit = defineEmits<{
|
||||
(e: "update", rows: Record<string, unknown>[]): void;
|
||||
(e: "groups-saved", groups: Record<string, unknown>[], mapping: Map<string, string>): void;
|
||||
(e: "sync-edit", rows: Record<string, unknown>[]): void;
|
||||
(e: "sync-query", rows: Record<string, unknown>[]): void;
|
||||
}>();
|
||||
|
||||
const gmOpen = ref(false);
|
||||
function fname(id: unknown) { const f = props.fields.find((x) => String(x.b_id) === String(id)); return f ? String(f.b_field || "") : String(id || ""); }
|
||||
function gname(id: unknown) { const g = props.groups.find((x) => String(x.b_id) === String(id)); return g ? String(g.b_name || g.b_i18n || "") : ""; }
|
||||
function up(idx: number, key: string, value: unknown) { const u = [...props.listConfig]; u[idx] = { ...u[idx], [key]: value }; emit("update", u); }
|
||||
function onCellChange(row: Record<string, unknown>, dataIndex: string, value: unknown) { const idx = props.listConfig.findIndex((x) => String(x.b_id) === String(row.b_id)); if (idx >= 0) up(idx, dataIndex, value); }
|
||||
|
||||
const searchFields = [
|
||||
{ value: "_field", label: "字段名" },
|
||||
{ value: "_group", label: "分组" },
|
||||
];
|
||||
const searchField = ref(searchFields[0].value);
|
||||
const searchText = ref("");
|
||||
const filteredListConfig = computed(() => {
|
||||
if (!searchText.value.trim()) return props.listConfig;
|
||||
const kw = searchText.value.trim().toLowerCase();
|
||||
return props.listConfig.filter((r) => {
|
||||
if (searchField.value === "_field") return fname(r.b_field_id).toLowerCase().includes(kw);
|
||||
return gname(r.b_group_id).toLowerCase().includes(kw);
|
||||
});
|
||||
});
|
||||
|
||||
function fname(id: unknown) {
|
||||
const f = props.fields.find((x) => String(x.b_id) === String(id));
|
||||
return f ? String(f.b_field || "") : String(id || "");
|
||||
}
|
||||
function gname(id: unknown) {
|
||||
const g = props.groups.find((x) => String(x.b_id) === String(id));
|
||||
return g ? String(g.b_name || g.b_i18n || "") : "";
|
||||
}
|
||||
function up(idx: number, key: string, value: unknown) {
|
||||
const u = [...props.listConfig];
|
||||
u[idx] = { ...u[idx], [key]: value };
|
||||
emit("update", u);
|
||||
}
|
||||
function onCellChange(row: Record<string, unknown>, dataIndex: string, value: unknown) {
|
||||
const idx = props.listConfig.findIndex((x) => String(x.b_id) === String(row.b_id));
|
||||
if (idx >= 0) up(idx, dataIndex, value);
|
||||
}
|
||||
function syncEdit() {
|
||||
const e = props.listConfig.filter((r) => Number(r.b_visible) === 1);
|
||||
const eid = new Set(props.editConfig.map((r) => String(r.b_field_id)));
|
||||
const nr = e.filter((r) => !eid.has(String(r.b_field_id))).map((r) => ({ b_id: (r as any).b_id, b_module_id: props.moduleId, b_field_id: r.b_field_id, b_required: 0, b_readonly: 0, b_disabled: 0, b_visible: 1, b_group_id: r.b_group_id || null, b_xh: r.b_xh || 0 }));
|
||||
const nr = e
|
||||
.filter((r) => !eid.has(String(r.b_field_id)))
|
||||
.map((r) => ({
|
||||
b_id: (r as any).b_id,
|
||||
b_module_id: props.moduleId,
|
||||
b_field_id: r.b_field_id,
|
||||
b_required: 0,
|
||||
b_readonly: 0,
|
||||
b_disabled: 0,
|
||||
b_visible: 1,
|
||||
b_group_id: r.b_group_id || null,
|
||||
b_xh: r.b_xh || 0,
|
||||
}));
|
||||
if (nr.length) emit("sync-edit", [...props.editConfig, ...nr]);
|
||||
}
|
||||
function syncQuery() {
|
||||
const e = props.listConfig.filter((r) => Number(r.b_visible) === 1);
|
||||
const qid = new Set(props.queryConfig.map((r) => String(r.b_field_id)));
|
||||
const nr = e.filter((r) => !qid.has(String(r.b_field_id))).map((r) => ({ b_id: (r as any).b_id, b_module_id: props.moduleId, b_field_id: r.b_field_id, b_component: "input", b_operator: "=", b_default_value: null, b_xh: r.b_xh || 0, b_canuse: 1 }));
|
||||
const nr = e
|
||||
.filter((r) => !qid.has(String(r.b_field_id)))
|
||||
.map((r) => ({
|
||||
b_id: (r as any).b_id,
|
||||
b_module_id: props.moduleId,
|
||||
b_field_id: r.b_field_id,
|
||||
b_component: "input",
|
||||
b_operator: "=",
|
||||
b_default_value: null,
|
||||
b_xh: r.b_xh || 0,
|
||||
b_canuse: 1,
|
||||
}));
|
||||
if (nr.length) emit("sync-query", [...props.queryConfig, ...nr]);
|
||||
}
|
||||
function onRowOrderChange(rows: Record<string, unknown>[]) { const reordered = rows.map((x, i) => ({ ...x, b_xh: (i + 1) * 10 })); emit("update", reordered); }
|
||||
function onRowOrderChange(rows: Record<string, unknown>[]) {
|
||||
const reordered = rows.map((x, i) => ({ ...x, b_xh: (i + 1) * 10 }));
|
||||
emit("update", reordered);
|
||||
}
|
||||
|
||||
function fieldCell(row: Record<string, unknown>) { return h("span", { class: "text-[13px]" }, fname(row.b_field_id)); }
|
||||
function groupCell(row: Record<string, unknown>) { return h("span", { class: "text-[13px] text-muted-foreground" }, gname(row.b_group_id)); }
|
||||
function fieldCell(row: Record<string, unknown>) {
|
||||
return h("span", { class: "text-[13px]" }, fname(row.b_field_id));
|
||||
}
|
||||
function groupCell(row: Record<string, unknown>) {
|
||||
return h("span", { class: "text-[13px] text-muted-foreground" }, gname(row.b_group_id));
|
||||
}
|
||||
|
||||
const columns = ref([
|
||||
{ dataIndex: "b_field_id", title: "字段", width: 130, customCell: ({ row }: { row: Record<string, unknown> }) => fieldCell(row) },
|
||||
{ dataIndex: "b_width", title: "列宽", width: 90, editor: { type: "input", inputType: "number", onChange: (v: string, row: Record<string, unknown>) => onCellChange(row, "b_width", Number(v)) } },
|
||||
{ dataIndex: "b_group_id", title: "分组", width: 100, customCell: ({ row }: { row: Record<string, unknown> }) => groupCell(row) },
|
||||
{ dataIndex: "b_visible", title: "显示", width: 70, editor: { type: "checkbox", onChange: (v: number, row: Record<string, unknown>) => onCellChange(row, "b_visible", v) } },
|
||||
{ dataIndex: "b_xh", title: "排序", width: 70, editor: { type: "input", inputType: "number", onChange: (v: string, row: Record<string, unknown>) => onCellChange(row, "b_xh", Number(v)) } },
|
||||
{
|
||||
dataIndex: "b_field_id",
|
||||
title: "字段",
|
||||
width: 130,
|
||||
customCell: ({ row }: { row: Record<string, unknown> }) => fieldCell(row),
|
||||
},
|
||||
{
|
||||
dataIndex: "b_width",
|
||||
title: "列宽",
|
||||
width: 90,
|
||||
editor: {
|
||||
type: "input",
|
||||
inputType: "number",
|
||||
onChange: (v: string, row: Record<string, unknown>) =>
|
||||
onCellChange(row, "b_width", Number(v)),
|
||||
},
|
||||
},
|
||||
{
|
||||
dataIndex: "b_group_id",
|
||||
title: "分组",
|
||||
width: 100,
|
||||
customCell: ({ row }: { row: Record<string, unknown> }) => groupCell(row),
|
||||
},
|
||||
{
|
||||
dataIndex: "b_visible",
|
||||
title: "显示",
|
||||
width: 70,
|
||||
editor: {
|
||||
type: "checkbox",
|
||||
onChange: (v: number, row: Record<string, unknown>) => onCellChange(row, "b_visible", v),
|
||||
},
|
||||
},
|
||||
{
|
||||
dataIndex: "b_xh",
|
||||
title: "排序",
|
||||
width: 70,
|
||||
editor: {
|
||||
type: "input",
|
||||
inputType: "number",
|
||||
onChange: (v: string, row: Record<string, unknown>) => onCellChange(row, "b_xh", Number(v)),
|
||||
},
|
||||
},
|
||||
]);
|
||||
</script>
|
||||
<script lang="ts">export default { name: "ModuleListConfigPanel" };</script>
|
||||
<script lang="ts">
|
||||
export default { name: "ModuleListConfigPanel" };
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex h-full min-h-0 flex-col gap-4">
|
||||
<div class="flex shrink-0 items-center justify-between">
|
||||
<h3 class="text-[13px] font-semibold">列表配置</h3>
|
||||
<div class="flex shrink-0 items-center justify-between gap-3">
|
||||
<div class="flex min-w-0 flex-1 items-center rounded-md border border-input shadow-sm">
|
||||
<Select v-model="searchField">
|
||||
<SelectTrigger class="w-[110px] shrink-0 border-0 shadow-none rounded-r-none focus:ring-0"
|
||||
><SelectValue
|
||||
/></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem v-for="opt in searchFields" :key="opt.value" :value="opt.value">{{
|
||||
opt.label
|
||||
}}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div class="w-px self-stretch bg-border" />
|
||||
<Input
|
||||
v-model="searchText"
|
||||
class="flex-1 border-0 shadow-none rounded-none focus-visible:ring-0"
|
||||
placeholder="搜索..."
|
||||
/>
|
||||
<Button variant="ghost" size="icon" class="rounded-l-none"><Search :size="16" /></Button>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Button variant="outline" size="sm" class="h-7 text-[12px]" :disabled="loading" @click="gmOpen = true"><FolderTree :size="12" class="mr-1" />分组</Button>
|
||||
<Button variant="outline" size="sm" class="h-7 text-[12px]" :disabled="loading" @click="syncEdit"><ArrowRightLeft :size="12" class="mr-1" />同步编辑</Button>
|
||||
<Button variant="outline" size="sm" class="h-7 text-[12px]" :disabled="loading" @click="syncQuery"><ArrowRightLeft :size="12" class="mr-1" />同步查询</Button>
|
||||
<Button variant="outline" :disabled="loading" @click="gmOpen = true"
|
||||
><FolderTree :size="12" class="mr-1" />分组</Button
|
||||
>
|
||||
<Button variant="outline" :disabled="loading" @click="syncEdit"
|
||||
><ArrowRightLeft :size="12" class="mr-1" />同步编辑</Button
|
||||
>
|
||||
<Button variant="outline" :disabled="loading" @click="syncQuery"
|
||||
><ArrowRightLeft :size="12" class="mr-1" />同步查询</Button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<FmsTable
|
||||
class="min-h-0 flex-1"
|
||||
v-model:columns="columns"
|
||||
:data-source="listConfig"
|
||||
:data-source="filteredListConfig"
|
||||
row-key="b_id"
|
||||
:editable="!loading"
|
||||
:virtual="false"
|
||||
:row-draggable="!loading"
|
||||
@row-order-change="onRowOrderChange"
|
||||
/>
|
||||
<FieldGroupManager :open="gmOpen" :module-id="moduleId" :groups="groups" :referenced-group-ids="referencedGroupIds" @update:open="(v: boolean) => gmOpen = v" @saved="(g, m) => emit('groups-saved', g, m)" />
|
||||
<FieldGroupManager
|
||||
:open="gmOpen"
|
||||
:module-id="moduleId"
|
||||
:groups="groups"
|
||||
:referenced-group-ids="referencedGroupIds"
|
||||
@update:open="(v: boolean) => (gmOpen = v)"
|
||||
@saved="(g, m) => emit('groups-saved', g, m)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
+187
-31
@@ -1,79 +1,235 @@
|
||||
<script setup lang="ts">
|
||||
import { h, ref, computed } from "vue";
|
||||
import { Trash2, Globe, Plus } from "@lucide/vue";
|
||||
import { Trash2, Globe, Plus, Search } from "@lucide/vue";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import FmsTable from "@/components/fms-table/FmsTable.vue";
|
||||
import I18nQuickSetModal from "./I18nQuickSetModal.vue";
|
||||
import { buildModuleItemI18nKey } from "@/utils/i18n";
|
||||
import { createTempId } from "@/utils/tempId";
|
||||
|
||||
const props = defineProps<{ moduleId: string; moduleCode: string; powers: Record<string, unknown>[]; loading?: boolean }>();
|
||||
const props = defineProps<{
|
||||
moduleId: string;
|
||||
moduleCode: string;
|
||||
powers: Record<string, unknown>[];
|
||||
loading?: boolean;
|
||||
}>();
|
||||
const emit = defineEmits<{ (e: "update", powers: Record<string, unknown>[]): void }>();
|
||||
|
||||
const i18nModalOpen = ref(false); const i18nTargetKey = ref(""); const i18nDefaultName = ref("");
|
||||
function openI18nModal(p: Record<string, unknown>) { i18nTargetKey.value = String(p.b_i18n || ""); i18nDefaultName.value = String(p.b_name || p.b_code || ""); i18nModalOpen.value = true; }
|
||||
const i18nModalOpen = ref(false);
|
||||
const i18nTargetKey = ref("");
|
||||
const i18nDefaultName = ref("");
|
||||
function openI18nModal(p: Record<string, unknown>) {
|
||||
i18nTargetKey.value = String(p.b_i18n || "");
|
||||
i18nDefaultName.value = String(p.b_name || p.b_code || "");
|
||||
i18nModalOpen.value = true;
|
||||
}
|
||||
function addPower() {
|
||||
const u = [...props.powers];
|
||||
u.push({ b_id: createTempId(), b_module_id: props.moduleId, b_code: "", b_name: "", b_i18n: null, b_xh: (u.length + 1) * 10, b_canuse: 1 });
|
||||
u.push({
|
||||
b_id: createTempId(),
|
||||
b_module_id: props.moduleId,
|
||||
b_code: "",
|
||||
b_name: "",
|
||||
b_i18n: null,
|
||||
b_xh: (u.length + 1) * 10,
|
||||
b_canuse: 1,
|
||||
});
|
||||
emit("update", u);
|
||||
}
|
||||
function updateField(idx: number, key: string, value: unknown) {
|
||||
const u = [...props.powers]; u[idx] = { ...u[idx], [key]: value };
|
||||
if (key === "b_code") u[idx].b_i18n = buildModuleItemI18nKey("power", props.moduleCode, String(value));
|
||||
const u = [...props.powers];
|
||||
u[idx] = { ...u[idx], [key]: value };
|
||||
if (key === "b_code")
|
||||
u[idx].b_i18n = buildModuleItemI18nKey("power", props.moduleCode, String(value));
|
||||
emit("update", u);
|
||||
}
|
||||
function deletePower(idx: number) {
|
||||
const u = [...props.powers];
|
||||
u.splice(idx, 1);
|
||||
emit("update", u);
|
||||
}
|
||||
function deletePower(idx: number) { const u = [...props.powers]; u.splice(idx, 1); emit("update", u); }
|
||||
function onCellChange(row: Record<string, unknown>, dataIndex: string, value: unknown) {
|
||||
const idx = props.powers.findIndex((x) => String(x.b_id) === String(row.b_id));
|
||||
if (idx >= 0) updateField(idx, dataIndex, value);
|
||||
}
|
||||
const selectedRowKeys = ref<string[]>([]);
|
||||
function onSelectionChange(keys: (string | number)[]) { if (props.loading) return; selectedRowKeys.value = keys.map(String); }
|
||||
function batchDelete() { emit("update", props.powers.filter((p) => !selectedRowKeys.value.includes(String(p.b_id)))); selectedRowKeys.value = []; }
|
||||
function onSelectionChange(keys: (string | number)[]) {
|
||||
if (props.loading) return;
|
||||
selectedRowKeys.value = keys.map(String);
|
||||
}
|
||||
function batchDelete() {
|
||||
emit(
|
||||
"update",
|
||||
props.powers.filter((p) => !selectedRowKeys.value.includes(String(p.b_id))),
|
||||
);
|
||||
selectedRowKeys.value = [];
|
||||
}
|
||||
|
||||
function i18nCell(row: Record<string, unknown>) {
|
||||
return h("div", { class: "flex items-center gap-1" }, [
|
||||
h("span", { class: "flex-1 truncate text-[12px] text-muted-foreground" }, String(row.b_i18n ?? "")),
|
||||
h("button", {
|
||||
type: "button",
|
||||
class: "inline-flex h-6 w-6 shrink-0 items-center justify-center rounded text-muted-foreground hover:bg-accent hover:text-foreground",
|
||||
title: "设置多语言",
|
||||
onClick: (e: MouseEvent) => { e.stopPropagation(); openI18nModal(row); },
|
||||
}, [h(Globe, { size: 12 })]),
|
||||
h(
|
||||
"span",
|
||||
{ class: "flex-1 truncate text-[12px] text-muted-foreground" },
|
||||
String(row.b_i18n ?? ""),
|
||||
),
|
||||
h(
|
||||
"button",
|
||||
{
|
||||
type: "button",
|
||||
class:
|
||||
"inline-flex h-6 w-6 shrink-0 items-center justify-center rounded text-muted-foreground hover:bg-accent hover:text-foreground",
|
||||
title: "设置多语言",
|
||||
onClick: (e: MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
openI18nModal(row);
|
||||
},
|
||||
},
|
||||
[h(Globe, { size: 12 })],
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
const rowSelection = computed(() => ({ selectedRowKeys: selectedRowKeys.value, onChange: onSelectionChange }));
|
||||
const rowSelection = computed(() => ({
|
||||
selectedRowKeys: selectedRowKeys.value,
|
||||
onChange: onSelectionChange,
|
||||
}));
|
||||
|
||||
const searchFields = [
|
||||
{ value: "b_code", label: "操作编码" },
|
||||
{ value: "b_name", label: "名称" },
|
||||
];
|
||||
const searchField = ref(searchFields[0].value);
|
||||
const searchText = ref("");
|
||||
const filteredPowers = computed(() => {
|
||||
if (!searchText.value.trim()) return props.powers;
|
||||
const kw = searchText.value.trim().toLowerCase();
|
||||
const sf = searchField.value;
|
||||
return props.powers.filter((r) =>
|
||||
String(r[sf] ?? "")
|
||||
.toLowerCase()
|
||||
.includes(kw),
|
||||
);
|
||||
});
|
||||
|
||||
const columns = ref([
|
||||
{ dataIndex: "b_code", title: "操作编码", width: 130, editor: { type: "input", onChange: (v: string, row: Record<string, unknown>) => onCellChange(row, "b_code", v) } },
|
||||
{ dataIndex: "b_name", title: "名称", width: 110, editor: { type: "input", onChange: (v: string, row: Record<string, unknown>) => onCellChange(row, "b_name", v) } },
|
||||
{ dataIndex: "b_i18n", title: "多语言键", width: 170, customCell: ({ row }: { row: Record<string, unknown> }) => i18nCell(row) },
|
||||
{ dataIndex: "b_xh", title: "排序", width: 70, editor: { type: "input", inputType: "number", onChange: (v: string, row: Record<string, unknown>) => onCellChange(row, "b_xh", Number(v)) } },
|
||||
{ dataIndex: "b_canuse", title: "启用", width: 60, editor: { type: "checkbox", onChange: (v: number, row: Record<string, unknown>) => onCellChange(row, "b_canuse", v) } },
|
||||
{ dataIndex: "actions", title: "操作", width: 60, align: "center", actions: [{ type: "delete", danger: true, onClick: (row: Record<string, unknown>) => { const i = props.powers.findIndex((x) => String(x.b_id) === String(row.b_id)); if (i >= 0) deletePower(i); } }] },
|
||||
{
|
||||
dataIndex: "b_code",
|
||||
title: "操作编码",
|
||||
width: 130,
|
||||
editor: {
|
||||
type: "input",
|
||||
onChange: (v: string, row: Record<string, unknown>) => onCellChange(row, "b_code", v),
|
||||
},
|
||||
},
|
||||
{
|
||||
dataIndex: "b_name",
|
||||
title: "名称",
|
||||
width: 110,
|
||||
editor: {
|
||||
type: "input",
|
||||
onChange: (v: string, row: Record<string, unknown>) => onCellChange(row, "b_name", v),
|
||||
},
|
||||
},
|
||||
{
|
||||
dataIndex: "b_i18n",
|
||||
title: "多语言键",
|
||||
width: 170,
|
||||
customCell: ({ row }: { row: Record<string, unknown> }) => i18nCell(row),
|
||||
},
|
||||
{
|
||||
dataIndex: "b_xh",
|
||||
title: "排序",
|
||||
width: 70,
|
||||
editor: {
|
||||
type: "input",
|
||||
inputType: "number",
|
||||
onChange: (v: string, row: Record<string, unknown>) => onCellChange(row, "b_xh", Number(v)),
|
||||
},
|
||||
},
|
||||
{
|
||||
dataIndex: "b_canuse",
|
||||
title: "启用",
|
||||
width: 60,
|
||||
editor: {
|
||||
type: "checkbox",
|
||||
onChange: (v: number, row: Record<string, unknown>) => onCellChange(row, "b_canuse", v),
|
||||
},
|
||||
},
|
||||
{
|
||||
dataIndex: "actions",
|
||||
title: "操作",
|
||||
width: 60,
|
||||
align: "center",
|
||||
actions: [
|
||||
{
|
||||
type: "delete",
|
||||
danger: true,
|
||||
onClick: (row: Record<string, unknown>) => {
|
||||
const i = props.powers.findIndex((x) => String(x.b_id) === String(row.b_id));
|
||||
if (i >= 0) deletePower(i);
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
</script>
|
||||
<script lang="ts">export default { name: "ModulePowersPanel" };</script>
|
||||
<script lang="ts">
|
||||
export default { name: "ModulePowersPanel" };
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex h-full min-h-0 flex-col gap-4">
|
||||
<div class="flex shrink-0 items-center justify-between">
|
||||
<h3 class="text-[13px] font-semibold">权限配置</h3>
|
||||
<div class="flex shrink-0 items-center justify-between gap-3">
|
||||
<div class="flex min-w-0 flex-1 items-center rounded-md border border-input shadow-sm">
|
||||
<Select v-model="searchField">
|
||||
<SelectTrigger class="w-[110px] shrink-0 border-0 shadow-none rounded-r-none focus:ring-0"
|
||||
><SelectValue
|
||||
/></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem v-for="opt in searchFields" :key="opt.value" :value="opt.value">{{
|
||||
opt.label
|
||||
}}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div class="w-px self-stretch bg-border" />
|
||||
<Input
|
||||
v-model="searchText"
|
||||
class="flex-1 border-0 shadow-none rounded-none focus-visible:ring-0"
|
||||
placeholder="搜索..."
|
||||
/>
|
||||
<Button variant="ghost" size="icon" class="rounded-l-none"><Search :size="16" /></Button>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Button v-if="selectedRowKeys.length" variant="destructive" size="sm" class="h-7 text-[12px]" @click="batchDelete"><Trash2 :size="12" class="mr-1" />批量删除</Button>
|
||||
<Button variant="outline" size="sm" class="h-7 text-[12px]" :disabled="loading" @click="addPower"><Plus :size="12" class="mr-1" />新增权限</Button>
|
||||
<Button v-if="selectedRowKeys.length" variant="destructive" @click="batchDelete"
|
||||
><Trash2 :size="12" class="mr-1" />批量删除</Button
|
||||
>
|
||||
<Button variant="outline" :disabled="loading" @click="addPower"
|
||||
><Plus :size="12" class="mr-1" />新增权限</Button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<FmsTable
|
||||
class="min-h-0 flex-1"
|
||||
v-model:columns="columns"
|
||||
:data-source="powers"
|
||||
:data-source="filteredPowers"
|
||||
row-key="b_id"
|
||||
:editable="!loading"
|
||||
:virtual="false"
|
||||
:row-selection="rowSelection"
|
||||
/>
|
||||
<I18nQuickSetModal :open="i18nModalOpen" :i18n-key="i18nTargetKey" :default-value="i18nDefaultName" @update:open="(v: boolean) => i18nModalOpen = v" />
|
||||
<I18nQuickSetModal
|
||||
:open="i18nModalOpen"
|
||||
:i18n-key="i18nTargetKey"
|
||||
:default-value="i18nDefaultName"
|
||||
@update:open="(v: boolean) => (i18nModalOpen = v)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
+166
-22
@@ -1,56 +1,200 @@
|
||||
<script setup lang="ts">
|
||||
import { h, ref } from "vue";
|
||||
import { Plus } from "@lucide/vue";
|
||||
import { computed, h, ref } from "vue";
|
||||
import { Plus, Search } from "@lucide/vue";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import FmsTable from "@/components/fms-table/FmsTable.vue";
|
||||
import { createTempId } from "@/utils/tempId";
|
||||
|
||||
const props = defineProps<{ moduleId: string; queryConfig: Record<string, unknown>[]; fields: Record<string, unknown>[]; loading?: boolean }>();
|
||||
const props = defineProps<{
|
||||
moduleId: string;
|
||||
queryConfig: Record<string, unknown>[];
|
||||
fields: Record<string, unknown>[];
|
||||
loading?: boolean;
|
||||
}>();
|
||||
const emit = defineEmits<{ (e: "update", rows: Record<string, unknown>[]): void }>();
|
||||
|
||||
const COMPS = [
|
||||
{ value: "input", label: "文本框" },{ value: "select", label: "下拉选择" },{ value: "date", label: "日期" },{ value: "datetime", label: "日期时间" },{ value: "number", label: "数字" },{ value: "checkbox", label: "复选框" },{ value: "month", label: "月份" },{ value: "year", label: "年份" },
|
||||
{ value: "input", label: "文本框" },
|
||||
{ value: "select", label: "下拉选择" },
|
||||
{ value: "date", label: "日期" },
|
||||
{ value: "datetime", label: "日期时间" },
|
||||
{ value: "number", label: "数字" },
|
||||
{ value: "checkbox", label: "复选框" },
|
||||
{ value: "month", label: "月份" },
|
||||
{ value: "year", label: "年份" },
|
||||
];
|
||||
const OPS = [
|
||||
{ value: "=", label: "等于" },{ value: "!=", label: "不等于" },{ value: ">", label: "大于" },{ value: ">=", label: "大于等于" },{ value: "<", label: "小于" },{ value: "<=", label: "小于等于" },{ value: "like", label: "包含" },{ value: "in", label: "在区间" },{ value: "between", label: "之间" },
|
||||
{ value: "=", label: "等于" },
|
||||
{ value: "!=", label: "不等于" },
|
||||
{ value: ">", label: "大于" },
|
||||
{ value: ">=", label: "大于等于" },
|
||||
{ value: "<", label: "小于" },
|
||||
{ value: "<=", label: "小于等于" },
|
||||
{ value: "like", label: "包含" },
|
||||
{ value: "in", label: "在区间" },
|
||||
{ value: "between", label: "之间" },
|
||||
];
|
||||
|
||||
function fname(id: unknown) { const f = props.fields.find((x) => String(x.b_id) === String(id)); return f ? String(f.b_field || "") : String(id || ""); }
|
||||
function fname(id: unknown) {
|
||||
const f = props.fields.find((x) => String(x.b_id) === String(id));
|
||||
return f ? String(f.b_field || "") : String(id || "");
|
||||
}
|
||||
|
||||
const searchFields = [
|
||||
{ value: "_field", label: "字段名" },
|
||||
{ value: "b_component", label: "组件" },
|
||||
{ value: "b_operate", label: "操作符" },
|
||||
];
|
||||
const searchField = ref(searchFields[0].value);
|
||||
const searchText = ref("");
|
||||
const filteredQueryConfig = computed(() => {
|
||||
if (!searchText.value.trim()) return props.queryConfig;
|
||||
const kw = searchText.value.trim().toLowerCase();
|
||||
return props.queryConfig.filter((r) => {
|
||||
if (searchField.value === "_field") return fname(r.b_field_id).toLowerCase().includes(kw);
|
||||
return String(r[searchField.value] ?? "")
|
||||
.toLowerCase()
|
||||
.includes(kw);
|
||||
});
|
||||
});
|
||||
|
||||
function addRow() {
|
||||
const en = props.fields.filter((f) => Number(f.b_canuse) === 1);
|
||||
if (!en.length) return;
|
||||
const f = en[0];
|
||||
const u = [...props.queryConfig];
|
||||
u.push({ b_id: createTempId(), b_module_id: props.moduleId, b_field_id: f.b_id, b_component: "input", b_operator: "=", b_default_value: "", b_xh: (u.length + 1) * 10, b_canuse: 1 });
|
||||
u.push({
|
||||
b_id: createTempId(),
|
||||
b_module_id: props.moduleId,
|
||||
b_field_id: f.b_id,
|
||||
b_component: "input",
|
||||
b_operator: "=",
|
||||
b_default_value: "",
|
||||
b_xh: (u.length + 1) * 10,
|
||||
b_canuse: 1,
|
||||
});
|
||||
emit("update", u);
|
||||
}
|
||||
function up(idx: number, key: string, value: unknown) { const u = [...props.queryConfig]; u[idx] = { ...u[idx], [key]: value }; emit("update", u); }
|
||||
function onCellChange(row: Record<string, unknown>, dataIndex: string, value: unknown) { const idx = props.queryConfig.findIndex((x) => String(x.b_id) === String(row.b_id)); if (idx >= 0) up(idx, dataIndex, value); }
|
||||
function onRowOrderChange(rows: Record<string, unknown>[]) { const reordered = rows.map((x, i) => ({ ...x, b_xh: (i + 1) * 10 })); emit("update", reordered); }
|
||||
function up(idx: number, key: string, value: unknown) {
|
||||
const u = [...props.queryConfig];
|
||||
u[idx] = { ...u[idx], [key]: value };
|
||||
emit("update", u);
|
||||
}
|
||||
function onCellChange(row: Record<string, unknown>, dataIndex: string, value: unknown) {
|
||||
const idx = props.queryConfig.findIndex((x) => String(x.b_id) === String(row.b_id));
|
||||
if (idx >= 0) up(idx, dataIndex, value);
|
||||
}
|
||||
function onRowOrderChange(rows: Record<string, unknown>[]) {
|
||||
const reordered = rows.map((x, i) => ({ ...x, b_xh: (i + 1) * 10 }));
|
||||
emit("update", reordered);
|
||||
}
|
||||
|
||||
function fieldCell(row: Record<string, unknown>) { return h("span", { class: "text-[13px]" }, fname(row.b_field_id)); }
|
||||
function fieldCell(row: Record<string, unknown>) {
|
||||
return h("span", { class: "text-[13px]" }, fname(row.b_field_id));
|
||||
}
|
||||
|
||||
const columns = ref([
|
||||
{ dataIndex: "b_field_id", title: "字段", width: 130, customCell: ({ row }: { row: Record<string, unknown> }) => fieldCell(row) },
|
||||
{ dataIndex: "b_component", title: "组件", width: 110, editor: { type: "select", options: COMPS, allowClear: false, onChange: (v: string, row: Record<string, unknown>) => onCellChange(row, "b_component", v) } },
|
||||
{ dataIndex: "b_operator", title: "操作符", width: 100, editor: { type: "select", options: OPS, allowClear: false, onChange: (v: string, row: Record<string, unknown>) => onCellChange(row, "b_operator", v) } },
|
||||
{ dataIndex: "b_default_value", title: "默认值", width: 110, editor: { type: "input", onChange: (v: string, row: Record<string, unknown>) => onCellChange(row, "b_default_value", v) } },
|
||||
{ dataIndex: "b_canuse", title: "启用", width: 60, editor: { type: "checkbox", onChange: (v: number, row: Record<string, unknown>) => onCellChange(row, "b_canuse", v) } },
|
||||
{ dataIndex: "b_xh", title: "排序", width: 70, editor: { type: "input", inputType: "number", onChange: (v: string, row: Record<string, unknown>) => onCellChange(row, "b_xh", Number(v)) } },
|
||||
{
|
||||
dataIndex: "b_field_id",
|
||||
title: "字段",
|
||||
width: 130,
|
||||
customCell: ({ row }: { row: Record<string, unknown> }) => fieldCell(row),
|
||||
},
|
||||
{
|
||||
dataIndex: "b_component",
|
||||
title: "组件",
|
||||
width: 110,
|
||||
editor: {
|
||||
type: "select",
|
||||
options: COMPS,
|
||||
allowClear: false,
|
||||
onChange: (v: string, row: Record<string, unknown>) => onCellChange(row, "b_component", v),
|
||||
},
|
||||
},
|
||||
{
|
||||
dataIndex: "b_operator",
|
||||
title: "操作符",
|
||||
width: 100,
|
||||
editor: {
|
||||
type: "select",
|
||||
options: OPS,
|
||||
allowClear: false,
|
||||
onChange: (v: string, row: Record<string, unknown>) => onCellChange(row, "b_operator", v),
|
||||
},
|
||||
},
|
||||
{
|
||||
dataIndex: "b_default_value",
|
||||
title: "默认值",
|
||||
width: 110,
|
||||
editor: {
|
||||
type: "input",
|
||||
onChange: (v: string, row: Record<string, unknown>) =>
|
||||
onCellChange(row, "b_default_value", v),
|
||||
},
|
||||
},
|
||||
{
|
||||
dataIndex: "b_canuse",
|
||||
title: "启用",
|
||||
width: 60,
|
||||
editor: {
|
||||
type: "checkbox",
|
||||
onChange: (v: number, row: Record<string, unknown>) => onCellChange(row, "b_canuse", v),
|
||||
},
|
||||
},
|
||||
{
|
||||
dataIndex: "b_xh",
|
||||
title: "排序",
|
||||
width: 70,
|
||||
editor: {
|
||||
type: "input",
|
||||
inputType: "number",
|
||||
onChange: (v: string, row: Record<string, unknown>) => onCellChange(row, "b_xh", Number(v)),
|
||||
},
|
||||
},
|
||||
]);
|
||||
</script>
|
||||
<script lang="ts">export default { name: "ModuleQueryConfigPanel" };</script>
|
||||
<script lang="ts">
|
||||
export default { name: "ModuleQueryConfigPanel" };
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex h-full min-h-0 flex-col gap-4">
|
||||
<div class="flex shrink-0 items-center justify-between">
|
||||
<h3 class="text-[13px] font-semibold">查询配置</h3>
|
||||
<Button variant="outline" size="sm" class="h-7 text-[12px]" :disabled="loading" @click="addRow"><Plus :size="12" class="mr-1" />新增查询条件</Button>
|
||||
<div class="flex shrink-0 items-center justify-between gap-3">
|
||||
<div class="flex min-w-0 flex-1 items-center rounded-md border border-input shadow-sm">
|
||||
<Select v-model="searchField">
|
||||
<SelectTrigger class="w-[110px] shrink-0 border-0 shadow-none rounded-r-none focus:ring-0"
|
||||
><SelectValue
|
||||
/></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem v-for="opt in searchFields" :key="opt.value" :value="opt.value">{{
|
||||
opt.label
|
||||
}}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div class="w-px self-stretch bg-border" />
|
||||
<Input
|
||||
v-model="searchText"
|
||||
class="min-w-0 flex-1 border-0 shadow-none rounded-none focus-visible:ring-0"
|
||||
placeholder="搜索..."
|
||||
/>
|
||||
<Button variant="ghost" size="icon" class="rounded-l-none"><Search :size="16" /></Button>
|
||||
</div>
|
||||
<Button variant="outline" :disabled="loading" @click="addRow"
|
||||
><Plus :size="12" class="mr-1" />新增查询条件</Button
|
||||
>
|
||||
</div>
|
||||
<FmsTable
|
||||
class="min-h-0 flex-1"
|
||||
v-model:columns="columns"
|
||||
:data-source="queryConfig"
|
||||
:data-source="filteredQueryConfig"
|
||||
row-key="b_id"
|
||||
:editable="!loading"
|
||||
:virtual="false"
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user