update:合并main分支
@@ -27,6 +27,23 @@ nav {
|
||||
}
|
||||
}
|
||||
|
||||
.copyright {
|
||||
text-align: center;
|
||||
color:rgb(0, 0, 0);
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
margin-top: auto;
|
||||
padding: 30px 0 20px;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 100%;
|
||||
}
|
||||
.el-message {
|
||||
top: 45px !important;
|
||||
}
|
||||
|
||||
</style>
|
||||
<script setup lang="ts">
|
||||
<script>
|
||||
</script>
|
||||
@@ -1,13 +1,13 @@
|
||||
// 引入各个模块的请求
|
||||
import user from './module/user.js'
|
||||
|
||||
import admin from './module/admin.js'
|
||||
/**
|
||||
* 接口地址
|
||||
* 当前8002端口接口还没开发完成,暂时用 apifoxmock 接口代替
|
||||
* 如果你想调用8002端口,就用'/xiaozhi-esp32-api/api/v1',请与vue.config.js的devServer配置相结合,方便跨域请求
|
||||
*
|
||||
*/
|
||||
const DEV_API_SERVICE = 'https://apifoxmock.com/m1/5931378-5618560-default'
|
||||
const DEV_API_SERVICE = 'https://2662r3426b.vicp.fun/xiaozhi-esp32-api'
|
||||
// 8002开发完成完成后使用这个
|
||||
// const DEV_API_SERVICE = '/xiaozhi-esp32-api'
|
||||
|
||||
@@ -16,6 +16,7 @@ const DEV_API_SERVICE = 'https://apifoxmock.com/m1/5931378-5618560-default'
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getServiceUrl() {
|
||||
// return '/xiaozhi-esp32-api'
|
||||
return DEV_API_SERVICE
|
||||
}
|
||||
|
||||
|
||||
@@ -105,11 +105,12 @@ function httpHandlerError(info, callBack) {
|
||||
/** 请求成功,退出该函数 可以根据项目需求来判断是否请求成功。这里判断的是status为200的时候是成功 */
|
||||
let networkError = false
|
||||
if (info.status === 200) {
|
||||
|
||||
if (info.data.code === 'success' || info.data.code === 0 || info.data.code === undefined) {
|
||||
return networkError
|
||||
}else if (info.data.code === 401) {
|
||||
goToPage(Constant.PAGE.LOGIN, true)
|
||||
console.log('触发 401,清除 Token 并跳转登录页');
|
||||
store.commit('clearAuth');
|
||||
goToPage(Constant.PAGE.LOGIN, true);
|
||||
return true
|
||||
} else {
|
||||
showDanger(info.data.msg)
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import RequestService from '../httpRequest'
|
||||
import {getServiceUrl} from '../api'
|
||||
|
||||
|
||||
export default {
|
||||
// 用户列表
|
||||
getUserList(params, callback) {
|
||||
const queryParams = new URLSearchParams({
|
||||
page: params.page,
|
||||
limit: params.limit,
|
||||
mobile: params.mobile
|
||||
}).toString();
|
||||
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/api/v1/admin/users?${queryParams}`)
|
||||
.method('GET')
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime()
|
||||
callback(res)
|
||||
})
|
||||
.fail((err) => {
|
||||
console.error('请求失败:', err)
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.getUserList(callback)
|
||||
})
|
||||
}).send()
|
||||
},
|
||||
// 删除用户
|
||||
deleteUser(id, callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/api/v1/admin/users/${id}`)
|
||||
.method('DELETE')
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime()
|
||||
callback(res)
|
||||
})
|
||||
.fail((err) => {
|
||||
console.error('删除失败:', err)
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.deleteUser(id, callback)
|
||||
})
|
||||
}).send()
|
||||
},
|
||||
// 重置用户密码
|
||||
resetUserPassword(id, callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/api/v1/admin/users/${id}`)
|
||||
.method('PUT')
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime()
|
||||
callback(res)
|
||||
})
|
||||
.fail((err) => {
|
||||
console.error('重置密码失败:', err)
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.resetUserPassword(id, callback)
|
||||
})
|
||||
}).send()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import RequestService from '../httpRequest'
|
||||
import {getServiceUrl} from '../api'
|
||||
|
||||
export default {
|
||||
// 已绑设备
|
||||
getAgentBindDevices(agentId, callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/api/v1/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/device/unbind`)
|
||||
.method('POST')
|
||||
.data({ deviceId: device_id })
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime();
|
||||
callback(res);
|
||||
})
|
||||
.fail((err) => {
|
||||
console.error('解绑设备失败:', err);
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.unbindDevice(device_id, callback);
|
||||
});
|
||||
}).send();
|
||||
},
|
||||
// 绑定设备
|
||||
bindDevice(agentId, deviceCode, callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/api/v1/device/bind/${agentId}/${deviceCode}`)
|
||||
.method('POST')
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime();
|
||||
callback(res);
|
||||
})
|
||||
.fail((err) => {
|
||||
console.error('绑定设备失败:', err);
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.bindDevice(agentId, deviceCode, callback);
|
||||
});
|
||||
}).send();
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import RequestService from '../httpRequest'
|
||||
import {getServiceUrl} from '../api'
|
||||
|
||||
|
||||
export default {
|
||||
// 获取模型配置列表
|
||||
getModelList(params, callback) {
|
||||
const queryParams = new URLSearchParams({
|
||||
modelType: params.modelType,
|
||||
modelName: params.modelName || '',
|
||||
page: params.page || 0,
|
||||
limit: params.limit || 10
|
||||
}).toString();
|
||||
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/api/v1/models/models/list?${queryParams}`)
|
||||
.method('GET')
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime()
|
||||
callback(res)
|
||||
})
|
||||
.fail((err) => {
|
||||
console.error('获取模型列表失败:', err)
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.getModelList(params, callback)
|
||||
})
|
||||
}).send()
|
||||
},
|
||||
|
||||
}
|
||||
@@ -21,7 +21,6 @@ export default {
|
||||
},
|
||||
// 获取验证码
|
||||
getCaptcha(uuid, callback) {
|
||||
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/api/v1/user/captcha?uuid=${uuid}`)
|
||||
.method('GET')
|
||||
@@ -52,7 +51,6 @@ export default {
|
||||
.fail(() => {
|
||||
}).send()
|
||||
},
|
||||
|
||||
// 保存设备配置
|
||||
saveDeviceConfig(device_id, configData, callback) {
|
||||
RequestService.sendRequest()
|
||||
@@ -106,54 +104,4 @@ export default {
|
||||
})
|
||||
.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();
|
||||
},
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 5.8 MiB |
|
After Width: | Height: | Size: 97 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 124 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 3.8 MiB |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 876 B |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 13 KiB |
@@ -0,0 +1,98 @@
|
||||
<template>
|
||||
<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="width: 40px;height: 40px;border-radius: 50%;background: #5778ff;display: flex;align-items: center;justify-content: center;">
|
||||
<img src="@/assets/home/equipment.png" alt="" style="width: 18px;height: 15px;" />
|
||||
</div>
|
||||
激活设备
|
||||
</div>
|
||||
<div style="height: 1px;background: #e8f0ff;" />
|
||||
<div style="margin: 22px 15px;">
|
||||
<div style="font-weight: 400;font-size: 14px;text-align: left;color: #3d4566;">
|
||||
<div style="color: red;display: inline-block;">*</div> 设备激活码:
|
||||
</div>
|
||||
<div class="input-46" style="margin-top: 12px;">
|
||||
<el-input placeholder="请输入设备播报的激活码.." v-model="activateCode" />
|
||||
</div>
|
||||
</div>
|
||||
<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-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import userApi from '@/apis/module/user';
|
||||
|
||||
|
||||
export default {
|
||||
name: 'ActivateDeviceDialog',
|
||||
props: {
|
||||
visible: { type: Boolean, required: true }
|
||||
},
|
||||
data() {
|
||||
return { activateCode: "" }
|
||||
},
|
||||
methods: {
|
||||
confirm() {
|
||||
if (!this.activateCode.trim()) {
|
||||
this.$message.error('请输入激活码');
|
||||
return;
|
||||
}
|
||||
userApi.activateDevice(this.activateCode, (res) => {
|
||||
this.$message.success('激活成功,请重新唤醒设备即可完成激活!');
|
||||
this.$emit('confirm', res);
|
||||
this.$emit('update:visible', false);
|
||||
console.log("res: ", res)
|
||||
this.activateCode = "";
|
||||
});
|
||||
},
|
||||
cancel() {
|
||||
this.$emit('update:visible', false)
|
||||
this.activateCode = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.input-46 {
|
||||
border: 1px solid #e4e6ef;
|
||||
background: #f6f8fb;
|
||||
border-radius: 15px;
|
||||
}
|
||||
|
||||
.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>
|
||||
@@ -33,16 +33,52 @@
|
||||
export default {
|
||||
name: 'AddDeviceDialog',
|
||||
props: {
|
||||
visible: { type: Boolean, required: true }
|
||||
visible: { type: Boolean, required: true },
|
||||
agentId: { type: String, required: true }
|
||||
},
|
||||
data() {
|
||||
return { deviceCode: "" }
|
||||
return {
|
||||
deviceCode: "",
|
||||
loading: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
confirm() {
|
||||
this.$emit('update:visible', false)
|
||||
this.$emit('added', this.deviceCode)
|
||||
this.deviceCode = ""
|
||||
if (!/^\d{6}$/.test(this.deviceCode)) {
|
||||
this.$message.error('请输入6位数字验证码');
|
||||
return;
|
||||
}
|
||||
this.loading = true;
|
||||
import('@/apis/module/device').then(({ default: deviceApi }) => {
|
||||
deviceApi.bindDevice(
|
||||
this.agentId,
|
||||
this.deviceCode, ({data}) => {
|
||||
this.loading = false;
|
||||
if (data.code === 0) {
|
||||
this.$emit('refresh');
|
||||
this.$message.success({
|
||||
message: '设备绑定成功',
|
||||
showClose: true
|
||||
});
|
||||
this.closeDialog();
|
||||
} else {
|
||||
this.$message.error({
|
||||
message: data.msg || '绑定失败',
|
||||
showClose: true
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
}).catch((err) => {
|
||||
this.loading = false;
|
||||
console.error('API模块加载失败:', err);
|
||||
this.$message.error('绑定服务不可用');
|
||||
});
|
||||
},
|
||||
closeDialog() {
|
||||
this.$emit('update:visible', false);
|
||||
this.deviceCode = '';
|
||||
|
||||
},
|
||||
cancel() {
|
||||
this.$emit('update:visible', false)
|
||||
|
||||
@@ -0,0 +1,387 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
:visible.sync="visible"
|
||||
width="975px"
|
||||
center
|
||||
custom-class="custom-dialog"
|
||||
:show-close="false"
|
||||
class="center-dialog"
|
||||
|
||||
>
|
||||
<div style="margin: 0 18px; text-align: left; padding: 10px; border-radius: 10px;">
|
||||
<div style="font-size: 30px; color: #3d4566; margin-top: -10px; margin-bottom: 10px; text-align: center;">
|
||||
添加模型
|
||||
</div>
|
||||
|
||||
<!-- 关闭按钮 -->
|
||||
<button class="custom-close-btn" @click="handleClose">
|
||||
×
|
||||
</button>
|
||||
|
||||
<!-- 模型信息部分 -->
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
|
||||
<div style="font-size: 20px; font-weight: bold; color: #3d4566;">模型信息</div>
|
||||
<div style="display: flex; align-items: center; gap: 20px;">
|
||||
<div style="display: flex; align-items: center;">
|
||||
<span style="margin-right: 8px;">是否启用</span>
|
||||
<el-switch v-model="formData.isEnabled" class="custom-switch"></el-switch>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center;">
|
||||
<span style="margin-right: 8px;">设为默认</span>
|
||||
<el-switch v-model="formData.isDefault" class="custom-switch"></el-switch>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="height: 2px; background: #e9e9e9; margin-bottom: 22px;"></div>
|
||||
|
||||
<el-form :model="formData" label-width="100px" label-position="left" class="custom-form">
|
||||
<!-- 第一行:模型名称和模型编码 -->
|
||||
<div style="display: flex; gap: 20px; margin-bottom: 0;">
|
||||
<el-form-item label="模型名称" prop="modelName" style="flex: 1;">
|
||||
<el-input
|
||||
v-model="formData.modelName"
|
||||
placeholder="请输入模型名称"
|
||||
class="custom-input-bg"
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="模型编码" prop="modelCode" style="flex: 1;">
|
||||
<el-input
|
||||
v-model="formData.modelCode"
|
||||
placeholder="请输入模型编码"
|
||||
class="custom-input-bg"
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<!-- 第二行:供应器和排序号 -->
|
||||
<div style="display: flex; gap: 20px; margin-bottom: 0;">
|
||||
<el-form-item label="供应器" prop="supplier" style="flex: 1;">
|
||||
<el-select
|
||||
v-model="formData.supplier"
|
||||
placeholder="请选择"
|
||||
class="custom-select custom-input-bg"
|
||||
style="width: 100%;"
|
||||
>
|
||||
<el-option label="硅基流动" value="硅基流动"></el-option>
|
||||
<el-option label="智脑科技" value="智脑科技"></el-option>
|
||||
<el-option label="云智科技" value="云智科技"></el-option>
|
||||
<el-option label="其他" value="其他"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序号" prop="sortOrder" style="flex: 1;">
|
||||
<el-input
|
||||
v-model="formData.sort"
|
||||
placeholder="请输入排序号"
|
||||
class="custom-input-bg"
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<!-- 文档地址 -->
|
||||
<el-form-item label="文档地址" prop="docLink" style="margin-bottom: 27px;">
|
||||
<el-input
|
||||
v-model="formData.docLink"
|
||||
placeholder="请输入文档地址"
|
||||
class="custom-input-bg"
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 备注 -->
|
||||
<el-form-item label="备注" prop="remark" class="prop-remark">
|
||||
<el-input
|
||||
v-model="formData.remark"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入模型备注"
|
||||
class="custom-input-bg"
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 调用信息部分 -->
|
||||
<div style="font-size: 20px; font-weight: bold; color: #3d4566; margin-bottom: 15px;">调用信息</div>
|
||||
<div style="height: 2px; background: #e9e9e9; margin-bottom: 15px;"></div>
|
||||
|
||||
<el-form :model="formData" label-width="100px" label-position="left" class="custom-form">
|
||||
<!-- 第一行:模型名称和接口地址 -->
|
||||
<div style="display: flex; gap: 10px; margin-bottom: 15px;">
|
||||
<el-form-item label="模型名称" prop="param1" style="flex: 0.5; margin-bottom: 0;">
|
||||
<el-input
|
||||
v-model="formData.configJson.param1"
|
||||
placeholder="请输入model_name"
|
||||
class="custom-input-bg"
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="接口地址" prop="param2" style="flex: 1; margin-bottom: 0;">
|
||||
<el-input
|
||||
v-model="formData.configJson.param2"
|
||||
placeholder="请输入base_url"
|
||||
class="custom-input-bg"
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<!-- 秘钥信息 -->
|
||||
<el-form-item label="秘钥信息" prop="apiKey">
|
||||
<el-input
|
||||
v-model="formData.configJson.apiKey"
|
||||
placeholder="请输入api_key"
|
||||
show-password
|
||||
class="custom-input-bg"
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 保存按钮 -->
|
||||
<div style="display: flex;justify-content: center;">
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="confirm"
|
||||
class="save-btn"
|
||||
>
|
||||
保存
|
||||
</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'AddModelDialog',
|
||||
props: {
|
||||
visible: {type: Boolean, required: true},
|
||||
modelType: {type: String, required: true}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
formData: {
|
||||
modelName: '',
|
||||
modelCode: '',
|
||||
supplier: '',
|
||||
sort: 1,
|
||||
docLink: '',
|
||||
remark: '',
|
||||
isEnabled: true,
|
||||
isDefault: true,
|
||||
configJson: {
|
||||
param1: '',
|
||||
param2: '',
|
||||
apiKey: ''
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
confirm() {
|
||||
if (!this.formData.modelName || !this.formData.modelCode || !this.formData.supplier ||
|
||||
!this.formData.configJson.param1 || !this.formData.configJson.param2 || !this.formData.configJson.apiKey) {
|
||||
this.$message.error('请填写所有必填字段');
|
||||
return;
|
||||
}
|
||||
|
||||
this.$emit('confirm', {
|
||||
...this.formData,
|
||||
provideType: this.formData.supplier
|
||||
});
|
||||
this.$emit('update:visible', false);
|
||||
this.resetForm();
|
||||
},
|
||||
resetForm() {
|
||||
this.formData = {
|
||||
modelName: '',
|
||||
modelCode: '',
|
||||
supplier: '',
|
||||
sort: 1,
|
||||
docLink: '',
|
||||
remark: '',
|
||||
isEnabled: true,
|
||||
isDefault: true,
|
||||
configJson: {
|
||||
param1: '',
|
||||
param2: '',
|
||||
apiKey: ''
|
||||
}
|
||||
};
|
||||
},
|
||||
handleClose() {
|
||||
this.resetForm();
|
||||
this.$emit('update:visible', false);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.custom-dialog {
|
||||
position: relative;
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
background: white;
|
||||
padding-bottom: 17px;
|
||||
}
|
||||
|
||||
.custom-dialog .el-dialog__header {
|
||||
padding: 0;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.center-dialog {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.center-dialog .el-dialog {
|
||||
margin: 4% 0 auto !important;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.custom-close-btn {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid #cfcfcf;
|
||||
background: none;
|
||||
font-size: 30px;
|
||||
font-weight: lighter;
|
||||
color: #cfcfcf;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1;
|
||||
padding: 0;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.custom-close-btn:hover {
|
||||
color: #409EFF;
|
||||
border-color: #409EFF;
|
||||
}
|
||||
|
||||
.custom-select .el-input__suffix {
|
||||
background: #e6e8ea;
|
||||
right: 6px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
top: 9px;
|
||||
}
|
||||
|
||||
.custom-select .el-input__suffix-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.custom-select .el-icon-arrow-up:before {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 5px solid transparent;
|
||||
border-right: 5px solid transparent;
|
||||
border-top: 7px solid #c0c4cc;
|
||||
position: relative;
|
||||
top: -2px;
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.custom-form .el-form-item {
|
||||
margin-bottom: 20px; /* 统一设置所有表单项的间距 */
|
||||
}
|
||||
|
||||
.custom-form .el-form-item__label {
|
||||
color: #3d4566;
|
||||
font-weight: normal;
|
||||
text-align: right;
|
||||
padding-right: 20px;
|
||||
|
||||
}
|
||||
|
||||
.custom-form .el-form-item.prop-remark .el-form-item__label {
|
||||
margin-top: -4px;
|
||||
}
|
||||
|
||||
/* 修改placeholder颜色 */
|
||||
.custom-input-bg .el-input__inner::-webkit-input-placeholder,
|
||||
.custom-input-bg .el-textarea__inner::-webkit-input-placeholder {
|
||||
color: #9c9f9e;
|
||||
}
|
||||
|
||||
/* 输入框背景色 */
|
||||
.custom-input-bg .el-input__inner,
|
||||
.custom-input-bg .el-textarea__inner {
|
||||
background-color: #f6f8fc;
|
||||
}
|
||||
|
||||
|
||||
.save-btn {
|
||||
background: #e6f0fd;
|
||||
color: #237ff4;
|
||||
border: 1px solid #b3d1ff;
|
||||
width: 150px;
|
||||
height: 40px;
|
||||
font-size: 16px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.save-btn:hover {
|
||||
background: linear-gradient(to right, #237ff4, #9c40d5);
|
||||
color: white;
|
||||
border: none;
|
||||
}
|
||||
|
||||
|
||||
/* 修改开关样式 */
|
||||
.custom-switch .el-switch__core {
|
||||
border-radius: 20px;
|
||||
height: 23px;
|
||||
background-color: #c0ccda;
|
||||
width: 35px;
|
||||
padding: 0 20px; /* 调整左右内边距 */
|
||||
}
|
||||
|
||||
.custom-switch .el-switch__core:after {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
background-color: white;
|
||||
top: 3px;
|
||||
left: 4px;
|
||||
transition: all .3s;
|
||||
}
|
||||
|
||||
.custom-switch.is-checked .el-switch__core {
|
||||
border-color: #b5bcf0;
|
||||
background-color: #cfd7fa;
|
||||
padding: 0 20px; /* 确保启用状态也有相同的间隔 */
|
||||
}
|
||||
|
||||
.custom-switch.is-checked .el-switch__core:after {
|
||||
left: 100%;
|
||||
margin-left: -18px;
|
||||
background-color: #1b47ee;
|
||||
}
|
||||
|
||||
|
||||
/* 调整flex布局的gap */
|
||||
[style*="display: flex"] {
|
||||
gap: 20px; /* 扩大flex项间距 */
|
||||
}
|
||||
|
||||
/* 调整输入框高度 */
|
||||
.custom-input-bg .el-input__inner {
|
||||
height: 32px; /* 固定输入框高度 */
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
@@ -2,7 +2,7 @@
|
||||
<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="width: 40px;height: 40px;border-radius: 50%;background: #5778ff;display: flex;align-items: center;justify-content: center;">
|
||||
<img src="@/assets/home/equipment.png" alt="" style="width: 18px;height: 15px;" />
|
||||
<img loading="lazy" src="@/assets/home/equipment.png" alt="" style="width: 18px;height: 15px;" />
|
||||
</div>
|
||||
添加智能体
|
||||
</div>
|
||||
@@ -47,7 +47,10 @@ export default {
|
||||
return;
|
||||
}
|
||||
userApi.addAgent(this.wisdomBodyName, (res) => {
|
||||
this.$message.success('添加成功');
|
||||
this.$message.success({
|
||||
message: '添加成功',
|
||||
showClose: true
|
||||
});
|
||||
this.$emit('confirm', res);
|
||||
this.$emit('update:visible', false);
|
||||
this.wisdomBodyName = "";
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
<template>
|
||||
<form>
|
||||
<el-dialog :visible.sync="value" 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="width: 40px;height: 40px;border-radius: 50%;background: #5778ff;display: flex;align-items: center;justify-content: center;">
|
||||
<img loading="lazy" src="@/assets/login/shield.png" alt="" style="width: 19px;height: 23px; filter: brightness(0) invert(1);" />
|
||||
</div>
|
||||
修改密码
|
||||
</div>
|
||||
<div style="height: 1px;background: #e8f0ff;"/>
|
||||
<div style="margin: 22px 15px;">
|
||||
<div style="font-weight: 400;font-size: 14px;text-align: left;color: #3d4566;">
|
||||
<div style="color: red;display: inline-block;">*</div>
|
||||
旧密码:
|
||||
</div>
|
||||
<div class="input-46" style="margin-top: 12px;">
|
||||
<el-input placeholder="请输入旧密码" v-model="oldPassword" type="password" show-password/>
|
||||
</div>
|
||||
<div style="font-weight: 400;font-size: 14px;text-align: left;color: #3d4566;margin-top: 12px;">
|
||||
<div style="color: red;display: inline-block;">*</div>
|
||||
新密码:
|
||||
</div>
|
||||
<div class="input-46" style="margin-top: 12px;">
|
||||
<el-input placeholder="请输入新密码" v-model="newPassword" type="password" show-password/>
|
||||
</div>
|
||||
<div style="font-weight: 400;font-size: 14px;text-align: left;color: #3d4566;margin-top: 12px;">
|
||||
<div style="color: red;display: inline-block;">*</div>
|
||||
确认新密码:
|
||||
</div>
|
||||
<div class="input-46" style="margin-top: 12px;">
|
||||
<el-input placeholder="请再次输入新密码" v-model="confirmNewPassword" type="password" show-password/>
|
||||
</div>
|
||||
</div>
|
||||
<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-dialog>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import userApi from '@/apis/module/user';
|
||||
import { mapActions } from 'vuex';
|
||||
|
||||
export default {
|
||||
name: 'ChangePasswordDialog',
|
||||
props: {
|
||||
value: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
oldPassword: "",
|
||||
newPassword: "",
|
||||
confirmNewPassword: ""
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['logout']), // 引入Vuex的logout action
|
||||
confirm() {
|
||||
if (!this.oldPassword.trim() || !this.newPassword.trim() || !this.confirmNewPassword.trim()) {
|
||||
this.$message.error('请填写所有字段');
|
||||
return;
|
||||
}
|
||||
if (this.newPassword !== this.confirmNewPassword) {
|
||||
this.$message.error('两次输入的新密码不一致');
|
||||
return;
|
||||
}
|
||||
if (this.newPassword === this.oldPassword) {
|
||||
this.$message.error('新密码不能与旧密码相同');
|
||||
return;
|
||||
}
|
||||
|
||||
// 修改后的接口调用
|
||||
userApi.changePassword(this.oldPassword, this.newPassword, (res) => {
|
||||
if (res.data.code === 0) {
|
||||
this.$message.success('密码修改成功,请重新登录');
|
||||
this.logout().then(() => {
|
||||
this.$router.push('/login');
|
||||
this.$emit('update:visible', false);
|
||||
});
|
||||
}else{
|
||||
this.$message.error(res.data.msg || '密码修改失败');
|
||||
}
|
||||
}, (err) => {
|
||||
this.$message.error(err.msg || '密码修改失败');
|
||||
});
|
||||
this.$emit('input', false);
|
||||
},
|
||||
cancel() {
|
||||
this.$emit('input', false);
|
||||
this.resetForm();
|
||||
},
|
||||
resetForm() {
|
||||
this.oldPassword = "";
|
||||
this.newPassword = "";
|
||||
this.confirmNewPassword = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.input-46 {
|
||||
background: #f6f8fb;
|
||||
border-radius: 15px;
|
||||
}
|
||||
|
||||
.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>
|
||||
@@ -6,7 +6,7 @@
|
||||
</div>
|
||||
<div>
|
||||
<img src="@/assets/home/delete.png" alt=""
|
||||
style="width: 18px;height: 18px;margin-right: 10px;" />
|
||||
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>
|
||||
@@ -17,16 +17,16 @@
|
||||
音色模型:{{ device.ttsVoiceName }}
|
||||
</div>
|
||||
<div style="display: flex;gap: 10px;align-items: center;">
|
||||
<div class="settings-btn" @click="$emit('configure')">
|
||||
<div class="settings-btn" @click="handleConfigure">
|
||||
配置角色
|
||||
</div>
|
||||
<div class="settings-btn">
|
||||
声纹识别
|
||||
</div>
|
||||
<div class="settings-btn">
|
||||
历史对话
|
||||
</div>
|
||||
<div class="settings-btn" @click="$emit('deviceManage')">
|
||||
<!-- <div class="settings-btn">-->
|
||||
<!-- 声纹识别-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="settings-btn">-->
|
||||
<!-- 历史对话-->
|
||||
<!-- </div>-->
|
||||
<div class="settings-btn" @click="handleDeviceManage">
|
||||
设备管理
|
||||
</div>
|
||||
</div>
|
||||
@@ -44,6 +44,17 @@ export default {
|
||||
},
|
||||
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>
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
<template>
|
||||
<el-dialog :visible.sync="dialogVisible" width="900px" @close="handleClose" class="compact-dialog" :append-to-body="true">
|
||||
<el-form :model="voiceForm" :rules="rules" ref="voiceForm" label-width="80px">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="音色编码" prop="voiceCode">
|
||||
<el-input v-model="voiceForm.voiceCode" placeholder="请输入内容" class="compact-input"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="音色名称" prop="voiceName">
|
||||
<el-input v-model="voiceForm.voiceName" placeholder="请输入内容" class="compact-input"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="语言类型" prop="languageType">
|
||||
<el-input v-model="voiceForm.languageType" placeholder="请输入内容" class="compact-input"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="排序号" prop="sortNumber">
|
||||
<el-input-number v-model="voiceForm.sortNumber" :min="1" :controls="false" class="compact-number"></el-input-number>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="voiceForm.remark" type="textarea" :rows="2" placeholder="请输入内容" class="compact-textarea"
|
||||
></el-input>
|
||||
<div class="audio-controls">
|
||||
<div class="audio-player">
|
||||
<audio
|
||||
:src="audioUrl"
|
||||
controls
|
||||
preload="metadata"
|
||||
class="custom-audio"
|
||||
></audio>
|
||||
</div>
|
||||
<el-button type="primary" size="mini" class="preview-btn">生成试听</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button type="primary" @click="handleSave">保存</el-button>
|
||||
<el-button type="primary" @click="handleClose">关闭</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'EditVoiceDialog',
|
||||
props: {
|
||||
showDialog: Boolean,
|
||||
voiceData: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
voiceCode: 'wawaxiaohe',
|
||||
voiceName: '湾湾小何',
|
||||
languageType: '中文',
|
||||
sortNumber: 123
|
||||
})
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dialogVisible: this.showDialog,
|
||||
voiceForm: { ...this.voiceData },
|
||||
audioUrl: 'http://music.163.com/song/media/outer/url?id=447925558.mp3',
|
||||
generatedAudio: null,
|
||||
rules: {
|
||||
voiceCode: [{ required: true, message: '请输入音色编码', trigger: 'blur' }],
|
||||
voiceName: [{ required: true, message: '请输入音色名称', trigger: 'blur' }]
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
showDialog(newVal) {
|
||||
this.dialogVisible = newVal
|
||||
if (newVal) this.voiceForm = { ...this.voiceData }
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleClose() {
|
||||
this.dialogVisible = false
|
||||
this.$emit('update:showDialog', false)
|
||||
},
|
||||
handleSave() {
|
||||
this.$refs.voiceForm.validate(valid => {
|
||||
if (valid) this.$emit('save', this.voiceForm)
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.compact-dialog {
|
||||
/deep/ .el-dialog__body {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.el-form-item {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.compact-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.compact-number {
|
||||
width: 100%;
|
||||
/deep/ .el-input__inner {
|
||||
padding-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.compact-textarea {
|
||||
width: 100%;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.audio-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 16px;
|
||||
margin-top: 8px;
|
||||
|
||||
|
||||
.preview-btn {
|
||||
padding: 7px 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
padding: 16px 20px 0;
|
||||
text-align: right;
|
||||
border-top: 1px solid #EBEEF5;
|
||||
|
||||
.el-button {
|
||||
min-width: 80px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,50 +1,85 @@
|
||||
<template>
|
||||
<el-header class="header">
|
||||
<div style="display: flex;justify-content: space-between;margin-top: 6px; ">
|
||||
<div style="display: flex;align-items: center;gap: 10px;">
|
||||
<img src="@/assets/xiaozhi-logo.png" alt="" style="width: 42px;height: 42px;" />
|
||||
<img src="@/assets/xiaozhi-ai.png" alt="" style="width: 58px;height: 12px;" />
|
||||
<div class="equipment-management" @click="goHome">
|
||||
<img src="@/assets/home/equipment.png" alt="" style="width: 12px;height: 10px;" />
|
||||
<div class="header-container">
|
||||
<!-- 左侧元素 -->
|
||||
<div class="header-left">
|
||||
<img loading="lazy" alt="" src="@/assets/xiaozhi-logo.png" class="logo-img"/>
|
||||
<img loading="lazy" alt="" src="@/assets/xiaozhi-ai.png" class="brand-img"/>
|
||||
</div>
|
||||
|
||||
<!-- 中间导航菜单 -->
|
||||
<div class="header-center">
|
||||
<div class="equipment-management" :class="{ 'active-tab': $route.path === '/home' }" @click="goHome">
|
||||
<img loading="lazy" alt="" src="@/assets/header/robot.png" :style="{ filter: $route.path === '/home' ? 'brightness(0) invert(1)' : 'None' }"/>
|
||||
智能体管理
|
||||
</div>
|
||||
<div class="console">
|
||||
<i class="el-icon-s-grid" style="font-size: 10px;color: #979db1;" />
|
||||
控制台
|
||||
<div v-if="isSuperAdmin" class="equipment-management" :class="{ 'active-tab': $route.path === '/user-management' }" @click="goUserManagement">
|
||||
<img loading="lazy" alt="" src="@/assets/header/user_management.png" :style="{ filter: $route.path === '/user-management' ? 'brightness(0) invert(1)' : 'None' }"/>
|
||||
用户管理
|
||||
</div>
|
||||
<div class="equipment-management2">
|
||||
设备管理
|
||||
<img src="@/assets/home/close.png" alt="" style="width: 6px;height: 6px;" />
|
||||
<div v-if="isSuperAdmin" class="equipment-management" :class="{ 'active-tab': $route.path === '/model-config' }" @click="goModelConfig">
|
||||
<img loading="lazy" alt="" src="@/assets/header/model_config.png" :style="{ filter: $route.path === '/model-config' ? 'brightness(0) invert(1)' : 'None' }"/>
|
||||
模型配置
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex;align-items: center;gap: 7px; margin-top: 2px;">
|
||||
<div class="serach-box">
|
||||
<el-input placeholder="输入名称搜索.." v-model="serach" style="border: none; background: transparent;" @keyup.enter.native="handleSearch" />
|
||||
<img src="@/assets/home/search.png" alt=""
|
||||
style="width: 14px;height: 14px;margin-right: 11px;cursor: pointer;" @click="handleSearch" />
|
||||
</div>
|
||||
<img src="@/assets/home/avatar.png" alt="" style="width: 21px;height: 21px;" />
|
||||
<div class="user-info">
|
||||
{{ userInfo.mobile || '加载中...' }}
|
||||
|
||||
<!-- 右侧元素 -->
|
||||
<div class="header-right">
|
||||
<div class="search-container">
|
||||
<el-input
|
||||
v-model="search"
|
||||
placeholder="输入名称搜索.."
|
||||
class="custom-search-input"
|
||||
@keyup.enter.native="handleSearch"
|
||||
>
|
||||
<i slot="suffix" class="el-icon-search search-icon" @click="handleSearch"></i>
|
||||
</el-input>
|
||||
</div>
|
||||
<img loading="lazy" alt="" src="@/assets/home/avatar.png" class="avatar-img"/>
|
||||
<el-dropdown trigger="click" class="user-dropdown">
|
||||
<span class="el-dropdown-link">
|
||||
{{ userInfo.username || '加载中...' }}<i class="el-icon-arrow-down el-icon--right"></i>
|
||||
</span>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<el-dropdown-item icon="el-icon-plus" @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-circle-plus-outline" @click.native="handleLogout">退出登录</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 修改密码弹窗 -->
|
||||
<ChangePasswordDialog v-model="isChangePasswordDialogVisible"/>
|
||||
</el-header>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import userApi from '@/apis/module/user'
|
||||
import userApi from '@/apis/module/user';
|
||||
import ChangePasswordDialog from './ChangePasswordDialog.vue'; // 引入修改密码弹窗组件
|
||||
import { mapActions, mapGetters } from 'vuex';
|
||||
|
||||
|
||||
export default {
|
||||
name: 'HeaderBar',
|
||||
components: {
|
||||
ChangePasswordDialog
|
||||
},
|
||||
props: ['devices'], // 接收父组件设备列表
|
||||
data() {
|
||||
return {
|
||||
serach: '',
|
||||
search: '',
|
||||
userInfo: {
|
||||
username: '',
|
||||
mobile: ''
|
||||
}
|
||||
},
|
||||
isChangePasswordDialogVisible: false // 控制修改密码弹窗的显示
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['getIsSuperAdmin']),
|
||||
isSuperAdmin() {
|
||||
return this.getIsSuperAdmin;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
@@ -53,25 +88,34 @@ export default {
|
||||
methods: {
|
||||
goHome() {
|
||||
// 跳转到首页
|
||||
this.$router.push('/')
|
||||
this.$router.push('/home')
|
||||
},
|
||||
goUserManagement() {
|
||||
this.$router.push('/user-management')
|
||||
},
|
||||
goModelConfig() {
|
||||
this.$router.push('/model-config')
|
||||
},
|
||||
// 获取用户信息
|
||||
fetchUserInfo() {
|
||||
userApi.getUserInfo(({data}) => {
|
||||
this.userInfo = data.data
|
||||
if (data.data.superAdmin !== undefined) {
|
||||
this.$store.commit('setUserInfo', data.data);
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 处理搜索
|
||||
handleSearch() {
|
||||
const searchValue = this.serach.trim();
|
||||
const searchValue = this.search.trim();
|
||||
let filteredDevices;
|
||||
|
||||
if (!searchValue) {
|
||||
// 当搜索内容为空时,显示原始完整列表
|
||||
filteredDevices = this.$parent.originalDevices;
|
||||
} else {
|
||||
// 过滤逻辑
|
||||
// 过滤逻辑
|
||||
filteredDevices = this.devices.filter(device => {
|
||||
return device.agentName.includes(searchValue) ||
|
||||
device.ttsModelName.includes(searchValue) ||
|
||||
@@ -80,96 +124,200 @@ export default {
|
||||
}
|
||||
|
||||
this.$emit('search-result', filteredDevices);
|
||||
}
|
||||
},
|
||||
|
||||
// 显示修改密码弹窗
|
||||
showChangePasswordDialog() {
|
||||
this.isChangePasswordDialogVisible = true;
|
||||
},
|
||||
// 退出登录
|
||||
async handleLogout() {
|
||||
try {
|
||||
// 调用 Vuex 的 logout action
|
||||
await this.logout();
|
||||
this.$message.success({
|
||||
message:'退出登录成功',
|
||||
showClose:true
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('退出登录失败:', error);
|
||||
this.$message.error({
|
||||
message:'退出登录失败,请重试',
|
||||
showClose:true
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 使用 mapActions 引入 Vuex 的 logout action
|
||||
...mapActions(['logout'])
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.equipment-management,
|
||||
.equipment-management2 {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.equipment-management {
|
||||
width: 82px;
|
||||
height: 24px;
|
||||
border-radius: 12px;
|
||||
background: #5778ff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 7px;
|
||||
font-weight: 500;
|
||||
color: #fff;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.equipment-management2 {
|
||||
width: 87px;
|
||||
height: 22px;
|
||||
border-radius: 11px;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
font-size: 9px;
|
||||
font-weight: 400;
|
||||
gap: 7px;
|
||||
color: #3d4566;
|
||||
margin-left: 2px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: #f6fcfe66;
|
||||
border: 1px solid #fff;
|
||||
height: 53px !important;
|
||||
min-width: 900px; /* 设置最小宽度防止过度压缩 */
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.serach-box {
|
||||
.header-container {
|
||||
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 {
|
||||
padding: 0 9px;
|
||||
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;
|
||||
border-radius: 15px;
|
||||
background-color: #f6fcfe66;
|
||||
background-color: #e2e5f8;
|
||||
border: 1px solid #e4e6ef;
|
||||
align-items: center;
|
||||
padding: 0 7px;
|
||||
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%;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
padding-left: 12px;
|
||||
}
|
||||
|
||||
|
||||
.user-info {
|
||||
font-weight: 600;
|
||||
padding-left: 15px;
|
||||
font-size: 12px;
|
||||
letter-spacing: -0.02px;
|
||||
text-align: left;
|
||||
color: #3d4566;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
width: 100%;
|
||||
}
|
||||
.console {
|
||||
width: 90px;
|
||||
height: 22px;
|
||||
border-radius: 11px;
|
||||
background: radial-gradient(50% 50% at 50% 50%, #fff 0%, #e8f0ff 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 9px;
|
||||
color: #979db1;
|
||||
font-weight: 400;
|
||||
gap: 7px;
|
||||
margin-left: 15px;
|
||||
|
||||
.search-icon {
|
||||
cursor: pointer;
|
||||
color: #909399;
|
||||
margin-right: 8px;
|
||||
font-size: 14px;
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
.avatar-img {
|
||||
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>
|
||||
@@ -0,0 +1,159 @@
|
||||
<template>
|
||||
<el-dialog :visible.sync="dialogVisible" width="800px" center>
|
||||
<el-form :model="form" ref="form" label-width="70px">
|
||||
<el-row :gutter="20" class="form-row">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="模型编码">
|
||||
<el-input v-model="form.code" placeholder="请输入内容" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="模型名称">
|
||||
<el-input v-model="form.name" placeholder="请输入内容" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 供应商 -->
|
||||
<el-row :gutter="20" class="form-row">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="供应器">
|
||||
<el-select v-model="form.supplier" placeholder="请选择">
|
||||
<el-option label="openai" value="openai" />
|
||||
<el-option label="dify" value="dify" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item>
|
||||
<div style="display: flex; align-items: center;">
|
||||
<span class="el-form-item__label" style="width: 70px;">设成默认</span>
|
||||
<el-switch v-model="form.isDefault" style="margin-right: 20px;" />
|
||||
<span class="el-form-item__label" style="width: 70px;">是否启用</span>
|
||||
<el-switch v-model="form.isEnable" />
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 文档 -->
|
||||
<el-row :gutter="20" class="form-row">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="文档地址">
|
||||
<el-input v-model="form.docUrl" placeholder="请输入内容" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="排序号">
|
||||
<el-input-number v-model="form.sort" :min="1" :max="999" controls-position="right" placeholder="123"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 备注 -->
|
||||
<el-form-item label="备注" class="form-row">
|
||||
<el-input type="textarea" v-model="form.remark" :rows="2" placeholder="请输入"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<div class="vertical-fields">
|
||||
<el-form-item label="接口地址">
|
||||
<el-input v-model="form.apiUrl" placeholder="请输入base_url" />
|
||||
</el-form-item>
|
||||
<el-form-item label="模型名称">
|
||||
<el-input v-model="form.modelName" placeholder="请输入model_name" />
|
||||
</el-form-item>
|
||||
<el-form-item label="密钥信息">
|
||||
<el-input v-model="form.apiKey" placeholder="请输入api_key" show-password/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button type="primary" @click="handleSave">保存</el-button>
|
||||
<el-button @click="dialogVisible = false">关闭</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "ModelConfigDialog",
|
||||
props: {
|
||||
visible: { type: Boolean, default: false },
|
||||
configData: { type: Object, default: () => ({}) }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dialogVisible: this.visible,
|
||||
form: {
|
||||
code: "",
|
||||
name: "",
|
||||
supplier: "",
|
||||
isDefault: true,
|
||||
isEnable: true,
|
||||
docUrl: "",
|
||||
sort: 123,
|
||||
remark: "",
|
||||
apiUrl: "",
|
||||
modelName: "",
|
||||
apiKey: ""
|
||||
}
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
dialogVisible(val) {
|
||||
this.$emit('update:visible', val);
|
||||
},
|
||||
visible(val) {
|
||||
this.dialogVisible = val;
|
||||
if (val) this.form = { ...this.form, ...this.configData };
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleSave() {
|
||||
this.$emit("submit", this.form);
|
||||
this.dialogVisible = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dialog-footer {
|
||||
margin-top: -30px;
|
||||
text-align: center;
|
||||
}
|
||||
.el-form-item {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
.el-input-number {
|
||||
width: 100%;
|
||||
}
|
||||
.form-row {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.vertical-fields {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
margin-top: -10px;
|
||||
text-align: center;
|
||||
}
|
||||
.el-input-number {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.el-input__inner {
|
||||
height: 36px;
|
||||
line-height: 36px;
|
||||
}
|
||||
|
||||
.el-form-item__label {
|
||||
text-align: left;
|
||||
padding-right: 10px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,201 @@
|
||||
<template>
|
||||
<el-dialog :visible.sync="localVisible" width="80%" @close="handleClose">
|
||||
<el-row class="main-container">
|
||||
<el-col :span="4">
|
||||
<el-menu class="model-menu" :default-active="activeModel" mode="vertical" @select="handleModelSelect">
|
||||
<el-menu-item index="EdgeTTS">EdgeTTS</el-menu-item>
|
||||
<el-menu-item index="DoubaoTTS">DoubaoTTS</el-menu-item>
|
||||
<el-menu-item index="TTS302AI">TTS302AI</el-menu-item>
|
||||
<el-menu-item index="CosyVoiceSiliconflow">CosyVoiceSiliconflow</el-menu-item>
|
||||
</el-menu>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="20">
|
||||
<div class="search-operate">
|
||||
<el-input placeholder="请输入音色名称查询" v-model="searchQuery" style="width: 300px; margin-right: 10px;"/>
|
||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button type="primary" plain @click="handleAddVoice">添加音色</el-button>
|
||||
<el-button type="danger" plain @click="handleBatchDelete">批量删除</el-button>
|
||||
</div>
|
||||
|
||||
<el-table :data="filteredTtsModels" style="width: 100%" border stripe header-row-class-name="table-header">
|
||||
<el-table-column label="音色编码" prop="voiceCode" width="150" align="center"></el-table-column>
|
||||
<el-table-column label="音色名称" prop="voiceName" width="180" align="center"></el-table-column>
|
||||
<el-table-column label="语言类型" prop="languageType" width="120" align="center"></el-table-column>
|
||||
<el-table-column label="备注" prop="remark" align="center"></el-table-column>
|
||||
<el-table-column label="操作" width="200" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-button size="mini" type="text" @click="editVoice(scope.row)" style="color: #409EFF; margin-right: 15px;">修改</el-button>
|
||||
<el-button size="mini" type="text" @click="deleteVoice(scope.row)" style="color: #F56C6C;">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="pagination-container">
|
||||
<el-pagination :current-page="currentPage" :page-size="pageSize" :total="total" layout="prev, pager, next" prev-text="<" next-text=">"/>
|
||||
</div>
|
||||
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button @click="handleClose" size="medium">关闭</el-button>
|
||||
<el-button type="primary" @click="handleImportExport" size="medium">导入导出配置</el-button>
|
||||
</div>
|
||||
<EditVoiceDialog :showDialog="editDialogVisible" :voiceData="editVoiceData" @update:showDialog="editDialogVisible = $event" @saveVoice="handleSaveEditedVoice"/>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import EditVoiceDialog from "@/components/EditVoiceDialog.vue";
|
||||
export default {
|
||||
components: { EditVoiceDialog },
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
localVisible: this.visible,
|
||||
activeModel: 'EdgeTTS',
|
||||
searchQuery: '',
|
||||
editDialogVisible: false,
|
||||
editVoiceData: {},
|
||||
ttsModels: [
|
||||
{ voiceCode: 'wawaxiaohe', voiceName: '湾湾小何', languageType: '中文', remark: '' },
|
||||
{ voiceCode: 'wawaxiaohe', voiceName: '湾湾小何', languageType: '中文', remark: '' },
|
||||
{ voiceCode: 'wawaxiaohe', voiceName: '湾湾小何', languageType: '中文', remark: '' },
|
||||
{ voiceCode: 'wawaxiaohe', voiceName: '湾湾小何', languageType: '中文', remark: '' },
|
||||
],
|
||||
currentPage: 1,
|
||||
pageSize: 4,
|
||||
total: 20
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
visible(newVal) {
|
||||
this.localVisible = newVal;
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
filteredTtsModels() {
|
||||
return this.ttsModels.filter(model =>
|
||||
model.voiceName.toLowerCase().includes(this.searchQuery.toLowerCase())
|
||||
);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleClose() {
|
||||
this.localVisible = false;
|
||||
this.$emit('update:visible', false);
|
||||
},
|
||||
handleModelSelect(index) {
|
||||
this.activeModel = index;
|
||||
// 可根据选中模型加载对应数据
|
||||
},
|
||||
handleSearch() {
|
||||
// 搜索
|
||||
},
|
||||
handleAddVoice() {
|
||||
// 添加音色
|
||||
},
|
||||
handleBatchDelete() {
|
||||
// 批量删除
|
||||
},
|
||||
editVoice(voice) {
|
||||
this.editVoiceData = { ...voice };
|
||||
this.editDialogVisible = true;
|
||||
},
|
||||
handleSaveEditedVoice(voiceForm) {
|
||||
const index = this.ttsModels.findIndex(item => item.voiceCode === voiceForm.voiceCode);
|
||||
if (index !== -1) {
|
||||
this.ttsModels.splice(index, 1, voiceForm);
|
||||
}
|
||||
},
|
||||
deleteVoice(voice) {
|
||||
// 删除
|
||||
},
|
||||
handleImportExport() {
|
||||
// 导入导出
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.main-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.model-menu {
|
||||
border-right: 1px solid #ebeef5;
|
||||
height: calc(100vh - 300px);
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
.search-operate {
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
margin: 20px 0;
|
||||
display: flex;
|
||||
justify-content: right;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
text-align: right;
|
||||
padding: 20px 20px 0;
|
||||
border-top: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
::v-deep .table-header th {
|
||||
background-color: #f5f7fa;
|
||||
color: #606266;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
::v-deep .el-table--striped .el-table__body tr.el-table__row--striped td {
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
::v-deep .el-table--border td, ::v-deep .el-table--border th {
|
||||
border-right: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
::v-deep .el-pagination {
|
||||
padding: 10px;
|
||||
background-color: #f5f7fa;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::v-deep .el-pagination .btn-prev, ::v-deep .el-pagination .btn-next {
|
||||
background-color: #fff;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::v-deep .el-pagination .btn-prev:hover, ::v-deep .el-pagination .btn-next:hover {
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
::v-deep .el-pagination .el-pager li {
|
||||
background-color: #fff;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
margin: 0 2px;
|
||||
}
|
||||
|
||||
::v-deep .el-pagination .el-pager li:hover {
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
::v-deep .el-pagination .el-pager li.active {
|
||||
background-color: #409EFF;
|
||||
color: #fff;
|
||||
border-color: #409EFF;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,97 @@
|
||||
<template>
|
||||
<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="width: 40px;height: 40px;border-radius: 50%;background: #5778ff;display: flex;align-items: center;justify-content: center;">
|
||||
<img src="@/assets/login/shield.png" alt="" style="width: 19px;height: 23px; filter: brightness(0) invert(1);" />
|
||||
</div>
|
||||
用户新密码
|
||||
</div>
|
||||
<div style="height: 1px;background: #e8f0ff;" />
|
||||
<div style="margin: 22px 15px;">
|
||||
<div style="font-weight: 400;font-size: 14px;text-align: left;color: #3d4566;">
|
||||
<div style="color: red;display: inline-block;">*</div>
|
||||
用户新密码:
|
||||
</div>
|
||||
<div class="input-46" style="margin-top: 12px;">
|
||||
<el-input
|
||||
v-model="password"
|
||||
type="text"
|
||||
:readonly="true"
|
||||
style="font-weight: bold; color: #333;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex;margin: 15px 15px;gap: 7px;">
|
||||
<div
|
||||
class="dialog-btn"
|
||||
style="background: #e6ebff;border: 1px solid #adbdff;color: #5778ff;"
|
||||
@click="closeDialog"
|
||||
>
|
||||
关闭
|
||||
</div>
|
||||
<div
|
||||
class="dialog-btn"
|
||||
style="background: #5778ff;color: white;"
|
||||
@click="copyPassword"
|
||||
>
|
||||
复制密码
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'ViewPasswordDialog',
|
||||
props: {
|
||||
visible: { type: Boolean, required: true },
|
||||
password: { type: String, default: '' }
|
||||
},
|
||||
methods: {
|
||||
closeDialog() {
|
||||
this.$emit('update:visible', false)
|
||||
},
|
||||
copyPassword() {
|
||||
navigator.clipboard.writeText(this.password)
|
||||
this.$message.success('密码已复制')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.input-46 {
|
||||
border: 1px solid #e4e6ef;
|
||||
background: #f6f8fb;
|
||||
border-radius: 15px;
|
||||
}
|
||||
|
||||
.dialog-btn {
|
||||
cursor: pointer;
|
||||
flex: 1;
|
||||
border-radius: 23px;
|
||||
height: 40px;
|
||||
font-weight: 500;
|
||||
font-size: 12px;
|
||||
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-input__inner {
|
||||
background-color: #f6f8fb !important;
|
||||
cursor: default !important;
|
||||
}
|
||||
</style>
|
||||
@@ -5,8 +5,10 @@ import store from './store'
|
||||
import 'normalize.css/normalize.css' // A modern alternative to CSS resets
|
||||
import ElementUI from 'element-ui';
|
||||
import 'element-ui/lib/theme-chalk/index.css';
|
||||
import './styles/global.scss'
|
||||
|
||||
Vue.use(ElementUI);
|
||||
|
||||
Vue.config.productionTip = false
|
||||
|
||||
new Vue({
|
||||
|
||||
@@ -39,19 +39,73 @@ const routes = [
|
||||
return import('../views/register.vue')
|
||||
}
|
||||
},
|
||||
// 新增设备管理页面路由
|
||||
// 设备管理页面路由
|
||||
{
|
||||
path: '/device-management',
|
||||
name: 'DeviceManagement',
|
||||
component: function () {
|
||||
return import('../views/DeviceManagement.vue')
|
||||
}
|
||||
},
|
||||
// 添加用户管理路由
|
||||
{
|
||||
path: '/user-management',
|
||||
name: 'UserManagement',
|
||||
component: function () {
|
||||
return import('../views/UserManagement.vue')
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/model-config',
|
||||
name: 'ModelConfig',
|
||||
component: function () {
|
||||
return import('../views/ModelConfig.vue')
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/test',
|
||||
name: 'TestServer',
|
||||
component: function () {
|
||||
return import('../views/test.vue')
|
||||
}
|
||||
},
|
||||
|
||||
}
|
||||
]
|
||||
|
||||
const router = new VueRouter({
|
||||
routes
|
||||
})
|
||||
|
||||
// 全局处理重复导航,改为刷新页面
|
||||
const originalPush = VueRouter.prototype.push
|
||||
VueRouter.prototype.push = function push(location) {
|
||||
return originalPush.call(this, location).catch(err => {
|
||||
if (err.name === 'NavigationDuplicated') {
|
||||
// 如果是重复导航,刷新页面
|
||||
window.location.reload()
|
||||
} else {
|
||||
// 其他错误正常抛出
|
||||
throw err
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 需要登录才能访问的路由
|
||||
const protectedRoutes = ['home', 'RoleConfig', 'DeviceManagement', 'UserManagement', 'ModelConfig']
|
||||
|
||||
// 路由守卫
|
||||
router.beforeEach((to, from, next) => {
|
||||
// 检查是否是需要保护的路由
|
||||
if (protectedRoutes.includes(to.name)) {
|
||||
// 从localStorage获取token
|
||||
const token = localStorage.getItem('token')
|
||||
if (!token) {
|
||||
// 未登录,跳转到登录页
|
||||
next({ name: 'login', query: { redirect: to.fullPath } })
|
||||
return
|
||||
}
|
||||
}
|
||||
next()
|
||||
})
|
||||
|
||||
export default router
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import Vue from 'vue'
|
||||
import Vuex from 'vuex'
|
||||
import Constant from '../utils/constant'
|
||||
import {goToPage} from "@/utils";
|
||||
|
||||
Vue.use(Vuex)
|
||||
|
||||
export default new Vuex.Store({
|
||||
state: {
|
||||
token: ''
|
||||
token: '',
|
||||
userInfo: {}, // 添加用户信息存储
|
||||
isSuperAdmin: false // 添加superAdmin状态
|
||||
},
|
||||
getters: {
|
||||
getToken(state) {
|
||||
@@ -14,16 +17,46 @@ export default new Vuex.Store({
|
||||
state.token = localStorage.getItem('token')
|
||||
}
|
||||
return state.token
|
||||
},
|
||||
getUserInfo(state) {
|
||||
return state.userInfo
|
||||
},
|
||||
getIsSuperAdmin(state) {
|
||||
if (localStorage.getItem('isSuperAdmin') === null) {
|
||||
return state.isSuperAdmin
|
||||
}
|
||||
return localStorage.getItem('isSuperAdmin') === 'true'
|
||||
}
|
||||
},
|
||||
mutations: {
|
||||
setToken(state, token) {
|
||||
state.token = token
|
||||
localStorage.token = token
|
||||
localStorage.setItem('token', token)
|
||||
},
|
||||
setUserInfo(state, userInfo) {
|
||||
state.userInfo = userInfo
|
||||
const isSuperAdmin = userInfo.superAdmin === 1
|
||||
state.isSuperAdmin = isSuperAdmin
|
||||
localStorage.setItem('isSuperAdmin', isSuperAdmin)
|
||||
},
|
||||
clearAuth(state) {
|
||||
state.token = ''
|
||||
state.userInfo = {}
|
||||
state.isSuperAdmin = false
|
||||
localStorage.removeItem('token')
|
||||
localStorage.removeItem('isSuperAdmin')
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
// 添加 logout action
|
||||
logout({ commit }) {
|
||||
return new Promise((resolve) => {
|
||||
commit('clearAuth')
|
||||
goToPage(Constant.PAGE.LOGIN,true);
|
||||
window.location.reload(); // 彻底重置状态
|
||||
})
|
||||
}
|
||||
},
|
||||
modules: {
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,14 @@
|
||||
// 覆盖 autofill 样式
|
||||
input:-webkit-autofill,
|
||||
input:-webkit-autofill:hover,
|
||||
input:-webkit-autofill:focus,
|
||||
textarea:-webkit-autofill,
|
||||
textarea:-webkit-autofill:hover,
|
||||
textarea:-webkit-autofill:focus,
|
||||
select:-webkit-autofill,
|
||||
select:-webkit-autofill:hover,
|
||||
select:-webkit-autofill:focus {
|
||||
-webkit-box-shadow: 0 0 0px 1000px transparent inset;
|
||||
transition: background-color 5000s ease-in-out 0s;
|
||||
background-color: transparent;
|
||||
}
|
||||
@@ -54,7 +54,8 @@ export function showDanger(msg) {
|
||||
}
|
||||
Message({
|
||||
message: msg,
|
||||
type: 'error'
|
||||
type: 'error',
|
||||
showClose: true
|
||||
})
|
||||
}
|
||||
|
||||
@@ -68,7 +69,8 @@ export function showWarning(msg) {
|
||||
}
|
||||
Message({
|
||||
message: msg,
|
||||
type: 'warning'
|
||||
type: 'warning',
|
||||
showClose: true
|
||||
});
|
||||
}
|
||||
|
||||
@@ -81,7 +83,8 @@ export function showWarning(msg) {
|
||||
export function showSuccess(msg) {
|
||||
Message({
|
||||
message: msg,
|
||||
type: 'success'
|
||||
type: 'success',
|
||||
showClose: true
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
<template>
|
||||
<div class="welcome">
|
||||
<HeaderBar />
|
||||
<HeaderBar />
|
||||
<el-main style="padding: 20px; display: flex; flex-direction: column;">
|
||||
<div class="table-container">
|
||||
<h3 class="device-list-title">设备列表</h3>
|
||||
<h3 class="device-list-title">设备列表</h3>
|
||||
<el-button type="primary" class="add-device-btn" @click="handleAddDevice">
|
||||
+ 添加设备
|
||||
</el-button>
|
||||
<el-table :data="deviceList" style="width: 100%; margin-top: 20px" border stripe>
|
||||
<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="140"></el-table-column>
|
||||
<el-table-column label="Mac地址" prop="macAddress" width="220"></el-table-column>
|
||||
<el-table-column label="绑定时间" prop="bindTime" width="260"></el-table-column>
|
||||
<el-table-column label="最近对话" prop="lastConversation" width="100"></el-table-column>
|
||||
<el-table-column label="备注" width="220">
|
||||
<template slot-scope="scope">
|
||||
<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>
|
||||
@@ -31,17 +31,27 @@
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="80">
|
||||
<template slot-scope="scope">
|
||||
<el-button size="mini" type="text" @click="handleUnbind(scope.row)" style="color: #ff4949">
|
||||
<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;">
|
||||
<div class="copyright">
|
||||
©2025 xiaozhi-esp32-server
|
||||
</div>
|
||||
<AddDeviceDialog :visible.sync="addDeviceDialogVisible" @added="handleDeviceAdded" />
|
||||
<AddDeviceDialog :visible.sync="addDeviceDialogVisible" :agent-id="currentAgentId" @refresh="fetchBindDevices(currentAgentId)" />
|
||||
</el-main>
|
||||
</div>
|
||||
</template>
|
||||
@@ -55,43 +65,32 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
addDeviceDialogVisible: false,
|
||||
deviceList: [
|
||||
{
|
||||
model: 'xingzhi-cube-0.96oled-wifi',
|
||||
firmwareVersion: '1.4.6',
|
||||
macAddress: 'fc:01:2c:c5:d5:7c',
|
||||
bindTime: '2025-03-10 18:16:21',
|
||||
lastConversation: '6 天前',
|
||||
remark: '',
|
||||
isEdit: false,
|
||||
otaSwitch: false
|
||||
},
|
||||
{
|
||||
model: 'xingzhi-board-1.3tft-ble',
|
||||
firmwareVersion: '2.1.0',
|
||||
macAddress: 'ac:12:3d:e7:f8:9a',
|
||||
bindTime: '2025-03-12 09:30:15',
|
||||
lastConversation: '4 天前',
|
||||
remark: '测试设备',
|
||||
isEdit: false,
|
||||
otaSwitch: true
|
||||
},
|
||||
{
|
||||
model: 'xingzhi-kit-0.91oled-4g',
|
||||
firmwareVersion: '1.8.3',
|
||||
macAddress: 'bc:45:6f:1e:2d:3c',
|
||||
bindTime: '2025-03-15 14:22:08',
|
||||
lastConversation: '2 天前',
|
||||
remark: '生产环境设备',
|
||||
isEdit: false,
|
||||
otaSwitch: 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/device').then(({ default: deviceApi }) => {
|
||||
this.deviceApi = deviceApi;
|
||||
if (agentId) {
|
||||
this.fetchBindDevices(agentId);
|
||||
}
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
handleAddDevice() {
|
||||
// 添加设备逻辑
|
||||
this.addDeviceDialogVisible = true;
|
||||
},
|
||||
startEditRemark(index, row) {
|
||||
@@ -100,15 +99,77 @@ export default {
|
||||
stopEditRemark(index) {
|
||||
this.deviceList[index].isEdit = false;
|
||||
},
|
||||
handleUnbind(device) {
|
||||
// 解绑逻辑
|
||||
console.log('解绑设备', device);
|
||||
handleUnbind(device_id) {
|
||||
if (!this.deviceApi) {
|
||||
this.$message.error('功能模块加载失败');
|
||||
return;
|
||||
}
|
||||
this.$confirm('确认要解绑该设备吗?', '警告', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
this.deviceApi.unbindDevice(device_id, ({ data }) => {
|
||||
if (data.code === 0) {
|
||||
this.$message.success({
|
||||
message: '设备解绑成功',
|
||||
showClose: true
|
||||
});
|
||||
this.fetchBindDevices(this.$route.query.agentId);
|
||||
} else {
|
||||
this.$message.error({
|
||||
message: data.msg || '设备解绑失败',
|
||||
showClose: true
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
handleDeviceAdded(deviceCode) {
|
||||
console.log('添加的智慧体名称:', deviceCode);
|
||||
handleSizeChange(val) {
|
||||
this.pageSize = val;
|
||||
},
|
||||
handleCurrentChange(val) {
|
||||
this.currentPage = val;
|
||||
},
|
||||
fetchBindDevices(agentId) {
|
||||
this.loading = true;
|
||||
import('@/apis/module/device').then(({ default: deviceApi }) => {
|
||||
deviceApi.getAgentBindDevices(agentId, ({ data }) => {
|
||||
this.loading = false;
|
||||
if (data.code === 0) {
|
||||
// 格式化日期并按照绑定时间降序排列
|
||||
this.deviceList = data.data.map(device => {
|
||||
// 格式化绑定时间
|
||||
const bindDate = new Date(device.createDate);
|
||||
const formattedBindTime = `${bindDate.getFullYear()}-${(bindDate.getMonth()+1).toString().padStart(2, '0')}-${bindDate.getDate().toString().padStart(2, '0')} ${bindDate.getHours().toString().padStart(2, '0')}:${bindDate.getMinutes().toString().padStart(2, '0')}:${bindDate.getSeconds().toString().padStart(2, '0')}`;
|
||||
return {
|
||||
device_id: device.id,
|
||||
model: device.board,
|
||||
firmwareVersion: device.appVersion,
|
||||
macAddress: device.macAddress,
|
||||
bindTime: formattedBindTime, // 使用格式化后的时间
|
||||
lastConversation: device.lastConnectedAt,
|
||||
remark: device.alias,
|
||||
isEdit: false,
|
||||
otaSwitch: device.autoUpdate === 1,
|
||||
// 添加原始时间用于排序
|
||||
rawBindTime: new Date(device.createDate).getTime()
|
||||
};
|
||||
})
|
||||
// 按照绑定时间降序排序
|
||||
.sort((a, b) => a.rawBindTime - b.rawBindTime);
|
||||
} else {
|
||||
this.$message.error(data.msg || '获取设备列表失败');
|
||||
}
|
||||
});
|
||||
}).catch(error => {
|
||||
console.error('模块加载失败:', error);
|
||||
this.$message.error('功能模块加载失败');
|
||||
});
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -166,4 +227,8 @@ export default {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
</style>
|
||||
.pagination {
|
||||
margin-top: 20px;
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,702 @@
|
||||
<template>
|
||||
<div class="welcome">
|
||||
<HeaderBar />
|
||||
|
||||
<div class="operation-bar">
|
||||
<h2 class="page-title">模型配置</h2>
|
||||
<div class="right-operations">
|
||||
<el-button plain size="small" @click="handleImport" style="background: #7b9de5; color: white;">
|
||||
<img loading="lazy" alt="" src="@/assets/model/inner_conf.png">
|
||||
导入配置
|
||||
</el-button>
|
||||
<el-button plain size="small" @click="handleExport" style="background: #71c9d1; color: white;">
|
||||
<img loading="lazy" alt="" src="@/assets/model/output_conf.png">
|
||||
导出配置
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 主体内容 -->
|
||||
<div class="main-wrapper">
|
||||
<div class="content-panel">
|
||||
<!-- 左侧导航 -->
|
||||
<el-menu :default-active="activeTab" class="nav-panel" @select="handleMenuSelect"
|
||||
style="background-size: cover; background-position: center;">
|
||||
<el-menu-item index="vad">
|
||||
<span class="menu-text">语言活动检测</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="asr">
|
||||
<span class="menu-text">语音识别</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="llm">
|
||||
<span class="menu-text">大语言模型</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="intent">
|
||||
<span class="menu-text">意图识别</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="tts">
|
||||
<span class="menu-text">语音合成</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="memory">
|
||||
<span class="menu-text">记忆</span>
|
||||
</el-menu-item>
|
||||
</el-menu>
|
||||
|
||||
<!-- 右侧内容 -->
|
||||
<div class="content-area">
|
||||
<div class="title-bar">
|
||||
<div class="title-wrapper">
|
||||
<h2 class="model-title">大语言模型(LLM)</h2>
|
||||
<el-button type="primary" size="small" @click="addModel" class="add-btn">
|
||||
添加
|
||||
</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>
|
||||
|
||||
<el-table ref="modelTable" style="width: 100%" :header-cell-style="{background: 'transparent'}" :data="modelList" class="data-table" header-row-class-name="table-header" :header-cell-class-name="headerCellClassName" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55" align="center"></el-table-column>
|
||||
<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">
|
||||
<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 v-if="activeTab === 'tts'" label="音色管理" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-button type="text" size="mini" @click="ttsDialogVisible = true" class="voice-management-btn">
|
||||
音色管理
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="center" width="150px">
|
||||
<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" style="width: 75px; background: #606ff3">{{ isAllSelected ? '取消全选' : '全选' }}</el-button>
|
||||
<el-button size="mini" type="danger" icon="el-icon-delete" @click="batchDelete">
|
||||
删除
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="custom-pagination">
|
||||
<button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">首页</button>
|
||||
<button class="pagination-btn" :disabled="currentPage === 1" @click="goPrev">上一页</button>
|
||||
|
||||
<button v-for="page in visiblePages" :key="page" class="pagination-btn" :class="{ active: page === currentPage }" @click="goToPage(page)">
|
||||
{{ page }}
|
||||
</button>
|
||||
|
||||
<button class="pagination-btn" :disabled="currentPage === pageCount" @click="goNext">下一页</button>
|
||||
<span class="total-text">共{{ total }}条记录</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ModelEditDialog :visible.sync="editDialogVisible" :modelData="editModelData" @save="handleModelSave"/>
|
||||
<TtsModel :visible.sync="ttsDialogVisible" />
|
||||
<AddModelDialog :modelType="activeTab" :visible.sync="addDialogVisible" @confirm="handleAddConfirm"/>
|
||||
</div>
|
||||
|
||||
<div class="copyright">
|
||||
©2025 xiaozhi-esp32-server
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import HeaderBar from "@/components/HeaderBar.vue";
|
||||
import ModelEditDialog from "@/components/ModelEditDialog.vue";
|
||||
import TtsModel from "@/components/TtsModel.vue";
|
||||
import AddModelDialog from "@/components/AddModelDialog.vue";
|
||||
|
||||
export default {
|
||||
components: { HeaderBar, ModelEditDialog, TtsModel, AddModelDialog },
|
||||
data() {
|
||||
return {
|
||||
addDialogVisible: false,
|
||||
activeTab: 'llm',
|
||||
search: '',
|
||||
editDialogVisible: false,
|
||||
editModelData: {},
|
||||
ttsDialogVisible: false,
|
||||
modelList: [
|
||||
{ code: 'DeepSeek', candidateName: '深度求索', isApplied: true, supplier: '硅基流动' },
|
||||
{ code: 'SmartAssist', candidateName: '智能助手', isApplied: false, supplier: '智脑科技' },
|
||||
{ code: 'CogEngine', candidateName: '认知引擎', isApplied: true, supplier: '云智科技' },
|
||||
],
|
||||
currentPage: 1,
|
||||
pageSize: 4,
|
||||
total: 20,
|
||||
selectedModels: [],
|
||||
isAllSelected: false
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
pageCount() {
|
||||
return Math.ceil(this.total / this.pageSize);
|
||||
},
|
||||
visiblePages() {
|
||||
const pages = [];
|
||||
const maxVisible = 3;
|
||||
let start = Math.max(1, this.currentPage - 1);
|
||||
let end = Math.min(this.pageCount, start + maxVisible - 1);
|
||||
|
||||
if (end - start + 1 < maxVisible) {
|
||||
start = Math.max(1, end - maxVisible + 1);
|
||||
}
|
||||
|
||||
for (let i = start; i <= end; i++) {
|
||||
pages.push(i);
|
||||
}
|
||||
return pages;
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
headerCellClassName({ column, columnIndex }) {
|
||||
if (columnIndex === 0) {
|
||||
return 'custom-selection-header';
|
||||
}
|
||||
return '';
|
||||
},
|
||||
handleMenuSelect(index) {
|
||||
this.activeTab = index;
|
||||
},
|
||||
handleSearch() {
|
||||
console.log('查询:', this.search);
|
||||
},
|
||||
batchDelete() {
|
||||
if (this.selectedModels.length === 0) {
|
||||
this.$message.warning('请先选择要删除的模型');
|
||||
return;
|
||||
}
|
||||
this.$confirm('确定要删除选中的模型吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
const selectedIds = this.selectedModels.map(model => model.code);
|
||||
this.modelList = this.modelList.filter(model => !selectedIds.includes(model.code));
|
||||
this.$message.success('删除成功');
|
||||
this.selectedModels = [];
|
||||
this.isAllSelected = false;
|
||||
}).catch(() => {
|
||||
this.$message.info('已取消删除');
|
||||
});
|
||||
},
|
||||
addModel() {
|
||||
this.addDialogVisible = true;
|
||||
},
|
||||
editModel(model) {
|
||||
this.editModelData = {
|
||||
code: model.code,
|
||||
name: model.candidateName,
|
||||
supplier: model.supplier,
|
||||
};
|
||||
this.editDialogVisible = true;
|
||||
},
|
||||
deleteModel(model) {
|
||||
this.$confirm(`确定要删除模型 ${model.candidateName} 吗?`, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
this.modelList = this.modelList.filter(item => item.code !== model.code);
|
||||
this.$message.success('删除成功');
|
||||
}).catch(() => {
|
||||
this.$message.info('已取消删除');
|
||||
});
|
||||
},
|
||||
handleCurrentChange(page) {
|
||||
this.currentPage = page;
|
||||
this.$refs.modelTable.clearSelection();
|
||||
console.log('当前页码:', page);
|
||||
},
|
||||
handleImport() {
|
||||
console.log('导入配置');
|
||||
},
|
||||
handleExport() {
|
||||
console.log('导出配置');
|
||||
},
|
||||
handleModelSave(formData) {
|
||||
console.log('保存的模型数据:', formData);
|
||||
},
|
||||
selectAll() {
|
||||
if (this.isAllSelected) {
|
||||
this.$refs.modelTable.clearSelection();
|
||||
} else {
|
||||
this.$refs.modelTable.toggleAllSelection();
|
||||
}
|
||||
},
|
||||
handleSelectionChange(val) {
|
||||
this.selectedModels = val;
|
||||
this.isAllSelected = val.length === this.modelList.length;
|
||||
if (val.length === 0) {
|
||||
this.isAllSelected = false;
|
||||
}
|
||||
},
|
||||
handleAddConfirm(newModel) {
|
||||
console.log('新增模型数据:', newModel);
|
||||
},
|
||||
|
||||
// 分页器
|
||||
goFirst() {
|
||||
this.currentPage = 1;
|
||||
this.loadData();
|
||||
},
|
||||
goPrev() {
|
||||
if (this.currentPage > 1) {
|
||||
this.currentPage--;
|
||||
this.loadData();
|
||||
}
|
||||
},
|
||||
goNext() {
|
||||
if (this.currentPage < this.pageCount) {
|
||||
this.currentPage++;
|
||||
this.loadData();
|
||||
}
|
||||
},
|
||||
goToPage(page) {
|
||||
this.currentPage = page;
|
||||
this.loadData();
|
||||
},
|
||||
loadData() {
|
||||
console.log('加载数据,当前页:', this.currentPage);
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
::v-deep .el-table tr{
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.welcome {
|
||||
min-width: 900px;
|
||||
min-height: 506px;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
position: relative;
|
||||
flex-direction: column;
|
||||
background-size: cover;
|
||||
background: linear-gradient(to bottom right, #dce8ff, #e4eeff, #e6cbfd) center;
|
||||
-webkit-background-size: cover;
|
||||
-o-background-size: cover;
|
||||
}
|
||||
|
||||
.main-wrapper {
|
||||
margin: 5px 60px;
|
||||
border-radius: 15px;
|
||||
min-height: 600px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||
position: relative;
|
||||
background: rgba(237,242,255,0.5);
|
||||
}
|
||||
|
||||
.operation-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 24px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 24px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.right-operations {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.content-panel {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
border-radius: 15px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.nav-panel {
|
||||
min-width: 242px;
|
||||
height: 100%;
|
||||
border-right: 1px solid #ebeef5;
|
||||
background:
|
||||
linear-gradient(
|
||||
120deg,
|
||||
rgba(107, 140, 255, 0.3) 0%,
|
||||
rgba(169, 102, 255, 0.3) 25%,
|
||||
transparent 60%
|
||||
),
|
||||
url("../assets/model/model.png") no-repeat center / cover;
|
||||
padding: 16px 0;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.nav-panel .el-menu-item {
|
||||
height: 50px;
|
||||
line-height: 50px;
|
||||
border-radius: 4px;
|
||||
transition: all 0.3s;
|
||||
display: flex !important;
|
||||
justify-content: flex-end;
|
||||
padding-right: 12px !important;
|
||||
width: fit-content;
|
||||
margin: 8px 0px 8px auto;
|
||||
min-width: unset;
|
||||
}
|
||||
|
||||
.nav-panel .el-menu-item.is-active {
|
||||
background: #e9f0ff;
|
||||
color: #0ba6f4 !important;
|
||||
position: relative;
|
||||
padding-left: 40px !important;
|
||||
}
|
||||
|
||||
.nav-panel .el-menu-item.is-active::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 15px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
background: #409EFF;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 4px rgba(64, 158, 255, 0.5);
|
||||
}
|
||||
|
||||
.menu-text {
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
text-align: right;
|
||||
width: 100%;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.content-area {
|
||||
flex: 1;
|
||||
padding: 24px;
|
||||
height: 100%;
|
||||
min-width: 600px;
|
||||
overflow-x: auto;
|
||||
background-color: white;
|
||||
|
||||
}
|
||||
|
||||
.title-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.model-title {
|
||||
font-size: 18px;
|
||||
color: #303133;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.action-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.search-group {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 240px;
|
||||
}
|
||||
|
||||
::v-deep .search-input .el-input__inner::placeholder {
|
||||
color: black;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
::v-deep .search-input .el-input__inner {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.search-btn {
|
||||
background: linear-gradient(135deg, #6B8CFF, #A966FF);
|
||||
border: none;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.data-table {
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.data-table /deep/ .el-table__row {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.table-header th {
|
||||
background-color: transparent !important;
|
||||
color: #606266;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.table-footer {
|
||||
margin-top: 24px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.batch-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.copyright {
|
||||
text-align: center;
|
||||
color: #979db1;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
margin-top: auto;
|
||||
padding: 30px 0 20px;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.add-btn {
|
||||
background: #cce5f9;
|
||||
width: 75px;
|
||||
border: none;
|
||||
color: black;
|
||||
padding: 8px 16px;
|
||||
}
|
||||
|
||||
.title-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.batch-actions .el-button:first-child {
|
||||
background: linear-gradient(135deg, #409EFF, #6B8CFF);
|
||||
border: none;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.batch-actions .el-button:first-child:hover {
|
||||
background: linear-gradient(135deg, #3A8EE6, #5A7CFF);
|
||||
}
|
||||
|
||||
.el-table th /deep/ .el-table__cell {
|
||||
overflow: hidden;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
user-select: none;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
::v-deep .el-table .custom-selection-header .cell .el-checkbox__inner {
|
||||
display: none !important; /* 使表头复选框不可见 */
|
||||
}
|
||||
|
||||
::v-deep .el-table .custom-selection-header .cell::before {
|
||||
content: '选择';
|
||||
display: block;
|
||||
text-align: center;
|
||||
line-height: 0;
|
||||
color: black;
|
||||
margin-top: 23px;
|
||||
}
|
||||
|
||||
::v-deep .el-table__body .el-checkbox__inner {
|
||||
display: inline-block !important;
|
||||
background: #e6edfa;
|
||||
}
|
||||
|
||||
::v-deep .el-table thead th:not(:first-child) .cell {
|
||||
color: #303133 !important;
|
||||
}
|
||||
|
||||
::v-deep .nav-panel .el-menu-item.is-active .menu-text {
|
||||
color: #409EFF !important;
|
||||
}
|
||||
|
||||
::v-deep .data-table {
|
||||
&.el-table::before,
|
||||
&.el-table::after,
|
||||
&.el-table__inner-wrapper::before {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
::v-deep .data-table .el-table__header-wrapper {
|
||||
border-bottom: 1px solid rgb(224,227,237);
|
||||
}
|
||||
|
||||
::v-deep .data-table .el-table__body td {
|
||||
border-bottom: 1px solid rgb(224,227,237) !important;
|
||||
}
|
||||
|
||||
.el-button img{
|
||||
height: 1em;
|
||||
vertical-align: middle;
|
||||
padding-right: 2px;
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
|
||||
::v-deep .el-checkbox__inner {
|
||||
border-color: #cfcfcf !important;
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
::v-deep .el-checkbox__input.is-checked .el-checkbox__inner {
|
||||
background-color: #409EFF !important;
|
||||
border-color: #409EFF !important;
|
||||
}
|
||||
|
||||
.voice-management-btn {
|
||||
background: #9db3ea;
|
||||
color: white;
|
||||
min-width: 68px;
|
||||
line-height: 14px;
|
||||
white-space: nowrap;
|
||||
transition: all 0.3s;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.voice-management-btn:hover {
|
||||
background: #8aa2e0; /* 悬停时颜色加深 */
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
::v-deep .el-table .el-table-column--selection .cell {
|
||||
padding-left: 15px !important;
|
||||
}
|
||||
|
||||
::v-deep .el-table .el-table__fixed-right .cell {
|
||||
padding-right: 15px !important;
|
||||
}
|
||||
|
||||
.edit-btn, .delete-btn {
|
||||
margin: 0 8px;
|
||||
color: #7079aa !important;
|
||||
}
|
||||
|
||||
::v-deep .el-table .cell {
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
/* 分页器 */
|
||||
.custom-pagination {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 15px;
|
||||
|
||||
/* 导航按钮样式 (首页、上一页、下一页) */
|
||||
|
||||
.pagination-btn:first-child,
|
||||
.pagination-btn:nth-child(2),
|
||||
.pagination-btn:nth-last-child(2) {
|
||||
min-width: 60px;
|
||||
height: 32px;
|
||||
padding: 0 12px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #e4e7ed;
|
||||
background: #DEE7FF;
|
||||
color: #606266;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
background: #d7dce6;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
/* 数字按钮样式 */
|
||||
|
||||
.pagination-btn:not(:first-child):not(:nth-child(2)):not(:nth-last-child(2)) {
|
||||
min-width: 28px;
|
||||
height: 32px;
|
||||
padding: 0;
|
||||
border-radius: 4px;
|
||||
border: 1px solid transparent;
|
||||
background: transparent;
|
||||
color: #606266;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
background: rgba(245, 247, 250, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
.pagination-btn.active {
|
||||
background: #5f70f3 !important;
|
||||
color: #ffffff !important;
|
||||
border-color: #5f70f3 !important;
|
||||
|
||||
&:hover {
|
||||
background: #6d7cf5 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.total-text {
|
||||
color: #909399;
|
||||
font-size: 14px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,550 @@
|
||||
<template>
|
||||
<div class="welcome">
|
||||
<HeaderBar />
|
||||
|
||||
<div class="operation-bar">
|
||||
<h2 class="page-title">用户管理</h2>
|
||||
<div class="right-operations">
|
||||
<el-input placeholder="请输入手机号码查询" v-model="searchPhone" class="search-input" @keyup.enter.native="handleSearch"/>
|
||||
<el-button class="btn-search" @click="handleSearch">搜索</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="main-wrapper">
|
||||
<div class="content-panel">
|
||||
<div class="content-area">
|
||||
<el-card class="user-card" shadow="never">
|
||||
<el-table ref="userTable" :data="userList" class="transparent-table" :header-cell-class-name="headerCellClassName">
|
||||
<el-table-column label="选择" type="selection" align="center" width="120"></el-table-column>
|
||||
<el-table-column label="用户Id" prop="userid" align="center"></el-table-column>
|
||||
<el-table-column label="手机号码" prop="mobile" align="center"></el-table-column>
|
||||
<el-table-column label="设备数量" prop="deviceCount" align="center"></el-table-column>
|
||||
<el-table-column label="状态" prop="status" align="center"></el-table-column>
|
||||
<el-table-column label="操作" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-button size="mini" type="text" @click="resetPassword(scope.row)" style="color: #989fdd">重置密码</el-button>
|
||||
<el-button size="mini" type="text"
|
||||
v-if="scope.row.status === '正常'"
|
||||
@click="disableUser(scope.row)">禁用账户</el-button>
|
||||
<el-button size="mini" type="text"
|
||||
v-if="scope.row.status === '禁用'"
|
||||
@click="restoreUser(scope.row)">恢复账号</el-button>
|
||||
<el-button size="mini" type="text" @click="deleteUser(scope.row)" style="color: #989fdd">删除用户</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="table_bottom">
|
||||
<div class="ctrl_btn">
|
||||
<el-button size="mini" type="primary" class="select-all-btn" @click="handleSelectAll">全选</el-button>
|
||||
<el-button size="mini" type="success" icon="el-icon-circle-check" @click="batchEnable">启用</el-button>
|
||||
<el-button size="mini" type="warning" @click="batchDisable"><i class="el-icon-remove-outline rotated-icon"></i>禁用</el-button>
|
||||
<el-button size="mini" type="danger" icon="el-icon-delete" @click="batchDelete">删除</el-button>
|
||||
</div>
|
||||
<div class="custom-pagination">
|
||||
<button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">首页</button>
|
||||
<button class="pagination-btn" :disabled="currentPage === 1" @click="goPrev">上一页</button>
|
||||
|
||||
<button v-for="page in visiblePages" :key="page" class="pagination-btn" :class="{ active: page === currentPage }" @click="goToPage(page)">
|
||||
{{ page }}
|
||||
</button>
|
||||
|
||||
<button class="pagination-btn" :disabled="currentPage === pageCount" @click="goNext">下一页</button>
|
||||
<span class="total-text">共{{ total }}条记录</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="copyright">
|
||||
©2025 xiaozhi-esp32-server
|
||||
</div>
|
||||
<view-password-dialog :visible.sync="showViewPassword" :password="currentPassword"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import HeaderBar from "@/components/HeaderBar.vue";
|
||||
import adminApi from '@/apis/module/admin';
|
||||
import ViewPasswordDialog from '@/components/ViewPasswordDialog.vue'
|
||||
|
||||
export default {
|
||||
components: { HeaderBar, ViewPasswordDialog },
|
||||
data() {
|
||||
return {
|
||||
showViewPassword: false,
|
||||
currentPassword: '',
|
||||
searchPhone: '',
|
||||
userList: [],
|
||||
currentPage: 1,
|
||||
pageSize: 5,
|
||||
total: 0
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.fetchUsers();
|
||||
},
|
||||
computed: {
|
||||
pageCount() {
|
||||
return Math.ceil(this.total / this.pageSize);
|
||||
},
|
||||
visiblePages() {
|
||||
const pages = [];
|
||||
const maxVisible = 3;
|
||||
let start = Math.max(1, this.currentPage - 1);
|
||||
let end = Math.min(this.pageCount, start + maxVisible - 1);
|
||||
|
||||
if (end - start + 1 < maxVisible) {
|
||||
start = Math.max(1, end - maxVisible + 1);
|
||||
}
|
||||
|
||||
for (let i = start; i <= end; i++) {
|
||||
pages.push(i);
|
||||
}
|
||||
return pages;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
fetchUsers() {
|
||||
adminApi.getUserList({
|
||||
page: this.currentPage,
|
||||
limit: this.pageSize,
|
||||
mobile: this.searchPhone
|
||||
}, ({ data }) => {
|
||||
if (data.code === 0) {
|
||||
this.userList = data.data.list.map(user => ({
|
||||
...user,
|
||||
status: user.status === '1' ? '正常' : '禁用'
|
||||
}));
|
||||
this.total = data.data.total;
|
||||
}
|
||||
});
|
||||
},
|
||||
handleSearch() {
|
||||
this.currentPage = 1;
|
||||
this.fetchUsers();
|
||||
},
|
||||
handleSelectAll() {
|
||||
this.$refs.userTable.toggleAllSelection();
|
||||
},
|
||||
batchDelete() {
|
||||
const selectedUsers = this.$refs.userTable.selection;
|
||||
if (selectedUsers.length === 0) {
|
||||
this.$message.warning('请先选择需要删除的用户');
|
||||
return;
|
||||
}
|
||||
|
||||
this.$confirm(`确定要删除选中的${selectedUsers.length}个用户吗?`, '警告', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
const loading = this.$loading({
|
||||
lock: true,
|
||||
text: '正在删除中...',
|
||||
spinner: 'el-icon-loading',
|
||||
background: 'rgba(0, 0, 0, 0.7)'
|
||||
});
|
||||
|
||||
try {
|
||||
const results = await Promise.all(
|
||||
selectedUsers.map(user => {
|
||||
return new Promise((resolve) => {
|
||||
adminApi.deleteUser(user.userid, ({data}) => {
|
||||
if (data.code === 0) {
|
||||
resolve({success: true, userid: user.userid});
|
||||
} else {
|
||||
resolve({success: false, userid: user.userid, msg: data.msg});
|
||||
}
|
||||
});
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
const successCount = results.filter(r => r.success).length;
|
||||
const failCount = results.length - successCount;
|
||||
|
||||
if (failCount === 0) {
|
||||
this.$message.success(`成功删除${successCount}个用户`);
|
||||
} else if (successCount === 0) {
|
||||
this.$message.error(`删除失败,请重试`);
|
||||
} else {
|
||||
this.$message.warning(`成功删除${successCount}个用户,${failCount}个删除失败`);
|
||||
}
|
||||
|
||||
this.fetchUsers();
|
||||
} catch (error) {
|
||||
this.$message.error('删除过程中发生错误');
|
||||
} finally {
|
||||
loading.close();
|
||||
}
|
||||
}).catch(() => {
|
||||
this.$message.info('已取消删除');
|
||||
});
|
||||
},
|
||||
batchEnable() {
|
||||
const selectedUsers = this.$refs.userTable.selection;
|
||||
if (selectedUsers.length === 0) {
|
||||
this.$message.warning('请先选择需要启用的用户');
|
||||
return;
|
||||
}
|
||||
selectedUsers.forEach(user => {
|
||||
user.status = '正常';
|
||||
});
|
||||
this.$message.success('启用操作成功');
|
||||
},
|
||||
batchDisable() {
|
||||
this.userList.forEach(user => {
|
||||
user.status = '禁用';
|
||||
});
|
||||
this.$message.success('状态已更新为禁用');
|
||||
},
|
||||
resetPassword(row) {
|
||||
this.$confirm('重置后将会生成新密码,是否继续?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消'
|
||||
}).then(() => {
|
||||
adminApi.resetUserPassword(row.userid, ({ data }) => {
|
||||
if (data.code === 0) {
|
||||
this.currentPassword = data.data
|
||||
this.showViewPassword = true
|
||||
this.$message.success('密码已重置,请通知用户使用新密码登录')
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
disableUser(row) {
|
||||
row.status = '禁用';
|
||||
console.log('禁用用户:', row);
|
||||
},
|
||||
restoreUser(row) {
|
||||
row.status = '正常';
|
||||
console.log('恢复用户:', row);
|
||||
},
|
||||
deleteUser(row) {
|
||||
this.$confirm('确定要删除该用户吗?', '警告', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
adminApi.deleteUser(row.userid, ({data}) => {
|
||||
if (data.code === 0) {
|
||||
this.$message.success('删除成功')
|
||||
this.fetchUsers()
|
||||
} else {
|
||||
this.$message.error(data.msg || '删除失败')
|
||||
}
|
||||
})
|
||||
}).catch(() => {})
|
||||
},
|
||||
headerCellClassName({columnIndex}) {
|
||||
if (columnIndex === 0) {
|
||||
return 'custom-selection-header'
|
||||
}
|
||||
return ''
|
||||
},
|
||||
goFirst() {
|
||||
this.currentPage = 1;
|
||||
this.fetchUsers();
|
||||
},
|
||||
goPrev() {
|
||||
if (this.currentPage > 1) {
|
||||
this.currentPage--;
|
||||
this.fetchUsers();
|
||||
}
|
||||
},
|
||||
goNext() {
|
||||
if (this.currentPage < this.pageCount) {
|
||||
this.currentPage++;
|
||||
this.fetchUsers();
|
||||
}
|
||||
},
|
||||
goToPage(page) {
|
||||
this.currentPage = page;
|
||||
this.fetchUsers();
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.welcome {
|
||||
min-width: 900px;
|
||||
min-height: 506px;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
position: relative;
|
||||
flex-direction: column;
|
||||
background-size: cover;
|
||||
background: linear-gradient(to bottom right, #dce8ff, #e4eeff, #e6cbfd) center;
|
||||
-webkit-background-size: cover;
|
||||
-o-background-size: cover;
|
||||
}
|
||||
|
||||
.main-wrapper {
|
||||
margin: 5px 22px;
|
||||
border-radius: 15px;
|
||||
min-height: 600px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||
position: relative;
|
||||
background: rgba(237,242,255,0.5);
|
||||
}
|
||||
|
||||
.operation-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 24px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 24px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.right-operations {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 240px;
|
||||
}
|
||||
|
||||
.btn-search {
|
||||
background: linear-gradient(135deg, #6B8CFF, #A966FF);
|
||||
border: none;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.content-panel {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
border-radius: 15px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.content-area {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
min-width: 600px;
|
||||
overflow-x: auto;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.user-card {
|
||||
background: white;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.table_bottom {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.ctrl_btn {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding-left: 26px;
|
||||
.el-button {
|
||||
min-width: 72px;
|
||||
height: 32px;
|
||||
padding: 7px 12px 7px 10px;
|
||||
font-size: 12px;
|
||||
border-radius: 4px;
|
||||
line-height: 1;
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
.el-button--primary {
|
||||
background: #5f70f3;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.el-button--success {
|
||||
background: #5bc98c;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.el-button--warning {
|
||||
background: #f6d075;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.el-button--danger {
|
||||
background: #fd5b63;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.rotated-icon {
|
||||
display: inline-block;
|
||||
transform: rotate(45deg);
|
||||
margin-right: 4px;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.copyright {
|
||||
text-align: center;
|
||||
color: #979db1;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
margin-top: auto;
|
||||
padding: 30px 0 20px;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.custom-pagination {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 15px;
|
||||
|
||||
.pagination-btn:first-child,
|
||||
.pagination-btn:nth-child(2),
|
||||
.pagination-btn:nth-last-child(2) {
|
||||
min-width: 60px;
|
||||
height: 32px;
|
||||
padding: 0 12px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #e4e7ed;
|
||||
background: #DEE7FF;
|
||||
color: #606266;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
background: #d7dce6;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.pagination-btn:not(:first-child):not(:nth-child(2)):not(:nth-last-child(2)) {
|
||||
min-width: 28px;
|
||||
height: 32px;
|
||||
padding: 0;
|
||||
border-radius: 4px;
|
||||
border: 1px solid transparent;
|
||||
background: transparent;
|
||||
color: #606266;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
background: rgba(245, 247, 250, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
.pagination-btn.active {
|
||||
background: #5f70f3 !important;
|
||||
color: #ffffff !important;
|
||||
border-color: #5f70f3 !important;
|
||||
|
||||
&:hover {
|
||||
background: #6d7cf5 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.total-text {
|
||||
color: #909399;
|
||||
font-size: 14px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.transparent-table) {
|
||||
background: white;
|
||||
.el-table__header th {
|
||||
background: white !important;
|
||||
color: black;
|
||||
}
|
||||
|
||||
&::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.el-table__body tr {
|
||||
background-color: white;
|
||||
td {
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.04);
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.custom-selection-header) {
|
||||
.el-checkbox {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '选择';
|
||||
display: inline-block;
|
||||
color: black;
|
||||
font-weight: bold;
|
||||
padding-bottom: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-checkbox__inner) {
|
||||
background-color: #eeeeee !important;
|
||||
border-color: #cccccc !important;
|
||||
}
|
||||
|
||||
:deep(.el-checkbox__inner:hover) {
|
||||
border-color: #cccccc !important;
|
||||
}
|
||||
|
||||
:deep(.el-checkbox__input.is-checked .el-checkbox__inner) {
|
||||
background-color: #5f70f3 !important;
|
||||
border-color: #5f70f3 !important;
|
||||
}
|
||||
|
||||
@media (min-width: 1144px) {
|
||||
.table_bottom {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 40px;
|
||||
}
|
||||
:deep(.transparent-table) {
|
||||
.el-table__body tr {
|
||||
td {
|
||||
padding-top: 16px;
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
& + tr {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -1,9 +1,11 @@
|
||||
|
||||
.welcome {
|
||||
min-width: 1200px;
|
||||
min-height: 675px;
|
||||
height: 100vh;
|
||||
background-image: url("@/assets/login/background.png");
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(145deg, #f5f8fd, #6baaff, #9ebbfc, #f5f8fd);
|
||||
background-size: cover;
|
||||
/* 确保背景图像覆盖整个元素 */
|
||||
background-position: center;
|
||||
@@ -130,3 +132,13 @@
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.login-person {
|
||||
width: 500px;
|
||||
color: #fff;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 25%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 1;
|
||||
}
|
||||
@@ -38,7 +38,7 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div style="font-size: 12px;font-weight: 400;margin-top: auto;padding-top: 30px;color: #979db1;">
|
||||
<div class="copyright">
|
||||
©2025 xiaozhi-esp32-server
|
||||
</div>
|
||||
<AddWisdomBodyDialog :visible.sync="addDeviceDialogVisible" @confirm="handleWisdomBodyAdded" />
|
||||
@@ -109,10 +109,16 @@ export default {
|
||||
import('@/apis/module/agent').then(({ default: userApi }) => {
|
||||
userApi.deleteAgent(agentId, (res) => {
|
||||
if (res.data.code === 0) {
|
||||
this.$message.success('删除成功');
|
||||
this.$message.success({
|
||||
message: '删除成功',
|
||||
showClose: true
|
||||
});
|
||||
this.fetchAgentList(); // 刷新列表
|
||||
} else {
|
||||
this.$message.error(res.data.msg || '删除失败');
|
||||
this.$message.error({
|
||||
message: res.data.msg || '删除失败',
|
||||
showClose: true
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,15 +4,18 @@
|
||||
<el-header>
|
||||
<div
|
||||
style="display: flex;align-items: center;margin-top: 15px;margin-left: 10px;gap: 10px;">
|
||||
<img src="@/assets/xiaozhi-logo.png" alt="" style="width: 45px;height: 45px;"/>
|
||||
<img src="@/assets/xiaozhi-ai.png" alt="" style="width: 70px;height: 13px;"/>
|
||||
<img loading="lazy" alt="" src="@/assets/xiaozhi-logo.png" style="width: 45px;height: 45px;"/>
|
||||
<img loading="lazy" alt="" src="@/assets/xiaozhi-ai.png" style="width: 70px;height: 13px;"/>
|
||||
</div>
|
||||
</el-header>
|
||||
<div class="login-person">
|
||||
<img loading="lazy" alt="" src="@/assets/login/login-person.png" style="width: 100%;"/>
|
||||
</div>
|
||||
<el-main style="position: relative;">
|
||||
<div class="login-box">
|
||||
<div class="login-box" @keyup.enter="login">
|
||||
<div
|
||||
style="display: flex;align-items: center;gap: 20px;margin-bottom: 39px;padding: 0 30px;">
|
||||
<img src="@/assets/login/hi.png" alt="" style="width: 34px;height: 34px;"/>
|
||||
<img loading="lazy" alt="" src="@/assets/login/hi.png" style="width: 34px;height: 34px;"/>
|
||||
<div class="login-text">登录</div>
|
||||
<div class="login-welcome">
|
||||
WELCOME TO LOGIN
|
||||
@@ -20,19 +23,19 @@
|
||||
</div>
|
||||
<div style="padding: 0 30px;">
|
||||
<div class="input-box">
|
||||
<img src="@/assets/login/username.png" alt="" class="input-icon"/>
|
||||
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/username.png"/>
|
||||
<el-input v-model="form.username" placeholder="请输入用户名"/>
|
||||
</div>
|
||||
<div class="input-box">
|
||||
<img src="@/assets/login/password.png" alt="" class="input-icon"/>
|
||||
<el-input v-model="form.password" type="password" placeholder="请输入密码"/>
|
||||
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/password.png"/>
|
||||
<el-input v-model="form.password" placeholder="请输入密码" type="password"/>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; margin-top: 20px; width: 100%; gap: 10px;">
|
||||
<div class="input-box" style="width: calc(100% - 130px); margin-top: 0;">
|
||||
<img src="@/assets/login/shield.png" alt="" class="input-icon"/>
|
||||
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/shield.png"/>
|
||||
<el-input v-model="form.captcha" placeholder="请输入验证码" style="flex: 1;"/>
|
||||
</div>
|
||||
<img v-if="captchaUrl"
|
||||
<img loading="lazy" v-if="captchaUrl"
|
||||
:src="captchaUrl"
|
||||
alt="验证码"
|
||||
style="width: 150px; height: 40px; cursor: pointer;"
|
||||
@@ -54,7 +57,7 @@
|
||||
</div>
|
||||
</el-main>
|
||||
<el-footer>
|
||||
<div style="font-size: 12px;font-weight: 400;color: #979db1;">
|
||||
<div class="copyright">
|
||||
©2025 xiaozhi-esp32-server
|
||||
</div>
|
||||
</el-footer>
|
||||
@@ -87,8 +90,11 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
fetchCaptcha() {
|
||||
console.log(this.$store.getters.getToken)
|
||||
if (this.$store.getters.getToken) {
|
||||
goToPage('/home')
|
||||
if (this.$route.path !== '/home'){
|
||||
this.$router.push('/home')
|
||||
}
|
||||
} else {
|
||||
this.captchaUuid = getUUID();
|
||||
|
||||
@@ -96,42 +102,51 @@ export default {
|
||||
if (res.status === 200) {
|
||||
const blob = new Blob([res.data], {type: res.data.type});
|
||||
this.captchaUrl = URL.createObjectURL(blob);
|
||||
|
||||
} else {
|
||||
console.error('验证码加载异常:', error);
|
||||
showDanger('验证码加载失败,点击刷新')
|
||||
showDanger('验证码加载失败,点击刷新');
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 封装输入验证逻辑
|
||||
validateInput(input, message) {
|
||||
if (!input.trim()) {
|
||||
showDanger(message);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
async login() {
|
||||
if (!this.form.username.trim()) { // 替换isNull校验
|
||||
showDanger('用户名不能为空')
|
||||
return
|
||||
// 验证用户名
|
||||
if (!this.validateInput(this.form.username, '用户名不能为空')) {
|
||||
return;
|
||||
}
|
||||
if (!this.form.password.trim()) { // 替换isNull校验
|
||||
showDanger('密码不能为空')
|
||||
return
|
||||
// 验证密码
|
||||
if (!this.validateInput(this.form.password, '密码不能为空')) {
|
||||
return;
|
||||
}
|
||||
if (!this.form.captcha.trim()) { // 替换isNull校验
|
||||
showDanger('验证码不能为空')
|
||||
return
|
||||
// 验证验证码
|
||||
if (!this.validateInput(this.form.captcha, '验证码不能为空')) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.form.captchaId = this.captchaUuid
|
||||
Api.user.login(this.form, ({data}) => {
|
||||
console.log(data)
|
||||
showSuccess('登陆成功!')
|
||||
|
||||
this.$store.commit('setToken', JSON.stringify(data.data))
|
||||
|
||||
goToPage('/home')
|
||||
if (data.code === 0) {
|
||||
showSuccess('登录成功!');
|
||||
this.$store.commit('setToken', JSON.stringify(data.data));
|
||||
goToPage('/home');
|
||||
} else {
|
||||
showDanger(data.msg || '登录失败');
|
||||
}
|
||||
})
|
||||
|
||||
// 重新获取验证码
|
||||
setTimeout(() => {
|
||||
this.fetchCaptcha()
|
||||
}, 1000)
|
||||
this.fetchCaptcha();
|
||||
}, 1000);
|
||||
},
|
||||
|
||||
goToRegister() {
|
||||
@@ -140,6 +155,6 @@ export default {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
<style lang="scss" scoped>
|
||||
@import './auth.scss'; // 添加这行引用
|
||||
</style>
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
<!-- 保持相同的头部 -->
|
||||
<el-header>
|
||||
<div style="display: flex;align-items: center;margin-top: 15px;margin-left: 10px;gap: 10px;">
|
||||
<img src="@/assets/xiaozhi-logo.png" alt="" style="width: 45px;height: 45px;"/>
|
||||
<img src="@/assets/xiaozhi-ai.png" alt="" style="width: 70px;height: 13px;"/>
|
||||
<img loading="lazy" alt="" src="@/assets/xiaozhi-logo.png" style="width: 45px;height: 45px;"/>
|
||||
<img loading="lazy" alt="" src="@/assets/xiaozhi-ai.png" style="width: 70px;height: 13px;"/>
|
||||
</div>
|
||||
</el-header>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<div class="login-box">
|
||||
<!-- 修改标题部分 -->
|
||||
<div style="display: flex;align-items: center;gap: 20px;margin-bottom: 39px;padding: 0 30px;">
|
||||
<img src="@/assets/login/hi.png" alt="" style="width: 34px;height: 34px;"/>
|
||||
<img loading="lazy" alt="" src="@/assets/login/hi.png" style="width: 34px;height: 34px;"/>
|
||||
<div class="login-text">注册</div>
|
||||
<div class="login-welcome">
|
||||
WELCOME TO REGISTER
|
||||
@@ -23,33 +23,33 @@
|
||||
<div style="padding: 0 30px;">
|
||||
<!-- 用户名输入框 -->
|
||||
<div class="input-box">
|
||||
<img src="@/assets/login/username.png" alt="" class="input-icon"/>
|
||||
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/username.png"/>
|
||||
<el-input v-model="form.username" placeholder="请输入用户名"/>
|
||||
</div>
|
||||
|
||||
<!-- 密码输入框 -->
|
||||
<div class="input-box">
|
||||
<img src="@/assets/login/password.png" alt="" class="input-icon"/>
|
||||
<el-input v-model="form.password" type="password" placeholder="请输入密码"/>
|
||||
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/password.png"/>
|
||||
<el-input v-model="form.password" placeholder="请输入密码" type="password"/>
|
||||
</div>
|
||||
|
||||
<!-- 新增确认密码 -->
|
||||
<div class="input-box">
|
||||
<img src="@/assets/login/password.png" alt="" class="input-icon"/>
|
||||
<el-input v-model="form.confirmPassword" type="password" placeholder="请确认密码"/>
|
||||
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/password.png"/>
|
||||
<el-input v-model="form.confirmPassword" placeholder="请确认密码" type="password"/>
|
||||
</div>
|
||||
|
||||
<!-- 验证码部分保持相同 -->
|
||||
<div style="display: flex; align-items: center; margin-top: 20px; width: 100%; gap: 10px;">
|
||||
<div class="input-box" style="width: calc(100% - 130px); margin-top: 0;">
|
||||
<img src="@/assets/login/shield.png" alt="" class="input-icon"/>
|
||||
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/shield.png"/>
|
||||
<el-input v-model="form.captcha" placeholder="请输入验证码" style="flex: 1;"/>
|
||||
</div>
|
||||
<img v-if="captchaUrl"
|
||||
:src="captchaUrl"
|
||||
alt="验证码"
|
||||
style="width: 150px; height: 40px; cursor: pointer;"
|
||||
@click="fetchCaptcha"
|
||||
<img loading="lazy" v-if="captchaUrl"
|
||||
:src="captchaUrl"
|
||||
alt="验证码"
|
||||
style="width: 150px; height: 40px; cursor: pointer;"
|
||||
@click="fetchCaptcha"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -74,7 +74,7 @@
|
||||
|
||||
<!-- 保持相同的页脚 -->
|
||||
<el-footer>
|
||||
<div style="font-size: 12px;font-weight: 400;color: #979db1;">
|
||||
<div class="copyright">
|
||||
©2025 xiaozhi-esp32-server
|
||||
</div>
|
||||
</el-footer>
|
||||
@@ -119,24 +119,33 @@ export default {
|
||||
});
|
||||
},
|
||||
|
||||
// 封装输入验证逻辑
|
||||
validateInput(input, message) {
|
||||
if (!input.trim()) {
|
||||
showDanger(message);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
// 注册逻辑
|
||||
register() {
|
||||
if (!this.form.username.trim()) {
|
||||
showDanger('用户名不能为空')
|
||||
return
|
||||
// 验证用户名
|
||||
if (!this.validateInput(this.form.username, '用户名不能为空')) {
|
||||
return;
|
||||
}
|
||||
if (!this.form.password.trim()) {
|
||||
showDanger('密码不能为空')
|
||||
return
|
||||
// 验证密码
|
||||
if (!this.validateInput(this.form.password, '密码不能为空')) {
|
||||
return;
|
||||
}
|
||||
if (this.form.password !== this.form.confirmPassword) {
|
||||
showDanger('两次输入的密码不一致')
|
||||
return
|
||||
}
|
||||
if (!this.form.captcha.trim()) {
|
||||
showDanger('验证码不能为空')
|
||||
return
|
||||
// 验证验证码
|
||||
if (!this.validateInput(this.form.captcha, '验证码不能为空')) {
|
||||
return;
|
||||
}
|
||||
|
||||
Api.user.register(this.form, ({data}) => {
|
||||
console.log(data)
|
||||
if (data.code === 0) {
|
||||
@@ -159,6 +168,6 @@ export default {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
<style lang="scss" scoped>
|
||||
@import './auth.scss'; // 修改为导入新建的SCSS文件
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -88,7 +88,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="font-size: 12px;font-weight: 400;margin-top: auto;padding-top: 24px;color: #979db1;">
|
||||
<div class="copyright">
|
||||
©2025 xiaozhi-esp32-server
|
||||
</div>
|
||||
</el-main>
|
||||
|
||||
@@ -0,0 +1,401 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<h1 class="title">XiaoZhi ESP32 Server 测试助手</h1>
|
||||
<div class="chat-container" ref="chatContainer">
|
||||
<div v-for="(message, index) in messages" :key="index"
|
||||
:class="['message', message.role === 'user' ? 'user' : 'assistant']">
|
||||
<span>{{ message.content }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<button @click="toggleRecording" :disabled="wsStatus !== 'connected'"
|
||||
:class="{ recording: isRecording }">
|
||||
{{ isRecording ? '停止录音' : '开始录音' }}
|
||||
</button>
|
||||
<p>WebSocket: {{ wsStatus }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Recorder from 'opus-recorder';
|
||||
import { OpusDecoder } from 'opus-decoder';
|
||||
|
||||
export default {
|
||||
name: 'TestPage',
|
||||
data() {
|
||||
return {
|
||||
messages: [],
|
||||
wsStatus: 'disconnected',
|
||||
isRecording: false,
|
||||
ws: null,
|
||||
recorder: null,
|
||||
stream: null,
|
||||
audioContext: null,
|
||||
sourceNode: null,
|
||||
audioDecoder: null,
|
||||
audioBufferQueue: [], // 当前句子的音频缓冲区
|
||||
playbackQueue: [], // 播放队列,存储待播放的句子
|
||||
isPlaying: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
connectWebSocket() {
|
||||
this.ws = new WebSocket('ws://192.168.3.97:8000');//修改服务端地址
|
||||
this.ws.binaryType = 'arraybuffer';
|
||||
this.ws.onopen = () => {
|
||||
this.wsStatus = 'connected';
|
||||
console.log('WebSocket 连接成功');
|
||||
this.ws.send(JSON.stringify({ type: 'auth', 'device-id': 'test-device' }));
|
||||
this.audioDecoder = new OpusDecoder({ sampleRate: 16000, channels: 1 });
|
||||
};
|
||||
this.ws.onmessage = async (event) => {
|
||||
if (typeof event.data === 'string') {
|
||||
const msg = JSON.parse(event.data);
|
||||
console.log('收到文本消息:', msg);
|
||||
|
||||
if (msg.type === 'stt') {
|
||||
this.messages.push({ role: 'user', content: msg.text });
|
||||
this.scrollToBottom();
|
||||
} else if (msg.type === 'llm') {
|
||||
this.messages.push({ role: 'assistant', content: msg.text });
|
||||
this.scrollToBottom();
|
||||
} else if (msg.type === 'tts') {
|
||||
if (msg.state === 'sentence_start') {
|
||||
this.audioBufferQueue = [];
|
||||
const lastMessage = this.messages[this.messages.length - 1];
|
||||
if (!lastMessage || lastMessage.content !== msg.text) {
|
||||
this.messages.push({ role: 'assistant', content: msg.text });
|
||||
this.scrollToBottom();
|
||||
}
|
||||
} else if (msg.state === 'sentence_end') {
|
||||
if (this.audioBufferQueue.length > 0) {
|
||||
this.playbackQueue.push([...this.audioBufferQueue]);
|
||||
this.audioBufferQueue = [];
|
||||
this.playNextInQueue();
|
||||
}
|
||||
} else if (msg.state === 'stop') {
|
||||
console.log('TTS 任务结束');
|
||||
if (this.audioBufferQueue.length > 0) {
|
||||
this.playbackQueue.push([...this.audioBufferQueue]);
|
||||
this.audioBufferQueue = [];
|
||||
this.playNextInQueue();
|
||||
}
|
||||
}
|
||||
} else if (msg.type === 'hello') {
|
||||
console.log('收到服务器初始化消息:', msg);
|
||||
}
|
||||
} else if (event.data instanceof ArrayBuffer) {
|
||||
console.log('收到音频帧,大小:', event.data.byteLength, '字节');
|
||||
const opusFrame = new Uint8Array(event.data);
|
||||
const frameHead = Array.from(opusFrame.slice(0, 8))
|
||||
.map(b => b.toString(16).padStart(2, '0'))
|
||||
.join(' ');
|
||||
console.log('音频帧前8字节:', frameHead);
|
||||
|
||||
try {
|
||||
const decoded = this.audioDecoder.decodeFrame(opusFrame);
|
||||
console.log('解码结果:', decoded);
|
||||
if (decoded && decoded.channelData && decoded.channelData[0]) {
|
||||
const pcmData = decoded.channelData[0];
|
||||
if (pcmData.length > 0) {
|
||||
this.audioBufferQueue.push(pcmData);
|
||||
console.log('解码音频帧,PCM 数据长度:', pcmData.length);
|
||||
if (this.audioBufferQueue.length >= 5 && this.playbackQueue.length === 0 && !this.isPlaying) {
|
||||
this.playbackQueue.push([...this.audioBufferQueue]);
|
||||
this.audioBufferQueue = [];
|
||||
this.playNextInQueue();
|
||||
}
|
||||
} else {
|
||||
console.warn('解码成功,但 PCM 数据长度为 0');
|
||||
}
|
||||
} else {
|
||||
console.warn('解码结果无效:', decoded);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Opus 解码错误:', e);
|
||||
}
|
||||
}
|
||||
};
|
||||
this.ws.onerror = (error) => {
|
||||
console.error('WebSocket 错误:', error);
|
||||
this.wsStatus = 'error';
|
||||
};
|
||||
this.ws.onclose = () => {
|
||||
this.wsStatus = 'disconnected';
|
||||
console.log('WebSocket 断开');
|
||||
setTimeout(this.connectWebSocket, 1000);
|
||||
};
|
||||
},
|
||||
scrollToBottom() {
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.chatContainer) {
|
||||
this.$refs.chatContainer.scrollTop = this.$refs.chatContainer.scrollHeight;
|
||||
}
|
||||
});
|
||||
},
|
||||
async playNextInQueue() {
|
||||
if (this.isPlaying || this.playbackQueue.length === 0) {
|
||||
return;
|
||||
}
|
||||
this.isPlaying = true;
|
||||
|
||||
if (!this.audioContext || this.audioContext.state === 'closed') {
|
||||
this.audioContext = new (window.AudioContext || window.webkitAudioContext)({ sampleRate: 16000 });
|
||||
}
|
||||
|
||||
const pcmBuffers = this.playbackQueue.shift();
|
||||
const totalLength = pcmBuffers.reduce((sum, pcm) => sum + pcm.length, 0);
|
||||
if (totalLength === 0) {
|
||||
console.error('音频缓冲区总长度为 0,无法播放');
|
||||
this.isPlaying = false;
|
||||
this.playNextInQueue();
|
||||
return;
|
||||
}
|
||||
|
||||
const mergedPcm = new Float32Array(totalLength);
|
||||
let offset = 0;
|
||||
for (const pcm of pcmBuffers) {
|
||||
mergedPcm.set(pcm, offset);
|
||||
offset += pcm.length;
|
||||
}
|
||||
|
||||
const audioBuffer = this.audioContext.createBuffer(1, totalLength, 16000);
|
||||
audioBuffer.getChannelData(0).set(mergedPcm);
|
||||
|
||||
const source = this.audioContext.createBufferSource();
|
||||
source.buffer = audioBuffer;
|
||||
source.connect(this.audioContext.destination);
|
||||
source.onended = () => {
|
||||
this.isPlaying = false;
|
||||
console.log('音频播放结束');
|
||||
this.playNextInQueue();
|
||||
};
|
||||
source.start();
|
||||
console.log('开始播放音频,总长度:', totalLength);
|
||||
},
|
||||
stripOggContainer(data) {
|
||||
let arrayBuffer;
|
||||
if (data instanceof ArrayBuffer) {
|
||||
arrayBuffer = data;
|
||||
} else if (data.buffer instanceof ArrayBuffer) {
|
||||
arrayBuffer = data.buffer;
|
||||
} else {
|
||||
console.error('输入数据类型不支持:', data);
|
||||
return [data];
|
||||
}
|
||||
|
||||
const dataView = new DataView(arrayBuffer);
|
||||
const frames = [];
|
||||
let offset = 0;
|
||||
|
||||
while (offset < arrayBuffer.byteLength) {
|
||||
if (dataView.getUint32(offset) === 0x4F676753) { // 'OggS'
|
||||
const segmentTableLength = dataView.getUint8(offset + 26);
|
||||
const segmentTableOffset = offset + 27;
|
||||
let segmentOffset = segmentTableOffset + segmentTableLength;
|
||||
|
||||
for (let i = 0; i < segmentTableLength; i++) {
|
||||
const segmentLength = dataView.getUint8(segmentTableOffset + i);
|
||||
const segmentData = arrayBuffer.slice(segmentOffset, segmentOffset + segmentLength);
|
||||
segmentOffset += segmentLength;
|
||||
|
||||
const header = new TextDecoder().decode(segmentData.slice(0, 8));
|
||||
if (header === 'OpusHead' || header === 'OpusTags') {
|
||||
console.log(`跳过 ${header},大小: ${segmentLength} 字节`);
|
||||
} else if (segmentLength >= 50 && segmentLength <= 300) {
|
||||
frames.push(segmentData);
|
||||
}
|
||||
}
|
||||
offset = segmentOffset;
|
||||
} else {
|
||||
const remainingLength = arrayBuffer.byteLength - offset;
|
||||
if (remainingLength >= 50 && remainingLength <= 300) {
|
||||
frames.push(arrayBuffer.slice(offset));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (frames.length === 0) {
|
||||
console.warn('未找到有效裸 Opus 帧,返回原始数据');
|
||||
return [arrayBuffer];
|
||||
}
|
||||
console.log('剥离后找到', frames.length, '个裸 Opus 帧');
|
||||
return frames;
|
||||
},
|
||||
async initRecorder() {
|
||||
console.log('开始初始化录音');
|
||||
try {
|
||||
if (this.stream) {
|
||||
this.stream.getTracks().forEach(track => track.stop());
|
||||
}
|
||||
this.stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||
console.log('获取麦克风权限成功,stream:', this.stream);
|
||||
|
||||
if (!this.audioContext || this.audioContext.state === 'closed') {
|
||||
this.audioContext = new (window.AudioContext || window.webkitAudioContext)({ sampleRate: 16000 });
|
||||
}
|
||||
this.sourceNode = this.audioContext.createMediaStreamSource(this.stream);
|
||||
|
||||
this.recorder = new Recorder({
|
||||
encoderPath: '/encoderWorker.min.js',
|
||||
sampleRate: 16000,
|
||||
numberOfChannels: 1,
|
||||
frameSize: 960,
|
||||
encoderApplication: 2048,
|
||||
encoderFrameSize: 60,
|
||||
encoderBitRate: 24000,
|
||||
monitorGain: 0,
|
||||
sourceNode: this.sourceNode,
|
||||
ogg: false,
|
||||
streamPages: false,
|
||||
maxFramesPerPage: 1,
|
||||
});
|
||||
|
||||
this.recorder.ondataavailable = (data) => {
|
||||
console.log('录音数据可用:', data.byteLength, '类型:', data.constructor.name);
|
||||
const frames = this.stripOggContainer(data);
|
||||
frames.forEach((frame, index) => {
|
||||
const frameHead = Array.from(new Uint8Array(frame.slice(0, 8)))
|
||||
.map(b => b.toString(16).padStart(2, '0'))
|
||||
.join(' ');
|
||||
console.log(`帧 ${index} 大小: ${frame.byteLength} 字节,前8字节: ${frameHead}`);
|
||||
|
||||
if (frame.byteLength < 50 || frame.byteLength > 300) {
|
||||
console.warn(`帧 ${index} 大小异常: ${frame.byteLength} 字节,预期 50-300 字节`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
||||
this.ws.send(frame);
|
||||
console.log('发送裸 Opus 帧:', frame.byteLength);
|
||||
} else {
|
||||
console.warn('WebSocket 未连接,跳过发送');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
this.recorder.onstart = () => {
|
||||
console.log('录音已启动');
|
||||
};
|
||||
|
||||
this.recorder.onstop = () => {
|
||||
console.log('录音停止');
|
||||
this.stream.getTracks().forEach(track => track.stop());
|
||||
this.stream = null;
|
||||
this.sourceNode = null;
|
||||
this.scrollToBottom();
|
||||
};
|
||||
|
||||
console.log('Recorder 初始化成功');
|
||||
} catch (err) {
|
||||
console.error('初始化录音失败:', err);
|
||||
alert('无法访问麦克风或录音初始化失败,请检查权限');
|
||||
}
|
||||
},
|
||||
async toggleRecording() {
|
||||
console.log('点击 toggleRecording,当前状态:', this.isRecording, 'WebSocket 状态:', this.wsStatus);
|
||||
if (!this.recorder) {
|
||||
await this.initRecorder();
|
||||
if (!this.recorder) {
|
||||
console.error('recorder 初始化失败');
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (this.isRecording) {
|
||||
console.log('停止录音');
|
||||
this.recorder.stop();
|
||||
this.isRecording = false;
|
||||
} else {
|
||||
try {
|
||||
console.log('开始录音');
|
||||
await this.initRecorder();
|
||||
await this.recorder.start();
|
||||
console.log('录音开始后,状态:', this.recorder.state);
|
||||
this.isRecording = true;
|
||||
} catch (err) {
|
||||
console.error('录音启动失败:', err);
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
console.log('组件挂载,初始化 WebSocket');
|
||||
this.connectWebSocket();
|
||||
},
|
||||
destroyed() {
|
||||
if (this.ws) this.ws.close();
|
||||
if (this.stream) this.stream.getTracks().forEach(track => track.stop());
|
||||
if (this.recorder) this.recorder.stop();
|
||||
if (this.audioContext) this.audioContext.close();
|
||||
if (this.audioDecoder) this.audioDecoder.destroy();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
|
||||
.title {
|
||||
text-align: center;
|
||||
font-size: 24px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.chat-container {
|
||||
height: 60vh;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #ccc;
|
||||
padding: 10px;
|
||||
background: #f9f9f9;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.message {
|
||||
margin: 10px 0;
|
||||
padding: 8px 12px;
|
||||
border-radius: 5px;
|
||||
max-width: 70%;
|
||||
}
|
||||
|
||||
.user {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.assistant {
|
||||
background: #e9ecef;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.controls {
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 10px 20px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
background: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
button.recording {
|
||||
background: #dc3545;
|
||||
}
|
||||
</style>
|
||||
@@ -1,131 +0,0 @@
|
||||
<template>
|
||||
<div class="welcome">
|
||||
<el-container style="height: 100%;">
|
||||
<el-header>
|
||||
<div
|
||||
style="display: flex;align-items: center;margin-top: 15px;margin-left: 10px;gap: 10px;">
|
||||
<img src="@/assets/xiaozhi-logo.png" alt="" style="width: 45px;height: 45px;" />
|
||||
<img src="@/assets/xiaozhi-ai.png" alt="" style="width: 70px;height: 13px;" />
|
||||
</div>
|
||||
</el-header>
|
||||
<el-main style="position: relative;">
|
||||
<div style="margin-left: 18%;position: absolute;top: 50%;transform: translateY(-50%);margin-top: -20px">
|
||||
<div style="display: flex;align-items: center;margin-bottom: 27px;">
|
||||
<div class="left-pillar" />
|
||||
<div class="hi-text-bg" />
|
||||
<div class="hi-text">Hi,你好</div>
|
||||
</div>
|
||||
<div class="introduction">让我们一起探索</div>
|
||||
<div class="introduction">人工智能与机器人技术</div>
|
||||
<div class="introduction">的迷人世界!</div>
|
||||
|
||||
<!-- 副标题 -->
|
||||
<p class="english-subtitle">Let's explore the fascinating world</p>
|
||||
|
||||
<!-- 按钮 -->
|
||||
<div style="margin-top: 60px;display: flex;gap: 20px;">
|
||||
<div class="btn">
|
||||
<img src="@/assets/welcome/questions.png" alt="" class="btn-icon" />
|
||||
DIY教程
|
||||
</div>
|
||||
<div class="btn">
|
||||
<img src="@/assets/welcome/github.png" alt="" class="btn-icon" />
|
||||
GitHub
|
||||
</div>
|
||||
<div class="btn" style="background: #5778ff;color: #fff;" @click="jumpHome">
|
||||
<img src="@/assets/welcome/more.png" alt="" class="btn-icon" />
|
||||
控制台
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</el-main>
|
||||
<el-footer>
|
||||
<div style="font-size: 12px;font-weight: 400;color: #3D4566">
|
||||
©2025 xiaozhi-esp32-server</div>
|
||||
</el-footer>
|
||||
</el-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// @ is an alias to /src
|
||||
|
||||
export default {
|
||||
name: 'home',
|
||||
methods:{
|
||||
jumpHome(){
|
||||
this.$router.push('/home')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.welcome {
|
||||
min-width: 1200px;
|
||||
min-height: 675px;
|
||||
height: 100vh;
|
||||
background-image: url("@/assets/welcome/background.png");
|
||||
background-size: cover;
|
||||
/* 确保背景图像覆盖整个元素 */
|
||||
background-position: center;
|
||||
/* 从顶部中心对齐 */
|
||||
-webkit-background-size: cover;
|
||||
/* 兼容老版本WebKit浏览器 */
|
||||
-o-background-size: cover;
|
||||
/* 兼容老版本Opera浏览器 */
|
||||
}
|
||||
.left-pillar {
|
||||
width: 4px;
|
||||
height: 36px;
|
||||
background: #5778ff;
|
||||
}
|
||||
.hi-text-bg {
|
||||
width: 129px;
|
||||
height: 36px;
|
||||
background: linear-gradient(90.66deg, #5778ff 0%, #f5f6fa00 100%);
|
||||
opacity: 0.5;
|
||||
}
|
||||
.hi-text {
|
||||
line-height: 36px;
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
text-align: left;
|
||||
color: #3d4566;
|
||||
padding-left: 14px;
|
||||
position: absolute;
|
||||
}
|
||||
.introduction {
|
||||
font-weight: 700;
|
||||
font-size: 42px;
|
||||
text-align: left;
|
||||
color: #3d4566;
|
||||
}
|
||||
.btn {
|
||||
width: 100px;
|
||||
height: 40px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 28px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
text-align: left;
|
||||
color: #3d4566;
|
||||
cursor: pointer;
|
||||
background-color: #fff;
|
||||
}
|
||||
.btn-icon {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
.english-subtitle {
|
||||
font-size: 11px;
|
||||
color: #818cae;
|
||||
text-align: left;
|
||||
margin-top: 5px;
|
||||
position: relative;
|
||||
top: 8px;
|
||||
}
|
||||
</style>
|
||||