千星奇域页面

This commit is contained in:
BTMuli
2025-10-27 19:42:02 +08:00
parent 6eab6c81f1
commit a368223805
13 changed files with 1222 additions and 176 deletions

View File

@@ -0,0 +1,144 @@
<!-- 千星奇域概览单项组件 -->
<template>
<div class="gbr-dl-box">
<div class="gbr-dl-progress" />
<div class="gbr-dl-icon">
<img :alt="props.data.name" :src="getIcon()" />
</div>
<div class="gbr-dl-base">
<div class="gbr-dl-name">{{ props.data.name }}</div>
<div class="gbr-dl-time">{{ props.data.time }}</div>
</div>
<div class="gbr-dl-info">
<div class="gbr-dl-cnt">{{ props.count }}</div>
<div class="gbr-dl-hint" v-if="hint !== ''">{{ hint }}</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed } from "vue";
export type GbrDataLineProps = { data: TGApp.Sqlite.GachaRecords.TableGachaB; count: number };
const props = defineProps<GbrDataLineProps>();
const hint = getEndHint();
function getIcon(): string {
console.log(props.data);
// const find = AppGachaBData.find((i) => i.id.toString() === props.data.itemId);
// if (!find) return `/source/UI/paimon.webp`;
// return `https://api.hakush.in/gi/UI/${find.icon}.webp`;
// TODO: 缺失元数据
return `/source/UI/paimon.webp`;
}
function getEndHint(): string {
if (props.data.gachaType === "1000") return "";
if (!props.data.isUp) 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";
return "#61afef";
});
const progressWidth = computed<string>(() => {
let final = 10;
if (props.data.rank === "5") {
if (props.data.gachaType === "302") final = 80;
else final = 90;
} else if (props.data.rank === "4") final = 10;
else return "0%";
return ((props.count / final) * 100).toFixed(2) + "%";
});
</script>
<style lang="scss" scoped>
.gbr-dl-box {
position: relative;
display: flex;
width: 100%;
height: 48px;
box-sizing: border-box;
align-items: center;
justify-content: flex-start;
padding: 8px;
border: 1px solid var(--common-shadow-1);
border-radius: 4px;
background: var(--box-bg-2);
column-gap: 4px;
}
.gbr-dl-progress {
position: absolute;
bottom: 0;
left: 0;
width: v-bind(progressWidth); /* stylelint-disable-line value-keyword-case */
max-width: 100%;
height: 4px;
border-radius: 4px;
background: v-bind(progressColor); /* stylelint-disable-line value-keyword-case */
}
.gbr-dl-icon {
display: flex;
width: 32px;
height: 32px;
flex-shrink: 0;
align-items: center;
justify-content: center;
img {
width: 100%;
height: 100%;
}
}
.gbr-dl-base {
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: center;
}
.gbr-dl-name {
color: var(--common-text-title);
font-family: var(--font-title);
font-size: 14px;
line-height: 18px;
}
.gbr-dl-time {
color: var(--box-text-7);
font-size: 12px;
line-height: 14px;
}
.gbr-dl-info {
display: flex;
align-items: center;
justify-content: center;
margin-left: auto;
column-gap: 4px;
}
.gbr-dl-cnt {
color: var(--common-text-title);
font-family: var(--font-title);
}
.gbr-dl-hint {
display: flex;
width: 32px;
height: 32px;
align-items: center;
justify-content: center;
padding: 4px;
border-radius: 50%;
background: var(--box-bg-3);
color: v-bind(progressColor); /* stylelint-disable-line value-keyword-case */
font-family: var(--font-title);
transform: rotate(25deg);
}
</style>

View File

