mirror of
https://github.com/BTMuli/TeyvatGuide.git
synced 2025-12-13 09:28:14 +08:00
✨ UIGF4导入/导出浮窗
This commit is contained in:
340
src/components/userGacha/ugo-uid.vue
Normal file
340
src/components/userGacha/ugo-uid.vue
Normal file
@@ -0,0 +1,340 @@
|
||||
<template>
|
||||
<TOverlay v-model="visible" blur-val="5px">
|
||||
<div class="ugo-box">
|
||||
<div class="ugo-top">
|
||||
<div class="ugo-title">{{ title }}</div>
|
||||
<div class="ugo-fp" title="点击选择文件路径" @click="selectFile()">文件路径:{{ fp }}</div>
|
||||
</div>
|
||||
<div class="ugo-header" v-if="props.mode === 'import' && dataRaw">
|
||||
<div class="ugo-header-item">
|
||||
<div>应用信息:</div>
|
||||
<div>{{ dataRaw.info.export_app }} {{ dataRaw.info.export_app_version }}</div>
|
||||
</div>
|
||||
<div class="ugo-header-item">
|
||||
<div>文件UIGF版本:</div>
|
||||
<div>{{ dataRaw.info.version }}</div>
|
||||
</div>
|
||||
<div class="ugo-header-item">
|
||||
<div>导出时间:</div>
|
||||
<div>{{ timestampToDate(Number(dataRaw.info.export_timestamp) * 1000) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<v-item-group multiple v-model="selectedData" class="ugo-content">
|
||||
<v-item
|
||||
v-for="(item, index) in data"
|
||||
:key="index"
|
||||
v-slot="{ isSelected, toggle }"
|
||||
:value="item"
|
||||
>
|
||||
<div class="ugo-item" @click="toggle">
|
||||
<div class="ugo-item-left">
|
||||
<div class="ugo-item-title">{{ item.uid }} - {{ item.length }}条记录</div>
|
||||
<div class="ugo-item-sub">{{ item.time }}</div>
|
||||
</div>
|
||||
<div class="ugo-item-right">
|
||||
<v-btn :class="{ active: isSelected }" class="ugo-item-btn">
|
||||
<v-icon>{{ isSelected ? "mdi-check" : "mdi-checkbox-blank-outline" }}</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</v-item>
|
||||
</v-item-group>
|
||||
<div class="ugo-bottom">
|
||||
<v-btn class="ugo-item-btn" @click="visible = false" :rounded="true">取消</v-btn>
|
||||
<v-btn class="ugo-item-btn" @click="handleSelected()" :rounded="true">确定</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</TOverlay>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { path } from "@tauri-apps/api";
|
||||
import { open } from "@tauri-apps/plugin-dialog";
|
||||
import { writeTextFile } from "@tauri-apps/plugin-fs";
|
||||
import { computed, onMounted, ref, shallowRef, watch } from "vue";
|
||||
|
||||
import TSUserGacha from "../../plugins/Sqlite/modules/userGacha.js";
|
||||
import TGLogger from "../../utils/TGLogger.js";
|
||||
import { timestampToDate } from "../../utils/toolFunc.js";
|
||||
import { getUigf4Header, getUigf4Item, readUigf4Data, verifyUigfData } from "../../utils/UIGF.js";
|
||||
import TOverlay from "../app/t-overlay.vue";
|
||||
import showLoading from "../func/loading.js";
|
||||
import showSnackbar from "../func/snackbar.js";
|
||||
|
||||
type UgoUidProps =
|
||||
| { modelValue: boolean; mode: "export" }
|
||||
| { modelValue: boolean; mode: "import" };
|
||||
type UgoUidEmits = (e: "update:modelValue", v: boolean) => void;
|
||||
type UgoUidItem = { uid: string; length: number; time: string };
|
||||
|
||||
const props = defineProps<UgoUidProps>();
|
||||
const emits = defineEmits<UgoUidEmits>();
|
||||
const dataRaw = shallowRef<TGApp.Plugins.UIGF.Schema4>();
|
||||
const data = shallowRef<UgoUidItem[]>([]);
|
||||
const selectedData = shallowRef<UgoUidItem[]>([]);
|
||||
const title = computed<string>(() => (props.mode === "import" ? "导入" : "导出"));
|
||||
const fp = ref<string>("未选择");
|
||||
|
||||
const visible = computed<boolean>({
|
||||
get: () => props.modelValue,
|
||||
set: (v) => emits("update:modelValue", v),
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
if (props.mode === "export") fp.value = await getDefaultSavePath();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
async (v) => (v ? await refreshData() : undefined),
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
async function getDefaultSavePath(): Promise<string> {
|
||||
const tsNow = new Date().getTime();
|
||||
return `${await path.downloadDir()}${path.sep()}UIGF4_${tsNow}.json`;
|
||||
}
|
||||
|
||||
async function refreshData(): Promise<void> {
|
||||
selectedData.value = [];
|
||||
data.value = [];
|
||||
dataRaw.value = undefined;
|
||||
if (props.mode === "import") {
|
||||
fp.value = "未选择";
|
||||
await handleImportData();
|
||||
} else {
|
||||
fp.value = await getDefaultSavePath();
|
||||
await handleExportData();
|
||||
}
|
||||
}
|
||||
|
||||
async function selectFile(): Promise<void> {
|
||||
const defaultPath =
|
||||
props.mode === "import" ? await getDefaultSavePath() : await path.downloadDir();
|
||||
const file = await open({
|
||||
multiple: false,
|
||||
title: "选择文件",
|
||||
filters: [{ name: "UIGF JSON", extensions: ["json"] }],
|
||||
defaultPath,
|
||||
directory: false,
|
||||
});
|
||||
if (file === null) {
|
||||
showSnackbar.cancel("已取消文件选择");
|
||||
return;
|
||||
}
|
||||
fp.value = file;
|
||||
if (props.mode === "import") await handleImportData();
|
||||
}
|
||||
|
||||
async function handleImportData(): Promise<void> {
|
||||
if (fp.value === "未选择") return;
|
||||
try {
|
||||
showLoading.start("正在导入数据...", "正在验证数据...");
|
||||
const check = await verifyUigfData(fp.value, true);
|
||||
if (!check) {
|
||||
showLoading.end();
|
||||
return;
|
||||
}
|
||||
showLoading.update("正在导入数据...", "正在读取数据...");
|
||||
const uigfData = await readUigf4Data(fp.value);
|
||||
dataRaw.value = uigfData;
|
||||
data.value = uigfData.hk4e.map(parseData);
|
||||
showLoading.end();
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
showSnackbar.error(`[${e.name}] ${e.message}`);
|
||||
await TGLogger.Error(`[UgoUid][handleImportData] ${e.name} ${e.message}`);
|
||||
} else {
|
||||
showSnackbar.error(`[${e}]`);
|
||||
await TGLogger.Error(`[UgoUid][handleImportData] ${e}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function parseData(data: TGApp.Plugins.UIGF.GachaHk4e): UgoUidItem {
|
||||
const timeList = data.list.map((item) => new Date(item.time).getTime());
|
||||
return {
|
||||
uid: data.uid.toString(),
|
||||
length: data.list.length,
|
||||
time: `${timestampToDate(Math.min(...timeList))} ~ ${timestampToDate(Math.max(...timeList))}`,
|
||||
};
|
||||
}
|
||||
|
||||
async function handleExportData(): Promise<void> {
|
||||
const uidList = await TSUserGacha.getUidList();
|
||||
const tmpData: UgoUidItem[] = [];
|
||||
for (const uid of uidList) {
|
||||
const dataRaw = await TSUserGacha.getGachaRecords(uid);
|
||||
tmpData.push(parseDataRaw(dataRaw));
|
||||
}
|
||||
data.value = tmpData;
|
||||
}
|
||||
|
||||
function parseDataRaw(data: TGApp.Sqlite.GachaRecords.SingleTable[]): UgoUidItem {
|
||||
const timeList = data.map((item) => new Date(item.time).getTime());
|
||||
return {
|
||||
uid: data[0].uid,
|
||||
length: data.length,
|
||||
time: `${timestampToDate(Math.min(...timeList))} ~ ${timestampToDate(Math.max(...timeList))}`,
|
||||
};
|
||||
}
|
||||
|
||||
async function handleSelected(): Promise<void> {
|
||||
if (props.mode === "import") {
|
||||
if (!dataRaw.value) {
|
||||
showSnackbar.error("未获取到数据!");
|
||||
fp.value = "未选择";
|
||||
return;
|
||||
}
|
||||
if (selectedData.value.length === 0) {
|
||||
showSnackbar.warn("请至少选择一个!");
|
||||
return;
|
||||
}
|
||||
for (const item of selectedData.value) {
|
||||
showLoading.start("正在导入数据...", `正在导入UID: ${item.uid}`);
|
||||
const dataFind = dataRaw.value.hk4e.find((i) => i.uid.toString() === item.uid);
|
||||
if (!dataFind) {
|
||||
showSnackbar.error(`未找到UID: ${item.uid}`);
|
||||
await new Promise<void>((resolve) => setTimeout(resolve, 1000));
|
||||
continue;
|
||||
}
|
||||
await TSUserGacha.mergeUIGF4(dataFind);
|
||||
}
|
||||
showLoading.end();
|
||||
showSnackbar.success("导入成功!");
|
||||
return;
|
||||
}
|
||||
if (selectedData.value.length === 0) {
|
||||
showSnackbar.warn("请至少选择一个!");
|
||||
return;
|
||||
}
|
||||
showLoading.start("正在导出数据...", "正在生成文件头");
|
||||
const header = await getUigf4Header();
|
||||
const data: TGApp.Plugins.UIGF.GachaHk4e[] = [];
|
||||
for (const item of selectedData.value) {
|
||||
showLoading.update("正在导出数据...", `正在导出UID: ${item.uid}`);
|
||||
const dataItem = await getUigf4Item(item.uid);
|
||||
data.push(dataItem);
|
||||
}
|
||||
showLoading.update("正在导出数据...", "正在生成文件...");
|
||||
await writeTextFile(fp.value, JSON.stringify({ info: header, hk4e: data }));
|
||||
showLoading.end();
|
||||
showSnackbar.success(`导出成功! 文件路径: ${fp.value}`);
|
||||
fp.value = await getDefaultSavePath();
|
||||
}
|
||||
</script>
|
||||
<style lang="css" scoped>
|
||||
.ugo-box {
|
||||
position: relative;
|
||||
width: 600px;
|
||||
padding: 10px;
|
||||
border: 1px solid var(--common-shadow-2);
|
||||
border-radius: 10px;
|
||||
background: var(--app-page-bg);
|
||||
}
|
||||
|
||||
.ugo-top {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
column-gap: 10px;
|
||||
}
|
||||
|
||||
.ugo-title {
|
||||
color: var(--common-text-title);
|
||||
font-family: var(--font-title);
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.ugo-fp {
|
||||
color: var(--tgc-od-white);
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.ugo-header {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.ugo-header-item {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
font-size: 16px;
|
||||
gap: 5px;
|
||||
|
||||
:first-child {
|
||||
color: var(--box-text-7);
|
||||
font-family: var(--font-title);
|
||||
}
|
||||
|
||||
:last-child {
|
||||
color: var(--box-text-5);
|
||||
}
|
||||
}
|
||||
|
||||
.ugo-content {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
max-height: 300px;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
gap: 10px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.ugo-item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 5px;
|
||||
border: 1px solid var(--common-shadow-1);
|
||||
border-radius: 5px;
|
||||
background: var(--box-bg-1);
|
||||
color: var(--box-text-1);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ugo-item-title {
|
||||
font-family: var(--font-title);
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.ugo-item-sub {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.ugo-item-btn {
|
||||
height: 40px;
|
||||
border: 1px solid var(--common-shadow-2);
|
||||
background: var(--btn-bg-1);
|
||||
color: var(--btn-text-1);
|
||||
font-family: var(--font-title);
|
||||
|
||||
&.active {
|
||||
color: var(--tgc-od-green);
|
||||
}
|
||||
}
|
||||
|
||||
.ugo-bottom {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user