@coffic/cosy-ui 0.6.8 → 0.6.12
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/components/icons/SettingsIcon.astro +36 -0
- package/dist/entities/Banner.ts +105 -0
- package/dist/entities/Feature.ts +53 -0
- package/dist/icons.ts +22 -0
- package/dist/index.ts +1 -21
- package/dist/vue/AlertDialog.vue +120 -0
- package/dist/vue/BannerBox.vue +267 -0
- package/dist/vue/ConfirmDialog.vue +76 -0
- package/dist/vue/FeatureButton.vue +95 -0
- package/dist/vue/FeatureCard.vue +209 -0
- package/dist/vue/FeatureGroup.vue +22 -0
- package/dist/vue/ListItem.vue +5 -0
- package/dist/vue/MacWindow.vue +224 -0
- package/dist/vue/SmartBanner.vue +45 -0
- package/dist/vue/SmartHero.vue +94 -0
- package/dist/vue/SmartLink.vue +21 -0
- package/dist/vue/TagList.vue +23 -0
- package/dist/vue/WildBanner.vue +15 -0
- package/dist/vue/iPhoneWindow.vue +189 -0
- package/dist/vue.ts +14 -0
- package/package.json +7 -3
@@ -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,224 @@
|
|
1
|
+
<!--
|
2
|
+
@component MacWindow
|
3
|
+
|
4
|
+
@description
|
5
|
+
MacWindow 组件模拟 macOS 风格的应用窗口,包含标题栏、工具栏按钮、标签页和状态栏。
|
6
|
+
适用于创建模拟桌面应用界面或代码编辑器等场景。
|
7
|
+
|
8
|
+
@usage
|
9
|
+
基本用法:
|
10
|
+
```vue
|
11
|
+
<MacWindow title="代码编辑器">
|
12
|
+
<div>窗口内容</div>
|
13
|
+
</MacWindow>
|
14
|
+
```
|
15
|
+
|
16
|
+
带标签页:
|
17
|
+
```vue
|
18
|
+
<template>
|
19
|
+
<MacWindow
|
20
|
+
title="设置"
|
21
|
+
v-model="activeTab"
|
22
|
+
:tabs="[
|
23
|
+
{ label: '通用', value: 'general' },
|
24
|
+
{ label: '外观', value: 'appearance' },
|
25
|
+
{ label: '高级', value: 'advanced' }
|
26
|
+
]"
|
27
|
+
>
|
28
|
+
<div v-if="activeTab === 'general'">通用设置内容</div>
|
29
|
+
<div v-if="activeTab === 'appearance'">外观设置内容</div>
|
30
|
+
<div v-if="activeTab === 'advanced'">高级设置内容</div>
|
31
|
+
</MacWindow>
|
32
|
+
</template>
|
33
|
+
|
34
|
+
<script setup>
|
35
|
+
import { ref } from 'vue';
|
36
|
+
import { MacWindow } from 'cosy-ui';
|
37
|
+
|
38
|
+
const activeTab = ref('general');
|
39
|
+
</script>
|
40
|
+
```
|
41
|
+
|
42
|
+
带工具栏和状态栏:
|
43
|
+
```vue
|
44
|
+
<MacWindow title="文件浏览器">
|
45
|
+
<template #toolbar>
|
46
|
+
<button class="cosy:btn cosy:btn-ghost cosy:btn-sm">
|
47
|
+
<SearchIcon class="cosy:w-4 cosy:h-4" />
|
48
|
+
</button>
|
49
|
+
<button class="cosy:btn cosy:btn-ghost cosy:btn-sm">
|
50
|
+
<SettingsIcon class="cosy:w-4 cosy:h-4" />
|
51
|
+
</button>
|
52
|
+
</template>
|
53
|
+
|
54
|
+
<div>窗口内容</div>
|
55
|
+
|
56
|
+
<template #status>
|
57
|
+
<div class="cosy:text-xs">就绪</div>
|
58
|
+
<button class="cosy:btn cosy:btn-ghost cosy:btn-xs">
|
59
|
+
<InfoIcon class="cosy:w-4 cosy:h-4" />
|
60
|
+
</button>
|
61
|
+
</template>
|
62
|
+
</MacWindow>
|
63
|
+
```
|
64
|
+
|
65
|
+
@props
|
66
|
+
@prop {String} [height='h-96'] - 窗口高度
|
67
|
+
@prop {String} [title=''] - 窗口标题
|
68
|
+
@prop {Boolean} [withShadow=true] - 是否显示阴影效果
|
69
|
+
@prop {Array} [tabs=[]] - 标签页数组,每个标签包含label和value属性
|
70
|
+
@prop {String} [modelValue=''] - 当前选中的标签页value值,可使用v-model绑定
|
71
|
+
|
72
|
+
@slots
|
73
|
+
@slot default - 窗口主要内容
|
74
|
+
@slot sidebar - 侧边栏内容
|
75
|
+
@slot toolbar - 工具栏内容,位于标题栏右侧
|
76
|
+
@slot status - 状态栏内容,位于窗口底部
|
77
|
+
|
78
|
+
@emits
|
79
|
+
@emit {close} - 点击关闭按钮时触发
|
80
|
+
@emit {minimize} - 点击最小化按钮时触发
|
81
|
+
@emit {maximize} - 点击最大化按钮时触发
|
82
|
+
@emit {update:modelValue} - 切换标签页时触发,用于v-model
|
83
|
+
-->
|
84
|
+
|
85
|
+
<script lang="ts">
|
86
|
+
import '../app.css'
|
87
|
+
import AlertDialog from './AlertDialog.vue'
|
88
|
+
import { ref, defineComponent } from 'vue'
|
89
|
+
|
90
|
+
// 定义标签页类型
|
91
|
+
interface Tab {
|
92
|
+
label: string;
|
93
|
+
value: string;
|
94
|
+
}
|
95
|
+
|
96
|
+
export default defineComponent({
|
97
|
+
name: 'MacWindow',
|
98
|
+
components: {
|
99
|
+
AlertDialog
|
100
|
+
},
|
101
|
+
props: {
|
102
|
+
height: {
|
103
|
+
type: String,
|
104
|
+
default: 'h-96'
|
105
|
+
},
|
106
|
+
title: {
|
107
|
+
type: String,
|
108
|
+
default: ''
|
109
|
+
},
|
110
|
+
withShadow: {
|
111
|
+
type: Boolean,
|
112
|
+
default: true
|
113
|
+
},
|
114
|
+
tabs: {
|
115
|
+
type: Array as () => Tab[],
|
116
|
+
default: () => []
|
117
|
+
},
|
118
|
+
modelValue: {
|
119
|
+
type: String,
|
120
|
+
default: ''
|
121
|
+
}
|
122
|
+
},
|
123
|
+
emits: ['close', 'minimize', 'maximize', 'update:modelValue'],
|
124
|
+
setup(props, { emit }) {
|
125
|
+
const showAlertDialog = ref(false)
|
126
|
+
const alertMessage = ref('')
|
127
|
+
|
128
|
+
const handleCloseWindow = () => {
|
129
|
+
alertMessage.value = '关闭APP窗口(这是演示,不会真实操作)'
|
130
|
+
showAlertDialog.value = true
|
131
|
+
emit('close')
|
132
|
+
}
|
133
|
+
|
134
|
+
const handleMinimizeWindow = () => {
|
135
|
+
alertMessage.value = '最小化窗口(这是演示,不会真实操作)'
|
136
|
+
showAlertDialog.value = true
|
137
|
+
emit('minimize')
|
138
|
+
}
|
139
|
+
|
140
|
+
const handleMaximizeWindow = () => {
|
141
|
+
alertMessage.value = '最大化窗口(这是演示,不会真实操作)'
|
142
|
+
showAlertDialog.value = true
|
143
|
+
emit('maximize')
|
144
|
+
}
|
145
|
+
|
146
|
+
const handleTabClick = (value: string) => {
|
147
|
+
emit('update:modelValue', value)
|
148
|
+
}
|
149
|
+
|
150
|
+
return {
|
151
|
+
showAlertDialog,
|
152
|
+
alertMessage,
|
153
|
+
handleCloseWindow,
|
154
|
+
handleMinimizeWindow,
|
155
|
+
handleMaximizeWindow,
|
156
|
+
handleTabClick
|
157
|
+
}
|
158
|
+
}
|
159
|
+
})
|
160
|
+
</script>
|
161
|
+
|
162
|
+
<template>
|
163
|
+
<div class="cosy:flex cosy:max-w-5xl cosy:mx-auto cosy:bg-base-100 cosy:relative cosy:rounded-2xl cosy:overflow-hidden"
|
164
|
+
:class="[
|
165
|
+
height,
|
166
|
+
withShadow ? 'cosy:shadow-lg' : ''
|
167
|
+
]">
|
168
|
+
<!-- 窗口控制按钮 -->
|
169
|
+
<div
|
170
|
+
class="cosy:absolute cosy:top-0 cosy:left-0 cosy:right-0 cosy:flex cosy:items-center cosy:h-12 cosy:px-4 cosy:bg-base-200 cosy:border-b cosy:border-base-300">
|
171
|
+
<div class="cosy:flex cosy:items-center cosy:space-x-2">
|
172
|
+
<div class="cosy:w-3 cosy:h-3 cosy:rounded-full cosy:bg-error cosy:cursor-pointer hover:cosy:opacity-80 cosy:transition-opacity"
|
173
|
+
@click="handleCloseWindow" />
|
174
|
+
<div class="cosy:w-3 cosy:h-3 cosy:rounded-full cosy:bg-warning cosy:cursor-pointer hover:cosy:opacity-80 cosy:transition-opacity"
|
175
|
+
@click="handleMinimizeWindow" />
|
176
|
+
<div class="cosy:w-3 cosy:h-3 cosy:rounded-full cosy:bg-success cosy:cursor-pointer hover:cosy:opacity-80 cosy:transition-opacity"
|
177
|
+
@click="handleMaximizeWindow" />
|
178
|
+
</div>
|
179
|
+
<div class="cosy:ml-6 cosy:text-sm cosy:font-medium cosy:text-base-content">
|
180
|
+
{{ title }}
|
181
|
+
</div>
|
182
|
+
|
183
|
+
<!-- 标签选择器 -->
|
184
|
+
<div v-if="tabs?.length" class="cosy:flex-1 cosy:flex cosy:justify-center">
|
185
|
+
<div class="cosy:inline-flex cosy:rounded-lg cosy:bg-base-300 cosy:p-1">
|
186
|
+
<button v-for="(tab, index) in tabs" :key="index" :class="[
|
187
|
+
'cosy:px-3 cosy:py-1 cosy:text-sm cosy:rounded-md cosy:transition-colors',
|
188
|
+
modelValue === tab.value
|
189
|
+
? 'cosy:bg-base-100 cosy:text-base-content cosy:shadow'
|
190
|
+
: 'cosy:text-base-content/70 hover:cosy:text-base-content'
|
191
|
+
]" @click="handleTabClick(tab.value)">
|
192
|
+
{{ tab.label }}
|
193
|
+
</button>
|
194
|
+
</div>
|
195
|
+
</div>
|
196
|
+
|
197
|
+
<!-- 工具栏插槽 -->
|
198
|
+
<div class="cosy:ml-auto cosy:flex cosy:items-center cosy:space-x-2">
|
199
|
+
<slot name="toolbar"></slot>
|
200
|
+
</div>
|
201
|
+
</div>
|
202
|
+
|
203
|
+
<!-- 主要内容区域 -->
|
204
|
+
<div class="cosy:flex-1 cosy:flex cosy:flex-col cosy:pt-12 cosy:h-full">
|
205
|
+
<div class="cosy:flex cosy:flex-1 cosy:h-full cosy:overflow-hidden">
|
206
|
+
<!-- 左侧栏插槽 -->
|
207
|
+
<slot name="sidebar" />
|
208
|
+
|
209
|
+
<!-- 主内容插槽 -->
|
210
|
+
<slot />
|
211
|
+
</div>
|
212
|
+
|
213
|
+
<!-- 底部状态栏 -->
|
214
|
+
<div v-if="$slots.status"
|
215
|
+
class="cosy:h-6 cosy:bg-base-200/95 cosy:border-t cosy:border-base-300 cosy:flex cosy:items-center cosy:justify-end cosy:px-4 cosy:text-sm">
|
216
|
+
<div class="cosy:flex cosy:items-center cosy:space-x-2">
|
217
|
+
<slot name="status"></slot>
|
218
|
+
</div>
|
219
|
+
</div>
|
220
|
+
</div>
|
221
|
+
</div>
|
222
|
+
<!-- AlertDialog 组件 -->
|
223
|
+
<AlertDialog v-model="showAlertDialog" :message="alertMessage" />
|
224
|
+
</template>
|
@@ -0,0 +1,45 @@
|
|
1
|
+
<script setup lang="ts">
|
2
|
+
import Banner from '../entities/Banner';
|
3
|
+
import BannerBox from './BannerBox.vue';
|
4
|
+
import FeatureCard from './FeatureCard.vue';
|
5
|
+
|
6
|
+
defineProps({
|
7
|
+
lang: {
|
8
|
+
type: String,
|
9
|
+
default: 'en',
|
10
|
+
validator: (value: string) => ['en', 'zh-cn'].includes(value)
|
11
|
+
},
|
12
|
+
banner: {
|
13
|
+
type: Banner,
|
14
|
+
required: true
|
15
|
+
},
|
16
|
+
backgroundClassIndex: {
|
17
|
+
type: Number,
|
18
|
+
default: 0
|
19
|
+
}
|
20
|
+
})
|
21
|
+
</script>
|
22
|
+
|
23
|
+
<template>
|
24
|
+
<BannerBox :background-class-index="backgroundClassIndex">
|
25
|
+
<div class="py-16 px-8 text-center w-full rounded-2xl" data-type="smart-banner">
|
26
|
+
<h2 class="text-4xl mb-4">
|
27
|
+
{{ banner.getTitle(lang) }}
|
28
|
+
</h2>
|
29
|
+
|
30
|
+
<p v-if="banner.getDescription(lang).length > 0" class="text-lg text-center max-w-2xl mx-auto">
|
31
|
+
{{ banner.getDescription(lang) }}
|
32
|
+
</p>
|
33
|
+
|
34
|
+
<div class="flex flex-row justify-center gap-8 mx-auto w-full mt-24">
|
35
|
+
<FeatureCard v-for="feature in banner.getFeatures()" :key="feature.getTitle(lang)"
|
36
|
+
:emoji="feature.emoji" :title="feature.getTitle(lang)" :link="feature.link" />
|
37
|
+
</div>
|
38
|
+
|
39
|
+
<div class="mt-12">
|
40
|
+
<component :is="banner.getComponent()" v-if="banner.getComponent()"
|
41
|
+
v-bind="banner.getComponentProps()" />
|
42
|
+
</div>
|
43
|
+
</div>
|
44
|
+
</BannerBox>
|
45
|
+
</template>
|
@@ -0,0 +1,94 @@
|
|
1
|
+
<template>
|
2
|
+
<div
|
3
|
+
class="py-16 px-8 text-center w-full min-h-screen relative overflow-hidden
|
4
|
+
"
|
5
|
+
>
|
6
|
+
<div class="relative z-10 rounded-lg w-full h-full">
|
7
|
+
<template v-if="image.src">
|
8
|
+
<img
|
9
|
+
:src="image.src"
|
10
|
+
:alt="image.alt"
|
11
|
+
class="h-1/2 mx-auto mb-8 drop-shadow-xl"
|
12
|
+
>
|
13
|
+
</template>
|
14
|
+
|
15
|
+
<h2 class="text-4xl mb-4 animate-fade-up">
|
16
|
+
{{ title }}
|
17
|
+
</h2>
|
18
|
+
<p class="text-lg mb-12 text-center max-w-2xl mx-auto">
|
19
|
+
{{ description }}
|
20
|
+
</p>
|
21
|
+
|
22
|
+
<div class="my-12 w-full">
|
23
|
+
<slot name="app" />
|
24
|
+
</div>
|
25
|
+
|
26
|
+
<div class="flex flex-row justify-center gap-8 mx-auto w-full">
|
27
|
+
<a
|
28
|
+
v-for="link in links"
|
29
|
+
:key="link.text"
|
30
|
+
:href="link.href"
|
31
|
+
target="_blank"
|
32
|
+
class="px-6 py-3 rounded-lg bg-blue-600 text-white font-medium
|
33
|
+
transition-all duration-300 ease-in-out
|
34
|
+
hover:bg-blue-700 hover:shadow-lg hover:-translate-y-0.5
|
35
|
+
focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2
|
36
|
+
active:bg-blue-800 active:translate-y-0"
|
37
|
+
>
|
38
|
+
{{ link.text }}
|
39
|
+
</a>
|
40
|
+
</div>
|
41
|
+
</div>
|
42
|
+
</div>
|
43
|
+
</template>
|
44
|
+
|
45
|
+
<script setup lang="ts">
|
46
|
+
import type { PropType } from 'vue'
|
47
|
+
|
48
|
+
interface Link {
|
49
|
+
text: string;
|
50
|
+
href: string;
|
51
|
+
}
|
52
|
+
|
53
|
+
defineProps({
|
54
|
+
title: {
|
55
|
+
type: String,
|
56
|
+
required: true
|
57
|
+
},
|
58
|
+
description: {
|
59
|
+
type: String,
|
60
|
+
required: true
|
61
|
+
},
|
62
|
+
image: {
|
63
|
+
type: Object,
|
64
|
+
required: false,
|
65
|
+
default: () => ({
|
66
|
+
src: '',
|
67
|
+
alt: ''
|
68
|
+
})
|
69
|
+
},
|
70
|
+
links: {
|
71
|
+
type: Array as PropType<Link[]>,
|
72
|
+
required: true,
|
73
|
+
default: () => []
|
74
|
+
}
|
75
|
+
})
|
76
|
+
</script>
|
77
|
+
|
78
|
+
<style scoped>
|
79
|
+
@keyframes fade-up {
|
80
|
+
from {
|
81
|
+
opacity: 0;
|
82
|
+
transform: translateY(-20px);
|
83
|
+
}
|
84
|
+
|
85
|
+
to {
|
86
|
+
opacity: 1;
|
87
|
+
transform: translateY(0);
|
88
|
+
}
|
89
|
+
}
|
90
|
+
|
91
|
+
.animate-fade-up {
|
92
|
+
animation: fade-up 0.8s ease-out forwards;
|
93
|
+
}
|
94
|
+
</style>
|
@@ -0,0 +1,21 @@
|
|
1
|
+
<script setup lang="ts">
|
2
|
+
defineProps<{
|
3
|
+
href: string;
|
4
|
+
external?: boolean;
|
5
|
+
externalClass?: string;
|
6
|
+
}>();
|
7
|
+
</script>
|
8
|
+
|
9
|
+
<template>
|
10
|
+
<a
|
11
|
+
:href="href"
|
12
|
+
:target="external ? '_blank' : undefined"
|
13
|
+
:rel="external ? 'noopener noreferrer' : undefined"
|
14
|
+
:class="{
|
15
|
+
'no-underline rounded-md p-1 transition-all duration-300 hover:text-primary/80': true,
|
16
|
+
[externalClass]: external
|
17
|
+
}"
|
18
|
+
>
|
19
|
+
<slot />
|
20
|
+
</a>
|
21
|
+
</template>
|
@@ -0,0 +1,23 @@
|
|
1
|
+
<template>
|
2
|
+
<div class="flex flex-col gap-1 card bg-base-100 shadow-xl p-4 rounded-md">
|
3
|
+
<a
|
4
|
+
v-for="tag in tags"
|
5
|
+
:key="tag"
|
6
|
+
:href="`/blogs/tags/${tag}`"
|
7
|
+
class="badge badge-primary no-underline"
|
8
|
+
>
|
9
|
+
{{ tag }}
|
10
|
+
</a>
|
11
|
+
</div>
|
12
|
+
</template>
|
13
|
+
|
14
|
+
<script>
|
15
|
+
export default {
|
16
|
+
props: {
|
17
|
+
tags: {
|
18
|
+
type: Array,
|
19
|
+
required: true
|
20
|
+
}
|
21
|
+
}
|
22
|
+
}
|
23
|
+
</script>
|
@@ -0,0 +1,15 @@
|
|
1
|
+
<template>
|
2
|
+
<div class="w-full px-8 z-50 mx-auto">
|
3
|
+
<div
|
4
|
+
class="mt-0 hero p-4 rounded-2xl bg-base-300/50 dark:bg-base-200/50 backdrop-blur-xl hover:shadow-2xl transform duration-100"
|
5
|
+
>
|
6
|
+
<div class="text-center hero-content">
|
7
|
+
<div class="max-w-md flex flex-row justify-center items-center mx-auto">
|
8
|
+
<p class="text-2xl md:text-3xl font-bold text-base-content">
|
9
|
+
<slot />
|
10
|
+
</p>
|
11
|
+
</div>
|
12
|
+
</div>
|
13
|
+
</div>
|
14
|
+
</div>
|
15
|
+
</template>
|