mirror of
https://github.com/BTMuli/TeyvatGuide.git
synced 2026-04-24 22:19:41 +08:00
449 lines
13 KiB
Vue
449 lines
13 KiB
Vue
<!-- UIGF4导入导出组件 -->
|
||
<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 v-if="props.mode === 'import' && importRaw" class="ugo-header">
|
||
<div class="ugo-header-item">
|
||
<div>应用信息:</div>
|
||
<div>
|
||
{{ importRaw.data.info.export_app }} {{ importRaw.data.info.export_app_version }}
|
||
</div>
|
||
</div>
|
||
<div class="ugo-header-item">
|
||
<div>文件UIGF版本:</div>
|
||
<div>
|
||
{{ importRaw.isV4 ? importRaw.data.info.version : importRaw.data.info.uigf_version }}
|
||
</div>
|
||
</div>
|
||
<div class="ugo-header-item">
|
||
<div>导出时间:</div>
|
||
<div>{{ timestampToDate(Number(importRaw.data.info.export_timestamp) * 1000) }}</div>
|
||
</div>
|
||
</div>
|
||
<v-item-group v-model="selectedData" class="ugo-content" multiple>
|
||
<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">
|
||
<span>{{ item.uid }} - {{ item.length }}条</span>
|
||
<span>{{ item.isUgc ? "颂愿记录" : "祈愿记录" }}</span>
|
||
</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 :rounded="true" class="ugo-item-btn" @click="visible = false">取消</v-btn>
|
||
<v-btn :rounded="true" class="ugo-item-btn" @click="handleSelected()">确定</v-btn>
|
||
</div>
|
||
</div>
|
||
</TOverlay>
|
||
</template>
|
||
<script lang="ts" setup>
|
||
import TOverlay from "@comp/app/t-overlay.vue";
|
||
import showLoading from "@comp/func/loading.js";
|
||
import showSnackbar from "@comp/func/snackbar.js";
|
||
import TSUserGacha from "@Sqlm/userGacha.js";
|
||
import TSUserGachaB from "@Sqlm/userGachaB.js";
|
||
import { path } from "@tauri-apps/api";
|
||
import { open } from "@tauri-apps/plugin-dialog";
|
||
import TGLogger from "@utils/TGLogger.js";
|
||
import { timestampToDate } from "@utils/toolFunc.js";
|
||
import { checkUigfData, exportUigf4Data, readUigf4Data, readUigfData } from "@utils/UIGF.js";
|
||
import { computed, onMounted, ref, shallowRef, watch } from "vue";
|
||
|
||
type UgoUidProps = {
|
||
/** 导入/导出 */
|
||
mode: "import" | "export";
|
||
/** filePathImport,导入路径 */
|
||
fpi?: string;
|
||
/** filePathExport,导出路径 */
|
||
fpe?: string;
|
||
};
|
||
/**
|
||
* UID项
|
||
*/
|
||
type UgoUidItem = {
|
||
/** UID */
|
||
uid: string;
|
||
/** 数据条数 */
|
||
length: number;
|
||
/** 数据时间段 */
|
||
time: string;
|
||
/** 是否是颂愿数据 */
|
||
isUgc: boolean;
|
||
};
|
||
/** 兼容不同版本的导入 */
|
||
type UgoUidImportRaw =
|
||
| { isV4: true; data: TGApp.Plugins.UIGF.Schema4 }
|
||
| { isV4: false; data: TGApp.Plugins.UIGF.Schema };
|
||
|
||
const fpEmptyText = "点击选择文件路径";
|
||
|
||
const props = defineProps<UgoUidProps>();
|
||
const visible = defineModel<boolean>();
|
||
const fp = ref<string>(fpEmptyText);
|
||
const importRaw = shallowRef<UgoUidImportRaw>();
|
||
const dataRaw = shallowRef<TGApp.Plugins.UIGF.Schema4>();
|
||
const data = shallowRef<Array<UgoUidItem>>([]);
|
||
const selectedData = shallowRef<Array<UgoUidItem>>([]);
|
||
const title = computed<string>(() => (props.mode === "import" ? "导入" : "导出"));
|
||
|
||
onMounted(async () => {
|
||
if (props.mode === "export") fp.value = await getDefaultSavePath();
|
||
});
|
||
|
||
watch(
|
||
() => [visible.value, props.mode, props.fpi, props.fpe],
|
||
async () => {
|
||
if (visible.value) await refreshData();
|
||
},
|
||
);
|
||
|
||
async function getDefaultSavePath(): Promise<string> {
|
||
const tsNow = Math.floor(Date.now() / 1000);
|
||
return `${await path.downloadDir()}${path.sep()}UIGFv4.2_${tsNow}.json`;
|
||
}
|
||
|
||
async function refreshData(): Promise<void> {
|
||
selectedData.value = [];
|
||
data.value = [];
|
||
dataRaw.value = undefined;
|
||
importRaw.value = undefined;
|
||
if (props.mode === "import") {
|
||
fp.value = props.fpi ?? fpEmptyText;
|
||
await refreshImport();
|
||
} else {
|
||
fp.value = props.fpe ?? (await getDefaultSavePath());
|
||
await refreshExport();
|
||
}
|
||
}
|
||
|
||
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 refreshImport();
|
||
}
|
||
|
||
async function refreshImport(): Promise<void> {
|
||
if (fp.value === fpEmptyText) return;
|
||
try {
|
||
await showLoading.start("正在导入数据...", "正在验证数据...");
|
||
const isV4 = await checkUigfData(fp.value);
|
||
console.info(isV4);
|
||
if (isV4 === null) {
|
||
await showLoading.end();
|
||
return;
|
||
}
|
||
await showLoading.update("数据验证成功,正在读取数据...");
|
||
if (isV4) {
|
||
const read = await readUigf4Data(fp.value);
|
||
importRaw.value = { isV4: true, data: read };
|
||
const hk4eUids = read.hk4e?.map(parseData4) ?? [];
|
||
const ugcUids = read.hk4e_ugc?.map(parseUgc) ?? [];
|
||
data.value = [...hk4eUids, ...ugcUids];
|
||
} else {
|
||
const read = await readUigfData(fp.value);
|
||
console.log(read.list.length);
|
||
importRaw.value = { isV4: false, data: read };
|
||
data.value = [parseData(read)];
|
||
}
|
||
await 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.Schema): UgoUidItem {
|
||
const timeList = data.list.map((item) => new Date(item.time).getTime());
|
||
return {
|
||
uid: data.info.uid,
|
||
length: data.list.length,
|
||
time: `${timestampToDate(Math.min(...timeList))} ~ ${timestampToDate(Math.max(...timeList))}`,
|
||
isUgc: false,
|
||
};
|
||
}
|
||
|
||
function parseData4(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))}`,
|
||
isUgc: false,
|
||
};
|
||
}
|
||
|
||
function parseUgc(data: TGApp.Plugins.UIGF.GachaUgc): 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))}`,
|
||
isUgc: true,
|
||
};
|
||
}
|
||
|
||
async function refreshExport(): Promise<void> {
|
||
const uidHk4e = await TSUserGacha.getUidList();
|
||
const uidUgc = await TSUserGachaB.getUidList();
|
||
const tmpData: Array<UgoUidItem> = [];
|
||
for (const uid of uidHk4e) {
|
||
const dataRaw = await TSUserGacha.record.all(uid);
|
||
tmpData.push(parseLocalHk4e(dataRaw));
|
||
}
|
||
for (const uid of uidUgc) {
|
||
const dataRaw = await TSUserGachaB.getGachaRecords(uid);
|
||
tmpData.push(parseLocalUgc(dataRaw));
|
||
}
|
||
data.value = tmpData;
|
||
}
|
||
|
||
function parseLocalHk4e(data: Array<TGApp.Sqlite.Gacha.Gacha>): 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))}`,
|
||
isUgc: false,
|
||
};
|
||
}
|
||
|
||
function parseLocalUgc(data: Array<TGApp.Sqlite.Gacha.GachaB>): 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))}`,
|
||
isUgc: true,
|
||
};
|
||
}
|
||
|
||
async function handleSelected(): Promise<void> {
|
||
if (props.mode === "import") return await handleImport();
|
||
return await handleExport();
|
||
}
|
||
|
||
async function handleImport(): Promise<void> {
|
||
if (!importRaw.value) {
|
||
showSnackbar.error("未获取到数据!");
|
||
fp.value = fpEmptyText;
|
||
return;
|
||
}
|
||
if (selectedData.value.length === 0) {
|
||
showSnackbar.warn("请至少选择一个!");
|
||
return;
|
||
}
|
||
await showLoading.start("正在导入数据...");
|
||
if (importRaw.value.isV4) {
|
||
for (const item of selectedData.value) {
|
||
await showLoading.update(`正在导入UID: ${item.uid}`);
|
||
if (!item.isUgc) {
|
||
const dataFind = importRaw.value.data.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, true);
|
||
} else {
|
||
const dataFind = importRaw.value.data.hk4e_ugc?.find((i) => i.uid.toString() === item.uid);
|
||
if (!dataFind) {
|
||
showSnackbar.error(`未找到UID: ${item.uid}`);
|
||
await new Promise<void>((resolve) => setTimeout(resolve, 1000));
|
||
continue;
|
||
}
|
||
await TSUserGachaB.mergeUIGF4(dataFind, true);
|
||
}
|
||
}
|
||
} else {
|
||
const iUid = selectedData.value[0].uid;
|
||
await showLoading.update(`正在导入UID:${iUid}`);
|
||
await TSUserGacha.mergeUIGF(iUid, importRaw.value.data.list, true);
|
||
}
|
||
await showLoading.end();
|
||
showSnackbar.success("导入成功!即将刷新页面...");
|
||
window.location.reload();
|
||
}
|
||
|
||
async function handleExport(): Promise<void> {
|
||
if (selectedData.value.length === 0) {
|
||
showSnackbar.warn("请至少选择一个!");
|
||
return;
|
||
}
|
||
const totalCnt = selectedData.value.map((s) => s.length).reduce((a, b) => a + b, 0);
|
||
await showLoading.start(
|
||
"正在导出数据...",
|
||
`${selectedData.value.length}条UID - ${totalCnt}条记录`,
|
||
);
|
||
await exportUigf4Data(
|
||
selectedData.value.filter((i) => !i.isUgc).map((s) => s.uid.toString()),
|
||
selectedData.value.filter((i) => i.isUgc).map((s) => s.uid.toString()),
|
||
fp.value,
|
||
);
|
||
await showLoading.end();
|
||
showSnackbar.success(`导出成功! 文件路径: ${fp.value}`);
|
||
fp.value = await getDefaultSavePath();
|
||
}
|
||
</script>
|
||
<style lang="scss" 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%;
|
||
flex-wrap: wrap;
|
||
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;
|
||
word-break: break-all;
|
||
}
|
||
|
||
.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 {
|
||
position: relative;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: flex-start;
|
||
column-gap: 8px;
|
||
font-family: var(--font-title);
|
||
font-size: 16px;
|
||
|
||
:last-child {
|
||
color: var(--tgc-od-red);
|
||
}
|
||
}
|
||
|
||
.ugo-item-sub {
|
||
font-size: 12px;
|
||
}
|
||
|
||
.ugo-item-btn {
|
||
height: 40px;
|
||
border: 1px solid var(--common-shadow-2);
|
||
background: var(--tgc-btn-1);
|
||
color: var(--btn-text);
|
||
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>
|