Compare commits

...

70 Commits

Author SHA1 Message Date
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
BTMuli
7cfd47c36b 🚀 v0.9.7 2026-02-26 18:33:04 +08:00
BTMuli
fcc5d3db15 💄 增加可见度
#221
2026-02-26 17:49:09 +08:00
BTMuli
2f19691a57 💄 UI适配 2026-02-26 17:30:36 +08:00
BTMuli
83ddadd451 💄 UI适配
#221
2026-02-26 17:27:53 +08:00
BTMuli
b965cccbf1 🚸 处理大小写
close #219
2026-02-26 17:07:10 +08:00
BTMuli
8b60a7f8dd 🐛 修复脚本页面账号切换异常 2026-02-26 15:41:59 +08:00
BTMuli
04c9907490 💄 调整浅色模式下滚动条可见度
#221
2026-02-26 12:56:16 +08:00
BTMuli
adef358534 💄 修正图标样式 2026-02-26 12:50:32 +08:00
BTMuli
fcdad22d94 💄 移除冗余跳转
#221
2026-02-26 12:44:24 +08:00
BTMuli
bbc142ac2d 💄 替换部分侧边栏图标
#221
2026-02-26 12:35:24 +08:00
BTMuli
7169bc202e 🚸 调整游戏安装目录选取逻辑
close #219
2026-02-26 11:58:56 +08:00
92 changed files with 3357 additions and 2201 deletions

View File

