Sfoglia il codice sorgente

feat: 增加联网和插件功能

ageer 1 mese fa
parent
commit
233520ca8a

+ 12 - 0
package-lock.json

@@ -9,6 +9,7 @@
       "version": "2.0.0",
       "dependencies": {
         "@traptitech/markdown-it-katex": "^3.6.0",
+        "@vicons/ionicons5": "^0.13.0",
         "@vue-office/pdf": "^2.0.10",
         "@vueuse/core": "^9.13.0",
         "await-to-js": "^3.0.0",
@@ -4058,6 +4059,12 @@
       "dev": true,
       "license": "ISC"
     },
+    "node_modules/@vicons/ionicons5": {
+      "version": "0.13.0",
+      "resolved": "https://registry.npmmirror.com/@vicons/ionicons5/-/ionicons5-0.13.0.tgz",
+      "integrity": "sha512-zvZKBPjEXKN7AXNo2Na2uy+nvuv6SP4KAMQxpKL2vfHMj0fSvuw7JZcOPCjQC3e7ayssKnaoFVAhbYcW6v41qQ==",
+      "license": "MIT"
+    },
     "node_modules/@vitejs/plugin-vue": {
       "version": "4.1.0",
       "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.1.0.tgz",
@@ -19137,6 +19144,11 @@
       "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
       "dev": true
     },
+    "@vicons/ionicons5": {
+      "version": "0.13.0",
+      "resolved": "https://registry.npmmirror.com/@vicons/ionicons5/-/ionicons5-0.13.0.tgz",
+      "integrity": "sha512-zvZKBPjEXKN7AXNo2Na2uy+nvuv6SP4KAMQxpKL2vfHMj0fSvuw7JZcOPCjQC3e7ayssKnaoFVAhbYcW6v41qQ=="
+    },
     "@vitejs/plugin-vue": {
       "version": "4.1.0",
       "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.1.0.tgz",

+ 1 - 0
package.json

@@ -33,6 +33,7 @@
   },
   "dependencies": {
     "@traptitech/markdown-it-katex": "^3.6.0",
+    "@vicons/ionicons5": "^0.13.0",
     "@vue-office/pdf": "^2.0.10",
     "@vueuse/core": "^9.13.0",
     "await-to-js": "^3.0.0",

+ 175 - 181
src/api/Recognition.ts

@@ -1,194 +1,188 @@
 import { mlog } from "./mjapi";
 
-export interface recType{
-    timeOut:number 
-    asrLanguage?:string
-    listener?: (result: string) => void
-    onEnd?: () => void
-    onStart?: () => void
+export interface recType {
+	timeOut: number;
+	asrLanguage?: string;
+	listener?: (result: string) => void;
+	onEnd?: () => void;
+	onStart?: () => void;
 }
 export class Recognition {
-  private recognition: any;
-
-  private listener?: (result: string) => void;
-
-  private isStop = false;
-
-  //选项
-  private recOpt:recType={timeOut:2000} 
-  
-  //
-  private handleTime: any ;
-
-  private hTime:Date | undefined;
-  //语言
-  private asrLanguage = 'cmn-Hans-CN'
-  //
-  private onEnd?: () => void;
-  private onStart?: () => void;
-  public setListener(fn: (result: string) => void) {
-    this.listener = fn;
-    return this;
-  }
-  public setOnEnd( fn: ( ) => void){
-    this.onEnd = fn;
-    return this;
-  }
-  public setOpt( opt:recType ){
-    this.recOpt= opt;
-
-    if(opt.listener)  this.setListener(opt.listener)
-    if(opt.onEnd)  this.setListener(opt.onEnd)
-    if(opt.asrLanguage)  this.setLang(opt.asrLanguage);
-    if(opt.onStart) this.onStart= opt.onStart;
-
-    return this;
-  }
-  
-  public setLang( lang:string ){
-    this.asrLanguage = lang;
-    return this;
-  }
-
-  public start() {
-    this.isStop = false;
-    // @ts-ignore
-    if (!window.SpeechRecognition && !window.webkitSpeechRecognition) return;
-    if (!this.recognition) {
-      // @ts-ignore
-      const recognition = new (window.SpeechRecognition || window.webkitSpeechRecognition)();
-      this.recognition = recognition;
-    }
-    const recognition = this.recognition;
-
-    // 返回实时识别结果
-    recognition.interimResults = true;
-    // 设置语言
-    const lang = this.asrLanguage;
-    recognition.lang = lang;
-
-    // 设置是否连续识别
-    recognition.continuous = true;
-
-    this.hTime = new Date();
-    this.handleTime = setInterval( ()=>this.check( this ), this.recOpt.timeOut ) 
-
-    // 当识别到语音时触发该事件
-    recognition.addEventListener('result', (event: any) => { 
-
-      let transcript = '';
-      for (let index = 0; index < event.results.length; index++) {
-        const item = event.results[index];
-        // 中文添加逗号
-        if (transcript && lang?.includes('Han')) transcript += ',';
-
-        transcript += (item as unknown as SpeechRecognitionAlternative[])[0]?.transcript;
-      }
-      if (!transcript) return; 
-      this.hTime = new Date();
-      this.listener?.(transcript);
-    });
-
-    // 当识别结束时触发该事件
-    recognition.addEventListener('end', () => {
-      mlog('recognition onEnd',  this.isStop );
-      if (this.isStop) {
-        this.onEnd?.();
-        this.handleTime && clearInterval( this.handleTime )
-        return;
-      }
-      // 继续监听
-      recognition.start();
-    });
-
-    // 启动语音识别
-    recognition.start();
-    this.onStart?.();
-
-    return this;
-  }
-
-  public stop() {
-    this.isStop = true;
-    this.recognition?.stop();
-    return this;
-  }
-
-  private check( that:Recognition ){
-     if( !that.hTime ) {
-         mlog('mcheck 未定义');
-        return ;
-     }  
-     const nTime =  new Date();
-     
-     const dt =  nTime.getTime()- that.hTime.getTime();
-     mlog('mcheck', dt,that.recOpt.timeOut );
-     if( dt> that.recOpt.timeOut ){
-        that.stop();
-     }
-     return this;
-  }
+	private recognition: any;
+	private listener?: (result: string) => void;
+	private isStop = false;
+	private recOpt: recType = { timeOut: 2000 };
+	private handleTime: any;
+	private hTime: Date | undefined;
+	private asrLanguage = "cmn-Hans-CN";
+	private onEnd?: () => void;
+	private onStart?: () => void;
+
+	public setListener(fn: (result: string) => void) {
+		this.listener = fn;
+		return this;
+	}
+
+	public setOnEnd(fn: () => void) {
+		this.onEnd = fn;
+		return this;
+	}
+
+	public setOpt(opt: recType) {
+		this.recOpt = opt;
+		if (opt.listener) this.setListener(opt.listener);
+		if (opt.onEnd) this.setOnEnd(opt.onEnd);
+		if (opt.asrLanguage) this.setLang(opt.asrLanguage);
+		if (opt.onStart) this.onStart = opt.onStart;
+		return this;
+	}
+
+	public setLang(lang: string) {
+		this.asrLanguage = lang;
+		return this;
+	}
+
+	public start() {
+		this.isStop = false;
+		if (
+			typeof window === "undefined" ||
+			(!window.SpeechRecognition && !window.webkitSpeechRecognition)
+		) {
+			console.warn("当前浏览器不支持 SpeechRecognition,请使用 Chrome 或 Edge");
+			return;
+		}
+
+		if (!this.recognition) {
+			const recognition = new (window.SpeechRecognition ||
+				window.webkitSpeechRecognition)();
+			this.recognition = recognition;
+		}
+		const recognition = this.recognition;
+
+		recognition.interimResults = true;
+		recognition.lang = this.asrLanguage;
+		recognition.continuous = true;
+
+		this.hTime = new Date();
+		this.handleTime = setInterval(() => this.check(this), this.recOpt.timeOut);
+
+		recognition.addEventListener("result", (event: any) => {
+			let transcript = "";
+			for (let index = 0; index < event.results.length; index++) {
+				const item = event.results[index];
+				if (transcript && this.asrLanguage.includes("Han")) transcript += ",";
+				transcript += (item as unknown as SpeechRecognitionAlternative[])[0]
+					?.transcript;
+			}
+			if (!transcript) return;
+			this.hTime = new Date();
+			this.listener?.(transcript);
+		});
+
+		recognition.addEventListener("end", () => {
+			if (this.isStop) {
+				clearInterval(this.handleTime);
+				return;
+			}
+			setTimeout(() => recognition.start(), 1000);
+		});
+
+		recognition.start();
+		this.onStart?.();
+		return this;
+	}
+
+	private check(that: Recognition) {
+		if (!that.hTime) return;
+		const dt = new Date().getTime() - that.hTime.getTime();
+		if (dt > that.recOpt.timeOut) {
+			that.stop(); // 只在 stop 中触发 onEnd
+		}
+	}
+
+	public stop() {
+		if (this.isStop) return this; // 如果已经停止,直接返回
+		this.isStop = true;
+		this.recognition?.stop();
+		clearInterval(this.handleTime);
+		this.onEnd?.(); // 只在这里触发一次 onEnd
+		return this;
+	}
+
+	// public stop() {
+	//   this.isStop = true;
+	//   this.recognition?.stop();
+	//   return this;
+	// }
+
+	// private check(that: Recognition) {
+	//   if (!that.hTime) return;
+	//   const dt = new Date().getTime() - that.hTime.getTime();
+	//   if (dt > that.recOpt.timeOut) that.stop();
+	// }
 }
 
-
 export const supportLanguages: Record<string, string> = {
-  'cmn-Hans-CN': '普通话 (中国大陆)',
-  'cmn-Hans-HK': '普通话 (香港)',
-  'yue-Hant-HK': '粵語 (香港)',
-  'en-US': 'English(United States)',
-  'en-GB': 'English(United Kingdom)',
-  'en-IN': 'English(India)',
-  'es-ES': 'Español',
-  'fr-FR': 'Français',
-  'de-DE': 'Deutsch',
-  'it-IT': 'Italiano',
-  'ja-JP': '日本語',
-  'ko-KR': '한국어',
-  'ar-SA': 'العربية',
-  'pt-BR': 'Português',
-  'ru-RU': 'Русский',
-  'nl-NL': 'Nederlands',
-  'tr-TR': 'Türkçe',
-  'sv-SE': 'Svenska',
-  'hi-IN': 'हिन्दी',
-  'el-GR': 'Ελληνικά',
-  'he-IL': 'עברית',
-  'id-ID': 'Bahasa Indonesia',
-  'pl-PL': 'Polski',
-  'th-TH': 'ไทย',
-  'cs-CZ': 'Čeština',
-  'hu-HU': 'Magyar',
-  'da-DK': 'Dansk',
-  'fi-FI': 'Suomi',
-  'no-NO': 'Norsk',
-  'sk-SK': 'Slovenčina',
-  'uk-UA': 'Українська',
-  'vi-VN': 'Tiếng Việt',
+	"cmn-Hans-CN": "普通话 (中国大陆)",
+	"cmn-Hans-HK": "普通话 (香港)",
+	"yue-Hant-HK": "粵語 (香港)",
+	"en-US": "English(United States)",
+	"en-GB": "English(United Kingdom)",
+	"en-IN": "English(India)",
+	"es-ES": "Español",
+	"fr-FR": "Français",
+	"de-DE": "Deutsch",
+	"it-IT": "Italiano",
+	"ja-JP": "日本語",
+	"ko-KR": "한국어",
+	"ar-SA": "العربية",
+	"pt-BR": "Português",
+	"ru-RU": "Русский",
+	"nl-NL": "Nederlands",
+	"tr-TR": "Türkçe",
+	"sv-SE": "Svenska",
+	"hi-IN": "हिन्दी",
+	"el-GR": "Ελληνικά",
+	"he-IL": "עברית",
+	"id-ID": "Bahasa Indonesia",
+	"pl-PL": "Polski",
+	"th-TH": "ไทย",
+	"cs-CZ": "Čeština",
+	"hu-HU": "Magyar",
+	"da-DK": "Dansk",
+	"fi-FI": "Suomi",
+	"no-NO": "Norsk",
+	"sk-SK": "Slovenčina",
+	"uk-UA": "Українська",
+	"vi-VN": "Tiếng Việt",
 };
 
 function sleep(time: number) {
-  return new Promise((resolve) => setTimeout(resolve, time));
+	return new Promise((resolve) => setTimeout(resolve, time));
 }
 
 //浏览器文字播放
-export async function speakText(content: string, callback: (playing: boolean) => void) {
-  if (!window.speechSynthesis) return;
-  if (speechSynthesis.speaking) {
-    speechSynthesis.cancel();
-    callback(false);
-  }
-
-  await sleep(300);
-
-  const msg = new SpeechSynthesisUtterance(content);
-  msg.lang = 'zh';
-  msg.rate = 1;
-  msg.addEventListener('end', () => {
-    callback(false);
-  });
-  msg.addEventListener('error', () => {
-    callback(false);
-  });
-  callback(true);
-  speechSynthesis.speak(msg);
-}
+export async function speakText(
+	content: string,
+	callback: (playing: boolean) => void
+) {
+	if (!window.speechSynthesis) return;
+	if (speechSynthesis.speaking) {
+		speechSynthesis.cancel();
+		callback(false);
+	}
+
+	await sleep(300);
+
+	const msg = new SpeechSynthesisUtterance(content);
+	msg.lang = "zh";
+	msg.rate = 1;
+	msg.addEventListener("end", () => {
+		callback(false);
+	});
+	msg.addEventListener("error", () => {
+		callback(false);
+	});
+	callback(true);
+	speechSynthesis.speak(msg);
+}

+ 35 - 39
src/api/knowledge.ts

@@ -1,73 +1,69 @@
-import request from '@/utils/request/req';
+import request from "@/utils/request/req";
 
 export interface KnowledgeReq {
-	id:string; // 知识库id
-	kid:string; // 附件id
-	uid:string;//  用户id  
-	kname:string; // 知识库名称
-	description:string;// 知识库描述 
+	id: string; // 知识库id
+	kid: string; // 附件id
+	uid: string; //  用户id
+	kname: string; // 知识库名称
+	description: string; // 知识库描述
 }
 
 export interface KnowledgeDelReq {
-	kid:string; // 附件id
+	kid: string; // 附件id
 }
 
 export interface KnowledgeDetailDelReq {
-	kid:string; // 附件id
-	docId:string; // 文档id
+	kid: string; // 附件id
+	docId: string; // 文档id
 }
 
 export interface SimpleGenerate {
-	model: string,
-	randomness: number,
-	stability_boost: number,
-	voiceId: string,
-	text: string
+	model: string;
+	randomness: number;
+	stability_boost: number;
+	voiceId: string;
+	text: string;
 }
 
 export function getKnowledge() {
 	return request({
-		url: '/knowledge/list',
-		method: 'get',
-	})
+		url: "/knowledge/list",
+		method: "get",
+	});
 }
 
-export function createKnowledgeReq(params:KnowledgeReq) {
+export function createKnowledgeReq(params: KnowledgeReq) {
 	return request({
-		url: '/knowledge/save',
-		method: 'post',
+		url: "/knowledge/save",
+		method: "post",
 		data: params,
-	})
+	});
 }
 
-export function delKnowledge(params:KnowledgeDelReq) {
+export function delKnowledge(params: KnowledgeDelReq) {
 	return request({
-		url: '/knowledge/remove/'+params.kid,
-		method: 'post',
-	})
+		url: "/knowledge/remove/" + params.kid,
+		method: "post",
+	});
 }
 
-
 export function getKnowledgeDetail(kid: string) {
 	return request({
-		url: '/knowledge/detail/'+kid,
-		method: 'get',
-	})
+		url: "/knowledge/detail/" + kid,
+		method: "get",
+	});
 }
 
-export function delKnowledgeDetail(params:KnowledgeDetailDelReq) {
+export function delKnowledgeDetail(params: KnowledgeDetailDelReq) {
 	return request({
-		url: 'knowledge/attach/remove/'+params.kid,
-		method: 'post',
-	})
+		url: "knowledge/attach/remove/" + params.docId,
+		method: "post",
+	});
 }
 
 export function getfragmentList(docId: string) {
 	return request({
-		url: '/knowledge/fragment/list/'+docId,
-		method: 'get',
-	})
+		url: "/knowledge/fragment/list/" + docId,
+		method: "get",
+	});
 }
