20260729173317

This commit is contained in:
oneao committed 2026-07-29 17:33:21 +08:00
1 parent f6b79ea09a
commit 1c839b707a
26 files changed
+1902 -29

No files matched your search

@@ -0,0 +1,250 @@
<script setup>
import { computed, onMounted, ref } from "vue";
import { message } from "antdv-next";
import { RefreshCw } from "@lucide/vue";
import FmsModuleListPage from "@/components/fms-module-list/FmsModuleListPage.vue";
import FmsTree from "@/components/FmsTree.vue";
import { escapeSqlText } from "@/components/fms-module-list/moduleListUtils";
import { loadDataApi } from "@/services/api";
import { buildTree, sortTree } from "@/utils/tree";
const ALL_CATEGORIES_KEY = "__all_categories__";
const CATEGORY_COLUMNS = [
"b_id",
"b_parent_id",
"b_code",
"b_name",
"b_level",
"b_path",
"b_xh",
"b_canuse",
];
const categories = ref([]);
const categoryLoading = ref(false);
const selectedCategoryKey = ref(ALL_CATEGORIES_KEY);
const expandedCategoryKeys = ref([ALL_CATEGORIES_KEY]);
const categoryById = computed(
() => new Map(categories.value.map((item) => [String(item.b_id), item])),
);
const categoryTree = computed(() => {
const roots = buildTree(categories.value, {
mapNode: (item) => ({
key: String(item.b_id),
title: Number(item.b_canuse) === 1 ? item.b_name : `${item.b_name}(停用)`,
b_id: String(item.b_id),
b_parent_id: item.b_parent_id == null ? null : String(item.b_parent_id),
b_xh: item.b_xh,
children: [],
}),
});
sortTree(roots);
return [
{
key: ALL_CATEGORIES_KEY,
title: "全部合作伙伴",
b_xh: 0,
children: roots,
},
];
});
const categoryFilterCondition = computed(() => {
if (selectedCategoryKey.value === ALL_CATEGORIES_KEY) return "";
const ids = collectDescendantIds(selectedCategoryKey.value);
const literals = ids.map((id) => `N'${escapeSqlText(id)}'`);
return literals.length ? `[b_cate_id] IN (${literals.join(", ")})` : "1 = 0";
});
const columnExtensions = computed(() => ({
b_cate_id: {
formatter: ({ value }) => categoryById.value.get(String(value || ""))?.b_name || value || "",
},
}));
function collectDescendantIds(rootId) {
const childrenByParent = new Map();
for (const item of categories.value) {
const parentId = item.b_parent_id == null ? "" : String(item.b_parent_id);
if (!childrenByParent.has(parentId)) childrenByParent.set(parentId, []);
childrenByParent.get(parentId).push(String(item.b_id));
}
const result = [];
const visited = new Set();
const pending = [String(rootId)];
while (pending.length) {
const id = pending.pop();
if (visited.has(id)) continue;
visited.add(id);
result.push(id);
for (const childId of childrenByParent.get(id) || []) pending.push(childId);
}
return result;
}
async function loadCategories() {
categoryLoading.value = true;
try {
const response = await loadDataApi(
"b_othercompany_bmfl",
"",
"b_path ASC, b_xh ASC, b_id ASC",
CATEGORY_COLUMNS,
);
categories.value = response.data || [];
const existingKeys = new Set(categories.value.map((item) => String(item.b_id)));
if (
selectedCategoryKey.value !== ALL_CATEGORIES_KEY &&
!existingKeys.has(selectedCategoryKey.value)
) {
selectedCategoryKey.value = ALL_CATEGORIES_KEY;
}
const parentKeys = categories.value
.map((item) => (item.b_parent_id == null ? "" : String(item.b_parent_id)))
.filter(Boolean);
expandedCategoryKeys.value = [
...new Set([...expandedCategoryKeys.value, ALL_CATEGORIES_KEY, ...parentKeys]),
];
} catch (error) {
message.error("分类加载失败:" + (error?.message || error));
} finally {
categoryLoading.value = false;
}
}
function onCategorySelect(keys) {
if (keys.length) selectedCategoryKey.value = String(keys[0]);
}
onMounted(loadCategories);
</script>
<template>
<div class="othercompany-page">
<a-splitter class="othercompany-splitter" lazy>
<a-splitter-panel :default-size="250" :min="210" :max="380" collapsible>
<aside class="category-panel">
<header class="category-header">
<div class="category-title">合作伙伴分类</div>
<a-tooltip title="刷新分类">
<a-button
type="text"
:disabled="categoryLoading"
aria-label="刷新分类"
@click="loadCategories"
>
<RefreshCw :size="15" />
</a-button>
</a-tooltip>
</header>
<div class="category-tree-stage">
<FmsTree
v-model:expanded-keys="expandedCategoryKeys"
:tree-data="categoryTree"
:selected-keys="[selectedCategoryKey]"
show-line
@select="onCategorySelect"
/>
<div v-if="categoryLoading" class="category-loading"><a-spin size="small" /></div>
</div>
</aside>
</a-splitter-panel>
<a-splitter-panel :min="520">
<main class="partner-panel">
<FmsModuleListPage
data-code="b_othercompany"
:fixed-search-condition="categoryFilterCondition"
:column-extensions="columnExtensions"
/>
</main>
</a-splitter-panel>
</a-splitter>
</div>
</template>
<style scoped lang="scss">
.othercompany-page,
.othercompany-splitter {
width: 100%;
height: 100%;
min-width: 0;
min-height: 0;
}
.othercompany-page {
overflow: hidden;
border: 1px solid var(--fms-border);
border-radius: 6px;
background: var(--fms-surface);
}
.category-panel,
.partner-panel {
display: flex;
width: 100%;
height: 100%;
min-width: 0;
min-height: 0;
flex-direction: column;
}
.category-header {
display: flex;
min-height: 50px;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 4px 6px 4px 12px;
border-bottom: 1px solid var(--fms-border);
}
.category-title {
min-width: 0;
font-size: 13px;
font-weight: 600;
white-space: nowrap;
}
.category-header :deep(.ant-btn) {
width: 28px;
height: 28px;
padding: 0;
}
.category-tree-stage {
position: relative;
flex: 1;
min-height: 0;
overflow: auto;
padding: 6px 4px;
}
.category-tree-stage :deep(.ant-tree) {
min-width: 100%;
}
.category-tree-stage :deep(.ant-tree-node-content-wrapper) {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.category-loading {
position: absolute;
inset: 0;
display: grid;
place-items: center;
background: rgb(255 255 255 / 70%);
}
.partner-panel :deep(.fms-module-list-page) {
border: 0;
border-radius: 0;
}
</style>
@@ -0,0 +1,28 @@
import { createTempId } from "@/utils/tempId";
function defaultFieldId(fields) {
const field = (fields || []).find(
(item) =>
Number(item.b_canuse) === 1 &&
item.b_field !== "b_id" &&
(item.b_type === "input" || item.b_type === "textarea"),
);
return field?.b_id != null ? String(field.b_id) : null;
}
export function createAutoCodeDraft(moduleId, fields, enabled = false) {
return {
b_id: createTempId(),
b_module_id: String(moduleId),
b_field_id: defaultFieldId(fields),
b_prefix: "",
b_date_format: "yyyyMM",
b_separator: "-",
b_sequence_width: 5,
b_reset_type: "month",
b_start_value: 1,
b_current_period: null,
b_current_value: 0,
b_canuse: enabled ? 1 : 0,
};
}
@@ -0,0 +1,230 @@
<script setup>
import { computed, ref, watch } from "vue";
import { createAutoCodeDraft } from "../autoCode";
const DATE_FORMAT_OPTIONS = [
{ label: "不使用日期", value: "" },
{ label: "yyyy", value: "yyyy" },
{ label: "yy", value: "yy" },
{ label: "yyyyMM", value: "yyyyMM" },
{ label: "yyMM", value: "yyMM" },
{ label: "yyyyMMdd", value: "yyyyMMdd" },
{ label: "yyyy-MM", value: "yyyy-MM" },
{ label: "yyyy-MM-dd", value: "yyyy-MM-dd" },
{ label: "yyyy/MM", value: "yyyy/MM" },
{ label: "yyyy/MM/dd", value: "yyyy/MM/dd" },
];
const RESET_OPTIONS = [
{ label: "不重置", value: "none" },
{ label: "每年", value: "year" },
{ label: "每月", value: "month" },
{ label: "每日", value: "day" },
];
const props = defineProps({
moduleId: { type: String, required: true },
autoCode: { type: Array, default: () => [] },
fields: { type: Array, default: () => [] },
loading: { type: Boolean, default: false },
});
const emit = defineEmits(["update"]);
const localRow = ref(null);
const enabled = computed(() => Number(localRow.value?.b_canuse) === 1);
const fieldOptions = computed(() => {
const options = [];
for (const field of props.fields || []) {
if (Number(field.b_canuse) !== 1) continue;
if (field.b_field === "b_id") continue;
if (field.b_type !== "input" && field.b_type !== "textarea") continue;
options.push({
label: `${field.b_name || field.b_field} (${field.b_field})`,
value: String(field.b_id),
});
}
return options;
});
function notifyUpdate() {
emit("update", localRow.value ? [{ ...localRow.value }] : []);
}
watch(
() => props.autoCode,
(rows) => {
const row = rows && rows.length ? rows[0] : null;
localRow.value = row ? { ...row } : createAutoCodeDraft(props.moduleId, props.fields, false);
},
{ immediate: true },
);
function setRowValue(field, value) {
if (!localRow.value) return;
localRow.value[field] = value;
notifyUpdate();
}
function pad(value) {
return String(value).padStart(2, "0");
}
function formatDate(date, format) {
const yyyy = String(date.getFullYear());
const yy = yyyy.slice(-2);
const MM = pad(date.getMonth() + 1);
const dd = pad(date.getDate());
return String(format || "")
.replaceAll("yyyy", yyyy)
.replaceAll("yy", yy)
.replaceAll("MM", MM)
.replaceAll("dd", dd);
}
function currentPeriod(date, resetType) {
const yyyy = String(date.getFullYear());
const MM = pad(date.getMonth() + 1);
const dd = pad(date.getDate());
if (resetType === "none") return "*";
if (resetType === "year") return yyyy;
if (resetType === "day") return yyyy + MM + dd;
return yyyy + MM;
}
const preview = computed(() => {
if (!localRow.value) return "";
const today = new Date();
const startValue = Number(localRow.value.b_start_value) || 1;
const storedValue = Number(localRow.value.b_current_value) || 0;
const activePeriod = currentPeriod(today, localRow.value.b_reset_type);
const sequence = localRow.value.b_current_period === activePeriod ? storedValue + 1 : startValue;
const parts = [];
if (localRow.value.b_prefix) parts.push(localRow.value.b_prefix);
const dateText = formatDate(today, localRow.value.b_date_format);
if (dateText) parts.push(dateText);
parts.push(String(sequence).padStart(Number(localRow.value.b_sequence_width) || 1, "0"));
return parts.join(localRow.value.b_separator || "");
});
</script>
<template>
<div class="auto-code-panel">
<div class="panel-body">
<a-form layout="vertical" class="auto-code-form" :disabled="loading">
<template v-if="localRow">
<a-row :gutter="16">
<a-col :span="8">
<a-form-item label="业务编号字段" :required="enabled">
<a-select
:value="String(localRow.b_field_id || '')"
:options="fieldOptions"
:disabled="loading"
@update:value="(value) => setRowValue('b_field_id', value)"
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="固定前缀">
<a-input
:value="localRow.b_prefix"
:disabled="loading"
:maxlength="100"
@update:value="(value) => setRowValue('b_prefix', value)"
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="日期格式">
<a-auto-complete
:value="localRow.b_date_format"
:options="DATE_FORMAT_OPTIONS"
:disabled="loading"
@update:value="(value) => setRowValue('b_date_format', value)"
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="分隔符">
<a-input
:value="localRow.b_separator"
:disabled="loading"
:maxlength="10"
@update:value="(value) => setRowValue('b_separator', value)"
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="流水号位数" required>
<a-input-number
:value="localRow.b_sequence_width"
:disabled="loading"
:min="1"
:max="18"
:precision="0"
@update:value="(value) => setRowValue('b_sequence_width', value)"
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="起始值" required>
<a-input-number
:value="localRow.b_start_value"
:disabled="loading"
:min="0"
:precision="0"
@update:value="(value) => setRowValue('b_start_value', value)"
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="重置周期" required>
<a-select
:value="localRow.b_reset_type"
:options="RESET_OPTIONS"
:disabled="loading"
@update:value="(value) => setRowValue('b_reset_type', value)"
/>
</a-form-item>
</a-col>
<a-col :span="16">
<a-form-item label="编号预览">
<div class="code-preview">{{ preview }}</div>
</a-form-item>
</a-col>
</a-row>
</template>
</a-form>
</div>
</div>
</template>
<style scoped lang="scss">
.auto-code-panel {
display: flex;
flex-direction: column;
height: 100%;
}
.panel-body {
flex: 1;
overflow: auto;
padding: 16px 20px;
}
.auto-code-form {
max-width: 960px;
}
.code-preview {
min-height: 32px;
padding: 4px 11px;
border: 1px solid var(--fms-border, #d9d9d9);
border-radius: 6px;
background: var(--fms-bg, #f5f5f5);
color: var(--fms-text, #1f2937);
font-family: Consolas, monospace;
line-height: 22px;
}
</style>
@@ -47,7 +47,6 @@ function onRowOrderChange(rows) {
const availableFields = computed(() => {
const options = [];
for (const field of props.fields || []) {
if (field.b_canuse !== 1) continue;
options.push({ label: field.b_name || field.b_field, value: String(field.b_id) });
}
return options;
@@ -78,6 +77,13 @@ const columns = shallowRef([
options: () => availableFields.value,
},
},
{
dataIndex: "b_visible",
title: "显示",
width: 60,
align: "center",
editor: { type: "checkbox" },
},
{
dataIndex: "b_required",
title: "必填",
@@ -46,7 +46,11 @@ watch(
);
function emitUpdate() {
emit("update", localFields.value);
const rows = [];
for (const row of localFields.value) {
rows.push({ ...row });
}
emit("update", rows);
}
// 编辑器已直接更新当前行,这里只向父级同步草稿。
@@ -47,7 +47,6 @@ function onRowOrderChange(rows) {
const availableFields = computed(() => {
const options = [];
for (const field of props.fields || []) {
if (field.b_canuse !== 1) continue;
options.push({ label: field.b_name || field.b_field, value: String(field.b_id) });
}
return options;
@@ -59,7 +59,6 @@ function onRowOrderChange(rows) {
const availableFields = computed(() => {
const options = [];
for (const field of props.fields || []) {
if (field.b_canuse !== 1) continue;
options.push({ label: field.b_name || field.b_field, value: String(field.b_id) });
}
return options;
@@ -27,8 +27,10 @@ import ModuleFieldsPanel from "./components/ModuleFieldsPanel.vue";
import ModuleListConfigPanel from "./components/ModuleListConfigPanel.vue";
import ModuleEditConfigPanel from "./components/ModuleEditConfigPanel.vue";
import ModuleQueryConfigPanel from "./components/ModuleQueryConfigPanel.vue";
import ModuleAutoCodePanel from "./components/ModuleAutoCodePanel.vue";
import ModulePowersPanel from "./components/ModulePowersPanel.vue";
import ModuleI18nPanel from "./components/ModuleI18nPanel.vue";
import { createAutoCodeDraft } from "./autoCode";
// ---- 状态 ----
const appStore = useAppStore();
@@ -63,6 +65,7 @@ const originals = ref({
listConfig: [],
editConfig: [],
queryConfig: [],
autoCode: [],
powers: [],
});
@@ -74,6 +77,7 @@ const drafts = ref({
listConfig: [],
editConfig: [],
queryConfig: [],
autoCode: [],
powers: [],
});
@@ -90,6 +94,7 @@ function setI18nPanelRef(instance) {
// 当前选中模块对象(从 drafts.module 获取)
const selectedModule = computed(() => drafts.value.module);
const autoCodeEnabled = computed(() => Number(drafts.value.autoCode?.[0]?.b_canuse) === 1);
const busy = computed(() => configLoading.value || configSaving.value || configSyncing.value);
const syncDisabled = computed(
() => busy.value || !String(selectedModule.value?.b_viewtable || "").trim(),
@@ -185,6 +190,7 @@ const visibleTabs = computed(() => {
{ key: "list", tab: "列表配置" },
{ key: "edit", tab: "编辑配置" },
{ key: "query", tab: "查询配置" },
{ key: "auto-code", tab: "自动编码" },
);
}
if (type === "page") {
@@ -252,6 +258,7 @@ async function loadModuleConfig(moduleId) {
let listResponse;
let editResponse;
let queryResponse;
let autoCodeResponse;
let powersResponse;
if (moduleType === "data") {
@@ -262,6 +269,7 @@ async function loadModuleConfig(moduleId) {
loadDataApi("s_module_field_list", `b_module_id = ${id}`, "b_xh ASC, b_id ASC"),
loadDataApi("s_module_field_edit", `b_module_id = ${id}`, "b_xh ASC, b_id ASC"),
loadDataApi("s_module_field_query", `b_module_id = ${id}`, "b_xh ASC, b_id ASC"),
loadDataApi("s_module_auto_code", `b_module_id = ${id}`, "b_id ASC"),
]);
moduleResponse = responses[0];
fieldsResponse = responses[1];
@@ -269,6 +277,7 @@ async function loadModuleConfig(moduleId) {
listResponse = responses[3];
editResponse = responses[4];
queryResponse = responses[5];
autoCodeResponse = responses[6];
} else if (moduleType === "page") {
const responses = await Promise.all([
loadDataApi("s_module", `b_id = ${id}`),
@@ -289,6 +298,7 @@ async function loadModuleConfig(moduleId) {
listConfig: listResponse ? listResponse.data || [] : [],
editConfig: editResponse ? editResponse.data || [] : [],
queryConfig: queryResponse ? queryResponse.data || [] : [],
autoCode: autoCodeResponse ? autoCodeResponse.data || [] : [],
powers: powersResponse ? powersResponse.data || [] : [],
};
originals.value = original;
@@ -309,11 +319,20 @@ async function loadModuleConfig(moduleId) {
}
// ---- 树节点选择 ----
function onTreeSelect(keys) {
function onTreeSelect(keys, info) {
if (busy.value) return;
if (keys.length > 0) {
selectedModuleId.value = String(keys[0]);
}
const nodeKey = info?.node?.key ?? keys[0];
if (nodeKey == null) return;
const key = String(nodeKey);
selectedModuleId.value = key;
const node = findNode(key);
if (!node?.children?.length) return;
const expandedKeys = expandedTreeKeys.value.map(String);
expandedTreeKeys.value = expandedKeys.includes(key)
? expandedKeys.filter((item) => item !== key)
: [...expandedKeys, key];
}
watch(selectedModuleId, (id) => {
@@ -337,6 +356,128 @@ function onUpdate(section, newData) {
drafts.value[section] = newData;
}
function isFieldEnabled(field) {
if (!field) return false;
return Number(field.b_canuse) === 1;
}
function applyDisabledFieldDefaults(previousFields, nextFields) {
const previousEnabledStates = new Map();
for (const field of previousFields || []) {
if (field.b_id != null) {
previousEnabledStates.set(String(field.b_id), isFieldEnabled(field));
}
}
const disabledFieldIds = new Set();
for (const field of nextFields || []) {
if (field.b_id == null) continue;
const fieldId = String(field.b_id);
if (previousEnabledStates.get(fieldId) && !isFieldEnabled(field)) {
disabledFieldIds.add(fieldId);
}
}
if (disabledFieldIds.size === 0) return;
for (const row of drafts.value.listConfig || []) {
if (disabledFieldIds.has(String(row.b_field_id))) row.b_visible = 0;
}
for (const row of drafts.value.editConfig || []) {
if (disabledFieldIds.has(String(row.b_field_id))) row.b_visible = 0;
}
for (const row of drafts.value.queryConfig || []) {
if (disabledFieldIds.has(String(row.b_field_id))) row.b_canuse = 0;
}
}
function onFieldsUpdate(newFields) {
applyDisabledFieldDefaults(drafts.value.fields, newFields);
drafts.value.fields = newFields;
}
function onAutoCodeUpdate(rows) {
const previousRule = drafts.value.autoCode?.[0];
const previousFieldId =
previousRule && Number(previousRule.b_canuse) === 1 ? previousRule.b_field_id : null;
drafts.value.autoCode = rows;
const rule = rows && rows.length ? rows[0] : null;
const nextFieldId = rule && Number(rule.b_canuse) === 1 ? rule.b_field_id : null;
if (previousFieldId != null && String(previousFieldId) !== String(nextFieldId)) {
const original = (originals.value.editConfig || []).find(
(config) => String(config.b_field_id) === String(previousFieldId),
);
for (const config of drafts.value.editConfig || []) {
if (String(config.b_field_id) === String(previousFieldId)) {
config.b_readonly = Number(original?.b_readonly) === 1 ? 1 : 0;
}
}
}
if (nextFieldId == null) return;
for (const config of drafts.value.editConfig || []) {
if (String(config.b_field_id) === String(nextFieldId)) {
config.b_readonly = 1;
}
}
}
function setAutoCodeEnabled(enabled) {
const current = drafts.value.autoCode?.[0];
const rule = current
? { ...current, b_canuse: enabled ? 1 : 0 }
: createAutoCodeDraft(selectedModule.value?.b_id, drafts.value.fields, enabled);
onAutoCodeUpdate([rule]);
}
function validateAutoCode() {
const rows = drafts.value.autoCode || [];
const rule = rows.length ? rows[0] : null;
if (!rule || Number(rule.b_canuse) !== 1) return "";
if (!String(selectedModule.value?.b_savetable || "").trim()) {
return "启用自动编码前请先配置保存表";
}
const field = (drafts.value.fields || []).find(
(item) => String(item.b_id) === String(rule.b_field_id),
);
if (!field || Number(field.b_canuse) !== 1) return "请选择已启用的业务编号字段";
if (field.b_type !== "input" && field.b_type !== "textarea") {
return "业务编号字段必须使用文本控件类型";
}
if (!Number.isInteger(Number(rule.b_start_value)) || Number(rule.b_start_value) < 0) {
return "起始值必须是大于或等于 0 的整数";
}
if (!["none", "year", "month", "day"].includes(rule.b_reset_type)) {
return "请选择正确的重置周期";
}
const width = Number(rule.b_sequence_width);
if (!Number.isInteger(width) || width < 1 || width > 18) {
return "流水号位数必须是 1 到 18 之间的整数";
}
if (String(rule.b_prefix || "").length > 100) return "固定前缀长度不能超过 100 个字符";
if (String(rule.b_separator || "").length > 10) return "分隔符长度不能超过 10 个字符";
const dateFormat = String(rule.b_date_format || "").trim();
if (dateFormat.length > 30) return "日期格式长度不能超过 30 个字符";
const dateLiterals = dateFormat
.replaceAll("yyyy", "")
.replaceAll("yy", "")
.replaceAll("MM", "")
.replaceAll("dd", "");
if (/[A-Za-z]/.test(dateLiterals)) return "日期格式只支持 yyyy、yy、MM 和 dd";
const hasYear = dateFormat.includes("yyyy") || dateFormat.includes("yy");
const hasMonth = dateFormat.includes("MM");
const hasDay = dateFormat.includes("dd");
if (rule.b_reset_type === "year" && !hasYear) return "按年重置时,日期格式必须包含年份";
if (rule.b_reset_type === "month" && (!hasYear || !hasMonth)) {
return "按月重置时,日期格式必须包含年份和月份";
}
if (rule.b_reset_type === "day" && (!hasYear || !hasMonth || !hasDay)) {
return "按日重置时,日期格式必须包含年月日";
}
for (const config of drafts.value.editConfig || []) {
if (String(config.b_field_id) === String(rule.b_field_id)) config.b_readonly = 1;
}
return "";
}
function onSetField(field, value) {
if (drafts.value.module) {
drafts.value.module[field] = value;
@@ -388,6 +529,7 @@ async function onAddModule(parentId, moduleType = "directory") {
listConfig: [],
editConfig: [],
queryConfig: [],
autoCode: [],
powers: [],
};
drafts.value = cloneData(originals.value);
@@ -469,6 +611,7 @@ async function onDeleteNodeById(moduleId) {
deleteByModuleId("s_module_field_list"),
deleteByModuleId("s_module_field_edit"),
deleteByModuleId("s_module_field_query"),
deleteByModuleId("s_module_auto_code"),
deleteByModuleId("s_module_power"),
deleteByModuleId("s_module_field"),
deleteByModuleId("s_module_field_group"),
@@ -497,6 +640,12 @@ async function saveAll() {
try {
const saveReqs = [];
const autoCodeError = validateAutoCode();
if (autoCodeError) {
message.warning(autoCodeError);
return;
}
// s_module 主表
const moduleData = drafts.value.module;
const moduleOriginal = originals.value.module;
@@ -543,18 +692,25 @@ async function saveAll() {
saveReqs.push(moduleRequest);
}
// 子表:fields, listConfig, editConfig, queryConfig, powers
// 子表:fields, listConfig, editConfig, queryConfig, autoCode, powers
const subTables = [
["s_module_field", "fields"],
["s_module_field_list", "listConfig"],
["s_module_field_edit", "editConfig"],
["s_module_field_query", "queryConfig"],
["s_module_auto_code", "autoCode"],
["s_module_power", "powers"],
];
for (const [table, section] of subTables) {
const current = drafts.value[section] || [];
const previous = originals.value[section] || [];
const changes = diffRows(current, previous);
if (table === "s_module_auto_code") {
changes.updates = changes.updates.map((row) => {
const { b_current_period: _period, b_current_value: _value, ...config } = row;
return config;
});
}
const req = tableChange(table, changes);
if (hasChanges(req)) {
await allocateTemporaryIds([req.inserts || [], req.updates || [], req.deletes || []]);
@@ -654,6 +810,7 @@ function buildMissingDefaultConfigs(module, fields) {
b_required: 0,
b_readonly: 0,
b_disabled: 0,
b_visible: 1,
b_group_id: null,
b_xh: order,
});
@@ -787,6 +944,7 @@ async function syncFields() {
}
}
applyDisabledFieldDefaults(existingFields, mergedFields);
const generated = buildMissingDefaultConfigs(module, mergedFields);
const generatedSections = ["listConfig", "editConfig", "queryConfig"];
let generatedCount = 0;
@@ -898,6 +1056,15 @@ void loadTree().catch((e) => message.error("模块树加载失败:" + (e.messa
:disabled="busy || drafts.module?.b_module_type === 'data'"
@update:checked="(v) => onSetField('b_canmenu', v ? 1 : 0)"
/>
<template v-if="drafts.module?.b_module_type === 'data'">
<a-divider type="vertical" />
<span>自动编码</span>
<a-checkbox
:checked="autoCodeEnabled"
:disabled="busy"
@update:checked="setAutoCodeEnabled"
/>
</template>
</a-space>
</div>
</div>
@@ -926,7 +1093,7 @@ void loadTree().catch((e) => message.error("模块树加载失败:" + (e.messa
:module-code="selectedModule?.b_code || ''"
:fields="drafts.fields"
:loading="busy"
@update="(v) => onUpdate('fields', v)"
@update="onFieldsUpdate"
@i18n-saved="onI18nSaved"
/>
<ModuleListConfigPanel
@@ -959,6 +1126,14 @@ void loadTree().catch((e) => message.error("模块树加载失败:" + (e.messa
:loading="busy"
@update="(v) => onUpdate('queryConfig', v)"
/>
<ModuleAutoCodePanel
v-else-if="tab.key === 'auto-code'"
:module-id="String(selectedModule.b_id)"
:auto-code="drafts.autoCode"
:fields="drafts.fields"
:loading="busy"
@update="onAutoCodeUpdate"
/>
<ModulePowersPanel
v-else-if="tab.key === 'powers'"
:module-id="String(selectedModule.b_id)"
@@ -1,7 +1,93 @@
<script setup>
import { computed, ref } from "vue";
import { message, Modal } from "antdv-next";
import { Trash2 } from "@lucide/vue";
import FmsModuleListPage from "@/components/fms-module-list/FmsModuleListPage.vue";
import { saveObjectApi } from "@/services/api";
import { useAppStore } from "@/stores/app";
const appStore = useAppStore();
const listRef = ref(null);
const selectedRowKeys = ref([]);
const saving = ref(false);
const rowSelection = computed(() => {
return {
selectedRowKeys: selectedRowKeys.value,
onChange: (keys) => {
selectedRowKeys.value = keys;
},
};
});
async function reloadList() {
selectedRowKeys.value = [];
if (listRef.value) await listRef.value.reload();
}
function batchDelete() {
if (saving.value || !selectedRowKeys.value.length) return;
const keys = [...selectedRowKeys.value];
Modal.confirm({
title: `${appStore.t("common.confirmBatchDelete", "确定删除选中的记录?")} (${keys.length})`,
okText: appStore.t("common.delete", "删除"),
okType: "danger",
cancelText: appStore.t("common.cancel", "取消"),
onOk: async () => {
saving.value = true;
try {
const deletes = keys.map((b_id) => ({ b_id }));
await saveObjectApi([
{
table: "s_log_login",
key_field: "b_id",
inserts: [],
updates: [],
deletes,
},
]);
message.success(appStore.t("common.deleteSuccess", "删除成功"));
await reloadList();
} catch (error) {
message.error(
appStore.t("common.deleteFailed", "删除失败") + ":" + (error?.message || error),
);
} finally {
saving.value = false;
}
},
});
}
</script>
<template>
<FmsModuleListPage />
<div class="login-log-page">
<FmsModuleListPage
ref="listRef"
:row-selection="rowSelection"
@loaded="selectedRowKeys = []"
>
<template #actions="{ loading }">
<a-space>
<a-button
danger
:disabled="loading || saving || !selectedRowKeys.length"
@click="batchDelete"
>
<Trash2 :size="16" />
{{ appStore.t("common.batchDelete", "批量删除")
}}{{ selectedRowKeys.length ? `(${selectedRowKeys.length})` : "" }}
</a-button>
</a-space>
</template>
</FmsModuleListPage>
</div>
</template>
<style scoped>
.login-log-page {
width: 100%;
height: 100%;
min-height: 0;
}
</style>