20260723225202
This commit is contained in:
1 parent
5f73b54802
commit
4d118a0037
22 files changed
+1403
-259
No files matched your search
@@ -1,10 +1,14 @@
|
||||
<script setup>
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { message } from "antdv-next";
|
||||
import { Languages } from "@lucide/vue";
|
||||
import { loadDataApi, saveObjectApi } from "@/services/api";
|
||||
|
||||
const props = defineProps({
|
||||
moduleId: { type: String, required: true },
|
||||
data: { type: Object, default: null },
|
||||
loading: { type: Boolean, default: false },
|
||||
parentName: { type: String, default: "" },
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update"]);
|
||||
@@ -32,6 +36,85 @@ function setField(field, value) {
|
||||
formData.value[field] = value === "" ? null : value;
|
||||
emitUpdate();
|
||||
}
|
||||
|
||||
// 多语言键由模块编码自动生成(module.<编码>),无需手动填写。
|
||||
function onCodeInput(v) {
|
||||
setField("b_code", v);
|
||||
if (!formData.value.b_i18n && v) {
|
||||
formData.value.b_i18n = `module.${v}`;
|
||||
emitUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
const i18nModalVisible = ref(false);
|
||||
const i18nSaving = ref(false);
|
||||
const i18nKey = ref("");
|
||||
const i18nEntries = ref([]);
|
||||
const langTypes = ref([]);
|
||||
|
||||
async function loadLangTypes() {
|
||||
try {
|
||||
const res = await loadDataApi("b_i18n_type", "b_canuse = 1", "b_xh ASC, b_id ASC");
|
||||
langTypes.value = res.data || [];
|
||||
} catch (e) {
|
||||
message.error("加载语言类型失败:" + (e.message || e));
|
||||
}
|
||||
}
|
||||
|
||||
async function openI18nModal() {
|
||||
const key = formData.value?.b_i18n || `module.${formData.value?.b_code || ""}`;
|
||||
i18nKey.value = key;
|
||||
const escaped = key.replace(/'/g, "''");
|
||||
try {
|
||||
const [i18nRes] = await Promise.all([
|
||||
loadDataApi("b_i18n", `b_key = '${escaped}'`, "b_locale ASC"),
|
||||
loadLangTypes(),
|
||||
]);
|
||||
const rows = i18nRes.data || [];
|
||||
const byLocale = Object.fromEntries(rows.map((r) => [r.b_locale, r]));
|
||||
i18nEntries.value = langTypes.value.map((t) => {
|
||||
const ex = byLocale[t.b_code];
|
||||
const isDefault = t.b_default === 1 || t.b_code === "zh-CN";
|
||||
return {
|
||||
b_id: ex ? ex.b_id : null,
|
||||
b_locale: t.b_code,
|
||||
b_name: t.b_name,
|
||||
b_value: ex ? ex.b_value : isDefault ? formData.value?.b_name || "" : "",
|
||||
};
|
||||
});
|
||||
} catch {
|
||||
i18nEntries.value = [];
|
||||
}
|
||||
i18nModalVisible.value = true;
|
||||
}
|
||||
|
||||
async function saveI18n() {
|
||||
if (!i18nKey.value) {
|
||||
message.warning("请填写多语言键");
|
||||
return;
|
||||
}
|
||||
i18nSaving.value = true;
|
||||
try {
|
||||
setField("b_i18n", i18nKey.value);
|
||||
const req = { table: "b_i18n", key_field: "b_id", inserts: [], updates: [] };
|
||||
for (const e of i18nEntries.value) {
|
||||
const value = e.b_value ?? "";
|
||||
if (!e.b_id && !value) continue;
|
||||
const row = { b_key: i18nKey.value, b_locale: e.b_locale, b_value: value };
|
||||
if (e.b_id) req.updates.push({ ...row, b_id: e.b_id });
|
||||
else req.inserts.push(row);
|
||||
}
|
||||
if (req.inserts.length || req.updates.length) {
|
||||
await saveObjectApi([req]);
|
||||
}
|
||||
message.success("多语言已保存");
|
||||
i18nModalVisible.value = false;
|
||||
} catch (err) {
|
||||
message.error("保存失败:" + (err?.message || err));
|
||||
} finally {
|
||||
i18nSaving.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -44,7 +127,7 @@ function setField(field, value) {
|
||||
<a-input
|
||||
:value="formData.b_code"
|
||||
placeholder="如 m_sea"
|
||||
@update:value="(v) => setField('b_code', v)"
|
||||
@update:value="onCodeInput"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
@@ -75,16 +158,26 @@ function setField(field, value) {
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="8">
|
||||
<a-form-item label="上级模块">
|
||||
<a-input :value="formData.b_parent_id" disabled placeholder="通过左侧树选中后自动设置" />
|
||||
<a-input :value="parentName" readonly placeholder="通过左侧树选中后自动设置" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item label="多语言键">
|
||||
<a-input
|
||||
:value="formData.b_i18n"
|
||||
placeholder="如 module.m_sea"
|
||||
@update:value="(v) => setField('b_i18n', v)"
|
||||
/>
|
||||
<div class="i18n-key-row">
|
||||
<a-input
|
||||
:value="formData.b_i18n"
|
||||
readonly
|
||||
placeholder="填写模块编码后自动生成"
|
||||
/>
|
||||
<a-button
|
||||
class="i18n-key-btn"
|
||||
type="text"
|
||||
title="快速设置多语言"
|
||||
@click="openI18nModal"
|
||||
>
|
||||
<Languages :size="16" />
|
||||
</a-button>
|
||||
</div>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="4">
|
||||
@@ -151,6 +244,25 @@ function setField(field, value) {
|
||||
</a-row>
|
||||
</template>
|
||||
</a-form>
|
||||
|
||||
<a-modal
|
||||
v-model:open="i18nModalVisible"
|
||||
title="快速设置多语言"
|
||||
:confirm-loading="i18nSaving"
|
||||
@ok="saveI18n"
|
||||
>
|
||||
<a-form layout="vertical">
|
||||
<a-form-item label="多语言键">
|
||||
<a-input v-model:value="i18nKey" disabled />
|
||||
</a-form-item>
|
||||
<a-form-item label="多语言值">
|
||||
<div v-for="(item, idx) in i18nEntries" :key="idx" class="i18n-entry-row">
|
||||
<span class="i18n-locale-label">{{ item.b_name }} ({{ item.b_locale }})</span>
|
||||
<a-input v-model:value="item.b_value" class="i18n-value" />
|
||||
</div>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -171,4 +283,37 @@ function setField(field, value) {
|
||||
.basic-form {
|
||||
max-width: 960px;
|
||||
}
|
||||
|
||||
.i18n-key-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
|
||||
.ant-input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.i18n-key-btn {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.i18n-entry-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.i18n-locale-label {
|
||||
flex: none;
|
||||
width: 140px;
|
||||
color: #646a73;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.i18n-value {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -3,7 +3,6 @@ import { computed, ref, watch } from "vue";
|
||||
import FmsTable from "@/components/fms-table/FmsTable.vue";
|
||||
import { createTempId } from "@/utils/tempId";
|
||||
import FieldGroupManager from "./FieldGroupManager.vue";
|
||||
import { createDeleteCell, createSelectCell, createToggleCell } from "./cellRenderers";
|
||||
|
||||
const props = defineProps({
|
||||
moduleId: { type: String, required: true },
|
||||
@@ -17,6 +16,7 @@ const emit = defineEmits(["update", "updateGroups"]);
|
||||
|
||||
const localEdit = ref([]);
|
||||
const groupManagerVisible = ref(false);
|
||||
const activeKey = ref(null);
|
||||
|
||||
watch(
|
||||
() => props.editConfig,
|
||||
@@ -30,11 +30,6 @@ function emitUpdate() {
|
||||
emit("update", localEdit.value.map((r) => ({ ...r })));
|
||||
}
|
||||
|
||||
function setField(row, field, value) {
|
||||
row[field] = value === undefined || value === null ? null : value;
|
||||
emitUpdate();
|
||||
}
|
||||
|
||||
const availableFields = computed(() =>
|
||||
(props.fields || [])
|
||||
.filter((f) => f.b_canuse === 1)
|
||||
@@ -72,7 +67,7 @@ function makeSwitchCol(field, title, width) {
|
||||
title,
|
||||
width: width || 60,
|
||||
align: "center",
|
||||
customCell: createToggleCell(field, (row) => setField(row, field, row[field] === 1 ? 0 : 1)),
|
||||
editor: { type: "checkbox", onChange: () => emitUpdate() },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -81,12 +76,11 @@ const columns = [
|
||||
dataIndex: "b_field_id",
|
||||
title: "字段",
|
||||
width: 160,
|
||||
customCell: createSelectCell({
|
||||
value: "b_field_id",
|
||||
getOptions: () => availableFields.value,
|
||||
onChange: (row, v) => setField(row, "b_field_id", v),
|
||||
placeholder: "选择字段",
|
||||
}),
|
||||
editor: {
|
||||
type: "select",
|
||||
options: () => availableFields.value,
|
||||
onChange: () => emitUpdate(),
|
||||
},
|
||||
},
|
||||
makeSwitchCol("b_required", "必填"),
|
||||
makeSwitchCol("b_readonly", "只读"),
|
||||
@@ -95,12 +89,11 @@ const columns = [
|
||||
dataIndex: "b_group_id",
|
||||
title: "编辑分组",
|
||||
width: 160,
|
||||
customCell: createSelectCell({
|
||||
value: "b_group_id",
|
||||
getOptions: () => groupOptions.value,
|
||||
onChange: (row, v) => setField(row, "b_group_id", v),
|
||||
placeholder: "选择分组",
|
||||
}),
|
||||
editor: {
|
||||
type: "select",
|
||||
options: () => groupOptions.value,
|
||||
onChange: () => emitUpdate(),
|
||||
},
|
||||
},
|
||||
{ dataIndex: "b_xh", title: "排序号", width: 80 },
|
||||
{
|
||||
@@ -108,7 +101,7 @@ const columns = [
|
||||
title: "操作",
|
||||
width: 60,
|
||||
align: "center",
|
||||
customCell: createDeleteCell((row) => deleteRow(row)),
|
||||
actions: [{ type: "danger", label: "删除", confirm: { title: "确定删除该记录?", okText: "删除" }, onClick: (row) => deleteRow(row) }],
|
||||
},
|
||||
];
|
||||
</script>
|
||||
@@ -122,7 +115,13 @@ const columns = [
|
||||
</a-space>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<FmsTable :columns="columns" :data-source="localEdit" row-key="b_id" />
|
||||
<FmsTable
|
||||
:columns="columns"
|
||||
:data-source="localEdit"
|
||||
row-key="b_id"
|
||||
:active-row-key="activeKey"
|
||||
@row-click="(ev, row) => (activeKey = row.b_id)"
|
||||
/>
|
||||
</div>
|
||||
<FieldGroupManager
|
||||
v-model:visible="groupManagerVisible"
|
||||
@@ -144,13 +143,13 @@ const columns = [
|
||||
display: flex;
|
||||
flex: none;
|
||||
gap: 8px;
|
||||
padding: 12px 20px;
|
||||
padding: 12px 20px 0;
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
padding: 16px 20px;
|
||||
padding: 12px 20px 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script setup>
|
||||
import { ref, watch } from "vue";
|
||||
import FmsTable from "@/components/fms-table/FmsTable.vue";
|
||||
import { createSelectCell, createToggleCell } from "./cellRenderers";
|
||||
|
||||
const FIELD_TYPES = [
|
||||
{ label: "输入框", value: "input" },
|
||||
@@ -24,6 +23,7 @@ const props = defineProps({
|
||||
const emit = defineEmits(["update", "sync"]);
|
||||
|
||||
const localFields = ref([]);
|
||||
const activeKey = ref(null);
|
||||
|
||||
watch(
|
||||
() => props.fields,
|
||||
@@ -37,11 +37,6 @@ function emitUpdate() {
|
||||
emit("update", localFields.value.map((r) => ({ ...r })));
|
||||
}
|
||||
|
||||
function setField(row, field, value) {
|
||||
row[field] = value === "" ? null : value;
|
||||
emitUpdate();
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{ dataIndex: "b_field", title: "字段名", width: 140 },
|
||||
{ dataIndex: "b_name", title: "字段名称", width: 140 },
|
||||
@@ -49,11 +44,11 @@ const columns = [
|
||||
dataIndex: "b_type",
|
||||
title: "控件类型",
|
||||
width: 130,
|
||||
customCell: createSelectCell({
|
||||
value: "b_type",
|
||||
getOptions: () => FIELD_TYPES,
|
||||
onChange: (row, v) => setField(row, "b_type", v),
|
||||
}),
|
||||
editor: {
|
||||
type: "select",
|
||||
options: FIELD_TYPES,
|
||||
onChange: () => emitUpdate(),
|
||||
},
|
||||
},
|
||||
{ dataIndex: "b_default_value", title: "默认值", width: 120 },
|
||||
{ dataIndex: "b_i18n", title: "多语言键", width: 140 },
|
||||
@@ -62,7 +57,7 @@ const columns = [
|
||||
title: "启用",
|
||||
width: 70,
|
||||
align: "center",
|
||||
customCell: createToggleCell("b_canuse", (row) => setField(row, "b_canuse", row.b_canuse === 1 ? 0 : 1)),
|
||||
editor: { type: "checkbox", onChange: () => emitUpdate() },
|
||||
},
|
||||
];
|
||||
</script>
|
||||
@@ -81,7 +76,13 @@ const columns = [
|
||||
</a-space>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<FmsTable :columns="columns" :data-source="localFields" row-key="b_id" />
|
||||
<FmsTable
|
||||
:columns="columns"
|
||||
:data-source="localFields"
|
||||
row-key="b_id"
|
||||
:active-row-key="activeKey"
|
||||
@row-click="(ev, row) => (activeKey = row.b_id)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -97,13 +98,13 @@ const columns = [
|
||||
display: flex;
|
||||
flex: none;
|
||||
gap: 8px;
|
||||
padding: 12px 20px;
|
||||
padding: 12px 20px 0;
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
padding: 16px 20px;
|
||||
padding: 12px 20px 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
<script setup>
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { message } from "antdv-next";
|
||||
import { computed, h, nextTick, ref, watch } from "vue";
|
||||
import { message, Select } from "antdv-next";
|
||||
import FmsTable from "@/components/fms-table/FmsTable.vue";
|
||||
import { loadDataApi, saveObjectApi } from "@/services/api";
|
||||
import { cloneData, diffRows, allocateTemporaryIds, tableChange, hasChanges } from "@/utils/dataChanges";
|
||||
import { createTempId } from "@/utils/tempId";
|
||||
import { createDeleteCell } from "./cellRenderers";
|
||||
|
||||
const props = defineProps({
|
||||
moduleId: { type: String, required: true },
|
||||
@@ -17,12 +16,30 @@ const originals = ref([]);
|
||||
const isLoading = ref(false);
|
||||
const isSaving = ref(false);
|
||||
const searchKey = ref("");
|
||||
const langOptions = ref([]);
|
||||
const activeKey = ref(null);
|
||||
const tableRef = ref(null);
|
||||
|
||||
async function loadLangTypes() {
|
||||
try {
|
||||
const res = await loadDataApi("b_i18n_type", "b_canuse = 1", "b_xh ASC, b_id ASC");
|
||||
langOptions.value = (res.data || []).map((t) => ({
|
||||
label: `${t.b_name} (${t.b_code})`,
|
||||
value: t.b_code,
|
||||
}));
|
||||
} catch (e) {
|
||||
message.error("加载语言类型失败:" + (e.message || e));
|
||||
}
|
||||
}
|
||||
|
||||
async function loadI18n() {
|
||||
isLoading.value = true;
|
||||
try {
|
||||
const res = await loadDataApi("b_i18n", null, "b_key ASC, b_locale ASC");
|
||||
originals.value = res.data || [];
|
||||
const [i18nRes] = await Promise.all([
|
||||
loadDataApi("b_i18n", null, "b_key ASC, b_locale ASC"),
|
||||
loadLangTypes(),
|
||||
]);
|
||||
originals.value = i18nRes.data || [];
|
||||
i18nData.value = cloneData(originals.value);
|
||||
} catch (e) {
|
||||
message.error("加载多语言失败:" + (e.message || e));
|
||||
@@ -33,13 +50,19 @@ async function loadI18n() {
|
||||
|
||||
watch(() => props.moduleId, loadI18n, { immediate: true });
|
||||
|
||||
function addRow() {
|
||||
async function addRow() {
|
||||
const def = langOptions.value[0]?.value || "zh-CN";
|
||||
const newId = createTempId();
|
||||
i18nData.value.push({
|
||||
b_id: createTempId(),
|
||||
b_id: newId,
|
||||
b_key: "",
|
||||
b_locale: "zh-CN",
|
||||
b_locale: def,
|
||||
b_value: "",
|
||||
});
|
||||
|
||||
activeKey.value = String(newId);
|
||||
await nextTick();
|
||||
tableRef.value?.scrollToRow(String(newId));
|
||||
}
|
||||
|
||||
function deleteRow(record) {
|
||||
@@ -76,16 +99,34 @@ const displayedData = computed(() => {
|
||||
);
|
||||
});
|
||||
|
||||
function createLocaleCell() {
|
||||
return (props) => {
|
||||
const row = props.row;
|
||||
return h(
|
||||
Select,
|
||||
{
|
||||
value: row.b_locale,
|
||||
size: "small",
|
||||
style: { width: "100%" },
|
||||
options: langOptions.value,
|
||||
onChange: (val) => {
|
||||
row.b_locale = val;
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{ dataIndex: "b_key", title: "键", width: 200 },
|
||||
{ dataIndex: "b_locale", title: "语言", width: 100 },
|
||||
{ dataIndex: "b_locale", title: "语言", width: 160, customCell: createLocaleCell() },
|
||||
{ dataIndex: "b_value", title: "翻译文本", width: 300 },
|
||||
{
|
||||
dataIndex: "actions",
|
||||
title: "操作",
|
||||
width: 60,
|
||||
align: "center",
|
||||
customCell: createDeleteCell((row) => deleteRow(row)),
|
||||
actions: [{ type: "danger", label: "删除", confirm: { title: "确定删除该记录?", okText: "删除" }, onClick: (row) => deleteRow(row) }],
|
||||
},
|
||||
];
|
||||
</script>
|
||||
@@ -105,7 +146,7 @@ const columns = [
|
||||
</a-space>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<FmsTable :columns="columns" :data-source="displayedData" row-key="b_id" />
|
||||
<FmsTable ref="tableRef" :columns="columns" :data-source="displayedData" row-key="b_id" :active-row-key="activeKey" @row-click="(_, row) => (activeKey = String(row.b_id))" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -121,13 +162,13 @@ const columns = [
|
||||
display: flex;
|
||||
flex: none;
|
||||
gap: 8px;
|
||||
padding: 12px 20px;
|
||||
padding: 12px 20px 0;
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
padding: 16px 20px;
|
||||
padding: 12px 20px 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import { computed, ref, watch } from "vue";
|
||||
import FmsTable from "@/components/fms-table/FmsTable.vue";
|
||||
import { createTempId } from "@/utils/tempId";
|
||||
import FieldGroupManager from "./FieldGroupManager.vue";
|
||||
import { createDeleteCell, createSelectCell, createToggleCell } from "./cellRenderers";
|
||||
|
||||
const props = defineProps({
|
||||
moduleId: { type: String, required: true },
|
||||
@@ -17,6 +16,7 @@ const emit = defineEmits(["update", "updateGroups"]);
|
||||
|
||||
const localList = ref([]);
|
||||
const groupManagerVisible = ref(false);
|
||||
const activeKey = ref(null);
|
||||
|
||||
watch(
|
||||
() => props.listConfig,
|
||||
@@ -30,11 +30,6 @@ function emitUpdate() {
|
||||
emit("update", localList.value.map((r) => ({ ...r })));
|
||||
}
|
||||
|
||||
function setField(row, field, value) {
|
||||
row[field] = value === undefined || value === null ? null : value;
|
||||
emitUpdate();
|
||||
}
|
||||
|
||||
const availableFields = computed(() =>
|
||||
(props.fields || [])
|
||||
.filter((f) => f.b_canuse === 1)
|
||||
@@ -73,12 +68,11 @@ const columns = [
|
||||
dataIndex: "b_field_id",
|
||||
title: "字段",
|
||||
width: 160,
|
||||
customCell: createSelectCell({
|
||||
value: "b_field_id",
|
||||
getOptions: () => availableFields.value,
|
||||
onChange: (row, v) => setField(row, "b_field_id", v),
|
||||
placeholder: "选择字段",
|
||||
}),
|
||||
editor: {
|
||||
type: "select",
|
||||
options: () => availableFields.value,
|
||||
onChange: () => emitUpdate(),
|
||||
},
|
||||
},
|
||||
{ dataIndex: "b_title", title: "列标题", width: 120 },
|
||||
{ dataIndex: "b_width", title: "列宽", width: 80 },
|
||||
@@ -86,26 +80,25 @@ const columns = [
|
||||
dataIndex: "b_group_id",
|
||||
title: "分组",
|
||||
width: 140,
|
||||
customCell: createSelectCell({
|
||||
value: "b_group_id",
|
||||
getOptions: () => groupOptions.value,
|
||||
onChange: (row, v) => setField(row, "b_group_id", v),
|
||||
placeholder: "选择分组",
|
||||
}),
|
||||
editor: {
|
||||
type: "select",
|
||||
options: () => groupOptions.value,
|
||||
onChange: () => emitUpdate(),
|
||||
},
|
||||
},
|
||||
{
|
||||
dataIndex: "b_visible",
|
||||
title: "显示",
|
||||
width: 60,
|
||||
align: "center",
|
||||
customCell: createToggleCell("b_visible", (row) => setField(row, "b_visible", row.b_visible === 1 ? 0 : 1)),
|
||||
editor: { type: "checkbox", onChange: () => emitUpdate() },
|
||||
},
|
||||
{
|
||||
dataIndex: "b_sortable",
|
||||
title: "排序",
|
||||
width: 60,
|
||||
align: "center",
|
||||
customCell: createToggleCell("b_sortable", (row) => setField(row, "b_sortable", row.b_sortable === 1 ? 0 : 1)),
|
||||
editor: { type: "checkbox", onChange: () => emitUpdate() },
|
||||
},
|
||||
{ dataIndex: "b_xh", title: "排序号", width: 80 },
|
||||
{
|
||||
@@ -113,7 +106,7 @@ const columns = [
|
||||
title: "操作",
|
||||
width: 60,
|
||||
align: "center",
|
||||
customCell: createDeleteCell((row) => deleteRow(row)),
|
||||
actions: [{ type: "danger", label: "删除", confirm: { title: "确定删除该记录?", okText: "删除" }, onClick: (row) => deleteRow(row) }],
|
||||
},
|
||||
];
|
||||
</script>
|
||||
@@ -127,7 +120,13 @@ const columns = [
|
||||
</a-space>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<FmsTable :columns="columns" :data-source="localList" row-key="b_id" />
|
||||
<FmsTable
|
||||
:columns="columns"
|
||||
:data-source="localList"
|
||||
row-key="b_id"
|
||||
:active-row-key="activeKey"
|
||||
@row-click="(ev, row) => (activeKey = row.b_id)"
|
||||
/>
|
||||
</div>
|
||||
<FieldGroupManager
|
||||
v-model:visible="groupManagerVisible"
|
||||
@@ -149,13 +148,13 @@ const columns = [
|
||||
display: flex;
|
||||
flex: none;
|
||||
gap: 8px;
|
||||
padding: 12px 20px;
|
||||
padding: 12px 20px 0;
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
padding: 16px 20px;
|
||||
padding: 12px 20px 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<script setup>
|
||||
import { ref, watch } from "vue";
|
||||
import { nextTick, ref, watch } from "vue";
|
||||
import FmsTable from "@/components/fms-table/FmsTable.vue";
|
||||
import { createTempId } from "@/utils/tempId";
|
||||
import { createDeleteCell, createToggleCell } from "./cellRenderers";
|
||||
|
||||
const props = defineProps({
|
||||
moduleId: { type: String, required: true },
|
||||
moduleCode: { type: String, default: "" },
|
||||
powers: { type: Array, default: () => [] },
|
||||
loading: { type: Boolean, default: false },
|
||||
});
|
||||
@@ -13,11 +13,20 @@ const props = defineProps({
|
||||
const emit = defineEmits(["update"]);
|
||||
|
||||
const localPowers = ref([]);
|
||||
const activeKey = ref(null);
|
||||
const tableRef = ref(null);
|
||||
|
||||
// 多语言键按 power.<模块代码>.<操作编码> 自动生成,无需手动填写。
|
||||
watch(
|
||||
() => props.powers,
|
||||
(val) => {
|
||||
localPowers.value = val?.map((r) => ({ ...r })) || [];
|
||||
() => [props.powers, props.moduleCode],
|
||||
([val]) => {
|
||||
localPowers.value = (val || []).map((r) => ({
|
||||
...r,
|
||||
b_i18n:
|
||||
r.b_code && props.moduleCode
|
||||
? `power.${props.moduleCode}.${r.b_code}`
|
||||
: (r.b_i18n ?? null),
|
||||
}));
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
@@ -26,9 +35,10 @@ function emitUpdate() {
|
||||
emit("update", localPowers.value.map((r) => ({ ...r })));
|
||||
}
|
||||
|
||||
function addRow() {
|
||||
async function addRow() {
|
||||
const newId = createTempId();
|
||||
localPowers.value.push({
|
||||
b_id: createTempId(),
|
||||
b_id: newId,
|
||||
b_module_id: props.moduleId,
|
||||
b_code: "",
|
||||
b_name: "",
|
||||
@@ -37,6 +47,10 @@ function addRow() {
|
||||
b_canuse: 1,
|
||||
});
|
||||
emitUpdate();
|
||||
|
||||
activeKey.value = String(newId);
|
||||
await nextTick();
|
||||
tableRef.value?.scrollToRow(String(newId));
|
||||
}
|
||||
|
||||
function deleteRow(record) {
|
||||
@@ -45,26 +59,23 @@ function deleteRow(record) {
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{ dataIndex: "b_code", title: "操作编码", width: 160 },
|
||||
{ dataIndex: "b_name", title: "操作名称", width: 160 },
|
||||
{ dataIndex: "b_code", title: "操作编码", width: 160, editor: { onChange: () => emitUpdate() } },
|
||||
{ dataIndex: "b_name", title: "操作名称", width: 160, editor: { onChange: () => emitUpdate() } },
|
||||
{ dataIndex: "b_i18n", title: "多语言键", width: 160 },
|
||||
{ dataIndex: "b_xh", title: "排序", width: 80 },
|
||||
{ dataIndex: "b_xh", title: "排序", width: 80, editor: { onChange: () => emitUpdate() } },
|
||||
{
|
||||
dataIndex: "b_canuse",
|
||||
title: "启用",
|
||||
width: 70,
|
||||
align: "center",
|
||||
customCell: createToggleCell("b_canuse", (row) => {
|
||||
row.b_canuse = row.b_canuse === 1 ? 0 : 1;
|
||||
emitUpdate();
|
||||
}),
|
||||
editor: { type: "checkbox", onChange: () => emitUpdate() },
|
||||
},
|
||||
{
|
||||
dataIndex: "actions",
|
||||
title: "操作",
|
||||
width: 60,
|
||||
align: "center",
|
||||
customCell: createDeleteCell((row) => deleteRow(row)),
|
||||
actions: [{ type: "danger", label: "删除", confirm: { title: "确定删除该记录?", okText: "删除" }, onClick: (row) => deleteRow(row) }],
|
||||
},
|
||||
];
|
||||
</script>
|
||||
@@ -77,7 +88,7 @@ const columns = [
|
||||
</a-space>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<FmsTable :columns="columns" :data-source="localPowers" row-key="b_id" />
|
||||
<FmsTable ref="tableRef" :columns="columns" :data-source="localPowers" row-key="b_id" :active-row-key="activeKey" @row-click="(_, row) => (activeKey = String(row.b_id))" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -93,13 +104,13 @@ const columns = [
|
||||
display: flex;
|
||||
flex: none;
|
||||
gap: 8px;
|
||||
padding: 12px 20px;
|
||||
padding: 12px 20px 0;
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
padding: 16px 20px;
|
||||
padding: 12px 20px 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
+27
-27
@@ -2,7 +2,6 @@
|
||||
import { computed, ref, watch } from "vue";
|
||||
import FmsTable from "@/components/fms-table/FmsTable.vue";
|
||||
import { createTempId } from "@/utils/tempId";
|
||||
import { createDeleteCell, createSelectCell, createToggleCell } from "./cellRenderers";
|
||||
|
||||
const COMPONENT_OPTIONS = [
|
||||
{ label: "输入框", value: "input" },
|
||||
@@ -28,6 +27,7 @@ const props = defineProps({
|
||||
const emit = defineEmits(["update"]);
|
||||
|
||||
const localQuery = ref([]);
|
||||
const activeKey = ref(null);
|
||||
|
||||
watch(
|
||||
() => props.queryConfig,
|
||||
@@ -41,11 +41,6 @@ function emitUpdate() {
|
||||
emit("update", localQuery.value.map((r) => ({ ...r })));
|
||||
}
|
||||
|
||||
function setField(row, field, value) {
|
||||
row[field] = value === undefined || value === null ? null : value;
|
||||
emitUpdate();
|
||||
}
|
||||
|
||||
const availableFields = computed(() =>
|
||||
(props.fields || [])
|
||||
.filter((f) => f.b_canuse === 1)
|
||||
@@ -76,32 +71,31 @@ const columns = [
|
||||
dataIndex: "b_field_id",
|
||||
title: "字段",
|
||||
width: 160,
|
||||
customCell: createSelectCell({
|
||||
value: "b_field_id",
|
||||
getOptions: () => availableFields.value,
|
||||
onChange: (row, v) => setField(row, "b_field_id", v),
|
||||
placeholder: "选择字段",
|
||||
}),
|
||||
editor: {
|
||||
type: "select",
|
||||
options: () => availableFields.value,
|
||||
onChange: () => emitUpdate(),
|
||||
},
|
||||
},
|
||||
{
|
||||
dataIndex: "b_component",
|
||||
title: "查询组件",
|
||||
width: 140,
|
||||
customCell: createSelectCell({
|
||||
value: "b_component",
|
||||
getOptions: () => COMPONENT_OPTIONS,
|
||||
onChange: (row, v) => setField(row, "b_component", v),
|
||||
}),
|
||||
editor: {
|
||||
type: "select",
|
||||
options: COMPONENT_OPTIONS,
|
||||
onChange: () => emitUpdate(),
|
||||
},
|
||||
},
|
||||
{
|
||||
dataIndex: "b_operator",
|
||||
title: "操作符",
|
||||
width: 140,
|
||||
customCell: createSelectCell({
|
||||
value: "b_operator",
|
||||
getOptions: () => OPERATOR_OPTIONS,
|
||||
onChange: (row, v) => setField(row, "b_operator", v),
|
||||
}),
|
||||
editor: {
|
||||
type: "select",
|
||||
options: OPERATOR_OPTIONS,
|
||||
onChange: () => emitUpdate(),
|
||||
},
|
||||
},
|
||||
{ dataIndex: "b_default_value", title: "默认查询值", width: 140 },
|
||||
{
|
||||
@@ -109,7 +103,7 @@ const columns = [
|
||||
title: "启用",
|
||||
width: 60,
|
||||
align: "center",
|
||||
customCell: createToggleCell("b_canuse", (row) => setField(row, "b_canuse", row.b_canuse === 1 ? 0 : 1)),
|
||||
editor: { type: "checkbox", onChange: () => emitUpdate() },
|
||||
},
|
||||
{ dataIndex: "b_xh", title: "排序号", width: 80 },
|
||||
{
|
||||
@@ -117,7 +111,7 @@ const columns = [
|
||||
title: "操作",
|
||||
width: 60,
|
||||
align: "center",
|
||||
customCell: createDeleteCell((row) => deleteRow(row)),
|
||||
actions: [{ type: "danger", label: "删除", confirm: { title: "确定删除该记录?", okText: "删除" }, onClick: (row) => deleteRow(row) }],
|
||||
},
|
||||
];
|
||||
</script>
|
||||
@@ -130,7 +124,13 @@ const columns = [
|
||||
</a-space>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<FmsTable :columns="columns" :data-source="localQuery" row-key="b_id" />
|
||||
<FmsTable
|
||||
:columns="columns"
|
||||
:data-source="localQuery"
|
||||
row-key="b_id"
|
||||
:active-row-key="activeKey"
|
||||
@row-click="(ev, row) => (activeKey = row.b_id)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -146,13 +146,13 @@ const columns = [
|
||||
display: flex;
|
||||
flex: none;
|
||||
gap: 8px;
|
||||
padding: 12px 20px;
|
||||
padding: 12px 20px 0;
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
padding: 16px 20px;
|
||||
padding: 12px 20px 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
import { h } from "vue";
|
||||
import { Button, Popconfirm, Select } from "antdv-next";
|
||||
|
||||
// 单元格渲染器统一采用 stk-table-vue 的 customCell 写法:
|
||||
// customCell 会被当作函数式组件调用(props 含 row/col/rowIndex/cellValue),
|
||||
// 必须返回 h(...) 生成的 VNode,返回普通对象会渲染成 [object Object]。
|
||||
|
||||
// 删除操作单元格
|
||||
export function createDeleteCell(onDelete) {
|
||||
return (props) =>
|
||||
h(
|
||||
Popconfirm,
|
||||
{
|
||||
title: "确定删除该记录?",
|
||||
okText: "删除",
|
||||
okButtonProps: { danger: true },
|
||||
onConfirm: () => onDelete(props.row),
|
||||
},
|
||||
{
|
||||
default: () =>
|
||||
h(
|
||||
Button,
|
||||
{ type: "link", size: "small", danger: true, onClick: (e) => e.stopPropagation() },
|
||||
"删除",
|
||||
),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// 是否(0/1)切换单元格
|
||||
export function createToggleCell(field, onChange) {
|
||||
return (props) => {
|
||||
const checked = props.row[field] === 1;
|
||||
return h(
|
||||
"span",
|
||||
{
|
||||
style: { cursor: "pointer" },
|
||||
onClick: (e) => {
|
||||
e.stopPropagation();
|
||||
onChange(props.row, field);
|
||||
},
|
||||
},
|
||||
checked ? "是" : "否",
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
// 下拉选择单元格。getOptions 返回最新选项(保持响应式)。
|
||||
export function createSelectCell({ value, getOptions, onChange, placeholder, allowClear }) {
|
||||
return (props) =>
|
||||
h(Select, {
|
||||
value:
|
||||
props.row[value] != null && props.row[value] !== ""
|
||||
? String(props.row[value])
|
||||
: undefined,
|
||||
size: "small",
|
||||
style: { width: "100%" },
|
||||
options: getOptions(),
|
||||
allowClear: allowClear !== false,
|
||||
placeholder: placeholder || "请选择",
|
||||
"onUpdate:value": (v) => onChange(props.row, v),
|
||||
onClick: (e) => e.stopPropagation(),
|
||||
});
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup>
|
||||
import { computed, h, ref, watch } from "vue";
|
||||
import { message } from "antdv-next";
|
||||
import { message, Modal } from "antdv-next";
|
||||
import FmsTree from "@/components/FmsTree.vue";
|
||||
import { loadDataApi, saveObjectApi, nextIdApi, describeApi } from "@/services/api";
|
||||
import { cloneData, diffRows, allocateTemporaryIds, tableChange, hasChanges } from "@/utils/dataChanges";
|
||||
@@ -20,6 +20,26 @@ const treeLoading = ref(false);
|
||||
const selectedModuleId = ref(null);
|
||||
const skipLoad = ref(false);
|
||||
const activeTab = ref("basic");
|
||||
const contextNode = ref(null);
|
||||
const contextMenuOpen = ref(false);
|
||||
|
||||
const contextMenuItems = computed(() => {
|
||||
const node = contextNode.value;
|
||||
const items = [];
|
||||
if (!node) return { items };
|
||||
if (node.b_module_type === "directory") {
|
||||
items.push(
|
||||
{ label: "添加目录", key: "add-directory" },
|
||||
{ label: "添加页面", key: "add-page" },
|
||||
{ label: "添加数据", key: "add-data" },
|
||||
);
|
||||
} else if (node.b_module_type === "page") {
|
||||
items.push({ label: "添加数据", key: "add-data" });
|
||||
}
|
||||
if (items.length) items.push({ type: "divider" });
|
||||
items.push({ label: "删除", key: "delete", danger: true });
|
||||
return { items, onClick: onContextMenuClick };
|
||||
});
|
||||
|
||||
// 原始数据(加载时保存,用于 diff)
|
||||
const originals = ref({
|
||||
@@ -49,6 +69,21 @@ const configSaving = ref(false);
|
||||
// 当前选中模块对象(从 drafts.module 获取)
|
||||
const selectedModule = computed(() => drafts.value.module);
|
||||
|
||||
// 上级模块名称(从模块树解析 b_parent_id 对应的名称,用于基本信息展示)
|
||||
const parentModuleName = computed(() => {
|
||||
const pid = selectedModule.value?.b_parent_id;
|
||||
if (!pid) return "";
|
||||
const find = (nodes) => {
|
||||
for (const n of nodes || []) {
|
||||
if (String(n.b_id) === String(pid)) return n.b_name || n.b_code;
|
||||
const child = find(n.children);
|
||||
if (child) return child;
|
||||
}
|
||||
return "";
|
||||
};
|
||||
return find(treeData.value);
|
||||
});
|
||||
|
||||
// 可见 Tab
|
||||
const visibleTabs = computed(() => {
|
||||
const type = selectedModule.value?.b_module_type;
|
||||
@@ -62,10 +97,12 @@ const visibleTabs = computed(() => {
|
||||
{ key: "query", tab: "查询配置", icon: "Search" },
|
||||
);
|
||||
}
|
||||
tabs.push(
|
||||
{ key: "powers", tab: "权限配置", icon: "Lock" },
|
||||
{ key: "i18n", tab: "多语言", icon: "Translation" },
|
||||
);
|
||||
if (type === "page") {
|
||||
tabs.push({ key: "powers", tab: "权限配置", icon: "Lock" });
|
||||
}
|
||||
if (type !== "directory") {
|
||||
tabs.push({ key: "i18n", tab: "多语言", icon: "Translation" });
|
||||
}
|
||||
return tabs;
|
||||
});
|
||||
|
||||
@@ -210,21 +247,43 @@ async function onAddModule(parentId, moduleType = "directory") {
|
||||
const addModuleMenu = {
|
||||
items: [
|
||||
{ key: "directory", label: "目录" },
|
||||
{ key: "page", label: "页面" },
|
||||
{ key: "data", label: "数据" },
|
||||
],
|
||||
onClick: ({ key }) => onAddModule(null, key),
|
||||
};
|
||||
|
||||
// ---- 删除模块 ----
|
||||
async function onDeleteModule() {
|
||||
const moduleId = selectedModuleId.value;
|
||||
if (!moduleId) return;
|
||||
// ---- 右键菜单 ----
|
||||
function onTreeRightClick({ node }) {
|
||||
contextNode.value = node;
|
||||
}
|
||||
|
||||
function onContextMenuClick({ key }) {
|
||||
contextMenuOpen.value = false;
|
||||
const node = contextNode.value;
|
||||
contextNode.value = null;
|
||||
if (!node) return;
|
||||
|
||||
if (key.startsWith("add-")) {
|
||||
onAddModule(node.key, key.replace("add-", ""));
|
||||
} else if (key === "delete") {
|
||||
Modal.confirm({
|
||||
title: `确定删除「${node.title}」?`,
|
||||
content: "此操作不可恢复",
|
||||
okText: "删除",
|
||||
cancelText: "取消",
|
||||
okButtonProps: { danger: true },
|
||||
onOk: () => onDeleteNodeById(node.key),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function onDeleteNodeById(moduleId) {
|
||||
try {
|
||||
await saveObjectApi([{ table: "b_module", key_field: "b_id", deletes: [{ b_id: moduleId }] }]);
|
||||
message.success("模块已删除");
|
||||
selectedModuleId.value = null;
|
||||
drafts.value.module = null;
|
||||
if (selectedModuleId.value === moduleId) {
|
||||
selectedModuleId.value = null;
|
||||
drafts.value.module = null;
|
||||
}
|
||||
await loadTree();
|
||||
} catch (e) {
|
||||
message.error("删除失败:" + (e.message || e));
|
||||
@@ -412,23 +471,28 @@ loadTree();
|
||||
<div v-if="treeData.length === 0" class="mm-tree-empty">
|
||||
<a-empty description="暂无模块" />
|
||||
</div>
|
||||
<FmsTree
|
||||
<a-dropdown
|
||||
v-else
|
||||
class="mm-tree"
|
||||
:tree-data="treeData"
|
||||
:selected-keys="selectedModuleId ? [selectedModuleId] : []"
|
||||
:field-names="{ title: 'title', key: 'key', children: 'children' }"
|
||||
@select="onTreeSelect"
|
||||
:trigger="['contextmenu']"
|
||||
:menu="contextMenuItems"
|
||||
>
|
||||
<template #title="{ key, title }">
|
||||
<span class="mm-tree-node">
|
||||
<span>{{ title }}</span>
|
||||
<span class="mm-tree-node-actions">
|
||||
<a-button type="text" @click.stop="onAddModule(key)">+</a-button>
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
</FmsTree>
|
||||
<div style="flex:1;min-height:0;overflow:hidden;display:flex;flex-direction:column;">
|
||||
<FmsTree
|
||||
class="mm-tree"
|
||||
:tree-data="treeData"
|
||||
:selected-keys="selectedModuleId ? [selectedModuleId] : []"
|
||||
:field-names="{ title: 'title', key: 'key', children: 'children' }"
|
||||
@select="onTreeSelect"
|
||||
@right-click="onTreeRightClick"
|
||||
>
|
||||
<template #title="node">
|
||||
<span class="mm-tree-node">
|
||||
<span>{{ node.title }}</span>
|
||||
</span>
|
||||
</template>
|
||||
</FmsTree>
|
||||
</div>
|
||||
</a-dropdown>
|
||||
</aside>
|
||||
|
||||
<!-- 右侧详情 -->
|
||||
@@ -438,9 +502,9 @@ loadTree();
|
||||
<div class="mm-header">
|
||||
<a-space>
|
||||
<a-button type="primary" :loading="configSaving" @click="saveAll">保存</a-button>
|
||||
<a-popconfirm title="确定删除该模块?" @confirm="onDeleteModule">
|
||||
<a-button danger>删除模块</a-button>
|
||||
</a-popconfirm>
|
||||
<a-popconfirm title="确定删除该模块?" @confirm="onDeleteNodeById(selectedModuleId)">
|
||||
<a-button danger>删除模块</a-button>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
<div class="mm-header-center">
|
||||
<a-space size="middle">
|
||||
@@ -481,6 +545,7 @@ loadTree();
|
||||
v-if="tab.key === 'basic'"
|
||||
:module-id="String(selectedModule.b_id)"
|
||||
:data="drafts.module"
|
||||
:parent-name="parentModuleName"
|
||||
:loading="configSaving"
|
||||
@update="(v) => onUpdate('module', v)"
|
||||
/>
|
||||
@@ -524,6 +589,7 @@ loadTree();
|
||||
<ModulePowersPanel
|
||||
v-else-if="tab.key === 'powers'"
|
||||
:module-id="String(selectedModule.b_id)"
|
||||
:module-code="selectedModule?.b_code || ''"
|
||||
:powers="drafts.powers"
|
||||
:loading="configSaving"
|
||||
@update="(v) => onUpdate('powers', v)"
|
||||
@@ -585,7 +651,7 @@ loadTree();
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
padding: 8px 0;
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
.mm-tree-empty {
|
||||
@@ -602,15 +668,7 @@ loadTree();
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.mm-tree-node-actions {
|
||||
display: none;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.mm-tree-node:hover .mm-tree-node-actions {
|
||||
display: flex;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.mm-main {
|
||||
@@ -687,6 +745,11 @@ loadTree();
|
||||
:deep(.ant-tabs-tabpane) {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:deep(.ant-tabs-body),
|
||||
:deep(.ant-tabs-body-top) {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.mm-loading {
|
||||
|
||||
@@ -0,0 +1,322 @@
|
||||
<script setup>
|
||||
import { computed, h, onMounted, ref } from "vue";
|
||||
import { message, Switch, Button, Popconfirm, Tag } from "antdv-next";
|
||||
import { Plus } from "@lucide/vue";
|
||||
import FmsTable from "@/components/fms-table/FmsTable.vue";
|
||||
import { loadDataApi, saveObjectApi, nextIdApi } from "@/services/api";
|
||||
import { usePermissionStore } from "@/stores/permissions";
|
||||
|
||||
const permissionStore = usePermissionStore();
|
||||
|
||||
const data = ref([]);
|
||||
const loading = ref(false);
|
||||
const saving = ref(false);
|
||||
|
||||
const canCreate = computed(() => permissionStore.canPower("m_i18n_type", "create"));
|
||||
const canUpdate = computed(() => permissionStore.canPower("m_i18n_type", "update"));
|
||||
const canDelete = computed(() => permissionStore.canPower("m_i18n_type", "delete"));
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await loadDataApi("b_i18n_type", "", "b_xh ASC, b_id ASC");
|
||||
data.value = res.data || [];
|
||||
} catch (e) {
|
||||
message.error("加载多语言类型失败:" + (e?.message || e));
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadData);
|
||||
|
||||
/* ---------- 行内单元格渲染 ---------- */
|
||||
function createDefaultCell() {
|
||||
return (props) =>
|
||||
props.row.b_default === 1
|
||||
? h(Tag, { color: "blue" }, "默认")
|
||||
: h("span", { class: "muted" }, "—");
|
||||
}
|
||||
|
||||
function createCanuseCell() {
|
||||
return (props) => {
|
||||
const row = props.row;
|
||||
return h(Switch, {
|
||||
checked: row.b_canuse === 1,
|
||||
size: "small",
|
||||
disabled: !canUpdate.value,
|
||||
"onUpdate:checked": (val) => {
|
||||
const next = val ? 1 : 0;
|
||||
if (row.b_canuse === next) return;
|
||||
row.b_canuse = next;
|
||||
saveRow(row, { b_canuse: next });
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function createActionsCell() {
|
||||
return (props) => {
|
||||
const row = props.row;
|
||||
const isDefault = row.b_default === 1;
|
||||
const children = [];
|
||||
if (canUpdate.value) {
|
||||
children.push(
|
||||
h(
|
||||
Button,
|
||||
{ type: "link", size: "small", onClick: (e) => { e.stopPropagation(); openEdit(row); } },
|
||||
"编辑",
|
||||
),
|
||||
);
|
||||
}
|
||||
if (canDelete.value && !isDefault) {
|
||||
children.push(
|
||||
h(
|
||||
Popconfirm,
|
||||
{
|
||||
title: "确定删除该语言?",
|
||||
okText: "删除",
|
||||
okButtonProps: { danger: true },
|
||||
onConfirm: () => removeRow(row),
|
||||
},
|
||||
{
|
||||
default: () =>
|
||||
h(
|
||||
Button,
|
||||
{ type: "link", size: "small", danger: true, onClick: (e) => e.stopPropagation() },
|
||||
"删除",
|
||||
),
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
return h("span", { class: "actions-cell" }, children);
|
||||
};
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{ dataIndex: "b_code", title: "简称 / 编码", width: 150 },
|
||||
{ dataIndex: "b_name", title: "名称", width: 160 },
|
||||
{ dataIndex: "b_default", title: "默认", width: 80, align: "center", customCell: createDefaultCell() },
|
||||
{ dataIndex: "b_canuse", title: "是否启用", width: 100, align: "center", customCell: createCanuseCell() },
|
||||
{ dataIndex: "b_xh", title: "排序", width: 80, align: "center" },
|
||||
{ dataIndex: "actions", title: "操作", width: 130, align: "center", customCell: createActionsCell() },
|
||||
];
|
||||
|
||||
/* ---------- 行内开关即时保存 ---------- */
|
||||
async function saveRow(row, patch) {
|
||||
try {
|
||||
await saveObjectApi([
|
||||
{ table: "b_i18n_type", key_field: "b_id", updates: [{ b_id: row.b_id, ...patch }] },
|
||||
]);
|
||||
message.success("已更新");
|
||||
} catch (e) {
|
||||
message.error("更新失败:" + (e?.message || e));
|
||||
await loadData();
|
||||
}
|
||||
}
|
||||
|
||||
async function removeRow(row) {
|
||||
if (row.b_default === 1) return;
|
||||
saving.value = true;
|
||||
try {
|
||||
await saveObjectApi([
|
||||
{ table: "b_i18n_type", key_field: "b_id", deletes: [{ b_id: row.b_id }] },
|
||||
]);
|
||||
message.success("删除成功");
|
||||
await loadData();
|
||||
} catch (e) {
|
||||
message.error("删除失败:" + (e?.message || e));
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- 新增 / 编辑弹窗 ---------- */
|
||||
const modalVisible = ref(false);
|
||||
const modalMode = ref("create");
|
||||
const editingId = ref(null);
|
||||
const editingDefault = ref(false);
|
||||
const form = ref({
|
||||
b_code: "",
|
||||
b_name: "",
|
||||
b_xh: 0,
|
||||
defaultOn: false,
|
||||
canuseOn: true,
|
||||
});
|
||||
|
||||
function openCreate() {
|
||||
modalMode.value = "create";
|
||||
editingId.value = null;
|
||||
editingDefault.value = false;
|
||||
form.value = { b_code: "", b_name: "", b_xh: 0, defaultOn: false, canuseOn: true };
|
||||
modalVisible.value = true;
|
||||
}
|
||||
|
||||
function openEdit(row) {
|
||||
modalMode.value = "edit";
|
||||
editingId.value = row.b_id;
|
||||
editingDefault.value = row.b_default === 1;
|
||||
form.value = {
|
||||
b_code: row.b_code,
|
||||
b_name: row.b_name,
|
||||
b_xh: row.b_xh ?? 0,
|
||||
defaultOn: row.b_default === 1,
|
||||
canuseOn: row.b_canuse === 1,
|
||||
};
|
||||
modalVisible.value = true;
|
||||
}
|
||||
|
||||
async function saveForm() {
|
||||
const f = form.value;
|
||||
if (!f.b_code || !f.b_code.trim()) {
|
||||
message.warning("请填写语言编码");
|
||||
return;
|
||||
}
|
||||
if (!f.b_name || !f.b_name.trim()) {
|
||||
message.warning("请填写语言名称");
|
||||
return;
|
||||
}
|
||||
saving.value = true;
|
||||
try {
|
||||
const isNew = modalMode.value === "create" || !editingId.value;
|
||||
const b_id = isNew ? (await nextIdApi(1)).data : editingId.value;
|
||||
const row = {
|
||||
b_id,
|
||||
b_code: f.b_code.trim(),
|
||||
b_name: f.b_name.trim(),
|
||||
b_canuse: f.canuseOn ? 1 : 0,
|
||||
b_default: f.defaultOn ? 1 : 0,
|
||||
b_xh: f.b_xh ?? 0,
|
||||
};
|
||||
const req = { table: "b_i18n_type", key_field: "b_id", inserts: [], updates: [], deletes: [] };
|
||||
if (isNew) req.inserts.push(row);
|
||||
else req.updates.push(row);
|
||||
// 保证仅一个默认语言:若本次设为默认,则将其它语言取消默认
|
||||
if (row.b_default === 1) {
|
||||
for (const r of data.value) {
|
||||
if (r.b_id !== b_id && r.b_default === 1) {
|
||||
req.updates.push({ b_id: r.b_id, b_default: 0 });
|
||||
}
|
||||
}
|
||||
}
|
||||
await saveObjectApi([req]);
|
||||
message.success("保存成功");
|
||||
modalVisible.value = false;
|
||||
await loadData();
|
||||
} catch (e) {
|
||||
message.error("保存失败:" + (e?.message || e));
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="i18n-type-page">
|
||||
<div class="panel-card">
|
||||
<div class="panel-toolbar">
|
||||
<a-button v-if="canCreate" type="primary" @click="openCreate">
|
||||
<Plus :size="16" /> 新增语言
|
||||
</a-button>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<FmsTable
|
||||
:columns="columns"
|
||||
:data-source="data"
|
||||
row-key="b_id"
|
||||
:loading="loading"
|
||||
:pagination="false"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a-modal
|
||||
v-model:open="modalVisible"
|
||||
:title="modalMode === 'create' ? '新增语言' : '编辑语言'"
|
||||
:confirm-loading="saving"
|
||||
@ok="saveForm"
|
||||
@cancel="modalVisible = false"
|
||||
>
|
||||
<a-form layout="vertical">
|
||||
<a-form-item label="语言编码(简称)" required>
|
||||
<a-input
|
||||
v-model:value="form.b_code"
|
||||
:disabled="editingDefault"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="语言名称" required>
|
||||
<a-input v-model:value="form.b_name" />
|
||||
</a-form-item>
|
||||
<a-form-item label="是否默认">
|
||||
<a-switch v-model:checked="form.defaultOn" :disabled="editingDefault" />
|
||||
<span class="form-tip">设为默认后,其它语言将自动取消默认</span>
|
||||
</a-form-item>
|
||||
<a-form-item label="是否启用">
|
||||
<a-switch v-model:checked="form.canuseOn" />
|
||||
</a-form-item>
|
||||
<a-form-item label="排序">
|
||||
<a-input-number v-model:value="form.b_xh" :min="0" style="width: 100%" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.i18n-type-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.panel-card {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
flex-direction: column;
|
||||
background: #fff;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 4px rgb(0 0 0 / 6%);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.panel-toolbar {
|
||||
display: flex;
|
||||
flex: none;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
|
||||
&__title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #1f2329;
|
||||
}
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
.actions-cell {
|
||||
display: inline-flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.muted {
|
||||
color: #c0c4cc;
|
||||
}
|
||||
|
||||
.form-tip {
|
||||
margin-left: 8px;
|
||||
font-size: 12px;
|
||||
color: #646a73;
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user