Compare commits

...

53 Commits

Author SHA1 Message Date
目棃
1efab84da0 🚀 v0.6.3 2024-11-19 17:12:02 +08:00
目棃
116d29823e 🍱 更新卡池数据
close #133
2024-11-19 17:04:57 +08:00
目棃
53c045e419 剧诗统计浮窗 2024-11-19 17:00:20 +08:00
目棃
3fef8467f4 ♻️ 调整目录结构 2024-11-19 14:45:29 +08:00
目棃
e1f85d1d92 💄 微调UI 2024-11-19 11:49:48 +08:00
目棃
878eef66fa 🍱 更新部分5.2资源
#133
2024-11-19 11:32:40 +08:00
目棃
be4e2c1039 🐛 修复成就页点击异常,调整UI 2024-11-19 10:44:39 +08:00
目棃
62171b78d9 🍱 更新部分5.2资源
#133
2024-11-19 10:28:37 +08:00
目棃
7932de8654 💄 调整收藏页UI,优化刷新逻辑 2024-11-19 10:04:20 +08:00
目棃
c6f45f0a35 ♻️ 请求模块重构 2024-11-19 09:34:18 +08:00
目棃
9b1fa22cbe 💄 修复file_size不存在导致的hint异常 2024-11-17 22:45:12 +08:00
目棃
63929bc9fd 🎨 调整加载逻辑 2024-11-17 16:39:27 +08:00
目棃
53f2612d32 💄 调整UI 2024-11-17 12:35:15 +08:00
目棃
c17339bab5 🐛 修复路径跳转异常 2024-11-17 11:29:40 +08:00
目棃
8a89883784 🐛 修复路径问题 2024-11-17 11:08:32 +08:00
目棃
4bc1808478 🎨 优化链接跳转,调整UI 2024-11-17 11:01:17 +08:00
目棃
c7b5bf34ef 🎨 优化错误处理 2024-11-17 08:59:15 +08:00
目棃
6b66467cff 💄 调整溢出 2024-11-17 08:43:44 +08:00
目棃
680a54a0f1 🎨 一些优化
* 优化公告解析逻辑
* 咨讯页loading显示版块名称
* 移除无用文件
2024-11-16 19:01:30 +08:00
目棃
2f60e128a5 🎨 调整刷新逻辑 2024-11-16 12:34:53 +08:00
目棃
863246707f 🔥 移除第9层统计数据 2024-11-16 12:29:40 +08:00
目棃
8f0853c41b 🐛 修复topic切换分区不生效 2024-11-16 12:07:08 +08:00
目棃
3166567486 💄 调整 ui 2024-11-16 11:58:43 +08:00
目棃
82f937a33e 🎨 优化结构 2024-11-16 09:13:50 +08:00
目棃
6974ada1c0 🐛 修复帖子loading异常 2024-11-15 18:02:15 +08:00
目棃
0c24b95fff ♻️ 函数式调用替代to-loading 2024-11-15 17:54:14 +08:00
目棃
c87ec77543 loading函数式调用 2024-11-15 16:51:17 +08:00
目棃
36b0d198a9 ♻️ showConfirm重构 2024-11-15 16:26:37 +08:00
目棃
76f8bc3c16 🐛 修复验证码登录-100,刷新后切换登录逻辑调整 2024-11-15 14:28:46 +08:00
目棃
f655b6b235 💄 topic显示在卡片中 2024-11-15 13:54:34 +08:00
目棃
83e0d35245 🐛 修复分类更新无效 2024-11-15 13:41:12 +08:00
目棃
10a5b88d24 🐛 修复分享图元素选取异常 2024-11-15 12:34:37 +08:00
目棃
afe53f3d30 话题跳转 2024-11-15 12:07:55 +08:00
目棃
b8afab093f 📝 修正文档链接 2024-11-15 09:53:48 +08:00
目棃
ac6dbe0cdf ♻️ showSnackbar重构 2024-11-14 18:01:38 +08:00
目棃
20b9631468 🐛 修复调用异常 2024-11-14 17:13:45 +08:00
目棃
857d99361a 👽️ 增加 genshinnet 域名处理 2024-11-13 13:16:04 +08:00
目棃
eaa07601a5 🎨 一些调整 2024-11-12 15:46:11 +08:00
目棃
d88e1d1429 🧪 剧诗表检测 2024-11-11 17:58:20 +08:00
目棃
da4a095618 真境剧诗适配
close #113
2024-11-11 17:51:48 +08:00
目棃
d35b94f79f ✏️ 修正类型 2024-11-11 13:44:02 +08:00
目棃
272f3bc14a 📝 添加UIGF4文档备份 2024-11-11 11:59:47 +08:00
目棃
a2ce468e2f 🌱 重构胡桃请求,增加幻想真境剧诗请求 2024-11-11 11:54:43 +08:00
目棃
6b8cbe0e57 🎨 调整帖子数量 2024-11-08 21:37:20 +08:00
目棃
dafcc30239 💄 修复帖子页兑换码弹窗高度异常 2024-11-08 21:36:14 +08:00
目棃
044059beb0 🔥 移除无用代码 2024-11-08 17:48:28 +08:00
目棃
eea9287cfa 🌱 幻想真境剧诗数据获取 2024-11-08 17:24:54 +08:00
目棃
8020d623e3 支持自定义表情组件渲染 2024-11-08 14:36:20 +08:00
目棃
74ff33e1fb 💄 调整UID卡片样式 2024-11-08 13:53:49 +08:00
目棃
cae2a0a1e6 🔧 调整格式化配置 2024-11-05 14:08:47 +08:00
目棃
45bc398626 新增 UID 卡片类型 2024-11-03 08:55:12 +08:00
目棃
7b5bf201ce 💄 调整UP四星颜色 2024-11-01 13:51:13 +08:00
目棃
8df9370932 🐛 修复数据undefined 2024-10-31 17:47:43 +08:00
326 changed files with 10823 additions and 6979 deletions

View File

@@ -47,7 +47,7 @@ jobs:
- name: setup pnpm
uses: pnpm/action-setup@v2
with:
version: 9.12.3
version: 9.13.2
- name: Install frontend dependencies
run: pnpm install

View File

@@ -16,7 +16,7 @@ jobs:
- name: setup pnpm
uses: pnpm/action-setup@v2
with:
version: 9.12.3
version: 9.13.2
- name: Install dependencies
run: pnpm install --no-frozen-lockfile
- name: "Qodana Scan"

1
.npmrc
View File

@@ -1 +0,0 @@
ELECTRON_MIRROR=https://npm.taobao.org/mirrors/electron/

View File

