前言
最近,刚好也是游玩了鸣潮这款游戏,然后在B站上看到了一些关于鸣潮的电子设定类网站,观摩了下打算在使用组件来写。在这个过程中,会去除了一些光效并采用自己之前写的一个卡片类文章组件。
注意
大多数组件未适配多角色类型,可以在该版本的基础上进行优化,在本站适配完全部角色后会移除该提示。
档案组件
人物主体
<script setup lang="ts">
import Title from '../card/title.vue';
defineProps<{
类型: "爱弥斯" | "尤诺" | "奥古斯塔"
头像?: string
徽章?: Record<string, string>
名字?: string
标签?: Record<string, string>
简介?: {
上部分?: string
称号?: string
下部分?: string
};
详情信息?: Record<string, string>
报告?: {
顶部标题?: string
主标题?: string
副标题?: string
内容?: Record<string, string>
状态?: string
权限?: string
更新?: string
}
}>();
</script>
<template>
<div class="heroMain">
<div class="heroCard">
<div class="leftInfo">
<NuxtImg class="avatarImage" :src="头像" />
<h3 class="avatarName">
{{ 名字 }}
</h3>
<div class="avatarMeta">
<span class="MetaSpan" v-for="([key, value]) in Object.entries(徽章 ?? {})" :key="key"> {{key}}:{{value }} </span>
</div>
</div>
<div class="rightInfo">
<div class="panelMain">
<Title title="简介"></Title>
<p class="heroDesc">
{{ 简介?.上部分 }}<span class="lightDesc">{{ 简介?.称号 }}</span
>{{ 简介?.下部分 }}
</p>
<Title title="标签"></Title>
<span class="tagItem" style="margin-top: 0.5em;margin-bottom: 0.5em;">
<span class="tag" v-for="([key, value]) in Object.entries(标签 ?? {})" :key="key">
#{{ value }}
</span>
</span>
<Title title="详情信息"></Title>
<div class="infoMain">
<div
class="infoCard"
v-for="([key, value]) in Object.entries(详情信息 ?? {})"
:key="key"
>
<div class="infoLabel">{{ key }}</div>
<div class="infoValue">{{ value }}</div>
</div>
</div>
<Title :title="报告?.顶部标题"></Title>
<div class="statusMain" style="margin-top: 0.5em;">
<div class="statusHeader">
<div class="HeaderTitle">
{{ 报告?.主标题 }}
</div>
<div class="HeaderSub" style="font-size: 0.5em;">
{{ 报告?.副标题 }}
</div>
</div>
<div class="statusContent">
<p v-for="([key, value]) in Object.entries(报告?.内容 ?? {})" :key="key">
{{ value }}
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.heroMain {
width: 100%;
height: 320px;
background: var(--ld-bg-card);
border: 1px solid var(--c-border);
border-radius: 0.75rem;
margin: 1.5rem 0;
overflow: hidden;
transition: border-color 0.2s ease;
display: flex;
.heroCard {
flex: 1;
display: flex;
gap: 1rem;
padding: 1rem;
overflow: hidden;
}
// 左侧信息区(头像+共鸣能力)
.leftInfo {
display: grid;
grid-template-rows: auto auto;
align-items: center;
justify-items: center;
border-radius: 16px;
padding: 12px;
border: 2px solid transparent;
background-clip: padding-box;
animation: cursorAnimation_link 1s infinite step-start;
transition: all 0.3s;
position: relative;
.avatarImage {
width: 100%;
height: auto;
border-radius: 12px;
display: block;
}
.avatarName {
margin-top: 8px;
font-size: 14px;
font-weight: bold;
text-align: center;
// color: var(--pink-core);
// text-shadow: 0 0 10px var(--pink-core), 0 0 20px var(--blue-glitch);
// position: relative;
// animation: glitch-b7066fb5 3s infinite;
// position: relative;
}
.avatarMeta {
display: flex;
gap: 8px;
align-items: center;
justify-content: center;
flex-wrap: wrap;
.MetaSpan {
font-weight: 600;
margin-top: 4px;
font-size: 12px;
color: var(--c-text-sub);
background: #ff8cb01a;
padding: 2px 6px;
border-radius: 4px;
border: 1px solid var(--pink-core);
}
}
}
// 右侧信息区(名称+描述+状态卡)
.rightInfo {
display: flex;
flex-direction: column;
gap: 12px;
z-index: 6;
overflow-y: scroll;
scrollbar-width: none;
.panelMain {
position: relative;
z-index: 6;
.heroName {
font-size: clamp(1.8rem, 3vw, 2.8rem);
margin: 0;
font-weight: 900;
letter-spacing: 1px;
line-height: 1;
color: var(--pink-core);
text-shadow: 0 0 10px var(--pink-core), 0 0 20px var(--blue-glitch);
position: relative;
animation: glitch-b7066fb5 3s infinite;
position: relative;
.heroTitle {
font-size: 0.95rem;
color: var(--blue-glow);
margin-left: 8px;
font-weight: 400;
}
&::before,
&::after {
content: attr(data-text);
position: absolute;
left: 2px;
text-shadow: -2px 0 var(--blue-glitch);
clip: rect(44px, 450px, 56px, 0);
animation: glitch-anim-b7066fb5 5s infinite linear alternate-reverse;
}
&::after {
left: -2px;
text-shadow: -2px 0 var(--pink-core);
clip: rect(44px, 450px, 56px, 0);
animation: glitch-anim2-b7066fb5 5s infinite linear alternate-reverse;
}
}
.heroDesc {
font-size: 14px;
color: var(--c-text-content);
line-height: 1.6;
display: -webkit-box;
-webkit-box-orient: vertical;
.lightDesc {
color: var(--pink-core);
text-shadow: 0 0 8px var(--pink-core);
}
}
.tagItem {
align-items: center;
display: flex;
flex-wrap: wrap;
gap: .3em .6em;
.tag {
background-color: var(--c-bg-soft);
border-radius: .4em;
color: var(--c-text-soft);
font-size: .9em;
padding: .25em .6em;
transition: all .2s;
&:hover {
background-color: var(--c-primary-soft);
color: var(--c-primary);
}
}
}
}
}
// 状态卡片(双列网格)
.infoMain {
background: transparent;
border-radius: 0;
display: grid;
font-size: 1rem;
gap: 0.4rem;
grid-template-columns: repeat(4, 1fr);
padding: 0;
margin-top: 0.5em;
.infoCard {
display: flex;
flex-direction: column;
gap: 0.1rem;
margin: 0.5em 0;
.infoLabel {
color: var(--c-text-2);
font-size: 0.8rem;
font-weight: 500;
}
.infoValue {
color: var(--c-text);
font-size: 0.8rem;
word-break: break-word;
}
}
}
.statusMain {
background: rgba(122, 92, 61, 0.08);
border-radius: 6px;
padding: 10px;
.statusHeader {
align-items: center;
gap: 8px;
margin-bottom: 6px;
}
.statusContent {
font-size: 13px;
color: var(--c-text-content);
line-height: 1.5;
}
}
}
/* ========== 移动端适配(max-width: 768px) ========== */
@media screen and (max-width: 768px) {
.heroMain {
flex-direction: column; // 改为垂直堆叠
height: auto; // 高度自适应内容
margin: 1rem 0; // 缩小上下边距
border-radius: 0.5rem;
overflow: hidden; // 防止内部滚动溢出
}
// 左侧信息区适配
.leftInfo {
width: 100%;
padding: 8px;
border-radius: 12px;
gap: 6px; // 缩小子元素间距
.avatarImage {
width: 120px; // 缩小头像尺寸
height: 120px;
border-radius: 10px;
}
.avatarMeta {
font-size: 0.8rem; // 缩小文字
gap: 6px;
.MetaSpan {
padding: 4px 8px; // 缩小内边距
font-size: 0.8rem;
border-radius: 8px;
}
}
}
// 右侧信息区适配
.rightInfo {
flex-direction: column;
gap: 8px;
padding: 0;
.panelMain {
padding: 16px;
border-radius: 12px;
.heroName {
font-size: clamp(1.2rem, 2.5vw, 1.8rem);
letter-spacing: 0.5px;
line-height: 1.2;
.heroTitle {
font-size: 0.8rem;
margin-left: 6px;
}
}
.heroDesc {
font-size: 0.9rem;
line-height: 1.4;
.lightDesc {
font-size: 0.9rem;
}
}
}
}
// 状态卡片适配(单列布局)
.infoMain {
grid-template-columns: 1fr;
gap: 0.2rem;
font-size: 0.8rem;
.infoCard {
gap: 0.1rem;
.infoLabel,
.infoValue {
font-size: 0.8rem;
}
}
}
// 隐藏滚动条(可选,提升移动端体验)
.rightInfo::-webkit-scrollbar {
display: none;
}
.rightInfo {
overflow-y: auto;
}
}
// 动效模块(预留动画,后续可以通过调用以下模块来实现效果,请勿开启会造成光污染损害视力)
@keyframes glitch-b7066fb5 {
0% {
transform: translate(0);
text-shadow: -2px 0 var(--blue-glitch), 2px 2px var(--pink-core)
}
20% {
transform: translate(-2px, 2px);
text-shadow: 2px -2px var(--blue-glitch), -2px 2px var(--pink-core)
}
40% {
transform: translate(2px, -2px);
text-shadow: -2px 2px var(--blue-glitch), 2px -2px var(--pink-core)
}
60% {
transform: translate(0);
text-shadow: 2px 0 var(--blue-glitch), -2px -2px var(--pink-core)
}
80% {
transform: translate(2px, 2px);
text-shadow: -2px -2px var(--blue-glitch), 2px 0 var(--pink-core)
}
to {
transform: translate(0);
text-shadow: none
}
}
@keyframes pulse-glow-b7066fb5 {
0%,
to {
filter: drop-shadow(0 0 5px var(--pink-glow)) drop-shadow(0 0 10px var(--blue-glitch))
}
50% {
filter: drop-shadow(0 0 15px var(--pink-core)) drop-shadow(0 0 20px var(--blue-glow))
}
}
@keyframes scanline-b7066fb5 {
0% {
transform: translateY(-100%)
}
to {
transform: translateY(100%)
}
}
@keyframes blink-b7066fb5 {
0%,
to {
opacity: 1
}
50% {
opacity: .3
}
}
@keyframes float-particle-b7066fb5 {
0% {
transform: translate(0) rotate(0);
opacity: 0
}
10% {
opacity: .5
}
90% {
opacity: .5
}
to {
transform: translate(calc(100vw * var(--dx)), calc(100vh * var(--dy))) rotate(360deg);
opacity: 0
}
}
@keyframes hologram-scan-b7066fb5 {
0% {
top: -10%;
opacity: 0
}
20% {
opacity: .8
}
80% {
opacity: .8
}
to {
top: 110%;
opacity: 0
}
}
@keyframes core-pulse-b7066fb5 {
0% {
box-shadow: 0 0 5px var(--pink-core), 0 0 15px var(--blue-glitch)
}
50% {
box-shadow: 0 0 15px var(--pink-core), 0 0 30px var(--blue-glow), 0 0 45px var(--pink-light)
}
to {
box-shadow: 0 0 5px var(--pink-core), 0 0 15px var(--blue-glitch)
}
}
@keyframes borderRotate-b7066fb5 {
0% {
filter: hue-rotate(0deg)
}
to {
filter: hue-rotate(360deg)
}
}
@keyframes itemIn-b7066fb5 {
to {
opacity: 1;
transform: translateY(0)
}
}
@keyframes glitch-anim-b7066fb5 {
0% {
clip: rect(31px, 9999px, 94px, 0)
}
5% {
clip: rect(70px, 9999px, 71px, 0)
}
10% {
clip: rect(29px, 9999px, 83px, 0)
}
15% {
clip: rect(16px, 9999px, 91px, 0)
}
20% {
clip: rect(2px, 9999px, 36px, 0)
}
25% {
clip: rect(27px, 9999px, 9px, 0)
}
30% {
clip: rect(9px, 9999px, 53px, 0)
}
35% {
clip: rect(17px, 9999px, 24px, 0)
}
40% {
clip: rect(74px, 9999px, 61px, 0)
}
45% {
clip: rect(17px, 9999px, 83px, 0)
}
50% {
clip: rect(74px, 9999px, 55px, 0)
}
55% {
clip: rect(38px, 9999px, 48px, 0)
}
60% {
clip: rect(94px, 9999px, 42px, 0)
}
65% {
clip: rect(35px, 9999px, 23px, 0)
}
70% {
clip: rect(41px, 9999px, 46px, 0)
}
75% {
clip: rect(35px, 9999px, 3px, 0)
}
80% {
clip: rect(41px, 9999px, 96px, 0)
}
85% {
clip: rect(52px, 9999px, 59px, 0)
}
90% {
clip: rect(69px, 9999px, 97px, 0)
}
95% {
clip: rect(10px, 9999px, 71px, 0)
}
to {
clip: rect(67px, 9999px, 38px, 0)
}
}
@keyframes glitch-anim2-b7066fb5 {
0% {
clip: rect(65px, 9999px, 59px, 0)
}
5% {
clip: rect(88px, 9999px, 67px, 0)
}
10% {
clip: rect(94px, 9999px, 7px, 0)
}
15% {
clip: rect(73px, 9999px, 14px, 0)
}
20% {
clip: rect(96px, 9999px, 71px, 0)
}
25% {
clip: rect(13px, 9999px, 35px, 0)
}
30% {
clip: rect(72px, 9999px, 66px, 0)
}
35% {
clip: rect(70px, 9999px, 22px, 0)
}
40% {
clip: rect(13px, 9999px, 98px, 0)
}
45% {
clip: rect(63px, 9999px, 7px, 0)
}
50% {
clip: rect(80px, 9999px, 21px, 0)
}
55% {
clip: rect(27px, 9999px, 52px, 0)
}
60% {
clip: rect(89px, 9999px, 14px, 0)
}
65% {
clip: rect(51px, 9999px, 80px, 0)
}
70% {
clip: rect(2px, 9999px, 37px, 0)
}
75% {
clip: rect(71px, 9999px, 86px, 0)
}
80% {
clip: rect(19px, 9999px, 46px, 0)
}
85% {
clip: rect(82px, 9999px, 8px, 0)
}
90% {
clip: rect(48px, 9999px, 3px, 0)
}
95% {
clip: rect(68px, 9999px, 100px, 0)
}
to {
clip: rect(47px, 9999px, 2px, 0)
}
}
</style>
整体说明
| 配置项 | 类型 | 必需 | 说明 |
|---|---|---|---|
| 类型 | 爱弥斯、尤诺、奥古斯塔 | ✓ | 角色类型(目前只有几种,未适配完成) |
| 头像 | string | ✓ | 角色头像 |
| 徽章 | Record<string, string> | ✓ | 角色徽章(共鸣能力、属性等等) |
| 名字 | string; | ✓ | 角色名字 |
| 称号 | string; | ✓ | 角色特定昵称 |
| 标签 | Record<string, string>; | ✓ | 角色曾用标签 |
| 简介 | 上部分: string 下部分: string | ✓ | 角色全局简介 |
| 详情信息 | Record<string, string> | ✓ | 角色全局信息 |
人物物品
<!-- .vitepress/components/InfoCard.vue -->
<script setup lang="ts">
import { ref } from 'vue'
import Title from '../card/title.vue';
defineProps<{
heroSpecialList?: Array<{
物品名称: string // 卡片标题
物品含意: string // 卡片描述
物品简介: string
物品图像: string // 卡片主图
物品彩蛋: string
}>
}>()
// 跟踪当前激活的卡片索引(初始激活第一个)
const activeIndex = ref(0)
</script>
<template>
<div class="infoCard">
<!-- 左侧导航区:渲染所有导航头像,点击切换激活项 -->
<div class="navArea">
<div
class="navItem"
v-for="(item, index) in heroSpecialList"
:key="index"
@click="activeIndex = index"
:class="{ active: activeIndex === index }"
>
<NuxtImg
v-if="item.物品图像"
:src="item.物品图像"
alt="导航头像"
class="nuxtImage"
style="width: 40px; height: 40px; border-radius: 50%; object-fit: cover;"
/>
</div>
</div>
<!-- 右侧内容区:仅渲染当前激活的卡片内容 -->
<div class="contentArea" v-if="heroSpecialList?.[activeIndex]">
<div class="cardLeft">
<!-- 卡片主图 -->
<img :src="heroSpecialList[activeIndex]?.物品图像" class="cardImage" alt="卡片主图" />
<!-- 卡片标题 -->
<h3 class="cardTitle">{{ heroSpecialList[activeIndex]?.物品名称 || '默认标题' }}</h3>
<!-- 卡片附属名称(如角色名) -->
<div class="cardSubInfo">
<span>{{ heroSpecialList[activeIndex]?.物品含意 || '默认名称' }}</span>
</div>
</div>
<div class="cardRight">
<!-- 卡片描述 -->
<Title title="描述" />
<div class="cardDesc">
{{ heroSpecialList[activeIndex]?.物品简介 || '暂无描述信息' }}
</div>
<Title title="彩蛋" />
<div class="cardYouLai">
{{ heroSpecialList[activeIndex]?.物品彩蛋 || "未写入" }}
</div>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.infoCard {
width: 100%;
height: 320px;
background: var(--ld-bg-card);
border: 1px solid var(--c-border);
border-radius: 0.75rem;
margin: 1.5rem 0;
overflow: hidden;
transition: border-color 0.2s ease;
display: flex; /* 整体左右布局 */
/* 左侧导航区:垂直排列头像 */
.navArea {
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
padding: 1rem;
width: 60px; /* 导航区宽度,适配头像垂直排列 */
gap: 8px; /* 头像之间的间距 */
.navItem {
cursor: pointer;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: all 0.2s ease;
&:hover {
background-color: var(--c-bg-hover); /* hover 背景色 */
}
&.active {
background-color: var(--c-bg-active); /* 激活态背景色 */
}
}
}
/* 右侧内容区:卡片详情 */
.contentArea {
flex: 1;
display: flex;
gap: 1rem;
padding: 1rem;
overflow: hidden;
.cardLeft {
flex-shrink: 0;
display: flex;
flex-direction: column;
align-items: center;
width: 200px; /* 左侧卡片预览区宽度 */
overflow: hidden;
.cardImage {
width: 100%;
object-fit: cover;
border-radius: 8px;
}
.cardTitle {
margin-top: 8px;
font-size: 14px;
font-weight: bold;
color: var(--c-text-title);
text-align: center;
// @include text-overflow; /* 若需单行省略,可封装 mixin */
}
.cardSubInfo {
margin-top: 4px;
font-size: 12px;
color: var(--c-text-sub);
background: rgba(122, 92, 61, 0.1);
padding: 2px 6px;
border-radius: 4px;
}
}
.cardRight {
flex: 1;
display: flex;
flex-direction: column;
gap: 10px;
overflow-y: scroll; /* 启用垂直滚动 */
padding-right: 20px; /* 防止内容被遮挡 */
/* 隐藏滚动条 - Webkit浏览器 */
&::-webkit-scrollbar {
width: 0;
background: transparent;
}
/* 隐藏滚动条 - Firefox */
scrollbar-width: none;
/* 隐藏滚动条 - IE/Edge */
-ms-overflow-style: none;
.cardDesc, .cardYouLai {
font-size: 14px;
color: var(--c-text-content);
line-height: 1.6;
display: -webkit-box;
-webkit-box-orient: vertical;
.title {
background: #ffffffb2;
color: #ff9900b2
}
}
.cardYouLai {
display: block;
color: var(--blue-glow);
font-size: .85rem;
border-left: 3px solid var(--pink-core);
padding-left: 12px;
}
.tagItem {
display: flex;
flex-wrap: wrap;
gap: .3rem;
.tag {
border-radius: .3rem;
display: inline-block;
font-size: 14px;
white-space: nowrap;
}
}
/* 技能模块样式 */
.cardSkills {
.skillsContainer {
display: flex;
flex-direction: column;
gap: 12px;
}
.skillItem {
background: rgba(122, 92, 61, 0.08);
border-radius: 6px;
padding: 10px;
.skillHeader {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 6px;
.skillIcon {
width: 24px;
height: 24px;
border-radius: 4px;
object-fit: cover;
}
.skillName {
font-size: 15px;
color: var(--c-text-title);
}
.skillXg {
font-size: 12px;
color: var(--c-bg-content);
}
}
.skillDesc {
font-size: 13px;
color: var(--c-text-content);
line-height: 1.5;
}
}
.noSkill {
color: var(--c-text-sub);
font-style: italic;
padding: 8px;
text-align: center;
background: rgba(122, 92, 61, 0.05);
border-radius: 6px;
}
}
}
}
}
/* 在原有样式基础上添加移动端适配 */
@media (max-width: 768px) {
.infoCard {
flex-direction: column;
height: auto;
padding: 0.5rem;
.navArea {
flex-direction: row;
width: 100%;
padding: 0.5rem;
justify-content: flex-start;
overflow-x: auto;
.navItem {
flex-shrink: 0;
margin: 0 4px;
.nuxtImage {
width: 32px;
height: 32px;
}
}
}
.contentArea {
flex-direction: column;
padding: 0.5rem;
gap: 0.5rem;
.cardLeft {
width: 100%;
align-items: center;
.cardImage {
max-width: 150px;
height: auto;
}
.cardTitle {
font-size: 20px;
}
.cardSubInfo {
font-size: 15px;
}
}
.cardRight {
width: 100%;
padding-right: 0;
.cardDesc, .cardYouLai {
font-size: 13px;
}
.tagItem .tag {
font-size: 12px;
padding: 2px 4px;
}
.cardSkills {
.skillItem {
padding: 8px;
.skillHeader .skillName {
font-size: 14px;
}
.skillDesc {
font-size: 12px;
}
}
}
}
}
}
}
/* 超小屏幕优化 */
@media (max-width: 480px) {
.infoCard {
.contentArea {
.cardLeft .cardImage {
max-width: 150px;
}
.cardRight {
.cardSkills .skillItem {
padding: 6px;
}
}
}
}
}
</style>
整体说明
| 配置项 | 类型 | 必需 | 说明 |
|---|---|---|---|
| 物品图像 | string | ✓ | 物品图片(与物品切换图标绑定) |
| 物品名称 | string | ✓ | 物品名称 |
| 物品含义 | string | ✓ | 物品的小标签,说明其中的含义 |
| 物品彩蛋 | string | ✓ | 物品的小彩蛋,说明该物品在过去或者地方的位置 |
| 物品简介 | string | ✓ | 物品的简介,通常与来历、分量等等有关 |
人物故事
<script setup lang="ts">
const props = defineProps<{
heroStories: Array<{
内容标题: string
内容: Record<string, string>
密钥: number
}>
顶部标题: string
}>()
const activeIndex = ref(0)
// 添加过渡动画方向
const slideDirection = ref<'left' | 'right'>('right')
function prevPage() {
if (props.heroStories.length === 0) return
if (activeIndex.value > 0) {
slideDirection.value = 'left'
activeIndex.value--
}
}
function nextPage() {
if (props.heroStories.length === 0) return
if (activeIndex.value < props.heroStories.length - 1) {
slideDirection.value = 'right'
activeIndex.value++
}
}
// 触摸滑动支持
const touchStartX = ref(0)
const touchEndX = ref(0)
// function handleTouchStart(e: TouchEvent) {
// touchStartX.value = e.touches[0].clientX
// }
// function handleTouchMove(e: TouchEvent) {
// touchEndX.value = e.touches[0].clientX
// }
function handleTouchEnd() {
const diff = touchStartX.value - touchEndX.value
const threshold = 50 // 最小滑动距离
if (Math.abs(diff) > threshold) {
if (diff > 0) {
nextPage()
} else {
prevPage()
}
}
touchStartX.value = 0
touchEndX.value = 0
}
</script>
<template>
<div class="storiesContainer">
<div
class="storiesMain"
v-if="heroStories?.[activeIndex]"
>
<!-- 装饰性背景元素 -->
<div class="decorative-bg">
<div class="circle circle-1"></div>
<div class="circle circle-2"></div>
</div>
<header class="storiesHeader">
<div class="header-content">
<span class="header-icon">📖</span>
<h2 class="header-title">{{ 顶部标题 || '未写入' }}</h2>
</div>
</header>
<main class="storiesBody">
<Transition :name="`slide-${slideDirection}`" mode="out-in">
<div :key="activeIndex" class="content-wrapper">
<h3 class="storiesTitle">
<span class="title-decorator"></span>
{{ heroStories[activeIndex]?.内容标题 }}
</h3>
<div class="storiesContent">
<p
class="storiesSpan"
v-for="([key, value], index) in Object.entries(heroStories[activeIndex]?.内容 ?? {})"
:key="key"
:style="{ animationDelay: `${index * 0.05}s` }"
>
{{ value }}
</p>
</div>
</div>
</Transition>
</main>
<footer class="storiesFooter">
<button
class="page-btn prev-btn"
@click="prevPage"
:disabled="activeIndex === 0"
aria-label="上一页"
>
<span class="btn-icon">◀</span>
<span class="btn-text">上一页</span>
</button>
<div class="footerPageNumber">
<span class="current-page">{{ heroStories[activeIndex]?.密钥 }}</span>
<span class="separator">/</span>
<span class="total-pages">{{ heroStories.length }}</span>
</div>
<button
class="page-btn next-btn"
@click="nextPage"
:disabled="activeIndex === heroStories.length - 1"
aria-label="下一页"
>
<span class="btn-text">下一页</span>
<span class="btn-icon">▶</span>
</button>
</footer>
<!-- 进度指示器 -->
<!-- <div class="progress-dots">
<span
v-for="(story, index) in heroStories.slice(0, 5)"
:key="story.密钥"
class="dot"
:class="{ active: index === activeIndex }"
@click="activeIndex = index"
></span>
<span v-if="heroStories.length > 5" class="dot-more">...</span>
</div> -->
</div>
<!-- 空状态 -->
<div v-else class="empty-state">
<div class="empty-icon">📚</div>
<p class="empty-text">暂无故事内容</p>
</div>
</div>
</template>
<style lang="scss" scoped>
.storiesContainer {
margin-top: 24px;
-webkit-backdrop-filter: blur(10px);
backdrop-filter: blur(10px);
border-radius: 0.75em;
padding: 24px;
transition: all .3s;
position: relative;
overflow: hidden;
background: var(--ld-bg-card);
border: 1px solid var(--c-border);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.05);
// 移动端适配
@media (max-width: 768px) {
margin-top: 16px;
padding: 16px;
border-radius: 0.5em;
}
.storiesMain {
position: relative;
z-index: 1;
// 装饰性背景
.decorative-bg {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
pointer-events: none;
overflow: hidden;
opacity: 0.4;
z-index: 0;
.circle {
position: absolute;
border-radius: 50%;
background: radial-gradient(circle, var(--pink-core) 0%, transparent 70%);
opacity: 0.1;
animation: float 8s ease-in-out infinite;
&.circle-1 {
width: 200px;
height: 200px;
top: -50px;
right: -50px;
}
&.circle-2 {
width: 150px;
height: 150px;
bottom: -30px;
left: -30px;
animation-delay: -4s;
}
}
}
.storiesHeader {
padding: 16px 20px;
border-bottom: 1px solid rgba(255, 140, 176, .2);
margin-bottom: 20px;
position: relative;
z-index: 1;
@media (max-width: 768px) {
padding: 12px 16px;
margin-bottom: 16px;
}
.header-content {
display: flex;
align-items: center;
gap: 12px;
.header-icon {
font-size: 1.5rem;
animation: pulse 2s ease-in-out infinite;
@media (max-width: 768px) {
font-size: 1.3rem;
}
}
.header-title {
font-size: 1.3rem;
color: var(--pink-core);
margin: 0;
font-weight: 600;
letter-spacing: 0.5px;
@media (max-width: 768px) {
font-size: 1.1rem;
}
}
}
}
.storiesBody {
padding: 0 20px;
min-height: 200px;
position: relative;
z-index: 1;
@media (max-width: 768px) {
padding: 0 16px;
min-height: 180px;
}
.content-wrapper {
animation: fadeInContent 0.3s ease-out;
}
.storiesTitle {
font-size: 1.2rem;
color: var(--blue-glow);
margin-bottom: 16px;
font-weight: 600;
display: flex;
align-items: center;
gap: 10px;
line-height: 1.4;
@media (max-width: 768px) {
font-size: 1.05rem;
margin-bottom: 12px;
}
.title-decorator {
width: 4px;
height: 1.2em;
background: linear-gradient(to bottom, var(--blue-glow), var(--pink-core));
border-radius: 2px;
flex-shrink: 0;
}
}
.storiesContent {
.storiesSpan {
display: block;
margin-bottom: 12px;
line-height: 1.8;
color: var(--c-text);
font-size: 0.95rem;
opacity: 0;
animation: fadeInUp 0.4s ease-out forwards;
@media (max-width: 768px) {
font-size: 0.9rem;
line-height: 1.7;
margin-bottom: 10px;
}
&:last-child {
margin-bottom: 0;
}
}
}
}
.storiesFooter {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
border-top: 1px solid rgba(255, 140, 176, .2);
margin-top: 20px;
position: relative;
z-index: 1;
@media (max-width: 768px) {
padding: 12px 16px;
margin-top: 16px;
gap: 8px;
}
.page-btn {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 16px;
border: 1px solid var(--c-border);
background: transparent;
color: var(--blue-glow);
border-radius: 6px;
cursor: pointer;
transition: all 0.3s ease;
font-size: 0.9rem;
font-weight: 500;
@media (max-width: 768px) {
padding: 6px 12px;
font-size: 0.85rem;
gap: 4px;
}
.btn-icon {
transition: transform 0.3s ease;
}
.btn-text {
@media (max-width: 480px) {
display: none;
}
}
&:hover:not(:disabled) {
background: var(--blue-glow);
color: white;
border-color: var(--blue-glow);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
.btn-icon {
transform: scale(1.2);
}
}
&:active:not(:disabled) {
transform: translateY(0);
}
&:disabled {
opacity: 0.4;
cursor: not-allowed;
}
&.prev-btn .btn-icon {
&:hover {
transform: translateX(-3px);
}
}
&.next-btn .btn-icon {
&:hover {
transform: translateX(3px);
}
}
}
.footerPageNumber {
display: flex;
align-items: center;
gap: 6px;
color: var(--blue-glow);
font-weight: 600;
font-size: 1rem;
@media (max-width: 768px) {
font-size: 0.9rem;
}
.current-page {
font-size: 1.2em;
color: var(--pink-core);
}
.separator {
opacity: 0.5;
}
.total-pages {
opacity: 0.7;
}
}
}
// 进度指示器
.progress-dots {
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
padding: 12px 0 0;
position: relative;
z-index: 1;
.dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--c-border);
cursor: pointer;
transition: all 0.3s ease;
&:hover {
background: var(--blue-glow);
transform: scale(1.3);
}
&.active {
background: var(--pink-core);
width: 24px;
border-radius: 4px;
transform: scale(1);
}
}
.dot-more {
color: var(--c-border);
font-weight: bold;
padding: 0 4px;
}
}
}
// 空状态
.empty-state {
text-align: center;
padding: 60px 20px;
color: var(--c-text);
opacity: 0.6;
@media (max-width: 768px) {
padding: 40px 20px;
}
.empty-icon {
font-size: 3rem;
margin-bottom: 16px;
animation: pulse 2s ease-in-out infinite;
@media (max-width: 768px) {
font-size: 2.5rem;
}
}
.empty-text {
font-size: 1rem;
margin: 0;
}
}
}
// 过渡动画
.slide-right-enter-active,
.slide-right-leave-active,
.slide-left-enter-active,
.slide-left-leave-active {
transition: all 0.3s ease;
}
.slide-right-enter-from {
opacity: 0;
transform: translateX(30px);
}
.slide-right-leave-to {
opacity: 0;
transform: translateX(-30px);
}
.slide-left-enter-from {
opacity: 0;
transform: translateX(-30px);
}
.slide-left-leave-to {
opacity: 0;
transform: translateX(30px);
}
// 关键帧动画
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes fadeInContent {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes pulse {
0%, 100% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.8;
transform: scale(1.05);
}
}
@keyframes float {
0%, 100% {
transform: translate(0, 0) scale(1);
}
33% {
transform: translate(10px, -10px) scale(1.05);
}
66% {
transform: translate(-10px, 10px) scale(0.95);
}
}
</style>
整体说明
hero-stories属性
| 配置项 | 类型 | 必需 | 说明 |
|---|---|---|---|
| 顶部标题 | string | ✓ | 组件标题显示 |
| heroStories | heroStories | ✓ | 组件全局信息 |
heroStories属性
| 配置项 | 类型 | 必需 | 说明 |
|---|---|---|---|
| 内容标题 | string | ✓ | 内容整体标题 |
| 内容 | Record<string, string> | ✓ | 内容填入显示 |
更新日志
V20260224-pre
- 1.更新了相关配置项的使用方式
- 2.更新了模块在文章中的写法
V20260223-pre
- 1.更新基础模块,并且优化部分逻辑
- 2.更新一些配置项,取消部分未使用的样式







评论加载中...