@liujitcn/kratos-admin-core 0.0.29 → 0.0.32

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.
Files changed (147) hide show
  1. package/README.md +15 -8
  2. package/build/auto-imports.json +73 -0
  3. package/build/plugins.ts +2 -58
  4. package/dist/package/declarations/src/api/base/{config.d.ts → v1/config.d.ts} +1 -1
  5. package/dist/package/declarations/src/api/base/{file.d.ts → v1/file.d.ts} +3 -1
  6. package/dist/package/declarations/src/api/base/{language.d.ts → v1/language.d.ts} +1 -1
  7. package/dist/package/declarations/src/api/base/{login.d.ts → v1/login.d.ts} +2 -2
  8. package/dist/package/declarations/src/api/base/v1/mfa.d.ts +26 -0
  9. package/dist/package/declarations/src/api/base/{oauth.d.ts → v1/oauth.d.ts} +1 -1
  10. package/dist/package/declarations/src/api/system/{auth.d.ts → admin/v1/auth.d.ts} +2 -2
  11. package/dist/package/declarations/src/api/system/{base_dict.d.ts → admin/v1/base_dict.d.ts} +1 -1
  12. package/dist/package/declarations/src/api/system/{base_tenant.d.ts → admin/v1/base_tenant.d.ts} +2 -2
  13. package/dist/package/declarations/src/components/Dialog/FormDialog.vue.d.ts +3 -3
  14. package/dist/package/declarations/src/components/Dialog/PasswordVerifyDialog.vue.d.ts +65 -0
  15. package/dist/package/declarations/src/components/Dialog/ProDialog.vue.d.ts +1 -1
  16. package/dist/package/declarations/src/components/Grid/components/GridItem.vue.d.ts +1 -1
  17. package/dist/package/declarations/src/components/Mfa/MfaRecoveryCodesDialog.vue.d.ts +19 -0
  18. package/dist/package/declarations/src/components/Mfa/MfaSetupPanel.vue.d.ts +8 -0
  19. package/dist/package/declarations/src/components/ProForm/components/ProFormItem.vue.d.ts +4 -4
  20. package/dist/package/declarations/src/components/ProForm/index.vue.d.ts +1 -1
  21. package/dist/package/declarations/src/components/ProForm/interface/index.d.ts +4 -1
  22. package/dist/package/declarations/src/components/ProTable/index.vue.d.ts +114 -0
  23. package/dist/package/declarations/src/components/RichTextPreview/index.vue.d.ts +8 -0
  24. package/dist/package/declarations/src/components/TreeFilter/index.vue.d.ts +84 -76
  25. package/dist/package/declarations/src/components/Upload/File.vue.d.ts +1 -1
  26. package/dist/package/declarations/src/components/Upload/Files.vue.d.ts +1 -1
  27. package/dist/package/declarations/src/components/Upload/Img.vue.d.ts +2 -2
  28. package/dist/package/declarations/src/components/Upload/Imgs.vue.d.ts +2 -2
  29. package/dist/package/declarations/src/components/WangEditor/index.vue.d.ts +2 -0
  30. package/dist/package/declarations/src/documentTitle.d.ts +2 -0
  31. package/dist/package/declarations/src/format.d.ts +1 -1
  32. package/dist/package/declarations/src/index.d.ts +2 -1
  33. package/dist/package/declarations/src/layouts/components/LockScreen.vue.d.ts +3 -0
  34. package/dist/package/declarations/src/locales/generated.d.ts +208 -0
  35. package/dist/package/declarations/src/locales/index.d.ts +2 -0
  36. package/dist/package/declarations/src/modules/index.d.ts +1 -3
  37. package/dist/package/declarations/src/request.d.ts +1 -1
  38. package/dist/package/declarations/src/routers/index.d.ts +1 -1
  39. package/dist/package/declarations/src/rpc/base/v1/login.d.ts +37 -5
  40. package/dist/package/declarations/src/rpc/base/v1/mfa.d.ts +152 -0
  41. package/dist/package/declarations/src/rpc/base/v1/oauth.d.ts +25 -0
  42. package/dist/package/declarations/src/rpc/base/v1/oauth_client.d.ts +23 -0
  43. package/dist/package/declarations/src/rpc/system/admin/v1/auth.d.ts +13 -0
  44. package/dist/package/declarations/src/rpc/system/admin/v1/base_dict.d.ts +0 -101
  45. package/dist/package/declarations/src/rpc/system/admin/v1/base_i18n.d.ts +10 -6
  46. package/dist/package/declarations/src/rpc/system/common/v1/common.d.ts +26 -0
  47. package/dist/package/declarations/src/security.d.ts +2 -0
  48. package/dist/package/declarations/src/stores/interface/index.d.ts +19 -1
  49. package/dist/package/declarations/src/stores/modules/lockScreen.d.ts +20 -0
  50. package/dist/package/declarations/src/stores/modules/user.d.ts +7 -6
  51. package/dist/package/declarations/src/stores/runtime.d.ts +1 -0
  52. package/dist/package/declarations/src/utils/clipboard.d.ts +2 -0
  53. package/dist/package/declarations/src/utils/passwordStrength.d.ts +1 -12
  54. package/dist/package/declarations/src/utils/request.d.ts +13 -2
  55. package/dist/package/declarations/src/utils/utils.d.ts +4 -0
  56. package/dist/package/declarations/src/utils/webauthn.d.ts +4 -0
  57. package/dist/package/declarations/vite.config.d.ts +2 -0
  58. package/dist/package/src/api/base/{config.ts → v1/config.ts} +2 -2
  59. package/dist/package/src/api/base/{file.ts → v1/file.ts} +20 -24
  60. package/dist/package/src/api/base/{language.ts → v1/language.ts} +2 -2
  61. package/dist/package/src/api/base/{login.ts → v1/login.ts} +3 -3
  62. package/dist/package/src/api/base/v1/mfa.ts +114 -0
  63. package/dist/package/src/api/base/{oauth.ts → v1/oauth.ts} +2 -2
  64. package/dist/package/src/api/system/{auth.ts → admin/v1/auth.ts} +3 -3
  65. package/dist/package/src/api/system/{base_dict.ts → admin/v1/base_dict.ts} +2 -2
  66. package/dist/package/src/api/system/{base_tenant.ts → admin/v1/base_tenant.ts} +3 -3
  67. package/dist/package/src/bootstrap.ts +6 -5
  68. package/dist/package/src/components/CronExpression/index.vue +4 -3
  69. package/dist/package/src/components/Dialog/PasswordVerifyDialog.vue +176 -0
  70. package/dist/package/src/components/Dialog/ProDialog.vue +1 -0
  71. package/dist/package/src/components/ImportExcel/index.vue +4 -2
  72. package/dist/package/src/components/Loading/fullScreen.ts +2 -1
  73. package/dist/package/src/components/MarkdownPreview/index.vue +22 -2
  74. package/dist/package/src/components/Mfa/MfaRecoveryCodesDialog.vue +91 -0
  75. package/dist/package/src/components/Mfa/MfaSetupPanel.vue +81 -0
  76. package/dist/package/src/components/PasswordStrength/index.vue +1 -1
  77. package/dist/package/src/components/ProForm/components/ProFormItem.vue +16 -1
  78. package/dist/package/src/components/ProForm/interface/index.ts +4 -0
  79. package/dist/package/src/components/ProTable/components/ColSetting.vue +1 -1
  80. package/dist/package/src/components/ProTable/components/TableColumn.vue +14 -6
  81. package/dist/package/src/components/ProTable/index.vue +9 -5
  82. package/dist/package/src/components/RichTextPreview/index.vue +56 -0
  83. package/dist/package/src/components/SelectIcon/index.vue +3 -2
  84. package/dist/package/src/components/Upload/File.vue +2 -2
  85. package/dist/package/src/components/Upload/Files.vue +2 -2
  86. package/dist/package/src/components/Upload/Img.vue +7 -5
  87. package/dist/package/src/components/Upload/Imgs.vue +6 -5
  88. package/dist/package/src/components/WangEditor/index.vue +10 -7
  89. package/dist/package/src/config/index.ts +1 -1
  90. package/dist/package/src/documentTitle.ts +8 -0
  91. package/dist/package/src/format.ts +1 -1
  92. package/dist/package/src/index.ts +2 -1
  93. package/dist/package/src/layouts/LayoutClassic/index.vue +3 -1
  94. package/dist/package/src/layouts/LayoutColumns/index.vue +3 -1
  95. package/dist/package/src/layouts/LayoutTransverse/index.vue +3 -1
  96. package/dist/package/src/layouts/LayoutVertical/index.vue +9 -1
  97. package/dist/package/src/layouts/components/Header/components/Avatar.vue +26 -6
  98. package/dist/package/src/layouts/components/Header/components/InfoDialog.vue +4 -3
  99. package/dist/package/src/layouts/components/Header/components/PasswordDialog.vue +4 -3
  100. package/dist/package/src/layouts/components/Header/components/SearchMenu.vue +16 -8
  101. package/dist/package/src/layouts/components/LockScreen.vue +573 -0
  102. package/dist/package/src/layouts/components/Tabs/index.vue +12 -3
  103. package/dist/package/src/layouts/components/ThemeDrawer/index.scss +4 -4
  104. package/dist/package/src/layouts/index.vue +2 -0
  105. package/dist/package/src/layouts/indexAsync.vue +2 -0
  106. package/dist/package/src/locales/README.md +2 -1
  107. package/dist/package/src/locales/en-US.json +51 -3
  108. package/dist/package/src/locales/index.ts +16 -12
  109. package/dist/package/src/locales/ja-JP.json +51 -3
  110. package/dist/package/src/locales/zh-CN.json +51 -3
  111. package/dist/package/src/locales/zh-TW.json +51 -3
  112. package/dist/package/src/modules/index.ts +2 -4
  113. package/dist/package/src/request.ts +3 -0
  114. package/dist/package/src/routers/index.ts +15 -14
  115. package/dist/package/src/rpc/base/v1/login.ts +37 -4
  116. package/dist/package/src/rpc/base/v1/mfa.ts +180 -0
  117. package/dist/package/src/rpc/base/v1/oauth.ts +25 -0
  118. package/dist/package/src/rpc/base/v1/oauth_client.ts +33 -0
  119. package/dist/package/src/rpc/system/admin/v1/auth.ts +13 -0
  120. package/dist/package/src/rpc/system/admin/v1/base_dict.ts +0 -112
  121. package/dist/package/src/rpc/system/admin/v1/base_i18n.ts +10 -6
  122. package/dist/package/src/rpc/system/common/v1/common.ts +35 -0
  123. package/dist/package/src/security.ts +2 -0
  124. package/dist/package/src/stores/interface/index.ts +20 -1
  125. package/dist/package/src/stores/modules/auth.ts +1 -1
  126. package/dist/package/src/stores/modules/config.ts +2 -2
  127. package/dist/package/src/stores/modules/dict.ts +1 -1
  128. package/dist/package/src/stores/modules/lockScreen.ts +78 -0
  129. package/dist/package/src/stores/modules/user.ts +34 -22
  130. package/dist/package/src/stores/runtime.ts +1 -0
  131. package/dist/package/src/styles/element.scss +35 -4
  132. package/dist/package/src/utils/clipboard.ts +22 -0
  133. package/dist/package/src/utils/passwordCrypto.ts +1 -1
  134. package/dist/package/src/utils/passwordStrength.ts +7 -24
  135. package/dist/package/src/utils/request.ts +175 -40
  136. package/dist/package/src/utils/tenant.ts +1 -1
  137. package/dist/package/src/utils/utils.ts +25 -6
  138. package/dist/package/src/utils/webauthn.ts +90 -0
  139. package/dist/package/src/views/login/components/LoginForm.vue +257 -24
  140. package/dist/package/src/views/login/index.scss +2 -2
  141. package/dist/package/src/views/login/index.vue +3 -1
  142. package/dist/package/types/generated/auto-imports.d.ts +132 -129
  143. package/dist/package/types/generated/components.d.ts +4 -72
  144. package/package.json +22 -1
  145. package/types/generated/auto-imports.d.ts +132 -129
  146. package/types/generated/components.d.ts +4 -72
  147. package/vite.config.ts +4 -1
