From bab8e2972f9764ae39f656c3a3c0c63cd8866475 Mon Sep 17 00:00:00 2001 From: ZhangAo Date: Tue, 21 Jul 2026 17:34:02 +0800 Subject: [PATCH] 20260721173401 --- code/fms/CODEBUDDY.md | 104 ++ code/fms/fms-vue/jsconfig.json | 16 + code/fms/fms-vue/package.json | 3 + code/fms/fms-vue/pnpm-lock.yaml | 248 ++- .../src/components/AutoHeightTable.vue | 197 --- .../src/components/FmsAutoComplete.vue | 68 + .../fms-vue/src/components/FmsMemoInput.vue | 171 +++ code/fms/fms-vue/src/components/FmsSelect.vue | 84 + code/fms/fms-vue/src/components/FmsTable.vue | 76 + code/fms/fms-vue/src/components/FmsTree.vue | 16 + .../src/components/select/useLookupOptions.js | 128 ++ .../src/layouts/components/AppSidebar.vue | 3 +- .../src/layouts/components/AppTabs.vue | 2 +- .../src/layouts/components/AppToolbar.vue | 4 +- code/fms/fms-vue/src/main.js | 1 + code/fms/fms-vue/src/router/index.js | 116 +- code/fms/fms-vue/src/services/http.js | 2 +- .../fms-vue/src/services/moduleManagement.js | 54 - code/fms/fms-vue/src/stores/auth.js | 13 +- code/fms/fms-vue/src/stores/permissions.js | 2 +- code/fms/fms-vue/src/views/DashboardView.vue | 139 +- code/fms/fms-vue/src/views/DataListView.vue | 632 -------- code/fms/fms-vue/src/views/DetailView.vue | 179 --- code/fms/fms-vue/src/views/LoginView.vue | 4 +- .../fms-vue/src/views/ModuleManageDemo.vue | 1355 ----------------- .../ModuleManagementView.vue | 205 +-- .../components/ModuleFieldDefinitionPanel.vue | 242 +-- .../components/ModulePowerPanel.vue | 145 +- .../components/ModuleTranslationsPanel.vue | 141 +- .../components/ModuleTreePanel.vue | 20 +- code/fms/fms-vue/vite.config.js | 6 + code/fms/开发规范.md | 2 + 32 files changed, 1207 insertions(+), 3171 deletions(-) create mode 100644 code/fms/CODEBUDDY.md create mode 100644 code/fms/fms-vue/jsconfig.json delete mode 100644 code/fms/fms-vue/src/components/AutoHeightTable.vue create mode 100644 code/fms/fms-vue/src/components/FmsAutoComplete.vue create mode 100644 code/fms/fms-vue/src/components/FmsMemoInput.vue create mode 100644 code/fms/fms-vue/src/components/FmsSelect.vue create mode 100644 code/fms/fms-vue/src/components/FmsTable.vue create mode 100644 code/fms/fms-vue/src/components/FmsTree.vue create mode 100644 code/fms/fms-vue/src/components/select/useLookupOptions.js delete mode 100644 code/fms/fms-vue/src/services/moduleManagement.js delete mode 100644 code/fms/fms-vue/src/views/DataListView.vue delete mode 100644 code/fms/fms-vue/src/views/DetailView.vue delete mode 100644 code/fms/fms-vue/src/views/ModuleManageDemo.vue diff --git a/code/fms/CODEBUDDY.md b/code/fms/CODEBUDDY.md new file mode 100644 index 00000000..3e314502 --- /dev/null +++ b/code/fms/CODEBUDDY.md @@ -0,0 +1,104 @@ +# CODEBUDDY.md + +This file provides guidance to CodeBuddy Code when working with code in this repository. + +## Repository layout + +This is a two-part application (FMS — 模块/数据管理平台): + +- `fms-api/` — Spring Boot 4.1 (Java 21, Maven) backend. Serves on port **8088** with context-path **`/api`**. +- `fms-vue/` — Vue 3 + Vite 8 (pnpm) frontend. Dev server on port **5180**, proxies `/api` → `http://127.0.0.1:8088`. + +Supporting docs in the repo root (Chinese): +- `开发规范.md` — binding development rules (read it; summarized below). +- `新数据库结构.md` — the module-management table design (authoritative schema). +- `旧数据库结构.md` — legacy schema, kept for reference only. +- `模块管理实施计划.md` — phased plan for the module-management feature. +- `fms-api/config/migrations/001_module_management.sql` — the only migration script. + +## Commands + +### Backend (`fms-api/`) +```bash +./mvnw clean package # build jar -> target/fms-api-0.0.1-SNAPSHOT.jar +./mvnw test # run all tests (8 test classes under src/test) +./mvnw test -Dtest=ClassName # run a single test class +./mvnw test -Dtest=ClassName#methodName # run a single test method +./mvnw spring-boot:run # run the dev server (or use start.cmd) +java -jar target/fms-api-0.0.1-SNAPSHOT.jar # run built jar +``` +JWT secret is read from env `FMS_JWT_SECRET` (defaults to a dev value in `application.yaml`). + +### Frontend (`fms-vue/`) +```bash +pnpm install +pnpm dev # vite dev server on :5180 +pnpm build # production build +pnpm preview # preview built output +pnpm fmt # format with oxfmt +pnpm fmt:check # check formatting +``` +There is **no lint or test script** in the frontend. + +### Build/test discipline (from `开发规范.md`) +Do **not** run build/test/start yourself after finishing a change. Only build, test, or start services when the user explicitly asks. When delivering, state that build/test were not executed. + +## Backend architecture + +The defining characteristic of this backend is that it has **almost no business-specific endpoints**. Almost all data access goes through a small set of generic endpoints in `DataController`, implemented by `DataService` / `DataSaveService`. There are no entity/DAO/mapper layers — `utils/DbUtils` does raw JDBC against SQL Server. + +Generic data endpoints (full path includes the `/api` context-path): +- `POST /api/data/loaddata` — load rows from a `view_name` with optional search/order (`DataService.loadData`). +- `POST /api/data/page` — paginated load (`page_no`, `page_size`, `view_name`, `order_by`). +- `POST /api/data/saveobjt` — batch upsert across multiple tables in one transaction (`DataSaveService.save`). Request body is a JSON array of `{ table, key_field, inserts[], updates[], deletes[] }`; the whole request rolls back if any table fails. +- `GET /api/data/nextid` — Snowflake ID from `utils/snowflake/IdGenerator`. +- `POST /api/data/loaddatabysql` — runs an arbitrary SQL string. **Avoid this**; it exists but new feature work should prefer the structured endpoints. +- `POST /api/auth/login` — `AuthController` / `AuthService`. + +Key rules when working on the backend: +- Prefer the generic endpoints for query/paging/save/ID-generation. Do **not** modify their URL, parameters, response shape, or logic, and do **not** add new business-specific endpoints, unless you first explain why the generic API cannot do it and get user confirmation. +- `b_id` values that are `bigint` snowflake IDs must be serialized to the frontend as **strings**. + +### Multi-organization database routing +Connections are per-organization SQL Server pools (Druid). The active org is selected at request time: +- `config/dbconfigs/.properties` holds `url`/`username`/`password`/`driver` for one org (e.g. `g3hd.properties` for org `G3HD`). **These files are git-ignored** and must not be committed. The `config-dir` is set by `fms.database.config-dir` in `application.yaml` (default `./config/dbconfigs`). +- `database/OrgDatabaseConfigLoader` loads a properties file by org id; `OrgDataSourceManager` caches one `DruidDataSource` per org. +- `config/JwtAuthFilter` parses the JWT, then `database/OrgContext` holds the current `orgId` so `DbUtils` targets the right datasource (`OrgRoutingDataSource`). +- New features operate on the `g3hd.properties` database (`fms`); do not touch the legacy database. + +### Auth / JWT +- `config/JwtAuthFilter` (skips `/auth/login`) validates `Authorization: Bearer`, checks `ActiveSessionRegistry` (single-device enforcement), and sets `OrgContext.orgId`. +- `utils/JwtUtils` (jjwt) puts `orgid` / `sessionId` claims. `config/AuthProperties` reads `fms.auth.jwt-secret` / `fms.auth.jwt-expiration`. +- Login response returns `id`, `account`, `name`; the JWT session identifier stays the account. + +## Frontend architecture + +Vue 3 ` - - - - diff --git a/code/fms/fms-vue/src/components/FmsAutoComplete.vue b/code/fms/fms-vue/src/components/FmsAutoComplete.vue new file mode 100644 index 00000000..2742688b --- /dev/null +++ b/code/fms/fms-vue/src/components/FmsAutoComplete.vue @@ -0,0 +1,68 @@ + + + + + diff --git a/code/fms/fms-vue/src/components/FmsMemoInput.vue b/code/fms/fms-vue/src/components/FmsMemoInput.vue new file mode 100644 index 00000000..c45f6ac2 --- /dev/null +++ b/code/fms/fms-vue/src/components/FmsMemoInput.vue @@ -0,0 +1,171 @@ + + + + + diff --git a/code/fms/fms-vue/src/components/FmsSelect.vue b/code/fms/fms-vue/src/components/FmsSelect.vue new file mode 100644 index 00000000..eaae9ad5 --- /dev/null +++ b/code/fms/fms-vue/src/components/FmsSelect.vue @@ -0,0 +1,84 @@ + + + + + diff --git a/code/fms/fms-vue/src/components/FmsTable.vue b/code/fms/fms-vue/src/components/FmsTable.vue new file mode 100644 index 00000000..6ea3ffa2 --- /dev/null +++ b/code/fms/fms-vue/src/components/FmsTable.vue @@ -0,0 +1,76 @@ + + + + + diff --git a/code/fms/fms-vue/src/components/FmsTree.vue b/code/fms/fms-vue/src/components/FmsTree.vue new file mode 100644 index 00000000..1d011a61 --- /dev/null +++ b/code/fms/fms-vue/src/components/FmsTree.vue @@ -0,0 +1,16 @@ + + + diff --git a/code/fms/fms-vue/src/components/select/useLookupOptions.js b/code/fms/fms-vue/src/components/select/useLookupOptions.js new file mode 100644 index 00000000..44035b20 --- /dev/null +++ b/code/fms/fms-vue/src/components/select/useLookupOptions.js @@ -0,0 +1,128 @@ +import { computed, onBeforeUnmount, ref, watch } from "vue"; +import { useTimeoutFn } from "@vueuse/core"; +import { pageDataApi } from "@/services/api"; + +function escapeSearchText(value) { + return String(value).replaceAll("'", "''"); +} + +function combineConditions(baseCondition, searchCondition) { + if (!baseCondition) return searchCondition; + if (!searchCondition) return baseCondition; + return `(${baseCondition}) AND (${searchCondition})`; +} + +export function useLookupOptions(props) { + const rows = ref([]); + const loading = ref(false); + const pageNo = ref(0); + const total = ref(0); + const query = ref(""); + + const searchColumns = computed(() => [ + ...new Set([props.labelKey, props.valueKey, ...props.searchColumns].filter(Boolean)), + ]); + + let requestId = 0; + + async function load(searchText = query.value, append = false) { + if (!props.viewName) return; + + const currentRequestId = ++requestId; + const requestedPage = append ? pageNo.value + 1 : 1; + const normalizedSearchText = String(searchText ?? "").trim(); + const searchFilter = normalizedSearchText + ? `${props.labelKey} LIKE '%${escapeSearchText(normalizedSearchText)}%'` + : ""; + + loading.value = true; + try { + const response = await pageDataApi({ + viewName: props.viewName, + orderBy: props.orderBy || props.labelKey, + searchColumns: searchColumns.value, + searchCondition: combineConditions(props.searchCondition, searchFilter), + pageNo: requestedPage, + pageSize: props.pageSize, + }); + + if (currentRequestId !== requestId) return; + + const result = response.data ?? {}; + const nextRows = Array.isArray(result.rows) ? result.rows : []; + rows.value = append ? [...rows.value, ...nextRows] : nextRows; + pageNo.value = Number(result.page_no) || requestedPage; + total.value = Number(result.total ?? result.row_count) || 0; + query.value = normalizedSearchText; + } catch { + if (currentRequestId === requestId && !append) { + rows.value = []; + pageNo.value = 0; + total.value = 0; + } + } finally { + if (currentRequestId === requestId) loading.value = false; + } + } + + const { start: startSearchTimer, stop: stopSearchTimer } = useTimeoutFn( + () => load(query.value), + () => props.searchDelay, + { immediate: false }, + ); + const { start: startResetTimer, stop: stopResetTimer } = useTimeoutFn(reset, 200, { + immediate: false, + }); + + function search(value) { + stopResetTimer(); + query.value = String(value ?? ""); + startSearchTimer(); + } + + function open() { + stopResetTimer(); + if (!rows.value.length && !loading.value) load(query.value); + } + + function reset() { + stopSearchTimer(); + stopResetTimer(); + requestId += 1; + rows.value = []; + loading.value = false; + pageNo.value = 0; + total.value = 0; + query.value = ""; + } + + function resetAfterClose() { + startResetTimer(); + } + + function loadMore() { + if (loading.value || rows.value.length >= total.value) return; + load(query.value, true); + } + + function onPopupScroll(event) { + const target = event.currentTarget || event.target; + if (target.scrollTop + target.clientHeight >= target.scrollHeight - 24) loadMore(); + } + + watch( + () => [ + props.viewName, + props.labelKey, + props.valueKey, + props.orderBy, + props.searchCondition, + props.searchColumns, + ], + reset, + ); + + onBeforeUnmount(reset); + + return { rows, loading, query, load, search, open, resetAfterClose, onPopupScroll }; +} diff --git a/code/fms/fms-vue/src/layouts/components/AppSidebar.vue b/code/fms/fms-vue/src/layouts/components/AppSidebar.vue index 355d8b16..89a1f0a9 100644 --- a/code/fms/fms-vue/src/layouts/components/AppSidebar.vue +++ b/code/fms/fms-vue/src/layouts/components/AppSidebar.vue @@ -14,7 +14,7 @@ import { Settings, Wallet, } from "@lucide/vue"; -import { useAppStore } from "../../stores/app"; +import { useAppStore } from "@/stores/app"; const route = useRoute(); const router = useRouter(); @@ -255,6 +255,7 @@ function updateOpenKeys(keys) { height: 100%; overflow: hidden; border-right: 1px solid var(--fms-border); + will-change: width; :deep(.ant-layout-sider-children) { display: flex; diff --git a/code/fms/fms-vue/src/layouts/components/AppTabs.vue b/code/fms/fms-vue/src/layouts/components/AppTabs.vue index b69746cd..7295aa6a 100644 --- a/code/fms/fms-vue/src/layouts/components/AppTabs.vue +++ b/code/fms/fms-vue/src/layouts/components/AppTabs.vue @@ -3,7 +3,7 @@ import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue" import { useRoute, useRouter } from "vue-router"; import { ChevronLeft, ChevronRight, X } from "@lucide/vue"; import { theme } from "antdv-next"; -import { useAppStore } from "../../stores/app"; +import { useAppStore } from "@/stores/app"; const route = useRoute(); const router = useRouter(); diff --git a/code/fms/fms-vue/src/layouts/components/AppToolbar.vue b/code/fms/fms-vue/src/layouts/components/AppToolbar.vue index 19456606..39da7435 100644 --- a/code/fms/fms-vue/src/layouts/components/AppToolbar.vue +++ b/code/fms/fms-vue/src/layouts/components/AppToolbar.vue @@ -2,8 +2,8 @@ import { computed, h, ref, watch } from "vue"; import { useRoute, useRouter } from "vue-router"; import { Bell, ChevronDown, Languages, LogOut, Settings, UserRound } from "@lucide/vue"; -import { useAppStore } from "../../stores/app"; -import { useAuthStore } from "../../stores/auth"; +import { useAppStore } from "@/stores/app"; +import { useAuthStore } from "@/stores/auth"; const route = useRoute(); const router = useRouter(); diff --git a/code/fms/fms-vue/src/main.js b/code/fms/fms-vue/src/main.js index be571869..f5705e7e 100644 --- a/code/fms/fms-vue/src/main.js +++ b/code/fms/fms-vue/src/main.js @@ -5,6 +5,7 @@ import App from "./App.vue"; import router from "./router"; import "./styles/tailwind.css"; import "./styles/main.scss"; +import 'stk-table-vue/lib/style.css'; const app = createApp(App); const pinia = createPinia(); diff --git a/code/fms/fms-vue/src/router/index.js b/code/fms/fms-vue/src/router/index.js index 2e8ddb61..888af816 100644 --- a/code/fms/fms-vue/src/router/index.js +++ b/code/fms/fms-vue/src/router/index.js @@ -2,8 +2,8 @@ import { createRouter, createWebHistory } from "vue-router"; import NProgress from "nprogress"; import { message } from "antdv-next"; import "nprogress/nprogress.css"; -import { useAuthStore } from "../stores/auth"; -import { usePermissionStore } from "../stores/permissions"; +import { useAuthStore } from "@/stores/auth"; +import { usePermissionStore } from "@/stores/permissions"; NProgress.configure({ showSpinner: false }); @@ -16,7 +16,7 @@ function getRedirectPath(value) { const routes = [ { path: "/", - component: () => import("../layouts/DefaultLayout.vue"), + component: () => import("@/layouts/DefaultLayout.vue"), meta: { requiresAuth: true }, children: [ { @@ -26,117 +26,13 @@ const routes = [ { path: "dashboard", name: "dashboard", - component: () => import("../views/DashboardView.vue"), + component: () => import("@/views/DashboardView.vue"), meta: { title: "工作台", menuKey: "/dashboard" }, }, - { - path: "business/orders", - name: "business-orders", - component: () => import("../views/DataListView.vue"), - meta: { - title: "业务单据", - section: "业务管理", - menuKey: "/business/orders", - source: "v_bs_business", - orderBy: "b_id DESC", - }, - }, - { - path: "business/orders/:id", - name: "business-order-detail", - component: () => import("../views/DetailView.vue"), - meta: { - title: "业务单据详情", - listTitle: "业务单据", - section: "业务管理", - menuKey: "/business/orders", - listPath: "/business/orders", - source: "v_bs_business", - detail: true, - }, - }, - { - path: "fees/receivable", - name: "fees-receivable", - component: () => import("../views/DataListView.vue"), - meta: { - title: "应收费用", - section: "费用管理", - menuKey: "/fees/receivable", - source: "v_fee_receivable", - orderBy: "b_id DESC", - }, - }, - { - path: "fees/receivable/:id", - name: "fees-receivable-detail", - component: () => import("../views/DetailView.vue"), - meta: { - title: "应收费用详情", - listTitle: "应收费用", - section: "费用管理", - menuKey: "/fees/receivable", - listPath: "/fees/receivable", - source: "v_fee_receivable", - detail: true, - }, - }, - { - path: "reports/operations", - name: "reports-operations", - component: () => import("../views/DataListView.vue"), - meta: { - title: "运营报表", - section: "报表中心", - menuKey: "/reports/operations", - source: "v_report_operations", - orderBy: "b_id DESC", - }, - }, - { - path: "reports/operations/:id", - name: "reports-operations-detail", - component: () => import("../views/DetailView.vue"), - meta: { - title: "运营报表详情", - listTitle: "运营报表", - section: "报表中心", - menuKey: "/reports/operations", - listPath: "/reports/operations", - source: "v_report_operations", - detail: true, - }, - }, - { - path: "system/users", - name: "system-users", - component: () => import("../views/DataListView.vue"), - meta: { - title: "用户管理", - section: "系统设置", - menuKey: "/system/users", - source: "b_user", - orderBy: "b_id ASC", - }, - }, - { - path: "system/users/:id", - name: "system-users-detail", - component: () => import("../views/DetailView.vue"), - meta: { - title: "用户管理详情", - listTitle: "用户管理", - section: "系统设置", - menuKey: "/system/users", - listPath: "/system/users", - source: "b_user", - detail: true, - }, - }, { path: "system/modules", name: "system-modules", - component: () => import("../views/module-management/ModuleManagementView.vue"), + component: () => import("@/views/module-management/ModuleManagementView.vue"), meta: { title: "模块管理", section: "系统管理", @@ -149,7 +45,7 @@ const routes = [ { path: "/login", name: "login", - component: () => import("../views/LoginView.vue"), + component: () => import("@/views/LoginView.vue"), meta: { title: "登录" }, }, { diff --git a/code/fms/fms-vue/src/services/http.js b/code/fms/fms-vue/src/services/http.js index 0e5acd78..0f2361b8 100644 --- a/code/fms/fms-vue/src/services/http.js +++ b/code/fms/fms-vue/src/services/http.js @@ -1,5 +1,5 @@ import axios from "axios"; -import { useAuthStore } from "../stores/auth"; +import { useAuthStore } from "@/stores/auth"; const AUTH_ERROR_CODE = 401; const SESSION_REPLACED_CODE = 40101; diff --git a/code/fms/fms-vue/src/services/moduleManagement.js b/code/fms/fms-vue/src/services/moduleManagement.js deleted file mode 100644 index 35c39fb0..00000000 --- a/code/fms/fms-vue/src/services/moduleManagement.js +++ /dev/null @@ -1,54 +0,0 @@ -import { loadDataApi, nextIdApi, saveObjectApi } from "./api"; - -export const MODULE_ID = "M_MODULE"; -export const LOCALES = [ - { value: "zh-CN", label: "简体中文" }, - { value: "en-US", label: "English" }, - { value: "zh-TW", label: "繁體中文" }, -]; - -export async function loadModuleConfiguration() { - const [modules, fields, powers, translations, userModules, userPowers] = await Promise.all([ - loadDataApi("b_module", null, "b_xh ASC"), - loadDataApi("b_module_field", null, "b_xh ASC"), - loadDataApi("b_module_power", null, "b_xh ASC"), - loadDataApi("b_i18n", null, "b_key ASC"), - loadDataApi("b_user_module", null, "b_id ASC"), - loadDataApi("b_user_power", null, "b_id ASC"), - ]); - return { - modules: modules.data, - fields: fields.data, - powers: powers.data, - translations: translations.data, - userModules: userModules.data, - userPowers: userPowers.data, - }; -} - -export async function nextIds(count) { - const ids = []; - for (let index = 0; index < count; index += 1) { - const response = await nextIdApi(); - ids.push(String(response.data)); - } - return ids; -} - -export function tableRequest(table, changes, keyField = "b_id") { - return { - table, - key_field: keyField, - inserts: changes.inserts || [], - updates: changes.updates || [], - deletes: changes.deletes || [], - }; -} - -export function saveChanges(requests) { - return saveObjectApi( - requests.filter( - (request) => request.inserts.length || request.updates.length || request.deletes.length, - ), - ); -} diff --git a/code/fms/fms-vue/src/stores/auth.js b/code/fms/fms-vue/src/stores/auth.js index 339c3121..9a8480a1 100644 --- a/code/fms/fms-vue/src/stores/auth.js +++ b/code/fms/fms-vue/src/stores/auth.js @@ -1,9 +1,14 @@ import { computed, ref } from "vue"; import { defineStore } from "pinia"; +const AUTH_STORAGE_KEY = "fms-auth"; + try { - // Remove sessions persisted by earlier versions; authentication is session-only now. - localStorage.removeItem("fms-auth"); + // Retain a session created before persistence moved from sessionStorage. + const session = sessionStorage.getItem(AUTH_STORAGE_KEY); + if (session && !localStorage.getItem(AUTH_STORAGE_KEY)) { + localStorage.setItem(AUTH_STORAGE_KEY, session); + } } catch { // Storage can be unavailable in restricted browser contexts. } @@ -39,8 +44,8 @@ export const useAuthStore = defineStore( }, { persist: { - key: "fms-auth", - storage: sessionStorage, + key: AUTH_STORAGE_KEY, + storage: localStorage, pick: ["token", "orgId", "user"], }, }, diff --git a/code/fms/fms-vue/src/stores/permissions.js b/code/fms/fms-vue/src/stores/permissions.js index 60c99a21..1ea73840 100644 --- a/code/fms/fms-vue/src/stores/permissions.js +++ b/code/fms/fms-vue/src/stores/permissions.js @@ -1,6 +1,6 @@ import { computed, ref } from "vue"; import { defineStore } from "pinia"; -import { loadDataApi } from "../services/api"; +import { loadDataApi } from "@/services/api"; import { useAuthStore } from "./auth"; export const usePermissionStore = defineStore("permissions", () => { diff --git a/code/fms/fms-vue/src/views/DashboardView.vue b/code/fms/fms-vue/src/views/DashboardView.vue index 2575245a..2d9ec44a 100644 --- a/code/fms/fms-vue/src/views/DashboardView.vue +++ b/code/fms/fms-vue/src/views/DashboardView.vue @@ -1,51 +1,100 @@ - + + diff --git a/code/fms/fms-vue/src/views/DataListView.vue b/code/fms/fms-vue/src/views/DataListView.vue deleted file mode 100644 index 6222bcf5..00000000 --- a/code/fms/fms-vue/src/views/DataListView.vue +++ /dev/null @@ -1,632 +0,0 @@ - - - - - diff --git a/code/fms/fms-vue/src/views/DetailView.vue b/code/fms/fms-vue/src/views/DetailView.vue deleted file mode 100644 index 56c94546..00000000 --- a/code/fms/fms-vue/src/views/DetailView.vue +++ /dev/null @@ -1,179 +0,0 @@ - - - - - diff --git a/code/fms/fms-vue/src/views/LoginView.vue b/code/fms/fms-vue/src/views/LoginView.vue index 4ffc73a9..32dc9de7 100644 --- a/code/fms/fms-vue/src/views/LoginView.vue +++ b/code/fms/fms-vue/src/views/LoginView.vue @@ -3,8 +3,8 @@ import { onMounted, reactive, ref } from "vue"; import { useRoute, useRouter } from "vue-router"; import { Building2, LockKeyhole, LogIn, MapPin, Ship, Truck, UserRound } from "@lucide/vue"; import { App } from "antdv-next"; -import { loginApi } from "../services/api"; -import { useAuthStore } from "../stores/auth"; +import { loginApi } from "@/services/api"; +import { useAuthStore } from "@/stores/auth"; const LOGIN_IDENTITY_KEY = "fms-login-identity"; diff --git a/code/fms/fms-vue/src/views/ModuleManageDemo.vue b/code/fms/fms-vue/src/views/ModuleManageDemo.vue deleted file mode 100644 index 04c1b449..00000000 --- a/code/fms/fms-vue/src/views/ModuleManageDemo.vue +++ /dev/null @@ -1,1355 +0,0 @@ - - - - - diff --git a/code/fms/fms-vue/src/views/module-management/ModuleManagementView.vue b/code/fms/fms-vue/src/views/module-management/ModuleManagementView.vue index 4deca530..b5ba81ab 100644 --- a/code/fms/fms-vue/src/views/module-management/ModuleManagementView.vue +++ b/code/fms/fms-vue/src/views/module-management/ModuleManagementView.vue @@ -2,15 +2,9 @@ import { computed, onMounted, reactive, ref } from "vue"; import { App } from "antdv-next"; import { Database, FileText, Languages, RotateCcw, Save, ShieldCheck, Trash2 } from "@lucide/vue"; -import { useAuthStore } from "../../stores/auth"; -import { usePermissionStore } from "../../stores/permissions"; -import { - LOCALES, - loadModuleConfiguration, - nextIds, - saveChanges, - tableRequest, -} from "../../services/moduleManagement"; +import { loadDataApi, nextIdApi, saveObjectApi } from "@/services/api"; +import { useAuthStore } from "@/stores/auth"; +import { usePermissionStore } from "@/stores/permissions"; import ModuleBasicInfoPanel from "./components/ModuleBasicInfoPanel.vue"; import ModuleFieldDefinitionPanel from "./components/ModuleFieldDefinitionPanel.vue"; import ModulePowerPanel from "./components/ModulePowerPanel.vue"; @@ -18,6 +12,7 @@ import ModuleTranslationsPanel from "./components/ModuleTranslationsPanel.vue"; import ModuleTreePanel from "./components/ModuleTreePanel.vue"; const { message } = App.useApp(); +const LOCALES = ["zh-CN", "en-US", "zh-TW"]; const authStore = useAuthStore(); const permissionStore = usePermissionStore(); const loading = ref(false); @@ -25,7 +20,6 @@ const saving = ref(false); const selectedKey = ref(""); const activeTab = ref("basic"); const isNew = ref(false); -const customI18nKeys = ref([]); const deleteConfirmOpen = ref(false); const pendingDeleteKey = ref(""); const original = ref(""); @@ -81,7 +75,6 @@ function rowKeys() { draft.module?.b_i18n, ...draft.fields.map((item) => item.b_i18n), ...draft.powers.map((item) => item.b_i18n), - ...customI18nKeys.value, ].filter(Boolean); } @@ -92,7 +85,6 @@ function clearDraft() { draft.fields = []; draft.powers = []; draft.translations = {}; - customI18nKeys.value = []; original.value = ""; activeTab.value = "basic"; } @@ -106,7 +98,6 @@ function selectModule(code) { draft.module = clone(module); draft.fields = clone(data.fields.filter((item) => item.b_module_id === code)); draft.powers = clone(data.powers.filter((item) => item.b_module_id === code)); - customI18nKeys.value = []; const keys = new Set(rowKeys()); draft.translations = {}; @@ -124,7 +115,22 @@ function selectModule(code) { async function loadAll(preferredKey = selectedKey.value) { loading.value = true; try { - Object.assign(data, await loadModuleConfiguration()); + const [modules, fields, powers, translations, userModules, userPowers] = await Promise.all([ + loadDataApi("b_module", null, "b_xh ASC"), + loadDataApi("b_module_field", null, "b_xh ASC"), + loadDataApi("b_module_power", null, "b_xh ASC"), + loadDataApi("b_i18n", null, "b_key ASC"), + loadDataApi("b_user_module", null, "b_id ASC"), + loadDataApi("b_user_power", null, "b_id ASC"), + ]); + Object.assign(data, { + modules: modules.data, + fields: fields.data, + powers: powers.data, + translations: translations.data, + userModules: userModules.data, + userPowers: userPowers.data, + }); const target = data.modules.some((item) => item.b_id === preferredKey) ? preferredKey : ""; if (target) { @@ -179,7 +185,6 @@ function addModule(kind, contextKey) { draft.translations = { ["module." + id]: { "zh-CN": draft.module.b_name }, }; - customI18nKeys.value = []; selectedKey.value = id; isNew.value = true; original.value = ""; @@ -212,52 +217,6 @@ function updateModuleId(value) { }); } -async function addField() { - if (!draft.module) return; - - const [id] = await nextIds(1); - const index = draft.fields.length + 1; - const fieldCode = "b_field" + index; - const i18nKey = "field." + draft.module.b_id + "." + fieldCode; - draft.fields.push({ - b_id: id, - b_module_id: draft.module.b_id, - b_field: fieldCode, - b_name: "新字段", - b_i18n: i18nKey, - b_type: "string", - b_canlist: 1, - b_canform: 1, - b_must: 0, - b_canquery: 0, - b_readonly: 0, - b_width: 120, - b_value: null, - b_xh: index * 10, - b_canuse: 1, - }); - draft.translations[i18nKey] = { "zh-CN": "新字段" }; -} - -async function addPower() { - if (!draft.module) return; - - const [id] = await nextIds(1); - const index = draft.powers.length + 1; - const code = "action" + index; - const i18nKey = "power." + draft.module.b_id + "." + code; - draft.powers.push({ - b_id: id, - b_module_id: draft.module.b_id, - b_code: code, - b_name: "新操作", - b_i18n: i18nKey, - b_xh: index * 10, - b_canuse: 1, - }); - draft.translations[i18nKey] = { "zh-CN": "新操作" }; -} - function normalizeModuleType() { if (draft.module.b_type === "directory") return; draft.module.b_type = draft.module.b_route?.trim() ? "page" : "data"; @@ -330,18 +289,22 @@ async function translationChanges(previousKeys) { for (const key of currentKeys) { for (const locale of LOCALES) { - const value = draft.translations[key]?.[locale.value]?.trim(); + const value = draft.translations[key]?.[locale]?.trim(); if (!value) continue; - const old = oldRows.find((item) => item.b_key === key && item.b_locale === locale.value); + const old = oldRows.find((item) => item.b_key === key && item.b_locale === locale); if (old) { desired.push({ ...old, b_value: value }); } else { - missing.push({ key, locale: locale.value, value }); + missing.push({ key, locale, value }); } } } - const ids = await nextIds(missing.length); + const ids = []; + for (let index = 0; index < missing.length; index += 1) { + const response = await nextIdApi(); + ids.push(String(response.data)); + } const now = new Date().toISOString().slice(0, 19).replace("T", " "); missing.forEach((item, index) => { desired.push({ @@ -400,19 +363,33 @@ async function saveModule() { const requests = []; if (assignedPowerDeletes.length) { - requests.push(tableRequest("b_user_power", { deletes: assignedPowerDeletes })); + requests.push({ + table: "b_user_power", + key_field: "b_id", + inserts: [], + updates: [], + deletes: assignedPowerDeletes, + }); } + const i18nChanges = await translationChanges(previousKeys); requests.push( - tableRequest("b_module", { + { + table: "b_module", + key_field: "b_id", inserts: isNew.value ? [draft.module] : [], updates: isNew.value ? [] : [draft.module], - }), - tableRequest("b_module_field", fieldChanges), - tableRequest("b_module_power", powerChanges), - tableRequest("b_i18n", await translationChanges(previousKeys)), + deletes: [], + }, + { table: "b_module_field", key_field: "b_id", ...fieldChanges }, + { table: "b_module_power", key_field: "b_id", ...powerChanges }, + { table: "b_i18n", key_field: "b_id", ...i18nChanges }, + ); + await saveObjectApi( + requests.filter( + (request) => request.inserts.length || request.updates.length || request.deletes.length, + ), ); - await saveChanges(requests); const savedKey = draft.module.b_id; message.success("模块配置已保存"); @@ -457,26 +434,61 @@ async function deleteModule(moduleId = draft.module?.b_id) { ].filter(Boolean), ); - await saveChanges([ - tableRequest("b_user_power", { + const requests = [ + { + table: "b_user_power", + key_field: "b_id", + inserts: [], + updates: [], deletes: data.userPowers .filter((item) => powerIds.has(String(item.b_power_id))) .map((item) => ({ b_id: item.b_id })), - }), - tableRequest("b_user_module", { + }, + { + table: "b_user_module", + key_field: "b_id", + inserts: [], + updates: [], deletes: data.userModules .filter((item) => item.b_module_id === moduleId) .map((item) => ({ b_id: item.b_id })), - }), - tableRequest("b_module_power", { deletes: powers.map((item) => ({ b_id: item.b_id })) }), - tableRequest("b_module_field", { deletes: fields.map((item) => ({ b_id: item.b_id })) }), - tableRequest("b_i18n", { + }, + { + table: "b_module_power", + key_field: "b_id", + inserts: [], + updates: [], + deletes: powers.map((item) => ({ b_id: item.b_id })), + }, + { + table: "b_module_field", + key_field: "b_id", + inserts: [], + updates: [], + deletes: fields.map((item) => ({ b_id: item.b_id })), + }, + { + table: "b_i18n", + key_field: "b_id", + inserts: [], + updates: [], deletes: data.translations .filter((item) => keys.has(item.b_key)) .map((item) => ({ b_id: item.b_id })), - }), - tableRequest("b_module", { deletes: [{ b_id: moduleId }] }), - ]); + }, + { + table: "b_module", + key_field: "b_id", + inserts: [], + updates: [], + deletes: [{ b_id: moduleId }], + }, + ]; + await saveObjectApi( + requests.filter( + (request) => request.inserts.length || request.updates.length || request.deletes.length, + ), + ); message.success("模块已删除"); deleteConfirmOpen.value = false; @@ -612,12 +624,7 @@ onMounted(async () => { 字段定义 - + - + - + @@ -749,6 +758,16 @@ h1 { overflow: hidden; } +.workspace-empty { + display: flex; + width: 100%; + height: 100%; + align-items: center; + justify-content: center; + flex-direction: column; + margin: 0; +} + :deep(.ant-spin-nested-loading), :deep(.ant-spin-container) { height: 100%; diff --git a/code/fms/fms-vue/src/views/module-management/components/ModuleFieldDefinitionPanel.vue b/code/fms/fms-vue/src/views/module-management/components/ModuleFieldDefinitionPanel.vue index aa3e645a..c156dae6 100644 --- a/code/fms/fms-vue/src/views/module-management/components/ModuleFieldDefinitionPanel.vue +++ b/code/fms/fms-vue/src/views/module-management/components/ModuleFieldDefinitionPanel.vue @@ -1,190 +1,30 @@ @@ -197,52 +37,8 @@ function deleteSelectedFields() { overflow: hidden; } -.table-toolbar { - display: flex; - min-height: 40px; - flex: 0 0 auto; - align-items: center; - gap: 8px; - margin-bottom: 4px; -} - -.field-name-editor, -.cell-name { - display: flex; - align-items: center; - gap: 4px; -} - -.field-name-editor .ant-input { - min-width: 0; -} - -.i18n-button { - flex: 0 0 auto; - padding-inline: 5px; -} - -.cell-name svg { - flex: 0 0 auto; - color: #8c8c8c; -} - -.cell-code { - color: #475569; - font-family: Consolas, monospace; - font-size: 12px; -} - -:deep(.editing-row > td) { - background: #f0f7ff !important; -} - -:deep(.ant-table-row) { - cursor: pointer; -} - -:deep(.ant-table-cell .ant-input-number), -:deep(.ant-table-cell .ant-select) { - width: 100%; +.sub-table { + flex: 1 1 auto; + min-height: 0; } diff --git a/code/fms/fms-vue/src/views/module-management/components/ModulePowerPanel.vue b/code/fms/fms-vue/src/views/module-management/components/ModulePowerPanel.vue index 177aa9be..146e609c 100644 --- a/code/fms/fms-vue/src/views/module-management/components/ModulePowerPanel.vue +++ b/code/fms/fms-vue/src/views/module-management/components/ModulePowerPanel.vue @@ -1,120 +1,25 @@ @@ -127,30 +32,8 @@ function deleteSelectedPowers() { overflow: hidden; } -.table-toolbar { - display: flex; - min-height: 40px; - flex: 0 0 auto; - align-items: center; - gap: 8px; - margin-bottom: 4px; -} - -.cell-code { - color: #475569; - font-family: Consolas, monospace; - font-size: 12px; -} - -:deep(.editing-row > td) { - background: #f0f7ff !important; -} - -:deep(.ant-table-row) { - cursor: pointer; -} - -:deep(.ant-table-cell .ant-input-number) { - width: 100%; +.sub-table { + flex: 1 1 auto; + min-height: 0; } diff --git a/code/fms/fms-vue/src/views/module-management/components/ModuleTranslationsPanel.vue b/code/fms/fms-vue/src/views/module-management/components/ModuleTranslationsPanel.vue index eb1b0efd..00a1d336 100644 --- a/code/fms/fms-vue/src/views/module-management/components/ModuleTranslationsPanel.vue +++ b/code/fms/fms-vue/src/views/module-management/components/ModuleTranslationsPanel.vue @@ -1,112 +1,43 @@ @@ -119,22 +50,8 @@ function removeCustomKey(key) { overflow: hidden; } -.table-toolbar { - display: flex; - min-height: 40px; - flex: 0 0 auto; - align-items: center; - gap: 8px; - margin-bottom: 4px; -} - -.new-key-input { - width: 260px; -} - -.translation-key { - color: #374151; - font-family: Consolas, monospace; - font-size: 12px; +.sub-table { + flex: 1 1 auto; + min-height: 0; } diff --git a/code/fms/fms-vue/src/views/module-management/components/ModuleTreePanel.vue b/code/fms/fms-vue/src/views/module-management/components/ModuleTreePanel.vue index a2210c93..611ff7dd 100644 --- a/code/fms/fms-vue/src/views/module-management/components/ModuleTreePanel.vue +++ b/code/fms/fms-vue/src/views/module-management/components/ModuleTreePanel.vue @@ -1,6 +1,7 @@