fix: 修复webUI刷新跳转登录页面的问题

This commit is contained in:
TOM88812
2025-02-16 23:01:44 +08:00
parent 1598f6cd14
commit 68ab05b220
12 changed files with 288 additions and 299 deletions
+3 -1
View File
@@ -8,4 +8,6 @@ tmp/
data/
LICENSE
README.md
README_en.md
README_en.md
manager/static
manager/static/webui/
+6 -4
View File
@@ -1,8 +1,11 @@
# 第一阶段:前端构建
FROM node:18 as frontend-builder
FROM node:18 AS frontend-builder
WORKDIR /app/ZhiKongTaiWeb
# 配置npm使用淘宝源
RUN npm config set registry https://registry.npmmirror.com
COPY ZhiKongTaiWeb/package*.json ./
RUN npm install
@@ -12,7 +15,7 @@ COPY ZhiKongTaiWeb .
RUN npm run build
# 第二阶段:构建Python依赖
FROM python:3.10-slim as builder
FROM python:3.10-slim AS builder
WORKDIR /app
@@ -58,8 +61,7 @@ RUN rm -rf /etc/apt/sources.list.d/* && \
# 从构建阶段复制虚拟环境和前端构建产物
COPY --from=builder /opt/venv /opt/venv
COPY --from=frontend-builder /app/ZhiKongTaiWeb/dist /opt/xiaozhi-esp32-server/manager/static
COPY --from=frontend-builder /app/ZhiKongTaiWeb/dist /opt/xiaozhi-esp32-server/manager/static/webui
# 设置虚拟环境路径
ENV PATH="/opt/venv/bin:$PATH"
+2 -1
View File
@@ -9,7 +9,8 @@
"preview": "vite preview"
},
"dependencies": {
"vue": "^3.5.10"
"vue": "^3.5.10",
"vue-router": "^4.0.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.1.4",
+2 -47
View File
@@ -1,64 +1,19 @@
<template>
<div id="app">
<div class="app-content" :class="{ 'auth-layout': !isLoggedIn }">
<div v-if="!isLoggedIn">
<Login v-if="!showRegistration" @show-registration="showRegistration = true" @login-success="handleLoginSuccess" />
<Registration v-else @show-login="showRegistration = false" />
</div>
<div v-else class="main-layout">
<role-setting v-if="currentView === 'RoleSetting'"
:device-id="selectedDeviceId"
@back-to-panel="switchView('panel')"/>
<panel v-else-if="currentView === 'panel'"
@go-home="switchView('main')"
@show-role="handleShowRole"
/>
<main-page v-else @enter-panel="switchView('panel')"/>
</div>
<div class="app-content">
<router-view></router-view>
</div>
<Footer />
</div>
</template>
<script>
import Login from './components/Login.vue';
import Registration from './components/Registration.vue';
import panel from './components/panel.vue';
import MainPage from './components/Main.vue';
import RoleSetting from './components/RoleSetting.vue';
import Footer from './components/Footer.vue';
export default {
name: 'App',
components: {
Login,
Registration,
panel,
MainPage,
RoleSetting,
Footer
},
data() {
return {
showRegistration: false,
isLoggedIn: false,
currentView: 'main',
selectedDeviceId: null // 添加selectedDeviceId状态
};
},
methods: {
handleLoginSuccess() {
this.isLoggedIn = true;
this.currentView = 'panel';
},
switchView(view) {
console.log('Switching to view:', view); // 添加调试日志
this.currentView = view;
},
handleShowRole(deviceId) {
this.selectedDeviceId = deviceId;
this.switchView('RoleSetting');
}
}
};
</script>
+136 -138
View File
@@ -12,148 +12,146 @@
</div>
<button type="submit" :disabled="isLoading">登录</button>
</form>
<p>还没注册账户? <a href="#" @click.prevent="$emit('show-registration')">点击注册</a></p>
<p>还没注册账户? <router-link to="/register">点击注册</router-link></p>
</div>
</template>
<script>
import { API_BASE_URL } from '../config/api';
<script setup>
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { API_BASE_URL } from '../config/api';
export default {
data() {
return {
username: '',
password: '',
isLoading: false
};
},
methods: {
async handleLogin() {
if (!this.username || !this.password) {
alert('请输入用户名和密码!');
return;
}
this.isLoading = true;
try {
const response = await fetch(`${API_BASE_URL}/api/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
username: this.username,
password: this.password
})
});
const data = await response.json();
if (data.success) {
// 登录成功
this.$emit('login-success');
} else {
// 登录失败
alert(data.message || '登录失败');
}
} catch (error) {
console.error('Login error:', error);
alert('登录失败,请检查网络连接');
} finally {
this.isLoading = false;
}
const router = useRouter();
const username = ref('');
const password = ref('');
const isLoading = ref(false);
const handleLogin = async () => {
if (!username.value || !password.value) {
alert('请输入用户名和密码!');
return;
}
isLoading.value = true;
try {
const response = await fetch(`${API_BASE_URL}/api/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
},
};
</script>
<style scoped>
.login-container {
max-width: 420px;
margin: 20px auto;
padding: 40px 50px;
border: none;
border-radius: 12px;
background-color: #ffffff;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
transition: all 0.3s ease;
}
h1 {
text-align: center;
color: #2c3e50;
font-size: 28px;
margin-bottom: 25px;
font-weight: 600;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 8px;
font-weight: 500;
color: #4a5568;
}
input {
width: 100%;
padding: 12px 16px;
border: 1px solid #e2e8f0;
border-radius: 8px;
background-color: #f8fafc;
transition: all 0.3s ease;
font-size: 15px;
}
input:focus {
outline: none;
border-color: #28a745;
box-shadow: 0 0 0 3px rgba(40, 167, 69, 0.2);
}
button {
width: 100%;
padding: 12px;
background-color: #28a745;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 16px;
font-weight: 500;
transition: all 0.3s ease;
margin-top: 10px;
}
button:hover {
background-color: #218838;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(40, 167, 69, 0.2);
}
body: JSON.stringify({
username: username.value,
password: password.value
})
});
button:disabled {
opacity: 0.7;
cursor: not-allowed;
const data = await response.json();
if (data.success) {
// 存储token和登录状态
localStorage.setItem('token', data.token);
localStorage.setItem('isLoggedIn', 'true');
// 使用路由导航到panel页面
router.push('/panel');
} else {
alert(data.message || '登录失败');
}
} catch (error) {
console.error('Login error:', error);
alert('登录失败,请检查网络连接');
} finally {
isLoading.value = false;
}
p {
margin-top: 20px;
color: #666;
font-size: 14px;
}
a {
color: #28a745;
text-decoration: none;
font-weight: 500;
transition: color 0.3s ease;
}
a:hover {
color: #218838;
text-decoration: underline;
}
</style>
};
</script>
<style scoped>
.login-container {
max-width: 420px;
margin: 20px auto;
padding: 40px 50px;
border: none;
border-radius: 12px;
background-color: #ffffff;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
transition: all 0.3s ease;
}
h1 {
text-align: center;
color: #2c3e50;
font-size: 28px;
margin-bottom: 25px;
font-weight: 600;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 8px;
font-weight: 500;
color: #4a5568;
}
input {
width: 100%;
padding: 12px 16px;
border: 1px solid #e2e8f0;
border-radius: 8px;
background-color: #f8fafc;
transition: all 0.3s ease;
font-size: 15px;
}
input:focus {
outline: none;
border-color: #28a745;
box-shadow: 0 0 0 3px rgba(40, 167, 69, 0.2);
}
button {
width: 100%;
padding: 12px;
background-color: #28a745;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 16px;
font-weight: 500;
transition: all 0.3s ease;
margin-top: 10px;
}
button:hover {
background-color: #218838;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(40, 167, 69, 0.2);
}
button:disabled {
opacity: 0.7;
cursor: not-allowed;
}
p {
margin-top: 20px;
color: #666;
font-size: 14px;
}
a {
color: #28a745;
text-decoration: none;
font-weight: 500;
transition: color 0.3s ease;
}
a:hover {
color: #218838;
text-decoration: underline;
}
</style>
+5 -3
View File
@@ -33,7 +33,9 @@
<script setup>
import NavBar from './NavBar.vue';
const emit = defineEmits(['enter-panel']);
import { useRouter } from 'vue-router';
const router = useRouter();
const openTutorial = () => {
window.open('https://ccnphfhqs21z.feishu.cn/wiki/F5krwD16viZoF0kKkvDcrZNYnhb', '_blank');
@@ -48,12 +50,12 @@ const openGithubServer = () => {
};
const enterPanel = () => {
emit('enter-panel');
router.push('/panel');
};
const handleTabChange = (tab) => {
if (tab === 'device') {
emit('enter-panel');
router.push('/panel');
}
};
</script>
+7 -8
View File
@@ -1,8 +1,8 @@
<template>
<header class="header">
<div class="nav-item" :class="{ active: currentTab === 'home' }" @click="switchTab('home')">小智 AI</div>
<div class="nav-item" :class="{ active: $route.path === '/' }" @click="switchTab('/')">小智 AI</div>
<nav class="nav">
<div class="nav-item" :class="{ active: currentTab === 'device' }" @click="switchTab('device')">
<div class="nav-item" :class="{ active: $route.path === '/panel' }" @click="switchTab('/panel')">
<i class="icon-device"></i> 设备管理
</div>
</nav>
@@ -10,14 +10,13 @@
</template>
<script setup>
const props = defineProps({
currentTab: String
});
import { useRouter, useRoute } from 'vue-router';
const emit = defineEmits(['tab-change']);
const router = useRouter();
const route = useRoute();
const switchTab = (tab) => {
emit('tab-change', tab);
const switchTab = (path) => {
router.push(path);
};
</script>
+51 -57
View File
@@ -18,72 +18,66 @@
</form>
<p>
已有账号
<a href="#" @click.prevent="$emit('show-login')">点击登录</a>
<router-link to="/login">点击登录</router-link>
</p>
</div>
</template>
<script>
<script setup>
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { API_BASE_URL } from '../config/api';
export default {
data() {
return {
username: '',
password: '',
confirmPassword: '',
isLoading: false
};
},
methods: {
async handleRegistration() {
if (this.password !== this.confirmPassword) {
alert('密码不匹配,请重试!');
return;
}
const router = useRouter();
const username = ref('');
const password = ref('');
const confirmPassword = ref('');
const isLoading = ref(false);
if (!this.username || !this.password) {
alert('用户名和密码不能为空!');
return;
}
const handleRegistration = async () => {
if (password.value !== confirmPassword.value) {
alert('密码不匹配,请重试!');
return;
}
this.isLoading = true;
try {
const response = await fetch(`${API_BASE_URL}/api/register`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
username: this.username,
password: this.password
})
});
if (!username.value || !password.value) {
alert('用户名和密码不能为空!');
return;
}
const data = await response.json();
if (data.success) {
alert('注册成功!即将跳转到登录页面。');
// 清空表单数据
this.username = '';
this.password = '';
this.confirmPassword = '';
// 延迟跳转到登录页面
setTimeout(() => {
this.$emit('show-login');
}, 100);
} else {
alert(data.message || '注册失败');
}
} catch (error) {
console.error('Registration error:', error);
alert('注册失败,请检查网络连接');
} finally {
this.isLoading = false;
}
},
},
isLoading.value = true;
try {
const response = await fetch(`${API_BASE_URL}/api/register`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
username: username.value,
password: password.value
})
});
const data = await response.json();
if (data.success) {
alert('注册成功!即将跳转到登录页面。');
// 清空表单数据
username.value = '';
password.value = '';
confirmPassword.value = '';
// 使用路由导航到登录页面
router.push('/login');
} else {
alert(data.message || '注册失败');
}
} catch (error) {
console.error('Registration error:', error);
alert('注册失败,请检查网络连接');
} finally {
isLoading.value = false;
}
};
</script>
+13 -27
View File
@@ -4,8 +4,8 @@
<!-- Breadcrumb -->
<div class="breadcrumb">
<router-link to="/control-panel">控制台</router-link> /
<router-link to="/device-management">设备管理</router-link> /
<router-link to="/">首页</router-link> /
<router-link to="/panel">设备管理</router-link> /
<span>配置角色</span>
</div>
@@ -107,12 +107,16 @@
<script setup>
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';
const roleTemplates = RoleTemplates.getTemplates();
const router = useRouter();
const route = useRoute();
const deviceId = ref(route.params.deviceId);
const roleTemplates = RoleTemplates.getTemplates();
const nickname = ref('小智');
const selectedTemplate = ref('');
const selectedVoice = ref('qingchun');
@@ -144,30 +148,12 @@ const selectTemplate = (templateId) => {
}
};
const emit = defineEmits(['back-to-panel']);
const handleBack = () => {
console.log('Triggering back-to-panel event'); // 添加调试日志
emit('back-to-panel');
};
const handleTabChange = (tab) => {
if (tab === 'device' || tab === 'home') {
emit('back-to-panel');
router.push('/panel');
}
};
// 修改deviceId的定义,接收props
const props = defineProps({
deviceId: {
type: String,
required: true
}
});
// 使用props中的deviceId替换原来的静态值
const deviceId = ref(props.deviceId);
const loadModuleOptions = async () => {
try {
// First try to load from cache
@@ -208,7 +194,7 @@ const saveConfig = async () => {
// Prepare the configuration including full module settings
const config = {
id: props.deviceId,
id: deviceId.value,
config: {
selected_module: selectedModules.value,
prompt: processedDescription,
@@ -233,7 +219,7 @@ const saveConfig = async () => {
const data = await response.json();
if (data.success) {
// Save the original description and nickname to local storage
localStorage.setItem(`deviceConfig_${props.deviceId}`, JSON.stringify({
localStorage.setItem(`deviceConfig_${deviceId.value}`, JSON.stringify({
selected_module: selectedModules.value,
prompt: roleDescription.value,
nickname: nickname.value
@@ -251,7 +237,7 @@ const saveConfig = async () => {
const loadDeviceConfig = async () => {
try {
// First try to get config from local storage
const localConfig = localStorage.getItem(`deviceConfig_${props.deviceId}`);
const localConfig = localStorage.getItem(`deviceConfig_${deviceId.value}`);
if (localConfig) {
const config = JSON.parse(localConfig);
selectedModules.value = config.selected_module || {};
@@ -264,14 +250,14 @@ const loadDeviceConfig = async () => {
const response = await fetch(`${baseUrl}/api/config/devices`);
const data = await response.json();
if (data.success) {
const deviceConfig = data.data.find(d => d.id === props.deviceId);
const deviceConfig = data.data.find(d => d.id === deviceId.value);
if (deviceConfig) {
selectedModules.value = deviceConfig.config.selected_module || {};
roleDescription.value = deviceConfig.config.prompt || '';
nickname.value = deviceConfig.config.nickname || '小智'; // Load nickname from server
// Save to local storage
localStorage.setItem(`deviceConfig_${props.deviceId}`, JSON.stringify({
localStorage.setItem(`deviceConfig_${deviceId.value}`, JSON.stringify({
selected_module: deviceConfig.config.selected_module,
prompt: deviceConfig.config.prompt,
nickname: deviceConfig.config.nickname
+6 -10
View File
@@ -3,7 +3,7 @@
<NavBar current-tab="device" @tab-change="handleTabChange"/>
<main class="content">
<div class="breadcrumb">
<router-link to="/control-panel">控制台</router-link> /
<router-link to="/">首页</router-link> /
<span>设备管理</span>
</div>
@@ -39,21 +39,20 @@
<script setup>
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';
const emit = defineEmits(['show-role', 'go-home']);
const router = useRouter();
const baseUrl = API_BASE_URL;
const devices = ref([]);
const formatLastActivity = (timestamp) => {
console.log('Formatting timestamp:', timestamp);
if (!timestamp) return '从未对话';
const now = Date.now();
const lastChat = timestamp * 1000; // Convert to milliseconds
const lastChat = timestamp * 1000;
const diffMinutes = Math.floor((now - lastChat) / (1000 * 60));
if (diffMinutes < 60) {
@@ -69,10 +68,8 @@ const formatLastActivity = (timestamp) => {
return `${diffDays} 天前`;
};
// Event handlers
const handleRoleConfig = (device) => {
console.log('Configure device:', device.id);
emit('show-role', device.id);
router.push(`/role-setting/${device.id}`);
};
const handleVoiceprint = (device) => {
@@ -108,7 +105,7 @@ const handleDelete = async (device) => {
const handleTabChange = (tab) => {
if (tab === 'home') {
emit('go-home');
router.push('/');
}
};
@@ -118,7 +115,6 @@ onMounted(async () => {
const response = await fetch(`${baseUrl}/api/config/devices`);
const data = await response.json();
if (data.success) {
console.log('Loaded devices:', data.data);
devices.value = data.data.map(device => ({
...device,
type: '面包板(WiFi',
+54 -1
View File
@@ -1,5 +1,58 @@
import { createApp } from 'vue'
import { createRouter, createWebHistory } from 'vue-router'
import './style.css'
import App from './App.vue'
import Login from './components/Login.vue'
import Panel from './components/panel.vue'
import MainPage from './components/Main.vue'
import RoleSetting from './components/RoleSetting.vue'
import Registration from './components/Registration.vue'
createApp(App).mount('#app')
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/login',
component: Login,
name: 'login'
},
{
path: '/register',
component: Registration,
name: 'register'
},
{
path: '/panel',
component: Panel,
name: 'panel',
meta: { requiresAuth: true }
},
{
path: '/role-setting/:deviceId',
component: RoleSetting,
name: 'role-setting',
meta: { requiresAuth: true }
},
{
path: '/',
component: MainPage,
name: 'main',
meta: { requiresAuth: true }
}
]
})
router.beforeEach((to, from, next) => {
const isLoggedIn = localStorage.getItem('isLoggedIn') === 'true'
if (to.meta.requiresAuth && !isLoggedIn) {
next('/login')
} else if (to.path === '/login' && isLoggedIn) {
next('/panel')
} else {
next()
}
})
const app = createApp(App)
app.use(router)
app.mount('#app')
+3 -2
View File
@@ -7,7 +7,7 @@ export default defineConfig({
server: {
open: true, // 自动启动浏览器
host: "0.0.0.0", // localhost
port: 8001, // 端口号
port: 8002, // 端口号
https: false,
hmr: {overlay: false},
proxy: {
@@ -16,5 +16,6 @@ export default defineConfig({
changeOrigin: true
}
}
}
},
base: '/'
})