Compare commits

..

53 Commits

Author SHA1 Message Date
BTMuli
cc930912dc 🚀 v0.9.2 2026-01-18 21:17:55 +08:00
BTMuli
40cf7edb6a 🐛 修复构建异常x2 2026-01-18 21:17:17 +08:00
BTMuli
9b48df759a 🐛 修复构建异常 2026-01-18 20:49:07 +08:00
BTMuli
b8ff71d71c 🍱 更新元数据 2026-01-18 20:09:05 +08:00
BTMuli
24e46706ab 🐛 copy前创建目录
close #206
2026-01-18 19:27:14 +08:00
BTMuli
6cc025cfb2 💄 调整样式 2026-01-18 19:16:44 +08:00
BTMuli
f5372b8e05 🚸 修正hint 2026-01-18 17:44:02 +08:00
BTMuli
c1ce2def26 ♻️ msix环境下将dll移动到文档目录
#206
2026-01-18 17:30:04 +08:00
BTMuli
1bd2fa34d3 🚸 仅在搜索到结果时置空 2026-01-18 15:26:13 +08:00
BTMuli
9f7763afd9 ♻️ 调整成就排序&搜索逻辑 2026-01-18 01:05:17 +08:00
BTMuli
b136a93464 ♻️ 调整dll移动方式
#206
2026-01-18 00:02:08 +08:00
BTMuli
6db1ab0a45 🚸 添加用户反馈显示控制入口 2026-01-17 23:07:54 +08:00
BTMuli
83de5beff8 💄 统一顶部样式 2026-01-17 22:40:06 +08:00
BTMuli
9003921f23 💄 对月谕圣牌进行特殊处理 2026-01-17 22:36:30 +08:00
BTMuli
10000f4aba 🐛 修正Up判断逻辑 2026-01-17 21:56:56 +08:00
BTMuli
50201fbbc8 🚸 调整优先级
close #197
2026-01-17 19:52:32 +08:00
BTMuli
eaa61e665a 🐛 移动dll以解决路径问题
#206
2026-01-17 17:05:23 +08:00
BTMuli
3a08234a78 🎨 调整解析逻辑 2026-01-17 16:40:50 +08:00
BTMuli
728dfe45d3 尝试更新cookie
#197
2026-01-17 16:12:53 +08:00
BTMuli
484d95790d 🚸 优化扫码加载 2026-01-17 16:04:40 +08:00
BTMuli
f8933c7ca1 🧑‍💻 埋点分析
#206
2026-01-17 15:24:00 +08:00
BTMuli
75b6ba40e9 ♻️ 调整解析逻辑,修复部分数据解析异常 2026-01-17 14:02:20 +08:00
BTMuli
734b01706f 🎨 调整scss格式化 2026-01-17 12:39:15 +08:00
BTMuli
4302c179d5 💄 调整Hyperlink处理 2026-01-17 12:29:09 +08:00
BTMuli
4d9b456b9d 🔨 调整debug脚本 2026-01-17 12:20:24 +08:00
BTMuli
fb8a6fdc4c 🐛 循环获取快照以尝试解决dll寻找问题
#206
2026-01-17 11:53:06 +08:00
BTMuli
9c1c665964 🚸 微调处理 2026-01-17 11:28:02 +08:00
BTMuli
d966fb2f82 💄 调整容器高度,支持单楼层分享 2026-01-17 00:12:41 +08:00
BTMuli
422f6231c8 处理 t-link
close #156
2026-01-16 23:30:32 +08:00
BTMuli
a2f0a532a8 🧪 调整dll寻找逻辑 2026-01-16 20:38:54 +08:00
BTMuli
d9f24dccaf 🧑‍💻 完善配置 2026-01-16 20:22:29 +08:00
BTMuli
87eddb7e87 🧑‍💻 配置分平台 2026-01-16 20:15:16 +08:00
BTMuli
e3f3a038f4 🎨 code fmt 2026-01-16 19:39:27 +08:00
BTMuli
79ead78eaf 🌱 尝试适配linux
https://docs.gtk.org/gio/type_func.Application.id_is_valid.html
2026-01-16 19:38:33 +08:00
BTMuli
00381a092e 🐛 上传深渊记录时更新角色列表,以修复511001异常 2026-01-16 19:23:07 +08:00
BTMuli
5715030114 🐛 修复活动奖励点击异常 2026-01-16 13:38:20 +08:00
BTMuli
e269719e4f 🐛 修复特定情况下分享图生成时屏幕异常发白,支持单幕分享,调整怪物样式 2026-01-15 23:08:35 +08:00
BTMuli
2e4171cced 🚸 调整排序 2026-01-15 22:33:36 +08:00
BTMuli
5cd4b120f4 🚸 修正深色模式下页面刷新背景色的突变 2026-01-15 22:29:30 +08:00
BTMuli
d2b5fcd416 💄 添加胡桃Logo 2026-01-15 21:03:24 +08:00
BTMuli
7dcbd8204a ♻️ 合并导入 2026-01-15 20:58:55 +08:00
BTMuli
bd54e86f5b 🧑‍💻 设置Sentry用户 2026-01-15 20:04:12 +08:00
BTMuli
1facdb9cec 🚸 调整首页部分图片缓存策略 2026-01-15 13:01:44 +08:00
BTMuli
a95c9479cd 🎨 优化组件结构,降低dom渲染 2026-01-15 12:57:48 +08:00
BTMuli
859ddc3d8d 🚸 隐藏完成成就支持隐藏成就系列
close #205
2026-01-15 12:51:56 +08:00
BTMuli
7b5a57fd5c 💄 调整forum背景计算 2026-01-15 12:11:47 +08:00
BTMuli
ed4adf20e9 🐛 尝试修复 Document is not focused. 2026-01-14 23:17:07 +08:00
BTMuli
21315cab58 🥅 当无法读取注册表时返回1.0 2026-01-14 23:09:51 +08:00
BTMuli
1975b989e0 🍱 增加旅行者衣装 2026-01-14 20:47:25 +08:00
BTMuli
fd2e80f0b5 🍱 增加璃月港阵营 2026-01-14 17:10:31 +08:00
BTMuli
882ea9b071 🚸 降低请求次数 2026-01-14 16:38:53 +08:00
BTMuli
2847042933 🚸 随机loading 2026-01-14 13:11:30 +08:00
BTMuli
aaf30d0df5 🐛 修复检测数据更新异常 2026-01-14 12:39:39 +08:00
96 changed files with 3199 additions and 1640 deletions

View File

@@ -1,3 +1,3 @@
VITE_SENTRY_RELEASE=TeyvatGuide@0.9.1
VITE_COMMIT_HASH=d895d162
VITE_BUILD_TIME=1768321217
VITE_SENTRY_RELEASE=TeyvatGuide@0.9.2
VITE_COMMIT_HASH=40cf7edb
VITE_BUILD_TIME=1768742260

View File

@@ -46,8 +46,11 @@ jobs:
- name: Test SSH connection
run: ssh -T git@github.com || true
- name: Load env.production
id: env
if: matrix.settings.target == 'windows'
run: |
echo "VITE_SENTRY_RELEASE=$(grep VITE_SENTRY_RELEASE .env.production | cut -d '=' -f2)" >> $GITHUB_ENV
$VITE_SENTRY_RELEASE = Get-Content .env.production | Where-Object { $_ -match '^VITE_SENTRY_RELEASE=' } | ForEach-Object { ($_ -split '=')[1] }
"VITE_SENTRY_RELEASE=$VITE_SENTRY_RELEASE" >> $env:GITHUB_OUTPUT
- name: Rust setup
uses: dtolnay/rust-toolchain@stable
- name: Rust cache
@@ -107,4 +110,4 @@ jobs:
sentry-cli releases finalize "$SENTRY_RELEASE"
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_RELEASE: ${{ env.VITE_SENTRY_RELEASE }}
SENTRY_RELEASE: ${{ steps.env.outputs.VITE_SENTRY_RELEASE }}

View File

@@ -37,7 +37,7 @@ rules:
prettier/prettier: true
property-no-vendor-prefix:
- true
- ignoreProperties: [backdrop-filter]
- ignoreProperties: [-webkit-backdrop-filter]
rule-empty-line-before:
- always
- except: ["first-nested"]

View File

