@liujitcn/kratos-admin-core 0.0.54 → 0.0.56
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/package/declarations/src/api/base/v1/config.d.ts +3 -1
- package/dist/package/declarations/src/api/base/v1/file.d.ts +3 -3
- package/dist/package/declarations/src/components/Upload/File.vue.d.ts +3 -1
- package/dist/package/declarations/src/components/Upload/Files.vue.d.ts +3 -1
- package/dist/package/declarations/src/components/Upload/Img.vue.d.ts +4 -2
- package/dist/package/declarations/src/components/Upload/Imgs.vue.d.ts +3 -1
- package/dist/package/declarations/src/hooks/useImageUrl.d.ts +8 -0
- package/dist/package/declarations/src/hooks/useLogoUrl.d.ts +8 -0
- package/dist/package/declarations/src/rpc/base/v1/config.d.ts +11 -1
- package/dist/package/declarations/src/rpc/base/v1/file.d.ts +11 -0
- package/dist/package/declarations/src/stores/modules/config.d.ts +8 -0
- package/dist/package/declarations/src/utils/tenant.d.ts +6 -1
- package/dist/package/src/api/base/v1/config.ts +16 -1
- package/dist/package/src/api/base/v1/file.ts +5 -3
- package/dist/package/src/assets/images/logo.svg +13 -1
- package/dist/package/src/bootstrap.ts +14 -1
- package/dist/package/src/components/Upload/File.vue +5 -3
- package/dist/package/src/components/Upload/Files.vue +5 -3
- package/dist/package/src/components/Upload/Img.vue +5 -3
- package/dist/package/src/components/Upload/Imgs.vue +5 -3
- package/dist/package/src/hooks/useImageUrl.ts +25 -0
- package/dist/package/src/hooks/useLogoUrl.ts +11 -0
- package/dist/package/src/layouts/LayoutClassic/index.vue +9 -2
- package/dist/package/src/layouts/LayoutColumns/index.vue +9 -2
- package/dist/package/src/layouts/LayoutTransverse/index.vue +9 -2
- package/dist/package/src/layouts/LayoutVertical/index.vue +4 -1
- package/dist/package/src/locales/README.md +1 -1
- package/dist/package/src/rpc/base/v1/config.ts +13 -1
- package/dist/package/src/rpc/base/v1/file.ts +12 -0
- package/dist/package/src/stores/modules/config.ts +17 -2
- package/dist/package/src/stores/modules/user.ts +2 -0
- package/dist/package/src/tenant.ts +12 -4
- package/dist/package/src/utils/errorHandler.ts +2 -0
- package/dist/package/src/utils/request.ts +39 -4
- package/dist/package/src/utils/tenant.ts +34 -2
- package/dist/package/src/views/login/components/LoginForm.vue +5 -0
- package/dist/package/src/views/login/index.vue +24 -4
- package/package.json +2 -2
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import type { ConfigService, GetConfigRequest, GetConfigResponse } from "../../../rpc/base/v1/config";
|
|
1
|
+
import type { ConfigService, GetConfigRequest, GetConfigResponse, GetI18nCustomRequest, GetI18nCustomResponse } from "../../../rpc/base/v1/config";
|
|
2
2
|
/** 系统配置公共服务 */
|
|
3
3
|
export declare class ConfigServiceImpl implements ConfigService {
|
|
4
4
|
/** 获取系统配置 */
|
|
5
5
|
GetConfig(request: GetConfigRequest): Promise<GetConfigResponse>;
|
|
6
|
+
/** 获取当前租户的自定义国际化覆盖项。 */
|
|
7
|
+
GetI18nCustom(request: GetI18nCustomRequest): Promise<GetI18nCustomResponse>;
|
|
6
8
|
}
|
|
7
9
|
export declare const defConfigService: ConfigServiceImpl;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { BaseFileAccessMode, type MultiUploadFileResponse, type FileInfo } from "../../../rpc/base/v1/file";
|
|
2
2
|
/** 文件服务 */
|
|
3
3
|
export declare class FileServiceImpl {
|
|
4
4
|
/** 多个文件上传 */
|
|
5
|
-
MultiUploadFile(files: File[], fileType: string): Promise<MultiUploadFileResponse>;
|
|
5
|
+
MultiUploadFile(files: File[], fileType: string, accessMode?: BaseFileAccessMode): Promise<MultiUploadFileResponse>;
|
|
6
6
|
/** 上传文件 */
|
|
7
|
-
UploadFile(file: File, fileType: string): Promise<FileInfo>;
|
|
7
|
+
UploadFile(file: File, fileType: string, accessMode?: BaseFileAccessMode): Promise<FileInfo>;
|
|
8
8
|
/** 读取文件内容并返回 Blob。 */
|
|
9
9
|
GetFileBlob(file: string, fileName: string): Promise<Blob>;
|
|
10
10
|
/** 下载文件 */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { UploadUserFile } from "element-plus";
|
|
2
|
-
import type
|
|
2
|
+
import { BaseFileAccessMode, type FileInfo } from "../../rpc/base/v1/file";
|
|
3
3
|
/** 单文件上传组件属性。 */
|
|
4
4
|
interface UploadFileProps {
|
|
5
5
|
fileInfo?: UploadUserFile;
|
|
@@ -8,6 +8,7 @@ interface UploadFileProps {
|
|
|
8
8
|
fileSize?: number;
|
|
9
9
|
fileType?: string[];
|
|
10
10
|
uploadType?: string;
|
|
11
|
+
accessMode?: BaseFileAccessMode;
|
|
11
12
|
}
|
|
12
13
|
declare const __VLS_export: import("vue").DefineComponent<UploadFileProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
13
14
|
"update:fileInfo": (value: UploadUserFile | undefined) => any;
|
|
@@ -16,6 +17,7 @@ declare const __VLS_export: import("vue").DefineComponent<UploadFileProps, {}, {
|
|
|
16
17
|
}>, {
|
|
17
18
|
disabled: boolean;
|
|
18
19
|
fileType: string[];
|
|
20
|
+
accessMode: BaseFileAccessMode;
|
|
19
21
|
uploadType: string;
|
|
20
22
|
fileInfo: UploadUserFile;
|
|
21
23
|
fileSize: number;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { UploadUserFile } from "element-plus";
|
|
2
|
-
import type
|
|
2
|
+
import { BaseFileAccessMode, type FileInfo } from "../../rpc/base/v1/file";
|
|
3
3
|
/** 多文件上传组件属性。 */
|
|
4
4
|
interface UploadFilesProps {
|
|
5
5
|
fileList: UploadUserFile[];
|
|
@@ -9,6 +9,7 @@ interface UploadFilesProps {
|
|
|
9
9
|
fileSize?: number;
|
|
10
10
|
fileType?: string[];
|
|
11
11
|
uploadType?: string;
|
|
12
|
+
accessMode?: BaseFileAccessMode;
|
|
12
13
|
}
|
|
13
14
|
declare const __VLS_export: import("vue").DefineComponent<UploadFilesProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
14
15
|
"update:fileList": (value: UploadUserFile[]) => any;
|
|
@@ -17,6 +18,7 @@ declare const __VLS_export: import("vue").DefineComponent<UploadFilesProps, {},
|
|
|
17
18
|
}>, {
|
|
18
19
|
disabled: boolean;
|
|
19
20
|
fileType: string[];
|
|
21
|
+
accessMode: BaseFileAccessMode;
|
|
20
22
|
uploadType: string;
|
|
21
23
|
fileSize: number;
|
|
22
24
|
fileList: UploadUserFile[];
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { BaseFileAccessMode, type FileInfo } from "../../rpc/base/v1/file";
|
|
2
2
|
/** 单图上传组件属性。 */
|
|
3
3
|
interface UploadFileProps {
|
|
4
4
|
imageUrl: string;
|
|
5
5
|
api?: (file: File) => Promise<FileInfo>;
|
|
6
6
|
uploadType?: string;
|
|
7
|
+
accessMode?: BaseFileAccessMode;
|
|
7
8
|
drag?: boolean;
|
|
8
9
|
disabled?: boolean;
|
|
9
10
|
fileSize?: number;
|
|
@@ -27,10 +28,11 @@ declare const __VLS_base: import("vue").DefineComponent<UploadFileProps, {}, {},
|
|
|
27
28
|
width: string;
|
|
28
29
|
height: string;
|
|
29
30
|
drag: boolean;
|
|
31
|
+
imageUrl: string;
|
|
30
32
|
fileType: File.ImageMimeType[];
|
|
33
|
+
accessMode: BaseFileAccessMode;
|
|
31
34
|
uploadType: string;
|
|
32
35
|
fileSize: number;
|
|
33
|
-
imageUrl: string;
|
|
34
36
|
borderRadius: string;
|
|
35
37
|
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
36
38
|
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import type { UploadUserFile } from "element-plus";
|
|
2
|
-
import type
|
|
2
|
+
import { BaseFileAccessMode, type FileInfo } from "../../rpc/base/v1/file";
|
|
3
3
|
/** 多图上传组件属性。 */
|
|
4
4
|
interface UploadFileProps {
|
|
5
5
|
fileList: UploadUserFile[];
|
|
6
6
|
api?: (file: File) => Promise<FileInfo>;
|
|
7
7
|
uploadType?: string;
|
|
8
|
+
accessMode?: BaseFileAccessMode;
|
|
8
9
|
drag?: boolean;
|
|
9
10
|
disabled?: boolean;
|
|
10
11
|
limit?: number;
|
|
@@ -30,6 +31,7 @@ declare const __VLS_base: import("vue").DefineComponent<UploadFileProps, {}, {},
|
|
|
30
31
|
height: string;
|
|
31
32
|
drag: boolean;
|
|
32
33
|
fileType: File.ImageMimeType[];
|
|
34
|
+
accessMode: BaseFileAccessMode;
|
|
33
35
|
uploadType: string;
|
|
34
36
|
fileSize: number;
|
|
35
37
|
fileList: UploadUserFile[];
|
|
@@ -29,8 +29,16 @@ export interface GetConfigResponse {
|
|
|
29
29
|
configs: ConfigItem[];
|
|
30
30
|
/** AI 助手是否可用 */
|
|
31
31
|
ai_enabled: boolean;
|
|
32
|
+
}
|
|
33
|
+
/** 当前租户自定义国际化覆盖项查询条件。 */
|
|
34
|
+
export interface GetI18nCustomRequest {
|
|
35
|
+
/** 位置:枚举【BaseConfigSite】 */
|
|
36
|
+
site: BaseConfigSite;
|
|
37
|
+
}
|
|
38
|
+
/** 当前租户自定义国际化覆盖项查询结果。 */
|
|
39
|
+
export interface GetI18nCustomResponse {
|
|
32
40
|
/** 全部语言的自定义国际化覆盖项 */
|
|
33
|
-
|
|
41
|
+
items: I18nCustomItem[];
|
|
34
42
|
}
|
|
35
43
|
/** 全部语言的自定义国际化覆盖项。 */
|
|
36
44
|
export interface I18nCustomItem {
|
|
@@ -45,4 +53,6 @@ export interface I18nCustomItem {
|
|
|
45
53
|
export interface ConfigService {
|
|
46
54
|
/** 获取系统配置 */
|
|
47
55
|
GetConfig(request: GetConfigRequest): Promise<GetConfigResponse>;
|
|
56
|
+
/** 获取当前租户的自定义国际化覆盖项 */
|
|
57
|
+
GetI18nCustom(request: GetI18nCustomRequest): Promise<GetI18nCustomResponse>;
|
|
48
58
|
}
|
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
import type { BytesValue } from "../../google/protobuf/wrappers";
|
|
2
|
+
/** 文件访问方式。 */
|
|
3
|
+
export declare enum BaseFileAccessMode {
|
|
4
|
+
/** BASE_FILE_ACCESS_MODE_UNSPECIFIED - 未指定文件访问方式,服务端按授权访问处理。 */
|
|
5
|
+
BASE_FILE_ACCESS_MODE_UNSPECIFIED = 0,
|
|
6
|
+
/** BASE_FILE_ACCESS_MODE_PUBLIC - 允许匿名公开访问。 */
|
|
7
|
+
BASE_FILE_ACCESS_MODE_PUBLIC = 1,
|
|
8
|
+
/** BASE_FILE_ACCESS_MODE_AUTHORIZED - 必须携带有效访问令牌。 */
|
|
9
|
+
BASE_FILE_ACCESS_MODE_AUTHORIZED = 2
|
|
10
|
+
}
|
|
2
11
|
/** 批量上传文件请求参数 */
|
|
3
12
|
export interface MultiUploadFileRequest {
|
|
4
13
|
/** 文件名 */
|
|
@@ -24,6 +33,8 @@ export interface UploadFileInfo {
|
|
|
24
33
|
path: string;
|
|
25
34
|
/** 二进制内容将通过 base64 编码传输 */
|
|
26
35
|
content: Uint8Array;
|
|
36
|
+
/** 文件访问方式,未指定时默认需要访问令牌 */
|
|
37
|
+
access_mode: BaseFileAccessMode;
|
|
27
38
|
}
|
|
28
39
|
/** 下载文件请求参数 */
|
|
29
40
|
export interface DownloadFileRequest {
|
|
@@ -23,6 +23,14 @@ export declare const useConfigStore: import("pinia").StoreDefinition<"admin-conf
|
|
|
23
23
|
adminLogo: string;
|
|
24
24
|
background: string;
|
|
25
25
|
}>;
|
|
26
|
+
/**
|
|
27
|
+
* 登录成功后加载当前租户的自定义国际化覆盖项。
|
|
28
|
+
*/
|
|
29
|
+
loadI18nCustom(): Promise<void>;
|
|
30
|
+
/**
|
|
31
|
+
* 清空租户自定义翻译并恢复本地默认文案。
|
|
32
|
+
*/
|
|
33
|
+
resetI18nCustom(): void;
|
|
26
34
|
}>;
|
|
27
35
|
/** 刷新当前管理端语言和站点运行配置。 */
|
|
28
36
|
export declare function refreshAdminRuntimeConfig(): Promise<void>;
|
|
@@ -1,6 +1,11 @@
|
|
|
1
|
+
import type { SelectOptionResponse_Option } from "../rpc/common/v1/common";
|
|
1
2
|
/** 默认租户编码。 */
|
|
2
3
|
export declare const DEFAULT_TENANT_CODE = "0000";
|
|
4
|
+
/** 读取共享租户选项,同一时间只发起一次请求。 */
|
|
5
|
+
export declare function loadSharedTenantOptions(): Promise<SelectOptionResponse_Option[]>;
|
|
6
|
+
/** 清除共享租户选项,租户数据变更后由下一次读取重新加载。 */
|
|
7
|
+
export declare function invalidateTenantOptions(): void;
|
|
3
8
|
/** 读取租户列表筛选选项。 */
|
|
4
9
|
export declare function requestTenantOptions(): Promise<{
|
|
5
|
-
data:
|
|
10
|
+
data: SelectOptionResponse_Option[];
|
|
6
11
|
}>;
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import service from "../../../utils/request";
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
ConfigService,
|
|
4
|
+
GetConfigRequest,
|
|
5
|
+
GetConfigResponse,
|
|
6
|
+
GetI18nCustomRequest,
|
|
7
|
+
GetI18nCustomResponse
|
|
8
|
+
} from "../../../rpc/base/v1/config";
|
|
3
9
|
|
|
4
10
|
const CONFIG_URL = "/v1/base/config";
|
|
5
11
|
|
|
@@ -14,6 +20,15 @@ export class ConfigServiceImpl implements ConfigService {
|
|
|
14
20
|
headers: { Authorization: "no-auth" }
|
|
15
21
|
});
|
|
16
22
|
}
|
|
23
|
+
|
|
24
|
+
/** 获取当前租户的自定义国际化覆盖项。 */
|
|
25
|
+
GetI18nCustom(request: GetI18nCustomRequest): Promise<GetI18nCustomResponse> {
|
|
26
|
+
return service<GetI18nCustomRequest, GetI18nCustomResponse>({
|
|
27
|
+
url: `${CONFIG_URL}/i18n-custom`,
|
|
28
|
+
method: "get",
|
|
29
|
+
params: request
|
|
30
|
+
});
|
|
31
|
+
}
|
|
17
32
|
}
|
|
18
33
|
|
|
19
34
|
export const defConfigService = new ConfigServiceImpl();
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
import service from "../../../utils/request";
|
|
2
|
-
import type
|
|
2
|
+
import { BaseFileAccessMode, type DownloadFileRequest, type MultiUploadFileResponse, type FileInfo } from "../../../rpc/base/v1/file";
|
|
3
3
|
|
|
4
4
|
const FILE_URL = "/v1/base/file";
|
|
5
5
|
|
|
6
6
|
/** 文件服务 */
|
|
7
7
|
export class FileServiceImpl {
|
|
8
8
|
/** 多个文件上传 */
|
|
9
|
-
MultiUploadFile(files: File[], fileType: string): Promise<MultiUploadFileResponse> {
|
|
9
|
+
MultiUploadFile(files: File[], fileType: string, accessMode = BaseFileAccessMode.BASE_FILE_ACCESS_MODE_AUTHORIZED): Promise<MultiUploadFileResponse> {
|
|
10
10
|
const formData = new FormData();
|
|
11
11
|
files.map(file => {
|
|
12
12
|
formData.append(file.name, file);
|
|
13
13
|
});
|
|
14
14
|
formData.append("fileType", fileType);
|
|
15
|
+
formData.append("accessMode", String(accessMode));
|
|
15
16
|
return service<FormData, MultiUploadFileResponse>({
|
|
16
17
|
url: `${FILE_URL}/multi`,
|
|
17
18
|
method: "post",
|
|
@@ -22,10 +23,11 @@ export class FileServiceImpl {
|
|
|
22
23
|
});
|
|
23
24
|
}
|
|
24
25
|
/** 上传文件 */
|
|
25
|
-
UploadFile(file: File, fileType: string): Promise<FileInfo> {
|
|
26
|
+
UploadFile(file: File, fileType: string, accessMode = BaseFileAccessMode.BASE_FILE_ACCESS_MODE_AUTHORIZED): Promise<FileInfo> {
|
|
26
27
|
const formData = new FormData();
|
|
27
28
|
formData.append("file", file);
|
|
28
29
|
formData.append("fileType", fileType);
|
|
30
|
+
formData.append("accessMode", String(accessMode));
|
|
29
31
|
return service<FormData, FileInfo>({
|
|
30
32
|
url: `${FILE_URL}`,
|
|
31
33
|
method: "post",
|
|
@@ -1 +1,13 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 56" role="img" aria-label="Admin logo">
|
|
2
|
+
<rect x="3" y="8" width="54" height="43" rx="7" fill="#eef6ff" stroke="#244987" stroke-width="4"/>
|
|
3
|
+
<rect x="11" y="17" width="22" height="24" rx="4" fill="#fff"/>
|
|
4
|
+
<path d="M17 22a5 5 0 1 0 5 5" fill="none" stroke="#ffd15c" stroke-width="4"/>
|
|
5
|
+
<path d="M22 22a5 5 0 0 1 5 5" fill="none" stroke="#ffbd00" stroke-width="4"/>
|
|
6
|
+
<rect x="17" y="34" width="3" height="4" rx="1" fill="#8679e8"/>
|
|
7
|
+
<rect x="22" y="31" width="3" height="7" rx="1" fill="#6f5bd9"/>
|
|
8
|
+
<rect x="27" y="28" width="3" height="10" rx="1" fill="#5540cf"/>
|
|
9
|
+
<path d="M39 23h10M39 29h10M39 35h7" fill="none" stroke="#8a7dea" stroke-width="3" stroke-linecap="round"/>
|
|
10
|
+
<circle cx="52" cy="10" r="10" fill="#c9f8e4"/>
|
|
11
|
+
<circle cx="52" cy="10" r="7" fill="#56dba0"/>
|
|
12
|
+
<path d="m48.5 10 2.5 2.5 4.5-5" fill="none" stroke="#fff" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
|
|
13
|
+
</svg>
|
|
@@ -30,6 +30,9 @@ import { useUserStore } from "./stores/modules/user";
|
|
|
30
30
|
import { getRouteMetaTitle } from "./utils";
|
|
31
31
|
import { defLanguageService } from "./api/base/v1/language";
|
|
32
32
|
import { setAdminDocumentTitle } from "./documentTitle";
|
|
33
|
+
import defaultLogoUrl from "./assets/images/logo.svg";
|
|
34
|
+
import defaultLoginBackgroundUrl from "./assets/images/login_bg.svg";
|
|
35
|
+
import defaultLoginIllustrationUrl from "./assets/images/login_left.png";
|
|
33
36
|
|
|
34
37
|
/**
|
|
35
38
|
* 管理端启动参数。
|
|
@@ -49,6 +52,15 @@ export async function bootstrapAdminApp(options: AdminBootstrapOptions = {}) {
|
|
|
49
52
|
registerAdminModules(modules);
|
|
50
53
|
registerLocaleMessages(modules);
|
|
51
54
|
initializeLocale();
|
|
55
|
+
|
|
56
|
+
// 登录页图片与启动接口并行加载,并在首次挂载前完成解码,避免页面先于图片绘制。
|
|
57
|
+
const loginImagePreloads = [defaultLogoUrl, defaultLoginBackgroundUrl, defaultLoginIllustrationUrl].map(
|
|
58
|
+
imageUrl => {
|
|
59
|
+
const image = new Image();
|
|
60
|
+
image.src = imageUrl;
|
|
61
|
+
return image.decode();
|
|
62
|
+
}
|
|
63
|
+
);
|
|
52
64
|
try {
|
|
53
65
|
applyLanguageConfig(await defLanguageService.OptionLanguage({}));
|
|
54
66
|
} catch {
|
|
@@ -82,6 +94,7 @@ export async function bootstrapAdminApp(options: AdminBootstrapOptions = {}) {
|
|
|
82
94
|
}
|
|
83
95
|
updateDocumentTitle();
|
|
84
96
|
|
|
97
|
+
await Promise.allSettled(loginImagePreloads);
|
|
85
98
|
app.mount(options.mount ?? "#app");
|
|
86
99
|
return app;
|
|
87
100
|
}
|
|
@@ -92,7 +105,7 @@ async function refreshLocalizedRuntimeData() {
|
|
|
92
105
|
const authStore = useAuthStore(pinia);
|
|
93
106
|
const dictStore = useDictStore(pinia);
|
|
94
107
|
const configStore = useConfigStore(pinia);
|
|
95
|
-
await Promise.allSettled([configStore.loadDisplayConfig()]);
|
|
108
|
+
await Promise.allSettled([configStore.loadDisplayConfig(), ...(userStore.token ? [configStore.loadI18nCustom()] : [])]);
|
|
96
109
|
if (userStore.token) {
|
|
97
110
|
await Promise.allSettled([authStore.getAuthMenuList(), dictStore.updateDictionaryCache()]);
|
|
98
111
|
syncLocalizedRouteState();
|
|
@@ -39,7 +39,7 @@ import { Delete, Document, Download } from "@element-plus/icons-vue";
|
|
|
39
39
|
import { ElNotification, formContextKey, formItemContextKey } from "element-plus";
|
|
40
40
|
import type { UploadProps, UploadRequestOptions, UploadUserFile } from "element-plus";
|
|
41
41
|
import { defFileService } from "../../api/base/v1/file";
|
|
42
|
-
import type
|
|
42
|
+
import { BaseFileAccessMode, type FileInfo } from "../../rpc/base/v1/file";
|
|
43
43
|
import { useLocaleStore } from "../../locales";
|
|
44
44
|
|
|
45
45
|
const { t } = useLocaleStore();
|
|
@@ -52,6 +52,7 @@ interface UploadFileProps {
|
|
|
52
52
|
fileSize?: number;
|
|
53
53
|
fileType?: string[];
|
|
54
54
|
uploadType?: string;
|
|
55
|
+
accessMode?: BaseFileAccessMode; // 文件访问方式,默认需要访问令牌。
|
|
55
56
|
}
|
|
56
57
|
|
|
57
58
|
const props = withDefaults(defineProps<UploadFileProps>(), {
|
|
@@ -59,7 +60,8 @@ const props = withDefaults(defineProps<UploadFileProps>(), {
|
|
|
59
60
|
disabled: false,
|
|
60
61
|
fileSize: 20,
|
|
61
62
|
fileType: () => [],
|
|
62
|
-
uploadType: "file"
|
|
63
|
+
uploadType: "file",
|
|
64
|
+
accessMode: BaseFileAccessMode.BASE_FILE_ACCESS_MODE_AUTHORIZED
|
|
63
65
|
});
|
|
64
66
|
|
|
65
67
|
const emit = defineEmits<{
|
|
@@ -113,7 +115,7 @@ const beforeUpload: UploadProps["beforeUpload"] = rawFile => {
|
|
|
113
115
|
/** 执行自定义文件上传。 */
|
|
114
116
|
const handleHttpUpload = async (options: UploadRequestOptions) => {
|
|
115
117
|
try {
|
|
116
|
-
const api = props.api ?? (file => defFileService.UploadFile(file, props.uploadType));
|
|
118
|
+
const api = props.api ?? (file => defFileService.UploadFile(file, props.uploadType, props.accessMode));
|
|
117
119
|
const data = await api(options.file);
|
|
118
120
|
options.onSuccess(data);
|
|
119
121
|
} catch (error) {
|
|
@@ -45,7 +45,7 @@ import { Delete, Document, Download } from "@element-plus/icons-vue";
|
|
|
45
45
|
import { ElNotification, formContextKey, formItemContextKey } from "element-plus";
|
|
46
46
|
import type { UploadProps, UploadRequestOptions, UploadUserFile } from "element-plus";
|
|
47
47
|
import { defFileService } from "../../api/base/v1/file";
|
|
48
|
-
import type
|
|
48
|
+
import { BaseFileAccessMode, type FileInfo } from "../../rpc/base/v1/file";
|
|
49
49
|
import { useLocaleStore } from "../../locales";
|
|
50
50
|
|
|
51
51
|
const { t } = useLocaleStore();
|
|
@@ -59,6 +59,7 @@ interface UploadFilesProps {
|
|
|
59
59
|
fileSize?: number;
|
|
60
60
|
fileType?: string[];
|
|
61
61
|
uploadType?: string;
|
|
62
|
+
accessMode?: BaseFileAccessMode; // 文件访问方式,默认需要访问令牌。
|
|
62
63
|
}
|
|
63
64
|
|
|
64
65
|
const props = withDefaults(defineProps<UploadFilesProps>(), {
|
|
@@ -67,7 +68,8 @@ const props = withDefaults(defineProps<UploadFilesProps>(), {
|
|
|
67
68
|
limit: 5,
|
|
68
69
|
fileSize: 20,
|
|
69
70
|
fileType: () => [],
|
|
70
|
-
uploadType: "file"
|
|
71
|
+
uploadType: "file",
|
|
72
|
+
accessMode: BaseFileAccessMode.BASE_FILE_ACCESS_MODE_AUTHORIZED
|
|
71
73
|
});
|
|
72
74
|
|
|
73
75
|
const emit = defineEmits<{
|
|
@@ -129,7 +131,7 @@ const beforeUpload: UploadProps["beforeUpload"] = rawFile => {
|
|
|
129
131
|
/** 执行多文件自定义上传。 */
|
|
130
132
|
const handleHttpUpload = async (options: UploadRequestOptions) => {
|
|
131
133
|
try {
|
|
132
|
-
const api = props.api ?? (file => defFileService.UploadFile(file, props.uploadType));
|
|
134
|
+
const api = props.api ?? (file => defFileService.UploadFile(file, props.uploadType, props.accessMode));
|
|
133
135
|
const data = await api(options.file);
|
|
134
136
|
options.onSuccess(data);
|
|
135
137
|
} catch (error) {
|
|
@@ -54,7 +54,7 @@ import { formatSrc } from "../../utils/utils";
|
|
|
54
54
|
import { defFileService } from "../../api/base/v1/file";
|
|
55
55
|
import { ElNotification, formContextKey, formItemContextKey } from "element-plus";
|
|
56
56
|
import type { UploadProps, UploadRequestOptions } from "element-plus";
|
|
57
|
-
import type
|
|
57
|
+
import { BaseFileAccessMode, type FileInfo } from "../../rpc/base/v1/file";
|
|
58
58
|
import { useLocaleStore } from "../../locales";
|
|
59
59
|
|
|
60
60
|
const { t } = useLocaleStore();
|
|
@@ -63,7 +63,8 @@ const { t } = useLocaleStore();
|
|
|
63
63
|
interface UploadFileProps {
|
|
64
64
|
imageUrl: string; // 图片地址 ==> 必传
|
|
65
65
|
api?: (file: File) => Promise<FileInfo>; // 上传图片的 api 方法,默认使用当前文件服务 ==> 非必传
|
|
66
|
-
uploadType?: string;
|
|
66
|
+
uploadType?: string;
|
|
67
|
+
accessMode?: BaseFileAccessMode; // 文件访问方式,默认需要访问令牌。
|
|
67
68
|
drag?: boolean; // 是否支持拖拽上传 ==> 非必传(默认为 true)
|
|
68
69
|
disabled?: boolean; // 是否禁用上传组件 ==> 非必传(默认为 false)
|
|
69
70
|
fileSize?: number; // 图片大小限制 ==> 非必传(默认为 5M)
|
|
@@ -77,6 +78,7 @@ interface UploadFileProps {
|
|
|
77
78
|
const props = withDefaults(defineProps<UploadFileProps>(), {
|
|
78
79
|
imageUrl: "",
|
|
79
80
|
uploadType: "image",
|
|
81
|
+
accessMode: BaseFileAccessMode.BASE_FILE_ACCESS_MODE_AUTHORIZED,
|
|
80
82
|
drag: true,
|
|
81
83
|
disabled: false,
|
|
82
84
|
fileSize: 5,
|
|
@@ -112,7 +114,7 @@ const emit = defineEmits<{
|
|
|
112
114
|
const handleHttpUpload = async (options: UploadRequestOptions) => {
|
|
113
115
|
try {
|
|
114
116
|
// 优先使用页面显式传入的业务上传类型,避免不同业务图片都落到同一个 image 分类。
|
|
115
|
-
const api = props.api ?? (file => defFileService.UploadFile(file, props.uploadType));
|
|
117
|
+
const api = props.api ?? (file => defFileService.UploadFile(file, props.uploadType, props.accessMode));
|
|
116
118
|
const data = await api(options.file);
|
|
117
119
|
emit("update:imageUrl", data.url);
|
|
118
120
|
// 调用 el-form 内部的校验方法(可自动校验)
|
|
@@ -50,7 +50,7 @@ import { defFileService } from "../../api/base/v1/file";
|
|
|
50
50
|
import { formatSrc } from "../../utils/utils";
|
|
51
51
|
import type { UploadProps, UploadFile, UploadUserFile, UploadRequestOptions } from "element-plus";
|
|
52
52
|
import { ElNotification, formContextKey, formItemContextKey } from "element-plus";
|
|
53
|
-
import type
|
|
53
|
+
import { BaseFileAccessMode, type FileInfo } from "../../rpc/base/v1/file";
|
|
54
54
|
import { useLocaleStore } from "../../locales";
|
|
55
55
|
|
|
56
56
|
const { t } = useLocaleStore();
|
|
@@ -59,7 +59,8 @@ const { t } = useLocaleStore();
|
|
|
59
59
|
interface UploadFileProps {
|
|
60
60
|
fileList: UploadUserFile[];
|
|
61
61
|
api?: (file: File) => Promise<FileInfo>; // 上传图片的 api 方法,默认使用当前文件服务 ==> 非必传
|
|
62
|
-
uploadType?: string;
|
|
62
|
+
uploadType?: string;
|
|
63
|
+
accessMode?: BaseFileAccessMode; // 文件访问方式,默认需要访问令牌。
|
|
63
64
|
drag?: boolean; // 是否支持拖拽上传 ==> 非必传(默认为 true)
|
|
64
65
|
disabled?: boolean; // 是否禁用上传组件 ==> 非必传(默认为 false)
|
|
65
66
|
limit?: number; // 最大图片上传数 ==> 非必传(默认为 5张)
|
|
@@ -73,6 +74,7 @@ interface UploadFileProps {
|
|
|
73
74
|
const props = withDefaults(defineProps<UploadFileProps>(), {
|
|
74
75
|
fileList: () => [],
|
|
75
76
|
uploadType: "image",
|
|
77
|
+
accessMode: BaseFileAccessMode.BASE_FILE_ACCESS_MODE_AUTHORIZED,
|
|
76
78
|
drag: true,
|
|
77
79
|
disabled: false,
|
|
78
80
|
limit: 5,
|
|
@@ -133,7 +135,7 @@ const beforeUpload: UploadProps["beforeUpload"] = rawFile => {
|
|
|
133
135
|
const handleHttpUpload = async (options: UploadRequestOptions) => {
|
|
134
136
|
try {
|
|
135
137
|
// 优先使用页面显式传入的业务上传类型,避免不同业务图片都落到同一个 image 分类。
|
|
136
|
-
const api = props.api ?? (file => defFileService.UploadFile(file, props.uploadType));
|
|
138
|
+
const api = props.api ?? (file => defFileService.UploadFile(file, props.uploadType, props.accessMode));
|
|
137
139
|
const data = await api(options.file);
|
|
138
140
|
options.onSuccess(data);
|
|
139
141
|
} catch (error) {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { ref, watch, type Ref } from "vue";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 管理图片地址,并在配置地址为空或加载失败时回退到指定的本地资源。
|
|
5
|
+
*/
|
|
6
|
+
export function useImageUrl(configuredImageUrl: Readonly<Ref<string>>, fallbackImageUrl: string) {
|
|
7
|
+
const imageUrl = ref(fallbackImageUrl);
|
|
8
|
+
|
|
9
|
+
watch(
|
|
10
|
+
configuredImageUrl,
|
|
11
|
+
value => {
|
|
12
|
+
imageUrl.value = value || fallbackImageUrl;
|
|
13
|
+
},
|
|
14
|
+
{ immediate: true }
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
/** 图片加载失败时回退到指定的本地资源。 */
|
|
18
|
+
const handleImageError = () => {
|
|
19
|
+
if (imageUrl.value !== fallbackImageUrl) {
|
|
20
|
+
imageUrl.value = fallbackImageUrl;
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
return { imageUrl, handleImageError };
|
|
25
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Ref } from "vue";
|
|
2
|
+
import defaultLogoUrl from "../assets/images/logo.svg";
|
|
3
|
+
import { useImageUrl } from "../hooks/useImageUrl";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 管理站点 Logo 地址,并在配置地址不可用时回退到本地默认 Logo。
|
|
7
|
+
*/
|
|
8
|
+
export function useLogoUrl(configuredLogoUrl: Readonly<Ref<string>>) {
|
|
9
|
+
const { imageUrl, handleImageError } = useImageUrl(configuredLogoUrl, defaultLogoUrl);
|
|
10
|
+
return { logoUrl: imageUrl, handleLogoError: handleImageError };
|
|
11
|
+
}
|
|
@@ -4,7 +4,12 @@
|
|
|
4
4
|
<el-header>
|
|
5
5
|
<div class="header-lf mask-image">
|
|
6
6
|
<div class="logo flx-center">
|
|
7
|
-
<img
|
|
7
|
+
<img
|
|
8
|
+
class="logo-img"
|
|
9
|
+
:src="logoUrl"
|
|
10
|
+
:alt="t('core.layout.logo_alt')"
|
|
11
|
+
@error="handleLogoError"
|
|
12
|
+
/>
|
|
8
13
|
<span class="logo-text">{{ title }}</span>
|
|
9
14
|
</div>
|
|
10
15
|
<ToolBarLeft />
|
|
@@ -47,6 +52,7 @@ import SubMenu from "../../layouts/components/Menu/SubMenu.vue";
|
|
|
47
52
|
import ToolBarLeft from "../../layouts/components/Header/ToolBarLeft.vue";
|
|
48
53
|
import ToolBarRight from "../../layouts/components/Header/ToolBarRight.vue";
|
|
49
54
|
import { useLocaleStore } from "../../locales";
|
|
55
|
+
import { useLogoUrl } from "../../hooks/useLogoUrl";
|
|
50
56
|
|
|
51
57
|
const route = useRoute();
|
|
52
58
|
const authStore = useAuthStore();
|
|
@@ -58,7 +64,8 @@ const isCollapse = computed(() => globalStore.isCollapse);
|
|
|
58
64
|
const menuList = computed(() => authStore.showMenuListGet);
|
|
59
65
|
const activeMenu = computed(() => route.path as string);
|
|
60
66
|
const title = computed(() => configStore.display.sysName || import.meta.env.VITE_GLOB_APP_TITLE);
|
|
61
|
-
const
|
|
67
|
+
const configuredLogoUrl = computed(() => configStore.display.adminLogo);
|
|
68
|
+
const { logoUrl, handleLogoError } = useLogoUrl(configuredLogoUrl);
|
|
62
69
|
</script>
|
|
63
70
|
|
|
64
71
|
<style scoped lang="scss">
|
|
@@ -3,7 +3,12 @@
|
|
|
3
3
|
<el-container class="layout">
|
|
4
4
|
<div class="aside-split">
|
|
5
5
|
<div class="logo flx-center">
|
|
6
|
-
<img
|
|
6
|
+
<img
|
|
7
|
+
class="logo-img"
|
|
8
|
+
:src="logoUrl"
|
|
9
|
+
:alt="t('core.layout.logo_alt')"
|
|
10
|
+
@error="handleLogoError"
|
|
11
|
+
/>
|
|
7
12
|
</div>
|
|
8
13
|
<el-scrollbar>
|
|
9
14
|
<div class="split-list">
|
|
@@ -61,6 +66,7 @@ import ToolBarLeft from "../../layouts/components/Header/ToolBarLeft.vue";
|
|
|
61
66
|
import ToolBarRight from "../../layouts/components/Header/ToolBarRight.vue";
|
|
62
67
|
import SubMenu from "../../layouts/components/Menu/SubMenu.vue";
|
|
63
68
|
import { useLocaleStore } from "../../locales";
|
|
69
|
+
import { useLogoUrl } from "../../hooks/useLogoUrl";
|
|
64
70
|
|
|
65
71
|
const route = useRoute();
|
|
66
72
|
const router = useRouter();
|
|
@@ -74,7 +80,8 @@ const menuList = computed(() => authStore.showMenuListGet.filter(item => Boolean
|
|
|
74
80
|
const activeMenu = computed(() => route.path as string);
|
|
75
81
|
const title = computed(() => configStore.display.sysName || import.meta.env.VITE_GLOB_APP_TITLE);
|
|
76
82
|
const collapseTitle = computed(() => title.value.slice(0, 1).toUpperCase() || "S");
|
|
77
|
-
const
|
|
83
|
+
const configuredLogoUrl = computed(() => configStore.display.adminLogo);
|
|
84
|
+
const { logoUrl, handleLogoError } = useLogoUrl(configuredLogoUrl);
|
|
78
85
|
|
|
79
86
|
const subMenuList = ref<RouteItem[]>([]);
|
|
80
87
|
const splitActive = ref("");
|
|
@@ -3,7 +3,12 @@
|
|
|
3
3
|
<el-container class="layout">
|
|
4
4
|
<el-header>
|
|
5
5
|
<div class="logo flx-center">
|
|
6
|
-
<img
|
|
6
|
+
<img
|
|
7
|
+
class="logo-img"
|
|
8
|
+
:src="logoUrl"
|
|
9
|
+
:alt="t('core.layout.logo_alt')"
|
|
10
|
+
@error="handleLogoError"
|
|
11
|
+
/>
|
|
7
12
|
<span class="logo-text">{{ title }}</span>
|
|
8
13
|
</div>
|
|
9
14
|
<el-menu mode="horizontal" :router="false" :default-active="activeMenu">
|
|
@@ -45,6 +50,7 @@ import Main from "../../layouts/components/Main/index.vue";
|
|
|
45
50
|
import ToolBarRight from "../../layouts/components/Header/ToolBarRight.vue";
|
|
46
51
|
import SubMenu from "../../layouts/components/Menu/SubMenu.vue";
|
|
47
52
|
import { useLocaleStore } from "../../locales";
|
|
53
|
+
import { useLogoUrl } from "../../hooks/useLogoUrl";
|
|
48
54
|
|
|
49
55
|
const route = useRoute();
|
|
50
56
|
const router = useRouter();
|
|
@@ -54,7 +60,8 @@ const { t } = useLocaleStore();
|
|
|
54
60
|
const menuList = computed(() => authStore.showMenuListGet);
|
|
55
61
|
const activeMenu = computed(() => route.path as string);
|
|
56
62
|
const title = computed(() => configStore.display.sysName || import.meta.env.VITE_GLOB_APP_TITLE);
|
|
57
|
-
const
|
|
63
|
+
const configuredLogoUrl = computed(() => configStore.display.adminLogo);
|
|
64
|
+
const { logoUrl, handleLogoError } = useLogoUrl(configuredLogoUrl);
|
|
58
65
|
|
|
59
66
|
const handleClickMenu = (subItem: RouteItem) => {
|
|
60
67
|
const target = getRouteTarget(subItem);
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
class="logo-img"
|
|
11
11
|
:src="logoUrl"
|
|
12
12
|
:alt="t('core.layout.logo_alt')"
|
|
13
|
+
@error="handleLogoError"
|
|
13
14
|
/>
|
|
14
15
|
<span
|
|
15
16
|
v-show="!isCollapse"
|
|
@@ -53,6 +54,7 @@ import ToolBarLeft from "../../layouts/components/Header/ToolBarLeft.vue";
|
|
|
53
54
|
import ToolBarRight from "../../layouts/components/Header/ToolBarRight.vue";
|
|
54
55
|
import SubMenu from "../../layouts/components/Menu/SubMenu.vue";
|
|
55
56
|
import { useLocaleStore } from "../../locales";
|
|
57
|
+
import { useLogoUrl } from "../../hooks/useLogoUrl";
|
|
56
58
|
|
|
57
59
|
const route = useRoute();
|
|
58
60
|
const authStore = useAuthStore();
|
|
@@ -64,7 +66,8 @@ const isCollapse = computed(() => globalStore.isCollapse);
|
|
|
64
66
|
const menuList = computed(() => authStore.showMenuListGet);
|
|
65
67
|
const activeMenu = computed(() => route.path as string);
|
|
66
68
|
const title = computed(() => configStore.display.sysName || import.meta.env.VITE_GLOB_APP_TITLE);
|
|
67
|
-
const
|
|
69
|
+
const configuredLogoUrl = computed(() => configStore.display.adminLogo);
|
|
70
|
+
const { logoUrl, handleLogoError } = useLogoUrl(configuredLogoUrl);
|
|
68
71
|
const logoWrapperRef = ref<HTMLElement>();
|
|
69
72
|
const logoIconRef = ref<HTMLImageElement>();
|
|
70
73
|
const logoTextRef = ref<HTMLElement>();
|
|
@@ -9,4 +9,4 @@
|
|
|
9
9
|
| `en-US.json` | 英文公共语言包。 | 管理端切换为英文时由 Vue I18n 使用。 |
|
|
10
10
|
| `ja-JP.json` | 日文公共语言包。 | 管理端切换为日文时由 Vue I18n 使用。 |
|
|
11
11
|
|
|
12
|
-
语言包是标准 JSON,不能在文件内直接写注释;新增语言或修改键后执行仓库根目录的 `make i18n
|
|
12
|
+
语言包是标准 JSON,不能在文件内直接写注释;新增语言或修改键后执行仓库根目录的 `make i18n`,脚本会校验所有语言并生成注册产物。语言切换选项固定显示后端 `base_language.native_name`,接口不可用时使用代码内置的原生名称;`common.language.*` 仅用于普通文案和语言迁移初始数据。新增语言的完整文件清单见[国际化语言扩展指南](../../../../../../docs/国际化语言扩展指南.md)。
|
|
@@ -40,8 +40,18 @@ export interface GetConfigResponse {
|
|
|
40
40
|
configs: ConfigItem[];
|
|
41
41
|
/** AI 助手是否可用 */
|
|
42
42
|
ai_enabled: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** 当前租户自定义国际化覆盖项查询条件。 */
|
|
46
|
+
export interface GetI18nCustomRequest {
|
|
47
|
+
/** 位置:枚举【BaseConfigSite】 */
|
|
48
|
+
site: BaseConfigSite;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** 当前租户自定义国际化覆盖项查询结果。 */
|
|
52
|
+
export interface GetI18nCustomResponse {
|
|
43
53
|
/** 全部语言的自定义国际化覆盖项 */
|
|
44
|
-
|
|
54
|
+
items: I18nCustomItem[];
|
|
45
55
|
}
|
|
46
56
|
|
|
47
57
|
/** 全部语言的自定义国际化覆盖项。 */
|
|
@@ -58,4 +68,6 @@ export interface I18nCustomItem {
|
|
|
58
68
|
export interface ConfigService {
|
|
59
69
|
/** 获取系统配置 */
|
|
60
70
|
GetConfig(request: GetConfigRequest): Promise<GetConfigResponse>;
|
|
71
|
+
/** 获取当前租户的自定义国际化覆盖项 */
|
|
72
|
+
GetI18nCustom(request: GetI18nCustomRequest): Promise<GetI18nCustomResponse>;
|
|
61
73
|
}
|
|
@@ -7,6 +7,16 @@
|
|
|
7
7
|
/* eslint-disable */
|
|
8
8
|
import type { BytesValue } from "../../google/protobuf/wrappers";
|
|
9
9
|
|
|
10
|
+
/** 文件访问方式。 */
|
|
11
|
+
export enum BaseFileAccessMode {
|
|
12
|
+
/** BASE_FILE_ACCESS_MODE_UNSPECIFIED - 未指定文件访问方式,服务端按授权访问处理。 */
|
|
13
|
+
BASE_FILE_ACCESS_MODE_UNSPECIFIED = 0,
|
|
14
|
+
/** BASE_FILE_ACCESS_MODE_PUBLIC - 允许匿名公开访问。 */
|
|
15
|
+
BASE_FILE_ACCESS_MODE_PUBLIC = 1,
|
|
16
|
+
/** BASE_FILE_ACCESS_MODE_AUTHORIZED - 必须携带有效访问令牌。 */
|
|
17
|
+
BASE_FILE_ACCESS_MODE_AUTHORIZED = 2,
|
|
18
|
+
}
|
|
19
|
+
|
|
10
20
|
/** 批量上传文件请求参数 */
|
|
11
21
|
export interface MultiUploadFileRequest {
|
|
12
22
|
/** 文件名 */
|
|
@@ -35,6 +45,8 @@ export interface UploadFileInfo {
|
|
|
35
45
|
path: string;
|
|
36
46
|
/** 二进制内容将通过 base64 编码传输 */
|
|
37
47
|
content: Uint8Array;
|
|
48
|
+
/** 文件访问方式,未指定时默认需要访问令牌 */
|
|
49
|
+
access_mode: BaseFileAccessMode;
|
|
38
50
|
}
|
|
39
51
|
|
|
40
52
|
/** 下载文件请求参数 */
|
|
@@ -118,18 +118,33 @@ export const useConfigStore = defineStore("admin-config", {
|
|
|
118
118
|
});
|
|
119
119
|
const configMap = buildConfigMap(configResponse.configs ?? []);
|
|
120
120
|
|
|
121
|
-
applyCustomLocaleMessages(configResponse.i18n_customs ?? []);
|
|
122
121
|
this.setDisplayConfig(normalizeSiteDisplayConfig(configMap));
|
|
123
122
|
this.setLoginCaptchaConfig(normalizeLoginCaptchaConfig(configMap));
|
|
124
123
|
this.showTenantCode = !["false", "0"].includes((configMap[SHOW_TENANT_CODE_KEY] ?? "true").toLowerCase());
|
|
125
124
|
this.i18nDraftEnabled = configMap[I18N_DRAFT_ENABLED_KEY] === "true";
|
|
126
125
|
this.aiEnabled = configResponse.ai_enabled === true;
|
|
127
126
|
return this.display;
|
|
127
|
+
},
|
|
128
|
+
/**
|
|
129
|
+
* 登录成功后加载当前租户的自定义国际化覆盖项。
|
|
130
|
+
*/
|
|
131
|
+
async loadI18nCustom() {
|
|
132
|
+
const response = await defConfigService.GetI18nCustom({
|
|
133
|
+
site: BaseConfigSite.BASE_CONFIG_SITE_ADMIN
|
|
134
|
+
});
|
|
135
|
+
applyCustomLocaleMessages(response.items ?? []);
|
|
136
|
+
},
|
|
137
|
+
/**
|
|
138
|
+
* 清空租户自定义翻译并恢复本地默认文案。
|
|
139
|
+
*/
|
|
140
|
+
resetI18nCustom() {
|
|
141
|
+
applyCustomLocaleMessages([]);
|
|
128
142
|
}
|
|
129
143
|
}
|
|
130
144
|
});
|
|
131
145
|
|
|
132
146
|
/** 刷新当前管理端语言和站点运行配置。 */
|
|
133
147
|
export async function refreshAdminRuntimeConfig(): Promise<void> {
|
|
134
|
-
|
|
148
|
+
const configStore = useConfigStore();
|
|
149
|
+
await Promise.all([configStore.loadDisplayConfig(), configStore.loadI18nCustom()]);
|
|
135
150
|
}
|
|
@@ -10,6 +10,7 @@ import piniaPersistConfig from "../../stores/helper/persist";
|
|
|
10
10
|
import { useDictStoreHook } from "../../stores/modules/dict";
|
|
11
11
|
import { useLockScreenStore } from "../../stores/modules/lockScreen";
|
|
12
12
|
import { useAuthStore } from "../../stores/modules/auth";
|
|
13
|
+
import { useConfigStore } from "../../stores/modules/config";
|
|
13
14
|
|
|
14
15
|
const defaultUserInfo: UserInfoForm = {
|
|
15
16
|
user_name: "",
|
|
@@ -100,6 +101,7 @@ export const useUserStore = defineStore("admin-user", {
|
|
|
100
101
|
useDictStoreHook().clearDictionaryCache();
|
|
101
102
|
useLockScreenStore().clearLock();
|
|
102
103
|
useAuthStore().$reset();
|
|
104
|
+
useConfigStore().resetI18nCustom();
|
|
103
105
|
this.authVersion += 1;
|
|
104
106
|
this.authInvalidated = true;
|
|
105
107
|
this.setToken("");
|
|
@@ -4,7 +4,11 @@ import type { ColumnProps, EnumProps, RenderScope, SearchType } from "./componen
|
|
|
4
4
|
import { defBaseTenantService } from "./api/system/admin/v1/base_tenant";
|
|
5
5
|
import type { SelectOptionResponse_Option } from "./rpc/common/v1/common";
|
|
6
6
|
import { useUserStore } from "./stores/runtime";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
DEFAULT_TENANT_CODE,
|
|
9
|
+
loadSharedTenantOptions,
|
|
10
|
+
requestTenantOptions as requestTenantOptionData
|
|
11
|
+
} from "./utils/tenant";
|
|
8
12
|
|
|
9
13
|
export * from "./utils/tenant";
|
|
10
14
|
|
|
@@ -89,8 +93,7 @@ export function useTenantScope(): TenantScope {
|
|
|
89
93
|
async function loadTenantOptions(includeCurrentTenant = false) {
|
|
90
94
|
if (!includeCurrentTenant && !isDefaultTenant.value) return;
|
|
91
95
|
if (tenantOptions.value.length) return;
|
|
92
|
-
|
|
93
|
-
tenantOptions.value = response.list ?? [];
|
|
96
|
+
tenantOptions.value = await loadSharedTenantOptions();
|
|
94
97
|
}
|
|
95
98
|
|
|
96
99
|
function toRequestTenantId(value: unknown) {
|
|
@@ -129,7 +132,12 @@ export function useTenantScope(): TenantScope {
|
|
|
129
132
|
search:
|
|
130
133
|
options.searchable === false
|
|
131
134
|
? undefined
|
|
132
|
-
: {
|
|
135
|
+
: {
|
|
136
|
+
el: options.searchEl ?? "tenant-select",
|
|
137
|
+
key: options.searchKey ?? prop,
|
|
138
|
+
props: { filterable: true },
|
|
139
|
+
order: options.order ?? 1
|
|
140
|
+
},
|
|
133
141
|
enum: options.enum ?? requestTenantOptionData,
|
|
134
142
|
render: options.render ?? (scope => resolveTenantLabel(scope.row, prop))
|
|
135
143
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import axios from "axios";
|
|
1
2
|
import { ElNotification } from "element-plus";
|
|
2
3
|
import { t } from "../locales";
|
|
3
4
|
|
|
@@ -5,6 +6,7 @@ import { t } from "../locales";
|
|
|
5
6
|
* @description 全局代码错误捕捉
|
|
6
7
|
* */
|
|
7
8
|
const errorHandler = (error: any) => {
|
|
9
|
+
if (axios.isCancel(error) || error === "canceled") return false;
|
|
8
10
|
// 过滤 HTTP 请求错误
|
|
9
11
|
if (error.status || error.status == 0) return false;
|
|
10
12
|
let errorMap: { [key: string]: string } = {
|
|
@@ -152,6 +152,12 @@ function shouldSkipErrorMessage(config?: InternalAxiosRequestConfig) {
|
|
|
152
152
|
return requestUrl === SESSION_URL && requestMethod === "delete";
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
+
/** 判断当前是否处于退出或认证失效过渡状态。 */
|
|
156
|
+
function isAuthTransitioning() {
|
|
157
|
+
const userStore = getUserStore();
|
|
158
|
+
return userStore.isLoggingOut || userStore.authInvalidated;
|
|
159
|
+
}
|
|
160
|
+
|
|
155
161
|
/** 判断响应是否要求用户先修改密码。 */
|
|
156
162
|
function isPasswordChangeRequired(data?: ErrorResponseData) {
|
|
157
163
|
return data?.metadata?.[PASSWORD_CHANGE_REQUIRED_METADATA_KEY] === "true";
|
|
@@ -262,7 +268,6 @@ export async function ensureAccessToken() {
|
|
|
262
268
|
await handleTokenRefresh(false);
|
|
263
269
|
return hasValidAccessToken();
|
|
264
270
|
} catch {
|
|
265
|
-
userStore.clearAuthData();
|
|
266
271
|
return false;
|
|
267
272
|
}
|
|
268
273
|
}
|
|
@@ -304,7 +309,14 @@ export function handleAuthExpired() {
|
|
|
304
309
|
// 请求拦截器
|
|
305
310
|
service.interceptors.request.use(
|
|
306
311
|
async (config: InternalAxiosRequestConfig) => {
|
|
312
|
+
const requestUrl = config.url ?? "";
|
|
313
|
+
const requestMethod = String(config.method ?? "").toLowerCase();
|
|
307
314
|
const skipAuth = shouldSkipAuth(config);
|
|
315
|
+
const isLogoutRequest = requestUrl === SESSION_URL && requestMethod === "delete";
|
|
316
|
+
if (isAuthTransitioning() && !skipAuth && !isLogoutRequest) {
|
|
317
|
+
throw new axios.CanceledError();
|
|
318
|
+
}
|
|
319
|
+
|
|
308
320
|
const accessToken = skipAuth ? "" : await getRequestAccessToken();
|
|
309
321
|
Object.assign(config.headers, getLocaleRequestHeaders());
|
|
310
322
|
config.headers[REFRESH_TOKEN_TRANSPORT_HEADER] = REFRESH_TOKEN_TRANSPORT_COOKIE;
|
|
@@ -341,14 +353,24 @@ service.interceptors.response.use(
|
|
|
341
353
|
return Promise.reject(new Error(message || t("common.message.system_error")));
|
|
342
354
|
},
|
|
343
355
|
async (error: AxiosError) => {
|
|
356
|
+
if (axios.isCancel(error)) {
|
|
357
|
+
return Promise.reject(error);
|
|
358
|
+
}
|
|
359
|
+
|
|
344
360
|
const status = error.response?.status;
|
|
345
361
|
const data = error.response?.data as ErrorResponseData | undefined;
|
|
346
362
|
const code = data?.code;
|
|
347
363
|
const message = data?.message;
|
|
348
364
|
const requestConfig = error.config as RetryableRequestConfig | undefined;
|
|
349
365
|
|
|
350
|
-
//
|
|
351
|
-
if ((
|
|
366
|
+
// 退出或认证失效后,页面中的在途请求返回 401 属于预期结果,不再重复刷新令牌、弹窗或提示错误。
|
|
367
|
+
if (isAuthTransitioning()) {
|
|
368
|
+
return Promise.reject(error);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// 业务请求仅在 401 时尝试刷新并重放,公共认证接口的 401 继续展示后端业务错误。
|
|
372
|
+
const isUnauthorized = status === 401 || code === 401;
|
|
373
|
+
if (isUnauthorized && !shouldSkipAuthExpiredPrompt(requestConfig)) {
|
|
352
374
|
const userStore = getUserStore();
|
|
353
375
|
if (requestConfig && !requestConfig._authRetried && !userStore.isLoggingOut && !userStore.authInvalidated && hasRefreshCookieHint()) {
|
|
354
376
|
requestConfig._authRetried = true;
|
|
@@ -374,7 +396,7 @@ service.interceptors.response.use(
|
|
|
374
396
|
} else if (!shouldSkipErrorMessage(requestConfig)) {
|
|
375
397
|
showRequestError(error.message || t("common.message.system_error"));
|
|
376
398
|
}
|
|
377
|
-
return Promise.reject(error
|
|
399
|
+
return Promise.reject(error);
|
|
378
400
|
}
|
|
379
401
|
);
|
|
380
402
|
|
|
@@ -389,12 +411,25 @@ export default request;
|
|
|
389
411
|
let isRefreshing = false;
|
|
390
412
|
let refreshPromise: Promise<void> | null = null;
|
|
391
413
|
|
|
414
|
+
/** 判断刷新令牌是否已被服务端判定为失效。 */
|
|
415
|
+
function isRefreshTokenUnauthorized(error: unknown) {
|
|
416
|
+
if (!axios.isAxiosError<ErrorResponseData>(error)) return false;
|
|
417
|
+
|
|
418
|
+
const status = error.response?.status;
|
|
419
|
+
const code = error.response?.data?.code;
|
|
420
|
+
return status === 401 || code === 401;
|
|
421
|
+
}
|
|
422
|
+
|
|
392
423
|
/** 刷新 Token 处理 */
|
|
393
424
|
async function handleTokenRefresh(promptOnFailure = true) {
|
|
394
425
|
if (!isRefreshing) {
|
|
395
426
|
isRefreshing = true;
|
|
396
427
|
refreshPromise = refreshAccessToken()
|
|
397
428
|
.catch(error => {
|
|
429
|
+
if (isRefreshTokenUnauthorized(error)) {
|
|
430
|
+
// 刷新令牌失效后立即终止后续自动刷新,避免旧 Cookie 被反复提交。
|
|
431
|
+
getUserStore().clearAuthData();
|
|
432
|
+
}
|
|
398
433
|
if (promptOnFailure) {
|
|
399
434
|
console.log("token 刷新失败", error);
|
|
400
435
|
handleAuthExpired();
|
|
@@ -1,10 +1,42 @@
|
|
|
1
1
|
import { defBaseTenantService } from "../api/system/admin/v1/base_tenant";
|
|
2
|
+
import type { SelectOptionResponse_Option } from "../rpc/common/v1/common";
|
|
2
3
|
|
|
3
4
|
/** 默认租户编码。 */
|
|
4
5
|
export const DEFAULT_TENANT_CODE = "0000";
|
|
5
6
|
|
|
7
|
+
const tenantOptions: SelectOptionResponse_Option[] = [];
|
|
8
|
+
let tenantOptionsLoaded = false;
|
|
9
|
+
let tenantOptionsRequest: Promise<SelectOptionResponse_Option[]> | undefined;
|
|
10
|
+
let tenantOptionsVersion = 0;
|
|
11
|
+
|
|
12
|
+
/** 读取共享租户选项,同一时间只发起一次请求。 */
|
|
13
|
+
export async function loadSharedTenantOptions() {
|
|
14
|
+
if (tenantOptionsLoaded) return tenantOptions;
|
|
15
|
+
if (!tenantOptionsRequest) {
|
|
16
|
+
const requestVersion = tenantOptionsVersion;
|
|
17
|
+
tenantOptionsRequest = defBaseTenantService.OptionBaseTenant({ keyword: "" })
|
|
18
|
+
.then(response => {
|
|
19
|
+
if (requestVersion !== tenantOptionsVersion) return tenantOptions;
|
|
20
|
+
tenantOptions.splice(0, tenantOptions.length, ...(response.list ?? []));
|
|
21
|
+
tenantOptionsLoaded = true;
|
|
22
|
+
return tenantOptions;
|
|
23
|
+
})
|
|
24
|
+
.finally(() => {
|
|
25
|
+
tenantOptionsRequest = undefined;
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
return tenantOptionsRequest;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** 清除共享租户选项,租户数据变更后由下一次读取重新加载。 */
|
|
32
|
+
export function invalidateTenantOptions() {
|
|
33
|
+
tenantOptionsVersion += 1;
|
|
34
|
+
tenantOptions.splice(0, tenantOptions.length);
|
|
35
|
+
tenantOptionsLoaded = false;
|
|
36
|
+
tenantOptionsRequest = undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
6
39
|
/** 读取租户列表筛选选项。 */
|
|
7
40
|
export async function requestTenantOptions() {
|
|
8
|
-
|
|
9
|
-
return { data: response.list ?? [] };
|
|
41
|
+
return { data: await loadSharedTenantOptions() };
|
|
10
42
|
}
|
|
@@ -629,6 +629,11 @@ const handleLoginResponse = async (result: LoginResponse) => {
|
|
|
629
629
|
clearPasswordChangeRequired();
|
|
630
630
|
}
|
|
631
631
|
userStore.updateTokenAuth(result.access_token, result.token_type ?? "", result.expires_in);
|
|
632
|
+
try {
|
|
633
|
+
await configStore.loadI18nCustom();
|
|
634
|
+
} catch {
|
|
635
|
+
configStore.resetI18nCustom();
|
|
636
|
+
}
|
|
632
637
|
await finishLogin(mustChangePassword);
|
|
633
638
|
};
|
|
634
639
|
|
|
@@ -4,11 +4,23 @@
|
|
|
4
4
|
<SwitchDark class="dark" />
|
|
5
5
|
<LocaleSwitch v-if="languageOptions.length > 1" class="locale" />
|
|
6
6
|
<div class="login-left">
|
|
7
|
-
<img
|
|
7
|
+
<img
|
|
8
|
+
class="login-left-img"
|
|
9
|
+
:src="backgroundUrl"
|
|
10
|
+
:alt="t('core.login.background_alt')"
|
|
11
|
+
@error="handleBackgroundError"
|
|
12
|
+
/>
|
|
8
13
|
</div>
|
|
9
14
|
<div ref="loginFormRef" class="login-form">
|
|
10
15
|
<div ref="loginLogoRef" class="login-logo">
|
|
11
|
-
<img
|
|
16
|
+
<img
|
|
17
|
+
v-show="showLogoIcon"
|
|
18
|
+
ref="logoIconRef"
|
|
19
|
+
class="login-icon"
|
|
20
|
+
:src="logoUrl"
|
|
21
|
+
alt=""
|
|
22
|
+
@error="handleLogoError"
|
|
23
|
+
/>
|
|
12
24
|
<h2 ref="logoTextRef" class="logo-text" :style="{ fontSize: `${logoFontSize}px` }">{{ projectName }}</h2>
|
|
13
25
|
</div>
|
|
14
26
|
<LoginForm :dialog-anchor="loginFormRef" />
|
|
@@ -24,12 +36,20 @@ import SwitchDark from "../../components/SwitchDark/index.vue";
|
|
|
24
36
|
import LocaleSwitch from "../../components/LocaleSwitch/index.vue";
|
|
25
37
|
import { useConfigStore } from "../../stores/modules/config";
|
|
26
38
|
import { useLocaleStore } from "../../locales";
|
|
39
|
+
import defaultBackgroundUrl from "../../assets/images/login_left.png";
|
|
40
|
+
import { useLogoUrl } from "../../hooks/useLogoUrl";
|
|
41
|
+
import { useImageUrl } from "../../hooks/useImageUrl";
|
|
27
42
|
|
|
28
43
|
const configStore = useConfigStore();
|
|
29
44
|
const { t, languageOptions } = useLocaleStore();
|
|
30
45
|
const projectName = computed(() => configStore.display.sysName || import.meta.env.VITE_GLOB_APP_TITLE);
|
|
31
|
-
const
|
|
32
|
-
const
|
|
46
|
+
const configuredLogoUrl = computed(() => configStore.display.adminLogo);
|
|
47
|
+
const { logoUrl, handleLogoError } = useLogoUrl(configuredLogoUrl);
|
|
48
|
+
const configuredBackgroundUrl = computed(() => configStore.display.background);
|
|
49
|
+
const { imageUrl: backgroundUrl, handleImageError: handleBackgroundError } = useImageUrl(
|
|
50
|
+
configuredBackgroundUrl,
|
|
51
|
+
defaultBackgroundUrl
|
|
52
|
+
);
|
|
33
53
|
|
|
34
54
|
const MAX_LOGO_FONT_SIZE = 42;
|
|
35
55
|
const PREFERRED_MIN_LOGO_FONT_SIZE = 30;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@liujitcn/kratos-admin-core",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.56",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "kratos-admin frontend runtime and base modules",
|
|
7
7
|
"author": {
|
|
@@ -236,7 +236,7 @@
|
|
|
236
236
|
"scripts": {
|
|
237
237
|
"build": "pnpm build:package",
|
|
238
238
|
"build:package": "node ../../scripts/build-package.mjs .",
|
|
239
|
-
"test": "tsc -p tsconfig.test.json && node --test dist/test/tests/modules.test.js dist/test/tests/dict.test.js dist/test/tests/menu.test.js dist/test/tests/source-patterns.test.js dist/test/tests/codegen-hmr.test.js dist/test/tests/dialog-request.test.js",
|
|
239
|
+
"test": "tsc -p tsconfig.test.json && node --test dist/test/tests/modules.test.js dist/test/tests/dict.test.js dist/test/tests/menu.test.js dist/test/tests/source-patterns.test.js dist/test/tests/codegen-hmr.test.js dist/test/tests/dialog-request.test.js dist/test/tests/request-error.test.js",
|
|
240
240
|
"type:check": "vue-tsc --noEmit --skipLibCheck"
|
|
241
241
|
}
|
|
242
242
|
}
|