-
-
-

+ 17 - 10
src/router/index.ts

@@ -4,6 +4,13 @@ import { createRouter, createWebHashHistory } from 'vue-router'
 import { setupPageGuard } from './permission'
 import { ChatLayout } from '@/views/chat/layout'
 import mjlayout from '@/views/mj/layout.vue'
+import lumalayout from '@/views/luma/layout.vue'
+import storelayout from '@/views/store/layout.vue'
+import fanyilayout from '@/views/fanyi/layout.vue'
+import pptlayout from '@/views/ppt/layout.vue'
+import musiclayout from '@/views/suno/layout.vue'
+import knowledgelayout from '@/views/knowledge/layout.vue'
+
 
 const routes: RouteRecordRaw[] = [
   {
@@ -50,7 +57,7 @@ const routes: RouteRecordRaw[] = [
   {
     path: '/draw',
     name: 'Rootdraw',
-    component: ChatLayout,
+    component: mjlayout,
     redirect: '/draw/index',
     children: [
       {
@@ -64,7 +71,7 @@ const routes: RouteRecordRaw[] = [
   {
 		path: "/fanyi",
 		name: "Fanyi",
-		component: ChatLayout,
+		component: fanyilayout,
 		redirect: "/fanyi/index",
 		children: [
 			{
@@ -78,7 +85,7 @@ const routes: RouteRecordRaw[] = [
   {
 		path: "/ppt",
 		name: "Ppt",
-		component: ChatLayout,
+		component: pptlayout,
 		redirect: "/ppt/index",
 		children: [
 			{
@@ -92,7 +99,7 @@ const routes: RouteRecordRaw[] = [
   {
     path: '/video',
     name: 'Video',
-    component: ChatLayout,
+    component: lumalayout,
     redirect: '/video/index',
     children: [
       {
@@ -106,7 +113,7 @@ const routes: RouteRecordRaw[] = [
 	{
 		path: "/music",
 		name: "Music",
-		component: ChatLayout,
+		component: musiclayout,
 		redirect: "/music/index",
 		children: [
 			{
@@ -120,7 +127,7 @@ const routes: RouteRecordRaw[] = [
   {
     path: '/store',
     name: 'Store',
-    component: ChatLayout,
+    component: storelayout,
     redirect: '/store/t',
     children: [
       {
@@ -134,7 +141,7 @@ const routes: RouteRecordRaw[] = [
   {
     path: '/wxbot',
     name: 'Wxbot',
-    component: ChatLayout,
+    component: mjlayout,
     redirect: '/wxbot/t',
     children: [
       {
@@ -147,7 +154,7 @@ const routes: RouteRecordRaw[] = [
   {
     path: '/knowledge',
     name: 'Knowledge',
-    component: ChatLayout,
+    component: knowledgelayout,
     redirect: '/knowledge/t',
     children: [
       {
@@ -161,7 +168,7 @@ const routes: RouteRecordRaw[] = [
   {
     path: '/annex',
     name: 'Annex',
-    component: ChatLayout,
+    component: knowledgelayout,
     redirect: '/annex/t',
     children: [
       {
@@ -175,7 +182,7 @@ const routes: RouteRecordRaw[] = [
   {
     path: '/fragment',
     name: 'Fragment',
-    component: ChatLayout,
+    component: knowledgelayout,
     redirect: '/fragment/t',
     children: [
       {

+ 3 - 3
src/views/chat/layout/sider/index.vue

@@ -187,7 +187,7 @@ async function store() {
       </main>
       <Footer v-if="isMobile"></Footer>
     </div>
-    <div class="nav-bar">
+    <!-- <div class="nav-bar">
       <div class="user-info" :style="{ height: isLogin ? '144px' : '90px', bottom: isLogin ? '84px' : '24px' }">
 
         <div v-show="isLogin">
@@ -208,7 +208,7 @@ async function store() {
 
           <div class="user-bottom" @click="store">
             <Button block>
-              <!-- 应用市场 -->
+        
               {{ $t('store.siderButton') }}
             </Button>
           </div>
@@ -232,7 +232,7 @@ async function store() {
           {{ $t('mjset.logout') }}
         </div>
       </div>
-    </div>
+    </div> -->
 
 
     

+ 657 - 87
src/views/fanyi/components/textComponent.vue

@@ -1,144 +1,714 @@
 <template>
-	<div class="name-textBox">
-		<div class="name-textBox-left">
-			<div class="name-selectBox">
-				<div>
-					<n-space vertical>
-						<n-select v-model:value="sourceLanguage" :options="options" />
-					</n-space>
+	<div class="translation-container">
+		<div class="translation-panel input-panel">
+			<div class="panel-header">
+				<div class="header-title">
+					<div>输入文本</div>
 				</div>
-				<div>
-					<n-space vertical>
-						<n-select v-model:value="targetLanguage" :options="options1" />
-					</n-space>
+				<div
+					class="voice-input"
+					@click="goASR"
+					:class="{ active: st.micStart }"
+				>
+					<IconSvg icon="voice" width="18px" height="18px"></IconSvg>
+					<span>语音输入</span>
 				</div>
+			</div>
+
+			<div class="input-wrapper">
+				<n-input
+					v-model:value="prompt"
+					type="textarea"
+					class="text-input"
+					placeholder="请在此输入需要翻译的文本..."
+					:autosize="{ minRows: 10, maxRows: 15 }"
+				/>
+				<!-- <div class="input-decoration"></div> -->
+			</div>
 
-				<div style="display: flex; align-items: center;">
-					<n-space vertical>
-						<n-select v-model:value="model" :options="modelListData" value-field="modelDescribe"
-							label-field="modelName" />
-					</n-space>
+			<div class="translation-controls">
+				<div class="control-group">
+					<label>目标语言</label>
+					<n-select v-model:value="targetLanguage" :options="options1" />
 				</div>
 
-				<n-button @click="handleTranslation" type="primary">翻译</n-button>
-			</div>
-			<n-input v-model:value="prompt" type="textarea" placeholder="请输入文本" :resizable="false" />
+				<div class="control-group">
+					<label>翻译模型</label>
+					<n-select
+						v-model:value="model"
+						:options="modelListData"
+						value-field="modelDescribe"
+						label-field="modelName"
+					/>
+				</div>
 
+				<n-button
+					class="translate-button"
+					@click="handleTranslation"
+					type="primary"
+					:disabled="isLoading"
+				>
+					<IconSvg
+						v-if="!isLoading"
+						icon="translate"
+						width="18px"
+						height="18px"
+					></IconSvg>
+					<template v-if="!isLoading">开始翻译</template>
+					<template v-else>翻译中...</template>
+				</n-button>
+			</div>
 		</div>
-		<div class="name-textBox-right">
-			<div class="name-textBox-right-content">
-				{{ translationResult }}
+
+		<div class="translation-panel result-panel">
+			<div class="panel-header">
+				<div class="header-title">
+					<div>翻译结果</div>
+				</div>
+			</div>
+
+			<div class="result-content" v-if="!isLoading">
+				<template v-if="parsedResult.original || parsedResult.translated">
+					<div class="result-section original">
+						<div class="section-header">
+							<div class="section-icon">原</div>
+							<h3>原文内容</h3>
+						</div>
+						<div class="section-body">{{ parsedResult.original }}</div>
+					</div>
+
+					<div class="result-divider">
+						<div class="divider-line"></div>
+						<div class="divider-icon">
+							<IconSvg icon="arrow-down" width="16px" height="16px"></IconSvg>
+						</div>
+						<div class="divider-line"></div>
+					</div>
+
+					<div class="result-section translated">
+						<div class="section-header">
+							<div class="section-icon">译</div>
+							<h3>翻译结果</h3>
+						</div>
+						<div class="section-body">{{ parsedResult.translated }}</div>
+					</div>
+				</template>
+
+				<div class="empty-result" v-else>
+					<div class="empty-illustration">
+						<!-- <div class="empty-icon">
+							<IconSvg icon="translate-empty" width="32px" height="32px"></IconSvg>
+						</div> -->
+					</div>
+					<p>翻译结果将显示在这里</p>
+					<small>请在左侧输入文本并点击"开始翻译"</small>
+				</div>
+			</div>
+
+			<div class="loading-container" v-else>
+				<div class="loading-animation">
+					<div class="loading-spinner"></div>
+					<div class="loading-spinner-shadow"></div>
+				</div>
+				<p>正在翻译中,请稍候...</p>
 			</div>
 		</div>
 	</div>
 </template>
 
 <script setup>
-import { ref, onMounted } from 'vue'
-import { NInput, NSelect, NButton } from 'naive-ui'
-import { modelList } from '@/api/model'
-import { translation } from '@/api/fanyi'
-const modelListData = ref([])
-
-
-
-const prompt = ref('')
-const sourceLanguage = ref('英文')
-const targetLanguage = ref('中文')
-const translationResult = ref('')
-const model = ref('')
-const options = ref([
-	// { label: '自动设别', value: '' },
-	{ label: '中文', value: '中文' },
-	{ label: '英文', value: '英文' },
-])
+import { t } from "@/locales";
+import { ref, onMounted } from "vue";
+import { NInput, NSelect, NButton, useMessage } from "naive-ui";
+import { Recognition } from "@/api";
+import { modelList } from "@/api/model";
+import { translation } from "@/api/fanyi";
+
+const modelListData = ref([]);
+const message = useMessage();
+const prompt = ref("");
+const targetLanguage = ref("中文");
+const model = ref("");
+const parsedResult = ref({ original: "", translated: "" });
+const isLoading = ref(false);
+
+// 语言选项配置
 const options1 = ref([
-	{ label: '中文', value: '中文' },
-	{ label: '英文', value: '英文' },
-])
+	{ label: "中文", value: "中文" },
+	{ label: "英文", value: "英文" },
+]);
+
+// 语音识别状态
+const st = ref({
+	micStart: false,
+});
 
 onMounted(() => {
-	getModelList()
-})
+	getModelList();
+});
 
 async function getModelList() {
-	const res = await modelList()
-	modelListData.value = res.data
-	model.value = modelListData.value[0].modelDescribe
+	try {
+		const res = await modelList();
+		modelListData.value = res.data;
+		model.value = modelListData.value[0]?.modelDescribe || "";
+	} catch (error) {
+		console.error("获取模型列表失败:", error);
+		message.error("获取模型列表失败");
+	}
 }
 
 async function handleTranslation() {
-	if (!prompt.value) {
+	if (!prompt.value.trim()) {
+		message.warning("请输入需要翻译的文本");
 		return;
 	}
-	const res = await translation({
-		prompt: prompt.value,
-		sourceLanguage: sourceLanguage.value,
-		targetLanguage: targetLanguage.value,
-		model: model.value,
-	})
-	translationResult.value = res
+
+	isLoading.value = true;
+	try {
+		const res = await translation({
+			prompt: prompt.value,
+			targetLanguage: targetLanguage.value,
+			model: model.value,
+		});
+
+		// 解析返回的 Markdown 格式结果
+		const matches = res.match(/\*\*原文\*\*\s*:(.*?)\s*\*\*翻译\*\*\s*:(.*)/s);
+		if (matches) {
+			parsedResult.value = {
+				original: matches[1].trim(),
+				translated: matches[2].trim(),
+			};
+		} else {
+			parsedResult.value = {
+				original: prompt.value,
+				translated: res,
+			};
+		}
+	} catch (error) {
+		console.error("翻译失败:", error);
+		message.error("翻译失败,请稍后重试");
+	} finally {
+		isLoading.value = false;
+	}
 }
+
+// 语音识别ASR
+const goASR = () => {
+	if (st.value.micStart) return; // 防止重复启动
+
+	const originalText = prompt.value;
+	const rec = new Recognition();
+
+	rec
+		.setOpt({
+			timeOut: 2000,
+			onStart: () => {
+				message.info(t("mj.micRec"));
+				st.value.micStart = true;
+			},
+			listener: (result) => {
+				prompt.value = originalText + result;
+			},
+			onEnd: () => {
+				message.info(t("mj.micRecEnd"));
+				st.value.micStart = false;
+				rec.recognition = null; // 清理实例,避免内存泄漏
+			},
+		})
+		.start();
+};
 </script>
 
 <style scoped lang="less">
-.name-textBox {
+.translation-container {
+	display: flex;
+	gap: 28px;
+	padding: 32px;
+	min-height: calc(100vh - 60px);
+	// background: linear-gradient(135deg, #f8faff 0%, #f0f4fa 100%);
+
+	@media (max-width: 1200px) {
+		flex-direction: column;
+		padding: 20px;
+		gap: 20px;
+	}
+}
+
+.translation-panel {
+	flex: 1;
+	display: flex;
+	flex-direction: column;
+	border-radius: 20px;
+	// background-color: #ffffff;
+	box-shadow: 0 10px 40px rgba(31, 45, 61, 0.08);
+	overflow: hidden;
+	transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
+	position: relative;
+
+	&:hover {
+		box-shadow: 0 15px 45px rgba(31, 45, 61, 0.12);
+	}
+
+	&::before {
+		content: "";
+		position: absolute;
+		top: 0;
+		left: 0;
+		right: 0;
+		height: 4px;
+		background: linear-gradient(90deg, #1890ff, #36cfc9);
+		z-index: 1;
+	}
+
+	&.input-panel::before {
+		background: linear-gradient(90deg, #1890ff, #36cfc9);
+	}
+
+	&.result-panel::before {
+		background: linear-gradient(90deg, #722ed1, #1890ff);
+	}
+}
+
+.panel-header {
 	display: flex;
+	justify-content: space-between;
+	align-items: center;
+	padding: 22px 28px;
+	border-bottom: 1px solid #f0f0f0;
 
-	:deep(.n-input) {
+	.header-title {
+		display: flex;
+		align-items: center;
+		gap: 12px;
 
-		.n-input__border,
-		.n-input__state-border {
-			border: none !important;
+		.title-icon {
+			display: flex;
+			align-items: center;
+			justify-content: center;
+			width: 36px;
+			height: 36px;
+			border-radius: 12px;
+			// background-color: rgba(24, 144, 255, 0.1);
+			color: #1890ff;
+
+			&.result-icon {
+				// background-color: rgba(114, 46, 209, 0.1);
+				color: #722ed1;
+			}
 		}
 
-		textarea {
-			min-height: 500px !important;
+		h2 {
+			margin: 0;
+			font-size: 18px;
+			font-weight: 600;
+			color: #262626;
+			letter-spacing: 0.5px;
 		}
 	}
+}
 
-	.name-textBox-left {
-		flex: 1;
-		height: calc(100vh - 100px);
-		border-radius: 10px;
+.voice-input {
+	display: flex;
+	align-items: center;
+	gap: 8px;
+	// padding: 10px 18px;
+	border-radius: 30px;
+	// background-color: #f5f7fa;
+	cursor: pointer;
+	transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
+	border: 1px solid transparent;
+
+	&:hover {
+		// background-color: #e6f7ff;
+		color: #1890ff;
+		border-color: rgba(24, 144, 255, 0.3);
+		transform: translateY(-2px);
+	}
+
+	&.active {
+		// background-color: #e6f7ff;
+		color: #1890ff;
+		border-color: rgba(24, 144, 255, 0.3);
+		animation: pulse 1.5s infinite;
+	}
 
-		padding: 10px;
+	span {
+		font-size: 14px;
+		font-weight: 500;
+	}
+}
 
-		.name-selectBox {
+@keyframes pulse {
+	0% {
+		box-shadow: 0 0 0 0 rgba(24, 144, 255, 0.4);
+	}
+	70% {
+		box-shadow: 0 0 0 10px rgba(24, 144, 255, 0);
+	}
+	100% {
+		box-shadow: 0 0 0 0 rgba(24, 144, 255, 0);
+	}
+}
+
+.input-wrapper {
+	flex: 1;
+	padding: 24px 28px 16px;
+	position: relative;
+
+	.input-decoration {
+		position: absolute;
+		bottom: 16px;
+		left: 28px;
+		right: 28px;
+		height: 60px;
+		// background: linear-gradient(
+		// 	to top,
+		// 	rgba(255, 255, 255, 1) 0%,
+		// 	rgba(255, 255, 255, 0) 100%
+		// );
+		pointer-events: none;
+		z-index: 1;
+		opacity: 0.8;
+	}
+}
+
+.text-input {
+	position: relative;
+	z-index: 0;
+
+	:deep(.n-input__textarea) {
+		font-size: 16px;
+		line-height: 1.8;
+		padding: 20px;
+		border-radius: 16px;
+		// border: 1px solid #e8e8e8;
+		// background-color: #fafafa;
+		resize: none;
+		transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
+
+		&:focus {
+			border-color: #1890ff;
+			box-shadow: 0 0 0 3px rgba(24, 144, 255, 0.15);
+			// background-color: #ffffff;
+		}
+	}
+}
+
+.translation-controls {
+	padding: 16px 28px 28px;
+	display: grid;
+	grid-template-columns: 1fr 1fr;
+	gap: 20px;
+
+	.control-group {
+		display: flex;
+		flex-direction: column;
+		gap: 10px;
+
+		label {
+			font-size: 14px;
+			font-weight: 500;
+			color: #595959;
+			margin-left: 4px;
+		}
+	}
+
+	:deep(.n-select) {
+		.n-base-selection {
+			border-radius: 12px;
+			transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
+			border: 1px solid #e8e8e8;
+
+			&:hover {
+				border-color: #1890ff;
+			}
+
+			&:focus,
+			&.n-base-selection--active {
+				border-color: #1890ff;
+				box-shadow: 0 0 0 3px rgba(24, 144, 255, 0.15);
+			}
+		}
+	}
+
+	.translate-button {
+		grid-column: span 2;
+		height: 52px;
+		font-size: 16px;
+		font-weight: 500;
+		border-radius: 12px;
+		margin-top: 12px;
+		background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%);
+		border: none;
+		transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
+		display: flex;
+		align-items: center;
+		justify-content: center;
+		gap: 10px;
+
+		&:hover {
+			transform: translateY(-2px);
+			box-shadow: 0 8px 20px rgba(24, 144, 255, 0.3);
+		}
+
+		&:active {
+			transform: translateY(0);
+		}
+	}
+}
+
+.result-content {
+	flex: 1;
+	padding: 24px 28px;
+	overflow-y: auto;
+}
+
+.result-section {
+	padding: 20px;
+	border-radius: 16px;
+	margin-bottom: 20px;
+	transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
+	border: 1px solid transparent;
+
+	&:hover {
+		transform: translateY(-2px);
+		box-shadow: 0 6px 16px rgba(0, 0, 0, 0.06);
+	}
+
+	&.original {
+		// background-color: #f9f9f9;
+
+		&:hover {
+			border-color: #e8e8e8;
+		}
+	}
+
+	&.translated {
+		// background-color: #f0f7ff;
+
+		&:hover {
+			border-color: rgba(24, 144, 255, 0.3);
+		}
+	}
+
+	.section-header {
+		display: flex;
+		align-items: center;
+		gap: 12px;
+		margin-bottom: 16px;
+
+		.section-icon {
 			display: flex;
-			justify-content: space-between;
-			margin-bottom: 10px;
+			align-items: center;
+			justify-content: center;
+			width: 32px;
+			height: 32px;
+			border-radius: 10px;
+			font-weight: bold;
+			font-size: 14px;
 
-			div {
-				flex: 1;
+			.original & {
+				// background-color: #f0f0f0;
+				color: #595959;
+			}
+
+			.translated & {
+				// background-color: #1890ff;
+				color: white;
+			}
+		}
+
+		h3 {
+			margin: 0;
+			font-size: 16px;
+			font-weight: 600;
+
+			.translated & {
+				color: #1890ff;
 			}
 		}
 	}
 
-	.name-textBox-right {
+	.section-body {
+		font-size: 15px;
+		line-height: 1.8;
+		white-space: pre-wrap;
+		word-break: break-word;
+		padding: 0 4px;
+
+		.translated & {
+			color: #1890ff;
+		}
+	}
+}
+
+.result-divider {
+	display: flex;
+	align-items: center;
+	margin: 28px 0;
+
+	.divider-line {
 		flex: 1;
-		min-height: 500px;
-		border-radius: 10px;
-		padding: 10px;
+		height: 1px;
+		background: linear-gradient(to right, transparent, #e8e8e8, transparent);
+	}
+
+	.divider-icon {
+		display: flex;
+		align-items: center;
+		justify-content: center;
+		width: 40px;
+		height: 40px;
+		border-radius: 50%;
+		background: linear-gradient(135deg, #1890ff, #096dd9);
+		color: white;
+		margin: 0 20px;
+		box-shadow: 0 6px 16px rgba(24, 144, 255, 0.3);
+		animation: float 3s ease-in-out infinite;
+	}
+}
 
+@keyframes float {
+	0%,
+	100% {
+		transform: translateY(0);
+	}
+	50% {
+		transform: translateY(-6px);
+	}
+}
+
+.empty-result {
+	display: flex;
+	flex-direction: column;
+	align-items: center;
+	justify-content: center;
+	height: 100%;
+	color: #8c8c8c;
+	text-align: center;
+	padding: 40px 0;
 
-		.name-selectBox {
-			margin-bottom: 10px;
+	.empty-illustration {
+		margin-bottom: 24px;
+		position: relative;
+
+		.empty-icon {
+			display: flex;
+			align-items: center;
+			justify-content: center;
+			width: 80px;
+			height: 80px;
+			border-radius: 50%;
+			// background-color: #f5f5f5;
+			color: #bfbfbf;
+			position: relative;
+			z-index: 1;
+
+			&::after {
+				content: "";
+				position: absolute;
+				width: 100%;
+				height: 100%;
+				border-radius: 50%;
+				// background-color: rgba(24, 144, 255, 0.05);
+				z-index: -1;
+				animation: ripple 2s linear infinite;
+			}
 		}
+	}
 
-		.name-textBox-right-content {
-			min-height: 500px;
-			border: 1px solid #ccc;
-			border-radius: 10px;
-			padding: 10px;
-			margin-top: 40px;
+	p {
+		font-size: 18px;
+		margin-bottom: 12px;
+		font-weight: 500;
+	}
+
+	small {
+		font-size: 14px;
+		opacity: 0.7;
+		max-width: 240px;
+		line-height: 1.6;
+	}
+}
+
+@keyframes ripple {
+	0% {
+		transform: scale(1);
+		opacity: 0.4;
+	}
+	100% {
+		transform: scale(1.5);
+		opacity: 0;
+	}
+}
+
+.loading-container {
+	display: flex;
+	flex-direction: column;
+	align-items: center;
+	justify-content: center;
+	height: 100%;
+	padding: 40px 0;
+
+	.loading-animation {
+		position: relative;
+		margin-bottom: 24px;
+
+		.loading-spinner {
+			width: 60px;
+			height: 60px;
+			border: 4px solid rgba(24, 144, 255, 0.1);
+			border-left-color: #1890ff;
+			border-radius: 50%;
+			animation: spin 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
+		}
+
+		.loading-spinner-shadow {
+			position: absolute;
+			top: 15px;
+			left: 0;
+			width: 60px;
+			height: 10px;
+			background: radial-gradient(
+				ellipse at center,
+				rgba(0, 0, 0, 0.1) 0%,
+				rgba(0, 0, 0, 0) 80%
+			);
+			border-radius: 50%;
+			z-index: -1;
+			animation: shadow 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
 		}
 	}
 
+	p {
+		font-size: 16px;
+		color: #595959;
+		font-weight: 500;
+	}
+}
 
+@keyframes spin {
+	to {
+		transform: rotate(360deg);
+	}
+}
 
-	div {
-		margin-right: 20px;
+@keyframes shadow {
+	0%,
+	100% {
+		transform: scale(1, 1);
+		opacity: 0.3;
 	}
+	50% {
+		transform: scale(1.2, 1);
+		opacity: 0.1;
+	}
+}
+
+:deep(.icon-svg) {
+	display: flex;
+	align-items: center;
+	justify-content: center;
 }
 </style>

+ 11 - 11
src/views/fanyi/index.vue

@@ -1,26 +1,26 @@
 <template>
 	<div class="fanyi-container">
 		<n-card>
-			<n-tabs animated :value="activeTab" @update:value="activeTab = $event">
+			<textComponent />
+			<!-- <n-tabs animated :value="activeTab" @update:value="activeTab = $event">
 				<n-tab-pane name="1" tab="文本">
 					<textComponent />
 				</n-tab-pane>
-				<!-- <n-tab-pane name="2" tab="文档">
-                    <documentComponent />
-                </n-tab-pane> -->
-			</n-tabs>
+				<n-tab-pane name="2" tab="文档">
+					<documentComponent />
+				</n-tab-pane>
+			</n-tabs> -->
 		</n-card>
 	</div>
 </template>
 
 <script setup>
-import { ref } from 'vue'
-import documentComponent from '@/views/fanyi/components/documentComponent.vue'
-import textComponent from '@/views/fanyi/components/textComponent.vue'
-import { NCard, NTabs, NTabPane } from 'naive-ui'
+import { ref } from "vue";
+import documentComponent from "@/views/fanyi/components/documentComponent.vue";
+import textComponent from "@/views/fanyi/components/textComponent.vue";
+import { NCard, NTabs, NTabPane } from "naive-ui";
 
-const activeTab = ref('1')
+const activeTab = ref("1");
 </script>
 
-
 <style scoped lang="less"></style>

+ 360 - 192
src/views/knowledge/annex.vue

@@ -1,225 +1,393 @@
 <script setup lang="ts">
-
-import { h, onMounted, ref } from 'vue'
-import { NButton, NDataTable, DrawerPlacement, NFormItem, NSpin, NSpace, UploadFileInfo, NUpload, NUploadDragger,
-  NP, useMessage, NModal
-} from 'naive-ui'
-import { SvgIcon } from '@/components/common'
-import { getToken } from '@/store/modules/auth/helper'
-import { getKnowledgeDetail, delKnowledgeDetail } from '@/api/knowledge'
+import { h, onMounted, ref } from "vue";
+import {
+	NButton,
+	NDataTable,
+	// DrawerPlacement,
+	NFormItem,
+	NSpin,
+	NSpace,
+	UploadFileInfo,
+	NUpload,
+	NUploadDragger,
+	NP,
+	useMessage,
+	NModal,
+	NCard,
+} from "naive-ui";
+import { SvgIcon } from "@/components/common";
+import { getToken } from "@/store/modules/auth/helper";
+import { getKnowledgeDetail, delKnowledgeDetail } from "@/api/knowledge";
 import to from "await-to-js";
-import { useRouter } from 'vue-router'
-import { t } from '@/locales';
+import { useRouter } from "vue-router";
+import { t } from "@/locales";
 
+const router = useRouter();
+const message = useMessage();
+const token = getToken();
+const headers = { Authorization: `Bearer ${token}` };
 
-const router = useRouter()
-const kid = ref<string>('');
-onMounted(() => { 
-  kid.value = router.currentRoute.value.query.kid as string
-  fetchData() 
+// 状态管理
+const kid = ref<string>("");
+const spinShow = ref(false);
+const showModal = ref(false);
+const tableData = ref([]);
+const pagination = ref({
+	page: 1,
+	pageSize: 10,
+	showSizePicker: true,
+	pageSizes: [10, 20, 30, 40],
+	onChange: (page: number) => {
+		pagination.value.page = page;
+	},
+	onUpdatePageSize: (pageSize: number) => {
+		pagination.value.pageSize = pageSize;
+		pagination.value.page = 1;
+	},
 });
 
-const token = getToken()
+// 生命周期钩子
+onMounted(() => {
+	kid.value = router.currentRoute.value.query.kid as string;
+	fetchData();
+});
 
-const message = useMessage()
+// 表格列定义
+const createColumns = () => {
+	return [
+		...(false
+			? [
+					{
+						title: "ID",
+						key: "id",
+						width: 80,
+						ellipsis: true,
+					},
+			  ]
+			: []),
+		{
+			title: t("annex.docId"),
+			key: "docId",
+		},
+		{
+			title: t("annex.docName"),
+			key: "docName",
+		},
+		{
+			title: t("annex.docType"),
+			key: "docType",
+		},
+		{
+			title: t("annex.action"),
+			key: "actions",
+			render: (row: any) => {
+				return [
+					h(
+						NButton,
+						{
+							onClick: () => delKnowledgeForm(row.docId),
+							style: "margin-left: 8px; color: #FF4500;",
+						},
+						{ default: () => t("annex.deleteAttachment") }
+					),
 
-const headers = {
-  Authorization: `Bearer ${token}`
-}
+					h(
+						NButton,
+						{
+							onClick: () => handleActionButtonClick(row, "action4"),
+							style: "margin-left: 8px; color: #32CD32;",
+						},
+						{ default: () => t("annex.knowledgeFragment") }
+					),
+				];
+			},
+		},
+	];
+};
+const columns = ref(createColumns());
 
-const spinShow = ref(false);
+// 数据获取
+const fetchData = async () => {
+	try {
+		const [err, result] = await to(getKnowledgeDetail(kid.value));
+		if (err) {
+			message.error(err.message);
+		} else {
+			tableData.value = result.rows;
+		}
+	} catch (error) {
+		console.error("Error fetching data:", error);
+	}
+};
 
-function handleFinish({event, file}: {
-  file: UploadFileInfo
-  event?: ProgressEvent
+// 上传相关函数
+function handleFinish({
+	file,
+	event,
+}: {
+	file: UploadFileInfo;
+	event?: ProgressEvent;
 }) {
-  message.success(t('annex.fileUploadSuccess'))
-  showModal.value = false
-  // 关闭加载条
-  spinShow.value = false
-  // 重新获取数据,更新表格
-  fetchData();
+	const xhr = event?.currentTarget as XMLHttpRequest;
+	if (xhr) {
+		try {
+			const responseData = JSON.parse(xhr.responseText);
+			if (responseData.code == 500) {
+				message.error(responseData.msg);
+			} else {
+				message.success(t("annex.fileUploadSuccess"));
+			}
+		} catch (error) {
+			console.error("解析响应数据失败:", error);
+		}
+	}
+	showModal.value = false;
+	spinShow.value = false;
+	fetchData();
 }
 
-function handleActionButtonClick(row: any, action1: string): void {
-  // 跳转到知识片段页面
-  router.push({ path: '/fragment/t', query: { docId: row.docId } });
+function handleBeforeUpload() {
+	spinShow.value = true;
 }
 
-// 上传之前触发事件
-function handleBeforeUpload(){
-    // 开启加载条
-    spinShow.value = true
+// 操作函数
+function handleActionButtonClick(row: any, action1: string): void {
+	router.push({ path: "/fragment/t", query: { docId: row.docId } });
 }
 
-const showModal = ref(false)
-// 定义一个激活抽屉的函数,接受一个 DrawerPlacement 类型的参数
-const activate = (place: DrawerPlacement) => {
-    showModal.value = true
+function activate() {
+	showModal.value = true;
 }
 
-// 删除附件 
+// 删除附件
 async function delKnowledgeForm(docId: string) {
-  // 发起一个请求
-  const req ={
-	  kid: kid.value, // 附件id
-    docId: docId
-  }
+	const req = {
+		kid: kid.value,
+		docId: docId,
+	};
 
-  const [err] = await to(delKnowledgeDetail(req));
+	const [err] = await to(delKnowledgeDetail(req));
 
-  if (err) {
-    message.error(t('annex.deletionFailed')) 
-  } else {
-    message.success(t('annex.attachmentDeletedSuccess'))
-  }
-  // 重新获取数据,更新表格
-  await fetchData();
+	if (err) {
+		message.error(t("annex.deletionFailed"));
+	} else {
+		message.success(t("annex.attachmentDeletedSuccess"));
+	}
+	await fetchData();
 }
 
-const pagination = ref({
-  page: 1,
-  pageSize: 10,
-  showSizePicker: true,
-  pageSizes: [10, 20, 30, 40],
-  onChange: (page: number) => {
-    pagination.value.page = page;
-  },
-  onUpdatePageSize: (pageSize: number) => {
-    pagination.value.pageSize = pageSize;
-    pagination.value.page = 1;
-  }
-});
+// 返回上一页
+function goBack() {
+	router.go(-1);
+}
+</script>
 
-const createColumns = () => {
-  return [
-  ...(false
-      ? [{
-          title: 'ID',
-          key: 'id',
-          width: 80,
-          ellipsis: true,
-        }]
-      : []),
-    {
-      title: t('annex.docId'),
-      key: 'docId'
-    },
-    {
-      title: t('annex.docName'),
-      key: 'docName'
-    },
-    {
-      title: t('annex.docType'),
-      key: 'docType'
-    },
-    {
-      title: t('annex.action'),
-      key: 'actions',
-      render: (row: any) => {
-        return [
-     
-          h(NButton, {
-            onClick: () => delKnowledgeForm(row.docId),
-            style: 'margin-left: 8px; color: #FF4500;',
-          }, { default: () => t('annex.deleteAttachment') }),
-
-          h(NButton, {
-            onClick: () => handleActionButtonClick(row, 'action4'),
-            style: 'margin-left: 8px; color: #32CD32;', 
-          }, { default: () => t('annex.knowledgeFragment') })
-        ];
-      }
-    }
-  ]
+<template>
+	<n-card class="annex-container" :bordered="false">
+		<div class="annex-header">
+			<n-button @click="goBack" class="back-button">
+				<svg-icon icon="mage:arrow-left" class="back-icon"></svg-icon>
+				返回
+			</n-button>
+			<n-button
+				@click="activate"
+				type="primary"
+				:bordered="false"
+				class="upload-button"
+			>
+				{{ $t("annex.uploadAttachment") }}
+			</n-button>
+		</div>
+
+		<n-data-table
+			:columns="columns"
+			:data="tableData"
+			:pagination="pagination"
+			:bordered="false"
+			class="annex-table"
+		/>
+	</n-card>
+
+	<n-modal
+		v-model:show="showModal"
+		:title="$t('annex.uploadAttachment')"
+		:auto-focus="false"
+		preset="dialog"
+	>
+		<n-space vertical>
+			<n-form-item>
+				<n-spin :show="spinShow">
+					<n-upload
+						class="annex-upload"
+						directory-dnd
+						action="/api/knowledge/attach/upload"
+						name="file"
+						:data="{ kid: kid }"
+						:on-before-upload="handleBeforeUpload"
+						:headers="headers"
+						@finish="handleFinish"
+						:max="1"
+					>
+						<n-upload-dragger class="upload-dragger">
+							<div class="upload-content">
+								<div class="upload-icon-container">
+									<SvgIcon icon="mage:upload" class="upload-icon"></SvgIcon>
+								</div>
+
+								<div class="upload-info">
+									<n-p class="upload-title">
+										{{ $t("annex.pleaseUploadFile") }}
+									</n-p>
+
+									<n-p class="upload-desc">
+										{{ $t("annex.supportedFormats") }}
+									</n-p>
+								</div>
+
+								<div class="upload-warnings">
+									<div class="warning-title"></div>
+									<n-p class="upload-warning">
+										<span class="warning-dot">•</span>
+										{{ $t("annex.friendlyReminder") }}
+									</n-p>
+									<n-p class="upload-warning">
+										<span class="warning-dot">•</span>
+										{{ $t("annex.largeFileWarning") }}
+									</n-p>
+									<n-p class="upload-warning">
+										<span class="warning-dot">•</span>
+										{{ $t("annex.utf8Warning") }}
+									</n-p>
+									<n-p class="upload-warning">
+										<span class="warning-dot">•</span>
+										{{ $t("annex.uploadCharacterLimit") }}
+									</n-p>
+								</div>
+							</div>
+						</n-upload-dragger>
+					</n-upload>
+				</n-spin>
+			</n-form-item>
+		</n-space>
+	</n-modal>
+</template>
+
+<style scoped>
+.annex-container {
+	height: 100%;
+	border-radius: 20px;
+	box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
 }
 
-const tableData = ref([]);
-const fetchData = async () => {
-  try {
-    // 发起一个请求
-    const [err, result] = await to(getKnowledgeDetail(kid.value));
-    console.log("result===", result)
-    if (err) {
-      message.error(err.message)
-    } else {
-      tableData.value = result.rows;
-    }
-  } catch (error) {
-    console.error('Error fetching data:', error);
-  }
-};
+.annex-header {
+	display: flex;
+	justify-content: space-between;
+	margin-bottom: 10px;
+	padding: 8px 0;
+}
 
-const columns = ref(createColumns());
+.back-button {
+	display: flex;
+	align-items: center;
+	font-weight: 500;
+	padding: 8px 16px;
+	border-radius: 6px;
+}
 
-</script>
-<template>
+.back-icon {
+	margin-right: 4px;
+	font-size: 1rem;
+}
 
+.upload-button {
+	font-weight: 500;
+	padding: 8px 16px;
+	border-radius: 6px;
+}
 
+.annex-table {
+	border-radius: 8px;
+	overflow: hidden;
+}
 
-<div class="flex h-full table-box" style="border-bottom-left-radius: 20px;"> 
-    <main class="flex-1 overflow-hidden h-full annex-main">
-      <div style="display: flex; justify-content: flex-start; margin:10px;border-top-left-radius: 20px;" class="know-header">
-        <n-button @click="activate('right')" type="primary" :bordered="false" class="success-button">
-          {{ $t('annex.uploadAttachment') }}
-        </n-button>
-      </div>
-
-      <n-data-table 
-        :columns="columns"
-        :data="tableData"
-        :pagination="pagination"
-        :bordered="false"
-      />
-    </main>
-</div>
-
-<n-modal class="annex-modal" v-model:show="showModal" :title="$t('annex.uploadAttachment')" :auto-focus="false" preset="card"
-         style="width: 100%; max-width: 540px; background-color: #f8f9fa;">
-  <n-space vertical>
-    <n-form-item>
-      <n-spin :show="spinShow">
-        <n-upload
-          class="annex-upload"
-          directory-dnd
-          action="/api/knowledge/attach/upload"
-          name="file"
-          :data="{ kid: kid}"
-          :on-before-upload="handleBeforeUpload"
-          :headers="headers"
-          @finish="handleFinish"
-          :max="1">
-          <n-upload-dragger style="padding: 20px; border: 1px dashed #d9d9d9; border-radius: 8px; text-align: left;">
-            <div style="display: flex; justify-content: center; align-items: center; margin-bottom: 12px; height: 50px;">
-              <SvgIcon icon="mage:upload" class="text-3xl"></SvgIcon>
-            </div>
-     
-            <n-p style="font-size: 16px; color: #B3B7B9; text-align: center;">
-               {{ $t('annex.pleaseUploadFile') }}
-            </n-p>
-
-            <n-p style="font-size: 16px; color: #B3B7B9; text-align: center;">
-              {{ $t('annex.supportedFormats') }}
-            </n-p>
-
-            <n-p depth="3" style="margin: 8px 0 0 0; color: #fa541c;">
-              {{ $t('annex.friendlyReminder') }}
-            </n-p>
-            <n-p depth="3" style="margin: 8px 0 0 0; color: #fa541c;">
-              {{ $t('annex.largeFileWarning') }}
-            </n-p>
-            <n-p depth="3" style="margin: 8px 0 0 0; color: #fa541c;">
-              {{ $t('annex.utf8Warning') }}
-            </n-p>
-            <n-p depth="3" style="margin: 8px 0 0 0; color: #fa541c;">
-              {{ $t('annex.uploadCharacterLimit') }}
-            </n-p>
-          </n-upload-dragger>
-        </n-upload>
-      </n-spin>
-    </n-form-item>
-  </n-space>
-  <br>
-</n-modal>
+.annex-modal {
+	width: 100%;
+	max-width: 520px;
+}
 
-</template>
+.upload-dragger {
+	padding: 32px;
+	border: 2px dashed #e0e0e0;
+	border-radius: 12px;
+	text-align: center;
+	transition: all 0.3s;
+}
+
+.upload-dragger:hover {
+	border-color: #1890ff;
+}
+
+.upload-content {
+	display: flex;
+	flex-direction: column;
+	align-items: center;
+}
+
+.upload-icon-container {
+	display: flex;
+	justify-content: center;
+	align-items: center;
+	width: 64px;
+	height: 64px;
+	border-radius: 50%;
+	margin-bottom: 16px;
+}
+
+.upload-icon {
+	font-size: 2.5rem;
+	color: #1890ff;
+}
+
+.upload-info {
+	margin-bottom: 24px;
+}
+
+.upload-title {
+	font-size: 18px;
+	font-weight: 500;
+	margin-bottom: 8px;
+}
+
+.upload-desc {
+	font-size: 14px;
+	margin-bottom: 8px;
+}
+
+.upload-warnings {
+	width: 100%;
+	border-radius: 8px;
+	padding: 16px;
+	text-align: left;
+	border-left: 4px solid #faad14;
+}
+
+.warning-title {
+	font-weight: 500;
+	color: #d46b08;
+	margin-bottom: 8px;
+	font-size: 15px;
+}
+
+.upload-warning {
+	margin: 6px 0;
+	color: #873800;
+	font-size: 13px;
+	display: flex;
+	align-items: flex-start;
+}
+
+.warning-dot {
+	margin-right: 6px;
+	color: #fa8c16;
+}
+</style>

+ 108 - 84
src/views/knowledge/fragment.vue

@@ -1,109 +1,133 @@
 <script setup lang="ts">
-
-import { onMounted, ref} from 'vue'
-import { NButton, NDataTable, useMessage } from 'naive-ui'
-import { getfragmentList } from '@/api/knowledge'
+import { onMounted, ref } from "vue";
+import { NButton, NDataTable, useMessage } from "naive-ui";
+import { getfragmentList } from "@/api/knowledge";
 import to from "await-to-js";
-import { useRouter } from 'vue-router'
-import { t } from '@/locales';
+import { useRouter } from "vue-router";
+import { t } from "@/locales";
+import { SvgIcon } from "@/components/common";
 
-const router = useRouter()
+const router = useRouter();
 
-const docId = ref<string>('');
+const docId = ref<string>("");
 
-onMounted(() => { 
-    docId.value = router.currentRoute.value.query.docId as string
-    fetchData() 
+onMounted(() => {
+	docId.value = router.currentRoute.value.query.docId as string;
+	fetchData();
 });
 
-const message = useMessage()
+const message = useMessage();
 
 const goBack = () => {
-  router.go(-1);
+	router.go(-1);
 };
 
-
 const pagination = ref({
-  page: 1,
-  pageSize: 10,
-  showSizePicker: true,
-  pageSizes: [10, 20, 30, 40],
-  onChange: (page: number) => {
-    pagination.value.page = page;
-  },
-  onUpdatePageSize: (pageSize: number) => {
-    pagination.value.pageSize = pageSize;
-    pagination.value.page = 1;
-  }
+	page: 1,
+	pageSize: 10,
+	showSizePicker: true,
+	pageSizes: [10, 20, 30, 40],
+	onChange: (page: number) => {
+		pagination.value.page = page;
+	},
+	onUpdatePageSize: (pageSize: number) => {
+		pagination.value.pageSize = pageSize;
+		pagination.value.page = 1;
+	},
 });
 
-
-
 const createColumns = () => {
-    return [
-        ...(false
-            ? [{
-                title: 'ID',
-                key: 'id',
-                width: 80,
-                ellipsis: true,
-            }]
-            : []),
-        {
-            title: t('knowledge.no'),
-            key: 'fid'
-        },
-
-        {
-            title: t('knowledge.content'),
-            key: 'content',
-            width: 800,
-            ellipsis: {
-                tooltip: {
-                    contentStyle: 'max-width:300px'
-                }
-            }
-        },
-    ]
-}
+	return [
+		...(false
+			? [
+					{
+						title: "ID",
+						key: "id",
+						width: 80,
+						ellipsis: true,
+					},
+			  ]
+			: []),
+		{
+			title: t("knowledge.no"),
+			key: "fid",
+		},
+
+		{
+			title: t("knowledge.content"),
+			key: "content",
+			width: 800,
+			ellipsis: {
+				tooltip: {
+					contentStyle: "max-width:300px",
+				},
+			},
+		},
+	];
+};
 
 const tableData = ref([]);
 const fetchData = async () => {
-    try {
-        // 发起一个请求
-        const [err, result] = await to(getfragmentList(docId.value));
-        console.log("fragmenresult===", result)
-        if (err) {
-            message.error(err.message)
-        } else {
-            tableData.value = result.rows;
-        }
-    } catch (error) {
-        console.error('Error fetching data:', error);
-    }
+	try {
+		// 发起一个请求
+		const [err, result] = await to(getfragmentList(docId.value));
+		console.log("fragmenresult===", result);
+		if (err) {
+			message.error(err.message);
+		} else {
+			tableData.value = result.rows;
+		}
+	} catch (error) {
+		console.error("Error fetching data:", error);
+	}
 };
 
-
 const columns = ref(createColumns());
-
 </script>
 <template>
-  
-
-    <div class="flex h-full table-box" style="border-bottom-left-radius: 20px;"> 
-    <main class="flex-1 overflow-hidden h-full annex-main">
-      <div style="display: flex; justify-content: flex-start; margin:10px;border-top-left-radius: 20px;" class="know-header">
-        <n-button @click="goBack" type="primary" :bordered="false" class="success-button">
-            {{ $t('knowledge.return') }}
-        </n-button>
-      </div> 
+	<div class="flex h-full table-box" style="border-bottom-left-radius: 20px">
+		<main class="flex-1 overflow-hidden h-full annex-main p-4">
+			<div class="annex-header">
+				<n-button @click="goBack" class="back-button">
+					<svg-icon icon="mage:arrow-left" class="back-icon"></svg-icon>
+					返回
+				</n-button>
+			</div>
+			<n-data-table
+				:columns="columns"
+				:data="tableData"
+				:pagination="pagination"
+				:theme-overrides="{
+					thPaddingMedium: '12px 16px',
+					tdPaddingMedium: '12px 16px',
+					thTextColor: 'var(--n-text-color)',
+					tdColor: 'var(--n-color)',
+					borderColor: 'var(--n-border-color)',
+				}"
+				class="rounded-lg shadow-sm"
+				:scroll-x="1200"
+			/>
+		</main>
+	</div>
+</template>
 
-      <n-data-table 
-        :columns="columns"
-        :data="tableData"
-        :pagination="pagination"
-      />
-    </main>
-  </div>
+<style scoped>
+.annex-header {
+	display: flex;
+	justify-content: space-between;
+	margin-bottom: 10px;
+	padding: 8px 0;
+}
+.back-button {
+	display: flex;
+	align-items: center;
+	font-weight: 500;
+	padding: 8px 16px;
+	border-radius: 6px;
+}
 
-</template>
+.back-icon {
+	margin-right: 4px;
+	font-size: 1rem;
+}
+</style>

+ 494 - 143
src/views/knowledge/index.vue

@@ -1,180 +1,531 @@
 <script setup lang="ts">
-import { h, onMounted, ref } from 'vue';
-import { NButton, NDataTable, DrawerPlacement, NDrawer, NDrawerContent, NForm, NFormItem, NInput, NDivider, NSpace, useMessage } from 'naive-ui';
-import { createKnowledgeReq, getKnowledge, delKnowledge } from '@/api/knowledge';
-import to from 'await-to-js';
-import { useRouter } from 'vue-router';
-import { t } from '@/locales';
+import { h, onMounted, reactive, ref } from "vue";
+import {
+	NButton,
+	NDataTable,
+	DrawerPlacement,
+	NDrawer,
+	NDrawerContent,
+	NForm,
+	NFormItem,
+	NInput,
+	// NDivider,
+	NSpace,
+	useMessage,
+	NGrid,
+	NGi,
+	NSwitch,
+	NInputNumber,
+	NSelect,
+} from "naive-ui";
+import {
+	createKnowledgeReq,
+	getKnowledge,
+	delKnowledge,
+} from "@/api/knowledge";
+import to from "await-to-js";
+import { useRouter } from "vue-router";
+import { t } from "@/locales";
 
-onMounted(() => { fetchData() });
+onMounted(() => {
+	fetchData();
+});
 
 const router = useRouter();
 const message = useMessage();
 
 // 初始化表单数据对象
 const formValue = ref({
-  id: '', // 知识库id
-  kid: '', // 附件id
-  uid: '', // 用户id  
-  kname: '', // 知识库名称
-  description: '', // 知识库描述 
+	id: "", // 知识库id
+	kid: "", // 附件id
+	uid: "", // 用户id
+	kname: "", // 知识库名称
+	share: "0", // 是否分享
+	description: "", // 知识库描述
+	knowledgeSeparator: "", // 知识分隔符
+	questionSeparator: "", // 提问分隔符
+	overlapChar: 50, // 重叠字符数
+	retrieveLimit: 3, // 知识库中检索的条数
+	textBlockSize: 500, // 文本块大小
+	vector: "", //  向量库
+	vectorModel: "", //  向量模型
 });
 
 async function submitForm() {
-  // 关闭弹框
-  active.value = false;
-  // 发起一个请求
-  const [err, result] = await to(createKnowledgeReq(formValue.value));
-  console.log("result===", result);
-  if (err) {
-    message.error(err.message);
-  } else {
-    message.success("添加成功");
-    // 重新获取数据,更新表格
-    await fetchData();
-  }
+	// 关闭弹框
+	active.value = false;
+	// 发起一个请求
+	const [err, result] = await to(createKnowledgeReq(formValue.value));
+	console.log("result===", result);
+	if (err) {
+		message.error(err.message);
+	} else {
+		message.success("添加成功");
+		// 重新获取数据,更新表格
+		await fetchData();
+	}
 }
 
 async function delKnowledgeForm(kid: string) {
-  // 发起一个请求
-  const req = {
-    kid: kid // 附件id
-  };
-  const [err] = await to(delKnowledge(req));
-  if (err) {
-    message.error("操作失败!");
-  } else {
-    message.success("删除成功!");
-  }
-  // 重新获取数据,更新表格
-  await fetchData();
+	// 发起一个请求
+	const req = {
+		kid: kid, // 附件id
+	};
+	const [err] = await to(delKnowledge(req));
+	if (err) {
+		message.error("操作失败!");
+	} else {
+		message.success("删除成功!");
+	}
+	// 重新获取数据,更新表格
+	await fetchData();
 }
 
 function handleActionButtonClick(row: any, action1: string): void {
-  // 跳转到知识库附件页面
-  router.push({ path: '/annex/t', query: { kid: row.id } });
+	// 跳转到知识库附件页面
+	router.push({ path: "/annex/t", query: { kid: row.id } });
+}
+
+function handleUpdateValue(value: string) {
+	formValue.value.share = value;
 }
 
 // 定义一个激活抽屉的函数,接受一个 DrawerPlacement 类型的参数
 const activate = (place: DrawerPlacement) => {
-  active.value = true;
-  placement.value = place;
-}
+	active.value = true;
+	placement.value = place;
+};
 
 // 使用 ref 来创建响应式变量
 const active = ref(false);
-const placement = ref<DrawerPlacement>('right');
+const placement = ref<DrawerPlacement>("right");
+
+const getVector = reactive([
+	{ label: "weaviate", value: "weaviate" },
+	{ label: "milvus", value: "milvus" },
+]);
+
+const getVectorModel = reactive([
+	{ label: "text-embedding-3-small", value: "text-embedding-3-small" },
+]);
 
 const createColumns = () => {
-  return [
-    ...(false
-      ? [{
-          title: "ID",
-          key: 'id',
-          width: 80,
-          ellipsis: true,
-        }]
-      : []),
-    {
-      title: t('knowledge.number'),
-      key: 'kid',
-      width: 200
-    },
-    {
-      title: t('knowledge.name'),
-      key: 'kname',
-      width: 200
-    },
-    {
-      title: t('knowledge.description'),
-      key: 'description',
-      width: 200
-    },
-    {
-      title: t('knowledge.actions'),
-      key: 'actions',
-      width: 200,
-      render: (row: any) => {
-        return [
-          h(NButton, {
-            onClick: () => delKnowledgeForm(row.kid),
-            style: 'margin-left: 8px; color: #FF4500;',
-            class: 'table-button',
-            bordered: false,
-          }, { default: () => t('knowledge.delete') }),
-
-          h(NButton, {
-            onClick: () => handleActionButtonClick(row, 'action3'),
-            style: 'margin-left: 8px; color: #32CD32;', 
-            class: 'table-button',
-            bordered: false,
-          }, { default: () => t('knowledge.attachment') }),
-        ];
-      }
-    }
-  ];
-}
+	return [
+		...(false
+			? [
+					{
+						title: "ID",
+						key: "id",
+						width: 80,
+						ellipsis: true,
+					},
+			  ]
+			: []),
+		{
+			title: t("knowledge.number"),
+			key: "kid",
+			width: 200,
+		},
+		{
+			title: t("knowledge.name"),
+			key: "kname",
+			width: 200,
+		},
+		{
+			title: t("knowledge.description"),
+			key: "description",
+			width: 200,
+		},
+		{
+			title: t("knowledge.actions"),
+			key: "actions",
+			width: 200,
+			render: (row: any) => {
+				return [
+					h(
+						NButton,
+						{
+							onClick: () => delKnowledgeForm(row.kid),
+							style: "margin-left: 8px; color: #FF4500;",
+							class: "table-button",
+							bordered: false,
+						},
+						{ default: () => t("knowledge.delete") }
+					),
+
+					h(
+						NButton,
+						{
+							onClick: () => handleActionButtonClick(row, "action3"),
+							style: "margin-left: 8px; color: #32CD32;",
+							class: "table-button",
+							bordered: false,
+						},
+						{ default: () => t("knowledge.attachment") }
+					),
+				];
+			},
+		},
+	];
+};
 
 const tableData = ref([]);
 
 const fetchData = async () => {
-  try {
-    // 发起一个请求
-    const [err, result] = await to(getKnowledge());
-    console.log("result===", result);
-    if (err) {
-      message.error(err.message);
-    } else {
-      tableData.value = result.rows;
-    }
-  } catch (error) {
-    console.error('Error fetching data:', error);
-  }
+	try {
+		// 发起一个请求
+		const [err, result] = await to(getKnowledge());
+		console.log("result===", result);
+		if (err) {
+			message.error(err.message);
+		} else {
+			tableData.value = result.rows;
+		}
+	} catch (error) {
+		console.error("Error fetching data:", error);
+	}
 };
 
 const columns = ref(createColumns());
 </script>
 
 <template>
-<div class="flex h-full table-box" style="border-bottom-left-radius: 20px;"> 
-    <main class="flex-1 overflow-hidden h-full">
-      <div style="display: flex; justify-content: flex-start; " class="know-header">
-        <n-button @click="activate('right')" type="primary" :bordered="false" class="success-button">
-          {{ $t('knowledge.createKnowledgeBase') }}
-        </n-button>
-      </div>
-      <n-data-table striped :bordered="false" :columns="columns" :data="tableData" />
-    </main>
-</div>
-
-<n-drawer class="knowledge-draw" v-model:show="active" :width="540" :placement="placement">
-      <n-drawer-content :title="$t('knowledge.createKnowledgeBase')">
-          {{ $t('knowledge.createYourKnowledgeBase') }}
-          <n-divider />
-          <n-space vertical>
-            <n-form ref="formRef">
-              <n-form-item
-                :label="$t('knowledge.knowledgeName')" path="formValue.kname">
-                <n-input v-model:value="formValue.kname" :placeholder="$t('knowledge.enterKnowledgeName')" />
-              </n-form-item>
-              <n-form-item :label="$t('knowledge.knowledgeDescription')" path="formValue.description">
-                <n-input maxlength="1000"
-                  type="textarea"
-                  v-model:value="formValue.description"
-                  :placeholder="$t('knowledge.enterKnowledgeDescription')" 
-                />
-              </n-form-item>
-
-              <n-col :span="24">
-                <div style="display: flex; justify-content: flex-end">
-                  <n-button @click="submitForm" :bordered="false" type="primary" class="draw-button">
-                    {{ $t('knowledge.add') }}
-                  </n-button>
-                </div>
-              </n-col>
-
-            </n-form>
-          </n-space>
-      </n-drawer-content>
-</n-drawer>
+	<div class="knowledge-container">
+		<div class="knowledge-header">
+			<n-button
+				@click="activate('right')"
+				type="primary"
+				:bordered="false"
+				class="create-button"
+			>
+				{{ $t("knowledge.createKnowledgeBase") }}
+			</n-button>
+		</div>
+
+		<div class="knowledge-table-wrapper">
+			<n-data-table
+				striped
+				:bordered="false"
+				:columns="columns"
+				:data="tableData"
+				class="knowledge-table"
+			/>
+		</div>
+	</div>
+
+	<n-drawer
+		class="knowledge-drawer"
+		v-model:show="active"
+		:width="800"
+		:placement="placement"
+		display-directive="show"
+		:mask-closable="false"
+	>
+		<n-drawer-content
+			:title="$t('knowledge.createKnowledgeBase')"
+			class="drawer-content"
+			closable
+		>
+			<n-space vertical>
+				<n-form
+					ref="formRef"
+					:label-width="100"
+					:model="formValue"
+					class="knowledge-form"
+					label-placement="left"
+				>
+					<n-grid :cols="24" :x-gap="24" :y-gap="16">
+						<n-gi :span="12">
+							<n-form-item label="知识库名称" required>
+								<n-input
+									v-model:value="formValue.kname"
+									placeholder="请输入知识库名称"
+									clearable
+								/>
+							</n-form-item>
+						</n-gi>
+
+						<n-gi :span="12">
+							<n-form-item label="分隔符">
+								<n-input
+									v-model:value="formValue.knowledgeSeparator"
+									placeholder="请输入知识分隔符"
+									clearable
+								/>
+							</n-form-item>
+						</n-gi>
+
+						<n-gi :span="12">
+							<n-form-item label="检索条数" required>
+								<n-input-number
+									v-model:value="formValue.retrieveLimit"
+									placeholder="请输入检索条数"
+									:min="1"
+									:max="10"
+									class="full-width"
+								/>
+							</n-form-item>
+						</n-gi>
+
+						<n-gi :span="12">
+							<n-form-item label="文本块大小" required>
+								<n-input-number
+									v-model:value="formValue.textBlockSize"
+									placeholder="请输入文本块大小"
+									:min="100"
+									:max="2000"
+									class="full-width"
+								/>
+							</n-form-item>
+						</n-gi>
+
+						<n-gi :span="12">
+							<n-form-item label="重叠字符">
+								<n-input-number
+									v-model:value="formValue.overlapChar"
+									placeholder="请输入重叠字符数"
+									:min="0"
+									:max="200"
+									class="full-width"
+								/>
+							</n-form-item>
+						</n-gi>
+
+						<!-- <n-gi :span="12">
+							<n-form-item label="向量库" required>
+								<n-select
+									:options="getVector"
+									v-model:value="formValue.vector"
+									placeholder="请选择向量库"
+									clearable
+								></n-select>
+							</n-form-item>
+						</n-gi> -->
+
+						<!-- <n-gi :span="12">
+							<n-form-item label="提问分割符">
+								<n-input
+									v-model:value="formValue.questionSeparator"
+									placeholder="请输入提问分割符"
+									clearable
+								/>
+							</n-form-item>
+						</n-gi> -->
+
+						<n-gi :span="12">
+							<n-form-item label="向量模型" required>
+								<n-select
+									:options="getVectorModel"
+									v-model:value="formValue.vectorModel"
+									placeholder="请选择向量模型"
+									clearable
+								></n-select>
+							</n-form-item>
+						</n-gi>
+
+						<n-gi :span="24">
+							<n-form-item :label="$t('knowledge.knowledgeDescription')">
+								<n-input
+									maxlength="1000"
+									type="textarea"
+									v-model:value="formValue.description"
+									:placeholder="$t('knowledge.enterKnowledgeDescription')"
+									:autosize="{ minRows: 3, maxRows: 5 }"
+									show-count
+								/>
+							</n-form-item>
+						</n-gi>
+
+						<!-- <n-gi :span="24">
+							<n-form-item label="是否公开" label-placement="left">
+								<n-switch
+									size="large"
+									checked-value="1"
+									unchecked-value="0"
+									@update:value="handleUpdateValue"
+								>
+									<template #checked>是</template>
+									<template #unchecked>否</template>
+								</n-switch>
+							</n-form-item>
+						</n-gi> -->
+					</n-grid>
+				</n-form>
+			</n-space>
+
+			<template #footer>
+				<div>
+					<n-button
+						@click="active = false"
+						:bordered="true"
+						style="margin-right: 10px"
+					>
+						取消
+					</n-button>
+					<n-button @click="submitForm" :bordered="false" type="primary">
+						{{ $t("knowledge.add") }}
+					</n-button>
+				</div>
+			</template>
+		</n-drawer-content>
+	</n-drawer>
 </template>
+
+<style scoped>
+.knowledge-container {
+	display: flex;
+	flex-direction: column;
+	height: 100%;
+	padding: 20px;
+	border-radius: 8px;
+}
+
+.knowledge-header {
+	display: flex;
+	justify-content: flex-start;
+	margin-bottom: 20px;
+}
+
+.create-button {
+	padding: 10px 20px;
+	font-weight: 500;
+	font-size: 14px;
+	border-radius: 6px;
+	box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
+	transition: all 0.3s ease;
+	height: 40px;
+	min-width: 120px;
+}
+
+.create-button:hover {
+	transform: translateY(-2px);
+	box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
+}
+
+.knowledge-table-wrapper {
+	flex: 1;
+	overflow: hidden;
+	border-radius: 8px;
+	box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
+}
+
+.knowledge-table {
+	--n-td-color: transparent;
+	--n-td-color-hover: rgba(0, 0, 0, 0.02);
+	--n-td-text-color: #333;
+	--n-border-color: #f0f0f0;
+}
+
+.knowledge-drawer {
+	--n-body-padding: 24px;
+}
+
+.drawer-content {
+	padding: 0;
+}
+
+.drawer-header {
+	padding: 16px 24px;
+	border-bottom: 1px solid #f0f0f0;
+}
+
+.drawer-title {
+	margin: 0;
+	font-size: 18px;
+	font-weight: 600;
+	color: #333;
+}
+
+.drawer-description {
+	color: #666;
+	margin: 16px 24px;
+	font-size: 14px;
+	line-height: 1.6;
+}
+
+.drawer-footer {
+	display: flex;
+	justify-content: flex-end;
+	gap: 12px;
+	padding: 16px 24px;
+	border-top: 1px solid #f0f0f0;
+}
+
+.knowledge-form {
+	padding: 0 24px 24px;
+}
+
+.knowledge-form :deep(.n-form-item-label) {
+	font-weight: 500;
+	/* color: #333; */
+}
+
+.knowledge-form :deep(.n-form-item-feedback-wrapper) {
+	min-height: 18px;
+}
+
+.knowledge-form :deep(.n-input),
+.knowledge-form :deep(.n-input-number),
+.knowledge-form :deep(.n-select) {
+	width: 100%;
+	border-radius: 6px;
+	height: 40px;
+}
+
+.knowledge-form :deep(.n-input .n-input__input-el),
+.knowledge-form :deep(.n-input-number-input),
+.knowledge-form :deep(.n-base-selection) {
+	height: 40px;
+	font-size: 14px;
+}
+
+.knowledge-form :deep(.n-input:hover),
+.knowledge-form :deep(.n-input-number:hover),
+.knowledge-form :deep(.n-select:hover) {
+	border-color: #18a058;
+}
+
+.knowledge-form :deep(.n-input:focus),
+.knowledge-form :deep(.n-input-number:focus),
+.knowledge-form :deep(.n-select:focus) {
+	border-color: #18a058;
+	box-shadow: 0 0 0 2px rgba(24, 160, 88, 0.2);
+}
+
+.knowledge-form :deep(.n-input-number) {
+	width: 100%;
+}
+
+.knowledge-form :deep(.n-button) {
+	height: 40px;
+	font-size: 14px;
+}
+
+.cancel-button {
+	padding: 10px 20px;
+	border-radius: 6px;
+	transition: all 0.3s ease;
+	height: 40px;
+	min-width: 100px;
+	font-size: 14px;
+}
+
+.cancel-button:hover {
+}
+
+.draw-button {
+	padding: 10px 20px;
+	font-weight: 500;
+	border-radius: 6px;
+	box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
+	transition: all 0.3s ease;
+	height: 40px;
+	min-width: 100px;
+	font-size: 14px;
+}
+
+.draw-button:hover {
+	transform: translateY(-2px);
+	box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
+}
+</style>

+ 4 - 4
src/views/login/index.vue

@@ -26,10 +26,10 @@ let loginLoading = ref(false);
 async function handleValidateButtonClick(e: MouseEvent) {
 	e.preventDefault();
 	const { username, password } = user.value;
-	if (!validateAccount(username)) {
-		message.error(t("login.accountFormatError"));
-		return;
-	}
+	// if (!validateAccount(username)) {
+	// 	message.error(t("login.accountFormatError"));
+	// 	return;
+	// }
 	if (username && password) {
 		loginLoading.value = true;
 		const [err] = await to(userStore.userLogin(user.value));

+ 483 - 476
src/views/mj/aiGptInput.vue

@@ -3,29 +3,29 @@ import { ref, computed, watch } from "vue";
 import { useBasicLayout } from "@/hooks/useBasicLayout";
 import { t } from "@/locales";
 import {
-  NInput,
-  NButton,
-  useMessage,
-  NImage,
-  NTooltip,
-  NAutoComplete,
-  NTag,
-  NPopover,
-  NModal,
-  NDropdown,
+	NInput,
+	NButton,
+	useMessage,
+	NImage,
+	NTooltip,
+	NAutoComplete,
+	NTag,
+	NPopover,
+	NModal,
+	NDropdown,
 } from "naive-ui";
 import { SvgIcon, PromptStore } from "@/components/common";
 import {
-  canVisionModel,
-  GptUploader,
-  mlog,
-  upImg,
-  getFileFromClipboard,
-  isFileMp3,
-  countTokens,
-  checkDisableGpt4,
-  Recognition,
-  chatSetting,
+	canVisionModel,
+	GptUploader,
+	mlog,
+	upImg,
+	getFileFromClipboard,
+	isFileMp3,
+	countTokens,
+	checkDisableGpt4,
+	Recognition,
+	chatSetting,
 } from "@/api";
 import { gptConfigStore, homeStore, useChatStore } from "@/store";
 import { AutoCompleteOptions } from "naive-ui/es/auto-complete/src/interface";
@@ -41,29 +41,29 @@ const route = useRoute();
 const chatStore = useChatStore();
 const emit = defineEmits(["update:modelValue", "export", "handleClear"]);
 const props = defineProps<{
-  modelValue: string;
-  disabled?: boolean;
-  searchOptions?: AutoCompleteOptions;
-  renderOption?: RenderLabel;
+	modelValue: string;
+	disabled?: boolean;
+	searchOptions?: AutoCompleteOptions;
+	renderOption?: RenderLabel;
 }>();
 const fsRef = ref();
 const st = ref<{
-  fileBase64: string[];
-  isLoad: number;
-  isShow: boolean;
-  showMic: boolean;
-  micStart: boolean;
+	fileBase64: string[];
+	isLoad: number;
+	isShow: boolean;
+	showMic: boolean;
+	micStart: boolean;
 }>({
-  fileBase64: [],
-  isLoad: 0,
-  isShow: false,
-  showMic: false,
-  micStart: false,
+	fileBase64: [],
+	isLoad: 0,
+	isShow: false,
+	showMic: false,
+	micStart: false,
 });
 const { isMobile } = useBasicLayout();
 const placeholder = computed(() => {
-  if (isMobile.value) return t("chat.placeholderMobile");
-  return t("chat.placeholder"); //可输入说点什么,也可贴截图或拖拽文件
+	if (isMobile.value) return t("chat.placeholderMobile");
+	return t("chat.placeholder"); //可输入说点什么,也可贴截图或拖拽文件
 });
 
 const { uuid } = route.params as { uuid: string };
@@ -73,56 +73,56 @@ const nGptStore = ref(chatSet.getGptConfig());
 const dataSources = computed(() => chatStore.getChatByUuid(+uuid));
 
 watch(
-  () => gptConfigStore.myData,
-  () => (nGptStore.value = chatSet.getGptConfig()),
-  { deep: true }
+	() => gptConfigStore.myData,
+	() => (nGptStore.value = chatSet.getGptConfig()),
+	{ deep: true }
 );
 watch(
-  () => homeStore.myData.act,
-  (n) => n == "saveChat" && (nGptStore.value = chatSet.getGptConfig()),
-  { deep: true }
+	() => homeStore.myData.act,
+	(n) => n == "saveChat" && (nGptStore.value = chatSet.getGptConfig()),
+	{ deep: true }
 );
 const handleSubmit = () => {
-  if (mvalue.value == "") return;
-  if (checkDisableGpt4(gptConfigStore.myData.model)) {
-    ms.error(t("mj.disableGpt4"));
-    return false;
-  }
-  if (homeStore.myData.isLoader) {
-    return;
-  }
-  let obj = {
-    prompt: mvalue.value,
-    fileBase64: st.value.fileBase64,
-  };
-  homeStore.setMyData({ act: "gpt.submit", actData: obj });
-  mvalue.value = "";
-  st.value.fileBase64 = [];
-  return false;
+	if (mvalue.value == "") return;
+	if (checkDisableGpt4(gptConfigStore.myData.model)) {
+		ms.error(t("mj.disableGpt4"));
+		return false;
+	}
+	if (homeStore.myData.isLoader) {
+		return;
+	}
+	let obj = {
+		prompt: mvalue.value,
+		fileBase64: st.value.fileBase64,
+	};
+	homeStore.setMyData({ act: "gpt.submit", actData: obj });
+	mvalue.value = "";
+	st.value.fileBase64 = [];
+	return false;
 };
 const ms = useMessage();
 const mvalue = computed({
-  get() {
-    return props.modelValue;
-  },
-  set(value) {
-    emit("update:modelValue", value);
-  },
+	get() {
+		return props.modelValue;
+	},
+	set(value) {
+		emit("update:modelValue", value);
+	},
 });
 function selectFile(input: any) {
-  const file = input.target.files[0];
-  upFile(file);
+	const file = input.target.files[0];
+	upFile(file);
 }
 
 const myToken = ref({ remain: 0, modelTokens: "4k" });
 const funt = async () => {
-  const d = await countTokens(
-    dataSources.value,
-    mvalue.value,
-    chatStore.active ?? 1002
-  );
-  myToken.value = d;
-  return d;
+	const d = await countTokens(
+		dataSources.value,
+		mvalue.value,
+		chatStore.active ?? 1002
+	);
+	myToken.value = d;
+	return d;
 };
 watch(() => mvalue.value, funt);
 watch(() => dataSources.value, funt);
@@ -131,452 +131,459 @@ watch(() => homeStore.myData.isLoader, funt, { deep: true });
 funt();
 
 const upFile = (file: any) => {
-  if (!canVisionModel(gptConfigStore.myData.model)) {
-    if (isFileMp3(file.name)) {
-      mlog("mp3", file);
-      //  const formData = new FormData( );
-      // formData.append('file', file);
-      // formData.append('model', 'whisper-1');
+	if (!canVisionModel(gptConfigStore.myData.model)) {
+		if (isFileMp3(file.name)) {
+			mlog("mp3", file);
+			//  const formData = new FormData( );
+			// formData.append('file', file);
+			// formData.append('model', 'whisper-1');
 
-      // GptUploader('/v1/audio/transcriptions',formData).then(r=>{
-      //     mlog('语音识别成功', r );
-      // }).catch(e=>ms.error('上传失败:'+ ( e.message?? JSON.stringify(e)) ));
-      homeStore.setMyData({
-        act: "gpt.whisper",
-        actData: { file, prompt: "whisper" },
-      });
-      return;
-    } else {
-      upImg(file)
-        .then((d) => {
-          fsRef.value.value = "";
-          if (st.value.fileBase64.findIndex((v) => v == d) > -1) {
-            ms.error(t("mj.noReUpload")); //'不能重复上传'
-            return;
-          }
-          st.value.fileBase64.push(d);
-        })
-        .catch((e) => ms.error(e));
-    }
-  } else {
-    const formData = new FormData();
-    //const file = input.target.files[0];
-    formData.append("file", file);
-    ms.info(t("mj.uploading"));
-    st.value.isLoad = 1;
-    GptUploader("/v1/upload", formData)
-      .then((r) => {
-        //mlog('上传成功', r);
-        st.value.isLoad = 0;
-        if (r.url) {
-          ms.info(t("mj.uploadSuccess"));
-          if (r.url.indexOf("http") > -1) {
-            st.value.fileBase64.push(r.url);
-          } else {
-            st.value.fileBase64.push(location.origin + r.url);
-          }
-        } else if (r.error) ms.error(r.error);
-      })
-      .catch((e) => {
-        st.value.isLoad = 0;
-        ms.error(t("mj.uploadFail") + (e.message ?? JSON.stringify(e)));
-      });
-  }
+			// GptUploader('/v1/audio/transcriptions',formData).then(r=>{
+			//     mlog('语音识别成功', r );
+			// }).catch(e=>ms.error('上传失败:'+ ( e.message?? JSON.stringify(e)) ));
+			homeStore.setMyData({
+				act: "gpt.whisper",
+				actData: { file, prompt: "whisper" },
+			});
+			return;
+		} else {
+			upImg(file)
+				.then((d) => {
+					fsRef.value.value = "";
+					if (st.value.fileBase64.findIndex((v) => v == d) > -1) {
+						ms.error(t("mj.noReUpload")); //'不能重复上传'
+						return;
+					}
+					st.value.fileBase64.push(d);
+				})
+				.catch((e) => ms.error(e));
+		}
+	} else {
+		const formData = new FormData();
+		//const file = input.target.files[0];
+		formData.append("file", file);
+		ms.info(t("mj.uploading"));
+		st.value.isLoad = 1;
+		GptUploader("/v1/upload", formData)
+			.then((r) => {
+				//mlog('上传成功', r);
+				st.value.isLoad = 0;
+				if (r.url) {
+					ms.info(t("mj.uploadSuccess"));
+					if (r.url.indexOf("http") > -1) {
+						st.value.fileBase64.push(r.url);
+					} else {
+						st.value.fileBase64.push(location.origin + r.url);
+					}
+				} else if (r.error) ms.error(r.error);
+			})
+			.catch((e) => {
+				st.value.isLoad = 0;
+				ms.error(t("mj.uploadFail") + (e.message ?? JSON.stringify(e)));
+			});
+	}
 };
 
 function handleEnter(event: KeyboardEvent) {
-  if (!isMobile.value) {
-    if (event.key === "Enter" && !event.shiftKey) {
-      event.preventDefault();
-      handleSubmit();
-    }
-  } else {
-    if (event.key === "Enter" && event.ctrlKey) {
-      event.preventDefault();
-      handleSubmit();
-    }
-  }
+	if (!isMobile.value) {
+		if (event.key === "Enter" && !event.shiftKey) {
+			event.preventDefault();
+			handleSubmit();
+		}
+	} else {
+		if (event.key === "Enter" && event.ctrlKey) {
+			event.preventDefault();
+			handleSubmit();
+		}
+	}
 }
 
 const acceptData = computed(() => {
-  if (canVisionModel(gptConfigStore.myData.model)) return "*/*";
-  return "image/jpeg, image/jpg, image/png, image/gif, .mp3, .mp4, .mpeg, .mpga, .m4a, .wav, .webm";
+	if (canVisionModel(gptConfigStore.myData.model)) return "*/*";
+	return "image/jpeg, image/jpg, image/png, image/gif, .mp3, .mp4, .mpeg, .mpga, .m4a, .wav, .webm";
 });
 
 const drop = (e: DragEvent) => {
-  e.preventDefault();
-  e.stopPropagation();
-  if (!e.dataTransfer || e.dataTransfer.files.length == 0) return;
-  const files = e.dataTransfer.files;
-  upFile(files[0]);
-  //mlog('drop', files);
+	e.preventDefault();
+	e.stopPropagation();
+	if (!e.dataTransfer || e.dataTransfer.files.length == 0) return;
+	const files = e.dataTransfer.files;
+	upFile(files[0]);
+	//mlog('drop', files);
 };
 const paste = (e: ClipboardEvent) => {
-  let rz = getFileFromClipboard(e);
-  if (rz.length > 0) upFile(rz[0]);
+	let rz = getFileFromClipboard(e);
+	if (rz.length > 0) upFile(rz[0]);
 };
 
 const sendMic = (e: any) => {
-  mlog("sendMic", e);
-  st.value.showMic = false;
-  let du = "whisper.wav"; // (e.stat && e.stat.duration)?(e.stat.duration.toFixed(2)+'s'):'whisper.wav';
-  const file = new File([e.blob], du, { type: "audio/wav" });
-  homeStore.setMyData({
-    act: "gpt.whisper",
-    actData: { file, prompt: "whisper", duration: e.stat?.duration },
-  });
+	mlog("sendMic", e);
+	st.value.showMic = false;
+	let du = "whisper.wav"; // (e.stat && e.stat.duration)?(e.stat.duration.toFixed(2)+'s'):'whisper.wav';
+	const file = new File([e.blob], du, { type: "audio/wav" });
+	homeStore.setMyData({
+		act: "gpt.whisper",
+		actData: { file, prompt: "whisper", duration: e.stat?.duration },
+	});
 };
 
 //语音识别ASR
 const goASR = () => {
-  const olod = mvalue.value;
-  const rec = new Recognition();
-  let rz = "";
-  rec
-    .setListener((r: string) => {
-      //mlog('result ', r  );
-      rz = r;
-      mvalue.value = r;
-      st.value.micStart = true;
-    })
-    .setOnEnd(() => {
-      //mlog('rec end');
-      mvalue.value = olod + rz;
-      ms.info(t("mj.micRecEnd"));
-      st.value.micStart = false;
-    })
-    .setOpt({
-      timeOut: 2000,
-      onStart: () => {
-        ms.info(t("mj.micRec"));
-        st.value.micStart = true;
-      },
-    })
-    .start();
+	console.log("触发语音识别");
+
+	const olod = mvalue.value;
+	const rec = new Recognition();
+	console.log("🚀 ~ goASR ~ rec:", rec);
+	let rz = "";
+	rec
+		.setListener((r: string) => {
+			//mlog('result ', r  );
+			rz = r;
+			mvalue.value = r;
+			console.log("mvalue.value1111", mvalue.value);
+			st.value.micStart = true;
+		})
+		.setOnEnd(() => {
+			//mlog('rec end');
+			mvalue.value = olod + rz;
+			console.log("mvalue.value", mvalue.value);
+
+			ms.info(t("mj.micRecEnd"));
+			st.value.micStart = false;
+		})
+		.setOpt({
+			timeOut: 3000,
+			onStart: () => {
+				ms.info(t("mj.micRec"));
+				st.value.micStart = true;
+			},
+		})
+		.start();
 };
 
 const drOption = [
-  {
-    label: t("mj.micWhisper"),
-    key: "whisper",
-    icon: iconRender({ icon: "ri:openai-fill" }),
-  },
-  {
-    label: t("mj.micAsr"),
-    icon: iconRender({ icon: "ri:chrome-line" }),
-    key: "asr",
-  },
+	{
+		label: t("mj.micWhisper"),
+		key: "whisper",
+		icon: iconRender({ icon: "ri:openai-fill" }),
+	},
+	{
+		label: t("mj.micAsr"),
+		icon: iconRender({ icon: "ri:chrome-line" }),
+		key: "asr",
+	},
 ];
 const handleSelectASR = (key: string | number) => {
-  if (key == "asr") goASR();
-  if (key == "whisper") st.value.showMic = true;
+	console.log("*********");
+
+	if (key == "asr") goASR();
+	if (key == "whisper") st.value.showMic = true;
 };
 const show = ref(false);
 function handleExport() {
-  emit("export");
+	emit("export");
 }
 function handleClear() {
-  emit("handleClear");
+	emit("handleClear");
 }
 </script>
 <template>
-  <div v-if="st.showMic" class="myinputs flex justify-center items-center">
-    <AiMic @cancel="st.showMic = false" @send="sendMic" />
-  </div>
-  <div v-else>
-      <div
-        class="flex items-base justify-start pb-1 flex-wrap-reverse"
-        v-if="st.fileBase64.length > 0"
-        style="margin: 0 40px;"
-      >
-        <div
-          class="w-[60px] h-[60px] rounded-sm bg-slate-50 mr-1 mt-1 text-red-300 relative group"
-          v-for="(v, ii) in st.fileBase64"
-        >
-          <NImage :src="v" object-fit="cover" class="w-full h-full">
-            <template #placeholder>
-              <a
-                class="w-full h-full flex items-center justify-center text-neutral-500"
-                :href="v"
-                target="_blank"
-              >
-                <SvgIcon icon="mdi:download" />{{ $t("mj.attr1") }} {{ ii + 1 }}
-              </a>
-            </template>
-          </NImage>
-          <SvgIcon
-            icon="mdi:close"
-            class="hidden group-hover:block absolute top-[-5px] right-[-5px] rounded-full bg-red-300 text-white cursor-pointer"
-            @click="st.fileBase64.splice(st.fileBase64.indexOf(v), 1)"
-          ></SvgIcon>
-        </div>
-      </div>
-    <div
-      class="myinputs"
-      :class="[!isMobile ? 'chat-footer' : '']"
-      @drop="drop"
-      @paste="paste"
-    >
-      <div class="top-bar" v-if="!isMobile">
-        <div class="left" v-if="st">
-          <div
-            v-if="homeStore.myData.local != 'draw'"
-            class="chage-model-select"
-            @click="st.isShow = true"
-          >
-            <template v-if="nGptStore.gpts">
-              <SvgIcon icon="ri:apps-fill" />
-              <span class="line-clamp-1 overflow-hidden">{{
-                nGptStore.gpts.name
-              }}</span>
-            </template>
-            <template v-else>
-              <SvgIcon icon="heroicons:sparkles" />
-              <span>{{
-                nGptStore.modelLabel ? nGptStore.modelLabel : "gpt-4o-mini"
-              }}</span>
-            </template>
-            <SvgIcon icon="icon-park-outline:right" />
-          </div>
-          <n-dropdown
-            trigger="hover"
-            :options="drOption"
-            @select="handleSelectASR"
-          >
-            <div class="relative; w-[22px]" style="margin: 0 25px">
-              <div
-                class="absolute bottom-[14px] left-[31px]"
-                v-if="st.micStart"
-              >
-                <span class="relative flex h-3 w-3">
-                  <span
-                    class="animate-ping absolute inline-flex h-full w-full rounded-full bg-red-500 opacity-75"
-                  ></span>
-                  <span
-                    class="relative inline-flex rounded-full h-3 w-3 bg-red-400"
-                  ></span>
-                </span>
-              </div>
-              <!-- <SvgIcon icon="bi:mic"  class="absolute bottom-[10px] left-[55px] cursor-pointer" @click="goASR()"></SvgIcon> -->
-              <IconSvg icon="voice" width="22px" height="22px"></IconSvg>
-            </div>
-          </n-dropdown>
-          <n-tooltip trigger="hover">
-            <template #trigger>
-              <SvgIcon
-                icon="line-md:uploading-loop"
-                class="absolute bottom-[10px] left-[8px] cursor-pointer"
-                v-if="st.isLoad == 1"
-              ></SvgIcon>
-              <IconSvg
-                icon="upload"
-                @click="fsRef.click()"
-                v-else
-                width="22px"
-                height="22px"
-              ></IconSvg>
-            </template>
-            <div
-              v-if="canVisionModel(gptConfigStore.myData.model)"
-              v-html="$t('mj.upPdf')"
-            ></div>
-            <div v-else v-html="$t('mj.upImg')"></div>
-          </n-tooltip>
-          <IconSvg
-            @click="handleExport"
-            icon="screenshot"
-            width="22px"
-            height="22px"
-          ></IconSvg>
-        </div>
-        <IconSvg
-          @click="handleClear"
-          class="right"
-          icon="clear"
-          width="28px"
-          height="22px"
-        ></IconSvg>
-        <!-- <div @click="show = true">
+	<div v-if="st.showMic" class="myinputs flex justify-center items-center">
+		<AiMic @cancel="st.showMic = false" @send="sendMic" />
+	</div>
+	<div v-else>
+		<div
+			class="flex items-base justify-start pb-1 flex-wrap-reverse"
+			v-if="st.fileBase64.length > 0"
+			style="margin: 0 40px"
+		>
+			<div
+				class="w-[60px] h-[60px] rounded-sm bg-slate-50 mr-1 mt-1 text-red-300 relative group"
+				v-for="(v, ii) in st.fileBase64"
+			>
+				<NImage :src="v" object-fit="cover" class="w-full h-full">
+					<template #placeholder>
+						<a
+							class="w-full h-full flex items-center justify-center text-neutral-500"
+							:href="v"
+							target="_blank"
+						>
+							<SvgIcon icon="mdi:download" />{{ $t("mj.attr1") }} {{ ii + 1 }}
+						</a>
+					</template>
+				</NImage>
+				<SvgIcon
+					icon="mdi:close"
+					class="hidden group-hover:block absolute top-[-5px] right-[-5px] rounded-full bg-red-300 text-white cursor-pointer"
+					@click="st.fileBase64.splice(st.fileBase64.indexOf(v), 1)"
+				></SvgIcon>
+			</div>
+		</div>
+		<div
+			class="myinputs"
+			:class="[!isMobile ? 'chat-footer' : '']"
+			@drop="drop"
+			@paste="paste"
+		>
+			<div class="top-bar" v-if="!isMobile">
+				<div class="left" v-if="st">
+					<div
+						v-if="homeStore.myData.local != 'draw'"
+						class="chage-model-select"
+						@click="st.isShow = true"
+					>
+						<template v-if="nGptStore.gpts">
+							<SvgIcon icon="ri:apps-fill" />
+							<span class="line-clamp-1 overflow-hidden">{{
+								nGptStore.gpts.name
+							}}</span>
+						</template>
+						<template v-else>
+							<SvgIcon icon="heroicons:sparkles" />
+							<span>{{
+								nGptStore.modelLabel ? nGptStore.modelLabel : "gpt-4o-mini"
+							}}</span>
+						</template>
+						<SvgIcon icon="icon-park-outline:right" />
+					</div>
+					<n-dropdown
+						trigger="hover"
+						:options="drOption"
+						@select="handleSelectASR"
+					>
+						<div class="relative; w-[22px]" style="margin: 0 25px">
+							<div
+								class="absolute bottom-[14px] left-[31px]"
+								v-if="st.micStart"
+							>
+								<span class="relative flex h-3 w-3">
+									<span
+										class="animate-ping absolute inline-flex h-full w-full rounded-full bg-red-500 opacity-75"
+									></span>
+									<span
+										class="relative inline-flex rounded-full h-3 w-3 bg-red-400"
+									></span>
+								</span>
+							</div>
+							<IconSvg icon="voice" width="22px" height="22px"></IconSvg>
+						</div>
+					</n-dropdown>
+					<n-tooltip trigger="hover">
+						<template #trigger>
+							<SvgIcon
+								icon="line-md:uploading-loop"
+								class="absolute bottom-[10px] left-[8px] cursor-pointer"
+								v-if="st.isLoad == 1"
+							></SvgIcon>
+							<IconSvg
+								icon="upload"
+								@click="fsRef.click()"
+								v-else
+								width="22px"
+								height="22px"
+							></IconSvg>
+						</template>
+						<div
+							v-if="canVisionModel(gptConfigStore.myData.model)"
+							v-html="$t('mj.upPdf')"
+						></div>
+						<div v-else v-html="$t('mj.upImg')"></div>
+					</n-tooltip>
+					<IconSvg
+						@click="handleExport"
+						icon="screenshot"
+						width="22px"
+						height="22px"
+					></IconSvg>
+				</div>
+				<IconSvg
+					@click="handleClear"
+					class="right"
+					icon="clear"
+					width="28px"
+					height="22px"
+				></IconSvg>
+				<!-- <div @click="show = true">
             {{ $t('store.siderButton') }}
         </div> -->
-      </div>
-      <input
-        type="file"
-        id="fileInput"
-        @change="selectFile"
-        class="hidden"
-        ref="fsRef"
-        :accept="acceptData"
-      />
-      <div class="w-full relative">
-        <div class="absolute bottom-0 right-0 z-1" v-if="isMobile">
-          <NPopover trigger="hover">
-            <template #trigger>
-              <NTag
-                type="info"
-                round
-                size="small"
-                style="cursor: pointer"
-                :bordered="false"
-              >
-                <div class="opacity-60 flex">
-                  <SvgIcon icon="material-symbols:token-outline" />
-                  {{ $t("mj.remain") }}{{ myToken.remain }}/{{
-                    myToken.modelTokens
-                  }}
-                </div>
-              </NTag>
-            </template>
-            <div class="w-[300px]">
-              {{ $t("mj.tokenInfo1") }}
-              <p class="py-1" v-text="$t('mj.tokenInfo2')"></p>
-              <p class="text-right">
-                <NButton @click="st.isShow = true" type="info" size="small">{{
-                  $t("setting.setting")
-                }}</NButton>
-              </p>
-            </div>
-          </NPopover>
-        </div>
-      </div>
-      <NAutoComplete
-        v-model:value="mvalue"
-        :options="searchOptions"
-        :render-label="renderOption"
-        :class="[!isMobile ? 'chat-input' : '']"
-      >
-        <template #default="{ handleInput, handleBlur, handleFocus }">
-          <NInput
-            ref="inputRef"
-            v-model:value="mvalue"
-            type="textarea"
-            :placeholder="placeholder"
-            rows="3"
-            :autosize="{ minRows: 3, maxRows: 3 }"
-            @input="handleInput"
-            @focus="handleFocus"
-            @blur="handleBlur"
-            @keypress="handleEnter"
-          >
-            <template #prefix v-if="isMobile">
-              <div class="relative; w-[22px]">
-                <n-tooltip trigger="hover">
-                  <template #trigger>
-                    <SvgIcon
-                      icon="line-md:uploading-loop"
-                      class="absolute bottom-[10px] left-[8px] cursor-pointer"
-                      v-if="st.isLoad == 1"
-                    ></SvgIcon>
-                    <SvgIcon
-                      icon="ri:attachment-line"
-                      class="absolute bottom-[10px] left-[8px] cursor-pointer"
-                      @click="fsRef.click()"
-                      v-else
-                    ></SvgIcon>
-                  </template>
-                  <div
-                    v-if="canVisionModel(gptConfigStore.myData.model)"
-                    v-html="$t('mj.upPdf')"
-                  ></div>
-                  <div v-else v-html="$t('mj.upImg')"></div>
-                </n-tooltip>
-              </div>
-              <!-- <div  class=" relative; w-[22px]">
+			</div>
+			<input
+				type="file"
+				id="fileInput"
+				@change="selectFile"
+				class="hidden"
+				ref="fsRef"
+				:accept="acceptData"
+			/>
+			<div class="w-full relative">
+				<div class="absolute bottom-0 right-0 z-1" v-if="isMobile">
+					<NPopover trigger="hover">
+						<template #trigger>
+							<NTag
+								type="info"
+								round
+								size="small"
+								style="cursor: pointer"
+								:bordered="false"
+							>
+								<div class="opacity-60 flex">
+									<SvgIcon icon="material-symbols:token-outline" />
+									{{ $t("mj.remain") }}{{ myToken.remain }}/{{
+										myToken.modelTokens
+									}}
+								</div>
+							</NTag>
+						</template>
+						<div class="w-[300px]">
+							{{ $t("mj.tokenInfo1") }}
+							<p class="py-1" v-text="$t('mj.tokenInfo2')"></p>
+							<p class="text-right">
+								<NButton @click="st.isShow = true" type="info" size="small">{{
+									$t("setting.setting")
+								}}</NButton>
+							</p>
+						</div>
+					</NPopover>
+				</div>
+			</div>
+			<NAutoComplete
+				v-model:value="mvalue"
+				:options="searchOptions"
+				:render-label="renderOption"
+				:class="[!isMobile ? 'chat-input' : '']"
+			>
+				<template #default="{ handleInput, handleBlur, handleFocus }">
+					<NInput
+						ref="inputRef"
+						v-model:value="mvalue"
+						type="textarea"
+						:placeholder="placeholder"
+						rows="3"
+						:autosize="{ minRows: 3, maxRows: 3 }"
+						@input="handleInput"
+						@focus="handleFocus"
+						@blur="handleBlur"
+						@keypress="handleEnter"
+					>
+						<template #prefix v-if="isMobile">
+							<div class="relative; w-[22px]">
+								<n-tooltip trigger="hover">
+									<template #trigger>
+										<SvgIcon
+											icon="line-md:uploading-loop"
+											class="absolute bottom-[10px] left-[8px] cursor-pointer"
+											v-if="st.isLoad == 1"
+										></SvgIcon>
+										<SvgIcon
+											icon="ri:attachment-line"
+											class="absolute bottom-[10px] left-[8px] cursor-pointer"
+											@click="fsRef.click()"
+											v-else
+										></SvgIcon>
+									</template>
+									<div
+										v-if="canVisionModel(gptConfigStore.myData.model)"
+										v-html="$t('mj.upPdf')"
+									></div>
+									<div v-else v-html="$t('mj.upImg')"></div>
+								</n-tooltip>
+							</div>
+							<!-- <div  class=" relative; w-[22px]">
                     <SvgIcon icon="bi:mic"  class="absolute bottom-[10px] left-[30px] cursor-pointer" @click="st.showMic=true"></SvgIcon>
                 </div> -->
-              <n-dropdown
-                trigger="hover"
-                :options="drOption"
-                @select="handleSelectASR"
-              >
-                <div class="relative; w-[22px]">
-                  <div
-                    class="absolute bottom-[14px] left-[31px]"
-                    v-if="st.micStart"
-                  >
-                    <span class="relative flex h-3 w-3">
-                      <span
-                        class="animate-ping absolute inline-flex h-full w-full rounded-full bg-red-500 opacity-75"
-                      ></span>
-                      <span
-                        class="relative inline-flex rounded-full h-3 w-3 bg-red-400"
-                      ></span>
-                    </span>
-                  </div>
-                  <!-- <SvgIcon icon="bi:mic"  class="absolute bottom-[10px] left-[55px] cursor-pointer" @click="goASR()"></SvgIcon> -->
-                  <SvgIcon
-                    icon="bi:mic"
-                    class="absolute bottom-[10px] left-[30px] cursor-pointer"
-                  ></SvgIcon>
-                </div>
-              </n-dropdown>
-            </template>
-            <template #suffix v-if="isMobile">
-              <div class="relative; w-[40px]">
-                <div class="absolute bottom-[-3px] right-[0px]">
-                  <NButton
-                    type="primary"
-                    :disabled="disabled || homeStore.myData.isLoader"
-                    @click="handleSubmit"
-                  >
-                    <template #icon>
-                      <span class="dark:text-black">
-                        <SvgIcon
-                          icon="ri:stop-circle-line"
-                          v-if="homeStore.myData.isLoader"
-                        />
-                        <SvgIcon icon="ri:send-plane-fill" v-else />
-                      </span>
-                    </template>
-                  </NButton>
-                </div>
-              </div>
-            </template>
-          </NInput>
-        </template>
-      </NAutoComplete>
-      <div class="send" @click="handleSubmit" v-if="!isMobile">
-        <IconSvg icon="send" width="16px" height="15px"></IconSvg>
-        |
-        <IconSvg icon="money" width="14px" height="24px"></IconSvg>
-        <NPopover trigger="hover">
-          <template #trigger>
-            {{ myToken.modelTokens }}
-          </template>
-          <div class="w-[300px]">
-            {{ $t("mj.tokenInfo1") }}
-            <p class="py-1" v-text="$t('mj.tokenInfo2')"></p>
-            <p class="text-right">
-              <NButton @click="st.isShow = true" type="info" size="small">{{
-                $t("setting.setting")
-              }}</NButton>
-            </p>
-          </div>
-        </NPopover>
-      </div>
-      <!-- translate-y-[-8px]       -->
-    </div>
-  </div>
+							<n-dropdown
+								trigger="hover"
+								:options="drOption"
+								@select="handleSelectASR"
+							>
+								<div class="relative; w-[22px]">
+									<div
+										class="absolute bottom-[14px] left-[31px]"
+										v-if="st.micStart"
+									>
+										<span class="relative flex h-3 w-3">
+											<span
+												class="animate-ping absolute inline-flex h-full w-full rounded-full bg-red-500 opacity-75"
+											></span>
+											<span
+												class="relative inline-flex rounded-full h-3 w-3 bg-red-400"
+											></span>
+										</span>
+									</div>
+									<!-- <SvgIcon icon="bi:mic"  class="absolute bottom-[10px] left-[55px] cursor-pointer" @click="goASR()"></SvgIcon> -->
+									<SvgIcon
+										icon="bi:mic"
+										class="absolute bottom-[10px] left-[30px] cursor-pointer"
+									></SvgIcon>
+								</div>
+							</n-dropdown>
+						</template>
+						<template #suffix v-if="isMobile">
+							<div class="relative; w-[40px]">
+								<div class="absolute bottom-[-3px] right-[0px]">
+									<NButton
+										type="primary"
+										:disabled="disabled || homeStore.myData.isLoader"
+										@click="handleSubmit"
+									>
+										<template #icon>
+											<span class="dark:text-black">
+												<SvgIcon
+													icon="ri:stop-circle-line"
+													v-if="homeStore.myData.isLoader"
+												/>
+												<SvgIcon icon="ri:send-plane-fill" v-else />
+											</span>
+										</template>
+									</NButton>
+								</div>
+							</div>
+						</template>
+					</NInput>
+				</template>
+			</NAutoComplete>
+			<div class="send" @click="handleSubmit" v-if="!isMobile">
+				<IconSvg icon="send" width="16px" height="15px"></IconSvg>
+				|
+				<IconSvg icon="money" width="14px" height="24px"></IconSvg>
+				<NPopover trigger="hover">
+					<template #trigger>
+						{{ myToken.modelTokens }}
+					</template>
+					<div class="w-[300px]">
+						{{ $t("mj.tokenInfo1") }}
+						<p class="py-1" v-text="$t('mj.tokenInfo2')"></p>
+						<p class="text-right">
+							<NButton @click="st.isShow = true" type="info" size="small">{{
+								$t("setting.setting")
+							}}</NButton>
+						</p>
+					</div>
+				</NPopover>
+			</div>
+			<!-- translate-y-[-8px]       -->
+		</div>
+	</div>
 
-  <NModal
-    v-model:show="st.isShow"
-    preset="card"
-    :title="$t('mjchat.modelChange')"
-    class="!max-w-[620px]"
-    @close="st.isShow = false"
-  >
-    <aiModel @close="st.isShow = false" />
-  </NModal>
+	<NModal
+		v-model:show="st.isShow"
+		preset="card"
+		:title="$t('mjchat.modelChange')"
+		class="!max-w-[620px]"
+		@close="st.isShow = false"
+	>
+		<aiModel @close="st.isShow = false" />
+	</NModal>
 
-  <PromptStore v-model:visible="show"></PromptStore>
-  <!-- <n-drawer v-model:show="st.showMic" :width="420" :on-update:show="onShowFun">
+	<PromptStore v-model:visible="show"></PromptStore>
+	<!-- <n-drawer v-model:show="st.showMic" :width="420" :on-update:show="onShowFun">
     <n-drawer-content title="录音" closable>
         <AiMic />
     </n-drawer-content>
 </n-drawer> -->
 </template>
-<style    >
+<style>
 .myinputs .n-input .n-input-wrapper {
-  @apply items-stretch;
+	@apply items-stretch;
 }
 </style>

+ 134 - 142
src/views/mj/aiSider.vue

@@ -1,37 +1,43 @@
 <script setup lang="ts">
-import { computed,defineAsyncComponent ,ref, onMounted} from "vue";
-import { PromptStore, SvgIcon} from '@/components/common'
+import { computed, defineAsyncComponent, ref, onMounted, Component, h } from "vue";
+import { IconSvg, PromptStore, SvgIcon } from '@/components/common'
 import { getConfigKey } from '@/api/user';
 import { useBasicLayout } from '@/hooks/useBasicLayout'
 const { isMobile } = useBasicLayout()
 import { removeToken } from '@/store/modules/auth/helper'
-import { NTooltip, useMessage } from 'naive-ui'
-import { homeStore,useChatStore } from '@/store'
-import { loginOut,getUserInfo} from '@/api/user'
+import { NTooltip, useMessage, NAvatar, NPopover, NMenu, NIcon } from 'naive-ui'
+import { homeStore, useChatStore } from '@/store'
+import { loginOut, getUserInfo } from '@/api/user'
 import { UserData } from "@/typings/user"
-import { defaultSetting,UserInfo } from '@/store/modules/user/helper'
+import { defaultSetting, UserInfo } from '@/store/modules/user/helper'
 import { useRouter } from 'vue-router'
 import { getToken } from "@/store/modules/auth/helper";
 import to from "await-to-js";
+import {
+  Settings as settings,
+  Storefront as storefront,
+  LogOut as out
+} from '@vicons/ionicons5'
+
 const chatStore = useChatStore()
 //import gallery from '@/views/gallery/index.vue'
 
 const Setting = defineAsyncComponent(() => import('@/components/common/Setting/index.vue'))
 
 
-const st= ref({'show':false,showImg:false, menu:[],active:'chat'})
+const st = ref({ 'show': false, showImg: false, menu: [], active: 'chat' })
 const router1 = useRouter()
 const userInfo = ref<UserInfo>(defaultSetting().userInfo)
 const message = useMessage()
 const show = ref(false)
-
-const isLogin =computed(  () => {
+const urouter = useRouter() //
+const isLogin = computed(() => {
   return localStorage.getItem('TOKEN')
 });
 
 import { router } from '@/router'
 
-const goHome =computed(  () => {
+const goHome = computed(() => {
   return router.currentRoute.value.name
 });
 
@@ -43,30 +49,30 @@ onMounted(() => {
 /**
  * 获取默认头像
  */
- async function getLogo() {
+async function getLogo() {
   const [err1, res1] = await to(getConfigKey("logoImage"));
-		if (err1) {
-			console.error("获取配置失败", err1.message);
-		} else {
-      userInfo.value.avatar  = res1.msg;
- }
+  if (err1) {
+    console.error("获取配置失败", err1.message);
+  } else {
+    userInfo.value.avatar = res1.msg;
+  }
 }
 
 /**
  * 获取当前登录用户信息
  */
- async function getLoginUserInfo() {
+async function getLoginUserInfo() {
   // 用户未登录,不需要获取用户信息
-  if(!getToken()){
-      return
+  if (!getToken()) {
+    return
   }
   const [err, newUserInfo] = await to(getUserInfo<UserData>());
-      if (err) {
-       // message.error(err.toString())
-        console.log(err.toString())
-      }
-  if(newUserInfo){
-    if(newUserInfo.data.user.avatar){
+  if (err) {
+    // message.error(err.toString())
+    console.log(err.toString())
+  }
+  if (newUserInfo) {
+    if (newUserInfo.data.user.avatar) {
       userInfo.value.avatar = newUserInfo.data.user.avatar;
     }
     userInfo.value.name = newUserInfo.data.user.nickName;
@@ -76,137 +82,123 @@ onMounted(() => {
   }
 }
 
-const chatId= computed(()=>chatStore.active??'1002' );/**
+const chatId = computed(() => chatStore.active ?? '1002');/**
  * 退出登录
  */
- async function handleReset() {
-    await loginOut()
-    // 删除用户token
-    removeToken();
-    // 跳转到登录页面
-    router1.push('/login')
+async function handleReset() {
+  await loginOut()
+  // 删除用户token
+  removeToken();
+  // 跳转到登录页面
+  router1.push('/login')
 }
 
 async function longin() {
-    // 跳转到登录页面
-    router1.push('/login')
+  // 跳转到登录页面
+  router1.push('/login')
 }
 
+function renderIcon(icon: Component) {
+  return () => h(NIcon, null, { default: () => h(icon) })
+}
 
+const menuOptions = ref([
+  {
+    label: '账号设置',
+    key: 'accountSettings',
+    icon: renderIcon(settings)
+  },
+  {
+    label: '购买套餐',
+    key: 'buy',
+    icon: renderIcon(storefront)
+  },
+  {
+    label: '退出账号',
+    key: 'logout',
+    icon: renderIcon(out)
+  }
+])
+
+const handleSelect = (key: string) => {
+  if (key === 'accountSettings') {
+    st.value.show = true
+  } else if (key === 'logout') {
+    handleReset()
+  } else if (key === 'buy') {
+    show.value = true
+  }
+}
 </script>
 <template>
-<!-- <div class="flex-shrink-0 w-[60px] z-[1000]  h-full nav-bar" v-if="!isMobile">
-    <div class="flex h-full select-none flex-col items-center justify-between bg-[#e8eaf1] px-2 pt-4 pb-8 dark:bg-[#25272d]">
-        <div class="flex flex-col space-y-4 flex-1 nemu-bar">
-            <a :href="`#/chat/${chatId}`"    @click="st.active='chat';homeStore.setMyData({act:'chat'})" class="router-link-active nemu-item">
-                <n-tooltip placement="right" trigger="hover">
-                  <template #trigger>
-                    <div  class="flex h-full items-center py-1 flex-row " :class="[ goHome =='Chat' ? 'active' : '']">
-                     <IconSvg icon="message" width="18" height="18"></IconSvg>
-                     <span class="text-[18px]">{{$t('mjtab.chat')}}</span>
-                    </div>
-                 </template>
-                AI Chat
-                </n-tooltip>
-            </a>
-
-            <a   @click="homeStore.setMyData({act:'showgpts'}) " class="nemu-item">
-                <n-tooltip placement="right" trigger="hover">
-                  <template #trigger>
-                    <div  class="flex h-full items-center   py-1 flex-row" >
-                      <IconSvg icon="Application" width="18" height="18"></IconSvg>
-                     <span class="text-[18px]">{{$t('mjtab.store')}}</span>
-                    </div>
-                  </template>
-                    ChatGPT Store
-                </n-tooltip>
-            </a>
-
-            <a :href="`#/draw/${chatId}`" @click="st.active='draw'" class="nemu-item">
-                <n-tooltip placement="right" trigger="hover">
-                  <template #trigger>
-                    <div  class="flex h-full items-center   py-1 flex-row" :class="[goHome=='draw' ? 'active' : '']">
-                      <IconSvg icon="Vector" width="18" height="18"></IconSvg>
-                     <span class="text-[18px]">{{$t('mjtab.draw')}}</span>
-                    </div>
-                  </template>
-                    {{$t('mjtab.drawinfo')}}
-                </n-tooltip>
-            </a>
-
-    
-          <a :href="`#/music`" @click="st.active='music';homeStore.setMyData({act:'music'}) " class="nemu-item">
-						<n-tooltip placement="right" trigger="hover">
-							<template #trigger>
-								<div class="flex h-full items-center py-1 flex-row" :class="[st.active=='music' ? 'active' : '']">
-									<IconSvg icon="music1" width="18" height="18"></IconSvg>
-									<span class="text-[18px]"> {{ $t('suno.menu') }}</span>
-								</div>
-							</template>
-              {{ $t('suno.menuinfo') }}
-						</n-tooltip>
-					</a>
-
-          <a :href="`#/knowledge`" @click="st.active = 'knowledge'; homeStore.setMyData({ local: 'knowledge' })"
-					class="nemu-item">
-					<n-tooltip placement="right" trigger="hover">
-						<template #trigger>
-							<div class="flex h-full items-center py-1 flex-row" :class="[st.active == 'knowledge' ? 'active' : '']">
-								<IconSvg icon="Book" width="18" height="18"></IconSvg>
-								<span class="text-[18px]">{{ $t('mjtab.knowledge') }}</span>
-							</div>
-						</template>
-						{{ $t('mjtab.knowledgeinfo') }}
-					</n-tooltip>
-				</a>
-        </div>
-
 
-        <div class="user-info" :style="{height: isLogin ? '144px' : '90px', bottom: isLogin ? '84px' : '24px'}">
 
-          <div v-show="isLogin">
-              <div class="top" >
-              <div class="avatar">
-                <img :src="userInfo.avatar" alt="">
-                <div class="circle"></div>
+  <div class="flex-shrink-0 w-[60px] z-[1000]  h-full" v-if="!isMobile" data-tauri-drag-region>
+    <div
+      class="flex h-full select-none flex-col items-center justify-between bg-[#e8eaf1] px-2 pt-4 pb-8 dark:bg-[#25272d]">
+      <div class="flex flex-col space-y-4 flex-1 " data-tauri-drag-region>
+        <a @click="st.active = 'chat'; urouter.push(`/chat`)"
+          class="router-link-active router-link-exact-active h-12 w-12 cursor-pointer rounded-xl bg-white duration-300 dark:bg-[#34373c] hover:bg-[#bbb] dark:hover:bg-[#555]">
+          <n-tooltip placement="right" trigger="hover">
+            <template #trigger>
+              <div class="flex h-full justify-center items-center py-1 flex-col "
+                :class="[goHome == 'Chat' ? 'active' : '']">
+                <SvgIcon icon="ri:wechat-line" class="text-3xl  flex-1"></SvgIcon>
               </div>
-              <p class="user-name">{{ userInfo.name ?? '熊猫助手' }}</p>
-              <div class="user-free">¥ {{ userInfo.userBalance }}</div>
-              <p class="user-email">{{ userInfo.userName }}</p>
-            </div>
-
-            <div class="user-bottom" @click="show = true">
-              <NButton block>
-                {{ $t('store.siderButton') }}
-              </NButton>
-            </div>
-          </div>
-
-          <div v-show="!isLogin" class="user-bottom"  @click="longin">
-            <NButton block>
-                {{ $t('store.login') }}
-            </NButton>
-          </div>
-
+            </template>
+            对话
+          </n-tooltip>
+        </a>
+
+        <a @click="urouter.push(`/store`)"
+          class=" router-link-exact-active h-12 w-12 cursor-pointer rounded-xl bg-white duration-300 dark:bg-[#34373c] hover:bg-[#bbb] dark:hover:bg-[#555]">
+          <n-tooltip placement="right" trigger="hover">
+            <template #trigger>
+              <div class="flex h-full justify-center items-center   py-1 flex-col">
+                <SvgIcon icon="ri:apps-fill" class="text-3xl flex-1"></SvgIcon>
+              </div>
+            </template>
+            应用中心
+          </n-tooltip>
+        </a>
+
+        <a
+          class=" router-link-exact-active h-12 w-12 cursor-pointer rounded-xl bg-white duration-300 dark:bg-[#34373c] hover:bg-[#bbb] dark:hover:bg-[#555]">
+          <n-tooltip placement="right" trigger="hover">
+            <template #trigger>
+              <div class="flex h-full justify-center items-center   py-1 flex-col">
+                <SvgIcon icon="weui:shop-filled" class="text-3xl flex-1"></SvgIcon>
+              </div>
+            </template>
+            插件市场
+          </n-tooltip>
+        </a>
+      </div>
+
+      <div class="flex flex-col  space-y-2 ">
+        <!-- <NAvatar  v-show="isLogin"  size="large"  round  :src="userInfo.avatar"   v-if="userInfo.avatar"  :fallback-src="defaultAvatar"
+         class=" cursor-pointer"  /> -->
+
+        <n-popover trigger="click" :show-arrow="false">
+          <template #trigger>
+            <n-avatar v-show="isLogin" size="large" round :src="userInfo.avatar" />
+          </template>
+          <n-menu :options="menuOptions" @select="handleSelect" />
+        </n-popover>
+
+        <div v-show="!isLogin" class="user-bottom" @click="longin">
+          <n-button tertiary type="info">
+            登录
+          </n-button>
         </div>
 
-        <div v-if="isLogin" class="user-footer" v-show="isLogin">
-          <div class="settings" @click="st.show = true">
-            <IconSvg icon="Setting" width="24" height="22"></IconSvg>
-            {{ $t('setting.setting') }}
-          </div>
-          <div class="log-out" @click="handleReset">
-            <IconSvg icon="Logout" width="24" height="24"></IconSvg>
-            {{ $t('mjset.logout') }}
-          </div>
-        </div>
-    </div>
-</div> -->
- <!-- <Setting v-if="st.show" v-model:visible="st.show" />
- <PromptStore v-model:visible="show"></PromptStore> -->
+      </div>
 
+      
+    </div>
+    
+  </div>
+  <Setting v-if="st.show" v-model:visible="st.show" />
+  <PromptStore v-model:visible="show"></PromptStore>
+  
 </template>
-
-
-
-

+ 2 - 2
src/views/store/layout.vue

@@ -13,8 +13,8 @@ const appStore = useAppStore()
 const chatStore = useChatStore()
 const authStore = useAuthStore()
 
-router.replace({ name: 'Sound', params: { uuid: chatStore.active } })
-homeStore.setMyData({local:'sound'});
+router.replace({ name: 'store', params: { uuid: chatStore.active } })
+homeStore.setMyData({local:'store'});
 const { isMobile } = useBasicLayout()
 
 const collapsed = computed(() => appStore.siderCollapsed)