💄 基本完成危战UI

#157
This commit is contained in:
BTMuli
2025-07-01 19:05:05 +08:00
parent bb092c0d8f
commit a4ebf47b56
5 changed files with 458 additions and 9 deletions

View File

@@ -0,0 +1,232 @@
<!-- 幽境危战单个怪物挑战 -->
<template>
<div class="tuc-challenge-item-comp" @click="console.log(props.data)">
<div class="top-title">
<div class="name">{{ props.data.name }} Lv.{{ props.data.monster.level }}</div>
<div class="append">
<span>战斗用时</span>
<span>{{ props.data.second }}</span>
<span></span>
</div>
</div>
<div class="main-box">
<div class="left-info">
<div class="team-box">
<TItemBox
:model-value="getTeamBox(avatar)"
v-for="(avatar, idx) in props.data.teams"
:key="idx"
/>
</div>
<div class="best-dps">
<div class="best-dps-item" v-for="(avatar, idx) in props.data.best_avatar" :key="idx">
<TMiImg :src="avatar.side_icon" :ori="true" :alt="`${avatar.avatar_id}`" />
<span>{{ avatar.type === 1 ? "最强一击" : "最高总伤害" }}</span>
<span>{{ avatar.dps }}</span>
</div>
</div>
</div>
<div class="right-monster">
<div class="icon">
<TMiImg :src="props.data.monster.icon" :alt="props.data.name" :ori="true" />
</div>
<div class="tags">
<div class="tag" v-for="(tag, idx) in props.data.monster.tags" :key="idx">
<TucMonsterTag :data="tag" />
</div>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import TItemBox, { type TItemBoxData } from "@comp/app/t-itemBox.vue";
import TMiImg from "@comp/app/t-mi-img.vue";
import TucMonsterTag from "@comp/userChallenge/tuc-monster-tag.vue";
import { getZhElement } from "@utils/toolFunc.js";
import { AppCharacterData } from "@/data/index.js";
type TucChallengeItemProps = { data: TGApp.Game.Challenge.ChallengeList };
const props = defineProps<TucChallengeItemProps>();
function getTeamBox(avatar: TGApp.Game.Challenge.ChallengeTeam): TItemBoxData {
const find = AppCharacterData.find((i) => i.id === avatar.avatar_id);
if (!find) {
return {
bg: `/icon/bg/${avatar.rarity}-BGC.webp`,
clickable: false,
icon: avatar.image,
lt: `/icon/element/${getZhElement(avatar.element)}元素.webp`,
ltSize: "20px",
rt: avatar.rank.toString(),
rtSize: "20px",
size: "80px",
height: "80px",
display: "inner",
innerText: avatar.name,
innerHeight: 20,
};
}
return {
bg: `/icon/bg/${find.star}-BGC.webp`,
clickable: false,
icon: `/WIKI/character/${find.id}.webp`,
lt: `/icon/element/${find.element}元素.webp`,
ltSize: "20px",
rt: avatar.rank.toString(),
rtSize: "20px",
size: "80px",
height: "80px",
display: "inner",
innerText: find.name,
innerHeight: 20,
innerIcon: `/icon/weapon/${find.weapon}.webp`,
};
}
</script>
<style lang="scss" scoped>
.tuc-challenge-item-comp {
position: relative;
display: flex;
width: 100%;
box-sizing: border-box;
flex-direction: column;
align-items: flex-start;
justify-content: center;
padding: 8px;
border: 1px solid var(--common-shadow-1);
border-radius: 4px;
background: var(--box-bg-1);
color: var(--box-text-1);
row-gap: 12px;
}
.top-title {
position: relative;
display: flex;
width: 100%;
align-items: center;
justify-content: space-between;
.name {
font-family: var(--font-title);
font-size: 16px;
}
.append {
display: flex;
align-items: center;
color: var(--box-text-2);
font-family: var(--font-title);
font-size: 14px;
gap: 4px;
span {
color: var(--box-text-2);
font-size: 14px;
&:nth-child(2) {
color: var(--tgc-yellow-1);
}
}
}
}
.main-box {
position: relative;
display: flex;
width: 100%;
height: 120px;
align-items: flex-start;
justify-content: space-between;
}
.left-info {
position: relative;
display: flex;
height: 100%;
flex-direction: column;
align-items: flex-start;
justify-content: space-between;
}
.team-box {
position: relative;
display: flex;
width: 100%;
height: 80px;
align-items: center;
justify-content: center;
gap: 8px;
}
.best-dps {
position: relative;
display: flex;
align-items: center;
justify-content: center;
column-gap: 16px;
}
.best-dps-item {
position: relative;
display: flex;
align-items: center;
justify-content: center;
border-radius: 20px;
background: linear-gradient(to right, var(--box-bg-3), var(--box-bg-1));
color: var(--box-text-2);
column-gap: 4px;
font-family: var(--font-title);
img {
width: 24px;
height: 24px;
object-fit: contain;
transform: translateY(-4px);
}
span {
font-size: 14px;
&:last-child {
color: var(--tgc-yellow-1);
}
}
}
.dark .best-dps-item {
background: linear-gradient(to right, var(--box-bg-2) 100px, var(--box-bg-1));
}
.right-monster {
position: relative;
display: flex;
flex-direction: column;
align-items: flex-end;
justify-content: space-between;
.icon {
position: relative;
width: 96px;
height: 96px;
flex-shrink: 0;
img {
width: 100%;
height: 100%;
object-fit: contain;
}
}
.tags {
position: relative;
display: flex;
align-items: center;
justify-content: center;
column-gap: 8px;
}
}
</style>

