Compare commits

..

69 Commits

Author SHA1 Message Date
BTMuli
2807ce48a6 🚸 统一UIGF导出交互,导出前选择导出路径 2026-03-22 00:40:41 +08:00
BTMuli
f9b64c5c6a 🚸 管理员模式下侧边栏启动仍然为启动游戏 2026-03-22 00:38:51 +08:00
BTMuli
1038c9cdb7 ⬆️ 更新依赖 2026-03-22 00:38:04 +08:00
BTMuli
87f9df80a5 💫 添加交互效果 2026-03-17 21:39:03 +08:00
BTMuli
d8f4a4c2bf 🚸 调整参数,处理点击事件 2026-03-14 13:25:34 +08:00
BTMuli
3537751d65 添加用户个人页面跳转功能 2026-03-14 12:23:31 +08:00
BTMuli
66f77da754 🐛 修复角色生日判断逻辑,优化返回结果 2026-03-14 00:14:32 +08:00
BTMuli
7da01c117d 🩹 补充首页mini参数&组件参数处理 2026-03-13 20:27:48 +08:00
BTMuli
722b5598fe 🐛 调整snackbar层级,修复兑换码浮窗分享异常 2026-03-13 20:20:49 +08:00
BTMuli
ba962ae4c6 ⬆️ 更新依赖,使用vite8 2026-03-13 20:19:40 +08:00
BTMuli
f121644bc4 🚀 0.9.8 2026-03-13 13:50:02 +08:00
BTMuli
1d810117b0 🚨 修复Sentry报错
TypeError
Cannot read properties of null (reading 'startsWith')
2026-03-13 11:25:25 +08:00
BTMuli
be7c294f7e 🚸 区分分区/版块点击跳转页面
分区→对应分区资讯页
版块→帖子分区+版块页
2026-03-12 23:30:30 +08:00
BTMuli
0e1bcdaffe 🍱 更新下半卡池数据 2026-03-12 23:28:43 +08:00
BTMuli
51d47c7ca6 跳转外部合集 2026-03-11 17:32:40 +08:00
BTMuli
28f05a757d 🚸 导入数据前进行提示 2026-03-11 17:22:52 +08:00
BTMuli
c2db42d9f7 ✏️ 修正最深抵达描述计算逻辑 2026-03-11 17:16:58 +08:00
BTMuli
49855ea118 🚸 调整动态头像缓存策略 2026-03-11 12:21:45 +08:00
BTMuli
f5da601620 🚨 完善部分处理 2026-03-11 11:44:17 +08:00
BTMuli
9f707db9f7 🚨 尝试修复Sentry报错
Cannot read properties of undefined (reading 'upvote_type')
2026-03-11 11:26:55 +08:00
BTMuli
d30a70d4aa 🚸 允许仅刷新Cookie而不刷新游戏账号 2026-03-11 11:18:08 +08:00
BTMuli
036b3c47a7 🚸 允许搜索清空并进行对应处理 2026-03-11 01:07:00 +08:00
BTMuli
77c513b516 💄 微调顶部样式 2026-03-11 00:52:31 +08:00
BTMuli
69f40cd495 🚸 处理搜索交互 2026-03-11 00:42:43 +08:00
BTMuli
9c79e0b822 ⬆️ 更新依赖 2026-03-10 13:39:49 +08:00
BTMuli
c56b05b4f1 检测版本更新
close #231
2026-03-10 12:46:46 +08:00
BTMuli
b5c7c6e8b1 🚸 调整动态头像缓存策略 2026-03-10 10:57:15 +08:00
BTMuli
480f1739f5 🐛 修复采用ck登录后本地ck未同步更新 2026-03-05 00:58:37 +08:00
BTMuli
b0a480d65b 🎨 颂愿采用同样处理
close#222,#230
2026-03-05 00:46:31 +08:00
BTMuli
d7aee50cc5 🎨 精简代码 2026-03-05 00:32:47 +08:00
BTMuli
fe176ad418 ️ 提高插入性能,调整删除后处理
#222 #230
2026-03-05 00:22:24 +08:00
BTMuli
5c2556a0c3 💄 load stat 2026-03-04 22:04:18 +08:00
BTMuli
320e53b567 💄 load stat 2026-03-04 19:15:11 +08:00
BTMuli
21698dc728 🔥 删除无用代码 2026-03-03 21:36:02 +08:00
BTMuli
9b4b6fb7ab 新增满好感筛选 2026-03-03 19:18:34 +08:00
BTMuli
8c8f8e3a2d 🚸 三状态 2026-03-03 19:07:08 +08:00
BTMuli
5e6e7ee047 展示筛选&排序 2026-03-03 19:06:17 +08:00
BTMuli
2872d0f983 💄 微调UI 2026-03-03 18:05:10 +08:00
BTMuli
67e242308e 🚸 调整缓存策略 2026-03-03 18:02:40 +08:00
BTMuli
a2df7b2d22 💄 尘歌壶UI迭代
#221
2026-03-03 17:59:12 +08:00
BTMuli
7b8be1adf9 🔒️ 二次确认
close #228
2026-03-03 16:49:06 +08:00
BTMuli
9c73290033 个人主页跳转 2026-03-03 16:24:25 +08:00
BTMuli
6aaf9ea7d9 🔧 设置最小尺寸 2026-03-01 00:07:14 +08:00
BTMuli
ada60d0d3b 💄 调节文本位置&大小
#221
2026-03-01 00:00:38 +08:00
BTMuli
bbe329d677 💄 优化浅色模式下的对比度
#221
2026-02-28 23:53:16 +08:00
BTMuli
47ed849f70 🚸 筛选增加70级判断
close #229
2026-02-28 22:55:12 +08:00
BTMuli
0d65ba7168 💫 添加平滑过渡,调整参数 2026-02-28 19:03:42 +08:00
BTMuli
da2285a8d0 💄 调整小组件显示 2026-02-28 18:49:50 +08:00
BTMuli
572180234f 💄 调整浮窗信息显示逻辑,优化自定义表情label显示判断 2026-02-28 18:10:20 +08:00
Copilot
1497533f14 完善非回正模式下的窗口位置&大小处理 (#226)
* ️大幅提升UIGF导入速度 (#225)

* Initial plan

* perf: optimize gacha import with batch transactions and reduced UI delays

- Wrap DB inserts in transactions (batches of 500) for mergeUIGF/mergeUIGF4
- Pre-transform all data before batch insert loop
- Pass timeout: 0 to showLoading.update in progress callbacks
- Remove 1500ms snackbar delay in cleanGachaRecords
- Reduce per-item loading update delay in refreshGachaPool

Co-authored-by: Mikachu2333 <63829496+Mikachu2333@users.noreply.github.com>

* fix: increment progress counter per item instead of per batch for accurate progress display

Co-authored-by: Mikachu2333 <63829496+Mikachu2333@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Mikachu2333 <63829496+Mikachu2333@users.noreply.github.com>

* Initial plan

* feat: calculate window size based on resolution/scaling with baseline check and centering

- resizeWindow: add baseline check (1920x1080@150%), clamp to screen bounds
- setWindowPos: ensure window fits on screen and always center
- App.vue: use setWindowPos instead of manual positioning, center on deep link show
- tray.rs: center window when showing from system tray

Co-authored-by: Mikachu2333 <63829496+Mikachu2333@users.noreply.github.com>

* fix: address code review - add zero guard and use setWindowPos consistently

Co-authored-by: Mikachu2333 <63829496+Mikachu2333@users.noreply.github.com>

* refactor: address review feedback - move baseline to setWindowPos, revert tray.rs

- tray.rs: reverted, center() removed as redundant
- TGWindow.ts: baseline check moved to setWindowPos, resizeWindow restored as fallback
- App.vue: needResize=false → setWindowPos, else → center; deep link reverted;
  handleResizeListen true path unchanged, setWindowPos moved inside else

Co-authored-by: Mikachu2333 <63829496+Mikachu2333@users.noreply.github.com>

* refactor: use needResize judgment condition (targetZoom/scaleFactor/textScale) in setWindowPos

Replace the simple curSize > screen.size overflow check with the same
condition used by resizeWindow(): targetZoom < 1, which considers
scaleFactor and textScale. Falls back to resizeWindow() when below
baseline or when targetZoom < 1.

Co-authored-by: Mikachu2333 <63829496+Mikachu2333@users.noreply.github.com>

* 🚸 调整尺寸判断

* 🚸 处理溢出

* 🚸 优化处理

* 🚸 移除冗余scale处理

* 🎨 CodeStyle

---------

Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Mikachu2333 <63829496+Mikachu2333@users.noreply.github.com>
Co-authored-by: BTMuli <bt-muli@outlook.com>
2026-02-28 17:58:19 +08:00
子寻
3b6970d8c3 🚸实用脚本支持一键执行多账号 (#227)
* 💄 标识当前登录账号

*  实用脚本支持一键执行全部账号

* fix: 去除非必要的代码,样式调整

* 😭 图标多好看啊

* 🎨 remove empty line

---------

Co-authored-by: BTMuli <bt-muli@outlook.com>
2026-02-28 13:19:18 +08:00
BTMuli
8fc90d7144 🚸 loading显示区域禁用右键
#221
2026-02-28 11:51:34 +08:00
BTMuli
c716cf79ed 🔒️ 调整用户数据目录选取&旧目录删除 2026-02-28 11:35:53 +08:00
BTMuli
9057e613c7 🚸 调整位置判断 2026-02-28 00:31:31 +08:00
BTMuli
a929e2cbe8 💄 调整最大宽度 2026-02-28 00:14:02 +08:00
Copilot
2d321aad9c ️大幅提升UIGF导入速度 (#225)
* Initial plan

* perf: optimize gacha import with batch transactions and reduced UI delays

- Wrap DB inserts in transactions (batches of 500) for mergeUIGF/mergeUIGF4
- Pre-transform all data before batch insert loop
- Pass timeout: 0 to showLoading.update in progress callbacks
- Remove 1500ms snackbar delay in cleanGachaRecords
- Reduce per-item loading update delay in refreshGachaPool

Co-authored-by: Mikachu2333 <63829496+Mikachu2333@users.noreply.github.com>

* fix: increment progress counter per item instead of per batch for accurate progress display

Co-authored-by: Mikachu2333 <63829496+Mikachu2333@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Mikachu2333 <63829496+Mikachu2333@users.noreply.github.com>
2026-02-27 23:52:15 +08:00
BTMuli
43de734884 💄 样式适配 2026-02-27 23:41:55 +08:00
BTMuli
b3997815e1 🌱 功能预留
#223
2026-02-27 22:48:53 +08:00
BTMuli
52cbfb9f6b 💄 微调位置 2026-02-27 22:41:13 +08:00
BTMuli
6d2d2b18d1 💄 调整展开后的侧边栏宽度 2026-02-27 22:34:57 +08:00
BTMuli
f7df9ec804 🚸 UI适配,修复 .v-application 颜色样式导致分享图生成异常 2026-02-27 22:34:32 +08:00
BTMuli
3f2ea530fe 🛂 添加SetPosition权限 2026-02-27 22:29:47 +08:00
BTMuli
240356da0a 🚸 调整部分图片缓存策略 2026-02-27 22:22:27 +08:00
BTMuli
57de268f06 💄 处理溢出 2026-02-27 22:08:33 +08:00
BTMuli
3c238a0f0b 💄 移除间距处理 2026-02-27 21:57:24 +08:00
BTMuli
d6dbddaf87 🚸 处理标题可能为空的情况 2026-02-27 11:46:11 +08:00
BTMuli
2a84e25f4a 🐛 处理vuetify4的color-mix导致的分享异常 2026-02-26 23:49:59 +08:00
BTMuli
d1a4b6e97d ️ 采用useTemplateRef获取dom 2026-02-26 23:38:10 +08:00
BTMuli
f4a9069ea4 💄 调整样式 2026-02-26 19:20:39 +08:00
84 changed files with 3880 additions and 2529 deletions

View File

@@ -1,3 +1,3 @@
VITE_SENTRY_RELEASE=TeyvatGuide@0.9.7
VITE_COMMIT_HASH=fcc5d3db
VITE_BUILD_TIME=1772101907
VITE_SENTRY_RELEASE=TeyvatGuide@0.9.8
VITE_COMMIT_HASH=1d810117
VITE_BUILD_TIME=1773380800

View File

@@ -69,7 +69,7 @@ jobs:
- name: setup pnpm
uses: pnpm/action-setup@v2
with:
version: 10.23.0
version: 10.32.1
- name: Install frontend dependencies
run: pnpm install
- name: Setup sentry-cli

3
.gitignore vendored
View File

@@ -10,4 +10,5 @@ dist
*.tsbuildinfo
# Sentry Config File
.env.development.local
.env.development.local
package-lock.json

View File

@@ -2,12 +2,36 @@
Author: 目棃
Description: CHANGELOG
Date: 2025-09-09
Update: 2026-02-26
Update: 2026-03-13
---
> 本文档 [`Frontmatter`](https://github.com/BTMuli/MuCli#Frontmatter) 由 [MuCli](https://github.com/BTMuli/Mucli) 自动生成于 `2025-09-09 14:30:56`
>
> 更新于 `2026-02-26 18:31:44`
> 更新于 `2026-03-13 13:41:58`
## [0.9.8](https://github.com/BTMuli/TeyvatGuide/releases/v0.9.8) (2026-03-13)
- 🍱 更新下半卡池数据
- 🐛 处理UI框架升级导致的分享图生成异常
- 🐛 修复采用ck登录后本地ck未同步更新
- ✏️ 修正深渊最深抵达描述计算逻辑
-大幅提升UIGF导入速度 [`#222`](https://github.com/BTMuli/TeyvatGuide/issues/222)
- ✨ 角色列表页展示当前筛选&排序
- ✨ 定时检测版本更新并提醒 [`#231`](https://github.com/BTMuli/TeyvatGuide/issues/231)
- 🔒️ 调整用户数据目录选取&旧目录删除处理,增加子目录检测&二次确认 [`#228`](https://github.com/BTMuli/TeyvatGuide/issues/228)
- 🚸 导入胡桃深渊/剧诗/危战数据前进行提示
- 🚸 设置页刷新信息允许仅刷新Cookie而不刷新游戏账号
- 🚸 搜索框增加清空按钮,并进行对应适配处理
- 🚸 完善非回正模式下的窗口位置&大小处理 [`#199`](https://github.com/BTMuli/TeyvatGuide/pull/199) [`#223`](https://github.com/BTMuli/TeyvatGuide/pull/223)
- 🚸 实用脚本支持一键执行多账号 by [HLFromZ](https://github.com/BTMuli/TeyvatGuide/pull/227)
- 🚸 角色列表页新增`等级>=70`筛选 [`#229`](https://github.com/BTMuli/TeyvatGuide/issues/229)
- 🚸 角色列表页新增满好感筛选
- 🚸 处理帖子标题为空时的渲染&事件
- 🚸 调整部分图片缓存策略
- 🚸 增加个人主页&合集主页的外部跳转
- 💄 优化调整多处样式 [`#221`](https://github.com/BTMuli/TeyvatGuide/issues/221)
- 💄 调整展开后的侧边栏宽度
- 💄 自定义表情调整浮窗信息显示逻辑优化自定义表情label显示判断
## [0.9.7](https://github.com/BTMuli/TeyvatGuide/releases/v0.9.7) (2026-02-26)

View File

@@ -2,12 +2,12 @@
Author: 目棃
Description: 说明文档
Date: 2023-03-05
Update: 2026-02-25
Update: 2026-03-13
---
> 本文档 [`Frontmatter`](https://github.com/BTMuli/MuCli#Frontmatter) 由 [MuCli](https://github.com/BTMuli/Mucli) 自动生成于 `2023-03-05 14:41:55`
>
> 更新于 `2026-02-25 19:28:26`
> 更新于 `2026-03-13 13:46:30`
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/BTMuli/TeyvatGuide) [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2FBTMuli%2FTeyvatGuide.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2FBTMuli%2FTeyvatGuide?ref=badge_shield)
@@ -89,10 +89,7 @@ Game Tool for Genshin Impact player, supports Windows and macOS.
## 贡献者 / Contributors
- [BTMuli](https://github.com/BTMuli)
- [舰队的偶像岛风酱!](https://github.com/frg2089)
- [jerry765](https://github.com/jerry765)
- [AuroraZiling](https://github.com/AuroraZiling)
[Contributors](https://github.com/BTMuli/TeyvatGuide/graphs/contributors)
## UI 参考 / UI Reference

View File

@@ -1,9 +1,9 @@
{
"name": "teyvatguide",
"version": "0.9.7",
"version": "0.9.8",
"description": "Game Tool for GenshinImpact player",
"private": true,
"packageManager": "pnpm@10.30.1",
"packageManager": "pnpm@10.32.1",
"type": "module",
"scripts": {
"build": "tsx scripts/auto-build.ts",
@@ -72,9 +72,9 @@
"dependencies": {
"@date-fns/tz": "^1.4.1",
"@mdi/font": "7.4.47",
"@sentry/core": "^10.40.0",
"@sentry/vite-plugin": "^5.1.0",
"@sentry/vue": "^10.40.0",
"@sentry/core": "^10.45.0",
"@sentry/vite-plugin": "^5.1.1",
"@sentry/vue": "^10.45.0",
"@skipperndt/plugin-machine-uid": "^0.1.3",
"@tauri-apps/api": "^2.10.1",
"@tauri-apps/plugin-cli": "^2.4.1",
@@ -89,7 +89,7 @@
"@tauri-apps/plugin-process": "^2.3.1",
"@tauri-apps/plugin-sql": "^2.3.2",
"ajv": "^8.18.0",
"artplayer": "^5.3.0",
"artplayer": "^5.4.0",
"colord": "^2.9.3",
"date-fns": "^4.1.0",
"echarts": "^6.0.0",
@@ -101,64 +101,64 @@
"pinia-plugin-persistedstate": "^4.7.1",
"qrcode.vue": "^3.8.0",
"rsa-oaep-encryption": "^1.1.0",
"sass-embedded": "^1.97.3",
"sass-embedded": "^1.98.0",
"swiper": "^12.1.2",
"uuid": "^13.0.0",
"vue": "^3.5.29",
"vue": "^3.5.30",
"vue-echarts": "^8.0.1",
"vue-json-pretty": "^2.6.0",
"vue-router": "^5.0.3",
"vuetify": "^4.0.0",
"vue-router": "^5.0.4",
"vuetify": "^4.0.3",
"wcag-color": "^1.1.1"
},
"devDependencies": {
"@btmuli/stylelint-plugin-color": "^0.1.0",
"@eslint/eslintrc": "^3.3.4",
"@eslint/eslintrc": "^3.3.5",
"@eslint/js": "9.39.2",
"@microsoft/tsdoc": "^0.16.0",
"@tauri-apps/cli": "2.10.0",
"@tauri-apps/cli": "2.10.1",
"@types/fs-extra": "^11.0.4",
"@types/js-md5": "^0.8.0",
"@types/json-bigint": "^1.0.4",
"@types/node": "^25.3.0",
"@typescript-eslint/parser": "^8.56.1",
"@typescript/native-preview": "7.0.0-dev.20260222.1",
"@vitejs/plugin-vue": "^6.0.4",
"@types/node": "^25.5.0",
"@typescript-eslint/parser": "^8.57.1",
"@typescript/native-preview": "7.0.0-dev.20260321.1",
"@vitejs/plugin-vue": "^6.0.5",
"app-root-path": "^3.1.0",
"concurrently": "^9.2.1",
"envfile": "^7.1.0",
"eslint": "9.39.2",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-jsonc": "^3.1.0",
"eslint-plugin-jsonc": "^3.1.2",
"eslint-plugin-prettier": "^5.5.5",
"eslint-plugin-tsdoc": "^0.5.1",
"eslint-plugin-tsdoc": "^0.5.2",
"eslint-plugin-vue": "^10.8.0",
"eslint-plugin-yml": "^3.3.0",
"fs-extra": "^11.3.3",
"globals": "^17.3.0",
"eslint-plugin-yml": "^3.3.1",
"fs-extra": "^11.3.4",
"globals": "^17.4.0",
"husky": "^9.1.7",
"jsonc-eslint-parser": "^3.1.0",
"lint-staged": "^16.2.7",
"oxlint": "^1.50.0",
"lint-staged": "16.4.0",
"oxlint": "^1.56.0",
"postcss-preset-env": "^11.2.0",
"prettier": "3.8.1",
"stylelint": "^17.3.0",
"stylelint": "^17.5.0",
"stylelint-config-idiomatic-order": "^10.0.0",
"stylelint-config-standard-scss": "^17.0.0",
"stylelint-config-standard-vue": "^1.0.0",
"stylelint-declaration-block-no-ignored-properties": "^3.0.0",
"stylelint-high-performance-animation": "^2.0.0",
"stylelint-order": "^7.0.1",
"stylelint-order": "^8.1.1",
"stylelint-prettier": "^5.0.3",
"stylelint-scss": "^7.0.0",
"tsx": "^4.21.0",
"typescript": "^5.9.3",
"typescript-eslint": "^8.56.1",
"vite": "npm:rolldown-vite@^7.3.1",
"vite-plugin-vue-devtools": "^8.0.6",
"typescript-eslint": "^8.57.1",
"vite": "^8.0.1",
"vite-plugin-vue-devtools": "^8.1.0",
"vite-plugin-vuetify": "^2.1.3",
"vue-eslint-parser": "^10.4.0",
"vue-tsc": "^3.2.5",
"vue-tsc": "^3.2.6",
"yaml-eslint-parser": "^2.0.0"
}
}

2937
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

850
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package]
name = "TeyvatGuide"
version = "0.9.7"
version = "0.9.8"
description = "Game Tool for Genshin Impact player"
authors = ["BTMuli <bt-muli@outlook.com>"]
license = "MIT"
@@ -17,19 +17,19 @@ name = "teyvat_guide_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]
tauri-build = { version = "2.5.5", features = [] }
tauri-build = { version = "2.5.6", features = [] }
[dependencies]
chrono = "0.4.43"
image = "0.25.9"
chrono = "0.4.44"
image = "0.25.10"
log = "0.4.29"
prost = "=0.14.1"
prost-types = "=0.14.1"
sentry = { version = "0.46.2", features = ["backtrace", "panic"] }
sentry = { version = "0.47.0", features = ["backtrace", "panic"] }
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.149"
tauri = { version = "2.10.2", features = ["tray-icon"] }
tauri-utils = "2.8.2"
tauri = { version = "2.10.3", features = ["tray-icon"] }
tauri-utils = "2.8.3"
tauri-plugin-machine-uid = "0.1.3"
url = "2.5.8"
walkdir = "2.5.0"

View File

@@ -26,6 +26,7 @@
"core:window:allow-set-title",
"core:window:allow-set-fullscreen",
"core:window:allow-set-focus",
"core:window:allow-set-position",
"core:window:allow-show",
"core:window:default",
{

View File

@@ -21,6 +21,7 @@
"core:window:allow-set-always-on-top",
"core:window:allow-set-focus",
"core:window:allow-set-fullscreen",
"core:window:allow-set-position",
"core:window:allow-set-size",
"core:window:allow-set-title",
"core:window:allow-show",

View File

@@ -19,6 +19,7 @@
"core:window:allow-destroy",
"core:window:allow-is-minimized",
"core:window:allow-set-focus",
"core:window:allow-set-position",
"core:window:allow-set-size",
"core:window:allow-set-title",
"core:window:allow-show",
@@ -58,7 +59,7 @@
{ "url": "https://*.mihoyo.com/*" },
{ "url": "https://homa.gentle.house/*" },
{ "url": "https://*.hoyoverse.com/*" },
{ "url": "https://api.hakush.in/*" }
{ "url": "https://api.github.com/repos/BTMuli/TeyvatGuide/releases/latest" }
]
}
],

View File

@@ -2,7 +2,7 @@
"$schema": "https://schema.tauri.app/config/2",
"productName": "TeyvatGuide",
"identifier": "TeyvatGuide",
"version": "0.9.7",
"version": "0.9.8",
"build": {
"beforeDevCommand": "pnpm vite:dev",
"beforeBuildCommand": "pnpm vite:build",
@@ -39,7 +39,9 @@
"label": "TeyvatGuide",
"additionalBrowserArgs": "--disable-features=msWebOOUI,msPdfOOUI,msSmartScreenProtection --autoplay-policy=no-user-gesture-required",
"width": 1600,
"minWidth": 1600,
"height": 900,
"minHeight": 900,
"center": true,
"visible": false
}

View File

@@ -28,12 +28,12 @@ import useAppStore from "@store/app.js";
import useUserStore from "@store/user.js";
import { app, core, event, webviewWindow } from "@tauri-apps/api";
import type { Event, UnlistenFn } from "@tauri-apps/api/event";
import { getCurrentWindow, LogicalSize } from "@tauri-apps/api/window";
import { getCurrentWindow } from "@tauri-apps/api/window";
import { type CliMatches, getMatches } from "@tauri-apps/plugin-cli";
import { mkdir } from "@tauri-apps/plugin-fs";
import { openUrl } from "@tauri-apps/plugin-opener";
import TGLogger from "@utils/TGLogger.js";
import { getWindowSize, resizeWindow } from "@utils/TGWindow.js";
import { resizeWindow, setWindowPos } from "@utils/TGWindow.js";
import { storeToRefs } from "pinia";
import { computed, nextTick, onMounted, onUnmounted, ref } from "vue";
import { useRouter } from "vue-router";
@@ -74,7 +74,11 @@ onMounted(async () => {
textScaleListener = await event.listen<void>("text_scale_change", resizeWindow);
const isShow = await win.isVisible();
if (!isShow) {
await win.center();
if (needResize.value === "false") {
await setWindowPos();
} else {
await win.center();
}
await win.show();
}
if (showFeedback.value) {
@@ -270,15 +274,11 @@ function handleThemeListen(event: Event<string>): void {
* @returns {Promise<void>}
*/
async function handleResizeListen(event: Event<string>): Promise<void> {
const win = getCurrentWindow();
const webview = webviewWindow.getCurrentWebviewWindow();
if (event.payload !== "false") {
await resizeWindow();
await win.center();
await getCurrentWindow().center();
} else {
const size = getWindowSize(webview.label);
await win.setSize(new LogicalSize(size.width, size.height));
await webview.setZoom(1);
await setWindowPos();
}
}
@@ -485,7 +485,11 @@ async function handleCommands(cmds: CliMatches): Promise<void> {
}
}
</script>
<style lang="css" scoped>
<style lang="scss" scoped>
.v-application {
color: var(--app-page-content);
}
.app-container {
height: 100%;
background: var(--app-page-bg);

View File

@@ -1,6 +1,6 @@
/**
* 全局样式文件
* @since Beta v0.9.6
* @since Beta v0.9.9
*/
@use "fonts/index";
@@ -54,7 +54,7 @@
--tgi-geetest: 100;
--tgi-loading: 100;
--tgi-hyperlink: 100;
--tgi-snackbar: 999;
--tgi-snackbar: 9999;
}
/**

View File

@@ -1,17 +1,27 @@
<!-- 版块小组件菜单 -->
<template>
<div class="tgn-container">
<div v-for="navItem in nav" :key="navItem.id" class="tgn-nav" @click="toNav(navItem)">
<TMiImg :ori="true" :src="navItem.icon" alt="navIcon" />
<span>{{ navItem.name }}</span>
</div>
<div v-if="hasNav" class="tgn-nav" title="查看兑换码">
<v-icon v-if="!loadCode" color="var(--tgc-od-orange)" size="25" @click="tryGetCode">
mdi-code-tags-check
</v-icon>
<v-progress-circular v-else color="var(--tgc-od-orange)" indeterminate size="25" />
</div>
<ToLivecode v-model="showOverlay" :actId="actId" :data="codeData" :gid="model" />
<TGameNavItem
v-for="navItem in nav"
:key="navItem.id"
:label="navItem.name"
:mini
class="tgn-nav"
@click="toNav(navItem)"
>
<template #icon>
<TMiImg :size="28" :ori="true" :src="navItem.icon" alt="navIcon" />
</template>
</TGameNavItem>
<TGameNavItem v-if="hasNav" :mini class="tgn-nav" label="兑换码">
<template #icon>
<v-icon v-if="!loadCode" color="var(--tgc-od-orange)" size="28" @click="tryGetCode">
mdi-code-tags-check
</v-icon>
<v-progress-circular v-else color="var(--tgc-od-orange)" indeterminate size="28" />
</template>
</TGameNavItem>
<ToLivecode v-model="showOverlay" :actId="actId" :data="codeData" :gid />
</div>
</template>
<script lang="ts" setup>
@@ -28,12 +38,15 @@ import { createPost } from "@utils/TGWindow.js";
import { storeToRefs } from "pinia";
import { computed, onMounted, ref, shallowRef, watch } from "vue";
import TGameNavItem from "./t-gameNavItem.vue";
import TMiImg from "./t-mi-img.vue";
import ToLivecode from "./to-livecode.vue";
const { isLogin } = storeToRefs(useAppStore());
type TGameNavProps = { gid: number; mini?: boolean };
const model = defineModel<number>({ default: 2 });
const props = withDefaults(defineProps<TGameNavProps>(), { gid: 2, mini: false });
const { isLogin } = storeToRefs(useAppStore());
const actId = ref<string>();
const showOverlay = ref<boolean>(false);
@@ -50,7 +63,7 @@ const hasNav = computed<TGApp.BBS.Navigator.Navigator | undefined>(() => {
onMounted(async () => await loadNav());
watch(
() => model.value,
() => props.gid,
async () => await loadNav(),
);
@@ -60,7 +73,7 @@ watch(
*/
async function loadNav(): Promise<void> {
try {
nav.value = await ApiHubReq.home(model.value);
nav.value = await ApiHubReq.home(props.gid);
console.debug(`[TGameNav][loadNav] 组件数据:`, nav.value);
if (loadCode.value) loadCode.value = false;
} catch (e) {
@@ -158,7 +171,7 @@ async function toBBS(link: URL): Promise<void> {
}
if (link.hostname === "forum") {
const forumId = link.pathname.split("/").pop();
const localPath = `/posts/forum/${model.value}/${forumId}`;
const localPath = `/posts/forum/${props.gid}/${forumId}`;
await emit("active_deep_link", `router?path=${localPath}`);
return;
}
@@ -179,31 +192,10 @@ async function toBBS(link: URL): Promise<void> {
.tgn-nav {
@include github-styles.github-card;
display: flex;
align-items: center;
justify-content: center;
padding: 4px;
border-radius: 4px;
color: var(--tgc-white-1);
cursor: pointer;
img {
width: 28px;
height: 28px;
}
span {
display: none;
margin-left: 4px;
color: var(--common-text-title);
font-family: var(--font-title);
font-size: 16px;
white-space: nowrap;
}
&:hover span {
display: block;
}
}
.dark .tgn-nav {

View File

@@ -0,0 +1,50 @@
<!-- 版块组件项 -->
<template>
<div :title="props.label" class="tgni-box">
<slot name="icon"></slot>
<span v-show="!props.mini" ref="TgniLabelRef">{{ props.label }}</span>
</div>
</template>
<script lang="ts" setup>
import { computed, useTemplateRef } from "vue";
type TGameNavItemProps = { label: string; mini: boolean };
const props = defineProps<TGameNavItemProps>();
const labelEl = useTemplateRef<HTMLSpanElement>("TgniLabelRef");
const width = computed<string>(() => {
if (!labelEl.value || props.mini) return "38px";
return `${labelEl.value.clientWidth + 42}px`;
});
</script>
<style lang="scss" scoped>
@use "@styles/github.styles.scss" as github-styles;
.tgni-box {
position: relative;
display: flex;
overflow: hidden;
width: 38px;
flex-wrap: nowrap;
align-items: center;
justify-content: flex-start;
padding: 4px;
border-radius: 4px;
color: var(--tgc-white-1);
column-gap: 4px;
cursor: pointer;
transition: width ease-in-out 0.5s;
span {
color: var(--common-text-title);
font-family: var(--font-title);
font-size: 16px;
white-space: nowrap;
}
&:hover {
width: v-bind(width);
transition: width ease-in-out 0.5s;
}
}
</style>

View File

@@ -34,6 +34,7 @@ async function switchPin(): Promise<void> {
@include github-styles.github-card;
position: fixed;
z-index: 1;
top: 64px;
left: 16px;
display: flex;
@@ -44,8 +45,16 @@ async function switchPin(): Promise<void> {
justify-content: center;
border-radius: 50%;
cursor: pointer;
transition:
transform 0.2s ease,
box-shadow 0.2s ease;
&:active {
transform: scale(0.92);
}
&.active {
animation: pin-pulse 0.3s ease;
background: var(--tgc-btn-1);
box-shadow: 1px 3px 6px var(--common-shadow-2);
color: var(--btn-text);
@@ -53,6 +62,21 @@ async function switchPin(): Promise<void> {
&:hover:not(.active) {
background: var(--common-shadow-1);
transform: scale(1.05);
}
}
@keyframes pin-pulse {
0% {
transform: scale(1);
}
50% {
transform: scale(1.15);
}
100% {
transform: scale(1);
}
}

View File

@@ -31,6 +31,7 @@ function switchPostWidth(): void {
@include github-styles.github-card;
position: fixed;
z-index: 1;
top: 112px;
left: 16px;
display: flex;
@@ -41,8 +42,16 @@ function switchPostWidth(): void {
justify-content: center;
border-radius: 50%;
cursor: pointer;
transition:
transform 0.2s ease,
box-shadow 0.2s ease;
&:active {
transform: scale(0.92);
}
&.active {
animation: width-pulse 0.3s ease;
background: var(--tgc-btn-1);
box-shadow: 1px 3px 6px var(--common-shadow-2);
color: var(--btn-text);
@@ -50,6 +59,21 @@ function switchPostWidth(): void {
&:hover:not(.active) {
background: var(--common-shadow-1);
transform: scale(1.05);
}
}
@keyframes width-pulse {
0% {
transform: scale(1);
}
50% {
transform: scale(1.15);
}
100% {
transform: scale(1);
}
}

View File

@@ -19,23 +19,18 @@
</div>
</div>
<div
v-else-if="props.modelValue.post.images.length > 1"
:title="`图片数:${props.modelValue.post.images.length}`"
v-else-if="props.post.post.images.length > 1"
:title="`图片数:${props.post.post.images.length}`"
class="tpc-image-cnt"
>
<v-icon size="10">mdi-folder-multiple-image</v-icon>
<span>{{ props.modelValue.post.images.length }}</span>
<span>{{ props.post.post.images.length }}</span>
</div>
</div>
<div :title="card.title" class="tpc-title" @click="shareCard()">{{ card.title }}</div>
</div>
<div v-if="card.user !== null" class="tpc-mid">
<TpAvatar
:data="card.user"
:style="{ cursor: props.userClick ? 'pointer' : 'default' }"
position="left"
@click="onUserClick()"
/>
<TpAvatar :data="card.user" position="left" @click="onUserClick()" />
</div>
<div class="tpc-bottom">
<div class="tpc-tags">
@@ -103,10 +98,10 @@
data-html2canvas-ignore
@click.stop="trySelect()"
/>
<div v-else class="tpc-info-id">
<span>{{ props.modelValue.post.post_id }}</span>
<div v-else class="tpc-info-id" @click="shareCard()">
<span>{{ props.post.post.post_id }}</span>
<template v-if="isDevEnv">
<span data-html2canvas-ignore>[{{ props.modelValue.post.view_type }}]</span>
<span data-html2canvas-ignore>[{{ props.post.post.view_type }}]</span>
</template>
</div>
</div>
@@ -126,10 +121,12 @@ import { storeToRefs } from "pinia";
import { computed, onMounted, ref, shallowRef, watch } from "vue";
import { useRoute, useRouter } from "vue-router";
/** 帖子卡片参数 */
type TPostCardProps = {
modelValue: TGApp.BBS.Post.FullData;
/** 帖子数据 */
post: TGApp.BBS.Post.FullData;
/** 是否开启选择模式 */
selectMode?: boolean;
userClick?: boolean;
};
type TPostCardEmits = {
(e: "onSelected", v: string): void;
@@ -178,17 +175,17 @@ const cardBg = computed<string>(() => {
const forumBg = computed<string>(() =>
str2Color(`${card.value?.forum?.id}${card.value?.forum?.icon}${card.value?.forum?.name}`, -60),
);
const idBg = computed<string>(() => str2Color(`${props.modelValue.post.post_id}`, 0));
const idBg = computed<string>(() => str2Color(`${props.post.post.post_id}`, 0));
onMounted(async () => (card.value = getPostCard(props.modelValue)));
onMounted(async () => (card.value = getPostCard(props.post)));
watch(
() => props.modelValue,
async () => (card.value = getPostCard(props.modelValue)),
() => props.post,
async () => (card.value = getPostCard(props.post)),
);
function trySelect(): void {
if (props.selectMode) emits("onSelected", props.modelValue.post.post_id);
if (props.selectMode) emits("onSelected", props.post.post.post_id);
isSelected.value = !isSelected.value;
}
@@ -230,7 +227,7 @@ function getCommonCard(item: TGApp.BBS.Post.FullData): RenderCard {
const findG = forumList.value.find((i) => i.game_id === item.post.game_id);
if (findG) {
console.log(findG, item);
const findF = findG.forums.find((i) => i.id === item.forum.id);
const findF = findG.forums.find((i) => i.id === item.forum!.id);
if (findF) forumIcon = findF.icon_pure;
}
forumData = { name: item.forum.name, icon: forumIcon, id: item.forum.id };
@@ -293,20 +290,20 @@ async function shareCard(): Promise<void> {
async function toTopic(topic: TGApp.BBS.Post.Topic): Promise<void> {
if (props.selectMode) return;
const gid = props.modelValue.post.game_id;
const gid = props.post.post.game_id;
await emit("active_deep_link", `router?path=/posts/topic/${gid}/${topic.id}`);
}
async function toForum(forum: RenderForum): Promise<void> {
if (props.selectMode) return;
const gid = props.modelValue.post.game_id;
const gid = props.post.post.game_id;
await emit("active_deep_link", `router?path=/posts/forum/${gid}/${forum.id}`);
}
function onUserClick(): void {
if (props.selectMode) return;
if (!card.value || card.value.user === null) return;
emits("onUserClick", card.value.user, props.modelValue.post.game_id);
emits("onUserClick", card.value.user, props.post.post.game_id);
}
</script>
<style lang="scss" scoped>
@@ -575,6 +572,7 @@ function onUserClick(): void {
border-top-left-radius: 4px;
box-shadow: 1px 1px 6px var(--tgc-dark-1);
color: var(--tgc-white-1);
cursor: pointer;
font-size: 12px;
text-shadow: 0 0 4px var(--tgc-dark-1);
}

View File

@@ -41,6 +41,7 @@ async function shareContent(): Promise<void> {
.share-box {
position: fixed;
z-index: 1;
top: 16px;
right: 16px;
display: flex;
@@ -54,6 +55,16 @@ async function shareContent(): Promise<void> {
box-shadow: 1px 3px 6px var(--common-shadow-2);
color: var(--btn-text);
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.share-box:hover {
box-shadow: 2px 4px 12px var(--common-shadow-4);
transform: scale(1.15);
}
.share-box:active {
transform: scale(0.95);
}
.dark .share-box {

View File

@@ -1,6 +1,6 @@
<!-- 应用侧边栏 -->
<template>
<v-navigation-drawer :permanent="true" :rail="rail" class="tsb-box">
<v-navigation-drawer :permanent="true" :rail="rail" :width="160" class="tsb-box">
<v-list :nav="true" class="side-list" density="compact">
<v-list-item
:append-icon="!rail ? 'mdi-chevron-left' : undefined"
@@ -332,7 +332,6 @@ import { invoke } from "@tauri-apps/api/core";
import type { Event, UnlistenFn } from "@tauri-apps/api/event";
import { readDir } from "@tauri-apps/plugin-fs";
import mhyClient from "@utils/TGClient.js";
import { isRunInAdmin, tryCopyYae, tryReadGameVer, YAE_GAME_VER } from "@utils/TGGame.js";
import TGLogger from "@utils/TGLogger.js";
import { storeToRefs } from "pinia";
import { computed, onMounted, onUnmounted, ref, shallowRef } from "vue";
@@ -690,6 +689,7 @@ async function addByCookie(): Promise<void> {
};
uid.value = briefRes.uid;
briefInfo.value = briefInfoGet;
cookie.value = ck;
isLogin.value = true;
await showLoading.update("正在保存用户数据");
await TSUserAccount.account.saveAccount({
@@ -749,34 +749,13 @@ async function tryLaunchGame(): Promise<void> {
await TGLogger.Error(`[sidebar][tryLaunchGame] resp: ${JSON.stringify(resp)}`);
return;
}
const isInAdmin = await isRunInAdmin();
const gameVer = await tryReadGameVer(gameDir.value);
if (!isInAdmin || !gameVer || gameVer !== YAE_GAME_VER) {
showSnackbar.success(`成功获取ticket:${resp},正在启动应用...`);
try {
await invoke("launch_game", { path: gamePath, ticket: resp });
} catch (error) {
showSnackbar.error(`${error}`);
}
return;
}
const isMsix = await invoke<boolean>("is_msix");
if (isMsix) {
const copy = await tryCopyYae();
if (!copy) return;
}
showSnackbar.success(`成功获取ticket:${resp},正在启动应用...`);
try {
await invoke("call_yae_dll", {
gamePath: gamePath,
uid: account.value.gameUid,
ticket: resp,
isMsix: isMsix,
});
} catch (err) {
showSnackbar.error(`调用Yae DLL失败: ${err}`);
await TGLogger.Error(`[pageAchi][toYae]调用Yae DLL失败: ${err}`);
return;
await invoke("launch_game", { path: gamePath, ticket: resp });
} catch (error) {
showSnackbar.error(`${error}`);
}
return;
}
</script>
<style lang="scss" scoped>
@@ -791,9 +770,6 @@ async function tryLaunchGame(): Promise<void> {
.side-list {
position: relative;
height: 100%;
padding-top: 0;
padding-right: 0;
padding-bottom: 0;
font-family: var(--font-title);
}
@@ -806,7 +782,7 @@ async function tryLaunchGame(): Promise<void> {
.bottom-menu {
position: absolute;
bottom: 0;
bottom: 8px;
width: 100%;
}
@@ -818,6 +794,7 @@ async function tryLaunchGame(): Promise<void> {
width: 24px;
height: 24px;
border-radius: 4px;
transition: transform 0.2s ease;
&.paimon {
position: relative;
@@ -826,6 +803,10 @@ async function tryLaunchGame(): Promise<void> {
height: 32px;
border-radius: 50%;
}
&:hover {
transform: scale(1.15);
}
}
.side-list-menu {

View File

@@ -1,6 +1,6 @@
<template>
<div class="switch-box" :title="isDefault ? '切换到深色模式' : '切换到浅色模式'">
<div class="switch-btn" @click="switchTheme()">
<div class="switch-btn" :class="{ 'is-dark': !isDefault }" @click="switchTheme()">
<v-icon size="20">
{{ isDefault ? "mdi-weather-night" : "mdi-weather-sunny" }}
</v-icon>
@@ -43,6 +43,7 @@ onUnmounted(() => {
.switch-box {
position: fixed;
z-index: 1;
top: 16px;
left: 16px;
display: flex;
@@ -56,6 +57,12 @@ onUnmounted(() => {
box-shadow: 1px 3px 6px var(--common-shadow-2);
color: var(--btn-text);
cursor: pointer;
transition: all 0.3s ease;
}
.switch-box:hover {
box-shadow: 1px 3px 10px var(--common-shadow-4);
transform: scale(1.1);
}
.dark .switch-box {
@@ -71,5 +78,10 @@ onUnmounted(() => {
height: 20px;
align-items: center;
justify-content: center;
transition: transform 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55);
}
.switch-btn.is-dark {
transform: rotate(360deg);
}
</style>

View File

@@ -1,23 +1,23 @@
<!-- 兑换码浮窗组件 -->
<template>
<TOverlay v-model="visible" class="tolc-overlay">
<div class="tolc-box">
<div ref="TolcRef" class="tolc-box">
<div class="tolc-title">
<span>{{ gameInfo?.name ?? "" }}兑换码</span>
<v-icon
data-html2canvas-ignore
icon="mdi-share-variant"
size="18px"
title="share"
@click="shareImg()"
icon="mdi-share-variant"
variant="outlined"
data-html2canvas-ignore
@click="shareImg()"
/>
</div>
<div class="tolc-info">ActID:{{ props.actId }}</div>
<div v-for="(item, index) in props.data" :key="index" class="tolc-list-box">
<div class="tolc-list-icon">
<img v-if="item.img === ''" src="/UI/app/empty.webp" alt="empty" />
<TMiImg :src="item.img" :ori="true" v-else alt="award" />
<img v-if="item.img === ''" alt="empty" src="/UI/app/empty.webp" />
<TMiImg v-else :ori="true" :src="item.img" alt="award" />
</div>
<div class="tolc-list-info">
<span>{{ item.code === "" ? "暂无兑换码" : item.code }}</span>
@@ -26,12 +26,12 @@
</div>
<div class="tolc-list-btn">
<v-btn
size="small"
:disabled="item.code === ''"
@click="copy(item.code)"
icon="mdi-content-copy"
variant="outlined"
data-html2canvas-ignore
icon="mdi-content-copy"
size="small"
variant="outlined"
@click="copy(item.code)"
/>
</div>
</div>
@@ -39,13 +39,13 @@
</div>
</TOverlay>
</template>
<script setup lang="ts">
<script lang="ts" setup>
import showSnackbar from "@comp/func/snackbar.js";
import useBBSStore from "@store/bbs.js";
import { generateShareImg } from "@utils/TGShare.js";
import { timestampToDate } from "@utils/toolFunc.js";
import { storeToRefs } from "pinia";
import { computed } from "vue";
import { computed, useTemplateRef } from "vue";
import TMiImg from "./t-mi-img.vue";
import TOverlay from "./t-overlay.vue";
@@ -66,6 +66,7 @@ const { gameList } = storeToRefs(useBBSStore());
const props = defineProps<ToLiveCodeProps>();
const visible = defineModel<boolean>({ default: false });
const tolcEl = useTemplateRef<HTMLDivElement>("TolcRef");
const gameInfo = computed<TGApp.BBS.Game.Item | undefined>(() =>
gameList.value.find((i) => i.id === props.gid),
);
@@ -80,23 +81,27 @@ async function copy(code: string): Promise<void> {
showSnackbar.success("已复制到剪贴板");
}
/**
* 生成分享图片
* @returns {Promise<void>}
*/
async function shareImg(): Promise<void> {
const element = document.querySelector<HTMLElement>(".tolc-box");
if (element === null) {
if (tolcEl.value === null) {
showSnackbar.warn("未获取到分享内容");
return;
}
const fileName = `LiveCode_${props.gid}_${props.actId}_${new Date().getTime()}`;
await generateShareImg(fileName, element, 4);
await generateShareImg(fileName, tolcEl.value, 4);
}
</script>
<style lang="css" scoped>
<style lang="scss" scoped>
/** 解决默认样式的上下 margin */
:deep(p) {
margin-block: 0;
}
.tolc-overlay {
position: fixed;
z-index: 1;
height: 100vh;
color: var(--app-page-content);
}
.tolc-box {

View File

@@ -1,7 +1,7 @@
<!-- Loading 组件 -->
<template>
<transition name="func-loading">
<div v-show="showBox || showOuter" class="loading-overlay">
<div v-show="showBox || showOuter" ref="LoadingRef" class="loading-overlay">
<transition name="func-loading-inner">
<div v-show="showInner" class="loading-container">
<div class="loading-box">
@@ -29,20 +29,22 @@
<script lang="ts" setup>
import showSnackbar from "@comp/func/snackbar.js";
import bbsReq from "@req/bbsReq.js";
import { onMounted, ref, shallowRef, watch } from "vue";
import { onMounted, onUnmounted, ref, shallowRef, useTemplateRef, watch } from "vue";
import { LoadingParams } from "./loading.js";
const defaultIcon = "/UI/app/loading.webp";
const props = defineProps<LoadingParams>();
const showBox = ref<boolean>(false);
const showOuter = ref<boolean>(false);
const showInner = ref<boolean>(false);
const props = defineProps<LoadingParams>();
const iconUrl = ref<string>(defaultIcon);
const data = shallowRef<LoadingParams>(props);
const localEmojis = shallowRef<Array<string>>([]);
const iconUrl = ref<string>(defaultIcon);
const loadingEl = useTemplateRef<HTMLDivElement>("LoadingRef");
watch(
() => showBox.value,
@@ -60,7 +62,11 @@ watch(
},
);
onMounted(async () => await displayBox(props));
onMounted(async () => {
await displayBox(props);
loadingEl.value?.addEventListener("contextmenu", (e) => e.preventDefault());
});
onUnmounted(() => loadingEl.value?.removeEventListener("contextmenu", (e) => e.preventDefault()));
async function getRandomEmoji(): Promise<void> {
if (localEmojis.value.length === 0) {
@@ -170,9 +176,9 @@ defineExpose({ displayBox });
align-items: center;
justify-content: center;
padding: 10px;
border: #f4d8a8ff 1px solid;
border: var(--tgc-yellow-3) 1px solid;
border-radius: 5px;
color: #f4d8a8ff;
color: var(--tgc-yellow-2);
}
.loading-title {

View File

@@ -76,6 +76,7 @@ import { openPath } from "@tauri-apps/plugin-opener";
import { platform } from "@tauri-apps/plugin-os";
import { backUpUserData } from "@utils/dataBS.js";
import { tryReadGameVer } from "@utils/TGGame.js";
import TGLogger from "@utils/TGLogger.js";
import { storeToRefs } from "pinia";
import { onMounted, ref, watch } from "vue";
@@ -109,7 +110,7 @@ onMounted(async () => {
async function confirmCUD(): Promise<void> {
const oriDir = userDir.value;
const changeCheck = await showDialog.check("确认修改用户数据路径吗?");
const changeCheck = await showDialog.check("确认修改用户数据路径吗?", "请选取空目录");
if (!changeCheck) {
showSnackbar.cancel("已取消修改");
return;
@@ -123,18 +124,43 @@ async function confirmCUD(): Promise<void> {
showSnackbar.warn("路径未修改!");
return;
}
const dirRead = await readDir(dir);
if (dirRead.length !== 0) {
showSnackbar.warn("请选择空目录");
return;
}
await TGLogger.Info(`[TcDataDir] 修改用户数据目录: ${userDir.value}${dir}`);
userDir.value = dir;
await TGSqlite.saveAppData("userDir", dir);
await backUpUserData(dir);
showSnackbar.success("已修改用户数据路径!");
const delCheck = await showDialog.check("是否删除原用户数据目录?");
if (delCheck) {
await remove(oriDir, { recursive: true });
showSnackbar.success("已删除原用户数据目录!");
if (!delCheck) {
showSnackbar.cancel(`取消删除原数据目录`);
return;
}
showSnackbar.info("即将刷新页面...");
await new Promise<void>((resolve) => setTimeout(resolve, 1500));
window.location.reload();
const delDirRead = await readDir(oriDir);
if (delDirRead.some((i) => i.isDirectory)) {
const check = await showDialog.check(`检测到子目录,确定删除?`, oriDir);
if (!check) {
showSnackbar.cancel(`取消删除原数据目录`);
return;
}
}
const delCheck2 = await showDialog.check("无法通过回收站恢复,确认删除?", oriDir);
if (!delCheck2) {
showSnackbar.cancel(`取消删除原数据目录`);
return;
}
try {
await remove(oriDir, { recursive: true });
} catch (err) {
if (err instanceof Error) {
showSnackbar.error(err.message);
} else showSnackbar.error(`${err}`);
return;
}
showSnackbar.success("已删除原用户数据目录!");
}
async function confirmCGD(): Promise<void> {

View File

@@ -62,6 +62,11 @@
</template>
<v-list>
<v-list-item v-for="ac in accounts" :key="ac.uid">
<template #prepend>
<v-avatar>
<v-img :src="ac.brief.avatar" alt="avatar" />
</v-avatar>
</template>
<v-list-item-title>{{ ac.brief.nickname }}</v-list-item-title>
<v-list-item-subtitle>{{ ac.brief.uid }}</v-list-item-subtitle>
<template #append>
@@ -252,7 +257,7 @@ async function tryCodeLogin(): Promise<void> {
showLoginQr.value = true;
}
async function refreshUser(uid: string) {
async function refreshUser(uid: string, full: boolean) {
let account = await TSUserAccount.account.getAccount(uid);
if (!account) {
showSnackbar.warn(`未获取到${uid}账号数据,请重新登录!`);
@@ -318,15 +323,19 @@ async function refreshUser(uid: string) {
};
await TGLogger.Info("[tc-userBadge][refreshUserInfo] 获取用户信息成功");
}
if (!full) {
await showLoading.end();
return;
}
await TSUserAccount.account.saveAccount(account);
await showLoading.update("正在获取账号信息");
await showLoading.update("正在获取游戏账号信息");
const accountRes = await takumiReq.bind.gameRoles(ck);
if (Array.isArray(accountRes)) {
await showLoading.update("获取账号信息成功");
await showLoading.update("获取游戏账号信息成功");
await TGLogger.Info("[tc-userBadge][refreshUserInfo] 获取账号信息成功");
await TSUserAccount.game.saveAccounts(account.uid, accountRes);
} else {
await showLoading.update("获取账号信息失败");
await showLoading.update("获取游戏账号信息失败");
showSnackbar.error(`[${accountRes.retcode}]${accountRes.message}`);
await TGLogger.Error("[tc-userBadge][refreshUserInfo] 获取账号信息失败");
await TGLogger.Error(
@@ -359,12 +368,16 @@ async function loadAccount(ac: string): Promise<void> {
}
async function confirmRefreshUser(ac: string): Promise<void> {
const freshCheck = await showDialog.check("确认刷新用户信息吗?", "将会重新获取用户信息");
if (!freshCheck) {
const freshCheck = await showDialog.checkF({
title: "确认刷新用户信息?",
text: "将刷新用户Cookie跟游戏账号",
cancelLabel: "仅刷新Cookie",
});
if (freshCheck === undefined) {
showSnackbar.cancel("已取消刷新用户信息");
return;
}
await refreshUser(ac);
await refreshUser(ac, freshCheck);
if (uid.value === ac) {
showSnackbar.success("成功刷新用户信息");
return;
@@ -510,6 +523,7 @@ async function addByCookie(): Promise<void> {
};
uid.value = briefRes.uid;
briefInfo.value = briefGet;
cookie.value = ck;
isLogin.value = true;
await showLoading.update("正在保存用户数据");
await TSUserAccount.account.saveAccount({

View File

@@ -5,7 +5,6 @@
<div v-if="pools.length < 3" class="pool-grid">
<PhPoolCard v-for="(pool, idx) in pools" :key="idx" :pool="pool" />
</div>
<!-- TODO: 优化Swiper效果 -->
<Swiper
v-else
:autoplay="{ delay: 3000, disableOnInteraction: false }"

View File

@@ -17,7 +17,7 @@
<script lang="ts" setup>
import { computed } from "vue";
// Reward state enum TODO:完善类型
// Reward state enum
const RewardState = <const>{
NORMAL: 0,
SIGNED: 1,

View File

@@ -10,7 +10,7 @@
class="toc-list-item"
@click="toChannel(item)"
>
<TMiImg :ori="true" :src="item.icon" alt="icon" />
<img :src="item.icon" alt="icon" />
<span class="toc-list-title">{{ item.title }}</span>
<span class="toc-list-id">GID:{{ item.gid }}</span>
</div>
@@ -19,7 +19,6 @@
</TOverlay>
</template>
<script lang="ts" setup>
import TMiImg from "@comp/app/t-mi-img.vue";
import TOverlay from "@comp/app/t-overlay.vue";
import showSnackbar from "@comp/func/snackbar.js";
import bbsEnum from "@enum/bbs.js";

View File

@@ -86,7 +86,13 @@ function handleSearch(kw: string): void {
async function searchAchi(): Promise<void> {
if (!props.isSearch) return;
if (!props.search || props.search === "") {
if (!props.search) {
achievements.value = await TSUserAchi.getAchievements(props.uid, props.series);
showSnackbar.success("已重置");
emits("update:isSearch", false);
return;
}
if (props.search === "") {
showSnackbar.warn("请输入搜索内容");
emits("update:isSearch", false);
return;

View File

@@ -265,7 +265,6 @@ function getWeaponTitle(): string {
justify-content: flex-end;
border-radius: 4px;
aspect-ratio: 21/10;
row-gap: 4px;
}
.tua-abl-bg {

View File

@@ -2,7 +2,7 @@
<template>
<div class="tua-dc-container">
<div class="tua-dc-avatar">
<TMiImg :ori="true" :src="fullIcon" alt="avatar" />
<img :src="fullIcon" alt="avatar" />
</div>
<v-btn
:loading="loading"
@@ -76,7 +76,6 @@
</div>
</template>
<script lang="ts" setup>
import TMiImg from "@comp/app/t-mi-img.vue";
import showSnackbar from "@comp/func/snackbar.js";
import TSUserAvatar from "@Sqlm/userAvatar.js";
import useUserStore from "@store/user.js";

View File

@@ -0,0 +1,233 @@
<!-- 筛选&&排序条件展示 -->
<template>
<div v-if="props.isSelected || isOrdered" class="tua-sv-container">
<div v-if="props.isSelected" class="tua-sv-selects">
<div class="tua-sv-title">筛选</div>
<div
v-if="props.selectOpts.costume.length === 1"
:class="props.selectOpts.costume[0] === 'true' ? 'pass' : 'ban'"
class="tua-svs-item"
>
<v-icon size="14">mdi-tshirt-crew</v-icon>
<v-icon v-if="props.selectOpts.costume[0] === 'false'" size="14">mdi-block-helper</v-icon>
</div>
<div
v-if="props.selectOpts.fetter.length === 1"
:class="props.selectOpts.fetter[0] === 'true' ? 'pass' : 'ban'"
class="tua-svs-item"
>
<span>好感:{{ getFetterLabel(props.selectOpts.fetter[0]) }}</span>
</div>
<div v-if="props.selectOpts.star.length === 1" class="tua-svs-item">
<span>{{ getStarLabel(props.selectOpts.star[0]) }}</span>
</div>
<div
v-if="props.selectOpts.level.length === 1"
:class="props.selectOpts.level[0] === 'true' ? 'pass' : 'ban'"
class="tua-svs-item"
>
<span>等级:{{ getLevelLabel(props.selectOpts.level[0]) }}</span>
</div>
<div
v-if="props.selectOpts.weapon.length > 0 && props.selectOpts.weapon.length < FULL_WEAPON"
class="tua-svs-item weapon"
>
<img
v-for="(weapon, idx) in props.selectOpts.weapon"
:key="idx"
:alt="weapon"
:src="`/icon/weapon/${weapon}.webp`"
/>
</div>
<div
v-if="props.selectOpts.element.length > 0 && props.selectOpts.element.length < FULL_ELEMENT"
class="tua-svs-item"
>
<img
v-for="(element, idx) in props.selectOpts.element"
:key="idx"
:alt="element"
:src="`/icon/element/${element}元素.webp`"
/>
</div>
<div
v-if="props.selectOpts.area.length > 0 && props.selectOpts.area.length < FULL_AREA"
class="tua-svs-item"
>
<span>地区</span>
<span v-for="(area, idx) in props.selectOpts.area" :key="idx">{{ area }}</span>
</div>
</div>
<div v-if="isOrdered" class="tua-sv-order">
<div class="tua-sv-title">排序</div>
<div
v-if="props.isLevelUp !== null"
:class="props.isLevelUp ? 'up' : 'down'"
class="tua-svo-item"
>
<span>等级</span>
<span>{{ props.isLevelUp ? "↑" : "↓" }}</span>
</div>
<div
v-if="props.isFetterUp !== null"
:class="props.isFetterUp ? 'up' : 'down'"
class="tua-svo-item"
>
<span>好感</span>
<span>{{ props.isFetterUp ? "↑" : "↓" }}</span>
</div>
<div
v-if="props.isConstUp !== null"
:class="props.isConstUp ? 'up' : 'down'"
class="tua-svo-item"
>
<span>命座</span>
<span>{{ props.isConstUp ? "↑" : "↓" }}</span>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed } from "vue";
import type { UavSelectModel } from "./uav-select.vue";
type TuaSelectValsProps = {
isLevelUp: boolean | null;
isFetterUp: boolean | null;
isConstUp: boolean | null;
isSelected: boolean;
selectOpts: UavSelectModel;
};
const FULL_WEAPON: Readonly<number> = 5;
const FULL_ELEMENT: Readonly<number> = 7;
const FULL_AREA: Readonly<number> = 14;
const props = defineProps<TuaSelectValsProps>();
const isOrdered = computed<boolean>(() => {
return !(props.isLevelUp === null && props.isFetterUp === null && props.isConstUp === null);
});
function getFetterLabel(fetter: string): string {
if (fetter === "true") return "已满";
return "未满";
}
function getStarLabel(star: string): string {
if (star === "4") return "⭐⭐⭐⭐";
return "⭐⭐⭐⭐⭐";
}
function getLevelLabel(level: string): string {
if (level === "true") return "≥70";
return "<70";
}
</script>
<style lang="scss" scoped>
@use "@styles/github.styles.scss" as github-styles;
.tua-sv-container {
position: relative;
display: flex;
width: 100%;
flex-direction: column;
align-items: flex-start;
justify-content: center;
row-gap: 8px;
}
.tua-sv-title {
color: var(--common-text-title);
font-family: var(--font-title);
}
.tua-sv-selects {
position: relative;
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
gap: 4px 12px;
}
.tua-svs-item {
@include github-styles.github-tag-dark-gen(#61afef);
position: relative;
display: flex;
height: 20px;
align-items: center;
justify-content: center;
padding: 0 8px;
border-radius: 12px;
column-gap: 4px;
font-size: 12px;
line-height: 20px;
&.pass {
@include github-styles.github-tag-dark-gen(#98c379);
}
&.ban {
@include github-styles.github-tag-dark-gen(#e06c75);
}
&.weapon {
img {
filter: invert(0.5);
}
}
img {
width: 16px;
height: 16px;
object-fit: contain;
}
& + .tua-svs-item {
margin-left: -4px;
}
}
.dark .tua-svs-item {
&.weapon {
img {
filter: unset;
}
}
}
.tua-sv-order {
position: relative;
display: flex;
align-items: center;
justify-content: center;
column-gap: 12px;
}
.tua-svo-item {
position: relative;
display: flex;
align-items: center;
justify-content: center;
padding: 0 8px;
border-radius: 12px;
column-gap: 4px;
font-size: 12px;
line-height: 20px;
&.down {
@include github-styles.github-tag-dark-gen(#98c379);
}
&.up {
@include github-styles.github-tag-dark-gen(#e06c75);
}
& + .tua-svo-item {
margin-left: -4px;
}
}
</style>

View File

@@ -8,6 +8,12 @@
<UavSelectChips v-model:selected="costumeSelected" :items="costumeOpts" size="small" />
</div>
</div>
<div class="uav-select-item">
<div class="uav-select-title">好感</div>
<div class="uav-select-props">
<UavSelectChips v-model:selected="fetterSelected" :items="fetterOpts" size="small" />
</div>
</div>
<div class="uav-select-item">
<div class="uav-select-title">星级</div>
<div class="uav-select-props">
@@ -15,8 +21,14 @@
</div>
</div>
<div class="uav-select-item">
<div class="uav-select-title">武器</div>
<div class="uav-select-title">等级</div>
<div class="uav-select-props">
<UavSelectChips v-model:selected="levelSelected" :items="levelOpts" size="small" />
</div>
</div>
<div class="uav-select-item">
<div class="uav-select-title">武器</div>
<div class="uav-select-props weapon">
<UavSelectChips v-model:selected="weaponSelected" :items="weaponOpts" size="small" />
</div>
</div>
@@ -51,8 +63,12 @@ import { ref, watch } from "vue";
export type UavSelectModel = {
/** 皮肤 */
costume: Array<string>;
/** 满好感 */
fetter: Array<string>;
/** 星级 */
star: Array<string>;
/** 等级 */
level: Array<string>;
/** 武器 */
weapon: Array<string>;
/** 元素 */
@@ -67,10 +83,18 @@ const costumeOpts: Array<UavSelectChipsItem> = [
{ label: "有", value: "true", title: "有衣装" },
{ label: "无", value: "false", title: "无衣装" },
];
const fetterOpts: Array<UavSelectChipsItem> = [
{ label: "已满", value: "true", title: "满好感" },
{ label: "未满", value: "false", title: "好感未满" },
];
const starOpts: Array<UavSelectChipsItem> = [
{ label: "⭐⭐⭐⭐", value: "4", title: "四星" },
{ label: "⭐⭐⭐⭐⭐", value: "5", title: "五星" },
];
const levelOpts: Array<UavSelectChipsItem> = [
{ label: "≥70", value: "true", title: "不低于70级" },
{ label: "<70", value: "false", title: "低于70级" },
];
const weaponOpts: Array<UavSelectChipsItem> = ["单手剑", "双手剑", "弓", "法器", "长柄武器"].map(
(i) => ({ label: i, value: i, title: i, icon: `/icon/weapon/${i}.webp` }),
);
@@ -97,13 +121,15 @@ const areaOpts: Array<UavSelectChipsItem> = [
const emits = defineEmits<UavSelectEmits>();
const costumeSelected = ref<Array<string>>([]);
const fetterSelected = ref<Array<string>>([]);
const starSelected = ref<Array<string>>([]);
const levelSelected = ref<Array<string>>([]);
const weaponSelected = ref<Array<string>>([]);
const elementSelected = ref<Array<string>>([]);
const areaSelected = ref<Array<string>>([]);
const model = defineModel<UavSelectModel>({
default: { costume: [], star: [], weapon: [], area: [], element: [] },
default: { costume: [], fetter: [], star: [], weapon: [], area: [], element: [] },
});
const visible = defineModel<boolean>("show");
@@ -112,7 +138,9 @@ watch(
() => {
if (visible.value) {
costumeSelected.value = model.value.costume;
fetterSelected.value = model.value.fetter;
starSelected.value = model.value.star;
levelSelected.value = model.value.level;
weaponSelected.value = model.value.weapon;
areaSelected.value = model.value.area;
elementSelected.value = model.value.element;
@@ -127,7 +155,9 @@ function onCancel(): void {
function onConfirm(): void {
model.value = {
costume: costumeSelected.value,
fetter: fetterSelected.value,
star: starSelected.value,
level: levelSelected.value,
weapon: weaponSelected.value,
element: elementSelected.value,
area: areaSelected.value,
@@ -162,6 +192,14 @@ function onConfirm(): void {
column-gap: 8px;
}
.uav-select-props.weapon:deep(img) {
filter: invert(0.75);
}
.dark .uav-select-props.weapon:deep(img) {
filter: unset;
}
.uav-select-acts {
position: relative;
display: flex;

View File

@@ -70,8 +70,10 @@ import { computed, onMounted, ref, shallowRef, watch } from "vue";
type UgoUidProps = {
/** 导入/导出 */
mode: "import" | "export";
/** filePathImport路径 */
/** filePathImport路径 */
fpi?: string;
/** filePathExport导出路径 */
fpe?: string;
};
/**
* UID项
@@ -107,7 +109,7 @@ onMounted(async () => {
});
watch(
() => [visible.value, props.mode, props.fpi],
() => [visible.value, props.mode, props.fpi, props.fpe],
async () => {
if (visible.value) await refreshData();
},
@@ -127,7 +129,7 @@ async function refreshData(): Promise<void> {
fp.value = props.fpi ?? fpEmptyText;
await refreshImport();
} else {
fp.value = await getDefaultSavePath();
fp.value = props.fpe ?? (await getDefaultSavePath());
await refreshExport();
}
}

View File

@@ -1,19 +0,0 @@
<template>
<div v-if="modelValue.length === 0">暂无数据</div>
<div v-else class="tur-hg-box">
<TurHomeSub v-for="(home, index) in modelValue" :key="index" :data="home" />
</div>
</template>
<script lang="ts" setup>
import TurHomeSub from "./tur-home-sub.vue";
defineProps<{ modelValue: Array<TGApp.Sqlite.Record.Home> }>();
</script>
<style lang="css" scoped>
.tur-hg-box {
display: grid;
width: 100%;
gap: 8px;
grid-template-columns: repeat(3, 1fr);
}
</style>

View File

@@ -0,0 +1,89 @@
<template>
<div :title="props.name" class="tur-hi-box">
<img ref="TurHiiRef" :src="props.icon" alt="bg" class="tur-hi-bg" @error="handleIconError" />
<img v-if="isErr" alt="empty" class="tur-hi-empty" src="/UI/app/empty.webp" />
<span class="tur-hi-name">{{ props.name }}</span>
</div>
</template>
<script lang="ts" setup>
import useAppStore from "@store/app.js";
import { str2Color } from "@utils/colorFunc.js";
import { storeToRefs } from "pinia";
import { computed, ref, useTemplateRef } from "vue";
type TurHomeNameProps = { name: string; icon: string };
const { theme } = storeToRefs(useAppStore());
const props = defineProps<TurHomeNameProps>();
const isErr = ref<boolean>(false);
const iconEl = useTemplateRef<HTMLImageElement>("TurHiiRef");
const isDarkMode = computed<boolean>(() => theme.value === "dark");
const color = computed<string>(() =>
tag2Color(`${props.name}_${encodeURIComponent(props.icon)}`, isDarkMode.value),
);
const bg = computed<string>(() => `rgba(${color.value.slice(4, -1)}, 0.5)`);
function handleIconError(e: Event) {
console.debug(e);
if (!iconEl.value) return;
isErr.value = true;
iconEl.value.style.display = "none";
}
function tag2Color(str: string, isDarkMode: boolean = false): string {
const adjust = isDarkMode ? 90 : 120;
return str2Color(str, adjust);
}
</script>
<style lang="scss" scoped>
.tur-hi-box {
position: relative;
display: flex;
overflow: hidden;
width: 100%;
height: 100%;
flex-shrink: 0;
align-items: center;
justify-content: center;
border-radius: 4px;
background: var(--box-bg-1);
}
.tur-hi-bg {
z-index: 0;
width: 100%;
height: 100%;
flex-shrink: 0;
object-fit: cover;
opacity: 0.8;
}
.dark .tur-hi-bg {
opacity: 1;
}
.tur-hi-empty {
width: 48px;
height: 48px;
object-fit: contain;
}
.tur-hi-name {
position: absolute;
z-index: 1;
top: 0;
right: 0;
display: flex;
align-items: center;
justify-content: center;
padding: 0 6px;
backdrop-filter: blur(10px);
background: v-bind(bg);
border-bottom-left-radius: 12px;
color: v-bind(color);
line-height: 24px;
text-shadow: 0 0 4px rgb(0 0 0 / 50%);
}
</style>

View File

@@ -0,0 +1,109 @@
<!-- 尘歌壶数据汇总 -->
<template>
<div class="tur-ho-container">
<div class="tur-hoc-overview">
<div v-if="overview" class="tur-hoco-item">
<img :src="overview.comfortIcon" alt="icon" />
<span>{{ overview.comfortName }}</span>
</div>
<div class="tur-hoco-item">
<span>{{ props.homes.length }}</span>
<span>解锁洞天</span>
</div>
<div class="tur-hoco-item">
<span>{{ overview.level ?? 0 }}</span>
<span>信任等阶</span>
</div>
<div class="tur-hoco-item">
<span>{{ overview.comfort ?? 0 }}</span>
<span>最高洞天仙力</span>
</div>
<div class="tur-hoco-item">
<span>{{ overview.furniture ?? 0 }}</span>
<span>获得摆设数</span>
</div>
<div class="tur-hoco-item">
<span>{{ overview.visit ?? 0 }}</span>
<span>历史访客数</span>
</div>
</div>
<div v-if="props.homes.length > 0" class="tur-hoc-list">
<TurHomeItem
v-for="(item, idx) in props.homes"
:key="idx"
:icon="item.bg"
:name="item.name"
/>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed } from "vue";
import TurHomeItem from "./tur-home-item.vue";
type TurHomeOverviewProps = { homes: Array<TGApp.Sqlite.Record.Home> };
const props = defineProps<TurHomeOverviewProps>();
const overview = computed<TGApp.Sqlite.Record.Home>(() => props.homes[0] ?? undefined);
</script>
<style lang="scss" scoped>
@use "@styles/github.styles.scss" as github-styles;
.tur-ho-container {
position: relative;
display: flex;
width: 100%;
flex-direction: column;
align-items: center;
justify-content: center;
row-gap: 8px;
}
.tur-hoc-overview {
position: relative;
display: flex;
width: 100%;
flex-wrap: wrap;
align-items: center;
justify-content: center;
column-gap: 16px;
}
.tur-hoco-item {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
row-gap: 4px;
img {
width: 24px;
height: 24px;
}
span {
&:first-child {
color: var(--tgc-yellow-1);
font-family: var(--font-text);
text-shadow: 0 0 2px #0d1117;
}
&:last-child {
font-family: var(--font-title);
font-size: 16px;
}
}
}
.tur-hoc-list {
position: relative;
display: grid;
width: 100%;
align-items: center;
justify-content: center;
gap: 8px;
grid-template-columns: repeat(auto-fit, minmax(360px, 0.5fr));
}
</style>

View File

@@ -1,116 +0,0 @@
<template>
<div class="tur-hs-box">
<div class="bg">
<img :src="data.bg" alt="bg" />
</div>
<div class="tur-hs-top">
<div class="tur-hs-title">
<img :src="data.comfortIcon" alt="icon" />
<span>{{ data.comfortName }}</span>
</div>
<div class="tur-hs-name">{{ data.name }}</div>
</div>
<div class="tur-hs-text-grid">
<div class="tur-hs-text">
<div>{{ data.level }}</div>
<div>信任等阶</div>
</div>
<div class="tur-hs-text">
<div>{{ data.comfort }}</div>
<div>最高洞天仙力</div>
</div>
<div class="tur-hs-text">
<div>{{ data.furniture }}</div>
<div>获得摆设数</div>
</div>
<div class="tur-hs-text">
<div>{{ data.visit }}</div>
<div>历史访客数</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
defineProps<{ data: TGApp.Sqlite.Record.Home }>();
</script>
<style lang="css" scoped>
.tur-hs-box {
position: relative;
display: flex;
overflow: hidden;
width: 100%;
height: 100%;
flex-direction: column;
align-items: center;
justify-content: space-between;
border: 1px solid var(--common-shadow-1);
border-radius: 4px;
background: var(--box-bg-2);
}
.bg {
position: absolute;
z-index: 0;
right: 0;
object-fit: cover;
}
.tur-hs-top {
position: relative;
z-index: 1;
display: flex;
width: 100%;
box-sizing: border-box;
align-items: center;
justify-content: space-between;
padding: 8px;
}
.tur-hs-name {
color: var(--tgc-white-1);
font-family: var(--font-text);
font-size: 16px;
text-shadow: 0 0 4px var(--tgc-yellow-1);
}
.tur-hs-title {
display: flex;
align-items: center;
color: var(--tgc-white-1);
font-family: var(--font-title);
font-size: 18px;
text-shadow: 0 0 4px var(--tgc-yellow-1);
}
.tur-hs-title img {
width: 24px;
height: 24px;
margin-right: 4px;
}
.tur-hs-text-grid {
position: relative;
z-index: 1;
display: flex;
width: 100%;
justify-content: space-between;
padding: 8px;
-webkit-backdrop-filter: blur(5px);
backdrop-filter: blur(5px);
background: #00000066;
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
color: var(--tgc-white-1);
text-align: center;
}
.tur-hs-text :nth-child(1) {
color: var(--tgc-yellow-1);
font-family: var(--font-text);
}
.tur-hs-text :nth-child(2) {
font-family: var(--font-title);
font-size: 16px;
}
</style>

View File

@@ -36,14 +36,17 @@ const props = defineProps<TAOProps>();
.tur-os-label {
position: absolute;
z-index: 0;
right: 2px;
bottom: 0;
bottom: -2px;
color: var(--box-text-4);
font-family: var(--font-text);
font-size: 12px;
font-size: 10px;
}
.tur-os-text {
position: relative;
z-index: 1;
display: flex;
align-items: center;
justify-content: center;

View File

@@ -226,7 +226,7 @@ async function tryAuto(skip: boolean = false): Promise<void> {
}
viewCnt++;
if (likeCnt < 5) {
const isLike = detailResp.self_operation.upvote_type === 1;
const isLike = (detailResp.self_operation?.upvote_type ?? 0) > 0;
if (isLike) {
await TGLogger.Script(`[米游币任务]帖子${post.post.post_id}已点赞,跳过`);
continue;

View File

@@ -271,6 +271,12 @@ async function trySign(
const signResp = await lunaReq.sign.oper(item.account, cookie, challenge);
console.log("签到信息", item, signResp);
if (challenge !== undefined) challenge = undefined;
if (typeof signResp !== "object") {
await TGLogger.Script(
`[签到任务]${item.info.title}-${item.account.regionName}-${item.account.gameUid} ${signResp}`,
);
break;
}
if ("retcode" in signResp) {
if (signResp.retcode === 1034) {
if (skip) {

View File

@@ -1,8 +1,10 @@
<!-- 头像组件 -->
<template>
<div class="tp-avatar-box">
<div class="tpa-img">
<div class="tpa-icon">
<TMiImg :ori="true" :src="getUserAvatar(props.data)" alt="avatar" />
<img v-if="avatarUrl.endsWith('.gif')" :src="avatarUrl" alt="avatar" />
<TMiImg v-else :ori="true" :src="avatarUrl" alt="avatar" />
</div>
<div v-if="props.data.pendant !== ''" class="tpa-pendant">
<TMiImg :ori="true" :src="props.data.pendant" alt="pendant" />
@@ -32,11 +34,11 @@ type TpAvatarProps = { data: TGApp.BBS.Post.User; position: "left" | "right" };
const props = defineProps<TpAvatarProps>();
const avatarUrl = computed<string>(() => getUserAvatar(props.data));
const authorDesc = computed<string>(() => {
if (props.data.certification.label !== "") return props.data.certification.label;
return props.data.introduce;
});
const levelColor = computed<string>(() => {
if (!props.data.level_exp) return "var(--tgc-od-white)";
const level = props.data.level_exp.level;
@@ -47,7 +49,7 @@ const levelColor = computed<string>(() => {
return "var(--tgc-od-white)";
});
</script>
<style lang="css" scoped>
<style lang="scss" scoped>
.tp-avatar-box {
display: flex;
overflow: hidden;
@@ -56,6 +58,7 @@ const levelColor = computed<string>(() => {
flex-direction: v-bind("props.position === 'left' ? 'row' : 'row-reverse'");
align-items: center;
justify-content: v-bind("props.position === 'left' ? 'flex-start' : 'flex-end'");
cursor: pointer;
}
.tpa-text {

View File

@@ -8,9 +8,7 @@
@click="handleEmoticonClick"
@error="handleEmoticonError"
/>
<div v-if="props.data.insert.custom_emoticon.size.width > 100" class="tp-emo-info">
自定义表情
</div>
<div v-if="showLabel" class="tp-emo-info">自定义表情</div>
</div>
<div v-else :title="props.data.insert.custom_emoticon.url" class="tp-image-load">
<v-progress-circular :indeterminate="true" color="blue" size="small" />
@@ -86,6 +84,11 @@ const image = computed<TpImage>(() => ({
size: props.data.insert.custom_emoticon.size.file_size,
},
}));
const showLabel = computed<boolean>(() => {
if (props.data.insert.custom_emoticon.size.width > 100) return true;
if (!emoticonEl.value) return false;
return emoticonEl.value.clientWidth > 100;
});
console.log("tp-emoticon", props.data.insert.custom_emoticon);

View File

@@ -87,6 +87,7 @@ async function switchCollect(): Promise<void> {
@include github-styles.github-card;
position: fixed;
z-index: 1;
top: 64px;
right: 16px;
display: flex;
@@ -97,6 +98,7 @@ async function switchCollect(): Promise<void> {
justify-content: center;
border-radius: 50%;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
&.active {
background: var(--tgc-btn-1);
@@ -107,6 +109,15 @@ async function switchCollect(): Promise<void> {
&:hover:not(.active) {
background: var(--common-shadow-1);
}
&:hover {
box-shadow: 2px 4px 12px var(--common-shadow-4);
transform: scale(1.15);
}
&:active {
transform: scale(0.95);
}
}
.dark .tbc-box {

View File

@@ -211,6 +211,7 @@ async function handleDebug(): Promise<void> {
<style lang="scss" scoped>
.tpr-main-box {
position: fixed;
z-index: 1;
bottom: 16px;
left: 16px;
display: flex;
@@ -224,6 +225,16 @@ async function handleDebug(): Promise<void> {
box-shadow: 1px 3px 6px var(--common-shadow-2);
color: var(--btn-text);
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.tpr-main-box:hover {
box-shadow: 2px 4px 12px var(--common-shadow-4);
transform: scale(1.15);
}
.tpr-main-box:active {
transform: scale(0.95);
}
.dark .tpr-main-box {

View File

@@ -2,20 +2,21 @@
<TOverlay v-model="visible" blur-val="5px">
<div class="tpoc-box">
<div class="tpoc-top">
<span>{{ props.collection.collection_title }}</span>
<span @click="toOuterCollect()">{{ props.collection.collection_title }}</span>
<span>合集ID{{ props.collection.collection_id }}</span>
</div>
<div class="tpoc-list" ref="postListRef">
<div class="tpoc-load" v-if="postList.length === 0">
<v-progress-circular indeterminate color="blue" size="24" />
<div ref="postListRef" class="tpoc-list">
<div v-if="postList.length === 0" class="tpoc-load">
<v-progress-circular color="blue" indeterminate size="24" />
<span>加载中...</span>
</div>
<TPostcard
class="tpoc-item"
v-for="(item, index) in postList"
v-for="(post, index) in postList"
:key="index"
:model-value="item"
:class="{ selected: index === props.collection.cur - 1 }"
:post
@onUserClick="toUserProfile"
class="tpoc-item"
/>
</div>
</div>
@@ -26,6 +27,7 @@ import TOverlay from "@comp/app/t-overlay.vue";
import TPostcard from "@comp/app/t-postcard.vue";
import bbsReq from "@req/bbsReq.js";
import postReq from "@req/postReq.js";
import { openUrl } from "@tauri-apps/plugin-opener";
import { nextTick, onMounted, shallowRef, useTemplateRef, watch } from "vue";
type TpoCollectionProps = { collection: TGApp.BBS.Post.Collection; gid: number };
@@ -64,6 +66,17 @@ async function refreshInfo(): Promise<void> {
async function refreshPosts(): Promise<void> {
postList.value = await postReq.collection(props.collection.collection_id);
}
async function toOuterCollect(): Promise<void> {
await openUrl(`https://www.miyoushe.com/ys/collection/${props.collection.collection_id}`);
}
async function toUserProfile(user: TGApp.BBS.Post.User, gid: number): Promise<void> {
// TODO: 专门的个人页面
console.log(user, gid);
const profileUrl = `https://www.miyoushe.com/ys/accountCenter/postList?id=${user.uid}`;
await openUrl(profileUrl);
}
</script>
<style lang="scss" scoped>
.tpoc-box {
@@ -73,21 +86,23 @@ async function refreshPosts(): Promise<void> {
}
.tpoc-top {
position: relative;
display: flex;
flex-direction: column;
border-bottom: 1px solid var(--common-shadow-2);
margin-bottom: 10px;
}
.tpoc-top :nth-child(1) {
color: var(--common-text-title);
font-family: var(--font-title);
font-size: 20px;
}
:first-child {
color: var(--common-text-title);
cursor: pointer;
font-family: var(--font-title);
font-size: 20px;
}
.tpoc-top :nth-child(2) {
font-size: 14px;
opacity: 0.8;
:last-child {
font-size: 14px;
opacity: 0.8;
}
}
.tpoc-list {

View File

@@ -3,32 +3,34 @@
<div class="vp-of-box">
<div class="vo-of-top">
<div class="vo-oft-left">
<img src="/platforms/mhy/mys.webp" alt="icon" />
<img alt="icon" src="/platforms/mhy/mys.webp" />
<span>关注动态</span>
</div>
<div class="vo-oft-right">已加载{{ posts.length }}</div>
</div>
<div class="vo-of-actions">
<v-btn class="vo-of-btn" @click="loadMore(true)" :loading="loading">刷新</v-btn>
<v-btn :loading="loading" class="vo-of-btn" @click="loadMore(true)">刷新</v-btn>
</div>
<div class="vp-of-list" ref="listRef">
<div ref="listRef" class="vp-of-list">
<TPostcard
v-for="post in posts"
:key="post.post.post_id"
:post
class="vp-of-list-item"
v-for="(item, index) in posts"
:key="index"
:model-value="item"
@onUserClick="toUserProfile"
/>
</div>
</div>
</TOverlay>
</template>
<script setup lang="ts">
<script lang="ts" setup>
import TOverlay from "@comp/app/t-overlay.vue";
import TPostcard from "@comp/app/t-postcard.vue";
import showSnackbar from "@comp/func/snackbar.js";
import { useBoxReachBottom } from "@hooks/reachBottom.js";
import painterReq from "@req/painterReq.js";
import useUserStore from "@store/user.js";
import { openUrl } from "@tauri-apps/plugin-opener";
import { storeToRefs } from "pinia";
import { ref, shallowRef, useTemplateRef, watch } from "vue";
@@ -87,6 +89,13 @@ async function loadMore(refresh: boolean = false): Promise<void> {
listEl.value.scrollTo({ top: 0, behavior: "smooth" });
}
}
async function toUserProfile(user: TGApp.BBS.Post.User, gid: number): Promise<void> {
// TODO: 专门的个人页面
console.log(user, gid);
const profileUrl = `https://www.miyoushe.com/ys/accountCenter/postList?id=${user.uid}`;
await openUrl(profileUrl);
}
</script>
<style lang="scss" scoped>
.vp-of-box {

View File

@@ -8,52 +8,55 @@
<div class="tpoi-bottom">
<template v-if="typeof props.image.insert.image !== 'string'">
<div class="tpoi-info">
<p class="tpoi-info-item">
<span class="tpoi-info-item">
<span>大小</span>
<span>{{ bytesToSize(Number(props.image.insert.image.size) ?? 0) }}</span>
</p>
<p class="tpoi-info-item">
</span>
<span class="tpoi-info-item">
<span>尺寸</span>
<span>
{{ props.image.insert.image.width }}x{{ props.image.insert.image.height }}
</span>
</p>
<p class="tpoi-info-item">
</span>
<span class="tpoi-info-item">
<span>格式</span>
<span>{{ format }}</span>
</p>
</span>
</div>
</template>
<template v-else-if="props.image.attributes">
<div class="tpoi-info">
<p v-if="props.image.attributes.size" class="tpoi-info-item">
<span v-if="props.image.attributes.size" class="tpoi-info-item">
<span>大小</span>
<span>{{ bytesToSize(props.image.attributes.size ?? 0) }}</span>
</p>
<p class="tpoi-info-item">
</span>
<span
v-if="props.image.attributes?.width && props.image.attributes?.height"
class="tpoi-info-item"
>
<span>尺寸</span>
<span>{{ props.image.attributes.width }}x{{ props.image.attributes.height }}</span>
</p>
<p class="tpoi-info-item">
</span>
<span class="tpoi-info-item">
<span>格式</span>
<span>{{ format }}</span>
</p>
</span>
</div>
</template>
<div class="tpoi-tools">
<v-icon @click="setBlackBg" title="切换背景色" v-if="showOri">
<v-icon v-if="showOri" title="切换背景色" @click="setBlackBg">
mdi-format-color-fill
</v-icon>
<v-icon @click="showOri = true" title="查看原图" v-else>mdi-magnify</v-icon>
<v-icon @click="onCopy" title="复制到剪贴板" v-if="showCopy">mdi-content-copy</v-icon>
<v-icon @click="onDownload" title="下载到本地">mdi-download</v-icon>
<v-icon @click="visible = false" title="关闭浮窗">mdi-close</v-icon>
<v-icon v-else title="查看原图" @click="showOri = true">mdi-magnify</v-icon>
<v-icon v-if="showCopy" title="复制到剪贴板" @click="onCopy">mdi-content-copy</v-icon>
<v-icon title="下载到本地" @click="onDownload">mdi-download</v-icon>
<v-icon title="关闭浮窗" @click="visible = false">mdi-close</v-icon>
</div>
</div>
</div>
</TOverlay>
</template>
<script setup lang="ts">
<script lang="ts" setup>
import TOverlay from "@comp/app/t-overlay.vue";
import showLoading from "@comp/func/loading.js";
import showSnackbar from "@comp/func/snackbar.js";

View File

@@ -6,7 +6,7 @@
<span>{{ timeStatus }}</span>
</div>
<div class="tpol-info">
<TpAvatar :data="card.creator" position="left" />
<TpAvatar @click="toUserProfile(card.creator.uid)" :data="card.creator" position="left" />
<div>参与方式{{ upWay }}</div>
<div>奖品详情</div>
<div v-for="reward in card.rewards" :key="reward.name" class="tpol-info-reward">
@@ -41,6 +41,7 @@ import showSnackbar from "@comp/func/snackbar.js";
import TpAvatar from "@comp/viewPost/tp-avatar.vue";
import painterReq from "@req/painterReq.js";
import { emit } from "@tauri-apps/api/event";
import { openUrl } from "@tauri-apps/plugin-opener";
import { generateShareImg } from "@utils/TGShare.js";
import { stamp2LastTime } from "@utils/toolFunc.js";
import { onUnmounted, ref, shallowRef, watch } from "vue";
@@ -155,6 +156,12 @@ async function shareLottery(): Promise<void> {
await generateShareImg(fileName, shareDom, 2, true);
}
async function toUserProfile(uid: string): Promise<void> {
// TODO: 专门的个人页面
const profileUrl = `https://www.miyoushe.com/ys/accountCenter/postList?id=${uid}`;
await openUrl(profileUrl);
}
onUnmounted(() => {
if (timer !== undefined) {
clearInterval(timer);

View File

@@ -24,9 +24,10 @@
<div class="tops-divider" />
<div ref="listRef" class="tops-list">
<TPostCard
v-for="item in results"
:key="item.post.post_id"
:model-value="item"
v-for="post in results"
:key="post.post.post_id"
:post
@onUserClick="toUserProfile"
class="tops-item"
/>
</div>
@@ -40,6 +41,7 @@ import showSnackbar from "@comp/func/snackbar.js";
import { useBoxReachBottom } from "@hooks/reachBottom.js";
import postReq from "@req/postReq.js";
import useBBSStore from "@store/bbs.js";
import { openUrl } from "@tauri-apps/plugin-opener";
import { storeToRefs } from "pinia";
import { computed, onMounted, ref, shallowRef, useTemplateRef, watch } from "vue";
@@ -111,7 +113,7 @@ watch(
search.value = props.keyword;
return;
}
if (search.value !== props.keyword && props.keyword !== "") {
if (search.value !== props.keyword && props.keyword !== "" && props.keyword !== null) {
search.value = props.keyword;
results.value = [];
lastId.value = "";
@@ -160,6 +162,13 @@ async function searchPosts(): Promise<void> {
if (!visible.value) visible.value = true;
showSnackbar.success(`成功加载${res.posts.length}条数据`);
}
async function toUserProfile(user: TGApp.BBS.Post.User, gid: number): Promise<void> {
// TODO: 专门的个人页面
console.log(user, gid);
const profileUrl = `https://www.miyoushe.com/ys/accountCenter/postList?id=${user.uid}`;
await openUrl(profileUrl);
}
</script>
<style lang="css" scoped>
.tops-box {

View File

@@ -1,14 +1,15 @@
<template>
<TOverlay v-model="visible">
<div class="vp-ou-box">
<div class="vp-ou-user" v-if="userInfo">
<div v-if="userInfo" class="vp-ou-user" @click="toUserProfile()">
<div class="vp-ouu-info">
<div class="left">
<div class="avatar">
<TMiImg :src="getUserAvatar(userInfo)" alt="avatar" :ori="true" />
<img v-if="avatarUrl.endsWith('.gif')" :src="avatarUrl" alt="avatar" />
<TMiImg v-else :ori="true" :src="avatarUrl" alt="avatar" />
</div>
<div class="pendant" v-if="userInfo.pendant !== ''">
<TMiImg :src="userInfo.pendant" alt="pendant" :ori="true" />
<div v-if="userInfo.pendant !== ''" class="pendant">
<TMiImg :ori="true" :src="userInfo.pendant" alt="pendant" />
</div>
</div>
<div class="right">
@@ -16,29 +17,30 @@
<div class="nickname">{{ userInfo.nickname }}</div>
<div class="level">Lv.{{ userInfo.level_exp.level }}</div>
</div>
<div class="desc" :title="userInfo.introduce">{{ userInfo.introduce }}</div>
<div :title="userInfo.introduce" class="desc">{{ userInfo.introduce }}</div>
</div>
</div>
</div>
<div class="vp-ou-mid">
<div class="vp-ouu-extra" v-if="userInfo">
<div v-if="userInfo" class="vp-ouu-extra">
<span>ID:{{ userInfo.uid }}</span>
<span>IP:{{ userInfo.ip_region }}</span>
</div>
</div>
<div class="vp-ou-divider" />
<div class="vp-ou-list" ref="listRef">
<div ref="listRef" class="vp-ou-list">
<TPostCard
@onUserClick="toUserProfile()"
v-for="post in results"
:key="post.post.post_id"
:post
class="vp-ou-item"
:model-value="item"
v-for="item in results"
:key="item.post.post_id"
/>
</div>
</div>
</TOverlay>
</template>
<script setup lang="ts">
<script lang="ts" setup>
import TMiImg from "@comp/app/t-mi-img.vue";
import TOverlay from "@comp/app/t-overlay.vue";
import TPostCard from "@comp/app/t-postcard.vue";
@@ -46,6 +48,7 @@ import showSnackbar from "@comp/func/snackbar.js";
import { useBoxReachBottom } from "@hooks/reachBottom.js";
import bbsReq from "@req/bbsReq.js";
import postReq from "@req/postReq.js";
import { openUrl } from "@tauri-apps/plugin-opener";
import { getUserAvatar } from "@utils/toolFunc.js";
import { computed, ref, shallowRef, useTemplateRef, watch } from "vue";
@@ -61,6 +64,10 @@ const isLast = ref<boolean>(false);
const load = ref<boolean>(false);
const userInfo = shallowRef<TGApp.BBS.User.Info>();
const results = shallowRef<Array<TGApp.BBS.Post.FullData>>([]);
const avatarUrl = computed<string>(() => {
if (!userInfo.value) return "";
return getUserAvatar(userInfo.value);
});
const levelColor = computed<string>(() => {
if (!userInfo.value) return "var(--tgc-od-white)";
const level = userInfo.value.level_exp.level;
@@ -107,6 +114,12 @@ async function loadUser(): Promise<void> {
userInfo.value = resp;
}
async function toUserProfile(): Promise<void> {
// TODO: 专门的个人页面
const profileUrl = `https://www.miyoushe.com/ys/accountCenter/postList?id=${props.uid}`;
await openUrl(profileUrl);
}
async function loadPosts(): Promise<void> {
if (load.value) return;
load.value = true;
@@ -152,6 +165,7 @@ async function loadPosts(): Promise<void> {
flex-direction: column;
align-items: flex-start;
justify-content: center;
cursor: pointer;
row-gap: 4px;
}

View File

@@ -1,22 +1,23 @@
<template>
<div class="tpr-reply-box" :id="replyId">
<div ref="VpReplyRef" class="tpr-reply-box">
<div
class="tpr-bubble"
v-if="props.modelValue.user.reply_bubble !== null"
:title="props.modelValue.user.reply_bubble.name"
class="tpr-bubble"
>
<TMiImg :ori="true" :src="props.modelValue.user.reply_bubble.url" alt="bubble" />
</div>
<div class="tpr-user" @click="handleUser()">
<div class="tpru-left">
<div class="avatar">
<TMiImg :ori="true" :src="getUserAvatar(props.modelValue.user)" alt="avatar" />
<img v-if="avatarUrl.endsWith('.gif')" :src="avatarUrl" alt="avatar" />
<TMiImg v-else :ori="true" :src="avatarUrl" alt="avatar" />
</div>
<div class="pendant" v-if="props.modelValue.user.pendant !== ''">
<div v-if="props.modelValue.user.pendant !== ''" class="pendant">
<TMiImg :ori="true" :src="props.modelValue.user.pendant" alt="pendant" />
</div>
</div>
<div class="tpru-right" :title="props.modelValue.user.nickname">
<div :title="props.modelValue.user.nickname" class="tpru-right">
<span>{{ props.modelValue.user.nickname }}</span>
<span class="level">Lv.{{ props.modelValue.user.level_exp.level }}</span>
<span v-if="props.modelValue.is_lz" class="tpru-lz">楼主</span>
@@ -33,7 +34,7 @@
<span>{{ props.modelValue.user.ip_region }}</span>
</div>
<div class="tpri-right">
<span title="点赞数" class="tpr-like">
<span class="tpr-like" title="点赞数">
<v-icon size="small">mdi-thumb-up</v-icon>
{{ props.modelValue.stat.like_num }}
</span>
@@ -46,16 +47,16 @@
<v-icon size="small">mdi-message-text</v-icon>
<span>{{ props.modelValue.sub_reply_count }}</span>
<v-menu
submenu
v-model="showSub"
:close-on-content-click="false"
activator="parent"
location="end"
:close-on-content-click="false"
v-model="showSub"
submenu
>
<v-list
class="tpr-reply-sub"
width="300px"
max-height="400px"
width="300px"
@scroll="handleSubScroll"
>
<VpReplyItem
@@ -68,25 +69,25 @@
<v-chip color="blue" label>没有更多了</v-chip>
</div>
<div v-else class="tpr-list-item">
<v-btn @click="loadSub()" color="blue" :loading="loading">加载更多</v-btn>
<v-btn :loading="loading" color="blue" @click="loadSub()">加载更多</v-btn>
</div>
</v-list>
</v-menu>
</span>
</div>
</div>
<div class="tpr-extra" :title="`ID:${props.modelValue.reply.reply_id}`">
<div class="tpr-share" @click="share" data-html2canvas-ignore title="分享">
<div :title="`ID:${props.modelValue.reply.reply_id}`" class="tpr-extra">
<div class="tpr-share" data-html2canvas-ignore title="分享" @click="share">
<v-icon size="small">mdi-share-variant</v-icon>
</div>
<span
class="tpr-pin"
v-if="props.mode === 'main' && props.modelValue.reply.reply_id === props.pinId"
class="tpr-pin"
>
<v-icon size="small">mdi-pin</v-icon>
<span>置顶评论</span>
</span>
<span class="tpr-debug" @click="exportData" data-html2canvas-ignore title="导出数据">
<span class="tpr-debug" data-html2canvas-ignore title="导出数据" @click="exportData">
<v-icon size="small">mdi-file-export</v-icon>
</span>
<span v-if="props.modelValue.r_user" class="tpr-reply-user">
@@ -115,7 +116,16 @@ import { save } from "@tauri-apps/plugin-dialog";
import { writeTextFile } from "@tauri-apps/plugin-fs";
import { generateShareImg } from "@utils/TGShare.js";
import { getNearTime, getUserAvatar, timestampToDate } from "@utils/toolFunc.js";
import { computed, onMounted, onUnmounted, ref, shallowRef, toRaw, watch } from "vue";
import {
computed,
onMounted,
onUnmounted,
ref,
shallowRef,
toRaw,
useTemplateRef,
watch,
} from "vue";
import TpParser from "./tp-parser.vue";
@@ -124,18 +134,21 @@ type TprReplyProps =
| { mode: "main"; modelValue: TGApp.BBS.Reply.ReplyFull; pinId?: string };
const props = defineProps<TprReplyProps>();
const replyId = `reply_${props.modelValue.reply.post_id}_${props.modelValue.reply.floor_id}_${props.modelValue.reply.reply_id}`;
const replyLabel = `reply_${props.modelValue.reply.post_id}_${props.modelValue.reply.floor_id}_${props.modelValue.reply.reply_id}`;
let subListener: UnlistenFn | null = null;
let closeSubListener: UnlistenFn | null = null;
console.log("TprReply", toRaw(props.modelValue));
const existingIds = new Set<string>();
const showSub = ref<boolean>(false);
const lastId = ref<string>();
const isLast = ref<boolean>(false);
const loading = ref<boolean>(false);
const subReplies = shallowRef<Array<TGApp.BBS.Reply.ReplyFull>>([]);
const existingIds = new Set<string>();
const vpReplyEl = useTemplateRef<HTMLDivElement>("VpReplyRef");
const avatarUrl = computed<string>(() => getUserAvatar(props.modelValue.user));
const levelColor = computed<string>(() => {
const level = props.modelValue.user.level_exp.level;
if (level < 5) return "var(--tgc-od-green)";
@@ -196,9 +209,11 @@ function handleSubScroll(e: globalThis.Event): void {
}
async function share(): Promise<void> {
const replyDom = document.querySelector<HTMLElement>(`#${replyId}`);
if (replyDom === null) return;
await generateShareImg(replyId, replyDom, 3);
if (!vpReplyEl.value) {
showSnackbar.warn("未找到分享Dom");
return;
}
await generateShareImg(replyLabel, vpReplyEl.value, 3);
}
async function showReply(): Promise<void> {
@@ -252,7 +267,7 @@ async function exportData(): Promise<void> {
const savePath = await save({
title: "导出回复数据",
filters: [{ name: "JSON", extensions: ["json"] }],
defaultPath: `${await path.downloadDir()}${path.sep()}${replyId}.json`,
defaultPath: `${await path.downloadDir()}${path.sep()}${replyLabel}.json`,
});
if (savePath === null) {
showSnackbar.cancel("已取消");
@@ -280,6 +295,7 @@ async function handleUser(): Promise<void> {
border: 1px solid var(--common-shadow-1);
border-radius: 4px;
background: var(--box-bg-1);
color: var(--app-page-content);
word-break: break-all;
}

View File

@@ -3226,5 +3226,41 @@
12402, 12401, 13407, 13401, 14410, 14409, 14403, 14402, 14401, 15412, 15410, 15405, 15403,
15402, 15401
]
},
{
"name": "虚星临渡",
"version": "6.4",
"order": 2,
"banner": "https://sdk-webstatic.mihoyo.com/upload/ann/2026/03/03/4c5b2ae5acc0c4a37e0ac6e5000eda82_7442422502466078931_transformed.jpg",
"from": "2026-03-17T18:00:00+08:00",
"to": "2026-04-07T14:59:00+08:00",
"type": 301,
"postId": "73885913",
"up5List": [10000114],
"up4List": [10000115, 10000072, 10000088]
},
{
"name": "莓色香颂",
"version": "6.4",
"order": 2,
"banner": "https://sdk-webstatic.mihoyo.com/upload/ann/2026/03/03/31193e9b9ece7627bf7accda42b54956_6780122588782148962_transformed.jpg",
"from": "2026-03-17T18:00:00+08:00",
"to": "2026-04-07T14:59:00+08:00",
"type": 400,
"postId": "73885912",
"up5List": [10000112],
"up4List": [10000115, 10000072, 10000088]
},
{
"name": "神铸赋形",
"version": "6.4",
"order": 2,
"banner": "https://sdk-webstatic.mihoyo.com/upload/ann/2026/03/03/0c705f5ed9c06fb91b8f495d9f7f1c0c_1780288503577900005_transformed.jpg",
"from": "2026-03-17T18:00:00+08:00",
"to": "2026-04-07T14:59:00+08:00",
"type": 302,
"postId": "73885911",
"up5List": [11517, 13514],
"up4List": [11402, 12402, 13407, 14401, 15405]
}
]

View File

@@ -94,12 +94,7 @@
<div class="uaw-o-box">
<TuaOverview :val-text="item.totalBattleTimes" title="战斗次数" />
<TuaOverview :val-text="item.totalStar" title="获得渊星" />
<TuaOverview
:val-text="
item.skippedFloor !== '' ? `${item.maxFloor}(${item.skippedFloor})` : item.maxFloor
"
title="最深抵达"
/>
<TuaOverview :val-text="getMaxFloor(item)" title="最深抵达" />
<TuaOverview :val-icons="item.defeatRank" title="最多击破" />
<TuaOverview :val-icons="item.takeDamageRank" title="最多承伤" />
<TuaOverview :val-icons="item.damageRank" title="最强一击" />
@@ -142,6 +137,7 @@ import useUserStore from "@store/user.js";
import { getVersion } from "@tauri-apps/api/app";
import { open } from "@tauri-apps/plugin-dialog";
import { readTextFile } from "@tauri-apps/plugin-fs";
import { openUrl } from "@tauri-apps/plugin-opener";
import TGLogger from "@utils/TGLogger.js";
import { generateShareImg } from "@utils/TGShare.js";
import { storeToRefs } from "pinia";
@@ -175,6 +171,13 @@ watch(
async () => await reloadUid(),
);
function getMaxFloor(abyss: TGApp.Sqlite.Abyss.TableTrans): string {
if (abyss.skippedFloor !== null && abyss.skippedFloor !== "") {
return `${abyss.maxFloor}(${abyss.skippedFloor})`;
}
return `${abyss.maxFloor}`;
}
async function reloadUid(uid?: string): Promise<void> {
uidList.value = await TSUserAbyss.getAllUid();
if (uidList.value.length === 0) uidList.value = [account.value.gameUid];
@@ -307,12 +310,21 @@ async function deleteAbyss(): Promise<void> {
await loadAbyss();
}
/**
* 尝试读取胡桃工具箱导出的深渊数据
* @since Beta v0.8.6
* @return {Promise<void>}
*/
/** 尝试读取胡桃工具箱导出的深渊数据 */
async function tryReadAbyss(): Promise<void> {
const checkF = await showDialog.checkF({
title: "确认导入外部数据?",
text: "仅适用于特定工具导出的胡桃深渊数据\n不适配本应用备份数据",
cancelLabel: "查看详细说明",
});
if (checkF === undefined) {
showSnackbar.cancel("取消导入深渊数据");
return;
}
if (!checkF) {
await openUrl("https://app.btmuli.ink/docs/TeyvatGuide/import-hutao-db.html");
return;
}
const file = await open({
multiple: false,
title: "选择胡桃工具箱导出的深渊数据文件",

View File

@@ -29,7 +29,7 @@
<div class="ucp-top-append">
<div class="act-list">
<v-btn
:disabled="localChallenge.length === 0"
:disabled="localChallenge.length === 0 || isRefresh"
class="ucp-btn"
prepend-icon="mdi-share"
variant="elevated"
@@ -38,6 +38,7 @@
分享
</v-btn>
<v-btn
:loading="isRefresh"
class="ucp-btn"
prepend-icon="mdi-refresh"
variant="elevated"
@@ -54,6 +55,7 @@
导入
</v-btn>
<v-btn
:disabled="isRefresh"
class="ucp-btn"
prepend-icon="mdi-delete"
variant="elevated"
@@ -153,6 +155,7 @@ import useUserStore from "@store/user.js";
import { getVersion } from "@tauri-apps/api/app";
import { open } from "@tauri-apps/plugin-dialog";
import { readTextFile } from "@tauri-apps/plugin-fs";
import { openUrl } from "@tauri-apps/plugin-opener";
import TGLogger from "@utils/TGLogger.js";
import { generateShareImg } from "@utils/TGShare.js";
import { storeToRefs } from "pinia";
@@ -171,6 +174,7 @@ const { account, cookie } = storeToRefs(useUserStore());
const version = ref<string>();
const userTab = ref<number>(0);
const isRefresh = ref<boolean>(false);
const uidCur = ref<string>();
const uidList = shallowRef<Array<string>>();
const localChallenge = shallowRef<Array<TGApp.Sqlite.Challenge.TableTrans>>([]);
@@ -185,6 +189,7 @@ onMounted(async () => {
await TGLogger.Info("[UserCombat][onMounted] 打开幽境危战页面");
await showLoading.update("正在获取UID列表");
await reloadUid();
isRefresh.value = false;
if (uidCur.value?.startsWith("5")) server.value = gameEnum.server.CN_QD01;
await refreshPopList(false);
});
@@ -282,15 +287,18 @@ async function refreshChallenge(): Promise<void> {
}
await TGLogger.Info("[Challenge][refreshChallenge] 开始刷新挑战数据");
await showLoading.start(`正在获取${rfAccount.gameUid}的幽境危战数据`);
isRefresh.value = true;
const resp = await recordReq.challenge.detail(rfCk!, rfAccount);
console.log(resp);
if ("retcode" in resp) {
isRefresh.value = false;
await showLoading.end();
showSnackbar.error(`[${resp.retcode}] ${resp.message}`);
await TGLogger.Error(`[Challenge][refreshChallenge] ${resp.retcode} - ${resp.message}`);
return;
}
if (!resp.is_unlock) {
isRefresh.value = false;
await showLoading.end();
showSnackbar.warn("幽境危战未解锁");
await TGLogger.Warn("[Challenge][refreshChallenge] 幽境危战未解锁");
@@ -304,6 +312,7 @@ async function refreshChallenge(): Promise<void> {
}
await reloadUid(uidCur.value);
await loadChallenge();
isRefresh.value = false;
await showLoading.end();
}
@@ -353,12 +362,21 @@ async function refreshPopList(hint: boolean = true): Promise<void> {
);
}
/**
* 尝试读取胡桃工具箱导出的危战数据
* @since Beta v0.8.6
* @return {Promise<void>}
*/
/** 尝试读取胡桃工具箱导出的危战数据 */
async function tryReadChallenge(): Promise<void> {
const checkF = await showDialog.checkF({
title: "确认导入外部数据?",
text: "仅适用于特定工具导出的胡桃危战数据\n不适配本应用备份数据",
cancelLabel: "查看详细说明",
});
if (checkF === undefined) {
showSnackbar.cancel("取消导入危战数据");
return;
}
if (!checkF) {
await openUrl("https://app.btmuli.ink/docs/TeyvatGuide/import-hutao-db.html");
return;
}
const file = await open({
multiple: false,
title: "选择胡桃工具箱导出的危战数据文件",

View File

@@ -101,6 +101,13 @@
</template>
</v-app-bar>
<div class="uc-box">
<div class="uc-box-info">
<span>角色详情</span>
<span>|</span>
<span>TeyvatGuide v{{ version }}</span>
<span>|</span>
<span>更新于 {{ getUpdateTime() }}</span>
</div>
<div class="uc-box-top">
<div class="uc-box-title">
<TurRoleInfo v-if="roleRecord && uidCur" :role="roleRecord" :uid="uidCur" />
@@ -119,15 +126,9 @@
<span v-else>{{ item.cnt }}</span>
</span>
</div>
<div class="uc-box-info">
<span>角色详情</span>
<span>|</span>
<span>TeyvatGuide v{{ version }}</span>
<span>|</span>
<span>更新于 {{ getUpdateTime() }}</span>
</div>
<!-- TODO: 渲染筛选条件 -->
<TuaSelectVals :isConstUp :isFetterUp :isLevelUp :isSelected :selectOpts />
</div>
<div class="uc-divider" />
<div v-if="!isEmpty" class="uc-grid">
<TuaAvatarBox
v-for="(role, index) in selectedList"
@@ -158,6 +159,7 @@ import showLoading from "@comp/func/loading.js";
import showSnackbar from "@comp/func/snackbar.js";
import TuaAvatarBox from "@comp/userAvatar/tua-avatar-box.vue";
import TuaDetailOverlay from "@comp/userAvatar/tua-detail-overlay.vue";
import TuaSelectVals from "@comp/userAvatar/tua-select-vals.vue";
import UavSelect, { type UavSelectModel } from "@comp/userAvatar/uav-select.vue";
import TurRoleInfo from "@comp/userRecord/tur-role-info.vue";
import recordReq from "@req/recordReq.js";
@@ -203,7 +205,9 @@ const isFetterUp = ref<boolean | null>(null);
const isConstUp = ref<boolean | null>(null);
const selectOpts = ref<UavSelectModel>({
costume: [],
fetter: [],
star: [],
level: [],
weapon: [],
area: [],
element: [],
@@ -260,12 +264,12 @@ watch(
},
);
function toggleSort(value: boolean | null): boolean {
function toggleSort(value: boolean | null): boolean | null {
switch (value) {
case true:
return false;
case false:
return true;
return null;
case null:
return true;
}
@@ -297,7 +301,15 @@ function resetList(): void {
isLevelUp.value = null;
isFetterUp.value = null;
isConstUp.value = null;
selectOpts.value = { costume: [], star: [], weapon: [], area: [], element: [] };
selectOpts.value = {
costume: [],
fetter: [],
star: [],
level: [],
weapon: [],
area: [],
element: [],
};
selectedList.value = getOrderedList(roleList.value);
showSnackbar.success("已重置筛选条件");
if (!dataVal.value) return;
@@ -544,6 +556,14 @@ function handleSelect(val: UavSelectModel): void {
const filterC = roleList.value.filter((role) => {
const info = AppCharacterData.find((i) => i.id === role.cid);
if (val.star.length > 0 && !val.star.includes(role.avatar.rarity.toString())) return false;
if (val.level.length > 0) {
if (!val.level.includes("true") && role.avatar.level >= 70) return false;
if (!val.level.includes("false") && role.avatar.level < 70) return false;
}
if (val.fetter.length > 0) {
if (!val.fetter.includes("true") && role.avatar.fetter === 10) return false;
if (!val.fetter.includes("false") && role.avatar.fetter !== 10) return false;
}
if (val.weapon.length > 0 && !val.weapon.includes(role.weapon.type_name)) return false;
if (val.element.length > 0 && !val.element.includes(getZhElement(role.avatar.element)))
return false;
@@ -645,6 +665,7 @@ function handleSwitch(next: boolean): void {
}
.uc-box {
position: relative;
display: flex;
flex-direction: column;
padding: 8px;
@@ -658,10 +679,18 @@ function handleSwitch(next: boolean): void {
position: relative;
display: flex;
width: 100%;
align-items: flex-end;
justify-content: flex-start;
padding: 8px 0;
border-bottom: 1px solid var(--common-shadow-2);
flex-direction: column;
align-items: flex-start;
justify-content: center;
row-gap: 8px;
}
.uc-divider {
position: relative;
width: 100%;
height: 1px;
border-radius: 1px;
background: var(--common-shadow-2);
}
.uc-box-title {
@@ -705,8 +734,8 @@ function handleSwitch(next: boolean): void {
.uc-box-info {
position: absolute;
z-index: -1;
right: 0;
bottom: 0;
top: 2px;
right: 4px;
display: flex;
align-items: center;
justify-content: center;

View File

@@ -164,6 +164,7 @@ import useUserStore from "@store/user.js";
import { getVersion } from "@tauri-apps/api/app";
import { open } from "@tauri-apps/plugin-dialog";
import { readTextFile } from "@tauri-apps/plugin-fs";
import { openUrl } from "@tauri-apps/plugin-opener";
import TGLogger from "@utils/TGLogger.js";
import { generateShareImg } from "@utils/TGShare.js";
import { storeToRefs } from "pinia";
@@ -480,12 +481,21 @@ async function uploadCombat(): Promise<void> {
await showLoading.end();
}
/**
* 尝试读取胡桃工具箱导出的剧诗数据
* @since Beta v0.8.6
* @returns {Promise<void>}
*/
/** 尝试读取胡桃工具箱导出的剧诗数据 */
async function tryReadCombat(): Promise<void> {
const checkF = await showDialog.checkF({
title: "确认导入外部数据?",
text: "仅适用于特定工具导出的胡桃剧诗数据\n不适配本应用备份数据",
cancelLabel: "查看详细说明",
});
if (checkF === undefined) {
showSnackbar.cancel("取消导入剧诗数据");
return;
}
if (!checkF) {
await openUrl("https://app.btmuli.ink/docs/TeyvatGuide/import-hutao-db.html");
return;
}
const file = await open({
multiple: false,
title: "选择胡桃工具箱导出的剧诗数据文件",

View File

@@ -140,7 +140,7 @@
</v-window-item>
</v-window>
</div>
<UgoUid v-model="ovShow" :fpi="ovFpi" :mode="ovMode" />
<UgoUid v-model="ovShow" :fpe="ovFpe" :fpi="ovFpi" :mode="ovMode" />
<UgoHutaoDu v-model="hutaoShow" :mode="htMode" @selected="handleHutaoDu" />
</template>
<script lang="ts" setup>
@@ -181,6 +181,7 @@ const { isLogin: isLoginHutao, accessToken, userName, userInfo } = storeToRefs(h
const ovMode = ref<"export" | "import">("import");
const ovShow = ref<boolean>(false);
const ovFpi = ref<string>();
const ovFpe = ref<string>();
const authkey = ref<string>("");
const uidCur = ref<string>();
@@ -549,7 +550,7 @@ async function refreshGachaPool(
if (force) await showLoading.update(`[${label}] 第${page}页,${gachaRes.length}`);
for (const item of gachaRes) {
if (!force) {
await showLoading.update(`[${item.item_type}][${item.time}] ${item.name}`);
await showLoading.update(`[${item.item_type}][${item.time}] ${item.name}`, { timeout: 0 });
}
const tempItem: TGApp.Plugins.UIGF.GachaItem = {
gacha_type: item.gacha_type,
@@ -654,7 +655,18 @@ async function exportUigf4(): Promise<void> {
showSnackbar.error("未获取到 UID");
return;
}
const tsNow = Math.floor(Date.now() / 1000);
const file = await save({
title: "导出祈愿数据",
filters: [{ name: "UIGF JSON", extensions: ["json"] }],
defaultPath: `${await path.downloadDir()}${path.sep()}UIGFv4.2_${tsNow}.json`,
});
if (!file) {
showSnackbar.cancel("已取消文件保存");
return;
}
await TGLogger.Info(`[UserGacha][${uidCur.value}][exportUigf4] 导出祈愿数据(v4)`);
ovFpe.value = file;
ovMode.value = "export";
ovShow.value = true;
}
@@ -684,11 +696,14 @@ async function deleteGacha(): Promise<void> {
}
}
await showLoading.start("正在删除祈愿数据", `UID:${uidCur.value}`);
const label = `已成功删除 ${uidCur.value} 的祈愿数据,即将刷新页面`;
await TSUserGacha.deleteGachaRecords(uidCur.value);
await reloadUid();
await loadGachaList();
await showLoading.end();
showSnackbar.success(`已成功删除 ${uidCur.value} 的祈愿数据,即将刷新页面`);
showSnackbar.success(label);
await new Promise<void>((resolve) => setTimeout(resolve, 1500));
window.location.reload();
}
async function checkData(): Promise<void> {

View File

@@ -84,7 +84,7 @@
</v-window-item>
</v-window>
</div>
<UgoUid v-model="ovShow" :fpi="ovFpi" :mode="ovMode" />
<UgoUid v-model="ovShow" :fpe="ovFpe" :fpi="ovFpi" :mode="ovMode" />
</template>
<script lang="ts" setup>
import showDialog from "@comp/func/dialog.js";
@@ -99,7 +99,7 @@ import TSUserAccount from "@Sqlm/userAccount.js";
import TSUserGachaB from "@Sqlm/userGachaB.js";
import useUserStore from "@store/user.js";
import { path } from "@tauri-apps/api";
import { open } from "@tauri-apps/plugin-dialog";
import { open, save } from "@tauri-apps/plugin-dialog";
import TGLogger from "@utils/TGLogger.js";
import { storeToRefs } from "pinia";
import { onMounted, ref, shallowRef, watch } from "vue";
@@ -113,6 +113,7 @@ const { account, cookie } = storeToRefs(useUserStore());
const ovMode = ref<"export" | "import">("import");
const ovShow = ref<boolean>(false);
const ovFpi = ref<string>();
const ovFpe = ref<string>();
const authkey = ref<string>("");
const uidCur = ref<string>();
@@ -315,14 +316,17 @@ async function deleteGacha(): Promise<void> {
}
}
await showLoading.start("正在删除祈愿数据", `UID:${uidCur.value}`);
const label = `已成功删除 ${uidCur.value} 的颂愿数据,即将刷新页面`;
await TSUserGachaB.deleteRecords(uidCur.value);
await TGLogger.Info(
`[UserGachaB][${uidCur.value}][deleteGacha] 成功删除 ${gachaListCur.value.length} 条祈愿数据`,
);
showSnackbar.success(`已成功删除 ${uidCur.value} 的祈愿数据,即将刷新页面`);
await reloadUid();
await loadGachaBList();
await showLoading.end();
showSnackbar.success(label);
await new Promise<void>((resolve) => setTimeout(resolve, 1500));
window.location.reload();
}
async function importUigf(): Promise<void> {
@@ -348,7 +352,18 @@ async function exportUigf(): Promise<void> {
showSnackbar.error("未获取到 UID");
return;
}
const tsNow = Math.floor(Date.now() / 1000);
const file = await save({
title: "导出祈愿数据",
filters: [{ name: "UIGF JSON", extensions: ["json"] }],
defaultPath: `${await path.downloadDir()}${path.sep()}UIGFv4.2_${tsNow}.json`,
});
if (!file) {
showSnackbar.cancel("已取消文件保存");
return;
}
await TGLogger.Info(`[UserGachaB][${uidCur.value}][exportUigf] 导出祈愿数据`);
ovFpe.value = file;
ovMode.value = "export";
ovShow.value = true;
}

View File

@@ -18,6 +18,7 @@
<template #append>
<div class="ur-top-btns">
<v-btn
:loading="isRefresh"
class="ur-top-btn"
prepend-icon="mdi-refresh"
variant="elevated"
@@ -26,7 +27,7 @@
更新
</v-btn>
<v-btn
:disabled="recordData === undefined"
:disabled="recordData === undefined || isRefresh"
class="ur-top-btn"
prepend-icon="mdi-share"
variant="elevated"
@@ -35,7 +36,7 @@
分享
</v-btn>
<v-btn
:disabled="recordData === undefined"
:disabled="recordData === undefined || isRefresh"
class="ur-top-btn"
prepend-icon="mdi-delete"
variant="elevated"
@@ -60,9 +61,8 @@
<PhCompCard title="世界探索">
<TurWorldGrid :worlds="recordData.worldExplore" />
</PhCompCard>
<!-- TODO: 优化UI -->
<PhCompCard title="尘歌壶">
<TurHomeGrid :model-value="recordData.homes" />
<TurHomeOverview :homes="recordData.homes" />
</PhCompCard>
</div>
<div v-else class="ur-empty">
@@ -76,7 +76,7 @@ import showLoading from "@comp/func/loading.js";
import showSnackbar from "@comp/func/snackbar.js";
import PhCompCard from "@comp/pageHome/ph-comp-card.vue";
import TurAvatarGrid from "@comp/userRecord/tur-avatar-grid.vue";
import TurHomeGrid from "@comp/userRecord/tur-home-grid.vue";
import TurHomeOverview from "@comp/userRecord/tur-home-overview.vue";
import TurOverviewGrid from "@comp/userRecord/tur-overview-grid.vue";
import TurRoleInfo from "@comp/userRecord/tur-role-info.vue";
import TurWorldGrid from "@comp/userRecord/tur-world-grid.vue";
@@ -92,8 +92,10 @@ import { onMounted, ref, shallowRef, watch } from "vue";
const userStore = useUserStore();
const { account, cookie } = storeToRefs(userStore);
const uidCur = ref<number>();
const version = ref<string>();
const isRefresh = ref<boolean>(false);
const uidCur = ref<number>();
const uidList = shallowRef<Array<number>>([]);
const recordData = shallowRef<TGApp.Sqlite.Record.TableTrans>();
@@ -102,6 +104,7 @@ onMounted(async () => {
await TGLogger.Info("[UserRecord][onMounted] 打开角色战绩页面");
version.value = await getVersion();
await loadUid();
isRefresh.value = false;
await showLoading.end();
});
@@ -165,6 +168,7 @@ async function refreshRecord(): Promise<void> {
}
await showLoading.start(`正在刷新${rfAccount.gameUid}的战绩数据`);
await TGLogger.Info(`[UserRecord][refresh][${rfAccount.gameUid}] 刷新战绩数据`);
isRefresh.value = true;
const resp = await recordReq.index(rfCk!, rfAccount);
console.log(resp);
if ("retcode" in resp) {
@@ -174,6 +178,7 @@ async function refreshRecord(): Promise<void> {
await TGLogger.Error(
`[UserRecord][refresh][${rfAccount.gameUid}] ${resp.retcode} ${resp.message}`,
);
isRefresh.value = false;
return;
}
await TGLogger.Info(`[UserRecord][refresh][${rfAccount.gameUid}] 获取战绩数据成功`);
@@ -184,6 +189,7 @@ async function refreshRecord(): Promise<void> {
await loadUid(rfAccount.gameUid);
await loadRecord();
await showLoading.end();
isRefresh.value = false;
showSnackbar.success(`成功刷新${rfAccount.gameUid}的战绩数据`);
}

View File

@@ -26,7 +26,12 @@
</div>
</template>
<template #item="{ props, item }">
<div class="us-select-item" v-bind="props" @click="() => (curAccount = item)">
<div
:class="{ selected: item.uid === curAccount?.uid }"
class="us-select-item"
v-bind="props"
@click="() => (curAccount = item)"
>
<img :src="item.brief.avatar" alt="icon" />
<div class="us-si-content">
<span>{{ item.brief.nickname }}</span>
@@ -35,9 +40,24 @@
</div>
</template>
</v-select>
<v-btn :loading="runAll" class="run-all-btn" variant="elevated" @click="tryExecAll()">
一键执行
</v-btn>
<template v-if="accounts.length > 1">
<v-btn :loading="runAll" class="run-all-btn" variant="elevated" @click="tryExecSingle()">
一键执行当前账号
</v-btn>
<v-btn
:loading="runAll"
class="run-all-btn"
variant="elevated"
@click="tryExecAllAccounts()"
>
一键执行全部账号
</v-btn>
</template>
<template v-else>
<v-btn :loading="runAll" class="run-all-btn" variant="elevated" @click="tryExecSingle()">
一键执行
</v-btn>
</template>
</div>
</template>
<template #append>
@@ -134,7 +154,7 @@ async function tryAutoRun(): Promise<void> {
continue;
}
curAccount.value = acFind;
await tryExecAll();
await tryExecSingle();
}
if (exitAfter.value) {
showSnackbar.success("任务执行完成,即将自动退出");
@@ -184,7 +204,7 @@ async function tryCkVerify(): Promise<void> {
else showSnackbar.success("CK验证成功");
}
async function tryExecAll(): Promise<void> {
async function tryExecSingle(): Promise<void> {
if (!curAccount.value) {
showSnackbar.warn("当前账号未选择,请先选择账号");
return;
@@ -198,6 +218,35 @@ async function tryExecAll(): Promise<void> {
await signEl.value?.tryAuto(skipGeetest.value);
runAll.value = false;
}
async function tryExecAllAccounts(): Promise<void> {
if (accounts.value.length === 0) {
showSnackbar.warn("未检测到可用账号");
return;
}
if (runScript.value || runAll.value) {
showSnackbar.warn("脚本正在执行,请稍后");
return;
}
runAll.value = true;
await TGLogger.ScriptSep(`全量执行`);
for (const account of accounts.value) {
curAccount.value = account;
await TGLogger.Script(`账号 UID:${account.uid} 执行开始`);
if (missionEl.value) await missionEl.value.tryAuto(skipGeetest.value);
if (signEl.value) await signEl.value.tryAuto(skipGeetest.value);
await TGLogger.Script(`账号 UID:${account.uid} 执行完毕`);
}
await TGLogger.ScriptSep(`全量执行`, false);
runAll.value = false;
showSnackbar.success("所有账号均已执行完毕");
}
</script>
<style lang="scss" scoped>
.us-top-title {
@@ -264,10 +313,14 @@ async function tryExecAll(): Promise<void> {
column-gap: 4px;
cursor: pointer;
&:hover {
&.selected:not(:hover) {
background: var(--common-shadow-1);
}
&:hover {
background: var(--common-shadow-2);
}
img {
width: 24px;
height: 24px;
@@ -289,7 +342,7 @@ async function tryExecAll(): Promise<void> {
}
}
.append {
.us-si-append {
display: flex;
align-items: center;
justify-content: center;
@@ -329,6 +382,10 @@ async function tryExecAll(): Promise<void> {
row-gap: 8px;
}
.run-all-btn + .run-all-btn {
margin-left: 8px;
}
.run-all-btn,
.us-test-btn {
background: var(--tgc-btn-1);

View File

@@ -32,14 +32,14 @@
<div class="twm-top-append">
<v-text-field
v-model="search"
:clearable="true"
:hide-details="true"
:single-line="true"
append-inner-icon="mdi-magnify"
density="compact"
label="搜索"
prepend-inner-icon="mdi-magnify"
variant="outlined"
@keydown.enter="searchMaterial()"
@click:prepend-inner="searchMaterial()"
@click:append-inner="searchMaterial()"
/>
</div>
</template>
@@ -137,7 +137,7 @@ function switchMaterial(isNext: boolean): void {
function searchMaterial(): void {
let selectData = getSelectMaterials();
if (search.value === undefined || search.value === "") {
if (search.value === undefined || search.value === "" || search.value === null) {
if (sortMaterialsData.value.length === selectData.length) {
showSnackbar.warn("请输入搜索内容!");
return;

View File

@@ -8,14 +8,14 @@
</div>
<v-select
v-model="selectType"
:items="namecardTypes"
item-title="type"
:hide-details="true"
:clearable="true"
width="250px"
label="名片类别"
:hide-details="true"
:items="namecardTypes"
density="compact"
item-title="type"
label="名片类别"
variant="outlined"
width="250px"
>
<template #item="{ props, item }">
<v-list-item v-bind="props">
@@ -31,33 +31,34 @@
<div class="wnc-top-append">
<v-text-field
v-model="search"
density="compact"
prepend-inner-icon="mdi-magnify"
label="搜索"
:clearable="true"
:hide-details="true"
append-inner-icon="mdi-magnify"
density="compact"
label="搜索"
variant="outlined"
@click:prepend-inner="searchNameCard()"
@click:append-inner="searchNameCard()"
@keyup.enter="searchNameCard()"
/>
</div>
</template>
</v-app-bar>
<div class="tw-nc-list">
<v-virtual-scroll class="v-scroll" :items="sortNameCardsData" :item-height="80" item-key="id">
<v-virtual-scroll :item-height="80" :items="sortNameCardsData" class="v-scroll" item-key="id">
<template #default="{ item }">
<TopNameCard class="item" :data="item" @selected="showNameCard(item)" />
<TopNameCard :data="item" class="item" @selected="showNameCard(item)" />
</template>
</v-virtual-scroll>
</div>
<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" />
<img alt="right" src="@/assets/icons/arrow-right.svg" />
</div>
</template>
<template #right>
<div class="card-arrow" @click="switchCard(true)">
<img src="@/assets/icons/arrow-right.svg" alt="right" />
<img alt="right" src="@/assets/icons/arrow-right.svg" />
</div>
</template>
</ToNameCard>
@@ -135,8 +136,9 @@ function switchCard(isNext: boolean): void {
}
function searchNameCard(): void {
if (search.value === undefined) {
if (search.value === undefined || search.value === null) {
sortData(AppNameCardsData);
showSnackbar.success("已重置");
return;
}
if (search.value === "") {

View File

@@ -22,9 +22,10 @@
<v-text-field
v-model="search"
:hide-details="true"
:single-line="true"
append-inner-icon="mdi-magnify"
@click:append-inner="isSearch = true"
variant="outlined"
:clearable="true"
density="compact"
label="搜索"
@keydown.enter="isSearch = true"

View File

@@ -21,14 +21,14 @@
<div class="pbm-nav-search">
<v-text-field
v-model="search"
:clearable="true"
:hide-details="true"
:single-line="true"
append-inner-icon="mdi-magnify"
density="compact"
label="搜索"
prepend-inner-icon="mdi-magnify"
variant="outlined"
@keydown.enter="searchMaterial()"
@click:prepend-inner="searchMaterial()"
@click:append-inner="searchMaterial()"
/>
</div>
<v-btn
@@ -299,7 +299,7 @@ function getItemInfo(id: number): TGApp.App.Material.WikiItem | false {
function searchMaterial(): void {
let selectData = getSelectMaterials();
if (search.value === undefined || search.value === "") {
if (search.value === undefined || search.value === "" || search.value === null) {
if (materialShow.value.length === selectData.length) {
showSnackbar.warn("请输入搜索内容!");
return;

View File

@@ -1,7 +1,7 @@
<!-- 首页 -->
<template>
<v-app-bar>
<template #prepend>
<div class="home-top-nav">
<div v-if="isLogin" class="home-tools">
<v-select
v-model="curGid"
@@ -15,10 +15,9 @@
>
<template #selection="{ item }">
<div class="select-item main">
<TMiImg
<img
v-if="item.icon"
:alt="item.title"
:ori="true"
:src="item.icon"
:title="item.title"
class="icon"
@@ -32,7 +31,7 @@
class="select-item sub"
v-bind="props"
>
<TMiImg
<img
v-if="item.icon"
:alt="item.title"
:src="item.icon"
@@ -43,10 +42,8 @@
</div>
</template>
</v-select>
<TGameNav :model-value="curGid" />
<TGameNav :gid="curGid" :mini="true" />
</div>
</template>
<template #append>
<div class="home-select">
<v-select
v-model="oldItems"
@@ -63,7 +60,7 @@
确定
</v-btn>
</div>
</template>
</div>
</v-app-bar>
<div class="home-container">
<component :is="item" v-for="item in components" :key="item" @success="loadEnd(item)" />
@@ -71,7 +68,7 @@
</template>
<script lang="ts" setup>
import TGameNav from "@comp/app/t-gameNav.vue";
import TMiImg from "@comp/app/t-mi-img.vue";
import showDialog from "@comp/func/dialog.js";
import showLoading from "@comp/func/loading.js";
import showSnackbar from "@comp/func/snackbar.js";
import PhCompCalendar from "@comp/pageHome/ph-comp-calendar.vue";
@@ -82,6 +79,10 @@ import TSUserAccount from "@Sqlm/userAccount.js";
import useAppStore from "@store/app.js";
import useBBSStore from "@store/bbs.js";
import useHomeStore from "@store/home.js";
import { getVersion } from "@tauri-apps/api/app";
import { invoke } from "@tauri-apps/api/core";
import { openUrl } from "@tauri-apps/plugin-opener";
import { getLatestReleaseVersion } from "@utils/Github.js";
import TGLogger from "@utils/TGLogger.js";
import { storeToRefs } from "pinia";
import { defineComponent, onMounted, ref, shallowRef, watch } from "vue";
@@ -105,7 +106,7 @@ type SelectItem = {
const homeStore = useHomeStore();
const bbsStore = useBBSStore();
const { devMode, isLogin } = storeToRefs(useAppStore());
const { devMode, isLogin, lastUcts } = storeToRefs(useAppStore());
const { gameList } = storeToRefs(bbsStore);
const curGid = ref<number>(2);
@@ -134,6 +135,7 @@ onMounted(async () => {
}
oldItems.value = showItems.value;
await loadComp();
await checkAppUpdate();
});
watch(
@@ -209,8 +211,50 @@ async function loadEnd(item: ReturnType<typeof defineComponent>): Promise<void>
else showSnackbar.warn(`${compName} 已加载`);
if (loadItems.value.length === components.value.length) await showLoading.end();
}
async function checkAppUpdate(): Promise<void> {
const nowTs = Math.floor(Date.now() / 1000);
const diffTime = nowTs - lastUcts.value;
if (diffTime < 60 * 60 * 24) return;
await TGLogger.Info("[Home][CheckAppUpdate]检测版本更新");
const versionApp = await getVersion();
const versionCheck = await getLatestReleaseVersion();
if (versionCheck === "0") return;
if (versionCheck === versionApp) {
await TGLogger.Info(`[Home][CheckAppUpdate]版本号一致:${versionCheck}`);
lastUcts.value = nowTs;
return;
}
await TGLogger.Info(`[Home][CheckAppUpdate]检测到新版本:${versionCheck}`);
const check = await showDialog.checkF({
title: "检测到新版本",
text: `${versionApp}${versionCheck}`,
otcancel: false,
confirmLabel: "前往更新",
cancelLabel: "稍后提醒",
});
lastUcts.value = nowTs;
if (!check) return;
const isMsix = await invoke<boolean>("is_msix");
if (isMsix) {
await openUrl("ms-windows-store://pdp/?ProductId=9nlbnnnbnsjn");
return;
}
await openUrl("https://github.com/BTMuli/TeyvatGuide/releases/latest");
}
</script>
<style lang="scss" scoped>
.home-top-nav {
position: relative;
display: flex;
width: 100%;
max-width: 100%;
height: 100%;
align-items: center;
justify-content: space-between;
overflow-x: auto;
}
.home-container {
position: relative;
display: flex;
@@ -218,14 +262,6 @@ async function loadEnd(item: ReturnType<typeof defineComponent>): Promise<void>
gap: 12px;
}
.home-top {
display: flex;
width: 100%;
align-items: center;
justify-content: space-between;
gap: 10px;
}
.home-tools {
display: flex;
align-items: center;
@@ -239,10 +275,12 @@ async function loadEnd(item: ReturnType<typeof defineComponent>): Promise<void>
}
.home-select {
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
justify-content: center;
margin-right: 16px;
margin-left: auto;
gap: 8px;
}

View File

@@ -103,11 +103,10 @@
</template>
</v-app-bar>
<div class="pc-posts">
<div v-for="item in curPosts" :key="item.post.post_id">
<div v-for="post in curPosts" :key="post.post.post_id">
<TPostCard
:model-value="item"
:post
:select-mode="selectedMode"
:user-click="true"
@onSelected="handleSelected"
@onUserClick="handleUserClick"
/>

View File

@@ -20,10 +20,9 @@
>
<template #selection="{ item }">
<div class="select-item main">
<TMiImg
<img
v-if="item.icon"
:alt="item.text"
:ori="true"
:src="item.icon"
:title="item.text"
class="icon"
@@ -33,7 +32,7 @@
</template>
<template #item="{ props, item }">
<div :class="{ selected: item.gid === curGid }" class="select-item sub" v-bind="props">
<TMiImg v-if="item.icon" :alt="item.text" :ori="true" :src="item.icon" class="icon" />
<img v-if="item.icon" :alt="item.text" :src="item.icon" class="icon" />
<span>{{ item.text }}</span>
</div>
</template>
@@ -49,7 +48,7 @@
>
<template #selection="{ item }">
<div class="select-item main">
<TMiImg :alt="item.text" :ori="true" :src="item.icon" :title="item.text" class="icon" />
<img :alt="item.text" :src="item.icon" :title="item.text" class="icon" />
<span>{{ item.text }}</span>
</div>
</template>
@@ -60,7 +59,7 @@
v-bind="props"
@click="() => (selectedForum = item)"
>
<TMiImg :alt="item.text" :ori="true" :src="item.icon" class="icon" />
<img :alt="item.text" :src="item.icon" class="icon" />
<span>{{ item.text }}</span>
</div>
</template>
@@ -78,12 +77,12 @@
<v-text-field
v-model="search"
:hide-details="true"
:single-line="true"
:clearable="true"
append-inner-icon="mdi-magnify"
class="post-switch-item"
label="请输入帖子 ID 或搜索词"
variant="outlined"
@click:append="searchPost"
@click:append-inner="searchPost"
@keyup.enter="searchPost"
/>
<v-btn
@@ -98,12 +97,12 @@
</v-btn>
</div>
<template #extension>
<TGameNav :model-value="curGid" style="margin-left: 8px" />
<TGameNav :mini="false" :gid="curGid" style="margin-left: 8px" />
</template>
</v-app-bar>
<div class="posts-grid">
<div v-for="post in posts" :key="post.post.post_id">
<TPostCard :model-value="post" :user-click="true" @onUserClick="handleUserClick" />
<TPostCard :post @onUserClick="handleUserClick" />
</div>
</div>
<VpOverlaySearch v-model="showSearch" :gid="curGid" :keyword="search" />
@@ -111,7 +110,6 @@
</template>
<script lang="ts" setup>
import TGameNav from "@comp/app/t-gameNav.vue";
import TMiImg from "@comp/app/t-mi-img.vue";
import TPostCard from "@comp/app/t-postcard.vue";
import showLoading from "@comp/func/loading.js";
import showSnackbar from "@comp/func/snackbar.js";

View File

@@ -1,8 +1,8 @@
<!-- 咨讯页面 -->
<template>
<v-app-bar>
<template #prepend>
<v-tabs v-model="recentNewsType" align-tabs="center" class="news-tab">
<div class="pn-nav">
<v-tabs v-model="recentNewsType" align-tabs="center" class="pn-nav-tabs">
<v-tab
v-for="(value, index) in bbsEnum.post.newsTypeList"
:key="index"
@@ -13,40 +13,39 @@
{{ rawData[value].name }}
</v-tab>
</v-tabs>
</template>
<template #title>
<v-text-field
v-model="search"
:hide-details="true"
:single-line="true"
append-icon="mdi-magnify"
class="news-search"
append-inner-icon="mdi-magnify"
class="pn-nav-search"
density="compact"
label="请输入帖子 ID 或搜索词"
variant="outlined"
:clearable="true"
@keydown.enter="searchPost()"
@click:append="searchPost()"
@click:append-inner="searchPost()"
/>
</template>
<template #append>
<v-btn
:loading="loading"
class="post-news-btn"
size="small"
variant="elevated"
@click="firstLoad(true)"
>
<v-icon>mdi-refresh</v-icon>
</v-btn>
<v-btn class="post-news-btn" size="small" variant="elevated" @click="handleList()">
<v-icon>mdi-view-list</v-icon>
</v-btn>
</template>
<div class="pn-nav-btns">
<v-btn
:loading="loading"
class="pn-nav-btn"
size="small"
variant="elevated"
@click="firstLoad(true)"
>
<v-icon>mdi-refresh</v-icon>
</v-btn>
<v-btn class="pn-nav-btn" size="small" variant="elevated" @click="handleList()">
<v-icon>mdi-view-list</v-icon>
</v-btn>
</div>
</div>
</v-app-bar>
<v-window v-model="recentNewsType">
<v-window-item v-for="(value, index) in bbsEnum.post.newsTypeList" :key="index" :value="value">
<div class="news-grid">
<div v-for="item in postData[value]" :key="item.post.post_id">
<TPostCard :model-value="item" />
<div class="pn-grid">
<div v-for="post in postData[value]" :key="post.post.post_id">
<TPostCard :post />
</div>
</div>
</v-window-item>
@@ -199,37 +198,47 @@ async function searchPost(): Promise<void> {
}
</script>
<style lang="scss" scoped>
.news-tab {
margin-bottom: 8px;
color: var(--common-text-title);
font-family: var(--font-title);
&:first-child {
margin-left: 12px;
}
.pn-nav {
position: relative;
display: flex;
overflow: auto hidden;
width: 100%;
max-width: 100%;
height: 100%;
box-sizing: border-box;
align-items: center;
justify-content: space-between;
padding-right: 16px;
padding-left: 16px;
column-gap: 16px;
}
.post-news-btn {
.pn-nav-tabs {
position: relative;
color: var(--common-text-title);
font-family: var(--font-title);
}
.pn-nav-search {
color: var(--box-text-1);
}
.pn-nav-btns {
position: relative;
display: flex;
align-items: center;
justify-content: center;
column-gap: 8px;
}
.pn-nav-btn {
height: 40px;
background: var(--tgc-btn-1);
color: var(--btn-text);
font-family: var(--font-title);
&:last-child {
margin-right: 12px;
}
}
.post-news-btn + .post-news-btn {
margin-left: 8px;
}
.news-search {
margin: 0 16px;
color: var(--box-text-1);
}
.news-grid {
.pn-grid {
display: grid;
font-family: var(--font-title);
gap: 8px;

View File

@@ -6,9 +6,9 @@
<TMiImg :ori="true" :src="topicInfo.topic.cover" alt="cover" />
<div class="post-topic-info">
<span class="post-topic-title">{{ topicInfo.topic.name }}</span>
<span :title="topicInfo.topic.desc" class="post-topic-desc">{{
topicInfo.topic.desc
}}</span>
<span :title="topicInfo.topic.desc" class="post-topic-desc">
{{ topicInfo.topic.desc }}
</span>
</div>
<div :title="`话题ID${topicInfo.topic.id}`" class="post-topic-id">
{{ topicInfo.topic.id }}
@@ -16,7 +16,7 @@
</div>
</template>
<template #extension>
<TGameNav v-if="curGid !== 0" :model-value="curGid" style="margin-left: 8px" />
<TGameNav v-if="curGid !== 0" :gid="curGid" :mini="false" style="margin-left: 8px" />
</template>
<div class="post-topic-switch">
<v-select
@@ -67,12 +67,12 @@
<v-text-field
v-model="search"
:hide-details="true"
:single-line="true"
:clearable="true"
append-inner-icon="mdi-magnify"
class="post-switch-item"
label="请输入帖子 ID 或搜索词"
variant="outlined"
@click:append="searchPost"
@click:append-inner="searchPost"
@keyup.enter="searchPost"
/>
<v-btn
@@ -89,7 +89,7 @@
</v-app-bar>
<div class="post-topic-grid">
<div v-for="post in posts" :key="post.post.post_id">
<TPostCard :model-value="post" :user-click="true" @onUserClick="handleUserClick" />
<TPostCard :post @onUserClick="handleUserClick" />
</div>
</div>
<VpOverlaySearch v-model="showSearch" :gid="curGid" :keyword="search" />

View File

@@ -1,6 +1,6 @@
/**
* 角色生日模块
* @since Beta v0.9.1
* @since Beta v0.9.9
*/
import {
@@ -12,7 +12,7 @@ import {
/**
* 判断今天是不是角色生日
* @since Beta v0.4.6
* @since Beta v0.9.9
* @returns 角色生日
*/
function isAvatarBirth(): Array<TGApp.Archive.Birth.CalendarItem> {
@@ -20,19 +20,28 @@ function isAvatarBirth(): Array<TGApp.Archive.Birth.CalendarItem> {
const month = date.getMonth() + 1;
const day = date.getDate();
const days = ArcBirCalendar[month];
const find = days.filter((i) => i.role_birthday === `${month}/${day}`);
if (find.length > 0) {
for (const i of find) i.is_subscribe = true;
return find;
const resId = new Set<number>();
const res: Array<TGApp.Archive.Birth.CalendarItem> = [];
const rawFind = days.filter((i) => i.role_birthday === `${month}/${day}`);
if (rawFind.length > 0) {
res.push(...rawFind);
rawFind.map((i) => resId.add(i.role_id));
}
const find2 = AppCharacterData.filter((i) => i.birthday.toString() === [month, day].toString());
return find2.map((i) => ({
role_id: i.id,
name: i.name,
role_birthday: `${month}/${day}`,
head_icon: `/WIKI/character/${i.id}.webp`,
is_subscribe: false,
}));
const wikiFind = AppCharacterData.filter(
(i) => i.birthday.toString() === [month, day].toString(),
);
for (const i of wikiFind) {
if (resId.has(i.id)) continue;
res.push({
role_id: i.id,
name: i.name,
role_birthday: `${month}/${day}`,
head_icon: `/WIKI/character/${i.id}.webp`,
is_subscribe: false,
});
resId.add(i.id);
}
return res;
}
/**

View File

@@ -1,54 +1,98 @@
/**
* 用户祈愿模块
* @since Beta v0.9.6
* @since Beta v0.9.8
*/
import showDialog from "@comp/func/dialog.js";
import showLoading from "@comp/func/loading.js";
import showSnackbar from "@comp/func/snackbar.js";
import Database from "@tauri-apps/plugin-sql";
import { getUtc8Time, getWikiBrief, timestampToDate } from "@utils/toolFunc.js";
import { ref, type Ref } from "vue";
import TGSqlite from "../index.js";
/**
* 导入物品
* @since Beta v0.9.6
* @param uid - UID
* @param item - UIGF数据
* @returns Promise<void>
* 批量插入祈愿数据
* @since Beta v0.9.8
* @param db - 数据库
* @param uid - 用户UID
* @param data - 祈愿数据
* @param size - batchSize
* @param cnt - cntRef
*/
async function insertGachaItem(uid: string, item: TGApp.Plugins.UIGF.GachaItem): Promise<void> {
const db = await TGSqlite.getDB();
const updateTime = timestampToDate(Date.now());
await db.execute(
`INSERT INTO GachaRecords(uid, gachaType, itemId, count, time,
name, type, rank, id, uigfType, updated)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
ON CONFLICT(id) DO UPDATE
SET uid = $1,
gachaType = $2,
itemId = $3,
count = $4,
time = $5,
name = $6,
type = $7,
rank = $8,
uigfType = $10,
updated = $11;
`,
[
uid,
item.gacha_type,
item.item_id ?? null,
item.count ?? 1,
item.time,
item.name,
item.item_type ?? null,
item.rank_type ?? null,
item.id,
item.uigf_gacha_type,
updateTime,
],
);
async function insertGachaList(
db: Database,
uid: string,
data: Array<TGApp.Plugins.UIGF.GachaItem>,
size: number,
cnt: Ref<number>,
): Promise<void> {
await db.execute("PRAGMA busy_timeout = 5000;");
for (let i = 0; i < data.length; i += size) {
await db.execute("BEGIN IMMEDIATE;");
try {
const batch = data.slice(i, i + size);
let batchSql = "";
const batchParams = [];
for (const item of batch) {
const updateTime = timestampToDate(Date.now());
batchSql += `
INSERT INTO GachaRecords(uid, gachaType, itemId, count, time,
name, type, rank, id, uigfType, updated)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id)
DO UPDATE
SET uid=?,
gachaType=?,
itemId=?,
count=?,
time=?,
name=?,
type=?,
rank=?,
uigfType=?,
updated=?;
`;
batchParams.push(
uid,
item.gacha_type,
item.item_id ?? null,
item.count ?? 1,
item.time,
item.name,
item.item_type ?? null,
item.rank_type ?? null,
item.id,
item.uigf_gacha_type,
updateTime,
// update 部分
uid,
item.gacha_type,
item.item_id ?? null,
item.count ?? 1,
item.time,
item.name,
item.item_type ?? null,
item.rank_type ?? null,
item.uigf_gacha_type,
updateTime,
);
cnt.value++;
}
await db.execute(batchSql, batchParams);
await db.execute("COMMIT;");
} catch (e) {
const msg = String(e);
if (/BUSY|LOCKED|SQLITE_BUSY|SQLITE_LOCKED/i.test(msg)) {
await showDialog.check(`数据库锁定`, `请刷新页面(F5)后重试操作`);
return;
}
// 其他错误直接抛出
await db.execute("ROLLBACK;");
throw e;
}
}
}
/**
@@ -235,14 +279,13 @@ async function cleanGachaRecords(
const res = await db.execute(sql);
if (res.rowsAffected > 0) {
showSnackbar.success(`[${uid}][${pool}][${time}]清理了${res.rowsAffected}条祈愿记录`);
await new Promise<void>((resolve) => setTimeout(resolve, 1500));
}
}
}
/**
* 合并祈愿数据
* @since Beta v0.9.5
* @since Beta v0.9.8
* @param uid - UID
* @param data - UIGF数据
* @param showProgress - 是否显示进度
@@ -253,33 +296,32 @@ async function mergeUIGF(
data: Array<TGApp.Plugins.UIGF.GachaItem>,
showProgress: boolean = false,
): Promise<void> {
let cnt = 0;
const db = await TGSqlite.getDB();
const len = data.length;
let progress = 0;
const cnt = ref<number>(0);
let timer: NodeJS.Timeout | null = null;
if (showProgress) {
timer = setInterval(async () => {
progress = Math.round((cnt / len) * 100 * 100) / 100;
const current = data[cnt]?.time ?? "";
const name = data[cnt]?.name ?? "";
const rank = data[cnt]?.rank_type ?? "0";
await showLoading.update(`[${progress}%][${current}] ${"⭐".repeat(Number(rank))}-${name}`);
const progress = Math.round((cnt.value / len) * 100 * 100) / 100;
const current = data[cnt.value]?.time ?? "";
const name = data[cnt.value]?.name ?? "";
const rank = data[cnt.value]?.rank_type ?? "0";
await showLoading.update(`[${progress}%][${current}] ${"⭐".repeat(Number(rank))}-${name}`, {
timeout: 0,
});
}, 1000);
}
for (const gacha of data) {
await insertGachaItem(uid, transGacha(gacha));
cnt++;
}
const transformed = data.map((g) => transGacha(g));
await insertGachaList(db, uid, transformed, 100, cnt);
if (timer) {
clearInterval(timer);
progress = 100;
await showLoading.update(`[${progress}%] 完成`);
await showLoading.update(`[100%] 完成`, { timeout: 0 });
}
}
/**
* 合并祈愿数据v4.x
* @since Beta v0.9.5
* @since Beta v0.9.8
* @param data - UIGF数据
* @param showProgress - 是否显示进度
* @returns 无返回值
@@ -288,27 +330,27 @@ async function mergeUIGF4(
data: TGApp.Plugins.UIGF.GachaHk4e,
showProgress: boolean = false,
): Promise<void> {
let cnt: number = 0;
const db = await TGSqlite.getDB();
const len = data.list.length;
let progress: number = 0;
const cnt = ref<number>(0);
let timer: NodeJS.Timeout | null = null;
if (showProgress) {
timer = setInterval(async () => {
progress = Math.round((cnt / len) * 100 * 100) / 100;
const current = data.list[cnt]?.time ?? "";
const name = data.list[cnt]?.name ?? data.list[cnt]?.item_id;
const rank = data.list[cnt]?.rank_type ?? "0";
await showLoading.update(`[${progress}%][${current}] ${"⭐".repeat(Number(rank))}-${name}`);
const progress = Math.round((cnt.value / len) * 100 * 100) / 100;
const current = data.list[cnt.value]?.time ?? "";
const name = data.list[cnt.value]?.name ?? data.list[cnt.value]?.item_id;
const rank = data.list[cnt.value]?.rank_type ?? "0";
await showLoading.update(`[${progress}%][${current}] ${"⭐".repeat(Number(rank))}-${name}`, {
timeout: 0,
});
}, 1000);
}
for (const gacha of data.list) {
await insertGachaItem(data.uid.toString(), transGacha(gacha, data.timezone));
cnt++;
}
const uid = data.uid.toString();
const transformed = data.list.map((g) => transGacha(g, data.timezone));
await insertGachaList(db, uid, transformed, 100, cnt);
if (timer) {
clearInterval(timer);
progress = 100;
await showLoading.update(`[${progress}%] 完成`);
await showLoading.update(`[100%] 完成`, { timeout: 0 });
}
}

View File

@@ -1,57 +1,99 @@
/**
* 千星奇域祈愿模块
* @since Beta v0.9.5
* @since Beta v0.9.8
*/
import showDialog from "@comp/func/dialog.js";
import showLoading from "@comp/func/loading.js";
import TGSqlite from "@Sql/index.js";
import Database from "@tauri-apps/plugin-sql";
import { getUtc8Time, timestampToDate } from "@utils/toolFunc.js";
import { ref, type Ref } from "vue";
import { AppGachaBData } from "@/data/index.js";
/**
* 插入颂愿数据
* @since Beta v0.9.5
* @param uid - UID
* @param item - 颂愿数据
* @returns 无返回值
* 批量插入颂愿数据
* @since Beta v0.9.8
* @param db - 数据库
* @param uid - 用户UID
* @param data - 祈愿数据
* @param size - batchSize
* @param cnt - cntRef
*/
async function insertGachaItem(uid: string, item: TGApp.Plugins.UIGF.GachaItemB): Promise<void> {
const db = await TGSqlite.getDB();
const gachaType = item.op_gacha_type === "1000" ? "1000" : "2000";
const updateTime = timestampToDate(Date.now());
await db.execute(
`
INSERT INTO GachaBRecords(id, uid, scheduleId, gachaType, opGachaType, time,
itemId, name, type, rank, updated)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
ON CONFLICT (id)
DO UPDATE
SET uid = $2,
scheduleId = $3,
gachaType = $4,
opGachaType = $5,
time = $6,
itemId = $7,
name = $8,
type = $9,
rank = $10,
updated = $11;
`,
[
item.id,
uid,
item.schedule_id,
gachaType,
item.op_gacha_type,
item.time,
item.item_id,
item.item_name,
item.item_type,
item.rank_type,
updateTime,
],
);
async function insertGachaBList(
db: Database,
uid: string,
data: Array<TGApp.Plugins.UIGF.GachaItemB>,
size: number,
cnt?: Ref<number>,
): Promise<void> {
await db.execute("PRAGMA busy_timeout = 5000;");
for (let i = 0; i < data.length; i += size) {
await db.execute("BEGIN IMMEDIATE;");
try {
const batch = data.slice(i, i + size);
let batchSql = "";
const batchParams = [];
for (const item of batch) {
const updateTime = timestampToDate(Date.now());
const gachaType = item.op_gacha_type === "1000" ? "1000" : "2000";
batchSql += `
INSERT INTO GachaBRecords(id, uid, scheduleId, gachaType, opGachaType, time,
itemId, name, type, rank, updated)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id)
DO UPDATE
SET uid=?,
scheduleId=?,
gachaType=?,
opGachaType=?,
time=?,
itemId=?,
name=?,
type=?,
rank=?,
updated=?;
`;
batchParams.push(
item.id,
uid,
item.schedule_id,
gachaType,
item.op_gacha_type,
item.time,
item.item_id,
item.item_name,
item.item_type,
item.rank_type,
updateTime,
// update 部分
uid,
item.schedule_id,
gachaType,
item.op_gacha_type,
item.time,
item.item_id,
item.item_name,
item.item_type,
item.rank_type,
updateTime,
);
if (cnt) cnt.value++;
}
await db.execute(batchSql, batchParams);
await db.execute("COMMIT;");
} catch (e) {
const msg = String(e);
if (/BUSY|LOCKED|SQLITE_BUSY|SQLITE_LOCKED/i.test(msg)) {
await showDialog.check(`数据库锁定`, `请刷新页面(F5)后重试操作`);
return;
}
// 其他错误直接抛出
await db.execute("ROLLBACK;");
throw e;
}
}
}
/**
@@ -65,9 +107,8 @@ async function insertGachaList(
uid: string,
list: Array<TGApp.Plugins.UIGF.GachaItemB>,
): Promise<void> {
for (const gacha of list) {
await insertGachaItem(uid, gacha);
}
const db = await TGSqlite.getDB();
await insertGachaBList(db, uid, list, 100);
}
/**
@@ -106,27 +147,26 @@ async function mergeUIGF4(
data: TGApp.Plugins.UIGF.GachaUgc,
showProgress: boolean = false,
): Promise<void> {
let cnt: number = 0;
const db = await TGSqlite.getDB();
const len = data.list.length;
let progress: number = 0;
const cnt = ref<number>(0);
let timer: NodeJS.Timeout | null = null;
if (showProgress) {
timer = setInterval(async () => {
progress = Math.round((cnt / len) * 100 * 100) / 100;
const current = data.list[cnt].time ?? "";
const name = data.list[cnt].item_name ?? "";
const rank = data.list[cnt].rank_type ?? "0";
await showLoading.update(`[${progress}%][${current}] ${"⭐".repeat(Number(rank))}-${name}`);
const progress = Math.round((cnt.value / len) * 100 * 100) / 100;
const current = data.list[cnt.value].time ?? "";
const name = data.list[cnt.value].item_name ?? "";
const rank = data.list[cnt.value].rank_type ?? "0";
await showLoading.update(`[${progress}%][${current}] ${"⭐".repeat(Number(rank))}-${name}`, {
timeout: 0,
});
}, 1000);
}
for (const gacha of data.list) {
await insertGachaItem(data.uid.toString(), transGacha(gacha, data.timezone));
cnt++;
}
const transformed = data.list.map((g) => transGacha(g));
await insertGachaBList(db, data.uid.toString(), transformed, 100, cnt);
if (timer) {
clearInterval(timer);
progress = 100;
await showLoading.update(`[${progress}%] 完成`);
await showLoading.update(`[100%] 完成`, { timeout: 0 });
}
}

View File

@@ -1,6 +1,6 @@
/**
* 应用状态管理
* @since Beta v0.9.1
* @since Beta v0.9.8
*/
import bbsEnum from "@enum/bbs.js";
@@ -67,6 +67,11 @@ const useAppStore = defineStore(
const closeToTray = ref<boolean>(false);
/** 展示反馈按钮 */
const showFeedback = ref<boolean>(true);
/**
* 上次检测更新时间
* @remarks LastUpdateCheckTimeStamp
*/
const lastUcts = ref<number>(0);
/**
* 初始化应用状态
@@ -91,6 +96,7 @@ const useAppStore = defineStore(
cancelLike.value = true;
closeToTray.value = false;
showFeedback.value = true;
lastUcts.value = 0;
initDevice();
}
@@ -149,6 +155,7 @@ const useAppStore = defineStore(
cancelLike,
closeToTray,
showFeedback,
lastUcts,
init,
changeTheme,
getImageUrl,
@@ -178,6 +185,7 @@ const useAppStore = defineStore(
"cancelLike",
"closeToTray",
"showFeedback",
"lastUcts",
],
},
{

View File

@@ -1,6 +1,6 @@
/**
* 帖子类型定义文件
* @since Beta v0.8.6
* @since Beta v0.9.8
*/
declare namespace TGApp.BBS.Post {
@@ -124,7 +124,7 @@ declare namespace TGApp.BBS.Post {
/**
* 帖子数据
* @since Beta v0.7.2
* @since Beta v0.9.8
*/
type FullData = {
/** 帖子信息 */
@@ -136,7 +136,7 @@ declare namespace TGApp.BBS.Post {
/** 发帖人,可能为 null */
user: User | null;
/** 当前用户操作 */
self_operation: TGApp.BBS.User.SelfOperation;
self_operation: TGApp.BBS.User.SelfOperation | null;
/** 帖子统计,可能为 null */
stat: Stat | null;
/** 帮助系统,可能为 null */

64
src/types/Plugins/Github.d.ts vendored Normal file
View File

@@ -0,0 +1,64 @@
/**
* Github API 类型
* @since Beta v0.9.8
*/
declare namespace TGApp.Plugins.Github {
/**
* LatestReleaseResponse
* @since Beta v0.9.8
* @see https://api.github.com/repos/BTMuli/TeyvatGuide/releases/latest
* @remarks 省略了不需要的子数据
*/
type LastestReleaseResp = {
/** URL */
url: string;
/** Assets URL */
assets_url: string;
/** Upload URL */
upload_url: string;
/** Html URL */
html_url: string;
/** Release ID */
id: number;
/** Author */
author: unknown;
/** Node ID */
node_id: string;
/** Tag */
tag_name: string;
/** Commit Hash */
target_commitish: string;
/** Release Name */
name: string;
/** Draft */
draft: boolean;
/** Immutable */
immutable: boolean;
/** PreRelease */
prerelease: boolean;
/**
* CreateTime
* @example 2026-02-26T10:33:04Z
*/
created_at: string;
/**
* UpdateTime
* @example 2026-02-27T08:53:27Z
*/
updated_at: string;
/**
* PublishTime
* @example 2026-02-27T08:53:27Z
*/
published_at: string;
/** Assets */
assets: Array<unknown>;
/** Tarball URL */
tarball_url: string;
/** Zipball URL */
zipball_url: string;
/** Release Body */
body: string;
};
}

26
src/utils/Github.ts Normal file
View File

@@ -0,0 +1,26 @@
/**
* Github API
* @since Beta v0.9.8
*/
import TGHttp from "@utils/TGHttp.js";
/**
* 获取最新Release版本
* @since Beta v0.9.8
* @returns 最新版本
*/
export async function getLatestReleaseVersion(): Promise<string> {
const latestReleaseApi: Readonly<string> =
"https://api.github.com/repos/BTMuli/TeyvatGuide/releases/latest";
try {
const latestReleaseResp = await TGHttp<TGApp.Plugins.Github.LastestReleaseResp>(
latestReleaseApi,
{ method: "GET" },
);
return latestReleaseResp.tag_name.replace("v", "");
} catch (e) {
console.error(e);
return "0";
}
}

View File

@@ -1,6 +1,6 @@
/**
* 游戏文件相关功能
* @since Beta v0.9.6
* @since Beta v0.9.8
*/
import showDialog from "@comp/func/dialog.js";
@@ -40,7 +40,10 @@ export async function tryReadGameVer(gameDir: string): Promise<false | string> {
const iniRead = await readTextFileLines(iniPath);
while (true) {
const line = await iniRead.next();
if (line.value.startsWith("game_version=")) return line.value.split("=")[1];
const lineRead = line.value;
if (typeof lineRead === "string" && lineRead.startsWith("game_version=")) {
return lineRead.split("=")[1];
}
if (line.done) break;
}
}

View File

@@ -1,13 +1,13 @@
/**
* 窗口创建相关工具函数
* @since Beta v0.9.6
* @since Beta v0.9.8
*/
import type { RenderCard } from "@comp/app/t-postcard.vue";
import showSnackbar from "@comp/func/snackbar.js";
import { core, webviewWindow, window as TauriWindow } from "@tauri-apps/api";
import { invoke } from "@tauri-apps/api/core";
import { PhysicalSize } from "@tauri-apps/api/dpi";
import { PhysicalPosition, PhysicalSize } from "@tauri-apps/api/dpi";
import { currentMonitor, WindowOptions } from "@tauri-apps/api/window";
import { openUrl } from "@tauri-apps/plugin-opener";
@@ -99,6 +99,45 @@ export function getWindowSize(label: string): PhysicalSize {
}
}
/**
* 判断窗口位置,确保窗口不超出屏幕并居中
* @since Beta v0.9.8
* @remarks 当窗口超出屏幕时回滚到 resizeWindow此时回正配置默认生效
* @returns 无返回值
*/
export async function setWindowPos(): Promise<void> {
const screen = await currentMonitor();
const NAV_BAR_HEIGHT = 28;
if (screen === null) {
showSnackbar.error("获取屏幕信息失败!", 3000);
return;
}
const windowCur = webviewWindow.getCurrentWebviewWindow();
if (await windowCur.isMaximized()) return;
const designSize = getWindowSize(windowCur.label);
const screenScale = screen.scaleFactor;
const targetWidth = Math.round(designSize.width * screenScale);
const targetHeight = Math.round(designSize.height * screenScale);
const cpWidth = screen.size.width - NAV_BAR_HEIGHT * screenScale;
const cpHeight = screen.size.height - NAV_BAR_HEIGHT * screenScale;
if (targetWidth > cpWidth && targetHeight > cpHeight) {
await resizeWindow();
await windowCur.center();
} else if (targetHeight > cpHeight) {
const left = (screen.size.width - targetWidth) / 2;
await windowCur.setSize(new PhysicalSize(targetWidth, targetHeight));
await windowCur.setPosition(new PhysicalPosition(left, 24));
} else if (targetWidth > screen.size.width) {
const top = (screen.size.height - targetHeight) / 2;
await windowCur.setSize(new PhysicalSize(targetWidth, targetHeight));
await windowCur.setPosition(new PhysicalPosition(24, top));
} else {
await windowCur.setSize(new PhysicalSize(targetWidth, targetHeight));
await windowCur.center();
}
await windowCur.setZoom(1);
}
/**
* 窗口适配
* @since Beta v0.9.6

View File

@@ -94,6 +94,7 @@ async function createAnnoJson(): Promise<void> {
.anno-body {
width: 800px;
max-width: calc(100% - 100px);
margin: 0 auto;
font-family: var(--font-text);
}

View File

@@ -15,9 +15,15 @@
<TMiImg
:src="getGameIcon(postData?.forum?.game_id || postData.post.game_id)"
alt="gameIcon"
title="点击前往资讯页面"
@click="toGame(postData?.forum?.game_id || postData.post.game_id)"
/>
<div v-if="postData.forum" class="mpm-forum" @click="toForum(postData.forum)">
<div
v-if="postData.forum"
class="mpm-forum"
title="点击前往版块页面"
@click="toForum(postData.forum)"
>
<TMiImg :ori="true" :src="postData.forum.icon" alt="forumIcon" />
<span>{{ postData.forum.name }}</span>
</div>
@@ -245,7 +251,7 @@ onMounted(async () => {
}
postData.value = resp;
console.log(resp);
isLike.value = postData.value.self_operation.upvote_type !== 0;
isLike.value = (postData.value.self_operation?.upvote_type ?? 0) > 0;
await showLoading.update("正在渲染数据");
renderPost.value = await getRenderPost(postData.value);
await webviewWindow
@@ -279,7 +285,10 @@ onUnmounted(() => {
async function openJson(): Promise<void> {
// @ts-expect-error import.meta
if (import.meta.env.MODE === "production") return;
if (import.meta.env.MODE === "production") {
await toPost();
return;
}
await createPostJson(postId);
}
@@ -452,7 +461,7 @@ async function toTopic(topic: TGApp.BBS.Post.Topic): Promise<void> {
}
async function toGame(gameId: number): Promise<void> {
await emit("active_deep_link", `router?path=/posts/forum/${gameId}`);
await emit("active_deep_link", `router?path=/news/${gameId}`);
}
async function toForum(forum: TGApp.BBS.Post.Forum): Promise<void> {
@@ -486,7 +495,9 @@ function handleUser(user: TGApp.BBS.Post.User): void {
@use "@styles/github.styles.scss" as github-styles;
.tp-post-body {
position: relative;
width: v-bind(viewWidth); /* stylelint-disable-line value-keyword-case */
max-width: calc(100% - 100px);
margin: 0 auto;
font-family: var(--font-text);
transition: width 0.3s ease-in-out;

View File

@@ -1,6 +1,6 @@
/**
* vite 配置文件
* @since Beta v0.9.6
* @since Beta v0.9.9
*/
import { sentryVitePlugin } from "@sentry/vite-plugin";