merge:main分支的manager-web和xiaozhi-server

This commit is contained in:
hrz
2025-03-29 10:49:55 +08:00
parent fe87889ccd
commit 2988e5eef2
31 changed files with 1269 additions and 1620 deletions
+1
View File
@@ -156,3 +156,4 @@ main/manager-web/node_modules
main/xiaozhi-server/models/SenseVoiceSmall/model.pt main/xiaozhi-server/models/SenseVoiceSmall/model.pt
main/xiaozhi-server/models/sherpa-onnx* main/xiaozhi-server/models/sherpa-onnx*
my_wakeup_words.mp3 my_wakeup_words.mp3
main/manager-api/.vscode
+2 -12
View File
@@ -1,20 +1,15 @@
// 引入各个模块的请求 // 引入各个模块的请求
import agent from './module/agent.js'
import device from './module/device.js'
import user from './module/user.js' import user from './module/user.js'
import ota from './module/ota.js'
import model from './module/model.js'
import admin from './module/admin.js' import admin from './module/admin.js'
/** /**
* 接口地址 * 接口地址
* 当前8002端口接口还没开发完成,暂时用 apifoxmock 接口代替 * 当前8002端口接口还没开发完成,暂时用 apifoxmock 接口代替
* 如果你想调用8002端口,就用'/xiaozhi-esp32-api/api/v1',请与vue.config.js的devServer配置相结合,方便跨域请求 * 如果你想调用8002端口,就用'/xiaozhi-esp32-api/api/v1',请与vue.config.js的devServer配置相结合,方便跨域请求
* *
*/ */
// const DEV_API_SERVICE = 'https://m1.apifoxmock.com/m1/5931378-5618560-default' const DEV_API_SERVICE = 'https://apifoxmock.com/m1/5931378-5618560-default'
// 8002开发完成完成后使用这个 // 8002开发完成完成后使用这个
const DEV_API_SERVICE = '/xiaozhi-esp32-api/api/v1' // const DEV_API_SERVICE = '/xiaozhi-esp32-api'
/** /**
* 根据开发环境返回接口url * 根据开发环境返回接口url
@@ -29,9 +24,4 @@ export function getServiceUrl() {
export default { export default {
getServiceUrl, getServiceUrl,
user, user,
agent,
device,
ota,
model,
admin
} }
-1
View File
@@ -109,7 +109,6 @@ function httpHandlerError(info, callBack) {
if (info.data.code === 'success' || info.data.code === 0 || info.data.code === undefined) { if (info.data.code === 'success' || info.data.code === 0 || info.data.code === undefined) {
return networkError return networkError
}else if (info.data.code === 401) { }else if (info.data.code === 401) {
store.commit('setToken','')
goToPage(Constant.PAGE.LOGIN, true) goToPage(Constant.PAGE.LOGIN, true)
return true return true
} else { } else {
+4 -4
View File
@@ -5,16 +5,16 @@ import {getServiceUrl} from '../api'
export default { export default {
// 用户列表 // 用户列表
getUserList(callback) { getUserList(callback) {
RequestService.sendRequest().url(`${getServiceUrl()}/admin/users`) RequestService.sendRequest().url(`${getServiceUrl()}/api/v1/admin/users`)
.method('GET') .method('GET')
.success((res) => { .success((res) => {
RequestService.clearRequestTime() RequestService.clearRequestTime()
callback(res) callback(res)
}) })
.fail(() => { .fail(() => {
// RequestService.reAjaxFun(() => { RequestService.reAjaxFun(() => {
// this.getUserList() this.getList()
// }) })
}).send() }).send()
}, },
} }
-93
View File
@@ -1,93 +0,0 @@
import RequestService from '../httpRequest'
import {getServiceUrl} from '../api'
export default {
//智能体列表
getAgentList(callback) {
RequestService.sendRequest().url(`${getServiceUrl()}/user/agent`)
.method('GET')
.success((res) => {
RequestService.clearRequestTime()
callback(res)
})
.fail((err) => {
console.error('获取智能体列表失败:', err);
}).send()
},
//添加智能体
addAgent(agentName, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/user/agent`)
.method('POST')
.data({agentName})
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.fail((err) => {
console.error('添加智能体失败:', err);
}).send();
},
//获取智能体配置
getAgentConfig(agentId, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/user/agent/${agentId}`)
.method('GET')
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.fail((err) => {
console.error('获取智能体配置失败:', err);
// RequestService.reAjaxFun(() => {
// this.getAgentConfig(agent_id, callback);
// });
}).send();
},
//配置智能体
saveAgentConfig(agentId, configData, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/user/agent/${agentId}`)
.method('PUT')
.data(configData)
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.fail((err) => {
console.error('保存智能体配置失败:', err);
// RequestService.reAjaxFun(() => {
// this.saveAgentConfig(device_id, configData, callback);
// });
}).send();
},
//删除智能体
delAgent(agentId, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/user/agent/${agentId}`)
.method('DELETE')
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.fail((err) => {
console.error('删除智能体失败:', err);
// RequestService.reAjaxFun(() => {
// this.deleteAgent(agent_id, callback);
// });
}).send();
},
//智能体模板列表
getAgentTemplateList(callback) {
RequestService.sendRequest().url(`${getServiceUrl()}/user/agent/template`)
.method('GET')
.success((res) => {
RequestService.clearRequestTime()
callback(res)
})
.fail((err) => {
console.error('获取智能体模板列表失败:', err);
}).send()
},
}
@@ -1,81 +0,0 @@
import RequestService from '../httpRequest'
import {getServiceUrl} from '../api'
export default {
//设备列表
getDeviceList(agent_id, page, callback) {
RequestService.sendRequest().url(`${getServiceUrl()}/user/agent/device/bind/${agent_id}`)
.method('GET')
.data(page)
.success((res) => {
RequestService.clearRequestTime()
callback(res)
})
.fail((err) => {
console.error('获取设备列表失败:', err);
}).send()
},
//绑定设备
bindDevice(agentId, deviceCode, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/user/agent/device/bind/${agentId}`)
.method('POST')
.data({deviceCode})
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.fail((err) => {
console.error('绑定设备失败:', err);
// RequestService.reAjaxFun(() => {
// this.addDevice(agentId, deviceCode, callback);
// });
}).send();
},
// 解绑设备
unbindDevice(deviceId, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/user/agent/device/unbind/${deviceId}`)
.method('PUT')
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.fail((err) => {
console.error('解绑设备失败:', err);
// RequestService.reAjaxFun(() => {
// this.unbindDevice(deviceId, callback);
// });
}).send()
},
// 切换设备OTA升级状态
toggleAutoUpdate(deviceId, autoUpdate, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/user/agent/device/autoUpdate/${deviceId}`)
.method('POST')
.data({autoUpdate})
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.fail((err) => {
console.error('切换设备OTA升级状态失败:', err);
}).send()
},
// 设置设备别名
setAlias(deviceId, alias, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/user/agent/device/alias/${deviceId}`)
.method('POST')
.data({alias})
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.fail((err) => {
console.error('设置设备别名失败:', err);
}).send()
},
}
-32
View File
@@ -1,32 +0,0 @@
import RequestService from '../httpRequest'
import {getServiceUrl} from '../api'
export default {
//模型配置列表
getModelList(callback) {
RequestService.sendRequest().url(`${getServiceUrl()}/model/list`)
.method('GET')
.success((res) => {
RequestService.clearRequestTime()
callback(res)
})
.fail((err) => {
console.error('获取模型配置列表失败:', err);
}).send()
},
//音色列表
getTtsVoiceList(ttsModelId, callback) {
RequestService.sendRequest().url(`${getServiceUrl()}/tts/voice`)
.method('GET')
.data({ttsModelId})
.success((res) => {
RequestService.clearRequestTime()
callback(res)
})
.fail((err) => {
console.error('获取音色列表失败:', err);
}).send()
},
}
-67
View File
@@ -1,67 +0,0 @@
import RequestService from '../httpRequest'
import {getServiceUrl} from '../api'
export default {
//设备OTA列表
getOtaList(page, callback) {
RequestService.sendRequest().url(`${getServiceUrl()}/ota/sys/getOtalist`)
.method('GET')
.data(page)
.success((res) => {
RequestService.clearRequestTime()
callback(res)
})
.fail((err) => {
console.error('获取设备OTA列表失败:', err);
}).send()
},
//添加设备OTA
saveOta(ota, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/ota/sys/save`)
.method('POST')
.data(ota)
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.fail((err) => {
console.error('添加设备OTA失败:', err);
}).send();
},
//配置设备OTA
updateOta(ota, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/ota/sys/update`)
.method('POST')
.data(ota)
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.fail((err) => {
console.error('保存设备OTA配置失败:', err);
// RequestService.reAjaxFun(() => {
// this.saveAgentConfig(device_id, configData, callback);
// });
}).send();
},
//切换设备OTA可用状态失败
toggleEnabled(ota, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/ota/sys/toggleEnabled`)
.method('POST')
.data(ota)
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.fail((err) => {
console.error('切换设备OTA可用状态失败:', err);
// RequestService.reAjaxFun(() => {
// this.deleteAgent(agent_id, callback);
// });
}).send();
},
}
+176 -46
View File
@@ -5,25 +5,25 @@ import {getServiceUrl} from '../api'
export default { export default {
// 登录 // 登录
login(loginForm, callback) { login(loginForm, callback) {
RequestService.sendRequest().url(`${getServiceUrl()}/user/login`) RequestService.sendRequest()
.url(`${getServiceUrl()}/api/v1/user/login`)
.method('POST') .method('POST')
.data(loginForm) .data(loginForm)
.success((res) => { .success((res) => {
RequestService.clearRequestTime() RequestService.clearRequestTime()
callback(res) callback(res)
}) })
.fail((err) => { .fail(() => {
console.error('登录失败:', err); RequestService.reAjaxFun(() => {
// RequestService.reAjaxFun(() => { this.login(loginForm, callback)
// this.login(loginForm, callback) })
// })
}).send() }).send()
}, },
// 获取验证码 // 获取验证码
getCaptcha(uuid, callback) { getCaptcha(uuid, callback) {
RequestService.sendRequest() RequestService.sendRequest()
.url(`${getServiceUrl()}/user/captcha?uuid=${uuid}`) .url(`${getServiceUrl()}/api/v1/user/captcha?uuid=${uuid}`)
.method('GET') .method('GET')
.type('blob') .type('blob')
.header({ .header({
@@ -36,57 +36,59 @@ export default {
callback(res); callback(res);
}) })
.fail((err) => { // 添加错误参数 .fail((err) => { // 添加错误参数
console.error('获取验证码失败:', err);
}).send() }).send()
}, },
// 注册账号 // 注册账号
register(registerForm, callback) { register(registerForm, callback) {
RequestService.sendRequest().url(`${getServiceUrl()}/user/register`).method('POST') RequestService.sendRequest()
.url(`${getServiceUrl()}/api/v1/user/register`)
.method('POST')
.data(registerForm) .data(registerForm)
.success((res) => { .success((res) => {
RequestService.clearRequestTime() RequestService.clearRequestTime()
callback(res) callback(res)
}) })
.fail((err) => { .fail(() => {
console.error('注册账号失败:', err);
}).send() }).send()
}, },
// 获取所有模型名称 // 保存设备配置
getModelNames(callback) { saveDeviceConfig(device_id, configData, callback) {
RequestService.sendRequest() RequestService.sendRequest()
.url(`${getServiceUrl()}/models/names`) .url(`${getServiceUrl()}/api/v1/user/configDevice/${device_id}`)
.method('PUT')
.data(configData)
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.fail((err) => {
console.error('保存配置失败:', err);
RequestService.reAjaxFun(() => {
this.saveDeviceConfig(device_id, configData, callback);
});
}).send();
},
// 获取智能体列表
getAgentList(callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/api/v1/user/agent`)
.method('GET') .method('GET')
.success((res) => { .success((res) => {
RequestService.clearRequestTime(); RequestService.clearRequestTime();
callback(res); callback(res);
}) })
.fail(() => { .fail(() => {
// RequestService.reAjaxFun(() => { RequestService.reAjaxFun(() => {
// this.getModelNames(callback); this.getAgentList(callback);
// }); });
}).send(); }).send();
}, },
// 用户信息获取
// 获取模型音色
getModelVoices(modelName, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/models/${modelName}/voices`)
.method('GET')
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.fail(() => {
// RequestService.reAjaxFun(() => {
// this.getModelVoices(modelName, callback);
// });
}).send();
},
// 获取用户信息
getUserInfo(callback) { getUserInfo(callback) {
RequestService.sendRequest() RequestService.sendRequest()
.url(`${getServiceUrl()}/user/info`) .url(`${getServiceUrl()}/api/v1/user/info`)
.method('GET') .method('GET')
.success((res) => { .success((res) => {
RequestService.clearRequestTime() RequestService.clearRequestTime()
@@ -94,29 +96,157 @@ export default {
}) })
.fail((err) => { .fail((err) => {
console.error('接口请求失败:', err) console.error('接口请求失败:', err)
// RequestService.reAjaxFun(() => { RequestService.reAjaxFun(() => {
// this.getUserInfo(callback) this.getUserInfo(callback)
// }) })
}).send() }).send()
}, },
// 添加智能体
addAgent(agentName, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/api/v1/user/agent`)
.method('POST')
.data({name: agentName})
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.fail(() => {
RequestService.reAjaxFun(() => {
this.addAgent(agentName, callback);
});
}).send();
},
// 删除智能体
deleteAgent(agentId, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/api/v1/user/agent/${agentId}`)
.method('DELETE')
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.fail(() => {
RequestService.reAjaxFun(() => {
this.deleteAgent(agentId, callback);
});
}).send();
},
// 修改用户密码 // 修改用户密码
changePassword(oldPassword, newPassword, successCallback, errorCallback) { changePassword(oldPassword, newPassword, successCallback, errorCallback) {
RequestService.sendRequest() RequestService.sendRequest()
.url(`${getServiceUrl()}/user/change-password`) // 修改URL .url(`${getServiceUrl()}/api/v1/user/change-password`)
.method('PUT') // 修改方法为PUT .method('PUT')
.data({ .data({
old_password: oldPassword, // 修改参数名 old_password: oldPassword,
new_password: newPassword // 修改参数名 new_password: newPassword,
}) })
.success((res) => { .success((res) => {
RequestService.clearRequestTime(); RequestService.clearRequestTime();
successCallback(res); successCallback(res);
}) })
.fail((error) => { .fail((error) => {
// RequestService.reAjaxFun(() => { RequestService.reAjaxFun(() => {
// this.changePassword(oldPassword, newPassword, successCallback, errorCallback); this.changePassword(oldPassword, newPassword, successCallback, errorCallback);
// }); });
}) })
.send(); .send();
}, },
// 获取智能体配置
getDeviceConfig(deviceId, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/api/v1/user/agent/${deviceId}`)
.method('GET')
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.fail((err) => {
console.error('获取配置失败:', err);
RequestService.reAjaxFun(() => {
this.getDeviceConfig(deviceId, callback);
});
}).send();
},
// 配置智能体
updateAgentConfig(agentId, configData, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/api/v1/user/agent/${agentId}`)
.method('PUT')
.data(configData)
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.fail(() => {
RequestService.reAjaxFun(() => {
this.updateAgentConfig(agentId, configData, callback);
});
}).send();
},
// 已绑设备
getAgentBindDevices(agentId, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/api/v1/user/agent/device/bind/${agentId}`)
.method('GET')
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.fail((err) => {
console.error('获取设备列表失败:', err);
RequestService.reAjaxFun(() => {
this.getAgentBindDevices(agentId, callback);
});
}).send();
},
// 解绑设备
unbindDevice(device_id, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/api/v1/user/device/unbind/${device_id}`)
.method('PUT')
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.fail((err) => {
console.error('解绑设备失败:', err);
RequestService.reAjaxFun(() => {
this.unbindDevice(device_id, callback);
});
}).send();
},
// 绑定设备
bindDevice(agentId, code, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/api/v1/user/agent/device/bind/${agentId}`)
.method('POST')
.data({ code })
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.fail((err) => {
console.error('绑定设备失败:', err);
RequestService.reAjaxFun(() => {
this.bindDevice(agentId, code, callback);
});
}).send();
},
// 新增方法:获取智能体模板
getAgentTemplate(templateName, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/api/v1/user/agent/templateId`)
.method('GET')
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.fail((err) => {
console.error('获取模板失败:', err);
RequestService.reAjaxFun(() => {
this.getAgentTemplate(templateName, callback);
});
}).send();
},
} }
@@ -1,15 +1,16 @@
<template> <template>
<el-dialog :visible.sync="visible" width="400px" center :close-on-click-modal="false" :close-on-press-escape="false"> <el-dialog :visible.sync="visible" width="400px" center >
<div style="margin: 0 10px 10px;display: flex;align-items: center;gap: 10px;font-weight: 700;font-size: 20px;text-align: left;color: #3d4566;"> <div style="margin: 0 10px 10px;display: flex;align-items: center;gap: 10px;font-weight: 700;font-size: 20px;text-align: left;color: #3d4566;">
<div style="width: 40px;height: 40px;border-radius: 50%;background: #5778ff;display: flex;align-items: center;justify-content: center;"> <div style="width: 40px;height: 40px;border-radius: 50%;background: #5778ff;display: flex;align-items: center;justify-content: center;">
<i class="el-icon-cpu" style="color: #fff;"></i> <img src="@/assets/home/equipment.png" alt="" style="width: 18px;height: 15px;" />
</div> </div>
添加设备 添加设备
</div> </div>
<div style="height: 1px;background: #e8f0ff;" /> <div style="height: 1px;background: #e8f0ff;" />
<div style="margin: 22px 15px;"> <div style="margin: 22px 15px;">
<div style="font-weight: 400;font-size: 14px;text-align: left;color: #3d4566;"> <div style="font-weight: 400;font-size: 14px;text-align: left;color: #3d4566;">
<div style="color: red;display: inline-block;">*</div>验证码 <div style="color: red;display: inline-block;">*</div>
<span style="font-size: 11px"> 验证码</span>
</div> </div>
<div class="input-46" style="margin-top: 12px;"> <div class="input-46" style="margin-top: 12px;">
<el-input placeholder="请输入设备播报的6位数验证码.." v-model="deviceCode" /> <el-input placeholder="请输入设备播报的6位数验证码.." v-model="deviceCode" />
@@ -32,20 +33,46 @@
export default { export default {
name: 'AddDeviceDialog', name: 'AddDeviceDialog',
props: { props: {
visible: { type: Boolean, required: true } visible: { type: Boolean, required: true },
agentId: { type: String, required: true }
}, },
data() { data() {
return { deviceCode: "" } return {
deviceCode: "",
loading: false,
}
}, },
methods: { methods: {
confirm() { confirm() {
if (!/^\d{6}$/.test(this.deviceCode)) { if (!/^\d{6}$/.test(this.deviceCode)) {
this.$message.error('请输入6位数字设备验证码'); this.$message.error('请输入6位数字验证码');
return; return;
} }
this.$emit('confirm',this.deviceCode) this.loading = true;
this.$emit('update:visible', false) import('@/apis/module/user').then(({ default: userApi }) => {
this.deviceCode = "" userApi.bindDevice(
this.agentId,
this.deviceCode, ({data}) => {
this.loading = false;
if (data.code === 0) {
this.$emit('refresh');
this.$message.success('设备绑定成功');
this.closeDialog();
} else {
this.$message.error(data.msg || '绑定失败');
}
}
);
}).catch((err) => {
this.loading = false;
console.error('API模块加载失败:', err);
this.$message.error('绑定服务不可用');
});
},
closeDialog() {
this.$emit('update:visible', false);
this.deviceCode = '';
}, },
cancel() { cancel() {
this.$emit('update:visible', false) this.$emit('update:visible', false)
@@ -56,6 +83,13 @@ export default {
</script> </script>
<style scoped> <style scoped>
.input-46 {
border: 1px solid #e4e6ef;
background: #f6f8fb;
border-radius: 10px;
}
.dialog-btn { .dialog-btn {
cursor: pointer; cursor: pointer;
flex: 1; flex: 1;
@@ -1,104 +0,0 @@
<template>
<el-dialog :visible.sync="visible" width="600px" center :close-on-click-modal="false" :close-on-press-escape="false">
<div style="margin: 0 10px 10px;display: flex;align-items: center;gap: 10px;font-weight: 700;font-size: 20px;text-align: left;color: #3d4566;">
<div style="width: 40px;height: 40px;border-radius: 50%;background: #5778ff;display: flex;align-items: center;justify-content: center;">
<i class="el-icon-lightning" style="color: #fff;"></i>
</div>
{{ data.id?'编辑':'添加' }}设备
</div>
<div style="height: 1px;background: #e8f0ff;" />
<div style="margin: 22px 15px;">
<el-form ref="form" :model="data" label-width="80px">
<el-form-item label="设备型号" prop="board" :rules="[{ required: true, message: '请输入设备型号', trigger: 'blur' }]">
<el-input v-model="data.board" placeholder="请输入设备型号..."></el-input>
</el-form-item>
<el-form-item label="固件版本" prop="appVersion" :rules="[{ required: true, message: '请输入固件版本', trigger: 'blur' }]">
<el-input v-model="data.appVersion" placeholder="请输入固件版本..."></el-input>
</el-form-item>
<el-form-item label="升级地址" prop="url" :rules="[{ required: true, message: '请输入升级地址', trigger: 'blur' },{ type: 'url', message: '请输入正确的升级地址', trigger: 'blur' }]">
<el-input type="textarea" v-model="data.url" placeholder="请输入升级地址..."></el-input>
</el-form-item>
<el-form-item label="是否启用">
<el-switch v-model="data.isEnabled" size="mini" :active-value="1" :inactive-value="0" active-color="#13ce66" inactive-color="#ff4949"></el-switch>
</el-form-item>
<el-form-item label-width="0">
<div style="display: flex;margin: 15px 15px;gap: 7px;">
<div class="dialog-btn" @click="confirm">
确定
</div>
<div class="dialog-btn" style="background: #e6ebff;border: 1px solid #adbdff;color: #5778ff;" @click="cancel">
取消
</div>
</div>
</el-form-item>
</el-form>
</div>
</el-dialog>
</template>
<script>
export default {
name: 'AddDeviceOtaDialog',
props: {
visible: { type: Boolean, required: true },
data: { type: Object,
default: () => ({
id: "",
board: "",
appVersion: "",
url: "",
isEnabled: 1
})
}
},
data() {
return {}
},
methods: {
confirm() {
this.$refs['form'].validate((valid) => {
if (valid) {
this.$emit(this.data.id?'confirmUpdate':'confirmSave',this.data)
this.$emit('update:visible', false)
} else {
return false;
}
});
},
cancel() {
this.$emit('update:visible', false)
}
}
}
</script>
<style scoped>
.dialog-btn {
cursor: pointer;
flex: 1;
border-radius: 23px;
background: #5778ff;
height: 40px;
font-weight: 500;
font-size: 12px;
color: #fff;
line-height: 40px;
text-align: center;
}
::v-deep .el-dialog {
border-radius: 15px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
::v-deep .el-dialog__headerbtn {
display: none;
}
::v-deep .el-dialog__body {
padding: 4px 6px;
}
::v-deep .el-dialog__header{
padding: 10px;
}
</style>
@@ -1,18 +1,18 @@
<template> <template>
<el-dialog :visible.sync="visible" width="400px" center :close-on-click-modal="false" :close-on-press-escape="false"> <el-dialog :visible.sync="visible" width="400px" center>
<div style="margin: 0 10px 10px;display: flex;align-items: center;gap: 10px;font-weight: 700;font-size: 20px;text-align: left;color: #3d4566;"> <div style="margin: 0 10px 10px;display: flex;align-items: center;gap: 10px;font-weight: 700;font-size: 20px;text-align: left;color: #3d4566;">
<div style="width: 40px;height: 40px;border-radius: 50%;background: #5778ff;display: flex;align-items: center;justify-content: center;"> <div style="width: 40px;height: 40px;border-radius: 50%;background: #5778ff;display: flex;align-items: center;justify-content: center;">
<i class="el-icon-cpu" style="color: #fff;"></i> <img src="@/assets/home/equipment.png" alt="" style="width: 18px;height: 15px;" />
</div> </div>
添加智 添加智
</div> </div>
<div style="height: 1px;background: #e8f0ff;" /> <div style="height: 1px;background: #e8f0ff;" />
<div style="margin: 22px 15px;"> <div style="margin: 22px 15px;">
<div style="font-weight: 400;font-size: 14px;text-align: left;color: #3d4566;"> <div style="font-weight: 400;font-size: 14px;text-align: left;color: #3d4566;">
<div style="color: red;display: inline-block;">*</div>体名称 <div style="color: red;display: inline-block;">*</div> 体名称
</div> </div>
<div style="margin-top: 12px;"> <div class="input-46" style="margin-top: 12px;">
<el-input placeholder="请输入智体名称.." v-model="agentName" /> <el-input placeholder="请输入智体名称.." v-model="wisdomBodyName" />
</div> </div>
</div> </div>
<div style="display: flex;margin: 15px 15px;gap: 7px;"> <div style="display: flex;margin: 15px 15px;gap: 7px;">
@@ -33,36 +33,42 @@ import userApi from '@/apis/module/user';
export default { export default {
name: 'AddAgentDialog', name: 'AddWisdomBodyDialog',
props: { props: {
visible: { type: Boolean, required: true } visible: { type: Boolean, required: true }
}, },
data() { data() {
return { agentName: "" } return { wisdomBodyName: "" }
}, },
methods: { methods: {
confirm() { confirm() {
if (!this.agentName.trim()) { if (!this.wisdomBodyName.trim()) {
this.$message.error('请输入智体名称'); this.$message.error('请输入智体名称');
return; return;
} }
this.$emit('confirm',this.agentName) userApi.addAgent(this.wisdomBodyName, (res) => {
// userApi.addAgent(this.agentName, (res) => { this.$message.success('添加成功');
// this.$message.success(''); this.$emit('confirm', res);
// this.$emit('confirm', res);
this.$emit('update:visible', false); this.$emit('update:visible', false);
this.agentName = ""; this.wisdomBodyName = "";
// }); });
}, },
cancel() { cancel() {
this.$emit('update:visible', false) this.$emit('update:visible', false)
this.agentName = "" this.wisdomBodyName = ""
} }
} }
} }
</script> </script>
<style scoped> <style scoped>
.input-46 {
border: 1px solid #e4e6ef;
background: #f6f8fb;
border-radius: 15px;
}
.dialog-btn { .dialog-btn {
cursor: pointer; cursor: pointer;
flex: 1; flex: 1;
@@ -1,77 +0,0 @@
<template>
<div class="agent-item">
<div style="display: flex;justify-content: space-between;">
<div style="font-weight: 700;font-size: 18px;text-align: left;color: #3d4566;">
{{ agent.agentName }}
</div>
<div>
<img src="@/assets/home/delete.png" alt=""
style="width: 18px;height: 18px;" @click="$emit('del', agent.id)" />
<!-- <img src="@/assets/home/info.png" alt="" style="width: 18px;height: 18px;" /> -->
</div>
</div>
<div class="agent-name">
角色音色{{ agent.ttsModelName }}
</div>
<div class="agent-name">
语言模型{{ agent.llmModelName }}
</div>
<div class="agent-name">
最近对话{{ agent.lastConnectedAt }}
</div>
<div style="display: flex;justify-content: space-between;align-items: center;gap: 10px;">
<div class="settings-btn" @click="$emit('configure', agent.id)">
配置角色
</div>
<div class="settings-btn" @click="$emit('asr', agent.id)">
声纹识别
</div>
<div class="settings-btn" @click="$emit('chat', agent.id)">
历史对话
</div>
<div class="settings-btn" @click="$emit('device', agent.id)">
设备数量({{agent.deviceCount}})
</div>
</div>
</div>
</template>
<script>
export default {
name: 'AgentItem',
props: {
agent: { type: Object, required: true }
},
data() {
return { }
}
}
</script>
<style scoped>
.agent-item {
width: 342px;
border-radius: 20px;
background: #fafcfe;
padding: 22px 22px 11px 22px;
box-sizing: border-box;
}
.agent-name {
margin: 7px 0 10px;
font-weight: 400;
font-size: 11px;
color: #3d4566;
text-align: left;
}
.settings-btn {
font-weight: 500;
font-size: 10px;
color: #5778ff;
background: #e6ebff;
padding: 2px 10px;
line-height: 21px;
cursor: pointer;
border-radius: 14px;
}
</style>
@@ -0,0 +1,98 @@
<template>
<div class="device-item">
<div style="display: flex;justify-content: space-between;">
<div style="font-weight: 700;font-size: 18px;text-align: left;color: #3d4566;">
{{ device.agentName }}
</div>
<div>
<img src="@/assets/home/delete.png" alt=""
style="width: 18px;height: 18px;margin-right: 10px;" @click.stop="handleDelete" />
<img src="@/assets/home/info.png" alt="" style="width: 18px;height: 18px;" />
</div>
</div>
<div class="device-name">
设备型号{{ device.ttsModelName }}
</div>
<div class="device-name">
音色模型{{ device.ttsVoiceName }}
</div>
<div style="display: flex;gap: 10px;align-items: center;">
<div class="settings-btn" @click="handleConfigure">
配置角色
</div>
<div class="settings-btn">
声纹识别
</div>
<div class="settings-btn">
历史对话
</div>
<div class="settings-btn" @click="handleDeviceManage">
设备管理
</div>
</div>
<div class="version-info">
<div>最近对话{{ device.lastConnectedAt }}</div>
</div>
</div>
</template>
<script>
export default {
name: 'DeviceItem',
props: {
device: { type: Object, required: true }
},
data() {
return { switchValue: false }
},
methods: {
handleDelete() {
this.$emit('delete', this.device.agentId)
},
handleConfigure() {
this.$router.push({ path: '/role-config', query: { agentId: this.device.agentId } });
},
handleDeviceManage() {
this.$router.push({ path: '/device-management', query: { agentId: this.device.agentId } });
}
}
}
</script>
<style scoped>
.device-item {
width: 342px;
border-radius: 20px;
background: #fafcfe;
padding: 22px;
box-sizing: border-box;
}
.device-name {
margin: 7px 0 10px;
font-weight: 400;
font-size: 11px;
color: #3d4566;
text-align: left;
}
.settings-btn {
font-weight: 500;
font-size: 10px;
color: #5778ff;
background: #e6ebff;
width: 57px;
height: 21px;
line-height: 21px;
cursor: pointer;
border-radius: 14px;
}
.version-info {
display: flex;
justify-content: space-between;
margin-top: 15px;
font-size: 10px;
color: #979db1;
font-weight: 400;
}
</style>
@@ -1,29 +0,0 @@
<template>
<div v-if="visible" class="message">
{{message}}
</div>
</template>
<script>
export default {
name: 'Footer',
props: {
visible: { type: Boolean, required: true },
message: { type: String, required: false, default: '©2025 xiaozhi-esp32-server' }
},
data() {
return { }
},
methods: {
}
}
</script>
<style scoped>
.message {
font-size: 14px;
font-weight: 400;
padding: 20px;
color: #979db1;
}
</style>
+188 -107
View File
@@ -1,58 +1,56 @@
<template> <template>
<el-header class="header"> <el-header class="header">
<div style="display: flex;justify-content: space-between;margin-top: 6px;"> <div class="header-container">
<div style="display: flex;align-items: center;gap: 10px;"> <!-- 左侧元素 -->
<img alt="" src="@/assets/xiaozhi-logo.png" style="width: 42px;height: 42px;"/> <div class="header-left">
<img alt="" src="@/assets/xiaozhi-ai.png" style="width: 58px;height: 12px;"/> <img alt="" src="@/assets/xiaozhi-logo.png" class="logo-img"/>
<div ref="menu-code_agent" class="ml-20 menu-btn" @click="goToPage('/home')"> <img alt="" src="@/assets/xiaozhi-ai.png" class="brand-img"/>
<!-- <img alt="" src="@/assets/home/equipment.png" style="width: 12px;height: 10px;"/> --> </div>
<i class="el-icon-cpu"></i>
智能体 <!-- 中间导航菜单 -->
<div class="header-center">
<div class="equipment-management" :class="{ 'active-tab': $route.path === '/home' }" @click="goHome">
<img alt="" src="@/assets/header/roboot.png" :style="{ filter: $route.path === '/home' ? 'brightness(0) invert(1)' : 'None' }"/>
智能体管理
</div> </div>
<div ref="menu-code_console" class="menu-btn"> <div class="equipment-management" :class="{ 'active-tab': $route.path === '/user-management' }" @click="goUserManagement">
<i class="el-icon-microphone" style="color: #979db1;"/> <img alt="" src="@/assets/header/user_management.png" :style="{ filter: $route.path === '/user-management' ? 'brightness(0) invert(1)' : 'None' }"/>
声音复刻
</div>
<div ref="menu-code_console" class="menu-btn">
<i class="el-icon-s-grid" style="color: #979db1;"/>
控制台
</div>
<div ref="menu-code_user" class="menu-btn" @click="goToPage('/user-management')">
<i class="el-icon-user"></i>
用户管理 用户管理
</div> </div>
<div ref="menu-code_model" class="menu-btn" @click="goToPage('/model-config')"> <div class="equipment-management" :class="{ 'active-tab': $route.path === '/model-config' }" @click="goModelConfig">
<i class="el-icon-news"></i> <img alt="" src="@/assets/header/model_config.png" :style="{ filter: $route.path === '/model-config' ? 'brightness(0) invert(1)' : 'None' }"/>
模型配置 模型配置
</div> </div>
<div ref="menu-code_ota" class="menu-btn" @click="goToPage('/ota')">
<i class="el-icon-lightning"></i>
OTA管理
</div>
</div> </div>
<div style="display: flex;align-items: center;gap: 7px; margin-top: 2px;">
<div class="serach-box"> <!-- 右侧元素 -->
<el-input v-model="serach" placeholder="输入名称搜索.." style="border: none; background: transparent;" <div class="header-right">
@keyup.enter.native="handleSearch"/> <div class="search-container">
<img alt="" src="@/assets/home/search.png" <el-input
style="width: 14px;height: 14px;margin-right: 11px;cursor: pointer;" @click="handleSearch"/> v-model="serach"
placeholder="输入名称搜索.."
class="custom-search-input"
@keyup.enter.native="handleSearch"
>
<i slot="suffix" class="el-icon-search search-icon" @click="handleSearch"></i>
</el-input>
</div> </div>
<img alt="" src="@/assets/home/avatar.png" style="width: 21px;height: 21px;"/> <img alt="" src="@/assets/home/avatar.png" class="avatar-img"/>
<el-dropdown trigger="click"> <el-dropdown trigger="click" class="user-dropdown">
<span class="el-dropdown-link"> <span class="el-dropdown-link">
{{ userInfo.username || '加载中...' }}<i class="el-icon-arrow-down el-icon--right"></i> {{ userInfo.mobile || '加载中...' }}<i class="el-icon-arrow-down el-icon--right"></i>
</span> </span>
<el-dropdown-menu slot="dropdown"> <el-dropdown-menu slot="dropdown">
<el-dropdown-item icon="el-icon-user" @click.native="">个人中心</el-dropdown-item> <el-dropdown-item icon="el-icon-plus" @click.native="">个人中心</el-dropdown-item>
<el-dropdown-item icon="el-icon-key" @click.native="">修改密码</el-dropdown-item> <el-dropdown-item icon="el-icon-circle-plus" @click.native="showChangePasswordDialog">修改密码</el-dropdown-item>
<el-dropdown-item icon="el-icon-connection" @click.native="handleLogout">退出登录</el-dropdown-item> <el-dropdown-item icon="el-icon-circle-plus-outline" @click.native="">退出登录</el-dropdown-item>
</el-dropdown-menu> </el-dropdown-menu>
</el-dropdown> </el-dropdown>
</div> </div>
</div> </div>
<!-- 修改密码弹窗 --> <!-- 修改密码弹窗 -->
<ChangePasswordDialog :visible.sync="isChangePasswordDialogVisible" /> <ChangePasswordDialog :visible.sync="isChangePasswordDialogVisible"/>
</el-header> </el-header>
</template> </template>
@@ -76,33 +74,19 @@ export default {
isChangePasswordDialogVisible: false // 控制修改密码弹窗的显示 isChangePasswordDialogVisible: false // 控制修改密码弹窗的显示
} }
}, },
watch: {
'$route.meta.menuCode': {
handler(to, from) {
const meta = this.$route.meta;
if(meta && meta.menuCode){
this.$nextTick(() => {
const menu = this.$refs[`menu-code_`+ meta.menuCode]
menu && menu.classList.add('active')
})
}
},
immediate: true,
deep: true
}
},
mounted() { mounted() {
this.fetchUserInfo() this.fetchUserInfo()
}, },
methods: { methods: {
handleLogout() { goHome() {
// 退出登录
this.$store.commit('setToken','')
this.$router.push('/login')
},
goToPage(path) {
// 跳转到首页 // 跳转到首页
this.$router.replace(path) this.$router.push('/')
},
goUserManagement() {
this.$router.push('/user-management')
},
goModelConfig() {
this.$router.push('/model-config')
}, },
// 获取用户信息 // 获取用户信息
fetchUserInfo() { fetchUserInfo() {
@@ -140,71 +124,168 @@ export default {
</script> </script>
<style scoped> <style scoped>
.ml-20 {
margin-left: 20px;
}
.menu-btn {
padding: 8px 14px;
border-radius: 8px;
background: #fff;
color: #979db1;
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
font-weight: 500;
font-size: 14px;
cursor: pointer;
}
.menu-btn.active {
background: #5778ff;
color: #fff;
}
.header { .header {
background: #f6fcfe66; background: #f6fcfe66;
border: 1px solid #fff; border: 1px solid #fff;
height: 53px !important; height: 53px !important;
min-width: 900px; /* 设置最小宽度防止过度压缩 */
overflow: hidden;
} }
.serach-box { .header-container {
display: flex; display: flex;
width: 220px; justify-content: space-between;
align-items: center;
height: 100%;
padding: 0 10px;
}
.header-left {
display: flex;
align-items: center;
gap: 10px;
min-width: 120px;
}
.logo-img {
width: 42px;
height: 42px;
}
.brand-img {
width: 58px;
height: 12px;
}
.header-center {
display: flex;
align-items: center;
gap: 25px;
position: absolute;
left: 50%;
transform: translateX(-50%);
}
.header-right {
display: flex;
align-items: center;
gap: 7px;
min-width: 300px;
justify-content: flex-end;
}
.equipment-management {
width: 82px;
height: 24px;
border-radius: 12px;
background: #deeafe;
display: flex;
justify-content: center;
font-size: 10px;
font-weight: 500;
gap: 7px;
color: #3d4566;
margin-left: 1px;
align-items: center;
transition: all 0.3s ease;
cursor: pointer;
flex-shrink: 0; /* 防止导航按钮被压缩 */
}
.equipment-management.active-tab {
background: #5778ff !important;
color: #fff !important;
}
.equipment-management img {
width: 15px;
height: 13px;
}
.search-container {
margin-right: 15px;
min-width: 150px;
flex-grow: 1;
max-width: 220px;
}
.custom-search-input >>> .el-input__inner {
height: 30px; height: 30px;
border-radius: 15px; border-radius: 15px;
background-color: #f6fcfe66; background-color: #e2e5f8;
border: 1px solid #e4e6ef; border: 1px solid #e4e6ef;
align-items: center; padding-left: 15px;
padding: 0 7px; font-size: 12px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
margin-right: 15px;
}
.serach-box /deep/ .el-input__inner {
border-radius: 15px;
height: 100%;
width: 100%; width: 100%;
border: 0;
background: transparent;
padding-left: 12px;
} }
.user-info { .search-icon {
font-weight: 600;
font-size: 12px;
letter-spacing: -0.02px;
text-align: left;
color: #3d4566;
}
.el-dropdown-link {
cursor: pointer; cursor: pointer;
color: #5778ff; color: #909399;
margin-right: 8px;
font-size: 14px;
line-height: 30px;
} }
.el-icon-arrow-down { .avatar-img {
font-size: 12px; width: 21px;
height: 21px;
flex-shrink: 0;
} }
.user-dropdown {
flex-shrink: 0;
}
/* 响应式调整 */
@media (max-width: 1200px) {
.header-center {
gap: 14px;
}
.equipment-management {
width: 70px;
font-size: 9px;
}
}
@media (max-width: 1024px) {
.search-container {
margin-right: 10px;
max-width: 150px;
}
.header-right {
gap: 5px;
}
}
@media (max-width: 900px) {
.header-left {
margin-right: auto;
}
.search-container {
max-width: 150px;
}
}
@media (max-width: 768px) {
.search-container {
max-width: 145px;
}
.custom-search-input >>> .el-input__inner {
padding-left: 10px;
font-size: 11px;
}
}
@media (max-width: 600px) {
.search-container {
max-width: 120px;
min-width: 100px;
}
}
</style> </style>
+1 -2
View File
@@ -122,10 +122,9 @@ export default {
} }
.model-menu { .model-menu {
border: 1px solid #ebeef5; border-right: 1px solid #ebeef5;
height: calc(100vh - 300px); height: calc(100vh - 300px);
background-color: #fafafa; background-color: #fafafa;
margin-right: 20px;
} }
.search-operate { .search-operate {
+12 -36
View File
@@ -12,10 +12,10 @@ const routes = [
} }
}, },
{ {
path: '/register', path: '/role-config',
name: 'Register', name: 'RoleConfig',
component: function () { component: function () {
return import('../views/register.vue') return import('../views/roleConfig.vue')
} }
}, },
{ {
@@ -28,62 +28,38 @@ const routes = [
{ {
path: '/home', path: '/home',
name: 'home', name: 'home',
meta: {
menuCode: 'agent',
},
component: function () { component: function () {
return import('../views/home.vue') return import('../views/home.vue')
} }
}, },
{ {
path: '/role-config', path: '/register',
name: 'RoleConfig', name: 'Register',
meta: {
menuCode: 'agent',
},
component: function () { component: function () {
return import('../views/roleConfig.vue') return import('../views/register.vue')
} }
}, },
// 设备管理页面路由
{ {
path: '/device', path: '/device-management',
name: 'Device', name: 'DeviceManagement',
meta: {
menuCode: 'agent',
},
component: function () { component: function () {
return import('../views/device.vue') return import('../views/DeviceManagement.vue')
}
},
{
path: '/ota',
name: 'Ota',
meta: {
menuCode: 'ota',
},
component: function () {
return import('../views/ota.vue')
} }
}, },
// 添加用户管理路由 // 添加用户管理路由
{ {
path: '/user-management', path: '/user-management',
name: 'UserManagement', name: 'UserManagement',
meta: {
menuCode: 'user',
},
component: function () { component: function () {
return import('../views/userManagement.vue') return import('../views/UserManagement.vue')
} }
}, },
{ {
path: '/model-config', path: '/model-config',
name: 'ModelConfig', name: 'ModelConfig',
meta: {
menuCode: 'model',
},
component: function () { component: function () {
return import('../views/modelConfig.vue') return import('../views/ModelConfig.vue')
} }
}, },
+1 -6
View File
@@ -39,13 +39,10 @@ function formatDateTool(date, fmt) {
const o = { const o = {
'M+': date.getMonth() + 1, 'M+': date.getMonth() + 1,
'd+': date.getDate(), 'd+': date.getDate(),
'H+': date.getHours(), 'h+': date.getHours(),
'h+': date.getHours() > 12 ? date.getHours() - 12 : date.getHours(),
'm+': date.getMinutes(), 'm+': date.getMinutes(),
's+': date.getSeconds() 's+': date.getSeconds()
} }
// 12小时制
const is_12Hours = fmt.indexOf('hh') > -1;
for (const k in o) { for (const k in o) {
if (new RegExp(`(${k})`).test(fmt)) { if (new RegExp(`(${k})`).test(fmt)) {
const str = o[k] + '' const str = o[k] + ''
@@ -55,8 +52,6 @@ function formatDateTool(date, fmt) {
) )
} }
} }
// 12小时制
fmt = is_12Hours ? date.getHours() > 12 ? fmt + " PM" : fmt + " AM" : fmt
return fmt return fmt
} }
+2 -2
View File
@@ -9,8 +9,8 @@ export function checkUserLogin(fn) {
let token = localStorage.getItem(Constant.STORAGE_KEY.TOKEN) let token = localStorage.getItem(Constant.STORAGE_KEY.TOKEN)
let userType = localStorage.getItem(Constant.STORAGE_KEY.USER_TYPE) let userType = localStorage.getItem(Constant.STORAGE_KEY.USER_TYPE)
if (isNull(token) || isNull(userType)) { if (isNull(token) || isNull(userType)) {
goToPage('/login', true) goToPage('console', true)
return false return
} }
if (fn) { if (fn) {
fn() fn()
@@ -0,0 +1,215 @@
<template>
<div class="welcome">
<HeaderBar />
<el-main style="padding: 20px; display: flex; flex-direction: column;">
<div class="table-container">
<h3 class="device-list-title">设备列表</h3>
<el-button type="primary" class="add-device-btn" @click="handleAddDevice">
+ 添加设备
</el-button>
<el-table :data="paginatedDeviceList" style="width: 100%; margin-top: 20px" border stripe>
<el-table-column label="设备型号" prop="model" flex></el-table-column>
<el-table-column label="固件版本" prop="firmwareVersion" width="120"></el-table-column>
<el-table-column label="Mac地址" prop="macAddress"></el-table-column>
<el-table-column label="绑定时间" prop="bindTime" width="200"></el-table-column>
<el-table-column label="最近对话" prop="lastConversation" width="140"></el-table-column>
<el-table-column label="备注" width="180">
<template slot-scope="scope">
<el-input v-if="scope.row.isEdit" v-model="scope.row.remark" size="mini" @blur="stopEditRemark(scope.$index)"></el-input>
<span v-else>
<i v-if="!scope.row.remark" class="el-icon-edit" @click="startEditRemark(scope.$index, scope.row)"></i>
<span v-else @click="startEditRemark(scope.$index, scope.row)">
{{ scope.row.remark }}
</span>
</span>
</template>
</el-table-column>
<el-table-column label="OTA升级" width="120">
<template slot-scope="scope">
<el-switch v-model="scope.row.otaSwitch" size="mini" active-color="#13ce66" inactive-color="#ff4949"></el-switch>
</template>
</el-table-column>
<el-table-column label="操作" width="80">
<template slot-scope="scope">
<el-button size="mini" type="text" @click="handleUnbind(scope.row.device_id)" style="color: #ff4949">
解绑
</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
class="pagination"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="currentPage"
:page-sizes="[5, 10, 20, 50]"
:page-size="pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="deviceList.length"
></el-pagination>
</div>
<div style="font-size: 12px; font-weight: 400; margin-top: auto; padding-top: 30px; color: #979db1;">
©2025 xiaozhi-esp32-server
</div>
<AddDeviceDialog :visible.sync="addDeviceDialogVisible" :agent-id="currentAgentId" @refresh="fetchBindDevices(currentAgentId)" />
</el-main>
</div>
</template>
<script>
import HeaderBar from "@/components/HeaderBar.vue";
import AddDeviceDialog from "@/components/AddDeviceDialog.vue";
export default {
components: {HeaderBar, AddDeviceDialog },
data() {
return {
addDeviceDialogVisible: false,
currentAgentId: this.$route.query.agentId || '',
currentPage: 1,
pageSize: 5,
deviceList: [],
loading: false,
userApi: null,
};
},
computed: {
paginatedDeviceList() {
const start = (this.currentPage - 1) * this.pageSize;
const end = start + this.pageSize;
return this.deviceList.slice(start, end);
}
},
mounted() {
const agentId = this.$route.query.agentId;
import('@/apis/module/user').then(({ default: userApi }) => {
this.userApi = userApi;
if (agentId) {
this.fetchBindDevices(agentId);
}
});
},
methods: {
handleAddDevice() {
this.addDeviceDialogVisible = true;
},
startEditRemark(index, row) {
this.deviceList[index].isEdit = true;
},
stopEditRemark(index) {
this.deviceList[index].isEdit = false;
},
handleUnbind(device_id) {
if (!this.userApi) {
this.$message.error('功能模块加载失败');
return;
}
this.$confirm('确认要解绑该设备吗?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.userApi.unbindDevice(device_id, ({ data }) => {
if (data.code === 0) {
this.$message.success('设备解绑成功');
this.fetchBindDevices(this.$route.query.agentId);
} else {
this.$message.error(data.msg || '设备解绑失败');
}
});
});
},
handleSizeChange(val) {
this.pageSize = val;
},
handleCurrentChange(val) {
this.currentPage = val;
},
fetchBindDevices(agentId) {
this.loading = true;
import('@/apis/module/user').then(({ default: userApi }) => {
userApi.getAgentBindDevices(agentId, ({ data }) => {
this.loading = false;
if (data.code === 0) {
this.deviceList = data.data[0].list.map(device => ({
device_id: device.id,
model: device.device_type,
firmwareVersion: device.app_version,
macAddress: device.mac_address,
lastConversation: device.recent_chat_time,
remark: '',
isEdit: false,
otaSwitch: device.ota_upgrade === 1
}));
} else {
this.$message.error(data.msg || '获取设备列表失败');
}
});
}).catch(error => {
console.error('模块加载失败:', error);
this.$message.error('功能模块加载失败');
});
},
}
};
</script>
<style scoped>
.welcome {
min-width: 900px;
min-height: 506px;
height: 100vh;
display: flex;
flex-direction: column;
background-image: url("@/assets/home/background.png");
background-size: cover;
background-position: center;
-webkit-background-size: cover;
-o-background-size: cover;
}
.table-container {
background: #f9fafc;
padding: 20px;
border-radius: 20px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
margin-top: 15px;
}
.add-device-btn {
float: right;
background: #409eff;
border: none;
border-radius: 10px;
width: 105px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
gap: 8px;
margin-bottom: 15px;
&:hover {
background: #3a8ee6;
}
}
.device-list-title {
float: left;
font-size: 18px;
font-weight: 700;
margin: 5px;
color: #2c3e50;
}
.el-icon-edit {
color: #409eff;
cursor: pointer;
font-size: 14px;
vertical-align: middle;
}
.pagination {
margin-top: 20px;
text-align: right;
}
</style>
+98 -104
View File
@@ -1,16 +1,9 @@
<template> <template>
<div class="welcome"> <div class="welcome">
<HeaderBar /> <HeaderBar />
<!-- 首页内容 -->
<el-main style="padding: 20px; display: flex; flex-direction: column;">
<div class="operation-bar"> <div class="operation-bar">
<!-- 面包屑-->
<div class="breadcrumbs">
<el-breadcrumb separator="/">
<el-breadcrumb-item>控制台</el-breadcrumb-item>
<el-breadcrumb-item>模型配置</el-breadcrumb-item>
</el-breadcrumb>
</div>
<div class="right-operations"> <div class="right-operations">
<el-button v-if="activeTab === 'tts'" type="primary" plain size="small" @click="ttsDialogVisible = true"> <el-button v-if="activeTab === 'tts'" type="primary" plain size="small" @click="ttsDialogVisible = true">
语音设置 语音设置
@@ -23,95 +16,96 @@
</el-button> </el-button>
</div> </div>
</div> </div>
<!-- 主体内容 --> <!-- 主体内容 -->
<div class="main-wrapper"> <div class="main-wrapper">
<div class="content-panel"> <div class="content-panel">
<!-- 左侧导航 --> <!-- 左侧导航 -->
<el-menu :default-active="activeTab" class="nav-panel" @select="handleMenuSelect" <el-menu :default-active="activeTab" class="nav-panel" @select="handleMenuSelect"
style="background-size: cover; background-position: center;"> style="background-size: cover; background-position: center;">
<el-menu-item index="vad"> <el-menu-item index="vad">
语言活动检测 <span class="menu-text">语言活动检测</span>
</el-menu-item> </el-menu-item>
<el-menu-item index="asr"> <el-menu-item index="asr">
语音识别 <span class="menu-text">语音识别</span>
</el-menu-item> </el-menu-item>
<el-menu-item index="llm"> <el-menu-item index="llm">
大语言模型 <span class="menu-text">大语言模型</span>
</el-menu-item> </el-menu-item>
<el-menu-item index="intent"> <el-menu-item index="intent">
意图识别 <span class="menu-text">意图识别</span>
</el-menu-item> </el-menu-item>
<el-menu-item index="tts"> <el-menu-item index="tts">
语音合成 <span class="menu-text">语音合成</span>
</el-menu-item> </el-menu-item>
<el-menu-item index="memory"> <el-menu-item index="memory">
记忆 <span class="menu-text">记忆</span>
</el-menu-item> </el-menu-item>
</el-menu> </el-menu>
<!-- 右侧内容 --> <!-- 右侧内容 -->
<div class="content-area"> <div class="content-area">
<div class="title-bar"> <div class="title-bar">
<div class="title-wrapper"> <div class="title-wrapper">
<h2 class="model-title">大语言模型LLM</h2> <h2 class="model-title">大语言模型LLM</h2>
<el-button type="primary" size="small" @click="addModel" class="add-btn"> <el-button type="primary" size="small" @click="addModel" class="add-btn">
添加 添加
</el-button> </el-button>
</div>
<div class="action-group">
<div class="search-group">
<el-input placeholder="请输入模型名称查询" v-model="search" size="small" class="search-input" clearable/>
<el-button type="primary" size="small" class="search-btn" @click="handleSearch">
查询
</el-button>
</div>
</div>
</div> </div>
<div class="action-group">
<el-table :header-cell-style="{background: 'transparent'}" :data="modelList" border class="data-table" header-row-class-name="table-header" > <div class="search-group">
<el-table-column type="selection" width="55" align="center"></el-table-column> <el-input placeholder="请输入模型名称查询" v-model="search" size="small" class="search-input" clearable/>
<el-table-column label="模型名称" prop="candidateName" align="center"></el-table-column> <el-button type="primary" size="small" class="search-btn" @click="handleSearch">
<el-table-column label="模型编码" prop="code" align="center"></el-table-column> 查询
<el-table-column label="提供商" prop="supplier" align="center"></el-table-column>
<el-table-column label="是否启用" align="center" width="120">
<template slot-scope="scope">
<el-switch v-model="scope.row.isApplied" class="custom-switch" :active-color="null" :inactive-color="null"/>
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="180">
<template slot-scope="scope">
<el-button type="text" size="mini" @click="editModel(scope.row)" class="edit-btn">
修改
</el-button>
<el-button type="text" size="mini" @click="deleteModel(scope.row)" class="delete-btn">
删除
</el-button>
</template>
</el-table-column>
</el-table>
<div class="table-footer">
<div class="batch-actions">
<el-button size="mini" @click="selectAll">全选</el-button>
<el-button size="mini" type="danger" icon="el-icon-delete" @click="batchDelete">
删除
</el-button> </el-button>
</div> </div>
<div class="pagination-container">
<el-pagination @current-change="handleCurrentChange" background :current-page="currentPage" :page-size="pageSize" layout="prev, pager, next" :total="total"/>
</div>
</div> </div>
</div> </div>
</div>
<ModelEditDialog :visible.sync="editDialogVisible" :modelData="editModelData" @save="handleModelSave"/> <el-table :header-cell-style="{background: 'transparent'}" :data="modelList" border class="data-table" header-row-class-name="table-header" >
<TtsModel :visible.sync="ttsDialogVisible" /> <el-table-column type="selection" width="55" align="center"></el-table-column>
<AddModelDialog :visible.sync="addDialogVisible" :modelType="activeTab" @confirm="handleAddConfirm"/> <el-table-column label="模型名称" prop="candidateName" align="center"></el-table-column>
<el-table-column label="模型编码" prop="code" align="center"></el-table-column>
<el-table-column label="提供商" prop="supplier" align="center"></el-table-column>
<el-table-column label="是否启用" align="center" width="120">
<template slot-scope="scope">
<el-switch v-model="scope.row.isApplied" class="custom-switch" :active-color="null" :inactive-color="null"/>
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="180">
<template slot-scope="scope">
<el-button type="text" size="mini" @click="editModel(scope.row)" class="edit-btn">
修改
</el-button>
<el-button type="text" size="mini" @click="deleteModel(scope.row)" class="delete-btn">
删除
</el-button>
</template>
</el-table-column>
</el-table>
<div class="table-footer">
<div class="batch-actions">
<el-button size="mini" @click="selectAll">全选</el-button>
<el-button size="mini" type="danger" icon="el-icon-delete" @click="batchDelete">
删除
</el-button>
</div>
<div class="pagination-container">
<el-pagination @current-change="handleCurrentChange" background :current-page="currentPage" :page-size="pageSize" layout="prev, pager, next" :total="total"/>
</div>
</div>
</div>
</div> </div>
</el-main>
<!-- 底部 --> <ModelEditDialog :visible.sync="editDialogVisible" :modelData="editModelData" @save="handleModelSave"/>
<Footer :visible="true" /> <TtsModel :visible.sync="ttsDialogVisible" />
</div> <AddModelDialog :visible.sync="addDialogVisible" @confirm="handleAddConfirm"/>
</div>
<div class="copyright">
©2025 xiaozhi-esp32-server
</div>
</div>
</template> </template>
<script> <script>
@@ -119,10 +113,9 @@ import HeaderBar from "@/components/HeaderBar.vue";
import ModelEditDialog from "@/components/ModelEditDialog.vue"; import ModelEditDialog from "@/components/ModelEditDialog.vue";
import TtsModel from "@/components/TtsModel.vue"; import TtsModel from "@/components/TtsModel.vue";
import AddModelDialog from "@/components/AddModelDialog.vue"; import AddModelDialog from "@/components/AddModelDialog.vue";
import Footer from '@/components/Footer.vue'
export default { export default {
components: { HeaderBar, ModelEditDialog, TtsModel, AddModelDialog,Footer }, components: { HeaderBar, ModelEditDialog, TtsModel, AddModelDialog },
data() { data() {
return { return {
addDialogVisible: false, addDialogVisible: false,
@@ -189,10 +182,6 @@ export default {
</script> </script>
<style scoped> <style scoped>
.breadcrumbs{
/* padding: 10px 0 0 5px; */
}
::v-deep .el-table tr{ ::v-deep .el-table tr{
background: transparent; background: transparent;
} }
@@ -210,7 +199,7 @@ export default {
} }
.main-wrapper { .main-wrapper {
/* margin: 5px 24px; */ margin: 5px 60px;
background-image: url("@/assets/home/background.png"); background-image: url("@/assets/home/background.png");
border-radius: 15px; border-radius: 15px;
min-height: 600px; min-height: 600px;
@@ -220,9 +209,10 @@ export default {
.operation-bar { .operation-bar {
display: flex; display: flex;
justify-content: space-between; justify-content: flex-end;
align-items: center; align-items: center;
/* padding: 16px 24px; */ padding: 16px 24px;
border-bottom: 1px solid #ebeef5;
} }
.content-panel { .content-panel {
@@ -249,7 +239,7 @@ export default {
.nav-panel .el-menu-item { .nav-panel .el-menu-item {
height: 48px; height: 48px;
line-height: 48px; line-height: 40px;
border-radius: 4px; border-radius: 4px;
transition: all 0.3s; transition: all 0.3s;
display: flex !important; display: flex !important;
@@ -261,16 +251,20 @@ export default {
} }
.nav-panel .el-menu-item.is-active { .nav-panel .el-menu-item.is-active {
background-image:linear-gradient(-45deg,#ecf5ff,#cfe4f9); background: #ecf5ff;
/* background: #ecf5ff; */
color: #409EFF; color: #409EFF;
border-right: 3px solid #409EFF; border-right: 3px solid #409EFF;
} }
.nav-panel .el-menu-item:hover { .menu-text {
background-image:linear-gradient(-45deg,#ecf5ff,#cfe4f9); font-size: 14px;
color: #606266;
text-align: right;
width: 100%;
padding-right: 8px;
} }
.content-area { .content-area {
flex: 1; flex: 1;
padding: 24px; padding: 24px;
+85 -28
View File
@@ -5,13 +5,13 @@
<div class="top-area"> <div class="top-area">
<div class="page-title">用户管理</div> <div class="page-title">用户管理</div>
<div class="page-search"> <div class="page-search">
<el-input placeholder="请输入手机号码查询" v-model="searchPhone" style="width: 300px; margin-right: 10px" /> <el-input placeholder="请输入手机号码查询" v-model="searchPhone" class="search-input" />
<el-button class="btn-search" @click="handleSearch">搜索</el-button> <el-button class="btn-search" @click="handleSearch">搜索</el-button>
<!-- <el-button type="danger" @click="batchDelete">批量删除</el-button> <!-- <el-button type="danger" @click="batchDelete">批量删除</el-button>
<el-button type="danger" @click="batchDisable">批量禁用</el-button> --> <el-button type="danger" @click="batchDisable">批量禁用</el-button> -->
</div> </div>
</div> </div>
<el-card class="user-card" shadow="never"> <el-card class="user-card" shadow="never">
<!-- <div class="user-search-operate" style="display: flex; align-items: center; margin-bottom: 20px;"> <!-- <div class="user-search-operate" style="display: flex; align-items: center; margin-bottom: 20px;">
<el-input placeholder="请输入手机号码查询" v-model="searchPhone" style="width: 300px; margin-right: 10px" /> <el-input placeholder="请输入手机号码查询" v-model="searchPhone" style="width: 300px; margin-right: 10px" />
@@ -19,45 +19,42 @@
<el-button type="danger" @click="batchDelete">批量删除</el-button> <el-button type="danger" @click="batchDelete">批量删除</el-button>
<el-button type="danger" @click="batchDisable">批量禁用</el-button> <el-button type="danger" @click="batchDisable">批量禁用</el-button>
</div> --> </div> -->
<el-table :data="userList" style="width: 100%;"> <el-table :data="userList" class="transparent-table" :header-cell-class-name="headerCellClassName">
<el-table-column label="选择" <el-table-column label="选择" type="selection" width="55"></el-table-column>
type="selection" <el-table-column label="用户Id" prop="user_id"></el-table-column>
width="55">
</el-table-column>
<el-table-column label="用户Id" prop="userid"></el-table-column>
<el-table-column label="手机号码" prop="mobile"></el-table-column> <el-table-column label="手机号码" prop="mobile"></el-table-column>
<el-table-column label="设备数量" prop="deviceCount"></el-table-column> <el-table-column label="设备数量" prop="device_count"></el-table-column>
<el-table-column label="状态" prop="status"></el-table-column> <el-table-column label="状态" prop="status"></el-table-column>
<el-table-column label="操作"> <el-table-column label="操作">
<template slot-scope="scope"> <template slot-scope="scope">
<el-button size="mini" type="text" @click="resetPassword(scope.row)">重置密码</el-button> <el-button size="mini" type="text" @click="resetPassword(scope.row)" style="color: #989fdd">重置密码</el-button>
<el-button size="mini" type="text" <el-button size="mini" type="text"
v-if="scope.row.status === '正常'" v-if="scope.row.status === '正常'"
@click="disableUser(scope.row)">禁用</el-button> @click="disableUser(scope.row)">禁用</el-button>
<el-button size="mini" type="text" <el-button size="mini" type="text"
v-if="scope.row.status === '禁用'" v-if="scope.row.status === '禁用'"
@click="restoreUser(scope.row)">恢复</el-button> @click="restoreUser(scope.row)">恢复</el-button>
<el-button size="mini" type="text" @click="deleteUser(scope.row)" style="color: #ff4949">删除用户</el-button> <el-button size="mini" type="text" @click="deleteUser(scope.row)" style="color: #989fdd">删除用户</el-button>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
<div class="table_bottom"> <div class="table_bottom">
<div class="ctrl_btn"> <div class="ctrl_btn">
<el-button size="mini" type="primary">全选</el-button> <el-button size="mini" type="primary" style="width: 72px; background: #5f70f3">全选</el-button>
<el-button size="mini" type="success" icon="el-icon-circle-check">启用</el-button> <el-button size="mini" type="success" icon="el-icon-circle-check" style="background: #5bc98c">启用</el-button>
<el-button size="mini" type="warning" icon="el-icon-circle-close">禁用</el-button> <el-button size="mini" type="warning" style="color: black; background: #f6d075"><i class="el-icon-remove-outline rotated-icon"></i>禁用</el-button>
<el-button size="mini" type="danger" icon="el-icon-delete">删除</el-button> <el-button size="mini" type="danger" icon="el-icon-delete" style="background: #fd5b63">删除</el-button>
</div> </div>
<div class="pagination-container"> <div class="pagination-container">
<el-pagination <el-pagination
background background
@current-change="handleCurrentChange" @current-change="handleCurrentChange"
:current-page="currentPage" :current-page="currentPage"
:page-sizes="[5, 10, 15]" :page-sizes="[5, 10, 15]"
:page-size="pageSize" :page-size="pageSize"
layout="prev, pager, next" layout="prev, pager, next"
:total="total" :total="total"
/> />
</div> </div>
</div> </div>
@@ -77,7 +74,6 @@ import adminApi from '@/apis/module/admin';
export default { export default {
name: 'UserManagement',
components: { HeaderBar }, components: { HeaderBar },
data() { data() {
return { return {
@@ -96,7 +92,7 @@ export default {
created() { created() {
adminApi.getUserList(({data}) => { adminApi.getUserList(({data}) => {
//mock偶尔会返回-1导致出错,又会返回两个list,所以这里只取第一个 //mock偶尔会返回-1导致出错,又会返回两个list,所以这里只取第一个
this.userList = data.data.list; this.userList = data.data[0].list;
console.log('用户列表:', this.userList); console.log('用户列表:', this.userList);
}) })
}, },
@@ -129,11 +125,20 @@ export default {
this.currentPage = page; this.currentPage = page;
console.log('当前页码:', page); console.log('当前页码:', page);
}, },
headerCellClassName({ column, columnIndex }) {
if (columnIndex === 0) {
return 'custom-selection-header'
}
return ''
},
} }
}; };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
$table-bg-color: #ecf1fd;
.main { .main {
padding: 20px; display: flex; flex-direction: column; padding: 20px; display: flex; flex-direction: column;
background: linear-gradient(to bottom right, #dce8ff, #e4eeff, #e6cbfd); background: linear-gradient(to bottom right, #dce8ff, #e4eeff, #e6cbfd);
@@ -142,7 +147,7 @@ export default {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: 20px; margin-bottom: 10px;
.page-title { .page-title {
font-size: 20px; font-size: 20px;
font-weight: 600; font-weight: 600;
@@ -188,11 +193,10 @@ export default {
} }
.user-card { .user-card {
background: #fff; background: $table-bg-color;
border-radius: 12px; border-radius: 12px;
padding: 20px; padding: 20px;
opacity: 0.9; //box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
// box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
} }
.table_bottom { .table_bottom {
@@ -211,4 +215,57 @@ export default {
justify-content: flex-end; justify-content: flex-end;
} }
.rotated-icon {
display: inline-block;
transform: rotate(45deg);
margin-right: 4px;
color: black;
}
:deep(.el-table) {
background: $table-bg-color;
&.transparent-table {
.el-table__header th {
background: $table-bg-color !important;
color: black;
}
.el-table__body tr {
background-color: $table-bg-color;
}
}
}
.search-input {
width: 300px;
margin-right: 10px;
:deep(.el-input__inner) {
background-color: transparent;
&:focus {
border-color: #409eff; // 保持聚焦状态下的边框颜色
}
//文字颜色
&::placeholder {
color: #606266;
opacity: 0.7;
}
}
}
:deep(.custom-selection-header) {
.el-checkbox {
display: none !important;
}
&::after {
content: '选择';
display: inline-block;
color: black;
font-weight: bold;
padding-bottom: 18px;
}
}
</style> </style>
-231
View File
@@ -1,231 +0,0 @@
<template>
<div class="welcome">
<!-- 公共头部 -->
<HeaderBar />
<!-- 首页内容 -->
<el-main style="padding: 20px; display: flex; flex-direction: column;">
<!-- 面包屑-->
<div class="breadcrumbs">
<el-breadcrumb separator="/">
<el-breadcrumb-item>控制台</el-breadcrumb-item>
<el-breadcrumb-item><router-link to="/home">智能体</router-link></el-breadcrumb-item>
<el-breadcrumb-item>设备管理</el-breadcrumb-item>
</el-breadcrumb>
</div>
<div class="table-container">
<h3 class="device-list-title">设备列表</h3>
<el-button type="primary" class="add-device-btn" @click="handleAddDevice">
+ 添加设备
</el-button>
<el-table :data="devices" style="width: 100%; margin-top: 20px" border>
<el-table-column label="设备型号" prop="board" flex></el-table-column>
<el-table-column label="固件版本" prop="appVersion" width="140"></el-table-column>
<el-table-column label="MAC地址" prop="macAddress" width="220"></el-table-column>
<el-table-column label="绑定时间" width="260" prop="createDate" :formatter="formatter">
</el-table-column>
<el-table-column label="最近对话" prop="recent_chat_time" width="100"></el-table-column>
<el-table-column label="备注" width="220">
<template slot-scope="scope">
<el-input :ref="'alias_input_'+scope.$index" v-if="scope.row.isEdit" v-model="scope.row.alias" size="small" class="input-pd0" @blur="stopEditRemark(scope.$index, scope.row)"></el-input>
<span v-else>
{{ scope.row.alias }}
</span>
<i v-if="!scope.row.isEdit" class="el-icon-edit" @click="startEditRemark(scope.$index, scope.row)"></i>
</template>
</el-table-column>
<el-table-column label="OTA升级" width="100" align="center">
<template slot-scope="scope">
<el-switch v-model="scope.row.autoUpdate" size="mini" :active-value="1" :inactive-value="0" active-color="#13ce66" inactive-color="#ff4949" @change="handleToggleEnabled(scope.row.id, $event)"></el-switch>
</template>
</el-table-column>
<el-table-column label="操作" width="80" align="center">
<template slot-scope="scope">
<el-button size="mini" type="text" @click="handleUnbind(scope.row.id)" style="color: #ff4949">
解绑
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination background :current-page="page.pageNo" :page-size="page.pageSize" :pager-count="5" layout="prev, pager, next" :total="page.total" style="margin-top: 4px;text-align: right;"></el-pagination>
</div>
<!-- 底部 -->
<Footer :visible="true" />
<!-- 添加设备对话框 -->
<AddDeviceDialog :visible.sync="addDeviceDialogVisible" @confirm="handleDeviceBind" />
</el-main>
</div>
</template>
<script>
import {getUUID, goToPage, showDanger, showSuccess} from '@/utils'
import { formatDate } from '@/utils/date'
import Api from '@/apis/api';
import AddDeviceDialog from '@/components/AddDeviceDialog.vue'
import HeaderBar from '@/components/HeaderBar.vue'
import Footer from '@/components/Footer.vue'
export default {
name: 'DevicePage',
components: { AddDeviceDialog, HeaderBar, Footer },
data() {
return {
addDeviceDialogVisible: false,
devices: [],
agentId: this.$route.query.agentId,
oldVal: '',
page: {
pageNo: 1,
pageSize: 10,
total: 0
}
}
},
mounted() {
this.fetchDeviceList();
},
methods: {
// 获取设备列表
fetchDeviceList() {
Api.device.getDeviceList(this.agentId, this.page, ({data}) => {
if (data.code === 0) {
this.page = {...this.page,total:parseInt(data.data.total)}
this.devices = data.data.records.map((item)=>{item.isEdit=false;return item;})
} else {
showDanger(data.msg)
}
})
},
handleAddDevice() {
// 添加设备逻辑
this.addDeviceDialogVisible = true;
},
startEditRemark(index, row) {
this.oldVal = row.alias;
this.devices[index].isEdit = true;
this.$nextTick(()=>{
this.$refs['alias_input_'+index].focus();
})
},
stopEditRemark(index, row) {
this.devices[index].isEdit = false;
if(this.oldVal === row.alias) return;
this.handleSetAlias(row.id, row.alias);
},
handleUnbind(deviceId) {
// 解绑逻辑
console.log('解绑设备', deviceId);
Api.device.unbindDevice(deviceId, ({data}) => {
if (data.code === 0) {
showSuccess('解绑成功')
this.fetchDeviceList()
} else {
showDanger(data.msg)
}
})
},
handleDeviceBind(deviceCode) {
// 绑定逻辑
console.log('设备验证码:', deviceCode)
Api.device.bindDevice(this.agentId, deviceCode, ({data}) => {
if (data.code === 0) {
this.fetchDeviceList();
this.showBindDialog = false;
} else {
showDanger(data.msg);
}
})
},
handleSetAlias(id, alias) {
// 设置备注逻辑
Api.device.setAlias(id, alias, ({data}) => {
if (data.code === 0) {
showSuccess('更新成功')
this.fetchDeviceList()
} else {
showDanger(data.msg)
}
})
},
handleToggleEnabled(id, autoUpdate) {
// 更新OTA升级状态
Api.device.toggleAutoUpdate(id, autoUpdate, ({data}) => {
if (data.code === 0) {
showSuccess('更新成功')
this.fetchDeviceList()
} else {
showDanger(data.msg)
}
})
},
formatter(row, column) {
return formatDate(row.createDate)
}
}
};
</script>
<style scoped>
.breadcrumbs{
padding: 10px 0 0 5px;
}
.welcome {
min-width: 900px;
min-height: 506px;
height: 100vh;
display: flex;
flex-direction: column;
background-image: url("@/assets/home/background.png");
background-size: cover;
background-position: center;
-webkit-background-size: cover;
-o-background-size: cover;
}
.table-container {
background: #f9fafc;
padding: 20px;
border-radius: 20px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
margin-top: 15px;
}
.add-device-btn {
float: right;
background: #409eff;
border: none;
border-radius: 10px;
width: 105px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
gap: 8px;
margin-bottom: 15px;
&:hover {
background: #3a8ee6;
}
}
.device-list-title {
float: left;
font-size: 18px;
font-weight: 700;
margin: 5px;
color: #2c3e50;
}
.el-icon-edit {
color: #409eff;
cursor: pointer;
font-size: 14px;
vertical-align: middle;
}
.input-pd0 /deep/ .el-input__inner {
padding: 0 2px;
}
</style>
+64 -73
View File
@@ -1,12 +1,12 @@
<template> <template>
<div class="welcome"> <div class="welcome">
<!-- 公共头部 --> <!-- 公共头部 -->
<HeaderBar :devices="agents" @search-result="handleSearchResult" /> <HeaderBar :devices="devices" @search-result="handleSearchResult" />
<el-main style="padding: 20px;display: flex;flex-direction: column;"> <el-main style="padding: 20px;display: flex;flex-direction: column;">
<div> <div>
<!-- 首页内容 --> <!-- 首页内容 -->
<div class="add-agent"> <div class="add-device">
<div class="add-agent-bg"> <div class="add-device-bg">
<div class="hellow-text" style="margin-top: 30px;"> <div class="hellow-text" style="margin-top: 30px;">
您好小智 您好小智
</div> </div>
@@ -19,7 +19,7 @@
<div class="hi-hint"> <div class="hi-hint">
Hello, Let's have a wonderful day! Hello, Let's have a wonderful day!
</div> </div>
<div class="add-agent-btn" @click="showAddDialog"> <div class="add-device-btn" @click="showAddDialog">
<div class="left-add"> <div class="left-add">
添加智能体 添加智能体
</div> </div>
@@ -30,107 +30,99 @@
</div> </div>
</div> </div>
</div> </div>
<!-- 面包屑-->
<div class="breadcrumbs">
<el-breadcrumb separator="/">
<el-breadcrumb-item>控制台</el-breadcrumb-item>
<el-breadcrumb-item>智能体</el-breadcrumb-item>
</el-breadcrumb>
</div>
<!-- 智能体列表-->
<div style="display: flex;flex-wrap: wrap;margin-top: 20px;gap: 20px;justify-content: flex-start;box-sizing: border-box;"> <div style="display: flex;flex-wrap: wrap;margin-top: 20px;gap: 20px;justify-content: flex-start;box-sizing: border-box;">
<AgentItem v-for="(item,index) in agents" :key="index" :agent="item" @configure="goToRoleConfig" @device="goToDevice" @del="handleAgentDel" /> <DeviceItem v-for="(item,index) in devices" :key="index" :device="item"
@configure="goToRoleConfig"
@deviceManage="handleDeviceManage"
@delete="handleDeleteAgent"
/>
</div> </div>
</div> </div>
<!-- 底部 --> <div style="font-size: 12px;font-weight: 400;margin-top: auto;padding-top: 30px;color: #979db1;">
<Footer :visible="true" /> ©2025 xiaozhi-esp32-server
<!-- 添加设备对话框 --> </div>
<AddAgentDialog :visible.sync="addAgentDialogVisible" @confirm="handleAgentAdded" /> <AddWisdomBodyDialog :visible.sync="addDeviceDialogVisible" @confirm="handleWisdomBodyAdded" />
</el-main> </el-main>
</div> </div>
</template> </template>
<script> <script>
import {getUUID, goToPage, showDanger, showSuccess} from '@/utils' import DeviceItem from '@/components/DeviceItem.vue'
import Api from '@/apis/api'; import AddWisdomBodyDialog from '@/components/AddWisdomBodyDialog.vue'
import AgentItem from '@/components/AgentItem.vue'
import AddAgentDialog from '@/components/AddAgentDialog.vue'
import HeaderBar from '@/components/HeaderBar.vue' import HeaderBar from '@/components/HeaderBar.vue'
import Footer from '@/components/Footer.vue'
export default { export default {
name: 'HomePage', name: 'HomePage',
components: { AgentItem, AddAgentDialog, HeaderBar, Footer }, components: { DeviceItem, AddWisdomBodyDialog, HeaderBar },
data() { data() {
return { return {
addAgentDialogVisible: false, addDeviceDialogVisible: false,
agents: [], devices: [],
originalDevices: [],
} }
}, },
mounted() { mounted() {
// 获取智能体列表
this.fetchAgentList(); this.fetchAgentList();
}, },
methods: { methods: {
fetchAgentList() {
// 获取智能体列表
Api.agent.getAgentList(({data}) => {
if (data.code === 0) {
this.agents = data.data
} else {
showDanger(data.msg)
}
})
},
showAddDialog() { showAddDialog() {
this.addAgentDialogVisible = true this.addDeviceDialogVisible = true
}, },
goToRoleConfig(agentId) { goToRoleConfig() {
// 点击配置角色后跳转到角色配置页 // 点击配置角色后跳转到角色配置页
this.$router.push({path:'/role-config', query: {agentId: agentId}}) this.$router.push('/role-config')
}, },
handleWisdomBodyAdded(res) {
goToDevice(agentId) { console.log('新增智能体响应', res);
// 点击设备后跳转到设备页 this.fetchAgentList();
this.$router.push({path:'/device', query: {agentId: agentId}}) this.addDeviceDialogVisible = false;
}, },
handleAgentAdded(agentName) { handleDeviceManage() {
// 添加智能体 this.$router.push('/device-management');
Api.agent.addAgent(agentName, ({data}) => {
if (data.code === 0) {
showSuccess('添加成功')
this.fetchAgentList();
} else {
showDanger(data.msg)
}
})
}, },
handleAgentDel(agetnId){ // 获取智能体列表
// 删除智能体 fetchAgentList() {
Api.agent.delAgent(agetnId, (data) => { import('@/apis/module/user').then(({ default: userApi }) => {
if (data.status === 200) { userApi.getAgentList(({data}) => {
showSuccess('删除成功') this.originalDevices = data.data.map(item => ({
this.fetchAgentList(); ...item,
} else { agentId: item.id // 字段映射
showDanger(data.msg) }));
} this.devices = this.originalDevices;
}) });
});
}, },
// 搜索更新智能体列表 // 搜索更新智能体列表
handleSearchResult(filteredList) { handleSearchResult(filteredList) {
this.agents = filteredList; // 更新设备列表 this.devices = filteredList; // 更新设备列表
},
// 删除智能体
handleDeleteAgent(agentId) {
this.$confirm('确定要删除该智能体吗', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
import('@/apis/module/user').then(({ default: userApi }) => {
userApi.deleteAgent(agentId, (res) => {
if (res.data.code === 0) {
this.$message.success('删除成功');
this.fetchAgentList(); // 刷新列表
} else {
this.$message.error(res.data.msg || '删除失败');
}
});
});
}).catch(() => {});
} }
} }
} }
</script> </script>
<style scoped> <style scoped>
.breadcrumbs{
padding: 10px 0 0 5px;
}
.welcome { .welcome {
min-width: 900px; min-width: 900px;
min-height: 506px; min-height: 506px;
@@ -147,7 +139,7 @@ export default {
-o-background-size: cover; -o-background-size: cover;
/* 兼容老版本Opera浏览器 */ /* 兼容老版本Opera浏览器 */
} }
.add-agent { .add-device {
height: 195px; height: 195px;
border-radius: 15px; border-radius: 15px;
position: relative; position: relative;
@@ -159,7 +151,7 @@ export default {
#d3d3fe 100% #d3d3fe 100%
); );
} }
.add-agent-bg { .add-device-bg {
width: 100%; width: 100%;
height: 100%; height: 100%;
text-align: left; text-align: left;
@@ -192,13 +184,12 @@ export default {
} }
} }
.add-agent-btn { .add-device-btn {
display: flex; display: flex;
align-items: center; align-items: center;
margin-left: 75px; margin-left: 75px;
margin-top: 15px; margin-top: 15px;
cursor: pointer; cursor: pointer;
width: fit-content;
.left-add { .left-add {
width: 105px; width: 105px;
@@ -206,7 +197,7 @@ export default {
border-radius: 17px; border-radius: 17px;
background: #5778ff; background: #5778ff;
color: #fff; color: #fff;
font-size: 12px; font-size: 10px;
font-weight: 500; font-weight: 500;
text-align: center; text-align: center;
line-height: 34px; line-height: 34px;
+5 -5
View File
@@ -44,7 +44,7 @@
<div style="cursor: pointer;" @click="goToRegister">新用户注册</div> <div style="cursor: pointer;" @click="goToRegister">新用户注册</div>
</div> </div>
</div> </div>
<div class="login-btn" @keyup.enter="login" @click="login">登陆</div> <div class="login-btn" @click="login">登陆</div>
<div style="font-size: 14px;color: #979db1;"> <div style="font-size: 14px;color: #979db1;">
登录即同意 登录即同意
<div style="display: inline-block;color: #5778FF;cursor: pointer;">用户协议</div> <div style="display: inline-block;color: #5778FF;cursor: pointer;">用户协议</div>
@@ -88,7 +88,7 @@ export default {
methods: { methods: {
fetchCaptcha() { fetchCaptcha() {
if (this.$store.getters.getToken) { if (this.$store.getters.getToken) {
this.$router.push('/home') goToPage('/home')
} else { } else {
this.captchaUuid = getUUID(); this.captchaUuid = getUUID();
@@ -135,17 +135,17 @@ export default {
// 将令牌存储到 Vuex 中 // 将令牌存储到 Vuex 中
this.$store.commit('setToken', JSON.stringify(data.data)) this.$store.commit('setToken', JSON.stringify(data.data))
// this.$router.push('/home') goToPage('/home')
}) })
// 重新获取验证码(token判断逻辑,存在即跳转home页,否则刷新验证码) // 重新获取验证码
setTimeout(() => { setTimeout(() => {
this.fetchCaptcha(); this.fetchCaptcha();
}, 1000); }, 1000);
}, },
goToRegister() { goToRegister() {
this.$router.push('/register') goToPage('/register')
} }
} }
} }
-190
View File
@@ -1,190 +0,0 @@
<template>
<div class="welcome">
<!-- 公共头部 -->
<HeaderBar />
<!-- 首页内容 -->
<el-main style="padding: 20px; display: flex; flex-direction: column;">
<!-- 面包屑-->
<div class="breadcrumbs">
<el-breadcrumb separator="/">
<el-breadcrumb-item>控制台</el-breadcrumb-item>
<el-breadcrumb-item>OTA管理</el-breadcrumb-item>
</el-breadcrumb>
</div>
<div class="table-container">
<h3 class="device-list-title">OTA设备列表</h3>
<el-button type="primary" class="add-device-btn" @click="handleAddDeviceOta()">
+ 添加设备
</el-button>
<el-table :data="otaList" style="width: 100%; margin-top: 20px" border>
<el-table-column label="设备型号" prop="board" flex></el-table-column>
<el-table-column label="固件版本" prop="appVersion" width="140"></el-table-column>
<el-table-column label="升级地址" prop="url"></el-table-column>
<el-table-column label="是否启用" width="100" align="center">
<template slot-scope="scope">
<el-switch v-model="scope.row.isEnabled" size="mini" :active-value="1" :inactive-value="0" active-color="#13ce66" inactive-color="#ff4949" @change="handleToggleEnabled(scope.row.id, $event)"></el-switch>
</template>
</el-table-column>
<el-table-column label="操作" width="80" align="center">
<template slot-scope="scope">
<el-button size="mini" type="text" @click="handleAddDeviceOta(scope.row)" style="color: #ff4949">
编辑
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination background :current-page="page.pageNo" :page-size="page.pageSize" :pager-count="5" layout="prev, pager, next" :total="page.total" style="margin-top: 4px;text-align: right;"></el-pagination>
</div>
</el-main>
<!-- 底部 -->
<Footer :visible="true" />
<!-- 添加设备OTA对话框 -->
<AddDeviceOtaDialog :visible.sync="addDeviceOtaDialogVisible" :data="otaData" @confirmSave="handleSaveDeviceOta" @confirmUpdate="handleUpdateDeviceOta" />
</div>
</template>
<script>
import {getUUID, goToPage, showDanger, showSuccess} from '@/utils'
import Api from '@/apis/api';
import AddDeviceOtaDialog from '@/components/AddDeviceOtaDialog.vue'
import HeaderBar from '@/components/HeaderBar.vue'
import Footer from '@/components/Footer.vue'
export default {
name: 'DeviceOtaPage',
components: { AddDeviceOtaDialog, HeaderBar, Footer },
data() {
return {
addDeviceOtaDialogVisible: false,
otaList: [],
otaData: {},
page: {
pageNo: 1,
pageSize: 10,
total: 0
}
}
},
mounted() {
this.fetchDeviceOtaList();
},
methods: {
// 获取设备列表
fetchDeviceOtaList() {
Api.ota.getOtaList(this.page, ({data}) => {
if (data.code === 0) {
this.page = {...this.page,total:parseInt(data.data.total)}
this.otaList = data.data.records
} else {
showDanger(data.msg)
}
})
},
handleAddDeviceOta(ota) {
if (ota) {
this.otaData = ota
}
// 添加设备逻辑
this.addDeviceOtaDialogVisible = true;
},
handleSaveDeviceOta(ota) {
console.log('添加设备', ota);
Api.ota.saveOta(ota, ({data}) => {
if (data.code === 0) {
showSuccess('添加成功')
this.fetchDeviceOtaList()
} else {
showDanger(data.msg)
}
this.otaData = {}
})
},
handleUpdateDeviceOta(ota) {
// 解绑逻辑
console.log('修改设备', ota);
Api.ota.updateOta(ota, ({data}) => {
if (data.code === 0) {
showSuccess('修改成功')
this.fetchDeviceOtaList()
} else {
showDanger(data.msg)
}
this.otaData = {}
})
},
handleToggleEnabled(id, isEnabled) {
// 启用禁用逻辑
Api.ota.toggleEnabled({id, isEnabled}, ({data}) => {
if (data.code === 0) {
showSuccess('更新成功')
this.fetchDeviceOtaList()
} else {
showDanger(data.msg)
}
})
}
}
};
</script>
<style scoped>
.breadcrumbs{
padding: 10px 0 0 5px;
}
.welcome {
min-width: 900px;
min-height: 506px;
height: 100vh;
display: flex;
flex-direction: column;
background-image: url("@/assets/home/background.png");
background-size: cover;
background-position: center;
-webkit-background-size: cover;
-o-background-size: cover;
}
.table-container {
background: #f9fafc;
padding: 20px;
border-radius: 20px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
margin-top: 15px;
}
.add-device-btn {
float: right;
background: #409eff;
border: none;
border-radius: 10px;
width: 105px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
gap: 8px;
margin-bottom: 15px;
&:hover {
background: #3a8ee6;
}
}
.device-list-title {
float: left;
font-size: 18px;
font-weight: 700;
margin: 5px;
color: #2c3e50;
}
.el-icon-edit {
color: #409eff;
cursor: pointer;
font-size: 14px;
vertical-align: middle;
}
</style>
+225 -160
View File
@@ -1,72 +1,71 @@
<template> <template>
<div class="welcome"> <div class="welcome">
<!-- 公共头部 -->
<HeaderBar/> <HeaderBar/>
<!-- 面包屑--> <el-main style="padding: 16px;display: flex;flex-direction: column;">
<div class="breadcrumbs" style="padding: 20px;"> <div style="border-radius: 16px;background: #fafcfe; border: 1px solid #e8f0ff;">
<el-breadcrumb separator="/">
<el-breadcrumb-item>控制台</el-breadcrumb-item>
<el-breadcrumb-item><router-link to="/home">智能体</router-link></el-breadcrumb-item>
<el-breadcrumb-item>配置智能体</el-breadcrumb-item>
</el-breadcrumb>
</div>
<el-main style="padding: 16px;display: flex;flex-direction: column;align-items: center;">
<div style="border-radius: 16px;background: #fafcfe; border: 1px solid #e8f0ff;max-width: 800px;">
<div <div
style="padding: 15px 24px;font-weight: 700;font-size: 19px;text-align: left;color: #3d4566;display: flex;gap: 13px;align-items: center;"> style="padding: 15px 24px;font-weight: 700;font-size: 19px;text-align: left;color: #3d4566;display: flex;gap: 13px;align-items: center;">
<div <div
style="width: 37px;height: 37px;background: #5778ff;border-radius: 50%;display: flex;align-items: center;justify-content: center;"> style="width: 37px;height: 37px;background: #5778ff;border-radius: 50%;display: flex;align-items: center;justify-content: center;">
<img src="@/assets/home/setting-user.png" alt="" style="width: 19px;height: 19px;"/> <img src="@/assets/home/setting-user.png" alt="" style="width: 19px;height: 19px;"/>
</div> </div>
{{ form.agentName }} ({{ agentId }}) {{ form.agentName }}
</div> </div>
<div style="height: 1px;background: #e8f0ff;"/> <div style="height: 1px;background: #e8f0ff;"/>
<div style="padding: 16px 24px;max-width: 792px;"> <el-form ref="form" :model="form" label-width="72px">
<el-form ref="form" :model="form" label-width="100px"> <div style="padding: 16px 24px;max-width: 792px;">
<el-form-item label="助手昵称:">
<div class="input-46" style="width: 100%; max-width: 412px;">
<el-input v-model="form.agentName"/>
</div>
</el-form-item>
<el-form-item label="角色模版:"> <el-form-item label="角色模版:">
<div style="display: flex;gap: 8px;flex-wrap: wrap;"> <div style="display: flex;gap: 8px;">
<div v-for="template in templates" :key="template.id" class="template-item" @click="selectTemplate(template)"> <div v-for="template in templates" :key="template" class="template-item" :class="{ 'template-loading': loadingTemplate }" @click="selectTemplate(template)">
{{ template.agentName }} {{ template }}
</div> </div>
</div> </div>
</el-form-item> </el-form-item>
<el-form-item label="助手昵称:">
<el-input v-model="form.agentCode"/>
</el-form-item>
<el-form-item label="角色音色:"> <el-form-item label="角色音色:">
<div style="display: flex;gap: 8px;align-items: center;"> <div style="display: flex;gap: 8px;align-items: center;">
<div style="flex:1;"> <div class="input-46" style="flex:1.4;">
<el-select v-model="form.ttsVoiceId" placeholder="请选择" @change="handleTtsVoiceChange" style="width: 100%;"> <el-select v-model="form.ttsVoiceId" placeholder="请选择" style="width: 100%;">
<el-option v-for="item in ttsVoices" :key="item.id" :label="item.name" <el-option v-for="item in options" :key="item.value" :label="item.label"
:value="item.id"> :value="item.value">
</el-option> </el-option>
</el-select> </el-select>
</div> </div>
<div class="audio-box"> <div class="audio-box">
<audio :src="voiceDemo" controls <audio src="http://music.163.com/song/media/outer/url?id=447925558.mp3" controls
style="height: 100%;width: 100%;"/> style="height: 100%;width: 100%;"/>
</div> </div>
</div> </div>
</el-form-item> </el-form-item>
<el-form-item label="角色介绍:"> <el-form-item label="角色介绍:">
<el-input type="textarea" rows="5" resize="none" placeholder="请输入内容" v-model="form.systemPrompt" maxlength="2000" show-word-limit/> <div class="textarea-box">
<el-input type="textarea" rows="5" resize="none" placeholder="请输入内容"
v-model="form.systemPrompt" maxlength="2000" show-word-limit/>
</div>
</el-form-item> </el-form-item>
<el-form-item label="记忆体:"> <el-form-item label="记忆体:">
<el-input type="textarea" rows="5" resize="none" placeholder="请输入内容" v-model="form.prompt" maxlength="1000" show-word-limit/> <div class="textarea-box">
<div style="display: flex;gap: 8px;align-items: center;"> <el-input type="textarea" rows="5" resize="none" placeholder="请输入内容"
<div style="color: #979db1;font-size: 11px;">当前记忆每次对话后重新生成</div> v-model="form.langCode" maxlength="1000"/>
<div class="clear-btn"> <div class="prompt-bottom" @click="clearMemory">
<i class="el-icon-delete-solid" style="font-size: 11px;"/> <div style="display: flex;gap: 8px;align-items: center;">
清除 <div style="color: #979db1;font-size: 11px;">当前记忆每次对话后重新生成</div>
<div class="clear-btn">
<i class="el-icon-delete-solid" style="font-size: 11px;"/>
清除
</div>
</div> </div>
<div style="color: #979db1;font-size:11px;">{{ form.langCode.length }}/1000</div>
</div> </div>
</div>
</el-form-item> </el-form-item>
<el-form-item v-for="model in models" :key="model.label" :label="model.label"> <el-form-item v-for="model in models" :key="model.label" :label="model.label" class="model-item">
<template slot="label"> <el-select v-model="form.model[model.key]" filterable placeholder="请选择" class="select-field">
<div style="line-height: 20px;">{{model.label}}</div> <el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value"/>
</template>
<el-select v-model="form[model.key+'ModelId']" filterable placeholder="请选择" style="width: 100%;" disabled>
<el-option v-for="item in model.list" :key="item.id" :label="item.modelName" :value="item.id"/>
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="" class="lh-form-item" style="margin-top: -25px;"> <el-form-item label="" class="lh-form-item" style="margin-top: -25px;">
@@ -74,8 +73,8 @@
实时其他模型通常会增加约1秒的延迟改变模型后建议清空记忆体以免影响体验 实时其他模型通常会增加约1秒的延迟改变模型后建议清空记忆体以免影响体验
</div> </div>
</el-form-item> </el-form-item>
</div>
</el-form> </el-form>
</div>
<div style="display: flex;padding: 16px;gap: 8px;align-items: center;"> <div style="display: flex;padding: 16px;gap: 8px;align-items: center;">
<div class="save-btn" @click="saveConfig"> <div class="save-btn" @click="saveConfig">
保存配置 保存配置
@@ -89,112 +88,80 @@
</div> </div>
</div> </div>
</div> </div>
<div style="font-size: 12px;font-weight: 400;margin-top: auto;padding-top: 24px;color: #979db1;">
©2025 xiaozhi-esp32-server
</div>
</el-main> </el-main>
<Footer :visible="true" />
</div> </div>
</template> </template>
<script> <script>
import Api from '@/apis/api';
import HeaderBar from "@/components/HeaderBar.vue"; import HeaderBar from "@/components/HeaderBar.vue";
import Footer from "@/components/Footer.vue";
import {getUUID, goToPage, showDanger, showSuccess} from '@/utils'
export default { export default {
name: 'RoleConfigPage', name: 'RoleConfigPage',
components: {HeaderBar,Footer}, components: {HeaderBar},
data() { data() {
return { return {
agentId: this.$route.query.agentId,
form: { form: {
agentCode:"", agentCode: "",
agentName:"", agentName: "",
asrModelId:"", ttsVoiceId: "",
intentModelId:"", systemPrompt: "",
llmModelId:"", langCode: "",
memoryModelId:"", language: "",
systemPrompt:"", sort: "",
ttsModelId:"", model: {
ttsVoiceId:"", ttsModelId: "",
vadModelId:"", vadModelId: "",
model:{} asrModelId: "",
llmModelId: "",
memModelId: "",
intentModelId: "",
}
}, },
ttsVoices: [], options: [
voiceDemo: "", {value: '选项1', label: '黄金糕'},
models: [ {value: '选项2', label: '双皮奶'}
{ label: '大语言模型(LLM)', key: 'llm', list: [] },
{ label: '语音转文本模型(ASR)', key: 'asr', list: [] },
{ label: '语音活动检测模型(VAD)', key: 'vad', list: [] },
{ label: '语音生成模型(TTS)', key: 'tts', list: [] },
{ label: '意图分类模型(Intent)', key: 'intent', list: [] },
{ label: '记忆增强模型(Memory)', key: 'memory', list: [] }
], ],
templates: [] models: [
{label: '大语言模型(LLM)', key: 'llmModelId'},
{label: '语音识别(ASR)', key: 'asrModelId'},
{label: '语音活动检测模型(VAD)', key: 'vadModelId'},
{label: '语音合成模型(TTS)', key: 'ttsModelId'},
{label: '意图识别模型(Intent)', key: 'intentModelId'},
{label: '记忆模型(Memory)', key: 'memModelId'}
],
templates: ['台湾女友', '土豆子', '英语老师', '好奇小男孩', '汪汪队队长'],
loadingTemplate: false
} }
}, },
mounted() {
// 获取智能体列表
this.fetchAgentTemplateList();
this.handleGetConfig();
this.fetchModelList();
this.getTtsVoicelList();
setTimeout(() => {
this.handleTtsVoiceChange(this.form.ttsVoiceId)
}, 1500)
},
methods: { methods: {
fetchAgentTemplateList() {
// 获取智能体列表
Api.agent.getAgentTemplateList(({data}) => {
if (data.code === 0) {
this.templates = data.data
} else {
showDanger(data.msg)
}
})
},
fetchModelList() {
// 获取模型配置列表
Api.model.getModelList(({data}) => {
if (data.code === 0) {
let models = data.data;
this.models.map(model => {
model.list = models.filter(item => item.modelType.toLowerCase() === model.key)
})
console.log("models", this.models)
} else {
showDanger(data.msg)
}
})
},
getTtsVoicelList(ttsModelId) {
// 获取智能体列表
Api.model.getTtsVoiceList(ttsModelId || '',({data}) => {
if (data.code === 0) {
this.ttsVoices = data.data
} else {
showDanger(data.msg)
}
})
},
handleGetConfig(){
Api.agent.getAgentConfig(this.agentId, ({data}) => {
if (data.code === 0) {
this.form = data.data
} else {
showDanger(data.msg)
}
})
},
saveConfig() { saveConfig() {
// 此处写保存配置逻辑 const configData = {
Api.agent.saveAgentConfig(this.agentId, this.form, ({data}) => { agentCode: this.form.agentCode,
if (data.code === 0) { agentName: this.form.agentName,
showSuccess('保存成功') asrModelId: this.form.model.asrModelId,
} else { vadModelId: this.form.model.vadModelId,
showDanger(data.msg) llmModelId: this.form.model.llmModelId,
} ttsModelId: this.form.model.ttsModelId,
}) ttsVoiceId: this.form.ttsVoiceId,
memModelId: this.form.model.memModelId,
intentModelId: this.form.model.intentModelId,
systemPrompt: this.form.systemPrompt,
langCode: this.form.langCode,
language: this.form.language,
sort: this.form.sort
};
import('@/apis/module/user').then(({default: userApi}) => {
userApi.updateAgentConfig(this.$route.query.agentId, configData, ({data}) => {
if (data.code === 0) {
this.$message.success('配置保存成功');
} else {
this.$message.error(data.msg || '配置保存失败');
}
});
});
}, },
resetConfig() { resetConfig() {
this.$confirm('确定要重置配置吗?', '提示', { this.$confirm('确定要重置配置吗?', '提示', {
@@ -204,47 +171,105 @@ export default {
}).then(() => { }).then(() => {
// 重置表单 // 重置表单
this.form = { this.form = {
name: "", agentCode: "",
timbre: "", agentName: "",
introduction: "", ttsVoiceId: "",
prompt: "", systemPrompt: "",
model: "" langCode: "",
language: "",
sort: "",
model: {
ttsModelId: "",
vadModelId: "",
asrModelId: "",
llmModelId: "",
memModelId: "",
intentModelId: "",
}
} }
this.$message.success('配置已重置') this.$message.success('配置已重置')
}).catch(() => { }).catch(() => {
}) })
}, },
// 处理选择模板的逻辑 selectTemplate(templateName) {
selectTemplate(template) { if (this.loadingTemplate) return;
Object.assign(this.form,{
agentCode: template.agentCode, this.loadingTemplate = true;
llmModelId: template.llmModelId, import('@/apis/module/user').then(({default: userApi}) => {
asrModelId: template.asrModelId, userApi.getAgentTemplate(
vadModelId: template.vadModelId, {templateName},
ttsModelId: template.ttsModelId, (response) => {
ttsVoiceId: template.ttsVoiceId, this.loadingTemplate = false;
intentModelId: template.intentModelId, if (response.data.code === 0 && response.data.data.length > 0) {
memoryModelId: template.memoryModelId, this.applyTemplateData(response.data.data[0]);
systemPrompt: template.systemPrompt this.$message.success(`${templateName}」模板已应用`);
}) } else {
this.$message.success(`已选择模板:${template.agentName}`); this.$message.warning(`未找到「${templateName}」模板`);
}
}
);
}).catch((error) => {
this.loadingTemplate = false;
this.$message.error('模板加载失败');
console.error('接口异常:', error);
});
}, },
handleTtsVoiceChange(ttsVoiceId) { applyTemplateData(templateData) {
if(ttsVoiceId){ this.form = {
const _ttsVoice = this.ttsVoices.find(item => item.id === ttsVoiceId) ...this.form,
this.voiceDemo = _ttsVoice.voiceDemo agentName: templateData.agentName || this.form.agentName,
this.form.ttsModelId = _ttsVoice.ttsModelId ttsVoiceId: templateData.ttsVoiceId || this.form.ttsVoiceId,
} systemPrompt: templateData.systemPrompt || this.form.systemPrompt,
langCode: templateData.langCode || this.form.langCode,
model: {
ttsModelId: templateData.ttsModelId || this.form.model.ttsModelId,
vadModelId: templateData.vadModelId || this.form.model.vadModelId,
asrModelId: templateData.asrModelId || this.form.model.asrModelId,
llmModelId: templateData.llmModelId || this.form.model.llmModelId,
memModelId: templateData.memModelId || this.form.model.memModelId,
intentModelId: templateData.intentModelId || this.form.model.intentModelId
}
};
},
fetchAgentConfig(agentId) {
import('@/apis/module/user').then(({default: userApi}) => {
userApi.getDeviceConfig(agentId, ({data}) => {
if (data.code === 0) {
this.form = {
...this.form,
...data.data,
model: {
ttsModelId: data.data.ttsModelId,
vadModelId: data.data.vadModelId,
asrModelId: data.data.asrModelId,
llmModelId: data.data.llmModelId,
memModelId: data.data.memModelId,
intentModelId: data.data.intentModelId
}
};
} else {
this.$message.error(data.msg || '获取配置失败');
}
});
});
},
// 清空记忆体内容
clearMemory() {
this.form.langCode = "";
this.$message.success("记忆体已清空");
},
},
mounted() {
const agentId = this.$route.query.agentId;
console.log('agentId2222',agentId);
if (agentId) {
this.fetchAgentConfig(agentId);
} }
} }
} }
</script> </script>
<style scoped> <style scoped>
.breadcrumbs{
padding: 20px 0 0 5px;
}
.welcome { .welcome {
min-width: 900px; min-width: 900px;
min-height: 506px; min-height: 506px;
@@ -262,6 +287,23 @@ export default {
/* 兼容老版本Opera浏览器 */ /* 兼容老版本Opera浏览器 */
} }
.el-form-item ::v-deep .el-form-item__label {
font-size: 10px !important;
color: #3d4566 !important;
font-weight: 400;
line-height: 22px;
padding-bottom: 2px;
}
.select-field{
width: 100%;
max-width: 720px;
border: 1px solid #e4e6ef;
background: #f6f8fb;
border-radius: 8px;
height: 36px !important;
}
.audio-box { .audio-box {
flex: 1; flex: 1;
height: 37px; height: 37px;
@@ -290,11 +332,13 @@ export default {
} }
.template-item { .template-item {
padding: 0 20px; height: 37px;
border-radius: 6px; width: 76px;
border-radius: 8px;
background: #e6ebff; background: #e6ebff;
font-weight: 500; line-height: 37px;
font-size: 14px; font-weight: 400;
font-size: 11px;
text-align: center; text-align: center;
color: #5778ff; color: #5778ff;
cursor: pointer; cursor: pointer;
@@ -305,6 +349,21 @@ export default {
background-color: #d0d8ff; background-color: #d0d8ff;
} }
.prompt-bottom {
margin-bottom: 4px;
display: flex;
justify-content: space-between;
padding: 0 16px;
align-items: center;
}
.input-46 {
border: 1px solid #e4e6ef;
background: #f6f8fb;
border-radius: 8px;
height: 36px !important;
}
.save-btn, .save-btn,
.reset-btn { .reset-btn {
width: 112px; width: 112px;
@@ -327,5 +386,11 @@ export default {
background: #e6ebff; background: #e6ebff;
color: #5778ff; color: #5778ff;
} }
.textarea-box {
border: 1px solid #e4e6ef;
border-radius: 8px;
background: #f6f8fb;
}
</style> </style>
+22 -13
View File
@@ -82,9 +82,10 @@ selected_module:
TTS: EdgeTTS TTS: EdgeTTS
# 记忆模块,默认不开启记忆;如果想使用超长记忆,推荐使用mem0ai;如果注重隐私,请使用本地的mem_local_short # 记忆模块,默认不开启记忆;如果想使用超长记忆,推荐使用mem0ai;如果注重隐私,请使用本地的mem_local_short
Memory: nomem Memory: nomem
# 意图识别模块,默认使用function_call。开启后,可以播放音乐、控制音量、识别退出指令 # 意图识别模块开启后,可以播放音乐、控制音量、识别退出指令
# 意图识别使用intent_llm,优点:通用性强,缺点:增加串行前置意图识别模块,会增加处理时间 # 不想开通意图识别,就设置成:nointent
# 意图识别使用function_call,缺点:需要所选择的LLM支持function_call,优点:按需调用工具、速度快 # 意图识别使用intent_llm,如果你的LLM是DifyLLM或CozeLLM,建议使用这个。优点:通用性强,缺点:增加串行前置意图识别模块,会增加处理时间,这个意图识别暂时不支持控制音量大小等iot操作
# 意图识别可使用function_call,缺点:需要所选择的LLM支持function_call,优点:按需调用工具、速度快,理论上能全部操作所有iot指令
# 默认免费的ChatGLMLLM就已经支持function_call,但是如果像追求稳定建议把LLM设置成:DoubaoLLM,使用的具体model_name是:doubao-pro-32k-functioncall-241028 # 默认免费的ChatGLMLLM就已经支持function_call,但是如果像追求稳定建议把LLM设置成:DoubaoLLM,使用的具体model_name是:doubao-pro-32k-functioncall-241028
Intent: function_call Intent: function_call
@@ -97,9 +98,13 @@ Intent:
intent_llm: intent_llm:
# 不需要动type # 不需要动type
type: intent_llm type: intent_llm
# 配备意图识别独立的思考模型
# 如果这里不填,则会默认使用selected_module.LLM的模型作为意图识别的思考模型
# 如果你的selected_module.LLM选择了DifyLLM或CozeLLM,这里最好使用独立的LLM作为意图识别,例如使用免费的ChatGLMLLM
llm: ChatGLMLLM
function_call: function_call:
# 不需要动type # 不需要动type
type: nointent type: function_call
# plugins_func/functions下的模块,可以通过配置,选择加载哪个模块,加载后对话支持相应的function调用 # plugins_func/functions下的模块,可以通过配置,选择加载哪个模块,加载后对话支持相应的function调用
# 系统默认已经记载“handle_exit_intent(退出识别)”、“play_music(音乐播放)”插件,请勿重复加载 # 系统默认已经记载“handle_exit_intent(退出识别)”、“play_music(音乐播放)”插件,请勿重复加载
# 下面是加载查天气、角色切换、加载查新闻的插件示例 # 下面是加载查天气、角色切换、加载查新闻的插件示例
@@ -187,7 +192,7 @@ LLM:
# 可在这里找到你的 api_key https://bailian.console.aliyun.com/?apiKey=1#/api-key # 可在这里找到你的 api_key https://bailian.console.aliyun.com/?apiKey=1#/api-key
base_url: https://dashscope.aliyuncs.com/compatible-mode/v1 base_url: https://dashscope.aliyuncs.com/compatible-mode/v1
model_name: qwen-turbo model_name: qwen-turbo
api_key: 你的ali api key api_key: 你的deepseek web key
temperature: 0.7 # 温度值 temperature: 0.7 # 温度值
max_tokens: 500 # 最大生成token数 max_tokens: 500 # 最大生成token数
top_p: 1 top_p: 1
@@ -278,6 +283,18 @@ LLM:
variables: variables:
k: "v" k: "v"
k2: "v2" k2: "v2"
XinferenceLLM:
# 定义LLM API类型
type: xinference
# Xinference服务地址和模型名称
model_name: qwen2.5:72b-AWQ # 使用的模型名称,需要预先在Xinference启动对应模型
base_url: http://localhost:9997 # Xinference服务地址
XinferenceSmallLLM:
# 定义轻量级LLM API类型,用于意图识别
type: xinference
# Xinference服务地址和模型名称
model_name: qwen2.5:3b-AWQ # 使用的小模型名称,用于意图识别
base_url: http://localhost:9997 # Xinference服务地址
TTS: TTS:
# 当前支持的type为edge、doubao,可自行适配 # 当前支持的type为edge、doubao,可自行适配
EdgeTTS: EdgeTTS:
@@ -535,11 +552,3 @@ wakeup_words:
- "喵喵同学" - "喵喵同学"
- "小滨小滨" - "小滨小滨"
- "小冰小冰" - "小冰小冰"
# 是否使用私有配置
use_private_config: true
# 远程配置
# 数据格式:{"code": 0,"msg": "success","data": {"prompt": "我是小智","owner": "用户ID","ASR": {"FunASR": {}},"LLM": {"ChatGLMLLM": {}},"TTS": {"EdgeTTS": {}},"VAD": {"SileroVAD": {}},"Intent": {"function_call": {}},"Memory": {"nomem": {}},"selected_module": {"ASR": "FunASR","LLM": "ChatGLMLLM","TTS": "EdgeTTS","Intent": "function_call","Memory": "nomem","VAD": "SileroVAD"}}}
remote_config:
enabled: true
url: http://192.168.5.11:8002/xiaozhi-esp32-api/api/v1/user/agent/loadAgentConfig/
+4 -73
View File
@@ -1,8 +1,6 @@
import os import os
import time import time
import yaml import yaml
import json
import requests
from config.logger import setup_logging from config.logger import setup_logging
from typing import Dict, Any, Optional from typing import Dict, Any, Optional
from copy import deepcopy from copy import deepcopy
@@ -24,14 +22,6 @@ class PrivateConfig:
async def load_or_create(self): async def load_or_create(self):
try: try:
# 优先通过远程获取配置
fetch_config_ = {}
remote_config = self.default_config['remote_config']
self.logger.bind(tag=TAG).info(f"remote config: {remote_config}")
if remote_config and remote_config['enabled']:
fetch_config_ = await self.fetch_config(remote_config['url'])
self.logger.bind(tag=TAG).info(f"fetch_config: {fetch_config_}")
await self.lock_manager.acquire_lock(self.config_path) await self.lock_manager.acquire_lock(self.config_path)
try: try:
if os.path.exists(self.config_path): if os.path.exists(self.config_path):
@@ -40,9 +30,6 @@ class PrivateConfig:
else: else:
all_configs = {} all_configs = {}
if fetch_config_:
all_configs[self.device_id] = fetch_config_
if self.device_id not in all_configs: if self.device_id not in all_configs:
# Get selected module names # Get selected module names
selected_modules = self.default_config['selected_module'] selected_modules = self.default_config['selected_module']
@@ -77,9 +64,9 @@ class PrivateConfig:
all_configs[self.device_id] = device_config all_configs[self.device_id] = device_config
# Save updated configs # Save updated configs
with open(self.config_path, 'w', encoding='utf-8') as f: with open(self.config_path, 'w', encoding='utf-8') as f:
yaml.dump(all_configs, f, allow_unicode=True) yaml.dump(all_configs, f, allow_unicode=True)
self.private_config = all_configs[self.device_id] self.private_config = all_configs[self.device_id]
@@ -251,60 +238,4 @@ class PrivateConfig:
def get_owner(self) -> Optional[str]: def get_owner(self) -> Optional[str]:
"""获取设备当前所有者""" """获取设备当前所有者"""
return self.private_config.get('owner') return self.private_config.get('owner')
async def fetch_config(self, fetch_url: str) -> Dict[str, Any]:
"""通过HTTP请求远程获取配置"""
url = f"{fetch_url}{self.device_id}"
headers = {
'device_id': self.device_id
}
try:
response = requests.get(url, headers=headers)
response.raise_for_status() # 如果响应状态码不是200,会抛出异常
# 检查响应内容类型是否为JSON
if response.headers.get('Content-Type') != 'application/json':
self.logger.bind(tag=TAG).error("Invalid content type: expected application/json")
return {}
# 解析JSON数据
config_data = response.json()
# 验证返回的数据结构
if not isinstance(config_data, dict):
self.logger.bind(tag=TAG).error("Invalid data format: expected a dictionary")
return {}
if config_data['code'] != 0:
self.logger.bind(tag=TAG).error(f"Fetch config error: {config_data['msg']}")
return {}
config_data_ = config_data['data']
if not isinstance(config_data_, dict):
self.logger.bind(tag=TAG).error("Invalid config data format: expected a dictionary")
return {}
# 检查必要的字段是否存在
required_fields = ['selected_module', 'prompt', 'LLM', 'TTS', 'ASR', 'VAD']
for field in required_fields:
if field not in config_data_:
self.logger.bind(tag=TAG).error(f"Missing required field: {field}")
return {}
# 检查每个模块的配置是否正确
for module in ['LLM', 'TTS', 'ASR', 'VAD']:
if not isinstance(config_data_[module], dict):
self.logger.bind(tag=TAG).error(f"Invalid data format for {module}: expected a dictionary")
return {}
selected_module = config_data_['selected_module'].get(module)
if selected_module not in config_data_[module]:
self.logger.bind(tag=TAG).error(f"Selected {module} not found in config: {selected_module}")
return {}
return config_data_
except requests.exceptions.RequestException as e:
self.logger.bind(tag=TAG).error(f"Error fetching config: {e}")
return {}
@@ -1,18 +0,0 @@
'''
自定义记忆,可以选择此模块
'''
from ..base import MemoryProviderBase, logger
TAG = __name__
class MemoryProvider(MemoryProviderBase):
def __init__(self, config):
super().__init__(config)
async def save_memory(self, msgs):
logger.bind(tag=TAG).debug("mem_custom mode: Custom memory saving is performed.")
return None
async def query_memory(self, query: str)-> str:
logger.bind(tag=TAG).debug("mem_custom mode: Custom memory query is performed.")
return ""