@koishi-ce/client 1.0.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.
Files changed (121) hide show
  1. package/app/assets/logo.png +0 -0
  2. package/app/home/home.vue +51 -0
  3. package/app/home/index.ts +18 -0
  4. package/app/home/welcome.en-US.yml +9 -0
  5. package/app/home/welcome.vue +111 -0
  6. package/app/home/welcome.zh-CN.yml +9 -0
  7. package/app/index.html +13 -0
  8. package/app/index.scss +64 -0
  9. package/app/index.ts +32 -0
  10. package/app/layout/header.vue +187 -0
  11. package/app/layout/index.ts +15 -0
  12. package/app/layout/layout.vue +194 -0
  13. package/app/layout/menu-item.vue +60 -0
  14. package/app/settings/index.ts +39 -0
  15. package/app/settings/settings.vue +82 -0
  16. package/app/settings/theme.vue +123 -0
  17. package/app/shims.d.ts +1 -0
  18. package/app/status/index.ts +21 -0
  19. package/app/status/loading.vue +23 -0
  20. package/app/status/status.vue +56 -0
  21. package/app/styles/element.scss +217 -0
  22. package/app/styles/hc.scss +35 -0
  23. package/app/styles/index.scss +140 -0
  24. package/app/styles/index.ts +30 -0
  25. package/app/styles/layout.scss +92 -0
  26. package/app/theme/activity/button.vue +97 -0
  27. package/app/theme/activity/index.vue +119 -0
  28. package/app/theme/activity/item.vue +185 -0
  29. package/app/theme/activity/separator.vue +163 -0
  30. package/app/theme/activity/utils.ts +27 -0
  31. package/app/theme/blank.vue +24 -0
  32. package/app/theme/index.ts +37 -0
  33. package/app/theme/index.vue +34 -0
  34. package/app/theme/menu/index.vue +26 -0
  35. package/app/theme/menu/menu-item.vue +58 -0
  36. package/app/theme/menu/menu.vue +45 -0
  37. package/app/theme/status.vue +45 -0
  38. package/app/tsconfig.json +15 -0
  39. package/client/components/chat/image.vue +20 -0
  40. package/client/components/chat/overlay.vue +231 -0
  41. package/client/components/chat/utils.ts +6 -0
  42. package/client/components/common/index.ts +11 -0
  43. package/client/components/common/k-button.vue +127 -0
  44. package/client/components/common/k-hint.vue +62 -0
  45. package/client/components/common/k-tab.vue +50 -0
  46. package/client/components/dynamic.vue +61 -0
  47. package/client/components/icons/activity/default.vue +6 -0
  48. package/client/components/icons/activity/ellipsis.vue +6 -0
  49. package/client/components/icons/activity/home.vue +13 -0
  50. package/client/components/icons/activity/moon.vue +6 -0
  51. package/client/components/icons/activity/settings.vue +6 -0
  52. package/client/components/icons/activity/sun.vue +6 -0
  53. package/client/components/icons/index.ts +133 -0
  54. package/client/components/icons/style.scss +3 -0
  55. package/client/components/icons/svg/arrow-left.vue +6 -0
  56. package/client/components/icons/svg/arrow-right.vue +6 -0
  57. package/client/components/icons/svg/box-open.vue +6 -0
  58. package/client/components/icons/svg/check-full.vue +6 -0
  59. package/client/components/icons/svg/chevron-down.vue +6 -0
  60. package/client/components/icons/svg/chevron-left.vue +6 -0
  61. package/client/components/icons/svg/chevron-right.vue +6 -0
  62. package/client/components/icons/svg/chevron-up.vue +6 -0
  63. package/client/components/icons/svg/clipboard-list.vue +6 -0
  64. package/client/components/icons/svg/edit.vue +6 -0
  65. package/client/components/icons/svg/exclamation-full.vue +6 -0
  66. package/client/components/icons/svg/expand.vue +6 -0
  67. package/client/components/icons/svg/file-archive.vue +6 -0
  68. package/client/components/icons/svg/filter.vue +6 -0
  69. package/client/components/icons/svg/github.vue +6 -0
  70. package/client/components/icons/svg/gitlab.vue +6 -0
  71. package/client/components/icons/svg/info-full.vue +6 -0
  72. package/client/components/icons/svg/koishi.vue +6 -0
  73. package/client/components/icons/svg/link.vue +6 -0
  74. package/client/components/icons/svg/paper-plane.vue +6 -0
  75. package/client/components/icons/svg/question-empty.vue +6 -0
  76. package/client/components/icons/svg/redo.vue +6 -0
  77. package/client/components/icons/svg/search-minus.vue +6 -0
  78. package/client/components/icons/svg/search-plus.vue +6 -0
  79. package/client/components/icons/svg/search.vue +6 -0
  80. package/client/components/icons/svg/star-empty.vue +6 -0
  81. package/client/components/icons/svg/star-full.vue +6 -0
  82. package/client/components/icons/svg/start.vue +6 -0
  83. package/client/components/icons/svg/tag.vue +6 -0
  84. package/client/components/icons/svg/times-full.vue +6 -0
  85. package/client/components/icons/svg/tools.vue +6 -0
  86. package/client/components/icons/svg/undo.vue +6 -0
  87. package/client/components/icons/svg/user.vue +6 -0
  88. package/client/components/index.ts +75 -0
  89. package/client/components/layout/card.vue +69 -0
  90. package/client/components/layout/content.vue +37 -0
  91. package/client/components/layout/empty.vue +41 -0
  92. package/client/components/layout/index.ts +15 -0
  93. package/client/components/layout/tab-group.vue +45 -0
  94. package/client/components/layout/tab-item.vue +35 -0
  95. package/client/components/link.ts +39 -0
  96. package/client/components/perms.vue +73 -0
  97. package/client/components/slot.ts +95 -0
  98. package/client/context.ts +143 -0
  99. package/client/data.ts +178 -0
  100. package/client/index.ts +76 -0
  101. package/client/plugins/action.ts +251 -0
  102. package/client/plugins/i18n.ts +43 -0
  103. package/client/plugins/loader.ts +154 -0
  104. package/client/plugins/router.ts +278 -0
  105. package/client/plugins/setting.ts +217 -0
  106. package/client/plugins/theme.ts +134 -0
  107. package/client/shims.d.ts +55 -0
  108. package/client/tsconfig.json +15 -0
  109. package/client/utils.ts +36 -0
  110. package/global.d.ts +24 -0
  111. package/lib/bin.d.ts +1 -0
  112. package/lib/bin.mjs +112 -0
  113. package/lib/client-CaTn_gVG.mjs +125 -0
  114. package/lib/index.d.ts +20 -0
  115. package/lib/index.mjs +2 -0
  116. package/lib/src-Bxzzi1Rx.mjs +160 -0
  117. package/lib/yaml-UzQbvquH.mjs +23 -0
  118. package/package.json +66 -0
  119. package/src/bin.ts +61 -0
  120. package/src/index.ts +280 -0
  121. package/src/yaml.ts +22 -0
