真境剧诗适配

close #113
This commit is contained in:
目棃
2024-11-11 15:09:21 +08:00
parent d35b94f79f
commit da4a095618
23 changed files with 1227 additions and 18 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@@ -0,0 +1,47 @@
<template>
<div class="tuca-box">
<TItembox v-for="(item, idx) in props.modelValue" :key="idx" :model-value="getItemBox(item)" />
</div>
</template>
<script lang="ts" setup>
import { computed } from "vue";
import { getZhElement } from "../../utils/toolFunc.js";
import TItembox, { TItemBoxData } from "../main/t-itembox.vue";
interface TucAvatarsProps {
modelValue: TGApp.Game.Combat.Avatar[];
}
const props = defineProps<TucAvatarsProps>();
const columnCnt = computed<number>(() => {
if (props.modelValue.length % 2 === 1) return (props.modelValue.length + 1) / 2;
return props.modelValue.length / 2;
});
function getItemBox(item: TGApp.Game.Combat.Avatar): TItemBoxData {
return {
bg: `/icon/bg/${item.rarity === 105 ? 5 : item.rarity}-BGC.webp`,
clickable: false,
display: "inner",
height: "80px",
icon: `/WIKI/character/${item.avatar_id}.webp`,
innerHeight: item.avatar_type !== 1 ? 20 : 0,
innerText: item.avatar_type === 2 ? "试用角色" : item.avatar_type === 3 ? "助演角色" : "",
lt: `/icon/element/${getZhElement(item.element)}元素.webp`,
ltSize: "20px",
innerBlur: "5px",
rt: "",
rtSize: "",
size: "80px",
};
}
</script>
<style lang="css" scoped>
.tuca-box {
display: grid;
width: 100%;
grid-gap: 10px;
grid-template-columns: repeat(v-bind(columnCnt), 1fr);
}
</style>

View File

@@ -0,0 +1,68 @@
<template>
<div class="tuc-buff-box">
<div class="tuc-buff-item">
<img alt="total" src="/source/UI/combatCrown.webp" />
<span>{{ props.modelValue.summary.total_level }}</span>
</div>
<div
class="tuc-buff-item"
v-for="(item, idx) in props.modelValue.buffs"
:key="idx"
:title="item.name"
>
<img :alt="item.name" :src="item.icon" />
<span>{{ item.level }}</span>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed } from "vue";
interface TucBuffProps {
modelValue: TGApp.Game.Combat.SplendourBuff;
}
const props = defineProps<TucBuffProps>();
const columnCnt = computed<number>(() => {
const len = props.modelValue.buffs.length;
if ((len + 1) % 2 === 1) return len / 2 + 1;
return (len + 1) / 2;
});
</script>
<style lang="css" scoped>
.tuc-buff-box {
display: grid;
width: 100%;
grid-gap: 5px;
grid-template-columns: repeat(v-bind(columnCnt), 1fr);
}
.tuc-buff-item {
position: relative;
display: flex;
width: 80px;
height: 80px;
align-items: center;
justify-content: center;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
span {
position: absolute;
right: 0;
bottom: 0;
padding: 0 5px;
background: var(--common-shadow-2);
border-bottom-right-radius: 5px;
border-top-left-radius: 5px;
color: var(--tgc-white-2);
font-family: var(--font-title);
font-size: 14px;
text-shadow: 0 0 5px rgb(0 0 0 / 20%);
}
}
</style>

View File

@@ -0,0 +1,50 @@
<template>
<div class="tuc-cards-box">
<div
class="tuc-cards-item"
v-for="(card, idx) in props.modelValue"
:key="idx"
:title="card.name"
>
<img :src="card.icon" :alt="card.name" />
</div>
</div>
</template>
<script lang="ts" setup>
interface TucCardsProps {
modelValue: TGApp.Game.Combat.Card[];
}
const props = defineProps<TucCardsProps>();
</script>
<style lang="css" scoped>
.tuc-cards-box {
display: flex;
width: 100%;
flex-wrap: wrap;
align-items: flex-start;
justify-content: flex-start;
gap: 5px;
}
.tuc-cards-item {
position: relative;
display: flex;
width: 50px;
align-items: center;
justify-content: center;
aspect-ratio: 1;
img {
max-width: 100%;
filter: invert(1);
object-fit: cover;
}
}
.dark .tuc-cards-item {
img {
filter: none;
}
}
</style>

View File

@@ -0,0 +1,88 @@
<template>
<div class="tucfi-box">
<div class="tucfi-label">
<slot name="label">{{ props.label }}</slot>
</div>
<div v-if="!Array.isArray(props.data)" class="tucfi-data">
<TItembox :model-value="getBox()" />
</div>
<div class="tucfi-icons" v-else>
<div v-for="(item, idx) in props.data" :key="idx" class="tucfi-icon">
<TItembox :model-value="getBox2(item)" />
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import TItembox, { TItemBoxData } from "../main/t-itembox.vue";
interface TucFightProps {
label: string;
data: TGApp.Game.Combat.AvatarMini | TGApp.Game.Combat.AvatarMini[];
}
const props = defineProps<TucFightProps>();
function getBox(): TItemBoxData {
const role = <TGApp.Game.Combat.AvatarMini>props.data;
return {
bg: `/icon/bg/${role.rarity === 105 ? 5 : role.rarity}-BGC.webp`,
clickable: false,
display: "inner",
height: "60px",
icon: `/WIKI/character/${role.avatar_id}.webp`,
innerText: role.value,
innerHeight: 20,
innerBlur: "5px",
lt: "",
ltSize: "0",
size: "60px",
};
}
function getBox2(item: TGApp.Game.Combat.AvatarMini): TItemBoxData {
return {
bg: `/icon/bg/${item.rarity === 105 ? 5 : item.rarity}-BGC.webp`,
clickable: false,
display: "inner",
height: "60px",
icon: `/WIKI/character/${item.avatar_id}.webp`,
innerText: "",
lt: "",
ltSize: "0",
size: "60px",
};
}
</script>
<style lang="css" scoped>
.tucfi-box {
display: flex;
width: 100%;
height: auto;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 10px;
border-radius: 5px;
background: var(--box-bg-1);
}
.tucfi-label {
color: var(--box-text-4);
font-family: var(--font-title);
font-size: 20px;
}
.tucfi-data {
color: var(--tgc-yellow-1);
font-family: var(--font-text);
font-size: 20px;
}
.tucfi-icons {
display: flex;
align-items: center;
justify-content: center;
column-gap: 10px;
}
</style>

