feat(TokenImport): 增加 URL 获取 Token 功能并优化手动输入流程
- 新增 URL表单,支持从 API 接口获取 Token - 优化手动输入表单,增加自定义连接地址选项 - 移除等级、职业等可选信息 - 添加主题切换功能 -调整页面样式,适配暗黑主题
This commit is contained in:
48
README.md
48
README.md
@@ -21,10 +21,12 @@
|
|||||||
## ✨ 核心特性
|
## ✨ 核心特性
|
||||||
|
|
||||||
### 🔐 Token管理系统
|
### 🔐 Token管理系统
|
||||||
|
- **双重导入方式**:支持手动输入和URL接口获取两种Token导入方式
|
||||||
- **Base64解码支持**:自动识别和解析多种Base64格式的游戏Token
|
- **Base64解码支持**:自动识别和解析多种Base64格式的游戏Token
|
||||||
- **多角色管理**:同时管理多个游戏账号,支持角色信息展示
|
- **多角色管理**:同时管理多个游戏账号,支持角色信息展示
|
||||||
- **本地存储**:安全的本地数据存储,无需后端服务器
|
- **本地存储**:安全的本地数据存储,无需后端服务器
|
||||||
- **Token验证**:自动验证Token有效性和格式完整性
|
- **Token验证**:自动验证Token有效性和格式完整性
|
||||||
|
- **自动刷新**:支持URL获取的Token自动刷新功能
|
||||||
|
|
||||||
### 🌐 WebSocket通信
|
### 🌐 WebSocket通信
|
||||||
- **BON协议支持**:内置Binary Object Notation协议编解码
|
- **BON协议支持**:内置Binary Object Notation协议编解码
|
||||||
@@ -147,10 +149,12 @@ npm run format # 代码格式化
|
|||||||
|
|
||||||
### 1. Token导入与管理
|
### 1. Token导入与管理
|
||||||
|
|
||||||
#### 支持的Token格式
|
#### 支持的导入方式
|
||||||
|
|
||||||
|
##### 方式一:手动输入
|
||||||
|
支持多种Base64格式的Token字符串:
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
|
|
||||||
|
|
||||||
// 纯Base64格式
|
// 纯Base64格式
|
||||||
"eyJ0b2tlbiI6ImFiY2QxMjM0In0="
|
"eyJ0b2tlbiI6ImFiY2QxMjM0In0="
|
||||||
|
|
||||||
@@ -158,13 +162,32 @@ npm run format # 代码格式化
|
|||||||
"token:eyJ0b2tlbiI6ImFiY2QxMjM0In0="
|
"token:eyJ0b2tlbiI6ImFiY2QxMjM0In0="
|
||||||
```
|
```
|
||||||
|
|
||||||
|
##### 方式二:URL接口获取
|
||||||
|
通过API接口自动获取Token,支持定时刷新:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// API接口返回格式
|
||||||
|
{
|
||||||
|
"token": "eyJ0b2tlbiI6ImFiY2QxMjM0In0=", // 必需字段
|
||||||
|
"server": "风云服" // 可选字段
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
#### 导入步骤
|
#### 导入步骤
|
||||||
1. 进入 **Token管理** 页面
|
1. 进入 **Token管理** 页面
|
||||||
2. 选择导入方式(文件上传/文本粘贴/手动输入)
|
2. 选择导入方式:
|
||||||
|
- **手动输入**:粘贴Base64编码的Token字符串
|
||||||
|
- **URL获取**:输入Token获取接口地址
|
||||||
3. 系统自动解析和验证Token格式
|
3. 系统自动解析和验证Token格式
|
||||||
4. 设置角色名称和基本信息
|
4. 设置角色名称和基本信息
|
||||||
5. 保存到本地存储
|
5. 保存到本地存储
|
||||||
|
|
||||||
|
#### Token刷新功能
|
||||||
|
- 通过URL方式导入的Token支持一键刷新
|
||||||
|
- 刷新时会重新请求原API接口获取最新Token
|
||||||
|
- 自动重新建立WebSocket连接
|
||||||
|
- 保持角色信息和配置不变
|
||||||
|
|
||||||
### 2. WebSocket连接配置
|
### 2. WebSocket连接配置
|
||||||
|
|
||||||
纯本地连接存储,不用担心封号及账号泄漏风险
|
纯本地连接存储,不用担心封号及账号泄漏风险
|
||||||
@@ -444,6 +467,21 @@ git push origin feature/new-feature
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 🗓️ 版本更新计划
|
||||||
|
|
||||||
|
### v2.1.0 (计划中 - Q4 2025)
|
||||||
|
- 🎯 **自动化增强**
|
||||||
|
- [ ] 智能任务调度系统
|
||||||
|
- [ ] 增加账号批量管理界面
|
||||||
|
- [ ] 界面已有bug修复
|
||||||
|
|
||||||
|
- 🔧 **功能扩展**
|
||||||
|
- [ ] 支持每日任务一件完成
|
||||||
|
- [x] 支持远端获取Token(URL接口方式)
|
||||||
|
- [x] 支持Token自动刷新功能
|
||||||
|
- [ ] 支持定时任务抢购符咒
|
||||||
|
- [ ] 支持自定义脚本生成
|
||||||
|
|
||||||
## 📄 许可证
|
## 📄 许可证
|
||||||
|
|
||||||
本项目基于 [Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License](LICENSE) 许可证。
|
本项目基于 [Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License](LICENSE) 许可证。
|
||||||
@@ -461,7 +499,7 @@ git push origin feature/new-feature
|
|||||||
|
|
||||||
- **项目主页**:[GitHub Repository](https://github.com/w1249178256/xyzw_web_helper)
|
- **项目主页**:[GitHub Repository](https://github.com/w1249178256/xyzw_web_helper)
|
||||||
- **问题反馈**:[GitHub Issues](https://github.com/w1249178256/xyzw_web_helper/issues)
|
- **问题反馈**:[GitHub Issues](https://github.com/w1249178256/xyzw_web_helper/issues)
|
||||||
- **联系邮箱**:[MAIL](stevefeng59@gmail.com)
|
- **联系邮箱**:[发邮件给我](mailto:stevefeng59@gmail.com)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
72
src/App.vue
72
src/App.vue
@@ -28,20 +28,88 @@ const checkThemePreference = () => {
|
|||||||
|
|
||||||
if (savedTheme === 'dark' || (!savedTheme && prefersDark)) {
|
if (savedTheme === 'dark' || (!savedTheme && prefersDark)) {
|
||||||
theme.value = darkTheme
|
theme.value = darkTheme
|
||||||
|
document.documentElement.classList.add('dark')
|
||||||
} else {
|
} else {
|
||||||
theme.value = null
|
theme.value = null
|
||||||
|
document.documentElement.classList.remove('dark')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 监听系统主题变化
|
||||||
|
const setupThemeListener = () => {
|
||||||
|
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
|
||||||
|
mediaQuery.addListener(() => {
|
||||||
|
const savedTheme = localStorage.getItem('theme')
|
||||||
|
// 只有在用户没有手动设置主题时才跟随系统
|
||||||
|
if (!savedTheme) {
|
||||||
|
checkThemePreference()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
checkThemePreference()
|
checkThemePreference()
|
||||||
|
setupThemeListener()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
/* 主题变量 */
|
||||||
|
:root {
|
||||||
|
--app-background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
--text-color: #333;
|
||||||
|
--text-secondary: #666;
|
||||||
|
--text-tertiary: #999;
|
||||||
|
--bg-color: #ffffff;
|
||||||
|
--border-color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 深色主题变量 */
|
||||||
|
.dark {
|
||||||
|
--app-background: linear-gradient(135deg, #2d3748 0%, #4a5568 100%);
|
||||||
|
--text-color: #ffffff !important;
|
||||||
|
--text-secondary: #cbd5e0 !important;
|
||||||
|
--text-tertiary: #a0aec0 !important;
|
||||||
|
--bg-color: #1a202c !important;
|
||||||
|
--border-color: #4a5568 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 强制深色主题样式 - 更具体的选择器 */
|
||||||
|
html.dark,
|
||||||
|
html.dark body,
|
||||||
|
html.dark #app,
|
||||||
|
html.dark * {
|
||||||
|
color: #ffffff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark .n-form-item-label,
|
||||||
|
html.dark .n-form-item-label__text,
|
||||||
|
html.dark .n-input,
|
||||||
|
html.dark .n-input__input,
|
||||||
|
html.dark .n-input__textarea,
|
||||||
|
html.dark .n-collapse-item__header,
|
||||||
|
html.dark .n-radio-button,
|
||||||
|
html.dark .n-card,
|
||||||
|
html.dark .n-card__content,
|
||||||
|
html.dark h1,
|
||||||
|
html.dark h2,
|
||||||
|
html.dark h3,
|
||||||
|
html.dark p,
|
||||||
|
html.dark span,
|
||||||
|
html.dark div {
|
||||||
|
color: #ffffff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark .n-input__placeholder,
|
||||||
|
html.dark ::placeholder {
|
||||||
|
color: rgba(255, 255, 255, 0.6) !important;
|
||||||
|
}
|
||||||
|
|
||||||
#app {
|
#app {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background: var(--app-background, linear-gradient(135deg, #667eea 0%, #764ba2 100%));
|
background: var(--app-background);
|
||||||
|
color: var(--text-color);
|
||||||
|
transition: background 0.3s ease, color 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 全局样式重置 */
|
/* 全局样式重置 */
|
||||||
@@ -54,6 +122,8 @@ onMounted(() => {
|
|||||||
html, body {
|
html, body {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
font-family: 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
font-family: 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||||
|
color: var(--text-color);
|
||||||
|
transition: color 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 滚动条样式 */
|
/* 滚动条样式 */
|
||||||
|
|||||||
@@ -4,13 +4,30 @@
|
|||||||
<!-- 页面头部 -->
|
<!-- 页面头部 -->
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<div class="header-content">
|
<div class="header-content">
|
||||||
|
<div class="header-top">
|
||||||
<img
|
<img
|
||||||
src="/icons/xiaoyugan.png"
|
src="/icons/xiaoyugan.png"
|
||||||
alt="XYZW"
|
alt="XYZW"
|
||||||
class="brand-logo"
|
class="brand-logo"
|
||||||
>
|
>
|
||||||
|
<!-- 主题切换按钮 -->
|
||||||
|
<n-button
|
||||||
|
circle
|
||||||
|
size="small"
|
||||||
|
@click="toggleTheme"
|
||||||
|
class="theme-toggle"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<n-icon v-if="isDarkTheme">
|
||||||
|
<Sunny />
|
||||||
|
</n-icon>
|
||||||
|
<n-icon v-else>
|
||||||
|
<Moon />
|
||||||
|
</n-icon>
|
||||||
|
</template>
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
<h1>游戏Token管理</h1>
|
<h1>游戏Token管理</h1>
|
||||||
<p>导入您的游戏Token开始自动化任务管理</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -23,56 +40,65 @@
|
|||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h2>
|
<h2>
|
||||||
<n-icon><Add /></n-icon>
|
<n-icon><Add /></n-icon>
|
||||||
导入游戏Token
|
添加游戏Token
|
||||||
</h2>
|
</h2>
|
||||||
<p>支持Base64编码的Token字符串导入</p>
|
|
||||||
<div class="help-info">
|
<!-- 导入方式选择 -->
|
||||||
<n-alert
|
<n-radio-group
|
||||||
type="info"
|
v-model:value="importMethod"
|
||||||
|
class="import-method-tabs"
|
||||||
size="small"
|
size="small"
|
||||||
:show-icon="false"
|
|
||||||
>
|
>
|
||||||
<strong>WebSocket连接:</strong>系统会自动将解码后的Token带入到固定地址:<br>
|
<n-radio-button value="manual">
|
||||||
<code>wss://xxz-xyzw.hortorgames.com/agent?p=[您的Token]&e=x&lang=chinese</code>
|
手动输入
|
||||||
</n-alert>
|
</n-radio-button>
|
||||||
</div>
|
<n-radio-button value="url">
|
||||||
|
URL获取
|
||||||
|
</n-radio-button>
|
||||||
|
</n-radio-group>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 手动输入表单 -->
|
||||||
<n-form
|
<n-form
|
||||||
|
v-if="importMethod === 'manual'"
|
||||||
ref="importFormRef"
|
ref="importFormRef"
|
||||||
:model="importForm"
|
:model="importForm"
|
||||||
:rules="importRules"
|
:rules="importRules"
|
||||||
label-placement="top"
|
:label-placement="'top'"
|
||||||
size="large"
|
:size="'large'"
|
||||||
|
:show-label="true"
|
||||||
>
|
>
|
||||||
<n-form-item
|
<n-form-item
|
||||||
label="Token名称"
|
:label="'游戏角色名称'"
|
||||||
path="name"
|
:path="'name'"
|
||||||
|
:show-label="true"
|
||||||
>
|
>
|
||||||
<n-input
|
<n-input
|
||||||
v-model:value="importForm.name"
|
v-model:value="importForm.name"
|
||||||
placeholder="为这个Token起个名字,例如:主号战士"
|
placeholder="例如:主号战士"
|
||||||
clearable
|
clearable
|
||||||
/>
|
/>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
|
|
||||||
<n-form-item
|
<n-form-item
|
||||||
label="Base64 Token"
|
:label="'Token字符串'"
|
||||||
path="base64Token"
|
:path="'base64Token'"
|
||||||
|
:show-label="true"
|
||||||
>
|
>
|
||||||
<n-input
|
<n-input
|
||||||
v-model:value="importForm.base64Token"
|
v-model:value="importForm.base64Token"
|
||||||
type="textarea"
|
type="textarea"
|
||||||
:rows="4"
|
:rows="3"
|
||||||
placeholder="粘贴您的Base64编码Token字符串..."
|
placeholder="粘贴Token字符串..."
|
||||||
clearable
|
clearable
|
||||||
/>
|
/>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
|
|
||||||
<!-- 可选信息 -->
|
<!-- 角色详情 -->
|
||||||
<n-collapse>
|
<n-collapse>
|
||||||
<n-collapse-item
|
<n-collapse-item
|
||||||
title="可选信息"
|
title="角色详情 (可选)"
|
||||||
name="optional"
|
name="optional"
|
||||||
>
|
>
|
||||||
<div class="optional-fields">
|
<div class="optional-fields">
|
||||||
@@ -83,38 +109,17 @@
|
|||||||
/>
|
/>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
|
|
||||||
<n-form-item label="等级">
|
<n-form-item label="自定义连接地址">
|
||||||
<n-input-number
|
|
||||||
v-model:value="importForm.level"
|
|
||||||
:min="1"
|
|
||||||
:max="200"
|
|
||||||
placeholder="角色等级"
|
|
||||||
/>
|
|
||||||
</n-form-item>
|
|
||||||
|
|
||||||
<n-form-item label="职业">
|
|
||||||
<n-select
|
|
||||||
v-model:value="importForm.profession"
|
|
||||||
:options="professionOptions"
|
|
||||||
placeholder="选择职业"
|
|
||||||
/>
|
|
||||||
</n-form-item>
|
|
||||||
|
|
||||||
<n-form-item label="WebSocket URL (可选)">
|
|
||||||
<n-input
|
<n-input
|
||||||
v-model:value="importForm.wsUrl"
|
v-model:value="importForm.wsUrl"
|
||||||
placeholder="留空将使用默认地址: wss://xxz-xyzw.hortorgames.com/agent"
|
placeholder="留空使用默认连接"
|
||||||
/>
|
/>
|
||||||
<template #feedback>
|
|
||||||
<span style="color: var(--text-tertiary); font-size: 12px;">
|
|
||||||
默认会自动使用游戏服务器地址,一般情况下无需填写
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
</div>
|
</div>
|
||||||
</n-collapse-item>
|
</n-collapse-item>
|
||||||
</n-collapse>
|
</n-collapse>
|
||||||
|
|
||||||
|
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
<n-button
|
<n-button
|
||||||
type="primary"
|
type="primary"
|
||||||
@@ -126,7 +131,92 @@
|
|||||||
<template #icon>
|
<template #icon>
|
||||||
<n-icon><CloudUpload /></n-icon>
|
<n-icon><CloudUpload /></n-icon>
|
||||||
</template>
|
</template>
|
||||||
导入Token
|
添加Token
|
||||||
|
</n-button>
|
||||||
|
|
||||||
|
<n-button
|
||||||
|
v-if="tokenStore.hasTokens"
|
||||||
|
size="large"
|
||||||
|
block
|
||||||
|
@click="showImportForm = false"
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
</n-form>
|
||||||
|
|
||||||
|
<!-- URL获取表单 -->
|
||||||
|
<n-form
|
||||||
|
v-if="importMethod === 'url'"
|
||||||
|
ref="urlFormRef"
|
||||||
|
:model="urlForm"
|
||||||
|
:rules="urlRules"
|
||||||
|
label-placement="top"
|
||||||
|
size="large"
|
||||||
|
>
|
||||||
|
<n-form-item
|
||||||
|
label="游戏角色名称"
|
||||||
|
path="name"
|
||||||
|
>
|
||||||
|
<n-input
|
||||||
|
v-model:value="urlForm.name"
|
||||||
|
placeholder="例如:主号战士"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
|
||||||
|
<n-form-item
|
||||||
|
label="Token获取地址"
|
||||||
|
path="url"
|
||||||
|
>
|
||||||
|
<n-input
|
||||||
|
v-model:value="urlForm.url"
|
||||||
|
placeholder="输入API接口地址..."
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
<template #feedback>
|
||||||
|
<span class="form-tip">
|
||||||
|
接口应返回包含token字段的JSON数据
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</n-form-item>
|
||||||
|
|
||||||
|
<!-- 角色详情 -->
|
||||||
|
<n-collapse>
|
||||||
|
<n-collapse-item
|
||||||
|
title="角色详情 (可选)"
|
||||||
|
name="optional"
|
||||||
|
>
|
||||||
|
<div class="optional-fields">
|
||||||
|
<n-form-item label="服务器">
|
||||||
|
<n-input
|
||||||
|
v-model:value="urlForm.server"
|
||||||
|
placeholder="服务器名称"
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
|
||||||
|
<n-form-item label="自定义连接地址">
|
||||||
|
<n-input
|
||||||
|
v-model:value="urlForm.wsUrl"
|
||||||
|
placeholder="留空使用默认连接"
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
</div>
|
||||||
|
</n-collapse-item>
|
||||||
|
</n-collapse>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<n-button
|
||||||
|
type="primary"
|
||||||
|
size="large"
|
||||||
|
block
|
||||||
|
:loading="isImporting"
|
||||||
|
@click="handleUrlImport"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<n-icon><CloudUpload /></n-icon>
|
||||||
|
</template>
|
||||||
|
获取并添加Token
|
||||||
</n-button>
|
</n-button>
|
||||||
|
|
||||||
<n-button
|
<n-button
|
||||||
@@ -196,14 +286,6 @@
|
|||||||
v-if="token.server"
|
v-if="token.server"
|
||||||
class="meta-item"
|
class="meta-item"
|
||||||
>{{ token.server }}</span>
|
>{{ token.server }}</span>
|
||||||
<span
|
|
||||||
v-if="token.level"
|
|
||||||
class="meta-item"
|
|
||||||
>Lv.{{ token.level }}</span>
|
|
||||||
<span
|
|
||||||
v-if="token.profession"
|
|
||||||
class="meta-item"
|
|
||||||
>{{ token.profession }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -238,6 +320,20 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="connection-actions">
|
||||||
|
<n-button
|
||||||
|
v-if="token.sourceUrl"
|
||||||
|
size="small"
|
||||||
|
type="default"
|
||||||
|
:loading="refreshingTokens.has(token.id)"
|
||||||
|
@click.stop="refreshToken(token)"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<n-icon><Refresh /></n-icon>
|
||||||
|
</template>
|
||||||
|
刷新
|
||||||
|
</n-button>
|
||||||
|
|
||||||
<n-button
|
<n-button
|
||||||
size="small"
|
size="small"
|
||||||
:type="getConnectionStatus(token.id) === 'connected' ? 'warning' : 'primary'"
|
:type="getConnectionStatus(token.id) === 'connected' ? 'warning' : 'primary'"
|
||||||
@@ -246,6 +342,7 @@
|
|||||||
{{ getConnectionStatus(token.id) === 'connected' ? '断开' : '连接' }}
|
{{ getConnectionStatus(token.id) === 'connected' ? '断开' : '连接' }}
|
||||||
</n-button>
|
</n-button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="token-timestamps">
|
<div class="token-timestamps">
|
||||||
<div class="timestamp-item">
|
<div class="timestamp-item">
|
||||||
@@ -325,20 +422,7 @@
|
|||||||
<n-form-item label="服务器">
|
<n-form-item label="服务器">
|
||||||
<n-input v-model:value="editForm.server" />
|
<n-input v-model:value="editForm.server" />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item label="等级">
|
<n-form-item label="WebSocket地址">
|
||||||
<n-input-number
|
|
||||||
v-model:value="editForm.level"
|
|
||||||
:min="1"
|
|
||||||
:max="200"
|
|
||||||
/>
|
|
||||||
</n-form-item>
|
|
||||||
<n-form-item label="职业">
|
|
||||||
<n-select
|
|
||||||
v-model:value="editForm.profession"
|
|
||||||
:options="professionOptions"
|
|
||||||
/>
|
|
||||||
</n-form-item>
|
|
||||||
<n-form-item label="WebSocket">
|
|
||||||
<n-input v-model:value="editForm.wsUrl" />
|
<n-input v-model:value="editForm.wsUrl" />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
</n-form>
|
</n-form>
|
||||||
@@ -361,7 +445,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, onMounted } from 'vue'
|
import { ref, reactive, onMounted, computed } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { useMessage, useDialog } from 'naive-ui'
|
import { useMessage, useDialog } from 'naive-ui'
|
||||||
import { useTokenStore } from '@/stores/tokenStore'
|
import { useTokenStore } from '@/stores/tokenStore'
|
||||||
@@ -370,7 +454,10 @@ import {
|
|||||||
CloudUpload,
|
CloudUpload,
|
||||||
Menu,
|
Menu,
|
||||||
EllipsisHorizontal,
|
EllipsisHorizontal,
|
||||||
Key
|
Key,
|
||||||
|
Refresh,
|
||||||
|
Sunny,
|
||||||
|
Moon
|
||||||
} from '@vicons/ionicons5'
|
} from '@vicons/ionicons5'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -383,16 +470,44 @@ const showImportForm = ref(false)
|
|||||||
const isImporting = ref(false)
|
const isImporting = ref(false)
|
||||||
const showEditModal = ref(false)
|
const showEditModal = ref(false)
|
||||||
const importFormRef = ref(null)
|
const importFormRef = ref(null)
|
||||||
|
const urlFormRef = ref(null)
|
||||||
const editFormRef = ref(null)
|
const editFormRef = ref(null)
|
||||||
const editingToken = ref(null)
|
const editingToken = ref(null)
|
||||||
|
const importMethod = ref('manual')
|
||||||
|
const refreshingTokens = ref(new Set())
|
||||||
|
|
||||||
|
// 主题控制
|
||||||
|
const isDarkTheme = computed(() => {
|
||||||
|
return document.documentElement.classList.contains('dark')
|
||||||
|
})
|
||||||
|
|
||||||
|
const toggleTheme = () => {
|
||||||
|
const isDark = document.documentElement.classList.contains('dark')
|
||||||
|
if (isDark) {
|
||||||
|
document.documentElement.classList.remove('dark')
|
||||||
|
localStorage.setItem('theme', 'light')
|
||||||
|
} else {
|
||||||
|
document.documentElement.classList.add('dark')
|
||||||
|
localStorage.setItem('theme', 'dark')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 触发全局主题更新
|
||||||
|
window.location.reload()
|
||||||
|
}
|
||||||
|
|
||||||
// 导入表单
|
// 导入表单
|
||||||
const importForm = reactive({
|
const importForm = reactive({
|
||||||
name: '',
|
name: '',
|
||||||
base64Token: '',
|
base64Token: '',
|
||||||
server: '',
|
server: '',
|
||||||
level: 1,
|
wsUrl: ''
|
||||||
profession: '',
|
})
|
||||||
|
|
||||||
|
// URL表单
|
||||||
|
const urlForm = reactive({
|
||||||
|
name: '',
|
||||||
|
url: '',
|
||||||
|
server: '',
|
||||||
wsUrl: ''
|
wsUrl: ''
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -400,8 +515,6 @@ const importForm = reactive({
|
|||||||
const editForm = reactive({
|
const editForm = reactive({
|
||||||
name: '',
|
name: '',
|
||||||
server: '',
|
server: '',
|
||||||
level: 1,
|
|
||||||
profession: '',
|
|
||||||
wsUrl: ''
|
wsUrl: ''
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -415,21 +528,22 @@ const importRules = {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const urlRules = {
|
||||||
|
name: [
|
||||||
|
{ required: true, message: '请输入Token名称', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
url: [
|
||||||
|
{ required: true, message: '请输入Token获取地址', trigger: 'blur' },
|
||||||
|
{ type: 'url', message: '请输入有效的URL地址', trigger: 'blur' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
const editRules = {
|
const editRules = {
|
||||||
name: [
|
name: [
|
||||||
{ required: true, message: '请输入Token名称', trigger: 'blur' }
|
{ required: true, message: '请输入Token名称', trigger: 'blur' }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
// 选项数据
|
|
||||||
const professionOptions = [
|
|
||||||
{ label: '战士', value: '战士' },
|
|
||||||
{ label: '法师', value: '法师' },
|
|
||||||
{ label: '道士', value: '道士' },
|
|
||||||
{ label: '刺客', value: '刺客' },
|
|
||||||
{ label: '弓手', value: '弓手' },
|
|
||||||
{ label: '牧师', value: '牧师' }
|
|
||||||
]
|
|
||||||
|
|
||||||
const bulkOptions = [
|
const bulkOptions = [
|
||||||
{ label: '导出所有Token', key: 'export' },
|
{ label: '导出所有Token', key: 'export' },
|
||||||
@@ -452,8 +566,6 @@ const handleImport = async () => {
|
|||||||
importForm.base64Token,
|
importForm.base64Token,
|
||||||
{
|
{
|
||||||
server: importForm.server,
|
server: importForm.server,
|
||||||
level: importForm.level,
|
|
||||||
profession: importForm.profession,
|
|
||||||
wsUrl: importForm.wsUrl
|
wsUrl: importForm.wsUrl
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -472,13 +584,107 @@ const handleImport = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// URL获取Token
|
||||||
|
const handleUrlImport = async () => {
|
||||||
|
if (!urlFormRef.value) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
await urlFormRef.value.validate()
|
||||||
|
isImporting.value = true
|
||||||
|
|
||||||
|
// 获取Token数据
|
||||||
|
const response = await fetch(urlForm.url)
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`请求失败: ${response.status} ${response.statusText}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
|
||||||
|
// 检查返回数据是否包含token
|
||||||
|
if (!data.token) {
|
||||||
|
throw new Error('返回数据中未找到token字段')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用获取到的token创建新的token记录
|
||||||
|
const result = tokenStore.importBase64Token(
|
||||||
|
urlForm.name,
|
||||||
|
data.token,
|
||||||
|
{
|
||||||
|
server: urlForm.server || data.server,
|
||||||
|
wsUrl: urlForm.wsUrl,
|
||||||
|
sourceUrl: urlForm.url // 保存源URL用于刷新
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
message.success(result.message)
|
||||||
|
resetUrlForm()
|
||||||
|
showImportForm.value = false
|
||||||
|
} else {
|
||||||
|
message.error(result.error || result.message)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('URL获取Token失败:', error)
|
||||||
|
message.error(error.message || 'URL获取Token失败')
|
||||||
|
} finally {
|
||||||
|
isImporting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新Token
|
||||||
|
const refreshToken = async (token) => {
|
||||||
|
if (!token.sourceUrl) {
|
||||||
|
message.warning('该Token未配置刷新地址')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshingTokens.value.add(token.id)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(token.sourceUrl)
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`请求失败: ${response.status} ${response.statusText}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
|
||||||
|
if (!data.token) {
|
||||||
|
throw new Error('返回数据中未找到token字段')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新token信息
|
||||||
|
tokenStore.updateToken(token.id, {
|
||||||
|
token: data.token,
|
||||||
|
server: data.server || token.server,
|
||||||
|
lastRefreshed: Date.now()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 如果当前token有连接,需要重新连接
|
||||||
|
if (tokenStore.getWebSocketStatus(token.id) === 'connected') {
|
||||||
|
tokenStore.closeWebSocketConnection(token.id)
|
||||||
|
setTimeout(() => {
|
||||||
|
tokenStore.createWebSocketConnection(token.id, data.token, token.wsUrl)
|
||||||
|
}, 500)
|
||||||
|
}
|
||||||
|
|
||||||
|
message.success('Token刷新成功')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('刷新Token失败:', error)
|
||||||
|
message.error(error.message || 'Token刷新失败')
|
||||||
|
} finally {
|
||||||
|
refreshingTokens.value.delete(token.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const resetImportForm = () => {
|
const resetImportForm = () => {
|
||||||
Object.keys(importForm).forEach(key => {
|
Object.keys(importForm).forEach(key => {
|
||||||
if (key === 'level') {
|
|
||||||
importForm[key] = 1
|
|
||||||
} else {
|
|
||||||
importForm[key] = ''
|
importForm[key] = ''
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const resetUrlForm = () => {
|
||||||
|
Object.keys(urlForm).forEach(key => {
|
||||||
|
urlForm[key] = ''
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -543,8 +749,6 @@ const editToken = (token) => {
|
|||||||
Object.assign(editForm, {
|
Object.assign(editForm, {
|
||||||
name: token.name,
|
name: token.name,
|
||||||
server: token.server || '',
|
server: token.server || '',
|
||||||
level: token.level || 1,
|
|
||||||
profession: token.profession || '',
|
|
||||||
wsUrl: token.wsUrl || ''
|
wsUrl: token.wsUrl || ''
|
||||||
})
|
})
|
||||||
showEditModal.value = true
|
showEditModal.value = true
|
||||||
@@ -559,8 +763,6 @@ const saveEdit = async () => {
|
|||||||
tokenStore.updateToken(editingToken.value.id, {
|
tokenStore.updateToken(editingToken.value.id, {
|
||||||
name: editForm.name,
|
name: editForm.name,
|
||||||
server: editForm.server,
|
server: editForm.server,
|
||||||
level: editForm.level,
|
|
||||||
profession: editForm.profession,
|
|
||||||
wsUrl: editForm.wsUrl
|
wsUrl: editForm.wsUrl
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -744,6 +946,23 @@ onMounted(() => {
|
|||||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.header-top {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-md);
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
.brand-logo {
|
.brand-logo {
|
||||||
width: 64px;
|
width: 64px;
|
||||||
height: 64px;
|
height: 64px;
|
||||||
@@ -798,19 +1017,84 @@ onMounted(() => {
|
|||||||
margin: 0 0 var(--spacing-md) 0;
|
margin: 0 0 var(--spacing-md) 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.help-info {
|
.subtitle {
|
||||||
margin-top: var(--spacing-md);
|
font-size: var(--font-size-md);
|
||||||
text-align: left;
|
color: var(--text-tertiary);
|
||||||
|
margin: 0;
|
||||||
|
font-weight: var(--font-weight-normal);
|
||||||
|
}
|
||||||
|
|
||||||
code {
|
.import-method-tabs {
|
||||||
background: rgba(24, 160, 88, 0.1);
|
margin-top: var(--spacing-md);
|
||||||
color: var(--success-color);
|
display: flex;
|
||||||
padding: 2px 4px;
|
justify-content: center;
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 12px;
|
|
||||||
word-break: break-all;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-tip {
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-xs);
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 深色主题强制覆盖 */
|
||||||
|
.dark .n-form-item-label,
|
||||||
|
.dark .n-form-item-label__text {
|
||||||
|
color: #ffffff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .n-input__input,
|
||||||
|
.dark .n-input__textarea {
|
||||||
|
color: #ffffff !important;
|
||||||
|
background-color: rgba(255, 255, 255, 0.1) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .n-input__placeholder {
|
||||||
|
color: rgba(255, 255, 255, 0.5) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .n-card {
|
||||||
|
background-color: rgba(255, 255, 255, 0.1) !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .import-card {
|
||||||
|
background: rgba(45, 55, 72, 0.9) !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .import-card h2 {
|
||||||
|
color: #ffffff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .import-card .subtitle {
|
||||||
|
color: rgba(255, 255, 255, 0.7) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .n-collapse-item__header {
|
||||||
|
color: #ffffff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .n-collapse-item__content-wrapper {
|
||||||
|
background-color: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .n-radio-button {
|
||||||
|
color: #ffffff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .n-radio-button--checked {
|
||||||
|
background-color: rgba(16, 185, 129, 0.8) !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .form-tip {
|
||||||
|
color: rgba(255, 255, 255, 0.6) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.optional-fields {
|
.optional-fields {
|
||||||
|
|||||||
Reference in New Issue
Block a user