@@ -0,0 +1,91 @@
1
+ <template>
2
+ <ProDialog
3
+ :model-value="modelValue"
4
+ :title="t('core.login.mfa_recovery_codes_title')"
5
+ width="420px"
6
+ :close-on-click-modal="false"
7
+ @update:model-value="emit('update:modelValue', $event)"
8
+ >
9
+ <div class="mfa-recovery-dialog__codes">
10
+ <span>{{ codesText }}</span>
11
+ <el-tooltip :content="t('core.login.mfa_copy_recovery_codes')" placement="top">
12
+ <el-button text circle :aria-label="t('core.login.mfa_copy_recovery_codes')" @click="copyRecoveryCodes">
13
+ <el-icon class="mfa-recovery-dialog__copy"><CopyDocument /></el-icon>
14
+ </el-button>
15
+ </el-tooltip>
16
+ </div>
17
+ <p class="mfa-recovery-dialog__warning">{{ t("core.login.mfa_recovery_codes_warning") }}</p>
18
+ <template #footer>
19
+ <el-button type="primary" @click="handleConfirm">{{ t("core.login.mfa_recovery_codes_confirm") }}</el-button>
20
+ </template>
21
+ </ProDialog>
22
+ </template>
23
+
24
+ <script setup lang="ts" name="MfaRecoveryCodesDialog">
25
+ import { computed } from "vue";
26
+ import ProDialog from "../../components/Dialog/ProDialog.vue";
27
+ import { useLocaleStore } from "../../locales";
28
+ import { copyText } from "../../utils/clipboard";
29
+
30
+ /** MFA 恢复码弹窗属性。 */
31
+ interface MfaRecoveryCodesDialogProps {
32
+ /** 弹窗显示状态。 */
33
+ modelValue: boolean;
34
+ /** 本次生成的一次性恢复码。 */
35
+ codes: string[];
36
+ }
37
+
38
+ const props = withDefaults(defineProps<MfaRecoveryCodesDialogProps>(), {
39
+ modelValue: false,
40
+ codes: () => []
41
+ });
42
+
43
+ const emit = defineEmits<{
44
+ "update:modelValue": [value: boolean];
45
+ confirm: [];
46
+ }>();
47
+
48
+ const { t } = useLocaleStore();
49
+ const codesText = computed(() => props.codes.join("\n"));
50
+
51
+ /** 复制当前显示的恢复码。 */
52
+ async function copyRecoveryCodes() {
53
+ await copyText(codesText.value);
54
+ ElMessage.success(t("core.login.mfa_recovery_codes_copied"));
55
+ }
56
+
57
+ /** 关闭弹窗并通知调用方完成绑定流程。 */
58
+ function handleConfirm() {
59
+ emit("update:modelValue", false);
60
+ emit("confirm");
61
+ }
62
+ </script>
63
+
64
+ <style scoped lang="scss">
65
+ .mfa-recovery-dialog__codes {
66
+ display: flex;
67
+ gap: 8px;
68
+ align-items: flex-start;
69
+ color: var(--el-text-color-regular);
70
+ line-height: 1.6;
71
+ overflow-wrap: anywhere;
72
+ }
73
+
74
+ .mfa-recovery-dialog__codes span {
75
+ flex: 1;
76
+ min-width: 0;
77
+ white-space: pre-wrap;
78
+ }
79
+
80
+ .mfa-recovery-dialog__copy {
81
+ color: var(--el-color-primary);
82
+ font-size: 18px;
83
+ }
84
+
85
+ .mfa-recovery-dialog__warning {
86
+ margin: 8px 0 0;
87
+ color: var(--el-color-warning);
88
+ font-size: 13px;
89
+ line-height: 1.6;
90
+ }
91
+ </style>
@@ -0,0 +1,81 @@
1
+ <template>
2
+ <div v-if="uri" class="mfa-setup-panel">
3
+ <img v-if="qrDataUrl" class="mfa-setup-panel__qr" :src="qrDataUrl" :alt="t('core.login.mfa_setup_qr_alt')" />
4
+ <el-collapse class="mfa-setup-panel__uri">
5
+ <el-collapse-item :title="t('core.login.mfa_show_uri')" name="uri">
6
+ <div class="mfa-setup-panel__value">
7
+ <span class="mfa-setup-panel__text">{{ uri }}</span>
8
+ <el-tooltip :content="t('core.login.mfa_copy_uri')" placement="top">
9
+ <el-button text circle :aria-label="t('core.login.mfa_copy_uri')" @click.stop="copySetupUri">
10
+ <el-icon class="mfa-setup-panel__copy"><CopyDocument /></el-icon>
11
+ </el-button>
12
+ </el-tooltip>
13
+ </div>
14
+ </el-collapse-item>
15
+ </el-collapse>
16
+ </div>
17
+ </template>
18
+
19
+ <script setup lang="ts" name="MfaSetupPanel">
20
+ import { computed } from "vue";
21
+ import qrcode from "qrcode-generator";
22
+ import { useLocaleStore } from "../../locales";
23
+ import { copyText } from "../../utils/clipboard";
24
+
25
+ /** MFA TOTP 绑定面板属性。 */
26
+ interface MfaSetupPanelProps {
27
+ /** TOTP 绑定地址。 */
28
+ uri: string;
29
+ }
30
+
31
+ const props = defineProps<MfaSetupPanelProps>();
32
+ const { t } = useLocaleStore();
33
+
34
+ const qrDataUrl = computed(() => {
35
+ if (!props.uri) return "";
36
+ const code = qrcode(0, "M");
37
+ code.addData(props.uri);
38
+ code.make();
39
+ return code.createDataURL(4, 8);
40
+ });
41
+
42
+ /** 复制 TOTP 绑定地址。 */
43
+ async function copySetupUri() {
44
+ await copyText(props.uri);
45
+ ElMessage.success(t("core.login.mfa_uri_copied"));
46
+ }
47
+ </script>
48
+
49
+ <style scoped lang="scss">
50
+ .mfa-setup-panel__qr {
51
+ display: block;
52
+ width: 220px;
53
+ height: 220px;
54
+ margin: 12px auto;
55
+ image-rendering: pixelated;
56
+ }
57
+
58
+ .mfa-setup-panel__uri {
59
+ margin-top: 12px;
60
+ }
61
+
62
+ .mfa-setup-panel__value {
63
+ display: flex;
64
+ gap: 8px;
65
+ align-items: flex-start;
66
+ color: var(--el-text-color-regular);
67
+ line-height: 1.6;
68
+ overflow-wrap: anywhere;
69
+ }
70
+
71
+ .mfa-setup-panel__text {
72
+ flex: 1;
73
+ min-width: 0;
74
+ white-space: pre-wrap;
75
+ }
76
+
77
+ .mfa-setup-panel__copy {
78
+ color: var(--el-color-primary);
79
+ font-size: 18px;
80
+ }
81
+ </style>
@@ -53,7 +53,7 @@ const strength = computed(() => getPasswordStrength(props.password));
53
53
  padding: 14px 16px;
