@liujitcn/kratos-admin-core 0.1.5 → 0.1.11
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/README.md +3 -2
- package/build/auto-imports.json +9 -0
- package/dist/package/declarations/src/hooks/useSpeechRecognition.d.ts +37 -0
- package/dist/package/declarations/src/index.d.ts +2 -0
- package/dist/package/declarations/src/utils/request.d.ts +1 -1
- package/dist/package/src/components/MarkdownPreview/index.vue +1 -1
- package/dist/package/src/components/PasswordStrength/index.vue +1 -1
- package/dist/package/src/components/ProTable/components/TableColumn.vue +6 -4
- package/dist/package/src/components/SelectFilter/index.scss +1 -1
- package/dist/package/src/components/YamlEditor/index.vue +2 -2
- package/dist/package/src/config/index.ts +1 -1
- package/dist/package/src/hooks/useSpeechRecognition.ts +177 -0
- package/dist/package/src/index.ts +2 -0
- package/dist/package/src/layouts/components/Header/ToolBarRight.vue +10 -0
- package/dist/package/src/routers/index.ts +11 -2
- package/dist/package/src/routers/modules/dynamicRouter.ts +1 -2
- package/dist/package/src/styles/common.scss +1 -1
- package/dist/package/src/styles/element-dark.scss +0 -1
- package/dist/package/src/styles/element.scss +53 -1
- package/dist/package/src/utils/request.ts +19 -18
- package/dist/package/src/views/login/components/LoginForm.vue +9 -3
- package/dist/package/types/generated/auto-imports.d.ts +9 -0
- package/package.json +2 -2
- package/types/generated/auto-imports.d.ts +9 -0
package/README.md
CHANGED
|
@@ -60,7 +60,7 @@ packages/core
|
|
|
60
60
|
|
|
61
61
|
| 路径 | 作用 |
|
|
62
62
|
| -------------------------------------- | ------------------------------------------------------------------------------ |
|
|
63
|
-
| `src/index.ts` | npm
|
|
63
|
+
| `src/index.ts` | npm 主入口,导出启动函数、模块 interface 和通用语音识别能力。 |
|
|
64
64
|
| `src/auth.ts` | 业务模块使用的按钮权限入口。 |
|
|
65
65
|
| `src/format.ts` | 业务模块使用的格式化入口。 |
|
|
66
66
|
| `src/navigation.ts` | 业务模块使用的路由与跳转入口。 |
|
|
@@ -114,6 +114,7 @@ packages/core
|
|
|
114
114
|
| `src/hooks/useHandleData.ts` | 删除、状态变更等确认交互。 |
|
|
115
115
|
| `src/hooks/useOnline.ts` | 网络在线状态监听。 |
|
|
116
116
|
| `src/hooks/useSelection.ts` | 表格选择状态。 |
|
|
117
|
+
| `src/hooks/useSpeechRecognition.ts` | AI 助手与 BI 问数共用的浏览器语音识别,通过 npm 主入口导出。 |
|
|
117
118
|
| `src/hooks/useTable.ts` | 表格请求、分页、搜索和加载状态。 |
|
|
118
119
|
| `src/hooks/useTheme.ts` | 主题初始化和切换。 |
|
|
119
120
|
| `src/hooks/useTime.ts` | 问候时间和时间展示。 |
|
|
@@ -193,6 +194,6 @@ Dict 和 DictLabel 直接响应字典 Store 的文案变化。字典刷新成功
|
|
|
193
194
|
|
|
194
195
|
纯表格 demo 的“所在城市”通过 `search.enum` 提供浙江、广东、四川的省市级联选项,选择城市后可筛选对应项目,清空或重置后恢复全部项目。
|
|
195
196
|
|
|
196
|
-
ProTable 的 `cellType: "actions"` 操作列按当前行的 `hidden` 过滤按钮,优先展示使用 `common.action.edit`、`common.action.delete`
|
|
197
|
+
ProTable 的 `cellType: "actions"` 操作列按当前行的 `hidden` 过滤按钮,优先展示使用 `common.action.edit`、`common.action.delete` 文案的编辑、删除操作。可见操作不超过三项时全部展示;四项及以上时,行内固定展示两个操作和“更多”,其余进入菜单。编辑、删除不足两个时按配置顺序补足,不补充未配置或无权限的操作。例如四项操作展示为行内两项、菜单两项。菜单保留图标、禁用状态和点击参数;自定义 render 或插槽不参与折叠。
|
|
197
198
|
|
|
198
199
|
操作列默认固定在右侧。配置式操作列(`cellType: "actions"`)根据当前页实际展示按钮和表头的最大宽度自适应,计入翻译文案、图标和间距,随语言、字体和行权限变化重新测量,最小宽度为 `80px`;折叠菜单内的按钮不参与测量。自定义 render 或插槽操作列仍默认 `160px`。页面可通过 `width` 或 `minWidth` 覆盖自动宽度,通过 `fixed: "left"` 调整固定位置,或通过 `fixed: false` 取消固定。
|
package/build/auto-imports.json
CHANGED
|
@@ -22,11 +22,13 @@
|
|
|
22
22
|
"Calendar",
|
|
23
23
|
"Camera",
|
|
24
24
|
"ChatDotRound",
|
|
25
|
+
"Check",
|
|
25
26
|
"CircleCheck",
|
|
26
27
|
"CircleCheckFilled",
|
|
27
28
|
"CircleClose",
|
|
28
29
|
"CirclePlus",
|
|
29
30
|
"Close",
|
|
31
|
+
"Coin",
|
|
30
32
|
"CollectionTag",
|
|
31
33
|
"CopyDocument",
|
|
32
34
|
"DataAnalysis",
|
|
@@ -41,23 +43,30 @@
|
|
|
41
43
|
"Fold",
|
|
42
44
|
"Folder",
|
|
43
45
|
"Goods",
|
|
46
|
+
"Grid",
|
|
44
47
|
"InfoFilled",
|
|
45
48
|
"List",
|
|
49
|
+
"Loading",
|
|
46
50
|
"Lock",
|
|
47
51
|
"Male",
|
|
48
52
|
"Message",
|
|
53
|
+
"Microphone",
|
|
49
54
|
"Moon",
|
|
55
|
+
"MoreFilled",
|
|
50
56
|
"Operation",
|
|
51
57
|
"Phone",
|
|
52
58
|
"Plus",
|
|
53
59
|
"Position",
|
|
54
60
|
"Promotion",
|
|
55
61
|
"QuestionFilled",
|
|
62
|
+
"Rank",
|
|
56
63
|
"Refresh",
|
|
57
64
|
"RefreshLeft",
|
|
58
65
|
"RefreshRight",
|
|
59
66
|
"Search",
|
|
60
67
|
"Setting",
|
|
68
|
+
"Star",
|
|
69
|
+
"StarFilled",
|
|
61
70
|
"Sunny",
|
|
62
71
|
"Switch",
|
|
63
72
|
"SwitchButton",
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/** 语音识别结果的最小结构,便于在不同浏览器实现间复用。 */
|
|
2
|
+
export type SpeechResultList = ArrayLike<{
|
|
3
|
+
0?: {
|
|
4
|
+
transcript: string;
|
|
5
|
+
};
|
|
6
|
+
isFinal?: boolean;
|
|
7
|
+
length: number;
|
|
8
|
+
}>;
|
|
9
|
+
/** 语音识别错误的最小结构,兼容浏览器事件和不支持错误。 */
|
|
10
|
+
export type SpeechRecognitionError = {
|
|
11
|
+
code?: number;
|
|
12
|
+
error?: string;
|
|
13
|
+
message?: string;
|
|
14
|
+
};
|
|
15
|
+
type SpeechRecognitionOptions = {
|
|
16
|
+
/** 识别错误回调。 */
|
|
17
|
+
onError?: (error: SpeechRecognitionError) => void;
|
|
18
|
+
/** 识别开始回调。 */
|
|
19
|
+
onStart?: () => void;
|
|
20
|
+
/** 识别结束回调。 */
|
|
21
|
+
onEnd?: (text: string) => void;
|
|
22
|
+
/** 识别结果回调。 */
|
|
23
|
+
onResult?: (text: string) => void;
|
|
24
|
+
};
|
|
25
|
+
/** 增量累积语音识别结果,避免连续模式下 interim 结果重复计入已确认文本。 */
|
|
26
|
+
export declare function accumulateSpeechResultText(results: SpeechResultList, resultIndex: number, previousFinalText: string): {
|
|
27
|
+
finalText: string;
|
|
28
|
+
text: string;
|
|
29
|
+
};
|
|
30
|
+
/** useSpeechRecognition 创建浏览器语音识别实例,并统一结果、状态和卸载处理。 */
|
|
31
|
+
export declare function useSpeechRecognition({ onError, onStart, onEnd, onResult }?: SpeechRecognitionOptions): {
|
|
32
|
+
loading: globalThis.Ref<boolean, boolean>;
|
|
33
|
+
start: () => void;
|
|
34
|
+
stop: () => void;
|
|
35
|
+
value: globalThis.Ref<string, string>;
|
|
36
|
+
};
|
|
37
|
+
export {};
|
|
@@ -10,3 +10,5 @@ export { setAdminDocumentTitle } from "./documentTitle";
|
|
|
10
10
|
export { kratosAdminModule } from "./modules/kratosAdmin";
|
|
11
11
|
export { bootstrapAdminApp } from "./bootstrap";
|
|
12
12
|
export { refreshAdminRuntimeConfig } from "./stores/modules/config";
|
|
13
|
+
export { useSpeechRecognition } from "./hooks/useSpeechRecognition";
|
|
14
|
+
export type { SpeechRecognitionError } from "./hooks/useSpeechRecognition";
|
|
@@ -15,7 +15,7 @@ export declare function hasValidAccessToken(): boolean;
|
|
|
15
15
|
export declare function getRequestAccessToken(): Promise<string>;
|
|
16
16
|
/** 尝试通过刷新令牌恢复访问令牌,供路由守卫进入页面前调用。 */
|
|
17
17
|
export declare function ensureAccessToken(): Promise<boolean>;
|
|
18
|
-
/**
|
|
18
|
+
/** 统一处理认证失效:会话已不可用时直接清理本地凭据并回到登录页,避免用户停留在全 401 的空壳页面。 */
|
|
19
19
|
export declare function handleAuthExpired(): void;
|
|
20
20
|
/** 以请求体、响应体顺序提供带类型约束的 HTTP 请求函数。 */
|
|
21
21
|
declare function request<Request = unknown, Response = unknown>(config: AxiosRequestConfig<Request>): Promise<Response>;
|
|
@@ -138,7 +138,7 @@ function createMarkdownHeadingId({ text, index }: Parameters<MdHeadingId>[0]) {
|
|
|
138
138
|
--md-theme-table-td-border-color: var(--admin-page-card-border-soft);
|
|
139
139
|
--md-theme-code-inline-color: var(--admin-page-accent-soft-text);
|
|
140
140
|
--md-theme-code-inline-bg-color: var(--admin-page-card-bg-muted);
|
|
141
|
-
--md-theme-code-inline-radius:
|
|
141
|
+
--md-theme-code-inline-radius: var(--admin-page-radius);
|
|
142
142
|
--md-theme-code-block-color: var(--admin-page-text-primary);
|
|
143
143
|
--md-theme-code-block-bg-color: var(--admin-page-card-bg-muted);
|
|
144
144
|
--md-theme-code-before-bg-color: var(--admin-page-card-bg-muted);
|
|
@@ -228,7 +228,7 @@ const renderMoneyCell = (item: ColumnProps, scope: RenderScope<any>) => {
|
|
|
228
228
|
};
|
|
229
229
|
|
|
230
230
|
/**
|
|
231
|
-
*
|
|
231
|
+
* 渲染操作按钮列,三项以内全部展示,超过三项保留两个并折叠其余操作。
|
|
232
232
|
*/
|
|
233
233
|
const renderActionsCell = (item: ColumnProps, scope: RenderScope<any>) => {
|
|
234
234
|
if (!item.actions?.length) return "--";
|
|
@@ -237,7 +237,9 @@ const renderActionsCell = (item: ColumnProps, scope: RenderScope<any>) => {
|
|
|
237
237
|
const primaryLabels = [t("common.action.edit"), t("common.action.delete")];
|
|
238
238
|
const primaryActions = primaryLabels.flatMap(label => visibleActions.filter(action => action.label === label));
|
|
239
239
|
const otherActions = visibleActions.filter(action => !primaryLabels.includes(action.label));
|
|
240
|
-
const
|
|
240
|
+
const orderedActions = [...primaryActions, ...otherActions];
|
|
241
|
+
const inlineActions = visibleActions.length > 3 ? orderedActions.slice(0, 2) : orderedActions;
|
|
242
|
+
const moreActions = visibleActions.length > 3 ? orderedActions.slice(2) : [];
|
|
241
243
|
const buttons = inlineActions.map((action: TableActionProps) => {
|
|
242
244
|
const params = resolveColumnParams(action.params, scope);
|
|
243
245
|
return h(
|
|
@@ -254,7 +256,7 @@ const renderActionsCell = (item: ColumnProps, scope: RenderScope<any>) => {
|
|
|
254
256
|
{ default: () => action.label }
|
|
255
257
|
);
|
|
256
258
|
});
|
|
257
|
-
if (
|
|
259
|
+
if (moreActions.length)
|
|
258
260
|
buttons.push(
|
|
259
261
|
h(
|
|
260
262
|
ElDropdown,
|
|
@@ -264,7 +266,7 @@ const renderActionsCell = (item: ColumnProps, scope: RenderScope<any>) => {
|
|
|
264
266
|
h(ElButton, { type: "primary", link: true, size: "small", icon: ArrowDown }, () => t("common.action.more")),
|
|
265
267
|
dropdown: () =>
|
|
266
268
|
h(ElDropdownMenu, null, () =>
|
|
267
|
-
|
|
269
|
+
moreActions.map(action => {
|
|
268
270
|
const disabled = getBooleanValue(action.disabled, scope);
|
|
269
271
|
const params = resolveColumnParams(action.params, scope);
|
|
270
272
|
return h(
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
list-style: none;
|
|
37
37
|
background: var(--el-color-primary-light-9);
|
|
38
38
|
border: 1px solid var(--el-color-primary-light-5);
|
|
39
|
-
border-radius:
|
|
39
|
+
border-radius: var(--admin-page-radius);
|
|
40
40
|
&:hover {
|
|
41
41
|
color: #ffffff;
|
|
42
42
|
background: var(--el-color-primary);
|
|
@@ -185,14 +185,14 @@ onBeforeUnmount(() => {
|
|
|
185
185
|
</script>
|
|
186
186
|
|
|
187
187
|
<style scoped>
|
|
188
|
-
.yaml-editor { display: flex; flex-direction: column; width: 100%; overflow: hidden; background: var(--el-fill-color-blank); border: 1px solid var(--el-border-color); border-radius:
|
|
188
|
+
.yaml-editor { display: flex; flex-direction: column; width: 100%; overflow: hidden; background: var(--el-fill-color-blank); border: 1px solid var(--el-border-color); border-radius: var(--admin-page-radius); }
|
|
189
189
|
.yaml-editor.is-bounded { max-height: v-bind(maxHeight); }
|
|
190
190
|
.yaml-editor:focus-within { border-color: var(--el-color-primary); }
|
|
191
191
|
.yaml-editor.is-invalid { border-color: var(--el-color-danger); }
|
|
192
192
|
.yaml-editor__toolbar { display: flex; align-items: center; justify-content: space-between; min-height: 32px; padding: 2px 8px; background: var(--el-fill-color-light); border-bottom: 1px solid var(--el-border-color-lighter); }
|
|
193
193
|
.yaml-editor__lang { display: inline-flex; gap: 5px; align-items: center; color: var(--el-text-color-secondary); font-size: 12px; font-weight: 600; letter-spacing: 0.5px; }
|
|
194
194
|
.yaml-editor__actions { display: inline-flex; gap: 2px; }
|
|
195
|
-
.yaml-editor__action { display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 24px; padding: 0; color: var(--el-text-color-secondary); cursor: pointer; background: transparent; border: none; border-radius:
|
|
195
|
+
.yaml-editor__action { display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 24px; padding: 0; color: var(--el-text-color-secondary); cursor: pointer; background: transparent; border: none; border-radius: var(--admin-page-radius); transition: color 0.15s ease, background-color 0.15s ease; }
|
|
196
196
|
.yaml-editor__action:hover, .yaml-editor__action:focus-visible { color: var(--el-color-primary); background: var(--el-fill-color); outline: none; }
|
|
197
197
|
.yaml-editor__action.is-active { color: var(--el-color-primary); }
|
|
198
198
|
.yaml-editor__host { flex: 1 1 auto; min-height: 0; }
|
|
@@ -10,7 +10,7 @@ export const LOGIN_URL: string = "/login";
|
|
|
10
10
|
export const DEFAULT_PRIMARY: string = "#009688";
|
|
11
11
|
|
|
12
12
|
// 路由白名单地址(本地存在的路由 staticRouter.ts 中)
|
|
13
|
-
export const ROUTER_WHITE_LIST: string[] = ["/500"];
|
|
13
|
+
export const ROUTER_WHITE_LIST: string[] = ["/403", "/500"];
|
|
14
14
|
|
|
15
15
|
// 高德地图 key
|
|
16
16
|
export const AMAP_MAP_KEY: string = "";
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { getCurrentInstance, onBeforeUnmount, ref } from "vue";
|
|
2
|
+
|
|
3
|
+
/** 语音识别结果的最小结构,便于在不同浏览器实现间复用。 */
|
|
4
|
+
export type SpeechResultList = ArrayLike<{
|
|
5
|
+
0?: {
|
|
6
|
+
transcript: string;
|
|
7
|
+
};
|
|
8
|
+
isFinal?: boolean;
|
|
9
|
+
length: number;
|
|
10
|
+
}>;
|
|
11
|
+
|
|
12
|
+
/** 语音识别错误的最小结构,兼容浏览器事件和不支持错误。 */
|
|
13
|
+
export type SpeechRecognitionError = {
|
|
14
|
+
code?: number;
|
|
15
|
+
error?: string;
|
|
16
|
+
message?: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
type SpeechRecognitionResultEvent = {
|
|
20
|
+
results: SpeechResultList;
|
|
21
|
+
resultIndex: number;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type SpeechRecognitionInstance = {
|
|
25
|
+
continuous: boolean;
|
|
26
|
+
interimResults: boolean;
|
|
27
|
+
lang: string;
|
|
28
|
+
onstart: (() => void) | null;
|
|
29
|
+
onend: (() => void) | null;
|
|
30
|
+
onerror: ((error: SpeechRecognitionError) => void) | null;
|
|
31
|
+
onresult: ((event: SpeechRecognitionResultEvent) => void) | null;
|
|
32
|
+
start: () => void;
|
|
33
|
+
stop: () => void;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
type SpeechRecognitionConstructor = new () => SpeechRecognitionInstance;
|
|
37
|
+
|
|
38
|
+
type SpeechRecognitionWindow = Window & {
|
|
39
|
+
SpeechRecognition?: SpeechRecognitionConstructor;
|
|
40
|
+
webkitSpeechRecognition?: SpeechRecognitionConstructor;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
type SpeechRecognitionOptions = {
|
|
44
|
+
/** 识别错误回调。 */
|
|
45
|
+
onError?: (error: SpeechRecognitionError) => void;
|
|
46
|
+
/** 识别开始回调。 */
|
|
47
|
+
onStart?: () => void;
|
|
48
|
+
/** 识别结束回调。 */
|
|
49
|
+
onEnd?: (text: string) => void;
|
|
50
|
+
/** 识别结果回调。 */
|
|
51
|
+
onResult?: (text: string) => void;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/** 增量累积语音识别结果,避免连续模式下 interim 结果重复计入已确认文本。 */
|
|
55
|
+
export function accumulateSpeechResultText(
|
|
56
|
+
results: SpeechResultList,
|
|
57
|
+
resultIndex: number,
|
|
58
|
+
previousFinalText: string
|
|
59
|
+
): { finalText: string; text: string } {
|
|
60
|
+
let finalText = previousFinalText;
|
|
61
|
+
let interimText = "";
|
|
62
|
+
for (let index = resultIndex; index < results.length; index++) {
|
|
63
|
+
const result = results[index];
|
|
64
|
+
const transcript = result?.[0]?.transcript ?? "";
|
|
65
|
+
if (result?.isFinal) {
|
|
66
|
+
finalText += transcript;
|
|
67
|
+
} else {
|
|
68
|
+
interimText += transcript;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// Chrome 连续模式下 interim 结果会累计包含已确认的 final 文本,去除重复前缀。
|
|
72
|
+
if (interimText && finalText && interimText.startsWith(finalText)) {
|
|
73
|
+
interimText = interimText.slice(finalText.length);
|
|
74
|
+
}
|
|
75
|
+
return { finalText, text: finalText + interimText };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** useSpeechRecognition 创建浏览器语音识别实例,并统一结果、状态和卸载处理。 */
|
|
79
|
+
export function useSpeechRecognition({ onError, onStart, onEnd, onResult }: SpeechRecognitionOptions = {}) {
|
|
80
|
+
const loading = ref(false);
|
|
81
|
+
const value = ref("");
|
|
82
|
+
let recognition: SpeechRecognitionInstance | null = null;
|
|
83
|
+
let recognitionSession = 0;
|
|
84
|
+
let finalText = "";
|
|
85
|
+
|
|
86
|
+
/** 获取当前浏览器支持的语音识别构造函数。 */
|
|
87
|
+
function resolveRecognitionConstructor() {
|
|
88
|
+
if (typeof window === "undefined") return undefined;
|
|
89
|
+
const speechWindow = window as SpeechRecognitionWindow;
|
|
90
|
+
return speechWindow.SpeechRecognition ?? speechWindow.webkitSpeechRecognition;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** 开始一次语音识别。 */
|
|
94
|
+
function start() {
|
|
95
|
+
if (loading.value) return;
|
|
96
|
+
const RecognitionConstructor = resolveRecognitionConstructor();
|
|
97
|
+
if (!RecognitionConstructor) {
|
|
98
|
+
onError?.({ code: -1, message: "Speech recognition is not supported by this browser" });
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const nextRecognition = new RecognitionConstructor();
|
|
103
|
+
const currentSession = ++recognitionSession;
|
|
104
|
+
recognition = nextRecognition;
|
|
105
|
+
finalText = "";
|
|
106
|
+
const isCurrentRecognition = () => recognition === nextRecognition && recognitionSession === currentSession;
|
|
107
|
+
nextRecognition.continuous = true;
|
|
108
|
+
nextRecognition.interimResults = true;
|
|
109
|
+
nextRecognition.lang = "zh-CN";
|
|
110
|
+
nextRecognition.onstart = () => {
|
|
111
|
+
if (!isCurrentRecognition()) return;
|
|
112
|
+
loading.value = true;
|
|
113
|
+
value.value = "";
|
|
114
|
+
onStart?.();
|
|
115
|
+
};
|
|
116
|
+
nextRecognition.onresult = event => {
|
|
117
|
+
if (!isCurrentRecognition()) return;
|
|
118
|
+
const accumulated = accumulateSpeechResultText(event.results, event.resultIndex, finalText);
|
|
119
|
+
finalText = accumulated.finalText;
|
|
120
|
+
value.value = accumulated.text;
|
|
121
|
+
onResult?.(value.value);
|
|
122
|
+
};
|
|
123
|
+
nextRecognition.onerror = error => {
|
|
124
|
+
if (!isCurrentRecognition()) return;
|
|
125
|
+
loading.value = false;
|
|
126
|
+
recognition = null;
|
|
127
|
+
onError?.(error);
|
|
128
|
+
};
|
|
129
|
+
nextRecognition.onend = () => {
|
|
130
|
+
if (!isCurrentRecognition()) return;
|
|
131
|
+
loading.value = false;
|
|
132
|
+
recognition = null;
|
|
133
|
+
onEnd?.(value.value);
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
loading.value = true;
|
|
137
|
+
try {
|
|
138
|
+
nextRecognition.start();
|
|
139
|
+
} catch (error) {
|
|
140
|
+
if (!isCurrentRecognition()) return;
|
|
141
|
+
loading.value = false;
|
|
142
|
+
recognition = null;
|
|
143
|
+
onError?.({
|
|
144
|
+
code: -2,
|
|
145
|
+
message: error instanceof Error ? error.message : String(error)
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** 停止当前语音识别。 */
|
|
151
|
+
function stop() {
|
|
152
|
+
const currentRecognition = recognition;
|
|
153
|
+
if (!currentRecognition) return;
|
|
154
|
+
recognition = null;
|
|
155
|
+
recognitionSession++;
|
|
156
|
+
loading.value = false;
|
|
157
|
+
onEnd?.(value.value);
|
|
158
|
+
currentRecognition.stop();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (getCurrentInstance()) {
|
|
162
|
+
onBeforeUnmount(() => {
|
|
163
|
+
const currentRecognition = recognition;
|
|
164
|
+
recognition = null;
|
|
165
|
+
recognitionSession++;
|
|
166
|
+
loading.value = false;
|
|
167
|
+
currentRecognition?.stop();
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
loading,
|
|
173
|
+
start,
|
|
174
|
+
stop,
|
|
175
|
+
value
|
|
176
|
+
};
|
|
177
|
+
}
|
|
@@ -11,3 +11,5 @@ export { setAdminDocumentTitle } from "./documentTitle";
|
|
|
11
11
|
export { kratosAdminModule } from "./modules/kratosAdmin";
|
|
12
12
|
export { bootstrapAdminApp } from "./bootstrap";
|
|
13
13
|
export { refreshAdminRuntimeConfig } from "./stores/modules/config";
|
|
14
|
+
export { useSpeechRecognition } from "./hooks/useSpeechRecognition";
|
|
15
|
+
export type { SpeechRecognitionError } from "./hooks/useSpeechRecognition";
|
|
@@ -42,5 +42,15 @@ const headerTools = getAdminHeaderTools();
|
|
|
42
42
|
:deep(.el-dropdown) {
|
|
43
43
|
margin-left: 18px;
|
|
44
44
|
}
|
|
45
|
+
|
|
46
|
+
@media screen and (width <= 600px) {
|
|
47
|
+
.header-icon > * {
|
|
48
|
+
margin-left: 8px;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
:deep(.el-dropdown) {
|
|
52
|
+
margin-left: 8px;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
45
55
|
}
|
|
46
56
|
</style>
|
|
@@ -93,14 +93,23 @@ router.beforeEach(async to => {
|
|
|
93
93
|
// 5.如果没有菜单列表,就重新请求菜单列表并添加动态路由
|
|
94
94
|
if (!authStore.authMenuListGet.length) {
|
|
95
95
|
await userStore.getUserInfo();
|
|
96
|
-
|
|
96
|
+
try {
|
|
97
|
+
await initDynamicRouter();
|
|
98
|
+
} catch {
|
|
99
|
+
// 无菜单权限账号保留登录态并引导到 403 页,避免踢回登录页后因会话有效被弹回。
|
|
100
|
+
return { path: "/403", replace: true };
|
|
101
|
+
}
|
|
97
102
|
if (isUnmatchedRoute(router, to.path)) return getFirstAccessibleRoutePath();
|
|
98
103
|
return { ...to, replace: true };
|
|
99
104
|
}
|
|
100
105
|
|
|
101
106
|
// 5.1 菜单已恢复但路由实例尚未重新挂载时,补跑一次动态路由注册,避免刷新或登录首跳直接命中 404。
|
|
102
107
|
if (isUnmatchedRoute(router, to.path)) {
|
|
103
|
-
|
|
108
|
+
try {
|
|
109
|
+
await initDynamicRouter();
|
|
110
|
+
} catch {
|
|
111
|
+
return { path: "/403", replace: true };
|
|
112
|
+
}
|
|
104
113
|
if (isUnmatchedRoute(router, to.path)) return getFirstAccessibleRoutePath();
|
|
105
114
|
return { ...to, replace: true };
|
|
106
115
|
}
|
|
@@ -129,8 +129,7 @@ export const initDynamicRouter = async () => {
|
|
|
129
129
|
type: "warning",
|
|
130
130
|
duration: 3000
|
|
131
131
|
});
|
|
132
|
-
|
|
133
|
-
router.replace(LOGIN_URL);
|
|
132
|
+
// 保留登录态由调用方引导到 403 页,避免清除会话后登录页回跳形成重定向循环。
|
|
134
133
|
return Promise.reject("No permission");
|
|
135
134
|
}
|
|
136
135
|
|
|
@@ -1,7 +1,59 @@
|
|
|
1
1
|
/* 统一 Element Plus 矩形控件与后台页面的圆角。 */
|
|
2
2
|
html:root {
|
|
3
3
|
--el-border-radius-base: var(--admin-page-radius);
|
|
4
|
-
--el-border-radius-small:
|
|
4
|
+
--el-border-radius-small: var(--admin-page-radius);
|
|
5
|
+
--el-border-radius-round: var(--admin-page-radius);
|
|
6
|
+
--el-popover-border-radius: var(--admin-page-radius);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/* 组件局部变量和小尺寸派生值不会继承基础圆角,需要同步覆盖。 */
|
|
10
|
+
:root .el-card {
|
|
11
|
+
--el-card-border-radius: var(--admin-page-radius);
|
|
12
|
+
}
|
|
13
|
+
:root .el-tag {
|
|
14
|
+
--el-tag-border-radius: var(--admin-page-radius);
|
|
15
|
+
--el-tag-border-radius-rounded: var(--admin-page-radius);
|
|
16
|
+
}
|
|
17
|
+
:root .el-popover {
|
|
18
|
+
--el-popover-border-radius: var(--admin-page-radius);
|
|
19
|
+
}
|
|
20
|
+
:root .el-badge {
|
|
21
|
+
--el-badge-radius: var(--admin-page-radius);
|
|
22
|
+
}
|
|
23
|
+
:root .el-pagination {
|
|
24
|
+
--el-pagination-border-radius: var(--admin-page-radius);
|
|
25
|
+
}
|
|
26
|
+
:root .el-button.el-button--small:not(.is-circle),
|
|
27
|
+
:root .el-checkbox.is-bordered.el-checkbox--small,
|
|
28
|
+
:root .el-color-picker__trigger,
|
|
29
|
+
:root .el-image-viewer__actions,
|
|
30
|
+
:root .el-progress-bar__outer,
|
|
31
|
+
:root .el-progress-bar__inner,
|
|
32
|
+
:root .el-scrollbar__bar,
|
|
33
|
+
:root .el-segmented.el-segmented--small,
|
|
34
|
+
:root .el-segmented .el-segmented__item,
|
|
35
|
+
:root .el-segmented .el-segmented__item-selected,
|
|
36
|
+
:root .el-steps--simple,
|
|
37
|
+
:root .el-switch .el-switch__core,
|
|
38
|
+
:root .el-table-filter,
|
|
39
|
+
:root .el-tabs__new-tab,
|
|
40
|
+
:root .el-tabs__item:focus-visible,
|
|
41
|
+
:root .el-upload--picture-card,
|
|
42
|
+
:root .el-upload-dragger,
|
|
43
|
+
:root .el-upload-list .el-upload-list__item {
|
|
44
|
+
border-radius: var(--admin-page-radius);
|
|
45
|
+
}
|
|
46
|
+
:root .el-input::-webkit-scrollbar-thumb {
|
|
47
|
+
border-radius: var(--admin-page-radius);
|
|
48
|
+
}
|
|
49
|
+
:root .el-tabs--card > .el-tabs__header .el-tabs__nav {
|
|
50
|
+
border-radius: var(--admin-page-radius) var(--admin-page-radius) 0 0;
|
|
51
|
+
}
|
|
52
|
+
:root .el-tabs--left.el-tabs--card .el-tabs__nav {
|
|
53
|
+
border-radius: var(--admin-page-radius) 0 0 var(--admin-page-radius);
|
|
54
|
+
}
|
|
55
|
+
:root .el-tabs--right.el-tabs--card .el-tabs__nav {
|
|
56
|
+
border-radius: 0 var(--admin-page-radius) var(--admin-page-radius) 0;
|
|
5
57
|
}
|
|
6
58
|
|
|
7
59
|
/* el-alert */
|
|
@@ -237,6 +237,12 @@ function hasRefreshCookieHint() {
|
|
|
237
237
|
return Number.isFinite(expiresAt) && expiresAt * 1000 > Date.now();
|
|
238
238
|
}
|
|
239
239
|
|
|
240
|
+
/** 清除刷新令牌过期提示 Cookie,供会话吊销与刷新失败路径复用,避免守卫误判仍可静默刷新。 */
|
|
241
|
+
function clearRefreshExpiryCookie() {
|
|
242
|
+
if (typeof document === "undefined") return;
|
|
243
|
+
document.cookie = `${REFRESH_EXPIRY_COOKIE}=; Max-Age=0; path=/`;
|
|
244
|
+
}
|
|
245
|
+
|
|
240
246
|
/** 判断当前访问令牌是否仍在有效期内。 */
|
|
241
247
|
export function hasValidAccessToken() {
|
|
242
248
|
const userStore = getUserStore();
|
|
@@ -291,30 +297,24 @@ export async function ensureAccessToken() {
|
|
|
291
297
|
let isHandlingAuthExpired = false;
|
|
292
298
|
let passwordChangePromptUntil = 0;
|
|
293
299
|
|
|
294
|
-
/**
|
|
300
|
+
/** 统一处理认证失效:会话已不可用时直接清理本地凭据并回到登录页,避免用户停留在全 401 的空壳页面。 */
|
|
295
301
|
export function handleAuthExpired() {
|
|
296
302
|
if (isHandlingAuthExpired) {
|
|
297
303
|
return;
|
|
298
304
|
}
|
|
299
305
|
|
|
300
306
|
isHandlingAuthExpired = true;
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
clearPasswordChangeRequired();
|
|
313
|
-
userStore.clearAuthData();
|
|
314
|
-
return router.replace({
|
|
315
|
-
path: LOGIN_URL,
|
|
316
|
-
query: redirect ? { redirect } : undefined
|
|
317
|
-
});
|
|
307
|
+
const userStore = getUserStore();
|
|
308
|
+
const currentRoute = router.currentRoute.value;
|
|
309
|
+
const redirect = currentRoute.path === LOGIN_URL ? undefined : currentRoute.fullPath;
|
|
310
|
+
clearPasswordChangeRequired();
|
|
311
|
+
clearRefreshExpiryCookie();
|
|
312
|
+
userStore.clearAuthData();
|
|
313
|
+
ElMessage.warning(t("core.auth.session_expired"));
|
|
314
|
+
void router
|
|
315
|
+
.replace({
|
|
316
|
+
path: LOGIN_URL,
|
|
317
|
+
query: redirect ? { redirect } : undefined
|
|
318
318
|
})
|
|
319
319
|
.finally(() => {
|
|
320
320
|
isHandlingAuthExpired = false;
|
|
@@ -447,6 +447,7 @@ async function handleTokenRefresh(promptOnFailure = true) {
|
|
|
447
447
|
.catch(error => {
|
|
448
448
|
if (isRefreshTokenUnauthorized(error)) {
|
|
449
449
|
// 刷新令牌失效后立即终止后续自动刷新,避免旧 Cookie 被反复提交。
|
|
450
|
+
clearRefreshExpiryCookie();
|
|
450
451
|
getUserStore().clearAuthData();
|
|
451
452
|
}
|
|
452
453
|
if (promptOnFailure) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<el-form ref="loginFormRef" :model="loginForm" :rules="loginRules" size="large">
|
|
2
|
+
<el-form ref="loginFormRef" :model="loginForm" :rules="loginRules" size="large" :validate-on-rule-change="false">
|
|
3
3
|
<el-form-item v-if="configStore.showTenantCode" prop="tenant_code">
|
|
4
4
|
<el-input v-model="loginForm.tenant_code" :placeholder="t('core.login.tenant_code')">
|
|
5
5
|
<template #prefix>
|
|
@@ -586,7 +586,13 @@ const finishLogin = async (mustChangePassword = false) => {
|
|
|
586
586
|
}
|
|
587
587
|
|
|
588
588
|
// 3.添加动态路由
|
|
589
|
-
|
|
589
|
+
try {
|
|
590
|
+
await initDynamicRouter();
|
|
591
|
+
} catch {
|
|
592
|
+
// 无菜单权限账号保留登录态并引导到 403 页,不再继续欢迎流程。
|
|
593
|
+
await router.replace("/403");
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
590
596
|
|
|
591
597
|
// 4.清空 tabs、keepAlive 数据
|
|
592
598
|
tabsStore.setTabs([]);
|
|
@@ -1086,7 +1092,7 @@ watch(
|
|
|
1086
1092
|
:global(.behavior-captcha-dialog .go-captcha .gc-drag-line) {
|
|
1087
1093
|
height: 12px;
|
|
1088
1094
|
margin-top: -6px;
|
|
1089
|
-
border-radius:
|
|
1095
|
+
border-radius: var(--admin-page-radius);
|
|
1090
1096
|
}
|
|
1091
1097
|
|
|
1092
1098
|
:global(.behavior-captcha-dialog .go-captcha .gc-drag-block) {
|
|
@@ -16,11 +16,13 @@ declare global {
|
|
|
16
16
|
const Calendar: typeof import('@element-plus/icons-vue').Calendar
|
|
17
17
|
const Camera: typeof import('@element-plus/icons-vue').Camera
|
|
18
18
|
const ChatDotRound: typeof import('@element-plus/icons-vue').ChatDotRound
|
|
19
|
+
const Check: typeof import('@element-plus/icons-vue').Check
|
|
19
20
|
const CircleCheck: typeof import('@element-plus/icons-vue').CircleCheck
|
|
20
21
|
const CircleCheckFilled: typeof import('@element-plus/icons-vue').CircleCheckFilled
|
|
21
22
|
const CircleClose: typeof import('@element-plus/icons-vue').CircleClose
|
|
22
23
|
const CirclePlus: typeof import('@element-plus/icons-vue').CirclePlus
|
|
23
24
|
const Close: typeof import('@element-plus/icons-vue').Close
|
|
25
|
+
const Coin: typeof import('@element-plus/icons-vue').Coin
|
|
24
26
|
const CollectionTag: typeof import('@element-plus/icons-vue').CollectionTag
|
|
25
27
|
const CopyDocument: typeof import('@element-plus/icons-vue').CopyDocument
|
|
26
28
|
const DataAnalysis: typeof import('@element-plus/icons-vue').DataAnalysis
|
|
@@ -46,23 +48,30 @@ declare global {
|
|
|
46
48
|
const Fold: typeof import('@element-plus/icons-vue').Fold
|
|
47
49
|
const Folder: typeof import('@element-plus/icons-vue').Folder
|
|
48
50
|
const Goods: typeof import('@element-plus/icons-vue').Goods
|
|
51
|
+
const Grid: typeof import('@element-plus/icons-vue').Grid
|
|
49
52
|
const InfoFilled: typeof import('@element-plus/icons-vue').InfoFilled
|
|
50
53
|
const List: typeof import('@element-plus/icons-vue').List
|
|
54
|
+
const Loading: typeof import('@element-plus/icons-vue').Loading
|
|
51
55
|
const Lock: typeof import('@element-plus/icons-vue').Lock
|
|
52
56
|
const Male: typeof import('@element-plus/icons-vue').Male
|
|
53
57
|
const Message: typeof import('@element-plus/icons-vue').Message
|
|
58
|
+
const Microphone: typeof import('@element-plus/icons-vue').Microphone
|
|
54
59
|
const Moon: typeof import('@element-plus/icons-vue').Moon
|
|
60
|
+
const MoreFilled: typeof import('@element-plus/icons-vue').MoreFilled
|
|
55
61
|
const Operation: typeof import('@element-plus/icons-vue').Operation
|
|
56
62
|
const Phone: typeof import('@element-plus/icons-vue').Phone
|
|
57
63
|
const Plus: typeof import('@element-plus/icons-vue').Plus
|
|
58
64
|
const Position: typeof import('@element-plus/icons-vue').Position
|
|
59
65
|
const Promotion: typeof import('@element-plus/icons-vue').Promotion
|
|
60
66
|
const QuestionFilled: typeof import('@element-plus/icons-vue').QuestionFilled
|
|
67
|
+
const Rank: typeof import('@element-plus/icons-vue').Rank
|
|
61
68
|
const Refresh: typeof import('@element-plus/icons-vue').Refresh
|
|
62
69
|
const RefreshLeft: typeof import('@element-plus/icons-vue').RefreshLeft
|
|
63
70
|
const RefreshRight: typeof import('@element-plus/icons-vue').RefreshRight
|
|
64
71
|
const Search: typeof import('@element-plus/icons-vue').Search
|
|
65
72
|
const Setting: typeof import('@element-plus/icons-vue').Setting
|
|
73
|
+
const Star: typeof import('@element-plus/icons-vue').Star
|
|
74
|
+
const StarFilled: typeof import('@element-plus/icons-vue').StarFilled
|
|
66
75
|
const Sunny: typeof import('@element-plus/icons-vue').Sunny
|
|
67
76
|
const Switch: typeof import('@element-plus/icons-vue').Switch
|
|
68
77
|
const SwitchButton: typeof import('@element-plus/icons-vue').SwitchButton
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@liujitcn/kratos-admin-core",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.11",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "kratos-admin frontend runtime and base modules",
|
|
7
7
|
"author": {
|
|
@@ -263,7 +263,7 @@
|
|
|
263
263
|
"scripts": {
|
|
264
264
|
"build": "pnpm build:package",
|
|
265
265
|
"build:package": "node ../../scripts/build-package.mjs .",
|
|
266
|
-
"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 dist/test/tests/locale-reactivity.test.js",
|
|
266
|
+
"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 dist/test/tests/locale-reactivity.test.js dist/test/tests/voice-input.test.js",
|
|
267
267
|
"type:check": "vue-tsc --noEmit --skipLibCheck"
|
|
268
268
|
}
|
|
269
269
|
}
|
|
@@ -16,11 +16,13 @@ declare global {
|
|
|
16
16
|
const Calendar: typeof import('@element-plus/icons-vue').Calendar
|
|
17
17
|
const Camera: typeof import('@element-plus/icons-vue').Camera
|
|
18
18
|
const ChatDotRound: typeof import('@element-plus/icons-vue').ChatDotRound
|
|
19
|
+
const Check: typeof import('@element-plus/icons-vue').Check
|
|
19
20
|
const CircleCheck: typeof import('@element-plus/icons-vue').CircleCheck
|
|
20
21
|
const CircleCheckFilled: typeof import('@element-plus/icons-vue').CircleCheckFilled
|
|
21
22
|
const CircleClose: typeof import('@element-plus/icons-vue').CircleClose
|
|
22
23
|
const CirclePlus: typeof import('@element-plus/icons-vue').CirclePlus
|
|
23
24
|
const Close: typeof import('@element-plus/icons-vue').Close
|
|
25
|
+
const Coin: typeof import('@element-plus/icons-vue').Coin
|
|
24
26
|
const CollectionTag: typeof import('@element-plus/icons-vue').CollectionTag
|
|
25
27
|
const CopyDocument: typeof import('@element-plus/icons-vue').CopyDocument
|
|
26
28
|
const DataAnalysis: typeof import('@element-plus/icons-vue').DataAnalysis
|
|
@@ -46,23 +48,30 @@ declare global {
|
|
|
46
48
|
const Fold: typeof import('@element-plus/icons-vue').Fold
|
|
47
49
|
const Folder: typeof import('@element-plus/icons-vue').Folder
|
|
48
50
|
const Goods: typeof import('@element-plus/icons-vue').Goods
|
|
51
|
+
const Grid: typeof import('@element-plus/icons-vue').Grid
|
|
49
52
|
const InfoFilled: typeof import('@element-plus/icons-vue').InfoFilled
|
|
50
53
|
const List: typeof import('@element-plus/icons-vue').List
|
|
54
|
+
const Loading: typeof import('@element-plus/icons-vue').Loading
|
|
51
55
|
const Lock: typeof import('@element-plus/icons-vue').Lock
|
|
52
56
|
const Male: typeof import('@element-plus/icons-vue').Male
|
|
53
57
|
const Message: typeof import('@element-plus/icons-vue').Message
|
|
58
|
+
const Microphone: typeof import('@element-plus/icons-vue').Microphone
|
|
54
59
|
const Moon: typeof import('@element-plus/icons-vue').Moon
|
|
60
|
+
const MoreFilled: typeof import('@element-plus/icons-vue').MoreFilled
|
|
55
61
|
const Operation: typeof import('@element-plus/icons-vue').Operation
|
|
56
62
|
const Phone: typeof import('@element-plus/icons-vue').Phone
|
|
57
63
|
const Plus: typeof import('@element-plus/icons-vue').Plus
|
|
58
64
|
const Position: typeof import('@element-plus/icons-vue').Position
|
|
59
65
|
const Promotion: typeof import('@element-plus/icons-vue').Promotion
|
|
60
66
|
const QuestionFilled: typeof import('@element-plus/icons-vue').QuestionFilled
|
|
67
|
+
const Rank: typeof import('@element-plus/icons-vue').Rank
|
|
61
68
|
const Refresh: typeof import('@element-plus/icons-vue').Refresh
|
|
62
69
|
const RefreshLeft: typeof import('@element-plus/icons-vue').RefreshLeft
|
|
63
70
|
const RefreshRight: typeof import('@element-plus/icons-vue').RefreshRight
|
|
64
71
|
const Search: typeof import('@element-plus/icons-vue').Search
|
|
65
72
|
const Setting: typeof import('@element-plus/icons-vue').Setting
|
|
73
|
+
const Star: typeof import('@element-plus/icons-vue').Star
|
|
74
|
+
const StarFilled: typeof import('@element-plus/icons-vue').StarFilled
|
|
66
75
|
const Sunny: typeof import('@element-plus/icons-vue').Sunny
|
|
67
76
|
const Switch: typeof import('@element-plus/icons-vue').Switch
|
|
68
77
|
const SwitchButton: typeof import('@element-plus/icons-vue').SwitchButton
|