View File

@@ -0,0 +1,95 @@
<!-- 怪物Tag -->
<template>
<div class="tuc-monster-tag-comp" :class="`buff-${props.data.type}`">
<template v-for="(descItem, idx) in desc" :key="idx">
<TMiImg
v-if="descItem.type === 'element'"
:src="`/icon/element/${getElement(descItem.value)}元素.webp`"
:alt="getElement(descItem.value)"
/>
<span v-else>{{ descItem.value }}</span>
</template>
</div>
</template>
<script lang="ts" setup>
import TMiImg from "@comp/app/t-mi-img.vue";
type TucMonsterTagProps = { data: TGApp.Game.Challenge.MonsterTag };
type MonsterDesc = { type: "element" | "text"; value: string };
const props = defineProps<TucMonsterTagProps>();
const desc: Array<MonsterDesc> = parseDesc(props.data.desc);
function getElement(value: string): string {
switch (value) {
case "11001":
return "冰";
case "11002":
return "水";
case "11003":
return "火";
// TODO: 待确认
case "11004":
return "雷";
// TODO: 待确认
case "11005":
return "风";
// TODO: 待确认
case "11006":
return "岩";
case "11007":
return "草";
default:
return value;
}
}
function parseDesc(desc: string): Array<MonsterDesc> {
// {SPRITE_PRESET#11003}元素优势 => [{type: "element", value: "11003"}, {type: "text", value: "元素优势"}]
const regex = /{SPRITE_PRESET#(\d+)}([^{}]*)/g;
const result: Array<MonsterDesc> = [];
let match;
while ((match = regex.exec(desc)) !== null) {
if (match[1]) {
result.push({ type: "element", value: match[1] });
}
if (match[2]) {
result.push({ type: "text", value: match[2].trim() });
}
}
return result;
}
</script>
<style lang="scss" scoped>
@use "@styles/github.styles.scss" as github-styles;
.tuc-monster-tag-comp {
position: relative;
display: flex;
box-sizing: border-box;
align-items: center;
justify-content: center;
padding: 4px 8px;
border-radius: 16px;
column-gap: 2px;
img {
width: 16px;
height: 16px;
}
span {
color: var(--box-text-1);
font-family: var(--font-title);
font-size: 12px;
}
&.buff-0 {
@include github-styles.github-tag-dark-gen(#e06c75);
}
&.buff-1 {
@include github-styles.github-tag-dark-gen(#98c379);
}
}
</style>

View File

@@ -0,0 +1,84 @@
<!-- 幽境危战单人/联机数据总览 -->
<template>
<div class="tuc-overview-comp" @click="onClick()">
<TSubline>{{ props.title }}{{ props.data.has_data ? "" : " (无数据) " }}</TSubline>
<div class="toc-best" v-if="props.data.best">
<div class="label">
<span>最佳记录</span>
</div>
<div class="append" :title="getDiffTitle(props.data.best)">
<span>{{ getDiffTitle(props.data.best) }}</span>
<span>{{ props.data.best.second }}s</span>
</div>
</div>
<TucChallengeItem
v-for="(challenge, idx) in props.data.challenge"
:key="idx"
:data="challenge"
/>
</div>
</template>
<script lang="ts" setup>
import TSubline from "@comp/app/t-subline.vue";
import TucChallengeItem from "@comp/userChallenge/tuc-challenge-item.vue";
type TucOverviewProps = {
title: string;
data: TGApp.Game.Challenge.Challenge;
};
const props = defineProps<TucOverviewProps>();
function onClick(): void {
console.log(props.data);
}
function getDiffTitle(best: TGApp.Game.Challenge.ChallengeBest): string {
switch (best.difficulty) {
case 1:
return "普通";
case 2:
return "进阶";
case 3:
return "困难";
case 4:
return "险恶";
case 5:
return "无畏";
case 6:
return "绝境";
default:
return `难度${best.difficulty}`;
}
}
</script>
<style lang="scss" scoped>
.tuc-overview-comp {
position: relative;
display: flex;
width: 100%;
flex-direction: column;
align-items: flex-start;
justify-content: center;
row-gap: 12px;
}
.toc-best {
display: flex;
width: 100%;
align-items: center;
justify-content: space-between;
color: var(--box-text-1);
.label {
font-family: var(--font-title);
}
.append {
display: flex;
align-items: center;
color: var(--box-text-2);
gap: 4px;
}
}
</style>

View File

@@ -95,7 +95,16 @@
class="ucb-window-item"
>
<div :id="`user-challenge-${item.id}`" class="ucb-window-box">
{{ JSON.stringify(item, null, 2) }}
<div class="ucw-top">
<div class="ucw-title">
<span>{{ item.name }}</span>
<span>{{ item.startTime }} ~ {{ item.endTime }}</span>
<span>更新于 {{ item.updated }}</span>
</div>
<div class="ucw-share">幽境危战 | Render by TeyvatGuide v{{ version }}</div>
</div>
<TucOverview title="单人模式" :data="item.single" />
<TucOverview title="联机模式" :data="item.mp" v-if="item.mp.has_data" />
</div>
</v-window-item>
</v-window>
@@ -109,6 +118,7 @@
import showDialog from "@comp/func/dialog.js";
import showLoading from "@comp/func/loading.js";
import showSnackbar from "@comp/func/snackbar.js";
import TucOverview from "@comp/userChallenge/tuc-overview.vue";
import TucPopItem from "@comp/userChallenge/tuc-pop-item.vue";
import { GameServerEnum, getGameServerDesc } from "@enum/game.js";
import recordReq from "@req/recordReq.js";
@@ -150,8 +160,8 @@ const popList = shallowRef<Array<TGApp.Game.Challenge.PopularityItem>>([]);
onMounted(async () => {
version.value = await getVersion();
await TGLogger.Info("[UserCombat][onMounted] 打开幽境危战页面");
await refreshPopList();
await reloadChallenge();
await refreshPopList(false);
});
watch(
@@ -301,10 +311,12 @@ async function deleteChallenge(): Promise<void> {
await reloadChallenge();
}
async function refreshPopList(): Promise<void> {
async function refreshPopList(hint: boolean = true): Promise<void> {
if (reqPop.value) return;
reqPop.value = true;
await showLoading.start("正在加载赋光之人列表", `服务器: ${getGameServerDesc(server.value)}`);
if (hint) {
await showLoading.start("正在加载赋光之人列表", `服务器: ${getGameServerDesc(server.value)}`);
}
const resp = await recordReq.challenge.pop(server.value);
if (resp.retcode !== 0) {
reqPop.value = false;
@@ -461,7 +473,7 @@ async function refreshPopList(): Promise<void> {
.ucb-window-box {
display: flex;
flex-direction: column;
gap: 8px;
gap: 16px;
}
.ucb-empty {
@@ -479,4 +491,30 @@ async function refreshPopList(): Promise<void> {
color: var(--common-text-title);
font-family: var(--font-title);
}
.ucw-top {
display: flex;
width: 100%;
align-items: flex-start;
justify-content: space-between;
}
.ucw-title {
display: flex;
flex-direction: column;
column-gap: 4px;
font-size: 12px;
:first-child {
color: var(--common-text-title);
font-family: var(--font-title);
font-size: 20px;
}
}
.ucw-share {
z-index: -1;
font-size: 12px;
opacity: 0.8;
}
</style>

View File

@@ -149,14 +149,14 @@ declare namespace TGApp.Game.Challenge {
* @since Beta v0.8.0
* @interface ChallengeList
* @property {string} name - 怪物名称
* @property {number} seconds - 挑战用时(秒)
* @property {number} second - 挑战用时(秒)
* @property {Array<ChallengeTeam>} teams - 挑战队伍列表
* @property {Array<ChallengeAvatar>} best_avatar - 最佳角色列表
* @property {ChallengeMonster} monster - 挑战怪物数据
*/
type ChallengeList = {
name: string;
seconds: number;
second: number;
teams: Array<ChallengeTeam>;
best_avatar: Array<ChallengeAvatar>;
monster: ChallengeMonster;
@@ -191,9 +191,9 @@ declare namespace TGApp.Game.Challenge {
* @property {number} avatar_id - 角色 ID
* @property {string} side_icon - 角色侧边图标 URL
* @property {string} dps - 角色 DPS 数据
* @property {string} type - 1-最强一击2-最高总伤害
* @property {number} type - 1-最强一击2-最高总伤害
*/
type ChallengeAvatar = { avatar_id: number; side_icon: string; dps: string; type: string };
type ChallengeAvatar = { avatar_id: number; side_icon: string; dps: string; type: number };
/**
* @description 单次挑战怪物数据