@@ -2,12 +2,32 @@
Author: 目棃
Description: CHANGELOG
Date: 2025-09-09
Update: 2026-01-14
Update: 2026-01-18
---
> 本文档 [`Frontmatter`](https://github.com/BTMuli/MuCli#Frontmatter) 由 [MuCli](https://github.com/BTMuli/Mucli) 自动生成于 `2025-09-09 14:30:56`
>
> 更新于 `2026-01-14 00:08:50`
> 更新于 `2026-01-18 20:35:21`
## [0.9.2](https://github.com/BTMuli/TeyvatGuide/releases/v0.9.2) (2025-01-18)
- 🍱 增加旅行者衣装相关资源
- ✨ WIKI新增 `{LINK#xx}{/LINK}` 数据支持 [`#156`](https://github.com/BTMuli/TeyvatGuide/issues/156)
- ✨ 自动更新 Cookie [`#197`](https://github.com/BTMuli/TeyvatGuide/issues/197)
- 🐛 修复祈愿页面检测数据更新异常
- 🐛 修复特定情况下生成剧诗分享图时应用白屏
- 🐛 修复首页活动奖励点击异常
- 🐛 上传深渊记录时更新角色列表,以修复 `511001` 异常
- 🐛 调整五星 UP 判断逻辑,修复特定数据 UP 判断异常
- 🐛 修复微软应用商店版本材料&成就导入异常 [`#206`](https://github.com/BTMuli/TeyvatGuide/issues/206)
- 🚸 `loading` 组件随机加载图标
- 🚸 隐藏完成成就支持隐藏成就系列 [`#205`](https://github.com/BTMuli/TeyvatGuide/issues/205)
- 🚸 调整首页部分图片缓存策略
- 🚸 调整成就排序&搜索逻辑
- 🚸 添加用户反馈显示控制入口
- 🥅 修复文本放缩比读取异常,注册表不存在时返回 1.0
- ♻️ 祈愿页面导入功能合并,仅显示一个导入按钮
- 💄 深渊支持单楼层分享,剧诗支持单幕分享
## [0.9.1](https://github.com/BTMuli/TeyvatGuide/releases/v0.9.1) (2025-01-14)

View File

@@ -1,13 +1,13 @@
{
"name": "teyvatguide",
"version": "0.9.1",
"version": "0.9.2",
"description": "Game Tool for GenshinImpact player",
"private": true,
"packageManager": "pnpm@10.28.0",
"type": "module",
"scripts": {
"build": "tsx scripts/auto-build.ts",
"debug": "tauri build --debug",
"debug": "tsx scripts/auto-build.ts su --debug",
"dev": "tsx scripts/auto-dev.ts",
"eslint:pre": "pnpx @eslint/config-inspector@latest",
"oxlint": "oxlint",
@@ -72,15 +72,15 @@
"dependencies": {
"@date-fns/tz": "^1.4.1",
"@mdi/font": "7.4.47",
"@sentry/vite-plugin": "^4.6.1",
"@sentry/vue": "^10.32.1",
"@sentry/vite-plugin": "^4.6.2",
"@sentry/vue": "^10.34.0",
"@skipperndt/plugin-machine-uid": "^0.1.3",
"@tauri-apps/api": "^2.9.1",
"@tauri-apps/plugin-cli": "^2.4.1",
"@tauri-apps/plugin-deep-link": "^2.4.6",
"@tauri-apps/plugin-dialog": "^2.5.0",
"@tauri-apps/plugin-dialog": "^2.6.0",
"@tauri-apps/plugin-fs": "^2.4.5",
"@tauri-apps/plugin-http": "^2.5.5",
"@tauri-apps/plugin-http": "^2.5.6",
"@tauri-apps/plugin-log": "^2.8.0",
"@tauri-apps/plugin-notification": "^2.3.3",
"@tauri-apps/plugin-opener": "^2.5.3",
@@ -115,14 +115,14 @@
"@eslint/eslintrc": "^3.3.3",
"@eslint/js": "^9.39.2",
"@microsoft/tsdoc": "^0.16.0",
"@sentry/core": "^10.32.1",
"@sentry/core": "^10.34.0",
"@tauri-apps/cli": "2.9.6",
"@types/fs-extra": "^11.0.4",
"@types/js-md5": "^0.8.0",
"@types/json-bigint": "^1.0.4",
"@types/node": "^25.0.6",
"@typescript-eslint/parser": "^8.52.0",
"@typescript/native-preview": "7.0.0-dev.20260109.1",
"@types/node": "^25.0.9",
"@typescript-eslint/parser": "^8.53.0",
"@typescript/native-preview": "7.0.0-dev.20260116.1",
"@vitejs/plugin-vue": "^6.0.3",
"app-root-path": "^3.1.0",
"concurrently": "^9.2.1",
@@ -130,30 +130,30 @@
"eslint": "^9.39.2",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-jsonc": "^2.21.0",
"eslint-plugin-prettier": "^5.5.4",
"eslint-plugin-prettier": "^5.5.5",
"eslint-plugin-tsdoc": "^0.5.0",
"eslint-plugin-vue": "^10.6.2",
"eslint-plugin-vue": "^10.7.0",
"eslint-plugin-yml": "^1.19.1",
"fs-extra": "^11.3.3",
"globals": "^17.0.0",
"husky": "^9.1.7",
"jsonc-eslint-parser": "^2.4.2",
"lint-staged": "^16.2.7",
"oxlint": "^1.38.0",
"postcss-preset-env": "^10.6.1",
"prettier": "3.7.4",
"stylelint": "^16.26.1",
"oxlint": "^1.39.0",
"postcss-preset-env": "^11.1.1",
"prettier": "3.8.0",
"stylelint": "^17.0.0",
"stylelint-config-idiomatic-order": "^10.0.0",
"stylelint-config-standard-scss": "^16.0.0",
"stylelint-config-standard-scss": "^17.0.0",
"stylelint-config-standard-vue": "^1.0.0",
"stylelint-declaration-block-no-ignored-properties": "^2.8.0",
"stylelint-high-performance-animation": "^1.11.0",
"stylelint-order": "^7.0.1",
"stylelint-prettier": "^5.0.3",
"stylelint-scss": "^6.14.0",
"stylelint-scss": "^7.0.0",
"tsx": "^4.21.0",
"typescript": "^5.9.3",
"typescript-eslint": "^8.52.0",
"typescript-eslint": "^8.53.0",
"vite": "npm:rolldown-vite@^7.3.1",
"vite-plugin-vue-devtools": "^8.0.5",
"vite-plugin-vuetify": "^2.1.2",

2323
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 14 KiB

109
src-tauri/Cargo.lock generated
View File

@@ -10,7 +10,7 @@ checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3"
[[package]]
name = "TeyvatGuide"
version = "0.9.1"
version = "0.9.2"
dependencies = [
"chrono",
"image",
@@ -949,9 +949,9 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "chrono"
version = "0.4.42"
version = "0.4.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2"
checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118"
dependencies = [
"iana-time-zone",
"js-sys",
@@ -2760,8 +2760,8 @@ dependencies = [
"rayon",
"rgb",
"tiff",
"zune-core 0.5.0",
"zune-jpeg 0.5.8",
"zune-core 0.5.1",
"zune-jpeg 0.5.9",
]
[[package]]
@@ -2942,9 +2942,9 @@ dependencies = [
[[package]]
name = "js-sys"
version = "0.3.83"
version = "0.3.85"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8"
checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3"
dependencies = [
"once_cell",
"wasm-bindgen",
@@ -4539,7 +4539,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
dependencies = [
"rand_chacha 0.9.0",
"rand_core 0.9.4",
"rand_core 0.9.5",
]
[[package]]
@@ -4569,7 +4569,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
dependencies = [
"ppv-lite86",
"rand_core 0.9.4",
"rand_core 0.9.5",
]
[[package]]
@@ -4592,9 +4592,9 @@ dependencies = [
[[package]]
name = "rand_core"
version = "0.9.4"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f1b3bc831f92381018fd9c6350b917c7b21f1eed35a65a51900e0e55a3d7afa"
checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
dependencies = [
"getrandom 0.3.4",
]
@@ -4941,9 +4941,9 @@ dependencies = [
[[package]]
name = "rust_decimal"
version = "1.39.0"
version = "1.40.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35affe401787a9bd846712274d97654355d21b2a2c092a3139aabe31e9022282"
checksum = "61f703d19852dbf87cbc513643fa81428361eb6940f1ac14fd58155d295a3eb0"
dependencies = [
"arrayvec",
"borsh",
@@ -4957,9 +4957,9 @@ dependencies = [
[[package]]
name = "rustc-demangle"
version = "0.1.26"
version = "0.1.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace"
checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d"
[[package]]
name = "rustc-hash"
@@ -5005,9 +5005,9 @@ dependencies = [
[[package]]
name = "rustls-pki-types"
version = "1.13.2"
version = "1.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21e6f2ab2928ca4291b86736a8bd920a277a399bba1589409d72154ff87c1282"
checksum = "4910321ebe4151be888e35fe062169554e74aad01beafed60410131420ceffbc"
dependencies = [
"web-time",
"zeroize",
@@ -6226,7 +6226,7 @@ dependencies = [
[[package]]
name = "tauri-plugin-cli"
version = "2.4.1"
source = "git+ssh://git@github.com/tauri-apps/plugins-workspace.git?branch=v2#f3d75f7abb536ba70d1746801096af6d9be7f8b1"
source = "git+ssh://git@github.com/tauri-apps/plugins-workspace.git?branch=v2#05c5da072b6e3254c19ee69024f80f4dfe1b779b"
dependencies = [
"clap",
"log",
@@ -6240,7 +6240,7 @@ dependencies = [
[[package]]
name = "tauri-plugin-deep-link"
version = "2.4.6"
source = "git+ssh://git@github.com/tauri-apps/plugins-workspace.git?branch=v2#f3d75f7abb536ba70d1746801096af6d9be7f8b1"
source = "git+ssh://git@github.com/tauri-apps/plugins-workspace.git?branch=v2#05c5da072b6e3254c19ee69024f80f4dfe1b779b"
dependencies = [
"dunce",
"plist",
@@ -6259,8 +6259,8 @@ dependencies = [
[[package]]
name = "tauri-plugin-dialog"
version = "2.5.0"
source = "git+ssh://git@github.com/tauri-apps/plugins-workspace.git?branch=v2#f3d75f7abb536ba70d1746801096af6d9be7f8b1"
version = "2.6.0"
source = "git+ssh://git@github.com/tauri-apps/plugins-workspace.git?branch=v2#05c5da072b6e3254c19ee69024f80f4dfe1b779b"
dependencies = [
"log",
"raw-window-handle",
@@ -6277,7 +6277,7 @@ dependencies = [
[[package]]
name = "tauri-plugin-fs"
version = "2.4.5"
source = "git+ssh://git@github.com/tauri-apps/plugins-workspace.git?branch=v2#f3d75f7abb536ba70d1746801096af6d9be7f8b1"
source = "git+ssh://git@github.com/tauri-apps/plugins-workspace.git?branch=v2#05c5da072b6e3254c19ee69024f80f4dfe1b779b"
dependencies = [
"anyhow",
"dunce",
@@ -6297,8 +6297,8 @@ dependencies = [
[[package]]
name = "tauri-plugin-http"
version = "2.5.5"
source = "git+ssh://git@github.com/tauri-apps/plugins-workspace.git?branch=v2#f3d75f7abb536ba70d1746801096af6d9be7f8b1"
version = "2.5.6"
source = "git+ssh://git@github.com/tauri-apps/plugins-workspace.git?branch=v2#05c5da072b6e3254c19ee69024f80f4dfe1b779b"
dependencies = [
"bytes",
"cookie_store 0.21.1",
@@ -6321,7 +6321,7 @@ dependencies = [
[[package]]
name = "tauri-plugin-log"
version = "2.8.0"
source = "git+ssh://git@github.com/tauri-apps/plugins-workspace.git?branch=v2#f3d75f7abb536ba70d1746801096af6d9be7f8b1"
source = "git+ssh://git@github.com/tauri-apps/plugins-workspace.git?branch=v2#05c5da072b6e3254c19ee69024f80f4dfe1b779b"
dependencies = [
"android_logger",
"byte-unit",
@@ -6357,7 +6357,7 @@ dependencies = [
[[package]]
name = "tauri-plugin-notification"
version = "2.3.3"
source = "git+ssh://git@github.com/tauri-apps/plugins-workspace.git?branch=v2#f3d75f7abb536ba70d1746801096af6d9be7f8b1"
source = "git+ssh://git@github.com/tauri-apps/plugins-workspace.git?branch=v2#05c5da072b6e3254c19ee69024f80f4dfe1b779b"
dependencies = [
"log",
"notify-rust",
@@ -6375,7 +6375,7 @@ dependencies = [
[[package]]
name = "tauri-plugin-opener"
version = "2.5.3"
source = "git+ssh://git@github.com/tauri-apps/plugins-workspace.git?branch=v2#f3d75f7abb536ba70d1746801096af6d9be7f8b1"
source = "git+ssh://git@github.com/tauri-apps/plugins-workspace.git?branch=v2#05c5da072b6e3254c19ee69024f80f4dfe1b779b"
dependencies = [
"dunce",
"glob",
@@ -6396,7 +6396,7 @@ dependencies = [
[[package]]
name = "tauri-plugin-os"
version = "2.3.2"
source = "git+ssh://git@github.com/tauri-apps/plugins-workspace.git?branch=v2#f3d75f7abb536ba70d1746801096af6d9be7f8b1"
source = "git+ssh://git@github.com/tauri-apps/plugins-workspace.git?branch=v2#05c5da072b6e3254c19ee69024f80f4dfe1b779b"
dependencies = [
"gethostname",
"log",
@@ -6413,7 +6413,7 @@ dependencies = [
[[package]]
name = "tauri-plugin-process"
version = "2.3.1"
source = "git+ssh://git@github.com/tauri-apps/plugins-workspace.git?branch=v2#f3d75f7abb536ba70d1746801096af6d9be7f8b1"
source = "git+ssh://git@github.com/tauri-apps/plugins-workspace.git?branch=v2#05c5da072b6e3254c19ee69024f80f4dfe1b779b"
dependencies = [
"tauri",
"tauri-plugin",
@@ -6422,7 +6422,7 @@ dependencies = [
[[package]]
name = "tauri-plugin-single-instance"
version = "2.3.7"
source = "git+ssh://git@github.com/tauri-apps/plugins-workspace.git?branch=v2#f3d75f7abb536ba70d1746801096af6d9be7f8b1"
source = "git+ssh://git@github.com/tauri-apps/plugins-workspace.git?branch=v2#05c5da072b6e3254c19ee69024f80f4dfe1b779b"
dependencies = [
"serde",
"serde_json",
@@ -6436,7 +6436,7 @@ dependencies = [
[[package]]
name = "tauri-plugin-sql"
version = "2.3.1"
source = "git+ssh://git@github.com/tauri-apps/plugins-workspace.git?branch=v2#f3d75f7abb536ba70d1746801096af6d9be7f8b1"
source = "git+ssh://git@github.com/tauri-apps/plugins-workspace.git?branch=v2#05c5da072b6e3254c19ee69024f80f4dfe1b779b"
dependencies = [
"futures-core",
"indexmap 2.13.0",
@@ -7309,9 +7309,9 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "wasip2"
version = "1.0.1+wasi-0.2.4"
version = "1.0.2+wasi-0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7"
checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5"
dependencies = [
"wit-bindgen",
]
@@ -7324,9 +7324,9 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b"
[[package]]
name = "wasm-bindgen"
version = "0.2.106"
version = "0.2.108"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd"
checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566"
dependencies = [
"cfg-if",
"once_cell",
@@ -7337,11 +7337,12 @@ dependencies = [
[[package]]
name = "wasm-bindgen-futures"
version = "0.4.56"
version = "0.4.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c"
checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f"
dependencies = [
"cfg-if",
"futures-util",
"js-sys",
"once_cell",
"wasm-bindgen",
@@ -7350,9 +7351,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.106"
version = "0.2.108"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3"
checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@@ -7360,9 +7361,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.106"
version = "0.2.108"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40"
checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55"
dependencies = [
"bumpalo",
"proc-macro2",
@@ -7373,9 +7374,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.106"
version = "0.2.108"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4"
checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12"
dependencies = [
"unicode-ident",
]
@@ -7395,9 +7396,9 @@ dependencies = [
[[package]]
name = "web-sys"
version = "0.3.83"
version = "0.3.85"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac"
checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598"
dependencies = [
"js-sys",
"wasm-bindgen",
@@ -8085,9 +8086,9 @@ dependencies = [
[[package]]
name = "wit-bindgen"
version = "0.46.0"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
[[package]]
name = "writeable"
@@ -8342,9 +8343,9 @@ dependencies = [
[[package]]
name = "zmij"
version = "1.0.13"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac93432f5b761b22864c774aac244fa5c0fd877678a4c37ebf6cf42208f9c9ec"
checksum = "bd8f3f50b848df28f887acb68e41201b5aea6bc8a8dacc00fb40635ff9a72fea"
[[package]]
name = "zune-core"
@@ -8354,9 +8355,9 @@ checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a"
[[package]]
name = "zune-core"
version = "0.5.0"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "111f7d9820f05fd715df3144e254d6fc02ee4088b0644c0ffd0efc9e6d9d2773"
checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9"
[[package]]
name = "zune-inflate"
@@ -8378,11 +8379,11 @@ dependencies = [
[[package]]
name = "zune-jpeg"
version = "0.5.8"
version = "0.5.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e35aee689668bf9bd6f6f3a6c60bb29ba1244b3b43adfd50edd554a371da37d5"
checksum = "87c86acb70a85b2c16f071f171847d1945e8f44812630463cd14ec83900ad01c"
dependencies = [
"zune-core 0.5.0",
"zune-core 0.5.1",
]
[[package]]

View File

@@ -1,11 +1,11 @@
[package]
name = "TeyvatGuide"
version = "0.9.1"
version = "0.9.2"
description = "Game Tool for Genshin Impact player"
authors = ["BTMuli <bt-muli@outlook.com>"]
license = "MIT"
repository = "https://github.com/BTMuli/TeyvatGuide"
edition = "2021"
edition = "2024"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -20,7 +20,7 @@ crate-type = ["staticlib", "cdylib", "rlib"]
tauri-build = { version = "2.5.3", features = [] }
[dependencies]
chrono = "0.4.42"
chrono = "0.4.43"
image = "0.25.9"
log = "0.4.29"
prost = "=0.14.1"
@@ -48,6 +48,7 @@ features = [
"Win32_System_Memory",
"Win32_System_Pipes",
"Win32_System_Threading",
"Win32_Storage_Packaging_Appx",
"Win32_System_WindowsProgramming",
]

View File

@@ -37,5 +37,5 @@
]
}
],
"platforms": ["windows", "macOS"]
"platforms": ["windows", "macOS", "linux"]
}

View File

@@ -34,5 +34,5 @@
"remote": {
"urls": ["https://*.mihoyo.com/*", "https://*.miyoushe.com/*", "https://*.genshinnet.com/*"]
},
"platforms": ["windows", "macOS"]
"platforms": ["windows", "macOS", "linux"]
}

View File

@@ -54,5 +54,5 @@
]
}
],
"platforms": ["windows", "macOS"]
"platforms": ["windows", "macOS", "linux"]
}

View File

@@ -0,0 +1,65 @@
{
"$schema": "./schemas/desktop-schema.json",
"identifier": "Teyvat.Guide",
"description": "Capability for the main window",
"windows": ["TeyvatGuide"],
"permissions": [
"core:app:default",
"core:app:allow-version",
"core:event:default",
"core:event:allow-listen",
"core:path:default",
"core:path:allow-resolve-directory",
"core:webview:default",
"core:webview:allow-create-webview-window",
"core:webview:allow-set-webview-zoom",
"core:window:default",
"core:window:allow-center",
"core:window:allow-close",
"core:window:allow-destroy",
"core:window:allow-is-minimized",
"core:window:allow-set-focus",
"core:window:allow-set-size",
"core:window:allow-set-title",
"core:window:allow-show",
"core:window:allow-unminimize",
"cli:default",
"cli:allow-cli-matches",
"dialog:default",
"dialog:allow-save",
"fs:default",
"http:allow-fetch",
"log:default",
"log:allow-log",
"machine-uid:default",
"machine-uid:allow-get-machine-uid",
"notification:default",
"opener:default",
"process:default",
"process:allow-exit",
"sql:default",
"sql:allow-load",
"sql:allow-execute",
{ "identifier": "fs:allow-exists", "allow": [{ "path": "**" }] },
{ "identifier": "fs:allow-mkdir", "allow": [{ "path": "**" }] },
{ "identifier": "fs:allow-read-dir", "allow": [{ "path": "**" }] },
{ "identifier": "fs:allow-read-text-file", "allow": [{ "path": "**" }] },
{ "identifier": "fs:allow-read-text-file-lines", "allow": [{ "path": "**" }] },
{ "identifier": "fs:allow-remove", "allow": [{ "path": "**" }] },
{ "identifier": "fs:allow-write-file", "allow": [{ "path": "**" }] },
{ "identifier": "fs:allow-write-text-file", "allow": [{ "path": "**" }] },
{ "identifier": "opener:allow-open-path", "allow": [{ "path": "**" }] },
{ "identifier": "opener:allow-open-url", "allow": [{ "url": "**" }] },
{
"identifier": "http:default",
"allow": [
{ "url": "https://*.miyoushe.com/*" },
{ "url": "https://*.mihoyo.com/*" },
{ "url": "https://homa.gentle.house/*" },
{ "url": "https://*.hoyoverse.com/*" },
{ "url": "https://api.hakush.in/*" }
]
}
],
"platforms": ["linux"]
}

View File

@@ -46,6 +46,7 @@
{ "identifier": "fs:allow-read-text-file", "allow": [{ "path": "**" }] },
{ "identifier": "fs:allow-read-text-file-lines", "allow": [{ "path": "**" }] },
{ "identifier": "fs:allow-remove", "allow": [{ "path": "**" }] },
{ "identifier": "fs:allow-copy-file", "allow": [{ "path": "**" }] },
{ "identifier": "fs:allow-write-file", "allow": [{ "path": "**" }] },
{ "identifier": "fs:allow-write-text-file", "allow": [{ "path": "**" }] },
{ "identifier": "opener:allow-open-path", "allow": [{ "path": "**" }] },

View File

@@ -4,6 +4,6 @@
max_width = 100
tab_spaces = 2
edition = "2018"
edition = "2024"
use_small_heuristics = "Max"
newline_style = "Auto"

View File

@@ -82,7 +82,7 @@ pub fn is_in_admin() -> bool {
{
use windows_sys::Win32::Foundation::{CloseHandle, HANDLE};
use windows_sys::Win32::Security::{
GetTokenInformation, TokenElevation, TOKEN_ELEVATION, TOKEN_QUERY,
GetTokenInformation, TOKEN_ELEVATION, TOKEN_QUERY, TokenElevation,
};
use windows_sys::Win32::System::Threading::{GetCurrentProcess, OpenProcessToken};
@@ -186,3 +186,35 @@ pub fn launch_game(path: String, ticket: String) -> Result<(), String> {
Err("This command is only supported on Windows".into())
}
}
#[tauri::command]
pub fn is_msix() -> bool {
#[cfg(not(windows))]
{
false
}
#[cfg(windows)]
{
use std::ptr;
use widestring::U16CStr;
use windows_sys::Win32::Foundation::ERROR_INSUFFICIENT_BUFFER;
use windows_sys::Win32::Storage::Packaging::Appx::GetCurrentPackageFullName;
unsafe {
let mut length: u32 = 0;
let result = GetCurrentPackageFullName(&mut length, ptr::null_mut());
if result != ERROR_INSUFFICIENT_BUFFER {
println!("Not running in MSIX package. Error code: {}", result);
return false;
}
let mut buffer = vec![0u16; length as usize];
let result = GetCurrentPackageFullName(&mut length, buffer.as_mut_ptr());
if result != 0 {
println!("Failed to retrieve package full name. Error code: {}", result);
return false;
}
let pkg_name = U16CStr::from_ptr_str(buffer.as_ptr());
println!("MSIX Package Full Name: {}", pkg_name.to_string_lossy());
true
}
}
}

View File

@@ -13,10 +13,10 @@ mod yae;
use crate::client::create_mhy_client;
use crate::commands::{
create_window, execute_js, get_dir_size, hide_main_window, init_app, is_in_admin, launch_game,
quit_app, read_text_scale,
create_window, execute_js, get_dir_size, hide_main_window, init_app, is_in_admin, is_msix,
launch_game, quit_app, read_text_scale,
};
use tauri::{generate_context, generate_handler, Emitter, Manager, Window, WindowEvent};
use tauri::{Emitter, Manager, Window, WindowEvent, generate_context, generate_handler};
// 子窗口 label 的数组
pub const SUB_WINDOW_LABELS: [&str; 3] = ["Sub_window", "Dev_JSON", "mhy_client"];
@@ -109,6 +109,7 @@ pub fn run() {
quit_app,
read_text_scale,
launch_game,
is_msix,
#[cfg(target_os = "windows")]
yae::call_yae_dll,
#[cfg(target_os = "windows")]

View File

@@ -1,5 +1,5 @@
// 主模块,用于启动应用
// @since Beta v0.9.1
// @since Beta v0.9.2
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
@@ -7,7 +7,7 @@
#[cfg(target_os = "windows")]
fn enable_dpi_v2() {
use windows_sys::Win32::UI::HiDpi::{
SetProcessDpiAwarenessContext, DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2,
DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2, SetProcessDpiAwarenessContext,
};
unsafe {
@@ -22,8 +22,14 @@ fn main() {
release: sentry::release_name!().into(),
send_default_pii: true,
..Default::default()
}));
},
));
#[cfg(target_os = "windows")]
enable_dpi_v2();
#[cfg(target_os = "linux")]
unsafe {
// Not unsafe if you don't use edition 2024
std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1");
}
teyvat_guide_lib::run()
}

View File

@@ -4,8 +4,8 @@
use chrono::DateTime;
use log::LevelFilter;
use std::time::SystemTime;
use tauri::plugin::TauriPlugin;
use tauri::Runtime;
use tauri::plugin::TauriPlugin;
use tauri_plugin_log::{Builder, Target, TargetKind, TimezoneStrategy};
// 获取当前日期 yyyy-mm-dd

View File

@@ -6,8 +6,8 @@ use tauri::{AppHandle, Emitter};
use widestring::U16CString;
use windows_sys::Win32::Foundation::ERROR_SUCCESS;
use windows_sys::Win32::System::Registry::{
RegNotifyChangeKeyValue, RegOpenKeyExW, HKEY, HKEY_CURRENT_USER, KEY_NOTIFY, KEY_READ,
REG_NOTIFY_CHANGE_LAST_SET,
HKEY, HKEY_CURRENT_USER, KEY_NOTIFY, KEY_READ, REG_NOTIFY_CHANGE_LAST_SET,
RegNotifyChangeKeyValue, RegOpenKeyExW,
};
pub fn init(app: AppHandle) {

View File

@@ -1,5 +1,5 @@
// 杂项
// @since Beta v0.9.1
// @since Beta v0.9.2
/// 获取当前系统的文本缩放比例TextScaleFactor
/// 返回值示例1.0 表示 100%1.25 表示 125%
@@ -10,13 +10,17 @@ pub fn read_text_scale_factor() -> Result<f64, String> {
}
#[cfg(target_os = "windows")]
{
use winreg::enums::*;
use winreg::RegKey;
use winreg::enums::*;
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
let key = hkcu
.open_subkey("Software\\Microsoft\\Accessibility")
.map_err(|e| format!("无法打开注册表: {}", e))?;
// 如果打开失败,直接返回默认值 1.0
let key = match hkcu.open_subkey("Software\\Microsoft\\Accessibility") {
Ok(k) => k,
Err(e) => {
log::error!("无法打开注册表: {}", e);
return Ok(1.0);
}
};
let value: u32 = key.get_value("TextScaleFactor").unwrap_or(100u32); // 默认值为 100%
Ok(value as f64 / 100.0)

View File

@@ -8,7 +8,7 @@ use tauri::AppHandle;
use widestring::U16CString;
use windows_sys::Win32::Foundation::{HANDLE, HWND};
use windows_sys::Win32::Storage::FileSystem::SYNCHRONIZE;
use windows_sys::Win32::System::Threading::{OpenProcess, WaitForSingleObject, INFINITE};
use windows_sys::Win32::System::Threading::{INFINITE, OpenProcess, WaitForSingleObject};
use windows_sys::Win32::UI::Shell::ShellExecuteW;
use windows_sys::Win32::UI::WindowsAndMessaging::SW_SHOWNORMAL;
@@ -37,11 +37,7 @@ fn shell_runas_with_args(args: &str) -> Result<(), String> {
if cwd_wide.len() > 1 { cwd_wide.as_ptr() } else { null_mut() },
SW_SHOWNORMAL,
);
if (result as usize) > 32 {
Ok(())
} else {
Err("ShellExecuteW runas 启动失败".into())
}
if (result as usize) > 32 { Ok(()) } else { Err("ShellExecuteW runas 启动失败".into()) }
}
}

View File

@@ -1,26 +1,31 @@
//! DLL 注入相关功能
//! @since Beta v0.9.1
//! @since Beta v0.9.2
#![cfg(target_os = "windows")]
use std::ptr;
use std::thread::sleep;
use std::time::Duration;
use widestring::U16CString;
use windows_sys::Win32::Foundation::{CloseHandle, FreeLibrary, HANDLE, INVALID_HANDLE_VALUE};
use windows_sys::Win32::Foundation::{
CloseHandle, ERROR_BAD_LENGTH, ERROR_SUCCESS, FreeLibrary, GetLastError, HANDLE,
INVALID_HANDLE_VALUE, SetLastError,
};
use windows_sys::Win32::Storage::FileSystem::PIPE_ACCESS_DUPLEX;
use windows_sys::Win32::System::Diagnostics::Debug::WriteProcessMemory;
use windows_sys::Win32::System::Diagnostics::ToolHelp::{
CreateToolhelp32Snapshot, Module32FirstW, Module32NextW, MODULEENTRY32W, TH32CS_SNAPMODULE,
CreateToolhelp32Snapshot, MODULEENTRY32W, Module32FirstW, Module32NextW, TH32CS_SNAPMODULE,
TH32CS_SNAPMODULE32,
};
use windows_sys::Win32::System::LibraryLoader::{
GetModuleHandleA, GetProcAddress, LoadLibraryExW, DONT_RESOLVE_DLL_REFERENCES,
DONT_RESOLVE_DLL_REFERENCES, GetModuleHandleA, GetProcAddress, LoadLibraryExW,
};
use windows_sys::Win32::System::Memory::{VirtualAllocEx, MEM_COMMIT, PAGE_READWRITE};
use windows_sys::Win32::System::Memory::{MEM_COMMIT, PAGE_READWRITE, VirtualAllocEx};
use windows_sys::Win32::System::Pipes::{
CreateNamedPipeW, PIPE_READMODE_MESSAGE, PIPE_TYPE_MESSAGE, PIPE_UNLIMITED_INSTANCES, PIPE_WAIT,
};
use windows_sys::Win32::System::Threading::{
CreateProcessW, CreateRemoteThread, WaitForSingleObject, INFINITE, PROCESS_INFORMATION,
STARTUPINFOW,
CreateProcessW, CreateRemoteThread, INFINITE, PROCESS_INFORMATION, STARTUPINFOW,
WaitForSingleObject,
};
/// 创建命名管道
@@ -133,38 +138,77 @@ pub fn inject_dll(pi: &PROCESS_INFORMATION, dll_path: &str) {
}
}
fn utf16_to_string(buf: &[u16]) -> String {
let nul_pos = buf.iter().position(|&c| c == 0).unwrap_or(buf.len());
String::from_utf16_lossy(&buf[..nul_pos])
struct Snapshot(HANDLE);
impl Drop for Snapshot {
fn drop(&mut self) {
unsafe {
if self.0 != INVALID_HANDLE_VALUE {
CloseHandle(self.0);
}
}
}
}
fn create_snapshot(pid: u32) -> Option<Snapshot> {
loop {
unsafe {
SetLastError(ERROR_SUCCESS);
// 加上 SNAPMODULE32 提高兼容性
let snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE | TH32CS_SNAPMODULE32, pid);
let error = GetLastError();
if error == ERROR_SUCCESS && snapshot != INVALID_HANDLE_VALUE {
return Some(Snapshot(snapshot));
}
// 如果不是 BAD_LENGTH直接返回错误
if error != ERROR_BAD_LENGTH {
eprintln!("CreateToolhelp32Snapshot failed: GetLastError = {}", error);
return None;
}
// 如果是 ERROR_BAD_LENGTH继续重试
}
}
}
/// 枚举模块,找到 DLL 基址
pub fn find_module_base(pid: u32, dll_name: &str) -> Option<usize> {
unsafe {
let snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE | TH32CS_SNAPMODULE32, pid);
if snapshot == INVALID_HANDLE_VALUE {
return None;
}
let snapshot = match create_snapshot(pid) {
Some(s) => s,
None => return None,
};
let mut me32 =
MODULEENTRY32W { dwSize: std::mem::size_of::<MODULEENTRY32W>() as u32, ..Default::default() };
if Module32FirstW(snapshot, &mut me32) != 0 {
loop {
let module_name = utf16_to_string(&me32.szModule);
if module_name.eq_ignore_ascii_case(dll_name) {
CloseHandle(snapshot);
return Some(me32.modBaseAddr as usize);
}
if Module32NextW(snapshot, &mut me32) == 0 {
break;
// 尝试多次枚举以应对时序问题
for _attempt in 0..5 {
if Module32FirstW(snapshot.0, &mut me32) != 0 {
loop {
let len = me32.szModule.iter().position(|&c| c == 0).unwrap_or(me32.szModule.len());
let name = String::from_utf16_lossy(&me32.szModule[..len]);
// 精确文件名比较或后缀比较(大小写不敏感)
if name.eq_ignore_ascii_case(dll_name)
|| name.to_lowercase().ends_with(&dll_name.to_lowercase())
{
return Some(me32.modBaseAddr as usize);
}
if Module32NextW(snapshot.0, &mut me32) == 0 {
break;
}
}
} else {
let err = GetLastError();
eprintln!("Module32FirstW failed: GetLastError = {}", err);
}
// 等待再重试
sleep(Duration::from_millis(100));
}
CloseHandle(snapshot);
None
}
None
}
/// 执行 YaeMain

View File

@@ -1,5 +1,5 @@
//! Yae 相关处理
//! @since Beta v0.9.1
//! @since Beta v0.9.2
#![cfg(target_os = "windows")]
pub mod inject;
@@ -73,8 +73,12 @@ pub fn call_yae_dll(
game_path: String,
uid: String,
ticket: Option<String>,
is_msix: bool,
) -> Result<(), String> {
let dll_path = app_handle.path().resource_dir().unwrap().join("resources/YaeAchievementLib.dll");
let mut dll_path = app_handle.path().app_config_dir().unwrap().join("YaeAchievementLib.dll");
if is_msix {
dll_path = app_handle.path().document_dir().unwrap().join("TeyvatGuide\\YaeAchievementLib.dll");
}
dbg!(&dll_path);
// 0. 创建 YaeAchievementPipe 的 命名管道,获取句柄
dbg!("开始启动 YaeAchievementPipe 命名管道");

View File

@@ -3,9 +3,9 @@
#![cfg(target_os = "windows")]
use crate::yae::read_conf;
use prost::encoding::{decode_key, WireType};
use prost::DecodeError;
use prost::Message;
use prost::encoding::{WireType, decode_key};
use serde::Serialize;
use std::collections::HashMap;
use std::io::{Cursor, Read};

View File

@@ -2,8 +2,8 @@
//! @since Beta v0.9.0
#![cfg(target_os = "windows")]
use prost::encoding::{decode_key, decode_varint, WireType};
use prost::DecodeError;
use prost::encoding::{WireType, decode_key, decode_varint};
use serde::ser::SerializeMap;
use serde::{Deserialize, Serialize, Serializer};
use std::collections::HashMap;

View File

@@ -2,7 +2,7 @@
"$schema": "https://schema.tauri.app/config/2",
"productName": "TeyvatGuide",
"identifier": "TeyvatGuide",
"version": "0.9.1",
"version": "0.9.2",
"build": {
"beforeDevCommand": "pnpm vite:dev",
"beforeBuildCommand": "pnpm vite:build",
@@ -27,11 +27,7 @@
"icons/Square150x150Logo.png",
"icons/Square284x284Logo.png",
"icons/Square310x310Logo.png"
],
"targets": ["msi", "app", "dmg"],
"windows": { "wix": { "language": "zh-CN" } },
"macOS": {},
"resources": { "lib/YaeAchievementLib.dll": "resources/YaeAchievementLib.dll" }
]
},
"app": {
"withGlobalTauri": true,

View File

@@ -0,0 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"identifier": "Teyvat.Guide",
"app": {
"security": { "capabilities": ["Teyvat.Guide", "Mys", "SubWindow", "DevJson"] }
}
}

View File

@@ -0,0 +1,6 @@
{
"$schema": "https://schema.tauri.app/config/2",
"bundle": {
"targets": ["app", "dmg"]
}
}

View File

@@ -0,0 +1,8 @@
{
"$schema": "https://schema.tauri.app/config/2",
"bundle": {
"resources": { "lib/YaeAchievementLib.dll": "resources/YaeAchievementLib.dll" },
"targets": ["msi"],
"windows": { "wix": { "language": "zh-CN" } }
}
}

View File

@@ -17,7 +17,9 @@ import showDialog from "@comp/func/dialog.js";
import showLoading from "@comp/func/loading.js";
import showSnackbar from "@comp/func/snackbar.js";
import OtherApi from "@req/otherReq.js";
import type { FeedbackInternalOptions, Integration } from "@sentry/core";
import * as Sentry from "@sentry/vue";
import { commands } from "@skipperndt/plugin-machine-uid";
import TGSqlite from "@Sql/index.js";
import TSUserAccount from "@Sqlm/userAccount.js";
import TSUserAchi from "@Sqlm/userAchi.js";
@@ -37,7 +39,7 @@ import { computed, nextTick, onMounted, onUnmounted, ref } from "vue";
import { useRouter } from "vue-router";
const router = useRouter();
const { theme, needResize, deviceInfo, isLogin, userDir, buildTime, closeToTray } =
const { theme, needResize, deviceInfo, isLogin, userDir, buildTime, closeToTray, showFeedback } =
storeToRefs(useAppStore());
const { uid, briefInfo, account, cookie } = storeToRefs(useUserStore());
@@ -59,11 +61,11 @@ onMounted(async () => {
const title = "Teyvat Guide v" + (await app.getVersion()) + " Beta";
await win.setTitle(title);
await listenOnInit();
await core.invoke("init_app");
dpListener = await event.listen<string>("active_deep_link", handleDpListen);
yaeListener = await event.listen<TGApp.Plugins.Yae.RsEvent>("yae_read", handleYaeListen);
closeListener = await event.listen("main-window-close-requested", handleWindowClose);
await nextTick();
await core.invoke("init_app");
}
if (needResize.value !== "false") await resizeWindow();
document.documentElement.className = theme.value;
@@ -75,6 +77,9 @@ onMounted(async () => {
await win.center();
await win.show();
}
if (showFeedback.value) {
Sentry.addIntegration(getSentryFeedback());
}
});
onUnmounted(() => {
@@ -104,6 +109,39 @@ onUnmounted(() => {
}
});
function getSentryFeedback(): Integration {
return Sentry.feedbackAsyncIntegration(<FeedbackInternalOptions>{
// 🌗 主题与注入行为
colorScheme: "system",
autoInject: true,
triggerLabel: "",
// 📝 表单标题与按钮文案
formTitle: "问题反馈",
cancelButtonLabel: "取消",
submitButtonLabel: "提交反馈",
successMessageText: "感谢您的反馈,我们将尽快处理。",
// 🧑 用户信息字段
nameLabel: "反馈人",
namePlaceholder: "请输入您的姓名或昵称",
emailLabel: "电子邮箱",
emailPlaceholder: "请输入您的邮箱地址,以便我们与您联系",
// 🐛 问题描述字段
messageLabel: "问题描述",
messagePlaceholder: "请详细描述您遇到的问题及复现步骤",
isRequiredLabel: "(必填)",
// 📸 截图工具相关
addScreenshotButtonLabel: "添加当前页面截图",
removeScreenshotButtonLabel: "移除截图",
highlightToolText: "标记重点区域",
removeHighlightText: "移除标记",
hideToolText: "遮挡敏感信息",
});
}
/**
* 自定义URL协议监听处理
* @param {Event<string>} event - 事件
@@ -248,6 +286,7 @@ async function handleResizeListen(event: Event<string>): Promise<void> {
async function listenOnInit(): Promise<void> {
console.info("[App][listenOnInit] 监听初始化事件!");
await event.listen<void>("initApp", async () => {
await setSentryUser();
await checkAppLoad();
await checkDeviceFp();
try {
@@ -267,6 +306,11 @@ async function listenOnInit(): Promise<void> {
});
}
async function setSentryUser(): Promise<void> {
const deviceRes = await commands.getMachineUid();
if (deviceRes.status === "ok") Sentry.setUser({ id: deviceRes.data.id! });
}
async function checkAppLoad(): Promise<void> {
let checkDB = false;
try {
@@ -310,6 +354,8 @@ async function checkUserLoad(): Promise<void> {
isLogin.value = false;
return;
}
// 检测ck刷新
await TSUserAccount.account.updateCk();
if (!isLogin.value) isLogin.value = true;
// 然后获取最近的UID
if (uid.value === undefined || !uidDB.includes(uid.value)) {

View File

@@ -53,6 +53,7 @@
--tgi-dialog: 100;
--tgi-geetest: 100;
--tgi-loading: 100;
--tgi-hyperlink: 100;
--tgi-snackbar: 9999;
}

View File

@@ -11,18 +11,14 @@ html #sentry-feedback {
--foreground: var(--btn-text);
--font-family: var(--font-text);
--accent-background: var(--tgc-od-blue);
--dialog-color: var(--common-text-title);
--dialog-background: var(--app-page-bg);
--input-color: var(--app-page-content);
--input-border: 1px solid var(--common-shadow-1);
--button-color: var(--tgc-od-blue);
--button-background: var(--app-side-bg);
--button-hover-background: var(--box-bg-1);
--button-primary-hover-background: var(--tgc-od-red);
--success-color: var(--tgc-od-green);
--error-color: var(--tgc-od-red);
--interactive-filter: none;

View File

@@ -3,6 +3,7 @@
* @since Beta v0.9.0
*/
@use "sass:map";
/** 根据传入星级返回颜色 */
@function get-od-star-color($star: 0) {
$star-color-map: (
@@ -12,8 +13,10 @@
4: #c678ddff,
5: #d19a66ff,
);
@if map.has-key($star-color-map, $star) {
@return map.get($star-color-map, $star);
}
@return #e06c75ff;
}

View File

@@ -170,7 +170,7 @@ const cardBg = computed<string>(() => {
return "none";
});
const forumBg = computed<string>(() =>
str2Color(`${props.modelValue.forum?.id}${props.modelValue.forum?.name}`, -60),
str2Color(`${card.value?.forum?.id}${card.value?.forum?.icon}${card.value?.forum?.name}`, -60),
);
const idBg = computed<string>(() => str2Color(`${props.modelValue.post.post_id}`, 0));
@@ -220,7 +220,13 @@ function getCommonCard(item: TGApp.BBS.Post.FullData): RenderCard {
let forumData: RenderForum | null = null;
let statData: RenderData | null = null;
if (item.forum !== null) {
forumData = { name: item.forum.name, icon: item.forum.icon, id: item.forum.id };
// 对部分缺失图标进行补充
let forumIcon = item.forum.icon;
if (item.forum.name === "攻略" && item.forum.id === 43) {
forumIcon =
"https://upload-bbs.mihoyo.com/upload/2020/09/14/ce666cea7c971b04e4b4a6fe0a9ebfd0.png";
}
forumData = { name: item.forum.name, icon: forumIcon, id: item.forum.id };
}
if (item.stat !== null) {
statData = {

View File

@@ -320,7 +320,7 @@ import { invoke } from "@tauri-apps/api/core";
import type { Event, UnlistenFn } from "@tauri-apps/api/event";
import { exists } from "@tauri-apps/plugin-fs";
import mhyClient from "@utils/TGClient.js";
import { isRunInAdmin, tryReadGameVer, YAE_GAME_VER } from "@utils/TGGame.js";
import { isRunInAdmin, tryCopyYae, tryReadGameVer, YAE_GAME_VER } from "@utils/TGGame.js";
import TGLogger from "@utils/TGLogger.js";
import { storeToRefs } from "pinia";
import { computed, onMounted, onUnmounted, ref, shallowRef } from "vue";
@@ -740,11 +740,17 @@ async function tryLaunchGame(): Promise<void> {
}
return;
}
const isMsix = await invoke<boolean>("is_msix");
if (isMsix) {
const copy = await tryCopyYae();
if (!copy) return;
}
try {
await invoke("call_yae_dll", {
gamePath: gamePath,
uid: account.value.gameUid,
ticket: resp,
is_msix: isMsix,
});
} catch (err) {
showSnackbar.error(`调用Yae DLL失败: ${err}`);

View File

@@ -0,0 +1,56 @@
/**
* hyperlink 组件封装,函数式调用
* @since Beta v0.9.1
*/
import type { ComponentInternalInstance, VNode } from "vue";
import { h, render } from "vue";
import hyperlink from "./hyperlink.vue";
const hyperlinkId = "tg-func-hyperlink";
export type HyperLinkParams = {
/** id */
id: string;
/** 名称 */
name: string;
/**
* 描述
* @remarks htmlText
*/
desc: string;
};
/**
* 自定义 HyperLink 组件
* @since Beta v0.6.3
*/
type HyperLinkInstance = {
exposeProxy: {
displayBox: (props: HyperLinkParams) => Promise<void>;
};
} & ComponentInternalInstance;
function renderBox(props: HyperLinkParams): VNode {
const container = document.createElement("div");
container.id = hyperlinkId;
const boxVNode: VNode = h(hyperlink, props);
render(boxVNode, container);
document.body.appendChild(container);
return boxVNode;
}
let hyperLinkInstance: VNode;
async function showHyperLink(info: HyperLinkParams): Promise<void> {
if (hyperLinkInstance !== undefined) {
const boxVue = <HyperLinkInstance>hyperLinkInstance.component;
await boxVue.exposeProxy.displayBox(info);
} else {
hyperLinkInstance = renderBox(info);
await showHyperLink(info);
}
}
export default showHyperLink;

View File

@@ -0,0 +1,160 @@
<!-- HyperLink Overlay -->
<template>
<transition name="func-hyperlink">
<div v-show="show || showOuter" class="hyperlink-overlay" @click.self.prevent="handleOuter">
<transition name="func-hyperlink-inner">
<div v-show="showInner" ref="hyperlinkRef" class="hyperlink-box">
<div class="hyperlink-title" @click="shareHyperLink()">{{ data.name }}</div>
<div class="hyperlink-desc">
<span v-html="parseHtmlText(data.desc)" />
</div>
<div class="hyperlink-id">{{ data.id }}</div>
</div>
</transition>
</div>
</transition>
</template>
<script lang="ts" setup>
import showSnackbar from "@comp/func/snackbar.js";
import { generateShareImg } from "@utils/TGShare.js";
import { parseHtmlText } from "@utils/toolFunc.js";
import { onMounted, ref, shallowRef, useTemplateRef, watch } from "vue";
import type { HyperLinkParams } from "./hyperlink.js";
const show = ref<boolean>(false);
const showOuter = ref<boolean>(false);
const showInner = ref<boolean>(false);
const props = defineProps<HyperLinkParams>();
const data = shallowRef<HyperLinkParams>(props);
const hyperLinkEl = useTemplateRef<HTMLDivElement>("hyperlinkRef");
watch(
() => show.value,
async () => {
if (show.value) {
showOuter.value = true;
await new Promise<void>((resolve) => setTimeout(resolve, 100));
showInner.value = true;
return;
}
await new Promise<void>((resolve) => setTimeout(resolve, 100));
showInner.value = false;
await new Promise<void>((resolve) => setTimeout(resolve, 300));
showOuter.value = false;
},
);
onMounted(async () => await displayBox(props));
function handleOuter(): void {
show.value = false;
}
async function displayBox(params: HyperLinkParams): Promise<void> {
data.value = params;
show.value = true;
}
async function shareHyperLink(): Promise<void> {
if (!hyperLinkEl.value) {
showSnackbar.warn("未捕获到 HyperLinkBox");
return;
}
const fileName = `Hyperlink_${props.id}_${props.name}`;
await generateShareImg(fileName, hyperLinkEl.value);
}
defineExpose({ displayBox });
</script>
<style lang="scss" scoped>
.func-hyperlink-outer-enter-active,
.func-hyperlink-outer-leave-active,
.func-hyperlink-inner-enter-active {
transition: all 0.3s;
}
.func-hyperlink-inner-leave-active {
transition: all 0.5s ease-in-out;
}
.func-hyperlink-inner-enter-from {
opacity: 0;
transform: scale(1.5);
}
.func-hyperlink-inner-enter-to,
.func-hyperlink-inner-leave-from {
opacity: 1;
transform: scale(1);
}
.func-hyperlink-outer-enter-to,
.func-hyperlink-outer-leave-from {
opacity: 1;
}
.func-hyperlink-outer-enter-from,
.func-hyperlink-outer-leave-to {
opacity: 0;
}
.func-hyperlink-inner-leave-to {
opacity: 0;
transform: scale(0);
}
.hyperlink-overlay {
position: fixed;
z-index: var(--tgi-hyperlink);
top: 0;
left: 0;
display: flex;
width: 100%;
height: 100%;
align-items: center;
justify-content: center;
-webkit-backdrop-filter: blur(10px);
backdrop-filter: blur(10px);
}
.hyperlink-box {
position: relative;
display: flex;
width: 520px;
max-height: 800px;
flex-direction: column;
align-items: center;
justify-content: flex-start;
padding: 10px 10px 16px;
border: 1px solid var(--common-shadow-1);
border-radius: 8px;
background: var(--app-page-bg);
box-shadow: 0 0 10px var(--common-shadow-t-1);
color: var(--app-page-content);
row-gap: 8px;
}
.hyperlink-title {
position: relative;
color: var(--common-text-title);
cursor: pointer;
font-family: var(--font-title);
font-size: 20px;
}
.hyperlink-desc {
position: relative;
padding-right: 8px;
overflow-y: auto;
white-space: pre-wrap;
}
.hyperlink-id {
position: absolute;
right: 8px;
bottom: 0;
font-size: 12px;
}
</style>

View File

@@ -17,8 +17,8 @@
{{ data.subtitle }}
</div>
<div class="loading-img">
<img v-if="!data.empty" alt="loading" src="/source/UI/loading.webp" />
<img v-else alt="empty" src="/source/UI/empty.webp" />
<img v-if="data.empty" alt="empty" src="/source/UI/empty.webp" />
<img v-else :src="iconUrl" alt="loading" />
</div>
</div>
</div>
@@ -27,16 +27,22 @@
</transition>
</template>
<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 { LoadingParams } from "./loading.js";
const defaultIcon = "/source/UI/loading.webp";
const showBox = ref<boolean>(false);
const showOuter = ref<boolean>(false);
const showInner = ref<boolean>(false);
const props = defineProps<LoadingParams>();
const data = shallowRef<LoadingParams>(props);
const localEmojis = shallowRef<Array<string>>([]);
const iconUrl = ref<string>(defaultIcon);
watch(
() => showBox.value,
@@ -56,11 +62,31 @@ watch(
onMounted(async () => await displayBox(props));
async function getRandomEmoji(): Promise<void> {
if (localEmojis.value.length === 0) {
const emojisRead = localStorage.getItem("emojis");
if (emojisRead) localEmojis.value = Object.values(JSON.parse(emojisRead));
else {
const resp = await bbsReq.emojis();
if ("retcode" in resp) {
console.error(resp);
showSnackbar.error("获取表情包失败!");
iconUrl.value = defaultIcon;
return;
}
localEmojis.value = Object.values(resp);
localStorage.setItem("emojis", JSON.stringify(resp));
}
}
iconUrl.value = localEmojis.value[Math.floor(Math.random() * localEmojis.value.length)];
}
async function displayBox(params: LoadingParams): Promise<void> {
if (!params.show) {
showBox.value = false;
await new Promise<void>((resolve) => setTimeout(resolve, 500));
data.value = { show: false, title: undefined, subtitle: undefined, empty: undefined };
await getRandomEmoji();
return;
}
data.value = {
@@ -175,14 +201,15 @@ defineExpose({ displayBox });
.loading-img {
display: flex;
width: 100%;
height: 200px;
height: 160px;
align-items: center;
justify-content: center;
img {
max-width: 100%;
max-height: 200px;
border-radius: 5px;
max-height: 160px;
border-radius: 4px;
aspect-ratio: 1;
}
}

View File

@@ -107,4 +107,13 @@ onMounted(async () => {
border-radius: 5px;
background: var(--box-bg-1);
}
/* stylelint-disable selector-class-pattern */
:deep(.v-virtual-scroll__item + .v-virtual-scroll__item) {
border-top: 1px solid var(--common-shadow-1);
margin-top: 8px;
}
/* stylelint-enable selector-class-pattern */
</style>

View File

@@ -10,7 +10,9 @@
<TibWikiAbyss
v-for="(item, index) in selectItem.Ranks"
:key="index"
:model-value="item"
:cur="item.cur.Rate"
:last="item.last.Rate"
:role="item.cur.Item"
/>
</div>
<div v-if="selectItem.Ranks.length === 0">暂无数据</div>
@@ -47,7 +49,7 @@ onMounted(async () => {
select.value = tmpData;
});
</script>
<style lang="css" scoped>
<style lang="scss" scoped>
.hta-tu-box {
display: flex;
height: 100%;

View File

@@ -10,7 +10,9 @@
<TibWikiAbyss
v-for="(item, index) in selectItem.Ranks"
:key="index"
:model-value="item"
:cur="item.cur.Rate"
:last="item.last.Rate"
:role="item.cur.Item"
/>
</div>
<div v-if="selectItem.Ranks.length === 0">暂无数据</div>
@@ -47,7 +49,7 @@ onMounted(async () => {
select.value = tempData;
});
</script>
<style lang="css" scoped>
<style lang="scss" scoped>
.hta-tus-box {
display: flex;
height: 100%;

View File

@@ -56,22 +56,24 @@ function getBoxData(id: number): TItemBoxData {
};
}
</script>
<style lang="css" scoped>
<style lang="scss" scoped>
.hta-tl-box {
position: relative;
display: flex;
width: calc(100% - 20px);
align-items: center;
justify-content: space-between;
padding: 10px;
border-radius: 5px;
margin: 10px;
background: var(--box-bg-2);
margin-top: 8px;
margin-bottom: 8px;
}
.hta-tl-item {
display: grid;
column-gap: 10px;
padding: 8px;
border-radius: 4px;
margin-left: 8px;
background: var(--box-bg-2);
column-gap: 8px;
grid-template-columns: repeat(4, 1fr);
}

View File

@@ -1,14 +1,16 @@
<!-- 单角色使用率/上场率 -->
<template>
<div class="twa-container">
<TItemBox :model-value="box" />
<div class="twa-diff">
<div class="twa-info">
<span>{{ avatar?.name ?? "旅行者" }}</span>
<span>{{ `${(props.modelValue.cur.Rate * 100).toFixed(3)}%` }}</span>
<span>{{ `${(props.cur * 100).toFixed(3)}%` }}</span>
<span
:class="{
up: props.modelValue.cur.Rate > props.modelValue.last.Rate,
down: props.modelValue.cur.Rate < props.modelValue.last.Rate,
up: props.cur > props.last,
down: props.cur < props.last,
}"
:title="`${(props.last * 100).toFixed(3)}%`"
>
{{ getDiffStr() }}
</span>
@@ -20,9 +22,15 @@ import TItemBox, { type TItemBoxData } from "@comp/app/t-itemBox.vue";
import { computed, onMounted, shallowRef } from "vue";
import { AppCharacterData } from "@/data/index.js";
import type { AbyssDataItem } from "@/pages/WIKI/Abyss.vue";
type TibWikiAbyssProps = { modelValue: AbyssDataItem<TGApp.Plugins.Hutao.Base.Rate> };
type TibWikiAbyssProps = {
/** 角色ID */
role: number;
/** 当前数据 */
cur: number;
/** 上一期数据 */
last: number;
};
const props = defineProps<TibWikiAbyssProps>();
const avatar = shallowRef<TGApp.App.Character.WikiBriefInfo>();
@@ -46,30 +54,29 @@ const box = computed<TItemBoxData>(() => ({
}));
onMounted(() => {
if ([10000005, 10000007].includes(props.modelValue.cur.Item)) {
if ([10000005, 10000007].includes(props.role)) {
avatar.value = {
area: "",
birthday: [0, 0],
contentId: 0,
costumes: [],
element: "",
id: props.modelValue.cur.Item,
name: props.modelValue.cur.Item === 10000005 ? "空" : "荧",
id: props.role,
name: props.role === 10000005 ? "空" : "荧",
nameCard: "",
release: "",
star: 5,
title: "",
weapon: "单手剑",
};
} else avatar.value = AppCharacterData.find((a) => a.id === props.modelValue.cur.Item);
} else avatar.value = AppCharacterData.find((a) => a.id === props.role);
});
function getDiffStr(): string {
if (props.modelValue.cur.Rate === props.modelValue.last.Rate) return "";
if (props.modelValue.last.Rate > props.modelValue.cur.Rate) {
return `${((props.modelValue.last.Rate - props.modelValue.cur.Rate) * 100).toFixed(3)}%`;
}
return `${((props.modelValue.cur.Rate - props.modelValue.last.Rate) * 100).toFixed(3)}%`;
const diff = (Math.abs(props.cur - props.last) * 100).toFixed(3);
if (props.cur === props.last) return `-${diff}%`;
if (props.last > props.cur) return `${diff}%`;
return `${diff}%`;
}
</script>
<style lang="css" scoped>
@@ -84,12 +91,12 @@ function getDiffStr(): string {
column-gap: 5px;
}
.twa-diff {
.twa-info {
display: flex;
height: 100%;
flex-direction: column;
align-items: flex-start;
justify-content: space-between;
justify-content: flex-start;
color: var(--box-text-4);
font-size: 12px;
@@ -97,15 +104,18 @@ function getDiffStr(): string {
font-family: var(--font-title);
font-size: 15px;
}
}
.twa-diff .up {
color: var(--tgc-od-red);
font-family: var(--font-title);
}
:last-child {
color: var(--tgc-od-blue);
font-family: var(--font-title);
.twa-diff .down {
color: var(--tgc-od-green);
font-family: var(--font-title);
&.up {
color: var(--tgc-od-red);
}
&.down {
color: var(--tgc-od-green);
}
}
}
</style>

View File

@@ -146,7 +146,7 @@ async function confirmCGD(): Promise<void> {
const oriEmpty = gameDir.value === "未设置";
const editCheck = await showDialog.check(
oriEmpty ? "确认设置游戏目录?" : "确认修改游戏目录?",
oriEmpty ? "请选择启动器所在目录" : `当前:${gameDir.value}`,
oriEmpty ? "请选择 Yuanshen.exe 所在目录" : `当前:${gameDir.value}`,
);
if (!editCheck) {
showSnackbar.cancel(oriEmpty ? "已取消设置" : "已取消修改");

View File

@@ -197,10 +197,10 @@ async function cycleGetDataGame(): Promise<void> {
const statusRaw: TGApp.Game.Login.StatPayloadRaw = JSON.parse(res.payload.raw);
await showLoading.start("正在获取SToken");
const stResp = await takumiReq.game.stoken(statusRaw);
await showLoading.end();
if ("retcode" in stResp) {
showSnackbar.error(`[${stResp.retcode}] ${stResp.message}`);
model.value = false;
await showLoading.end();
return;
}
const ck: TGApp.App.Account.Cookie = {

View File

@@ -7,6 +7,7 @@
density="compact"
title="重置胡桃云密码"
>
<img src="/platforms/other/hutao2.webp" alt="logo" class="thvc-logo" />
<v-form ref="formEl" class="thvc-mid">
<v-text-field
ref="usernameInput"
@@ -174,6 +175,15 @@ onUnmounted(() => {
background-color: var(--box-bg-1);
}
.thvc-logo {
position: absolute;
z-index: 0;
bottom: -16px;
left: -16px;
width: 128px;
height: 128px;
}
.thvc-mid {
position: relative;
display: flex;

View File

@@ -157,6 +157,10 @@ async function handleMaterial(cur: TGApp.Game.ActCalendar.ActReward): Promise<vo
showCalendar.value = true;
return;
}
if (cur.wiki_url === "") {
showSnackbar.warn("未检测到跳转链接");
return;
}
await openUrl(cur.wiki_url);
}
</script>

View File

@@ -1,8 +1,8 @@
<template>
<div class="ph-pool-card">
<div class="ph-pool-cover" @click="toPool()">
<TMiImg v-if="cover" :src="cover" alt="cover" :ori="true" />
<img src="/source/UI/empty.webp" class="empty" v-else alt="empty" />
<img v-if="cover" :src="cover" alt="cover" />
<img v-else alt="empty" class="empty" src="/source/UI/empty.webp" />
</div>
<div class="ph-pool-bottom">
<div class="ph-pool-avatars">
@@ -14,10 +14,10 @@
>
<TItemBox
v-if="avatar.info"
:title="avatar.info.name"
:model-value="getBox(avatar.info)"
:title="avatar.info.name"
/>
<TMiImg v-else :src="avatar.icon" alt="icon" :ori="true" />
<img v-else :src="avatar.icon" alt="icon" />
</div>
</div>
<div class="ph-pool-info">
@@ -25,7 +25,7 @@
<v-icon>mdi-calendar-clock</v-icon>
<span>{{ props.pool.start_time }} ~ {{ props.pool.end_time }}</span>
</div>
<v-progress-linear color="var(--tgc-od-green)" :model-value="percent" :rounded="true" />
<v-progress-linear :model-value="percent" :rounded="true" color="var(--tgc-od-green)" />
<div v-if="restTs > durationTs" class="ph-pool-stat">未开始</div>
<div v-else-if="restTs > 0" class="ph-pool-stat">
剩余时间{{ stamp2LastTime(restTs) }}
@@ -37,7 +37,6 @@
</template>
<script lang="ts" setup>
import TItemBox, { TItemBoxData } from "@comp/app/t-itemBox.vue";
import TMiImg from "@comp/app/t-mi-img.vue";
import showSnackbar from "@comp/func/snackbar.js";
import postReq from "@req/postReq.js";
import useHomeStore from "@store/home.js";

View File

@@ -106,14 +106,13 @@
@click="showMaterial(reward)"
>
<img :src="`/icon/bg/${reward.rarity}-Star.webp`" alt="bg" class="bg" />
<TMiImg :alt="reward.name" :ori="true" :src="reward.icon" class="icon" />
<img :alt="reward.name" :src="reward.icon" class="icon" />
<span v-if="reward.num > 0" class="count">{{ reward.num }}</span>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import TMiImg from "@comp/app/t-mi-img.vue";
import gameEnum from "@enum/game.js";
import { getHardChallengeDesc } from "@Sql/utils/transUserRecord.js";
import { generateShareImg } from "@utils/TGShare.js";

View File

@@ -96,6 +96,7 @@ const selectAreaList = [
"至冬",
"寰宇劫灭",
"挪德卡莱",
"璃月港",
];
const selectedStar = ref<Array<number>>([]);

View File

@@ -1,17 +1,28 @@
<!-- 楼层组件 -->
<template>
<div class="tuad-box">
<div ref="floorEl" class="tuad-box">
<div class="tuad-title">
<div class="title">{{ props.floor.id }}</div>
<div class="title" @click="shareFloor()">{{ props.floor.id }}</div>
<div class="append">
<span>{{ props.floor.winStar }}</span>
<span>/{{ props.floor.maxStar }}</span>
<img alt="Abyss" src="/icon/star/Abyss.webp" />
</div>
</div>
<div v-if="props.floor.buff && props.floor.buff.length > 0" class="tuad-buff">
<span>地脉异常:</span>
<span v-for="(b, i) in props.floor.buff" :key="i">{{ b }}</span>
<div class="tuad-mid">
<div v-if="show" class="tuad-share">UID {{ props.uid }} | {{ props.id }}</div>
<div class="tuad-buff">
<span>地脉异常:</span>
<template v-if="props.floor.buff === undefined">
<span>数据缺失</span>
</template>
<template v-else-if="props.floor.buff.length > 0">
<span v-for="(b, i) in props.floor.buff" :key="i">{{ b }}</span>
</template>
<template v-else>
<span>无地脉异常</span>
</template>
</div>
</div>
<div class="tuad-index-box">
<TuaDetailLevel v-for="level in props.floor.levels" :key="level.id" :level />
@@ -19,11 +30,29 @@
</div>
</template>
<script lang="ts" setup>
import showSnackbar from "@comp/func/snackbar.js";
import { generateShareImg } from "@utils/TGShare.js";
import { nextTick, ref, useTemplateRef } from "vue";
import TuaDetailLevel from "./tua-detail-level.vue";
type TuaDetailProps = { floor: TGApp.Sqlite.Abyss.Floor };
type TuaDetailProps = { floor: TGApp.Sqlite.Abyss.Floor; uid?: string; id: number };
const props = defineProps<TuaDetailProps>();
const show = ref<boolean>(false);
const floorRef = useTemplateRef<HTMLDivElement>("floorEl");
async function shareFloor(): Promise<void> {
if (!floorRef.value) {
showSnackbar.warn("未捕获到Floor Dom");
return;
}
show.value = true;
await nextTick();
const fileName = `深境螺旋_第${props.id}期_${props?.uid ?? ""}_${props.floor.id}`;
await generateShareImg(fileName, floorRef.value);
show.value = false;
}
</script>
<style lang="css" scoped>
.tuad-box {
@@ -50,6 +79,10 @@ const props = defineProps<TuaDetailProps>();
font-size: 20px;
line-height: 24px;
.title {
cursor: pointer;
}
.append {
position: relative;
display: flex;
@@ -72,6 +105,22 @@ const props = defineProps<TuaDetailProps>();
}
}
.tuad-mid {
position: relative;
display: flex;
width: 100%;
align-items: center;
justify-content: space-between;
}
.tuad-share {
position: relative;
z-index: -1;
color: var(--box-text-1);
font-size: 14px;
opacity: 0.8;
}
.tuad-buff {
position: relative;
margin-left: auto;

View File

@@ -1,41 +1,44 @@
<template>
<div class="tua-al-container">
<div v-if="ncData !== undefined" class="tua-al-nc">
<TopNameCard :data="ncData" @selected="showNc = true" :finish="isFinish" />
<TopNameCard :data="ncData" :finish="isFinish" @selected="showNc = true" />
</div>
<v-virtual-scroll :items="renderAchi" :item-height="60" class="tua-al-list">
<v-virtual-scroll :item-height="60" :items="renderAchi" class="tua-al-list">
<template #default="{ item }">
<TuaAchi :modelValue="item" @select-achi="selectAchi" />
</template>
</v-virtual-scroll>
<ToNameCard v-model="showNc" :data="ncData" v-if="ncData" />
<ToAchiInfo
<ToNameCard v-if="ncData" v-model="showNc" :data="ncData" />
<TuaAchiOverlay
v-if="selectedAchi"
v-model="showOverlay"
:data="selectedAchi"
@search="handleSearch"
@select-series="selectSeries"
>
<template #left>
<div class="card-arrow" @click="switchAchiInfo(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="switchAchiInfo(true)">
<img src="@/assets/icons/arrow-right.svg" alt="right" />
<img alt="right" src="@/assets/icons/arrow-right.svg" />
</div>
</template>
</ToAchiInfo>
</TuaAchiOverlay>
<VpOverlaySearch v-model="showSearch" :gid="2" :keyword="searchWd" />
</div>
</template>
<script lang="ts" setup>
import ToNameCard from "@comp/app/to-nameCard.vue";
import TopNameCard from "@comp/app/top-nameCard.vue";
import showSnackbar from "@comp/func/snackbar.js";
import VpOverlaySearch from "@comp/viewPost/vp-overlay-search.vue";
import TSUserAchi from "@Sqlm/userAchi.js";
import { computed, onMounted, ref, shallowRef, watch } from "vue";
import { computed, nextTick, onMounted, ref, shallowRef, watch } from "vue";
import ToAchiInfo from "./tua-achi-overlay.vue";
import TuaAchiOverlay from "./tua-achi-overlay.vue";
import TuaAchi from "./tua-achi.vue";
import { AppAchievementSeriesData, AppNameCardsData } from "@/data/index.js";
@@ -48,7 +51,7 @@ type TuaAchiListProps = {
isSearch: boolean;
};
type TuaAchiListEmits = {
(e: "update:series", v: number | undefined): void;
(e: "update:series", v: number): void;
(e: "update:isSearch", v: boolean): false;
};
@@ -59,6 +62,8 @@ const nameCard = ref<string>();
const showNc = ref<boolean>(false);
const showOverlay = ref<boolean>(false);
const isFinish = ref<boolean>(false);
const searchWd = ref<string>();
const showSearch = ref<boolean>(false);
const ncData = shallowRef<TGApp.App.NameCard.Item>();
const achievements = shallowRef<Array<TGApp.App.Achievement.RenderItem>>([]);
@@ -74,6 +79,11 @@ onMounted(async () => await loadAchi());
watch(() => [props.search, props.isSearch], searchAchi);
watch(() => [props.series, props.uid], loadAchi);
function handleSearch(kw: string): void {
searchWd.value = kw;
showSearch.value = true;
}
async function searchAchi(): Promise<void> {
if (!props.isSearch) return;
if (!props.search || props.search === "") {
@@ -81,19 +91,23 @@ async function searchAchi(): Promise<void> {
emits("update:isSearch", false);
return;
}
nameCard.value = undefined;
ncData.value = undefined;
achievements.value = await TSUserAchi.searchAchi(props.uid, props.search);
const searchRes = await TSUserAchi.searchAchi(props.uid, props.search);
if (showOverlay.value) showOverlay.value = false;
if (achievements.value.length > 0) {
if (searchRes.length > 0) {
nameCard.value = undefined;
ncData.value = undefined;
achievements.value = searchRes;
showSnackbar.success(`成功获取${achievements.value.length}条成就`);
emits("update:series", undefined);
emits("update:series", -1);
await nextTick();
} else {
showSnackbar.warn("未搜索到相关成就");
}
emits("update:isSearch", false);
}
async function loadAchi(): Promise<void> {
if (props.isSearch || props.series === undefined) return;
if (props.isSearch) return;
achievements.value = await TSUserAchi.getAchievements(props.uid, props.series);
const ov = await TSUserAchi.getOverview(props.uid, props.series);
isFinish.value = ov.fin === ov.total;

View File

@@ -56,31 +56,28 @@
<slot name="right"></slot>
</div>
</TOverlay>
<VpOverlaySearch v-model="showSearch" :keyword="search" gid="2" />
</template>
<script lang="ts" setup>
import TOverlay from "@comp/app/t-overlay.vue";
import showSnackbar from "@comp/func/snackbar.js";
import VpOverlaySearch from "@comp/viewPost/vp-overlay-search.vue";
import TGLogger from "@utils/TGLogger.js";
import { generateShareImg } from "@utils/TGShare.js";
import { ref } from "vue";
import { AppAchievementSeriesData } from "@/data/index.js";
type ToAchiInfoProps = { data: TGApp.App.Achievement.RenderItem };
type ToAchiInfoEmits = (e: "select-series", v: number) => void;
type ToAchiInfoEmits = {
(e: "select-series", v: number): void;
(e: "search", v: string): void;
};
const props = defineProps<ToAchiInfoProps>();
const emits = defineEmits<ToAchiInfoEmits>();
const visible = defineModel<boolean>();
const showSearch = ref<boolean>(false);
const search = ref<string>();
async function searchDirect(word: string): Promise<void> {
await TGLogger.Info(`[ToAchiInfo][${props.data.id}][Search] 查询 ${word}`);
search.value = word;
showSearch.value = true;
emits("search", word);
}
async function share(): Promise<void> {

View File

@@ -96,7 +96,7 @@ async function setAchiStat(stat: boolean): Promise<void> {
@use "@styles/github.styles.scss" as github-styles;
.achi-container {
@include github-styles.github-card;
@include github-styles.github-card-shadow;
position: relative;
display: flex;
@@ -105,12 +105,14 @@ async function setAchiStat(stat: boolean): Promise<void> {
align-items: center;
justify-content: space-between;
padding: 8px;
border: 1px solid var(--common-shadow-1);
border-radius: 4px;
background: var(--box-bg-1);
cursor: pointer;
}
.dark .achi-container {
@include github-styles.github-card("dark");
@include github-styles.github-card-shadow("dark");
}
.achi-version {

View File

@@ -1,25 +1,26 @@
<!-- 成就系列 -->
<template>
<div
v-if="data"
v-if="series"
v-show="!(hideFin && progress === 100)"
:class="{
'tuas-selected': props.cur === props.series,
'tuas-selected': props.cur === props.series.id,
'tuas-radius': showCard,
}"
:title="data.name"
:title="series.name"
class="tuas-card"
@click="selectSeries"
>
<div class="tuas-version">v{{ data.version }}</div>
<div class="tuas-version">v{{ series.version }}</div>
<div v-if="showCard" class="tuas-reward">
<img
:class="{ finish: progress === 100 }"
:src="`/WIKI/nameCard/bg/${data.card}.webp`"
:class="progress === 100 ? 'finish' : ''"
:src="`/WIKI/nameCard/bg/${series.card}.webp`"
alt="card"
/>
</div>
<div class="tuas-icon">
<img :src="`/icon/achievement/${data.icon}.webp`" alt="icon" />
<img :src="`/icon/achievement/${series.icon}.webp`" alt="icon" />
<v-progress-circular
:model-value="progress"
bg-color="var(--tgc-od-white)"
@@ -28,37 +29,39 @@
/>
</div>
<div class="tuas-content">
<span :title="data.name">{{ data.name }}</span>
<span :title="series.name">{{ series.name }}</span>
<span>{{ overview.fin }}/{{ overview.total }}</span>
</div>
</div>
</template>
<script lang="ts" setup>
import showSnackbar from "@comp/func/snackbar.js";
import TSUserAchi from "@Sqlm/userAchi.js";
import { type Event, listen, type UnlistenFn } from "@tauri-apps/api/event";
import { computed, onMounted, onUnmounted, shallowRef, watch } from "vue";
import { AppAchievementSeriesData } from "@/data/index.js";
type TuaSeriesProps = { uid: number; series: number; cur: number };
type TuaSeriesEmits = (e: "selectSeries", v: number) => void;
type TuaSeriesProps = {
/** 存档 UID */
uid: number;
/** 成就系列数据 */
series: TGApp.App.Achievement.Series;
/** 当前选中系列 ID-1表示未选择 */
cur: number;
/** 是否隐藏已完成 */
hideFin: boolean;
};
type TuaSeriesEmits = (e: "selected", v: number) => void;
let achiListener: UnlistenFn | null = null;
const props = defineProps<TuaSeriesProps>();
const emits = defineEmits<TuaSeriesEmits>();
const overview = shallowRef<TGApp.App.Achievement.Overview>({ fin: 0, total: 0 });
const data = computed<TGApp.App.Achievement.Series | undefined>(() =>
AppAchievementSeriesData.find((s) => s.id === props.series),
);
const progress = computed<number>(() => {
if (overview.value.total === 0) return 0;
return Math.round((overview.value.fin / overview.value.total) * 100);
});
const showCard = computed<boolean>(() => {
if (data.value === undefined) return false;
return data.value.card !== "";
return props.series.card !== "";
});
onMounted(async () => {
@@ -72,12 +75,12 @@ watch(
);
async function refreshOverview(): Promise<void> {
overview.value = await TSUserAchi.getOverview(props.uid, props.series);
overview.value = await TSUserAchi.getOverview(props.uid, props.series.id);
}
async function listenAchi(): Promise<UnlistenFn> {
return await listen<number>("updateAchi", async (e: Event<number>) => {
if (e.payload === props.series) await refreshOverview();
if (e.payload === props.series.id) await refreshOverview();
});
}
@@ -89,11 +92,7 @@ onUnmounted(async () => {
});
function selectSeries(): void {
if (props.cur === props.series) {
showSnackbar.warn("已选中当前系列!");
return;
}
emits("selectSeries", props.series);
emits("selected", props.series.id);
}
</script>
<style lang="scss" scoped>
@@ -106,6 +105,7 @@ function selectSeries(): void {
display: flex;
overflow: hidden;
height: 60px;
flex-shrink: 0;
align-items: center;
justify-content: flex-start;
padding: 8px;

View File

@@ -27,7 +27,7 @@
</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="areaSelected" :items="areaOpts" size="small" />
</div>
@@ -91,6 +91,7 @@ const areaOpts: Array<UavSelectChipsItem> = [
"至冬",
"寰宇劫灭",
"挪德卡莱",
"璃月港",
].map((i) => ({ label: i, value: i, title: i }));
const emits = defineEmits<UavSelectEmits>();

View File

@@ -135,7 +135,7 @@ function getAvatarBox(item: TGApp.Game.Combat.Avatar): TItemBoxData {
display: flex;
align-items: center;
justify-content: center;
padding: 4px 8px 4px 4px;
padding: 4px 20px 4px 4px;
border-radius: 40px;
background: var(--box-bg-3);
column-gap: 4px;

View File

@@ -1,7 +1,7 @@
<!-- 剧诗单幕 -->
<template>
<div class="tucr-box">
<div class="tucr-title">
<div ref="tucrEl" class="tucr-box">
<div class="tucr-title" @click="shareRound()">
<img
:class="`stat_${props.round.is_get_medal}`"
:src="`/icon/combat/${getIcon()}.webp`"
@@ -13,6 +13,7 @@
<span v-else class="main">{{ props.round.round_id }}</span>
<span class="sub">{{ timestampToDate(Number(props.round.finish_time) * 1000) }}</span>
</div>
<span v-if="showInfo" class="tucr-info">UID {{ props.uid }} | {{ props.id }}</span>
<TucAeBox :avatars="props.round.avatars" :enemies="props.round.enemies" />
<div class="tucr-content">
<TucBuffBox
@@ -24,21 +25,39 @@
</div>
</template>
<script lang="ts" setup>
import showSnackbar from "@comp/func/snackbar.js";
import { generateShareImg } from "@utils/TGShare.js";
import { timestampToDate } from "@utils/toolFunc.js";
import { nextTick, ref, useTemplateRef } from "vue";
import TucAeBox from "./tuc-ae-box.vue";
import TucBuffBox from "./tuc-buff-box.vue";
import TucCardBox from "./tuc-card-box.vue";
type TucRoundProps = { round: TGApp.Game.Combat.RoundData };
type TucRoundProps = { round: TGApp.Game.Combat.RoundData; uid: string; id: number };
const props = defineProps<TucRoundProps>();
const showInfo = ref<boolean>(false);
const tucrRef = useTemplateRef<HTMLDivElement>("tucrEl");
function getIcon(): string {
return `${props.round.is_tarot ? "tarot" : "star"}_${props.round.is_get_medal ? "1" : "0"}`;
}
async function shareRound(): Promise<void> {
if (!tucrRef.value) {
showSnackbar.warn("未捕获到分享Dom");
return;
}
showInfo.value = true;
await nextTick();
const fileName = `真境剧诗_第${props.round.round_id}`;
await generateShareImg(fileName, tucrRef.value);
showInfo.value = false;
}
</script>
<style lang="scss" scoped>
.tucr-box {
position: relative;
display: flex;
width: 100%;
height: fit-content;
@@ -51,11 +70,13 @@ function getIcon(): string {
}
.tucr-title {
position: relative;
display: flex;
align-items: flex-end;
justify-content: center;
margin-right: auto;
column-gap: 4px;
cursor: pointer;
img {
height: 30px;
@@ -77,6 +98,14 @@ function getIcon(): string {
}
}
.tucr-info {
position: absolute;
z-index: -1;
top: 24px;
right: 8px;
font-size: 12px;
}
.tucr-content {
position: relative;
display: flex;

View File

@@ -93,6 +93,7 @@
</div>
</template>
<script lang="ts" setup>
import gameEnum from "@enum/game.js";
import { UnlistenFn } from "@tauri-apps/api/event";
import { getCurrentWindow } from "@tauri-apps/api/window";
import {
@@ -248,9 +249,14 @@ function checkIsUp(item: TGApp.Sqlite.Gacha.Gacha): boolean | undefined {
if (item.gachaType === "100" || item.gachaType === "200") return undefined;
const itemTime = new Date(item.time).getTime();
const itemIdNum = Number(item.itemId);
const strictPool: Array<string> = [gameEnum.gachaType.WeaponUp, gameEnum.gachaType.MixUp];
const avatarUpPool: Array<string> = [gameEnum.gachaType.AvatarUp, gameEnum.gachaType.AvatarUp2];
const poolsFind = AppGachaData.filter((pool) => {
// 对于武器池,严格要求 gachaType 对应,角色池放宽以修复特殊情况下的异常
if (pool.type.toLocaleString() !== item.gachaType && item.gachaType === "302") return false;
// 对于武器池&集录池,严格要求 gachaType 对应,角色池放宽以修复特殊情况下的异常
if (pool.type.toString() !== item.gachaType) {
if (strictPool.includes(item.gachaType.toString())) return false;
if (!avatarUpPool.includes(pool.type.toString())) return false;
}
const startTime = new Date(pool.from).getTime();
const endTime = new Date(pool.to).getTime();
return itemTime >= startTime && itemTime <= endTime;

View File

@@ -319,9 +319,9 @@ function getBox2(item: UgcHisCardBox): TItemBoxData {
align-items: center;
justify-content: flex-start;
color: var(--common-text-title);
gap: 4px;
font-family: var(--font-title);
font-size: 20px;
gap: 4px;
}
.ug-hisci-tag {

View File

@@ -5,18 +5,22 @@
<div class="ugo-title">{{ title }}</div>
<div class="ugo-fp" title="点击选择文件路径" @click="selectFile()">文件路径:{{ fp }}</div>
</div>
<div v-if="props.mode === 'import' && dataRaw" class="ugo-header">
<div v-if="props.mode === 'import' && importRaw" class="ugo-header">
<div class="ugo-header-item">
<div>应用信息</div>
<div>{{ dataRaw.info.export_app }} {{ dataRaw.info.export_app_version }}</div>
<div>
{{ importRaw.data.info.export_app }} {{ importRaw.data.info.export_app_version }}
</div>
</div>
<div class="ugo-header-item">
<div>文件UIGF版本</div>
<div>{{ dataRaw.info.version }}</div>
<div>
{{ importRaw.isV4 ? importRaw.data.info.version : importRaw.data.info.uigf_version }}
</div>
</div>
<div class="ugo-header-item">
<div>导出时间</div>
<div>{{ timestampToDate(Number(dataRaw.info.export_timestamp) * 1000) }}</div>
<div>{{ timestampToDate(Number(importRaw.data.info.export_timestamp) * 1000) }}</div>
</div>
</div>
<v-item-group v-model="selectedData" class="ugo-content" multiple>
@@ -55,17 +59,27 @@ import { path } from "@tauri-apps/api";
import { open } from "@tauri-apps/plugin-dialog";
import TGLogger from "@utils/TGLogger.js";
import { timestampToDate } from "@utils/toolFunc.js";
import { exportUigf4Data, readUigf4Data, verifyUigfData } from "@utils/UIGF.js";
import { checkUigfData, exportUigf4Data, readUigf4Data, readUigfData } from "@utils/UIGF.js";
import { computed, onMounted, ref, shallowRef, watch } from "vue";
type UgoUidProps = { mode: "import" | "export" };
type UgoUidProps = {
/** 导入/导出 */
mode: "import" | "export";
/** filePathImport导出路径 */
fpi?: string;
};
type UgoUidItem = { uid: string; length: number; time: string };
/** 兼容不同版本的导入 */
type UgoUidImportRaw =
| { isV4: true; data: TGApp.Plugins.UIGF.Schema4 }
| { isV4: false; data: TGApp.Plugins.UIGF.Schema };
const fpEmptyText = "点击选择文件路径";
const props = defineProps<UgoUidProps>();
const visible = defineModel<boolean>();
const fp = ref<string>(fpEmptyText);
const importRaw = shallowRef<UgoUidImportRaw>();
const dataRaw = shallowRef<TGApp.Plugins.UIGF.Schema4>();
const data = shallowRef<Array<UgoUidItem>>([]);
const selectedData = shallowRef<Array<UgoUidItem>>([]);
@@ -76,7 +90,7 @@ onMounted(async () => {
});
watch(
() => [visible.value, props.mode],
() => [visible.value, props.mode, props.fpi],
async () => {
if (visible.value) await refreshData();
},
@@ -91,12 +105,13 @@ async function refreshData(): Promise<void> {
selectedData.value = [];
data.value = [];
dataRaw.value = undefined;
importRaw.value = undefined;
if (props.mode === "import") {
fp.value = fpEmptyText;
await handleImportData();
fp.value = props.fpi ?? fpEmptyText;
await refreshImport();
} else {
fp.value = await getDefaultSavePath();
await handleExportData();
await refreshExport();
}
}
@@ -115,22 +130,30 @@ async function selectFile(): Promise<void> {
return;
}
fp.value = file;
if (props.mode === "import") await handleImportData();
if (props.mode === "import") await refreshImport();
}
async function handleImportData(): Promise<void> {
async function refreshImport(): Promise<void> {
if (fp.value === fpEmptyText) return;
try {
await showLoading.start("正在导入数据...", "正在验证数据...");
const check = await verifyUigfData(fp.value, true);
if (!check) {
const isV4 = await checkUigfData(fp.value);
console.info(isV4);
if (isV4 === null) {
await showLoading.end();
return;
}
await showLoading.update("数据验证成功,正在读取数据...");
const uigfData = await readUigf4Data(fp.value);
dataRaw.value = uigfData;
data.value = uigfData.hk4e.map(parseData);
if (isV4) {
const read = await readUigf4Data(fp.value);
importRaw.value = { isV4: true, data: read };
data.value = read.hk4e.map(parseData4);
} else {
const read = await readUigfData(fp.value);
console.log(read.list.length);
importRaw.value = { isV4: false, data: read };
data.value = [parseData(read)];
}
await showLoading.end();
} catch (e) {
if (e instanceof Error) {
@@ -143,7 +166,16 @@ async function handleImportData(): Promise<void> {
}
}
function parseData(data: TGApp.Plugins.UIGF.GachaHk4e): UgoUidItem {
function parseData(data: TGApp.Plugins.UIGF.Schema): UgoUidItem {
const timeList = data.list.map((item) => new Date(item.time).getTime());
return {
uid: data.info.uid,
length: data.list.length,
time: `${timestampToDate(Math.min(...timeList))} ~ ${timestampToDate(Math.max(...timeList))}`,
};
}
function parseData4(data: TGApp.Plugins.UIGF.GachaHk4e): UgoUidItem {
const timeList = data.list.map((item) => new Date(item.time).getTime());
return {
uid: data.uid.toString(),
@@ -152,7 +184,7 @@ function parseData(data: TGApp.Plugins.UIGF.GachaHk4e): UgoUidItem {
};
}
async function handleExportData(): Promise<void> {
async function refreshExport(): Promise<void> {
const uidList = await TSUserGacha.getUidList();
const tmpData: Array<UgoUidItem> = [];
for (const uid of uidList) {
@@ -177,7 +209,7 @@ async function handleSelected(): Promise<void> {
}
async function handleImport(): Promise<void> {
if (!dataRaw.value) {
if (!importRaw.value) {
showSnackbar.error("未获取到数据!");
fp.value = fpEmptyText;
return;
@@ -187,15 +219,21 @@ async function handleImport(): Promise<void> {
return;
}
await showLoading.start("正在导入数据...");
for (const item of selectedData.value) {
await showLoading.update(`正在导入UID: ${item.uid}`);
const dataFind = dataRaw.value.hk4e.find((i) => i.uid.toString() === item.uid);
if (!dataFind) {
showSnackbar.error(`未找到UID: ${item.uid}`);
await new Promise<void>((resolve) => setTimeout(resolve, 1000));
continue;
if (importRaw.value.isV4) {
for (const item of selectedData.value) {
await showLoading.update(`正在导入UID: ${item.uid}`);
const dataFind = importRaw.value.data.hk4e.find((i) => i.uid.toString() === item.uid);
if (!dataFind) {
showSnackbar.error(`未找到UID: ${item.uid}`);
await new Promise<void>((resolve) => setTimeout(resolve, 1000));
continue;
}
await TSUserGacha.mergeUIGF4(dataFind, true);
}
await TSUserGacha.mergeUIGF4(dataFind, true);
} else {
const iUid = selectedData.value[0].uid;
await showLoading.update(`正在导入UID:${iUid}`);
await TSUserGacha.mergeUIGF(iUid, importRaw.value.data.list, true);
}
await showLoading.end();
showSnackbar.success("导入成功!即将刷新页面...");

View File

@@ -0,0 +1,33 @@
<template>
<span
style="cursor: pointer"
@click="showOverlay()"
v-html="parseHtmlText(decodeURIComponent(props.content))"
/>
</template>
<script lang="ts" setup>
import showHyperLink from "@comp/func/hyperlink.js";
import showSnackbar from "@comp/func/snackbar.js";
import { parseHtmlText } from "@utils/toolFunc.js";
import { onMounted, shallowRef } from "vue";
import { WikiHyperLinkData } from "@/data/index.js";
type TLinkProps = { link: string; content: string };
const props = defineProps<TLinkProps>();
const info = shallowRef<TGApp.App.HyperLink.HyperLinkItem>();
onMounted(async () => {
const find = WikiHyperLinkData.find((i) => i.id.toString() === props.link.slice(1));
if (find) info.value = find;
});
async function showOverlay() {
if (!info.value) {
showSnackbar.warn("未获取到对应HyperLink数据");
return;
}
await showHyperLink({ ...info.value, id: props.link });
}
</script>

View File

@@ -1,5 +1,5 @@
{
"area": "未知",
"area": "璃月港",
"brief": {
"camp": "璃月港",
"constellation": "白驹座",

View File

@@ -5,7 +5,7 @@
"description": "冒险阅历,用于提升冒险等阶",
"type": "系统开放",
"star": 3,
"source": [{ "type": "single", "name": "秘境获取" }],
"source": [{ "name": "秘境获取", "type": "single" }],
"convert": []
},
{
@@ -14,7 +14,7 @@
"description": "好感经验,用于提升好感等级",
"type": "好感成长",
"star": 3,
"source": [{ "type": "single", "name": "秘境获取" }],
"source": [{ "name": "秘境获取", "type": "single" }],
"convert": []
},
{
@@ -45,7 +45,7 @@
{ "name": "冒险与探索奖励", "type": "single" },
{ "name": "洞天百宝兑换", "type": "single" },
{ "name": "激活藏金之花奖励", "type": "single" },
{ "type": "single", "name": "秘境获取" }
{ "name": "秘境获取", "type": "single" }
],
"convert": []
},

View File

@@ -460,7 +460,7 @@
},
{
"id": 10000125,
"contentId": 0,
"contentId": 507505,
"dropDays": [1, 4, 7],
"name": "哥伦比娅",
"itemType": "character",
@@ -2719,7 +2719,7 @@
},
{
"id": 14522,
"contentId": 0,
"contentId": 507623,
"dropDays": [3, 6, 7],
"name": "帷间夜曲",
"itemType": "weapon",

View File

@@ -53,7 +53,7 @@
"contentId": 507503,
"name": "兹白",
"title": "驹隙隐泉",
"area": "未知",
"area": "璃月港",
"birthday": [5, 15],
"star": 5,
"element": "岩",
@@ -1344,7 +1344,20 @@
"release": "",
"weapon": "单手剑",
"nameCard": "",
"costumes": []
"costumes": [
{
"id": 200701,
"isDefault": false,
"name": "复地重天",
"desc": "旅行者的装扮款式。陪伴双「星」遍访无数世界的衣服。它已然见过无数的世界,但还是不明白宇宙的目的究竟为何。但只要思考,总会得到那个「最后的答案」。"
},
{
"id": 200700,
"isDefault": true,
"name": "初升之星",
"desc": "旅行者的装扮款式。陪伴旅人走过漫漫长路的实用衣装。"
}
]
},
{
"id": 10000007,
@@ -1358,7 +1371,20 @@
"release": "",
"weapon": "单手剑",
"nameCard": "",
"costumes": []
"costumes": [
{
"id": 200701,
"isDefault": false,
"name": "复地重天",
"desc": "旅行者的装扮款式。陪伴双「星」遍访无数世界的衣服。它已然见过无数的世界,但还是不明白宇宙的目的究竟为何。但只要思考,总会得到那个「最后的答案」。"
},
{
"id": 200700,
"isDefault": true,
"name": "初升之星",
"desc": "旅行者的装扮款式。陪伴旅人走过漫漫长路的实用衣装。"
}
]
},
{
"id": 10000007,
@@ -1372,7 +1398,20 @@
"release": "",
"weapon": "单手剑",
"nameCard": "",
"costumes": []
"costumes": [
{
"id": 200701,
"isDefault": false,
"name": "复地重天",
"desc": "旅行者的装扮款式。陪伴双「星」遍访无数世界的衣服。它已然见过无数的世界,但还是不明白宇宙的目的究竟为何。但只要思考,总会得到那个「最后的答案」。"
},
{
"id": 200700,
"isDefault": true,
"name": "初升之星",
"desc": "旅行者的装扮款式。陪伴旅人走过漫漫长路的实用衣装。"
}
]
},
{
"id": 10000007,
@@ -1386,7 +1425,20 @@
"release": "",
"weapon": "单手剑",
"nameCard": "",
"costumes": []
"costumes": [
{
"id": 200701,
"isDefault": false,
"name": "复地重天",
"desc": "旅行者的装扮款式。陪伴双「星」遍访无数世界的衣服。它已然见过无数的世界,但还是不明白宇宙的目的究竟为何。但只要思考,总会得到那个「最后的答案」。"
},
{
"id": 200700,
"isDefault": true,
"name": "初升之星",
"desc": "旅行者的装扮款式。陪伴旅人走过漫漫长路的实用衣装。"
}
]
},
{
"id": 10000007,
@@ -1400,7 +1452,20 @@
"release": "",
"weapon": "单手剑",
"nameCard": "",
"costumes": []
"costumes": [
{
"id": 200701,
"isDefault": false,
"name": "复地重天",
"desc": "旅行者的装扮款式。陪伴双「星」遍访无数世界的衣服。它已然见过无数的世界,但还是不明白宇宙的目的究竟为何。但只要思考,总会得到那个「最后的答案」。"
},
{
"id": 200700,
"isDefault": true,
"name": "初升之星",
"desc": "旅行者的装扮款式。陪伴旅人走过漫漫长路的实用衣装。"
}
]
},
{
"id": 10000007,
@@ -1414,7 +1479,20 @@
"release": "",
"weapon": "单手剑",
"nameCard": "",
"costumes": []
"costumes": [
{
"id": 200701,
"isDefault": false,
"name": "复地重天",
"desc": "旅行者的装扮款式。陪伴双「星」遍访无数世界的衣服。它已然见过无数的世界,但还是不明白宇宙的目的究竟为何。但只要思考,总会得到那个「最后的答案」。"
},
{
"id": 200700,
"isDefault": true,
"name": "初升之星",
"desc": "旅行者的装扮款式。陪伴旅人走过漫漫长路的实用衣装。"
}
]
},
{
"id": 10000005,
@@ -1428,7 +1506,20 @@
"release": "",
"weapon": "单手剑",
"nameCard": "",
"costumes": []
"costumes": [
{
"id": 200501,
"isDefault": false,
"name": "复地重天",
"desc": "旅行者的装扮款式。陪伴双「星」遍访无数世界的衣服。它已然见过无数的世界,但还是不明白宇宙的目的究竟为何。但只要思考,总会得到那个「最后的答案」。"
},
{
"id": 200500,
"isDefault": true,
"name": "初升之星",
"desc": "旅行者的装扮款式。陪伴旅人走过漫漫长路的实用衣装。"
}
]
},
{
"id": 10000005,
@@ -1442,7 +1533,20 @@
"release": "",
"weapon": "单手剑",
"nameCard": "",
"costumes": []
"costumes": [
{
"id": 200501,
"isDefault": false,
"name": "复地重天",
"desc": "旅行者的装扮款式。陪伴双「星」遍访无数世界的衣服。它已然见过无数的世界,但还是不明白宇宙的目的究竟为何。但只要思考,总会得到那个「最后的答案」。"
},
{
"id": 200500,
"isDefault": true,
"name": "初升之星",
"desc": "旅行者的装扮款式。陪伴旅人走过漫漫长路的实用衣装。"
}
]
},
{
"id": 10000005,
@@ -1456,7 +1560,20 @@
"release": "",
"weapon": "单手剑",
"nameCard": "",
"costumes": []
"costumes": [
{
"id": 200501,
"isDefault": false,
"name": "复地重天",
"desc": "旅行者的装扮款式。陪伴双「星」遍访无数世界的衣服。它已然见过无数的世界,但还是不明白宇宙的目的究竟为何。但只要思考,总会得到那个「最后的答案」。"
},
{
"id": 200500,
"isDefault": true,
"name": "初升之星",
"desc": "旅行者的装扮款式。陪伴旅人走过漫漫长路的实用衣装。"
}
]
},
{
"id": 10000005,
@@ -1470,7 +1587,20 @@
"release": "",
"weapon": "单手剑",
"nameCard": "",
"costumes": []
"costumes": [
{
"id": 200501,
"isDefault": false,
"name": "复地重天",
"desc": "旅行者的装扮款式。陪伴双「星」遍访无数世界的衣服。它已然见过无数的世界,但还是不明白宇宙的目的究竟为何。但只要思考,总会得到那个「最后的答案」。"
},
{
"id": 200500,
"isDefault": true,
"name": "初升之星",
"desc": "旅行者的装扮款式。陪伴旅人走过漫漫长路的实用衣装。"
}
]
},
{
"id": 10000005,
@@ -1484,7 +1614,20 @@
"release": "",
"weapon": "单手剑",
"nameCard": "",
"costumes": []
"costumes": [
{
"id": 200501,
"isDefault": false,
"name": "复地重天",
"desc": "旅行者的装扮款式。陪伴双「星」遍访无数世界的衣服。它已然见过无数的世界,但还是不明白宇宙的目的究竟为何。但只要思考,总会得到那个「最后的答案」。"
},
{
"id": 200500,
"isDefault": true,
"name": "初升之星",
"desc": "旅行者的装扮款式。陪伴旅人走过漫漫长路的实用衣装。"
}
]
},
{
"id": 10000005,
@@ -1498,7 +1641,20 @@
"release": "",
"weapon": "单手剑",
"nameCard": "",
"costumes": []
"costumes": [
{
"id": 200501,
"isDefault": false,
"name": "复地重天",
"desc": "旅行者的装扮款式。陪伴双「星」遍访无数世界的衣服。它已然见过无数的世界,但还是不明白宇宙的目的究竟为何。但只要思考,总会得到那个「最后的答案」。"
},
{
"id": 200500,
"isDefault": true,
"name": "初升之星",
"desc": "旅行者的装扮款式。陪伴旅人走过漫漫长路的实用衣装。"
}
]
},
{
"id": 10000003,

432
src/data/app/hyperlink.json Normal file
View File

@@ -0,0 +1,432 @@
[
{
"id": 210101,
"name": "<color=#0075de>浪涌冲击</color>",
"desc": "角色对敌人触发<color=#f39001>蒸发反应</color>时释放,造成<color=#0075de>水元素伤害</color>"
},
{
"id": 210102,
"name": "<color=#ef5534>炽燃爆轰</color>",
"desc": "角色对敌人触发<color=#f39001>蒸发反应</color>时释放,造成<color=#ef5534>火元素伤害</color>"
},
{
"id": 210201,
"name": "<color=#7bb100>萦绕种</color>",
"desc": "每产生一枚草原核,都将在当前场上角色周围生成一枚萦绕种(至多同时存在<color=#f39001>4</color>枚):触碰敌人后将被消耗,对其造成<color=#7bb100>草元素伤害</color>"
},
{
"id": 210301,
"name": "<color=#ef5534>荒火冲击</color>",
"desc": "通过<color=#f39001>燃烧反应</color>对敌人造成伤害时,将对该敌人进行计数,计数达到<color=#f39001>6</color>时,将清空计数释放,造成<color=#ef5534>火元素范围伤害</color>"
},
{
"id": 210302,
"name": "<color=#7bb100>助燃响核</color>",
"desc": "角色草元素伤害命中<color=#f39001>燃烧状态</color>的敌人后触发,造成<color=#7bb100>草元素伤害</color>"
},
{
"id": 210401,
"name": "<color=#f39001>火晶庇佑</color>",
"desc": "使队伍中所有角色获得<color=#f39001>50%</color>火元素与岩元素伤害加成。此外,每<color=#f39001>4</color>秒,将获取附近的结晶反应产生的晶片"
},
{
"id": 210402,
"name": "<color=#ef5534>迸火冲击</color>",
"desc": "<color=#f39001>火晶庇佑</color>效果持续期间,每<color=#f39001>6</color>秒,将在角色的位置释放,造成<color=#ef5534>火元素范围伤害</color>"
},
{
"id": 210403,
"name": "<color=#e09800>岩晶破袭</color>",
"desc": "<color=#f39001>火晶庇佑</color>效果持续期间,每<color=#f39001>4</color>秒,将在角色的位置释放,造成<color=#e09800>岩元素范围伤害</color>"
},
{
"id": 210501,
"name": "<color=#f39001>雷晶庇佑</color>",
"desc": "使队伍中所有角色获得<color=#f39001>50%</color>雷元素与岩元素伤害加成。此外,每<color=#f39001>4</color>秒,将获取附近的结晶反应产生的晶片"
},
{
"id": 210502,
"name": "<color=#e09800>轰转磐岩</color>",
"desc": "<color=#f39001>雷晶庇佑</color>效果持续期间,角色的攻击命中敌人后,将在敌人的位置降下,造成<color=#e09800>岩元素伤害</color>"
},
{
"id": 210503,
"name": "<color=#e09800>轰转磐岩</color>",
"desc": "角色每获取<color=#f39001>{0}</color>枚<color=#f39001>雷元素结晶反应</color>产生的晶片,将对附近的一名敌人降下,造成<color=#e09800>岩元素伤害</color>"
},
{
"id": 210601,
"name": "<color=#f39001>易碎</color>",
"desc": "敌人的火元素与雷元素抗性降低<color=#f39001>5.5%</color>至多叠加4层"
},
{
"id": 210602,
"name": "<color=#ef5534>爆炎火球</color>",
"desc": "对处于易碎效果影响下的敌人造成雷元素伤害后,对场上<color=#f39001>所有</color>处于易碎效果的敌人释放,造成<color=#ef5534>火元素伤害</color>"
},
{
"id": 210603,
"name": "<color=#b863ee>霆雷打击</color>",
"desc": "对处于易碎效果影响下的敌人造成火元素伤害后,对场上<color=#f39001>所有</color>处于易碎效果的敌人释放,造成<color=#b863ee>雷元素伤害</color>"
},
{
"id": 210701,
"name": "<color=#7bb100>茂雷飞核</color>",
"desc": "角色对敌人触发原激化、超激化或者蔓激化反应时发射,命中敌人后爆炸,造成<color=#7bb100>草元素范围伤害</color>"
},
{
"id": 210702,
"name": "<color=#b863ee>丰饶落雷</color>",
"desc": "角色对处于原激化状态下的敌人造成雷元素或草元素伤害后降下,造成<color=#b863ee>雷元素伤害</color>"
},
{
"id": 210901,
"name": "<color=#f39001>风之祝祷·水</color>",
"desc": "角色触发扩散反应造成的伤害提升<color=#f39001>80%</color>,角色的生命值上限提升<color=#f39001>8%</color>"
},
{
"id": 210902,
"name": "<color=#0075de>疗愈水球</color>",
"desc": "记录角色治疗的生命值回复量,持续时间结束后爆炸,基于记录的治疗量和<color=#f39001>风之祝祷·水</color>的层数对周围的敌人造成<color=#0075de>水元素范围伤害</color>"
},
{
"id": 210903,
"name": "<color=#0075de>水瀑旋涡</color>",
"desc": "牵引附近的敌人,并基于<color=#f39001>风之祝祷·水</color>的层数造成<color=#0075de>水元素范围伤害</color>"
},
{
"id": 211001,
"name": "<color=#f39001>风之祝祷·雷</color>",
"desc": "角色触发扩散反应造成的伤害提升<color=#f39001>80%</color>,且每次获得<color=#f39001>风之祝祷·雷</color>时为队伍中所有角色恢复<color=#f39001>1.5</color>点元素能量"
},
{
"id": 211002,
"name": "<color=#b863ee>迅切雷刃</color>",
"desc": "角色施放元素爆发时,将生成<color=#f39001>迅切印记</color>。当前场上角色的攻击命中敌人时,将消耗迅切印记释放<color=#b863ee>迅切雷刃</color>,基于<color=#f39001>风之祝祷·雷</color>的层数,对路径上的敌人造成<color=#b863ee>雷元素伤害</color>"
},
{
"id": 211003,
"name": "<color=#b863ee>霆跃雷击</color>",
"desc": "<color=#f39001>风之祝祷·雷</color>持续期间,下落攻击命中敌人时释放,基于风之祝祷·雷的层数计算伤害,造成<color=#b863ee>雷元素范围伤害</color>"
},
{
"id": 211101,
"name": "<color=#f39001>电涌</color>",
"desc": "队伍中所有角色触发<color=#f39001>感电反应</color>造成的伤害提升<color=#f39001>250%</color>,触发<color=#f39001>月感电反应</color>造成的伤害提升<color=#f39001>70%</color>"
},
{
"id": 211102,
"name": "<color=#b863ee>浮雷攻核</color>",
"desc": "电涌效果持续期间跟随当前场上角色生成角色的攻击命中敌人后将向敌人释放3枚雷弹造成<color=#b863ee>雷元素伤害</color>"
},
{
"id": 211103,
"name": "<color=#f39001>雳云</color>",
"desc": "电涌效果持续期间生成,每<color=#f39001>2</color>秒随机进行一次以下形式的攻击:\n1.<color=#b863ee>雳云落雷</color>:对影响范围内的敌人造成<color=#b863ee>雷元素伤害</color>,为当前场上角色恢复<color=#f39001>2</color>点元素能量;\n2.<color=#0075de>雳云暴雨</color>:对影响范围内的敌人造成<color=#0075de>水元素伤害</color>,为当前场上角色恢复<color=#f39001>5%</color>生命值"
},
{
"id": 211201,
"name": "<color=#f39001>火晶庇佑</color>",
"desc": "使队伍中所有角色获得<color=#f39001>70%</color>火元素与岩元素伤害加成。此外,每<color=#f39001>4</color>秒,将获取附近的结晶反应产生的晶片"
},
{
"id": 211202,
"name": "<color=#e09800>岩晶破袭</color>",
"desc": "<color=#f39001>火晶庇佑</color>效果持续期间,将间歇性在角色的位置释放,造成<color=#e09800>岩元素范围伤害</color>"
},
{
"id": 211203,
"name": "<color=#e09800>轰转磐岩</color>",
"desc": "<color=#f39001>火晶庇佑</color>效果持续期间,角色的普通攻击、重击或下落攻击命中敌人时,将在敌人的位置降下,造成<color=#e09800>岩元素伤害</color>"
},
{
"id": 211301,
"name": "<color=#f39001>冰晶庇佑</color>",
"desc": "使队伍中所有角色获得<color=#f39001>50%</color>冰元素与岩元素伤害加成。此外,每<color=#f39001>4</color>秒,将获取附近的结晶反应产生的晶片"
},
{
"id": 211302,
"name": "<color=#e09800>岩晶强袭</color>",
"desc": "<color=#f39001>冰晶庇佑</color>效果持续期间,将间歇性在角色的位置释放,造成<color=#e09800>岩元素范围伤害</color>"
},
{
"id": 211303,
"name": "<color=#e09800>碎冰迸石</color>",
"desc": "<color=#f39001>冰晶庇佑</color>效果持续期间,队伍中冰元素或岩元素角色造成暴击时,将在敌人的位置降下,造成<color=#e09800>岩元素伤害</color>"
},
{
"id": 211401,
"name": "<color=#2bb7d5>冰霜崩破</color>",
"desc": "当前场上的<color=#ef5534>火元素</color>角色触发融化反应后释放,造成<color=#2bb7d5>冰元素伤害</color>"
},
{
"id": 211402,
"name": "<color=#ef5534>炽升火柱</color>",
"desc": "当前场上的<color=#2bb7d5>冰元素</color>角色触发融化反应后释放,造成<color=#ef5534>火元素伤害</color>"
},
{
"id": 211403,
"name": "<color=#f39001>高度温差</color>",
"desc": "角色触发融化反应造成的伤害提升<color=#f39001>30%</color>"
},
{
"id": 211501,
"name": "<color=#f39001>风之祝祷·火</color>",
"desc": "角色触发扩散反应造成的伤害提升<color=#f39001>80%</color>,角色的攻击力提升<color=#f39001>9%</color>。"
},
{
"id": 211502,
"name": "<color=#ef5534>焚风</color>",
"desc": "当前场上角色的攻击对敌人造成伤害时,将消耗该角色的生命值释放,基于风之祝祷·火的层数计算伤害,造成<color=#ef5534>火元素范围伤害</color>"
},
{
"id": 211503,
"name": "<color=#f39001>炽岚</color>",
"desc": "队伍中的<color=#f39001>火元素或风元素</color>角色登场时,将根据该角色的<color=#f39001>元素类型</color>释放,基于风之祝祷·火的层数,造成对应元素类型的范围伤害"
},
{
"id": 211601,
"name": "<color=#f39001>寒意</color>",
"desc": "队伍中所有冰元素和水元素角色的暴击伤害提升<color=#f39001>40%</color>"
},
{
"id": 211602,
"name": "<color=#0075de>寒水震波</color>",
"desc": "当前场上的<color=#2bb7d5>冰元素</color>角色对敌人触发冻结反应后释放,造成<color=#0075de>水元素范围伤害</color>"
},
{
"id": 211603,
"name": "<color=#2bb7d5>冽冰震波</color>",
"desc": "当前场上的<color=#0075de>水元素</color>角色对敌人触发冻结反应后释放,造成<color=#2bb7d5>冰元素范围伤害</color>"
},
{
"id": 211604,
"name": "<color=#f39001>冰裂</color>",
"desc": "敌人的水元素与冰元素抗性降低<color=#f39001>8%</color>,至多叠加<color=#f39001>3</color>层"
},
{
"id": 211701,
"name": "<color=#f39001>低阻</color>",
"desc": "敌人的冰元素和雷元素抗性降低<color=#f39001>3%</color>,物理抗性降低<color=#f39001>6%</color>,至多叠加<color=#f39001>6</color>层"
},
{
"id": 211702,
"name": "<color=#f39001>涌现震波</color>",
"desc": "角色对处于<color=#f39001>低阻</color>效果影响下的敌人造成<color=#f39001>物理伤害</color>后释放,造成物理伤害,每<color=#f39001>3</color>秒至多触发一次"
},
{
"id": 211801,
"name": "<color=#f39001>水晶庇佑</color>",
"desc": "使队伍中所有角色获得<color=#f39001>70%</color>水元素与岩元素伤害加成,月结晶反应造成的伤害提升<color=#f39001>75%</color>。此外,每<color=#f39001>4</color>秒,将获取附近的结晶反应产生的晶片"
},
{
"id": 211802,
"name": "<color=#e09800>岩晶轰袭</color>",
"desc": "<color=#f39001>水晶庇佑</color>效果持续期间,将间歇性在角色的位置释放,造成<color=#e09800>岩元素范围伤害</color>"
},
{
"id": 211803,
"name": "<color=#e09800>涧石</color>",
"desc": "<color=#f39001>水晶庇佑</color>效果持续期间,队伍中水元素或岩元素角色受到治疗时,将在敌人的位置降下,造成<color=#e09800>岩元素伤害</color>"
},
{
"id": 211804,
"name": "<color=#f39001>寒冰荆棘</color>",
"desc": "每<color=#f39001>2</color>秒对周围敌人各造成一次<color=#f39001>冰元素和草元素范围伤害</color>,场上最多同时存在两根寒冰荆棘"
},
{
"id": 211805,
"name": "<color=#b863ee>电气蔓棘</color>",
"desc": "<color=#f39001>寒冰荆棘</color>范围内,角色的雷元素攻击命中敌人后,<color=#f39001>寒冰荆棘</color>将转化为一根存在<color=#f39001>10</color>秒的电气蔓棘,攻击频率提升至每秒<color=#f39001>1</color>次,且造成<color=#f39001>1.5</color>倍雷元素范围伤害"
},
{
"id": 211806,
"name": "<color=#f39001>磁化荆棘</color>",
"desc": "不具有攻击效果,使附近的敌人受到角色的<color=#f39001>冰元素、草元素、雷元素以及物理攻击伤害提升</color>。场上每存在一根磁化荆棘,将获得<color=#f39001>45%</color>的上述提升效果,至多提升<color=#f39001>90%</color>"
},
{
"id": 211901,
"name": "<color=#f39001>风之祝祷·冰</color>",
"desc": "角色触发扩散反应造成的伤害提升<color=#f39001>80%</color>,角色的暴击伤害提升<color=#f39001>12%</color>"
},
{
"id": 211902,
"name": "<color=#2bb7d5>肃蚀</color>",
"desc": "<color=#f39001>风之祝祷·冰</color>持续期间,角色的攻击命中敌人时施加,基于<color=#f39001>风之祝祷·冰</color>的层数间歇性造成<color=#2bb7d5>冰元素伤害</color>"
},
{
"id": 211903,
"name": "<color=#19c78e>冰岚</color>",
"desc": "<color=#f39001>风之祝祷·冰</color>持续期间,角色冲刺时释放,牵引附近的敌人,并基于<color=#f39001>风之祝祷·冰</color>的层数造成<color=#19c78e>风元素伤害</color>"
},
{
"id": 11120001,
"name": "低温冷藏模式",
"desc": "<color=#FFD780FF>点按</color>\n以「低温冷藏」模式启动全频谱多重任务厨艺机关对周围的敌人造成<color=#99FFFFFF>冰元素范围伤害</color>。\n\n<color=#FFD780FF>厨艺机关·低温冷藏模式</color>\n将跟随当前场上角色并将间歇性对附近的敌人发射「冻霜芭菲」造成<color=#99FFFFFF>冰元素伤害</color>。"
},
{
"id": 11120002,
"name": "即兴烹饪模式",
"desc": "<color=#FFD780FF>厨艺机关·即兴烹饪模式</color>\n·在场上放置厨艺机关。厨艺机关可以吸收元素攻击。吸收的元素能量达到临界值时将使爱可菲事先放入其中的食材转化为美食。\n·爱可菲需要时间采购新的食材每周只能通过这种方式制作一定数量的料理。制作料理的次数每周一凌晨4点重置。"
},
{
"id": 11130001,
"name": "援护射击",
"desc": "在夜魂加持状态下,伊法的普通攻击将会转为进行援护射击。\n<color=#FFD780FF>点按</color>\n发射秘药弹。\n<color=#FFD780FF>长按</color>\n持续发射秘药弹。\n<color=#FFD780FF>秘药弹</color>\n造成具有夜魂性质的<color=#80FFD7FF>风元素伤害</color>,并在命中敌人时为队伍中附近的所有角色恢复生命值,回复量受益于伊法自己的元素精通。"
},
{
"id": 11130002,
"name": "救援要义",
"desc": "上限为150点。基于伊法持有的救援要义每1点救援要义会使队伍中附近的角色触发的扩散反应与感电反应造成的伤害提升1.5%月感电反应造成的伤害提升0.2%。救援要义将在伊法的夜魂加持结束时被移除。"
},
{
"id": 11130003,
"name": "夜魂加持·伊法",
"desc": "持续消耗夜魂值。夜魂值耗尽时,或是再次施放时,伊法的夜魂加持将会结束。夜魂加持状态具有如下特性:\n·在咔库库的帮助下悬浮提升伊法的移动速度与抗打断能力在这种状态下伊法将持续消耗夜魂值维持悬浮状态进行冲刺或抬升高度时将额外消耗夜魂值。\n·进行普通攻击时将依据点按、长按转为以不同的方式进行「援护射击」。"
},
{
"id": 11130004,
"name": "镇静标记",
"desc": "该效果会在片刻后爆发消失并造成对应元素属性的范围伤害。至多对4名命中的敌人施加「镇静标记」敌人无法同时处于多种元素属性的「镇静标记」状态下。"
},
{
"id": 11130005,
"name": "蛇之狡谋",
"desc": "丝柯克可以消耗蛇之狡谋,以维持七相一闪模式,或是施放元素爆发极恶技·灭。\n\n丝柯克可以通过以下方式获得蛇之狡谋\n·施放元素战技极恶技·闪后\n·触发突破天赋理外之理中汲取虚境裂隙的效果后。\n\n丝柯克在七相一闪模式下退场时或七相一闪模式结束时将移除丝柯克持有的蛇之狡谋。"
},
{
"id": 11130006,
"name": "七相一闪",
"desc": "施放元素战技<color=#FFD780FF>极恶技·闪</color>时,丝柯克将在一段时间内切换至该模式,并具有如下特性:\n·持续消耗蛇之狡谋\n·提升普通攻击与重击的攻击范围以及丝柯克的抗打断能力\n·进行普通攻击、重击和下落攻击时将转为造成无法被附魔覆盖的<color=#99FFFFFF>冰元素伤害</color>\n·元素爆发<color=#FFD780FF>极恶技·灭</color>将会被替换为特殊的元素爆发<color=#FFD780FF>极恶技·尽</color>\n·丝柯克进行冲刺时能够在水面上移动\n·蛇之狡谋耗尽时或持续时间结束时丝柯克将退出该模式并使元素战技进入冷却。"
},
{
"id": 11130007,
"name": "理外之理",
"desc": "队伍中附近的角色对敌人触发冻结、超导、冰元素扩散或冰元素结晶反应时将在敌人附近创造一枚虚境裂隙。该效果每2.5秒至多触发一次场上至多存在3枚丝柯克自己创造的虚境裂隙。\n\n丝柯克可以通过以下方式汲取附近一定范围内的虚境裂隙\n·在七相一闪模式下进行的重击命中敌人时\n·施放七相一闪模式下的特殊元素爆发<color=#FFD780FF>极恶技·尽</color>时;\n·长按施放元素战技<color=#FFD780FF>极恶技·闪</color>进行快速移动时。\n\n每汲取一枚虚境裂隙丝柯克将获得8点蛇之狡谋。"
},
{
"id": 11130008,
"name": "万流归寂",
"desc": "队伍中附近的元素类型为<color=#80C0FFFF>水元素</color>的角色的<color=#80C0FFFF>水元素</color>攻击命中敌人时,或是队伍中附近的元素类型为<color=#99FFFFFF>冰元素</color>的其他角色的<color=#99FFFFFF>冰元素</color>攻击命中敌人时丝柯克将获得一层持续20秒的死河渡断效果至多叠加3层每层独立计算持续时间。\n每名角色至多通过这种方式使丝柯克获得1层死河渡断效果。\n每层死河渡断效果会使丝柯克在七相一闪模式下时进行的普通攻击造成原本110%/120%/170%的伤害,且施放的元素爆发<color=#FFD780FF>极恶技·灭</color>造成原本105%/115%/160%的伤害。"
},
{
"id": 11130009,
"name": "雾雨秘迹",
"desc": "敌人或队伍中附近的角色接触到雾雨秘迹后,依据接触者的不同,将会产生不同的效果:\n若敌人接触到雾雨秘迹雾雨秘迹将会发生爆炸对周围的敌人造成<color=#80C0FFFF>水元素范围伤害</color>\n若队伍中附近的角色接触到雾雨秘迹雾雨秘迹会将接触者高高弹起。\n\n同时只能存在一个由塔利雅自己创造的雾雨秘迹。"
},
{
"id": 11130010,
"name": "西风之眷",
"desc": "塔利雅施放元素爆发<color=#FFD780FF>纯耀的祷咏</color>时,将会为队伍中自己的角色赋予该效果。\n持续期间圣眷护盾在效果结束时或因伤害破碎时塔利雅可以消耗一层祝颂效果重新唤出圣眷护盾。"
},
{
"id": 11130011,
"name": "祝颂",
"desc": "塔利雅可以通过以下方式获得祝颂效果:\n·队伍中自己的当前场上角色处于西风之眷效果影响下时普通攻击每命中敌人4次塔利雅将获得X层祝颂效果\n·触发突破天赋风色柔愿的眷宠的效果后。\n\n每次西风之眷效果持续期间塔利雅至多获得X层祝颂效果。"
},
{
"id": 11160001,
"name": "薇尔琪塔",
"desc": "与伊涅芙一同战斗的好帮手,可以通过施放元素战技或元素爆发召唤至场上。\n若周围存在敌人每隔2秒薇尔琪塔将快速移动至一名敌人附近对其进行放电攻击造成<color=#FFACFFFF>雷元素范围伤害</color>。\n同时至多存在一个伊涅芙自己创造的薇尔琪塔。\n\n此外在挪德卡莱似乎可以通过某种途径来改变薇尔琪塔的外观…"
},
{
"id": 11190001,
"name": "霜林圣域",
"desc": "用以护守咏月使的术阵,通过施放元素战技<color=#FFD780FF>圣言述咏·终宵永眠</color>唤出。\n霜林圣域将跟随当前场上角色每2秒对术阵中的敌人造成一次<color=#99FF88FF>草元素范围伤害</color>,该伤害视为元素战技伤害。"
},
{
"id": 11190002,
"name": "月咏",
"desc": "用以纺出祷歌的乐节,可以在施放元素爆发时或施放元素爆发后的一段时间内转化为提升绽放、超绽放、烈绽放、月绽放反应伤害的苍色祷歌。\n菈乌玛消耗草露以长按施放元素战技<color=#FFD780FF>圣言述咏·终宵永眠</color>时,依据消耗的草露数,每一枚草露都会使菈乌玛获得一层「月咏」。"
},
{
"id": 11190003,
"name": "苍色祷歌",
"desc": "咏月使世代传承的祷歌,能够提升绽放、超绽放、烈绽放、月绽放反应造成的伤害。\n\n菈乌玛可以通过以下方式获得苍色祷歌\n·施放元素爆发<color=#FFD780FF>圣言述咏·众心为月</color>时;\n·触发元素爆发<color=#FFD780FF>圣言述咏·众心为月</color>中消耗「月咏」的效果时;\n·触发命之座<color=#FFD780FF>「我愿将这血与泪奉予月明」</color>中霜林圣域的效果时。\n\n队伍中的角色造成绽放、超绽放、烈绽放、月绽放反应伤害时将消耗一层「苍色祷歌」提升造成的伤害提升值基于菈乌玛的元素精通。上述伤害同时命中多名敌人时会依据命中敌人的数量消耗「苍色祷歌」层数。\n每层「苍色祷歌」独立计算持续时间。"
},
{
"id": 11190004,
"name": "宵色夜咏",
"desc": "队伍中的角色触发的绽放、超绽放、烈绽放反应造成的伤害能够造成暴击,暴击率固定为{PARAM#P1192101|1S100}%,暴击伤害固定为{PARAM#P1192101|2S100}%。该效果提供的暴击率可以与使对应元素反应能够造成暴击的同类效果提供的暴击率叠加。"
},
{
"id": 11190005,
"name": "月华圣咏",
"desc": "队伍中的角色造成的月绽放反应伤害,暴击率提升{PARAM#P1192101|3S100}%,暴击伤害提升{PARAM#P1192101|4S100}%。"
},
{
"id": 11190007,
"name": "擢升",
"desc": "特殊的伤害提升效果,与其他的伤害提升效果分别独立计算。"
},
{
"id": 11190008,
"name": "草露",
"desc": "队伍中的角色触发月绽放反应后,会为队伍提供的资源。\n触发月绽放反应后的2.5秒内每2.5秒便会为队伍提供一枚草露,草露上限为三枚。持续期间,再次触发月绽放反应时,将刷新上述效果的持续时间。"
},
{
"id": 11200001,
"name": "雷霆交响",
"desc": "消耗更少的元素能量便可以施放的特殊元素爆发。菲林斯造成一次视为月感电反应伤害的<color=#FFACFFFF>雷元素范围伤害</color>。\n<color=#FFD780FF>月兆·满辉</color>:若附近存在雷暴云,还会额外造成一次视为月感电反应伤害的<color=#FFACFFFF>雷元素范围伤害</color>。"
},
{
"id": 11220001,
"name": "影舞",
"desc": "施放元素战技<color=#FFD780FF>弈术·千夜一舞</color>后,奈芙尔将会进入该状态。\n处于「影舞」状态下时若拥有至少1枚草露奈芙尔施放的重击将会被替换为特殊的重击「幻戏」。\n「影舞」状态将在奈芙尔施放{PARAM#P1223201|3S1}次幻戏或持续时间结束时解除。"
},
{
"id": 11220002,
"name": "幻戏",
"desc": "处于元素战技<color=#FFD780FF>弈术·千夜一舞</color>造成的「影舞」状态下时若拥有至少1枚草露奈芙尔施放的重击将会被替换为特殊的、不消耗体力的重击「幻戏」。\n奈芙尔召唤出自身的虚影对敌人发起协同攻击自身与虚影分别依次造成2/3段<color=#99FF88FF>草元素范围伤害</color>。虚影造成的伤害视为月绽放反应伤害,每次施放幻戏后首次召唤虚影时,将消耗一枚草露。"
},
{
"id": 11220003,
"name": "伪秘之帷",
"desc": "奈芙尔通过突破天赋「月下的豪赌」吸收诳言之核后获得的效果,可以提升幻戏造成的伤害。\n每层「伪秘之帷」都会使奈芙尔施放的幻戏造成比原本高{PARAM#P1222101|6S100}%的伤害,初始至多叠加{PARAM#P1222101|1S1}层,持续{PARAM#P1222101|2S1}秒,每层独立计算持续时间。"
},
{
"id": 11230001,
"name": "精质转变",
"desc": "杜林施放元素战技<color=#FFD780FF>二元式·聚分熔炼</color>后获得的效果持续6秒。\n持续期间杜林可以通过点按元素战技进入<color=#FFD780FF>白化之是</color>状态,或是通过点按普通攻击进入<color=#FFD780FF>黑度之否</color>状态。\n在两种状态下杜林会获得不同的强化效果并可以分别施放不同的元素爆发。"
},
{
"id": 11240001,
"name": "呼噜噜秘藏瓶",
"desc": "雅珂达施放元素战技<color=#FFD780FF>奇策·财富分配方案</color>后取出的小型元素瓶。\n雅珂达处于「掠影追袭」状态下时若附近的敌人处于<color=#FF9999FF>火元素</color>、<color=#80C0FFFF>水元素</color>、<color=#FFACFFFF>雷元素</color>或<color=#99FFFFFF>冰元素</color>附着下,呼噜噜秘藏瓶会转化为对应的元素类型,并持续进行装填,且会按照<color=#FF9999FF>火元素</color>、<color=#80C0FFFF>水元素</color>、<color=#FFACFFFF>雷元素</color>、<color=#99FFFFFF>冰元素</color>的优先级进行转化。每次施放元素战技后,呼噜噜秘藏瓶至多能进行一次转化。\n呼噜噜秘藏瓶装满时雅珂达将会退出「掠影追袭」状态对附近的敌人造成一次<color=#80FFD7FF>风元素伤害</color>,并清空呼噜噜秘藏瓶。\n每次施放元素战技时雅珂达都会重新取出一个新的呼噜噜秘藏瓶。\n\n<color=#FFD780FF>月兆·满辉</color><color=#FFD780FF>呼噜噜秘藏瓶</color>装满时,雅珂达不会立刻清空<color=#FFD780FF>呼噜噜秘藏瓶</color>,而是会在接下来的一段时间内,持续消耗瓶中装填的元素,并间歇性向附近的敌人发射<color=#FFD780FF>软绒绒猫猫球</color>,造成<color=#FFD780FF>呼噜噜秘藏瓶</color>对应元素类型的伤害。"
},
{
"id": 11240002,
"name": "猫型家用互助协调器",
"desc": "曾是宝藏猎人的雅珂达随身携带的小道具之一,施放元素爆发<color=#FFD780FF>秘器·猎人的七道具</color>后取出。\n猫型家用互助协调器将会持续为队伍中自己的当前场上角色恢复生命值如果受到治疗的角色生命值高于70%,还会同时为队伍中附近生命值最低的角色恢复生命值。\n\n<color=#FFD780FF>月兆·满辉</color>:若附近的敌人处于<color=#FF9999FF>火元素</color>、<color=#80C0FFFF>水元素</color>、<color=#FFACFFFF>雷元素</color>或<color=#99FFFFFF>冰元素</color>附着下,猫型家用互助协调器将会转化为对应的元素类型,并会按照<color=#FF9999FF>火元素</color>、<color=#80C0FFFF>水元素</color>、<color=#FFACFFFF>雷元素</color>、<color=#99FFFFFF>冰元素</color>的优先级进行转化。每次取出猫型家用互助协调器后至多通过这种方式进行一次元素转化。触发元素转化后猫型家用互助协调器将会间歇性攻击附近至多3名敌人造成对应元素伤害。"
},
{
"id": 10050001,
"name": "额外强化效果",
"desc": "每与一种元素进行过共鸣,旅行者便会获得对应的属性提升:\n<color=#80FFD7FF>风元素</color>暴击率提升10%\n<color=#FFE699FF>岩元素</color>防御力提升20%\n<color=#FFACFFFF>雷元素</color>元素充能效率提升20%\n<color=#99FF88FF>草元素</color>元素精通提升60点\n<color=#80C0FFFF>水元素</color>生命值提升20%\n<color=#FF9999FF>火元素</color>攻击力提升20%\n<color=#99FFFFFF>冰元素</color>暴击伤害提升20%。"
},
{
"id": 11250001,
"name": "引力涟漪",
"desc": "施放元素战技<color=#FFD780FF>万古潮汐</color>后,哥伦比娅将唤出<color=#FFD780FF>引力涟漪</color>。\n引力涟漪将跟随当前场上角色并持续对周围的敌人造成<color=#80C0FFFF>水元素范围伤害</color>。\n\n此外引力涟漪存在期间队伍中附近的角色触发月曜反应时或造成月曜反应伤害时哥伦比娅将在接下来的2秒内获得新月之示效果持续积攒<color=#FFD780FF>引力值</color>每2秒可以积攒20点引力值。\n引力值上限为60点且会在引力涟漪持续时间结束时被移除。\n当引力值积攒至上限时将触发特殊的<color=#FFD780FF>引力干涉</color>,依据为哥伦比娅积攒最多引力值的月曜反应类型,分别产生不同的效果。若为哥伦比娅积攒最多引力值的月曜反应类型为:\n·<color=#FFD780FF>月感电反应</color>:引力干涉将造成一次<color=#FFACFFFF>雷元素范围伤害</color>,该伤害视为月感电反应伤害;\n·<color=#FFD780FF>月绽放反应</color>引力干涉将对周围的敌人发射5枚月露之印造成<color=#99FF88FF>草元素伤害</color>,该伤害视为月绽放反应伤害;\n·<color=#FFD780FF>月结晶反应</color>:引力干涉将造成一次<color=#FFE699FF>岩元素范围伤害</color>,该伤害视为月结晶反应伤害。"
},
{
"id": 11250002,
"name": "月之领域",
"desc": "施放元素爆发<color=#FFD780FF>她的乡愁</color>后,哥伦比娅唤出的领域。\n当前场上角色处于<color=#FFD780FF>月之领域</color>中时,队伍中的所有角色造成的月曜反应伤害将会提升。"
},
{
"id": 11250003,
"name": "山月草露",
"desc": "由哥伦比娅的固有天赋<color=#FFD780FF>新月自己的法则</color>提供的、特殊的草露资源,与草露分别计算上限与持续时间。\n队伍拥有的草露耗尽时将转而消耗<color=#FFD780FF>山月草露</color>。\n<color=#FFD780FF>山月草露</color>上限为三枚。"
},
{
"id": 11270001,
"name": "「夜莺之歌」",
"desc": "叶洛亚施放元素爆发<color=#FFD780FF>鉴照无影</color>后获得的效果,可以提升当前场上角色造成的<color=#FFE699FF>岩元素伤害</color>。"
},
{
"id": 11260001,
"name": "月转时隙",
"desc": "施放元素战技<color=#FFD780FF>天地忽然身</color>后,兹白将切换至该模式。该模式至多持续{PARAM#P1263201|4S1}秒,并具有以下特性:\n·进行普通攻击与重击时将转为造成无法被附魔覆盖的<color=#FFE699FF>岩元素伤害</color>\n·元素战技<color=#FFD780FF>天地忽然身</color>将会被替换为特殊的元素战技<color=#FFD780FF>灵驹飞踏</color>当兹白拥有至少70点<color=#FFD780FF>「时隙浮光」</color>时兹白可以消耗70点「时隙浮光」施放灵驹飞踏造成两次<color=#FFE699FF>岩元素伤害</color>,其中的第二段伤害视为月结晶反应伤害。\n\n<color=#FFD780FF>「时隙浮光」</color>上限为100点兹白可以通过以下方式积攒「时隙浮光」\n·处于<color=#FFD780FF>「月转时隙」</color>模式下时每秒积攒10点「时隙浮光」\n·普通攻击命中敌人时会积攒5点「时隙浮光」每0.5秒至多通过这种方式为兹白积攒一次「时隙浮光」。\n\n<color=#FFD780FF>月兆·满辉</color> \n队伍中附近的角色触发月结晶反应时也会为兹白积攒35点<color=#FFD780FF>「时隙浮光」</color>每4秒至多通过这种方式为兹白积攒一次「时隙浮光」施放特殊元素战技<color=#FFD780FF>灵驹飞踏</color>将会重置通过这种方式积攒<color=#FFD780FF>「时隙浮光」</color>的冷却时间。\n\n施放4次<color=#FFD780FF>灵驹飞踏</color>后,或持续时间结束时,兹白将退出该模式。"
}
]

View File

@@ -10,7 +10,7 @@
{ "id": 15503, "contentId": 1682, "name": "终末嗟叹之诗", "star": 5, "weapon": "弓" },
{ "id": 15502, "contentId": 219, "name": "阿莫斯之弓", "star": 5, "weapon": "弓" },
{ "id": 15501, "contentId": 323, "name": "天空之翼", "star": 5, "weapon": "弓" },
{ "id": 14522, "contentId": 0, "name": "帷间夜曲", "star": 5, "weapon": "法器" },
{ "id": 14522, "contentId": 507623, "name": "帷间夜曲", "star": 5, "weapon": "法器" },
{ "id": 14521, "contentId": 506913, "name": "真语秘匣", "star": 5, "weapon": "法器" },
{ "id": 14520, "contentId": 506390, "name": "纺夜天镜", "star": 5, "weapon": "法器" },
{ "id": 14519, "contentId": 504739, "name": "溢彩心念", "star": 5, "weapon": "法器" },

View File

@@ -1,6 +1,6 @@
/**
* 数据文件入口
* @since Beta v0.9.1
* @since Beta v0.9.2
*/
import type { Schema } from "ajv";
@@ -11,6 +11,7 @@ import calendar from "./app/calendar.json" with { type: "json" };
import character from "./app/character.json" with { type: "json" };
import gacha from "./app/gacha.json" with { type: "json" };
import gachaB from "./app/gachaB.json" with { type: "json" };
import hyperlink from "./app/hyperlink.json" with { type: "json" };
import nameCards from "./app/namecard.json" with { type: "json" };
import weapon from "./app/weapon.json" with { type: "json" };
import arcBirCalendar from "./archive/birth_calendar.json" with { type: "json" };
@@ -42,6 +43,7 @@ export const ArcBirRole: Array<TGApp.Archive.Birth.RoleItem> = arcBirRole;
// Wiki
export const WikiWeaponData: Array<TGApp.App.Weapon.WikiItem> = wikiWeapon;
export const WikiMaterialData: Array<TGApp.App.Material.WikiItem> = wikiMaterial;
export const WikiHyperLinkData: TGApp.App.HyperLink.AppHyperLink = hyperlink;
const avatarFiles = import.meta.glob("./WIKI/character/*.json");

View File

@@ -1,14 +1,14 @@
/**
* 应用入口
* @since Beta v0.9.1
* @since Beta v0.9.2
*/
import type { FeedbackInternalOptions } from "@sentry/core";
import * as Sentry from "@sentry/vue";
import { createApp } from "vue";
import { createApp, defineCustomElement } from "vue";
import { createVuetify } from "vuetify";
import App from "./App.vue";
import TLink from "./components/web/t-link.vue";
import router from "./router/index.js";
import store from "./store/index.js";
@@ -17,6 +17,7 @@ import "vuetify/styles";
import "./assets/index.scss";
const app = createApp(App);
customElements.define("t-link", defineCustomElement(TLink));
Sentry.init({
app,
@@ -25,36 +26,6 @@ Sentry.init({
enableLogs: true,
environment: process.env.NODE_ENV,
integrations: [
Sentry.feedbackAsyncIntegration(<FeedbackInternalOptions>{
// 🌗 主题与注入行为
colorScheme: "system",
autoInject: true,
triggerLabel: "",
// 📝 表单标题与按钮文案
formTitle: "问题反馈",
cancelButtonLabel: "取消",
submitButtonLabel: "提交反馈",
successMessageText: "感谢您的反馈,我们将尽快处理。",
// 🧑 用户信息字段
nameLabel: "反馈人",
namePlaceholder: "请输入您的姓名或昵称",
emailLabel: "电子邮箱",
emailPlaceholder: "请输入您的邮箱地址,以便我们与您联系",
// 🐛 问题描述字段
messageLabel: "问题描述",
messagePlaceholder: "请详细描述您遇到的问题及复现步骤",
isRequiredLabel: "(必填)",
// 📸 截图工具相关
addScreenshotButtonLabel: "添加当前页面截图",
removeScreenshotButtonLabel: "移除截图",
highlightToolText: "标记重点区域",
removeHighlightText: "移除标记",
hideToolText: "遮挡敏感信息",
}),
Sentry.consoleLoggingIntegration({ levels: ["error"] }),
Sentry.browserTracingIntegration({ router }),
],

View File

@@ -108,7 +108,13 @@
<TuaOverview :val-icons="item.energySkillRank" title="元素爆发" />
</div>
<div class="uaw-d-box">
<TuaDetail v-for="floor in item.floors" :key="floor.id" :floor />
<TuaDetail
:uid="uidCur"
:id="item.id"
v-for="floor in item.floors"
:key="floor.id"
:floor
/>
</div>
</div>
</v-window-item>
@@ -146,7 +152,7 @@ const router = useRouter();
const hutaoStore = useHutaoStore();
const { isLogin } = storeToRefs(useAppStore());
const { account, cookie } = storeToRefs(useUserStore());
const { account, cookie, propMap } = storeToRefs(useUserStore());
const { userName } = storeToRefs(hutaoStore);
const userTab = ref<number>(0);
const version = ref<string>();
@@ -342,6 +348,10 @@ async function uploadAbyss(): Promise<void> {
const check = await showDialog.check("确定上传?", "未设置胡桃云账号");
if (!check) return;
}
if (!cookie.value) {
showSnackbar.warn("请登录米社账号");
return;
}
await TGLogger.Info("[UserAbyss][uploadAbyss] 上传深渊数据");
const maxId = Math.max(...localAbyss.value.map((i) => i.id));
const abyssData = localAbyss.value.find((item) => item.id === maxId);
@@ -368,7 +378,8 @@ async function uploadAbyss(): Promise<void> {
await showLoading.start(`正在上传${account.value.gameUid}的深渊数据`, `期数:${abyssData.id}`);
const transAbyss = hutao.Abyss.utils.transData(abyssData);
if (userName.value) transAbyss.ReservedUserName = userName.value;
await showLoading.update("正在获取角色数据");
const check = await refreshAvatars(cookie.value, account.value);
if (!check) return;
const roles = await TSUserAvatar.getAvatars(Number(account.value.gameUid));
if (!roles) {
await showLoading.end();
@@ -378,6 +389,7 @@ async function uploadAbyss(): Promise<void> {
await showLoading.update("正在转换角色数据");
transAbyss.Avatars = hutao.Abyss.utils.transAvatars(roles);
await showLoading.update("正在上传深渊数据");
console.log("uploadAbyss", transAbyss);
const res = await hutao.Abyss.upload(transAbyss);
if (res.retcode !== 0) {
showSnackbar.error(`[${res.retcode}]${res.message}`);
@@ -397,6 +409,44 @@ async function uploadAbyss(): Promise<void> {
}
await showLoading.end();
}
async function refreshAvatars(
ck: TGApp.App.Account.Cookie,
ac: TGApp.Sqlite.Account.Game,
): Promise<boolean> {
await showLoading.update("正在更新角色数据");
const idxRes = await recordReq.index(ck, ac, 1);
if ("retcode" in idxRes) {
await showLoading.update("角色更新失败");
showSnackbar.error(`[${idxRes.retcode}] ${idxRes.message}`);
await TGLogger.Error(JSON.stringify(idxRes));
await showLoading.end();
return false;
}
await showLoading.update("正在更新角色列表");
const listRes = await recordReq.character.list(ck, account.value);
if ("retcode" in listRes) {
await showLoading.update("角色列表更新失败");
showSnackbar.error(`[${listRes.message}] ${listRes.message}`);
await TGLogger.Error(JSON.stringify(listRes));
await showLoading.end();
return false;
}
const idList = listRes.map((i) => i.id.toString());
await showLoading.update(`正在获取 ${idList.length} 个角色详情`);
const detailRes = await recordReq.character.detail(ck, ac, idList);
if ("retcode" in detailRes) {
await showLoading.update("角色详情获取失败");
showSnackbar.error(`[${detailRes.retcode}] ${detailRes.message}`);
await TGLogger.Error(JSON.stringify(detailRes));
await showLoading.end();
return false;
}
propMap.value = detailRes.property_map;
await showLoading.update("正在保存角色数据");
await TSUserAvatar.saveAvatars(ac.gameUid, detailRes.list);
return true;
}
</script>
<style lang="css" scoped>
.uat-left {
@@ -439,7 +489,6 @@ async function uploadAbyss(): Promise<void> {
.ua-btn {
background: var(--tgc-btn-1);
color: var(--btn-text);
font-family: var(--font-title);
img {
width: 24px;
@@ -476,7 +525,7 @@ async function uploadAbyss(): Promise<void> {
.ua-box {
display: flex;
height: calc(100vh - 96px);
height: calc(100vh - 144px);
align-items: flex-start;
justify-content: center;
border: 1px solid var(--common-shadow-2);

View File

@@ -3,8 +3,10 @@
<v-app-bar>
<template #prepend>
<div class="ucp-top-prepend">
<img alt="icon" src="/source/UI/userChallenge.webp" />
<span>幽境危战</span>
<div class="ucp-top-title">
<img alt="icon" src="/source/UI/userChallenge.webp" />
<span>幽境危战</span>
</div>
<v-select
v-model="uidCur"
:hide-details="true"
@@ -417,20 +419,22 @@ async function tryReadChallenge(): Promise<void> {
justify-content: center;
margin-left: 12px;
column-gap: 8px;
}
.ucp-top-title {
position: relative;
display: flex;
align-items: center;
justify-content: center;
color: var(--common-text-title);
column-gap: 8px;
font-family: var(--font-title);
font-size: 20px;
img {
width: 32px;
height: 32px;
}
span {
font-family: var(--font-title);
font-size: 20px;
}
span :first-child {
color: var(--common-text-title);
}
}
.ucp-btn {

View File

@@ -418,7 +418,7 @@ async function refresh(): Promise<void> {
const indexRes = await recordReq.index(cookie.value, account.value, 1);
if ("retcode" in indexRes) {
showSnackbar.error(`[${indexRes.retcode}] ${indexRes.message}`);
await TGLogger.Error(JSON.stringify(indexRes.message));
await TGLogger.Error(JSON.stringify(indexRes));
await showLoading.end();
loadData.value = false;
return;

View File

@@ -88,8 +88,14 @@
:value="item.id"
class="uc-window-item"
>
<div :id="`user-combat-${item.id}`" class="ucw-i-ref">
<div :class="userTab === item.id ? 'ucw-i-ref active' : 'ucw-i-ref'">
<div class="ucw-top">
<img
v-if="isFinTarot(item)"
alt="tarot"
class="ucw-tarot"
src="/icon/combat/tarot.webp"
/>
<div class="ucw-title">
<span></span>
<span>{{ item.id }}</span>
@@ -108,7 +114,13 @@
<TSubLine>使用角色({{ item.detail.backup_avatars.length }})</TSubLine>
<TucAvatars :detail="false" :model-value="item.detail.backup_avatars" />
<div class="ucw-rounds">
<TucRound v-for="(round, idx) in item.detail.rounds_data" :key="idx" :round="round" />
<TucRound
v-for="(round, idx) in item.detail.rounds_data"
:id="item.id"
:key="idx"
:round="round"
:uid="item.uid"
/>
</div>
</div>
</v-window-item>
@@ -271,7 +283,7 @@ async function refreshCombat(): Promise<void> {
async function shareCombat(): Promise<void> {
await TGLogger.Info(`[UserCombat][shareCombat][${userTab.value}] 生成剧诗数据分享图片`);
const fileName = `【真境剧诗】${userTab.value}-${uidCur.value}.png`;
const shareDom = document.querySelector<HTMLElement>(`#user-combat-${userTab.value}`);
const shareDom = document.querySelector<HTMLElement>(`.ucw-i-ref.active`);
if (shareDom === null) {
showSnackbar.error("未找到分享数据");
await TGLogger.Warn(`[UserCombat][shareCombat][${userTab.value}] 未找到分享数据`);
@@ -390,6 +402,11 @@ async function tryReadCombat(): Promise<void> {
showSnackbar.error("导入剧诗数据失败,请检查文件格式是否正确");
}
}
function isFinTarot(data: TGApp.Sqlite.Combat.TableTrans): boolean {
if (!data.hasData) return false;
return data.stat.max_round_id === 10 && data.stat.tarot_finished_cnt > 0;
}
</script>
<style lang="scss" scoped>
.uct-left {
@@ -507,7 +524,8 @@ async function tryReadCombat(): Promise<void> {
display: flex;
width: 100%;
align-items: flex-end;
justify-content: space-between;
justify-content: flex-start;
column-gap: 8px;
}
.ucw-title {
@@ -523,8 +541,13 @@ async function tryReadCombat(): Promise<void> {
}
}
.ucw-tarot {
width: 48px;
}
.ucw-share {
z-index: -1;
margin-left: auto;
font-size: 12px;
opacity: 0.8;
}

View File

@@ -82,14 +82,6 @@
>
导入
</v-btn>
<v-btn
class="gacha-top-btn"
prepend-icon="mdi-import"
variant="elevated"
@click="importUigf4()"
>
导入(v4)
</v-btn>
<v-btn
class="gacha-top-btn"
prepend-icon="mdi-export"
@@ -153,7 +145,7 @@
</v-window-item>
</v-window>
</div>
<UgoUid v-model="ovShow" :mode="ovMode" />
<UgoUid v-model="ovShow" :fpi="ovFpi" :mode="ovMode" />
<UgoHutaoDu v-model="hutaoShow" :mode="htMode" @selected="handleHutaoDu" />
</template>
<script lang="ts" setup>
@@ -179,12 +171,12 @@ import { open, save } from "@tauri-apps/plugin-dialog";
import Hakushi from "@utils/Hakushi.js";
import TGLogger from "@utils/TGLogger.js";
import { str2timeStr, timeStr2str } from "@utils/toolFunc.js";
import { exportUigfData, readUigfData, verifyUigfData } from "@utils/UIGF.js";
import { exportUigfData } from "@utils/UIGF.js";
import { storeToRefs } from "pinia";
import { onMounted, ref, shallowRef, watch } from "vue";
import { useRouter } from "vue-router";
import { AppCalendarData, AppCharacterData, AppWeaponData } from "@/data/index.js";
import { AppCalendarData } from "@/data/index.js";
const router = useRouter();
const hutaoStore = useHutaoStore();
@@ -193,12 +185,14 @@ const { isLogin } = storeToRefs(useAppStore());
const { account, cookie } = storeToRefs(useUserStore());
const { isLogin: isLoginHutao, accessToken, userName, userInfo } = storeToRefs(hutaoStore);
const ovMode = ref<"export" | "import">("import");
const ovShow = ref<boolean>(false);
const ovFpi = ref<string>();
const authkey = ref<string>("");
const uidCur = ref<string>();
const tab = ref<string>("overview");
const ovShow = ref<boolean>(false);
const hutaoShow = ref<boolean>(false);
const ovMode = ref<"export" | "import">("import");
const htMode = ref<UgoHutaoMode>("download");
const selectItem = shallowRef<Array<string>>([]);
const gachaListCur = shallowRef<Array<TGApp.Sqlite.Gacha.Gacha>>([]);
@@ -574,13 +568,9 @@ async function refreshGachaPool(
id: item.id.toString(),
uigf_gacha_type: item.gacha_type === "400" ? "301" : item.gacha_type,
};
if (item.item_type === "角色") {
const find = AppCharacterData.find((char) => char.name === item.name);
if (find) tempItem.item_id = find.id.toString();
} else if (item.item_type === "武器") {
const find = AppWeaponData.find((weapon) => weapon.name === item.name);
if (find) tempItem.item_id = find.id.toString();
}
// TODO: 如果有名字重复的需要注意
const find = AppCalendarData.find((i) => i.name === item.name);
if (find) tempItem.item_id = find.id.toString();
if (tempItem.item_id === "") {
if (hakushiData.value.length === 0) {
await showLoading.update(`未查找到 ${tempItem.name} 的 ItemId正在获取 Hakushi 数据`);
@@ -609,11 +599,6 @@ async function refreshGachaPool(
}
}
function importUigf4(): void {
ovMode.value = "import";
ovShow.value = true;
}
async function loadHakushi(): Promise<void> {
try {
hakushiData.value = await Hakushi.fetch();
@@ -638,28 +623,9 @@ async function importUigf(): Promise<void> {
showSnackbar.cancel("已取消文件选择");
return;
}
await showLoading.start("正在导入祈愿数据", "正在验证祈愿数据");
const check = await verifyUigfData(selectedFile, false);
if (!check) {
await showLoading.end();
return;
}
await showLoading.update("正在读取祈愿数据");
const remoteData = await readUigfData(selectedFile);
await showLoading.update(`UID${remoteData.info.uid},共 ${remoteData.list.length} 条数据`);
if (remoteData.list.length === 0) {
await showLoading.end();
showSnackbar.error("导入的祈愿数据为空");
return;
}
await TSUserGacha.mergeUIGF(remoteData.info.uid, remoteData.list, true);
await showLoading.end();
showSnackbar.success(`成功导入 ${remoteData.list.length} 条祈愿数据,即将刷新页面`);
await TGLogger.Info(
`[UserGacha][importUigf] 成功导入 ${remoteData.info.uid}${remoteData.list.length} 条祈愿数据`,
);
await new Promise<void>((resolve) => setTimeout(resolve, 1500));
window.location.reload();
ovFpi.value = selectedFile;
ovMode.value = "import";
ovShow.value = true;
}
// 导出当前UID的祈愿数据
@@ -742,16 +708,28 @@ async function checkData(): Promise<void> {
await showLoading.start("正在检测数据", `UID:${uidCur.value},共${gachaListCur.value.length}`);
for (const data of gachaListCur.value) {
if (data.itemId === "") {
const find = hakushiData.value.find((i) => i.name === data.name && i.type === data.type);
// TODO: 如果有名字重复的需要注意
const find = AppCalendarData.find((i) => i.name === data.name);
if (find) {
await showLoading.update(`${data.name} -> ${find.id}`);
await TSUserGacha.update.itemId(data, find.id);
await TSUserGacha.update.itemId(data, find.id.toString());
cnt++;
} else {
await showLoading.update(`[${data.id}]${data.type}-${data.name}未找到ID`);
await TGLogger.Warn(`[${data.id}]${data.type}-${data.name}未找到ID`);
fail++;
continue;
}
if (hakushiData.value.length === 0) {
await showLoading.update(`尝试获取Hakushi数据`);
await loadHakushi();
}
const find2 = hakushiData.value.find((i) => i.name === data.name && i.type === data.type);
if (find2) {
await showLoading.update(`${data.name} -> ${find2.id}`);
await TSUserGacha.update.itemId(data, find2.id);
cnt++;
continue;
}
await showLoading.update(`[${data.id}]${data.type}-${data.name}未找到ID`);
await TGLogger.Warn(`[${data.id}]${data.type}-${data.name}未找到ID`);
fail++;
}
}
await showLoading.end();

View File

@@ -1,4 +1,4 @@
<!-- 胡桃云统计数据 TODO: 角色持有优化&旅行者处理 -->
<!-- 胡桃云统计数据 -->
<template>
<v-app-bar>
<template #prepend>

View File

@@ -144,11 +144,11 @@ async function toOuter(item?: TGApp.App.Weapon.WikiBriefInfo): Promise<void> {
.ww-list {
position: relative;
display: grid;
overflow: hidden auto;
width: 100%;
padding-right: 8px;
gap: 8px;
grid-template-columns: repeat(3, 160px);
overflow: hidden auto;
}
.ww-detail {

View File

@@ -64,21 +64,22 @@
</template>
</v-app-bar>
<div class="wrap">
<v-virtual-scroll :items="seriesList" class="left-wrap" item-height="60">
<template #default="{ item }">
<TuaSeries
v-model:cur="selectedSeries"
:series="item"
:uid="uidCur"
@click="selectedSeries = item"
/>
</template>
</v-virtual-scroll>
<div class="left-wrap">
<TuaSeries
v-for="item in seriesList"
:key="item.id"
v-model:cur="selectedSeries"
:hideFin
:series="item"
:uid="uidCur"
@selected="handleSeriesSelect"
/>
</div>
<TuaAchiList
v-model:isSearch="isSearch"
v-model:search="search"
v-model:series="selectedSeries"
:hideFin="hideFin"
:hideFin
:uid="uidCur"
/>
</div>
@@ -110,7 +111,7 @@ import { useRoute, useRouter } from "vue-router";
import { AppAchievementSeriesData } from "@/data/index.js";
const seriesList = AppAchievementSeriesData.sort((a, b) => a.order - b.order).map((s) => s.id);
const seriesList = AppAchievementSeriesData.sort((a, b) => a.order - b.order);
const route = useRoute();
const router = useRouter();
@@ -171,6 +172,14 @@ function switchHideFin(): void {
showSnackbar.success(`${text}`);
}
function handleSeriesSelect(id: number): void {
if (selectedSeries.value === id) {
showSnackbar.warn("已经选中当前系列");
return;
}
selectedSeries.value = id;
}
async function refreshOverview(): Promise<void> {
overview.value = await TSUserAchi.getOverview(uidCur.value);
}
@@ -253,7 +262,7 @@ async function handleImportOuter(app: string): Promise<void> {
await showLoading.start("正在导入数据", "正在读取剪贴板");
const clipboard = await window.navigator.clipboard.readText();
await showLoading.update("正在验证数据");
const check = await verifyUiafDataClipboard();
const check = await verifyUiafDataClipboard(clipboard);
if (!check) {
await showLoading.end();
return;
@@ -388,12 +397,15 @@ async function toYae(): Promise<void> {
.left-wrap {
position: relative;
display: flex;
width: 332px;
height: 100%;
box-sizing: border-box;
flex-direction: column;
flex-shrink: 0;
padding-right: 8px;
overflow-y: auto;
row-gap: 8px;
}
:deep(.v-virtual-scroll__item + .v-virtual-scroll__item) {

View File

@@ -26,7 +26,7 @@
</div>
</template>
</v-list-item>
<v-list-item @click="confirmUpdateDevice()" title="刷新设备信息">
<v-list-item title="刷新设备信息" @click="confirmUpdateDevice()">
<template #prepend>
<div class="config-icon">
<v-icon>mdi-refresh</v-icon>
@@ -133,6 +133,22 @@
/>
</template>
</v-list-item>
<v-list-item subtitle="右下反馈按钮显隐,页面刷新后生效" title="用户反馈">
<template #prepend>
<div class="config-icon">
<v-icon>mdi-lightbulb-on-outline</v-icon>
</div>
</template>
<template #append>
<v-switch
v-model="appStore.showFeedback"
:inset="true"
:label="appStore.showFeedback ? '开启' : '关闭'"
class="config-switch"
color="#FAC51E"
/>
</template>
</v-list-item>
<v-list-item v-if="platform() === 'windows'" title="分享设置">
<template #subtitle>默认保存到剪贴板超过{{ shareDefaultFile }}MB时保存到文件</template>
<template #prepend>

View File

@@ -19,14 +19,18 @@
</div>
</template>
<script lang="ts" setup>
import useAppStore from "@store/app.js";
import { tryReadGameVer } from "@utils/TGGame.js";
import { storeToRefs } from "pinia";
const { gameDir } = storeToRefs(useAppStore());
import { invoke } from "@tauri-apps/api/core";
import { appConfigDir, resourceDir } from "@tauri-apps/api/path";
import { copyFile, exists } from "@tauri-apps/plugin-fs";
async function test() {
await tryReadGameVer(gameDir.value);
await invoke("is_msix");
const filePath = `${await resourceDir()}\\resources\\YaeAchievementLib.dll`;
console.log(filePath);
const check = await exists(filePath);
console.log(check);
const targetPath = `${await appConfigDir()}\\YaeAchievementLib.dll`;
await copyFile(filePath, targetPath);
}
</script>
<style lang="css" scoped>

View File

@@ -1,8 +1,12 @@
/**
* 用户账户模块
* @since Beta v0.9.0
* @since Beta v0.9.2
*/
import showLoading from "@comp/func/loading.js";
import showSnackbar from "@comp/func/snackbar.js";
import bbsReq from "@req/bbsReq.js";
import passportReq from "@req/passportReq.js";
import { path } from "@tauri-apps/api";
import { exists, mkdir, readTextFile, writeTextFile } from "@tauri-apps/plugin-fs";
import TGLogger from "@utils/TGLogger.js";
@@ -136,6 +140,87 @@ async function saveAccount(data: TGApp.App.Account.User): Promise<void> {
await db.execute(sql);
}
/**
* 检测并更新
* @since Beta v0.9.2
* @returns 无返回值
*/
async function updateAllAccountCk(): Promise<void> {
const accounts = await getAllAccount();
const checkTime = 5 * 24 * 3600 * 1000;
let cnt = 0;
for (const account of accounts) {
const diffTime = Date.now() - new Date(account.updated).getTime();
if (diffTime > checkTime) {
await TGLogger.Info(`更新${account.uid}Cookie上次更新:${account.updated}`);
const update = await updateAccountCk(account);
if (update) {
showSnackbar.success(`成功更新${account.uid}的Cookie`);
cnt++;
}
}
}
if (cnt > 0) await showLoading.end();
}
/**
* 更新用户Cookie
* @since Beta v0.9.2
* @param data - 用户信息
* @returns 是否更新成功
*/
async function updateAccountCk(data: TGApp.App.Account.User): Promise<boolean> {
await showLoading.start("正在更新用户Cookie", `UID:${data.uid},上次更新:${data.updated}`);
const ck = data.cookie;
await showLoading.update("正在获取 LToken");
const ltokenRes = await passportReq.lToken.get(ck);
if (typeof ltokenRes !== "string") {
await showLoading.end();
showSnackbar.error(`[${ltokenRes.retcode}]${ltokenRes.message}`);
await TGLogger.Error(`获取LToken失败${ltokenRes.retcode}-${ltokenRes.message}`);
return false;
}
ck.ltoken = ltokenRes;
await showLoading.update("正在获取 CookieToken");
const cookieTokenRes = await passportReq.cookieToken(ck);
if (typeof cookieTokenRes !== "string") {
await showLoading.end();
showSnackbar.error(`[${cookieTokenRes.retcode}]${cookieTokenRes.message}`);
await TGLogger.Error(
`获取CookieToken失败${cookieTokenRes.retcode}-${cookieTokenRes.message}`,
);
return false;
}
ck.cookie_token = cookieTokenRes;
await showLoading.update("正在获取用户信息");
const briefRes = await bbsReq.userInfo(ck);
if ("retcode" in briefRes) {
await showLoading.end();
showSnackbar.error(`[${briefRes.retcode}]${briefRes.message}`);
await TGLogger.Error(`获取用户数据失败:${briefRes.retcode}-${briefRes.message}`);
return false;
}
const updated = timestampToDate(new Date().getTime());
await showLoading.update("正在写入数据库");
const db = await TGSqlite.getDB();
try {
// 让 SQLite 在遇到锁时等待(毫秒)
await db.execute("PRAGMA busy_timeout = 1000;");
// 立即获取写锁,减少中途被抢占的概率
await db.execute("BEGIN IMMEDIATE;");
await db.execute(
"UPDATE UserAccount SET cookie = '?' AND brief = '?' AND updated = '?' WHERE uid = '?';",
[JSON.stringify(ck), JSON.stringify(briefRes), updated, data.uid],
);
await db.execute("COMMIT;");
return true;
} catch (innerErr) {
await db.execute("ROLLBACK;");
console.error(innerErr);
return false;
}
}
/**
* 备份用户数据
* @since Beta v0.6.0
@@ -311,6 +396,7 @@ const TSUserAccount = {
getAllAccount,
getAccount: getUserAccount,
saveAccount,
updateCk: updateAllAccountCk,
copy: copyCookie,
deleteAccount,
backup: backUpAccount,

View File

@@ -1,6 +1,6 @@
/**
* 用户成就模块
* @since Beta v0.9.0
* @since Beta v0.9.2
*/
import { UiafAchiStatEnum } from "@enum/uiaf.js";
@@ -123,9 +123,30 @@ async function getAchi(
return res[0];
}
/**
* 对混合系列成就数据进行排序
* @since Beta v0.9.2
* @param data - 成旧数据
* @returns 排序后的成就数据
*/
function sortMixAchi(
data: Array<TGApp.App.Achievement.RenderItem>,
): Array<TGApp.App.Achievement.RenderItem> {
return data.sort((a, b) => {
if (a.isCompleted !== b.isCompleted) return Number(a.isCompleted) - Number(b.isCompleted);
if (!a.isCompleted) {
if (a.version !== b.version) return Number(b.version) - Number(a.version);
return a.order - b.order;
}
if (b.completedTime !== a.completedTime) return b.completedTime.localeCompare(a.completedTime);
if (a.version !== b.version) return Number(b.version) - Number(a.version);
return b.order - a.order;
});
}
/**
* 获取成就数据
* @since Beta v0.6.0
* @since Beta v0.9.2
* @param uid - 存档 UID
* @param series - 成就系列ID
* @returns 成就数据
@@ -135,7 +156,7 @@ async function getAchievements(
series?: number,
): Promise<Array<TGApp.App.Achievement.RenderItem>> {
const db = await TGSqlite.getDB();
const res: Array<TGApp.App.Achievement.RenderItem> = [];
let res: Array<TGApp.App.Achievement.RenderItem> = [];
const userData = await db.select<Array<TGApp.Sqlite.Achievement.TableRaw>>(
"SELECT * FROM Achievements WHERE uid = ?;",
[uid],
@@ -148,9 +169,11 @@ async function getAchievements(
const achievement = getRenderAchi(achi, uid, achiFind);
res.push(achievement);
}
res.sort(
(a, b) => a.isCompleted.toString().localeCompare(b.isCompleted.toString()) || a.order - b.order,
);
if (series && series !== -1) {
res.sort((a, b) => Number(a.isCompleted) - Number(b.isCompleted) || a.order - b.order);
} else {
res = sortMixAchi(res);
}
return res;
}
@@ -173,7 +196,7 @@ async function searchAchi(
const versionReg = /^v\d+(\.\d+)?$/;
const idReg = /^i\d+$/;
let rawData: Array<TGApp.App.Achievement.Item>;
const res: Array<TGApp.App.Achievement.RenderItem> = [];
let res: Array<TGApp.App.Achievement.RenderItem> = [];
if (versionReg.test(keyword)) {
const version = keyword.replace("v", "");
rawData = AppAchievementsData.filter((i) => i.version.includes(version));
@@ -193,9 +216,7 @@ async function searchAchi(
else achievement = getRenderAchi(data, uid, achiFind);
res.push(achievement);
}
res.sort(
(a, b) => a.isCompleted.toString().localeCompare(b.isCompleted.toString()) || a.order - b.order,
);
res = sortMixAchi(res);
return res;
}

View File

@@ -65,6 +65,8 @@ const useAppStore = defineStore(
const cancelLike = ref<boolean>(true);
/** 关闭窗口时最小化到托盘 */
const closeToTray = ref<boolean>(false);
/** 展示反馈按钮 */
const showFeedback = ref<boolean>(true);
/**
* 初始化应用状态
@@ -88,6 +90,7 @@ const useAppStore = defineStore(
postViewWide.value = true;
cancelLike.value = true;
closeToTray.value = false;
showFeedback.value = true;
initDevice();
}
@@ -145,6 +148,7 @@ const useAppStore = defineStore(
postViewWide,
cancelLike,
closeToTray,
showFeedback,
init,
changeTheme,
getImageUrl,
@@ -173,6 +177,7 @@ const useAppStore = defineStore(
"postViewWide",
"cancelLike",
"closeToTray",
"showFeedback",
],
},
{

28
src/types/App/HyperLink.d.ts vendored Normal file
View File

@@ -0,0 +1,28 @@
/**
* HyperLink
* @since Beta v0.9.2
*/
declare namespace TGApp.App.HyperLink {
/**
* 文件类型
* @since Beta v0.9.2
*/
type AppHyperLink = Array<HyperLinkItem>;
/**
* HyperLinkItem
* @since Beta v0.9.2
*/
type HyperLinkItem = {
/** id */
id: number;
/** name */
name: string;
/**
* 描述
* @remarks htmlText
*/
desc: string;
};
}

View File

@@ -1,13 +1,13 @@
/**
* 游戏文件相关功能
* @since Beta v0.9.1
* @since Beta v0.9.2
*/
import showDialog from "@comp/func/dialog.js";
import showSnackbar from "@comp/func/snackbar.js";
import { invoke } from "@tauri-apps/api/core";
import { sep } from "@tauri-apps/api/path";
import { exists, readTextFile, readTextFileLines } from "@tauri-apps/plugin-fs";
import { documentDir, resourceDir, sep } from "@tauri-apps/api/path";
import { copyFile, exists, mkdir, readTextFile, readTextFileLines } from "@tauri-apps/plugin-fs";
import { platform } from "@tauri-apps/plugin-os";
import TGLogger from "@utils/TGLogger.js";
@@ -60,9 +60,34 @@ export async function isRunInAdmin(): Promise<boolean> {
return isAdmin;
}
/**
* 尝试移动dll
* @since Beta v0.9.2
* @returns 无返回值
*/
export async function tryCopyYae(): Promise<boolean> {
const srcDllPath = `${await resourceDir()}${sep()}resources${sep()}YaeAchievementLib.dll`;
const srcCheck = await exists(srcDllPath);
if (!srcCheck) {
showSnackbar.warn("未检测到本地 dll");
return false;
}
const targetDir = `${await documentDir()}${sep()}TeyvatGuide`;
await mkdir(targetDir, { recursive: true });
const targetPath = `${targetDir}${sep()}YaeAchievementLib.dll`;
console.log(targetPath);
await copyFile(srcDllPath, targetPath);
const check2 = await exists(targetPath);
if (!check2) {
showSnackbar.warn("移动 dll 失败,请手动移动");
return false;
}
return true;
}
/**
* 尝试调用Yae
* @since Beta v0.9.1
* @since Beta v0.9.2
* @param gameDir - 游戏目录
* @param uid - 启动UID
* @returns void
@@ -92,7 +117,6 @@ export async function tryCallYae(gameDir: string, uid?: string): Promise<void> {
}
const adminCheck = await isRunInAdmin();
if (!adminCheck) {
showSnackbar.warn("未检测到管理员权限");
const check = await showDialog.check("是否以管理员模式重启?", "该功能需要管理员权限才能使用");
if (!check) {
showSnackbar.cancel("已取消以管理员模式重启");
@@ -107,6 +131,8 @@ export async function tryCallYae(gameDir: string, uid?: string): Promise<void> {
}
return;
}
const isMsix = await invoke<boolean>("is_msix");
if (isMsix) await tryCopyYae();
const input = await showDialog.input("请输入存档UID", "UID:", uid);
if (!input) {
showSnackbar.cancel("已取消存档导入");
@@ -117,7 +143,7 @@ export async function tryCallYae(gameDir: string, uid?: string): Promise<void> {
return;
}
try {
await invoke("call_yae_dll", { gamePath: gamePath, uid: input });
await invoke("call_yae_dll", { gamePath: gamePath, uid: input, is_msix: isMsix });
} catch (err) {
showSnackbar.error(`调用Yae DLL失败: ${err}`);
}

View File

@@ -1,6 +1,6 @@
/**
* UIAF工具类
* @since Beta v0.6.0
* @since Beta v0.9.2
*/
import showSnackbar from "@comp/func/snackbar.js";
@@ -59,14 +59,14 @@ export async function verifyUiafData(path: string): Promise<boolean> {
/**
* 验证UIAF数据-剪贴板
* @since Beta v0.4.7
* @since Beta v0.9.2
* @param data - 剪贴板文本
* @returns 是否验证通过
*/
export async function verifyUiafDataClipboard(): Promise<boolean> {
export async function verifyUiafDataClipboard(data: string): Promise<boolean> {
// @ts-expect-error-next-line
const ajv = new Ajv();
const validate = ajv.compile(UiafSchema);
const data = await window.navigator.clipboard.readText();
try {
const fileJson = JSON.parse(data);
if (!validate(fileJson)) {
@@ -80,7 +80,6 @@ export async function verifyUiafDataClipboard(): Promise<boolean> {
return true;
} catch (e) {
showSnackbar.error(`UIAF 数据格式错误 ${e}`);
await TGLogger.Error(`UIAF 数据格式错误,剪贴板数据:${data}`);
await TGLogger.Error(`错误信息 ${e}`);
return false;
}

View File

@@ -1,6 +1,6 @@
/**
* UIGF工具类
* @since Beta v0.9.0
* @since Beta v0.9.2
*/
import showLoading from "@comp/func/loading.js";
@@ -105,6 +105,36 @@ function convertDataToUigf(
});
}
/**
* 检测是否是v4版本的UIGF
* @since Beta v0.9.2
* @param path - UIGF 文件路径
* @returns 是否是v4null表示数据异常
*/
export async function checkUigfData(path: string): Promise<boolean | null> {
try {
const fileData: string = await readTextFile(path);
const fileJson = JSON.parse(fileData);
if (!("info" in fileJson) || typeof fileJson.info !== "object") {
validateUigf4Data(fileJson);
return null;
}
if ("uigf_version" in fileJson.info) {
const check = validateUigfData(fileJson);
if (!check) return null;
return false;
}
const check = validateUigf4Data(fileJson);
if (!check) return null;
return true;
} catch (e) {
showSnackbar.error(`UIGF校验异常${e}`);
await TGLogger.Error(`[checkUigfData]路径:${path}`);
await TGLogger.Error(`[checkUigfData]异常:${e}`);
return null;
}
}
/**
* 检测是否存在 UIGF 数据,采用 ajv 验证 schema
* @since Beta v0.6.5

View File

@@ -1,6 +1,6 @@
/**
* 一些工具函数
* @since Beta v0.9.1
* @since Beta v0.9.2
*/
import { tz } from "@date-fns/tz";
@@ -185,31 +185,36 @@ export function getRandomString(length: number, type: string = "all"): string {
/**
* 解析带样式的文本
* @since Beta v0.8.1
* @since Beta v0.9.2
* @param desc - 带样式的文本
* @returns 解析后的文本
*/
export function parseHtmlText(desc: string): string {
const linkReg = /\{LINK#(.*?)}(.*?)\{\/LINK}/g;
let linkMatch = linkReg.exec(desc);
while (linkMatch !== null) {
const link = linkMatch[1];
const text = linkMatch[2];
// TODO: 后续处理 t-link
desc = desc.replace(linkMatch[0], `<t-link data-link="${link}">${text}</t-link>`);
linkMatch = linkReg.exec(desc);
}
const colorReg = /<color=(.*?)>(.*?)<\/color>/g;
const linkReg = /\{LINK#(.*?)}(.*?)\{\/LINK}/g;
let colorMatch = colorReg.exec(desc);
while (colorMatch !== null) {
const color = colorMatch[1];
const text = new DOMParser().parseFromString(colorMatch[2], "text/html").body.textContent;
let title = text;
const colorLinkMatch = text.match(linkReg);
if (colorLinkMatch !== null) title = colorLinkMatch[2];
desc = desc.replace(
colorMatch[0],
`<span title="${text}" style="color: ${color}">${text}</span>`,
`<span title="${title}" style="color: ${color}">${text}</span>`,
);
colorMatch = colorReg.exec(desc);
}
let linkMatch = linkReg.exec(desc);
while (linkMatch !== null) {
const link = linkMatch[1];
const text = linkMatch[2];
desc = desc.replace(
linkMatch[0],
`<t-link content="${encodeURIComponent(text)}" link="${link}"></t-link>`,
);
linkMatch = linkReg.exec(desc);
}
desc = desc.replace(/\\n/g, "<br />");
return desc;
}