Merge pull request #3247 from xinnan-tech/update-theme

Update theme
This commit is contained in:
wengzh
2026-06-26 17:58:18 +08:00
committed by GitHub
27 changed files with 481 additions and 430 deletions
+28 -270
View File
@@ -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;
+3
View File
@@ -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',
+3
View File
@@ -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',
+4 -1
View File
@@ -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',
+3
View File
@@ -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',
+3
View File
@@ -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': '上一页',
+3
View File
@@ -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;
}
+1 -3
View File
@@ -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 {
+1 -1
View File
@@ -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>
+1 -1
View File
@@ -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;
+147 -32
View File
@@ -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;
// MAC4
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;
// MAC4
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));
+1 -3
View File
@@ -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;
}