Compare commits

...

38 Commits

Author SHA1 Message Date
目棃
0f546e8a57 🚀 v0.7.0 2025-02-28 10:05:04 +08:00
目棃
26070919c9 🚸 昵称转码 2025-02-28 09:53:09 +08:00
目棃
bfd73c3d73 💄 调整UI 2025-02-28 09:43:57 +08:00
目棃
415927cd0f 📝 更新文档 2025-02-28 09:43:32 +08:00
目棃
aa6c75e59f 🐛 修复路由跳转不生效 2025-02-27 23:08:04 +08:00
目棃
c31c86bd56 🍱 更新下半卡池 2025-02-27 21:49:14 +08:00
目棃
1ee3c35216 💄 调整等级样式 2025-02-27 17:21:28 +08:00
目棃
6676357296 🧑‍💻 更换json渲染组件,渲染公告解析json 2025-02-27 14:06:08 +08:00
目棃
3ba72969d9 ⬆️ 更新依赖 2025-02-27 14:01:51 +08:00
目棃
c9ea10f0ef 🔊 打印到文件 2025-02-26 17:33:04 +08:00
目棃
17f1b39414 🚸 二维码支持生成分享图 2025-02-26 17:27:15 +08:00
目棃
70216734a3 🌱 comboToken登录 2025-02-26 11:31:15 +08:00
目棃
c25bde1b7a 💡 v0.6.10 → v0.7.0 2025-02-26 10:10:36 +08:00
目棃
beb457a884 🚸 确保同一时间只能执行一个脚本
close #144
2025-02-26 09:05:39 +08:00
目棃
1b1abb9b88 🏷️ 修正调用异常 2025-02-25 17:56:33 +08:00
目棃
112bd3b938 🚸 调整逻辑 2025-02-25 16:17:51 +08:00
目棃
55adf31613 完成米游社任务
#144
2025-02-25 15:49:08 +08:00
目棃
cdddbae520 🌱 获取任务完成情况 2025-02-25 13:43:47 +08:00
目棃
c4bd07069c 🎨 移除测试点击 2025-02-25 12:00:14 +08:00
目棃
71b45584e8 🎨 修正逻辑 2025-02-25 10:42:22 +08:00
目棃
e343d37a01 💡 特定请求需要验证码登录返回ck 2025-02-25 10:39:59 +08:00
目棃
7f5ffab2a7 🎨 调整条件 2025-02-25 10:39:40 +08:00
目棃
d2e6d112d5 🔧 无痕浏览,默认开启 2025-02-24 19:09:49 +08:00
目棃
5b390d3ad1 🌱 新页面 2025-02-24 17:56:49 +08:00
目棃
798c4bd7d5 🍱 png2webp 2025-02-24 15:18:07 +08:00
目棃
7e133176e5 🌱 分享 2025-02-24 15:12:41 +08:00
目棃
87a345ffa7 🐛 修正浏览参数 2025-02-24 15:02:09 +08:00
目棃
2bafb6d491 🌱 点赞 2025-02-24 14:43:09 +08:00
目棃
0f278ad25e 🌱 浏览帖子 2025-02-24 13:44:41 +08:00
目棃
1da157abbd 🌱 获取任务完成情况 2025-02-24 12:01:50 +08:00
目棃
2fa9f88da2 💄 调整可视页码 2025-02-23 21:31:06 +08:00
目棃
9e2f91b4d4 ♻️ 重构生日计算 2025-02-22 11:30:14 +08:00
目棃
2b15e1a351 💄 微调UI 2025-02-20 17:00:59 +08:00
目棃
25f95d9f90 🐛 修复背景图片更新异常 2025-02-13 17:20:02 +08:00
目棃
03e33872c2 ♻️ 处理特殊情况x2 2025-02-12 15:34:57 +08:00
目棃
5b5f96c2d3 ♻️ 处理特殊情况 2025-02-12 15:15:18 +08:00
目棃
0005e4eb74 🐛 修复解析异常 2025-02-12 13:47:14 +08:00
目棃
89d1b2c6a7 ⬆️ 更新依赖 2025-02-12 13:33:42 +08:00
77 changed files with 3530 additions and 2104 deletions

View File

