@liujitcn/kratos-admin-core 0.0.97 → 0.1.0
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 +6 -0
- package/dist/package/declarations/src/components/CodeEditor/index.vue.d.ts +22 -0
- package/dist/package/declarations/src/components/ProTable/columns.d.ts +3 -0
- package/dist/package/declarations/src/stores/modules/dict.d.ts +2 -2
- package/dist/package/declarations/src/views/demo/demo-data.d.ts +13 -0
- package/dist/package/src/components/CodeEditor/index.vue +92 -0
- package/dist/package/src/components/Dict/DictLabel.vue +7 -24
- package/dist/package/src/components/Dict/index.vue +21 -23
- package/dist/package/src/components/ProTable/columns.ts +27 -0
- package/dist/package/src/components/ProTable/index.vue +53 -20
- package/dist/package/src/components/WangEditor/index.vue +15 -4
- package/dist/package/src/rpc/base/v1/config.ts +1 -1
- package/dist/package/src/rpc/base/v1/file.ts +1 -1
- package/dist/package/src/rpc/base/v1/language.ts +1 -1
- package/dist/package/src/rpc/base/v1/login.ts +1 -1
- package/dist/package/src/rpc/base/v1/mfa.ts +1 -1
- package/dist/package/src/rpc/base/v1/oauth.ts +1 -1
- package/dist/package/src/rpc/base/v1/oauth_client.ts +1 -1
- package/dist/package/src/rpc/base/v1/secret_crypto.ts +1 -1
- package/dist/package/src/rpc/common/v1/common.ts +1 -1
- package/dist/package/src/rpc/common/v1/enum.ts +1 -1
- package/dist/package/src/rpc/common/v1/types.ts +1 -1
- package/dist/package/src/rpc/google/protobuf/empty.ts +1 -1
- package/dist/package/src/rpc/google/protobuf/struct.ts +1 -1
- package/dist/package/src/rpc/google/protobuf/wrappers.ts +1 -1
- package/dist/package/src/rpc/system/admin/v1/auth.ts +1 -1
- package/dist/package/src/rpc/system/admin/v1/base_dict.ts +1 -1
- package/dist/package/src/rpc/system/admin/v1/base_i18n.ts +1 -1
- package/dist/package/src/rpc/system/admin/v1/base_tenant.ts +1 -1
- package/dist/package/src/rpc/system/admin/v1/common.ts +1 -1
- package/dist/package/src/rpc/system/common/v1/common.ts +1 -1
- package/dist/package/src/stores/modules/dict.ts +1 -2
- package/dist/package/src/utils/request.ts +3 -2
- package/dist/package/src/views/demo/demo-data.ts +21 -0
- package/dist/package/src/views/demo/table/index.vue +4 -3
- package/package.json +7 -2
package/README.md
CHANGED
|
@@ -187,6 +187,12 @@ pnpm --filter @liujitcn/kratos-admin-core pack
|
|
|
187
187
|
|
|
188
188
|
时间列的对齐方式由页面通过 `align` 显式配置,ProTable 不按时间字段名称判断;修改时间列使用 `align: "center"` 居中显示。
|
|
189
189
|
|
|
190
|
+
管理端切换语言后,ProTable 同步响应式列配置中的表头、搜索项、行操作和列设置文案,保留当前筛选、分页、勾选及用户调整的列显隐、排序设置;异步枚举在公共字典刷新后重新加载,过期请求不会覆盖最新结果。页面文案应在模板或 `computed` 中调用 `t()`,避免将翻译结果保存为固定字符串。
|
|
191
|
+
|
|
192
|
+
Dict 和 DictLabel 直接响应字典 Store 的文案变化。字典刷新成功后整体替换缓存,刷新期间保留已加载选项和表单值,无需重新挂载页面。
|
|
193
|
+
|
|
194
|
+
纯表格 demo 的“所在城市”通过 `search.enum` 提供浙江、广东、四川的省市级联选项,选择城市后可筛选对应项目,清空或重置后恢复全部项目。
|
|
195
|
+
|
|
190
196
|
ProTable 的 `cellType: "actions"` 操作列按当前行的 `hidden` 过滤按钮,优先展示使用 `common.action.edit`、`common.action.delete` 文案的编辑、删除操作;可见按钮超过三项时,其余操作收入“更多”菜单。未配置或无权限的编辑、删除不会补充显示,菜单保留图标、禁用状态和点击参数;自定义 render 或插槽不参与折叠。
|
|
191
197
|
|
|
192
198
|
操作列默认固定在右侧。配置式操作列(`cellType: "actions"`)根据当前页实际展示按钮和表头的最大宽度自适应,计入翻译文案、图标和间距,随语言、字体和行权限变化重新测量,最小宽度为 `80px`;折叠菜单内的按钮不参与测量。自定义 render 或插槽操作列仍默认 `160px`。页面可通过 `width` 或 `minWidth` 覆盖自动宽度,通过 `fixed: "left"` 调整固定位置,或通过 `fixed: false` 取消固定。
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** 代码编辑器属性。 */
|
|
2
|
+
interface CodeEditorProps {
|
|
3
|
+
/** 编辑器源码。 */
|
|
4
|
+
modelValue: string;
|
|
5
|
+
/** Monaco 语言标识。 */
|
|
6
|
+
language?: string;
|
|
7
|
+
/** 编辑器高度。 */
|
|
8
|
+
height?: string;
|
|
9
|
+
/** 是否只读。 */
|
|
10
|
+
readonly?: boolean;
|
|
11
|
+
}
|
|
12
|
+
declare const __VLS_export: import("vue").DefineComponent<CodeEditorProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
13
|
+
"update:modelValue": (value: string) => any;
|
|
14
|
+
}, string, import("vue").PublicProps, Readonly<CodeEditorProps> & Readonly<{
|
|
15
|
+
"onUpdate:modelValue"?: ((value: string) => any) | undefined;
|
|
16
|
+
}>, {
|
|
17
|
+
readonly: boolean;
|
|
18
|
+
height: string;
|
|
19
|
+
language: string;
|
|
20
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
21
|
+
declare const _default: typeof __VLS_export;
|
|
22
|
+
export default _default;
|
|
@@ -10,7 +10,7 @@ export declare const useDictStore: import("pinia").StoreDefinition<"admin-dict",
|
|
|
10
10
|
ensureDictionary(dictCode: string): Promise<OptionBaseDictResponse_BaseDictItem[]>;
|
|
11
11
|
/** 清空字典缓存 */
|
|
12
12
|
clearDictionaryCache(): void;
|
|
13
|
-
/**
|
|
13
|
+
/** 加载成功后整体替换字典缓存,刷新期间保留当前文案和表单选中值。 */
|
|
14
14
|
updateDictionaryCache(): Promise<void>;
|
|
15
15
|
}>;
|
|
16
16
|
/**
|
|
@@ -27,6 +27,6 @@ export declare function useDictStoreHook(): import("pinia").Store<"admin-dict",
|
|
|
27
27
|
ensureDictionary(dictCode: string): Promise<OptionBaseDictResponse_BaseDictItem[]>;
|
|
28
28
|
/** 清空字典缓存 */
|
|
29
29
|
clearDictionaryCache(): void;
|
|
30
|
-
/**
|
|
30
|
+
/** 加载成功后整体替换字典缓存,刷新期间保留当前文案和表单选中值。 */
|
|
31
31
|
updateDictionaryCache(): Promise<void>;
|
|
32
32
|
}>;
|
|
@@ -139,6 +139,19 @@ export declare const demoRegionTree: {
|
|
|
139
139
|
label: string;
|
|
140
140
|
}[];
|
|
141
141
|
}[];
|
|
142
|
+
/** 团队树静态数据:与城市级联数据分离,供纯表格团队筛选使用。 */
|
|
143
|
+
export declare const demoOwnerGroupTree: ({
|
|
144
|
+
value: string;
|
|
145
|
+
label: string;
|
|
146
|
+
children: {
|
|
147
|
+
value: string;
|
|
148
|
+
label: string;
|
|
149
|
+
}[];
|
|
150
|
+
} | {
|
|
151
|
+
value: string;
|
|
152
|
+
label: string;
|
|
153
|
+
children?: undefined;
|
|
154
|
+
})[];
|
|
142
155
|
/** 项目静态数据:25 条覆盖分页、多级表头、进度条、标签列等展示场景。 */
|
|
143
156
|
export declare const demoProjects: DemoProject[];
|
|
144
157
|
/** 目录树静态数据:三层嵌套演示树形表格的展开、折叠与子级操作。 */
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div ref="editorHost" class="code-editor" :style="{ height }" />
|
|
3
|
+
</template>
|
|
4
|
+
|
|
5
|
+
<script setup lang="ts">
|
|
6
|
+
import { onBeforeUnmount, onMounted, ref, watch } from "vue";
|
|
7
|
+
import * as monaco from "monaco-editor";
|
|
8
|
+
import editorWorker from "monaco-editor/esm/vs/editor/editor.worker?worker";
|
|
9
|
+
|
|
10
|
+
/** 代码编辑器属性。 */
|
|
11
|
+
interface CodeEditorProps {
|
|
12
|
+
/** 编辑器源码。 */
|
|
13
|
+
modelValue: string;
|
|
14
|
+
/** Monaco 语言标识。 */
|
|
15
|
+
language?: string;
|
|
16
|
+
/** 编辑器高度。 */
|
|
17
|
+
height?: string;
|
|
18
|
+
/** 是否只读。 */
|
|
19
|
+
readonly?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const props = withDefaults(defineProps<CodeEditorProps>(), {
|
|
23
|
+
language: "javascript",
|
|
24
|
+
height: "360px",
|
|
25
|
+
readonly: false
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const emit = defineEmits<{ "update:modelValue": [value: string] }>();
|
|
29
|
+
const editorHost = ref<HTMLElement>();
|
|
30
|
+
let editor: monaco.editor.IStandaloneCodeEditor | undefined;
|
|
31
|
+
let model: monaco.editor.ITextModel | undefined;
|
|
32
|
+
|
|
33
|
+
function createEditor() {
|
|
34
|
+
if (!editorHost.value) return;
|
|
35
|
+
self.MonacoEnvironment = {
|
|
36
|
+
getWorker() {
|
|
37
|
+
return new editorWorker();
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
model = monaco.editor.createModel(props.modelValue, props.language);
|
|
41
|
+
editor = monaco.editor.create(editorHost.value, {
|
|
42
|
+
model,
|
|
43
|
+
automaticLayout: true,
|
|
44
|
+
minimap: { enabled: false },
|
|
45
|
+
fontSize: 13,
|
|
46
|
+
tabSize: 2,
|
|
47
|
+
scrollBeyondLastLine: false,
|
|
48
|
+
wordWrap: "on",
|
|
49
|
+
readOnly: props.readonly,
|
|
50
|
+
padding: { top: 12, bottom: 12 }
|
|
51
|
+
});
|
|
52
|
+
editor.onDidChangeModelContent(() => {
|
|
53
|
+
emit("update:modelValue", editor?.getValue() ?? "");
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function disposeEditor() {
|
|
58
|
+
editor?.dispose();
|
|
59
|
+
model?.dispose();
|
|
60
|
+
editor = undefined;
|
|
61
|
+
model = undefined;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
onMounted(createEditor);
|
|
65
|
+
onBeforeUnmount(disposeEditor);
|
|
66
|
+
|
|
67
|
+
watch(
|
|
68
|
+
() => props.modelValue,
|
|
69
|
+
value => {
|
|
70
|
+
if (!editor || value === editor.getValue()) return;
|
|
71
|
+
editor.setValue(value);
|
|
72
|
+
}
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
watch(
|
|
76
|
+
() => props.language,
|
|
77
|
+
value => {
|
|
78
|
+
if (!model) return;
|
|
79
|
+
monaco.editor.setModelLanguage(model, value);
|
|
80
|
+
}
|
|
81
|
+
);
|
|
82
|
+
</script>
|
|
83
|
+
|
|
84
|
+
<style scoped>
|
|
85
|
+
.code-editor {
|
|
86
|
+
width: 100%;
|
|
87
|
+
min-height: 240px;
|
|
88
|
+
overflow: hidden;
|
|
89
|
+
border: 1px solid var(--el-border-color);
|
|
90
|
+
border-radius: var(--admin-page-radius);
|
|
91
|
+
}
|
|
92
|
+
</style>
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
</template>
|
|
5
5
|
|
|
6
6
|
<script setup lang="ts">
|
|
7
|
-
import {
|
|
7
|
+
import { computed, watch } from "vue";
|
|
8
8
|
import { useDictStore } from "../../stores/modules/dict";
|
|
9
9
|
|
|
10
10
|
/** Element Plus 标签支持的展示类型。 */
|
|
@@ -22,8 +22,9 @@ const props = withDefaults(defineProps<DictLabelProps>(), {
|
|
|
22
22
|
});
|
|
23
23
|
|
|
24
24
|
const dictStore = useDictStore();
|
|
25
|
-
const
|
|
26
|
-
const
|
|
25
|
+
const matchedItem = computed(() => dictStore.getDictionary(props.code).find(item => item.value == props.modelValue));
|
|
26
|
+
const label = computed(() => matchedItem.value?.label ?? "");
|
|
27
|
+
const tagType = computed(() => normalizeTagType(matchedItem.value?.tag_type));
|
|
27
28
|
|
|
28
29
|
/**
|
|
29
30
|
* 过滤后端返回的 tag 类型,确保只传递 Element Plus 支持的枚举值。
|
|
@@ -34,28 +35,10 @@ function normalizeTagType(rawTagType?: string): TagType | undefined {
|
|
|
34
35
|
return supportedTagTypes.find(tag => tag === rawTagType);
|
|
35
36
|
}
|
|
36
37
|
|
|
37
|
-
/**
|
|
38
|
-
* 根据字典值刷新标签文本和标签类型。
|
|
39
|
-
*/
|
|
40
|
-
async function refreshLabelAndTag() {
|
|
41
|
-
if (!props.code) {
|
|
42
|
-
label.value = "";
|
|
43
|
-
tagType.value = undefined;
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// 字典缓存可能来自持久化旧数据,按当前编码兜底刷新一次。
|
|
48
|
-
const dictList = await dictStore.ensureDictionary(props.code);
|
|
49
|
-
|
|
50
|
-
const matchedItem = dictList.find(dictItem => dictItem.value == props.modelValue);
|
|
51
|
-
label.value = matchedItem?.label ?? "";
|
|
52
|
-
tagType.value = normalizeTagType(matchedItem?.tag_type);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
38
|
watch(
|
|
56
|
-
() =>
|
|
57
|
-
async
|
|
58
|
-
await
|
|
39
|
+
() => props.code,
|
|
40
|
+
async code => {
|
|
41
|
+
if (code) await dictStore.ensureDictionary(code);
|
|
59
42
|
},
|
|
60
43
|
{ immediate: true }
|
|
61
44
|
);
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
|
|
39
39
|
<script setup lang="ts">
|
|
40
40
|
import type { CSSProperties } from "vue";
|
|
41
|
-
import { ref, watch } from "vue";
|
|
41
|
+
import { computed, ref, watch } from "vue";
|
|
42
42
|
import { useDictStore } from "../../stores/modules/dict";
|
|
43
43
|
import type { OptionBaseDictResponse_BaseDictItem } from "../../rpc/system/admin/v1/base_dict";
|
|
44
44
|
import { useLocaleStore } from "../../locales";
|
|
@@ -80,7 +80,12 @@ const emit = defineEmits<{
|
|
|
80
80
|
}>();
|
|
81
81
|
|
|
82
82
|
const dictStore = useDictStore();
|
|
83
|
-
const options =
|
|
83
|
+
const options = computed(() =>
|
|
84
|
+
dictStore.getDictionary(props.code).map(dictItem => ({
|
|
85
|
+
label: dictItem.label,
|
|
86
|
+
value: convertDictValue(dictItem)
|
|
87
|
+
}))
|
|
88
|
+
);
|
|
84
89
|
const selectedSingleValue = ref<DictValue | undefined>();
|
|
85
90
|
const selectedMultiValue = ref<DictValue[]>([]);
|
|
86
91
|
const optionsLoaded = ref(false);
|
|
@@ -104,21 +109,6 @@ function convertDictValue(dictItem: OptionBaseDictResponse_BaseDictItem): DictVa
|
|
|
104
109
|
return dictItem.value;
|
|
105
110
|
}
|
|
106
111
|
|
|
107
|
-
/**
|
|
108
|
-
* 加载当前字典选项,若本地无缓存则自动拉取。
|
|
109
|
-
*/
|
|
110
|
-
async function loadOptions() {
|
|
111
|
-
// 按当前字典编码兜底刷新,避免持久化旧缓存里缺少该字典导致下拉无数据。
|
|
112
|
-
optionsLoaded.value = false;
|
|
113
|
-
const dictList = await dictStore.ensureDictionary(props.code);
|
|
114
|
-
|
|
115
|
-
options.value = dictList.map(dictItem => ({
|
|
116
|
-
label: dictItem.label,
|
|
117
|
-
value: convertDictValue(dictItem)
|
|
118
|
-
}));
|
|
119
|
-
optionsLoaded.value = true;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
112
|
/**
|
|
123
113
|
* 校验当前值是否仍在可选项内,避免字典切换后残留无效值。
|
|
124
114
|
*/
|
|
@@ -131,10 +121,7 @@ function ensureSelectedValueValid() {
|
|
|
131
121
|
emit("update:modelValue", normalizedValues);
|
|
132
122
|
return;
|
|
133
123
|
}
|
|
134
|
-
const normalizedValue = normalizeDictValue(
|
|
135
|
-
selectedSingleValue.value,
|
|
136
|
-
availableValues
|
|
137
|
-
);
|
|
124
|
+
const normalizedValue = normalizeDictValue(selectedSingleValue.value, availableValues);
|
|
138
125
|
if (normalizedValue === selectedSingleValue.value) return;
|
|
139
126
|
selectedSingleValue.value = normalizedValue;
|
|
140
127
|
emit("update:modelValue", normalizedValue);
|
|
@@ -165,10 +152,21 @@ watch(
|
|
|
165
152
|
|
|
166
153
|
watch(
|
|
167
154
|
() => [props.code, props.codeType],
|
|
168
|
-
async () => {
|
|
169
|
-
|
|
155
|
+
async (_, __, onCleanup) => {
|
|
156
|
+
let active = true;
|
|
157
|
+
onCleanup(() => {
|
|
158
|
+
active = false;
|
|
159
|
+
});
|
|
160
|
+
optionsLoaded.value = false;
|
|
161
|
+
await dictStore.ensureDictionary(props.code);
|
|
162
|
+
if (!active) return;
|
|
163
|
+
optionsLoaded.value = true;
|
|
170
164
|
ensureSelectedValueValid();
|
|
171
165
|
},
|
|
172
166
|
{ immediate: true }
|
|
173
167
|
);
|
|
168
|
+
|
|
169
|
+
watch(options, () => {
|
|
170
|
+
if (optionsLoaded.value) ensureSelectedValueValid();
|
|
171
|
+
});
|
|
174
172
|
</script>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { unref } from "vue";
|
|
2
|
+
import type { ColumnProps } from "./interface";
|
|
3
|
+
|
|
4
|
+
/** 同步最新列配置,保留未被页面配置覆盖的用户显隐和排序设置。 */
|
|
5
|
+
export function syncTableColumns(
|
|
6
|
+
columns: ColumnProps[],
|
|
7
|
+
current: ColumnProps[] = [],
|
|
8
|
+
previous: ColumnProps[] = []
|
|
9
|
+
): ColumnProps[] {
|
|
10
|
+
const currentMap = new Map(current.map((column, index) => [column.prop ?? column.type ?? index, column]));
|
|
11
|
+
const previousMap = new Map(previous.map((column, index) => [column.prop ?? column.type ?? index, column]));
|
|
12
|
+
return columns.map((column, index) => {
|
|
13
|
+
const key = column.prop ?? column.type ?? index;
|
|
14
|
+
const existing = currentMap.get(key);
|
|
15
|
+
const source = previousMap.get(key);
|
|
16
|
+
const isShow = unref(column.isShow) ?? true;
|
|
17
|
+
return {
|
|
18
|
+
...column,
|
|
19
|
+
isShow: existing && source && isShow === (unref(source.isShow) ?? true) ? existing.isShow : isShow,
|
|
20
|
+
sortable: existing && source && column.sortable === source.sortable ? existing.sortable : column.sortable,
|
|
21
|
+
isSetting: column.isSetting ?? true,
|
|
22
|
+
isFilterEnum: column.isFilterEnum ?? true,
|
|
23
|
+
...(column.search ? { search: { ...column.search } } : {}),
|
|
24
|
+
...(column._children ? { _children: syncTableColumns(column._children, existing?._children, source?._children) } : {})
|
|
25
|
+
};
|
|
26
|
+
});
|
|
27
|
+
}
|
|
@@ -128,11 +128,25 @@
|
|
|
128
128
|
</slot>
|
|
129
129
|
</div>
|
|
130
130
|
<!-- 列设置 -->
|
|
131
|
-
<ColSetting v-if="toolButton" ref="colRef"
|
|
131
|
+
<ColSetting v-if="toolButton" ref="colRef" :col-setting="colSetting" />
|
|
132
132
|
</template>
|
|
133
133
|
|
|
134
134
|
<script setup lang="ts" name="ProTable">
|
|
135
|
-
import {
|
|
135
|
+
import {
|
|
136
|
+
ref,
|
|
137
|
+
watch,
|
|
138
|
+
provide,
|
|
139
|
+
onMounted,
|
|
140
|
+
onScopeDispose,
|
|
141
|
+
unref,
|
|
142
|
+
computed,
|
|
143
|
+
nextTick,
|
|
144
|
+
useAttrs,
|
|
145
|
+
useSlots,
|
|
146
|
+
isProxy,
|
|
147
|
+
markRaw,
|
|
148
|
+
toRaw
|
|
149
|
+
} from "vue";
|
|
136
150
|
import { ElTable, type TableInstance } from "element-plus";
|
|
137
151
|
import { useTable } from "../../hooks/useTable";
|
|
138
152
|
import { useSelection } from "../../hooks/useSelection";
|
|
@@ -151,8 +165,9 @@ import SearchForm from "../../components/SearchForm/index.vue";
|
|
|
151
165
|
import Pagination from "./components/Pagination.vue";
|
|
152
166
|
import ColSetting from "./components/ColSetting.vue";
|
|
153
167
|
import TableColumn from "./components/TableColumn.vue";
|
|
168
|
+
import { syncTableColumns } from "./columns";
|
|
154
169
|
import Sortable from "sortablejs";
|
|
155
|
-
import { useLocaleStore } from "../../locales";
|
|
170
|
+
import { registerLocaleChangeHandler, useLocaleStore } from "../../locales";
|
|
156
171
|
|
|
157
172
|
const { t } = useLocaleStore();
|
|
158
173
|
|
|
@@ -313,14 +328,24 @@ watch(
|
|
|
313
328
|
// 监听页面 initParam 改化,重新获取表格数据
|
|
314
329
|
watch(() => props.initParam, getTableList, { deep: true });
|
|
315
330
|
|
|
316
|
-
//
|
|
317
|
-
const tableColumns = ref<ColumnProps[]>(
|
|
331
|
+
// 保留独立的列设置和源配置快照,语言重算时同步新文案并保留用户设置。
|
|
332
|
+
const tableColumns = ref<ColumnProps[]>([]);
|
|
333
|
+
let previousColumns: ColumnProps[] = [];
|
|
334
|
+
watch(
|
|
335
|
+
() => props.columns,
|
|
336
|
+
columns => {
|
|
337
|
+
tableColumns.value = syncTableColumns(columns, tableColumns.value, previousColumns);
|
|
338
|
+
previousColumns = syncTableColumns(columns);
|
|
339
|
+
},
|
|
340
|
+
{ deep: true, immediate: true }
|
|
341
|
+
);
|
|
318
342
|
|
|
319
343
|
// 扁平化 columns
|
|
320
344
|
const flatColumns = computed(() => flatColumnsFunc(tableColumns.value as any) as ColumnProps[]);
|
|
321
345
|
|
|
322
346
|
// 定义 enumMap 存储 enum 值(避免异步请求无法格式化单元格内容 || 无法填充搜索下拉选择)
|
|
323
347
|
const enumMap = ref(new Map<string, { [key: string]: any }[]>());
|
|
348
|
+
const enumRequests = new Map<string, symbol>();
|
|
324
349
|
|
|
325
350
|
/** 更新枚举缓存时替换 Map 引用,确保搜索下拉能响应异步枚举结果。 */
|
|
326
351
|
const setEnumMapValue = (prop: string | undefined, data: { [key: string]: any }[]) => {
|
|
@@ -346,24 +371,29 @@ const buildStatusEnum = (column: ColumnProps): EnumProps[] => {
|
|
|
346
371
|
const setEnumMap = async ({ prop, enum: enumValue }: ColumnProps) => {
|
|
347
372
|
if (!prop || !enumValue) return;
|
|
348
373
|
|
|
349
|
-
// 如果当前 enumMap 存在相同的值 return
|
|
350
|
-
if (enumMap.value.has(prop) && (typeof enumValue === "function" || enumMap.value.get(prop) === enumValue)) return;
|
|
351
|
-
|
|
352
374
|
// 当前 enum 为静态数据,则直接存储到 enumMap
|
|
353
375
|
if (typeof enumValue !== "function") {
|
|
354
|
-
|
|
376
|
+
enumRequests.delete(prop);
|
|
377
|
+
const data = unref(enumValue);
|
|
378
|
+
if (enumMap.value.get(prop) !== data) setEnumMapValue(prop, data);
|
|
355
379
|
return;
|
|
356
380
|
}
|
|
357
381
|
|
|
382
|
+
if (enumMap.value.has(prop)) return;
|
|
383
|
+
|
|
384
|
+
const request = Symbol(prop);
|
|
385
|
+
enumRequests.set(prop, request);
|
|
358
386
|
// 为了防止接口执行慢,而存储慢,导致重复请求,所以预先存储为[],接口返回后再二次存储
|
|
359
387
|
setEnumMapValue(prop, []);
|
|
360
388
|
|
|
361
389
|
// 当前 enum 为后台数据需要请求数据,则调用该请求接口,并存储到 enumMap
|
|
362
390
|
try {
|
|
363
391
|
const response = await enumValue();
|
|
392
|
+
if (enumRequests.get(prop) !== request) return;
|
|
364
393
|
const data = Array.isArray(response) ? response : (response?.data ?? response?.list ?? []);
|
|
365
394
|
setEnumMapValue(prop, data);
|
|
366
395
|
} catch {
|
|
396
|
+
if (enumRequests.get(prop) !== request) return;
|
|
367
397
|
// 请求层负责展示接口错误;删除占位缓存,允许列配置再次同步时重试。
|
|
368
398
|
const nextEnumMap = new Map(enumMap.value);
|
|
369
399
|
nextEnumMap.delete(prop);
|
|
@@ -377,6 +407,7 @@ const setEnumMap = async ({ prop, enum: enumValue }: ColumnProps) => {
|
|
|
377
407
|
const syncColumnEnumMap = async (column: ColumnProps) => {
|
|
378
408
|
const statusEnum = buildStatusEnum(column);
|
|
379
409
|
if (statusEnum.length && column.prop) {
|
|
410
|
+
enumRequests.delete(column.prop);
|
|
380
411
|
setEnumMapValue(column.prop, statusEnum);
|
|
381
412
|
return;
|
|
382
413
|
}
|
|
@@ -387,25 +418,25 @@ const syncColumnEnumMap = async (column: ColumnProps) => {
|
|
|
387
418
|
const refreshEnums = async () => {
|
|
388
419
|
const nextEnumMap = new Map(enumMap.value);
|
|
389
420
|
flatColumns.value.forEach(column => {
|
|
390
|
-
if (column.prop)
|
|
421
|
+
if (!column.prop) return;
|
|
422
|
+
nextEnumMap.delete(column.prop);
|
|
423
|
+
enumRequests.delete(column.prop);
|
|
391
424
|
});
|
|
392
425
|
enumMap.value = nextEnumMap;
|
|
393
426
|
await Promise.all(flatColumns.value.map(column => syncColumnEnumMap(column)));
|
|
394
427
|
};
|
|
395
428
|
|
|
429
|
+
// 公共运行时先刷新字典缓存,随后按当前语言重新加载当前表格的枚举。
|
|
430
|
+
onScopeDispose(registerLocaleChangeHandler(refreshEnums));
|
|
431
|
+
|
|
396
432
|
// 注入 enumMap
|
|
397
433
|
provide("enumMap", enumMap);
|
|
398
434
|
|
|
399
|
-
|
|
435
|
+
/** 扁平化多级列配置,供搜索项和枚举缓存使用。 */
|
|
400
436
|
const flatColumnsFunc = (columns: ColumnProps[], flatArr: ColumnProps[] = []) => {
|
|
401
437
|
columns.forEach(col => {
|
|
402
438
|
if (col._children?.length) flatArr.push(...flatColumnsFunc(col._children));
|
|
403
439
|
flatArr.push(col);
|
|
404
|
-
|
|
405
|
-
// column 添加默认 isShow && isSetting && isFilterEnum 属性值
|
|
406
|
-
col.isShow = col.isShow ?? true;
|
|
407
|
-
col.isSetting = col.isSetting ?? true;
|
|
408
|
-
col.isFilterEnum = col.isFilterEnum ?? true;
|
|
409
440
|
});
|
|
410
441
|
return flatArr.filter(item => !item._children?.length);
|
|
411
442
|
};
|
|
@@ -441,10 +472,12 @@ searchColumns.value?.forEach((column, index) => {
|
|
|
441
472
|
|
|
442
473
|
// 列设置 ==> 需要过滤掉不需要设置的列
|
|
443
474
|
const colRef = ref();
|
|
444
|
-
const colSetting =
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
475
|
+
const colSetting = computed<ColumnProps[]>(() =>
|
|
476
|
+
(tableColumns.value as ColumnProps[]).filter(item => {
|
|
477
|
+
const { type, prop, isSetting } = item;
|
|
478
|
+
return !columnTypes.includes(type!) && prop !== "operation" && isSetting;
|
|
479
|
+
})
|
|
480
|
+
);
|
|
448
481
|
/** 打开列设置弹窗。 */
|
|
449
482
|
const openColSetting = () => colRef.value.openColSetting();
|
|
450
483
|
|
|
@@ -86,14 +86,24 @@ const emit = defineEmits<{
|
|
|
86
86
|
"update:value": [value: string];
|
|
87
87
|
"check-validate": [];
|
|
88
88
|
}>();
|
|
89
|
+
|
|
90
|
+
/** 判断富文本 HTML 是否只包含空白节点。 */
|
|
91
|
+
const normalizeEditorValue = (value: string) => {
|
|
92
|
+
if (!value) return "";
|
|
93
|
+
if (/<(?:img|video|iframe)\b/i.test(value)) return value;
|
|
94
|
+
const text = value
|
|
95
|
+
.replace(/<[^>]*>/g, "")
|
|
96
|
+
.replace(/ | |​/gi, "")
|
|
97
|
+
.trim();
|
|
98
|
+
return text ? value : "";
|
|
99
|
+
};
|
|
89
100
|
const valueHtml = computed({
|
|
90
101
|
get() {
|
|
91
102
|
return normalizeRichTextMediaPaths(props.value);
|
|
92
103
|
},
|
|
93
104
|
set(val: string) {
|
|
94
|
-
//
|
|
95
|
-
|
|
96
|
-
emit("update:value", val);
|
|
105
|
+
// 以编辑器回写的 HTML 为准,避免 isEmpty() 在回写时序中误清空有效内容。
|
|
106
|
+
emit("update:value", normalizeEditorValue(val));
|
|
97
107
|
}
|
|
98
108
|
});
|
|
99
109
|
|
|
@@ -180,7 +190,8 @@ editorMenuConfig["uploadVideo"] = {
|
|
|
180
190
|
};
|
|
181
191
|
|
|
182
192
|
// 编辑框失去焦点时触发
|
|
183
|
-
const handleBlur = () => {
|
|
193
|
+
const handleBlur = async () => {
|
|
194
|
+
await nextTick();
|
|
184
195
|
formItemContext?.prop && formContext?.validateField([formItemContext.prop as string]);
|
|
185
196
|
};
|
|
186
197
|
|
|
@@ -48,9 +48,8 @@ export const useDictStore = defineStore("admin-dict", {
|
|
|
48
48
|
clearDictionaryCache() {
|
|
49
49
|
this.dictionary = {};
|
|
50
50
|
},
|
|
51
|
-
/**
|
|
51
|
+
/** 加载成功后整体替换字典缓存,刷新期间保留当前文案和表单选中值。 */
|
|
52
52
|
async updateDictionaryCache() {
|
|
53
|
-
this.clearDictionaryCache();
|
|
54
53
|
await this.loadDictionaries(true);
|
|
55
54
|
}
|
|
56
55
|
},
|
|
@@ -121,14 +121,15 @@ const service = axios.create({
|
|
|
121
121
|
baseURL: requestBaseURL,
|
|
122
122
|
timeout: 50000,
|
|
123
123
|
headers: { "Content-Type": "application/json;charset=utf-8" },
|
|
124
|
-
paramsSerializer: params => qs.stringify(compactTenantProjectQueryParams(params)),
|
|
124
|
+
paramsSerializer: params => qs.stringify(compactTenantProjectQueryParams(params), { arrayFormat: "repeat" }),
|
|
125
125
|
withCredentials: true
|
|
126
126
|
});
|
|
127
127
|
|
|
128
128
|
// 刷新令牌请求使用独立实例,避免与主请求拦截器互相递归。
|
|
129
129
|
const refreshService = axios.create({
|
|
130
130
|
baseURL: requestBaseURL,
|
|
131
|
-
|
|
131
|
+
// 路由守卫会同步等待刷新结果:超时过长时后端重启窗口内整站黑屏挂起,收敛到 8 秒。
|
|
132
|
+
timeout: 8000,
|
|
132
133
|
headers: { "Content-Type": "application/json;charset=utf-8" },
|
|
133
134
|
withCredentials: true
|
|
134
135
|
});
|
|
@@ -220,6 +220,27 @@ export const demoRegionTree = [
|
|
|
220
220
|
}
|
|
221
221
|
];
|
|
222
222
|
|
|
223
|
+
/** 团队树静态数据:与城市级联数据分离,供纯表格团队筛选使用。 */
|
|
224
|
+
export const demoOwnerGroupTree = [
|
|
225
|
+
{
|
|
226
|
+
value: "engineering",
|
|
227
|
+
label: "研发团队",
|
|
228
|
+
children: [
|
|
229
|
+
{ value: "frontend", label: "前端组" },
|
|
230
|
+
{ value: "backend", label: "后端组" }
|
|
231
|
+
]
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
value: "product",
|
|
235
|
+
label: "产品团队",
|
|
236
|
+
children: [
|
|
237
|
+
{ value: "design", label: "设计组" },
|
|
238
|
+
{ value: "research", label: "研究组" }
|
|
239
|
+
]
|
|
240
|
+
},
|
|
241
|
+
{ value: "operations", label: "运营团队" }
|
|
242
|
+
];
|
|
243
|
+
|
|
223
244
|
/** 项目静态数据:25 条覆盖分页、多级表头、进度条、标签列等展示场景。 */
|
|
224
245
|
export const demoProjects: DemoProject[] = [
|
|
225
246
|
{ id: 1, name: "星尘数据平台", level: "S", owner: "陈默", status: STATUS_ENABLE, budget: 128000000, task_total: 46, progress: 82, region: ["zhejiang", "hangzhou"], created_at: "2025-01-06 09:30:00" },
|
|
@@ -46,7 +46,7 @@ import { t } from "../../../locales";
|
|
|
46
46
|
import DemoFormDialog from "../components/DemoFormDialog.vue";
|
|
47
47
|
import DemoDetailDrawer from "../components/DemoDetailDrawer.vue";
|
|
48
48
|
import type { DemoDetailModel, DemoProject } from "../demo-data";
|
|
49
|
-
import { STATUS_DISABLE, STATUS_ENABLE, demoLevelOptions, demoProjects, demoRegionTree } from "../demo-data";
|
|
49
|
+
import { STATUS_DISABLE, STATUS_ENABLE, demoLevelOptions, demoOwnerGroupTree, demoProjects, demoRegionTree } from "../demo-data";
|
|
50
50
|
|
|
51
51
|
defineOptions({
|
|
52
52
|
name: "DemoTable",
|
|
@@ -71,7 +71,7 @@ const statusTagOptions = [
|
|
|
71
71
|
const tagSearchOptions = Array.from({ length: 50 }, (_, index) => ({ label: t("core.demo.table.tag_item", { index: index + 1 }), value: `tag-${index + 1}` }));
|
|
72
72
|
|
|
73
73
|
/** tree-select 搜索项的静态团队树(isShow: false,仅出现在搜索区)。 */
|
|
74
|
-
const ownerGroupOptions =
|
|
74
|
+
const ownerGroupOptions = demoOwnerGroupTree;
|
|
75
75
|
|
|
76
76
|
/** 把级联编码翻译成“省 / 市”文本,用于 region 列单元格展示。 */
|
|
77
77
|
function formatRegion(region: string[]): string {
|
|
@@ -165,7 +165,8 @@ const columns = computed<ColumnProps[]>(() => [
|
|
|
165
165
|
minWidth: 160,
|
|
166
166
|
search: {
|
|
167
167
|
el: "cascader",
|
|
168
|
-
|
|
168
|
+
enum: demoRegionTree,
|
|
169
|
+
props: { clearable: true, placeholder: t("common.placeholder.select") },
|
|
169
170
|
order: 3
|
|
170
171
|
},
|
|
171
172
|
render: scope => formatRegion((scope.row as DemoProject).region)
|
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.1.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "kratos-admin frontend runtime and base modules",
|
|
7
7
|
"author": {
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
"md-editor-v3": "^6.5.5",
|
|
46
46
|
"md5": "^2.3.0",
|
|
47
47
|
"mitt": "^3.0.1",
|
|
48
|
+
"monaco-editor": "^0.52.2",
|
|
48
49
|
"nprogress": "^0.2.0",
|
|
49
50
|
"pinia": "^4.0.3",
|
|
50
51
|
"pinia-plugin-persistedstate": "^4.7.1",
|
|
@@ -150,6 +151,10 @@
|
|
|
150
151
|
"types": "./dist/package/declarations/src/components/YamlEditor/index.vue.d.ts",
|
|
151
152
|
"default": "./dist/package/src/components/YamlEditor/index.vue"
|
|
152
153
|
},
|
|
154
|
+
"./components/CodeEditor/index.vue": {
|
|
155
|
+
"types": "./dist/package/declarations/src/components/CodeEditor/index.vue.d.ts",
|
|
156
|
+
"default": "./dist/package/src/components/CodeEditor/index.vue"
|
|
157
|
+
},
|
|
153
158
|
"./components/ProForm/index.vue": {
|
|
154
159
|
"types": "./dist/package/declarations/src/components/ProForm/index.vue.d.ts",
|
|
155
160
|
"default": "./dist/package/src/components/ProForm/index.vue"
|
|
@@ -258,7 +263,7 @@
|
|
|
258
263
|
"scripts": {
|
|
259
264
|
"build": "pnpm build:package",
|
|
260
265
|
"build:package": "node ../../scripts/build-package.mjs .",
|
|
261
|
-
"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",
|
|
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",
|
|
262
267
|
"type:check": "vue-tsc --noEmit --skipLibCheck"
|
|
263
268
|
}
|
|
264
269
|
}
|