@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.
- package/app/assets/logo.png +0 -0
- package/app/home/home.vue +51 -0
- package/app/home/index.ts +18 -0
- package/app/home/welcome.en-US.yml +9 -0
- package/app/home/welcome.vue +111 -0
- package/app/home/welcome.zh-CN.yml +9 -0
- package/app/index.html +13 -0
- package/app/index.scss +64 -0
- package/app/index.ts +32 -0
- package/app/layout/header.vue +187 -0
- package/app/layout/index.ts +15 -0
- package/app/layout/layout.vue +194 -0
- package/app/layout/menu-item.vue +60 -0
- package/app/settings/index.ts +39 -0
- package/app/settings/settings.vue +82 -0
- package/app/settings/theme.vue +123 -0
- package/app/shims.d.ts +1 -0
- package/app/status/index.ts +21 -0
- package/app/status/loading.vue +23 -0
- package/app/status/status.vue +56 -0
- package/app/styles/element.scss +217 -0
- package/app/styles/hc.scss +35 -0
- package/app/styles/index.scss +140 -0
- package/app/styles/index.ts +30 -0
- package/app/styles/layout.scss +92 -0
- package/app/theme/activity/button.vue +97 -0
- package/app/theme/activity/index.vue +119 -0
- package/app/theme/activity/item.vue +185 -0
- package/app/theme/activity/separator.vue +163 -0
- package/app/theme/activity/utils.ts +27 -0
- package/app/theme/blank.vue +24 -0
- package/app/theme/index.ts +37 -0
- package/app/theme/index.vue +34 -0
- package/app/theme/menu/index.vue +26 -0
- package/app/theme/menu/menu-item.vue +58 -0
- package/app/theme/menu/menu.vue +45 -0
- package/app/theme/status.vue +45 -0
- package/app/tsconfig.json +15 -0
- package/client/components/chat/image.vue +20 -0
- package/client/components/chat/overlay.vue +231 -0
- package/client/components/chat/utils.ts +6 -0
- package/client/components/common/index.ts +11 -0
- package/client/components/common/k-button.vue +127 -0
- package/client/components/common/k-hint.vue +62 -0
- package/client/components/common/k-tab.vue +50 -0
- package/client/components/dynamic.vue +61 -0
- package/client/components/icons/activity/default.vue +6 -0
- package/client/components/icons/activity/ellipsis.vue +6 -0
- package/client/components/icons/activity/home.vue +13 -0
- package/client/components/icons/activity/moon.vue +6 -0
- package/client/components/icons/activity/settings.vue +6 -0
- package/client/components/icons/activity/sun.vue +6 -0
- package/client/components/icons/index.ts +133 -0
- package/client/components/icons/style.scss +3 -0
- package/client/components/icons/svg/arrow-left.vue +6 -0
- package/client/components/icons/svg/arrow-right.vue +6 -0
- package/client/components/icons/svg/box-open.vue +6 -0
- package/client/components/icons/svg/check-full.vue +6 -0
- package/client/components/icons/svg/chevron-down.vue +6 -0
- package/client/components/icons/svg/chevron-left.vue +6 -0
- package/client/components/icons/svg/chevron-right.vue +6 -0
- package/client/components/icons/svg/chevron-up.vue +6 -0
- package/client/components/icons/svg/clipboard-list.vue +6 -0
- package/client/components/icons/svg/edit.vue +6 -0
- package/client/components/icons/svg/exclamation-full.vue +6 -0
- package/client/components/icons/svg/expand.vue +6 -0
- package/client/components/icons/svg/file-archive.vue +6 -0
- package/client/components/icons/svg/filter.vue +6 -0
- package/client/components/icons/svg/github.vue +6 -0
- package/client/components/icons/svg/gitlab.vue +6 -0
- package/client/components/icons/svg/info-full.vue +6 -0
- package/client/components/icons/svg/koishi.vue +6 -0
- package/client/components/icons/svg/link.vue +6 -0
- package/client/components/icons/svg/paper-plane.vue +6 -0
- package/client/components/icons/svg/question-empty.vue +6 -0
- package/client/components/icons/svg/redo.vue +6 -0
- package/client/components/icons/svg/search-minus.vue +6 -0
- package/client/components/icons/svg/search-plus.vue +6 -0
- package/client/components/icons/svg/search.vue +6 -0
- package/client/components/icons/svg/star-empty.vue +6 -0
- package/client/components/icons/svg/star-full.vue +6 -0
- package/client/components/icons/svg/start.vue +6 -0
- package/client/components/icons/svg/tag.vue +6 -0
- package/client/components/icons/svg/times-full.vue +6 -0
- package/client/components/icons/svg/tools.vue +6 -0
- package/client/components/icons/svg/undo.vue +6 -0
- package/client/components/icons/svg/user.vue +6 -0
- package/client/components/index.ts +75 -0
- package/client/components/layout/card.vue +69 -0
- package/client/components/layout/content.vue +37 -0
- package/client/components/layout/empty.vue +41 -0
- package/client/components/layout/index.ts +15 -0
- package/client/components/layout/tab-group.vue +45 -0
- package/client/components/layout/tab-item.vue +35 -0
- package/client/components/link.ts +39 -0
- package/client/components/perms.vue +73 -0
- package/client/components/slot.ts +95 -0
- package/client/context.ts +143 -0
- package/client/data.ts +178 -0
- package/client/index.ts +76 -0
- package/client/plugins/action.ts +251 -0
- package/client/plugins/i18n.ts +43 -0
- package/client/plugins/loader.ts +154 -0
- package/client/plugins/router.ts +278 -0
- package/client/plugins/setting.ts +217 -0
- package/client/plugins/theme.ts +134 -0
- package/client/shims.d.ts +55 -0
- package/client/tsconfig.json +15 -0
- package/client/utils.ts +36 -0
- package/global.d.ts +24 -0
- package/lib/bin.d.ts +1 -0
- package/lib/bin.mjs +112 -0
- package/lib/client-CaTn_gVG.mjs +125 -0
- package/lib/index.d.ts +20 -0
- package/lib/index.mjs +2 -0
- package/lib/src-Bxzzi1Rx.mjs +160 -0
- package/lib/yaml-UzQbvquH.mjs +23 -0
- package/package.json +66 -0
- package/src/bin.ts +61 -0
- package/src/index.ts +280 -0
- package/src/yaml.ts +22 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
单个浮动上下文菜单(页面模板中 <k-menu> 的内置实现):
|
|
3
|
+
依据触发点的锚点矩形(props.relative)与自身实际尺寸,
|
|
4
|
+
逐轴选择向左 / 向右、向上 / 向下展开,避免菜单溢出视口。
|
|
5
|
+
-->
|
|
6
|
+
<template>
|
|
7
|
+
<div ref="el" class="k-menu" :style="getStyle()">
|
|
8
|
+
<template v-for="item of ctx.internal.menus[id]">
|
|
9
|
+
<div class="k-menu-separator" v-if="item.id === '@separator'"></div>
|
|
10
|
+
<menu-item v-else v-bind="{ prefix: id, ...item }"></menu-item>
|
|
11
|
+
</template>
|
|
12
|
+
</div>
|
|
13
|
+
</template>
|
|
14
|
+
|
|
15
|
+
<script lang="ts" setup>
|
|
16
|
+
import { type ActiveMenu, useContext } from "@koishi-ce/client";
|
|
17
|
+
import { ref } from "vue";
|
|
18
|
+
import MenuItem from "./menu-item.vue";
|
|
19
|
+
|
|
20
|
+
const props = defineProps<ActiveMenu>();
|
|
21
|
+
|
|
22
|
+
const ctx = useContext();
|
|
23
|
+
|
|
24
|
+
const el = ref<HTMLElement>();
|
|
25
|
+
|
|
26
|
+
// relative 携带触发点矩形的 left / right / bottom(相对视口),
|
|
27
|
+
// 结合菜单自身渲染后的实际宽高计算定位;尚未挂载时先隐藏避免闪烁
|
|
28
|
+
const getStyle = () => {
|
|
29
|
+
if (!el.value) return { visibility: "hidden" };
|
|
30
|
+
const { height, width } = el.value.getBoundingClientRect();
|
|
31
|
+
const style: Partial<Record<"top" | "right" | "bottom" | "left", string>> =
|
|
32
|
+
{};
|
|
33
|
+
if (props.relative.right + width > window.innerWidth) {
|
|
34
|
+
style.right = `${window.innerWidth - props.relative.left}px`;
|
|
35
|
+
} else {
|
|
36
|
+
style.left = `${props.relative.right}px`;
|
|
37
|
+
}
|
|
38
|
+
if (props.relative.bottom + height > window.innerHeight) {
|
|
39
|
+
style.bottom = `0px`;
|
|
40
|
+
} else {
|
|
41
|
+
style.top = `${props.relative.bottom}px`;
|
|
42
|
+
}
|
|
43
|
+
return style;
|
|
44
|
+
};
|
|
45
|
+
</script>
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
底部状态栏容器:左右两组插槽(status-left / status-right),
|
|
3
|
+
移动端(<=768px)整条隐藏。
|
|
4
|
+
-->
|
|
5
|
+
<template>
|
|
6
|
+
<footer class="layout-status">
|
|
7
|
+
<div><k-slot name="status-left"></k-slot></div>
|
|
8
|
+
<div><k-slot name="status-right"></k-slot></div>
|
|
9
|
+
</footer>
|
|
10
|
+
</template>
|
|
11
|
+
|
|
12
|
+
<style lang="scss">
|
|
13
|
+
|
|
14
|
+
.layout-status {
|
|
15
|
+
position: fixed;
|
|
16
|
+
box-sizing: border-box;
|
|
17
|
+
z-index: 50;
|
|
18
|
+
bottom: 0;
|
|
19
|
+
height: var(--footer-height);
|
|
20
|
+
left: var(--activity-width);
|
|
21
|
+
right: 0;
|
|
22
|
+
background-color: var(--k-status-bg, var(--k-side-bg));
|
|
23
|
+
display: flex;
|
|
24
|
+
flex-direction: row;
|
|
25
|
+
justify-content: space-between;
|
|
26
|
+
border-top: var(--k-status-divider, var(--k-color-divider-dark)) 1px solid;
|
|
27
|
+
transition: var(--color-transition);
|
|
28
|
+
font-size: 0.875rem;
|
|
29
|
+
line-height: var(--footer-height);
|
|
30
|
+
|
|
31
|
+
@media screen and (max-width: 768px) {
|
|
32
|
+
display: none;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
> * {
|
|
36
|
+
display: inline-flex;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.el-progress-bar {
|
|
40
|
+
width: 120px;
|
|
41
|
+
margin-right: 2px;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
</style>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../../../tsconfig.client",
|
|
3
|
+
"include": ["."],
|
|
4
|
+
"compilerOptions": {
|
|
5
|
+
"alwaysStrict": true,
|
|
6
|
+
"noPropertyAccessFromIndexSignature": true,
|
|
7
|
+
"noUnusedLocals": true,
|
|
8
|
+
"noUnusedParameters": true,
|
|
9
|
+
"noImplicitReturns": true,
|
|
10
|
+
"noFallthroughCasesInSwitch": true,
|
|
11
|
+
"noImplicitOverride": true,
|
|
12
|
+
"allowUnreachableCode": false,
|
|
13
|
+
"allowUnusedLabels": false
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
chat-image:聊天消息中的图片。
|
|
3
|
+
普通点击打开内置大图查看器(overlay.vue,经 global 插槽全局挂载);
|
|
4
|
+
按住 meta 键(macOS 为 Cmd)点击则在新标签页直接打开原图。
|
|
5
|
+
-->
|
|
6
|
+
<template>
|
|
7
|
+
<img class="chat-image" :src="src" @click="handleClick"/>
|
|
8
|
+
</template>
|
|
9
|
+
|
|
10
|
+
<script lang="ts" setup>
|
|
11
|
+
import { shared } from "./utils";
|
|
12
|
+
|
|
13
|
+
const props = defineProps<{ src: string }>();
|
|
14
|
+
|
|
15
|
+
function handleClick(ev: MouseEvent) {
|
|
16
|
+
ev.preventDefault();
|
|
17
|
+
if (ev.metaKey) return window.open(props.src, "_blank");
|
|
18
|
+
shared.overlayImage = ev.target as HTMLImageElement;
|
|
19
|
+
}
|
|
20
|
+
</script>
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
overlay.vue:全屏图片查看器(经 router 的 global 插槽挂载)。
|
|
3
|
+
打开时把图片从原位"飞入"屏幕中心(appear 阶段从原坐标过渡到居中),
|
|
4
|
+
关闭时回到原坐标;支持放大/缩小/旋转/复原,左右切换页面内相邻图片,
|
|
5
|
+
键盘操作:方向键切换、Esc 关闭、Enter 关闭并滚动定位到原图片。
|
|
6
|
+
-->
|
|
7
|
+
<template>
|
|
8
|
+
<transition name="overlay">
|
|
9
|
+
<div class="overlay-image-viewer" v-if="shared.overlayImage" @click="setImage(null)">
|
|
10
|
+
<span class="button left" :class="{ disabled: !siblings.prev }" @click.stop="setImage(siblings.prev)">
|
|
11
|
+
<k-icon name="chevron-left"/>
|
|
12
|
+
</span>
|
|
13
|
+
<span class="button right" :class="{ disabled: !siblings.next }" @click.stop="setImage(siblings.next)">
|
|
14
|
+
<k-icon name="chevron-right"/>
|
|
15
|
+
</span>
|
|
16
|
+
<span class="button bottom" @click.stop>
|
|
17
|
+
<el-tooltip placement="top" content="缩小" :offset="20">
|
|
18
|
+
<k-icon name="search-minus" @click="scale -= 0.2"/>
|
|
19
|
+
</el-tooltip>
|
|
20
|
+
<el-tooltip placement="top" content="放大" :offset="20">
|
|
21
|
+
<k-icon name="search-plus" @click="scale += 0.2"/>
|
|
22
|
+
</el-tooltip>
|
|
23
|
+
<el-tooltip placement="top" content="复原" :offset="20">
|
|
24
|
+
<k-icon name="expand" @click="scale = 1, rotate = 0"/>
|
|
25
|
+
</el-tooltip>
|
|
26
|
+
<el-tooltip placement="top" content="逆时针旋转" :offset="20">
|
|
27
|
+
<k-icon name="undo" @click="rotate -= 90"/>
|
|
28
|
+
</el-tooltip>
|
|
29
|
+
<el-tooltip placement="top" content="逆时针旋转" :offset="20">
|
|
30
|
+
<k-icon name="redo" @click="rotate += 90"/>
|
|
31
|
+
</el-tooltip>
|
|
32
|
+
</span>
|
|
33
|
+
<transition appear :duration="1" @before-appear="moveToOrigin" @after-appear="moveToCenter">
|
|
34
|
+
<img ref="img" :style="{ transform }" :src="shared.overlayImage.src"/>
|
|
35
|
+
</transition>
|
|
36
|
+
</div>
|
|
37
|
+
</transition>
|
|
38
|
+
</template>
|
|
39
|
+
|
|
40
|
+
<script lang="ts" setup>
|
|
41
|
+
import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue";
|
|
42
|
+
import { shared } from "./utils";
|
|
43
|
+
|
|
44
|
+
// 用户手动调整的缩放与旋转量(复原即回到 1 / 0)
|
|
45
|
+
const scale = ref(1);
|
|
46
|
+
const rotate = ref(0);
|
|
47
|
+
const img = ref<HTMLImageElement>(null);
|
|
48
|
+
|
|
49
|
+
const transform = computed(() => {
|
|
50
|
+
return `scale(${scale.value}) rotate(${rotate.value}deg)`;
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// 相邻图片:以文档中 .chat-image(chat/image.vue 渲染)的出现顺序为准
|
|
54
|
+
const siblings = computed(() => {
|
|
55
|
+
if (!shared.overlayImage) return;
|
|
56
|
+
const elements = Array.from(
|
|
57
|
+
document.querySelectorAll<HTMLImageElement>(".chat-image"),
|
|
58
|
+
);
|
|
59
|
+
const index = elements.indexOf(shared.overlayImage);
|
|
60
|
+
return {
|
|
61
|
+
prev: elements[index - 1],
|
|
62
|
+
next: elements[index + 1],
|
|
63
|
+
};
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// 初始(适配屏幕)缩放比:按视口剩余空间把图片等比缩小,不放大
|
|
67
|
+
const defaultScale = computed(() => {
|
|
68
|
+
const { naturalHeight, naturalWidth } = shared.overlayImage;
|
|
69
|
+
const maxHeight = innerHeight - paddingVertical * 2;
|
|
70
|
+
const maxWidth = innerWidth - paddingHorizontal * 2;
|
|
71
|
+
return Math.min(1, maxHeight / naturalHeight, maxWidth / naturalWidth);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// 切换/关闭查看器时重置手动缩放与旋转;
|
|
75
|
+
// 关闭(el 为空)时把图片移回原位,切换则平滑过渡到新图居中
|
|
76
|
+
watch(
|
|
77
|
+
() => shared.overlayImage,
|
|
78
|
+
(el, origin) => {
|
|
79
|
+
scale.value = 1;
|
|
80
|
+
rotate.value = 0;
|
|
81
|
+
if (!el) return moveToOrigin(img.value, origin);
|
|
82
|
+
if (img.value) {
|
|
83
|
+
img.value.style.transition = "0.3s transform ease";
|
|
84
|
+
moveToCenter(img.value);
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
function setImage(el: HTMLImageElement) {
|
|
90
|
+
if (el === undefined) return;
|
|
91
|
+
shared.overlayImage = el;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** 把大图元素摆到原图片所在的位置与尺寸(关闭时的"飞回"动画终点) */
|
|
95
|
+
function moveToOrigin(el: HTMLImageElement, origin = shared.overlayImage) {
|
|
96
|
+
const { height, width } = origin;
|
|
97
|
+
const { left, top } = origin.getBoundingClientRect();
|
|
98
|
+
el.style.width = `${width}px`;
|
|
99
|
+
el.style.height = `${height}px`;
|
|
100
|
+
el.style.left = `${left}px`;
|
|
101
|
+
el.style.top = `${top}px`;
|
|
102
|
+
el.style.transition = "0.3s ease";
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// 视口四周预留的边距(当前为 0,即允许图片占满视口)
|
|
106
|
+
const paddingVertical = 0;
|
|
107
|
+
const paddingHorizontal = 0;
|
|
108
|
+
|
|
109
|
+
/** 把图片按适配缩放比居中摆放到视口中央 */
|
|
110
|
+
function moveToCenter(el: HTMLImageElement) {
|
|
111
|
+
const { naturalHeight, naturalWidth } = shared.overlayImage;
|
|
112
|
+
const scale = defaultScale.value;
|
|
113
|
+
const width = naturalWidth * scale;
|
|
114
|
+
const height = naturalHeight * scale;
|
|
115
|
+
el.style.width = `${width}px`;
|
|
116
|
+
el.style.height = `${height}px`;
|
|
117
|
+
el.style.left = `${(innerWidth - width) / 2}px`;
|
|
118
|
+
el.style.top = `${(innerHeight - height) / 2}px`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
onMounted(() => {
|
|
122
|
+
document.addEventListener("keydown", onKeyDown);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
onBeforeUnmount(() => {
|
|
126
|
+
document.removeEventListener("keydown", onKeyDown);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// 全局键盘操作:查看器打开期间接管方向键 / Esc / Enter
|
|
130
|
+
function onKeyDown(ev: KeyboardEvent) {
|
|
131
|
+
if (!shared.overlayImage) return;
|
|
132
|
+
ev.preventDefault();
|
|
133
|
+
if (ev.key === "ArrowUp" || ev.key === "ArrowLeft") {
|
|
134
|
+
setImage(siblings.value.prev);
|
|
135
|
+
} else if (ev.key === "ArrowDown" || ev.key === "ArrowRight") {
|
|
136
|
+
setImage(siblings.value.next);
|
|
137
|
+
} else if (ev.key === "Escape") {
|
|
138
|
+
setImage(null);
|
|
139
|
+
} else if (ev.key === "Enter") {
|
|
140
|
+
// 关闭并把页面滚动到原图所在位置,便于继续浏览消息
|
|
141
|
+
shared.overlayImage.offsetParent.scrollIntoView({ behavior: "smooth" });
|
|
142
|
+
setImage(null);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
</script>
|
|
146
|
+
|
|
147
|
+
<style lang="scss">
|
|
148
|
+
|
|
149
|
+
@use "sass:math";
|
|
150
|
+
|
|
151
|
+
$buttonSize: 3rem;
|
|
152
|
+
$buttonBg: #303133;
|
|
153
|
+
|
|
154
|
+
.overlay-enter-from, .overlay-leave-to {
|
|
155
|
+
opacity: 0;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.overlay-enter, .overlay-leave {
|
|
159
|
+
opacity: 1;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.overlay-image-viewer {
|
|
163
|
+
position: fixed;
|
|
164
|
+
left: 0;
|
|
165
|
+
bottom: 0;
|
|
166
|
+
top: 0;
|
|
167
|
+
right: 0;
|
|
168
|
+
z-index: 1000;
|
|
169
|
+
transition: 0.4s opacity ease;
|
|
170
|
+
user-select: none;
|
|
171
|
+
background-color: #0006;
|
|
172
|
+
|
|
173
|
+
.button {
|
|
174
|
+
position: absolute;
|
|
175
|
+
border-radius: $buttonSize;
|
|
176
|
+
cursor: pointer;
|
|
177
|
+
user-select: none;
|
|
178
|
+
opacity: 0.5;
|
|
179
|
+
font-size: math.div($buttonSize, 2);
|
|
180
|
+
height: $buttonSize;
|
|
181
|
+
background-color: $buttonBg;
|
|
182
|
+
display: flex;
|
|
183
|
+
align-items: center;
|
|
184
|
+
justify-content: space-evenly;
|
|
185
|
+
transition: 0.4s ease;
|
|
186
|
+
|
|
187
|
+
.k-icon {
|
|
188
|
+
transition: 0.4s ease;
|
|
189
|
+
height: 1.25rem;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
&:not(.disabled):hover {
|
|
193
|
+
opacity: 0.8;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
&:not(.disabled) .k-icon:hover {
|
|
197
|
+
color: rgba(244, 244, 245, .8);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
@each $tag in left, right {
|
|
201
|
+
&.#{$tag} {
|
|
202
|
+
top: 50%;
|
|
203
|
+
z-index: 2000;
|
|
204
|
+
transform: translateY(-50%);
|
|
205
|
+
width: $buttonSize;
|
|
206
|
+
#{$tag}: $buttonSize;
|
|
207
|
+
.k-icon {
|
|
208
|
+
margin-#{$tag}: -3px;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
&.bottom {
|
|
214
|
+
z-index: 2000;
|
|
215
|
+
bottom: $buttonSize;
|
|
216
|
+
width: $buttonSize * 6;
|
|
217
|
+
left: 50%;
|
|
218
|
+
transform: translateX(-50%);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
&.disabled {
|
|
222
|
+
cursor: not-allowed;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
img {
|
|
227
|
+
position: absolute;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
</style>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { App } from "vue";
|
|
2
|
+
import Button from "./k-button.vue";
|
|
3
|
+
import Hint from "./k-hint.vue";
|
|
4
|
+
import Tab from "./k-tab.vue";
|
|
5
|
+
|
|
6
|
+
/** 注册公共基础组件(k-button / k-hint / k-tab) */
|
|
7
|
+
export default function (app: App) {
|
|
8
|
+
app.component("k-button", Button);
|
|
9
|
+
app.component("k-hint", Hint);
|
|
10
|
+
app.component("k-tab", Tab);
|
|
11
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
k-button:通用按钮组件。
|
|
3
|
+
通过 type(primary / warning / success / error)着色,支持 solid(实心)、
|
|
4
|
+
frameless(无边框)、round(圆形)与 disabled(禁用)等形态;
|
|
5
|
+
禁用时不触发 click 事件、不显示 title 提示。
|
|
6
|
+
-->
|
|
7
|
+
<template>
|
|
8
|
+
<button
|
|
9
|
+
@click.prevent="onClick"
|
|
10
|
+
:title="disabled ? '' : title"
|
|
11
|
+
:class="['k-button', type, { disabled, solid, round, frameless }]">
|
|
12
|
+
<slot/>
|
|
13
|
+
</button>
|
|
14
|
+
</template>
|
|
15
|
+
|
|
16
|
+
<script lang="ts" setup>
|
|
17
|
+
const props = defineProps({
|
|
18
|
+
type: {
|
|
19
|
+
type: String,
|
|
20
|
+
default: "primary",
|
|
21
|
+
},
|
|
22
|
+
solid: Boolean,
|
|
23
|
+
frameless: Boolean,
|
|
24
|
+
title: String,
|
|
25
|
+
round: Boolean,
|
|
26
|
+
disabled: Boolean,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const emit = defineEmits(["click"]);
|
|
30
|
+
|
|
31
|
+
function onClick(event: MouseEvent) {
|
|
32
|
+
// 禁用态吞掉点击,不向外发 click 事件
|
|
33
|
+
if (props.disabled) return;
|
|
34
|
+
emit("click", event);
|
|
35
|
+
}
|
|
36
|
+
</script>
|
|
37
|
+
|
|
38
|
+
<style lang="scss">
|
|
39
|
+
|
|
40
|
+
@mixin apply-color($name) {
|
|
41
|
+
&.#{$name} {
|
|
42
|
+
background-color: var(--#{$name}) !important;
|
|
43
|
+
&:hover {
|
|
44
|
+
background-color: var(--#{$name}-tint) !important;
|
|
45
|
+
}
|
|
46
|
+
&:active {
|
|
47
|
+
background-color: var(--#{$name}-shade) !important;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.k-button {
|
|
53
|
+
font-size: 14px;
|
|
54
|
+
font-weight: bolder;
|
|
55
|
+
line-height: 20px;
|
|
56
|
+
appearance: none;
|
|
57
|
+
user-select: none;
|
|
58
|
+
border: none;
|
|
59
|
+
border-radius: 0.4em;
|
|
60
|
+
cursor: pointer;
|
|
61
|
+
padding: 0.4em 1em;
|
|
62
|
+
transition: var(--color-transition);
|
|
63
|
+
display: inline-block;
|
|
64
|
+
|
|
65
|
+
// 默认形态:透明背景 + 描边
|
|
66
|
+
color: var(--el-button-text-color);
|
|
67
|
+
border: 1px solid var(--k-color-border);
|
|
68
|
+
background-color: transparent;
|
|
69
|
+
|
|
70
|
+
&.round {
|
|
71
|
+
border-radius: 50%;
|
|
72
|
+
}
|
|
73
|
+
&:focus {
|
|
74
|
+
outline: 0;
|
|
75
|
+
}
|
|
76
|
+
&.disabled {
|
|
77
|
+
cursor: default;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
&.disabled {
|
|
81
|
+
color: var(--k-color-disabled);
|
|
82
|
+
border-color: var(--k-color-border);
|
|
83
|
+
}
|
|
84
|
+
&:hover:not(.disabled) {
|
|
85
|
+
color: var(--k-text-dark);
|
|
86
|
+
border: 1px solid var(--k-color-border);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
&.solid {
|
|
90
|
+
color: #ffffff !important;
|
|
91
|
+
border-color: transparent !important;
|
|
92
|
+
&.disabled {
|
|
93
|
+
background-color: var(--k-color-disabled) !important;
|
|
94
|
+
}
|
|
95
|
+
&:not(.disabled) {
|
|
96
|
+
@include apply-color(primary);
|
|
97
|
+
@include apply-color(warning);
|
|
98
|
+
@include apply-color(success);
|
|
99
|
+
@include apply-color(error);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 无边框形态:常用于行内文字按钮
|
|
104
|
+
&.frameless {
|
|
105
|
+
padding: 0;
|
|
106
|
+
border-color: transparent;
|
|
107
|
+
background-color: transparent;
|
|
108
|
+
&:hover {
|
|
109
|
+
border-color: transparent;
|
|
110
|
+
background-color: transparent;
|
|
111
|
+
}
|
|
112
|
+
&.primary {
|
|
113
|
+
color: var(--el-color-primary);
|
|
114
|
+
}
|
|
115
|
+
&.error {
|
|
116
|
+
color: var(--el-color-error);
|
|
117
|
+
}
|
|
118
|
+
&.success {
|
|
119
|
+
color: var(--el-color-success);
|
|
120
|
+
}
|
|
121
|
+
&.disabled {
|
|
122
|
+
color: var(--k-color-disabled);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
</style>
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
k-hint:带悬浮提示(tooltip)的问号图标。
|
|
3
|
+
默认是纯展示的提示图标;当传入 v-model(modelValue 有值)时变为
|
|
4
|
+
可点击的开关图标(常用于控制帮助文案或更多信息的显隐)。
|
|
5
|
+
-->
|
|
6
|
+
<template>
|
|
7
|
+
<el-tooltip :placement="placement">
|
|
8
|
+
<template #content>
|
|
9
|
+
<div class="k-hint-content">
|
|
10
|
+
<slot></slot>
|
|
11
|
+
</div>
|
|
12
|
+
</template>
|
|
13
|
+
<span class="k-hint" :class="{ active: modelValue, pointer }" @click="onClick">
|
|
14
|
+
<k-icon :name="name"/>
|
|
15
|
+
</span>
|
|
16
|
+
</el-tooltip>
|
|
17
|
+
</template>
|
|
18
|
+
|
|
19
|
+
<script lang="ts" setup>
|
|
20
|
+
import { computed } from "vue";
|
|
21
|
+
|
|
22
|
+
const props = defineProps({
|
|
23
|
+
placement: String,
|
|
24
|
+
modelValue: {},
|
|
25
|
+
name: { type: String, default: "question-empty" },
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const emit = defineEmits(["update:modelValue"]);
|
|
29
|
+
|
|
30
|
+
// modelValue 被显式传入时才呈现手型光标并响应点击(开关语义)
|
|
31
|
+
const pointer = computed(() => props.modelValue !== undefined);
|
|
32
|
+
|
|
33
|
+
function onClick() {
|
|
34
|
+
if (!pointer.value) return;
|
|
35
|
+
emit("update:modelValue", !props.modelValue);
|
|
36
|
+
}
|
|
37
|
+
</script>
|
|
38
|
+
|
|
39
|
+
<style lang="scss">
|
|
40
|
+
|
|
41
|
+
.k-hint {
|
|
42
|
+
opacity: 0.5;
|
|
43
|
+
color: var(--k-text-normal);
|
|
44
|
+
margin-left: 0.25rem;
|
|
45
|
+
transition: 0.3s ease;
|
|
46
|
+
vertical-align: -2px;
|
|
47
|
+
|
|
48
|
+
&:hover, &.active {
|
|
49
|
+
opacity: 1;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.k-hint.pointer {
|
|
54
|
+
cursor: pointer;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.k-hint-content {
|
|
58
|
+
line-height: 1.5;
|
|
59
|
+
max-width: 20rem;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
</style>
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
k-tab:水平选项卡(文字式切换器)。
|
|
3
|
+
data 为 "键 → 文案" 映射,点击某项时通过 v-model 更新选中键;
|
|
4
|
+
相邻项之间以 "|" 分隔,选中项高亮。
|
|
5
|
+
-->
|
|
6
|
+
<template>
|
|
7
|
+
<span class="k-horizontal-tab">
|
|
8
|
+
<span class="k-horizontal-tab-item"
|
|
9
|
+
v-for="(label, key) in data" :key="key"
|
|
10
|
+
:class="{ active: modelValue === key }"
|
|
11
|
+
@click="$emit('update:modelValue', key)">{{ label }}</span>
|
|
12
|
+
</span>
|
|
13
|
+
</template>
|
|
14
|
+
|
|
15
|
+
<script lang="ts" setup>
|
|
16
|
+
defineProps<{
|
|
17
|
+
data: Record<string | number, string>;
|
|
18
|
+
modelValue: string | number;
|
|
19
|
+
}>();
|
|
20
|
+
|
|
21
|
+
defineEmits(["update:modelValue"]);
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<style lang="scss">
|
|
25
|
+
|
|
26
|
+
.k-horizontal-tab-item {
|
|
27
|
+
cursor: pointer;
|
|
28
|
+
position: relative;
|
|
29
|
+
color: var(--k-color-disabled);
|
|
30
|
+
transition: color 0.3s ease;
|
|
31
|
+
|
|
32
|
+
& + & {
|
|
33
|
+
margin-left: 2rem;
|
|
34
|
+
|
|
35
|
+
&::before {
|
|
36
|
+
content: '|';
|
|
37
|
+
color: var(--k-color-disabled);
|
|
38
|
+
left: -1rem;
|
|
39
|
+
position: absolute;
|
|
40
|
+
transition: color 0.3s ease;
|
|
41
|
+
transform: translateX(-50%);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
&.active {
|
|
46
|
+
color: var(--fg1);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
</style>
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
dynamic.vue:动态 schema 表单控件(any + dynamic 角色)。
|
|
3
|
+
适用于"具体结构由服务端决定"的配置项:本地 schema 只带 meta.extra.name,
|
|
4
|
+
实际 schema 由服务端经 schema 数据服务下发(store.schema),
|
|
5
|
+
在此"水合"为完整 Schema 后转交 k-schema 渲染;
|
|
6
|
+
未取到时回退到 schema-base 按本地(不完整)schema 渲染。
|
|
7
|
+
-->
|
|
8
|
+
<template>
|
|
9
|
+
<k-schema
|
|
10
|
+
v-if="inner"
|
|
11
|
+
:modelValue="modelValue"
|
|
12
|
+
@update:modelValue="$emit('update:modelValue', $event)"
|
|
13
|
+
:schema="{ ...inner, meta: { ...schema.meta, ...inner.meta } }"
|
|
14
|
+
:initial="initial"
|
|
15
|
+
:disabled="disabled"
|
|
16
|
+
:prefix="prefix"
|
|
17
|
+
>
|
|
18
|
+
<template #title><slot name="title"></slot></template>
|
|
19
|
+
<template #prefix><slot name="prefix"></slot></template>
|
|
20
|
+
<template #suffix><slot name="suffix"></slot></template>
|
|
21
|
+
</k-schema>
|
|
22
|
+
|
|
23
|
+
<schema-base
|
|
24
|
+
v-else
|
|
25
|
+
:modelValue="modelValue"
|
|
26
|
+
@update:modelValue="$emit('update:modelValue', $event)"
|
|
27
|
+
:schema="schema"
|
|
28
|
+
:initial="initial"
|
|
29
|
+
:disabled="disabled"
|
|
30
|
+
:prefix="prefix"
|
|
31
|
+
>
|
|
32
|
+
<template #title><slot name="title"></slot></template>
|
|
33
|
+
<template #desc><slot name="desc"></slot></template>
|
|
34
|
+
<template #menu><slot name="menu"></slot></template>
|
|
35
|
+
</schema-base>
|
|
36
|
+
</template>
|
|
37
|
+
|
|
38
|
+
<script setup lang="ts">
|
|
39
|
+
import { Schema, SchemaBase } from "@koishi-ce/components";
|
|
40
|
+
import { computed, type PropType } from "vue";
|
|
41
|
+
import { store } from "../data";
|
|
42
|
+
|
|
43
|
+
const props = defineProps({
|
|
44
|
+
schema: {} as PropType<Schema>,
|
|
45
|
+
modelValue: {} as PropType<unknown>,
|
|
46
|
+
disabled: {} as PropType<boolean>,
|
|
47
|
+
prefix: {} as PropType<string>,
|
|
48
|
+
initial: {} as PropType<unknown>,
|
|
49
|
+
extra: {} as PropType<unknown>,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
defineEmits(["update:modelValue"]);
|
|
53
|
+
|
|
54
|
+
// 按本地 schema 标注的名称,从服务端下发的 schema 仓库中取出完整定义;
|
|
55
|
+
// 元信息做一层合并,保留本地的 meta 覆盖权
|
|
56
|
+
const inner = computed(() => {
|
|
57
|
+
const hydrated = store.schema?.[props.schema?.meta.extra?.name];
|
|
58
|
+
return hydrated && new Schema(hydrated);
|
|
59
|
+
});
|
|
60
|
+
</script>
|
|
61
|
+
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<!-- activity 侧栏页面默认图标(注册名 "activity:default"):仪表盘样式 -->
|
|
2
|
+
<template>
|
|
3
|
+
<svg class="k-icon" viewBox="0 0 448 512" xmlns="http://www.w3.org/2000/svg">
|
|
4
|
+
<path fill="currentColor" d="M0 80C0 53.49 21.49 32 48 32H144C170.5 32 192 53.49 192 80V176C192 202.5 170.5 224 144 224H48C21.49 224 0 202.5 0 176V80zM32 80V176C32 184.8 39.16 192 48 192H144C152.8 192 160 184.8 160 176V80C160 71.16 152.8 64 144 64H48C39.16 64 32 71.16 32 80zM0 336C0 309.5 21.49 288 48 288H144C170.5 288 192 309.5 192 336V432C192 458.5 170.5 480 144 480H48C21.49 480 0 458.5 0 432V336zM32 336V432C32 440.8 39.16 448 48 448H144C152.8 448 160 440.8 160 432V336C160 327.2 152.8 320 144 320H48C39.16 320 32 327.2 32 336zM400 32C426.5 32 448 53.49 448 80V176C448 202.5 426.5 224 400 224H304C277.5 224 256 202.5 256 176V80C256 53.49 277.5 32 304 32H400zM400 64H304C295.2 64 288 71.16 288 80V176C288 184.8 295.2 192 304 192H400C408.8 192 416 184.8 416 176V80C416 71.16 408.8 64 400 64zM256 336C256 309.5 277.5 288 304 288H400C426.5 288 448 309.5 448 336V432C448 458.5 426.5 480 400 480H304C277.5 480 256 458.5 256 432V336zM288 336V432C288 440.8 295.2 448 304 448H400C408.8 448 416 440.8 416 432V336C416 327.2 408.8 320 400 320H304C295.2 320 288 327.2 288 336z"/>
|
|
5
|
+
</svg>
|
|
6
|
+
</template>
|