@@ -0,0 +1,263 @@
<!-- 千星奇域概览数据视图组件 -->
<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>
<div class="gbr-mid-list">
<div class="gbr-ml-item">
<span>4已垫</span>
<span>{{ reset4count - 1 }}</span>
</div>
<div class="gbr-ml-item">
<span>5已垫</span>
<span>{{ reset5count - 1 }}</span>
</div>
<div class="gbr-ml-item">
<span>5平均</span>
<span>{{ star5avg }}</span>
</div>
</div>
<div class="gbr-mid-list">
<div class="gbr-ml-item">
<span>5统计</span>
<span>{{ getTitle("5") }}</span>
</div>
<div class="gbr-ml-item">
<span>4统计</span>
<span>{{ getTitle("4") }}</span>
</div>
</div>
<!-- 这边放具体物品的列表 -->
<div class="gbr-bottom">
<v-tabs v-model="tab" density="compact">
<v-tab value="5">5</v-tab>
<v-tab value="4">4</v-tab>
<v-tab value="3">3</v-tab>
</v-tabs>
<v-window v-model="tab" class="gbr-bottom-window">
<v-window-item value="5" class="gbr-b-window-item">
<v-virtual-scroll :items="star5List" :item-height="48">
<template #default="{ item }">
<GbrDataLine :data="item.data" :count="item.count" />
</template>
</v-virtual-scroll>
</v-window-item>
<v-window-item value="4" class="gbr-b-window-item">
<v-virtual-scroll :items="star4List" :item-height="48">
<template #default="{ item }">
<GbrDataLine :data="item.data" :count="item.count" />
</template>
</v-virtual-scroll>
</v-window-item>
<v-window-item value="3" class="gbr-b-window-item">
<v-virtual-scroll :items="star3List" :item-height="48">
<template #default="{ item }">
<GbrDataLine :data="item.data" :count="item.count" />
</template>
</v-virtual-scroll>
</v-window-item>
</v-window>
</div>
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref, shallowRef, watch } from "vue";
import GbrDataLine, { type GbrDataLineProps } from "./gbr-data-line.vue";
type GachaDataViewProps = {
dataType: "normal" | "boy" | "girl";
dataVal: Array<TGApp.Sqlite.GachaRecords.TableGachaB>;
};
const props = defineProps<GachaDataViewProps>();
// data
const loading = ref<boolean>(true); // 是否加载完
const title = ref<string>(""); // 卡片标题
const startDate = ref<string>(""); // 最早的时间
const endDate = ref<string>(""); // 最晚的时间
const star5List = shallowRef<Array<GbrDataLineProps>>([]); // 5星物品数据
const star4List = shallowRef<Array<GbrDataLineProps>>([]); // 4星物品数据
const star3List = shallowRef<Array<GbrDataLineProps>>([]);
const reset5count = ref<number>(1); // 5星垫抽数量
const reset4count = ref<number>(1); // 4星垫抽数量
const reset3count = ref<number>(1); // 3星垫抽数量
const star3count = ref<number>(0); // 3星物品数量
const star5avg = ref<string>(""); // 5星平均抽数
const tab = ref<string>("5"); // tab
onMounted(() => {
loadData();
loading.value = false;
});
function loadData(): void {
title.value = getTitle("top");
const tempData = props.dataVal;
const temp5Data: Array<GbrDataLineProps> = [];
const temp4Data: Array<GbrDataLineProps> = [];
const temp3Data: Array<GbrDataLineProps> = [];
// 按照 id 升序
tempData
.sort((a, b) => a.id.localeCompare(b.id))
.forEach((item) => {
// 处理时间
if (startDate.value === "" || item.time < startDate.value) startDate.value = item.time;
if (endDate.value === "" || item.time > endDate.value) endDate.value = item.time;
if (item.rank === "2") {
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;
}
});
star5List.value = temp5Data.reverse();
star4List.value = temp4Data.reverse();
star3List.value = temp3Data.reverse();
star5avg.value = getStar5Avg();
}
// 获取标题
function getTitle(type: "top" | "5" | "4" | "3"): string {
if (type === "top") {
if (props.dataType === "normal") return "常驻颂愿";
if (props.dataType === "boy") return "活动颂愿(男)";
if (props.dataType === "girl") return "活动颂愿(女)";
return "";
}
if (props.dataVal.length === 0) return "暂无数据";
if (type === "5") {
// 5星物品统计 00.00%
return `${star5List.value.length} [${((star5List.value.length * 100) / props.dataVal.length)
.toFixed(2)
.padStart(5, "0")}%]`;
}
if (type === "4") {
// 4星物品统计
return `${star4List.value.length} [${((star4List.value.length * 100) / props.dataVal.length)
.toFixed(2)
.padStart(5, "0")}%]`;
}
// 3星物品统计
return `${star3count.value} [${((star3count.value * 100) / props.dataVal.length)
.toFixed(2)
.padStart(5, "0")}%]`;
}
// 获取5星平均抽数
function getStar5Avg(): string {
const resetList = star5List.value.map((item) => item.count);
if (resetList.length === 0) return "0";
const total = resetList.reduce((a, b) => a + b);
return (total / star5List.value.length).toFixed(2);
}
// 监听数据变化
watch(
() => props.dataVal,
() => {
star5List.value = [];
star4List.value = [];
reset5count.value = 1;
reset4count.value = 1;
star3count.value = 1;
startDate.value = "";
endDate.value = "";
star5avg.value = "";
tab.value = "5";
loadData();
},
);
</script>
<style lang="css" scoped>
.gbr-dv-container {
height: 100%;
box-sizing: border-box;
padding: 8px;
border-radius: 4px;
background: var(--box-bg-1);
}
.gbr-dvt-title {
display: flex;
width: 100%;
align-items: center;
justify-content: space-between;
color: var(--common-text-title);
font-family: var(--font-title);
font-size: 18px;
}
.gbr-dvt-subtitle {
width: 100%;
font-family: var(--font-text);
font-size: 12px;
opacity: 0.6;
}
.gbr-mid-list {
padding-top: 4px;
padding-bottom: 4px;
border-top: 1px solid var(--common-shadow-4);
color: var(--box-text-7);
}
.gbr-ml-item {
display: flex;
width: 100%;
align-items: center;
justify-content: space-between;
font-family: var(--font-title);
font-size: 14px;
}
.gbr-bottom {
position: relative;
display: flex;
width: 100%;
height: calc(100% - 150px);
box-sizing: border-box;
flex-direction: column;
gap: 8px;
}
.gbr-bottom-window {
position: relative;
height: calc(100vh - 428px);
overflow-y: auto;
}
.gbr-b-window-item {
position: relative;
width: 100%;
box-sizing: border-box;
padding-right: 4px;
}
/* stylelint-disable selector-class-pattern */
:deep(.v-virtual-scroll__item + .v-virtual-scroll__item) {
margin-top: 8px;
}
/* stylelint-enable selector-class-pattern */
</style>

View File

@@ -0,0 +1,34 @@
<!-- 千星奇域祈愿概览组件 -->
<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" />
</div>
</template>
<script lang="ts" setup>
import { computed } from "vue";
import GbrDataView from "./gbr-data-view.vue";
type GachaOverviewProps = { modelValue: Array<TGApp.Sqlite.GachaRecords.TableGachaB> };
const props = defineProps<GachaOverviewProps>();
const normalData = computed<Array<TGApp.Sqlite.GachaRecords.TableGachaB>>(() =>
props.modelValue.filter((item) => item.opGachaType === "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"),
);
</script>
<style lang="css" scoped>
.gro-o-container {
display: grid;
height: 100%;
column-gap: 8px;
grid-template-columns: repeat(3, 1fr);
}
</style>

View File

