🚸重构千星页面,高度动态计算&活动次数计算共享 (#186)

* Initial plan

* Fix event pool pity count sharing for male/female gacha pools

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

* Use gachaType for pool filtering and simplify data passing

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

* Add dynamic height calculation and window resize listener to GbrDataView

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

* 🎨 Code style

* 🚸 没有数据更新时不刷新

---------

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>
This commit is contained in:
Copilot
2025-12-14 17:23:59 +08:00
committed by GitHub
parent 9a0fbe11e9
commit 6cca6e56be
4 changed files with 210 additions and 105 deletions

View File

@@ -1,69 +1,71 @@
<!-- 颂愿数据概览 TODO: 页面高度动态计算参考GroDataView -->
<!-- 颂愿数据概览 -->
<template>
<div class="gbr-dv-container">
<div class="gbr-dvt-title">
<span>{{ title }}</span>
<span>{{ props.dataVal.length }}</span>
</div>
<div class="gbr-dvt-subtitle">
<span v-show="props.dataVal.length === 0">暂无数据</span>
<span v-show="props.dataVal.length !== 0">{{ startDate }} ~ {{ endDate }}</span>
</div>
<!-- 3星相关数据 -->
<div class="gbr-mid-list">
<div class="gbr-ml-title s3"></div>
<div class="gbr-ml-card">
<span>已垫</span>
<span>{{ reset3count - 1 }}</span>
<div ref="gbrDvBoxRef" class="gbr-dv-container">
<div ref="headerRef" class="gbr-dv-header">
<div class="gbr-dvt-title">
<span>{{ title }}</span>
<span>{{ props.dataVal.length }}</span>
</div>
<div class="gbr-ml-card">
<span>平均</span>
<span>{{ star3avg }}</span>
<div class="gbr-dvt-subtitle">
<span v-show="props.dataVal.length === 0">暂无数据</span>
<span v-show="props.dataVal.length !== 0">{{ startDate }} ~ {{ endDate }}</span>
</div>
<div class="gbr-ml-card">
<span>统计</span>
<span>{{ star3List.length }}</span>
<!-- 3星相关数据 -->
<div class="gbr-mid-list">
<div class="gbr-ml-title s3"></div>
<div class="gbr-ml-card">
<span>已垫</span>
<span>{{ reset3count - 1 }}</span>
</div>
<div class="gbr-ml-card">
<span>平均</span>
<span>{{ star3avg }}</span>
</div>
<div class="gbr-ml-card">
<span>统计</span>
<span>{{ star3List.length }}</span>
</div>
</div>
</div>
<!-- 4星相关数据 -->
<div class="gbr-mid-list">
<div class="gbr-ml-title s4"></div>
<div class="gbr-ml-card">
<span>已垫</span>
<span>{{ reset4count - 1 }}</span>
<!-- 4星相关数据 -->
<div class="gbr-mid-list">
<div class="gbr-ml-title s4"></div>
<div class="gbr-ml-card">
<span>已垫</span>
<span>{{ reset4count - 1 }}</span>
</div>
<div class="gbr-ml-card">
<span>平均</span>
<span>{{ star4avg }}</span>
</div>
<div class="gbr-ml-card">
<span>统计</span>
<span>{{ star4List.length }}</span>
</div>
</div>
<div class="gbr-ml-card">
<span>平均</span>
<span>{{ star4avg }}</span>
<!-- 5星相关数据 -->
<div v-if="!isNormalPool" class="gbr-mid-list">
<div class="gbr-ml-title s5"></div>
<div class="gbr-ml-card">
<span>已垫</span>
<span>{{ reset5count - 1 }}</span>
</div>
<div class="gbr-ml-card">
<span>平均</span>
<span>{{ star5avg }}</span>
</div>
<div class="gbr-ml-card">
<span>统计</span>
<span>{{ star5List.length }}</span>
</div>
</div>
<div class="gbr-ml-card">
<span>统计</span>
<span>{{ star4List.length }}</span>
<!-- 进度条拼接 -->
<div v-if="props.dataVal.length > 0" class="gbr-mid-progress">
<div v-if="pg2 !== '0'" :style="{ width: pg2 }" :title="`2星占比:${pg2}`" class="s2" />
<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>
<!-- 5星相关数据 -->
<div v-if="!isNormalPool" class="gbr-mid-list">
<div class="gbr-ml-title s5"></div>
<div class="gbr-ml-card">
<span>已垫</span>
<span>{{ reset5count - 1 }}</span>
</div>
<div class="gbr-ml-card">
<span>平均</span>
<span>{{ star5avg }}</span>
</div>
<div class="gbr-ml-card">
<span>统计</span>
<span>{{ star5List.length }}</span>
</div>
</div>
<!-- 进度条拼接 -->
<div v-if="props.dataVal.length > 0" class="gbr-mid-progress">
<div v-if="pg2 !== '0'" :style="{ width: pg2 }" :title="`2星占比:${pg2}`" class="s2" />
<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="gbr-bottom">
<v-tabs v-model="tab" density="compact">
@@ -98,7 +100,8 @@
</div>
</template>
<script lang="ts" setup>
import { computed, onMounted, ref, shallowRef, watch } from "vue";
import { getCurrentWindow } from "@tauri-apps/api/window";
import { computed, nextTick, onMounted, ref, shallowRef, useTemplateRef, watch } from "vue";
import GbrDataLine, { type GbrDataLineProps } from "./gbr-data-line.vue";
@@ -108,8 +111,17 @@ type GachaDataViewProps = {
};
const props = defineProps<GachaDataViewProps>();
const curWin = getCurrentWindow();
const isNormalPool = computed<boolean>(() => props.dataType === "normal");
// Template refs for dynamic height calculation
const gbrDvBoxEl = useTemplateRef<HTMLElement>("gbrDvBoxRef");
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>(""); // 卡片标题
@@ -131,9 +143,38 @@ const pg3 = computed<string>(() => getPg("3"));
const pg4 = computed<string>(() => getPg("4"));
const pg5 = computed<string>(() => getPg("5"));
onMounted(() => {
// Calculate dynamic heights
function calculateHeights(): void {
if (!gbrDvBoxEl.value || !headerEl.value) return;
const containerHeight = gbrDvBoxEl.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 - 8;
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();
},
);
curWin.onResized(async () => {
await nextTick();
calculateHeights();
});
function loadData(): void {
@@ -142,31 +183,86 @@ function loadData(): void {
const temp5Data: Array<GbrDataLineProps> = [];
const temp4Data: Array<GbrDataLineProps> = [];
const temp3Data: Array<GbrDataLineProps> = [];
// 按照 id 升序
// Create a map to store pity counts for each item by ID
const pityMap = new Map<string, { count5: number; count4: number; count3: number }>();
let currentReset5 = 1;
let currentReset4 = 1;
let currentReset3 = 1;
// First pass: calculate pity counts using all pool data
tempData
.sort((a, b) => a.id.localeCompare(b.id))
.forEach((item) => {
if (item.rank === "2") {
currentReset3++;
currentReset4++;
currentReset5++;
} else if (item.rank === "3") {
currentReset4++;
currentReset5++;
pityMap.set(item.id, {
count3: currentReset3,
count4: currentReset4,
count5: currentReset5,
});
currentReset3 = 1;
} else if (item.rank === "4") {
currentReset5++;
pityMap.set(item.id, {
count3: currentReset3,
count4: currentReset4,
count5: currentReset5,
});
currentReset4 = 1;
} else if (item.rank === "5") {
currentReset4++;
pityMap.set(item.id, {
count3: currentReset3,
count4: currentReset4,
count5: currentReset5,
});
currentReset5 = 1;
}
});
// Store current reset counts for display
reset5count.value = currentReset5;
reset4count.value = currentReset4;
reset3count.value = currentReset3;
// Helper function to check if item should be displayed based on dataType
function shouldDisplay(item: TGApp.Sqlite.GachaRecords.TableGachaB): boolean {
if (props.dataType === "normal") return true;
if (props.dataType === "boy") {
return item.opGachaType === "20011" || item.opGachaType === "20012";
}
if (props.dataType === "girl") {
return item.opGachaType === "20021" || item.opGachaType === "20022";
}
return false;
}
// Second pass: build display data, filtering by dataType for event pools
tempData
.sort((a, b) => a.id.localeCompare(b.id))
.forEach((item) => {
// Only process items that should be displayed for this view
if (!shouldDisplay(item)) return;
// 处理时间
if (startDate.value === "" || item.time < startDate.value) startDate.value = item.time;
if (endDate.value === "" || item.time > endDate.value) endDate.value = item.time;
const pityCounts = pityMap.get(item.id);
if (item.rank === "2") {
star2count.value++;
reset3count.value++;
reset4count.value++;
reset5count.value++;
} else if (item.rank === "3") {
reset4count.value++;
reset5count.value++;
temp3Data.push({ data: item, count: reset3count.value });
reset3count.value = 1;
} else if (item.rank === "4") {
reset5count.value++;
temp4Data.push({ data: item, count: reset4count.value });
reset4count.value = 1;
} else if (item.rank === "5") {
reset4count.value++;
temp5Data.push({ data: item, count: reset5count.value });
reset5count.value = 1;
} else if (item.rank === "3" && pityCounts) {
temp3Data.push({ data: item, count: pityCounts.count3 });
} else if (item.rank === "4" && pityCounts) {
temp4Data.push({ data: item, count: pityCounts.count4 });
} else if (item.rank === "5" && pityCounts) {
temp5Data.push({ data: item, count: pityCounts.count5 });
}
});
star5List.value = temp5Data.reverse();
@@ -228,7 +324,7 @@ function getStar3Avg(): string {
// 监听数据变化
watch(
() => props.dataVal,
() => {
async () => {
star5List.value = [];
star4List.value = [];
reset5count.value = 1;
@@ -241,11 +337,14 @@ watch(
star3avg.value = "";
tab.value = isNormalPool.value ? "4" : "5";
loadData();
await nextTick();
calculateHeights();
},
);
</script>
<style lang="css" scoped>
.gbr-dv-container {
position: relative;
height: 100%;
box-sizing: border-box;
padding: 8px;
@@ -253,6 +352,10 @@ watch(
background: var(--box-bg-1);
}
.gbr-dv-header {
position: relative;
}
.gbr-dvt-title {
display: flex;
width: 100%;
@@ -349,7 +452,7 @@ watch(
position: relative;
display: flex;
width: 100%;
height: calc(100% - 150px);
height: v-bind(bottomHeight); /* stylelint-disable-line value-keyword-case */
box-sizing: border-box;
flex-direction: column;
gap: 8px;
@@ -357,7 +460,7 @@ watch(
.gbr-bottom-window {
position: relative;
height: calc(100vh - 428px);
height: v-bind(windowHeight); /* stylelint-disable-line value-keyword-case */
overflow-y: auto;
}

View File

@@ -1,9 +1,8 @@
<!-- 千星奇域祈愿概览组件 -->
<template>
<div class="gro-o-container">
<GbrDataView :data-val="normalData" data-type="normal" />
<GbrDataView :data-val="boyData" data-type="boy" />
<GbrDataView :data-val="girlData" data-type="girl" />
<GbrDataView :data-val="eventData" data-type="boy" />
<GbrDataView :data-val="eventData" data-type="girl" />
</div>
</template>
<script lang="ts" setup>
@@ -15,18 +14,17 @@ type GachaOverviewProps = { modelValue: Array<TGApp.Sqlite.GachaRecords.TableGac
const props = defineProps<GachaOverviewProps>();
const normalData = computed<Array<TGApp.Sqlite.GachaRecords.TableGachaB>>(() =>
props.modelValue.filter((item) => item.opGachaType === "1000"),
props.modelValue.filter((item) => item.gachaType === "1000"),
);
const girlData = computed<Array<TGApp.Sqlite.GachaRecords.TableGachaB>>(() =>
props.modelValue.filter((item) => item.opGachaType === "20011" || item.opGachaType === "20012"),
);
const boyData = computed<Array<TGApp.Sqlite.GachaRecords.TableGachaB>>(() =>
props.modelValue.filter((item) => item.opGachaType === "20021" || item.opGachaType === "20022"),
const eventData = computed<Array<TGApp.Sqlite.GachaRecords.TableGachaB>>(() =>
props.modelValue.filter((item) => item.gachaType === "2000"),
);
</script>
<style lang="css" scoped>
.gro-o-container {
position: relative;
display: grid;
overflow: hidden;
height: 100%;
column-gap: 8px;
grid-template-columns: repeat(3, 1fr);

View File

@@ -425,9 +425,13 @@ async function checkData(): Promise<void> {
}
}
await showLoading.end();
showSnackbar.success(`成功补充遗漏数据${cnt}条,失败${fail}条,即将刷新`);
await new Promise<void>((resolve) => setTimeout(resolve, 1500));
window.location.reload();
if (cnt > 0 || fail > 0) {
showSnackbar.success(`成功补充遗漏数据${cnt}条,失败${fail}条,即将刷新`);
await new Promise<void>((resolve) => setTimeout(resolve, 1500));
window.location.reload();
} else {
showSnackbar.success(`成功检测${gachaListCur.value.length}条数据,无需更新`);
}
}
</script>
<style lang="css" scoped>

View File

@@ -1,30 +1,30 @@
<!-- 千星奇域祈愿记录页面 -->
<!-- 千星奇域祈愿记录页面 TODO处理活动卡池次数共享 -->
<template>
<v-app-bar>
<template #prepend>
<div class="gb-top-title">
<img class="gb-top-byd" src="/icon/nation/千星奇域.webp" alt="byd" />
<img alt="byd" class="gb-top-byd" src="/icon/nation/千星奇域.webp" />
<span>祈愿记录</span>
<v-select
:hide-details="true"
density="compact"
v-model="uidCur"
:hide-details="true"
:items="selectItem"
variant="outlined"
density="compact"
label="游戏UID"
variant="outlined"
/>
<img src="/source/UI/userGacha.webp" alt="gacha" @click="toGacha()" title="祈愿" />
<img alt="gacha" src="/source/UI/userGacha.webp" title="祈愿" @click="toGacha()" />
</div>
</template>
<template #extension>
<div class="gb-top-btns">
<v-btn prepend-icon="mdi-refresh" class="gb-top-btn" @click="confirmRefresh(false)">
<v-btn class="gb-top-btn" prepend-icon="mdi-refresh" @click="confirmRefresh(false)">
增量刷新
</v-btn>
<v-btn prepend-icon="mdi-refresh" class="gb-top-btn" @click="confirmRefresh(true)">
<v-btn class="gb-top-btn" prepend-icon="mdi-refresh" @click="confirmRefresh(true)">
全量刷新
</v-btn>
<v-btn prepend-icon="mdi-delete" class="gb-top-btn" @click="deleteGacha()">删除</v-btn>
<v-btn class="gb-top-btn" prepend-icon="mdi-delete" @click="deleteGacha()">删除</v-btn>
</div>
</template>
</v-app-bar>
@@ -33,16 +33,16 @@
<v-tab value="overview">数据概览</v-tab>
<v-tab value="table">数据表格</v-tab>
<!-- TODO: 暂时隐藏内置祈愿链接 -->
<v-tab value="iframe" v-if="false">祈愿详情</v-tab>
<v-tab v-if="false" value="iframe">祈愿详情</v-tab>
</v-tabs>
<v-window v-model="tab" class="gb-window">
<v-window-item value="overview" class="gb-window-item">
<v-window-item class="gb-window-item" value="overview">
<gbr-overview v-model="gachaListCur" />
</v-window-item>
<v-window-item value="table" class="gb-window-item">
<v-window-item class="gb-window-item" value="table">
<gbr-table v-model="gachaListCur" />
</v-window-item>
<v-window-item value="iframe" class="gb-window-item">
<v-window-item class="gb-window-item" value="iframe">
<gro-iframe mode="beyond" />
</v-window-item>
</v-window>