Compare commits
53 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1efab84da0 | ||
|
|
116d29823e | ||
|
|
53c045e419 | ||
|
|
3fef8467f4 | ||
|
|
e1f85d1d92 | ||
|
|
878eef66fa | ||
|
|
be4e2c1039 | ||
|
|
62171b78d9 | ||
|
|
7932de8654 | ||
|
|
c6f45f0a35 | ||
|
|
9b1fa22cbe | ||
|
|
63929bc9fd | ||
|
|
53f2612d32 | ||
|
|
c17339bab5 | ||
|
|
8a89883784 | ||
|
|
4bc1808478 | ||
|
|
c7b5bf34ef | ||
|
|
6b66467cff | ||
|
|
680a54a0f1 | ||
|
|
2f60e128a5 | ||
|
|
863246707f | ||
|
|
8f0853c41b | ||
|
|
3166567486 | ||
|
|
82f937a33e | ||
|
|
6974ada1c0 | ||
|
|
0c24b95fff | ||
|
|
c87ec77543 | ||
|
|
36b0d198a9 | ||
|
|
76f8bc3c16 | ||
|
|
f655b6b235 | ||
|
|
83e0d35245 | ||
|
|
10a5b88d24 | ||
|
|
afe53f3d30 | ||
|
|
b8afab093f | ||
|
|
ac6dbe0cdf | ||
|
|
20b9631468 | ||
|
|
857d99361a | ||
|
|
eaa07601a5 | ||
|
|
d88e1d1429 | ||
|
|
da4a095618 | ||
|
|
d35b94f79f | ||
|
|
272f3bc14a | ||
|
|
a2ce468e2f | ||
|
|
6b8cbe0e57 | ||
|
|
dafcc30239 | ||
|
|
044059beb0 | ||
|
|
eea9287cfa | ||
|
|
8020d623e3 | ||
|
|
74ff33e1fb | ||
|
|
cae2a0a1e6 | ||
|
|
45bc398626 | ||
|
|
7b5bf201ce | ||
|
|
8df9370932 |
2
.github/workflows/build.yml
vendored
@@ -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
|
||||
|
||||
|
||||
2
.github/workflows/qodana_code_quality.yml
vendored
@@ -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"
|
||||
|
||||
@@ -16,3 +16,4 @@ qodana.yaml
|
||||
# data
|
||||
!src/data/**/*.json
|
||||
src-tauri/gen/*.json
|
||||
!eslint
|
||||
|
||||
31
CHANGELOG.md
@@ -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)
|
||||
- 💄 帖子子回复取消保持,点击其他隐藏
|
||||
|
||||
13
README.md
@@ -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`
|
||||
|
||||
 
|
||||
|
||||
@@ -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.0,UIGF 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
|
||||
|
||||
|
||||
445
docs/UIGF.md
@@ -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
@@ -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 根对象"
|
||||
}
|
||||
```
|
||||
@@ -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",
|
||||
@@ -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];
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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"],
|
||||
},
|
||||
};
|
||||
|
||||
58
package.json
@@ -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
BIN
public/WIKI/character/10000104.webp
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
public/WIKI/character/10000105.webp
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
public/WIKI/weapon/11432.webp
Normal file
|
After Width: | Height: | Size: 8.2 KiB |
BIN
public/WIKI/weapon/14430.webp
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
public/WIKI/weapon/15430.webp
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
public/WIKI/weapon/15514.webp
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
public/icon/achievement/UI_AchievementIcon_A019_Part2.webp
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
public/icon/achievement/UI_AchievementIcon_B007_Part2.webp
Normal file
|
After Width: | Height: | Size: 9.1 KiB |
BIN
public/icon/constellations/UI_Talent_S_Chasca_01.webp
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
public/icon/constellations/UI_Talent_S_Chasca_02.webp
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
public/icon/constellations/UI_Talent_S_Chasca_03.webp
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
public/icon/constellations/UI_Talent_S_Chasca_04.webp
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
public/icon/constellations/UI_Talent_S_Olorun_01.webp
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
public/icon/constellations/UI_Talent_S_Olorun_02.webp
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
public/icon/constellations/UI_Talent_S_Olorun_03.webp
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
public/icon/constellations/UI_Talent_S_Olorun_04.webp
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
public/icon/constellations/UI_Talent_U_Chasca_01.webp
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
public/icon/constellations/UI_Talent_U_Chasca_02.webp
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
public/icon/constellations/UI_Talent_U_Olorun_01.webp
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
public/icon/constellations/UI_Talent_U_Olorun_02.webp
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
public/icon/material/101252.webp
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
BIN
public/icon/material/101253.webp
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
public/icon/material/112116.webp
Normal file
|
After Width: | Height: | Size: 6.6 KiB |
BIN
public/icon/material/112117.webp
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
public/icon/material/112118.webp
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
public/icon/material/113067.webp
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
public/icon/material/140016.webp
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
public/icon/star/combat0.webp
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
public/icon/star/combat1.webp
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
public/icon/talents/Skill_E_Chasca_01.webp
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
public/icon/talents/Skill_E_Olorun_01.webp
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
public/icon/talents/Skill_S_Chasca_01.webp
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
public/icon/talents/Skill_S_Olorun_01.webp
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
public/icon/talents/UI_Talent_S_Chasca_05.webp
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
public/icon/talents/UI_Talent_S_Chasca_06.webp
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
public/icon/talents/UI_Talent_S_Chasca_07.webp
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
public/icon/talents/UI_Talent_S_Chasca_08.webp
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
public/icon/talents/UI_Talent_S_Olorun_05.webp
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
public/icon/talents/UI_Talent_S_Olorun_06.webp
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
public/icon/talents/UI_Talent_S_Olorun_07.webp
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
public/icon/talents/UI_Talent_S_Olorun_08.webp
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
public/source/UI/combatCrown.webp
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
public/source/UI/userCombat.webp
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
public/source/nameCard/bg/恰斯卡·全弹.webp
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
public/source/nameCard/bg/成就·战意.webp
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
public/source/nameCard/bg/欧洛伦·夜翳.webp
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
public/source/nameCard/bg/纪行·灵织.webp
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
public/source/nameCard/bg/纳塔·空梦.webp
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
public/source/nameCard/icon/恰斯卡·全弹.webp
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
BIN
public/source/nameCard/icon/成就·战意.webp
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
public/source/nameCard/icon/欧洛伦·夜翳.webp
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
BIN
public/source/nameCard/icon/纪行·灵织.webp
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
public/source/nameCard/icon/纳塔·空梦.webp
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
BIN
public/source/nameCard/profile/恰斯卡·全弹.webp
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
public/source/nameCard/profile/成就·战意.webp
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
public/source/nameCard/profile/欧洛伦·夜翳.webp
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
public/source/nameCard/profile/纪行·灵织.webp
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
public/source/nameCard/profile/纳塔·空梦.webp
Normal file
|
After Width: | Height: | Size: 63 KiB |
BIN
public/source/post/tp_uid_bg.webp
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
1011
src-tauri/Cargo.lock
generated
@@ -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 插件
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -78,6 +78,9 @@
|
||||
},
|
||||
{
|
||||
"url": "https://*.hoyoverse.com/*"
|
||||
},
|
||||
{
|
||||
"url": "https://*.genshinnet.com/*"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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/*" }
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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",
|
||||
|
||||
38
src/App.vue
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
113
src/components/func/dialog.ts
Normal 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;
|
||||
294
src/components/func/dialog.vue
Normal 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>
|
||||
@@ -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,
|
||||
|
||||