mirror of
https://github.com/BTMuli/TeyvatGuide.git
synced 2026-03-21 04:49:46 +08:00
25
src/App.vue
25
src/App.vue
@@ -103,7 +103,6 @@ onMounted(async () => {
|
||||
function listenOnInit(): void {
|
||||
console.info("[App][listenOnInit] 监听初始化事件!");
|
||||
event.listen("initApp", async () => {
|
||||
await checkAppLoad();
|
||||
await checkDeviceFp();
|
||||
try {
|
||||
await checkUserLoad();
|
||||
@@ -116,30 +115,6 @@ function listenOnInit(): void {
|
||||
});
|
||||
}
|
||||
|
||||
async function checkAppLoad(): Promise<void> {
|
||||
let checkDB = false;
|
||||
try {
|
||||
checkDB = await TGSqlite.check();
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
await TGLogger.Error(`[App][checkAppLoad] ${error.name}: ${error.message}`);
|
||||
} else console.error(error);
|
||||
}
|
||||
if (!checkDB) await resetDB();
|
||||
else await TGLogger.Info("[App][checkAppLoad] 数据库已成功加载!");
|
||||
}
|
||||
|
||||
async function resetDB(): Promise<void> {
|
||||
await TGSqlite.reset();
|
||||
showSnackbar({
|
||||
text: "检测到数据库不完整!已重置数据库!",
|
||||
color: "error",
|
||||
timeout: 3000,
|
||||
});
|
||||
appStore.loading = true;
|
||||
await TGLogger.Info("[App][resetDB] 数据库已重置!");
|
||||
}
|
||||
|
||||
// 检测 deviceFp
|
||||
async function checkDeviceFp(): Promise<void> {
|
||||
const appData = await TGSqlite.getAppData();
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<img class="config-icon" src="../../assets/icons/achievements.svg" alt="Achievements" />
|
||||
</template>
|
||||
<template #append>
|
||||
<v-list-item-subtitle>{{ achievementsStore.lastVersion }}</v-list-item-subtitle>
|
||||
<v-list-item-subtitle>{{ latestAchiVersion }}</v-list-item-subtitle>
|
||||
</template>
|
||||
</v-list-item>
|
||||
<v-list-item title="系统平台">
|
||||
@@ -78,11 +78,11 @@ import { platform, version } from "@tauri-apps/plugin-os";
|
||||
import { onMounted, ref } from "vue";
|
||||
|
||||
import TGSqlite from "../../plugins/Sqlite/index.js";
|
||||
import { useAchievementsStore } from "../../store/modules/achievements.js";
|
||||
import TSUserAchi from "../../plugins/Sqlite/modules/userAchi.js";
|
||||
import TGLogger from "../../utils/TGLogger.js";
|
||||
import showSnackbar from "../func/snackbar.js";
|
||||
|
||||
const achievementsStore = useAchievementsStore();
|
||||
const latestAchiVersion = TSUserAchi.getLatestAchiVersion();
|
||||
|
||||
const versionApp = ref<string>("");
|
||||
const versionTauri = ref<string>("");
|
||||
|
||||
@@ -37,6 +37,8 @@ let confirmInstance: VNode;
|
||||
|
||||
/**
|
||||
* @function showConfirm
|
||||
* @since Beta v0.3.9
|
||||
* @todo 重载重构
|
||||
* @description 弹出 confirm
|
||||
* @param {TGApp.Component.Confirm.Params} props confirm 的参数
|
||||
* @return {Promise<string | boolean | undefined>} 点击确认返回 true,点击取消返回 false,点击外部返回 undefined
|
||||
|
||||
@@ -36,10 +36,17 @@ import { nextTick, onMounted, reactive, ref, watch, useTemplateRef } from "vue";
|
||||
interface ConfirmProps {
|
||||
title: string;
|
||||
text?: string;
|
||||
mode?: "confirm" | "input";
|
||||
mode: "confirm" | "input";
|
||||
otcancel?: boolean;
|
||||
}
|
||||
|
||||
const defaultProp: ConfirmProps = {
|
||||
title: "",
|
||||
text: "",
|
||||
mode: "confirm",
|
||||
otcancel: false,
|
||||
};
|
||||
|
||||
const props = withDefaults(defineProps<ConfirmProps>(), {
|
||||
title: "",
|
||||
text: "",
|
||||
@@ -48,9 +55,7 @@ const props = withDefaults(defineProps<ConfirmProps>(), {
|
||||
});
|
||||
|
||||
// 组件参数
|
||||
const data = reactive<TGApp.Component.Confirm.Params>({
|
||||
title: "",
|
||||
});
|
||||
const data = reactive<TGApp.Component.Confirm.Params>(defaultProp);
|
||||
const show = ref<boolean>(false);
|
||||
const showOuter = ref<boolean>(false);
|
||||
const showInner = ref<boolean>(false);
|
||||
|
||||
@@ -35,6 +35,7 @@ const renderBox = (props: TGApp.Component.Snackbar.Params): VNode => {
|
||||
|
||||
let snackbarInstance: VNode;
|
||||
|
||||
// todo 参数重构
|
||||
function showSnackbar(props: TGApp.Component.Snackbar.Params): void {
|
||||
if (snackbarInstance !== undefined) {
|
||||
const boxVue = <SnackbarInstance>snackbarInstance.component;
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
<template>
|
||||
<v-list
|
||||
:style="{ backgroundImage: props.data.name === '原神·印象' ? 'none' : `url(${props.data.bg})` }"
|
||||
class="top-nc-box"
|
||||
@click="toNameCard(props.data)"
|
||||
>
|
||||
<v-list class="top-nc-box" @click="toNameCard(props.data)">
|
||||
<v-list-item :title="props.data.name">
|
||||
<template #subtitle>
|
||||
<span :title="props.data.desc">{{ props.data.desc }}</span>
|
||||
@@ -15,16 +11,23 @@
|
||||
</v-list>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
interface TopNamecardProps {
|
||||
import { computed } from "vue";
|
||||
|
||||
interface TopNameCardProps {
|
||||
data: TGApp.App.NameCard.Item;
|
||||
}
|
||||
|
||||
interface TopNamecardEmits {
|
||||
interface TopNameCardEmits {
|
||||
(e: "selected", data: TGApp.App.NameCard.Item): void;
|
||||
}
|
||||
|
||||
const props = defineProps<TopNamecardProps>();
|
||||
const emit = defineEmits<TopNamecardEmits>();
|
||||
const props = defineProps<TopNameCardProps>();
|
||||
const emit = defineEmits<TopNameCardEmits>();
|
||||
|
||||
const bgImage = computed<string>(() => {
|
||||
if (props.data.name === "原神·印象") return "none;";
|
||||
return `url("${props.data.bg}")`;
|
||||
});
|
||||
|
||||
function toNameCard(item: TGApp.App.NameCard.Item) {
|
||||
emit("selected", item);
|
||||
@@ -37,6 +40,7 @@ function toNameCard(item: TGApp.App.NameCard.Item) {
|
||||
border: 1px solid var(--common-shadow-2);
|
||||
border-radius: 10px 50px 50px 10px;
|
||||
background-color: var(--box-bg-1);
|
||||
background-image: v-bind(bgImage);
|
||||
background-position: right;
|
||||
background-repeat: no-repeat;
|
||||
cursor: pointer;
|
||||
|
||||
174
src/components/userAchi/tua-achi-list.vue
Normal file
174
src/components/userAchi/tua-achi-list.vue
Normal file
@@ -0,0 +1,174 @@
|
||||
<template>
|
||||
<div class="tua-al-container">
|
||||
<div v-if="ncData !== undefined">
|
||||
<TopNameCard :data="ncData" @selected="showNc = true" />
|
||||
</div>
|
||||
<!-- todo 虚拟列表优化 -->
|
||||
<div v-for="(item, index) in renderAchi" :key="index">
|
||||
<TuaAchi :modelValue="item" @select-achi="selectAchi" />
|
||||
</div>
|
||||
<ToNameCard v-model="showNc" :data="ncData" v-if="ncData" />
|
||||
<ToAchiInfo
|
||||
v-if="selectedAchi"
|
||||
v-model="showOverlay"
|
||||
:data="selectedAchi"
|
||||
@select-series="selectSeries"
|
||||
>
|
||||
<template #left>
|
||||
<div class="card-arrow left" @click="switchAchiInfo(false)">
|
||||
<img src="../../assets/icons/arrow-right.svg" alt="right" />
|
||||
</div>
|
||||
</template>
|
||||
<template #right>
|
||||
<div class="card-arrow" @click="switchAchiInfo(true)">
|
||||
<img src="../../assets/icons/arrow-right.svg" alt="right" />
|
||||
</div>
|
||||
</template>
|
||||
</ToAchiInfo>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, watch, onMounted } from "vue";
|
||||
|
||||
import { AppAchievementSeriesData, AppNameCardsData } from "../../data/index.js";
|
||||
import TSUserAchi from "../../plugins/Sqlite/modules/userAchi.js";
|
||||
import showSnackbar from "../func/snackbar.js";
|
||||
import ToNameCard from "../overlay/to-namecard.vue";
|
||||
import TopNameCard from "../overlay/top-namecard.vue";
|
||||
|
||||
import ToAchiInfo from "./tua-achi-overlay.vue";
|
||||
import TuaAchi from "./tua-achi.vue";
|
||||
|
||||
interface TuaAchiListProps {
|
||||
uid: number;
|
||||
hideFin: boolean;
|
||||
series?: number;
|
||||
search?: string;
|
||||
}
|
||||
|
||||
interface TuaAchiListEmits {
|
||||
(e: "update:series", v: number): void;
|
||||
}
|
||||
|
||||
const props = defineProps<TuaAchiListProps>();
|
||||
const emits = defineEmits<TuaAchiListEmits>();
|
||||
|
||||
const achievements = ref<TGApp.Sqlite.Achievement.RenderAchi[]>([]);
|
||||
const selectedAchi = ref<TGApp.Sqlite.Achievement.RenderAchi | undefined>(undefined);
|
||||
|
||||
const nameCard = ref<string>();
|
||||
const ncData = ref<TGApp.App.NameCard.Item | undefined>(undefined);
|
||||
const showNc = ref<boolean>(false);
|
||||
|
||||
const showOverlay = ref<boolean>(false);
|
||||
|
||||
const renderAchi = computed<Array<TGApp.Sqlite.Achievement.RenderAchi>>(() => {
|
||||
if (props.hideFin) return achievements.value.filter((a) => a.isCompleted);
|
||||
return achievements.value;
|
||||
});
|
||||
|
||||
onMounted(async () => await loadAchi());
|
||||
|
||||
watch(
|
||||
() => [props.series, props.search, props.uid],
|
||||
async () => await loadAchi(),
|
||||
);
|
||||
|
||||
async function loadAchi(): Promise<void> {
|
||||
if (props.search && props.search !== "") {
|
||||
nameCard.value = undefined;
|
||||
ncData.value = undefined;
|
||||
achievements.value = await TSUserAchi.searchAchi(props.uid, props.search);
|
||||
return;
|
||||
}
|
||||
achievements.value = await TSUserAchi.getAchievements(props.uid, props.series);
|
||||
if (!selectedAchi.value && achievements.value.length > 0) {
|
||||
selectedAchi.value = achievements.value[0];
|
||||
} else if (selectedAchi.value !== undefined && achievements.value.length > 0) {
|
||||
const index = achievements.value.findIndex((a) => a.id === selectedAchi.value!.id);
|
||||
if (index === -1) selectedAchi.value = achievements.value[0];
|
||||
}
|
||||
const seriesFind = AppAchievementSeriesData.find((s) => s.id === props.series);
|
||||
if (!seriesFind || seriesFind.card === "") {
|
||||
nameCard.value = undefined;
|
||||
ncData.value = undefined;
|
||||
} else nameCard.value = seriesFind.card;
|
||||
if (nameCard.value && nameCard.value !== "") {
|
||||
const ncFind = AppNameCardsData.find((c) => c.name === nameCard.value);
|
||||
if (ncFind) ncData.value = ncFind;
|
||||
else ncData.value = undefined;
|
||||
}
|
||||
showSnackbar({ text: `已获取 ${renderAchi.value.length} 条成就数据`, color: "success" });
|
||||
}
|
||||
|
||||
function selectAchi(data: TGApp.Sqlite.Achievement.RenderAchi): void {
|
||||
selectedAchi.value = data;
|
||||
showOverlay.value = true;
|
||||
}
|
||||
|
||||
function selectSeries(data: number): void {
|
||||
emits("update:series", data);
|
||||
}
|
||||
|
||||
function switchAchiInfo(next: boolean): void {
|
||||
if (selectedAchi.value === undefined) {
|
||||
showSnackbar({ text: "当前未选中成就!", color: "warn" });
|
||||
return;
|
||||
}
|
||||
const index = renderAchi.value.findIndex((i) => i === selectedAchi.value);
|
||||
if (index === -1) {
|
||||
showSnackbar({
|
||||
text: `未找到选中成就 ${selectedAchi.value.name}(${selectedAchi.value.id}) 的索引!`,
|
||||
color: "error",
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (next) {
|
||||
if (index === renderAchi.value.length - 1) {
|
||||
showSnackbar({ text: "已经是最后一个了", color: "warn" });
|
||||
return;
|
||||
}
|
||||
selectedAchi.value = renderAchi.value[index + 1];
|
||||
return;
|
||||
}
|
||||
if (index === 0) {
|
||||
showSnackbar({ text: "已经是第一个了", color: "warn" });
|
||||
return;
|
||||
}
|
||||
selectedAchi.value = renderAchi.value[index - 1];
|
||||
}
|
||||
</script>
|
||||
<style lang="css" scoped>
|
||||
.tua-al-container {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
flex-direction: column;
|
||||
padding-right: 10px;
|
||||
overflow-y: scroll;
|
||||
row-gap: 10px;
|
||||
}
|
||||
|
||||
.card-arrow {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dark .card-arrow {
|
||||
filter: invert(11%) sepia(73%) saturate(11%) hue-rotate(139deg) brightness(97%) contrast(81%);
|
||||
}
|
||||
|
||||
.card-arrow img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.card-arrow.left img {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
</style>
|
||||
@@ -1,50 +1,48 @@
|
||||
<template>
|
||||
<TOverlay v-model="visible" hide :to-click="onCancel" blur-val="0">
|
||||
<div class="toai-container" v-if="props.data">
|
||||
<div class="tua-ao-container" v-if="props.data">
|
||||
<slot name="left"></slot>
|
||||
<div class="toai-box">
|
||||
<div class="toai-top">
|
||||
<span class="toai-click" @click="searchDirect(props.data.name)">{{
|
||||
props.data.name
|
||||
}}</span>
|
||||
<div class="tua-ao-box">
|
||||
<div class="tua-ao-top">
|
||||
<span class="tua-ao-click" @click="searchDirect(props.data.name)">
|
||||
{{ props.data.name }}
|
||||
</span>
|
||||
<span>{{ props.data.description }}</span>
|
||||
</div>
|
||||
<div v-if="achiLC">
|
||||
<div class="toai-mid-title">
|
||||
<span>所属系列:</span>
|
||||
<span class="toai-click" @click="emits('selectS', props.data.series)">{{
|
||||
AppAchievementSeriesData.find((s) => s.id === props.data?.series)?.name ?? "未知"
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="toai-mid-title">
|
||||
<span>原石奖励:</span>
|
||||
<span>{{ props.data.reward }}</span>
|
||||
</div>
|
||||
<div class="toai-mid-title">
|
||||
<span>触发方式:</span>
|
||||
<span>{{ achiLC.trigger.task ? "完成以下所有任务" : achiLC.trigger.type }}</span>
|
||||
</div>
|
||||
<div class="toai-mid-item" v-for="item in achiLC.trigger.task" :key="item.questId">
|
||||
<v-icon>mdi-alert-decagram</v-icon>
|
||||
<span class="toai-click" @click="searchDirect(item.name)">{{ item.name }}</span>
|
||||
<span>({{ item.type }})</span>
|
||||
</div>
|
||||
<div class="tua-ao-mid-title">
|
||||
<span>所属系列:</span>
|
||||
<span class="tua-ao-click" @click="emits('select-series', props.data.series)">
|
||||
{{ AppAchievementSeriesData.find((s) => s.id === props.data?.series)?.name ?? "未知" }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="tua-ao-mid-title">
|
||||
<span>原石奖励:</span>
|
||||
<span>{{ props.data.reward }}</span>
|
||||
</div>
|
||||
<div class="tua-ao-mid-title">
|
||||
<span>触发方式:</span>
|
||||
<span>{{ props.data.trigger.task ? "完成以下所有任务" : props.data.trigger.type }}</span>
|
||||
</div>
|
||||
<div class="tua-ao-mid-item" v-for="item in props.data.trigger.task" :key="item.questId">
|
||||
<v-icon>mdi-alert-decagram</v-icon>
|
||||
<span class="tua-ao-click" @click="searchDirect(item.name)">{{ item.name }}</span>
|
||||
<span>({{ item.type }})</span>
|
||||
</div>
|
||||
<div>
|
||||
<div class="toai-bottom-title">
|
||||
<div class="tua-ao-bottom-title">
|
||||
<span>是否完成:</span>
|
||||
<span>{{ props.data.isCompleted ? "是" : "否" }}</span>
|
||||
</div>
|
||||
<div class="toai-bottom-title">
|
||||
<div class="tua-ao-bottom-title">
|
||||
<span>完成时间:</span>
|
||||
<span>{{ props.data.completedTime }}</span>
|
||||
</div>
|
||||
<div class="toai-bottom-title">
|
||||
<div class="tua-ao-bottom-title">
|
||||
<span>当前进度:</span>
|
||||
<span>{{ props.data.progress }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="toai-extra">
|
||||
<div class="tua-ao-extra">
|
||||
<span>ID:{{ props.data.id }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -54,22 +52,22 @@
|
||||
<ToPostSearch gid="2" v-model="showSearch" :keyword="search" />
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, ref, watch } from "vue";
|
||||
import { computed, ref } from "vue";
|
||||
|
||||
import { AppAchievementsData, AppAchievementSeriesData } from "../../data/index.js";
|
||||
import { AppAchievementSeriesData } from "../../data/index.js";
|
||||
import TGLogger from "../../utils/TGLogger.js";
|
||||
import TOverlay from "../main/t-overlay.vue";
|
||||
import ToPostSearch from "../post/to-postSearch.vue";
|
||||
|
||||
interface ToAchiInfoProps {
|
||||
modelValue: boolean;
|
||||
data?: TGApp.Sqlite.Achievement.SingleTable;
|
||||
data: TGApp.Sqlite.Achievement.RenderAchi;
|
||||
}
|
||||
|
||||
interface ToAchiInfoEmits {
|
||||
(e: "update:modelValue", v: boolean): void;
|
||||
|
||||
(e: "selectS", v: number): void;
|
||||
(e: "select-series", v: number): void;
|
||||
}
|
||||
|
||||
const props = defineProps<ToAchiInfoProps>();
|
||||
@@ -84,40 +82,25 @@ const visible = computed({
|
||||
},
|
||||
});
|
||||
|
||||
const achiLC = ref<TGApp.App.Achievement.Item>();
|
||||
|
||||
onMounted(() => getData);
|
||||
watch(() => props.data, getData);
|
||||
|
||||
function getData(): void {
|
||||
const res = AppAchievementsData.find((item) => item.id === props.data?.id);
|
||||
if (res) {
|
||||
achiLC.value = res;
|
||||
} else {
|
||||
achiLC.value = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// 点击外部关闭
|
||||
function onCancel() {
|
||||
visible.value = false;
|
||||
}
|
||||
|
||||
async function searchDirect(word: string): Promise<void> {
|
||||
await TGLogger.Info(`[ToAchiInfo][${props.data?.id}][Search] 查询 ${word}`);
|
||||
await TGLogger.Info(`[ToAchiInfo][${props.data.id}][Search] 查询 ${word}`);
|
||||
search.value = word;
|
||||
showSearch.value = true;
|
||||
}
|
||||
</script>
|
||||
<style lang="css" scoped>
|
||||
.toai-container {
|
||||
.tua-ao-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
column-gap: 10px;
|
||||
}
|
||||
|
||||
.toai-box {
|
||||
.tua-ao-box {
|
||||
position: relative;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
@@ -130,7 +113,7 @@ async function searchDirect(word: string): Promise<void> {
|
||||
row-gap: 10px;
|
||||
}
|
||||
|
||||
.toai-top {
|
||||
.tua-ao-top {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
@@ -138,13 +121,13 @@ async function searchDirect(word: string): Promise<void> {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.toai-top :first-child {
|
||||
.tua-ao-top :first-child {
|
||||
color: var(--common-text-title);
|
||||
font-family: var(--font-title);
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.toai-top-main :last-child {
|
||||
.tua-ao-top-main :last-child {
|
||||
padding: 0 5px;
|
||||
border-radius: 5px;
|
||||
background: var(--box-bg-2);
|
||||
@@ -152,21 +135,21 @@ async function searchDirect(word: string): Promise<void> {
|
||||
font-family: var(--font-title);
|
||||
}
|
||||
|
||||
.toai-mid-title,
|
||||
.toai-bottom-title {
|
||||
.tua-ao-mid-title,
|
||||
.tua-ao-bottom-title {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.toai-mid-title :first-child,
|
||||
.toai-bottom-title :first-child {
|
||||
.tua-ao-mid-title :first-child,
|
||||
.tua-ao-bottom-title :first-child {
|
||||
font-family: var(--font-title);
|
||||
}
|
||||
|
||||
.toai-mid-title :last-child {
|
||||
.tua-ao-mid-title :last-child {
|
||||
color: var(--box-text-3);
|
||||
}
|
||||
|
||||
.toai-mid-item {
|
||||
.tua-ao-mid-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
@@ -176,17 +159,17 @@ async function searchDirect(word: string): Promise<void> {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.toai-mid-item :first-child {
|
||||
.tua-ao-mid-item :first-child {
|
||||
color: var(--box-text-5);
|
||||
}
|
||||
|
||||
.toai-extra {
|
||||
.tua-ao-extra {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
bottom: 10px;
|
||||
}
|
||||
|
||||
.toai-click {
|
||||
.tua-ao-click {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
233
src/components/userAchi/tua-achi.vue
Normal file
233
src/components/userAchi/tua-achi.vue
Normal file
@@ -0,0 +1,233 @@
|
||||
<template>
|
||||
<div class="achi-container" :title="getAchiTitle()">
|
||||
<div class="achi-version">v{{ data.version }}</div>
|
||||
<div class="achi-pre">
|
||||
<div class="achi-pre-icon">
|
||||
<v-icon v-if="!data.isCompleted" color="var(--tgc-blue-3)" @click="setAchiStat(true)">
|
||||
mdi-circle
|
||||
</v-icon>
|
||||
<v-icon v-else class="achi-finish" @click="setAchiStat(false)">
|
||||
<img alt="finish" src="/source/UI/finish.webp" />
|
||||
</v-icon>
|
||||
</div>
|
||||
<div class="achi-pre-info" @click="selectAchi()">
|
||||
<span>
|
||||
<span>{{ data.name }}</span>
|
||||
<span v-if="data.progress !== 0">
|
||||
{{ data.progress }}
|
||||
</span>
|
||||
</span>
|
||||
<span>{{ data.description }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="achi-append">
|
||||
<span v-show="data.isCompleted">{{ data.completedTime }}</span>
|
||||
<div class="achi-append-icon">
|
||||
<img alt="icon" src="/icon/material/201.webp" />
|
||||
<span>{{ data.reward }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { event } from "@tauri-apps/api";
|
||||
import { toRaw, ref, watch } from "vue";
|
||||
|
||||
import { AppAchievementSeriesData } from "../../data/index.js";
|
||||
import TSUserAchi from "../../plugins/Sqlite/modules/userAchi.js";
|
||||
import { timestampToDate } from "../../utils/toolFunc.js";
|
||||
import showConfirm from "../func/confirm.js";
|
||||
import showSnackbar from "../func/snackbar.js";
|
||||
|
||||
interface TuaAchiProps {
|
||||
modelValue: TGApp.Sqlite.Achievement.RenderAchi;
|
||||
}
|
||||
|
||||
interface TuaAchiEmits {
|
||||
(e: "update:modelValue", data: TGApp.Sqlite.Achievement.RenderAchi): void;
|
||||
|
||||
(e: "update:update", data: boolean): void;
|
||||
|
||||
(e: "select-achi", data: TGApp.Sqlite.Achievement.RenderAchi): void;
|
||||
}
|
||||
|
||||
const props = defineProps<TuaAchiProps>();
|
||||
const emits = defineEmits<TuaAchiEmits>();
|
||||
const data = ref<TGApp.Sqlite.Achievement.RenderAchi>(toRaw(props.modelValue));
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal: TGApp.Sqlite.Achievement.RenderAchi) => {
|
||||
data.value = toRaw(newVal);
|
||||
},
|
||||
);
|
||||
|
||||
function getAchiTitle(): string {
|
||||
const seriesFind = AppAchievementSeriesData.find((s) => s.id === data.value.series);
|
||||
if (!seriesFind) return "未知";
|
||||
return seriesFind.name;
|
||||
}
|
||||
|
||||
function selectAchi(): void {
|
||||
emits("select-achi", props.modelValue);
|
||||
}
|
||||
|
||||
async function setAchiStat(stat: boolean): Promise<void> {
|
||||
if (!stat) {
|
||||
data.value.isCompleted = false;
|
||||
await TSUserAchi.updateAchi(data.value);
|
||||
emits("update:modelValue", data.value);
|
||||
await event.emit("updateAchi", data.value.series);
|
||||
showSnackbar({
|
||||
text: `已将成就 ${data.value.name}(${data.value.id}) 状态设为未完成`,
|
||||
color: "success",
|
||||
});
|
||||
return;
|
||||
}
|
||||
let progress: false | undefined | string = await showConfirm({
|
||||
mode: "input",
|
||||
title: "请输入成就进度",
|
||||
text: "进度",
|
||||
input: data.value.progress,
|
||||
});
|
||||
if (progress === false) {
|
||||
showSnackbar({ text: "已取消成就编辑", color: "cancel" });
|
||||
return;
|
||||
}
|
||||
if (progress === undefined) progress = data.value.progress.toString();
|
||||
if (isNaN(Number(progress)) || progress === "0") {
|
||||
showSnackbar({ text: "请输入有效数字!", color: "warn" });
|
||||
return;
|
||||
}
|
||||
data.value.progress = Number(progress);
|
||||
data.value.completedTime = timestampToDate(new Date().getTime());
|
||||
data.value.isCompleted = true;
|
||||
await TSUserAchi.updateAchi(data.value);
|
||||
await event.emit("updateAchi", data.value.series);
|
||||
showSnackbar({
|
||||
text: `已将成就 ${data.value.name}(${data.value.id}) 状态设为已完成`,
|
||||
color: "success",
|
||||
});
|
||||
emits("update:modelValue", data.value);
|
||||
}
|
||||
</script>
|
||||
<style lang="css" scoped>
|
||||
.achi-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px;
|
||||
border-radius: 10px;
|
||||
background: var(--box-bg-1);
|
||||
color: var(--box-text-7);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.achi-version {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 50px;
|
||||
border-right: 1px solid var(--common-shadow-1);
|
||||
border-bottom: 1px solid var(--common-shadow-1);
|
||||
background: var(--box-bg-2);
|
||||
border-bottom-right-radius: 20px;
|
||||
border-top-left-radius: 10px;
|
||||
color: var(--tgc-pink-1);
|
||||
font-family: var(--font-title);
|
||||
font-size: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.achi-pre {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
column-gap: 10px;
|
||||
}
|
||||
|
||||
.achi-pre-icon {
|
||||
display: flex;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.achi-finish img {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
filter: invert(51%) sepia(100%) saturate(353%) hue-rotate(42deg) brightness(107%) contrast(91%);
|
||||
}
|
||||
|
||||
.achi-pre-info {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
flex-flow: column wrap;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.achi-pre-info :nth-child(1) {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
column-gap: 5px;
|
||||
font-family: var(--font-title);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.achi-pre-info :nth-child(1) :nth-child(2) {
|
||||
color: var(--tgc-blue-2);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.achi-pre-info :nth-child(2) {
|
||||
font-size: 12px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.achi-append-icon span {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 10px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgb(0 0 0 / 50%);
|
||||
border-bottom-left-radius: 5px;
|
||||
border-bottom-right-radius: 5px;
|
||||
color: var(--tgc-white-1);
|
||||
font-size: 8px;
|
||||
}
|
||||
|
||||
.achi-append {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
column-gap: 10px;
|
||||
}
|
||||
|
||||
.achi-append :nth-last-child(2) {
|
||||
margin-right: 10px;
|
||||
color: var(--box-text-4);
|
||||
font-size: small;
|
||||
}
|
||||
|
||||
.achi-append-icon {
|
||||
position: relative;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 5px;
|
||||
background-image: url("/icon/bg/5-Star.webp");
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.achi-append-icon img {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
</style>
|
||||
0
src/components/userAchi/tua-nc-overlay.vue
Normal file
0
src/components/userAchi/tua-nc-overlay.vue
Normal file
135
src/components/userAchi/tua-series.vue
Normal file
135
src/components/userAchi/tua-series.vue
Normal file
@@ -0,0 +1,135 @@
|
||||
<template>
|
||||
<div class="tuas-card" @click="selectSeries" v-if="data">
|
||||
<div class="tuas-version">v{{ data.version }}</div>
|
||||
<img alt="icon" class="tuas-icon" :src="data.icon" />
|
||||
<div class="tuas-content">
|
||||
<span :title="data.name">{{ data.name }}</span>
|
||||
<span>{{ overview.fin }}/{{ overview.total }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { listen, UnlistenFn } from "@tauri-apps/api/event";
|
||||
import { ref, onMounted, watch, onUnmounted } from "vue";
|
||||
|
||||
import { AppAchievementSeriesData } from "../../data/index.js";
|
||||
import TSUserAchi from "../../plugins/Sqlite/modules/userAchi.js";
|
||||
import showSnackbar from "../func/snackbar.js";
|
||||
|
||||
interface TuaSeriesProps {
|
||||
uid: number;
|
||||
series: number;
|
||||
cur: number;
|
||||
}
|
||||
|
||||
interface TuaSeriesEmits {
|
||||
(e: "selectSeries", v: number): void;
|
||||
}
|
||||
|
||||
const props = defineProps<TuaSeriesProps>();
|
||||
const emits = defineEmits<TuaSeriesEmits>();
|
||||
|
||||
const overview = ref<TGApp.Sqlite.Achievement.Overview>({ fin: 0, total: 0 });
|
||||
const data = ref<TGApp.App.Achievement.Series | undefined>(
|
||||
AppAchievementSeriesData.find((s) => s.id === props.series),
|
||||
);
|
||||
let achiListener: UnlistenFn | null = null;
|
||||
|
||||
onMounted(async () => {
|
||||
await refreshOverview();
|
||||
achiListener = await listenAchi();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.cur,
|
||||
async () => await refreshOverview(),
|
||||
);
|
||||
|
||||
async function refreshOverview(): Promise<void> {
|
||||
overview.value = await TSUserAchi.getOverview(props.uid, props.series);
|
||||
console.log(overview.value);
|
||||
}
|
||||
|
||||
async function listenAchi(): Promise<UnlistenFn> {
|
||||
return await listen<number>("updateAchi", async (e) => {
|
||||
if (e.payload === props.series) await refreshOverview();
|
||||
});
|
||||
}
|
||||
|
||||
onUnmounted(async () => {
|
||||
if (achiListener !== null) {
|
||||
achiListener();
|
||||
achiListener = null;
|
||||
}
|
||||
});
|
||||
|
||||
async function selectSeries(): Promise<void> {
|
||||
if (props.cur === props.series) {
|
||||
showSnackbar({
|
||||
text: "已选中当前系列!",
|
||||
color: "warn",
|
||||
});
|
||||
return;
|
||||
}
|
||||
emits("selectSeries", props.series);
|
||||
}
|
||||
</script>
|
||||
<style lang="css" scoped>
|
||||
.tuas-card {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 10px;
|
||||
border-radius: 10px;
|
||||
background: var(--box-bg-1);
|
||||
color: var(--box-text-1);
|
||||
column-gap: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tuas-version {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 80px;
|
||||
border-top: 1px solid var(--common-shadow-1);
|
||||
border-left: 1px solid var(--common-shadow-1);
|
||||
background: var(--box-bg-2);
|
||||
border-bottom-right-radius: 10px;
|
||||
border-top-left-radius: 20px;
|
||||
color: var(--tgc-yellow-1);
|
||||
font-family: var(--font-title);
|
||||
font-size: 10px;
|
||||
text-align: center;
|
||||
text-shadow: 1px 1px 1px var(--common-shadow-1);
|
||||
}
|
||||
|
||||
.tuas-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
padding: 5px;
|
||||
border-radius: 50%;
|
||||
background: var(--tgc-dark-7);
|
||||
}
|
||||
|
||||
.tuas-content {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
flex-flow: column wrap;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
color: var(--box-text-1);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.tuas-content :first-child {
|
||||
font-family: var(--font-title);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.tuas-content :last-child {
|
||||
font-size: 12px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
</style>
|
||||
@@ -1,127 +1,73 @@
|
||||
<template>
|
||||
<div class="top-bar">
|
||||
<div class="top-title" @click="switchHideFin">{{ title }}</div>
|
||||
<v-text-field
|
||||
v-model="search"
|
||||
append-icon="mdi-magnify"
|
||||
label="搜索"
|
||||
:hide-details="true"
|
||||
variant="outlined"
|
||||
@click:append="searchCard"
|
||||
@keyup.enter="searchCard"
|
||||
/>
|
||||
<div class="top-btns">
|
||||
<v-btn prepend-icon="mdi-import" class="top-btn" @click="importJson"> 导入</v-btn>
|
||||
<v-btn prepend-icon="mdi-export" class="top-btn" @click="exportJson"> 导出</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
<ToLoading v-model="loading" :title="loadingTitle" />
|
||||
<v-app-bar>
|
||||
<div class="top-title" @click="switchHideFin">{{ title }}</div>
|
||||
<template #append>
|
||||
<div class="achi-search">
|
||||
<v-text-field
|
||||
v-model="search"
|
||||
append-icon="mdi-magnify"
|
||||
label="搜索"
|
||||
:hide-details="true"
|
||||
:single-line="true"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #extension>
|
||||
<v-btn prepend-icon="mdi-import" class="top-btn" @click="importJson()">导入</v-btn>
|
||||
<v-btn prepend-icon="mdi-export" class="top-btn" @click="exportJson()">导出</v-btn>
|
||||
<div class="uid-select">
|
||||
<v-select
|
||||
variant="outlined"
|
||||
v-model="uidCur"
|
||||
:items="uidList"
|
||||
:hide-details="true"
|
||||
label="存档UID"
|
||||
/>
|
||||
</div>
|
||||
<v-btn prepend-icon="mdi-plus" class="top-btn" @click="createUid()">新建存档</v-btn>
|
||||
<v-btn prepend-icon="mdi-delete" class="top-btn" @click="deleteUid()">删除存档</v-btn>
|
||||
<v-spacer />
|
||||
</template>
|
||||
</v-app-bar>
|
||||
<div class="wrap">
|
||||
<!-- 左侧菜单 -->
|
||||
<div class="left-wrap">
|
||||
<div
|
||||
v-for="series in allSeriesData"
|
||||
:key="series.id"
|
||||
class="card-series"
|
||||
@click="selectSeries(series.id)"
|
||||
>
|
||||
<div class="series-version">v{{ series.version }}</div>
|
||||
<img alt="icon" class="series-icon" :src="getIcon(series.id)" />
|
||||
<div class="series-content">
|
||||
<span :title="series.name">
|
||||
{{ series.name }}
|
||||
</span>
|
||||
<span> {{ series.finCount }} / {{ series.totalCount }} </span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 右侧内容-->
|
||||
<div class="right-wrap">
|
||||
<div v-if="curCardName !== '' && selectedSeries !== -1 && !loading">
|
||||
<TopNamecard :data="curCard" @selected="openImg()" v-if="curCard" />
|
||||
</div>
|
||||
<div
|
||||
v-for="(achievement, index) in renderSelect"
|
||||
:key="achievement.id"
|
||||
class="card-achi"
|
||||
:title="allSeriesData.find((item) => item.id === achievement.series)?.name ?? ''"
|
||||
@click="showAchiInfo(achievement, index)"
|
||||
>
|
||||
<div class="achi-version">v{{ achievement.version }}</div>
|
||||
<div class="achi-pre">
|
||||
<div class="achi-pre-icon">
|
||||
<v-icon
|
||||
v-if="!achievement.isCompleted"
|
||||
color="var(--tgc-blue-3)"
|
||||
@click="setAchi(achievement, true)"
|
||||
style="cursor: pointer"
|
||||
>
|
||||
mdi-circle
|
||||
</v-icon>
|
||||
<v-icon
|
||||
v-else
|
||||
class="achievement-finish"
|
||||
style="cursor: pointer"
|
||||
@click="setAchi(achievement, false)"
|
||||
>
|
||||
<img alt="finish" src="/source/UI/finish.webp" />
|
||||
</v-icon>
|
||||
</div>
|
||||
<div class="achi-pre-info">
|
||||
<span>
|
||||
<span>{{ achievement.name }}</span>
|
||||
<span v-if="achievement.progress !== 0">
|
||||
{{ achievement.progress }}
|
||||
</span>
|
||||
</span>
|
||||
<span>{{ achievement.description }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="achi-append">
|
||||
<span v-show="achievement.isCompleted">
|
||||
{{ achievement.completedTime }}
|
||||
</span>
|
||||
<div class="achi-append-icon">
|
||||
<img alt="icon" src="/icon/material/201.webp" />
|
||||
<span>{{ achievement.reward }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="left-wrap" v-if="uidCur">
|
||||
<TuaSeries
|
||||
v-for="(series, index) in seriesList"
|
||||
:key="index"
|
||||
@click="selectSeries(series)"
|
||||
v-model:cur="selectedSeries"
|
||||
:uid="uidCur"
|
||||
:series="series"
|
||||
/>
|
||||
</div>
|
||||
<TuaAchiList
|
||||
v-if="uidCur"
|
||||
:uid="uidCur"
|
||||
:hideFin="hideFin"
|
||||
v-model:series="selectedSeries"
|
||||
v-model:search="search"
|
||||
/>
|
||||
</div>
|
||||
<ToNamecard v-model="showNameCard" :data="curCard" />
|
||||
<ToAchiInfo v-model="showAchi" :data="showAchiData" @select-s="selectSeries">
|
||||
<template #left>
|
||||
<div class="card-arrow left" @click="switchAchiInfo(false)">
|
||||
<img src="../../assets/icons/arrow-right.svg" alt="right" />
|
||||
</div>
|
||||
</template>
|
||||
<template #right>
|
||||
<div class="card-arrow" @click="switchAchiInfo(true)">
|
||||
<img src="../../assets/icons/arrow-right.svg" alt="right" />
|
||||
</div>
|
||||
</template>
|
||||
</ToAchiInfo>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { path } from "@tauri-apps/api";
|
||||
import { UnlistenFn, listen } from "@tauri-apps/api/event";
|
||||
import { open, save } from "@tauri-apps/plugin-dialog";
|
||||
import { writeTextFile } from "@tauri-apps/plugin-fs";
|
||||
import { computed, nextTick, onMounted, ref } from "vue";
|
||||
import { onMounted, ref, computed, onUnmounted } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
|
||||
import showConfirm from "../../components/func/confirm.js";
|
||||
import showSnackbar from "../../components/func/snackbar.js";
|
||||
import ToAchiInfo from "../../components/overlay/to-achiInfo.vue";
|
||||
import ToLoading from "../../components/overlay/to-loading.vue";
|
||||
import ToNamecard from "../../components/overlay/to-namecard.vue";
|
||||
import TopNamecard from "../../components/overlay/top-namecard.vue";
|
||||
import { AppAchievementSeriesData, AppNameCardsData } from "../../data/index.js";
|
||||
import TuaAchiList from "../../components/userAchi/tua-achi-list.vue";
|
||||
import TuaSeries from "../../components/userAchi/tua-series.vue";
|
||||
import { AppAchievementSeriesData } from "../../data/index.js";
|
||||
import TSUserAchi from "../../plugins/Sqlite/modules/userAchi.js";
|
||||
import { useAchievementsStore } from "../../store/modules/achievements.js";
|
||||
import TGLogger from "../../utils/TGLogger.js";
|
||||
import { getNowStr } from "../../utils/toolFunc.js";
|
||||
import {
|
||||
getUiafHeader,
|
||||
readUiafData,
|
||||
@@ -129,214 +75,67 @@ import {
|
||||
verifyUiafDataClipboard,
|
||||
} from "../../utils/UIAF.js";
|
||||
|
||||
// Store
|
||||
const achievementsStore = useAchievementsStore();
|
||||
|
||||
// loading
|
||||
const loading = ref<boolean>(true);
|
||||
const loadingTitle = ref<string>("正在加载数据");
|
||||
const search = ref<string>("");
|
||||
const hideFin = ref<boolean>(false);
|
||||
const showNameCard = ref<boolean>(false);
|
||||
const showAchi = ref<boolean>(false);
|
||||
// data
|
||||
const title = ref(achievementsStore.title);
|
||||
const curCardName = ref<string>("");
|
||||
let curCard = ref<TGApp.App.NameCard.Item>();
|
||||
// series
|
||||
const allSeriesData = ref<TGApp.Sqlite.Achievement.SeriesTable[]>([]);
|
||||
|
||||
const uidList = ref<number[]>([]);
|
||||
const uidCur = ref<number>(0);
|
||||
const overview = ref<TGApp.Sqlite.Achievement.Overview>({ fin: 0, total: 1 });
|
||||
const seriesList = AppAchievementSeriesData.sort((a, b) => a.order - b.order).map((s) => s.id);
|
||||
const selectedSeries = ref<number>(-1);
|
||||
const selectedAchievement = ref<TGApp.Sqlite.Achievement.SingleTable[]>([]);
|
||||
const showAchiData = ref<TGApp.Sqlite.Achievement.SingleTable>();
|
||||
const curAchiDataIndex = ref<number>(0);
|
||||
const renderSelect = computed(() => {
|
||||
if (hideFin.value) {
|
||||
return selectedAchievement.value.filter((item) => item.isCompleted === 0);
|
||||
}
|
||||
return selectedAchievement.value;
|
||||
|
||||
const title = computed<string>(() => {
|
||||
const percentage = ((overview.value.fin * 100) / overview.value.total).toFixed(2);
|
||||
return `${overview.value.fin}/${overview.value.total} ${percentage}%`;
|
||||
});
|
||||
|
||||
// route
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
// 更改是否隐藏已完成
|
||||
let achiListener: UnlistenFn | null = null;
|
||||
|
||||
async function switchHideFin() {
|
||||
const text = hideFin.value ? "显示已完成" : "隐藏已完成";
|
||||
const res = await showConfirm({
|
||||
title: "是否切换显示已完成?",
|
||||
text,
|
||||
});
|
||||
const res = await showConfirm({ title: "是否切换显示已完成?", text });
|
||||
if (!res) {
|
||||
showSnackbar({
|
||||
color: "cancel",
|
||||
text: "已取消切换",
|
||||
});
|
||||
showSnackbar({ color: "cancel", text: "已取消切换" });
|
||||
return;
|
||||
}
|
||||
hideFin.value = !hideFin.value;
|
||||
showSnackbar({
|
||||
text: `已${text}`,
|
||||
});
|
||||
}
|
||||
|
||||
// 刷新概况
|
||||
async function flushOverview(): Promise<void> {
|
||||
const { total, fin } = await TSUserAchi.getOverview();
|
||||
achievementsStore.flushData(total, fin);
|
||||
title.value = achievementsStore.title;
|
||||
showSnackbar({ text: `已${text}`, color: "success" });
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await TGLogger.Info("[Achievements][onMounted] 打开成就页面");
|
||||
await TGLogger.Info(`[Achievements][onMounted] 当前版本:${achievementsStore.lastVersion}`);
|
||||
loading.value = true;
|
||||
loadingTitle.value = "正在获取成就系列数据";
|
||||
await flushOverview();
|
||||
await TGLogger.Info(`[Achievements][onMounted] ${title.value}`);
|
||||
allSeriesData.value = await TSUserAchi.getSeries();
|
||||
achievementsStore.lastVersion = await TSUserAchi.getLatestAchiVersion();
|
||||
loadingTitle.value = "正在获取成就数据";
|
||||
selectedAchievement.value = await getAchiData("all");
|
||||
uidList.value = await TSUserAchi.getAllUid();
|
||||
if (uidList.value.length === 0) uidList.value = [0];
|
||||
uidCur.value = uidList.value[0];
|
||||
await refreshOverview();
|
||||
loading.value = false;
|
||||
if (route.query.app && typeof route.query.app === "string") {
|
||||
await handleImportOuter(route.query.app);
|
||||
} else {
|
||||
// 等 500ms 动画
|
||||
setTimeout(() => {
|
||||
showSnackbar({
|
||||
text: `已获取 ${renderSelect.value.length} 条成就数据`,
|
||||
});
|
||||
}, 500);
|
||||
}
|
||||
achiListener = await listen<number>("updateAchi", async () => await refreshOverview());
|
||||
});
|
||||
|
||||
onUnmounted(async () => {
|
||||
if (achiListener !== null) {
|
||||
achiListener();
|
||||
achiListener = null;
|
||||
}
|
||||
});
|
||||
|
||||
// 渲染选中的成就系列
|
||||
async function selectSeries(index: number): Promise<void> {
|
||||
// 如果选中的是已经选中的系列,则不进行操作
|
||||
if (selectedSeries.value === index) {
|
||||
showSnackbar({
|
||||
color: "warn",
|
||||
text: "已经选中该系列",
|
||||
});
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
loadingTitle.value = "正在获取对应的成就数据";
|
||||
selectedSeries.value = index;
|
||||
selectedAchievement.value = await getAchiData("series", index.toString());
|
||||
loadingTitle.value = "正在查找对应的成就名片";
|
||||
curCardName.value = await TSUserAchi.getSeriesNameCard(String(index));
|
||||
if (curCardName.value !== "") {
|
||||
curCard.value = AppNameCardsData.find((item) => item.name === curCardName.value);
|
||||
}
|
||||
// 右侧滚动到顶部
|
||||
const rightWrap = document.querySelector(".right-wrap");
|
||||
if (rightWrap) {
|
||||
rightWrap.scrollTop = 0;
|
||||
}
|
||||
// 刷新overlay数据
|
||||
curAchiDataIndex.value = selectedAchievement.value.findIndex(
|
||||
(i) => i.id === showAchiData.value?.id,
|
||||
);
|
||||
await nextTick(() => {
|
||||
loading.value = false;
|
||||
// 等 500ms 动画
|
||||
setTimeout(() => {
|
||||
showSnackbar({
|
||||
text: `已获取 ${renderSelect.value.length} 条成就数据`,
|
||||
});
|
||||
}, 500);
|
||||
});
|
||||
async function refreshOverview(): Promise<void> {
|
||||
overview.value = await TSUserAchi.getOverview(uidCur.value);
|
||||
}
|
||||
|
||||
// 打开图片
|
||||
function openImg(): void {
|
||||
showNameCard.value = true;
|
||||
function selectSeries(series: number): void {
|
||||
selectedSeries.value = series;
|
||||
}
|
||||
|
||||
// 打开成就详情
|
||||
function showAchiInfo(item: TGApp.Sqlite.Achievement.SingleTable, index: number): void {
|
||||
showAchiData.value = item;
|
||||
showAchi.value = true;
|
||||
curAchiDataIndex.value = index;
|
||||
}
|
||||
|
||||
// 切换成就详情
|
||||
function switchAchiInfo(next: boolean) {
|
||||
if (next) {
|
||||
if (curAchiDataIndex.value === renderSelect.value.length - 1) {
|
||||
showSnackbar({
|
||||
color: "warn",
|
||||
text: "已经是最后一个了",
|
||||
});
|
||||
return;
|
||||
}
|
||||
curAchiDataIndex.value++;
|
||||
} else {
|
||||
if (curAchiDataIndex.value === 0) {
|
||||
showSnackbar({
|
||||
color: "warn",
|
||||
text: "已经是第一个了",
|
||||
});
|
||||
return;
|
||||
}
|
||||
curAchiDataIndex.value--;
|
||||
}
|
||||
showAchiData.value = renderSelect.value[curAchiDataIndex.value];
|
||||
}
|
||||
|
||||
async function searchAll(): Promise<void> {
|
||||
if (selectedAchievement.value.length === achievementsStore.totalAchievements) {
|
||||
showSnackbar({
|
||||
color: "warn",
|
||||
text: "已经是全部成就",
|
||||
});
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
loadingTitle.value = "正在获取全部成就数据";
|
||||
selectedSeries.value = -1;
|
||||
selectedAchievement.value = await getAchiData("all");
|
||||
await nextTick(() => {
|
||||
loading.value = false;
|
||||
setTimeout(() => {
|
||||
showSnackbar({
|
||||
text: `已获取 ${renderSelect.value.length} 条成就数据`,
|
||||
});
|
||||
}, 500);
|
||||
});
|
||||
}
|
||||
|
||||
async function searchCard(): Promise<void> {
|
||||
if (search.value === "") {
|
||||
await searchAll();
|
||||
return;
|
||||
}
|
||||
selectedSeries.value = -1;
|
||||
loadingTitle.value = "正在搜索";
|
||||
loading.value = true;
|
||||
await TGLogger.Info(`[Achievements][searchCard] 搜索内容:${search.value}`);
|
||||
selectedAchievement.value = await getAchiData("search", search.value);
|
||||
await nextTick(() => {
|
||||
loading.value = false;
|
||||
setTimeout(() => {
|
||||
if (renderSelect.value.length === 0) {
|
||||
showSnackbar({
|
||||
color: "error",
|
||||
text: "没有搜索到相关成就",
|
||||
});
|
||||
return;
|
||||
}
|
||||
showSnackbar({
|
||||
text: `已获取 ${renderSelect.value.length} 条成就数据`,
|
||||
});
|
||||
}, 500);
|
||||
});
|
||||
await TGLogger.Info(`[Achievements][searchCard] 搜索到 ${renderSelect.value.length} 条成就数据`);
|
||||
}
|
||||
|
||||
// 导入 UIAF 数据,进行数据合并、刷新
|
||||
async function importJson(): Promise<void> {
|
||||
await TGLogger.Info("[Achievements][importJson] 导入 UIAF 数据");
|
||||
const selectedFile = await open({
|
||||
@@ -361,40 +160,50 @@ async function importJson(): Promise<void> {
|
||||
}
|
||||
const check = await verifyUiafData(selectedFile);
|
||||
if (!check) return;
|
||||
let uidInput = await showConfirm({
|
||||
mode: "input",
|
||||
title: "请输入存档UID",
|
||||
text: "UID:",
|
||||
input: uidCur.value.toString(),
|
||||
});
|
||||
if (uidInput === false) {
|
||||
showSnackbar({ text: "已取消存档导入!", color: "cancel" });
|
||||
return;
|
||||
}
|
||||
if (uidInput === undefined) uidInput = uidCur.value.toString();
|
||||
else if (isNaN(Number(uidInput))) {
|
||||
showSnackbar({ text: "请输入合法数字", color: "warn" });
|
||||
return;
|
||||
}
|
||||
const remoteRaw = await readUiafData(selectedFile);
|
||||
await TGLogger.Info("[Achievements][importJson] 读取 UIAF 数据成功");
|
||||
await TGLogger.Info(`[Achievements][importJson] 导入来源:${remoteRaw.info.export_app}`);
|
||||
await TGLogger.Info(`[Achievements][importJson] 导入版本:${remoteRaw.info.export_app_version}`);
|
||||
await TGLogger.Info(`[Achievements][importJson] 导入时间:${remoteRaw.info.export_timestamp}`);
|
||||
await TGLogger.Info(`[Achievements][importJson] 导入数据:${remoteRaw.list.length} 条`);
|
||||
await TGLogger.Info(`[Achievements][importJson] 导入存档:${uidInput}`);
|
||||
loadingTitle.value = "正在解析数据";
|
||||
loading.value = true;
|
||||
loadingTitle.value = "正在合并成就数据";
|
||||
await TSUserAchi.mergeUIAF(remoteRaw.list);
|
||||
await TSUserAchi.mergeUiaf(remoteRaw.list, Number(uidInput));
|
||||
loadingTitle.value = "即将刷新页面";
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// 导出
|
||||
async function exportJson(): Promise<void> {
|
||||
await TGLogger.Info("[Achievements][exportJson] 导出 UIAF 数据");
|
||||
// 判断是否有数据
|
||||
if (achievementsStore.finAchievements === 0) {
|
||||
showSnackbar({
|
||||
color: "error",
|
||||
text: "没有可导出的数据",
|
||||
});
|
||||
if (overview.value.fin === 0) {
|
||||
showSnackbar({ color: "error", text: "没有可导出的数据" });
|
||||
await TGLogger.Warn("[Achievements][exportJson] 没有可导出的数据");
|
||||
return;
|
||||
}
|
||||
// 获取本地数据
|
||||
const UiafData = {
|
||||
info: await getUiafHeader(),
|
||||
list: await TSUserAchi.getUIAF(),
|
||||
list: await TSUserAchi.getUiafData(uidCur.value),
|
||||
};
|
||||
const fileName = `UIAF_${UiafData.info.export_app}_${UiafData.info.export_app_version}_${UiafData.info.export_timestamp}`;
|
||||
const fileName = `UIAF_${UiafData.info.export_app}_${UiafData.info.export_app_version}_${uidCur.value}`;
|
||||
const isSave = await save({
|
||||
title: "导出 UIAF 数据",
|
||||
filters: [
|
||||
@@ -406,24 +215,16 @@ async function exportJson(): Promise<void> {
|
||||
defaultPath: `${await path.downloadDir()}${path.sep()}${fileName}.json`,
|
||||
});
|
||||
if (isSave === null) {
|
||||
showSnackbar({
|
||||
color: "warn",
|
||||
text: "已取消导出",
|
||||
});
|
||||
showSnackbar({ color: "warn", text: "已取消导出" });
|
||||
await TGLogger.Info("[Achievements][exportJson] 已取消导出");
|
||||
return;
|
||||
}
|
||||
await writeTextFile(isSave, JSON.stringify(UiafData));
|
||||
showSnackbar({ text: "导出成功" });
|
||||
showSnackbar({ text: "导出成功", color: "success" });
|
||||
await TGLogger.Info("[Achievements][exportJson] 导出成功");
|
||||
await TGLogger.Info(`[Achievements][exportJson] 导出路径:${isSave}`);
|
||||
}
|
||||
|
||||
function getIcon(series: number): string | undefined {
|
||||
return AppAchievementSeriesData.find((item) => item.id === series)?.icon;
|
||||
}
|
||||
|
||||
// 处理外部导入
|
||||
async function handleImportOuter(app: string): Promise<void> {
|
||||
await TGLogger.Info(`[Achievements][handleImportOuter] 导入来源:${app}`);
|
||||
const confirm = await showConfirm({
|
||||
@@ -442,110 +243,85 @@ async function handleImportOuter(app: string): Promise<void> {
|
||||
const clipboard = await window.navigator.clipboard.readText();
|
||||
const check = await verifyUiafDataClipboard();
|
||||
if (!check) return;
|
||||
let uidInput = await showConfirm({
|
||||
mode: "input",
|
||||
title: "请输入存档UID",
|
||||
text: "UID:",
|
||||
input: uidCur.value.toString(),
|
||||
});
|
||||
if (uidInput === false) {
|
||||
showSnackbar({ text: "已取消存档导入!", color: "cancel" });
|
||||
return;
|
||||
}
|
||||
if (uidInput === undefined) uidInput = uidCur.value.toString();
|
||||
else if (isNaN(Number(uidInput))) {
|
||||
showSnackbar({ text: "请输入合法数字", color: "warn" });
|
||||
return;
|
||||
}
|
||||
const data: TGApp.Plugins.UIAF.Data = JSON.parse(clipboard);
|
||||
loadingTitle.value = "正在导入数据";
|
||||
loading.value = true;
|
||||
await TSUserAchi.mergeUIAF(data.list);
|
||||
await TSUserAchi.mergeUiaf(data.list, Number(uidInput));
|
||||
loading.value = false;
|
||||
showSnackbar({
|
||||
color: "success",
|
||||
text: "导入成功,即将刷新页面",
|
||||
});
|
||||
showSnackbar({ color: "success", text: "导入成功,即将刷新页面" });
|
||||
await TGLogger.Info("[Achievements][handleImportOuter] 导入成功");
|
||||
setTimeout(async () => {
|
||||
await router.push("/achievements");
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
// 改变成就状态
|
||||
async function setAchi(
|
||||
achievement: TGApp.Sqlite.Achievement.SingleTable,
|
||||
target: boolean,
|
||||
): Promise<void> {
|
||||
const newAchievement = achievement;
|
||||
if (target) {
|
||||
// 取消已完成
|
||||
newAchievement.isCompleted = 1;
|
||||
newAchievement.completedTime = getNowStr();
|
||||
} else {
|
||||
newAchievement.isCompleted = 0;
|
||||
newAchievement.completedTime = "";
|
||||
}
|
||||
renderSelect.value[renderSelect.value.findIndex((item) => item.id === achievement.id)] =
|
||||
newAchievement;
|
||||
await TSUserAchi.updateAchievement(newAchievement);
|
||||
await flushOverview();
|
||||
const seriesIndex = allSeriesData.value.findIndex((item) => item.id === newAchievement.series);
|
||||
if (seriesIndex === -1) return;
|
||||
const seriesGet = await TSUserAchi.getSeries(newAchievement.series);
|
||||
allSeriesData.value[seriesIndex] = seriesGet[0];
|
||||
showSnackbar({
|
||||
text: `已将成就 ${newAchievement.name}[${newAchievement.id}] 标记为 ${
|
||||
target ? "已完成" : "未完成"
|
||||
}`,
|
||||
});
|
||||
await TGLogger.Info(
|
||||
`[Achievements][setAchi] 已将成就 ${newAchievement.name}[${newAchievement.id}] 标记为 ${
|
||||
target ? "已完成" : "未完成"
|
||||
}`,
|
||||
);
|
||||
async function createUid(): Promise<void> {
|
||||
// todo
|
||||
}
|
||||
|
||||
// 获取成就(某个系列)
|
||||
async function getAchiData(
|
||||
type: "all" | "series" | "search",
|
||||
value?: string,
|
||||
): Promise<TGApp.Sqlite.Achievement.SingleTable[]> {
|
||||
if (type !== "search") {
|
||||
return TSUserAchi.getAchievements(value);
|
||||
}
|
||||
if (value === undefined) {
|
||||
showSnackbar({
|
||||
color: "error",
|
||||
text: "搜索内容不能为空",
|
||||
});
|
||||
return [];
|
||||
}
|
||||
return TSUserAchi.searchAchievements(value);
|
||||
async function deleteUid(): Promise<void> {
|
||||
// todo
|
||||
}
|
||||
</script>
|
||||
<!-- 顶部栏跟 wrap 大概布局 -->
|
||||
<style lang="css" scoped>
|
||||
.top-bar {
|
||||
.achi-search {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 80px;
|
||||
width: 400px;
|
||||
height: 50px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
justify-content: center;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
margin-bottom: 10px;
|
||||
background: var(--box-bg-1);
|
||||
column-gap: 50px;
|
||||
font-family: var(--font-title);
|
||||
font-size: 20px;
|
||||
margin: 0 10px;
|
||||
color: var(--box-text-1);
|
||||
}
|
||||
|
||||
.top-title {
|
||||
margin-left: 15px;
|
||||
color: var(--common-text-title);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.top-btns {
|
||||
display: flex;
|
||||
column-gap: 10px;
|
||||
font-family: var(--font-title);
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.top-btn {
|
||||
border-radius: 5px;
|
||||
background: var(--tgc-btn-1);
|
||||
color: var(--btn-text);
|
||||
height: 40px;
|
||||
border: 1px solid var(--common-shadow-2);
|
||||
margin-left: 15px;
|
||||
background: var(--btn-bg-1);
|
||||
color: var(--btn-text-1);
|
||||
font-family: var(--font-title);
|
||||
}
|
||||
|
||||
.uid-select {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 15px;
|
||||
margin-left: 15px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* 内容区域 */
|
||||
.wrap {
|
||||
display: flex;
|
||||
height: calc(100vh - 130px);
|
||||
height: calc(100vh - 150px);
|
||||
column-gap: 10px;
|
||||
}
|
||||
|
||||
@@ -559,222 +335,4 @@ async function getAchiData(
|
||||
overflow-y: auto;
|
||||
row-gap: 10px;
|
||||
}
|
||||
|
||||
/* 右侧成就 */
|
||||
.right-wrap {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
flex-direction: column;
|
||||
padding-right: 10px;
|
||||
overflow-y: scroll;
|
||||
row-gap: 10px;
|
||||
}
|
||||
</style>
|
||||
<!-- 左侧成就系列 wrap -->
|
||||
<style lang="css" scoped>
|
||||
.card-series {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 10px;
|
||||
border-radius: 10px;
|
||||
background: var(--box-bg-1);
|
||||
color: var(--box-text-1);
|
||||
column-gap: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* 版本信息 */
|
||||
.series-version {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 80px;
|
||||
border-top: 1px solid var(--common-shadow-1);
|
||||
border-left: 1px solid var(--common-shadow-1);
|
||||
background: var(--box-bg-2);
|
||||
border-bottom-right-radius: 10px;
|
||||
border-top-left-radius: 20px;
|
||||
color: var(--tgc-yellow-1);
|
||||
font-family: var(--font-title);
|
||||
font-size: 10px;
|
||||
text-align: center;
|
||||
text-shadow: 1px 1px 1px var(--common-shadow-1);
|
||||
}
|
||||
|
||||
.series-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
padding: 5px;
|
||||
border-radius: 50%;
|
||||
background: var(--tgc-dark-7);
|
||||
}
|
||||
|
||||
.series-content {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
flex-flow: column wrap;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
color: var(--box-text-1);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.series-content :nth-child(1) {
|
||||
font-family: var(--font-title);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.series-content :nth-child(2) {
|
||||
font-size: 12px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
</style>
|
||||
<!-- 右侧成就 -->
|
||||
<style lang="css" scoped>
|
||||
/* 成就卡片 */
|
||||
.card-achi {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px;
|
||||
border-radius: 10px;
|
||||
background: var(--box-bg-1);
|
||||
color: var(--box-text-7);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* 成就进度 */
|
||||
.achi-version {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 50px;
|
||||
border-right: 1px solid var(--common-shadow-1);
|
||||
border-bottom: 1px solid var(--common-shadow-1);
|
||||
background: var(--box-bg-2);
|
||||
border-bottom-right-radius: 20px;
|
||||
border-top-left-radius: 10px;
|
||||
color: var(--tgc-pink-1);
|
||||
font-family: var(--font-title);
|
||||
font-size: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.achi-pre {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
column-gap: 10px;
|
||||
}
|
||||
|
||||
.achi-pre-icon {
|
||||
display: flex;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.achievement-finish img {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
filter: invert(51%) sepia(100%) saturate(353%) hue-rotate(42deg) brightness(107%) contrast(91%);
|
||||
}
|
||||
|
||||
.achi-pre-info {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
flex-flow: column wrap;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.achi-pre-info :nth-child(1) {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
column-gap: 5px;
|
||||
font-family: var(--font-title);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.achi-pre-info :nth-child(1) :nth-child(2) {
|
||||
color: var(--tgc-blue-2);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.achi-pre-info :nth-child(2) {
|
||||
font-size: 12px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.achi-append-icon span {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 10px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgb(0 0 0 / 50%);
|
||||
border-bottom-left-radius: 5px;
|
||||
border-bottom-right-radius: 5px;
|
||||
color: var(--tgc-white-1);
|
||||
font-size: 8px;
|
||||
}
|
||||
|
||||
.achi-append {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
column-gap: 10px;
|
||||
}
|
||||
|
||||
.achi-append :nth-last-child(2) {
|
||||
margin-right: 10px;
|
||||
color: var(--box-text-4);
|
||||
font-size: small;
|
||||
}
|
||||
|
||||
.achi-append-icon {
|
||||
position: relative;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 5px;
|
||||
background-image: url("/icon/bg/5-Star.webp");
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.achi-append-icon img {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.card-arrow {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dark .card-arrow {
|
||||
filter: invert(11%) sepia(73%) saturate(11%) hue-rotate(139deg) brightness(97%) contrast(81%);
|
||||
}
|
||||
|
||||
.card-arrow img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.card-arrow.left img {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -121,8 +121,6 @@ import showConfirm from "../../components/func/confirm.js";
|
||||
import showSnackbar from "../../components/func/snackbar.js";
|
||||
import ToLoading from "../../components/overlay/to-loading.vue";
|
||||
import TGSqlite from "../../plugins/Sqlite/index.js";
|
||||
import TSUserAchi from "../../plugins/Sqlite/modules/userAchi.js";
|
||||
import { useAchievementsStore } from "../../store/modules/achievements.js";
|
||||
import { useAppStore } from "../../store/modules/app.js";
|
||||
import { useHomeStore } from "../../store/modules/home.js";
|
||||
import { backUpUserData, restoreUserData } from "../../utils/dataBS.js";
|
||||
@@ -134,7 +132,6 @@ import TGRequest from "../../web/request/TGRequest.js";
|
||||
// Store
|
||||
const appStore = useAppStore();
|
||||
const homeStore = useHomeStore();
|
||||
const achievementsStore = useAchievementsStore();
|
||||
|
||||
const isDevEnv = ref<boolean>(import.meta.env.MODE === "development");
|
||||
|
||||
@@ -257,7 +254,6 @@ async function confirmUpdate(title?: string): Promise<void> {
|
||||
loadingTitle.value = "正在更新数据库...";
|
||||
loading.value = true;
|
||||
await TGSqlite.update();
|
||||
achievementsStore.lastVersion = await TSUserAchi.getLatestAchiVersion();
|
||||
appStore.buildTime = getBuildTime();
|
||||
loading.value = false;
|
||||
showSnackbar({
|
||||
@@ -398,7 +394,6 @@ async function confirmResetApp(): Promise<void> {
|
||||
}
|
||||
appStore.init();
|
||||
homeStore.init();
|
||||
achievementsStore.init();
|
||||
await TGLogger.Info("[Config][confirmResetApp] 恢复默认设置完成");
|
||||
showSnackbar({ text: "已恢复默认配置!即将刷新页面..." });
|
||||
setTimeout(() => {
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
append-icon="mdi-magnify"
|
||||
label="请输入帖子 ID 或搜索词"
|
||||
:single-line="true"
|
||||
hide-details
|
||||
:hide-details="true"
|
||||
@keyup.enter="searchPost()"
|
||||
@click:append="searchPost()"
|
||||
/>
|
||||
@@ -47,7 +47,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="load-news">
|
||||
<v-btn class="news-top-btn" rounded :loading="loadingSub" @click="loadMore(value)">
|
||||
<v-btn class="news-top-btn" :rounded="true" :loading="loadingSub" @click="loadMore(value)">
|
||||
已加载:{{ rawData[value].lastId }},加载更多
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* @file plugins/Sqlite/index.ts
|
||||
* @description Sqlite 数据库操作类
|
||||
* @since Beta v0.5.3
|
||||
* @since Beta v0.6.0
|
||||
*/
|
||||
|
||||
import { app } from "@tauri-apps/api";
|
||||
@@ -165,22 +165,6 @@ class Sqlite {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 检测数据库完整性
|
||||
* @since Beta v0.4.4
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
public async check(): Promise<boolean> {
|
||||
const db = await this.getDB();
|
||||
let isVerified = false;
|
||||
const sqlT = "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name;";
|
||||
const res: Array<{ name: string }> = await db.select(sqlT);
|
||||
if (this.tables.every((item) => res.map((i) => i.name).includes(item))) {
|
||||
isVerified = true;
|
||||
}
|
||||
return isVerified;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 重置数据库
|
||||
* @since Beta v0.4.0
|
||||
|
||||
@@ -1,150 +1,240 @@
|
||||
/**
|
||||
* @file plugins/Sqlite/modules/userAchi.ts
|
||||
* @description 用户成就模块
|
||||
* @since Beta v0.5.5
|
||||
* @since Beta v0.6.0
|
||||
*/
|
||||
|
||||
import { getUiafStatus } from "../../../utils/UIAF.js";
|
||||
import { path } from "@tauri-apps/api";
|
||||
import { exists, mkdir, readDir, readTextFile, writeTextFile } from "@tauri-apps/plugin-fs";
|
||||
|
||||
import { AppAchievementsData, AppAchievementSeriesData } from "../../../data/index.js";
|
||||
import TGLogger from "../../../utils/TGLogger.js";
|
||||
import { timestampToDate } from "../../../utils/toolFunc.js";
|
||||
import TGSqlite from "../index.js";
|
||||
import { importUIAFData } from "../sql/updateData.js";
|
||||
|
||||
/**
|
||||
* @description 获取成就概况
|
||||
* @since Beta v0.4.7
|
||||
* @returns {Promise<TGApp.Sqlite.Achievement.Overview}> 成就概况
|
||||
* @description 根据 completed 跟 progress 获取 status
|
||||
* @since Beta v0.6.0
|
||||
* @param {boolean} completed - 是否完成
|
||||
* @param {number} progress - 进度
|
||||
* @returns {number} status
|
||||
*/
|
||||
async function getOverview(): Promise<TGApp.Sqlite.Achievement.Overview> {
|
||||
const db = await TGSqlite.getDB();
|
||||
const res = await db.select<TGApp.Sqlite.Achievement.Overview[]>(
|
||||
"SELECT SUM(totalCount) as total,SUM(finCount) AS fin From AchievementSeries",
|
||||
);
|
||||
return res[0];
|
||||
function getUiafStatus(completed: boolean, progress: number): number {
|
||||
if (progress !== 0 && !completed) {
|
||||
return 1;
|
||||
} else if (progress === 0 && completed) {
|
||||
return 2;
|
||||
} else if (progress !== 0 && completed) {
|
||||
return 3;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 获取最新成就版本
|
||||
* @since Beta v0.4.7
|
||||
* @returns {Promise<string>} 最新成就版本
|
||||
* @since Beta v0.6.0
|
||||
* @returns {string} 最新成就版本
|
||||
*/
|
||||
async function getLatestAchiVersion(): Promise<string> {
|
||||
const db = await TGSqlite.getDB();
|
||||
type resType = { version: string };
|
||||
const res = await db.select<resType[]>(
|
||||
"SELECT version FROM Achievements ORDER BY version DESC LIMIT 1;",
|
||||
);
|
||||
return res[0].version;
|
||||
function getLatestAchiVersion(): string {
|
||||
let maxVersion = "0";
|
||||
for (const series of AppAchievementSeriesData) {
|
||||
if (series.version > maxVersion) maxVersion = series.version;
|
||||
}
|
||||
return maxVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 获取成就系列数据
|
||||
* @since Beta v0.4.7
|
||||
* @param {number|undefined} id 成就系列ID
|
||||
* @returns {Promise<TGApp.Sqlite.Achievement.SeriesTable[]>} 成就系列数据
|
||||
* @description 获取成就系列概况
|
||||
* @since Beta v0.6.0
|
||||
* @param {number} uid - 存档UID
|
||||
* @param {[number]} series - 系列ID
|
||||
* @returns {Promise<TGApp.Sqlite.Achievement.Overview>}
|
||||
*/
|
||||
async function getSeries(id?: number): Promise<TGApp.Sqlite.Achievement.SeriesTable[]> {
|
||||
async function getOverview(
|
||||
uid: number,
|
||||
series?: number,
|
||||
): Promise<TGApp.Sqlite.Achievement.Overview> {
|
||||
const db = await TGSqlite.getDB();
|
||||
let res: TGApp.Sqlite.Achievement.SeriesTable[];
|
||||
if (id === undefined) {
|
||||
res = await db.select<TGApp.Sqlite.Achievement.SeriesTable[]>(
|
||||
"SELECT * FROM AchievementSeries ORDER BY `order`;",
|
||||
);
|
||||
let totalAchi: number[] = [];
|
||||
if (series === undefined) {
|
||||
totalAchi = AppAchievementsData.map((i) => i.id);
|
||||
} else {
|
||||
res = await db.select<TGApp.Sqlite.Achievement.SeriesTable[]>(
|
||||
"SELECT * FROM AchievementSeries WHERE id = ?;",
|
||||
[id],
|
||||
);
|
||||
totalAchi = AppAchievementsData.filter((s) => s.series === series).map((i) => i.id);
|
||||
}
|
||||
return res;
|
||||
const finAchi = (
|
||||
await db.select<TGApp.Sqlite.Achievement.TableAchi[]>(
|
||||
"SELECT * FROM Achievements WHERE uid = ? AND isCompleted = 1;",
|
||||
[uid],
|
||||
)
|
||||
).filter((i) => totalAchi.includes(i.id));
|
||||
return { total: totalAchi.length, fin: finAchi.length };
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 合并成就数据
|
||||
* @since Beta v0.6.0
|
||||
* @param {TGApp.App.Achievement.Item} raw - 元数据
|
||||
* @param {[TGApp.Sqlite.Achievement.TableAchi]} data - 数据库数据
|
||||
* @param {[number]} uid - 存档 UID
|
||||
* @return {TGApp.Sqlite.Achievement.RenderAchi} - 渲染数据
|
||||
*/
|
||||
function getRenderAchi(
|
||||
raw: TGApp.App.Achievement.Item,
|
||||
uid?: number,
|
||||
data?: TGApp.Sqlite.Achievement.TableAchi,
|
||||
): TGApp.Sqlite.Achievement.RenderAchi {
|
||||
const emptyAchi: TGApp.Sqlite.Achievement.TableAchi = {
|
||||
id: 0,
|
||||
uid: uid ?? 0,
|
||||
isCompleted: 0,
|
||||
completedTime: "",
|
||||
progress: 0,
|
||||
updated: "",
|
||||
};
|
||||
const achiData = data ?? emptyAchi;
|
||||
return {
|
||||
id: raw.id,
|
||||
uid: achiData.uid,
|
||||
order: raw.order,
|
||||
series: raw.series,
|
||||
name: raw.name,
|
||||
description: raw.description,
|
||||
reward: raw.reward,
|
||||
version: raw.version,
|
||||
trigger: raw.trigger,
|
||||
isCompleted: achiData.isCompleted === 1,
|
||||
completedTime: achiData.completedTime,
|
||||
progress: achiData.progress,
|
||||
updated: achiData.updated,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 获取单个成就
|
||||
* @since Beta v0.6.0
|
||||
* @param {number} uid - 存档 UID
|
||||
* @param {number} id - 成就 ID
|
||||
* @returns {Promise<TGApp.Sqlite.Achievement.TableAchi|false>}
|
||||
*/
|
||||
async function getAchi(
|
||||
uid: number,
|
||||
id: number,
|
||||
): Promise<TGApp.Sqlite.Achievement.TableAchi | false> {
|
||||
const db = await TGSqlite.getDB();
|
||||
const res = await db.select<TGApp.Sqlite.Achievement.TableAchi[]>(
|
||||
"SELECT * FROM Achievements WHERE uid = ? AND id = ?;",
|
||||
[uid, id],
|
||||
);
|
||||
if (res.length === 0) return false;
|
||||
return res[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 获取成就数据
|
||||
* @since Beta v0.4.8
|
||||
* @param {number|undefined} id 成就系列ID
|
||||
* @returns {Promise<TGApp.Sqlite.Achievement.SingleTable[]>} 成就数据
|
||||
* @since Beta v0.6.0
|
||||
* @param {number} uid - 存档 UID
|
||||
* @param {[number]} series - 成就系列ID
|
||||
* @returns {Promise<TGApp.Sqlite.Achievement.RenderAchi[]>} 成就数据
|
||||
*/
|
||||
async function getAchievements(id?: string): Promise<TGApp.Sqlite.Achievement.SingleTable[]> {
|
||||
async function getAchievements(
|
||||
uid: number,
|
||||
series?: number,
|
||||
): Promise<TGApp.Sqlite.Achievement.RenderAchi[]> {
|
||||
const db = await TGSqlite.getDB();
|
||||
let res: TGApp.Sqlite.Achievement.SingleTable[];
|
||||
if (id === undefined) {
|
||||
res = await db.select<TGApp.Sqlite.Achievement.SingleTable[]>(
|
||||
"SELECT * FROM Achievements ORDER BY isCompleted,`order`;",
|
||||
);
|
||||
} else {
|
||||
res = await db.select<TGApp.Sqlite.Achievement.SingleTable[]>(
|
||||
"SELECT * FROM Achievements WHERE series = ? ORDER BY isCompleted,`order`;",
|
||||
[id],
|
||||
);
|
||||
const res: TGApp.Sqlite.Achievement.RenderAchi[] = [];
|
||||
const userData = await db.select<TGApp.Sqlite.Achievement.TableAchi[]>(
|
||||
"SELECT * FROM Achievements WHERE uid = ?;",
|
||||
[uid],
|
||||
);
|
||||
let rawData: TGApp.App.Achievement.Item[];
|
||||
if (series === undefined || series === -1) rawData = AppAchievementsData;
|
||||
else rawData = AppAchievementsData.filter((a) => a.series === series);
|
||||
for (const achi of rawData) {
|
||||
const achiFind = userData.find((u) => u.id === achi.id);
|
||||
const achievement = getRenderAchi(achi, uid, achiFind);
|
||||
res.push(achievement);
|
||||
}
|
||||
res.sort(
|
||||
(a, b) => a.isCompleted.toString().localeCompare(b.isCompleted.toString()) || a.order - b.order,
|
||||
);
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 获取成就名片
|
||||
* @since Beta v0.4.7
|
||||
* @param {string} id 成就系列ID
|
||||
* @returns {Promise<string>} 成就名片
|
||||
*/
|
||||
async function getSeriesNameCard(id: string): Promise<string> {
|
||||
const db = await TGSqlite.getDB();
|
||||
type resType = { nameCard: string };
|
||||
const res = await db.select<resType[]>("SELECT nameCard FROM AchievementSeries WHERE id = ?;", [
|
||||
id,
|
||||
]);
|
||||
return res[0].nameCard;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 查找成就数据
|
||||
* @since Beta v0.4.8
|
||||
* @param {string} keyword 关键词
|
||||
* @returns {Promise<TGApp.Sqlite.Achievement.SingleTable[]>} 成就数据
|
||||
* @since Beta v0.6.0
|
||||
* @param {number} uid - 存档 UID
|
||||
* @param {string} keyword - 关键词
|
||||
* @returns {Promise<TGApp.Sqlite.Achievement.RenderAchi[]>} 成就数据
|
||||
*/
|
||||
async function searchAchievements(
|
||||
async function searchAchi(
|
||||
uid: number,
|
||||
keyword: string,
|
||||
): Promise<TGApp.Sqlite.Achievement.SingleTable[]> {
|
||||
if (keyword === "") return await getAchievements();
|
||||
const db = await TGSqlite.getDB();
|
||||
): Promise<TGApp.Sqlite.Achievement.RenderAchi[]> {
|
||||
if (keyword === "") return await getAchievements(uid);
|
||||
const versionReg = /^v\d+(\.\d+)?$/;
|
||||
let rawData: TGApp.App.Achievement.Item[];
|
||||
const res: TGApp.Sqlite.Achievement.RenderAchi[] = [];
|
||||
if (versionReg.test(keyword)) {
|
||||
const version = keyword.replace("v", "");
|
||||
return await db.select<TGApp.Sqlite.Achievement.SingleTable[]>(
|
||||
"SELECT * FROM Achievements WHERE version LIKE ? ORDER BY isCompleted,`order`;",
|
||||
[`%${version}%`],
|
||||
);
|
||||
rawData = AppAchievementsData.filter((i) => i.version.includes(version));
|
||||
} else {
|
||||
rawData = AppAchievementsData.filter((a) => {
|
||||
if (a.name.includes(keyword)) return true;
|
||||
if (a.description.includes(keyword)) return true;
|
||||
});
|
||||
}
|
||||
return await db.select<TGApp.Sqlite.Achievement.SingleTable[]>(
|
||||
"SELECT * FROM Achievements WHERE name LIKE ? OR description LIKE ? ORDER BY isCompleted,`order`;",
|
||||
[`%${keyword}%`, `%${keyword}%`],
|
||||
for (const data of rawData) {
|
||||
const achiFind = await getAchi(uid, data.id);
|
||||
let achievement: TGApp.Sqlite.Achievement.RenderAchi;
|
||||
if (achiFind === false) achievement = getRenderAchi(data, uid);
|
||||
else achievement = getRenderAchi(data, uid, achiFind);
|
||||
res.push(achievement);
|
||||
}
|
||||
res.sort(
|
||||
(a, b) => a.isCompleted.toString().localeCompare(b.isCompleted.toString()) || a.order - b.order,
|
||||
);
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 更新成就数据
|
||||
* @since Beta v0.4.7
|
||||
* @param {TGApp.Sqlite.Achievement.SingleTable} data UIAF数据
|
||||
* @since Beta v0.6.0
|
||||
* @param {TGApp.Sqlite.Achievement.RenderAchi} data 成就数据
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function updateAchievement(data: TGApp.Sqlite.Achievement.SingleTable): Promise<void> {
|
||||
async function updateAchi(data: TGApp.Sqlite.Achievement.RenderAchi): Promise<void> {
|
||||
const db = await TGSqlite.getDB();
|
||||
await db.execute("UPDATE Achievements SET isCompleted = ?, completedTime = ? WHERE id = ?;", [
|
||||
data.isCompleted,
|
||||
data.completedTime.toString(),
|
||||
data.id,
|
||||
]);
|
||||
await db.execute(
|
||||
"INSERT INTO Achievements(id, uid, isCompleted, completedTime, progress, updated) \
|
||||
VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT(id,uid) DO UPDATE \
|
||||
SET isCompleted=?,completedTime=?,progress=?,updated=?;",
|
||||
[
|
||||
data.id,
|
||||
data.uid,
|
||||
data.isCompleted ? 1 : 0,
|
||||
data.completedTime,
|
||||
data.progress,
|
||||
timestampToDate(new Date().getTime()),
|
||||
data.isCompleted ? 1 : 0,
|
||||
data.completedTime,
|
||||
data.progress,
|
||||
timestampToDate(new Date().getTime()),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 将数据库数据转换为UIAF数据
|
||||
* @since Beta v0.5.5
|
||||
* @param {TGApp.Sqlite.Achievement.SingleTable} data 数据库数据
|
||||
* @since Beta v0.6.0
|
||||
* @param {TGApp.Sqlite.Achievement.TableAchi} data 数据库数据
|
||||
* @returns {TGApp.Plugins.UIAF.Achievement} UIAF数据
|
||||
*/
|
||||
function transDb2Uiaf(data: TGApp.Sqlite.Achievement.SingleTable): TGApp.Plugins.UIAF.Achievement {
|
||||
const isCompleted = data.isCompleted === 1;
|
||||
function transDb2Uiaf(data: TGApp.Sqlite.Achievement.TableAchi): TGApp.Plugins.UIAF.Achievement {
|
||||
let timestamp = 0;
|
||||
if (isCompleted) timestamp = Math.floor(new Date(data.completedTime).getTime() / 1000);
|
||||
const status = getUiafStatus(isCompleted, data.progress);
|
||||
if (data.isCompleted === 1) timestamp = Math.floor(new Date(data.completedTime).getTime() / 1000);
|
||||
const status = getUiafStatus(data.isCompleted === 1, data.progress);
|
||||
return {
|
||||
id: data.id,
|
||||
timestamp: timestamp,
|
||||
@@ -154,14 +244,16 @@ function transDb2Uiaf(data: TGApp.Sqlite.Achievement.SingleTable): TGApp.Plugins
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 获取UIAF数据
|
||||
* @since Beta v0.4.7
|
||||
* @description 获取指定Uid的UIAF数据
|
||||
* @since Beta v0.6.0
|
||||
* @param {number} uid - 存档UID
|
||||
* @returns {Promise<TGApp.Plugins.UIAF.Achievement[]>}
|
||||
*/
|
||||
async function getUIAF(): Promise<TGApp.Plugins.UIAF.Achievement[]> {
|
||||
async function getUiafData(uid: number): Promise<TGApp.Plugins.UIAF.Achievement[]> {
|
||||
const db = await TGSqlite.getDB();
|
||||
const data = await db.select<TGApp.Sqlite.Achievement.SingleTable[]>(
|
||||
"SELECT * FROM Achievements;",
|
||||
const data = await db.select<TGApp.Sqlite.Achievement.TableAchi[]>(
|
||||
"SELECT * FROM Achievements WHERE uid=?;",
|
||||
[uid],
|
||||
);
|
||||
const res: TGApp.Plugins.UIAF.Achievement[] = [];
|
||||
for (const item of data) {
|
||||
@@ -171,29 +263,111 @@ async function getUIAF(): Promise<TGApp.Plugins.UIAF.Achievement[]> {
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 合并UIAF数据
|
||||
* @since Beta v0.4.7
|
||||
* @param {TGApp.Plugins.UIAF.Achievement[]} data UIAF数据
|
||||
* @description 获取所有存档 uid
|
||||
* @since Beta v0.6.0
|
||||
* @return {Promise<Array<number>>}
|
||||
*/
|
||||
async function getAllUid(): Promise<Array<number>> {
|
||||
const db = await TGSqlite.getDB();
|
||||
type resType = Array<{ uid: number }>;
|
||||
const res = await db.select<resType>("SELECT DISTINCT uid FROM Achievements;");
|
||||
return res.map((i) => i.uid);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 备份成就数据
|
||||
* @since Beta v0.6.0
|
||||
* @param {string} dir - 存档数据
|
||||
* @param {number} uid - 存档UID,未指定则导出所有
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function mergeUIAF(data: TGApp.Plugins.UIAF.Achievement[]): Promise<void> {
|
||||
const db = await TGSqlite.getDB();
|
||||
for (const item of data) {
|
||||
const sql = importUIAFData(item);
|
||||
await db.execute(sql);
|
||||
async function backupUiaf(dir: string, uid?: number): Promise<void> {
|
||||
let uidList = [];
|
||||
if (uid === undefined) uidList = await getAllUid();
|
||||
else uidList.push(uid);
|
||||
if (!(await exists(dir))) {
|
||||
await TGLogger.Warn("不存在指定的成就备份目录,即将创建");
|
||||
await mkdir(dir, { recursive: true });
|
||||
}
|
||||
for (const uidItem of uidList) {
|
||||
const data = await getUiafData(uidItem);
|
||||
const fileName = `UIAF_${uidItem}`;
|
||||
await writeTextFile(`${dir}${path.sep()}${fileName}.json`, JSON.stringify(data, null, 2));
|
||||
await TGLogger.Info(`成功备份${uidItem}的成就存档`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 恢复成就数据
|
||||
* @since Beta v0.6.0
|
||||
* @param {string} dir - 数据目录
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async function restoreUiaf(dir: string): Promise<boolean> {
|
||||
if (!(await exists(dir))) return false;
|
||||
const filesRead = await readDir(dir);
|
||||
const files = filesRead.filter((item) => item.name.includes("UIAF_") && item.isFile);
|
||||
// 正则匹配 UIAF_xx.json
|
||||
for (const file of files) {
|
||||
try {
|
||||
const reg = new RegExp(/(.*)UIAF_d{9}.json/);
|
||||
if (!file.name.match(reg)) return false;
|
||||
const uid: number = Number(file.name.match(reg)![0]);
|
||||
const data: TGApp.Plugins.UIAF.Achievement[] = JSON.parse(await readTextFile(file.name));
|
||||
await TSUserAchi.mergeUiaf(data, uid);
|
||||
} catch (e) {
|
||||
await TGLogger.Error(`[UIAF][RESTORE] 恢复成就数据${file.name} `);
|
||||
await TGLogger.Error(`${e}`);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 导入Uiaf数据
|
||||
* @since Beta v0.6.0
|
||||
* @param {TGApp.Plugins.UIAF.Achievement[]} data - 成就数据
|
||||
* @param {number} uid - 存档UID
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function mergeUiaf(data: TGApp.Plugins.UIAF.Achievement[], uid: number): Promise<void> {
|
||||
const db = await TGSqlite.getDB();
|
||||
for (const achi of data) {
|
||||
const status = achi.status === 2 || achi.status === 3 ? 1 : 0;
|
||||
const timeC = status === 1 ? timestampToDate(achi.timestamp * 1000) : "";
|
||||
const timeN = timestampToDate(new Date().getTime());
|
||||
await db.execute(
|
||||
"INSERT INTO Achievements(id, uid, isCompleted, completedTime, progress, updated) \
|
||||
VALUES (?,?,?,?,?,?) ON CONFLICT(id,uid) DO UPDATE SET\
|
||||
isCompleted=?,completedTime=?,progress=?,updated=?;",
|
||||
[achi.id, uid, status, timeC, achi.current, timeN, status, timeC, achi.current, timeN],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 删除指定 UID 存档的数据
|
||||
* @since Beta v0.6.0
|
||||
* @param {number} uid - 存档UID
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function delUid(uid: number): Promise<void> {
|
||||
const db = await TGSqlite.getDB();
|
||||
await db.execute("DELETE FROM Achievements WHERE uid=?;", [uid]);
|
||||
}
|
||||
|
||||
const TSUserAchi = {
|
||||
getOverview,
|
||||
getLatestAchiVersion,
|
||||
getSeries,
|
||||
getSeriesNameCard,
|
||||
getOverview,
|
||||
getAchievements,
|
||||
searchAchievements,
|
||||
updateAchievement,
|
||||
getUIAF,
|
||||
mergeUIAF,
|
||||
getAllUid,
|
||||
getUiafData,
|
||||
searchAchi,
|
||||
updateAchi,
|
||||
mergeUiaf,
|
||||
backupUiaf,
|
||||
restoreUiaf,
|
||||
delUid,
|
||||
};
|
||||
|
||||
export default TSUserAchi;
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
/**
|
||||
* @file plugins/Sqlite/modules/userGacha.ts
|
||||
* @description 用户祈愿模块
|
||||
* @since Beta v0.5.1
|
||||
* @since Beta v0.6.0
|
||||
*/
|
||||
|
||||
import { AppCharacterData, AppWeaponData } from "../../../data/index.js";
|
||||
import TGSqlite from "../index.js";
|
||||
import { importUIGFData } from "../sql/updateData.js";
|
||||
|
||||
type gachaItemTypeRes =
|
||||
| ["角色", TGApp.App.Character.WikiBriefInfo]
|
||||
@@ -27,6 +26,34 @@ function getGachaItemType(item_id: string): gachaItemTypeRes {
|
||||
return ["未知", "未知"];
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 获取导入 Sql
|
||||
* @since Beta v.6.0
|
||||
* @param {string} uid - UID
|
||||
* @param {TGApp.Plugins.UIGF.GachaItem} gacha - UIGF数据
|
||||
* @returns {string}
|
||||
*/
|
||||
function getInsertSql(uid: string, gacha: TGApp.Plugins.UIGF.GachaItem): string {
|
||||
return `
|
||||
INSERT INTO GachaRecords (uid, gachaType, itemId, count, time, name, type, rank, id, uigfType, updated)
|
||||
VALUES ('${uid}', '${gacha.gacha_type}', '${gacha.item_id ?? null}', '${gacha.count ?? null}', '${gacha.time}',
|
||||
'${gacha.name}', '${gacha.item_type ?? null}', '${gacha.rank_type ?? null}', '${gacha.id}',
|
||||
'${gacha.uigf_gacha_type}', datetime('now', 'localtime'))
|
||||
ON CONFLICT (id)
|
||||
DO UPDATE
|
||||
SET uid = '${uid}',
|
||||
gachaType = '${gacha.gacha_type}',
|
||||
uigfType = '${gacha.uigf_gacha_type}',
|
||||
time = '${gacha.time}',
|
||||
itemId = '${gacha.item_id ?? null}',
|
||||
count = '${gacha.count ?? null}',
|
||||
name = '${gacha.name}',
|
||||
type = '${gacha.item_type ?? null}',
|
||||
rank = '${gacha.rank_type ?? null}',
|
||||
updated = datetime('now', 'localtime');
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 转换祈愿数据,防止多语言
|
||||
* @since Beta v0.5.1
|
||||
@@ -130,7 +157,7 @@ async function mergeUIGF(uid: string, data: TGApp.Plugins.UIGF.GachaItem[]): Pro
|
||||
const db = await TGSqlite.getDB();
|
||||
for (const gacha of data) {
|
||||
const trans = transGacha(gacha);
|
||||
const sql = importUIGFData(uid, trans);
|
||||
const sql = getInsertSql(uid, trans);
|
||||
await db.execute(sql);
|
||||
}
|
||||
}
|
||||
@@ -145,7 +172,7 @@ async function mergeUIGF4(data: TGApp.Plugins.UIGF.GachaHk4e): Promise<void> {
|
||||
const db = await TGSqlite.getDB();
|
||||
for (const gacha of data.list) {
|
||||
const trans = transGacha(gacha);
|
||||
const sql = importUIGFData(data.uid.toString(), trans);
|
||||
const sql = getInsertSql(data.uid.toString(), trans);
|
||||
await db.execute(sql);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,35 +1,23 @@
|
||||
-- @file plugins/Sqlite/sql/createTable.sql
|
||||
-- @brief sqlite数据库创建表语句
|
||||
-- @since Beta v0.5.5
|
||||
-- @since Beta v0.6.0
|
||||
|
||||
-- @brief 重新创建成就数据表
|
||||
drop table if exists Achievements;
|
||||
-- @brief 创建成就数据表
|
||||
create table if not exists Achievements
|
||||
(
|
||||
id integer primary key,
|
||||
series integer,
|
||||
`order` integer,
|
||||
name text,
|
||||
description text,
|
||||
reward integer,
|
||||
id integer not null,
|
||||
uid integer not null,
|
||||
isCompleted boolean default false,
|
||||
completedTime text,
|
||||
progress integer default 0,
|
||||
version text,
|
||||
updated text
|
||||
updated text,
|
||||
primary key (id, uid)
|
||||
);
|
||||
|
||||
-- @brief 创建成就系列数据表
|
||||
create table if not exists AchievementSeries
|
||||
(
|
||||
id integer primary key,
|
||||
`order` integer,
|
||||
name text,
|
||||
version text,
|
||||
totalCount integer default 0,
|
||||
finCount integer default 0,
|
||||
nameCard text,
|
||||
updated text
|
||||
);
|
||||
-- @brief 重新创建成就系列数据表
|
||||
drop table if exists AchievementSeries;
|
||||
|
||||
-- @brief 创建角色数据表
|
||||
create table if not exists AppCharacters
|
||||
@@ -117,19 +105,19 @@ create table if not exists UserRecord
|
||||
-- @brief 创建角色数据表
|
||||
create table if not exists UserCharacters
|
||||
(
|
||||
uid integer,
|
||||
cid integer,
|
||||
avatar text,
|
||||
weapon text,
|
||||
relics text,
|
||||
constellations text,
|
||||
costumes text,
|
||||
skills text,
|
||||
propSelected text,
|
||||
propBase text,
|
||||
propExtra text,
|
||||
propRecommend text,
|
||||
updated text,
|
||||
uid integer,
|
||||
cid integer,
|
||||
avatar text,
|
||||
weapon text,
|
||||
relics text,
|
||||
constellations text,
|
||||
costumes text,
|
||||
skills text,
|
||||
propSelected text,
|
||||
propBase text,
|
||||
propExtra text,
|
||||
propRecommend text,
|
||||
updated text,
|
||||
primary key (uid, cid)
|
||||
);
|
||||
|
||||
|
||||
@@ -1,40 +1,9 @@
|
||||
-- @file plugins Sqlite sql createTrigger.sql
|
||||
-- @file plugins/Sqlite/sql/createTrigger.sql
|
||||
-- @brief 创建触发器
|
||||
-- @author BTMuli <bt-muli@outlook.com>
|
||||
-- @since Alpha v0.2.0
|
||||
-- @since Beta v0.6.0
|
||||
|
||||
-- @brief 成就表相关
|
||||
create trigger if not exists insertAchievement
|
||||
after insert
|
||||
on Achievements
|
||||
for each row
|
||||
begin
|
||||
update AchievementSeries
|
||||
set totalCount = totalCount + 1,
|
||||
updated = datetime('now', 'localtime')
|
||||
where id = new.series;
|
||||
update AchievementSeries
|
||||
set version = new.version,
|
||||
updated = datetime('now', 'localtime')
|
||||
where id = new.series
|
||||
and new.version > version;
|
||||
end;
|
||||
-- @brief 重新创建成就表插入触发器
|
||||
drop trigger if exists insertAchievement;
|
||||
|
||||
create trigger if not exists updateAchievement
|
||||
after update
|
||||
on Achievements
|
||||
for each row
|
||||
begin
|
||||
update AchievementSeries
|
||||
set updated = datetime('now', 'localtime'),
|
||||
finCount = finCount + 1
|
||||
where id = new.series
|
||||
and old.isCompleted = 0
|
||||
and new.isCompleted = 1;
|
||||
update AchievementSeries
|
||||
set updated = datetime('now', 'localtime'),
|
||||
finCount = finCount - 1
|
||||
where id = new.series
|
||||
and old.isCompleted = 1
|
||||
and new.isCompleted = 0;
|
||||
end;
|
||||
-- @brief 重新创建成就表更新触发器
|
||||
drop trigger if exists updateAchievement;
|
||||
|
||||
@@ -1,26 +1,16 @@
|
||||
/**
|
||||
* @file plugins/Sqlite/sql/initData.ts
|
||||
* @description Sqlite 初始化数据 sql 语句
|
||||
* @since Beta v0.4.5
|
||||
* @since Beta v0.6.0
|
||||
*/
|
||||
|
||||
import { app } from "@tauri-apps/api";
|
||||
|
||||
import {
|
||||
AppAchievementsData,
|
||||
AppAchievementSeriesData,
|
||||
AppNameCardsData,
|
||||
AppCharacterData,
|
||||
} from "../../../data/index.js";
|
||||
import { AppNameCardsData, AppCharacterData } from "../../../data/index.js";
|
||||
import { getBuildTime } from "../../../utils/TGBuild.js";
|
||||
|
||||
import initTableSql from "./initTable.js";
|
||||
import {
|
||||
insertAchievementData,
|
||||
insertAchievementSeriesData,
|
||||
insertNameCardData,
|
||||
insertCharacterData,
|
||||
} from "./insertData.js";
|
||||
import { insertNameCardData, insertCharacterData } from "./insertData.js";
|
||||
|
||||
/**
|
||||
* @description 初始化应用表数据
|
||||
@@ -51,24 +41,6 @@ async function initAppData(): Promise<string[]> {
|
||||
return sqlRes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 初始化成就系列数据
|
||||
* @since Alpha v0.2.0
|
||||
* @returns {string[]} sql
|
||||
*/
|
||||
function initAchievementSeriesData(): string[] {
|
||||
return AppAchievementSeriesData.map((item) => insertAchievementSeriesData(item));
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 初始化成就数据
|
||||
* @since Alpha v0.2.0
|
||||
* @returns {string[]} sql
|
||||
*/
|
||||
function initAchievementData(): string[] {
|
||||
return AppAchievementsData.map((item) => insertAchievementData(item));
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 初始化应用名片数据
|
||||
* @since Alpha v0.2.0
|
||||
@@ -96,8 +68,6 @@ async function initDataSql(): Promise<string[]> {
|
||||
const sqlRes: string[] = [];
|
||||
sqlRes.push(...initTableSql());
|
||||
sqlRes.push(...(await initAppData()));
|
||||
sqlRes.push(...initAchievementSeriesData());
|
||||
sqlRes.push(...initAchievementData());
|
||||
sqlRes.push(...initNameCardData());
|
||||
sqlRes.push(...initCharacterData());
|
||||
return sqlRes;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* @file plugins/Sqlite/sql/insertData.ts
|
||||
* @description Sqlite 插入数据 sql 语句
|
||||
* @since Beta v0.5.3
|
||||
* @since Beta v0.6.0
|
||||
*/
|
||||
|
||||
import { transCharacterData, transFloorData } from "../utils/transAbyssData.js";
|
||||
@@ -9,48 +9,6 @@ import { timeToSecond } from "../utils/transTime.js";
|
||||
import { transUserRecord } from "../utils/transUserRecord.js";
|
||||
import { transUserRoles } from "../utils/transUserRoles.js";
|
||||
|
||||
/**
|
||||
* @description 插入成就数据
|
||||
* @since Alpha v0.2.0
|
||||
* @param {TGApp.App.Achievement.Item} data 成就数据
|
||||
* @returns {string} sql
|
||||
*/
|
||||
export function insertAchievementData(data: TGApp.App.Achievement.Item): string {
|
||||
return `
|
||||
INSERT INTO Achievements (id, series, "order", name, description, reward, completedTime, version, updated)
|
||||
VALUES (${data.id}, ${data.series}, ${data.order}, '${data.name}', '${data.description}', ${data.reward}, '',
|
||||
'${data.version}', datetime('now', 'localtime'))
|
||||
ON CONFLICT(id) DO UPDATE
|
||||
SET series = ${data.series},
|
||||
"order" = ${data.order},
|
||||
name = '${data.name}',
|
||||
description = '${data.description}',
|
||||
reward = '${data.reward}',
|
||||
version = '${data.version}',
|
||||
updated = datetime('now', 'localtime');
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 插入成就系列数据
|
||||
* @since Alpha v0.2.0
|
||||
* @param {TGApp.App.Achievement.Series} data 成就系列数据
|
||||
* @returns {string} sql
|
||||
*/
|
||||
export function insertAchievementSeriesData(data: TGApp.App.Achievement.Series): string {
|
||||
return `
|
||||
INSERT INTO AchievementSeries (id, "order", name, version, nameCard, updated)
|
||||
VALUES (${data.id}, ${data.order}, '${data.name}', '${data.version}', '${data.card}',
|
||||
datetime('now', 'localtime'))
|
||||
ON CONFLICT(id) DO UPDATE
|
||||
SET name = '${data.name}',
|
||||
"order" = ${data.order},
|
||||
version = '${data.version}',
|
||||
nameCard = '${data.card}',
|
||||
updated = datetime('now', 'localtime');
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 插入应用数据
|
||||
* @since Alpha v0.2.0
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
/**
|
||||
* @file plugins/Sqlite/sql/updateData.ts
|
||||
* @description 更新数据
|
||||
* @since Beta v0.4.9
|
||||
*/
|
||||
|
||||
import minifySql from "../../../utils/minifySql.js";
|
||||
|
||||
/**
|
||||
* @description 导入UIAF数据-单项
|
||||
* @since Beta v0.4.9
|
||||
* @param {TGApp.Plugins.UIAF.Achievement} data
|
||||
* @returns {string} sql
|
||||
*/
|
||||
export function importUIAFData(data: TGApp.Plugins.UIAF.Achievement): string {
|
||||
let sql;
|
||||
const isCompleted = data.status === 2 || data.status === 3;
|
||||
if (isCompleted) {
|
||||
const completedTime = new Date(data.timestamp * 1000)
|
||||
.toISOString()
|
||||
.replace("T", " ")
|
||||
.slice(0, 19);
|
||||
sql = `
|
||||
UPDATE Achievements
|
||||
SET isCompleted = 1,
|
||||
completedTime = '${completedTime}',
|
||||
progress = ${data.current},
|
||||
updated = datetime('now', 'localtime')
|
||||
WHERE id = ${data.id}
|
||||
AND (isCompleted = 0 OR completedTime != '${completedTime}'
|
||||
OR progress != ${data.current});
|
||||
`;
|
||||
} else {
|
||||
sql = `
|
||||
UPDATE Achievements
|
||||
SET progress = ${data.current},
|
||||
updated = datetime('now', 'localtime')
|
||||
WHERE id = ${data.id}
|
||||
AND progress != ${data.current};
|
||||
`;
|
||||
}
|
||||
return minifySql(sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 导入UIGF数据-单项
|
||||
* @since Beta v0.4.7
|
||||
* @param {string} uid - UID
|
||||
* @param {TGApp.Plugins.UIGF.GachaItem} gacha - UIGF数据
|
||||
* @returns {string} sql
|
||||
*/
|
||||
export function importUIGFData(uid: string, gacha: TGApp.Plugins.UIGF.GachaItem): string {
|
||||
const sql = `
|
||||
INSERT INTO GachaRecords (uid, gachaType, itemId, count, time, name, type, rank, id, uigfType, updated)
|
||||
VALUES ('${uid}', '${gacha.gacha_type}', '${gacha.item_id ?? null}', '${gacha.count ?? null}', '${gacha.time}',
|
||||
'${gacha.name}', '${gacha.item_type ?? null}', '${gacha.rank_type ?? null}', '${gacha.id}',
|
||||
'${gacha.uigf_gacha_type}', datetime('now', 'localtime'))
|
||||
ON CONFLICT (id)
|
||||
DO UPDATE
|
||||
SET uid = '${uid}',
|
||||
gachaType = '${gacha.gacha_type}',
|
||||
uigfType = '${gacha.uigf_gacha_type}',
|
||||
time = '${gacha.time}',
|
||||
itemId = '${gacha.item_id ?? null}',
|
||||
count = '${gacha.count ?? null}',
|
||||
name = '${gacha.name}',
|
||||
type = '${gacha.item_type ?? null}',
|
||||
rank = '${gacha.rank_type ?? null}',
|
||||
updated = datetime('now', 'localtime');
|
||||
`;
|
||||
return minifySql(sql);
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
/**
|
||||
* @file store modules achievements.ts
|
||||
* @description Achievements store module
|
||||
* @since Alpha v0.1.3
|
||||
*/
|
||||
|
||||
import { defineStore } from "pinia";
|
||||
import { ref } from "vue";
|
||||
|
||||
export const useAchievementsStore = defineStore(
|
||||
"achievements",
|
||||
() => {
|
||||
// 成就数据
|
||||
const totalAchievements = ref(950);
|
||||
const finAchievements = ref(0);
|
||||
const lastVersion = ref("v3.6");
|
||||
const UIAFVersion = ref("v1.1");
|
||||
const title = ref("成就完成数:0/950 完成率:0%");
|
||||
|
||||
function init(): void {
|
||||
totalAchievements.value = 950;
|
||||
finAchievements.value = 0;
|
||||
lastVersion.value = "v3.6";
|
||||
title.value = getTitle();
|
||||
}
|
||||
|
||||
function getTitle(): string {
|
||||
return `成就完成数:${finAchievements.value}/${totalAchievements.value} 完成率:${(
|
||||
(finAchievements.value / totalAchievements.value) *
|
||||
100
|
||||
).toFixed(2)}%`;
|
||||
}
|
||||
|
||||
function flushData(total: number, fin: number): void {
|
||||
totalAchievements.value = total;
|
||||
finAchievements.value = fin;
|
||||
title.value = getTitle();
|
||||
}
|
||||
|
||||
return {
|
||||
totalAchievements,
|
||||
finAchievements,
|
||||
lastVersion,
|
||||
UIAFVersion,
|
||||
title,
|
||||
init,
|
||||
getTitle,
|
||||
flushData,
|
||||
};
|
||||
},
|
||||
{
|
||||
persist: true,
|
||||
},
|
||||
);
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* @file store/modules/app.ts
|
||||
* @description App store module
|
||||
* @since Beta v0.5.5
|
||||
* @since Beta v0.6.0
|
||||
*/
|
||||
|
||||
import { path } from "@tauri-apps/api";
|
||||
@@ -21,8 +21,6 @@ const logDataDir = await path.appLogDir();
|
||||
export const useAppStore = defineStore(
|
||||
"app",
|
||||
() => {
|
||||
// 应用加载状态
|
||||
const loading = ref(false);
|
||||
// 应用打包时间
|
||||
const buildTime = ref("");
|
||||
// 侧边栏设置
|
||||
@@ -55,7 +53,6 @@ export const useAppStore = defineStore(
|
||||
|
||||
// 初始化
|
||||
function init(): void {
|
||||
loading.value = false;
|
||||
devMode.value = false;
|
||||
theme.value = "default";
|
||||
isLogin.value = false;
|
||||
@@ -78,7 +75,6 @@ export const useAppStore = defineStore(
|
||||
|
||||
return {
|
||||
theme,
|
||||
loading,
|
||||
buildTime,
|
||||
sidebar,
|
||||
devMode,
|
||||
|
||||
24
src/types/Plugins/UIAF.d.ts
vendored
24
src/types/Plugins/UIAF.d.ts
vendored
@@ -1,10 +1,15 @@
|
||||
/**
|
||||
* @file types Plugins UIAF.d.ts
|
||||
* @file types/Plugins/UIAF.d.ts
|
||||
* @description UIAF 插件类型定义文件
|
||||
* @author BTMuli<bt-muli@outlook.com>
|
||||
* @since Alpha v0.1.5
|
||||
* @since Beta v0.6.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* @description UIAF 插件类型命名空间
|
||||
* @namespace TGApp.Plugins.UIAF
|
||||
* @merberof TGApp.Plugins
|
||||
* @since Beta v0.6.0
|
||||
*/
|
||||
declare namespace TGApp.Plugins.UIAF {
|
||||
/**
|
||||
* @interface Data
|
||||
@@ -52,4 +57,17 @@ declare namespace TGApp.Plugins.UIAF {
|
||||
current: number;
|
||||
status: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* @interface Backup
|
||||
* @description 数据备份时的格式,用于标识不同存档
|
||||
* @since Beta v0.6.0
|
||||
* @property {number} uid - 存档UID
|
||||
* @property {Achievement[]} data - 存档数据
|
||||
* @returns Backup
|
||||
*/
|
||||
interface Backup {
|
||||
uid: number;
|
||||
data: Achievement[];
|
||||
}
|
||||
}
|
||||
|
||||
75
src/types/Sqlite/Achievement.d.ts
vendored
75
src/types/Sqlite/Achievement.d.ts
vendored
@@ -1,72 +1,71 @@
|
||||
/**
|
||||
* @file types/Sqlite/Achievement.d.ts
|
||||
* @description 数据库成就相关类型定义文件
|
||||
* @since Beta v0.4.7
|
||||
* @since Beta v0.6.0
|
||||
*/
|
||||
|
||||
declare namespace TGApp.Sqlite.Achievement {
|
||||
/**
|
||||
* @description 数据库-成就表
|
||||
* @since Alpha v0.1.5
|
||||
* @interface SingleTable
|
||||
* @since Beta v0.6.0
|
||||
* @interface TableAchi
|
||||
* @property {number} id - 成就 ID
|
||||
* @property {number} series - 成就系列 ID
|
||||
* @property {number} order - 成就排列顺序,用于展示全部成就
|
||||
* @property {string} name - 成就名称
|
||||
* @property {string} description - 成就描述
|
||||
* @property {number} reward - 成就奖励
|
||||
* @property {number} isCompleted - 成就是否完成
|
||||
* @property {number} uid - 存档 UID
|
||||
* @property {number} isCompleted - 成就是否完成 // 0:未完成,1:完成
|
||||
* @property {string} completedTime - 成就完成时间
|
||||
* @property {number} progress - 成就进度
|
||||
* @property {string} version - 成就版本
|
||||
* @property {string} updated - 数据库更新时间
|
||||
* @return SingleTable
|
||||
* @return TableAchi
|
||||
*/
|
||||
interface SingleTable {
|
||||
interface TableAchi {
|
||||
id: number;
|
||||
series: number;
|
||||
order: number;
|
||||
name: string;
|
||||
description: string;
|
||||
reward: number;
|
||||
uid: number;
|
||||
isCompleted: 0 | 1;
|
||||
completedTime: string;
|
||||
progress: number;
|
||||
version: string;
|
||||
updated: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 数据库-成就系列表
|
||||
* @since Alpha v0.2.0
|
||||
* @interface SeriesTable
|
||||
* @property {number} id - 成就系列 ID
|
||||
* @property {number} order - 成就系列排列顺序,用于展示全部成就系列
|
||||
* @property {string} name - 成就系列名称
|
||||
* @property {string} version - 成就系列版本
|
||||
* @property {number} totalCount - 成就系列包含的成就数
|
||||
* @property {number} finCount - 成就系列已完成的成就数
|
||||
* @property {string} nameCard - 成就系列对应名片
|
||||
* @property {string} updated - 数据库更新时间
|
||||
* @returns SeriesTable
|
||||
* @description 渲染用的成就数据
|
||||
* @since Beta v0.6.0
|
||||
* @interface RenderAchi
|
||||
* @property {number} id - 成就 ID
|
||||
* @property {number} uid - 存档 UID
|
||||
* @property {number} series - 成就对应系列 ID
|
||||
* @property {string} name - 成就名称
|
||||
* @property {string} version - 成就版本
|
||||
* @property {string} description - 成就描述
|
||||
* @property {number} reward - 成就奖励
|
||||
* @property {TGApp.App.Achievement.Trigger} trigger - 成就触发器
|
||||
* @property {boolean} isCompleted - 是否完成
|
||||
* @property {string} completedTime - 完成时间
|
||||
* @property {number} progress - 完成进度
|
||||
* @property {string} updated - 更新时间
|
||||
* @return RenderAchi
|
||||
*/
|
||||
interface SeriesTable {
|
||||
interface RenderAchi {
|
||||
id: number;
|
||||
uid: number;
|
||||
order: number;
|
||||
series: number;
|
||||
name: string;
|
||||
description: string;
|
||||
reward: number;
|
||||
version: string;
|
||||
totalCount: number;
|
||||
finCount: number;
|
||||
nameCard: string;
|
||||
trigger: TGApp.App.Achievement.Trigger;
|
||||
isCompleted: boolean;
|
||||
completedTime: string;
|
||||
progress: number;
|
||||
updated: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 成就概况
|
||||
* @since Beta v0.4.7
|
||||
* @description 概况
|
||||
* @since Beta v0.6.0
|
||||
* @interface Overview
|
||||
* @property {number} total - 总成就数
|
||||
* @property {number} fin - 已完成成就数
|
||||
* @property {number} total - 全部
|
||||
* @property {number} fin - 已完成
|
||||
* @returns Overview
|
||||
*/
|
||||
interface Overview {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* @file utils/UIAF.ts
|
||||
* @description UIAF工具类
|
||||
* @since Beta v0.5.0
|
||||
* @since Beta v0.6.0
|
||||
*/
|
||||
|
||||
import { app } from "@tauri-apps/api";
|
||||
@@ -14,25 +14,6 @@ import { UiafSchema } from "../data/index.js";
|
||||
|
||||
import TGLogger from "./TGLogger.js";
|
||||
|
||||
/**
|
||||
* @description 根据 completed 跟 progress 获取 status
|
||||
* @since Alpha v0.1.4
|
||||
* @param {boolean} completed - 是否完成
|
||||
* @param {number} progress - 进度
|
||||
* @returns {number} status
|
||||
*/
|
||||
export function getUiafStatus(completed: boolean, progress: number): number {
|
||||
if (progress !== 0 && !completed) {
|
||||
return 1;
|
||||
} else if (progress === 0 && completed) {
|
||||
return 2;
|
||||
} else if (progress !== 0 && completed) {
|
||||
return 3;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 获取 UIAF 头部信息
|
||||
* @since Beta v0.3.4
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* @file utils/dataBS.ts
|
||||
* @description 用户数据的备份、恢复、迁移
|
||||
* @since Beta v0.5.5
|
||||
* @since Beta v0.6.0
|
||||
*/
|
||||
|
||||
import { path } from "@tauri-apps/api";
|
||||
@@ -17,7 +17,7 @@ import { exportUigfData, readUigfData, verifyUigfData } from "./UIGF.js";
|
||||
|
||||
/**
|
||||
* @description 备份用户数据
|
||||
* @since Beta v0.5.0
|
||||
* @since Beta v0.6.0
|
||||
* @param {string} dir 备份目录路径
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
@@ -26,8 +26,8 @@ export async function backUpUserData(dir: string): Promise<void> {
|
||||
console.log("备份目录不存在,创建备份目录");
|
||||
await mkdir(dir, { recursive: true });
|
||||
}
|
||||
const dataAchi = await TSUserAchi.getUIAF();
|
||||
await writeTextFile(`${dir}${path.sep()}UIAF.json`, JSON.stringify(dataAchi));
|
||||
// 备份成就
|
||||
await TSUserAchi.backupUiaf(dir);
|
||||
// 备份 ck
|
||||
const dataCK = await TGSqlite.getCookie();
|
||||
await writeTextFile(`${dir}${path.sep()}cookie.json`, JSON.stringify(dataCK));
|
||||
@@ -45,46 +45,23 @@ export async function backUpUserData(dir: string): Promise<void> {
|
||||
|
||||
/**
|
||||
* @description 恢复用户数据
|
||||
* @since Beta v0.5.5
|
||||
* @since Beta v0.6.0
|
||||
* @param {string} dir 备份目录路径
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function restoreUserData(dir: string): Promise<void> {
|
||||
let errNum = 0;
|
||||
if (!(await exists(dir))) {
|
||||
showSnackbar({
|
||||
text: "备份目录不存在",
|
||||
color: "error",
|
||||
});
|
||||
showSnackbar({ text: "备份目录不存在", color: "error" });
|
||||
return;
|
||||
}
|
||||
const filesRead = await readDir(dir);
|
||||
const files = filesRead.filter((item) => item.isFile && item.name.endsWith(".json"));
|
||||
await TGLogger.Info(`[DataBS][restoreUserData] files: ${JSON.stringify(files)}`);
|
||||
// 恢复成就数据
|
||||
const achiFind = files.find((item) => item.name === "UIAF.json");
|
||||
if (achiFind) {
|
||||
try {
|
||||
const dataAchi: TGApp.Plugins.UIAF.Achievement[] = JSON.parse(
|
||||
await readTextFile(achiFind.name),
|
||||
);
|
||||
await TSUserAchi.mergeUIAF(dataAchi);
|
||||
await TGLogger.Info(`[DataBS][restoreUserData] 成就数据恢复成功`);
|
||||
} catch (e) {
|
||||
showSnackbar({
|
||||
text: `成就数据恢复失败 ${e}`,
|
||||
color: "error",
|
||||
});
|
||||
await TGLogger.Error(`[DataBS][restoreUserData] 成就数据恢复失败 ${e}`);
|
||||
errNum++;
|
||||
}
|
||||
} else {
|
||||
showSnackbar({
|
||||
text: "成就数据恢复失败,备份文件不存在",
|
||||
color: "warn",
|
||||
});
|
||||
await TGLogger.Warn(`[DataBS][restoreUserData] 未检测到成就数据备份文件`);
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500));
|
||||
const restoreAchi = await TSUserAchi.restoreUiaf(dir);
|
||||
if (!restoreAchi) {
|
||||
showSnackbar({ text: `成就数据恢复失败`, color: "error" });
|
||||
errNum++;
|
||||
}
|
||||
// 恢复 ck
|
||||
const ckFind = files.find((item) => item.name === "cookie.json");
|
||||
@@ -94,18 +71,12 @@ export async function restoreUserData(dir: string): Promise<void> {
|
||||
await TGSqlite.saveAppData("cookie", JSON.stringify(JSON.parse(dataCK)));
|
||||
await TGLogger.Info(`[DataBS][restoreUserData] Cookie 数据恢复成功`);
|
||||
} catch (e) {
|
||||
showSnackbar({
|
||||
text: `Cookie 数据恢复失败 ${e}`,
|
||||
color: "error",
|
||||
});
|
||||
showSnackbar({ text: `Cookie 数据恢复失败 ${e}`, color: "error" });
|
||||
await TGLogger.Error(`[DataBS][restoreUserData] Cookie 数据恢复失败 ${e}`);
|
||||
errNum++;
|
||||
}
|
||||
} else {
|
||||
showSnackbar({
|
||||
text: "Cookie 数据恢复失败,备份文件不存在",
|
||||
color: "warn",
|
||||
});
|
||||
showSnackbar({ text: "Cookie 数据恢复失败,备份文件不存在", color: "warn" });
|
||||
await TGLogger.Warn(`[DataBS][restoreUserData] 未检测到 Cookie 数据备份文件`);
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500));
|
||||
}
|
||||
@@ -119,17 +90,11 @@ export async function restoreUserData(dir: string): Promise<void> {
|
||||
await TGSqlite.restoreAbyss(dataAbyss);
|
||||
} catch (e) {
|
||||
await TGLogger.Error(`[DataBS][restoreUserData] 深渊数据恢复失败 ${e}`);
|
||||
showSnackbar({
|
||||
text: "深渊数据恢复失败",
|
||||
color: "error",
|
||||
});
|
||||
showSnackbar({ text: "深渊数据恢复失败", color: "error" });
|
||||
errNum++;
|
||||
}
|
||||
} else {
|
||||
showSnackbar({
|
||||
text: "深渊数据恢复失败,备份文件不存在",
|
||||
color: "warn",
|
||||
});
|
||||
showSnackbar({ text: "深渊数据恢复失败,备份文件不存在", color: "warn" });
|
||||
await TGLogger.Warn(`[DataBS][restoreUserData] 未检测到深渊数据备份文件`);
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500));
|
||||
}
|
||||
@@ -148,23 +113,14 @@ export async function restoreUserData(dir: string): Promise<void> {
|
||||
await TSUserGacha.mergeUIGF(uid, data.list);
|
||||
await TGLogger.Info(`[DataBS][restoreUserData] UID: ${uid} 祈愿数据恢复成功`);
|
||||
} catch (e) {
|
||||
showSnackbar({
|
||||
text: `UID: ${uid} 祈愿数据恢复失败`,
|
||||
color: "error",
|
||||
});
|
||||
showSnackbar({ text: `UID: ${uid} 祈愿数据恢复失败`, color: "error" });
|
||||
await TGLogger.Error(`[DataBS][restoreUserData] UID: ${uid} 祈愿数据恢复失败 ${e}`);
|
||||
errNum++;
|
||||
}
|
||||
}
|
||||
if (errNum === 0) {
|
||||
showSnackbar({
|
||||
text: "数据恢复成功",
|
||||
color: "success",
|
||||
});
|
||||
showSnackbar({ text: "数据恢复成功", color: "success" });
|
||||
} else {
|
||||
showSnackbar({
|
||||
text: `数据恢复失败,失败数量:${errNum}`,
|
||||
color: "error",
|
||||
});
|
||||
showSnackbar({ text: `数据恢复失败,失败数量:${errNum}`, color: "error" });
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user