mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-22 23:23:55 +08:00
Merge branch 'newsnow' into add_local_tts_change_voice
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<el-dialog :title="title" :visible.sync="dialogVisible" @close="handleClose" @open="handleOpen">
|
||||
<el-dialog :title="title" :visible.sync="dialogVisible" :close-on-click-modal="false" @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>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<el-drawer :visible.sync="dialogVisible" direction="rtl" size="50%" :wrapperClosable="false" :withHeader="false">
|
||||
<el-drawer :visible.sync="dialogVisible" direction="rtl" size="80%" :wrapperClosable="false" :withHeader="false">
|
||||
<!-- 自定义标题区域 -->
|
||||
<div class="custom-header">
|
||||
<div class="header-left">
|
||||
@@ -16,15 +16,18 @@
|
||||
<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 v-if="unselected.length">
|
||||
<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>
|
||||
</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 v-else style="display: flex; justify-content: center; align-items: center;">
|
||||
<el-empty description="没有更多的插件了" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -36,30 +39,120 @@
|
||||
<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 v-if="selectedList.length > 0">
|
||||
<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 v-else style="display: flex; justify-content: center; align-items: center;">
|
||||
<el-empty description="请选择插件功能" />
|
||||
</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-if="currentFunction" class="params-container">
|
||||
<el-form :model="currentFunction" class="param-form">
|
||||
<!-- 遍历 fieldsMeta,而不是 params 的 keys -->
|
||||
<div v-if="currentFunction.fieldsMeta.length == 0">
|
||||
<el-empty :description="currentFunction.name + ' 无需配置参数'" />
|
||||
</div>
|
||||
<el-form-item v-for="field in currentFunction.fieldsMeta" :key="field.key" :label="field.label"
|
||||
class="param-item" :class="{ 'textarea-field': field.type === 'array' || field.type === 'json' }">
|
||||
<template #label>
|
||||
<span style="font-size: 16px; margin-right: 6px;">{{ field.label }}</span>
|
||||
<el-tooltip effect="dark" :content="fieldRemark(field)" placement="top">
|
||||
<img src="@/assets/home/info.png" alt="" class="info-icon">
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<!-- ARRAY -->
|
||||
<el-input v-if="field.type === 'array'" type="textarea" v-model="currentFunction.params[field.key]"
|
||||
@change="val => handleParamChange(currentFunction, field.key, val)" />
|
||||
|
||||
<!-- JSON -->
|
||||
<el-input v-else-if="field.type === 'json'" type="textarea" :rows="6" placeholder="请输入合法的 JSON"
|
||||
v-model="textCache[field.key]" @blur="flushJson(field)" />
|
||||
|
||||
<!-- number -->
|
||||
<el-input-number v-else-if="field.type === 'number'" :value="currentFunction.params[field.key]"
|
||||
@change="val => handleParamChange(currentFunction, field.key, val)" />
|
||||
|
||||
<!-- boolean -->
|
||||
<el-switch v-else-if="field.type === 'boolean' || field.type === 'bool'"
|
||||
:value="currentFunction.params[field.key]"
|
||||
@change="val => handleParamChange(currentFunction, field.key, val)" />
|
||||
|
||||
<!-- string or fallback -->
|
||||
<el-input v-else v-model="currentFunction.params[field.key]"
|
||||
@change="val => handleParamChange(currentFunction, field.key, val)" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<div v-else class="empty-tip">请选择已配置的功能进行参数设置</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- MCP区域 -->
|
||||
<div class="mcp-access-point">
|
||||
<div class="mcp-container">
|
||||
<!-- 左侧区域 -->
|
||||
<div class="mcp-left">
|
||||
<div class="mcp-header">
|
||||
<h3 class="bold-title">MCP接入点</h3>
|
||||
</div>
|
||||
<div class="url-header">
|
||||
<div class="address-desc">
|
||||
<span>以下是智能体的MCP接入点地址。</span>
|
||||
<a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/blob/main/docs/mcp-endpoint-integration.md"
|
||||
target="_blank" class="doc-link">查看接入点使用文档</a>
|
||||
</div>
|
||||
</div>
|
||||
<el-input v-model="mcpUrl" readonly class="url-input">
|
||||
<template #suffix>
|
||||
<el-button @click="copyUrl" class="inner-copy-btn" icon="el-icon-document-copy">
|
||||
复制
|
||||
</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
|
||||
<!-- 右侧区域 -->
|
||||
<div class="mcp-right">
|
||||
<div class="mcp-header">
|
||||
<h3 class="bold-title">接入点状态</h3>
|
||||
</div>
|
||||
<div class="status-container">
|
||||
<span class="status-indicator" :class="mcpStatus"></span>
|
||||
<span class="status-text">{{
|
||||
mcpStatus === 'connected' ? '已连接' :
|
||||
mcpStatus === 'loading' ? '加载中...' : '未连接'
|
||||
}}</span>
|
||||
<button class="refresh-btn" @click="refreshStatus">
|
||||
<span class="refresh-icon">↻</span>
|
||||
<span>刷新</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="mcp-tools-list">
|
||||
<div v-if="mcpTools.length > 0" class="tools-grid">
|
||||
<el-button v-for="tool in mcpTools" :key="tool" size="small" class="tool-btn" plain>
|
||||
{{ tool }}
|
||||
</el-button>
|
||||
</div>
|
||||
<div v-else class="no-tools">
|
||||
<span>暂无可用工具</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="drawer-footer">
|
||||
<el-button @click="closeDialog">取消</el-button>
|
||||
<el-button type="primary" @click="saveSelection">保存配置</el-button>
|
||||
@@ -68,30 +161,31 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Api from '@/apis/api';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
value: Boolean,
|
||||
functions: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
allFunctions: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
agentId: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
textCache: {},
|
||||
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'
|
||||
@@ -100,6 +194,10 @@ export default {
|
||||
// 添加一个标志位来跟踪是否已经保存
|
||||
hasSaved: false,
|
||||
loading: false,
|
||||
|
||||
mcpUrl: "",
|
||||
mcpStatus: "disconnected",
|
||||
mcpTools: [],
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -111,11 +209,42 @@ export default {
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value(newVal) {
|
||||
this.dialogVisible = newVal;
|
||||
if (newVal) {
|
||||
currentFunction(newFn) {
|
||||
if (!newFn) return;
|
||||
// 对每个字段,如果是 array 或 json,就在 textCache 里生成初始字符串
|
||||
newFn.fieldsMeta.forEach(f => {
|
||||
const v = newFn.params[f.key];
|
||||
if (f.type === 'array') {
|
||||
this.$set(this.textCache, f.key, Array.isArray(v) ? v.join('\n') : '');
|
||||
}
|
||||
else if (f.type === 'json') {
|
||||
try {
|
||||
this.$set(this.textCache, f.key, JSON.stringify(v ?? {}, null, 2));
|
||||
} catch {
|
||||
this.$set(this.textCache, f.key, '');
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
value(v) {
|
||||
this.dialogVisible = v;
|
||||
if (v) {
|
||||
// 对话框打开时,初始化选中态
|
||||
this.selectedNames = this.functions.map(f => f.name);
|
||||
// 把后端传来的 this.functions(带 params)merge 到 allFunctions 上
|
||||
this.functions.forEach(saved => {
|
||||
const idx = this.allFunctions.findIndex(f => f.name === saved.name);
|
||||
if (idx >= 0) {
|
||||
// 保留用户之前在 saved.params 上的改动
|
||||
this.allFunctions[idx].params = { ...saved.params };
|
||||
}
|
||||
});
|
||||
// 右侧默认指向第一个
|
||||
this.currentFunction = this.selectedList[0] || null;
|
||||
|
||||
// 加载MCP数据
|
||||
this.loadMcpAddress();
|
||||
this.loadMcpTools();
|
||||
}
|
||||
},
|
||||
dialogVisible(newVal) {
|
||||
@@ -123,14 +252,86 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
copyUrl() {
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.value = this.mcpUrl;
|
||||
textarea.style.position = 'fixed'; // 防止页面滚动
|
||||
document.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
|
||||
try {
|
||||
const successful = document.execCommand('copy');
|
||||
if (successful) {
|
||||
this.$message.success('已复制到剪贴板');
|
||||
} else {
|
||||
this.$message.error('复制失败,请手动复制');
|
||||
}
|
||||
} catch (err) {
|
||||
this.$message.error('复制失败,请手动复制');
|
||||
console.error('复制失败:', err);
|
||||
} finally {
|
||||
document.body.removeChild(textarea);
|
||||
}
|
||||
},
|
||||
|
||||
refreshStatus() {
|
||||
this.mcpStatus = "loading";
|
||||
this.loadMcpTools();
|
||||
},
|
||||
|
||||
// 加载MCP接入点地址
|
||||
loadMcpAddress() {
|
||||
Api.agent.getAgentMcpAccessAddress(this.agentId, (res) => {
|
||||
if (res.data.code === 0) {
|
||||
this.mcpUrl = res.data.data || "";
|
||||
} else {
|
||||
this.mcpUrl = "";
|
||||
console.error('获取MCP地址失败:', res.data.msg);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 加载MCP工具列表
|
||||
loadMcpTools() {
|
||||
Api.agent.getAgentMcpToolsList(this.agentId, (res) => {
|
||||
if (res.data.code === 0) {
|
||||
this.mcpTools = res.data.data || [];
|
||||
// 根据工具列表更新状态
|
||||
this.mcpStatus = this.mcpTools.length > 0 ? "connected" : "disconnected";
|
||||
} else {
|
||||
this.mcpTools = [];
|
||||
this.mcpStatus = "disconnected";
|
||||
console.error('获取MCP工具列表失败:', res.data.msg);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
flushArray(key) {
|
||||
const text = this.textCache[key] || '';
|
||||
const arr = text
|
||||
.split('\n')
|
||||
.map(s => s.trim())
|
||||
.filter(Boolean);
|
||||
this.handleParamChange(this.currentFunction, key, arr);
|
||||
},
|
||||
|
||||
flushJson(field) {
|
||||
const key = field.key;
|
||||
if (!key) {
|
||||
return;
|
||||
}
|
||||
const text = this.textCache[key] || '';
|
||||
try {
|
||||
const obj = JSON.parse(text);
|
||||
this.handleParamChange(this.currentFunction, key, obj);
|
||||
} catch {
|
||||
this.$message.error(`${this.currentFunction.name}的${key}字段格式错误:JSON格式有误`);
|
||||
}
|
||||
},
|
||||
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);
|
||||
const tempFunc = this.tempFunctions[func.name];
|
||||
this.currentFunction = tempFunc ? tempFunc : func;
|
||||
}
|
||||
},
|
||||
handleParamChange(func, key, value) {
|
||||
@@ -185,23 +386,31 @@ export default {
|
||||
|
||||
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))
|
||||
}));
|
||||
return {
|
||||
id: f.id,
|
||||
name: f.name,
|
||||
params: modified
|
||||
? { ...modified.params }
|
||||
: { ...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];
|
||||
}
|
||||
return this.functionColorMap[hash % this.functionColorMap.length];
|
||||
},
|
||||
fieldRemark(field) {
|
||||
let description = (field && field.label) ? field.label : '';
|
||||
if (field.default) {
|
||||
description += `(默认值:${field.default})`;
|
||||
}
|
||||
return description;
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -209,9 +418,9 @@ export default {
|
||||
<style lang="scss" scoped>
|
||||
.function-manager {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(120px, 0.5fr) minmax(120px, 0.5fr) minmax(200px, 2fr);
|
||||
grid-template-columns: max-content max-content 1fr;
|
||||
gap: 12px;
|
||||
height: calc(70vh - 60px);
|
||||
height: calc(58vh);
|
||||
}
|
||||
|
||||
.custom-header {
|
||||
@@ -248,6 +457,7 @@ export default {
|
||||
overflow-y: auto;
|
||||
border-right: 1px solid #EBEEF5;
|
||||
scrollbar-width: none;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.function-column::-webkit-scrollbar {
|
||||
@@ -257,7 +467,7 @@ export default {
|
||||
.function-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.function-item {
|
||||
@@ -317,9 +527,31 @@ export default {
|
||||
}
|
||||
|
||||
.param-form {
|
||||
.param-item {
|
||||
font-size: 16px;
|
||||
|
||||
&.textarea-field {
|
||||
::v-deep .el-form-item__content {
|
||||
margin-left: 0 !important;
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
::v-deep .el-form-item__label {
|
||||
display: block;
|
||||
width: 100% !important;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.param-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
::v-deep .el-form-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.el-form-item__label {
|
||||
@@ -356,9 +588,6 @@ export default {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.param-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.drawer-footer {
|
||||
position: absolute;
|
||||
@@ -407,4 +636,202 @@ export default {
|
||||
::v-deep .el-checkbox__label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mcp-access-point {
|
||||
border-top: 1px solid #EBEEF5;
|
||||
padding: 20px 24px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.mcp-header {
|
||||
.bold-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
margin: 5px 0 30px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.mcp-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 30px;
|
||||
}
|
||||
|
||||
.mcp-left,
|
||||
.mcp-right {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.url-header {
|
||||
margin-bottom: 8px;
|
||||
color: black;
|
||||
|
||||
h4 {
|
||||
margin: 0 0 15px 0;
|
||||
font-size: 16px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.address-desc {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.doc-link {
|
||||
color: #1677ff;
|
||||
text-decoration: none;
|
||||
margin-left: 4px;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.url-input {
|
||||
border-radius: 4px 0 0 4px;
|
||||
font-size: 14px;
|
||||
height: 36px;
|
||||
box-sizing: border-box;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
::v-deep .el-input__inner {
|
||||
background-color: #f5f5f5;
|
||||
padding-right: 80px;
|
||||
}
|
||||
|
||||
.url-input {
|
||||
|
||||
::v-deep .el-input__suffix {
|
||||
right: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-right: 10px;
|
||||
|
||||
.inner-copy-btn {
|
||||
pointer-events: auto;
|
||||
border: none;
|
||||
background: #1677ff;
|
||||
color: white;
|
||||
padding: 6px;
|
||||
margin-top: 4px;
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mcp-right {
|
||||
h4 {
|
||||
margin: 0 0 10px 0;
|
||||
font-size: 16px;
|
||||
font-weight: normal;
|
||||
color: black;
|
||||
}
|
||||
}
|
||||
|
||||
.status-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.status-indicator {
|
||||
display: inline-block;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
margin-right: 8px;
|
||||
|
||||
&.disconnected {
|
||||
background-color: #909399;
|
||||
/* 灰色 - 未连接 */
|
||||
}
|
||||
|
||||
&.connected {
|
||||
background-color: #67C23A;
|
||||
/* 绿色 - 已连接 */
|
||||
}
|
||||
|
||||
&.loading {
|
||||
background-color: #E6A23C;
|
||||
/* 橙色 - 加载中 */
|
||||
animation: pulse 1.5s infinite;
|
||||
}
|
||||
}
|
||||
|
||||
.status-text {
|
||||
font-size: 14px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 2px 10px;
|
||||
background: white;
|
||||
color: black;
|
||||
border: 1px solid #DCDFE6;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
background: #1677ff;
|
||||
color: white;
|
||||
border-color: #1677ff;
|
||||
}
|
||||
|
||||
.refresh-icon {
|
||||
margin-right: 6px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.mcp-tools-list {
|
||||
margin-top: 10px;
|
||||
|
||||
.tools-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.tool-btn {
|
||||
padding: 6px 12px;
|
||||
border-color: #1677ff;
|
||||
color: #1677ff;
|
||||
background-color: white;
|
||||
font-size: 12px;
|
||||
|
||||
&:hover {
|
||||
background-color: #1677ff;
|
||||
color: white;
|
||||
border-color: #1677ff;
|
||||
}
|
||||
}
|
||||
|
||||
.no-tools {
|
||||
text-align: center;
|
||||
color: #909399;
|
||||
font-size: 14px;
|
||||
padding: 10px 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,158 @@
|
||||
<template>
|
||||
<el-dialog title="手动添加设备" :visible="visible" @close="handleClose" width="30%" center>
|
||||
<div class="dialog-content">
|
||||
<el-form :model="deviceForm" :rules="rules" ref="deviceForm" label-width="100px">
|
||||
<el-form-item label="设备型号" prop="board">
|
||||
<el-select v-model="deviceForm.board" placeholder="请选择设备型号" style="width: 100%">
|
||||
<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="appVersion">
|
||||
<el-input v-model="deviceForm.appVersion" placeholder="请输入固件版本"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="Mac地址" prop="macAddress">
|
||||
<el-input v-model="deviceForm.macAddress" placeholder="请输入Mac地址"></el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<div style="display: flex;margin: 15px 15px;gap: 7px;">
|
||||
<div class="dialog-btn" @click="submitForm">确定</div>
|
||||
<div class="dialog-btn cancel-btn" @click="cancel">取消</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Api from '@/apis/api';
|
||||
|
||||
export default {
|
||||
name: 'ManualAddDeviceDialog',
|
||||
props: {
|
||||
visible: { type: Boolean, required: true },
|
||||
agentId: { type: String, required: true }
|
||||
},
|
||||
data() {
|
||||
// MAC地址验证规则
|
||||
const validateMac = (rule, value, callback) => {
|
||||
const macRegex = /^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/;
|
||||
if (!value) {
|
||||
callback(new Error('请输入Mac地址'));
|
||||
} else if (!macRegex.test(value)) {
|
||||
callback(new Error('请输入正确的Mac地址格式,例如:00:1A:2B:3C:4D:5E'));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
deviceForm: {
|
||||
board: '',
|
||||
appVersion: '',
|
||||
macAddress: ''
|
||||
},
|
||||
firmwareTypes: [],
|
||||
rules: {
|
||||
board: [
|
||||
{ required: true, message: '请选择设备型号', trigger: 'change' }
|
||||
],
|
||||
appVersion: [
|
||||
{ required: true, message: '请输入固件版本', trigger: 'blur' }
|
||||
],
|
||||
macAddress: [
|
||||
{ required: true, validator: validateMac, trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
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 || '获取固件类型失败');
|
||||
}
|
||||
},
|
||||
submitForm() {
|
||||
this.$refs.deviceForm.validate((valid) => {
|
||||
if (valid) {
|
||||
this.addDevice();
|
||||
}
|
||||
});
|
||||
},
|
||||
addDevice() {
|
||||
const params = {
|
||||
agentId: this.agentId,
|
||||
...this.deviceForm
|
||||
};
|
||||
|
||||
Api.device.manualAddDevice(params, ({ data }) => {
|
||||
if (data.code === 0) {
|
||||
this.$message.success('设备添加成功');
|
||||
this.$emit('refresh');
|
||||
this.closeDialog();
|
||||
} else {
|
||||
this.$message.error(data.msg || '添加失败');
|
||||
}
|
||||
});
|
||||
},
|
||||
closeDialog() {
|
||||
this.$emit('update:visible', false);
|
||||
this.$refs.deviceForm.resetFields();
|
||||
},
|
||||
cancel() {
|
||||
this.closeDialog();
|
||||
},
|
||||
handleClose() {
|
||||
this.closeDialog();
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dialog-content {
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.dialog-btn {
|
||||
cursor: pointer;
|
||||
flex: 1;
|
||||
border-radius: 23px;
|
||||
background: #5778ff;
|
||||
height: 40px;
|
||||
font-weight: 500;
|
||||
font-size: 12px;
|
||||
color: #fff;
|
||||
line-height: 40px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.cancel-btn {
|
||||
background: #e6ebff;
|
||||
border: 1px solid #adbdff;
|
||||
color: #5778ff;
|
||||
}
|
||||
|
||||
::v-deep .el-dialog {
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
::v-deep .el-dialog__body {
|
||||
padding: 20px 6px;
|
||||
}
|
||||
|
||||
::v-deep .el-form-item {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<el-dialog :visible.sync="dialogVisible" width="57%" center custom-class="custom-dialog" :show-close="false"
|
||||
<el-dialog :visible.sync="dialogVisible" :close-on-click-modal="false" 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;">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<el-dialog :visible="visible" @update:visible="handleVisibleChange" width="57%" center custom-class="custom-dialog"
|
||||
<el-dialog :visible="visible" :close-on-click-modal="false" @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;">
|
||||
@@ -18,7 +18,7 @@
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="供应器编码" prop="providerCode" style="flex: 1;">
|
||||
<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>
|
||||
@@ -87,6 +87,7 @@
|
||||
<el-option label="数字" value="number"></el-option>
|
||||
<el-option label="布尔值" value="boolean"></el-option>
|
||||
<el-option label="字典" value="dict"></el-option>
|
||||
<el-option label="分号分割的列表" value="array"></el-option>
|
||||
</el-select>
|
||||
</template>
|
||||
<template v-else>
|
||||
@@ -97,10 +98,10 @@
|
||||
<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>
|
||||
<el-input v-model="scope.row.default" placeholder="请输入默认值"></el-input>
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ scope.row.default_value }}
|
||||
{{ scope.row.default }}
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -161,7 +162,8 @@ export default {
|
||||
'string': '字符串',
|
||||
'number': '数字',
|
||||
'boolean': '布尔值',
|
||||
'dict': '字典'
|
||||
'dict': '字典',
|
||||
'array': '分号分割的列表'
|
||||
};
|
||||
return typeMap[type];
|
||||
},
|
||||
@@ -220,7 +222,7 @@ export default {
|
||||
key: '',
|
||||
label: '',
|
||||
type: 'string',
|
||||
default_value: '',
|
||||
default: '',
|
||||
selected: false,
|
||||
editing: true
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user