♻️ 重构留影叙佳期页面,处理部分文本加载异常

This commit is contained in:
BTMuli
2025-12-21 23:05:57 +08:00
parent 26d7df66d3
commit f8b9500def
3 changed files with 358 additions and 247 deletions

View File

@@ -0,0 +1,139 @@
<!-- 留影叙佳期页面卡片 -->
<template>
<div class="pac-bc-box">
<div class="pac-bc-cover" @click="handleClick()">
<div class="pac-bc-img">
<TMiImg
v-if="!props.isAether"
:alt="item.word_text"
:ori="true"
:src="item.unread_picture[0]"
/>
<TMiImg v-else :alt="item.word_text" :ori="true" :src="item.unread_picture[1]" />
</div>
<div class="pac-bc-hide" />
<v-icon class="pac-bc-icon">mdi-magnify</v-icon>
</div>
<div class="pac-bc-info">
<div class="pac-bc-title">
<span>{{ item.year }}/{{ item.birthday }}</span>
<span>{{ item.role_name }}</span>
</div>
<div :title="item.word_text" class="pac-bc-desc">{{ item.word_text }}</div>
</div>
</div>
</template>
<script lang="ts" setup>
import TMiImg from "@comp/app/t-mi-img.vue";
type PacBirthCardProps = {
/** 元数据 */
item: TGApp.Archive.Birth.DrawItem;
/**是否为空 */
isAether: boolean;
};
type PacBirthCardEmits = (e: "open") => void;
const props = defineProps<PacBirthCardProps>();
const emits = defineEmits<PacBirthCardEmits>();
function handleClick(): void {
emits("open");
}
</script>
<style lang="scss" scoped>
.pac-bc-box {
position: relative;
display: flex;
height: fit-content;
flex-direction: column;
align-items: center;
justify-content: flex-start;
row-gap: 4px;
transition: all 0.3s ease-in-out;
}
.pac-bc-cover {
position: relative;
overflow: hidden;
width: 100%;
max-width: 100%;
border-radius: 4px;
aspect-ratio: 125 / 54;
cursor: pointer;
}
.pac-bc-img {
position: relative;
display: flex;
width: 100%;
height: 100%;
align-items: center;
justify-content: center;
}
.pac-bc-hide {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: 4px;
background: var(--common-shadow-t-2);
}
.pac-bc-cover img {
width: 100%;
height: 100%;
object-fit: cover;
object-position: center;
transition: all 0.3s ease-in-out;
&:hover {
transform: scale(1.1);
transition: all 0.3s ease-in-out;
}
}
.pac-bc-cover:hover {
img {
overflow: hidden;
cursor: pointer;
transform: scale(1.2);
transition: all 0.5s ease-in-out;
}
.pac-bc-hide {
background: var(--common-shadow-2);
cursor: pointer;
transition: all 0.3s ease-in-out;
}
}
.pac-bc-info {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.pac-bc-title {
position: relative;
display: flex;
align-items: center;
justify-content: center;
color: var(--common-text-title);
column-gap: 8px;
font-family: var(--font-title);
font-size: 16px;
}
.pac-bc-desc {
position: relative;
width: fit-content;
color: var(--tgc-od-white);
font-size: 14px;
font-style: italic;
text-align: center;
}
</style>

View File

@@ -1,38 +1,35 @@
<!-- 留影叙佳期浮窗 TODO 左右SLOT -->
<template>
<TOverlay v-model="visible" blur-val="5px">
<div class="toab-container" v-if="props.data">
<div class="toab-img">
<TMiImg :ori="true" :src="props.data.take_picture[Number(props.choice)]" alt="顶部图像" />
<div class="toab-dialog" v-show="showText">
<div v-for="(item, index) in textParse" :key="index" class="toab-dialog-item">
<div class="toab-dialog-item-icon" v-if="item.icon" :title="item.name">
<TMiImg :src="item.icon" alt="对白头像" :ori="true" />
</div>
<div v-else-if="item.name !== '未知'" class="toab-dialog-item-name">
{{ item.name }}
</div>
<div
:class="{
'toab-dialog-item-text': item.icon !== undefined,
'toab-dialog-item-text-mini': item.icon === undefined,
}"
>
{{ item.text }}
</div>
<div class="pao-bc-container">
<div v-if="props.data" class="pao-bc-cover">
<TMiImg v-if="props.choice" :ori="true" :src="props.data.take_picture[1]" alt="顶部图像" />
<TMiImg v-else :ori="true" :src="props.data.take_picture[0]" alt="顶部图像" />
</div>
<div v-show="showText" class="pao-bc-comments">
<div v-for="(item, index) in textParse" :key="index" class="pao-bc-comment">
<div v-if="item.icon" :title="item.name" class="pao-bcc-icon">
<TMiImg :ori="true" :src="item.icon" alt="对白头像" />
</div>
<div v-else-if="item.name !== '未知'" class="pao-bcc-name">
{{ item.name }}
</div>
<div :class="item.icon ? 'pao-bcc-text' : 'pao-bcc-quote'">
{{ item.text }}
</div>
</div>
</div>
<div class="toab-top-tools">
<v-icon @click="onCopy" title="复制到剪贴板">mdi-content-copy</v-icon>
<v-icon @click="onDownload" title="下载到本地">mdi-download</v-icon>
<v-icon @click="loadText" :title="showText ? '隐藏对白' : '显示对白'">
<div class="pao-bc-top-tools">
<v-icon title="复制到剪贴板" @click="onCopy">mdi-content-copy</v-icon>
<v-icon title="下载到本地" @click="onDownload">mdi-download</v-icon>
<v-icon :title="showText ? '隐藏对白' : '显示对白'" @click="showComments()">
{{ showText ? "mdi-eye-off" : "mdi-eye" }}
</v-icon>
</div>
</div>
</TOverlay>
</template>
<script setup lang="ts">
<script lang="ts" setup>
import TMiImg from "@comp/app/t-mi-img.vue";
import TOverlay from "@comp/app/t-overlay.vue";
import showSnackbar from "@comp/func/snackbar.js";
@@ -54,14 +51,25 @@ const showText = ref<boolean>(false);
const buffer = shallowRef<ArrayBuffer | null>(null);
const textParse = shallowRef<Array<XmlTextParse>>([]);
onMounted(() => clearData());
watch(() => props.data, clearData);
watch(() => props.choice, clearData);
onMounted(async () => await clearData());
watch(
() => [props.data, props.choice],
async () => await clearData(),
);
watch(
() => showText.value,
async () => {
if (showText.value) await loadText();
else textParse.value = [];
},
);
function clearData(): void {
async function clearData(): Promise<void> {
buffer.value = null;
textParse.value = [];
showText.value = false;
if (showText.value) {
textParse.value = [];
await loadText();
}
}
async function onCopy(): Promise<void> {
@@ -82,6 +90,10 @@ async function onDownload(): Promise<void> {
showSnackbar.success(`图片已下载到本地,大小:${size}`);
}
function showComments(): void {
showText.value = !showText.value;
}
async function loadText(): Promise<void> {
if (!props.data) return;
if (textParse.value.length > 0) {
@@ -95,13 +107,14 @@ async function loadText(): Promise<void> {
}
const keyMap = getKeyMap(resSource);
const resXml: unknown = await parseXml(props.data.gal_xml);
console.log(resXml);
const textList = getTextList(resXml);
console.log(textList);
textParse.value = textList.map((item) => {
const key = keyMap.find((keyItem) => keyItem.id === item.img);
if (!key) return { name: "未知", text: item.text };
return { name: key.group ?? key.id, text: item.text, icon: key.icon };
});
showText.value = true;
}
function getKeyMap(resSource: unknown): Array<XmlKeyMap> {
@@ -124,6 +137,7 @@ function getKeyMap(resSource: unknown): Array<XmlKeyMap> {
return res;
}
// TODO: ||
function getTextList(resXml: unknown): Array<XmlTextList> {
const res: Array<XmlTextList> = [];
if (!resXml || typeof resXml !== "object") return res;
@@ -143,7 +157,11 @@ function getTextList(resXml: unknown): Array<XmlTextList> {
if (!("chara" in attr) || !("img" in attr)) continue;
if (item.name !== "simple_dialog") continue;
const img = props.choice ? attr.img : attr.img.replace("aether", "lumine");
res.push({ chara: attr.chara, img: img, text: item.elements[0].text });
let findText = "";
const arr5 = item.elements[0].elements;
if (arr5 && arr5.length > 0 && arr5[0].text !== "") findText = arr5[0].text;
else findText = item.elements[0].text;
res.push({ chara: attr.chara, img: img, text: findText });
}
return res;
}
@@ -164,90 +182,105 @@ async function parseXml(link: string): Promise<false | unknown> {
}
</script>
<style lang="css" scoped>
.toab-container {
.pao-bc-container {
position: relative;
display: flex;
overflow: hidden;
width: 50vw;
align-items: center;
justify-content: center;
border-radius: 4px;
aspect-ratio: 125 / 54;
background: var(--app-page-bg);
}
.toab-img {
.pao-bc-cover {
position: relative;
z-index: 0;
display: flex;
width: 100%;
height: 100%;
align-items: center;
justify-content: center;
img {
width: 100%;
height: 100%;
}
}
.toab-img img {
overflow: hidden;
width: 100%;
max-width: 100%;
height: auto;
border-radius: 10px;
}
.toab-top-tools {
.pao-bc-comments {
position: absolute;
z-index: 1;
bottom: 0;
left: 0;
display: flex;
width: 100%;
height: 100%;
max-height: 100%;
box-sizing: border-box;
flex-direction: column;
align-items: flex-start;
justify-content: flex-start;
padding: 8px;
border-radius: 4px;
-webkit-backdrop-filter: blur(2px);
backdrop-filter: blur(2px);
background: var(--common-shadow-t-2);
color: var(--box-text-1);
overflow-y: auto;
row-gap: 4px;
}
.pao-bc-comment {
position: relative;
display: flex;
align-items: center;
justify-content: flex-start;
column-gap: 4px;
}
.pao-bcc-icon {
position: relative;
width: 40px;
flex-shrink: 0;
img {
width: 100%;
}
}
.pao-bcc-name {
font-size: 1.5rem;
font-weight: bold;
}
.pao-bcc-text {
font-size: 16px;
text-shadow: 0 0 2px var(--common-shadow-t-8);
word-break: break-all;
}
.pao-bcc-quote {
margin-left: 3rem;
font-size: 1rem;
opacity: 0.8;
word-break: break-all;
}
.pao-bc-top-tools {
position: absolute;
z-index: 2;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
padding: 5px;
-webkit-backdrop-filter: blur(5px);
backdrop-filter: blur(5px);
-webkit-backdrop-filter: blur(2px);
backdrop-filter: blur(2px);
background-color: var(--common-shadow-t-2);
border-bottom-right-radius: 5px;
border-top-left-radius: 5px;
}
.toab-dialog {
position: absolute;
bottom: 0;
left: 0;
display: flex;
width: 100%;
height: 100%;
max-height: 40vh;
flex-direction: column;
align-items: center;
justify-content: flex-start;
border-radius: 10px;
-webkit-backdrop-filter: blur(5px);
backdrop-filter: blur(5px);
background: var(--common-shadow-t-2);
color: var(--box-text-1);
overflow-y: auto;
}
.toab-dialog-item {
display: flex;
width: 100%;
align-items: center;
justify-content: flex-start;
}
.toab-dialog-item-icon {
width: 50px;
min-width: 50px;
}
.toab-dialog-item-name {
font-size: 1.5rem;
font-weight: bold;
}
.toab-dialog-item-text {
font-size: 1.2rem;
word-break: break-all;
}
.toab-dialog-item-text-mini {
margin-left: 3rem;
font-size: 1rem;
opacity: 0.8;
word-break: break-all;
}
</style>

View File

@@ -1,42 +1,60 @@
<!-- 留影叙佳期 -->
<template>
<div class="ab-container">
<div class="ab-draw-top">
<div @click="toAct" class="ab-draw-act" title="前往网页活动">
<img src="/source/UI/act_birthday.webp" alt="archive_birthday_icon" class="side-icon" />
<v-app-bar>
<div class="ab-top">
<div class="ab-title">
<img alt="archive_birthday_icon" src="/source/UI/act_birthday.webp" @click="toAct()" />
<span>留影叙佳期</span>
</div>
<div class="ab-switch">
<span>{{ isAether ? "空" : "荧" }}</span>
<v-switch
v-model="isAether"
class="ab-switch-btn"
color="var(--tgc-od-orange)"
density="compact"
/>
</div>
<v-switch class="ab-draw-switch" v-model="isAether" />
<span>{{ isAether ? "空" : "荧" }}</span>
<v-select
v-model="curSelect"
class="ab-select"
:items="ArcBirRole"
clearable
variant="outlined"
label="角色"
:item-value="(item: TGApp.Archive.Birth.RoleItem) => item"
:hide-details="true"
:item-props="(item: TGApp.Archive.Birth.RoleItem) => getItemProps(item)"
:item-value="(item: TGApp.Archive.Birth.RoleItem) => item"
:items="ArcBirRole"
class="ab-select"
clearable
density="compact"
label="角色"
variant="outlined"
/>
</div>
<div class="ab-draw-grid">
<div v-for="item in selectedItem" :key="item.op_id" class="ab-draw">
<div class="ab-draw-cover" @click="showImg(item)">
<div class="ab-draw-img">
<TMiImg :src="item.unread_picture[Number(isAether)]" :alt="item.word_text" />
</div>
<div class="ab-draw-hide" />
<v-icon class="ab-draw-icon">mdi-magnify</v-icon>
</div>
<div class="ab-di-info">{{ item.year }} {{ item.birthday }} {{ item.role_name }}</div>
<div class="ab-di-text" :title="item.word_text">{{ item.word_text }}</div>
<template #append>
<div class="ab-top-append">
<v-pagination
v-model="page"
:length="length"
:total-visible="visible"
density="compact"
variant="elevated"
/>
</div>
</div>
<v-pagination v-model="page" :length="length" :total-visible="visible" />
</template>
</v-app-bar>
<div class="ab-grid-container">
<PacBirthCard
v-for="item in selectedItem"
:key="item.op_id"
:isAether
:item
@open="showImg(item)"
/>
</div>
<ToArcBrith v-model="showOverlay" :data="current" :choice="isAether" />
<!-- TODO: 左右SLOT -->
<ToArcBrith v-model="showOverlay" :choice="isAether" :data="current" />
</template>
<script lang="ts" setup>
import TMiImg from "@comp/app/t-mi-img.vue";
import ToArcBrith from "@comp/pageArchive/to-arcBrith.vue";
import PacBirthCard from "@comp/pageArchive/pac-birth-card.vue";
import ToArcBrith from "@comp/pageArchive/pao-birth-card.vue";
import TGClient from "@utils/TGClient.js";
import { computed, onMounted, ref, shallowRef, watch } from "vue";
import { useRoute } from "vue-router";
@@ -94,152 +112,73 @@ async function toAct(): Promise<void> {
function getItemProps(item: TGApp.Archive.Birth.RoleItem) {
return {
title: `${item.name} ${item.role_birthday}`,
subtitle: new DOMParser().parseFromString(item.text, "text/html").body.textContent,
prependAvatar: item.head_icon,
};
}
</script>
<style lang="css" scoped>
.ab-container {
<style lang="scss" scoped>
.ab-top {
position: relative;
display: flex;
margin-left: 16px;
column-gap: 24px;
}
.ab-title {
display: flex;
height: 100%;
flex-direction: column;
align-items: center;
padding: 20px;
justify-content: center;
gap: 8px;
img {
width: 32px;
height: 32px;
cursor: pointer;
}
span {
color: var(--common-text-title);
font-family: var(--font-title);
font-size: 20px;
}
}
.ab-draw-top {
.ab-switch {
position: relative;
display: flex;
width: 100%;
height: 60px;
align-items: center;
justify-content: flex-start;
column-gap: 10px;
justify-content: center;
column-gap: 12px;
span {
color: var(--common-text-title);
font-family: var(--font-title);
font-size: 20px;
}
.ab-switch-btn {
position: relative;
display: flex;
}
}
.ab-draw-act {
cursor: pointer;
.ab-select {
position: relative;
width: 360px;
}
.ab-draw-act img {
width: 40px;
height: 40px;
object-fit: cover;
object-position: center;
transition: all 0.3s ease-in-out;
.ab-top-append {
position: relative;
margin-right: 12px;
}
.ab-draw-switch {
display: flex;
}
.ab-draw-grid {
.ab-grid-container {
position: relative;
display: grid;
width: 100%;
height: 100%;
gap: 10px;
gap: 8px;
grid-template-columns: repeat(4, 1fr);
}
.ab-draw {
display: flex;
height: fit-content;
flex-direction: column;
align-items: center;
justify-content: flex-start;
transition: all 0.3s ease-in-out;
}
.ab-draw-cover {
position: relative;
overflow: hidden;
width: 100%;
max-width: 100%;
border-radius: 5px;
aspect-ratio: 125 / 54;
}
.ab-draw-img {
position: relative;
display: flex;
width: 100%;
height: 100%;
align-items: center;
justify-content: center;
}
.ab-draw-hide {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: 5px;
-webkit-backdrop-filter: blur(5px);
backdrop-filter: blur(5px);
background: var(--common-shadow-t-2);
}
.ab-draw-icon {
position: absolute;
top: 50%;
left: 50%;
color: var(--common-shadow-8);
font-size: 30px;
transform: translate(-50%, -50%);
transition: all 0.5s ease-in-out;
}
.ab-draw-cover img {
width: 100%;
height: 100%;
object-fit: cover;
object-position: center;
transition: all 0.3s ease-in-out;
}
.ab-draw-act img:hover {
transform: scale(1.1);
transition: all 0.3s ease-in-out;
}
.ab-draw:hover img {
overflow: hidden;
cursor: pointer;
transform: scale(1.1);
transition: all 0.3s ease-in-out;
}
.ab-draw:hover .ab-draw-hide {
-webkit-backdrop-filter: blur(0);
backdrop-filter: blur(0);
background: var(--common-shadow-2);
cursor: pointer;
transition: all 0.3s ease-in-out;
}
.ab-draw:hover .ab-draw-icon {
color: var(--common-shadow-t-4);
cursor: pointer;
scale: 2;
transition: all 0.5s ease-in-out;
}
.ab-di-info {
display: flex;
width: 100%;
align-items: center;
justify-content: center;
}
.ab-di-text {
width: fit-content;
max-width: 100%;
margin-right: auto;
margin-left: auto;
font-size: 14px;
line-height: 1.5;
opacity: 0.8;
grid-template-rows: repeat(3, 1fr);
}
</style>