🎈 perf(eslint): 第二次 eslint

剩下的全给过了,明天起来跑 devtool 改 bug

Signed-off-by: BTMuli <BT-Muli@outlook.com>
(cherry picked from commit 59baf08cf897d31cabce4741910ea83c1a3a52d9)
This commit is contained in:
BTMuli
2023-04-06 03:08:40 +08:00
parent 6ec12bf664
commit 3c3df24f7d
24 changed files with 1968 additions and 2090 deletions

View File

@@ -29,9 +29,6 @@ rules:
no-case-declarations: off # 禁止在 case 子句中使用词法声明
no-useless-constructor: off # 禁用不必要的构造函数
# rules of standard
strictNullChecks: # 严格空值检查
- warn
- true
array-callback-return:
- error
- allowImplicit: true
@@ -65,9 +62,7 @@ rules:
"@typescript-eslint/comma-dangle":
- warn
- always-multiline
"@typescript-eslint/prefer-unllish-coalescing": # 优先使用 ?? 而不是 || 进行空值判断
- warn
- ignoreConditionalTests: true
"@typescript-eslint/prefer-nullish-coalescing": off # on 会出现一些问题
"@typescript-eslint/space-before-function-paren":
- warn
- always # 函数名和括号之间必须有空格

View File

@@ -1,27 +1,27 @@
<template>
<div v-if="isMain">
<v-layout>
<!-- 侧边栏菜单 -->
<t-sidebar />
<!-- 主体内容 -->
<v-main class="app-main">
<v-container fluid>
<router-view />
</v-container>
</v-main>
</v-layout>
</div>
<div v-else>
<v-layout>
<!-- 主体内容 -->
<v-main class="app-main">
<v-container fluid>
<router-view />
</v-container>
</v-main>
</v-layout>
</div>
<t-back-top />
<div v-if="isMain">
<v-layout>
<!-- 侧边栏菜单 -->
<TSidebar />
<!-- 主体内容 -->
<v-main class="app-main">
<v-container fluid>
<router-view />
</v-container>
</v-main>
</v-layout>
</div>
<div v-else>
<v-layout>
<!-- 主体内容 -->
<v-main class="app-main">
<v-container fluid>
<router-view />
</v-container>
</v-main>
</v-layout>
</div>
<TBackTop />
</template>
<script lang="ts" setup>
@@ -42,53 +42,53 @@ const appStore = useAppStore();
const isMain = ref(true as boolean);
onMounted(async () => {
// 获取当前窗口
const win = await window.getCurrent();
isMain.value = win.label === "tauri-genshin";
if (isMain.value) {
const title = "Tauri.Genshin v" + (await app.getVersion()) + " Alpha";
await win.setTitle(title);
await checkLoad();
}
// 获取当前窗口
const win = await window.getCurrent();
isMain.value = win.label === "tauri-genshin";
if (isMain.value) {
const title = "Tauri.Genshin v" + (await app.getVersion()) + " Alpha";
await win.setTitle(title);
await checkLoad();
}
});
async function checkLoad() {
await appStore.check();
if (appStore.loading) {
console.info("数据已加载!");
return;
}
DeleteTGData();
await createDataDir();
await writeData();
await writeIndex();
appStore.loading = true;
console.info("数据加载完成!");
async function checkLoad () {
await appStore.check();
if (appStore.loading) {
console.info("数据已加载!");
return;
}
DeleteTGData();
await createDataDir();
await writeData();
await writeIndex();
appStore.loading = true;
console.info("数据加载完成!");
}
// 创建数据文件夹
async function createDataDir() {
console.info("开始创建数据文件夹...");
await fs.createDir("appData", { dir: fs.BaseDirectory.AppLocalData, recursive: true });
await fs.createDir("userData", { dir: fs.BaseDirectory.AppLocalData, recursive: true });
await fs.createDir("tempData", { dir: fs.BaseDirectory.AppLocalData, recursive: true });
console.info("数据文件夹创建完成!");
async function createDataDir () {
console.info("开始创建数据文件夹...");
await fs.createDir("appData", { dir: fs.BaseDirectory.AppLocalData, recursive: true });
await fs.createDir("userData", { dir: fs.BaseDirectory.AppLocalData, recursive: true });
await fs.createDir("tempData", { dir: fs.BaseDirectory.AppLocalData, recursive: true });
console.info("数据文件夹创建完成!");
}
// 将数据写入文件夹
async function writeData() {
console.info("开始写入数据...");
TGAppDataList.map(async item => {
await fs.writeFile(`${appStore.dataPath.app}\\${item.name}`, JSON.stringify(item.data));
});
console.info("数据写入完成!");
async function writeData () {
console.info("开始写入数据...");
TGAppDataList.map(async (item) => {
await fs.writeFile(`${appStore.dataPath.app}\\${item.name}`, JSON.stringify(item.data));
});
console.info("数据写入完成!");
}
// 写入 IndexedDB
async function writeIndex() {
console.info("开始写入 IndexedDB...");
await InitTGData();
TGGetDataList.map(async item => {
await WriteTGData(item.name, item.data);
});
console.info("IndexedDB 写入完成!");
async function writeIndex () {
console.info("开始写入 IndexedDB...");
await InitTGData();
TGGetDataList.map(async (item) => {
await WriteTGData(item.name, item.data);
});
console.info("IndexedDB 写入完成!");
}
</script>
<style lang="css">

View File

@@ -1,50 +1,50 @@
<template>
<transition name="fade">
<div class="back-top" v-show="canTop" @click="handleScrollTop">
<img src="../assets/icons/arrow-top.svg" alt="back-icon" />
</div>
</transition>
<transition name="fade">
<div v-show="canTop" class="back-top" @click="handleScrollTop">
<img src="../assets/icons/arrow-top.svg" alt="back-icon">
</div>
</transition>
</template>
<script lang="ts" setup>
// vue
import { ref, onMounted } from "vue";
const scrollTop = ref(0); //滚动条距离顶部的距离
const canTop = ref(false); //默认不显示
const scrollTop = ref(0); // 滚动条距离顶部的距离
const canTop = ref(false); // 默认不显示
// 监听滚动事件
function handleScroll() {
scrollTop.value = document.documentElement.scrollTop || document.body.scrollTop;
// 超过500px显示回到顶部按钮
canTop.value = scrollTop.value > 500;
// 没超过500但是到底部了也显示回到顶部按钮
if (!canTop.value) {
const scrollHeight = document.documentElement.scrollHeight || document.body.scrollHeight;
const clientHeight = document.documentElement.clientHeight || document.body.clientHeight;
canTop.value = scrollHeight - clientHeight - scrollTop.value <= 0;
}
function handleScroll () {
scrollTop.value = document.documentElement.scrollTop || document.body.scrollTop;
// 超过500px显示回到顶部按钮
canTop.value = scrollTop.value > 500;
// 没超过500但是到底部了也显示回到顶部按钮
if (!canTop.value) {
const scrollHeight = document.documentElement.scrollHeight || document.body.scrollHeight;
const clientHeight = document.documentElement.clientHeight || document.body.clientHeight;
canTop.value = scrollHeight - clientHeight - scrollTop.value <= 0;
}
}
// 点击回到顶部
function handleScrollTop() {
let timer = 0;
cancelAnimationFrame(timer);
timer = requestAnimationFrame(function fn() {
if (scrollTop.value > 0) {
scrollTop.value -= 50;
document.body.scrollTop = document.documentElement.scrollTop = scrollTop.value;
timer = requestAnimationFrame(fn);
} else {
cancelAnimationFrame(timer);
canTop.value = false;
}
});
function handleScrollTop () {
let timer = 0;
cancelAnimationFrame(timer);
timer = requestAnimationFrame(function fn () {
if (scrollTop.value > 0) {
scrollTop.value -= 50;
document.body.scrollTop = document.documentElement.scrollTop = scrollTop.value;
timer = requestAnimationFrame(fn);
} else {
cancelAnimationFrame(timer);
canTop.value = false;
}
});
}
// 监听滚动事件
// @ts-ignore
onMounted(() => {
window.addEventListener("scroll", handleScroll);
window.addEventListener("scroll", handleScroll);
});
</script>

View File

@@ -1,52 +1,42 @@
<template>
<v-list class="calendar-card">
<v-list-item>
<v-list-item-title
style="color: #fec90b; margin-left: 10px; margin-bottom: 10px; font-family: Genshin, serif"
>
<v-icon color="#EBD49E">mdi-calendar-clock</v-icon> 今日素材
<span style="color: #faf7e8">{{ new Date().toLocaleDateString() }}</span>
<v-btn
v-for="text of btnText"
@click="getShowCards(text.week)"
class="calendar-btn"
:style="{
border: text.week === weekNow ? '2px solid #fec90b' : '0',
background: text.week === btnNow ? '#fec90b' : '#4A5366',
}"
>
{{ text.text }}
</v-btn>
</v-list-item-title>
<div v-if="!loading" class="calendar-grid">
<v-card title="天赋培养" class="calendar-single">
<v-card-text class="calendar-icons">
<v-img
v-for="item in showCharacters"
:src="item.cover"
class="calendar-icon"
@click="showContent(item)"
/>
</v-card-text>
</v-card>
<v-card title="武器突破" class="calendar-single">
<v-card-text class="calendar-icons">
<v-img
v-for="item in showWeapons"
:src="item.cover"
class="calendar-icon"
@click="showContent(item)"
/>
</v-card-text>
</v-card>
</div>
</v-list-item>
</v-list>
<v-list class="calendar-card">
<v-list-item>
<v-list-item-title style="color: #fec90b; margin-left: 10px; margin-bottom: 10px; font-family: Genshin, serif">
<v-icon color="#EBD49E">
mdi-calendar-clock
</v-icon> 今日素材
<span style="color: #faf7e8">{{ new Date().toLocaleDateString() }}</span>
<v-btn
v-for="text of btnText"
:key="text.week"
class="calendar-btn"
:style="{
border: text.week === weekNow ? '2px solid #fec90b' : '0',
background: text.week === btnNow ? '#fec90b' : '#4A5366',
}"
@click="getShowCards(text.week)"
>
{{ text.text }}
</v-btn>
</v-list-item-title>
<div v-if="!loading" class="calendar-grid">
<v-card title="天赋培养" class="calendar-single">
<v-card-text class="calendar-icons">
<v-img v-for="item in showCharacters" :key="item.id" :src="item.cover" class="calendar-icon" @click="showContent(item)" />
</v-card-text>
</v-card>
<v-card title="武器突破" class="calendar-single">
<v-card-text class="calendar-icons">
<v-img v-for="item in showWeapons" :key="item.id" :src="item.cover" class="calendar-icon" @click="showContent(item)" />
</v-card-text>
</v-card>
</div>
</v-list-item>
</v-list>
</template>
<script lang="ts" setup>
// vue
import { ref, onMounted } from "vue";
import { useRouter } from "vue-router";
// plugins
import MysOper from "../plugins/Mys";
// interface
@@ -63,77 +53,76 @@ const weekNow = ref(new Date().getDay());
const btnNow = ref(0 as number);
const btnText = [
{
week: 0,
text: "周日",
},
{
week: 1,
text: "周一",
},
{
week: 2,
text: "周二",
},
{
week: 3,
text: "周三",
},
{
week: 4,
text: "周四",
},
{
week: 5,
text: "周五",
},
{
week: 6,
text: "周六",
},
{
week: 0,
text: "周日",
},
{
week: 1,
text: "周一",
},
{
week: 2,
text: "周二",
},
{
week: 3,
text: "周三",
},
{
week: 4,
text: "周四",
},
{
week: 5,
text: "周五",
},
{
week: 6,
text: "周六",
},
];
// show data
const showCharacters = ref([] as CalendarCard[]);
const showWeapons = ref([] as CalendarCard[]);
const router = useRouter();
// expose
defineExpose({
name: "素材日历",
loading,
name: "素材日历",
loading,
});
onMounted(async () => {
const calendarData = await MysOper.Calendar.get();
if (!calendarData) {
await console.error("获取材料日历失败");
return;
}
const calendarCards = MysOper.Calendar.card(calendarData);
const week = new Date().getDay();
btnNow.value = week;
characterCards.value = calendarCards.filter(card => card.type === 2);
weaponCards.value = calendarCards.filter(card => card.type === 1);
getShowCards(week);
loading.value = false;
const calendarData = await MysOper.Calendar.get();
if (!calendarData) {
await console.error("获取材料日历失败");
return;
}
const calendarCards = MysOper.Calendar.card(calendarData);
const week = new Date().getDay();
btnNow.value = week;
characterCards.value = calendarCards.filter((card) => card.type === 2);
weaponCards.value = calendarCards.filter((card) => card.type === 1);
getShowCards(week);
loading.value = false;
});
// 根据星期几获取显示内容
function getShowCards(choice: number) {
btnNow.value = choice;
const week = choice === 0 ? 7 : choice;
showCharacters.value = characterCards.value
.filter(card => card.drop_day.includes(week.toString()))
.sort((a, b) => a.sort_day[week] - b.sort_day[week]);
showWeapons.value = weaponCards.value
.filter(card => card.drop_day.includes(week.toString()))
.sort((a, b) => a.sort_day[week] - b.sort_day[week]);
function getShowCards (choice: number) {
btnNow.value = choice;
const week = choice === 0 ? 7 : choice;
showCharacters.value = characterCards.value
.filter((card) => card.drop_day.includes(week.toString()))
.sort((a, b) => a.sort_day[week] - b.sort_day[week]);
showWeapons.value = weaponCards.value
.filter((card) => card.drop_day.includes(week.toString()))
.sort((a, b) => a.sort_day[week] - b.sort_day[week]);
}
function showContent(item: CalendarCard) {
// todo二级跳转目前先直接跳到角色详情页
window.open(OBC_CONTENT_API.replace("{content_id}", item.url));
function showContent (item: CalendarCard) {
// todo二级跳转目前先直接跳到角色详情页
window.open(OBC_CONTENT_API.replace("{content_id}", item.url));
}
</script>
<style lang="css" scoped>

View File

@@ -1,27 +1,27 @@
<template>
<v-overlay v-model="visible">
<div class="confirm-div">
<div class="confirm-box">
<div class="confirm-title">
{{ title }}
</div>
<div class="confirm-btn-box">
<button class="confirm-btn" @click="onCancel">
<img class="btn-icon" src="../assets/icons/circle-cancel.svg" alt="cancel" />
<span class="btn-text">
{{ cancel }}
</span>
</button>
<button class="confirm-btn" @click="onConfirm">
<img class="btn-icon" src="../assets/icons/circle-check.svg" alt="confirm" />
<span class="btn-text">
{{ confirm }}
</span>
</button>
</div>
</div>
</div>
</v-overlay>
<v-overlay v-model="visible">
<div class="confirm-div">
<div class="confirm-box">
<div class="confirm-title">
{{ title }}
</div>
<div class="confirm-btn-box">
<button class="confirm-btn" @click="onCancel">
<img class="btn-icon" src="../assets/icons/circle-cancel.svg" alt="cancel">
<span class="btn-text">
{{ cancel }}
</span>
</button>
<button class="confirm-btn" @click="onConfirm">
<img class="btn-icon" src="../assets/icons/circle-check.svg" alt="confirm">
<span class="btn-text">
{{ confirm }}
</span>
</button>
</div>
</div>
</div>
</v-overlay>
</template>
<script lang="ts" setup>
@@ -29,40 +29,40 @@
import { computed } from "vue";
interface TConfirmProps {
title: string;
cancel?: string;
confirm?: string;
/** 此值为 true 时显示对话框 */
modelValue: boolean;
title: string;
cancel?: string;
confirm?: string;
/** 此值为 true 时显示对话框 */
modelValue: boolean;
}
interface TConfirmEmits {
(e: "update:show", v: boolean): void;
(e: "update:modelValue", v: boolean): void;
(e: "confirm"): void;
(e: "cancel"): void;
(e: "update:show", v: boolean): void;
(e: "update:modelValue", v: boolean): void;
(e: "confirm"): void;
(e: "cancel"): void;
}
const emits = defineEmits<TConfirmEmits>();
const props = withDefaults(defineProps<TConfirmProps>(), {
title: "确认",
cancel: "取消",
confirm: "确定",
title: "确认",
cancel: "取消",
confirm: "确定",
});
const visible = computed({
get: () => props.modelValue,
set: v => emits("update:modelValue", v),
get: () => props.modelValue,
set: (v) => emits("update:modelValue", v),
});
const onCancel = () => {
visible.value = false;
emits("cancel");
visible.value = false;
emits("cancel");
};
const onConfirm = () => {
visible.value = false;
emits("confirm");
visible.value = false;
emits("confirm");
};
</script>
@@ -73,7 +73,7 @@ const onConfirm = () => {
height: 20vh;
top: 40vh;
left: 30vw;
background: #ffffff;
background: #fff;
border-radius: 10px;
padding: 10px;
display: flex;

View File

@@ -1,35 +1,43 @@
<template>
<div class="loading-div">
<div class="loading-content">
<div class="loading-title">
{{ title }}
<v-progress-circular indeterminate color="#f4d8a8" v-show="!empty" />
</div>
<div class="loading-subtitle" v-show="subtitle">{{ subtitle }}</div>
<div class="loading-img" v-if="!empty">
<img src="/source/UI/loading.webp" alt="loading" />
</div>
<div class="loading-img" v-else>
<img src="/source/UI/empty.webp" alt="empty" />
</div>
<div class="loading-text" v-show="content">{{ content }}</div>
</div>
</div>
<div class="loading-div">
<div class="loading-content">
<div class="loading-title">
{{ title }}
<v-progress-circular v-show="!empty" indeterminate color="#f4d8a8" />
</div>
<div v-if="subtitle !== ''" class="loading-subtitle">
{{ subtitle }}
</div>
<div v-if="!empty" class="loading-img">
<img src="/source/UI/loading.webp" alt="loading">
</div>
<div v-else class="loading-img">
<img src="/source/UI/empty.webp" alt="empty">
</div>
<div v-if="content !== ''" class="loading-text">
{{ content }}
</div>
</div>
</div>
</template>
<script lang="ts" setup>
interface LoadingProps {
title?: string;
subtitle?: string;
content?: string;
empty?: boolean;
position?: string;
}
withDefaults(
defineProps<{
title?: string;
subtitle?: string;
content?: string;
empty?: boolean;
position?: string;
}>(),
{
title: "加载中",
empty: false,
position: "absolute",
}
defineProps<LoadingProps>(),
{
title: "加载中",
subtitle: "",
content: "",
empty: false,
position: "absolute",
},
);
</script>
<style lang="css" scoped>
@@ -40,7 +48,7 @@ withDefaults(
left: 25%;
width: 50%;
height: 50%;
background: rgba(57, 59, 64, 0.5);
background: rgb(57 59 64 / 50%);
backdrop-filter: blur(10px);
border-radius: 20px;
}

View File

@@ -1,56 +1,45 @@
<template>
<v-list class="pool-card">
<v-list-item>
<v-list-item-title style="color: #fec90b; margin-left: 10px; font-family: Genshin, serif">
<img src="../assets/icons/icon-wish.svg" alt="wish" class="pool-wish-icon" />
限时祈愿</v-list-item-title
>
<div v-if="!loading" class="pool-grid">
<v-card
v-for="pool in poolCards"
style="background: #faf7e8; color: #546d8b; border-radius: 10px"
>
<v-list style="background: #faf7e8; color: #546d8b">
<v-list-item :title="pool.title" :subtitle="pool.subtitle">
<template v-slot:prepend>
<v-img
:src="pool.voice.icon"
style="transform: translate(0, -10px); width: 60px; height: 60px"
/>
</template>
<template v-slot:append>
<audio :src="pool.voice.url" controls />
</template>
</v-list-item>
</v-list>
<div class="pool-cover" @click="toPost(pool)">
<img :src="pool.cover" alt="cover" />
</div>
<div class="pool-character">
<div v-for="character in pool.characters" @click="toOuter(character.url, pool.title)">
<img :src="character.icon" class="pool-icon" alt="character" />
</div>
<div class="pool-clock">
<v-progress-circular
:model-value="poolTimePass[pool.post_id]"
size="100"
width="10"
color="#90caf9"
>
{{ poolTimeGet[pool.post_id] }}
</v-progress-circular>
</div>
</div>
<v-card-text>
<span style="width: 60%">
<v-icon>mdi-calendar-clock</v-icon>
{{ pool.time.start }}~{{ pool.time.end }}
</span>
</v-card-text>
</v-card>
</div>
</v-list-item>
</v-list>
<v-list class="pool-card">
<v-list-item>
<v-list-item-title style="color: #fec90b; margin-left: 10px; font-family: Genshin, serif">
<img src="../assets/icons/icon-wish.svg" alt="wish" class="pool-wish-icon">
限时祈愿
</v-list-item-title>
<div v-if="!loading" class="pool-grid">
<v-card v-for="pool in poolCards" :key="pool.post_id" style="background: #faf7e8; color: #546d8b; border-radius: 10px">
<v-list style="background: #faf7e8; color: #546d8b">
<v-list-item :title="pool.title" :subtitle="pool.subtitle">
<template #prepend>
<v-img :src="pool.voice.icon" style="transform: translate(0, -10px); width: 60px; height: 60px" />
</template>
<template #append>
<audio :src="pool.voice.url" controls />
</template>
</v-list-item>
</v-list>
<div class="pool-cover" @click="toPost(pool)">
<img :src="pool.cover" alt="cover">
</div>
<div class="pool-character">
<div v-for="character in pool.characters" :key="character.url" @click="toOuter(character.url, pool.title)">
<img :src="character.icon" class="pool-icon" alt="character">
</div>
<div class="pool-clock">
<v-progress-circular :model-value="poolTimePass[pool.post_id]" size="100" width="10" color="#90caf9">
{{ poolTimeGet[pool.post_id] }}
</v-progress-circular>
</div>
</div>
<v-card-text>
<span style="width: 60%">
<v-icon>mdi-calendar-clock</v-icon>
{{ pool.time.start }}~{{ pool.time.end }}
</span>
</v-card-text>
</v-card>
</div>
</v-list-item>
</v-list>
</template>
<script lang="ts" setup>
// vue
@@ -82,78 +71,87 @@ const poolTimePass = ref({} as Map<number>);
// expose
defineExpose({
name: "限时祈愿",
loading,
name: "限时祈愿",
loading,
});
onMounted(async () => {
const gachaData = await MysOper.Gacha.get();
if (!gachaData) {
await console.error("获取限时祈愿数据失败");
return;
}
if (!checkCover(gachaData)) {
poolCards.value = await MysOper.Gacha.card(gachaData);
let coverData: Map<string> = {};
poolCards.value.map(pool => {
coverData[pool.post_id] = pool.cover;
});
homeStore.poolCover = coverData;
} else {
poolCards.value = await MysOper.Gacha.card(gachaData, homeStore.poolCover);
}
poolCards.value.map(pool => {
poolTimeGet.value[pool.post_id] = getLastPoolTime(pool.time.end_stamp - Date.now());
poolTimePass.value[pool.post_id] = pool.time.end_stamp - Date.now();
});
await setInterval(() => {
poolCards.value.map(pool => {
poolTimeGet.value[pool.post_id] = getLastPoolTime(pool.time.end_stamp - Date.now());
poolTimePass.value[pool.post_id] =
const gachaData = await MysOper.Gacha.get();
if (!gachaData) {
await console.error("获取限时祈愿数据失败");
return;
}
if (!checkCover(gachaData)) {
poolCards.value = await MysOper.Gacha.card(gachaData);
const coverData: Map<string> = {};
poolCards.value.map((pool) => {
coverData[pool.post_id] = pool.cover;
return pool;
});
homeStore.poolCover = coverData;
} else {
poolCards.value = await MysOper.Gacha.card(gachaData, homeStore.poolCover);
}
poolCards.value.map((pool) => {
poolTimeGet.value[pool.post_id] = getLastPoolTime(pool.time.end_stamp - Date.now());
poolTimePass.value[pool.post_id] = pool.time.end_stamp - Date.now();
return pool;
});
await setInterval(() => {
poolCards.value.map((pool) => {
poolTimeGet.value[pool.post_id] = getLastPoolTime(pool.time.end_stamp - Date.now());
poolTimePass.value[pool.post_id] =
((pool.time.end_stamp - Date.now()) / (pool.time.end_stamp - pool.time.start_stamp)) * 100;
});
}, 1000);
loading.value = false;
return pool;
});
}, 1000);
loading.value = false;
});
// 检测是否有新的限时祈愿
function checkCover(data: GachaData[]) {
// 如果没有缓存
if (!homeStore.poolCover || Object.keys(homeStore.poolCover).length === 0) {
return false;
}
// 获取缓存
const cover = homeStore.poolCover;
return data.every(item => {
const post_id = item.activity_url.split("/").pop();
if (!post_id || isNaN(Number(post_id))) {
return false;
}
return (
cover[Number(post_id)] !== undefined && cover[Number(post_id)] !== "/source/UI/empty.webp"
);
});
function checkCover (data: GachaData[]) {
// 如果没有缓存
if (!homeStore.poolCover || Object.keys(homeStore.poolCover).length === 0) {
return false;
}
// 获取缓存
const cover = homeStore.poolCover satisfies Map<string>;
if (cover === undefined || cover === null) {
return false;
}
return data.every((item) => {
const post_id = item.activity_url.split("/").pop();
if (!post_id || isNaN(Number(post_id))) {
return false;
}
if (!Object.keys(cover).includes(post_id)) {
return false;
} else {
const coverUrl = Object.keys(cover).find((key) => key === post_id);
return coverUrl !== "/source/UI/empty.webp";
}
});
}
function toOuter(url: string, title: string) {
createTGWindow(url, "祈愿", title, 1200, 800, true);
function toOuter (url: string, title: string) {
createTGWindow(url, "祈愿", title, 1200, 800, true);
}
function getLastPoolTime(time: number) {
const hour = Math.floor(time / 1000 / 60 / 60);
const minute = Math.floor((time / 1000 / 60 / 60 - hour) * 60);
const second = Math.floor(((time / 1000 / 60 / 60 - hour) * 60 - minute) * 60);
return `${hour}:${minute.toFixed(0).padStart(2, "0")}:${second.toFixed(0).padStart(2, "0")}`;
function getLastPoolTime (time: number) {
const hour = Math.floor(time / 1000 / 60 / 60);
const minute = Math.floor((time / 1000 / 60 / 60 - hour) * 60);
const second = Math.floor(((time / 1000 / 60 / 60 - hour) * 60 - minute) * 60);
return `${hour}:${minute.toFixed(0).padStart(2, "0")}:${second.toFixed(0).padStart(2, "0")}`;
}
function toPost(pool: GachaCard) {
const path = router.resolve({
name: "帖子详情",
params: {
post_id: pool.post_id.toString(),
},
}).href;
createTGWindow(path, "限时祈愿", pool.title, 960, 720, false);
function toPost (pool: GachaCard) {
const path = router.resolve({
name: "帖子详情",
params: {
post_id: pool.post_id.toString(),
},
}).href;
createTGWindow(path, "限时祈愿", pool.title, 960, 720, false);
}
</script>
@@ -165,7 +163,7 @@ function toPost(pool: GachaCard) {
}
.pool-card {
font-family: "Genshin", serif;
font-family: Genshin, serif;
width: 100%;
background: #546d8b;
border-radius: 10px;

View File

@@ -1,52 +1,47 @@
<template>
<v-list class="position-card">
<v-list-item>
<v-list-item-title style="color: #fec90b; margin-left: 10px; font-family: Genshin, serif">
<img src="../assets/icons/board.svg" alt="act" class="position-act-icon" />
近期活动
</v-list-item-title>
<div v-if="!loading" class="position-grid">
<v-card
v-for="card in positionCards"
style="background: #faf7e8; color: #546d8b; border-radius: 10px"
>
<v-list style="background: #faf7e8; color: #546d8b">
<v-list-item :title="card.title" :subtitle="card.abstract">
<template v-slot:prepend>
<v-avatar rounded="0" @click="toPost(card)" style="cursor: pointer">
<v-img :src="card.icon" style="border-radius: 10px" />
</v-avatar>
</template>
</v-list-item>
</v-list>
<v-divider class="border-opacity-75"></v-divider>
<v-card-text>
<span style="width: 60%">
<v-icon>mdi-calendar-clock</v-icon>
{{ card.time.start }}~{{ card.time.end }}
</span>
</v-card-text>
<v-card-actions>
<span style="width: 80%; margin-left: 10px">
<v-icon>mdi-clock-outline</v-icon>
剩余时间
<span style="color: #90caf9" v-if="positionTimeGet[card.post_id] !== '已结束'">{{
positionTimeGet[card.post_id]
}}</span>
<span style="color: #ff6d6d" v-if="positionTimeGet[card.post_id] === '已结束'"
>已结束</span
>
</span>
<v-btn @click="toPost(card)" class="card-btn">
<template v-slot:prepend>
<img src="../assets/icons/circle-check.svg" alt="check" />查看
</template>
</v-btn>
</v-card-actions>
</v-card>
</div>
</v-list-item>
</v-list>
<v-list class="position-card">
<v-list-item>
<v-list-item-title style="color: #fec90b; margin-left: 10px; font-family: Genshin, serif">
<img src="../assets/icons/board.svg" alt="act" class="position-act-icon">
近期活动
</v-list-item-title>
<div v-if="!loading" class="position-grid">
<v-card v-for="card in positionCards" :key="card.post_id" style="background: #faf7e8; color: #546d8b; border-radius: 10px">
<v-list style="background: #faf7e8; color: #546d8b">
<v-list-item :title="card.title" :subtitle="card.abstract">
<template #prepend>
<v-avatar rounded="0" style="cursor: pointer" @click="toPost(card)">
<v-img :src="card.icon" style="border-radius: 10px" />
</v-avatar>
</template>
</v-list-item>
</v-list>
<v-divider class="border-opacity-75" />
<v-card-text>
<span style="width: 60%">
<v-icon>mdi-calendar-clock</v-icon>
{{ card.time.start }}~{{ card.time.end }}
</span>
</v-card-text>
<v-card-actions>
<span style="width: 80%; margin-left: 10px">
<v-icon>mdi-clock-outline</v-icon>
剩余时间
<span v-if="positionTimeGet[card.post_id] !== '已结束'" style="color: #90caf9">{{
positionTimeGet[card.post_id]
}}</span>
<span v-if="positionTimeGet[card.post_id] === '已结束'" style="color: #ff6d6d">已结束</span>
</span>
<v-btn class="card-btn" @click="toPost(card)">
<template #prepend>
<img src="../assets/icons/circle-check.svg" alt="check">查看
</template>
</v-btn>
</v-card-actions>
</v-card>
</div>
</v-list-item>
</v-list>
</template>
<script lang="ts" setup>
// vue
@@ -71,55 +66,55 @@ const router = useRouter();
// expose
defineExpose({
name: "近期活动",
loading,
name: "近期活动",
loading,
});
onMounted(async () => {
const positionData = await MysOper.Position.get();
if (!positionData) {
console.error("获取近期活动失败");
return;
}
positionCards.value = MysOper.Position.card(positionData);
positionCards.value.forEach(card => {
positionTimeGet.value[card.post_id] = getLastPositionTime(card.time.end_stamp - Date.now());
positionTimeEnd.value[card.post_id] = card.time.end_stamp;
});
await setInterval(() => {
positionCards.value.forEach(card => {
const time = card.time.end_stamp - Date.now();
if (time <= 0) {
positionTimeGet.value[card.post_id] = "已结束";
return;
}
positionTimeGet.value[card.post_id] = getLastPositionTime(time);
});
}, 1000);
loading.value = false;
const positionData = await MysOper.Position.get();
if (!positionData) {
console.error("获取近期活动失败");
return;
}
positionCards.value = MysOper.Position.card(positionData);
positionCards.value.forEach((card) => {
positionTimeGet.value[card.post_id] = getLastPositionTime(card.time.end_stamp - Date.now());
positionTimeEnd.value[card.post_id] = card.time.end_stamp;
});
await setInterval(() => {
positionCards.value.forEach((card) => {
const time = card.time.end_stamp - Date.now();
if (time <= 0) {
positionTimeGet.value[card.post_id] = "已结束";
return;
}
positionTimeGet.value[card.post_id] = getLastPositionTime(time);
});
}, 1000);
loading.value = false;
});
function getLastPositionTime(time: number) {
const day = Math.floor(time / (24 * 3600 * 1000));
const hour = Math.floor((time % (24 * 3600 * 1000)) / (3600 * 1000));
const minute = Math.floor((time % (3600 * 1000)) / (60 * 1000));
const second = Math.floor((time % (60 * 1000)) / 1000);
return `${day}${hour.toFixed(0).padStart(2, "0")}:${minute
.toFixed(0)
.padStart(2, "0")}:${second.toFixed(0).padStart(2, "0")}`;
function getLastPositionTime (time: number) {
const day = Math.floor(time / (24 * 3600 * 1000));
const hour = Math.floor((time % (24 * 3600 * 1000)) / (3600 * 1000));
const minute = Math.floor((time % (3600 * 1000)) / (60 * 1000));
const second = Math.floor((time % (60 * 1000)) / 1000);
return `${day}${hour.toFixed(0).padStart(2, "0")}:${minute.toFixed(0).padStart(2, "0")}:${second
.toFixed(0)
.padStart(2, "0")}`;
}
async function toPost(card: PositionCard) {
const post_id = card.post_id;
// 获取路由路径
const path = router.resolve({
name: "帖子详情",
params: {
post_id: post_id,
},
}).href;
// 打开新窗口
createTGWindow(path, "近期活动", card.title, 960, 720, false);
async function toPost (card: PositionCard) {
const post_id = card.post_id;
// 获取路由路径
const path = router.resolve({
name: "帖子详情",
params: {
post_id,
},
}).href;
// 打开新窗口
createTGWindow(path, "近期活动", card.title, 960, 720, false);
}
</script>
@@ -132,7 +127,7 @@ async function toPost(card: PositionCard) {
.position-card {
margin-top: 10px;
font-family: "Genshin", serif;
font-family: Genshin, serif;
background: #546d8b;
border-radius: 10px;
}

View File

@@ -1,104 +1,110 @@
<template>
<v-navigation-drawer permanent :rail="rail" style="background: #485466; color: #faf7e8">
<v-list class="sideList" density="compact" v-model:opened="open" nav>
<!-- 负责收缩侧边栏 -->
<v-list-item @click="collapse">
<template v-slot:prepend v-if="rail">
<v-list-item-action>
<v-icon color="rgb(205, 182, 145)">mdi-chevron-right</v-icon>
</v-list-item-action>
</template>
<template v-slot:append v-else>
<v-list-item-action>
<v-icon color="rgb(205, 182, 145)">mdi-chevron-left</v-icon>
</v-list-item-action>
</template>
</v-list-item>
<!-- 菜单项 -->
<v-list-item value="home" title="首页" link href="/">
<template v-slot:prepend>
<img src="/source/UI/paimon.webp" alt="homeIcon" class="sideIcon" />
</template>
</v-list-item>
<v-list-item title="公告" value="announcements" link href="/announcements">
<template v-slot:prepend>
<img src="../assets/icons/board.svg" alt="annoIcon" class="sideIcon" />
</template>
</v-list-item>
<v-divider></v-divider>
<v-list-group value="mihoyo" fluid>
<template v-slot:activator="{ props }">
<v-list-item title="米游社" v-bind="props">
<template v-slot:prepend>
<img src="/platforms/mhy/mys.webp" alt="mihoyo" class="sideIcon" />
</template>
</v-list-item>
</template>
<v-list-item title="原神" value="mhy-ys" link href="/news/2">
<template v-slot:prepend>
<img src="/platforms/mhy/ys.webp" alt="ys" class="sideIcon" />
</template>
</v-list-item>
<v-list-item title="崩坏3" value="mhy-bh3" link href="/news/1">
<template v-slot:prepend>
<img src="/platforms/mhy/bh3.webp" alt="bh3" class="sideIcon" />
</template>
</v-list-item>
<v-list-item title="崩坏2" value="mhy-bh2" link href="/news/3">
<template v-slot:prepend>
<img src="/platforms/mhy/bh2.webp" alt="bh2" class="sideIcon" />
</template>
</v-list-item>
<v-list-item title="未定事件簿" value="mhy-wd" link href="/news/4">
<template v-slot:prepend>
<img src="/platforms/mhy/wd.webp" alt="wd" class="sideIcon" />
</template>
</v-list-item>
<v-list-item title="星穹铁道" value="mhy-sr" link href="/news/6">
<template v-slot:prepend>
<img src="/platforms/mhy/sr.webp" alt="sr" class="sideIcon" />
</template>
</v-list-item>
<v-list-item title="绝区零" value="mhy-zzz" link href="/news/8">
<template v-slot:prepend>
<img src="/platforms/mhy/zzz.webp" alt="zzz" class="sideIcon" />
</template>
</v-list-item>
<v-list-item title="大别野" value="mhy-dby" link href="/news/5">
<template v-slot:prepend>
<img src="/platforms/mhy/dby.webp" alt="dby" class="sideIcon" />
</template>
</v-list-item>
</v-list-group>
<v-divider></v-divider>
<v-list-item title="成就" value="achievements" link href="/achievements">
<template v-slot:prepend>
<img src="../assets/icons/achievements.svg" alt="achievementsIcon" class="sideIcon" />
</template>
</v-list-item>
<v-divider></v-divider>
<v-list-group value="database" fluid>
<template v-slot:activator="{ props }">
<v-list-item title="数据库" v-bind="props">
<template v-slot:prepend>
<v-icon color="rgb(205, 182, 145)">mdi-database</v-icon>
</template>
</v-list-item>
</template>
<v-list-item title="GCG" value="db-GCG" link href="/GCG">
<template v-slot:prepend>
<img src="../assets/icons/GCG.svg" alt="gcgIcon" class="sideIcon" />
</template>
</v-list-item>
</v-list-group>
<v-divider></v-divider>
<v-list-item title="设置" value="config" link href="/config">
<template v-slot:prepend>
<img src="../assets/icons/setting.svg" alt="setting" class="sideIcon" />
</template>
</v-list-item>
</v-list>
</v-navigation-drawer>
<v-navigation-drawer permanent :rail="rail" style="background: #485466; color: #faf7e8">
<v-list v-model:opened="open" class="side-list" density="compact" nav>
<!-- 负责收缩侧边栏 -->
<v-list-item @click="collapse">
<template v-if="rail" #prepend>
<v-list-item-action>
<v-icon color="rgb(205, 182, 145)">
mdi-chevron-right
</v-icon>
</v-list-item-action>
</template>
<template v-else #append>
<v-list-item-action>
<v-icon color="rgb(205, 182, 145)">
mdi-chevron-left
</v-icon>
</v-list-item-action>
</template>
</v-list-item>
<!-- 菜单项 -->
<v-list-item value="home" title="首页" link href="/">
<template #prepend>
<img src="/source/UI/paimon.webp" alt="homeIcon" class="side-icon">
</template>
</v-list-item>
<v-list-item title="公告" value="announcements" link href="/announcements">
<template #prepend>
<img src="../assets/icons/board.svg" alt="annoIcon" class="side-icon">
</template>
</v-list-item>
<v-divider />
<v-list-group value="mihoyo" fluid>
<template #activator="{ props }">
<v-list-item title="米游社" v-bind="props">
<template #prepend>
<img src="/platforms/mhy/mys.webp" alt="mihoyo" class="side-icon">
</template>
</v-list-item>
</template>
<v-list-item title="原神" value="mhy-ys" link href="/news/2">
<template #prepend>
<img src="/platforms/mhy/ys.webp" alt="ys" class="side-icon">
</template>
</v-list-item>
<v-list-item title="崩坏3" value="mhy-bh3" link href="/news/1">
<template #prepend>
<img src="/platforms/mhy/bh3.webp" alt="bh3" class="side-icon">
</template>
</v-list-item>
<v-list-item title="崩坏2" value="mhy-bh2" link href="/news/3">
<template #prepend>
<img src="/platforms/mhy/bh2.webp" alt="bh2" class="side-icon">
</template>
</v-list-item>
<v-list-item title="未定事件簿" value="mhy-wd" link href="/news/4">
<template #prepend>
<img src="/platforms/mhy/wd.webp" alt="wd" class="side-icon">
</template>
</v-list-item>
<v-list-item title="星穹铁道" value="mhy-sr" link href="/news/6">
<template #prepend>
<img src="/platforms/mhy/sr.webp" alt="sr" class="side-icon">
</template>
</v-list-item>
<v-list-item title="绝区零" value="mhy-zzz" link href="/news/8">
<template #prepend>
<img src="/platforms/mhy/zzz.webp" alt="zzz" class="side-icon">
</template>
</v-list-item>
<v-list-item title="大别野" value="mhy-dby" link href="/news/5">
<template #prepend>
<img src="/platforms/mhy/dby.webp" alt="dby" class="side-icon">
</template>
</v-list-item>
</v-list-group>
<v-divider />
<v-list-item title="成就" value="achievements" link href="/achievements">
<template #prepend>
<img src="../assets/icons/achievements.svg" alt="achievementsIcon" class="side-icon">
</template>
</v-list-item>
<v-divider />
<v-list-group value="database" fluid>
<template #activator="{ props }">
<v-list-item title="数据库" v-bind="props">
<template #prepend>
<v-icon color="rgb(205, 182, 145)">
mdi-database
</v-icon>
</template>
</v-list-item>
</template>
<v-list-item title="GCG" value="db-GCG" link href="/GCG">
<template #prepend>
<img src="../assets/icons/GCG.svg" alt="gcgIcon" class="side-icon">
</template>
</v-list-item>
</v-list-group>
<v-divider />
<v-list-item title="设置" value="config" link href="/config">
<template #prepend>
<img src="../assets/icons/setting.svg" alt="setting" class="side-icon">
</template>
</v-list-item>
</v-list>
</v-navigation-drawer>
</template>
<script lang="ts" setup>
@@ -111,27 +117,27 @@ const appStore = useAppStore();
const rail = ref(appStore.sidebar.collapse);
const open = computed({
get() {
return appStore.getSubmenu();
},
set(value: string[]) {
appStore.sidebar.submenu.mihoyo = value.includes("mihoyo");
appStore.sidebar.submenu.database = value.includes("database");
},
get () {
return appStore.getSubmenu();
},
set (value: string[]) {
appStore.sidebar.submenu.mihoyo = value.includes("mihoyo");
appStore.sidebar.submenu.database = value.includes("database");
},
});
function collapse() {
rail.value = !rail.value;
appStore.sidebar.collapse = rail.value;
function collapse () {
rail.value = !rail.value;
appStore.sidebar.collapse = rail.value;
}
</script>
<style lang="css" scoped>
.sideList {
font-family: "Genshin-Light", serif;
.side-list {
font-family: Genshin-Light, serif;
}
.sideIcon {
.side-icon {
width: 24px;
height: 24px;
margin-right: 32px;

View File

@@ -8,8 +8,6 @@
/**
* @description 定义一个 Map<T> 接口
* @since Alpha v0.1.2
* @description 该接口的方法实现在 TGMap<T> 中
* @see TGMap
* @interface Map
* @template T
* @returns {Map<T>}

View File

@@ -1,100 +1,102 @@
<template>
<!-- 顶部操作栏 -->
<v-app-bar style="background: rgba(0, 0, 0, 0.5); color: #f4d8a8; font-family: Genshin, serif">
<template v-slot:prepend>
<span style="font-size: 30px">{{ title }}</span>
</template>
<v-spacer></v-spacer>
<v-text-field
v-model="search"
append-icon="mdi-magnify"
label="搜索"
hide-details
@click:append="searchCard"
@keyup.enter="searchCard"
/>
<template v-slot:append>
<v-btn @click="importJson" prepend-icon="mdi-import" class="ms-2 top-btn">导入</v-btn>
<v-btn @click="exportJson" prepend-icon="mdi-export" class="ms-2 top-btn"> 导出 </v-btn>
</template>
</v-app-bar>
<div v-show="loading">
<t-loading :title="loadingTitle" />
</div>
<div v-show="!loading" class="wrap">
<!-- 左侧菜单 -->
<div class="left-wrap">
<v-list class="card-left" v-for="(series, index) in seriesList" @click="selectSeries(index)">
<div class="version-icon-series">v{{ series.version }}</div>
<v-list-item>
<template v-slot:prepend>
<v-img width="40px" style="margin-right: 10px" :src="series.icon" />
</template>
<v-list-item-title>
{{ series.name }}
</v-list-item-title>
<v-list-item-subtitle>
{{ series.completed_count }} / {{ series.total_count }}
</v-list-item-subtitle>
</v-list-item>
</v-list>
</div>
<!-- 右侧内容-->
<div class="right-wrap">
<v-list
v-show="selectedIndex !== -1 && selectedSeries !== 0 && selectedSeries !== 17"
@click="openImg()"
:style="{
backgroundImage: 'url(' + getCardInfo.bg || null + ')',
backgroundPosition: 'right',
backgroundSize: 'auto 100%',
backgroundRepeat: 'no-repeat',
margin: '10px',
borderRadius: '10px 50px 50px 10px',
color: '#485466',
fontFamily: 'Genshin,serif',
cursor: 'pointer',
}"
>
<v-list-item :title="getCardInfo.name" :subtitle="getCardInfo.description">
<template v-slot:prepend>
<v-img width="80px" style="margin-right: 10px" :src="getCardInfo.icon" />
</template>
</v-list-item>
</v-list>
<v-list class="card-right" v-for="achievement in selectedAchievement" :key="achievement.id">
<v-list-item>
<template v-slot:prepend>
<v-icon :color="achievement.completed ? '#fec90b' : '#485466'">
<!-- todo 图标替换 -->
{{ achievement.completed ? "mdi-check-circle" : "mdi-circle" }}
</v-icon>
</template>
<v-list-item-title>
{{ achievement.name }}
{{ achievement.progress !== 0 ? "| " + achievement.progress : null }}
<span class="version-icon-single">v{{ achievement.version }}</span>
</v-list-item-title>
<v-list-item-subtitle>{{ achievement.description }}</v-list-item-subtitle>
<template v-slot:append>
<span v-show="achievement.completed" class="right-time">{{
achievement.completed_time
}}</span>
<v-card class="reward-card" @click="showMaterial('/source/material/原石.webp')">
<v-img src="/source/material/原石.webp" sizes="32" />
<div class="reward-num">
<span>{{ achievement.reward }}</span>
</div>
</v-card>
</template>
</v-list-item>
</v-list>
</div>
<!-- 弹窗提示 -->
<v-snackbar v-model="snackbar" timeout="1500" color="#F5810A" top>
{{ snackbarText }}
</v-snackbar>
</div>
<!-- 顶部操作栏 -->
<v-app-bar style="background: rgb(0 0 0 / 50%); color: #f4d8a8; font-family: Genshin, serif">
<template #prepend>
<span style="font-size: 30px">{{ title }}</span>
</template>
<v-spacer />
<v-text-field
v-model="search"
append-icon="mdi-magnify"
label="搜索"
hide-details
@click:append="searchCard"
@keyup.enter="searchCard"
/>
<template #append>
<v-btn prepend-icon="mdi-import" class="ms-2 top-btn" @click="importJson">
导入
</v-btn>
<v-btn prepend-icon="mdi-export" class="ms-2 top-btn" @click="exportJson">
导出
</v-btn>
</template>
</v-app-bar>
<div v-show="loading">
<TLoading :title="loadingTitle" />
</div>
<div v-show="!loading" class="wrap">
<!-- 左侧菜单 -->
<div class="left-wrap">
<v-list v-for="(series, index) in seriesList" :key="series.id" class="card-left" @click="selectSeries(index)">
<div class="version-icon-series">
v{{ series.version }}
</div>
<v-list-item>
<template #prepend>
<v-img width="40px" style="margin-right: 10px" :src="series.icon" />
</template>
<v-list-item-title>
{{ series.name }}
</v-list-item-title>
<v-list-item-subtitle> {{ series.completed_count }} / {{ series.total_count }} </v-list-item-subtitle>
</v-list-item>
</v-list>
</div>
<!-- 右侧内容-->
<div class="right-wrap">
<v-list
v-show="selectedIndex !== -1 && selectedSeries !== 0 && selectedSeries !== 17"
:style="{
backgroundImage: 'url(' + getCardInfo.bg || null + ')',
backgroundPosition: 'right',
backgroundSize: 'auto 100%',
backgroundRepeat: 'no-repeat',
margin: '10px',
borderRadius: '10px 50px 50px 10px',
color: '#485466',
fontFamily: 'Genshin,serif',
cursor: 'pointer',
}"
@click="openImg()"
>
<v-list-item :title="getCardInfo.name" :subtitle="getCardInfo.description">
<template #prepend>
<v-img width="80px" style="margin-right: 10px" :src="getCardInfo.icon" />
</template>
</v-list-item>
</v-list>
<v-list v-for="achievement in selectedAchievement" :key="achievement.id" class="card-right">
<v-list-item>
<template #prepend>
<v-icon :color="achievement.completed ? '#fec90b' : '#485466'">
<!-- todo 图标替换 -->
{{ achievement.completed ? "mdi-check-circle" : "mdi-circle" }}
</v-icon>
</template>
<v-list-item-title>
{{ achievement.name }}
{{ achievement.progress !== 0 ? "| " + achievement.progress : null }}
<span class="version-icon-single">v{{ achievement.version }}</span>
</v-list-item-title>
<v-list-item-subtitle>{{ achievement.description }}</v-list-item-subtitle>
<template #append>
<span v-show="achievement.completed" class="right-time">{{ achievement.completed_time }}</span>
<v-card class="reward-card" @click="showMaterial('/source/material/原石.webp')">
<v-img src="/source/material/原石.webp" sizes="32" />
<div class="reward-num">
<span>{{ achievement.reward }}</span>
</div>
</v-card>
</template>
</v-list-item>
</v-list>
</div>
<!-- 弹窗提示 -->
<v-snackbar v-model="snackbar" timeout="1500" color="#F5810A" top>
{{ snackbarText }}
</v-snackbar>
</div>
</template>
<script lang="ts" setup>
@@ -106,22 +108,14 @@ import { dialog, fs } from "@tauri-apps/api";
// Store
import useAchievementsStore from "../store/modules/achievements";
// Interface
import { Achievements, UIAF_Info, UIAF_Achievement } from "../plugins/UIAF/interface/UIAF";
import {
Achievement as TGAchievement,
AchievementSeries as TGSeries,
} from "../interface/Achievements";
import { Achievements, UiafHeader, UiafAchievement } from "../plugins/UIAF/interface/UIAF";
import { Achievement as TGAchievement, AchievementSeries as TGSeries } from "../interface/Achievements";
import { NameCard } from "../interface/NameCard";
// Plugins
import UIAF_Oper from "../plugins/UIAF";
import UiafOper from "../plugins/UIAF";
// Utils
import { createTGWindow } from "../utils/TGWindow";
import {
ReadAllTGData,
ReadTGDataByIndex,
ReadTGDataByKey,
UpdateTGDataByKey,
} from "../utils/TGIndex";
import { ReadAllTGData, ReadTGDataByIndex, ReadTGDataByKey, UpdateTGDataByKey } from "../utils/TGIndex";
// Store
const achievementsStore = useAchievementsStore();
@@ -146,241 +140,238 @@ const snackbar = ref(false as boolean);
const snackbarText = ref("" as string);
onMounted(async () => {
await loadData();
await loadData();
});
// 加载数据,数据源:合并后的本地数据
async function loadData() {
loadingTitle.value = "正在获取成就系列数据";
const seriesDB: TGSeries[] = await ReadAllTGData("AchievementSeries");
loadingTitle.value = "正在获取成就系列名片数据";
CardsInfo.value = await ReadTGDataByIndex("NameCard", "type", 1);
loadingTitle.value = "对成就系列数据进行排序";
seriesList.value = seriesDB.sort((a, b) => a.order - b.order);
loadingTitle.value = "正在获取成就数据";
const getAchievements = await ReadAllTGData("Achievements");
loadingTitle.value = "正在对成就数据进行排序";
getAchievements.sort((a, b) => {
if (a.completed === b.completed) {
return a.id - b.id;
} else {
return a.completed ? 1 : -1;
}
});
loadingTitle.value = "正在渲染成就数据";
selectedAchievement.value = getAchievements;
title.value = achievementsStore.title;
loading.value = false;
async function loadData () {
loadingTitle.value = "正在获取成就系列数据";
const seriesDB: TGSeries[] = await ReadAllTGData("AchievementSeries");
loadingTitle.value = "正在获取成就系列名片数据";
CardsInfo.value = await ReadTGDataByIndex("NameCard", "type", 1);
loadingTitle.value = "对成就系列数据进行排序";
seriesList.value = seriesDB.sort((a, b) => a.order - b.order);
loadingTitle.value = "正在获取成就数据";
const getAchievements = await ReadAllTGData("Achievements");
loadingTitle.value = "正在对成就数据进行排序";
getAchievements.sort((a, b) => {
if (a.completed === b.completed) {
return a.id - b.id;
} else {
return a.completed ? 1 : -1;
}
});
loadingTitle.value = "正在渲染成就数据";
selectedAchievement.value = getAchievements;
title.value = achievementsStore.title;
loading.value = false;
}
// 渲染选中的成就系列
async function selectSeries(index: number) {
// 如果选中的是已经选中的系列,则不进行操作
if (selectedIndex.value === index) {
snackbarText.value = "已经选中该系列";
snackbar.value = true;
return;
}
loading.value = true;
loadingTitle.value = "正在获取对应的成就数据";
const getAchievements = await ReadTGDataByIndex(
"Achievements",
"series",
seriesList.value[index].id
);
selectedIndex.value = index;
selectedSeries.value = seriesList.value[index].id;
loadingTitle.value = "正在查找对应的成就名片";
let getCard: NameCard;
if (selectedSeries.value !== 0 && selectedSeries.value !== 17) {
getCard = CardsInfo.value.find(card => card.name === seriesList.value[index].card)!;
} else {
getCard = {} as NameCard;
}
loadingTitle.value = "正在对成就数据进行排序";
getAchievements.sort((a, b) => {
if (a.completed === b.completed) {
return a.id - b.id;
} else {
return a.completed ? 1 : -1;
}
});
loadingTitle.value = "正在渲染成就数据";
selectedAchievement.value = getAchievements;
getCardInfo.value = getCard;
loading.value = false;
async function selectSeries (index: number) {
// 如果选中的是已经选中的系列,则不进行操作
if (selectedIndex.value === index) {
snackbarText.value = "已经选中该系列";
snackbar.value = true;
return;
}
loading.value = true;
loadingTitle.value = "正在获取对应的成就数据";
const getAchievements = await ReadTGDataByIndex("Achievements", "series", seriesList.value[index].id);
selectedIndex.value = index;
selectedSeries.value = seriesList.value[index].id;
loadingTitle.value = "正在查找对应的成就名片";
let getCard: NameCard;
if (selectedSeries.value !== 0 && selectedSeries.value !== 17) {
getCard = CardsInfo.value.find((card) => card.name === seriesList.value[index].card)!;
} else {
getCard = {} as NameCard;
}
loadingTitle.value = "正在对成就数据进行排序";
getAchievements.sort((a, b) => {
if (a.completed === b.completed) {
return a.id - b.id;
} else {
return a.completed ? 1 : -1;
}
});
loadingTitle.value = "正在渲染成就数据";
selectedAchievement.value = getAchievements;
getCardInfo.value = getCard;
loading.value = false;
}
// 打开图片
function openImg() {
createTGWindow(getCardInfo.value.profile, "nameCard", getCardInfo.value.name, 840, 400, false);
function openImg () {
createTGWindow(getCardInfo.value.profile, "nameCard", getCardInfo.value.name, 840, 400, false);
}
function showMaterial(path: string) {
createTGWindow(path, "material", "原石", 256, 256, false);
function showMaterial (path: string) {
createTGWindow(path, "material", "原石", 256, 256, false);
}
async function searchCard() {
if (search.value === "") {
snackbarText.value = "请输入搜索内容";
snackbar.value = true;
return;
}
loadingTitle.value = "正在搜索";
loading.value = true;
const res: TGAchievement[] = [];
const allAchievements = await ReadAllTGData("Achievements");
allAchievements.map(achievement => {
if (achievement.name.includes(search.value) || achievement.description.includes(search.value)) {
res.push(achievement);
}
});
selectedIndex.value = -1;
setTimeout(() => {
loading.value = false;
}, 500);
if (res.length === 0) {
snackbarText.value = "没有找到对应的成就";
snackbar.value = true;
selectedAchievement.value = allAchievements;
} else {
res.sort((a, b) => {
if (a.completed === b.completed) {
return a.id - b.id;
} else {
return a.completed ? 1 : -1;
}
});
selectedAchievement.value = res;
}
async function searchCard () {
if (search.value === "") {
snackbarText.value = "请输入搜索内容";
snackbar.value = true;
return;
}
loadingTitle.value = "正在搜索";
loading.value = true;
const res: TGAchievement[] = [];
const allAchievements = await ReadAllTGData("Achievements");
allAchievements.map((achievement) => {
if (achievement.name.includes(search.value) || achievement.description.includes(search.value)) {
return res.push(achievement);
}
return null;
});
selectedIndex.value = -1;
setTimeout(() => {
loading.value = false;
}, 500);
if (res.length === 0) {
snackbarText.value = "没有找到对应的成就";
snackbar.value = true;
selectedAchievement.value = allAchievements;
} else {
res.sort((a, b) => {
if (a.completed === b.completed) {
return a.id - b.id;
} else {
return a.completed ? 1 : -1;
}
});
selectedAchievement.value = res;
}
}
// 导入 UIAF 数据,进行数据合并、刷新
async function importJson() {
const selectedFile = await dialog.open({
multiple: false,
filters: [
{
name: "JSON",
extensions: ["json"],
},
],
});
if (selectedFile && (await UIAF_Oper.checkUIAFData(<string>selectedFile))) {
const remoteRaw: string | false = await UIAF_Oper.readUIAFData(<string>selectedFile);
if (remoteRaw === false) {
snackbarText.value = "读取 UIAF 数据失败,请检查文件是否符合规范";
snackbar.value = true;
return;
}
loadingTitle.value = "正在解析数据";
loading.value = true;
let remoteData: Achievements = JSON.parse(remoteRaw);
loadingTitle.value = "正在合并成就数据";
await Promise.allSettled(
remoteData.list.map(async data => {
const id = data.id;
let localData: TGAchievement = (await ReadTGDataByKey("Achievements", [id]))[0];
// 获取 timeStamp 2023-03-15 00:00:00
const localTime = localData.completed_time;
// 如果本地数据不存在,或者本地数据的 timeStamp 小于远程数据的 timeStamp更新数据
if (data.timestamp !== 0) {
const fin_time = new Date(data.timestamp * 1000).toLocaleString("zh", {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
});
if (fin_time !== localTime || localData.progress !== data.current) {
localData.completed_time = fin_time;
localData.progress = data.current;
localData.completed = true;
// 更新数据
await UpdateTGDataByKey("Achievements", localData);
}
} else {
if (localData.progress !== data.current) {
localData.completed_time = "";
localData.progress = data.current;
localData.completed = false;
// 更新数据
await UpdateTGDataByKey("Achievements", localData);
}
}
})
);
loadingTitle.value = "正在更新成就系列数据";
let seriesDB = await ReadAllTGData("AchievementSeries");
await Promise.allSettled(
seriesDB.map(async data => {
const seriesId = data.id;
const achievementsDB = await ReadTGDataByIndex("Achievements", "series", seriesId);
data.completed_count = achievementsDB.filter(data => {
return data.completed === true;
}).length;
await UpdateTGDataByKey("AchievementSeries", data);
})
);
loadingTitle.value = "正在刷新数据";
seriesDB = await ReadAllTGData("AchievementSeries");
const fin_achievements = seriesDB.reduce((a, b) => {
return a + b.completed_count;
}, 0);
const total_achievements = seriesDB.reduce((a, b) => {
return a + b.total_count;
}, 0);
achievementsStore.flushData(total_achievements, fin_achievements);
// 刷新数据
await loadData();
}
async function importJson () {
const selectedFile = await dialog.open({
multiple: false,
filters: [
{
name: "JSON",
extensions: ["json"],
},
],
});
if (selectedFile && (await UiafOper.checkUiafData(<string>selectedFile))) {
const remoteRaw: string | false = await UiafOper.readUiafData(<string>selectedFile);
if (remoteRaw === false) {
snackbarText.value = "读取 UIAF 数据失败,请检查文件是否符合规范";
snackbar.value = true;
return;
}
loadingTitle.value = "正在解析数据";
loading.value = true;
const remoteData: Achievements = JSON.parse(remoteRaw);
loadingTitle.value = "正在合并成就数据";
await Promise.allSettled(
remoteData.list.map(async (data) => {
const id = data.id;
const localData: TGAchievement = (await ReadTGDataByKey("Achievements", [id]))[0];
// 获取 timeStamp 2023-03-15 00:00:00
const localTime = localData.completed_time;
// 如果本地数据不存在,或者本地数据的 timeStamp 小于远程数据的 timeStamp更新数据
if (data.timestamp !== 0) {
const fin_time = new Date(data.timestamp * 1000).toLocaleString("zh", {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
});
if (fin_time !== localTime || localData.progress !== data.current) {
localData.completed_time = fin_time;
localData.progress = data.current;
localData.completed = true;
// 更新数据
await UpdateTGDataByKey("Achievements", localData);
}
} else {
if (localData.progress !== data.current) {
localData.completed_time = "";
localData.progress = data.current;
localData.completed = false;
// 更新数据
await UpdateTGDataByKey("Achievements", localData);
}
}
}),
);
loadingTitle.value = "正在更新成就系列数据";
let seriesDB = await ReadAllTGData("AchievementSeries");
await Promise.allSettled(
seriesDB.map(async (data) => {
const seriesId = data.id;
const achievementsDB = await ReadTGDataByIndex("Achievements", "series", seriesId);
data.completed_count = achievementsDB.filter((data) => {
return data.completed === true;
}).length;
await UpdateTGDataByKey("AchievementSeries", data);
}),
);
loadingTitle.value = "正在刷新数据";
seriesDB = await ReadAllTGData("AchievementSeries");
const fin_achievements = seriesDB.reduce((a, b) => {
return a + b.completed_count;
}, 0);
const total_achievements = seriesDB.reduce((a, b) => {
return a + b.total_count;
}, 0);
achievementsStore.flushData(total_achievements, fin_achievements);
// 刷新数据
await loadData();
}
}
// 导出
async function exportJson() {
// 判断是否有数据
if (achievementsStore.fin_achievements === 0) {
snackbarText.value = "没有可导出的数据";
snackbar.value = true;
return;
}
// 获取本地数据
const achievements = (await ReadAllTGData("Achievements")).filter(data => {
return data.progress !== 0 || data.completed === true;
});
let UIAF_DATA = {
info: {} as UIAF_Info,
list: [] as UIAF_Achievement[],
};
// 转换数据
UIAF_DATA.list = achievements.map(data => {
let status;
// 计算点数但是没有完成
if (data.progress !== 0 && data.completed === false) {
status = 1;
// 已完成且未计算点数
} else if (data.progress === 0 && data.completed === true) {
status = 2;
// 已完成且已计算点数
} else if (data.progress !== 0 && data.completed === true) {
status = 3;
} else {
status = 0;
}
return {
id: data.id,
timestamp: data.completed ? Math.round(new Date(data.completed_time).getTime() / 1000) : 0,
current: data.progress,
status: status,
};
});
UIAF_DATA.info = await UIAF_Oper.getUIAFInfo();
const is_save = await dialog.save({
filters: [
{
name: "achievements",
extensions: ["json"],
},
],
});
if (is_save) {
await fs.writeTextFile(is_save, JSON.stringify(UIAF_DATA));
}
async function exportJson () {
// 判断是否有数据
if (achievementsStore.fin_achievements === 0) {
snackbarText.value = "没有可导出的数据";
snackbar.value = true;
return;
}
// 获取本地数据
const achievements = (await ReadAllTGData("Achievements")).filter((data) => {
return data.progress !== 0 || data.completed === true;
});
const UIAF_DATA = {
info: {} as UiafHeader,
list: [] as UiafAchievement[],
};
// 转换数据
UIAF_DATA.list = achievements.map((data) => {
let status;
// 计算点数但是没有完成
if (data.progress !== 0 && data.completed === false) {
status = 1;
// 已完成且未计算点数
} else if (data.progress === 0 && data.completed === true) {
status = 2;
// 已完成且已计算点数
} else if (data.progress !== 0 && data.completed === true) {
status = 3;
} else {
status = 0;
}
return {
id: data.id,
timestamp: data.completed ? Math.round(new Date(data.completed_time).getTime() / 1000) : 0,
current: data.progress,
status,
};
});
UIAF_DATA.info = await UiafOper.getUiafInfo();
const is_save = await dialog.save({
filters: [
{
name: "achievements",
extensions: ["json"],
},
],
});
if (is_save) {
await fs.writeTextFile(is_save, JSON.stringify(UIAF_DATA));
}
}
</script>
@@ -397,8 +388,9 @@ async function exportJson() {
flex-direction: row;
overflow: auto;
max-height: 90vh;
font-family: Genshin-Light, "serif";
font-family: Genshin-Light, serif;
}
/* 左侧系列 */
.left-wrap {
float: left;
@@ -406,6 +398,7 @@ async function exportJson() {
max-height: calc(100vh - 100px);
overflow: auto;
}
/* 右侧成就 */
.right-wrap {
float: right;
@@ -413,6 +406,7 @@ async function exportJson() {
max-height: calc(100vh - 100px);
overflow: auto;
}
/* 版本信息 */
.version-icon-series {
font-family: Genshin, serif;
@@ -422,9 +416,9 @@ async function exportJson() {
text-align: center;
width: 80px;
background: #546d8b;
border-radius: 10px 0 0 0;
border-top: #ffffff 2px solid;
border-left: #ffffff 2px solid;
border-radius: 10px 0 0;
border-top: #fff 2px solid;
border-left: #fff 2px solid;
color: #fec90b;
font-size: 10px;
}
@@ -475,7 +469,7 @@ async function exportJson() {
left: 0;
width: 100%;
height: 10px;
background: rgba(0, 0, 0, 0.5);
background: rgb(0 0 0 / 50%);
color: #faf7e8;
display: flex;
font-size: 8px;

View File

@@ -1,86 +1,90 @@
<template>
<div v-if="loading">
<t-loading :title="loadingTitle" />
</div>
<div v-else>
<v-tabs v-model="tab" align-tabs="start" class="global-font mb-2">
<v-tab value="activity" title="活动公告" />
<v-tab value="game" title="游戏公告" />
<v-spacer></v-spacer>
<v-btn class="switch-btn" @click="switchNews">
<template v-slot:prepend>
<v-icon>mdi-bullhorn</v-icon>
</template>
切换米游社咨讯
</v-btn>
</v-tabs>
<v-window v-model="tab">
<v-window-item value="activity">
<div class="anno-grid">
<v-card class="anno-card" v-for="item in annoCards.activity" width="340">
<div class="anno-cover" @click="toPost(item)">
<img :src="item.banner" alt="cover" />
</div>
<v-card-title>
{{ item.title }}
</v-card-title>
<v-card-subtitle>{{ item.subtitle }}</v-card-subtitle>
<v-card-actions>
<v-btn @click="toPost(item)" class="anno-btn">
<template v-slot:prepend>
<img :src="item.tag_icon || '../assets/icons/arrow-right.svg'" alt="right" />
</template>
查看
</v-btn>
<v-card-subtitle v-show="!appStore.devMode">
<v-icon>mdi-calendar</v-icon>
{{ item.start_time.split(" ")[0] }} -
{{ item.end_time.split(" ")[0] }}</v-card-subtitle
>
<v-card-subtitle v-show="appStore.devMode">id: {{ item.id }}</v-card-subtitle>
<v-btn v-show="appStore.devMode" class="card-dev-btn" @click="toJson(item)">
<template v-slot:prepend>
<img src="../assets/icons/arrow-right.svg" alt="right" />
</template>
查看数据
</v-btn>
</v-card-actions>
</v-card>
</div>
</v-window-item>
<v-window-item value="game">
<div class="anno-grid">
<v-card class="anno-card" v-for="item in annoCards.game" width="340">
<div class="anno-cover" @click="toPost(item)">
<img :src="item.banner" alt="cover" />
</div>
<v-card-title>{{ item.title }}</v-card-title>
<v-card-subtitle>{{ item.subtitle }}</v-card-subtitle>
<v-card-actions>
<v-btn @click="toPost(item)" class="anno-btn">
<template v-slot:prepend>
<img :src="item.tag_icon || '../assets/icons/arrow-right.svg'" alt="right" />
</template>
查看
</v-btn>
<v-card-subtitle v-show="!appStore.devMode">
<v-icon>mdi-calendar</v-icon>
{{ item.start_time.split(" ")[0] }} -
{{ item.end_time.split(" ")[0] }}</v-card-subtitle
>
<v-card-subtitle v-show="appStore.devMode">id: {{ item.id }}</v-card-subtitle>
<v-btn v-show="appStore.devMode" class="card-dev-btn" @click="toJson(item)">
<template v-slot:prepend>
<img src="../assets/icons/arrow-right.svg" alt="right" />
</template>
查看数据
</v-btn>
</v-card-actions>
</v-card>
</div>
</v-window-item>
</v-window>
</div>
<div v-if="loading">
<TLoading :title="loadingTitle" />
</div>
<div v-else>
<v-tabs v-model="tab" align-tabs="start" class="global-font mb-2">
<v-tab value="activity" title="活动公告" />
<v-tab value="game" title="游戏公告" />
<v-spacer />
<v-btn class="switch-btn" @click="switchNews">
<template #prepend>
<v-icon>mdi-bullhorn</v-icon>
</template>
切换米游社咨讯
</v-btn>
</v-tabs>
<v-window v-model="tab">
<v-window-item value="activity">
<div class="anno-grid">
<v-card v-for="item in annoCards.activity" :key="item.id" class="anno-card" width="340">
<div class="anno-cover" @click="toPost(item)">
<img :src="item.banner" alt="cover">
</div>
<v-card-title>
{{ item.title }}
</v-card-title>
<v-card-subtitle>{{ item.subtitle }}</v-card-subtitle>
<v-card-actions>
<v-btn class="anno-btn" @click="toPost(item)">
<template #prepend>
<img :src="item.tag_icon || '../assets/icons/arrow-right.svg'" alt="right">
</template>
查看
</v-btn>
<v-card-subtitle v-show="!appStore.devMode">
<v-icon>mdi-calendar</v-icon>
{{ item.start_time.split(" ")[0] }} -
{{ item.end_time.split(" ")[0] }}
</v-card-subtitle>
<v-card-subtitle v-show="appStore.devMode">
id: {{ item.id }}
</v-card-subtitle>
<v-btn v-show="appStore.devMode" class="card-dev-btn" @click="toJson(item)">
<template #prepend>
<img src="../assets/icons/arrow-right.svg" alt="right">
</template>
查看数据
</v-btn>
</v-card-actions>
</v-card>
</div>
</v-window-item>
<v-window-item value="game">
<div class="anno-grid">
<v-card v-for="item in annoCards.game" :key="item.id" class="anno-card" width="340">
<div class="anno-cover" @click="toPost(item)">
<img :src="item.banner" alt="cover">
</div>
<v-card-title>{{ item.title }}</v-card-title>
<v-card-subtitle>{{ item.subtitle }}</v-card-subtitle>
<v-card-actions>
<v-btn class="anno-btn" @click="toPost(item)">
<template #prepend>
<img :src="item.tag_icon || '../assets/icons/arrow-right.svg'" alt="right">
</template>
查看
</v-btn>
<v-card-subtitle v-show="!appStore.devMode">
<v-icon>mdi-calendar</v-icon>
{{ item.start_time.split(" ")[0] }} -
{{ item.end_time.split(" ")[0] }}
</v-card-subtitle>
<v-card-subtitle v-show="appStore.devMode">
id: {{ item.id }}
</v-card-subtitle>
<v-btn v-show="appStore.devMode" class="card-dev-btn" @click="toJson(item)">
<template #prepend>
<img src="../assets/icons/arrow-right.svg" alt="right">
</template>
查看数据
</v-btn>
</v-card-actions>
</v-card>
</div>
</v-window-item>
</v-window>
</div>
</template>
<script lang="ts" setup>
@@ -108,48 +112,48 @@ const router = useRouter();
// 数据
const tab = ref("");
const annoCards = ref({
activity: [] as AnnoListCard[],
game: [] as AnnoListCard[],
activity: [] as AnnoListCard[],
game: [] as AnnoListCard[],
});
const annoData = ref({} as AnnoListData);
onMounted(async () => {
loadingTitle.value = "正在获取公告数据";
annoData.value = await GenshinOper.Announcement.getList();
loadingTitle.value = "正在转换公告数据";
const listCards = GenshinOper.Announcement.card(annoData.value);
const activityCard = listCards.filter(item => item.type_label === "活动公告");
const newsCard = listCards.filter(item => item.type_label === "游戏公告");
annoCards.value = {
activity: activityCard,
game: newsCard,
};
tab.value = "activity";
loading.value = false;
loadingTitle.value = "正在获取公告数据";
annoData.value = await GenshinOper.Announcement.getList();
loadingTitle.value = "正在转换公告数据";
const listCards = GenshinOper.Announcement.card(annoData.value);
const activityCard = listCards.filter((item) => item.type_label === "活动公告");
const newsCard = listCards.filter((item) => item.type_label === "游戏公告");
annoCards.value = {
activity: activityCard,
game: newsCard,
};
tab.value = "activity";
loading.value = false;
});
function switchNews() {
router.push("/news");
async function switchNews () {
await router.push("/news");
}
async function toPost(item: AnnoListCard) {
const path = router.resolve({
name: "游戏内公告",
params: {
anno_id: item.id,
},
}).href;
createTGWindow(path, "游戏内公告", item.title, 960, 720, false);
async function toPost (item: AnnoListCard) {
const path = router.resolve({
name: "游戏内公告",
params: {
anno_id: item.id,
},
}).href;
createTGWindow(path, "游戏内公告", item.title, 960, 720, false);
}
async function toJson(item: AnnoListCard) {
const path = router.resolve({
name: "游戏内公告JSON",
params: {
anno_id: item.id,
},
}).href;
createTGWindow(path, "游戏内公告-JSON", item.title, 960, 720, false);
async function toJson (item: AnnoListCard) {
const path = router.resolve({
name: "游戏内公告JSON",
params: {
anno_id: item.id,
},
}).href;
createTGWindow(path, "游戏内公告-JSON", item.title, 960, 720, false);
}
</script>

View File

@@ -1,122 +1,127 @@
<template>
<div v-if="loading">
<t-loading />
</div>
<div v-else>
<v-list class="config-list">
<v-list-subheader inset class="config-header">应用信息</v-list-subheader>
<v-divider inset class="border-opacity-75" />
<v-list-item title="Tauri 版本" @click="toOuter('https://next--tauri.netlify.app/')">
<template v-slot:prepend>
<img class="config-icon" src="/platforms/tauri.webp" alt="Tauri" />
</template>
<template v-slot:append>
<v-list-item-subtitle>{{ versionTauri }}</v-list-item-subtitle>
</template>
</v-list-item>
<v-list-item>
<template v-slot:prepend>
<img class="config-icon" src="/icon.webp" alt="App" />
</template>
<v-list-item-title>
应用版本
<v-btn
class="card-btn"
size="small"
@click="toOuter('https://github.com/BTMuli/Tauri.Genshin/releases/latest')"
>Alpha</v-btn
>
</v-list-item-title>
<template v-slot:append>
<v-list-item-subtitle>{{ versionApp }}</v-list-item-subtitle>
</template>
</v-list-item>
<v-list-item title="成就版本">
<template v-slot:prepend>
<img class="config-icon" src="../assets/icons/achievements.svg" alt="Achievements" />
</template>
<template v-slot:append>
<v-list-item-subtitle>{{ achievementsStore.last_version }}</v-list-item-subtitle>
</template>
</v-list-item>
<v-list-subheader inset class="config-header">系统信息</v-list-subheader>
<v-divider inset class="border-opacity-75" />
<v-list-item title="系统平台">
<template v-slot:prepend>
<v-icon>mdi-desktop-classic</v-icon>
</template>
<template v-slot:append>
<v-list-item-subtitle>{{ osPlatform }}</v-list-item-subtitle>
</template>
</v-list-item>
<v-list-item title="系统版本">
<template v-slot:prepend>
<v-icon>mdi-desktop-classic</v-icon>
</template>
<template v-slot:append>
<v-list-item-subtitle>{{ osVersion }}</v-list-item-subtitle>
</template>
</v-list-item>
<v-list-subheader inset class="config-header">设置</v-list-subheader>
<v-divider inset class="border-opacity-75" />
<v-list-item @click="openMergeData" prepend-icon="mdi-folder" title="打开用户数据目录" />
<v-list-item @click="tryConfirm('delUser')" prepend-icon="mdi-delete" title="清除用户缓存" />
<v-list-item @click="tryConfirm('delTemp')" prepend-icon="mdi-delete" title="清除临时数据" />
<v-list-item @click="tryConfirm('delApp')" prepend-icon="mdi-cog" title="恢复默认设置" />
<v-list-subheader inset class="config-header">调试</v-list-subheader>
<v-divider inset class="border-opacity-75" />
<v-list-item title="开发者模式" subtitle="开启后将显示调试信息">
<template v-slot:prepend>
<v-icon>mdi-bug</v-icon>
</template>
<template v-slot:append>
<v-switch
:label="appStore.devMode ? '开启' : '关闭'"
inset
v-model="appStore.devMode"
color="#FAC51E"
@click="submitDevMode"
/>
</template>
</v-list-item>
<v-list-item>
<template v-slot:prepend>
<v-icon>mdi-view-dashboard</v-icon>
</template>
<v-select
v-model="showHome"
:items="homeStore.getShowItem()"
label="首页显示组件"
multiple
chips
></v-select>
<template v-slot:append>
<v-btn @click="submitHome" class="card-btn">
<template v-slot:prepend>
<img src="../assets/icons/circle-check.svg" alt="check" />
提交
</template>
</v-btn>
</template>
</v-list-item>
<v-list-subheader inset class="config-header">路径</v-list-subheader>
<v-divider inset class="border-opacity-75" />
<v-list-item prepend-icon="mdi-folder">
<v-list-item-title>本地应用数据路径</v-list-item-title>
<v-list-item-subtitle>{{ appStore.dataPath.app }}</v-list-item-subtitle>
</v-list-item>
<v-list-item prepend-icon="mdi-folder">
<v-list-item-title>本地用户数据路径</v-list-item-title>
<v-list-item-subtitle>{{ appStore.dataPath.user }}</v-list-item-subtitle>
</v-list-item>
</v-list>
<!-- 弹窗提示条 -->
<v-snackbar v-model="snackbar" timeout="1500" :color="snackbarColor">
{{ snackbarText }}
</v-snackbar>
<!-- 确认弹窗 -->
<t-confirm :title="confirmText" v-model="confirmShow" @confirm="doConfirm(confirmOper)" />
</div>
<div v-if="loading">
<TLoading />
</div>
<div v-else>
<v-list class="config-list">
<v-list-subheader inset class="config-header">
应用信息
</v-list-subheader>
<v-divider inset class="border-opacity-75" />
<v-list-item title="Tauri 版本" @click="toOuter('https://next--tauri.netlify.app/')">
<template #prepend>
<img class="config-icon" src="/platforms/tauri.webp" alt="Tauri">
</template>
<template #append>
<v-list-item-subtitle>{{ versionTauri }}</v-list-item-subtitle>
</template>
</v-list-item>
<v-list-item>
<template #prepend>
<img class="config-icon" src="/icon.webp" alt="App">
</template>
<v-list-item-title>
应用版本
<v-btn
class="card-btn"
size="small"
@click="toOuter('https://github.com/BTMuli/Tauri.Genshin/releases/latest')"
>
Alpha
</v-btn>
</v-list-item-title>
<template #append>
<v-list-item-subtitle>{{ versionApp }}</v-list-item-subtitle>
</template>
</v-list-item>
<v-list-item title="成就版本">
<template #prepend>
<img class="config-icon" src="../assets/icons/achievements.svg" alt="Achievements">
</template>
<template #append>
<v-list-item-subtitle>{{ achievementsStore.last_version }}</v-list-item-subtitle>
</template>
</v-list-item>
<v-list-subheader inset class="config-header">
系统信息
</v-list-subheader>
<v-divider inset class="border-opacity-75" />
<v-list-item title="系统平台">
<template #prepend>
<v-icon>mdi-desktop-classic</v-icon>
</template>
<template #append>
<v-list-item-subtitle>{{ osPlatform }}</v-list-item-subtitle>
</template>
</v-list-item>
<v-list-item title="系统版本">
<template #prepend>
<v-icon>mdi-desktop-classic</v-icon>
</template>
<template #append>
<v-list-item-subtitle>{{ osVersion }}</v-list-item-subtitle>
</template>
</v-list-item>
<v-list-subheader inset class="config-header">
设置
</v-list-subheader>
<v-divider inset class="border-opacity-75" />
<v-list-item prepend-icon="mdi-folder" title="打开用户数据目录" @click="openMergeData" />
<v-list-item prepend-icon="mdi-delete" title="清除用户缓存" @click="tryConfirm('delUser')" />
<v-list-item prepend-icon="mdi-delete" title="清除临时数据" @click="tryConfirm('delTemp')" />
<v-list-item prepend-icon="mdi-cog" title="恢复默认设置" @click="tryConfirm('delApp')" />
<v-list-subheader inset class="config-header">
调试
</v-list-subheader>
<v-divider inset class="border-opacity-75" />
<v-list-item title="开发者模式" subtitle="开启后将显示调试信息">
<template #prepend>
<v-icon>mdi-bug</v-icon>
</template>
<template #append>
<v-switch
v-model="appStore.devMode"
:label="appStore.devMode ? '开启' : '关闭'"
inset
color="#FAC51E"
@click="submitDevMode"
/>
</template>
</v-list-item>
<v-list-item>
<template #prepend>
<v-icon>mdi-view-dashboard</v-icon>
</template>
<v-select v-model="showHome" :items="homeStore.getShowItem()" label="首页显示组件" multiple chips />
<template #append>
<v-btn class="card-btn" @click="submitHome">
<template #prepend>
<img src="../assets/icons/circle-check.svg" alt="check">
提交
</template>
</v-btn>
</template>
</v-list-item>
<v-list-subheader inset class="config-header">
路径
</v-list-subheader>
<v-divider inset class="border-opacity-75" />
<v-list-item prepend-icon="mdi-folder">
<v-list-item-title>本地应用数据路径</v-list-item-title>
<v-list-item-subtitle>{{ appStore.dataPath.app }}</v-list-item-subtitle>
</v-list-item>
<v-list-item prepend-icon="mdi-folder">
<v-list-item-title>本地用户数据路径</v-list-item-title>
<v-list-item-subtitle>{{ appStore.dataPath.user }}</v-list-item-subtitle>
</v-list-item>
</v-list>
<!-- 弹窗提示条 -->
<v-snackbar v-model="snackbar" timeout="1500" :color="snackbarColor">
{{ snackbarText }}
</v-snackbar>
<!-- 确认弹窗 -->
<TConfirm v-model="confirmShow" :title="confirmText" @confirm="doConfirm(confirmOper)" />
</div>
</template>
<script lang="ts" setup>
@@ -166,140 +171,138 @@ const confirmShow = ref(false as boolean);
// load version
onMounted(async () => {
versionApp.value = await app.getVersion();
versionTauri.value = await app.getTauriVersion();
osPlatform.value = `${await os.platform()}`;
osVersion.value = await os.version();
setTimeout(() => {
loading.value = false;
}, 1000);
versionApp.value = await app.getVersion();
versionTauri.value = await app.getTauriVersion();
osPlatform.value = `${await os.platform()}`;
osVersion.value = await os.version();
setTimeout(() => {
loading.value = false;
}, 1000);
});
// 打开外部链接
function toOuter(url: string) {
window.open(url);
function toOuter (url: string) {
window.open(url);
}
// 打开用户数据目录
async function openMergeData() {
await dialog.open({
defaultPath: appStore.dataPath.user,
filters: [],
});
async function openMergeData () {
await dialog.open({
defaultPath: appStore.dataPath.user,
filters: [],
});
}
// open confirm
function tryConfirm(oper: string) {
switch (oper) {
case "delTemp":
confirmText.value = "确认清除临时数据吗?";
confirmOper.value = "delTemp";
confirmShow.value = true;
break;
case "delUser":
confirmText.value = "确认清除用户缓存吗?";
confirmOper.value = "delUser";
confirmShow.value = true;
break;
case "delApp":
confirmText.value = "确认恢复默认设置吗?";
confirmOper.value = "delApp";
confirmShow.value = true;
break;
}
function tryConfirm (oper: string) {
switch (oper) {
case "delTemp":
confirmText.value = "确认清除临时数据吗?";
confirmOper.value = "delTemp";
confirmShow.value = true;
break;
case "delUser":
confirmText.value = "确认清除用户缓存吗?";
confirmOper.value = "delUser";
confirmShow.value = true;
break;
case "delApp":
confirmText.value = "确认恢复默认设置吗?";
confirmOper.value = "delApp";
confirmShow.value = true;
break;
}
}
// transfer confirm oper
function doConfirm(oper: string) {
switch (oper) {
case "delTemp":
delTempData();
break;
case "delUser":
delUserData();
break;
case "delApp":
initAppData();
break;
default:
break;
}
async function doConfirm (oper: string) {
switch (oper) {
case "delTemp":
await delTempData();
break;
case "delUser":
await delUserData();
break;
case "delApp":
await initAppData();
break;
default:
break;
}
}
// confirmOper
async function delTempData() {
await fs.removeDir("tempData", {
dir: fs.BaseDirectory.AppLocalData,
recursive: true,
});
await fs.createDir("tempData", { dir: fs.BaseDirectory.AppLocalData });
snackbarText.value = "临时数据已删除!";
snackbar.value = true;
async function delTempData () {
await fs.removeDir("tempData", {
dir: fs.BaseDirectory.AppLocalData,
recursive: true,
});
await fs.createDir("tempData", { dir: fs.BaseDirectory.AppLocalData });
snackbarText.value = "临时数据已删除!";
snackbar.value = true;
}
async function delUserData() {
await fs.removeDir("userData", {
dir: fs.BaseDirectory.AppLocalData,
recursive: true,
});
await fs.removeDir("tempData", {
dir: fs.BaseDirectory.AppLocalData,
recursive: true,
});
getDataList.map(async item => {
await WriteTGData(item.name, item.data);
});
snackbarText.value = "用户数据已删除!";
snackbar.value = true;
await achievementsStore.init();
await fs.createDir("userData", { dir: fs.BaseDirectory.AppLocalData });
await fs.createDir("tempData", { dir: fs.BaseDirectory.AppLocalData });
async function delUserData () {
await fs.removeDir("userData", {
dir: fs.BaseDirectory.AppLocalData,
recursive: true,
});
await fs.removeDir("tempData", {
dir: fs.BaseDirectory.AppLocalData,
recursive: true,
});
getDataList.map(async (item) => {
await WriteTGData(item.name, item.data);
});
snackbarText.value = "用户数据已删除!";
snackbar.value = true;
await achievementsStore.init();
await fs.createDir("userData", { dir: fs.BaseDirectory.AppLocalData });
await fs.createDir("tempData", { dir: fs.BaseDirectory.AppLocalData });
}
// 恢复默认配置
async function initAppData() {
await appStore.init();
await homeStore.init();
await achievementsStore.init();
snackbarText.value = "已恢复默认配置!";
snackbar.value = true;
setTimeout(() => {
window.location.reload();
}, 1500);
async function initAppData () {
await appStore.init();
await homeStore.init();
await achievementsStore.init();
snackbarText.value = "已恢复默认配置!";
snackbar.value = true;
setTimeout(() => {
window.location.reload();
}, 1500);
}
// 开启 dev 模式
async function submitDevMode() {
await new Promise(resolve => setTimeout(resolve, 200));
appStore.devMode
? (snackbarText.value = "已开启 dev 模式!")
: (snackbarText.value = "已关闭 dev 模式!");
snackbarColor.value = "success";
snackbar.value = true;
async function submitDevMode () {
await new Promise((resolve) => setTimeout(resolve, 200));
appStore.devMode ? (snackbarText.value = "已开启 dev 模式!") : (snackbarText.value = "已关闭 dev 模式!");
snackbarColor.value = "success";
snackbar.value = true;
}
// 修改首页显示
async function submitHome() {
// 获取已选
const show = showHome.value;
if (show.length < 1) {
snackbarText.value = "请至少选择一个!";
snackbarColor.value = "error";
snackbar.value = true;
return;
}
// 设置
await homeStore.setShowValue(show);
snackbarText.value = "已修改!";
snackbarColor.value = "success";
snackbar.value = true;
async function submitHome () {
// 获取已选
const show = showHome.value;
if (show.length < 1) {
snackbarText.value = "请至少选择一个!";
snackbarColor.value = "error";
snackbar.value = true;
return;
}
// 设置
await homeStore.setShowValue(show);
snackbarText.value = "已修改!";
snackbarColor.value = "success";
snackbar.value = true;
}
</script>
<style lang="css" scoped>
.config-list {
margin: 10px;
font-family: "Genshin-Light", serif;
font-family: Genshin-Light, serif;
background: #faf7e8;
color: #546d8b;
border-radius: 10px;

View File

@@ -1,111 +1,93 @@
<template>
<div v-if="loading">
<t-loading title="正在加载卡牌列表" />
</div>
<div v-else>
<v-tabs v-model="tab" align-tabs="start" class="global-font">
<div v-show="!doSearch">
<v-tab value="character" title="角色牌" />
<v-tab value="action" title="行动牌" />
<v-tab value="monster" title="魔物牌" />
</div>
<v-spacer></v-spacer>
<v-text-field
v-model="search"
append-icon="mdi-magnify"
label="搜索"
single-line
hide-details
@click:append="searchCard"
@keyup.enter="searchCard"
></v-text-field>
</v-tabs>
<div v-if="!doSearch">
<v-window v-model="tab">
<v-window-item value="character">
<div class="GCG-grid">
<v-card
v-for="item in CardsInfoC"
:key="item.id"
class="card-cls"
@click="toOuter(item.name, item.id)"
>
<div class="GCG-border">
<img src="/source/GCG/base/bg-normal.webp" alt="border" />
</div>
<div class="GCG-cover">
<img :src="item.icon.normal" alt="cover" />
</div>
<div class="GCG-content">
<span>{{ item.name }}</span>
</div>
</v-card>
</div>
</v-window-item>
<v-window-item value="action">
<div class="GCG-grid">
<v-card
v-for="item in CardsInfoA"
:key="item.id"
class="card-cls"
@click="toOuter(item.name, item.id)"
>
<div class="GCG-border">
<img src="/source/GCG/base/bg-normal.webp" alt="border" />
</div>
<div class="GCG-cover">
<img :src="item.icon.normal" alt="cover" />
</div>
<div class="GCG-content">
<span>{{ item.name }}</span>
</div>
</v-card>
</div>
</v-window-item>
<v-window-item value="monster">
<div class="GCG-grid">
<v-card
v-for="item in CardsInfoM"
:key="item.id"
class="card-cls"
@click="toOuter(item.name, item.id)"
>
<div class="GCG-border">
<img src="/source/GCG/base/bg-normal.webp" alt="border" />
</div>
<div class="GCG-cover">
<img :src="item.icon.normal" alt="cover" />
</div>
<div class="GCG-content">
<span>{{ item.name }}</span>
</div>
</v-card>
</div>
</v-window-item>
</v-window>
</div>
<div v-else>
<div class="GCG-grid">
<div
v-for="item in CardsInfoS"
:key="item.id"
class="card-cls"
@click="toOuter(item.name, item.id)"
>
<div class="GCG-border">
<img src="/source/GCG/base/bg-normal.webp" alt="border" />
</div>
<div class="GCG-cover">
<img :src="item.icon.normal" alt="cover" />
</div>
<div class="GCG-content">
<span>{{ item.name }}</span>
</div>
</div>
</div>
</div>
<v-snackbar v-model="snackbar" timeout="1500" color="error"> 未找到相关卡牌 </v-snackbar>
</div>
<div v-if="loading">
<TLoading title="正在加载卡牌列表" />
</div>
<div v-else>
<v-tabs v-model="tab" align-tabs="start" class="global-font">
<div v-show="!doSearch">
<v-tab value="character" title="角色牌" />
<v-tab value="action" title="行动牌" />
<v-tab value="monster" title="魔物牌" />
</div>
<v-spacer />
<v-text-field
v-model="search"
append-icon="mdi-magnify"
label="搜索"
single-line
hide-details
@click:append="searchCard"
@keyup.enter="searchCard"
/>
</v-tabs>
<div v-if="!doSearch">
<v-window v-model="tab">
<v-window-item value="character">
<div class="cards-grid">
<v-card v-for="item in CardsInfoC" :key="item.id" class="card-cls" @click="toOuter(item.name, item.id)">
<div class="card-border">
<img src="/source/GCG/base/bg-normal.webp" alt="border">
</div>
<div class="card-cover">
<img :src="item.icon.normal" alt="cover">
</div>
<div class="card-content">
<span>{{ item.name }}</span>
</div>
</v-card>
</div>
</v-window-item>
<v-window-item value="action">
<div class="cards-grid">
<v-card v-for="item in CardsInfoA" :key="item.id" class="card-cls" @click="toOuter(item.name, item.id)">
<div class="card-border">
<img src="/source/GCG/base/bg-normal.webp" alt="border">
</div>
<div class="card-cover">
<img :src="item.icon.normal" alt="cover">
</div>
<div class="card-content">
<span>{{ item.name }}</span>
</div>
</v-card>
</div>
</v-window-item>
<v-window-item value="monster">
<div class="cards-grid">
<v-card v-for="item in CardsInfoM" :key="item.id" class="card-cls" @click="toOuter(item.name, item.id)">
<div class="card-border">
<img src="/source/GCG/base/bg-normal.webp" alt="border">
</div>
<div class="card-cover">
<img :src="item.icon.normal" alt="cover">
</div>
<div class="card-content">
<span>{{ item.name }}</span>
</div>
</v-card>
</div>
</v-window-item>
</v-window>
</div>
<div v-else>
<div class="cards-grid">
<div v-for="item in CardsInfoS" :key="item.id" class="card-cls" @click="toOuter(item.name, item.id)">
<div class="card-border">
<img src="/source/GCG/base/bg-normal.webp" alt="border">
</div>
<div class="card-cover">
<img :src="item.icon.normal" alt="cover">
</div>
<div class="card-content">
<span>{{ item.name }}</span>
</div>
</div>
</div>
</div>
<v-snackbar v-model="snackbar" timeout="1500" color="error">
未找到相关卡牌
</v-snackbar>
</div>
</template>
<script lang="ts" setup>
// vue
@@ -133,38 +115,38 @@ const CardsInfoM = ref([] as MonsterCard[]);
const CardsInfoS = ref([] as BaseCard[]);
onMounted(async () => {
await loadData();
await loadData();
});
async function loadData() {
const CardsInfo = await ReadAllTGData("GCG");
CardsInfoC.value = CardsInfo.filter(item => item.type == "角色牌") as CharacterCard[];
CardsInfoA.value = CardsInfo.filter(item => item.type == "行动牌") as ActionCard[];
CardsInfoM.value = CardsInfo.filter(item => item.type == "魔物牌") as MonsterCard[];
loading.value = false;
async function loadData () {
const CardsInfo = await ReadAllTGData("GCG");
CardsInfoC.value = CardsInfo.filter((item) => item.type === "角色牌") as CharacterCard[];
CardsInfoA.value = CardsInfo.filter((item) => item.type === "行动牌") as ActionCard[];
CardsInfoM.value = CardsInfo.filter((item) => item.type === "魔物牌") as MonsterCard[];
loading.value = false;
}
function toOuter(card_name: string, card_id: number) {
const url = OBC_CONTENT_API.replace("{content_id}", card_id.toString());
createTGWindow(url, "GCG", card_name, 1200, 800, true);
function toOuter (card_name: string, card_id: number) {
const url = OBC_CONTENT_API.replace("{content_id}", card_id.toString());
createTGWindow(url, "GCG", card_name, 1200, 800, true);
}
async function searchCard() {
loading.value = true;
doSearch.value = true;
const res: BaseCard[] = [];
const allCardsInfo = await ReadAllTGData("GCG");
allCardsInfo.map(item => (item.name.includes(search.value) ? res.push(item) : null));
res.sort((a, b) => a.name.localeCompare(b.name));
loading.value = false;
if (res.length == 0) {
snackbar.value = true;
doSearch.value = false;
} else {
CardsInfoS.value = res;
}
async function searchCard () {
loading.value = true;
doSearch.value = true;
const res: BaseCard[] = [];
const allCardsInfo = await ReadAllTGData("GCG");
allCardsInfo.map((item) => (item.name.includes(search.value) ? res.push(item) : null));
res.sort((a, b) => a.name.localeCompare(b.name));
loading.value = false;
if (res.length === 0) {
snackbar.value = true;
doSearch.value = false;
} else {
CardsInfoS.value = res;
}
}
</script>
<style lang="css" scoped>
.GCG-grid {
.cards-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
grid-gap: 10px;
@@ -182,26 +164,7 @@ async function searchCard() {
border-radius: 10px;
}
.card-cls:hover .GCG-cover {
transform: scale(1.1);
transition: all 0.3s;
}
.GCG-border {
position: absolute;
border-radius: 10px;
top: 0;
left: 0;
overflow: hidden;
}
.GCG-border img {
width: 100%;
height: 100%;
border-radius: 10px;
}
.GCG-cover {
.card-cover {
position: absolute;
transition: all 0.3s;
top: 0;
@@ -211,20 +174,39 @@ async function searchCard() {
z-index: -1;
}
.GCG-cover img {
.card-cls:hover .card-cover {
transform: scale(1.1);
transition: all 0.3s;
}
.card-border {
position: absolute;
border-radius: 10px;
top: 0;
left: 0;
overflow: hidden;
}
.card-border img {
width: 100%;
height: 100%;
border-radius: 10px;
}
.card-cover img {
width: 100%;
height: 100%;
border-radius: 10px;
object-fit: cover;
}
.GCG-content {
.card-content {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 40px;
background: rgba(0, 0, 0, 0.5);
background: rgb(0 0 0 / 50%);
color: white;
display: flex;
font-size: small;

View File

@@ -1,12 +1,6 @@
<template>
<t-loading v-if="loading" :title="loadingTitle" :subtitle="loadingSubtitle" />
<component
v-show="!loading"
v-for="item in components"
:is="item"
:key="item"
:ref="setItemRef"
/>
<TLoading v-if="loading" :title="loadingTitle" :subtitle="loadingSubtitle" />
<component :is="item" v-for="item in components" v-show="!loading" :key="item" :ref="setItemRef" />
</template>
<script lang="ts" setup>
@@ -29,44 +23,42 @@ const loadingSubtitle = ref("");
// data
const components = ref([] as any[]);
let itemRefs = ref([] as any[]);
const itemRefs = ref([] as any[]);
onMounted(async () => {
loadingTitle.value = "正在加载首页";
const showItems = homeStore.getShowValue();
await Promise.allSettled(
showItems.map(item => {
switch (item) {
case "限时祈愿":
return components.value.push(markRaw(TPool));
case "近期活动":
return components.value.push(markRaw(TPosition));
case "素材日历":
return components.value.push(markRaw(TCalendar));
default:
break;
}
})
);
setInterval(() => {
if (!loading.value) clearInterval(this);
const loadingMap = itemRefs.value.map(item => {
if (item.loading) {
return item.name;
}
});
loadingSubtitle.value = "正在加载 " + loadingMap.filter(item => item)?.join("、");
if (loadingMap.every(item => !item)) {
loading.value = false;
}
}, 100);
loadingTitle.value = "正在加载首页";
const showItems = homeStore.getShowValue();
await Promise.allSettled(
showItems.map((item) => {
switch (item) {
case "限时祈愿":
return components.value.push(markRaw(TPool));
case "近期活动":
return components.value.push(markRaw(TPosition));
case "素材日历":
return components.value.push(markRaw(TCalendar));
default:
return null;
}
}),
);
setInterval(() => {
if (!loading.value) clearInterval(this);
const loadingMap = itemRefs.value.map((item) => {
return item.loading ? item.name : null;
});
loadingSubtitle.value = "正在加载 " + loadingMap.filter((item) => item)?.join("、");
if (loadingMap.every((item) => !item)) {
loading.value = false;
}
}, 100);
});
function setItemRef(item: any) {
if (itemRefs.value.includes(item)) return;
itemRefs.value.push(item);
function setItemRef (item: any) {
if (itemRefs.value.includes(item)) return;
itemRefs.value.push(item);
}
onUnmounted(() => {
itemRefs.value = [];
itemRefs.value = [];
});
</script>

View File

@@ -1,109 +0,0 @@
/**
* @file utils TGMap
* @description TGMap 的具体实现
* @author BTMuli<bt-muli@outlook.com>
* @since Alpha
*/
import { Map } from "../interface/Base";
/**
* @description TGMap 的具体实现
* @since Alpha
* @class TGMap
* @template T
* @see Map
* @return TGMap
*/
class TGMap<T> {
/**
* @description TGMap 的实例化数据
* @see Map
* @description 使用 protected 修饰符,防止外部直接修改
* @since Alpha
* @return Map<T>
*/
protected map: Map<T>;
/**
* @description TGMap 的构造函数
* @since Alpha
* @param {Map<T>} map - 传入的 Map<T>
* @description 可选参数
* @return TGMap
*/
constructor(map: Map<T> = {}) {
this.map = map;
}
/**
* @description 返回 TGMap 的实例化数据
* @since Alpha
* @return Map<T>
*/
getMap(): Map<T> {
return this.map;
}
/**
* @description Map<T> 的 forEach 方法
* @since Alpha
* @param {(value: T, key: number, map: Map<T>) => void} callback - 回调函数
* @return void
*/
forEach(callback: (value: T, key: number, map: Map<T>) => void): void {
Object.keys(this.map).forEach(key => {
callback(this.map[Number(key)], Number(key), this.map);
});
}
/**
* @description Map<T> 的 get 方法
* @since Alpha
* @param {number} key - 键
* @return T
*/
get(key: number): T {
return this.map[key];
}
/**
* @description Map<T> 的 set 方法
* @since Alpha
* @param {number} key - 键
* @param {T} value - 值
* @return void
*/
set(key: number, value: T): void {
this.map[key] = value;
}
/**
* @description Map<T> 的 has 方法
* @since Alpha
* @param {number} key - 键
* @return boolean
*/
has(key: number): boolean {
return this.map.hasOwnProperty(key);
}
/**
* @description Map<T> 的 sort 方法
* @since Alpha
* @param {(a: T, b: T) => number} callback - 回调函数
* @return TGMap<T>
*/
sort(callback: (a: T, b: T) => number): TGMap<T> {
const keys: string[] = Object.keys(this.map);
const values: T[] = Object.values(this.map);
const sortedValues: T[] = values.sort(callback);
const sortedMap: Map<T> = {};
keys.forEach((key, index) => {
sortedMap[Number(key)] = sortedValues[index];
});
return new TGMap(sortedMap);
}
}
export default TGMap;

View File

@@ -1,13 +1,17 @@
<template>
<div v-if="loading">
<t-loading :empty="loadingEmpty" :title="loadingTitle" />
</div>
<div v-else class="dev-json">
<div class="anno-title">活动列表 JSON</div>
<json-viewer :value="jsonList" copyable boxed />
<div class="anno-title">活动内容 JSON</div>
<json-viewer :value="jsonContent" copyable boxed />
</div>
<div v-if="loading">
<TLoading :empty="loadingEmpty" :title="loadingTitle" />
</div>
<div v-else class="dev-json">
<div class="anno-title">
活动列表 JSON
</div>
<JsonViewer :value="jsonList" copyable boxed />
<div class="anno-title">
活动内容 JSON
</div>
<JsonViewer :value="jsonContent" copyable boxed />
</div>
</template>
<script lang="ts" setup>
// vue
@@ -33,28 +37,25 @@ let jsonList = reactive({});
let jsonContent = reactive({});
onMounted(async () => {
await appWindow.show();
// 检查数据
if (!anno_id) {
loadingEmpty.value = true;
loadingTitle.value = "未找到数据";
return;
}
// 获取数据
loadingTitle.value = "正在获取数据...";
const listData = await GenshinOper.Announcement.getList();
listData.list.map((item: Announcement) => {
return item.list.map((single: AnnoListItem) => {
if (single.ann_id === anno_id) {
jsonList = single;
return;
}
});
});
jsonContent = await GenshinOper.Announcement.getContent(anno_id);
setTimeout(() => {
loading.value = false;
}, 200);
await appWindow.show();
// 检查数据
if (!anno_id) {
loadingEmpty.value = true;
loadingTitle.value = "未找到数据";
return;
}
// 获取数据
loadingTitle.value = "正在获取数据...";
const listData = await GenshinOper.Announcement.getList();
listData.list.map((item: Announcement) => {
return item.list.map((single: AnnoListItem) => {
return single.ann_id === anno_id ? (jsonList = single) : null;
});
});
jsonContent = await GenshinOper.Announcement.getContent(anno_id);
setTimeout(() => {
loading.value = false;
}, 200);
});
</script>
<style lang="css" scoped>

View File

@@ -1,13 +1,18 @@
<!-- eslint-disable vue/no-v-html -->
<template>
<div v-if="loading" class="loading">
<t-loading :title="loadingTitle" :empty="loadingEmpty" />
</div>
<div v-else class="anno-body">
<div class="anno-title">{{ annoData.title }}</div>
<div class="anno-subtitle">{{ annoData.subtitle }}</div>
<img :src="annoData.banner" alt="cover" class="anno-img" />
<div v-html="annoHtml" class="anno-content" />
</div>
<div v-if="loading" class="loading">
<TLoading :title="loadingTitle" :empty="loadingEmpty" />
</div>
<div v-else class="anno-body">
<div class="anno-title">
{{ annoData.title }}
</div>
<div class="anno-subtitle">
{{ annoData.subtitle }}
</div>
<img :src="annoData.banner" alt="cover" class="anno-img">
<div class="anno-content" v-html="annoHtml" />
</div>
</template>
<script lang="ts" setup>
// vue
@@ -32,27 +37,27 @@ const annoData = ref({} as AnnoContentItem);
const annoHtml = ref("");
onMounted(async () => {
await appWindow.show();
// 检查数据
if (!anno_id) {
loadingEmpty.value = true;
loadingTitle.value = "未找到数据";
return;
}
// 获取数据
loadingTitle.value = "正在获取数据...";
try {
annoData.value = await GenshinOper.Announcement.getContent(anno_id);
loadingTitle.value = "正在渲染数据...";
annoHtml.value = GenshinOper.Announcement.parser(annoData.value.content);
} catch (error) {
loadingEmpty.value = true;
loadingTitle.value = "公告不存在或解析失败";
return;
}
setTimeout(() => {
loading.value = false;
}, 200);
await appWindow.show();
// 检查数据
if (!anno_id) {
loadingEmpty.value = true;
loadingTitle.value = "未找到数据";
return;
}
// 获取数据
loadingTitle.value = "正在获取数据...";
try {
annoData.value = await GenshinOper.Announcement.getContent(anno_id);
loadingTitle.value = "正在渲染数据...";
annoHtml.value = GenshinOper.Announcement.parser(annoData.value.content);
} catch (error) {
loadingEmpty.value = true;
loadingTitle.value = "公告不存在或解析失败";
return;
}
setTimeout(() => {
loading.value = false;
}, 200);
});
</script>
<style lang="css" src="../assets/css/anno-parser.css" scoped />

View File

@@ -1,59 +1,69 @@
<template>
<div v-if="loading">
<t-loading :empty="loadingEmpty" :title="loadingTitle" />
</div>
<div v-else>
<div class="lottery-div">
<div class="lottery-title">抽奖详情 {{ timeStatus }}</div>
<v-list class="lottery-list">
<v-list-item>
<template v-slot:prepend>
<v-avatar>
<v-img :src="lotteryCard.creator.avatar_url" />
</v-avatar>
</template>
{{ lotteryCard.creator.nickname }}
<v-list-item-subtitle>{{ lotteryCard.creator.introduce }}</v-list-item-subtitle>
<template v-slot:append>发起人</template>
</v-list-item>
<v-list-item>
<v-list-item-title>{{ lotteryCard.participantWay }}</v-list-item-title>
<v-list-item-subtitle>{{ lotteryCard.id }}</v-list-item-subtitle>
<template v-slot:append>抽奖 ID</template>
</v-list-item>
</v-list>
<v-btn class="lottery-back" @click="backPost">返回</v-btn>
<v-btn @click="showJson = true" class="card-dev-btn" v-show="appStore.devMode">
<template v-slot:prepend>
<img src="../assets/icons/arrow-right.svg" alt="right" />
</template>
JSON
</v-btn>
</div>
<div class="dev-json" v-show="showJson">
<json-viewer :value="jsonData" copyable boxed />
</div>
<div class="lottery-div">
<div class="lottery-title">奖品详情</div>
<div v-for="reward in lotteryCard.rewards">
<v-list class="lottery-list">
<v-list-item :title="reward.rewardName" :subtitle="'中奖人数' + reward.winnerNumber" />
</v-list>
<div class="lottery-grid">
<v-list v-for="user in reward.users" class="lottery-sub-list">
<v-list-item>
<template v-slot:prepend>
<v-avatar>
<v-img :src="user.avatar_url" />
</v-avatar>
</template>
{{ user.nickname }}
</v-list-item>
</v-list>
</div>
</div>
</div>
</div>
<div v-if="loading">
<TLoading :empty="loadingEmpty" :title="loadingTitle" />
</div>
<div v-else>
<div class="lottery-div">
<div class="lottery-title">
抽奖详情 {{ timeStatus }}
</div>
<v-list class="lottery-list">
<v-list-item>
<template #prepend>
<v-avatar>
<v-img :src="lotteryCard.creator.avatar_url" />
</v-avatar>
</template>
{{ lotteryCard.creator.nickname }}
<v-list-item-subtitle>{{ lotteryCard.creator.introduce }}</v-list-item-subtitle>
<template #append>
发起人
</template>
</v-list-item>
<v-list-item>
<v-list-item-title>{{ lotteryCard.participantWay }}</v-list-item-title>
<v-list-item-subtitle>{{ lotteryCard.id }}</v-list-item-subtitle>
<template #append>
抽奖 ID
</template>
</v-list-item>
</v-list>
<v-btn class="lottery-back" @click="backPost">
返回
</v-btn>
<v-btn v-show="appStore.devMode" class="card-dev-btn" @click="showJson = true">
<template #prepend>
<img src="../assets/icons/arrow-right.svg" alt="right">
</template>
JSON
</v-btn>
</div>
<div v-show="showJson" class="dev-json">
<JsonViewer :value="jsonData" copyable boxed />
</div>
<div class="lottery-div">
<div class="lottery-title">
奖品详情
</div>
<div v-for="reward in lotteryCard.rewards" :key="reward.rewardName">
<v-list class="lottery-list">
<v-list-item :title="reward.rewardName" :subtitle="'中奖人数' + reward.winnerNumber" />
</v-list>
<div class="lottery-grid">
<v-list v-for="user in reward.users" :key="user.uid" class="lottery-sub-list">
<v-list-item>
<template #prepend>
<v-avatar>
<v-img :src="user.avatar_url" />
</v-avatar>
</template>
{{ user.nickname }}
</v-list-item>
</v-list>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
// vue
@@ -85,50 +95,50 @@ const showJson = ref(false as boolean);
let jsonData = reactive({} as LotteryData);
const timeStatus = ref("未知" as string);
function backPost() {
window.history.back();
function backPost () {
window.history.back();
}
onMounted(async () => {
await appWindow.show();
// 检查数据
if (!lottery_id) {
loadingEmpty.value = true;
loadingTitle.value = "未找到数据";
return;
}
// 获取数据
loadingTitle.value = "正在获取数据...";
jsonData = await MysOper.Lottery.get(lottery_id);
if (!jsonData) {
loadingEmpty.value = true;
loadingTitle.value = "未找到数据";
return;
}
await appWindow.setTitle("抽奖详情 " + jsonData.lottery_entity_summary);
loadingTitle.value = "正在渲染数据...";
lotteryCard.value = MysOper.Lottery.card.lottery(jsonData);
if (jsonData.status === "Settled") {
timeStatus.value = "已开奖";
} else {
await setInterval(() => {
const timeNow = new Date().getTime();
const timeDiff = Number(jsonData.draw_time) * 1000 - timeNow;
if (timeDiff <= 0) {
timeStatus.value = "已开奖";
clearInterval(this);
} else {
const day = Math.floor(timeDiff / (24 * 3600 * 1000));
const hour = Math.floor((timeDiff % (24 * 3600 * 1000)) / (3600 * 1000));
const minute = Math.floor((timeDiff % (3600 * 1000)) / (60 * 1000));
const second = Math.floor((timeDiff % (60 * 1000)) / 1000);
timeStatus.value = `${day}${hour}小时${minute}${second}`;
}
}, 1000);
}
setTimeout(() => {
loading.value = false;
}, 200);
await appWindow.show();
// 检查数据
if (!lottery_id) {
loadingEmpty.value = true;
loadingTitle.value = "未找到数据";
return;
}
// 获取数据
loadingTitle.value = "正在获取数据...";
jsonData = await MysOper.Lottery.get(lottery_id);
if (!jsonData) {
loadingEmpty.value = true;
loadingTitle.value = "未找到数据";
return;
}
await appWindow.setTitle("抽奖详情 " + jsonData.lottery_entity_summary);
loadingTitle.value = "正在渲染数据...";
lotteryCard.value = MysOper.Lottery.card.lottery(jsonData);
if (jsonData.status === "Settled") {
timeStatus.value = "已开奖";
} else {
await setInterval(() => {
const timeNow = new Date().getTime();
const timeDiff = Number(jsonData.draw_time) * 1000 - timeNow;
if (timeDiff <= 0) {
timeStatus.value = "已开奖";
clearInterval(this);
} else {
const day = Math.floor(timeDiff / (24 * 3600 * 1000));
const hour = Math.floor((timeDiff % (24 * 3600 * 1000)) / (3600 * 1000));
const minute = Math.floor((timeDiff % (3600 * 1000)) / (60 * 1000));
const second = Math.floor((timeDiff % (60 * 1000)) / 1000);
timeStatus.value = `${day}${hour}小时${minute}${second}`;
}
}, 1000);
}
setTimeout(() => {
loading.value = false;
}, 200);
});
</script>
<style lang="css">

View File

@@ -1,142 +1,143 @@
<template>
<div v-if="loading">
<t-loading :title="loadingTitle" />
</div>
<div v-else>
<v-tabs v-model="tab" align-tabs="start" class="news-tabs">
<v-tab value="notice" title="公告" />
<v-tab value="activity" title="活动" />
<v-tab value="news" title="新闻" v-if="showNews" />
<v-spacer></v-spacer>
<v-btn class="switch-btn" @click="switchAnno" v-if="showSwitch">
<template v-slot:prepend>
<v-icon>mdi-bullhorn</v-icon>
</template>
切换游戏内公告
</v-btn>
<v-text-field
v-show="appStore.devMode"
v-model="search"
append-icon="mdi-magnify"
label="搜索"
single-line
hide-details
@click:append="searchPost"
@keyup.enter="searchPost"
></v-text-field>
</v-tabs>
<v-window v-model="tab">
<v-window-item value="notice">
<div class="news-grid">
<v-card v-for="item in postData.notice" class="news-card" width="340">
<div class="news-cover" @click="toPost(item)">
<img :src="item.cover" alt="cover" />
</div>
<v-card-title>{{ item.title }}</v-card-title>
<v-card-actions>
<v-btn @click="toPost(item)" class="card-btn">
<template v-slot:prepend>
<img src="../assets/icons/circle-check.svg" alt="check" />查看
</template>
</v-btn>
<v-card-subtitle>id:{{ item.post_id }}</v-card-subtitle>
<v-btn @click="toJson(item)" class="card-dev-btn" v-show="appStore.devMode">
<template v-slot:prepend>
<img src="../assets/icons/arrow-right.svg" alt="right" />
</template>
JSON
</v-btn>
</v-card-actions>
</v-card>
</div>
<div class="load-news">
<v-btn @click="loadMore('notice')" :loading="loadingSub">
<template v-slot:append>
<img src="../assets/icons/arrow-left.svg" alt="right" />
</template>
已加载{{ rawData.notice.last_id }}加载更多
</v-btn>
</div>
</v-window-item>
<v-window-item value="activity">
<div class="news-grid">
<v-card class="news-card" v-for="item in postData.activity" width="340">
<div class="news-cover" @click="toPost(item)">
<img :src="item.cover" alt="cover" />
</div>
<v-card-title>{{ item.title }}</v-card-title>
<v-card-subtitle>{{ item.subtitle }}</v-card-subtitle>
<v-card-actions>
<v-btn @click="toPost(item)" class="card-btn">
<template v-slot:prepend>
<img src="../assets/icons/circle-check.svg" alt="check" />查看
</template>
</v-btn>
<v-card-subtitle>id:{{ item.post_id }}</v-card-subtitle>
<div v-show="!appStore.devMode">
<v-btn
:style="{
background: item.status?.colorCss,
color: '#faf7e8 !important',
}"
>{{ item.status?.status }}</v-btn
>
</div>
<v-btn @click="toJson(item)" class="card-dev-btn" v-show="appStore.devMode">
<template v-slot:prepend>
<img src="../assets/icons/arrow-right.svg" alt="right" />
</template>
JSON
</v-btn>
</v-card-actions>
</v-card>
</div>
<div class="load-news">
<v-btn @click="loadMore('activity')" :loading="loadingSub">
<template v-slot:append>
<img src="../assets/icons/arrow-left.svg" alt="right" />
</template>
已加载:{{ rawData.activity.last_id }}加载更多
</v-btn>
</div>
</v-window-item>
<v-window-item value="news" v-if="showNews">
<div class="news-grid">
<v-card class="news-card" v-for="item in postData.news" width="340">
<div class="news-cover" @click="toPost(item)">
<img :src="item.cover" alt="cover" />
</div>
<v-card-title>{{ item.title }}</v-card-title>
<v-card-actions>
<v-btn @click="toPost(item)" class="card-btn">
<template v-slot:prepend>
<img src="../assets/icons/circle-check.svg" alt="check" />查看
</template>
</v-btn>
<v-card-subtitle>id:{{ item.post_id }}</v-card-subtitle>
<v-btn @click="toJson(item)" class="card-dev-btn" v-show="appStore.devMode">
<template v-slot:prepend>
<img src="../assets/icons/arrow-right.svg" alt="right" />
</template>
JSON
</v-btn>
</v-card-actions>
</v-card>
</div>
<div class="load-news">
<v-btn @click="loadMore('news')" :loading="loadingSub">
<template v-slot:append>
<img src="../assets/icons/arrow-left.svg" alt="right" />
</template>
已加载{{ rawData.news.last_id }}加载更多
</v-btn>
</div>
</v-window-item>
</v-window>
<v-snackbar v-model="snackbar" timeout="1500" :color="snackbarColor">
{{ snackbarText }}
</v-snackbar>
</div>
<div v-if="loading">
<TLoading :title="loadingTitle" />
</div>
<div v-else>
<v-tabs v-model="tab" align-tabs="start" class="news-tabs">
<v-tab value="notice" title="公告" />
<v-tab value="activity" title="活动" />
<v-tab v-if="showNews" value="news" title="新闻" />
<v-spacer />
<v-btn v-if="showSwitch" class="switch-btn" @click="switchAnno">
<template #prepend>
<v-icon>mdi-bullhorn</v-icon>
</template>
切换游戏内公告
</v-btn>
<v-text-field
v-show="appStore.devMode"
v-model="search"
append-icon="mdi-magnify"
label="搜索"
single-line
hide-details
@click:append="searchPost"
@keyup.enter="searchPost"
/>
</v-tabs>
<v-window v-model="tab">
<v-window-item value="notice">
<div class="news-grid">
<v-card v-for="item in postData.notice" :key="item.post_id" class="news-card" width="340">
<div class="news-cover" @click="toPost(item)">
<img :src="item.cover" alt="cover">
</div>
<v-card-title>{{ item.title }}</v-card-title>
<v-card-actions>
<v-btn class="card-btn" @click="toPost(item)">
<template #prepend>
<img src="../assets/icons/circle-check.svg" alt="check">查看
</template>
</v-btn>
<v-card-subtitle>id:{{ item.post_id }}</v-card-subtitle>
<v-btn v-show="appStore.devMode" class="card-dev-btn" @click="toJson(item)">
<template #prepend>
<img src="../assets/icons/arrow-right.svg" alt="right">
</template>
JSON
</v-btn>
</v-card-actions>
</v-card>
</div>
<div class="load-news">
<v-btn :loading="loadingSub" @click="loadMore('notice')">
<template #append>
<img src="../assets/icons/arrow-left.svg" alt="right">
</template>
已加载{{ rawData.notice.last_id }}加载更多
</v-btn>
</div>
</v-window-item>
<v-window-item value="activity">
<div class="news-grid">
<v-card v-for="item in postData.activity" :key="item.post_id" class="news-card" width="340">
<div class="news-cover" @click="toPost(item)">
<img :src="item.cover" alt="cover">
</div>
<v-card-title>{{ item.title }}</v-card-title>
<v-card-subtitle>{{ item.subtitle }}</v-card-subtitle>
<v-card-actions>
<v-btn class="card-btn" @click="toPost(item)">
<template #prepend>
<img src="../assets/icons/circle-check.svg" alt="check">查看
</template>
</v-btn>
<v-card-subtitle>id:{{ item.post_id }}</v-card-subtitle>
<div v-show="!appStore.devMode">
<v-btn
:style="{
background: item.status?.colorCss,
color: '#faf7e8 !important',
}"
>
{{ item.status?.status }}
</v-btn>
</div>
<v-btn v-show="appStore.devMode" class="card-dev-btn" @click="toJson(item)">
<template #prepend>
<img src="../assets/icons/arrow-right.svg" alt="right">
</template>
JSON
</v-btn>
</v-card-actions>
</v-card>
</div>
<div class="load-news">
<v-btn :loading="loadingSub" @click="loadMore('activity')">
<template #append>
<img src="../assets/icons/arrow-left.svg" alt="right">
</template>
已加载:{{ rawData.activity.last_id }}加载更多
</v-btn>
</div>
</v-window-item>
<v-window-item v-if="showNews" value="news">
<div class="news-grid">
<v-card v-for="item in postData.news" :key="item.post_id" class="news-card" width="340">
<div class="news-cover" @click="toPost(item)">
<img :src="item.cover" alt="cover">
</div>
<v-card-title>{{ item.title }}</v-card-title>
<v-card-actions>
<v-btn class="card-btn" @click="toPost(item)">
<template #prepend>
<img src="../assets/icons/circle-check.svg" alt="check">查看
</template>
</v-btn>
<v-card-subtitle>id:{{ item.post_id }}</v-card-subtitle>
<v-btn v-show="appStore.devMode" class="card-dev-btn" @click="toJson(item)">
<template #prepend>
<img src="../assets/icons/arrow-right.svg" alt="right">
</template>
JSON
</v-btn>
</v-card-actions>
</v-card>
</div>
<div class="load-news">
<v-btn :loading="loadingSub" @click="loadMore('news')">
<template #append>
<img src="../assets/icons/arrow-left.svg" alt="right">
</template>
已加载{{ rawData.news.last_id }}加载更多
</v-btn>
</div>
</v-window-item>
</v-window>
<v-snackbar v-model="snackbar" timeout="1500" :color="snackbarColor">
{{ snackbarText }}
</v-snackbar>
</div>
</template>
<script lang="ts" setup>
@@ -177,170 +178,167 @@ const search = ref("" as string);
// 数据
const tab = ref("" as string);
const postData = ref({
notice: [] as NewsCard[],
activity: [] as NewsCard[],
news: [] as NewsCard[],
notice: [] as NewsCard[],
activity: [] as NewsCard[],
news: [] as NewsCard[],
});
const rawData = ref({
notice: {
is_last: false,
last_id: 0,
},
activity: {
is_last: false,
last_id: 0,
},
news: {
is_last: false,
last_id: 0,
},
notice: {
is_last: false,
last_id: 0,
},
activity: {
is_last: false,
last_id: 0,
},
news: {
is_last: false,
last_id: 0,
},
});
onMounted(async () => {
loadingTitle.value = "正在获取公告数据...";
const noticeData = await MysOper.News.get.notice(gid);
rawData.value.notice.is_last = noticeData.is_last;
rawData.value.notice.last_id = noticeData.list.length;
loadingTitle.value = "正在获取活动数据...";
const activityData = await MysOper.News.get.activity(gid);
rawData.value.activity.is_last = activityData.is_last;
rawData.value.activity.last_id = activityData.list.length;
if (showNews.value) {
loadingTitle.value = "正在获取新闻数据...";
const newsData = await MysOper.News.get.news(gid);
rawData.value.news.is_last = newsData.is_last;
rawData.value.news.last_id = newsData.list.length;
postData.value = {
notice: MysOper.News.card.notice(noticeData),
activity: MysOper.News.card.activity(activityData),
news: MysOper.News.card.news(newsData),
};
} else {
postData.value = {
notice: MysOper.News.card.notice(noticeData),
activity: MysOper.News.card.activity(activityData),
news: [],
};
}
tab.value = "notice";
loading.value = false;
loadingTitle.value = "正在获取公告数据...";
const noticeData = await MysOper.News.get.notice(gid);
rawData.value.notice.is_last = noticeData.is_last;
rawData.value.notice.last_id = noticeData.list.length;
loadingTitle.value = "正在获取活动数据...";
const activityData = await MysOper.News.get.activity(gid);
rawData.value.activity.is_last = activityData.is_last;
rawData.value.activity.last_id = activityData.list.length;
if (showNews.value) {
loadingTitle.value = "正在获取新闻数据...";
const newsData = await MysOper.News.get.news(gid);
rawData.value.news.is_last = newsData.is_last;
rawData.value.news.last_id = newsData.list.length;
postData.value = {
notice: MysOper.News.card.notice(noticeData),
activity: MysOper.News.card.activity(activityData),
news: MysOper.News.card.news(newsData),
};
} else {
postData.value = {
notice: MysOper.News.card.notice(noticeData),
activity: MysOper.News.card.activity(activityData),
news: [],
};
}
tab.value = "notice";
loading.value = false;
});
function switchAnno() {
router.push("/announcements");
async function switchAnno () {
await router.push("/announcements");
}
// 加载更多
async function loadMore(data: string) {
loadingSub.value = true;
switch (data) {
case "notice":
if (rawData.value.notice.is_last) {
snackbarText.value = "已经是最后一页了";
snackbarColor.value = "#35acce";
snackbar.value = true;
loadingSub.value = false;
return;
}
const getNotice = await MysOper.News.get.notice(gid, 20, rawData.value.notice.last_id);
rawData.value.notice.last_id = rawData.value.notice.last_id + getNotice.list.length;
rawData.value.notice.is_last = getNotice.is_last;
const noticeCard = MysOper.News.card.notice(getNotice);
postData.value.notice = postData.value.notice.concat(noticeCard);
loadingSub.value = false;
break;
case "activity":
if (rawData.value.activity.is_last) {
snackbarText.value = "已经是最后一页了";
snackbarColor.value = "#35acce";
snackbar.value = true;
loadingSub.value = false;
return;
}
const getActivity = await MysOper.News.get.activity(gid, 20, rawData.value.activity.last_id);
rawData.value.activity.last_id = rawData.value.activity.last_id + getActivity.list.length;
rawData.value.activity.is_last = getActivity.is_last;
const activityCard = MysOper.News.card.activity(getActivity);
postData.value.activity = postData.value.activity.concat(activityCard);
loadingSub.value = false;
break;
case "news":
if (rawData.value.news.is_last) {
snackbarText.value = "已经是最后一页了";
snackbarColor.value = "#35acce";
snackbar.value = true;
loadingSub.value = false;
return;
}
const getNews = await MysOper.News.get.news(gid, 20, rawData.value.news.last_id);
rawData.value.news.last_id = rawData.value.news.last_id + getNews.list.length;
rawData.value.news.is_last = getNews.is_last;
const newsCard = MysOper.News.card.news(getNews);
postData.value.news = postData.value.news.concat(newsCard);
loadingSub.value = false;
break;
default:
break;
}
async function loadMore (data: string) {
loadingSub.value = true;
switch (data) {
case "notice":
if (rawData.value.notice.is_last) {
snackbarText.value = "已经是最后一页了";
snackbarColor.value = "#35acce";
snackbar.value = true;
loadingSub.value = false;
return;
}
const getNotice = await MysOper.News.get.notice(gid, 20, rawData.value.notice.last_id);
rawData.value.notice.last_id = rawData.value.notice.last_id + getNotice.list.length;
rawData.value.notice.is_last = getNotice.is_last;
const noticeCard = MysOper.News.card.notice(getNotice);
postData.value.notice = postData.value.notice.concat(noticeCard);
loadingSub.value = false;
break;
case "activity":
if (rawData.value.activity.is_last) {
snackbarText.value = "已经是最后一页了";
snackbarColor.value = "#35acce";
snackbar.value = true;
loadingSub.value = false;
return;
}
const getActivity = await MysOper.News.get.activity(gid, 20, rawData.value.activity.last_id);
rawData.value.activity.last_id = rawData.value.activity.last_id + getActivity.list.length;
rawData.value.activity.is_last = getActivity.is_last;
const activityCard = MysOper.News.card.activity(getActivity);
postData.value.activity = postData.value.activity.concat(activityCard);
loadingSub.value = false;
break;
case "news":
if (rawData.value.news.is_last) {
snackbarText.value = "已经是最后一页了";
snackbarColor.value = "#35acce";
snackbar.value = true;
loadingSub.value = false;
return;
}
const getNews = await MysOper.News.get.news(gid, 20, rawData.value.news.last_id);
rawData.value.news.last_id = rawData.value.news.last_id + getNews.list.length;
rawData.value.news.is_last = getNews.is_last;
const newsCard = MysOper.News.card.news(getNews);
postData.value.news = postData.value.news.concat(newsCard);
loadingSub.value = false;
break;
default:
break;
}
}
async function toPost(item: NewsCard | string) {
if (typeof item === "string") {
const path = router.resolve({
name: "帖子详情",
params: {
post_id: item,
},
}).href;
createTGWindow(path, "帖子-Dev", item, 960, 720, false);
} else {
const path = router.resolve({
name: "帖子详情",
params: {
post_id: item.post_id.toString(),
},
}).href;
createTGWindow(path, "帖子", item.title, 960, 720, false);
}
async function toPost (item: NewsCard | string) {
if (typeof item === "string") {
const path = router.resolve({
name: "帖子详情",
params: {
post_id: item,
},
}).href;
createTGWindow(path, "帖子-Dev", item, 960, 720, false);
} else {
const path = router.resolve({
name: "帖子详情",
params: {
post_id: item.post_id.toString(),
},
}).href;
createTGWindow(path, "帖子", item.title, 960, 720, false);
}
}
async function toJson(item: NewsCard | string) {
if (typeof item === "string") {
const path = router.resolve({
name: "帖子详情JSON",
params: {
post_id: item,
},
}).href;
createTGWindow(path, "帖子-JSON-Dev", `${item}-JSON`, 960, 720, false);
return;
} else {
const path = router.resolve({
name: "帖子详情JSON",
params: {
post_id: item.post_id.toString(),
},
}).href;
createTGWindow(path, "帖子-JSON", `${item.title}-JSON`, 960, 720, false);
return;
}
async function toJson (item: NewsCard | string) {
if (typeof item === "string") {
const path = router.resolve({
name: "帖子详情JSON",
params: {
post_id: item,
},
}).href;
createTGWindow(path, "帖子-JSON-Dev", `${item}-JSON`, 960, 720, false);
} else {
const path = router.resolve({
name: "帖子详情JSON",
params: {
post_id: item.post_id.toString(),
},
}).href;
createTGWindow(path, "帖子-JSON", `${item.title}-JSON`, 960, 720, false);
}
}
async function searchPost() {
if (search.value === "") {
snackbarText.value = "请输入搜索内容";
snackbarColor.value = "error";
snackbar.value = true;
return;
}
if (!isNaN(Number(search.value))) {
await toPost(search.value);
await toJson(search.value);
} else {
snackbarText.value = "请输入搜索内容";
snackbarColor.value = "error";
snackbar.value = true;
return;
}
async function searchPost () {
if (search.value === "") {
snackbarText.value = "请输入搜索内容";
snackbarColor.value = "error";
snackbar.value = true;
return;
}
if (!isNaN(Number(search.value))) {
await toPost(search.value);
await toJson(search.value);
} else {
snackbarText.value = "请输入搜索内容";
snackbarColor.value = "error";
snackbar.value = true;
}
}
</script>
@@ -380,6 +378,7 @@ async function searchPost() {
height: 150px;
transition: all 0.3s linear;
}
/* switch */
.switch-btn {
font-family: Genshin, serif;
@@ -389,6 +388,7 @@ async function searchPost() {
margin-top: 5px;
color: #546d8b;
}
/* load more */
.load-news {
font-family: Genshin, serif;

View File

@@ -1,10 +1,10 @@
<template>
<div v-if="loading">
<t-loading :empty="loadingEmpty" :title="loadingTitle" />
</div>
<div v-else class="dev-json">
<json-viewer :value="jsonData" copyable boxed />
</div>
<div v-if="loading">
<TLoading :empty="loadingEmpty" :title="loadingTitle" />
</div>
<div v-else class="dev-json">
<JsonViewer :value="jsonData" copyable boxed />
</div>
</template>
<script lang="ts" setup>
// vue
@@ -27,18 +27,18 @@ const post_id = Number(useRoute().params.post_id);
let jsonData = reactive({});
onMounted(async () => {
await appWindow.show();
// 检查数据
if (!post_id) {
loadingEmpty.value = true;
loadingTitle.value = "未找到数据";
return;
}
// 获取数据
loadingTitle.value = "正在获取数据...";
jsonData = await MysOper.Post.get(post_id);
setTimeout(() => {
loading.value = false;
}, 200);
await appWindow.show();
// 检查数据
if (!post_id) {
loadingEmpty.value = true;
loadingTitle.value = "未找到数据";
return;
}
// 获取数据
loadingTitle.value = "正在获取数据...";
jsonData = await MysOper.Post.get(post_id);
setTimeout(() => {
loading.value = false;
}, 200);
});
</script>

View File

@@ -1,8 +1,9 @@
<!-- eslint-disable vue/no-v-html -->
<template>
<div v-if="loading">
<t-loading :empty="loadingEmpty" :title="loadingTitle" />
</div>
<div v-else v-html="postHtml" class="mys-post-body" />
<div v-if="loading">
<TLoading :empty="loadingEmpty" :title="loadingTitle" />
</div>
<div v-else class="mys-post-body" v-html="postHtml" />
</template>
<script lang="ts" setup>
// vue
@@ -24,31 +25,31 @@ const post_id = Number(useRoute().params.post_id);
const postHtml = ref("");
onMounted(async () => {
await appWindow.show();
// 检查数据
if (!post_id) {
loadingEmpty.value = true;
loadingTitle.value = "未找到数据";
await appWindow.setTitle("未找到数据");
return;
}
// 获取数据
loadingTitle.value = "正在获取数据...";
try {
const postData = await MysOper.Post.get(post_id);
loadingTitle.value = "正在渲染数据...";
postHtml.value = MysOper.Post.parser(postData);
await appWindow.setTitle(postData.post.subject);
} catch (error) {
console.error(error);
loadingEmpty.value = true;
loadingTitle.value = "帖子不存在或解析失败";
await appWindow.setTitle("帖子不存在或解析失败");
return;
}
setTimeout(() => {
loading.value = false;
}, 200);
await appWindow.show();
// 检查数据
if (!post_id) {
loadingEmpty.value = true;
loadingTitle.value = "未找到数据";
await appWindow.setTitle("未找到数据");
return;
}
// 获取数据
loadingTitle.value = "正在获取数据...";
try {
const postData = await MysOper.Post.get(post_id);
loadingTitle.value = "正在渲染数据...";
postHtml.value = MysOper.Post.parser(postData);
await appWindow.setTitle(postData.post.subject);
} catch (error) {
console.error(error);
loadingEmpty.value = true;
loadingTitle.value = "帖子不存在或解析失败";
await appWindow.setTitle("帖子不存在或解析失败");
return;
}
setTimeout(() => {
loading.value = false;
}, 200);
});
</script>
<style lang="css" scoped src="../assets/css/post-parser.css"></style>

18
src/vite-env.d.ts vendored
View File

@@ -1,12 +1,18 @@
/**
* @file vite-env.d.ts
* @description vite-env.d.ts
* @author BTMuli<bt-muli@outlook.com>
* @since Alpha v0.1.2
*/
declare module "*.vue" {
import type { DefineComponent } from "vue";
const component: DefineComponent<{}, {}, any>;
export default component;
import type { DefineComponent } from "vue";
const component: DefineComponent<object, object, any>;
export default component;
}
declare module "vue-json-viewer" {
import type { DefineComponent } from "vue";
const component: DefineComponent<{}, {}, any>;
export default component;
import type { DefineComponent } from "vue";
const component: DefineComponent<object, object, any>;
export default component;
}

View File

@@ -5,36 +5,36 @@ import vuetify from "vite-plugin-vuetify";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue(), vuetify()],
plugins: [vue(), vuetify()],
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
// prevent vite from obscuring rust errors
clearScreen: false,
// tauri expects a fixed port, fail if that port is not available
server: {
port: 3000,
strictPort: true,
},
// to make use of `TAURI_DEBUG` and other env variables
// https://tauri.studio/v1/api/config#buildconfig.beforedevcommand
envPrefix: ["VITE_", "TAURI_"],
build: {
// Tauri supports es2021
target: process.env.TAURI_PLATFORM == "windows" ? "chrome105" : "safari13",
// don't minify for debug builds
minify: !process.env.TAURI_DEBUG ? "esbuild" : false,
// produce sourcemaps for debug builds
sourcemap: !!process.env.TAURI_DEBUG,
// rollup options
rollupOptions: {
// chunking
output: {
manualChunks(id) {
if (id.includes("node_modules")) {
return id.toString().split("node_modules/")[1].split("/")[0].toString();
}
},
},
},
},
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
// prevent vite from obscuring rust errors
clearScreen: false,
// tauri expects a fixed port, fail if that port is not available
server: {
port: 3000,
strictPort: true,
},
// to make use of `TAURI_DEBUG` and other env variables
// https://tauri.studio/v1/api/config#buildconfig.beforedevcommand
envPrefix: ["VITE_", "TAURI_"],
build: {
// Tauri supports es2021
target: process.env.TAURI_PLATFORM === "windows" ? "chrome105" : "safari13",
// don't minify for debug builds
minify: !process.env.TAURI_DEBUG ? "esbuild" : false,
// produce sourcemaps for debug builds
sourcemap: !!process.env.TAURI_DEBUG,
// rollup options
rollupOptions: {
// chunking
output: {
manualChunks (id) {
if (id.includes("node_modules")) {
return id.toString().split("node_modules/")[1].split("/")[0].toString();
}
},
},
},
},
});