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

@@ -2,8 +2,8 @@
import { computed, h, nextTick, reactive, ref, shallowRef, useSlots, watch } from "vue";
import { useRoute } from "vue-router";
import { message } from "antdv-next";
import { Columns3, RefreshCw } from "@lucide/vue";
import { loadDataApi, pageDataApi } from "@/services/api";
import { Columns3, RefreshCw, Save } from "@lucide/vue";
import { loadDataApi, pageDataApi, saveObjectApi } from "@/services/api";
import { useAppStore } from "@/stores/app";
import {
identifierRe,
@@ -18,6 +18,7 @@ import FmsQueryToolbar from "./FmsQueryToolbar.vue";
const props = defineProps({
dataCode: { type: String, default: "" },
fixedSearchCondition: { type: String, default: "" },
columnExtensions: { type: Object, default: () => ({}) },
rowActions: { type: Array, default: () => [] },
rowSelection: { type: Object, default: null },
@@ -44,6 +45,7 @@ const searchConditions = shallowRef([]);
const keyField = ref("");
const configLoading = ref(false);
const dataLoading = ref(false);
const columnSaving = ref(false);
const configError = ref("");
const dataError = ref("");
const pagination = reactive({
@@ -51,6 +53,7 @@ const pagination = reactive({
pageSize: Number(props.pageSize) || 20,
total: 0,
});
let dataRequestVersion = 0;
const loading = computed(() => configLoading.value || dataLoading.value);
@@ -63,9 +66,15 @@ const moreMenuItems = computed(() => [
},
{
key: "reset-columns",
label: appStore.t("common.resetColumns", "恢复配置列宽"),
label: appStore.t("common.resetColumnSettings", "恢复栏位设置"),
icon: h(Columns3, { size: 14 }),
},
{
key: "save-column-settings",
label: appStore.t("common.saveColumnSettings", "保存栏位设置"),
icon: h(Save, { size: 14 }),
disabled: columnSaving.value,
},
]);
const cellSlotNames = computed(() => Object.keys(slots).filter((name) => name.startsWith("cell-")));
@@ -225,12 +234,17 @@ function resolveOrderBy() {
}
async function fetchData() {
if (!dataModule.value || !dataModule.value.b_viewtable || dataLoading.value) return;
if (!dataModule.value || !dataModule.value.b_viewtable) return;
const currentModule = dataModule.value;
const viewName = currentModule.b_viewtable;
const currentCode = getDataCode();
const currentPath = route.path;
const requestVersion = ++dataRequestVersion;
const conditions = [];
const fixedCondition = String(props.fixedSearchCondition || "").trim();
if (fixedCondition) conditions.push(`(${fixedCondition})`);
conditions.push(...searchConditions.value);
dataLoading.value = true;
dataError.value = "";
@@ -239,17 +253,18 @@ async function fetchData() {
viewName,
orderBy: resolveOrderBy(),
searchColumns: [],
searchCondition: searchConditions.value.join(" AND "),
searchCondition: conditions.join(" AND "),
pageNo: pagination.current,
pageSize: pagination.pageSize,
});
if (dataModule.value !== currentModule) return;
if (requestVersion !== dataRequestVersion || dataModule.value !== currentModule) return;
if (getDataCode() !== currentCode || route.path !== currentPath) return;
dataSource.value = res.data?.rows || [];
pagination.total = Number(res.data?.total ?? res.data?.row_count) || 0;
} catch (error) {
if (
requestVersion === dataRequestVersion &&
dataModule.value === currentModule &&
getDataCode() === currentCode &&
route.path === currentPath
@@ -260,6 +275,7 @@ async function fetchData() {
}
} finally {
if (
requestVersion === dataRequestVersion &&
dataModule.value === currentModule &&
getDataCode() === currentCode &&
route.path === currentPath
@@ -271,6 +287,7 @@ async function fetchData() {
// ── 公开方法 ──
async function initialize() {
dataRequestVersion++;
dataModule.value = null;
activeFields.value = [];
activeFieldById.value = new Map();
@@ -324,6 +341,56 @@ async function resetColumns() {
if (tableRef.value) tableRef.value.resetColumns();
}
async function saveColumnSettings() {
if (loading.value || columnSaving.value || !tableRef.value) return;
if (typeof tableRef.value.getColumnSettings !== "function") return;
const settings = tableRef.value.getColumnSettings();
if (!settings.length) {
message.info("当前没有可保存的栏位设置");
return;
}
const updates = [];
for (const row of settings) {
updates.push({
b_id: row.b_id,
b_xh: row.b_xh,
b_width: row.b_width,
});
}
columnSaving.value = true;
try {
await saveObjectApi([
{
table: "s_module_field_list",
key_field: "b_id",
updates,
},
]);
const currentById = new Map();
for (const row of listConfig.value) {
currentById.set(String(row.b_id), row);
}
const savedListConfig = [];
for (const saved of settings) {
const current = currentById.get(String(saved.b_id));
if (!current) continue;
savedListConfig.push({ ...current, b_xh: saved.b_xh, b_width: saved.b_width });
}
listConfig.value = savedListConfig;
message.success("栏位设置已保存");
} catch (error) {
let errorMessage = error;
if (error && error.message) errorMessage = error.message;
message.error("栏位设置保存失败:" + errorMessage);
} finally {
columnSaving.value = false;
}
}
function resolveRowKey(row) {
if (keyField.value) return row[keyField.value];
return JSON.stringify(row);
@@ -336,13 +403,26 @@ async function handleMoreAction({ key }) {
}
if (key === "reset-columns") {
await resetColumns();
message.success(appStore.t("common.columnsReset", "已恢复配置列宽"));
message.success(appStore.t("common.columnSettingsReset", "已恢复栏位设置"));
return;
}
if (key === "save-column-settings") {
await saveColumnSettings();
}
}
// ── 生命周期 ──
watch(() => [getDataCode(), route.path], initialize, { immediate: true });
watch(
() => props.fixedSearchCondition,
() => {
if (!dataModule.value) return;
pagination.current = 1;
fetchData();
},
);
watch(
() => dataSource.value,
(rows) => {
@@ -37,7 +37,7 @@ const expansionOptions = computed(() =>
const expandConfig = computed(() => ({ height: Number(expansionOptions.value.height) || 180 }));
function formatCellValue(value, field, optionInfo) {
if (value === null || value === undefined || value === "") return "-";
if (value === null || value === undefined || value === "") return "";
if (field.b_type === "date") return formatDateValue(value, false);
if (field.b_type === "datetime") return formatDateValue(value, true);
if (field.b_type === "checkbox") {
@@ -199,6 +199,35 @@ function resetColumns() {
tableRef.value?.initVirtualScroll?.();
}
function getColumnSettings() {
const configByFieldId = new Map();
for (const config of props.listConfig || []) {
configByFieldId.set(String(config.b_field_id), config);
}
const settings = [];
let order = 10;
for (const column of columns.value) {
if (!column.dataIndex || String(column.dataIndex).startsWith("__")) continue;
const field = props.fields.find((item) => item.b_field === column.dataIndex);
if (!field) continue;
const config = configByFieldId.get(String(field.b_id));
if (!config) continue;
const parsedWidth = Number.parseFloat(String(column.width));
let columnWidth = config.b_width;
if (Number.isFinite(parsedWidth) && parsedWidth > 0) {
columnWidth = Math.round(parsedWidth);
}
settings.push({
...config,
b_xh: order,
b_width: columnWidth,
});
order += 10;
}
return settings;
}
function handleCellClick(event, record, column, meta) {
if (!column?.dataIndex || String(column.dataIndex).startsWith("__")) return;
const field = props.fields.find((item) => item.b_field === column.dataIndex);
@@ -251,7 +280,11 @@ watch(
},
);
defineExpose({ resetColumns, clearSelectedArea: () => tableRef.value?.clearSelectedArea?.() });
defineExpose({
resetColumns,
getColumnSettings,
clearSelectedArea: () => tableRef.value?.clearSelectedArea?.(),
});
</script>
<template>
@@ -226,7 +226,7 @@ watch(() => props.queryItems, initializeDrafts, { immediate: true });
<Search :size="15" />
{{ appStore.t("common.search", "查询") }}
</a-button>
<a-button :disabled="loading" @click="handleAdvancedReset">
<a-button type="text" :disabled="loading" @click="handleAdvancedReset">
<RotateCcw :size="15" />
{{ appStore.t("common.reset", "重置") }}
</a-button>
@@ -15,8 +15,10 @@ const props = defineProps({
noDataFull: { type: Boolean, default: true },
showOverflow: { type: Boolean, default: true },
columns: { type: Array, default: () => [] },
// 序号列:false=关闭;true=默认;{ title, width }=自定义
seq: { type: [Boolean, Object], default: false },
// 序号列默认显示;false=关闭;{ title, width }=自定义
seq: { type: [Boolean, Object], default: true },
// 列头拖拽默认开启;false=关闭;{ mode, disabled }=自定义
columnDraggable: { type: [Boolean, Object], default: true },
// 行多选:{ selectedRowKeys, onChange },提供时在最前列插入复选框列
rowSelection: { type: Object, default: null },
dataSource: { type: Array, default: () => [] },
@@ -137,6 +139,27 @@ const seqConfig = computed(() => {
width: Number(cfg.width ?? 50),
};
});
const columnDragConfig = computed(() => {
if (!props.columnDraggable) return false;
const configured = props.columnDraggable === true ? {} : props.columnDraggable;
return {
...configured,
disabled: (column) => {
if (
isSelectionColumn(column) ||
column.type === "seq" ||
column.type === "dragRow" ||
column.type === "expand" ||
column.dataIndex === "" ||
column.dataIndex === "__actions__"
) {
return true;
}
if (typeof configured.disabled === "function") return configured.disabled(column);
return Boolean(configured.disabled);
},
};
});
const hasRowSelection = computed(() => Boolean(props.rowSelection));
const dragRowColumn = {
type: "dragRow",
@@ -158,8 +181,6 @@ function getHeaderAlign(column) {
const displayColumns = computed(() => {
const cols = props.columns.map((col) => resolveCustomCell(col, editingCtx));
if (props.rowDraggable) cols.unshift(dragRowColumn);
if (hasRowSelection.value) cols.unshift(selectionColumn);
const seq = seqConfig.value;
if (seq.enabled) {
cols.unshift({
@@ -170,6 +191,8 @@ const displayColumns = computed(() => {
resizable: false,
});
}
if (props.rowDraggable) cols.unshift(dragRowColumn);
if (hasRowSelection.value) cols.unshift(selectionColumn);
return cols.map((column) => ({
...column,
headerAlign: getHeaderAlign(column),
@@ -435,6 +458,7 @@ defineExpose({
:virtual-x="virtualX"
:auto-resize="false"
:col-resizable="colResizable"
:header-drag="columnDragConfig"
:columns="displayColumns"
@update:columns="onColumnsUpdate"
:data-source="dataSource"
@@ -1,7 +1,7 @@
<script setup>
import { computed, h, onMounted, ref, watch } from "vue";
import { useRoute, useRouter } from "vue-router";
import { House, PanelLeftClose, PanelLeftOpen, Settings } from "@lucide/vue";
import { Database, House, PanelLeftClose, PanelLeftOpen, Settings } from "@lucide/vue";
import { useAppStore } from "@/stores/app";
import { usePermissionStore } from "@/stores/permissions";
@@ -10,7 +10,7 @@ const router = useRouter();
const appStore = useAppStore();
const permissionStore = usePermissionStore();
const menuIcon = (icon) => () => h(icon, { size: 16, strokeWidth: 1.8 });
const openKeys = ref(["system"]);
const openKeys = ref(["base", "system"]);
const effectiveCollapsed = computed(() => appStore.sidebarCollapsed);
function moduleLabel(moduleId, path, fallback) {
const module = permissionStore.modules.find(
@@ -29,6 +29,13 @@ function canAccessMenu(item) {
return true;
}
const menuItems = computed(() => {
const basePages = [
{
key: "/base/othercompany",
label: moduleLabel("base_othercompany", "/base/othercompany", "合作伙伴"),
moduleId: "base_othercompany",
},
].filter(canAccessMenu);
const systemPages = [
{
key: "/system/module-management",
@@ -53,6 +60,13 @@ const menuItems = computed(() => {
label: appStore.t("common.dashboard", "工作台"),
},
];
if (basePages.length)
items.push({
key: "base",
icon: menuIcon(Database),
label: directoryLabel("基础数据"),
children: basePages,
});
if (systemPages.length)
items.push({
key: "system",
@@ -66,6 +80,8 @@ const selectedKeys = computed(() => [route.meta.menuKey || route.path]);
watch(
() => route.path,
(path) => {
if (path.startsWith("/base") && !openKeys.value.includes("base"))
openKeys.value = [...openKeys.value, "base"];
if (path.startsWith("/system") && !openKeys.value.includes("system"))
openKeys.value = [...openKeys.value, "system"];
},
+13 -1
View File
@@ -14,6 +14,18 @@ const routes = [
component: () => import("@/views/dashboard/index.vue"),
meta: { title: "工作台", menuKey: "/dashboard" },
},
{
path: "base/othercompany",
name: "base-othercompany",
component: () => import("@/views/base/othercompany/index.vue"),
meta: {
title: "合作伙伴",
section: "基础数据",
menuKey: "/base/othercompany",
moduleId: "base_othercompany",
dataCode: "b_othercompany",
},
},
{
path: "system/module-management",
name: "system-module-management",
@@ -44,7 +56,7 @@ const routes = [
title: "登录日志",
section: "系统管理",
menuKey: "/system/login-log",
moduleId: "d_login_log",
moduleId: "s_login_log",
},
},
],
+10
View File
@@ -97,6 +97,16 @@ export function nextIdApi(count = 1) {
return get("/data/nextid", { params: { count } });
}
/**
* 按模块规则原子获取业务编号;count 大于 1 时一次返回多个编号。
* @param {string} moduleId 数据模块 ID。
* @param {number} [count=1] 需要获取的编号数量。
* @returns {Promise<ApiResult<string|string[]>>} 业务编号或编号数组。
*/
export function nextCodeApi(moduleId, count = 1) {
return get("/data/nextcode", { params: { moduleId: String(moduleId), count } });
}
/**
* 读取表或视图的列结构,用于同步字段等场景。
* @param {string} table 表名或视图名,必填,后端会校验标识符。
@@ -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>