From 20ac6f3f9699cbb2b1192b19379efd871e8cc2c3 Mon Sep 17 00:00:00 2001 From: zhuoqinglian <1035449612@qq.com> Date: Tue, 17 Mar 2026 16:01:10 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=88=B7=E6=96=B0?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2=E8=8F=9C=E5=8D=95=E3=80=81=E9=A1=B5=E8=84=9A?= =?UTF-8?q?=E9=97=AA=E5=8A=A8=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/manager-web/.env | 4 +- main/manager-web/public/index.html | 6 +- main/manager-web/src/App.vue | 5 + main/manager-web/src/components/HeaderBar.vue | 47 +++------- .../src/components/ParamDialog.vue | 93 ++++++++++++++++--- .../src/components/ProviderDialog.vue | 1 - main/manager-web/src/store/index.js | 15 +-- main/manager-web/src/utils/featureManager.js | 15 ++- .../src/views/AgentTemplateManagement.vue | 5 + .../src/views/FeatureManagement.vue | 11 --- .../src/views/ProviderManagement.vue | 3 +- main/manager-web/src/views/auth.scss | 1 - main/manager-web/src/views/login.vue | 24 +++-- 13 files changed, 143 insertions(+), 87 deletions(-) diff --git a/main/manager-web/.env b/main/manager-web/.env index 1b5f8138..3fd5202e 100644 --- a/main/manager-web/.env +++ b/main/manager-web/.env @@ -1 +1,3 @@ -VUE_APP_TITLE=智控台 \ No newline at end of file +VUE_APP_TITLE=智控台 +VUE_APP_DESCRIPTION=是由华南理工大学刘思源教授团队研发的智能终端软硬件体系后端服务系统,专为xiaozhi-esp32开源硬件打造,具备多协议兼容、声纹识别、知识库管理等核心能力。 +VUE_APP_KEYWORDS=xiaozhi-server,小智服务端,智控台,AI硬件,智能硬件,AI玩具,情感陪伴,聊天机器人,智能家居,车载机器人 diff --git a/main/manager-web/public/index.html b/main/manager-web/public/index.html index e3ae5a53..3cd3bb73 100644 --- a/main/manager-web/public/index.html +++ b/main/manager-web/public/index.html @@ -5,6 +5,8 @@ + + <%= process.env.VUE_APP_TITLE %> @@ -17,10 +19,6 @@ </head> <body> - <noscript> - <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. - Please enable it to continue.</strong> - </noscript> <div id="app"></div> <!-- built files will be auto injected --> <% if (htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.js) { %> diff --git a/main/manager-web/src/App.vue b/main/manager-web/src/App.vue index 180f8f0d..bf4c7a45 100644 --- a/main/manager-web/src/App.vue +++ b/main/manager-web/src/App.vue @@ -60,6 +60,11 @@ export default { isCDNEnabled: process.env.VUE_APP_USE_CDN === 'true' }; }, + created() { + // 挂载 store 状态 + this.$store.commit('setUserInfo', JSON.parse(localStorage.getItem('userInfo') || '{}')); + this.$store.commit('setPubConfig', JSON.parse(localStorage.getItem('pubConfig') || '{}')); + }, mounted() { // 检测是否为移动设备且VUE_APP_H5_URL不为空,如果两个条件都满足则跳转到H5页面 if (this.isMobileDevice() && process.env.VUE_APP_H5_URL) { diff --git a/main/manager-web/src/components/HeaderBar.vue b/main/manager-web/src/components/HeaderBar.vue index 24f55ab9..6e3196d8 100644 --- a/main/manager-web/src/components/HeaderBar.vue +++ b/main/manager-web/src/components/HeaderBar.vue @@ -26,7 +26,7 @@ <span class="nav-text">{{ $t("header.smartManagement") }}</span> </div> <!-- 普通用户显示音色克隆 --> - <div v-if="!isSuperAdmin && featureStatus.voiceClone" class="equipment-management" + <div v-if="!userInfo.superAdmin && featureStatus.voiceClone" class="equipment-management" :class="{ 'active-tab': $route.path === '/voice-clone-management' }" @click="goVoiceCloneManagement"> <img loading="lazy" alt="" src="@/assets/header/voice.png" :style="{ filter: @@ -38,7 +38,7 @@ </div> <!-- 超级管理员显示音色克隆下拉菜单 --> - <el-dropdown v-if="isSuperAdmin && featureStatus.voiceClone" trigger="click" class="equipment-management more-dropdown" :class="{ + <el-dropdown v-if="userInfo.superAdmin && featureStatus.voiceClone" trigger="click" class="equipment-management more-dropdown" :class="{ 'active-tab': $route.path === '/voice-clone-management' || $route.path === '/voice-resource-management', @@ -64,7 +64,7 @@ </el-dropdown-menu> </el-dropdown> - <div v-if="isSuperAdmin" class="equipment-management" :class="{ 'active-tab': $route.path === '/model-config' }" + <div v-if="userInfo.superAdmin" class="equipment-management" :class="{ 'active-tab': $route.path === '/model-config' }" @click="goModelConfig"> <img loading="lazy" alt="" src="@/assets/header/model_config.png" :style="{ filter: @@ -81,7 +81,7 @@ }" /> <span class="nav-text">{{ $t("header.knowledgeBase") }}</span> </div> - <el-dropdown v-if="isSuperAdmin" trigger="click" class="equipment-management more-dropdown" :class="{ + <el-dropdown v-if="userInfo.superAdmin" trigger="click" class="equipment-management more-dropdown" :class="{ 'active-tab': $route.path === '/dict-management' || $route.path === '/params-management' || @@ -140,7 +140,7 @@ <!-- 右侧元素 --> <div class="header-right"> - <div class="search-container" v-if="$route.path === '/home' && !(isSuperAdmin && isSmallScreen)"> + <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 @@ -189,7 +189,7 @@ <script> import userApi from "@/apis/module/user"; import i18n, { changeLanguage } from "@/i18n"; -import { mapActions, mapGetters } from "vuex"; +import { mapActions, mapState } from "vuex"; import ChangePasswordDialog from "./ChangePasswordDialog.vue"; // 引入修改密码弹窗组件 import featureManager from "@/utils/featureManager"; // 引入功能管理工具类 @@ -202,10 +202,6 @@ export default { data() { return { search: "", - userInfo: { - username: "", - mobile: "", - }, isChangePasswordDialogVisible: false, // 控制修改密码弹窗的显示 paramDropdownVisible: false, voiceCloneDropdownVisible: false, @@ -224,18 +220,16 @@ export default { label: "label", children: "children", }, - // 功能状态 - featureStatus: { - voiceClone: false, // 音色克隆功能状态 - knowledgeBase: false, // 知识库功能状态 - }, }; }, computed: { - ...mapGetters(["getIsSuperAdmin"]), - isSuperAdmin() { - return this.getIsSuperAdmin; - }, + ...mapState({ + featureStatus: (state) => ({ + voiceClone: state.pubConfig.systemWebMenu?.features?.voiceClone?.enabled, // 音色克隆功能状态 + knowledgeBase: state.pubConfig.systemWebMenu?.features?.knowledgeBase?.enabled, // 知识库功能状态 + }), + userInfo: (state) => state.userInfo, + }), // 获取当前语言 currentLanguage() { return i18n.locale || "zh_CN"; @@ -325,7 +319,6 @@ export default { }, }, async mounted() { - this.fetchUserInfo(); this.checkScreenSize(); window.addEventListener("resize", this.checkScreenSize); // 从localStorage加载搜索历史 @@ -386,20 +379,6 @@ export default { async loadFeatureStatus() { // 等待featureManager初始化完成 await featureManager.waitForInitialization(); - - const config = featureManager.getConfig(); - - this.featureStatus.voiceClone = config.voiceClone; - this.featureStatus.knowledgeBase = config.knowledgeBase; - }, - // 获取用户信息 - fetchUserInfo() { - userApi.getUserInfo(({ data }) => { - this.userInfo = data.data; - if (data.data.superAdmin !== undefined) { - this.$store.commit("setUserInfo", data.data); - } - }); }, checkScreenSize() { this.isSmallScreen = window.innerWidth <= 1386; diff --git a/main/manager-web/src/components/ParamDialog.vue b/main/manager-web/src/components/ParamDialog.vue index 8c30ef0b..ec861ce3 100644 --- a/main/manager-web/src/components/ParamDialog.vue +++ b/main/manager-web/src/components/ParamDialog.vue @@ -17,17 +17,37 @@ class="custom-input"></el-input> </el-form-item> - <el-form-item :label="$t('paramDialog.paramValue')" prop="paramValue" class="form-item"> - <el-input v-model="form.paramValue" :placeholder="$t('paramDialog.paramValuePlaceholder')" - class="custom-input"></el-input> + <el-form-item :label="$t('paramDialog.valueType')" prop="valueType" class="form-item"> + <el-select + v-model="form.valueType" + :placeholder="$t('paramDialog.valueTypePlaceholder')" + class="custom-select" + @change="handleValueTypeChange" + > + <el-option + v-for="item in valueTypeOptions" + :key="item.value" + :label="$t(`paramDialog.${item.value}Type`)" + :value="item.value" + /> + </el-select> </el-form-item> - <el-form-item :label="$t('paramDialog.valueType')" prop="valueType" class="form-item"> - <el-select v-model="form.valueType" :placeholder="$t('paramDialog.valueTypePlaceholder')" - class="custom-select"> - <el-option v-for="item in valueTypeOptions" :key="item.value" :label="$t(`paramDialog.${item.value}Type`)" - :value="item.value" /> - </el-select> + <el-form-item :label="$t('paramDialog.paramValue')" prop="paramValue" class="form-item"> + <el-input + v-if="form.valueType !== 'json' && form.valueType !== 'array'" + v-model="form.paramValue" + :placeholder="$t('paramDialog.paramValuePlaceholder')" + class="custom-input" + ></el-input> + <el-input + v-else + type="textarea" + v-model="form.paramValue" + :placeholder="$t('paramDialog.paramValuePlaceholder')" + :rows="6" + class="custom-textarea" + ></el-input> </el-form-item> <el-form-item :label="$t('paramDialog.remark')" prop="remark" class="form-item remark-item"> @@ -95,11 +115,45 @@ export default { }; }, methods: { + handleValueTypeChange(value) { + if (value === 'json') { + + } + }, submit() { this.$refs.form.validate((valid) => { if (valid) { + const submitData = { ...this.form }; + + // 如果是 array 类型,校验格式并转换 + if (submitData.valueType === 'array' && submitData.paramValue) { + const lines = submitData.paramValue.split('\n').filter(line => line.trim()); + + // 检查除最后一行外的每行是否以分号结尾 + for (let i = 0; i < lines.length - 1; i++) { + if (!lines[i].trim().endsWith(';')) { + this.$message.error('数组格式错误,需要使用英文分号结尾'); + return; + } + } + + const items = lines + .map(item => item.trim().replace(/;$/, '')) + .filter(item => item); + submitData.paramValue = items.join(';'); + } + // 如果是 json 类型,压缩 JSON 格式后再提交 + else if (submitData.valueType === 'json' && submitData.paramValue) { + try { + const parsed = JSON.parse(submitData.paramValue); + submitData.paramValue = JSON.stringify(parsed); + } catch (e) { + // 如果解析失败,保持原值 + } + } + this.saving = true; // 开始加载 - this.$emit('submit', this.form); + this.$emit('submit', submitData); } }); }, @@ -116,7 +170,24 @@ export default { }, watch: { visible(newVal) { - if (!newVal) { + if (newVal) { + if (this.form.paramValue) { + // 如果是 json 类型,格式化显示 + if (this.form.valueType === 'json') { + try { + const parsed = JSON.parse(this.form.paramValue); + this.form.paramValue = JSON.stringify(parsed, null, 2); + } catch (e) { + // 如果解析失败,保持原值 + } + } + // 如果是 array 类型,将分号分隔的字符串转换为每行一个项目 + else if (this.form.valueType === 'array') { + const items = this.form.paramValue.split(';').filter(item => item.trim()); + this.form.paramValue = items.join(';\n'); + } + } + } else { // 当对话框关闭时,重置saving状态 this.saving = false; } diff --git a/main/manager-web/src/components/ProviderDialog.vue b/main/manager-web/src/components/ProviderDialog.vue index 61e5b9de..7abd7ea9 100644 --- a/main/manager-web/src/components/ProviderDialog.vue +++ b/main/manager-web/src/components/ProviderDialog.vue @@ -88,7 +88,6 @@ <el-option :label="$t('providerDialog.booleanType')" value="boolean"></el-option> <el-option :label="$t('providerDialog.dictType')" value="dict"></el-option> <el-option :label="$t('providerDialog.arrayType')" value="array"></el-option> - <el-option :label="$t('providerDialog.ragType')" value="RAG"></el-option> </el-select> </template> <template v-else> diff --git a/main/manager-web/src/store/index.js b/main/manager-web/src/store/index.js index b6b61685..271c5827 100644 --- a/main/manager-web/src/store/index.js +++ b/main/manager-web/src/store/index.js @@ -10,7 +10,6 @@ export default new Vuex.Store({ state: { token: '', userInfo: {}, // 添加用户信息存储 - isSuperAdmin: false, // 添加superAdmin状态 pubConfig: { // 添加公共配置存储 version: '', beianIcpNum: 'null', @@ -29,12 +28,6 @@ export default new Vuex.Store({ getUserInfo(state) { return state.userInfo }, - getIsSuperAdmin(state) { - if (localStorage.getItem('isSuperAdmin') === null) { - return state.isSuperAdmin - } - return localStorage.getItem('isSuperAdmin') === 'true' - }, getPubConfig(state) { return state.pubConfig } @@ -46,19 +39,17 @@ export default new Vuex.Store({ }, setUserInfo(state, userInfo) { state.userInfo = userInfo - const isSuperAdmin = userInfo.superAdmin === 1 - state.isSuperAdmin = isSuperAdmin - localStorage.setItem('isSuperAdmin', isSuperAdmin) + localStorage.setItem('userInfo', JSON.stringify(userInfo)) }, setPubConfig(state, config) { state.pubConfig = config + localStorage.setItem('pubConfig', JSON.stringify(config)) }, clearAuth(state) { state.token = '' state.userInfo = {} - state.isSuperAdmin = false localStorage.removeItem('token') - localStorage.removeItem('isSuperAdmin') + localStorage.removeItem('userInfo') } }, actions: { diff --git a/main/manager-web/src/utils/featureManager.js b/main/manager-web/src/utils/featureManager.js index 2a4a85e0..dff84cad 100644 --- a/main/manager-web/src/utils/featureManager.js +++ b/main/manager-web/src/utils/featureManager.js @@ -1,5 +1,6 @@ //功能配置工具 import Api from "@/apis/api"; +import store from "@/store"; class FeatureManager { constructor() { @@ -72,7 +73,13 @@ class FeatureManager { this.initialized = true; } - + /** + * 更新config缓存 + */ + updateConfigCache(config) { + store.commit('setPubConfig', config); + localStorage.setItem('pubConfig', JSON.stringify(config)); + } /** * 从pub-config接口获取配置 @@ -85,6 +92,7 @@ class FeatureManager { if (result && result.status === 200) { // 检查是否有data字段 if (result.data) { + const configCache = result.data.data || {}; // 检查是否有code字段,如果有则按照code判断 if (result.data.code !== undefined) { if (result.data.code === 0 && result.data.data && result.data.data.systemWebMenu) { @@ -110,6 +118,7 @@ class FeatureManager { console.warn('配置中缺少features对象,使用默认配置'); resolve(this.defaultFeatures); } + configCache.systemWebMenu = config; } catch (error) { console.warn('处理systemWebMenu配置失败:', error); resolve(null); @@ -143,6 +152,7 @@ class FeatureManager { console.warn('配置中缺少features对象,使用默认配置'); resolve(this.defaultFeatures); } + configCache.systemWebMenu = config; } catch (error) { console.warn('处理systemWebMenu配置失败:', error); resolve(null); @@ -152,6 +162,7 @@ class FeatureManager { resolve(null); } } + this.updateConfigCache(configCache) } else { console.warn('接口返回数据中缺少data字段,使用默认配置'); resolve(null); @@ -183,6 +194,8 @@ class FeatureManager { // 异步保存到后端API this.saveConfigToAPI(config).catch(error => { console.warn('保存配置到API失败:', error); + }).finally(() => { + this.init() }); // 触发配置变更事件 diff --git a/main/manager-web/src/views/AgentTemplateManagement.vue b/main/manager-web/src/views/AgentTemplateManagement.vue index e7c36106..ef935f0a 100644 --- a/main/manager-web/src/views/AgentTemplateManagement.vue +++ b/main/manager-web/src/views/AgentTemplateManagement.vue @@ -136,17 +136,22 @@ </div> </div> </div> + <el-footer> + <version-footer /> + </el-footer> </div> </template> <script> import HeaderBar from "@/components/HeaderBar"; import agentApi from "@/apis/module/agent"; +import VersionFooter from "@/components/VersionFooter.vue"; export default { name: "AgentTemplateManagement", components: { HeaderBar, + VersionFooter }, data() { diff --git a/main/manager-web/src/views/FeatureManagement.vue b/main/manager-web/src/views/FeatureManagement.vue index 96c5f3cd..1b7dd1a6 100644 --- a/main/manager-web/src/views/FeatureManagement.vue +++ b/main/manager-web/src/views/FeatureManagement.vue @@ -131,9 +131,7 @@ export default { async created() { // 等待功能配置管理器初始化完成 try { - console.log('等待功能配置管理器初始化...') await featureManager.waitForInitialization() - console.log('功能配置管理器初始化完成,开始加载功能配置') await this.loadFeatures() this.setupConfigChangeListener() } catch (error) { @@ -152,14 +150,8 @@ export default { async getFeaturesByIds(featureIds) { try { const featureConfig = await featureManager.getAllFeatures() - console.log('获取到的功能配置:', JSON.stringify(featureConfig, null, 2)) - console.log('请求的功能ID列表:', featureIds) - const result = featureIds.map(id => { const feature = featureConfig[id] - console.log(`功能 ${id} 的配置:`, feature) - console.log(`功能 ${id} 的启用状态:`, feature?.enabled) - return { id: id, name: this.$t(`feature.${id}.name`), @@ -168,7 +160,6 @@ export default { } }) - console.log('最终返回的功能列表:', JSON.stringify(result, null, 2)) return result } catch (error) { console.error('获取功能配置失败:', error) @@ -245,7 +236,6 @@ export default { setTimeout(() => { this.loadFeatures() - this.$router.go(0) }, 1000) } catch (error) { console.error('保存配置失败:', error) @@ -261,7 +251,6 @@ export default { // 设置配置变化监听器 setupConfigChangeListener() { this.configChangeHandler = () => { - console.log('检测到配置变化,重新加载功能列表') this.loadFeatures() } window.addEventListener('featureConfigReloaded', this.configChangeHandler) diff --git a/main/manager-web/src/views/ProviderManagement.vue b/main/manager-web/src/views/ProviderManagement.vue index 3c55c4b4..830d2867 100644 --- a/main/manager-web/src/views/ProviderManagement.vue +++ b/main/manager-web/src/views/ProviderManagement.vue @@ -546,7 +546,6 @@ export default { justify-content: space-between; align-items: center; margin-top: 10px; - padding-bottom: 10px; } .ctrl_btn { @@ -794,7 +793,7 @@ export default { } .el-table { - --table-max-height: calc(100vh - 40vh); + // --table-max-height: calc(100vh - 40vh); max-height: var(--table-max-height); .el-table__body-wrapper { diff --git a/main/manager-web/src/views/auth.scss b/main/manager-web/src/views/auth.scss index 2ed9db58..95f71e0a 100644 --- a/main/manager-web/src/views/auth.scss +++ b/main/manager-web/src/views/auth.scss @@ -76,7 +76,6 @@ margin-top: 20px; align-items: center; border-radius: 10px; - background: #f6f8fb; border: 1px solid #e4e6ef; height: 40px; padding: 0 15px; diff --git a/main/manager-web/src/views/login.vue b/main/manager-web/src/views/login.vue index 828e6a83..a34f2343 100644 --- a/main/manager-web/src/views/login.vue +++ b/main/manager-web/src/views/login.vue @@ -238,13 +238,6 @@ export default { this.$store.dispatch("fetchPubConfig").then(() => { // 根据配置决定默认登录方式 this.isMobileLogin = this.enableMobileRegister; - - // pub-config接口调用完成后,重新初始化featureManager以确保使用最新的配置 - featureManager.waitForInitialization().then(() => { - console.log('featureManager重新初始化完成,使用pub-config配置'); - }).catch(error => { - console.warn('featureManager重新初始化失败:', error); - }); }); }, methods: { @@ -256,7 +249,9 @@ export default { window.open(url, '_blank'); }, fetchCaptcha() { - if (this.$store.getters.getToken) { + // 处理手动清空localstorage导致无法获取验证码的问题 + const token = localStorage.getItem('token') + if (token) { if (this.$route.path !== "/home") { this.$router.push("/home"); } @@ -308,6 +303,17 @@ export default { } return true; }, + + getUserInfo() { + Api.user.getUserInfo(({ data }) => { + if (data.code === 0) { + this.$store.commit("setUserInfo", data.data); + goToPage("/home"); + } else { + showDanger("用户信息获取失败"); + } + }); + }, async login() { if (this.isMobileLogin) { @@ -361,7 +367,7 @@ export default { ({ data }) => { showSuccess(this.$t('login.loginSuccess')); this.$store.commit("setToken", JSON.stringify(data.data)); - goToPage("/home"); + this.getUserInfo(); }, (err) => { // 直接使用后端返回的国际化消息