🌱 幽境危战页面

#157
This commit is contained in:
BTMuli
2025-06-25 21:01:31 +08:00
parent d3ab4fb6dc
commit 4022df08ce
12 changed files with 481 additions and 19 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@@ -50,12 +50,33 @@
<img src="/source/UI/userAvatar.webp" alt="characters" class="side-icon" />
</template>
</v-list-item>
<v-list-item :title.attr="'深渊记录'" :link="true" href="/user/abyss">
<template #title>深渊记录</template>
<template #prepend>
<img src="/source/UI/userAbyss.webp" alt="abyss" class="side-icon" />
<v-menu :open-on-click="true" location="end" :offset="[8, 0]">
<template #activator="{ props }">
<v-list-item :title.attr="'高难挑战'" v-bind="props">
<template #title>高难挑战</template>
<template #prepend>
<img src="/source/UI/userAbyssLab.webp" alt="abyssLab" class="side-icon" />
</template>
</v-list-item>
</template>
</v-list-item>
<v-list class="side-list-menu sub" density="compact" :nav="true">
<v-list-item class="side-item-menu" title="深境螺旋" :link="true" href="/user/abyss">
<template #prepend>
<img src="/source/UI/userAbyss.webp" alt="abyss" class="side-icon-menu" />
</template>
</v-list-item>
<v-list-item class="side-item-menu" title="真境剧诗" :link="true" href="/user/weapons/my">
<template #prepend>
<img src="/source/UI/userCombat.webp" alt="combat" class="side-icon-menu" />
</template>
</v-list-item>
<v-list-item class="side-item-menu" title="幽境危战" :link="true" href="/user/challenge">
<template #prepend>
<img src="/source/UI/userChallenge.webp" alt="challenge" class="side-icon-menu" />
</template>
</v-list-item>
</v-list>
</v-menu>
<v-list-item :title.attr="'祈愿记录'" :link="true" href="/user/gacha">
<template #title>祈愿记录</template>
<template #prepend>

View File