@@ -16,3 +16,4 @@ qodana.yaml
# data
!src/data/**/*.json
src-tauri/gen/*.json
!eslint

View File

@@ -2,14 +2,39 @@
Author: 目棃
Description: CHANGELOG
Date: 2024-10-09
Update: 2024-10-31
Update: 2024-11-19
---
> 本文档 [`Frontmatter`](https://github.com/BTMuli/MuCli#Frontmatter) 由 [MuCli](https://github.com/BTMuli/Mucli) 自动生成于 `2024-10-09 15:51:43`
>
> 更新于 `2024-10-31 10:36:33`
> 更新于 `2024-11-19 17:06:05`
## [0.6.2](https://github.com/BTMuli/TeyvatGuide/releases/v0.6.2)
## [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)
- 💄 帖子子回复取消保持,点击其他隐藏

View File

@@ -2,12 +2,12 @@
Author: 目棃
Description: 说明文档
Date: 2023-03-05
Update: 2024-10-07
Update: 2024-11-19
---
> 本文档 [`Frontmatter`](https://github.com/BTMuli/MuCli#Frontmatter) 由 [MuCli](https://github.com/BTMuli/Mucli) 自动生成于 `2023-03-05 14:41:55`
>
> 更新于 `2024-10-07 21:51:00`
> 更新于 `2024-11-19 17:06:18`
![](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)
@@ -49,6 +49,7 @@ Game Tool for Genshin Impact player, supports Windows and macOS.
- [x] 游戏内公告&活动获取
- [x] 米游社官方帖获取(支持通过 ID 获取)
- [x] 米游社各分区帖子获取(支持通过 ID 获取)
- [x] 米游社话题帖子获取(通过话题点击跳转)
- [x] 成就管理UIAF v1.1),支持 [`YaeAchievement`](https://github.com/HolographicHat/YaeAchievement) 导入
- [x] 祈愿管理UIGF v3.0UIGF v4.0
- [x] 留影叙佳期画片查看
@@ -60,7 +61,9 @@ Game Tool for Genshin Impact player, supports Windows and macOS.
- [x] 原神战绩数据获取
- [x] 角色详情数据获取
- [x] 螺旋深渊数据获取
- [x] 真境剧诗数据获取
- [x] 祈愿数据获取(近一年)
- [x] 用户收藏帖子获取
- Wiki 功能:
@@ -93,14 +96,14 @@ 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/UIGF.md)
- UIGF[UIGF v3.0](docs/UIGF3.md)[UIGF v4.0](docs/UIGF.md)
- [macOS 平台门禁属性导致应用无法打开应用的修复指引](docs/macos-gatekeeper/README.md)
## 特定项目 / Special Project
- [MuCli](https://github.com/BTMuli/MuCli):基于 NodeJS 的命令行工具,用于生成项目文档。
- [TGAssistant](https://github.com/BTMuli/TGAssistant)Teyvat Guide 的资源获取、解析、处理仓库。
- ~~[WhiteTea](https://github.com/BTMuli/WhiteTea)Github Bot自动化处理 Teyvat Guide 的 Issue 和 Pull Request。~~ 服务已挂T_T
- [WhiteTea](https://github.com/BTMuli/WhiteTea)Github Bot自动化处理 Teyvat Guide 的 Issue 和 Pull Request。
## 技术栈 / Tech Stack
@@ -122,7 +125,7 @@ Game Tool for Genshin Impact player, supports Windows and macOS.
应用版本号遵循 [Semantic Versioning 2.0.0](https://semver.org/lang/zh-CN/) 规范。
隐私政策:[Privacy](https://app.btmuli.ink/docs/privacy.html)
隐私政策:[Privacy](https://app.btmuli.ink/docs/TeyvatGuide/privacy.html)
## 鸣谢 / Thanks

View File

@@ -1,190 +1,371 @@
---
Author: 目棃
Description: UIGF v2.4 Backup
Date: 2023-11-15
Update: 2024-03-13
Description: UIGF v4 Backup
Date: 2024-11-11
Update: 2024-11-11
---
> 本文档 [`Frontmatter`](https://github.com/BTMuli/MuCli#Frontmatter) 由 [MuCli](https://github.com/BTMuli/Mucli) 自动生成于 `2023-11-15 20:58:36`
> 本文档 [`Frontmatter`](https://github.com/BTMuli/MuCli#Frontmatter) 由 [MuCli](https://github.com/BTMuli/Mucli) 自动生成于 `2024-11-11 11:57:27`
>
> 更新于 `2024-03-13 15:50:36`
> 更新于 `2024-11-11 11:57:27`
> 本文档为 [UIGF v3.0](https://github.com/UIGF-org/UIGF-org.github.io/blob/main/docs/zh/standards/UIGF.md) 的备份,仅供参考。
> 本文档为 [UIGF v4.0](https://github.com/UIGF-org/UIGF-org.github.io/blob/main/docs/zh/standards/uigf.md) 的备份,仅供参考。
# 统一可交换抽卡记录标准 v3.0
# 统一可交换抽卡记录标准 v4.0
> Uniformed Interchangeable GachaLog Format standard (UIGF) v3.0 <Badge text="Current" type="message" />
>
> ::: warning UIGF 标准使用声明
> 应用必须在同时支持 UIGF 数据格式**导入**和**导出**功能并在相关功能区域或文档中提供跳转至 [UIGF-Org](https://uigf.org) 的超链接后声明支持 UIGF 格式
> Uniformed Interchangeable GachaLog Format standard (UIGF) v4.0 <Badge text="Current" type="message" />
仅包含导入或导出功能降低了用户数据可流通性,且将数据至于用户不可控的风险中,不符合 UIGF-Org 设计的初衷。
::: warning 中断性更新警告
`UIGF v4.0 及更高版本` 对于 `UIGF v3.0 及更低版本``SRGF v1.0` **不具备向下兼容性**。UIGF/SRGF 合作项目如需适配,需重新认证。
:::
## 更新记录
| 版本 | 说明 | 兼容 |
| ----------------------------- | ---------------------------------------------------------- | -------------- |
| `v2.0` | 首个正式版本 | v2.0 |
| `v2.1` | 简化了部分语言表述,与 v2.0在数据格式上完全一致 | v2.1 and lower |
| [`v2.2`](UIGF-legacy-v2.2.md) | 新增 `info.export_timestamp` 填充 UNIX 时间戳 | v2.2 and lower |
| [`v2.3`](UIGF-legacy-v2.3.md) | 扩充至非中文语境,使用 Json Schema 表述。移除了 Excel 格式 | v2.3 and lower |
| [`v2.4`](UIGF-legacy-v2.4.md) | 新增 `info.region_time_zone` 支持时区处理 | v2.4 and lower |
| `v3.0` | 新增 集录祈愿类型支持 | v3.0 and lower |
| 版本 | 说明 | 兼容 |
| ------ | --------------------------------- | --------------- |
| `v3.0` | 低版本的更新日志请查看历史版本 | v3.0 及更低版本 |
| `v4.0` | 合并 SRGF新增绝区零抽卡格式支持 | v4.0 |
### v3.0 更新内容
## 前言
- `gacha_type` 增加新枚举项
-`gacha_type` 枚举新增值为 `500` 的项,用于表示集录祈愿类型
为了统一不同应用、不同游戏、不同账号间的抽卡记录导入导出行为,我们决定将所有支持的游戏抽卡格式合并入 UIGF 中。不同的游戏、不同的账号将能够以单个文件或字符串的形式进行表示,导入与导出操作对用户而言将变得史无前例的简单。
## `info` 字段说明
## 实现与认证
### `region_time_zone`
实现 `UIGF v4.0 及更高版本`格式的导入导出并不意味着需要移除对 `UIGF v3.0 及更低版本``SRGF v1.0` 的导入导出支持。但是,不建议对 `UIGF v4.0 和更高版本``UIGF v3.0 及更低版本``SRGF v1.0` 使用同一套导入导出逻辑。
由于在获取祈愿记录时得到的`time`为服务器时间,为了准确判断时间的时区偏移,引入此字段
导出方可以选择性地填充针对每个游戏的字段或直接忽略;导入方可以选择性地读取针对每个游戏的字段或直接忽略
与 SRGF 不同,由于无法直接从服务器获取`region_time_zone`,在导出方未提供此字段时,需要根据 `uid` 进行推断
针对对某一款游戏的支持,必须同时实现数据的导入和导出功能,否则将无法通过认证
#### 映射关系
::: info UIGF 标准使用声明
请在应用内提供跳转至 [UIGF-Org](https://uigf.org) 的超链接,声明支持 UIGF 数据格式。
| `uid`首个字符 | `region_time_zone` | 游戏服务器 |
| ------------- | ------------------ | --------------------------------- |
| `'6'` | `-5` | os_usa |
| `'7'` | `1` | os_euro |
| 剩余情况 | `8` | os_cht, os_asia, cn_gf01, cn_qd01 |
App 不应假定 `region_time_zone` 的值为上表中给出的值,应具有处理非标准 `region_time_zone` 值的能力。
`region_time_zone` 的值与 `uid` 推断结果不一致,则优先选择 `region_time_zone` 给出的值。
## `list` 字段说明
### `id`
物品内包含了一项较为特殊的字段: `id`,为原神官方 API 中包含的,代表每条抽卡记录唯一性的 `id`。App 导出 UIGF 时
- 需要确保每个物品都有一个有效的唯一 `id`
- 若有记录中不包含`id`,则应从下一个自带有效 `id` 的物品开始,为每条缺失`id`字段的数据补全`id`
赋值数据向前(时间排序)依次递减,每次递减的值应保持为 `1`
### `gacha_type`
由于存在会共享保底与概率的卡池,所以需要一个额外的字段来界定
我们在 `UIGF` 的所有格式中注入了 `uigf_gacha_type` 字段
在导出到 `UIGF` 格式时需要注意添加对应的 `uigf_gacha_type` 字段
#### 映射关系
| `uigf_gacha_type` | `gacha_type` |
| ----------------- | -------------- |
| `100` | `100` |
| `200` | `200` |
| `301` | `301` or `400` |
| `302` | `302` |
| `500` | `500` |
### `item_id`
物品游戏内ID你可以通过 [UIGF API](../API.md) 获取这一数据
仅包含导入或导出功能降低了用户数据的流通性,且将数据置于用户不可控的风险中,不符合 UIGF-Org 设计的初衷。
:::
## Json Schema
> UIGF-Org 提供[Json Schema](/schema/uigf.json) 用于验证
> UIGF-Org 提供下述 Json Schema 以用于验证资料结构的正确性。
::: warning 注意字段类型
开发者务必遵循 Schema 内定义的字段类型。使用错误的类型可能会导致其他由强类型编程语言编写的工具在解析 Json 文件时产生错误,进而导致数据转移失败。
为了避免这类问题,我们建议您针对 UIGF 格式设计专用的数据结构。同时,设计相关的单元测试以确保导入导出的一致性。
我们也提供 [UIGF 格式校验工具](https://schema.uigf.org/?schema=uigf)来帮助你校验 Json 文件。
:::
```json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"info": {
"type": "object",
"properties": {
"uid": {
"type": "string",
"title": "导出记录的 UID"
},
"lang": {
"type": "string",
"title": "语言 languagecode2-country/regioncode2"
},
"export_timestamp": {
"type": "number",
"title": "导出 UNIX 时间戳(秒)"
},
"export_time": {
"type": "string",
"title": "导出时间",
"description": "yyyy-MM-dd HH:mm:ss"
"oneOf": [
{
"type": "string"
},
{
"type": "integer"
}
],
"description": "导出档案的时间戳,秒级"
},
"export_app": {
"type": "string",
"title": "导出 App 名称"
"description": "导出档案的 App 名称"
},
"export_app_version": {
"type": "string",
"title": "导出 App 版本"
"description": "导出档案的 App 版本"
},
"uigf_version": {
"version": {
"type": "string",
"title": "UIGF 版本号",
"pattern": "v\\d+\\.\\d+"
},
"region_time_zone": {
"type": "number",
"title": "区域时区偏移"
"pattern": "^v\\d+\\.\\d+$",
"description": "导出档案的 UIGF 版本号,格式为 'v{major}.{minor}',如 v4.0"
}
},
"required": ["uid", "uigf_version"],
"title": "UIGF 导出信息"
"required": ["export_timestamp", "export_app", "export_app_version", "version"]
},
"list": {
"hk4e": {
"type": "array",
"items": {
"type": "object",
"properties": {
"uigf_gacha_type": {
"type": "string",
"title": "UIGF 卡池类型",
"description": "用于区分卡池类型不同,但卡池保底计算相同的物品"
"uid": {
"oneOf": [
{
"type": "string"
},
{
"type": "integer"
}
],
"description": "UID"
},
"gacha_type": {
"type": "string",
"title": "卡池类型"
"timezone": {
"type": "integer",
"description": "时区偏移,由米哈游 API 返回,若与服务器时区不同请注意 list 中 time 的转换"
},
"item_id": {
"lang": {
"type": "string",
"title": "物品的内部 ID"
"description": "语言代码",
"enum": [
"de-de",
"en-us",
"es-es",
"fr-fr",
"id-id",
"it-it",
"ja-jp",
"ko-kr",
"pt-pt",
"ru-ru",
"th-th",
"tr-tr",
"vi-vn",
"zh-cn",
"zh-tw"
]
},
"count": {
"type": "string",
"title": "个数一般为1"
},
"time": {
"type": "string",
"title": "获取物品的时间"
},
"name": {
"type": "string",
"title": "物品名称"
},
"item_type": {
"type": "string",
"title": "物品类型"
},
"rank_type": {
"type": "string",
"title": "物品等级"
},
"id": {
"type": "string",
"title": "记录内部 ID"
"list": {
"type": "array",
"items": {
"type": "object",
"properties": {
"uigf_gacha_type": {
"type": "string",
"description": "UIGF 卡池类型,用于区分卡池类型不同,但卡池保底计算相同的物品",
"enum": ["100", "200", "301", "302", "500"]
},
"gacha_type": {
"type": "string",
"description": "卡池类型,米哈游 API 返回",
"enum": ["100", "200", "301", "302", "400", "500"]
},
"item_id": {
"type": "string",
"description": "物品的内部 ID"
},
"count": {
"type": "string",
"description": "物品个数一般为1米哈游 API 返回"
},
"time": {
"type": "string",
"description": "抽取物品时对应时区timezone下的当地时间"
},
"name": {
"type": "string",
"description": "物品名称,米哈游 API 返回"
},
"item_type": {
"type": "string",
"description": "物品类型,米哈游 API 返回"
},
"rank_type": {
"type": "string",
"description": "物品等级,米哈游 API 返回"
},
"id": {
"type": "string",
"description": "记录内部 ID米哈游 API 返回"
}
},
"required": ["uigf_gacha_type", "gacha_type", "item_id", "time", "id"]
}
}
},
"required": ["uigf_gacha_type", "gacha_type", "id", "item_id", "time"],
"title": "UIGF 物品"
},
"title": "物品列表"
"required": ["uid", "timezone", "list"]
}
},
"hkrpg": {
"type": "array",
"items": {
"type": "object",
"properties": {
"uid": {
"oneOf": [
{
"type": "string"
},
{
"type": "integer"
}
],
"description": "UID"
},
"timezone": {
"type": "integer",
"description": "时区偏移,由米哈游 API 返回,若与服务器时区不同请注意 list 中 time 的转换"
},
"lang": {
"type": "string",
"description": "语言代码",
"enum": [
"de-de",
"en-us",
"es-es",
"fr-fr",
"id-id",
"it-it",
"ja-jp",
"ko-kr",
"pt-pt",
"ru-ru",
"th-th",
"tr-tr",
"vi-vn",
"zh-cn",
"zh-tw"
]
},
"list": {
"type": "array",
"items": {
"type": "object",
"properties": {
"gacha_id": {
"type": "string",
"description": "卡池 Id"
},
"gacha_type": {
"type": "string",
"description": "卡池类型",
"enum": ["1", "2", "11", "12"]
},
"item_id": {
"type": "string",
"description": "物品的内部 ID"
},
"count": {
"type": "string",
"description": "物品个数一般为1米哈游 API 返回"
},
"time": {
"type": "string",
"description": "抽取物品时对应时区timezone下的当地时间"
},
"name": {
"type": "string",
"description": "物品名称,米哈游 API 返回"
},
"item_type": {
"type": "string",
"description": "物品类型,米哈游 API 返回"
},
"rank_type": {
"type": "string",
"description": "物品等级,米哈游 API 返回"
},
"id": {
"type": "string",
"description": "记录内部 ID米哈游 API 返回"
}
},
"required": ["gacha_type", "gacha_id", "time", "item_id", "id"]
}
}
},
"required": ["uid", "timezone", "list"]
}
},
"nap": {
"type": "array",
"items": {
"type": "object",
"properties": {
"uid": {
"oneOf": [
{
"type": "string"
},
{
"type": "integer"
}
],
"description": "UID"
},
"timezone": {
"type": "integer",
"description": "时区偏移,由米哈游 API 返回,若与服务器时区不同请注意 list 中 time 的转换"
},
"lang": {
"type": "string",
"description": "语言代码",
"enum": [
"de-de",
"en-us",
"es-es",
"fr-fr",
"id-id",
"it-it",
"ja-jp",
"ko-kr",
"pt-pt",
"ru-ru",
"th-th",
"tr-tr",
"vi-vn",
"zh-cn",
"zh-tw"
]
},
"list": {
"type": "array",
"items": {
"type": "object",
"properties": {
"gacha_id": {
"type": "string",
"description": "卡池 Id"
},
"gacha_type": {
"type": "string",
"description": "卡池类型",
"enum": ["1", "2", "3", "5"]
},
"item_id": {
"type": "string",
"description": "物品的内部 ID"
},
"count": {
"type": "string",
"description": "物品个数一般为1米哈游 API 返回"
},
"time": {
"type": "string",
"description": "抽取物品时对应时区timezone下的当地时间"
},
"name": {
"type": "string",
"description": "物品名称,米哈游 API 返回"
},
"item_type": {
"type": "string",
"description": "物品类型,米哈游 API 返回"
},
"rank_type": {
"type": "string",
"description": "物品等级,米哈游 API 返回"
},
"id": {
"type": "string",
"description": "记录内部 ID米哈游 API 返回"
}
},
"required": ["gacha_type", "item_id", "time", "id"]
}
}
},
"required": ["uid", "timezone", "list"]
}
}
},
"required": ["info", "list"],
"title": "UIGF 根对象"
"required": ["info"]
}
```

190
docs/UIGF3.md Normal file
View File

@@ -0,0 +1,190 @@
---
Author: 目棃
Description: UIGF v3 Backup
Date: 2023-11-15
Update: 2024-11-11
---
> 本文档 [`Frontmatter`](https://github.com/BTMuli/MuCli#Frontmatter) 由 [MuCli](https://github.com/BTMuli/Mucli) 自动生成于 `2023-11-15 20:58:36`
>
> 更新于 `2024-11-11 11:56:11`
> 本文档为 [UIGF v3.0](https://github.com/UIGF-org/UIGF-org.github.io/blob/main/docs/zh/standards/uigf-legacy-v3.0.md) 的备份,仅供参考。
# 统一可交换抽卡记录标准 v3.0
> Uniformed Interchangeable GachaLog Format standard (UIGF) v3.0 <Badge text="Current" type="message" />
>
> ::: warning UIGF 标准使用声明
> 应用必须在同时支持 UIGF 数据格式**导入**和**导出**功能并在相关功能区域或文档中提供跳转至 [UIGF-Org](https://uigf.org) 的超链接后声明支持 UIGF 格式
仅包含导入或导出功能降低了用户数据可流通性,且将数据至于用户不可控的风险中,不符合 UIGF-Org 设计的初衷。
:::
## 更新记录
| 版本 | 说明 | 兼容 |
| ----------------------------- | ---------------------------------------------------------- | -------------- |
| `v2.0` | 首个正式版本 | v2.0 |
| `v2.1` | 简化了部分语言表述,与 v2.0在数据格式上完全一致 | v2.1 and lower |
| [`v2.2`](UIGF-legacy-v2.2.md) | 新增 `info.export_timestamp` 填充 UNIX 时间戳 | v2.2 and lower |
| [`v2.3`](UIGF-legacy-v2.3.md) | 扩充至非中文语境,使用 Json Schema 表述。移除了 Excel 格式 | v2.3 and lower |
| [`v2.4`](UIGF-legacy-v2.4.md) | 新增 `info.region_time_zone` 支持时区处理 | v2.4 and lower |
| `v3.0` | 新增 集录祈愿类型支持 | v3.0 and lower |
### v3.0 更新内容
- `gacha_type` 增加新枚举项
-`gacha_type` 枚举新增值为 `500` 的项,用于表示集录祈愿类型
## `info` 字段说明
### `region_time_zone`
由于在获取祈愿记录时得到的`time`为服务器时间,为了准确判断时间的时区偏移,引入此字段。
与 SRGF 不同,由于无法直接从服务器获取`region_time_zone`,在导出方未提供此字段时,需要根据 `uid` 进行推断。
#### 映射关系
| `uid`首个字符 | `region_time_zone` | 游戏服务器 |
| ------------- | ------------------ | --------------------------------- |
| `'6'` | `-5` | os_usa |
| `'7'` | `1` | os_euro |
| 剩余情况 | `8` | os_cht, os_asia, cn_gf01, cn_qd01 |
App 不应假定 `region_time_zone` 的值为上表中给出的值,应具有处理非标准 `region_time_zone` 值的能力。
`region_time_zone` 的值与 `uid` 推断结果不一致,则优先选择 `region_time_zone` 给出的值。
## `list` 字段说明
### `id`
物品内包含了一项较为特殊的字段: `id`,为原神官方 API 中包含的,代表每条抽卡记录唯一性的 `id`。App 导出 UIGF 时
- 需要确保每个物品都有一个有效的唯一 `id`
- 若有记录中不包含`id`,则应从下一个自带有效 `id` 的物品开始,为每条缺失`id`字段的数据补全`id`
赋值数据向前(时间排序)依次递减,每次递减的值应保持为 `1`
### `gacha_type`
由于存在会共享保底与概率的卡池,所以需要一个额外的字段来界定
我们在 `UIGF` 的所有格式中注入了 `uigf_gacha_type` 字段
在导出到 `UIGF` 格式时需要注意添加对应的 `uigf_gacha_type` 字段
#### 映射关系
| `uigf_gacha_type` | `gacha_type` |
| ----------------- | -------------- |
| `100` | `100` |
| `200` | `200` |
| `301` | `301` or `400` |
| `302` | `302` |
| `500` | `500` |
### `item_id`
物品游戏内ID你可以通过 [UIGF API](../API.md) 获取这一数据
## Json Schema
> UIGF-Org 提供[Json Schema](/schema/uigf.json) 用于验证
```json
{
"type": "object",
"properties": {
"info": {
"type": "object",
"properties": {
"uid": {
"type": "string",
"title": "导出记录的 UID"
},
"lang": {
"type": "string",
"title": "语言 languagecode2-country/regioncode2"
},
"export_timestamp": {
"type": "number",
"title": "导出 UNIX 时间戳(秒)"
},
"export_time": {
"type": "string",
"title": "导出时间",
"description": "yyyy-MM-dd HH:mm:ss"
},
"export_app": {
"type": "string",
"title": "导出 App 名称"
},
"export_app_version": {
"type": "string",
"title": "导出 App 版本"
},
"uigf_version": {
"type": "string",
"title": "UIGF 版本号",
"pattern": "v\\d+\\.\\d+"
},
"region_time_zone": {
"type": "number",
"title": "区域时区偏移"
}
},
"required": ["uid", "uigf_version"],
"title": "UIGF 导出信息"
},
"list": {
"type": "array",
"items": {
"type": "object",
"properties": {
"uigf_gacha_type": {
"type": "string",
"title": "UIGF 卡池类型",
"description": "用于区分卡池类型不同,但卡池保底计算相同的物品"
},
"gacha_type": {
"type": "string",
"title": "卡池类型"
},
"item_id": {
"type": "string",
"title": "物品的内部 ID"
},
"count": {
"type": "string",
"title": "个数一般为1"
},
"time": {
"type": "string",
"title": "获取物品的时间"
},
"name": {
"type": "string",
"title": "物品名称"
},
"item_type": {
"type": "string",
"title": "物品类型"
},
"rank_type": {
"type": "string",
"title": "物品等级"
},
"id": {
"type": "string",
"title": "记录内部 ID"
}
},
"required": ["uigf_gacha_type", "gacha_type", "id", "item_id", "time"],
"title": "UIGF 物品"
},
"title": "物品列表"
}
},
"required": ["info", "list"],
"title": "UIGF 根对象"
}
```

View File

@@ -1,11 +1,20 @@
import eslint_jsonc from "eslint-plugin-jsonc";
import eslint_js from "@eslint/js";
import eslint_ts from "typescript-eslint";
import eslint_vue from "eslint-plugin-vue";
import { jsonEslintConfig } from "./eslint/jsonEslint.js";
import { vueEslintConfig } from "./eslint/vueEslint.js";
import ymlEslintConfig from "./eslint/ymlEslint.js";
export default [
eslint_js.configs.recommended,
...eslint_jsonc.configs["flat/recommended-with-jsonc"],
...eslint_ts.configs.recommended,
...eslint_vue.configs["flat/essential"],
...jsonEslintConfig,
ymlEslintConfig,
...vueEslintConfig,
ymlEslintConfig,
{
ignores: [
"dist",
@@ -13,7 +22,7 @@ export default [
"pnpm-lock.yaml",
"src/data/**/*.json",
"src-tauri/tauri.conf.json",
"src-tauri/gen/*.json",
"src-tauri/**/*.json",
"qodana.yaml",
".github",
".vscode",