View File

@@ -0,0 +1,55 @@
<template>
<div class="tuco-box">
<TucTile title="最佳记录" :val="props.data.max_round_id" />
<TucTile :title="`获得星章-${props.data.medal_num}`" :val="props.data.get_medal_round_list" />
<TucTile :title="getTitle()" :val="`第${props.data.max_round_id}幕`" />
<TucTile title="消耗幻剧之花" :val="props.data.coin_num" />
<TucFight label="最快完成演出" :data="props.fights.shortest_avatar_list" />
<TucTile title="总耗时" :val="getTime()" />
<!-- <TucTile title="助演角色支援" :val="`${props.data.rent_cnt}次`" />-->
<!-- <TucTile title="场外声援" :val="`${props.data.avatar_bonus_num}次`" />-->
<TucFight label="击败最多敌人" :data="props.fights.max_defeat_avatar" />
<TucFight label="最高伤害输出" :data="props.fights.max_damage_avatar" />
<TucFight label="最高承受伤害" :data="props.fights.max_take_damage_avatar" />
</div>
</template>
<script lang="ts" setup>
import TucFight from "./tuc-fight.vue";
import TucTile from "./tuc-tile.vue";
interface TucOverviewProps {
data: TGApp.Game.Combat.Stat;
fights: TGApp.Game.Combat.FightStatisic;
}
const props = defineProps<TucOverviewProps>();
function getTitle(): string {
switch (props.data.difficulty_id) {
case 1:
return "轻简模式";
case 2:
return "普通模式";
case 3:
return "困难模式";
case 4:
return "卓越模式";
default:
return `未知模式${props.data.difficulty_id}`;
}
}
function getTime(): string {
const sec = props.fights.total_use_time % 60;
const min = (props.fights.total_use_time - sec) / 60;
return `${min}${sec}`;
}
</script>
<style lang="css" scoped>
.tuco-box {
display: grid;
width: 100%;
grid-gap: 10px;
grid-template-columns: repeat(3, 1fr);
}
</style>

View File

@@ -0,0 +1,86 @@
<template>
<div class="tucr-box">
<div class="tucr-title">
<img :src="`/icon/star/combat${props.modelValue.is_get_medal ? 1 : 0}.webp`" alt="combat" />
<span class="main">{{ props.modelValue.round_id }}</span>
<span class="sub">{{ timestampToDate(Number(props.modelValue.finish_time) * 1000) }}</span>
</div>
<div class="tucr-content">
<TucSub title="出演角色" class="main">
<TucAvatars :model-value="props.modelValue.avatars" />
</TucSub>
<TucSub title="辉彩祝福" class="main">
<TucBuffs :model-value="props.modelValue.splendour_buff" />
</TucSub>
<TucSub :title="`神秘收获(${props.modelValue.choice_cards.length})`" class="sub">
<TucCards :model-value="props.modelValue.choice_cards" />
</TucSub>
</div>
</div>
</template>
<script lang="ts" setup>
import { timestampToDate } from "../../utils/toolFunc.js";
import TucAvatars from "./tuc-avatars.vue";
import TucBuffs from "./tuc-buffs.vue";
import TucCards from "./tuc-cards.vue";
import TucSub from "./tuc-sub.vue";
interface TucRoundProps {
modelValue: TGApp.Game.Combat.RoundData;
}
const props = defineProps<TucRoundProps>();
</script>
<style lang="css" scoped>
.tucr-box {
display: flex;
width: 100%;
height: fit-content;
flex-direction: column;
align-items: center;
padding: 5px;
border-radius: 5px;
background: var(--box-bg-1);
row-gap: 5px;
}
.tucr-title {
display: flex;
align-items: flex-end;
justify-content: center;
margin-right: auto;
column-gap: 5px;
img {
width: 30px;
aspect-ratio: 1;
}
.main {
color: var(--common-text-title);
font-family: var(--font-title);
font-size: 20px;
}
.sub {
opacity: 0.8;
}
}
.tucr-content {
display: flex;
width: 100%;
align-items: flex-start;
justify-content: flex-start;
column-gap: 10px;
.main {
width: 180px;
}
.sub {
width: 100%;
}
}
</style>

View File

@@ -0,0 +1,32 @@
<template>
<div class="tuc-sub-box">
<div class="tuc-sub-title">{{ props.title }}</div>
<slot name="default" />
</div>
</template>
<script lang="ts" setup>
interface TucSubProps {
title: string;
}
const props = defineProps<TucSubProps>();
</script>
<style lang="css" scoped>
.tuc-sub-box {
position: relative;
display: flex;
height: 200px;
flex-direction: column;
align-items: center;
justify-content: flex-start;
padding: 5px;
border-radius: 5px;
background: var(--box-bg-2);
}
.tuc-sub-title {
margin-right: auto;
color: var(--box-text-2);
font-family: var(--font-title);
}
</style>

View File

