20260716215550
This commit is contained in:
1 parent
5cd4e1e1d2
commit
49dafe524c
21 files changed
+1218
-272
No files matched your search
+9
-1
@@ -1,2 +1,10 @@
|
|||||||
# 忽略 software 文件夹及其所有内容
|
# 忽略 software 文件夹及其所有内容
|
||||||
software/
|
software/
|
||||||
|
|
||||||
|
# 忽略 Windows 保留设备名文件(NUL、CON、PRN 等)
|
||||||
|
**/NUL
|
||||||
|
**/CON
|
||||||
|
**/PRN
|
||||||
|
**/AUX
|
||||||
|
**/COM[0-9]
|
||||||
|
**/LPT[0-9]
|
||||||
@@ -28,6 +28,9 @@ public class OrgDataSourceFactory {
|
|||||||
dataSource.setTestWhileIdle(poolProperties.isTestWhileIdle());
|
dataSource.setTestWhileIdle(poolProperties.isTestWhileIdle());
|
||||||
dataSource.setTestOnBorrow(poolProperties.isTestOnBorrow());
|
dataSource.setTestOnBorrow(poolProperties.isTestOnBorrow());
|
||||||
dataSource.setTestOnReturn(poolProperties.isTestOnReturn());
|
dataSource.setTestOnReturn(poolProperties.isTestOnReturn());
|
||||||
|
dataSource.setConnectionErrorRetryAttempts(0);
|
||||||
|
dataSource.setBreakAfterAcquireFailure(true);
|
||||||
|
dataSource.setFailFast(true);
|
||||||
dataSource.setTimeBetweenEvictionRunsMillis(
|
dataSource.setTimeBetweenEvictionRunsMillis(
|
||||||
poolProperties.getTimeBetweenEvictionRuns().toMillis()
|
poolProperties.getTimeBetweenEvictionRuns().toMillis()
|
||||||
);
|
);
|
||||||
|
|||||||
+2
-1
@@ -1,5 +1,6 @@
|
|||||||
package cn.g3soft.fmsapi.database;
|
package cn.g3soft.fmsapi.database;
|
||||||
|
|
||||||
|
import cn.g3soft.fmsapi.exception.AuthenticationException;
|
||||||
import cn.g3soft.fmsapi.exception.BusinessException;
|
import cn.g3soft.fmsapi.exception.BusinessException;
|
||||||
import jakarta.annotation.PostConstruct;
|
import jakarta.annotation.PostConstruct;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
@@ -39,7 +40,7 @@ public class OrgDatabaseConfigLoader {
|
|||||||
throw new IllegalArgumentException("机构配置文件路径不合法");
|
throw new IllegalArgumentException("机构配置文件路径不合法");
|
||||||
}
|
}
|
||||||
if (!Files.isRegularFile(configFile)) {
|
if (!Files.isRegularFile(configFile)) {
|
||||||
throw new IllegalStateException("未找到机构数据库配置文件: " + configFile);
|
throw new AuthenticationException("机构码错误");
|
||||||
}
|
}
|
||||||
|
|
||||||
Properties properties = new Properties();
|
Properties properties = new Properties();
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package cn.g3soft.fmsapi.exception;
|
||||||
|
|
||||||
|
public class AuthenticationException extends BusinessException {
|
||||||
|
|
||||||
|
public AuthenticationException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,23 +4,48 @@ import cn.g3soft.fmsapi.utils.ApiResponse;
|
|||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.dao.DataAccessException;
|
||||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||||
|
|
||||||
|
import java.sql.SQLException;
|
||||||
|
|
||||||
@RestControllerAdvice
|
@RestControllerAdvice
|
||||||
public class GlobalExceptionHandler {
|
public class GlobalExceptionHandler {
|
||||||
|
|
||||||
private static final Logger LOGGER = LoggerFactory.getLogger(GlobalExceptionHandler.class);
|
private static final Logger LOGGER = LoggerFactory.getLogger(GlobalExceptionHandler.class);
|
||||||
|
|
||||||
|
@ExceptionHandler(AuthenticationException.class)
|
||||||
|
public ApiResponse<Void> handleAuthenticationException(AuthenticationException exception) {
|
||||||
|
return ApiResponse.fail(ApiResponse.AUTH_ERROR_CODE, exception.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
@ExceptionHandler(BusinessException.class)
|
@ExceptionHandler(BusinessException.class)
|
||||||
public ApiResponse<Void> handleBusinessException(BusinessException exception) {
|
public ApiResponse<Void> handleBusinessException(BusinessException exception) {
|
||||||
return ApiResponse.fail(exception.getMessage());
|
return ApiResponse.fail(exception.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler({SQLException.class, DataAccessException.class})
|
||||||
|
public ApiResponse<Void> handleDatabaseException(
|
||||||
|
Exception exception,
|
||||||
|
HttpServletRequest request
|
||||||
|
) {
|
||||||
|
logSystemException(exception, request);
|
||||||
|
return systemErrorResponse();
|
||||||
|
}
|
||||||
|
|
||||||
@ExceptionHandler(Exception.class)
|
@ExceptionHandler(Exception.class)
|
||||||
public ApiResponse<Void> handleException(
|
public ApiResponse<Void> handleException(
|
||||||
Exception exception,
|
Exception exception,
|
||||||
HttpServletRequest request
|
HttpServletRequest request
|
||||||
|
) {
|
||||||
|
logSystemException(exception, request);
|
||||||
|
return systemErrorResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void logSystemException(
|
||||||
|
Exception exception,
|
||||||
|
HttpServletRequest request
|
||||||
) {
|
) {
|
||||||
LOGGER.error(
|
LOGGER.error(
|
||||||
"系统异常,method={},uri={}",
|
"系统异常,method={},uri={}",
|
||||||
@@ -28,6 +53,9 @@ public class GlobalExceptionHandler {
|
|||||||
request.getRequestURI(),
|
request.getRequestURI(),
|
||||||
exception
|
exception
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ApiResponse<Void> systemErrorResponse() {
|
||||||
return ApiResponse.fail(
|
return ApiResponse.fail(
|
||||||
ApiResponse.SYSTEM_ERROR_CODE,
|
ApiResponse.SYSTEM_ERROR_CODE,
|
||||||
"系统异常,请稍后重试"
|
"系统异常,请稍后重试"
|
||||||
|
|||||||
@@ -17,10 +17,10 @@ public class DataService {
|
|||||||
|
|
||||||
public List<Map<String, Object>> loadData(Map<String, Object> params) throws SQLException {
|
public List<Map<String, Object>> loadData(Map<String, Object> params) throws SQLException {
|
||||||
return dbUtils.loadData(
|
return dbUtils.loadData(
|
||||||
ParamUtils.getRequiredString(params, "viewName"),
|
ParamUtils.getRequiredString(params, "view_name"),
|
||||||
ParamUtils.getString(params, "searchCondition"),
|
ParamUtils.getString(params, "search_condition"),
|
||||||
ParamUtils.getString(params, "orderField"),
|
ParamUtils.getString(params, "order_field"),
|
||||||
ParamUtils.getList(params, "searchColumns")
|
ParamUtils.getList(params, "search_columns")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+30
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+33
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
+27
@@ -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<Void> response = handler.handleDatabaseException(
|
||||||
|
new SQLException("connection failed"),
|
||||||
|
request
|
||||||
|
);
|
||||||
|
|
||||||
|
assertThat(response.code()).isEqualTo(ApiResponse.SYSTEM_ERROR_CODE);
|
||||||
|
assertThat(response.message()).isEqualTo("系统异常,请稍后重试");
|
||||||
|
assertThat(response.data()).isNull();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@lucide/vue": "^1.24.0",
|
"@lucide/vue": "^1.24.0",
|
||||||
"antdv-next": "^1.4.2",
|
"antdv-next": "^1.4.2",
|
||||||
|
"axios": "^1.18.1",
|
||||||
"nprogress": "^0.2.0",
|
"nprogress": "^0.2.0",
|
||||||
"pinia": "^4.0.2",
|
"pinia": "^4.0.2",
|
||||||
"pinia-plugin-persistedstate": "^4.7.1",
|
"pinia-plugin-persistedstate": "^4.7.1",
|
||||||
|
|||||||
Generated
+232
@@ -14,6 +14,9 @@ importers:
|
|||||||
antdv-next:
|
antdv-next:
|
||||||
specifier: ^1.4.2
|
specifier: ^1.4.2
|
||||||
version: 1.4.2(vue@3.5.39)
|
version: 1.4.2(vue@3.5.39)
|
||||||
|
axios:
|
||||||
|
specifier: ^1.18.1
|
||||||
|
version: 1.18.1
|
||||||
nprogress:
|
nprogress:
|
||||||
specifier: ^0.2.0
|
specifier: ^0.2.0
|
||||||
version: 0.2.0
|
version: 0.2.0
|
||||||
@@ -888,6 +891,10 @@ packages:
|
|||||||
engines: {node: '>=0.4.0'}
|
engines: {node: '>=0.4.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
agent-base@6.0.2:
|
||||||
|
resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==}
|
||||||
|
engines: {node: '>= 6.0.0'}
|
||||||
|
|
||||||
antdv-next@1.4.2:
|
antdv-next@1.4.2:
|
||||||
resolution: {integrity: sha512-3hVPjxjuRN8gHQnmaZgPs5PYrwlkmX7pDDMdsrb1NUGdrF8qvUp4vpFb1B+eGkJRjfWKG1xxyyHw7jQsE8xprw==}
|
resolution: {integrity: sha512-3hVPjxjuRN8gHQnmaZgPs5PYrwlkmX7pDDMdsrb1NUGdrF8qvUp4vpFb1B+eGkJRjfWKG1xxyyHw7jQsE8xprw==}
|
||||||
|
|
||||||
@@ -899,13 +906,27 @@ packages:
|
|||||||
resolution: {integrity: sha512-IJdzo2vLiElBxKzwS36VsCue/62d6IdWjnPB2v3nuPKeWGynp6FF/CYoLa5i/3jXH/z97ZDdsXz6abpgM6w07A==}
|
resolution: {integrity: sha512-IJdzo2vLiElBxKzwS36VsCue/62d6IdWjnPB2v3nuPKeWGynp6FF/CYoLa5i/3jXH/z97ZDdsXz6abpgM6w07A==}
|
||||||
engines: {node: '>=20.19.0'}
|
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:
|
birpc@2.9.0:
|
||||||
resolution: {integrity: sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==}
|
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:
|
chokidar@5.0.0:
|
||||||
resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==}
|
resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==}
|
||||||
engines: {node: '>= 20.19.0'}
|
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:
|
compute-scroll-into-view@3.1.1:
|
||||||
resolution: {integrity: sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==}
|
resolution: {integrity: sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==}
|
||||||
|
|
||||||
@@ -921,13 +942,30 @@ packages:
|
|||||||
dayjs@1.11.21:
|
dayjs@1.11.21:
|
||||||
resolution: {integrity: sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==}
|
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:
|
defu@6.1.7:
|
||||||
resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==}
|
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:
|
detect-libc@2.1.2:
|
||||||
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
|
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
dunder-proto@1.0.1:
|
||||||
|
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
enhanced-resolve@5.21.6:
|
enhanced-resolve@5.21.6:
|
||||||
resolution: {integrity: sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==}
|
resolution: {integrity: sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==}
|
||||||
engines: {node: '>=10.13.0'}
|
engines: {node: '>=10.13.0'}
|
||||||
@@ -936,6 +974,22 @@ packages:
|
|||||||
resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==}
|
resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==}
|
||||||
engines: {node: '>=0.12'}
|
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:
|
es-toolkit@1.49.0:
|
||||||
resolution: {integrity: sha512-G5iZ6Pc/FNRY/soKZHC+TxGDD83rHUDXxzaWhGCX44vAv/tMs56WMusnm/KMNK+luUPsgA9U28cGr4RDlSzL2g==}
|
resolution: {integrity: sha512-G5iZ6Pc/FNRY/soKZHC+TxGDD83rHUDXxzaWhGCX44vAv/tMs56WMusnm/KMNK+luUPsgA9U28cGr4RDlSzL2g==}
|
||||||
|
|
||||||
@@ -961,17 +1015,61 @@ packages:
|
|||||||
picomatch:
|
picomatch:
|
||||||
optional: true
|
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:
|
fsevents@2.3.3:
|
||||||
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
||||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||||
os: [darwin]
|
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:
|
graceful-fs@4.2.11:
|
||||||
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
|
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:
|
hookable@5.5.3:
|
||||||
resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==}
|
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:
|
immutable@5.1.9:
|
||||||
resolution: {integrity: sha512-m8nVez3rwrgmWxtLMt1ZYXB2Lv7OKYn/disyxAlSDYAlKSlFoPPfIAmAM/M5xqL4m4C/wAPw7S2/CNaUii1Hxg==}
|
resolution: {integrity: sha512-m8nVez3rwrgmWxtLMt1ZYXB2Lv7OKYn/disyxAlSDYAlKSlFoPPfIAmAM/M5xqL4m4C/wAPw7S2/CNaUii1Hxg==}
|
||||||
|
|
||||||
@@ -1085,9 +1183,24 @@ packages:
|
|||||||
magic-string@0.30.21:
|
magic-string@0.30.21:
|
||||||
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
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:
|
mlly@1.8.2:
|
||||||
resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==}
|
resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==}
|
||||||
|
|
||||||
|
ms@2.1.3:
|
||||||
|
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||||
|
|
||||||
muggle-string@0.4.1:
|
muggle-string@0.4.1:
|
||||||
resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==}
|
resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==}
|
||||||
|
|
||||||
@@ -1169,6 +1282,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==}
|
resolution: {integrity: sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==}
|
||||||
engines: {node: ^10 || ^12 || >=14}
|
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:
|
quansync@0.2.11:
|
||||||
resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==}
|
resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==}
|
||||||
|
|
||||||
@@ -2093,6 +2210,12 @@ snapshots:
|
|||||||
|
|
||||||
acorn@8.17.0: {}
|
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):
|
antdv-next@1.4.2(vue@3.5.39):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@ant-design/colors': 8.0.1
|
'@ant-design/colors': 8.0.1
|
||||||
@@ -2159,12 +2282,33 @@ snapshots:
|
|||||||
'@babel/types': 7.29.7
|
'@babel/types': 7.29.7
|
||||||
ast-kit: 2.2.0
|
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: {}
|
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:
|
chokidar@5.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
readdirp: 5.0.0
|
readdirp: 5.0.0
|
||||||
|
|
||||||
|
combined-stream@1.0.8:
|
||||||
|
dependencies:
|
||||||
|
delayed-stream: 1.0.0
|
||||||
|
|
||||||
compute-scroll-into-view@3.1.1: {}
|
compute-scroll-into-view@3.1.1: {}
|
||||||
|
|
||||||
confbox@0.1.8: {}
|
confbox@0.1.8: {}
|
||||||
@@ -2175,10 +2319,22 @@ snapshots:
|
|||||||
|
|
||||||
dayjs@1.11.21: {}
|
dayjs@1.11.21: {}
|
||||||
|
|
||||||
|
debug@4.4.3:
|
||||||
|
dependencies:
|
||||||
|
ms: 2.1.3
|
||||||
|
|
||||||
defu@6.1.7: {}
|
defu@6.1.7: {}
|
||||||
|
|
||||||
|
delayed-stream@1.0.0: {}
|
||||||
|
|
||||||
detect-libc@2.1.2: {}
|
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:
|
enhanced-resolve@5.21.6:
|
||||||
dependencies:
|
dependencies:
|
||||||
graceful-fs: 4.2.11
|
graceful-fs: 4.2.11
|
||||||
@@ -2186,6 +2342,21 @@ snapshots:
|
|||||||
|
|
||||||
entities@7.0.1: {}
|
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: {}
|
es-toolkit@1.49.0: {}
|
||||||
|
|
||||||
escape-string-regexp@5.0.0: {}
|
escape-string-regexp@5.0.0: {}
|
||||||
@@ -2202,13 +2373,62 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
picomatch: 4.0.5
|
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:
|
fsevents@2.3.3:
|
||||||
optional: true
|
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: {}
|
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: {}
|
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: {}
|
immutable@5.1.9: {}
|
||||||
|
|
||||||
is-extglob@2.1.1:
|
is-extglob@2.1.1:
|
||||||
@@ -2290,6 +2510,14 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/sourcemap-codec': 1.5.5
|
'@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:
|
mlly@1.8.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
acorn: 8.17.0
|
acorn: 8.17.0
|
||||||
@@ -2297,6 +2525,8 @@ snapshots:
|
|||||||
pkg-types: 1.3.1
|
pkg-types: 1.3.1
|
||||||
ufo: 1.6.4
|
ufo: 1.6.4
|
||||||
|
|
||||||
|
ms@2.1.3: {}
|
||||||
|
|
||||||
muggle-string@0.4.1: {}
|
muggle-string@0.4.1: {}
|
||||||
|
|
||||||
nanoid@3.3.16: {}
|
nanoid@3.3.16: {}
|
||||||
@@ -2372,6 +2602,8 @@ snapshots:
|
|||||||
picocolors: 1.1.1
|
picocolors: 1.1.1
|
||||||
source-map-js: 1.2.1
|
source-map-js: 1.2.1
|
||||||
|
|
||||||
|
proxy-from-env@2.1.0: {}
|
||||||
|
|
||||||
quansync@0.2.11: {}
|
quansync@0.2.11: {}
|
||||||
|
|
||||||
readdirp@5.0.0: {}
|
readdirp@5.0.0: {}
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ const userMenu = {
|
|||||||
onClick: handleUserMenu,
|
onClick: handleUserMenu,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const displayName = computed(() => authStore.user?.name || authStore.user?.id || "当前用户");
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => appStore.locale,
|
() => appStore.locale,
|
||||||
(locale) => document.documentElement.setAttribute("lang", locale),
|
(locale) => document.documentElement.setAttribute("lang", locale),
|
||||||
@@ -57,7 +59,7 @@ function handleUserMenu({ key }) {
|
|||||||
function confirmLogout() {
|
function confirmLogout() {
|
||||||
authStore.clearSession();
|
authStore.clearSession();
|
||||||
logoutOpen.value = false;
|
logoutOpen.value = false;
|
||||||
router.replace("/dashboard");
|
router.replace("/login");
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -85,7 +87,7 @@ function confirmLogout() {
|
|||||||
<a-dropdown :menu="userMenu" :trigger="['click']" placement="bottomRight">
|
<a-dropdown :menu="userMenu" :trigger="['click']" placement="bottomRight">
|
||||||
<button type="button" class="toolbar-user" aria-label="用户菜单">
|
<button type="button" class="toolbar-user" aria-label="用户菜单">
|
||||||
<a-avatar :size="24"><UserRound /></a-avatar>
|
<a-avatar :size="24"><UserRound /></a-avatar>
|
||||||
<span>管理员</span>
|
<span>{{ displayName }}</span>
|
||||||
<ChevronDown class="toolbar-user__arrow" />
|
<ChevronDown class="toolbar-user__arrow" />
|
||||||
</button>
|
</button>
|
||||||
</a-dropdown>
|
</a-dropdown>
|
||||||
@@ -94,10 +96,9 @@ function confirmLogout() {
|
|||||||
|
|
||||||
<a-modal v-model:open="profileOpen" title="个人资料" :footer="null" width="420px">
|
<a-modal v-model:open="profileOpen" title="个人资料" :footer="null" width="420px">
|
||||||
<a-descriptions :column="1" bordered size="small">
|
<a-descriptions :column="1" bordered size="small">
|
||||||
<a-descriptions-item label="用户名">admin</a-descriptions-item>
|
<a-descriptions-item label="用户ID">{{ authStore.user?.id || "--" }}</a-descriptions-item>
|
||||||
<a-descriptions-item label="姓名">管理员</a-descriptions-item>
|
<a-descriptions-item label="姓名">{{ authStore.user?.name || "--" }}</a-descriptions-item>
|
||||||
<a-descriptions-item label="角色">系统管理员</a-descriptions-item>
|
<a-descriptions-item label="机构码">{{ authStore.orgId || "--" }}</a-descriptions-item>
|
||||||
<a-descriptions-item label="机构码">{{ authStore.orgid || "--" }}</a-descriptions-item>
|
|
||||||
</a-descriptions>
|
</a-descriptions>
|
||||||
</a-modal>
|
</a-modal>
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,21 @@
|
|||||||
import { createRouter, createWebHistory } from "vue-router";
|
import { createRouter, createWebHistory } from "vue-router";
|
||||||
import NProgress from "nprogress";
|
import NProgress from "nprogress";
|
||||||
import "nprogress/nprogress.css";
|
import "nprogress/nprogress.css";
|
||||||
|
import { useAuthStore } from "../stores/auth";
|
||||||
|
|
||||||
NProgress.configure({ showSpinner: false });
|
NProgress.configure({ showSpinner: false });
|
||||||
|
|
||||||
|
function getRedirectPath(value) {
|
||||||
|
return typeof value === "string" && value.startsWith("/") && !value.startsWith("//")
|
||||||
|
? value
|
||||||
|
: "/dashboard";
|
||||||
|
}
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{
|
{
|
||||||
path: "/",
|
path: "/",
|
||||||
component: () => import("../layouts/DefaultLayout.vue"),
|
component: () => import("../layouts/DefaultLayout.vue"),
|
||||||
|
meta: { requiresAuth: true },
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: "",
|
path: "",
|
||||||
@@ -117,6 +125,12 @@ const routes = [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/login",
|
||||||
|
name: "login",
|
||||||
|
component: () => import("../views/LoginView.vue"),
|
||||||
|
meta: { title: "登录" },
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/:pathMatch(.*)*",
|
path: "/:pathMatch(.*)*",
|
||||||
redirect: "/dashboard",
|
redirect: "/dashboard",
|
||||||
@@ -133,6 +147,14 @@ router.beforeEach((to) => {
|
|||||||
NProgress.start();
|
NProgress.start();
|
||||||
document.title = to.meta.title ? `${to.meta.title} - FMS 管理系统` : "FMS 管理系统";
|
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;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +1,42 @@
|
|||||||
import { post } from "./http";
|
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);
|
* @param {string} viewName 表名或视图名,必填。
|
||||||
},
|
* @param {string} [searchCondition] 查询条件,不包含 WHERE。
|
||||||
loadDataBySql(sql) {
|
* @param {string} [orderField] 排序字段,可带 ASC 或 DESC。
|
||||||
return post("/api/data/loaddatabysql", { sql });
|
* @param {string[]} [searchColumns] 需要返回的字段列表,省略时返回全部字段。
|
||||||
},
|
* @returns {Promise<object[]>} 查询结果行。
|
||||||
page(params) {
|
*/
|
||||||
return post("/api/data/page", params);
|
export function loadDataApi(viewName, searchCondition, orderField, searchColumns) {
|
||||||
},
|
return post("/data/loaddata", {
|
||||||
saveObject(tables) {
|
view_name: viewName,
|
||||||
return post("/api/data/saveobjt", { tables });
|
search_condition: searchCondition,
|
||||||
},
|
order_field: orderField,
|
||||||
};
|
search_columns: searchColumns,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行只读 SQL 查询。
|
||||||
|
* @param {string} sql 完整的 SELECT 或 WITH 查询语句。
|
||||||
|
* @returns {Promise<object[]>} 查询结果行。
|
||||||
|
*/
|
||||||
|
export function loadDataBySqlApi(sql) {
|
||||||
|
return post("/data/loaddatabysql", { sql });
|
||||||
|
}
|
||||||
@@ -1,40 +1,72 @@
|
|||||||
|
import axios from "axios";
|
||||||
import { useAuthStore } from "../stores/auth";
|
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 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) {
|
if (authStore.token) {
|
||||||
headers.set("Authorization", `Bearer ${authStore.token}`);
|
config.headers.Authorization = `Bearer ${authStore.token}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(`${API_BASE_URL}${path}`, {
|
return config;
|
||||||
...options,
|
});
|
||||||
headers,
|
|
||||||
});
|
|
||||||
const payload = await response.json().catch(() => null);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
http.interceptors.response.use(
|
||||||
if (response.status === 401) authStore.clearSession();
|
(response) => {
|
||||||
throw new Error(payload?.message ?? `请求失败 (${response.status})`);
|
const payload = response.data;
|
||||||
}
|
|
||||||
|
|
||||||
if (payload?.code !== undefined && payload.code !== 0) {
|
if (payload?.code === undefined) return payload;
|
||||||
throw new Error(payload.message ?? "请求失败");
|
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) {
|
export default http;
|
||||||
return request(path, {
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify(body),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,35 +1,47 @@
|
|||||||
|
import { ref } from "vue";
|
||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
|
|
||||||
const createDefaultVisitedTabs = () => [{ key: "/dashboard", label: "工作台", closable: false }];
|
const createDefaultVisitedTabs = () => [{ key: "/dashboard", label: "工作台", closable: false }];
|
||||||
|
|
||||||
export const useAppStore = defineStore("app", {
|
export const useAppStore = defineStore(
|
||||||
state: () => ({
|
"app",
|
||||||
locale: "zh-CN",
|
() => {
|
||||||
sidebarCollapsed: false,
|
const locale = ref("zh-CN");
|
||||||
visitedTabs: createDefaultVisitedTabs(),
|
const sidebarCollapsed = ref(false);
|
||||||
}),
|
const visitedTabs = ref(createDefaultVisitedTabs());
|
||||||
actions: {
|
|
||||||
toggleSidebar() {
|
function toggleSidebar() {
|
||||||
this.sidebarCollapsed = !this.sidebarCollapsed;
|
sidebarCollapsed.value = !sidebarCollapsed.value;
|
||||||
},
|
}
|
||||||
setLocale(locale) {
|
|
||||||
this.locale = locale;
|
function setLocale(value) {
|
||||||
},
|
locale.value = value;
|
||||||
addVisitedTab(tab) {
|
}
|
||||||
if (!this.visitedTabs.some((item) => item.key === tab.key)) {
|
|
||||||
this.visitedTabs.push(tab);
|
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();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -5,25 +5,25 @@ export const useAuthStore = defineStore(
|
|||||||
"auth",
|
"auth",
|
||||||
() => {
|
() => {
|
||||||
const token = ref("");
|
const token = ref("");
|
||||||
const orgid = ref("");
|
const orgId = ref("");
|
||||||
const user = ref(null);
|
const user = ref(null);
|
||||||
const isAuthenticated = computed(() => Boolean(token.value));
|
const isAuthenticated = computed(() => Boolean(token.value));
|
||||||
|
|
||||||
function setSession(payload, organization) {
|
function setSession(session, organizationId) {
|
||||||
token.value = payload.token ?? payload.access_token ?? "";
|
token.value = session.token ?? session.access_token ?? "";
|
||||||
orgid.value = payload.orgid ?? organization;
|
orgId.value = session.orgid ?? organizationId;
|
||||||
user.value = payload.user ?? payload.user_info ?? null;
|
user.value = session.user ?? session.user_info ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearSession() {
|
function clearSession() {
|
||||||
token.value = "";
|
token.value = "";
|
||||||
orgid.value = "";
|
orgId.value = "";
|
||||||
user.value = null;
|
user.value = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
token,
|
token,
|
||||||
orgid,
|
orgId,
|
||||||
user,
|
user,
|
||||||
isAuthenticated,
|
isAuthenticated,
|
||||||
setSession,
|
setSession,
|
||||||
@@ -33,7 +33,7 @@ export const useAuthStore = defineStore(
|
|||||||
{
|
{
|
||||||
persist: {
|
persist: {
|
||||||
key: "fms-auth",
|
key: "fms-auth",
|
||||||
pick: ["token", "orgid", "user"],
|
pick: ["token", "orgId", "user"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -2,11 +2,13 @@
|
|||||||
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from "vue";
|
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from "vue";
|
||||||
import { useRoute, useRouter } from "vue-router";
|
import { useRoute, useRouter } from "vue-router";
|
||||||
import { Ellipsis, ListFilter, Plus, Search } from "@lucide/vue";
|
import { Ellipsis, ListFilter, Plus, Search } from "@lucide/vue";
|
||||||
import { mockRecords } from "../data/mockRecords";
|
import { loadDataApi } from "../services/api";
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
const loadError = ref("");
|
||||||
|
const sourceRows = ref([]);
|
||||||
const rows = ref([]);
|
const rows = ref([]);
|
||||||
const total = ref(0);
|
const total = ref(0);
|
||||||
const filters = reactive({ field: "name", keyword: "" });
|
const filters = reactive({ field: "name", keyword: "" });
|
||||||
@@ -16,6 +18,7 @@ const tableScrollY = ref(240);
|
|||||||
const advancedOpen = ref(false);
|
const advancedOpen = ref(false);
|
||||||
const appliedFilterCount = ref(0);
|
const appliedFilterCount = ref(0);
|
||||||
let resizeObserver;
|
let resizeObserver;
|
||||||
|
let loadSequence = 0;
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{ title: "编号", dataIndex: "id", key: "id", width: 160 },
|
{ title: "编号", dataIndex: "id", key: "id", width: 160 },
|
||||||
@@ -74,20 +77,130 @@ function isFilterValueFilled(value) {
|
|||||||
if (Array.isArray(value)) return value.some(isFilterValueFilled);
|
if (Array.isArray(value)) return value.some(isFilterValueFilled);
|
||||||
return value !== undefined && value !== null && value !== "";
|
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 keyword = filters.keyword.trim().toLowerCase();
|
||||||
const filteredRows = keyword
|
const filteredRows = sourceRows.value.filter(
|
||||||
? mockRecords.filter((row) =>
|
(row) =>
|
||||||
|
(!keyword ||
|
||||||
String(row[filters.field] ?? "")
|
String(row[filters.field] ?? "")
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.includes(keyword),
|
.includes(keyword)) &&
|
||||||
)
|
matchesAdvancedFilters(row),
|
||||||
: mockRecords;
|
);
|
||||||
|
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;
|
const start = (pagination.current - 1) * pagination.pageSize;
|
||||||
rows.value = filteredRows.slice(start, start + pagination.pageSize);
|
rows.value = filteredRows.slice(start, start + pagination.pageSize);
|
||||||
total.value = filteredRows.length;
|
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) {
|
function openDetail(record) {
|
||||||
@@ -97,7 +210,7 @@ function openDetail(record) {
|
|||||||
function changePage(page, pageSize) {
|
function changePage(page, pageSize) {
|
||||||
pagination.current = page;
|
pagination.current = page;
|
||||||
pagination.pageSize = pageSize;
|
pagination.pageSize = pageSize;
|
||||||
loadData();
|
applyLocalFilters();
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetAdvancedFilters() {
|
function resetAdvancedFilters() {
|
||||||
@@ -108,7 +221,7 @@ function applyAdvancedFilters() {
|
|||||||
pagination.current = 1;
|
pagination.current = 1;
|
||||||
appliedFilterCount.value = draftFilterCount.value;
|
appliedFilterCount.value = draftFilterCount.value;
|
||||||
advancedOpen.value = false;
|
advancedOpen.value = false;
|
||||||
loadData();
|
applyLocalFilters();
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateTableHeight() {
|
function updateTableHeight() {
|
||||||
@@ -128,7 +241,17 @@ onMounted(() => {
|
|||||||
|
|
||||||
onBeforeUnmount(() => resizeObserver?.disconnect());
|
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 },
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -153,7 +276,7 @@ watch(() => route.meta.source, loadData, { immediate: true });
|
|||||||
class="keyword-search"
|
class="keyword-search"
|
||||||
allow-clear
|
allow-clear
|
||||||
enter-button="搜索"
|
enter-button="搜索"
|
||||||
@search="loadData"
|
@search="searchData"
|
||||||
/>
|
/>
|
||||||
</a-space-compact>
|
</a-space-compact>
|
||||||
|
|
||||||
@@ -179,13 +302,16 @@ watch(() => route.meta.source, loadData, { immediate: true });
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="table-panel">
|
<section class="table-panel">
|
||||||
|
<a-alert v-if="loadError" class="load-alert" type="error" show-icon :message="loadError">
|
||||||
|
<template #action><a-button size="small" @click="loadData">重试</a-button></template>
|
||||||
|
</a-alert>
|
||||||
<div
|
<div
|
||||||
ref="tableAreaRef"
|
ref="tableAreaRef"
|
||||||
class="table-area"
|
class="table-area"
|
||||||
:style="{ '--table-body-height': `${tableScrollY}px` }"
|
:style="{ '--table-body-height': `${tableScrollY}px` }"
|
||||||
>
|
>
|
||||||
<a-table
|
<a-table
|
||||||
row-key="id"
|
row-key="__rowKey"
|
||||||
:columns="columns"
|
:columns="columns"
|
||||||
:data-source="rows"
|
:data-source="rows"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
@@ -196,7 +322,7 @@ watch(() => route.meta.source, loadData, { immediate: true });
|
|||||||
>
|
>
|
||||||
<template #bodyCell="{ column, text, record }">
|
<template #bodyCell="{ column, text, record }">
|
||||||
<a-button
|
<a-button
|
||||||
v-if="column.key === 'id'"
|
v-if="column.key === 'id' && record.id"
|
||||||
type="link"
|
type="link"
|
||||||
class="table-id-link"
|
class="table-id-link"
|
||||||
@click="openDetail(record)"
|
@click="openDetail(record)"
|
||||||
@@ -232,89 +358,69 @@ watch(() => route.meta.source, loadData, { immediate: true });
|
|||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<a-form layout="vertical" class="advanced-filter-form">
|
<a-form layout="vertical" class="advanced-filter-form">
|
||||||
<a-form-item label="单据编号">
|
<a-form-item label="单据编号">
|
||||||
<a-input v-model:value="advancedFilters.documentNo" allow-clear />
|
<a-input v-model:value="advancedFilters.documentNo" allow-clear />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="客户名称">
|
<a-form-item label="客户名称">
|
||||||
<a-input
|
<a-input v-model:value="advancedFilters.customerName" allow-clear />
|
||||||
v-model:value="advancedFilters.customerName"
|
</a-form-item>
|
||||||
allow-clear
|
<a-form-item label="业务类型">
|
||||||
/>
|
<a-select
|
||||||
</a-form-item>
|
v-model:value="advancedFilters.businessType"
|
||||||
<a-form-item label="业务类型">
|
allow-clear
|
||||||
<a-select
|
:options="businessTypeOptions"
|
||||||
v-model:value="advancedFilters.businessType"
|
/>
|
||||||
allow-clear
|
</a-form-item>
|
||||||
:options="businessTypeOptions"
|
<a-form-item label="业务状态">
|
||||||
/>
|
<a-select v-model:value="advancedFilters.status" allow-clear :options="statusOptions" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="业务状态">
|
<a-form-item label="业务日期" class="filter-span-full">
|
||||||
<a-select
|
<a-range-picker
|
||||||
v-model:value="advancedFilters.status"
|
v-model:value="advancedFilters.businessDate"
|
||||||
allow-clear
|
:allow-empty="[true, true]"
|
||||||
:options="statusOptions"
|
/>
|
||||||
/>
|
</a-form-item>
|
||||||
</a-form-item>
|
|
||||||
<a-form-item label="业务日期" class="filter-span-full">
|
|
||||||
<a-range-picker
|
|
||||||
v-model:value="advancedFilters.businessDate"
|
|
||||||
:allow-empty="[true, true]"
|
|
||||||
/>
|
|
||||||
</a-form-item>
|
|
||||||
|
|
||||||
<a-form-item label="负责人">
|
<a-form-item label="负责人">
|
||||||
<a-select
|
<a-select v-model:value="advancedFilters.owner" allow-clear :options="ownerOptions" />
|
||||||
v-model:value="advancedFilters.owner"
|
</a-form-item>
|
||||||
allow-clear
|
<a-form-item label="所属部门">
|
||||||
:options="ownerOptions"
|
<a-select
|
||||||
/>
|
v-model:value="advancedFilters.department"
|
||||||
</a-form-item>
|
allow-clear
|
||||||
<a-form-item label="所属部门">
|
:options="departmentOptions"
|
||||||
<a-select
|
/>
|
||||||
v-model:value="advancedFilters.department"
|
</a-form-item>
|
||||||
allow-clear
|
<a-form-item label="结算状态">
|
||||||
:options="departmentOptions"
|
<a-select
|
||||||
/>
|
v-model:value="advancedFilters.settlementStatus"
|
||||||
</a-form-item>
|
allow-clear
|
||||||
<a-form-item label="结算状态">
|
:options="settlementOptions"
|
||||||
<a-select
|
/>
|
||||||
v-model:value="advancedFilters.settlementStatus"
|
</a-form-item>
|
||||||
allow-clear
|
<a-form-item label="币种">
|
||||||
:options="settlementOptions"
|
<a-select
|
||||||
/>
|
v-model:value="advancedFilters.currency"
|
||||||
</a-form-item>
|
allow-clear
|
||||||
<a-form-item label="币种">
|
:options="currencyOptions"
|
||||||
<a-select
|
/>
|
||||||
v-model:value="advancedFilters.currency"
|
</a-form-item>
|
||||||
allow-clear
|
<a-form-item label="金额下限">
|
||||||
:options="currencyOptions"
|
<a-input-number v-model:value="advancedFilters.amountMin" :min="0" />
|
||||||
/>
|
</a-form-item>
|
||||||
</a-form-item>
|
<a-form-item label="金额上限">
|
||||||
<a-form-item label="金额下限">
|
<a-input-number v-model:value="advancedFilters.amountMax" :min="0" />
|
||||||
<a-input-number
|
</a-form-item>
|
||||||
v-model:value="advancedFilters.amountMin"
|
|
||||||
:min="0"
|
|
||||||
/>
|
|
||||||
</a-form-item>
|
|
||||||
<a-form-item label="金额上限">
|
|
||||||
<a-input-number
|
|
||||||
v-model:value="advancedFilters.amountMax"
|
|
||||||
:min="0"
|
|
||||||
/>
|
|
||||||
</a-form-item>
|
|
||||||
|
|
||||||
<a-form-item label="创建人">
|
<a-form-item label="创建人">
|
||||||
<a-input v-model:value="advancedFilters.creator" allow-clear />
|
<a-input v-model:value="advancedFilters.creator" allow-clear />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="创建日期">
|
<a-form-item label="创建日期">
|
||||||
<a-range-picker
|
<a-range-picker v-model:value="advancedFilters.createdDate" :allow-empty="[true, true]" />
|
||||||
v-model:value="advancedFilters.createdDate"
|
</a-form-item>
|
||||||
:allow-empty="[true, true]"
|
<a-form-item label="备注关键词" class="filter-span-full">
|
||||||
/>
|
<a-input v-model:value="advancedFilters.remark" allow-clear />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="备注关键词" class="filter-span-full">
|
|
||||||
<a-input v-model:value="advancedFilters.remark" allow-clear />
|
|
||||||
</a-form-item>
|
|
||||||
</a-form>
|
</a-form>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
@@ -396,6 +502,11 @@ watch(() => route.meta.source, loadData, { immediate: true });
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.load-alert {
|
||||||
|
flex: none;
|
||||||
|
margin: 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.table-area {
|
.table-area {
|
||||||
position: relative;
|
position: relative;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|||||||
@@ -1,31 +1,38 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { reactive, ref } from "vue";
|
import { reactive, ref } from "vue";
|
||||||
import { useRoute, useRouter } from "vue-router";
|
import { useRoute, useRouter } from "vue-router";
|
||||||
import { LockKeyhole, UserRound } from "@lucide/vue";
|
import { Building2, LockKeyhole, LogIn, MapPin, Ship, Truck, UserRound } from "@lucide/vue";
|
||||||
import { authApi } from "../services/api";
|
import { App } from "antdv-next";
|
||||||
|
import { loginApi } from "../services/api";
|
||||||
import { useAuthStore } from "../stores/auth";
|
import { useAuthStore } from "../stores/auth";
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
|
const { message } = App.useApp();
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const errorMessage = ref("");
|
|
||||||
const form = reactive({
|
const form = reactive({
|
||||||
orgid: "",
|
orgId: "",
|
||||||
userid: "",
|
userId: "",
|
||||||
password: "",
|
password: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function getRedirectPath() {
|
||||||
|
const redirect = route.query.redirect;
|
||||||
|
return typeof redirect === "string" && redirect.startsWith("/") && !redirect.startsWith("//")
|
||||||
|
? redirect
|
||||||
|
: "/dashboard";
|
||||||
|
}
|
||||||
|
|
||||||
async function submit() {
|
async function submit() {
|
||||||
errorMessage.value = "";
|
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const session = await authApi.login(form);
|
const session = await loginApi(form.orgId, form.userId, form.password);
|
||||||
authStore.setSession(session, form.orgid);
|
authStore.setSession(session, form.orgId);
|
||||||
await router.replace(route.query.redirect || "/dashboard");
|
await router.replace(getRedirectPath());
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
errorMessage.value = error.message || "登录失败";
|
message.error(error.message || "登录失败");
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
@@ -34,128 +41,490 @@ async function submit() {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<main class="login-page">
|
<main class="login-page">
|
||||||
<section class="login-shell">
|
<section class="login-shell" aria-label="FMS 登录">
|
||||||
<div class="login-brand">
|
<aside class="login-overview">
|
||||||
<div class="login-brand__mark">F</div>
|
<div class="overview-brand">
|
||||||
<div>
|
<div class="overview-brand__mark">F</div>
|
||||||
<h1>FMS 管理系统</h1>
|
<div class="overview-brand__copy">
|
||||||
<p>机构业务管理平台</p>
|
<strong>FMS</strong>
|
||||||
|
<span>Freight Management System</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<a-form layout="vertical" :model="form" @finish="submit">
|
<div class="overview-copy">
|
||||||
<a-alert
|
<span class="overview-copy__eyebrow">一体化业务工作台</span>
|
||||||
v-if="errorMessage"
|
<h1>FMS 管理系统</h1>
|
||||||
class="login-alert"
|
<p>让业务、费用与数据在同一工作台高效流转。</p>
|
||||||
type="error"
|
</div>
|
||||||
show-icon
|
|
||||||
:message="errorMessage"
|
<div class="route-visual" aria-hidden="true">
|
||||||
/>
|
<div class="route-line route-line--first"></div>
|
||||||
<a-form-item
|
<div class="route-line route-line--second"></div>
|
||||||
label="机构码"
|
<div class="route-node route-node--origin"><Building2 /></div>
|
||||||
name="orgid"
|
<div class="route-node route-node--truck"><Truck /></div>
|
||||||
:rules="[{ required: true, message: '请输入机构码' }]"
|
<div class="route-node route-node--ship"><Ship /></div>
|
||||||
>
|
<div class="route-node route-node--destination"><MapPin /></div>
|
||||||
<a-input v-model:value="form.orgid" size="large" autocomplete="organization" />
|
</div>
|
||||||
</a-form-item>
|
|
||||||
<a-form-item
|
<div class="overview-footer">
|
||||||
label="用户ID"
|
<span>多机构协同</span>
|
||||||
name="userid"
|
<span>业务数据一体化</span>
|
||||||
:rules="[{ required: true, message: '请输入用户ID' }]"
|
</div>
|
||||||
>
|
</aside>
|
||||||
<a-input v-model:value="form.userid" size="large" autocomplete="username">
|
|
||||||
<template #prefix><UserRound /></template>
|
<div class="login-panel">
|
||||||
</a-input>
|
<div class="mobile-brand">
|
||||||
</a-form-item>
|
<div class="mobile-brand__mark">F</div>
|
||||||
<a-form-item
|
<strong>FMS 管理系统</strong>
|
||||||
label="密码"
|
</div>
|
||||||
name="password"
|
|
||||||
:rules="[{ required: true, message: '请输入密码' }]"
|
<header class="login-heading">
|
||||||
>
|
<span>ACCOUNT ACCESS</span>
|
||||||
<a-input-password
|
<h2>账号登录</h2>
|
||||||
v-model:value="form.password"
|
<p>欢迎回来,请登录所属机构</p>
|
||||||
size="large"
|
</header>
|
||||||
autocomplete="current-password"
|
|
||||||
|
<a-form layout="vertical" :model="form" class="login-form" @finish="submit">
|
||||||
|
<a-form-item
|
||||||
|
label="机构码"
|
||||||
|
name="orgId"
|
||||||
|
:rules="[{ required: true, message: '请输入机构码' }]"
|
||||||
>
|
>
|
||||||
<template #prefix><LockKeyhole /></template>
|
<a-input v-model:value="form.orgId" size="large" autocomplete="organization">
|
||||||
</a-input-password>
|
<template #prefix><Building2 /></template>
|
||||||
</a-form-item>
|
</a-input>
|
||||||
<a-button type="primary" html-type="submit" size="large" block :loading="loading">
|
</a-form-item>
|
||||||
登录
|
<a-form-item
|
||||||
</a-button>
|
label="账号"
|
||||||
</a-form>
|
name="userId"
|
||||||
|
:rules="[{ required: true, message: '请输入账号' }]"
|
||||||
|
>
|
||||||
|
<a-input v-model:value="form.userId" size="large" autocomplete="username">
|
||||||
|
<template #prefix><UserRound /></template>
|
||||||
|
</a-input>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="密码" name="password">
|
||||||
|
<a-input-password
|
||||||
|
v-model:value="form.password"
|
||||||
|
size="large"
|
||||||
|
autocomplete="current-password"
|
||||||
|
>
|
||||||
|
<template #prefix><LockKeyhole /></template>
|
||||||
|
</a-input-password>
|
||||||
|
</a-form-item>
|
||||||
|
<a-button
|
||||||
|
type="primary"
|
||||||
|
html-type="submit"
|
||||||
|
size="large"
|
||||||
|
block
|
||||||
|
class="login-button"
|
||||||
|
:loading="loading"
|
||||||
|
>
|
||||||
|
<template #icon><LogIn /></template>
|
||||||
|
登录
|
||||||
|
</a-button>
|
||||||
|
</a-form>
|
||||||
|
|
||||||
|
<footer class="login-footer">FMS · 机构业务管理平台</footer>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.login-page {
|
.login-page {
|
||||||
|
position: relative;
|
||||||
display: grid;
|
display: grid;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
padding: 24px;
|
padding: 32px;
|
||||||
background:
|
background: #e8edf2;
|
||||||
linear-gradient(90deg, var(--fms-border) 1px, transparent 1px),
|
|
||||||
linear-gradient(var(--fms-border) 1px, transparent 1px), var(--fms-bg);
|
|
||||||
background-size: 32px 32px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-shell {
|
.login-shell {
|
||||||
width: min(100%, 400px);
|
position: relative;
|
||||||
padding: 30px;
|
display: grid;
|
||||||
border: 1px solid var(--fms-border);
|
width: min(100%, 920px);
|
||||||
|
min-height: 560px;
|
||||||
|
grid-template-columns: minmax(0, 1.08fr) minmax(380px, 0.92fr);
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid #d8e0e8;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background: var(--fms-surface);
|
background: #fff;
|
||||||
box-shadow: var(--fms-shadow);
|
box-shadow: 0 22px 60px rgb(31 45 61 / 14%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-brand {
|
.login-overview {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 38px 42px 34px;
|
||||||
|
overflow: hidden;
|
||||||
|
color: #f7fafc;
|
||||||
|
background: #22313f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-brand {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 11px;
|
||||||
margin-bottom: 28px;
|
|
||||||
|
|
||||||
&__mark {
|
&__mark {
|
||||||
display: grid;
|
display: grid;
|
||||||
width: 44px;
|
width: 38px;
|
||||||
height: 44px;
|
height: 38px;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
border-radius: 7px;
|
border: 1px solid rgb(255 255 255 / 24%);
|
||||||
background: #1677ff;
|
border-radius: 6px;
|
||||||
|
background: #168aad;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-size: 22px;
|
font-size: 19px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__copy {
|
||||||
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
strong {
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
margin-top: 3px;
|
||||||
|
color: #aebdca;
|
||||||
|
font-size: 10px;
|
||||||
|
letter-spacing: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-copy {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
margin-top: 76px;
|
||||||
|
|
||||||
|
&__eyebrow {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: #55c2b6;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: var(--fms-text);
|
color: #fff;
|
||||||
font-size: 20px;
|
font-size: 30px;
|
||||||
|
line-height: 1.25;
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
p {
|
||||||
margin: 3px 0 0;
|
max-width: 320px;
|
||||||
color: var(--fms-text-secondary);
|
margin: 14px 0 0;
|
||||||
|
color: #c5d0d9;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.route-visual {
|
||||||
|
position: relative;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 150px;
|
||||||
|
margin-top: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.route-line {
|
||||||
|
position: absolute;
|
||||||
|
height: 1px;
|
||||||
|
border-top: 1px dashed #6f8290;
|
||||||
|
transform-origin: left center;
|
||||||
|
|
||||||
|
&--first {
|
||||||
|
top: 75px;
|
||||||
|
left: 24px;
|
||||||
|
width: 45%;
|
||||||
|
transform: rotate(-11deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
&--second {
|
||||||
|
top: 62px;
|
||||||
|
left: 45%;
|
||||||
|
width: 44%;
|
||||||
|
transform: rotate(12deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.route-node {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 2;
|
||||||
|
display: grid;
|
||||||
|
width: 34px;
|
||||||
|
height: 34px;
|
||||||
|
place-items: center;
|
||||||
|
border: 1px solid #667987;
|
||||||
|
border-radius: 50%;
|
||||||
|
color: #d7e1e8;
|
||||||
|
background: #2c3e4c;
|
||||||
|
|
||||||
|
:deep(.lucide) {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--origin {
|
||||||
|
top: 56px;
|
||||||
|
left: 5px;
|
||||||
|
border-color: #45ad9f;
|
||||||
|
color: #66d0c2;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--truck {
|
||||||
|
top: 26px;
|
||||||
|
left: 37%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--ship {
|
||||||
|
top: 58px;
|
||||||
|
right: 25%;
|
||||||
|
border-color: #d3a34c;
|
||||||
|
color: #f0bd5f;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--destination {
|
||||||
|
top: 86px;
|
||||||
|
right: 3px;
|
||||||
|
border-color: #3f9bb7;
|
||||||
|
color: #62bad5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-footer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 18px;
|
||||||
|
color: #91a4b2;
|
||||||
|
font-size: 11px;
|
||||||
|
|
||||||
|
span {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
span + span::before {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: -10px;
|
||||||
|
width: 2px;
|
||||||
|
height: 2px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #667987;
|
||||||
|
content: "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-panel {
|
||||||
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 54px 48px 30px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-brand {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-heading {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
|
||||||
|
span {
|
||||||
|
color: #168aad;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin: 8px 0 0;
|
||||||
|
color: #1f2d3a;
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 8px 0 0;
|
||||||
|
color: #7a8793;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-alert {
|
.login-form {
|
||||||
margin-bottom: 18px;
|
:deep(.ant-form-item) {
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-form-item-label) {
|
||||||
|
padding-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-form-item-label > label) {
|
||||||
|
color: #354452;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-input-affix-wrapper) {
|
||||||
|
min-height: 44px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-input-prefix) {
|
||||||
|
margin-inline-end: 10px;
|
||||||
|
color: #83909b;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-input-prefix .lucide) {
|
||||||
|
width: 15px;
|
||||||
|
height: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-button {
|
||||||
|
height: 44px;
|
||||||
|
margin-top: 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-weight: 600;
|
||||||
|
|
||||||
|
:deep(.lucide) {
|
||||||
|
width: 15px;
|
||||||
|
height: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-footer {
|
||||||
|
margin-top: auto;
|
||||||
|
padding-top: 28px;
|
||||||
|
color: #9aa4ad;
|
||||||
|
font-size: 11px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 760px) {
|
||||||
|
.login-page {
|
||||||
|
align-items: start;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-shell {
|
||||||
|
width: min(100%, 500px);
|
||||||
|
min-height: calc(100vh - 40px);
|
||||||
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-overview {
|
||||||
|
min-height: 170px;
|
||||||
|
padding: 25px 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-brand {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-copy {
|
||||||
|
margin-top: 0;
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin-top: 7px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.route-visual,
|
||||||
|
.overview-footer {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-panel {
|
||||||
|
padding: 30px 34px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-brand {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 9px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
|
||||||
|
&__mark {
|
||||||
|
display: grid;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
place-items: center;
|
||||||
|
border-radius: 5px;
|
||||||
|
color: #fff;
|
||||||
|
background: #168aad;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
strong {
|
||||||
|
color: #263746;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
@media (max-width: 480px) {
|
||||||
.login-page {
|
.login-page {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
background: var(--fms-surface);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-shell {
|
.login-shell {
|
||||||
|
min-height: 100%;
|
||||||
border: 0;
|
border: 0;
|
||||||
|
border-radius: 0;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.login-overview {
|
||||||
|
min-height: 140px;
|
||||||
|
padding: 22px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-copy {
|
||||||
|
&__eyebrow {
|
||||||
|
margin-bottom: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-panel {
|
||||||
|
padding: 26px 24px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-brand {
|
||||||
|
margin-bottom: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-heading {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -10,6 +10,12 @@ export default defineConfig({
|
|||||||
host: "127.0.0.1",
|
host: "127.0.0.1",
|
||||||
port: 5180,
|
port: 5180,
|
||||||
strictPort: true,
|
strictPort: true,
|
||||||
|
proxy: {
|
||||||
|
"/api": {
|
||||||
|
target: "http://127.0.0.1:8080",
|
||||||
|
changeOrigin: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
tailwindcss(),
|
tailwindcss(),
|
||||||
|
|||||||
@@ -178,6 +178,8 @@
|
|||||||
|
|
||||||
## 统一约定
|
## 统一约定
|
||||||
|
|
||||||
|
- JavaScript 和 Java 内部函数、方法及变量使用小驼峰命名法。
|
||||||
|
- JSON 请求与响应字段、数据库字段使用 `snake_case`,在 API 边界显式映射。
|
||||||
- API 路径使用全小写名称,JSON 多单词字段使用 `snake_case`。
|
- API 路径使用全小写名称,JSON 多单词字段使用 `snake_case`。
|
||||||
- `code = 0` 表示成功,非零值表示失败。
|
- `code = 0` 表示成功,非零值表示失败。
|
||||||
- 查询结果统一放在 `data.rows` 中,没有数据时返回空数组。
|
- 查询结果统一放在 `data.rows` 中,没有数据时返回空数组。
|
||||||
|
|||||||
Reference in new issue
Block a user