@@ -0,0 +1,62 @@
<!-- 千星奇域数据表格 -->
<template>
<v-data-table
:headers="headers"
:items="props.modelValue"
fixed-header
fixed-footer
class="gbr-t-box"
>
<template v-slot:item="{ item }">
<tr class="gbr-t-tr">
<td>{{ item.time }}</td>
<td>{{ getPool(item.opGachaType) }}</td>
<td>{{ item.type }}</td>
<td>{{ item.name }}</td>
<td>{{ item.rank }}</td>
</tr>
</template>
</v-data-table>
</template>
<script lang="ts" setup>
type GroTableProps = { modelValue: Array<TGApp.Sqlite.GachaRecords.TableGachaB> };
const props = defineProps<GroTableProps>();
const headers = <const>[
{ title: "时间", align: "center", key: "time" },
{ title: "卡池", align: "center", key: "opGachaType" },
{ title: "类型", align: "center", key: "type" },
{ title: "名称", align: "center", key: "name" },
{ title: "星级", align: "center", key: "rank" },
];
function getPool(type: string) {
switch (type) {
case "1000":
return "常驻颂愿";
case "2000":
return "活动颂愿";
case "20011":
case "20012":
return "活动颂愿-男";
case "20021":
case "20022":
return "活动颂愿-女";
default:
return "未知";
}
}
</script>
<style lang="css" scoped>
.gbr-t-box {
height: calc(100vh - 200px);
padding-right: 5px;
border-radius: 5px;
overflow-y: auto;
}
.gbr-t-tr {
text-align: center;
}
</style>

View File

