Files
TeyvatGuide/src/pages/Achievements.vue
2023-05-25 22:06:40 +08:00

433 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<!-- 顶部操作栏 -->
<v-app-bar style="background: rgb(0 0 0 / 50%); color: #f4d8a8; font-family: Genshin, serif">
<template #prepend>
<span style="font-size: 30px">{{ title }}</span>
</template>
<v-spacer />
<v-text-field
v-model="search"
append-icon="mdi-magnify"
label="搜索"
hide-details
@click:append="searchCard"
@keyup.enter="searchCard"
/>
<template #append>
<v-btn prepend-icon="mdi-import" class="ms-2 top-btn" @click="importJson">
导入
</v-btn>
<v-btn prepend-icon="mdi-export" class="ms-2 top-btn" @click="exportJson">
导出
</v-btn>
</template>
</v-app-bar>
<TLoading v-model="loading" :title="loadingTitle" />
<div class="wrap">
<!-- 左侧菜单 -->
<div class="left-wrap">
<v-list v-for="series in seriesList" :key="series.id" class="card-left" @click="selectSeries(series.id)">
<div class="version-icon-series">
v{{ series.version }}
</div>
<v-list-item>
<template #prepend>
<v-img width="40px" style="margin-right: 10px" :src="series.icon" />
</template>
<v-list-item-title>
{{ series.name }}
</v-list-item-title>
<v-list-item-subtitle> {{ series.finCount }} / {{ series.totalCount }} </v-list-item-subtitle>
</v-list-item>
</v-list>
</div>
<!-- 右侧内容-->
<div class="right-wrap" @scroll="handleScroll">
<v-list
v-if="selectedSeries !== 0 && selectedSeries !== 17 && selectedSeries !== -1"
:style="{
backgroundImage: 'url(' + getCardInfo.bg || null + ')',
backgroundPosition: 'right',
backgroundSize: 'auto 100%',
backgroundRepeat: 'no-repeat',
margin: '10px',
borderRadius: '10px 50px 50px 10px',
color: '#485466',
fontFamily: 'Genshin,serif',
cursor: 'pointer',
position: 'relative',
}"
@click="openImg()"
>
<v-list-item :title="getCardInfo.name" :subtitle="getCardInfo.desc">
<template #prepend>
<v-img width="80px" style="margin-right: 10px" :src="getCardInfo.icon" />
</template>
</v-list-item>
</v-list>
<div
class="list-empty"
:style="{height: `${emptyHeight}px`}"
>
<v-list v-for="achievement in renderAchievement" :key="achievement.id" class="card-right" :style="{Transform:`translateY(${translateY})`}">
<div v-if="achievement.progress !== 0" class="achievement-progress">
{{ achievement.progress }}
</div>
<v-list-item>
<template #prepend>
<v-icon :color="achievement.isCompleted ? '#fec90b' : '#485466'">
<!-- todo 图标替换 -->
{{ achievement.isCompleted ? "mdi-check-circle" : "mdi-circle" }}
</v-icon>
</template>
<v-list-item-title>
{{ achievement.name }}
<span class="version-icon-single">v{{ achievement.version }}</span>
</v-list-item-title>
<v-list-item-subtitle>{{ achievement.description }}</v-list-item-subtitle>
<template #append>
<span v-show="achievement.isCompleted" class="right-time">{{ achievement.completedTime }}</span>
<v-card class="reward-card">
<v-img src="/icon/material/201.webp" sizes="32" />
<div class="reward-num">
<span>{{ achievement.reward }}</span>
</div>
</v-card>
</template>
</v-list-item>
</v-list>
</div>
</div>
<!-- 弹窗提示 -->
<v-snackbar v-model="snackbar" timeout="1500" :color="snackbarColor" top>
{{ snackbarText }}
</v-snackbar>
</div>
</template>
<script lang="ts" setup>
// vue
import { onMounted, ref, onBeforeMount, computed } from "vue";
import TLoading from "../components/main/t-loading.vue";
// tauri
import { dialog, fs } from "@tauri-apps/api";
// Store
import { useAchievementsStore } from "../store/modules/achievements";
// Utils
import { createTGWindow } from "../utils/TGWindow";
import { getUiafHeader, readUiafData, verifyUiafData } from "../utils/UIAF";
import TGSqlite from "../utils/TGSqlite";
// Store
const achievementsStore = useAchievementsStore();
// loading
const loading = ref(true as boolean);
const loadingTitle = ref("正在加载数据" as string);
// data
const title = ref(achievementsStore.title as string);
const getCardInfo = ref({} as TGApp.Sqlite.NameCard.Item);
// series
const seriesList = ref([] as TGApp.Sqlite.Achievement.SeriesTable[]);
const selectedSeries = ref(-1 as number);
const selectedAchievement = ref([] as TGApp.Sqlite.Achievement.SingleTable[]);
const renderAchievement = computed(() => {
return selectedAchievement.value.slice(start.value, start.value + itemCount.value + 1);
});
// virtual list
const start = ref(0 as number);
const itemCount = computed(() => {
return Math.ceil((window.innerHeight - 100) / 76);
});
const emptyHeight = computed(() => {
return selectedAchievement.value.length * 76;
});
const translateY = ref("0px" as string);
// render
const search = ref("" as string);
const snackbar = ref(false as boolean);
const snackbarText = ref("" as string);
const snackbarColor = ref("#F5810A" as string);
onBeforeMount(async () => {
const { total, fin } = await TGSqlite.getAchievementsOverview();
achievementsStore.flushData(total, fin);
title.value = achievementsStore.title;
});
onMounted(async () => {
loading.value = true;
loadingTitle.value = "正在获取成就系列数据";
seriesList.value = await TGSqlite.getAchievementSeries();
loadingTitle.value = "正在获取成就数据";
selectedAchievement.value = await TGSqlite.getAchievements();
loading.value = false;
});
function handleScroll (e: Event) {
// 如果 scrollTop 到底部了
if ((e.target as HTMLElement).scrollTop + (e.target as HTMLElement).offsetHeight >= (e.target as HTMLElement).scrollHeight) {
// 如果 selectedAchievement 的长度小于 itemCount不进行偏移
if (selectedAchievement.value.length <= itemCount.value) {
window.requestAnimationFrame(() => {
start.value = 0;
translateY.value = "0px";
});
return;
}
window.requestAnimationFrame(() => {
start.value = selectedAchievement.value.length - itemCount.value;
translateY.value = `${(selectedAchievement.value.length - itemCount.value) * 76}px`;
});
return;
}
if (selectedSeries.value !== 0 && selectedSeries.value !== 17 && selectedSeries.value !== -1) {
window.requestAnimationFrame(() => {
const { scrollTop } = e.target as HTMLElement;
if (scrollTop < 86.8) {
start.value = 0;
translateY.value = "0px";
} else {
start.value = Math.floor((scrollTop - 86.8) / 76);
translateY.value = `${scrollTop - 86.8}px`;
}
});
} else {
window.requestAnimationFrame(() => {
const { scrollTop } = e.target as HTMLElement;
start.value = Math.floor(scrollTop / 76);
translateY.value = `${scrollTop}px`;
});
}
}
// 渲染选中的成就系列
async function selectSeries (index: number) {
// 如果选中的是已经选中的系列,则不进行操作
if (selectedSeries.value === index) {
snackbarText.value = "已经选中该系列";
snackbar.value = true;
return;
}
loading.value = true;
loadingTitle.value = "正在获取对应的成就数据";
selectedSeries.value = index;
selectedAchievement.value = await TGSqlite.getAchievements(index);
loadingTitle.value = "正在查找对应的成就名片";
if (selectedSeries.value !== 0 && selectedSeries.value !== 17) {
getCardInfo.value = await TGSqlite.getNameCard(index);
}
loading.value = false;
}
// 打开图片
function openImg () {
createTGWindow(getCardInfo.value.profile, "nameCard", getCardInfo.value.name, 840, 400, false);
}
async function searchCard () {
if (search.value === "") {
snackbarColor.value = "#F5810A";
snackbarText.value = "请输入搜索内容";
snackbar.value = true;
return;
}
selectedSeries.value = -1;
loadingTitle.value = "正在搜索";
loading.value = true;
selectedAchievement.value = await TGSqlite.searchAchievements(search.value);
if (selectedAchievement.value.length === 0) {
snackbarColor.value = "#F5810A";
snackbarText.value = "没有找到对应的成就";
snackbar.value = true;
}
loading.value = false;
}
// 导入 UIAF 数据,进行数据合并、刷新
async function importJson () {
const selectedFile = await dialog.open({
multiple: false,
filters: [
{
name: "JSON",
extensions: ["json"],
},
],
});
if (selectedFile && (await verifyUiafData(<string>selectedFile))) {
const remoteRaw: string | false = await readUiafData(<string>selectedFile);
if (remoteRaw === false) {
snackbarText.value = "读取 UIAF 数据失败,请检查文件是否符合规范";
snackbar.value = true;
return;
}
loadingTitle.value = "正在解析数据";
loading.value = true;
loadingTitle.value = "正在合并成就数据";
await TGSqlite.mergeUIAF(JSON.parse(remoteRaw).list);
loadingTitle.value = "即将刷新页面";
setTimeout(() => {
window.location.reload();
}, 1000);
}
}
// 导出
async function exportJson () {
// 判断是否有数据
if (achievementsStore.finAchievements === 0) {
snackbarColor.value = "#F5810A";
snackbarText.value = "没有可导出的数据";
snackbar.value = true;
return;
}
// 获取本地数据
const UiafData = {
info: await getUiafHeader(),
list: await TGSqlite.getUIAF(),
};
const isSave = await dialog.save({
// TODO: 设置保存文件名
filters: [
{
name: "uiaf",
extensions: ["json"],
},
],
});
if (isSave) {
await fs.writeTextFile(isSave, JSON.stringify(UiafData));
snackbarColor.value = "#00BFA5";
snackbarText.value = "导出成功";
snackbar.value = true;
} else {
snackbarColor.value = "#F5810A";
snackbarText.value = "导出已取消";
snackbar.value = true;
}
}
</script>
<style lang="css" scoped>
/* 顶部按钮 */
.top-btn {
background: #393b40;
color: #faf7e8 !important;
}
/* 内容区域 */
.wrap {
display: flex;
justify-content: space-between;
}
/* 左侧系列 */
.left-wrap {
width: 400px;
height: calc(100vh - 100px);
overflow-y: scroll;
overflow-x: hidden;
}
/* 右侧成就 */
.right-wrap {
width: calc(100% - 410px);
height: calc(100vh - 100px);
overflow-y: scroll;
overflow-x: hidden;
}
.list-empty {
position: relative;
width: 100%;
}
/* 版本信息 */
.version-icon-series {
font-family: Genshin, serif;
position: absolute;
right: 0;
bottom: 0;
text-align: center;
width: 80px;
background: #546d8b;
border-radius: 10px 0 0;
border-top: #fff 2px solid;
border-left: #fff 2px solid;
color: #fec90b;
font-size: 10px;
}
.version-icon-single {
font-family: Genshin, serif;
border-radius: 5px;
text-align: center;
color: #ff6d6d;
font-size: 10px;
}
.card-left {
border-radius: 10px;
margin: 10px;
background: #485466;
color: #fec90b;
cursor: pointer;
}
/* 成就卡片 */
.card-right {
border-radius: 10px;
margin: 10px;
background: #546d8b;
color: #faf7e8;
}
/* 成就进度 */
.achievement-progress {
font-family: Genshin, serif;
position: absolute;
left: 0;
top: 0;
text-align: center;
width: 65px;
background: #8BA5C5;
border-bottom-right-radius: 20px;
border-bottom: #fff 2px solid;
border-right: #fff 2px solid;
color: #485466;
font-size: 10px;
}
/* 成就完成时间 */
.right-time {
margin-right: 10px;
font-size: small;
color: #faf7e8;
}
/* 成就奖励 */
.reward-card {
position: relative;
width: 40px;
height: 40px;
border-radius: 5px;
background-image: url("/icon/bg/5-Star.webp");
background-size: cover;
}
.reward-num {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 10px;
background: rgb(0 0 0 / 50%);
color: #faf7e8;
display: flex;
font-size: 8px;
justify-content: center;
align-items: center;
}
</style>