♻️ 重构成就表格,支持多存档

#126
This commit is contained in:
目棃
2024-09-20 15:57:02 +08:00
parent a8a667871a
commit 1dc5aa0ef8
28 changed files with 1198 additions and 1239 deletions

View File

@@ -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();

View File

@@ -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>("");

View File

@@ -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

View File

@@ -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);

View File

@@ -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;

View File

@@ -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;

View 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>

View File

@@ -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>

View 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>

View 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>

View File

@@ -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>

View File

@@ -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(() => {

View File

@@ -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>

View File

@@ -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

View File

@@ -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;

View File

@@ -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);
}
}

View File

@@ -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)
);

View File

@@ -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;

View File

@@ -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;

View File

@@ -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

View File

@@ -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);
}

View File

@@ -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,
},
);

View File

@@ -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,

View File

@@ -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[];
}
}

View File

@@ -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 {

View File

@@ -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

View File

@@ -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" });
}
}