@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,53 @@
|
|
1
|
+
class Feature {
|
2
|
+
private translations: Map<string, string>;
|
3
|
+
public emoji: string = '';
|
4
|
+
public link: string = '';
|
5
|
+
public presetIcon: string = '';
|
6
|
+
|
7
|
+
private constructor() {
|
8
|
+
this.translations = new Map();
|
9
|
+
this.translations.set('zh-cn', '');
|
10
|
+
this.translations.set('en', '');
|
11
|
+
}
|
12
|
+
|
13
|
+
public static createWithIcon(emoji: string): Feature {
|
14
|
+
const feature = new Feature();
|
15
|
+
feature.emoji = emoji;
|
16
|
+
return feature;
|
17
|
+
}
|
18
|
+
|
19
|
+
public static createWithPresetIcon(presetIcon: string): Feature {
|
20
|
+
const feature = new Feature();
|
21
|
+
feature.presetIcon = presetIcon;
|
22
|
+
return feature;
|
23
|
+
}
|
24
|
+
|
25
|
+
public setTitle(title: string, lang: string): Feature {
|
26
|
+
this.translations.set(lang, title);
|
27
|
+
return this;
|
28
|
+
}
|
29
|
+
|
30
|
+
public setLink(link: string): Feature {
|
31
|
+
this.link = link;
|
32
|
+
return this;
|
33
|
+
}
|
34
|
+
|
35
|
+
public setPresetIcon(presetIcon: string): Feature {
|
36
|
+
this.presetIcon = presetIcon;
|
37
|
+
return this;
|
38
|
+
}
|
39
|
+
|
40
|
+
public setZh(title: string): Feature {
|
41
|
+
return this.setTitle(title, 'zh-cn');
|
42
|
+
}
|
43
|
+
|
44
|
+
public setEn(title: string): Feature {
|
45
|
+
return this.setTitle(title, 'en');
|
46
|
+
}
|
47
|
+
|
48
|
+
public getTitle(lang: string): string {
|
49
|
+
return this.translations.get(lang) || '';
|
50
|
+
}
|
51
|
+
}
|
52
|
+
|
53
|
+
export default Feature;
|
package/dist/icons.ts
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
export { default as SocialIcon } from './components/icons/SocialIcon.astro';
|
2
|
+
export { default as TwitterIcon } from './components/icons/TwitterIcon.astro';
|
3
|
+
export { default as UserIcon } from './components/icons/UserIcon.astro';
|
4
|
+
export { default as WarningIcon } from './components/icons/WarningIcon.astro';
|
5
|
+
export { default as XCircle } from './components/icons/XCircle.astro';
|
6
|
+
export { default as InfoIcon } from './components/icons/InfoIcon.astro';
|
7
|
+
export { default as LinkIcon } from './components/icons/LinkIcon.astro';
|
8
|
+
export { default as LinkedinIcon } from './components/icons/LinkedinIcon.astro';
|
9
|
+
export { default as MenuIcon } from './components/icons/MenuIcon.astro';
|
10
|
+
export { default as SearchIcon } from './components/icons/SearchIcon.astro';
|
11
|
+
export { default as SuccessIcon } from './components/icons/SuccessIcon.astro';
|
12
|
+
export { default as SunCloudyIcon } from './components/icons/SunCloudyIcon.astro';
|
13
|
+
export { default as AlertTriangle } from './components/icons/AlertTriangle.astro';
|
14
|
+
export { default as CalendarIcon } from './components/icons/CalendarIcon.astro';
|
15
|
+
export { default as CheckCircle } from './components/icons/CheckCircle.astro';
|
16
|
+
export { default as CheckIcon } from './components/icons/CheckIcon.astro';
|
17
|
+
export { default as ClipboardIcon } from './components/icons/ClipboardIcon.astro';
|
18
|
+
export { default as CloseIcon } from './components/icons/CloseIcon.astro';
|
19
|
+
export { default as ErrorIcon } from './components/icons/ErrorIcon.astro';
|
20
|
+
export { default as GithubIcon } from './components/icons/GithubIcon.astro';
|
21
|
+
export { default as InfoCircle } from './components/icons/InfoCircle.astro';
|
22
|
+
export { default as SettingsIcon } from './components/icons/SettingsIcon.astro';
|
package/dist/index.ts
CHANGED
@@ -46,27 +46,7 @@ export { default as Heading } from './components/typography/Heading.astro';
|
|
46
46
|
export { default as ErrorPage404 } from './components/errors/404.astro';
|
47
47
|
|
48
48
|
// Icons
|
49
|
-
export
|
50
|
-
export { default as TwitterIcon } from './components/icons/TwitterIcon.astro';
|
51
|
-
export { default as UserIcon } from './components/icons/UserIcon.astro';
|
52
|
-
export { default as WarningIcon } from './components/icons/WarningIcon.astro';
|
53
|
-
export { default as XCircle } from './components/icons/XCircle.astro';
|
54
|
-
export { default as InfoIcon } from './components/icons/InfoIcon.astro';
|
55
|
-
export { default as LinkIcon } from './components/icons/LinkIcon.astro';
|
56
|
-
export { default as LinkedinIcon } from './components/icons/LinkedinIcon.astro';
|
57
|
-
export { default as MenuIcon } from './components/icons/MenuIcon.astro';
|
58
|
-
export { default as SearchIcon } from './components/icons/SearchIcon.astro';
|
59
|
-
export { default as SuccessIcon } from './components/icons/SuccessIcon.astro';
|
60
|
-
export { default as SunCloudyIcon } from './components/icons/SunCloudyIcon.astro';
|
61
|
-
export { default as AlertTriangle } from './components/icons/AlertTriangle.astro';
|
62
|
-
export { default as CalendarIcon } from './components/icons/CalendarIcon.astro';
|
63
|
-
export { default as CheckCircle } from './components/icons/CheckCircle.astro';
|
64
|
-
export { default as CheckIcon } from './components/icons/CheckIcon.astro';
|
65
|
-
export { default as ClipboardIcon } from './components/icons/ClipboardIcon.astro';
|
66
|
-
export { default as CloseIcon } from './components/icons/CloseIcon.astro';
|
67
|
-
export { default as ErrorIcon } from './components/icons/ErrorIcon.astro';
|
68
|
-
export { default as GithubIcon } from './components/icons/GithubIcon.astro';
|
69
|
-
export { default as InfoCircle } from './components/icons/InfoCircle.astro';
|
49
|
+
export * from './icons';
|
70
50
|
|
71
51
|
// Containers
|
72
52
|
export { default as Container } from './components/containers/Container.astro';
|
@@ -0,0 +1,120 @@
|
|
1
|
+
<!--
|
2
|
+
@component AlertDialog
|
3
|
+
|
4
|
+
@description
|
5
|
+
AlertDialog 组件用于显示简单的确认对话框,支持国际化,带有淡入淡出动画效果。
|
6
|
+
|
7
|
+
@usage
|
8
|
+
基本用法:
|
9
|
+
```vue
|
10
|
+
<AlertDialog v-model="showDialog" message="操作已完成" />
|
11
|
+
```
|
12
|
+
|
13
|
+
指定语言:
|
14
|
+
```vue
|
15
|
+
<AlertDialog v-model="showDialog" message="Operation completed" lang="en" />
|
16
|
+
```
|
17
|
+
|
18
|
+
组合使用:
|
19
|
+
```vue
|
20
|
+
<template>
|
21
|
+
<button @click="showDialog = true">显示对话框</button>
|
22
|
+
<AlertDialog v-model="showDialog" message="确认要继续吗?" />
|
23
|
+
</template>
|
24
|
+
|
25
|
+
<script setup lang="ts">
|
26
|
+
import { ref } from 'vue';
|
27
|
+
import { AlertDialog } from 'cosy-ui';
|
28
|
+
|
29
|
+
const showDialog = ref(false);
|
30
|
+
</script>
|
31
|
+
```
|
32
|
+
|
33
|
+
@props
|
34
|
+
@prop {boolean} modelValue - 控制对话框显示状态,支持v-model双向绑定
|
35
|
+
@prop {string} message - 对话框显示的消息内容
|
36
|
+
@prop {('zh-cn'|'en')} [lang='zh-cn'] - 语言设置,影响按钮文本
|
37
|
+
|
38
|
+
@emits
|
39
|
+
@emit {update:modelValue} - 当对话框关闭时触发,用于更新v-model绑定值
|
40
|
+
-->
|
41
|
+
|
42
|
+
<script lang="ts">
|
43
|
+
import '../app.css'
|
44
|
+
import { defineComponent } from 'vue'
|
45
|
+
|
46
|
+
type MessageKey = 'confirm';
|
47
|
+
|
48
|
+
interface Messages {
|
49
|
+
[key: string]: {
|
50
|
+
[key in MessageKey]: string;
|
51
|
+
};
|
52
|
+
}
|
53
|
+
|
54
|
+
export default defineComponent({
|
55
|
+
name: 'AlertDialog',
|
56
|
+
props: {
|
57
|
+
modelValue: {
|
58
|
+
type: Boolean,
|
59
|
+
required: true
|
60
|
+
},
|
61
|
+
message: {
|
62
|
+
type: String,
|
63
|
+
required: true
|
64
|
+
},
|
65
|
+
lang: {
|
66
|
+
type: String as () => 'zh-cn' | 'en',
|
67
|
+
default: 'zh-cn'
|
68
|
+
}
|
69
|
+
},
|
70
|
+
emits: ['update:modelValue'],
|
71
|
+
setup(props) {
|
72
|
+
// 多语言文本
|
73
|
+
const t = (key: MessageKey) => {
|
74
|
+
const messages: Messages = {
|
75
|
+
'zh-cn': {
|
76
|
+
confirm: '确定'
|
77
|
+
},
|
78
|
+
'en': {
|
79
|
+
confirm: 'OK'
|
80
|
+
}
|
81
|
+
};
|
82
|
+
return messages[props.lang][key];
|
83
|
+
};
|
84
|
+
|
85
|
+
return {
|
86
|
+
t
|
87
|
+
};
|
88
|
+
}
|
89
|
+
})
|
90
|
+
</script>
|
91
|
+
|
92
|
+
<template>
|
93
|
+
<Transition name="fade" class="cosy:transition-opacity cosy:duration-200 cosy:ease-in-out">
|
94
|
+
<div v-if="modelValue"
|
95
|
+
class="cosy:fixed cosy:inset-0 cosy:z-50 cosy:flex cosy:items-center cosy:justify-center cosy:opacity-100 cosy:enter:opacity-0 cosy:leave:opacity-0">
|
96
|
+
<!-- 背景遮罩 -->
|
97
|
+
<div class="cosy:absolute cosy:inset-0 cosy:bg-base-200/80 cosy:backdrop-blur-sm"
|
98
|
+
@click="$emit('update:modelValue', false)" />
|
99
|
+
|
100
|
+
<!-- 对话框 -->
|
101
|
+
<div
|
102
|
+
class="cosy:relative cosy:bg-base-100 cosy:rounded-xl cosy:shadow-lg cosy:w-[400px] cosy:transform cosy:transition-all">
|
103
|
+
<!-- 内容区域 -->
|
104
|
+
<div class="cosy:p-6">
|
105
|
+
<p class="cosy:text-base-content">
|
106
|
+
{{ message }}
|
107
|
+
</p>
|
108
|
+
</div>
|
109
|
+
|
110
|
+
<!-- 按钮区域 -->
|
111
|
+
<div class="cosy:flex cosy:border-t cosy:border-base-300">
|
112
|
+
<button class="cosy:btn cosy:btn-ghost cosy:flex-1 cosy:rounded-none cosy:rounded-b-xl"
|
113
|
+
@click="$emit('update:modelValue', false)">
|
114
|
+
{{ t('confirm') }}
|
115
|
+
</button>
|
116
|
+
</div>
|
117
|
+
</div>
|
118
|
+
</div>
|
119
|
+
</Transition>
|
120
|
+
</template>
|
@@ -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>
|