mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-22 07:03:53 +08:00
@@ -101,7 +101,8 @@
|
||||
$route.path === '/agent-template-management' ||
|
||||
$route.path === '/ota-management' ||
|
||||
$route.path === '/user-management' ||
|
||||
$route.path === '/feature-management',
|
||||
$route.path === '/feature-management' ||
|
||||
$route.path === '/replacement-word-management'
|
||||
}" @visible-change="handleParamDropdownVisibleChange">
|
||||
<span class="el-dropdown-link">
|
||||
<img loading="lazy" alt="" src="@/assets/header/param_management.png" :style="{
|
||||
@@ -113,7 +114,8 @@
|
||||
$route.path === '/agent-template-management' ||
|
||||
$route.path === '/ota-management' ||
|
||||
$route.path === '/user-management' ||
|
||||
$route.path === '/feature-management'
|
||||
$route.path === '/feature-management' ||
|
||||
$route.path === '/replacement-word-management'
|
||||
? 'brightness(0) invert(1)'
|
||||
: 'None',
|
||||
}" />
|
||||
@@ -154,32 +156,6 @@
|
||||
|
||||
<!-- 右侧元素 -->
|
||||
<div class="header-right">
|
||||
<div class="search-container" v-if="$route.path === '/home' && !(userInfo.superAdmin && isSmallScreen)">
|
||||
<div class="search-wrapper">
|
||||
<el-input v-model="search" :placeholder="$t('header.searchPlaceholder')" class="custom-search-input"
|
||||
@keyup.enter.native="handleSearch" @focus="showSearchHistory" @blur="hideSearchHistory" clearable
|
||||
ref="searchInput">
|
||||
<i slot="suffix" class="el-icon-search search-icon" @click="handleSearch"></i>
|
||||
</el-input>
|
||||
<!-- 搜索历史下拉框 -->
|
||||
<div v-if="showHistory && searchHistory.length > 0" class="search-history-dropdown">
|
||||
<div class="search-history-header">
|
||||
<span>{{ $t("header.searchHistory") }}</span>
|
||||
<el-button type="text" size="small" class="clear-history-btn" @click="clearSearchHistory">
|
||||
{{ $t("header.clearHistory") }}
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="search-history-list">
|
||||
<div v-for="(item, index) in searchHistory" :key="index" class="search-history-item"
|
||||
@click.stop="selectSearchHistory(item)">
|
||||
<span class="history-text">{{ item }}</span>
|
||||
<i class="el-icon-close clear-item-icon" @click.stop="removeSearchHistory(index)"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<img loading="lazy" alt="" src="@/assets/home/avatar.png" class="avatar-img" @click="handleAvatarClick" />
|
||||
<span class="el-user-dropdown" @click="handleAvatarClick">
|
||||
{{ userInfo.username || "加载中..." }}
|
||||
@@ -220,12 +196,6 @@ export default {
|
||||
voiceCloneDropdownVisible: false,
|
||||
userMenuVisible: false, // 添加用户菜单可见状态
|
||||
menuVisibleTimer: null, // 菜单显示定时器,防止够快触发
|
||||
isSmallScreen: false,
|
||||
// 搜索历史相关
|
||||
searchHistory: [],
|
||||
showHistory: false,
|
||||
SEARCH_HISTORY_KEY: "xiaozhi_search_history",
|
||||
MAX_HISTORY_COUNT: 3,
|
||||
// Cascader 配置
|
||||
cascaderProps: {
|
||||
expandTrigger: "click",
|
||||
@@ -351,17 +321,9 @@ export default {
|
||||
},
|
||||
},
|
||||
async mounted() {
|
||||
this.checkScreenSize();
|
||||
window.addEventListener("resize", this.checkScreenSize);
|
||||
// 从localStorage加载搜索历史
|
||||
this.loadSearchHistory();
|
||||
// 等待featureManager初始化完成后再加载功能状态
|
||||
await this.loadFeatureStatus();
|
||||
},
|
||||
//移除事件监听器
|
||||
beforeDestroy() {
|
||||
window.removeEventListener("resize", this.checkScreenSize);
|
||||
},
|
||||
methods: {
|
||||
handleRouter(type) {
|
||||
this.$router.push(this.routerPaths[type]);
|
||||
@@ -371,104 +333,6 @@ export default {
|
||||
// 等待featureManager初始化完成
|
||||
await featureManager.waitForInitialization();
|
||||
},
|
||||
checkScreenSize() {
|
||||
this.isSmallScreen = window.innerWidth <= 1386;
|
||||
},
|
||||
// 处理搜索
|
||||
handleSearch() {
|
||||
const searchValue = this.search.trim();
|
||||
|
||||
// 如果搜索内容为空,触发重置事件
|
||||
if (!searchValue) {
|
||||
this.$emit("search-reset");
|
||||
return;
|
||||
}
|
||||
|
||||
// 保存搜索历史
|
||||
this.saveSearchHistory(searchValue);
|
||||
|
||||
// 触发搜索事件,将搜索关键词传递给父组件
|
||||
this.$emit("search", searchValue);
|
||||
|
||||
// 搜索完成后让输入框失去焦点,从而触发blur事件隐藏搜索历史
|
||||
if (this.$refs.searchInput) {
|
||||
this.$refs.searchInput.blur();
|
||||
}
|
||||
},
|
||||
|
||||
// 显示搜索历史
|
||||
showSearchHistory() {
|
||||
this.showHistory = true;
|
||||
},
|
||||
|
||||
// 隐藏搜索历史
|
||||
hideSearchHistory() {
|
||||
// 延迟隐藏,以便点击事件能够执行
|
||||
setTimeout(() => {
|
||||
this.showHistory = false;
|
||||
}, 200);
|
||||
},
|
||||
|
||||
// 加载搜索历史
|
||||
loadSearchHistory() {
|
||||
try {
|
||||
const history = localStorage.getItem(this.SEARCH_HISTORY_KEY);
|
||||
if (history) {
|
||||
this.searchHistory = JSON.parse(history);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("加载搜索历史失败:", error);
|
||||
this.searchHistory = [];
|
||||
}
|
||||
},
|
||||
|
||||
// 保存搜索历史
|
||||
saveSearchHistory(keyword) {
|
||||
if (!keyword || this.searchHistory.includes(keyword)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 添加到历史记录开头
|
||||
this.searchHistory.unshift(keyword);
|
||||
|
||||
// 限制历史记录数量
|
||||
if (this.searchHistory.length > this.MAX_HISTORY_COUNT) {
|
||||
this.searchHistory = this.searchHistory.slice(0, this.MAX_HISTORY_COUNT);
|
||||
}
|
||||
|
||||
// 保存到localStorage
|
||||
try {
|
||||
localStorage.setItem(this.SEARCH_HISTORY_KEY, JSON.stringify(this.searchHistory));
|
||||
} catch (error) {
|
||||
console.error("保存搜索历史失败:", error);
|
||||
}
|
||||
},
|
||||
|
||||
// 选择搜索历史项
|
||||
selectSearchHistory(keyword) {
|
||||
this.search = keyword;
|
||||
this.handleSearch();
|
||||
},
|
||||
|
||||
// 移除单个搜索历史项
|
||||
removeSearchHistory(index) {
|
||||
this.searchHistory.splice(index, 1);
|
||||
try {
|
||||
localStorage.setItem(this.SEARCH_HISTORY_KEY, JSON.stringify(this.searchHistory));
|
||||
} catch (error) {
|
||||
console.error("更新搜索历史失败:", error);
|
||||
}
|
||||
},
|
||||
|
||||
// 清空所有搜索历史
|
||||
clearSearchHistory() {
|
||||
this.searchHistory = [];
|
||||
try {
|
||||
localStorage.removeItem(this.SEARCH_HISTORY_KEY);
|
||||
} catch (error) {
|
||||
console.error("清空搜索历史失败:", error);
|
||||
}
|
||||
},
|
||||
// 显示修改密码弹窗
|
||||
showChangePasswordDialog() {
|
||||
this.isChangePasswordDialogVisible = true;
|
||||
@@ -631,11 +495,9 @@ export default {
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.header {
|
||||
background: #f6fcfe66;
|
||||
border: 1px solid #fff;
|
||||
background: linear-gradient(180deg, #dfeafe, #eff4ff);
|
||||
height: 63px !important;
|
||||
min-width: 900px;
|
||||
/* 设置最小宽度防止过度压缩 */
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
@@ -651,7 +513,7 @@ export default {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-width: 120px;
|
||||
min-width: 130px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@@ -668,42 +530,53 @@ export default {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 25px;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(calc(-50% - 80px));
|
||||
background: white;
|
||||
border-radius: 30px;
|
||||
box-shadow: 0 0 6px 0px #cfe1fb;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
min-width: 300px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.equipment-management {
|
||||
height: 30px;
|
||||
border-radius: 15px;
|
||||
background: #deeafe;
|
||||
padding: 8px 16px;
|
||||
border-radius: 30px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
gap: 7px;
|
||||
color: #3d4566;
|
||||
color: #6c79a8;
|
||||
margin-left: 1px;
|
||||
align-items: center;
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
/* 防止导航按钮被压缩 */
|
||||
padding: 0 15px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.equipment-management.active-tab {
|
||||
background: #5778ff !important;
|
||||
color: #fff !important;
|
||||
background: linear-gradient(90deg, #2983fe 0%, #5251fc 100%);
|
||||
box-shadow: 0 1px 8px rgba(41, 131, 254, 0.5), inset 0 1px 0 rgba(255, 255, 255, 0.3);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.equipment-management.active-tab::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 50%;
|
||||
background: linear-gradient(to bottom, rgba(255, 255, 255, 0.4) 0%, rgba(255, 255, 255, 0) 100%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.equipment-management img {
|
||||
@@ -711,121 +584,6 @@ export default {
|
||||
height: 13px;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
margin-right: 5px;
|
||||
flex: 0.9;
|
||||
min-width: 60px;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.search-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.search-history-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: white;
|
||||
border: 1px solid #e4e6ef;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
z-index: 1000;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.search-history-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.clear-history-btn {
|
||||
color: #909399;
|
||||
font-size: 11px;
|
||||
padding: 0;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.clear-history-btn:hover {
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.search-history-list {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.search-history-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.search-history-item:hover {
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
.clear-item-icon {
|
||||
font-size: 10px;
|
||||
color: #909399;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.more-dropdown {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.more-dropdown .el-dropdown-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
height: 100%;
|
||||
padding: 0 15px;
|
||||
}
|
||||
|
||||
.search-history-item:hover .clear-item-icon {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.clear-item-icon:hover {
|
||||
color: #ff4949;
|
||||
}
|
||||
|
||||
.custom-search-input>>>.el-input__inner {
|
||||
height: 18px;
|
||||
border-radius: 9px;
|
||||
background-color: #fff;
|
||||
border: 1px solid #e4e6ef;
|
||||
padding-left: 8px;
|
||||
font-size: 9px;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
cursor: pointer;
|
||||
color: #909399;
|
||||
margin-right: 3px;
|
||||
font-size: 9px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.custom-search-input::v-deep .el-input__suffix-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.avatar-img {
|
||||
width: 21px;
|
||||
height: 21px;
|
||||
|
||||
@@ -1205,6 +1205,8 @@ export default {
|
||||
'voiceClone.operationClosed': 'Popup geschlossen',
|
||||
'voiceClone.cloneSuccess': 'Klonen erfolgreich',
|
||||
'voiceClone.cloneFailed': 'Klonen fehlgeschlagen',
|
||||
'voiceClone.cloneErrorTip': 'Klonen fehlgeschlagen, bitte bewegen Sie die Maus über die Fehlermeldung für Details',
|
||||
'voiceClone.apiError': 'API-Aufruf Fehler',
|
||||
'voiceClone.confirmClone': 'Sind Sie sicher, dass Sie diese Stimme klonen möchten?',
|
||||
'voiceClone.onlySuccessCanClone': 'Nur erfolgreich trainierte Stimmen können geklont werden',
|
||||
'common.insufficient': 'Unzureichend',
|
||||
@@ -1222,6 +1224,7 @@ export default {
|
||||
'voiceClone.training': 'Training',
|
||||
'voiceClone.trainSuccess': 'Training erfolgreich',
|
||||
'voiceClone.trainFailed': 'Training fehlgeschlagen',
|
||||
'voiceClone.trainFailedWithError': 'Training fehlgeschlagen: {error}',
|
||||
'voiceClone.itemsPerPage': '{items} Einträge/Seite',
|
||||
'voiceClone.firstPage': 'Erste Seite',
|
||||
'voiceClone.prevPage': 'Vorherige Seite',
|
||||
|
||||
@@ -1205,6 +1205,8 @@ export default {
|
||||
'voiceClone.operationClosed': 'Popup closed',
|
||||
'voiceClone.cloneSuccess': 'Clone successful',
|
||||
'voiceClone.cloneFailed': 'Clone failed',
|
||||
'voiceClone.cloneErrorTip': 'Clone failed, please hover over the error tip to view error details',
|
||||
'voiceClone.apiError': 'API call error',
|
||||
'voiceClone.confirmClone': 'Are you sure you want to clone this voice?',
|
||||
'voiceClone.onlySuccessCanClone': 'Only successfully trained voices can be cloned',
|
||||
'common.insufficient': 'Insufficient',
|
||||
@@ -1222,6 +1224,7 @@ export default {
|
||||
'voiceClone.training': 'Training',
|
||||
'voiceClone.trainSuccess': 'Training successful',
|
||||
'voiceClone.trainFailed': 'Training failed',
|
||||
'voiceClone.trainFailedWithError': 'Training failed: {error}',
|
||||
'voiceClone.itemsPerPage': '{items} items/page',
|
||||
'voiceClone.firstPage': 'First Page',
|
||||
'voiceClone.prevPage': 'Previous Page',
|
||||
|
||||
@@ -1203,8 +1203,10 @@ export default {
|
||||
'voiceClone.selectFirst': 'Por favor, selecione recursos de voz para excluir primeiro',
|
||||
'voiceClone.operationCancelled': 'Cancelar',
|
||||
'voiceClone.operationClosed': 'Popup fechado',
|
||||
'voiceClone.cloneSuccess': 'Clonado com sucesso',
|
||||
'voiceClone.cloneSuccess': 'Clonagem bem-sucedida',
|
||||
'voiceClone.cloneFailed': 'Falha ao clonar',
|
||||
'voiceClone.cloneErrorTip': 'Falha na clonagem, passe o mouse sobre a dica de erro para ver os detalhes',
|
||||
'voiceClone.apiError': 'Erro na chamada da API',
|
||||
'voiceClone.confirmClone': 'Tem certeza de que deseja clonar esta voz?',
|
||||
'voiceClone.onlySuccessCanClone': 'Apenas vozes treinadas com sucesso podem ser clonadas',
|
||||
'common.insufficient': 'Insuficiente',
|
||||
@@ -1222,6 +1224,7 @@ export default {
|
||||
'voiceClone.training': 'Treinando',
|
||||
'voiceClone.trainSuccess': 'Treinamento bem-sucedido',
|
||||
'voiceClone.trainFailed': 'Treinamento falhou',
|
||||
'voiceClone.trainFailedWithError': 'Treinamento falhou: {error}',
|
||||
'voiceClone.itemsPerPage': '{items} itens/página',
|
||||
'voiceClone.firstPage': 'Primeira Página',
|
||||
'voiceClone.prevPage': 'Página Anterior',
|
||||
|
||||
@@ -1205,6 +1205,8 @@ export default {
|
||||
'voiceClone.operationClosed': 'Đã đóng popup',
|
||||
'voiceClone.cloneSuccess': 'Nhân bản thành công',
|
||||
'voiceClone.cloneFailed': 'Nhân bản thất bại',
|
||||
'voiceClone.cloneErrorTip': 'Nhân bản thất bại, hãy di chuột qua thông báo lỗi để xem chi tiết',
|
||||
'voiceClone.apiError': 'Lỗi gọi API',
|
||||
'voiceClone.confirmClone': 'Bạn có chắc chắn muốn nhân bản giọng này?',
|
||||
'voiceClone.onlySuccessCanClone': 'Chỉ có thể nhân bản giọng đã huấn luyện thành công',
|
||||
'common.insufficient': 'Không đủ',
|
||||
@@ -1222,6 +1224,7 @@ export default {
|
||||
'voiceClone.training': 'Đang huấn luyện',
|
||||
'voiceClone.trainSuccess': 'Huấn luyện thành công',
|
||||
'voiceClone.trainFailed': 'Huấn luyện thất bại',
|
||||
'voiceClone.trainFailedWithError': 'Huấn luyện thất bại: {error}',
|
||||
'voiceClone.itemsPerPage': '{items} mục/trang',
|
||||
'voiceClone.firstPage': 'Trang đầu',
|
||||
'voiceClone.prevPage': 'Trang trước',
|
||||
|
||||
@@ -1205,6 +1205,8 @@ export default {
|
||||
'voiceClone.action': '操作',
|
||||
'voiceClone.cloneSuccess': '复刻成功',
|
||||
'voiceClone.cloneFailed': '复刻失败',
|
||||
'voiceClone.cloneErrorTip': '克隆失败,请将鼠标悬停在错误提示上,查看错误详情',
|
||||
'voiceClone.apiError': '调用API时出错',
|
||||
'voiceClone.confirmClone': '确定要复刻此音色吗?',
|
||||
'voiceClone.onlySuccessCanClone': '只有训练成功的音色才能复刻',
|
||||
'common.insufficient': '不足',
|
||||
@@ -1222,6 +1224,7 @@ export default {
|
||||
'voiceClone.training': '训练中',
|
||||
'voiceClone.trainSuccess': '训练成功',
|
||||
'voiceClone.trainFailed': '训练失败',
|
||||
'voiceClone.trainFailedWithError': '训练失败:{error}',
|
||||
'voiceClone.itemsPerPage': '{items}条/页',
|
||||
'voiceClone.firstPage': '首页',
|
||||
'voiceClone.prevPage': '上一页',
|
||||
|
||||
@@ -1205,6 +1205,8 @@ export default {
|
||||
'voiceClone.operationClosed': '已關閉彈窗',
|
||||
'voiceClone.cloneSuccess': '複刻成功',
|
||||
'voiceClone.cloneFailed': '複刻失敗',
|
||||
'voiceClone.cloneErrorTip': '複刻失敗,請將鼠標懸停在錯誤提示上,查看錯誤詳情',
|
||||
'voiceClone.apiError': '調用API時出錯',
|
||||
'voiceClone.confirmClone': '確定要複刻此音色嗎?',
|
||||
'voiceClone.onlySuccessCanClone': '只有訓練成功的音色才能複刻',
|
||||
'common.insufficient': '不足',
|
||||
@@ -1222,6 +1224,7 @@ export default {
|
||||
'voiceClone.training': '訓練中',
|
||||
'voiceClone.trainSuccess': '訓練成功',
|
||||
'voiceClone.trainFailed': '訓練失敗',
|
||||
'voiceClone.trainFailedWithError': '訓練失敗:{error}',
|
||||
'voiceClone.itemsPerPage': '{items}條/頁',
|
||||
'voiceClone.firstPage': '首頁',
|
||||
'voiceClone.prevPage': '上一頁',
|
||||
|
||||
@@ -586,7 +586,7 @@ export default {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-size: cover;
|
||||
background: linear-gradient(to bottom right, #dce8ff, #e4eeff, #e6cbfd) center;
|
||||
background: #eff4ff;
|
||||
-webkit-background-size: cover;
|
||||
-o-background-size: cover;
|
||||
overflow: hidden;
|
||||
|
||||
@@ -401,7 +401,7 @@ export default {
|
||||
display: flex;
|
||||
position: relative;
|
||||
flex-direction: column;
|
||||
background: linear-gradient(to bottom right, #dce8ff, #e4eeff, #e6cbfd) center;
|
||||
background: #eff4ff;
|
||||
background-size: cover;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -449,7 +449,7 @@ export default {
|
||||
display: flex;
|
||||
position: relative;
|
||||
flex-direction: column;
|
||||
background: linear-gradient(to bottom right, #dce8ff, #e4eeff, #e6cbfd) center;
|
||||
background: #eff4ff;
|
||||
background-size: cover;
|
||||
-webkit-background-size: cover;
|
||||
-o-background-size: cover;
|
||||
|
||||
@@ -390,7 +390,7 @@ export default {
|
||||
position: relative;
|
||||
flex-direction: column;
|
||||
background-size: cover;
|
||||
background: linear-gradient(to bottom right, #dce8ff, #e4eeff, #e6cbfd) center;
|
||||
background: #eff4ff;
|
||||
-webkit-background-size: cover;
|
||||
-o-background-size: cover;
|
||||
overflow: hidden;
|
||||
@@ -517,7 +517,6 @@ export default {
|
||||
flex-direction: column;
|
||||
border: none;
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
|
||||
::v-deep .el-card__body {
|
||||
|
||||
@@ -365,7 +365,7 @@ export default {
|
||||
position: relative;
|
||||
flex-direction: column;
|
||||
background-size: cover;
|
||||
background: linear-gradient(to bottom right, #dce8ff, #e4eeff, #e6cbfd) center;
|
||||
background: #eff4ff;
|
||||
-webkit-background-size: cover;
|
||||
-o-background-size: cover;
|
||||
overflow: hidden;
|
||||
@@ -424,9 +424,7 @@ export default {
|
||||
height: calc(100vh - 63px - 35px);
|
||||
margin: 20px 22px 0;
|
||||
border-radius: 15px;
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -383,8 +383,6 @@ export default {
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
padding: 16px 20px;
|
||||
box-shadow: 0 4px 16px rgba(31, 42, 68, 0.06);
|
||||
border: 1px solid #f0f3f9;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -695,7 +695,7 @@ export default {
|
||||
display: flex;
|
||||
position: relative;
|
||||
flex-direction: column;
|
||||
background: linear-gradient(to bottom right, #dce8ff, #e4eeff, #e6cbfd) center;
|
||||
background: #eff4ff;
|
||||
background-size: cover;
|
||||
-webkit-background-size: cover;
|
||||
-o-background-size: cover;
|
||||
@@ -758,7 +758,6 @@ export default {
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
padding: 14px 20px;
|
||||
box-shadow: 0 4px 16px rgba(31, 42, 68, 0.06);
|
||||
border: 1px solid #f0f3f9;
|
||||
}
|
||||
|
||||
|
||||
@@ -576,7 +576,7 @@ export default {
|
||||
position: relative;
|
||||
flex-direction: column;
|
||||
background-size: cover;
|
||||
background: linear-gradient(to bottom right, #dce8ff, #e4eeff, #e6cbfd) center;
|
||||
background: #eff4ff;
|
||||
-webkit-background-size: cover;
|
||||
-o-background-size: cover;
|
||||
}
|
||||
@@ -586,9 +586,7 @@ export default {
|
||||
height: calc(100vh - 63px - 35px - 72px);
|
||||
margin: 0 22px;
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||
position: relative;
|
||||
background: rgba(237, 242, 255, 0.5);
|
||||
}
|
||||
|
||||
.operation-bar {
|
||||
|
||||
@@ -348,7 +348,7 @@ export default {
|
||||
position: relative;
|
||||
flex-direction: column;
|
||||
background-size: cover;
|
||||
background: linear-gradient(to bottom right, #dce8ff, #e4eeff, #e6cbfd) center;
|
||||
background: #eff4ff;
|
||||
-webkit-background-size: cover;
|
||||
-o-background-size: cover;
|
||||
overflow: hidden;
|
||||
|
||||
@@ -361,7 +361,7 @@ export default {
|
||||
position: relative;
|
||||
flex-direction: column;
|
||||
background-size: cover;
|
||||
background: linear-gradient(to bottom right, #dce8ff, #e4eeff, #e6cbfd) center;
|
||||
background: #eff4ff;
|
||||
-webkit-background-size: cover;
|
||||
-o-background-size: cover;
|
||||
overflow: hidden;
|
||||
|
||||
@@ -376,7 +376,7 @@ export default {
|
||||
position: relative;
|
||||
flex-direction: column;
|
||||
background-size: cover;
|
||||
background: linear-gradient(to bottom right, #dce8ff, #e4eeff, #e6cbfd) center;
|
||||
background: #eff4ff;
|
||||
-webkit-background-size: cover;
|
||||
-o-background-size: cover;
|
||||
overflow: hidden;
|
||||
|
||||
@@ -386,7 +386,7 @@ export default {
|
||||
position: relative;
|
||||
flex-direction: column;
|
||||
background-size: cover;
|
||||
background: linear-gradient(to bottom right, #dce8ff, #e4eeff, #e6cbfd) center;
|
||||
background: #eff4ff;
|
||||
-webkit-background-size: cover;
|
||||
-o-background-size: cover;
|
||||
overflow: hidden;
|
||||
|
||||
@@ -137,7 +137,7 @@ export default {
|
||||
position: relative;
|
||||
flex-direction: column;
|
||||
background-size: cover;
|
||||
background: linear-gradient(to bottom right, #dce8ff, #e4eeff, #e6cbfd) center;
|
||||
background: #eff4ff;
|
||||
-webkit-background-size: cover;
|
||||
-o-background-size: cover;
|
||||
overflow: hidden;
|
||||
|
||||
@@ -269,7 +269,7 @@ export default {
|
||||
display: flex;
|
||||
position: relative;
|
||||
flex-direction: column;
|
||||
background: linear-gradient(to bottom right, #dce8ff, #e4eeff, #e6cbfd);
|
||||
background: #eff4ff;
|
||||
background-size: cover;
|
||||
-webkit-background-size: cover;
|
||||
-o-background-size: cover;
|
||||
|
||||
@@ -365,7 +365,7 @@ export default {
|
||||
position: relative;
|
||||
flex-direction: column;
|
||||
background-size: cover;
|
||||
background: linear-gradient(to bottom right, #dce8ff, #e4eeff, #e6cbfd) center;
|
||||
background: #eff4ff;
|
||||
-webkit-background-size: cover;
|
||||
-o-background-size: cover;
|
||||
overflow: hidden;
|
||||
|
||||
@@ -13,59 +13,85 @@
|
||||
<CustomButton type="confirm" icon="el-icon-search" @click="handleSearch">{{ $t('voiceClone.search') }}</CustomButton>
|
||||
</div>
|
||||
</div>
|
||||
<CustomTable
|
||||
ref="voiceCloneTable"
|
||||
:data="voiceCloneList"
|
||||
:columns="tableColumns"
|
||||
:loading="loading"
|
||||
<div v-loading="loading" class="voice-clone-grid">
|
||||
<div v-for="item in voiceCloneList" :key="item.id" class="voice-clone-card">
|
||||
<div class="card-top">
|
||||
<img src="@/assets/setting/voiceclone.png" alt="" width="80px" height="80px">
|
||||
<div class="card-info">
|
||||
<div class="info-left">
|
||||
<div class="info-title">
|
||||
<el-input v-show="item.isEdit" v-model="item.name" size="mini" maxlength="64"
|
||||
show-word-limit @keyup.enter.native="onNameEnter(item)"
|
||||
ref="nameInput" />
|
||||
<span v-show="!item.isEdit" class="name-text" :title="item.name">{{ item.name || '-' }}</span>
|
||||
</div>
|
||||
<div class="info-grid">
|
||||
<div class="info-row">
|
||||
<span class="info-label">{{ $t('voiceClone.voiceId') }}</span>
|
||||
<span class="info-value" :title="item.voiceId">{{ item.voiceId || '-' }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">{{ $t('voiceClone.languages') }}</span>
|
||||
<span class="info-value">{{ item.languages || '-' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-right" :class="{ 'info-right--centered': !item.hasVoice }">
|
||||
<el-tooltip :content="getTooltipContent(item)" placement="top" effect="light">
|
||||
<div class="status-button" :class="getStatusButtonClass(item)">
|
||||
<span>{{ getTrainStatusText(item) }}</span>
|
||||
</div>
|
||||
</el-tooltip>
|
||||
<div v-if="item.hasVoice" class="voice-wave" :class="{ 'is-playing': playingRowId === item.id }">
|
||||
<span class="wave-bar"></span>
|
||||
<span class="wave-bar"></span>
|
||||
<span class="wave-bar"></span>
|
||||
<span class="wave-bar"></span>
|
||||
<span class="wave-bar"></span>
|
||||
<span class="wave-bar"></span>
|
||||
<span class="wave-bar"></span>
|
||||
<span class="wave-bar"></span>
|
||||
<span class="wave-bar"></span>
|
||||
<span class="wave-bar"></span>
|
||||
<span class="wave-bar"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-actions">
|
||||
<el-button v-if="item.hasVoice" size="mini" type="text" icon="el-icon-video-play"
|
||||
@click="handlePlay(item)">
|
||||
{{ playingRowId === item.id ? $t('voiceClone.stop') : $t('voiceClone.play') }}
|
||||
</el-button>
|
||||
<el-button size="mini" type="text" icon="el-icon-upload2" @click="handleUpload(item)">
|
||||
{{ $t('voiceClone.upload') }}
|
||||
</el-button>
|
||||
<el-button v-if="item.hasVoice" size="mini" type="text" icon="el-icon-copy-document"
|
||||
@click="handleClone(item)" :loading="item._cloning">
|
||||
{{ $t('voiceClone.clone') }}
|
||||
</el-button>
|
||||
<el-button size="mini" type="text"
|
||||
:icon="item.isEdit ? 'el-icon-check' : 'el-icon-edit'"
|
||||
@click="handleEditButtonClick(item)">
|
||||
{{ item.isEdit ? $t('button.save') : $t('common.edit') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!loading && voiceCloneList.length === 0" class="empty-state">
|
||||
{{ $t('voiceClone.noVoiceCloneAssigned') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CustomPagination
|
||||
:total="total"
|
||||
:current-page="currentPage"
|
||||
:page-size="pageSize"
|
||||
:page-size-options="pageSizeOptions"
|
||||
@size-change="handlePageSizeChange"
|
||||
@page-change="goToPage"
|
||||
>
|
||||
<template slot="name" slot-scope="scope">
|
||||
<el-input v-show="scope.row.isEdit" v-model="scope.row.name" size="mini" maxlength="64"
|
||||
show-word-limit @blur="onNameBlur(scope.row)" @keyup.enter.native="onNameEnter(scope.row)"
|
||||
ref="nameInput" />
|
||||
<span v-show="!scope.row.isEdit" class="name-view">
|
||||
<i class="el-icon-edit" @click="handleEditName(scope.row)" style="cursor: pointer;"></i>
|
||||
<span @click="handleEditName(scope.row)">
|
||||
{{ scope.row.name || '-' }}
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
<template slot="trainStatus" slot-scope="scope">
|
||||
<div class="status-button" :class="getStatusButtonClass(scope.row)">
|
||||
<span>{{ getTrainStatusText(scope.row) }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template slot="Details" slot-scope="scope">
|
||||
<el-tooltip :content="getTooltipContent(scope.row)" placement="top">
|
||||
<el-button size="mini" type="text" icon="el-icon-info"
|
||||
@click="handleViewDetails(scope.row)">
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<template slot="action" slot-scope="scope">
|
||||
<el-button v-if="scope.row.hasVoice" size="mini" type="text"
|
||||
@click="handlePlay(scope.row)">
|
||||
{{ playingRowId === scope.row.id ? $t('voiceClone.stop') : $t('voiceClone.play') }}
|
||||
</el-button>
|
||||
<el-button size="mini" type="text" @click="handleUpload(scope.row)">
|
||||
{{ $t('voiceClone.upload') }}
|
||||
</el-button>
|
||||
<el-button v-if="scope.row.hasVoice" size="mini" type="text"
|
||||
@click="handleClone(scope.row)" :loading="scope.row._cloning">
|
||||
{{ $t('voiceClone.clone') }}
|
||||
</el-button>
|
||||
</template>
|
||||
<!-- 占位作用保持分页在右边展示 -->
|
||||
<template slot="footer-btns">
|
||||
<div></div>
|
||||
</template>
|
||||
</CustomTable>
|
||||
/>
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
@@ -86,11 +112,11 @@ import HeaderBar from "@/components/HeaderBar.vue";
|
||||
import VersionFooter from "@/components/VersionFooter.vue";
|
||||
import VoiceCloneDialog from "@/components/VoiceCloneDialog.vue";
|
||||
import CustomButton from "@/components/CustomButton.vue";
|
||||
import CustomTable from "@/components/CustomTable.vue";
|
||||
import CustomPagination from "@/components/CustomPagination.vue";
|
||||
import { formatDate } from "@/utils/format";
|
||||
|
||||
export default {
|
||||
components: { HeaderBar, VersionFooter, VoiceCloneDialog, CustomButton, CustomTable },
|
||||
components: { HeaderBar, VersionFooter, VoiceCloneDialog, CustomButton, CustomPagination },
|
||||
data() {
|
||||
return {
|
||||
searchName: "",
|
||||
@@ -108,39 +134,27 @@ export default {
|
||||
userId: null
|
||||
},
|
||||
currentAudio: null,
|
||||
playingRowId: null,
|
||||
tableColumns: []
|
||||
playingRowId: null
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.initTableColumns();
|
||||
this.fetchVoiceCloneList();
|
||||
},
|
||||
methods: {
|
||||
initTableColumns() {
|
||||
this.tableColumns = [
|
||||
{ prop: 'voiceId', label: this.$t('voiceClone.voiceId'), align: 'center' },
|
||||
{ prop: 'name', label: this.$t('voiceClone.name'), align: 'center' },
|
||||
{ prop: 'languages', label: this.$t('voiceClone.languages'), align: 'center' },
|
||||
{ prop: 'trainStatus', label: this.$t('voiceClone.trainStatus'), align: 'center' },
|
||||
{ prop: 'Details', label: this.$t('voiceClone.Details'), align: 'center', width: 120 },
|
||||
{ prop: 'action', label: this.$t('voiceClone.action'), align: 'center', width: 230 }
|
||||
];
|
||||
},
|
||||
getTooltipContent(row) {
|
||||
if (!row.hasVoice) {
|
||||
return '待上传';
|
||||
return this.$t('voiceClone.waitingUpload');
|
||||
}
|
||||
switch (row.trainStatus) {
|
||||
case 0:
|
||||
return '待复刻';
|
||||
return this.$t('voiceClone.waitingTraining');
|
||||
case 2:
|
||||
return '训练成功';
|
||||
return this.$t('voiceClone.trainSuccess');
|
||||
case 3:
|
||||
if (row.trainError) {
|
||||
return `训练失败:${row.trainError}`;
|
||||
return this.$t('voiceClone.trainFailedWithError', { error: row.trainError });
|
||||
}
|
||||
return '训练失败';
|
||||
return this.$t('voiceClone.trainFailed');
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
@@ -240,13 +254,13 @@ export default {
|
||||
}
|
||||
}, (error) => {
|
||||
console.error('API调用失败:', error);
|
||||
this.$message.error('克隆失败,请将鼠标悬停在错误提示上,查看错误详情');
|
||||
this.$message.error(this.$t('voiceClone.cloneErrorTip'));
|
||||
this.fetchVoiceCloneList();
|
||||
this.$set(row, '_cloning', false);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('调用API时出错:', error);
|
||||
this.$message.error('调用API时出错');
|
||||
this.$message.error(this.$t('voiceClone.apiError'));
|
||||
this.fetchVoiceCloneList();
|
||||
this.$set(row, '_cloning', false);
|
||||
}
|
||||
@@ -287,11 +301,13 @@ export default {
|
||||
row._submitting = false;
|
||||
});
|
||||
},
|
||||
onNameBlur(row) {
|
||||
row.isEdit = false;
|
||||
setTimeout(() => {
|
||||
handleEditButtonClick(row) {
|
||||
if (row.isEdit) {
|
||||
row.isEdit = false;
|
||||
this.submitName(row);
|
||||
}, 100);
|
||||
} else {
|
||||
this.handleEditName(row);
|
||||
}
|
||||
},
|
||||
onNameEnter(row) {
|
||||
row.isEdit = false;
|
||||
@@ -355,7 +371,7 @@ export default {
|
||||
position: relative;
|
||||
flex-direction: column;
|
||||
background-size: cover;
|
||||
background: linear-gradient(to bottom right, #dce8ff, #e4eeff, #e6cbfd) center;
|
||||
background: #eff4ff;
|
||||
-webkit-background-size: cover;
|
||||
-o-background-size: cover;
|
||||
overflow: hidden;
|
||||
@@ -430,28 +446,6 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
.name-view {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
cursor: pointer;
|
||||
|
||||
i {
|
||||
color: #909399;
|
||||
font-size: 14px;
|
||||
|
||||
&:hover {
|
||||
color: #5a64b5;
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
&:hover {
|
||||
color: #5a64b5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.status-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -480,11 +474,187 @@ export default {
|
||||
border: 1px solid #ffccc7;
|
||||
}
|
||||
|
||||
:deep(.el-table .el-button--text) {
|
||||
color: #7079aa;
|
||||
.voice-clone-grid {
|
||||
padding: 10px 16px;
|
||||
margin-bottom: 20px;
|
||||
height: calc(100% - 90px);
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(360px, 1fr));
|
||||
gap: 16px;
|
||||
align-content: start;
|
||||
min-height: 200px;
|
||||
overflow: auto;
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: #a1c9fd;
|
||||
border-radius: 3px;
|
||||
}
|
||||
&::-webkit-scrollbar-track {
|
||||
background: #f0f3fe;
|
||||
border-radius: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-table .el-button--text:hover) {
|
||||
color: #5a64b5;
|
||||
.voice-clone-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
padding: 16px 16px 8px;
|
||||
background: #fff;
|
||||
box-shadow: 0 0 10px 0 #e8ecf5;
|
||||
border-radius: 8px;
|
||||
transition: box-shadow 0.2s, border-color 0.2s;
|
||||
|
||||
&:hover {
|
||||
border-color: #c0caff;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
}
|
||||
|
||||
.card-top {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.voice-wave {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 5px;
|
||||
flex: 1;
|
||||
min-height: 40px;
|
||||
width: 100%;
|
||||
padding: 0 4px;
|
||||
|
||||
.wave-bar {
|
||||
width: 3px;
|
||||
height: 50%;
|
||||
max-height: 80px;
|
||||
background: linear-gradient(to top, #a5b4fc 0%, #c4b5fd 100%);
|
||||
border-radius: 2px;
|
||||
transform-origin: center;
|
||||
animation: voice-wave 1.2s ease-in-out infinite paused;
|
||||
|
||||
&:nth-child(1) { animation-delay: -0s; }
|
||||
&:nth-child(2) { animation-delay: -0.12s; }
|
||||
&:nth-child(3) { animation-delay: -0.24s; }
|
||||
&:nth-child(4) { animation-delay: -0.36s; }
|
||||
&:nth-child(5) { animation-delay: -0.48s; }
|
||||
&:nth-child(6) { animation-delay: -0.6s; }
|
||||
&:nth-child(7) { animation-delay: -0.72s; }
|
||||
&:nth-child(8) { animation-delay: -0.84s; }
|
||||
&:nth-child(9) { animation-delay: -0.96s; }
|
||||
&:nth-child(10) { animation-delay: -1.08s; }
|
||||
&:nth-child(11) { animation-delay: -1.2s; }
|
||||
}
|
||||
|
||||
&.is-playing .wave-bar {
|
||||
animation-play-state: running;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes voice-wave {
|
||||
0%, 100% { transform: scaleY(0.2); }
|
||||
50% { transform: scaleY(0.95); }
|
||||
}
|
||||
|
||||
.card-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.info-left {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
|
||||
.info-right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 10px;
|
||||
flex-shrink: 0;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.info-title {
|
||||
min-width: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
|
||||
.name-text {
|
||||
line-height: 28px;
|
||||
text-align: left;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
margin-right: 10px;
|
||||
color: #909399;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
color: #303133;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px 8px;
|
||||
padding-top: 10px;
|
||||
border-top: 1px solid #f0f2f5;
|
||||
|
||||
::v-deep .el-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
::v-deep .el-button--text {
|
||||
color: #7079aa;
|
||||
}
|
||||
|
||||
::v-deep .el-button--text:hover {
|
||||
color: #5a64b5;
|
||||
}
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
grid-column: 1 / -1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px 0;
|
||||
color: #909399;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
@@ -228,7 +228,7 @@ export default {
|
||||
position: relative;
|
||||
flex-direction: column;
|
||||
background-size: cover;
|
||||
background: linear-gradient(to bottom right, #dce8ff, #e4eeff, #e6cbfd) center;
|
||||
background: #eff4ff;
|
||||
-webkit-background-size: cover;
|
||||
-o-background-size: cover;
|
||||
overflow: hidden;
|
||||
|
||||
@@ -286,7 +286,7 @@ export default {
|
||||
position: relative;
|
||||
flex-direction: column;
|
||||
background-size: cover;
|
||||
background: linear-gradient(to bottom right, #dce8ff, #e4eeff, #e6cbfd) center;
|
||||
background: #eff4ff;
|
||||
-webkit-background-size: cover;
|
||||
-o-background-size: cover;
|
||||
overflow: hidden;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="welcome">
|
||||
<!-- 公共头部 -->
|
||||
<HeaderBar :devices="devices" @search="handleSearch" @search-reset="handleSearchReset" />
|
||||
<HeaderBar :devices="devices" />
|
||||
<el-main style="padding: 20px;display: flex;flex-direction: column;">
|
||||
<div>
|
||||
<!-- 首页内容 -->
|
||||
@@ -16,13 +16,30 @@
|
||||
<div class="hi-hint">
|
||||
let's have a wonderful day!
|
||||
</div>
|
||||
<div class="add-device-btn">
|
||||
<div class="left-add" @click="showAddDialog">
|
||||
{{ $t('home.addAgent') }}
|
||||
<div class="add-device-options">
|
||||
<div class="search-container" v-if="userInfo.superAdmin">
|
||||
<div class="search-wrapper">
|
||||
<el-input
|
||||
v-model="search"
|
||||
:placeholder="$t('header.searchPlaceholder')"
|
||||
class="custom-search-input"
|
||||
@keyup.enter.native="handleSearch"
|
||||
@clear="handleSearchReset"
|
||||
clearable
|
||||
ref="searchInput"
|
||||
>
|
||||
<i slot="suffix" class="el-icon-search search-icon" @click="handleSearch"></i>
|
||||
</el-input>
|
||||
</div>
|
||||
</div>
|
||||
<div style="width: 23px;height: 13px;background: #5778ff;margin-left: -10px;" />
|
||||
<div class="right-add">
|
||||
<i class="el-icon-right" @click="showAddDialog" style="font-size: 20px;color: #fff;" />
|
||||
<div class="add-device-btn">
|
||||
<div class="left-add" @click="showAddDialog">
|
||||
{{ $t('home.addAgent') }}
|
||||
</div>
|
||||
<div style="width: 23px;height: 13px;background: #5778ff;margin-left: -10px;" />
|
||||
<div class="right-add">
|
||||
<i class="el-icon-right" @click="showAddDialog" style="font-size: 20px;color: #fff;" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -57,6 +74,7 @@
|
||||
|
||||
<script>
|
||||
import Api from '@/apis/api';
|
||||
import { mapState } from "vuex";
|
||||
import AddWisdomBodyDialog from '@/components/AddWisdomBodyDialog.vue';
|
||||
import ChatHistoryDialog from '@/components/ChatHistoryDialog.vue';
|
||||
import DeviceItem from '@/components/DeviceItem.vue';
|
||||
@@ -84,10 +102,17 @@ export default {
|
||||
voiceprintRecognition: false,
|
||||
voiceClone: false,
|
||||
knowledgeBase: false
|
||||
}
|
||||
},
|
||||
search: "",
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapState({
|
||||
userInfo: (state) => state.userInfo,
|
||||
}),
|
||||
},
|
||||
|
||||
async mounted() {
|
||||
this.fetchAgentList();
|
||||
await this.loadFeatureStatus();
|
||||
@@ -119,26 +144,6 @@ export default {
|
||||
handleDeviceManage() {
|
||||
this.$router.push('/device-management');
|
||||
},
|
||||
handleSearch(keyword) {
|
||||
this.isSearching = true;
|
||||
this.isLoading = true;
|
||||
// 检测MAC地址格式:包含4个冒号
|
||||
const isMac = /^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$/.test(keyword)
|
||||
const searchType = isMac ? 'mac' : 'name';
|
||||
Api.agent.searchAgent(keyword, searchType, ({ data }) => {
|
||||
if (data?.data) {
|
||||
this.devices = data.data.map(item => ({
|
||||
...item,
|
||||
agentId: item.id
|
||||
}));
|
||||
}
|
||||
this.isLoading = false;
|
||||
}, (error) => {
|
||||
console.error('搜索智能体失败:', error);
|
||||
this.isLoading = false;
|
||||
this.$message.error(this.$t('message.searchFailed'));
|
||||
});
|
||||
},
|
||||
handleSearchReset() {
|
||||
this.isSearching = false;
|
||||
// 直接将原始设备列表赋值给显示设备列表,避免重新加载数据
|
||||
@@ -200,7 +205,36 @@ export default {
|
||||
this.currentAgentId = agentId;
|
||||
this.currentAgentName = agentName;
|
||||
this.showChatHistory = true;
|
||||
}
|
||||
},
|
||||
// 处理搜索
|
||||
handleSearch() {
|
||||
const searchValue = this.search.trim();
|
||||
|
||||
// 如果搜索内容为空,触发重置事件
|
||||
if (!searchValue) {
|
||||
this.handleSearchReset();
|
||||
return;
|
||||
}
|
||||
|
||||
this.isSearching = true;
|
||||
this.isLoading = true;
|
||||
// 检测MAC地址格式:包含4个冒号
|
||||
const isMac = /^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$/.test(searchValue)
|
||||
const searchType = isMac ? 'mac' : 'name';
|
||||
Api.agent.searchAgent(searchValue, searchType, ({ data }) => {
|
||||
if (data?.data) {
|
||||
this.devices = data.data.map(item => ({
|
||||
...item,
|
||||
agentId: item.id
|
||||
}));
|
||||
}
|
||||
this.isLoading = false;
|
||||
}, (error) => {
|
||||
console.error('搜索智能体失败:', error);
|
||||
this.isLoading = false;
|
||||
this.$message.error(this.$t('message.searchFailed'));
|
||||
});
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -212,7 +246,7 @@ export default {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: linear-gradient(145deg, #e6eeff, #eff0ff);
|
||||
background: #eff4ff;
|
||||
background-size: cover;
|
||||
/* 确保背景图像覆盖整个元素 */
|
||||
background-position: center;
|
||||
@@ -267,12 +301,17 @@ export default {
|
||||
margin-top: 5px;
|
||||
}
|
||||
}
|
||||
.add-device-options {
|
||||
display: flex;
|
||||
margin-top: 16px;
|
||||
margin-left: 75px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.add-device-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: 75px;
|
||||
margin-top: 15px;
|
||||
margin-left: 20px;
|
||||
cursor: pointer;
|
||||
|
||||
.left-add {
|
||||
@@ -299,6 +338,82 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
.search-container {
|
||||
margin-right: 5px;
|
||||
min-width: 60px;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.custom-search-input::v-deep .el-input__suffix-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.search-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.search-history-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: white;
|
||||
border: 1px solid #e4e6ef;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
z-index: 1000;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.search-history-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.clear-history-btn {
|
||||
color: #909399;
|
||||
font-size: 11px;
|
||||
padding: 0;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.clear-history-btn:hover {
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.search-history-list {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.search-history-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.search-history-item:hover {
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
.clear-item-icon {
|
||||
font-size: 10px;
|
||||
color: #909399;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.device-list-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
|
||||
|
||||
@@ -1407,7 +1407,7 @@ export default {
|
||||
display: flex;
|
||||
position: relative;
|
||||
flex-direction: column;
|
||||
background: linear-gradient(to bottom right, #dce8ff, #e4eeff, #e6cbfd);
|
||||
background: #eff4ff;
|
||||
background-size: cover;
|
||||
-webkit-background-size: cover;
|
||||
-o-background-size: cover;
|
||||
@@ -1431,9 +1431,7 @@ export default {
|
||||
height: calc(100vh - 63px - 35px - 60px);
|
||||
margin: 0 22px;
|
||||
border-radius: 15px;
|
||||
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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user