@@ -0,0 +1,62 @@
<template>
<div class="tuct-box">
<div class="tuct-title">
<slot name="title">{{ props.title }}</slot>
</div>
<div class="tuct-text" v-if="!Array.isArray(props.val)">
<slot name="text">{{ props.val }}</slot>
</div>
<div class="tuct-icons" v-else>
<img
v-for="(val, idx) in props.val"
:key="idx"
:src="`/icon/star/combat${val}.webp`"
:alt="`${val}`"
/>
</div>
</div>
</template>
<script lang="ts" setup>
interface TucTileProps {
title: string;
val: string | number | number[];
}
const props = defineProps<TucTileProps>();
</script>
<style lang="css" scoped>
.tuct-box {
display: flex;
width: 100%;
height: auto;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 10px;
border-radius: 5px;
background: var(--box-bg-1);
}
.tuct-title {
color: var(--box-text-4);
font-family: var(--font-title);
font-size: 20px;
}
.tuct-text {
color: var(--tgc-yellow-1);
font-family: var(--font-text);
font-size: 20px;
}
.tuct-icons {
display: flex;
align-items: center;
justify-content: center;
img {
height: 30px;
aspect-ratio: 1;
}
}
</style>

View File

@@ -12,6 +12,18 @@
:hide-details="true" :hide-details="true"
title="游戏UID" title="游戏UID"
/> />
<v-btn :rounded="true" class="ua-btn" @click="toCombat()">
<template #prepend>
<img src="/source/UI/userCombat.webp" alt="combat" />
</template>
<span>幻想真境剧诗</span>
</v-btn>
<v-btn :rounded="true" class="ua-btn" @click="toWiki()">
<template #prepend>
<img src="/source/UI/wikiAbyss.webp" alt="wiki" />
</template>
<span>深渊数据库</span>
</v-btn>
</div> </div>
</template> </template>
<template #append> <template #append>
@@ -61,7 +73,7 @@
<span>更新于</span> <span>更新于</span>
<span>{{ item.updated }}</span> <span>{{ item.updated }}</span>
</div> </div>
<div class="uaw-share">Render by TeyvatGuide v{{ version }}</div> <div class="uaw-share">深境螺旋 | Render by TeyvatGuide v{{ version }}</div>
</div> </div>
<TSubLine>统计周期 {{ item.startTime }} ~ {{ item.endTime }}</TSubLine> <TSubLine>统计周期 {{ item.startTime }} ~ {{ item.endTime }}</TSubLine>
<div class="uaw-o-box"> <div class="uaw-o-box">
@@ -101,6 +113,7 @@
import { getVersion } from "@tauri-apps/api/app"; import { getVersion } from "@tauri-apps/api/app";
import { storeToRefs } from "pinia"; import { storeToRefs } from "pinia";
import { onMounted, ref, watch, computed } from "vue"; import { onMounted, ref, watch, computed } from "vue";
import { useRouter } from "vue-router";
import showConfirm from "../../components/func/confirm.js"; import showConfirm from "../../components/func/confirm.js";
import showSnackbar from "../../components/func/snackbar.js"; import showSnackbar from "../../components/func/snackbar.js";
@@ -130,6 +143,7 @@ const user = computed<TGApp.Sqlite.Account.Game>(() => userStore.account.value);
const localAbyss = ref<TGApp.Sqlite.Abyss.SingleTable[]>([]); const localAbyss = ref<TGApp.Sqlite.Abyss.SingleTable[]>([]);
const abyssRef = ref<HTMLElement>(<HTMLElement>{}); const abyssRef = ref<HTMLElement>(<HTMLElement>{});
const version = ref<string>(); const version = ref<string>();
const router = useRouter();
const uidList = ref<string[]>(); const uidList = ref<string[]>();
const uidCur = ref<string>(); const uidCur = ref<string>();
@@ -154,6 +168,14 @@ watch(
async () => await loadAbyss(), async () => await loadAbyss(),
); );
async function toCombat(): Promise<void> {
await router.push({ name: "真境剧诗" });
}
async function toWiki(): Promise<void> {
await router.push({ name: "深渊数据库" });
}
async function loadAbyss(): Promise<void> { async function loadAbyss(): Promise<void> {
localAbyss.value = []; localAbyss.value = [];
if (uidCur.value === undefined || uidCur.value === "") return; if (uidCur.value === undefined || uidCur.value === "") return;
@@ -330,10 +352,13 @@ async function deleteAbyss(): Promise<void> {
} }
span { span {
color: var(--common-text-title);
font-family: var(--font-title); font-family: var(--font-title);
font-size: 20px; font-size: 20px;
} }
span :first-child {
color: var(--common-text-title);
}
} }
.uat-right { .uat-right {

424
src/pages/User/Combat.vue Normal file
View File

@@ -0,0 +1,424 @@
<template>
<ToLoading v-model="loading" :title="loadingTitle" :subtitle="loadingSub" />
<v-app-bar>
<template #prepend>
<div class="uct-left">
<img alt="icon" src="/source/UI/userCombat.webp" />
<span>幻想真境剧诗</span>
<v-select
variant="outlined"
v-model="uidCur"
:items="uidList"
:hide-details="true"
title="游戏UID"
/>
<v-btn :rounded="true" class="uc-btn" @click="toAbyss()">
<template #prepend>
<img src="/source/UI/userAbyss.webp" alt="abyss" />
</template>
<span>深境螺旋</span>
</v-btn>
</div>
</template>
<template #append>
<div class="uct-right">
<v-btn
class="uc-btn"
@click="shareCombat()"
:rounded="true"
:disabled="localCombat.length === 0"
>
<v-icon>mdi-share</v-icon>
<span>分享</span>
</v-btn>
<v-btn class="uc-btn" @click="refreshCombat()" :rounded="true">
<v-icon>mdi-refresh</v-icon>
<span>刷新</span>
</v-btn>
<v-btn class="uc-btn" @click="uploadCombat()" :rounded="true">
<v-icon>mdi-cloud-upload</v-icon>
<span>上传</span>
</v-btn>
<v-btn class="uc-btn" @click="deleteCombat()" :rounded="true">
<v-icon>mdi-delete</v-icon>
<span>删除</span>
</v-btn>
</div>
</template>
</v-app-bar>
<div class="uc-box">
<v-tabs v-model="userTab" direction="vertical" class="uc-tabs-box" center-active>
<v-tab v-for="item in localCombat" :key="item.id" :value="item.id"> {{ item.id }}</v-tab>
</v-tabs>
<v-window v-model="userTab" class="uc-window">
<v-window-item
v-for="item in localCombat"
:key="item.id"
:value="item.id"
class="uc-window-item"
>
<div :id="`user-combat-${item.id}`" class="ucw-i-ref">
<div class="ucw-top">
<div class="ucw-title">
<span></span>
<span>{{ item.id }}</span>
<span> UID</span>
<span>{{ uidCur }}</span>
<span>更新于</span>
<span>{{ item.updated }}</span>
</div>
<div class="ucw-share">真境剧诗 | Render by TeyvatGuide v{{ version }}</div>
</div>
<TSubLine>统计周期 {{ item.startTime }} ~ {{ item.endTime }}</TSubLine>
<TucOverview :data="item.stat" :fights="item.detail.fight_statisic" />
<TSubLine>使用角色</TSubLine>
<TucAvatars :model-value="item.detail.backup_avatars" />
<TSubLine>详情</TSubLine>
<div class="ucw-rounds">
<TucRound
v-for="(round, idx) in item.detail.rounds_data"
:key="idx"
:model-value="round"
/>
</div>
</div>
</v-window-item>
</v-window>
<div v-show="localCombat.length === 0" class="user-empty">
<img src="/source/UI/empty.webp" alt="empty" />
<span>暂无数据请尝试刷新</span>
</div>
</div>
</template>
<script lang="ts" setup>
import { getVersion } from "@tauri-apps/api/app";
import { storeToRefs } from "pinia";
import { onMounted, ref, watch, computed } from "vue";
import { useRouter } from "vue-router";
import showConfirm from "../../components/func/confirm.js";
import showSnackbar from "../../components/func/snackbar.js";
import TSubLine from "../../components/main/t-subline.vue";
import ToLoading from "../../components/overlay/to-loading.vue";
import TucAvatars from "../../components/userCombat/tuc-avatars.vue";
import TucOverview from "../../components/userCombat/tuc-overview.vue";
import TucRound from "../../components/userCombat/tuc-round.vue";
import Hutao from "../../plugins/Hutao/index.js";
import TSUserCombat from "../../plugins/Sqlite/modules/userCombat.js";
import { useUserStore } from "../../store/modules/user.js";
import TGLogger from "../../utils/TGLogger.js";
import { generateShareImg } from "../../utils/TGShare.js";
import TGRequest from "../../web/request/TGRequest.js";
// store
const userStore = storeToRefs(useUserStore());
// loading
const loading = ref<boolean>(true);
const loadingTitle = ref<string>();
const loadingSub = ref<string>();
// data
const userTab = ref<number>(0);
const user = computed<TGApp.Sqlite.Account.Game>(() => userStore.account.value);
const localCombat = ref<TGApp.Sqlite.Combat.SingleTable[]>([]);
const combatRef = ref<HTMLElement>(<HTMLElement>{});
const version = ref<string>();
const router = useRouter();
const uidList = ref<string[]>();
const uidCur = ref<string>();
const combatIdList = computed<number[]>(() => {
return localCombat.value.map((combat) => combat.id);
});
onMounted(async () => {
version.value = await getVersion();
await TGLogger.Info("[UserCombat][onMounted] 打开真境剧诗页面");
loadingTitle.value = "正在加载剧诗数据";
uidList.value = await TSUserCombat.getAllUid();
if (uidList.value.includes(user.value.gameUid)) uidCur.value = user.value.gameUid;
else if (uidList.value.length > 0) uidCur.value = uidList.value[0];
else uidCur.value = "";
await loadCombat();
loading.value = false;
});
watch(
() => uidCur.value,
async () => await loadCombat(),
);
async function toAbyss(): Promise<void> {
await router.push({ name: "深渊记录" });
}
async function loadCombat(): Promise<void> {
localCombat.value = [];
if (uidCur.value === undefined || uidCur.value === "") return;
localCombat.value = await TSUserCombat.getCombat(uidCur.value);
if (localCombat.value.length > 0) userTab.value = localCombat.value[0].id;
}
async function refreshCombat(): Promise<void> {
if (!userStore.cookie.value) {
showSnackbar({ text: "未登录", color: "error" });
await TGLogger.Warn("[UserCombat][getAbyssData] 未登录");
return;
}
if (uidCur.value && uidCur.value !== user.value.gameUid) {
const confirmSwitch = await showConfirm({
title: "是否切换游戏账户",
text: `确认则尝试切换至 ${uidCur.value}`,
});
if (confirmSwitch) {
await useUserStore().switchGameAccount(uidCur.value);
await refreshCombat();
return;
}
const confirm = await showConfirm({
title: "确定刷新?",
text: `用户${user.value.gameUid}与当前UID${uidCur.value}不一致`,
});
if (!confirm) {
showSnackbar({ text: "已取消剧诗数据刷新", color: "cancel" });
return;
}
}
await TGLogger.Info("[UserCombat][getCombatData] 更新剧诗数据");
loadingTitle.value = `正在获取${user.value.gameUid}的深渊数据`;
loading.value = true;
loadingTitle.value = `正在获取${user.value.gameUid}的剧诗数据`;
const res = await TGRequest.User.byCookie.getCombat(userStore.cookie.value, user.value);
if (res === false) {
loading.value = false;
showSnackbar({ text: "用户未解锁幻想真境剧诗!", color: "warn" });
return;
}
if ("retcode" in res) {
showSnackbar({ text: `[${res.retcode}]${res.message}`, color: "error" });
loading.value = false;
await TGLogger.Error(`[UserCombat][getCombatData] 获取${user.value.gameUid}的剧诗数据失败`);
await TGLogger.Error(`[UserCombat][getCombatData] ${res.retcode} ${res.message}`);
return;
}
loadingTitle.value = `正在保存剧诗数据`;
for (const combat of res) {
loadingSub.value = `正在保存第${combat.schedule.schedule_id}期数据`;
await TSUserCombat.saveCombat(user.value.gameUid, combat);
}
loadingTitle.value = "正在加载剧诗数据";
uidList.value = await TSUserCombat.getAllUid();
uidCur.value = user.value.gameUid;
await loadCombat();
loading.value = false;
}
async function shareCombat(): Promise<void> {
await TGLogger.Info(`[UserCombat][shareCombat][${userTab.value}] 生成剧诗数据分享图片`);
const fileName = `【剧诗数据】${userTab.value}-${user.value.gameUid}`;
loadingTitle.value = "正在生成图片";
loadingSub.value = `${fileName}.png`;
loading.value = true;
combatRef.value = <HTMLElement>document.getElementById(`user-combat-${userTab.value}`);
await generateShareImg(fileName, combatRef.value);
loadingSub.value = "";
loading.value = false;
await TGLogger.Info(`[UserCombat][shareCombat][${userTab.value}] 生成剧诗数据分享图片成功`);
}
async function uploadCombat(): Promise<void> {
await TGLogger.Info("[UserCombat][uploadCombat] 上传剧诗数据");
const combatData = localCombat.value.find((item) => item.id === Math.max(...combatIdList.value));
if (!combatData) {
showSnackbar({ text: "未找到剧诗数据", color: "error" });
await TGLogger.Warn("[UserCombat][uploadCombat] 未找到深渊数据");
return;
}
if (!combatData.hasDetailData) {
showSnackbar({ text: "未获取到详情数据", color: "error" });
await TGLogger.Warn(`[UserCombat][uploadCombat] 未获取到详细数据`);
return;
}
const startTime = new Date(combatData.startTime).getTime();
const endTime = new Date(combatData.endTime).getTime();
const nowTime = new Date().getTime();
if (nowTime < startTime || nowTime > endTime) {
showSnackbar({ text: "非最新剧诗数据,请刷新剧诗数据后重试!", color: "error" });
await TGLogger.Warn("[UserCombat][uploadCombat] 非最新剧诗数据");
return;
}
try {
loadingTitle.value = "正在转换剧诗数据";
loadingSub.value = "";
loading.value = true;
const transCombat = Hutao.Combat.trans(combatData);
loadingTitle.value = "正在上传剧诗数据";
const res = await Hutao.Combat.upload(transCombat);
loading.value = false;
if (res.retcode === 0) {
showSnackbar({ text: res.message ?? "上传剧诗数据成功" });
await TGLogger.Info("[UserCombat][uploadCombat] 上传剧诗数据成功");
} else {
showSnackbar({ text: `[${res.retcode}]${res.message}`, color: "error" });
await TGLogger.Error("[UserCombat][uploadCombat] 上传剧诗数据失败");
await TGLogger.Error(`[UserCombat][uploadCombat] ${res.retcode} ${res.message}`);
}
} catch (e) {
if (e instanceof Error) {
showSnackbar({ text: e.message, color: "error" });
await TGLogger.Error("[UserCombat][uploadCombat] 上传剧诗数据失败");
await TGLogger.Error(`[UserCombat][uploadCombat] ${e.message}`);
}
}
if (loading.value) loading.value = false;
}
async function deleteCombat(): Promise<void> {
if (uidCur.value === undefined || uidCur.value === "") {
showSnackbar({ text: "未找到符合条件的数据!", color: "error" });
return;
}
const confirm = await showConfirm({
title: "确定删除数据?",
text: `将清除${uidCur.value}的所有剧诗数据`,
});
if (!confirm) {
showSnackbar({ text: "已取消删除", color: "cancel" });
return;
}
loadingTitle.value = `正在删除 ${uidCur.value} 的剧诗数据`;
loading.value = true;
await TSUserCombat.delCombat(uidCur.value);
await new Promise((resolve) => setTimeout(resolve, 1000));
loading.value = false;
showSnackbar({ text: `已清除 ${uidCur.value} 的剧诗数据`, color: "success" });
uidList.value = await TSUserCombat.getAllUid();
if (uidList.value.length > 0) uidCur.value = uidList.value[0];
else uidCur.value = undefined;
await loadCombat();
}
</script>
<style lang="css" scoped>
.uct-left {
display: flex;
width: 100%;
align-items: center;
justify-content: center;
padding: 10px;
gap: 10px;
img {
width: 32px;
height: 32px;
}
span {
font-family: var(--font-title);
font-size: 20px;
}
span :first-child {
color: var(--common-text-title);
}
}
.uct-right {
display: flex;
width: 100%;
align-items: center;
justify-content: center;
padding: 10px;
gap: 10px;
}
.uc-btn {
background: var(--tgc-btn-1);
color: var(--btn-text);
font-family: var(--font-text);
}
.uc-box {
display: flex;
height: calc(100vh - 100px);
align-items: flex-start;
justify-content: center;
border: 1px solid var(--common-shadow-4);
border-radius: 5px;
}
.uc-tabs-box {
max-height: 100%;
overflow-y: auto;
}
.uc-window {
overflow: hidden;
width: calc(100% - 100px);
height: 100%;
padding: 10px;
}
.uc-window-item {
height: 100%;
padding: 10px;
border-radius: 5px;
overflow-y: auto;
}
.ucw-i-ref {
display: flex;
flex-direction: column;
gap: 5px;
}
.ucw-top {
display: flex;
width: 100%;
align-items: flex-end;
justify-content: space-between;
}
.ucw-title {
display: flex;
align-items: center;
color: var(--common-text-title);
font-family: var(--font-title);
font-size: 20px;
}
.ucw-title :nth-child(2n) {
margin-right: 10px;
margin-left: 10px;
color: var(--tgc-yellow-1);
}
.ucw-share {
z-index: -1;
font-size: 12px;
opacity: 0.8;
}
.user-empty {
position: absolute;
top: calc(50vh - 200px);
left: calc(50vw - 400px);
display: flex;
width: 800px;
height: 400px;
flex-direction: column;
align-items: center;
border-radius: 5px;
background: var(--common-shadow-t-2);
box-shadow: 0 0 5px var(--common-shadow-2);
color: var(--common-text-title);
font-family: var(--font-title);
font-size: 1.5rem;
}
.ucw-rounds {
display: grid;
gap: 10px;
grid-template-columns: repeat(2, 1fr);
}
</style>

View File

@@ -7,21 +7,17 @@
/** /**
* @description 将本地数据转为上传用的数据 * @description 将本地数据转为上传用的数据
* @since Beta v0.6.3 * @since Beta v0.6.3
* @param {number[]} avatars 角色 * @param {TGApp.Sqlite.Combat.SingleTable} data 数据
* @param {number} schedule 期数
* @param {number} uid UID
* @returns {TGApp.Plugins.Hutao.Combat.UploadData} 上传用的数据 * @returns {TGApp.Plugins.Hutao.Combat.UploadData} 上传用的数据
*/ */
export function transCombatLocal( export function transCombatLocal(
avatars: number[], data: TGApp.Sqlite.Combat.SingleTable,
schedule: number,
uid: string,
): TGApp.Plugins.Hutao.Combat.UploadData { ): TGApp.Plugins.Hutao.Combat.UploadData {
return { return {
Version: 1, Version: 1,
Uid: uid, Uid: data.uid,
Identity: "TeyvatGuide", Identity: "TeyvatGuide",
BackupAvatars: avatars, BackupAvatars: data.detail.backup_avatars.map((i) => i.avatar_id),
ScheduleId: schedule, ScheduleId: data.id,
}; };
} }

View File

@@ -0,0 +1,159 @@
/**
* @file plugins/Sqlite/modules/userCombat.ts
* @description Sqlite-幻想真境剧诗模块
* @since Beta v0.6.3
*/
import { path } from "@tauri-apps/api";
import { exists, mkdir, readTextFile, writeTextFile } from "@tauri-apps/plugin-fs";
import TGLogger from "../../../utils/TGLogger.js";
import { timestampToDate } from "../../../utils/toolFunc.js";
import TGSqlite from "../index.js";
import { transUserCombat } from "../utils/transUserCombat.js";
/**
* @description 直接插入数据
* @since Beta v0.6.3
* @param {string} uid 用户UID
* @param {TGApp.Sqlite.Combat.SingleTable} data 剧诗数据
* @returns {string}
*/
function getInsertSql(data: TGApp.Sqlite.Combat.SingleTable, uid?: string): string {
const timeNow = timestampToDate(new Date().getTime());
const hasData = data.hasData ? 1 : 0;
const hasDetailData = data.hasDetailData ? 1 : 0;
return `
INSERT INTO RoleCombat(uid, id, startTime, endTime, hasData, hasDetailData, stat, detail, updated)
VALUES ('${uid ?? data.uid}', ${data.id}, '${data.startTime}', '${data.endTime}', ${hasData}, ${hasDetailData},
'${JSON.stringify(data.stat)}', '${JSON.stringify(data.detail)}', '${timeNow}')
ON CONFLICT(uid,id) DO UPDATE
SET startTime = '${data.startTime}',
endTime = '${data.endTime}',
hasData = ${hasData},
hasDetailData = ${hasDetailData},
stat = '${JSON.stringify(data.stat)}',
detail = '${JSON.stringify(data.detail)}',
updated = '${timeNow}'
`;
}
/**
* @description 获取所有有数据的UID
* @since Beta v0.6.3
* @returns {Promise<void>}
*/
async function getAllUid(): Promise<Array<string>> {
const db = await TGSqlite.getDB();
type resType = Array<{ uid: string }>;
const res = await db.select<resType>("SELECT DISTINCT uid FROM RoleCombat;");
return res.map((i) => i.uid);
}
/**
* @description 获取剧诗数据
* @since Beta v0.6.3
* @param {string} uid - 游戏UID
* @returns {Promise<TGApp.Sqlite.Abyss.SingleTable[]>}
*/
async function getCombat(uid?: string): Promise<TGApp.Sqlite.Combat.SingleTable[]> {
const db = await TGSqlite.getDB();
let resR: TGApp.Sqlite.Combat.RawTable[];
if (uid === undefined) {
resR = await db.select<TGApp.Sqlite.Combat.RawTable[]>(
"SELECT * FROM RoleCombat order by id DESC;",
);
} else {
resR = await db.select<TGApp.Sqlite.Combat.RawTable[]>(
"SELECT * FROM RoleCombat WHERE uid = ? order by id DESC;",
[uid],
);
}
const res: TGApp.Sqlite.Combat.SingleTable[] = [];
for (const raw of resR) {
res.push({
uid: raw.uid,
detail: JSON.parse(raw.detail),
endTime: raw.endTime,
hasData: raw.hasData === 1,
hasDetailData: raw.hasDetailData === 1,
id: raw.id,
startTime: raw.startTime,
stat: JSON.parse(raw.stat),
updated: raw.updated,
});
}
return res;
}
/**
* @description 保存剧诗数据
* @since Beta v0.6.3
* @param {string} uid - 游戏UID
* @param {TGApp.Game.Abyss.FullData} data - 深渊数据
* @returns {Promise<void>}
*/
async function saveCombat(uid: string, data: TGApp.Game.Combat.Combat): Promise<void> {
const db = await TGSqlite.getDB();
await db.execute(getInsertSql(transUserCombat(data), uid));
}
/**
* @description 删除指定UID存档的数据
* @since Beta v0.6.3
* @param {string} uid - 游戏UID
* @returns {Promise<void>}
*/
async function delCombat(uid: string): Promise<void> {
const db = await TGSqlite.getDB();
await db.execute("DELETE FROM RoleCombat WHERE uid = ?;", [uid]);
}
/**
* @description 备份剧诗数据
* @since Beta v0.6.3
* @param {string} dir - 备份目录
* @returns {Promise<void>}
*/
async function backupCombat(dir: string): Promise<void> {
if (!(await exists(dir))) {
await mkdir(dir, { recursive: true });
await TGLogger.Warn(`未检测到备份目录,已创建`);
}
const data = await getCombat();
await writeTextFile(`${dir}${path.sep()}combat.json`, JSON.stringify(data));
}
/**
* @description 恢复剧诗数据
* @since Beta v0.6.3
* @param {string} dir - 备份文件目录
* @returns {Promise<boolean>}
*/
async function restoreCombat(dir: string): Promise<boolean> {
const filePath = `${dir}${path.sep()}combat.json`;
if (!(await exists(filePath))) return false;
try {
const data: TGApp.Sqlite.Combat.SingleTable[] = JSON.parse(await readTextFile(filePath));
const db = await TGSqlite.getDB();
for (const abyss of data) {
await db.execute(getInsertSql(abyss));
}
return true;
} catch (e) {
await TGLogger.Error(`恢复剧诗数据失败${filePath}`);
await TGLogger.Error(`${e}`);
return false;
}
}
const TSUserCombat = {
getAllUid,
getCombat,
saveCombat,
delCombat,
backupCombat,
restoreCombat,
};
export default TSUserCombat;

View File

@@ -1,6 +1,6 @@
-- @file plugins/Sqlite/sql/createTable.sql -- @file plugins/Sqlite/sql/createTable.sql
-- @brief sqlite数据库创建表语句 -- @brief sqlite数据库创建表语句
-- @since Beta v0.6.1 -- @since Beta v0.6.3
-- @brief 创建成就数据表 -- @brief 创建成就数据表
create table if not exists Achievements create table if not exists Achievements
@@ -71,6 +71,21 @@ create table if not exists SpiralAbyss
primary key (uid, id) primary key (uid, id)
); );
-- @brief 创建幻想真境剧诗数据表
create table if not exists RoleCombat
(
uid text,
id integer,
startTime text,
endTime text,
hasData boolean,
hasDetailData boolean,
stat text,
detail text,
updated text,
primary key (uid, id)
);
-- @brief 创建战绩数据表 -- @brief 创建战绩数据表
create table if not exists UserRecord create table if not exists UserRecord
( (

View File

@@ -0,0 +1,27 @@
/**
* @file plugins/Sqlite/utils/transUserCombat.ts
* @description Sqlite 数据转换-幻想真境剧诗
* @since Beta v0.6.3
*/
import { timestampToDate } from "../../../utils/toolFunc.js";
/**
* @description 将通过 api 获取到的数据转换为数据库中的数据
* @since Beta v0.6.3
* @param {TGApp.Game.Combat.Combat} data - 剧诗数据
* @returns {TGApp.Sqlite.Combat.SingleTable} 转换后端数据
*/
export function transUserCombat(data: TGApp.Game.Combat.Combat): TGApp.Sqlite.Combat.SingleTable {
return {
uid: "",
detail: data.detail,
endTime: timestampToDate(Number(data.schedule.end_time) * 1000),
hasData: data.has_data,
hasDetailData: data.has_detail_data,
id: data.schedule.schedule_id,
startTime: timestampToDate(Number(data.schedule.start_time) * 1000),
stat: data.stat,
updated: timestampToDate(new Date().getTime()),
};
}

View File

@@ -1,7 +1,7 @@
/** /**
* @file router modules user.ts * @file router modules user.ts
* @description user 路由模块 * @description user 路由模块
* @since Beta v0.3.3 * @since Beta v0.6.3
*/ */
const userRoutes = [ const userRoutes = [
@@ -10,6 +10,11 @@ const userRoutes = [
name: "深渊记录", name: "深渊记录",
component: async () => await import("../../pages/User/Abyss.vue"), component: async () => await import("../../pages/User/Abyss.vue"),
}, },
{
path: "/user/combat",
name: "真境剧诗",
component: async () => await import("../../pages/User/Combat.vue"),
},
{ {
path: "/user/characters", path: "/user/characters",
name: "我的角色", name: "我的角色",

View File

@@ -43,7 +43,7 @@ declare namespace TGApp.Game.Combat {
* @interface Avatar * @interface Avatar
* @since Beta v0.6.3 * @since Beta v0.6.3
* @property {number} avatar_id 角色id * @property {number} avatar_id 角色id
* @property {number} avatar_type 角色武器类型 // todo * @property {number} avatar_type 角色类型 // 0-自己角色1-试用角色2-助演角色
* @property {string} name 角色名称 * @property {string} name 角色名称
* @property {string} element 角色元素 // todo Dendro * @property {string} element 角色元素 // todo Dendro
* @property {string} image 角色图像 * @property {string} image 角色图像

61
src/types/Sqlite/Combat.d.ts vendored Normal file
View File

@@ -0,0 +1,61 @@
/**
* @file types/Sqlite/Combat.d.ts
* @description 幻想真境剧诗类型定义文件
* @since Beta v0.6.3
*/
declare namespace TGApp.Sqlite.Combat {
/**
* @description 数据库-幻想真境剧诗表
* @since Beta v0.6.3
* @interface SingleTable
* @property {string} uid - 用户 UID
* @property {number} id - 剧诗 ID
* @property {string} startTime - 开始时间
* @property {string} endTime - 结束时间
* @property {boolean} hasData - 是否有数据
* @property {boolean} hasDetailData - 是否有详细数据
* @property {TGApp.Game.Combat.Stat} stat - 概况
* @property {TGApp.Game.Combat.Detail} detail - 详情
* @property {string} updated - 更新时间
* @return SingleTable
*/
interface RawTable {
uid: string;
id: number;
startTime: string;
endTime: string;
hasData: 0 | 1;
hasDetailData: 0 | 1;
stat: string;
detail: string;
updated: string;
}
/**
* @description 数据库-幻想真境剧诗表
* @since Beta v0.6.3
* @interface SingleTable
* @property {string} uid - 用户 UID
* @property {number} id - 剧诗 ID
* @property {string} startTime - 开始时间
* @property {string} endTime - 结束时间
* @property {boolean} hasData - 是否有数据
* @property {boolean} hasDetailData - 是否有详细数据
* @property {TGApp.Game.Combat.Stat} stat - 概况
* @property {TGApp.Game.Combat.Detail} detail - 详情
* @property {string} updated - 更新时间
* @return SingleTable
*/
interface SingleTable {
uid: string;
id: number;
startTime: string;
endTime: string;
hasData: boolean;
hasDetailData: boolean;
stat: TGApp.Game.Combat.Stat;
detail: TGApp.Game.Combat.Detail;
updated: string;
}
}

View File

@@ -1,7 +1,7 @@
/** /**
* @file utils/dataBS.ts * @file utils/dataBS.ts
* @description 用户数据的备份、恢复、迁移 * @description 用户数据的备份、恢复、迁移
* @since Beta v0.6.0 * @since Beta v0.6.3
*/ */
import { exists, mkdir } from "@tauri-apps/plugin-fs"; import { exists, mkdir } from "@tauri-apps/plugin-fs";
@@ -9,13 +9,14 @@ import showSnackbar from "../components/func/snackbar.js";
import TSUserAbyss from "../plugins/Sqlite/modules/userAbyss.js"; import TSUserAbyss from "../plugins/Sqlite/modules/userAbyss.js";
import TSUserAccount from "../plugins/Sqlite/modules/userAccount.js"; import TSUserAccount from "../plugins/Sqlite/modules/userAccount.js";
import TSUserAchi from "../plugins/Sqlite/modules/userAchi.js"; import TSUserAchi from "../plugins/Sqlite/modules/userAchi.js";
import TSUserCombat from "../plugins/Sqlite/modules/userCombat.js";
import TSUserGacha from "../plugins/Sqlite/modules/userGacha.js"; import TSUserGacha from "../plugins/Sqlite/modules/userGacha.js";
import TGLogger from "./TGLogger.js"; import TGLogger from "./TGLogger.js";
/** /**
* @description 备份用户数据 * @description 备份用户数据
* @since Beta v0.6.0 * @since Beta v0.6.3
* @param {string} dir 备份目录路径 * @param {string} dir 备份目录路径
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
@@ -27,12 +28,13 @@ export async function backUpUserData(dir: string): Promise<void> {
await TSUserAchi.backupUiaf(dir); await TSUserAchi.backupUiaf(dir);
await TSUserAccount.account.backup(dir); await TSUserAccount.account.backup(dir);
await TSUserAbyss.backupAbyss(dir); await TSUserAbyss.backupAbyss(dir);
await TSUserCombat.backupCombat(dir);
await TSUserGacha.backUpUigf(dir); await TSUserGacha.backUpUigf(dir);
} }
/** /**
* @description 恢复用户数据 * @description 恢复用户数据
* @since Beta v0.6.0 * @since Beta v0.6.3
* @param {string} dir 备份目录路径 * @param {string} dir 备份目录路径
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
@@ -57,6 +59,11 @@ export async function restoreUserData(dir: string): Promise<void> {
showSnackbar({ text: "深渊数据恢复失败", color: "error" }); showSnackbar({ text: "深渊数据恢复失败", color: "error" });
errNum++; errNum++;
} }
const restoreCombat = await TSUserCombat.restoreCombat(dir);
if (!restoreCombat) {
showSnackbar({ text: "真境剧诗数据恢复失败", color: "error" });
errNum++;
}
const restoreGacha = await TSUserGacha.restoreUigf(dir); const restoreGacha = await TSUserGacha.restoreUigf(dir);
if (!restoreGacha) { if (!restoreGacha) {
showSnackbar({ text: "祈愿数据恢复失败", color: "error" }); showSnackbar({ text: "祈愿数据恢复失败", color: "error" });

View File

@@ -1,7 +1,7 @@
/** /**
* @file web/request/TGRequest.ts * @file web/request/TGRequest.ts
* @description 应用用到的请求函数 * @description 应用用到的请求函数
* @since Beta v0.6.2 * @since Beta v0.6.3
*/ */
import { genAuthkey, genAuthkey2 } from "./genAuthkey.js"; import { genAuthkey, genAuthkey2 } from "./genAuthkey.js";
@@ -17,6 +17,7 @@ import { getGachaLog } from "./getGachaLog.js";
import { getGameAccountsByCookie, getGameAccountsBySToken } from "./getGameAccounts.js"; import { getGameAccountsByCookie, getGameAccountsBySToken } from "./getGameAccounts.js";
import { getGameRecord } from "./getGameRecord.js"; import { getGameRecord } from "./getGameRecord.js";
import { getLTokenBySToken } from "./getLToken.js"; import { getLTokenBySToken } from "./getLToken.js";
import { getRoleCombat } from "./getRoleCombat.js";
import { getStokenByGameToken, getTokenBySToken } from "./getStoken.js"; import { getStokenByGameToken, getTokenBySToken } from "./getStoken.js";
import { getUserCollect } from "./getUserCollect.js"; import { getUserCollect } from "./getUserCollect.js";
import { getUserInfoByCookie } from "./getUserInfo.js"; import { getUserInfoByCookie } from "./getUserInfo.js";
@@ -45,6 +46,7 @@ const TGRequest = {
getAvatarIndex, getAvatarIndex,
getAvatarList, getAvatarList,
getAvatarDetail, getAvatarDetail,
getCombat: getRoleCombat,
}, },
bySToken: { bySToken: {
update: getTokenBySToken, update: getTokenBySToken,