将 web-admin 从子模块改为仓库内普通目录,便于单仓克隆与维护。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
2e9d020d50
commit
c87732fcd5
515 files changed
+74372
-1
No files matched your search
@@ -0,0 +1,343 @@
|
||||
<!-- 基础横幅组件 -->
|
||||
<template>
|
||||
<div
|
||||
class="art-card basic-banner"
|
||||
:class="[{ 'has-decoration': decoration }, boxStyle]"
|
||||
:style="{ height }"
|
||||
@click="emit('click')"
|
||||
>
|
||||
<!-- 流星效果 -->
|
||||
<div v-if="meteorConfig?.enabled && isDark" class="basic-banner__meteors">
|
||||
<span
|
||||
v-for="(meteor, index) in meteors"
|
||||
:key="index"
|
||||
class="meteor"
|
||||
:style="{
|
||||
top: '-60px',
|
||||
left: `${meteor.x}%`,
|
||||
animationDuration: `${meteor.speed}s`,
|
||||
animationDelay: `${meteor.delay}s`
|
||||
}"
|
||||
></span>
|
||||
</div>
|
||||
|
||||
<div class="basic-banner__content">
|
||||
<!-- title slot -->
|
||||
<slot name="title">
|
||||
<p v-if="title" class="basic-banner__title" :style="{ color: titleColor }">{{ title }}</p>
|
||||
</slot>
|
||||
|
||||
<!-- subtitle slot -->
|
||||
<slot name="subtitle">
|
||||
<p v-if="subtitle" class="basic-banner__subtitle" :style="{ color: subtitleColor }">{{
|
||||
subtitle
|
||||
}}</p>
|
||||
</slot>
|
||||
|
||||
<!-- button slot -->
|
||||
<slot name="button">
|
||||
<div
|
||||
v-if="buttonConfig?.show"
|
||||
class="basic-banner__button"
|
||||
:style="{
|
||||
backgroundColor: buttonColor,
|
||||
color: buttonTextColor,
|
||||
borderRadius: buttonRadius
|
||||
}"
|
||||
@click.stop="emit('buttonClick')"
|
||||
>
|
||||
{{ buttonConfig?.text }}
|
||||
</div>
|
||||
</slot>
|
||||
|
||||
<!-- default slot -->
|
||||
<slot></slot>
|
||||
|
||||
<!-- background image -->
|
||||
<img
|
||||
v-if="imageConfig.src"
|
||||
class="basic-banner__background-image"
|
||||
:src="imageConfig.src"
|
||||
:style="{ width: imageConfig.width, bottom: imageConfig.bottom, right: imageConfig.right }"
|
||||
loading="lazy"
|
||||
alt="背景图片"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref, computed } from 'vue'
|
||||
import { useSettingStore } from '@/store/modules/setting'
|
||||
const settingStore = useSettingStore()
|
||||
const { isDark } = storeToRefs(settingStore)
|
||||
|
||||
defineOptions({ name: 'ArtBasicBanner' })
|
||||
|
||||
// 流星对象接口定义
|
||||
interface Meteor {
|
||||
/** 流星的水平位置(百分比) */
|
||||
x: number
|
||||
/** 流星划过的速度 */
|
||||
speed: number
|
||||
/** 流星出现的延迟时间 */
|
||||
delay: number
|
||||
}
|
||||
|
||||
// 按钮配置接口定义
|
||||
interface ButtonConfig {
|
||||
/** 是否启用按钮 */
|
||||
show: boolean
|
||||
/** 按钮文本 */
|
||||
text: string
|
||||
/** 按钮背景色 */
|
||||
color?: string
|
||||
/** 按钮文字颜色 */
|
||||
textColor?: string
|
||||
/** 按钮圆角大小 */
|
||||
radius?: string
|
||||
}
|
||||
|
||||
// 流星效果配置接口定义
|
||||
interface MeteorConfig {
|
||||
/** 是否启用流星效果 */
|
||||
enabled: boolean
|
||||
/** 流星数量 */
|
||||
count?: number
|
||||
}
|
||||
|
||||
// 背景图片配置接口定义
|
||||
interface ImageConfig {
|
||||
/** 图片源地址 */
|
||||
src: string
|
||||
/** 图片宽度 */
|
||||
width?: string
|
||||
/** 距底部距离 */
|
||||
bottom?: string
|
||||
/** 距右侧距离 */
|
||||
right?: string // 距右侧距离
|
||||
}
|
||||
|
||||
// 组件属性接口定义
|
||||
interface Props {
|
||||
/** 横幅高度 */
|
||||
height?: string
|
||||
/** 标题文本 */
|
||||
title?: string
|
||||
/** 副标题文本 */
|
||||
subtitle?: string
|
||||
/** 盒子样式 */
|
||||
boxStyle?: string
|
||||
/** 是否显示装饰效果 */
|
||||
decoration?: boolean
|
||||
/** 按钮配置 */
|
||||
buttonConfig?: ButtonConfig
|
||||
/** 流星配置 */
|
||||
meteorConfig?: MeteorConfig
|
||||
/** 图片配置 */
|
||||
imageConfig?: ImageConfig
|
||||
/** 标题颜色 */
|
||||
titleColor?: string
|
||||
/** 副标题颜色 */
|
||||
subtitleColor?: string
|
||||
}
|
||||
|
||||
// 组件属性默认值设置
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
height: '11rem',
|
||||
titleColor: 'white',
|
||||
subtitleColor: 'white',
|
||||
boxStyle: '!bg-theme/60',
|
||||
decoration: true,
|
||||
buttonConfig: () => ({
|
||||
show: true,
|
||||
text: '查看',
|
||||
color: '#fff',
|
||||
textColor: '#333',
|
||||
radius: '6px'
|
||||
}),
|
||||
meteorConfig: () => ({ enabled: false, count: 10 }),
|
||||
imageConfig: () => ({ src: '', width: '12rem', bottom: '-3rem', right: '0' })
|
||||
})
|
||||
|
||||
// 定义组件事件
|
||||
const emit = defineEmits<{
|
||||
(e: 'click'): void // 整体点击事件
|
||||
(e: 'buttonClick'): void // 按钮点击事件
|
||||
}>()
|
||||
|
||||
// 计算按钮样式属性
|
||||
const buttonColor = computed(() => props.buttonConfig?.color ?? '#fff')
|
||||
const buttonTextColor = computed(() => props.buttonConfig?.textColor ?? '#333')
|
||||
const buttonRadius = computed(() => props.buttonConfig?.radius ?? '6px')
|
||||
|
||||
// 流星数据初始化
|
||||
const meteors = ref<Meteor[]>([])
|
||||
onMounted(() => {
|
||||
if (props.meteorConfig?.enabled) {
|
||||
meteors.value = generateMeteors(props.meteorConfig?.count ?? 10)
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 生成流星数据数组
|
||||
* @param count 流星数量
|
||||
* @returns 流星数据数组
|
||||
*/
|
||||
function generateMeteors(count: number): Meteor[] {
|
||||
// 计算每个流星的区域宽度
|
||||
const segmentWidth = 100 / count
|
||||
return Array.from({ length: count }, (_, index) => {
|
||||
// 计算流星起始位置
|
||||
const segmentStart = index * segmentWidth
|
||||
// 在区域内随机生成x坐标
|
||||
const x = segmentStart + Math.random() * segmentWidth
|
||||
// 随机决定流星速度快慢
|
||||
const isSlow = Math.random() > 0.5
|
||||
return {
|
||||
x,
|
||||
speed: isSlow ? 5 + Math.random() * 3 : 2 + Math.random() * 2,
|
||||
delay: Math.random() * 5
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.basic-banner {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding: 0 2rem;
|
||||
overflow: hidden;
|
||||
color: white;
|
||||
border-radius: calc(var(--custom-radius) + 2px) !important;
|
||||
|
||||
&__content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
&__title {
|
||||
margin: 0 0 0.5rem;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&__subtitle {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
margin: 0 0 1.5rem;
|
||||
font-size: 0.9rem;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
&__button {
|
||||
box-sizing: border-box;
|
||||
display: inline-block;
|
||||
min-width: 80px;
|
||||
height: var(--el-component-custom-height);
|
||||
padding: 0 12px;
|
||||
font-size: 14px;
|
||||
line-height: var(--el-component-custom-height);
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
&__background-image {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: -3rem;
|
||||
z-index: 0;
|
||||
width: 12rem;
|
||||
}
|
||||
|
||||
&.has-decoration::after {
|
||||
position: absolute;
|
||||
right: -10%;
|
||||
bottom: -20%;
|
||||
width: 60%;
|
||||
height: 140%;
|
||||
content: '';
|
||||
background: rgb(255 255 255 / 10%);
|
||||
border-radius: 30%;
|
||||
transform: rotate(-20deg);
|
||||
}
|
||||
|
||||
&__meteors {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
|
||||
.meteor {
|
||||
position: absolute;
|
||||
width: 2px;
|
||||
height: 60px;
|
||||
background: linear-gradient(
|
||||
to top,
|
||||
rgb(255 255 255 / 40%),
|
||||
rgb(255 255 255 / 10%),
|
||||
transparent
|
||||
);
|
||||
opacity: 0;
|
||||
transform-origin: top left;
|
||||
animation-name: meteor-fall;
|
||||
animation-timing-function: linear;
|
||||
animation-iteration-count: infinite;
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 2px;
|
||||
height: 2px;
|
||||
content: '';
|
||||
background: rgb(255 255 255 / 50%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes meteor-fall {
|
||||
0% {
|
||||
opacity: 1;
|
||||
transform: translate(0, -60px) rotate(-45deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: translate(400px, 340px) rotate(-45deg);
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 640px) {
|
||||
.basic-banner {
|
||||
box-sizing: border-box;
|
||||
justify-content: flex-start;
|
||||
padding: 16px;
|
||||
|
||||
&__title {
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
|
||||
&__background-image {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.has-decoration::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,114 @@
|
||||
<!-- 卡片横幅组件 -->
|
||||
<template>
|
||||
<div class="art-card-sm flex-c flex-col pb-6" :style="{ height: height }">
|
||||
<div class="flex-c flex-col gap-4 text-center">
|
||||
<div class="w-45">
|
||||
<img :src="image" :alt="title" class="w-full h-full object-contain" />
|
||||
</div>
|
||||
<div class="box-border px-4">
|
||||
<p class="mb-2 text-lg font-semibold text-g-800">{{ title }}</p>
|
||||
<p class="m-0 text-sm text-g-600">{{ description }}</p>
|
||||
</div>
|
||||
<div class="flex-c gap-3">
|
||||
<div
|
||||
v-if="cancelButton?.show"
|
||||
class="inline-block h-9 px-3 text-sm/9 c-p select-none rounded-md border border-g-300"
|
||||
:style="{
|
||||
backgroundColor: cancelButton?.color,
|
||||
color: cancelButton?.textColor
|
||||
}"
|
||||
@click="handleCancel"
|
||||
>
|
||||
{{ cancelButton?.text }}
|
||||
</div>
|
||||
<div
|
||||
v-if="button?.show"
|
||||
class="inline-block h-9 px-3 text-sm/9 c-p select-none rounded-md"
|
||||
:style="{ backgroundColor: button?.color, color: button?.textColor }"
|
||||
@click="handleClick"
|
||||
>
|
||||
{{ button?.text }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// 导入默认图标
|
||||
import defaultIcon from '@imgs/3d/icon1.webp'
|
||||
|
||||
defineOptions({ name: 'ArtCardBanner' })
|
||||
|
||||
// 定义卡片横幅组件的属性接口
|
||||
interface CardBannerProps {
|
||||
/** 高度 */
|
||||
height?: string
|
||||
/** 图片路径 */
|
||||
image?: string
|
||||
/** 标题文本 */
|
||||
title: string
|
||||
/** 描述文本 */
|
||||
description: string
|
||||
/** 主按钮配置 */
|
||||
button?: {
|
||||
/** 是否显示 */
|
||||
show?: boolean
|
||||
/** 按钮文本 */
|
||||
text?: string
|
||||
/** 背景颜色 */
|
||||
color?: string
|
||||
/** 文字颜色 */
|
||||
textColor?: string
|
||||
}
|
||||
/** 取消按钮配置 */
|
||||
cancelButton?: {
|
||||
/** 是否显示 */
|
||||
show?: boolean
|
||||
/** 按钮文本 */
|
||||
text?: string
|
||||
/** 背景颜色 */
|
||||
color?: string
|
||||
/** 文字颜色 */
|
||||
textColor?: string
|
||||
}
|
||||
}
|
||||
|
||||
// 定义组件属性默认值
|
||||
withDefaults(defineProps<CardBannerProps>(), {
|
||||
height: '24rem',
|
||||
image: defaultIcon,
|
||||
title: '',
|
||||
description: '',
|
||||
// 主按钮默认配置
|
||||
button: () => ({
|
||||
show: true,
|
||||
text: '查看详情',
|
||||
color: 'var(--theme-color)',
|
||||
textColor: '#fff'
|
||||
}),
|
||||
// 取消按钮默认配置
|
||||
cancelButton: () => ({
|
||||
show: false,
|
||||
text: '取消',
|
||||
color: '#f5f5f5',
|
||||
textColor: '#666'
|
||||
})
|
||||
})
|
||||
|
||||
// 定义组件事件
|
||||
const emit = defineEmits<{
|
||||
(e: 'click'): void // 主按钮点击事件
|
||||
(e: 'cancel'): void // 取消按钮点击事件
|
||||
}>()
|
||||
|
||||
// 主按钮点击处理函数
|
||||
const handleClick = () => {
|
||||
emit('click')
|
||||
}
|
||||
|
||||
// 取消按钮点击处理函数
|
||||
const handleCancel = () => {
|
||||
emit('cancel')
|
||||
}
|
||||
</script>
|
||||
Reference in new issue
Block a user