@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,69 @@
1
+ <!--
2
+ k-card:通用卡片容器。
3
+ 可选 header(标题文字或 header 插槽)与 footer 插槽;
4
+ scrollbar 开启时正文区使用 el-scrollbar 滚动。
5
+ -->
6
+ <template>
7
+ <section class="k-card">
8
+ <header v-if="title || $slots.header">
9
+ <slot name="header">{{ title }}</slot>
10
+ </header>
11
+ <el-scrollbar v-if="scrollbar">
12
+ <div class="k-card-body"><slot/></div>
13
+ </el-scrollbar>
14
+ <div v-else class="k-card-body"><slot/></div>
15
+ <footer v-if="$slots.footer">
16
+ <slot name="footer"></slot>
17
+ </footer>
18
+ </section>
19
+ </template>
20
+
21
+ <script lang="ts" setup>
22
+ defineProps<{
23
+ title?: string;
24
+ scrollbar?: boolean;
25
+ }>();
26
+ </script>
27
+
28
+ <style lang="scss">
29
+
30
+ $paddingX: 1.25rem;
31
+ $paddingY: 1.5rem;
32
+
33
+ .k-card {
34
+ border-radius: 8px;
35
+ overflow: hidden;
36
+ position: relative;
37
+ background-color: var(--k-card-bg);
38
+ border: 1px solid var(--k-card-border);
39
+ box-shadow: var(--k-card-shadow);
40
+ transition: var(--color-transition);
41
+
42
+ header {
43
+ position: relative;
44
+ font-size: 1.25rem;
45
+ font-weight: bolder;
46
+ margin: $paddingX 0;
47
+ padding: 0 $paddingY;
48
+ transition: color 0.3s ease;
49
+ }
50
+
51
+ footer {
52
+ position: relative;
53
+ margin: $paddingX 0;
54
+ padding: 0 $paddingY;
55
+ }
56
+
57
+ .k-card-body {
58
+ position: relative;
59
+ margin: $paddingX 0;
60
+ padding: 0 $paddingY;
61
+ }
62
+
63
+ &.frameless .k-card-body {
64
+ margin: 0;
65
+ padding: 0;
66
+ }
67
+ }
68
+
69
+ </style>
@@ -0,0 +1,37 @@
1
+ <!--
2
+ k-content:页面主内容区容器。
3
+ 水平居中、限宽 55rem,自带垂直滚动(el-scrollbar),
4
+ 左右留白随屏幕宽度自适应收窄。
5
+ -->
6
+ <template>
7
+ <el-scrollbar>
8
+ <div class="k-content">
9
+ <slot></slot>
10
+ </div>
11
+ </el-scrollbar>
12
+ </template>
13
+
14
+ <style lang="scss">
15
+
16
+ .k-content {
17
+ --content-padding: 3rem;
18
+
19
+ margin: auto;
20
+ max-width: 55rem;
21
+ box-sizing: border-box;
22
+ padding: 3rem var(--content-padding);
23
+
24
+ > :first-child {
25
+ margin-top: 0;
26
+ }
27
+
28
+ @media screen and (max-width: 768px) {
29
+ --content-padding: 2rem;
30
+ }
31
+
32
+ @media screen and (max-width: 480px) {
33
+ --content-padding: 1.5rem;
34
+ }
35
+ }
36
+
37
+ </style>
@@ -0,0 +1,41 @@
1
+ <!--
2
+ k-empty:空状态占位组件。
3
+ 居中展示 Koishi 徽标图标与默认插槽文案,用于无数据 / 无权限等场景。
4
+ -->
5
+ <template>
6
+ <div class="k-empty">
7
+ <div class="image">
8
+ <k-icon name="koishi"></k-icon>
9
+ </div>
10
+ <slot></slot>
11
+ </div>
12
+ </template>
13
+
14
+ <style lang="scss">
15
+
16
+ .k-empty {
17
+ height: 100%;
18
+ display: flex;
19
+ justify-content: center;
20
+ align-items: center;
21
+ flex-direction: column;
22
+ text-align: center;
23
+ box-sizing: border-box;
24
+ padding: 2rem 0;
25
+ gap: 1.5rem;
26
+ font-size: 14px;
27
+ color: var(--el-text-color-secondary);
28
+
29
+ .image {
30
+ width: 10rem;
31
+ color: var(--k-page-bg);
32
+ transition: color 0.3s ease;
33
+
34
+ svg {
35
+ height: 100%;
36
+ width: 100%;
37
+ }
38
+ }
39
+ }
40
+
41
+ </style>
@@ -0,0 +1,15 @@
1
+ import type { App } from "vue";
2
+ import Card from "./card.vue";
3
+ import Content from "./content.vue";
4
+ import Empty from "./empty.vue";
5
+ import TabGroup from "./tab-group.vue";
6
+ import TabItem from "./tab-item.vue";
7
+
8
+ /** 注册布局组件(k-card / k-content / k-empty / k-tab-group / k-tab-item) */
9
+ export default function (app: App) {
10
+ app.component("k-card", Card);
11
+ app.component("k-content", Content);
12
+ app.component("k-empty", Empty);
13
+ app.component("k-tab-group", TabGroup);
14
+ app.component("k-tab-item", TabItem);
15
+ }
@@ -0,0 +1,45 @@
1
+ <!--
2
+ k-tab-group:侧栏式选项卡分组。
3
+ 遍历 data(键 → 条目对象),filter 可按条目过滤显示;
4
+ 每个条目渲染为一个 k-tab-item,选中键经 v-model 双向绑定,
5
+ 条目数据通过作用域插槽传给内容。
6
+ -->
7
+ <template>
8
+ <template v-for="(item, key) in data" :key="key">
9
+ <k-tab-item v-model="model" :label="key"
10
+ v-if="filter ? filter(item) : true">
11
+ <slot v-bind="item"></slot>
12
+ </k-tab-item>
13
+ </template>
14
+ </template>
15
+
16
+ <script lang="ts" setup>
17
+ import { computed } from "vue";
18
+
19
+ const props = defineProps<{
20
+ data: object;
21
+ modelValue?: string;
22
+ filter?: (item: unknown) => boolean;
23
+ }>();
24
+
25
+ const emits = defineEmits(["update:modelValue"]);
26
+
27
+ const model = computed({
28
+ get: () => props.modelValue,
29
+ set: (val) => emits("update:modelValue", val),
30
+ });
31
+ </script>
32
+
33
+ <style lang="scss">
34
+
35
+ .k-tab-group-title {
36
+ line-height: 2.25rem;
37
+ padding: 0 2rem !important;
38
+ font-weight: bold;
39
+ }
40
+
41
+ .k-tab-group-title:not(.k-select-item) {
42
+ margin-top: 0.5rem;
43
+ }
44
+
45
+ </style>
@@ -0,0 +1,35 @@
1
+ <!--
2
+ k-tab-item:选项卡条目(配合 k-tab-group 使用)。
3
+ label 为条目标识兼默认文案,点击时 emit update:modelValue;
4
+ 与当前选中值相等时高亮。
5
+ -->
6
+ <template>
7
+ <div class="k-tab-item k-tab-menu-item truncate"
8
+ :title="label"
9
+ :class="{ active: label === modelValue }"
10
+ @click="$emit('update:modelValue', label)">
11
+ <slot>{{ label }}</slot>
12
+ </div>
13
+ </template>
14
+
15
+ <script lang="ts" setup>
16
+ defineEmits(["update:modelValue"]);
17
+
18
+ defineProps<{
19
+ label: string;
20
+ modelValue: string;
21
+ }>();
22
+ </script>
23
+
24
+ <style lang="scss">
25
+
26
+ .k-tab-item {
27
+ line-height: 2.25rem;
28
+ padding: 0 2rem 0 4rem;
29
+
30
+ &.active {
31
+ background-color: var(--k-hover-bg);
32
+ }
33
+ }
34
+
35
+ </style>
@@ -0,0 +1,39 @@
1
+ import { type App, defineComponent, h } from "vue";
2
+ import { RouterLink } from "vue-router";
3
+ import { useContext } from "../context";
4
+
5
+ /**
6
+ * activity 页面链接组件(k-activity-link):
7
+ * 传入页面 id,渲染一个指向该页面的 RouterLink。
8
+ * 目标路径优先取该页面上次访问的完整路径(缓存),
9
+ * 无缓存则回退到页面路由路径(去掉动态参数段);默认插槽为空时
10
+ * 以页面名称作为链接文案。
11
+ */
12
+ const KActivityLink = defineComponent({
13
+ props: {
14
+ /** 目标 activity 的 id */
15
+ id: String,
16
+ },
17
+ setup(props, { slots }) {
18
+ const ctx = useContext();
19
+ return () => {
20
+ const activity = props.id ? ctx.$router.pages[props.id] : undefined;
21
+ return h(
22
+ RouterLink,
23
+ {
24
+ // /:.+/ 去掉路径中的动态参数段,得到可直接访问的静态前缀
25
+ to: (ctx.$router.cache[activity?.id ?? ""] ||
26
+ activity?.path.replace(/:.+/, "")) as string,
27
+ },
28
+ {
29
+ default: () => slots["default"]?.() ?? activity?.name,
30
+ },
31
+ );
32
+ };
33
+ },
34
+ });
35
+
36
+ /** 注册全局组件 k-activity-link */
37
+ export default function (app: App) {
38
+ app.component("k-activity-link", KActivityLink);
39
+ }
@@ -0,0 +1,73 @@
1
+ <!--
2
+ perms.vue:权限选择控件(array + perms 角色)。
3
+ 把服务端下发的扁平权限名(如 "channel.xxx")按 ":" 逐级拆分,
4
+ 构造成 el-cascader 的级联选项树;checkStrictly 允只勾选父级权限,
5
+ emitPath: false 使选中值仍是完整的权限路径字符串。
6
+ -->
7
+ <template>
8
+ <schema-base>
9
+ <template #title><slot name="title"></slot></template>
10
+ <template #desc><slot name="desc"></slot></template>
11
+ <template #menu><slot name="menu"></slot></template>
12
+ <template #prefix><slot name="prefix"></slot></template>
13
+ <template #suffix><slot name="suffix"></slot></template>
14
+ <template #control>
15
+ <el-cascader
16
+ collapse-tags
17
+ v-model="config"
18
+ :options="options"
19
+ :props="{ multiple: true, checkStrictly: true, emitPath: false }"
20
+ :disabled="disabled">
21
+ </el-cascader>
22
+ </template>
23
+ </schema-base>
24
+ </template>
25
+
26
+ <script lang="ts" setup>
27
+ // SchemaBase 经由 components 包的再导出获取:本包的 node_modules 没有
28
+ // schemastery-vue 链接(它是 components 的依赖,Bun 隔离布局下不跨包可见)
29
+
30
+ import { store } from "@koishi-ce/client";
31
+ import { type Schema, SchemaBase } from "@koishi-ce/components";
32
+ import type { CascaderOption } from "element-plus";
33
+ import { computed, type PropType } from "vue";
34
+
35
+ defineProps({
36
+ schema: {} as PropType<Schema>,
37
+ modelValue: {} as PropType<string>,
38
+ disabled: {} as PropType<boolean>,
39
+ prefix: {} as PropType<string>,
40
+ initial: {} as PropType<Record<never, never>>,
41
+ });
42
+
43
+ defineEmits(["update:modelValue"]);
44
+
45
+ const config = SchemaBase.useModel();
46
+
47
+ /**
48
+ * 递归插入一个权限路径(如 ["channel", "admin", "x"]):
49
+ * 逐段查找/创建级联节点;中间段(还有下级时)标记为 disabled,
50
+ * 因为 el-cascader 的父节点勾选会影响子级,而中间段并非真实权限。
51
+ */
52
+ function addNode(nodes: CascaderOption[], path: string[], prefix = "") {
53
+ const name = path.shift();
54
+ let node = nodes.find((node) => node.value === prefix + name);
55
+ if (!node) {
56
+ node = { value: prefix + name, label: name, disabled: !!path.length };
57
+ nodes.push(node);
58
+ }
59
+ if (!path.length) return;
60
+ addNode((node.children ||= []), path, `${prefix + name}:`);
61
+ }
62
+
63
+ // 由全部权限名构建级联选项树
64
+ const options = computed(() => {
65
+ const result: CascaderOption[] = [];
66
+ for (const name of store.permissions) {
67
+ const path = name.split(":");
68
+ addNode(result, path);
69
+ }
70
+ console.log(result);
71
+ return result;
72
+ });
73
+ </script>
@@ -0,0 +1,95 @@
1
+ import { type App, type Component, defineComponent, h } from "vue";
2
+ import { useContext } from "../context";
3
+
4
+ /** 插槽条目的基础形状:参与排序的组件 */
5
+ export interface SlotItem {
6
+ order?: number;
7
+ component: Component;
8
+ }
9
+
10
+ /**
11
+ * 通过 `ctx.slot()` 注册的外部插槽视图。
12
+ * type 为插槽名(layout / status / global 等)。
13
+ */
14
+ export interface SlotOptions extends SlotItem {
15
+ type: string;
16
+ /** @deprecated */
17
+ when?: () => boolean;
18
+ disabled?: () => boolean;
19
+ }
20
+
21
+ /**
22
+ * 具名插槽渲染组件(k-slot)。
23
+ *
24
+ * 合并两种来源的内容并按 order 降序渲染:
25
+ * - internal:模板里直接写在 k-slot 内的 k-slot-item 子节点;
26
+ * - external:由 ctx.slot() 注册到该插槽名的视图组件。
27
+ * single 模式下只渲染排序最高的一项(用于 activity 页面等独占插槽)。
28
+ */
29
+ export const KSlot = defineComponent({
30
+ props: {
31
+ /** 插槽名 */
32
+ name: String,
33
+ /** 透传给外部视图组件的属性 */
34
+ data: Object,
35
+ /** 是否单选模式(只渲染排序最高的一项) */
36
+ single: Boolean,
37
+ },
38
+ setup(props, { slots }) {
39
+ const ctx = useContext();
40
+ return () => {
41
+ // internal:模板内以 k-slot-item 声明的节点,order 取其 props
42
+ const internal = props.single
43
+ ? []
44
+ : [...(slots["default"]?.() || [])]
45
+ .filter((node) => node.type === KSlotItem)
46
+ .map((node) => ({ node, order: node.props?.["order"] || 0 }));
47
+ // external:ctx.slot() 注册的视图,disabled 的不渲染
48
+ const external = [
49
+ ...(props.name ? ctx.$router.views[props.name] || [] : []),
50
+ ]
51
+ .filter((item) => !item.disabled?.())
52
+ .map((item) => ({
53
+ node: h(item.component, { ...props.data }, slots),
54
+ order: item.order,
55
+ layer: 1,
56
+ }));
57
+ const children = [...internal, ...external].sort(
58
+ (a, b) => b.order - a.order,
59
+ );
60
+ if (props.single) {
61
+ // 无外部视图时回退到模板默认内容
62
+ return children[0]?.node || slots["default"]?.();
63
+ }
64
+ return children.map((item) => item.node);
65
+ };
66
+ },
67
+ });
68
+
69
+ /** 插槽条目包装组件(k-slot-item):仅透传 order 排序权重 */
70
+ const KSlotItem = defineComponent({
71
+ props: {
72
+ order: Number,
73
+ },
74
+ setup(_props, { slots }) {
75
+ return () => slots["default"]?.();
76
+ },
77
+ });
78
+
79
+ /** 生成绑定固定插槽名的单选 k-slot 快捷组件 */
80
+ function defineSlotComponent(name: string) {
81
+ return defineComponent({
82
+ inheritAttrs: false,
83
+ setup(_, { slots, attrs }) {
84
+ return () => h(KSlot, { name, data: attrs, single: true }, slots);
85
+ },
86
+ });
87
+ }
88
+
89
+ /** 注册插槽相关全局组件(k-slot / k-slot-item / k-layout / k-status) */
90
+ export default (app: App) => {
91
+ app.component("k-slot", KSlot);
92
+ app.component("k-slot-item", KSlotItem);
93
+ app.component("k-layout", defineSlotComponent("layout"));
94
+ app.component("k-status", defineSlotComponent("status"));
95
+ };
@@ -0,0 +1,143 @@
1
+ import * as cordis from "cordis";
2
+ import {
3
+ type App,
4
+ type Component,
5
+ createApp,
6
+ defineComponent,
7
+ h,
8
+ inject,
9
+ markRaw,
10
+ onBeforeUnmount,
11
+ provide,
12
+ type Ref,
13
+ resolveComponent,
14
+ } from "vue";
15
+ import ActionService from "./plugins/action";
16
+ import I18nService from "./plugins/i18n";
17
+ import LoaderService from "./plugins/loader";
18
+ import RouterService from "./plugins/router";
19
+ import SettingService from "./plugins/setting";
20
+ import ThemeService from "./plugins/theme";
21
+
22
+ // —— 布局相关的 Context 类型扩展 ——
23
+
24
+ /** 前端侧事件表:在 cordis 事件的基础上扩展本库自定义事件 */
25
+ export interface Events<C extends Context = Context> extends cordis.Events<C> {}
26
+
27
+ /**
28
+ * 前端 Context 接口:各服务插件通过 `declare module "../context"`
29
+ * 向本接口合并自身的方法与属性(如 `$router`、`page()` 等)。
30
+ */
31
+ export interface Context {
32
+ [Context.events]: Events<this>;
33
+ internal: Internal;
34
+ }
35
+
36
+ /**
37
+ * 在组件 setup 中获取与当前组件生命周期绑定的 Context。
38
+ *
39
+ * 实现:从父级注入根 Context,创建一个空的插件 fork,使得返回的
40
+ * 子上下文在组件卸载时(onBeforeUnmount)自动 dispose,从而组件内
41
+ * 通过它注册的副作用(effect / 事件监听等)会随组件销毁被清理。
42
+ */
43
+ export function useContext() {
44
+ const parent = inject("cordis") as Context;
45
+ const fork = parent.plugin(() => {});
46
+ onBeforeUnmount(() => fork.dispose());
47
+ return fork.ctx;
48
+ }
49
+
50
+ /**
51
+ * 获取当前扩展(extension)通过 RPC 携带的只读数据。
52
+ * 仅在由 loader 动态加载的扩展组件内可用。
53
+ */
54
+ export function useRpc<T>(): Ref<T> {
55
+ const parent = inject("cordis") as Context;
56
+ return parent.extension?.data as Ref<T>;
57
+ }
58
+
59
+ // 各服务通过 `declare module "../context"` 对 Internal 做接口合并,
60
+ // 因此这里必须是 interface 而非类型别名(空 interface 不能被 biome 自动
61
+ // 改写为 type alias,否则合并会变成 TS2300 重复标识符)
62
+ // biome-ignore lint/suspicious/noEmptyInterface: 需要保持 interface 以支持声明合并
63
+ export interface Internal {}
64
+
65
+ /**
66
+ * 前端根 Context:持有 Vue 根应用 `app`,构造时安装六个核心服务插件,
67
+ * 并在 `ready` 阶段(扩展入口加载完毕后)挂载到 `#app` 节点。
68
+ */
69
+ export class Context extends cordis.Context {
70
+ app: App;
71
+
72
+ constructor() {
73
+ super();
74
+ // cordis.Context 上对应字段不含 null(exactOptionalPropertyTypes),
75
+ // 运行时以 null 表示「尚未加载」,类型层面按已知模式收窄
76
+ this.extension = null as never;
77
+ this.internal = {} as Internal;
78
+ // Vue 根应用:渲染两个顶级插槽位——root(单选,承载 activity 页面)
79
+ // 与 global(多选,承载全局浮层如右键菜单、图片查看器)
80
+ this.app = createApp(
81
+ defineComponent({
82
+ setup: () => () => [
83
+ h(resolveComponent("k-slot"), { name: "root", single: true }),
84
+ h(resolveComponent("k-slot"), { name: "global" }),
85
+ ],
86
+ }),
87
+ );
88
+ // 向整棵组件树提供根 Context,供 useContext() 注入
89
+ this.app.provide("cordis", this);
90
+
91
+ this.plugin(ActionService);
92
+ this.plugin(I18nService);
93
+ this.plugin(LoaderService);
94
+ this.plugin(RouterService);
95
+ this.plugin(SettingService);
96
+ this.plugin(ThemeService);
97
+
98
+ this.on("ready", async () => {
99
+ // 先等待全部扩展入口就绪(i18n / router 依赖扩展注册的资源),
100
+ // 再依次挂载国际化与路由,最后把 Vue 应用渲染到页面上
101
+ await this.$loader.initTask;
102
+ this.app.use(this.$i18n.i18n);
103
+ this.app.use(this.$router.router);
104
+ this.app.mount("#app");
105
+ });
106
+ }
107
+
108
+ /**
109
+ * 注册 window 级事件监听,并纳入 cordis effect 管理:
110
+ * 当前作用域销毁时自动移除监听。
111
+ */
112
+ addEventListener<K extends keyof WindowEventMap>(
113
+ type: K,
114
+ listener: (this: Window, ev: WindowEventMap[K]) => unknown,
115
+ options?: boolean | AddEventListenerOptions,
116
+ ) {
117
+ return this.effect(() => {
118
+ window.addEventListener(type, listener, options);
119
+ return () => window.removeEventListener(type, listener, options);
120
+ });
121
+ }
122
+
123
+ /**
124
+ * 包装组件:为动态加载的组件提供独立的 cordis 注入源。
125
+ *
126
+ * 未加载任何扩展(extension 为 null)时原样返回;
127
+ * 否则包一层 wrapper 组件,将当前 ctx provide 为 "cordis",
128
+ * 使组件树内部通过 useContext() 拿到的是注册它时的上下文。
129
+ */
130
+ wrapComponent(component: Component | undefined) {
131
+ if (!component) return;
132
+ if (!this.extension) return component;
133
+ return defineComponent((props, { slots }) => {
134
+ provide("cordis", this);
135
+ return () => h(component, props, slots);
136
+ });
137
+ }
138
+ }
139
+
140
+ // cordis 的原型对象不参与 Vue 响应式代理,避免被 reactive 包裹后
141
+ // 触发不必要的依赖收集与性能开销
142
+ markRaw(cordis.Context.prototype);
143
+ markRaw(cordis.EffectScope.prototype);