@@ -0,0 +1,119 @@
1
+ <!--
2
+ 活动栏(最左侧图标栏):分 top / bottom 两区渲染活动项,
3
+ 项与项之间的分隔槽(activity-separator)同时充当拖拽排序的落点。
4
+ 空间不足时放不下的活动项折叠进 "..." 溢出组;
5
+ 整条栏支持右键菜单(theme.activity,如"重置活动栏")。
6
+ -->
7
+ <template>
8
+ <nav
9
+ class="layout-activity flex flex-col justify-evenly"
10
+ @contextmenu.stop="trigger($event, null)">
11
+ <template v-for="(data, index) in groups.top" :key="data[0].id">
12
+ <activity-separator position="top" :index="index" />
13
+ <activity-item placement="right" :children="data"></activity-item>
14
+ </template>
15
+ <activity-separator position="top" :index="groups.top.length" />
16
+ <activity-item v-if="groups.hidden" placement="bottom" :children="groups.hidden"></activity-item>
17
+ <div v-else class="spacer"></div>
18
+ <activity-separator position="bottom" :index="groups.top.length" />
19
+ <template v-for="(data, index) in groups.bottom" :key="data.id">
20
+ <activity-item placement="right" :children="data"></activity-item>
21
+ <activity-separator position="bottom" :index="index" />
22
+ </template>
23
+ </nav>
24
+ </template>
25
+
26
+ <script lang="ts" setup>
27
+ import {
28
+ type Activity,
29
+ useConfig,
30
+ useContext,
31
+ useMenu,
32
+ } from "@koishi-ce/client";
33
+ import { useWindowSize } from "@vueuse/core";
34
+ import { computed, provide } from "vue";
35
+ import ActivityItem from "./item.vue";
36
+ import ActivitySeparator from "./separator.vue";
37
+
38
+ const ctx = useContext();
39
+ const config = useConfig();
40
+ const trigger = useMenu("theme.activity");
41
+ const { height, width } = useWindowSize();
42
+
43
+ // 计算最终的活动栏分组:{ top, bottom, hidden(溢出折叠组,可能为空) }
44
+ const groups = computed(() => {
45
+ let hidden: Activity[];
46
+ // 单个活动项占位(与 CSS 中 --activity-width / --activity-padding 对应)
47
+ const unit = width.value <= 768 ? 52 : 56;
48
+ // 可用总高度:视口高度减去上下留白(窄屏 4px / 宽屏 8px)
49
+ const total = height.value - (width.value <= 768 ? 4 : 8);
50
+ // 初始分组表:每个未禁用页面各自成一组(值为 Activity 数组)
51
+ const available = Object.fromEntries(
52
+ Object.entries(ctx.$router.pages)
53
+ .filter(([, data]) => !data.disabled())
54
+ .map(([key, data]) => [key, [data]]),
55
+ );
56
+ // 应用用户的覆盖配置:hidden 的直接移除;声明 parent 的并入父项所在组
57
+ for (const id of Object.keys(available)) {
58
+ const override = config.value.activities?.[id];
59
+ if (!override) continue;
60
+ if (override.hidden) {
61
+ delete available[id];
62
+ continue;
63
+ }
64
+ Object.assign(available[id][0], override);
65
+ const parent = available[override.parent];
66
+ if (parent) {
67
+ parent.push(available[id][0]);
68
+ delete available[id];
69
+ }
70
+ }
71
+ const list = Object.values(available).sort(([a], [b]) => a.order - b.order);
72
+ // 放不下时:从头部取出溢出项折叠成一个组,top 位优先、同位再按 order 排,
73
+ // 并以 "..." 图标作为该组的展示入口
74
+ if (list.length * unit > total) {
75
+ hidden = list
76
+ .splice(0, list.length + 1 - Math.floor(total / unit))
77
+ .sort(([a], [b]) => {
78
+ const scale = a.position === "top" ? -1 : 1;
79
+ if (a.position === b.position) {
80
+ return scale * (a.order - b.order);
81
+ }
82
+ return scale;
83
+ })
84
+ .flat();
85
+ hidden.unshift({ icon: "activity:ellipsis" } as Activity);
86
+ }
87
+ // top 区按 order 逆序输出(order 大者靠上),bottom 区保持正序
88
+ const top = list.filter(([data]) => data.position !== "bottom").reverse();
89
+ const bottom = list.filter(([data]) => data.position === "bottom");
90
+ return { top, bottom, hidden };
91
+ });
92
+
93
+ // 供 separator 子组件注入,用于拖拽落点计算
94
+ provide("groups", groups);
95
+ </script>
96
+
97
+ <style lang="scss" scoped>
98
+
99
+ .marker {
100
+ position: absolute;
101
+ }
102
+
103
+ .layout-activity {
104
+ position: fixed;
105
+ box-sizing: border-box;
106
+ z-index: 100;
107
+ top: 0;
108
+ bottom: 0;
109
+ width: var(--activity-width);
110
+ background-color: var(--k-activity-bg);
111
+ border-right: var(--k-activity-divider, var(--k-color-divider-dark)) 1px solid;
112
+ transition: var(--color-transition);
113
+
114
+ .spacer {
115
+ flex: 1 0 auto;
116
+ }
117
+ }
118
+
119
+ </style>
@@ -0,0 +1,185 @@
1
+ <!--
2
+ 活动栏条目:children[0] 为组的主图标,其余为并入该组的子活动,
3
+ 悬停 tooltip 中列出组内成员。条目本身也是拖拽落点——
4
+ 把另一个活动拖到本条目上即并入(或互换)分组。
5
+ -->
6
+ <template>
7
+ <div
8
+ class="activity-item"
9
+ :class="{ 'active': isActive, 'drag-over': hasDragOver }"
10
+ @contextmenu.stop="trigger($event, children[0])"
11
+ @dragenter="handleDragEnter"
12
+ @dragleave="handleDragLeave"
13
+ @drop="handleDrop"
14
+ @dragover.prevent>
15
+ <el-tooltip placement="right" :popper-class="`activity-item-tooltip`">
16
+ <template #content>
17
+ <div class="activity-info">
18
+ <div class="title">{{ children[hoverIndex].name }}</div>
19
+ <div class="desc" v-if="children[hoverIndex].desc">{{ children[hoverIndex].desc }}</div>
20
+ </div>
21
+ <div class="activity-group" v-if="children.length > 1">
22
+ <div class="activity-group-item" v-for="(child, index) in children.slice(1)" :key="child.id">
23
+ <activity-button
24
+ :data="child"
25
+ @mouseenter="hoverIndex = index + 1"
26
+ @mouseleave="hoverIndex = 0"
27
+ ></activity-button>
28
+ </div>
29
+ </div>
30
+ </template>
31
+ <activity-button :data="children[0]" :class="{ 'is-group': children.length > 1 }"></activity-button>
32
+ </el-tooltip>
33
+ </div>
34
+ </template>
35
+
36
+ <script lang="ts" setup>
37
+ import { type Activity, useConfig, useMenu } from "@koishi-ce/client";
38
+ import type { Placement } from "element-plus";
39
+ import { computed, ref, watch } from "vue";
40
+ import { useRoute } from "vue-router";
41
+ import ActivityButton from "./button.vue";
42
+
43
+ const route = useRoute();
44
+
45
+ const props = defineProps<{
46
+ children: Activity[];
47
+ placement: Placement;
48
+ }>();
49
+
50
+ const isActive = computed(() => {
51
+ // 组内任一成员命中当前路由即整组高亮
52
+ return Object.values(props.children).some(
53
+ (child) => route.meta?.activity?.id === child.id,
54
+ );
55
+ });
56
+
57
+ const hasDragOver = ref(false);
58
+
59
+ const trigger = useMenu("theme.activity");
60
+
61
+ // tooltip 中当前悬停的成员下标(0 为主图标,其余对应组内子活动)
62
+ const hoverIndex = ref(0);
63
+
64
+ watch(
65
+ () => props.children,
66
+ () => {
67
+ hoverIndex.value = 0;
68
+ },
69
+ );
70
+
71
+ function handleDragEnter(event: DragEvent) {
72
+ hasDragOver.value = true;
73
+ }
74
+
75
+ function handleDragLeave(event: DragEvent) {
76
+ hasDragOver.value = false;
77
+ }
78
+
79
+ const config = useConfig();
80
+
81
+ // 原型链保留键:这类键在普通对象上会触发原型链存取器,禁止作为配置键读写
82
+ // (id 来自拖拽事件的 dataTransfer 文本,属外部输入)
83
+ const UNSAFE_KEYS = new Set(["__proto__", "constructor", "prototype"]);
84
+
85
+ /** 取某活动的覆盖配置(不存在则创建);保留键返回一次性空对象,防原型污染。
86
+ * 守卫须用显式字符串比较(Set.has 形式 CodeQL 无法识别为阻断) */
87
+ function ensureOverride(id: string): Record<string, unknown> {
88
+ const activities = (config.value.activities ??= {});
89
+ if (id === "__proto__" || id === "constructor" || id === "prototype") {
90
+ return {};
91
+ }
92
+ return (activities[id] ??= {});
93
+ }
94
+
95
+ function handleDrop(event: DragEvent) {
96
+ hasDragOver.value = false;
97
+ const text = event.dataTransfer.getData("text/plain");
98
+ // 只响应活动栏自身的拖拽协议,忽略外部拖入内容
99
+ if (!text.startsWith("activity:")) return;
100
+ const id = text.slice(9);
101
+ const target = props.children[0].id;
102
+ // 拖到自身所在组上无需处理
103
+ if (target === id) return;
104
+ event.preventDefault();
105
+
106
+ const override = ensureOverride(id);
107
+ if (override.parent === target) {
108
+ // 原本 id 已是 target 的子项:视为"拖出",反转父子关系,
109
+ // 并让原父项(及其它子项)改挂到新的父项 id 之下
110
+ delete override.parent;
111
+ ensureOverride(target).parent = id;
112
+ for (const key in config.value.activities) {
113
+ // 跳过原型链保留键:JSON 配置可携带自有 "__proto__" 属性,
114
+ // 普通对象对其 [] 读取会命中原型链存取器
115
+ if (UNSAFE_KEYS.has(key)) continue;
116
+ const override = config.value.activities[key];
117
+ if (override?.parent === target) {
118
+ override.parent = id;
119
+ }
120
+ }
121
+ } else {
122
+ // 常规情形:把拖入项并到 target 组
123
+ override.parent = target;
124
+ }
125
+ }
126
+ </script>
127
+
128
+ <style lang="scss">
129
+
130
+ .activity-item {
131
+ position: relative;
132
+ box-sizing: border-box;
133
+ width: var(--activity-width);
134
+ padding: 0 var(--activity-padding);
135
+
136
+ .layout-activity &::before {
137
+ content: '';
138
+ position: absolute;
139
+ top: 50%;
140
+ left: 0;
141
+ width: var(--activity-marker-width);
142
+ height: var(--activity-marker-height);
143
+ transform: translateX(-100%) translateY(-50%);
144
+ display: block;
145
+ border-radius: 0 var(--activity-marker-width) var(--activity-marker-width) 0;
146
+ background-color: var(--k-text-active);
147
+ transition: all 0.3s ease;
148
+ }
149
+
150
+ .layout-activity &.active::before,
151
+ .layout-activity &.drag-over::before {
152
+ transform: translateY(-50%);
153
+ }
154
+ }
155
+
156
+ .activity-item-tooltip {
157
+ padding: 0;
158
+
159
+ .activity-info {
160
+ padding: 6px 11px;
161
+ line-height: 1.6;
162
+
163
+ .title {
164
+ font-size: 13px;
165
+ font-weight: 500;
166
+ }
167
+
168
+ .desc {
169
+ font-size: 12px;
170
+ }
171
+ }
172
+
173
+ .activity-group {
174
+ display: flex;
175
+ padding: var(--activity-padding);
176
+ gap: 0 var(--activity-padding);
177
+ border-top: 1px solid var(--k-color-divider);
178
+
179
+ .activity-group-item {
180
+ width: calc(var(--activity-width) - 2 * var(--activity-padding));
181
+ }
182
+ }
183
+ }
184
+
185
+ </style>
@@ -0,0 +1,163 @@
1
+ <!--
2
+ 活动栏条目间的分隔槽:平时不可见,拖拽悬停时显示横线提示,
3
+ 松手即把被拖项移动到该位置(拖拽排序的核心落点组件)。
4
+ -->
5
+ <template>
6
+ <div
7
+ class="separator"
8
+ :class="{ 'drag-over': hasDragOver }"
9
+ @dragenter="handleDragEnter"
10
+ @dragleave="handleDragLeave"
11
+ @drop="handleDrop"
12
+ @dragover.prevent
13
+ ></div>
14
+ </template>
15
+
16
+ <script lang="ts" setup>
17
+ import { type Activity, useConfig, useContext } from "@koishi-ce/client";
18
+ import { type ComputedRef, inject, ref } from "vue";
19
+
20
+ type Position = "top" | "bottom";
21
+
22
+ const props = defineProps<{
23
+ index: number;
24
+ position: Position;
25
+ }>();
26
+
27
+ const groups = inject("groups") as ComputedRef<Record<Position, Activity[][]>>;
28
+
29
+ const hasDragOver = ref(false);
30
+
31
+ function handleDragEnter(event: DragEvent) {
32
+ hasDragOver.value = true;
33
+ }
34
+
35
+ function handleDragLeave(event: DragEvent) {
36
+ hasDragOver.value = false;
37
+ }
38
+
39
+ const config = useConfig();
40
+ const ctx = useContext();
41
+
42
+ // 原型链保留键:这类键在普通对象上会触发原型链存取器,禁止作为配置键读写
43
+ // (id 来自拖拽事件的 dataTransfer 文本,属外部输入)
44
+ const UNSAFE_KEYS = new Set(["__proto__", "constructor", "prototype"]);
45
+
46
+ /** 取某活动的覆盖配置(不存在则创建);保留键返回一次性空对象,防原型污染。
47
+ * 守卫须用显式字符串比较(Set.has 形式 CodeQL 无法识别为阻断) */
48
+ function ensureOverride(id: string): Record<string, unknown> {
49
+ const activities = (config.value.activities ??= {});
50
+ if (id === "__proto__" || id === "constructor" || id === "prototype") {
51
+ return {};
52
+ }
53
+ return (activities[id] ??= {});
54
+ }
55
+
56
+ function handleDrop(event: DragEvent) {
57
+ hasDragOver.value = false;
58
+ const text = event.dataTransfer.getData("text/plain");
59
+ // 只响应活动栏自身的拖拽协议
60
+ if (!text.startsWith("activity:")) return;
61
+ const id = text.slice(9);
62
+ const list = groups.value[props.position].map(([item]) => item);
63
+ const oldIndex = list.findIndex((item) => item.id === id);
64
+ // 落点即原位(含紧邻原位的前一格)时无需移动
65
+ if (
66
+ oldIndex === props.index ||
67
+ (oldIndex === props.index - 1 && oldIndex !== -1)
68
+ )
69
+ return;
70
+ event.preventDefault();
71
+
72
+ // 先在副本上完成移动,得到目标排列,再据此反推各项的 order 覆盖值
73
+ let index = props.index;
74
+ const item = ctx.$router.pages[id];
75
+ if (oldIndex < 0) {
76
+ list.splice(index, 0, item);
77
+ } else {
78
+ // 原位置在落点之前时,移除自身会使落点前移一格,需补偿
79
+ if (oldIndex < index) index--;
80
+ list.splice(oldIndex, 1);
81
+ list.splice(index, 0, item);
82
+ }
83
+
84
+ const override = ensureOverride(id);
85
+ // 拖拽落在条目之间意味着脱离原分组,清除父项;
86
+ // 位置(top / bottom)与注册默认不同才记录覆盖,相同则删掉以保持配置干净
87
+ delete override.parent;
88
+ if (item.options.position !== props.position) {
89
+ override.position = props.position;
90
+ } else {
91
+ delete override.position;
92
+ }
93
+
94
+ // 左右最近的"未改序"条目(order 仍等于注册默认值)作为锚点,
95
+ // 介于两锚点之间的项按线性插值重算 order;
96
+ // 只有一侧锚点时按步长 100 单向递增 / 递减;两侧都没有则恢复默认
97
+ const anchorL = list.findLastIndex(
98
+ (item, i) => i < index && item.order === item.options.order,
99
+ );
100
+ const anchorR = list.findIndex(
101
+ (item, i) => i > index && item.order === item.options.order,
102
+ );
103
+ if (anchorL === -1) {
104
+ if (anchorR === -1) {
105
+ delete override.order;
106
+ } else {
107
+ let order = list[anchorR].options.order;
108
+ for (let index = anchorR - 1; index >= 0; index--) {
109
+ const override = ensureOverride(list[index].id);
110
+ override.order = order += 100;
111
+ }
112
+ }
113
+ } else {
114
+ if (anchorR === -1) {
115
+ let order = list[anchorL].options.order;
116
+ for (let index = anchorL + 1; index < list.length; index++) {
117
+ const override = ensureOverride(list[index].id);
118
+ override.order = order -= 100;
119
+ }
120
+ } else {
121
+ let orderL = list[anchorL].options.order;
122
+ let orderR = list[anchorR].options.order;
123
+ for (let index = anchorL + 1; index < anchorR; index++) {
124
+ const override = ensureOverride(list[index].id);
125
+ override.order =
126
+ orderL +
127
+ ((orderR - orderL) * (index - anchorL)) / (anchorR - anchorL);
128
+ }
129
+ }
130
+ }
131
+
132
+ // 覆盖配置为空对象时删除该键,避免残留无意义的配置项
133
+ if (!Object.keys(override).length && !UNSAFE_KEYS.has(id)) {
134
+ delete config.value.activities[id];
135
+ }
136
+ }
137
+ </script>
138
+
139
+ <style lang="scss" scoped>
140
+
141
+ .separator {
142
+ position: relative;
143
+ height: var(--activity-padding);
144
+
145
+ &::before {
146
+ position: absolute;
147
+ content: '';
148
+ top: 50%;
149
+ left: var(--activity-padding);
150
+ right: var(--activity-padding);
151
+ height: 2px;
152
+ border-radius: 2px;
153
+ transform: translateY(-50%);
154
+ transition: var(--color-transition);
155
+ background-color: transparent;
156
+ }
157
+
158
+ &.drag-over::before {
159
+ background-color: var(--k-text-active);
160
+ }
161
+ }
162
+
163
+ </style>
@@ -0,0 +1,27 @@
1
+ /**
2
+ * 活动栏(activity bar)相关的类型扩充:
3
+ * - 为 ActionContext 补充 "theme.activity" 菜单的求值上下文类型;
4
+ * - 为控制台 Config 声明 activities 覆盖配置(隐藏 / 归组 / 排序 / 上下位置)。
5
+ */
6
+
7
+ import type { Activity, Dict } from "@koishi-ce/client";
8
+
9
+ declare module "@koishi-ce/client" {
10
+ interface ActionContext {
11
+ "theme.activity": Activity;
12
+ }
13
+
14
+ interface Config {
15
+ // 运行时的默认配置(initial fallback)并不包含该字段,
16
+ // 故声明为可选(exactOptionalPropertyTypes 下不显式赋 undefined)
17
+ activities?: Dict<ActivityOverride>;
18
+ }
19
+ }
20
+
21
+ // 单个活动项的覆盖配置:不声明的字段沿用页面注册时的默认值
22
+ interface ActivityOverride {
23
+ hidden?: boolean;
24
+ parent?: string;
25
+ order?: number;
26
+ position?: "top" | "bottom";
27
+ }
@@ -0,0 +1,24 @@
1
+ <!-- 404 空白页:控制台本体加载成功但没有任何扩展页面时的兜底展示 -->
2
+ <template>
3
+ <div class="blank">
4
+ <h1>404</h1>
5
+ <p>如果你看到了这个页面,说明控制台插件已经加载成功,但你尚未安装任何控制台扩展。</p>
6
+ </div>
7
+ </template>
8
+
9
+ <style lang="scss" scoped>
10
+
11
+ .blank {
12
+ position: fixed;
13
+ top: 0;
14
+ left: 0;
15
+ right: 0;
16
+ bottom: 0;
17
+ display: flex;
18
+ flex-direction: column;
19
+ align-items: center;
20
+ justify-content: center;
21
+ padding: 0 2rem;
22
+ }
23
+
24
+ </style>
@@ -0,0 +1,37 @@
1
+ /**
2
+ * 主题插件(控制台外壳):
3
+ * - 向 "root" 插槽注册应用根组件(活动栏 + 路由视图 + 状态栏 + 全局菜单层);
4
+ * - 注册活动栏右键菜单 "theme.activity" 及其动作(重置活动栏自定义配置)。
5
+ */
6
+
7
+ import { type Context, router, useConfig } from "@koishi-ce/client";
8
+ import App from "./index.vue";
9
+
10
+ export default function (ctx: Context) {
11
+ ctx.slot({
12
+ type: "root",
13
+ component: App,
14
+ order: -1000,
15
+ });
16
+
17
+ const config = useConfig();
18
+
19
+ ctx.action("theme.activity.settings", {
20
+ action: () => router.push("/settings/activity"),
21
+ });
22
+
23
+ ctx.action("theme.activity.reset", {
24
+ // 清空 activities 覆盖配置,活动栏恢复默认排序与分组
25
+ action: () => (config.value.activities = {}),
26
+ });
27
+
28
+ ctx.menu("theme.activity", [
29
+ {
30
+ // id: '.settings',
31
+ // label: '活动栏设置',
32
+ // }, {
33
+ id: ".reset",
34
+ label: "重置活动栏",
35
+ },
36
+ ]);
37
+ }
@@ -0,0 +1,34 @@
1
+ <!--
2
+ 控制台根组件:活动栏 + 路由页面(keep-alive 缓存) + 状态栏 + 全局菜单层。
3
+ 路由声明了所需 store 字段(route.meta.activity.fields)时,
4
+ 等全部字段就绪才渲染页面,期间显示全屏 loading。
5
+ -->
6
+ <template>
7
+ <activity-bar></activity-bar>
8
+ <router-view v-if="loaded" #="{ Component }">
9
+ <keep-alive>
10
+ <component :is="Component"></component>
11
+ </keep-alive>
12
+ </router-view>
13
+ <div class="loading" v-else v-loading="true" element-loading-text="正在加载数据……"></div>
14
+ <status-bar></status-bar>
15
+ <menu-list></menu-list>
16
+ </template>
17
+
18
+ <script lang="ts" setup>
19
+ import { store } from "@koishi-ce/client";
20
+ import { computed } from "vue";
21
+ import { useRoute } from "vue-router";
22
+ import ActivityBar from "./activity/index.vue";
23
+ import MenuList from "./menu/index.vue";
24
+ import StatusBar from "./status.vue";
25
+
26
+ const route = useRoute();
27
+
28
+ // 页面就绪判定:未声明所需字段的页面恒为已就绪;
29
+ // 否则要求 meta.activity.fields 中每个 store 字段都有数据
30
+ const loaded = computed(() => {
31
+ if (!route.meta.activity?.fields) return true;
32
+ return route.meta.activity.fields.every((key) => store[key]);
33
+ });
34
+ </script>
@@ -0,0 +1,26 @@
1
+ <!--
2
+ 全局菜单层:渲染当前处于活动状态的所有上下文菜单(activeMenus)。
3
+ 任意 click / contextmenu 事件都视为点击了菜单外区域,关闭全部活动菜单。
4
+ -->
5
+ <template>
6
+ <template v-for="menu of ctx.internal.activeMenus" :key="menu.id">
7
+ <k-menu v-bind="menu"></k-menu>
8
+ </template>
9
+ </template>
10
+
11
+ <script lang="ts" setup>
12
+ import { useContext } from "@koishi-ce/client";
13
+ import { useEventListener } from "@vueuse/core";
14
+ import KMenu from "./menu.vue";
15
+
16
+ const ctx = useContext();
17
+
18
+ // useEventListener 会随组件卸载自动解绑,无需手动清理
19
+ useEventListener("click", () => {
20
+ ctx.internal.activeMenus.splice(0);
21
+ });
22
+
23
+ useEventListener("contextmenu", () => {
24
+ ctx.internal.activeMenus.splice(0);
25
+ });
26
+ </script>
@@ -0,0 +1,58 @@
1
+ <!--
2
+ 上下文菜单的单个菜单项:点击时调用对应 action。
3
+ id 支持两种特殊写法:"!" 前缀会被去除(兼容旧的否定式 id),
4
+ "." 开头则是相对 id,需拼上所属菜单前缀(props.prefix)再查找 action。
5
+ label / icon / hidden / disabled 同样支持以 scope 为参的 getter。
6
+ -->
7
+ <template>
8
+ <div
9
+ class="k-menu-item"
10
+ v-if="!hidden"
11
+ :class="[toValue(type), { disabled }]"
12
+ @click.prevent="item?.action(ctx.$action.createScope())"
13
+ >
14
+ <span v-if="icon" class="k-menu-icon"><k-icon :name="icon"/></span>
15
+ {{ toValue(label) }}
16
+ </div>
17
+ </template>
18
+
19
+ <script lang="ts" setup>
20
+ import { type MaybeGetter, type MenuItem, useContext } from "@koishi-ce/client";
21
+ import { computed } from "vue";
22
+
23
+ const props = defineProps<MenuItem & { prefix: string }>();
24
+
25
+ const ctx = useContext();
26
+
27
+ // 解析出实际的 action:去掉 "!" 前缀、拼接相对 id 后查注册表
28
+ const item = computed(() => {
29
+ let id = props.id.replace(/^!/, "");
30
+ if (id.startsWith(".")) id = props.prefix + id;
31
+ return ctx.internal.actions[id];
32
+ });
33
+
34
+ // action 不存在(未注册)时整项隐藏;声明了 hidden 的按求值结果判断
35
+ const hidden = computed(() => {
36
+ if (!item.value) return true;
37
+ if (!item.value.hidden) return false;
38
+ return toValue(item.value.hidden);
39
+ });
40
+
41
+ // action 不存在或声明禁用时置灰不可点
42
+ const disabled = computed(() => {
43
+ if (!item.value) return true;
44
+ if (!item.value.disabled) return false;
45
+ return toValue(item.value.disabled);
46
+ });
47
+
48
+ const icon = computed(() => toValue(props.icon));
49
+
50
+ // label / icon 等字段可能是静态值或以 action scope 为参的 getter,统一解包
51
+ function toValue<T>(getter: MaybeGetter<T>): T {
52
+ if (typeof getter !== "function") return getter;
53
+ // 收窄到 MaybeGetter 的函数分支(以作用域为参的 getter)
54
+ return (getter as (current: ReturnType<typeof ctx.$action.createScope>) => T)(
55
+ ctx.$action.createScope(),
56
+ );
57
+ }
58
+ </script>