Prechádzať zdrojové kódy

feat:新增系统设置模块,通用配置接口调试完成

hanyang 1 rok pred
rodič
commit
1b24d14b7f

+ 15 - 2
public/i18n/lang-en.json

@@ -44,6 +44,7 @@
     "usernameLength": "Username must be between 3 and 20 characters long",
     "passwordLength": "Password must be between 6 and 20 characters long",
     "emailFormat": "Incorrect email format",
+    "system": "System Settings",
     "logout": "Exit",
     "okBtn": "OK",
     "cancelBtn": "Cancel",
@@ -477,6 +478,7 @@
     "click_2_download": "Click to download the file"
   },
   "settings": {
+    "menuTitle": "System Settings",
     "global": "General configuration",
     "password": "Change Password",
     "current_password": "Current Password",
@@ -487,6 +489,17 @@
     "placeholder_new2_password": "Please confirm the new password",
     "btn_cancel": "Cancel",
     "btn_submit": "Confirm",
-    "warning_not_match": "The new password entered twice does not match. Please confirm"
+    "warning_not_match": "The new password entered twice does not match. Please confirm",
+    "platform_name_empty": "Please enter the platform name",
+    "platform_name_length": "Platform name cannot exceed 20 characters",
+    "platform_logo_empty": "Please upload a logo",
+    "platform_introduce_empty": "Please enter the platform introduction",
+    "platform_introduce_length": "Platform introduction cannot exceed 20 characters",
+    "platform_name": "Platform Name",
+    "platform_logo": "Logo",
+    "platform_introduce": "Platform Introduction",
+    "platform_name_placeholder": "Please enter the platform name",
+    "platform_logo_placeholder": "Please upload a logo",
+    "platform_introduce_placeholder": "Please enter the platform introduction"
   }
-}
+}

+ 14 - 1
public/i18n/lang-zh.json

@@ -44,6 +44,7 @@
     "usernameLength": "用户名长度必须在3-20个字符之间",
     "passwordLength": "密码长度必须在6-20个字符之间",
     "emailFormat": "邮箱格式不正确",
+    "system": "系统设置",
     "logout": "退出登录",
     "okBtn": "确定",
     "cancelBtn": "取消",
@@ -476,6 +477,7 @@
     "click_2_download": "点击下载文件"
   },
   "settings": {
+    "menuTitle": "系统设置",
     "global": "通用配置",
     "password": "修改密码",
     "current_password": "当前密码",
@@ -486,6 +488,17 @@
     "placeholder_new2_password": "请确认新密码",
     "btn_cancel": "取消",
     "btn_submit": "保存",
-    "warning_not_match": "新密码两次输入不一致,请确认"
+    "warning_not_match": "新密码两次输入不一致,请确认",
+    "platform_name_empty": "请输入平台名称",
+    "platform_name_length": "平台名称长度不能超过20个字符",
+    "platform_logo_empty": "请上传logo",
+    "platform_introduce_empty": "请输入平台介绍",
+    "platform_introduce_length": "平台介绍长度不能超过20个字符",
+    "platform_name": "平台名称",
+    "platform_logo": "logo",
+    "platform_introduce": "平台介绍",
+    "platform_name_placeholder": "请输入平台名称",
+    "platform_logo_placeholder": "请上传logo",
+    "platform_introduce_placeholder": "请输入平台介绍"
   }
 }

+ 39 - 0
src/apis/login.js

@@ -63,3 +63,42 @@ export function changeUserPassword(pre, cur) {
     })
   });
 }
