feat(TokenImport): 增加 URL 获取 Token 功能并优化手动输入流程

- 新增 URL表单,支持从 API 接口获取 Token
- 优化手动输入表单,增加自定义连接地址选项
- 移除等级、职业等可选信息
- 添加主题切换功能
-调整页面样式,适配暗黑主题
This commit is contained in:
steve
2025-09-02 21:22:54 +08:00
parent 453544e9b3
commit 74dfb4ec32
4 changed files with 540 additions and 148 deletions

View File

@@ -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] 支持远端获取TokenURL接口方式
- [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)
--- ---

View File

@@ -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;
} }
/* 滚动条样式 */ /* 滚动条样式 */

View File

@@ -4,13 +4,30 @@
<!-- 页面头部 --> <!-- 页面头部 -->
<div class="page-header"> <div class="page-header">
<div class="header-content"> <div class="header-content">
<img <div class="header-top">
src="/icons/xiaoyugan.png" <img
alt="XYZW" src="/icons/xiaoyugan.png"
class="brand-logo" alt="XYZW"
> 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"
size="small" class="import-method-tabs"
:show-icon="false" size="small"
> >
<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,13 +320,28 @@
</span> </span>
</div> </div>
<n-button <div class="connection-actions">
size="small" <n-button
:type="getConnectionStatus(token.id) === 'connected' ? 'warning' : 'primary'" v-if="token.sourceUrl"
@click.stop="toggleConnection(token)" size="small"
> type="default"
{{ getConnectionStatus(token.id) === 'connected' ? '断开' : '连接' }} :loading="refreshingTokens.has(token.id)"
</n-button> @click.stop="refreshToken(token)"
>
<template #icon>
<n-icon><Refresh /></n-icon>
</template>
刷新
</n-button>
<n-button
size="small"
:type="getConnectionStatus(token.id) === 'connected' ? 'warning' : 'primary'"
@click.stop="toggleConnection(token)"
>
{{ getConnectionStatus(token.id) === 'connected' ? '断开' : '连接' }}
</n-button>
</div>
</div> </div>
<div class="token-timestamps"> <div class="token-timestamps">
@@ -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] = ''
importForm[key] = 1 })
} else { }
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;
code { font-weight: var(--font-weight-normal);
background: rgba(24, 160, 88, 0.1);
color: var(--success-color);
padding: 2px 4px;
border-radius: 4px;
font-size: 12px;
word-break: break-all;
}
} }
.import-method-tabs {
margin-top: var(--spacing-md);
display: flex;
justify-content: center;
}
}
.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 {