From 49dafe524ce1e2600393bd5e760fe97b5c42370a Mon Sep 17 00:00:00 2001 From: oneao Date: Thu, 16 Jul 2026 21:55:51 +0800 Subject: [PATCH] 20260716215550 --- .gitignore | 10 +- .../fmsapi/database/OrgDataSourceFactory.java | 3 + .../database/OrgDatabaseConfigLoader.java | 3 +- .../exception/AuthenticationException.java | 8 + .../exception/GlobalExceptionHandler.java | 28 + .../cn/g3soft/fmsapi/service/DataService.java | 8 +- .../database/OrgDataSourceFactoryTests.java | 30 + .../OrgDatabaseConfigLoaderTests.java | 33 ++ .../GlobalExceptionHandlerTests.java | 27 + code/fms/fms-vue/package.json | 1 + code/fms/fms-vue/pnpm-lock.yaml | 232 ++++++++ .../src/layouts/components/AppToolbar.vue | 13 +- code/fms/fms-vue/src/router/index.js | 22 + code/fms/fms-vue/src/services/api.js | 58 +- code/fms/fms-vue/src/services/http.js | 86 ++- code/fms/fms-vue/src/stores/app.js | 68 ++- code/fms/fms-vue/src/stores/auth.js | 16 +- code/fms/fms-vue/src/views/DataListView.vue | 303 ++++++---- code/fms/fms-vue/src/views/LoginView.vue | 533 +++++++++++++++--- code/fms/fms-vue/vite.config.js | 6 + code/fms/软件需求.md | 2 + 21 files changed, 1218 insertions(+), 272 deletions(-) create mode 100644 code/fms/fms-api/src/main/java/cn/g3soft/fmsapi/exception/AuthenticationException.java create mode 100644 code/fms/fms-api/src/test/java/cn/g3soft/fmsapi/database/OrgDataSourceFactoryTests.java create mode 100644 code/fms/fms-api/src/test/java/cn/g3soft/fmsapi/database/OrgDatabaseConfigLoaderTests.java create mode 100644 code/fms/fms-api/src/test/java/cn/g3soft/fmsapi/exception/GlobalExceptionHandlerTests.java diff --git a/.gitignore b/.gitignore index 41b91f56..6829b28d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,10 @@ # 忽略 software 文件夹及其所有内容 -software/ \ No newline at end of file +software/ + +# 忽略 Windows 保留设备名文件(NUL、CON、PRN 等) +**/NUL +**/CON +**/PRN +**/AUX +**/COM[0-9] +**/LPT[0-9] \ No newline at end of file diff --git a/code/fms/fms-api/src/main/java/cn/g3soft/fmsapi/database/OrgDataSourceFactory.java b/code/fms/fms-api/src/main/java/cn/g3soft/fmsapi/database/OrgDataSourceFactory.java index bfa89f00..c95f8aee 100644 --- a/code/fms/fms-api/src/main/java/cn/g3soft/fmsapi/database/OrgDataSourceFactory.java +++ b/code/fms/fms-api/src/main/java/cn/g3soft/fmsapi/database/OrgDataSourceFactory.java @@ -28,6 +28,9 @@ public class OrgDataSourceFactory { dataSource.setTestWhileIdle(poolProperties.isTestWhileIdle()); dataSource.setTestOnBorrow(poolProperties.isTestOnBorrow()); dataSource.setTestOnReturn(poolProperties.isTestOnReturn()); + dataSource.setConnectionErrorRetryAttempts(0); + dataSource.setBreakAfterAcquireFailure(true); + dataSource.setFailFast(true); dataSource.setTimeBetweenEvictionRunsMillis( poolProperties.getTimeBetweenEvictionRuns().toMillis() ); diff --git a/code/fms/fms-api/src/main/java/cn/g3soft/fmsapi/database/OrgDatabaseConfigLoader.java b/code/fms/fms-api/src/main/java/cn/g3soft/fmsapi/database/OrgDatabaseConfigLoader.java index ebeee826..82cafdf7 100644 --- a/code/fms/fms-api/src/main/java/cn/g3soft/fmsapi/database/OrgDatabaseConfigLoader.java +++ b/code/fms/fms-api/src/main/java/cn/g3soft/fmsapi/database/OrgDatabaseConfigLoader.java @@ -1,5 +1,6 @@ package cn.g3soft.fmsapi.database; +import cn.g3soft.fmsapi.exception.AuthenticationException; import cn.g3soft.fmsapi.exception.BusinessException; import jakarta.annotation.PostConstruct; import jakarta.annotation.Resource; @@ -39,7 +40,7 @@ public class OrgDatabaseConfigLoader { throw new IllegalArgumentException("机构配置文件路径不合法"); } if (!Files.isRegularFile(configFile)) { - throw new IllegalStateException("未找到机构数据库配置文件: " + configFile); + throw new AuthenticationException("机构码错误"); } Properties properties = new Properties(); diff --git a/code/fms/fms-api/src/main/java/cn/g3soft/fmsapi/exception/AuthenticationException.java b/code/fms/fms-api/src/main/java/cn/g3soft/fmsapi/exception/AuthenticationException.java new file mode 100644 index 00000000..52cfef2a --- /dev/null +++ b/code/fms/fms-api/src/main/java/cn/g3soft/fmsapi/exception/AuthenticationException.java @@ -0,0 +1,8 @@ +package cn.g3soft.fmsapi.exception; + +public class AuthenticationException extends BusinessException { + + public AuthenticationException(String message) { + super(message); + } +} diff --git a/code/fms/fms-api/src/main/java/cn/g3soft/fmsapi/exception/GlobalExceptionHandler.java b/code/fms/fms-api/src/main/java/cn/g3soft/fmsapi/exception/GlobalExceptionHandler.java index 5128998c..18776c8a 100644 --- a/code/fms/fms-api/src/main/java/cn/g3soft/fmsapi/exception/GlobalExceptionHandler.java +++ b/code/fms/fms-api/src/main/java/cn/g3soft/fmsapi/exception/GlobalExceptionHandler.java @@ -4,23 +4,48 @@ import cn.g3soft.fmsapi.utils.ApiResponse; import jakarta.servlet.http.HttpServletRequest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.dao.DataAccessException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; +import java.sql.SQLException; + @RestControllerAdvice public class GlobalExceptionHandler { private static final Logger LOGGER = LoggerFactory.getLogger(GlobalExceptionHandler.class); + @ExceptionHandler(AuthenticationException.class) + public ApiResponse handleAuthenticationException(AuthenticationException exception) { + return ApiResponse.fail(ApiResponse.AUTH_ERROR_CODE, exception.getMessage()); + } + @ExceptionHandler(BusinessException.class) public ApiResponse handleBusinessException(BusinessException exception) { return ApiResponse.fail(exception.getMessage()); } + @ExceptionHandler({SQLException.class, DataAccessException.class}) + public ApiResponse handleDatabaseException( + Exception exception, + HttpServletRequest request + ) { + logSystemException(exception, request); + return systemErrorResponse(); + } + @ExceptionHandler(Exception.class) public ApiResponse handleException( Exception exception, HttpServletRequest request + ) { + logSystemException(exception, request); + return systemErrorResponse(); + } + + private void logSystemException( + Exception exception, + HttpServletRequest request ) { LOGGER.error( "系统异常,method={},uri={}", @@ -28,6 +53,9 @@ public class GlobalExceptionHandler { request.getRequestURI(), exception ); + } + + private ApiResponse systemErrorResponse() { return ApiResponse.fail( ApiResponse.SYSTEM_ERROR_CODE, "系统异常,请稍后重试" diff --git a/code/fms/fms-api/src/main/java/cn/g3soft/fmsapi/service/DataService.java b/code/fms/fms-api/src/main/java/cn/g3soft/fmsapi/service/DataService.java index 2af4b673..f59cd964 100644 --- a/code/fms/fms-api/src/main/java/cn/g3soft/fmsapi/service/DataService.java +++ b/code/fms/fms-api/src/main/java/cn/g3soft/fmsapi/service/DataService.java @@ -17,10 +17,10 @@ public class DataService { public List> loadData(Map params) throws SQLException { return dbUtils.loadData( - ParamUtils.getRequiredString(params, "viewName"), - ParamUtils.getString(params, "searchCondition"), - ParamUtils.getString(params, "orderField"), - ParamUtils.getList(params, "searchColumns") + ParamUtils.getRequiredString(params, "view_name"), + ParamUtils.getString(params, "search_condition"), + ParamUtils.getString(params, "order_field"), + ParamUtils.getList(params, "search_columns") ); } diff --git a/code/fms/fms-api/src/test/java/cn/g3soft/fmsapi/database/OrgDataSourceFactoryTests.java b/code/fms/fms-api/src/test/java/cn/g3soft/fmsapi/database/OrgDataSourceFactoryTests.java new file mode 100644 index 00000000..da2356ca --- /dev/null +++ b/code/fms/fms-api/src/test/java/cn/g3soft/fmsapi/database/OrgDataSourceFactoryTests.java @@ -0,0 +1,30 @@ +package cn.g3soft.fmsapi.database; + +import com.alibaba.druid.pool.DruidDataSource; +import org.junit.jupiter.api.Test; +import org.springframework.test.util.ReflectionTestUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +class OrgDataSourceFactoryTests { + + @Test + void databaseConnectionFailureDoesNotRetry() { + OrgDatabaseProperties properties = new OrgDatabaseProperties(); + OrgDataSourceFactory factory = new OrgDataSourceFactory(); + ReflectionTestUtils.setField(factory, "properties", properties); + + OrgDatabaseConfig config = new OrgDatabaseConfig( + "jdbc:sqlserver://127.0.0.1:1433;databaseName=test", + "test", + "test", + "com.microsoft.sqlserver.jdbc.SQLServerDriver" + ); + + try (DruidDataSource dataSource = factory.create("TEST", config)) { + assertThat(dataSource.getConnectionErrorRetryAttempts()).isZero(); + assertThat(dataSource.isBreakAfterAcquireFailure()).isTrue(); + assertThat(dataSource.isFailFast()).isTrue(); + } + } +} diff --git a/code/fms/fms-api/src/test/java/cn/g3soft/fmsapi/database/OrgDatabaseConfigLoaderTests.java b/code/fms/fms-api/src/test/java/cn/g3soft/fmsapi/database/OrgDatabaseConfigLoaderTests.java new file mode 100644 index 00000000..4b5bd1a0 --- /dev/null +++ b/code/fms/fms-api/src/test/java/cn/g3soft/fmsapi/database/OrgDatabaseConfigLoaderTests.java @@ -0,0 +1,33 @@ +package cn.g3soft.fmsapi.database; + +import cn.g3soft.fmsapi.exception.AuthenticationException; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.springframework.test.util.ReflectionTestUtils; + +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class OrgDatabaseConfigLoaderTests { + + @TempDir + Path configDir; + + @Test + void missingConfigFileIsReportedAsInvalidOrgId() { + OrgDatabaseProperties properties = new OrgDatabaseProperties(); + properties.setConfigDir(configDir); + + OrgDatabaseConfigLoader loader = new OrgDatabaseConfigLoader(); + ReflectionTestUtils.setField(loader, "properties", properties); + loader.init(); + + AuthenticationException exception = assertThrows( + AuthenticationException.class, + () -> loader.load("UNKNOWN") + ); + assertEquals("机构码错误", exception.getMessage()); + } +} diff --git a/code/fms/fms-api/src/test/java/cn/g3soft/fmsapi/exception/GlobalExceptionHandlerTests.java b/code/fms/fms-api/src/test/java/cn/g3soft/fmsapi/exception/GlobalExceptionHandlerTests.java new file mode 100644 index 00000000..1c533686 --- /dev/null +++ b/code/fms/fms-api/src/test/java/cn/g3soft/fmsapi/exception/GlobalExceptionHandlerTests.java @@ -0,0 +1,27 @@ +package cn.g3soft.fmsapi.exception; + +import cn.g3soft.fmsapi.utils.ApiResponse; +import org.junit.jupiter.api.Test; +import org.springframework.mock.web.MockHttpServletRequest; + +import java.sql.SQLException; + +import static org.assertj.core.api.Assertions.assertThat; + +class GlobalExceptionHandlerTests { + + @Test + void databaseExceptionReturnsSystemError() { + GlobalExceptionHandler handler = new GlobalExceptionHandler(); + MockHttpServletRequest request = new MockHttpServletRequest("POST", "/api/auth/login"); + + ApiResponse response = handler.handleDatabaseException( + new SQLException("connection failed"), + request + ); + + assertThat(response.code()).isEqualTo(ApiResponse.SYSTEM_ERROR_CODE); + assertThat(response.message()).isEqualTo("系统异常,请稍后重试"); + assertThat(response.data()).isNull(); + } +} diff --git a/code/fms/fms-vue/package.json b/code/fms/fms-vue/package.json index 01a7b5f3..cb43045e 100644 --- a/code/fms/fms-vue/package.json +++ b/code/fms/fms-vue/package.json @@ -13,6 +13,7 @@ "dependencies": { "@lucide/vue": "^1.24.0", "antdv-next": "^1.4.2", + "axios": "^1.18.1", "nprogress": "^0.2.0", "pinia": "^4.0.2", "pinia-plugin-persistedstate": "^4.7.1", diff --git a/code/fms/fms-vue/pnpm-lock.yaml b/code/fms/fms-vue/pnpm-lock.yaml index 277e0208..98f5aab6 100644 --- a/code/fms/fms-vue/pnpm-lock.yaml +++ b/code/fms/fms-vue/pnpm-lock.yaml @@ -14,6 +14,9 @@ importers: antdv-next: specifier: ^1.4.2 version: 1.4.2(vue@3.5.39) + axios: + specifier: ^1.18.1 + version: 1.18.1 nprogress: specifier: ^0.2.0 version: 0.2.0 @@ -888,6 +891,10 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + antdv-next@1.4.2: resolution: {integrity: sha512-3hVPjxjuRN8gHQnmaZgPs5PYrwlkmX7pDDMdsrb1NUGdrF8qvUp4vpFb1B+eGkJRjfWKG1xxyyHw7jQsE8xprw==} @@ -899,13 +906,27 @@ packages: resolution: {integrity: sha512-IJdzo2vLiElBxKzwS36VsCue/62d6IdWjnPB2v3nuPKeWGynp6FF/CYoLa5i/3jXH/z97ZDdsXz6abpgM6w07A==} engines: {node: '>=20.19.0'} + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + axios@1.18.1: + resolution: {integrity: sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==} + birpc@2.9.0: resolution: {integrity: sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==} + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + chokidar@5.0.0: resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} engines: {node: '>= 20.19.0'} + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + compute-scroll-into-view@3.1.1: resolution: {integrity: sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==} @@ -921,13 +942,30 @@ packages: dayjs@1.11.21: resolution: {integrity: sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==} + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + defu@6.1.7: resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==} + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + enhanced-resolve@5.21.6: resolution: {integrity: sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==} engines: {node: '>=10.13.0'} @@ -936,6 +974,22 @@ packages: resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} engines: {node: '>=0.12'} + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.2: + resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + es-toolkit@1.49.0: resolution: {integrity: sha512-G5iZ6Pc/FNRY/soKZHC+TxGDD83rHUDXxzaWhGCX44vAv/tMs56WMusnm/KMNK+luUPsgA9U28cGr4RDlSzL2g==} @@ -961,17 +1015,61 @@ packages: picomatch: optional: true + follow-redirects@1.16.0: + resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + form-data@4.0.6: + resolution: {integrity: sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==} + engines: {node: '>= 6'} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} + hookable@5.5.3: resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} + https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + immutable@5.1.9: resolution: {integrity: sha512-m8nVez3rwrgmWxtLMt1ZYXB2Lv7OKYn/disyxAlSDYAlKSlFoPPfIAmAM/M5xqL4m4C/wAPw7S2/CNaUii1Hxg==} @@ -1085,9 +1183,24 @@ packages: magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + mlly@1.8.2: resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + muggle-string@0.4.1: resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} @@ -1169,6 +1282,10 @@ packages: resolution: {integrity: sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==} engines: {node: ^10 || ^12 || >=14} + proxy-from-env@2.1.0: + resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==} + engines: {node: '>=10'} + quansync@0.2.11: resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} @@ -2093,6 +2210,12 @@ snapshots: acorn@8.17.0: {} + agent-base@6.0.2: + dependencies: + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + antdv-next@1.4.2(vue@3.5.39): dependencies: '@ant-design/colors': 8.0.1 @@ -2159,12 +2282,33 @@ snapshots: '@babel/types': 7.29.7 ast-kit: 2.2.0 + asynckit@0.4.0: {} + + axios@1.18.1: + dependencies: + follow-redirects: 1.16.0 + form-data: 4.0.6 + https-proxy-agent: 5.0.1 + proxy-from-env: 2.1.0 + transitivePeerDependencies: + - debug + - supports-color + birpc@2.9.0: {} + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + chokidar@5.0.0: dependencies: readdirp: 5.0.0 + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + compute-scroll-into-view@3.1.1: {} confbox@0.1.8: {} @@ -2175,10 +2319,22 @@ snapshots: dayjs@1.11.21: {} + debug@4.4.3: + dependencies: + ms: 2.1.3 + defu@6.1.7: {} + delayed-stream@1.0.0: {} + detect-libc@2.1.2: {} + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + enhanced-resolve@5.21.6: dependencies: graceful-fs: 4.2.11 @@ -2186,6 +2342,21 @@ snapshots: entities@7.0.1: {} + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.2: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.4 + es-toolkit@1.49.0: {} escape-string-regexp@5.0.0: {} @@ -2202,13 +2373,62 @@ snapshots: optionalDependencies: picomatch: 4.0.5 + follow-redirects@1.16.0: {} + + form-data@4.0.6: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.4 + mime-types: 2.1.35 + fsevents@2.3.3: optional: true + function-bind@1.1.2: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.4 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.2 + + gopd@1.2.0: {} + graceful-fs@4.2.11: {} + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 + hookable@5.5.3: {} + https-proxy-agent@5.0.1: + dependencies: + agent-base: 6.0.2 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + immutable@5.1.9: {} is-extglob@2.1.1: @@ -2290,6 +2510,14 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + math-intrinsics@1.1.0: {} + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + mlly@1.8.2: dependencies: acorn: 8.17.0 @@ -2297,6 +2525,8 @@ snapshots: pkg-types: 1.3.1 ufo: 1.6.4 + ms@2.1.3: {} + muggle-string@0.4.1: {} nanoid@3.3.16: {} @@ -2372,6 +2602,8 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + proxy-from-env@2.1.0: {} + quansync@0.2.11: {} readdirp@5.0.0: {} diff --git a/code/fms/fms-vue/src/layouts/components/AppToolbar.vue b/code/fms/fms-vue/src/layouts/components/AppToolbar.vue index 82dffaaf..19456606 100644 --- a/code/fms/fms-vue/src/layouts/components/AppToolbar.vue +++ b/code/fms/fms-vue/src/layouts/components/AppToolbar.vue @@ -43,6 +43,8 @@ const userMenu = { onClick: handleUserMenu, }; +const displayName = computed(() => authStore.user?.name || authStore.user?.id || "当前用户"); + watch( () => appStore.locale, (locale) => document.documentElement.setAttribute("lang", locale), @@ -57,7 +59,7 @@ function handleUserMenu({ key }) { function confirmLogout() { authStore.clearSession(); logoutOpen.value = false; - router.replace("/dashboard"); + router.replace("/login"); } @@ -85,7 +87,7 @@ function confirmLogout() { @@ -94,10 +96,9 @@ function confirmLogout() { - admin - 管理员 - 系统管理员 - {{ authStore.orgid || "--" }} + {{ authStore.user?.id || "--" }} + {{ authStore.user?.name || "--" }} + {{ authStore.orgId || "--" }} diff --git a/code/fms/fms-vue/src/router/index.js b/code/fms/fms-vue/src/router/index.js index c7f063b3..4d7cfbde 100644 --- a/code/fms/fms-vue/src/router/index.js +++ b/code/fms/fms-vue/src/router/index.js @@ -1,13 +1,21 @@ import { createRouter, createWebHistory } from "vue-router"; import NProgress from "nprogress"; import "nprogress/nprogress.css"; +import { useAuthStore } from "../stores/auth"; NProgress.configure({ showSpinner: false }); +function getRedirectPath(value) { + return typeof value === "string" && value.startsWith("/") && !value.startsWith("//") + ? value + : "/dashboard"; +} + const routes = [ { path: "/", component: () => import("../layouts/DefaultLayout.vue"), + meta: { requiresAuth: true }, children: [ { path: "", @@ -117,6 +125,12 @@ const routes = [ }, ], }, + { + path: "/login", + name: "login", + component: () => import("../views/LoginView.vue"), + meta: { title: "登录" }, + }, { path: "/:pathMatch(.*)*", redirect: "/dashboard", @@ -133,6 +147,14 @@ router.beforeEach((to) => { NProgress.start(); document.title = to.meta.title ? `${to.meta.title} - FMS 管理系统` : "FMS 管理系统"; + const authStore = useAuthStore(); + if (to.meta.requiresAuth && !authStore.isAuthenticated) { + return { name: "login", query: { redirect: to.fullPath } }; + } + if (to.name === "login" && authStore.isAuthenticated) { + return getRedirectPath(to.query.redirect); + } + return true; }); diff --git a/code/fms/fms-vue/src/services/api.js b/code/fms/fms-vue/src/services/api.js index 425bd49f..19a9fe50 100644 --- a/code/fms/fms-vue/src/services/api.js +++ b/code/fms/fms-vue/src/services/api.js @@ -1,22 +1,42 @@ import { post } from "./http"; -export const authApi = { - login(credentials) { - return post("/api/auth/login", credentials); - }, -}; +/** + * 机构用户登录。 + * @param {string} orgId 机构码。 + * @param {string} userId 用户 ID。 + * @param {string} password 密码。 + * @returns {Promise<{token: string, orgid: string, user: {id: string, name: string}}>} 登录会话。 + */ +export function loginApi(orgId, userId, password) { + return post("/auth/login", { + orgid: orgId, + userid: userId, + password, + }); +} -export const dataApi = { - loadData(params) { - return post("/api/data/loaddata", params); - }, - loadDataBySql(sql) { - return post("/api/data/loaddatabysql", { sql }); - }, - page(params) { - return post("/api/data/page", params); - }, - saveObject(tables) { - return post("/api/data/saveobjt", { tables }); - }, -}; +/** + * 按表或视图加载数据。 + * @param {string} viewName 表名或视图名,必填。 + * @param {string} [searchCondition] 查询条件,不包含 WHERE。 + * @param {string} [orderField] 排序字段,可带 ASC 或 DESC。 + * @param {string[]} [searchColumns] 需要返回的字段列表,省略时返回全部字段。 + * @returns {Promise} 查询结果行。 + */ +export function loadDataApi(viewName, searchCondition, orderField, searchColumns) { + return post("/data/loaddata", { + view_name: viewName, + search_condition: searchCondition, + order_field: orderField, + search_columns: searchColumns, + }); +} + +/** + * 执行只读 SQL 查询。 + * @param {string} sql 完整的 SELECT 或 WITH 查询语句。 + * @returns {Promise} 查询结果行。 + */ +export function loadDataBySqlApi(sql) { + return post("/data/loaddatabysql", { sql }); +} diff --git a/code/fms/fms-vue/src/services/http.js b/code/fms/fms-vue/src/services/http.js index 839b2d7e..f4c21ded 100644 --- a/code/fms/fms-vue/src/services/http.js +++ b/code/fms/fms-vue/src/services/http.js @@ -1,40 +1,72 @@ +import axios from "axios"; import { useAuthStore } from "../stores/auth"; -const API_BASE_URL = import.meta.env.VITE_API_BASE_URL ?? ""; +const http = axios.create({ + baseURL: import.meta.env.VITE_API_BASE_URL || "/api", + timeout: 15000, + headers: { + Accept: "application/json", + "Content-Type": "application/json", + }, +}); -export async function request(path, options = {}) { +let redirectingToLogin = false; + +function redirectToLogin() { + const currentPath = window.location.pathname.replace(/\/+$/, "") || "/"; + if (currentPath === "/login" || redirectingToLogin) return; + + redirectingToLogin = true; + const redirect = `${window.location.pathname}${window.location.search}${window.location.hash}`; + window.location.assign(`/login?redirect=${encodeURIComponent(redirect)}`); +} + +function handleUnauthorized() { + const authStore = useAuthStore(); + authStore.clearSession(); + redirectToLogin(); +} + +http.interceptors.request.use((config) => { const authStore = useAuthStore(); - const headers = new Headers(options.headers); - headers.set("Accept", "application/json"); - if (options.body && !headers.has("Content-Type")) { - headers.set("Content-Type", "application/json"); - } if (authStore.token) { - headers.set("Authorization", `Bearer ${authStore.token}`); + config.headers.Authorization = `Bearer ${authStore.token}`; } - const response = await fetch(`${API_BASE_URL}${path}`, { - ...options, - headers, - }); - const payload = await response.json().catch(() => null); + return config; +}); - if (!response.ok) { - if (response.status === 401) authStore.clearSession(); - throw new Error(payload?.message ?? `请求失败 (${response.status})`); - } +http.interceptors.response.use( + (response) => { + const payload = response.data; - if (payload?.code !== undefined && payload.code !== 0) { - throw new Error(payload.message ?? "请求失败"); - } + if (payload?.code === undefined) return payload; + if (payload.code === 0) return payload.data; - return payload?.data ?? payload; + if (payload.code === 401) handleUnauthorized(); + + const error = new Error(payload.message || "请求失败"); + error.code = payload.code; + throw error; + }, + (error) => { + const status = error.response?.status; + const payload = error.response?.data; + + if (status === 401 || payload?.code === 401) handleUnauthorized(); + + const requestError = new Error( + payload?.message || + (error.code === "ECONNABORTED" ? "请求超时,请稍后重试" : "网络异常,请检查服务是否正常"), + ); + requestError.code = payload?.code ?? status; + throw requestError; + }, +); + +export function post(path, data, config) { + return http.post(path, data, config); } -export function post(path, body) { - return request(path, { - method: "POST", - body: JSON.stringify(body), - }); -} +export default http; diff --git a/code/fms/fms-vue/src/stores/app.js b/code/fms/fms-vue/src/stores/app.js index 9290dbc0..809ec067 100644 --- a/code/fms/fms-vue/src/stores/app.js +++ b/code/fms/fms-vue/src/stores/app.js @@ -1,35 +1,47 @@ +import { ref } from "vue"; import { defineStore } from "pinia"; const createDefaultVisitedTabs = () => [{ key: "/dashboard", label: "工作台", closable: false }]; -export const useAppStore = defineStore("app", { - state: () => ({ - locale: "zh-CN", - sidebarCollapsed: false, - visitedTabs: createDefaultVisitedTabs(), - }), - actions: { - toggleSidebar() { - this.sidebarCollapsed = !this.sidebarCollapsed; - }, - setLocale(locale) { - this.locale = locale; - }, - addVisitedTab(tab) { - if (!this.visitedTabs.some((item) => item.key === tab.key)) { - this.visitedTabs.push(tab); +export const useAppStore = defineStore( + "app", + () => { + const locale = ref("zh-CN"); + const sidebarCollapsed = ref(false); + const visitedTabs = ref(createDefaultVisitedTabs()); + + function toggleSidebar() { + sidebarCollapsed.value = !sidebarCollapsed.value; + } + + function setLocale(value) { + locale.value = value; + } + + function addVisitedTab(tab) { + if (!visitedTabs.value.some((item) => item.key === tab.key)) { + visitedTabs.value.push(tab); } - }, - removeVisitedTab(key) { - this.visitedTabs = this.visitedTabs.filter((item) => item.key !== key); + } + + function removeVisitedTab(key) { + visitedTabs.value = visitedTabs.value.filter((item) => item.key !== key); + } + + return { + locale, + sidebarCollapsed, + visitedTabs, + toggleSidebar, + setLocale, + addVisitedTab, + removeVisitedTab, + }; + }, + { + persist: { + key: "fms-app", + pick: ["locale"], }, }, - persist: { - key: "fms-app", - pick: ["locale"], - afterHydrate: ({ store }) => { - store.sidebarCollapsed = false; - store.visitedTabs = createDefaultVisitedTabs(); - }, - }, -}); +); diff --git a/code/fms/fms-vue/src/stores/auth.js b/code/fms/fms-vue/src/stores/auth.js index d8dedb68..97f1661b 100644 --- a/code/fms/fms-vue/src/stores/auth.js +++ b/code/fms/fms-vue/src/stores/auth.js @@ -5,25 +5,25 @@ export const useAuthStore = defineStore( "auth", () => { const token = ref(""); - const orgid = ref(""); + const orgId = ref(""); const user = ref(null); const isAuthenticated = computed(() => Boolean(token.value)); - function setSession(payload, organization) { - token.value = payload.token ?? payload.access_token ?? ""; - orgid.value = payload.orgid ?? organization; - user.value = payload.user ?? payload.user_info ?? null; + function setSession(session, organizationId) { + token.value = session.token ?? session.access_token ?? ""; + orgId.value = session.orgid ?? organizationId; + user.value = session.user ?? session.user_info ?? null; } function clearSession() { token.value = ""; - orgid.value = ""; + orgId.value = ""; user.value = null; } return { token, - orgid, + orgId, user, isAuthenticated, setSession, @@ -33,7 +33,7 @@ export const useAuthStore = defineStore( { persist: { key: "fms-auth", - pick: ["token", "orgid", "user"], + pick: ["token", "orgId", "user"], }, }, ); diff --git a/code/fms/fms-vue/src/views/DataListView.vue b/code/fms/fms-vue/src/views/DataListView.vue index 5fe4ac6b..4cabfe49 100644 --- a/code/fms/fms-vue/src/views/DataListView.vue +++ b/code/fms/fms-vue/src/views/DataListView.vue @@ -2,11 +2,13 @@ import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from "vue"; import { useRoute, useRouter } from "vue-router"; import { Ellipsis, ListFilter, Plus, Search } from "@lucide/vue"; -import { mockRecords } from "../data/mockRecords"; +import { loadDataApi } from "../services/api"; const route = useRoute(); const router = useRouter(); const loading = ref(false); +const loadError = ref(""); +const sourceRows = ref([]); const rows = ref([]); const total = ref(0); const filters = reactive({ field: "name", keyword: "" }); @@ -16,6 +18,7 @@ const tableScrollY = ref(240); const advancedOpen = ref(false); const appliedFilterCount = ref(0); let resizeObserver; +let loadSequence = 0; const columns = [ { title: "编号", dataIndex: "id", key: "id", width: 160 }, @@ -74,20 +77,130 @@ function isFilterValueFilled(value) { if (Array.isArray(value)) return value.some(isFilterValueFilled); return value !== undefined && value !== null && value !== ""; } -function loadData() { - loading.value = true; + +function getRowValue(row, candidates) { + const keys = Object.keys(row); + for (const candidate of candidates) { + const matchedKey = keys.find((key) => key.toLowerCase() === candidate.toLowerCase()); + if (matchedKey !== undefined && row[matchedKey] != null) return row[matchedKey]; + } + return undefined; +} + +function normalizeRow(row, index) { + const id = getRowValue(row, ["id", "b_id", "user_id", "userid"]); + const name = getRowValue(row, ["name", "b_name", "customer_name", "title"]); + const state = getRowValue(row, ["state", "b_state", "status"]); + const updatedAt = getRowValue(row, ["updated_at", "update_time", "updated_time", "b_updated_at"]); + + return { + ...row, + id: id == null ? "" : String(id), + name: name == null ? "" : String(name), + state: state == null ? "" : String(state), + updated_at: updatedAt == null ? "" : String(updatedAt), + __rowKey: `${String(route.meta.source)}-${String(id ?? index)}`, + }; +} + +function includesValue(row, candidates, expected) { + if (!isFilterValueFilled(expected)) return true; + const actual = getRowValue(row, candidates); + return String(actual ?? "") + .toLowerCase() + .includes(String(expected).trim().toLowerCase()); +} + +function isInDateRange(row, candidates, range) { + if (!Array.isArray(range) || !range.some(Boolean)) return true; + const actual = String(getRowValue(row, candidates) ?? "").slice(0, 10); + if (!actual) return false; + const start = range[0]?.format?.("YYYY-MM-DD"); + const end = range[1]?.format?.("YYYY-MM-DD"); + return (!start || actual >= start) && (!end || actual <= end); +} + +function matchesAdvancedFilters(row) { + const amount = Number( + String(getRowValue(row, ["amount", "total_amount", "b_amount"]) ?? "").replaceAll(",", ""), + ); + + return ( + includesValue(row, ["id", "b_id", "document_no"], advancedFilters.documentNo) && + includesValue( + row, + ["customer", "customer_name", "name", "b_name"], + advancedFilters.customerName, + ) && + includesValue(row, ["business_type", "businessType", "b_type"], advancedFilters.businessType) && + includesValue(row, ["state", "b_state", "status"], advancedFilters.status) && + isInDateRange(row, ["business_date", "b_date"], advancedFilters.businessDate) && + includesValue(row, ["owner", "owner_name", "sales_name"], advancedFilters.owner) && + includesValue(row, ["department", "department_name"], advancedFilters.department) && + includesValue( + row, + ["settlement_status", "settlementStatus"], + advancedFilters.settlementStatus, + ) && + includesValue(row, ["currency", "currency_code"], advancedFilters.currency) && + (!isFilterValueFilled(advancedFilters.amountMin) || + (Number.isFinite(amount) && amount >= advancedFilters.amountMin)) && + (!isFilterValueFilled(advancedFilters.amountMax) || + (Number.isFinite(amount) && amount <= advancedFilters.amountMax)) && + includesValue(row, ["creator", "created_by", "creator_name"], advancedFilters.creator) && + isInDateRange( + row, + ["created_at", "create_time", "created_time"], + advancedFilters.createdDate, + ) && + includesValue(row, ["remark", "remarks", "memo"], advancedFilters.remark) + ); +} + +function applyLocalFilters() { const keyword = filters.keyword.trim().toLowerCase(); - const filteredRows = keyword - ? mockRecords.filter((row) => + const filteredRows = sourceRows.value.filter( + (row) => + (!keyword || String(row[filters.field] ?? "") .toLowerCase() - .includes(keyword), - ) - : mockRecords; + .includes(keyword)) && + matchesAdvancedFilters(row), + ); + const pageCount = Math.max(Math.ceil(filteredRows.length / pagination.pageSize), 1); + if (pagination.current > pageCount) pagination.current = pageCount; const start = (pagination.current - 1) * pagination.pageSize; rows.value = filteredRows.slice(start, start + pagination.pageSize); total.value = filteredRows.length; - loading.value = false; +} + +function searchData() { + pagination.current = 1; + applyLocalFilters(); +} + +async function loadData() { + const currentSequence = ++loadSequence; + const viewName = String(route.meta.source || ""); + if (!viewName) return; + + loading.value = true; + loadError.value = ""; + + try { + const result = await loadDataApi(viewName); + if (currentSequence !== loadSequence) return; + sourceRows.value = (Array.isArray(result) ? result : []).map(normalizeRow); + applyLocalFilters(); + } catch (error) { + if (currentSequence !== loadSequence) return; + sourceRows.value = []; + rows.value = []; + total.value = 0; + loadError.value = error.message || "数据加载失败"; + } finally { + if (currentSequence === loadSequence) loading.value = false; + } } function openDetail(record) { @@ -97,7 +210,7 @@ function openDetail(record) { function changePage(page, pageSize) { pagination.current = page; pagination.pageSize = pageSize; - loadData(); + applyLocalFilters(); } function resetAdvancedFilters() { @@ -108,7 +221,7 @@ function applyAdvancedFilters() { pagination.current = 1; appliedFilterCount.value = draftFilterCount.value; advancedOpen.value = false; - loadData(); + applyLocalFilters(); } function updateTableHeight() { @@ -128,7 +241,17 @@ onMounted(() => { onBeforeUnmount(() => resizeObserver?.disconnect()); -watch(() => route.meta.source, loadData, { immediate: true }); +watch( + () => route.meta.source, + () => { + pagination.current = 1; + filters.keyword = ""; + resetAdvancedFilters(); + appliedFilterCount.value = 0; + loadData(); + }, + { immediate: true }, +);