@@ -1,3 +1,3 @@
VITE_SENTRY_RELEASE=TeyvatGuide@0.9.6
VITE_COMMIT_HASH=40ffb41f
VITE_BUILD_TIME=1772034641
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,44 @@
Author: 目棃
Description: CHANGELOG
Date: 2025-09-09
Update: 2026-02-25
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-25 23:50:25`
> 更新于 `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)
- 🐛 修复脚本页面账号切换异常
- 🚸 调整游戏安装目录选取逻辑,调整大小写处理 [`#219`](https://github.com/BTMuli/TeyvatGuide/issues/219)
- 💄 替换部分侧边栏图标
- 💄 调整浅色模式下滚动条可见度
- 💄 调整部分页面UI
## [0.9.6](https://github.com/BTMuli/TeyvatGuide/releases/v0.9.6) (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.6",
"version": "0.9.8",
"description": "Game Tool for GenshinImpact player",
"private": true,
"packageManager": "pnpm@10.30.1",
"packageManager": "pnpm@10.32.0",
"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.43.0",
"@sentry/vite-plugin": "^5.1.1",
"@sentry/vue": "^10.43.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,61 +101,61 @@
"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",
"vuetify": "^4.0.2",
"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",
"@types/node": "^25.4.0",
"@typescript-eslint/parser": "^8.57.0",
"@typescript/native-preview": "7.0.0-dev.20260222.1",
"@vitejs/plugin-vue": "^6.0.4",
"@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.3.3",
"oxlint": "^1.54.0",
"postcss-preset-env": "^11.2.0",
"prettier": "3.8.1",
"stylelint": "^17.3.0",
"stylelint": "^17.4.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.0.0",
"stylelint-prettier": "^5.0.3",
"stylelint-scss": "^7.0.0",
"tsx": "^4.21.0",
"typescript": "^5.9.3",
"typescript-eslint": "^8.56.1",
"typescript-eslint": "^8.57.0",
"vite": "npm:rolldown-vite@^7.3.1",
"vite-plugin-vue-devtools": "^8.0.6",
"vite-plugin-vue-devtools": "^8.0.7",
"vite-plugin-vuetify": "^2.1.3",
"vue-eslint-parser": "^10.4.0",
"vue-tsc": "^3.2.5",

2477
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

BIN
public/UI/nav/subSign.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
public/UI/nav/userBag.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

577
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.6"
version = "0.9.8"
description = "Game Tool for Genshin Impact player"
authors = ["BTMuli <bt-muli@outlook.com>"]
license = "MIT"
@@ -17,10 +17,10 @@ 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"
chrono = "0.4.44"
image = "0.25.9"
log = "0.4.29"
prost = "=0.14.1"
@@ -28,8 +28,8 @@ prost-types = "=0.14.1"
sentry = { version = "0.46.2", 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.6",
"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

@@ -93,7 +93,7 @@ html {
::-webkit-scrollbar-thumb {
min-height: 48px;
border-radius: 4px;
background: var(--tgc-od-white);
background: var(--app-scroll-bg);
}
::-webkit-scrollbar-thumb:hover {

View File

@@ -1,6 +1,6 @@
/**
* 主题样式文件-深色主题
* @since v0.8.9
* @since v0.9.7
*/
/* dark mode */
@@ -11,6 +11,7 @@ html.dark {
--app-page-content: #d0d0d0ff;
--app-side-bg: #151c26ff;
--app-side-content: #ddddddff;
--app-scroll-bg: var(--tgc-od-white);
/* box container */
--box-bg-1: #21252bff;

View File

@@ -1,6 +1,6 @@
/**
* 主题样式文件-默认(浅色)主题
* @since v0.8.9
* @since v0.9.7
*/
/* default(light) theme */
@@ -11,6 +11,7 @@ html.default {
--app-page-content: #2f2f2fff;
--app-side-bg: #f2f2f2ff;
--app-side-content: #222222ff;
--app-scroll-bg: var(--tgc-yellow-3);
/* box container */
--box-bg-1: #f9f6f2ff;

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 class="tgni-box" :title="props.label">
<slot name="icon"></slot>
<span 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;

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;

View File

@@ -103,7 +103,7 @@
data-html2canvas-ignore
@click.stop="trySelect()"
/>
<div v-else class="tpc-info-id">
<div v-else class="tpc-info-id" @click="shareCard()">
<span>{{ props.modelValue.post.post_id }}</span>
<template v-if="isDevEnv">
<span data-html2canvas-ignore>[{{ props.modelValue.post.view_type }}]</span>
@@ -230,7 +230,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 };
@@ -575,6 +575,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;

View File

@@ -1,6 +1,6 @@
<!-- 应用侧边栏 -->
<template>
<v-navigation-drawer :permanent="true" :rail="rail" class="tsb-box">
<v-navigation-drawer :width="160" :permanent="true" :rail="rail" class="tsb-box">
<v-list :nav="true" class="side-list" density="compact">
<v-list-item
:append-icon="!rail ? 'mdi-chevron-left' : undefined"
@@ -42,26 +42,26 @@
<v-list-item :link="true" :title.attr="'背包材料'" href="/bag/material">
<template #title>背包材料</template>
<template #prepend>
<img alt="materialBagIcon" class="side-icon" src="/icon/material/121234.webp" />
<img alt="materialBagIcon" class="side-icon" src="/UI/nav/userBag.webp" />
</template>
</v-list-item>
<v-divider />
<v-list-item :link="true" :title.attr="'原神战绩'" href="/user/record">
<template #title>原神战绩</template>
<template #prepend>
<img alt="record" class="side-icon-menu" src="/UI/nav/userRecord.webp" />
<img alt="record" class="side-icon" src="/UI/nav/userRecord.webp" />
</template>
</v-list-item>
<v-list-item :link="true" :title.attr="'角色列表'" href="/user/characters">
<template #title>角色列表</template>
<template #prepend>
<img alt="characters" class="side-icon-menu" src="/UI/nav/userAvatar.webp" />
<img alt="characters" class="side-icon" src="/UI/nav/userAvatar.webp" />
</template>
</v-list-item>
<v-list-item :link="true" :title.attr="'祈愿记录'" href="/user/gacha">
<template #title>祈愿记录</template>
<template #prepend>
<img alt="gacha" class="side-icon-menu" src="/UI/nav/userGacha.webp" />
<img alt="gacha" class="side-icon" src="/UI/nav/userGacha.webp" />
</template>
</v-list-item>
<!-- 高难挑战包括深渊&剧诗&危战 -->
@@ -182,7 +182,7 @@
@click="openClient('sign_in')"
>
<template #prepend>
<img alt="sing_in" class="side-icon-menu" src="/UI/nav/userGacha.webp" />
<img alt="sing_in" class="side-icon-menu" src="/UI/nav/subSign.webp" />
</template>
</v-list-item>
<v-list-item
@@ -192,7 +192,7 @@
@click="openClient('game_record')"
>
<template #prepend>
<img alt="game_record" class="side-icon-menu" src="/UI/nav/userRecord.webp" />
<img alt="game_record" class="side-icon-menu" src="/UI/nav/subRecord.webp" />
</template>
</v-list-item>
<v-list-item
@@ -330,7 +330,7 @@ import useUserStore from "@store/user.js";
import { event, path, webviewWindow } from "@tauri-apps/api";
import { invoke } from "@tauri-apps/api/core";
import type { Event, UnlistenFn } from "@tauri-apps/api/event";
import { exists } from "@tauri-apps/plugin-fs";
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";
@@ -690,6 +690,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({
@@ -729,11 +730,17 @@ async function tryLaunchGame(): Promise<void> {
showSnackbar.warn("请先登录");
return;
}
const gamePath = `${gameDir.value}${path.sep()}YuanShen.exe`;
if (!(await exists(gamePath))) {
if (gameDir.value === "未设置") {
showSnackbar.warn("请前往设置页面设置游戏安装目录");
return;
}
const dirRead = await readDir(gameDir.value);
const find = dirRead.find((i) => i.isFile && i.name.toLowerCase() === "yuanshen.exe");
if (!find) {
showSnackbar.warn("未检测到原神本体应用");
return;
}
const gamePath = `${gameDir.value}${path.sep()}${find.name}`;
const resp = await passportReq.authTicket(account.value, cookie.value);
if (typeof resp !== "string") {
showSnackbar.error(`[${resp.retcode}] ${resp.message}`);
@@ -785,9 +792,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);
}
@@ -800,7 +804,7 @@ async function tryLaunchGame(): Promise<void> {
.bottom-menu {
position: absolute;
bottom: 0;
bottom: 8px;
width: 100%;
}

View File

@@ -43,6 +43,7 @@ onUnmounted(() => {
.switch-box {
position: fixed;
z-index: 1;
top: 16px;
left: 16px;
display: flex;

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

View File

@@ -70,13 +70,13 @@ import showSnackbar from "@comp/func/snackbar.js";
import TGSqlite from "@Sql/index.js";
import useAppStore from "@store/app.js";
import { path } from "@tauri-apps/api";
import { sep } from "@tauri-apps/api/path";
import { open } from "@tauri-apps/plugin-dialog";
import { exists, readDir, remove } from "@tauri-apps/plugin-fs";
import { readDir, remove } from "@tauri-apps/plugin-fs";
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";
@@ -110,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;
@@ -124,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> {
@@ -146,31 +171,32 @@ async function confirmCGD(): Promise<void> {
const oriEmpty = gameDir.value === "未设置";
const editCheck = await showDialog.check(
oriEmpty ? "确认设置游戏目录?" : "确认修改游戏目录?",
oriEmpty ? "请选择 Yuanshen.exe 所在目录" : `当前:${gameDir.value}`,
oriEmpty ? "请选择 YuanShen.exe 所在目录" : `当前:${gameDir.value}`,
);
if (!editCheck) {
showSnackbar.cancel(oriEmpty ? "已取消设置" : "已取消修改");
return;
}
const dir: string | null = await open({
directory: true,
defaultPath: oriEmpty ? undefined : gameDir.value,
const file: string | null = await open({
defaultPath: oriEmpty ? undefined : `${gameDir.value}${path.sep()}YuanShen.exe`,
multiple: false,
});
if (dir === null) {
if (file === null) {
showSnackbar.warn("路径不能为空!");
return;
}
if (!oriEmpty && gameDir.value === dir) {
if (!file.toLowerCase().endsWith("yuanshen.exe")) {
showSnackbar.warn("请选中游戏本体(YuanShen.exe)");
return;
}
if (
!oriEmpty &&
`${gameDir.value}${path.sep()}YuanShen.exe`.toLowerCase() === file.toLowerCase()
) {
showSnackbar.warn("路径未修改!");
return;
}
// 校验是否存在游戏本体
if (!(await exists(`${dir}${path.sep()}YuanShen.exe`))) {
showSnackbar.warn("未检测到游戏本体");
return;
}
gameDir.value = dir;
gameDir.value = file.substring(0, file.lastIndexOf(path.sep()));
showSnackbar.success(oriEmpty ? "成功设置游戏目录" : "成功修改游戏目录");
}
@@ -204,7 +230,7 @@ async function confirmCLD(): Promise<void> {
await showLoading.start("正在清理日志文件...");
for (const file of delFiles) {
await showLoading.update(`正在清理 ${file.name}`);
const filePath = `${logDir.value}${sep()}${file.name}`;
const filePath = `${logDir.value}${path.sep()}${file.name}`;
await remove(filePath);
}
await new Promise<void>((resolve) => setTimeout(resolve, 1000));

View File

@@ -2,7 +2,7 @@
<div class="tgb-box">
<div class="tgb-top">
<div class="tgb-title">原神启动</div>
<v-btn size="small" icon="mdi-rocket" variant="outlined" @click="tryPlayGame()" />
<v-btn icon="mdi-rocket" size="small" variant="outlined" @click="tryPlayGame()" />
</div>
<v-list-item v-if="account.uid">
<v-list-item-title class="tgb-name">
@@ -22,7 +22,7 @@ import useAppStore from "@store/app.js";
import useUserStore from "@store/user.js";
import { path } from "@tauri-apps/api";
import { invoke } from "@tauri-apps/api/core";
import { exists } from "@tauri-apps/plugin-fs";
import { readDir } from "@tauri-apps/plugin-fs";
import TGLogger from "@utils/TGLogger.js";
import { storeToRefs } from "pinia";
@@ -42,11 +42,13 @@ async function tryPlayGame(): Promise<void> {
showSnackbar.warn("未设置游戏安装目录!");
return;
}
const gamePath = `${gameDir.value}${path.sep()}YuanShen.exe`;
if (!(await exists(gamePath))) {
const dirRead = await readDir(gameDir.value);
const find = dirRead.find((i) => i.isFile && i.name.toLowerCase() === "yuanshen.exe");
if (!find) {
showSnackbar.warn("未检测到原神本体应用!");
return;
}
const gamePath = `${gameDir.value}${path.sep()}${find.name}`;
const resp = await passportReq.authTicket(account.value, cookie.value);
if (typeof resp !== "string") {
showSnackbar.error(`[${resp.retcode}] ${resp.message}`);

View File

@@ -89,6 +89,7 @@ async function toDonate(): Promise<void> {
display: flex;
width: 100%;
flex-direction: column;
flex-shrink: 0;
align-items: flex-start;
justify-content: flex-start;
padding: 8px;

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

@@ -18,7 +18,7 @@
</div>
<div class="twc-bi-desc">{{ data.description }}</div>
</div>
<div class="twc-bi-grid1">
<div class="twc-bi-grid">
<div class="twc-big-item">
<span>{{ data.elePrefix }}</span>
<span>{{ data.element }}</span>
@@ -40,7 +40,7 @@
<span>{{ data.brief.birth }}</span>
</div>
</div>
<div class="twc-bi-grid2">
<div class="twc-bi-grid">
<div class="twc-big-item">
<span>汉语CV</span>
<span>{{ data.brief.cv.cn }}</span>
@@ -182,7 +182,7 @@ async function toBirth(date: string): Promise<void> {
await router.push({ name: "留影叙佳期", params: { date: birth } });
}
</script>
<style lang="css" scoped>
<style lang="scss" scoped>
:deep(.v-expansion-panel-title) {
background: var(--common-shadow-1);
}
@@ -191,7 +191,7 @@ async function toBirth(date: string): Promise<void> {
display: flex;
flex-direction: column;
margin: 0 auto;
row-gap: 10px;
row-gap: 8px;
}
.twc-brief {
@@ -201,7 +201,9 @@ async function toBirth(date: string): Promise<void> {
}
.twc-brief-info {
position: relative;
display: flex;
width: 100%;
flex-direction: column;
justify-content: space-between;
}
@@ -212,8 +214,10 @@ async function toBirth(date: string): Promise<void> {
}
.twc-bi-title {
position: relative;
display: flex;
width: fit-content;
flex-wrap: wrap;
align-items: center;
justify-content: center;
color: var(--common-text-title);
@@ -240,16 +244,12 @@ async function toBirth(date: string): Promise<void> {
opacity: 0.8;
}
.twc-bi-grid1 {
display: grid;
column-gap: 10px;
grid-template-columns: repeat(4, 1fr);
}
.twc-bi-grid2 {
display: grid;
column-gap: 10px;
grid-template-columns: repeat(2, 1fr);
.twc-bi-grid {
position: relative;
display: flex;
width: 100%;
flex-wrap: wrap;
column-gap: 16px;
}
.twc-big-item {

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

@@ -58,20 +58,19 @@ const mixData = computed<Array<TGApp.Sqlite.Gacha.Gacha>>(() =>
props.modelValue.filter((item) => item.uigfType === "500"),
);
</script>
<style lang="css" scoped>
<style lang="scss" scoped>
.gro-o-swiper {
--swiper-pagination-bottom: 16px;
--swiper-pagination-color: var(--tgc-pink-1);
--swiper-pagination-bullet-inactive-color: var(--tgc-od-white);
--swiper-pagination-bullet-inactive-opacity: 1;
width: 100%;
height: 100%;
column-gap: 8px;
}
/* swiper dot */
:deep(.swiper-pagination-bullet) {
background: var(--tgc-od-white);
}
:deep(.swiper-pagination-bullet-active) {
background-color: var(--tgc-pink-1);
box-shadow: 0 0 4px var(--common-shadow-4);
}
</style>

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

@@ -2,7 +2,8 @@
<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 +33,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;

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;

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;

View File

@@ -2,20 +2,20 @@
<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"
:key="index"
:model-value="item"
:class="{ selected: index === props.collection.cur - 1 }"
:model-value="item"
class="tpoc-item"
/>
</div>
</div>
@@ -26,6 +26,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 +65,10 @@ 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}`);
}
</script>
<style lang="scss" scoped>
.tpoc-box {
@@ -73,21 +78,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

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

@@ -111,7 +111,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 = "";

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,29 @@
<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
class="vp-ou-item"
:model-value="item"
v-for="item in results"
:key="item.post.post_id"
:model-value="item"
class="vp-ou-item"
/>
</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 +47,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 +63,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 +113,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 +164,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

@@ -549,7 +549,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,
@@ -684,11 +684,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

@@ -315,14 +315,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> {

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

@@ -6,10 +6,10 @@
<img alt="icon" src="/UI/nav/toolbox.webp" />
<span>实用脚本</span>
<v-select
v-model="curAccount"
:disabled="runScript || runAll"
:hide-details="true"
:items="accounts"
:model-value="curAccount"
class="us-top-select"
density="compact"
item-title="uid"
@@ -17,27 +17,47 @@
variant="outlined"
>
<template #selection="{ item }">
<div class="select-main">
<div class="us-select-main">
<img :src="item.brief.avatar" alt="icon" />
<div class="content">
<div class="us-sm-content">
<span>{{ item.brief.nickname }}</span>
<span>UID:{{ item.uid }}</span>
</div>
</div>
</template>
<template #item="{ props, item }">
<div class="select-item" v-bind="props">
<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="content">
<div class="us-si-content">
<span>{{ item.brief.nickname }}</span>
<span>UID:{{ item.uid }}</span>
</div>
</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 {
@@ -224,7 +273,7 @@ async function tryExecAll(): Promise<void> {
max-width: 250px;
}
.select-main {
.us-select-main {
position: relative;
display: flex;
height: 24px;
@@ -236,24 +285,24 @@ async function tryExecAll(): Promise<void> {
width: 24px;
height: 24px;
}
}
.content {
position: relative;
display: flex;
flex-direction: column;
.us-sm-content {
position: relative;
display: flex;
flex-direction: column;
:first-child {
font-family: var(--font-title);
font-size: 12px;
}
:first-child {
font-family: var(--font-title);
font-size: 12px;
}
:last-child {
font-size: 10px;
}
:last-child {
font-size: 10px;
}
}
.select-item {
.us-select-item {
position: relative;
display: flex;
width: 100%;
@@ -262,35 +311,44 @@ async function tryExecAll(): Promise<void> {
justify-content: flex-start;
padding: 8px;
column-gap: 4px;
cursor: pointer;
&.selected:not(:hover) {
background: var(--common-shadow-1);
}
&:hover {
background: var(--common-shadow-2);
}
img {
width: 24px;
height: 24px;
}
}
.content {
position: relative;
display: flex;
flex-direction: column;
.us-si-content {
position: relative;
display: flex;
flex-direction: column;
:first-child {
font-family: var(--font-title);
font-size: 12px;
}
:last-child {
font-size: 10px;
}
:first-child {
font-family: var(--font-title);
font-size: 12px;
}
.append {
display: flex;
align-items: center;
justify-content: center;
margin-left: auto;
:last-child {
font-size: 10px;
}
}
.us-si-append {
display: flex;
align-items: center;
justify-content: center;
margin-left: auto;
}
.top-hint {
position: relative;
padding: 8px;
@@ -324,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

@@ -139,7 +139,7 @@ async function toOuter(item?: TGApp.App.Character.WikiBriefInfo): Promise<void>
.wc-left {
position: relative;
display: flex;
width: fit-content;
flex: 3;
flex-direction: column;
flex-shrink: 0;
gap: 8px;
@@ -169,13 +169,14 @@ async function toOuter(item?: TGApp.App.Character.WikiBriefInfo): Promise<void>
width: 100%;
padding-right: 8px;
gap: 8px;
grid-template-columns: repeat(3, 160px);
grid-template-columns: repeat(auto-fill, minmax(144px, 1fr));
}
.wc-detail {
position: relative;
width: 100%;
box-sizing: border-box;
flex: 5;
padding: 8px;
border-radius: 4px;
box-shadow: 0 0 4px var(--common-shadow-2);

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

@@ -117,8 +117,9 @@ async function toOuter(item?: TGApp.App.Weapon.WikiBriefInfo): Promise<void> {
}
.ww-left {
position: relative;
display: flex;
width: fit-content;
flex: 3;
flex-direction: column;
flex-shrink: 0;
gap: 8px;
@@ -148,13 +149,14 @@ async function toOuter(item?: TGApp.App.Weapon.WikiBriefInfo): Promise<void> {
width: 100%;
padding-right: 8px;
gap: 8px;
grid-template-columns: repeat(3, 160px);
grid-template-columns: repeat(auto-fill, minmax(144px, 1fr));
}
.ww-detail {
position: relative;
width: 100%;
box-sizing: border-box;
flex: 5;
padding: 8px;
border-radius: 4px;
box-shadow: 0 0 4px var(--common-shadow-2);

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

@@ -4,7 +4,7 @@
<template #prepend>
<div class="pa-prepend">
<v-tabs v-model="tab" align-tabs="start" class="pa-tabs">
<v-tab v-for="tab in tabList" :key="tab.id" :value="tab.id" :title="tab.name">
<v-tab v-for="tab in tabList" :key="tab.id" :title="tab.name" :value="tab.id">
{{ tab.mi18n_name }}
</v-tab>
</v-tabs>
@@ -36,23 +36,16 @@
</template>
<template #append>
<div class="anno-top-append">
<v-btn
class="anno-btn"
prepend-icon="mdi-bullhorn"
rounded
variant="elevated"
@click="switchNews"
>
切换米游社资讯
</v-btn>
<v-btn
v-if="isLogin"
class="anno-btn"
prepend-icon="mdi-web"
rounded
size="small"
variant="elevated"
@click="showIframe()"
>
<v-icon>mdi-web</v-icon>
游戏内公告
</v-btn>
</div>
</template>
@@ -82,7 +75,6 @@ import useAppStore from "@store/app.js";
import TGLogger from "@utils/TGLogger.js";
import { storeToRefs } from "pinia";
import { onMounted, ref, shallowRef, watch } from "vue";
import { useRouter } from "vue-router";
type AnnoSelect<T = string> = { text: string; value: T };
@@ -92,7 +84,6 @@ const langList: ReadonlyArray<AnnoSelect<TGApp.Game.Anno.AnnoLangEnum>> =
gameEnum.anno.langList.map((i) => ({ text: gameEnum.anno.langDesc(i), value: i }));
const { server, lang, isLogin } = storeToRefs(useAppStore());
const router = useRouter();
const tab = ref<number>(0);
const tabList = shallowRef<Array<TGApp.Game.Anno.ListType>>([]);
@@ -158,11 +149,6 @@ async function loadData(): Promise<void> {
await showLoading.end();
isReq.value = false;
}
async function switchNews(): Promise<void> {
await TGLogger.Info("[Announcements][switchNews] 切换米游社资讯");
await router.push("/news/2");
}
</script>
<style lang="scss" scoped>
.pa-prepend {

View File

@@ -3,7 +3,7 @@
<v-app-bar>
<template #prepend>
<div class="pbm-nav-prepend">
<img alt="icon" src="/icon/material/121234.webp" />
<img alt="icon" src="/UI/nav/userBag.webp" />
<span>背包材料</span>
<v-select
v-model="curUid"
@@ -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

@@ -607,6 +607,7 @@ async function switchIncognito(): Promise<void> {
flex-direction: column;
flex-shrink: 0;
justify-content: flex-start;
padding-right: 4px;
row-gap: 8px;
}
</style>

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" />
</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

@@ -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,7 +97,7 @@
</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">
@@ -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,48 +13,37 @@
{{ 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>
<v-btn
v-if="gid === '2'"
class="post-news-btn"
prepend-icon="mdi-bullhorn"
rounded
variant="elevated"
@click="switchAnno"
>
切换游戏内公告
</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 class="pn-grid">
<div v-for="item in postData[value]" :key="item.post.post_id">
<TPostCard :model-value="item" />
</div>
@@ -62,7 +51,7 @@
</v-window-item>
</v-window>
<ToChannel v-model="showList" :gid="gid" />
<VpOverlaySearch v-model="showSearch" :gid="gid" :keyword="search" />
<VpOverlaySearch v-model="showSearch" :gid="Number(gid)" :keyword="search" />
</template>
<script lang="ts" setup>
import TPostCard from "@comp/app/t-postcard.vue";
@@ -78,14 +67,13 @@ import useBBSStore from "@store/bbs.js";
import TGLogger from "@utils/TGLogger.js";
import { createPost } from "@utils/TGWindow.js";
import { storeToRefs } from "pinia";
import { computed, onMounted, reactive, Ref, ref, shallowRef, watch } from "vue";
import { useRoute, useRouter } from "vue-router";
import { computed, onMounted, reactive, type Ref, ref, shallowRef, watch } from "vue";
import { useRoute } from "vue-router";
type PostData = Record<TGApp.BBS.Post.NewsTypeEnum, Ref<Array<TGApp.BBS.Post.FullData>>>;
type RawItem = { isLast: boolean; name: string; lastId: number };
type RawData = Record<TGApp.BBS.Post.NewsTypeEnum, Ref<RawItem>>;
const router = useRouter();
const { recentNewsType } = storeToRefs(useAppStore());
const { gameList } = storeToRefs(useBBSStore());
const { gid } = <{ gid: string }>useRoute().params;
@@ -159,11 +147,6 @@ async function firstLoad(refresh: boolean = false): Promise<void> {
loading.value = false;
}
async function switchAnno(): Promise<void> {
await TGLogger.Info(`[News][${gid}][switchAnno] 切换公告`);
await router.push("/announcements");
}
function handleList(): void {
if (showSearch.value === true) showSearch.value = false;
showList.value = true;
@@ -215,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

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,13 +1,20 @@
/**
* 游戏文件相关功能
* @since Beta v0.9.6
* @since Beta v0.9.8
*/
import showDialog from "@comp/func/dialog.js";
import showSnackbar from "@comp/func/snackbar.js";
import { invoke } from "@tauri-apps/api/core";
import { documentDir, resourceDir, sep } from "@tauri-apps/api/path";
import { copyFile, exists, mkdir, readTextFile, readTextFileLines } from "@tauri-apps/plugin-fs";
import {
copyFile,
exists,
mkdir,
readDir,
readTextFile,
readTextFileLines,
} from "@tauri-apps/plugin-fs";
import { platform } from "@tauri-apps/plugin-os";
import TGLogger from "@utils/TGLogger.js";
@@ -33,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;
}
}
@@ -97,11 +107,13 @@ export async function tryCallYae(gameDir: string, uid?: string): Promise<void> {
showSnackbar.warn("请前往设置页面设置游戏安装目录");
return;
}
const gamePath = `${gameDir}${sep()}YuanShen.exe`;
if (!(await exists(gamePath))) {
const dirRead = await readDir(gameDir);
const find = dirRead.find((i) => i.isFile && i.name.toLowerCase() === "yuanshen.exe");
if (!find) {
showSnackbar.warn("未检测到游戏本体");
return;
}
const gamePath = `${gameDir}${sep()}${find.name}`;
const isRun = await invoke<boolean>("is_process_running", { processName: "Yuanshen.exe" });
if (isRun) {
showSnackbar.warn("检测到已启动的原神进程请关闭进程Yuanshen.exe后重试");

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;