Compare commits

...

4 Commits

Author SHA1 Message Date
BTMuli
ed3daa2277 🍱 更新卡池数据 2025-12-05 22:23:03 +08:00
BTMuli
637be3c4af 使用Swiper组件重构Gacha概览展示,优化数据展示方式 2025-12-05 22:05:17 +08:00
BTMuli
f30ee36a83 💄 调整高度计算&星级占比计算逻辑 2025-12-05 22:04:51 +08:00
Copilot
8d5cb52320 祈愿添加UP抽数数据,采取动态高度计算 (#174)
* Initial plan

* feat: Add UP average pull count for 5-star items in gacha records

Co-authored-by: BTMuli <72692909+BTMuli@users.noreply.github.com>

* refactor: Optimize isStar5Up function by extracting Number conversion

Co-authored-by: BTMuli <72692909+BTMuli@users.noreply.github.com>

* refactor: Add 4-star UP average and optimize hint calculation

- Add star4UpAvg variable and getStar4UpAvg() for 4-star UP average
- Add getItemHint() to calculate UP/歪 hint for both 4 and 5-star items
- Calculate hints in gro-data-view and pass via hint prop to gro-data-line
- Remove duplicate getEndHint() calculation from gro-data-line.vue
- Add UP average display in 4-star section with dynamic grid layout

Co-authored-by: BTMuli <72692909+BTMuli@users.noreply.github.com>

* ♻️ 调整传递数据类型,样式适配调整

* 💄 微调高度

* fix: Use flexbox for dynamic height calculation in gro-data-view

Replace hardcoded height calculations with flexbox layout:
- Make gro-dv-container a flex column container
- Use flex: 1 for gro-bottom and gro-bottom-window to take remaining space
- Add min-height: 0 to enable proper flex shrinking for scrollable areas

Co-authored-by: BTMuli <72692909+BTMuli@users.noreply.github.com>

* fix: Resolve v-window height overflow issue

- Add overflow: hidden to .gro-bottom and .gro-bottom-window containers
- Move overflow-y: auto scrolling to .gro-b-window-item level
- Add height: 100% to .gro-b-window-item for proper containment

Co-authored-by: BTMuli <72692909+BTMuli@users.noreply.github.com>

* fix: Use dynamic height calculation via template refs

- Add template refs for container and header elements
- Calculate bottom and window heights dynamically based on actual DOM element sizes
- Remove flexbox approach that didn't work with Vuetify v-window
- Apply calculated heights via style binding

Co-authored-by: BTMuli <72692909+BTMuli@users.noreply.github.com>

* 🎨 useTemplateRef & v-bind

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: BTMuli <72692909+BTMuli@users.noreply.github.com>
Co-authored-by: BTMuli <bt-muli@outlook.com>
2025-12-05 12:57:54 +08:00
4 changed files with 250 additions and 125 deletions

View File

@@ -1,3 +1,4 @@
<!-- 祈愿数据项展示行 -->
<template>
<div class="gro-dl-box">
<div class="gro-dl-progress" />
@@ -10,7 +11,7 @@
</div>
<div class="gro-dl-info">
<div class="gro-dl-cnt">{{ props.count }}</div>
<div class="gro-dl-hint" v-if="hint !== ''">{{ hint }}</div>
<div v-if="props.isUp !== undefined" class="gro-dl-hint">{{ props.isUp ? "UP" : "歪" }}</div>
</div>
</div>
</template>
@@ -18,12 +19,19 @@
import { getWikiBrief } from "@utils/toolFunc.js";
import { computed } from "vue";
import { AppGachaData } from "@/data/index.js";
export type GroDataLineProps = { data: TGApp.Sqlite.GachaRecords.TableGacha; count: number };
/**
* 祈愿数据项展示行组件参数
*/
export type GroDataLineProps = {
/* 原始数据 */
data: TGApp.Sqlite.GachaRecords.TableGacha;
/* 抽数 */
count: number;
/* 是否是 Up */
isUp: boolean | undefined;
};
const props = defineProps<GroDataLineProps>();
const hint = getEndHint();
function getIcon(): string {
const find = getWikiBrief(props.data.itemId);
@@ -32,32 +40,11 @@ function getIcon(): string {
return `/WIKI/weapon/${props.data.itemId}.webp`;
}
function getEndHint(): string {
if (props.data.gachaType === "100" || props.data.gachaType === "200") return "";
// if (props.data.rank !== "5") return "";
const itemTime = new Date(props.data.time).getTime();
const poolsFind = AppGachaData.filter((pool) => {
if (pool.type.toLocaleString() !== props.data.gachaType) return false;
const startTime = new Date(pool.from).getTime();
const endTime = new Date(pool.to).getTime();
return itemTime >= startTime && itemTime <= endTime;
});
if (poolsFind.length === 0) return "";
if (props.data.rank === "5") {
if (poolsFind.some((pool) => pool.up5List.includes(Number(props.data.itemId)))) return "UP";
return "歪";
}
if (props.data.rank === "4") {
if (poolsFind.some((pool) => pool.up4List.includes(Number(props.data.itemId)))) return "UP";
return "歪";
}
return "";
}
const progressColor = computed<string>(() => {
if (hint === "UP" && props.data.rank === "5") return "#d19a66";
if (hint === "UP" && props.data.rank === "4") return "#c678dd";
if (hint === "") return "#e06c75";
if (props.isUp === undefined) return "#61afef";
if (props.isUp && props.data.rank === "5") return "#d19a66";
if (props.isUp && props.data.rank === "4") return "#c678dd";
if (!props.isUp) return "#e06c75";
return "#61afef";
});
const progressWidth = computed<string>(() => {

View File

@@ -1,51 +1,61 @@
<!-- 祈愿数据概览 -->
<template>
<div class="gro-dv-container">
<div class="gro-dvt-title">
<span>{{ title }}</span>
<span>{{ props.dataVal.length }}</span>
</div>
<div class="gro-dvt-subtitle">
<span v-show="props.dataVal.length === 0">暂无数据</span>
<span v-show="props.dataVal.length !== 0">{{ startDate }} ~ {{ endDate }}</span>
</div>
<!-- 4星相关数据 -->
<div class="gro-mid-list">
<div class="gro-ml-title s4"></div>
<div class="gro-ml-card">
<span>已垫</span>
<span>{{ reset4count - 1 }}</span>
<div ref="groDvBoxRef" class="gro-dv-container">
<div ref="headerRef" class="gro-dv-header">
<div class="gro-dvt-title">
<span>{{ title }}</span>
<span>{{ props.dataVal.length }}</span>
</div>
<div class="gro-ml-card">
<span>平均</span>
<span>{{ star4avg }}</span>
<div class="gro-dvt-subtitle">
<span v-show="props.dataVal.length === 0">暂无数据</span>
<span v-show="props.dataVal.length !== 0">{{ startDate }} ~ {{ endDate }}</span>
</div>
<div class="gro-ml-card">
<span>统计</span>
<span>{{ star4List.length }}</span>
<!-- 4星相关数据 -->
<div :class="{ 'has-up': isUpPool }" class="gro-mid-list">
<div class="gro-ml-title s4"></div>
<div class="gro-ml-card">
<span></span>
<span>{{ reset4count - 1 }}</span>
</div>
<div class="gro-ml-card">
<span></span>
<span>{{ star4avg }}</span>
</div>
<div v-if="star4UpAvg !== ''" class="gro-ml-card">
<span>UP</span>
<span>{{ star4UpAvg }}</span>
</div>
<div class="gro-ml-card">
<span></span>
<span>{{ star4List.length }}</span>
</div>
</div>
</div>
<!-- 5星相关数据 -->
<div class="gro-mid-list">
<div class="gro-ml-title s5"></div>
<div class="gro-ml-card">
<span>已垫</span>
<span>{{ reset5count - 1 }}</span>
<!-- 5星相关数据 -->
<div :class="{ 'has-up': star5UpAvg !== '' }" class="gro-mid-list">
<div class="gro-ml-title s5"></div>
<div class="gro-ml-card">
<span></span>
<span>{{ reset5count - 1 }}</span>
</div>
<div class="gro-ml-card">
<span></span>
<span>{{ star5avg }}</span>
</div>
<div v-if="star5UpAvg !== ''" class="gro-ml-card">
<span>UP</span>
<span>{{ star5UpAvg }}</span>
</div>
<div class="gro-ml-card">
<span></span>
<span>{{ star5List.length }}</span>
</div>
</div>
<div class="gro-ml-card">
<span>平均</span>
<span>{{ star5avg }}</span>
<!-- 进度条拼接 -->
<div v-if="props.dataVal.length > 0" class="gro-mid-progress">
<div v-if="pg3 !== '0'" :style="{ width: pg3 }" class="s3" />
<div v-if="pg4 !== '0'" :style="{ width: pg4 }" class="s4" />
<div v-if="pg5 !== '0'" :style="{ width: pg5 }" class="s5" />
</div>
<div class="gro-ml-card">
<span>统计</span>
<span>{{ star5List.length }}</span>
</div>
</div>
<!-- 进度条拼接 -->
<div v-if="props.dataVal.length > 0" class="gro-mid-progress">
<div v-if="pg3 !== '0'" :style="{ width: pg3 }" :title="`3星占比:${pg3}`" class="s3" />
<div v-if="pg4 !== '0'" :style="{ width: pg4 }" :title="`4星占比:${pg4}`" class="s4" />
<div v-if="pg5 !== '0'" :style="{ width: pg5 }" :title="`5星占比:${pg5}`" class="s5" />
</div>
<!-- 这边放具体物品的列表 -->
<div class="gro-bottom">
@@ -57,14 +67,24 @@
<v-window-item class="gro-b-window-item" value="5">
<v-virtual-scroll :item-height="48" :items="star5List">
<template #default="{ item }">
<GroDataLine :key="item.data.id" :count="item.count" :data="item.data" />
<GroDataLine
:key="item.data.id"
:count="item.count"
:data="item.data"
:is-up="item.isUp"
/>
</template>
</v-virtual-scroll>
</v-window-item>
<v-window-item class="gro-b-window-item" value="4">
<v-virtual-scroll :item-height="48" :items="star4List">
<template #default="{ item }">
<GroDataLine :key="item.data.id" :count="item.count" :data="item.data" />
<GroDataLine
:key="item.data.id"
:count="item.count"
:data="item.data"
:is-up="item.isUp"
/>
</template>
</v-virtual-scroll>
</v-window-item>
@@ -73,10 +93,12 @@
</div>
</template>
<script lang="ts" setup>
import { computed, onMounted, ref, shallowRef, watch } from "vue";
import { computed, nextTick, onMounted, ref, shallowRef, useTemplateRef, watch } from "vue";
import GroDataLine, { type GroDataLineProps } from "./gro-data-line.vue";
import { AppGachaData } from "@/data/index.js";
type GachaDataViewProps = {
dataType: "new" | "avatar" | "weapon" | "normal" | "mix";
dataVal: Array<TGApp.Sqlite.GachaRecords.TableGacha>;
@@ -84,6 +106,14 @@ type GachaDataViewProps = {
const props = defineProps<GachaDataViewProps>();
// Template refs for dynamic height calculation
const groDvBoxEl = useTemplateRef<HTMLElement>("groDvBoxRef");
const headerEl = useTemplateRef<HTMLElement>("headerRef");
// Dynamic heights
const bottomHeight = ref<string>("auto");
const windowHeight = ref<string>("auto");
// data
const loading = ref<boolean>(true); // 是否加载完
const title = ref<string>(""); // 卡片标题
@@ -95,17 +125,44 @@ const reset5count = ref<number>(1); // 5星垫抽数量
const reset4count = ref<number>(1); // 4星垫抽数量
const star3count = ref<number>(0); // 3星物品数量
const star5avg = ref<string>(""); // 5星平均抽数
const star5UpAvg = ref<string>(""); // 5星UP平均抽数
const star4avg = ref<string>(""); // 4星平均抽数
const star4UpAvg = ref<string>(""); // 4星UP平均抽数
const tab = ref<string>("5"); // tab
const pg3 = computed<string>(() => getPg("3"));
const pg4 = computed<string>(() => getPg("4"));
const pg5 = computed<string>(() => getPg("5"));
const isUpPool = computed<boolean>(() => props.dataType !== "new" && props.dataType !== "normal");
onMounted(() => {
// Calculate dynamic heights
function calculateHeights(): void {
if (!groDvBoxEl.value || !headerEl.value) return;
const containerHeight = groDvBoxEl.value.clientHeight;
const headerHeight = headerEl.value.clientHeight;
const padding = 20; // 8px padding top + 8px padding bottom + 4px magic
const tabsHeight = 36; // v-tabs compact height
const gap = 8; // gap between tabs and window
const bottomHeightPx = containerHeight - headerHeight - padding;
const windowHeightPx = bottomHeightPx - tabsHeight - gap;
bottomHeight.value = `${bottomHeightPx}px`;
windowHeight.value = `${windowHeightPx}px`;
}
onMounted(async () => {
loadData();
loading.value = false;
await nextTick();
calculateHeights();
});
watch(
() => [props.dataVal, props.dataType],
async () => {
await nextTick();
calculateHeights();
},
);
function loadData(): void {
title.value = getTitle();
const tempData = props.dataVal;
@@ -125,18 +182,20 @@ function loadData(): void {
star3count.value++;
} else if (item.rank === "4") {
reset5count.value++;
temp4Data.push({ data: item, count: reset4count.value });
temp4Data.push({ data: item, count: reset4count.value, isUp: checkIsUp(item) });
reset4count.value = 1;
} else if (item.rank === "5") {
reset4count.value++;
temp5Data.push({ data: item, count: reset5count.value });
temp5Data.push({ data: item, count: reset5count.value, isUp: checkIsUp(item) });
reset5count.value = 1;
}
});
star5List.value = temp5Data.reverse();
star4List.value = temp4Data.reverse();
star5avg.value = getStar5Avg();
star5UpAvg.value = getStar5UpAvg();
star4avg.value = getStar4Avg();
star4UpAvg.value = getStar4UpAvg();
}
// 获取标题
@@ -157,6 +216,42 @@ function getStar5Avg(): string {
return (total / star5List.value.length).toFixed(2);
}
/**
* 判断是否是Up物品
* @param {TGApp.Sqlite.GachaRecords.TableGacha} item 原始数据
* @returns {boolean|undefined}
*/
function checkIsUp(item: TGApp.Sqlite.GachaRecords.TableGacha): boolean | undefined {
// 新手池和常驻池不存在UP概念
if (item.gachaType === "100" || item.gachaType === "200") return undefined;
const itemTime = new Date(item.time).getTime();
const itemIdNum = Number(item.itemId);
const poolsFind = AppGachaData.filter((pool) => {
if (pool.type.toLocaleString() !== item.gachaType) return false;
const startTime = new Date(pool.from).getTime();
const endTime = new Date(pool.to).getTime();
return itemTime >= startTime && itemTime <= endTime;
});
if (poolsFind.length === 0) return undefined;
if (item.rank === "5") {
return poolsFind.some((pool) => pool.up5List.includes(itemIdNum));
}
if (item.rank === "4") {
return poolsFind.some((pool) => pool.up4List.includes(itemIdNum));
}
return undefined;
}
// 获取5星UP平均抽数
function getStar5UpAvg(): string {
// 新手池和常驻池不显示UP平均
if (props.dataType === "new" || props.dataType === "normal") return "";
const upList = star5List.value.filter((item) => item.isUp);
if (upList.length === 0) return "0";
const total = upList.reduce((a, b) => a + b.count, 0);
return (total / upList.length).toFixed(2);
}
// 获取4星平均抽数
function getStar4Avg(): string {
const resetList = star4List.value.map((item) => item.count);
@@ -165,15 +260,30 @@ function getStar4Avg(): string {
return (total / star4List.value.length).toFixed(2);
}
// 获取4星UP平均抽数
function getStar4UpAvg(): string {
// 新手池和常驻池不显示UP平均
if (props.dataType === "new" || props.dataType === "normal") return "";
const upList = star4List.value.filter((item) => item.isUp);
if (upList.length === 0) return "0";
const total = upList.reduce((a, b) => a + b.count, 0);
return (total / upList.length).toFixed(2);
}
// 获取占比
function getPg(star: "5" | "4" | "3"): string {
let progress: number;
// 开根号
const sq5 = Math.sqrt(star5List.value.length);
const sq4 = Math.sqrt(star4List.value.length);
const sq3 = Math.sqrt(star3count.value);
const total = sq5 + sq4 + sq3;
if (star === "5") {
progress = (star5List.value.length * 100) / props.dataVal.length;
progress = (sq5 * 100) / total;
} else if (star === "4") {
progress = (star4List.value.length * 100) / props.dataVal.length;
progress = (sq4 * 100) / total;
} else {
progress = (star3count.value * 100) / props.dataVal.length;
progress = (sq3 * 100) / total;
}
if (progress === 0) return "0";
return `${progress.toFixed(2)}%`;
@@ -182,7 +292,7 @@ function getPg(star: "5" | "4" | "3"): string {
// 监听数据变化
watch(
() => props.dataVal,
() => {
async () => {
star5List.value = [];
star4List.value = [];
reset5count.value = 1;
@@ -191,14 +301,19 @@ watch(
startDate.value = "";
endDate.value = "";
star5avg.value = "";
star5UpAvg.value = "";
star4avg.value = "";
star4UpAvg.value = "";
tab.value = "5";
loadData();
await nextTick();
calculateHeights();
},
);
</script>
<style lang="css" scoped>
.gro-dv-container {
position: relative;
height: 100%;
box-sizing: border-box;
padding: 8px;
@@ -206,6 +321,10 @@ watch(
background: var(--box-bg-1);
}
.gro-dv-header {
position: relative;
}
.gro-dvt-title {
display: flex;
width: 100%;
@@ -225,12 +344,16 @@ watch(
.gro-mid-list {
display: grid;
margin-top: 8px;
margin-bottom: 8px;
margin-top: 4px;
margin-bottom: 4px;
color: var(--box-text-7);
column-gap: 12px;
column-gap: 4px;
font-size: 14px;
grid-template-columns: 1fr 1fr 1fr 1fr;
&.has-up {
grid-template-columns: 1fr 1fr 1fr 1fr 1fr;
}
}
.gro-ml-title {
@@ -294,7 +417,7 @@ watch(
position: relative;
display: flex;
width: 100%;
height: calc(100% - 120px);
height: v-bind(bottomHeight); /* stylelint-disable-line value-keyword-case */
box-sizing: border-box;
flex-direction: column;
gap: 8px;
@@ -302,13 +425,14 @@ watch(
.gro-bottom-window {
position: relative;
height: calc(100vh - 380px);
height: v-bind(windowHeight); /* stylelint-disable-line value-keyword-case */
overflow-y: auto;
}
.gro-b-window-item {
position: relative;
width: 100%;
height: 100%;
box-sizing: border-box;
padding-right: 4px;
}

View File

@@ -1,20 +1,46 @@
<template>
<div class="gro-o-container">
<GroDataView v-if="newData.length !== 0" :data-val="newData" data-type="new" />
<GroDataView :data-val="normalData" data-type="normal" />
<GroDataView :data-val="avatarData" data-type="avatar" />
<GroDataView :data-val="weaponData" data-type="weapon" />
<GroDataView v-if="mixData.length !== 0" :data-val="mixData" data-type="mix" />
</div>
<Swiper
:autoplay="false"
:centered-slides="false"
:loop="false"
:modules="swiperModules"
:pagination="{ clickable: true }"
:slides-per-view="3"
:space-between="12"
class="gro-o-swiper"
>
<SwiperSlide v-if="newData.length !== 0">
<GroDataView :data-val="newData" data-type="new" />
</SwiperSlide>
<SwiperSlide>
<GroDataView :data-val="normalData" data-type="normal" />
</SwiperSlide>
<SwiperSlide>
<GroDataView :data-val="avatarData" data-type="avatar" />
</SwiperSlide>
<SwiperSlide>
<GroDataView :data-val="weaponData" data-type="weapon" />
</SwiperSlide>
<SwiperSlide v-if="mixData.length !== 0">
<GroDataView :data-val="mixData" data-type="mix" />
</SwiperSlide>
</Swiper>
</template>
<script lang="ts" setup>
import { computed, ref, watch } from "vue";
import "swiper/css";
import "swiper/css/pagination";
import "swiper/css/navigation";
import { A11y, Autoplay, Pagination } from "swiper/modules";
import { Swiper, SwiperSlide } from "swiper/vue";
import { computed } from "vue";
import GroDataView from "./gro-data-view.vue";
type GachaOverviewProps = { modelValue: Array<TGApp.Sqlite.GachaRecords.TableGacha> };
const props = defineProps<GachaOverviewProps>();
const swiperModules = [Autoplay, A11y, Pagination];
const newData = computed<Array<TGApp.Sqlite.GachaRecords.TableGacha>>(() =>
props.modelValue.filter((item) => item.uigfType === "100"),
);
@@ -30,33 +56,21 @@ const weaponData = computed<Array<TGApp.Sqlite.GachaRecords.TableGacha>>(() =>
const mixData = computed<Array<TGApp.Sqlite.GachaRecords.TableGacha>>(() =>
props.modelValue.filter((item) => item.uigfType === "500"),
);
const cnCols = ref<string>(getCnCols());
function getCnCols(): string {
let total = 5;
if (newData.value.length === 0) {
total -= 1;
}
if (mixData.value.length === 0) {
total -= 1;
}
if (total === 5) return "repeat(5, 0.2fr)";
if (total === 4) return "repeat(4, 0.25fr)";
return "repeat(3, 0.33fr)";
}
// 监听数据变化
watch(
() => props.modelValue,
() => (cnCols.value = getCnCols()),
);
</script>
<style lang="css" scoped>
.gro-o-container {
display: grid;
.gro-o-swiper {
width: 100%;
height: 100%;
column-gap: 8px;
grid-template-columns: v-bind(cnCols); /* stylelint-disable-line value-keyword-case */
}
/* swiper dot */
:deep(.swiper-pagination-bullet) {
background: var(--tgc-od-white);
}
:deep(.swiper-pagination-bullet-active) {
background-color: var(--tgc-pink-1);
}
</style>

View File

@@ -3031,8 +3031,8 @@
"name": "白礜赤成",
"version": "6.2",
"order": 1,
"banner": "https://sdk-webstatic.mihoyo.com/upload/ann/2025/11/19/e9291b4ae7e6840d81adf8c3bcf2ad5e_5777095417575400489_transformed.jpg",
"from": "2025-12-03T10:00:00+08:00",
"banner": "https://sdk.hoyoverse.com/upload/ann/2025/11/19/e9291b4ae7e6840d81adf8c3bcf2ad5e_5777095417575400489_transformed.jpg",
"from": "2025-12-03T06:00:00+08:00",
"to": "2025-12-23T17:59:00+08:00",
"type": 301,
"postId": "71075619",
@@ -3043,20 +3043,20 @@
"name": "杯装之诗",
"version": "6.2",
"order": 1,
"banner": "https://sdk-webstatic.mihoyo.com/upload/ann/2025/11/19/123cbe77341ba0337e56f56600effd09_1549480919145244906_transformed.webp",
"from": "2025-12-03T10:00:00+08:00",
"banner": "https://sdk.hoyoverse.com/upload/ann/2025/11/19/123cbe77341ba0337e56f56600effd09_1549480919145244906_transformed.jpg",
"from": "2025-12-03T06:00:00+08:00",
"to": "2025-12-23T17:59:00+08:00",
"type": 400,
"postId": "71075617",
"up5List": [10000002],
"up5List": [10000022],
"up4List": [10000124, 10000032, 10000076]
},
{
"name": "神铸赋形",
"version": "6.2",
"order": 1,
"banner": "https://sdk-webstatic.mihoyo.com/upload/ann/2025/11/19/0a9191e9c94c34f1862223aea72f88d8_6761506822979172855_transformed.jpg",
"from": "2025-12-03T10:00:00+08:00",
"banner": "https://sdk.hoyoverse.com/upload/ann/2025/11/19/0a9191e9c94c34f1862223aea72f88d8_6761506822979172855_transformed.jpg",
"from": "2025-12-03T06:00:00+08:00",
"to": "2025-12-23T17:59:00+08:00",
"type": 302,
"postId": "71075615",