+
+/**
+ * 获取系统信息
+ *
+ * */
+export function getSystemInfo() {
+  return request({
+    url: '/api/system/info',
+    method: 'get'
+  });
+}
+/**
+ * 修改系统信息
+ * @param {String} title - 系统标题
+ * @param {String} desc - 系统描述
+ * */
+export function updateSystemInfo(title, desc, logo) {
+  return request({
+    url: '/api/system/info',
+    method: 'put',
+    data: {
+      title,
+      desc,
+      logo
+    }
+  });
+}
+
+/**
+ * 上传logo
+ * */
+export function uploadLogo(data) {
+  return request({
+    url: '/api/system/upload',
+    method: 'post',
+    data,
+    headers: { 'Content-Type': 'multipart/form-data' }
+  });
+}

+ 32 - 0
src/base/store/use-platform-information.js

@@ -0,0 +1,32 @@
+import { defineStore } from 'pinia';
+
+/**
+ * 平台信息
+ */
+export const usePlatformInformation = defineStore('platformInformation', {
+  persist: {
+    enabled: true,
+    // 可选:自定义存储的 key 和要持久化的路径
+    strategies: [
+      {
+        key: 'platform',
+        storage: localStorage,
+        paths: ['platform_name', 'platform_introduce', 'logo_key'] // 只持久化 username 属性
+      }
+    ]
+  },
+  state: () => {
+    return {
+      platform_name: 'SmartAI大模型平台',
+      platform_introduce: '覆盖大多数典型业务场景',
+      logo_key: 'xxxx'
+    };
+  },
+  actions: {
+    updatePlatformInformation(platform_name, platform_introduce, key) {
+      this.platform_name = platform_name;
+      this.platform_introduce = platform_introduce;
+      this.logo_key = key;
+    }
+  }
+});

+ 0 - 1
src/modules/conversation-dialog/format-dialog-answer-from-sse_receive_message.js