@@ -0,0 +1,77 @@
<!-- 幽境危战赋光之人 -->
<template>
<div class="tuc-pop-item-comp" :title="props.avatar.name">
<div class="bg">
<img :src="bg" alt="Avatar" />
</div>
<div class="icon">
<TMiImg :src="icon" :alt="props.avatar.name" :ori="true" />
</div>
</div>
</template>
<script lang="ts" setup>
import TMiImg from "@comp/app/t-mi-img.vue";
import { computed } from "vue";
import { AppCharacterData } from "@/data/index.js";
type TucPopItemProps = { avatar: TGApp.Game.Challenge.PopularityItem };
const props = defineProps<TucPopItemProps>();
const avatarR = computed<TGApp.App.Character.WikiBriefInfo | undefined>(() => {
const find = AppCharacterData.find((i) => i.id === props.avatar.avatar_id);
if (find) return find;
return undefined;
});
const bg = computed<string>(() => {
if (avatarR.value) return `/icon/bg/${avatarR.value.star}-BGC.webp`;
return `/icon/bg/${props.avatar.rarity > 5 ? 5 : props.avatar.rarity}-BGC.webp`;
});
const icon = computed<string>(() => {
if (avatarR.value) return `/WIKI/character/${avatarR.value.id}.webp`;
return props.avatar.image;
});
</script>
<style lang="scss" scoped>
.tuc-pop-item-comp {
position: relative;
display: flex;
overflow: hidden;
width: 40px;
height: 40px;
align-items: flex-end;
justify-content: center;
border-radius: 50%;
&:hover {
filter: brightness(0.9);
}
}
.bg {
position: absolute;
z-index: 0;
overflow: hidden;
width: 100%;
height: 100%;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.icon {
position: relative;
z-index: 1;
width: 36px;
height: 36px;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
</style>

View File

@@ -13,15 +13,15 @@
label="游戏UID"
/>
<v-btn :rounded="true" class="ua-btn" @click="toCombat()">
<template #prepend>
<img src="/source/UI/userCombat.webp" alt="combat" />
</template>
<span>幻想真境剧诗</span>
<img src="/source/UI/userCombat.webp" alt="combat" />
<span>真境剧诗</span>
</v-btn>
<v-btn :rounded="true" class="ua-btn" @click="toChallenge()">
<img src="/source/UI/userChallenge.webp" alt="challenge" />
<span>幽境危战</span>
</v-btn>
<v-btn :rounded="true" class="ua-btn" @click="toWiki()">
<template #prepend>
<img src="/source/UI/wikiAbyss.webp" alt="wiki" />
</template>
<img src="/source/UI/wikiAbyss.webp" alt="wiki" />
<span>深渊数据库</span>
</v-btn>
</div>
@@ -148,7 +148,9 @@ watch(() => uidCur.value, loadAbyss);
async function toCombat(): Promise<void> {
await router.push({ name: "真境剧诗" });
}
async function toChallenge(): Promise<void> {
await router.push({ name: "幽境危战" });
}
async function toWiki(): Promise<void> {
await router.push({ name: "深渊数据库" });
}
@@ -387,6 +389,13 @@ async function deleteAbyss(): Promise<void> {
background: var(--tgc-btn-1);
color: var(--btn-text);
font-family: var(--font-text);
img {
width: 24px;
height: 24px;
margin-right: 4px;
object-fit: contain;
}
}
.dark .ua-btn {

View File

@@ -0,0 +1,259 @@
<!-- 幽境危战 -->
<template>
<v-app-bar>
<template #prepend>
<div class="ucp-top-prepend">
<img alt="icon" src="/source/UI/userChallenge.webp" />
<span>幽境危战</span>
<v-select
density="compact"
variant="outlined"
v-model="uidCur"
:items="uidList"
:hide-details="true"
label="游戏UID"
/>
<v-btn :rounded="true" class="ucp-btn" @click="toAbyss()">
<img src="/source/UI/userAbyss.webp" alt="abyss" />
<span>深境螺旋</span>
</v-btn>
<v-btn :rounded="true" class="ucp-btn" @click="toCombat()">
<img src="/source/UI/userCombat.webp" alt="abyss" />
<span>真境剧诗</span>
</v-btn>
<v-btn :rounded="true" class="ucp-btn" @click="loadWiki()">
<img src="/source/UI/wikiAbyss.webp" alt="abyss" />
<span>统计数据</span>
</v-btn>
</div>
</template>
<template #append>
<div class="ucp-top-append">
<v-select
:items="serverList"
v-model="server"
item-title="text"
item-value="value"
label="服务器"
width="200px"
density="compact"
:disabled="isReq"
/>
</div>
</template>
<template #extension>
<div class="ucp-top-extension">
<div class="act-list">
<v-btn
class="ucp-btn"
@click="shareChallenge()"
:disabled="localChallenge.length === 0"
prepend-icon="mdi-share"
>
分享
</v-btn>
<v-btn class="ucp-btn" @click="refreshChallenge()" prepend-icon="mdi-refresh">刷新</v-btn>
<v-btn class="ucp-btn" @click="uploadChallenge()" prepend-icon="mdi-cloud-upload">
上传
</v-btn>
<v-btn class="ucp-btn" @click="deleteChallenge()" prepend-icon="mdi-delete">删除</v-btn>
</div>
<div class="pop-list">
<TucPopItem v-for="avatar in popList" :key="avatar.avatar_id" :avatar />
<v-btn
:loading="reqPop"
size="36"
class="pop-btn"
@click="refreshPopList"
icon="mdi-refresh"
:disabled="reqPop"
/>
</div>
</div>
</template>
</v-app-bar>
</template>
<script lang="ts" setup>
import showLoading from "@comp/func/loading.js";
import showSnackbar from "@comp/func/snackbar.js";
import TucPopItem from "@comp/userChallenge/tuc-pop-item.vue";
import { GameServerEnum, getGameServerDesc } from "@enum/game.js";
import recordReq from "@req/recordReq.js";
import TGLogger from "@utils/TGLogger.js";
import { onMounted, ref, shallowRef, watch } from "vue";
import { useRouter } from "vue-router";
type SelectItem<T = string> = { text: string; value: T };
const serverList: ReadonlyArray<SelectItem<TGApp.Game.Base.ServerTypeEnum>> = [
GameServerEnum.CN_GF01,
GameServerEnum.CN_QD01,
// TODO: 目前不支持国际服务器
// GameServerEnum.OS_ASIA,
// GameServerEnum.OS_EURO,
// GameServerEnum.OS_USA,
// GameServerEnum.OS_CHT,
].map((i) => ({ text: getGameServerDesc(i), value: i }));
const router = useRouter();
const isReq = ref<boolean>(false);
const uidCur = ref<string>();
const uidList = shallowRef<Array<string>>();
const localChallenge = shallowRef<unknown[]>([]);
const server = ref<TGApp.Game.Base.ServerTypeEnum>(GameServerEnum.CN_GF01);
const reqPop = ref<boolean>(false);
const popList = shallowRef<Array<TGApp.Game.Challenge.PopularityItem>>([]);
onMounted(async () => {
await refreshPopList();
// TODO: 获取数据库数据
});
watch(
() => server.value,
async () => {
const name = getGameServerDesc(server.value);
await TGLogger.Info(`[UserChallenge][watch][server] 切换服务器: ${name}`);
await refreshPopList();
},
);
async function toAbyss(): Promise<void> {
await router.push({ name: "深境螺旋" });
}
async function toCombat(): Promise<void> {
await router.push({ name: "真境剧诗" });
}
async function loadWiki(): Promise<void> {
showSnackbar.warn("暂未接入胡桃数据库");
}
async function shareChallenge(): Promise<void> {
// TODO: 实现分享功能
}
async function refreshChallenge(): Promise<void> {
// TODO: 实现刷新挑战记录功能
}
async function uploadChallenge(): Promise<void> {
showSnackbar.warn("暂未接入胡桃数据库");
}
async function deleteChallenge(): Promise<void> {
if (localChallenge.value.length === 0) {
showSnackbar.warn("没有可删除的挑战记录");
return;
}
// TODO: 实现删除挑战记录功能
}
async function refreshPopList(): Promise<void> {
if (reqPop.value) return;
reqPop.value = true;
await showLoading.start("正在加载赋光之人列表", `服务器: ${getGameServerDesc(server.value)}`);
const resp = await recordReq.challenge.pop(server.value);
if (resp.retcode !== 0) {
reqPop.value = false;
showSnackbar.error(`[${resp.retcode}] ${resp.message}`);
await TGLogger.Error(
`[UserChallenge][RefreshPopList] Error: ${resp.retcode} - ${resp.message}`,
);
await showLoading.end();
return;
}
popList.value = resp.data.avatar_list;
await showLoading.end();
reqPop.value = false;
showSnackbar.success(`加载完成,共 ${popList.value.length} 位赋光之人`);
}
</script>
<style lang="scss" scoped>
.ucp-top-prepend {
position: relative;
display: flex;
box-sizing: border-box;
align-items: center;
justify-content: center;
padding: 8px;
column-gap: 8px;
img {
width: 32px;
height: 32px;
}
span {
font-family: var(--font-title);
font-size: 20px;
}
span :first-child {
color: var(--common-text-title);
}
}
.ucp-btn {
background: var(--tgc-btn-1);
color: var(--btn-text);
font-family: var(--font-text);
img {
width: 24px;
height: 24px;
margin-right: 4px;
object-fit: contain;
}
}
.dark .ucp-btn {
border: 1px solid var(--common-shadow-2);
}
.ucp-top-append {
position: relative;
width: fit-content;
height: 100%;
box-sizing: border-box;
padding: 8px;
}
.ucp-top-extension {
position: relative;
display: flex;
width: 100%;
box-sizing: border-box;
align-items: center;
justify-content: space-between;
padding: 0 8px;
}
.act-list {
position: relative;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.pop-list {
position: relative;
display: flex;
align-items: center;
justify-content: center;
column-gap: 4px;
}
.pop-btn {
background: var(--tgc-btn-1);
color: var(--btn-text);
font-family: var(--font-title);
}
.dark .pop-btn {
border: 1px solid var(--common-shadow-2);
}
</style>

View File

@@ -13,11 +13,15 @@
label="游戏UID"
/>
<v-btn :rounded="true" class="uc-btn" @click="toAbyss()">
<template #prepend><img src="/source/UI/userAbyss.webp" alt="abyss" /></template>
<img src="/source/UI/userAbyss.webp" alt="abyss" />
<span>深境螺旋</span>
</v-btn>
<v-btn :rounded="true" class="uc-btn" @click="toChallenge()">
<img src="/source/UI/userChallenge.webp" alt="challenge" />
<span>幽境危战</span>
</v-btn>
<v-btn :rounded="true" class="uc-btn" @click="loadWiki()">
<template #prepend><img src="/source/UI/wikiAbyss.webp" alt="abyss" /></template>
<img src="/source/UI/wikiAbyss.webp" alt="abyss" />
<span>统计数据</span>
</v-btn>
</div>
@@ -144,7 +148,10 @@ onMounted(async () => {
watch(() => uidCur.value, loadCombat);
async function toAbyss(): Promise<void> {
await router.push({ name: "深渊记录" });
await router.push({ name: "深境螺旋" });
}
async function toChallenge(): Promise<void> {
await router.push({ name: "幽境危战" });
}
async function loadCombat(): Promise<void> {
@@ -330,6 +337,13 @@ async function deleteCombat(): Promise<void> {
background: var(--tgc-btn-1);
color: var(--btn-text);
font-family: var(--font-text);
img {
width: 24px;
height: 24px;
margin-right: 4px;
object-fit: contain;
}
}
.dark .uc-btn {

View File

@@ -1,9 +1,10 @@
/**
* @file request/recordReq.ts
* @description TakumiRecordGenshinApi 相关请求
* @since Beta v0.6.3
* @since Beta v0.8.0
*/
import { GameServerEnum } from "@enum/game.js";
import { getRequestHeader } from "@utils/getRequestHeader.js";
import TGHttp from "@utils/TGHttp.js";
@@ -133,11 +134,46 @@ async function spiralAbyss(
return resp.data;
}
/**
* @description 获取赋光之人列表
* @since Beta v0.8.0
* @param {TGApp.Game.Base.ServerTypeEnum} server 区服
* @returns {Promise<TGApp.Game.Challenge.PopularityResp | TGApp.BBS.Response.Base>}
*/
async function hardChallengePopularity(
server: TGApp.Game.Base.ServerTypeEnum = GameServerEnum.CN_GF01,
): Promise<TGApp.Game.Challenge.PopularityResp | TGApp.BBS.Response.Base> {
let roleId: number | undefined;
switch (server) {
case GameServerEnum.CN_GF01:
roleId = 147991965; // CN
break;
case GameServerEnum.CN_QD01:
roleId = 500299765; // QD
break;
default:
return <TGApp.BBS.Response.Base>{
retcode: -1,
message: "不支持的服务器",
data: null,
};
}
const resp = await TGHttp<TGApp.Game.Challenge.PopularityResp | TGApp.BBS.Response.Base>(
`${trgAbu}hard_challenge/popularity`,
{ method: "GET", query: { server, role_id: roleId } },
);
if (resp.retcode !== 0) return <TGApp.BBS.Response.Base>resp;
return resp;
}
const recordReq = {
index: index,
character: { list: characterList, detail: characterDetail },
roleCombat: roleCombat,
spiralAbyss: spiralAbyss,
challenge: {
pop: hardChallengePopularity,
},
};
export default recordReq;

View File

@@ -1,14 +1,14 @@
/**
* @file router modules user.ts
* @description user 路由模块
* @since Beta v0.7.0
* @since Beta v0.8.0
*/
import type { RouteRecordRaw } from "vue-router";
const userRoutes = (<const>[
{
path: "/user/abyss",
name: "深渊记录",
name: "深境螺旋",
component: async () => await import("@/pages/User/Abyss.vue"),
},
{
@@ -16,6 +16,11 @@ const userRoutes = (<const>[
name: "真境剧诗",
component: async () => await import("@/pages/User/Combat.vue"),
},
{
path: "/user/challenge",
name: "幽境危战",
component: async () => await import("@/pages/User/Challenge.vue"),
},
{
path: "/user/characters",
name: "我的角色",

41
src/types/Game/Challenge.d.ts vendored Normal file
View File

@@ -0,0 +1,41 @@
/**
* @file types/Game/Challenge.d.ts
* @description 幽境危战相关类型定义文件
* @since Beta v0.8.0
*/
declare namespace TGApp.Game.Challenge {
/**
* @description 幽境危战赋光之人列表返回响应
* @since Beta v0.8.0
* @interface PopularityResp
* @extends TGApp.BBS.Response.BaseWithData<PopularityRes>
*/
type PopularityResp = TGApp.BBS.Response.BaseWithData<PopularityRes>;
/**
* @description 幽境危战赋光之人列表数据
* @since Beta v0.8.0
* @interface PopularityRes
* @property {Array<PopularityItem>} avatar_list - 赋光之人列表
*/
type PopularityRes = { avatar_list: Array<PopularityItem> };
/**
* @description 幽境危战赋光之人列表项
* @since Beta v0.8.0
* @interface PopularityItem
* @property {number} avatar_id - 角色 ID
* @property {string} name - 角色名称
* @property {string} element - 角色元素
* @property {string} image - 角色头像图片 URL
* @property {number} rarity - 角色稀有度
*/
type PopularityItem = {
avatar_id: number;
name: string;
element: string;
image: string;
rarity: number;
};
}