@@ -35,22 +35,28 @@ const GachaIdMap: Record<string, string> = {
"20021": "57016dec6b768231ba1342c01935417a799b", // 千星奇域角色活动-女
};
const tabNormal: ReadonlyArray<GroTab> = [
{ label: "常驻祈愿", value: "200" },
{ label: "角色活动祈愿", value: "301" },
{ label: "武器活动祈愿", value: "302" },
{ label: "角色活动祈愿-2", value: "400" },
];
const tabBeyond: ReadonlyArray<GroTab> = [
{ label: "常驻颂愿", value: "1000", beyond: true },
{ label: "活动颂愿-男", value: "20011", beyond: true },
{ label: "活动颂愿-女", value: "20021", beyond: true },
];
type GroTabKey = keyof typeof GachaIdMap;
type GroTab = { label: string; value: string; beyond?: boolean };
type GroIframeProps = { mode: "normal" | "beyond" };
const props = defineProps<GroIframeProps>();
const { cookie, account } = storeToRefs(useUserStore());
const authkey = ref<string>("");
const link = ref<string>("");
const poolTab = ref<GroTabKey>("200");
const tabList = shallowRef<ReadonlyArray<GroTab>>([
{ label: "常驻祈愿", value: "200" },
{ label: "角色活动祈愿", value: "301" },
{ label: "武器活动祈愿", value: "302" },
{ label: "角色活动祈愿-2", value: "400" },
{ label: "常驻颂愿", value: "1000", beyond: true },
{ label: "活动颂愿-男", value: "20011", beyond: true },
{ label: "活动颂愿-女", value: "20021", beyond: true },
]);
const tabList = shallowRef<ReadonlyArray<GroTab>>(props.mode === "beyond" ? tabBeyond : tabNormal);
onMounted(async () => {
link.value = await getUrl();
@@ -107,7 +113,7 @@ async function refreshAuthkey(): Promise<void> {
display: flex;
width: 100%;
height: 100%;
align-items: center;
align-items: flex-start;
justify-content: space-between;
}

View File

@@ -1,3 +1,4 @@
<!-- 祈愿记录页面 -->
<template>
<v-app-bar>
<template #prepend>
@@ -12,6 +13,7 @@
variant="outlined"
label="游戏UID"
/>
<img src="/icon/nation/千星奇域.webp" alt="byd" @click="toBeyond()" title="千星奇域" />
</div>
</template>
<template #extension>
@@ -56,7 +58,7 @@
<gro-history />
</v-window-item>
<v-window-item value="iframe" class="gacha-window-item">
<gro-iframe />
<gro-iframe mode="normal" />
</v-window-item>
</v-window>
</div>
@@ -83,9 +85,12 @@ import TGLogger from "@utils/TGLogger.js";
import { exportUigfData, readUigfData, verifyUigfData } from "@utils/UIGF.js";
import { storeToRefs } from "pinia";
import { onMounted, ref, shallowRef, watch } from "vue";
import { useRouter } from "vue-router";
import { AppCharacterData, AppWeaponData } from "@/data/index.js";
const router = useRouter();
const { isLogin } = storeToRefs(useAppStore());
const { account, cookie } = storeToRefs(useUserStore());
@@ -130,6 +135,10 @@ watch(
},
);
async function toBeyond(): Promise<void> {
await router.push({ name: "千星奇域祈愿记录" });
}
// 刷新按钮点击事件
async function confirmRefresh(force: boolean): Promise<void> {
await TGLogger.Info(`[UserGacha][${account.value.gameUid}][confirmRefresh] 刷新祈愿数据`);

341
src/pages/User/GachaB.vue Normal file
View File

@@ -0,0 +1,341 @@
<!-- 千星奇域祈愿记录页面 -->
<template>
<v-app-bar>
<template #prepend>
<div class="gb-top-title">
<img src="/icon/nation/千星奇域.webp" alt="gacha" />
<span>祈愿记录</span>
<v-select
:hide-details="true"
density="compact"
v-model="uidCur"
:items="selectItem"
variant="outlined"
label="游戏UID"
/>
<img src="/source/UI/userGacha.webp" alt="byd" @click="toGacha()" title="祈愿" />
</div>
</template>
<template #extension>
<div class="gb-top-btns">
<v-btn prepend-icon="mdi-refresh" class="gb-top-btn" @click="confirmRefresh(false)">
增量刷新
</v-btn>
<v-btn prepend-icon="mdi-refresh" class="gb-top-btn" @click="confirmRefresh(true)">
全量刷新
</v-btn>
<v-btn prepend-icon="mdi-delete" class="gb-top-btn" @click="deleteGacha()">删除</v-btn>
</div>
</template>
</v-app-bar>
<div class="gb-container">
<v-tabs v-model="tab" align-tabs="start" class="gb-tab" density="compact">
<v-tab value="overview">数据概览</v-tab>
<v-tab value="table">数据表格</v-tab>
<v-tab value="iframe" v-if="isLogin">祈愿详情</v-tab>
</v-tabs>
<v-window v-model="tab" class="gb-window">
<v-window-item value="overview" class="gb-window-item">
<gbr-overview v-model="gachaListCur" />
</v-window-item>
<v-window-item value="table" class="gb-window-item">
<gbr-table v-model="gachaListCur" />
</v-window-item>
<v-window-item value="iframe" class="gb-window-item">
<gro-iframe mode="beyond" />
</v-window-item>
</v-window>
</div>
</template>
<script lang="ts" setup>
import showDialog from "@comp/func/dialog.js";
import showLoading from "@comp/func/loading.js";
import showSnackbar from "@comp/func/snackbar.js";
import GbrOverview from "@comp/userGacha/gbr-overview.vue";
import GbrTable from "@comp/userGacha/gbr-table.vue";
import GroIframe from "@comp/userGacha/gro-iframe.vue";
import hk4eReq from "@req/hk4eReq.js";
import takumiReq from "@req/takumiReq.js";
import TSUserGachaB from "@Sqlm/userGachaB.js";
import useAppStore from "@store/app.js";
import useUserStore from "@store/user.js";
import TGLogger from "@utils/TGLogger.js";
import { storeToRefs } from "pinia";
import { onMounted, ref, shallowRef, watch } from "vue";
import { useRouter } from "vue-router";
const router = useRouter();
const { isLogin } = storeToRefs(useAppStore());
const { account, cookie } = storeToRefs(useUserStore());
const authkey = ref<string>("");
const uidCur = ref<string>();
const tab = ref<string>("overview");
const selectItem = shallowRef<Array<string>>([]);
const gachaListCur = shallowRef<Array<TGApp.Sqlite.GachaRecords.TableGachaB>>([]);
onMounted(async () => {
await TGLogger.Info("[UserGachaB][onMounted] 进入千星奇域页面");
await showLoading.start("正在加载千星奇域祈愿数据", "正在获取UID列表...");
selectItem.value = await TSUserGachaB.getUidList();
await TGLogger.Info(`[UserGachaB][onMounted] 总UID数${selectItem.value.length}`);
if (selectItem.value.length === 0) {
await showLoading.end();
await TGLogger.Info("[UserGachaB][onMounted] UID列表为空");
return;
}
uidCur.value = selectItem.value[0];
await TGLogger.Info(`[UserGachaB][onMounted] 当前UID:${uidCur.value}`);
await showLoading.update(`UID: ${uidCur.value}`);
gachaListCur.value = await TSUserGachaB.getGachaRecords(uidCur.value);
await TGLogger.Info(`[UserGachaB][onMounted] 祈愿记录数: ${gachaListCur.value.length}`);
await showLoading.end();
showSnackbar.success(`加载完成,共 ${gachaListCur.value.length} 条祈愿记录`);
});
watch(
() => uidCur.value,
async (newUid) => {
if (!newUid) return;
gachaListCur.value = await TSUserGachaB.getGachaRecords(newUid);
showSnackbar.success(`成功获取 ${gachaListCur.value.length} 条祈愿数据`);
await TGLogger.Info(
`[UserGachaB][${newUid}][watch] 成功获取 ${gachaListCur.value.length} 条祈愿数据`,
);
},
);
/**
* 跳转至祈愿页面
*/
async function toGacha(): Promise<void> {
await router.push({ name: "祈愿记录" });
}
/**
* 刷新祈愿数据
* @param {boolean} force 是否强制刷新
* @return {Promise<void>} void
*/
async function confirmRefresh(force: boolean): Promise<void> {
if (!isLogin.value || !cookie.value) {
showSnackbar.warn("请先登录账号");
return;
}
await TGLogger.Info(`[UserGachaB][${account.value.gameUid}] 开始刷新千星奇域祈愿数据`);
if (uidCur.value && uidCur.value !== account.value.gameUid) {
const switchCheck = await showDialog.check(
"是否切换游戏账户",
`确认则尝试切换至 ${uidCur.value}`,
);
if (switchCheck) {
await useUserStore().switchGameAccount(uidCur.value);
await confirmRefresh(force);
return;
}
const freshCheck = await showDialog.check(
"确定刷新?",
`用户${account.value.gameUid}与当前UID${uidCur.value}不一致`,
);
if (!freshCheck) {
showSnackbar.cancel("已取消祈愿数据刷新");
return;
}
}
await showLoading.start(`正在刷新祈愿数据`, `UID:${account.value.gameUid},正在获取 authkey`);
const authkeyRes = await takumiReq.bind.authKey(cookie.value, account.value);
if (typeof authkeyRes === "string") {
authkey.value = authkeyRes;
await TGLogger.Info(`[UserGacha][${account.value.gameUid}] 成功获取 authkey`);
} else {
showSnackbar.error("获取 authkey 失败");
await TGLogger.Error(`[UserGacha][${account.value.gameUid}] 获取 authkey 失败`);
await TGLogger.Error(
`[UserGachaB][${account.value.gameUid}] ${authkeyRes.retcode} ${authkeyRes.message}`,
);
await showLoading.end();
return;
}
await refreshGachaPool("1000", "常驻颂愿", force);
await refreshGachaPool("20011", "活动颂愿·男", force);
await refreshGachaPool("20012", "活动颂愿·男2", force);
await refreshGachaPool("20021", "活动颂愿·女", force);
await refreshGachaPool("20022", "活动颂愿·女2", force);
await showLoading.end();
await TGLogger.Info(`[UserGacha][${account.value.gameUid}] 刷新祈愿数据完成`);
showSnackbar.success("祈愿数据刷新完成,即将刷新页面");
await new Promise<void>((resolve) => setTimeout(resolve, 1500));
window.location.reload();
}
/**
* 刷新指定祈愿池数据
* @param {string} gachaType 祈愿池类型
* @param {string} gachaName 祈愿池名称
* @param {boolean} force 是否强制刷新
* @return {Promise<void>} void
*/
async function refreshGachaPool(
gachaType: string,
gachaName: string,
force: boolean,
): Promise<void> {
let endId = "0";
let reqId = "0";
let page = 0;
await showLoading.start(`正在刷新${gachaName}数据`, "");
if (!force) {
endId = (await TSUserGachaB.getGachaCheck(account.value.gameUid, gachaType)) ?? "0";
}
while (true) {
page++;
const gachaRes = await hk4eReq.gachaB(authkey.value, gachaType, reqId);
if (!Array.isArray(gachaRes)) {
showSnackbar.error(`[${gachaType}][${gachaRes.retcode}] ${gachaRes.message}`);
await TGLogger.Error(
`[UserGachaB][${account.value.gameUid}][refreshGachaPool] 获取祈愿数据失败`,
);
await TGLogger.Error(
`[UserGachaB][${account.value.gameUid}][refreshGachaPool] ${gachaRes.retcode} ${gachaRes.message}`,
);
await new Promise<void>((resolve) => setTimeout(resolve, 1000));
break;
}
if (gachaRes.length === 0) {
// if (force) {
// await showLoading.update(`正在清理${label}数据`);
// if (gachaDataMap) {
// await TSUserGacha.cleanGachaRecords(account.value.gameUid, type, gachaDataMap);
// }
// }
break;
}
if (force) await showLoading.update(`[${gachaName}] 第${page}页,${gachaRes.length}`);
for (const item of gachaRes) {
if (!force) {
await showLoading.update(`[${item.item_type}][${item.time}] ${item.item_name}`);
}
if (force) {
// if (!gachaDataMap) gachaDataMap = {};
// if (!gachaDataMap[item.time]) gachaDataMap[item.time] = [];
// gachaDataMap[item.time].push(item.id.toString());
}
}
await TSUserGachaB.insertGachaList(gachaRes);
if (!force && gachaRes.some((i) => i.id.toString() === endId.toString())) break;
reqId = gachaRes[gachaRes.length - 1].id.toString();
if (force) await new Promise<void>((resolve) => setTimeout(resolve, 1000));
}
}
/**
* 删除当前UID的祈愿数据
* @return {Promise<void>} void
*/
async function deleteGacha(): Promise<void> {
if (gachaListCur.value.length === 0 || !uidCur.value) {
showSnackbar.error("暂无祈愿数据");
return;
}
await TGLogger.Info(`[UserGachaB][${uidCur.value}][deleteGacha] 删除祈愿数据`);
const delCheck = await showDialog.check(
"确定删除祈愿数据?",
`UID${uidCur.value},共 ${gachaListCur.value.length} 条数据`,
);
if (!delCheck) {
showSnackbar.cancel("已取消祈愿数据删除");
await TGLogger.Info(`[UserGachaB][${uidCur.value}][deleteGacha] 已取消祈愿数据删除`);
return;
}
const uidList = await TSUserGachaB.getUidList();
if (uidList.length <= 1) {
const forceCheck = await showDialog.check("删除后数据库将为空,确定删除?");
if (!forceCheck) {
showSnackbar.cancel("已取消祈愿数据删除");
return;
}
}
await showLoading.start("正在删除祈愿数据", `UID:${uidCur.value}`);
await TSUserGachaB.deleteRecords(uidCur.value);
await showLoading.end();
showSnackbar.success(`已成功删除 ${uidCur.value} 的祈愿数据,即将刷新页面`);
await TGLogger.Info(
`[UserGachaB][${uidCur.value}][deleteGacha] 成功删除 ${gachaListCur.value.length} 条祈愿数据`,
);
await new Promise<void>((resolve) => setTimeout(resolve, 1500));
window.location.reload();
}
</script>
<style lang="scss" scoped>
.gb-top-title {
display: flex;
align-items: center;
justify-content: center;
margin-left: 12px;
column-gap: 8px;
img {
width: 32px;
height: 32px;
&:last-child {
cursor: pointer;
}
}
span {
color: var(--common-text-title);
font-family: var(--font-title);
font-size: 20px;
}
}
.gb-top-btns {
display: flex;
margin-left: 16px;
column-gap: 8px;
}
.gb-top-btn {
border-radius: 4px;
background: var(--tgc-btn-1);
color: var(--btn-text);
}
.dark .gb-top-btn {
border: 1px solid var(--common-shadow-2);
}
.gb-container {
display: flex;
height: calc(100vh - 144px);
flex-direction: column;
align-items: flex-start;
justify-content: center;
border: 1px solid var(--common-shadow-2);
border-radius: 4px;
background: var(--app-page-bg);
}
.gb-tab {
height: 40px;
color: var(--box-text-4);
font-family: var(--font-title);
img {
width: 16px;
height: 16px;
margin-right: 4px;
}
}
.gb-window {
width: 100%;
height: 100%;
padding: 8px;
}
.gb-window-item {
height: 100%;
}
</style>

View File

@@ -1,7 +1,6 @@
/**
* @file plugins/Sqlite/index.ts
* @description Sqlite 数据库操作类
* @since Beta v0.8.0
* Sqlite 数据库操作类
* @since Beta v0.8.4
*/
import { app } from "@tauri-apps/api";
@@ -17,10 +16,11 @@ class Sqlite {
"Achievements",
"AppData",
"GachaRecords",
"GachaBRecords",
"GameAccount",
"SpiralAbyss",
"RoleCombat",
"HardChallenge",
"RoleCombat",
"SpiralAbyss",
"UFCollection",
"UFMap",
"UFPost",
@@ -39,7 +39,7 @@ class Sqlite {
private constructor() {}
/**
* @description 获取数据库实例
* 获取数据库实例
* @since Beta v0.3.3
* @returns {Promise<Database>}
*/
@@ -49,7 +49,7 @@ class Sqlite {
}
/**
* @description 检测是否需要创建数据库
* 检测是否需要创建数据库
* @since Beta v0.6.1
* @returns {Promise<boolean>}
*/
@@ -70,7 +70,7 @@ class Sqlite {
}
/**
* @description 初始化数据库
* 初始化数据库
* @since Beta v0.4.5
* @returns {Promise<void>}
*/
@@ -81,7 +81,7 @@ class Sqlite {
}
/**
* @description 获取数据库信息
* 获取数据库信息
* @since Beta v0.3.3
* @returns {Promise<TGApp.Sqlite.AppData.Item[]>}
*/
@@ -92,7 +92,7 @@ class Sqlite {
}
/**
* @description 对比数据判断是否需要更新
* 对比数据判断是否需要更新
* @since Beta v0.3.3
* @returns {Promise<boolean>}
*/
@@ -105,7 +105,7 @@ class Sqlite {
}
/**
* @description 保存 appData
* 保存 appData
* @since Beta v0.3.3
* @param {string} key
* @param {string} value
@@ -118,7 +118,7 @@ class Sqlite {
}
/**
* @description 已有数据表跟触发器不变的情况下,更新数据库数据
* 已有数据表跟触发器不变的情况下,更新数据库数据
* @since Beta v0.3.3
* @returns {Promise<void>}
*/
@@ -131,7 +131,7 @@ class Sqlite {
}
/**
* @description 更新 SpiralAbyss 表
* 更新 SpiralAbyss 表
* @since Beta v0.6.1
* @returns {Promise<void>}
*/
@@ -147,7 +147,7 @@ class Sqlite {
}
/**
* @description 重置数据库
* 重置数据库
* @since Beta v0.4.0
* @returns {Promise<void>}
*/

View File

@@ -0,0 +1,161 @@
/**
* 千星奇域祈愿模块
* @since Beta v0.8.4
*/
import showSnackbar from "@comp/func/snackbar.js";
import TGSqlite from "@Sql/index.js";
import { exists, mkdir } from "@tauri-apps/plugin-fs";
import TGLogger from "@utils/TGLogger.js";
/**
* 获取导入 Sql
* @since Beta v0.8.4
* @param {TGApp.Game.Gacha.GachaBItem} gacha - 抽卡记录数据
* @returns {string}
*/
function getInsertSql(gacha: TGApp.Game.Gacha.GachaBItem): string {
return `
INSERT INTO GachaBRecords(id, uid, region, scheduleId, gachaType,
opGachaType, time, itemId, name, type,
rank, isUp, updated)
VALUES ('${gacha.id}', '${gacha.uid}', '${gacha.region}', '${gacha.schedule_id}',
'${gacha.op_gacha_type === "1000" ? "1000" : "2000"}', '${gacha.op_gacha_type}', '${gacha.time}',
'${gacha.item_id}', '${gacha.item_name}', '${gacha.item_type}',
'${gacha.rank_type}', '${gacha.is_up}', datetime('now', 'localtime'))
ON CONFLICT (id)
DO UPDATE
SET uid = '${gacha.uid}',
region = '${gacha.region}',
scheduleId = '${gacha.schedule_id}',
gachaType = '${gacha.op_gacha_type === "1000" ? "1000" : "2000"}',
opGachaType = '${gacha.op_gacha_type}',
time = '${gacha.time}',
itemId = '${gacha.item_id}',
name = '${gacha.item_name}',
type = '${gacha.item_type}',
rank = '${gacha.rank_type}',
isUp = '${gacha.is_up}',
updated = datetime('now', 'localtime');
`;
}
/**
* 插入列表数据
* @since Beta v0.8.4
* @param {Array<TGApp.Game.Gacha.GachaBItem>} list - 抽卡记录列表
* @returns {Promise<void>}
*/
async function insertGachaList(list: Array<TGApp.Game.Gacha.GachaBItem>): Promise<void> {
const db = await TGSqlite.getDB();
for (const gacha of list) {
const sql = getInsertSql(gacha);
await db.execute(sql);
}
}
/**
* 获取数据库UID列表
* @since Beta v0.8.4
* @returns {Promise<Array<string>>}
*/
async function getUidList(): Promise<Array<string>> {
const db = await TGSqlite.getDB();
type resType = Array<{ uid: string }>;
const res = await db.select<resType>("SELECT DISTINCT uid FROM GachaBRecords;");
return res.map((i) => i.uid);
}
/**
* 获取增量更新的记录 ID
* @since Beta v0.8.4
* @param {string} uid - UID
* @param {string} type - 类型
* @returns {Promise<string|undefined>}
*/
async function getGachaCheck(uid: string, type: string): Promise<string | undefined> {
const db = await TGSqlite.getDB();
type resType = Array<{ id: string }>;
const res = await db.select<resType>(
"SELECT id FROM GachaBRecords WHERE uid = ? AND opGachaType = ? ORDER BY id DESC LIMIT 1;",
[uid, type],
);
if (res.length === 0) return undefined;
return res[0].id;
}
/**
* 获取用户祈愿记录
* @since Beta v0.8.4
* @param {string} uid - UID
* @param {string} [type] - 类型
* @return {Promise<Array<TGApp.Sqlite.GachaRecords.TableGachaB>>}
*/
async function getGachaRecords(
uid: string,
type?: string,
): Promise<Array<TGApp.Sqlite.GachaRecords.TableGachaB>> {
const db = await TGSqlite.getDB();
if (type) {
return await db.select("SELECT * FROM GachaBRecords WHERE uid = ? AND opGachaType = ?;", [
uid,
type,
]);
}
return await db.select("SELECT * FROM GachaBRecords WHERE uid = ?;", [uid]);
}
/**
* 备份祈愿数据
* @since Beta v0.8.4
* @param {string} dir - 备份目录
* @remarks 等UIGF标准最终确定后与TSUserGacha合并
*/
async function backUpUigf(dir: string): Promise<void> {
if (!(await exists(dir))) {
await TGLogger.Warn("不存在指定的祈愿备份目录,即将创建");
await mkdir(dir, { recursive: true });
}
showSnackbar.error(`千星奇域祈愿数据备份功能尚未实现,请耐心等待后续版本更新。`);
}
/**
* 恢复祈愿数据
* @since Beta v0.8.4
* @param {string} dir - 恢复目录
* @remarks 等UIGF标准最终确定后与TSUserGacha合并
*/
async function restoreUigf(dir: string): Promise<boolean> {
if (!(await exists(dir))) {
await TGLogger.Warn("不存在指定的祈愿备份目录");
return false;
}
return true;
}
/**
* 删除用户祈愿数据
* @since Beta v0.8.4
* @param {string} uid - UID
* @returns {Promise<void>}
*/
async function deleteRecords(uid: string): Promise<void> {
const db = await TGSqlite.getDB();
await db.execute("DELETE FROM GachaBRecords WHERE uid = ?;", [uid]);
}
/**
* 千星奇域祈愿模块
* @since Beta v0.8.4
*/
const TSUserGachaB = {
getUidList,
getGachaCheck,
getGachaRecords,
insertGachaList,
backUpUigf,
restoreUigf,
deleteRecords,
};
export default TSUserGachaB;

View File

@@ -1,6 +1,5 @@
-- @file plugins/Sqlite/sql/createTable.sql
-- @brief sqlite数据库创建表语句
-- @since Beta v0.8.0
-- @since Beta v0.8.4
-- @brief 创建成就数据表
create table if not exists Achievements
@@ -148,6 +147,24 @@ create table if not exists GachaRecords
updated text
);
-- @brief 创建千星奇域祈愿数据表
create table if not exists GachaBRecords
(
id text primary key not null,
uid text,
region text,
scheduleId text,
gachaType text,
opGachaType text,
time text,
itemId text,
name text,
type text,
rank text,
isUp text,
updated text
);
-- @brief 创建用户帖子收藏
create table if not exists UFPost
(

View File

@@ -150,7 +150,7 @@ async function getBeyondGachaLog(
authkey_ver: "1",
sign_type: "2",
gacha_type: gachaType,
size: "20",
size: "5",
end_id: endId,
};
const resp = await TGHttp<TGApp.Game.Gacha.GachaBLogResp | TGApp.BBS.Response.Base>(

View File

@@ -31,6 +31,11 @@ const userRoutes = (<const>[
name: "祈愿记录",
component: async () => await import("@/pages/User/Gacha.vue"),
},
{
path: "/user/gachaB",
name: "千星奇域祈愿记录",
component: async () => await import("@/pages/User/GachaB.vue"),
},
{
path: "/user/record",
name: "原神战绩",

View File

@@ -1,161 +1,165 @@
/**
* @file types/Plugins/UIGF.d.ts
* @description UIGF 插件类型定义文件
* @since Beta v0.5.1
* @version UIGF v3.0 | UIGF v4.0
* UIGF 标准类型定义文件
* @since Beta v0.8.4
* @version UIGF v3.0 | UIGF v4.1
*/
declare namespace TGApp.Plugins.UIGF {
/**
* @description UIGF 数据
* UIGF 数据
* @since Beta v0.5.0
* @interface Schema
* @property {Info} info - UIGF 头部信息
* @property {GachaItem[]} list - UIGF 祈愿列表
* @return Schema
*/
interface Schema {
info: Info;
list: GachaItem[];
}
/**
* @description UIGF 数据, v4.0
* @since Beta v0.5.0
* @interface Schema4
* @property {Info4} info - UIGF 头部信息
* @property {GachaHk4e[]} hk4e - UIGF 祈愿列表,原神数据
* @return Schema4
*/
interface Schema4 {
info: Info4;
hk4e: GachaHk4e[];
}
/**
* @description UIGF 头部信息
* @since Beta v0.5.0
* @interface Info
* @see docs\UIGF.md
* @property {string} uid - UID
* @property {string} lang - 语言
* @property {string} uigf_version - UIGF 版本,应用使用的是 2.3.0
* @property {number} export_timestamp - 导出时间戳(秒)
* @property {string} export_time - 导出时间 yyyy-MM-dd HH:mm:ss
* @property {string} export_app - 导出应用
* @property {string} export_app_version - 导出应用版本
* @property {number} region_time_zone - 时区
* @return Info
*/
interface Info {
uid: string;
lang: string;
uigf_version: string;
export_timestamp?: number;
export_time?: string;
export_app?: string;
export_app_version?: string;
region_time_zone?: number;
}
/**
* @description UIGF 头部信息, v4.0
* @since Beta v0.5.1
* @interface Info4
* @see docs\UIGF4.md
* @property {string} export_timestamp - 导出时间戳(秒)
* @property {string} export_app - 导出应用
* @property {string} export_app_version - 导出应用版本
* @property {string} version - UIGF 版本
* @property {string} lang - 语言
* @return Info4
*/
interface Info4 {
export_timestamp: string;
export_app: string;
export_app_version: string;
version: string;
lang?: string;
}
/**
* @description 祈愿类型
* @since Alpha v0.2.3
* @enum EnumGachaType
* @property {string} CharacterUp - 角色活动祈愿
* @property {string} CharacterUp2 - 角色活动祈愿2
* @property {string} WeaponUp - 武器活动祈愿
* @property {string} Normal - 普通祈愿
* @property {string} Newbie - 新手祈愿
* @return EnumGachaType
*/
enum EnumGachaType {
CharacterUp = "301",
CharacterUp2 = "400",
WeaponUp = "302",
Normal = "200",
Newbie = "100",
}
/**
* @description UIGF 祈愿类型
* @since Alpha v0.2.3
* @enum EnumUigfGachaType
* @property {string} CharacterUp - 角色活动祈愿&角色活动祈愿2
* @property {string} WeaponUp - 武器活动祈愿
* @property {string} Normal - 普通祈愿
* @property {string} Newbie - 新手祈愿
* @return EnumUigfGachaType
*/
enum EnumUigfGachaType {
CharacterUp = "301",
WeaponUp = "302",
Normal = "200",
Newbie = "100",
}
/**
* @description UIGF 祈愿列表
* @Beta v0.5.1
* @version UIGF v3.0
* @interface GachaItem
* @property {EnumGachaType} gacha_type - 祈愿类型
* @property {string} item_id - 物品ID
* @property {string} count - 数量
* @property {string} time - 时间 yyyy-MM-dd HH:mm:ss
* @property {string} name - 名称
* @property {string} item_type - 物品类型
* @property {string} rank_type - 稀有度
* @property {string} id - ID
* @property {EnumUigfGachaType} uigf_gacha_type - UIGF 祈愿类型
* @return GachaItem
*/
interface GachaItem {
uigf_gacha_type: string;
gacha_type: string;
item_id: string;
count?: string;
time: string;
name: string;
item_type?: string;
rank_type?: string;
id: string;
}
type Schema = {
/** 头部信息 */
info: Info;
/** 祈愿列表 */
list: Array<GachaItem>;
};
/**
* @description UIGF 祈愿列表 v4.0,原神数据
* @since Beta v0.5.0
* @interface GachaHk4e
* @property {string|number} uid - UID
* @property {number} timezone - 时区
* @property {string} lang - 语言
* @property {GachaItem[]} list - 祈愿列表
* @return GachaHk4e
* UIGF 数据 v4.0
* @since Beta v0.8.4
*/
interface GachaHk4e {
uid: string | number;
timezone: number;
type Schema4 = {
/** 头部信息 */
info: Info4;
/** 祈愿列表,原神数据 */
hk4e: Array<GachaHk4e>;
/** 祈愿列表,千星奇域数据 */
hk4e_ugc?: Array<GachaUgc>;
};
/**
* UIGF 头部信息
* @since Beta v0.5.0
* @version UIGF v3.0
*/
type Info = {
/** UID */
uid: string;
/** 语言 */
lang: string;
/** UIGF 版本,应用使用的是 3.0 */
uigf_version: string;
/** 导出时间戳(秒) */
export_timestamp?: number;
/** 导出时间 yyyy-MM-dd HH:mm:ss */
export_time?: string;
/** 导出应用 */
export_app?: string;
/** 导出应用版本 */
export_app_version?: string;
/** 时区 */
region_time_zone?: number;
};
/**
* UIGF 头部信息
* @since Beta v0.5.1
* @version v4.0+
*/
type Info4 = {
/** 导出时间戳(秒) */
export_timestamp: string;
/** 导出应用 */
export_app: string;
/** 导出应用版本 */
export_app_version: string;
/** UIGF 版本 */
version: string;
/** 语言 */
lang?: string;
list: GachaItem[];
}
};
/**
* UIGF4 祈愿项,原神
* @since Beta v0.5.0
*/
type GachaHk4e = {
/** UID */
uid: string | number;
/** 时区 */
timezone: number;
/** 语言 */
lang?: string;
list: Array<GachaItem>;
};
/**
* UIGF4 祈愿项,千星奇域
* @since Beta v0.8.4
* @remarks 该标准尚未最终确定
*/
type GachaUgc = {
/** UID */
uid: string | number;
/** 时区 */
timezone: number;
/** 语言 */
lang?: string;
/** 服务器区域 */
region: string;
/** 祈愿列表 */
list: Array<GachaItemB>;
};
/**
* UIGF 祈愿项-原神
* @since Beta v0.5.1
* @version UIGF v3.0
*/
type GachaItem = {
/** UIGF 祈愿类型 */
uigf_gacha_type: string;
/** 祈愿类型 */
gacha_type: string;
/** 物品ID */
item_id: string;
/** 数量 */
count?: string;
/** 时间 yyyy-MM-dd HH:mm:ss */
time: string;
/** 名称 */
name: string;
/** 物品类型 */
item_type?: string;
/** 稀有度 */
rank_type?: string;
/** ID */
id: string;
};
/**
* UIGF 祈愿项-千星奇域
* @since Beta v0.8.4
* @remarks 该标准尚未最终确定
*/
type GachaItemB = {
/** id */
id: string;
/** 排期id */
schedule_id: string;
/** 物品类型 */
item_type: string;
/** 物品id */
item_id: string;
/** 名称 */
item_name: string;
/** 稀有度 */
rank_type: string;
/** 是否限定 */
is_up: string;
/** 时间 yyyy-MM-dd HH:mm:ss */
time: string;
/**
* 祈愿类型
* @remarks
* 1000-常驻池
* 2000-活动池
*/
gacha_type: string;
/** 祈愿类型,用于接口请求 */
op_gacha_type: string;
};
}