mirror of
https://github.com/BTMuli/TeyvatGuide.git
synced 2026-03-19 04:29:45 +08:00
232
src/components/userChallenge/tuc-challenge-item.vue
Normal file
232
src/components/userChallenge/tuc-challenge-item.vue
Normal 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>
|
||||
95
src/components/userChallenge/tuc-monster-tag.vue
Normal file
95
src/components/userChallenge/tuc-monster-tag.vue
Normal 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>
|
||||
84
src/components/userChallenge/tuc-overview.vue
Normal file
84
src/components/userChallenge/tuc-overview.vue
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
8
src/types/Game/Challenge.d.ts
vendored
8
src/types/Game/Challenge.d.ts
vendored
@@ -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 单次挑战怪物数据
|
||||
|
||||
Reference in New Issue
Block a user