@@ -354,7 +354,6 @@ export default function formatDialogAnswerFromSseReceiveMessage(message_obj, pre
       if (Array.isArray(resMessageFiles)) {
         resMessageFiles.forEach((item, index) => {
           arr.push(item);
-          d;
         });
 
         // fullImageUrls.url = arr;

+ 2 - 2
src/modules/login/auth.js

@@ -2,7 +2,7 @@ export function getLoginFormRules(t) {
   return {
     username: [
       { required: true, message: t('login.usernameTips'), trigger: 'blur' },
-      { min: 3, max: 20, message: t('login.usernameLength'), trigger: 'blur' }
+      { minLength: 3, maxLength: 20, message: t('login.usernameLength'), trigger: 'blur' }
     ],
     email: [
       { required: true, message: t('login.emailTips'), trigger: 'blur' },
@@ -10,7 +10,7 @@ export function getLoginFormRules(t) {
     ],
     password: [
       { required: true, message: t('login.passwordTips'), trigger: 'blur' },
-      { min: 8, max: 20, message: t('login.passwordLength'), trigger: 'blur' }
+      { minLength: 8, maxLength: 20, message: t('login.passwordLength'), trigger: 'blur' }
     ]
   };
 }

+ 13 - 0
src/modules/settings/use-settings-form-rules.js

@@ -0,0 +1,13 @@
+export function useSettingFormRules(t) {
+  return {
+    platform_name: [
+      { required: true, message: t('settings.platform_name_empty'), trigger: 'blur' },
+      { minLength: 1, maxLength: 20, message: t('settings.platform_name_length'), trigger: 'blur' }
+    ],
+    // platform_logo: [{ required: true, message: t('settings.platform_logo_empty'), trigger: 'blur' }],
+    platform_introduce: [
+      { required: true, message: t('settings.platform_introduce_empty'), trigger: 'blur' },
+      { minLength: 1, maxLength: 20, message: t('settings.platform_introduce_length'), trigger: 'blur' }
+    ]
+  };
+}

+ 28 - 4
src/views/Login.vue

@@ -1,13 +1,13 @@
 <template>
   <div class="login">
     <div class="login-logo">
-      <img class="logo-img" src="../assets/image/login/logo.png" alt="logo" />
-      <div class="logo-title">{{ $t('login.logoTitle') }}</div>
+      <img class="logo-img" src="/logo.png" alt="logo" />
+      <div class="logo-title">{{ platform_name }}</div>
     </div>
     <div class="login-section">
       <div class="intro">
-        <div class="intro-title">{{ $t('login.logoTitle') }}</div>
-        <div class="intro-title">{{ $t('login.titleTips') }}</div>
+        <div class="intro-title">{{ platform_name }}</div>
+        <div class="intro-title">{{ platform_introduce }}</div>
         <img src="../assets/image/login/login_group.png" alt="" />
       </div>
       <div class="login-form">
@@ -18,7 +18,31 @@
 </template>
 
 <script setup>
+import { ref, onMounted } from 'vue';
 import FormItem from '../modules/login/FormItem.vue';
+import { getSystemInfo } from '../apis/login';
+import { usePlatformInformation } from '../base/store/use-platform-information';
+
+const platformInformation = usePlatformInformation();
+
+const platform_name = ref(platformInformation.platform_name);
+const platform_introduce = ref(platformInformation.platform_introduce);
+
+const getSystemInfoFun = async () => {
+  try {
+    const res = await getSystemInfo();
+    console.log(res);
+    if (res.code / 1 === 200) {
+      platformInformation.updatePlatformInformation(res.data.title, res.data.desc);
+    }
+  } catch (e) {
+    console.log(e);
+  }
+};
+onMounted(() => {
+  // TODO: 获取系统名称和系统描述
+  getSystemInfoFun();
+});
 </script>
 <style scoped lang="css">
 .login {

+ 59 - 7
src/views/Settings.vue

@@ -3,11 +3,18 @@
     <ContentContainer :containerStyle="content_style">
       <div class="settings-container">
         <div class="settings-aside-menu-container">
-          <a-menu :style="{ width: '200px', borderRadius: '4px' }" @menu-item-click="handleMenuClick">
+          <div class="settings-aside-menu-title">
+            {{ $t('settings.menuTitle') }}
+          </div>
+          <a-menu
+            :style="{ width: '400px', borderRadius: '4px', paddingTop: '20px' }"
+            :default-selected-keys="['/settings/global']"
+            @menu-item-click="handleMenuClick"
+          >
             <!-- <a-menu-item key="/settings/profile">修改资料</a-menu-item> -->
             <a-menu-item key="/settings/global">
               <template #icon>
-                <icon-file />
+                <icon-file class="icon" />
               </template>
 
               {{ $t('settings.global') }}
@@ -15,7 +22,7 @@
 
             <a-menu-item key="/settings/password">
               <template #icon>
-                <icon-lock />
+                <icon-lock class="icon" />
               </template>
 
               {{ $t('settings.password') }}
@@ -30,7 +37,13 @@
         </div>
 
         <div class="settings-main-content-container">
-          <router-view></router-view>
+          <div class="settings-aside-menu-title">
+            <!-- {{ $t('settings.title') }} -->
+            {{ current_menu === 1 ? $t('settings.global') : $t('settings.password') }}
+          </div>
+          <div :style="{ paddingTop: '20px' }">
+            <router-view></router-view>
+          </div>
         </div>
       </div>
     </ContentContainer>
@@ -38,7 +51,7 @@
 </template>
 
 <script setup>
-import { ref } from 'vue';
+import { ref, onMounted, nextTick, computed } from 'vue';
 import { useRouter } from 'vue-router';
 
 import LayoutExperience from './layout/LayoutExperience.vue';
@@ -46,6 +59,10 @@ import ContentContainer from './layout/ContentContainer.vue';
 
 import bg_image from '../assets/chat/board-bg.png';
 
+import { useI18n } from 'vue-i18n';
+
+const { t } = useI18n();
+
 const router = useRouter();
 
 const content_style = {
@@ -55,18 +72,32 @@ const content_style = {
 };
 
 const collapsed = ref(false);
+const menu_map = {
+  '/settings/global': 1,
+  '/settings/password': 2
+};
+
+const current_menu = ref('');
 
 const handleMenuClick = (key) => {
+  current_menu.value = menu_map[key];
+  console.log(current_menu.value);
   router.push({
     path: key
   });
 };
+onMounted(() => {
+  nextTick(() => {
+    handleMenuClick('/settings/global');
+  });
+});
 </script>
 
 <style scoped>
 .settings-container {
   display: flex;
   justify-content: flex-start;
+  gap: 20px;
 }
 
 .settings-aside-menu-container {
@@ -81,11 +112,32 @@ const handleMenuClick = (key) => {
 .settings-main-content-container {
   box-sizing: border-box;
 
-  background-color: var(--color-fill-1);
+  background-color: var(--color-bg-1);
 
   flex-grow: 1;
 
-  padding: 15px;
+  padding: 10px;
   height: calc(100vh - 90px);
 }
+.settings-aside-menu-title {
+  height: 60px;
+  line-height: 60px;
+  border-bottom: 1px solid #f5e6eb;
+  font-size: 16px;
+  font-weight: 600;
+  color: var(--color-text-1);
+  padding-left: 10px;
+}
+。settings-main-content-container-content {
+  padding-top: 20px;
+}
+
+.arco-menu-light .arco-menu-item.arco-menu-selected {
+  background-color: #165dff;
+  color: #fff;
+  border-radius: 8px;
+}
+.arco-menu-light .arco-menu-item.arco-menu-selected .icon {
+  color: #fff;
+}
 </style>

+ 86 - 19
src/views/SettingsGlobal.vue

@@ -1,15 +1,16 @@
 <template>
   <div>
-    <a-form ref="ref_form" :model="form" :style="{ width: '700px' }">
-      <a-form-item field="平台名称" label="平台名称" :required="true">
-        <a-input v-model="form.platform_name" placeholder="请输入平台名称" />
+    <a-form ref="ref_form" :model="form" :style="{ width: '900px' }" :rules="form_rules">
+      <a-form-item field="platform_name" :label="$t('settings.platform_name')">
+        <a-input v-model="form.platform_name" :placeholder="$t('settings.platform_name_placeholder')" />
       </a-form-item>
 
-      <a-form-item field="logo" label="logo" :required="true">
+      <a-form-item field="platform_logo" :label="$t('settings.platform_logo')">
         <a-upload
           action="/"
           :fileList="file ? [file] : []"
           :show-file-list="false"
+          :accept="'image/png,image/jpeg'"
           @change="onChange"
           @progress="onProgress"
         >
@@ -37,8 +38,9 @@
               </div>
               <div class="arco-upload-picture-card" v-else>
                 <div class="arco-upload-picture-card-text">
-                  <IconPlus />
-                  <div style="margin-top: 10px; font-weight: 600">Upload</div>
+                  <img width="82px" src="/logo.png" alt="" />
+                  <!-- <IconPlus />
+                  <div style="margin-top: 10px; font-weight: 600">Upload</div> -->
                 </div>
               </div>
             </div>
@@ -46,22 +48,21 @@
         </a-upload>
       </a-form-item>
 
-      <a-form-item field="平台介绍" label="平台介绍" :required="true">
-        <a-textarea v-model="form.platform_introduce" placeholder="Please enter something" allow-clear />
+      <a-form-item field="platform_introduce" :label="$t('settings.platform_introduce')">
+        <a-textarea
+          v-model="form.platform_introduce"
+          :placeholder="$t('settings.platform_introduce_placeholder')"
+          allow-clear
+        />
       </a-form-item>
 
       <a-form-item>
         <div class="setting-password-btn-container">
           <a-space>
-            <a-button class="submit-btn">取消 </a-button>
-
-            <a-button
-              class="submit-btn"
-              type="primary"
-              html-type="submit"
-              :loading="loading"
-              @click="handleUpdatePassword"
-              >保存
+            <a-button class="submit-btn">{{ $t('settings.btn_cancel') }} </a-button>
+
+            <a-button class="submit-btn" type="primary" html-type="submit" :loading="loading" @click="handleUpdate">
+              {{ $t('settings.btn_submit') }}
             </a-button>
           </a-space>
         </div>
@@ -71,10 +72,18 @@
 </template>
 
 <script setup>
-import { ref, reactive, getCurrentInstance, computed } from 'vue';
+import { ref, reactive, getCurrentInstance, computed, onMounted } from 'vue';
 import { useRouter } from 'vue-router';
-
 import { Message } from '../3rd-libs/arco-vue-libs';
+import { usePlatformInformation } from '../base/store/use-platform-information';
+import { updateSystemInfo, uploadLogo } from '../apis/login';
+import { useI18n } from 'vue-i18n';
+
+import { useSettingFormRules } from '../modules/settings/use-settings-form-rules';
+
+const { t } = useI18n();
+
+const platformInformation = usePlatformInformation();
 
 const router = useRouter();
 
@@ -83,6 +92,9 @@ const form = reactive({
   platform_logo: '',
   platform_introduce: ''
 });
+const form_rules = computed(() => {
+  return useSettingFormRules(t);
+});
 
 const loading = ref(false);
 
@@ -98,6 +110,58 @@ const onChange = (_, currentFile) => {
 const onProgress = (currentFile) => {
   file.value = currentFile;
 };
+
+/**
+ * 修改系统信息
+ * */
+const updateSystemInfoFun = async () => {
+  try {
+    const res = await updateSystemInfo(form.platform_name, form.platform_introduce, form.platform_logo);
+    console.log(res);
+    if (res.code / 1 === 200) {
+      /**
+       * 获取当前时间的时间戳,作为key值存储,方便更新logo后,页面上logo及时更新
+       * */
+      const logo_key = new Date().getTime();
+      platformInformation.updatePlatformInformation(res.data.title, res.data.desc, logo_key);
+      Message.success(res.msg);
+    } else {
+      Message.error(res.msg);
+    }
+  } catch (e) {
+    console.log(e);
+  }
+};
+const uploadLogoFun = async () => {
+  const formData = new FormData();
+  formData.append('file', file.value.file);
+  try {
+    const res = await uploadLogo(formData);
+    console.log(res);
+    if (res.code / 1 === 200) {
+      console.log(res);
+      form.platform_logo = res.data.logo;
+    }
+  } catch (e) {
+    console.log(e);
+  }
+};
+
+const handleUpdate = async () => {
+  console.log('form', form);
+  if (!form.platform_name.trim().length || !form.platform_introduce.trim().length) {
+    return;
+  }
+  if (file) {
+    await uploadLogoFun();
+  }
+
+  await updateSystemInfoFun();
+};
+onMounted(() => {
+  form.platform_name = platformInformation.platform_name;
+  form.platform_introduce = platformInformation.platform_introduce;
+});
 </script>
 
 <style scoped>
@@ -106,4 +170,7 @@ const onProgress = (currentFile) => {
   justify-content: flex-end;
   flex-grow: 1;
 }
+.arco-upload-picture-card {
+  background-color: transparent;
+}
 </style>

+ 1 - 1
src/views/SettingsPassword.vue

@@ -1,6 +1,6 @@
 <template>
   <div>
-    <a-form ref="ref_form" :model="form" :style="{ width: '700px' }">
+    <a-form ref="ref_form" :model="form" :style="{ width: '900px' }">
       <a-form-item field="password_current" :label="$t('settings.current_password')" required>
         <a-input-password
           v-model="form.password_current"

+ 9 - 3
src/views/common/Header.vue

@@ -1,9 +1,9 @@
 <template>
   <div class="header">
     <div class="header-left">
-      <img class="header-logo-img" src="../../assets/logo.png" alt="SmartAI Logo" />
+      <img class="header-logo-img" :key="logo_key" src="/logo.png" alt="SmartAI Logo" />
 
-      <span>{{ $t('site.name') }}</span>
+      <span>{{ platform_name }}</span>
     </div>
 
     <div class="header-center">
@@ -41,7 +41,7 @@
 </template>
 
 <script setup>
-import { ref, onMounted } from 'vue';
+import { ref, onMounted, computed } from 'vue';
 import { setI18nLanguage } from '../../i18n';
 import { getCurrentTheme, setThemeDark, setThemeLight } from '../../base/theme';
 import { requestScreenFull, toggleScreenFull } from '../../3rd-libs/screenfull';
@@ -49,6 +49,8 @@ import { requestScreenFull, toggleScreenFull } from '../../3rd-libs/screenfull';
 import { useCurrentUserStore } from '../../base/store/use-current-user';
 import { localST_RoutesList } from '../../base/storage';
 
+import { usePlatformInformation } from '../../base/store/use-platform-information';
+
 import NavItem from './NavItem.vue';
 
 import PermissionDropdown from './PermissionDropdown.vue';
@@ -60,9 +62,13 @@ import BtnFullScreen from './header-btns/BtnFullScreen.vue';
 import BtnSettings from './header-btns/BtnSettings.vue';
 import BtnCurrentUser from './header-btns/BtnCurrentUser.vue';
 
+const platformInformation = usePlatformInformation();
+
 const nav_list = ref([]);
 const dropdown_list = ref([]);
 const permission_icon = ref('IconLock');
+const platform_name = computed(() => platformInformation.platform_name);
+const logo_key = computed(() => platformInformation.logo_key);
 /**
  * 请求网页全屏
  */

+ 4 - 1
src/views/common/header-btns/BtnCurrentUser.vue

@@ -5,6 +5,7 @@
     </div>
 
     <template #content>
+      <!-- <a-doption value="settings"><icon-settings />{{ $t('login.system') }}</a-doption> -->
       <a-doption value="logout"><icon-export />{{ $t('login.logout') }}</a-doption>
     </template>
   </a-dropdown>
@@ -24,7 +25,9 @@ const currentUser = useCurrentUserStore();
 const visible = ref(false);
 
 const handleSelect = (value) => {
-  visible.value = true;
+  if (value === 'logout') {
+    visible.value = true;
+  }
 };
 
 // 进入设置页面

+ 5 - 1
src/views/conversation/ChatAsideMenu.vue

@@ -3,7 +3,7 @@
     <div class="chat-aside-menu" :class="current_theme_klass">
       <div class="menu-content">
         <div class="menu-actions-top">
-          <img class="header-logo-img" src="../../assets/logo.png" alt="SmartAI Logo" />
+          <img class="header-logo-img" :key="logo_key" src="/logo.png" alt="SmartAI Logo" />
         </div>
 
         <div class="aside-menu-h-line"></div>
@@ -52,6 +52,7 @@ import { useThemeStore } from '../../base/store/use-theme';
 import { useRecentAgentStore } from '../../base/store/use-recent-agents';
 import { useAllAgentStore } from '../../base/store/use-all-agents';
 import { getIconIntelligent } from '../../modules/common/icon-map';
+import { usePlatformInformation } from '../../base/store/use-platform-information';
 
 import ChatAsideBtn from './ChatAsideBtn.vue';
 import ChatAsideAgent from './ChatAsideAgent.vue';
@@ -67,6 +68,7 @@ const router = useRouter();
 
 const themeStore = useThemeStore();
 const recentAgents = useRecentAgentStore();
+const platformInformation = usePlatformInformation();
 
 const current_theme_klass = computed(() => {
   const theme = themeStore.value;
@@ -101,6 +103,8 @@ const agents_list = computed(() => {
   return arr;
 });
 
+const logo_key = computed(() => platformInformation.logo_key);
+
 const handleMenuClick = (path) => {
   router.push(path);
 };