update:优化页面样式

This commit is contained in:
hrz
2025-02-15 16:17:08 +08:00
parent 2379cdc3ba
commit 2901506157
13 changed files with 80257 additions and 74 deletions
+2 -1
View File
@@ -21,7 +21,8 @@ manager:
enabled: false
ip: 0.0.0.0
port: 8001
token: password
# 请把密码设置10位数,且不能包含xiaozhi这几个字符
password: 123456
xiaozhi:
type: hello
+41 -11
View File
@@ -1,35 +1,61 @@
import logging
import os
from aiohttp import web
from core.utils.util import get_local_ip, get_project_dir
from manager.api.prompt import PromptHandler
from core.utils.util import get_local_ip, get_project_dir, check_password
from manager.api.prompt import PromptApi
from manager.api.auth import AuthApi
from aiohttp.web_middlewares import middleware
logger = logging.getLogger(__name__)
async def handle_options(request):
headers = {
@middleware
async def cors_middleware(request, handler):
# 预检请求处理
if request.method == 'OPTIONS':
return web.Response(
status=204,
headers={
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization, Token',
'Access-Control-Max-Age': '86400',
}
)
try:
response = await handler(request)
except web.HTTPException as ex:
response = ex
# 添加CORS头到所有响应
cors_headers = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization'
'Access-Control-Expose-Headers': 'Content-Type, Authorization, Token',
'Vary': 'Origin' # 避免缓存问题
}
return web.Response(headers=headers)
response.headers.update(cors_headers)
return response
class ConfigServer:
def __init__(self, config: dict):
self.config = config
self.app = web.Application()
self.app = web.Application(middlewares=[cors_middleware]) # 注册中间件
# 初始化接口处理器
self.prompt_handler = PromptHandler(config)
self.prompt_handler = PromptApi(config)
self.auth_handler = AuthApi(config)
self.setup_routes()
def setup_routes(self):
# 注册prompt接口
self.app.router.add_get('/api/prompt', self.prompt_handler.get_prompt)
self.app.router.add_post('/api/prompt', self.prompt_handler.update_prompt)
self.app.router.add_options('/api/prompt', handle_options)
# 注册auth接口
self.app.router.add_post('/api/login', self.auth_handler.login)
# 注册静态文件路由
static_dir = os.path.join(get_project_dir(), 'manager/static') # 获取static目录绝对路径
@@ -49,13 +75,17 @@ class ConfigServer:
if not http_config.get('enabled', False):
logger.info("HTTP server is disabled")
return
token = self.config['manager']['token']
if not check_password(token):
logger.info("您设置的后台密码太弱了,启动后台管理失败!")
return
runner = web.AppRunner(self.app)
await runner.setup()
site = web.TCPSite(runner, http_config['ip'], http_config['port'])
await site.start()
logger.info(
f"Config HTTP server is running at http://{get_local_ip()}:{http_config['port']}/manager/index.html")
f"Config HTTP server is running at http://{get_local_ip()}:{http_config['port']}/manager/login.html")
return runner # 返回runner以便后续清理
except Exception as e:
logger.error(f"Failed to start HTTP server: {e}")
+30
View File
@@ -1,4 +1,5 @@
import os
import re
import json
import yaml
import socket
@@ -91,3 +92,32 @@ def remove_punctuation_and_length(text):
if result == "Yeah":
return 0
return len(result), result
def check_password(password):
"""
检查密码是否满足以下条件:
1. 密码长度大于八位。
2. 密码包含英文和数字。
3. 密码不能包含“xiaozhi”字符。
:param password: 要检查的密码
:return: 如果密码满足条件,则返回True;否则返回False。
"""
# 检查密码长度
if len(password) < 10:
return False
# 检查是否包含英文字符和数字
if not re.search(r'[A-Za-z]', password) or not re.search(r'[0-9]', password):
return False
# 检查是否包含“xiaozhi”字符
if "xiaozhi" in password:
return False
if "123456" in password:
return False
# 如果满足所有条件,则返回True
return True
+21 -1
View File
@@ -1,10 +1,30 @@
import uuid
from manager.api.response import response_success, response_error
async def verify_token(config, request):
if 'token' not in config['manager']:
return True
expected_token = config['manager']['token']
token = request.headers.get('Authorization', '').replace('Bearer ', '')
if not token or token != expected_token:
return False
return True
class AuthApi:
def __init__(self, config):
self.config = config
async def login(self, request):
try:
data = await request.json()
if 'password' not in data:
return response_error("密码不能为空")
# 通过config参数回传修改能力
if self.config['manager']['token'] == data['password']:
return response_success(data=str(uuid.uuid4()))
return response_error("密码不正确")
except Exception as e:
return response_error(str(e))
+7 -7
View File
@@ -1,17 +1,18 @@
import logging
from aiohttp import web
from manager.api.auth import verify_token
from manager.api.response import response_unauthorized, response_success, response_error
logger = logging.getLogger(__name__)
class PromptHandler:
class PromptApi:
def __init__(self, config):
self.config = config
async def get_prompt(self, request):
if not await verify_token(self.config, request):
return web.json_response({'error': 'Unauthorized'}, status=401)
return response_unauthorized()
return web.json_response({
'prompt': self.config['prompt'],
@@ -20,17 +21,16 @@ class PromptHandler:
async def update_prompt(self, request):
if not await verify_token(self.config, request):
return web.json_response({'error': 'Unauthorized'}, status=401)
return response_unauthorized()
try:
data = await request.json()
if 'prompt' not in data:
return web.json_response({'error': 'Missing prompt field'}, status=400)
return response_success()
# 通过config参数回传修改能力
self.config['prompt'] = data['prompt']
return web.json_response({'success': True}, headers={'Access-Control-Allow-Origin': '*'})
return response_success()
except Exception as e:
logger.error(f"Failed to update prompt: {e}")
return web.json_response({'error': str(e)}, status=500)
return response_error(str(e))
+16
View File
@@ -0,0 +1,16 @@
from aiohttp import web
from typing import Optional, Dict, Any # 导入Optional用于表示可选类型
def response_error(msg):
return web.json_response({"code": -1, 'msg': msg}, status=200)
def response_success(msg: str = '', data: Optional[Any] = None):
if data is None:
data = {}
return web.json_response({"code": 0, 'msg': msg, 'data': data}, status=200)
def response_unauthorized():
return web.json_response({"code": 401}, status=200)
File diff suppressed because one or more lines are too long
+185 -51
View File
@@ -1,53 +1,187 @@
<!DOCTYPE html>
<html>
<head>
<title>小智提示词配置</title>
<meta charset="utf-8">
<style>
body { max-width: 800px; margin: 20px auto; padding: 0 20px; font-family: Arial, sans-serif; }
textarea { width: 100%; height: 300px; margin: 20px 0; padding: 10px; }
button { padding: 10px 20px; font-size: 16px; }
.success { color: green; }
.error { color: red; }
</style>
</head>
<body>
<h1>小智提示词配置</h1>
<textarea id="prompt"></textarea>
<div>
<button onclick="updatePrompt()">更新提示词</button>
<span id="status"></span>
</div>
<script>
// 加载当前prompt
fetch('/api/prompt')
.then(response => response.json())
.then(data => {
document.getElementById('prompt').value = data.prompt;
});
<html>
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1.0"/>
<script src="js/vue3.5.13.js"></script>
<link rel="stylesheet" href="css/element-plus2.9.4.css">
<script src="js/element-plus2.9.4.js"></script>
<script src="js/icons-vue2.3.1.js"></script>
<script src="js/common.js"></script>
<title>小智-server</title>
<style>
body {
margin: 0;
padding: 0;
background: linear-gradient(135deg, #1a1f25 0%, #0d1117 100%);
min-height: 100vh;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
}
function updatePrompt() {
const prompt = document.getElementById('prompt').value;
fetch('/api/prompt', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({prompt: prompt})
})
.then(response => response.json())
.then(data => {
const status = document.getElementById('status');
if(data.success) {
status.textContent = '更新成功!';
status.className = 'success';
} else {
status.textContent = '更新失败: ' + data.error;
status.className = 'error';
}
setTimeout(() => status.textContent = '', 3000);
});
}
</script>
</body>
</html>
.header {
background: rgba(13, 17, 23, 0.8);
backdrop-filter: blur(10px);
color: #fff;
padding: 1.5rem 2rem;
font-size: 1.4rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
display: flex;
align-items: center;
gap: 1rem;
}
.header-logo {
width: 32px;
height: 32px;
background: linear-gradient(45deg, #00c6fb 0%, #005bea 100%);
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
}
.content-container {
max-width: 1200px;
margin: 20px auto;
padding: 2rem;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.role-title {
font-size: 16px;
font-weight: bold;
margin-bottom: 1rem;
}
.role-id {
color: #333;
margin-bottom: 1rem;
}
.role-description {
margin: 1rem 0;
}
.character-count {
text-align: right;
color: #999;
font-size: 12px;
margin-top: 4px;
}
.button-group {
margin-top: 1rem;
}
.note {
color: #909399;
font-size: 12px;
margin-top: 1rem;
}
.footer {
text-align: center;
color: rgba(255, 255, 255, 0.6);
font-size: 13px;
padding: 20px;
position: fixed;
bottom: 0;
width: 100%;
}
</style>
</head>
<body>
<div id="app">
<div class="animated-bg"></div>
<div class="header">
<div class="header-logo">AI</div>
xiaozhi-esp32-server
</div>
<div class="content-container">
<div class="role-title">配置角色:</div>
<div class="role-id">cc:ba:97:11:a6:ac</div>
<div class="role-description">角色介绍</div>
<el-input
type="textarea"
v-model="form.prompt"
:rows="6"
resize="vertical"
@input="updateCharCount"
></el-input>
<div class="character-count">{{ charCount }} / 2000</div>
<div class="button-group">
<el-button type="primary" @click="savePrompt">保存配置</el-button>
<el-button @click="resetPrompt">重置</el-button>
</div>
<div class="note">注意:保存配置后,需要重启设备,新的配置才会生效。</div>
</div>
<div class="footer">
© 2024 小智 AI 控制面板 2.0 粤ICP备2022121736号-2
</div>
</div>
<script>
const {ElMessage} = ElementPlus;
const App = {
data() {
return {
form: {
prompt: ''
},
charCount: 0
};
},
methods: {
loadPrompt() {
let that = this;
that.form.prompt = '';
get('/api/prompt', function (data) {
if (data.code === -1) {
ElMessage({
message: data.msg,
type: 'error'
});
} else {
that.form.prompt = data.prompt;
that.updateCharCount();
}
})
},
updateCharCount() {
this.charCount = this.form.prompt.length;
},
savePrompt() {
post('/api/prompt', {prompt: this.form.prompt}, (data) => {
if (data.code === 0) {
ElMessage({
message: '保存成功',
type: 'success'
});
this.loadPrompt();
} else {
ElMessage({
message: data.msg || '保存失败',
type: 'error'
});
}
});
}
},
mounted() {
this.loadPrompt();
}
};
const app = Vue.createApp(App);
app.use(ElementPlus);
app.mount("#app");
</script>
</body>
</html>
+40
View File
@@ -0,0 +1,40 @@
function post(api, bodyData, fn) {
let basePath = 'http://localhost:8001';
let token = localStorage.getItem('token');
fetch(basePath + api, {
method: "POST",
headers: {
'Content-Type': 'application/json',
'Authorization': token,
},
body: bodyData==null ? null : JSON.stringify(bodyData)
})
.then(response => response.json())
.then(data => {
if(data.code === 401){
window.location = 'login.html';
return
}
fn(data);
});
}
function get(api, fn) {
let basePath = 'http://localhost:8001';
let token = localStorage.getItem('token');
fetch(basePath + api, {
method: "GET",
headers: {
'Content-Type': 'application/json',
'Authorization': token,
},
})
.then(response => response.json())
.then(data => {
if(data.code === 401){
window.location = 'login.html';
return
}
fn(data);
});
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
+196 -3
View File
@@ -1,13 +1,206 @@
<!DOCTYPE html>
<html>
<head>
<title>login</title>
<meta charset="utf-8">
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1.0"/>
<script src="js/vue3.5.13.js"></script>
<link rel="stylesheet" href="css/element-plus2.9.4.css">
<script src="js/element-plus2.9.4.js"></script>
<script src="js/icons-vue2.3.1.js"></script>
<script src="js/common.js"></script>
<title>小智-server</title>
<style>
body {
margin: 0;
padding: 0;
background: linear-gradient(135deg, #1a1f25 0%, #0d1117 100%);
min-height: 100vh;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
}
.header {
background: rgba(13, 17, 23, 0.8);
backdrop-filter: blur(10px);
color: #fff;
padding: 1.5rem 2rem;
font-size: 1.4rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
display: flex;
align-items: center;
gap: 1rem;
}
.header-logo {
width: 32px;
height: 32px;
background: linear-gradient(45deg, #00c6fb 0%, #005bea 100%);
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
}
.login-container {
max-width: 440px;
margin: 60px auto;
padding: 2.5rem;
background: rgba(255, 255, 255, 0.05);
border-radius: 16px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
backdrop-filter: blur(8px);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.login-title {
font-size: 28px;
color: #fff;
margin-bottom: 40px;
font-weight: 600;
text-align: center;
}
.login-form {
margin-top: 20px;
}
.custom-input :deep(.el-input__wrapper) {
background: rgba(255, 255, 255, 0.05) !important;
border: 1px solid rgba(255, 255, 255, 0.1) !important;
box-shadow: none !important;
}
.custom-input :deep(.el-input__inner) {
color: #fff !important;
}
.custom-button {
background: linear-gradient(45deg, #00c6fb 0%, #005bea 100%) !important;
border: none !important;
height: 44px !important;
font-size: 16px !important;
font-weight: 500 !important;
letter-spacing: 1px;
transition: all 0.3s ease !important;
}
.custom-button:hover {
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(0, 198, 251, 0.3) !important;
}
.footer {
text-align: center;
color: rgba(255, 255, 255, 0.6);
font-size: 13px;
padding: 20px;
position: fixed;
bottom: 0;
width: 100%;
}
@keyframes gradient {
0% {
background-position: 0% 50%
}
50% {
background-position: 100% 50%
}
100% {
background-position: 0% 50%
}
}
.animated-bg {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: -1;
background: linear-gradient(-45deg, #1a1f25, #0d1117, #162030, #1c1c1c);
background-size: 400% 400%;
animation: gradient 15s ease infinite;
}
</style>
</head>
<body>
login
<div id="app">
<div class="animated-bg"></div>
<div class="header">
<div class="header-logo">AI</div>
xiaozhi-esp32-server
</div>
<div class="login-container">
<h2 class="login-title">安全验证</h2>
<div class="login-form">
<el-form :model="form" size="large">
<el-form-item>
<el-input
class="custom-input"
v-model="form.password"
type="password"
placeholder="请输入访问密码"
show-password>
</el-input>
</el-form-item>
<el-form-item>
<el-button class="custom-button" type="primary" style="width: 100%" @click="handleLogin">
验证并进入系统
</el-button>
</el-form-item>
</el-form>
</div>
</div>
<div class="footer">
© 2025 xiaozhi-esp32-server
</div>
</div>
<script>
const {ElMessage} = ElementPlus;
const App = {
data() {
return {
form: {
password: ''
}
};
},
methods: {
handleLogin() {
let that = this;
post('/api/login', that.form, function (data) {
if (data.code === -1) {
// 弹出错误提示
ElMessage({
message: '登录失败,请检查您的密码',
type: 'error'
});
localStorage.removeItem('token');
} else {
// 登录成功时的处理逻辑
ElMessage({
message: '登录成功',
type: 'success'
});
localStorage.setItem('token', that.form.password); // 设置登录状态为已登录
location.href = 'index.html'; // 跳转到主页
}
})
}
},
mounted() {
// 在页面加载时自动调用 handleLogin 方法
let token = localStorage.getItem('token')
if (token) {
this.form.password = token;
this.handleLogin();
}
}
};
const app = Vue.createApp(App);
app.use(ElementPlus);
app.mount("#app");
</script>
</body>
</html>