mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-28 19:23:52 +08:00
@@ -1 +1 @@
|
||||
VITE_API_BASE_URL=''
|
||||
VITE_API_BASE_URL='http://127.0.0.1:8002'
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
</div>
|
||||
|
||||
<div class="device-actions">
|
||||
<button class="action-btn primary" @click="$emit('configure')">配置角色</button>
|
||||
<button class="action-btn primary" @click="handleConfigure">配置角色</button>
|
||||
<button class="action-btn" @click="$emit('voiceprint')">声纹识别</button>
|
||||
<button class="action-btn" @click="$emit('history')">历史对话</button>
|
||||
<div class="delete-container">
|
||||
@@ -81,6 +81,18 @@ const handleDelete = () => {
|
||||
emit('delete');
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfigure = () => {
|
||||
// 保存设备配置到 localStorage,确保 RoleSetting 可以访问
|
||||
if (props.deviceId && props.deviceConfig) {
|
||||
localStorage.setItem(`deviceConfig_${props.deviceId}`, JSON.stringify({
|
||||
selected_module: props.selectedModules,
|
||||
prompt: props.deviceConfig.prompt || '',
|
||||
nickname: props.deviceConfig.nickname || '小智'
|
||||
}));
|
||||
}
|
||||
emit('configure');
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { API_BASE_URL } from '../config/api';
|
||||
import apiClient from '../utils/api';
|
||||
|
||||
const router = useRouter();
|
||||
const username = ref('');
|
||||
@@ -34,24 +34,16 @@ const handleLogin = async () => {
|
||||
|
||||
isLoading.value = true;
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/login`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
username: username.value,
|
||||
password: password.value
|
||||
})
|
||||
const response = await apiClient.post('/api/login', {
|
||||
username: username.value,
|
||||
password: password.value
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
const data = response.data;
|
||||
|
||||
if (data.success) {
|
||||
// 存储token和登录状态
|
||||
localStorage.setItem('token', data.token);
|
||||
localStorage.setItem('session_id', data.session_id);
|
||||
localStorage.setItem('isLoggedIn', 'true');
|
||||
// 使用路由导航到panel页面
|
||||
router.push('/panel');
|
||||
} else {
|
||||
alert(data.message || '登录失败');
|
||||
|
||||
@@ -110,7 +110,7 @@ import { ref, onMounted } from 'vue';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import NavBar from './NavBar.vue';
|
||||
import RoleTemplates from '../utils/RoleTemplates';
|
||||
import { API_BASE_URL } from '../config/api';
|
||||
import apiClient from '../utils/api'; // 替换 API_BASE_URL 导入
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
@@ -125,7 +125,6 @@ const activeMemoryTab = ref('recent');
|
||||
const memoryContent = ref('');
|
||||
const selectedModel = ref('qianwen');
|
||||
|
||||
const baseUrl = API_BASE_URL;
|
||||
const moduleOptions = ref({
|
||||
LLM: [],
|
||||
TTS: [],
|
||||
@@ -170,15 +169,15 @@ const loadModuleOptions = async () => {
|
||||
|
||||
const refreshModuleOptions = async () => {
|
||||
try {
|
||||
const response = await fetch(`${baseUrl}/api/config/module-options`);
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
moduleOptions.value = data.data;
|
||||
const response = await apiClient.get('/api/config/module-options');
|
||||
if (response.data.success) {
|
||||
moduleOptions.value = response.data.data;
|
||||
// Update cache
|
||||
localStorage.setItem('moduleOptions', JSON.stringify(data.data));
|
||||
localStorage.setItem('moduleOptions', JSON.stringify(response.data.data));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error refreshing module options:', error);
|
||||
alert(error.response?.data?.message || '刷新配置选项失败');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -208,16 +207,9 @@ const saveConfig = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const response = await fetch(`${baseUrl}/api/config/save_device_config`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(config)
|
||||
});
|
||||
const response = await apiClient.post('/api/config/save_device_config', config);
|
||||
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
if (response.data.success) {
|
||||
// Save the original description and nickname to local storage
|
||||
localStorage.setItem(`deviceConfig_${deviceId.value}`, JSON.stringify({
|
||||
selected_module: selectedModules.value,
|
||||
@@ -226,46 +218,57 @@ const saveConfig = async () => {
|
||||
}));
|
||||
alert('保存成功');
|
||||
} else {
|
||||
throw new Error(data.message || '保存失败');
|
||||
throw new Error(response.data.message || '保存失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error saving config:', error);
|
||||
alert(error.message || '保存失败');
|
||||
alert(error.response?.data?.message || '保存失败');
|
||||
}
|
||||
};
|
||||
|
||||
const loadDeviceConfig = async () => {
|
||||
try {
|
||||
// First try to get config from local storage
|
||||
// 首先尝试从 localStorage 获取配置
|
||||
const localConfig = localStorage.getItem(`deviceConfig_${deviceId.value}`);
|
||||
if (localConfig) {
|
||||
const config = JSON.parse(localConfig);
|
||||
selectedModules.value = config.selected_module || {};
|
||||
selectedModules.value = config.selected_module || {
|
||||
LLM: '',
|
||||
TTS: '',
|
||||
VAD: '',
|
||||
ASR: ''
|
||||
};
|
||||
roleDescription.value = config.prompt || '';
|
||||
nickname.value = config.nickname || '小智'; // Load saved nickname
|
||||
return;
|
||||
nickname.value = config.nickname || '小智';
|
||||
return; // 如果找到本地配置就直接返回
|
||||
}
|
||||
|
||||
// If no local config, fetch from server
|
||||
const response = await fetch(`${baseUrl}/api/config/devices`);
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
const deviceConfig = data.data.find(d => d.id === deviceId.value);
|
||||
if (deviceConfig) {
|
||||
selectedModules.value = deviceConfig.config.selected_module || {};
|
||||
// 如果没有本地配置,则从服务器获取
|
||||
const response = await apiClient.get('/api/config/devices');
|
||||
if (response.data.success && response.data.data) {
|
||||
const deviceConfig = response.data.data[deviceId.value];
|
||||
|
||||
if (deviceConfig && deviceConfig.config) {
|
||||
selectedModules.value = deviceConfig.config.selected_module || {
|
||||
LLM: '',
|
||||
TTS: '',
|
||||
VAD: '',
|
||||
ASR: ''
|
||||
};
|
||||
roleDescription.value = deviceConfig.config.prompt || '';
|
||||
nickname.value = deviceConfig.config.nickname || '小智'; // Load nickname from server
|
||||
nickname.value = deviceConfig.config.nickname || '小智';
|
||||
|
||||
// Save to local storage
|
||||
// 保存到 localStorage
|
||||
localStorage.setItem(`deviceConfig_${deviceId.value}`, JSON.stringify({
|
||||
selected_module: deviceConfig.config.selected_module,
|
||||
prompt: deviceConfig.config.prompt,
|
||||
nickname: deviceConfig.config.nickname
|
||||
selected_module: selectedModules.value,
|
||||
prompt: roleDescription.value,
|
||||
nickname: nickname.value
|
||||
}));
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading device config:', error);
|
||||
alert(error.response?.data?.message || '加载设备配置失败');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -2,11 +2,46 @@
|
||||
<div class="app">
|
||||
<NavBar current-tab="device" @tab-change="handleTabChange"/>
|
||||
<main class="content">
|
||||
<div class="breadcrumb">
|
||||
<router-link to="/">首页</router-link> /
|
||||
<span>设备管理</span>
|
||||
<div class="page-header">
|
||||
<div class="header-left">
|
||||
|
||||
<div class="breadcrumb">
|
||||
<router-link to="/">首页</router-link> /
|
||||
<span>设备管理</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="add-btn" @click="showBindDialog = true">
|
||||
<i class="icon-plus"></i>添加设备
|
||||
</button>
|
||||
<!-- 绑定设备弹窗 -->
|
||||
<div v-if="showBindDialog" class="dialog-overlay">
|
||||
<div class="dialog">
|
||||
<h3>绑定新设备</h3>
|
||||
<div class="form-group">
|
||||
<label>请输入6位认证码:</label>
|
||||
<input
|
||||
type="text"
|
||||
v-model="authCode"
|
||||
maxlength="6"
|
||||
pattern="\d*"
|
||||
placeholder="请输入6位数字认证码"
|
||||
@input="handleAuthCodeInput"
|
||||
/>
|
||||
</div>
|
||||
<div class="dialog-buttons">
|
||||
<button @click="showBindDialog = false">取消</button>
|
||||
<button
|
||||
class="primary"
|
||||
@click="handleBindDevice"
|
||||
:disabled="authCode.length !== 6 || isBinding"
|
||||
>
|
||||
{{ isBinding ? '绑定中...' : '确认绑定' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="devices.length > 0">
|
||||
<div class="device-list">
|
||||
<DeviceCard
|
||||
@@ -25,14 +60,7 @@
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="empty-state">
|
||||
<div class="empty-message">
|
||||
<i class="icon-info"></i>
|
||||
<p>目前没有设备,请确认是否启用私有配置,并且和设备进行一次对话</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
@@ -42,12 +70,78 @@ import { ref, onMounted } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import NavBar from './NavBar.vue';
|
||||
import DeviceCard from './DeviceCard.vue';
|
||||
import { API_BASE_URL } from '../config/api';
|
||||
import apiClient from '../utils/api';
|
||||
|
||||
const router = useRouter();
|
||||
const baseUrl = API_BASE_URL;
|
||||
const devices = ref([]);
|
||||
|
||||
// 绑定设备相关的状态
|
||||
const showBindDialog = ref(false);
|
||||
const authCode = ref('');
|
||||
const isBinding = ref(false);
|
||||
|
||||
// 处理认证码输入,只允许数字
|
||||
const handleAuthCodeInput = (event) => {
|
||||
authCode.value = event.target.value.replace(/\D/g, '').slice(0, 6);
|
||||
};
|
||||
|
||||
// 处理设备绑定
|
||||
const handleBindDevice = async () => {
|
||||
if (authCode.value.length !== 6) {
|
||||
alert('请输入6位数字认证码');
|
||||
return;
|
||||
}
|
||||
|
||||
isBinding.value = true;
|
||||
try {
|
||||
const response = await apiClient.post('/api/config/bind_device', {
|
||||
auth_code: authCode.value
|
||||
});
|
||||
|
||||
if (response.data.success) {
|
||||
alert('设备绑定成功');
|
||||
showBindDialog.value = false;
|
||||
authCode.value = '';
|
||||
// 刷新设备列表
|
||||
loadDevices();
|
||||
} else {
|
||||
throw new Error(response.data.message);
|
||||
}
|
||||
} catch (error) {
|
||||
alert(error.response?.data?.message || error.message || '绑定失败');
|
||||
} finally {
|
||||
isBinding.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 将现有的加载设备方法提取出来
|
||||
const loadDevices = async () => {
|
||||
try {
|
||||
const response = await apiClient.get('/api/config/devices');
|
||||
|
||||
if (response.data.success) {
|
||||
const deviceArray = Object.entries(response.data.data).map(([id, config]) => ({
|
||||
id,
|
||||
config,
|
||||
type: '面包板(WiFi)',
|
||||
version: '0.9.9',
|
||||
lastActivity: '3 天前',
|
||||
note: ''
|
||||
}));
|
||||
devices.value = deviceArray;
|
||||
} else {
|
||||
throw new Error(response.data.message || '加载设备失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading devices:', error);
|
||||
// Show error message to user
|
||||
const errorMessage = error.message || '加载设备失败,请检查网络连接';
|
||||
alert(errorMessage);
|
||||
|
||||
// If user is not logged in, redirect will be handled by api interceptor
|
||||
}
|
||||
};
|
||||
|
||||
const formatLastActivity = (timestamp) => {
|
||||
if (!timestamp) return '从未对话';
|
||||
|
||||
@@ -69,6 +163,14 @@ const formatLastActivity = (timestamp) => {
|
||||
};
|
||||
|
||||
const handleRoleConfig = (device) => {
|
||||
// 在跳转前保存完整的设备配置到 localStorage
|
||||
localStorage.setItem(`deviceConfig_${device.id}`, JSON.stringify({
|
||||
selected_module: device.config.selected_module || {},
|
||||
prompt: device.config.prompt || '',
|
||||
nickname: device.config.nickname || '小智'
|
||||
}));
|
||||
|
||||
// 跳转到角色配置页面
|
||||
router.push(`/role-setting/${device.id}`);
|
||||
};
|
||||
|
||||
@@ -82,20 +184,15 @@ const handleHistory = (device) => {
|
||||
|
||||
const handleDelete = async (device) => {
|
||||
try {
|
||||
const response = await fetch(`${baseUrl}/api/config/delete_device`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ device_id: device.id })
|
||||
const response = await apiClient.post('/api/config/delete_device', {
|
||||
device_id: device.id
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
if (response.data.success) {
|
||||
devices.value = devices.value.filter(d => d.id !== device.id);
|
||||
alert('设备已删除');
|
||||
} else {
|
||||
throw new Error(data.message || '删除失败');
|
||||
throw new Error(response.data.message || '删除失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error deleting device:', error);
|
||||
@@ -110,23 +207,7 @@ const handleTabChange = (tab) => {
|
||||
};
|
||||
|
||||
// Load devices on mount
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const response = await fetch(`${baseUrl}/api/config/devices`);
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
devices.value = data.data.map(device => ({
|
||||
...device,
|
||||
type: '面包板(WiFi)',
|
||||
version: '0.9.9',
|
||||
lastActivity: '3 天前',
|
||||
note: ''
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading devices:', error);
|
||||
}
|
||||
});
|
||||
onMounted(loadDevices);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -305,4 +386,115 @@ onMounted(async () => {
|
||||
background-size: contain;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.add-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 16px;
|
||||
background-color: #28a745;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: all 0.3s ease;
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.add-btn:hover {
|
||||
background-color: #218838;
|
||||
}
|
||||
|
||||
.icon-plus {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.dialog-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.dialog {
|
||||
background: white;
|
||||
padding: 24px;
|
||||
border-radius: 8px;
|
||||
width: 90%;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.dialog h3 {
|
||||
margin: 0 0 20px;
|
||||
font-size: 18px;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.dialog .form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.dialog label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
color: #4a5568;
|
||||
}
|
||||
|
||||
.dialog input {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 4px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.dialog-buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.dialog-buttons button {
|
||||
padding: 8px 16px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.dialog-buttons button.primary {
|
||||
background-color: #28a745;
|
||||
color: white;
|
||||
border-color: #28a745;
|
||||
}
|
||||
|
||||
.dialog-buttons button.primary:disabled {
|
||||
background-color: #90be9c;
|
||||
border-color: #90be9c;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,61 @@
|
||||
import axios from 'axios';
|
||||
import { API_BASE_URL } from '../config/api';
|
||||
|
||||
// Add server status check utility
|
||||
export const checkServerStatus = async () => {
|
||||
try {
|
||||
await axios.get(`${API_BASE_URL}/health`, { timeout: 5000 });
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const apiClient = axios.create({
|
||||
baseURL: API_BASE_URL,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
timeout: 10000 // Add timeout
|
||||
});
|
||||
|
||||
// 添加请求拦截器,自动添加 session_id
|
||||
apiClient.interceptors.request.use(config => {
|
||||
const sessionId = localStorage.getItem('session_id');
|
||||
if (sessionId) {
|
||||
config.headers.Authorization = sessionId;
|
||||
}
|
||||
return config;
|
||||
});
|
||||
|
||||
// 响应拦截器
|
||||
apiClient.interceptors.response.use(
|
||||
response => response,
|
||||
error => {
|
||||
// Network error or server not reachable
|
||||
if (!error.response || error.code === 'ERR_NETWORK' || error.code === 'ECONNABORTED') {
|
||||
localStorage.removeItem('session_id');
|
||||
localStorage.removeItem('isLoggedIn');
|
||||
|
||||
const errorMessage = error.code === 'ECONNABORTED'
|
||||
? '服务器响应超时'
|
||||
: '无法连接到服务器,请检查服务器是否正常运行';
|
||||
|
||||
if (window.location.pathname !== '/login') {
|
||||
window.location.href = '/login';
|
||||
}
|
||||
return Promise.reject(new Error(errorMessage));
|
||||
}
|
||||
|
||||
// Unauthorized error
|
||||
if (error.response && error.response.status === 401) {
|
||||
localStorage.removeItem('session_id');
|
||||
localStorage.removeItem('isLoggedIn');
|
||||
window.location.href = '/login';
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
export default apiClient;
|
||||
Reference in New Issue
Block a user