Compare commits

..

14 Commits

Author SHA1 Message Date
BTMuli
a8bd4be8ea 🚀 v0.8.1 2025-09-11 13:38:07 +08:00
BTMuli
bd37e3e491 ♻️ 战绩世界探索数据结构调整 2025-09-11 13:30:58 +08:00
BTMuli
286c1e2459 💄 调整名片间距 2025-09-11 12:59:07 +08:00
BTMuli
b3b3eae57c 🐛 修复描述显示异常 2025-09-11 12:52:35 +08:00
BTMuli
3235545a02 🐛 修复爱诺天赋显示异常,增加是否解锁显示 2025-09-11 12:35:17 +08:00
BTMuli
ddbc382b8b 🐛 修复分享图生成异常 2025-09-10 15:09:53 +08:00
BTMuli
58e99467ae 💄 调整材料图鉴顶部样式,保持UI一致性 2025-09-10 14:57:01 +08:00
BTMuli
10b184950d 🍱 手动添加月神瞳数据&资源 2025-09-10 14:50:28 +08:00
BTMuli
17eb6cc001 🐛 修复战绩显示异常 2025-09-10 11:38:08 +08:00
BTMuli
ae68653938 🐛 隐藏未开始活动,修复未开始活动时间显示异常 2025-09-10 11:11:56 +08:00
BTMuli
aabb9776d4 🚸 微调签到奖励交互效果 2025-09-10 09:36:16 +08:00
BTMuli
fd47ebe7c1 🚸 优化名片Wiki样式 2025-09-10 00:30:19 +08:00
BTMuli
1a27dc5f02 💄 优化成就项UI 2025-09-10 00:13:15 +08:00
BTMuli
8062935b2c 💄 优化名片UI 2025-09-10 00:07:21 +08:00
28 changed files with 558 additions and 227 deletions

View File