View File

@@ -1,14 +1,10 @@
import eslint_jsonc from "eslint-plugin-jsonc";
import jsonc_parser from "jsonc-eslint-parser";
import pluginJsonc from "eslint-plugin-jsonc";
import parserJsonc from "jsonc-eslint-parser";
const pkgJsonConfig = {
files: ["package.json"],
plugins: {
jsonc: eslint_jsonc,
},
languageOptions: {
parser: jsonc_parser,
},
plugins: { jsonc: pluginJsonc },
languageOptions: { parser: parserJsonc },
rules: {
"jsonc/comma-dangle": ["error", "never"],
"jsonc/sort-keys": [
@@ -38,12 +34,8 @@ const pkgJsonConfig = {
const tscJsonConfig = {
files: ["tsconfig.json"],
plugins: {
jsonc: eslint_jsonc,
},
languageOptions: {
parser: jsonc_parser,
},
plugins: { jsonc: pluginJsonc },
languageOptions: { parser: parserJsonc },
rules: {
"jsonc/comma-dangle": ["error", "never"],
"jsonc/sort-keys": [
@@ -66,29 +58,12 @@ const tscJsonConfig = {
const jsoncConfig = {
files: ["source/data/out/**/*.json", ".vscode/**/*.json"],
plugins: {
jsonc: eslint_jsonc,
},
languageOptions: {
parser: jsonc_parser,
},
plugins: { jsonc: pluginJsonc },
languageOptions: { parser: parserJsonc },
rules: {
"jsonc/comma-dangle": ["error", "never"],
"jsonc/sort-keys": [
"error",
{
pathPattern: "^$",
order: {
type: "asc",
},
},
],
"jsonc/sort-keys": ["error", { pathPattern: "^$", order: { type: "asc" } }],
},
};
export const jsonEslintConfig = [
...eslint_jsonc.configs["flat/recommended-with-json"],
pkgJsonConfig,
tscJsonConfig,
jsoncConfig,
];
export const jsonEslintConfig = [pkgJsonConfig, tscJsonConfig, jsoncConfig];

View File

@@ -1,66 +1,42 @@
import eslint_js from "@eslint/js";
import eslint_import from "eslint-plugin-import";
import eslint_prettier from "eslint-plugin-prettier";
import eslint_vue from "eslint-plugin-vue";
import pluginImport from "eslint-plugin-import";
import pluginPrettier from "eslint-plugin-prettier";
import pluginVue from "eslint-plugin-vue";
import globals from "globals";
import eslint_ts from "typescript-eslint";
import vue_parser from "vue-eslint-parser";
const tsConfigRules = {
"@typescript-eslint/consistent-type-assertions": [
"error",
{
assertionStyle: "angle-bracket",
},
],
"@typescript-eslint/consistent-type-assertions": ["error", { assertionStyle: "angle-bracket" }],
"@typescript-eslint/no-import-type-side-effects": "error",
"@typescript-eslint/strict-boolean-expressions": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-unused-expressions": ["error", { allowShortCircuit: false }],
"import/order": [
"error",
{
groups: ["builtin", "external", "internal", "parent", "sibling", "index", "unknown"],
"newlines-between": "always",
alphabetize: {
order: "asc",
caseInsensitive: true,
},
alphabetize: { order: "asc", caseInsensitive: true },
},
],
"prettier/prettier": "error",
};
const tsConfig = {
files: ["*.ts"],
plugins: {
typescript: eslint_ts,
import: eslint_import,
prettier: eslint_prettier,
},
files: ["*.ts", "*.d.ts", "src/**/*.ts", "src/**/*.d.ts"],
plugins: { typescript: eslint_ts, import: pluginImport, prettier: pluginPrettier },
languageOptions: {
parser: eslint_ts.parser,
parserOptions: {
project: "tsconfig.json",
tsconfigRootDir: ".",
},
parserOptions: { project: "tsconfig.json", tsconfigRootDir: "." },
},
rules: tsConfigRules,
};
const vueConfig = {
plugins: {
vue: eslint_vue,
import: eslint_import,
prettier: eslint_prettier,
},
files: ["src/**/*.vue"],
plugins: { vue: pluginVue, import: pluginImport, prettier: pluginPrettier },
languageOptions: {
globals: {
...globals.browser,
...globals.es2021,
TGApp: "readonly",
window: "readonly",
},
globals: { ...globals.browser, ...globals.es2021, TGApp: "readonly", window: "readonly" },
ecmaVersion: "latest",
sourceType: "module",
parser: vue_parser,
@@ -71,17 +47,7 @@ const vueConfig = {
tsconfigRootDir: ".",
},
},
rules: {
...tsConfigRules,
"vue/multi-word-component-names": "off",
},
rules: { ...tsConfigRules, "vue/multi-word-component-names": "off" },
};
export const vueEslintConfig = [
eslint_js.configs.recommended,
...eslint_ts.configs.recommended,
...eslint_vue.configs["flat/essential"],
...pluginVue.configs["flat/essential"],
tsConfig,
vueConfig,
];
export const vueEslintConfig = [tsConfig, vueConfig];

View File

@@ -1,28 +1,17 @@
import eslint_yml from "eslint-plugin-yml";
import yml_parser from "yaml-eslint-parser";
import pluginYml from "eslint-plugin-yml";
import parserYml from "yaml-eslint-parser";
const ymlEslintConfig = {
files: ["**/*.yml", "**/*.yaml"],
plugins: {
yml: eslint_yml,
},
plugins: { yml: pluginYml },
languageOptions: {
parser: yml_parser,
parserOptions: {
defaultYAMLVersion: "1.2",
extraFileExtensions: [".yaml", ".yml"],
},
parser: parserYml,
parserOptions: { defaultYAMLVersion: "1.2", extraFileExtensions: [".yaml", ".yml"] },
},
rules: {
"yml/indent": ["error", 2],
"yml/key-spacing": ["error"],
"yml/quotes": [
"error",
{
prefer: "double",
avoidEscape: true,
},
],
"yml/quotes": ["error", { prefer: "double", avoidEscape: true }],
"yml/sort-keys": ["error", "asc"],
},
};

View File

@@ -1,6 +1,6 @@
{
"name": "TeyvatGuide",
"version": "0.6.2",
"name": "teyvatguide",
"version": "0.6.3",
"description": "Game Tool for GenshinImpact player",
"private": true,
"type": "module",
@@ -66,61 +66,57 @@
},
"dependencies": {
"@mdi/font": "7.4.47",
"@tauri-apps/api": "^2.0.3",
"@tauri-apps/api": "^2.1.1",
"@tauri-apps/plugin-deep-link": "^2.0.0",
"@tauri-apps/plugin-dialog": "^2.0.1",
"@tauri-apps/plugin-fs": "^2.0.1",
"@tauri-apps/plugin-fs": "^2.0.2",
"@tauri-apps/plugin-http": "^2.0.1",
"@tauri-apps/plugin-log": "^2.0.0",
"@tauri-apps/plugin-os": "^2.0.0",
"@tauri-apps/plugin-process": "^2.0.0",
"@tauri-apps/plugin-shell": "^2.0.1",
"@tauri-apps/plugin-sql": "^2.0.0",
"@tauri-apps/plugin-sql": "^2.0.1",
"ajv": "^8.17.1",
"artplayer": "^5.2.0",
"artplayer": "^5.2.1",
"clipboard": "^2.0.11",
"color-convert": "^2.0.1",
"echarts": "^5.5.1",
"html2canvas": "^1.4.1",
"js-md5": "^0.8.3",
"jsencrypt": "^3.3.2",
"pinia": "^2.2.4",
"pinia-plugin-persistedstate": "^4.1.1",
"uuid": "^10.0.0",
"vue": "^3.5.12",
"pinia": "^2.2.6",
"pinia-plugin-persistedstate": "^4.1.3",
"uuid": "^11.0.3",
"vue": "^3.5.13",
"vue-echarts": "^7.0.3",
"vue-json-viewer": "^3.0.4",
"vue-router": "^4.4.5",
"vuetify": "^3.7.3",
"vuetify": "^3.7.4",
"wcag-color": "^1.1.1",
"xml-js": "^1.6.11"
},
"devDependencies": {
"@eslint/eslintrc": "^3.1.0",
"@eslint/js": "^9.13.0",
"@tauri-apps/cli": "2.0.4",
"@eslint/eslintrc": "^3.2.0",
"@eslint/js": "^9.15.0",
"@tauri-apps/cli": "2.1.0",
"@types/color-convert": "^2.0.4",
"@types/js-md5": "^0.7.2",
"@types/node": "^22.7.9",
"@types/node": "^22.9.0",
"@types/uuid": "^10.0.0",
"@typescript-eslint/parser": "^8.11.0",
"@vitejs/plugin-vue": "^5.1.4",
"concurrently": "^9.0.1",
"eslint": "^9.13.0",
"eslint-config-love": "^89.0.1",
"eslint-config-prettier": "^9.1.0",
"@typescript-eslint/parser": "^8.14.0",
"@vitejs/plugin-vue": "^5.2.0",
"concurrently": "^9.1.0",
"eslint": "^9.15.0",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-jsonc": "^2.16.0",
"eslint-plugin-n": "^17.11.1",
"eslint-plugin-jsonc": "^2.18.1",
"eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-promise": "^7.1.0",
"eslint-plugin-vue": "^9.29.1",
"eslint-plugin-yml": "^1.14.0",
"globals": "^15.11.0",
"eslint-plugin-vue": "^9.31.0",
"eslint-plugin-yml": "^1.15.0",
"globals": "^15.12.0",
"husky": "^9.1.6",
"jsonc-eslint-parser": "^2.4.0",
"lint-staged": "^15.2.10",
"oxlint": "^0.10.2",
"oxlint": "^0.11.1",
"prettier": "3.3.3",
"stylelint": "^16.10.0",
"stylelint-config-idiomatic-order": "^10.0.0",
@@ -130,9 +126,9 @@
"stylelint-order": "^6.0.4",
"stylelint-prettier": "^5.0.2",
"typescript": "^5.6.3",
"typescript-eslint": "^8.11.0",
"vite": "^5.4.10",
"vite-plugin-vue-devtools": "^7.5.3",
"typescript-eslint": "^8.14.0",
"vite": "^5.4.11",
"vite-plugin-vue-devtools": "^7.6.4",
"vite-plugin-vuetify": "^2.0.4",
"vue-eslint-parser": "^9.4.3",
"yaml-eslint-parser": "^1.2.3"

1153
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

1011
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.2"
version = "0.6.3"
description = "Game Tool for Genshin Impact player"
authors = ["BTMuli <bt-muli@outlook.com>"]
license = "MIT"
@@ -10,16 +10,16 @@ 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.0", features = [] }
tauri-build = { version = "2.0.2", features = [] }
[dependencies]
chrono = "0.4.38"
log = "0.4.22"
serde = { version = "1.0.213", features = ["derive"] }
serde = { version = "1.0.214", features = ["derive"] }
serde_json = "1.0.132"
tauri = { version = "2.0.6", features = [] }
tauri-utils = "2.0.2"
url = "2.5.2"
tauri = { version = "2.1.1", features = [] }
tauri-utils = "2.1.0"
url = "2.5.3"
walkdir = "2.5.0"
# deep link 插件

View File

@@ -78,12 +78,15 @@
},
{
"url": "https://*.mihoyo.com/*"
},
{
"url": "https://*.genshinnet.com/*"
}
]
}
],
"remote": {
"urls": ["https://*.mihoyo.com/*", "https://*.miyoushe.com/*"]
"urls": ["https://*.mihoyo.com/*", "https://*.miyoushe.com/*", "https://*.genshinnet.com/*"]
},
"platforms": ["windows", "macOS"]
}

View File

@@ -78,6 +78,9 @@
},
{
"url": "https://*.hoyoverse.com/*"
},
{
"url": "https://*.genshinnet.com/*"
}
]
}

View File

@@ -1744,7 +1744,7 @@
"fs": {
"default_permission": {
"identifier": "default",
"description": "This set of permissions describes the what kind of\nfile system access the `fs` plugin has enabled or denied by default.\n\n#### Granted Permissions\n\nThis default permission set enables read access to the\napplication specific directories (AppConfig, AppData, AppLocalData, AppCache,\nAppLog) and all files and sub directories created in it.\nThe location of these directories depends on the operating system,\nwhere the application is run.\n\nIn general these directories need to be manually created\nby the application at runtime, before accessing files or folders\nin it is possible.\n\nTherefore, it is also allowed to create all of these folders via\nthe `mkdir` command.\n\n#### Denied Permissions\n\nThis default permission set prevents access to critical components\nof the Tauri application by default.\nOn Windows the webview data folder access is denied.\n\n",
"description": "This set of permissions describes the what kind of\nfile system access the `fs` plugin has enabled or denied by default.\n\n#### Granted Permissions\n\nThis default permission set enables read access to the\napplication specific directories (AppConfig, AppData, AppLocalData, AppCache,\nAppLog) and all files and sub directories created in it.\nThe location of these directories depends on the operating system,\nwhere the application is run.\n\nIn general these directories need to be manually created\nby the application at runtime, before accessing files or folders\nin it is possible.\n\nTherefore, it is also allowed to create all of these folders via\nthe `mkdir` command.\n\n#### Denied Permissions\n\nThis default permission set prevents access to critical components\nof the Tauri application by default.\nOn Windows the webview data folder access is denied.\n\n#### Included permissions within this default permission set:\n",
"permissions": [
"create-app-specific-dirs",
"read-app-specific-dirs-recursive",

View File

@@ -41,7 +41,9 @@
"Mys": {
"identifier": "Mys",
"description": "Capability for the mys client window",
"remote": { "urls": ["https://*.mihoyo.com/*", "https://*.miyoushe.com/*"] },
"remote": {
"urls": ["https://*.mihoyo.com/*", "https://*.miyoushe.com/*", "https://*.genshinnet.com/*"]
},
"local": true,
"windows": ["mhy_client"],
"permissions": [
@@ -64,7 +66,11 @@
{ "identifier": "fs:allow-write-text-file", "allow": [{ "path": "**" }] },
{
"identifier": "http:default",
"allow": [{ "url": "https://*.miyoushe.com/*" }, { "url": "https://*.mihoyo.com/*" }]
"allow": [
{ "url": "https://*.miyoushe.com/*" },
{ "url": "https://*.mihoyo.com/*" },
{ "url": "https://*.genshinnet.com/*" }
]
}
],
"platforms": ["windows", "macOS"]
@@ -118,7 +124,8 @@
{ "url": "https://*.mihoyo.com/*" },
{ "url": "https://*.mihoyogift.com/*" },
{ "url": "https://*.bilibili.com/*" },
{ "url": "https://*.hoyoverse.com/*" }
{ "url": "https://*.hoyoverse.com/*" },
{ "url": "https://*.genshinnet.com/*" }
]
}
],

View File

@@ -79,7 +79,7 @@
}
},
"permissions": {
"description": "List of permissions attached to this capability.\n\nMust include the plugin name as prefix in the form of `${plugin-name}:${permission-name}`. For commands directly implemented in the application itself only `${permission-name}` is required.\n\n## Example\n\n```json [ \"core:default\", \"shell:allow-open\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] } ```",
"description": "List of permissions attached to this capability.\n\nMust include the plugin name as prefix in the form of `${plugin-name}:${permission-name}`. For commands directly implemented in the application itself only `${permission-name}` is required.\n\n## Example\n\n```json [ \"core:default\", \"shell:allow-open\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] } ] ```",
"type": "array",
"items": {
"$ref": "#/definitions/PermissionEntry"
@@ -130,7 +130,7 @@
"identifier": {
"anyOf": [
{
"description": "This set of permissions describes the what kind of\nfile system access the `fs` plugin has enabled or denied by default.\n\n#### Granted Permissions\n\nThis default permission set enables read access to the\napplication specific directories (AppConfig, AppData, AppLocalData, AppCache,\nAppLog) and all files and sub directories created in it.\nThe location of these directories depends on the operating system,\nwhere the application is run.\n\nIn general these directories need to be manually created\nby the application at runtime, before accessing files or folders\nin it is possible.\n\nTherefore, it is also allowed to create all of these folders via\nthe `mkdir` command.\n\n#### Denied Permissions\n\nThis default permission set prevents access to critical components\nof the Tauri application by default.\nOn Windows the webview data folder access is denied.\n\n",
"description": "This set of permissions describes the what kind of\nfile system access the `fs` plugin has enabled or denied by default.\n\n#### Granted Permissions\n\nThis default permission set enables read access to the\napplication specific directories (AppConfig, AppData, AppLocalData, AppCache,\nAppLog) and all files and sub directories created in it.\nThe location of these directories depends on the operating system,\nwhere the application is run.\n\nIn general these directories need to be manually created\nby the application at runtime, before accessing files or folders\nin it is possible.\n\nTherefore, it is also allowed to create all of these folders via\nthe `mkdir` command.\n\n#### Denied Permissions\n\nThis default permission set prevents access to critical components\nof the Tauri application by default.\nOn Windows the webview data folder access is denied.\n\n#### Included permissions within this default permission set:\n",
"type": "string",
"const": "fs:default"
},
@@ -3537,7 +3537,7 @@
"const": "dialog:deny-save"
},
{
"description": "This set of permissions describes the what kind of\nfile system access the `fs` plugin has enabled or denied by default.\n\n#### Granted Permissions\n\nThis default permission set enables read access to the\napplication specific directories (AppConfig, AppData, AppLocalData, AppCache,\nAppLog) and all files and sub directories created in it.\nThe location of these directories depends on the operating system,\nwhere the application is run.\n\nIn general these directories need to be manually created\nby the application at runtime, before accessing files or folders\nin it is possible.\n\nTherefore, it is also allowed to create all of these folders via\nthe `mkdir` command.\n\n#### Denied Permissions\n\nThis default permission set prevents access to critical components\nof the Tauri application by default.\nOn Windows the webview data folder access is denied.\n\n",
"description": "This set of permissions describes the what kind of\nfile system access the `fs` plugin has enabled or denied by default.\n\n#### Granted Permissions\n\nThis default permission set enables read access to the\napplication specific directories (AppConfig, AppData, AppLocalData, AppCache,\nAppLog) and all files and sub directories created in it.\nThe location of these directories depends on the operating system,\nwhere the application is run.\n\nIn general these directories need to be manually created\nby the application at runtime, before accessing files or folders\nin it is possible.\n\nTherefore, it is also allowed to create all of these folders via\nthe `mkdir` command.\n\n#### Denied Permissions\n\nThis default permission set prevents access to critical components\nof the Tauri application by default.\nOn Windows the webview data folder access is denied.\n\n#### Included permissions within this default permission set:\n",
"type": "string",
"const": "fs:default"
},

View File

@@ -79,7 +79,7 @@
}
},
"permissions": {
"description": "List of permissions attached to this capability.\n\nMust include the plugin name as prefix in the form of `${plugin-name}:${permission-name}`. For commands directly implemented in the application itself only `${permission-name}` is required.\n\n## Example\n\n```json [ \"core:default\", \"shell:allow-open\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] } ```",
"description": "List of permissions attached to this capability.\n\nMust include the plugin name as prefix in the form of `${plugin-name}:${permission-name}`. For commands directly implemented in the application itself only `${permission-name}` is required.\n\n## Example\n\n```json [ \"core:default\", \"shell:allow-open\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] } ] ```",
"type": "array",
"items": {
"$ref": "#/definitions/PermissionEntry"
@@ -130,7 +130,7 @@
"identifier": {
"anyOf": [
{
"description": "This set of permissions describes the what kind of\nfile system access the `fs` plugin has enabled or denied by default.\n\n#### Granted Permissions\n\nThis default permission set enables read access to the\napplication specific directories (AppConfig, AppData, AppLocalData, AppCache,\nAppLog) and all files and sub directories created in it.\nThe location of these directories depends on the operating system,\nwhere the application is run.\n\nIn general these directories need to be manually created\nby the application at runtime, before accessing files or folders\nin it is possible.\n\nTherefore, it is also allowed to create all of these folders via\nthe `mkdir` command.\n\n#### Denied Permissions\n\nThis default permission set prevents access to critical components\nof the Tauri application by default.\nOn Windows the webview data folder access is denied.\n\n",
"description": "This set of permissions describes the what kind of\nfile system access the `fs` plugin has enabled or denied by default.\n\n#### Granted Permissions\n\nThis default permission set enables read access to the\napplication specific directories (AppConfig, AppData, AppLocalData, AppCache,\nAppLog) and all files and sub directories created in it.\nThe location of these directories depends on the operating system,\nwhere the application is run.\n\nIn general these directories need to be manually created\nby the application at runtime, before accessing files or folders\nin it is possible.\n\nTherefore, it is also allowed to create all of these folders via\nthe `mkdir` command.\n\n#### Denied Permissions\n\nThis default permission set prevents access to critical components\nof the Tauri application by default.\nOn Windows the webview data folder access is denied.\n\n#### Included permissions within this default permission set:\n",
"type": "string",
"const": "fs:default"
},
@@ -3537,7 +3537,7 @@
"const": "dialog:deny-save"
},
{
"description": "This set of permissions describes the what kind of\nfile system access the `fs` plugin has enabled or denied by default.\n\n#### Granted Permissions\n\nThis default permission set enables read access to the\napplication specific directories (AppConfig, AppData, AppLocalData, AppCache,\nAppLog) and all files and sub directories created in it.\nThe location of these directories depends on the operating system,\nwhere the application is run.\n\nIn general these directories need to be manually created\nby the application at runtime, before accessing files or folders\nin it is possible.\n\nTherefore, it is also allowed to create all of these folders via\nthe `mkdir` command.\n\n#### Denied Permissions\n\nThis default permission set prevents access to critical components\nof the Tauri application by default.\nOn Windows the webview data folder access is denied.\n\n",
"description": "This set of permissions describes the what kind of\nfile system access the `fs` plugin has enabled or denied by default.\n\n#### Granted Permissions\n\nThis default permission set enables read access to the\napplication specific directories (AppConfig, AppData, AppLocalData, AppCache,\nAppLog) and all files and sub directories created in it.\nThe location of these directories depends on the operating system,\nwhere the application is run.\n\nIn general these directories need to be manually created\nby the application at runtime, before accessing files or folders\nin it is possible.\n\nTherefore, it is also allowed to create all of these folders via\nthe `mkdir` command.\n\n#### Denied Permissions\n\nThis default permission set prevents access to critical components\nof the Tauri application by default.\nOn Windows the webview data folder access is denied.\n\n#### Included permissions within this default permission set:\n",
"type": "string",
"const": "fs:default"
},

View File

@@ -12,7 +12,7 @@ use tauri_utils::config::WebviewUrl;
pub async fn create_mhy_client(handle: AppHandle, func: String, url: String) {
let mut win_width = 400.0;
let mut win_height = 800.0;
let win_ua = "Mozilla/5.0 (Linux; Android 12) Mobile miHoYoBBS/2.76.1";
let win_ua = "Mozilla/5.0 (Linux; Android 12) Mobile miHoYoBBS/2.77.2";
let url_parse;
if url != "" {
url_parse = WebviewUrl::External(url.parse().unwrap());

View File

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

View File

@@ -21,7 +21,7 @@ import { useRouter } from "vue-router";
import TBackTop from "./components/app/t-backTop.vue";
import TSidebar from "./components/app/t-sidebar.vue";
import showConfirm from "./components/func/confirm.js";
import showDialog from "./components/func/dialog.js";
import showSnackbar from "./components/func/snackbar.js";
import TGSqlite from "./plugins/Sqlite/index.js";
import TSUserAccount from "./plugins/Sqlite/modules/userAccount.js";
@@ -29,7 +29,7 @@ import { useAppStore } from "./store/modules/app.js";
import { useUserStore } from "./store/modules/user.js";
import { getBuildTime } from "./utils/TGBuild.js";
import TGLogger from "./utils/TGLogger.js";
import TGRequest from "./web/request/TGRequest.js";
import OtherApi from "./web/request/otherReq.js";
const appStore = useAppStore();
const userStore = storeToRefs(useUserStore());
@@ -60,7 +60,7 @@ onBeforeMount(async () => {
async function checkResize(): Promise<void> {
const screen = await TauriWindow.currentMonitor();
if (screen === null) {
showSnackbar({ text: "获取屏幕信息失败!", color: "error", timeout: 3000 });
showSnackbar.error("获取屏幕信息失败!", 3000);
return;
}
const windowCur = await webviewWindow.getCurrentWebviewWindow();
@@ -133,7 +133,7 @@ async function checkDeviceFp(): Promise<void> {
const deviceFind = appData.find((item) => item.key === "deviceInfo");
if (typeof deviceFind === "undefined") {
if (deviceLocal.device_fp === "0000000000000") {
appStore.deviceInfo = await TGRequest.Device.getFp(appStore.deviceInfo);
appStore.deviceInfo = await OtherApi.fp(appStore.deviceInfo);
}
await TGSqlite.saveAppData("deviceInfo", JSON.stringify(deviceLocal));
return;
@@ -153,7 +153,7 @@ async function checkUserLoad(): Promise<void> {
// 检测用户数据
const uidDB = await TSUserAccount.account.getAllUid();
if (uidDB.length === 0 && appStore.isLogin) {
showSnackbar({ text: "未检测到可用UID请重新登录!", color: "warn" });
showSnackbar.warn("未检测到可用UID请重新登录");
appStore.isLogin = false;
return;
}
@@ -164,7 +164,7 @@ async function checkUserLoad(): Promise<void> {
}
const curAccount = await TSUserAccount.account.getAccount(userStore.uid.value);
if (curAccount === false) {
showSnackbar({ text: `未获取到${userStore.uid.value}账号数据`, color: "error" });
showSnackbar.error(`未获取到${userStore.uid.value}账号数据`);
await TGLogger.Error(`[App][listenOnInit] 获取${userStore.uid.value}账号数据失败`);
await new Promise((resolve) => setTimeout(resolve, 1000));
} else {
@@ -173,7 +173,7 @@ async function checkUserLoad(): Promise<void> {
}
const curGameAccount = await TSUserAccount.game.getCurAccount(userStore.uid.value);
if (curGameAccount === false) {
showSnackbar({ text: `未获取到${userStore.uid.value}游戏数据`, color: "error" });
showSnackbar.error(`未获取到${userStore.uid.value}游戏数据`);
await TGLogger.Error(`[App][listenOnInit] 获取${userStore.uid.value}游戏数据失败`);
await new Promise((resolve) => setTimeout(resolve, 1000));
} else {
@@ -184,13 +184,11 @@ async function checkUserLoad(): Promise<void> {
async function getDeepLink(): Promise<UnlistenFn> {
return await event.listen("active_deep_link", async (e: Event<string>) => {
const windowGet = new webviewWindow.WebviewWindow("TeyvatGuide");
if (await windowGet.isMinimized()) {
await windowGet.unminimize();
}
if (await windowGet.isMinimized()) await windowGet.unminimize();
await windowGet.setFocus();
const payload = parseDeepLink(e.payload);
if (payload === false) {
showSnackbar({ text: "无效的 deep link", color: "error", timeout: 3000 });
showSnackbar.error("无效的 deep link", 3000);
await TGLogger.Error(`[App][getDeepLink] 无效的 deep link ${JSON.stringify(e.payload)}`);
return;
}
@@ -224,7 +222,7 @@ async function handleDeepLink(payload: string): Promise<void> {
if (payload.startsWith("router?path=")) {
const routerPath = payload.replace("router?path=", "");
if (router.currentRoute.value.path === routerPath) {
showSnackbar({ text: "已在当前页面!", color: "warn", timeout: 3000 });
showSnackbar.warn("已在当前页面!", 3000);
return;
}
await router.push(routerPath);
@@ -244,25 +242,19 @@ async function toUIAF(link: string) {
// 检测更新
async function checkUpdate(): Promise<void> {
// @ts-expect-error-next-line
const isProdEnv = import.meta.env.MODE === "production";
const needUpdate = await TGSqlite.checkUpdate();
if (needUpdate && isProdEnv) {
await TGLogger.Info("[App][checkUpdate] 检测到版本更新!");
const confirm = await showConfirm({
title: "检测到版本更新",
text: "是否更新数据库数据?(请确保成就数据已导出)",
});
if (!confirm) {
showSnackbar({
text: "请到设置页手动更新数据库!",
color: "error",
timeout: 3000,
});
const updateCheck = await showDialog.check("检测到版本更新", "是否更新数据库数据?");
if (!updateCheck) {
showSnackbar.error("请到设置页手动更新数据库!", 3000);
return;
}
appStore.buildTime = getBuildTime();
await TGSqlite.update();
showSnackbar({ text: "数据库已更新!", color: "success", timeout: 3000 });
showSnackbar.success("数据库已更新!", 3000);
window.open("https://app.btmuli.ink/docs/TeyvatGuide/changelogs.html");
}
}

View File

@@ -19,10 +19,11 @@ import { useAppStore } from "../../store/modules/app.js";
import TGClient from "../../utils/TGClient.js";
import TGLogger from "../../utils/TGLogger.js";
import { createPost } from "../../utils/TGWindow.js";
import TGRequest from "../../web/request/TGRequest.js";
import showConfirm from "../func/confirm.js";
import OtherApi from "../../web/request/otherReq.js";
import showDialog from "../func/dialog.js";
import showSnackbar from "../func/snackbar.js";
import ToLivecode from "../overlay/to-livecode.vue";
import ToLivecode from "./to-livecode.vue";
interface TGameNavProps {
modelValue: number;
@@ -60,27 +61,24 @@ async function tryGetCode(): Promise<void> {
if (!navFind) return;
const actIdFind = new URL(navFind.app_path).searchParams.get("act_id");
if (!actIdFind) {
showSnackbar({ text: "未找到活动ID", color: "warn" });
showSnackbar.warn("未找到活动ID");
return;
}
actId.value = actIdFind;
const res = await TGRequest.Nav.getCode(actIdFind);
const res = await OtherApi.code(actIdFind);
if (!Array.isArray(res)) {
showSnackbar({ text: `[${res.retcode}] ${res.message}`, color: "warn" });
showSnackbar.warn(`[${res.retcode}] ${res.message}`);
return;
}
codeData.value = res;
showSnackbar({ text: "获取兑换码成功", color: "success" });
showSnackbar.success("获取兑换码成功");
await TGLogger.Info(JSON.stringify(res));
showOverlay.value = true;
}
async function toNav(item: TGApp.BBS.Navigator.Navigator): Promise<void> {
if (!appStore.isLogin) {
showSnackbar({
text: "请先登录",
color: "warn",
});
showSnackbar.warn("请先登录");
return;
}
await TGLogger.Info(`[TGameNav][toNav] 打开网页活动 ${item.name}`);
@@ -106,19 +104,13 @@ async function toNav(item: TGApp.BBS.Navigator.Navigator): Promise<void> {
await TGClient.open("web_act_thin", item.app_path);
return;
}
const modeConfirm = await showConfirm({
title: "是否采用宽屏模式打开?",
text: "取消则采用竖屏模式打开",
});
if (modeConfirm === undefined) {
showSnackbar({
text: "已取消打开",
color: "cancel",
});
const modeCheck = await showDialog.check("是否采用宽屏模式打开?", "取消则采用竖屏模式打开");
if (modeCheck === undefined) {
showSnackbar.cancel("取消打开");
return;
}
if (modeConfirm) await TGClient.open("web_act", item.app_path);
else await TGClient.open("web_act_thin", item.app_path);
if (!modeCheck) await TGClient.open("web_act_thin", item.app_path);
else await TGClient.open("web_act", item.app_path);
}
// protocol
@@ -133,20 +125,14 @@ async function toBBS(link: URL): Promise<void> {
const forumId = link.pathname.split("/").pop();
const localPath = getLocalPath(forumId);
if (localPath === "") {
showSnackbar({
text: `不支持的链接:${link.href}`,
color: "warn",
});
showSnackbar.warn(`不支持的链接:${link.href}`);
return;
}
await emit("active_deep_link", `router?path=${localPath}`);
return;
}
}
showSnackbar({
text: `不支持的链接:${link.href}`,
color: "warn",
});
showSnackbar.warn(`不支持的链接:${link.href}`);
}
function getLocalPath(forum?: string): string {
@@ -167,13 +153,13 @@ function getLocalPath(forum?: string): string {
const wdForums = ["37", "60", "42", "38"];
const zzzForums = ["57", "59", "64", "65"];
const dbyForums = ["54", "35", "34", "39", "47", "48", "55", "36"];
if (ysForums.includes(forum)) return `/posts/2/${forum}`;
if (srForums.includes(forum)) return `/posts/6/${forum}`;
if (bh3Forums.includes(forum)) return `/posts/1/${forum}`;
if (bh2Forums.includes(forum)) return `/posts/3/${forum}`;
if (wdForums.includes(forum)) return `/posts/4/${forum}`;
if (zzzForums.includes(forum)) return `/posts/8/${forum}`;
if (dbyForums.includes(forum)) return `/posts/5/${forum}`;
if (ysForums.includes(forum)) return `/posts/forum/2/${forum}`;
if (srForums.includes(forum)) return `/posts/forum/6/${forum}`;
if (bh3Forums.includes(forum)) return `/posts/forum/1/${forum}`;
if (bh2Forums.includes(forum)) return `/posts/forum/3/${forum}`;
if (wdForums.includes(forum)) return `/posts/forum/4/${forum}`;
if (zzzForums.includes(forum)) return `/posts/forum/8/${forum}`;
if (dbyForums.includes(forum)) return `/posts/forum/5/${forum}`;
return "";
}
</script>

View File

@@ -24,8 +24,7 @@ onMounted(async () => {
async function switchPin(): Promise<void> {
isPined.value = !isPined.value;
await getCurrentWindow().setAlwaysOnTop(isPined.value);
const text = isPined.value ? "已将窗口置顶!" : "已经取消窗口置顶!";
showSnackbar({ text: text, color: "success" });
showSnackbar.success(isPined.value ? "已将窗口置顶!" : "已经取消窗口置顶!");
}
</script>
<style lang="css" scoped>

View File

@@ -1,23 +1,31 @@
<template>
<div v-if="card" :id="`post-card-${card.postId}`" class="tpc-card">
<div class="tpc-cover" @click="createPost(card)">
<img :src="localCover" alt="cover" v-if="localCover" />
<v-progress-circular color="primary" :indeterminate="true" v-else-if="card.cover !== ''" />
<img src="/source/UI/defaultCover.webp" alt="cover" v-else />
<div v-if="isAct" class="tpc-act">
<div class="tpc-status" :style="{ background: card.status?.colorCss }">
{{ card.status?.status }}
</div>
<div class="tpc-time">
<v-icon>mdi-clock-time-four-outline</v-icon>
<span>{{ card.subtitle }}</span>
<div class="tpc-top">
<div class="tpc-cover" @click="createPost(card)">
<img :src="localCover" alt="cover" v-if="localCover" />
<v-progress-circular color="primary" :indeterminate="true" v-else-if="card.cover !== ''" />
<img src="/source/UI/defaultCover.webp" alt="cover" v-else />
<div v-if="isAct" class="tpc-act">
<div class="tpc-status">{{ card.status?.status }}</div>
<div class="tpc-time">
<v-icon>mdi-clock-time-four-outline</v-icon>
<span>{{ card.subtitle }}</span>
</div>
</div>
</div>
</div>
<div class="tpc-content">
<div class="tpc-title" :title="card.title" @click="shareCard">{{ card.title }}</div>
<TpAvatar v-if="card.user" :data="card.user" position="left" />
<div class="tpc-data" v-if="card.data">
</div>
<div class="tpc-mid" v-if="card.user">
<TpAvatar :data="card.user" position="left" />
</div>
<div class="tpc-bottom" v-if="card.data">
<div class="tpc-tags">
<div v-for="topic in card.topics" :key="topic.id" class="tpc-tag" @click="toTopic(topic)">
<v-icon size="10">mdi-tag</v-icon>
<span>{{ topic.name }}</span>
</div>
</div>
<div class="tpc-data">
<div class="tpc-info-item" :title="`浏览数:${card.data.view}`">
<v-icon>mdi-eye</v-icon>
<span>{{ card.data.view }}</span>
@@ -44,6 +52,7 @@
class="tpc-forum"
v-if="card.forum && card.forum.name !== ''"
:title="`频道: ${card.forum.name}`"
@click="toForum(card.forum)"
>
<img :src="card.forum.icon" :alt="card.forum.name" />
<span>{{ card.forum.name }}</span>
@@ -51,28 +60,27 @@
<v-checkbox-btn
v-if="props.selectMode"
class="tpc-select"
v-model="selectedList"
:value="props.modelValue.post.post_id"
@click="emits('onSelected', props.modelValue.post.post_id)"
data-html2canvas-ignore
/>
<div class="tpc-info-id" v-else>{{ props.modelValue.post.post_id }}</div>
</div>
</template>
<script lang="ts" setup>
import { emit } from "@tauri-apps/api/event";
import { computed, onMounted, onUnmounted, ref, watch } from "vue";
import { generateShareImg, saveImgLocal } from "../../utils/TGShare.js";
import { createPost } from "../../utils/TGWindow.js";
import TpAvatar from "../post/tp-avatar.vue";
import TpAvatar from "../viewPost/tp-avatar.vue";
interface TPostCardProps {
modelValue: TGApp.Plugins.Mys.Post.FullData;
selectMode?: boolean;
selected?: string[];
}
interface TPostCardEmits {
(e: "update:selected", value: string[]): void;
(e: "onSelected", value: string): void;
}
const props = withDefaults(defineProps<TPostCardProps>(), {
@@ -82,12 +90,10 @@ const emits = defineEmits<TPostCardEmits>();
const isAct = ref<boolean>(false);
const card = ref<TGApp.Plugins.Mys.News.RenderCard>();
const localCover = ref<string>();
const selectedList = computed({
get: () => props.selected,
set: (v) => {
if (v === undefined) return;
emits("update:selected", v);
},
const cardBg = computed<string>(() => {
if (card.value && card.value.status) return card.value.status.colorCss;
return "none";
});
onMounted(async () => await reload(props.modelValue));
@@ -180,12 +186,13 @@ function getPostCover(item: TGApp.Plugins.Mys.Post.FullData): string {
* @returns {TGApp.Plugins.Mys.News.RenderCard} 渲染用咨讯列表项
*/
function getCommonCard(item: TGApp.Plugins.Mys.Post.FullData): TGApp.Plugins.Mys.News.RenderCard {
let forum = null;
let data = null;
let forum: TGApp.Plugins.Mys.News.RenderForum | null = null;
let data: TGApp.Plugins.Mys.News.RenderData | null = null;
if (item.forum !== null) {
forum = {
name: item.forum.name,
icon: item.forum.icon,
id: item.forum.id,
};
}
if (item.stat !== null) {
@@ -205,6 +212,7 @@ function getCommonCard(item: TGApp.Plugins.Mys.Post.FullData): TGApp.Plugins.Mys
user: item.user,
forum: forum,
data: data,
topics: item.topics,
};
}
@@ -231,15 +239,41 @@ async function shareCard(): Promise<void> {
const fileName = `PostCard_${card.value.postId}`;
await generateShareImg(fileName, dom, 2.5);
}
async function toTopic(topic: TGApp.Plugins.Mys.Topic.Info): Promise<void> {
const gid = props.modelValue.post.game_id;
await emit("active_deep_link", `router?path=/posts/topic/${gid}/${topic.id}`);
}
async function toForum(forum: TGApp.Plugins.Mys.News.RenderForum): Promise<void> {
const gid = props.modelValue.post.game_id;
await emit("active_deep_link", `router?path=/posts/forum/${gid}/${forum.id}`);
}
</script>
<style lang="css" scoped>
.tpc-card {
position: relative;
display: flex;
overflow: hidden;
width: 100%;
height: 100%;
flex-direction: column;
align-items: center;
justify-content: space-between;
border: 1px solid var(--common-shadow-1);
border-radius: 5px;
background: var(--box-bg-1);
box-shadow: 2px 2px 5px var(--common-shadow-2);
row-gap: 10px;
}
.tpc-top {
display: flex;
width: 100%;
flex-direction: column;
align-items: center;
justify-content: center;
row-gap: 5px;
}
.tpc-cover {
@@ -261,18 +295,25 @@ async function shareCard(): Promise<void> {
transition: all 0.3s linear;
}
.tpc-content {
.tpc-mid {
position: relative;
width: 100%;
padding: 0 10px;
}
.tpc-bottom {
position: relative;
display: flex;
width: 100%;
flex-direction: column;
padding: 10px;
gap: 10px;
padding: 5px 10px;
row-gap: 5px;
}
.tpc-title {
overflow: hidden;
width: 100%;
padding: 5px 10px;
cursor: pointer;
font-family: var(--font-title);
font-size: 18px;
@@ -280,6 +321,32 @@ async function shareCard(): Promise<void> {
white-space: nowrap;
}
.tpc-tags {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
justify-content: flex-start;
color: var(--box-text-5);
cursor: pointer;
font-size: 12px;
gap: 5px;
:hover {
color: var(--box-text-3);
}
}
.tpc-tag {
display: flex;
align-items: center;
justify-content: center;
padding: 0 3px;
border: 1px solid var(--common-shadow-1);
border-radius: 5px;
background: var(--box-bg-2);
gap: 3px;
}
.tpc-forum {
position: absolute;
top: 0;
@@ -294,6 +361,7 @@ async function shareCard(): Promise<void> {
border-bottom-left-radius: 5px;
box-shadow: 0 0 10px var(--tgc-dark-1);
color: var(--tgc-white-1);
cursor: pointer;
text-shadow: 0 0 5px var(--tgc-dark-1);
}
@@ -308,9 +376,10 @@ async function shareCard(): Promise<void> {
justify-content: center;
-webkit-backdrop-filter: blur(20px);
backdrop-filter: blur(20px);
background: var(--tgc-yellow-2);
background: var(--box-bg-2);
border-bottom-right-radius: 4px;
box-shadow: 0 0 10px var(--tgc-dark-1);
color: var(--box-text-5);
}
.tpc-forum img {
@@ -364,6 +433,7 @@ async function shareCard(): Promise<void> {
align-items: center;
justify-content: flex-start;
padding: 5px 30px 5px 5px;
background-color: v-bind(cardBg);
clip-path: polygon(0 0, calc(100% - 15px) 0, 100% 50%, calc(100% - 15px) 100%, 0 100%);
color: var(--tgc-white-1);

View File

@@ -1,29 +1,25 @@
<template>
<div class="share-box" title="分享">
<div class="share-btn" @click="shareContent()">
<v-icon> mdi-share-variant</v-icon>
<v-icon>mdi-share-variant</v-icon>
</div>
</div>
</template>
<script lang="ts" setup>
// utils
import TGLogger from "../../utils/TGLogger.js";
import { generateShareImg } from "../../utils/TGShare.js";
import showLoading from "../func/loading.js";
interface TShareBtnProps {
modelValue: HTMLElement;
title: string;
loading: boolean;
}
type TShareBtnEmits = (e: "update:loading", value: boolean) => void;
const props = defineProps<TShareBtnProps>();
const emit = defineEmits<TShareBtnEmits>();
async function shareContent(): Promise<void> {
showLoading.start("正在生成分享图片", props.title);
await TGLogger.Info("[TShareBtn][shareContent] 开始生成分享图片");
emit("update:loading", true);
props.modelValue.querySelectorAll("details").forEach((item) => {
if (item.open) {
item.setAttribute("details-open", "");
@@ -39,7 +35,7 @@ async function shareContent(): Promise<void> {
item.open = false;
}
});
emit("update:loading", false);
showLoading.end();
await TGLogger.Info("[TShareBtn][shareContent] 生成分享图片完成");
}
</script>

View File

@@ -38,7 +38,7 @@
<img src="/platforms/mhy/mys.webp" alt="mihoyo" class="side-icon" />
</template>
</v-list-item>
<v-list-item :title.attr="'帖子'" value="posts" :link="true" href="/posts">
<v-list-item :title.attr="'帖子'" value="posts" :link="true" href="/posts/forum">
<template #title>帖子</template>
<template #prepend>
<img src="/source/UI/posts.png" alt="posts" class="side-icon" />

View File

@@ -1,5 +1,11 @@
<template>
<TOverlay v-model="visible" :hide="true" :to-click="onCancel" blur-val="20px">
<TOverlay
v-model="visible"
:hide="true"
:to-click="onCancel"
blur-val="20px"
class="tolc-overlay"
>
<div class="tolc-box">
<div class="tolc-title">
<span>兑换码</span>
@@ -49,7 +55,8 @@ import { computed } from "vue";
import { generateShareImg } from "../../utils/TGShare.js";
import { timestampToDate } from "../../utils/toolFunc.js";
import showSnackbar from "../func/snackbar.js";
import TOverlay from "../main/t-overlay.vue";
import TOverlay from "./t-overlay.vue";
interface ToLiveCodeProps {
data: TGApp.BBS.Navigator.CodeData[];
@@ -75,7 +82,7 @@ function onCancel(): void {
function copy(code: string): void {
navigator.clipboard.writeText(code);
showSnackbar({ text: "已复制到剪贴板", color: "success" });
showSnackbar.success("已复制到剪贴板");
}
async function shareImg(): Promise<void> {
@@ -85,6 +92,10 @@ async function shareImg(): Promise<void> {
}
</script>
<style lang="css" scoped>
.tolc-overlay {
height: 100vh;
}
.tolc-box {
position: relative;
width: 340px;

View File

@@ -29,7 +29,8 @@
import { computed, ref } from "vue";
import { generateShareImg } from "../../utils/TGShare.js";
import TOverlay from "../main/t-overlay.vue";
import TOverlay from "./t-overlay.vue";
interface ToNamecardProps {
modelValue: boolean;

View File

@@ -1,66 +0,0 @@
/**
* @file component/func/confirm.ts
* @description 封装自定义 confirm 组件,通过函数调用的方式,简化 confirm 的使用
* @since Beta v0.3.9
*/
import { h, render } from "vue";
import type { ComponentInternalInstance, VNode } from "vue";
import confirm from "./confirm.vue";
const confirmId = "tg-func-confirm";
/**
* @description 自定义 confirm 组件
* @since Beta v0.3.4
* @extends ComponentInternalInstance
* @property {Function} exposeProxy.displayBox 显示 confirm
* @return ConfirmInstance
*/
interface ConfirmInstance extends ComponentInternalInstance {
exposeProxy: {
displayBox: (props: TGApp.Component.Confirm.Params) => Promise<string | boolean>;
};
}
const renderBox = (props: TGApp.Component.Confirm.Params): VNode => {
const container = document.createElement("div");
container.id = confirmId;
const boxVNode: VNode = h(confirm, props);
render(boxVNode, container);
document.body.appendChild(container);
return boxVNode;
};
let confirmInstance: VNode;
/**
* @function showConfirm
* @since Beta v0.3.9
* @description 弹出 confirm
* @param {TGApp.Component.Confirm.Params} props confirm 的参数
* @return {Promise<string | boolean | undefined>} 点击确认返回 true点击取消返回 false点击外部返回 undefined
*/
async function showConfirm(
props: TGApp.Component.Confirm.ParamsConfirm,
): Promise<boolean | undefined>;
async function showConfirm(
props: TGApp.Component.Confirm.ParamsInput,
): Promise<string | false | undefined>;
async function showConfirm(
props: TGApp.Component.Confirm.Params,
): Promise<string | boolean | undefined>;
async function showConfirm(
props: TGApp.Component.Confirm.Params,
): Promise<string | boolean | undefined> {
if (confirmInstance !== undefined) {
const boxVue = <ConfirmInstance>confirmInstance.component;
return await boxVue.exposeProxy.displayBox(props);
} else {
confirmInstance = renderBox(props);
return await showConfirm(props);
}
}
export default showConfirm;

View File

@@ -1,289 +0,0 @@
<template>
<transition name="func-confirm-outer">
<div v-show="show || showOuter" class="confirm-overlay" @click.self.prevent="handleOuter">
<transition name="func-confirm-inner">
<div v-show="showInner" class="confirm-box">
<div class="confirm-title">{{ data.title }}</div>
<div
v-show="data?.text !== '' && data.mode === 'confirm'"
class="confirm-subtitle"
:title="data.text"
>
{{ data.text }}
</div>
<div v-show="data?.text !== '' && data.mode === 'input'" class="confirm-input">
<div class="confirm-input-label">{{ data.text }}</div>
<input
v-model="inputVal"
class="confirm-input-box"
ref="inputRef"
@keydown.enter="handleConfirm"
/>
</div>
<div class="confirm-btn-box">
<button class="confirm-btn no-btn" @click="handleCancel">取消</button>
<button class="confirm-btn ok-btn" @click="handleConfirm">确定</button>
</div>
</div>
</transition>
</div>
</transition>
</template>
<script lang="ts" setup>
import { nextTick, onMounted, reactive, ref, watch, useTemplateRef } from "vue";
interface ConfirmProps {
title: string;
text?: string;
mode?: "confirm" | "input";
otcancel?: boolean;
}
const defaultProp: ConfirmProps = {
title: "",
text: "",
mode: "confirm",
otcancel: false,
};
const props = withDefaults(defineProps<ConfirmProps>(), {
title: "",
text: "",
mode: "confirm",
otcancel: false,
});
// 组件参数
const data = reactive<TGApp.Component.Confirm.Params>(defaultProp);
const show = ref<boolean>(false);
const showOuter = ref<boolean>(false);
const showInner = ref<boolean>(false);
const confirmVal = ref<boolean | string | undefined>();
const inputVal = ref<string>("");
const inputEl = useTemplateRef<HTMLInputElement>("inputRef");
watch(show, () => {
if (show.value) {
showOuter.value = true;
setTimeout(() => {
showInner.value = true;
}, 100);
} else {
setTimeout(() => {
showInner.value = false;
}, 100);
setTimeout(() => {
showOuter.value = false;
}, 300);
}
});
onMounted(async () => {
await displayBox(props);
});
async function displayBox(
params: TGApp.Component.Confirm.Params,
): Promise<string | boolean | undefined> {
data.title = params.title;
data.text = params.text ?? "";
data.mode = params.mode ?? "confirm";
data.otcancel = params.otcancel ?? true;
if (params.mode === "input" && params.input) {
inputVal.value = params.input;
}
show.value = true;
// 等待确认框关闭返回关闭后的confirmVal
return await new Promise<string | boolean | undefined>((resolve) => {
nextTick(() => {
if (data.mode === "input") {
// 等待确认框打开,聚焦输入框
setTimeout(() => {
inputEl.value?.focus();
}, 100);
}
});
watch(show, () => {
// 等 0.5s 动画
setTimeout(() => {
resolve(confirmVal.value);
}, 500);
});
});
}
// 确认
function handleConfirm(): void {
if (data.mode === "input") {
confirmVal.value = inputVal.value;
inputVal.value = "";
} else {
confirmVal.value = true;
}
show.value = false;
}
// 取消
function handleCancel(): void {
confirmVal.value = false;
show.value = false;
}
// 点击外部事件
function handleOuter(): void {
if (data.otcancel) {
confirmVal.value = undefined;
show.value = false;
}
}
defineExpose({
displayBox,
});
</script>
<style scoped>
.func-confirm-outer-enter-active,
.func-confirm-outer-leave-active,
.func-confirm-inner-enter-active {
transition: all 0.3s;
}
.func-confirm-inner-leave-active {
transition: all 0.5s ease-in-out;
}
.func-confirm-inner-enter-from {
opacity: 0;
transform: scale(1.5);
}
.func-confirm-inner-enter-to,
.func-confirm-inner-leave-from {
opacity: 1;
transform: scale(1);
}
.func-confirm-outer-enter-to,
.func-confirm-outer-leave-from {
opacity: 1;
}
.func-confirm-outer-enter-from,
.func-confirm-outer-leave-to {
opacity: 0;
}
.func-confirm-inner-leave-to {
opacity: 0;
transform: scale(0);
}
.confirm-overlay {
position: fixed;
z-index: 100;
top: 0;
left: 0;
display: flex;
width: 100%;
height: 100%;
align-items: center;
justify-content: center;
-webkit-backdrop-filter: blur(10px);
backdrop-filter: blur(10px);
/* 颜色变量 */
--confirm-title: var(--tgc-dark-7);
--confirm-bg: var(--tgc-white-1);
}
/* 深色模式 */
.dark .confirm-overlay {
--confirm-title: var(--tgc-white-1);
--confirm-bg: var(--tgc-dark-7);
}
.confirm-box {
display: flex;
width: 520px;
height: 240px;
flex-direction: column;
align-items: center;
justify-content: space-between;
padding: 10px;
border: 1px solid var(--common-shadow-1);
border-radius: 15px;
background: var(--confirm-bg);
box-shadow: 0 0 10px var(--common-shadow-t-1);
color: var(--tgc-yellow-3);
}
.confirm-title {
width: 100%;
border-bottom: 1px solid var(--confirm-title);
color: var(--confirm-title);
font-family: var(--font-title);
font-size: 30px;
text-align: center;
}
.confirm-subtitle {
overflow: hidden;
width: 100%;
font-family: var(--font-text);
font-size: 20px;
text-align: center;
text-overflow: ellipsis;
white-space: normal;
word-break: break-all;
}
.confirm-input {
display: flex;
width: 100%;
align-items: center;
justify-content: center;
font-family: var(--font-text);
font-size: 16px;
gap: 10px;
}
.confirm-input-box {
width: 50%;
height: 100%;
padding: 5px;
border: 1px solid var(--confirm-title);
border-radius: 5px;
background: inherit;
color: var(--confirm-title);
}
.confirm-btn-box {
display: flex;
width: 100%;
align-items: flex-end;
justify-content: space-around;
}
.confirm-btn {
position: relative;
display: flex;
width: 180px;
height: 60px;
align-items: center;
justify-content: center;
border-radius: 15px;
cursor: pointer;
font-family: var(--font-title);
font-size: 20px;
}
.no-btn {
border: 1px solid var(--tgc-yellow-1);
}
.ok-btn {
background: var(--confirm-title);
}
</style>

View File

@@ -0,0 +1,113 @@
/**
* @file component/func/dialog.ts
* @description dialog 组件封装,函数式调用
* @since Beta v0.6.3
*/
import { h, render } from "vue";
import type { ComponentInternalInstance, VNode } from "vue";
import dialog from "./dialog.vue";
const dialogId = "tg-func-dialog";
export type DialogParams = DialogCheckParams | DialogInputParams;
export type DialogCheckParams = {
mode: "check";
title: string;
text?: string;
otcancel?: boolean;
};
export type DialogInputParams = {
mode: "input";
title: string;
text?: string;
otcancel?: boolean;
input?: string;
};
/**
* @description 自定义 confirm 组件
* @since Beta v0.6.3
* @extends ComponentInternalInstance
* @property {Function} exposeProxy.displayBox 显示 confirm
* @return DialogInstance
*/
interface DialogInstance extends ComponentInternalInstance {
exposeProxy: {
displayCheckBox: (props: DialogCheckParams) => Promise<boolean | undefined>;
displayInputBox: (props: DialogInputParams) => Promise<string | false | undefined>;
};
}
function renderBox(props: DialogParams): VNode {
const container = document.createElement("div");
container.id = dialogId;
const boxVNode: VNode = h(dialog, props);
render(boxVNode, container);
document.body.appendChild(container);
return boxVNode;
}
let dialogInstance: VNode;
async function showDialogFull(
mode: "check" | "input",
title: string,
text?: string,
input?: string,
otcancel?: boolean,
): Promise<boolean | string | undefined> {
if (mode === "check") return await showDialogCheck(title, text, otcancel);
return await showDialogInput(title, text, input, otcancel);
}
async function showDialogCheck(
title: string,
text?: string,
otcancel?: boolean,
): Promise<boolean | undefined> {
const params: DialogCheckParams = {
mode: "check",
title: title,
text: text,
otcancel: otcancel,
};
if (dialogInstance !== undefined) {
const boxVue = <DialogInstance>dialogInstance.component;
return await boxVue.exposeProxy.displayCheckBox(params);
} else {
dialogInstance = renderBox(params);
return await showDialogCheck(title, text, otcancel);
}
}
async function showDialogInput(
title: string,
text?: string,
input?: string,
otcancel?: boolean,
): Promise<string | false | undefined> {
const params: DialogInputParams = {
mode: "input",
title: title,
text: text,
input: input,
otcancel: otcancel,
};
if (dialogInstance !== undefined) {
const boxVue = <DialogInstance>dialogInstance.component;
return await boxVue.exposeProxy.displayInputBox(params);
} else {
dialogInstance = renderBox(params);
return await showDialogInput(title, text, input, otcancel);
}
}
const showDialog = {
_: showDialogFull,
check: showDialogCheck,
input: showDialogInput,
};
export default showDialog;

View File

@@ -0,0 +1,294 @@
<template>
<transition name="func-dialog-outer">
<div v-show="show || showOuter" class="dialog-overlay" @click.self.prevent="handleOuter">
<transition name="func-dialog-inner">
<div v-show="showInner" class="dialog-box">
<div class="dialog-title">{{ data.title }}</div>
<div
v-show="data?.text !== '' && data.mode === 'check'"
class="dialog-subtitle"
:title="data.text"
>
{{ data.text }}
</div>
<div v-show="data?.text !== '' && data.mode === 'input'" class="dialog-input">
<div class="dialog-input-label">{{ data.text }}</div>
<input
v-model="inputDefault"
class="dialog-input-box"
ref="inputRef"
@keydown.enter="handleConfirm"
/>
</div>
<div class="dialog-btn-box">
<button class="dialog-btn no-btn" @click="handleCancel">取消</button>
<button class="dialog-btn ok-btn" @click="handleConfirm">确定</button>
</div>
</div>
</transition>
</div>
</transition>
</template>
<script lang="ts" setup>
import { nextTick, onMounted, reactive, ref, watch, useTemplateRef, computed } from "vue";
import { DialogCheckParams, DialogInputParams, DialogParams } from "./dialog.js";
const defaultProp: DialogParams = { title: "", text: "", mode: "check", otcancel: false };
const props = defineProps<DialogParams>();
// 组件参数
const data = reactive<DialogParams>(defaultProp);
const show = ref<boolean>(false);
const showOuter = ref<boolean>(false);
const showInner = ref<boolean>(false);
const dialogVal = ref<boolean | string | undefined>();
const inputDefault = ref<string>("");
const inputEl = useTemplateRef<HTMLInputElement>("inputRef");
const checkVal = computed<boolean | undefined>(() => {
if (typeof dialogVal.value === "string") return dialogVal.value !== "";
return dialogVal.value;
});
const inputVal = computed<string | false | undefined>(() => {
if (typeof dialogVal.value === "string") return dialogVal.value;
if (dialogVal.value === undefined) return undefined;
return false;
});
watch(
() => show.value,
() => {
if (show.value) {
showOuter.value = true;
setTimeout(() => (showInner.value = true), 100);
return;
}
setTimeout(() => (showInner.value = false), 100);
setTimeout(() => (showOuter.value = false), 300);
},
);
onMounted(async () => {
if (props.mode === "input") {
const param: DialogInputParams = {
mode: "input",
title: props.title,
text: props.text,
input: props.input,
otcancel: props.otcancel,
};
await displayInputBox(param);
} else {
const param: DialogCheckParams = {
mode: "check",
title: props.title,
text: props.text,
otcancel: props.otcancel,
};
await displayCheckBox(param);
}
});
async function displayCheckBox(params: DialogCheckParams): Promise<boolean | undefined> {
data.title = params.title;
data.text = params.text ?? "";
data.mode = "check";
data.otcancel = params.otcancel ?? true;
show.value = true;
return await new Promise<boolean | undefined>((resolve) => {
watch(
() => show.value,
() => setTimeout(() => resolve(checkVal.value), 500),
);
});
}
async function displayInputBox(params: DialogInputParams): Promise<string | false | undefined> {
data.title = params.title;
data.text = params.text ?? "";
data.mode = "input";
data.otcancel = params.otcancel ?? true;
show.value = true;
return await new Promise<string | false | undefined>((resolve) => {
nextTick(() => setTimeout(() => inputEl.value?.focus(), 100));
watch(
() => show.value,
() => setTimeout(() => resolve(inputVal.value), 500),
);
});
}
// 确认
function handleConfirm(): void {
if (data.mode === "input") {
dialogVal.value = inputDefault.value;
inputDefault.value = "";
} else {
dialogVal.value = true;
}
show.value = false;
}
// 取消
function handleCancel(): void {
dialogVal.value = false;
show.value = false;
}
// 点击外部事件
function handleOuter(): void {
if (data.otcancel) {
dialogVal.value = undefined;
show.value = false;
}
}
defineExpose({ displayInputBox, displayCheckBox });
</script>
<style scoped>
.func-dialog-outer-enter-active,
.func-dialog-outer-leave-active,
.func-dialog-inner-enter-active {
transition: all 0.3s;
}
.func-dialog-inner-leave-active {
transition: all 0.5s ease-in-out;
}
.func-dialog-inner-enter-from {
opacity: 0;
transform: scale(1.5);
}
.func-dialog-inner-enter-to,
.func-dialog-inner-leave-from {
opacity: 1;
transform: scale(1);
}
.func-dialog-outer-enter-to,
.func-dialog-outer-leave-from {
opacity: 1;
}
.func-dialog-outer-enter-from,
.func-dialog-outer-leave-to {
opacity: 0;
}
.func-dialog-inner-leave-to {
opacity: 0;
transform: scale(0);
}
.dialog-overlay {
position: fixed;
z-index: 100;
top: 0;
left: 0;
display: flex;
width: 100%;
height: 100%;
align-items: center;
justify-content: center;
-webkit-backdrop-filter: blur(10px);
backdrop-filter: blur(10px);
/* 颜色变量 */
--dialog-title: var(--tgc-dark-7);
--dialog-bg: var(--tgc-white-1);
}
/* 深色模式 */
.dark .dialog-overlay {
--dialog-title: var(--tgc-white-1);
--dialog-bg: var(--tgc-dark-7);
}
.dialog-box {
display: flex;
width: 520px;
height: 240px;
flex-direction: column;
align-items: center;
justify-content: space-between;
padding: 10px;
border: 1px solid var(--common-shadow-1);
border-radius: 15px;
background: var(--dialog-bg);
box-shadow: 0 0 10px var(--common-shadow-t-1);
color: var(--tgc-yellow-3);
}
.dialog-title {
width: 100%;
border-bottom: 1px solid var(--dialog-title);
color: var(--dialog-title);
font-family: var(--font-title);
font-size: 30px;
text-align: center;
}
.dialog-subtitle {
overflow: hidden;
width: 100%;
font-family: var(--font-text);
font-size: 20px;
text-align: center;
text-overflow: ellipsis;
white-space: normal;
word-break: break-all;
}
.dialog-input {
display: flex;
width: 100%;
align-items: center;
justify-content: center;
font-family: var(--font-text);
font-size: 16px;
gap: 10px;
}
.dialog-input-box {
width: 50%;
height: 100%;
padding: 5px;
border: 1px solid var(--dialog-title);
border-radius: 5px;
background: inherit;
color: var(--dialog-title);
}
.dialog-btn-box {
display: flex;
width: 100%;
align-items: flex-end;
justify-content: space-around;
}
.dialog-btn {
position: relative;
display: flex;
width: 180px;
height: 60px;
align-items: center;
justify-content: center;
border-radius: 15px;
cursor: pointer;
font-family: var(--font-title);
font-size: 20px;
}
.no-btn {
border: 1px solid var(--tgc-yellow-1);
}
.ok-btn {
background: var(--dialog-title);
}
</style>

View File

@@ -23,21 +23,18 @@ const showInner = ref<boolean>(false);
const geetestEl = useTemplateRef<HTMLDivElement>("geetestRef");
watch(show, () => {
if (show.value) {
showOuter.value = true;
setTimeout(() => {
showInner.value = true;
}, 100);
} else {
setTimeout(() => {
showInner.value = false;
}, 100);
setTimeout(() => {
showOuter.value = false;
}, 300);
}
});
watch(
() => show.value,
() => {
if (show.value) {
showOuter.value = true;
setTimeout(() => (showInner.value = true), 100);
} else {
setTimeout(() => (showInner.value = false), 100);
setTimeout(() => (showOuter.value = false), 300);
}
},
);
async function displayBox(
props: TGApp.Plugins.Mys.Geetest.reqResp,
@@ -58,24 +55,18 @@ async function displayBox(
if (geetestEl.value === null) return;
geetestEl.value.innerHTML = "";
captchaObj.appendTo("#geetest");
captchaObj.onReady(() => {
show.value = true;
});
captchaObj.onSuccess(async () => {
captchaObj.onReady(() => (show.value = true));
captchaObj.onSuccess(() => {
const validate = captchaObj.getValidate();
resolve(validate);
});
captchaObj.onClose(() => {
show.value = false;
});
captchaObj.onClose(() => (show.value = false));
},
);
});
}
defineExpose({
displayBox,
});
defineExpose({ displayBox });
</script>
<style lang="css" scoped>
.func-geetest-outer-enter-active,

Some files were not shown because too many files have changed in this diff Show More