54
54
  background: #fafbfd;
55
55
  border: 1px solid #ebeef5;
56
- border-radius: 10px;
56
+ border-radius: var(--admin-page-radius);
57
57
  }
58
58
  .password-strength__header {
59
59
  display: flex;
@@ -13,6 +13,8 @@
13
13
 
14
14
  <el-input-number v-else-if="field.component === 'input-number'" v-model="fieldValue" v-bind="fieldProps" />
15
15
 
16
+ <el-color-picker v-else-if="field.component === 'color-picker'" v-model="fieldValue" v-bind="fieldProps" />
17
+
16
18
  <el-segmented v-else-if="field.component === 'segmented'" v-model="fieldValue" :options="fieldOptions" v-bind="fieldProps" />
17
19
 
18
20
  <el-checkbox v-else-if="field.component === 'checkbox'" v-model="fieldValue" v-bind="fieldProps">
@@ -28,7 +30,12 @@
28
30
  :label="option.label"
29
31
  :value="option.value"
30
32
  :disabled="option.disabled"
31
- />
33
+ >
34
+ <span class="pro-form-option">
35
+ <component :is="option.icon" v-if="option.icon" />
36
+ <span>{{ option.label }}</span>
37
+ </span>
38
+ </el-option>
32
39
  </el-select>
33
40
 
34
41
  <el-radio-group v-else-if="field.component === 'radio-group'" v-model="fieldValue" v-bind="fieldProps">
@@ -189,3 +196,11 @@ const multipleKvValue = computed({
189
196
  set: value => setFieldValue(value)
190
197
  });
191
198
  </script>
199
+
200
+ <style scoped lang="scss">
201
+ .pro-form-option {
202
+ display: inline-flex;
203
+ align-items: center;
204
+ gap: 6px;
205
+ }
206
+ </style>
@@ -1,4 +1,5 @@
1
1
  import type { FormItemRule } from "element-plus";
2
+ import type { Component } from "vue";
2
3
 
3
4
  /** ProForm 支持的字段组件类型。 */
4
5
  export type ProFormComponentType =
@@ -6,6 +7,7 @@ export type ProFormComponentType =
6
7
  | "password"
7
8
  | "textarea"
8
9
  | "input-number"
10
+ | "color-picker"
9
11
  | "segmented"
10
12
  | "switch"
11
13
  | "checkbox"
@@ -31,6 +33,8 @@ export interface ProFormOption {
31
33
  label: string;
32
34
  value: string | number | boolean;
33
35
  disabled?: boolean;
36
+ /** 下拉项前置图标。 */
37
+ icon?: Component;
34
38
  children?: ProFormOption[];
35
39
  /** Element Plus 懒加载树节点是否为叶子节点。 */
36
40
  isLeaf?: boolean;
@@ -12,7 +12,7 @@
12
12
  </el-table-column>
13
13
  <template #empty>
14
14
  <div class="table-empty">
15
- <img src="../../../assets/images/notData.png" alt="notData" />
15
+ <img src="../../../assets/images/notData.png" :alt="t('core.table.no_configurable_columns')" />
16
16
  <div>{{ t("core.table.no_configurable_columns") }}</div>
17
17
  </div>
18
18
  </template>
@@ -9,7 +9,7 @@ import DictLabel from "../../../components/Dict/DictLabel.vue";
9
9
  import { ColumnProps, HeaderRenderScope, RenderScope, TableActionProps } from "../../../components/ProTable/interface";
10
10
  import type { TableAlign } from "../../../utils/proTable";
11
11
  import { filterEnum, formatValue, handleProp, handleRowAccordingToProp } from "../../../utils";
12
- import { formatPrice } from "../../../utils/utils";
12
+ import { formatPrice, formatSrc } from "../../../utils/utils";
13
13
  import { t } from "../../../locales";
14
14
 
15
15
  const props = defineProps<{
@@ -93,12 +93,14 @@ const getBooleanValue = (value: boolean | ((scope: RenderScope<any>) => boolean)
93
93
  */
94
94
  const renderImageCell = (item: ColumnProps, scope: RenderScope<any>) => {
95
95
  const imageProps = item.imageProps ?? {};
96
- const src =
96
+ const rawSrc =
97
97
  typeof imageProps.src === "function"
98
98
  ? imageProps.src(scope)
99
99
  : (imageProps.src ?? handleRowAccordingToProp(scope.row, item.prop!));
100
- const previewSrc = typeof imageProps.previewSrc === "function" ? imageProps.previewSrc(scope) : (imageProps.previewSrc ?? src);
101
- if (!src || src === "--") return "--";
100
+ const rawPreviewSrc = typeof imageProps.previewSrc === "function" ? imageProps.previewSrc(scope) : (imageProps.previewSrc ?? rawSrc);
101
+ if (!rawSrc || rawSrc === "--") return "--";
102
+ const src = formatSrc(String(rawSrc));
103
+ const previewSrc = formatSrc(String(rawPreviewSrc));
102
104
  const thumbWidth = typeof imageProps.width === "number" ? `${imageProps.width}px` : (imageProps.width ?? "60px");
103
105
  const thumbHeight = typeof imageProps.height === "number" ? `${imageProps.height}px` : (imageProps.height ?? "60px");
104
106
  return h(
@@ -116,7 +118,7 @@ const renderImageCell = (item: ColumnProps, scope: RenderScope<any>) => {
116
118
  style: {
117
119
  width: thumbWidth,
118
120
  height: thumbHeight,
119
- borderRadius: "4px"
121
+ borderRadius: "var(--admin-page-radius)"
120
122
  }
121
123
  },
122
124
  {
@@ -187,6 +189,12 @@ const renderActionsCell = (item: ColumnProps, scope: RenderScope<any>) => {
187
189
  });
188
190
  };
189
191
 
192
+ /** 渲染默认表头标题,避免窄列换行并保留完整标题提示。 */
193
+ const renderHeaderLabel = (item: ColumnProps) => {
194
+ if (!item.label || item.showOverflowTooltip === false) return item.label;
195
+ return h("span", { title: item.label }, item.label);
196
+ };
197
+
190
198
  /**
191
199
  * 渲染预置列类型,统一收敛图片、状态和操作按钮等通用场景。
192
200
  */
@@ -227,7 +235,7 @@ const RenderTableColumn = (item: ColumnProps) => {
227
235
  header: (scope: HeaderRenderScope<any>) => {
228
236
  if (item.headerRender) return item.headerRender(scope);
229
237
  if (item.prop && slots[`${handleProp(item.prop)}Header`]) return slots[`${handleProp(item.prop)}Header`]!(scope);
230
- return item.label;
238
+ return renderHeaderLabel(item);
231
239
  }
232
240
  }
233
241
  );
@@ -41,7 +41,11 @@
41
41
  <el-tooltip v-if="showToolButton('refresh')" :content="t('core.table.refresh')" placement="top">
42
42
  <el-button class="tool-button" :icon="Refresh" circle @click="handleToolRefresh" />
43
43
  </el-tooltip>
44
- <el-tooltip v-if="showToolButton('setting') && columns.length" :content="t('core.table.column_setting')" placement="top">
44
+ <el-tooltip
45
+ v-if="showToolButton('setting') && columns.length"
46
+ :content="t('core.table.column_setting')"
47
+ placement="top"
48
+ >
45
49
  <el-button class="tool-button" :icon="Operation" circle @click="openColSetting" />
46
50
  </el-tooltip>
47
51
  <el-tooltip
@@ -68,7 +72,7 @@
68
72
  >
69
73
  <!-- 默认插槽 -->
70
74
  <slot />
71
- <template v-for="item in tableColumns" :key="item">
75
+ <template v-for="(item, index) in tableColumns" :key="item.prop ?? item.type ?? index">
72
76
  <!-- selection || radio || index || expand || sort -->
73
77
  <el-table-column
74
78
  v-if="item.type && columnTypes.includes(item.type)"
@@ -107,7 +111,7 @@
107
111
  <template #empty>
108
112
  <div class="table-empty">
109
113
  <slot name="empty">
110
- <img src="../../assets/images/notData.png" alt="notData" />
114
+ <img src="../../assets/images/notData.png" :alt="t('common.message.no_data')" />
111
115
  <div>{{ t("common.message.no_data") }}</div>
112
116
  </slot>
113
117
  </div>
@@ -129,7 +133,7 @@
129
133
 
130
134
  <script setup lang="ts" name="ProTable">
131
135
  import { ref, watch, provide, onMounted, unref, computed, nextTick, useAttrs, useSlots, isProxy, markRaw, toRaw } from "vue";
132
- import { ElTable } from "element-plus";
136
+ import { ElTable, type TableInstance } from "element-plus";
133
137
  import { useTable } from "../../hooks/useTable";
134
138
  import { useSelection } from "../../hooks/useSelection";
135
139
  import {
@@ -167,7 +171,7 @@ const props = withDefaults(defineProps<ProTableProps>(), {
167
171
  });
168
172
 
169
173
  // table 实例
170
- const tableRef = ref<InstanceType<typeof ElTable>>();
174
+ const tableRef = ref<TableInstance>();
171
175
  const attrs = useAttrs();
172
176
  const slots = useSlots();
173
177
 
@@ -0,0 +1,56 @@
1
+ <template>
2
+ <div class="core-rich-text-preview" v-html="sanitizedHtml" />
3
+ </template>
4
+
5
+ <script setup lang="ts">
6
+ import { computed } from "vue";
7
+ import DOMPurify from "dompurify";
8
+ import { formatSrc } from "../../utils/utils";
9
+
10
+ defineOptions({
11
+ name: "CoreRichTextPreview"
12
+ });
13
+
14
+ /** 富文本预览组件属性。 */
15
+ interface RichTextPreviewProps {
16
+ /** 待展示的 HTML 内容。 */
17
+ modelValue: string;
18
+ }
19
+
20
+ const props = defineProps<RichTextPreviewProps>();
21
+ const sanitizedHtml = computed(() => {
22
+ const sanitized = DOMPurify.sanitize(props.modelValue, { USE_PROFILES: { html: true } });
23
+ if (typeof DOMParser === "undefined") return sanitized;
24
+
25
+ const document = new DOMParser().parseFromString(sanitized, "text/html");
26
+ document.body.querySelectorAll<HTMLElement>("img[src], video[src]").forEach(element => {
27
+ const src = element.getAttribute("src");
28
+ if (src) element.setAttribute("src", formatSrc(src));
29
+ });
30
+ return document.body.innerHTML;
31
+ });
32
+ </script>
33
+
34
+ <style scoped lang="scss">
35
+ .core-rich-text-preview {
36
+ line-height: 1.7;
37
+ overflow-wrap: anywhere;
38
+ word-break: break-word;
39
+ }
40
+
41
+ .core-rich-text-preview :deep(p) {
42
+ margin: 0 0 8px;
43
+ }
44
+
45
+ .core-rich-text-preview :deep(ul),
46
+ .core-rich-text-preview :deep(ol) {
47
+ margin: 0 0 8px;
48
+ padding-left: 24px;
49
+ }
50
+
51
+ .core-rich-text-preview :deep(img),
52
+ .core-rich-text-preview :deep(video) {
53
+ max-width: 100%;
54
+ height: auto;
55
+ }
56
+ </style>
@@ -13,7 +13,7 @@
13
13
  <el-button :icon="customIcons[iconValue]" />
14
14
  </template>
15
15
  </el-input>
16
- <el-dialog v-model="dialogVisible" :title="placeholder" top="50px" width="760px">
16
+ <ProDialog v-model="dialogVisible" :title="placeholder" top="50px" width="760px" :show-footer="false">
17
17
  <el-input v-model="inputValue" :placeholder="t('core.icon.search')" size="large" :prefix-icon="Icons.Search" />
18
18
  <el-scrollbar v-if="Object.keys(iconsList).length">
19
19
  <div class="icon-list">
@@ -24,13 +24,14 @@
24
24
  </div>
25
25
  </el-scrollbar>
26
26
  <el-empty v-else :description="t('core.icon.empty')" />
27
- </el-dialog>
27
+ </ProDialog>
28
28
  </div>
29
29
  </template>
30
30
 
31
31
  <script setup lang="ts" name="SelectIcon">
32
32
  import { ref, computed, watch } from "vue";
33
33
  import * as Icons from "@element-plus/icons-vue";
34
+ import ProDialog from "../../components/Dialog/ProDialog.vue";
34
35
  import { useLocaleStore } from "../../locales";
35
36
 
36
37
  const { t } = useLocaleStore();
@@ -38,7 +38,7 @@ import { computed, inject } from "vue";
38
38
  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
- import { defFileService } from "../../api/base/file";
41
+ import { defFileService } from "../../api/base/v1/file";
42
42
  import type { FileInfo } from "../../rpc/base/v1/file";
43
43
  import { useLocaleStore } from "../../locales";
44
44
 
@@ -171,7 +171,7 @@ async function handleDownload() {
171
171
  margin-top: 12px;
172
172
  background: var(--el-fill-color-light);
173
173
  border: 1px solid var(--el-border-color-light);
174
- border-radius: 8px;
174
+ border-radius: var(--admin-page-radius);
175
175
  }
176
176
  .file-card__main {
177
177
  display: flex;
@@ -44,7 +44,7 @@ import { computed, inject, ref, watch } from "vue";
44
44
  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
- import { defFileService } from "../../api/base/file";
47
+ import { defFileService } from "../../api/base/v1/file";
48
48
  import type { FileInfo } from "../../rpc/base/v1/file";
49
49
  import { useLocaleStore } from "../../locales";
50
50
 
@@ -206,7 +206,7 @@ async function handleDownload(file: UploadUserFile) {
206
206
  padding: 12px 14px;
207
207
  background: var(--el-fill-color-light);
208
208
  border: 1px solid var(--el-border-color-light);
209
- border-radius: 8px;
209
+ border-radius: var(--admin-page-radius);
210
210
  }
211
211
  .file-card__main {
212
212
  display: flex;
@@ -15,7 +15,7 @@
15
15
  :accept="fileType.join(',')"
16
16
  >
17
17
  <template v-if="imageUrl">
18
- <img :src="imageUrl" class="upload-image" />
18
+ <img :src="displayImageUrl" class="upload-image" />
19
19
  <div class="upload-handle" @click.stop>
20
20
  <div v-if="!self_disabled" class="handle-icon" @click="editImg">
21
21
  <el-icon><Edit /></el-icon>
@@ -43,14 +43,15 @@
43
43
  <div class="el-upload__tip">
44
44
  <slot name="tip"></slot>
45
45
  </div>
46
- <el-image-viewer v-if="imgViewVisible" :url-list="[imageUrl]" @close="imgViewVisible = false" />
46
+ <el-image-viewer v-if="imgViewVisible" :url-list="[displayImageUrl]" @close="imgViewVisible = false" />
47
47
  </div>
48
48
  </template>
49
49
 
50
50
  <script setup lang="ts" name="UploadImg">
51
51
  import { ref, computed, inject } from "vue";
52
52
  import { generateUUID } from "../../utils";
53
- import { defFileService } from "../../api/base/file";
53
+ import { formatSrc } from "../../utils/utils";
54
+ import { defFileService } from "../../api/base/v1/file";
54
55
  import { ElNotification, formContextKey, formItemContextKey } from "element-plus";
55
56
  import type { UploadProps, UploadRequestOptions } from "element-plus";
56
57
  import type { FileInfo } from "../../rpc/base/v1/file";
@@ -69,7 +70,7 @@ interface UploadFileProps {
69
70
  fileType?: File.ImageMimeType[]; // 图片类型限制 ==> 非必传(默认为 ["image/jpeg", "image/png", "image/gif"])
70
71
  height?: string; // 组件高度 ==> 非必传(默认为 150px)
71
72
  width?: string; // 组件宽度 ==> 非必传(默认为 150px)
72
- borderRadius?: string; // 组件边框圆角 ==> 非必传(默认为 8px)
73
+ borderRadius?: string; // 组件边框圆角 ==> 非必传(默认为全局圆角)
73
74
  }
74
75
 
75
76
  // 接受父组件参数
@@ -82,7 +83,7 @@ const props = withDefaults(defineProps<UploadFileProps>(), {
82
83
  fileType: () => ["image/jpeg", "image/png", "image/gif"],
83
84
  height: "150px",
84
85
  width: "150px",
85
- borderRadius: "8px"
86
+ borderRadius: "var(--admin-page-radius)"
86
87
  });
87
88
 
88
89
  // 生成组件唯一id
@@ -98,6 +99,7 @@ const formItemContext = inject(formItemContextKey, void 0);
98
99
  const self_disabled = computed(() => {
99
100
  return props.disabled || formContext?.disabled;
100
101
  });
102
+ const displayImageUrl = computed(() => formatSrc(props.imageUrl));
101
103
 
102
104
  /**
103
105
  * @description 图片上传
@@ -23,7 +23,7 @@
23
23
  </slot>
24
24
  </div>
25
25
  <template #file="{ file }">
26
- <img :src="file.url" class="upload-image" />
26
+ <img :src="formatSrc(file.url ?? '')" class="upload-image" />
27
27
  <div class="upload-handle" @click.stop>
28
28
  <div class="handle-icon" @click="handlePictureCardPreview(file)">
29
29
  <el-icon><ZoomIn /></el-icon>
@@ -46,7 +46,8 @@
46
46
  <script setup lang="ts" name="UploadImgs">
47
47
  import { ref, computed, inject, watch } from "vue";
48
48
  import { Plus } from "@element-plus/icons-vue";
49
- import { defFileService } from "../../api/base/file";
49
+ import { defFileService } from "../../api/base/v1/file";
50
+ import { formatSrc } from "../../utils/utils";
50
51
  import type { UploadProps, UploadFile, UploadUserFile, UploadRequestOptions } from "element-plus";
51
52
  import { ElNotification, formContextKey, formItemContextKey } from "element-plus";
52
53
  import type { FileInfo } from "../../rpc/base/v1/file";
@@ -66,7 +67,7 @@ interface UploadFileProps {
66
67
  fileType?: File.ImageMimeType[]; // 图片类型限制 ==> 非必传(默认为 ["image/jpeg", "image/png", "image/gif"])
67
68
  height?: string; // 组件高度 ==> 非必传(默认为 150px)
68
69
  width?: string; // 组件宽度 ==> 非必传(默认为 150px)
69
- borderRadius?: string; // 组件边框圆角 ==> 非必传(默认为 8px)
70
+ borderRadius?: string; // 组件边框圆角 ==> 非必传(默认为全局圆角)
70
71
  }
71
72
 
72
73
  const props = withDefaults(defineProps<UploadFileProps>(), {
@@ -79,7 +80,7 @@ const props = withDefaults(defineProps<UploadFileProps>(), {
79
80
  fileType: () => ["image/jpeg", "image/png", "image/gif"],
80
81
  height: "150px",
81
82
  width: "150px",
82
- borderRadius: "8px"
83
+ borderRadius: "var(--admin-page-radius)"
83
84
  });
84
85
 
85
86
  // 获取 el-form 组件上下文
@@ -200,7 +201,7 @@ const handleExceed = () => {
200
201
  const viewImageUrl = ref("");
201
202
  const imgViewVisible = ref(false);
202
203
  const handlePictureCardPreview: UploadProps["onPreview"] = file => {
203
- viewImageUrl.value = file.url!;
204
+ viewImageUrl.value = formatSrc(file.url ?? "");
204
205
  imgViewVisible.value = true;
205
206
  };
206
207
  </script>
@@ -17,7 +17,8 @@
17
17
  import { nextTick, computed, inject, shallowRef, onBeforeUnmount } from "vue";
18
18
  import { IToolbarConfig, IEditorConfig } from "@wangeditor/editor";
19
19
  import { Editor, Toolbar } from "@wangeditor/editor-for-vue";
20
- import { defFileService } from "../../api/base/file";
20
+ import { defFileService } from "../../api/base/v1/file";
21
+ import { normalizeRichTextMediaPaths, normalizeStaticAssetPath } from "../../utils/utils";
21
22
  import "@wangeditor/editor/dist/css/style.css";
22
23
  import { formContextKey, formItemContextKey } from "element-plus";
23
24
  import { useLocaleStore } from "../../locales";
@@ -41,6 +42,7 @@ interface RichEditorProps {
41
42
  mode?: "default" | "simple"; // 富文本模式 ==> 非必传(默认为 default)
42
43
  hideToolBar?: boolean; // 是否隐藏工具栏 ==> 非必传(默认为false)
43
44
  disabled?: boolean; // 是否禁用编辑器 ==> 非必传(默认为false)
45
+ uploadType?: string; // 富文本上传业务类型 ==> 非必传(默认为 content)
44
46
  }
45
47
  const props = withDefaults(defineProps<RichEditorProps>(), {
46
48
  toolbarConfig: () => {
@@ -56,7 +58,8 @@ const props = withDefaults(defineProps<RichEditorProps>(), {
56
58
  height: "500px",
57
59
  mode: "default",
58
60
  hideToolBar: false,
59
- disabled: false
61
+ disabled: false,
62
+ uploadType: "content"
60
63
  });
61
64
  const editorMenuConfig = props.editorConfig.MENU_CONF ?? {};
62
65
  const resolvedEditorConfig = computed(() => ({
@@ -84,7 +87,7 @@ const emit = defineEmits<{
84
87
  }>();
85
88
  const valueHtml = computed({
86
89
  get() {
87
- return props.value;
90
+ return normalizeRichTextMediaPaths(props.value);
88
91
  },
89
92
  set(val: string) {
90
93
  // 防止富文本内容为空时,校验失败
@@ -103,8 +106,8 @@ editorMenuConfig["uploadImage"] = {
103
106
  async customUpload(file: File, insertFn: InsertFnTypeImg) {
104
107
  if (!uploadImgValidate(file)) return;
105
108
  try {
106
- const data = await defFileService.UploadFile(file, "image");
107
- insertFn(data.url);
109
+ const data = await defFileService.UploadFile(file, props.uploadType);
110
+ insertFn(normalizeStaticAssetPath(data.url));
108
111
  } catch (error) {
109
112
  console.log(error);
110
113
  }
@@ -127,8 +130,8 @@ editorMenuConfig["uploadVideo"] = {
127
130
  async customUpload(file: File, insertFn: InsertFnTypeVideo) {
128
131
  if (!uploadVideoValidate(file)) return;
129
132
  try {
130
- const data = await defFileService.UploadFile(file, "video");
131
- insertFn(data.url);
133
+ const data = await defFileService.UploadFile(file, props.uploadType);
134
+ insertFn(normalizeStaticAssetPath(data.url));
132
135
  } catch (error) {
133
136
  console.log(error);
134
137
  }
@@ -1,7 +1,7 @@
1
1
  // ? 全局默认配置项
2
2
 
3
3
  // 首页地址(默认)
4
- export const HOME_URL: string = "/dashboard/workspace";
4
+ export const HOME_URL: string = "/dashboard";
5
5
 
6
6
  // 登录页地址(默认)
7
7
  export const LOGIN_URL: string = "/login";
@@ -0,0 +1,8 @@
1
+ import pinia from "./stores";
2
+ import { useConfigStore } from "./stores/modules/config";
3
+
4
+ /** setAdminDocumentTitle 使用后端站点名称更新管理端浏览器标题。 */
5
+ export function setAdminDocumentTitle(pageTitle = "") {
6
+ const appTitle = useConfigStore(pinia).display.sysName || import.meta.env.VITE_GLOB_APP_TITLE;
7
+ document.title = pageTitle ? `${pageTitle} - ${appTitle}` : appTitle;
8
+ }
@@ -1 +1 @@
1
- export { formatJson, formatSrc } from "./utils/utils";
1
+ export { formatJson, formatSrc, normalizeRichTextMediaPaths, normalizeStaticAssetPath } from "./utils/utils";