@@ -2,12 +2,27 @@
Author: 目棃
Description: CHANGELOG
Date: 2025-09-09
Update: 2025-09-09
Update: 2025-09-11
---
> 本文档 [`Frontmatter`](https://github.com/BTMuli/MuCli#Frontmatter) 由 [MuCli](https://github.com/BTMuli/Mucli) 自动生成于 `2025-09-09 14:30:56`
>
> 更新于 `2025-09-09 15:18:01`
> 更新于 `2025-09-11 13:38:00`
## [0.8.1](https://github.com/BTMuli/TeyvatGuide/releases/v0.8.1) (2025-09-11)
- 🍱 添加月神瞳数据&资源
- ♻️ 战绩世界探索数据结构调整,增加数据显示
- 🐛 修复战绩显示异常
- 🐛 首页活动组件隐藏未开始活动,修复未开始活动时间显示异常
- 🐛 修复材料浮窗分享图生成异常
- 🐛 角色简略视图修复天赋显示异常,增加是否解锁显示
- 🐛 修复角色天赋/技能描述显示异常
- 🚸 调整名片Wiki页面支持按类型筛选
- 🚸 微调签到奖励交互效果
- 💄 优化名片UI
- 💄 优化成就项UI
- 💄 调整材料图鉴顶部样式保持UI一致性
## [0.8.0](https://github.com/BTMuli/TeyvatGuide/releases/v0.8.0) (2025-09-09)

View File

@@ -1,6 +1,6 @@
{
"name": "teyvatguide",
"version": "0.8.0",
"version": "0.8.1",
"description": "Game Tool for GenshinImpact player",
"private": true,
"packageManager": "pnpm@10.15.1",

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

2
src-tauri/Cargo.lock generated
View File

@@ -4,7 +4,7 @@ version = 4
[[package]]
name = "TeyvatGuide"
version = "0.8.0"
version = "0.8.1"
dependencies = [
"chrono",
"log",

View File

@@ -1,6 +1,6 @@
[package]
name = "TeyvatGuide"
version = "0.8.0"
version = "0.8.1"
description = "Game Tool for Genshin Impact player"
authors = ["BTMuli <bt-muli@outlook.com>"]
license = "MIT"

View File

@@ -2,7 +2,7 @@
"$schema": "https://schema.tauri.app/config/2",
"productName": "TeyvatGuide",
"identifier": "TeyvatGuide",
"version": "0.8.0",
"version": "0.8.1",
"build": {
"beforeDevCommand": "pnpm vite:dev",
"beforeBuildCommand": "pnpm vite:build",

View File

@@ -14,7 +14,8 @@
<span>{{ parseNameCard(props.data.desc) }}</span>
<span>获取途径{{ props.data.source }}</span>
</div>
<div class="ton-type" :title="`ID:${props.data.id}`">{{ props.data.type }}</div>
<TwnTypeTag :type="props.data.type" class="ton-type" />
<div class="ton-sign">ID:{{ props.data.id }} | TeyvatGuide v{{ version }}</div>
<v-btn
class="ton-share"
@click="shareNameCard"
@@ -32,8 +33,10 @@
</template>
<script setup lang="ts">
import showSnackbar from "@comp/func/snackbar.js";
import TwnTypeTag from "@comp/pageWiki/twn-type-tag.vue";
import { getVersion } from "@tauri-apps/api/app";
import { generateShareImg } from "@utils/TGShare.js";
import { ref } from "vue";
import { onMounted, ref } from "vue";
import TOverlay from "./t-overlay.vue";
@@ -42,6 +45,11 @@ type ToNameCardProps = { data?: TGApp.App.NameCard.Item };
const props = defineProps<ToNameCardProps>();
const visible = defineModel<boolean>();
const loading = ref<boolean>(false);
const version = ref<string>("");
onMounted(async () => {
version.value = await getVersion();
});
function parseNameCard(desc: string): string {
let array = [];
@@ -141,16 +149,6 @@ async function shareNameCard(): Promise<void> {
border-radius: 4px;
}
.ton-type {
position: absolute;
top: 10px;
left: 10px;
padding: 0 4px;
border: 1px solid var(--tgc-white-1);
border-radius: 4px;
color: var(--tgc-white-1);
}
.ton-content {
position: absolute;
right: 0;
@@ -167,27 +165,42 @@ async function shareNameCard(): Promise<void> {
backdrop-filter: blur(5px);
background: #00000040;
color: var(--tgc-white-1);
:first-child {
font-family: var(--font-title);
font-size: 20px;
text-shadow: 0 0 5px #000000cc;
}
:nth-child(2) {
border-bottom: 1px dotted var(--tgc-white-1);
text-shadow: 0 0 2px #000000cc;
white-space: pre-wrap;
}
:last-child {
opacity: 0.8;
text-shadow: 0 0 2px black;
}
}
.dark .ton-content {
background: #00000080;
}
.ton-content :first-child {
font-family: var(--font-title);
font-size: 20px;
text-shadow: 0 0 5px #000000cc;
.ton-type {
position: absolute;
top: 10px;
left: 10px;
font-size: 14px;
}
.ton-content :nth-child(2) {
border-bottom: 1px dotted var(--tgc-white-1);
text-shadow: 0 0 2px #000000cc;
white-space: pre-wrap;
}
.ton-content :last-child {
opacity: 0.8;
text-shadow: 0 0 2px black;
.ton-sign {
position: absolute;
top: 10px;
right: 10px;
color: var(--tgc-white-1);
font-size: 12px;
}
.ton-share {

View File

@@ -2,10 +2,16 @@
<div
class="top-nc-box"
@click="emit('selected', props.data)"
:class="{ grey: !props.finish }"
:class="props.finish ? '' : 'grey'"
:title.attr="props.data.name"
>
<v-list-item :title="props.data.name">
<v-list-item>
<template #title>
<div class="title">
<TwnTypeTag :type="props.data.type" />
<span>{{ props.data.name }}</span>
</div>
</template>
<template #subtitle>
<span class="desc" :title="props.data.desc">{{ props.data.desc }}</span>
</template>
@@ -16,14 +22,13 @@
</div>
</template>
<script lang="ts" setup>
import TwnTypeTag from "@comp/pageWiki/twn-type-tag.vue";
import { computed } from "vue";
type TopNameCardProps = { data: TGApp.App.NameCard.Item; finish?: boolean };
type TopNameCardEmits = (e: "selected", v: TGApp.App.NameCard.Item) => void;
const props = withDefaults(defineProps<TopNameCardProps>(), {
finish: true,
});
const props = withDefaults(defineProps<TopNameCardProps>(), { finish: true });
const emit = defineEmits<TopNameCardEmits>();
const bgImage = computed<string>(() => {
@@ -44,7 +49,6 @@ const bgImage = computed<string>(() => {
justify-content: flex-start;
border: 1px solid var(--common-shadow-1);
border-radius: 4px 50px 50px 4px;
margin-bottom: 8px;
background-color: var(--box-bg-1);
background-image: v-bind(bgImage); /* stylelint-disable-line value-keyword-case */
background-position: right;
@@ -72,6 +76,12 @@ const bgImage = computed<string>(() => {
aspect-ratio: 23 / 15;
}
.title {
display: flex;
align-items: center;
column-gap: 4px;
}
.desc {
text-shadow: 0 0 2px var(--common-shadow-t-8);
}

View File

@@ -2,6 +2,7 @@
<THomeCard :append="false">
<template #title>限时祈愿</template>
<template #default>
<!-- TODO: 当数量超过2时改为走轮播显示2个 -->
<div class="pool-grid">
<PhPoolCard v-for="(pool, idx) in pools" :key="idx" :pool="pool" />
</div>

View File

@@ -84,7 +84,9 @@ async function loadUserPosition(): Promise<void> {
await TGLogger.Error(`获取近期活动失败:[${resp.retcode}-${resp.message}`);
return;
}
userPos.value = [...resp.act_list, ...resp.fixed_act_list];
userPos.value = [...resp.act_list, ...resp.fixed_act_list].filter(
(i) => i.start_timestamp !== "0",
);
}
async function loadWikiPosition(): Promise<void> {

View File

@@ -68,11 +68,16 @@
</div>
</div>
<div class="ph-puc-duration">
<span title="剩余时间">{{ stamp2LastTime(restTs * 1000) }}</span>
<span title="活动时间">
{{ timestampToDate(Number(props.pos.start_timestamp) * 1000) }} ~
{{ timestampToDate(Number(props.pos.end_timestamp) * 1000) }}
</span>
<template v-if="isStart">
<span title="剩余时间">{{ stamp2LastTime(restTs * 1000) }}</span>
<span title="活动时间">
{{ timestampToDate(Number(props.pos.start_timestamp) * 1000) }} ~
{{ timestampToDate(Number(props.pos.end_timestamp) * 1000) }}
</span>
</template>
<template v-else>
<span>未开始</span>
</template>
</div>
<div class="ph-puc-rewards">
<div
@@ -94,7 +99,7 @@ import TMiImg from "@comp/app/t-mi-img.vue";
import { ActCalendarTypeEnum } from "@enum/game.js";
import { getHardChallengeDesc } from "@Sql/utils/transUserRecord.js";
import { stamp2LastTime, timestampToDate } from "@utils/toolFunc.js";
import { onMounted, onUnmounted, ref } from "vue";
import { computed, onMounted, onUnmounted, ref } from "vue";
import { useRouter } from "vue-router";
type PhCompPositionUserProps = { pos: TGApp.Game.ActCalendar.ActItem };
@@ -110,6 +115,9 @@ const emits = defineEmits<PhCompPositionUserEmits>();
const endTs = ref<number>(0);
const restTs = ref<number>(0);
const durationTs = ref<number>(0);
const isStart = computed<boolean>(() => {
return props.pos.start_timestamp !== "0";
});
onMounted(() => {
endTs.value = Number(props.pos.end_timestamp);

View File

@@ -0,0 +1,62 @@
<template>
<div class="twn-type-tag" :class="typeCls">
{{ props.type }}
</div>
</template>
<script lang="ts" setup>
import { computed } from "vue";
type TwnTypeTagProps = { type: string };
const props = defineProps<TwnTypeTagProps>();
const typeCls = computed<string>(() => {
switch (props.type) {
case "成就":
return "achi";
case "好感":
return "fetter";
case "活动":
return "act";
case "纪行":
return "journey";
case "声望":
return "pop";
default:
return "default";
}
});
</script>
<style lang="scss" scoped>
@use "@styles/github.styles.scss" as github-styles;
.twn-type-tag {
padding: 0 4px;
border-radius: 4px;
font-size: 12px;
&.achi {
@include github-styles.github-tag-dark-gen(#4db6ac);
}
&.fetter {
@include github-styles.github-tag-dark-gen(#ba68c8);
}
&.act {
@include github-styles.github-tag-dark-gen(#81c784);
}
&.journey {
@include github-styles.github-tag-dark-gen(#64b5f6);
}
&.pop {
@include github-styles.github-tag-dark-gen(#e57373);
}
&.default {
@include github-styles.github-tag-dark-gen(#ffb74d);
}
}
</style>

View File

@@ -57,6 +57,7 @@ const props = defineProps<TwoConvertProps>();
}
.icon {
position: relative;
z-index: 2;
width: 40px;
height: 40px;

View File

@@ -1,6 +1,6 @@
<template>
<div class="tua-al-container">
<div v-if="ncData !== undefined">
<div v-if="ncData !== undefined" class="tua-al-nc">
<TopNameCard :data="ncData" @selected="showNc = true" :finish="isFinish" />
</div>
<v-virtual-scroll :items="renderAchi" :item-height="60" class="tua-al-list">
@@ -160,6 +160,10 @@ function switchAchiInfo(next: boolean): void {
overflow-y: auto;
}
.tua-al-nc {
margin-bottom: 8px;
}
.tua-al-list {
padding-right: 10px;
}

View File

@@ -160,7 +160,7 @@ async function setAchiStat(stat: boolean): Promise<void> {
&__title {
display: flex;
align-items: flex-end;
align-items: center;
column-gap: 4px;
font-family: var(--font-title);
font-size: 14px;
@@ -175,12 +175,18 @@ async function setAchiStat(stat: boolean): Promise<void> {
@include github-styles.github-tag-dark-gen(#00aeec);
display: flex;
height: 21px;
height: 18px;
box-sizing: border-box;
align-items: center;
justify-content: center;
padding: 0 4px;
border-radius: 4px;
font-size: 12px;
border-radius: 9px;
font-family: var(--font-text);
font-size: 10px;
&:hover {
@include github-styles.github-tag-dark-gen(#7ab61f);
}
}
}

View File

@@ -8,6 +8,9 @@
>
<span>{{ talent.name }}</span>
<div class="duc-dort-icon">
<div v-if="!talent.is_unlock" class="duc-dort-lock">
<v-icon color="white">mdi-lock</v-icon>
</div>
<TMiImg :ori="true" :src="talent.icon" alt="talent" />
</div>
<span>Lv.{{ talent.level === 0 ? 1 : talent.level }}</span>
@@ -25,20 +28,49 @@ const props = defineProps<DucDetailOrtProps>();
.duc-dort-box {
display: flex;
flex-direction: column;
row-gap: 10px;
row-gap: 8px;
}
.duc-dort-item {
display: flex;
justify-content: flex-end;
column-gap: 8px;
span {
display: flex;
align-items: center;
justify-content: flex-start;
color: var(--tgc-white-1);
font-family: var(--font-title);
font-size: 16px;
text-shadow: 0 0 5px #00000066;
&:last-child {
width: 48px;
}
}
}
.duc-dort-lock {
position: absolute;
z-index: 2;
display: flex;
width: 44px;
height: 44px;
align-items: center;
justify-content: center;
padding: 3px;
border-radius: 50%;
-webkit-backdrop-filter: blur(5px);
backdrop-filter: blur(5px);
background-color: #00000066;
}
.duc-dort-icon {
position: relative;
display: flex;
width: 48px;
height: 48px;
width: 40px;
height: 40px;
box-sizing: border-box;
align-items: center;
justify-content: center;
@@ -54,14 +86,4 @@ const props = defineProps<DucDetailOrtProps>();
object-fit: contain;
}
}
.duc-dort-item span {
display: flex;
align-items: center;
justify-content: center;
color: var(--tgc-white-1);
font-family: var(--font-title);
font-size: 16px;
text-shadow: 0 0 5px #00000066;
}
</style>

View File

@@ -35,8 +35,7 @@
</div>
<!-- 底部水印信息 -->
<div class="duc-doc-bt">
UID: {{ props.modelValue.uid }} Updated: {{ props.modelValue.updated }} | Rendered by
TeyvatGuide v{{ version }}
UID: {{ props.modelValue.uid }} {{ props.modelValue.updated }} | TeyvatGuide v{{ version }}
</div>
</div>
</template>
@@ -149,9 +148,8 @@ async function share(): Promise<void> {
.duc-doc-rt {
position: absolute;
top: 10px;
right: 10px;
padding: 5px;
top: 8px;
right: 8px;
}
.duc-doc-lb {

View File

@@ -49,9 +49,9 @@
/>
<TurOverviewSub :text="modelValue.domainNumber" title="解锁秘境" />
<TurOverviewSub
:text="modelValue.pyroCulus"
icon="/icon/material/107028.webp"
title="神瞳"
:text="modelValue.moonCulus"
icon="/icon/material/107030.webp"
title="神瞳"
/>
<TurOverviewSub
:text="modelValue.anemoCulus"
@@ -78,6 +78,11 @@
icon="/icon/material/107023.webp"
title="水神瞳"
/>
<TurOverviewSub
:text="modelValue.pyroCulus"
icon="/icon/material/107028.webp"
title="火神瞳"
/>
<TurOverviewSub :text="modelValue.luxuriousChest" title="华丽宝箱数" />
<TurOverviewSub :text="modelValue.preciousChest" title="珍贵宝箱数" />
<TurOverviewSub :text="modelValue.exquisiteChest" title="精致宝箱数" />
@@ -95,15 +100,15 @@ defineProps<{ modelValue: TGApp.Sqlite.Record.Stats }>();
.tur-og-box {
display: grid;
width: 100%;
grid-gap: 8px;
grid-template-columns: repeat(4, 0.25fr);
gap: 8px;
grid-template-columns: repeat(3, 0.33fr);
}
.tur-og-box-3 {
display: grid;
width: 100%;
margin-bottom: 8px;
grid-gap: 8px;
gap: 8px;
grid-template-columns: repeat(3, 1fr);
}
</style>

View File

@@ -9,10 +9,22 @@
<div class="tur-ws-content">
<div class="tur-ws-title">
<span>{{ data.name }}</span>
<span v-if="data.offering" class="tur-ws-sub">
<img :src="data.offering.icon" alt="offer" />
<span>{{ data.offering.name }}-</span>
<span>{{ data.offering.level }}</span>
<span v-if="data.offerings?.length === 1" class="tur-ws-sub">
<img :src="data.offerings[0].icon" alt="offer" />
<span>{{ data.offerings[0].name }}-</span>
<span>{{ data.offerings[0].level }}</span>
<span></span>
</span>
</div>
<div class="tur-ws-offerings" v-if="data.offerings && data.offerings.length > 1">
<span
v-for="(offer, idx) in data.offerings"
:key="idx"
class="tur-ws-sub"
:title="offer.name + '-' + offer.level + '级'"
>
<img :src="offer.icon" alt="offer" />
<span>{{ offer.level }}</span>
<span></span>
</span>
</div>
@@ -33,6 +45,24 @@
<span>%</span>
</div>
</div>
<div
v-if="
data.area_exploration_list &&
data.area_exploration_list.length > 0 &&
data.exploration < 1000
"
class="tur-ws-areas"
>
<span
v-for="area in data.area_exploration_list.filter((i) => i.exploration_percentage < 1000)"
:key="area.name"
class="tur-ws-sub"
>
<span>{{ area.name }}</span>
<span>{{ Math.min(area.exploration_percentage / 10, 100) }}</span>
<span>%</span>
</span>
</div>
<div v-if="data.reputation" class="tur-ws-sub">
<span>声望等级:</span>
<span>{{ data.reputation }}</span>
@@ -84,6 +114,7 @@ const icon = computed<string>(() => {
}
.tur-ws-icon {
position: relative;
z-index: 1;
width: 64px;
height: 64px;
@@ -96,6 +127,7 @@ const icon = computed<string>(() => {
}
.tur-ws-content {
position: relative;
z-index: 1;
width: calc(100% - 68px);
height: 100%;
@@ -111,6 +143,21 @@ const icon = computed<string>(() => {
font-size: 18px;
}
.tur-ws-offerings {
display: flex;
align-items: center;
justify-content: start;
column-gap: 8px;
}
.tur-ws-areas {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: start;
gap: 4px 8px;
}
.tur-ws-sub {
display: flex;
align-items: center;

View File

@@ -396,7 +396,6 @@ async function trySign(ac: SignAccount[], ck: TGApp.App.Account.Cookie): Promise
img {
width: 100%;
height: 100%;
border-radius: 4px;
}
.delete {
@@ -430,16 +429,16 @@ async function trySign(ac: SignAccount[], ck: TGApp.App.Account.Cookie): Promise
height: 48px;
align-items: center;
justify-content: center;
border: 1px solid var(--common-shadow-1);
border-radius: 50%;
background: var(--box-bg-4);
img {
width: 100%;
height: 100%;
width: 36px;
height: 36px;
box-sizing: border-box;
padding: 10px;
border: 1px solid var(--common-shadow-1);
border-radius: 50%;
transition: all 0.3s;
}
span {
@@ -454,5 +453,11 @@ async function trySign(ac: SignAccount[], ck: TGApp.App.Account.Cookie): Promise
font-size: 8px;
text-align: center;
}
&:hover img {
width: 40px;
height: 40px;
transition: all 0.3s;
}
}
</style>

View File

@@ -5527,6 +5527,15 @@
"source": [],
"convert": []
},
{
"id": 107030,
"name": "散失的月神瞳",
"description": "浓烈的月矩力积聚成的物质。献给新月的神像,能弥补它在漫长岁月中散失的力量。",
"type": "冒险道具",
"star": 1,
"source": [],
"convert": []
},
{
"id": 110001,
"name": "面粉",

View File

@@ -39,9 +39,7 @@
<div class="ur-box" v-if="recordData">
<div class="ur-box-title">
<TurRoleInfo :role="recordData.role" :uid="uidCur ?? 0" />
<span class="sign">
原神战绩|Render by TeyvatGuide v{{ version }}|更新于 {{ recordData.updated }}
</span>
<span class="sign">TeyvatGuide v{{ version }} | {{ recordData.updated }}</span>
</div>
<PhCompCard>
<template #title>数据总览</template>
@@ -115,6 +113,7 @@ async function loadRecord(): Promise<void> {
const record = await TSUserRecord.getRecord(uidCur.value);
if (!record) return;
recordData.value = record;
console.log(recordData.value);
}
async function refreshRecord(): Promise<void> {
@@ -144,21 +143,21 @@ async function refreshRecord(): Promise<void> {
}
await showLoading.start(`正在刷新${account.value.gameUid}的战绩数据`);
await TGLogger.Info(`[UserRecord][refresh][${account.value.gameUid}] 刷新战绩数据`);
const res = await recordReq.index(cookie.value, account.value);
if ("retcode" in res) {
const resp = await recordReq.index(cookie.value, account.value);
console.log(resp);
if ("retcode" in resp) {
await showLoading.end();
showSnackbar.error(`[${res.retcode}] ${res.message}`);
showSnackbar.error(`[${resp.retcode}] ${resp.message}`);
await TGLogger.Error(`[UserRecord][refresh][${account.value.gameUid}] 获取战绩数据失败`);
await TGLogger.Error(
`[UserRecord][refresh][${account.value.gameUid}] ${res.retcode} ${res.message}`,
`[UserRecord][refresh][${account.value.gameUid}] ${resp.retcode} ${resp.message}`,
);
return;
}
await TGLogger.Info(`[UserRecord][refresh][${account.value.gameUid}] 获取战绩数据成功`);
await TGLogger.Info(`[UserRecord][refresh][${account.value.gameUid}]`, false);
console.log(res);
await showLoading.update("正在保存战绩数据");
await TSUserRecord.saveRecord(Number(account.value.gameUid), res);
await TSUserRecord.saveRecord(Number(account.value.gameUid), resp);
await showLoading.update("正在加载战绩数据");
await loadUid();
await loadRecord();

View File

@@ -1,8 +1,8 @@
<template>
<v-app-bar density="compact">
<v-app-bar>
<template #prepend>
<div class="twm-title">
<div class="twm-title-left">
<div class="twm-top-prepend">
<div class="title">
<img src="/source/UI/wikiGCG.webp" alt="icon" />
<span>材料图鉴</span>
</div>
@@ -14,6 +14,8 @@
:clearable="true"
width="250px"
label="材料类别"
density="compact"
variant="outlined"
>
<template #item="{ props, item }">
<v-list-item v-bind="props">
@@ -26,16 +28,19 @@
</div>
</template>
<template #append>
<v-text-field
v-model="search"
width="200px"
append-icon="mdi-magnify"
label="搜索"
:single-line="true"
:hide-details="true"
@keydown.enter="searchMaterial()"
@click:append="searchMaterial()"
/>
<div class="twm-top-append">
<v-text-field
v-model="search"
variant="outlined"
density="compact"
prepend-inner-icon="mdi-magnify"
label="搜索"
:single-line="true"
:hide-details="true"
@keydown.enter="searchMaterial()"
@click:prepend-inner="searchMaterial()"
/>
</div>
</template>
</v-app-bar>
<div class="twm-box">
@@ -158,33 +163,36 @@ function searchMaterial(): void {
}
</script>
<style lang="css" scoped>
.twm-title {
.twm-top-prepend {
position: relative;
display: flex;
align-items: center;
justify-content: center;
padding: 5px;
margin: 5px;
column-gap: 10px;
justify-content: flex-start;
margin-left: 16px;
column-gap: 16px;
.title {
position: relative;
display: flex;
align-items: center;
justify-content: center;
color: var(--common-text-title);
column-gap: 4px;
font-family: var(--font-title);
font-size: 20px;
img {
width: 32px;
height: 32px;
object-fit: cover;
}
}
}
.twm-title-left {
.twm-top-append {
position: relative;
display: flex;
align-items: center;
justify-content: center;
column-gap: 5px;
img {
width: 30px;
height: 30px;
object-fit: cover;
}
span {
font-family: var(--font-title);
font-size: 18px;
}
width: 600px;
margin-right: 16px;
}
.twm-box {

View File

@@ -1,22 +1,53 @@
<!-- TODO: UI一致性&类别筛选 -->
<template>
<div class="tw-nc-box">
<v-text-field
v-model="search"
prepend-inner-icon="mdi-magnify"
label="搜索"
:hide-details="true"
variant="outlined"
@click:prepend-inner="searchNameCard()"
@keyup.enter="searchNameCard()"
/>
<div class="tw-nc-list">
<v-virtual-scroll :items="sortNameCardsData" :item-height="80" item-key="id">
<template #default="{ item }">
<TopNameCard :data="item" @selected="showNameCard(item)" />
</template>
</v-virtual-scroll>
</div>
<v-app-bar>
<template #prepend>
<div class="wnc-top-prepend">
<div class="title">
<v-icon size="32">mdi-credit-card-outline</v-icon>
<span>名片图鉴</span>
</div>
<v-select
v-model="selectType"
:items="namecardTypes"
item-title="type"
:hide-details="true"
:clearable="true"
width="250px"
label="名片类别"
density="compact"
variant="outlined"
>
<template #item="{ props, item }">
<v-list-item v-bind="props">
<template #append>
<v-chip>{{ item.raw.number }}</v-chip>
</template>
</v-list-item>
</template>
</v-select>
</div>
</template>
<template #append>
<div class="wnc-top-append">
<v-text-field
v-model="search"
density="compact"
prepend-inner-icon="mdi-magnify"
label="搜索"
:hide-details="true"
variant="outlined"
@click:prepend-inner="searchNameCard()"
@keyup.enter="searchNameCard()"
/>
</div>
</template>
</v-app-bar>
<div class="tw-nc-list">
<v-virtual-scroll class="v-scroll" :items="sortNameCardsData" :item-height="80" item-key="id">
<template #default="{ item }">
<TopNameCard class="item" :data="item" @selected="showNameCard(item)" />
</template>
</v-virtual-scroll>
</div>
<ToNameCard v-model="visible" :data="curNameCard">
<template #left>
@@ -35,18 +66,46 @@
import ToNameCard from "@comp/app/to-nameCard.vue";
import TopNameCard from "@comp/app/top-nameCard.vue";
import showSnackbar from "@comp/func/snackbar.js";
import { onMounted, ref, shallowRef } from "vue";
import { onMounted, ref, shallowRef, watch } from "vue";
import { AppNameCardsData } from "@/data/index.js";
type NameCardType = { type: string; number: number };
const curIndex = ref<number>(0);
const total = ref<number>(0);
const visible = ref<boolean>(false);
const search = ref<string>();
const selectType = ref<string | null>(null);
const namecardTypes = shallowRef<Array<NameCardType>>([]);
const curNameCard = shallowRef<TGApp.App.NameCard.Item>();
const sortNameCardsData = shallowRef<Array<TGApp.App.NameCard.Item>>([]);
onMounted(() => sortData(AppNameCardsData));
onMounted(() => {
const tmpData: Array<NameCardType> = [];
for (const item of AppNameCardsData) {
const typeFindIndex = tmpData.findIndex((itemT) => itemT.type === item.type);
if (typeFindIndex === -1) {
const itemN: NameCardType = { type: item.type, number: 1 };
tmpData.push(itemN);
continue;
}
tmpData[typeFindIndex].number++;
}
namecardTypes.value = tmpData;
sortData(AppNameCardsData);
showSnackbar.success(`成功获取${sortNameCardsData.value.length}条数据`);
});
watch(
() => selectType.value,
() => sortData(getSelectNameCards()),
);
function getSelectNameCards(): TGApp.App.NameCard.Item[] {
if (selectType.value === null) return AppNameCardsData;
else return AppNameCardsData.filter((item) => item.type === selectType.value);
}
function sortData(data: TGApp.App.NameCard.Item[]): void {
sortNameCardsData.value = data.sort((a, b) => a.type.localeCompare(b.type) || a.id - b.id);
@@ -98,16 +157,47 @@ function searchNameCard(): void {
}
</script>
<style lang="css" scoped>
.tw-nc-box {
.wnc-top-prepend {
position: relative;
display: flex;
flex-direction: column;
row-gap: 10px;
align-items: center;
justify-content: flex-start;
margin-left: 16px;
column-gap: 16px;
.title {
position: relative;
display: flex;
align-items: center;
justify-content: center;
color: var(--common-text-title);
column-gap: 4px;
font-family: var(--font-title);
font-size: 20px;
}
}
.wnc-top-append {
position: relative;
width: 600px;
margin-right: 16px;
}
.tw-nc-list {
position: relative;
display: flex;
overflow: auto;
height: calc(100vh - 100px);
padding-right: 10px;
flex-direction: column;
row-gap: 10px;
.v-scroll {
padding-right: 8px;
.item {
margin-bottom: 8px;
}
}
}
.card-arrow {

View File

@@ -1,7 +1,7 @@
/**
* @file plugins/Sqlite/utils/transUserRecord.ts
* @description Sqlite 数据转换 用户战绩数据转换模块
* @since Beta v0.7.2
* @since Beta v0.8.1
*/
import { getZhElement } from "@utils/toolFunc.js";
@@ -89,9 +89,9 @@ export function getHardChallengeDesc(difficulty: number): string {
/**
* @description 将统计信息转换为数据库中的数据
* @since Beta v0.8.0
* @since Beta v0.8.1
* @param {TGApp.Game.Record.Stats} data 统计信息
* @return {TGApp.Sqlite.Record.Stats } 转换后的统计信息
* @return {TGApp.Sqlite.Record.Stats} 转换后的统计信息
*/
function transStat(data: TGApp.Game.Record.Stats): TGApp.Sqlite.Record.Stats {
return {
@@ -107,6 +107,7 @@ function transStat(data: TGApp.Game.Record.Stats): TGApp.Sqlite.Record.Stats {
dendroCulus: data.dendroculus_number,
hydroCulus: data.hydroculus_number,
pyroCulus: data.pyroculus_number,
moonCulus: data.moonoculus_number,
sprialAbyss: data.spiral_abyss,
combatRole: data.role_combat.is_unlock ? `${data.role_combat.max_round_id}` : "未解锁",
hardChallenge: data.hard_challenge.is_unlock
@@ -122,11 +123,13 @@ function transStat(data: TGApp.Game.Record.Stats): TGApp.Sqlite.Record.Stats {
/**
* @description 将探索信息转换为数据库中的数据
* @since Beta v0.7.2
* @param {TGApp.Game.Record.WorldExplore[]} data 城市探索信息
* @returns {TGApp.Sqlite.Record.WorldExplore[]} 转换后的城市探索信息
* @since Beta v0.8.1
* @param {Array<TGApp.Game.Record.WorldExplore>} data 城市探索信息
* @returns {Array<TGApp.Sqlite.Record.WorldExplore>} 转换后的城市探索信息
*/
function transWorld(data: TGApp.Game.Record.WorldExplore[]): TGApp.Sqlite.Record.WorldExplore[] {
function transWorld(
data: Array<TGApp.Game.Record.WorldExplore>,
): Array<TGApp.Sqlite.Record.WorldExplore> {
const areaParent = data.filter((i) => i.parent_id === 0);
const areaChild = data.filter((i) => i.parent_id !== 0);
const worlds: TGApp.Sqlite.Record.WorldExplore[] = [];
@@ -140,16 +143,11 @@ function transWorld(data: TGApp.Game.Record.WorldExplore[]): TGApp.Sqlite.Record
bg: area.background_image,
cover: area.cover,
exploration: area.exploration_percentage,
area_exploration_list: area.area_exploration_list,
children: [],
};
if (area.type === "Reputation") world.reputation = area.level;
if (area.offerings !== undefined && area.offerings.length > 0) {
world.offering = {
name: area.offerings[0].name,
level: area.offerings[0].level,
icon: area.offerings[0].icon,
};
}
if (area.offerings !== undefined && area.offerings.length > 0) world.offerings = area.offerings;
// 对纳塔的特殊处理
if (area.name === "纳塔") {
world.icon =
@@ -164,6 +162,13 @@ function transWorld(data: TGApp.Game.Record.WorldExplore[]): TGApp.Sqlite.Record
world.iconLight = world.icon;
world.bg =
"https://fastcdn.mihoyo.com/static-resource-v2/2025/03/17/8ee1648101a8b292ffb37eb49559032e_6583057448168798147.png";
// 对挪德卡莱的特殊处理
} else if (area.name === "挪德卡莱") {
world.icon =
"https://webstatic.mihoyo.com/app/community-game-records/images/world-logo-17.dadac5bf.png";
world.iconLight = world.icon;
world.bg =
"https://fastcdn.mihoyo.com/static-resource-v2/2025/08/22/ace66cea9c5074b70310ecbbb712cd94_2619077306700596372.png";
}
const children = areaChild.filter((i) => i.parent_id === area.id);
for (const child of children) {

View File

@@ -1,7 +1,7 @@
/**
* @file types/Game/Record.d.ts
* @description 原神战绩相关类型定义文件
* @since Beta v0.8.0
* @since Beta v0.8.1
*/
declare namespace TGApp.Game.Record {
@@ -11,7 +11,6 @@ declare namespace TGApp.Game.Record {
* @since Alpha v0.2.0
* @extends TGApp.BBS.Response.BaseWithData
* @property {FullData} data - 原神战绩数据
* @return Response
*/
type Response = TGApp.BBS.Response.BaseWithData<FullData>;
@@ -27,7 +26,6 @@ declare namespace TGApp.Game.Record {
* @property {Array<Home>} homes - 尘歌壶信息
* @property {string} query_tool_link - 查询工具链接
* @property {string} query_tool_image - 查询工具图片
* @return FullData
*/
type FullData = {
role: Role;
@@ -49,7 +47,6 @@ declare namespace TGApp.Game.Record {
* @property {string} region - 区域
* @property {number} level - 等级
* @property {string} game_head_icon - 游戏头像
* @return Role
*/
type Role = {
AvatarUrl: string;
@@ -75,7 +72,6 @@ declare namespace TGApp.Game.Record {
* @property {boolean} is_chosen - 角色是否展示
* @property {unknown} weapon - 角色武器 // null
* @property {Array<unknown>} relics - 角色圣遗物 // []
* @return Avatar
*/
type Avatar = {
id: number;
@@ -95,51 +91,53 @@ declare namespace TGApp.Game.Record {
/**
* @description 统计信息类型
* @interface Stats
* @since Beta v0.8.0
* @property {number} active_day_number - 活跃天数
* @since Beta v0.8.1
* @property {number} achievement_number - 成就数量
* @property {number} active_day_number - 活跃天数
* @property {number} anemoculus_number - 风神瞳数量
* @property {number} geoculus_number - 岩神瞳数量
* @property {number} avatar_number - 角色数量
* @property {number} way_point_number - 解锁传送点数量
* @property {number} domain_number - 解锁秘境数量
* @property {string} spiral_abyss - 深境螺旋最深达到几层
* @property {number} precious_chest_number - 珍贵宝箱数量
* @property {number} luxurious_chest_number - 豪华宝箱数量
* @property {number} exquisite_chest_number - 精致宝箱数量
* @property {number} common_chest_number - 普通宝箱数量
* @property {number} electroculus_number - 雷神瞳数量
* @property {number} magic_chest_number - 奇馈宝箱数量
* @property {number} dendroculus_number - 草神瞳数量
* @property {number} hydroculus_number - 水神瞳数量
* @property {number} pyroculus_number - 神瞳数量
* @property {number} domain_number - 解锁秘境数量
* @property {number} electroculus_number - 神瞳数量
* @property {number} exquisite_chest_number - 精致宝箱数量
* @property {unknown} field_ext_map - 数据对应链接的map用不到设为 unknown
* @property {CombatStats} role_combat - 幻想真境剧诗数据
* @property {number} full_fetter_avatar_num - 满好感角色数
* @property {number} geoculus_number - 岩神瞳数量
* @property {ChallengeStats} hard_challenge - 幽境危战挑战数据
* @property {number} hydroculus_number - 水神瞳数量
* @property {number} luxurious_chest_number - 豪华宝箱数量
* @property {number} magic_chest_number - 奇馈宝箱数量
* @property {number} moonoculus_number - 月神瞳数量
* @property {number} precious_chest_number - 珍贵宝箱数量
* @property {number} pyroculus_number - 火神瞳数量
* @property {CombatStats} role_combat - 幻想真境剧诗数据
* @property {string} spiral_abyss - 深境螺旋最深达到几层
* @property {number} way_point_number - 解锁传送点数量
*/
type Stats = {
active_day_number: number;
achievement_number: number;
active_day_number: number;
anemoculus_number: number;
geoculus_number: number;
avatar_number: number;
way_point_number: number;
domain_number: number;
spiral_abyss: string;
precious_chest_number: number;
luxurious_chest_number: number;
exquisite_chest_number: number;
common_chest_number: number;
electroculus_number: number;
magic_chest_number: number;
dendroculus_number: number;
hydroculus_number: number;
pyroculus_number: number;
domain_number: number;
electroculus_number: number;
exquisite_chest_number: number;
field_ext_map: unknown;
role_combat: CombatStats;
full_fetter_avatar_num: number;
geoculus_number: number;
hard_challenge: ChallengeStats;
hydroculus_number: number;
luxurious_chest_number: number;
magic_chest_number: number;
moonoculus_number: number;
precious_chest_number: number;
pyroculus_number: number;
role_combat: CombatStats;
spiral_abyss: string;
way_point_number: number;
};
/**
@@ -150,7 +148,6 @@ declare namespace TGApp.Game.Record {
* @property {number} max_round_id - 最大报幕数
* @property {boolean} has_data - 是否有数据
* @property {boolean} has_detail_data - 是否有详细数据
* @return CombatStats
*/
type CombatStats = {
is_unlock: boolean;
@@ -178,7 +175,7 @@ declare namespace TGApp.Game.Record {
/**
* @description 世界探索信息类型
* @interface WorldExplore
* @since Beta 0.7.2
* @since Beta 0.8.1
* @property {number} level - 声望等级
* @property {number} exploration_percentage - 探索千分比
* @property {string} icon - 图标
@@ -198,9 +195,8 @@ declare namespace TGApp.Game.Record {
* @property {boolean} index_active - 索引激活
* @property {boolean} detail_active - 详细激活
* @property {number} seven_status_level - 七天神像等级
* @property {NataReputation[] | null} nata_reputation - 纳塔声望
* @property {NataReputation | null} nata_reputation - 纳塔声望
* @property {number} world_type - 世界类型
* @return WorldExplore
*/
type WorldExplore = {
level: number;
@@ -216,7 +212,7 @@ declare namespace TGApp.Game.Record {
background_image: string;
inner_icon: string;
cover: string;
area_exploration_list: Array<unknown>;
area_exploration_list: Array<AreaExploration>;
boss_list: Array<unknown>;
is_hot: boolean;
index_active: boolean;
@@ -233,16 +229,23 @@ declare namespace TGApp.Game.Record {
* @property {string} name - 名称
* @property {number} level - 等级
* @property {string} icon - 图标
* @return WorldOffering
*/
type WorldOffering = { name: string; level: number; icon: string };
/**
* @description 区域探索类型
* @interface AreaExploration
* @since Beta v0.8.1
* @property {string} name - 名称
* @property {number} exploration_percentage - 探索千分比
*/
type AreaExploration = { name: string; exploration_percentage: number };
/**
* @description 纳塔声望类型
* @interface NataReputation
* @since Beta v0.7.2
* @property {Array<NataOffering>} tribal_list - 部落列表
* @returns NataReputation
*/
type NataReputation = { tribal_list: Array<NataOffering> };
@@ -252,7 +255,6 @@ declare namespace TGApp.Game.Record {
* @extends WorldOffering
* @property {number} id - ID
* @property {string} image - 图片
* @returns NataOffering
*/
type NataOffering = WorldOffering & { id: number; image: string };
@@ -268,7 +270,6 @@ declare namespace TGApp.Game.Record {
* @property {string} icon - 图标
* @property {string} comfort_level_name - 洞天仙力等级名称
* @property {string} comfort_level_icon - 洞天仙力等级图标
* @return Home
*/
type Home = {
level: number;

View File

@@ -1,7 +1,7 @@
/**
* @file types/Sqlite/Record.d.ts
* @description Sqlite 原神战绩相关类型定义文件
* @since Beta v0.8.0
* @since Beta v0.8.1
*/
declare namespace TGApp.Sqlite.Record {
@@ -16,7 +16,6 @@ declare namespace TGApp.Sqlite.Record {
* @property {string} worldExplore - 世界探索信息
* @property {string} homes - 尘歌壶信息
* @property {string} updated - 更新时间
* @return SingleTable
*/
type SingleTable = {
uid: number;
@@ -39,7 +38,6 @@ declare namespace TGApp.Sqlite.Record {
* @property {WorldExplore[]} worldExplore - 世界探索信息
* @property {Home[]} homes - 尘歌壶信息
* @property {string} updated - 更新时间
* @returns RenderData
*/
type RenderData = {
uid: number;
@@ -59,7 +57,6 @@ declare namespace TGApp.Sqlite.Record {
* @property {string} region - 区域
* @property {number} level - 等级
* @property {string} avatar - 头像
* @return Role
*/
type Role = { nickname: string; region: string; level: number; avatar: string };
@@ -75,7 +72,6 @@ declare namespace TGApp.Sqlite.Record {
* @property {number} star - 角色星级
* @property {number} constellation - 角色命座
* @property {boolean} isShow - 角色是否展示
* @return Avatar
*/
type Avatar = {
id: number;
@@ -91,7 +87,7 @@ declare namespace TGApp.Sqlite.Record {
/**
* @description 统计信息类型
* @interface Stats
* @since Beta v0.8.0
* @since Beta v0.8.1
* @property {number} activeDays - 活跃天数
* @property {number} achievementNumber - 成就达成数
* @property {number} avatarNumber - 获得角色数
@@ -104,6 +100,7 @@ declare namespace TGApp.Sqlite.Record {
* @property {number} dendroCulus - 草神瞳数
* @property {number} hydroCulus - 水神瞳数
* @property {number} pyroCulus - 火神瞳数
* @property {number} moonCulus - 月神瞳数
* @property {string} sprialAbyss - 深境螺旋信息
* @property {string} combatRole - 幻想真境剧诗
* @property {string} hardChallenge - 幽境危战挑战
@@ -112,7 +109,6 @@ declare namespace TGApp.Sqlite.Record {
* @property {number} exquisiteChest - 精致宝箱数
* @property {number} commonChest - 普通宝箱数
* @property {number} magicChest - 奇馈宝箱数
* @return Stats
*/
type Stats = {
activeDays: number;
@@ -127,6 +123,7 @@ declare namespace TGApp.Sqlite.Record {
dendroCulus: number;
hydroCulus: number;
pyroCulus: number;
moonCulus: number;
sprialAbyss: string;
combatRole: string;
hardChallenge: string;
@@ -140,7 +137,7 @@ declare namespace TGApp.Sqlite.Record {
/**
* @description 世界探索信息类型
* @interface WorldExplore
* @since Beta v0.7.2
* @since Beta v0.8.1
* @property {number} id - 地区 ID
* @property {string} name - 地区名称
* @property {string} iconLight - 地区图标(亮)
@@ -148,9 +145,9 @@ declare namespace TGApp.Sqlite.Record {
* @property {string} cover - 封面
* @property {number} reputation - 地区声望等级
* @property {WorldOffering} offering - 地区供奉信息
* @property {Array<WorldOffering>} offerings - 地区供奉列表
* @property {number} exploration - 地区探索进度
* @property {WorldChild[]} children - 子地区
* @return WorldExplore
* @property {Array<WorldChild>} children - 子地区
*/
type WorldExplore = {
id: number;
@@ -160,9 +157,14 @@ declare namespace TGApp.Sqlite.Record {
bg: string;
cover: string;
reputation?: number;
/**
* @deprecated 已弃用,建议使用 offerings
*/
offering?: WorldOffering;
offerings?: Array<WorldOffering>;
exploration: number;
children: WorldChild[];
area_exploration_list?: Array<AreaExploration>;
children: Array<WorldChild>;
};
/**
@@ -172,10 +174,18 @@ declare namespace TGApp.Sqlite.Record {
* @property {string} name - 名称
* @property {number} level - 等级
* @property {string} icon - 图标
* @return WorldOffering
*/
type WorldOffering = { name: string; level: number; icon: string };
/**
* @description 区域探索类型
* @interface AreaExploration
* @since Beta v0.8.1
* @property {string} name - 名称
* @property {number} exploration_percentage - 探索千分比
*/
type AreaExploration = { name: string; exploration_percentage: number };
/**
* @description 子地区类型
* @interface WorldChild
@@ -183,7 +193,6 @@ declare namespace TGApp.Sqlite.Record {
* @property {number} id - 子地区 ID
* @property {string} name - 子地区名称
* @property {number} exploration - 子地区探索进度
* @return WorldChild
*/
type WorldChild = { id: number; name: string; exploration: number };
@@ -199,7 +208,6 @@ declare namespace TGApp.Sqlite.Record {
* @property {number} furniture - 获得摆设数
* @property {number} visit - ;历史访客数
* @property {string} bg - 背景
* @return Home
*/
type Home = {
comfortIcon: string;

View File

@@ -1,7 +1,7 @@
/**
* @file utils/toolFunc.ts
* @description 一些工具函数
* @since Beta v0.8.0
* @since Beta v0.8.1
*/
import { AvatarExtResTypeEnum, AvatarExtTypeEnum } from "@enum/bbs.js";
@@ -220,18 +220,30 @@ export function isColorSimilar(colorBg: string, colorText: string): boolean {
/**
* @description 解析带样式的文本
* @since Beta v0.3.8
* @since Beta v0.8.1
* @param {string} desc - 带样式的文本
* @returns {string} 解析后的文本
*/
export function parseHtmlText(desc: string): string {
const reg = /<color=(.*?)>(.*?)<\/color>/g;
let match = reg.exec(desc);
while (match !== null) {
const color = match[1];
const text = match[2];
desc = desc.replace(match[0], `<span title="${text}" style="color: ${color}">${text}</span>`);
match = reg.exec(desc);
const linkReg = /\{LINK#(.*?)}(.*?)\{\/LINK}/g;
let linkMatch = linkReg.exec(desc);
while (linkMatch !== null) {
const link = linkMatch[1];
const text = linkMatch[2];
// TODO: 后续处理 t-link
desc = desc.replace(linkMatch[0], `<t-link data-link="${link}">${text}</t-link>`);
linkMatch = linkReg.exec(desc);
}
const colorReg = /<color=(.*?)>(.*?)<\/color>/g;
let colorMatch = colorReg.exec(desc);
while (colorMatch !== null) {
const color = colorMatch[1];
const text = new DOMParser().parseFromString(colorMatch[2], "text/html").body.textContent;
desc = desc.replace(
colorMatch[0],
`<span title="${text}" style="color: ${color}">${text}</span>`,
);
colorMatch = colorReg.exec(desc);
}
desc = desc.replace(/\\n/g, "<br />");
return desc;