@@ -9,160 +9,16 @@ Update: 2025-02-11
>
> 更新于 `2025-02-11 10:57:49`
## [0.6.9](https://github.com/BTMuli/TeyvatGuide/releases/v0.6.9) (2025-02-11)
## [0.7.0](https://github.com/BTMuli/TeyvatGuide/releases/v0.7.0) (025-02-28)
- 🍱 更新5.4资源 [`#141`](https://github.com/BTMuli/TeyvatGuide/issues/141)
- 🐛 修复米游社子窗口路径解析异常
- 🐛 修复特定条件下真境剧诗角色元素图标渲染异常
- 🐛 修复名片图鉴浮窗渲染异常
- 🚸 调整兑换码入口显示判断逻辑
- 🚸 成就导入不允许点击外部取消,调整刷新逻辑
- 🚸 下载封面图时显示封面链接
- 💄 调整部分UI
## [0.6.8](https://github.com/BTMuli/TeyvatGuide/releases/v0.6.8) (2025-01-22)
- ✨ 扫码登录
- ✨ 调整祈愿记录图表样式,新增祈愿日历&祈愿堆叠柱状图
- ✨ 支持配置帖子详情图像质量默认80%
- ✨ 支持帖子详情图像查看原图当质量配置为100%时,该按钮不显示
- ✨ 深渊上传支持胡桃账户设置
- 🚸 降低祈愿全量刷新耗时
- 🚸 加快帖子加载速度,降低内存占用
- 💄 调整角色卡片样式
- 💄 调整角色名片样式,增加描述清晰度
- 💄 调整深渊Wiki队伍搭配窗口高度
- 💄 mac下不显示分享设置
- 💄 调整战绩页新洞天渲染样式
- 🔥 深渊Wiki移除第10层数据
- 🐛 修复深渊数据恢复异常
- 🐛 修复 loading 组件 empty 状态设置异常
- ♻️ 优化帖子加载逻辑当刷新内容不足20条时下次刷新数量为20-当前数量如刷新数量为19条则下次刷新数量为1条
- ♻️ 动态获取分区列表&版块列表
- ♻️ 重构部分路由处理,当话题/帖子切换分区/版块时,页面刷新不重置当前分区/版块
## [0.6.7](https://github.com/BTMuli/TeyvatGuide/releases/v0.6.7) (2024-12-31)
- 🍱 更新5.3版本游戏资源 [`#139`](https://github.com/BTMuli/TeyvatGuide/issues/139)
- ✨ 支持嵌入B站视频的分享图渲染
- 🐛 修复版块跳转异常
- 🐛 修复清理日志异常
- 🐛 修复特定帖子`link_card_ids`数据解析异常
- 🐛 修复帖子文本居中异常
- 🐛 修复侧边栏跳转角色/武器图鉴异常
- ✏️ 调整分享图大小计算方式采用1024进制而非原有的1000进制
- 💄 调整用户等级UI浅色深色下统一为白色文字
- 💄 调整回复弹窗位置,上移一段距离以避免底部提示遮挡
- 💄 首页素材日历组件只显示日期,移除具体时间
- 💄 调整链接卡片提示文字
- 💄 调整剧诗角色列表显示UI
- 🚸 版块/咨讯页数据获取/刷新显示成功提示
- 🚸 首页近期活动卡片Icon补充缺失的点击逻辑
- 🚸 调整合集组件改版后的滚动逻辑,更加流畅
- 👽️ 由于API变更调整版块数据获取逻辑
- 👽️ 由于返回数据格式变更,调整视频时长的计算逻辑
- 👽️ 由于返回数据格式变更,处理帖子内的转义字符
- ♻️ loading组件重构部分页面显示更精准的进度
- ♻️ 应用元数据格式重构,剔除冗余数据
## [0.6.6](https://github.com/BTMuli/TeyvatGuide/releases/v0.6.6) (2024-12-13)
- 🐛 修复主题切换响应异常
- 🐛 修复增量刷新逻辑异常
- ⚡️ 显著降低运行内存占用
## [0.6.5](https://github.com/BTMuli/TeyvatGuide/releases/v0.6.5) (2024-12-11)
- 🍱 添加下半卡池数据&部分资源
- ✨ 帖子内容中涉及的话题链接支持应用内跳转
- ♻️ 首页组件加载逻辑重构
- ✨ UIGF4导入/导出浮窗支持自选UID
- 💄 调整剧诗部分数据缺失时的显示
- 🐛 调整部分UI修复切换账户后角色详情刷新异常
## [0.6.4](https://github.com/BTMuli/TeyvatGuide/releases/v0.6.4) (2024-12-03)
- 🐛 修复子回复渲染异常
- ✏️ 祈愿记录将验证非空ID
- 🐛 修复战绩分享图渲染异常
- 🐛 修复`dialog`组件`input`默认值无效
- 🎨 调整帖子查找overlay逻辑
- ✨ 分享图生成阈值自定义
- 👽️ 全量刷新时清理旧数据,修复由于米哈游数据异常导致的重复数据
## [0.6.3](https://github.com/BTMuli/TeyvatGuide/releases/v0.6.3) (2024-11-19)
- 🐛 修复用户战绩角色数据`undefined`
- 🐛 修复咨讯页加载更多异常
- 🐛 修复验证码登录提示`-100`数据刷新后若为已登录UID则不会再提示切换
- 🐛 修复部分公告渲染异常
- 🐛 修复成就页面在存在搜索内容时点击左侧成就系列无响应
- ✨ 帖子新增 UID 卡片解析&渲染
- ✨ 帖子新增自定义表情解析&渲染
- ✨ 真境剧诗适配,新增真境剧诗页面,支持获取&分享&上传(胡桃数据库),可通过深渊页面进入
- ✨ 新增话题页面,可通过帖子卡片标签点击或帖子详情顶部标签点击进入
- ✨ 更完善的`loading`显示调整了组件UI
- 🍱 更新5.2版本资源 [`#133`](https://github.com/BTMuli/TeyvatGuide/issues/133)
- 💄 调整祈愿记录UP四星颜色
- 💄 修复帖子页兑换码弹窗高度异常
- 💄 调整帖子卡片UI增加显示帖子话题如存在话题&版块支持点击跳转
- 💄 调整帖子详情页UI顶部话题&版块支持点击跳转
- 💄 调整帖子显示数量,支持加载更多,默认排序改为`最新回复`,移除`默认排序`,增加`热门`排序
- 💄 咨讯、帖子等页面刷新时自动滚动到顶部
- 🔥 深渊数据库显示移除第9层统计数据
- 👽️ 米游社子窗口增加`genshinnet`域名支持
- 🎨 优化帖子详情数据加载的错误处理
- ♻️ `snackbar``confirm``loading`组件重构
- ♻️ 请求模块重构
## [0.6.2](https://github.com/BTMuli/TeyvatGuide/releases/v0.6.2) (2024-10-31)
- 🐛 修复用户登录状态异常 [`#132`](https://github.com/BTMuli/TeyvatGudie/issues/132)
- 💄 帖子子回复取消保持,点击其他隐藏
- 💄 调整未登录时的部分内容渲染
- 💄 调整保存时图片的hint
- 💄 `mac`:修复回顶组件宽度异常
- 💄 `mac`:修复视频封面位置异常
- 💄 调整角色卡片UI维持名片比例
- ♻️ 深渊数据库重构,概览显示差距
- 🍱 更新下半卡池
- 👽️ 修正咨讯Api
## [0.6.1](https://github.com/BTMuli/TeyvatGuide/releases/v0.6.1) (2024-10-22)
- 🐛 新用户数据库初始化异常 [`#131`](https://github.com/BTMuli/TeyavtGuide/issues/131)
- 🐛 修复角色数据未即时刷新
- 🐛 修复`openSystemBrowser`回调执行异常
- ♻️ 公告卡片组件抽离,支持分享
- 🎨 成就页面&名片图鉴页面采用虚拟列表优化性能
- 🎨 调整卡片封面加载逻辑
- 💄 处理特定情况下的内容溢出
- 💄 适配深渊新字段,显示跳过楼层
- 💄深渊分享显示应用信息,圣遗物详情推荐属性高亮
- 💄调整帖子子窗口副标题样式
- 💄调整留影叙佳期选项样式
## [0.6.0](https://github.com/BTMuli/TeyvatGuide/releases/v0.6.0) (2024-10-09)
- ✨ 应用支持多账号 [`#126`](https://github.com/BTMuli/TeyvatGuide/issues/126)
- ✨ 支持手动输入CK&用户删除
- ✨ 帖子卡片支持分享
- ✨ 支持官服用户直接启动原神 [`#80`](https://github.com/BTMuli/TeyvatGuide/issues/80)
- ♻️ 重构成就表格,支持多存档
- ♻️ 重构深渊数据加载逻辑,适配多存档
- ♻️ 重构用户登录逻辑及切换
- ♻️ 重构祈愿、深渊、角色页面逻辑,支持游戏账号切换
- ♻️ 战绩页面适配多账户
- 💄 帖子/公告子窗口添加窗口置顶按钮
- 💄 调整视频分享截图
- 💄 回复分享图忽略导出图标
- 💄 显示用户等级
- 💄 处理特定情况下的回复内容溢出
- 💄 兑换码支持分享调整了兑换码浮窗UI
- 💄 公告对列表进行缩进
- 💄 材料Wiki样式优化支持分类筛选&查询
- 💄 材料详情浮窗支持分享
- ✏️ JSBridge新增`openSystemBrowser`回调处理
- ✏️ 修正公告正则
- 👽️ 更新国际服公告Api
- 📖 添加 macOS 平台门禁属性导致无法打开应用的修复指引 [`#130`](https://github.com/BTMuli/TeyvatGuide/issues/130)
- ✨ 新增无痕浏览配置,默认开启
- ✨ 登录状态且关闭无痕浏览时,可对帖子进行点赞操作
- ✨ 新增实用脚本页面,支持一键完成米游币每日任务 [`#144`](https://github.com/BTMuli/TeyvatGuide/issues/144)
- 🐛 修复公告解析异常
- 🐛 修复角色卡片视图(详细)浮窗切换时背景图更新异常
- 🐛 修复路由跳转不生效
- ♻️ 重构首页素材日历组件生日计算,修复生日计算异常
- 🚸 设置页登录二维码支持生成分享图,点击底部图标触发
- 💄 调整首页素材日历组件可视页码
- 💄 调整部分页面UI
- 🍱 更新下半卡池数据

View File

@@ -2,12 +2,12 @@
Author: 目棃
Description: 说明文档
Date: 2023-03-05
Update: 2025-02-11
Update: 2025-02-28
---
> 本文档 [`Frontmatter`](https://github.com/BTMuli/MuCli#Frontmatter) 由 [MuCli](https://github.com/BTMuli/Mucli) 自动生成于 `2023-03-05 14:41:55`
>
> 更新于 `2025-02-11 10:55:00`
> 更新于 `2025-02-28 09:40:24`
![](https://img.shields.io/github/last-commit/BTMuli/TeyvatGuide?style=for-the-badge) ![](https://img.shields.io/github/commits-since/BTMuli/TeyvatGuide/latest?include_prereleases&style=for-the-badge)
@@ -66,6 +66,7 @@ Game Tool for Genshin Impact player, supports Windows and macOS.
- [x] 真境剧诗数据获取
- [x] 祈愿数据获取(近一年)
- [x] 用户收藏帖子获取
- [x] 一键完成米游币每日任务 **需要验证码登录**
- Wiki 功能:
@@ -97,8 +98,8 @@ Game Tool for Genshin Impact player, supports Windows and macOS.
- Changelog: [CHANGELOG](CHANGELOG.md)
- 资源来源:[项目资源说明](docs/项目资源说明.md)
- UIAF[UIAF v1.1](docs/UIAF.md)
- UIGF[UIGF v3.0](docs/UIGF3.md)[UIGF v4.0](docs/UIGF.md)
- UIAF[UIAF v1.1](docs/standards/UIAF.md)
- UIGF[UIGF v3.0](docs/standards/UIGF3.md)[UIGF v4.0](docs/standards/UIGF.md)
- [macOS 平台门禁属性导致应用无法打开应用的修复指引](docs/macos-gatekeeper/README.md)
## 特定项目 / Special Project

View File

@@ -0,0 +1,168 @@
---
Author: 目棃
Description: CHANGELOG
Date: 2024-10-09
Update: 2025-02-11
---
> 本文档 [`Frontmatter`](https://github.com/BTMuli/MuCli#Frontmatter) 由 [MuCli](https://github.com/BTMuli/Mucli) 自动生成于 `2024-10-09 15:51:43`
>
> 更新于 `2025-02-11 10:57:49`
## [0.6.9](https://github.com/BTMuli/TeyvatGuide/releases/v0.6.9) (2025-02-11)
- 🍱 更新5.4资源 [`#141`](https://github.com/BTMuli/TeyvatGuide/issues/141)
- 🐛 修复米游社子窗口路径解析异常
- 🐛 修复特定条件下真境剧诗角色元素图标渲染异常
- 🐛 修复名片图鉴浮窗渲染异常
- 🚸 调整兑换码入口显示判断逻辑
- 🚸 成就导入不允许点击外部取消,调整刷新逻辑
- 🚸 下载封面图时显示封面链接
- 💄 调整部分UI
## [0.6.8](https://github.com/BTMuli/TeyvatGuide/releases/v0.6.8) (2025-01-22)
- ✨ 扫码登录
- ✨ 调整祈愿记录图表样式,新增祈愿日历&祈愿堆叠柱状图
- ✨ 支持配置帖子详情图像质量默认80%
- ✨ 支持帖子详情图像查看原图当质量配置为100%时,该按钮不显示
- ✨ 深渊上传支持胡桃账户设置
- 🚸 降低祈愿全量刷新耗时
- 🚸 加快帖子加载速度,降低内存占用
- 💄 调整角色卡片样式
- 💄 调整角色名片样式,增加描述清晰度
- 💄 调整深渊Wiki队伍搭配窗口高度
- 💄 mac下不显示分享设置
- 💄 调整战绩页新洞天渲染样式
- 🔥 深渊Wiki移除第10层数据
- 🐛 修复深渊数据恢复异常
- 🐛 修复 loading 组件 empty 状态设置异常
- ♻️ 优化帖子加载逻辑当刷新内容不足20条时下次刷新数量为20-当前数量如刷新数量为19条则下次刷新数量为1条
- ♻️ 动态获取分区列表&版块列表
- ♻️ 重构部分路由处理,当话题/帖子切换分区/版块时,页面刷新不重置当前分区/版块
## [0.6.7](https://github.com/BTMuli/TeyvatGuide/releases/v0.6.7) (2024-12-31)
- 🍱 更新5.3版本游戏资源 [`#139`](https://github.com/BTMuli/TeyvatGuide/issues/139)
- ✨ 支持嵌入B站视频的分享图渲染
- 🐛 修复版块跳转异常
- 🐛 修复清理日志异常
- 🐛 修复特定帖子`link_card_ids`数据解析异常
- 🐛 修复帖子文本居中异常
- 🐛 修复侧边栏跳转角色/武器图鉴异常
- ✏️ 调整分享图大小计算方式采用1024进制而非原有的1000进制
- 💄 调整用户等级UI浅色深色下统一为白色文字
- 💄 调整回复弹窗位置,上移一段距离以避免底部提示遮挡
- 💄 首页素材日历组件只显示日期,移除具体时间
- 💄 调整链接卡片提示文字
- 💄 调整剧诗角色列表显示UI
- 🚸 版块/咨讯页数据获取/刷新显示成功提示
- 🚸 首页近期活动卡片Icon补充缺失的点击逻辑
- 🚸 调整合集组件改版后的滚动逻辑,更加流畅
- 👽️ 由于API变更调整版块数据获取逻辑
- 👽️ 由于返回数据格式变更,调整视频时长的计算逻辑
- 👽️ 由于返回数据格式变更,处理帖子内的转义字符
- ♻️ loading组件重构部分页面显示更精准的进度
- ♻️ 应用元数据格式重构,剔除冗余数据
## [0.6.6](https://github.com/BTMuli/TeyvatGuide/releases/v0.6.6) (2024-12-13)
- 🐛 修复主题切换响应异常
- 🐛 修复增量刷新逻辑异常
- ⚡️ 显著降低运行内存占用
## [0.6.5](https://github.com/BTMuli/TeyvatGuide/releases/v0.6.5) (2024-12-11)
- 🍱 添加下半卡池数据&部分资源
- ✨ 帖子内容中涉及的话题链接支持应用内跳转
- ♻️ 首页组件加载逻辑重构
- ✨ UIGF4导入/导出浮窗支持自选UID
- 💄 调整剧诗部分数据缺失时的显示
- 🐛 调整部分UI修复切换账户后角色详情刷新异常
## [0.6.4](https://github.com/BTMuli/TeyvatGuide/releases/v0.6.4) (2024-12-03)
- 🐛 修复子回复渲染异常
- ✏️ 祈愿记录将验证非空ID
- 🐛 修复战绩分享图渲染异常
- 🐛 修复`dialog`组件`input`默认值无效
- 🎨 调整帖子查找overlay逻辑
- ✨ 分享图生成阈值自定义
- 👽️ 全量刷新时清理旧数据,修复由于米哈游数据异常导致的重复数据
## [0.6.3](https://github.com/BTMuli/TeyvatGuide/releases/v0.6.3) (2024-11-19)
- 🐛 修复用户战绩角色数据`undefined`
- 🐛 修复咨讯页加载更多异常
- 🐛 修复验证码登录提示`-100`数据刷新后若为已登录UID则不会再提示切换
- 🐛 修复部分公告渲染异常
- 🐛 修复成就页面在存在搜索内容时点击左侧成就系列无响应
- ✨ 帖子新增 UID 卡片解析&渲染
- ✨ 帖子新增自定义表情解析&渲染
- ✨ 真境剧诗适配,新增真境剧诗页面,支持获取&分享&上传(胡桃数据库),可通过深渊页面进入
- ✨ 新增话题页面,可通过帖子卡片标签点击或帖子详情顶部标签点击进入
- ✨ 更完善的`loading`显示调整了组件UI
- 🍱 更新5.2版本资源 [`#133`](https://github.com/BTMuli/TeyvatGuide/issues/133)
- 💄 调整祈愿记录UP四星颜色
- 💄 修复帖子页兑换码弹窗高度异常
- 💄 调整帖子卡片UI增加显示帖子话题如存在话题&版块支持点击跳转
- 💄 调整帖子详情页UI顶部话题&版块支持点击跳转
- 💄 调整帖子显示数量,支持加载更多,默认排序改为`最新回复`,移除`默认排序`,增加`热门`排序
- 💄 咨讯、帖子等页面刷新时自动滚动到顶部
- 🔥 深渊数据库显示移除第9层统计数据
- 👽️ 米游社子窗口增加`genshinnet`域名支持
- 🎨 优化帖子详情数据加载的错误处理
- ♻️ `snackbar``confirm``loading`组件重构
- ♻️ 请求模块重构
## [0.6.2](https://github.com/BTMuli/TeyvatGuide/releases/v0.6.2) (2024-10-31)
- 🐛 修复用户登录状态异常 [`#132`](https://github.com/BTMuli/TeyvatGudie/issues/132)
- 💄 帖子子回复取消保持,点击其他隐藏
- 💄 调整未登录时的部分内容渲染
- 💄 调整保存时图片的hint
- 💄 `mac`:修复回顶组件宽度异常
- 💄 `mac`:修复视频封面位置异常
- 💄 调整角色卡片UI维持名片比例
- ♻️ 深渊数据库重构,概览显示差距
- 🍱 更新下半卡池
- 👽️ 修正咨讯Api
## [0.6.1](https://github.com/BTMuli/TeyvatGuide/releases/v0.6.1) (2024-10-22)
- 🐛 新用户数据库初始化异常 [`#131`](https://github.com/BTMuli/TeyavtGuide/issues/131)
- 🐛 修复角色数据未即时刷新
- 🐛 修复`openSystemBrowser`回调执行异常
- ♻️ 公告卡片组件抽离,支持分享
- 🎨 成就页面&名片图鉴页面采用虚拟列表优化性能
- 🎨 调整卡片封面加载逻辑
- 💄 处理特定情况下的内容溢出
- 💄 适配深渊新字段,显示跳过楼层
- 💄深渊分享显示应用信息,圣遗物详情推荐属性高亮
- 💄调整帖子子窗口副标题样式
- 💄调整留影叙佳期选项样式
## [0.6.0](https://github.com/BTMuli/TeyvatGuide/releases/v0.6.0) (2024-10-09)
- ✨ 应用支持多账号 [`#126`](https://github.com/BTMuli/TeyvatGuide/issues/126)
- ✨ 支持手动输入CK&用户删除
- ✨ 帖子卡片支持分享
- ✨ 支持官服用户直接启动原神 [`#80`](https://github.com/BTMuli/TeyvatGuide/issues/80)
- ♻️ 重构成就表格,支持多存档
- ♻️ 重构深渊数据加载逻辑,适配多存档
- ♻️ 重构用户登录逻辑及切换
- ♻️ 重构祈愿、深渊、角色页面逻辑,支持游戏账号切换
- ♻️ 战绩页面适配多账户
- 💄 帖子/公告子窗口添加窗口置顶按钮
- 💄 调整视频分享截图
- 💄 回复分享图忽略导出图标
- 💄 显示用户等级
- 💄 处理特定情况下的回复内容溢出
- 💄 兑换码支持分享调整了兑换码浮窗UI
- 💄 公告对列表进行缩进
- 💄 材料Wiki样式优化支持分类筛选&查询
- 💄 材料详情浮窗支持分享
- ✏️ JSBridge新增`openSystemBrowser`回调处理
- ✏️ 修正公告正则
- 👽️ 更新国际服公告Api
- 📖 添加 macOS 平台门禁属性导致无法打开应用的修复指引 [`#130`](https://github.com/BTMuli/TeyvatGuide/issues/130)

View File

@@ -2,12 +2,12 @@
Author: 目棃
Description: 项目资源说明
Date: 2023-03-10
Update: 2024-09-19
Update: 2025-02-28
---
> 本文档 [`Frontmatter`](https://github.com/BTMuli/MuCli#Frontmatter) 由 [MuCli](https://github.com/BTMuli/Mucli) 自动生成于 `2023-03-10 22:05:44`
>
> 更新于 `2024-09-19 15:28:16`
> 更新于 `2025-02-28 09:40:33`
## 说明
@@ -28,6 +28,7 @@ Update: 2024-09-19
- 我的角色Hoyolab
- 深渊记录Hoyolab
- 祈愿记录Hoyolab
- 实用脚本Hoyolab
- 图鉴Hoyolab其子目录图标同样来源于 Hoyolab
- 模式切换:`mdi-weather-night` `mdi-weather-sunny`
- 设置:个人绘制 SVG

View File

@@ -1,9 +1,9 @@
{
"name": "teyvatguide",
"version": "0.6.9",
"version": "0.7.0",
"description": "Game Tool for GenshinImpact player",
"private": true,
"packageManager": "pnpm@10.2.0",
"packageManager": "pnpm@10.5.2",
"type": "module",
"scripts": {
"build": "tauri build",
@@ -12,6 +12,7 @@
"eslint:pre": "pnpx @eslint/config-inspector@latest",
"lint": "concurrently \"pnpm:lint:*(!fix)\"",
"lint:fix": "concurrently \"pnpm:lint:*:fix\"",
"lint:vue": "vue-tsc --noEmit",
"lint:code": "eslint .",
"lint:code:fix": "eslint . --fix",
"lint:style": "stylelint \"src/**/*.{vue,css,scss}\" -f verbose",
@@ -68,12 +69,12 @@
},
"dependencies": {
"@mdi/font": "7.4.47",
"@tauri-apps/api": "^2.2.0",
"@tauri-apps/api": "^2.3.0",
"@tauri-apps/plugin-deep-link": "^2.2.0",
"@tauri-apps/plugin-dialog": "^2.2.0",
"@tauri-apps/plugin-fs": "^2.2.0",
"@tauri-apps/plugin-http": "^2.3.0",
"@tauri-apps/plugin-log": "^2.2.1",
"@tauri-apps/plugin-log": "^2.2.2",
"@tauri-apps/plugin-os": "^2.2.0",
"@tauri-apps/plugin-process": "^2.2.0",
"@tauri-apps/plugin-shell": "^2.2.0",
@@ -81,65 +82,66 @@
"ajv": "^8.17.1",
"artplayer": "^5.2.2",
"clipboard": "^2.0.11",
"color-convert": "^2.0.1",
"color-convert": "^3.0.1",
"echarts": "^5.6.0",
"html2canvas": "^1.4.1",
"js-md5": "^0.8.3",
"jsencrypt": "^3.3.2",
"pinia": "^2.3.1",
"pinia": "^3.0.1",
"pinia-plugin-persistedstate": "^4.2.0",
"qrcode.vue": "^3.6.0",
"sass": "^1.83.4",
"sass-loader": "^16.0.4",
"uuid": "^11.0.5",
"sass": "^1.85.1",
"sass-loader": "^16.0.5",
"uuid": "^11.1.0",
"vue": "^3.5.13",
"vue-echarts": "^7.0.3",
"vue-json-viewer": "^3.0.4",
"vue-json-pretty": "^2.4.0",
"vue-router": "^4.5.0",
"vuetify": "^3.7.9",
"vuetify": "^3.7.14",
"wcag-color": "^1.1.1",
"xml-js": "^1.6.11"
},
"devDependencies": {
"@eslint/eslintrc": "^3.2.0",
"@eslint/js": "^9.19.0",
"@tauri-apps/cli": "2.2.7",
"@eslint/eslintrc": "^3.3.0",
"@eslint/js": "^9.21.0",
"@tauri-apps/cli": "2.3.0",
"@types/color-convert": "^2.0.4",
"@types/fs-extra": "^11.0.4",
"@types/js-md5": "^0.7.2",
"@types/node": "^22.13.0",
"@types/node": "^22.13.5",
"@types/uuid": "^10.0.0",
"@typescript-eslint/parser": "^8.23.0",
"@typescript-eslint/parser": "^8.25.0",
"@vitejs/plugin-vue": "^5.2.1",
"concurrently": "^9.1.2",
"eslint": "^9.19.0",
"eslint": "^9.21.0",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-jsonc": "^2.19.1",
"eslint-plugin-prettier": "^5.2.3",
"eslint-plugin-vue": "^9.32.0",
"eslint-plugin-yml": "^1.16.0",
"eslint-plugin-yml": "^1.17.0",
"fs-extra": "^11.3.0",
"globals": "^15.14.0",
"globals": "^16.0.0",
"husky": "^9.1.7",
"jsonc-eslint-parser": "^2.4.0",
"lint-staged": "^15.4.3",
"oxlint": "^0.15.9",
"prettier": "3.4.2",
"oxlint": "^0.15.12",
"prettier": "3.5.2",
"stylelint": "^16.14.1",
"stylelint-config-idiomatic-order": "^10.0.0",
"stylelint-config-standard-vue": "^1.0.0",
"stylelint-declaration-block-no-ignored-properties": "^2.8.0",
"stylelint-high-performance-animation": "^1.10.0",
"stylelint-high-performance-animation": "^1.11.0",
"stylelint-order": "^6.0.4",
"stylelint-prettier": "^5.0.3",
"stylelint-scss": "^6.11.0",
"tsx": "^4.19.2",
"stylelint-scss": "^6.11.1",
"tsx": "^4.19.3",
"typescript": "^5.7.3",
"typescript-eslint": "^8.23.0",
"vite": "^6.0.11",
"vite-plugin-vue-devtools": "^7.7.1",
"vite-plugin-vuetify": "^2.0.4",
"typescript-eslint": "^8.25.0",
"vite": "^6.2.0",
"vite-plugin-vue-devtools": "^7.7.2",
"vite-plugin-vuetify": "^2.1.0",
"vue-eslint-parser": "^9.4.3",
"vue-tsc": "^2.2.4",
"yaml-eslint-parser": "^1.2.3"
}
}

2140
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

BIN
public/source/UI/posts.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 776 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

920
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package]
name = "TeyvatGuide"
version = "0.6.9"
version = "0.7.0"
description = "Game Tool for Genshin Impact player"
authors = ["BTMuli <bt-muli@outlook.com>"]
license = "MIT"
@@ -10,15 +10,15 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[build-dependencies]
tauri-build = { version = "2.0.5", features = [] }
tauri-build = { version = "2.0.6", features = [] }
[dependencies]
chrono = "0.4.39"
log = "0.4.25"
serde = { version = "1.0.217", features = ["derive"] }
serde_json = "1.0.138"
tauri = { version = "2.2.5", features = [] }
tauri-utils = "2.1.1"
chrono = "0.4.40"
log = "0.4.26"
serde = { version = "1.0.218", features = ["derive"] }
serde_json = "1.0.139"
tauri = { version = "2.3.0", features = [] }
tauri-utils = "2.2.0"
url = "2.5.4"
walkdir = "2.5.0"

View File

@@ -1,6 +1,6 @@
//! @file src/client/mod.rs
//! @desc 客户端模块,负责操作米游社客户端
//! @since Beta v0.6.8
//! @since Beta v0.7.0
mod menu;
mod utils;
@@ -8,7 +8,7 @@ mod utils;
use tauri::{AppHandle, Manager, WebviewWindowBuilder};
use tauri_utils::config::WebviewUrl;
static BBS_VERSION: &'static str = "2.80.1";
static BBS_VERSION: &'static str = "2.82.0";
#[tauri::command]
pub async fn create_mhy_client(handle: AppHandle, func: String, url: String) {

View File

@@ -1,7 +1,7 @@
{
"productName": "TeyvatGuide",
"identifier": "TeyvatGuide",
"version": "0.6.9",
"version": "0.7.0",
"build": {
"beforeDevCommand": "pnpm vite:dev",
"beforeBuildCommand": "pnpm vite:build",

View File

@@ -28,7 +28,7 @@
<v-list-item :title.attr="'帖子'" :link="true" href="/posts/forum">
<template #title>帖子</template>
<template #prepend>
<img src="/source/UI/posts.png" alt="posts" class="side-icon" />
<img src="/source/UI/posts.webp" alt="posts" class="side-icon" />
</template>
</v-list-item>
<v-list-item :title.attr="'成就'" :link="true" href="/achievements">
@@ -62,6 +62,12 @@
<img src="/source/UI/userGacha.webp" alt="gacha" class="side-icon" />
</template>
</v-list-item>
<v-list-item :title.attr="'实用脚本'" :link="true" href="/user/scripts">
<template #title>实用脚本</template>
<template #prepend>
<img src="/source/UI/toolbox.webp" alt="scripts" class="side-icon" />
</template>
</v-list-item>
<v-divider />
<v-list-item
v-show="isDevEnv"
@@ -114,7 +120,7 @@
<v-list-item :title.attr="'留影叙佳期'" :link="true" href="/archive/birthday">
<template #title>留影叙佳期</template>
<template #prepend>
<img src="/source/UI/act_birthday.png" alt="archive_birthday_icon" class="side-icon" />
<img src="/source/UI/act_birthday.webp" alt="archive_birthday_icon" class="side-icon" />
</template>
</v-list-item>
<div class="bottom-menu">
@@ -145,7 +151,7 @@
</v-list-item>
<v-list-item class="side-item-menu" title="收藏" :link="true" href="/collection">
<template #prepend>
<img src="/source/UI/posts.png" alt="collect" class="side-icon-menu" />
<img src="/source/UI/posts.webp" alt="collect" class="side-icon-menu" />
</template>
</v-list-item>
</v-list>

View File

@@ -1,5 +1,5 @@
<template>
<ToGameLogin v-model="showLoginQr" @success="tryGetTokens" />
<ToGameLogin v-model="showLoginQr" @success="tryGetTokens" v-model:launcher="isLauncherQr" />
<v-card class="tcu-box">
<template #prepend>
<v-avatar :image="userInfo.avatar" />
@@ -100,8 +100,15 @@
<v-list-item-title>验证码登录</v-list-item-title>
<v-list-item-subtitle>使用手机号登录</v-list-item-subtitle>
</v-list-item>
<v-list-item @click="showLoginQr = true" append-icon="mdi-qrcode-scan">
<v-list-item-title>扫码登录</v-list-item-title>
<v-list-item @click="tryCodeLogin(true)">
<v-list-item-title>扫码登录(启动器)</v-list-item-title>
<v-list-item-subtitle>使用米游社扫码登录</v-list-item-subtitle>
<template #append>
<img src="/platforms/mhy/launcher.webp" alt="launcher" class="menu-icon" />
</template>
</v-list-item>
<v-list-item @click="tryCodeLogin(false)" append-icon="mdi-qrcode-scan" v-if="false">
<v-list-item-title>扫码登录(游戏)</v-list-item-title>
<v-list-item-subtitle>使用米游社扫码登录</v-list-item-subtitle>
</v-list-item>
</v-list>
@@ -131,6 +138,7 @@ const { isLogin } = storeToRefs(useAppStore());
const { uid, briefInfo, cookie, account } = storeToRefs(useUserStore());
const showLoginQr = ref<boolean>(false);
const isLauncherQr = ref<boolean>(true);
const accounts = shallowRef<Array<TGApp.App.Account.User>>([]);
const gameAccounts = shallowRef<Array<TGApp.Sqlite.Account.Game>>([]);
const userInfo = computed<TGApp.App.Account.BriefInfo>(() => {
@@ -247,6 +255,11 @@ async function tryCaptchaLogin(): Promise<void> {
await tryGetTokens(ck);
}
async function tryCodeLogin(isLauncher: boolean): Promise<void> {
isLauncherQr.value = isLauncher;
showLoginQr.value = true;
}
async function refreshUser(uid: string) {
let account = await TSUserAccount.account.getAccount(uid);
if (!account) {
@@ -554,4 +567,9 @@ async function clearUser(user: TGApp.App.Account.User): Promise<void> {
.tcu-btn {
margin-left: 5px;
}
.menu-icon {
width: 24px;
height: 24px;
}
</style>

View File

@@ -6,24 +6,32 @@
</div>
<div class="tog-mid">
<qrcode-vue
v-if="codeData"
v-if="codeUrl"
class="tog-qr"
:value="codeData.url"
:value="codeUrl"
render-as="svg"
:background="'var(--box-bg-1)'"
foreground="var(--box-text-1)"
/>
</div>
<div class="tog-bottom" @click="share()">
<img src="/platforms/mhy/launcher.webp" alt="icon" v-if="isLauncherCode" />
<img src="/platforms/mhy/mys.webp" alt="icon" v-else />
</div>
</div>
</TOverlay>
</template>
<script setup lang="ts">
import TOverlay from "@comp/app/t-overlay.vue";
import showLoading from "@comp/func/loading.js";
import showSnackbar from "@comp/func/snackbar.js";
import QrcodeVue from "qrcode.vue";
import { onUnmounted, shallowRef, watch } from "vue";
import { computed, onUnmounted, ref, watch } from "vue";
import { generateShareImg } from "@/utils/TGShare.js";
import hk4eReq from "@/web/request/hk4eReq.js";
import PassportReq from "@/web/request/passportReq.js";
import takumiReq from "@/web/request/takumiReq.js";
type ToGameLoginEmits = (e: "success", data: TGApp.App.Account.Cookie) => void;
@@ -31,8 +39,14 @@ type ToGameLoginEmits = (e: "success", data: TGApp.App.Account.Cookie) => void;
let cycleTimer: NodeJS.Timeout | null = null;
const model = defineModel<boolean>({ default: false });
const isLauncherCode = defineModel<boolean>("launcher", { default: false });
const emits = defineEmits<ToGameLoginEmits>();
const codeData = shallowRef<TGApp.BBS.GameLogin.GetLoginQrData>();
const codeUrl = ref<string>();
const codeTicket = computed<string>(() => {
if (!codeUrl.value) return "";
const url = new URL(codeUrl.value);
return url.searchParams.get("ticket") || "";
});
watch(model, async (value) => {
if (value) {
@@ -41,25 +55,43 @@ watch(model, async (value) => {
}
});
async function share(): Promise<void> {
const shareDom = document.querySelector<HTMLDivElement>(".tog-box");
if (shareDom === null) {
showSnackbar.error("分享失败");
return;
}
await generateShareImg(`tco-gameLogin`, shareDom);
}
async function freshQr(): Promise<void> {
const res = await PassportReq.qrLogin.create();
let res;
if (isLauncherCode.value) res = await PassportReq.qrLogin.create();
else res = await hk4eReq.loginQr.create();
console.log(res);
if ("retcode" in res) {
showSnackbar.error(`[${res.retcode}] ${res.message}`);
return;
}
codeData.value = res;
codeUrl.value = res.url;
}
async function cycleGetData() {
if (cycleTimer === null || !codeData.value) return;
const res = await PassportReq.qrLogin.query(codeData.value.ticket);
if (cycleTimer === null || codeTicket.value === "") return;
if (isLauncherCode.value) await cycleGetDataLauncher(cycleTimer);
else await cycleGetDataGame(cycleTimer);
}
// eslint-disable-next-line no-undef
async function cycleGetDataLauncher(timer: NodeJS.Timeout): Promise<void> {
const res = await PassportReq.qrLogin.query(codeTicket.value);
console.log(res);
if ("retcode" in res) {
showSnackbar.error(`[${res.retcode}] ${res.message}`);
if (res.retcode === -106) {
await freshQr();
} else {
clearInterval(cycleTimer);
clearInterval(timer);
cycleTimer = null;
model.value = false;
}
@@ -67,7 +99,7 @@ async function cycleGetData() {
}
if (res.status === "Created" || res.status === "Scanned") return;
if (res.status === "Confirmed") {
clearInterval(cycleTimer);
clearInterval(timer);
cycleTimer = null;
const ck: TGApp.App.Account.Cookie = {
account_id: res.user_info.aid,
@@ -83,6 +115,54 @@ async function cycleGetData() {
}
}
// eslint-disable-next-line no-undef
async function cycleGetDataGame(timer: NodeJS.Timeout): Promise<void> {
const res = await hk4eReq.loginQr.state(codeTicket.value);
console.log(res);
if ("retcode" in res) {
showSnackbar.error(`[${res.retcode}] ${res.message}`);
if (res.retcode === -106) {
await freshQr();
} else {
clearInterval(timer);
cycleTimer = null;
model.value = false;
}
return;
}
if (res.stat === "Init" || res.stat === "Scanned") return;
if (res.stat === "Confirmed") {
clearInterval(timer);
cycleTimer = null;
if (res.payload.proto === "Raw") {
showSnackbar.error(`返回数据异常:${res.payload}`);
model.value = false;
return;
}
const statusRaw: TGApp.Game.Login.StatusPayloadRaw = JSON.parse(res.payload.raw);
await showLoading.start("正在获取SToken");
const stResp = await takumiReq.game.stoken(statusRaw);
console.log(stResp);
await showLoading.end();
if ("retcode" in stResp) {
showSnackbar.error(`[${stResp.retcode}] ${stResp.message}`);
model.value = false;
return;
}
// const ck: TGApp.App.Account.Cookie = {
// account_id: statusRaw.uid,
// ltuid: statusRaw.uid,
// stuid: statusRaw.uid,
// mid: res.user_info.mid,
// cookie_token: "",
// stoken: res.tokens[0].token,
// ltoken: "",
// };
// emits("success", ck);
model.value = false;
}
}
onUnmounted(() => {
if (cycleTimer !== null) clearInterval(cycleTimer);
cycleTimer = null;
@@ -126,4 +206,15 @@ onUnmounted(() => {
width: 256px;
height: 256px;
}
.tog-bottom {
margin: 0 auto;
cursor: pointer;
img {
width: 32px;
height: 32px;
border-radius: 4px;
}
}
</style>

View File

@@ -17,7 +17,7 @@
<img @click="toBirth(true)" :src="i.head_icon" alt="empty" class="tcb-cur" v-else />
</div>
<span>的生日哦~</span>
<img @click="toBirth(true)" src="/source/UI/act_birthday.png" alt="empty" class="active" />
<img @click="toBirth(true)" src="/source/UI/act_birthday.webp" alt="empty" class="active" />
</div>
<div>即将到来{{ next[0].role_birthday }}</div>
<div v-for="i in next" :key="i.role_id" class="tcb-item">
@@ -143,6 +143,7 @@ function parseDesc(intro: string): string {
.tcb-item img {
height: 100px;
border-radius: 50%;
aspect-ratio: 1;
cursor: pointer;
}

View File

@@ -19,7 +19,7 @@
{{ text.text }}
</v-btn>
</div>
<v-pagination class="tc-page" v-model="page" :total-visible="visible" :length="length" />
<v-pagination class="tc-page" v-model="page" :total-visible="9" :length="length" />
</div>
<div class="tc-content">
<TCalendarBirth />

View File

@@ -78,7 +78,7 @@
import TMiImg from "@comp/app/t-mi-img.vue";
import showSnackbar from "@comp/func/snackbar.js";
import TSUserAvatar from "@Sqlite/modules/userAvatar.js";
import { computed, onMounted, ref } from "vue";
import { computed, ref } from "vue";
import TuaDcConstellations from "./tua-dc-constellations.vue";
import TuaDcProp from "./tua-dc-prop.vue";
@@ -109,15 +109,11 @@ const propMain = computed<Array<TGApp.Game.Avatar.PropMapItem | false>>(() =>
props.modelValue.propSelected.map((item) => userStore.getProp(item.property_type)),
);
const bg = ref<string>("/WIKI/nameCard/profile/原神·印象.webp");
const loading = ref<boolean>(false);
onMounted(async () => await loadData());
async function loadData(): Promise<void> {
const bg = computed<string>(() => {
const card = TSUserAvatar.getAvatarCard(props.modelValue.cid);
bg.value = `url("/WIKI/nameCard/profile/${card}.webp")`;
}
return `url("/WIKI/nameCard/profile/${card}.webp")`;
});
const loading = ref<boolean>(false);
async function share(): Promise<void> {
const shareBox = document.querySelector<HTMLElement>(".tua-dc-container");

View File

@@ -26,7 +26,6 @@
<script lang="ts" setup>
// about import err,see:https://github.com/apache/echarts/issues/19992
import showLoading from "@comp/func/loading.js";
// @ts-expect-error no-exported-member
import { BarChart, HeatmapChart, PieChart } from "echarts/charts.js";
import {
CalendarComponent,
@@ -38,11 +37,8 @@ import {
TooltipComponent,
VisualMapComponent,
} from "echarts/components.js";
// @ts-expect-error no-exported-member
import { use } from "echarts/core.js";
// @ts-expect-error no-exported-member
import { LabelLayout } from "echarts/features.js";
// @ts-expect-error no-exported-member
import { CanvasRenderer } from "echarts/renderers.js";
import type { EChartsOption } from "echarts/types/dist/shared.js";
import { storeToRefs } from "pinia";

View File

@@ -1,24 +1,22 @@
<!-- todo 优化增加筛选功能 -->
<template>
<div class="ua-gt-box">
<v-data-table
:headers="headers"
:items="props.modelValue"
height="500px"
fixed-header
fixed-footer
>
<template v-slot:item="{ item }">
<tr class="ua-gt-tr">
<td>{{ item.time }}</td>
<td>{{ getPool(item.uigfType) }}</td>
<td>{{ item.type }}</td>
<td>{{ item.name }}</td>
<td>{{ item.rank }}</td>
</tr>
</template>
</v-data-table>
</div>
<v-data-table
:headers="headers"
:items="props.modelValue"
fixed-header
fixed-footer
class="ua-gt-box"
>
<template v-slot:item="{ item }">
<tr class="ua-gt-tr">
<td>{{ item.time }}</td>
<td>{{ getPool(item.uigfType) }}</td>
<td>{{ item.type }}</td>
<td>{{ item.name }}</td>
<td>{{ item.rank }}</td>
</tr>
</template>
</v-data-table>
</template>
<script lang="ts" setup>
type GroTableProps = { modelValue: Array<TGApp.Sqlite.GachaRecords.SingleTable> };
@@ -52,8 +50,7 @@ function getPool(type: string) {
</script>
<style lang="css" scoped>
.ua-gt-box {
height: 100%;
max-height: calc(100vh - 120px);
height: calc(100vh - 200px);
padding-right: 5px;
border-radius: 5px;
overflow-y: auto;

View File

@@ -0,0 +1,356 @@
<template>
<div class="tusm-box">
<div class="tusm-top">
<div class="tusm-title">米游币任务({{ todayPoints }}/{{ totalPoints }})</div>
<div class="tusm-acts">
<v-btn @click="tryRefresh()" class="tusm-btn" :loading="loadState">刷新</v-btn>
<v-btn @click="tryAuto()" class="tusm-btn" :loading="loadMission">执行</v-btn>
</div>
</div>
<div class="tusm-content">
<div v-for="mission in parseMissions" :key="mission.id" class="mission-item">
<div class="left">
<v-icon v-if="!mission.status" color="var(--tgc-od-grey)">mdi-circle</v-icon>
<v-icon v-else color="var(--tgc-od-green)">mdi-check-circle</v-icon>
<span>{{ mission.name }} - {{ mission.reward }}米游币</span>
</div>
<div class="right">
<span>
<v-progress-linear
rounded
:model-value="(mission.process / mission.total) * 100"
height="8"
color="var(--tgc-od-blue)"
/>
</span>
<span>{{ mission.process }}/{{ mission.total }}</span>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import showSnackbar from "@comp/func/snackbar.js";
import { getRecentForumPostList } from "@Mys/request/painterReq.js";
import { getPostFull } from "@Mys/request/postReq.js";
import { storeToRefs } from "pinia";
import { ref, shallowRef } from "vue";
import { useUserStore } from "@/store/modules/user.js";
import TGLogger from "@/utils/TGLogger.js";
import apiHubReq from "@/web/request/apiHubReq.js";
type ParseMission = {
id: number;
key: string;
name: string;
process: number;
total: number;
status: boolean;
reward: number;
};
const { cookie } = storeToRefs(useUserStore());
const parseMissions = shallowRef<Array<ParseMission>>([]);
const missionList = shallowRef<Array<TGApp.BBS.Mission.MissionItem>>([]);
const todayPoints = ref<number>(0);
const totalPoints = ref<number>(0);
const loadScript = defineModel<boolean>();
const loadState = ref<boolean>(false);
const loadMission = ref<boolean>(false);
function mergeMission(
list: Array<TGApp.BBS.Mission.MissionItem>,
state: Array<TGApp.BBS.Mission.StateItem>,
): void {
const res: Array<ParseMission> = [];
for (const item of list) {
const stateFind = state.find((i) => i.mission_id === item.id);
if (!stateFind) {
res.push({
id: item.id,
key: item.mission_key,
name: item.name,
process: 0,
total: item.threshold,
status: false,
reward: item.points,
});
continue;
}
res.push({
id: item.id,
key: item.mission_key,
name: item.name,
total: item.threshold,
process: stateFind.happened_times,
status: stateFind.process === 1,
reward: item.points,
});
}
res.sort((a, b) => a.id - b.id);
parseMissions.value = res;
}
async function tryRefresh(): Promise<void> {
if (loadScript.value) {
showSnackbar.warn("任务正在执行中,请稍后再试");
return;
}
loadScript.value = true;
loadState.value = true;
await TGLogger.ScriptSep("米游币任务");
await TGLogger.Script("[米游币任务]刷新任务状态");
if (!cookie.value) {
await TGLogger.Script("[米游币任务]未检测到Cookie");
showSnackbar.warn("当前账号未登录,请先登录");
await TGLogger.ScriptSep("米游币任务", false);
return;
}
const ck = {
stoken: cookie.value.stoken,
stuid: cookie.value.stuid,
mid: cookie.value.mid,
};
await refreshState(ck);
await TGLogger.ScriptSep("米游币任务", false);
loadScript.value = false;
loadState.value = false;
}
async function tryAuto(): Promise<void> {
if (loadScript.value) {
showSnackbar.warn("任务正在执行中,请稍后再试");
return;
}
loadScript.value = true;
loadMission.value = true;
await TGLogger.ScriptSep("米游币任务");
await TGLogger.Script("[米游币任务]开始执行任务");
if (!cookie.value) {
await TGLogger.Script("[米游币任务]未检测到Cookie");
showSnackbar.warn("当前账号未登录,请先登录");
await TGLogger.ScriptSep("米游币任务", false);
return;
}
const ck = {
stoken: cookie.value.stoken,
stuid: cookie.value.stuid,
mid: cookie.value.mid,
};
const ckPost = { ltoken: cookie.value.ltoken, ltuid: cookie.value.ltuid };
await refreshState(ck);
const signFind = parseMissions.value.find((i) => i.key === "continuous_sign");
if (signFind && !signFind.status) {
await autoSign(ck);
} else {
await TGLogger.Script("[米游币任务]未找到打卡任务或今日已打卡");
}
const postFilter = parseMissions.value.filter((i) => i.key !== "continuous_sign");
if (postFilter.every((i) => i.status)) {
await TGLogger.Script("[米游币任务]所有任务已完成");
await TGLogger.ScriptSep("米游币任务", false);
loadScript.value = false;
loadMission.value = false;
return;
}
let isShare = false;
let likeCnt = 0;
let viewCnt = 0;
const shareFind = postFilter.find((i) => i.key === "share_post_0");
if (shareFind) isShare = shareFind.status;
const likeFind = postFilter.find((i) => i.key === "post_up_0");
if (likeFind) likeCnt = likeFind.process;
const viewFind = postFilter.find((i) => i.key === "view_post_0");
if (viewFind) viewCnt = viewFind.process;
await TGLogger.Script("[米游币任务]获取帖子列表");
const listResp = await getRecentForumPostList(26, 2, 2, undefined, 20);
for (const post of listResp.list) {
if (!isShare) {
await TGLogger.Script(`[米游币任务]正在分享帖子${post.post.post_id}`);
const shareResp = await apiHubReq.post.share(post.post.post_id, ck);
if (shareResp.retcode === 0) {
await TGLogger.Script("[米游币任务]分享成功");
isShare = true;
} else {
await TGLogger.Script(`[米游币任务]分享失败:${shareResp.retcode} ${shareResp.message}`);
}
}
if (likeCnt < 5 || viewCnt < 3) {
await TGLogger.Script(`[米游币任务]正在浏览帖子${post.post.post_id}`);
const detailResp = await getPostFull(Number(post.post.post_id), ckPost);
if ("retcode" in detailResp) {
await TGLogger.Script(
`[米游币任务]获取帖子${post.post.post_id}失败:${detailResp.retcode} ${detailResp.message}`,
);
continue;
}
viewCnt++;
if (likeCnt < 5) {
const isLike = detailResp.self_operation.upvote_type === 1;
if (isLike) {
await TGLogger.Script(`[米游币任务]帖子${post.post.post_id}已点赞,跳过`);
continue;
}
await TGLogger.Script(`[米游币任务]正在点赞帖子${post.post.post_id}`);
const likeResp = await apiHubReq.post.like(post.post.post_id, ckPost);
if (likeResp.retcode === 0) {
await TGLogger.Script("[米游币任务]点赞成功");
likeCnt++;
} else {
await TGLogger.Script(`[米游币任务]点赞失败:${likeResp.retcode} ${likeResp.message}`);
continue;
}
await TGLogger.Script(`[米游币任务]正在取消点赞帖子${post.post.post_id}`);
await new Promise<void>((resolve) => setTimeout(resolve, 1000));
const unlikeResp = await apiHubReq.post.like(post.post.post_id, ckPost, true);
if (unlikeResp.retcode === 0) {
await TGLogger.Script("[米游币任务]取消点赞成功");
} else {
await TGLogger.Script(
`[米游币任务]取消点赞失败:${unlikeResp.retcode} ${unlikeResp.message}`,
);
}
}
}
if (isShare && likeCnt >= 5 && viewCnt >= 3) {
await TGLogger.Script("[米游币任务]所有任务已完成");
break;
}
}
await TGLogger.Script("[米游币任务]任务执行完毕,即将刷新任务状态");
await refreshState(ck);
await TGLogger.ScriptSep("米游币任务", false);
loadScript.value = false;
loadMission.value = false;
}
async function refreshState(ck: Record<string, string>): Promise<void> {
await TGLogger.Script("[米游币任务]刷新任务状态");
if (missionList.value.length === 0) {
await TGLogger.Script("[米游币任务]未检测到任务列表,正在获取");
const listResp = await apiHubReq.mission.list(ck);
if (listResp.retcode !== 0) {
await TGLogger.Script(
`[米游币任务]获取任务列表失败:${listResp.retcode} ${listResp.message}`,
);
showSnackbar.error(`[${listResp.retcode}] ${listResp.message}`);
await TGLogger.ScriptSep("米游币任务", false);
return;
}
missionList.value = listResp.data.missions;
await TGLogger.Script("[米游币任务]获取任务列表成功");
}
await TGLogger.Script("[米游币任务]正在获取任务状态");
const stateResp = await apiHubReq.mission.state(ck);
if (stateResp.retcode !== 0) {
await TGLogger.Script(
`[米游币任务]获取任务状态失败:${stateResp.retcode} ${stateResp.message}`,
);
showSnackbar.error(`[${stateResp.retcode}] ${stateResp.message}`);
await TGLogger.ScriptSep("米游币任务", false);
return;
}
await TGLogger.Script("[米游币任务]获取任务状态成功");
todayPoints.value = stateResp.data.already_received_points;
totalPoints.value = stateResp.data.today_total_points;
await TGLogger.Script("[米游币任务]合并任务数据");
mergeMission(missionList.value, stateResp.data.states);
await TGLogger.Script("[米游币任务]任务数据合并完成");
}
async function autoSign(ck: Record<string, string>): Promise<void> {
const signFind = parseMissions.value.find((i) => i.key === "continuous_sign");
if (!signFind) {
await TGLogger.Script("[米游币任务]未找到打卡任务");
return;
}
if (signFind.status) {
await TGLogger.Script("[米游币任务]今日已打卡");
return;
}
await TGLogger.Script("[米游币任务]正在执行打卡");
const resp = await apiHubReq.sign(ck);
if (resp.retcode !== 0) {
await TGLogger.Script(`[米游币任务]打卡失败:${resp.retcode} ${resp.message}`);
showSnackbar.error(`[${resp.retcode}] ${resp.message}`);
return;
}
await TGLogger.Script("[米游币任务]打卡成功");
}
</script>
<style lang="scss" scoped>
.tusm-box {
position: relative;
width: 100%;
box-sizing: border-box;
padding: 12px;
background: var(--box-bg-1);
border-radius: 4px;
border: 1px solid var(--common-shadow-2);
display: flex;
flex-direction: column;
gap: 8px;
color: var(--box-text-1);
}
.tusm-top {
position: relative;
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
}
.tusm-title {
font-family: var(--font-title);
font-size: 18px;
}
.tusm-acts {
display: flex;
gap: 10px;
}
.tusm-btn {
background: var(--tgc-btn-1);
color: var(--btn-text);
}
.tusm-content {
display: flex;
flex-direction: column;
gap: 4px;
}
.mission-item {
position: relative;
width: 100%;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px;
background: var(--box-bg-2);
border-radius: 4px;
color: var(--box-text-2);
.left {
display: flex;
align-items: center;
gap: 4px;
}
.right {
position: relative;
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
:first-child {
width: 100px;
}
}
}
</style>

View File

@@ -0,0 +1,101 @@
<template>
<div class="tuso-box">
<div class="tuso-top">
<div class="tuso-title">输出日志</div>
<div class="tuso-top-acts">
<v-btn class="tuso-btn" @click="clearLog()">清空</v-btn>
</div>
</div>
<div class="tuso-mid" ref="logRef">
<div class="tuso-log" v-for="log in logs" :key="log">{{ log }}</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { event } from "@tauri-apps/api";
import type { Event, UnlistenFn } from "@tauri-apps/api/event";
import { nextTick, onMounted, onUnmounted, ref, useTemplateRef } from "vue";
let logListener: UnlistenFn | null = null;
const logs = ref<Array<string>>([]);
const logEl = useTemplateRef<HTMLDivElement>("logRef");
onMounted(async () => {
logListener = await event.listen<string>("userScriptLog", async (e: Event<string>) => {
logs.value.push(e.payload);
await nextTick();
logEl.value?.scrollTo({ top: logEl.value.scrollHeight, behavior: "smooth" });
});
});
function clearLog(): void {
logs.value = [];
}
onUnmounted(() => {
if (logListener !== null) {
logListener();
logListener = null;
}
});
</script>
<style lang="scss" scoped>
.tuso-box {
position: relative;
height: 100%;
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: 4px;
margin-left: auto;
width: 800px;
min-width: 800px;
padding: 12px;
background: var(--box-bg-1);
border-radius: 4px;
border: 1px solid var(--common-shadow-2);
}
.tuso-top {
position: relative;
width: 100%;
display: flex;
justify-content: space-between;
align-items: flex-end;
}
.tuso-title {
color: var(--common-text-title);
font-family: var(--font-title);
font-size: 24px;
}
.tuso-top-acts {
display: flex;
gap: 10px;
}
.tuso-btn {
background: var(--tgc-btn-1);
color: var(--btn-text);
}
.tuso-mid {
position: relative;
display: flex;
flex-direction: column;
height: 100%;
background: var(--box-bg-2);
border-radius: 4px;
padding: 8px;
box-sizing: border-box;
overflow-y: auto;
}
.tuso-log {
position: relative;
height: 24px;
min-height: 24px;
white-space: pre;
}
</style>

View File

@@ -133,15 +133,14 @@ const levelColor = computed<string>(() => {
.tpa-level-right {
position: absolute;
bottom: 0;
display: flex;
width: 18px;
height: 18px;
align-items: center;
justify-content: center;
border-radius: 50%;
background: v-bind(levelColor);
color: var(--tgc-white-1);
font-size: 10px;
line-height: 18px;
text-align: center;
}
.tpa-level-right {

View File

@@ -45,7 +45,9 @@ function getParsedData(data: SctPostDataArr): SctPostDataArr {
res.push(cur);
continue;
}
tp.insert = tp.insert.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
if (typeof tp.insert === "string") {
tp.insert = tp.insert.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
}
if (tp.insert === "\n") {
child.push(tp);
cur = { insert: "", attributes: tp.attributes, children: child };
@@ -53,7 +55,7 @@ function getParsedData(data: SctPostDataArr): SctPostDataArr {
child = [];
continue;
}
const parsedText = getParsedText(tp);
const parsedText = getParsedText(<TpTextType>tp);
let check = 0;
for (let i = 0; i < parsedText.length; i++) {
const text = parsedText[i];

View File

@@ -11,7 +11,6 @@
'tp-texts-header6': props.data.attributes && props.data.attributes.header === 6,
}"
:title="getTitle()"
:style="{ textAlign: props.data.attributes?.align }"
>
<component
:is="getComp(text)"
@@ -27,6 +26,7 @@ import type { Component } from "vue";
import TpImage from "./tp-image.vue";
import TpMention, { type TpMention as TpMentionType } from "./tp-mention.vue";
import TpText, { type TpText as TpTextType } from "./tp-text.vue";
import TpUnknown from "./tp-unknown.vue";
type TpTexts = { children: Array<TpTextType | TpMentionType> } & TpTextType;
type TpTextsProps = { data: TpTexts };
@@ -36,7 +36,8 @@ const props = defineProps<TpTextsProps>();
function getComp(text: TpTextType | TpMentionType): Component {
if (typeof text.insert === "string") return TpText;
if ("image" in text.insert) return TpImage;
return TpMention;
if ("mention" in text.insert) return TpMention;
return TpUnknown;
}
function getTitle(): string {
@@ -51,6 +52,7 @@ function getTitle(): string {
line-break: anywhere;
white-space: pre-wrap;
word-break: break-all;
text-align: v-bind("props.data.attributes?.align");
&.tp-inline {
display: inline;

View File

@@ -9,7 +9,7 @@
</div>
<div class="tpu-main">UID {{ props.data.insert.game_user_info.game_uid }}</div>
<div class="tpu-sub">
<span>{{ props.data.insert.game_user_info.nickname }}</span>
<span>{{ nickname }}</span>
<span>|</span>
<span>{{ props.data.insert.game_user_info.region_name }}</span>
<span>|</span>
@@ -19,6 +19,7 @@
</template>
<script lang="ts" setup>
import showSnackbar from "@comp/func/snackbar.js";
import { computed } from "vue";
type TpUid = {
insert: {
@@ -35,6 +36,9 @@ type TpUid = {
type TpUidProps = { data: TpUid };
const props = defineProps<TpUidProps>();
const nickname = computed<string>(() =>
decodeURIComponent(props.data.insert.game_user_info.nickname),
);
console.log("tpUid", props.data.insert.game_user_info);
function copyUid(): void {

View File

@@ -10,7 +10,7 @@
/>
<div class="tp-video-share">
<img alt="cover" :src="videoCover" class="tp-video-cover" />
<img alt="icon" src="/source/UI/video_play_bili.png" class="tp-video-icon" />
<img alt="icon" src="/source/UI/video_play_bili.webp" class="tp-video-icon" />
<div class="tp-video-info">
<span>{{ videoData.bvid }}|{{ timestampToDate(videoData.ctime * 1000) }}</span>
<span>{{ videoData.title }}</span>

View File

@@ -7,8 +7,8 @@
<div class="tp-vote-list">
<div v-for="(item, index) in votes?.data" :key="index" class="tp-vote-item">
<div class="tp-vote-item-title">
<span>{{ item.title }}</span>
<span>
<span class="title">{{ item.title }}</span>
<span class="val">
<span>{{ item.count }}</span>
<span>{{ item.percent.toFixed(2) }}%</span>
</span>
@@ -90,25 +90,26 @@ function getWidth(item: TpVoteData): string {
display: flex;
align-items: center;
justify-content: space-between;
}
.tp-vote-item-title :first-child {
font-size: 16px;
font-weight: bold;
}
.title {
font-size: 16px;
font-weight: bold;
}
.tp-vote-item-title :last-child {
display: flex;
flex-direction: column;
gap: 5px;
}
.val {
display: flex;
flex-direction: column;
gap: 5px;
white-space: nowrap;
.tp-vote-item-title :last-child :first-child {
font-size: 12px;
}
:first-child {
font-size: 12px;
}
.tp-vote-item-title :last-child :last-child {
font-size: 10px;
:last-child {
font-size: 10px;
}
}
}
.tp-vote-progress {

View File

@@ -769,7 +769,7 @@
},
{
"id": 10000109,
"contentId": 0,
"contentId": 504440,
"dropDays": [1, 4, 7],
"name": "梦见月瑞希",
"itemType": "character",
@@ -2129,7 +2129,7 @@
},
{
"id": 14518,
"contentId": 0,
"contentId": 504503,
"dropDays": [2, 5, 7],
"name": "寝正月初晴",
"itemType": "weapon",
@@ -4129,7 +4129,7 @@
},
{
"id": 13432,
"contentId": 0,
"contentId": 504502,
"dropDays": [3, 6, 7],
"name": "且住亭御咄",
"itemType": "weapon",

View File

@@ -2520,5 +2520,41 @@
"postId": "61893154",
"up5List": [14518, 15513],
"up4List": [12416, 13416, 15416, 11402, 14409]
},
{
"name": "众水的颂诗",
"version": "5.4",
"order": 2,
"banner": "https://sdk.hoyoverse.com/upload/ann/2025/02/17/16502c6ed976ad895e0e36db251af49b_5339316421173554000.jpg",
"from": "2025-03-04T18:00:00+08:00",
"to": "2025-03-25T14:59:00+08:00",
"type": 301,
"postId": "62298070",
"up5List": [10000089],
"up4List": [10000088, 10000036, 10000080]
},
{
"name": "劫中泛滥",
"version": "5.4",
"order": 2,
"banner": "https://sdk.hoyoverse.com/upload/ann/2025/02/17/1b6cf538066209d3aa510291d351f7cb_20450832996386688.jpg",
"from": "2025-03-04T18:00:00+08:00",
"to": "2025-03-25T14:59:00+08:00",
"type": 400,
"postId": "62298071",
"up5List": [10000086],
"up4List": [10000088, 10000036, 10000080]
},
{
"name": "神铸赋形",
"version": "5.4",
"order": 2,
"banner": "https://sdk.hoyoverse.com/upload/ann/2025/02/17/f517f95eb862d8196bedcb9e44db3b5b_2023560355910093809.jpg",
"from": "2025-03-04T18:00:00+08:00",
"to": "2025-03-25T14:59:00+08:00",
"type": 302,
"postId": "62298072",
"up5List": [11513, 14513],
"up4List": [11401, 12403, 13401, 14403, 15405]
}
]

View File

@@ -9,7 +9,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": 14518, "contentId": 0, "name": "寝正月初晴", "star": 5, "weapon": "法器" },
{ "id": 14518, "contentId": 504503, "name": "寝正月初晴", "star": 5, "weapon": "法器" },
{ "id": 14517, "contentId": 503948, "name": "祭星者之望", "star": 5, "weapon": "法器" },
{ "id": 14516, "contentId": 501962, "name": "冲浪时光", "star": 5, "weapon": "法器" },
{ "id": 14515, "contentId": 500674, "name": "鹤鸣余音", "star": 5, "weapon": "法器" },
@@ -101,7 +101,7 @@
{ "id": 14403, "contentId": 197, "name": "祭礼残章", "star": 4, "weapon": "法器" },
{ "id": 14402, "contentId": 192, "name": "流浪乐章", "star": 4, "weapon": "法器" },
{ "id": 14401, "contentId": 185, "name": "西风秘典", "star": 4, "weapon": "法器" },
{ "id": 13432, "contentId": 0, "name": "且住亭御咄", "star": 4, "weapon": "长柄武器" },
{ "id": 13432, "contentId": 504502, "name": "且住亭御咄", "star": 4, "weapon": "长柄武器" },
{ "id": 13431, "contentId": 501966, "name": "虹的行迹", "star": 4, "weapon": "长柄武器" },
{ "id": 13430, "contentId": 502897, "name": "镇山之钉", "star": 4, "weapon": "长柄武器" },
{ "id": 13427, "contentId": 7407, "name": "勘探钻机", "star": 4, "weapon": "长柄武器" },

View File

@@ -2,7 +2,7 @@
<div class="ab-container">
<div class="ab-draw-top">
<div @click="toAct" class="ab-draw-act" title="前往网页活动">
<img src="/source/UI/act_birthday.png" alt="archive_birthday_icon" class="side-icon" />
<img src="/source/UI/act_birthday.webp" alt="archive_birthday_icon" class="side-icon" />
</div>
<v-switch class="ab-draw-switch" v-model="isAether" />
<span>{{ isAether ? "空" : "荧" }}</span>
@@ -20,7 +20,9 @@
<div class="ab-draw-grid">
<div v-for="item in selectedItem" :key="item.op_id" class="ab-draw">
<div class="ab-draw-cover" @click="showImg(item)">
<TMiImg :src="item.unread_picture[Number(isAether)]" :alt="item.word_text" />
<div class="ab-draw-img">
<TMiImg :src="item.unread_picture[Number(isAether)]" :alt="item.word_text" />
</div>
<div class="ab-draw-hide" />
<v-icon class="ab-draw-icon">mdi-magnify</v-icon>
</div>
@@ -158,6 +160,15 @@ function getItemProps(item: TGApp.Archive.Birth.RoleItem) {
aspect-ratio: 125 / 54;
}
.ab-draw-img {
position: relative;
display: flex;
width: 100%;
height: 100%;
align-items: center;
justify-content: center;
}
.ab-draw-hide {
position: absolute;
top: 0;

View File

@@ -305,6 +305,7 @@ async function uploadAbyss(): Promise<void> {
}
showSnackbar.success(res.message ?? "上传深渊数据成功");
await TGLogger.Info("[UserAbyss][uploadAbyss] 上传深渊数据成功");
await TGLogger.Info(`[${res.retcode}] ${res.message}`);
} catch (e) {
if (e instanceof Error) {
showSnackbar.error(e.message);

264
src/pages/User/Scripts.vue Normal file
View File

@@ -0,0 +1,264 @@
<template>
<v-app-bar>
<template #prepend>
<div class="us-top-title">
<img alt="icon" src="/source/UI/toolbox.webp" />
<span>实用脚本</span>
<v-select
class="us-top-select"
variant="outlined"
v-model="curAccount"
:items="accounts"
item-title="uid"
:hide-details="true"
title="账号UID"
>
<template #selection="{ item }">
<div class="select-main">
<img alt="icon" :src="item.raw.brief.avatar" />
<div class="content">
<span>{{ item.raw.brief.nickname }}</span>
<span>UID:{{ item.raw.uid }}</span>
</div>
</div>
</template>
<template #item="{ props, item }">
<div class="select-item" v-bind="props">
<img alt="icon" :src="item.raw.brief.avatar" />
<div class="content">
<span>{{ item.raw.brief.nickname }}</span>
<span>UID:{{ item.raw.uid }}</span>
</div>
<div class="append">
<v-icon v-if="item.raw.uid === uid" color="green" title="当前登录账号">
mdi-account-check
</v-icon>
<v-icon
v-else
size="small"
icon="mdi-account-convert"
title="切换用户"
@click="loadAccount(item.raw.uid)"
/>
</div>
</div>
</template>
</v-select>
</div>
</template>
<template #append>
<span class="top-hint" @click="tryCkVerify()" title="点击验证">
需要验证码登录所需cookie
</span>
</template>
</v-app-bar>
<div class="us-page-container">
<!-- 左侧脚本列表 -->
<div class="us-scripts">
<div class="us-title">脚本列表</div>
<TusMission v-model="runScript" />
</div>
<!-- 右侧脚本输出 -->
<TusOutput />
</div>
</template>
<script lang="ts" setup>
import showDialog from "@comp/func/dialog.js";
import showLoading from "@comp/func/loading.js";
import showSnackbar from "@comp/func/snackbar.js";
import TusMission from "@comp/userScripts/tus-mission.vue";
import TusOutput from "@comp/userScripts/tus-output.vue";
import TSUserAccount from "@Sqlite/modules/userAccount.js";
import { storeToRefs } from "pinia";
import { onMounted, ref, shallowRef } from "vue";
import { useUserStore } from "@/store/modules/user.js";
import apiHubReq from "@/web/request/apiHubReq.js";
const { uid, briefInfo, cookie, account } = storeToRefs(useUserStore());
const accounts = shallowRef<Array<TGApp.App.Account.User>>([]);
const curAccount = shallowRef<TGApp.App.Account.User>();
const runScript = ref<boolean>(false);
onMounted(async () => {
accounts.value = await TSUserAccount.account.getAllAccount();
curAccount.value = accounts.value.find((i) => i.uid === uid.value);
});
async function loadAccount(ac: string): Promise<void> {
if (uid.value && ac === uid.value) {
showSnackbar.warn("该账户已经登录,无需切换");
return;
}
const accountGet = await TSUserAccount.account.getAccount(ac);
if (!accountGet) {
showSnackbar.warn(`未找到${uid}的账号信息,请重新登录`);
return;
}
uid.value = ac;
briefInfo.value = accountGet.brief;
cookie.value = accountGet.cookie;
const gameAccount = await TSUserAccount.game.getCurAccount(ac);
if (!gameAccount) {
showSnackbar.warn(`未找到${uid}的游戏账号信息,请尝试刷新`);
return;
}
account.value = gameAccount;
showSnackbar.success(`成功切换到用户${uid}`);
}
async function tryCkVerify(): Promise<void> {
if (!cookie.value) {
showSnackbar.warn("当前账号未登录,请先登录");
return;
}
const check = await showDialog.check("确定验证?", "将通过执行米社社区打卡以验证ck有效性");
if (!check) {
showSnackbar.cancel("已取消验证");
return;
}
await showLoading.start("正在验证CK有效性");
const ck = {
stoken: cookie.value.stoken,
stuid: cookie.value.stuid,
mid: cookie.value.mid,
};
const resp = await apiHubReq.sign(ck);
await showLoading.update(`[${resp.retcode}] ${resp.message}`);
if (resp.retcode === -100) {
showSnackbar.error("CK验证失败请通过验证码登录重新获取CK");
await showLoading.end();
return;
}
await showLoading.end();
showSnackbar.success("CK验证成功");
}
</script>
<style lang="scss" scoped>
.us-top-title {
display: flex;
align-items: center;
justify-content: center;
padding: 10px;
gap: 10px;
img {
width: 32px;
height: 32px;
}
span {
color: var(--common-text-title);
font-family: var(--font-title);
font-size: 20px;
}
}
.us-top-select {
width: 250px;
max-width: 250px;
}
.select-main {
position: relative;
display: flex;
align-items: center;
justify-content: center;
column-gap: 4px;
height: 24px;
img {
width: 24px;
height: 24px;
}
.content {
position: relative;
display: flex;
flex-direction: column;
:first-child {
font-family: var(--font-title);
font-size: 12px;
}
:last-child {
font-size: 10px;
}
}
}
.select-item {
position: relative;
width: 100%;
display: flex;
align-items: center;
justify-content: flex-start;
column-gap: 4px;
padding: 8px;
box-sizing: border-box;
img {
width: 24px;
height: 24px;
}
.content {
position: relative;
display: flex;
flex-direction: column;
:first-child {
font-family: var(--font-title);
font-size: 12px;
}
:last-child {
font-size: 10px;
}
}
.append {
display: flex;
align-items: center;
justify-content: center;
margin-left: auto;
}
}
.top-hint {
position: relative;
padding: 8px;
font-size: 20px;
color: var(--tgc-pink-1);
font-family: var(--font-title);
cursor: pointer;
}
.us-page-container {
display: flex;
height: calc(100vh - 100px);
align-items: flex-start;
justify-content: center;
column-gap: 12px;
}
.us-scripts {
position: relative;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
overflow-y: auto;
row-gap: 4px;
}
.us-title {
position: relative;
margin-right: auto;
color: var(--common-text-title);
font-family: var(--font-title);
font-size: 24px;
}
</style>

View File

@@ -95,11 +95,11 @@ watch(
);
onMounted(async () => {
await showLoading.start("正在获取深渊数据", "正在获取深渊概览");
overview.value = {
cur: await Hutao.Abyss.overview(),
last: await Hutao.Abyss.overview(true),
};
await showLoading.start("正在获取深渊数据", "正在获取上期深渊概览");
const lastData = await Hutao.Abyss.overview(true);
await showLoading.update("正在获取本期深渊概览");
const curData = await Hutao.Abyss.overview();
overview.value = { scur: curData, last: lastData };
await showLoading.update("正在获取角色使用率数据");
abyssData.use = <AbyssDataItem<TGApp.Plugins.Hutao.Abyss.AvatarUse[]>>await getData("use");
await showLoading.end();

View File

@@ -2,8 +2,8 @@
<div class="wc-box">
<div class="wc-left">
<div class="wc-select">
<v-btn @click="showSelect = true">筛选角色</v-btn>
<v-btn @click="resetSelect = true">重置筛选</v-btn>
<v-btn @click="showSelect = true" class="wc-btn">筛选角色</v-btn>
<v-btn @click="resetSelect = true" class="wc-btn">重置筛选</v-btn>
</div>
<div class="wc-list">
<TwcListItem
@@ -129,6 +129,16 @@ async function toOuter(item?: TGApp.App.Character.WikiBriefInfo): Promise<void>
gap: 10px;
}
.wc-btn {
display: flex;
align-items: center;
justify-content: center;
padding: 10px;
border-radius: 5px;
background: var(--tgc-btn-1);
color: var(--btn-text);
}
.wc-list {
position: relative;
display: grid;

View File

@@ -228,7 +228,7 @@ function searchMaterial(): void {
.twm-item-right {
position: relative;
overflow: hidden;
max-width: 100%;
max-width: calc(100% - 50px);
color: var(--box-text-2);
font-size: 14px;
text-overflow: ellipsis;

View File

@@ -2,10 +2,8 @@
<div class="ww-box">
<div class="ww-left">
<div class="ww-select">
<v-btn @click="showSelect = true">
<span>筛选武器</span>
</v-btn>
<v-btn @click="resetSelect = true">重置筛选</v-btn>
<v-btn @click="showSelect = true" class="ww-btn">筛选武器</v-btn>
<v-btn @click="resetSelect = true" class="ww-btn">重置筛选</v-btn>
</div>
<div class="ww-list">
<TwcListItem
@@ -116,6 +114,16 @@ async function toOuter(item?: TGApp.App.Weapon.WikiBriefInfo): Promise<void> {
gap: 10px;
}
.ww-btn {
display: flex;
align-items: center;
justify-content: center;
padding: 10px;
border-radius: 5px;
background: var(--tgc-btn-1);
color: var(--btn-text);
}
.ww-list {
position: relative;
display: grid;

View File

@@ -76,6 +76,7 @@
:label="devMode ? '开启' : '关闭'"
:inset="true"
color="#FAC51E"
class="config-switch"
@click="submitDevMode"
/>
</template>
@@ -92,10 +93,28 @@
:label="isNeedResize ? '开启' : '关闭'"
:inset="true"
color="#FAC51E"
class="config-switch"
@click="submitResize"
/>
</template>
</v-list-item>
<v-list-item title="无痕浏览" subtitle="关闭后将记录帖子浏览记录">
<template #prepend>
<div class="config-icon">
<v-icon>mdi-incognito</v-icon>
</div>
</template>
<template #append>
<v-switch
v-model="appStore.incognito"
:label="appStore.incognito ? '开启' : '关闭'"
:inset="true"
class="config-switch"
color="#FAC51E"
@click="switchIncognito"
/>
</template>
</v-list-item>
<v-list-item title="分享设置" v-if="platform() === 'windows'">
<template #subtitle>默认保存到剪贴板超过{{ shareDefaultFile }}MB时保存到文件</template>
<template #prepend>
@@ -138,7 +157,7 @@ import TcGameBadge from "@comp/pageConfig/tc-gameBadge.vue";
import TcInfo from "@comp/pageConfig/tc-info.vue";
import TcUserBadge from "@comp/pageConfig/tc-userBadge.vue";
import TGSqlite from "@Sqlite/index.js";
import { core } from "@tauri-apps/api";
import { core, event } from "@tauri-apps/api";
import { open } from "@tauri-apps/plugin-dialog";
import { remove } from "@tauri-apps/plugin-fs";
import { platform } from "@tauri-apps/plugin-os";
@@ -491,6 +510,16 @@ function submitResize(): void {
}
showSnackbar.success("已开启窗口回正!");
}
// 开启无痕浏览
async function switchIncognito(): Promise<void> {
await event.emitTo("Sub_window", "switchIncognito");
if (appStore.incognito) {
showSnackbar.success("已关闭无痕浏览!");
return;
}
showSnackbar.success("已开启无痕浏览!");
}
</script>
<style lang="css" scoped>
.config-box {
@@ -529,6 +558,10 @@ function submitResize(): void {
color: var(--box-text-2);
}
.config-switch {
height: 40px;
}
.config-right {
position: fixed;
top: 16px;

View File

@@ -13,9 +13,27 @@
</div>
</div>
</div>
<div class="btn-list">
<v-btn @click="test()" class="test-btn">测试</v-btn>
</div>
</div>
</template>
<script lang="ts" setup></script>
<script lang="ts" setup>
import showSnackbar from "@comp/func/snackbar.js";
import takumiReq from "@/web/request/takumiReq.js";
const gameToken = "LCLQ2pYLnEDh7p03ogJVxL9dZqbeLtUE";
const uid = "249066520";
async function test(): Promise<void> {
const resp = await takumiReq.game.stoken({ uid: uid, token: gameToken });
if (resp.retcode !== 0) {
showSnackbar.error(`[${resp.retcode}] ${resp.message}`);
return;
}
}
</script>
<style lang="css" scoped>
.test-box {
display: flex;

View File

@@ -2,7 +2,7 @@
<v-app-bar>
<div class="pc-top">
<div class="pc-title">
<img src="/source/UI/posts.png" alt="posts" />
<img src="/source/UI/posts.webp" alt="posts" />
<span>收藏</span>
</div>
<v-select

View File

@@ -2,7 +2,7 @@
<v-app-bar>
<template #prepend>
<div class="posts-top">
<img src="/source/UI/posts.png" alt="posts" />
<img src="/source/UI/posts.webp" alt="posts" />
<span>帖子</span>
</div>
</template>

View File

@@ -1,10 +1,11 @@
/**
* @file plugins/Mys/request/postReq.ts
* @description 帖子相关的获取
* @since Beta v0.6.7
* @since Beta v0.7.0
*/
import TGHttp from "@/utils/TGHttp.js";
import { getRequestHeader } from "@/web/utils/getRequestHeader.js";
// MysPostApiBaseUrl => Mpabu
const Mpabu: Readonly<string> = "https://bbs-api.mihoyo.com/post/wapi/";
@@ -14,17 +15,27 @@ const Referer: Readonly<string> = "https://bbs.mihoyo.com/";
/**
* @description 获取单个帖子信息
* @since Beta v0.6.3
* @since Beta v0.7.0
* @param {number} postId 帖子 ID
* @param {Record<string, string>} cookie Cookie
* @return {Promise<TGApp.Plugins.Mys.Post.FullData|TGApp.BBS.Response.Base>}
*/
export async function getPostFull(
postId: number,
cookie?: Record<string, string>,
): Promise<TGApp.Plugins.Mys.Post.FullData | TGApp.BBS.Response.Base> {
const param = { post_id: postId, read: 1 };
let header;
if (cookie) {
header = {
...getRequestHeader(cookie, "GET", param, "K2", true),
"x-rpc-client_type": "2",
};
} else header = { referer: Referer };
const resp = await TGHttp<TGApp.Plugins.Mys.Post.Response>(`${Mpabu}getPostFull`, {
method: "GET",
headers: { referer: Referer },
query: { post_id: postId },
headers: header,
query: param,
});
if (resp.retcode !== 0) return <TGApp.BBS.Response.Base>resp;
return resp.data.post;

View File

@@ -1,10 +1,10 @@
/**
* @file plugins/Sqlite/modules/avatarBirth.ts
* @description 角色生日模块
* @since Beta v0.4.6
* @since Beta v0.7.0
*/
import { AppCharacterData, ArcBirCalendar, ArcBirRole } from "@/data/index.js";
import { AppCharacterData, ArcBirCalendar, ArcBirRole, WikiCharacterData } from "@/data/index.js";
/**
* @description 判断今天是不是角色生日
@@ -32,44 +32,77 @@ function isAvatarBirth(): TGApp.Archive.Birth.CalendarItem[] {
}
/**
* @description 获取角色生日
* @description 校验是否是闰年
* @since Beta v0.4.6
* @param {string} roleBirthday - 角色生日
* @return {[number,number]} 角色生日
* @param {number} year - 年份
* @return {boolean} 是否是闰年
*/
function getRoleBirth(roleBirthday: string): [number, number] {
const arr: string[] = roleBirthday.split("/");
return [Number(arr[0]), Number(arr[1])];
function isLeapYear(year: number): boolean {
return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
}
/**
* @description 获取下一个角色生日
* @since Beta v0.4.5
* @since Beta v0.7.0
* @param {[number,number]} date - 日期
* @return {TGApp.Archive.Birth.RoleItem[]} 下一个角色生日
*/
function getNextAvatarBirth(date?: [number, number]): TGApp.Archive.Birth.RoleItem[] {
const year = new Date().getFullYear();
let month, day;
if (date) {
month = date[0];
day = date[1];
} else {
const dateNow = new Date();
month = dateNow.getMonth() + 1;
day = dateNow.getDate();
month = new Date().getMonth() + 1;
day = new Date().getDate();
}
const birthDateList: Date[] = [];
for (const item of ArcBirRole) {
const roleBirth = getRoleBirth(item.role_birthday);
if (roleBirth[0] < month || (roleBirth[0] === month && roleBirth[1] <= day)) {
birthDateList.push(new Date(year + 1, roleBirth[0] - 1, roleBirth[1]));
} else birthDateList.push(new Date(year, roleBirth[0] - 1, roleBirth[1]));
const sortList = AppCharacterData.sort((a, b) => {
if (a.birthday[0] === b.birthday[0]) return a.birthday[1] - b.birthday[1];
return a.birthday[0] - b.birthday[0];
});
let filterList = sortList.filter((i) => {
if (i.birthday[0] > month) return true;
return i.birthday[0] === month && i.birthday[1] > day;
});
if (filterList.length === 0) filterList = sortList;
let birthGet = filterList[0];
if (birthGet.id === 10000032 && !isLeapYear(new Date().getFullYear())) {
birthGet = filterList[1];
}
birthDateList.sort((a, b) => a.getTime() - b.getTime());
const nextDateGet = birthDateList[0];
const nextDate = [nextDateGet.getMonth() + 1, nextDateGet.getDate()];
return ArcBirRole.filter((i) => i.role_birthday === `${nextDate[0]}/${nextDate[1]}`);
const dataGet = AppCharacterData.filter(
(i) => i.birthday.toString() === birthGet.birthday.toString(),
);
const res: TGApp.Archive.Birth.RoleItem[] = [];
for (const i of dataGet) {
const find = ArcBirRole.find((j) => j.role_id === i.id);
if (find) {
res.push(find);
continue;
}
const find2 = WikiCharacterData.find((j) => j.id === i.id);
if (!find2) continue;
// 只写了用到的字段
res.push({
belong: find2.brief.camp,
current_compensate_num: 0,
divine_type: "",
element: find2.element,
head_image: `/WIKI/character/${i.id}.webp`,
introduce: find2.description,
is_compensate_num: false,
is_finish_task: false,
is_god: false,
seat_life: "",
text: "",
year_compensate_num: 0,
role_id: i.id,
name: i.name,
role_birthday: `${i.birthday[0]}/${i.birthday[1]}`,
head_icon: `/WIKI/character/${i.id}.webp`,
is_subscribe: false,
});
}
return res;
}
const TSAvatarBirth = { isAvatarBirth, getNextAvatarBirth };

View File

@@ -1,7 +1,7 @@
/**
* @file router index.ts
* @description 路由入口
* @since Beta v0.6.8
* @since Beta v0.7.0
*/
import { createRouter, createWebHistory } from "vue-router";
@@ -9,11 +9,13 @@ import { createRouter, createWebHistory } from "vue-router";
import routes from "./routes.js";
const router = createRouter({ history: createWebHistory(), routes: routes });
// 只有在特定页面忽略参数变化
const ignoreRoutes: ReadonlyArray<string> = ["酒馆", "话题"];
// 解决路由重复问题
router.afterEach((to, from) => {
if (from.name === to.name) {
if (from.query !== to.query) return;
if (from.query !== to.query && ignoreRoutes.includes(from.name?.toString() ?? "")) return;
window.location.reload();
}
});

View File

@@ -1,7 +1,7 @@
/**
* @file router modules user.ts
* @description user 路由模块
* @since Beta v0.6.3
* @since Beta v0.7.0
*/
import type { RouteRecordRaw } from "vue-router";
@@ -31,6 +31,11 @@ const userRoutes = (<const>[
name: "原神战绩",
component: async () => await import("@/pages/User/Record.vue"),
},
{
path: "/user/scripts",
name: "实用脚本",
component: async () => await import("@/pages/User/Scripts.vue"),
},
]) satisfies Array<RouteRecordRaw>;
export default userRoutes;

View File

@@ -61,6 +61,8 @@ export const useAppStore = defineStore(
const shareDefaultFile = ref<number>(10);
// 图像压缩质量
const imageQualityPercent = ref<number>(80);
// 无痕浏览
const incognito = ref<boolean>(true);
// 初始化
function init(): void {
@@ -75,6 +77,7 @@ export const useAppStore = defineStore(
gameDir.value = "未设置";
shareDefaultFile.value = 10;
imageQualityPercent.value = 10;
incognito.value = true;
initDevice();
}
@@ -109,6 +112,7 @@ export const useAppStore = defineStore(
gameDir,
shareDefaultFile,
imageQualityPercent,
incognito,
init,
changeTheme,
getImageUrl,
@@ -132,6 +136,7 @@ export const useAppStore = defineStore(
"needResize",
"shareDefaultFile",
"imageQualityPercent",
"incognito",
],
},
{

112
src/types/BBS/Mission.d.ts vendored Normal file
View File

@@ -0,0 +1,112 @@
/**
* @file types/BBS/Mission.d.ts
* @description BBS 任务相关类型定义文件
* @since Beta v0.7.0
*/
declare namespace TGApp.BBS.Mission {
/**
* @description 任务信息返回
* @interface InfoResp
* @extends TGApp.BBS.Response.BaseWithData
* @since Beta v0.7.0
* @property {TGApp.BBS.Mission.InfoRes} data 任务信息
* @return InfoResp
*/
type InfoResp = TGApp.BBS.Response.BaseWithData<InfoRes>;
/**
* @description 任务信息
* @interface InfoRes
* @since Beta v0.7.0
* @property {Array<MissionItem>} missions 任务列表
* @property {Array<MissionItem>} more_missions 更多任务列表
* @return InfoRes
*/
type InfoRes = { missions: Array<MissionItem>; more_missions: Array<MissionItem> };
/**
* @description 任务项
* @interface MissionItem
* @since Beta v0.7.0
* @property {number} id 任务 ID
* @property {string} name 任务名称
* @property {string} desc 任务描述
* @property {number} threshold 任务完成阈值
* @property {number} limit 任务限制
* @property {number} exp 任务经验
* @property {number} points 米游币
* @property {number} active_time 任务激活时间,秒级时间戳
* @property {number} end_time 任务结束时间
* @property {boolean} is_auto_send_award 是否自动发放奖励
* @property {number} continuous_cycle_times 连续周期次数
* @property {number} next_points 下一次奖励米游币
* @property {string} mission_key 任务 key
* @return MissionItem
*/
type MissionItem = {
id: number;
name: string;
desc: string;
threshold: number;
limit: number;
exp: number;
points: number;
active_time: number;
end_time: number;
is_auto_send_award: boolean;
continuous_cycle_times: number;
next_points: number;
mission_key: string;
};
/**
* @description 任务状态返回
* @interface StateResp
* @extends TGApp.BBS.Response.BaseWithData
* @since Beta v0.7.0
* @property {TGApp.BBS.Mission.StateRes} data 任务状态
* @return StateResp
*/
type StateResp = TGApp.BBS.Response.BaseWithData<StateRes>;
/**
* @description 任务状态
* @interface StateRes
* @since Beta v0.7.0
* @property {Array<StateItem>} states 任务状态列表
* @property {number} already_received_points 已领取的米游币
* @property {number} total_points 总米游币
* @property {number} today_total_points 今日总米游币
* @property {boolean} is_unclaimed 是否有未领取的奖励
* @property {number} can_get_points 可领取的米游币
* @return StateRes
*/
type StateRes = {
states: Array<StateItem>;
already_received_points: number;
total_points: number;
today_total_points: number;
is_unclaimed: boolean;
can_get_points: number;
};
/**
* @description 任务状态项
* @interface StateItem
* @since Beta v0.7.0
* @property {number} mission_id 任务 ID
* @property {number} process 任务进度 0未完成1已完成
* @property {number} happened_times 发生次数
* @property {boolean} is_get_award 是否领取奖励
* @property {string} mission_key 任务 key
* @return StateItem
*/
type StateItem = {
mission_id: number;
process: number;
happened_times: number;
is_get_award: boolean;
mission_key: string;
};
}

75
src/types/Game/Login.d.ts vendored Normal file
View File

@@ -0,0 +1,75 @@
/**
* @file types/Game/Login.d.ts
* @description 扫码登录模块类型定义文件
* @since Beta v0.7.0
*/
declare namespace TGApp.Game.Login {
/**
* @description 获取登录二维码返回数据
* @since Beta v0.7.0
* @interface QrResp
* @extends TGApp.BBS.Response.BaseWithData
* @property {QrRes} data 数据
* @return QrResp
*/
type QrResp = TGApp.BBS.Response.BaseWithData & { data: QrRes };
/**
* @description 获取登录二维码返回数据
* @since Beta v0.7.0
* @interface QrRes
* @property {string} url 二维码链接
* @return QrRes
*/
type QrRes = { url: string };
/**
* @description 获取登录状态返回数据
* @since Beta v0.7.0
* @interface StatusResp
* @extends TGApp.BBS.Response.BaseWithData
* @property {StatusRes} data 数据
* @return StatusResp
*/
type StatusResp = TGApp.BBS.Response.BaseWithData & { data: StatusRes };
/**
* @description 二维码状态
* @since Beta v0.7.0
* @interface QrStatus
* @return QrStatus
*/
type QrStatus = "Init" | "Scanned" | "Confirmed";
/**
* @description 获取登录状态返回数据
* @since Beta v0.7.0
* @interface StatusRes
* @property {string} stat 状态 // Init: 未扫码Scanned: 已扫码Confirmed: 已确认
* @property {string} payload 状态数据
* @return StatusRes
*/
type StatusRes = { stat: QrStatus; payload: StatusPayload };
/**
* @description 获取登录状态返回数据
* @since Beta v0.7.0
* @interface StatusPayload
* @property {string} ext 未知
* @property {string} proto 未知
* @property {string} raw 序列化数据,反序列化后是 StatusPayloadRaw
* @return StatusPayload
*/
type StatusPayload = { ext: string; proto: string; raw: string };
/**
* @description 反序列化后的登录状态数据
* @since Beta v0.7.0
* @interface StatusPayloadRaw
* @property {string} uid 用户 UID
* @property {string} token 用户 token
* @return StatusPayloadRaw
*/
type StatusPayloadRaw = { uid: string; token: string };
}

View File

@@ -1,10 +1,10 @@
/**
* @file utils/TGBbs.ts
* @description 关于 BBS 的工具函数
* @since Beta v0.6.8
* @since Beta v0.7.0
*/
const BBS_VERSION: Readonly<string> = "2.80.1";
const BBS_VERSION: Readonly<string> = "2.82.0";
const BBS_UA_MOBILE: Readonly<string> = `Mozilla/5.0 (Linux; Android 12) Mobile miHoYoBBS/${BBS_VERSION}`;
/**
@@ -33,17 +33,6 @@ const CHANNEL_LIST: Readonly<Array<ChannelItem>> = [
{ title: "大别野", gid: 5, mini: "dby" },
];
/**
* @description 获取游戏id
* @since Beta v0.6.8
* @param {string} mini
* @returns {string}
*/
export function getGameId(mini: string): number {
const game = CHANNEL_LIST.find((item) => item.mini === mini);
return game ? game.gid : 0;
}
const TGBbs = { version: BBS_VERSION, ua: BBS_UA_MOBILE, channels: CHANNEL_LIST };
export default TGBbs;

View File

@@ -1,10 +1,13 @@
/**
* @file utils/TGLogger.ts
* @description 日志工具
* @since Beta v0.6.8
* @since Beta v0.7.0
*/
import { attachConsole, error, info, warn, debug } from "@tauri-apps/plugin-log";
import { event } from "@tauri-apps/api";
import { attachConsole, debug, error, info, warn } from "@tauri-apps/plugin-log";
import { timestampToDate } from "@/utils/toolFunc.js";
/**
* @description 日志工具
@@ -71,6 +74,34 @@ class Logger {
if (write) await error(message);
console.error(message);
}
/**
* @description 输出日志-脚本
* @since Beta v0.7.0
* @param {string} message 日志信息
* @returns {Promise<void>} 无返回值
*/
async Script(message: string): Promise<void> {
const timeNow = timestampToDate(new Date().getTime());
const msg = `[${timeNow}]${message}`;
await event.emitTo("TeyvatGuide", "userScriptLog", msg);
await info(message);
}
/**
* @description 输出日志-脚本分隔符
* @since Beta v0.7.0
* @param {string} label 标签
* @param {boolean} [start] 是否为开始,默认为 true
* @returns {Promise<void>} 无返回值
*/
async ScriptSep(label: string, start: boolean = true): Promise<void> {
const midStr = `${label} ${start ? "START" : "END--"}`;
const msg = `//--------------------${midStr}--------------------//`;
await event.emitTo("TeyvatGuide", "userScriptLog", msg);
if (!start) await event.emitTo("TeyvatGuide", "userScriptLog", "");
await info(msg);
}
}
const TGLogger = Logger.getInstance();

View File

@@ -5,7 +5,6 @@
*/
import TSUserGacha from "@Sqlite/modules/userGacha.js";
// @ts-expect-error no-export-member
import type { BarSeriesOption } from "echarts/charts.js";
import type { EChartsOption, XAXisOption } from "echarts/types/dist/shared.js";

View File

@@ -1,28 +1,96 @@
<template>
<TSwitchTheme />
<div class="anno-json">
<div class="anno-title">活动列表 JSON</div>
<JsonViewer :value="jsonList" copyable boxed />
<div class="anno-title">活动内容 JSON</div>
<JsonViewer :value="jsonContent" copyable boxed />
<div class="taj-page">
<v-expansion-panels>
<v-expansion-panel>
<template #title>
<div class="taj-title">活动列表 JSON</div>
</template>
<template #text>
<div class="taj-box">
<vue-json-pretty
:data="JSON.parse(JSON.stringify(jsonList))"
:show-icon="true"
:show-length="true"
:show-line="true"
:show-line-number="true"
:show-double-quotes="true"
:show-key-value-space="true"
:collapsed-on-click-brackets="true"
:deep="2"
:theme="jsonTheme"
/>
</div>
</template>
</v-expansion-panel>
<v-expansion-panel>
<template #title>
<div class="taj-title">活动内容 JSON</div>
</template>
<template #text>
<div class="taj-box">
<vue-json-pretty
:data="JSON.parse(JSON.stringify(jsonContent))"
:show-icon="true"
:show-length="true"
:show-line="true"
:show-line-number="true"
:show-double-quotes="true"
:show-key-value-space="true"
:collapsed-on-click-brackets="true"
:deep="2"
:theme="jsonTheme"
/>
</div>
</template>
</v-expansion-panel>
<v-expansion-panel>
<template #title>
<div class="taj-title">解析 JSON</div>
</template>
<template #text>
<div class="taj-box">
<vue-json-pretty
:data="JSON.parse(JSON.stringify(parsedJson))"
:show-icon="true"
:show-length="true"
:show-line="true"
:show-line-number="true"
:show-double-quotes="true"
:show-key-value-space="true"
:collapsed-on-click-brackets="true"
:deep="2"
:theme="jsonTheme"
/>
</div>
</template>
</v-expansion-panel>
</v-expansion-panels>
</div>
</template>
<script lang="ts" setup>
import TSwitchTheme from "@comp/app/t-switchTheme.vue";
import showLoading from "@comp/func/loading.js";
import { onMounted, shallowRef } from "vue";
import JsonViewer from "vue-json-viewer";
import { storeToRefs } from "pinia";
import { computed, onMounted, shallowRef } from "vue";
import VueJsonPretty from "vue-json-pretty";
import "vue-json-pretty/lib/styles.css";
import { useRoute } from "vue-router";
import { useAppStore } from "@/store/modules/app.js";
import Hk4eApi, { type AnnoLang, AnnoServer } from "@/web/request/hk4eReq.js";
import parseAnnoContent from "@/web/utils/annoParser.js";
// 数据
const route = useRoute();
const annoId = Number(route.params.anno_id);
const region = <AnnoServer>route.params.region;
const lang = <AnnoLang>route.params.lang;
const { theme } = storeToRefs(useAppStore());
const jsonList = shallowRef<TGApp.BBS.Announcement.AnnoSingle>();
const jsonContent = shallowRef<TGApp.BBS.Announcement.ContentItem>();
const parsedJson = shallowRef<Array<TGApp.Plugins.Mys.SctPost.Base>>();
const jsonTheme = computed<"dark" | "light">(() => (theme.value === "dark" ? "dark" : "light"));
onMounted(async () => {
await showLoading.start("正在获取公告数据");
@@ -41,32 +109,30 @@ onMounted(async () => {
}
}
jsonContent.value = await Hk4eApi.anno.content(annoId, region, lang);
parsedJson.value = parseAnnoContent(jsonContent.value);
await showLoading.end();
});
</script>
<style lang="css" scoped>
.anno-json {
padding: 20px;
border-radius: 20px;
<style lang="scss" scoped>
.taj-page {
width: 800px;
margin: 0 auto;
font-family: var(--font-text);
}
.anno-title {
width: 100%;
margin: 10px 0;
color: #546d8b;
.taj-title {
color: var(--common-text-title);
font-family: var(--font-title);
font-size: 20px;
font-weight: 600;
text-align: right;
}
.jv-container {
background: var(--box-bg-2) !important;
}
.jv-key,
.jv-array {
color: var(--box-text-4) !important;
.taj-box {
border-radius: 4px;
position: relative;
width: 100%;
padding: 12px;
box-sizing: border-box;
max-width: 100%;
}
</style>

View File

@@ -3,7 +3,9 @@
<TPinWin />
<TShareBtn selector=".anno-body" :title="`Anno_${route.params.anno_id}`" />
<div class="anno-body" v-if="annoData">
<div class="anno-info">AnnoID: {{ annoId }} | Render by TeyvatGuide v{{ appVersion }}</div>
<div class="anno-info" @click="createAnnoJson">
AnnoID: {{ annoId }} | Render by TeyvatGuide v{{ appVersion }}
</div>
<div class="anno-title">{{ annoData.title }}</div>
<div class="anno-subtitle">{{ parseText(annoData.subtitle) }}</div>
<div class="anno-content">
@@ -57,7 +59,7 @@ onMounted(async () => {
return;
}
const isDev = useAppStore().devMode ?? false;
if (isDev) await createAnnoJson(annoId, region, lang);
if (isDev) await createAnnoJson();
await showLoading.end();
});
@@ -67,7 +69,7 @@ function parseText(title: string): string {
return div.innerText;
}
async function createAnnoJson(annoId: number, region: AnnoServer, lang: AnnoLang): Promise<void> {
async function createAnnoJson(): Promise<void> {
const jsonPath = `/anno_detail_json/${region}/${annoId}/${lang}`;
const jsonTitle = `Anno_${region}_${annoId}_${lang}_JSON`;
await createTGWindow(jsonPath, "Dev_JSON", jsonTitle, 960, 720, false, false);

View File

@@ -1,10 +1,50 @@
<template>
<TSwitchTheme />
<div class="post-json">
<div class="post-title">帖子返回内容 JSON</div>
<JsonViewer :value="jsonData" copyable boxed />
<div class="post-title" v-show="!isEmpty">结构化内容解析</div>
<JsonViewer v-if="!isEmpty" :value="parseData" copyable boxed />
<div class="tpj-page">
<v-expansion-panels>
<v-expansion-panel>
<template #title>
<div class="tpj-title">帖子返回内容 JSON</div>
</template>
<template #text>
<div class="tpj-box">
<vue-json-pretty
:data="JSON.parse(JSON.stringify(jsonData))"
:show-icon="true"
:show-length="true"
:show-line="true"
:show-line-number="true"
:show-double-quotes="true"
:show-key-value-space="true"
:collapsed-on-click-brackets="true"
:deep="2"
:theme="jsonTheme"
/>
</div>
</template>
</v-expansion-panel>
<v-expansion-panel>
<template #title>
<div class="tpj-title">帖子解析内容 JSON</div>
</template>
<template #text>
<div class="tpj-box">
<vue-json-pretty
:data="parseData"
:show-icon="true"
:show-length="true"
:show-line="true"
:show-line-number="true"
:show-double-quotes="true"
:show-key-value-space="true"
:collapsed-on-click-brackets="true"
:deep="2"
:theme="jsonTheme"
/>
</div>
</template>
</v-expansion-panel>
</v-expansion-panels>
</div>
</template>
<script lang="ts" setup>
@@ -12,16 +52,23 @@ import TSwitchTheme from "@comp/app/t-switchTheme.vue";
import showLoading from "@comp/func/loading.js";
import showSnackbar from "@comp/func/snackbar.js";
import Mys from "@Mys/index.js";
import { onMounted, ref, shallowRef } from "vue";
import JsonViewer from "vue-json-viewer";
import { storeToRefs } from "pinia";
import { computed, onMounted, ref, shallowRef } from "vue";
import VueJsonPretty from "vue-json-pretty";
import "vue-json-pretty/lib/styles.css";
import { useRoute } from "vue-router";
import { useAppStore } from "@/store/modules/app.js";
import { useUserStore } from "@/store/modules/user.js";
import TGLogger from "@/utils/TGLogger.js";
const { theme } = storeToRefs(useAppStore());
const { cookie } = storeToRefs(useUserStore());
const postId = Number(useRoute().params.post_id);
const isEmpty = ref<boolean>(false);
const jsonData = shallowRef<TGApp.Plugins.Mys.Post.FullData>();
const parseData = shallowRef<Array<TGApp.Plugins.Mys.SctPost.Base>>();
const jsonTheme = computed<"dark" | "light">(() => (theme.value === "dark" ? "dark" : "light"));
onMounted(async () => {
await showLoading.start(`正在获取帖子数据`);
@@ -29,7 +76,9 @@ onMounted(async () => {
await showLoading.empty("未获取到PostID");
return;
}
const resp = await Mys.Post.getPostFull(postId);
let ck: Record<string, string> | undefined = undefined;
if (cookie.value) ck = { ltoken: cookie.value.ltoken, ltuid: cookie.value.ltuid };
const resp = await Mys.Post.getPostFull(postId, ck);
if ("retcode" in resp) {
await showLoading.empty("获取数据失败", `[${resp.retcode}]${resp.message}`);
showSnackbar.error(`[${resp.retcode}]${resp.message}`);
@@ -52,29 +101,27 @@ onMounted(async () => {
await showLoading.end();
});
</script>
<style lang="css" scoped>
.post-json {
padding: 20px;
border-radius: 20px;
<style lang="scss" scoped>
.tpj-page {
width: 800px;
margin: 0 auto;
font-family: var(--font-text);
}
.post-title {
width: 100%;
margin: 10px 0;
color: #546d8b;
.tpj-title {
color: var(--common-text-title);
font-family: var(--font-title);
font-size: 20px;
font-weight: 600;
text-align: right;
}
.jv-container {
background: var(--box-bg-2) !important;
}
.jv-key,
.jv-array {
color: var(--box-text-4) !important;
.tpj-box {
border-radius: 4px;
position: relative;
width: 100%;
padding: 12px;
box-sizing: border-box;
max-width: 100%;
background: var(--box-bg-1);
}
</style>

View File

@@ -27,7 +27,12 @@
<v-icon>mdi-comment</v-icon>
<span>{{ postData?.stat?.reply_num }}</span>
</div>
<div class="mpm-item" :title="`点赞数:${postData?.stat?.like_num}`">
<div
class="mpm-item"
:title="`点赞数:${postData?.stat?.like_num}`"
@click="tryLike()"
:class="{ like: isLike }"
>
<v-icon>mdi-thumb-up</v-icon>
<span>{{ postData?.stat?.like_num }}</span>
</div>
@@ -96,24 +101,32 @@ import VpBtnReply from "@comp/viewPost/vp-btn-reply.vue";
import VpOverlayCollection from "@comp/viewPost/vp-overlay-collection.vue";
import Mys from "@Mys/index.js";
import { app, webviewWindow } from "@tauri-apps/api";
import { emit } from "@tauri-apps/api/event";
import { onMounted, onUnmounted, ref, shallowRef } from "vue";
import { emit, listen, UnlistenFn } from "@tauri-apps/api/event";
import { storeToRefs } from "pinia";
import { onBeforeMount, onMounted, onUnmounted, ref, shallowRef } from "vue";
import { useRoute } from "vue-router";
import { useAppStore } from "@/store/modules/app.js";
import { useUserStore } from "@/store/modules/user.js";
import TGBbs from "@/utils/TGBbs.js";
import TGClient from "@/utils/TGClient.js";
import TGLogger from "@/utils/TGLogger.js";
import { createTGWindow } from "@/utils/TGWindow.js";
import apiHubReq from "@/web/request/apiHubReq.js";
const { cookie } = storeToRefs(useUserStore());
const { incognito } = storeToRefs(useAppStore());
const appVersion = ref<string>();
const postId = Number(useRoute().params.post_id);
const showCollection = ref<boolean>(false);
const isLike = ref<boolean>(false);
const shareTime = ref<number>(Math.floor(Date.now() / 1000));
const renderPost = shallowRef<Array<TGApp.Plugins.Mys.SctPost.Base>>([]);
const postData = shallowRef<TGApp.Plugins.Mys.Post.FullData>();
// eslint-disable-next-line no-undef
let shareTimer: NodeJS.Timeout | null = null;
let incognitoListener: UnlistenFn | null = null;
function getGameIcon(gameId: number): string {
const find = TGBbs.channels.find((item) => item.gid === gameId);
@@ -121,6 +134,10 @@ function getGameIcon(gameId: number): string {
return "/platforms/mhy/mys.webp";
}
onBeforeMount(async () => {
incognitoListener = await listen<void>("switchIncognito", () => window.location.reload());
});
onMounted(async () => {
await showLoading.start(`正在加载帖子数据`);
appVersion.value = await app.getVersion();
@@ -131,7 +148,11 @@ onMounted(async () => {
return;
}
await showLoading.update(`帖子ID: ${postId}`);
const resp = await Mys.Post.getPostFull(postId);
let ck: undefined | Record<string, string> = undefined;
if (cookie.value && incognito.value === false) {
ck = { ltoken: cookie.value.ltoken, ltuid: cookie.value.ltuid };
}
const resp = await Mys.Post.getPostFull(postId, ck);
if ("retcode" in resp) {
await showLoading.empty("数据加载失败", `[${resp.retcode}]${resp.message}`);
showSnackbar.error(`[${resp.retcode}] ${resp.message}`);
@@ -140,6 +161,7 @@ onMounted(async () => {
return;
}
postData.value = resp;
isLike.value = postData.value.self_operation.upvote_type !== 0;
await showLoading.update("正在渲染数据");
renderPost.value = await getRenderPost(postData.value);
await webviewWindow
@@ -242,13 +264,36 @@ async function createPostJson(postId: number): Promise<void> {
await createTGWindow(jsonPath, "Dev_JSON", jsonTitle, 960, 720, false, false);
}
function toPost(): void {
const channel = TGBbs.channels.find((item) => item.gid === postData.value?.post.game_id);
if (channel) {
window.open(`https://miyoushe.com/${channel.mini}/#/article/${postId}`);
} else {
window.open(`https://miyoushe.com/ys/#/article/${postId}`);
async function tryLike(): Promise<void> {
if (!cookie.value) {
showSnackbar.error("请先登录");
return;
}
if (!postData.value) {
showSnackbar.error("数据未加载");
return;
}
if (incognito.value) {
showSnackbar.error("无法在无痕浏览模式下操作");
return;
}
const ck = { ltoken: cookie.value.ltoken, ltuid: cookie.value.ltuid };
const resp = await apiHubReq.post.like(postData.value.post.post_id, ck, isLike.value);
if (resp.retcode !== 0) {
showSnackbar.error(`[${resp.retcode}] ${resp.message}`);
return;
}
isLike.value = !isLike.value;
postData.value.stat!.like_num += isLike.value ? 1 : -1;
showSnackbar.success(isLike.value ? "点赞成功" : "取消点赞成功");
}
async function toPost(): Promise<void> {
const channel = TGBbs.channels.find((item) => item.gid === postData.value?.post.game_id);
const link = channel
? `https://m.miyoushe.com/${channel.mini}/#/article/${postId}`
: `https://m.miyoushe.com/ys/#/article/${postId}`;
await TGClient.open("web_thin", link);
}
async function toTopic(topic: TGApp.Plugins.Mys.Topic.Info): Promise<void> {
@@ -265,6 +310,10 @@ onUnmounted(() => {
clearInterval(shareTimer);
shareTimer = null;
}
if (incognitoListener !== null) {
incognitoListener();
incognitoListener = null;
}
});
</script>
<style lang="css" scoped>
@@ -380,6 +429,10 @@ onUnmounted(() => {
margin-left: 10px;
column-gap: 2px;
opacity: 0.8;
&.like {
color: var(--tgc-pink-1);
}
}
/* extra */

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

@@ -13,21 +13,6 @@ declare module "*.vue" {
export default component;
}
/**
* @description vue-json-viewer
* @package vue-json-viewer
* @version 3.0.4
*/
declare module "vue-json-viewer" {
import type { DefineComponent } from "vue";
const component: DefineComponent<{
value: any;
copyable: boolean;
boxed: boolean;
}>;
export default component;
}
declare type ImportMeta = { readonly env: { MODE: string } };
declare interface TauriProcessEnv extends NodeJS.ProcessEnv {

View File

@@ -1,15 +1,14 @@
/**
* @file web/request/apiHubReq.ts
* @description apiHub下的请求
* @since Beta v0.6.8
* @since Beta v0.7.0
*/
import TGHttp from "@/utils/TGHttp.js";
import { getRequestHeader } from "@/web/utils/getRequestHeader.js";
// MysApiHubApiBaseUrl => Mahabu
const Mahabu: Readonly<string> = "https://bbs-api.miyoushe.com/apihub/api/";
// MysApiHubWapiBaseUrl => Mahwbu
const Mahwbu: Readonly<string> = "https://bbs-api.miyoushe.com/apihub/wapi/";
// MysApiHubBaseUrl => Mahbu
const Mahbu: Readonly<string> = "https://bbs-api.miyoushe.com/apihub/";
const Referer: Readonly<string> = "https://bbs.mihoyo.com/";
/**
@@ -19,7 +18,7 @@ const Referer: Readonly<string> = "https://bbs.mihoyo.com/";
*/
async function getAllGamesForums(): Promise<Array<TGApp.BBS.Forum.GameForum>> {
return (
await TGHttp<TGApp.BBS.Forum.GameForumResp>(`${Mahwbu}getAllGamesForums`, {
await TGHttp<TGApp.BBS.Forum.GameForumResp>(`${Mahbu}wapi/getAllGamesForums`, {
method: "GET",
headers: { "Content-Type": "application/json", referer: Referer },
})
@@ -33,13 +32,68 @@ async function getAllGamesForums(): Promise<Array<TGApp.BBS.Forum.GameForum>> {
*/
async function getGameList(): Promise<Array<TGApp.BBS.Game.Item>> {
return (
await TGHttp<TGApp.BBS.Game.ListResp>(`${Mahwbu}getGameList`, {
await TGHttp<TGApp.BBS.Game.ListResp>(`${Mahbu}wapi/getGameList`, {
method: "GET",
headers: { "Content-Type": "application/json", referer: Referer },
})
).data.list;
}
/**
* @description 获取用户米游币任务完成情况
* @since Beta v0.7.0
* @param {Record<string,string>} cookie 用户 Cookie
* @return {Promise<TGApp.BBS.Mission.InfoRes>}
*/
async function getMissions(cookie: Record<string, string>): Promise<TGApp.BBS.Mission.InfoResp> {
const header = getRequestHeader(cookie, "GET", {});
return await TGHttp<TGApp.BBS.Mission.InfoResp>(`${Mahbu}api/getMissions`, {
method: "GET",
headers: header,
});
}
/**
* @description 获取分享配置
* @since Beta v0.7.0
* @description **需要验证码登录返回的 Cookie**
* @param {string} postId 帖子 ID
* @param {Record<string,string>} cookie 用户 Cookie
* @return {Promise<TGApp.BBS.Response.Base>}
*/
async function getShareConf(
postId: string,
cookie: Record<string, string>,
): Promise<TGApp.BBS.Response.Base> {
const params = { entity_id: postId, entity_type: 1 };
const header = {
...getRequestHeader(cookie, "GET", params, "K2", true),
"x-rpc-client_type": "2",
};
return await TGHttp<TGApp.BBS.Response.Base>(`${Mahbu}api/getShareConf`, {
method: "GET",
headers: header,
query: params,
});
}
/**
* @description 获取任务完成情况
* @since Beta v0.7.0
* @description **需要验证码登录的 Cookie**
* @param {Record<string,string>} cookie 用户 Cookie
* @return {Promise<TGApp.BBS.Mission.StateResp>}
*/
async function getUserMissionsState(
cookie: Record<string, string>,
): Promise<TGApp.BBS.Mission.StateResp> {
const header = getRequestHeader(cookie, "GET", {});
return await TGHttp<TGApp.BBS.Mission.StateResp>(`${Mahbu}sapi/getUserMissionsState`, {
method: "GET",
headers: header,
});
}
/**
* @description 获取投票信息
* @since Beta v0.6.2
@@ -49,7 +103,7 @@ async function getGameList(): Promise<Array<TGApp.BBS.Game.Item>> {
*/
async function getVotes(id: string, uid: string): Promise<TGApp.BBS.Vote.Info> {
return (
await TGHttp<TGApp.BBS.Vote.InfoResp>(`${Mahabu}getVotes`, {
await TGHttp<TGApp.BBS.Vote.InfoResp>(`${Mahbu}api/getVotes`, {
method: "GET",
headers: { "Content-Type": "application/json", referer: Referer },
query: { owner_uid: uid, vote_ids: id },
@@ -66,7 +120,7 @@ async function getVotes(id: string, uid: string): Promise<TGApp.BBS.Vote.Info> {
*/
async function getVoteResult(id: string, uid: string): Promise<TGApp.BBS.Vote.Result> {
return (
await TGHttp<TGApp.BBS.Vote.ResultResp>(`${Mahabu}getVotesResult`, {
await TGHttp<TGApp.BBS.Vote.ResultResp>(`${Mahbu}api/getVotesResult`, {
method: "GET",
headers: { "Content-Type": "application/json", referer: Referer },
query: { owner_uid: uid, vote_ids: id },
@@ -82,7 +136,7 @@ async function getVoteResult(id: string, uid: string): Promise<TGApp.BBS.Vote.Re
*/
async function homeNew(gid: number = 2): Promise<TGApp.BBS.Navigator.Navigator[]> {
return (
await TGHttp<TGApp.BBS.Navigator.HomeResponse>(`${Mahabu}home/new`, {
await TGHttp<TGApp.BBS.Navigator.HomeResponse>(`${Mahbu}api/home/new`, {
method: "GET",
headers: { "x-rpc-client_type": "2" },
query: { gids: gid },
@@ -90,11 +144,63 @@ async function homeNew(gid: number = 2): Promise<TGApp.BBS.Navigator.Navigator[]
).data.navigator;
}
/**
* @description 签到
* @since Beta v0.7.0
* @description **需要验证码登录获取的 Cookie**
* @param {Record<string,string>} cookie 用户 Cookie
* @param {string} gid
* @return {Promise<TGApp.BBS.Response.Base>}
*/
async function signIn(
cookie: Record<string, string>,
gid: number = 2,
): Promise<TGApp.BBS.Response.Base> {
const data = { gids: gid };
const header = {
...getRequestHeader(cookie, "POST", JSON.stringify(data), "X6"),
"x-rpc-client_type": "2",
};
return await TGHttp<TGApp.BBS.Response.Base>(`${Mahbu}app/api/signIn`, {
method: "POST",
headers: header,
body: JSON.stringify(data),
});
}
/**
* @description 点赞
* @since Beta v0.7.0
* @param {string} id 帖子 ID
* @param {Record<string,string>} cookie 用户 Cookie
* @param {boolean} cancel 是否取消点赞
* @return {Promise<TGApp.BBS.Response.Base>}
*/
async function upVotePost(
id: string,
cookie: Record<string, string>,
cancel: boolean = false,
): Promise<TGApp.BBS.Response.Base> {
const data = { is_cancel: cancel, post_id: id };
const header = {
...getRequestHeader(cookie, "POST", data, "K2", true),
"x-rpc-client_type": "2",
};
return await TGHttp<TGApp.BBS.Response.Base>(`${Mahbu}api/upvotePost`, {
method: "POST",
headers: header,
body: JSON.stringify(data),
});
}
const apiHubReq = {
vote: { info: getVotes, result: getVoteResult },
home: homeNew,
forum: getAllGamesForums,
game: getGameList,
mission: { list: getMissions, state: getUserMissionsState },
sign: signIn,
post: { like: upVotePost, share: getShareConf },
};
export default apiHubReq;

View File

@@ -1,10 +1,11 @@
/**
* @file web/request/hk4eReq.ts
* @description Hk4eApi 请求模块
* @since Beta v0.6.3
* @since Beta v0.7.0
*/
import TGHttp from "@/utils/TGHttp.js";
import { getDeviceInfo } from "@/utils/toolFunc.js";
export enum AnnoServer {
CN_ISLAND = "cn_gf01",
@@ -20,6 +21,7 @@ export type AnnoLang = "zh-cn" | "zh-tw" | "en" | "ja";
const AnnoApi: Readonly<string> = "https://hk4e-ann-api.mihoyo.com/common/hk4e_cn/announcement/api";
const AnnoApiGlobal: Readonly<string> =
"https://sg-hk4e-api.hoyoverse.com/common/hk4e_global/announcement/api";
const SdkApi: Readonly<string> = "https://hk4e-sdk.mihoyo.com/hk4e_cn/";
/**
* @description 判断是否为国内服务器
@@ -137,9 +139,48 @@ async function getGachaLog(
return resp.data.list;
}
/**
* @description 获取登录二维码
* @since Beta v0.7.0
* @param {string} appId 应用 ID // 目前只有2/7能用
* @returns {Promise<TGApp.Game.Login.QrRes|TGApp.BBS.Response.Base>}
*/
async function fetchPandaQr(
appId: string = "2",
): Promise<TGApp.Game.Login.QrRes | TGApp.BBS.Response.Base> {
const data = { app_id: appId, device: getDeviceInfo("device_id") };
const resp = await TGHttp<TGApp.Game.Login.QrResp>(`${SdkApi}combo/panda/qrcode/fetch`, {
method: "POST",
body: JSON.stringify(data),
});
if (resp.retcode !== 0) return <TGApp.BBS.Response.Base>resp;
return resp.data;
}
/**
* @description 获取登录状态
* @since Beta v0.7.0
* @param {string} ticket 二维码 ticket
* @param {string} appId 应用 ID
* @returns {Promise<TGApp.BBS.Response.Base|TGApp.Game.Login.StatusRes>}
*/
async function queryPandaQr(
ticket: string,
appId: string = "2",
): Promise<TGApp.BBS.Response.Base | TGApp.Game.Login.StatusRes> {
const data = { app_id: appId, ticket, device: getDeviceInfo("device_id") };
const resp = await TGHttp<TGApp.Game.Login.StatusResp>(`${SdkApi}combo/panda/qrcode/query`, {
method: "POST",
body: JSON.stringify(data),
});
if (resp.retcode !== 0) return <TGApp.BBS.Response.Base>resp;
return resp.data;
}
const Hk4eApi = {
anno: { list: getAnnoList, content: getAnnoContent },
gacha: getGachaLog,
loginQr: { create: fetchPandaQr, state: queryPandaQr },
};
export default Hk4eApi;

View File

@@ -1,15 +1,59 @@
/**
* @file web/request/takumiReq.ts
* @description Takumi 相关请求函数
* @since Beta v0.6.3
* @since Beta v0.7.0
*/
import TGBbs from "@/utils/TGBbs.js";
import TGHttp from "@/utils/TGHttp.js";
import { getRequestHeader } from "@/web/utils/getRequestHeader.js";
import { getDeviceInfo } from "@/utils/toolFunc.js";
import { getDS, getRequestHeader } from "@/web/utils/getRequestHeader.js";
// TakumiAuthApiBaseUrl => taAbu
const taAbu: Readonly<string> = "https://api-takumi.mihoyo.com/auth/api/";
// TakumiBingApiBaseUrl => tbAbu
const tbAbu: Readonly<string> = "https://api-takumi.mihoyo.com/binding/api/";
// TakumiApiBaseUrl => taBu
const taBu: Readonly<string> = "https://api-takumi.mihoyo.com/";
/**
* @description 根据gameToken获取stoken
* @todo -100
* @param {TGApp.Game.Login.StatusPayloadRaw} raw 状态数据
* @returns {Promise<TGApp.BBS.Response.Base|string>}
*/
async function getSTokenByGameToken(
raw: TGApp.Game.Login.StatusPayloadRaw,
): Promise<TGApp.BBS.Response.Base> {
const data = { account_id: raw.uid, game_token: raw.token };
// const header = {
// ...getRequestHeader(ck, "POST", JSON.stringify(data), "X6"),
// "x-rpc-client_type": "4",
// "x-rpc-app_id": "bll8iq97cem8",
// "x-rpc-game_biz": "bbs_cn",
// };
const header = {
"x-rpc-app_version": TGBbs.version,
"x-rpc-aigis": "",
"Content-Type": "application/json",
"x-rpc-game_biz": "bbs_cn",
"x-rpc-sys_version": "12",
"x-rpc-device_id": getDeviceInfo("device_id"),
"x-rpc-device_name": getDeviceInfo("device_name"),
"x-rpc-device_model": getDeviceInfo("product"),
"x-rpc-app_id": "bll8iq97cem8",
"x-rpc-client_type": "4",
"User-Agent": "okhttp/4.9.3",
ds: getDS("POST", JSON.stringify(data), "X6", false),
cookie: `account_id=${raw.uid};ltuid=${raw.uid};stuid=${raw.uid};game_token=${raw.token};`,
};
const resp = await TGHttp<TGApp.BBS.Response.Base>(
`${taBu}account/ma-cn-session/app/getTokenByGameToken`,
{
method: "POST",
headers: header,
body: JSON.stringify(data),
},
);
if (resp.retcode !== 0) return <TGApp.BBS.Response.Base>resp;
console.log(resp);
return resp;
}
/**
* @description 根据stoken获取action_ticket
@@ -26,7 +70,7 @@ async function getActionTicketBySToken(
): Promise<ActionTicketByStokenResp> {
const ck = { stoken: cookie.stoken, mid: cookie.mid };
const params = { action_type: actionType, stoken: cookie.stoken, uid: user.gameUid };
return await TGHttp<ActionTicketByStokenResp>(`${taAbu}getActionTicketBySToken`, {
return await TGHttp<ActionTicketByStokenResp>(`${taBu}auth/api/getActionTicketBySToken`, {
method: "GET",
headers: getRequestHeader(ck, "GET", params, "K2"),
query: params,
@@ -52,7 +96,7 @@ async function genAuthKey(
region: account.region,
};
const resp = await TGHttp<TGApp.Game.Gacha.AuthkeyResponse | TGApp.BBS.Response.Base>(
`${tbAbu}genAuthKey`,
`${taBu}binding/api/genAuthKey`,
{
method: "POST",
headers: getRequestHeader(ck, "POST", JSON.stringify(data), "LK2", true),
@@ -74,7 +118,7 @@ async function genAuthKey2(
cookie: Record<string, string>,
payload: Record<string, string>,
): Promise<TGApp.BBS.Response.Base> {
return await TGHttp<TGApp.BBS.Response.Base>(`${tbAbu}genAuthKey`, {
return await TGHttp<TGApp.BBS.Response.Base>(`${taBu}binding/api/genAuthKey`, {
method: "POST",
headers: getRequestHeader(cookie, "POST", JSON.stringify(payload), "LK2", true),
body: JSON.stringify(payload),
@@ -92,7 +136,7 @@ async function getUserGameRolesByCookie(
): Promise<TGApp.BBS.Account.GameAccount[] | TGApp.BBS.Response.Base> {
const ck = { account_id: cookie.account_id, cookie_token: cookie.cookie_token };
const params = { game_biz: "hk4e_cn" };
const resp = await TGHttp<GameAccountsResp>(`${tbAbu}getUserGameRolesByCookie`, {
const resp = await TGHttp<GameAccountsResp>(`${taBu}binding/api/getUserGameRolesByCookie`, {
method: "GET",
headers: getRequestHeader(ck, "GET", params),
query: params,
@@ -104,6 +148,7 @@ async function getUserGameRolesByCookie(
const TakumiApi = {
auth: { actionTicket: getActionTicketBySToken },
bind: { authKey: genAuthKey, authKey2: genAuthKey2, gameRoles: getUserGameRolesByCookie },
game: { stoken: getSTokenByGameToken },
};
export default TakumiApi;

View File

@@ -1,7 +1,7 @@
/**
* @file web/utils/annoParser.ts
* @description 解析游戏内公告数据
* @since Beta v0.6.8
* @since Beta v0.7.0
*/
import TpText from "@comp/viewPost/tp-text.vue";
@@ -9,41 +9,6 @@ import { h, render } from "vue";
import { decodeRegExp } from "@/utils/toolFunc.js";
/**
* @description 预处理p
* @since Beta v0.5.2
* @param {HTMLParagraphElement} p p 元素
* @returns {HTMLParagraphElement} 解析后的 p 元素
*/
function handleAnnoP(p: HTMLParagraphElement): HTMLParagraphElement {
if (p.children.length === 0) p.innerHTML = decodeRegExp(p.innerHTML);
else {
p.querySelectorAll("*").forEach((child) => {
child.innerHTML = decodeRegExp(child.innerHTML);
child.querySelectorAll("span").forEach(handleAnnoSpan);
});
}
return p;
}
/**
* @description 预处理 span
* @since Beta v0.4.4
* @param {HTMLSpanElement} span span 元素
* @returns {HTMLSpanElement} 解析后的 span 元素
*/
function handleAnnoSpan(span: HTMLSpanElement): HTMLSpanElement {
if (span.style.fontSize) span.style.fontSize = "";
if (span.children.length === 0) span.innerHTML = decodeRegExp(span.innerHTML);
else {
span.querySelectorAll("*").forEach((child) => {
if (child.children.length === 0) child.innerHTML = decodeRegExp(child.innerHTML);
if (child.tagName === "T") child.outerHTML = child.innerHTML;
});
}
return span;
}
/**
* @description 预处理table
* @since Beta v0.4.7
@@ -62,14 +27,12 @@ function handleAnnoTable(table: HTMLTableElement): HTMLTableElement {
/**
* @description 预处理公告内容
* @since Beta v0.4.4
* @since Beta v0.7.0
* @param {string} data 游戏内公告数据
* @returns {string} 解析后的数据
*/
function handleAnnoContent(data: string): string {
const htmlBase = new DOMParser().parseFromString(data, "text/html");
htmlBase.querySelectorAll("p").forEach(handleAnnoP);
htmlBase.querySelectorAll("span").forEach(handleAnnoSpan);
const htmlBase = new DOMParser().parseFromString(decodeRegExp(data), "text/html");
htmlBase.querySelectorAll("table").forEach(handleAnnoTable);
return htmlBase.body.innerHTML;
}
@@ -94,7 +57,7 @@ function parseAnnoContent(
/**
* @description 解析公告节点
* @since Beta v0.6.8
* @since Beta v0.7.0
* @param {Node} node - 节点
* @param {Record<string, string>} attr - 属性
* @returns {TGApp.Plugins.Mys.SctPost.Base} 结构化数据
@@ -104,7 +67,12 @@ function parseAnnoNode(
attr?: Record<string, string>,
): Array<TGApp.Plugins.Mys.SctPost.Base> {
let defaultRes: TGApp.Plugins.Mys.SctPost.Base = {
insert: { tag: node.nodeName, text: node.textContent, type: node.nodeType },
insert: {
tag: node.nodeName,
text: node.textContent,
type: node.nodeType,
func: "parseAnnoNode",
},
};
if (node.nodeType !== Node.ELEMENT_NODE && node.nodeType !== Node.TEXT_NODE) return [defaultRes];
if (node.nodeType === Node.TEXT_NODE) {
@@ -118,8 +86,10 @@ function parseAnnoNode(
style: element.style.cssText,
html: element.innerHTML,
outerHtml: element.outerHTML,
func: "parseAnnoNode",
},
};
console.log(defaultRes);
if (element.tagName === "P") {
element.innerHTML = decodeRegExp(element.innerHTML);
return [parseAnnoParagraph(element, attr)];
@@ -172,7 +142,7 @@ function parseAnnoNode(
/**
* @description 解析公告段落
* @since Beta v0.6.8
* @since Beta v0.7.0
* @param {HTMLElement} p - 段落元素
* @param {Record<string, string>} attr - 属性
* @returns {TGApp.Plugins.Mys.SctPost.Base} 结构化数据
@@ -189,8 +159,10 @@ function parseAnnoParagraph(
html: p.innerHTML,
outerHtml: p.outerHTML,
childrenLength: p.childNodes.length,
func: "parseAnnoParagraph",
},
};
console.log(defaultRes);
if (p.childNodes.length === 0) {
return { insert: p.innerHTML === "" ? "\n" : (p.textContent ?? "") };
}
@@ -236,6 +208,7 @@ function parseAnnoParagraph(
if (resS.length > 1) childRes = { insert: element.outerHTML };
else childRes = resS[0];
} else if (element.tagName === "T") {
console.log(element.outerHTML);
element.innerHTML = element.outerHTML;
const resE = parseAnnoNode(element);
if (resE.length > 1) childRes = { insert: element.outerHTML };
@@ -251,7 +224,7 @@ function parseAnnoParagraph(
/**
* @description 解析公告 span
* @since Beta v0.6.8
* @since Beta v0.7.0
* @param {HTMLElement} span - span 元素
* @param {Record<string, string>} attr - 属性
* @returns {TGApp.Plugins.Mys.SctPost.Base} 结构化数据
@@ -264,12 +237,16 @@ function parseAnnoSpan(
insert: {
tag: span.tagName,
text: span.textContent,
style: span.style.cssText,
style: span.style?.cssText,
html: span.innerHTML,
outerHtml: span.outerHTML,
childrenLength: span.childNodes.length,
func: "parseAnnoSpan",
},
};
let spanAttrs: Record<string, string> | undefined = attr;
if (!spanAttrs) spanAttrs = {};
if (span.style.color !== "") spanAttrs.color = span.style.color;
if (span.childNodes.length === 0) {
console.error(span.innerHTML);
return {
@@ -289,9 +266,6 @@ function parseAnnoSpan(
if (res.length > 1) return defaultRes;
return res[0];
}
let spanAttrs: Record<string, string> | undefined = attr;
if (!spanAttrs) spanAttrs = {};
if (span.style.color !== "") spanAttrs.color = span.style.color;
const parse = decodeRegExp(span.innerHTML);
if (parse.includes("</t>")) {
const dom = new DOMParser().parseFromString(parse, "text/html");
@@ -299,12 +273,13 @@ function parseAnnoSpan(
}
return { insert: parse, attributes: spanAttrs };
}
return defaultRes;
// todo 优化处理
return { insert: span.textContent ?? "", attributes: spanAttrs };
}
/**
* @description 解析公告图片
* @since Beta v0.5.3
* @since Beta v0.7.0
* @param {HTMLElement} img - 图片元素
* @returns {TGApp.Plugins.Mys.SctPost.Base} 结构化数据
*/
@@ -317,6 +292,7 @@ function parseAnnoImage(img: HTMLElement): TGApp.Plugins.Mys.SctPost.Base {
style: img.style.cssText,
html: img.innerHTML,
outerHtml: img.outerHTML,
func: "parseAnnoImage",
},
};
}
@@ -326,7 +302,7 @@ function parseAnnoImage(img: HTMLElement): TGApp.Plugins.Mys.SctPost.Base {
/**
* @description 解析公告锚点
* @since Beta v0.6.8
* @since Beta v0.7.0
* @param {HTMLElement} a - 锚点元素
* @returns {TGApp.Plugins.Mys.SctPost.Base} 结构化数据
*/
@@ -339,6 +315,7 @@ function parseAnnoAnchor(a: HTMLElement): TGApp.Plugins.Mys.SctPost.Base {
style: a.style.cssText,
html: a.innerHTML,
outerHtml: a.outerHTML,
func: "parseAnnoAnchor",
},
};
}
@@ -354,7 +331,7 @@ function parseAnnoAnchor(a: HTMLElement): TGApp.Plugins.Mys.SctPost.Base {
/**
* @description 解析公告详情
* @since Beta v0.5.3
* @since Beta v0.7.0
* @param {HTMLElement} details - 详情元素
* @returns {TGApp.Plugins.Mys.SctPost.Base} 结构化数据
*/
@@ -366,6 +343,7 @@ function parseAnnoDetails(details: HTMLElement): TGApp.Plugins.Mys.SctPost.Base
style: details.style.cssText,
html: details.innerHTML,
outerHtml: details.outerHTML,
func: "parseAnnoDetails",
},
};
if (details.tagName !== "DETAILS") return defaultRes;
@@ -388,7 +366,7 @@ function parseAnnoDetails(details: HTMLElement): TGApp.Plugins.Mys.SctPost.Base
/**
* @description 解析公告表格
* @since Beta v0.6.8
* @since Beta v0.7.0
* @param {HTMLElement} table - 表格元素
* @returns {TGApp.Plugins.Mys.SctPost.Base} 结构化数据
*/
@@ -400,6 +378,7 @@ function parseAnnoTable(table: HTMLElement): TGApp.Plugins.Mys.SctPost.Base {
style: table.style.cssText,
html: table.innerHTML,
outerHtml: table.outerHTML,
func: "parseAnnoTable",
},
};
if (table.tagName !== "TABLE") return defaultRes;

View File

@@ -1,7 +1,7 @@
/**
* @file web/utils/getRequestHeader.ts
* @description 获取请求头
* @since Beta v0.6.8
* @since Beta v0.7.0
*/
import Md5 from "js-md5";
@@ -24,12 +24,12 @@ const enum SaltType {
/**
* @description salt 值
* @version 2.80.1
* @since Beta v0.6.7
* @version 2.82.0
* @since Beta v0.7.0
*/
const Salt: Readonly<Record<keyof typeof SaltType, string>> = {
K2: "G1rXOpMLQS77VPWEGycOSxekCTZe2Q8M",
LK2: "sd1e1gQJuvqBfZxas1oeAACXzbim5cge",
K2: "RGcLwIWYOQwTQPJ8Qw41kioel738ch3Z",
LK2: "1M69A7AaPUhTFCdH0D2iMatZ0MTiLmPf",
X4: "xV8v4Qu54lUKrEYFZkJhB8cuOh9Asafs",
X6: "t0qEgfub6cvueAPgR5m9aQWWVciEer7v",
PROD: "t0qEgfub6cvueAPgR5m9aQWWVciEer7v",
@@ -57,7 +57,7 @@ function getRandomNumber(min: number, max: number): number {
* @param {boolean} isSign 是否为签名
* @returns {string} ds
*/
function getDS(
export function getDS(
method: string,
data: string,
saltType: keyof typeof SaltType,

View File

@@ -13,7 +13,7 @@
"esModuleInterop": true,
"lib": ["DOM", "ESNext"],
"skipLibCheck": true,
"types": ["vite/client", "node", "color-convert", "js-md5", "uuid", "src/vite-env"],
"types": ["vite/client", "node", "color-convert", "js-md5", "uuid"],
"allowSyntheticDefaultImports": true,
"composite": true,
"baseUrl": ".",
@@ -30,6 +30,7 @@
"*.yml",
"*.yaml",
"package.json",
"src/vite-env.d.ts",
"src/**/*.d.ts",
"src/**/*.ts",
"src/**/*.vue",