update:更新manager-api模块
@@ -1,4 +1 @@
|
||||
# 暂时使用群主的接口
|
||||
# VUE_APP_API_BASE_URL=https://2662r3426b.vicp.fun/xiaozhi
|
||||
# 如果本地开发,请使用以下接口
|
||||
VUE_APP_API_BASE_URL=http://localhost:8002/xiaozhi
|
||||
VUE_APP_API_BASE_URL=/xiaozhi
|
||||
@@ -1 +1,4 @@
|
||||
VUE_APP_API_BASE_URL=/xiaozhi
|
||||
VUE_APP_API_BASE_URL=/xiaozhi
|
||||
VUE_APP_PUBLIC_PATH=/
|
||||
# 是否开启CDN
|
||||
VUE_APP_USE_CDN=false
|
||||
@@ -1,4 +1,4 @@
|
||||
本文档是开发类文档,如需部署小智服务端,[点击这里查看部署教程](../../../docs/Deployment.md)
|
||||
本文档是开发类文档,如需部署小智服务端,[点击这里查看部署教程](../../README.md#%E9%83%A8%E7%BD%B2%E6%96%87%E6%A1%A3)
|
||||
|
||||
# xiaozhi
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"dependencies": {
|
||||
"core-js": "^3.41.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"dotenv": "^16.5.0",
|
||||
"element-ui": "^2.15.14",
|
||||
"flyio": "^0.6.14",
|
||||
"normalize.css": "^8.0.1",
|
||||
@@ -31,7 +32,8 @@
|
||||
"sass": "^1.32.7",
|
||||
"sass-loader": "^12.0.0",
|
||||
"vue-template-compiler": "^2.6.14",
|
||||
"webpack-bundle-analyzer": "^4.10.2"
|
||||
"webpack-bundle-analyzer": "^4.10.2",
|
||||
"workbox-webpack-plugin": "^7.3.0"
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
@@ -43,4 +45,4 @@
|
||||
"*.scss",
|
||||
"*.vue"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<title>
|
||||
<%= process.env.VUE_APP_TITLE %>
|
||||
</title>
|
||||
<% if (htmlWebpackPlugin.options.cdn) { %>
|
||||
<% if (htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.css) { %>
|
||||
<% for (var i in htmlWebpackPlugin.options.cdn.css) { %>
|
||||
<link rel="stylesheet" href="<%= htmlWebpackPlugin.options.cdn.css[i] %>">
|
||||
<% } %>
|
||||
@@ -23,7 +23,7 @@
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
<% if (htmlWebpackPlugin.options.cdn) { %>
|
||||
<% if (htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.js) { %>
|
||||
<% for (var i in htmlWebpackPlugin.options.cdn.js) { %>
|
||||
<script src="<%= htmlWebpackPlugin.options.cdn.js[i] %>"></script>
|
||||
<% } %>
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>离线模式 - 小智控制台</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
background-color: #f5f7fa;
|
||||
color: #333;
|
||||
}
|
||||
.container {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
|
||||
max-width: 80%;
|
||||
}
|
||||
h1 {
|
||||
margin-bottom: 1rem;
|
||||
color: #409EFF;
|
||||
}
|
||||
p {
|
||||
margin-bottom: 1.5rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.btn {
|
||||
background-color: #409EFF;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.8rem 1.5rem;
|
||||
border-radius: 4px;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
.btn:hover {
|
||||
background-color: #337ecc;
|
||||
}
|
||||
.icon {
|
||||
font-size: 4rem;
|
||||
margin-bottom: 1rem;
|
||||
color: #409EFF;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="icon">📶</div>
|
||||
<h1>您当前处于离线模式</h1>
|
||||
<p>看起来您的网络连接有问题,无法连接到小智控制台服务器。</p>
|
||||
<p>部分已缓存的内容和静态资源可能仍然可用,但功能可能受到限制。</p>
|
||||
<button class="btn" onclick="window.location.reload()">重新尝试连接</button>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 检测网络状态变化
|
||||
window.addEventListener('online', () => {
|
||||
window.location.reload();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<router-view />
|
||||
<cache-viewer v-if="isCDNEnabled" :visible.sync="showCacheViewer" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -45,4 +46,118 @@ nav {
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
import CacheViewer from '@/components/CacheViewer.vue';
|
||||
import { logCacheStatus } from '@/utils/cacheViewer';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CacheViewer
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showCacheViewer: false,
|
||||
isCDNEnabled: process.env.VUE_APP_USE_CDN === 'true'
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
// 只有在启用CDN时才添加相关事件和功能
|
||||
if (this.isCDNEnabled) {
|
||||
// 添加全局快捷键Alt+C用于显示缓存查看器
|
||||
document.addEventListener('keydown', this.handleKeyDown);
|
||||
|
||||
// 在全局对象上添加缓存检查方法,便于调试
|
||||
window.checkCDNCacheStatus = () => {
|
||||
this.showCacheViewer = true;
|
||||
};
|
||||
|
||||
// 在控制台输出提示信息
|
||||
console.info(
|
||||
'%c[小智服务] CDN缓存检查工具已加载',
|
||||
'color: #409EFF; font-weight: bold;'
|
||||
);
|
||||
console.info(
|
||||
'按下 Alt+C 组合键或在控制台运行 checkCDNCacheStatus() 可以查看CDN缓存状态'
|
||||
);
|
||||
|
||||
// 检查Service Worker状态
|
||||
this.checkServiceWorkerStatus();
|
||||
} else {
|
||||
console.info(
|
||||
'%c[小智服务] CDN模式已禁用,使用本地打包资源',
|
||||
'color: #67C23A; font-weight: bold;'
|
||||
);
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
// 只有在启用CDN时才需要移除事件监听
|
||||
if (this.isCDNEnabled) {
|
||||
document.removeEventListener('keydown', this.handleKeyDown);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleKeyDown(e) {
|
||||
// Alt+C 快捷键
|
||||
if (e.altKey && e.key === 'c') {
|
||||
this.showCacheViewer = true;
|
||||
}
|
||||
},
|
||||
async checkServiceWorkerStatus() {
|
||||
// 检查Service Worker是否已注册
|
||||
if ('serviceWorker' in navigator) {
|
||||
try {
|
||||
const registrations = await navigator.serviceWorker.getRegistrations();
|
||||
if (registrations.length > 0) {
|
||||
console.info(
|
||||
'%c[小智服务] Service Worker已注册',
|
||||
'color: #67C23A; font-weight: bold;'
|
||||
);
|
||||
|
||||
// 输出缓存状态到控制台
|
||||
setTimeout(async () => {
|
||||
const hasCaches = await logCacheStatus();
|
||||
if (!hasCaches) {
|
||||
console.info(
|
||||
'%c[小智服务] 还未检测到缓存,请刷新页面或等待缓存建立',
|
||||
'color: #E6A23C; font-weight: bold;'
|
||||
);
|
||||
|
||||
// 开发环境下提供额外提示
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.info(
|
||||
'%c[小智服务] 在开发环境中,Service Worker可能无法正常初始化缓存',
|
||||
'color: #E6A23C; font-weight: bold;'
|
||||
);
|
||||
console.info('请尝试以下方法检查Service Worker是否生效:');
|
||||
console.info('1. 在开发者工具的Application/Application标签页中查看Service Worker状态');
|
||||
console.info('2. 在开发者工具的Application/Cache/Cache Storage中查看缓存内容');
|
||||
console.info('3. 使用生产构建(npm run build)并通过HTTP服务器访问以测试完整功能');
|
||||
}
|
||||
}
|
||||
}, 2000);
|
||||
} else {
|
||||
console.info(
|
||||
'%c[小智服务] Service Worker未注册,CDN资源可能无法缓存',
|
||||
'color: #F56C6C; font-weight: bold;'
|
||||
);
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.info(
|
||||
'%c[小智服务] 在开发环境中,这是正常现象',
|
||||
'color: #E6A23C; font-weight: bold;'
|
||||
);
|
||||
console.info('Service Worker通常只在生产环境中生效');
|
||||
console.info('要测试Service Worker功能:');
|
||||
console.info('1. 运行npm run build构建生产版本');
|
||||
console.info('2. 通过HTTP服务器访问构建后的页面');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('检查Service Worker状态失败:', error);
|
||||
}
|
||||
} else {
|
||||
console.warn('当前浏览器不支持Service Worker,CDN资源缓存功能不可用');
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -2,17 +2,17 @@
|
||||
import admin from './module/admin.js'
|
||||
import agent from './module/agent.js'
|
||||
import device from './module/device.js'
|
||||
import dict from './module/dict.js'
|
||||
import model from './module/model.js'
|
||||
import ota from './module/ota.js'
|
||||
import timbre from "./module/timbre.js"
|
||||
import user from './module/user.js'
|
||||
import timbre from "./module/timbre.js";
|
||||
|
||||
/**
|
||||
* 接口地址
|
||||
* 开发时自动读取使用.env.development文件
|
||||
* 编译时自动读取使用.env.production文件
|
||||
*/
|
||||
// const DEV_API_SERVICE = process.env.VUE_APP_API_BASE_URL
|
||||
const DEV_API_SERVICE = 'https://2662r3426b.vicp.fun/xiaozhi'
|
||||
const DEV_API_SERVICE = process.env.VUE_APP_API_BASE_URL
|
||||
|
||||
/**
|
||||
* 根据开发环境返回接口url
|
||||
@@ -32,4 +32,6 @@ export default {
|
||||
device,
|
||||
model,
|
||||
timbre,
|
||||
ota,
|
||||
dict
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ function sendRequest() {
|
||||
return {
|
||||
_sucCallback: null,
|
||||
_failCallback: null,
|
||||
_networkFailCallback: null,
|
||||
_method: 'GET',
|
||||
_data: {},
|
||||
_header: { 'content-type': 'application/json; charset=utf-8' },
|
||||
@@ -36,7 +37,7 @@ function sendRequest() {
|
||||
headers: this._header,
|
||||
responseType: this._responseType
|
||||
}).then((res) => {
|
||||
const error = httpHandlerError(res, this._failCallback);
|
||||
const error = httpHandlerError(res, this._failCallback, this._networkFailCallback);
|
||||
if (error) {
|
||||
return
|
||||
}
|
||||
@@ -47,7 +48,7 @@ function sendRequest() {
|
||||
}).catch((res) => {
|
||||
// 打印失败响应
|
||||
console.log('catch', res)
|
||||
httpHandlerError(res, this._failCallback)
|
||||
httpHandlerError(res, this._failCallback, this._networkFailCallback)
|
||||
})
|
||||
return this
|
||||
},
|
||||
@@ -59,6 +60,10 @@ function sendRequest() {
|
||||
this._failCallback = callback
|
||||
return this
|
||||
},
|
||||
'networkFail'(callback) {
|
||||
this._networkFailCallback = callback
|
||||
return this
|
||||
},
|
||||
'url'(url) {
|
||||
if (url) {
|
||||
url = url.replaceAll('$', '/')
|
||||
@@ -95,11 +100,11 @@ function sendRequest() {
|
||||
|
||||
/**
|
||||
* Info 请求完成后返回信息
|
||||
* callBack 回调函数
|
||||
* errTip 自定义错误信息
|
||||
* failCallback 回调函数
|
||||
* networkFailCallback 回调函数
|
||||
*/
|
||||
// 在错误处理函数中添加日志
|
||||
function httpHandlerError(info, callBack) {
|
||||
function httpHandlerError(info, failCallback, networkFailCallback) {
|
||||
|
||||
/** 请求成功,退出该函数 可以根据项目需求来判断是否请求成功。这里判断的是status为200的时候是成功 */
|
||||
let networkError = false
|
||||
@@ -111,12 +116,16 @@ function httpHandlerError(info, callBack) {
|
||||
goToPage(Constant.PAGE.LOGIN, true);
|
||||
return true
|
||||
} else {
|
||||
showDanger(info.data.msg)
|
||||
if (failCallback) {
|
||||
failCallback(info)
|
||||
} else {
|
||||
showDanger(info.data.msg)
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
if (callBack) {
|
||||
callBack(info)
|
||||
if (networkFailCallback) {
|
||||
networkFailCallback(info)
|
||||
} else {
|
||||
showDanger(`网络请求出现了错误【${info.status}】`)
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ export default {
|
||||
RequestService.clearRequestTime()
|
||||
callback(res)
|
||||
})
|
||||
.fail((err) => {
|
||||
.networkFail((err) => {
|
||||
console.error('请求失败:', err)
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.getUserList(callback)
|
||||
@@ -34,7 +34,7 @@ export default {
|
||||
RequestService.clearRequestTime()
|
||||
callback(res)
|
||||
})
|
||||
.fail((err) => {
|
||||
.networkFail((err) => {
|
||||
console.error('删除失败:', err)
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.deleteUser(id, callback)
|
||||
@@ -50,17 +50,19 @@ export default {
|
||||
RequestService.clearRequestTime()
|
||||
callback(res)
|
||||
})
|
||||
.fail((err) => {
|
||||
.networkFail((err) => {
|
||||
console.error('重置密码失败:', err)
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.resetUserPassword(id, callback)
|
||||
})
|
||||
}).send()
|
||||
},
|
||||
// 获取参数列表
|
||||
getParamsList(params, callback) {
|
||||
const queryParams = new URLSearchParams({
|
||||
page: params.page,
|
||||
limit: params.limit
|
||||
limit: params.limit,
|
||||
paramCode: params.paramCode || ''
|
||||
}).toString();
|
||||
|
||||
RequestService.sendRequest()
|
||||
@@ -70,13 +72,14 @@ export default {
|
||||
RequestService.clearRequestTime()
|
||||
callback(res)
|
||||
})
|
||||
.fail((err) => {
|
||||
.networkFail((err) => {
|
||||
console.error('获取参数列表失败:', err)
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.getParamsList(params, callback)
|
||||
})
|
||||
}).send()
|
||||
},
|
||||
// 保存
|
||||
addParam(data, callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/admin/params`)
|
||||
@@ -86,13 +89,14 @@ export default {
|
||||
RequestService.clearRequestTime()
|
||||
callback(res)
|
||||
})
|
||||
.fail((err) => {
|
||||
.networkFail((err) => {
|
||||
console.error('添加参数失败:', err)
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.addParam(data, callback)
|
||||
})
|
||||
}).send()
|
||||
},
|
||||
// 修改
|
||||
updateParam(data, callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/admin/params`)
|
||||
@@ -102,43 +106,61 @@ export default {
|
||||
RequestService.clearRequestTime()
|
||||
callback(res)
|
||||
})
|
||||
.fail((err) => {
|
||||
.networkFail((err) => {
|
||||
console.error('更新参数失败:', err)
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.updateParam(data, callback)
|
||||
})
|
||||
}).send()
|
||||
},
|
||||
deleteParam(id, callback) {
|
||||
// 删除
|
||||
deleteParam(ids, callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/admin/params`)
|
||||
.method('DELETE')
|
||||
.data({ ids: [id] })
|
||||
.url(`${getServiceUrl()}/admin/params/delete`)
|
||||
.method('POST')
|
||||
.data(ids)
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime()
|
||||
callback(res)
|
||||
callback(res);
|
||||
})
|
||||
.fail((err) => {
|
||||
.networkFail((err) => {
|
||||
console.error('删除参数失败:', err)
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.deleteParam(id, callback)
|
||||
this.deleteParam(ids, callback)
|
||||
})
|
||||
}).send()
|
||||
},
|
||||
batchDeleteParams(ids, callback) {
|
||||
// 获取ws服务端列表
|
||||
getWsServerList(params, callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/admin/params`)
|
||||
.method('DELETE')
|
||||
.data({ ids })
|
||||
.url(`${getServiceUrl()}/admin/server/server-list`)
|
||||
.method('GET')
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime()
|
||||
callback(res)
|
||||
})
|
||||
.fail((err) => {
|
||||
console.error('批量删除参数失败:', err)
|
||||
.networkFail((err) => {
|
||||
console.error('获取ws服务端列表失败:', err)
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.batchDeleteParams(ids, callback)
|
||||
this.getWsServerList(params, callback)
|
||||
})
|
||||
}).send()
|
||||
}).send();
|
||||
},
|
||||
// 发送ws服务器动作指令
|
||||
sendWsServerAction(data, callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/admin/server/emit-action`)
|
||||
.method('POST')
|
||||
.data(data)
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime()
|
||||
callback(res)
|
||||
})
|
||||
.networkFail((err) => {
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.sendWsServerAction(data, callback)
|
||||
})
|
||||
}).send();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ export default {
|
||||
RequestService.clearRequestTime();
|
||||
callback(res);
|
||||
})
|
||||
.fail(() => {
|
||||
.networkFail(() => {
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.getAgentList(callback);
|
||||
});
|
||||
@@ -28,7 +28,7 @@ export default {
|
||||
RequestService.clearRequestTime();
|
||||
callback(res);
|
||||
})
|
||||
.fail(() => {
|
||||
.networkFail(() => {
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.addAgent(agentName, callback);
|
||||
});
|
||||
@@ -43,25 +43,25 @@ export default {
|
||||
RequestService.clearRequestTime();
|
||||
callback(res);
|
||||
})
|
||||
.fail(() => {
|
||||
.networkFail(() => {
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.deleteAgent(agentId, callback);
|
||||
});
|
||||
}).send();
|
||||
},
|
||||
// 获取智能体配置
|
||||
getDeviceConfig(deviceId, callback) {
|
||||
getDeviceConfig(agentId, callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/agent/${deviceId}`)
|
||||
.url(`${getServiceUrl()}/agent/${agentId}`)
|
||||
.method('GET')
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime();
|
||||
callback(res);
|
||||
})
|
||||
.fail((err) => {
|
||||
.networkFail((err) => {
|
||||
console.error('获取配置失败:', err);
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.getDeviceConfig(deviceId, callback);
|
||||
this.getDeviceConfig(agentId, callback);
|
||||
});
|
||||
}).send();
|
||||
},
|
||||
@@ -75,7 +75,7 @@ export default {
|
||||
RequestService.clearRequestTime();
|
||||
callback(res);
|
||||
})
|
||||
.fail(() => {
|
||||
.networkFail(() => {
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.updateAgentConfig(agentId, configData, callback);
|
||||
});
|
||||
@@ -90,11 +90,57 @@ export default {
|
||||
RequestService.clearRequestTime();
|
||||
callback(res);
|
||||
})
|
||||
.fail((err) => {
|
||||
.networkFail((err) => {
|
||||
console.error('获取模板失败:', err);
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.getAgentTemplate(callback);
|
||||
});
|
||||
}).send();
|
||||
},
|
||||
// 获取智能体会话列表
|
||||
getAgentSessions(agentId, params, callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/agent/${agentId}/sessions`)
|
||||
.method('GET')
|
||||
.data(params)
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime();
|
||||
callback(res);
|
||||
})
|
||||
.networkFail(() => {
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.getAgentSessions(agentId, params, callback);
|
||||
});
|
||||
}).send();
|
||||
},
|
||||
// 获取智能体聊天记录
|
||||
getAgentChatHistory(agentId, sessionId, callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/agent/${agentId}/chat-history/${sessionId}`)
|
||||
.method('GET')
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime();
|
||||
callback(res);
|
||||
})
|
||||
.networkFail(() => {
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.getAgentChatHistory(agentId, sessionId, callback);
|
||||
});
|
||||
}).send();
|
||||
},
|
||||
// 获取音频下载ID
|
||||
getAudioId(audioId, callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/agent/audio/${audioId}`)
|
||||
.method('POST')
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime();
|
||||
callback(res);
|
||||
})
|
||||
.networkFail(() => {
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.getAudioId(audioId, callback);
|
||||
});
|
||||
}).send();
|
||||
},
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ export default {
|
||||
RequestService.clearRequestTime();
|
||||
callback(res);
|
||||
})
|
||||
.fail((err) => {
|
||||
.networkFail((err) => {
|
||||
console.error('获取设备列表失败:', err);
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.getAgentBindDevices(agentId, callback);
|
||||
@@ -28,7 +28,7 @@ export default {
|
||||
RequestService.clearRequestTime();
|
||||
callback(res);
|
||||
})
|
||||
.fail((err) => {
|
||||
.networkFail((err) => {
|
||||
console.error('解绑设备失败:', err);
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.unbindDevice(device_id, callback);
|
||||
@@ -44,11 +44,27 @@ export default {
|
||||
RequestService.clearRequestTime();
|
||||
callback(res);
|
||||
})
|
||||
.fail((err) => {
|
||||
.networkFail((err) => {
|
||||
console.error('绑定设备失败:', err);
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.bindDevice(agentId, deviceCode, callback);
|
||||
});
|
||||
}).send();
|
||||
},
|
||||
enableOtaUpgrade(id, status, callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/device/enableOta/${id}/${status}`)
|
||||
.method('PUT')
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime()
|
||||
callback(res)
|
||||
})
|
||||
.networkFail((err) => {
|
||||
console.error('更新OTA状态失败:', err)
|
||||
this.$message.error(err.msg || '更新OTA状态失败')
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.enableOtaUpgrade(id, status, callback)
|
||||
})
|
||||
}).send()
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
import { getServiceUrl } from '../api';
|
||||
import RequestService from '../httpRequest';
|
||||
|
||||
export default {
|
||||
// 获取字典类型列表
|
||||
getDictTypeList(params, callback) {
|
||||
const queryParams = new URLSearchParams({
|
||||
dictType: params.dictType || '',
|
||||
dictName: params.dictName || '',
|
||||
page: params.page || 1,
|
||||
limit: params.limit || 10
|
||||
}).toString();
|
||||
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/admin/dict/type/page?${queryParams}`)
|
||||
.method('GET')
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime()
|
||||
callback(res)
|
||||
})
|
||||
.networkFail((err) => {
|
||||
console.error('获取字典类型列表失败:', err)
|
||||
this.$message.error(err.msg || '获取字典类型列表失败')
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.getDictTypeList(params, callback)
|
||||
})
|
||||
}).send()
|
||||
},
|
||||
|
||||
// 获取字典类型详情
|
||||
getDictTypeDetail(id, callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/admin/dict/type/${id}`)
|
||||
.method('GET')
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime()
|
||||
callback(res)
|
||||
})
|
||||
.networkFail((err) => {
|
||||
console.error('获取字典类型详情失败:', err)
|
||||
this.$message.error(err.msg || '获取字典类型详情失败')
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.getDictTypeDetail(id, callback)
|
||||
})
|
||||
}).send()
|
||||
},
|
||||
|
||||
// 新增字典类型
|
||||
addDictType(data, callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/admin/dict/type/save`)
|
||||
.method('POST')
|
||||
.data(data)
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime()
|
||||
callback(res)
|
||||
})
|
||||
.networkFail((err) => {
|
||||
console.error('新增字典类型失败:', err)
|
||||
this.$message.error(err.msg || '新增字典类型失败')
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.addDictType(data, callback)
|
||||
})
|
||||
}).send()
|
||||
},
|
||||
|
||||
// 更新字典类型
|
||||
updateDictType(data, callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/admin/dict/type/update`)
|
||||
.method('PUT')
|
||||
.data(data)
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime()
|
||||
callback(res)
|
||||
})
|
||||
.networkFail((err) => {
|
||||
console.error('更新字典类型失败:', err)
|
||||
this.$message.error(err.msg || '更新字典类型失败')
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.updateDictType(data, callback)
|
||||
})
|
||||
}).send()
|
||||
},
|
||||
|
||||
// 删除字典类型
|
||||
deleteDictType(ids, callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/admin/dict/type/delete`)
|
||||
.method('POST')
|
||||
.data(ids)
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime()
|
||||
callback(res)
|
||||
})
|
||||
.networkFail((err) => {
|
||||
console.error('删除字典类型失败:', err)
|
||||
this.$message.error(err.msg || '删除字典类型失败')
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.deleteDictType(ids, callback)
|
||||
})
|
||||
}).send()
|
||||
},
|
||||
|
||||
// 获取字典数据列表
|
||||
getDictDataList(params, callback) {
|
||||
const queryParams = new URLSearchParams({
|
||||
dictTypeId: params.dictTypeId,
|
||||
dictLabel: params.dictLabel || '',
|
||||
dictValue: params.dictValue || '',
|
||||
page: params.page || 1,
|
||||
limit: params.limit || 10
|
||||
}).toString();
|
||||
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/admin/dict/data/page?${queryParams}`)
|
||||
.method('GET')
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime()
|
||||
callback(res)
|
||||
})
|
||||
.networkFail((err) => {
|
||||
console.error('获取字典数据列表失败:', err)
|
||||
this.$message.error(err.msg || '获取字典数据列表失败')
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.getDictDataList(params, callback)
|
||||
})
|
||||
}).send()
|
||||
},
|
||||
|
||||
// 获取字典数据详情
|
||||
getDictDataDetail(id, callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/admin/dict/data/${id}`)
|
||||
.method('GET')
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime()
|
||||
callback(res)
|
||||
})
|
||||
.networkFail((err) => {
|
||||
console.error('获取字典数据详情失败:', err)
|
||||
this.$message.error(err.msg || '获取字典数据详情失败')
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.getDictDataDetail(id, callback)
|
||||
})
|
||||
}).send()
|
||||
},
|
||||
|
||||
// 新增字典数据
|
||||
addDictData(data, callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/admin/dict/data/save`)
|
||||
.method('POST')
|
||||
.data(data)
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime()
|
||||
callback(res)
|
||||
})
|
||||
.networkFail((err) => {
|
||||
console.error('新增字典数据失败:', err)
|
||||
this.$message.error(err.msg || '新增字典数据失败')
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.addDictData(data, callback)
|
||||
})
|
||||
}).send()
|
||||
},
|
||||
|
||||
// 更新字典数据
|
||||
updateDictData(data, callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/admin/dict/data/update`)
|
||||
.method('PUT')
|
||||
.data(data)
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime()
|
||||
callback(res)
|
||||
})
|
||||
.networkFail((err) => {
|
||||
console.error('更新字典数据失败:', err)
|
||||
this.$message.error(err.msg || '更新字典数据失败')
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.updateDictData(data, callback)
|
||||
})
|
||||
}).send()
|
||||
},
|
||||
|
||||
// 删除字典数据
|
||||
deleteDictData(ids, callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/admin/dict/data/delete`)
|
||||
.method('POST')
|
||||
.data(ids)
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime()
|
||||
callback(res)
|
||||
})
|
||||
.networkFail((err) => {
|
||||
console.error('删除字典数据失败:', err)
|
||||
this.$message.error(err.msg || '删除字典数据失败')
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.deleteDictData(ids, callback)
|
||||
})
|
||||
}).send()
|
||||
},
|
||||
|
||||
// 获取字典数据列表
|
||||
getDictDataByType(dictType) {
|
||||
return new Promise((resolve, reject) => {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/admin/dict/data/type/${dictType}`)
|
||||
.method('GET')
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime()
|
||||
if (res.data && res.data.code === 0) {
|
||||
resolve(res.data)
|
||||
} else {
|
||||
reject(new Error(res.data?.msg || '获取字典数据列表失败'))
|
||||
}
|
||||
})
|
||||
.networkFail((err) => {
|
||||
console.error('获取字典数据列表失败:', err)
|
||||
reject(err)
|
||||
}).send()
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import { getServiceUrl } from '../api';
|
||||
import RequestService from '../httpRequest';
|
||||
|
||||
|
||||
export default {
|
||||
// 获取模型配置列表
|
||||
getModelList(params, callback) {
|
||||
@@ -19,7 +18,7 @@ export default {
|
||||
RequestService.clearRequestTime()
|
||||
callback(res)
|
||||
})
|
||||
.fail((err) => {
|
||||
.networkFail((err) => {
|
||||
console.error('获取模型列表失败:', err)
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.getModelList(params, callback)
|
||||
@@ -35,7 +34,7 @@ export default {
|
||||
RequestService.clearRequestTime()
|
||||
callback(res.data?.data || [])
|
||||
})
|
||||
.fail((err) => {
|
||||
.networkFail((err) => {
|
||||
console.error('获取供应器列表失败:', err)
|
||||
this.$message.error('获取供应器列表失败')
|
||||
RequestService.reAjaxFun(() => {
|
||||
@@ -66,7 +65,7 @@ export default {
|
||||
RequestService.clearRequestTime()
|
||||
callback(res)
|
||||
})
|
||||
.fail((err) => {
|
||||
.networkFail((err) => {
|
||||
console.error('新增模型失败:', err)
|
||||
this.$message.error(err.msg || '新增模型失败')
|
||||
RequestService.reAjaxFun(() => {
|
||||
@@ -83,7 +82,7 @@ export default {
|
||||
RequestService.clearRequestTime()
|
||||
callback(res)
|
||||
})
|
||||
.fail((err) => {
|
||||
.networkFail((err) => {
|
||||
console.error('删除模型失败:', err)
|
||||
this.$message.error(err.msg || '删除模型失败')
|
||||
RequestService.reAjaxFun(() => {
|
||||
@@ -101,7 +100,7 @@ export default {
|
||||
RequestService.clearRequestTime();
|
||||
callback(res);
|
||||
})
|
||||
.fail(() => {
|
||||
.networkFail(() => {
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.getModelNames(modelType, modelName, callback);
|
||||
});
|
||||
@@ -119,7 +118,7 @@ export default {
|
||||
RequestService.clearRequestTime();
|
||||
callback(res);
|
||||
})
|
||||
.fail(() => {
|
||||
.networkFail(() => {
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.getModelVoices(modelId, voiceName, callback);
|
||||
});
|
||||
@@ -134,7 +133,7 @@ export default {
|
||||
RequestService.clearRequestTime()
|
||||
callback(res)
|
||||
})
|
||||
.fail((err) => {
|
||||
.networkFail((err) => {
|
||||
console.error('获取模型配置失败:', err)
|
||||
this.$message.error(err.msg || '获取模型配置失败')
|
||||
RequestService.reAjaxFun(() => {
|
||||
@@ -142,40 +141,168 @@ export default {
|
||||
})
|
||||
}).send()
|
||||
},
|
||||
// 启用/禁用模型状态
|
||||
updateModelStatus(id, status, callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/models/enable/${id}/${status}`)
|
||||
.method('PUT')
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime()
|
||||
callback(res)
|
||||
// 启用/禁用模型状态
|
||||
updateModelStatus(id, status, callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/models/enable/${id}/${status}`)
|
||||
.method('PUT')
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime()
|
||||
callback(res)
|
||||
})
|
||||
.networkFail((err) => {
|
||||
console.error('更新模型状态失败:', err)
|
||||
this.$message.error(err.msg || '更新模型状态失败')
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.updateModelStatus(id, status, callback)
|
||||
})
|
||||
.fail((err) => {
|
||||
console.error('更新模型状态失败:', err)
|
||||
this.$message.error(err.msg || '更新模型状态失败')
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.updateModelStatus(id, status, callback)
|
||||
})
|
||||
}).send()
|
||||
},
|
||||
// 更新模型配置
|
||||
updateModel(params, callback) {
|
||||
const { modelType, provideCode, id, formData } = params;
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/models/${modelType}/${provideCode}/${id}`)
|
||||
.method('PUT')
|
||||
.data(formData)
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime();
|
||||
callback(res);
|
||||
}).send()
|
||||
},
|
||||
// 更新模型配置
|
||||
updateModel(params, callback) {
|
||||
const { modelType, provideCode, id, formData } = params;
|
||||
const payload = {
|
||||
...formData,
|
||||
configJson: formData.configJson
|
||||
};
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/models/${modelType}/${provideCode}/${id}`)
|
||||
.method('PUT')
|
||||
.data(payload)
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime();
|
||||
callback(res);
|
||||
})
|
||||
.networkFail((err) => {
|
||||
console.error('更新模型失败:', err);
|
||||
this.$message.error(err.msg || '更新模型失败');
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.updateModel(params, callback);
|
||||
});
|
||||
}).send();
|
||||
},
|
||||
// 设置默认模型
|
||||
setDefaultModel(id, callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/models/default/${id}`)
|
||||
.method('PUT')
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime()
|
||||
callback(res)
|
||||
})
|
||||
.networkFail((err) => {
|
||||
console.error('设置默认模型失败:', err)
|
||||
this.$message.error(err.msg || '设置默认模型失败')
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.setDefaultModel(id, callback)
|
||||
})
|
||||
.fail((err) => {
|
||||
console.error('更新模型失败:', err);
|
||||
this.$message.error(err.msg || '更新模型失败');
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.updateModel(params, callback);
|
||||
});
|
||||
}).send();
|
||||
},
|
||||
}).send()
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取模型配置列表(支持查询参数)
|
||||
* @param {Object} params - 查询参数对象,例如 { name: 'test', modelType: 1 }
|
||||
* @param {Function} callback - 回调函数
|
||||
*/
|
||||
getModelProvidersPage(params, callback) {
|
||||
// 构建查询参数
|
||||
const queryParams = new URLSearchParams();
|
||||
if (params.name) queryParams.append('name', params.name);
|
||||
if (params.modelType !== undefined) queryParams.append('modelType', params.modelType);
|
||||
if (params.page !== undefined) queryParams.append('page', params.page);
|
||||
if (params.limit !== undefined) queryParams.append('limit', params.limit);
|
||||
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/models/provider?${queryParams.toString()}`)
|
||||
.method('GET')
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime();
|
||||
callback(res);
|
||||
})
|
||||
.networkFail((err) => {
|
||||
this.$message.error(err.msg || '获取供应器列表失败');
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.getModelProviders(params, callback);
|
||||
});
|
||||
}).send();
|
||||
},
|
||||
|
||||
/**
|
||||
* 新增模型供应器配置
|
||||
* @param {Object} params - 请求参数对象,例如 { modelType: '1', providerCode: '1', name: '1', fields: '1', sort: 1 }
|
||||
* @param {Function} callback - 成功回调函数
|
||||
*/
|
||||
addModelProvider(params, callback) {
|
||||
const postData = {
|
||||
modelType: params.modelType || '',
|
||||
providerCode: params.providerCode || '',
|
||||
name: params.name || '',
|
||||
fields: JSON.stringify(params.fields || []),
|
||||
sort: params.sort || 0
|
||||
};
|
||||
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/models/provider`)
|
||||
.method('POST')
|
||||
.data(postData)
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime();
|
||||
callback(res);
|
||||
})
|
||||
.networkFail((err) => {
|
||||
console.error('新增模型供应器失败:', err)
|
||||
this.$message.error(err.msg || '新增模型供应器失败')
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.addModelProvider(params, callback);
|
||||
});
|
||||
}).send();
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新模型供应器配置
|
||||
* @param {Object} params - 请求参数对象,例如 { id: '111', modelType: '1', providerCode: '1', name: '1', fields: '1', sort: 1 }
|
||||
* @param {Function} callback - 成功回调函数
|
||||
*/
|
||||
updateModelProvider(params, callback) {
|
||||
const putData = {
|
||||
id: params.id || '',
|
||||
modelType: params.modelType || '',
|
||||
providerCode: params.providerCode || '',
|
||||
name: params.name || '',
|
||||
fields: JSON.stringify(params.fields || []),
|
||||
sort: params.sort || 0
|
||||
};
|
||||
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/models/provider`)
|
||||
.method('PUT')
|
||||
.data(putData)
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime();
|
||||
callback(res);
|
||||
})
|
||||
.networkFail((err) => {
|
||||
this.$message.error(err.msg || '更新模型供应器失败')
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.updateModelProvider(params, callback);
|
||||
});
|
||||
}).send();
|
||||
},
|
||||
// 删除
|
||||
deleteModelProviderByIds(ids, callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/models/provider/delete`)
|
||||
.method('POST')
|
||||
.data(ids)
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime()
|
||||
callback(res);
|
||||
})
|
||||
.networkFail((err) => {
|
||||
this.$message.error(err.msg || '删除模型供应器失败')
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.deleteModelProviderByIds(ids, callback)
|
||||
})
|
||||
}).send()
|
||||
},
|
||||
}
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
import { getServiceUrl } from '../api';
|
||||
import RequestService from '../httpRequest';
|
||||
|
||||
export default {
|
||||
// 分页查询OTA固件信息
|
||||
getOtaList(params, callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/otaMag`)
|
||||
.method('GET')
|
||||
.data(params)
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime();
|
||||
callback(res);
|
||||
})
|
||||
.networkFail((err) => {
|
||||
console.error('获取OTA固件列表失败:', err);
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.getOtaList(params, callback);
|
||||
});
|
||||
}).send();
|
||||
},
|
||||
// 获取单个OTA固件信息
|
||||
getOtaInfo(id, callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/otaMag/${id}`)
|
||||
.method('GET')
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime();
|
||||
callback(res);
|
||||
})
|
||||
.networkFail((err) => {
|
||||
console.error('获取OTA固件信息失败:', err);
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.getOtaInfo(id, callback);
|
||||
});
|
||||
}).send();
|
||||
},
|
||||
// 保存OTA固件信息
|
||||
saveOta(entity, callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/otaMag`)
|
||||
.method('POST')
|
||||
.data(entity)
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime();
|
||||
callback(res);
|
||||
})
|
||||
.networkFail((err) => {
|
||||
console.error('保存OTA固件信息失败:', err);
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.saveOta(entity, callback);
|
||||
});
|
||||
}).send();
|
||||
},
|
||||
// 更新OTA固件信息
|
||||
updateOta(id, entity, callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/otaMag/${id}`)
|
||||
.method('PUT')
|
||||
.data(entity)
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime();
|
||||
callback(res);
|
||||
})
|
||||
.networkFail((err) => {
|
||||
console.error('更新OTA固件信息失败:', err);
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.updateOta(id, entity, callback);
|
||||
});
|
||||
}).send();
|
||||
},
|
||||
// 删除OTA固件
|
||||
deleteOta(id, callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/otaMag/${id}`)
|
||||
.method('DELETE')
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime();
|
||||
callback(res);
|
||||
})
|
||||
.networkFail((err) => {
|
||||
console.error('删除OTA固件失败:', err);
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.deleteOta(id, callback);
|
||||
});
|
||||
}).send();
|
||||
},
|
||||
// 上传固件文件
|
||||
uploadFirmware(file, callback) {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/otaMag/upload`)
|
||||
.method('POST')
|
||||
.data(formData)
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime();
|
||||
callback(res);
|
||||
})
|
||||
.networkFail((err) => {
|
||||
console.error('上传固件文件失败:', err);
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.uploadFirmware(file, callback);
|
||||
});
|
||||
}).send();
|
||||
},
|
||||
// 获取固件下载链接
|
||||
getDownloadUrl(id, callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/otaMag/getDownloadUrl/${id}`)
|
||||
.method('GET')
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime();
|
||||
callback(res);
|
||||
})
|
||||
.networkFail((err) => {
|
||||
console.error('获取下载链接失败:', err);
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.getDownloadUrl(id, callback);
|
||||
});
|
||||
}).send();
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,8 @@
|
||||
// timbre.js
|
||||
import {getServiceUrl} from '../api';
|
||||
import { getServiceUrl } from '../api';
|
||||
import RequestService from '../httpRequest';
|
||||
|
||||
export default {
|
||||
/**
|
||||
* 获取音色数据
|
||||
*/
|
||||
// 获取音色
|
||||
getVoiceList(params, callback) {
|
||||
const queryParams = new URLSearchParams({
|
||||
ttsModelId: params.ttsModelId,
|
||||
@@ -21,16 +18,14 @@ export default {
|
||||
RequestService.clearRequestTime();
|
||||
callback(res.data || []);
|
||||
})
|
||||
.fail((err) => {
|
||||
.networkFail((err) => {
|
||||
console.error('获取音色列表失败:', err);
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.getVoiceList(params, callback);
|
||||
});
|
||||
}).send();
|
||||
},
|
||||
/**
|
||||
* 音色保存
|
||||
*/
|
||||
// 音色保存
|
||||
saveVoice(params, callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/ttsVoice`)
|
||||
@@ -47,36 +42,31 @@ export default {
|
||||
.success((res) => {
|
||||
callback(res.data);
|
||||
})
|
||||
.fail((err) => {
|
||||
.networkFail((err) => {
|
||||
console.error('保存音色失败:', err);
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.saveVoice(params, callback);
|
||||
});
|
||||
}).send();
|
||||
},
|
||||
/**
|
||||
* 音色删除
|
||||
*/
|
||||
deleteVoice(id, callback) {
|
||||
console.log('尝试删除音色,ID:', id);
|
||||
// 音色删除
|
||||
deleteVoice(ids, callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/ttsVoice`)
|
||||
.method('DELETE')
|
||||
.data({ ids: [id] })
|
||||
.url(`${getServiceUrl()}/ttsVoice/delete`)
|
||||
.method('POST')
|
||||
.data(ids)
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime()
|
||||
callback(res);
|
||||
})
|
||||
.fail((err) => {
|
||||
.networkFail((err) => {
|
||||
console.error('删除音色失败:', err);
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.deleteVoice(id, callback);
|
||||
this.deleteVoice(ids, callback);
|
||||
});
|
||||
}).send();
|
||||
},
|
||||
/**
|
||||
* 音色修改
|
||||
*/
|
||||
// 音色修改
|
||||
updateVoice(params, callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/ttsVoice/${params.id}`)
|
||||
@@ -92,7 +82,7 @@ export default {
|
||||
.success((res) => {
|
||||
callback(res.data);
|
||||
})
|
||||
.fail((err) => {
|
||||
.networkFail((err) => {
|
||||
console.error('修改音色失败:', err);
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.updateVoice(params, callback);
|
||||
|
||||
@@ -4,7 +4,7 @@ import RequestService from '../httpRequest'
|
||||
|
||||
export default {
|
||||
// 登录
|
||||
login(loginForm, callback) {
|
||||
login(loginForm, callback, failCallback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/user/login`)
|
||||
.method('POST')
|
||||
@@ -13,7 +13,11 @@ export default {
|
||||
RequestService.clearRequestTime()
|
||||
callback(res)
|
||||
})
|
||||
.fail(() => {
|
||||
.fail((err) => {
|
||||
RequestService.clearRequestTime()
|
||||
failCallback(err)
|
||||
})
|
||||
.networkFail(() => {
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.login(loginForm, callback)
|
||||
})
|
||||
@@ -34,12 +38,32 @@ export default {
|
||||
RequestService.clearRequestTime();
|
||||
callback(res);
|
||||
})
|
||||
.fail((err) => { // 添加错误参数
|
||||
.networkFail((err) => { // 添加错误参数
|
||||
|
||||
}).send()
|
||||
},
|
||||
// 发送短信验证码
|
||||
sendSmsVerification(data, callback, failCallback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/user/smsVerification`)
|
||||
.method('POST')
|
||||
.data(data)
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime()
|
||||
callback(res)
|
||||
})
|
||||
.fail((err) => {
|
||||
RequestService.clearRequestTime()
|
||||
failCallback(err)
|
||||
})
|
||||
.networkFail(() => {
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.sendSmsVerification(data, callback, failCallback)
|
||||
})
|
||||
}).send()
|
||||
},
|
||||
// 注册账号
|
||||
register(registerForm, callback) {
|
||||
register(registerForm, callback, failCallback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/user/register`)
|
||||
.method('POST')
|
||||
@@ -48,7 +72,14 @@ export default {
|
||||
RequestService.clearRequestTime()
|
||||
callback(res)
|
||||
})
|
||||
.fail(() => {
|
||||
.fail((err) => {
|
||||
RequestService.clearRequestTime()
|
||||
failCallback(err)
|
||||
})
|
||||
.networkFail(() => {
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.register(registerForm, callback, failCallback)
|
||||
})
|
||||
}).send()
|
||||
},
|
||||
// 保存设备配置
|
||||
@@ -61,7 +92,7 @@ export default {
|
||||
RequestService.clearRequestTime();
|
||||
callback(res);
|
||||
})
|
||||
.fail((err) => {
|
||||
.networkFail((err) => {
|
||||
console.error('保存配置失败:', err);
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.saveDeviceConfig(device_id, configData, callback);
|
||||
@@ -77,7 +108,7 @@ export default {
|
||||
RequestService.clearRequestTime()
|
||||
callback(res)
|
||||
})
|
||||
.fail((err) => {
|
||||
.networkFail((err) => {
|
||||
console.error('接口请求失败:', err)
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.getUserInfo(callback)
|
||||
@@ -97,7 +128,7 @@ export default {
|
||||
RequestService.clearRequestTime();
|
||||
successCallback(res);
|
||||
})
|
||||
.fail((error) => {
|
||||
.networkFail((error) => {
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.changePassword(oldPassword, newPassword, successCallback, errorCallback);
|
||||
});
|
||||
@@ -106,7 +137,8 @@ export default {
|
||||
},
|
||||
// 修改用户状态
|
||||
changeUserStatus(status, userIds, successCallback) {
|
||||
return RequestService.sendRequest()
|
||||
console.log(555, userIds)
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/admin/users/changeStatus/${status}`)
|
||||
.method('put')
|
||||
.data(userIds)
|
||||
@@ -114,11 +146,51 @@ export default {
|
||||
RequestService.clearRequestTime()
|
||||
successCallback(res);
|
||||
})
|
||||
.fail((err) => {
|
||||
.networkFail((err) => {
|
||||
console.error('修改用户状态失败:', err)
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.changeUserStatus(status, userIds)
|
||||
})
|
||||
}).send()
|
||||
},
|
||||
// 获取公共配置
|
||||
getPubConfig(callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/user/pub-config`)
|
||||
.method('GET')
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime();
|
||||
callback(res);
|
||||
})
|
||||
.networkFail((err) => {
|
||||
console.error('获取公共配置失败:', err);
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.getPubConfig(callback);
|
||||
});
|
||||
}).send();
|
||||
},
|
||||
// 找回用户密码
|
||||
retrievePassword(passwordData, callback, failCallback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/user/retrieve-password`)
|
||||
.method('PUT')
|
||||
.data({
|
||||
phone: passwordData.phone,
|
||||
code: passwordData.code,
|
||||
password: passwordData.password
|
||||
})
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime();
|
||||
callback(res);
|
||||
})
|
||||
.fail((err) => {
|
||||
RequestService.clearRequestTime();
|
||||
failCallback(err);
|
||||
})
|
||||
.networkFail(() => {
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.retrievePassword(passwordData, callback, failCallback);
|
||||
});
|
||||
}).send()
|
||||
}
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 101 KiB |
|
Before Width: | Height: | Size: 124 KiB After Width: | Height: | Size: 88 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 8.1 KiB |
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<el-dialog :visible.sync="visible" width="400px" center>
|
||||
<el-dialog :visible="visible" @close="handleClose" width="24%" 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
|
||||
@@ -79,7 +79,10 @@ export default {
|
||||
cancel() {
|
||||
this.$emit('update:visible', false)
|
||||
this.deviceCode = ""
|
||||
}
|
||||
},
|
||||
handleClose() {
|
||||
this.$emit('update:visible', false);
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
<template>
|
||||
<el-dialog :visible="dialogVisible" @update:visible="handleVisibleChange" width="975px" center custom-class="custom-dialog" :show-close="false"
|
||||
class="center-dialog">
|
||||
<el-dialog :visible="dialogVisible" @update:visible="handleVisibleChange" width="57%" 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>
|
||||
@@ -19,7 +18,7 @@
|
||||
<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;">
|
||||
<div style="display: none; align-items: center;">
|
||||
<span style="margin-right: 8px;">设为默认</span>
|
||||
<el-switch v-model="formData.isDefault" class="custom-switch"></el-switch>
|
||||
</div>
|
||||
@@ -45,7 +44,7 @@
|
||||
</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-input v-model="formData.sort" type="number" placeholder="请输入排序号" class="custom-input-bg"></el-input>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
@@ -55,7 +54,7 @@
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="备注" prop="remark" class="prop-remark">
|
||||
<el-input v-model="formData.remark" type="textarea" :rows="3" placeholder="请输入模型备注"
|
||||
<el-input v-model="formData.remark" type="textarea" :rows="3" placeholder="请输入模型备注" :autosize="{ minRows: 3, maxRows: 5 }"
|
||||
class="custom-input-bg"></el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
@@ -66,18 +65,10 @@
|
||||
<el-form :model="formData.configJson" label-width="auto" label-position="left" class="custom-form">
|
||||
<template v-for="(row, rowIndex) in chunkedCallInfoFields">
|
||||
<div :key="rowIndex" style="display: flex; gap: 20px; margin-bottom: 0;">
|
||||
<el-form-item
|
||||
v-for="field in row"
|
||||
:key="field.prop"
|
||||
:label="field.label"
|
||||
:prop="field.prop"
|
||||
<el-form-item v-for="field in row" :key="field.prop" :label="field.label" :prop="field.prop"
|
||||
style="flex: 1;">
|
||||
<el-input
|
||||
v-model="formData.configJson[field.prop]"
|
||||
:placeholder="field.placeholder"
|
||||
:type="field.type || 'text'"
|
||||
class="custom-input-bg"
|
||||
:show-password="field.type === 'password'">
|
||||
<el-input v-model="formData.configJson[field.prop]" :placeholder="field.placeholder"
|
||||
:type="field.type || 'text'" class="custom-input-bg" :show-password="field.type === 'password'">
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</div>
|
||||
@@ -86,7 +77,12 @@
|
||||
</div>
|
||||
|
||||
<div style="display: flex;justify-content: center;">
|
||||
<el-button type="primary" @click="confirm" class="save-btn">
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="confirm"
|
||||
class="save-btn"
|
||||
:loading="saving"
|
||||
:disabled="saving">
|
||||
保存
|
||||
</el-button>
|
||||
</div>
|
||||
@@ -103,6 +99,7 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
saving: false,
|
||||
providers: [],
|
||||
dialogVisible: false,
|
||||
providersLoaded: false,
|
||||
@@ -124,7 +121,7 @@ export default {
|
||||
watch: {
|
||||
visible(val) {
|
||||
this.dialogVisible = val;
|
||||
if(val) {
|
||||
if (val) {
|
||||
this.initConfigJson();
|
||||
} else {
|
||||
this.resetForm();
|
||||
@@ -184,6 +181,7 @@ export default {
|
||||
},
|
||||
|
||||
handleClose() {
|
||||
this.saving = false;
|
||||
this.$emit('update:visible', false);
|
||||
},
|
||||
initDynamicConfig() {
|
||||
@@ -194,8 +192,11 @@ export default {
|
||||
this.formData.configJson = newConfig;
|
||||
},
|
||||
confirm() {
|
||||
this.saving = true;
|
||||
|
||||
if (!this.formData.supplier) {
|
||||
this.$message.error('请选择供应器');
|
||||
this.saving = false;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -215,11 +216,18 @@ export default {
|
||||
}
|
||||
};
|
||||
|
||||
this.$emit('confirm', submitData);
|
||||
this.$emit('update:visible', false);
|
||||
this.resetForm();
|
||||
try {
|
||||
this.$emit('confirm', submitData);
|
||||
this.$emit('update:visible', false);
|
||||
this.resetForm();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
},
|
||||
resetForm() {
|
||||
this.saving = false;
|
||||
this.formData = {
|
||||
modelName: '',
|
||||
modelCode: '',
|
||||
@@ -263,7 +271,7 @@ export default {
|
||||
}
|
||||
|
||||
.center-dialog .el-dialog {
|
||||
margin: 4% 0 auto !important;
|
||||
margin: 0 0 auto !important;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<el-dialog :visible.sync="visible" width="400px" center @open="handleOpen">
|
||||
<el-dialog :visible="visible" @close="handleClose" width="25%" center @open="handleOpen">
|
||||
<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
|
||||
@@ -10,8 +10,8 @@
|
||||
</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 style="font-weight: 400;text-align: left;color: #3d4566;">
|
||||
<div style="color: red;display: inline-block;">*</div> 智能体名称:
|
||||
</div>
|
||||
<div class="input-46" style="margin-top: 12px;">
|
||||
<el-input ref="inputRef" placeholder="请输入智能体名称.." v-model="wisdomBodyName" @keyup.enter.native="confirm" />
|
||||
@@ -66,7 +66,10 @@ export default {
|
||||
cancel() {
|
||||
this.$emit('update:visible', false)
|
||||
this.wisdomBodyName = ""
|
||||
}
|
||||
},
|
||||
handleClose() {
|
||||
this.$emit('update:visible', false);
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
title="CDN资源缓存状态"
|
||||
:visible.sync="visible"
|
||||
width="70%"
|
||||
:before-close="handleClose"
|
||||
>
|
||||
<div v-if="isLoading" class="loading-container">
|
||||
<p>正在加载缓存信息...</p>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<div v-if="!cacheAvailable" class="no-cache-message">
|
||||
<i class="el-icon-warning-outline"></i>
|
||||
<p>您的浏览器不支持Cache API或Service Worker未安装</p>
|
||||
<el-button type="primary" @click="refreshPage">刷新页面</el-button>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<el-alert
|
||||
v-if="cacheData.totalCached === 0"
|
||||
title="未发现缓存的CDN资源"
|
||||
type="warning"
|
||||
:closable="false"
|
||||
show-icon
|
||||
>
|
||||
<p>Service Worker可能尚未完成初始化或缓存尚未建立。请刷新页面或等待一会后再试。</p>
|
||||
</el-alert>
|
||||
|
||||
<div v-else>
|
||||
<el-alert
|
||||
title="CDN资源缓存状态"
|
||||
type="success"
|
||||
:closable="false"
|
||||
show-icon
|
||||
>
|
||||
共发现 {{ cacheData.totalCached }} 个缓存资源
|
||||
</el-alert>
|
||||
|
||||
<h3>JavaScript 资源 ({{ cacheData.js.length }})</h3>
|
||||
<el-table :data="cacheData.js" stripe style="width: 100%">
|
||||
<el-table-column prop="url" label="URL" width="auto" show-overflow-tooltip />
|
||||
<el-table-column prop="cached" label="状态" width="100">
|
||||
<template slot-scope="scope">
|
||||
<el-tag type="success" v-if="scope.row.cached">已缓存</el-tag>
|
||||
<el-tag type="danger" v-else>未缓存</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<h3>CSS 资源 ({{ cacheData.css.length }})</h3>
|
||||
<el-table :data="cacheData.css" stripe style="width: 100%">
|
||||
<el-table-column prop="url" label="URL" width="auto" show-overflow-tooltip />
|
||||
<el-table-column prop="cached" label="状态" width="100">
|
||||
<template slot-scope="scope">
|
||||
<el-tag type="success" v-if="scope.row.cached">已缓存</el-tag>
|
||||
<el-tag type="danger" v-else>未缓存</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button @click="handleClose">关闭</el-button>
|
||||
<el-button type="primary" @click="refreshCache">刷新缓存状态</el-button>
|
||||
<el-button type="danger" @click="clearCache">清除缓存</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
getCacheNames,
|
||||
checkCdnCacheStatus,
|
||||
clearAllCaches,
|
||||
logCacheStatus
|
||||
} from '../utils/cacheViewer';
|
||||
|
||||
export default {
|
||||
name: 'CacheViewer',
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isLoading: true,
|
||||
cacheAvailable: false,
|
||||
cacheData: {
|
||||
css: [],
|
||||
js: [],
|
||||
totalCached: 0,
|
||||
totalNotCached: 0
|
||||
}
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
visible(newVal) {
|
||||
if (newVal) {
|
||||
this.loadCacheData();
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async loadCacheData() {
|
||||
this.isLoading = true;
|
||||
|
||||
try {
|
||||
// 先检查是否支持缓存API
|
||||
if (!('caches' in window)) {
|
||||
this.cacheAvailable = false;
|
||||
this.isLoading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否有Service Worker缓存
|
||||
const cacheNames = await getCacheNames();
|
||||
this.cacheAvailable = cacheNames.length > 0;
|
||||
|
||||
if (this.cacheAvailable) {
|
||||
// 获取CDN缓存状态
|
||||
this.cacheData = await checkCdnCacheStatus();
|
||||
|
||||
// 在控制台输出完整缓存状态
|
||||
await logCacheStatus();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载缓存数据失败:', error);
|
||||
this.$message.error('加载缓存数据失败');
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async refreshCache() {
|
||||
this.loadCacheData();
|
||||
this.$message.success('正在刷新缓存状态');
|
||||
},
|
||||
|
||||
async clearCache() {
|
||||
this.$confirm('确定要清除所有缓存吗?', '警告', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
try {
|
||||
const success = await clearAllCaches();
|
||||
if (success) {
|
||||
this.$message.success('缓存已清除');
|
||||
await this.loadCacheData();
|
||||
} else {
|
||||
this.$message.error('清除缓存失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('清除缓存失败:', error);
|
||||
this.$message.error('清除缓存失败');
|
||||
}
|
||||
}).catch(() => {
|
||||
this.$message.info('已取消清除');
|
||||
});
|
||||
},
|
||||
|
||||
refreshPage() {
|
||||
window.location.reload();
|
||||
},
|
||||
|
||||
handleClose() {
|
||||
this.$emit('update:visible', false);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.loading-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.no-cache-message {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.no-cache-message i {
|
||||
font-size: 48px;
|
||||
color: #E6A23C;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-top: 20px;
|
||||
margin-bottom: 10px;
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<form>
|
||||
<el-dialog :visible.sync="value" width="400px" center>
|
||||
<el-dialog :visible.sync="dialogVisible" width="24%" 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
|
||||
@@ -60,11 +60,20 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dialogVisible: this.value,
|
||||
oldPassword: "",
|
||||
newPassword: "",
|
||||
confirmNewPassword: ""
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value(val) {
|
||||
this.dialogVisible = val;
|
||||
},
|
||||
dialogVisible(val) {
|
||||
this.$emit('input', val);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['logout']), // 引入Vuex的logout action
|
||||
confirm() {
|
||||
@@ -101,7 +110,7 @@ export default {
|
||||
this.$emit('input', false);
|
||||
},
|
||||
cancel() {
|
||||
this.$emit('input', false);
|
||||
this.dialogVisible = false;
|
||||
this.resetForm();
|
||||
},
|
||||
resetForm() {
|
||||
|
||||
@@ -0,0 +1,447 @@
|
||||
<template>
|
||||
<el-dialog :title="'与' + agentName + '的聊天记录' + (currentMacAddress ? '[' + currentMacAddress + ']' : '')"
|
||||
:visible.sync="dialogVisible" width="80%" :before-close="handleClose" custom-class="chat-history-dialog">
|
||||
<div class="chat-container">
|
||||
<div class="session-list" @scroll="handleScroll">
|
||||
<div v-for="session in sessions" :key="session.sessionId" class="session-item"
|
||||
:class="{ active: currentSessionId === session.sessionId }" @click="selectSession(session)">
|
||||
<img :src="getUserAvatar(session.sessionId)" class="avatar" />
|
||||
<div class="session-info">
|
||||
<div class="session-time">{{ formatTime(session.createdAt) }}</div>
|
||||
<div class="message-count">{{ session.chatCount > 99 ? '99' : session.chatCount }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="loading" class="loading">加载中...</div>
|
||||
<div v-if="!hasMore" class="no-more">没有更多记录了</div>
|
||||
</div>
|
||||
<div class="chat-content">
|
||||
<div v-if="currentSessionId" class="messages">
|
||||
<div v-for="(message, index) in messagesWithTime" :key="message.id">
|
||||
<div v-if="message.type === 'time'" class="time-divider">
|
||||
{{ message.content }}
|
||||
</div>
|
||||
<div v-else class="message-item" :class="{ 'user-message': message.chatType === 1 }">
|
||||
<img :src="message.chatType === 1 ? getUserAvatar(currentSessionId) : require('@/assets/xiaozhi-logo.png')"
|
||||
class="avatar" />
|
||||
<div class="message-content">
|
||||
{{ message.content }}
|
||||
<i v-if="message.audioId" :class="getAudioIconClass(message)"
|
||||
@click="playAudio(message)" class="audio-icon"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="no-session-selected">
|
||||
请选择会话查看聊天记录
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Api from '@/apis/api';
|
||||
|
||||
export default {
|
||||
name: 'ChatHistoryDialog',
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
agentId: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
agentName: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dialogVisible: false,
|
||||
sessions: [],
|
||||
messages: [],
|
||||
currentSessionId: '',
|
||||
currentMacAddress: '',
|
||||
page: 1,
|
||||
limit: 20,
|
||||
loading: false,
|
||||
hasMore: true,
|
||||
scrollTimer: null,
|
||||
isFirstLoad: true,
|
||||
playingAudioId: null,
|
||||
audioElement: null
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
visible(val) {
|
||||
this.dialogVisible = val;
|
||||
if (val) {
|
||||
this.resetData();
|
||||
this.loadSessions();
|
||||
}
|
||||
},
|
||||
dialogVisible(val) {
|
||||
if (!val) {
|
||||
this.$emit('update:visible', false);
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
messagesWithTime() {
|
||||
if (!this.messages || this.messages.length === 0) return [];
|
||||
|
||||
const result = [];
|
||||
const TIME_INTERVAL = 60 * 1000; // 1分钟的时间间隔(毫秒)
|
||||
|
||||
// 添加第一条消息的时间标记
|
||||
if (this.messages[0]) {
|
||||
result.push({
|
||||
type: 'time',
|
||||
content: this.formatTime(this.messages[0].createdAt),
|
||||
id: `time-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
|
||||
});
|
||||
}
|
||||
|
||||
// 处理消息列表
|
||||
for (let i = 0; i < this.messages.length; i++) {
|
||||
const currentMessage = this.messages[i];
|
||||
result.push(currentMessage);
|
||||
|
||||
// 检查是否需要添加时间标记
|
||||
if (i < this.messages.length - 1) {
|
||||
const currentTime = new Date(currentMessage.createdAt).getTime();
|
||||
const nextTime = new Date(this.messages[i + 1].createdAt).getTime();
|
||||
|
||||
if (nextTime - currentTime > TIME_INTERVAL) {
|
||||
result.push({
|
||||
type: 'time',
|
||||
content: this.formatTime(this.messages[i + 1].createdAt),
|
||||
id: `time-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
resetData() {
|
||||
this.sessions = [];
|
||||
this.messages = [];
|
||||
this.currentSessionId = '';
|
||||
this.currentMacAddress = '';
|
||||
this.page = 1;
|
||||
this.loading = false;
|
||||
this.hasMore = true;
|
||||
this.isFirstLoad = true;
|
||||
},
|
||||
handleClose() {
|
||||
this.dialogVisible = false;
|
||||
},
|
||||
loadSessions() {
|
||||
if (this.loading || (!this.isFirstLoad && !this.hasMore)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
const params = {
|
||||
page: this.page,
|
||||
limit: this.limit
|
||||
};
|
||||
|
||||
Api.agent.getAgentSessions(this.agentId, params, (res) => {
|
||||
if (res.data && res.data.data && Array.isArray(res.data.data.list)) {
|
||||
const list = res.data.data.list;
|
||||
this.hasMore = list.length === this.limit;
|
||||
|
||||
this.sessions = [...this.sessions, ...list];
|
||||
this.page++;
|
||||
|
||||
if (this.sessions.length > 0 && !this.currentSessionId) {
|
||||
this.selectSession(this.sessions[0]);
|
||||
}
|
||||
}
|
||||
this.loading = false;
|
||||
this.isFirstLoad = false;
|
||||
});
|
||||
},
|
||||
selectSession(session) {
|
||||
this.currentSessionId = session.sessionId;
|
||||
Api.agent.getAgentChatHistory(this.agentId, session.sessionId, (res) => {
|
||||
if (res.data && res.data.data) {
|
||||
this.messages = res.data.data;
|
||||
if (this.messages.length > 0 && this.messages[0].macAddress) {
|
||||
this.currentMacAddress = this.messages[0].macAddress;
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
handleScroll(e) {
|
||||
if (this.scrollTimer) {
|
||||
clearTimeout(this.scrollTimer);
|
||||
}
|
||||
|
||||
this.scrollTimer = setTimeout(() => {
|
||||
const { scrollTop, scrollHeight, clientHeight } = e.target;
|
||||
// 当滚动到底部时加载更多
|
||||
if (scrollHeight - scrollTop <= clientHeight + 50) {
|
||||
this.loadSessions();
|
||||
}
|
||||
}, 200);
|
||||
},
|
||||
formatTime(timestamp) {
|
||||
const date = new Date(timestamp);
|
||||
const now = new Date();
|
||||
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||||
const yesterday = new Date(today);
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
|
||||
const hours = date.getHours().toString().padStart(2, '0');
|
||||
const minutes = date.getMinutes().toString().padStart(2, '0');
|
||||
|
||||
if (date >= today) {
|
||||
return `今天 ${hours}:${minutes}`;
|
||||
} else if (date >= yesterday) {
|
||||
return `昨天 ${hours}:${minutes}`;
|
||||
} else {
|
||||
const year = date.getFullYear();
|
||||
const month = (date.getMonth() + 1).toString().padStart(2, '0');
|
||||
const day = date.getDate().toString().padStart(2, '0');
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}`;
|
||||
}
|
||||
},
|
||||
getAudioIconClass(message) {
|
||||
if (this.playingAudioId === message.audioId) {
|
||||
return 'el-icon-loading';
|
||||
}
|
||||
return 'el-icon-video-play';
|
||||
},
|
||||
playAudio(message) {
|
||||
if (this.playingAudioId === message.audioId) {
|
||||
// 如果正在播放当前音频,则停止播放
|
||||
if (this.audioElement) {
|
||||
this.audioElement.pause();
|
||||
this.audioElement = null;
|
||||
}
|
||||
this.playingAudioId = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// 停止当前正在播放的音频
|
||||
if (this.audioElement) {
|
||||
this.audioElement.pause();
|
||||
this.audioElement = null;
|
||||
}
|
||||
|
||||
// 先获取音频下载ID
|
||||
this.playingAudioId = message.audioId;
|
||||
Api.agent.getAudioId(message.audioId, (res) => {
|
||||
if (res.data && res.data.data) {
|
||||
// 使用获取到的下载ID播放音频
|
||||
this.audioElement = new Audio(Api.getServiceUrl() + `/agent/play/${res.data.data}`);
|
||||
|
||||
this.audioElement.onended = () => {
|
||||
this.playingAudioId = null;
|
||||
this.audioElement = null;
|
||||
};
|
||||
|
||||
this.audioElement.play();
|
||||
}
|
||||
});
|
||||
},
|
||||
getUserAvatar(sessionId) {
|
||||
// 从 sessionId 中提取所有数字
|
||||
const numbers = sessionId.match(/\d+/g);
|
||||
if (!numbers) return require('@/assets/user-avatar1.png');
|
||||
|
||||
// 将所有数字相加
|
||||
const sum = numbers.reduce((acc, num) => acc + parseInt(num), 0);
|
||||
|
||||
// 计算模5并加1,得到1-5之间的数字
|
||||
const avatarIndex = (sum % 5) + 1;
|
||||
|
||||
// 返回对应的头像图片
|
||||
return require(`@/assets/user-avatar${avatarIndex}.png`);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.chat-container {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.session-list {
|
||||
width: 250px;
|
||||
border-right: 1px solid #eee;
|
||||
overflow-y: auto;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.session-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
cursor: pointer;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.session-item:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.session-item.active {
|
||||
background-color: #e6f7ff;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.session-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.session-time {
|
||||
font-size: 14px;
|
||||
color: #272727;
|
||||
float: left;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
width: calc(100% - 30px);
|
||||
/* 为消息数量留出空间 */
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.message-count {
|
||||
font-size: 14px;
|
||||
color: #fff;
|
||||
background-color: #b4b4b4;
|
||||
border-radius: 20px;
|
||||
float: left;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
margin-top: 5px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.chat-content {
|
||||
flex: 1;
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.message-item {
|
||||
display: flex;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.message-item.user-message {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
.message-content {
|
||||
max-width: 60%;
|
||||
padding: 10px 15px;
|
||||
border-radius: 8px;
|
||||
background-color: #f0f0f0;
|
||||
margin: 0 10px;
|
||||
text-align: left;
|
||||
line-height: 20px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.audio-icon {
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
margin: 0 5px;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.user-message .message-content {
|
||||
background-color: #1890ff;
|
||||
color: white;
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
.user-message .audio-icon {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.loading,
|
||||
.no-more {
|
||||
text-align: center;
|
||||
padding: 10px 10px 30px 10px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.no-session-selected {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.time-divider {
|
||||
text-align: center;
|
||||
margin: 10px 0;
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.time-divider::before,
|
||||
.time-divider::after {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: 30%;
|
||||
height: 1px;
|
||||
background-color: #eee;
|
||||
vertical-align: middle;
|
||||
margin: 0 10px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
.chat-history-dialog {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 700px;
|
||||
margin: 0 !important;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
height: 90vh;
|
||||
max-width: 85vw;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.chat-history-dialog .el-dialog__header {
|
||||
background-color: #e6f7ff;
|
||||
padding: 15px 20px;
|
||||
}
|
||||
|
||||
.chat-history-dialog .el-dialog__body {
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
height: calc(90vh - 54px);
|
||||
/* 减去标题栏的高度 */
|
||||
}
|
||||
</style>
|
||||
@@ -23,18 +23,19 @@
|
||||
<div class="settings-btn" @click="handleConfigure">
|
||||
配置角色
|
||||
</div>
|
||||
<!-- <div class="settings-btn">-->
|
||||
<!-- 声纹识别-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="settings-btn">-->
|
||||
<!-- 历史对话-->
|
||||
<!-- </div>-->
|
||||
<div class="settings-btn" @click="handleDeviceManage">
|
||||
设备管理({{ device.deviceCount }})
|
||||
</div>
|
||||
<div class="settings-btn" @click="handleChatHistory"
|
||||
:class="{ 'disabled-btn': device.memModelId === 'Memory_nomem' }">
|
||||
<el-tooltip v-if="device.memModelId === 'Memory_nomem'" content="未开启记忆" placement="top">
|
||||
<span>聊天记录</span>
|
||||
</el-tooltip>
|
||||
<span v-else>聊天记录</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="version-info">
|
||||
<div>最近对话:{{ device.lastConnectedAt }}</div>
|
||||
<div>最近对话:{{ formattedLastConnectedTime }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -48,6 +49,27 @@ export default {
|
||||
data() {
|
||||
return { switchValue: false }
|
||||
},
|
||||
computed: {
|
||||
formattedLastConnectedTime() {
|
||||
if (!this.device.lastConnectedAt) return '暂未对话';
|
||||
|
||||
const lastTime = new Date(this.device.lastConnectedAt);
|
||||
const now = new Date();
|
||||
const diffMinutes = Math.floor((now - lastTime) / (1000 * 60));
|
||||
|
||||
if (diffMinutes <= 1) {
|
||||
return '刚刚';
|
||||
} else if (diffMinutes < 60) {
|
||||
return `${diffMinutes}分钟前`;
|
||||
} else if (diffMinutes < 24 * 60) {
|
||||
const hours = Math.floor(diffMinutes / 60);
|
||||
const minutes = diffMinutes % 60;
|
||||
return `${hours}小时${minutes > 0 ? minutes + '分钟' : ''}前`;
|
||||
} else {
|
||||
return this.device.lastConnectedAt;
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleDelete() {
|
||||
this.$emit('delete', this.device.agentId)
|
||||
@@ -57,6 +79,12 @@ export default {
|
||||
},
|
||||
handleDeviceManage() {
|
||||
this.$router.push({ path: '/device-management', query: { agentId: this.device.agentId } });
|
||||
},
|
||||
handleChatHistory() {
|
||||
if (this.device.memModelId === 'Memory_nomem') {
|
||||
return
|
||||
}
|
||||
this.$emit('chat-history', { agentId: this.device.agentId, agentName: this.device.agentName })
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -80,10 +108,11 @@ export default {
|
||||
|
||||
.settings-btn {
|
||||
font-weight: 500;
|
||||
font-size: 10px;
|
||||
font-size: 12px;
|
||||
color: #5778ff;
|
||||
background: #e6ebff;
|
||||
width: 57px;
|
||||
width: auto;
|
||||
padding: 0 12px;
|
||||
height: 21px;
|
||||
line-height: 21px;
|
||||
cursor: pointer;
|
||||
@@ -94,10 +123,16 @@ export default {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 15px;
|
||||
font-size: 10px;
|
||||
font-size: 12px;
|
||||
color: #979db1;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.disabled-btn {
|
||||
background: #e6e6e6;
|
||||
color: #999;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
<template>
|
||||
<el-dialog :title="title" :visible.sync="dialogVisible" width="30%" @close="handleClose">
|
||||
<el-form :model="form" :rules="rules" ref="form" label-width="100px">
|
||||
<el-form-item label="字典标签" prop="dictLabel">
|
||||
<el-input v-model="form.dictLabel" placeholder="请输入字典标签"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="字典值" prop="dictValue">
|
||||
<el-input v-model="form.dictValue" placeholder="请输入字典值"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序" prop="sort">
|
||||
<el-input-number v-model="form.sort" :min="0" :max="999" style="width: 100%;"></el-input-number>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button @click="handleClose">取 消</el-button>
|
||||
<el-button type="primary" @click="handleSave">确 定</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'DictDataDialog',
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: '新增字典数据'
|
||||
},
|
||||
dictData: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
dictTypeId: {
|
||||
type: [Number, String],
|
||||
default: null
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dialogVisible: this.visible,
|
||||
form: {
|
||||
id: null,
|
||||
dictTypeId: null,
|
||||
dictLabel: '',
|
||||
dictValue: '',
|
||||
sort: 0
|
||||
},
|
||||
rules: {
|
||||
dictLabel: [{ required: true, message: '请输入字典标签', trigger: 'blur' }],
|
||||
dictValue: [{ required: true, message: '请输入字典值', trigger: 'blur' }]
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
dictData: {
|
||||
handler(val) {
|
||||
if (val) {
|
||||
this.form = { ...val }
|
||||
}
|
||||
},
|
||||
immediate: true
|
||||
},
|
||||
dictTypeId: {
|
||||
handler(val) {
|
||||
if (val) {
|
||||
this.form.dictTypeId = val
|
||||
}
|
||||
},
|
||||
immediate: true
|
||||
},
|
||||
visible(val) {
|
||||
this.dialogVisible = val;
|
||||
},
|
||||
dialogVisible(val) {
|
||||
this.$emit('update:visible', val);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleClose() {
|
||||
this.dialogVisible = false;
|
||||
this.resetForm();
|
||||
},
|
||||
resetForm() {
|
||||
this.form = {
|
||||
id: null,
|
||||
dictTypeId: this.dictTypeId,
|
||||
dictLabel: '',
|
||||
dictValue: '',
|
||||
sort: 0
|
||||
}
|
||||
this.$refs.form?.resetFields()
|
||||
},
|
||||
handleSave() {
|
||||
this.$refs.form.validate(valid => {
|
||||
if (valid) {
|
||||
this.$emit('save', this.form)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dialog-footer {
|
||||
text-align: right;
|
||||
}
|
||||
:deep(.el-dialog) {
|
||||
border-radius: 15px;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,97 @@
|
||||
<template>
|
||||
<el-dialog :title="title" :visible.sync="dialogVisible" width="30%" @close="handleClose">
|
||||
<el-form :model="form" :rules="rules" ref="form" label-width="120px">
|
||||
<el-form-item label="字典类型名称" prop="dictName">
|
||||
<el-input v-model="form.dictName" placeholder="请输入字典类型名称"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="字典类型编码" prop="dictType">
|
||||
<el-input v-model="form.dictType" placeholder="请输入字典类型编码"></el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button @click="handleClose">取 消</el-button>
|
||||
<el-button type="primary" @click="handleSave">确 定</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'DictTypeDialog',
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: '新增字典类型'
|
||||
},
|
||||
dictTypeData: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dialogVisible: this.visible,
|
||||
form: {
|
||||
id: null,
|
||||
dictName: '',
|
||||
dictType: ''
|
||||
},
|
||||
rules: {
|
||||
dictName: [{ required: true, message: '请输入字典类型名称', trigger: 'blur' }],
|
||||
dictType: [{ required: true, message: '请输入字典类型编码', trigger: 'blur' }]
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
visible(val) {
|
||||
this.dialogVisible = val;
|
||||
},
|
||||
dialogVisible(val) {
|
||||
this.$emit('update:visible', val);
|
||||
},
|
||||
dictTypeData: {
|
||||
handler(val) {
|
||||
if (val) {
|
||||
this.form = { ...val }
|
||||
}
|
||||
},
|
||||
immediate: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleClose() {
|
||||
this.dialogVisible = false;
|
||||
this.resetForm()
|
||||
},
|
||||
resetForm() {
|
||||
this.form = {
|
||||
id: null,
|
||||
dictName: '',
|
||||
dictType: ''
|
||||
}
|
||||
this.$refs.form?.resetFields()
|
||||
},
|
||||
handleSave() {
|
||||
this.$refs.form.validate(valid => {
|
||||
if (valid) {
|
||||
this.$emit('save', this.form)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dialog-footer {
|
||||
text-align: right;
|
||||
}
|
||||
:deep(.el-dialog) {
|
||||
border-radius: 15px;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,234 @@
|
||||
<template>
|
||||
<el-dialog :title="title" :visible.sync="dialogVisible" @close="handleClose" @open="handleOpen">
|
||||
<el-form ref="form" :model="form" :rules="rules" label-width="100px">
|
||||
<el-form-item label="固件名称" prop="firmwareName">
|
||||
<el-input v-model="form.firmwareName" placeholder="请输入固件名称(板子+版本号)"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="固件类型" prop="type">
|
||||
<el-select v-model="form.type" placeholder="请选择固件类型" style="width: 100%;" filterable :disabled="isTypeDisabled">
|
||||
<el-option v-for="item in firmwareTypes" :key="item.key" :label="item.name" :value="item.key"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="版本号" prop="version">
|
||||
<el-input v-model="form.version" placeholder="请输入版本号(x.x.x格式)"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="固件文件" prop="firmwarePath">
|
||||
<el-upload ref="upload" class="upload-demo" action="#" :http-request="handleUpload"
|
||||
:before-upload="beforeUpload" :accept="'.bin,.apk'" :limit="1" :multiple="false" :auto-upload="true"
|
||||
:on-remove="handleRemove">
|
||||
<el-button size="small" type="primary">点击上传</el-button>
|
||||
<div slot="tip" class="el-upload__tip">只能上传固件文件(.bin/.apk),且不超过100MB</div>
|
||||
</el-upload>
|
||||
<el-progress v-if="isUploading || uploadStatus === 'success'" :percentage="uploadProgress"
|
||||
:status="uploadStatus"></el-progress>
|
||||
<div class="hint-text">
|
||||
<span>温馨提示:请上传合并前的xiaozhi.bin文件,而不是合并后的merged-binary.bin文件</span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input type="textarea" v-model="form.remark" placeholder="请输入备注信息"></el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button @click="handleCancel">取 消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">确 定</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Api from '@/apis/api';
|
||||
|
||||
export default {
|
||||
name: 'FirmwareDialog',
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
form: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
firmwareTypes: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
uploadProgress: 0,
|
||||
uploadStatus: '',
|
||||
isUploading: false,
|
||||
dialogVisible: this.visible,
|
||||
rules: {
|
||||
firmwareName: [
|
||||
{ required: true, message: '请输入固件名称(板子+版本号)', trigger: 'blur' }
|
||||
],
|
||||
type: [
|
||||
{ required: true, message: '请选择固件类型', trigger: 'change' }
|
||||
],
|
||||
version: [
|
||||
{ required: true, message: '请输入版本号', trigger: 'blur' },
|
||||
{ pattern: /^\d+\.\d+\.\d+$/, message: '版本号格式不正确,请输入x.x.x格式', trigger: 'blur' }
|
||||
],
|
||||
firmwarePath: [
|
||||
{ required: false, message: '请上传固件文件', trigger: 'change' }
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isTypeDisabled() {
|
||||
// 如果有id,说明是编辑模式,禁用类型选择
|
||||
return !!this.form.id
|
||||
}
|
||||
},
|
||||
created() {
|
||||
// 移除 getDictDataByType 调用
|
||||
},
|
||||
watch: {
|
||||
visible(val) {
|
||||
this.dialogVisible = val;
|
||||
},
|
||||
dialogVisible(val) {
|
||||
this.$emit('update:visible', val);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
// 移除 getFirmwareTypes 方法
|
||||
handleClose() {
|
||||
this.dialogVisible = false;
|
||||
this.$emit('cancel');
|
||||
},
|
||||
handleCancel() {
|
||||
this.$refs.form.clearValidate();
|
||||
this.$emit('cancel');
|
||||
},
|
||||
handleSubmit() {
|
||||
this.$refs.form.validate(valid => {
|
||||
if (valid) {
|
||||
// 如果是新增模式且没有上传文件,则提示错误
|
||||
if (!this.form.id && !this.form.firmwarePath) {
|
||||
this.$message.error('请上传固件文件')
|
||||
return
|
||||
}
|
||||
// 提交成功后将关闭对话框的逻辑交给父组件处理
|
||||
this.$emit('submit', this.form)
|
||||
}
|
||||
})
|
||||
},
|
||||
beforeUpload(file) {
|
||||
const isValidSize = file.size / 1024 / 1024 < 100
|
||||
const isValidType = ['.bin', '.apk'].some(ext => file.name.toLowerCase().endsWith(ext))
|
||||
|
||||
if (!isValidType) {
|
||||
this.$message.error('只能上传.bin/.apk格式的固件文件!')
|
||||
return false
|
||||
}
|
||||
if (!isValidSize) {
|
||||
this.$message.error('固件文件大小不能超过100MB!')
|
||||
return false
|
||||
}
|
||||
return true
|
||||
},
|
||||
handleUpload(options) {
|
||||
const { file } = options
|
||||
this.uploadProgress = 0
|
||||
this.uploadStatus = ''
|
||||
this.isUploading = true
|
||||
|
||||
// 使用setTimeout实现简单的0-50%过渡
|
||||
const timer = setTimeout(() => {
|
||||
if (this.uploadProgress < 50) { // 只有当进度小于50%时才设置
|
||||
this.uploadProgress = 50
|
||||
}
|
||||
}, 1000)
|
||||
|
||||
Api.ota.uploadFirmware(file, (res) => {
|
||||
clearTimeout(timer) // 清除定时器
|
||||
res = res.data
|
||||
if (res.code === 0) {
|
||||
this.form.firmwarePath = res.data
|
||||
this.form.size = file.size
|
||||
this.uploadProgress = 100
|
||||
this.uploadStatus = 'success'
|
||||
this.$message.success('固件文件上传成功')
|
||||
// 延迟2秒后隐藏进度条
|
||||
setTimeout(() => {
|
||||
this.isUploading = false
|
||||
}, 2000)
|
||||
} else {
|
||||
this.uploadStatus = 'exception'
|
||||
this.$message.error(res.msg || '文件上传失败')
|
||||
this.isUploading = false
|
||||
}
|
||||
}, (progressEvent) => {
|
||||
if (progressEvent.total) {
|
||||
const progress = Math.round((progressEvent.loaded * 100) / progressEvent.total)
|
||||
// 只有当进度大于50%时才更新
|
||||
if (progress > 50) {
|
||||
this.uploadProgress = progress
|
||||
}
|
||||
// 如果上传完成但还没收到成功响应,保持进度条显示
|
||||
if (progress === 100) {
|
||||
this.uploadStatus = ''
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
handleRemove() {
|
||||
this.form.firmwarePath = ''
|
||||
this.form.size = 0
|
||||
this.uploadProgress = 0
|
||||
this.uploadStatus = ''
|
||||
this.isUploading = false
|
||||
},
|
||||
handleOpen() {
|
||||
// 重置上传相关状态
|
||||
this.uploadProgress = 0
|
||||
this.uploadStatus = ''
|
||||
this.isUploading = false
|
||||
// 重置表单中的文件相关字段
|
||||
if (!this.form.id) { // 只在新增时重置
|
||||
this.form.firmwarePath = ''
|
||||
this.form.size = 0
|
||||
}
|
||||
// 无论是否编辑模式,都重置上传组件
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.upload) {
|
||||
this.$refs.upload.clearFiles()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
::v-deep .el-dialog {
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.upload-demo {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.el-upload__tip {
|
||||
line-height: 1.2;
|
||||
padding-top: 2%;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.hint-text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,410 @@
|
||||
<template>
|
||||
<el-drawer :visible.sync="dialogVisible" direction="rtl" size="50%" :wrapperClosable="false" :withHeader="false">
|
||||
<!-- 自定义标题区域 -->
|
||||
<div class="custom-header">
|
||||
<div class="header-left">
|
||||
<h3 class="bold-title">功能管理</h3>
|
||||
</div>
|
||||
<button class="custom-close-btn" @click="closeDialog">×</button>
|
||||
</div>
|
||||
|
||||
<div class="function-manager">
|
||||
<!-- 左侧:未选功能 -->
|
||||
<div class="function-column">
|
||||
<div class="column-header">
|
||||
<h4 class="column-title">未选功能</h4>
|
||||
<el-button type="text" @click="selectAll" class="select-all-btn">全选</el-button>
|
||||
</div>
|
||||
<div class="function-list">
|
||||
<div v-for="func in unselected" :key="func.name" class="function-item">
|
||||
<el-checkbox :label="func.name" v-model="selectedNames" @change="(val) => handleCheckboxChange(func, val)" @click.native.stop></el-checkbox>
|
||||
<div class="func-tag" @click="handleFunctionClick(func)">
|
||||
<div class="color-dot" :style="{backgroundColor: getFunctionColor(func.name)}"></div>
|
||||
<span>{{ func.name }}</span>
|
||||
</div>
|
||||
<el-tooltip class="item" effect="dark" :content="func.description || '暂无功能描述'" placement="top">
|
||||
<img src="@/assets/home/info.png" alt="" class="info-icon">
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 中间:已选功能 -->
|
||||
<div class="function-column">
|
||||
<div class="column-header">
|
||||
<h4 class="column-title">已选功能</h4>
|
||||
<el-button type="text" @click="deselectAll" class="select-all-btn">全选</el-button>
|
||||
</div>
|
||||
<div class="function-list">
|
||||
<div v-for="func in selectedList" :key="func.name" class="function-item">
|
||||
<el-checkbox :label="func.name" v-model="selectedNames" @change="(val) => handleCheckboxChange(func, val)" @click.native.stop></el-checkbox>
|
||||
<div class="func-tag" @click="handleFunctionClick(func)">
|
||||
<div class="color-dot" :style="{backgroundColor: getFunctionColor(func.name)}"></div>
|
||||
<span>{{ func.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧:参数配置 -->
|
||||
<div class="params-column">
|
||||
<h4 v-if="currentFunction" class="column-title">参数配置 - {{ currentFunction.name }}</h4>
|
||||
<div v-if="currentFunction" class="params-container">
|
||||
<el-form :model="currentFunction" size="mini" class="param-form" v-loading="loading" element-loading-text="拼命加载中" element-loading-spinner="el-icon-loading" element-loading-background="rgba(255, 255, 255, 0.7)">
|
||||
<el-form-item v-for="(value, key) in currentFunction.params" :key="key" :label="key" class="param-item">
|
||||
<el-input v-model="currentFunction.params[key]" size="mini" class="param-input" @change="(val) => handleParamChange(currentFunction, key, val)"/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<div v-else class="empty-tip">请选择已配置的功能进行参数设置</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="drawer-footer">
|
||||
<el-button @click="closeDialog">取消</el-button>
|
||||
<el-button type="primary" @click="saveSelection">保存配置</el-button>
|
||||
</div>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
value: Boolean,
|
||||
functions: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dialogVisible: this.value,
|
||||
selectedNames: [],
|
||||
currentFunction: null,
|
||||
modifiedFunctions: {},
|
||||
allFunctions: [
|
||||
{name: '天气', params: {city: '北京'}, description: '查看指定城市的天气情况'},
|
||||
{name: '新闻', params: {type: '科技'}, description: '获取最新科技类新闻资讯'},
|
||||
{name: '工具', params: {category: '常用'}, description: '提供常用工具集合'},
|
||||
{name: '退出', params: {}, description: '退出当前系统'},
|
||||
{name: '音乐', params: {genre: '流行'}, description: '播放流行音乐'},
|
||||
{name: '翻译', params: {from: '中文', to: '英文'}, description: '提供中英文互译功能'},
|
||||
{name: '计算', params: {precision: '2'}, description: '提供精确计算功能'},
|
||||
{name: '日历', params: {view: '月'}, description: '查看月历视图'}
|
||||
],
|
||||
functionColorMap: [
|
||||
'#FF6B6B', '#4ECDC4', '#45B7D1',
|
||||
'#96CEB4', '#FFEEAD', '#D4A5A5', '#A2836E'
|
||||
],
|
||||
tempFunctions: {},
|
||||
// 添加一个标志位来跟踪是否已经保存
|
||||
hasSaved: false,
|
||||
loading: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
selectedList() {
|
||||
return this.allFunctions.filter(f => this.selectedNames.includes(f.name));
|
||||
},
|
||||
unselected() {
|
||||
return this.allFunctions.filter(f => !this.selectedNames.includes(f.name));
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value(newVal) {
|
||||
this.dialogVisible = newVal;
|
||||
if (newVal) {
|
||||
this.selectedNames = this.functions.map(f => f.name);
|
||||
this.currentFunction = this.selectedList[0] || null;
|
||||
}
|
||||
},
|
||||
dialogVisible(newVal) {
|
||||
this.$emit('input', newVal);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleFunctionClick(func) {
|
||||
if (this.selectedNames.includes(func.name)) {
|
||||
this.loading = true;
|
||||
setTimeout(() => {
|
||||
const tempFunc = this.tempFunctions[func.name];
|
||||
this.currentFunction = tempFunc ? tempFunc : JSON.parse(JSON.stringify(func));
|
||||
this.loading = false;
|
||||
}, 300);
|
||||
}
|
||||
},
|
||||
handleParamChange(func, key, value) {
|
||||
if (!this.tempFunctions[func.name]) {
|
||||
this.tempFunctions[func.name] = JSON.parse(JSON.stringify(func));
|
||||
}
|
||||
this.tempFunctions[func.name].params[key] = value;
|
||||
},
|
||||
handleCheckboxChange(func, checked) {
|
||||
if (checked) {
|
||||
if (!this.selectedNames.includes(func.name)) {
|
||||
this.selectedNames = [...this.selectedNames, func.name];
|
||||
}
|
||||
} else {
|
||||
this.selectedNames = this.selectedNames.filter(name => name !== func.name);
|
||||
}
|
||||
|
||||
if (this.selectedList.length > 0) {
|
||||
this.currentFunction = this.selectedList[0];
|
||||
} else {
|
||||
this.currentFunction = null;
|
||||
}
|
||||
},
|
||||
|
||||
selectAll() {
|
||||
this.selectedNames = [...this.allFunctions.map(f => f.name)];
|
||||
if (this.selectedList.length > 0) {
|
||||
this.currentFunction = JSON.parse(JSON.stringify(this.selectedList[0]));
|
||||
}
|
||||
},
|
||||
|
||||
deselectAll() {
|
||||
this.selectedNames = [];
|
||||
this.currentFunction = null;
|
||||
},
|
||||
|
||||
closeDialog() {
|
||||
this.tempFunctions = {};
|
||||
this.selectedNames = this.functions.map(f => f.name);
|
||||
this.currentFunction = null;
|
||||
this.dialogVisible = false;
|
||||
this.$emit('input', false);
|
||||
this.$emit('dialog-closed', false);
|
||||
},
|
||||
|
||||
saveSelection() {
|
||||
Object.keys(this.tempFunctions).forEach(name => {
|
||||
this.modifiedFunctions[name] = JSON.parse(JSON.stringify(this.tempFunctions[name]));
|
||||
});
|
||||
this.tempFunctions = {};
|
||||
this.hasSaved = true;
|
||||
|
||||
const selected = this.selectedList.map(f => {
|
||||
const modified = this.modifiedFunctions[f.name];
|
||||
return modified || f;
|
||||
}).map(f => ({
|
||||
...f,
|
||||
params: JSON.parse(JSON.stringify(f.params))
|
||||
}));
|
||||
|
||||
this.$emit('update-functions', selected);
|
||||
this.dialogVisible = false;
|
||||
this.$message.success('配置保存成功');
|
||||
// 通知父组件对话框已关闭且已保存
|
||||
this.$emit('dialog-closed', true);
|
||||
},
|
||||
|
||||
getFunctionColor(name) {
|
||||
const hash = [...name].reduce((acc, char) => acc + char.charCodeAt(0), 0);
|
||||
return this.functionColorMap[hash % 7];
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.function-manager {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(120px, 0.5fr) minmax(120px, 0.5fr) minmax(200px, 2fr);
|
||||
gap: 12px;
|
||||
height: calc(70vh - 60px);
|
||||
}
|
||||
|
||||
.custom-header {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px 24px;
|
||||
border-bottom: 1px solid #EBEEF5;
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.bold-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.select-all-btn {
|
||||
padding: 0;
|
||||
height: auto;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.function-column {
|
||||
position: relative;
|
||||
width: auto;
|
||||
padding: 10px;
|
||||
overflow-y: auto;
|
||||
border-right: 1px solid #EBEEF5;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.function-column::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.function-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.function-item {
|
||||
padding: 8px 12px;
|
||||
margin: 4px 0;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
&:hover {
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
}
|
||||
|
||||
.params-column {
|
||||
min-width: 280px;
|
||||
padding: 10px;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.params-column::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.column-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.column-title {
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.func-tag {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
flex-grow: 1;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.color-dot {
|
||||
flex-shrink: 0;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
margin-right: 8px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.param-form {
|
||||
::v-deep .el-form-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.el-form-item__label {
|
||||
font-size: 14px !important;
|
||||
color: #606266;
|
||||
text-align: left;
|
||||
padding-right: 10px;
|
||||
flex-shrink: 0;
|
||||
width: auto !important;
|
||||
}
|
||||
|
||||
.el-form-item__content {
|
||||
margin-left: 0 !important;
|
||||
flex-grow: 1;
|
||||
|
||||
.el-input__inner {
|
||||
text-align: left;
|
||||
padding-left: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.params-container {
|
||||
padding: 16px;
|
||||
border-radius: 4px;
|
||||
min-width: 280px;
|
||||
}
|
||||
|
||||
.empty-tip {
|
||||
padding: 20px;
|
||||
color: #909399;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.param-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.drawer-footer {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
border-top: 1px solid #e8e8e8;
|
||||
padding: 10px 16px;
|
||||
text-align: center;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.info-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-right: 1vh;
|
||||
}
|
||||
|
||||
.custom-close-btn {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 10px;
|
||||
transform: translateY(-50%);
|
||||
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;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.custom-close-btn:hover {
|
||||
color: #409EFF;
|
||||
border-color: #409EFF;
|
||||
}
|
||||
|
||||
::v-deep .el-checkbox__label {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
@@ -9,9 +9,11 @@
|
||||
|
||||
<!-- 中间导航菜单 -->
|
||||
<div class="header-center">
|
||||
<div class="equipment-management" :class="{ 'active-tab': $route.path === '/home' }" @click="goHome">
|
||||
<div class="equipment-management"
|
||||
:class="{ 'active-tab': $route.path === '/home' || $route.path === '/role-config' || $route.path === '/device-management' }"
|
||||
@click="goHome">
|
||||
<img loading="lazy" alt="" src="@/assets/header/robot.png"
|
||||
:style="{ filter: $route.path === '/home' ? 'brightness(0) invert(1)' : 'None' }" />
|
||||
:style="{ filter: $route.path === '/home' || $route.path === '/role-config' || $route.path === '/device-management' ? 'brightness(0) invert(1)' : 'None' }" />
|
||||
智能体管理
|
||||
</div>
|
||||
<div v-if="isSuperAdmin" class="equipment-management" :class="{ 'active-tab': $route.path === '/model-config' }"
|
||||
@@ -27,25 +29,49 @@
|
||||
用户管理
|
||||
</div>
|
||||
<div v-if="isSuperAdmin" class="equipment-management"
|
||||
:class="{ 'active-tab': $route.path === '/params-management' }" @click="goParamManagement">
|
||||
<img loading="lazy" alt="" src="@/assets/header/param_management.png"
|
||||
:style="{ filter: $route.path === '/params-management' ? 'brightness(0) invert(1)' : 'None' }" />
|
||||
参数管理
|
||||
:class="{ 'active-tab': $route.path === '/ota-management' }" @click="goOtaManagement">
|
||||
<img loading="lazy" alt="" src="@/assets/header/firmware_update.png"
|
||||
:style="{ filter: $route.path === '/ota-management' ? 'brightness(0) invert(1)' : 'None' }" />
|
||||
OTA管理
|
||||
</div>
|
||||
<el-dropdown v-if="isSuperAdmin" trigger="click" class="equipment-management more-dropdown"
|
||||
:class="{ 'active-tab': $route.path === '/dict-management' || $route.path === '/params-management' || $route.path === '/provider-management' }" @visible-change="handleParamDropdownVisibleChange">
|
||||
<span class="el-dropdown-link">
|
||||
<img loading="lazy" alt="" src="@/assets/header/param_management.png"
|
||||
:style="{ filter: $route.path === '/dict-management' || $route.path === '/params-management' || $route.path === '/provider-management' ? 'brightness(0) invert(1)' : 'None' }" />
|
||||
参数字典
|
||||
<i class="el-icon-arrow-down el-icon--right" :class="{ 'rotate-down': paramDropdownVisible }"></i>
|
||||
</span>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<el-dropdown-item @click.native="goParamManagement">
|
||||
参数管理
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click.native="goDictManagement">
|
||||
字典管理
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click.native="goProviderManagement">
|
||||
供应器管理
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click.native="goServerSideManagement">
|
||||
服务端管理
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
|
||||
<!-- 右侧元素 -->
|
||||
<div class="header-right">
|
||||
<div class="search-container" v-if="$route.path === '/home'">
|
||||
<div class="search-container" v-if="$route.path === '/home' && !(isSuperAdmin && isSmallScreen)">
|
||||
<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">
|
||||
<el-dropdown trigger="click" class="user-dropdown" @visible-change="handleUserDropdownVisibleChange">
|
||||
<span class="el-dropdown-link">
|
||||
{{ userInfo.username || '加载中...' }}<i class="el-icon-arrow-down el-icon--right"></i>
|
||||
{{ userInfo.username || '加载中...' }}
|
||||
<i class="el-icon-arrow-down el-icon--right" :class="{ 'rotate-down': userDropdownVisible }"></i>
|
||||
</span>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<el-dropdown-item @click.native="showChangePasswordDialog">修改密码</el-dropdown-item>
|
||||
@@ -65,7 +91,6 @@ import userApi from '@/apis/module/user';
|
||||
import { mapActions, mapGetters } from 'vuex';
|
||||
import ChangePasswordDialog from './ChangePasswordDialog.vue'; // 引入修改密码弹窗组件
|
||||
|
||||
|
||||
export default {
|
||||
name: 'HeaderBar',
|
||||
components: {
|
||||
@@ -79,7 +104,10 @@ export default {
|
||||
username: '',
|
||||
mobile: ''
|
||||
},
|
||||
isChangePasswordDialogVisible: false // 控制修改密码弹窗的显示
|
||||
isChangePasswordDialogVisible: false, // 控制修改密码弹窗的显示
|
||||
userDropdownVisible: false,
|
||||
paramDropdownVisible: false,
|
||||
isSmallScreen: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -89,7 +117,13 @@ export default {
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.fetchUserInfo()
|
||||
this.fetchUserInfo();
|
||||
this.checkScreenSize();
|
||||
window.addEventListener('resize', this.checkScreenSize);
|
||||
},
|
||||
//移除事件监听器
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('resize', this.checkScreenSize);
|
||||
},
|
||||
methods: {
|
||||
goHome() {
|
||||
@@ -105,6 +139,18 @@ export default {
|
||||
goParamManagement() {
|
||||
this.$router.push('/params-management')
|
||||
},
|
||||
goOtaManagement() {
|
||||
this.$router.push('/ota-management')
|
||||
},
|
||||
goDictManagement() {
|
||||
this.$router.push('/dict-management')
|
||||
},
|
||||
goProviderManagement() {
|
||||
this.$router.push('/provider-management')
|
||||
},
|
||||
goServerSideManagement() {
|
||||
this.$router.push('/server-side-management')
|
||||
},
|
||||
// 获取用户信息
|
||||
fetchUserInfo() {
|
||||
userApi.getUserInfo(({ data }) => {
|
||||
@@ -114,7 +160,9 @@ export default {
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
checkScreenSize() {
|
||||
this.isSmallScreen = window.innerWidth <= 1386;
|
||||
},
|
||||
// 处理搜索
|
||||
handleSearch() {
|
||||
const searchValue = this.search.trim();
|
||||
@@ -159,6 +207,13 @@ export default {
|
||||
});
|
||||
}
|
||||
},
|
||||
handleUserDropdownVisibleChange(visible) {
|
||||
this.userDropdownVisible = visible;
|
||||
},
|
||||
// 监听第二个下拉菜单的可见状态变化
|
||||
handleParamDropdownVisibleChange(visible) {
|
||||
this.paramDropdownVisible = visible;
|
||||
},
|
||||
|
||||
// 使用 mapActions 引入 Vuex 的 logout action
|
||||
...mapActions(['logout'])
|
||||
@@ -166,11 +221,11 @@ export default {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
<style lang="scss" scoped>
|
||||
.header {
|
||||
background: #f6fcfe66;
|
||||
border: 1px solid #fff;
|
||||
height: 53px !important;
|
||||
height: 63px !important;
|
||||
min-width: 900px;
|
||||
/* 设置最小宽度防止过度压缩 */
|
||||
overflow: hidden;
|
||||
@@ -197,7 +252,7 @@ export default {
|
||||
}
|
||||
|
||||
.brand-img {
|
||||
height: 18px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.header-center {
|
||||
@@ -218,14 +273,12 @@ export default {
|
||||
}
|
||||
|
||||
.equipment-management {
|
||||
padding: 0 9px;
|
||||
width: 82px;
|
||||
height: 24px;
|
||||
border-radius: 12px;
|
||||
height: 30px;
|
||||
border-radius: 15px;
|
||||
background: #deeafe;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
font-size: 10px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
gap: 7px;
|
||||
color: #3d4566;
|
||||
@@ -235,6 +288,8 @@ export default {
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
/* 防止导航按钮被压缩 */
|
||||
padding: 0 15px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.equipment-management.active-tab {
|
||||
@@ -283,6 +338,25 @@ export default {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.more-dropdown {
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
.more-dropdown .el-dropdown-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.rotate-down {
|
||||
transform: rotate(180deg);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.el-icon-arrow-down {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
/* 响应式调整 */
|
||||
@media (max-width: 1200px) {
|
||||
.header-center {
|
||||
@@ -290,47 +364,27 @@ export default {
|
||||
}
|
||||
|
||||
.equipment-management {
|
||||
width: 70px;
|
||||
width: 79px;
|
||||
font-size: 9px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.search-container {
|
||||
margin-right: 10px;
|
||||
max-width: 150px;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
gap: 5px;
|
||||
}
|
||||
.equipment-management.more-dropdown {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.header-left {
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
max-width: 150px;
|
||||
}
|
||||
.equipment-management.more-dropdown .el-dropdown-menu {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
min-width: 120px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
.el-dropdown-menu__item {
|
||||
min-width: 60px;
|
||||
padding: 8px 20px;
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<el-dialog :visible.sync="dialogVisible" width="975px" center custom-class="custom-dialog" :show-close="false"
|
||||
class="center-dialog">
|
||||
<el-dialog :visible.sync="dialogVisible" width="57%" 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;">
|
||||
修改模型
|
||||
@@ -17,7 +17,7 @@
|
||||
<span style="margin-right: 8px;">是否启用</span>
|
||||
<el-switch v-model="form.isEnabled" :active-value="1" :inactive-value="0" class="custom-switch"></el-switch>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center;">
|
||||
<div style="display: none; align-items: center;">
|
||||
<span style="margin-right: 8px;">设为默认</span>
|
||||
<el-switch v-model="form.isDefault" :active-value="1" :inactive-value="0" class="custom-switch"></el-switch>
|
||||
</div>
|
||||
@@ -53,7 +53,7 @@
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="备注" prop="remark" class="prop-remark">
|
||||
<el-input v-model="form.remark" type="textarea" :rows="3" placeholder="请输入模型备注"
|
||||
<el-input v-model="form.remark" type="textarea" :rows="3" placeholder="请输入模型备注" :autosize="{ minRows: 3, maxRows: 5 }"
|
||||
class="custom-input-bg"></el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
@@ -64,18 +64,10 @@
|
||||
<el-form :model="form.configJson" ref="callInfoForm" label-width="auto" class="custom-form">
|
||||
<template v-for="(row, rowIndex) in chunkedCallInfoFields">
|
||||
<div :key="rowIndex" style="display: flex; gap: 20px; margin-bottom: 0;">
|
||||
<el-form-item
|
||||
v-for="field in row"
|
||||
:key="field.prop"
|
||||
:label="field.label"
|
||||
:prop="field.prop"
|
||||
<el-form-item v-for="field in row" :key="field.prop" :label="field.label" :prop="field.prop"
|
||||
style="flex: 1;">
|
||||
<el-input
|
||||
v-model="form.configJson[field.prop]"
|
||||
:placeholder="field.placeholder"
|
||||
:type="field.type"
|
||||
class="custom-input-bg"
|
||||
:show-password="field.type === 'password'">
|
||||
<el-input v-model="form.configJson[field.prop]" :placeholder="field.placeholder" :type="field.type"
|
||||
class="custom-input-bg" :show-password="field.type === 'password'">
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</div>
|
||||
@@ -84,7 +76,12 @@
|
||||
</div>
|
||||
|
||||
<div style="display: flex;justify-content: center;">
|
||||
<el-button type="primary" @click="handleSave" class="save-btn">
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleSave"
|
||||
class="save-btn"
|
||||
:loading="saving"
|
||||
:disabled="saving">
|
||||
保存
|
||||
</el-button>
|
||||
</div>
|
||||
@@ -94,26 +91,6 @@
|
||||
<script>
|
||||
import Api from '@/apis/api';
|
||||
|
||||
const DEFAULT_CONFIG_JSON = {
|
||||
type: "",
|
||||
base_url: "",
|
||||
model_name: "",
|
||||
api_key: "",
|
||||
raw: {},
|
||||
config: {
|
||||
keyComparator: {},
|
||||
ignoreError: false,
|
||||
ignoreCase: false,
|
||||
dateFormat: "",
|
||||
ignoreNullValue: false,
|
||||
transientSupport: false,
|
||||
stripTrailingZeros: false,
|
||||
checkDuplicate: false,
|
||||
order: false
|
||||
},
|
||||
empty: false
|
||||
};
|
||||
|
||||
export default {
|
||||
name: "ModelEditDialog",
|
||||
props: {
|
||||
@@ -130,6 +107,7 @@ export default {
|
||||
dialogVisible: this.visible,
|
||||
providers: [],
|
||||
providersLoaded: false,
|
||||
saving: false,
|
||||
allProvidersData: null,
|
||||
pendingProviderType: null,
|
||||
pendingModelData: null,
|
||||
@@ -144,7 +122,7 @@ export default {
|
||||
docLink: "",
|
||||
remark: "",
|
||||
sort: 0,
|
||||
configJson: JSON.parse(JSON.stringify(DEFAULT_CONFIG_JSON))
|
||||
configJson: {}
|
||||
}
|
||||
};
|
||||
},
|
||||
@@ -194,9 +172,12 @@ export default {
|
||||
isEnabled: false,
|
||||
docLink: "",
|
||||
remark: "",
|
||||
sort: "",
|
||||
configJson: JSON.parse(JSON.stringify(DEFAULT_CONFIG_JSON))
|
||||
sort: 0,
|
||||
configJson: {}
|
||||
};
|
||||
this.dynamicCallInfoFields.forEach(field => {
|
||||
this.$set(this.form.configJson, field.prop, '');
|
||||
});
|
||||
},
|
||||
resetProviders() {
|
||||
this.providers = [];
|
||||
@@ -220,10 +201,11 @@ export default {
|
||||
}
|
||||
},
|
||||
handleSave() {
|
||||
this.saving = true; // 开始保存加载
|
||||
|
||||
const provideCode = this.form.configJson.type;
|
||||
const { provider, ...restConfigJson } = this.form.configJson;
|
||||
const formData = {
|
||||
id: this.form.id,
|
||||
id: this.modelData.id,
|
||||
modelCode: this.form.modelCode,
|
||||
modelName: this.form.modelName,
|
||||
isDefault: this.form.isDefault ? 1 : 0,
|
||||
@@ -232,16 +214,22 @@ export default {
|
||||
remark: this.form.remark,
|
||||
sort: this.form.sort || 0,
|
||||
configJson: {
|
||||
...restConfigJson,
|
||||
config: {
|
||||
...restConfigJson.config,
|
||||
ignoreError: !!restConfigJson.config?.ignoreError,
|
||||
ignoreCase: !!restConfigJson.config?.ignoreCase,
|
||||
}
|
||||
...this.form.configJson,
|
||||
}
|
||||
};
|
||||
this.$emit("save", { provideCode, formData });
|
||||
this.dialogVisible = false;
|
||||
|
||||
this.$emit("save", {
|
||||
provideCode,
|
||||
formData,
|
||||
done: () => {
|
||||
this.saving = false; // 保存完成后回调
|
||||
}
|
||||
});
|
||||
|
||||
// 如果父组件不处理done回调,3秒后自动关闭加载状态
|
||||
setTimeout(() => {
|
||||
this.saving = false;
|
||||
}, 3000);
|
||||
},
|
||||
loadProviders() {
|
||||
if (this.providersLoaded) return;
|
||||
@@ -249,7 +237,7 @@ export default {
|
||||
Api.model.getModelProviders(this.modelType, (data) => {
|
||||
this.providers = data.map(item => ({
|
||||
label: item.name,
|
||||
value: item.providerCode
|
||||
value: String(item.providerCode)
|
||||
}));
|
||||
this.providersLoaded = true;
|
||||
|
||||
@@ -284,35 +272,31 @@ export default {
|
||||
this.dynamicCallInfoFields.forEach(field => {
|
||||
if (!configJson.hasOwnProperty(field.prop)) {
|
||||
configJson[field.prop] = '';
|
||||
} else if (typeof configJson[field.prop] !== 'string') {
|
||||
configJson[field.prop] = String(configJson[field.prop]);
|
||||
}
|
||||
});
|
||||
|
||||
this.form = {
|
||||
id: model.id || "",
|
||||
modelType: model.modelType || "",
|
||||
modelCode: model.modelCode || "",
|
||||
modelName: model.modelName || "",
|
||||
isDefault: model.isDefault || 0,
|
||||
isEnabled: model.isEnabled || 0,
|
||||
docLink: model.docLink || "",
|
||||
remark: model.remark || "",
|
||||
id: model.id,
|
||||
modelType: model.modelType,
|
||||
modelCode: model.modelCode,
|
||||
modelName: model.modelName,
|
||||
isDefault: model.isDefault,
|
||||
isEnabled: model.isEnabled,
|
||||
docLink: model.docLink,
|
||||
remark: model.remark,
|
||||
sort: Number(model.sort) || 0,
|
||||
configJson: {
|
||||
...JSON.parse(JSON.stringify(DEFAULT_CONFIG_JSON)),
|
||||
...configJson,
|
||||
config: {
|
||||
...DEFAULT_CONFIG_JSON.config,
|
||||
...(configJson.config || {})
|
||||
}
|
||||
...configJson
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
<style lang="scss" scoped>
|
||||
.custom-dialog {
|
||||
position: relative;
|
||||
border-radius: 20px;
|
||||
@@ -332,11 +316,6 @@ export default {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.center-dialog .el-dialog {
|
||||
margin: 4% 0 auto !important;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.custom-close-btn {
|
||||
position: absolute;
|
||||
|
||||
@@ -0,0 +1,364 @@
|
||||
<template>
|
||||
<el-dialog :title="title"
|
||||
:visible.sync="visible"
|
||||
width="520px"
|
||||
class="param-dialog-wrapper"
|
||||
:append-to-body="true"
|
||||
:close-on-click-modal="false"
|
||||
:key="dialogKey"
|
||||
custom-class="custom-param-dialog"
|
||||
:show-close="false"
|
||||
>
|
||||
<div class="dialog-container">
|
||||
<div class="dialog-header">
|
||||
<h2 class="dialog-title">{{ title }}</h2>
|
||||
<button class="custom-close-btn" @click="cancel">
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M13 1L1 13M1 1L13 13" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<el-form :model="form" :rules="rules" ref="form" label-width="110px" label-position="left" class="param-form">
|
||||
<el-form-item label="参数编码" prop="paramCode" class="form-item">
|
||||
<el-input v-model="form.paramCode" placeholder="请输入参数编码" class="custom-input"></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="参数值" prop="paramValue" class="form-item">
|
||||
<el-input v-model="form.paramValue" placeholder="请输入参数值" class="custom-input"></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="值类型" prop="valueType" class="form-item">
|
||||
<el-select v-model="form.valueType" placeholder="请选择值类型" class="custom-select">
|
||||
<el-option v-for="item in valueTypeOptions" :key="item.value" :label="item.label" :value="item.value"/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="备注" prop="remark" class="form-item remark-item">
|
||||
<el-input type="textarea" v-model="form.remark" placeholder="请输入备注" :rows="3" class="custom-textarea"></el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<div class="dialog-footer">
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="submit"
|
||||
class="save-btn"
|
||||
:loading="saving"
|
||||
:disabled="saving">
|
||||
保存
|
||||
</el-button>
|
||||
<el-button @click="cancel" class="cancel-btn">
|
||||
取消
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
default: '新增参数'
|
||||
},
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
form: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
id: null,
|
||||
paramCode: '',
|
||||
paramValue: '',
|
||||
valueType: 'string',
|
||||
remark: ''
|
||||
})
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dialogKey: Date.now(),
|
||||
saving: false,
|
||||
valueTypeOptions: [
|
||||
{ value: 'string', label: '字符串(string)' },
|
||||
{ value: 'number', label: '数字(number)' },
|
||||
{ value: 'boolean', label: '布尔值(boolean)' },
|
||||
{ value: 'array', label: '数组(array)' },
|
||||
{ value: 'json', label: 'JSON对象(json)' }
|
||||
],
|
||||
rules: {
|
||||
paramCode: [
|
||||
{ required: true, message: "请输入参数编码", trigger: "blur" }
|
||||
],
|
||||
paramValue: [
|
||||
{ required: true, message: "请输入参数值", trigger: "blur" }
|
||||
],
|
||||
valueType: [
|
||||
{ required: true, message: "请选择值类型", trigger: "change" }
|
||||
]
|
||||
}
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
submit() {
|
||||
this.$refs.form.validate((valid) => {
|
||||
if (valid) {
|
||||
this.saving = true; // 开始加载
|
||||
this.$emit('submit', {
|
||||
form: this.form,
|
||||
done: () => {
|
||||
this.saving = false; // 加载完成
|
||||
}
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
this.saving = false;
|
||||
}, 3000);
|
||||
}
|
||||
});
|
||||
},
|
||||
cancel() {
|
||||
this.saving = false; // 取消时重置状态
|
||||
this.$emit('cancel');
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
visible(newVal) {
|
||||
if (newVal) {
|
||||
this.dialogKey = Date.now();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.custom-param-dialog {
|
||||
border-radius: 16px !important;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15) !important;
|
||||
border: none !important;
|
||||
|
||||
.el-dialog__header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.el-dialog__body {
|
||||
padding: 0 !important;
|
||||
border-radius: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.param-dialog-wrapper {
|
||||
.dialog-container {
|
||||
padding: 24px 32px;
|
||||
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
|
||||
}
|
||||
|
||||
.dialog-header {
|
||||
position: relative;
|
||||
margin-bottom: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.dialog-title {
|
||||
font-size: 20px;
|
||||
color: #1e293b;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.custom-close-btn {
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
right: -8px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
background: #f1f5f9;
|
||||
color: #64748b;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
outline: none;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
|
||||
&:hover {
|
||||
color: #ffffff;
|
||||
background: #ef4444;
|
||||
transform: rotate(90deg);
|
||||
box-shadow: 0 4px 6px rgba(239, 68, 68, 0.2);
|
||||
}
|
||||
|
||||
svg {
|
||||
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
}
|
||||
|
||||
.param-form {
|
||||
.form-item {
|
||||
margin-bottom: 20px;
|
||||
|
||||
:deep(.el-form-item__label) {
|
||||
color: #475569;
|
||||
font-weight: 500;
|
||||
padding-right: 12px;
|
||||
text-align: right;
|
||||
font-size: 14px;
|
||||
letter-spacing: 0.2px;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-input {
|
||||
:deep(.el-input__inner) {
|
||||
background-color: #ffffff;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e2e8f0;
|
||||
height: 42px;
|
||||
padding: 0 14px;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
font-size: 14px;
|
||||
color: #334155;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
|
||||
&:focus {
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2);
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: #94a3b8;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.custom-select {
|
||||
width: 100%;
|
||||
|
||||
:deep(.el-input__inner) {
|
||||
background-color: #ffffff;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e2e8f0;
|
||||
height: 42px;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
font-size: 14px;
|
||||
color: #334155;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
|
||||
&:focus {
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2);
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: #94a3b8;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.custom-textarea {
|
||||
:deep(.el-textarea__inner) {
|
||||
background-color: #ffffff;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e2e8f0;
|
||||
padding: 12px 14px;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
font-size: 14px;
|
||||
color: #334155;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
line-height: 1.5;
|
||||
|
||||
&:focus {
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2);
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: #94a3b8;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.remark-item :deep(.el-form-item__label) {
|
||||
margin-top: -4px;
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 16px 0 0;
|
||||
margin-top: 16px;
|
||||
|
||||
.save-btn {
|
||||
width: 120px;
|
||||
height: 42px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
border: none;
|
||||
letter-spacing: 0.5px;
|
||||
box-shadow: 0 2px 4px rgba(59, 130, 246, 0.2);
|
||||
|
||||
&:hover {
|
||||
background: #2563eb;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 6px rgba(59, 130, 246, 0.3);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 2px 3px rgba(59, 130, 246, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
.cancel-btn {
|
||||
width: 120px;
|
||||
height: 42px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
background: #ffffff;
|
||||
color: #64748b;
|
||||
border: 1px solid #e2e8f0;
|
||||
margin-left: 16px;
|
||||
letter-spacing: 0.5px;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
|
||||
&:hover {
|
||||
background: #f8fafc;
|
||||
color: #475569;
|
||||
border-color: #cbd5e1;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,435 @@
|
||||
<template>
|
||||
<el-dialog :visible="visible" @update:visible="handleVisibleChange" width="57%" 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: -15px; margin-bottom: 20px; text-align: center;">
|
||||
{{ title }}
|
||||
</div>
|
||||
|
||||
<button class="custom-close-btn" @click="handleClose">×</button>
|
||||
|
||||
<el-form :model="form" label-width="100px" :rules="rules" ref="form" class="custom-form">
|
||||
<div style="display: flex; gap: 20px; margin-bottom: 20px;">
|
||||
<el-form-item label="类别" prop="modelType" style="flex: 1;">
|
||||
<el-select v-model="form.modelType" placeholder="请选择类别" class="custom-input-bg" style="width: 100%;">
|
||||
<el-option v-for="item in modelTypes" :key="item.value" :label="item.label" :value="item.value">
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="供应器编码" prop="providerCode" style="flex: 1;">
|
||||
<el-input v-model="form.providerCode" placeholder="请输入供应器编码" class="custom-input-bg"></el-input>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 20px; margin-bottom: 20px;">
|
||||
<el-form-item label="名称" prop="name" style="flex: 1;">
|
||||
<el-input v-model="form.name" placeholder="请输入供应器名称" class="custom-input-bg"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序" prop="sort" style="flex: 1;">
|
||||
<el-input-number v-model="form.sort" :min="0" controls-position="right" class="custom-input-bg"
|
||||
style="width: 100%;"></el-input-number>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div style="font-size: 20px; font-weight: bold; color: #3d4566; margin-bottom: 15px;">
|
||||
字段配置
|
||||
<div style="display: inline-block; float: right;">
|
||||
<el-button type="primary" @click="addField" size="small" style="background: #5bc98c; border: none;"
|
||||
:disabled="hasIncompleteFields">
|
||||
添加
|
||||
</el-button>
|
||||
<el-button type="primary" @click="toggleSelectAllFields" size="small"
|
||||
style="background: #5f70f3; border: none; margin-left: 10px;">
|
||||
{{ isAllFieldsSelected ? '取消全选' : '全选' }}
|
||||
</el-button>
|
||||
<el-button type="danger" @click="batchRemoveFields" size="small"
|
||||
style="background: red; border: none; margin-left: 10px;">
|
||||
批量删除
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div style="height: 2px; background: #e9e9e9; margin-bottom: 22px;"></div>
|
||||
|
||||
<div class="fields-container">
|
||||
<el-table :data="form.fields" style="width: 100%;" border size="medium" :key="tableKey">
|
||||
<el-table-column label="选择" align="center" width="50">
|
||||
<template slot-scope="scope">
|
||||
<el-checkbox v-model="scope.row.selected" @change="handleFieldSelectChange"></el-checkbox>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="字段key">
|
||||
<template slot-scope="scope">
|
||||
<template v-if="scope.row.editing">
|
||||
<el-input v-model="scope.row.key" placeholder="字段key"></el-input>
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ scope.row.key }}
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="字段标签">
|
||||
<template slot-scope="scope">
|
||||
<template v-if="scope.row.editing">
|
||||
<el-input v-model="scope.row.label" placeholder="字段标签"></el-input>
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ scope.row.label }}
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="字段类型">
|
||||
<template slot-scope="scope">
|
||||
<template v-if="scope.row.editing">
|
||||
<el-select v-model="scope.row.type" placeholder="类型">
|
||||
<el-option label="字符串" value="string"></el-option>
|
||||
<el-option label="数字" value="number"></el-option>
|
||||
<el-option label="布尔值" value="boolean"></el-option>
|
||||
<el-option label="字典" value="dict"></el-option>
|
||||
</el-select>
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ getTypeLabel(scope.row.type) }}
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="默认值">
|
||||
<template slot-scope="scope">
|
||||
<template v-if="scope.row.editing">
|
||||
<el-input v-model="scope.row.default_value" placeholder="请输入默认值"></el-input>
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ scope.row.default_value }}
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="150" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-button v-if="!scope.row.editing" type="primary" size="mini" @click="startEditing(scope.row)">
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button v-else type="success" size="mini" @click="stopEditing(scope.row)">
|
||||
完成
|
||||
</el-button>
|
||||
<el-button type="danger" size="mini" @click="removeField(scope.$index)">
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; justify-content: center;">
|
||||
<el-button type="primary" @click="submit" class="save-btn" :loading="saving">保存</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
title: String,
|
||||
visible: Boolean,
|
||||
form: Object,
|
||||
modelTypes: Array
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
saving: false,
|
||||
rules: {
|
||||
modelType: [{ required: true, message: '请选择类别', trigger: 'change' }],
|
||||
providerCode: [{ required: true, message: '请输入供应器编码', trigger: 'blur' }],
|
||||
name: [{ required: true, message: '请输入供应器名称', trigger: 'blur' }]
|
||||
},
|
||||
isAllFieldsSelected: false,
|
||||
tableKey: 0 // 用于强制表格重新渲染
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
hasIncompleteFields() {
|
||||
return this.form.fields && this.form.fields.some(field =>
|
||||
!field.key || !field.label || !field.type
|
||||
);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getTypeLabel(type) {
|
||||
const typeMap = {
|
||||
'string': '字符串',
|
||||
'number': '数字',
|
||||
'boolean': '布尔值',
|
||||
'dict': '字典'
|
||||
};
|
||||
return typeMap[type];
|
||||
},
|
||||
|
||||
startEditing(row) {
|
||||
this.$set(row, 'editing', true);
|
||||
},
|
||||
|
||||
stopEditing(row) {
|
||||
this.$set(row, 'editing', false);
|
||||
|
||||
const index = this.form.fields.indexOf(row);
|
||||
if (index > -1) {
|
||||
this.form.fields.splice(index, 1);
|
||||
this.form.fields.push(row);
|
||||
this.forceTableRerender();
|
||||
}
|
||||
},
|
||||
|
||||
handleFieldSelectChange() {
|
||||
this.isAllFieldsSelected = this.form.fields.length > 0 &&
|
||||
this.form.fields.every(field => field.selected);
|
||||
},
|
||||
|
||||
toggleSelectAllFields() {
|
||||
this.isAllFieldsSelected = !this.isAllFieldsSelected;
|
||||
this.form.fields = this.form.fields.map(field => ({
|
||||
...field,
|
||||
selected: this.isAllFieldsSelected
|
||||
}));
|
||||
},
|
||||
|
||||
handleVisibleChange(val) {
|
||||
this.$emit('update:visible', val);
|
||||
if (!val) {
|
||||
this.resetForm();
|
||||
}
|
||||
},
|
||||
|
||||
handleClose() {
|
||||
this.resetForm();
|
||||
this.$emit('update:visible', false);
|
||||
this.$emit('cancel');
|
||||
},
|
||||
|
||||
addField() {
|
||||
if (this.hasIncompleteFields) {
|
||||
this.$message.warning({
|
||||
message: '请先完成当前字段的编辑',
|
||||
showClose: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.form.fields.unshift({
|
||||
key: '',
|
||||
label: '',
|
||||
type: 'string',
|
||||
default_value: '',
|
||||
selected: false,
|
||||
editing: true
|
||||
});
|
||||
this.forceTableRerender();
|
||||
},
|
||||
|
||||
removeField(index) {
|
||||
this.$confirm('确定要删除该字段吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
this.form.fields = this.form.fields.filter((_, i) => i !== index);
|
||||
this.updateSelectAllStatus();
|
||||
this.forceTableRerender();
|
||||
this.$message.success({
|
||||
message: '删除成功',
|
||||
showClose: true
|
||||
});
|
||||
}).catch(() => {
|
||||
this.$message.info({
|
||||
message: '已取消删除',
|
||||
showClose: true
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
batchRemoveFields() {
|
||||
const selectedFields = this.form.fields.filter(field => field.selected);
|
||||
if (selectedFields.length === 0) {
|
||||
this.$message.warning({
|
||||
message: '请先选择要删除的字段',
|
||||
showClose: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.$confirm(`确定要删除选中的 ${selectedFields.length} 个字段吗?`, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
this.form.fields = this.form.fields.filter(field => !field.selected);
|
||||
this.isAllFieldsSelected = false;
|
||||
this.forceTableRerender();
|
||||
this.$message.success({
|
||||
message: `成功删除 ${selectedFields.length} 个字段`,
|
||||
showClose: true
|
||||
});
|
||||
}).catch(() => {
|
||||
this.$message.info({
|
||||
message: '已取消删除',
|
||||
showClose: true
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
updateSelectAllStatus() {
|
||||
this.isAllFieldsSelected = this.form.fields.length > 0 &&
|
||||
this.form.fields.every(field => field.selected);
|
||||
},
|
||||
|
||||
forceTableRerender() {
|
||||
this.tableKey += 1; // 改变key值强制表格重新渲染
|
||||
},
|
||||
|
||||
submit() {
|
||||
this.$refs.form.validate(valid => {
|
||||
if (valid) {
|
||||
const editingField = this.form.fields.find(field => field.editing);
|
||||
if (editingField) {
|
||||
this.$message.warning({
|
||||
message: '请先完成当前字段的编辑',
|
||||
showClose: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.form.fields = this.form.fields.map(field => ({
|
||||
...field,
|
||||
selected: false
|
||||
}));
|
||||
this.isAllFieldsSelected = false;
|
||||
|
||||
this.saving = true;
|
||||
this.$emit('submit', {
|
||||
form: this.form,
|
||||
done: () => {
|
||||
this.saving = false;
|
||||
this.resetForm();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
resetForm() {
|
||||
this.$refs.form.resetFields();
|
||||
if (this.form.fields) {
|
||||
this.form.fields.forEach(field => {
|
||||
field.selected = false;
|
||||
field.editing = false;
|
||||
});
|
||||
}
|
||||
this.isAllFieldsSelected = false;
|
||||
this.forceTableRerender();
|
||||
},
|
||||
|
||||
},
|
||||
watch: {
|
||||
visible(val) {
|
||||
if (!val) {
|
||||
this.resetForm();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
::v-deep .custom-dialog.el-dialog {
|
||||
margin-top: 0 !important;
|
||||
border-radius: 20px !important;
|
||||
}
|
||||
|
||||
::v-deep .custom-dialog .el-dialog__header {
|
||||
padding: 0;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.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-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-input-bg .el-input__inner {
|
||||
background-color: #f6f8fc;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.custom-input-bg .el-input__inner::-webkit-input-placeholder {
|
||||
color: #9c9f9e;
|
||||
}
|
||||
|
||||
.fields-container {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.el-table {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.el-table::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.el-table th,
|
||||
.el-table td {
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.el-button.is-circle {
|
||||
border-radius: 2px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
:visible.sync="localVisible"
|
||||
width="75%"
|
||||
width="85%"
|
||||
@close="handleClose"
|
||||
:show-close="false"
|
||||
:append-to-body="true"
|
||||
@@ -13,15 +13,17 @@
|
||||
<div
|
||||
class="table-container"
|
||||
ref="tableContainer"
|
||||
@scroll="handleScroll"
|
||||
>
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="filteredTtsModels"
|
||||
style="width: 100%;"
|
||||
class="data-table"
|
||||
header-row-class-name="table-header"
|
||||
:fit="true">
|
||||
@scroll="handleScroll">
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="filteredTtsModels"
|
||||
style="width: 100%;"
|
||||
class="data-table"
|
||||
header-row-class-name="table-header"
|
||||
:fit="true"
|
||||
element-loading-text="拼命加载中"
|
||||
element-loading-spinner="el-icon-loading"
|
||||
element-loading-background="rgba(0, 0, 0, 0.8)">
|
||||
<el-table-column label="选择" width="50" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-checkbox v-model="scope.row.selected"></el-checkbox>
|
||||
@@ -45,7 +47,7 @@
|
||||
<span v-else>{{ scope.row.languageType }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="试听" align="center" min-width="225" class-name="audio-column">
|
||||
<el-table-column label="试听" align="center" min-width="100px" class-name="audio-column">
|
||||
<template slot-scope="scope">
|
||||
<div class="custom-audio-container">
|
||||
<el-input
|
||||
@@ -53,13 +55,13 @@
|
||||
v-model="scope.row.voiceDemo"
|
||||
placeholder="请输入MP3地址"
|
||||
size="mini"
|
||||
class="audio-input"
|
||||
></el-input>
|
||||
class="audio-input">
|
||||
</el-input>
|
||||
<AudioPlayer v-else-if="isValidAudioUrl(scope.row.voiceDemo)" :audioUrl="scope.row.voiceDemo"/>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="备注" align="center" min-width="120">
|
||||
<el-table-column label="备注" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-input
|
||||
v-if="scope.row.editing"
|
||||
@@ -75,10 +77,10 @@
|
||||
<el-table-column label="操作" align="center" width="150">
|
||||
<template slot-scope="scope">
|
||||
<template v-if="!scope.row.editing">
|
||||
<el-button type="primary" size="mini" @click="startEdit(scope.row)"
|
||||
style="background: #5cca8e;border:None">编辑
|
||||
<el-button type="text" size="mini" @click="startEdit(scope.row)" class="edit-btn">
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button type="primary" size="mini" @click="deleteRow(scope.row)" style="background: red;border:None">
|
||||
<el-button type="text" size="mini" @click="deleteRow(scope.row)" class="delete-btn">
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
@@ -105,10 +107,14 @@
|
||||
<el-button type="primary" size="mini" @click="toggleSelectAll" style="background: #606ff3;border: None">
|
||||
{{ selectAll ? '取消全选' : '全选' }}
|
||||
</el-button>
|
||||
<el-button type="primary" size="mini" @click="addNew" style="background: #f6cf79;border: None; color: #000012">
|
||||
<el-button type="primary" size="mini" @click="addNew" style="background: #5bc98c;border: None;">
|
||||
新增
|
||||
</el-button>
|
||||
<el-button type="primary" size="mini" @click="batchDelete" style="background: red;border:None">删除</el-button>
|
||||
<el-button type="primary"
|
||||
size="mini"
|
||||
@click="deleteRow(filteredTtsModels.filter(row => row.selected))"
|
||||
style="background: red;border:None">删除
|
||||
</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
@@ -184,28 +190,28 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
loadData() {
|
||||
this.loading = true;
|
||||
const params = {
|
||||
ttsModelId: this.ttsModelId,
|
||||
page: this.currentPage,
|
||||
limit: this.pageSize,
|
||||
name: this.searchQuery
|
||||
};
|
||||
|
||||
Api.timbre.getVoiceList(params, (data) => {
|
||||
if (data.code === 0) {
|
||||
this.ttsModels = data.data.list
|
||||
.map(item => ({
|
||||
id: item.id || '',
|
||||
voiceCode: item.ttsVoice || '',
|
||||
voiceName: item.name || '未命名音色',
|
||||
languageType: item.languages || '',
|
||||
remark: item.remark || '',
|
||||
voiceDemo: item.voiceDemo || '',
|
||||
selected: false,
|
||||
editing: false,
|
||||
sort: Number(item.sort)
|
||||
}))
|
||||
.sort((a, b) => a.sort - b.sort);
|
||||
.map(item => ({
|
||||
id: item.id || '',
|
||||
voiceCode: item.ttsVoice || '',
|
||||
voiceName: item.name || '未命名音色',
|
||||
languageType: item.languages || '',
|
||||
remark: item.remark || '',
|
||||
voiceDemo: item.voiceDemo || '',
|
||||
selected: false,
|
||||
editing: false,
|
||||
sort: Number(item.sort)
|
||||
}))
|
||||
.sort((a, b) => a.sort - b.sort);
|
||||
this.total = data.total;
|
||||
} else {
|
||||
this.$message.error({
|
||||
@@ -213,12 +219,14 @@ export default {
|
||||
showClose: true
|
||||
});
|
||||
}
|
||||
this.loading = false;
|
||||
}, (err) => {
|
||||
console.error('加载失败:', err);
|
||||
this.$message.error({
|
||||
message: '加载音色数据失败',
|
||||
showClose: true
|
||||
});
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
|
||||
@@ -328,6 +336,14 @@ export default {
|
||||
},
|
||||
|
||||
saveEdit(row) {
|
||||
if (!row.voiceCode || !row.voiceName || !row.languageType) {
|
||||
this.$message.error({
|
||||
message: '音色编码、音色名称和语言类型不能为空',
|
||||
showClose: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const params = {
|
||||
id: row.id,
|
||||
@@ -400,6 +416,12 @@ export default {
|
||||
},
|
||||
|
||||
addNew() {
|
||||
const hasEditing = this.ttsModels.some(row => row.editing);
|
||||
if (hasEditing) {
|
||||
this.$message.warning('请先完成当前编辑再新增');
|
||||
return;
|
||||
}
|
||||
|
||||
const maxSort = this.ttsModels.length > 0
|
||||
? Math.max(...this.ttsModels.map(item => Number(item.sort) || 0))
|
||||
: 0;
|
||||
@@ -419,105 +441,57 @@ export default {
|
||||
},
|
||||
|
||||
deleteRow(row) {
|
||||
this.$confirm("确定要删除该音色吗?", "警告", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
})
|
||||
.then(() => {
|
||||
Api.timbre.deleteVoice(row.id, (response) => {
|
||||
if (response.code === 0) {
|
||||
this.$message.success({
|
||||
message: "删除成功",
|
||||
showClose: true
|
||||
});
|
||||
this.loadData(); // 刷新数据
|
||||
} else {
|
||||
this.$message.error({
|
||||
message: response.msg || "删除失败",
|
||||
showClose: true
|
||||
});
|
||||
// 处理单个音色或音色数组
|
||||
const voices = Array.isArray(row) ? row : [row];
|
||||
|
||||
if (Array.isArray(row) && row.length === 0) {
|
||||
this.$message.warning("请先选择需要删除的音色");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const voiceCount = voices.length;
|
||||
this.$confirm(`确定要删除选中的${voiceCount}个音色吗?`, "警告", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
distinguishCancelAndClose: true
|
||||
}).then(() => {
|
||||
const ids = voices.map(voice => voice.id);
|
||||
if (ids.some(id => !id)) {
|
||||
this.$message.error("存在无效的音色ID");
|
||||
return;
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
this.$message.info("已取消删除");
|
||||
});
|
||||
},
|
||||
|
||||
batchDelete() {
|
||||
const selectedRows = this.filteredTtsModels.filter(row => row.selected);
|
||||
if (selectedRows.length === 0) {
|
||||
this.$message.warning("请先选择需要删除的音色");
|
||||
return;
|
||||
}
|
||||
|
||||
this.$confirm(`确定要删除选中的${selectedRows.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(
|
||||
selectedRows.map((row) => {
|
||||
return new Promise((resolve) => {
|
||||
Api.timbre.deleteVoice(row.id, (response) => {
|
||||
if (response.code === 0) {
|
||||
resolve({ success: true, id: row.id });
|
||||
} else {
|
||||
resolve({
|
||||
success: false,
|
||||
id: row.id,
|
||||
msg: response.msg || '删除失败'
|
||||
});
|
||||
}
|
||||
});
|
||||
Api.timbre.deleteVoice(ids, ({data}) => {
|
||||
if (data.code === 0) {
|
||||
this.$message.success({
|
||||
message: `成功删除${voiceCount}个参数`,
|
||||
showClose: true
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
const successCount = results.filter(r => r.success).length;
|
||||
const failCount = results.length - successCount;
|
||||
|
||||
if (failCount === 0) {
|
||||
this.$message.success({
|
||||
message: `成功删除${successCount}个音色`,
|
||||
showClose: true
|
||||
});
|
||||
} else if (successCount === 0) {
|
||||
this.$message.error({
|
||||
message: '删除失败,请重试',
|
||||
showClose: true
|
||||
});
|
||||
this.loadData(); // 刷新参数列表
|
||||
} else {
|
||||
this.$message.warning({
|
||||
message: `成功删除${successCount}个音色,${failCount}个删除失败`,
|
||||
showClose: true
|
||||
});
|
||||
this.$message.error({
|
||||
message: data.msg || '删除失败,请重试',
|
||||
showClose: true
|
||||
});
|
||||
}
|
||||
|
||||
this.loadData(); // 刷新数据
|
||||
} catch (error) {
|
||||
console.error('批量删除出错:', error);
|
||||
this.$message.error({
|
||||
message: '删除过程中发生错误',
|
||||
showClose: true
|
||||
});
|
||||
} finally {
|
||||
loading.close();
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
this.$message.info("已取消删除");
|
||||
});
|
||||
}).catch(action => {
|
||||
if (action === 'cancel') {
|
||||
this.$message({
|
||||
type: 'info',
|
||||
message: '已取消删除操作',
|
||||
duration: 1000
|
||||
});
|
||||
} else {
|
||||
this.$message({
|
||||
type: 'info',
|
||||
message: '操作已关闭',
|
||||
duration: 1000
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
isValidAudioUrl(url) {
|
||||
@@ -527,12 +501,12 @@ export default {
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
<style lang="scss" scoped>
|
||||
|
||||
::v-deep .el-dialog {
|
||||
border-radius: 8px !important;
|
||||
overflow: hidden;
|
||||
top: 8vh !important;
|
||||
top: 1vh !important;
|
||||
}
|
||||
|
||||
::v-deep .el-dialog__header {
|
||||
@@ -596,28 +570,26 @@ export default {
|
||||
|
||||
/* 备注文本 */
|
||||
::v-deep .remark-input .el-textarea__inner {
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #e6e6e6;
|
||||
padding: 8px 12px;
|
||||
resize: none;
|
||||
max-height: 40px !important;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
::v-deep .remark-input .el-textarea__inner::placeholder {
|
||||
color: black !important;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
::v-deep .remark-input .el-textarea__inner {
|
||||
background-color: #f4f6fa;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
::v-deep .remark-input .el-textarea__inner:focus {
|
||||
background-color: #edeffb;
|
||||
border-color: #409EFF !important;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
::v-deep .remark-input .el-textarea__inner::placeholder {
|
||||
color: #c0c4cc !important;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
|
||||
/* 滚动容器 */
|
||||
.scroll-wrapper {
|
||||
display: flex;
|
||||
@@ -627,14 +599,14 @@ export default {
|
||||
|
||||
.table-container {
|
||||
flex: 1;
|
||||
overflow-y: scroll;
|
||||
scrollbar-width: none; /* Firefox */
|
||||
padding-right: 15px; /* 为滚动条留出空间 */
|
||||
width: calc(100% - 16px); /* 减去滚动条宽度 */
|
||||
overflow: auto;
|
||||
scrollbar-width: none;
|
||||
padding-right: 15px;
|
||||
width: calc(100% - 16px);
|
||||
}
|
||||
|
||||
.table-container::-webkit-scrollbar {
|
||||
display: none; /* Chrome/Safari */
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 自定义滚动条 */
|
||||
@@ -684,10 +656,68 @@ export default {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 新增按钮组样式 */
|
||||
/* 音频播放器容器样式 */
|
||||
.custom-audio-container {
|
||||
width: 90%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.action-buttons .el-button {
|
||||
padding: 8px 15px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.edit-btn,
|
||||
.delete-btn,
|
||||
.save-btn {
|
||||
margin: 0 8px;
|
||||
color: #7079aa !important;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.edit-btn:hover,
|
||||
.delete-btn:hover,
|
||||
.save-btn:hover {
|
||||
color: #5f70f3 !important;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.save-btn {
|
||||
color: #5cca8e !important;
|
||||
}
|
||||
|
||||
/* 表格单元格自适应 */
|
||||
::v-deep .el-table__body-wrapper {
|
||||
overflow-x: hidden !important;
|
||||
}
|
||||
|
||||
::v-deep .el-table td {
|
||||
white-space: pre-wrap !important;
|
||||
word-break: break-all !important;
|
||||
}
|
||||
/* 按钮组定位调整 */
|
||||
.action-buttons {
|
||||
bottom: 20px;
|
||||
padding-top: 10px;
|
||||
position: static;
|
||||
padding: 15px 0;
|
||||
background: white;
|
||||
}
|
||||
|
||||
/* 输入框自适应 */
|
||||
::v-deep .el-input__inner,
|
||||
::v-deep .el-textarea__inner {
|
||||
width: 100% !important;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
/* 音频输入框特殊处理 */
|
||||
.audio-input ::v-deep .el-input__inner {
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
/* 操作按钮弹性布局 */
|
||||
::v-deep .el-table__row .el-button {
|
||||
flex-shrink: 0;
|
||||
margin: 2px !important;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<div class="copyright">
|
||||
<div class="footer-content">
|
||||
<span>{{ year }} {{ name }} {{ version }}</span>
|
||||
<template v-if="beianGaNum !== 'null'">
|
||||
<span v-if="beianIcpNum !== 'null' || name">|</span>
|
||||
<a :href="'http://www.beian.gov.cn/portal/registerSystemInfo?recordcode=' + beianGaNum" target="_blank"
|
||||
rel="noopener" class="beian-link">
|
||||
<img
|
||||
src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAMAAABhEH5lAAACXlBMVEUAAADax4rGxqP556zmsVD/6KXu5JvwvFPglEFnY3nnpEjosU1+XFTmuVfu13Hrv1jpsFa1k3mne2HlvnPdqmPqz3fu3YrJoWv04J/mz5z246aSi3i2p2/0zGj82WDrtFjar1O7klO4dkfimUJcWnbho0rgr1vntFfmvFflvGOZdFKXZkOUemnmv2XkuWWVe2ziqlXnt1Tr0HDpwl6ObmCkbEKqhVyOc2jdoVHmuljfrmDq0nPapWDUnErqy3XjtWXesXLrzHjz34rqzYfmw3fy4YrqyYDevW/dzoTIhkndsXLRsIneuHjlyIzQroTjuXjlyYfktWvw2prhvoHTmGfx45j3773TuYT/25LdijyKf321oXU+Nm3cumXfuWLtwlzpwVz40FvpvlvqvFfouFXjrlHipEjcmEfknEXPbzfbZyzZQR/YMBrWKBrhJhLdHxDTGg5HV4xBT4YYKYaWk4SDgYIWIYBybH0qLn1KS3tlZHpNS3WGfHIcHXEwLHAnF28rKW07NGuzn2pNQ2ruzWnDrGnuy2jryGjAnGgoH2j1x2RCNGPcrmIsEGGQdl/pu17YtV7FoV3muVxoRVzWqFvSmVp2SVbyw1XbkVTuvFPfo1PgmVPpt1LcnFLWkVLlp1F7W1HpsU92QE5oMUtFAUrpqUjnjUFOAEHUgz+DUj14FzvbczieMTdhATfodjXWcDXcbjXVcTS/Xi/hay7WXS3cWS3vYyvaVSrUVymnJSjXSyeKDSbNSiWfHCPMTyLFQiHkSR/lPxvVLRrPLBnUJRfUHhbaJxXVGxTZJxPNFhBdOhm/AAAAWXRSTlMABQIU/hIJ/v79/Pj29PPx4cC6s7CNfmxZWRv+/v7+/v7+/v78/Pr6+fPz8vHq6ujl5eTj4ODe2NTQzMW6ubKsnJeQkJCJiIOBgX9ubGpoW1lMREM0JR8dDgvYx1gAAAE7SURBVBjTYgADJmZJQUFJZiYGOGD2EzLV1jIT8paCibCJ86zctGPD1D4ecUaICKuPyYLMYznZOfNqzP0jwEJBOt1KB4/mHjqZ3dGsHwxW5M4Zk5l/PPdIQcGMShUPVqBlgQLlMSvyDq+P3HVidjWnkQQTA5MXV1SFYtb+jZGrl02si2J3Y2JgFMmILl68efueLVvXLSqLXirCCBgDow1HT+GS/AP7srblLS+K5bIHCjmodpXUr929d+eaVb3SsuouQCHhufPjShsbJk/rrK2KW8hiBxRyVkvnkI9tnaDQJDNFOU1DDOjRMEfe9Mjpiewz5RIzZmmKMoPcKqGbkJqSkBSfnDqpXS+ACeRpJ76W/uSUpDlpLPFtfK5soLAKFbU05Odm4eblN7YWk4KEGWOIp62FgYCVsG84SAAAL7BaooX965sAAAAASUVORK5CYII="
|
||||
class="beian-icon" alt="备案图标">
|
||||
<span class="beian-text">{{ beianGaNum }}</span>
|
||||
</a>
|
||||
</template>
|
||||
<template v-if="beianIcpNum !== 'null'">
|
||||
<span v-if="name">|</span>
|
||||
<a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener" class="beian-link">
|
||||
<span class="beian-text">{{ beianIcpNum }}</span>
|
||||
</a>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
|
||||
export default {
|
||||
name: 'VersionFooter',
|
||||
computed: {
|
||||
...mapState({
|
||||
version: state => state.pubConfig.version,
|
||||
name: state => state.pubConfig.name,
|
||||
beianIcpNum: state => state.pubConfig.beianIcpNum,
|
||||
beianGaNum: state => state.pubConfig.beianGaNum,
|
||||
year: state => state.pubConfig.year
|
||||
})
|
||||
},
|
||||
mounted() {
|
||||
this.$store.dispatch('fetchPubConfig')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.copyright {
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.footer-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.beian-link {
|
||||
text-decoration: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.beian-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.beian-text {
|
||||
color: #000;
|
||||
font-size: 12px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
</style>
|
||||
@@ -6,11 +6,16 @@ import App from './App.vue';
|
||||
import router from './router';
|
||||
import store from './store';
|
||||
import './styles/global.scss';
|
||||
import { register as registerServiceWorker } from './registerServiceWorker';
|
||||
|
||||
Vue.use(ElementUI);
|
||||
|
||||
Vue.config.productionTip = false
|
||||
|
||||
// 注册Service Worker
|
||||
registerServiceWorker();
|
||||
|
||||
// 创建Vue实例
|
||||
new Vue({
|
||||
router,
|
||||
store,
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
/* eslint-disable no-console */
|
||||
|
||||
export const register = () => {
|
||||
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
|
||||
window.addEventListener('load', () => {
|
||||
const swUrl = `${process.env.BASE_URL}service-worker.js`;
|
||||
|
||||
console.info(`[小智服务] 正在尝试注册Service Worker,URL: ${swUrl}`);
|
||||
|
||||
// 先检查Service Worker是否已注册
|
||||
navigator.serviceWorker.getRegistrations().then(registrations => {
|
||||
if (registrations.length > 0) {
|
||||
console.info('[小智服务] 发现已有Service Worker注册,正在检查更新');
|
||||
}
|
||||
|
||||
// 继续注册Service Worker
|
||||
navigator.serviceWorker
|
||||
.register(swUrl)
|
||||
.then(registration => {
|
||||
console.info('[小智服务] Service Worker注册成功');
|
||||
|
||||
// 更新处理
|
||||
registration.onupdatefound = () => {
|
||||
const installingWorker = registration.installing;
|
||||
if (installingWorker == null) {
|
||||
return;
|
||||
}
|
||||
installingWorker.onstatechange = () => {
|
||||
if (installingWorker.state === 'installed') {
|
||||
if (navigator.serviceWorker.controller) {
|
||||
// 内容已缓存更新,通知用户刷新
|
||||
console.log('[小智服务] 新内容可用,请刷新页面');
|
||||
// 可以在这里展示更新提示
|
||||
const updateNotification = document.createElement('div');
|
||||
updateNotification.style.cssText = `
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
background: #409EFF;
|
||||
color: white;
|
||||
padding: 12px 20px;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0,0,0,.1);
|
||||
z-index: 9999;
|
||||
`;
|
||||
updateNotification.innerHTML = `
|
||||
<div style="display: flex; align-items: center;">
|
||||
<span style="margin-right: 10px;">发现新版本,点击刷新应用</span>
|
||||
<button style="background: white; color: #409EFF; border: none; padding: 5px 10px; border-radius: 3px; cursor: pointer;">刷新</button>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(updateNotification);
|
||||
updateNotification.querySelector('button').addEventListener('click', () => {
|
||||
window.location.reload();
|
||||
});
|
||||
} else {
|
||||
// 一切正常,Service Worker已成功安装
|
||||
console.log('[小智服务] 内容已缓存供离线使用');
|
||||
|
||||
// 可以在这里初始化缓存
|
||||
setTimeout(() => {
|
||||
// 预热CDN缓存
|
||||
const cdnUrls = [
|
||||
'https://unpkg.com/element-ui@2.15.14/lib/theme-chalk/index.css',
|
||||
'https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css',
|
||||
'https://unpkg.com/vue@2.6.14/dist/vue.min.js',
|
||||
'https://unpkg.com/vue-router@3.6.5/dist/vue-router.min.js',
|
||||
'https://unpkg.com/vuex@3.6.2/dist/vuex.min.js',
|
||||
'https://unpkg.com/element-ui@2.15.14/lib/index.js',
|
||||
'https://unpkg.com/axios@0.27.2/dist/axios.min.js',
|
||||
'https://unpkg.com/opus-decoder@0.7.7/dist/opus-decoder.min.js'
|
||||
];
|
||||
|
||||
// 预热缓存
|
||||
cdnUrls.forEach(url => {
|
||||
fetch(url, { mode: 'no-cors' }).catch(err => {
|
||||
console.log(`预热缓存 ${url} 失败`, err);
|
||||
});
|
||||
});
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Service Worker 注册失败:', error);
|
||||
|
||||
if (error.name === 'TypeError' && error.message.includes('Failed to register a ServiceWorker')) {
|
||||
console.warn('[小智服务] 注册Service Worker时出现网络错误,CDN资源可能无法缓存');
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
console.info(
|
||||
'可能原因:1. 服务器未配置正确的MIME类型 2. 服务器SSL证书问题 3. 服务器未返回service-worker.js文件'
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const unregister = () => {
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.ready
|
||||
.then(registration => {
|
||||
registration.unregister();
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error.message);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -39,6 +39,13 @@ const routes = [
|
||||
return import('../views/register.vue')
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/retrieve-password',
|
||||
name: 'RetrievePassword',
|
||||
component: function () {
|
||||
return import('../views/retrievePassword.vue')
|
||||
}
|
||||
},
|
||||
// 设备管理页面路由
|
||||
{
|
||||
path: '/device-management',
|
||||
@@ -62,13 +69,6 @@ const routes = [
|
||||
return import('../views/ModelConfig.vue')
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/test',
|
||||
name: 'TestServer',
|
||||
component: function () {
|
||||
return import('../views/test.vue')
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/params-management',
|
||||
name: 'ParamsManagement',
|
||||
@@ -80,9 +80,46 @@ const routes = [
|
||||
title: '参数管理'
|
||||
}
|
||||
},
|
||||
]
|
||||
|
||||
{
|
||||
path: '/server-side-management',
|
||||
name: 'ServerSideManager',
|
||||
component: function () {
|
||||
return import('../views/ServerSideManager.vue')
|
||||
},
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
title: '服务端管理'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/ota-management',
|
||||
name: 'OtaManagement',
|
||||
component: function () {
|
||||
return import('../views/OtaManagement.vue')
|
||||
},
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
title: 'OTA管理'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/dict-management',
|
||||
name: 'DictManagement',
|
||||
component: function () {
|
||||
return import('../views/DictManagement.vue')
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/provider-management',
|
||||
name: 'ProviderManagement',
|
||||
component: function () {
|
||||
return import('../views/ProviderManagement.vue')
|
||||
}
|
||||
},
|
||||
]
|
||||
const router = new VueRouter({
|
||||
base: process.env.VUE_APP_PUBLIC_PATH || '/',
|
||||
routes
|
||||
})
|
||||
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
/* global self, workbox */
|
||||
|
||||
// 自定义Service Worker安装和激活的处理逻辑
|
||||
self.addEventListener('message', (event) => {
|
||||
if (event.data && event.data.type === 'SKIP_WAITING') {
|
||||
self.skipWaiting();
|
||||
}
|
||||
});
|
||||
|
||||
// CDN资源列表
|
||||
const CDN_CSS = [
|
||||
'https://unpkg.com/element-ui@2.15.14/lib/theme-chalk/index.css',
|
||||
'https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css'
|
||||
];
|
||||
|
||||
const CDN_JS = [
|
||||
'https://unpkg.com/vue@2.6.14/dist/vue.min.js',
|
||||
'https://unpkg.com/vue-router@3.6.5/dist/vue-router.min.js',
|
||||
'https://unpkg.com/vuex@3.6.2/dist/vuex.min.js',
|
||||
'https://unpkg.com/element-ui@2.15.14/lib/index.js',
|
||||
'https://unpkg.com/axios@0.27.2/dist/axios.min.js',
|
||||
'https://unpkg.com/opus-decoder@0.7.7/dist/opus-decoder.min.js'
|
||||
];
|
||||
|
||||
// 当Service Worker被注入manifest后会自动执行
|
||||
const manifest = self.__WB_MANIFEST || [];
|
||||
|
||||
// 检查是否启用CDN模式
|
||||
const isCDNEnabled = manifest.some(entry =>
|
||||
entry.url === 'cdn-mode' && entry.revision === 'enabled'
|
||||
);
|
||||
|
||||
console.log(`Service Worker 已初始化, CDN模式: ${isCDNEnabled ? '启用' : '禁用'}`);
|
||||
|
||||
// 注入workbox相关代码
|
||||
importScripts('https://storage.googleapis.com/workbox-cdn/releases/7.0.0/workbox-sw.js');
|
||||
workbox.setConfig({ debug: false });
|
||||
|
||||
// 开启workbox
|
||||
workbox.core.skipWaiting();
|
||||
workbox.core.clientsClaim();
|
||||
|
||||
// 预缓存离线页面
|
||||
const OFFLINE_URL = '/offline.html';
|
||||
workbox.precaching.precacheAndRoute([
|
||||
{ url: OFFLINE_URL, revision: null }
|
||||
]);
|
||||
|
||||
// 添加安装完成事件处理器,在控制台显示安装消息
|
||||
self.addEventListener('install', event => {
|
||||
if (isCDNEnabled) {
|
||||
console.log('Service Worker 已安装,开始缓存CDN资源');
|
||||
} else {
|
||||
console.log('Service Worker 已安装,CDN模式禁用,仅缓存本地资源');
|
||||
}
|
||||
|
||||
// 确保离线页面被缓存
|
||||
event.waitUntil(
|
||||
caches.open('offline-cache').then((cache) => {
|
||||
return cache.add(OFFLINE_URL);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// 添加激活事件处理器
|
||||
self.addEventListener('activate', event => {
|
||||
console.log('Service Worker 已激活,现在控制着页面');
|
||||
|
||||
// 清理旧版本缓存
|
||||
event.waitUntil(
|
||||
caches.keys().then(cacheNames => {
|
||||
return Promise.all(
|
||||
cacheNames.filter(cacheName => {
|
||||
// 清理除当前版本外的缓存
|
||||
return cacheName.startsWith('workbox-') && !workbox.core.cacheNames.runtime.includes(cacheName);
|
||||
}).map(cacheName => {
|
||||
return caches.delete(cacheName);
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// 添加fetch事件拦截器,用于查看CDN资源是否命中缓存
|
||||
self.addEventListener('fetch', event => {
|
||||
// 只有启用CDN模式时才进行CDN资源缓存监控
|
||||
if (isCDNEnabled) {
|
||||
const url = new URL(event.request.url);
|
||||
|
||||
// 针对CDN资源,输出是否命中缓存的信息
|
||||
if ([...CDN_CSS, ...CDN_JS].includes(url.href)) {
|
||||
// 不干扰正常的fetch流程,只添加日志
|
||||
console.log(`请求CDN资源: ${url.href}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 仅在CDN模式下缓存CDN资源
|
||||
if (isCDNEnabled) {
|
||||
// 缓存CDN的CSS资源
|
||||
workbox.routing.registerRoute(
|
||||
({ url }) => CDN_CSS.includes(url.href),
|
||||
new workbox.strategies.CacheFirst({
|
||||
cacheName: 'cdn-stylesheets',
|
||||
plugins: [
|
||||
new workbox.expiration.ExpirationPlugin({
|
||||
maxAgeSeconds: 365 * 24 * 60 * 60, // 增加到1年缓存
|
||||
maxEntries: 10, // 最多缓存10个CSS文件
|
||||
}),
|
||||
new workbox.cacheableResponse.CacheableResponsePlugin({
|
||||
statuses: [0, 200], // 缓存成功响应
|
||||
}),
|
||||
],
|
||||
})
|
||||
);
|
||||
|
||||
// 缓存CDN的JS资源
|
||||
workbox.routing.registerRoute(
|
||||
({ url }) => CDN_JS.includes(url.href),
|
||||
new workbox.strategies.CacheFirst({
|
||||
cacheName: 'cdn-scripts',
|
||||
plugins: [
|
||||
new workbox.expiration.ExpirationPlugin({
|
||||
maxAgeSeconds: 365 * 24 * 60 * 60, // 增加到1年缓存
|
||||
maxEntries: 20, // 最多缓存20个JS文件
|
||||
}),
|
||||
new workbox.cacheableResponse.CacheableResponsePlugin({
|
||||
statuses: [0, 200], // 缓存成功响应
|
||||
}),
|
||||
],
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// 无论是否启用CDN模式,都缓存本地静态资源
|
||||
workbox.routing.registerRoute(
|
||||
/\.(?:js|css|png|jpg|jpeg|svg|gif|ico|woff|woff2|eot|ttf|otf)$/,
|
||||
new workbox.strategies.StaleWhileRevalidate({
|
||||
cacheName: 'static-resources',
|
||||
plugins: [
|
||||
new workbox.expiration.ExpirationPlugin({
|
||||
maxAgeSeconds: 7 * 24 * 60 * 60, // 7天缓存
|
||||
maxEntries: 50, // 最多缓存50个文件
|
||||
}),
|
||||
],
|
||||
})
|
||||
);
|
||||
|
||||
// 缓存HTML页面
|
||||
workbox.routing.registerRoute(
|
||||
/\.html$/,
|
||||
new workbox.strategies.NetworkFirst({
|
||||
cacheName: 'html-cache',
|
||||
plugins: [
|
||||
new workbox.expiration.ExpirationPlugin({
|
||||
maxAgeSeconds: 1 * 24 * 60 * 60, // 1天缓存
|
||||
maxEntries: 10, // 最多缓存10个HTML文件
|
||||
}),
|
||||
],
|
||||
})
|
||||
);
|
||||
|
||||
// 离线页面 - 使用更可靠的处理方式
|
||||
workbox.routing.setCatchHandler(async ({ event }) => {
|
||||
// 根据请求类型返回适当的默认页面
|
||||
switch (event.request.destination) {
|
||||
case 'document':
|
||||
// 如果是网页请求,返回离线页面
|
||||
return caches.match(OFFLINE_URL);
|
||||
default:
|
||||
// 所有其他请求返回错误
|
||||
return Response.error();
|
||||
}
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
import { goToPage } from "@/utils";
|
||||
import Vue from 'vue';
|
||||
import Vuex from 'vuex';
|
||||
import Api from '../apis/api';
|
||||
import Constant from '../utils/constant';
|
||||
|
||||
Vue.use(Vuex)
|
||||
@@ -9,7 +10,13 @@ export default new Vuex.Store({
|
||||
state: {
|
||||
token: '',
|
||||
userInfo: {}, // 添加用户信息存储
|
||||
isSuperAdmin: false // 添加superAdmin状态
|
||||
isSuperAdmin: false, // 添加superAdmin状态
|
||||
pubConfig: { // 添加公共配置存储
|
||||
version: '',
|
||||
beianIcpNum: 'null',
|
||||
beianGaNum: 'null',
|
||||
allowUserRegister: false
|
||||
}
|
||||
},
|
||||
getters: {
|
||||
getToken(state) {
|
||||
@@ -26,6 +33,9 @@ export default new Vuex.Store({
|
||||
return state.isSuperAdmin
|
||||
}
|
||||
return localStorage.getItem('isSuperAdmin') === 'true'
|
||||
},
|
||||
getPubConfig(state) {
|
||||
return state.pubConfig
|
||||
}
|
||||
},
|
||||
mutations: {
|
||||
@@ -39,6 +49,9 @@ export default new Vuex.Store({
|
||||
state.isSuperAdmin = isSuperAdmin
|
||||
localStorage.setItem('isSuperAdmin', isSuperAdmin)
|
||||
},
|
||||
setPubConfig(state, config) {
|
||||
state.pubConfig = config
|
||||
},
|
||||
clearAuth(state) {
|
||||
state.token = ''
|
||||
state.userInfo = {}
|
||||
@@ -55,6 +68,17 @@ export default new Vuex.Store({
|
||||
goToPage(Constant.PAGE.LOGIN, true);
|
||||
window.location.reload(); // 彻底重置状态
|
||||
})
|
||||
},
|
||||
// 添加获取公共配置的 action
|
||||
fetchPubConfig({ commit }) {
|
||||
return new Promise((resolve) => {
|
||||
Api.user.getPubConfig(({ data }) => {
|
||||
if (data.code === 0) {
|
||||
commit('setPubConfig', data.data);
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
modules: {
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
/**
|
||||
* 缓存查看工具 - 用于检查CDN资源是否已被Service Worker缓存
|
||||
*/
|
||||
|
||||
/**
|
||||
* 获取所有Service Worker缓存的名称
|
||||
* @returns {Promise<string[]>} 缓存名称列表
|
||||
*/
|
||||
export const getCacheNames = async () => {
|
||||
if (!('caches' in window)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
return await caches.keys();
|
||||
} catch (error) {
|
||||
console.error('获取缓存名称失败:', error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取指定缓存中的所有URL
|
||||
* @param {string} cacheName 缓存名称
|
||||
* @returns {Promise<string[]>} 缓存的URL列表
|
||||
*/
|
||||
export const getCacheUrls = async (cacheName) => {
|
||||
if (!('caches' in window)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const cache = await caches.open(cacheName);
|
||||
const requests = await cache.keys();
|
||||
return requests.map(request => request.url);
|
||||
} catch (error) {
|
||||
console.error(`获取缓存 ${cacheName} 的URL失败:`, error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 检查特定URL是否已被缓存
|
||||
* @param {string} url 要检查的URL
|
||||
* @returns {Promise<boolean>} 是否已缓存
|
||||
*/
|
||||
export const isUrlCached = async (url) => {
|
||||
if (!('caches' in window)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const cacheNames = await getCacheNames();
|
||||
for (const cacheName of cacheNames) {
|
||||
const cache = await caches.open(cacheName);
|
||||
const match = await cache.match(url);
|
||||
if (match) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error(`检查URL ${url} 是否缓存失败:`, error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取当前页面所有CDN资源的缓存状态
|
||||
* @returns {Promise<Object>} 缓存状态对象
|
||||
*/
|
||||
export const checkCdnCacheStatus = async () => {
|
||||
// 从CDN缓存中查找资源
|
||||
const cdnCaches = ['cdn-stylesheets', 'cdn-scripts'];
|
||||
const results = {
|
||||
css: [],
|
||||
js: [],
|
||||
totalCached: 0,
|
||||
totalNotCached: 0
|
||||
};
|
||||
|
||||
for (const cacheName of cdnCaches) {
|
||||
try {
|
||||
const urls = await getCacheUrls(cacheName);
|
||||
|
||||
// 区分CSS和JS资源
|
||||
for (const url of urls) {
|
||||
if (url.endsWith('.css')) {
|
||||
results.css.push({ url, cached: true });
|
||||
} else if (url.endsWith('.js')) {
|
||||
results.js.push({ url, cached: true });
|
||||
}
|
||||
results.totalCached++;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`获取 ${cacheName} 缓存信息失败:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
};
|
||||
|
||||
/**
|
||||
* 清除所有Service Worker缓存
|
||||
* @returns {Promise<boolean>} 是否成功清除
|
||||
*/
|
||||
export const clearAllCaches = async () => {
|
||||
if (!('caches' in window)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const cacheNames = await getCacheNames();
|
||||
for (const cacheName of cacheNames) {
|
||||
await caches.delete(cacheName);
|
||||
}
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('清除所有缓存失败:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 将缓存状态输出到控制台
|
||||
*/
|
||||
export const logCacheStatus = async () => {
|
||||
console.group('Service Worker 缓存状态');
|
||||
|
||||
const cacheNames = await getCacheNames();
|
||||
console.log('已发现的缓存:', cacheNames);
|
||||
|
||||
for (const cacheName of cacheNames) {
|
||||
const urls = await getCacheUrls(cacheName);
|
||||
console.group(`缓存: ${cacheName} (${urls.length} 项)`);
|
||||
urls.forEach(url => console.log(url));
|
||||
console.groupEnd();
|
||||
}
|
||||
|
||||
console.groupEnd();
|
||||
return cacheNames.length > 0;
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
// 日期格式化函数
|
||||
export function formatDate(date) {
|
||||
if (!date) return '';
|
||||
const d = new Date(date);
|
||||
const year = d.getFullYear();
|
||||
const month = String(d.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(d.getDate()).padStart(2, '0');
|
||||
const hour = String(d.getHours()).padStart(2, '0');
|
||||
const minute = String(d.getMinutes()).padStart(2, '0');
|
||||
const second = String(d.getSeconds()).padStart(2, '0');
|
||||
return `${year}-${month}-${day} ${hour}:${minute}:${second}`;
|
||||
}
|
||||
|
||||
// 文件大小格式化函数
|
||||
export function formatFileSize(bytes) {
|
||||
if (!bytes || bytes === 0) return '0 B';
|
||||
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
const k = 1024;
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + units[i];
|
||||
}
|
||||
@@ -136,3 +136,67 @@ export function getUUID() {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证手机号格式
|
||||
* @param {string} mobile 手机号
|
||||
* @param {string} areaCode 区号
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function validateMobile(mobile, areaCode) {
|
||||
// 移除所有非数字字符
|
||||
const cleanMobile = mobile.replace(/\D/g, '');
|
||||
|
||||
// 根据不同区号使用不同的验证规则
|
||||
switch (areaCode) {
|
||||
case '+86': // 中国大陆
|
||||
return /^1[3-9]\d{9}$/.test(cleanMobile);
|
||||
case '+852': // 中国香港
|
||||
return /^[569]\d{7}$/.test(cleanMobile);
|
||||
case '+853': // 中国澳门
|
||||
return /^6\d{7}$/.test(cleanMobile);
|
||||
case '+886': // 中国台湾
|
||||
return /^9\d{8}$/.test(cleanMobile);
|
||||
case '+1': // 美国/加拿大
|
||||
return /^[2-9]\d{9}$/.test(cleanMobile);
|
||||
case '+44': // 英国
|
||||
return /^7[1-9]\d{8}$/.test(cleanMobile);
|
||||
case '+81': // 日本
|
||||
return /^[7890]\d{8}$/.test(cleanMobile);
|
||||
case '+82': // 韩国
|
||||
return /^1[0-9]\d{7}$/.test(cleanMobile);
|
||||
case '+65': // 新加坡
|
||||
return /^[89]\d{7}$/.test(cleanMobile);
|
||||
case '+61': // 澳大利亚
|
||||
return /^[4578]\d{8}$/.test(cleanMobile);
|
||||
case '+49': // 德国
|
||||
return /^1[5-7]\d{8}$/.test(cleanMobile);
|
||||
case '+33': // 法国
|
||||
return /^[67]\d{8}$/.test(cleanMobile);
|
||||
case '+39': // 意大利
|
||||
return /^3[0-9]\d{8}$/.test(cleanMobile);
|
||||
case '+34': // 西班牙
|
||||
return /^[6-9]\d{8}$/.test(cleanMobile);
|
||||
case '+55': // 巴西
|
||||
return /^[1-9]\d{10}$/.test(cleanMobile);
|
||||
case '+91': // 印度
|
||||
return /^[6-9]\d{9}$/.test(cleanMobile);
|
||||
case '+971': // 阿联酋
|
||||
return /^[5]\d{8}$/.test(cleanMobile);
|
||||
case '+966': // 沙特阿拉伯
|
||||
return /^[5]\d{8}$/.test(cleanMobile);
|
||||
case '+880': // 孟加拉国
|
||||
return /^1[3-9]\d{8}$/.test(cleanMobile);
|
||||
case '+234': // 尼日利亚
|
||||
return /^[789]\d{9}$/.test(cleanMobile);
|
||||
case '+254': // 肯尼亚
|
||||
return /^[17]\d{8}$/.test(cleanMobile);
|
||||
case '+255': // 坦桑尼亚
|
||||
return /^[67]\d{8}$/.test(cleanMobile);
|
||||
case '+7': // 哈萨克斯坦
|
||||
return /^[67]\d{9}$/.test(cleanMobile);
|
||||
default:
|
||||
// 其他国际号码:至少5位,最多15位
|
||||
return /^\d{5,15}$/.test(cleanMobile);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,54 +1,98 @@
|
||||
<template>
|
||||
<div class="welcome">
|
||||
<HeaderBar />
|
||||
<el-main style="padding: 20px; display: flex; flex-direction: column;">
|
||||
<div class="table-container">
|
||||
<h3 class="device-list-title">设备列表</h3>
|
||||
<el-button type="primary" class="add-device-btn" @click="handleAddDevice">
|
||||
+ 添加设备
|
||||
</el-button>
|
||||
<el-table :data="paginatedDeviceList" style="width: 100%; margin-top: 20px" border stripe>
|
||||
<el-table-column label="设备型号" prop="model" flex></el-table-column>
|
||||
<el-table-column label="固件版本" prop="firmwareVersion" width="120"></el-table-column>
|
||||
<el-table-column label="Mac地址" prop="macAddress"></el-table-column>
|
||||
<el-table-column label="绑定时间" prop="bindTime" width="200"></el-table-column>
|
||||
<el-table-column label="最近对话" prop="lastConversation" width="140"></el-table-column>
|
||||
<el-table-column label="备注" width="180">
|
||||
<template slot-scope="scope">
|
||||
<el-input v-if="scope.row.isEdit" v-model="scope.row.remark" size="mini"
|
||||
@blur="stopEditRemark(scope.$index)"></el-input>
|
||||
<span v-else>
|
||||
<i v-if="!scope.row.remark" class="el-icon-edit" @click="startEditRemark(scope.$index, scope.row)"></i>
|
||||
<span v-else @click="startEditRemark(scope.$index, scope.row)">
|
||||
{{ scope.row.remark }}
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="OTA升级" width="120">
|
||||
<template slot-scope="scope">
|
||||
<el-switch v-model="scope.row.otaSwitch" size="mini" active-color="#13ce66"
|
||||
inactive-color="#ff4949"></el-switch>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="80">
|
||||
<template slot-scope="scope">
|
||||
<el-button size="mini" type="text" @click="handleUnbind(scope.row.device_id)" style="color: #ff4949">
|
||||
解绑
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination class="pagination" @size-change="handleSizeChange" @current-change="handleCurrentChange"
|
||||
:current-page="currentPage" :page-sizes="[5, 10, 20, 50]" :page-size="pageSize"
|
||||
layout="total, sizes, prev, pager, next, jumper" :total="deviceList.length"></el-pagination>
|
||||
|
||||
<div class="operation-bar">
|
||||
<h2 class="page-title">设备管理</h2>
|
||||
<div class="right-operations">
|
||||
<el-input placeholder="请输入设备型号或Mac地址查询" v-model="searchKeyword" class="search-input"
|
||||
@keyup.enter.native="handleSearch" clearable />
|
||||
<el-button class="btn-search" @click="handleSearch">搜索</el-button>
|
||||
</div>
|
||||
<div class="copyright">
|
||||
©2025 xiaozhi-esp32-server
|
||||
</div>
|
||||
|
||||
<div class="main-wrapper">
|
||||
<div class="content-panel">
|
||||
<div class="content-area">
|
||||
<el-card class="device-card" shadow="never">
|
||||
<el-table ref="deviceTable" :data="paginatedDeviceList" class="transparent-table"
|
||||
:header-cell-class-name="headerCellClassName" v-loading="loading" element-loading-text="拼命加载中"
|
||||
element-loading-spinner="el-icon-loading" element-loading-background="rgba(255, 255, 255, 0.7)">
|
||||
<el-table-column label="选择" align="center" width="120">
|
||||
<template slot-scope="scope">
|
||||
<el-checkbox v-model="scope.row.selected"></el-checkbox>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="设备型号" prop="model" align="center">
|
||||
<template slot-scope="scope">
|
||||
{{ getFirmwareTypeName(scope.row.model) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="固件版本" prop="firmwareVersion" align="center"></el-table-column>
|
||||
<el-table-column label="Mac地址" prop="macAddress" align="center"></el-table-column>
|
||||
<el-table-column label="绑定时间" prop="bindTime" align="center"></el-table-column>
|
||||
<el-table-column label="最近对话" prop="lastConversation" align="center"></el-table-column>
|
||||
<el-table-column label="备注" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-input v-if="scope.row.isEdit" v-model="scope.row.remark" size="mini"
|
||||
@blur="stopEditRemark(scope.$index)"></el-input>
|
||||
<span v-else>
|
||||
<i v-if="!scope.row.remark" class="el-icon-edit"
|
||||
@click="startEditRemark(scope.$index, scope.row)"></i>
|
||||
<span v-else @click="startEditRemark(scope.$index, scope.row)">
|
||||
{{ scope.row.remark }}
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="OTA升级" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-switch v-model="scope.row.otaSwitch" size="mini" active-color="#13ce66" inactive-color="#ff4949"
|
||||
@change="handleOtaSwitchChange(scope.row)"></el-switch>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-button size="mini" type="text" @click="handleUnbind(scope.row.device_id)">
|
||||
解绑
|
||||
</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">
|
||||
{{ isAllSelected ? '取消全选' : '全选' }}
|
||||
</el-button>
|
||||
<el-button type="success" size="mini" class="add-device-btn" @click="handleAddDevice">
|
||||
新增
|
||||
</el-button>
|
||||
<el-button size="mini" type="danger" icon="el-icon-delete" @click="deleteSelected">解绑</el-button>
|
||||
</div>
|
||||
<div class="custom-pagination">
|
||||
<el-select v-model="pageSize" @change="handlePageSizeChange" class="page-size-select">
|
||||
<el-option v-for="item in pageSizeOptions" :key="item" :label="`${item}条/页`" :value="item">
|
||||
</el-option>
|
||||
</el-select>
|
||||
<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">共{{ deviceList.length }}条记录</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
<AddDeviceDialog :visible.sync="addDeviceDialogVisible" :agent-id="currentAgentId"
|
||||
@refresh="fetchBindDevices(currentAgentId)" />
|
||||
</el-main>
|
||||
</div>
|
||||
|
||||
<AddDeviceDialog :visible.sync="addDeviceDialogVisible" :agent-id="currentAgentId"
|
||||
@refresh="fetchBindDevices(currentAgentId)" />
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -62,20 +106,56 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
addDeviceDialogVisible: false,
|
||||
selectedDevices: [],
|
||||
isAllSelected: false,
|
||||
searchKeyword: "",
|
||||
activeSearchKeyword: "",
|
||||
currentAgentId: this.$route.query.agentId || '',
|
||||
currentPage: 1,
|
||||
pageSize: 5,
|
||||
pageSize: 10,
|
||||
pageSizeOptions: [10, 20, 50, 100],
|
||||
deviceList: [],
|
||||
loading: false,
|
||||
userApi: null,
|
||||
firmwareTypes: [],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
filteredDeviceList() {
|
||||
const keyword = this.activeSearchKeyword.toLowerCase();
|
||||
if (!keyword) return this.deviceList;
|
||||
return this.deviceList.filter(device =>
|
||||
(device.model && device.model.toLowerCase().includes(keyword)) ||
|
||||
(device.macAddress && device.macAddress.toLowerCase().includes(keyword))
|
||||
);
|
||||
},
|
||||
|
||||
paginatedDeviceList() {
|
||||
const start = (this.currentPage - 1) * this.pageSize;
|
||||
const end = start + this.pageSize;
|
||||
return this.deviceList.slice(start, end);
|
||||
}
|
||||
return this.filteredDeviceList.slice(start, end).map(item => ({
|
||||
...item,
|
||||
selected: false
|
||||
}));
|
||||
},
|
||||
pageCount() {
|
||||
return Math.ceil(this.filteredDeviceList.length / 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;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
const agentId = this.$route.query.agentId;
|
||||
@@ -83,7 +163,87 @@ export default {
|
||||
this.fetchBindDevices(agentId);
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.getFirmwareTypes()
|
||||
},
|
||||
methods: {
|
||||
async getFirmwareTypes() {
|
||||
try {
|
||||
const res = await Api.dict.getDictDataByType('FIRMWARE_TYPE')
|
||||
this.firmwareTypes = res.data
|
||||
} catch (error) {
|
||||
console.error('获取固件类型失败:', error)
|
||||
this.$message.error(error.message || '获取固件类型失败')
|
||||
}
|
||||
},
|
||||
handlePageSizeChange(val) {
|
||||
this.pageSize = val;
|
||||
this.currentPage = 1;
|
||||
},
|
||||
handleSearch() {
|
||||
this.activeSearchKeyword = this.searchKeyword;
|
||||
this.currentPage = 1;
|
||||
},
|
||||
|
||||
handleSelectAll() {
|
||||
this.isAllSelected = !this.isAllSelected;
|
||||
this.paginatedDeviceList.forEach(row => {
|
||||
row.selected = this.isAllSelected;
|
||||
});
|
||||
this.selectedDevices = this.paginatedDeviceList.filter(device => device.selected);
|
||||
},
|
||||
|
||||
deleteSelected() {
|
||||
this.selectedDevices = this.paginatedDeviceList.filter(device => device.selected);
|
||||
if (this.selectedDevices.length === 0) {
|
||||
this.$message.warning({
|
||||
message: '请至少选择一条记录',
|
||||
showClose: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.$confirm(`确认要解绑选中的 ${this.selectedDevices.length} 台设备吗?`, '警告', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
const deviceIds = this.selectedDevices.map(device => device.device_id);
|
||||
this.batchUnbindDevices(deviceIds);
|
||||
});
|
||||
},
|
||||
|
||||
batchUnbindDevices(deviceIds) {
|
||||
const promises = deviceIds.map(id => {
|
||||
return new Promise((resolve, reject) => {
|
||||
Api.device.unbindDevice(id, ({ data }) => {
|
||||
if (data.code === 0) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(data.msg || '解绑失败');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Promise.all(promises)
|
||||
.then(() => {
|
||||
this.$message.success({
|
||||
message: `成功解绑 ${deviceIds.length} 台设备`,
|
||||
showClose: true
|
||||
});
|
||||
this.fetchBindDevices(this.currentAgentId);
|
||||
this.selectedDevices = [];
|
||||
this.isAllSelected = false;
|
||||
})
|
||||
.catch(error => {
|
||||
this.$message.error({
|
||||
message: error || '批量解绑过程中出现错误',
|
||||
showClose: true
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
handleAddDevice() {
|
||||
this.addDeviceDialogVisible = true;
|
||||
},
|
||||
@@ -115,46 +275,68 @@ export default {
|
||||
});
|
||||
});
|
||||
},
|
||||
handleSizeChange(val) {
|
||||
this.pageSize = val;
|
||||
goFirst() {
|
||||
this.currentPage = 1;
|
||||
},
|
||||
handleCurrentChange(val) {
|
||||
this.currentPage = val;
|
||||
goPrev() {
|
||||
if (this.currentPage > 1) this.currentPage--;
|
||||
},
|
||||
goNext() {
|
||||
if (this.currentPage < this.pageCount) this.currentPage++;
|
||||
},
|
||||
goToPage(page) {
|
||||
this.currentPage = page;
|
||||
},
|
||||
|
||||
fetchBindDevices(agentId) {
|
||||
this.loading = true;
|
||||
Api.device.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, // 使用格式化后的时间
|
||||
bindTime: device.createDate,
|
||||
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);
|
||||
this.activeSearchKeyword = "";
|
||||
this.searchKeyword = "";
|
||||
} else {
|
||||
this.$message.error(data.msg || '获取设备列表失败');
|
||||
}
|
||||
});
|
||||
},
|
||||
headerCellClassName({ columnIndex }) {
|
||||
if (columnIndex === 0) {
|
||||
return "custom-selection-header";
|
||||
}
|
||||
return "";
|
||||
},
|
||||
getFirmwareTypeName(type) {
|
||||
const firmwareType = this.firmwareTypes.find(item => item.key === type)
|
||||
return firmwareType ? firmwareType.name : type
|
||||
},
|
||||
handleOtaSwitchChange(row) {
|
||||
Api.device.enableOtaUpgrade(row.device_id, row.otaSwitch ? 1 : 0, ({ data }) => {
|
||||
if (data.code === 0) {
|
||||
this.$message.success(row.otaSwitch ? '已设置成自动升级' : '已关闭自动升级')
|
||||
} else {
|
||||
row.otaSwitch = !row.otaSwitch
|
||||
this.$message.error(data.msg || '操作失败')
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -163,58 +345,360 @@ export default {
|
||||
min-height: 506px;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
position: relative;
|
||||
flex-direction: column;
|
||||
background: linear-gradient(145deg, #e6eeff, #eff0ff);
|
||||
background: linear-gradient(to bottom right, #dce8ff, #e4eeff, #e6cbfd);
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
-webkit-background-size: cover;
|
||||
-o-background-size: cover;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
background: #f9fafc;
|
||||
padding: 20px;
|
||||
border-radius: 20px;
|
||||
.main-wrapper {
|
||||
margin: 5px 22px;
|
||||
border-radius: 15px;
|
||||
min-height: calc(100vh - 24vh);
|
||||
height: auto;
|
||||
max-height: 80vh;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.add-device-btn {
|
||||
float: right;
|
||||
background: #409eff;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
width: 105px;
|
||||
height: 32px;
|
||||
position: relative;
|
||||
background: rgba(237, 242, 255, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
gap: 8px;
|
||||
margin-bottom: 15px;
|
||||
|
||||
&:hover {
|
||||
background: #3a8ee6;
|
||||
}
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.device-list-title {
|
||||
float: left;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
margin: 5px;
|
||||
.operation-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 24px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 24px;
|
||||
margin: 0;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.el-icon-edit {
|
||||
color: #409eff;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
vertical-align: middle;
|
||||
.right-operations {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin-top: 20px;
|
||||
text-align: right;
|
||||
.search-input {
|
||||
width: 280px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.btn-search {
|
||||
background: linear-gradient(135deg, #6b8cff, #a966ff);
|
||||
border: none;
|
||||
color: white;
|
||||
}
|
||||
|
||||
::v-deep .search-input .el-input__inner {
|
||||
border-radius: 4px;
|
||||
border: 1px solid #DCDFE6;
|
||||
background-color: white;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
::v-deep .page-size-select {
|
||||
width: 100px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
::v-deep .page-size-select .el-input__inner {
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #e4e7ed;
|
||||
background: #dee7ff;
|
||||
color: #606266;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
::v-deep .page-size-select .el-input__suffix {
|
||||
right: 6px;
|
||||
width: 15px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
top: 6px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::v-deep .page-size-select .el-input__suffix-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
::v-deep .page-size-select .el-icon-arrow-up:before {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
border-left: 6px solid transparent;
|
||||
border-right: 6px solid transparent;
|
||||
border-top: 9px solid #606266;
|
||||
position: relative;
|
||||
transform: rotate(0deg);
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
::v-deep .search-input .el-input__inner:focus {
|
||||
border-color: #6b8cff;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.content-panel {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
border-radius: 15px;
|
||||
background: transparent;
|
||||
border: 1px solid #fff;
|
||||
}
|
||||
|
||||
.content-area {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
min-width: 600px;
|
||||
overflow: auto;
|
||||
background-color: white;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.device-card {
|
||||
background: white;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
::v-deep .el-card__body {
|
||||
padding: 15px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.table_bottom {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
|
||||
.ctrl_btn {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding-left: 26px;
|
||||
}
|
||||
|
||||
.ctrl_btn .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);
|
||||
}
|
||||
|
||||
.ctrl_btn .el-button:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.ctrl_btn .el-button--primary {
|
||||
background: #5f70f3;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.ctrl_btn .el-button--success {
|
||||
background: #5bc98c;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.ctrl_btn .el-button--danger {
|
||||
background: #fd5b63;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.custom-pagination {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.custom-pagination .el-select {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.custom-pagination .pagination-btn:first-child,
|
||||
.custom-pagination .pagination-btn:nth-child(2),
|
||||
.custom-pagination .pagination-btn:nth-last-child(2),
|
||||
.custom-pagination .pagination-btn:nth-child(3) {
|
||||
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;
|
||||
}
|
||||
|
||||
.custom-pagination .pagination-btn:first-child:hover,
|
||||
.custom-pagination .pagination-btn:nth-child(2):hover,
|
||||
.custom-pagination .pagination-btn:nth-last-child(2):hover,
|
||||
.custom-pagination .pagination-btn:nth-child(3):hover {
|
||||
background: #d7dce6;
|
||||
}
|
||||
|
||||
.custom-pagination .pagination-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.custom-pagination .pagination-btn:not(:first-child):not(:nth-child(3)):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;
|
||||
}
|
||||
|
||||
.custom-pagination .pagination-btn:not(:first-child):not(:nth-child(3)):not(:nth-child(2)):not(:nth-last-child(2)):hover {
|
||||
background: rgba(245, 247, 250, 0.3);
|
||||
}
|
||||
|
||||
.custom-pagination .pagination-btn.active {
|
||||
background: #5f70f3 !important;
|
||||
color: #ffffff !important;
|
||||
border-color: #5f70f3 !important;
|
||||
}
|
||||
|
||||
.custom-pagination .pagination-btn.active:hover {
|
||||
background: #6d7cf5 !important;
|
||||
}
|
||||
|
||||
.custom-pagination .total-text {
|
||||
color: #909399;
|
||||
font-size: 14px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
:deep(.transparent-table) {
|
||||
background: white;
|
||||
border: none;
|
||||
}
|
||||
|
||||
:deep(.transparent-table .el-table__header th) {
|
||||
background: white !important;
|
||||
color: black;
|
||||
border-right: none !important;
|
||||
}
|
||||
|
||||
:deep(.transparent-table .el-table__body tr td) {
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.04);
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.04);
|
||||
border-right: none !important;
|
||||
}
|
||||
|
||||
:deep(.transparent-table .el-table__header tr th:first-child .cell),
|
||||
:deep(.transparent-table .el-table__body tr td:first-child .cell) {
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
:deep(.el-icon-edit) {
|
||||
color: #7079aa;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
:deep(.el-icon-edit:hover) {
|
||||
color: #5a64b5;
|
||||
}
|
||||
|
||||
:deep(.custom-selection-header .el-checkbox) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
|
||||
|
||||
:deep(.el-table .el-button--text) {
|
||||
color: #7079aa;
|
||||
}
|
||||
|
||||
:deep(.el-table .el-button--text:hover) {
|
||||
color: #5a64b5;
|
||||
}
|
||||
|
||||
:deep(.transparent-table) {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: calc(100vh - 40vh);
|
||||
}
|
||||
|
||||
:deep(.el-table__body-wrapper) {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
max-height: none !important;
|
||||
}
|
||||
|
||||
:deep(.el-table__header-wrapper) {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@media (min-width: 1144px) {
|
||||
.table_bottom {
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
:deep(.transparent-table) .el-table__body tr td {
|
||||
padding-top: 16px;
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
: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;
|
||||
}
|
||||
|
||||
::v-deep .el-table--border::after,
|
||||
::v-deep .el-table--group::after,
|
||||
::v-deep .el-table::before {
|
||||
display: none !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,851 @@
|
||||
<template>
|
||||
<div class="welcome">
|
||||
<HeaderBar />
|
||||
|
||||
<div class="operation-bar">
|
||||
<h2 class="page-title">字典管理</h2>
|
||||
<div class="action-group">
|
||||
<div class="search-group">
|
||||
<el-input placeholder="请输入字典值标签查询" v-model="search" class="search-input" clearable
|
||||
@keyup.enter.native="handleSearch" style="width: 240px" />
|
||||
<el-button class="btn-search" @click="handleSearch">
|
||||
搜索
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 主体内容 -->
|
||||
<div class="main-wrapper">
|
||||
<div class="content-panel">
|
||||
<!-- 左侧字典类型列表 -->
|
||||
<div class="dict-type-panel">
|
||||
<div class="dict-type-header">
|
||||
<el-button type="success" size="mini" @click="showAddDictTypeDialog">新增字典类型</el-button>
|
||||
<el-button type="danger" size="mini" @click="batchDeleteDictType"
|
||||
:disabled="selectedDictTypes.length === 0">
|
||||
批量删除字典类型
|
||||
</el-button>
|
||||
</div>
|
||||
<el-table ref="dictTypeTable" :data="dictTypeList" style="width: 100%" v-loading="dictTypeLoading"
|
||||
element-loading-text="拼命加载中" element-loading-spinner="el-icon-loading"
|
||||
element-loading-background="rgba(255, 255, 255, 0.7)" @row-click="handleDictTypeRowClick"
|
||||
@selection-change="handleDictTypeSelectionChange" :row-class-name="tableRowClassName"
|
||||
class="dict-type-table">
|
||||
<el-table-column type="selection" width="55" align="center"></el-table-column>
|
||||
<el-table-column label="字典类型名称" prop="dictName" align="center"></el-table-column>
|
||||
<el-table-column label="操作" width="100" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-button type="text" size="mini" @click.stop="editDictType(scope.row)">编辑</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<!-- 右侧字典数据列表 -->
|
||||
<div class="content-area">
|
||||
<el-card class="dict-data-card" shadow="never">
|
||||
<el-table ref="dictDataTable" :data="dictDataList" style="width: 100%"
|
||||
v-loading="dictDataLoading" element-loading-text="拼命加载中"
|
||||
element-loading-spinner="el-icon-loading"
|
||||
element-loading-background="rgba(255, 255, 255, 0.7)" class="data-table"
|
||||
header-row-class-name="table-header">
|
||||
<el-table-column label="选择" align="center" width="55">
|
||||
<template slot-scope="scope">
|
||||
<el-checkbox v-model="scope.row.selected"></el-checkbox>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="字典标签" prop="dictLabel" align="center"></el-table-column>
|
||||
<el-table-column label="字典值" prop="dictValue" align="center"></el-table-column>
|
||||
<el-table-column label="排序" prop="sort" align="center"></el-table-column>
|
||||
<el-table-column label="操作" align="center" width="180px">
|
||||
<template slot-scope="scope">
|
||||
<el-button type="text" size="mini" @click="editDictData(scope.row)"
|
||||
class="edit-btn">
|
||||
修改
|
||||
</el-button>
|
||||
<el-button type="text" size="mini" @click="deleteDictData(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" type="primary" @click="selectAllDictData">
|
||||
{{ isAllDictDataSelected ? '取消全选' : '全选' }}
|
||||
</el-button>
|
||||
<el-button type="success" size="mini" @click="showAddDictDataDialog" class="add-btn">
|
||||
新增字典数据
|
||||
</el-button>
|
||||
<el-button size="mini" type="danger" icon="el-icon-delete" @click="batchDeleteDictData">
|
||||
批量删除字典数据
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="custom-pagination">
|
||||
<el-select v-model="pageSize" @change="handlePageSizeChange" class="page-size-select">
|
||||
<el-option v-for="item in pageSizeOptions" :key="item" :label="`${item}条/页`"
|
||||
:value="item">
|
||||
</el-option>
|
||||
</el-select>
|
||||
|
||||
<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>
|
||||
|
||||
<!-- 使用字典类型编辑弹框组件 -->
|
||||
<DictTypeDialog :visible.sync="dictTypeDialogVisible" :title="dictTypeDialogTitle" :dictTypeData="dictTypeForm"
|
||||
@save="saveDictType" />
|
||||
|
||||
<!-- 使用字典数据编辑弹框组件 -->
|
||||
<DictDataDialog :visible.sync="dictDataDialogVisible" :title="dictDataDialogTitle" :dictData="dictDataForm"
|
||||
:dictTypeId="selectedDictType?.id" @save="saveDictData" />
|
||||
<el-footer style="flex-shrink:unset;">
|
||||
<version-footer />
|
||||
</el-footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import dictApi from '@/apis/module/dict'
|
||||
import DictDataDialog from '@/components/DictDataDialog.vue'
|
||||
import DictTypeDialog from '@/components/DictTypeDialog.vue'
|
||||
import HeaderBar from '@/components/HeaderBar.vue'
|
||||
import VersionFooter from '@/components/VersionFooter.vue'
|
||||
export default {
|
||||
name: 'DictManagement',
|
||||
components: {
|
||||
HeaderBar,
|
||||
DictTypeDialog,
|
||||
DictDataDialog,
|
||||
VersionFooter
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
// 字典类型相关
|
||||
dictTypeList: [],
|
||||
dictTypeLoading: false,
|
||||
selectedDictType: null,
|
||||
selectedDictTypes: [], // 恢复多选数组
|
||||
dictTypeDialogVisible: false,
|
||||
dictTypeDialogTitle: '新增字典类型',
|
||||
dictTypeForm: {
|
||||
id: null,
|
||||
dictName: '',
|
||||
dictType: ''
|
||||
},
|
||||
|
||||
// 字典数据相关
|
||||
dictDataList: [],
|
||||
dictDataLoading: false,
|
||||
isAllDictDataSelected: false,
|
||||
dictDataDialogVisible: false,
|
||||
dictDataDialogTitle: '新增字典数据',
|
||||
dictDataForm: {
|
||||
id: null,
|
||||
dictTypeId: null,
|
||||
dictLabel: '',
|
||||
dictValue: '',
|
||||
sort: 0
|
||||
},
|
||||
search: '',
|
||||
// 添加分页相关数据
|
||||
pageSizeOptions: [10, 20, 50, 100],
|
||||
currentPage: 1,
|
||||
pageSize: 10,
|
||||
total: 0
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.loadDictTypeList()
|
||||
},
|
||||
methods: {
|
||||
// 字典类型相关方法
|
||||
loadDictTypeList() {
|
||||
this.dictTypeLoading = true
|
||||
dictApi.getDictTypeList({
|
||||
page: 1,
|
||||
limit: 100,
|
||||
dictName: this.search
|
||||
}, ({ data }) => {
|
||||
if (data.code === 0) {
|
||||
this.dictTypeList = data.data.list
|
||||
if (this.dictTypeList.length > 0) {
|
||||
this.selectedDictType = this.dictTypeList[0]
|
||||
this.loadDictDataList(this.dictTypeList[0].id)
|
||||
this.$nextTick(() => {
|
||||
this.$refs.dictTypeTable.setCurrentRow(this.dictTypeList[0])
|
||||
})
|
||||
}
|
||||
}
|
||||
this.dictTypeLoading = false
|
||||
})
|
||||
},
|
||||
handleDictTypeRowClick(row) {
|
||||
this.selectedDictType = row
|
||||
this.loadDictDataList(row.id)
|
||||
this.$refs.dictTypeTable.setCurrentRow(row)
|
||||
},
|
||||
handleDictTypeSelectionChange(val) {
|
||||
this.selectedDictTypes = val
|
||||
},
|
||||
tableRowClassName({ row }) {
|
||||
return row === this.selectedDictType ? 'current-row' : ''
|
||||
},
|
||||
showAddDictTypeDialog() {
|
||||
this.dictTypeDialogTitle = '新增字典类型'
|
||||
this.dictTypeForm = {
|
||||
id: null,
|
||||
dictName: '',
|
||||
dictType: ''
|
||||
}
|
||||
this.dictTypeDialogVisible = true
|
||||
},
|
||||
editDictType(row) {
|
||||
this.dictTypeDialogTitle = '编辑字典类型'
|
||||
this.dictTypeForm = { ...row }
|
||||
this.dictTypeDialogVisible = true
|
||||
},
|
||||
saveDictType(formData) {
|
||||
const api = formData.id ? dictApi.updateDictType : dictApi.addDictType
|
||||
api(formData, ({ data }) => {
|
||||
if (data.code === 0) {
|
||||
this.$message.success('保存成功')
|
||||
this.dictTypeDialogVisible = false
|
||||
this.loadDictTypeList()
|
||||
}
|
||||
})
|
||||
},
|
||||
batchDeleteDictType() {
|
||||
if (this.selectedDictTypes.length === 0) {
|
||||
this.$message.warning('请选择要删除的字典类型')
|
||||
return
|
||||
}
|
||||
|
||||
this.$confirm('确定要删除选中的字典类型吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
const ids = this.selectedDictTypes.map(item => item.id)
|
||||
dictApi.deleteDictType(ids, ({ data }) => {
|
||||
if (data.code === 0) {
|
||||
this.$message.success('删除成功')
|
||||
this.loadDictTypeList()
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
// 字典数据相关方法
|
||||
loadDictDataList(dictTypeId) {
|
||||
if (!dictTypeId) return
|
||||
this.dictDataLoading = true
|
||||
dictApi.getDictDataList({
|
||||
dictTypeId,
|
||||
page: this.currentPage,
|
||||
limit: this.pageSize,
|
||||
dictLabel: this.search,
|
||||
dictValue: ''
|
||||
}, ({ data }) => {
|
||||
if (data.code === 0) {
|
||||
this.dictDataList = data.data.list.map(item => ({
|
||||
...item,
|
||||
selected: false
|
||||
}))
|
||||
this.total = data.data.total
|
||||
} else {
|
||||
this.$message.error(data.msg || '获取字典数据失败')
|
||||
}
|
||||
this.dictDataLoading = false
|
||||
})
|
||||
},
|
||||
selectAllDictData() {
|
||||
this.isAllDictDataSelected = !this.isAllDictDataSelected
|
||||
this.dictDataList.forEach(row => {
|
||||
row.selected = this.isAllDictDataSelected
|
||||
})
|
||||
},
|
||||
showAddDictDataDialog() {
|
||||
if (!this.selectedDictType) {
|
||||
this.$message.warning('请先选择字典类型')
|
||||
return
|
||||
}
|
||||
this.dictDataDialogTitle = '新增字典数据'
|
||||
this.dictDataForm = {
|
||||
id: null,
|
||||
dictTypeId: this.selectedDictType.id,
|
||||
dictLabel: '',
|
||||
dictValue: '',
|
||||
sort: 0
|
||||
}
|
||||
this.dictDataDialogVisible = true
|
||||
},
|
||||
editDictData(row) {
|
||||
this.dictDataDialogTitle = '编辑字典数据'
|
||||
this.dictDataForm = { ...row }
|
||||
this.dictDataDialogVisible = true
|
||||
},
|
||||
saveDictData(formData) {
|
||||
const api = formData.id ? dictApi.updateDictData : dictApi.addDictData
|
||||
api(formData, ({ data }) => {
|
||||
if (data.code === 0) {
|
||||
this.$message.success('保存成功')
|
||||
this.dictDataDialogVisible = false
|
||||
this.loadDictDataList(formData.dictTypeId)
|
||||
}
|
||||
})
|
||||
},
|
||||
deleteDictData(row) {
|
||||
this.$confirm('确定要删除该字典数据吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
dictApi.deleteDictData([row.id], ({ data }) => {
|
||||
if (data.code === 0) {
|
||||
this.$message.success('删除成功')
|
||||
this.loadDictDataList(row.dictTypeId)
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
batchDeleteDictData() {
|
||||
const selectedRows = this.dictDataList.filter(row => row.selected)
|
||||
if (selectedRows.length === 0) {
|
||||
this.$message.warning('请选择要删除的字典数据')
|
||||
return
|
||||
}
|
||||
|
||||
this.$confirm(`确定要删除选中的${selectedRows.length}个字典数据吗?`, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
const ids = selectedRows.map(item => item.id)
|
||||
dictApi.deleteDictData(ids, ({ data }) => {
|
||||
if (data.code === 0) {
|
||||
this.$message.success('删除成功')
|
||||
this.loadDictDataList(this.selectedDictType.id)
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
handleSearch() {
|
||||
if (!this.selectedDictType) {
|
||||
this.$message.warning('请先选择字典类型')
|
||||
return
|
||||
}
|
||||
this.currentPage = 1
|
||||
this.loadDictDataList(this.selectedDictType.id)
|
||||
},
|
||||
// 添加分页相关方法
|
||||
handlePageSizeChange(val) {
|
||||
this.pageSize = val;
|
||||
this.currentPage = 1;
|
||||
this.loadDictDataList(this.selectedDictType?.id);
|
||||
},
|
||||
goFirst() {
|
||||
this.currentPage = 1;
|
||||
this.loadDictDataList(this.selectedDictType?.id);
|
||||
},
|
||||
goPrev() {
|
||||
if (this.currentPage > 1) {
|
||||
this.currentPage--;
|
||||
this.loadDictDataList(this.selectedDictType?.id);
|
||||
}
|
||||
},
|
||||
goNext() {
|
||||
if (this.currentPage < this.pageCount) {
|
||||
this.currentPage++;
|
||||
this.loadDictDataList(this.selectedDictType?.id);
|
||||
}
|
||||
},
|
||||
goToPage(page) {
|
||||
this.currentPage = page;
|
||||
this.loadDictDataList(this.selectedDictType?.id);
|
||||
}
|
||||
},
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
</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;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.main-wrapper {
|
||||
margin: 5px 22px;
|
||||
border-radius: 15px;
|
||||
min-height: calc(100vh - 24vh);
|
||||
height: auto;
|
||||
max-height: 80vh;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||
position: relative;
|
||||
background: rgba(237, 242, 255, 0.5);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.operation-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 24px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 24px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.action-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.search-group {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 240px;
|
||||
}
|
||||
|
||||
.btn-search {
|
||||
background: linear-gradient(135deg, #6b8cff, #a966ff);
|
||||
border: none;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-search:hover {
|
||||
opacity: 0.9;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
:deep(.search-input .el-input__inner) {
|
||||
border-radius: 4px;
|
||||
border: 1px solid #DCDFE6;
|
||||
background-color: white;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
:deep(.search-input .el-input__inner:focus) {
|
||||
border-color: #6b8cff;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.content-panel {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
border-radius: 15px;
|
||||
background: transparent;
|
||||
border: 1px solid #fff;
|
||||
}
|
||||
|
||||
.dict-type-panel {
|
||||
width: 300px;
|
||||
background: white;
|
||||
border-right: 1px solid #ebeef5;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.dict-type-header {
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.dict-type-table {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.content-area {
|
||||
flex: 1;
|
||||
padding: 24px;
|
||||
height: 100%;
|
||||
min-width: 600px;
|
||||
overflow: hidden;
|
||||
background-color: white;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.dict-data-card {
|
||||
background: white;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.data-table {
|
||||
border-radius: 6px;
|
||||
overflow-y: auto;
|
||||
background-color: transparent !important;
|
||||
--table-max-height: calc(100vh - 40vh);
|
||||
max-height: var(--table-max-height);
|
||||
|
||||
:deep(.el-table__body-wrapper) {
|
||||
max-height: calc(var(--table-max-height) - 40px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
:deep(.el-table__body) {
|
||||
tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-table) {
|
||||
&::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.table-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 0;
|
||||
width: 100%;
|
||||
flex-shrink: 0;
|
||||
min-height: 60px;
|
||||
background: white;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.batch-actions {
|
||||
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--danger {
|
||||
background: #fd5b63;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-pagination {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.el-select {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.pagination-btn:first-child,
|
||||
.pagination-btn:nth-child(2),
|
||||
.pagination-btn:nth-child(3),
|
||||
.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-child(3)):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;
|
||||
}
|
||||
}
|
||||
|
||||
.page-size-select {
|
||||
width: 100px;
|
||||
margin-right: 10px;
|
||||
|
||||
:deep(.el-input__inner) {
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #e4e7ed;
|
||||
background: #dee7ff;
|
||||
color: #606266;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
:deep(.el-input__suffix) {
|
||||
right: 6px;
|
||||
width: 15px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
top: 6px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
:deep(.el-input__suffix-inner) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.el-icon-arrow-up:before) {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
border-left: 6px solid transparent;
|
||||
border-right: 6px solid transparent;
|
||||
border-top: 9px solid #606266;
|
||||
position: relative;
|
||||
transform: rotate(0deg);
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
}
|
||||
|
||||
.edit-btn,
|
||||
.delete-btn {
|
||||
margin: 0 8px;
|
||||
color: #7079aa !important;
|
||||
font-size: 12px;
|
||||
padding: 7px 12px;
|
||||
height: 32px;
|
||||
line-height: 1;
|
||||
border-radius: 4px;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
color: #5a64b5 !important;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.dict-type-header .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--success {
|
||||
background: #5bc98c;
|
||||
color: white;
|
||||
}
|
||||
|
||||
&.el-button--danger {
|
||||
background: #fd5b63;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-table .cell) {
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
:deep(.el-loading-mask) {
|
||||
background-color: rgba(255, 255, 255, 0.6) !important;
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
:deep(.el-loading-spinner .circular) {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
:deep(.el-loading-spinner .path) {
|
||||
stroke: #6b8cff;
|
||||
}
|
||||
|
||||
:deep(.el-loading-text) {
|
||||
color: #6b8cff !important;
|
||||
font-size: 14px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
:deep(.dict-type-table .el-table__row) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
:deep(.dict-type-table .el-table__row.current-row) {
|
||||
background-color: #5778ff !important;
|
||||
color: white;
|
||||
}
|
||||
|
||||
:deep(.dict-type-table .el-table__row.current-row .el-button--text) {
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
:deep(.dict-type-table .el-table__row:hover) {
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
:deep(.dict-type-table .el-table__row.current-row:hover) {
|
||||
background-color: #5778ff !important;
|
||||
}
|
||||
|
||||
:deep(.dict-type-table .el-table__row td) {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
:deep(.el-table thead) {
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
:deep(.el-card__body) {
|
||||
padding: 15px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
: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;
|
||||
}
|
||||
</style>
|
||||
@@ -3,17 +3,16 @@
|
||||
<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>-->
|
||||
<h2 class="page-title">{{ modelTypeText }}</h2>
|
||||
<div class="action-group">
|
||||
<div class="search-group">
|
||||
<el-input placeholder="请输入模型名称查询" v-model="search" class="search-input" clearable
|
||||
@keyup.enter.native="handleSearch" style="width: 240px" />
|
||||
<el-button class="btn-search" @click="handleSearch">
|
||||
搜索
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 主体内容 -->
|
||||
@@ -44,97 +43,94 @@
|
||||
|
||||
<!-- 右侧内容 -->
|
||||
<div class="content-area">
|
||||
<div class="title-bar">
|
||||
<div class="title-wrapper">
|
||||
<h2 class="model-title">{{ modelTypeText }}</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 @keyup.enter.native="handleSearch" />
|
||||
<el-button type="primary" size="small" class="search-btn" @click="handleSearch">
|
||||
查询
|
||||
<el-card class="model-card" shadow="never">
|
||||
<el-table ref="modelTable" style="width: 100%" v-loading="loading" element-loading-text="拼命加载中"
|
||||
element-loading-spinner="el-icon-loading" element-loading-background="rgba(255, 255, 255, 0.7)"
|
||||
: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="模型ID" prop="id" align="center"></el-table-column>
|
||||
<el-table-column label="模型名称" prop="modelName" align="center"></el-table-column>
|
||||
<el-table-column label="提供商" align="center">
|
||||
<template slot-scope="scope">
|
||||
{{ scope.row.configJson.type || '未知' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="是否启用" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-switch v-model="scope.row.isEnabled" class="custom-switch" :active-value="1" :inactive-value="0"
|
||||
@change="handleStatusChange(scope.row)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="是否默认" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-switch v-model="scope.row.isDefault" class="custom-switch" :active-value="1" :inactive-value="0"
|
||||
@change="handleDefaultChange(scope.row)" />
|
||||
</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="openTtsDialog(scope.row)" 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" type="primary" @click="selectAll">
|
||||
{{ isAllSelected ?
|
||||
'取消全选' : '全选' }}
|
||||
</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="modelName" align="center"></el-table-column>
|
||||
<el-table-column label="模型编码" prop="modelCode" align="center"></el-table-column>
|
||||
<el-table-column label="提供商" align="center">
|
||||
<template slot-scope="scope">
|
||||
{{ scope.row.configJson.type || '未知' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="是否启用" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-switch v-model="scope.row.isEnabled" class="custom-switch" :active-value="1" :inactive-value="0"
|
||||
@change="handleStatusChange(scope.row)" />
|
||||
</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="openTtsDialog(scope.row)"
|
||||
class="voice-management-btn">
|
||||
音色管理
|
||||
<el-button type="success" size="mini" @click="addModel" class="add-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 size="mini" type="danger" icon="el-icon-delete" @click="batchDelete">
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="custom-pagination">
|
||||
|
||||
<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>
|
||||
<el-select v-model="pageSize" @change="handlePageSizeChange" class="page-size-select">
|
||||
<el-option v-for="item in pageSizeOptions" :key="item" :label="`${item}条/页`" :value="item">
|
||||
</el-option>
|
||||
</el-select>
|
||||
|
||||
<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 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>
|
||||
|
||||
<ModelEditDialog :modelType="activeTab" :visible.sync="editDialogVisible" :modelData="editModelData"
|
||||
@save="handleModelSave" />
|
||||
<TtsModel :visible.sync="ttsDialogVisible" :ttsModelId="selectedTtsModelId"/>
|
||||
<TtsModel :visible.sync="ttsDialogVisible" :ttsModelId="selectedTtsModelId" />
|
||||
<AddModelDialog :modelType="activeTab" :visible.sync="addDialogVisible" @confirm="handleAddConfirm" />
|
||||
</div>
|
||||
|
||||
<div class="copyright">
|
||||
©2025 xiaozhi-esp32-server
|
||||
</div>
|
||||
<el-footer>
|
||||
<version-footer />
|
||||
</el-footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -144,9 +140,9 @@ import AddModelDialog from "@/components/AddModelDialog.vue";
|
||||
import HeaderBar from "@/components/HeaderBar.vue";
|
||||
import ModelEditDialog from "@/components/ModelEditDialog.vue";
|
||||
import TtsModel from "@/components/TtsModel.vue";
|
||||
|
||||
import VersionFooter from "@/components/VersionFooter.vue";
|
||||
export default {
|
||||
components: { HeaderBar, ModelEditDialog, TtsModel, AddModelDialog },
|
||||
components: { HeaderBar, ModelEditDialog, TtsModel, AddModelDialog, VersionFooter },
|
||||
data() {
|
||||
return {
|
||||
addDialogVisible: false,
|
||||
@@ -157,11 +153,13 @@ export default {
|
||||
ttsDialogVisible: false,
|
||||
selectedTtsModelId: '',
|
||||
modelList: [],
|
||||
pageSizeOptions: [10, 20, 50, 100],
|
||||
currentPage: 1,
|
||||
pageSize: 5,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
selectedModels: [],
|
||||
isAllSelected: false
|
||||
isAllSelected: false,
|
||||
loading: false
|
||||
};
|
||||
},
|
||||
|
||||
@@ -170,7 +168,6 @@ export default {
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
||||
modelTypeText() {
|
||||
const map = {
|
||||
vad: '语言活动检测模型(VAD)',
|
||||
@@ -182,8 +179,6 @@ export default {
|
||||
}
|
||||
return map[this.activeTab] || '模型配置'
|
||||
},
|
||||
|
||||
|
||||
pageCount() {
|
||||
return Math.ceil(this.total / this.pageSize);
|
||||
},
|
||||
@@ -205,6 +200,11 @@ export default {
|
||||
},
|
||||
|
||||
methods: {
|
||||
handlePageSizeChange(val) {
|
||||
this.pageSize = val;
|
||||
this.currentPage = 1;
|
||||
this.loadData();
|
||||
},
|
||||
openTtsDialog(row) {
|
||||
this.selectedTtsModelId = row.id;
|
||||
this.ttsDialogVisible = true;
|
||||
@@ -217,7 +217,8 @@ export default {
|
||||
},
|
||||
handleMenuSelect(index) {
|
||||
this.activeTab = index;
|
||||
this.currentPage = 1;
|
||||
this.currentPage = 1; // 重置到第一页
|
||||
this.pageSize = 10; // 可选:重置每页条数
|
||||
this.loadData();
|
||||
},
|
||||
handleSearch() {
|
||||
@@ -302,17 +303,10 @@ export default {
|
||||
this.currentPage = page;
|
||||
this.$refs.modelTable.clearSelection();
|
||||
},
|
||||
handleImport() {
|
||||
// TODO: 导入配置
|
||||
console.log('导入配置');
|
||||
},
|
||||
handleExport() {
|
||||
// TODO: 导出配置
|
||||
console.log('导出配置');
|
||||
},
|
||||
handleModelSave({ provideCode, formData }) {
|
||||
handleModelSave({ provideCode, formData, done }) {
|
||||
const modelType = this.activeTab;
|
||||
const id = formData.id;
|
||||
|
||||
Api.model.updateModel(
|
||||
{ modelType, provideCode, id, formData },
|
||||
({ data }) => {
|
||||
@@ -323,6 +317,7 @@ export default {
|
||||
} else {
|
||||
this.$message.error(data.msg || '保存失败');
|
||||
}
|
||||
done && done(); // 调用done回调关闭加载状态
|
||||
}
|
||||
);
|
||||
},
|
||||
@@ -394,6 +389,7 @@ export default {
|
||||
|
||||
// 获取模型配置列表
|
||||
loadData() {
|
||||
this.loading = true; // 开始加载
|
||||
const params = {
|
||||
modelType: this.activeTab,
|
||||
modelName: this.search,
|
||||
@@ -402,6 +398,7 @@ export default {
|
||||
};
|
||||
|
||||
Api.model.getModelList(params, ({ data }) => {
|
||||
this.loading = false; // 结束加载
|
||||
if (data.code === 0) {
|
||||
this.modelList = data.data.list;
|
||||
this.total = data.data.total;
|
||||
@@ -410,11 +407,11 @@ export default {
|
||||
}
|
||||
});
|
||||
},
|
||||
// 处理启用/禁用状态变更
|
||||
// 处理启用/禁用状态变更
|
||||
handleStatusChange(model) {
|
||||
const newStatus = model.isEnabled ? 1 : 0
|
||||
const originalStatus = model.isEnabled
|
||||
|
||||
|
||||
model.isEnabled = !model.isEnabled
|
||||
|
||||
Api.model.updateModelStatus(
|
||||
@@ -432,12 +429,24 @@ export default {
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
handleDefaultChange(model) {
|
||||
Api.model.setDefaultModel(model.id, ({ data }) => {
|
||||
if (data.code === 0) {
|
||||
this.$message.success('设置默认模型成功')
|
||||
this.loadData()
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.el-switch {
|
||||
height: 23px;
|
||||
}
|
||||
|
||||
::v-deep .el-table tr {
|
||||
background: transparent;
|
||||
}
|
||||
@@ -456,9 +465,11 @@ export default {
|
||||
}
|
||||
|
||||
.main-wrapper {
|
||||
margin: 5px 20px;
|
||||
margin: 5px 22px;
|
||||
border-radius: 15px;
|
||||
min-height: 600px;
|
||||
min-height: calc(100vh - 24vh);
|
||||
height: auto;
|
||||
max-height: 80vh;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||
position: relative;
|
||||
background: rgba(237, 242, 255, 0.5);
|
||||
@@ -476,12 +487,6 @@ export default {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.right-operations {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.content-panel {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
@@ -510,20 +515,20 @@ export default {
|
||||
|
||||
.nav-panel .el-menu-item {
|
||||
height: 50px;
|
||||
background: #e9f0ff;
|
||||
line-height: 50px;
|
||||
border-radius: 4px;
|
||||
border-radius: 4px 0 0 4px !important;
|
||||
transition: all 0.3s;
|
||||
display: flex !important;
|
||||
justify-content: flex-end;
|
||||
padding-right: 12px !important;
|
||||
width: fit-content;
|
||||
margin: 8px 0px 8px auto;
|
||||
margin: 8px 0 8px auto;
|
||||
min-width: unset;
|
||||
}
|
||||
|
||||
.nav-panel .el-menu-item.is-active {
|
||||
background: #e9f0ff;
|
||||
color: #0ba6f4 !important;
|
||||
background: #5778ff;
|
||||
position: relative;
|
||||
padding-left: 40px !important;
|
||||
}
|
||||
@@ -536,7 +541,7 @@ export default {
|
||||
transform: translateY(-50%);
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
background: #409EFF;
|
||||
background: #fff;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 4px rgba(64, 158, 255, 0.5);
|
||||
}
|
||||
@@ -554,23 +559,10 @@ export default {
|
||||
padding: 24px;
|
||||
height: 100%;
|
||||
min-width: 600px;
|
||||
overflow-x: auto;
|
||||
overflow: hidden;
|
||||
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;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.action-group {
|
||||
@@ -581,26 +573,78 @@ export default {
|
||||
|
||||
.search-group {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 240px;
|
||||
}
|
||||
|
||||
::v-deep .search-input .el-input__inner::placeholder {
|
||||
color: black;
|
||||
opacity: 0.6;
|
||||
.btn-search {
|
||||
background: linear-gradient(135deg, #6b8cff, #a966ff);
|
||||
border: none;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-search:hover {
|
||||
opacity: 0.9;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
::v-deep .search-input .el-input__inner {
|
||||
background: transparent;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #DCDFE6;
|
||||
background-color: white;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.search-btn {
|
||||
background: linear-gradient(135deg, #6B8CFF, #A966FF);
|
||||
border: none;
|
||||
color: white;
|
||||
::v-deep .page-size-select {
|
||||
width: 100px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
::v-deep .page-size-select .el-input__inner {
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #e4e7ed;
|
||||
background: #dee7ff;
|
||||
color: #606266;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
::v-deep .page-size-select .el-input__suffix {
|
||||
right: 6px;
|
||||
width: 15px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
top: 6px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::v-deep .page-size-select .el-input__suffix-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
::v-deep .page-size-select .el-icon-arrow-up:before {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
border-left: 6px solid transparent;
|
||||
border-right: 6px solid transparent;
|
||||
border-top: 9px solid #606266;
|
||||
position: relative;
|
||||
transform: rotate(0deg);
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
::v-deep .search-input .el-input__inner:focus {
|
||||
border-color: #6b8cff;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.data-table {
|
||||
@@ -620,12 +664,14 @@ export default {
|
||||
}
|
||||
|
||||
.table-footer {
|
||||
margin-top: 24px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 0;
|
||||
width: 100%;
|
||||
flex-shrink: 0;
|
||||
min-height: 60px;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.batch-actions {
|
||||
@@ -633,32 +679,37 @@ export default {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.copyright {
|
||||
text-align: center;
|
||||
color: #979db1;
|
||||
.batch-actions .el-button {
|
||||
min-width: 72px;
|
||||
height: 32px;
|
||||
padding: 7px 12px 7px 10px;
|
||||
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-radius: 4px;
|
||||
line-height: 1;
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
color: black;
|
||||
padding: 8px 16px;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.title-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
.batch-actions .el-button:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.batch-actions .el-button--primary {
|
||||
background: #5f70f3 !important;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.batch-actions .el-button--success {
|
||||
background: #5bc98c;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.batch-actions .el-button--danger {
|
||||
background: #fd5b63;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.batch-actions .el-button:first-child {
|
||||
@@ -679,14 +730,8 @@ export default {
|
||||
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 {
|
||||
@@ -708,7 +753,7 @@ export default {
|
||||
}
|
||||
|
||||
::v-deep .nav-panel .el-menu-item.is-active .menu-text {
|
||||
color: #409EFF !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
::v-deep .data-table {
|
||||
@@ -741,8 +786,8 @@ export default {
|
||||
}
|
||||
|
||||
::v-deep .el-checkbox__input.is-checked .el-checkbox__inner {
|
||||
background-color: #409EFF !important;
|
||||
border-color: #409EFF !important;
|
||||
background-color: #5f70f3;
|
||||
border-color: #5f70f3;
|
||||
}
|
||||
|
||||
.voice-management-btn {
|
||||
@@ -785,11 +830,11 @@ export default {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 15px;
|
||||
|
||||
/* 导航按钮样式 (首页、上一页、下一页) */
|
||||
.pagination-btn:first-child,
|
||||
.pagination-btn:nth-child(2),
|
||||
.pagination-btn:nth-child(3),
|
||||
.pagination-btn:nth-last-child(2) {
|
||||
min-width: 60px;
|
||||
height: 32px;
|
||||
@@ -813,7 +858,7 @@ export default {
|
||||
}
|
||||
|
||||
/* 数字按钮样式 */
|
||||
.pagination-btn:not(:first-child):not(:nth-child(2)):not(:nth-last-child(2)) {
|
||||
.pagination-btn:not(:first-child):not(:nth-child(2)):not(:nth-child(3)):not(:nth-last-child(2)) {
|
||||
min-width: 28px;
|
||||
height: 32px;
|
||||
padding: 0;
|
||||
@@ -846,4 +891,52 @@ export default {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.model-card {
|
||||
background: white;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.model-card ::v-deep .el-card__body {
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.data-table {
|
||||
--table-max-height: calc(100vh - 45vh);
|
||||
max-height: var(--table-max-height);
|
||||
}
|
||||
|
||||
.data-table ::v-deep .el-table__body-wrapper {
|
||||
max-height: calc(var(--table-max-height) - 80px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
::v-deep .el-loading-mask {
|
||||
background-color: rgba(255, 255, 255, 0.6) !important;
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
::v-deep .el-loading-spinner .circular {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
::v-deep .el-loading-spinner .path {
|
||||
stroke: #6b8cff;
|
||||
}
|
||||
|
||||
::v-deep .el-loading-text {
|
||||
color: #6b8cff !important;
|
||||
font-size: 14px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,766 @@
|
||||
<template>
|
||||
<div class="welcome">
|
||||
<HeaderBar />
|
||||
|
||||
<div class="operation-bar">
|
||||
<h2 class="page-title">固件管理</h2>
|
||||
<div class="right-operations">
|
||||
<el-input placeholder="请输入固件名称查询" v-model="searchName" class="search-input"
|
||||
@keyup.enter.native="handleSearch" clearable />
|
||||
<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="params-card" shadow="never">
|
||||
<el-table ref="paramsTable" :data="paramsList" class="transparent-table" v-loading="loading"
|
||||
element-loading-text="拼命加载中" element-loading-spinner="el-icon-loading"
|
||||
element-loading-background="rgba(255, 255, 255, 0.7)"
|
||||
:header-cell-class-name="headerCellClassName">
|
||||
<el-table-column label="选择" align="center" width="120">
|
||||
<template slot-scope="scope">
|
||||
<el-checkbox v-model="scope.row.selected"></el-checkbox>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="固件名称" prop="firmwareName" align="center"></el-table-column>
|
||||
<el-table-column label="固件类型" prop="type" align="center">
|
||||
<template slot-scope="scope">
|
||||
{{ getFirmwareTypeName(scope.row.type) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="版本号" prop="version" align="center"></el-table-column>
|
||||
<el-table-column label="文件大小" prop="size" align="center">
|
||||
<template slot-scope="scope">
|
||||
{{ formatFileSize(scope.row.size) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="备注" prop="remark" align="center"
|
||||
show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="创建时间" prop="createDate" align="center">
|
||||
<template slot-scope="scope">
|
||||
{{ formatDate(scope.row.createDate) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="更新时间" prop="updateDate" align="center">
|
||||
<template slot-scope="scope">
|
||||
{{ formatDate(scope.row.updateDate) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-button size="mini" type="text"
|
||||
@click="downloadFirmware(scope.row)">下载</el-button>
|
||||
<el-button size="mini" type="text" @click="editParam(scope.row)">编辑</el-button>
|
||||
<el-button size="mini" type="text" @click="deleteParam(scope.row)">删除</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">
|
||||
{{ isAllSelected ? '取消全选' : '全选' }}
|
||||
</el-button>
|
||||
<el-button size="mini" type="success" @click="showAddDialog"
|
||||
style="background: #5bc98c;border: None;">新增</el-button>
|
||||
<el-button size="mini" type="danger" icon="el-icon-delete"
|
||||
@click="deleteSelectedParams">删除</el-button>
|
||||
</div>
|
||||
<div class="custom-pagination">
|
||||
<el-select v-model="pageSize" @change="handlePageSizeChange" class="page-size-select">
|
||||
<el-option v-for="item in pageSizeOptions" :key="item" :label="`${item}条/页`"
|
||||
:value="item">
|
||||
</el-option>
|
||||
</el-select>
|
||||
<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>
|
||||
|
||||
<!-- 新增/编辑固件对话框 -->
|
||||
<firmware-dialog :title="dialogTitle" :visible.sync="dialogVisible" :form="firmwareForm"
|
||||
:firmware-types="firmwareTypes" @submit="handleSubmit" @cancel="dialogVisible = false" />
|
||||
<el-footer>
|
||||
<version-footer />
|
||||
</el-footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Api from "@/apis/api";
|
||||
import FirmwareDialog from "@/components/FirmwareDialog.vue";
|
||||
import HeaderBar from "@/components/HeaderBar.vue";
|
||||
import VersionFooter from "@/components/VersionFooter.vue";
|
||||
import { formatDate, formatFileSize } from "@/utils/format";
|
||||
|
||||
export default {
|
||||
components: { HeaderBar, FirmwareDialog, VersionFooter },
|
||||
data() {
|
||||
return {
|
||||
searchName: "",
|
||||
loading: false,
|
||||
paramsList: [],
|
||||
firmwareList: [],
|
||||
currentPage: 1,
|
||||
pageSize: 10,
|
||||
pageSizeOptions: [10, 20, 50, 100],
|
||||
total: 0,
|
||||
dialogVisible: false,
|
||||
dialogTitle: "新增固件",
|
||||
isAllSelected: false,
|
||||
firmwareForm: {
|
||||
id: null,
|
||||
firmwareName: "",
|
||||
type: "",
|
||||
version: "",
|
||||
size: 0,
|
||||
remark: "",
|
||||
firmwarePath: ""
|
||||
},
|
||||
firmwareTypes: [],
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.fetchFirmwareList();
|
||||
this.getFirmwareTypes();
|
||||
},
|
||||
|
||||
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: {
|
||||
handlePageSizeChange(val) {
|
||||
this.pageSize = val;
|
||||
this.currentPage = 1;
|
||||
this.fetchFirmwareList();
|
||||
},
|
||||
fetchFirmwareList() {
|
||||
this.loading = true;
|
||||
const params = {
|
||||
pageNum: this.currentPage,
|
||||
pageSize: this.pageSize,
|
||||
firmwareName: this.searchName || "",
|
||||
orderField: "create_date",
|
||||
order: "desc"
|
||||
};
|
||||
Api.ota.getOtaList(params, (res) => {
|
||||
this.loading = false;
|
||||
res = res.data
|
||||
if (res.code === 0) {
|
||||
this.firmwareList = res.data.list.map(item => ({
|
||||
...item,
|
||||
selected: false
|
||||
}));
|
||||
this.paramsList = this.firmwareList;
|
||||
this.total = res.data.total || 0;
|
||||
} else {
|
||||
this.firmwareList = [];
|
||||
this.paramsList = [];
|
||||
this.total = 0;
|
||||
this.$message.error({
|
||||
message: res?.data?.msg || '获取固件列表失败',
|
||||
showClose: true
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
handleSearch() {
|
||||
this.currentPage = 1;
|
||||
this.fetchFirmwareList();
|
||||
},
|
||||
handleSelectAll() {
|
||||
this.isAllSelected = !this.isAllSelected;
|
||||
this.firmwareList.forEach(row => {
|
||||
row.selected = this.isAllSelected;
|
||||
});
|
||||
},
|
||||
showAddDialog() {
|
||||
this.dialogTitle = "新增固件";
|
||||
// 完全重置表单数据
|
||||
this.firmwareForm = {
|
||||
id: null,
|
||||
firmwareName: "",
|
||||
type: "",
|
||||
version: "",
|
||||
size: 0,
|
||||
remark: "",
|
||||
firmwarePath: ""
|
||||
};
|
||||
this.$nextTick(() => {
|
||||
// 重置表单的校验状态
|
||||
if (this.$refs.firmwareDialog && this.$refs.firmwareDialog.$refs.form) {
|
||||
this.$refs.firmwareDialog.$refs.form.clearValidate();
|
||||
}
|
||||
});
|
||||
this.dialogVisible = true;
|
||||
},
|
||||
editParam(row) {
|
||||
this.dialogTitle = "编辑固件";
|
||||
this.firmwareForm = { ...row };
|
||||
this.dialogVisible = true;
|
||||
},
|
||||
handleSubmit(form) {
|
||||
if (form.id) {
|
||||
// 编辑
|
||||
Api.ota.updateOta(form.id, form, (res) => {
|
||||
res = res.data;
|
||||
if (res.code === 0) {
|
||||
this.$message.success({
|
||||
message: "修改成功",
|
||||
showClose: true
|
||||
});
|
||||
this.dialogVisible = false;
|
||||
this.fetchFirmwareList();
|
||||
} else {
|
||||
this.$message.error({
|
||||
message: res.msg || "修改失败",
|
||||
showClose: true
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// 新增
|
||||
Api.ota.saveOta(form, (res) => {
|
||||
res = res.data;
|
||||
if (res.code === 0) {
|
||||
this.$message.success({
|
||||
message: "新增成功",
|
||||
showClose: true
|
||||
});
|
||||
this.dialogVisible = false;
|
||||
this.fetchFirmwareList();
|
||||
} else {
|
||||
this.$message.error({
|
||||
message: res.msg || "新增失败",
|
||||
showClose: true
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
deleteSelectedParams() {
|
||||
const selectedRows = this.firmwareList.filter(row => row.selected);
|
||||
if (selectedRows.length === 0) {
|
||||
this.$message.warning({
|
||||
message: "请先选择需要删除的固件",
|
||||
showClose: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.deleteParam(selectedRows);
|
||||
},
|
||||
deleteParam(row) {
|
||||
// 处理单个参数或参数数组
|
||||
const params = Array.isArray(row) ? row : [row];
|
||||
|
||||
if (Array.isArray(row) && row.length === 0) {
|
||||
this.$message.warning({
|
||||
message: "请先选择需要删除的参数",
|
||||
showClose: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const paramCount = params.length;
|
||||
this.$confirm(`确定要删除选中的${paramCount}个固件吗?`, '警告', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
distinguishCancelAndClose: true
|
||||
}).then(() => {
|
||||
const ids = params.map(param => param.id);
|
||||
if (ids.some(id => !id)) {
|
||||
this.$message.error({
|
||||
message: '存在无效的参数ID',
|
||||
showClose: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
Api.ota.deleteOta(ids, (res) => {
|
||||
res = res.data;
|
||||
if (res.code === 0) {
|
||||
this.$message.success({
|
||||
message: `成功删除${paramCount}个固件`,
|
||||
showClose: true
|
||||
});
|
||||
this.fetchFirmwareList();
|
||||
} else {
|
||||
this.$message.error({
|
||||
message: res.msg || '删除失败,请重试',
|
||||
showClose: true
|
||||
});
|
||||
}
|
||||
});
|
||||
}).catch(action => {
|
||||
if (action === 'cancel') {
|
||||
this.$message({
|
||||
type: 'info',
|
||||
message: '已取消删除操作',
|
||||
duration: 1000
|
||||
});
|
||||
} else {
|
||||
this.$message({
|
||||
type: 'info',
|
||||
message: '操作已关闭',
|
||||
duration: 1000
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
headerCellClassName({ columnIndex }) {
|
||||
if (columnIndex === 0) {
|
||||
return "custom-selection-header";
|
||||
}
|
||||
return "";
|
||||
},
|
||||
goFirst() {
|
||||
this.currentPage = 1;
|
||||
this.fetchFirmwareList();
|
||||
},
|
||||
goPrev() {
|
||||
if (this.currentPage > 1) {
|
||||
this.currentPage--;
|
||||
this.fetchFirmwareList();
|
||||
}
|
||||
},
|
||||
goNext() {
|
||||
if (this.currentPage < this.pageCount) {
|
||||
this.currentPage++;
|
||||
this.fetchFirmwareList();
|
||||
}
|
||||
},
|
||||
goToPage(page) {
|
||||
this.currentPage = page;
|
||||
this.fetchFirmwareList();
|
||||
},
|
||||
downloadFirmware(firmware) {
|
||||
if (!firmware || !firmware.id) {
|
||||
this.$message.error('固件信息不完整');
|
||||
return;
|
||||
}
|
||||
// 先获取下载链接
|
||||
Api.ota.getDownloadUrl(firmware.id, (res) => {
|
||||
if (res.data.code === 0) {
|
||||
const uuid = res.data.data;
|
||||
const baseUrl = process.env.VUE_APP_API_BASE_URL || '';
|
||||
window.open(`${window.location.origin}${baseUrl}/otaMag/download/${uuid}`);
|
||||
} else {
|
||||
this.$message.error('获取下载链接失败');
|
||||
}
|
||||
});
|
||||
},
|
||||
formatDate,
|
||||
formatFileSize,
|
||||
async getFirmwareTypes() {
|
||||
try {
|
||||
const res = await Api.dict.getDictDataByType('FIRMWARE_TYPE')
|
||||
this.firmwareTypes = res.data
|
||||
} catch (error) {
|
||||
console.error('获取固件类型失败:', error)
|
||||
this.$message.error(error.message || '获取固件类型失败')
|
||||
}
|
||||
},
|
||||
getFirmwareTypeName(type) {
|
||||
const firmwareType = this.firmwareTypes.find(item => item.key === type)
|
||||
return firmwareType ? firmwareType.name : type
|
||||
},
|
||||
},
|
||||
};
|
||||
</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;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.main-wrapper {
|
||||
margin: 5px 22px;
|
||||
border-radius: 15px;
|
||||
min-height: calc(100vh - 24vh);
|
||||
height: auto;
|
||||
max-height: 80vh;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||
position: relative;
|
||||
background: rgba(237, 242, 255, 0.5);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.operation-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 24px;
|
||||
}
|
||||
|
||||
.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;
|
||||
border: 1px solid #fff;
|
||||
}
|
||||
|
||||
.content-area {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
min-width: 600px;
|
||||
overflow: auto;
|
||||
background-color: white;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.params-card {
|
||||
background: white;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
overflow: hidden;
|
||||
|
||||
::v-deep .el-card__body {
|
||||
padding: 15px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.table_bottom {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 10px;
|
||||
padding-bottom: 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--danger {
|
||||
background: #fd5b63;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-pagination {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
|
||||
.el-select {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.pagination-btn:first-child,
|
||||
.pagination-btn:nth-child(2),
|
||||
.pagination-btn:nth-last-child(2),
|
||||
.pagination-btn:nth-child(3) {
|
||||
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(3)):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 {
|
||||
margin-left: 10px;
|
||||
color: #606266;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.page-size-select {
|
||||
width: 100px;
|
||||
margin-right: 10px;
|
||||
|
||||
:deep(.el-input__inner) {
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #e4e7ed;
|
||||
background: #dee7ff;
|
||||
color: #606266;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
:deep(.el-input__suffix) {
|
||||
right: 6px;
|
||||
width: 15px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
top: 6px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
:deep(.el-input__suffix-inner) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.el-icon-arrow-up:before) {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
border-left: 6px solid transparent;
|
||||
border-right: 6px solid transparent;
|
||||
border-top: 9px solid #606266;
|
||||
position: relative;
|
||||
transform: rotate(0deg);
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.transparent-table) {
|
||||
background: white;
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.el-table__body-wrapper {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
max-height: none !important;
|
||||
}
|
||||
|
||||
.el-table__header-wrapper {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.el-table__header th {
|
||||
background: white !important;
|
||||
color: black;
|
||||
font-weight: 600;
|
||||
height: 40px;
|
||||
padding: 8px 0;
|
||||
font-size: 14px;
|
||||
border-bottom: 1px solid #e4e7ed;
|
||||
}
|
||||
|
||||
.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);
|
||||
padding: 8px 0;
|
||||
height: 40px;
|
||||
color: #606266;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.el-table__row:hover>td {
|
||||
background-color: #f5f7fa !important;
|
||||
}
|
||||
|
||||
&::before {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-table .el-button--text) {
|
||||
color: #7079aa !important;
|
||||
}
|
||||
|
||||
:deep(.el-table .el-button--text:hover) {
|
||||
color: #5a64b5 !important;
|
||||
}
|
||||
|
||||
: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;
|
||||
}
|
||||
|
||||
:deep(.el-loading-mask) {
|
||||
background-color: rgba(255, 255, 255, 0.6) !important;
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
:deep(.el-loading-spinner .path) {
|
||||
stroke: #6b8cff;
|
||||
}
|
||||
|
||||
.el-table {
|
||||
--table-max-height: calc(100vh - 40vh);
|
||||
max-height: var(--table-max-height);
|
||||
|
||||
.el-table__body-wrapper {
|
||||
max-height: calc(var(--table-max-height) - 40px);
|
||||
}
|
||||
}
|
||||
|
||||
@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>
|
||||
@@ -5,10 +5,9 @@
|
||||
<div class="operation-bar">
|
||||
<h2 class="page-title">参数管理</h2>
|
||||
<div class="right-operations">
|
||||
<el-input placeholder="请输入参数编码查询" v-model="searchCode" class="search-input"
|
||||
@keyup.enter.native="handleSearch" />
|
||||
<el-input placeholder="请输入参数编码或备注查询" v-model="searchCode" class="search-input"
|
||||
@keyup.enter.native="handleSearch" clearable />
|
||||
<el-button class="btn-search" @click="handleSearch">搜索</el-button>
|
||||
<el-button type="primary" @click="showAddDialog">新增参数</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -16,11 +15,29 @@
|
||||
<div class="content-panel">
|
||||
<div class="content-area">
|
||||
<el-card class="params-card" shadow="never">
|
||||
<el-table ref="paramsTable" :data="paramsList" class="transparent-table"
|
||||
<el-table ref="paramsTable" :data="paramsList" class="transparent-table" v-loading="loading"
|
||||
element-loading-text="拼命加载中" element-loading-spinner="el-icon-loading"
|
||||
element-loading-background="rgba(255, 255, 255, 0.7)"
|
||||
:header-cell-class-name="headerCellClassName">
|
||||
<el-table-column label="选择" type="selection" align="center" width="120"></el-table-column>
|
||||
<el-table-column label="选择" align="center" width="120">
|
||||
<template slot-scope="scope">
|
||||
<el-checkbox v-model="scope.row.selected"></el-checkbox>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="参数编码" prop="paramCode" align="center"></el-table-column>
|
||||
<el-table-column label="参数值" prop="paramValue" align="center"></el-table-column>
|
||||
<el-table-column label="参数值" prop="paramValue" align="center" show-overflow-tooltip>
|
||||
<template slot-scope="scope">
|
||||
<div v-if="isSensitiveParam(scope.row.paramCode)">
|
||||
<span v-if="!scope.row.showValue">{{ maskSensitiveValue(scope.row.paramValue)
|
||||
}}</span>
|
||||
<span v-else>{{ scope.row.paramValue }}</span>
|
||||
<el-button size="mini" type="text" @click="toggleSensitiveValue(scope.row)">
|
||||
{{ scope.row.showValue ? '隐藏' : '查看' }}
|
||||
</el-button>
|
||||
</div>
|
||||
<span v-else>{{ scope.row.paramValue }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="备注" prop="remark" align="center"></el-table-column>
|
||||
<el-table-column label="操作" align="center">
|
||||
<template slot-scope="scope">
|
||||
@@ -32,12 +49,19 @@
|
||||
|
||||
<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="primary" class="select-all-btn" @click="handleSelectAll">
|
||||
{{ isAllSelected ? '取消全选' : '全选' }}
|
||||
</el-button>
|
||||
<el-button size="mini" type="success" @click="showAddDialog">新增</el-button>
|
||||
<el-button size="mini" type="danger" icon="el-icon-delete"
|
||||
@click="batchDelete">删除</el-button>
|
||||
@click="deleteSelectedParams">删除</el-button>
|
||||
</div>
|
||||
<div class="custom-pagination">
|
||||
<el-select v-model="pageSize" @change="handlePageSizeChange" class="page-size-select">
|
||||
<el-option v-for="item in pageSizeOptions" :key="item" :label="`${item}条/页`"
|
||||
:value="item">
|
||||
</el-option>
|
||||
</el-select>
|
||||
<button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">
|
||||
首页
|
||||
</button>
|
||||
@@ -60,62 +84,47 @@
|
||||
</div>
|
||||
|
||||
<!-- 新增/编辑参数对话框 -->
|
||||
<el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="500px">
|
||||
<el-form :model="paramForm" :rules="rules" ref="paramForm" label-width="100px">
|
||||
<el-form-item label="参数编码" prop="paramCode">
|
||||
<el-input v-model="paramForm.paramCode" placeholder="请输入参数编码"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="参数值" prop="paramValue">
|
||||
<el-input v-model="paramForm.paramValue" placeholder="请输入参数值"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input type="textarea" v-model="paramForm.remark" placeholder="请输入备注"></el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button @click="dialogVisible = false">取 消</el-button>
|
||||
<el-button type="primary" @click="submitForm">确 定</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<div class="copyright">©2025 xiaozhi-esp32-server</div>
|
||||
<param-dialog :title="dialogTitle" :visible.sync="dialogVisible" :form="paramForm" @submit="handleSubmit"
|
||||
@cancel="dialogVisible = false" />
|
||||
<el-footer>
|
||||
<version-footer />
|
||||
</el-footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Api from "@/apis/api";
|
||||
import HeaderBar from "@/components/HeaderBar.vue";
|
||||
|
||||
import ParamDialog from "@/components/ParamDialog.vue";
|
||||
import VersionFooter from "@/components/VersionFooter.vue";
|
||||
export default {
|
||||
components: { HeaderBar },
|
||||
components: { HeaderBar, ParamDialog, VersionFooter },
|
||||
data() {
|
||||
return {
|
||||
searchCode: "",
|
||||
paramsList: [],
|
||||
currentPage: 1,
|
||||
pageSize: 5,
|
||||
loading: false,
|
||||
pageSize: 10,
|
||||
pageSizeOptions: [10, 20, 50, 100],
|
||||
total: 0,
|
||||
dialogVisible: false,
|
||||
dialogTitle: "新增参数",
|
||||
isAllSelected: false,
|
||||
sensitive_keys: ["api_key", "personal_access_token", "access_token", "token", "secret", "access_key_secret", "secret_key"],
|
||||
paramForm: {
|
||||
id: null,
|
||||
paramCode: "",
|
||||
paramValue: "",
|
||||
remark: ""
|
||||
},
|
||||
rules: {
|
||||
paramCode: [
|
||||
{ required: true, message: "请输入参数编码", trigger: "blur" }
|
||||
],
|
||||
paramValue: [
|
||||
{ required: true, message: "请输入参数值", trigger: "blur" }
|
||||
]
|
||||
}
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.fetchParams();
|
||||
|
||||
},
|
||||
|
||||
computed: {
|
||||
pageCount() {
|
||||
return Math.ceil(this.total / this.pageSize);
|
||||
@@ -137,7 +146,13 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handlePageSizeChange(val) {
|
||||
this.pageSize = val;
|
||||
this.currentPage = 1;
|
||||
this.fetchParams();
|
||||
},
|
||||
fetchParams() {
|
||||
this.loading = true;
|
||||
Api.admin.getParamsList(
|
||||
{
|
||||
page: this.currentPage,
|
||||
@@ -145,9 +160,19 @@ export default {
|
||||
paramCode: this.searchCode,
|
||||
},
|
||||
({ data }) => {
|
||||
this.loading = false;
|
||||
if (data.code === 0) {
|
||||
this.paramsList = data.data.list;
|
||||
this.paramsList = data.data.list.map(item => ({
|
||||
...item,
|
||||
selected: false,
|
||||
showValue: false
|
||||
}));
|
||||
this.total = data.data.total;
|
||||
} else {
|
||||
this.$message.error({
|
||||
message: data.msg || '获取参数列表失败',
|
||||
showClose: true
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -157,7 +182,10 @@ export default {
|
||||
this.fetchParams();
|
||||
},
|
||||
handleSelectAll() {
|
||||
this.$refs.paramsTable.toggleAllSelection();
|
||||
this.isAllSelected = !this.isAllSelected;
|
||||
this.paramsList.forEach(row => {
|
||||
row.selected = this.isAllSelected;
|
||||
});
|
||||
},
|
||||
showAddDialog() {
|
||||
this.dialogTitle = "新增参数";
|
||||
@@ -174,69 +202,105 @@ export default {
|
||||
this.paramForm = { ...row };
|
||||
this.dialogVisible = true;
|
||||
},
|
||||
submitForm() {
|
||||
this.$refs.paramForm.validate((valid) => {
|
||||
if (valid) {
|
||||
if (this.paramForm.id) {
|
||||
// 编辑
|
||||
Api.admin.updateParam(this.paramForm, ({ data }) => {
|
||||
if (data.code === 0) {
|
||||
this.$message.success("修改成功");
|
||||
this.dialogVisible = false;
|
||||
this.fetchParams();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// 新增
|
||||
Api.admin.addParam(this.paramForm, ({ data }) => {
|
||||
if (data.code === 0) {
|
||||
this.$message.success("新增成功");
|
||||
this.dialogVisible = false;
|
||||
this.fetchParams();
|
||||
}
|
||||
|
||||
handleSubmit({ form, done }) {
|
||||
if (form.id) {
|
||||
// 编辑
|
||||
Api.admin.updateParam(form, ({ data }) => {
|
||||
if (data.code === 0) {
|
||||
this.$message.success({
|
||||
message: "修改成功",
|
||||
showClose: true
|
||||
});
|
||||
this.dialogVisible = false;
|
||||
this.fetchParams();
|
||||
}
|
||||
}
|
||||
});
|
||||
done && done();
|
||||
});
|
||||
} else {
|
||||
// 新增
|
||||
Api.admin.addParam(form, ({ data }) => {
|
||||
if (data.code === 0) {
|
||||
this.$message.success({
|
||||
message: "新增成功",
|
||||
showClose: true
|
||||
});
|
||||
this.dialogVisible = false;
|
||||
this.fetchParams();
|
||||
}
|
||||
done && done();
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
deleteSelectedParams() {
|
||||
const selectedRows = this.paramsList.filter(row => row.selected);
|
||||
if (selectedRows.length === 0) {
|
||||
this.$message.warning({
|
||||
message: "请先选择需要删除的参数",
|
||||
showClose: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.deleteParam(selectedRows);
|
||||
},
|
||||
deleteParam(row) {
|
||||
this.$confirm("确定要删除该参数吗?", "警告", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
})
|
||||
.then(() => {
|
||||
Api.admin.deleteParam(row.id, ({ data }) => {
|
||||
if (data.code === 0) {
|
||||
this.$message.success("删除成功");
|
||||
this.fetchParams();
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(() => { });
|
||||
},
|
||||
batchDelete() {
|
||||
const selectedParams = this.$refs.paramsTable.selection;
|
||||
if (selectedParams.length === 0) {
|
||||
this.$message.warning("请先选择需要删除的参数");
|
||||
// 处理单个参数或参数数组
|
||||
const params = Array.isArray(row) ? row : [row];
|
||||
|
||||
if (Array.isArray(row) && row.length === 0) {
|
||||
this.$message.warning({
|
||||
message: "请先选择需要删除的参数",
|
||||
showClose: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.$confirm(`确定要删除选中的${selectedParams.length}个参数吗?`, "警告", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
})
|
||||
.then(() => {
|
||||
const ids = selectedParams.map(item => item.id);
|
||||
Api.admin.batchDeleteParams(ids, ({ data }) => {
|
||||
if (data.code === 0) {
|
||||
this.$message.success("删除成功");
|
||||
this.fetchParams();
|
||||
}
|
||||
const paramCount = params.length;
|
||||
this.$confirm(`确定要删除选中的${paramCount}个参数吗?`, '警告', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
distinguishCancelAndClose: true
|
||||
}).then(() => {
|
||||
const ids = params.map(param => param.id);
|
||||
if (ids.some(id => isNaN(id))) {
|
||||
this.$message.error({
|
||||
message: '存在无效的参数ID',
|
||||
showClose: true
|
||||
});
|
||||
})
|
||||
.catch(() => { });
|
||||
return;
|
||||
}
|
||||
|
||||
Api.admin.deleteParam(ids, ({ data }) => {
|
||||
if (data.code === 0) {
|
||||
this.$message.success({
|
||||
message: `成功删除${paramCount}个参数`,
|
||||
showClose: true
|
||||
});
|
||||
this.fetchParams();
|
||||
} else {
|
||||
this.$message.error({
|
||||
message: data.msg || '删除失败,请重试',
|
||||
showClose: true
|
||||
});
|
||||
}
|
||||
});
|
||||
}).catch(action => {
|
||||
if (action === 'cancel') {
|
||||
this.$message({
|
||||
type: 'info',
|
||||
message: '已取消删除操作',
|
||||
duration: 1000
|
||||
});
|
||||
} else {
|
||||
this.$message({
|
||||
type: 'info',
|
||||
message: '操作已关闭',
|
||||
duration: 1000
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
headerCellClassName({ columnIndex }) {
|
||||
if (columnIndex === 0) {
|
||||
@@ -263,7 +327,18 @@ export default {
|
||||
goToPage(page) {
|
||||
this.currentPage = page;
|
||||
this.fetchParams();
|
||||
}
|
||||
},
|
||||
isSensitiveParam(paramCode) {
|
||||
return this.sensitive_keys.some(key => paramCode.toLowerCase().includes(key.toLowerCase()));
|
||||
},
|
||||
maskSensitiveValue(value) {
|
||||
if (!value) return '';
|
||||
if (value.length <= 8) return '****';
|
||||
return value.substring(0, 4) + '****' + value.substring(value.length - 4);
|
||||
},
|
||||
toggleSensitiveValue(row) {
|
||||
this.$set(row, 'showValue', !row.showValue);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -280,15 +355,20 @@ export default {
|
||||
background: linear-gradient(to bottom right, #dce8ff, #e4eeff, #e6cbfd) center;
|
||||
-webkit-background-size: cover;
|
||||
-o-background-size: cover;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.main-wrapper {
|
||||
margin: 5px 22px;
|
||||
border-radius: 15px;
|
||||
min-height: 600px;
|
||||
min-height: calc(100vh - 24vh);
|
||||
height: auto;
|
||||
max-height: 80vh;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||
position: relative;
|
||||
background: rgba(237, 242, 255, 0.5);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.operation-bar {
|
||||
@@ -333,14 +413,28 @@ export default {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
min-width: 600px;
|
||||
overflow-x: auto;
|
||||
overflow: auto;
|
||||
background-color: white;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.params-card {
|
||||
background: white;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
overflow: hidden;
|
||||
|
||||
::v-deep .el-card__body {
|
||||
padding: 15px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.table_bottom {
|
||||
@@ -348,6 +442,7 @@ export default {
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.ctrl_btn {
|
||||
@@ -384,29 +479,19 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
gap: 10px;
|
||||
|
||||
.el-select {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.pagination-btn:first-child,
|
||||
.pagination-btn:nth-child(2),
|
||||
.pagination-btn:nth-last-child(2) {
|
||||
.pagination-btn:nth-last-child(2),
|
||||
.pagination-btn:nth-child(3) {
|
||||
min-width: 60px;
|
||||
height: 32px;
|
||||
padding: 0 12px;
|
||||
@@ -428,7 +513,7 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
.pagination-btn:not(:first-child):not(:nth-child(2)):not(:nth-last-child(2)) {
|
||||
.pagination-btn:not(:first-child):not(:nth-child(3)):not(:nth-child(2)):not(:nth-last-child(2)) {
|
||||
min-width: 28px;
|
||||
height: 32px;
|
||||
padding: 0;
|
||||
@@ -464,6 +549,20 @@ export default {
|
||||
|
||||
:deep(.transparent-table) {
|
||||
background: white;
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.el-table__body-wrapper {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
max-height: none !important;
|
||||
}
|
||||
|
||||
.el-table__header-wrapper {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.el-table__header th {
|
||||
background: white !important;
|
||||
@@ -484,19 +583,6 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
: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;
|
||||
@@ -533,4 +619,102 @@ export default {
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
:deep(.el-table .el-button--text) {
|
||||
color: #7079aa;
|
||||
}
|
||||
|
||||
:deep(.el-table .el-button--text:hover) {
|
||||
color: #5a64b5;
|
||||
}
|
||||
|
||||
.el-button--success {
|
||||
background: #5bc98c;
|
||||
color: white;
|
||||
}
|
||||
|
||||
:deep(.el-table .cell) {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.page-size-select {
|
||||
width: 100px;
|
||||
margin-right: 10px;
|
||||
|
||||
:deep(.el-input__inner) {
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #e4e7ed;
|
||||
background: #dee7ff;
|
||||
color: #606266;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
:deep(.el-input__suffix) {
|
||||
right: 6px;
|
||||
width: 15px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
top: 6px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
:deep(.el-input__suffix-inner) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.el-icon-arrow-up:before) {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
border-left: 6px solid transparent;
|
||||
border-right: 6px solid transparent;
|
||||
border-top: 9px solid #606266;
|
||||
position: relative;
|
||||
transform: rotate(0deg);
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-table) {
|
||||
.el-table__body-wrapper {
|
||||
transition: height 0.3s ease;
|
||||
}
|
||||
}
|
||||
|
||||
.el-table {
|
||||
--table-max-height: calc(100vh - 40vh);
|
||||
max-height: var(--table-max-height);
|
||||
|
||||
.el-table__body-wrapper {
|
||||
max-height: calc(var(--table-max-height) - 40px);
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-loading-mask) {
|
||||
background-color: rgba(255, 255, 255, 0.6) !important;
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
:deep(.el-loading-spinner .circular) {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
:deep(.el-loading-spinner .path) {
|
||||
stroke: #6b8cff;
|
||||
}
|
||||
|
||||
:deep(.el-loading-text) {
|
||||
color: #6b8cff !important;
|
||||
font-size: 14px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,876 @@
|
||||
<template>
|
||||
<div class="welcome">
|
||||
<HeaderBar />
|
||||
|
||||
<div class="operation-bar">
|
||||
<h2 class="page-title">供应器管理</h2>
|
||||
<div class="right-operations">
|
||||
<el-dropdown trigger="click" @command="handleSelectModelType" @visible-change="handleDropdownVisibleChange">
|
||||
<el-button class="category-btn">
|
||||
类别筛选 {{ selectedModelTypeLabel }}<i class="el-icon-arrow-down el-icon--right"
|
||||
:class="{ 'rotate-down': DropdownVisible }"></i>
|
||||
</el-button>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<el-dropdown-item command="">全部</el-dropdown-item>
|
||||
<el-dropdown-item v-for="item in modelTypes" :key="item.value" :command="item.value">
|
||||
{{ item.label }}
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
<el-input placeholder="请输入供应器名称查询" v-model="searchName" class="search-input" @keyup.enter.native="handleSearch"
|
||||
clearable />
|
||||
<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="provider-card" shadow="never">
|
||||
<el-table ref="providersTable" :data="filteredProvidersList" class="transparent-table" v-loading="loading"
|
||||
element-loading-text="拼命加载中" element-loading-spinner="el-icon-loading"
|
||||
element-loading-background="rgba(255, 255, 255, 0.7)" :header-cell-class-name="headerCellClassName">
|
||||
<el-table-column label="选择" align="center" width="120">
|
||||
<template slot-scope="scope">
|
||||
<el-checkbox v-model="scope.row.selected"></el-checkbox>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="类别" prop="modelType" align="center" width="200">
|
||||
<template slot="header" slot-scope="scope">
|
||||
<el-dropdown trigger="click" @command="handleSelectModelType"
|
||||
@visible-change="isDropdownOpen = $event">
|
||||
<span class="dropdown-trigger" :class="{ 'active': isDropdownOpen }">
|
||||
类别{{ selectedModelTypeLabel }} <i class="dropdown-arrow"
|
||||
:class="{ 'is-active': isDropdownOpen }"></i>
|
||||
</span>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<el-dropdown-item command="">全部</el-dropdown-item>
|
||||
<el-dropdown-item v-for="item in modelTypes" :key="item.value" :command="item.value">
|
||||
{{ item.label }}
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
<template slot-scope="scope">
|
||||
<el-tag :type="getModelTypeTag(scope.row.modelType)">
|
||||
{{ getModelTypeLabel(scope.row.modelType) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="供应器编码" prop="providerCode" align="center" width="150"></el-table-column>
|
||||
<el-table-column label="名称" prop="name" align="center"></el-table-column>
|
||||
<el-table-column label="字段配置" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-popover placement="top-start" width="400" trigger="hover">
|
||||
<div v-for="field in scope.row.fields" :key="field.key" class="field-item">
|
||||
<span class="field-label">{{ field.label }}:</span>
|
||||
<span class="field-type">{{ field.type }}</span>
|
||||
<span v-if="isSensitiveField(field.key)" class="sensitive-tag">敏感</span>
|
||||
</div>
|
||||
<el-button slot="reference" size="mini" type="text">查看字段</el-button>
|
||||
</el-popover>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="排序" prop="sort" align="center" width="80"></el-table-column>
|
||||
<el-table-column label="操作" align="center" width="180">
|
||||
<template slot-scope="scope">
|
||||
<el-button size="mini" type="text" @click="editProvider(scope.row)">编辑</el-button>
|
||||
<el-button size="mini" type="text" @click="deleteProvider(scope.row)">删除</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">
|
||||
{{ isAllSelected ? '取消全选' : '全选' }}
|
||||
</el-button>
|
||||
<el-button size="mini" type="success" @click="showAddDialog">新增</el-button>
|
||||
<el-button size="mini" type="danger" icon="el-icon-delete" @click="deleteSelectedProviders">删除
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="custom-pagination">
|
||||
<el-select v-model="pageSize" @change="handlePageSizeChange" class="page-size-select">
|
||||
<el-option v-for="item in pageSizeOptions" :key="item" :label="`${item}条/页`" :value="item">
|
||||
</el-option>
|
||||
</el-select>
|
||||
<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>
|
||||
|
||||
<!-- 新增/编辑供应器对话框 -->
|
||||
<provider-dialog :title="dialogTitle" :visible.sync="dialogVisible" :form="providerForm" :model-types="modelTypes"
|
||||
@submit="handleSubmit" @cancel="dialogVisible = false" />
|
||||
|
||||
<el-footer>
|
||||
<version-footer />
|
||||
</el-footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Api from "@/apis/api";
|
||||
import HeaderBar from "@/components/HeaderBar.vue";
|
||||
import ProviderDialog from "@/components/ProviderDialog.vue";
|
||||
import VersionFooter from "@/components/VersionFooter.vue";
|
||||
|
||||
export default {
|
||||
components: { HeaderBar, ProviderDialog, VersionFooter },
|
||||
data() {
|
||||
return {
|
||||
searchName: "",
|
||||
searchModelType: "",
|
||||
providersList: [],
|
||||
modelTypes: [
|
||||
{ value: "ASR", label: "语音识别" },
|
||||
{ value: "TTS", label: "语音合成" },
|
||||
{ value: "LLM", label: "大语言模型" },
|
||||
{ value: "Intent", label: "意图识别" },
|
||||
{ value: "Memory", label: "记忆模块" },
|
||||
{ value: "VAD", label: "语音活动检测" }
|
||||
],
|
||||
currentPage: 1,
|
||||
loading: false,
|
||||
pageSize: 10,
|
||||
pageSizeOptions: [10, 20, 50, 100],
|
||||
total: 0,
|
||||
dialogVisible: false,
|
||||
dialogTitle: "新增供应器",
|
||||
isAllSelected: false,
|
||||
isDropdownOpen: false,
|
||||
sensitive_keys: ["api_key", "personal_access_token", "access_token", "token", "secret", "access_key_secret", "secret_key"],
|
||||
providerForm: {
|
||||
id: null,
|
||||
modelType: "",
|
||||
providerCode: "",
|
||||
name: "",
|
||||
fields: [],
|
||||
sort: 0
|
||||
},
|
||||
DropdownVisible: false,
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.fetchProviders();
|
||||
},
|
||||
computed: {
|
||||
selectedModelTypeLabel() {
|
||||
if (!this.searchModelType) return "(全部)";
|
||||
const selectedType = this.modelTypes.find(item => item.value === this.searchModelType);
|
||||
return selectedType ? `(${selectedType.label})` : "";
|
||||
},
|
||||
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;
|
||||
},
|
||||
filteredProvidersList() {
|
||||
return this.providersList;
|
||||
|
||||
// let list = this.providersList.filter(item => {
|
||||
// const nameMatch = item.name.toLowerCase().includes(this.searchName.toLowerCase());
|
||||
// const typeMatch = !this.searchModelType || item.model_type === this.searchModelType;
|
||||
// return nameMatch && typeMatch;
|
||||
// });
|
||||
|
||||
// list.sort((a, b) => a.sort - b.sort);
|
||||
|
||||
// // 分页处理
|
||||
// const start = (this.currentPage - 1) * this.pageSize;
|
||||
// return list.slice(start, start + this.pageSize);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
fetchProviders() {
|
||||
this.loading = true;
|
||||
|
||||
Api.model.getModelProvidersPage(
|
||||
{
|
||||
page: this.currentPage,
|
||||
limit: this.pageSize,
|
||||
name: this.searchName,
|
||||
modelType: this.searchModelType
|
||||
},
|
||||
({ data }) => {
|
||||
this.loading = false;
|
||||
if (data.code === 0) {
|
||||
this.providersList = data.data.list.map(item => {
|
||||
return {
|
||||
...item,
|
||||
selected: false,
|
||||
fields: JSON.parse(item.fields)
|
||||
};
|
||||
});
|
||||
this.total = data.data.total;
|
||||
} else {
|
||||
this.$message.error({
|
||||
message: data.msg || '获取参数列表失败'
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
},
|
||||
handleSearch() {
|
||||
this.currentPage = 1;
|
||||
this.fetchProviders();
|
||||
},
|
||||
handleSelectModelType(value) {
|
||||
this.isDropdownOpen = false;
|
||||
this.searchModelType = value;
|
||||
this.handleSearch();
|
||||
},
|
||||
handleSelectAll() {
|
||||
this.isAllSelected = !this.isAllSelected;
|
||||
this.providersList.forEach(row => {
|
||||
row.selected = this.isAllSelected;
|
||||
});
|
||||
},
|
||||
showAddDialog() {
|
||||
this.dialogTitle = "新增供应器";
|
||||
this.providerForm = {
|
||||
id: null,
|
||||
modelType: "",
|
||||
providerCode: "",
|
||||
name: "",
|
||||
fields: [],
|
||||
sort: 0
|
||||
};
|
||||
this.dialogVisible = true;
|
||||
},
|
||||
editProvider(row) {
|
||||
this.dialogTitle = "编辑供应器";
|
||||
this.providerForm = {
|
||||
...row,
|
||||
fields: JSON.parse(JSON.stringify(row.fields))
|
||||
};
|
||||
this.dialogVisible = true;
|
||||
},
|
||||
handleSubmit({ form, done }) {
|
||||
this.loading = true;
|
||||
if (form.id) {
|
||||
// 编辑
|
||||
Api.model.updateModelProvider(form, ({ data }) => {
|
||||
|
||||
if (data.code === 0) {
|
||||
this.fetchProviders(); // 刷新表格
|
||||
this.$message.success({
|
||||
message: "修改成功",
|
||||
showClose: true
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// 新增
|
||||
Api.model.addModelProvider(form, ({ data }) => {
|
||||
if (data.code === 0) {
|
||||
this.fetchProviders(); // 刷新表格
|
||||
this.$message.success({
|
||||
message: "新增成功",
|
||||
showClose: true
|
||||
});
|
||||
this.total += 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
this.loading = false;
|
||||
this.dialogVisible = false;
|
||||
done && done();
|
||||
},
|
||||
deleteSelectedProviders() {
|
||||
const selectedRows = this.providersList.filter(row => row.selected);
|
||||
if (selectedRows.length === 0) {
|
||||
this.$message.warning({
|
||||
message: "请先选择需要删除的供应器",
|
||||
showClose: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.deleteProvider(selectedRows);
|
||||
},
|
||||
deleteProvider(row) {
|
||||
const providers = Array.isArray(row) ? row : [row];
|
||||
const providerCount = providers.length;
|
||||
|
||||
this.$confirm(`确定要删除选中的${providerCount}个供应器吗?`, '警告', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}).then(() => {
|
||||
const ids = providers.map(provider => provider.id);
|
||||
Api.model.deleteModelProviderByIds(ids, ({ data }) => {
|
||||
if (data.code === 0) {
|
||||
|
||||
this.isAllSelected = false;
|
||||
this.fetchProviders(); // 刷新表格
|
||||
|
||||
this.$message.success({
|
||||
message: `成功删除${providerCount}个参数`,
|
||||
showClose: true
|
||||
});
|
||||
} else {
|
||||
this.$message.error({
|
||||
message: data.msg || '删除失败,请重试',
|
||||
showClose: true
|
||||
});
|
||||
}
|
||||
});
|
||||
}).catch(() => {
|
||||
this.$message({
|
||||
type: 'info',
|
||||
message: '已取消删除',
|
||||
showClose: true,
|
||||
duration: 1000
|
||||
});
|
||||
});
|
||||
},
|
||||
getModelTypeTag(type) {
|
||||
const typeMap = {
|
||||
'ASR': 'success',
|
||||
'TTS': 'warning',
|
||||
'LLM': 'danger',
|
||||
'Intent': 'info',
|
||||
'Memory': '',
|
||||
'VAD': 'primary'
|
||||
};
|
||||
return typeMap[type] || '';
|
||||
},
|
||||
getModelTypeLabel(type) {
|
||||
const typeItem = this.modelTypes.find(item => item.value === type);
|
||||
return typeItem ? typeItem.label : type;
|
||||
},
|
||||
isSensitiveField(fieldKey) {
|
||||
if (typeof fieldKey !== 'string') return false;
|
||||
return this.sensitive_keys.some(key =>
|
||||
fieldKey.toLowerCase().includes(key.toLowerCase())
|
||||
);
|
||||
},
|
||||
handlePageSizeChange(val) {
|
||||
this.pageSize = val;
|
||||
this.currentPage = 1;
|
||||
this.fetchProviders();
|
||||
},
|
||||
headerCellClassName({ columnIndex }) {
|
||||
if (columnIndex === 0) {
|
||||
return "custom-selection-header";
|
||||
}
|
||||
return "";
|
||||
},
|
||||
goFirst() {
|
||||
this.currentPage = 1;
|
||||
this.fetchProviders();
|
||||
},
|
||||
goPrev() {
|
||||
if (this.currentPage > 1) {
|
||||
this.currentPage--;
|
||||
this.fetchProviders();
|
||||
}
|
||||
},
|
||||
goNext() {
|
||||
if (this.currentPage < this.pageCount) {
|
||||
console.log("this.currentPage", this.currentPage);
|
||||
this.currentPage++;
|
||||
this.fetchProviders();
|
||||
}
|
||||
},
|
||||
goToPage(page) {
|
||||
this.currentPage = page;
|
||||
this.fetchProviders();
|
||||
},
|
||||
handleDropdownVisibleChange(visible) {
|
||||
this.DropdownVisible = visible;
|
||||
},
|
||||
},
|
||||
};
|
||||
</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;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.main-wrapper {
|
||||
margin: 5px 22px;
|
||||
border-radius: 15px;
|
||||
min-height: calc(100vh - 24vh);
|
||||
height: auto;
|
||||
max-height: 80vh;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||
position: relative;
|
||||
background: rgba(237, 242, 255, 0.5);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.operation-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 24px;
|
||||
}
|
||||
|
||||
.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;
|
||||
border: 1px solid #fff;
|
||||
}
|
||||
|
||||
.content-area {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
min-width: 600px;
|
||||
overflow: auto;
|
||||
background-color: white;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.el-card {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.provider-card {
|
||||
background: white;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: none;
|
||||
overflow: hidden;
|
||||
|
||||
::v-deep .el-card__body {
|
||||
padding: 15px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.table_bottom {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 10px;
|
||||
padding-bottom: 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--danger {
|
||||
background: #fd5b63;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-pagination {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
|
||||
.el-select {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.pagination-btn:first-child,
|
||||
.pagination-btn:nth-child(2),
|
||||
.pagination-btn:nth-last-child(2),
|
||||
.pagination-btn:nth-child(3) {
|
||||
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(3)):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;
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.el-table__body-wrapper {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
max-height: none !important;
|
||||
}
|
||||
|
||||
.el-table__header-wrapper {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.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(.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-table .el-button--text) {
|
||||
color: #7079aa;
|
||||
}
|
||||
|
||||
:deep(.el-table .el-button--text:hover) {
|
||||
color: #5a64b5;
|
||||
}
|
||||
|
||||
.el-button--success {
|
||||
background: #5bc98c;
|
||||
color: white;
|
||||
}
|
||||
|
||||
:deep(.el-table .cell) {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.page-size-select {
|
||||
width: 100px;
|
||||
margin-right: 10px;
|
||||
|
||||
:deep(.el-input__inner) {
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #e4e7ed;
|
||||
background: #dee7ff;
|
||||
color: #606266;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
:deep(.el-input__suffix) {
|
||||
right: 6px;
|
||||
width: 15px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
top: 6px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
:deep(.el-input__suffix-inner) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.el-icon-arrow-up:before) {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
border-left: 6px solid transparent;
|
||||
border-right: 6px solid transparent;
|
||||
border-top: 9px solid #606266;
|
||||
position: relative;
|
||||
transform: rotate(0deg);
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-table) {
|
||||
.el-table__body-wrapper {
|
||||
transition: height 0.3s ease;
|
||||
}
|
||||
}
|
||||
|
||||
.el-table {
|
||||
--table-max-height: calc(100vh - 40vh);
|
||||
max-height: var(--table-max-height);
|
||||
|
||||
.el-table__body-wrapper {
|
||||
max-height: calc(var(--table-max-height) - 40px);
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-loading-mask) {
|
||||
background-color: rgba(255, 255, 255, 0.6) !important;
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
:deep(.el-loading-spinner .circular) {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
:deep(.el-loading-spinner .path) {
|
||||
stroke: #6b8cff;
|
||||
}
|
||||
|
||||
:deep(.el-loading-text) {
|
||||
color: #6b8cff !important;
|
||||
font-size: 14px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.field-item {
|
||||
padding: 5px 0;
|
||||
border-bottom: 1px solid #eee;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.field-label {
|
||||
flex: 1;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.field-type {
|
||||
width: 80px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.sensitive-tag {
|
||||
margin-left: 10px;
|
||||
color: #f56c6c;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-trigger {
|
||||
font-size: 14px;
|
||||
color: #303133;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&:hover {
|
||||
color: #409EFF;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-trigger.active {
|
||||
color: #409EFF;
|
||||
}
|
||||
|
||||
.dropdown-arrow {
|
||||
display: inline-block;
|
||||
margin-left: 5px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 5px solid transparent;
|
||||
border-right: 5px solid transparent;
|
||||
border-top: 7px solid black;
|
||||
position: relative;
|
||||
transition: transform 0.3s ease;
|
||||
transform: rotate(0deg);
|
||||
|
||||
&.is-active {
|
||||
transform: rotate(180deg);
|
||||
border-top-color: #409EFF;
|
||||
}
|
||||
}
|
||||
|
||||
.rotate-down {
|
||||
transform: rotate(180deg);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.el-icon-arrow-down {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.dropdown-trigger {
|
||||
font-size: 14px;
|
||||
color: #303133;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&:hover {
|
||||
color: #409EFF;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-trigger.active {
|
||||
color: #409EFF;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,475 @@
|
||||
<template>
|
||||
<div class="welcome">
|
||||
<HeaderBar />
|
||||
|
||||
<div class="operation-bar">
|
||||
<h2 class="page-title">服务端管理</h2>
|
||||
</div>
|
||||
|
||||
<div class="main-wrapper">
|
||||
<div class="content-panel">
|
||||
<div class="content-area">
|
||||
<el-card class="params-card" shadow="never">
|
||||
<el-table ref="paramsTable" :data="paramsList" class="transparent-table" v-loading="loading"
|
||||
element-loading-text="拼命加载中" element-loading-spinner="el-icon-loading"
|
||||
element-loading-background="rgba(255, 255, 255, 0.7)" :header-cell-class-name="headerCellClassName">
|
||||
<el-table-column label="选择" align="center" width="120">
|
||||
<template slot-scope="scope">
|
||||
<el-checkbox v-model="scope.row.selected"></el-checkbox>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="ws地址" prop="address" align="center"></el-table-column>
|
||||
<el-table-column label="操作" prop="operator" align="center" show-overflow-tooltip>
|
||||
<template slot-scope="scope">
|
||||
<el-button size="medium" type="text" @click="emitAction(scope.row, actionMap.restart)">重启</el-button>
|
||||
<el-button size="medium" type="text"
|
||||
@click="emitAction(scope.row, actionMap.update_config)">更新配置</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<el-footer>
|
||||
<version-footer />
|
||||
</el-footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Api from "@/apis/api";
|
||||
import HeaderBar from "@/components/HeaderBar.vue";
|
||||
import ParamDialog from "@/components/ParamDialog.vue";
|
||||
import VersionFooter from "@/components/VersionFooter.vue";
|
||||
|
||||
export default {
|
||||
components: { HeaderBar, ParamDialog, VersionFooter },
|
||||
data() {
|
||||
return {
|
||||
paramsList: [],
|
||||
actionMap: {
|
||||
restart: {
|
||||
value: 'restart',
|
||||
title: "重启服务端",
|
||||
message: "确定要重启服务端吗?",
|
||||
confirmText: "重启",
|
||||
},
|
||||
update_config: {
|
||||
value: 'update_config',
|
||||
title: "更新配置",
|
||||
message: "确定要更新配置吗?",
|
||||
confirmText: "更新",
|
||||
}
|
||||
},
|
||||
currentPage: 1,
|
||||
loading: false,
|
||||
pageSize: 10,
|
||||
pageSizeOptions: [10, 20, 50, 100],
|
||||
total: 0,
|
||||
dialogVisible: false,
|
||||
dialogTitle: "新增参数",
|
||||
isAllSelected: false,
|
||||
sensitive_keys: ["api_key", "personal_access_token", "access_token", "token", "secret", "access_key_secret", "secret_key"],
|
||||
paramForm: {
|
||||
id: null,
|
||||
paramCode: "",
|
||||
paramValue: "",
|
||||
remark: ""
|
||||
},
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.fetchParams();
|
||||
},
|
||||
|
||||
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: {
|
||||
handlePageSizeChange(val) {
|
||||
this.pageSize = val;
|
||||
this.currentPage = 1;
|
||||
this.fetchParams();
|
||||
},
|
||||
fetchParams() {
|
||||
this.loading = true;
|
||||
Api.admin.getWsServerList(
|
||||
{},
|
||||
({ data }) => {
|
||||
this.loading = false;
|
||||
if (data.code === 0) {
|
||||
this.paramsList = data.data.map(item => ({ address: item }));
|
||||
this.total = data.data.length;
|
||||
} else {
|
||||
this.$message.error({
|
||||
message: data.msg || '获取参数列表失败',
|
||||
showClose: true
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
},
|
||||
emitAction(rowItem, actionItem) {
|
||||
if (actionItem === undefined || rowItem.address === undefined) {
|
||||
return;
|
||||
}
|
||||
// 弹开询问框
|
||||
this.$confirm(actionItem.message, actionItem.title, {
|
||||
confirmButtonText: actionItem.confirmText, // 确认按钮文本
|
||||
}).then(() => {
|
||||
// 用户点击了确认按钮
|
||||
Api.admin.sendWsServerAction({
|
||||
targetWs: rowItem.address,
|
||||
action: actionItem.value
|
||||
}, ({ data }) => {
|
||||
if (data.code !== 0) {
|
||||
this.$message.error({
|
||||
message: data.msg || '操作失败',
|
||||
showClose: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.$message.success({
|
||||
message: `${actionItem.title}成功`,
|
||||
showClose: true
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
headerCellClassName({ columnIndex }) {
|
||||
if (columnIndex === 0) {
|
||||
return "custom-selection-header";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
},
|
||||
};
|
||||
</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;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.main-wrapper {
|
||||
margin: 5px 22px;
|
||||
border-radius: 15px;
|
||||
min-height: calc(100vh - 24vh);
|
||||
height: auto;
|
||||
max-height: 80vh;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||
position: relative;
|
||||
background: rgba(237, 242, 255, 0.5);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.operation-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 24px;
|
||||
}
|
||||
|
||||
.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;
|
||||
border: 1px solid #fff;
|
||||
}
|
||||
|
||||
.content-area {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
min-width: 600px;
|
||||
overflow: auto;
|
||||
background-color: white;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.params-card {
|
||||
background: white;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
overflow: hidden;
|
||||
|
||||
::v-deep .el-card__body {
|
||||
padding: 15px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.table_bottom {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 10px;
|
||||
padding-bottom: 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--danger {
|
||||
background: #fd5b63;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.transparent-table) {
|
||||
background: white;
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.el-table__body-wrapper {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
max-height: none !important;
|
||||
}
|
||||
|
||||
.el-table__header-wrapper {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.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(.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-table .el-button--text) {
|
||||
color: #7079aa;
|
||||
}
|
||||
|
||||
:deep(.el-table .el-button--text:hover) {
|
||||
color: #5a64b5;
|
||||
}
|
||||
|
||||
.el-button--success {
|
||||
background: #5bc98c;
|
||||
color: white;
|
||||
}
|
||||
|
||||
:deep(.el-table .cell) {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.page-size-select {
|
||||
width: 100px;
|
||||
margin-right: 10px;
|
||||
|
||||
:deep(.el-input__inner) {
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #e4e7ed;
|
||||
background: #dee7ff;
|
||||
color: #606266;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
:deep(.el-input__suffix) {
|
||||
right: 6px;
|
||||
width: 15px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
top: 6px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
:deep(.el-input__suffix-inner) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.el-icon-arrow-up:before) {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
border-left: 6px solid transparent;
|
||||
border-right: 6px solid transparent;
|
||||
border-top: 9px solid #606266;
|
||||
position: relative;
|
||||
transform: rotate(0deg);
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-table) {
|
||||
.el-table__body-wrapper {
|
||||
transition: height 0.3s ease;
|
||||
}
|
||||
}
|
||||
|
||||
.el-table {
|
||||
--table-max-height: calc(100vh - 40vh);
|
||||
max-height: var(--table-max-height);
|
||||
|
||||
.el-table__body-wrapper {
|
||||
max-height: calc(var(--table-max-height) - 40px);
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-loading-mask) {
|
||||
background-color: rgba(255, 255, 255, 0.6) !important;
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
:deep(.el-loading-spinner .circular) {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
:deep(.el-loading-spinner .path) {
|
||||
stroke: #6b8cff;
|
||||
}
|
||||
|
||||
:deep(.el-loading-text) {
|
||||
color: #6b8cff !important;
|
||||
font-size: 14px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -15,12 +15,18 @@
|
||||
<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 ref="userTable" :data="userList" class="transparent-table" v-loading="loading"
|
||||
element-loading-text="拼命加载中" element-loading-spinner="el-icon-loading"
|
||||
element-loading-background="rgba(255, 255, 255, 0.7)">
|
||||
<el-table-column label="选择" align="center" width="120">
|
||||
<template slot-scope="scope">
|
||||
<el-checkbox v-model="scope.row.selected"></el-checkbox>
|
||||
</template>
|
||||
</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="createDate" align="center"></el-table-column>
|
||||
<el-table-column label="状态" prop="status" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-tag v-if="scope.row.status === 1" type="success">正常</el-tag>
|
||||
@@ -41,13 +47,20 @@
|
||||
|
||||
<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="primary" class="select-all-btn" @click="handleSelectAll">
|
||||
{{ isAllSelected ? '取消全选' : '全选' }}
|
||||
</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">
|
||||
<el-select v-model="pageSize" @change="handlePageSizeChange" class="page-size-select">
|
||||
<el-option v-for="item in pageSizeOptions" :key="item" :label="`${item}条/页`" :value="item">
|
||||
</el-option>
|
||||
</el-select>
|
||||
|
||||
<button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">
|
||||
首页
|
||||
</button>
|
||||
@@ -68,27 +81,32 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="copyright">©2025 xiaozhi-esp32-server</div>
|
||||
<view-password-dialog :visible.sync="showViewPassword" :password="currentPassword" />
|
||||
<el-footer>
|
||||
<version-footer />
|
||||
</el-footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Api from "@/apis/api";
|
||||
import HeaderBar from "@/components/HeaderBar.vue";
|
||||
import VersionFooter from "@/components/VersionFooter.vue";
|
||||
import ViewPasswordDialog from "@/components/ViewPasswordDialog.vue";
|
||||
|
||||
export default {
|
||||
components: { HeaderBar, ViewPasswordDialog },
|
||||
components: { HeaderBar, ViewPasswordDialog, VersionFooter },
|
||||
data() {
|
||||
return {
|
||||
showViewPassword: false,
|
||||
currentPassword: "",
|
||||
searchPhone: "",
|
||||
userList: [],
|
||||
pageSizeOptions: [10, 20, 50, 100],
|
||||
currentPage: 1,
|
||||
pageSize: 5,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
isAllSelected: false,
|
||||
loading: false,
|
||||
};
|
||||
},
|
||||
created() {
|
||||
@@ -115,7 +133,14 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handlePageSizeChange(val) {
|
||||
this.pageSize = val;
|
||||
this.currentPage = 1;
|
||||
this.fetchUsers();
|
||||
},
|
||||
|
||||
fetchUsers() {
|
||||
this.loading = true;
|
||||
Api.admin.getUserList(
|
||||
{
|
||||
page: this.currentPage,
|
||||
@@ -123,8 +148,12 @@ export default {
|
||||
mobile: this.searchPhone,
|
||||
},
|
||||
({ data }) => {
|
||||
this.loading = false; // 结束加载
|
||||
if (data.code === 0) {
|
||||
this.userList = data.data.list
|
||||
this.userList = data.data.list.map(item => ({
|
||||
...item,
|
||||
selected: false
|
||||
}));
|
||||
this.total = data.data.total;
|
||||
}
|
||||
}
|
||||
@@ -135,10 +164,13 @@ export default {
|
||||
this.fetchUsers();
|
||||
},
|
||||
handleSelectAll() {
|
||||
this.$refs.userTable.toggleAllSelection();
|
||||
this.isAllSelected = !this.isAllSelected;
|
||||
this.userList.forEach(row => {
|
||||
row.selected = this.isAllSelected;
|
||||
});
|
||||
},
|
||||
batchDelete() {
|
||||
const selectedUsers = this.$refs.userTable.selection;
|
||||
const selectedUsers = this.userList.filter(user => user.selected);
|
||||
if (selectedUsers.length === 0) {
|
||||
this.$message.warning("请先选择需要删除的用户");
|
||||
return;
|
||||
@@ -203,19 +235,11 @@ export default {
|
||||
});
|
||||
},
|
||||
batchEnable() {
|
||||
const selectedUsers = this.$refs.userTable.selection;
|
||||
if (selectedUsers.length === 0) {
|
||||
this.$message.warning("请先选择需要启用的用户");
|
||||
return;
|
||||
}
|
||||
const selectedUsers = this.userList.filter(user => user.selected);
|
||||
this.handleChangeStatus(selectedUsers, 1);
|
||||
},
|
||||
batchDisable() {
|
||||
const selectedUsers = this.$refs.userTable.selection;
|
||||
if (selectedUsers.length === 0) {
|
||||
this.$message.warning("请先选择需要禁用的用户");
|
||||
return;
|
||||
}
|
||||
const selectedUsers = this.userList.filter(user => user.selected);
|
||||
this.handleChangeStatus(selectedUsers, 0);
|
||||
},
|
||||
resetPassword(row) {
|
||||
@@ -259,12 +283,6 @@ export default {
|
||||
})
|
||||
.catch(() => { });
|
||||
},
|
||||
headerCellClassName({ columnIndex }) {
|
||||
if (columnIndex === 0) {
|
||||
return "custom-selection-header";
|
||||
}
|
||||
return "";
|
||||
},
|
||||
goFirst() {
|
||||
this.currentPage = 1;
|
||||
this.fetchUsers();
|
||||
@@ -319,7 +337,7 @@ export default {
|
||||
}).catch(() => {
|
||||
// 用户取消操作
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -336,15 +354,20 @@ export default {
|
||||
background: linear-gradient(to bottom right, #dce8ff, #e4eeff, #e6cbfd) center;
|
||||
-webkit-background-size: cover;
|
||||
-o-background-size: cover;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.main-wrapper {
|
||||
margin: 5px 22px;
|
||||
border-radius: 15px;
|
||||
min-height: 600px;
|
||||
min-height: calc(100vh - 24vh);
|
||||
height: auto;
|
||||
max-height: 80vh;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||
position: relative;
|
||||
background: rgba(237, 242, 255, 0.5);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.operation-bar {
|
||||
@@ -391,12 +414,26 @@ export default {
|
||||
min-width: 600px;
|
||||
overflow-x: auto;
|
||||
background-color: white;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.user-card {
|
||||
background: white;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
overflow: hidden;
|
||||
|
||||
::v-deep .el-card__body {
|
||||
padding: 15px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.table_bottom {
|
||||
@@ -457,28 +494,18 @@ export default {
|
||||
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;
|
||||
|
||||
.el-select {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.pagination-btn:first-child,
|
||||
.pagination-btn:nth-child(2),
|
||||
.pagination-btn:nth-child(3),
|
||||
.pagination-btn:nth-last-child(2) {
|
||||
min-width: 60px;
|
||||
height: 32px;
|
||||
@@ -501,7 +528,7 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
.pagination-btn:not(:first-child):not(:nth-child(2)):not(:nth-last-child(2)) {
|
||||
.pagination-btn:not(:first-child):not(:nth-child(2)):not(:nth-child(3)):not(:nth-last-child(2)) {
|
||||
min-width: 28px;
|
||||
height: 32px;
|
||||
padding: 0;
|
||||
@@ -537,6 +564,20 @@ export default {
|
||||
|
||||
:deep(.transparent-table) {
|
||||
background: white;
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.el-table__body-wrapper {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
max-height: none !important;
|
||||
}
|
||||
|
||||
.el-table__header-wrapper {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.el-table__header th {
|
||||
background: white !important;
|
||||
@@ -556,6 +597,7 @@ export default {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-table .el-button--text) {
|
||||
color: #7079aa !important;
|
||||
}
|
||||
@@ -564,19 +606,6 @@ export default {
|
||||
color: #5a64b5 !important;
|
||||
}
|
||||
|
||||
: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;
|
||||
@@ -613,4 +642,77 @@ export default {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.page-size-select {
|
||||
width: 100px;
|
||||
margin-right: 10px;
|
||||
|
||||
:deep(.el-input__inner) {
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #e4e7ed;
|
||||
background: #dee7ff;
|
||||
color: #606266;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
:deep(.el-input__suffix) {
|
||||
right: 6px;
|
||||
width: 15px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
top: 6px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
:deep(.el-input__suffix-inner) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.el-icon-arrow-up:before) {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
border-left: 6px solid transparent;
|
||||
border-right: 6px solid transparent;
|
||||
border-top: 9px solid #606266;
|
||||
position: relative;
|
||||
transform: rotate(0deg);
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
}
|
||||
|
||||
.el-table {
|
||||
--table-max-height: calc(100vh - 40vh);
|
||||
max-height: var(--table-max-height);
|
||||
|
||||
.el-table__body-wrapper {
|
||||
max-height: calc(var(--table-max-height) - 40px);
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-loading-mask) {
|
||||
background-color: rgba(255, 255, 255, 0.6) !important;
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
:deep(.el-loading-spinner .circular) {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
:deep(.el-loading-spinner .path) {
|
||||
stroke: #6b8cff;
|
||||
}
|
||||
|
||||
:deep(.el-loading-text) {
|
||||
color: #6b8cff !important;
|
||||
font-size: 14px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -66,20 +66,7 @@
|
||||
cursor: pointer;
|
||||
color: #fff;
|
||||
line-height: 35px;
|
||||
margin: 35px 15px 15px;
|
||||
}
|
||||
|
||||
.code-send {
|
||||
width: 70px;
|
||||
height: 32px;
|
||||
border-radius: 10px;
|
||||
background: #e6ebff;
|
||||
line-height: 32px;
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
color: #5778ff;
|
||||
flex-shrink: 0;
|
||||
cursor: pointer;
|
||||
margin: 15px 30px 15px 30px;
|
||||
}
|
||||
|
||||
.input-box {
|
||||
|
||||
@@ -19,27 +19,40 @@
|
||||
<div class="hi-hint">
|
||||
Hello, Let's have a wonderful day!
|
||||
</div>
|
||||
<div class="add-device-btn" @click="showAddDialog">
|
||||
<div class="left-add">
|
||||
<div class="add-device-btn">
|
||||
<div class="left-add" @click="showAddDialog">
|
||||
添加智能体
|
||||
</div>
|
||||
<div style="width: 23px;height: 13px;background: #5778ff;margin-left: -10px;" />
|
||||
<div class="right-add">
|
||||
<i class="el-icon-right" style="font-size: 20px;color: #fff;" />
|
||||
<i class="el-icon-right" @click="showAddDialog" style="font-size: 20px;color: #fff;" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="device-list-container">
|
||||
<DeviceItem v-for="(item, index) in devices" :key="index" :device="item" @configure="goToRoleConfig"
|
||||
@deviceManage="handleDeviceManage" @delete="handleDeleteAgent" />
|
||||
<template v-if="isLoading">
|
||||
<div v-for="i in skeletonCount" :key="'skeleton-' + i" class="skeleton-item">
|
||||
<div class="skeleton-image"></div>
|
||||
<div class="skeleton-content">
|
||||
<div class="skeleton-line"></div>
|
||||
<div class="skeleton-line-short"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<DeviceItem v-for="(item, index) in devices" :key="index" :device="item" @configure="goToRoleConfig"
|
||||
@deviceManage="handleDeviceManage" @delete="handleDeleteAgent" @chat-history="handleShowChatHistory" />
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="copyright">
|
||||
©2025 xiaozhi-esp32-server
|
||||
</div>
|
||||
<AddWisdomBodyDialog :visible.sync="addDeviceDialogVisible" @confirm="handleWisdomBodyAdded" />
|
||||
</el-main>
|
||||
<el-footer>
|
||||
<version-footer />
|
||||
</el-footer>
|
||||
<chat-history-dialog :visible.sync="showChatHistory" :agent-id="currentAgentId" :agent-name="currentAgentName" />
|
||||
</div>
|
||||
|
||||
</template>
|
||||
@@ -47,19 +60,26 @@
|
||||
<script>
|
||||
import Api from '@/apis/api';
|
||||
import AddWisdomBodyDialog from '@/components/AddWisdomBodyDialog.vue';
|
||||
import ChatHistoryDialog from '@/components/ChatHistoryDialog.vue';
|
||||
import DeviceItem from '@/components/DeviceItem.vue';
|
||||
import HeaderBar from '@/components/HeaderBar.vue';
|
||||
import VersionFooter from '@/components/VersionFooter.vue';
|
||||
|
||||
export default {
|
||||
name: 'HomePage',
|
||||
components: { DeviceItem, AddWisdomBodyDialog, HeaderBar },
|
||||
components: { DeviceItem, AddWisdomBodyDialog, HeaderBar, VersionFooter, ChatHistoryDialog },
|
||||
data() {
|
||||
return {
|
||||
addDeviceDialogVisible: false,
|
||||
devices: [],
|
||||
originalDevices: [],
|
||||
isSearching: false,
|
||||
searchRegex: null
|
||||
searchRegex: null,
|
||||
isLoading: true,
|
||||
skeletonCount: localStorage.getItem('skeletonCount') || 8,
|
||||
showChatHistory: false,
|
||||
currentAgentId: '',
|
||||
currentAgentName: ''
|
||||
}
|
||||
},
|
||||
|
||||
@@ -108,12 +128,26 @@ export default {
|
||||
},
|
||||
// 获取智能体列表
|
||||
fetchAgentList() {
|
||||
this.isLoading = true;
|
||||
Api.agent.getAgentList(({ data }) => {
|
||||
this.originalDevices = data.data.map(item => ({
|
||||
...item,
|
||||
agentId: item.id // 字段映射
|
||||
}));
|
||||
this.handleSearchReset(); // 重置搜索状态
|
||||
if (data?.data) {
|
||||
this.originalDevices = data.data.map(item => ({
|
||||
...item,
|
||||
agentId: item.id
|
||||
}));
|
||||
|
||||
// 动态设置骨架屏数量(可选)
|
||||
this.skeletonCount = Math.min(
|
||||
Math.max(this.originalDevices.length, 3), // 最少3个
|
||||
10 // 最多10个
|
||||
);
|
||||
|
||||
this.handleSearchReset();
|
||||
}
|
||||
this.isLoading = false;
|
||||
}, (error) => {
|
||||
console.error('Failed to fetch agent list:', error);
|
||||
this.isLoading = false;
|
||||
});
|
||||
},
|
||||
// 删除智能体
|
||||
@@ -138,6 +172,11 @@ export default {
|
||||
}
|
||||
});
|
||||
}).catch(() => { });
|
||||
},
|
||||
handleShowChatHistory({ agentId, agentName }) {
|
||||
this.currentAgentId = agentId;
|
||||
this.currentAgentName = agentName;
|
||||
this.showChatHistory = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -198,7 +237,7 @@ export default {
|
||||
|
||||
.hi-hint {
|
||||
font-weight: 400;
|
||||
font-size: 10px;
|
||||
font-size: 12px;
|
||||
text-align: left;
|
||||
color: #818cae;
|
||||
margin-left: 75px;
|
||||
@@ -219,7 +258,7 @@ export default {
|
||||
border-radius: 17px;
|
||||
background: #5778ff;
|
||||
color: #fff;
|
||||
font-size: 10px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
line-height: 34px;
|
||||
@@ -260,4 +299,66 @@ export default {
|
||||
text-align: center;
|
||||
/* 居中显示 */
|
||||
}
|
||||
|
||||
/* 骨架屏动画 */
|
||||
@keyframes shimmer {
|
||||
100% {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
||||
|
||||
.skeleton-item {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
height: 120px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.skeleton-image {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
background: #f0f2f5;
|
||||
border-radius: 4px;
|
||||
float: left;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.skeleton-content {
|
||||
margin-left: 100px;
|
||||
}
|
||||
|
||||
.skeleton-line {
|
||||
height: 16px;
|
||||
background: #f0f2f5;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 12px;
|
||||
width: 70%;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.skeleton-line-short {
|
||||
height: 12px;
|
||||
background: #f0f2f5;
|
||||
border-radius: 4px;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.skeleton-item::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg,
|
||||
rgba(255, 255, 255, 0),
|
||||
rgba(255, 255, 255, 0.3),
|
||||
rgba(255, 255, 255, 0));
|
||||
animation: shimmer 1.5s infinite;
|
||||
}
|
||||
</style>
|
||||
@@ -20,10 +20,27 @@
|
||||
</div>
|
||||
</div>
|
||||
<div style="padding: 0 30px;">
|
||||
<div class="input-box">
|
||||
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/username.png" />
|
||||
<el-input v-model="form.username" placeholder="请输入用户名" />
|
||||
</div>
|
||||
<!-- 用户名登录 -->
|
||||
<template v-if="!isMobileLogin">
|
||||
<div class="input-box">
|
||||
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/username.png" />
|
||||
<el-input v-model="form.username" placeholder="请输入用户名" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 手机号登录 -->
|
||||
<template v-else>
|
||||
<div class="input-box">
|
||||
<div style="display: flex; align-items: center; width: 100%;">
|
||||
<el-select v-model="form.areaCode" style="width: 220px; margin-right: 10px;">
|
||||
<el-option v-for="item in mobileAreaList" :key="item.key" :label="`${item.name} (${item.key})`"
|
||||
:value="item.key" />
|
||||
</el-select>
|
||||
<el-input v-model="form.mobile" placeholder="请输入手机号码" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="input-box">
|
||||
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/password.png" />
|
||||
<el-input v-model="form.password" placeholder="请输入密码" type="password" />
|
||||
@@ -38,10 +55,24 @@
|
||||
</div>
|
||||
<div
|
||||
style="font-weight: 400;font-size: 14px;text-align: left;color: #5778ff;display: flex;justify-content: space-between;margin-top: 20px;">
|
||||
<div style="cursor: pointer;" @click="goToRegister">新用户注册</div>
|
||||
<div v-if="allowUserRegister" style="cursor: pointer;" @click="goToRegister">新用户注册</div>
|
||||
<div style="cursor: pointer;" @click="goToForgetPassword" v-if="enableMobileRegister">忘记密码?</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="login-btn" @click="login">登录</div>
|
||||
|
||||
<!-- 登录方式切换按钮 -->
|
||||
<div class="login-type-container" v-if="enableMobileRegister">
|
||||
<el-tooltip content="手机号码登录" placement="bottom">
|
||||
<el-button :type="isMobileLogin ? 'primary' : 'default'" icon="el-icon-mobile" circle
|
||||
@click="switchLoginType('mobile')"></el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="用户名登录" placement="bottom">
|
||||
<el-button :type="!isMobileLogin ? 'primary' : 'default'" icon="el-icon-user" circle
|
||||
@click="switchLoginType('username')"></el-button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
|
||||
<div style="font-size: 14px;color: #979db1;">
|
||||
登录即同意
|
||||
<div style="display: inline-block;color: #5778FF;cursor: pointer;">《用户协议》</div>
|
||||
@@ -51,9 +82,7 @@
|
||||
</div>
|
||||
</el-main>
|
||||
<el-footer>
|
||||
<div class="copyright">
|
||||
©2025 xiaozhi-esp32-server
|
||||
</div>
|
||||
<version-footer />
|
||||
</el-footer>
|
||||
</el-container>
|
||||
</div>
|
||||
@@ -61,11 +90,22 @@
|
||||
|
||||
<script>
|
||||
import Api from '@/apis/api';
|
||||
import { getUUID, goToPage, showDanger, showSuccess } from '@/utils';
|
||||
|
||||
import VersionFooter from '@/components/VersionFooter.vue';
|
||||
import { getUUID, goToPage, showDanger, showSuccess, validateMobile } from '@/utils';
|
||||
import { mapState } from 'vuex';
|
||||
|
||||
export default {
|
||||
name: 'login',
|
||||
components: {
|
||||
VersionFooter
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
allowUserRegister: state => state.pubConfig.allowUserRegister,
|
||||
enableMobileRegister: state => state.pubConfig.enableMobileRegister,
|
||||
mobileAreaList: state => state.pubConfig.mobileAreaList
|
||||
})
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
activeName: "username",
|
||||
@@ -73,14 +113,21 @@ export default {
|
||||
username: '',
|
||||
password: '',
|
||||
captcha: '',
|
||||
captchaId: ''
|
||||
captchaId: '',
|
||||
areaCode: '+86',
|
||||
mobile: ''
|
||||
},
|
||||
captchaUuid: '',
|
||||
captchaUrl: ''
|
||||
captchaUrl: '',
|
||||
isMobileLogin: false
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.fetchCaptcha();
|
||||
this.$store.dispatch('fetchPubConfig').then(() => {
|
||||
// 根据配置决定默认登录方式
|
||||
this.isMobileLogin = this.enableMobileRegister;
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
fetchCaptcha() {
|
||||
@@ -102,6 +149,17 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
// 切换登录方式
|
||||
switchLoginType(type) {
|
||||
this.isMobileLogin = type === 'mobile';
|
||||
// 清空表单
|
||||
this.form.username = '';
|
||||
this.form.mobile = '';
|
||||
this.form.password = '';
|
||||
this.form.captcha = '';
|
||||
this.fetchCaptcha();
|
||||
},
|
||||
|
||||
// 封装输入验证逻辑
|
||||
validateInput(input, message) {
|
||||
if (!input.trim()) {
|
||||
@@ -112,10 +170,21 @@ export default {
|
||||
},
|
||||
|
||||
async login() {
|
||||
// 验证用户名
|
||||
if (!this.validateInput(this.form.username, '用户名不能为空')) {
|
||||
return;
|
||||
if (this.isMobileLogin) {
|
||||
// 手机号登录验证
|
||||
if (!validateMobile(this.form.mobile, this.form.areaCode)) {
|
||||
showDanger('请输入正确的手机号码');
|
||||
return;
|
||||
}
|
||||
// 拼接手机号作为用户名
|
||||
this.form.username = this.form.areaCode + this.form.mobile;
|
||||
} else {
|
||||
// 用户名登录验证
|
||||
if (!this.validateInput(this.form.username, '用户名不能为空')) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 验证密码
|
||||
if (!this.validateInput(this.form.password, '密码不能为空')) {
|
||||
return;
|
||||
@@ -127,12 +196,13 @@ export default {
|
||||
|
||||
this.form.captchaId = this.captchaUuid
|
||||
Api.user.login(this.form, ({ data }) => {
|
||||
if (data.code === 0) {
|
||||
showSuccess('登录成功!');
|
||||
this.$store.commit('setToken', JSON.stringify(data.data));
|
||||
goToPage('/home');
|
||||
} else {
|
||||
showDanger(data.msg || '登录失败');
|
||||
showSuccess('登录成功!');
|
||||
this.$store.commit('setToken', JSON.stringify(data.data));
|
||||
goToPage('/home');
|
||||
}, (err) => {
|
||||
showDanger(err.data.msg || '登录失败')
|
||||
if (err.data != null && err.data.msg != null && err.data.msg.indexOf('图形验证码') > -1) {
|
||||
this.fetchCaptcha()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -144,9 +214,33 @@ export default {
|
||||
|
||||
goToRegister() {
|
||||
goToPage('/register')
|
||||
}
|
||||
},
|
||||
goToForgetPassword() {
|
||||
goToPage('/retrieve-password')
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import './auth.scss'; // 添加这行引用</style>
|
||||
@import './auth.scss';
|
||||
|
||||
.login-type-container {
|
||||
margin: 10px 20px;
|
||||
}
|
||||
|
||||
:deep(.el-button--primary) {
|
||||
background-color: #5778ff;
|
||||
border-color: #5778ff;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: #4a6ae8;
|
||||
border-color: #4a6ae8;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: #3d5cd6;
|
||||
border-color: #3d5cd6;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="welcome">
|
||||
<div class="welcome" @keyup.enter="register">
|
||||
<el-container style="height: 100%;">
|
||||
<!-- 保持相同的头部 -->
|
||||
<el-header>
|
||||
@@ -23,38 +23,78 @@
|
||||
</div>
|
||||
|
||||
<div style="padding: 0 30px;">
|
||||
<!-- 用户名输入框 -->
|
||||
<div class="input-box">
|
||||
<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 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 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 loading="lazy" alt="" class="input-icon" src="@/assets/login/shield.png" />
|
||||
<el-input v-model="form.captcha" placeholder="请输入验证码" style="flex: 1;" />
|
||||
<form @submit.prevent="register">
|
||||
<!-- 用户名/手机号输入框 -->
|
||||
<div class="input-box" v-if="!enableMobileRegister">
|
||||
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/username.png" />
|
||||
<el-input v-model="form.username" placeholder="请输入用户名" />
|
||||
</div>
|
||||
<img loading="lazy" v-if="captchaUrl" :src="captchaUrl" alt="验证码"
|
||||
style="width: 150px; height: 40px; cursor: pointer;" @click="fetchCaptcha" />
|
||||
</div>
|
||||
|
||||
<!-- 修改底部链接 -->
|
||||
<div style="font-weight: 400;font-size: 14px;text-align: left;color: #5778ff;margin-top: 20px;">
|
||||
<div style="cursor: pointer;" @click="goToLogin">已有账号?立即登录</div>
|
||||
</div>
|
||||
<!-- 手机号注册部分 -->
|
||||
<template v-if="enableMobileRegister">
|
||||
<div class="input-box">
|
||||
<div style="display: flex; align-items: center; width: 100%;">
|
||||
<el-select v-model="form.areaCode" style="width: 220px; margin-right: 10px;">
|
||||
<el-option v-for="item in mobileAreaList" :key="item.key" :label="`${item.name} (${item.key})`"
|
||||
:value="item.key" />
|
||||
</el-select>
|
||||
<el-input v-model="form.mobile" placeholder="请输入手机号码" />
|
||||
</div>
|
||||
</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 loading="lazy" alt="" class="input-icon" src="@/assets/login/shield.png" />
|
||||
<el-input v-model="form.captcha" placeholder="请输入验证码" style="flex: 1;" />
|
||||
</div>
|
||||
<img loading="lazy" v-if="captchaUrl" :src="captchaUrl" alt="验证码"
|
||||
style="width: 150px; height: 40px; cursor: pointer;" @click="fetchCaptcha" />
|
||||
</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 loading="lazy" alt="" class="input-icon" src="@/assets/login/phone.png" />
|
||||
<el-input v-model="form.mobileCaptcha" placeholder="请输入手机验证码" style="flex: 1;" maxlength="6" />
|
||||
</div>
|
||||
<el-button type="primary" class="send-captcha-btn" :disabled="!canSendMobileCaptcha"
|
||||
@click="sendMobileCaptcha">
|
||||
<span>
|
||||
{{ countdown > 0 ? `${countdown}秒后重试` : '发送验证码' }}
|
||||
</span>
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 密码输入框 -->
|
||||
<div class="input-box">
|
||||
<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 loading="lazy" alt="" class="input-icon" src="@/assets/login/password.png" />
|
||||
<el-input v-model="form.confirmPassword" placeholder="请确认密码" type="password" />
|
||||
</div>
|
||||
|
||||
<!-- 验证码部分保持相同 -->
|
||||
<div v-if="!enableMobileRegister"
|
||||
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 loading="lazy" alt="" class="input-icon" src="@/assets/login/shield.png" />
|
||||
<el-input v-model="form.captcha" placeholder="请输入验证码" style="flex: 1;" />
|
||||
</div>
|
||||
<img loading="lazy" v-if="captchaUrl" :src="captchaUrl" alt="验证码"
|
||||
style="width: 150px; height: 40px; cursor: pointer;" @click="fetchCaptcha" />
|
||||
</div>
|
||||
|
||||
<!-- 修改底部链接 -->
|
||||
<div style="font-weight: 400;font-size: 14px;text-align: left;color: #5778ff;margin-top: 20px;">
|
||||
<div style="cursor: pointer;" @click="goToLogin">已有账号?立即登录</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- 修改按钮文本 -->
|
||||
@@ -72,9 +112,7 @@
|
||||
|
||||
<!-- 保持相同的页脚 -->
|
||||
<el-footer>
|
||||
<div class="copyright">
|
||||
©2025 xiaozhi-esp32-server
|
||||
</div>
|
||||
<version-footer />
|
||||
</el-footer>
|
||||
</el-container>
|
||||
</div>
|
||||
@@ -82,10 +120,25 @@
|
||||
|
||||
<script>
|
||||
import Api from '@/apis/api';
|
||||
import { getUUID, goToPage, showDanger, showSuccess } from '@/utils';
|
||||
import VersionFooter from '@/components/VersionFooter.vue';
|
||||
import { getUUID, goToPage, showDanger, showSuccess, validateMobile } from '@/utils';
|
||||
import { mapState } from 'vuex';
|
||||
|
||||
export default {
|
||||
name: 'register',
|
||||
components: {
|
||||
VersionFooter
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
allowUserRegister: state => state.pubConfig.allowUserRegister,
|
||||
enableMobileRegister: state => state.pubConfig.enableMobileRegister,
|
||||
mobileAreaList: state => state.pubConfig.mobileAreaList
|
||||
}),
|
||||
canSendMobileCaptcha() {
|
||||
return this.countdown === 0 && validateMobile(this.form.mobile, this.form.areaCode);
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
form: {
|
||||
@@ -93,12 +146,25 @@ export default {
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
captcha: '',
|
||||
captchaId: ''
|
||||
captchaId: '',
|
||||
areaCode: '+86',
|
||||
mobile: '',
|
||||
mobileCaptcha: ''
|
||||
},
|
||||
captchaUrl: ''
|
||||
captchaUrl: '',
|
||||
countdown: 0,
|
||||
timer: null
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$store.dispatch('fetchPubConfig').then(() => {
|
||||
if (!this.allowUserRegister) {
|
||||
showDanger('当前不允许普通用户注册');
|
||||
setTimeout(() => {
|
||||
goToPage('/login');
|
||||
}, 1500);
|
||||
}
|
||||
});
|
||||
this.fetchCaptcha();
|
||||
},
|
||||
methods: {
|
||||
@@ -125,12 +191,70 @@ export default {
|
||||
}
|
||||
return true;
|
||||
},
|
||||
// 注册逻辑
|
||||
register() {
|
||||
// 验证用户名
|
||||
if (!this.validateInput(this.form.username, '用户名不能为空')) {
|
||||
|
||||
// 发送手机验证码
|
||||
sendMobileCaptcha() {
|
||||
if (!validateMobile(this.form.mobile, this.form.areaCode)) {
|
||||
showDanger('请输入正确的手机号码');
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证图形验证码
|
||||
if (!this.validateInput(this.form.captcha, '请输入图形验证码')) {
|
||||
this.fetchCaptcha();
|
||||
return;
|
||||
}
|
||||
|
||||
// 清除可能存在的旧定时器
|
||||
if (this.timer) {
|
||||
clearInterval(this.timer);
|
||||
this.timer = null;
|
||||
}
|
||||
|
||||
// 开始倒计时
|
||||
this.countdown = 60;
|
||||
this.timer = setInterval(() => {
|
||||
if (this.countdown > 0) {
|
||||
this.countdown--;
|
||||
} else {
|
||||
clearInterval(this.timer);
|
||||
this.timer = null;
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
// 调用发送验证码接口
|
||||
Api.user.sendSmsVerification({
|
||||
phone: this.form.areaCode + this.form.mobile,
|
||||
captcha: this.form.captcha,
|
||||
captchaId: this.form.captchaId
|
||||
}, (res) => {
|
||||
showSuccess('验证码发送成功');
|
||||
}, (err) => {
|
||||
showDanger(err.data.msg || '验证码发送失败');
|
||||
this.countdown = 0;
|
||||
this.fetchCaptcha();
|
||||
});
|
||||
},
|
||||
|
||||
// 注册逻辑
|
||||
register() {
|
||||
if (this.enableMobileRegister) {
|
||||
// 手机号注册验证
|
||||
if (!validateMobile(this.form.mobile, this.form.areaCode)) {
|
||||
showDanger('请输入正确的手机号码');
|
||||
return;
|
||||
}
|
||||
if (!this.form.mobileCaptcha) {
|
||||
showDanger('请输入手机验证码');
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// 用户名注册验证
|
||||
if (!this.validateInput(this.form.username, '用户名不能为空')) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 验证密码
|
||||
if (!this.validateInput(this.form.password, '密码不能为空')) {
|
||||
return;
|
||||
@@ -144,26 +268,50 @@ export default {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.enableMobileRegister) {
|
||||
this.form.username = this.form.areaCode + this.form.mobile
|
||||
}
|
||||
|
||||
Api.user.register(this.form, ({ data }) => {
|
||||
if (data.code === 0) {
|
||||
showSuccess('注册成功!')
|
||||
goToPage('/login')
|
||||
} else {
|
||||
showDanger(data.msg || '注册失败')
|
||||
showSuccess('注册成功!')
|
||||
goToPage('/login')
|
||||
}, (err) => {
|
||||
showDanger(err.data.msg || '注册失败')
|
||||
if (err.data != null && err.data.msg != null && err.data.msg.indexOf('图形验证码') > -1) {
|
||||
this.fetchCaptcha()
|
||||
}
|
||||
})
|
||||
setTimeout(() => {
|
||||
this.fetchCaptcha()
|
||||
}, 1000)
|
||||
},
|
||||
|
||||
goToLogin() {
|
||||
goToPage('/login')
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.timer) {
|
||||
clearInterval(this.timer);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import './auth.scss'; // 修改为导入新建的SCSS文件</style>
|
||||
@import './auth.scss';
|
||||
|
||||
.send-captcha-btn {
|
||||
margin-right: -5px;
|
||||
min-width: 100px;
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
background: rgb(87, 120, 255);
|
||||
border: none;
|
||||
padding: 0px;
|
||||
|
||||
&:disabled {
|
||||
background: #c0c4cc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,275 @@
|
||||
<template>
|
||||
<div class="welcome" @keyup.enter="retrievePassword">
|
||||
<el-container style="height: 100%;">
|
||||
<!-- 保持相同的头部 -->
|
||||
<el-header>
|
||||
<div style="display: flex;align-items: center;margin-top: 15px;margin-left: 10px;gap: 10px;">
|
||||
<img loading="lazy" alt="" src="@/assets/xiaozhi-logo.png" style="width: 45px;height: 45px;" />
|
||||
<img loading="lazy" alt="" src="@/assets/xiaozhi-ai.png" style="height: 18px;" />
|
||||
</div>
|
||||
</el-header>
|
||||
<div class="login-person">
|
||||
<img loading="lazy" alt="" src="@/assets/login/register-person.png" style="width: 100%;" />
|
||||
</div>
|
||||
<el-main style="position: relative;">
|
||||
<form @submit.prevent="retrievePassword">
|
||||
<div class="login-box">
|
||||
<!-- 修改标题部分 -->
|
||||
<div style="display: flex;align-items: center;gap: 20px;margin-bottom: 39px;padding: 0 30px;">
|
||||
<img loading="lazy" alt="" src="@/assets/login/hi.png" style="width: 34px;height: 34px;" />
|
||||
<div class="login-text">重置密码</div>
|
||||
<div class="login-welcome">
|
||||
PASSWORD RETRIEVE
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="padding: 0 30px;">
|
||||
<!-- 手机号输入 -->
|
||||
<div class="input-box">
|
||||
<div style="display: flex; align-items: center; width: 100%;">
|
||||
<el-select v-model="form.areaCode" style="width: 220px; margin-right: 10px;">
|
||||
<el-option v-for="item in mobileAreaList" :key="item.key" :label="`${item.name} (${item.key})`"
|
||||
:value="item.key" />
|
||||
</el-select>
|
||||
<el-input v-model="form.mobile" placeholder="请输入手机号码" />
|
||||
</div>
|
||||
</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 loading="lazy" alt="" class="input-icon" src="@/assets/login/shield.png" />
|
||||
<el-input v-model="form.captcha" placeholder="请输入验证码" style="flex: 1;" />
|
||||
</div>
|
||||
<img loading="lazy" v-if="captchaUrl" :src="captchaUrl" alt="验证码"
|
||||
style="width: 150px; height: 40px; cursor: pointer;" @click="fetchCaptcha" />
|
||||
</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 loading="lazy" alt="" class="input-icon" src="@/assets/login/phone.png" />
|
||||
<el-input v-model="form.mobileCaptcha" placeholder="请输入手机验证码" style="flex: 1;" maxlength="6" />
|
||||
</div>
|
||||
<el-button type="primary" class="send-captcha-btn" :disabled="!canSendMobileCaptcha"
|
||||
@click="sendMobileCaptcha">
|
||||
<span>
|
||||
{{ countdown > 0 ? `${countdown}秒后重试` : '发送验证码' }}
|
||||
</span>
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 新密码 -->
|
||||
<div class="input-box">
|
||||
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/password.png" />
|
||||
<el-input v-model="form.newPassword" placeholder="请输入新密码" type="password" />
|
||||
</div>
|
||||
|
||||
<!-- 确认新密码 -->
|
||||
<div class="input-box">
|
||||
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/password.png" />
|
||||
<el-input v-model="form.confirmPassword" placeholder="请确认新密码" type="password" />
|
||||
</div>
|
||||
|
||||
<!-- 修改底部链接 -->
|
||||
<div style="font-weight: 400;font-size: 14px;text-align: left;color: #5778ff;margin-top: 20px;">
|
||||
<div style="cursor: pointer;" @click="goToLogin">返回登录</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 修改按钮文本 -->
|
||||
<div class="login-btn" @click="retrievePassword">立即修改</div>
|
||||
|
||||
<!-- 保持相同的协议声明 -->
|
||||
<div style="font-size: 14px;color: #979db1;">
|
||||
同意
|
||||
<div style="display: inline-block;color: #5778FF;cursor: pointer;">《用户协议》</div>
|
||||
和
|
||||
<div style="display: inline-block;color: #5778FF;cursor: pointer;">《隐私政策》</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</el-main>
|
||||
|
||||
<!-- 保持相同的页脚 -->
|
||||
<el-footer>
|
||||
<version-footer />
|
||||
</el-footer>
|
||||
</el-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Api from '@/apis/api';
|
||||
import VersionFooter from '@/components/VersionFooter.vue';
|
||||
import { getUUID, goToPage, showDanger, showSuccess, validateMobile } from '@/utils';
|
||||
import { mapState } from 'vuex';
|
||||
|
||||
export default {
|
||||
name: 'retrieve',
|
||||
components: {
|
||||
VersionFooter
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
allowUserRegister: state => state.pubConfig.allowUserRegister,
|
||||
mobileAreaList: state => state.pubConfig.mobileAreaList
|
||||
}),
|
||||
canSendMobileCaptcha() {
|
||||
return this.countdown === 0 && validateMobile(this.form.mobile, this.form.areaCode);
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
form: {
|
||||
areaCode: '+86',
|
||||
mobile: '',
|
||||
captcha: '',
|
||||
captchaId: '',
|
||||
smsCode: '',
|
||||
newPassword: '',
|
||||
confirmPassword: ''
|
||||
},
|
||||
captchaUrl: '',
|
||||
countdown: 0,
|
||||
timer: null
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.fetchCaptcha();
|
||||
},
|
||||
methods: {
|
||||
// 复用验证码获取方法
|
||||
fetchCaptcha() {
|
||||
this.form.captchaId = getUUID();
|
||||
Api.user.getCaptcha(this.form.captchaId, (res) => {
|
||||
if (res.status === 200) {
|
||||
const blob = new Blob([res.data], { type: res.data.type });
|
||||
this.captchaUrl = URL.createObjectURL(blob);
|
||||
|
||||
} else {
|
||||
console.error('验证码加载异常:', error);
|
||||
showDanger('验证码加载失败,点击刷新');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 封装输入验证逻辑
|
||||
validateInput(input, message) {
|
||||
if (!input.trim()) {
|
||||
showDanger(message);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
// 发送手机验证码
|
||||
sendMobileCaptcha() {
|
||||
if (!validateMobile(this.form.mobile, this.form.areaCode)) {
|
||||
showDanger('请输入正确的手机号码');
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证图形验证码
|
||||
if (!this.validateInput(this.form.captcha, '请输入图形验证码')) {
|
||||
this.fetchCaptcha();
|
||||
return;
|
||||
}
|
||||
|
||||
// 清除可能存在的旧定时器
|
||||
if (this.timer) {
|
||||
clearInterval(this.timer);
|
||||
this.timer = null;
|
||||
}
|
||||
|
||||
// 开始倒计时
|
||||
this.countdown = 60;
|
||||
this.timer = setInterval(() => {
|
||||
if (this.countdown > 0) {
|
||||
this.countdown--;
|
||||
} else {
|
||||
clearInterval(this.timer);
|
||||
this.timer = null;
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
// 调用发送验证码接口
|
||||
Api.user.sendSmsVerification({
|
||||
phone: this.form.areaCode + this.form.mobile,
|
||||
captcha: this.form.captcha,
|
||||
captchaId: this.form.captchaId
|
||||
}, (res) => {
|
||||
showSuccess('验证码发送成功');
|
||||
}, (err) => {
|
||||
showDanger(err.data.msg || '验证码发送失败');
|
||||
this.countdown = 0;
|
||||
this.fetchCaptcha();
|
||||
});
|
||||
},
|
||||
|
||||
// 修改逻辑
|
||||
retrievePassword() {
|
||||
// 验证逻辑
|
||||
if (!validateMobile(this.form.mobile, this.form.areaCode)) {
|
||||
showDanger('请输入正确的手机号码');
|
||||
return;
|
||||
}
|
||||
if (!this.form.captcha) {
|
||||
showDanger('请输入图形验证码');
|
||||
return;
|
||||
}
|
||||
if (!this.form.mobileCaptcha) {
|
||||
showDanger('请输入短信验证码');
|
||||
return;
|
||||
}
|
||||
if (this.form.newPassword !== this.form.confirmPassword) {
|
||||
showDanger('两次输入的密码不一致');
|
||||
return;
|
||||
}
|
||||
|
||||
Api.user.retrievePassword({
|
||||
phone: this.form.areaCode + this.form.mobile,
|
||||
password: this.form.newPassword,
|
||||
code: this.form.mobileCaptcha
|
||||
}, (res) => {
|
||||
showSuccess('密码重置成功');
|
||||
goToPage('/login');
|
||||
}, (err) => {
|
||||
showDanger(err.data.msg || '重置失败');
|
||||
if (err.data != null && err.data.msg != null && err.data.msg.indexOf('图形验证码') > -1) {
|
||||
this.fetchCaptcha()
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
goToLogin() {
|
||||
goToPage('/login')
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.timer) {
|
||||
clearInterval(this.timer);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import './auth.scss';
|
||||
|
||||
.send-captcha-btn {
|
||||
margin-right: -5px;
|
||||
min-width: 100px;
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
background: rgb(87, 120, 255);
|
||||
border: none;
|
||||
padding: 0;
|
||||
|
||||
&:disabled {
|
||||
background: #c0c4cc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,108 +1,145 @@
|
||||
<template>
|
||||
<div class="welcome">
|
||||
<HeaderBar />
|
||||
<el-main style="padding: 16px;display: flex;flex-direction: column;">
|
||||
<div style="border-radius: 16px;background: #fafcfe; border: 1px solid #e8f0ff;">
|
||||
<div
|
||||
style="padding: 15px 24px;font-weight: 700;font-size: 19px;text-align: left;color: #3d4566;display: flex;gap: 13px;align-items: center;">
|
||||
<div
|
||||
style="width: 37px;height: 37px;background: #5778ff;border-radius: 50%;display: flex;align-items: center;justify-content: center;">
|
||||
<img loading="lazy" src="@/assets/home/setting-user.png" alt="" style="width: 19px;height: 19px;" />
|
||||
</div>
|
||||
{{ form.agentName }}
|
||||
</div>
|
||||
<div style="height: 1px;background: #e8f0ff;" />
|
||||
<el-form ref="form" :model="form" label-width="72px">
|
||||
<div style="padding: 16px 24px;">
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 24px;">
|
||||
<div>
|
||||
<el-form-item label="助手昵称:">
|
||||
<div class="input-46" style="width: 100%;">
|
||||
<el-input v-model="form.agentName" />
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="角色模版:">
|
||||
<div style="display: flex;gap: 8px;flex-wrap: wrap;">
|
||||
<div v-for="(template, index) in templates" :key="`template-${index}`" class="template-item"
|
||||
:class="{ 'template-loading': loadingTemplate }" @click="selectTemplate(template)">
|
||||
{{ template.agentName }}
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="角色介绍:">
|
||||
<div class="textarea-box">
|
||||
<el-input type="textarea" rows="5" resize="none" placeholder="请输入内容" v-model="form.systemPrompt"
|
||||
maxlength="2000" show-word-limit />
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="语言编码:">
|
||||
<div class="input-46" style="width: 100%;">
|
||||
<el-input v-model="form.langCode" placeholder="请输入语言编码,如:zh_CN" maxlength="10" show-word-limit />
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="交互语种:">
|
||||
<div class="input-46" style="width: 100%;">
|
||||
<el-input v-model="form.language" placeholder="请输入交互语种,如:中文" maxlength="10" show-word-limit />
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<div class="operation-bar">
|
||||
<h2 class="page-title">角色配置</h2>
|
||||
</div>
|
||||
|
||||
<div class="main-wrapper">
|
||||
<div class="content-panel">
|
||||
<div class="content-area">
|
||||
<el-card class="config-card" shadow="never">
|
||||
<div class="config-header">
|
||||
<div class="header-icon">
|
||||
<img loading="lazy" src="@/assets/home/setting-user.png" alt="">
|
||||
</div>
|
||||
<div>
|
||||
<el-form-item v-for="(model, index) in models" :key="`model-${index}`" :label="model.label"
|
||||
class="model-item">
|
||||
<el-select v-model="form.model[model.key]" filterable placeholder="请选择" class="select-field">
|
||||
<el-option v-for="(item, optionIndex) in modelOptions[model.type]"
|
||||
:key="`option-${index}-${optionIndex}`" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="角色音色:">
|
||||
<div style="display: flex;gap: 8px;align-items: center;">
|
||||
<div class="input-46" style="width: 100%;">
|
||||
<el-select v-model="form.ttsVoiceId" placeholder="请选择" style="width: 100%;">
|
||||
<span class="header-title">{{ form.agentName }}</span>
|
||||
<div class="header-actions">
|
||||
<div class="hint-text">
|
||||
<img loading="lazy" src="@/assets/home/info.png" alt="">
|
||||
<span>保存配置后,需要重启设备,新的配置才会生效。</span>
|
||||
</div>
|
||||
<el-button type="primary" class="save-btn" @click="saveConfig">保存配置</el-button>
|
||||
<el-button class="reset-btn" @click="resetConfig">重置</el-button>
|
||||
<button class="custom-close-btn" @click="goToHome">
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="divider"></div>
|
||||
|
||||
<el-form ref="form" :model="form" label-width="72px">
|
||||
<div class="form-content">
|
||||
<div class="form-grid">
|
||||
<div class="form-column">
|
||||
<el-form-item label="助手昵称:">
|
||||
<el-input v-model="form.agentName" class="form-input" maxlength="10" />
|
||||
</el-form-item>
|
||||
<el-form-item label="角色模版:">
|
||||
<div class="template-container">
|
||||
<div v-for="(template, index) in templates" :key="`template-${index}`" class="template-item"
|
||||
:class="{ 'template-loading': loadingTemplate }" @click="selectTemplate(template)">
|
||||
{{ template.agentName }}
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="角色介绍:">
|
||||
<el-input type="textarea" rows="9" resize="none" placeholder="请输入内容" v-model="form.systemPrompt"
|
||||
maxlength="2000" show-word-limit class="form-textarea" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="记忆:">
|
||||
<el-input type="textarea" rows="6" resize="none" v-model="form.summaryMemory" maxlength="2000"
|
||||
show-word-limit class="form-textarea"
|
||||
:disabled="form.model.memModelId !== 'Memory_mem_local_short'" />
|
||||
</el-form-item>
|
||||
<el-form-item label="语言编码:" style="display: none;">
|
||||
<el-input v-model="form.langCode" placeholder="请输入语言编码,如:zh_CN" maxlength="10" show-word-limit
|
||||
class="form-input" />
|
||||
</el-form-item>
|
||||
<el-form-item label="交互语种:" style="display: none;">
|
||||
<el-input v-model="form.language" placeholder="请输入交互语种,如:中文" maxlength="10" show-word-limit
|
||||
class="form-input" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="form-column">
|
||||
<el-form-item v-for="(model, index) in models" :key="`model-${index}`" :label="model.label"
|
||||
class="model-item">
|
||||
<div class="model-select-wrapper">
|
||||
<el-select v-model="form.model[model.key]" filterable placeholder="请选择" class="form-select"
|
||||
@change="handleModelChange(model.type, $event)">
|
||||
<el-option v-for="(item, optionIndex) in modelOptions[model.type]"
|
||||
:key="`option-${index}-${optionIndex}`" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
<div v-if="showFunctionIcons(model.type)" class="function-icons">
|
||||
<el-tooltip v-for="func in currentFunctions" :key="func.name" effect="dark" placement="top"
|
||||
popper-class="custom-tooltip">
|
||||
<div slot="content">
|
||||
<div><strong>功能名称:</strong> {{ func.name }}</div>
|
||||
<div v-if="Object.keys(func.params).length > 0">
|
||||
<strong>参数配置:</strong>
|
||||
<div v-for="(value, key) in func.params" :key="key">
|
||||
{{ key }}: {{ value }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>无参数配置</div>
|
||||
</div>
|
||||
<div class="icon-dot" :style="{ backgroundColor: getFunctionColor(func.name) }">
|
||||
{{ func.name.charAt(0) }}
|
||||
</div>
|
||||
</el-tooltip>
|
||||
<el-button class="edit-function-btn" @click="showFunctionDialog = true"
|
||||
:class="{ 'active-btn': showFunctionDialog }">
|
||||
编辑功能
|
||||
</el-button>
|
||||
</div>
|
||||
<div v-if="model.type === 'Memory' && form.model.memModelId !== 'Memory_nomem'"
|
||||
class="chat-history-options">
|
||||
<el-radio-group v-model="form.chatHistoryConf" @change="updateChatHistoryConf">
|
||||
<el-radio-button :label="1">上报文字</el-radio-button>
|
||||
<el-radio-button :label="2">上报文字+语音</el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="角色音色">
|
||||
<el-select v-model="form.ttsVoiceId" placeholder="请选择" class="form-select">
|
||||
<el-option v-for="(item, index) in voiceOptions" :key="`voice-${index}`" :label="item.label"
|
||||
:value="item.value" />
|
||||
</el-select>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
<div style="display: flex;padding: 16px;gap: 8px;align-items: center;">
|
||||
<div class="save-btn" @click="saveConfig">
|
||||
保存配置
|
||||
</div>
|
||||
<div class="reset-btn" @click="resetConfig">
|
||||
重制
|
||||
</div>
|
||||
<div class="clear-text">
|
||||
<img loading="lazy" src="@/assets/home/red-info.png" alt="" style="width: 19px;height: 19px;" />
|
||||
保存配置后,需要重启设备,新的配置才会生效。
|
||||
</div>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
<div class="copyright">
|
||||
©2025 xiaozhi-esp32-server
|
||||
</div>
|
||||
</el-main>
|
||||
</div>
|
||||
|
||||
<function-dialog v-model="showFunctionDialog" :functions="currentFunctions"
|
||||
@update-functions="handleUpdateFunctions" @dialog-closed="handleDialogClosed" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Api from '@/apis/api';
|
||||
import FunctionDialog from "@/components/FunctionDialog.vue";
|
||||
import HeaderBar from "@/components/HeaderBar.vue";
|
||||
|
||||
|
||||
export default {
|
||||
name: 'RoleConfigPage',
|
||||
components: { HeaderBar },
|
||||
components: { HeaderBar, FunctionDialog },
|
||||
data() {
|
||||
return {
|
||||
form: {
|
||||
agentCode: "",
|
||||
agentName: "",
|
||||
ttsVoiceId: "",
|
||||
chatHistoryConf: 0,
|
||||
systemPrompt: "",
|
||||
summaryMemory: "",
|
||||
langCode: "",
|
||||
language: "",
|
||||
sort: "",
|
||||
@@ -127,9 +164,24 @@ export default {
|
||||
templates: [],
|
||||
loadingTemplate: false,
|
||||
voiceOptions: [],
|
||||
showFunctionDialog: false,
|
||||
currentFunctions: [],
|
||||
functionColorMap: [
|
||||
'#FF6B6B', '#4ECDC4', '#45B7D1',
|
||||
'#96CEB4', '#FFEEAD', '#D4A5A5', '#A2836E'
|
||||
],
|
||||
allFunctions: [
|
||||
{ name: '天气', params: {} },
|
||||
{ name: '新闻', params: {} },
|
||||
{ name: '工具', params: {} },
|
||||
{ name: '退出', params: {} }
|
||||
],
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
goToHome() {
|
||||
this.$router.push('/home');
|
||||
},
|
||||
saveConfig() {
|
||||
const configData = {
|
||||
agentCode: this.form.agentCode,
|
||||
@@ -139,12 +191,15 @@ export default {
|
||||
llmModelId: this.form.model.llmModelId,
|
||||
ttsModelId: this.form.model.ttsModelId,
|
||||
ttsVoiceId: this.form.ttsVoiceId,
|
||||
chatHistoryConf: this.form.chatHistoryConf,
|
||||
memModelId: this.form.model.memModelId,
|
||||
intentModelId: this.form.model.intentModelId,
|
||||
systemPrompt: this.form.systemPrompt,
|
||||
summaryMemory: this.form.summaryMemory,
|
||||
langCode: this.form.langCode,
|
||||
language: this.form.language,
|
||||
sort: this.form.sort
|
||||
sort: this.form.sort,
|
||||
functions: this.currentFunctions
|
||||
};
|
||||
Api.agent.updateAgentConfig(this.$route.query.agentId, configData, ({ data }) => {
|
||||
if (data.code === 0) {
|
||||
@@ -166,12 +221,13 @@ export default {
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
// 重置表单
|
||||
this.form = {
|
||||
agentCode: "",
|
||||
agentName: "",
|
||||
ttsVoiceId: "",
|
||||
chatHistoryConf: 0,
|
||||
systemPrompt: "",
|
||||
summaryMemory: "",
|
||||
langCode: "",
|
||||
language: "",
|
||||
sort: "",
|
||||
@@ -184,12 +240,12 @@ export default {
|
||||
intentModelId: "",
|
||||
}
|
||||
}
|
||||
this.currentFunctions = [];
|
||||
this.$message.success({
|
||||
message: '配置已重置',
|
||||
showClose: true
|
||||
})
|
||||
}).catch(() => {
|
||||
})
|
||||
}).catch(() => { });
|
||||
},
|
||||
fetchTemplates() {
|
||||
Api.agent.getAgentTemplate(({ data }) => {
|
||||
@@ -224,7 +280,9 @@ export default {
|
||||
...this.form,
|
||||
agentName: templateData.agentName || this.form.agentName,
|
||||
ttsVoiceId: templateData.ttsVoiceId || this.form.ttsVoiceId,
|
||||
chatHistoryConf: templateData.chatHistoryConf || this.form.chatHistoryConf,
|
||||
systemPrompt: templateData.systemPrompt || this.form.systemPrompt,
|
||||
summaryMemory: templateData.summaryMemory || this.form.summaryMemory,
|
||||
langCode: templateData.langCode || this.form.langCode,
|
||||
model: {
|
||||
ttsModelId: templateData.ttsModelId || this.form.model.ttsModelId,
|
||||
@@ -251,13 +309,13 @@ export default {
|
||||
intentModelId: data.data.intentModelId
|
||||
}
|
||||
};
|
||||
this.currentFunctions = data.data.functions || [];
|
||||
} else {
|
||||
this.$message.error(data.msg || '获取配置失败');
|
||||
}
|
||||
});
|
||||
},
|
||||
fetchModelOptions() {
|
||||
// 为每个模型类型获取选项
|
||||
this.models.forEach(model => {
|
||||
Api.model.getModelNames(model.type, '', ({ data }) => {
|
||||
if (data.code === 0) {
|
||||
@@ -286,12 +344,60 @@ export default {
|
||||
this.voiceOptions = [];
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
getFunctionColor(name) {
|
||||
const hash = [...name].reduce((acc, char) => acc + char.charCodeAt(0), 0);
|
||||
return this.functionColorMap[hash % 7];
|
||||
},
|
||||
showFunctionIcons(type) {
|
||||
// TODO 暂时不放出来
|
||||
return false;
|
||||
// return type === 'Intent' &&
|
||||
// this.form.model.intentModelId !== 'Intent_nointent';
|
||||
},
|
||||
handleModelChange(type, value) {
|
||||
if (type === 'Intent' && value !== 'Intent_nointent') {
|
||||
this.fetchFunctionList();
|
||||
}
|
||||
if (type === 'Memory' && value === 'Memory_nomem') {
|
||||
this.form.chatHistoryConf = 0;
|
||||
}
|
||||
if (type === 'Memory' && value !== 'Memory_nomem' && (this.form.chatHistoryConf === 0 || this.form.chatHistoryConf === null)) {
|
||||
this.form.chatHistoryConf = 2;
|
||||
}
|
||||
},
|
||||
fetchFunctionList() {
|
||||
// 使用假数据代替API调用
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
this.currentFunctions = [
|
||||
{ name: '天气', params: { city: '北京' } },
|
||||
{ name: '新闻', params: { type: '科技' } }
|
||||
];
|
||||
resolve();
|
||||
}, 500);
|
||||
});
|
||||
},
|
||||
handleUpdateFunctions(selected) {
|
||||
this.currentFunctions = selected;
|
||||
console.log('保存的功能列表:', selected);
|
||||
this.$message.success('功能配置已保存');
|
||||
},
|
||||
handleDialogClosed(saved) {
|
||||
if (!saved) {
|
||||
// 如果未保存,恢复原始功能列表
|
||||
this.currentFunctions = JSON.parse(JSON.stringify(this.originalFunctions));
|
||||
}
|
||||
},
|
||||
updateChatHistoryConf() {
|
||||
if (this.form.model.memModelId === 'Memory_nomem') {
|
||||
this.form.chatHistoryConf = 0;
|
||||
}
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
'form.model.ttsModelId': {
|
||||
handler(newVal, oldVal) {
|
||||
console.log('TTS模型变化:', newVal);
|
||||
if (oldVal && newVal !== oldVal) {
|
||||
this.form.ttsVoiceId = '';
|
||||
this.fetchVoiceOptions(newVal);
|
||||
@@ -314,6 +420,9 @@ export default {
|
||||
const agentId = this.$route.query.agentId;
|
||||
if (agentId) {
|
||||
this.fetchAgentConfig(agentId);
|
||||
this.fetchFunctionList().then(() => {
|
||||
this.originalFunctions = JSON.parse(JSON.stringify(this.currentFunctions));
|
||||
});
|
||||
}
|
||||
this.fetchModelOptions();
|
||||
this.fetchTemplates();
|
||||
@@ -324,71 +433,142 @@ export default {
|
||||
<style scoped>
|
||||
.welcome {
|
||||
min-width: 900px;
|
||||
min-height: 506px;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
position: relative;
|
||||
flex-direction: column;
|
||||
background: linear-gradient(145deg, #e6eeff, #eff0ff);
|
||||
background: linear-gradient(to bottom right, #dce8ff, #e4eeff, #e6cbfd);
|
||||
background-size: cover;
|
||||
/* 确保背景图像覆盖整个元素 */
|
||||
background-position: center;
|
||||
/* 从顶部中心对齐 */
|
||||
-webkit-background-size: cover;
|
||||
/* 兼容老版本WebKit浏览器 */
|
||||
-o-background-size: cover;
|
||||
/* 兼容老版本Opera浏览器 */
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.el-form-item ::v-deep .el-form-item__label {
|
||||
font-size: 10px !important;
|
||||
color: #3d4566 !important;
|
||||
font-weight: 400;
|
||||
line-height: 22px;
|
||||
padding-bottom: 2px;
|
||||
.operation-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1.5vh 24px;
|
||||
}
|
||||
|
||||
.select-field {
|
||||
width: 100%;
|
||||
max-width: 720px;
|
||||
border: 1px solid #e4e6ef;
|
||||
background: #f6f8fb;
|
||||
border-radius: 8px;
|
||||
height: 36px !important;
|
||||
.page-title {
|
||||
font-size: 24px;
|
||||
margin: 0;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.audio-box {
|
||||
.main-wrapper {
|
||||
margin: 1vh 22px;
|
||||
border-radius: 15px;
|
||||
height: calc(100vh - 24vh);
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||
position: relative;
|
||||
background: rgba(237, 242, 255, 0.5);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.content-panel {
|
||||
flex: 1;
|
||||
height: 37px;
|
||||
border-radius: 20px;
|
||||
border: 1px solid #e4e6ef;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
border-radius: 15px;
|
||||
background: transparent;
|
||||
border: 1px solid #fff;
|
||||
}
|
||||
|
||||
.clear-btn {
|
||||
width: 48px;
|
||||
height: 19px;
|
||||
background: #fd8383;
|
||||
border-radius: 10px;
|
||||
line-height: 19px;
|
||||
font-size: 11px;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
.content-area {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
min-width: 600px;
|
||||
overflow: auto;
|
||||
background-color: white;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.clear-text {
|
||||
color: #979db1;
|
||||
font-size: 11px;
|
||||
.config-card {
|
||||
background: white;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.config-header {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 13px;
|
||||
padding: 0 0 5px 0;
|
||||
font-weight: 700;
|
||||
font-size: 19px;
|
||||
color: #3d4566;
|
||||
}
|
||||
|
||||
.header-icon {
|
||||
width: 37px;
|
||||
height: 37px;
|
||||
background: #5778ff;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.header-icon img {
|
||||
width: 19px;
|
||||
height: 19px;
|
||||
}
|
||||
|
||||
.divider {
|
||||
height: 1px;
|
||||
background: #e8f0ff;
|
||||
}
|
||||
|
||||
.form-content {
|
||||
padding: 2vh 0;
|
||||
}
|
||||
|
||||
.form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.form-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.form-select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.form-textarea {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.template-container {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-left: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.template-item {
|
||||
height: 37px;
|
||||
height: 4vh;
|
||||
width: 76px;
|
||||
border-radius: 8px;
|
||||
background: #e6ebff;
|
||||
line-height: 37px;
|
||||
line-height: 4vh;
|
||||
font-weight: 400;
|
||||
font-size: 11px;
|
||||
text-align: center;
|
||||
@@ -401,47 +581,142 @@ export default {
|
||||
background-color: #d0d8ff;
|
||||
}
|
||||
|
||||
.prompt-bottom {
|
||||
margin-bottom: 4px;
|
||||
.model-select-wrapper {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0 16px;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.input-46 {
|
||||
border: 1px solid #e4e6ef;
|
||||
background: #f6f8fb;
|
||||
border-radius: 8px;
|
||||
height: 36px !important;
|
||||
.function-icons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: auto;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.save-btn,
|
||||
.reset-btn {
|
||||
width: 112px;
|
||||
height: 37px;
|
||||
border-radius: 18px;
|
||||
line-height: 37px;
|
||||
box-sizing: border-box;
|
||||
.icon-dot {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
border-radius: 50%;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
margin-right: 8px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
::v-deep .el-form-item__label {
|
||||
font-size: 12px !important;
|
||||
color: #3d4566 !important;
|
||||
font-weight: 400;
|
||||
line-height: 22px;
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
|
||||
::v-deep .el-textarea .el-input__count {
|
||||
color: #909399;
|
||||
background: none;
|
||||
position: absolute;
|
||||
font-size: 12px;
|
||||
right: 3%;
|
||||
}
|
||||
|
||||
.custom-close-btn {
|
||||
position: absolute;
|
||||
top: 25%;
|
||||
right: 0;
|
||||
transform: translateY(-50%);
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid #cfcfcf;
|
||||
background: none;
|
||||
font-size: 30px;
|
||||
font-weight: lighter;
|
||||
color: #cfcfcf;
|
||||
cursor: pointer;
|
||||
font-size: 11px
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1;
|
||||
padding: 0;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.save-btn {
|
||||
border-radius: 18px;
|
||||
background: #5778ff;
|
||||
color: #fff;
|
||||
.custom-close-btn:hover {
|
||||
color: #409EFF;
|
||||
border-color: #409EFF;
|
||||
}
|
||||
|
||||
.reset-btn {
|
||||
border: 1px solid #adbdff;
|
||||
.edit-function-btn {
|
||||
background: #e6ebff;
|
||||
color: #5778ff;
|
||||
border: 1px solid #adbdff;
|
||||
border-radius: 18px;
|
||||
padding: 10px 20px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.textarea-box {
|
||||
border: 1px solid #e4e6ef;
|
||||
border-radius: 8px;
|
||||
background: #f6f8fb;
|
||||
.edit-function-btn.active-btn {
|
||||
background: #5778ff;
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
|
||||
.chat-history-options {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
min-width: 250px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.header-actions .hint-text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
color: #979db1;
|
||||
font-size: 12px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.header-actions .hint-text img {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.header-actions .save-btn {
|
||||
background: #5778ff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 18px;
|
||||
padding: 8px 16px;
|
||||
height: 32px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.header-actions .reset-btn {
|
||||
background: #e6ebff;
|
||||
color: #5778ff;
|
||||
border: 1px solid #adbdff;
|
||||
border-radius: 18px;
|
||||
padding: 8px 16px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.header-actions .custom-close-btn {
|
||||
position: static;
|
||||
transform: none;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,400 +0,0 @@
|
||||
<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 { OpusDecoder } from 'opus-decoder';
|
||||
import Recorder from 'opus-recorder';
|
||||
|
||||
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>
|
||||
@@ -6,40 +6,61 @@ const TerserPlugin = require('terser-webpack-plugin');
|
||||
const CompressionPlugin = require('compression-webpack-plugin')
|
||||
// BundleAnalyzerPlugin 用于分析打包后的文件
|
||||
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
|
||||
// WorkboxPlugin 用于生成Service Worker
|
||||
const { InjectManifest } = require('workbox-webpack-plugin');
|
||||
// 引入 path 模块
|
||||
const path = require('path');
|
||||
|
||||
const path = require('path')
|
||||
|
||||
function resolve(dir) {
|
||||
return path.join(__dirname, dir)
|
||||
}
|
||||
|
||||
// 确保加载 .env 文件
|
||||
dotenv.config();
|
||||
|
||||
// 定义CDN资源列表,确保Service Worker也能访问
|
||||
const cdnResources = {
|
||||
css: [
|
||||
'https://unpkg.com/element-ui@2.15.14/lib/theme-chalk/index.css',
|
||||
'https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css'
|
||||
],
|
||||
js: [
|
||||
'https://unpkg.com/vue@2.6.14/dist/vue.min.js',
|
||||
'https://unpkg.com/vue-router@3.6.5/dist/vue-router.min.js',
|
||||
'https://unpkg.com/vuex@3.6.2/dist/vuex.min.js',
|
||||
'https://unpkg.com/element-ui@2.15.14/lib/index.js',
|
||||
'https://unpkg.com/axios@0.27.2/dist/axios.min.js',
|
||||
'https://unpkg.com/opus-decoder@0.7.7/dist/opus-decoder.min.js'
|
||||
]
|
||||
};
|
||||
|
||||
// 判断是否使用CDN
|
||||
const useCDN = process.env.VUE_APP_USE_CDN === 'true';
|
||||
|
||||
module.exports = defineConfig({
|
||||
productionSourceMap: process.env.NODE_ENV === 'production' ? false : true, // 生产环境不生成 source map
|
||||
productionSourceMap: process.env.NODE_ENV !=='production', // 生产环境不生成 source map
|
||||
devServer: {
|
||||
port: 8001, // 指定端口为 8001
|
||||
proxy: {
|
||||
'/xiaozhi': {
|
||||
target: 'http://127.0.0.1:8002',
|
||||
changeOrigin: true
|
||||
}
|
||||
},
|
||||
client: {
|
||||
overlay: false, // 不显示 webpack 错误覆盖层
|
||||
},
|
||||
},
|
||||
publicPath: process.env.VUE_APP_PUBLIC_PATH || "/",
|
||||
chainWebpack: config => {
|
||||
|
||||
// 修改 HTML 插件配置,动态插入 CDN 链接
|
||||
config.plugin('html')
|
||||
.tap(args => {
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
args[0].cdn = {
|
||||
css: [
|
||||
'https://cdn.jsdelivr.net/npm/element-ui@2.15.14/lib/theme-chalk/index.css',
|
||||
'https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css'
|
||||
],
|
||||
js: [
|
||||
'https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.min.js',
|
||||
'https://cdn.jsdelivr.net/npm/vue-router@3.6.5/dist/vue-router.min.js',
|
||||
'https://cdn.jsdelivr.net/npm/vuex@3.6.2/dist/vuex.min.js',
|
||||
'https://cdn.jsdelivr.net/npm/element-ui@2.15.14/lib/index.js',
|
||||
'https://cdn.jsdelivr.net/npm/axios@0.27.2/dist/axios.min.js',
|
||||
'https://cdn.jsdelivr.net/npm/opus-decoder@0.7.7/dist/opus-decoder.min.js'
|
||||
]
|
||||
};
|
||||
// 根据配置决定是否使用CDN
|
||||
if (process.env.NODE_ENV === 'production' && useCDN) {
|
||||
args[0].cdn = cdnResources;
|
||||
}
|
||||
return args;
|
||||
});
|
||||
@@ -97,14 +118,38 @@ module.exports = defineConfig({
|
||||
minRatio: 0.8
|
||||
})
|
||||
);
|
||||
config.externals = {
|
||||
'vue': 'Vue',
|
||||
'vue-router': 'VueRouter',
|
||||
'vuex': 'Vuex',
|
||||
'element-ui': 'ELEMENT',
|
||||
'axios': 'axios',
|
||||
'opus-decoder': 'OpusDecoder'
|
||||
};
|
||||
|
||||
// 根据是否使用CDN来决定是否添加Service Worker
|
||||
config.plugins.push(
|
||||
new InjectManifest({
|
||||
swSrc: path.resolve(__dirname, 'src/service-worker.js'),
|
||||
swDest: 'service-worker.js',
|
||||
exclude: [/\.map$/, /asset-manifest\.json$/],
|
||||
maximumFileSizeToCacheInBytes: 5 * 1024 * 1024, // 5MB
|
||||
// 自定义Service Worker注入点
|
||||
injectionPoint: 'self.__WB_MANIFEST',
|
||||
// 添加额外信息传递给Service Worker
|
||||
additionalManifestEntries: useCDN ?
|
||||
[{ url: 'cdn-mode', revision: 'enabled' }] :
|
||||
[{ url: 'cdn-mode', revision: 'disabled' }]
|
||||
})
|
||||
);
|
||||
|
||||
// 如果使用CDN,则配置externals排除依赖包
|
||||
if (useCDN) {
|
||||
config.externals = {
|
||||
'vue': 'Vue',
|
||||
'vue-router': 'VueRouter',
|
||||
'vuex': 'Vuex',
|
||||
'element-ui': 'ELEMENT',
|
||||
'axios': 'axios',
|
||||
'opus-decoder': 'OpusDecoder'
|
||||
};
|
||||
} else {
|
||||
// 确保不使用CDN时不设置externals,让webpack打包所有依赖
|
||||
config.externals = {};
|
||||
}
|
||||
|
||||
if (process.env.ANALYZE === 'true') { // 通过环境变量控制
|
||||
config.plugins.push(
|
||||
new BundleAnalyzerPlugin({
|
||||
@@ -124,13 +169,13 @@ module.exports = defineConfig({
|
||||
config: [__filename] // 每次配置文件修改时缓存失效
|
||||
}
|
||||
};
|
||||
config.resolve.alias = {
|
||||
'@': path.resolve(__dirname, 'src'), // 让 '@' 代表 'src' 目录
|
||||
'@assets': path.resolve(__dirname, 'src/assets'), // 设置 '@assets' 为 'src/assets' 目录
|
||||
'@components': path.resolve(__dirname, 'src/components'), // 设置 '@components' 为 'src/components' 目录
|
||||
'@views': path.resolve(__dirname, 'src/views'), // 设置 '@views' 为 'src/views' 目录
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 将CDN资源信息暴露给service-worker.js
|
||||
pwa: {
|
||||
workboxOptions: {
|
||||
skipWaiting: true,
|
||||
clientsClaim: true
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||