@coffic/cosy-ui 0.8.25 → 0.8.27
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/dist/app.css +1 -1
- package/dist/index-astro.ts +1 -0
- package/dist/index-vue.ts +18 -0
- package/dist/src-astro/alert/Alert.astro +45 -5
- package/dist/src-astro/badge/Badge.astro +67 -0
- package/dist/src-astro/badge/index.ts +1 -0
- package/dist/src-vue/alert/Alert.vue +65 -39
- package/dist/src-vue/badge/Badge.vue +66 -0
- package/dist/src-vue/badge/index.ts +1 -0
- package/dist/src-vue/card/Card.vue +52 -0
- package/dist/src-vue/card/CardCourse.vue +34 -0
- package/dist/src-vue/card/index.ts +2 -0
- package/dist/src-vue/key-catcher/KeyCatcher.vue +121 -0
- package/dist/src-vue/key-catcher/index.ts +1 -0
- package/dist/src-vue/progress/Progress.vue +68 -0
- package/dist/src-vue/progress/index.ts +1 -0
- package/dist/src-vue/status-bar/StatusBar.vue +101 -0
- package/dist/src-vue/status-bar/StatusBarItem.vue +97 -0
- package/dist/src-vue/status-bar/index.ts +2 -0
- package/dist/src-vue/tool-bar/ToolBar.vue +95 -0
- package/dist/src-vue/tool-bar/index.ts +1 -0
- package/package.json +1 -1
package/dist/index-astro.ts
CHANGED
@@ -2,6 +2,7 @@
|
|
2
2
|
export * from './src-astro/alert';
|
3
3
|
export * from './src-astro/article';
|
4
4
|
export * from './src-astro/banner';
|
5
|
+
export * from './src-astro/badge';
|
5
6
|
export * from './src-astro/button/index_astro';
|
6
7
|
export * from './src-astro/card';
|
7
8
|
export * from './src-astro/code-block';
|
package/dist/index-vue.ts
CHANGED
@@ -4,12 +4,18 @@ export * from './src-vue/alert-dialog/index';
|
|
4
4
|
// Alert
|
5
5
|
export * from './src-vue/alert/index';
|
6
6
|
|
7
|
+
// Badge
|
8
|
+
export * from './src-vue/badge/index';
|
9
|
+
|
7
10
|
// Banner
|
8
11
|
export * from './src-vue/banner-box/index';
|
9
12
|
|
10
13
|
// BlogList
|
11
14
|
export * from './src-vue/blog/index';
|
12
15
|
|
16
|
+
// Card
|
17
|
+
export * from './src-vue/card/index';
|
18
|
+
|
13
19
|
// Container
|
14
20
|
export * from './src-vue/container/index';
|
15
21
|
|
@@ -25,9 +31,21 @@ export * from './src-vue/buttons/index';
|
|
25
31
|
// Icons
|
26
32
|
export * from './src-vue/icons/index';
|
27
33
|
|
34
|
+
// KeyCatcher
|
35
|
+
export * from './src-vue/key-catcher/index';
|
36
|
+
|
28
37
|
// List
|
29
38
|
export * from './src-vue/list/index';
|
30
39
|
|
40
|
+
// Progress
|
41
|
+
export * from './src-vue/progress/index';
|
42
|
+
|
43
|
+
// StatusBar
|
44
|
+
export * from './src-vue/status-bar/index';
|
45
|
+
|
46
|
+
// ToolBar
|
47
|
+
export * from './src-vue/tool-bar/index';
|
48
|
+
|
31
49
|
// Windows
|
32
50
|
export * from './src-vue/mac-window/index';
|
33
51
|
export * from './src-vue/iPhone/index';
|
@@ -27,13 +27,25 @@
|
|
27
27
|
* </Alert>
|
28
28
|
* ```
|
29
29
|
*
|
30
|
+
* 自定义操作按钮:
|
31
|
+
* ```astro
|
32
|
+
* <Alert type="info">
|
33
|
+
* 这是带自定义操作的提示
|
34
|
+
* <slot name="action">
|
35
|
+
* <button>操作</button>
|
36
|
+
* </slot>
|
37
|
+
* </Alert>
|
38
|
+
* ```
|
39
|
+
*
|
30
40
|
* @props
|
31
41
|
* @prop {('info'|'success'|'warning'|'error')} [type='info'] - 提示类型,影响颜色和图标
|
32
42
|
* @prop {string} [title] - 提示标题,可选
|
33
43
|
* @prop {string} [class] - 自定义 CSS 类名
|
44
|
+
* @prop {boolean} [closable=true] - 是否可关闭
|
34
45
|
*
|
35
46
|
* @slots
|
36
47
|
* @slot default - 提示内容
|
48
|
+
* @slot action - 自定义操作按钮,显示在 alert 右侧
|
37
49
|
*/
|
38
50
|
|
39
51
|
// 注意:
|
@@ -46,15 +58,22 @@ import {
|
|
46
58
|
SuccessIcon,
|
47
59
|
WarningIcon,
|
48
60
|
ErrorIcon,
|
61
|
+
CloseIcon,
|
49
62
|
} from '../../index-astro';
|
50
63
|
|
51
64
|
interface Props {
|
52
65
|
type?: 'info' | 'success' | 'warning' | 'error';
|
53
66
|
title?: string;
|
54
67
|
class?: string;
|
68
|
+
closable?: boolean;
|
55
69
|
}
|
56
70
|
|
57
|
-
const {
|
71
|
+
const {
|
72
|
+
type = 'info',
|
73
|
+
title,
|
74
|
+
class: className = '',
|
75
|
+
closable = true,
|
76
|
+
} = Astro.props;
|
58
77
|
|
59
78
|
// 根据类型设置样式
|
60
79
|
const alertClass = {
|
@@ -73,12 +92,17 @@ const IconComponent = {
|
|
73
92
|
}[type as 'info' | 'success' | 'warning' | 'error'];
|
74
93
|
---
|
75
94
|
|
76
|
-
<div
|
95
|
+
<div
|
96
|
+
class={`cosy:alert cosy:w-full cosy:flex ${alertClass} ${className}`}
|
97
|
+
role="alert">
|
77
98
|
<div
|
78
|
-
class="cosy:flex cosy:flex-row cosy:items-center cosy:gap-4 cosy:
|
79
|
-
<IconComponent
|
99
|
+
class="cosy:flex cosy:flex-row cosy:items-center cosy:gap-4 cosy:justify-between cosy:w-full">
|
100
|
+
<IconComponent
|
101
|
+
class="cosy:btn cosy:btn-sm cosy:btn-ghost cosy:btn-circle"
|
102
|
+
/>
|
80
103
|
|
81
|
-
<div
|
104
|
+
<div
|
105
|
+
class="cosy:flex cosy:flex-col cosy:items-start cosy:h-full cosy:flex-1">
|
82
106
|
{
|
83
107
|
title && (
|
84
108
|
<h3 class="cosy:font-bold" style="margin-top: 0 !important">
|
@@ -96,5 +120,21 @@ const IconComponent = {
|
|
96
120
|
|
97
121
|
{!title && <slot />}
|
98
122
|
</div>
|
123
|
+
|
124
|
+
<div
|
125
|
+
class="cosy:flex cosy:flex-row cosy:items-center cosy:gap-2"
|
126
|
+
data-role="actions">
|
127
|
+
<slot name="action" />
|
128
|
+
|
129
|
+
{
|
130
|
+
closable && (
|
131
|
+
<button
|
132
|
+
class="cosy:ml-auto cosy:btn cosy:btn-ghost cosy:btn-sm cosy:btn-circle"
|
133
|
+
onclick="this.parentElement.parentElement.style.display = 'none';">
|
134
|
+
<CloseIcon class="cosy:h-5 cosy:w-5" />
|
135
|
+
</button>
|
136
|
+
)
|
137
|
+
}
|
138
|
+
</div>
|
99
139
|
</div>
|
100
140
|
</div>
|
@@ -0,0 +1,67 @@
|
|
1
|
+
---
|
2
|
+
/**
|
3
|
+
* @component Badge
|
4
|
+
* @description 一个用于高亮信息的小组件。
|
5
|
+
* @usage
|
6
|
+
* ```astro
|
7
|
+
* <Badge>默认</Badge>
|
8
|
+
* <Badge variant="primary">Primary</Badge>
|
9
|
+
* ```
|
10
|
+
* @props
|
11
|
+
* @prop {('primary'|'secondary'|'accent'|'ghost'|'info'|'success'|'warning'|'error')} [variant] - 徽章的颜色变体。
|
12
|
+
* @prop {('xs'|'sm'|'md'|'lg')} [size] - 徽章的尺寸。
|
13
|
+
* @prop {boolean} [outline=false] - 徽章是否为描边样式。
|
14
|
+
* @prop {string} [class] - 自定义 CSS 类。
|
15
|
+
* @slots
|
16
|
+
* @slot default - 徽章内容。
|
17
|
+
*/
|
18
|
+
import '../../style.ts';
|
19
|
+
|
20
|
+
interface Props {
|
21
|
+
variant?:
|
22
|
+
| 'primary'
|
23
|
+
| 'secondary'
|
24
|
+
| 'accent'
|
25
|
+
| 'ghost'
|
26
|
+
| 'info'
|
27
|
+
| 'success'
|
28
|
+
| 'warning'
|
29
|
+
| 'error';
|
30
|
+
size?: 'xs' | 'sm' | 'md' | 'lg';
|
31
|
+
outline?: boolean;
|
32
|
+
class?: string;
|
33
|
+
}
|
34
|
+
|
35
|
+
const { variant, size, outline = false, class: className } = Astro.props;
|
36
|
+
|
37
|
+
function getVariantClass(variant: Props['variant']) {
|
38
|
+
if (variant === 'primary') return 'cosy:badge-primary';
|
39
|
+
if (variant === 'secondary') return 'cosy:badge-secondary';
|
40
|
+
if (variant === 'accent') return 'cosy:badge-accent';
|
41
|
+
if (variant === 'ghost') return 'cosy:badge-ghost';
|
42
|
+
if (variant === 'info') return 'cosy:badge-info';
|
43
|
+
if (variant === 'success') return 'cosy:badge-success';
|
44
|
+
if (variant === 'warning') return 'cosy:badge-warning';
|
45
|
+
if (variant === 'error') return 'cosy:badge-error';
|
46
|
+
return '';
|
47
|
+
}
|
48
|
+
|
49
|
+
function getSizeClass(size: Props['size']) {
|
50
|
+
if (size === 'xs') return 'cosy:badge-xs';
|
51
|
+
if (size === 'sm') return 'cosy:badge-sm';
|
52
|
+
if (size === 'md') return 'cosy:badge-md';
|
53
|
+
if (size === 'lg') return 'cosy:badge-lg';
|
54
|
+
return '';
|
55
|
+
}
|
56
|
+
---
|
57
|
+
|
58
|
+
<div
|
59
|
+
class:list={[
|
60
|
+
'cosy:badge',
|
61
|
+
getVariantClass(variant),
|
62
|
+
getSizeClass(size),
|
63
|
+
outline ? 'cosy:badge-outline' : '',
|
64
|
+
className,
|
65
|
+
]}>
|
66
|
+
<slot />
|
67
|
+
</div>
|
@@ -0,0 +1 @@
|
|
1
|
+
export { default as Badge } from './Badge.astro';
|
@@ -26,75 +26,101 @@ Alert 组件用于向用户显示重要的提示信息,支持多种类型的
|
|
26
26
|
</Alert>
|
27
27
|
```
|
28
28
|
|
29
|
+
自定义操作按钮:
|
30
|
+
```vue
|
31
|
+
<Alert type="info">
|
32
|
+
这是带自定义操作的提示
|
33
|
+
<template #action>
|
34
|
+
<button @click="doSomething">操作</button>
|
35
|
+
</template>
|
36
|
+
</Alert>
|
37
|
+
```
|
38
|
+
|
29
39
|
@props
|
30
40
|
@prop {('info'|'success'|'warning'|'error')} [type='info'] - 提示类型,影响颜色和图标
|
31
41
|
@prop {string} [title] - 提示标题,可选
|
32
42
|
@prop {string} [class] - 自定义 CSS 类名
|
43
|
+
@prop {boolean} [closable] - 是否可关闭,默认可关闭
|
33
44
|
|
34
45
|
@slots
|
35
46
|
@slot default - 提示内容
|
47
|
+
@slot action - 自定义操作按钮,显示在 alert 右侧
|
36
48
|
-->
|
37
49
|
|
38
50
|
<script setup lang="ts">
|
39
51
|
import '../../style';
|
40
52
|
import { computed } from 'vue';
|
41
53
|
import { InfoIcon, SuccessIcon, WarningIcon, ErrorIcon } from '../icons/index';
|
54
|
+
import { RiCloseLine } from '@remixicon/vue';
|
42
55
|
|
43
56
|
interface Props {
|
44
|
-
|
45
|
-
|
46
|
-
|
57
|
+
type?: 'info' | 'success' | 'warning' | 'error';
|
58
|
+
title?: string;
|
59
|
+
class?: string;
|
60
|
+
closable?: boolean;
|
47
61
|
}
|
48
62
|
|
49
63
|
const props = withDefaults(defineProps<Props>(), {
|
50
|
-
|
51
|
-
|
52
|
-
|
64
|
+
type: 'info',
|
65
|
+
title: '',
|
66
|
+
class: '',
|
67
|
+
closable: true,
|
53
68
|
});
|
54
69
|
|
70
|
+
const emit = defineEmits(['close']);
|
71
|
+
|
72
|
+
const handleClose = () => {
|
73
|
+
emit('close');
|
74
|
+
};
|
75
|
+
|
55
76
|
// 根据类型设置样式
|
56
77
|
const alertClass = computed(() => {
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
78
|
+
const alertClasses = {
|
79
|
+
info: 'cosy:alert-info',
|
80
|
+
success: 'cosy:alert-success',
|
81
|
+
warning: 'cosy:alert-warning',
|
82
|
+
error: 'cosy:alert-error',
|
83
|
+
};
|
84
|
+
return alertClasses[props.type];
|
64
85
|
});
|
65
86
|
|
66
87
|
// 根据类型设置图标组件
|
67
88
|
const IconComponent = computed(() => {
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
89
|
+
const iconComponents = {
|
90
|
+
info: InfoIcon,
|
91
|
+
success: SuccessIcon,
|
92
|
+
warning: WarningIcon,
|
93
|
+
error: ErrorIcon,
|
94
|
+
};
|
95
|
+
return iconComponents[props.type];
|
75
96
|
});
|
76
97
|
</script>
|
77
98
|
|
78
99
|
<template>
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
100
|
+
<div :class="['cosy:alert cosy:w-full cosy:flex', alertClass, props.class]" role="alert">
|
101
|
+
<div class="cosy:flex cosy:flex-row cosy:items-center cosy:gap-4 cosy:justify-between cosy:w-full">
|
102
|
+
<div class="cosy:flex cosy:items-center cosy:gap-4">
|
103
|
+
<component :is="IconComponent" class="cosy:btn cosy:btn-sm cosy:btn-ghost cosy:btn-circle" />
|
104
|
+
|
105
|
+
<div class="cosy:flex cosy:flex-col cosy:items-start cosy:h-full cosy:flex-1">
|
106
|
+
<h3 v-if="props.title" class="cosy:font-bold" style="margin-top: 0 !important">
|
107
|
+
{{ props.title }}
|
108
|
+
</h3>
|
109
|
+
<div v-if="props.title" class="cosy:text-xs">
|
110
|
+
<slot />
|
111
|
+
</div>
|
112
|
+
<slot v-else />
|
113
|
+
</div>
|
114
|
+
</div>
|
115
|
+
|
116
|
+
<div class="cosy:flex cosy:flex-row cosy:items-center cosy:gap-2" data-role="actions">
|
117
|
+
<slot name="action" />
|
118
|
+
|
119
|
+
<button v-if="props.closable" @click="handleClose"
|
120
|
+
class="cosy:btn cosy:btn-ghost cosy:btn-sm cosy:btn-circle">
|
121
|
+
<RiCloseLine class="cosy:h-5 cosy:w-5" />
|
122
|
+
</button>
|
123
|
+
</div>
|
95
124
|
</div>
|
96
|
-
<slot v-else />
|
97
|
-
</div>
|
98
125
|
</div>
|
99
|
-
</div>
|
100
126
|
</template>
|
@@ -0,0 +1,66 @@
|
|
1
|
+
<!--
|
2
|
+
@component Badge
|
3
|
+
@description 一个用于高亮信息的小组件。
|
4
|
+
@usage
|
5
|
+
```vue
|
6
|
+
<Badge>默认</Badge>
|
7
|
+
<Badge variant="primary">Primary</Badge>
|
8
|
+
```
|
9
|
+
@props
|
10
|
+
@prop {('primary'|'secondary'|'accent'|'ghost'|'info'|'success'|'warning'|'error')} [variant] - 徽章的颜色变体。
|
11
|
+
@prop {('xs'|'sm'|'md'|'lg')} [size] - 徽章的尺寸。
|
12
|
+
@prop {boolean} [outline=false] - 徽章是否为描边样式。
|
13
|
+
@prop {string} [class] - 自定义 CSS 类。
|
14
|
+
@slots
|
15
|
+
@slot default - 徽章内容。
|
16
|
+
-->
|
17
|
+
<script setup lang="ts">
|
18
|
+
import '../../style';
|
19
|
+
import { computed } from 'vue';
|
20
|
+
|
21
|
+
const props = defineProps({
|
22
|
+
variant: {
|
23
|
+
type: String,
|
24
|
+
default: undefined,
|
25
|
+
validator: (v: string) => ['primary', 'secondary', 'accent', 'ghost', 'info', 'success', 'warning', 'error'].includes(v)
|
26
|
+
},
|
27
|
+
size: {
|
28
|
+
type: String,
|
29
|
+
default: undefined,
|
30
|
+
validator: (v: string) => ['xs', 'sm', 'md', 'lg'].includes(v)
|
31
|
+
},
|
32
|
+
outline: Boolean,
|
33
|
+
class: String
|
34
|
+
});
|
35
|
+
|
36
|
+
const variantClass = computed(() => {
|
37
|
+
if (props.variant === 'primary') return 'cosy:badge-primary';
|
38
|
+
if (props.variant === 'secondary') return 'cosy:badge-secondary';
|
39
|
+
if (props.variant === 'accent') return 'cosy:badge-accent';
|
40
|
+
if (props.variant === 'ghost') return 'cosy:badge-ghost';
|
41
|
+
if (props.variant === 'info') return 'cosy:badge-info';
|
42
|
+
if (props.variant === 'success') return 'cosy:badge-success';
|
43
|
+
if (props.variant === 'warning') return 'cosy:badge-warning';
|
44
|
+
if (props.variant === 'error') return 'cosy:badge-error';
|
45
|
+
return '';
|
46
|
+
});
|
47
|
+
const sizeClass = computed(() => {
|
48
|
+
if (props.size === 'xs') return 'cosy:badge-xs';
|
49
|
+
if (props.size === 'sm') return 'cosy:badge-sm';
|
50
|
+
if (props.size === 'md') return 'cosy:badge-md';
|
51
|
+
if (props.size === 'lg') return 'cosy:badge-lg';
|
52
|
+
return '';
|
53
|
+
});
|
54
|
+
</script>
|
55
|
+
|
56
|
+
<template>
|
57
|
+
<span :class="[
|
58
|
+
'cosy:badge',
|
59
|
+
variantClass,
|
60
|
+
sizeClass,
|
61
|
+
props.outline ? 'cosy:badge-outline' : '',
|
62
|
+
props.class
|
63
|
+
]">
|
64
|
+
<slot />
|
65
|
+
</span>
|
66
|
+
</template>
|
@@ -0,0 +1 @@
|
|
1
|
+
export { default as Badge } from './Badge.vue';
|
@@ -0,0 +1,52 @@
|
|
1
|
+
<script setup lang="ts">
|
2
|
+
import { computed } from 'vue';
|
3
|
+
|
4
|
+
defineOptions({
|
5
|
+
name: 'Card',
|
6
|
+
});
|
7
|
+
|
8
|
+
const props = defineProps({
|
9
|
+
title: { type: String, required: true },
|
10
|
+
subtitle: String,
|
11
|
+
imageUrl: String,
|
12
|
+
href: String,
|
13
|
+
compact: Boolean,
|
14
|
+
class: String,
|
15
|
+
});
|
16
|
+
|
17
|
+
const cardClasses = computed(() => [
|
18
|
+
'cosy:card',
|
19
|
+
'cosy:w-full',
|
20
|
+
'cosy:bg-base-100',
|
21
|
+
'cosy:shadow-xl',
|
22
|
+
'cosy:hover:shadow-2xl',
|
23
|
+
'cosy:transition-all',
|
24
|
+
'cosy:duration-300',
|
25
|
+
'cosy:ease-in-out',
|
26
|
+
props.compact ? 'cosy:card-compact' : '',
|
27
|
+
props.href ? 'cosy:cursor-pointer cosy:hover:scale-105 cosy:transform cosy:no-underline' : '',
|
28
|
+
props.class,
|
29
|
+
].filter(Boolean).join(' '));
|
30
|
+
|
31
|
+
const contentPadding = computed(() => props.compact ? 'cosy:p-4' : 'cosy:p-6');
|
32
|
+
</script>
|
33
|
+
|
34
|
+
<template>
|
35
|
+
<component :is="props.href ? 'a' : 'article'" :href="props.href" :class="cardClasses">
|
36
|
+
<template v-if="props.imageUrl">
|
37
|
+
<figure class="not-prose cosy:m-0 cosy:p-0">
|
38
|
+
<img :src="props.imageUrl" :alt="props.title"
|
39
|
+
class="cosy:w-full cosy:h-48 cosy:object-cover cosy:rounded-t-lg" />
|
40
|
+
</figure>
|
41
|
+
</template>
|
42
|
+
<div :class="['cosy:card-body', contentPadding]">
|
43
|
+
<h2 class="cosy:card-title cosy:text-xl cosy:font-bold">{{ props.title }}</h2>
|
44
|
+
<p v-if="props.subtitle" class="cosy:text-base-content/70 cosy:text-sm cosy:leading-relaxed">{{
|
45
|
+
props.subtitle }}
|
46
|
+
</p>
|
47
|
+
<div v-if="$slots.default" class="cosy:mt-4">
|
48
|
+
<slot />
|
49
|
+
</div>
|
50
|
+
</div>
|
51
|
+
</component>
|
52
|
+
</template>
|
@@ -0,0 +1,34 @@
|
|
1
|
+
<script setup lang="ts">
|
2
|
+
defineOptions({
|
3
|
+
name: 'CardCourse',
|
4
|
+
});
|
5
|
+
|
6
|
+
const props = defineProps({
|
7
|
+
title: { type: String, required: true },
|
8
|
+
link: { type: String, required: true },
|
9
|
+
});
|
10
|
+
</script>
|
11
|
+
|
12
|
+
<template>
|
13
|
+
<div>
|
14
|
+
<a :href="props.link" class="cosy:justify-center cosy:flex cosy:no-underline cosy:text-base-content">
|
15
|
+
<div class="cosy:w-56 cosy:h-80">
|
16
|
+
<div
|
17
|
+
class="cosy:bg-gradient-to-br cosy:w-full cosy:h-full cosy:from-accent/50 cosy:to-primary/30 cosy:rounded-3xl cosy:shadow-lg cosy:backdrop-blur-sm">
|
18
|
+
<div
|
19
|
+
class="cosy:bg-base-100/60 cosy:w-full cosy:h-full cosy:rounded-3xl cosy:border cosy:border-base-content/30 hover:cosy:scale-105 hover:cosy:shadow-2xl cosy:transform cosy:duration-200 cosy:backdrop-filter cosy:backdrop-blur-sm">
|
20
|
+
<div class="card-body cosy:p-1 cosy:h-full">
|
21
|
+
<div
|
22
|
+
class="cosy:h-3/5 cosy:w-full cosy:flex cosy:items-center cosy:justify-center cosy:text-6xl cosy:text-primary">
|
23
|
+
📚
|
24
|
+
</div>
|
25
|
+
<div>
|
26
|
+
<h2 class="cosy:text-lg cosy:text-center cosy:font-medium">{{ props.title }}</h2>
|
27
|
+
</div>
|
28
|
+
</div>
|
29
|
+
</div>
|
30
|
+
</div>
|
31
|
+
</div>
|
32
|
+
</a>
|
33
|
+
</div>
|
34
|
+
</template>
|
@@ -0,0 +1,121 @@
|
|
1
|
+
<!--
|
2
|
+
@component KeyCatcher
|
3
|
+
|
4
|
+
@description
|
5
|
+
KeyCatcher 组件用于全局捕获键盘按键事件,并可通过自定义事件通知外部。支持可选的按键展示浮窗。
|
6
|
+
|
7
|
+
@usage
|
8
|
+
基本用法:
|
9
|
+
```vue
|
10
|
+
<KeyCatcher />
|
11
|
+
```
|
12
|
+
|
13
|
+
显示最近按下的按键:
|
14
|
+
```vue
|
15
|
+
<KeyCatcher :showKey="true" />
|
16
|
+
```
|
17
|
+
|
18
|
+
监听全局按键事件:
|
19
|
+
```vue
|
20
|
+
<KeyCatcher @globalKey="onGlobalKey" />
|
21
|
+
```
|
22
|
+
|
23
|
+
@props
|
24
|
+
@prop {boolean} [showKey=false] - 是否显示最近按下的按键浮窗
|
25
|
+
|
26
|
+
@events
|
27
|
+
@event globalKey - 当捕获到全局按键时触发,参数为按键字符串
|
28
|
+
|
29
|
+
@slots
|
30
|
+
无
|
31
|
+
-->
|
32
|
+
|
33
|
+
<script setup lang="ts">
|
34
|
+
import { ref, onMounted, onUnmounted } from 'vue';
|
35
|
+
import '../../style';
|
36
|
+
|
37
|
+
const lastKey = ref<string | null>(null);
|
38
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
39
|
+
|
40
|
+
const props = defineProps<{ showKey?: boolean }>();
|
41
|
+
const emit = defineEmits<{ (e: 'globalKey', key: string): void }>();
|
42
|
+
|
43
|
+
const handleKeydown = (event: KeyboardEvent) => {
|
44
|
+
const tag = (event.target as HTMLElement)?.tagName?.toLowerCase();
|
45
|
+
const isEditable =
|
46
|
+
tag === 'input' ||
|
47
|
+
tag === 'textarea' ||
|
48
|
+
(event.target as HTMLElement)?.isContentEditable;
|
49
|
+
if (
|
50
|
+
event.key.length === 1 ||
|
51
|
+
[
|
52
|
+
'Enter',
|
53
|
+
'Escape',
|
54
|
+
'Backspace',
|
55
|
+
'Tab',
|
56
|
+
'Shift',
|
57
|
+
'Control',
|
58
|
+
'Alt',
|
59
|
+
'Meta',
|
60
|
+
'ArrowUp',
|
61
|
+
'ArrowDown',
|
62
|
+
'ArrowLeft',
|
63
|
+
'ArrowRight',
|
64
|
+
'CapsLock',
|
65
|
+
'Delete',
|
66
|
+
'Home',
|
67
|
+
'End',
|
68
|
+
'PageUp',
|
69
|
+
'PageDown',
|
70
|
+
].includes(event.key)
|
71
|
+
) {
|
72
|
+
// 只在不是输入框、textarea、contenteditable 时发事件
|
73
|
+
if (/^[a-zA-Z]$/.test(event.key) && !isEditable) {
|
74
|
+
emit('globalKey', event.key);
|
75
|
+
}
|
76
|
+
// 展示按键(如果允许)
|
77
|
+
if (props.showKey) {
|
78
|
+
let key = event.key;
|
79
|
+
if (key === ' ') key = 'Space';
|
80
|
+
lastKey.value = key;
|
81
|
+
if (timer) clearTimeout(timer);
|
82
|
+
timer = setTimeout(() => {
|
83
|
+
lastKey.value = null;
|
84
|
+
}, 3000);
|
85
|
+
}
|
86
|
+
}
|
87
|
+
};
|
88
|
+
|
89
|
+
onMounted(() => {
|
90
|
+
window.addEventListener('keydown', handleKeydown);
|
91
|
+
});
|
92
|
+
|
93
|
+
onUnmounted(() => {
|
94
|
+
window.removeEventListener('keydown', handleKeydown);
|
95
|
+
if (timer) clearTimeout(timer);
|
96
|
+
});
|
97
|
+
</script>
|
98
|
+
|
99
|
+
<template>
|
100
|
+
<Transition name="key-fade">
|
101
|
+
<div v-if="props.showKey && lastKey"
|
102
|
+
class="cosy:fixed cosy:bottom-4 cosy:right-4 cosy:bg-accent cosy:shadow-lg cosy:rounded cosy:px-6 cosy:py-3 cosy:z-50 cosy:text-xl cosy:font-bold cosy:text-gray-800 cosy:select-none cosy:pointer-events-none cosy:border cosy:border-gray-200 cosy:backdrop-blur-sm">
|
103
|
+
<span class="cosy:text-blue-600">{{ lastKey }}</span>
|
104
|
+
</div>
|
105
|
+
</Transition>
|
106
|
+
</template>
|
107
|
+
|
108
|
+
<style scoped>
|
109
|
+
.key-fade-enter-active,
|
110
|
+
.key-fade-leave-active {
|
111
|
+
transition:
|
112
|
+
opacity 0.2s,
|
113
|
+
transform 0.2s;
|
114
|
+
}
|
115
|
+
|
116
|
+
.key-fade-enter-from,
|
117
|
+
.key-fade-leave-to {
|
118
|
+
opacity: 0;
|
119
|
+
transform: translateY(20px);
|
120
|
+
}
|
121
|
+
</style>
|
@@ -0,0 +1 @@
|
|
1
|
+
export { default as KeyCatcher } from './KeyCatcher.vue';
|