20260812221200

This commit is contained in:
oneao committed 2026-08-12 22:12:01 +08:00
1 parent 87b2d3c482
commit 4bd730f649
47 files changed
+6286 -1881

No files matched your search

@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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"
@@ -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>
@@ -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>
@@ -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