mirror of
https://github.com/BTMuli/TeyvatGuide.git
synced 2026-03-25 05:29:45 +08:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7108741400 | ||
|
|
45debc85d8 | ||
|
|
702fda94bd | ||
|
|
7fd5292174 | ||
|
|
81b1e15e31 | ||
|
|
87a970b776 | ||
|
|
d5ec4b1b0e | ||
|
|
c7f24d2345 | ||
|
|
a54f2f5f53 | ||
|
|
a69fb8ca50 | ||
|
|
d15cece933 | ||
|
|
6a3d2d6b36 | ||
|
|
dd65bb565d | ||
|
|
f4678be198 | ||
|
|
dbed43bf7e | ||
|
|
5357df3743 | ||
|
|
02ddb441a0 | ||
|
|
2a83fcbcb8 |
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
@@ -43,11 +43,11 @@ jobs:
|
||||
- name: setup node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 23.1.0
|
||||
node-version: 23.3.0
|
||||
- name: setup pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 9.14.4
|
||||
version: 9.15.0
|
||||
- name: Install frontend dependencies
|
||||
run: pnpm install
|
||||
|
||||
|
||||
4
.github/workflows/qodana_code_quality.yml
vendored
4
.github/workflows/qodana_code_quality.yml
vendored
@@ -12,11 +12,11 @@ jobs:
|
||||
- name: setup node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 23.1.0
|
||||
node-version: 23.3.0
|
||||
- name: setup pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 9.14.4
|
||||
version: 9.15.0
|
||||
- name: Install dependencies
|
||||
run: pnpm install --no-frozen-lockfile
|
||||
- name: "Qodana Scan"
|
||||
|
||||
13
CHANGELOG.md
13
CHANGELOG.md
@@ -2,12 +2,21 @@
|
||||
Author: 目棃
|
||||
Description: CHANGELOG
|
||||
Date: 2024-10-09
|
||||
Update: 2024-12-03
|
||||
Update: 2024-12-11
|
||||
---
|
||||
|
||||
> 本文档 [`Frontmatter`](https://github.com/BTMuli/MuCli#Frontmatter) 由 [MuCli](https://github.com/BTMuli/Mucli) 自动生成于 `2024-10-09 15:51:43`
|
||||
>
|
||||
> 更新于 `2024-12-03 19:57:50`
|
||||
> 更新于 `2024-12-11 15:37:05`
|
||||
|
||||
## [0.6.5](https://github.com/BTMuli/TeyvatGuide/releases/v0.6.5) (2024-12-11)
|
||||
|
||||
- 🍱 添加下半卡池数据&部分资源
|
||||
- ✨ 帖子内容中涉及的话题链接支持应用内跳转
|
||||
- ♻️ 首页组件加载逻辑重构
|
||||
- ✨ UIGF4导入/导出浮窗,支持自选UID
|
||||
- 💄 调整剧诗部分数据缺失时的显示
|
||||
- 🐛 调整部分UI,修复切换账户后角色详情刷新异常
|
||||
|
||||
## [0.6.4](https://github.com/BTMuli/TeyvatGuide/releases/v0.6.4) (2024-12-03)
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ import pluginImport from "eslint-plugin-import";
|
||||
import pluginPrettier from "eslint-plugin-prettier";
|
||||
import pluginVue from "eslint-plugin-vue";
|
||||
import globals from "globals";
|
||||
import eslint_ts from "typescript-eslint";
|
||||
import vue_parser from "vue-eslint-parser";
|
||||
import eslintTs from "typescript-eslint";
|
||||
import parserVue from "vue-eslint-parser";
|
||||
|
||||
const tsConfigRules = {
|
||||
"@typescript-eslint/consistent-type-assertions": ["error", { assertionStyle: "angle-bracket" }],
|
||||
@@ -24,25 +24,25 @@ const tsConfigRules = {
|
||||
|
||||
const tsConfig = {
|
||||
files: ["*.ts", "*.d.ts", "src/**/*.ts", "src/**/*.d.ts"],
|
||||
plugins: { typescript: eslint_ts, import: pluginImport, prettier: pluginPrettier },
|
||||
plugins: { typescript: eslintTs, import: pluginImport, prettier: pluginPrettier },
|
||||
languageOptions: {
|
||||
parser: eslint_ts.parser,
|
||||
parser: eslintTs.parser,
|
||||
parserOptions: { project: "tsconfig.json", tsconfigRootDir: "." },
|
||||
},
|
||||
rules: tsConfigRules,
|
||||
};
|
||||
|
||||
const vueConfig = {
|
||||
files: ["src/**/*.vue"],
|
||||
files: ["src/**/*.vue", "src/App.vue"],
|
||||
plugins: { vue: pluginVue, import: pluginImport, prettier: pluginPrettier },
|
||||
languageOptions: {
|
||||
globals: { ...globals.browser, ...globals.es2021, TGApp: "readonly", window: "readonly" },
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module",
|
||||
parser: vue_parser,
|
||||
parser: parserVue,
|
||||
parserOptions: {
|
||||
project: "tsconfig.json",
|
||||
parser: eslint_ts.parser,
|
||||
parser: eslintTs.parser,
|
||||
extraFileExtensions: [".vue"],
|
||||
tsconfigRootDir: ".",
|
||||
},
|
||||
|
||||
46
package.json
46
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "teyvatguide",
|
||||
"version": "0.6.4",
|
||||
"version": "0.6.5",
|
||||
"description": "Game Tool for GenshinImpact player",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@9.14.4",
|
||||
@@ -68,15 +68,15 @@
|
||||
"dependencies": {
|
||||
"@mdi/font": "7.4.47",
|
||||
"@tauri-apps/api": "^2.1.1",
|
||||
"@tauri-apps/plugin-deep-link": "^2.0.0",
|
||||
"@tauri-apps/plugin-deep-link": "^2.0.1",
|
||||
"@tauri-apps/plugin-dialog": "^2.0.1",
|
||||
"@tauri-apps/plugin-fs": "^2.0.2",
|
||||
"@tauri-apps/plugin-fs": "^2.0.3",
|
||||
"@tauri-apps/plugin-http": "^2.0.1",
|
||||
"@tauri-apps/plugin-log": "^2.0.0",
|
||||
"@tauri-apps/plugin-log": "^2.0.1",
|
||||
"@tauri-apps/plugin-os": "^2.0.0",
|
||||
"@tauri-apps/plugin-process": "^2.0.0",
|
||||
"@tauri-apps/plugin-shell": "^2.0.1",
|
||||
"@tauri-apps/plugin-sql": "^2.0.1",
|
||||
"@tauri-apps/plugin-sql": "^2.0.2",
|
||||
"ajv": "^8.17.1",
|
||||
"artplayer": "^5.2.1",
|
||||
"clipboard": "^2.0.11",
|
||||
@@ -85,51 +85,51 @@
|
||||
"html2canvas": "^1.4.1",
|
||||
"js-md5": "^0.8.3",
|
||||
"jsencrypt": "^3.3.2",
|
||||
"pinia": "^2.2.6",
|
||||
"pinia": "^2.2.8",
|
||||
"pinia-plugin-persistedstate": "^4.1.3",
|
||||
"uuid": "^11.0.3",
|
||||
"vue": "^3.5.13",
|
||||
"vue-echarts": "^7.0.3",
|
||||
"vue-json-viewer": "^3.0.4",
|
||||
"vue-router": "^4.4.5",
|
||||
"vuetify": "^3.7.4",
|
||||
"vue-router": "^4.5.0",
|
||||
"vuetify": "^3.7.5",
|
||||
"wcag-color": "^1.1.1",
|
||||
"xml-js": "^1.6.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.2.0",
|
||||
"@eslint/js": "^9.15.0",
|
||||
"@eslint/js": "^9.16.0",
|
||||
"@tauri-apps/cli": "2.1.0",
|
||||
"@types/color-convert": "^2.0.4",
|
||||
"@types/js-md5": "^0.7.2",
|
||||
"@types/node": "^22.9.0",
|
||||
"@types/node": "^22.10.1",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@typescript-eslint/parser": "^8.15.0",
|
||||
"@vitejs/plugin-vue": "^5.2.0",
|
||||
"@typescript-eslint/parser": "^8.17.0",
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"concurrently": "^9.1.0",
|
||||
"eslint": "^9.15.0",
|
||||
"eslint": "^9.16.0",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"eslint-plugin-jsonc": "^2.18.2",
|
||||
"eslint-plugin-prettier": "^5.2.1",
|
||||
"eslint-plugin-vue": "^9.31.0",
|
||||
"eslint-plugin-yml": "^1.15.0",
|
||||
"globals": "^15.12.0",
|
||||
"eslint-plugin-vue": "^9.32.0",
|
||||
"eslint-plugin-yml": "^1.16.0",
|
||||
"globals": "^15.13.0",
|
||||
"husky": "^9.1.7",
|
||||
"jsonc-eslint-parser": "^2.4.0",
|
||||
"lint-staged": "^15.2.10",
|
||||
"oxlint": "^0.11.1",
|
||||
"prettier": "3.3.3",
|
||||
"stylelint": "^16.10.0",
|
||||
"oxlint": "^0.14.0",
|
||||
"prettier": "3.4.1",
|
||||
"stylelint": "^16.11.0",
|
||||
"stylelint-config-idiomatic-order": "^10.0.0",
|
||||
"stylelint-config-standard-vue": "^1.0.0",
|
||||
"stylelint-declaration-block-no-ignored-properties": "^2.8.0",
|
||||
"stylelint-high-performance-animation": "^1.10.0",
|
||||
"stylelint-order": "^6.0.4",
|
||||
"stylelint-prettier": "^5.0.2",
|
||||
"typescript": "^5.6.3",
|
||||
"typescript-eslint": "^8.15.0",
|
||||
"vite": "^5.4.11",
|
||||
"vite-plugin-vue-devtools": "^7.6.4",
|
||||
"typescript": "^5.7.2",
|
||||
"typescript-eslint": "^8.17.0",
|
||||
"vite": "^6.0.2",
|
||||
"vite-plugin-vue-devtools": "^7.6.7",
|
||||
"vite-plugin-vuetify": "^2.0.4",
|
||||
"vue-eslint-parser": "^9.4.3",
|
||||
"yaml-eslint-parser": "^1.2.3"
|
||||
|
||||
1306
pnpm-lock.yaml
generated
1306
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -25,3 +25,4 @@ profile:
|
||||
linter: jetbrains/qodana-js:latest
|
||||
include:
|
||||
- name: CheckDependencyLicenses
|
||||
|
||||
|
||||
413
src-tauri/Cargo.lock
generated
413
src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "TeyvatGuide"
|
||||
version = "0.6.4"
|
||||
version = "0.6.5"
|
||||
description = "Game Tool for Genshin Impact player"
|
||||
authors = ["BTMuli <bt-muli@outlook.com>"]
|
||||
license = "MIT"
|
||||
@@ -10,7 +10,7 @@ edition = "2021"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2.0.2", features = [] }
|
||||
tauri-build = { version = "2.0.3", features = [] }
|
||||
|
||||
[dependencies]
|
||||
chrono = "0.4.38"
|
||||
@@ -19,7 +19,7 @@ serde = { version = "1.0.215", features = ["derive"] }
|
||||
serde_json = "1.0.133"
|
||||
tauri = { version = "2.1.1", features = [] }
|
||||
tauri-utils = "2.1.0"
|
||||
url = "2.5.3"
|
||||
url = "2.5.4"
|
||||
walkdir = "2.5.0"
|
||||
|
||||
# deep link 插件
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! @file src/client/mod.rs
|
||||
//! @desc 客户端模块,负责操作米游社客户端
|
||||
//! @since Beta v0.6.2
|
||||
//! @since Beta v0.6.5
|
||||
|
||||
mod menu;
|
||||
mod utils;
|
||||
@@ -12,7 +12,7 @@ use tauri_utils::config::WebviewUrl;
|
||||
pub async fn create_mhy_client(handle: AppHandle, func: String, url: String) {
|
||||
let mut win_width = 400.0;
|
||||
let mut win_height = 800.0;
|
||||
let win_ua = "Mozilla/5.0 (Linux; Android 12) Mobile miHoYoBBS/2.77.2";
|
||||
let win_ua = "Mozilla/5.0 (Linux; Android 12) Mobile miHoYoBBS/2.78.1";
|
||||
let url_parse;
|
||||
if url != "" {
|
||||
url_parse = WebviewUrl::External(url.parse().unwrap());
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"productName": "TeyvatGuide",
|
||||
"identifier": "TeyvatGuide",
|
||||
"version": "0.6.4",
|
||||
"version": "0.6.5",
|
||||
"build": {
|
||||
"beforeDevCommand": "pnpm vite:dev",
|
||||
"beforeBuildCommand": "pnpm vite:build",
|
||||
|
||||
105
src/App.vue
105
src/App.vue
@@ -11,9 +11,10 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { app, event, core, webviewWindow, window as TauriWindow } from "@tauri-apps/api";
|
||||
import { app, core, event, webviewWindow } from "@tauri-apps/api";
|
||||
import { PhysicalSize } from "@tauri-apps/api/dpi";
|
||||
import { UnlistenFn, Event } from "@tauri-apps/api/event";
|
||||
import { Event, UnlistenFn } from "@tauri-apps/api/event";
|
||||
import { currentMonitor, getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import { mkdir } from "@tauri-apps/plugin-fs";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { computed, onBeforeMount, onMounted, onUnmounted, ref } from "vue";
|
||||
@@ -31,20 +32,17 @@ import { getBuildTime } from "./utils/TGBuild.js";
|
||||
import TGLogger from "./utils/TGLogger.js";
|
||||
import OtherApi from "./web/request/otherReq.js";
|
||||
|
||||
const appStore = useAppStore();
|
||||
const userStore = storeToRefs(useUserStore());
|
||||
const isMain = ref<boolean>(false);
|
||||
const theme = ref<string>(appStore.theme);
|
||||
const router = useRouter();
|
||||
const vuetifyTheme = computed(() => {
|
||||
return appStore.theme === "dark" ? "dark" : "light";
|
||||
});
|
||||
const { theme, needResize, deviceInfo, isLogin, userDir, buildTime } = storeToRefs(useAppStore());
|
||||
const { uid, briefInfo, account, cookie } = storeToRefs(useUserStore());
|
||||
const isMain = ref<boolean>(false);
|
||||
const vuetifyTheme = computed<string>(() => (theme.value === "dark" ? "dark" : "light"));
|
||||
|
||||
let themeListener: UnlistenFn | null = null;
|
||||
let urlListener: UnlistenFn | null = null;
|
||||
|
||||
onBeforeMount(async () => {
|
||||
const win = webviewWindow.getCurrentWebviewWindow();
|
||||
const win = getCurrentWindow();
|
||||
isMain.value = win.label === "TeyvatGuide";
|
||||
if (isMain.value) {
|
||||
const title = "Teyvat Guide v" + (await app.getVersion()) + " Beta";
|
||||
@@ -53,17 +51,28 @@ onBeforeMount(async () => {
|
||||
await core.invoke("init_app");
|
||||
urlListener = await getDeepLink();
|
||||
}
|
||||
if (appStore.needResize !== "false") await checkResize();
|
||||
await win.show();
|
||||
if (needResize.value !== "false") await checkResize();
|
||||
document.documentElement.className = theme.value;
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
await getCurrentWindow().show();
|
||||
themeListener = await event.listen("readTheme", async (e: Event<string>) => {
|
||||
const themeGet = e.payload;
|
||||
if (theme.value !== themeGet) {
|
||||
theme.value = themeGet;
|
||||
document.documentElement.className = theme.value;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
async function checkResize(): Promise<void> {
|
||||
const screen = await TauriWindow.currentMonitor();
|
||||
const screen = await currentMonitor();
|
||||
if (screen === null) {
|
||||
showSnackbar.error("获取屏幕信息失败!", 3000);
|
||||
return;
|
||||
}
|
||||
const windowCur = await webviewWindow.getCurrentWebviewWindow();
|
||||
const windowCur = webviewWindow.getCurrentWebviewWindow();
|
||||
if (await windowCur.isMaximized()) return;
|
||||
const designSize = getSize(windowCur.label);
|
||||
const widthScale = screen.size.width / 1920;
|
||||
@@ -75,7 +84,7 @@ async function checkResize(): Promise<void> {
|
||||
),
|
||||
);
|
||||
await windowCur.setZoom((1 / screen.scaleFactor) * Math.min(widthScale, heightScale));
|
||||
await windowCur.center();
|
||||
await windowCur.setFocus();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -85,17 +94,6 @@ function getSize(label: string): PhysicalSize {
|
||||
return new PhysicalSize(1280, 720);
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
document.documentElement.className = theme.value;
|
||||
themeListener = await event.listen("readTheme", async (e: Event<string>) => {
|
||||
const themeGet = e.payload;
|
||||
if (theme.value !== themeGet) {
|
||||
theme.value = themeGet;
|
||||
document.documentElement.className = theme.value;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 启动后只执行一次的监听
|
||||
function listenOnInit(): void {
|
||||
console.info("[App][listenOnInit] 监听初始化事件!");
|
||||
@@ -129,56 +127,55 @@ async function checkAppLoad(): Promise<void> {
|
||||
// 检测 deviceFp
|
||||
async function checkDeviceFp(): Promise<void> {
|
||||
const appData = await TGSqlite.getAppData();
|
||||
const deviceLocal = appStore.deviceInfo;
|
||||
const deviceFind = appData.find((item) => item.key === "deviceInfo");
|
||||
if (typeof deviceFind === "undefined") {
|
||||
if (deviceLocal.device_fp === "0000000000000") {
|
||||
appStore.deviceInfo = await OtherApi.fp(appStore.deviceInfo);
|
||||
if (deviceInfo.value.device_fp === "0000000000000") {
|
||||
deviceInfo.value = await OtherApi.fp(deviceInfo.value);
|
||||
}
|
||||
await TGSqlite.saveAppData("deviceInfo", JSON.stringify(deviceLocal));
|
||||
await TGSqlite.saveAppData("deviceInfo", JSON.stringify(deviceInfo.value));
|
||||
return;
|
||||
}
|
||||
if (JSON.parse(deviceFind.value) !== deviceLocal) {
|
||||
appStore.deviceInfo = JSON.parse(deviceFind.value);
|
||||
if (JSON.parse(deviceFind.value) !== deviceInfo.value) {
|
||||
deviceInfo.value = JSON.parse(deviceFind.value);
|
||||
}
|
||||
}
|
||||
|
||||
async function checkUserLoad(): Promise<void> {
|
||||
// 检测用户数据目录
|
||||
const appData = await TGSqlite.getAppData();
|
||||
const userDir = appData.find((item) => item.key === "userDir")?.value;
|
||||
if (typeof userDir === "undefined") await TGSqlite.saveAppData("userDir", appStore.userDir);
|
||||
else if (userDir !== appStore.userDir) appStore.userDir = userDir;
|
||||
await mkdir(appStore.userDir, { recursive: true });
|
||||
const userDirGet = appData.find((item) => item.key === "userDir")?.value;
|
||||
if (typeof userDirGet === "undefined") await TGSqlite.saveAppData("userDir", userDir.value);
|
||||
else if (userDirGet !== userDir.value) userDir.value = userDirGet;
|
||||
await mkdir(userDir.value, { recursive: true });
|
||||
// 检测用户数据
|
||||
const uidDB = await TSUserAccount.account.getAllUid();
|
||||
if (uidDB.length === 0 && appStore.isLogin) {
|
||||
if (uidDB.length === 0 && isLogin.value) {
|
||||
showSnackbar.warn("未检测到可用UID,请重新登录!");
|
||||
appStore.isLogin = false;
|
||||
isLogin.value = false;
|
||||
return;
|
||||
}
|
||||
if (!appStore.isLogin) appStore.isLogin = true;
|
||||
if (!isLogin.value) isLogin.value = true;
|
||||
// 然后获取最近的UID
|
||||
if (userStore.uid.value === undefined || !uidDB.includes(userStore.uid.value)) {
|
||||
userStore.uid.value = uidDB[0];
|
||||
if (uid.value === undefined || !uidDB.includes(uid.value)) {
|
||||
uid.value = uidDB[0];
|
||||
}
|
||||
const curAccount = await TSUserAccount.account.getAccount(userStore.uid.value);
|
||||
const curAccount = await TSUserAccount.account.getAccount(uid.value);
|
||||
if (curAccount === false) {
|
||||
showSnackbar.error(`未获取到${userStore.uid.value}的账号数据!`);
|
||||
await TGLogger.Error(`[App][listenOnInit] 获取${userStore.uid.value}账号数据失败`);
|
||||
showSnackbar.error(`未获取到${uid.value}的账号数据!`);
|
||||
await TGLogger.Error(`[App][listenOnInit] 获取${uid.value}账号数据失败`);
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
} else {
|
||||
userStore.briefInfo.value = curAccount.brief;
|
||||
userStore.cookie.value = curAccount.cookie;
|
||||
briefInfo.value = curAccount.brief;
|
||||
cookie.value = curAccount.cookie;
|
||||
}
|
||||
const curGameAccount = await TSUserAccount.game.getCurAccount(userStore.uid.value);
|
||||
if (curGameAccount === false) {
|
||||
showSnackbar.error(`未获取到${userStore.uid.value}的游戏数据!`);
|
||||
await TGLogger.Error(`[App][listenOnInit] 获取${userStore.uid.value}游戏数据失败`);
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
} else {
|
||||
userStore.account.value = curGameAccount;
|
||||
const curGameAccount = await TSUserAccount.game.getCurAccount(uid.value);
|
||||
if (curGameAccount !== false) {
|
||||
account.value = curGameAccount;
|
||||
return;
|
||||
}
|
||||
showSnackbar.error(`未获取到${uid.value}的游戏数据!`);
|
||||
await TGLogger.Error(`[App][listenOnInit] 获取${uid.value}游戏数据失败`);
|
||||
await new Promise<void>((resolve) => setTimeout(resolve, 1000));
|
||||
}
|
||||
|
||||
async function getDeepLink(): Promise<UnlistenFn> {
|
||||
@@ -252,7 +249,7 @@ async function checkUpdate(): Promise<void> {
|
||||
showSnackbar.error("请到设置页手动更新数据库!", 3000);
|
||||
return;
|
||||
}
|
||||
appStore.buildTime = getBuildTime();
|
||||
buildTime.value = getBuildTime();
|
||||
await TGSqlite.update();
|
||||
showSnackbar.success("数据库已更新!", 3000);
|
||||
window.open("https://app.btmuli.ink/docs/TeyvatGuide/changelogs.html");
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
</transition>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted, onUnmounted } from "vue";
|
||||
import { onMounted, onUnmounted, ref } from "vue";
|
||||
|
||||
const scrollTop = ref(0); // 滚动条距离顶部的距离
|
||||
const canTop = ref(false); // 默认不显示
|
||||
const scrollTop = ref<number>(0); // 滚动条距离顶部的距离
|
||||
const canTop = ref<boolean>(false); // 默认不显示
|
||||
|
||||
// 监听滚动事件
|
||||
function handleScroll(): void {
|
||||
@@ -40,15 +40,8 @@ function handleScrollTop(): void {
|
||||
});
|
||||
}
|
||||
|
||||
// 监听滚动事件
|
||||
onMounted(() => {
|
||||
window.addEventListener("scroll", handleScroll);
|
||||
});
|
||||
|
||||
// 销毁监听
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener("scroll", handleScroll);
|
||||
});
|
||||
onMounted(() => window.addEventListener("scroll", handleScroll));
|
||||
onUnmounted(() => window.removeEventListener("scroll", handleScroll));
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { emit } from "@tauri-apps/api/event";
|
||||
import { computed, onMounted, ref, watch } from "vue";
|
||||
import { computed, onMounted, ref, shallowRef, watch } from "vue";
|
||||
|
||||
import Mys from "../../plugins/Mys/index.js";
|
||||
import { useAppStore } from "../../store/modules/app.js";
|
||||
@@ -25,17 +25,12 @@ import showSnackbar from "../func/snackbar.js";
|
||||
|
||||
import ToLivecode from "./to-livecode.vue";
|
||||
|
||||
interface TGameNavProps {
|
||||
modelValue: number;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<TGameNavProps>(), {
|
||||
modelValue: 2,
|
||||
});
|
||||
type TGameNavProps = { modelValue: number };
|
||||
const props = withDefaults(defineProps<TGameNavProps>(), { modelValue: 2 });
|
||||
|
||||
const appStore = useAppStore();
|
||||
const nav = ref<TGApp.BBS.Navigator.Navigator[]>([]);
|
||||
const codeData = ref<TGApp.BBS.Navigator.CodeData[]>([]);
|
||||
const nav = shallowRef<TGApp.BBS.Navigator.Navigator[]>([]);
|
||||
const codeData = shallowRef<TGApp.BBS.Navigator.CodeData[]>([]);
|
||||
const showOverlay = ref<boolean>(false);
|
||||
const actId = ref<string>();
|
||||
|
||||
|
||||
@@ -51,11 +51,6 @@ export type TItemBoxData = {
|
||||
type TItemBoxProps = { modelValue: TItemBoxData };
|
||||
|
||||
const props = defineProps<TItemBoxProps>();
|
||||
const size = props.modelValue.size;
|
||||
const height = props.modelValue.height;
|
||||
const cursor = props.modelValue.clickable ? "pointer" : "default";
|
||||
const sizeLt = props.modelValue.ltSize;
|
||||
const sizeRt = props.modelValue.rtSize;
|
||||
const sizeInner = `${props.modelValue.innerHeight ?? 0}px`;
|
||||
const fontSizeInner = props.modelValue.innerHeight ? `${props.modelValue.innerHeight / 2}px` : "0";
|
||||
const sizeOuter = `${props.modelValue.outerHeight ?? 0}px`;
|
||||
@@ -65,9 +60,9 @@ const innerBlur = props.modelValue.innerBlur ?? "0";
|
||||
<style lang="css" scoped>
|
||||
.tib-box {
|
||||
position: relative;
|
||||
width: v-bind(size);
|
||||
height: v-bind(height);
|
||||
cursor: v-bind(cursor);
|
||||
width: v-bind("props.modelValue.size");
|
||||
height: v-bind("props.modelValue.height");
|
||||
cursor: v-bind('props.modelValue.clickable ? "pointer" : "default"');
|
||||
}
|
||||
|
||||
.tib-bg {
|
||||
@@ -75,8 +70,8 @@ const innerBlur = props.modelValue.innerBlur ?? "0";
|
||||
top: 0;
|
||||
left: 0;
|
||||
overflow: hidden;
|
||||
width: v-bind(size);
|
||||
height: v-bind(size);
|
||||
width: v-bind("props.modelValue.size");
|
||||
height: v-bind("props.modelValue.size");
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
@@ -89,8 +84,8 @@ const innerBlur = props.modelValue.innerBlur ?? "0";
|
||||
.tib-icon {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
width: v-bind(size);
|
||||
height: v-bind(size);
|
||||
width: v-bind("props.modelValue.size");
|
||||
height: v-bind("props.modelValue.size");
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
@@ -105,8 +100,8 @@ const innerBlur = props.modelValue.innerBlur ?? "0";
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
width: v-bind(size);
|
||||
height: v-bind(size);
|
||||
width: v-bind("props.modelValue.size");
|
||||
height: v-bind("props.modelValue.size");
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -118,8 +113,8 @@ const innerBlur = props.modelValue.innerBlur ?? "0";
|
||||
top: 3%;
|
||||
left: 3%;
|
||||
display: flex;
|
||||
width: v-bind(sizeLt);
|
||||
height: v-bind(sizeLt);
|
||||
width: v-bind("props.modelValue.ltSize");
|
||||
height: v-bind("props.modelValue.ltSize");
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
@@ -135,8 +130,8 @@ const innerBlur = props.modelValue.innerBlur ?? "0";
|
||||
top: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
width: v-bind(sizeRt);
|
||||
height: v-bind(sizeRt);
|
||||
width: v-bind("props.modelValue.rtSize");
|
||||
height: v-bind("props.modelValue.rtSize");
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgb(0 0 0 / 40%);
|
||||
@@ -152,7 +147,7 @@ const innerBlur = props.modelValue.innerBlur ?? "0";
|
||||
left: 0;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: v-bind(sizeInner);
|
||||
height: v-bind("props.modelValue.innerHeight ?? 0") px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
-webkit-backdrop-filter: blur(v-bind(innerBlur));
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<transition enter-from-class="tolo-enter-from" name="tolo">
|
||||
<div v-if="showTolo" class="tolo-box" @click.self.prevent="toClick">
|
||||
<div v-if="showTolo" class="tolo-box" @click.self.prevent="toClick()">
|
||||
<transition enter-from-class="toli-enter-from" name="toli">
|
||||
<slot v-if="showToli" />
|
||||
</transition>
|
||||
@@ -8,23 +8,18 @@
|
||||
</transition>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
// vue
|
||||
import { ref, watch } from "vue";
|
||||
|
||||
interface TolProps {
|
||||
modelValue: boolean;
|
||||
toClick?: () => void;
|
||||
blurVal: string;
|
||||
hide?: true;
|
||||
}
|
||||
|
||||
type TolProps = { modelValue: boolean; blurVal?: string; dismissible?: boolean };
|
||||
type TolEmits = (e: "update:modelValue", v: boolean) => void;
|
||||
const emit = defineEmits<TolEmits>();
|
||||
const props = withDefaults(defineProps<TolProps>(), {
|
||||
modelValue: false,
|
||||
blurVal: "20px",
|
||||
dismissible: true,
|
||||
});
|
||||
|
||||
const showTolo = ref(!props.hide);
|
||||
const showToli = ref(!props.hide);
|
||||
const showTolo = ref<boolean>(false);
|
||||
const showToli = ref<boolean>(false);
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
@@ -32,16 +27,17 @@ watch(
|
||||
if (props.modelValue) {
|
||||
showTolo.value = true;
|
||||
showToli.value = true;
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
showToli.value = false;
|
||||
}, 100);
|
||||
setTimeout(() => {
|
||||
showTolo.value = false;
|
||||
}, 300);
|
||||
return;
|
||||
}
|
||||
setTimeout(() => (showToli.value = false), 100);
|
||||
setTimeout(() => (showTolo.value = false), 300);
|
||||
},
|
||||
);
|
||||
|
||||
function toClick(): void {
|
||||
if (!props.dismissible) return;
|
||||
emit("update:modelValue", false);
|
||||
}
|
||||
</script>
|
||||
<style lang="css" scoped>
|
||||
.tolo-enter-active,
|
||||
|
||||
@@ -15,10 +15,10 @@
|
||||
</div>
|
||||
<div class="tpc-title" :title="card.title" @click="shareCard">{{ card.title }}</div>
|
||||
</div>
|
||||
<div class="tpc-mid" v-if="card.user">
|
||||
<div class="tpc-mid" v-if="card.user !== null">
|
||||
<TpAvatar :data="card.user" position="left" />
|
||||
</div>
|
||||
<div class="tpc-bottom" v-if="card.data">
|
||||
<div class="tpc-bottom" v-if="card.data !== null">
|
||||
<div class="tpc-tags">
|
||||
<div v-for="topic in card.topics" :key="topic.id" class="tpc-tag" @click="toTopic(topic)">
|
||||
<v-icon size="10">mdi-tag</v-icon>
|
||||
@@ -50,7 +50,7 @@
|
||||
</div>
|
||||
<div
|
||||
class="tpc-forum"
|
||||
v-if="card.forum && card.forum.name !== ''"
|
||||
v-if="card.forum !== null && card.forum.name !== ''"
|
||||
:title="`频道: ${card.forum.name}`"
|
||||
@click="toForum(card.forum)"
|
||||
>
|
||||
@@ -68,27 +68,20 @@
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { emit } from "@tauri-apps/api/event";
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from "vue";
|
||||
import { computed, onMounted, onUnmounted, ref, shallowRef, watch } from "vue";
|
||||
|
||||
import { generateShareImg, saveImgLocal } from "../../utils/TGShare.js";
|
||||
import { createPost } from "../../utils/TGWindow.js";
|
||||
import showSnackbar from "../func/snackbar.js";
|
||||
import TpAvatar from "../viewPost/tp-avatar.vue";
|
||||
|
||||
interface TPostCardProps {
|
||||
modelValue: TGApp.Plugins.Mys.Post.FullData;
|
||||
selectMode?: boolean;
|
||||
}
|
||||
type TPostCardProps = { modelValue: TGApp.Plugins.Mys.Post.FullData; selectMode?: boolean };
|
||||
type TPostCardEmits = (e: "onSelected", v: string) => void;
|
||||
|
||||
interface TPostCardEmits {
|
||||
(e: "onSelected", value: string): void;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<TPostCardProps>(), {
|
||||
selectMode: false,
|
||||
});
|
||||
const props = withDefaults(defineProps<TPostCardProps>(), { selectMode: false });
|
||||
const emits = defineEmits<TPostCardEmits>();
|
||||
const isAct = ref<boolean>(false);
|
||||
const card = ref<TGApp.Plugins.Mys.News.RenderCard>();
|
||||
const card = shallowRef<TGApp.Plugins.Mys.News.RenderCard>();
|
||||
const localCover = ref<string>();
|
||||
|
||||
const cardBg = computed<string>(() => {
|
||||
@@ -106,9 +99,8 @@ async function reload(data: TGApp.Plugins.Mys.Post.FullData): Promise<void> {
|
||||
localCover.value = undefined;
|
||||
}
|
||||
card.value = getPostCard(data);
|
||||
if (card.value && card.value.cover !== "") {
|
||||
if (card.value && card.value.cover !== "")
|
||||
localCover.value = await saveImgLocal(card.value.cover);
|
||||
}
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
@@ -186,32 +178,30 @@ function getPostCover(item: TGApp.Plugins.Mys.Post.FullData): string {
|
||||
* @returns {TGApp.Plugins.Mys.News.RenderCard} 渲染用咨讯列表项
|
||||
*/
|
||||
function getCommonCard(item: TGApp.Plugins.Mys.Post.FullData): TGApp.Plugins.Mys.News.RenderCard {
|
||||
let forum: TGApp.Plugins.Mys.News.RenderForum | null = null;
|
||||
let data: TGApp.Plugins.Mys.News.RenderData | null = null;
|
||||
if (item.forum !== null) {
|
||||
forum = {
|
||||
name: item.forum.name,
|
||||
icon: item.forum.icon,
|
||||
id: item.forum.id,
|
||||
};
|
||||
}
|
||||
if (item.stat !== null) {
|
||||
data = {
|
||||
mark: item.stat.bookmark_num,
|
||||
forward: item.stat.forward_num,
|
||||
like: item.stat.like_num,
|
||||
reply: item.stat.reply_num,
|
||||
view: item.stat.view_num,
|
||||
};
|
||||
}
|
||||
return {
|
||||
title: item.post.subject,
|
||||
cover: getPostCover(item),
|
||||
postId: Number(item.post.post_id),
|
||||
subtitle: item.post.post_id,
|
||||
user: item.user,
|
||||
forum: forum,
|
||||
data: data,
|
||||
forum:
|
||||
item.forum !== null
|
||||
? {
|
||||
name: item.forum.name,
|
||||
icon: item.forum.icon,
|
||||
id: item.forum.id,
|
||||
}
|
||||
: null,
|
||||
data:
|
||||
item.stat !== null
|
||||
? {
|
||||
mark: item.stat.bookmark_num,
|
||||
forward: item.stat.forward_num,
|
||||
like: item.stat.like_num,
|
||||
reply: item.stat.reply_num,
|
||||
view: item.stat.view_num,
|
||||
}
|
||||
: null,
|
||||
topics: item.topics,
|
||||
};
|
||||
}
|
||||
@@ -235,9 +225,13 @@ function getPostCard(item: TGApp.Plugins.Mys.Post.FullData): TGApp.Plugins.Mys.N
|
||||
|
||||
async function shareCard(): Promise<void> {
|
||||
if (!card.value) return;
|
||||
const dom = <HTMLDivElement>document.querySelector(`#post-card-${card.value.postId}`);
|
||||
const shareDom = document.querySelector<HTMLDivElement>(`#post-card-${card.value.postId}`);
|
||||
if (shareDom === null) {
|
||||
showSnackbar.error("分享内容不存在", 3000);
|
||||
return;
|
||||
}
|
||||
const fileName = `PostCard_${card.value.postId}`;
|
||||
await generateShareImg(fileName, dom, 2.5);
|
||||
await generateShareImg(fileName, shareDom, 2.5);
|
||||
}
|
||||
|
||||
async function toTopic(topic: TGApp.Plugins.Mys.Topic.Info): Promise<void> {
|
||||
|
||||
@@ -9,31 +9,28 @@
|
||||
import TGLogger from "../../utils/TGLogger.js";
|
||||
import { generateShareImg } from "../../utils/TGShare.js";
|
||||
import showLoading from "../func/loading.js";
|
||||
import showSnackbar from "../func/snackbar.js";
|
||||
|
||||
interface TShareBtnProps {
|
||||
modelValue: HTMLElement;
|
||||
title: string;
|
||||
}
|
||||
|
||||
type TShareBtnProps = { selector: string; title: string };
|
||||
const props = defineProps<TShareBtnProps>();
|
||||
|
||||
async function shareContent(): Promise<void> {
|
||||
showLoading.start("正在生成分享图片", props.title);
|
||||
await TGLogger.Info("[TShareBtn][shareContent] 开始生成分享图片");
|
||||
props.modelValue.querySelectorAll("details").forEach((item) => {
|
||||
if (item.open) {
|
||||
item.setAttribute("details-open", "");
|
||||
} else {
|
||||
item.open = true;
|
||||
}
|
||||
const shareDom = document.querySelector<HTMLElement>(props.selector);
|
||||
if (shareDom === null) {
|
||||
showSnackbar.error("分享内容不存在", 3000);
|
||||
showLoading.end();
|
||||
return;
|
||||
}
|
||||
shareDom.querySelectorAll("details").forEach((item) => {
|
||||
if (item.open) item.setAttribute("details-open", "");
|
||||
else item.open = true;
|
||||
});
|
||||
await generateShareImg(props.title, props.modelValue);
|
||||
props.modelValue.querySelectorAll("details").forEach((item) => {
|
||||
if (item.hasAttribute("details-open")) {
|
||||
item.removeAttribute("details-open");
|
||||
} else {
|
||||
item.open = false;
|
||||
}
|
||||
await generateShareImg(props.title, shareDom);
|
||||
shareDom.querySelectorAll("details").forEach((item) => {
|
||||
if (item.hasAttribute("details-open")) item.removeAttribute("details-open");
|
||||
else item.open = false;
|
||||
});
|
||||
showLoading.end();
|
||||
await TGLogger.Info("[TShareBtn][shareContent] 生成分享图片完成");
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<v-navigation-drawer :permanent="true" :rail="rail" class="tsb-box">
|
||||
<v-list class="side-list" density="compact" :nav="true">
|
||||
<!-- 负责收缩侧边栏 -->
|
||||
<v-list-item @click="collapse()">
|
||||
<v-list-item @click="rail = !rail">
|
||||
<template v-if="rail" #prepend>
|
||||
<v-list-item-action>
|
||||
<v-icon>mdi-chevron-right</v-icon>
|
||||
@@ -198,16 +198,6 @@
|
||||
<img src="/source/UI/posts.png" alt="collect" class="side-icon-menu" />
|
||||
</template>
|
||||
</v-list-item>
|
||||
<v-list-item
|
||||
class="side-item-menu"
|
||||
title="登录"
|
||||
@click="login"
|
||||
v-show="userStore.cookie.value?.stoken === ''"
|
||||
>
|
||||
<template #prepend>
|
||||
<img src="/source/UI/lumine.webp" class="side-icon-menu" alt="login" />
|
||||
</template>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
<v-list-item :title.attr="themeTitle" @click="switchTheme()">
|
||||
@@ -231,71 +221,45 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { event, webviewWindow } from "@tauri-apps/api";
|
||||
import { UnlistenFn, Event } from "@tauri-apps/api/event";
|
||||
import { Event, UnlistenFn } from "@tauri-apps/api/event";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from "vue";
|
||||
import { computed, onMounted, onUnmounted } from "vue";
|
||||
|
||||
import { useAppStore } from "../../store/modules/app.js";
|
||||
import { useUserStore } from "../../store/modules/user.js";
|
||||
import mhyClient from "../../utils/TGClient.js";
|
||||
import TGLogger from "../../utils/TGLogger.js";
|
||||
import showSnackbar from "../func/snackbar.js";
|
||||
|
||||
const appStore = useAppStore();
|
||||
const userStore = storeToRefs(useUserStore());
|
||||
|
||||
const isDevEnv = ref<boolean>(import.meta.env.MODE === "development");
|
||||
|
||||
const userInfo = ref({
|
||||
nickname: "未登录",
|
||||
uid: "-1",
|
||||
desc: "请扫码登录",
|
||||
avatar: "/source/UI/lumine.webp",
|
||||
const { briefInfo } = storeToRefs(useUserStore());
|
||||
let themeListener: UnlistenFn | null = null;
|
||||
// @ts-expect-error The import.meta meta-property is not allowed in files which will build into CommonJS output.
|
||||
const isDevEnv = import.meta.env.MODE === "development";
|
||||
const rail = computed<boolean>({
|
||||
get: () => appStore.sidebar.collapse,
|
||||
set: (v) => (appStore.sidebar.collapse = v),
|
||||
});
|
||||
|
||||
watch(userStore.briefInfo, (val) => {
|
||||
if (val && val.nickname) {
|
||||
userInfo.value = val;
|
||||
}
|
||||
const userInfo = computed<TGApp.App.Account.BriefInfo>(() => {
|
||||
if (briefInfo.value && briefInfo.value.nickname) return briefInfo.value;
|
||||
return {
|
||||
nickname: "未登录",
|
||||
uid: "-1",
|
||||
desc: "请扫码登录",
|
||||
avatar: "/source/UI/lumine.webp",
|
||||
};
|
||||
});
|
||||
|
||||
const rail = ref(appStore.sidebar.collapse);
|
||||
// theme
|
||||
const themeGet = computed({
|
||||
get() {
|
||||
return appStore.theme;
|
||||
},
|
||||
set(value: string) {
|
||||
appStore.theme = value;
|
||||
},
|
||||
const themeGet = computed<string>({
|
||||
get: () => appStore.theme,
|
||||
set: (v) => (appStore.theme = v),
|
||||
});
|
||||
const themeTitle = computed(() => {
|
||||
return themeGet.value === "default" ? "夜间模式" : "日间模式";
|
||||
});
|
||||
|
||||
watch(themeTitle, async (val) => {
|
||||
const themeNow = val === "夜间模式" ? "浅色模式" : "深色模式";
|
||||
await TGLogger.Info(`[App][theme] 已切换到${themeNow}`);
|
||||
});
|
||||
|
||||
function collapse(): void {
|
||||
rail.value = !rail.value;
|
||||
appStore.sidebar.collapse = rail.value;
|
||||
}
|
||||
|
||||
let themeListener: UnlistenFn;
|
||||
const themeTitle = computed<string>(() => (themeGet.value === "default" ? "夜间模式" : "日间模式"));
|
||||
|
||||
onMounted(async () => {
|
||||
themeListener = await event.listen("readTheme", (e: Event<string>) => {
|
||||
const theme = e.payload;
|
||||
themeGet.value = theme === "default" ? "default" : "dark";
|
||||
});
|
||||
if (webviewWindow.getCurrentWebviewWindow().label === "TeyvatGuide") {
|
||||
await mhyClient.run();
|
||||
}
|
||||
if (userStore.briefInfo.value && userStore.briefInfo.value.nickname) {
|
||||
userInfo.value = userStore.briefInfo.value;
|
||||
}
|
||||
if (webviewWindow.getCurrentWebviewWindow().label === "TeyvatGuide") await mhyClient.run();
|
||||
});
|
||||
|
||||
async function switchTheme(): Promise<void> {
|
||||
@@ -303,21 +267,16 @@ async function switchTheme(): Promise<void> {
|
||||
}
|
||||
|
||||
async function openClient(func: string): Promise<void> {
|
||||
if (appStore.isLogin) {
|
||||
await mhyClient.open(func);
|
||||
} else {
|
||||
login();
|
||||
if (appStore.isLogin) await mhyClient.open(func);
|
||||
else showSnackbar.warn("请前往设置页面登录!");
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
if (themeListener !== null) {
|
||||
themeListener();
|
||||
themeListener = null;
|
||||
}
|
||||
}
|
||||
|
||||
function login(): void {
|
||||
showSnackbar({
|
||||
text: "请前往设置页面登录!",
|
||||
color: "warn",
|
||||
});
|
||||
}
|
||||
|
||||
onUnmounted(() => themeListener());
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
<template>
|
||||
<div class="tsl-box">
|
||||
<img src="../../assets/icons/arrow-right.svg" alt="right" />
|
||||
<slot>{{ props.title }}</slot>
|
||||
<slot>{{ title }}</slot>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
const props = defineProps<{
|
||||
title?: string;
|
||||
}>();
|
||||
defineProps<{ title?: string }>();
|
||||
</script>
|
||||
<style lang="css" scoped>
|
||||
.tsl-box {
|
||||
|
||||
@@ -9,25 +9,19 @@
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { event } from "@tauri-apps/api";
|
||||
import { UnlistenFn, Event } from "@tauri-apps/api/event";
|
||||
import { computed, onMounted, onUnmounted } from "vue";
|
||||
import { Event, UnlistenFn } from "@tauri-apps/api/event";
|
||||
import { computed, onMounted, onUnmounted, shallowRef } from "vue";
|
||||
|
||||
import { useAppStore } from "../../store/modules/app.js";
|
||||
|
||||
const appStore = useAppStore();
|
||||
const themeGet = computed({
|
||||
get() {
|
||||
return appStore.theme;
|
||||
},
|
||||
set(value: string) {
|
||||
appStore.theme = value;
|
||||
},
|
||||
const themeGet = computed<string>({
|
||||
get: () => appStore.theme,
|
||||
set: (v: string) => (appStore.theme = v),
|
||||
});
|
||||
let themeListener: UnlistenFn | null = null;
|
||||
const themeListener = shallowRef<UnlistenFn | null>(null);
|
||||
|
||||
onMounted(async () => {
|
||||
themeListener = await listenOnTheme();
|
||||
});
|
||||
onMounted(async () => (themeListener.value = await listenOnTheme()));
|
||||
|
||||
async function switchTheme(): Promise<void> {
|
||||
appStore.changeTheme();
|
||||
@@ -42,9 +36,9 @@ async function listenOnTheme(): Promise<UnlistenFn> {
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
if (themeListener !== null) {
|
||||
themeListener();
|
||||
themeListener = null;
|
||||
if (themeListener.value !== null) {
|
||||
themeListener.value();
|
||||
themeListener.value = null;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
<template>
|
||||
<TOverlay
|
||||
v-model="visible"
|
||||
:hide="true"
|
||||
:to-click="onCancel"
|
||||
blur-val="20px"
|
||||
class="tolc-overlay"
|
||||
>
|
||||
<TOverlay v-model="visible" class="tolc-overlay">
|
||||
<div class="tolc-box">
|
||||
<div class="tolc-title">
|
||||
<span>兑换码</span>
|
||||
@@ -58,35 +52,31 @@ import showSnackbar from "../func/snackbar.js";
|
||||
|
||||
import TOverlay from "./t-overlay.vue";
|
||||
|
||||
interface ToLiveCodeProps {
|
||||
type ToLiveCodeProps = {
|
||||
data: TGApp.BBS.Navigator.CodeData[];
|
||||
actId: string | undefined;
|
||||
modelValue: boolean;
|
||||
}
|
||||
|
||||
type ToLiveCodeEmits = (e: "update:modelValue", value: boolean) => void;
|
||||
};
|
||||
type ToLiveCodeEmits = (e: "update:modelValue", v: boolean) => void;
|
||||
|
||||
const props = withDefaults(defineProps<ToLiveCodeProps>(), { modelValue: false });
|
||||
const emits = defineEmits<ToLiveCodeEmits>();
|
||||
|
||||
const visible = computed<boolean>({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => {
|
||||
emits("update:modelValue", value);
|
||||
},
|
||||
set: (v) => emits("update:modelValue", v),
|
||||
});
|
||||
|
||||
function onCancel(): void {
|
||||
visible.value = false;
|
||||
}
|
||||
|
||||
function copy(code: string): void {
|
||||
navigator.clipboard.writeText(code);
|
||||
showSnackbar.success("已复制到剪贴板");
|
||||
}
|
||||
|
||||
async function shareImg(): Promise<void> {
|
||||
const element = <HTMLElement>document.querySelector(".tolc-box");
|
||||
const element = document.querySelector<HTMLElement>(".tolc-box");
|
||||
if (element === null) {
|
||||
showSnackbar.error("未获取到分享内容");
|
||||
return;
|
||||
}
|
||||
const fileName = `LiveCode_${props.actId}_${new Date().getTime()}`;
|
||||
await generateShareImg(fileName, element, 2);
|
||||
}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
<template>
|
||||
<TOverlay v-model="visible" hide :to-click="onCancel" blur-val="20px">
|
||||
<TOverlay v-model="visible">
|
||||
<div v-if="props.data" class="ton-container">
|
||||
<slot name="left"></slot>
|
||||
<div class="ton-box">
|
||||
<img alt="bg" class="ton-bg" v-if="props.data" :src="props.data.profile" />
|
||||
<div class="ton-content">
|
||||
<span>{{ props.data.name }}</span>
|
||||
<span>{{ parseNamecard(props.data.desc) }}</span>
|
||||
<span>{{ parseNameCard(props.data.desc) }}</span>
|
||||
<span>获取途径:{{ props.data.source }}</span>
|
||||
</div>
|
||||
<div class="ton-type">{{ getType }}</div>
|
||||
<v-btn
|
||||
class="ton-share"
|
||||
@click="shareNamecard"
|
||||
@click="shareNameCard"
|
||||
variant="outlined"
|
||||
:loading="loading"
|
||||
data-html2canvas-ignore
|
||||
@@ -29,15 +29,11 @@
|
||||
import { computed, ref } from "vue";
|
||||
|
||||
import { generateShareImg } from "../../utils/TGShare.js";
|
||||
import showSnackbar from "../func/snackbar.js";
|
||||
|
||||
import TOverlay from "./t-overlay.vue";
|
||||
|
||||
interface ToNamecardProps {
|
||||
modelValue: boolean;
|
||||
data?: TGApp.App.NameCard.Item;
|
||||
}
|
||||
|
||||
enum ToNamecardTypeEnum {
|
||||
enum ToNameCardTypeEnum {
|
||||
other = 0,
|
||||
achievement = 1,
|
||||
role = 2,
|
||||
@@ -46,55 +42,32 @@ enum ToNamecardTypeEnum {
|
||||
unknown = 5,
|
||||
}
|
||||
|
||||
type ToNamecardTypeMap = {
|
||||
[key in ToNamecardTypeEnum]: string;
|
||||
type ToNameCardTypeMap = { [key in ToNameCardTypeEnum]: string };
|
||||
type ToNameCardProps = { modelValue: boolean; data?: TGApp.App.NameCard.Item };
|
||||
type ToNameCardEmits = (e: "update:modelValue", v: boolean) => void;
|
||||
const props = defineProps<ToNameCardProps>();
|
||||
const emits = defineEmits<ToNameCardEmits>();
|
||||
const typeMap: ToNameCardTypeMap = {
|
||||
0: "其他",
|
||||
1: "成就",
|
||||
2: "角色",
|
||||
3: "纪行",
|
||||
4: "活动",
|
||||
5: "未知",
|
||||
};
|
||||
|
||||
const typeMap: ToNamecardTypeMap = {
|
||||
[ToNamecardTypeEnum.other]: "其他",
|
||||
[ToNamecardTypeEnum.achievement]: "成就",
|
||||
[ToNamecardTypeEnum.role]: "角色",
|
||||
[ToNamecardTypeEnum.record]: "纪行",
|
||||
[ToNamecardTypeEnum.activity]: "活动",
|
||||
[ToNamecardTypeEnum.unknown]: "未知",
|
||||
};
|
||||
|
||||
type ToNamecardEmits = (e: "update:modelValue", value: boolean) => void;
|
||||
|
||||
const props = defineProps<ToNamecardProps>();
|
||||
|
||||
const emits = defineEmits<ToNamecardEmits>();
|
||||
|
||||
const loading = ref<boolean>(false);
|
||||
|
||||
const getType = computed(() => {
|
||||
if (!props.data) return typeMap[ToNamecardTypeEnum.unknown];
|
||||
switch (props.data.type) {
|
||||
case ToNamecardTypeEnum.achievement:
|
||||
return typeMap[ToNamecardTypeEnum.achievement];
|
||||
case ToNamecardTypeEnum.role:
|
||||
return typeMap[ToNamecardTypeEnum.role];
|
||||
case ToNamecardTypeEnum.record:
|
||||
return typeMap[ToNamecardTypeEnum.record];
|
||||
case ToNamecardTypeEnum.activity:
|
||||
return typeMap[ToNamecardTypeEnum.activity];
|
||||
default:
|
||||
return typeMap[ToNamecardTypeEnum.other];
|
||||
}
|
||||
});
|
||||
|
||||
const visible = computed({
|
||||
const visible = computed<boolean>({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => {
|
||||
emits("update:modelValue", value);
|
||||
},
|
||||
set: (v) => emits("update:modelValue", v),
|
||||
});
|
||||
const getType = computed<string>(() => {
|
||||
if (!props.data) return typeMap[ToNameCardTypeEnum.unknown];
|
||||
if (!(props.data.type satisfies ToNameCardTypeEnum)) return typeMap[5];
|
||||
const type: ToNameCardTypeEnum = props.data.type;
|
||||
return typeMap[type];
|
||||
});
|
||||
|
||||
function onCancel() {
|
||||
visible.value = false;
|
||||
}
|
||||
|
||||
function parseNamecard(desc: string): string {
|
||||
function parseNameCard(desc: string): string {
|
||||
let array = [];
|
||||
if (desc.startsWith("名片纹饰。「") && desc.endsWith("」")) {
|
||||
array.push("名片纹饰。");
|
||||
@@ -104,22 +77,19 @@ function parseNamecard(desc: string): string {
|
||||
for (const item of match) {
|
||||
if (item.length <= 34) {
|
||||
array.push(item);
|
||||
} else {
|
||||
array.push("「");
|
||||
array.push(...parseDesc(item.slice(1, -1), true));
|
||||
const maxLength = Math.max(...array.map((item) => item.length));
|
||||
array.push(" ".repeat(maxLength - 4) + "」");
|
||||
continue;
|
||||
}
|
||||
array.push("「");
|
||||
array.push(...parseDesc(item.slice(1, -1), true));
|
||||
const maxLength = Math.max(...array.map((item) => item.length));
|
||||
array.push(" ".repeat(maxLength - 4) + "」");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
array.push("名片纹饰。");
|
||||
const content = desc.slice(5);
|
||||
if (content.length <= 32) {
|
||||
array.push(content);
|
||||
} else {
|
||||
array.push(...parseDesc(content));
|
||||
}
|
||||
if (content.length <= 32) array.push(content);
|
||||
else array.push(...parseDesc(content));
|
||||
}
|
||||
const res = array.join("\n");
|
||||
if (!res.endsWith("\n")) return res + "\n";
|
||||
@@ -136,31 +106,31 @@ function parseDesc(desc: string, inQuote: boolean = false): string[] {
|
||||
res = res.replace("时候,", "时候,\n");
|
||||
res = res.replace("。\n」", "。」");
|
||||
}
|
||||
if (!desc.includes("!」")) {
|
||||
res = res.replace(/!/g, "!\n");
|
||||
}
|
||||
if (!desc.includes("!」")) res = res.replace(/!/g, "!\n");
|
||||
res = res.replace(/…/g, "…\n");
|
||||
const match = res.split("\n");
|
||||
let array: string[] = [];
|
||||
for (const item of match) {
|
||||
if (item.length > 0 && item.length <= 32) {
|
||||
array.push(item);
|
||||
} else {
|
||||
const match2 = item.replace(/,/g, ",\n").split("\n");
|
||||
for (const item2 of match2) {
|
||||
if (item2.length > 0) array.push(item2);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const match2 = item.replace(/,/g, ",\n").split("\n");
|
||||
match2.map((i) => (i.length > 0 ? array.push(i) : null));
|
||||
}
|
||||
if (inQuote) array = array.map((item) => ` ${item}`);
|
||||
return array;
|
||||
}
|
||||
|
||||
async function shareNamecard(): Promise<void> {
|
||||
const namecardBox = <HTMLElement>document.querySelector(".ton-box");
|
||||
async function shareNameCard(): Promise<void> {
|
||||
const nameCardBox = document.querySelector<HTMLElement>(".ton-box");
|
||||
if (nameCardBox === null) {
|
||||
showSnackbar.error("未找到名片内容");
|
||||
return;
|
||||
}
|
||||
const fileName = `【${getType.value}名片】-${props.data?.name}`;
|
||||
loading.value = true;
|
||||
await generateShareImg(fileName, namecardBox);
|
||||
await generateShareImg(fileName, nameCardBox);
|
||||
loading.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<v-list class="top-nc-box" @click="toNameCard(props.data)">
|
||||
<v-list class="top-nc-box" @click="emit('selected', props.data)">
|
||||
<v-list-item :title="props.data.name">
|
||||
<template #subtitle>
|
||||
<span :title="props.data.desc">{{ props.data.desc }}</span>
|
||||
@@ -13,13 +13,8 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from "vue";
|
||||
|
||||
interface TopNameCardProps {
|
||||
data: TGApp.App.NameCard.Item;
|
||||
}
|
||||
|
||||
interface TopNameCardEmits {
|
||||
(e: "selected", data: TGApp.App.NameCard.Item): void;
|
||||
}
|
||||
type TopNameCardProps = { data: TGApp.App.NameCard.Item };
|
||||
type TopNameCardEmits = (e: "selected", v: TGApp.App.NameCard.Item) => void;
|
||||
|
||||
const props = defineProps<TopNameCardProps>();
|
||||
const emit = defineEmits<TopNameCardEmits>();
|
||||
@@ -28,10 +23,6 @@ const bgImage = computed<string>(() => {
|
||||
if (props.data.name === "原神·印象") return "none;";
|
||||
return `url("${props.data.bg}")`;
|
||||
});
|
||||
|
||||
function toNameCard(item: TGApp.App.NameCard.Item) {
|
||||
emit("selected", item);
|
||||
}
|
||||
</script>
|
||||
<style lang="css" scoped>
|
||||
.top-nc-box {
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
</transition>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, useTemplateRef } from "vue";
|
||||
import { ref, useTemplateRef, watch } from "vue";
|
||||
|
||||
const show = ref<boolean>(false);
|
||||
const showOuter = ref<boolean>(false);
|
||||
@@ -36,11 +36,15 @@ watch(
|
||||
},
|
||||
);
|
||||
|
||||
declare function initGeetest(
|
||||
params: TGApp.Plugins.Mys.Geetest.InitGeetestParams,
|
||||
callback: (captchaObj: TGApp.Plugins.Mys.Geetest.GeetestCaptcha) => void,
|
||||
): void;
|
||||
|
||||
async function displayBox(
|
||||
props: TGApp.Plugins.Mys.Geetest.reqResp,
|
||||
): Promise<TGApp.Plugins.Mys.Geetest.validateResp | false> {
|
||||
return await new Promise<TGApp.Plugins.Mys.Geetest.validateResp>((resolve) => {
|
||||
// eslint-disable-next-line no-undef
|
||||
initGeetest(
|
||||
{
|
||||
gt: props.gt,
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
</transition>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref, watch, onMounted, toRaw } from "vue";
|
||||
import { ref, watch, onMounted, toRaw } from "vue";
|
||||
|
||||
import { LoadingParams } from "./loading.js";
|
||||
|
||||
@@ -32,7 +32,7 @@ const showOuter = ref<boolean>(false);
|
||||
const showInner = ref<boolean>(false);
|
||||
|
||||
const props = defineProps<LoadingParams>();
|
||||
const data = reactive<LoadingParams>(toRaw(props));
|
||||
const data = ref<LoadingParams>(toRaw(props));
|
||||
|
||||
watch(
|
||||
() => showBox.value,
|
||||
@@ -53,15 +53,15 @@ function displayBox(params: LoadingParams): void {
|
||||
if (!params.show) {
|
||||
showBox.value = false;
|
||||
setTimeout(() => {
|
||||
data.title = "";
|
||||
data.subtitle = "";
|
||||
data.empty = false;
|
||||
data.value.title = "";
|
||||
data.value.subtitle = "";
|
||||
data.value.empty = false;
|
||||
}, 500);
|
||||
return;
|
||||
}
|
||||
data.title = params.title;
|
||||
data.subtitle = params.subtitle;
|
||||
data.empty = params.empty;
|
||||
data.value.title = params.title;
|
||||
data.value.subtitle = params.subtitle;
|
||||
data.value.empty = params.empty;
|
||||
showBox.value = true;
|
||||
}
|
||||
|
||||
@@ -180,7 +180,6 @@ defineExpose({ displayBox });
|
||||
.loading-circle > div {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.loading-circle {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<TOverlay v-model="visible" hide :to-click="onCancel" blur-val="20px">
|
||||
<TOverlay v-model="visible">
|
||||
<div class="hta-oo-box">
|
||||
<v-btn
|
||||
:loading="loadShare"
|
||||
@@ -59,41 +59,34 @@ import { AbyssDataItem } from "../../pages/WIKI/Abyss.vue";
|
||||
import { generateShareImg } from "../../utils/TGShare.js";
|
||||
import { timestampToDate } from "../../utils/toolFunc.js";
|
||||
import TOverlay from "../app/t-overlay.vue";
|
||||
import showSnackbar from "../func/snackbar.js";
|
||||
|
||||
import HtaOverviewLine from "./hta-overview-line.vue";
|
||||
|
||||
interface HtaOverlayOverviewProps {
|
||||
type HtaOverlayOverviewProps = {
|
||||
modelValue: boolean;
|
||||
data: AbyssDataItem<TGApp.Plugins.Hutao.Abyss.OverviewData>;
|
||||
}
|
||||
|
||||
interface HtaOverlayOverviewEmits {
|
||||
(e: "update:modelValue", value: boolean): void;
|
||||
|
||||
(e: "cancel"): void;
|
||||
}
|
||||
};
|
||||
type HtaOverlayOverviewEmits = (e: "update:modelValue", v: boolean) => void;
|
||||
|
||||
const props = defineProps<HtaOverlayOverviewProps>();
|
||||
const emits = defineEmits<HtaOverlayOverviewEmits>();
|
||||
const loadShare = ref<boolean>(false);
|
||||
const visible = computed<boolean>({
|
||||
get: () => props.modelValue,
|
||||
set: (v) => emits("update:modelValue", v),
|
||||
});
|
||||
const dataCur = computed(() => props.data.cur);
|
||||
const dataLast = computed(() => props.data.last);
|
||||
const loadShare = ref<boolean>(false);
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => {
|
||||
emits("update:modelValue", value);
|
||||
},
|
||||
});
|
||||
|
||||
function onCancel(): void {
|
||||
visible.value = false;
|
||||
emits("cancel");
|
||||
}
|
||||
|
||||
async function share(): Promise<void> {
|
||||
loadShare.value = true;
|
||||
const shareEl = <HTMLElement>document.querySelector(".hta-oo-box");
|
||||
const shareEl = document.querySelector<HTMLElement>(".hta-oo-box");
|
||||
if (shareEl === null) {
|
||||
showSnackbar.warn("分享失败");
|
||||
loadShare.value = false;
|
||||
return;
|
||||
}
|
||||
const fileName = `深渊数据统计_${timestampToDate(dataCur.value.Timestamp)}.png`;
|
||||
await generateShareImg(fileName, shareEl, 2);
|
||||
loadShare.value = false;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<TOverlay v-model="visible" hide :to-click="onCancel" blur-val="5px">
|
||||
<TOverlay v-model="visible" blur-val="5px">
|
||||
<div class="toab-container" v-if="props.data">
|
||||
<div class="toab-img">
|
||||
<img :src="props.data.take_picture[Number(props.choice)]" alt="顶部图像" />
|
||||
@@ -29,61 +29,39 @@
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { fetch } from "@tauri-apps/plugin-http";
|
||||
import { computed, onMounted, ref, watch } from "vue";
|
||||
import { computed, onMounted, ref, shallowRef, watch } from "vue";
|
||||
import { xml2json } from "xml-js";
|
||||
|
||||
import TGLogger from "../../utils/TGLogger.js";
|
||||
import { copyToClipboard, getImageBuffer, saveCanvasImg } from "../../utils/TGShare.js";
|
||||
import { bytesToSize } from "../../utils/toolFunc.js";
|
||||
import TOverlay from "../app/t-overlay.vue";
|
||||
import showSnackbar from "../func/snackbar.js";
|
||||
|
||||
interface ToArcBirthProps {
|
||||
type ToArcBirthProps = {
|
||||
modelValue: boolean;
|
||||
data?: TGApp.Archive.Birth.DrawItem;
|
||||
choice: boolean;
|
||||
}
|
||||
|
||||
interface ToArcBirthEmits {
|
||||
(event: "update:modelValue", value: boolean): void;
|
||||
}
|
||||
};
|
||||
type ToArcBirthEmits = (e: "update:modelValue", v: boolean) => void;
|
||||
type XmlKeyMap = { id: string; rel: string; group?: string; icon: string };
|
||||
type XmlTextList = { chara: string; img: string; text: string };
|
||||
type XmlTextParse = { name: string; icon?: string; text: string };
|
||||
|
||||
const props = defineProps<ToArcBirthProps>();
|
||||
const emits = defineEmits<ToArcBirthEmits>();
|
||||
const buffer = ref<Uint8Array | null>(null);
|
||||
const showText = ref(false);
|
||||
const textParse = ref<Array<XmlTextParse>>([]);
|
||||
|
||||
const visible = computed({
|
||||
const showText = ref<boolean>(false);
|
||||
const textParse = shallowRef<Array<XmlTextParse>>([]);
|
||||
const visible = computed<boolean>({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => {
|
||||
emits("update:modelValue", value);
|
||||
},
|
||||
set: (v) => emits("update:modelValue", v),
|
||||
});
|
||||
onMounted(() => clearData());
|
||||
watch(() => props.data, clearData);
|
||||
watch(() => props.choice, clearData);
|
||||
|
||||
onMounted(loadData);
|
||||
watch(() => props.data, loadData);
|
||||
watch(() => props.choice, loadData);
|
||||
|
||||
interface XmlKeyMap {
|
||||
id: string;
|
||||
rel: string;
|
||||
group?: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
interface XmlTextList {
|
||||
chara: string;
|
||||
img: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
interface XmlTextParse {
|
||||
name: string;
|
||||
icon?: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
function loadData() {
|
||||
function clearData(): void {
|
||||
buffer.value = null;
|
||||
textParse.value = [];
|
||||
showText.value = false;
|
||||
@@ -113,68 +91,80 @@ async function loadText(): Promise<void> {
|
||||
showText.value = !showText.value;
|
||||
return;
|
||||
}
|
||||
const resSource: any = await parseXml(props.data.gal_resource);
|
||||
const resSource: unknown = await parseXml(props.data.gal_resource);
|
||||
if (resSource === false) {
|
||||
showSnackbar.warn("对白数据加载失败");
|
||||
return;
|
||||
}
|
||||
const keyMap: XmlKeyMap[] = resSource["elements"][0]["elements"][0]["elements"]
|
||||
.map((item: any) => {
|
||||
if (item["name"] === "chara")
|
||||
return <XmlKeyMap>{
|
||||
id: item["attributes"]["id"],
|
||||
rel: item["attributes"]["rel"],
|
||||
group: item["attributes"]["group"],
|
||||
icon: item["attributes"]["src"],
|
||||
};
|
||||
})
|
||||
.filter((item: any) => item !== undefined);
|
||||
const resXml = await parseXml(props.data.gal_xml);
|
||||
const textList: XmlTextList[] = resXml["elements"][0]["elements"][0]["elements"][0]["elements"]
|
||||
.map((item: any) => {
|
||||
if (item["name"] === "simple_dialog") {
|
||||
let img = item["attributes"]["img"];
|
||||
if (!props.choice && img) img = img.replace("aether", "lumine");
|
||||
return <XmlTextList>{
|
||||
chara: item["attributes"]["chara"],
|
||||
img: img,
|
||||
text: item["elements"][0]["text"],
|
||||
};
|
||||
}
|
||||
})
|
||||
.filter((item: any) => item !== undefined);
|
||||
textParse.value = textList.map((item: XmlTextList) => {
|
||||
const key = keyMap.find((keyItem: XmlKeyMap) => keyItem.id === item.img);
|
||||
if (!key) {
|
||||
return {
|
||||
name: "未知",
|
||||
text: item.text,
|
||||
};
|
||||
}
|
||||
return {
|
||||
name: key.group ?? key.id,
|
||||
text: item.text,
|
||||
icon: key.icon,
|
||||
};
|
||||
const keyMap = getKeyMap(resSource);
|
||||
const resXml: unknown = await parseXml(props.data.gal_xml);
|
||||
const textList = getTextList(resXml);
|
||||
textParse.value = textList.map((item) => {
|
||||
const key = keyMap.find((keyItem) => keyItem.id === item.img);
|
||||
if (!key) return { name: "未知", text: item.text };
|
||||
return { name: key.group ?? key.id, text: item.text, icon: key.icon };
|
||||
});
|
||||
showText.value = true;
|
||||
}
|
||||
|
||||
async function parseXml(link: string) {
|
||||
function getKeyMap(resSource: unknown): XmlKeyMap[] {
|
||||
const res: XmlKeyMap[] = [];
|
||||
if (!resSource || typeof resSource !== "object") return res;
|
||||
if (!("elements" in resSource) || !Array.isArray(resSource["elements"])) return res;
|
||||
const arr1 = resSource.elements;
|
||||
if (arr1.length === 0 || !("elements" in arr1[0]) || !Array.isArray(arr1[0].elements)) return res;
|
||||
const arr2 = arr1[0].elements;
|
||||
if (arr2.length === 0 || !("elements" in arr2[0]) || !Array.isArray(arr2[0].elements)) return res;
|
||||
const arr3 = arr2[0].elements;
|
||||
for (const item of arr3) {
|
||||
if (!("name" in item)) continue;
|
||||
if (!("attributes" in item)) continue;
|
||||
const attr = item.attributes;
|
||||
if (!("id" in attr) || !("rel" in attr) || !("group" in attr) || !("src" in attr)) continue;
|
||||
if (item.name !== "chara") continue;
|
||||
res.push({ id: attr.id, rel: attr.rel, group: attr.group, icon: attr.src });
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
function getTextList(resXml: unknown): XmlTextList[] {
|
||||
const res: XmlTextList[] = [];
|
||||
if (!resXml || typeof resXml !== "object") return res;
|
||||
if (!("elements" in resXml) || !Array.isArray(resXml["elements"])) return res;
|
||||
const arr1 = resXml.elements;
|
||||
if (arr1.length === 0 || !("elements" in arr1[0]) || !Array.isArray(arr1[0].elements)) return res;
|
||||
const arr2 = arr1[0].elements;
|
||||
if (arr2.length === 0 || !("elements" in arr2[0]) || !Array.isArray(arr2[0].elements)) return res;
|
||||
const arr3 = arr2[0].elements;
|
||||
if (arr3.length === 0 || !("elements" in arr3[0]) || !Array.isArray(arr3[0].elements)) return res;
|
||||
const arr4 = arr3[0].elements;
|
||||
for (const item of arr4) {
|
||||
if (!("name" in item)) continue;
|
||||
if (!("attributes" in item)) continue;
|
||||
if (!("elements" in item) || !Array.isArray(item.elements)) continue;
|
||||
const attr = item.attributes;
|
||||
if (!("chara" in attr) || !("img" in attr)) continue;
|
||||
if (item.name !== "simple_dialog") continue;
|
||||
const img = props.choice ? attr.img : attr.img.replace("aether", "lumine");
|
||||
res.push({ chara: attr.chara, img: img, text: item.elements[0].text });
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
async function parseXml(link: string): Promise<false | unknown> {
|
||||
try {
|
||||
const response = await fetch(link, { method: "GET" });
|
||||
const data = await response.arrayBuffer();
|
||||
const xml = new TextDecoder("utf-8").decode(data);
|
||||
return JSON.parse(xml2json(xml));
|
||||
return JSON.parse(xml2json(new TextDecoder("utf-8").decode(data)));
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
if (error instanceof Error) {
|
||||
await TGLogger.Error(`[to-arcBirth] parseXml: ${error.message}`);
|
||||
} else {
|
||||
await TGLogger.Error(`[to-arcBirth] parseXml: 未知错误-${error}`);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function onCancel() {
|
||||
visible.value = false;
|
||||
}
|
||||
</script>
|
||||
<style lang="css" scoped>
|
||||
.toab-container {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<TOverlay v-model="visible" hide :to-click="onCancel" blur-val="20px">
|
||||
<TOverlay v-model="visible">
|
||||
<div class="tocp-container">
|
||||
<div class="tocp-title">选择分类</div>
|
||||
<div class="tocp-list">
|
||||
@@ -15,55 +15,38 @@
|
||||
</div>
|
||||
<div class="tocp-bottom">
|
||||
<v-btn class="tocp-btn" rounded @click="newCollect">新建分类</v-btn>
|
||||
<v-btn class="tocp-btn" rounded @click="onCancel">取消</v-btn>
|
||||
<v-btn class="tocp-btn" rounded @click="visible = false">取消</v-btn>
|
||||
<v-btn :loading="submit" class="tocp-btn" rounded @click="onSubmit">确定</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</TOverlay>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { computed, ref, shallowRef, watch } from "vue";
|
||||
|
||||
import TSUserCollection from "../../plugins/Sqlite/modules/userCollect.js";
|
||||
import TOverlay from "../app/t-overlay.vue";
|
||||
import showDialog from "../func/dialog.js";
|
||||
import showSnackbar from "../func/snackbar.js";
|
||||
|
||||
interface ToPostCollectProps {
|
||||
modelValue: boolean;
|
||||
post: string[];
|
||||
}
|
||||
|
||||
interface ToPostCollectEmits {
|
||||
(e: "update:modelValue", value: boolean): void;
|
||||
|
||||
type ToPostCollectProps = { modelValue: boolean; post: string[] };
|
||||
type ToPostCollectEmits = {
|
||||
(e: "update:modelValue", v: boolean): void;
|
||||
(e: "submit"): void;
|
||||
}
|
||||
};
|
||||
|
||||
const props = defineProps<ToPostCollectProps>();
|
||||
const emits = defineEmits<ToPostCollectEmits>();
|
||||
const select = ref<string>();
|
||||
const collectList = ref<TGApp.Sqlite.UserCollection.UFCollection[]>([]);
|
||||
const submit = ref(false);
|
||||
|
||||
const visible = computed({
|
||||
const submit = ref<boolean>(false);
|
||||
const collectList = shallowRef<TGApp.Sqlite.UserCollection.UFCollection[]>([]);
|
||||
const visible = computed<boolean>({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => {
|
||||
emits("update:modelValue", value);
|
||||
},
|
||||
set: (v) => emits("update:modelValue", v),
|
||||
});
|
||||
|
||||
function onCancel() {
|
||||
visible.value = false;
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
async (val) => {
|
||||
if (val) {
|
||||
await freshData();
|
||||
}
|
||||
},
|
||||
async (val) => (val ? await freshData() : null),
|
||||
);
|
||||
|
||||
async function onSubmit(): Promise<void> {
|
||||
@@ -83,7 +66,6 @@ async function onSubmit(): Promise<void> {
|
||||
}
|
||||
showSnackbar.success(`成功处理 ${props.post.length} 个帖子`);
|
||||
submit.value = false;
|
||||
visible.value = false;
|
||||
emits("submit");
|
||||
}
|
||||
|
||||
|
||||
@@ -532,6 +532,7 @@ async function clearUser(user: TGApp.App.Account.User): Promise<void> {
|
||||
.tcu-box {
|
||||
border-radius: 10px;
|
||||
background-image: linear-gradient(to right, #f78ca0 0%, #f9748f 19%, #fd868c 60%, #fe9a8b 100%);
|
||||
color: var(--tgc-white-1);
|
||||
}
|
||||
|
||||
.tcu-btn {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<TOverlay v-model="visible" :to-click="onCancel" blur-val="20px" hide>
|
||||
<TOverlay v-model="visible">
|
||||
<div class="toc-box">
|
||||
<div class="box-div">
|
||||
<div class="toc-top">
|
||||
<TItemBox :model-value="getBoxData()" />
|
||||
<TItemBox :model-value="boxData" />
|
||||
<div class="toc-material-grid">
|
||||
<TibCalendarMaterial
|
||||
v-for="(item, index) in itemVal.materials"
|
||||
@@ -31,76 +31,47 @@ import { useRouter } from "vue-router";
|
||||
|
||||
import TItemBox, { TItemBoxData } from "../app/t-item-box.vue";
|
||||
import TOverlay from "../app/t-overlay.vue";
|
||||
import showSnackbar from "../func/snackbar.js";
|
||||
|
||||
import TibCalendarMaterial from "./ph-calendar-material.vue";
|
||||
|
||||
interface ToCalendarProps {
|
||||
type ToCalendarProps = {
|
||||
modelValue: boolean;
|
||||
dataType: "weapon" | "avatar";
|
||||
dataVal: TGApp.App.Calendar.Item;
|
||||
}
|
||||
};
|
||||
type ToCalendarEmits = (e: "update:modelValue", v: boolean) => void;
|
||||
|
||||
interface ToCalendarEmits {
|
||||
(e: "update:modelValue", value: boolean): void;
|
||||
|
||||
(e: "cancel"): void;
|
||||
}
|
||||
|
||||
const emits = defineEmits<ToCalendarEmits>();
|
||||
const props = defineProps<ToCalendarProps>();
|
||||
|
||||
const emits = defineEmits<ToCalendarEmits>();
|
||||
const router = useRouter();
|
||||
|
||||
const visible = computed({
|
||||
const visible = computed<boolean>({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => {
|
||||
emits("update:modelValue", value);
|
||||
},
|
||||
set: (v) => emits("update:modelValue", v),
|
||||
});
|
||||
const itemVal = computed<TGApp.App.Calendar.Item>(() => props.dataVal);
|
||||
|
||||
const onCancel = (): void => {
|
||||
visible.value = false;
|
||||
emits("cancel");
|
||||
};
|
||||
const boxData = computed<TItemBoxData>(() => {
|
||||
return {
|
||||
bg: itemVal.value.bg,
|
||||
icon: itemVal.value.icon,
|
||||
size: "100px",
|
||||
height: "100px",
|
||||
display: "inner",
|
||||
clickable: false,
|
||||
lt: props.dataType === "avatar" ? (itemVal.value.elementIcon ?? "") : itemVal.value.weaponIcon,
|
||||
ltSize: "20px",
|
||||
innerHeight: 25,
|
||||
innerIcon: props.dataType === "avatar" ? itemVal.value.weaponIcon : undefined,
|
||||
innerText: itemVal.value.name,
|
||||
};
|
||||
});
|
||||
|
||||
async function toDetail(item: TGApp.App.Calendar.Item): Promise<void> {
|
||||
if (item.itemType === "character") {
|
||||
await router.push(`/wiki/character/${item.id}`);
|
||||
} else {
|
||||
await router.push(`/wiki/weapon/${item.id}`);
|
||||
}
|
||||
}
|
||||
|
||||
function getBoxData(): TItemBoxData {
|
||||
if (props.dataType === "avatar") {
|
||||
return {
|
||||
bg: props.dataVal.bg,
|
||||
icon: props.dataVal.icon,
|
||||
size: "100px",
|
||||
height: "100px",
|
||||
display: "inner",
|
||||
clickable: false,
|
||||
lt: props.dataVal.elementIcon ?? "",
|
||||
ltSize: "20px",
|
||||
innerHeight: 25,
|
||||
innerIcon: props.dataVal.weaponIcon,
|
||||
innerText: props.dataVal.name,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
bg: props.dataVal.bg,
|
||||
icon: props.dataVal.icon,
|
||||
size: "100px",
|
||||
height: "100px",
|
||||
display: "inner",
|
||||
clickable: false,
|
||||
lt: props.dataVal.weaponIcon,
|
||||
ltSize: "20px",
|
||||
innerHeight: 25,
|
||||
innerText: props.dataVal.name,
|
||||
};
|
||||
if (!["character", "weapon"].includes(item.itemType)) {
|
||||
showSnackbar.error("未知类型");
|
||||
return;
|
||||
}
|
||||
await router.push(`/wiki/${item.itemType}/${item.id}`);
|
||||
}
|
||||
</script>
|
||||
<style lang="css" scoped>
|
||||
|
||||
@@ -12,10 +12,7 @@
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
interface THomeCardProps {
|
||||
append?: boolean;
|
||||
}
|
||||
|
||||
type THomeCardProps = { append?: boolean };
|
||||
const props = defineProps<THomeCardProps>();
|
||||
</script>
|
||||
<style lang="css" scoped>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<TOverlay v-model="visible" hide :to-click="onCancel" blur-val="20px">
|
||||
<TOverlay v-model="visible">
|
||||
<div class="toc-box">
|
||||
<div class="toc-top">
|
||||
<div class="toc-title">
|
||||
@@ -9,7 +9,8 @@
|
||||
<div
|
||||
v-for="(item, index) in channelList"
|
||||
:key="index"
|
||||
:class="props.gid === item.gid ? 'toc-list-item active' : 'toc-list-item'"
|
||||
class="toc-list-item"
|
||||
:class="{ active: props.gid === item.gid }"
|
||||
@click="toChannel(item)"
|
||||
>
|
||||
<img :src="item.icon" alt="icon" />
|
||||
@@ -17,7 +18,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="toc-close" @click="onCancel">
|
||||
<div class="toc-close" @click="visible = false">
|
||||
<div class="toc-close-btn">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</div>
|
||||
@@ -26,41 +27,27 @@
|
||||
</TOverlay>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { storeToRefs } from "pinia";
|
||||
import { computed } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
|
||||
import { useAppStore } from "../../store/modules/app.js";
|
||||
import { NewsType, useAppStore } from "../../store/modules/app.js";
|
||||
import { ToChannelItem } from "../../web/constant/bbs.js";
|
||||
import TGConstant from "../../web/constant/TGConstant.js";
|
||||
import TOverlay from "../app/t-overlay.vue";
|
||||
import showSnackbar from "../func/snackbar.js";
|
||||
|
||||
interface ToChannelProps {
|
||||
gid?: string;
|
||||
curType?: string;
|
||||
modelValue: boolean;
|
||||
}
|
||||
|
||||
type ToChannelEmits = (e: "update:modelValue", value: boolean) => void;
|
||||
|
||||
const props = withDefaults(defineProps<ToChannelProps>(), {
|
||||
modelValue: false,
|
||||
});
|
||||
const emits = defineEmits<ToChannelEmits>();
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => {
|
||||
emits("update:modelValue", value);
|
||||
},
|
||||
});
|
||||
type ToChannelProps = { gid?: string; curType?: string; modelValue: boolean };
|
||||
type ToChannelEmits = (e: "update:modelValue", v: boolean) => void;
|
||||
const router = useRouter();
|
||||
const appStore = useAppStore();
|
||||
const props = withDefaults(defineProps<ToChannelProps>(), { modelValue: false });
|
||||
const emits = defineEmits<ToChannelEmits>();
|
||||
const { recentNewsType } = storeToRefs(useAppStore());
|
||||
const channelList = TGConstant.BBS.CHANNELS;
|
||||
|
||||
function onCancel(): void {
|
||||
visible.value = false;
|
||||
}
|
||||
const visible = computed<boolean>({
|
||||
get: () => props.modelValue,
|
||||
set: (v) => emits("update:modelValue", v),
|
||||
});
|
||||
|
||||
async function toChannel(item: ToChannelItem): Promise<void> {
|
||||
if (props.gid === item.gid) {
|
||||
@@ -69,12 +56,11 @@ async function toChannel(item: ToChannelItem): Promise<void> {
|
||||
}
|
||||
visible.value = false;
|
||||
let link = `/news/${item.gid}/{type}`;
|
||||
const typeList = ["notice", "news", "activity"];
|
||||
if (typeList.includes(appStore.recentNewsType)) {
|
||||
link = link.replace("{type}", appStore.recentNewsType);
|
||||
if (recentNewsType.value satisfies NewsType) {
|
||||
link = link.replace("{type}", recentNewsType.value);
|
||||
} else {
|
||||
link = link.replace("{type}", "notice");
|
||||
appStore.recentNewsType = "notice";
|
||||
recentNewsType.value = "notice";
|
||||
}
|
||||
await router.push(link);
|
||||
}
|
||||
@@ -113,26 +99,26 @@ async function toChannel(item: ToChannelItem): Promise<void> {
|
||||
color: var(--box-text-1);
|
||||
cursor: pointer;
|
||||
transition: all 0.5s linear;
|
||||
}
|
||||
|
||||
.toc-list-item.active {
|
||||
border: 1px solid var(--common-shadow-1);
|
||||
background: var(--box-bg-2);
|
||||
color: var(--box-text-2);
|
||||
}
|
||||
&.active {
|
||||
border: 1px solid var(--common-shadow-1);
|
||||
background: var(--box-bg-2);
|
||||
color: var(--box-text-2);
|
||||
}
|
||||
|
||||
.toc-list-item img {
|
||||
width: 45px;
|
||||
height: 45px;
|
||||
margin-right: 10px;
|
||||
border-bottom-left-radius: 5px;
|
||||
border-top-left-radius: 5px;
|
||||
}
|
||||
img {
|
||||
width: 45px;
|
||||
height: 45px;
|
||||
margin-right: 10px;
|
||||
border-bottom-left-radius: 5px;
|
||||
border-top-left-radius: 5px;
|
||||
}
|
||||
|
||||
.toc-list-item span {
|
||||
margin-right: 10px;
|
||||
font-family: var(--font-title);
|
||||
font-size: 16px;
|
||||
span {
|
||||
margin-right: 10px;
|
||||
font-family: var(--font-title);
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.toc-close {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<TOverlay v-model="visible" hide :to-click="onCancel" blur-val="20px">
|
||||
<TOverlay v-model="visible">
|
||||
<div v-if="props.data" class="twom-container">
|
||||
<slot name="left"></slot>
|
||||
<div class="twom-box">
|
||||
@@ -32,37 +32,31 @@ import { computed } from "vue";
|
||||
import { generateShareImg } from "../../utils/TGShare.js";
|
||||
import { parseHtmlText } from "../../utils/toolFunc.js";
|
||||
import TOverlay from "../app/t-overlay.vue";
|
||||
import showSnackbar from "../func/snackbar.js";
|
||||
|
||||
import TwoConvert from "./two-convert.vue";
|
||||
import TwoSource from "./two-source.vue";
|
||||
|
||||
interface TwoMaterialProps {
|
||||
modelValue: boolean;
|
||||
data: TGApp.App.Material.WikiItem;
|
||||
}
|
||||
|
||||
type TwoMaterialEmits = (e: "update:modelValue", value: boolean) => void;
|
||||
|
||||
type TwoMaterialProps = { modelValue: boolean; data: TGApp.App.Material.WikiItem };
|
||||
type TwoMaterialEmits = (e: "update:modelValue", v: boolean) => void;
|
||||
const props = defineProps<TwoMaterialProps>();
|
||||
const emits = defineEmits<TwoMaterialEmits>();
|
||||
|
||||
const visible = computed<boolean>({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emits("update:modelValue", val),
|
||||
set: (v) => emits("update:modelValue", v),
|
||||
});
|
||||
const iconBg = computed<string>(() => {
|
||||
if (!props.data) return "url('/icon/bg/0-BGC.webp')";
|
||||
return `url('/icon/bg/${props.data.star}-BGC.webp')`;
|
||||
});
|
||||
|
||||
const version = await getVersion();
|
||||
|
||||
function onCancel() {
|
||||
visible.value = false;
|
||||
}
|
||||
|
||||
async function shareMaterial(): Promise<void> {
|
||||
const element = <HTMLElement>document.querySelector(".twom-box");
|
||||
const element = document.querySelector<HTMLElement>(".twom-box");
|
||||
if (element === null) {
|
||||
showSnackbar.error("未获取到分享内容");
|
||||
return;
|
||||
}
|
||||
const fileName = `material_${props.data.id}`;
|
||||
await generateShareImg(fileName, element, 1.2, true);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<TOverlay v-model="visible" hide :to-click="onCancel" blur-val="20px">
|
||||
<TOverlay v-model="visible">
|
||||
<div class="two-sc-container">
|
||||
<div class="two-sc-item">
|
||||
<div class="two-sc-title">星级</div>
|
||||
@@ -58,7 +58,7 @@
|
||||
</v-item-group>
|
||||
</div>
|
||||
<div class="tow-sc-submit">
|
||||
<v-btn variant="tonal" @click="onCancel">取消</v-btn>
|
||||
<v-btn variant="tonal" @click="visible = false">取消</v-btn>
|
||||
<v-btn @click="confirmSelect">确定</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
@@ -69,7 +69,22 @@ import { computed, ref, watch } from "vue";
|
||||
|
||||
import TOverlay from "../app/t-overlay.vue";
|
||||
|
||||
// 一些数据
|
||||
export type SelectedCValue = {
|
||||
star: number[];
|
||||
weapon: string[];
|
||||
elements: string[];
|
||||
area: string[];
|
||||
};
|
||||
type TwoSelectCProps = { modelValue: boolean; reset: boolean };
|
||||
|
||||
type TwoSelectCEmits = {
|
||||
(e: "update:modelValue", v: boolean): void;
|
||||
(e: "update:reset", v: boolean): void;
|
||||
(e: "select-c", v: SelectedCValue): void;
|
||||
};
|
||||
|
||||
const props = defineProps<TwoSelectCProps>();
|
||||
const emits = defineEmits<TwoSelectCEmits>();
|
||||
const selectStarList = [4, 5];
|
||||
const selectWeaponList = ["单手剑", "双手剑", "弓", "法器", "长柄武器"];
|
||||
const selectElementList = ["冰", "岩", "水", "火", "草", "雷", "风"];
|
||||
@@ -80,45 +95,13 @@ const selectedStar = ref<number[]>(selectStarList);
|
||||
const selectedWeapon = ref<string[]>(selectWeaponList);
|
||||
const selectedElements = ref<string[]>(selectElementList);
|
||||
const selectedArea = ref<string[]>(selectAreaList);
|
||||
|
||||
export interface SelectedCValue {
|
||||
star: number[];
|
||||
weapon: string[];
|
||||
elements: string[];
|
||||
area: string[];
|
||||
}
|
||||
|
||||
interface TwoSelectCProps {
|
||||
modelValue: boolean;
|
||||
reset: boolean;
|
||||
}
|
||||
|
||||
interface TwoSelectCEmits {
|
||||
(e: "update:modelValue", value: boolean): void;
|
||||
|
||||
(e: "update:reset", value: boolean): void;
|
||||
|
||||
(e: "select-c", value: SelectedCValue): void;
|
||||
}
|
||||
|
||||
const props = defineProps<TwoSelectCProps>();
|
||||
const emits = defineEmits<TwoSelectCEmits>();
|
||||
|
||||
const visible = computed({
|
||||
get() {
|
||||
return props.modelValue;
|
||||
},
|
||||
set(value) {
|
||||
emits("update:modelValue", value);
|
||||
},
|
||||
const visible = computed<boolean>({
|
||||
get: () => props.modelValue,
|
||||
set: (v) => emits("update:modelValue", v),
|
||||
});
|
||||
const reset = computed({
|
||||
get() {
|
||||
return props.reset;
|
||||
},
|
||||
set(value) {
|
||||
emits("update:reset", value);
|
||||
},
|
||||
const reset = computed<boolean>({
|
||||
get: () => props.reset,
|
||||
set: (v: boolean) => emits("update:reset", v),
|
||||
});
|
||||
|
||||
watch(
|
||||
@@ -134,10 +117,6 @@ watch(
|
||||
},
|
||||
);
|
||||
|
||||
function onCancel() {
|
||||
visible.value = false;
|
||||
}
|
||||
|
||||
function confirmSelect() {
|
||||
const value: SelectedCValue = {
|
||||
star: selectedStar.value,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<TOverlay v-model="visible" hide :to-click="onCancel" blur-val="20px">
|
||||
<TOverlay v-model="visible">
|
||||
<div class="two-sw-container">
|
||||
<div class="two-sw-item">
|
||||
<div class="two-sw-title">星级</div>
|
||||
@@ -28,7 +28,7 @@
|
||||
</v-item-group>
|
||||
</div>
|
||||
<div class="tow-sc-submit">
|
||||
<v-btn variant="tonal" @click="onCancel">取消</v-btn>
|
||||
<v-btn variant="tonal" @click="visible = false">取消</v-btn>
|
||||
<v-btn @click="confirmSelect">确定</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
@@ -39,50 +39,29 @@ import { computed, ref, watch } from "vue";
|
||||
|
||||
import TOverlay from "../app/t-overlay.vue";
|
||||
|
||||
// 一些数据
|
||||
const selectStarList = [4, 5];
|
||||
const selectWeaponList = ["单手剑", "双手剑", "弓", "法器", "长柄武器"];
|
||||
|
||||
// 选中的元素
|
||||
const selectedStar = ref<number[]>(selectStarList);
|
||||
const selectedWeapon = ref<string[]>(selectWeaponList);
|
||||
|
||||
export interface SelectedWValue {
|
||||
star: number[];
|
||||
weapon: string[];
|
||||
}
|
||||
|
||||
interface TwoSelectWProps {
|
||||
modelValue: boolean;
|
||||
reset: boolean;
|
||||
}
|
||||
|
||||
interface TwoSelectWEmits {
|
||||
export type SelectedWValue = { star: number[]; weapon: string[] };
|
||||
type TwoSelectWProps = { modelValue: boolean; reset: boolean };
|
||||
type TwoSelectWEmits = {
|
||||
(e: "update:modelValue", value: boolean): void;
|
||||
|
||||
(e: "update:reset", value: boolean): void;
|
||||
|
||||
(e: "select-w", value: SelectedWValue): void;
|
||||
}
|
||||
};
|
||||
|
||||
const props = defineProps<TwoSelectWProps>();
|
||||
const emits = defineEmits<TwoSelectWEmits>();
|
||||
|
||||
const visible = computed({
|
||||
get() {
|
||||
return props.modelValue;
|
||||
},
|
||||
set(value) {
|
||||
emits("update:modelValue", value);
|
||||
},
|
||||
const selectStarList = [4, 5];
|
||||
const selectWeaponList = ["单手剑", "双手剑", "弓", "法器", "长柄武器"];
|
||||
|
||||
const selectedStar = ref<number[]>(selectStarList);
|
||||
const selectedWeapon = ref<string[]>(selectWeaponList);
|
||||
const visible = computed<boolean>({
|
||||
get: () => props.modelValue,
|
||||
set: (v) => emits("update:modelValue", v),
|
||||
});
|
||||
const reset = computed({
|
||||
get() {
|
||||
return props.reset;
|
||||
},
|
||||
set(value) {
|
||||
emits("update:reset", value);
|
||||
},
|
||||
const reset = computed<boolean>({
|
||||
get: () => props.reset,
|
||||
set: (v) => emits("update:reset", v),
|
||||
});
|
||||
|
||||
watch(
|
||||
@@ -97,16 +76,8 @@ watch(
|
||||
},
|
||||
);
|
||||
|
||||
function onCancel() {
|
||||
visible.value = false;
|
||||
}
|
||||
|
||||
function confirmSelect() {
|
||||
const value: SelectedWValue = {
|
||||
star: selectedStar.value,
|
||||
weapon: selectedWeapon.value,
|
||||
};
|
||||
emits("select-w", value);
|
||||
function confirmSelect(): void {
|
||||
emits("select-w", { star: selectedStar.value, weapon: selectedWeapon.value });
|
||||
visible.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<TOverlay v-model="visible" hide :to-click="onCancel" blur-val="0">
|
||||
<TOverlay v-model="visible" blur-val="0">
|
||||
<div class="tua-ao-container" v-if="props.data">
|
||||
<slot name="left"></slot>
|
||||
<div class="tua-ao-box">
|
||||
@@ -59,33 +59,21 @@ import TGLogger from "../../utils/TGLogger.js";
|
||||
import TOverlay from "../app/t-overlay.vue";
|
||||
import VpOverlaySearch from "../viewPost/vp-overlay-search.vue";
|
||||
|
||||
interface ToAchiInfoProps {
|
||||
modelValue: boolean;
|
||||
data: TGApp.Sqlite.Achievement.RenderAchi;
|
||||
}
|
||||
|
||||
interface ToAchiInfoEmits {
|
||||
type ToAchiInfoProps = { modelValue: boolean; data: TGApp.Sqlite.Achievement.RenderAchi };
|
||||
type ToAchiInfoEmits = {
|
||||
(e: "update:modelValue", v: boolean): void;
|
||||
|
||||
(e: "select-series", v: number): void;
|
||||
}
|
||||
};
|
||||
|
||||
const props = defineProps<ToAchiInfoProps>();
|
||||
const emits = defineEmits<ToAchiInfoEmits>();
|
||||
const showSearch = ref<boolean>(false);
|
||||
const search = ref<string>();
|
||||
|
||||
const visible = computed({
|
||||
const visible = computed<boolean>({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => {
|
||||
emits("update:modelValue", value);
|
||||
},
|
||||
set: (v) => emits("update:modelValue", v),
|
||||
});
|
||||
|
||||
function onCancel() {
|
||||
visible.value = false;
|
||||
}
|
||||
|
||||
async function searchDirect(word: string): Promise<void> {
|
||||
await TGLogger.Info(`[ToAchiInfo][${props.data.id}][Search] 查询 ${word}`);
|
||||
search.value = word;
|
||||
|
||||
@@ -130,8 +130,10 @@ function getWeaponTitle(): string {
|
||||
title.push(`${weapon.rarity}星 精炼${weapon.affix_level} Lv.${weapon.level}`);
|
||||
const propMain = userStore.getProp(weapon.main_property.property_type);
|
||||
title.push(`${propMain !== false ? propMain.name : "未知属性"} - ${weapon.main_property.final}`);
|
||||
const propSub = userStore.getProp(weapon.sub_property.property_type);
|
||||
title.push(`${propSub !== false ? propSub.name : "未知属性"} - ${weapon.sub_property.final}`);
|
||||
if (weapon.sub_property !== null) {
|
||||
const propSub = userStore.getProp(weapon.sub_property.property_type);
|
||||
title.push(`${propSub !== false ? propSub.name : "未知属性"} - ${weapon.sub_property.final}`);
|
||||
}
|
||||
return title.join("\n");
|
||||
}
|
||||
</script>
|
||||
@@ -144,6 +146,7 @@ function getWeaponTitle(): string {
|
||||
border: 1px inset var(--common-shadow-2);
|
||||
border-radius: 5px;
|
||||
background: var(--box-bg-2);
|
||||
color: var(--tgc-white-1);
|
||||
cursor: pointer;
|
||||
row-gap: 5px;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<TOverlay v-model="visible" :hide="true" blur-val="5px" :to-click="onCancel">
|
||||
<TOverlay v-model="visible" blur-val="5px">
|
||||
<div class="tdo-box">
|
||||
<div class="tdo-avatars-container">
|
||||
<v-tabs
|
||||
@@ -12,7 +12,7 @@
|
||||
v-for="avatar in avatars"
|
||||
:key="avatar.avatar.id"
|
||||
:value="avatar.avatar.id"
|
||||
@click="onAvatarClick(avatar)"
|
||||
@click="emits('toAvatar', avatar)"
|
||||
:title="avatar.avatar.name"
|
||||
>
|
||||
<div class="tdo-avatar" :style="getAvatarBg(avatar)">
|
||||
@@ -52,34 +52,29 @@ import TucDetailOld from "../userAvatarOld/tuc-detail-old.vue";
|
||||
|
||||
import TuaDetailCard from "./tua-detail-card.vue";
|
||||
|
||||
interface TuaDetailOverlayProps {
|
||||
type TuaDetailOverlayProps = {
|
||||
modelValue: boolean;
|
||||
avatar: TGApp.Sqlite.Character.UserRole;
|
||||
mode: "classic" | "card" | "dev";
|
||||
avatars: TGApp.Sqlite.Character.UserRole[];
|
||||
}
|
||||
|
||||
interface TuaDetailOverlayEmits {
|
||||
(e: "update:modelValue", val: boolean): void;
|
||||
|
||||
};
|
||||
type TuaDetailOverlayEmits = {
|
||||
(e: "update:modelValue", v: boolean): void;
|
||||
(e: "update:mode", val: "classic" | "card" | "dev"): void;
|
||||
|
||||
(e: "toNext", val: boolean): void;
|
||||
|
||||
(e: "toAvatar", val: TGApp.Sqlite.Character.UserRole): void;
|
||||
}
|
||||
};
|
||||
|
||||
const props = defineProps<TuaDetailOverlayProps>();
|
||||
const emits = defineEmits<TuaDetailOverlayEmits>();
|
||||
const avatarTab = ref<number>();
|
||||
|
||||
const visible = computed<boolean>({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emits("update:modelValue", val),
|
||||
set: (v) => emits("update:modelValue", v),
|
||||
});
|
||||
const modeTab = computed<"classic" | "card" | "dev">({
|
||||
get: () => props.mode,
|
||||
set: (val) => emits("update:mode", val),
|
||||
set: (v) => emits("update:mode", v),
|
||||
});
|
||||
|
||||
const avatarsWidth = computed<string>(() => {
|
||||
@@ -102,19 +97,11 @@ watch(
|
||||
},
|
||||
);
|
||||
|
||||
function onCancel(): void {
|
||||
visible.value = false;
|
||||
}
|
||||
|
||||
function handleClick(pos: "left" | "right"): void {
|
||||
if (pos === "left") emits("toNext", false);
|
||||
else emits("toNext", true);
|
||||
}
|
||||
|
||||
function onAvatarClick(avatar: TGApp.Sqlite.Character.UserRole): void {
|
||||
emits("toAvatar", avatar);
|
||||
}
|
||||
|
||||
function getAvatarBg(avatar: TGApp.Sqlite.Character.UserRole): string {
|
||||
if (props.avatar.avatar.id === avatar.avatar.id) {
|
||||
return "background-color:var(--tgc-od-white);";
|
||||
|
||||
@@ -9,11 +9,7 @@
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
interface TucDetailConstellationProps {
|
||||
modelValue: TGApp.Game.Avatar.Constellation;
|
||||
}
|
||||
|
||||
defineProps<TucDetailConstellationProps>();
|
||||
defineProps<{ modelValue: TGApp.Game.Avatar.Constellation }>();
|
||||
</script>
|
||||
<style lang="css" scoped>
|
||||
.tuc-dc-box {
|
||||
|
||||
@@ -4,22 +4,22 @@
|
||||
<span>命之座</span>
|
||||
</template>
|
||||
<template #content>
|
||||
<TucDetailConstellation :model-value="props.modelValue" />
|
||||
<TucDetailConstellation :model-value="modelValue" />
|
||||
<div class="tuc-ddc-content">
|
||||
<div class="tuc-ddc-top">
|
||||
{{ props.modelValue.name }}
|
||||
{{ modelValue.name }}
|
||||
</div>
|
||||
<div class="tuc-ddc-bottom">
|
||||
<span>命之座</span>
|
||||
<span>第</span>
|
||||
<span>{{ props.modelValue.pos }}</span>
|
||||
<span>{{ modelValue.pos }}</span>
|
||||
<span>层</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #desc>
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<span v-html="parseHtmlText(props.modelValue.effect)"></span>
|
||||
<span v-html="parseHtmlText(modelValue.effect)"></span>
|
||||
</template>
|
||||
</TucDetailDesc>
|
||||
</template>
|
||||
@@ -29,11 +29,7 @@ import { parseHtmlText } from "../../utils/toolFunc.js";
|
||||
import TucDetailConstellation from "./tuc-detail-constellation.vue";
|
||||
import TucDetailDesc from "./tuc-detail-desc.vue";
|
||||
|
||||
interface TucDetailDescConstellationProps {
|
||||
modelValue: TGApp.Game.Avatar.Constellation;
|
||||
}
|
||||
|
||||
const props = defineProps<TucDetailDescConstellationProps>();
|
||||
defineProps<{ modelValue: TGApp.Game.Avatar.Constellation }>();
|
||||
</script>
|
||||
<style lang="css" scoped>
|
||||
.tuc-ddc-content {
|
||||
|
||||
@@ -3,13 +3,17 @@
|
||||
<div class="tucfi-label">
|
||||
<slot name="label">{{ props.label }}</slot>
|
||||
</div>
|
||||
<div v-if="!Array.isArray(props.data)" class="tucfi-data">
|
||||
<div v-if="props.data === null">
|
||||
<span class="tucfi-data">暂无数据</span>
|
||||
</div>
|
||||
<div v-else-if="!Array.isArray(props.data)" class="tucfi-data">
|
||||
<TItembox :model-value="getBox()" />
|
||||
</div>
|
||||
<div class="tucfi-icons" v-else>
|
||||
<div v-for="(item, idx) in props.data" :key="idx" class="tucfi-icon">
|
||||
<TItembox :model-value="getBox2(item)" />
|
||||
</div>
|
||||
<div v-if="props.data.length === 0" class="tucfi-data">暂无数据</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -18,7 +22,7 @@ import TItembox, { TItemBoxData } from "../app/t-item-box.vue";
|
||||
|
||||
interface TucFightProps {
|
||||
label: string;
|
||||
data: TGApp.Game.Combat.AvatarMini | TGApp.Game.Combat.AvatarMini[];
|
||||
data: TGApp.Game.Combat.AvatarMini | TGApp.Game.Combat.AvatarMini[] | null;
|
||||
}
|
||||
|
||||
const props = defineProps<TucFightProps>();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<TOverlay v-model="visible" hide :to-click="onCancel" blur-val="20px">
|
||||
<TOverlay v-model="visible">
|
||||
<div class="tuc-overlay-box" v-if="data">
|
||||
<div class="tuc-overlay-top">
|
||||
<span class="tuc-overlay-title" @click="share()">
|
||||
@@ -25,29 +25,26 @@ import { timestampToDate } from "../../utils/toolFunc.js";
|
||||
import TItemBox, { TItemBoxData } from "../app/t-item-box.vue";
|
||||
import TOverlay from "../app/t-overlay.vue";
|
||||
import showLoading from "../func/loading.js";
|
||||
import showSnackbar from "../func/snackbar.js";
|
||||
|
||||
interface TucOverlayProps {
|
||||
type TucOverlayProps = {
|
||||
modelValue: boolean;
|
||||
data: TGApp.Plugins.Hutao.Combat.Data | undefined;
|
||||
}
|
||||
|
||||
};
|
||||
type TucOverlayEmits = (e: "update:modelValue", v: boolean) => void;
|
||||
|
||||
const props = defineProps<TucOverlayProps>();
|
||||
const emits = defineEmits<TucOverlayEmits>();
|
||||
|
||||
const visible = computed<boolean>({
|
||||
get: () => props.modelValue,
|
||||
set: (v) => emits("update:modelValue", v),
|
||||
});
|
||||
const raw = computed<TGApp.Plugins.Hutao.Base.Rate[]>(() => {
|
||||
if (!props.data) return [];
|
||||
const res: TGApp.Plugins.Hutao.Base.Rate[] = props.data.BackupAvatarRates;
|
||||
return res.sort((a, b) => b.Rate - a.Rate);
|
||||
});
|
||||
|
||||
function onCancel(): void {
|
||||
visible.value = false;
|
||||
}
|
||||
|
||||
function getBoxData(item: TGApp.Plugins.Hutao.Base.Rate): TItemBoxData {
|
||||
const avatar = AppCharacterData.find((i) => i.id === item.Item);
|
||||
return {
|
||||
@@ -73,7 +70,11 @@ function getBoxData(item: TGApp.Plugins.Hutao.Base.Rate): TItemBoxData {
|
||||
|
||||
async function share(): Promise<void> {
|
||||
showLoading.start("正在生成分享图");
|
||||
const element = <HTMLElement>document.querySelector(".tuc-overlay-box");
|
||||
const element = document.querySelector<HTMLElement>(".tuc-overlay-box");
|
||||
if (element === null) {
|
||||
showSnackbar.error("未获取到分享内容");
|
||||
return;
|
||||
}
|
||||
const fileName = `真境剧诗_${new Date().getTime()}.png`;
|
||||
await generateShareImg(fileName, element, 1.2, true);
|
||||
showLoading.end();
|
||||
|
||||
340
src/components/userGacha/ugo-uid.vue
Normal file
340
src/components/userGacha/ugo-uid.vue
Normal file
@@ -0,0 +1,340 @@
|
||||
<template>
|
||||
<TOverlay v-model="visible" blur-val="5px">
|
||||
<div class="ugo-box">
|
||||
<div class="ugo-top">
|
||||
<div class="ugo-title">{{ title }}</div>
|
||||
<div class="ugo-fp" title="点击选择文件路径" @click="selectFile()">文件路径:{{ fp }}</div>
|
||||
</div>
|
||||
<div class="ugo-header" v-if="props.mode === 'import' && dataRaw">
|
||||
<div class="ugo-header-item">
|
||||
<div>应用信息:</div>
|
||||
<div>{{ dataRaw.info.export_app }} {{ dataRaw.info.export_app_version }}</div>
|
||||
</div>
|
||||
<div class="ugo-header-item">
|
||||
<div>文件UIGF版本:</div>
|
||||
<div>{{ dataRaw.info.version }}</div>
|
||||
</div>
|
||||
<div class="ugo-header-item">
|
||||
<div>导出时间:</div>
|
||||
<div>{{ timestampToDate(Number(dataRaw.info.export_timestamp) * 1000) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<v-item-group multiple v-model="selectedData" class="ugo-content">
|
||||
<v-item
|
||||
v-for="(item, index) in data"
|
||||
:key="index"
|
||||
v-slot="{ isSelected, toggle }"
|
||||
:value="item"
|
||||
>
|
||||
<div class="ugo-item" @click="toggle">
|
||||
<div class="ugo-item-left">
|
||||
<div class="ugo-item-title">{{ item.uid }} - {{ item.length }}条记录</div>
|
||||
<div class="ugo-item-sub">{{ item.time }}</div>
|
||||
</div>
|
||||
<div class="ugo-item-right">
|
||||
<v-btn :class="{ active: isSelected }" class="ugo-item-btn">
|
||||
<v-icon>{{ isSelected ? "mdi-check" : "mdi-checkbox-blank-outline" }}</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</v-item>
|
||||
</v-item-group>
|
||||
<div class="ugo-bottom">
|
||||
<v-btn class="ugo-item-btn" @click="visible = false" :rounded="true">取消</v-btn>
|
||||
<v-btn class="ugo-item-btn" @click="handleSelected()" :rounded="true">确定</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</TOverlay>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { path } from "@tauri-apps/api";
|
||||
import { open } from "@tauri-apps/plugin-dialog";
|
||||
import { writeTextFile } from "@tauri-apps/plugin-fs";
|
||||
import { computed, onMounted, ref, shallowRef, watch } from "vue";
|
||||
|
||||
import TSUserGacha from "../../plugins/Sqlite/modules/userGacha.js";
|
||||
import TGLogger from "../../utils/TGLogger.js";
|
||||
import { timestampToDate } from "../../utils/toolFunc.js";
|
||||
import { getUigf4Header, getUigf4Item, readUigf4Data, verifyUigfData } from "../../utils/UIGF.js";
|
||||
import TOverlay from "../app/t-overlay.vue";
|
||||
import showLoading from "../func/loading.js";
|
||||
import showSnackbar from "../func/snackbar.js";
|
||||
|
||||
type UgoUidProps =
|
||||
| { modelValue: boolean; mode: "export" }
|
||||
| { modelValue: boolean; mode: "import" };
|
||||
type UgoUidEmits = (e: "update:modelValue", v: boolean) => void;
|
||||
type UgoUidItem = { uid: string; length: number; time: string };
|
||||
|
||||
const props = defineProps<UgoUidProps>();
|
||||
const emits = defineEmits<UgoUidEmits>();
|
||||
const dataRaw = shallowRef<TGApp.Plugins.UIGF.Schema4>();
|
||||
const data = shallowRef<UgoUidItem[]>([]);
|
||||
const selectedData = shallowRef<UgoUidItem[]>([]);
|
||||
const title = computed<string>(() => (props.mode === "import" ? "导入" : "导出"));
|
||||
const fp = ref<string>("未选择");
|
||||
|
||||
const visible = computed<boolean>({
|
||||
get: () => props.modelValue,
|
||||
set: (v) => emits("update:modelValue", v),
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
if (props.mode === "export") fp.value = await getDefaultSavePath();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
async (v) => (v ? await refreshData() : undefined),
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
async function getDefaultSavePath(): Promise<string> {
|
||||
const tsNow = new Date().getTime();
|
||||
return `${await path.downloadDir()}${path.sep()}UIGF4_${tsNow}.json`;
|
||||
}
|
||||
|
||||
async function refreshData(): Promise<void> {
|
||||
selectedData.value = [];
|
||||
data.value = [];
|
||||
dataRaw.value = undefined;
|
||||
if (props.mode === "import") {
|
||||
fp.value = "未选择";
|
||||
await handleImportData();
|
||||
} else {
|
||||
fp.value = await getDefaultSavePath();
|
||||
await handleExportData();
|
||||
}
|
||||
}
|
||||
|
||||
async function selectFile(): Promise<void> {
|
||||
const defaultPath =
|
||||
props.mode === "import" ? await getDefaultSavePath() : await path.downloadDir();
|
||||
const file = await open({
|
||||
multiple: false,
|
||||
title: "选择文件",
|
||||
filters: [{ name: "UIGF JSON", extensions: ["json"] }],
|
||||
defaultPath,
|
||||
directory: false,
|
||||
});
|
||||
if (file === null) {
|
||||
showSnackbar.cancel("已取消文件选择");
|
||||
return;
|
||||
}
|
||||
fp.value = file;
|
||||
if (props.mode === "import") await handleImportData();
|
||||
}
|
||||
|
||||
async function handleImportData(): Promise<void> {
|
||||
if (fp.value === "未选择") return;
|
||||
try {
|
||||
showLoading.start("正在导入数据...", "正在验证数据...");
|
||||
const check = await verifyUigfData(fp.value, true);
|
||||
if (!check) {
|
||||
showLoading.end();
|
||||
return;
|
||||
}
|
||||
showLoading.update("正在导入数据...", "正在读取数据...");
|
||||
const uigfData = await readUigf4Data(fp.value);
|
||||
dataRaw.value = uigfData;
|
||||
data.value = uigfData.hk4e.map(parseData);
|
||||
showLoading.end();
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
showSnackbar.error(`[${e.name}] ${e.message}`);
|
||||
await TGLogger.Error(`[UgoUid][handleImportData] ${e.name} ${e.message}`);
|
||||
} else {
|
||||
showSnackbar.error(`[${e}]`);
|
||||
await TGLogger.Error(`[UgoUid][handleImportData] ${e}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function parseData(data: TGApp.Plugins.UIGF.GachaHk4e): UgoUidItem {
|
||||
const timeList = data.list.map((item) => new Date(item.time).getTime());
|
||||
return {
|
||||
uid: data.uid.toString(),
|
||||
length: data.list.length,
|
||||
time: `${timestampToDate(Math.min(...timeList))} ~ ${timestampToDate(Math.max(...timeList))}`,
|
||||
};
|
||||
}
|
||||
|
||||
async function handleExportData(): Promise<void> {
|
||||
const uidList = await TSUserGacha.getUidList();
|
||||
const tmpData: UgoUidItem[] = [];
|
||||
for (const uid of uidList) {
|
||||
const dataRaw = await TSUserGacha.getGachaRecords(uid);
|
||||
tmpData.push(parseDataRaw(dataRaw));
|
||||
}
|
||||
data.value = tmpData;
|
||||
}
|
||||
|
||||
function parseDataRaw(data: TGApp.Sqlite.GachaRecords.SingleTable[]): UgoUidItem {
|
||||
const timeList = data.map((item) => new Date(item.time).getTime());
|
||||
return {
|
||||
uid: data[0].uid,
|
||||
length: data.length,
|
||||
time: `${timestampToDate(Math.min(...timeList))} ~ ${timestampToDate(Math.max(...timeList))}`,
|
||||
};
|
||||
}
|
||||
|
||||
async function handleSelected(): Promise<void> {
|
||||
if (props.mode === "import") {
|
||||
if (!dataRaw.value) {
|
||||
showSnackbar.error("未获取到数据!");
|
||||
fp.value = "未选择";
|
||||
return;
|
||||
}
|
||||
if (selectedData.value.length === 0) {
|
||||
showSnackbar.warn("请至少选择一个!");
|
||||
return;
|
||||
}
|
||||
for (const item of selectedData.value) {
|
||||
showLoading.start("正在导入数据...", `正在导入UID: ${item.uid}`);
|
||||
const dataFind = dataRaw.value.hk4e.find((i) => i.uid.toString() === item.uid);
|
||||
if (!dataFind) {
|
||||
showSnackbar.error(`未找到UID: ${item.uid}`);
|
||||
await new Promise<void>((resolve) => setTimeout(resolve, 1000));
|
||||
continue;
|
||||
}
|
||||
await TSUserGacha.mergeUIGF4(dataFind);
|
||||
}
|
||||
showLoading.end();
|
||||
showSnackbar.success("导入成功!");
|
||||
return;
|
||||
}
|
||||
if (selectedData.value.length === 0) {
|
||||
showSnackbar.warn("请至少选择一个!");
|
||||
return;
|
||||
}
|
||||
showLoading.start("正在导出数据...", "正在生成文件头");
|
||||
const header = await getUigf4Header();
|
||||
const data: TGApp.Plugins.UIGF.GachaHk4e[] = [];
|
||||
for (const item of selectedData.value) {
|
||||
showLoading.update("正在导出数据...", `正在导出UID: ${item.uid}`);
|
||||
const dataItem = await getUigf4Item(item.uid);
|
||||
data.push(dataItem);
|
||||
}
|
||||
showLoading.update("正在导出数据...", "正在生成文件...");
|
||||
await writeTextFile(fp.value, JSON.stringify({ info: header, hk4e: data }));
|
||||
showLoading.end();
|
||||
showSnackbar.success(`导出成功! 文件路径: ${fp.value}`);
|
||||
fp.value = await getDefaultSavePath();
|
||||
}
|
||||
</script>
|
||||
<style lang="css" scoped>
|
||||
.ugo-box {
|
||||
position: relative;
|
||||
width: 600px;
|
||||
padding: 10px;
|
||||
border: 1px solid var(--common-shadow-2);
|
||||
border-radius: 10px;
|
||||
background: var(--app-page-bg);
|
||||
}
|
||||
|
||||
.ugo-top {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
column-gap: 10px;
|
||||
}
|
||||
|
||||
.ugo-title {
|
||||
color: var(--common-text-title);
|
||||
font-family: var(--font-title);
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.ugo-fp {
|
||||
color: var(--tgc-od-white);
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.ugo-header {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.ugo-header-item {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
font-size: 16px;
|
||||
gap: 5px;
|
||||
|
||||
:first-child {
|
||||
color: var(--box-text-7);
|
||||
font-family: var(--font-title);
|
||||
}
|
||||
|
||||
:last-child {
|
||||
color: var(--box-text-5);
|
||||
}
|
||||
}
|
||||
|
||||
.ugo-content {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
max-height: 300px;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
gap: 10px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.ugo-item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 5px;
|
||||
border: 1px solid var(--common-shadow-1);
|
||||
border-radius: 5px;
|
||||
background: var(--box-bg-1);
|
||||
color: var(--box-text-1);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ugo-item-title {
|
||||
font-family: var(--font-title);
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.ugo-item-sub {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.ugo-item-btn {
|
||||
height: 40px;
|
||||
border: 1px solid var(--common-shadow-2);
|
||||
background: var(--btn-bg-1);
|
||||
color: var(--btn-text-1);
|
||||
font-family: var(--font-title);
|
||||
|
||||
&.active {
|
||||
color: var(--tgc-od-green);
|
||||
}
|
||||
}
|
||||
|
||||
.ugo-bottom {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
</style>
|
||||
@@ -25,7 +25,7 @@ import VpOverlayCollect from "./vp-overlay-collect.vue";
|
||||
|
||||
const isCollected = ref(false);
|
||||
const collect = ref<Array<TGApp.Sqlite.UserCollection.UFMap>>([]);
|
||||
const showEdit = ref(false);
|
||||
const showEdit = ref<boolean>(false);
|
||||
|
||||
interface TbCollectProps {
|
||||
modelValue: number;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<!-- 编辑收藏帖子的合集 -->
|
||||
<template>
|
||||
<TOverlay v-model="visible" hide :to-click="onCancel" blur-val="20px">
|
||||
<TOverlay v-model="visible">
|
||||
<div class="topc-container">
|
||||
<div class="topc-post-info">
|
||||
{{ props.post?.post.subject }}
|
||||
@@ -27,7 +26,7 @@
|
||||
</div>
|
||||
<div class="topc-bottom">
|
||||
<v-btn class="topc-btn" rounded @click="newCollect">新建分类</v-btn>
|
||||
<v-btn class="topc-btn" rounded @click="onCancel">取消</v-btn>
|
||||
<v-btn class="topc-btn" rounded @click="visible = false">取消</v-btn>
|
||||
<v-btn :loading="submit" class="topc-btn" rounded @click="onSubmit">确定</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
@@ -41,31 +40,29 @@ import TOverlay from "../app/t-overlay.vue";
|
||||
import showDialog from "../func/dialog.js";
|
||||
import showSnackbar from "../func/snackbar.js";
|
||||
|
||||
interface ToPostCollectProps {
|
||||
type ToPostCollectProps = {
|
||||
modelValue: boolean;
|
||||
post: TGApp.Plugins.Mys.Post.FullData | undefined;
|
||||
}
|
||||
|
||||
interface ToPostCollectEmits {
|
||||
(e: "update:modelValue", value: boolean): void;
|
||||
|
||||
};
|
||||
type ToPostCollectEmits = {
|
||||
(e: "update:modelValue", v: boolean): void;
|
||||
(e: "submit"): void;
|
||||
}
|
||||
};
|
||||
|
||||
const props = defineProps<ToPostCollectProps>();
|
||||
const emits = defineEmits<ToPostCollectEmits>();
|
||||
const collectList = ref<TGApp.Sqlite.UserCollection.UFCollection[]>([]);
|
||||
const postCollect = ref<TGApp.Sqlite.UserCollection.UFMap[]>([]);
|
||||
const selectList = ref<string[]>([]);
|
||||
const submit = ref(false);
|
||||
const submit = ref<boolean>(false);
|
||||
const visible = computed<boolean>({
|
||||
get: () => props.modelValue,
|
||||
set: (v) => emits("update:modelValue", v),
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
async (val) => {
|
||||
if (val) {
|
||||
await freshData();
|
||||
}
|
||||
},
|
||||
async (v) => (v ? await freshData() : null),
|
||||
);
|
||||
|
||||
async function freshData(): Promise<void> {
|
||||
@@ -75,19 +72,14 @@ async function freshData(): Promise<void> {
|
||||
if (Array.isArray(collectRes)) {
|
||||
postCollect.value = collectRes;
|
||||
selectList.value = postCollect.value.map((i) => i.collection);
|
||||
} else if (collectRes) {
|
||||
return;
|
||||
}
|
||||
if (collectRes) {
|
||||
postCollect.value = [];
|
||||
selectList.value = [];
|
||||
}
|
||||
}
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => {
|
||||
emits("update:modelValue", value);
|
||||
},
|
||||
});
|
||||
|
||||
async function deleteCollect(item: TGApp.Sqlite.UserCollection.UFCollection): Promise<void> {
|
||||
const delCheck = await showDialog.check("确定删除分类?", "该分类若有帖子,则会变为未分类");
|
||||
if (!delCheck) {
|
||||
@@ -148,10 +140,6 @@ async function onSubmit(): Promise<void> {
|
||||
}
|
||||
showSnackbar.success("更新成功");
|
||||
}
|
||||
|
||||
function onCancel() {
|
||||
visible.value = false;
|
||||
}
|
||||
</script>
|
||||
<style lang="css" scoped>
|
||||
.topc-container {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<TOverlay v-model="visible" :hide="true" :to-click="onCancel" blur-val="5px">
|
||||
<TOverlay v-model="visible" blur-val="5px">
|
||||
<div class="tpoc-box">
|
||||
<div class="tpoc-top">
|
||||
<span>{{ props.collection.collection_title }}</span>
|
||||
@@ -47,43 +47,35 @@
|
||||
</TOverlay>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, ref, useTemplateRef, watch } from "vue";
|
||||
import { computed, onMounted, shallowRef, useTemplateRef, watch } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
|
||||
import Mys from "../../plugins/Mys/index.js";
|
||||
import TOverlay from "../app/t-overlay.vue";
|
||||
import showSnackbar from "../func/snackbar.js";
|
||||
|
||||
interface TpoCollectionProps {
|
||||
type TpoCollectionProps = {
|
||||
collection: TGApp.Plugins.Mys.Post.Collection;
|
||||
modelValue: boolean;
|
||||
}
|
||||
|
||||
type TpoCollectionEmits = (e: "update:modelValue", value: boolean) => void;
|
||||
|
||||
interface TpoCollectionItem {
|
||||
};
|
||||
type TpoCollectionEmits = (e: "update:modelValue", v: boolean) => void;
|
||||
type TpoCollectionItem = {
|
||||
postId: string;
|
||||
title: string;
|
||||
created: number;
|
||||
updated: number;
|
||||
comments: number;
|
||||
likes: number;
|
||||
}
|
||||
|
||||
};
|
||||
const router = useRouter();
|
||||
const props = defineProps<TpoCollectionProps>();
|
||||
const emits = defineEmits<TpoCollectionEmits>();
|
||||
const postListEl = useTemplateRef<HTMLDivElement>("postListRef");
|
||||
|
||||
const visible = computed({
|
||||
const posts = shallowRef<TpoCollectionItem[]>([]);
|
||||
const visible = computed<boolean>({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => {
|
||||
emits("update:modelValue", value);
|
||||
},
|
||||
set: (v) => emits("update:modelValue", v),
|
||||
});
|
||||
const posts = ref<TpoCollectionItem[]>([]);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
watch(
|
||||
() => visible.value,
|
||||
async (value) => {
|
||||
@@ -115,10 +107,6 @@ onMounted(async () => {
|
||||
posts.value = tempArr;
|
||||
});
|
||||
|
||||
function onCancel() {
|
||||
visible.value = false;
|
||||
}
|
||||
|
||||
function getDate(date: number): string {
|
||||
return new Date(date * 1000).toLocaleString().replace(/\//g, "-").split(" ")[0];
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<TOverlay v-model="visible" hide :to-click="onCancel" blur-val="10px">
|
||||
<TOverlay v-model="visible" blur-val="10px">
|
||||
<div class="tpoi-box">
|
||||
<div :class="isOriSize ? 'tpoi-top-ori' : 'tpoi-top'">
|
||||
<div :class="{ 'tpoi-top-ori': isOriSize, 'tpoi-top': !isOriSize }">
|
||||
<img :src="props.image.insert.image" alt="图片" @click="resizeImg" />
|
||||
</div>
|
||||
<div class="tpoi-bottom">
|
||||
@@ -16,7 +16,7 @@
|
||||
<v-icon @click="setBlackBg" title="切换背景色">mdi-format-color-fill</v-icon>
|
||||
<v-icon @click="onCopy" title="复制到剪贴板">mdi-content-copy</v-icon>
|
||||
<v-icon @click="onDownload" title="下载到本地">mdi-download</v-icon>
|
||||
<v-icon @click="onCancel" title="关闭浮窗">mdi-close</v-icon>
|
||||
<v-icon @click="visible = false" title="关闭浮窗">mdi-close</v-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -32,49 +32,34 @@ import showSnackbar from "../func/snackbar.js";
|
||||
|
||||
import { TpImage } from "./tp-image.vue";
|
||||
|
||||
interface TpoImageProps {
|
||||
image: TpImage;
|
||||
modelValue: boolean;
|
||||
}
|
||||
|
||||
type TpoImageEmits = {
|
||||
(e: "update:modelValue", v: boolean): void;
|
||||
};
|
||||
|
||||
type TpoImageProps = { image: TpImage; modelValue: boolean };
|
||||
type TpoImageEmits = (e: "update:modelValue", v: boolean) => void;
|
||||
const props = defineProps<TpoImageProps>();
|
||||
const emits = defineEmits<TpoImageEmits>();
|
||||
const buffer = ref<Uint8Array | null>(null);
|
||||
const bgMode = ref(0); // 0: transparent, 1: black, 2: white
|
||||
const isOriSize = ref(false);
|
||||
|
||||
const visible = computed({
|
||||
const bgMode = ref<number>(0); // 0: transparent, 1: black, 2: white
|
||||
const isOriSize = ref<boolean>(false);
|
||||
const visible = computed<boolean>({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => {
|
||||
emits("update:modelValue", value);
|
||||
},
|
||||
set: (v) => emits("update:modelValue", v),
|
||||
});
|
||||
const bg = computed(() => {
|
||||
if (bgMode.value === 1) return "black";
|
||||
if (bgMode.value === 2) return "white";
|
||||
return "transparent";
|
||||
});
|
||||
|
||||
const format = computed(() => {
|
||||
const format = computed<string>(() => {
|
||||
if (props.image.attributes?.ext) return props.image.attributes.ext;
|
||||
const imageFormat = props.image.insert.image.split(".").pop();
|
||||
if (imageFormat !== undefined) return imageFormat;
|
||||
return "png";
|
||||
});
|
||||
|
||||
function onCancel() {
|
||||
visible.value = false;
|
||||
}
|
||||
|
||||
function resizeImg() {
|
||||
function resizeImg(): void {
|
||||
isOriSize.value = !isOriSize.value;
|
||||
}
|
||||
|
||||
function setBlackBg() {
|
||||
function setBlackBg(): void {
|
||||
bgMode.value = (bgMode.value + 1) % 3;
|
||||
const bgLabelList = ["透明", "黑色", "白色"];
|
||||
showSnackbar.success(`背景已切换为${bgLabelList[bgMode.value]}`);
|
||||
@@ -92,7 +77,7 @@ async function onCopy(): Promise<void> {
|
||||
showSnackbar.success(`图片已复制到剪贴板,大小:${size}`);
|
||||
}
|
||||
|
||||
async function onDownload() {
|
||||
async function onDownload(): Promise<void> {
|
||||
const image = props.image.insert.image;
|
||||
if (buffer.value === null) buffer.value = await getImageBuffer(image);
|
||||
const size = bytesToSize(buffer.value.byteLength);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<TOverlay v-model="visible" hide :to-click="onCancel" blur-val="20px">
|
||||
<TOverlay v-model="visible">
|
||||
<div class="tpol-box" v-if="card">
|
||||
<div class="tpol-title">
|
||||
<span>抽奖详情</span>
|
||||
@@ -48,45 +48,24 @@ import Mys from "../../plugins/Mys/index.js";
|
||||
import TOverlay from "../app/t-overlay.vue";
|
||||
import showSnackbar from "../func/snackbar.js";
|
||||
|
||||
interface TpoLotteryProps {
|
||||
modelValue: boolean;
|
||||
lottery: string | undefined;
|
||||
}
|
||||
|
||||
interface TpoLotteryEmits {
|
||||
(e: "update:modelValue", value: boolean): void;
|
||||
|
||||
(e: "cancel"): void;
|
||||
}
|
||||
|
||||
type TpoLotteryProps = { modelValue: boolean; lottery: string | undefined };
|
||||
type TpoLotteryEmits = (e: "update:modelValue", v: boolean) => void;
|
||||
const props = defineProps<TpoLotteryProps>();
|
||||
const emits = defineEmits<TpoLotteryEmits>();
|
||||
const card = ref<TGApp.Plugins.Mys.Lottery.RenderCard>();
|
||||
const jsonData = ref<TGApp.Plugins.Mys.Lottery.FullData>();
|
||||
const timeStatus = ref<string>("未知");
|
||||
const upWay = ref<string>("未知");
|
||||
|
||||
const visible = computed<boolean>({
|
||||
get: () => props.modelValue,
|
||||
set: (v) => emits("update:modelValue", v),
|
||||
});
|
||||
// eslint-disable-next-line no-undef
|
||||
let timer: NodeJS.Timeout | undefined = undefined;
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => {
|
||||
emits("update:modelValue", value);
|
||||
},
|
||||
});
|
||||
|
||||
const onCancel = (): void => {
|
||||
visible.value = false;
|
||||
emits("cancel");
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.lottery,
|
||||
async (value) => {
|
||||
if (!value) return;
|
||||
await load();
|
||||
},
|
||||
async (v) => (v ? await load() : undefined),
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<TOverlay v-model="visible" hide :to-click="onCancel" blur-val="20px">
|
||||
<TOverlay v-model="visible">
|
||||
<div class="tops-box">
|
||||
<div class="tops-top">查找:{{ search }}</div>
|
||||
<div class="tops-act">
|
||||
<span>分区:{{ getGidLabel() }}</span>
|
||||
<span>分区:{{ getGameName(Number(game)) }}</span>
|
||||
<v-btn :loading="load" size="small" class="tops-btn" @click="searchPosts()" rounded>
|
||||
加载更多({{ results.length }})
|
||||
</v-btn>
|
||||
@@ -17,57 +17,30 @@
|
||||
</TOverlay>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, ref, watch } from "vue";
|
||||
import { computed, onMounted, ref, shallowRef, watch } from "vue";
|
||||
|
||||
import Mys from "../../plugins/Mys/index.js";
|
||||
import { getGameName } from "../../web/utils/tools.js";
|
||||
import TOverlay from "../app/t-overlay.vue";
|
||||
import TPostCard from "../app/t-postcard.vue";
|
||||
import showSnackbar from "../func/snackbar.js";
|
||||
|
||||
// data
|
||||
const search = ref<string>();
|
||||
const lastId = ref<string>("");
|
||||
const game = ref<string>("2");
|
||||
const isLast = ref<boolean>(false);
|
||||
const results = ref<TGApp.Plugins.Mys.Post.FullData[]>([]);
|
||||
const results = shallowRef<TGApp.Plugins.Mys.Post.FullData[]>([]);
|
||||
const load = ref<boolean>(false);
|
||||
|
||||
interface ToPostSearchProps {
|
||||
modelValue: boolean;
|
||||
gid: string;
|
||||
keyword?: string;
|
||||
}
|
||||
|
||||
interface ToPostSearchEmits {
|
||||
(e: "update:modelValue", value: boolean): void;
|
||||
|
||||
(e: "cancel"): void;
|
||||
}
|
||||
|
||||
type ToPostSearchProps = { modelValue: boolean; gid: string; keyword?: string };
|
||||
type ToPostSearchEmits = (e: "update:modelValue", v: boolean) => void;
|
||||
const props = defineProps<ToPostSearchProps>();
|
||||
const emits = defineEmits<ToPostSearchEmits>();
|
||||
|
||||
const gameList: Record<string, string> = {
|
||||
"1": "崩坏3",
|
||||
"2": "原神",
|
||||
"3": "崩坏2",
|
||||
"4": "未定事件簿",
|
||||
"5": "大别野",
|
||||
"6": "崩坏:星穹铁道",
|
||||
"8": "绝区零",
|
||||
};
|
||||
// overlay
|
||||
const visible = computed({
|
||||
const visible = computed<boolean>({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => {
|
||||
emits("update:modelValue", value);
|
||||
},
|
||||
set: (v) => emits("update:modelValue", v),
|
||||
});
|
||||
|
||||
function onCancel() {
|
||||
visible.value = false;
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
game.value = props.gid;
|
||||
if (props.keyword && props.keyword !== "") search.value = props.keyword;
|
||||
@@ -145,13 +118,6 @@ async function searchPosts() {
|
||||
visible.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
function getGidLabel(): string {
|
||||
if (gameList[game.value]) {
|
||||
return gameList[game.value];
|
||||
}
|
||||
return "未知";
|
||||
}
|
||||
</script>
|
||||
<style lang="css" scoped>
|
||||
.tops-box {
|
||||
@@ -181,6 +147,7 @@ function getGidLabel(): string {
|
||||
}
|
||||
|
||||
.tops-list {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 400px;
|
||||
max-height: 400px;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<TOverlay v-model="visible" :hide="true" :to-click="onCancel" blur-val="0">
|
||||
<TOverlay v-model="visible" blur-val="0">
|
||||
<div class="tpr-debug-box">
|
||||
<div class="tpr-debug-title">
|
||||
<span>文件:</span>
|
||||
@@ -15,37 +15,24 @@
|
||||
<script lang="ts" setup>
|
||||
import { open } from "@tauri-apps/plugin-dialog";
|
||||
import { readTextFile } from "@tauri-apps/plugin-fs";
|
||||
import { computed, ref } from "vue";
|
||||
import { computed, ref, shallowRef } from "vue";
|
||||
|
||||
import TOverlay from "../app/t-overlay.vue";
|
||||
import showSnackbar from "../func/snackbar.js";
|
||||
|
||||
import TprReply from "./vp-reply-item.vue";
|
||||
|
||||
interface TprDebugProps {
|
||||
modelValue: boolean;
|
||||
}
|
||||
|
||||
interface TprDebugEmits {
|
||||
(event: "update:modelValue", value: boolean): void;
|
||||
}
|
||||
type TprDebugProps = { modelValue: boolean };
|
||||
type TprDebugEmits = (e: "update:modelValue", v: boolean) => void;
|
||||
|
||||
const props = defineProps<TprDebugProps>();
|
||||
const emits = defineEmits<TprDebugEmits>();
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => {
|
||||
emits("update:modelValue", value);
|
||||
},
|
||||
});
|
||||
|
||||
const filePath = ref<string>("");
|
||||
const replyData = ref<TGApp.Plugins.Mys.Reply.ReplyFull | null>(null);
|
||||
|
||||
function onCancel(): void {
|
||||
visible.value = false;
|
||||
}
|
||||
const replyData = shallowRef<TGApp.Plugins.Mys.Reply.ReplyFull | null>(null);
|
||||
const visible = computed<boolean>({
|
||||
get: () => props.modelValue,
|
||||
set: (v) => emits("update:modelValue", v),
|
||||
});
|
||||
|
||||
async function selectFile(): Promise<void> {
|
||||
const file = await open({
|
||||
|
||||
@@ -5491,7 +5491,7 @@
|
||||
},
|
||||
{
|
||||
"id": 15514,
|
||||
"contentId": 0,
|
||||
"contentId": 503390,
|
||||
"dropDays": [3, 6, 7],
|
||||
"name": "星鹫赤羽",
|
||||
"itemType": "weapon",
|
||||
@@ -7888,7 +7888,7 @@
|
||||
},
|
||||
{
|
||||
"id": 11432,
|
||||
"contentId": 0,
|
||||
"contentId": 503608,
|
||||
"dropDays": [2, 5, 7],
|
||||
"name": "厄水之祸",
|
||||
"itemType": "weapon",
|
||||
@@ -10285,7 +10285,7 @@
|
||||
},
|
||||
{
|
||||
"id": 15430,
|
||||
"contentId": 0,
|
||||
"contentId": 503391,
|
||||
"dropDays": [3, 6, 7],
|
||||
"name": "缀花之翎",
|
||||
"itemType": "weapon",
|
||||
@@ -11407,7 +11407,7 @@
|
||||
},
|
||||
{
|
||||
"id": 14430,
|
||||
"contentId": 0,
|
||||
"contentId": 503389,
|
||||
"dropDays": [1, 4, 7],
|
||||
"name": "乘浪的回旋",
|
||||
"itemType": "weapon",
|
||||
|
||||
@@ -2357,5 +2357,41 @@
|
||||
"postId": "59469137",
|
||||
"up5List": [15514, 15512],
|
||||
"up4List": [14430, 15430, 11402, 12402, 13407]
|
||||
},
|
||||
{
|
||||
"name": "渊海界令",
|
||||
"version": "5.2",
|
||||
"order": 2,
|
||||
"banner": "https://sdk.hoyoverse.com/upload/ann/2024/11/25/28f7587d6e7764786466d6d1869877d2_2964059866754889601.jpg",
|
||||
"from": "2024-12-10T18:00:00+08:00",
|
||||
"to": "2024-12-31T14:59:00+08:00",
|
||||
"type": 301,
|
||||
"postId": "59929712",
|
||||
"up5List": [10000087],
|
||||
"up4List": [10000059, 10000031, 10000077]
|
||||
},
|
||||
{
|
||||
"name": "陵薮市朝",
|
||||
"version": "5.2",
|
||||
"order": 2,
|
||||
"banner": "https://sdk.hoyoverse.com/upload/ann/2024/11/25/51ed5e02b10d1aaeaa1a2d45a736d3b2_9081339439483505922.jpg",
|
||||
"from": "2024-12-10T18:00:00+08:00",
|
||||
"to": "2024-12-31T14:59:00+08:00",
|
||||
"type": 400,
|
||||
"postId": "59929713",
|
||||
"up5List": [10000030],
|
||||
"up4List": [10000059, 10000031, 10000077]
|
||||
},
|
||||
{
|
||||
"name": "神铸赋形",
|
||||
"version": "5.2",
|
||||
"order": 2,
|
||||
"banner": "https://sdk.hoyoverse.com/upload/ann/2024/11/25/357191e7ae2949da0bd8c5b8c4dce663_6627628462211014980.jpg",
|
||||
"from": "2024-12-10T18:00:00+08:00",
|
||||
"to": "2024-12-31T14:59:00+08:00",
|
||||
"type": 302,
|
||||
"postId": "59929714",
|
||||
"up5List": [14514, 13504],
|
||||
"up4List": [15412, 11401, 12401, 13401, 14403]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[
|
||||
{
|
||||
"id": 15514,
|
||||
"contentId": 0,
|
||||
"contentId": 503390,
|
||||
"name": "星鹫赤羽",
|
||||
"star": 5,
|
||||
"bg": "/icon/bg/5-Star.webp",
|
||||
@@ -478,7 +478,7 @@
|
||||
},
|
||||
{
|
||||
"id": 15430,
|
||||
"contentId": 0,
|
||||
"contentId": 503391,
|
||||
"name": "缀花之翎",
|
||||
"star": 4,
|
||||
"bg": "/icon/bg/4-Star.webp",
|
||||
@@ -703,7 +703,7 @@
|
||||
},
|
||||
{
|
||||
"id": 14430,
|
||||
"contentId": 0,
|
||||
"contentId": 503389,
|
||||
"name": "乘浪的回旋",
|
||||
"star": 4,
|
||||
"bg": "/icon/bg/4-Star.webp",
|
||||
@@ -1279,7 +1279,7 @@
|
||||
},
|
||||
{
|
||||
"id": 11432,
|
||||
"contentId": 0,
|
||||
"contentId": 503608,
|
||||
"name": "厄水之祸",
|
||||
"star": 4,
|
||||
"bg": "/icon/bg/4-Star.webp",
|
||||
|
||||
@@ -6,22 +6,22 @@
|
||||
|
||||
import type { Schema } from "ajv";
|
||||
|
||||
import achievements from "./app/achievements.json";
|
||||
import achievementSeries from "./app/achievementSeries.json";
|
||||
import calendar from "./app/calendar.json";
|
||||
import character from "./app/character.json";
|
||||
import gacha from "./app/gacha.json";
|
||||
import nameCards from "./app/namecard.json";
|
||||
import weapon from "./app/weapon.json";
|
||||
import arcBirCalendar from "./archive/birth_calendar.json";
|
||||
import arcBirDraw from "./archive/birth_draw.json";
|
||||
import arcBirRole from "./archive/birth_role.json";
|
||||
import schemaUiaf from "./schema/uiaf-schema.json";
|
||||
import schemaUigf from "./schema/uigf-schema.json";
|
||||
import scheamUigf4 from "./schema/uigf4-schema.json";
|
||||
import wikiCharacter from "./WIKI/character.json";
|
||||
import wikiMaterial from "./WIKI/material.json";
|
||||
import wikiWeapon from "./WIKI/weapon.json";
|
||||
import achievements from "./app/achievements.json" assert { type: "json" };
|
||||
import achievementSeries from "./app/achievementSeries.json" assert { type: "json" };
|
||||
import calendar from "./app/calendar.json" assert { type: "json" };
|
||||
import character from "./app/character.json" assert { type: "json" };
|
||||
import gacha from "./app/gacha.json" assert { type: "json" };
|
||||
import nameCards from "./app/namecard.json" assert { type: "json" };
|
||||
import weapon from "./app/weapon.json" assert { type: "json" };
|
||||
import arcBirCalendar from "./archive/birth_calendar.json" assert { type: "json" };
|
||||
import arcBirDraw from "./archive/birth_draw.json" assert { type: "json" };
|
||||
import arcBirRole from "./archive/birth_role.json" assert { type: "json" };
|
||||
import schemaUiaf from "./schema/uiaf-schema.json" assert { type: "json" };
|
||||
import schemaUigf from "./schema/uigf-schema.json" assert { type: "json" };
|
||||
import schemaUigf4 from "./schema/uigf4-schema.json" assert { type: "json" };
|
||||
import wikiCharacter from "./WIKI/character.json" assert { type: "json" };
|
||||
import wikiMaterial from "./WIKI/material.json" assert { type: "json" };
|
||||
import wikiWeapon from "./WIKI/weapon.json" assert { type: "json" };
|
||||
|
||||
// App
|
||||
export const AppAchievementsData: TGApp.App.Achievement.Item[] = achievements;
|
||||
@@ -34,7 +34,7 @@ export const AppWeaponData: TGApp.App.Weapon.WikiBriefInfo[] = weapon;
|
||||
// Schema
|
||||
export const UiafSchema: Schema = schemaUiaf;
|
||||
export const UigfSchema: Schema = schemaUigf;
|
||||
export const Uigf4Schema: Schema = scheamUigf4;
|
||||
export const Uigf4Schema: Schema = schemaUigf4;
|
||||
// Archive
|
||||
export const ArcBirCalendar: TGApp.Archive.Birth.CalendarData = arcBirCalendar;
|
||||
export const ArcBirDraw: TGApp.Archive.Birth.DrawItem[] = arcBirDraw;
|
||||
|
||||
@@ -113,11 +113,11 @@
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "记录内部 ID,米哈游 API 返回"
|
||||
},
|
||||
"maxLength": 19,
|
||||
"minLength": 1,
|
||||
"pattern": "^[0-9]+$"
|
||||
"description": "记录内部 ID,米哈游 API 返回",
|
||||
"maxLength": 19,
|
||||
"minLength": 1,
|
||||
"pattern": "^[0-9]+$"
|
||||
}
|
||||
},
|
||||
"required": ["uigf_gacha_type", "gacha_type", "item_id", "time", "id"]
|
||||
}
|
||||
|
||||
@@ -34,32 +34,27 @@
|
||||
<ToArcBrith v-model="showOverlay" :data="current" :choice="isAether" />
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref, watch } from "vue";
|
||||
import { computed, onMounted, ref, shallowRef, watch } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
|
||||
import ToArcBrith from "../../components/pageArchive/to-arcBrith.vue";
|
||||
import { ArcBirDraw, ArcBirRole } from "../../data/index.js";
|
||||
import TGClient from "../../utils/TGClient.js";
|
||||
|
||||
const page = ref(1);
|
||||
const length = ref(0);
|
||||
const visible = ref(0);
|
||||
const renderItems = ref<TGApp.Archive.Birth.DrawItem[]>([]);
|
||||
const curSelect = ref<TGApp.Archive.Birth.RoleItem | null>(null);
|
||||
const selectedItem = ref<TGApp.Archive.Birth.DrawItem[]>([]);
|
||||
const current = ref<TGApp.Archive.Birth.DrawItem>();
|
||||
const isAether = ref<boolean>(false);
|
||||
const showOverlay = ref(false);
|
||||
const route = useRoute();
|
||||
|
||||
watch(
|
||||
() => page.value,
|
||||
() => {
|
||||
const start = (page.value - 1) * 12;
|
||||
const end = start + 12;
|
||||
selectedItem.value = renderItems.value.slice(start, end);
|
||||
},
|
||||
);
|
||||
const page = ref<number>(1);
|
||||
const length = ref<number>(0);
|
||||
const visible = ref(0);
|
||||
const renderItems = shallowRef<TGApp.Archive.Birth.DrawItem[]>([]);
|
||||
const curSelect = shallowRef<TGApp.Archive.Birth.RoleItem | null>(null);
|
||||
const current = shallowRef<TGApp.Archive.Birth.DrawItem>();
|
||||
const isAether = ref<boolean>(false);
|
||||
const showOverlay = ref<boolean>(false);
|
||||
|
||||
const selectedItem = computed<TGApp.Archive.Birth.DrawItem[]>(() => {
|
||||
return renderItems.value.slice((page.value - 1) * 12, page.value * 12);
|
||||
});
|
||||
|
||||
watch(
|
||||
() => curSelect.value,
|
||||
@@ -73,7 +68,6 @@ watch(
|
||||
}
|
||||
length.value = Math.ceil(renderItems.value.length / 12);
|
||||
page.value = 1;
|
||||
selectedItem.value = renderItems.value.slice(0, 12);
|
||||
visible.value = length.value > 5 ? 5 : length.value;
|
||||
},
|
||||
);
|
||||
@@ -85,7 +79,6 @@ onMounted(() => {
|
||||
if (dataFind) curSelect.value = dataFind;
|
||||
} else {
|
||||
renderItems.value = ArcBirDraw;
|
||||
selectedItem.value = renderItems.value.slice(0, 12);
|
||||
}
|
||||
length.value = Math.ceil(renderItems.value.length / 12);
|
||||
visible.value = length.value > 5 ? 5 : length.value;
|
||||
|
||||
@@ -118,9 +118,7 @@ import { generateShareImg } from "../../utils/TGShare.js";
|
||||
import { timestampToDate } from "../../utils/toolFunc.js";
|
||||
import TakumiRecordGenshinApi from "../../web/request/recordReq.js";
|
||||
|
||||
// store
|
||||
const userStore = storeToRefs(useUserStore());
|
||||
const user = computed<TGApp.Sqlite.Account.Game>(() => userStore.account.value);
|
||||
const { cookie, account, propMap } = storeToRefs(useUserStore());
|
||||
|
||||
// loading
|
||||
const loadData = ref<boolean>(false);
|
||||
@@ -214,9 +212,9 @@ function getOrderedList(
|
||||
|
||||
async function loadUid(): Promise<void> {
|
||||
uidList.value = await TSUserAvatar.getAllUid();
|
||||
if (uidList.value.length === 0) uidList.value = [user.value.gameUid];
|
||||
if (uidList.value.includes(user.value.gameUid)) {
|
||||
uidCur.value = user.value.gameUid;
|
||||
if (uidList.value.length === 0) uidList.value = [account.value.gameUid];
|
||||
if (uidList.value.includes(account.value.gameUid)) {
|
||||
uidCur.value = account.value.gameUid;
|
||||
} else {
|
||||
uidCur.value = uidList.value[0];
|
||||
}
|
||||
@@ -239,7 +237,10 @@ async function loadRole(): Promise<void> {
|
||||
}
|
||||
|
||||
async function refresh(): Promise<void> {
|
||||
if (!user.value) return;
|
||||
if (!account.value) {
|
||||
showSnackbar.warn("未获取到游戏账户");
|
||||
return;
|
||||
}
|
||||
if (showSelect.value) {
|
||||
showSelect.value = false;
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
@@ -248,7 +249,7 @@ async function refresh(): Promise<void> {
|
||||
showOverlay.value = false;
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
}
|
||||
if (uidCur.value && uidCur.value !== user.value.gameUid) {
|
||||
if (uidCur.value && uidCur.value !== account.value.gameUid) {
|
||||
const switchCheck = await showDialog.check(
|
||||
"是否切换游戏账户",
|
||||
`确认则尝试切换至${uidCur.value}`,
|
||||
@@ -260,24 +261,24 @@ async function refresh(): Promise<void> {
|
||||
}
|
||||
const freshCheck = await showDialog.check(
|
||||
"是否刷新角色数据",
|
||||
`用户${user.value.gameUid}与当前UID${uidCur.value}不一致`,
|
||||
`用户${account.value.gameUid}与当前UID${uidCur.value}不一致`,
|
||||
);
|
||||
if (!freshCheck) {
|
||||
showSnackbar.cancel("已取消角色数据刷新");
|
||||
return;
|
||||
}
|
||||
}
|
||||
await TGLogger.Info(`[Character][refreshRoles][${user.value.gameUid}] 正在更新角色数据`);
|
||||
showLoading.start("正在更新角色数据...", `UID: ${user.value.gameUid}`);
|
||||
await TGLogger.Info(`[Character][refreshRoles][${account.value.gameUid}] 正在更新角色数据`);
|
||||
showLoading.start("正在更新角色数据...", `UID: ${account.value.gameUid}`);
|
||||
loadData.value = true;
|
||||
if (!userStore.cookie.value) {
|
||||
if (!cookie.value) {
|
||||
showLoading.end();
|
||||
showSnackbar.warn("请先登录");
|
||||
loadData.value = false;
|
||||
return;
|
||||
}
|
||||
showLoading.update("正在更新角色数据...", "正在刷新首页数据");
|
||||
const indexRes = await TakumiRecordGenshinApi.index(userStore.cookie.value, user.value, 1);
|
||||
const indexRes = await TakumiRecordGenshinApi.index(cookie.value, account.value, 1);
|
||||
if ("retcode" in indexRes) {
|
||||
showSnackbar.error(`[${indexRes.retcode}] ${indexRes.message}`);
|
||||
await TGLogger.Error(JSON.stringify(indexRes.message));
|
||||
@@ -286,12 +287,12 @@ async function refresh(): Promise<void> {
|
||||
return;
|
||||
}
|
||||
showLoading.update("正在更新角色数据...", "正在获取角色列表");
|
||||
const listRes = await TakumiRecordGenshinApi.character.list(userStore.cookie.value, user.value);
|
||||
const listRes = await TakumiRecordGenshinApi.character.list(cookie.value, account.value);
|
||||
if (!Array.isArray(listRes)) {
|
||||
showSnackbar.error(`[${listRes.retcode}] ${listRes.message}`);
|
||||
await TGLogger.Error(`[Character][refreshRoles][${user.value.gameUid}] 获取角色列表失败`);
|
||||
await TGLogger.Error(`[Character][refreshRoles][${account.value.gameUid}] 获取角色列表失败`);
|
||||
await TGLogger.Error(
|
||||
`[Character][refreshRoles][${user.value.gameUid}] ${listRes.retcode} ${listRes.message}`,
|
||||
`[Character][refreshRoles][${account.value.gameUid}] ${listRes.retcode} ${listRes.message}`,
|
||||
);
|
||||
showLoading.end();
|
||||
loadData.value = false;
|
||||
@@ -299,47 +300,44 @@ async function refresh(): Promise<void> {
|
||||
}
|
||||
const idList = listRes.map((i) => i.id.toString());
|
||||
showLoading.update("正在更新角色数据...", `共${idList.length}个角色`);
|
||||
const res = await TakumiRecordGenshinApi.character.detail(
|
||||
userStore.cookie.value,
|
||||
user.value,
|
||||
idList,
|
||||
);
|
||||
const res = await TakumiRecordGenshinApi.character.detail(cookie.value, account.value, idList);
|
||||
if ("retcode" in res) {
|
||||
showSnackbar.error(`[${res.retcode}] ${res.message}`);
|
||||
await TGLogger.Error(`[Character][refreshRoles][${user.value.gameUid}] 获取角色数据失败`);
|
||||
await TGLogger.Error(`[Character][refreshRoles][${account.value.gameUid}] 获取角色数据失败`);
|
||||
await TGLogger.Error(
|
||||
`[Character][refreshRoles][${user.value.gameUid}] ${res.retcode} ${res.message}`,
|
||||
`[Character][refreshRoles][${account.value.gameUid}] ${res.retcode} ${res.message}`,
|
||||
);
|
||||
showLoading.end();
|
||||
loadData.value = false;
|
||||
return;
|
||||
}
|
||||
userStore.propMap.value = res.property_map;
|
||||
propMap.value = res.property_map;
|
||||
showLoading.update("正在更新角色数据...", "正在保存角色数据");
|
||||
await TSUserAvatar.saveAvatars(user.value.gameUid, res.list);
|
||||
await TGLogger.Info(`[Character][refreshRoles][${user.value.gameUid}] 成功更新角色数据`);
|
||||
await TSUserAvatar.saveAvatars(account.value.gameUid, res.list);
|
||||
await TGLogger.Info(`[Character][refreshRoles][${account.value.gameUid}] 成功更新角色数据`);
|
||||
await TGLogger.Info(
|
||||
`[Character][refreshRoles][${user.value.gameUid}] 共更新${res.list.length}个角色`,
|
||||
`[Character][refreshRoles][${account.value.gameUid}] 共更新${res.list.length}个角色`,
|
||||
);
|
||||
await loadUid();
|
||||
await loadRole();
|
||||
showLoading.end();
|
||||
loadData.value = false;
|
||||
}
|
||||
|
||||
async function share(): Promise<void> {
|
||||
if (!user.value || isEmpty.value) {
|
||||
if (!account.value || isEmpty.value) {
|
||||
showSnackbar.warn("暂无数据");
|
||||
return;
|
||||
}
|
||||
await TGLogger.Info(`[Character][shareRoles][${user.value.gameUid}] 正在生成分享图片`);
|
||||
await TGLogger.Info(`[Character][shareRoles][${account.value.gameUid}] 正在生成分享图片`);
|
||||
const rolesBox = <HTMLElement>document.querySelector(".uc-box");
|
||||
const fileName = `【角色列表】-${user.value.gameUid}`;
|
||||
const fileName = `【角色列表】-${account.value.gameUid}`;
|
||||
showLoading.start("正在生成图片", `${fileName}.png`);
|
||||
loadShare.value = true;
|
||||
await generateShareImg(fileName, rolesBox);
|
||||
showLoading.end();
|
||||
loadShare.value = false;
|
||||
await TGLogger.Info(`[Character][shareRoles][${user.value.gameUid}] 生成分享图片成功`);
|
||||
await TGLogger.Info(`[Character][shareRoles][${account.value.gameUid}] 生成分享图片成功`);
|
||||
}
|
||||
|
||||
async function deleteUid(): Promise<void> {
|
||||
|
||||
@@ -76,7 +76,7 @@
|
||||
</div>
|
||||
<TSubLine>统计周期 {{ item.startTime }} ~ {{ item.endTime }}</TSubLine>
|
||||
<TucOverview :data="item.stat" :fights="item.detail.fight_statisic" />
|
||||
<TSubLine>使用角色</TSubLine>
|
||||
<TSubLine>使用角色({{ item.detail.backup_avatars.length }}名)</TSubLine>
|
||||
<TucAvatars :model-value="item.detail.backup_avatars" />
|
||||
<TSubLine>详情</TSubLine>
|
||||
<div class="ucw-rounds">
|
||||
|
||||
@@ -12,10 +12,8 @@
|
||||
<v-btn prepend-icon="mdi-refresh" class="gacha-top-btn" @click="confirmRefresh(true)">
|
||||
全量刷新
|
||||
</v-btn>
|
||||
<v-btn prepend-icon="mdi-import" class="gacha-top-btn" @click="handleImportBtn(false)">
|
||||
导入
|
||||
</v-btn>
|
||||
<v-btn prepend-icon="mdi-import" class="gacha-top-btn" @click="handleImportBtn(true)">
|
||||
<v-btn prepend-icon="mdi-import" class="gacha-top-btn" @click="importUigf()">导入</v-btn>
|
||||
<v-btn prepend-icon="mdi-import" class="gacha-top-btn" @click="importUigf4()">
|
||||
导入(v4)
|
||||
</v-btn>
|
||||
<v-btn prepend-icon="mdi-export" class="gacha-top-btn" @click="exportUigf()">导出</v-btn>
|
||||
@@ -47,6 +45,7 @@
|
||||
</v-window-item>
|
||||
</v-window>
|
||||
</div>
|
||||
<UgoUid v-model="ovShow" :mode="ovMode" />
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { path } from "@tauri-apps/api";
|
||||
@@ -61,17 +60,12 @@ import GroEcharts from "../../components/userGacha/gro-echarts.vue";
|
||||
import GroHistory from "../../components/userGacha/gro-history.vue";
|
||||
import GroOverview from "../../components/userGacha/gro-overview.vue";
|
||||
import GroTable from "../../components/userGacha/gro-table.vue";
|
||||
import UgoUid from "../../components/userGacha/ugo-uid.vue";
|
||||
import { AppCharacterData, AppWeaponData } from "../../data/index.js";
|
||||
import TSUserGacha from "../../plugins/Sqlite/modules/userGacha.js";
|
||||
import { useUserStore } from "../../store/modules/user.js";
|
||||
import TGLogger from "../../utils/TGLogger.js";
|
||||
import {
|
||||
exportUigf4Data,
|
||||
exportUigfData,
|
||||
readUigf4Data,
|
||||
readUigfData,
|
||||
verifyUigfData,
|
||||
} from "../../utils/UIGF.js";
|
||||
import { exportUigfData, readUigfData, verifyUigfData } from "../../utils/UIGF.js";
|
||||
import Hk4eApi from "../../web/request/hk4eReq.js";
|
||||
import TakumiApi from "../../web/request/takumiReq.js";
|
||||
|
||||
@@ -86,6 +80,10 @@ const uidCur = ref<string>();
|
||||
const gachaListCur = ref<TGApp.Sqlite.GachaRecords.SingleTable[]>([]);
|
||||
const tab = ref<string>("overview");
|
||||
|
||||
// overlay
|
||||
const ovShow = ref<boolean>(false);
|
||||
const ovMode = ref<"export" | "import">("import");
|
||||
|
||||
// 监听 UID 变化
|
||||
watch(
|
||||
() => uidCur.value,
|
||||
@@ -243,9 +241,14 @@ async function refreshGachaPool(
|
||||
}
|
||||
}
|
||||
|
||||
// 导入按钮点击事件
|
||||
async function handleImportBtn(isV4: boolean): Promise<void> {
|
||||
await TGLogger.Info(`[UserGacha][handleImportBtn] 导入祈愿数据${isV4 ? "(v4)" : ""}`);
|
||||
// 导入 v4 版本的祈愿数据
|
||||
async function importUigf4(): Promise<void> {
|
||||
ovMode.value = "import";
|
||||
ovShow.value = true;
|
||||
}
|
||||
|
||||
async function importUigf(): Promise<void> {
|
||||
await TGLogger.Info(`[UserGacha][handleImportBtn] 导入祈愿数据`);
|
||||
const selectedFile = await open({
|
||||
multiple: false,
|
||||
title: "导入UIGF文件",
|
||||
@@ -257,40 +260,9 @@ async function handleImportBtn(isV4: boolean): Promise<void> {
|
||||
showSnackbar.cancel("已取消文件选择");
|
||||
return;
|
||||
}
|
||||
const check = await verifyUigfData(selectedFile, isV4);
|
||||
const check = await verifyUigfData(selectedFile, false);
|
||||
if (!check) return;
|
||||
if (isV4) await importUigf4(selectedFile);
|
||||
else await importUigf(selectedFile);
|
||||
}
|
||||
|
||||
// 导入 v4 版本的祈愿数据
|
||||
async function importUigf4(filePath: string): Promise<void> {
|
||||
const remoteData = await readUigf4Data(filePath);
|
||||
const uidCount = remoteData.hk4e.length;
|
||||
const dataCount = remoteData.hk4e.reduce((acc, cur) => acc + cur.list.length, 0);
|
||||
const importCheck = await showDialog.check(
|
||||
"是否导入祈愿数据?",
|
||||
`共 ${uidCount} 个 UID,${dataCount} 条数据`,
|
||||
);
|
||||
if (!importCheck) {
|
||||
showSnackbar.cancel("已取消祈愿数据导入");
|
||||
return;
|
||||
}
|
||||
showLoading.start("正在导入祈愿数据(v4)");
|
||||
for (const account of remoteData.hk4e) {
|
||||
showLoading.update("正在导入祈愿数据(v4)", `UID:${account.uid}`);
|
||||
await TSUserGacha.mergeUIGF4(account);
|
||||
}
|
||||
showLoading.end();
|
||||
showSnackbar.success(`成功导入 ${uidCount} 个 UID 的 ${dataCount} 条祈愿数据`);
|
||||
await TGLogger.Info(
|
||||
`[UserGacha][importUigf4] 成功导入 ${uidCount} 个 UID,${dataCount} 条祈愿数据`,
|
||||
);
|
||||
setTimeout(() => window.location.reload(), 1000);
|
||||
}
|
||||
|
||||
async function importUigf(filePath: string): Promise<void> {
|
||||
const remoteData = await readUigfData(filePath);
|
||||
const remoteData = await readUigfData(selectedFile);
|
||||
const importCheck = await showDialog.check(
|
||||
"是否导入祈愿数据?",
|
||||
`UID:${remoteData.info.uid},共 ${remoteData.list.length} 条数据`,
|
||||
@@ -353,47 +325,13 @@ async function exportUigf(): Promise<void> {
|
||||
|
||||
// 导出 UIGF v4 版本的祈愿数据
|
||||
async function exportUigf4(): Promise<void> {
|
||||
if (!uidCur.value) return;
|
||||
const exportCheck = await showDialog.check("确定导出UIGFv4格式的祈愿数据?");
|
||||
if (!exportCheck) {
|
||||
showSnackbar.cancel("已取消 UIGF v4 格式导出");
|
||||
if (!uidCur.value) {
|
||||
showSnackbar.error("未获取到 UID");
|
||||
return;
|
||||
}
|
||||
await TGLogger.Info(`[UserGacha][${uidCur.value}][exportUigf4] 导出祈愿数据(v4)`);
|
||||
// todo 单开一个overlay用于选取导出的UID
|
||||
const exportAllCheck = await showDialog.check(
|
||||
"是否导出所有 UID 的祈愿数据?",
|
||||
"取消则只导出当前 UID 的祈愿数据",
|
||||
);
|
||||
if (exportAllCheck === undefined) {
|
||||
showSnackbar.cancel("已取消 UIGF v4 格式导出");
|
||||
return;
|
||||
}
|
||||
if (!exportAllCheck) {
|
||||
const gachaList = await TSUserGacha.getGachaRecords(uidCur.value);
|
||||
if (gachaList.length === 0) {
|
||||
showSnackbar.error(`UID ${uidCur.value} 暂无祈愿数据`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
const file = await save({
|
||||
title: "选择导出祈愿数据的文件路径",
|
||||
filters: [{ name: "UIGF JSON", extensions: ["json"] }],
|
||||
defaultPath: `${await path.downloadDir()}${path.sep()}UIGF4.json`,
|
||||
});
|
||||
if (!file) {
|
||||
showSnackbar.cancel("已取消文件保存");
|
||||
return;
|
||||
}
|
||||
showLoading.start("正在导出祈愿数据", `路径:${file}`);
|
||||
if (!exportAllCheck) {
|
||||
await exportUigf4Data(file, uidCur.value);
|
||||
} else {
|
||||
await exportUigf4Data(file);
|
||||
}
|
||||
showLoading.end();
|
||||
showSnackbar.success("祈愿数据已成功导出");
|
||||
await TGLogger.Info(`[UserGacha][${uidCur.value}][exportUigf4] 导出祈愿数据完成`);
|
||||
ovMode.value = "export";
|
||||
ovShow.value = true;
|
||||
}
|
||||
|
||||
// 删除当前 UID 的祈愿数据
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<TwoSelectC v-model="showSelect" @select-c="handleSelect" v-model:reset="resetSelect" />
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { onBeforeMount, ref, watch } from "vue";
|
||||
import { onBeforeMount, ref, shallowRef, watch } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
|
||||
import showDialog from "../../components/func/dialog.js";
|
||||
@@ -35,10 +35,10 @@ import { AppCharacterData } from "../../data/index.js";
|
||||
import { createObc } from "../../utils/TGWindow.js";
|
||||
|
||||
const id = useRoute().params.id.toString() ?? "0";
|
||||
const showSelect = ref(false);
|
||||
const resetSelect = ref(false);
|
||||
const cardsInfo = ref(AppCharacterData);
|
||||
const curItem = ref<TGApp.App.Character.WikiBriefInfo>({
|
||||
const showSelect = ref<boolean>(false);
|
||||
const resetSelect = ref<boolean>(false);
|
||||
const cardsInfo = shallowRef<TGApp.App.Character.WikiBriefInfo[]>(AppCharacterData);
|
||||
const curItem = shallowRef<TGApp.App.Character.WikiBriefInfo>({
|
||||
id: 0,
|
||||
contentId: 0,
|
||||
name: "",
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<Suspense>
|
||||
<TwoMaterial v-model="visible" :data="curMaterial">
|
||||
<TwoMaterial v-model="visible" :data="curMaterial" v-if="curMaterial">
|
||||
<template #left>
|
||||
<div class="card-arrow left" @click="switchMaterial(false)">
|
||||
<img src="../../assets/icons/arrow-right.svg" alt="right" />
|
||||
@@ -70,47 +70,41 @@
|
||||
</Suspense>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref, watch } from "vue";
|
||||
import { onMounted, ref, shallowRef, watch } from "vue";
|
||||
|
||||
import showSnackbar from "../../components/func/snackbar.js";
|
||||
import TwoMaterial from "../../components/pageWiki/two-material.vue";
|
||||
import { WikiMaterialData } from "../../data/index.js";
|
||||
|
||||
const curMaterial = ref<TGApp.App.Material.WikiItem>(<TGApp.App.Material.WikiItem>{});
|
||||
const sortMaterialsData = ref<Array<TGApp.App.Material.WikiItem>>([]);
|
||||
const curIndex = ref(0);
|
||||
const total = ref(0);
|
||||
const visible = ref(false);
|
||||
const curMaterial = shallowRef<TGApp.App.Material.WikiItem | undefined>();
|
||||
const sortMaterialsData = shallowRef<Array<TGApp.App.Material.WikiItem>>([]);
|
||||
const curIndex = ref<number>(0);
|
||||
const total = ref<number>(0);
|
||||
const visible = ref<boolean>(false);
|
||||
|
||||
interface MaterialType {
|
||||
type: string;
|
||||
number: number;
|
||||
}
|
||||
type MaterialType = { type: string; number: number };
|
||||
|
||||
const search = ref<string>();
|
||||
const selectType = ref<string | null>(null);
|
||||
const materialTypes = ref<MaterialType[]>([]);
|
||||
|
||||
onMounted(() => {
|
||||
WikiMaterialData.forEach((item: TGApp.App.Material.WikiItem) => {
|
||||
for (const item of WikiMaterialData) {
|
||||
const typeFindIndex = materialTypes.value.findIndex((itemT) => itemT.type === item.type);
|
||||
if (typeFindIndex !== -1) {
|
||||
materialTypes.value[typeFindIndex].number++;
|
||||
} else {
|
||||
if (typeFindIndex === -1) {
|
||||
const itemN: MaterialType = { type: item.type, number: 1 };
|
||||
materialTypes.value.push(itemN);
|
||||
continue;
|
||||
}
|
||||
});
|
||||
materialTypes.value[typeFindIndex].number++;
|
||||
}
|
||||
sortData(WikiMaterialData);
|
||||
showSnackbar.success(`成功获取${sortMaterialsData.value.length}条数据`);
|
||||
});
|
||||
|
||||
function getSelectMaterials(): TGApp.App.Material.WikiItem[] {
|
||||
if (selectType.value === null) {
|
||||
return WikiMaterialData;
|
||||
} else {
|
||||
return WikiMaterialData.filter((item) => item.type === selectType.value);
|
||||
}
|
||||
if (selectType.value === null) return WikiMaterialData;
|
||||
else return WikiMaterialData.filter((item) => item.type === selectType.value);
|
||||
}
|
||||
|
||||
watch(
|
||||
@@ -133,14 +127,10 @@ function toMaterial(item: TGApp.App.Material.WikiItem) {
|
||||
|
||||
function switchMaterial(isNext: boolean) {
|
||||
if (isNext) {
|
||||
if (curIndex.value === total.value - 1) {
|
||||
return;
|
||||
}
|
||||
if (curIndex.value === total.value - 1) return;
|
||||
curIndex.value++;
|
||||
} else {
|
||||
if (curIndex.value === 0) {
|
||||
return;
|
||||
}
|
||||
if (curIndex.value === 0) return;
|
||||
curIndex.value--;
|
||||
}
|
||||
curMaterial.value = sortMaterialsData.value[curIndex.value];
|
||||
|
||||
@@ -6,19 +6,19 @@
|
||||
label="搜索"
|
||||
:hide-details="true"
|
||||
variant="outlined"
|
||||
@click:prepend-inner="searchNamecard"
|
||||
@keyup.enter="searchNamecard"
|
||||
@click:prepend-inner="searchNameCard()"
|
||||
@keyup.enter="searchNameCard()"
|
||||
/>
|
||||
<div class="tw-nc-list">
|
||||
<v-virtual-scroll :items="sortNameCardsData" :item-height="80">
|
||||
<template #default="{ item }">
|
||||
<TopNamecard :data="item" @selected="toNameCard" />
|
||||
<TopNameCard :data="item" @selected="showNameCard(item)" />
|
||||
<div style="height: 10px" />
|
||||
</template>
|
||||
</v-virtual-scroll>
|
||||
</div>
|
||||
</div>
|
||||
<ToNamecard v-model="visible" :data="curNameCard">
|
||||
<ToNameCard v-model="visible" :data="curNameCard">
|
||||
<template #left>
|
||||
<div class="card-arrow left" @click="switchCard(false)">
|
||||
<img src="../../assets/icons/arrow-right.svg" alt="right" />
|
||||
@@ -29,28 +29,26 @@
|
||||
<img src="../../assets/icons/arrow-right.svg" alt="right" />
|
||||
</div>
|
||||
</template>
|
||||
</ToNamecard>
|
||||
</ToNameCard>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from "vue";
|
||||
import { onMounted, ref, shallowRef } from "vue";
|
||||
|
||||
import ToNamecard from "../../components/app/to-namecard.vue";
|
||||
import TopNamecard from "../../components/app/top-namecard.vue";
|
||||
import ToNameCard from "../../components/app/to-namecard.vue";
|
||||
import TopNameCard from "../../components/app/top-namecard.vue";
|
||||
import showSnackbar from "../../components/func/snackbar.js";
|
||||
import { AppNameCardsData } from "../../data/index.js";
|
||||
|
||||
const curNameCard = ref<TGApp.App.NameCard.Item>();
|
||||
const sortNameCardsData = ref<TGApp.App.NameCard.Item[]>([]);
|
||||
const curIndex = ref(0);
|
||||
const total = ref(0);
|
||||
const visible = ref(false);
|
||||
const search = ref("");
|
||||
const curNameCard = shallowRef<TGApp.App.NameCard.Item>();
|
||||
const sortNameCardsData = shallowRef<TGApp.App.NameCard.Item[]>([]);
|
||||
const curIndex = ref<number>(0);
|
||||
const total = ref<number>(0);
|
||||
const visible = ref<boolean>(false);
|
||||
const search = ref<string>();
|
||||
|
||||
onMounted(() => {
|
||||
sortData(AppNameCardsData);
|
||||
});
|
||||
onMounted(() => sortData(AppNameCardsData));
|
||||
|
||||
function sortData(data: TGApp.App.NameCard.Item[]) {
|
||||
function sortData(data: TGApp.App.NameCard.Item[]): void {
|
||||
sortNameCardsData.value = data.sort((a, b) => a.type - b.type || a.index - b.index);
|
||||
curIndex.value = 0;
|
||||
total.value = sortNameCardsData.value.length;
|
||||
@@ -58,48 +56,45 @@ function sortData(data: TGApp.App.NameCard.Item[]) {
|
||||
showSnackbar.success(`共搜索到 ${sortNameCardsData.value.length} 个结果`);
|
||||
}
|
||||
|
||||
function toNameCard(item: TGApp.App.NameCard.Item) {
|
||||
function showNameCard(item: TGApp.App.NameCard.Item): void {
|
||||
curNameCard.value = item;
|
||||
curIndex.value = sortNameCardsData.value.findIndex((i) => i.name === item.name);
|
||||
visible.value = true;
|
||||
}
|
||||
|
||||
function switchCard(isNext: boolean) {
|
||||
if (isNext) {
|
||||
if (curIndex.value === total.value - 1) {
|
||||
showSnackbar.warn("已经是最后一个了");
|
||||
return;
|
||||
}
|
||||
curIndex.value++;
|
||||
} else {
|
||||
if (curIndex.value === 0) {
|
||||
showSnackbar.warn("已经是第一个了");
|
||||
return;
|
||||
}
|
||||
curIndex.value--;
|
||||
function switchCard(isNext: boolean): void {
|
||||
if (isNext && curIndex.value === total.value - 1) {
|
||||
showSnackbar.warn("已经是最后一个了");
|
||||
return;
|
||||
}
|
||||
if (!isNext && curIndex.value === 0) {
|
||||
showSnackbar.warn("已经是第一个了");
|
||||
return;
|
||||
}
|
||||
curIndex.value += isNext ? 1 : -1;
|
||||
curNameCard.value = sortNameCardsData.value[curIndex.value];
|
||||
}
|
||||
|
||||
function searchNamecard() {
|
||||
if (!search.value) {
|
||||
function searchNameCard(): void {
|
||||
if (search.value === undefined) {
|
||||
sortData(AppNameCardsData);
|
||||
} else if (search.value === "") {
|
||||
return;
|
||||
}
|
||||
if (search.value === "") {
|
||||
if (sortNameCardsData.value.length === AppNameCardsData.length) {
|
||||
showSnackbar.warn("请先输入搜索内容");
|
||||
} else {
|
||||
sortData(AppNameCardsData);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
const searchResult = AppNameCardsData.filter((item) => {
|
||||
return (
|
||||
item.name.includes(search.value) ||
|
||||
item.desc.includes(search.value) ||
|
||||
item.source.includes(search.value)
|
||||
);
|
||||
});
|
||||
sortData(searchResult);
|
||||
sortData(AppNameCardsData);
|
||||
return;
|
||||
}
|
||||
const searchResult = AppNameCardsData.filter(
|
||||
(item) =>
|
||||
item.name.includes(search.value!) ||
|
||||
item.desc.includes(search.value!) ||
|
||||
item.source.includes(search.value!),
|
||||
);
|
||||
sortData(searchResult);
|
||||
}
|
||||
</script>
|
||||
<style lang="css" scoped>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
v-model:cur-item="curItem"
|
||||
:key="index"
|
||||
:data="item"
|
||||
@click="switchW(item)"
|
||||
@click="curItem = item"
|
||||
mode="weapon"
|
||||
/>
|
||||
</div>
|
||||
@@ -26,7 +26,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onBeforeMount, ref } from "vue";
|
||||
import { onBeforeMount, ref, shallowRef } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
|
||||
import showDialog from "../../components/func/dialog.js";
|
||||
@@ -38,10 +38,10 @@ import { AppWeaponData } from "../../data/index.js";
|
||||
import { createObc } from "../../utils/TGWindow.js";
|
||||
|
||||
const id = useRoute().params.id.toString() ?? "0";
|
||||
const showSelect = ref(false);
|
||||
const resetSelect = ref(false);
|
||||
const cardsInfo = ref(AppWeaponData);
|
||||
const curItem = ref<TGApp.App.Weapon.WikiBriefInfo>({
|
||||
const showSelect = ref<boolean>(false);
|
||||
const resetSelect = ref<boolean>(false);
|
||||
const cardsInfo = shallowRef<Array<TGApp.App.Weapon.WikiBriefInfo>>(AppWeaponData);
|
||||
const curItem = shallowRef<TGApp.App.Weapon.WikiBriefInfo>({
|
||||
id: 0,
|
||||
contentId: 0,
|
||||
name: "",
|
||||
@@ -82,10 +82,6 @@ function handleSelectW(val: SelectedWValue) {
|
||||
cardsInfo.value = filterW;
|
||||
}
|
||||
|
||||
async function switchW(item: TGApp.App.Weapon.WikiBriefInfo): Promise<void> {
|
||||
curItem.value = item;
|
||||
}
|
||||
|
||||
async function toOuter(item?: TGApp.App.Weapon.WikiBriefInfo): Promise<void> {
|
||||
if (!item) return;
|
||||
if (item.contentId === 0) {
|
||||
|
||||
@@ -61,11 +61,7 @@
|
||||
</v-list-item>
|
||||
<v-list-item title="分享设置">
|
||||
<template #subtitle>
|
||||
{{
|
||||
appStore.shareDefaultFile === true
|
||||
? "默认保存到文件"
|
||||
: `默认保存到剪贴板,超过${appStore.shareDefaultFile}MB时保存到文件`
|
||||
}}
|
||||
默认保存到剪贴板,超过{{ appStore.shareDefaultFile }}MB时保存到文件
|
||||
</template>
|
||||
<template #prepend>
|
||||
<div class="config-icon">
|
||||
@@ -84,6 +80,7 @@
|
||||
</template>
|
||||
<v-list-item-title @click="confirmUpdateDevice()">刷新设备信息</v-list-item-title>
|
||||
<v-list-item-subtitle>
|
||||
<!-- @ts-expect-error-next-line Deprecated symbol used -->
|
||||
{{ appStore.deviceInfo.device_name }}({{ appStore.deviceInfo.product }}) -
|
||||
{{ appStore.deviceInfo.device_fp }}
|
||||
</v-list-item-subtitle>
|
||||
@@ -255,7 +252,7 @@ async function confirmShare(): Promise<void> {
|
||||
"阈值:",
|
||||
appStore.shareDefaultFile.toString(),
|
||||
);
|
||||
if (input === null) {
|
||||
if (input === undefined) {
|
||||
showSnackbar.cancel("已取消修改分享设置");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
<div class="home-select">
|
||||
<v-select
|
||||
variant="outlined"
|
||||
v-model="showHome"
|
||||
:items="homeStore.getShowItems()"
|
||||
v-model="showItems"
|
||||
:items="showItemsAll"
|
||||
:hide-details="true"
|
||||
:multiple="true"
|
||||
:chips="true"
|
||||
@@ -32,7 +32,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, onUnmounted, ref, shallowRef, toRaw } from "vue";
|
||||
import { type Component, computed, onMounted, ref, shallowRef, watch } from "vue";
|
||||
|
||||
import TGameNav from "../../components/app/t-gamenav.vue";
|
||||
import showLoading from "../../components/func/loading.js";
|
||||
@@ -41,33 +41,55 @@ import PhCompCalendar from "../../components/pageHome/ph-comp-calendar.vue";
|
||||
import PhCompPool from "../../components/pageHome/ph-comp-pool.vue";
|
||||
import PhCompPosition from "../../components/pageHome/ph-comp-position.vue";
|
||||
import { useAppStore } from "../../store/modules/app.js";
|
||||
import { useHomeStore } from "../../store/modules/home.js";
|
||||
import { ShowItemEnum, useHomeStore } from "../../store/modules/home.js";
|
||||
import TGLogger from "../../utils/TGLogger.js";
|
||||
import TGConstant from "../../web/constant/TGConstant.js";
|
||||
|
||||
// store
|
||||
type SFComp = Component & {
|
||||
__file?: string;
|
||||
__hmrId?: string;
|
||||
__name?: string;
|
||||
__scopeId?: string;
|
||||
};
|
||||
|
||||
const appStore = useAppStore();
|
||||
const homeStore = useHomeStore();
|
||||
|
||||
// data
|
||||
const endNum = ref<number>(0);
|
||||
const components = shallowRef<any[]>([]);
|
||||
const showHome = ref<string[]>(homeStore.getShowValue());
|
||||
const loadComp = ref<string[]>(toRaw(showHome.value));
|
||||
const showItemsAll: Array<ShowItemEnum> = [
|
||||
ShowItemEnum.calendar,
|
||||
ShowItemEnum.pool,
|
||||
ShowItemEnum.position,
|
||||
];
|
||||
const showItems = computed<ShowItemEnum[]>({
|
||||
get: () => homeStore.getShowItems(),
|
||||
set: (v: ShowItemEnum[]) => homeStore.setShowItems(v),
|
||||
});
|
||||
const loadItems = shallowRef<ShowItemEnum[]>([]);
|
||||
const components = shallowRef<SFComp[]>([]);
|
||||
|
||||
const gameSelectList = TGConstant.BBS.CHANNELS;
|
||||
const curGid = ref<string>(gameSelectList[0].gid);
|
||||
|
||||
onMounted(async () => {
|
||||
showLoading.start("正在加载首页...");
|
||||
// @ts-expect-error-next-line
|
||||
// @ts-expect-error-next-line The import.meta meta-property is not allowed in files which will build into CommonJS output.
|
||||
const isProdEnv = import.meta.env.MODE === "production";
|
||||
// 获取当前环境
|
||||
if (isProdEnv && appStore.devMode) {
|
||||
appStore.devMode = false;
|
||||
}
|
||||
const temp = [];
|
||||
for (const item of showHome.value) {
|
||||
if (isProdEnv && appStore.devMode) appStore.devMode = false;
|
||||
await loadComp();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => components.value,
|
||||
(cur, old) => {
|
||||
const newComp = cur.filter((i) => !old.includes(i));
|
||||
if (newComp.length === 0) showLoading.end();
|
||||
},
|
||||
);
|
||||
|
||||
async function loadComp(): Promise<void> {
|
||||
showLoading.start("正在加载首页...");
|
||||
const temp: SFComp[] = [];
|
||||
for (const item of showItems.value) {
|
||||
switch (item) {
|
||||
case "限时祈愿":
|
||||
temp.push(PhCompPool);
|
||||
@@ -78,54 +100,46 @@ onMounted(async () => {
|
||||
case "素材日历":
|
||||
temp.push(PhCompCalendar);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
const items = showHome.value.join("、");
|
||||
showLoading.update("正在加载首页...", `正在加载:${items}`);
|
||||
showLoading.update("正在加载首页...", `正在加载:${showItems.value.join("、")}`);
|
||||
components.value = temp;
|
||||
await TGLogger.Info(`[Home][onMounted] 打开首页,当前显示:${items}`);
|
||||
});
|
||||
await TGLogger.Info(`[Home][loadComp] 打开首页,当前显示:${showItems.value.join("、")}`);
|
||||
}
|
||||
|
||||
async function submitHome(): Promise<void> {
|
||||
const show = showHome.value;
|
||||
if (show.length < 1) {
|
||||
if (showItems.value.length === 0) {
|
||||
showSnackbar.warn("请至少选择一个!");
|
||||
return;
|
||||
}
|
||||
homeStore.setShowValue(show);
|
||||
showSnackbar.success("设置成功!");
|
||||
await TGLogger.Info("[Home][submitHome] 首页设置成功,当前显示:" + show.join("、"));
|
||||
setTimeout(() => window.location.reload(), 1000);
|
||||
await TGLogger.Info("[Home][submitHome] 首页设置成功,当前显示:" + showItems.value.join("、"));
|
||||
loadItems.value = showItems.value.filter((i) => loadItems.value.includes(i));
|
||||
await loadComp();
|
||||
}
|
||||
|
||||
function getName(name: string): string {
|
||||
function getName(name: string): ShowItemEnum | undefined {
|
||||
switch (name) {
|
||||
case "ph-comp-pool":
|
||||
return "限时祈愿";
|
||||
return ShowItemEnum.pool;
|
||||
case "ph-comp-position":
|
||||
return "近期活动";
|
||||
return ShowItemEnum.position;
|
||||
case "ph-comp-calendar":
|
||||
return "素材日历";
|
||||
return ShowItemEnum.calendar;
|
||||
default:
|
||||
return "";
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// 组件加载完成
|
||||
async function loadEnd(item: any): Promise<void> {
|
||||
await TGLogger.Info(`[Home][loadEnd] ${item.__name} 加载完成`);
|
||||
loadComp.value = loadComp.value.filter((v) => v !== getName(item.__name));
|
||||
endNum.value++;
|
||||
if (endNum.value === components.value.length) {
|
||||
showLoading.end();
|
||||
return;
|
||||
}
|
||||
showLoading.update("正在加载首页...", `正在加载:${loadComp.value.join("、")}`);
|
||||
async function loadEnd(item: SFComp): Promise<void> {
|
||||
const compName = getName(item.__name ?? "");
|
||||
if (!compName) return;
|
||||
await TGLogger.Info(`[Home][loadEnd] ${compName} 加载完成`);
|
||||
showLoading.update("正在加载首页...", `${compName} 加载完成`);
|
||||
if (!loadItems.value.includes(compName)) loadItems.value.push(compName);
|
||||
else showSnackbar.warn(`${compName} 已加载`);
|
||||
if (loadItems.value.length === components.value.length) showLoading.end();
|
||||
}
|
||||
|
||||
onUnmounted(() => (components.value = []));
|
||||
</script>
|
||||
<style lang="css" scoped>
|
||||
.home-container {
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
:key="index"
|
||||
:value="value"
|
||||
@click="firstLoad(value)"
|
||||
>{{ rawData[value].name }}
|
||||
>
|
||||
{{ rawData[value].name }}
|
||||
</v-tab>
|
||||
</v-tabs>
|
||||
</template>
|
||||
@@ -66,83 +67,44 @@ import showSnackbar from "../../components/func/snackbar.js";
|
||||
import ToChannel from "../../components/pageNews/to-channel.vue";
|
||||
import VpOverlaySearch from "../../components/viewPost/vp-overlay-search.vue";
|
||||
import Mys from "../../plugins/Mys/index.js";
|
||||
import { useAppStore } from "../../store/modules/app.js";
|
||||
import { NewsType, NewsTypeEnum, useAppStore } from "../../store/modules/app.js";
|
||||
import TGLogger from "../../utils/TGLogger.js";
|
||||
import { createPost } from "../../utils/TGWindow.js";
|
||||
import { getGameName } from "../../web/utils/tools.js";
|
||||
|
||||
// 类型定义
|
||||
enum NewsType {
|
||||
notice = "1",
|
||||
activity = "2",
|
||||
news = "3",
|
||||
}
|
||||
|
||||
type NewsKey = keyof typeof NewsType;
|
||||
type PostData = {
|
||||
[key in NewsKey]: TGApp.Plugins.Mys.Post.FullData[];
|
||||
};
|
||||
type PostData = { [key in NewsType]: TGApp.Plugins.Mys.Post.FullData[] };
|
||||
type RawData = {
|
||||
[key in NewsKey]: {
|
||||
isLast: boolean;
|
||||
name: string;
|
||||
lastId: number;
|
||||
};
|
||||
[key in NewsType]: { isLast: boolean; name: string; lastId: number };
|
||||
};
|
||||
|
||||
// 路由
|
||||
const router = useRouter();
|
||||
const appStore = useAppStore();
|
||||
const gid = <string>useRoute().params.gid;
|
||||
const gameName = getGameName(Number(gid));
|
||||
// loading
|
||||
const loading = ref<boolean>(false);
|
||||
|
||||
// UI 数据
|
||||
const appStore = useAppStore();
|
||||
const tabValues: Readonly<Array<NewsType>> = ["notice", "activity", "news"];
|
||||
const showList = ref<boolean>(false);
|
||||
const showSearch = ref<boolean>(false);
|
||||
const tabValues = ref<Array<NewsKey>>(["notice", "activity", "news"]);
|
||||
const tabList = ["notice", "activity", "news"];
|
||||
const tab = computed({
|
||||
const tab = computed<NewsType>({
|
||||
get: () => {
|
||||
if (appStore.recentNewsType === "" || !tabList.includes(appStore.recentNewsType)) {
|
||||
return "notice";
|
||||
}
|
||||
return <NewsKey>appStore.recentNewsType;
|
||||
},
|
||||
set: (val) => {
|
||||
appStore.recentNewsType = val;
|
||||
if (!(appStore.recentNewsType satisfies NewsType)) return "notice";
|
||||
return appStore.recentNewsType;
|
||||
},
|
||||
set: (v) => (appStore.recentNewsType = v),
|
||||
});
|
||||
|
||||
// 渲染数据
|
||||
const search = ref<string>("");
|
||||
const postData = ref<PostData>({
|
||||
notice: [],
|
||||
activity: [],
|
||||
news: [],
|
||||
});
|
||||
const postData = ref<PostData>({ notice: [], activity: [], news: [] });
|
||||
const rawData = ref<RawData>({
|
||||
notice: {
|
||||
isLast: false,
|
||||
name: "公告",
|
||||
lastId: 0,
|
||||
},
|
||||
activity: {
|
||||
isLast: false,
|
||||
name: "活动",
|
||||
lastId: 0,
|
||||
},
|
||||
news: {
|
||||
isLast: false,
|
||||
name: "咨讯",
|
||||
lastId: 0,
|
||||
},
|
||||
notice: { isLast: false, name: "公告", lastId: 0 },
|
||||
activity: { isLast: false, name: "活动", lastId: 0 },
|
||||
news: { isLast: false, name: "咨讯", lastId: 0 },
|
||||
});
|
||||
|
||||
onMounted(async () => await firstLoad(tab.value));
|
||||
|
||||
async function firstLoad(key: NewsKey, refresh: boolean = false): Promise<void> {
|
||||
async function firstLoad(key: NewsType, refresh: boolean = false): Promise<void> {
|
||||
if (rawData.value[key].lastId !== 0) {
|
||||
if (!refresh) return;
|
||||
postData.value[key] = [];
|
||||
@@ -150,7 +112,7 @@ async function firstLoad(key: NewsKey, refresh: boolean = false): Promise<void>
|
||||
}
|
||||
showLoading.start(`正在获取${gameName}${rawData.value[key].name}数据...`);
|
||||
document.documentElement.scrollTo({ top: 0, behavior: "smooth" });
|
||||
const getData = await Mys.Painter.getNewsList(gid, NewsType[key]);
|
||||
const getData = await Mys.Painter.getNewsList(gid, NewsTypeEnum[key]);
|
||||
rawData.value[key].isLast = getData.is_last;
|
||||
rawData.value[key].lastId = getData.list.length;
|
||||
postData.value[key] = getData.list;
|
||||
@@ -165,7 +127,7 @@ async function switchAnno(): Promise<void> {
|
||||
}
|
||||
|
||||
// 加载更多
|
||||
async function loadMore(key: NewsKey): Promise<void> {
|
||||
async function loadMore(key: NewsType): Promise<void> {
|
||||
loading.value = true;
|
||||
if (rawData.value[key].isLast) {
|
||||
showSnackbar.warn("已经是最后一页了");
|
||||
@@ -173,7 +135,12 @@ async function loadMore(key: NewsKey): Promise<void> {
|
||||
return;
|
||||
}
|
||||
showLoading.start(`正在获取${gameName}${rawData.value[key].name}数据...`);
|
||||
const getData = await Mys.Painter.getNewsList(gid, NewsType[key], 20, rawData.value[key].lastId);
|
||||
const getData = await Mys.Painter.getNewsList(
|
||||
gid,
|
||||
NewsTypeEnum[key],
|
||||
20,
|
||||
rawData.value[key].lastId,
|
||||
);
|
||||
rawData.value[key].lastId = rawData.value[key].lastId + getData.list.length;
|
||||
rawData.value[key].isLast = getData.is_last;
|
||||
postData.value[key] = postData.value[key].concat(getData.list);
|
||||
@@ -195,10 +162,10 @@ async function searchPost(): Promise<void> {
|
||||
const numCheck = Number(search.value);
|
||||
if (isNaN(numCheck)) {
|
||||
showSearch.value = true;
|
||||
} else {
|
||||
await createPost(search.value);
|
||||
showSearch.value = false;
|
||||
return;
|
||||
}
|
||||
await createPost(search.value);
|
||||
showSearch.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import TGHttp from "../../../utils/TGHttp.js";
|
||||
import { getDeviceInfo } from "../../../utils/toolFunc.js";
|
||||
import TGConstant from "../../../web/constant/TGConstant.js";
|
||||
|
||||
const PUB_KEY_STR = `-----BEGIN PUBLIC KEY-----
|
||||
const PUB_KEY_STR: Readonly<string> = `-----BEGIN PUBLIC KEY-----
|
||||
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDDvekdPMHN3AYhm/vktJT+YJr7cI5DcsNKqdsx5DZX0gDuWFuIjzdwButrIYPNmRJ1G8ybDIF7oDW2eEpm5sMbL9zs
|
||||
9ExXCdvqrn51qELbqj0XxtMTIpaCHFSI50PfPpTFV9Xt/hmyVwokoOXFlAEgCn+Q
|
||||
CgGs52bFoYMtyi+xEQIDAQAB
|
||||
@@ -55,7 +55,7 @@ export async function getCaptcha(
|
||||
"x-rpc-aigis": aigis || "",
|
||||
"x-rpc-app_version": TGConstant.BBS.VERSION,
|
||||
"x-rpc-client_type": "2",
|
||||
"x-rpc-app_id": TGConstant.BBS.APP_ID,
|
||||
"x-rpc-app_id": "bll8iq97cem8",
|
||||
"x-rpc-device_fp": device_fp,
|
||||
"x-rpc-device_name": device_name,
|
||||
"x-rpc-device_id": device_id,
|
||||
@@ -63,7 +63,7 @@ export async function getCaptcha(
|
||||
"user-agent": TGConstant.BBS.UA_MOBILE,
|
||||
"content-type": "application/json",
|
||||
referer: "https://user.miyoushe.com/",
|
||||
"x-rpc-game_biz": TGConstant.GAME_BIZ,
|
||||
"x-rpc-game_biz": "hk4e_cn",
|
||||
};
|
||||
const resp = await TGHttp<
|
||||
TGApp.Plugins.Mys.CaptchaLogin.CaptchaResponse | TGApp.BBS.Response.Base
|
||||
@@ -117,7 +117,7 @@ export async function doCaptchaLogin(
|
||||
"x-rpc-aigis": aigis || "",
|
||||
"x-rpc-app_version": TGConstant.BBS.VERSION,
|
||||
"x-rpc-client_type": "2",
|
||||
"x-rpc-app_id": TGConstant.BBS.APP_ID,
|
||||
"x-rpc-app_id": "bll8iq97cem8",
|
||||
"x-rpc-device_fp": device_fp,
|
||||
"x-rpc-device_name": device_name,
|
||||
"x-rpc-device_id": device_id,
|
||||
|
||||
@@ -18,15 +18,15 @@ const APP_ID = 8;
|
||||
export async function getLoginQr(): Promise<
|
||||
TGApp.Plugins.Mys.GameLogin.GetLoginQrData | TGApp.BBS.Response.Base
|
||||
> {
|
||||
const url = "https://hk4e-sdk.mihoyo.com/hk4e_cn/combo/panda/qrcode/fetch";
|
||||
const device = getDeviceInfo("device_id");
|
||||
const data = { app_id: APP_ID, device };
|
||||
const header = getRequestHeader({}, "POST", data, "common");
|
||||
const data: Record<string, string | number> = {
|
||||
app_id: APP_ID,
|
||||
device: getDeviceInfo("device_id"),
|
||||
};
|
||||
const resp = await TGHttp<
|
||||
TGApp.Plugins.Mys.GameLogin.GetLoginQrResponse | TGApp.BBS.Response.Base
|
||||
>(url, {
|
||||
>("https://hk4e-sdk.mihoyo.com/hk4e_cn/combo/panda/qrcode/fetch", {
|
||||
method: "POST",
|
||||
headers: header,
|
||||
headers: getRequestHeader({}, "POST", data),
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
if (resp.retcode !== 0) return <TGApp.BBS.Response.Base>resp;
|
||||
@@ -42,15 +42,16 @@ export async function getLoginQr(): Promise<
|
||||
export async function getLoginStatus(
|
||||
ticket: string,
|
||||
): Promise<TGApp.Plugins.Mys.GameLogin.GetLoginStatusData | TGApp.BBS.Response.Base> {
|
||||
const url = "https://hk4e-sdk.mihoyo.com/hk4e_cn/combo/panda/qrcode/query";
|
||||
const device = getDeviceInfo("device_id");
|
||||
const data = { app_id: APP_ID, device, ticket };
|
||||
const header = getRequestHeader({}, "POST", data, "common");
|
||||
const data: Record<string, string | number> = {
|
||||
app_id: APP_ID,
|
||||
device: getDeviceInfo("device_id"),
|
||||
ticket,
|
||||
};
|
||||
const resp = await TGHttp<
|
||||
TGApp.Plugins.Mys.GameLogin.GetLoginStatusResponse | TGApp.BBS.Response.Base
|
||||
>(url, {
|
||||
>("https://hk4e-sdk.mihoyo.com/hk4e_cn/combo/panda/qrcode/query", {
|
||||
method: "POST",
|
||||
headers: header,
|
||||
headers: getRequestHeader({}, "POST", data),
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
if (resp.retcode !== 0) return <TGApp.BBS.Response.Base>resp;
|
||||
|
||||
@@ -174,22 +174,6 @@ class Sqlite {
|
||||
);
|
||||
await this.initDB();
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 检测特定表是否存在
|
||||
* @since Beta v0.4.5
|
||||
* @param {string} table 表名
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async checkTableExist(table: string): Promise<boolean> {
|
||||
const db = await this.getDB();
|
||||
const sql = `SELECT name
|
||||
FROM sqlite_master
|
||||
WHERE type = 'table'
|
||||
AND name = '${table}';`;
|
||||
const res: Array<{ name: string }> = await db.select(sql);
|
||||
return res.length > 0;
|
||||
}
|
||||
}
|
||||
|
||||
const TGSqlite = new Sqlite();
|
||||
|
||||
@@ -18,6 +18,14 @@ const dbDataPath = `${await path.appConfigDir()}${path.sep()}TeyvatGuide.db`;
|
||||
// 用于存放日志的路径
|
||||
const logDataDir = await path.appLogDir();
|
||||
|
||||
export enum NewsTypeEnum {
|
||||
notice = "1",
|
||||
activity = "2",
|
||||
news = "3",
|
||||
}
|
||||
|
||||
export type NewsType = keyof typeof NewsTypeEnum;
|
||||
|
||||
export const useAppStore = defineStore(
|
||||
"app",
|
||||
() => {
|
||||
@@ -46,7 +54,7 @@ export const useAppStore = defineStore(
|
||||
// 语言
|
||||
const lang = ref<AnnoLang>("zh-cn");
|
||||
// 最近的咨讯类型
|
||||
const recentNewsType = ref<string>("notice");
|
||||
const recentNewsType = ref<NewsType>("notice");
|
||||
// 是否开启分辨率回正
|
||||
const needResize = ref<string>("true");
|
||||
// 分享图生成默认设置,为0表示默认保存到文件,为数字表示当大小超过xMB时保存到文件,否则保存到剪贴板
|
||||
|
||||
@@ -1,125 +1,51 @@
|
||||
/**
|
||||
* @file store modules home.ts
|
||||
* @file store/modules/home.ts
|
||||
* @description Home store module
|
||||
* @since Alpha v0.1.6
|
||||
* @since Beta v0.6.5
|
||||
*/
|
||||
|
||||
import { defineStore } from "pinia";
|
||||
import { ref } from "vue";
|
||||
|
||||
export const useHomeStore = defineStore(
|
||||
"home",
|
||||
() => {
|
||||
const calendarShow = ref({
|
||||
show: true,
|
||||
order: 3,
|
||||
});
|
||||
const poolShow = ref({
|
||||
show: true,
|
||||
order: 1,
|
||||
});
|
||||
const positionShow = ref({
|
||||
show: true,
|
||||
order: 2,
|
||||
});
|
||||
const homeShow = ref({
|
||||
calendarShow,
|
||||
poolShow,
|
||||
positionShow,
|
||||
});
|
||||
const poolCover = ref({} satisfies Record<number, string>);
|
||||
export const enum ShowItemEnum {
|
||||
calendar = "素材日历",
|
||||
pool = "限时祈愿",
|
||||
position = "近期活动",
|
||||
}
|
||||
|
||||
function init(): void {
|
||||
calendarShow.value = {
|
||||
show: true,
|
||||
order: 3,
|
||||
};
|
||||
poolShow.value = {
|
||||
show: true,
|
||||
order: 1,
|
||||
};
|
||||
positionShow.value = {
|
||||
show: true,
|
||||
order: 2,
|
||||
};
|
||||
poolCover.value = {};
|
||||
export type ShowItem = { show: boolean; order: number; label: ShowItemEnum };
|
||||
|
||||
export const useHomeStore = defineStore("home", () => {
|
||||
const homeShow = ref<Array<ShowItem>>([
|
||||
{ show: true, order: 1, label: ShowItemEnum.pool },
|
||||
{ show: true, order: 2, label: ShowItemEnum.position },
|
||||
{ show: true, order: 3, label: ShowItemEnum.calendar },
|
||||
]);
|
||||
const poolCover = ref<Record<number, string>>();
|
||||
|
||||
function getShowItems(): Array<ShowItemEnum> {
|
||||
const homeShowLocal = localStorage.getItem("homeShow");
|
||||
if (homeShowLocal === null || !Array.isArray(JSON.parse(homeShowLocal))) {
|
||||
localStorage.setItem("homeShow", JSON.stringify(homeShow.value));
|
||||
}
|
||||
homeShow.value = JSON.parse(localStorage.getItem("homeShow")!);
|
||||
return homeShow.value
|
||||
.filter((item) => item.show)
|
||||
.sort((a, b) => a.order - b.order)
|
||||
.map((item) => item.label);
|
||||
}
|
||||
|
||||
function getShowItems(): string[] {
|
||||
const defaultList = ["素材日历", "限时祈愿", "近期活动"];
|
||||
defaultList.sort((a, b) => {
|
||||
return getItemOrder(a) - getItemOrder(b);
|
||||
});
|
||||
return defaultList;
|
||||
function setShowItems(items: Array<ShowItemEnum>): void {
|
||||
let order = 1;
|
||||
for (const item of items) {
|
||||
const findIdx = homeShow.value.findIndex((i) => i.label === item);
|
||||
if (findIdx === -1) continue;
|
||||
homeShow.value[findIdx].show = true;
|
||||
homeShow.value[findIdx].order = order++;
|
||||
}
|
||||
for (const item of homeShow.value) if (!items.includes(item.label)) item.show = false;
|
||||
localStorage.setItem("homeShow", JSON.stringify(homeShow.value));
|
||||
}
|
||||
|
||||
function getShowValue(): string[] {
|
||||
const showValue = [];
|
||||
if (calendarShow.value.show) showValue.push("素材日历");
|
||||
if (poolShow.value.show) showValue.push("限时祈愿");
|
||||
if (positionShow.value.show) showValue.push("近期活动");
|
||||
showValue.sort((a, b) => {
|
||||
return getItemOrder(a) - getItemOrder(b);
|
||||
});
|
||||
return showValue;
|
||||
}
|
||||
|
||||
function getItemOrder(item: string): number {
|
||||
if (item === "素材日历") return calendarShow.value.order;
|
||||
if (item === "限时祈愿") return poolShow.value.order;
|
||||
if (item === "近期活动") return positionShow.value.order;
|
||||
return 4;
|
||||
}
|
||||
|
||||
function setShowValue(value: string[]): void {
|
||||
let order = 1;
|
||||
// 遍历 value
|
||||
value.forEach((item) => {
|
||||
if (!getShowItems().includes(item)) {
|
||||
throw new Error("传入的值不在可选范围内");
|
||||
}
|
||||
if (item === "素材日历") {
|
||||
calendarShow.value.order = order;
|
||||
calendarShow.value.show = true;
|
||||
order++;
|
||||
}
|
||||
if (item === "限时祈愿") {
|
||||
poolShow.value.order = order;
|
||||
poolShow.value.show = true;
|
||||
order++;
|
||||
}
|
||||
if (item === "近期活动") {
|
||||
positionShow.value.order = order;
|
||||
positionShow.value.show = true;
|
||||
order++;
|
||||
}
|
||||
});
|
||||
// 遍历 getShowItems()
|
||||
getShowItems().forEach((item) => {
|
||||
if (!value.includes(item)) {
|
||||
if (item === "素材日历") {
|
||||
calendarShow.value.show = false;
|
||||
}
|
||||
if (item === "限时祈愿") {
|
||||
poolShow.value.show = false;
|
||||
}
|
||||
if (item === "近期活动") {
|
||||
positionShow.value.show = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
homeShow,
|
||||
poolCover,
|
||||
init,
|
||||
getShowItems,
|
||||
getShowValue,
|
||||
setShowValue,
|
||||
};
|
||||
},
|
||||
{
|
||||
persist: true,
|
||||
},
|
||||
);
|
||||
return { poolCover, getShowItems, setShowItems };
|
||||
});
|
||||
|
||||
2
src/types/Game/Avatar.d.ts
vendored
2
src/types/Game/Avatar.d.ts
vendored
@@ -175,7 +175,7 @@ declare namespace TGApp.Game.Avatar {
|
||||
desc: string;
|
||||
affix_level: number;
|
||||
main_property: Prop;
|
||||
sub_property: Prop;
|
||||
sub_property: Prop | null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
12
src/types/Game/Combat.d.ts
vendored
12
src/types/Game/Combat.d.ts
vendored
@@ -75,7 +75,7 @@ declare namespace TGApp.Game.Combat {
|
||||
avatar_id: number;
|
||||
avatar_icon: string;
|
||||
value: string;
|
||||
rarity: number;
|
||||
rarity?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -276,7 +276,7 @@ declare namespace TGApp.Game.Combat {
|
||||
/**
|
||||
* @description 战斗数据
|
||||
* @interface FightStatisic
|
||||
* @since Beta v0.6.3
|
||||
* @since Beta v0.6.5
|
||||
* @property {AvatarMini} max_defeat_avatar 击败最多敌人
|
||||
* @property {AvatarMini} max_damage_avatar 最高伤害输出
|
||||
* @property {AvatarMini} max_take_damage_avatar 最高承受伤害
|
||||
@@ -287,10 +287,10 @@ declare namespace TGApp.Game.Combat {
|
||||
* @return FightStatisic
|
||||
*/
|
||||
interface FightStatisic {
|
||||
max_defeat_avatar: AvatarMini;
|
||||
max_damage_avatar: AvatarMini;
|
||||
max_take_damage_avatar: AvatarMini;
|
||||
total_coin_consumed: AvatarMini;
|
||||
max_defeat_avatar: AvatarMini | null;
|
||||
max_damage_avatar: AvatarMini | null;
|
||||
max_take_damage_avatar: AvatarMini | null;
|
||||
total_coin_consumed: AvatarMini | null;
|
||||
shortest_avatar_list: Array<AvatarMini>;
|
||||
total_use_time: number;
|
||||
is_show_battle_stats: boolean;
|
||||
|
||||
13
src/types/Plugins/UIAF.d.ts
vendored
13
src/types/Plugins/UIAF.d.ts
vendored
@@ -57,17 +57,4 @@ declare namespace TGApp.Plugins.UIAF {
|
||||
current: number;
|
||||
status: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* @interface Backup
|
||||
* @description 数据备份时的格式,用于标识不同存档
|
||||
* @since Beta v0.6.0
|
||||
* @property {number} uid - 存档UID
|
||||
* @property {Achievement[]} data - 存档数据
|
||||
* @returns Backup
|
||||
*/
|
||||
interface Backup {
|
||||
uid: number;
|
||||
data: Achievement[];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,8 @@
|
||||
* @since Beta v0.6.3
|
||||
*/
|
||||
|
||||
import { event, core, webviewWindow } from "@tauri-apps/api";
|
||||
import type { Event } from "@tauri-apps/api/event";
|
||||
import type { UnlistenFn } from "@tauri-apps/api/event";
|
||||
import { core, event, webviewWindow } from "@tauri-apps/api";
|
||||
import type { Event, UnlistenFn } from "@tauri-apps/api/event";
|
||||
|
||||
import showSnackbar from "../components/func/snackbar.js";
|
||||
import TGSqlite from "../plugins/Sqlite/index.js";
|
||||
@@ -615,7 +614,7 @@ class TGClient {
|
||||
* @returns {void} - 无返回值
|
||||
*/
|
||||
async getDS(arg: TGApp.Plugins.JSBridge.NullArg): Promise<void> {
|
||||
const data = { DS: getDS4JS("lk2", 1, undefined, undefined) };
|
||||
const data = { DS: getDS4JS("LK2", 1, undefined, undefined) };
|
||||
await this.callback(arg.callback, data);
|
||||
}
|
||||
|
||||
@@ -630,7 +629,7 @@ class TGClient {
|
||||
arg: TGApp.Plugins.JSBridge.Arg<TGApp.Plugins.JSBridge.GetDS2Payload>,
|
||||
): Promise<void> {
|
||||
const data = {
|
||||
DS: getDS4JS("common", 2, arg.payload.body, arg.payload.query),
|
||||
DS: getDS4JS("X4", 2, arg.payload.body, arg.payload.query),
|
||||
};
|
||||
await this.callback(arg.callback, data);
|
||||
}
|
||||
|
||||
@@ -66,9 +66,7 @@ export async function createPost(
|
||||
}
|
||||
const postPath = `/post_detail/${postId}`;
|
||||
await createTGWindow(postPath, "Sub_window", postTitle, 960, 720, false, false);
|
||||
TGLogger.Info(`[createPost][${postId}] 打开帖子`).catch((err) => {
|
||||
console.error(err);
|
||||
});
|
||||
await TGLogger.Info(`[createPost][${postId}] 打开帖子`);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* @file utils/UIGF.ts
|
||||
* @description UIGF工具类
|
||||
* @since Beta v0.5.5
|
||||
* @since Beta v0.6.5
|
||||
*/
|
||||
|
||||
import { app, path } from "@tauri-apps/api";
|
||||
@@ -53,7 +53,7 @@ async function getUigfHeader(uid: string): Promise<TGApp.Plugins.UIGF.Info> {
|
||||
* @since Beta v0.5.1
|
||||
* @returns {TGApp.Plugins.UIGF.Info4} UIGF v4.0 头部信息
|
||||
*/
|
||||
async function getUigf4Header(): Promise<TGApp.Plugins.UIGF.Info4> {
|
||||
export async function getUigf4Header(): Promise<TGApp.Plugins.UIGF.Info4> {
|
||||
const stamp = Date.now();
|
||||
return {
|
||||
export_timestamp: Math.floor(stamp / 1000).toString(),
|
||||
@@ -90,14 +90,14 @@ function convertDataToUigf(
|
||||
|
||||
/**
|
||||
* @description 检测是否存在 UIGF 数据,采用 ajv 验证 schema
|
||||
* @since Beta v0.5.0
|
||||
* @since Beta v0.6.5
|
||||
* @param {string} path - UIGF 数据路径
|
||||
* @param {boolean} isVersion4 - 是否为 UIGF v4.0
|
||||
* @returns {Promise<boolean>} 是否存在 UIGF 数据
|
||||
*/
|
||||
export async function verifyUigfData(path: string, isVersion4: boolean = false): Promise<boolean> {
|
||||
const fileData: string = await readTextFile(path);
|
||||
try {
|
||||
const fileData: string = await readTextFile(path);
|
||||
const fileJson = JSON.parse(fileData);
|
||||
if (isVersion4) return validateUigf4Data(fileJson);
|
||||
return validateUigfData(fileJson);
|
||||
@@ -194,31 +194,15 @@ export async function exportUigfData(
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 导出 UIGF v4.0 数据
|
||||
* @since Beta v0.5.0
|
||||
* @param {string} filePath - 保存路径
|
||||
* @param {string} uid - UID,如果为空表示导出所有数据
|
||||
* @returns {Promise<void>}
|
||||
* @description 获取单项UID的UIGF4.0数据
|
||||
* @param {string} uid - UID
|
||||
* @returns {Promise<TGApp.Plugins.UIGF.GachaHk4e>}
|
||||
*/
|
||||
export async function exportUigf4Data(filePath: string, uid?: string): Promise<void> {
|
||||
const UigfData: TGApp.Plugins.UIGF.Schema4 = {
|
||||
info: await getUigf4Header(),
|
||||
hk4e: [],
|
||||
export async function getUigf4Item(uid: string): Promise<TGApp.Plugins.UIGF.GachaHk4e> {
|
||||
const gachaList = await TSUserGacha.getGachaRecords(uid);
|
||||
return {
|
||||
uid: uid,
|
||||
timezone: getUigfTimeZone(uid),
|
||||
list: convertDataToUigf(gachaList),
|
||||
};
|
||||
let uidList: string[] = [];
|
||||
if (uid) {
|
||||
uidList.push(uid);
|
||||
} else {
|
||||
uidList = await TSUserGacha.getUidList();
|
||||
}
|
||||
for (const uid of uidList) {
|
||||
const gachaList = await TSUserGacha.getGachaRecords(uid);
|
||||
const data: TGApp.Plugins.UIGF.GachaHk4e = {
|
||||
uid: uid,
|
||||
timezone: getUigfTimeZone(uid),
|
||||
list: convertDataToUigf(gachaList),
|
||||
};
|
||||
UigfData.hk4e.push(data);
|
||||
}
|
||||
await writeTextFile(filePath, JSON.stringify(UigfData));
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
/**
|
||||
* @file src/utils/linkParser.ts
|
||||
* @description 处理链接
|
||||
* @since Beta v0.6.3
|
||||
* @since Beta v0.6.5
|
||||
*/
|
||||
|
||||
import { emit } from "@tauri-apps/api/event";
|
||||
|
||||
import showDialog from "../components/func/dialog.js";
|
||||
import showSnackbar from "../components/func/snackbar.js";
|
||||
import { getGameId } from "../web/utils/tools.js";
|
||||
|
||||
import TGClient from "./TGClient.js";
|
||||
import { createPost } from "./TGWindow.js";
|
||||
@@ -56,7 +57,7 @@ export async function parsePost(link: string): Promise<false | string> {
|
||||
|
||||
/**
|
||||
* @function parseLink
|
||||
* @since Beta v0.6.3
|
||||
* @since Beta v0.6.5
|
||||
* @description 处理链接
|
||||
* @param {string} link - 链接
|
||||
* @param {boolean} useInner - 是否采用内置 JSBridge 打开
|
||||
@@ -118,6 +119,14 @@ export async function parseLink(
|
||||
}
|
||||
await createPost(postId);
|
||||
return true;
|
||||
} else if (url.pathname.includes("/topicDetail/")) {
|
||||
const regex = /\/(\w+)\/topicDetail\/(\d+)/;
|
||||
const result = url.pathname.match(regex);
|
||||
if (!result) return false;
|
||||
const [, game, topicId] = result;
|
||||
const id = getGameId(game);
|
||||
await emit("active_deep_link", `router?path=/posts/topic/${id}/${topicId}`);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (url.hostname === "webstatic.mihoyo.com") {
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from "vue";
|
||||
import { onMounted, shallowRef } from "vue";
|
||||
import JsonViewer from "vue-json-viewer";
|
||||
import { useRoute } from "vue-router";
|
||||
|
||||
@@ -21,8 +21,8 @@ const route = useRoute();
|
||||
const annoId = Number(route.params.anno_id);
|
||||
const region = <AnnoServer>route.params.region;
|
||||
const lang = <AnnoLang>route.params.lang;
|
||||
const jsonList = ref<TGApp.BBS.Announcement.AnnoSingle>();
|
||||
const jsonContent = ref<TGApp.BBS.Announcement.ContentItem>();
|
||||
const jsonList = shallowRef<TGApp.BBS.Announcement.AnnoSingle>();
|
||||
const jsonContent = shallowRef<TGApp.BBS.Announcement.ContentItem>();
|
||||
|
||||
onMounted(async () => {
|
||||
showLoading.start("正在获取公告数据...");
|
||||
@@ -32,11 +32,14 @@ onMounted(async () => {
|
||||
}
|
||||
showLoading.update("正在获取数据...", `公告ID: ${annoId}`);
|
||||
const listData = await Hk4eApi.anno.list(region, lang);
|
||||
listData.list.map((item: TGApp.BBS.Announcement.ListItem) => {
|
||||
return item.list.map((single: TGApp.BBS.Announcement.AnnoSingle) => {
|
||||
return single.ann_id === annoId ? (jsonList.value = single) : null;
|
||||
});
|
||||
});
|
||||
for (const listItem of listData.list) {
|
||||
for (const single of listItem.list) {
|
||||
if (single.ann_id === annoId) {
|
||||
jsonList.value = single;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
jsonContent.value = await Hk4eApi.anno.content(annoId, region, lang);
|
||||
showLoading.end();
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<TSwitchTheme />
|
||||
<TPinWin />
|
||||
<TShareBtn v-model="annoRef" :title="annoTitle" />
|
||||
<TShareBtn selector=".anno-body" :title="`Anno_${route.params.anno_id}`" />
|
||||
<div class="anno-body" v-if="annoData">
|
||||
<div class="anno-info">AnnoID: {{ annoId }} | Render by TeyvatGuide v{{ appVersion }}</div>
|
||||
<div class="anno-title">{{ annoData.title }}</div>
|
||||
@@ -13,7 +13,7 @@
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { app, webviewWindow } from "@tauri-apps/api";
|
||||
import { ref, onMounted } from "vue";
|
||||
import { onMounted, ref, shallowRef } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
|
||||
import TPinWin from "../components/app/t-pinWin.vue";
|
||||
@@ -26,16 +26,12 @@ import TGLogger from "../utils/TGLogger.js";
|
||||
import { createTGWindow } from "../utils/TGWindow.js";
|
||||
import Hk4eApi, { AnnoLang, AnnoServer } from "../web/request/hk4eReq.js";
|
||||
|
||||
const annoRef = ref<HTMLElement>(<HTMLElement>{});
|
||||
const annoTitle = ref<string>("");
|
||||
|
||||
// 数据
|
||||
const route = useRoute();
|
||||
const annoId = Number(route.params.anno_id);
|
||||
const region = <AnnoServer>route.params.region;
|
||||
const lang = <AnnoLang>route.params.lang;
|
||||
const appVersion = ref<string>();
|
||||
const annoData = ref<TGApp.BBS.Announcement.ContentItem | undefined>();
|
||||
const annoData = shallowRef<TGApp.BBS.Announcement.ContentItem | undefined>();
|
||||
|
||||
onMounted(async () => {
|
||||
showLoading.start("正在加载公告数据...");
|
||||
@@ -49,11 +45,9 @@ onMounted(async () => {
|
||||
try {
|
||||
annoData.value = await Hk4eApi.anno.content(annoId, region, lang);
|
||||
showLoading.update("正在渲染数据...", `公告ID:${annoId}`);
|
||||
annoTitle.value = `Anno_${annoId}`;
|
||||
await webviewWindow
|
||||
.getCurrentWebviewWindow()
|
||||
.setTitle(`Anno_${annoId} ${annoData.value.title}`);
|
||||
annoRef.value = <HTMLElement>document.querySelector(".anno-body");
|
||||
} catch (error) {
|
||||
if (error instanceof Error)
|
||||
await TGLogger.Error(`[t-anno.vue][${annoId}] ${error.name}:${error.message}`);
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from "vue";
|
||||
import { onMounted, ref, shallowRef } from "vue";
|
||||
import JsonViewer from "vue-json-viewer";
|
||||
import { useRoute } from "vue-router";
|
||||
|
||||
@@ -19,8 +19,8 @@ import Mys from "../plugins/Mys/index.js";
|
||||
import TGLogger from "../utils/TGLogger.js";
|
||||
|
||||
const postId = Number(useRoute().params.post_id);
|
||||
const jsonData = ref<TGApp.Plugins.Mys.Post.FullData>();
|
||||
const parseData = ref<TGApp.Plugins.Mys.SctPost.Base[]>();
|
||||
const jsonData = shallowRef<TGApp.Plugins.Mys.Post.FullData>();
|
||||
const parseData = shallowRef<TGApp.Plugins.Mys.SctPost.Base[]>();
|
||||
const isEmpty = ref<boolean>(false);
|
||||
|
||||
onMounted(async () => {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<TSwitchTheme />
|
||||
<TPinWin />
|
||||
<VpBtnCollect :model-value="postId" :data="postData" />
|
||||
<TShareBtn v-model="postRef" :title="shareTitle" />
|
||||
<TShareBtn selector=".tp-post-body" :title="`Post_${postId}`" />
|
||||
<VpBtnReply :gid="postData.post.game_id" :post-id="postData.post.post_id" v-if="postData" />
|
||||
<div class="tp-post-body" v-if="postData">
|
||||
<div class="tp-post-info">
|
||||
@@ -83,10 +83,9 @@
|
||||
/>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { app } from "@tauri-apps/api";
|
||||
import { webviewWindow } from "@tauri-apps/api";
|
||||
import { app, webviewWindow } from "@tauri-apps/api";
|
||||
import { emit } from "@tauri-apps/api/event";
|
||||
import { nextTick, onMounted, onUnmounted, ref } from "vue";
|
||||
import { nextTick, onMounted, onUnmounted, ref, shallowRef } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
|
||||
import TPinWin from "../components/app/t-pinWin.vue";
|
||||
@@ -106,21 +105,14 @@ import TGLogger from "../utils/TGLogger.js";
|
||||
import { createTGWindow } from "../utils/TGWindow.js";
|
||||
import TGConstant from "../web/constant/TGConstant.js";
|
||||
|
||||
// share
|
||||
const postRef = ref<HTMLElement>(<HTMLElement>{});
|
||||
const shareTitle = ref<string>("");
|
||||
|
||||
// 数据
|
||||
const appVersion = ref<string>();
|
||||
const postId = Number(useRoute().params.post_id);
|
||||
const renderPost = ref<TGApp.Plugins.Mys.SctPost.Base[]>([]);
|
||||
const postData = ref<TGApp.Plugins.Mys.Post.FullData>();
|
||||
// 当前时间,秒级时间戳
|
||||
const showCollection = ref<boolean>(false);
|
||||
const shareTime = ref<number>(Math.floor(Date.now() / 1000));
|
||||
// eslint-disable-next-line no-undef
|
||||
const shareTimeTimer = ref<NodeJS.Timeout | undefined>(undefined);
|
||||
// 合集
|
||||
const showCollection = ref<boolean>(false);
|
||||
const renderPost = shallowRef<TGApp.Plugins.Mys.SctPost.Base[]>([]);
|
||||
const postData = shallowRef<TGApp.Plugins.Mys.Post.FullData>();
|
||||
|
||||
function getGameIcon(gameId: number): string {
|
||||
const find = TGConstant.BBS.CHANNELS.find((item) => item.gid === gameId.toString());
|
||||
@@ -129,8 +121,8 @@ function getGameIcon(gameId: number): string {
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
showLoading.start(`正在加载帖子数据...`);
|
||||
appVersion.value = await app.getVersion();
|
||||
showLoading.start(`正在加载帖子数据...`);
|
||||
// 检查数据
|
||||
if (!postId) {
|
||||
showLoading.empty("未找到数据", "PostID 不存在");
|
||||
@@ -150,7 +142,6 @@ onMounted(async () => {
|
||||
postData.value = resp;
|
||||
showLoading.update("正在渲染数据...", `帖子ID: ${postId}`);
|
||||
renderPost.value = getRenderPost(postData.value);
|
||||
shareTitle.value = `Post_${postId}`;
|
||||
await webviewWindow
|
||||
.getCurrentWebviewWindow()
|
||||
.setTitle(`Post_${postId} ${postData.value.post.subject}`);
|
||||
@@ -167,7 +158,6 @@ onMounted(async () => {
|
||||
shareTimeTimer.value = undefined;
|
||||
}
|
||||
shareTimeTimer.value = setInterval(() => (shareTime.value = Math.floor(Date.now() / 1000)), 1000);
|
||||
postRef.value = <HTMLElement>document.querySelector(".tp-post-body");
|
||||
showLoading.end();
|
||||
});
|
||||
|
||||
|
||||
11
src/vite-env.d.ts
vendored
11
src/vite-env.d.ts
vendored
@@ -50,14 +50,3 @@ interface ImportMetaEnv {
|
||||
declare interface ImportMeta {
|
||||
readonly env: ImportMetaEnv;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 极验验证的请求方法-请求参数
|
||||
* @param {TGApp.BBS.Geetest.InitGeetestParams} params
|
||||
* @param {(captchaObj: TGApp.BBS.Geetest.GeetestCaptcha) => void} callback
|
||||
* @return void
|
||||
*/
|
||||
declare function initGeetest(
|
||||
params: TGApp.Plugins.Mys.Geetest.InitGeetestParams,
|
||||
callback: (captchaObj: TGApp.Plugins.Mys.Geetest.GeetestCaptcha) => void,
|
||||
): void;
|
||||
|
||||
@@ -4,26 +4,14 @@
|
||||
* @since Beta v0.6.3
|
||||
*/
|
||||
|
||||
import {
|
||||
BBS_APP_ID,
|
||||
BBS_SALT,
|
||||
BBS_UA_MOBILE,
|
||||
BBS_UA_PC,
|
||||
BBS_VERSION,
|
||||
CHANNEL_LIST,
|
||||
} from "./bbs.js";
|
||||
import { GAME_BIZ } from "./utils.js";
|
||||
import { BBS_UA_MOBILE, BBS_VERSION, CHANNEL_LIST } from "./bbs.js";
|
||||
|
||||
const TGConstant = {
|
||||
BBS: {
|
||||
VERSION: BBS_VERSION,
|
||||
UA_PC: BBS_UA_PC,
|
||||
UA_MOBILE: BBS_UA_MOBILE,
|
||||
APP_ID: BBS_APP_ID,
|
||||
CHANNELS: CHANNEL_LIST,
|
||||
},
|
||||
Salt: BBS_SALT,
|
||||
GAME_BIZ,
|
||||
};
|
||||
|
||||
export default TGConstant;
|
||||
|
||||
@@ -1,83 +1,75 @@
|
||||
/**
|
||||
* @file web/constant/bbs.ts
|
||||
* @description 常量-应用数据
|
||||
* @since Beta v0.6.3
|
||||
* @since Beta v0.6.5
|
||||
*/
|
||||
|
||||
export const BBS_VERSION = "2.77.2";
|
||||
export const BBS_UA_PC = `Mozilla/5.0 (Windows NT 10.0; Win64; x64) miHoYoBBS/${BBS_VERSION}`;
|
||||
export const BBS_VERSION = "2.78.1";
|
||||
export const BBS_UA_MOBILE = `Mozilla/5.0 (Linux; Android 12) Mobile miHoYoBBS/${BBS_VERSION}`;
|
||||
export const BBS_APP_ID = "bll8iq97cem8";
|
||||
|
||||
/**
|
||||
* @description salt 值
|
||||
* @version 2.77.2
|
||||
* @since Beta v0.6.3
|
||||
*/
|
||||
export const BBS_SALT = {
|
||||
K2: "TKAsNctXTFctUMIgTfkHncRKJjvvRuOf",
|
||||
LK2: "LfWCPiYcIZyzGUgeFUbuJZNOyqpLoNlv",
|
||||
X4: "xV8v4Qu54lUKrEYFZkJhB8cuOh9Asafs",
|
||||
X6: "t0qEgfub6cvueAPgR5m9aQWWVciEer7v",
|
||||
PROD: "t0qEgfub6cvueAPgR5m9aQWWVciEer7v",
|
||||
};
|
||||
|
||||
/**
|
||||
* @description 频道列表
|
||||
* @version 2.72.2
|
||||
* @since Beta v0.5.1
|
||||
* @since Beta v0.6.5
|
||||
* @interface ToChannelItem
|
||||
* @property {string} title - 频道名称
|
||||
* @property {string} icon - 频道图标
|
||||
* @property {string} gid - 频道 gid
|
||||
* @property {string} mini - 频道简称
|
||||
* @return ToChannelItem
|
||||
*/
|
||||
export interface ToChannelItem {
|
||||
title: string;
|
||||
icon: string;
|
||||
gid: string;
|
||||
mini: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 渠道列表
|
||||
* @version 2.72.2
|
||||
* @since Beta v0.5.1
|
||||
* @since Beta v0.6.5
|
||||
* @type {Array<ToChannelItem>}
|
||||
*/
|
||||
export const CHANNEL_LIST: ToChannelItem[] = [
|
||||
export const CHANNEL_LIST: Readonly<Array<ToChannelItem>> = [
|
||||
{
|
||||
title: "原神",
|
||||
icon: "/platforms/mhy/ys.webp",
|
||||
gid: "2",
|
||||
mini: "ys",
|
||||
},
|
||||
{
|
||||
title: "崩坏:星穹铁道",
|
||||
icon: "/platforms/mhy/sr.webp",
|
||||
gid: "6",
|
||||
mini: "sr",
|
||||
},
|
||||
{
|
||||
title: "绝区零",
|
||||
icon: "/platforms/mhy/zzz.webp",
|
||||
gid: "8",
|
||||
mini: "zzz",
|
||||
},
|
||||
{
|
||||
title: "崩坏3",
|
||||
icon: "/platforms/mhy/bh3.webp",
|
||||
gid: "1",
|
||||
mini: "bh3",
|
||||
},
|
||||
{
|
||||
title: "崩坏2",
|
||||
icon: "/platforms/mhy/bh2.webp",
|
||||
gid: "3",
|
||||
mini: "bh2",
|
||||
},
|
||||
{
|
||||
title: "未定事件簿",
|
||||
icon: "/platforms/mhy/wd.webp",
|
||||
gid: "4",
|
||||
mini: "wd",
|
||||
},
|
||||
{
|
||||
title: "大别野",
|
||||
icon: "/platforms/mhy/dby.webp",
|
||||
gid: "5",
|
||||
mini: "dby",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
/**
|
||||
* @file web constant utils.ts
|
||||
* @description 一些杂项的常量
|
||||
* @author BTMuli<bt-muli@outlook.com>
|
||||
* @since Alpha v0.2.0
|
||||
*/
|
||||
|
||||
// game_biz
|
||||
export const GAME_BIZ = "hk4e_cn";
|
||||
@@ -16,15 +16,12 @@ import { getRequestHeader } from "../utils/getRequestHeader.js";
|
||||
async function getUserFullInfo(
|
||||
cookie: TGApp.App.Account.Cookie,
|
||||
): Promise<TGApp.BBS.Response.Base | TGApp.Plugins.Mys.User.Info> {
|
||||
const url = "https://bbs-api.miyoushe.com/user/wapi/getUserFullInfo";
|
||||
const ck = { cookie_token: cookie.cookie_token, account_id: cookie.account_id };
|
||||
const params = { gids: "2" };
|
||||
const header = getRequestHeader(ck, "GET", params, "common", true);
|
||||
const resp = await TGHttp<TGApp.Plugins.Mys.User.HomeResponse | TGApp.BBS.Response.Base>(url, {
|
||||
method: "GET",
|
||||
headers: header,
|
||||
query: params,
|
||||
});
|
||||
const resp = await TGHttp<TGApp.Plugins.Mys.User.HomeResponse | TGApp.BBS.Response.Base>(
|
||||
"https://bbs-api.miyoushe.com/user/wapi/getUserFullInfo",
|
||||
{ method: "GET", headers: getRequestHeader(ck, "GET", params, "X4", true), query: params },
|
||||
);
|
||||
if (resp.retcode !== 0) return <TGApp.BBS.Response.Base>resp;
|
||||
return resp.data.user_info;
|
||||
}
|
||||
@@ -42,22 +39,16 @@ async function userFavouritePost(
|
||||
uid: string,
|
||||
offset: string = "",
|
||||
): Promise<TGApp.BBS.Collection.PostRespData | TGApp.BBS.Response.Base> {
|
||||
const url = "https://bbs-api.miyoushe.com/post/wapi/userFavouritePost";
|
||||
const ck = { cookie_token: cookie.cookie_token, account_id: cookie.account_id };
|
||||
const params = { size: "20", uid, offset };
|
||||
const header = getRequestHeader(ck, "GET", params, "common");
|
||||
const resp = await TGHttp<TGApp.BBS.Collection.PostResponse | TGApp.BBS.Response.Base>(url, {
|
||||
method: "GET",
|
||||
headers: header,
|
||||
query: params,
|
||||
});
|
||||
const resp = await TGHttp<TGApp.BBS.Collection.PostResponse | TGApp.BBS.Response.Base>(
|
||||
"https://bbs-api.miyoushe.com/post/wapi/userFavouritePost",
|
||||
{ method: "GET", headers: getRequestHeader(ck, "GET", params), query: params },
|
||||
);
|
||||
if (resp.retcode !== 0) return <TGApp.BBS.Response.Base>resp;
|
||||
return resp.data;
|
||||
}
|
||||
|
||||
const BBSApi = {
|
||||
userInfo: getUserFullInfo,
|
||||
lovePost: userFavouritePost,
|
||||
};
|
||||
const BBSApi = { userInfo: getUserFullInfo, lovePost: userFavouritePost };
|
||||
|
||||
export default BBSApi;
|
||||
|
||||
@@ -17,8 +17,29 @@ export enum AnnoServer {
|
||||
|
||||
export type AnnoLang = "zh-cn" | "zh-tw" | "en" | "ja";
|
||||
|
||||
const AnnoApi = "https://hk4e-ann-api.mihoyo.com/common/hk4e_cn/announcement/api";
|
||||
const AnnoApiGlobal = "https://sg-hk4e-api.hoyoverse.com/common/hk4e_global/announcement/api";
|
||||
const AnnoApi: Readonly<string> = "https://hk4e-ann-api.mihoyo.com/common/hk4e_cn/announcement/api";
|
||||
const AnnoApiGlobal: Readonly<string> =
|
||||
"https://sg-hk4e-api.hoyoverse.com/common/hk4e_global/announcement/api";
|
||||
|
||||
/**
|
||||
* @description 判断是否为国内服务器
|
||||
* @since Beta v0.6.5
|
||||
* @param {AnnoServer} region 服务器
|
||||
* @returns {boolean} 是否为国内服务器
|
||||
*/
|
||||
function isCN(region: AnnoServer): boolean {
|
||||
return region === AnnoServer.CN_ISLAND || region === AnnoServer.CN_TREE;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 根据服务器获取公告地址
|
||||
* @since Beta v0.6.5
|
||||
* @param {AnnoServer} region 服务器
|
||||
* @returns {string} 公告地址
|
||||
*/
|
||||
function getAnnoApi(region: AnnoServer): string {
|
||||
return isCN(region) ? AnnoApi : AnnoApiGlobal;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 获取游戏内公告参数
|
||||
@@ -31,22 +52,16 @@ function getAnnoParams(
|
||||
region: AnnoServer = AnnoServer.CN_ISLAND,
|
||||
lang: AnnoLang = "zh-cn",
|
||||
): TGApp.BBS.Announcement.Params {
|
||||
const params: TGApp.BBS.Announcement.Params = {
|
||||
return {
|
||||
game: "hk4e",
|
||||
game_biz: "hk4e_cn",
|
||||
game_biz: isCN(region) ? "hk4e_cn" : "hk4e_global",
|
||||
lang,
|
||||
bundle_id: "hk4e_cn",
|
||||
bundle_id: isCN(region) ? "hk4e_cn" : "hk4e_global",
|
||||
platform: "pc",
|
||||
region,
|
||||
level: "55",
|
||||
uid: "100000000",
|
||||
};
|
||||
if (region === AnnoServer.CN_ISLAND || region === AnnoServer.CN_TREE) {
|
||||
return params;
|
||||
}
|
||||
params.game_biz = "hk4e_global";
|
||||
params.bundle_id = "hk4e_global";
|
||||
return params;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -60,15 +75,10 @@ async function getAnnoList(
|
||||
region: AnnoServer = AnnoServer.CN_ISLAND,
|
||||
lang: AnnoLang = "zh-cn",
|
||||
): Promise<TGApp.BBS.Announcement.ListData> {
|
||||
const params: TGApp.BBS.Announcement.Params = getAnnoParams(region, lang);
|
||||
let url = `${AnnoApi}/getAnnList`;
|
||||
if (region !== AnnoServer.CN_ISLAND && region !== AnnoServer.CN_TREE) {
|
||||
url = `${AnnoApiGlobal}/getAnnList`;
|
||||
}
|
||||
const resp = await TGHttp<TGApp.BBS.Announcement.ListResponse>(url, {
|
||||
method: "GET",
|
||||
query: params,
|
||||
});
|
||||
const resp = await TGHttp<TGApp.BBS.Announcement.ListResponse>(
|
||||
`${getAnnoApi(region)}/getAnnList`,
|
||||
{ method: "GET", query: getAnnoParams(region, lang) },
|
||||
);
|
||||
return resp.data;
|
||||
}
|
||||
|
||||
@@ -85,21 +95,15 @@ async function getAnnoContent(
|
||||
region: AnnoServer = AnnoServer.CN_ISLAND,
|
||||
lang: AnnoLang = "zh-cn",
|
||||
): Promise<TGApp.BBS.Announcement.ContentItem> {
|
||||
const params: TGApp.BBS.Announcement.Params = getAnnoParams(region, lang);
|
||||
let url = `${AnnoApi}/getAnnContent`;
|
||||
if (region !== AnnoServer.CN_ISLAND && region !== AnnoServer.CN_TREE) {
|
||||
url = `${AnnoApiGlobal}/getAnnContent`;
|
||||
}
|
||||
const annoResp = await TGHttp<TGApp.BBS.Announcement.ContentResponse>(url, {
|
||||
method: "GET",
|
||||
query: params,
|
||||
});
|
||||
const annoResp = await TGHttp<TGApp.BBS.Announcement.ContentResponse>(
|
||||
`${getAnnoApi(region)}/getAnnContent`,
|
||||
{ method: "GET", query: getAnnoParams(region, lang) },
|
||||
);
|
||||
const annoContent = annoResp.data.list.find((item) => item.ann_id === annId);
|
||||
if (annoContent != null) {
|
||||
return annoContent;
|
||||
} else {
|
||||
if (annoContent === undefined) {
|
||||
throw new Error("公告内容不存在");
|
||||
}
|
||||
return annoContent;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -115,7 +119,6 @@ async function getGachaLog(
|
||||
gachaType: string,
|
||||
endId: string = "0",
|
||||
): Promise<TGApp.Game.Gacha.GachaItem[] | TGApp.BBS.Response.Base> {
|
||||
const url = "https://public-operation-hk4e.mihoyo.com/gacha_info/api/getGachaLog";
|
||||
const params = {
|
||||
lang: "zh-cn",
|
||||
auth_appid: "webview_gacha",
|
||||
@@ -126,19 +129,16 @@ async function getGachaLog(
|
||||
size: "20",
|
||||
end_id: endId,
|
||||
};
|
||||
const resp = await TGHttp<TGApp.Game.Gacha.GachaLogResponse | TGApp.BBS.Response.Base>(url, {
|
||||
method: "GET",
|
||||
query: params,
|
||||
});
|
||||
const resp = await TGHttp<TGApp.Game.Gacha.GachaLogResponse | TGApp.BBS.Response.Base>(
|
||||
"https://public-operation-hk4e.mihoyo.com/gacha_info/api/getGachaLog",
|
||||
{ method: "GET", query: params },
|
||||
);
|
||||
if (resp.retcode !== 0) return <TGApp.BBS.Response.Base>resp;
|
||||
return resp.data.list;
|
||||
}
|
||||
|
||||
const Hk4eApi = {
|
||||
anno: {
|
||||
list: getAnnoList,
|
||||
content: getAnnoContent,
|
||||
},
|
||||
anno: { list: getAnnoList, content: getAnnoContent },
|
||||
gacha: getGachaLog,
|
||||
};
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ async function getDeviceFp(
|
||||
Info?: TGApp.App.Device.DeviceInfo,
|
||||
): Promise<TGApp.App.Device.DeviceInfo> {
|
||||
const info = Info ?? getInitDeviceInfo();
|
||||
const deviceFPHeader = {
|
||||
const deviceFPHeader: Record<string, string | number> = {
|
||||
proxyStatus: 0,
|
||||
isRoot: 0,
|
||||
romCapacity: "512",
|
||||
@@ -74,8 +74,7 @@ async function getDeviceFp(
|
||||
hasKeyboard: 0,
|
||||
board: "taro",
|
||||
};
|
||||
const url = "https://public-data-api.mihoyo.com/device-fp/api/getFp";
|
||||
const data = {
|
||||
const data: Record<string, string> = {
|
||||
device_id: info.device_id,
|
||||
seed_id: info.seed_id,
|
||||
platform: "2",
|
||||
@@ -85,24 +84,20 @@ async function getDeviceFp(
|
||||
bbs_device_id: info.device_id,
|
||||
device_fp: info.device_fp,
|
||||
};
|
||||
const header = {
|
||||
"User-Agent": TGConstant.BBS.UA_MOBILE,
|
||||
const header: Record<string, string> = {
|
||||
"user-agent": TGConstant.BBS.UA_MOBILE,
|
||||
"x-rpc-app_version": TGConstant.BBS.VERSION,
|
||||
"x-rpc-client_type": "5",
|
||||
"x-requested-with": "com.mihoyo.hyperion",
|
||||
Referer: "https://webstatic.mihoyo.com/",
|
||||
};
|
||||
try {
|
||||
const resp = await TGHttp<TGApp.BBS.Response.getDeviceFp>(url, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
headers: header,
|
||||
});
|
||||
if (resp.retcode !== 0) {
|
||||
info.device_fp = "0000000000000";
|
||||
} else {
|
||||
info.device_fp = resp.data.device_fp;
|
||||
}
|
||||
const resp = await TGHttp<TGApp.BBS.Response.getDeviceFp>(
|
||||
"https://public-data-api.mihoyo.com/device-fp/api/getFp",
|
||||
{ method: "POST", body: JSON.stringify(data), headers: header },
|
||||
);
|
||||
if (resp.retcode !== 0) info.device_fp = "0000000000000";
|
||||
else info.device_fp = resp.data.device_fp;
|
||||
} catch (error) {
|
||||
info.device_fp = "0000000000000";
|
||||
await TGLogger.Error(`获取设备指纹失败: ${error}`);
|
||||
@@ -119,19 +114,14 @@ async function getDeviceFp(
|
||||
async function refreshCode(
|
||||
actId: string,
|
||||
): Promise<TGApp.BBS.Navigator.CodeData[] | TGApp.BBS.Response.Base> {
|
||||
const url = "https://api-takumi-static.mihoyo.com/event/miyolive/refreshCode";
|
||||
const header = { "x-rpc-act_id": actId };
|
||||
const res = await TGHttp<TGApp.BBS.Navigator.CodeResponse | TGApp.BBS.Response.Base>(url, {
|
||||
method: "GET",
|
||||
headers: header,
|
||||
});
|
||||
const res = await TGHttp<TGApp.BBS.Navigator.CodeResponse | TGApp.BBS.Response.Base>(
|
||||
"https://api-takumi-static.mihoyo.com/event/miyolive/refreshCode",
|
||||
{ method: "GET", headers: { "x-rpc-act_id": actId } },
|
||||
);
|
||||
if (res.retcode !== 0) return <TGApp.BBS.Response.Base>res;
|
||||
return res.data.code_list;
|
||||
}
|
||||
|
||||
const OtherApi = {
|
||||
code: refreshCode,
|
||||
fp: getDeviceFp,
|
||||
};
|
||||
const OtherApi = { code: refreshCode, fp: getDeviceFp };
|
||||
|
||||
export default OtherApi;
|
||||
|
||||
@@ -7,9 +7,9 @@ import TGHttp from "../../utils/TGHttp.js";
|
||||
import { getRequestHeader } from "../utils/getRequestHeader.js";
|
||||
|
||||
// PassportApiBaseUrl => pAbu
|
||||
const pAbu = "https://passport-api.mihoyo.com/";
|
||||
const pAbu: Readonly<string> = "https://passport-api.mihoyo.com/";
|
||||
// PassportV4ApiBaseUrl => p4Abu
|
||||
const p4Abu = "https://passport-api-v4.mihoyo.com/";
|
||||
const p4Abu: Readonly<string> = "https://passport-api-v4.mihoyo.com/";
|
||||
|
||||
/**
|
||||
* @description 获取登录ticket
|
||||
@@ -22,22 +22,20 @@ async function createAuthTicketByGameBiz(
|
||||
account: TGApp.Sqlite.Account.Game,
|
||||
cookie: TGApp.App.Account.Cookie,
|
||||
): Promise<TGApp.BBS.Response.Base | string> {
|
||||
const url = `${pAbu}account/ma-cn-verifier/app/createAuthTicketByGameBiz`;
|
||||
const params = {
|
||||
const params: Record<string, string> = {
|
||||
game_biz: account.gameBiz,
|
||||
stoken: cookie.stoken,
|
||||
uid: account.gameUid,
|
||||
mid: cookie.mid,
|
||||
};
|
||||
const header = {
|
||||
const headers: Record<string, string> = {
|
||||
"x-rpc-client_type": "3",
|
||||
"x-rpc-app_id": "ddxf5dufpuyo",
|
||||
};
|
||||
const resp = await TGHttp<TGApp.BBS.Response.getAuthTicketByGameBiz>(url, {
|
||||
method: "POST",
|
||||
headers: header,
|
||||
query: params,
|
||||
});
|
||||
const resp = await TGHttp<TGApp.BBS.Response.getAuthTicketByGameBiz>(
|
||||
`${pAbu}account/ma-cn-verifier/app/createAuthTicketByGameBiz`,
|
||||
{ method: "POST", headers: headers, query: params },
|
||||
);
|
||||
if (resp.retcode !== 0) return <TGApp.BBS.Response.Base>resp;
|
||||
return resp.data.ticket;
|
||||
}
|
||||
@@ -51,17 +49,11 @@ async function createAuthTicketByGameBiz(
|
||||
async function getCookieAccountInfoBySToken(
|
||||
cookie: TGApp.App.Account.Cookie,
|
||||
): Promise<string | TGApp.BBS.Response.Base> {
|
||||
const url = `${pAbu}account/auth/api/getCookieAccountInfoBySToken`;
|
||||
const ck = { stoken: cookie.stoken, mid: cookie.mid };
|
||||
const params = { stoken: cookie.stoken };
|
||||
const header = getRequestHeader(ck, "GET", params, "common");
|
||||
const resp = await TGHttp<TGApp.BBS.Response.getCookieTokenBySToken | TGApp.BBS.Response.Base>(
|
||||
url,
|
||||
{
|
||||
method: "GET",
|
||||
headers: header,
|
||||
query: params,
|
||||
},
|
||||
`${pAbu}account/auth/api/getCookieAccountInfoBySToken`,
|
||||
{ method: "GET", headers: getRequestHeader(ck, "GET", params), query: params },
|
||||
);
|
||||
if (resp.retcode !== 0) return <TGApp.BBS.Response.Base>resp;
|
||||
return resp.data.cookie_token;
|
||||
@@ -76,39 +68,30 @@ async function getCookieAccountInfoBySToken(
|
||||
async function getLTokenBySToken(
|
||||
cookie: TGApp.App.Account.Cookie,
|
||||
): Promise<string | TGApp.BBS.Response.Base> {
|
||||
const url = `${pAbu}account/auth/api/getLTokenBySToken`;
|
||||
const ck = { mid: cookie.mid, stoken: cookie.stoken };
|
||||
const params = { stoken: cookie.stoken };
|
||||
const header = getRequestHeader(ck, "GET", params, "common");
|
||||
const resp = await TGHttp<TGApp.BBS.Response.getLTokenBySToken | TGApp.BBS.Response.Base>(url, {
|
||||
method: "GET",
|
||||
headers: header,
|
||||
query: params,
|
||||
});
|
||||
const resp = await TGHttp<TGApp.BBS.Response.getLTokenBySToken | TGApp.BBS.Response.Base>(
|
||||
`${pAbu}account/auth/api/getLTokenBySToken`,
|
||||
{ method: "GET", headers: getRequestHeader(ck, "GET", params), query: params },
|
||||
);
|
||||
if (resp.retcode !== 0) return <TGApp.BBS.Response.Base>resp;
|
||||
return resp.data.ltoken;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 验证 ltoken 有效性,返回 mid
|
||||
* @since Beta v0.5.0
|
||||
* @since Beta v0.6.5
|
||||
* @param {TGApp.App.Account.Cookie} cookie - 账户 cookie
|
||||
* @returns {Promise<string | TGApp.BBS.Response.Base>}
|
||||
*/
|
||||
async function verifyLToken(
|
||||
cookie: TGApp.App.Account.Cookie,
|
||||
): Promise<string | TGApp.BBS.Response.Base> {
|
||||
const url = `${p4Abu}account/ma-cn-session/web/verifyLtoken`;
|
||||
const ck = { ltoken: cookie.ltoken, ltuid: cookie.ltuid };
|
||||
const data = { ltoken: cookie.ltoken };
|
||||
const header = getRequestHeader(ck, "POST", data, "common");
|
||||
const resp = await TGHttp<TGApp.BBS.Response.verifyUserInfoBySToken | TGApp.BBS.Response.Base>(
|
||||
url,
|
||||
{
|
||||
method: "POST",
|
||||
headers: header,
|
||||
body: JSON.stringify(data),
|
||||
},
|
||||
`${p4Abu}account/ma-cn-session/web/verifyLtoken`,
|
||||
{ method: "POST", headers: getRequestHeader(ck, "POST", data), body: JSON.stringify(data) },
|
||||
);
|
||||
if (resp.retcode !== 0) return <TGApp.BBS.Response.Base>resp;
|
||||
return resp.data.user_info.mid;
|
||||
@@ -117,10 +100,7 @@ async function verifyLToken(
|
||||
const PassportApi = {
|
||||
authTicket: createAuthTicketByGameBiz,
|
||||
cookieToken: getCookieAccountInfoBySToken,
|
||||
lToken: {
|
||||
get: getLTokenBySToken,
|
||||
verify: verifyLToken,
|
||||
},
|
||||
lToken: { get: getLTokenBySToken, verify: verifyLToken },
|
||||
};
|
||||
|
||||
export default PassportApi;
|
||||
|
||||
@@ -8,7 +8,8 @@ import TGHttp from "../../utils/TGHttp.js";
|
||||
import { getRequestHeader } from "../utils/getRequestHeader.js";
|
||||
|
||||
// TakumiRecordGenshinApiBaseUrl => trgAbu
|
||||
const trgAbu = "https://api-takumi-record.mihoyo.com/game_record/app/genshin/api/";
|
||||
const trgAbu: Readonly<string> =
|
||||
"https://api-takumi-record.mihoyo.com/game_record/app/genshin/api/";
|
||||
|
||||
/**
|
||||
* @description 获取角色详情
|
||||
@@ -23,15 +24,12 @@ async function characterDetail(
|
||||
user: TGApp.Sqlite.Account.Game,
|
||||
avatarIds: string[],
|
||||
): Promise<TGApp.Game.Avatar.AvatarDetail | TGApp.BBS.Response.Base> {
|
||||
const url = `${trgAbu}character/detail`;
|
||||
const ck = { account_id: cookie.account_id, cookie_token: cookie.cookie_token };
|
||||
const data = { role_id: user.gameUid, server: user.region, character_ids: avatarIds };
|
||||
const header = getRequestHeader(ck, "POST", data, "common");
|
||||
const resp = await TGHttp<TGApp.Game.Avatar.DetailResponse | TGApp.BBS.Response.Base>(url, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
headers: header,
|
||||
});
|
||||
const resp = await TGHttp<TGApp.Game.Avatar.DetailResponse | TGApp.BBS.Response.Base>(
|
||||
`${trgAbu}character/detail`,
|
||||
{ method: "POST", body: JSON.stringify(data), headers: getRequestHeader(ck, "POST", data) },
|
||||
);
|
||||
if (resp.retcode !== 0) return <TGApp.BBS.Response.Base>resp;
|
||||
return resp.data;
|
||||
}
|
||||
@@ -47,15 +45,12 @@ async function characterList(
|
||||
cookie: TGApp.App.Account.Cookie,
|
||||
user: TGApp.Sqlite.Account.Game,
|
||||
): Promise<TGApp.Game.Avatar.Avatar[] | TGApp.BBS.Response.Base> {
|
||||
const url = `${trgAbu}character/list`;
|
||||
const ck = { account_id: cookie.account_id, cookie_token: cookie.cookie_token };
|
||||
const data = { role_id: user.gameUid, server: user.region };
|
||||
const header = getRequestHeader(ck, "POST", data, "common");
|
||||
const resp = await TGHttp<TGApp.Game.Avatar.ListResponse | TGApp.BBS.Response.Base>(url, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
headers: header,
|
||||
});
|
||||
const resp = await TGHttp<TGApp.Game.Avatar.ListResponse | TGApp.BBS.Response.Base>(
|
||||
`${trgAbu}character/list`,
|
||||
{ method: "POST", body: JSON.stringify(data), headers: getRequestHeader(ck, "POST", data) },
|
||||
);
|
||||
if (resp.retcode !== 0) return <TGApp.BBS.Response.Base>resp;
|
||||
return resp.data.list;
|
||||
}
|
||||
@@ -73,15 +68,12 @@ async function index(
|
||||
user: TGApp.Sqlite.Account.Game,
|
||||
listType: number = 0,
|
||||
): Promise<TGApp.Game.Record.FullData | TGApp.BBS.Response.Base> {
|
||||
const url = `${trgAbu}index`;
|
||||
const ck = { account_id: cookie.account_id, cookie_token: cookie.cookie_token };
|
||||
const params = { avatar_list_type: listType, role_id: user.gameUid, server: user.region };
|
||||
const header = getRequestHeader(ck, "GET", params, "common");
|
||||
const resp = await TGHttp<TGApp.Game.Record.Response | TGApp.BBS.Response.Base>(url, {
|
||||
method: "GET",
|
||||
headers: header,
|
||||
query: params,
|
||||
});
|
||||
const resp = await TGHttp<TGApp.Game.Record.Response | TGApp.BBS.Response.Base>(
|
||||
`${trgAbu}index`,
|
||||
{ method: "GET", headers: getRequestHeader(ck, "GET", params), query: params },
|
||||
);
|
||||
if (resp.retcode !== 0) return <TGApp.BBS.Response.Base>resp;
|
||||
return resp.data;
|
||||
}
|
||||
@@ -97,7 +89,6 @@ async function roleCombat(
|
||||
cookie: TGApp.App.Account.Cookie,
|
||||
user: TGApp.Sqlite.Account.Game,
|
||||
): Promise<TGApp.Game.Combat.Combat[] | TGApp.BBS.Response.Base | false> {
|
||||
const url = `${trgAbu}role_combat`;
|
||||
const ck = {
|
||||
account_id: cookie.account_id,
|
||||
cookie_token: cookie.cookie_token,
|
||||
@@ -105,12 +96,10 @@ async function roleCombat(
|
||||
ltuid: cookie.ltuid,
|
||||
};
|
||||
const params = { role_id: user.gameUid, server: user.region, active: 1, need_detail: true };
|
||||
const header = getRequestHeader(ck, "GET", params, "common");
|
||||
const resp = await TGHttp<TGApp.Game.Combat.Response | TGApp.BBS.Response.Base>(url, {
|
||||
method: "GET",
|
||||
headers: header,
|
||||
query: params,
|
||||
});
|
||||
const resp = await TGHttp<TGApp.Game.Combat.Response | TGApp.BBS.Response.Base>(
|
||||
`${trgAbu}role_combat`,
|
||||
{ method: "GET", headers: getRequestHeader(ck, "GET", params), query: params },
|
||||
);
|
||||
if (resp.retcode !== 0) return <TGApp.BBS.Response.Base>resp;
|
||||
if (!resp.data.is_unlock) return false;
|
||||
return resp.data.data;
|
||||
@@ -129,7 +118,6 @@ async function spiralAbyss(
|
||||
user: TGApp.Sqlite.Account.Game,
|
||||
schedule: string,
|
||||
): Promise<TGApp.Game.Abyss.FullData | TGApp.BBS.Response.Base> {
|
||||
const url = `${trgAbu}spiralAbyss`;
|
||||
const ck = {
|
||||
account_id: cookie.account_id,
|
||||
cookie_token: cookie.cookie_token,
|
||||
@@ -137,22 +125,17 @@ async function spiralAbyss(
|
||||
ltuid: cookie.ltuid,
|
||||
};
|
||||
const params = { role_id: user.gameUid, schedule_type: schedule, server: user.region };
|
||||
const header = getRequestHeader(ck, "GET", params, "common");
|
||||
const resp = await TGHttp<TGApp.Game.Abyss.Response | TGApp.BBS.Response.Base>(url, {
|
||||
method: "GET",
|
||||
headers: header,
|
||||
query: params,
|
||||
});
|
||||
const resp = await TGHttp<TGApp.Game.Abyss.Response | TGApp.BBS.Response.Base>(
|
||||
`${trgAbu}spiralAbyss`,
|
||||
{ method: "GET", headers: getRequestHeader(ck, "GET", params), query: params },
|
||||
);
|
||||
if (resp.retcode !== 0) return <TGApp.BBS.Response.Base>resp;
|
||||
return resp.data;
|
||||
}
|
||||
|
||||
const TakumiRecordGenshinApi = {
|
||||
index: index,
|
||||
character: {
|
||||
list: characterList,
|
||||
detail: characterDetail,
|
||||
},
|
||||
character: { list: characterList, detail: characterDetail },
|
||||
roleCombat: roleCombat,
|
||||
spiralAbyss: spiralAbyss,
|
||||
};
|
||||
|
||||
@@ -4,13 +4,12 @@
|
||||
* @since Beta v0.6.3
|
||||
*/
|
||||
import TGHttp from "../../utils/TGHttp.js";
|
||||
import TGConstant from "../constant/TGConstant.js";
|
||||
import { getRequestHeader } from "../utils/getRequestHeader.js";
|
||||
|
||||
// TakumiAuthApiBaseUrl => taAbu
|
||||
const taAbu = "https://api-takumi.mihoyo.com/auth/api/";
|
||||
const taAbu: Readonly<string> = "https://api-takumi.mihoyo.com/auth/api/";
|
||||
// TakumiBingApiBaseUrl => tbAbu
|
||||
const tbAbu = "https://api-takumi.mihoyo.com/binding/api/";
|
||||
const tbAbu: Readonly<string> = "https://api-takumi.mihoyo.com/binding/api/";
|
||||
|
||||
/**
|
||||
* @description 根据stoken获取action_ticket
|
||||
@@ -25,15 +24,12 @@ async function getActionTicketBySToken(
|
||||
user: TGApp.Sqlite.Account.Game,
|
||||
actionType: string,
|
||||
): Promise<TGApp.BBS.Response.getActionTicketBySToken> {
|
||||
const url = `${taAbu}getActionTicketBySToken`;
|
||||
const ck = { stoken: cookie.stoken, mid: cookie.mid };
|
||||
const params = { action_type: actionType, stoken: cookie.stoken, uid: user.gameUid };
|
||||
const header = getRequestHeader(ck, "GET", params, "k2");
|
||||
return await TGHttp<TGApp.BBS.Response.getActionTicketBySToken>(url, {
|
||||
method: "GET",
|
||||
headers: header,
|
||||
query: params,
|
||||
});
|
||||
return await TGHttp<TGApp.BBS.Response.getActionTicketBySToken>(
|
||||
`${taAbu}getActionTicketBySToken`,
|
||||
{ method: "GET", headers: getRequestHeader(ck, "GET", params, "K2"), query: params },
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -47,7 +43,6 @@ async function genAuthKey(
|
||||
cookie: TGApp.App.Account.Cookie,
|
||||
account: TGApp.Sqlite.Account.Game,
|
||||
): Promise<string | TGApp.BBS.Response.Base> {
|
||||
const url = `${tbAbu}genAuthKey`;
|
||||
const ck = { stoken: cookie.stoken, mid: cookie.mid };
|
||||
const data = {
|
||||
auth_appid: "webview_gacha",
|
||||
@@ -55,12 +50,14 @@ async function genAuthKey(
|
||||
game_uid: account.gameUid,
|
||||
region: account.region,
|
||||
};
|
||||
const header = getRequestHeader(ck, "POST", JSON.stringify(data), "lk2", true);
|
||||
const resp = await TGHttp<TGApp.Game.Gacha.AuthkeyResponse | TGApp.BBS.Response.Base>(url, {
|
||||
method: "POST",
|
||||
headers: header,
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
const resp = await TGHttp<TGApp.Game.Gacha.AuthkeyResponse | TGApp.BBS.Response.Base>(
|
||||
`${tbAbu}genAuthKey`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: getRequestHeader(ck, "POST", JSON.stringify(data), "LK2", true),
|
||||
body: JSON.stringify(data),
|
||||
},
|
||||
);
|
||||
if (resp.retcode !== 0) return <TGApp.BBS.Response.Base>resp;
|
||||
return resp.data.authkey;
|
||||
}
|
||||
@@ -76,46 +73,35 @@ async function genAuthKey2(
|
||||
cookie: Record<string, string>,
|
||||
payload: Record<string, string>,
|
||||
): Promise<TGApp.BBS.Response.Base> {
|
||||
const url = `${tbAbu}genAuthKey`;
|
||||
const header = getRequestHeader(cookie, "POST", JSON.stringify(payload), "lk2", true);
|
||||
return await TGHttp<TGApp.BBS.Response.Base>(url, {
|
||||
return await TGHttp<TGApp.BBS.Response.Base>(`${tbAbu}genAuthKey`, {
|
||||
method: "POST",
|
||||
headers: header,
|
||||
headers: getRequestHeader(cookie, "POST", JSON.stringify(payload), "LK2", true),
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 通过cookie获取游戏账号
|
||||
* @since Beta v0.6.3
|
||||
* @since Beta v0.6.5
|
||||
* @param {TGApp.App.Account.Cookie} cookie cookie
|
||||
* @returns {Promise<TGApp.BBS.Account.GameAccount[]|TGApp.BBS.Response.Base>}
|
||||
*/
|
||||
async function getUserGameRolesByCookie(
|
||||
cookie: TGApp.App.Account.Cookie,
|
||||
): Promise<TGApp.BBS.Account.GameAccount[] | TGApp.BBS.Response.Base> {
|
||||
const url = `${tbAbu}getUserGameRolesByCookie`;
|
||||
const ck = { account_id: cookie.account_id, cookie_token: cookie.cookie_token };
|
||||
const params = { game_biz: TGConstant.GAME_BIZ };
|
||||
const header = getRequestHeader(ck, "GET", params, "common");
|
||||
const resp = await TGHttp<TGApp.BBS.Response.getGameAccounts | TGApp.BBS.Response.Base>(url, {
|
||||
method: "GET",
|
||||
headers: header,
|
||||
query: params,
|
||||
});
|
||||
const params = { game_biz: "hk4e_cn" };
|
||||
const resp = await TGHttp<TGApp.BBS.Response.getGameAccounts | TGApp.BBS.Response.Base>(
|
||||
`${tbAbu}getUserGameRolesByCookie`,
|
||||
{ method: "GET", headers: getRequestHeader(ck, "GET", params), query: params },
|
||||
);
|
||||
if (resp.retcode !== 0) return <TGApp.BBS.Response.Base>resp;
|
||||
return resp.data.list;
|
||||
}
|
||||
|
||||
const TakumiApi = {
|
||||
auth: {
|
||||
actionTicket: getActionTicketBySToken,
|
||||
},
|
||||
bind: {
|
||||
authKey: genAuthKey,
|
||||
authKey2: genAuthKey2,
|
||||
gameRoles: getUserGameRolesByCookie,
|
||||
},
|
||||
auth: { actionTicket: getActionTicketBySToken },
|
||||
bind: { authKey: genAuthKey, authKey2: genAuthKey2, gameRoles: getUserGameRolesByCookie },
|
||||
};
|
||||
|
||||
export default TakumiApi;
|
||||
|
||||
@@ -7,34 +7,35 @@
|
||||
import Md5 from "js-md5";
|
||||
|
||||
import { getDeviceInfo, getRandomString } from "../../utils/toolFunc.js";
|
||||
import TGConstant from "../constant/TGConstant.js";
|
||||
|
||||
import { transCookie, transParams } from "./tools.js";
|
||||
|
||||
type SaltType = "common" | "prod" | "lk2" | "k2";
|
||||
import { BBS_VERSION } from "../constant/bbs.js";
|
||||
|
||||
/**
|
||||
* @description 获取 salt
|
||||
* @since Beta v0.6.3
|
||||
* @version 2.59.1
|
||||
* @param {SaltType} saltType salt 类型
|
||||
* @returns {string} salt
|
||||
* @description salt 类型
|
||||
* @since Beta v0.6.5
|
||||
* @enum {number}
|
||||
*/
|
||||
function getSalt(saltType: SaltType): string {
|
||||
switch (saltType) {
|
||||
case "common":
|
||||
return TGConstant.Salt.X4;
|
||||
case "prod":
|
||||
return TGConstant.Salt.PROD;
|
||||
case "lk2":
|
||||
return TGConstant.Salt.LK2;
|
||||
case "k2":
|
||||
return TGConstant.Salt.K2;
|
||||
default:
|
||||
return TGConstant.Salt.X4;
|
||||
}
|
||||
const enum SaltType {
|
||||
K2,
|
||||
LK2,
|
||||
X4,
|
||||
X6,
|
||||
PROD,
|
||||
}
|
||||
|
||||
/**
|
||||
* @description salt 值
|
||||
* @version 2.78.1
|
||||
* @since Beta v0.6.5
|
||||
*/
|
||||
const Salt: Readonly<Record<keyof typeof SaltType, string>> = {
|
||||
K2: "GuODIETRPuJxpiUQoZairQxHtmzZKYFl",
|
||||
LK2: "ACDpsiiEFSqqLiEpzXMuXNsLNqGkrIQc",
|
||||
X4: "xV8v4Qu54lUKrEYFZkJhB8cuOh9Asafs",
|
||||
X6: "t0qEgfub6cvueAPgR5m9aQWWVciEer7v",
|
||||
PROD: "t0qEgfub6cvueAPgR5m9aQWWVciEer7v",
|
||||
};
|
||||
const UserAgent: Readonly<string> = `Mozilla/5.0 (Windows NT 10.0; Win64; x64) miHoYoBBS/${BBS_VERSION}`;
|
||||
|
||||
/**
|
||||
* @description 获取随机数
|
||||
* @since Alpha v0.2.0
|
||||
@@ -56,8 +57,13 @@ function getRandomNumber(min: number, max: number): number {
|
||||
* @param {boolean} isSign 是否为签名
|
||||
* @returns {string} ds
|
||||
*/
|
||||
function getDS(method: string, data: string, saltType: SaltType, isSign: boolean): string {
|
||||
const salt = getSalt(saltType);
|
||||
function getDS(
|
||||
method: string,
|
||||
data: string,
|
||||
saltType: keyof typeof SaltType,
|
||||
isSign: boolean,
|
||||
): string {
|
||||
const salt = Salt[saltType];
|
||||
const time = Math.floor(Date.now() / 1000).toString();
|
||||
let random = getRandomNumber(100000, 200000).toString();
|
||||
if (isSign) random = getRandomString(6);
|
||||
@@ -69,38 +75,64 @@ function getDS(method: string, data: string, saltType: SaltType, isSign: boolean
|
||||
return `${time},${random},${md5Str}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description ds 算法需要数据转换后的字符串是按照字典序排序的
|
||||
* @since Beta v0.6.5
|
||||
* @param { Record<string, string | number | boolean | Array<string>> | string} obj object
|
||||
* @returns {string} query string
|
||||
*/
|
||||
function transParams(
|
||||
obj: Record<string, string | number | boolean | Array<string>> | string,
|
||||
): string {
|
||||
if (typeof obj === "string") return obj;
|
||||
let res = "";
|
||||
const keys = Object.keys(obj).sort();
|
||||
for (const key of keys) {
|
||||
res += `${key}=${obj[key].toString()}&`;
|
||||
}
|
||||
return res.slice(0, -1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 将 cookie 对象转换为字符串
|
||||
* @since Alpha v0.1.5
|
||||
* @param {Record<string, string>} cookie cookie
|
||||
* @returns {string} 转换后的 cookie
|
||||
*/
|
||||
function transCookie(cookie: Record<string, string>): string {
|
||||
let res = "";
|
||||
for (const key of Object.keys(cookie).sort()) {
|
||||
res += `${key}=${cookie[key]};`;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 获取请求头
|
||||
* @since Beta v0.6.2
|
||||
* @since Beta v0.6.5
|
||||
* @param {Record<string, string>} cookie cookie
|
||||
* @param {string} method 请求方法
|
||||
* @param {Record<string, string|number|string[]|boolean>|string} data 请求数据
|
||||
* @param {SaltType} saltType salt 类型
|
||||
* @param {Record<string, string | number | boolean | Array<string>> | string} data 请求数据
|
||||
* @param {keyof typeof SaltType} saltType salt 类型
|
||||
* @param {boolean} isSign 是否为签名
|
||||
* @returns {Record<string, string>} 请求头
|
||||
*/
|
||||
export function getRequestHeader(
|
||||
cookie: Record<string, string>,
|
||||
method: string,
|
||||
data: Record<string, string | number | string[] | boolean> | string,
|
||||
saltType: SaltType,
|
||||
data: Record<string, string | number | boolean | Array<string>> | string,
|
||||
saltType: keyof typeof SaltType = "X4",
|
||||
isSign: boolean = false,
|
||||
): Record<string, string> {
|
||||
let ds;
|
||||
if (typeof data === "string") {
|
||||
ds = getDS(method, data, saltType, isSign);
|
||||
} else {
|
||||
ds = getDS(method, transParams(data), saltType, isSign);
|
||||
}
|
||||
return {
|
||||
"user-agent": TGConstant.BBS.UA_PC,
|
||||
"x-rpc-app_version": TGConstant.BBS.VERSION,
|
||||
"user-agent": UserAgent,
|
||||
"x-rpc-app_version": BBS_VERSION,
|
||||
"x-rpc-client_type": "5",
|
||||
"x-requested-with": "com.mihoyo.hyperion",
|
||||
referer: "https://webstatic.mihoyo.com",
|
||||
"x-rpc-device_id": getDeviceInfo("device_id"),
|
||||
"x-rpc-device_fp": getDeviceInfo("device_fp"),
|
||||
ds,
|
||||
ds: getDS(method, transParams(data), saltType, isSign),
|
||||
cookie: transCookie(cookie),
|
||||
};
|
||||
}
|
||||
@@ -108,26 +140,31 @@ export function getRequestHeader(
|
||||
/**
|
||||
* @description 获取 DS
|
||||
* @since Beta v0.3.9
|
||||
* @param {SaltType} saltType salt 类型
|
||||
* @param {keyof typeof SaltType} saltType salt 类型
|
||||
* @param {number} dsType ds 类型
|
||||
* @param {Record<string, string|number>|string} body
|
||||
* @param {Record<string, string|number>|string} query
|
||||
* @returns {string} DS
|
||||
*/
|
||||
export function getDS4JS(saltType: SaltType, dsType: 1, body: undefined, query: undefined): string;
|
||||
export function getDS4JS(
|
||||
saltType: SaltType,
|
||||
saltType: keyof typeof SaltType,
|
||||
dsType: 1,
|
||||
body: undefined,
|
||||
query: undefined,
|
||||
): string;
|
||||
export function getDS4JS(
|
||||
saltType: keyof typeof SaltType,
|
||||
dsType: 2,
|
||||
body: Record<string, string | number> | string,
|
||||
query: Record<string, string | number> | string,
|
||||
): string;
|
||||
export function getDS4JS(
|
||||
saltType: SaltType,
|
||||
saltType: keyof typeof SaltType,
|
||||
dsType: 1 | 2,
|
||||
body?: Record<string, string | number> | string,
|
||||
query?: Record<string, string | number> | string,
|
||||
): string {
|
||||
const salt = getSalt(saltType);
|
||||
const salt = Salt[saltType];
|
||||
const time = Math.floor(Date.now() / 1000).toString();
|
||||
let random = getRandomNumber(100000, 200000).toString();
|
||||
let hashStr: string;
|
||||
|
||||
@@ -26,35 +26,6 @@ export function decodeRegExp(data: string): string {
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 将 cookie 对象转换为字符串
|
||||
* @since Alpha v0.1.5
|
||||
* @param {Record<string, string>} cookie cookie
|
||||
* @returns {string} 转换后的 cookie
|
||||
*/
|
||||
export function transCookie(cookie: Record<string, string>): string {
|
||||
let res = "";
|
||||
for (const key of Object.keys(cookie).sort()) {
|
||||
res += `${key}=${cookie[key]};`;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description ds 算法需要数据转换后的字符串是按照字典序排序的
|
||||
* @since Beta v0.6.3
|
||||
* @param {Record<string, string|number>} obj object
|
||||
* @returns {string} query string
|
||||
*/
|
||||
export function transParams(obj: Record<string, string | number | string[] | boolean>): string {
|
||||
let res = "";
|
||||
const keys = Object.keys(obj).sort();
|
||||
for (const key of keys) {
|
||||
res += `${key}=${obj[key].toString()}&`;
|
||||
}
|
||||
return res.slice(0, -1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 根据 gid 获取游戏名称
|
||||
* @param {number} gid
|
||||
@@ -64,3 +35,13 @@ export function getGameName(gid: number): string {
|
||||
const game = TGConstant.BBS.CHANNELS.find((item) => item.gid === gid.toString());
|
||||
return game ? game.title : "未知游戏";
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 获取游戏id
|
||||
* @param {string} mini
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getGameId(mini: string): string {
|
||||
const game = TGConstant.BBS.CHANNELS.find((item) => item.mini === mini);
|
||||
return game ? game.gid : "0";
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
"src/**/*.d.ts",
|
||||
"src/**/*.ts",
|
||||
"src/**/*.vue",
|
||||
"src/App.vue",
|
||||
"src/data/**/*.json",
|
||||
"tsconfig.json",
|
||||
"vite.config.ts",
|
||||
|
||||
Reference in New Issue
Block a user