@coffic/cosy-ui 0.6.6 → 0.6.10

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.
@@ -0,0 +1,267 @@
1
+ <script setup lang="ts">
2
+ import { ref, onMounted, watch, onUnmounted } from 'vue';
3
+ import { RiDownloadLine } from '@remixicon/vue';
4
+ import { toPng } from 'html-to-image';
5
+
6
+ const props = defineProps({
7
+ showDownloadButton: {
8
+ type: Boolean,
9
+ default: true
10
+ },
11
+ backgroundClassIndex: {
12
+ type: Number,
13
+ default: 0
14
+ }
15
+ })
16
+
17
+ const componentRef = ref<HTMLElement | null>(null);
18
+
19
+ const isDropdownOpen = ref(false);
20
+
21
+ const sizePresets = [
22
+ { name: 'Default', width: 'w-full', height: 'h-full' },
23
+ { name: 'Square', width: 'w-[600px]', height: 'h-[600px]' },
24
+ { name: 'Landscape', width: 'w-[800px]', height: 'h-[450px]' },
25
+ { name: 'Portrait', width: 'w-[450px]', height: 'h-[800px]' },
26
+ { name: 'Wide', width: 'w-[1200px]', height: 'h-[675px]' },
27
+ { name: 'Banner', width: 'w-[1200px]', height: 'h-[300px]' },
28
+ { name: '1280 × 800', width: 'w-[1280px]', height: 'h-[800px]' },
29
+ { name: '1440 × 900', width: 'w-[1440px]', height: 'h-[900px]' },
30
+ { name: '2560 × 1600', width: 'w-[2560px]', height: 'h-[1600px]' },
31
+ { name: '2880 × 1800', width: 'w-[2880px]', height: 'h-[1800px]' },
32
+ ];
33
+
34
+ const selectedSize = ref(sizePresets[0]);
35
+
36
+ const toggleDropdown = () => {
37
+ isDropdownOpen.value = !isDropdownOpen.value;
38
+ };
39
+
40
+ // Add new ref for tracking if size was loaded from storage
41
+ const isLoadedFromStorage = ref(false);
42
+
43
+ // 监听尺寸变化并保存到 localStorage
44
+ watch(selectedSize, (newSize) => {
45
+ localStorage.setItem('bannerBoxSize', JSON.stringify(newSize));
46
+ // 当尺寸改变时,发出事件通知其他组件
47
+ window.dispatchEvent(new CustomEvent('bannerBoxSizeChange', {
48
+ detail: newSize
49
+ }));
50
+ // 设置为已加载状态,显示尺寸标签
51
+ isLoadedFromStorage.value = true;
52
+ });
53
+
54
+ // 创建自定义事件处理函数
55
+ const handleSizeClear = () => {
56
+ selectedSize.value = sizePresets[0];
57
+ isLoadedFromStorage.value = false;
58
+ };
59
+
60
+ // 处理尺寸变化的事件
61
+ const handleSizeChange = (event: Event) => {
62
+ const customEvent = event as CustomEvent;
63
+ selectedSize.value = customEvent.detail;
64
+ isLoadedFromStorage.value = true;
65
+ };
66
+
67
+ onMounted(() => {
68
+ const savedSize = localStorage.getItem('bannerBoxSize');
69
+ if (savedSize) {
70
+ const parsed = JSON.parse(savedSize);
71
+ const found = sizePresets.find(preset => preset.name === parsed.name);
72
+ if (found) {
73
+ selectedSize.value = found;
74
+ isLoadedFromStorage.value = true;
75
+ }
76
+ }
77
+
78
+ // 添加事件监听
79
+ window.addEventListener('bannerBoxClear', handleSizeClear);
80
+ window.addEventListener('bannerBoxSizeChange', handleSizeChange);
81
+
82
+ document.addEventListener('click', (event) => {
83
+ const target = event.target as HTMLElement;
84
+ if (!target.closest('.relative')) {
85
+ isDropdownOpen.value = false;
86
+ }
87
+ });
88
+ });
89
+
90
+ // Update downloadAsImage function to accept scale parameter
91
+ const downloadAsImage = async () => {
92
+ try {
93
+ const element = componentRef.value;
94
+ if (!element) {
95
+ console.error('Component reference is null');
96
+ return;
97
+ }
98
+
99
+ const dataUrl = await toPng(element, {
100
+ backgroundColor: undefined,
101
+ style: {
102
+ transform: 'scale(1)',
103
+ transformOrigin: 'top left'
104
+ }
105
+ });
106
+
107
+ const link = document.createElement('a');
108
+ const fileName = `feature-${element.offsetWidth}x${element.offsetHeight}.png`;
109
+ link.download = fileName;
110
+ link.href = dataUrl;
111
+ link.click();
112
+ isDropdownOpen.value = false;
113
+ } catch (error) {
114
+ console.error('Failed to download image:', error);
115
+ }
116
+ };
117
+
118
+ const bgClasses = [
119
+ 'bg-gradient-to-b from-blue-100/50 to-blue-200/50 dark:from-blue-500/10 dark:to-blue-200/10',
120
+ 'bg-gradient-to-b from-blue-200/50 to-purple-200/50 dark:from-blue-500/10 dark:to-purple-200/10',
121
+ 'bg-gradient-to-b from-yellow-200/50 to-green-200/50 dark:from-yellow-500/10 dark:to-green-200/10',
122
+ 'bg-gradient-to-b from-teal-200/50 to-blue-200/50 dark:from-teal-500/10 dark:to-blue-200/10',
123
+ 'bg-gradient-to-b from-pink-200/50 to-indigo-200/20 dark:from-pink-500/10 dark:to-indigo-200/10',
124
+ 'bg-gradient-to-b from-red-200/50 to-orange-200/50 dark:from-red-500/10 dark:to-orange-200/10',
125
+ 'bg-gradient-to-b from-orange-200/50 to-yellow-200/50 dark:from-orange-500/10 dark:to-yellow-200/10',
126
+ 'bg-gradient-to-b from-green-200/50 to-teal-200/50 dark:from-green-500/10 dark:to-teal-200/10',
127
+
128
+ // 不透明的背景
129
+ 'bg-gradient-to-b from-blue-100 to-blue-200 dark:from-blue-500 dark:to-blue-200',
130
+ 'bg-gradient-to-b from-blue-200 to-purple-200 dark:from-blue-500 dark:to-purple-200',
131
+ 'bg-gradient-to-b from-yellow-200 to-green-200 dark:from-yellow-500 dark:to-green-200',
132
+ 'bg-gradient-to-b from-teal-200 to-blue-200 dark:from-teal-500 dark:to-blue-200',
133
+ 'bg-gradient-to-b from-pink-200 to-red-200 dark:from-pink-500 dark:to-red-200',
134
+ 'bg-gradient-to-b from-red-200 to-orange-200 dark:from-red-500 dark:to-orange-200',
135
+ 'bg-gradient-to-b from-orange-200 to-yellow-200 dark:from-orange-500 dark:to-yellow-200',
136
+ 'bg-gradient-to-b from-green-200 to-teal-200 dark:from-green-500 dark:to-teal-200',
137
+
138
+ // 不透明的深色背景
139
+ 'bg-gradient-to-b from-blue-900 to-blue-200 dark:from-blue-900 dark:to-blue-200',
140
+ 'bg-gradient-to-b from-blue-900 to-purple-200 dark:from-blue-900 dark:to-purple-200',
141
+ 'bg-gradient-to-b from-yellow-900 to-green-200 dark:from-yellow-900 dark:to-green-200',
142
+ 'bg-gradient-to-b from-teal-900 to-blue-200 dark:from-teal-900 dark:to-blue-200',
143
+ 'bg-gradient-to-b from-pink-900 to-red-200 dark:from-pink-900 dark:to-red-200',
144
+ 'bg-gradient-to-b from-red-900 to-orange-200 dark:from-red-900 dark:to-orange-200',
145
+ 'bg-gradient-to-b from-orange-900 to-yellow-200 dark:from-orange-900 dark:to-yellow-200',
146
+ 'bg-gradient-to-b from-green-900 to-teal-900 dark:from-green-900 dark:to-teal-900',
147
+ // 不透明的渐变背景
148
+ 'bg-gradient-to-br from-emerald-400 to-cyan-400 dark:from-emerald-600 dark:to-cyan-600',
149
+ 'bg-gradient-to-br from-violet-400 to-fuchsia-400 dark:from-violet-600 dark:to-fuchsia-600',
150
+ 'bg-gradient-to-br from-amber-400 to-orange-400 dark:from-amber-600 dark:to-orange-600',
151
+ 'bg-gradient-to-br from-rose-400 to-pink-400 dark:from-rose-600 dark:to-pink-600',
152
+ 'bg-gradient-to-br from-sky-400 to-indigo-400 dark:from-sky-600 dark:to-indigo-600',
153
+ 'bg-gradient-to-br from-lime-400 to-emerald-400 dark:from-lime-600 dark:to-emerald-600',
154
+ 'bg-gradient-to-br from-purple-400 to-indigo-400 dark:from-purple-600 dark:to-indigo-600',
155
+ 'bg-gradient-to-br from-blue-400 to-violet-400 dark:from-blue-600 dark:to-violet-600',
156
+
157
+ // 纯色背景
158
+ 'bg-emerald-400 dark:bg-emerald-600',
159
+ 'bg-violet-400 dark:bg-violet-600',
160
+ 'bg-amber-400 dark:bg-amber-600',
161
+ 'bg-rose-400 dark:bg-rose-600',
162
+ 'bg-sky-400 dark:bg-sky-600',
163
+ 'bg-lime-400 dark:bg-lime-600',
164
+ 'bg-purple-400 dark:bg-purple-600',
165
+ 'bg-blue-400 dark:bg-blue-600'
166
+
167
+ ]
168
+
169
+ const selectedBgIndex = ref(props.backgroundClassIndex);
170
+
171
+ const getBackgroundClass = (): string => {
172
+ return bgClasses[selectedBgIndex.value % bgClasses.length];
173
+ }
174
+
175
+ const clearStoredSize = () => {
176
+ localStorage.removeItem('bannerBoxSize');
177
+ // 触发自定义事件
178
+ window.dispatchEvent(new CustomEvent('bannerBoxClear'));
179
+ isDropdownOpen.value = false;
180
+ };
181
+
182
+ // 清理事件监听
183
+ onUnmounted(() => {
184
+ window.removeEventListener('bannerBoxClear', handleSizeClear);
185
+ window.removeEventListener('bannerBoxSizeChange', handleSizeChange);
186
+ });
187
+ </script>
188
+
189
+ <template>
190
+ <div class="relative w-full rounded-2xl max-w-7xl mx-auto">
191
+ <!-- Add size indicator -->
192
+ <div v-if="isLoadedFromStorage"
193
+ class="absolute top-4 right-4 bg-yellow-500/30 backdrop-blur-sm px-3 py-1 rounded-lg text-sm text-white">
194
+ {{ selectedSize.name }}
195
+ </div>
196
+
197
+ <!-- Download button with dropdown menu -->
198
+ <div v-if="showDownloadButton" class="absolute top-4 left-4 opacity-0 hover:opacity-100 transition-opacity">
199
+ <div class="relative">
200
+ <button class="bg-yellow-500/30 backdrop-blur-sm p-2 rounded-lg hover:bg-yellow-500/40"
201
+ @click="toggleDropdown">
202
+ <RiDownloadLine class="w-6 h-6 text-white" />
203
+ </button>
204
+ <!-- Size selection dropdown -->
205
+ <div v-if="isDropdownOpen"
206
+ class="absolute left-0 mt-2 w-96 bg-white dark:bg-gray-800 rounded-lg shadow-lg py-2 z-50">
207
+ <!-- Component size presets -->
208
+ <div class="px-4 py-2 border-b border-gray-200 dark:border-gray-700">
209
+ <div class="grid grid-cols-3 gap-2">
210
+ <button v-for="preset in sizePresets" :key="preset.name" :class="[
211
+ 'p-2 text-left rounded text-sm',
212
+ selectedSize.name === preset.name
213
+ ? 'bg-yellow-500/30 text-yellow-900 dark:text-yellow-100'
214
+ : 'hover:bg-gray-100 dark:hover:bg-gray-700'
215
+ ]" @click="selectedSize = preset">
216
+ <div class="flex flex-col">
217
+ <span class="font-medium">{{ preset.name }}</span>
218
+ <span class="text-xs text-gray-500 dark:text-gray-400">
219
+ {{ preset.width.replace('w-[', '').replace(']', '') }}
220
+ </span>
221
+ </div>
222
+ </button>
223
+ <!-- Clear size button -->
224
+ <button class="p-2 text-left rounded text-sm hover:bg-gray-100 dark:hover:bg-gray-700"
225
+ @click="clearStoredSize">
226
+ <div class="flex flex-col">
227
+ <span class="font-medium text-red-600 dark:text-red-400">清除记住的尺寸</span>
228
+ <span class="text-xs text-gray-500 dark:text-gray-400">重置为默认尺寸</span>
229
+ </div>
230
+ </button>
231
+ </div>
232
+ </div>
233
+ <!-- Background settings -->
234
+ <div class="px-4 py-2 border-b border-gray-200 dark:border-gray-700">
235
+ <div class="mt-2">
236
+ <div class="grid grid-cols-8 gap-2">
237
+ <button v-for="(_, index) in bgClasses" :key="index" :class="[
238
+ bgClasses[index],
239
+ 'w-8 h-8 rounded-lg border-2',
240
+ selectedBgIndex === index ? 'border-yellow-500' : 'border-transparent'
241
+ ]" @click="selectedBgIndex = index" />
242
+ </div>
243
+ </div>
244
+ </div>
245
+ <!-- Size options -->
246
+ <div class="p-4">
247
+ <button class="w-full p-2 text-center rounded hover:bg-gray-100 dark:hover:bg-gray-700"
248
+ @click="downloadAsImage()">
249
+ <div class="flex items-center justify-center gap-2">
250
+ <RiDownloadLine class="w-4 h-4" />
251
+ <span class="font-medium">下载图片</span>
252
+ </div>
253
+ </button>
254
+ </div>
255
+ </div>
256
+ </div>
257
+ </div>
258
+
259
+ <div ref="componentRef" class="flex p-8 rounded-2xl shadow" :class="[
260
+ getBackgroundClass(),
261
+ selectedSize.width,
262
+ selectedSize.height
263
+ ]">
264
+ <slot />
265
+ </div>
266
+ </div>
267
+ </template>
@@ -0,0 +1,76 @@
1
+ <template>
2
+ <Transition name="fade">
3
+ <div
4
+ v-if="modelValue"
5
+ class="fixed inset-0 z-50 flex items-center justify-center"
6
+ >
7
+ <!-- 背景遮罩 -->
8
+ <div
9
+ class="absolute inset-0 bg-black/20 dark:bg-black/40 backdrop-blur-sm"
10
+ @click="$emit('update:modelValue', false)"
11
+ />
12
+
13
+ <!-- 对话框 -->
14
+ <div class="relative bg-white dark:bg-gray-800 rounded-xl shadow-xl w-[400px] transform transition-all">
15
+ <!-- 内容区域 -->
16
+ <div class="p-6">
17
+ <h3 class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2">
18
+ {{ title }}
19
+ </h3>
20
+ <p class="text-gray-600 dark:text-gray-300">
21
+ {{ message }}
22
+ </p>
23
+ </div>
24
+
25
+ <!-- 按钮区域 -->
26
+ <div class="flex border-t border-gray-200 dark:border-gray-700">
27
+ <button
28
+ class="flex-1 px-4 py-3 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors rounded-bl-xl"
29
+ @click="$emit('update:modelValue', false)"
30
+ >
31
+ 取消
32
+ </button>
33
+ <button
34
+ class="flex-1 px-4 py-3 text-blue-600 dark:text-blue-400 hover:bg-blue-50 dark:hover:bg-blue-900/30 transition-colors border-l border-gray-200 dark:border-gray-700 rounded-br-xl font-medium"
35
+ @click="handleConfirm"
36
+ >
37
+ 确认
38
+ </button>
39
+ </div>
40
+ </div>
41
+ </div>
42
+ </Transition>
43
+ </template>
44
+
45
+ <script setup>
46
+ defineProps({
47
+ modelValue: Boolean,
48
+ title: {
49
+ type: String,
50
+ default: 'Confirm'
51
+ },
52
+ message: {
53
+ type: String,
54
+ default: 'Are you sure you want to confirm this action?'
55
+ }
56
+ })
57
+
58
+ const emit = defineEmits(['update:modelValue', 'confirm'])
59
+
60
+ const handleConfirm = () => {
61
+ emit('update:modelValue', false)
62
+ emit('confirm')
63
+ }
64
+ </script>
65
+
66
+ <style scoped>
67
+ .fade-enter-active,
68
+ .fade-leave-active {
69
+ transition: opacity 0.2s ease;
70
+ }
71
+
72
+ .fade-enter-from,
73
+ .fade-leave-to {
74
+ opacity: 0;
75
+ }
76
+ </style>
@@ -0,0 +1,95 @@
1
+ <template>
2
+ <div>
3
+ <!-- Button with hover effects -->
4
+ <button
5
+ :class="[
6
+ 'bg-cyan-500/20 p-4 rounded-2xl text-center backdrop-blur-lg text-2xl transition-all duration-300 hover:scale-105 hover:bg-cyan-500/30',
7
+ props.size
8
+ ]"
9
+ @click="showPopup"
10
+ >
11
+ <slot />
12
+ {{ props.title }}
13
+ </button>
14
+
15
+ <!-- Popup message -->
16
+ <Transition name="fade">
17
+ <div
18
+ v-if="isPopupVisible"
19
+ class="fixed inset-0 flex items-center justify-center z-50"
20
+ @click="hidePopup"
21
+ >
22
+ <div class="bg-black/80 backdrop-blur-sm p-6 rounded-xl text-white animate-popup">
23
+ {{ lang === 'zh' ? '这是展示图,不支持操作' : 'This is a preview image, no operation is supported' }}
24
+ </div>
25
+ </div>
26
+ </Transition>
27
+ </div>
28
+ </template>
29
+
30
+ <script setup lang="ts">
31
+ import { ref } from 'vue'
32
+
33
+ const props = defineProps({
34
+ title: {
35
+ type: String,
36
+ required: false,
37
+ default: 'Feature Button'
38
+ },
39
+ size: {
40
+ type: String,
41
+ default: 'w-64',
42
+ validator: (value: string) => ['w-64', 'w-32', 'w-16', 'w-12', 'w-8'].includes(value)
43
+ },
44
+ lang: {
45
+ type: String,
46
+ default: 'en',
47
+ validator: (value: string) => ['en', 'zh'].includes(value)
48
+ },
49
+ showTips: {
50
+ type: Boolean,
51
+ default: false
52
+ }
53
+ })
54
+
55
+ const isPopupVisible = ref(false)
56
+
57
+ const showPopup = () => {
58
+ if (props.showTips) {
59
+ isPopupVisible.value = true
60
+ setTimeout(hidePopup, 2000) // Auto-hide after 2 seconds
61
+ }
62
+ }
63
+
64
+ const hidePopup = () => {
65
+ isPopupVisible.value = false
66
+ }
67
+ </script>
68
+
69
+ <style scoped>
70
+ .fade-enter-active,
71
+ .fade-leave-active {
72
+ transition: opacity 0.3s ease;
73
+ }
74
+
75
+ .fade-enter-from,
76
+ .fade-leave-to {
77
+ opacity: 0;
78
+ }
79
+
80
+ .animate-popup {
81
+ animation: popup 0.3s ease-out;
82
+ }
83
+
84
+ @keyframes popup {
85
+ from {
86
+ transform: scale(0.95);
87
+ opacity: 0;
88
+ }
89
+
90
+ to {
91
+ transform: scale(1);
92
+ opacity: 1;
93
+ }
94
+ }
95
+ </style>
@@ -0,0 +1,209 @@
1
+ <template>
2
+ <component
3
+ :is="link ? 'a' : 'div'"
4
+ :href="link || undefined"
5
+ :target="link ? '_blank' : undefined"
6
+ :rel="link ? 'noopener noreferrer' : undefined"
7
+ :class="[
8
+ 'card bg-base-100/10 backdrop-blur-lg p-8 transition-all duration-300 hover:-translate-y-1 shadow-lg',
9
+ {
10
+ 'hover:bg-primary/15 cursor-pointer': link,
11
+ 'cursor-default': !link
12
+ }
13
+ ]"
14
+ >
15
+ <div class="card-body p-0">
16
+ <div class="mb-4">
17
+ <component
18
+ :is="icon"
19
+ v-if="icon"
20
+ class="text-4xl text-base-content"
21
+ />
22
+ <component
23
+ :is="getPresetIcon"
24
+ v-else-if="presetIcon"
25
+ class="text-4xl text-base-content"
26
+ />
27
+ <div
28
+ v-else
29
+ class="text-4xl text-base-content"
30
+ >
31
+ {{ emoji }}
32
+ </div>
33
+ </div>
34
+ <h3 class="card-title text-lg font-medium text-base-content">
35
+ {{ title }}
36
+ </h3>
37
+ <p
38
+ v-if="description"
39
+ class="text-base-content/70"
40
+ >
41
+ {{ description }}
42
+ </p>
43
+ </div>
44
+ </component>
45
+ </template>
46
+
47
+ <script setup lang="ts">
48
+ import { computed, type Component } from 'vue';
49
+ import {
50
+ // 开发相关
51
+ RiGithubFill,
52
+ RiGitBranchLine,
53
+ RiTerminalBoxLine,
54
+ RiCommandLine,
55
+ RiCodeSSlashLine,
56
+ RiBracesLine,
57
+ RiDatabase2Line,
58
+ RiServerLine,
59
+ // 文档相关
60
+ RiBookOpenLine,
61
+ RiFileTextLine,
62
+ RiArticleLine,
63
+ RiDraftLine,
64
+ RiFileListLine,
65
+ // 媒体相关
66
+ RiImage2Line,
67
+ RiVideoLine,
68
+ RiMusic2Line,
69
+ RiPlayCircleLine,
70
+ RiMovieLine,
71
+ // 社交相关
72
+ RiUserLine,
73
+ RiTeamLine,
74
+ RiChat1Line,
75
+ RiMessage2Line,
76
+ RiShareLine,
77
+ // 工具相关
78
+ RiToolsLine,
79
+ RiSettings4Line,
80
+ RiDashboardLine,
81
+ RiAppsLine,
82
+ RiPlugLine,
83
+ // 安全相关
84
+ RiShieldLine,
85
+ RiLockLine,
86
+ RiKeyLine,
87
+ RiUserSettingsLine,
88
+ // 云服务相关
89
+ RiCloudLine,
90
+ RiUploadCloud2Line,
91
+ RiDownloadCloud2Line,
92
+ RiCloudOffLine,
93
+ // 设备相关
94
+ RiSmartphoneLine,
95
+ RiTabletLine,
96
+ RiComputerLine,
97
+ RiWifiLine,
98
+ // 数据相关
99
+ RiPieChartLine,
100
+ RiLineChartLine,
101
+ RiBarChartLine,
102
+ // AI/机器学习相关
103
+ RiRobot2Line,
104
+ RiBrainLine,
105
+ RiCpuLine,
106
+ // 其他常用
107
+ RiRocketLine,
108
+ RiLightbulbLine,
109
+ RiStarLine,
110
+ RiHeartLine,
111
+ RiThumbUpLine
112
+ } from '@remixicon/vue';
113
+
114
+ // 预设图标映射
115
+ const presetIcons = {
116
+ // 开发类
117
+ github: RiGithubFill,
118
+ git: RiGitBranchLine,
119
+ terminal: RiTerminalBoxLine,
120
+ command: RiCommandLine,
121
+ code: RiCodeSSlashLine,
122
+ api: RiBracesLine,
123
+ database: RiDatabase2Line,
124
+ server: RiServerLine,
125
+
126
+ // 文档类
127
+ book: RiBookOpenLine,
128
+ file: RiFileTextLine,
129
+ article: RiArticleLine,
130
+ draft: RiDraftLine,
131
+ list: RiFileListLine,
132
+
133
+ // 媒体类
134
+ image: RiImage2Line,
135
+ video: RiVideoLine,
136
+ music: RiMusic2Line,
137
+ play: RiPlayCircleLine,
138
+ movie: RiMovieLine,
139
+
140
+ // 社交类
141
+ user: RiUserLine,
142
+ team: RiTeamLine,
143
+ chat: RiChat1Line,
144
+ message: RiMessage2Line,
145
+ share: RiShareLine,
146
+
147
+ // 工具类
148
+ tools: RiToolsLine,
149
+ settings: RiSettings4Line,
150
+ dashboard: RiDashboardLine,
151
+ apps: RiAppsLine,
152
+ plugin: RiPlugLine,
153
+
154
+ // 安全类
155
+ shield: RiShieldLine,
156
+ lock: RiLockLine,
157
+ key: RiKeyLine,
158
+ security: RiUserSettingsLine,
159
+
160
+ // 云服务类
161
+ cloud: RiCloudLine,
162
+ upload: RiUploadCloud2Line,
163
+ download: RiDownloadCloud2Line,
164
+ offline: RiCloudOffLine,
165
+
166
+ // 设备类
167
+ mobile: RiSmartphoneLine,
168
+ tablet: RiTabletLine,
169
+ computer: RiComputerLine,
170
+ wifi: RiWifiLine,
171
+
172
+ // 数据类
173
+ chart: RiPieChartLine,
174
+ line: RiLineChartLine,
175
+ bar: RiBarChartLine,
176
+ data: RiDatabase2Line,
177
+
178
+ // AI/机器学习类
179
+ robot: RiRobot2Line,
180
+ brain: RiBrainLine,
181
+ cpu: RiCpuLine,
182
+
183
+ // 其他常用
184
+ rocket: RiRocketLine,
185
+ idea: RiLightbulbLine,
186
+ star: RiStarLine,
187
+ heart: RiHeartLine,
188
+ like: RiThumbUpLine
189
+ } as const;
190
+
191
+ type PresetIconType = keyof typeof presetIcons;
192
+
193
+ interface Props {
194
+ title: string;
195
+ description?: string;
196
+ link?: string;
197
+ emoji?: string;
198
+ icon?: Component; // 自定义图标组件
199
+ presetIcon?: PresetIconType; // 预设图标名称
200
+ }
201
+
202
+ const props = defineProps<Props>();
203
+
204
+ // 获取预设图标组件
205
+ const getPresetIcon = computed(() => {
206
+ if (!props.presetIcon) return null;
207
+ return presetIcons[props.presetIcon];
208
+ });
209
+ </script>
@@ -0,0 +1,22 @@
1
+ <template>
2
+ <div
3
+ data-type="feature-group"
4
+ class="flex mt-8 flex-col items-center container mx-auto justify-center my-2 gap-24 w-full"
5
+ >
6
+ <div
7
+ v-for="(component, index) in $slots.default?.() || []"
8
+ :key="index"
9
+ class="w-full relative group"
10
+ >
11
+ <div class="relative">
12
+ <BaseFeature :background-class-index="index">
13
+ <component :is="component" />
14
+ </BaseFeature>
15
+ </div>
16
+ </div>
17
+ </div>
18
+ </template>
19
+
20
+ <script setup>
21
+ import BaseFeature from './BannerBox.vue'
22
+ </script>
@@ -0,0 +1,5 @@
1
+ <template>
2
+ <li class="mb-2 rounded-md p-2 flex items-center gap-3 hover:bg-accent">
3
+ <slot />
4
+ </li>
5
+ </template>