增加通过认证码绑定设备和管理账户的功能,需要开启私有设备配置

增加登录用户鉴权
This commit is contained in:
玄凤科技
2025-02-17 15:53:48 +08:00
parent 325312327c
commit aa2d323951
14 changed files with 1052 additions and 274 deletions
+6 -14
View File
@@ -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 || '登录失败');
+224 -40
View File
@@ -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 '从未对话';
@@ -82,20 +176,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 +199,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 +378,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>