Просмотр исходного кода

fix: 修复知识片段显示异常

ageerle 1 месяц назад
Родитель
Сommit
1f94aa2900
40 измененных файлов с 3800 добавлено и 597 удалено
  1. 804 87
      package-lock.json
  2. 9 2
      package.json
  3. 594 0
      pnpm-lock.yaml
  4. 488 0
      public/docmee-ui-sdk-iframe.min.js
  5. 9 0
      src/api/fanyi.ts
  6. 9 13
      src/api/knowledge.ts
  7. 19 1
      src/api/mjapi.ts
  8. 12 16
      src/api/openapi.ts
  9. 17 0
      src/api/ppt.ts
  10. 87 0
      src/api/store.ts
  11. BIN
      src/assets/fileType.png
  12. 1 2
      src/components/common/PromptStore/index.vue
  13. 3 3
      src/locales/zh-CN.ts
  14. 60 49
      src/router/index.ts
  15. 1 1
      src/store/homeStore.ts
  16. 2 1
      src/store/modules/app/helper.ts
  17. 4 0
      src/store/modules/app/index.ts
  18. 1 1
      src/store/modules/settings/helper.ts
  19. 70 35
      src/styles/lib/highlight.less
  20. 1 0
      src/views/chat/components/Message/index.vue
  21. 7 2
      src/views/chat/index.vue
  22. 0 2
      src/views/chat/layout/Layout.vue
  23. 138 24
      src/views/chat/layout/sider/index.vue
  24. 155 0
      src/views/fanyi/components/documentComponent.vue
  25. 144 0
      src/views/fanyi/components/textComponent.vue
  26. 26 0
      src/views/fanyi/index.vue
  27. 61 0
      src/views/fanyi/layout.vue
  28. 9 7
      src/views/knowledge/annex.vue
  29. 9 7
      src/views/knowledge/fragment.vue
  30. 204 136
      src/views/knowledge/index.vue
  31. 138 139
      src/views/login/index.vue
  32. 24 37
      src/views/mj/aiGpt.vue
  33. 1 1
      src/views/mj/aiGptInput.vue
  34. 26 3
      src/views/mj/aiModel.vue
  35. 21 28
      src/views/mj/aiSider.vue
  36. 112 0
      src/views/ppt/index.vue
  37. 61 0
      src/views/ppt/layout.vue
  38. 132 0
      src/views/store/appList.vue
  39. 280 0
      src/views/store/index.vue
  40. 61 0
      src/views/store/layout.vue

Разница между файлами не показана из-за своего большого размера
+ 804 - 87
package-lock.json


+ 9 - 2
package.json

@@ -3,7 +3,7 @@
   "version": "2.0.0",
   "private": false,
   "description": "ruoyi-web",
-  "author": "ageerle@163.com",
+  "author": "ageer <ageerle@163.com>",
   "keywords": [
     "rouyi-web",
     "chatgpt",
@@ -26,12 +26,17 @@
     "lint:fix": "eslint . --fix",
     "bootstrap": "pnpm install && pnpm run common:prepare",
     "common:cleanup": "rimraf node_modules && rimraf pnpm-lock.yaml",
-    "common:prepare": "husky install"
+    "common:prepare": "husky install",
+    "docs:dev": "vitepress dev docs",
+    "docs:build": "vitepress build docs",
+    "docs:preview": "vitepress preview docs"
   },
   "dependencies": {
     "@traptitech/markdown-it-katex": "^3.6.0",
+    "@vue-office/pdf": "^2.0.10",
     "@vueuse/core": "^9.13.0",
     "await-to-js": "^3.0.0",
+    "element-plus": "^2.9.2",
     "eventsource-parser": "^1.1.1",
     "form-data": "^4.0.0",
     "gpt-tokenizer": "^2.1.2",
@@ -44,6 +49,7 @@
     "naive-ui": "^2.34.3",
     "pinia": "^2.0.33",
     "vue": "^3.2.47",
+    "vue-demi": "^0.14.10",
     "vue-i18n": "^9.2.2",
     "vue-router": "^4.1.6",
     "vue-waterfall-plugin-next": "^2.3.1"
@@ -78,6 +84,7 @@
     "vite": "^4.2.0",
     "vite-plugin-pwa": "^0.14.4",
     "vite-plugin-svg-icons": "^2.0.1",
+    "vitepress": "^1.6.3",
     "vue-tsc": "^1.2.0"
   },
   "lint-staged": {

Разница между файлами не показана из-за своего большого размера
+ 594 - 0
pnpm-lock.yaml


+ 488 - 0
public/docmee-ui-sdk-iframe.min.js

@@ -0,0 +1,488 @@
+var R = Object.defineProperty;
+var q = (e, r, t) => r in e ? R(e, r, { enumerable: !0, configurable: !0, writable: !0, value: t }) : e[r] = t;
+var u = (e, r, t) => q(e, typeof r != "symbol" ? r + "" : r, t);
+const I = "%[a-f0-9]{2}", F = new RegExp("(" + I + ")|([^%]+?)", "gi"), b = new RegExp("(" + I + ")+", "gi");
+console.log('加载js文件');
+function l(e, r) {
+	try {
+		return [decodeURIComponent(e.join(""))];
+	} catch {
+	}
+	if (e.length === 1)
+		return e;
+	r = r || 1;
+	const t = e.slice(0, r), n = e.slice(r);
+	return Array.prototype.concat.call([], l(t), l(n));
+}
+function D(e) {
+	try {
+		return decodeURIComponent(e);
+	} catch {
+		let r = e.match(F) || [];
+		for (let t = 1; t < r.length; t++)
+			e = l(r, t).join(""), r = e.match(F) || [];
+		return e;
+	}
+}
+function L(e) {
+	const r = {
+		"%FE%FF": "��",
+		"%FF%FE": "��"
+	};
+	let t = b.exec(e);
+	for (; t;) {
+		try {
+			r[t[0]] = decodeURIComponent(t[0]);
+		} catch {
+			const a = D(t[0]);
+			a !== t[0] && (r[t[0]] = a);
+		}
+		t = b.exec(e);
+	}
+	r["%C2"] = "�";
+	const n = Object.keys(r);
+	for (const a of n)
+		e = e.replace(new RegExp(a, "g"), r[a]);
+	return e;
+}
+function P(e) {
+	if (typeof e != "string")
+		throw new TypeError("Expected `encodedURI` to be of type `string`, got `" + typeof e + "`");
+	try {
+		return decodeURIComponent(e);
+	} catch {
+		return L(e);
+	}
+}
+function M(e, r) {
+	if (!(typeof e == "string" && typeof r == "string"))
+		throw new TypeError("Expected the arguments to be of type `string`");
+	if (e === "" || r === "")
+		return [];
+	const t = e.indexOf(r);
+	return t === -1 ? [] : [
+		e.slice(0, t),
+		e.slice(t + r.length)
+	];
+}
+function k(e, r) {
+	const t = {};
+	if (Array.isArray(r))
+		for (const n of r) {
+			const a = Object.getOwnPropertyDescriptor(e, n);
+			a != null && a.enumerable && Object.defineProperty(t, n, a);
+		}
+	else
+		for (const n of Reflect.ownKeys(e)) {
+			const a = Object.getOwnPropertyDescriptor(e, n);
+			if (a.enumerable) {
+				const i = e[n];
+				r(n, i, e) && Object.defineProperty(t, n, a);
+			}
+		}
+	return t;
+}
+const H = (e) => e == null, T = (e) => encodeURIComponent(e).replaceAll(/[!'()*]/g, (r) => `%${r.charCodeAt(0).toString(16).toUpperCase()}`), m = Symbol("encodeFragmentIdentifier");
+function B(e) {
+	switch (e.arrayFormat) {
+		case "index":
+			return (r) => (t, n) => {
+				const a = t.length;
+				return n === void 0 || e.skipNull && n === null || e.skipEmptyString && n === "" ? t : n === null ? [
+					...t,
+					[f(r, e), "[", a, "]"].join("")
+				] : [
+					...t,
+					[f(r, e), "[", f(a, e), "]=", f(n, e)].join("")
+				];
+			};
+		case "bracket":
+			return (r) => (t, n) => n === void 0 || e.skipNull && n === null || e.skipEmptyString && n === "" ? t : n === null ? [
+				...t,
+				[f(r, e), "[]"].join("")
+			] : [
+				...t,
+				[f(r, e), "[]=", f(n, e)].join("")
+			];
+		case "colon-list-separator":
+			return (r) => (t, n) => n === void 0 || e.skipNull && n === null || e.skipEmptyString && n === "" ? t : n === null ? [
+				...t,
+				[f(r, e), ":list="].join("")
+			] : [
+				...t,
+				[f(r, e), ":list=", f(n, e)].join("")
+			];
+		case "comma":
+		case "separator":
+		case "bracket-separator": {
+			const r = e.arrayFormat === "bracket-separator" ? "[]=" : "=";
+			return (t) => (n, a) => a === void 0 || e.skipNull && a === null || e.skipEmptyString && a === "" ? n : (a = a === null ? "" : a, n.length === 0 ? [[f(t, e), r, f(a, e)].join("")] : [[n, f(a, e)].join(e.arrayFormatSeparator)]);
+		}
+		default:
+			return (r) => (t, n) => n === void 0 || e.skipNull && n === null || e.skipEmptyString && n === "" ? t : n === null ? [
+				...t,
+				f(r, e)
+			] : [
+				...t,
+				[f(r, e), "=", f(n, e)].join("")
+			];
+	}
+}
+function W(e) {
+	let r;
+	switch (e.arrayFormat) {
+		case "index":
+			return (t, n, a) => {
+				if (r = /\[(\d*)]$/.exec(t), t = t.replace(/\[\d*]$/, ""), !r) {
+					a[t] = n;
+					return;
+				}
+				a[t] === void 0 && (a[t] = {}), a[t][r[1]] = n;
+			};
+		case "bracket":
+			return (t, n, a) => {
+				if (r = /(\[])$/.exec(t), t = t.replace(/\[]$/, ""), !r) {
+					a[t] = n;
+					return;
+				}
+				if (a[t] === void 0) {
+					a[t] = [n];
+					return;
+				}
+				a[t] = [...a[t], n];
+			};
+		case "colon-list-separator":
+			return (t, n, a) => {
+				if (r = /(:list)$/.exec(t), t = t.replace(/:list$/, ""), !r) {
+					a[t] = n;
+					return;
+				}
+				if (a[t] === void 0) {
+					a[t] = [n];
+					return;
+				}
+				a[t] = [...a[t], n];
+			};
+		case "comma":
+		case "separator":
+			return (t, n, a) => {
+				const i = typeof n == "string" && n.includes(e.arrayFormatSeparator), s = typeof n == "string" && !i && d(n, e).includes(e.arrayFormatSeparator);
+				n = s ? d(n, e) : n;
+				const o = i || s ? n.split(e.arrayFormatSeparator).map((c) => d(c, e)) : n === null ? n : d(n, e);
+				a[t] = o;
+			};
+		case "bracket-separator":
+			return (t, n, a) => {
+				const i = /(\[])$/.test(t);
+				if (t = t.replace(/\[]$/, ""), !i) {
+					a[t] = n && d(n, e);
+					return;
+				}
+				const s = n === null ? [] : n.split(e.arrayFormatSeparator).map((o) => d(o, e));
+				if (a[t] === void 0) {
+					a[t] = s;
+					return;
+				}
+				a[t] = [...a[t], ...s];
+			};
+		default:
+			return (t, n, a) => {
+				if (a[t] === void 0) {
+					a[t] = n;
+					return;
+				}
+				a[t] = [...[a[t]].flat(), n];
+			};
+	}
+}
+function E(e) {
+	if (typeof e != "string" || e.length !== 1)
+		throw new TypeError("arrayFormatSeparator must be single character string");
+}
+function f(e, r) {
+	return r.encode ? r.strict ? T(e) : encodeURIComponent(e) : e;
+}
+function d(e, r) {
+	return r.decode ? P(e) : e;
+}
+function x(e) {
+	return Array.isArray(e) ? e.sort() : typeof e == "object" ? x(Object.keys(e)).sort((r, t) => Number(r) - Number(t)).map((r) => e[r]) : e;
+}
+function A(e) {
+	const r = e.indexOf("#");
+	return r !== -1 && (e = e.slice(0, r)), e;
+}
+function V(e) {
+	let r = "";
+	const t = e.indexOf("#");
+	return t !== -1 && (r = e.slice(t)), r;
+}
+function w(e, r) {
+	return r.parseNumbers && !Number.isNaN(Number(e)) && typeof e == "string" && e.trim() !== "" ? e = Number(e) : r.parseBooleans && e !== null && (e.toLowerCase() === "true" || e.toLowerCase() === "false") && (e = e.toLowerCase() === "true"), e;
+}
+function h(e) {
+	e = A(e);
+	const r = e.indexOf("?");
+	return r === -1 ? "" : e.slice(r + 1);
+}
+function g(e, r) {
+	r = {
+		decode: !0,
+		sort: !0,
+		arrayFormat: "none",
+		arrayFormatSeparator: ",",
+		parseNumbers: !1,
+		parseBooleans: !1,
+		...r
+	}, E(r.arrayFormatSeparator);
+	const t = W(r), n = /* @__PURE__ */ Object.create(null);
+	if (typeof e != "string" || (e = e.trim().replace(/^[?#&]/, ""), !e))
+		return n;
+	for (const a of e.split("&")) {
+		if (a === "")
+			continue;
+		const i = r.decode ? a.replaceAll("+", " ") : a;
+		let [s, o] = M(i, "=");
+		s === void 0 && (s = i), o = o === void 0 ? null : ["comma", "separator", "bracket-separator"].includes(r.arrayFormat) ? o : d(o, r), t(d(s, r), o, n);
+	}
+	for (const [a, i] of Object.entries(n))
+		if (typeof i == "object" && i !== null)
+			for (const [s, o] of Object.entries(i))
+				i[s] = w(o, r);
+		else
+			n[a] = w(i, r);
+	return r.sort === !1 ? n : (r.sort === !0 ? Object.keys(n).sort() : Object.keys(n).sort(r.sort)).reduce((a, i) => {
+		const s = n[i];
+		return a[i] = s && typeof s == "object" && !Array.isArray(s) ? x(s) : s, a;
+	}, /* @__PURE__ */ Object.create(null));
+}
+function C(e, r) {
+	if (!e)
+		return "";
+	r = {
+		encode: !0,
+		strict: !0,
+		arrayFormat: "none",
+		arrayFormatSeparator: ",",
+		...r
+	}, E(r.arrayFormatSeparator);
+	const t = (s) => r.skipNull && H(e[s]) || r.skipEmptyString && e[s] === "", n = B(r), a = {};
+	for (const [s, o] of Object.entries(e))
+		t(s) || (a[s] = o);
+	const i = Object.keys(a);
+	return r.sort !== !1 && i.sort(r.sort), i.map((s) => {
+		const o = e[s];
+		return o === void 0 ? "" : o === null ? f(s, r) : Array.isArray(o) ? o.length === 0 && r.arrayFormat === "bracket-separator" ? f(s, r) + "[]" : o.reduce(n(s), []).join("&") : f(s, r) + "=" + f(o, r);
+	}).filter((s) => s.length > 0).join("&");
+}
+function O(e, r) {
+	var a;
+	r = {
+		decode: !0,
+		...r
+	};
+	let [t, n] = M(e, "#");
+	return t === void 0 && (t = e), {
+		url: ((a = t == null ? void 0 : t.split("?")) == null ? void 0 : a[0]) ?? "",
+		query: g(h(e), r),
+		...r && r.parseFragmentIdentifier && n ? { fragmentIdentifier: d(n, r) } : {}
+	};
+}
+function _(e, r) {
+	r = {
+		encode: !0,
+		strict: !0,
+		[m]: !0,
+		...r
+	};
+	const t = A(e.url).split("?")[0] || "", n = h(e.url), a = {
+		...g(n, { sort: !1 }),
+		...e.query
+	};
+	let i = C(a, r);
+	i && (i = `?${i}`);
+	let s = V(e.url);
+	if (typeof e.fragmentIdentifier == "string") {
+		const o = new URL(t);
+		o.hash = e.fragmentIdentifier, s = r[m] ? o.hash : `#${e.fragmentIdentifier}`;
+	}
+	return `${t}${i}${s}`;
+}
+function U(e, r, t) {
+	t = {
+		parseFragmentIdentifier: !0,
+		[m]: !1,
+		...t
+	};
+	const { url: n, query: a, fragmentIdentifier: i } = O(e, t);
+	return _({
+		url: n,
+		query: k(a, r),
+		fragmentIdentifier: i
+	}, t);
+}
+function K(e, r, t) {
+	const n = Array.isArray(r) ? (a) => !r.includes(a) : (a, i) => !r(a, i);
+	return U(e, n, t);
+}
+const z = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
+	__proto__: null,
+	exclude: K,
+	extract: h,
+	parse: g,
+	parseUrl: O,
+	pick: U,
+	stringify: C,
+	stringifyUrl: _
+}, Symbol.toStringTag, { value: "Module" })), j = {
+	BASE_URL: "https://docmee.cn"
+}, G = () => j.BASE_URL, S = (e) => {
+	const r = $[e], t = G();
+	return t.endsWith("/") ? `${t}${r}` : `${t}/${r}`;
+}, $ = {
+	dashboard: "sdk-ui/dashboard",
+	editor: "sdk-ui/editor",
+	creator: "sdk-ui/creator/0",
+	customTemplate: "sdk-ui/custom-template",
+	templateCreator: "sdk-ui/custom-template-creator",
+	templateMarker: "sdk-ui/marker"
+};
+class J {
+	/**
+	 *
+	 * @param {DocmeeUIConstructorOptions} options
+	 */
+	constructor({
+		token: r,
+		page: t = "dashboard",
+		container: n,
+		pptId: a,
+		onMessage: i,
+		DOMAIN: s,
+		...o
+	}) {
+		u(this, "docmeeHref", S("dashboard"));
+		u(this, "query", { iframe: "1" });
+		u(this, "iframe", null);
+		u(this, "onMessage", () => Promise.resolve(!0));
+		u(this, "iframeMounted", !1);
+		u(this, "initInterval", null);
+		this.onMessage = i, this.container = n, location.protocol.startsWith("file") && console.log(
+			"%c %s",
+			"color: red; background-color: #f7c600",
+			"🔴 不能在file协议下运行,请启动一个http服务来运行! 🔴 "
+		), r || console.log(
+			"%c 初始化时,token不能为空!",
+			"color: #d7514f; background-color: #2e2e2e"
+		), s && (j.BASE_URL = s), this.init({ token: r, page: t, pptId: a, ...o });
+	}
+	_postMessage(r) {
+		var t, n;
+		if (!this.iframe.contentWindow) throw new Error("iframe未挂载!");
+		(n = (t = this.iframe) == null ? void 0 : t.contentWindow) == null || n.postMessage(r, this.docmeeHref);
+	}
+	init({ token: r, page: t = "dashboard", ...n }) {
+		if (t === "editor" && !n.pptId)
+			throw new Error("初始化editor页面时,必须传入pptId");
+		this.query = Object.assign({}, this.query, n), this.docmeeHref = S(t), this.updateToken(r), this._initIframe(!0);
+	}
+	// 初始化iframe
+	_initIframe(r) {
+		const t = this.container, n = document.createElement("iframe"), a = location.href, i = z.stringifyUrl({
+			url: this.docmeeHref,
+			query: r ? { iframe: 1, targetOrigin: a } : this.query
+		});
+		n.src = i, n.style.width = "100%", n.style.height = "100%", n.style.border = "0", n.style.outline = "none", n.style.padding = "0px", n.setAttribute("allowfullscreen", "true"), this.iframe = n, this.iframeMounted = !1, t.innerHTML = "", t.appendChild(n), this.iframe.addEventListener("load", () => {
+			let s = 0;
+			setTimeout(() => {
+				this.iframeMounted || (this.initInterval = setInterval(() => {
+					if (this.iframeMounted || s >= 5)
+						return s = 0, clearInterval(this.initInterval);
+					r && this._postMessage({
+						type: "transParams",
+						data: this.query
+					}), s++;
+				}, 200));
+			}, 300), window.addEventListener("message", async (o) => {
+				var y, p;
+				if (o.source !== this.iframe.contentWindow) return;
+				const c = o.data;
+				if (r && (c.type === "mounted" || c.type === "invalid-token") && (this.iframeMounted = !0, c.type === "mounted" && this._postMessage({
+					type: "transParams",
+					data: this.query
+				})), c.type === "user-info" && (this.iframeMounted = !0), c.type.startsWith("before")) {
+					const N = await ((y = this.onMessage) == null ? void 0 : y.call(this, c));
+					this._postMessage({ data: N, type: `recover_${c.type}` });
+				} else
+					(p = this.onMessage) == null || p.call(this, c);
+			});
+		});
+	}
+	/**
+	 * 更新用户token
+	 * @param {string} latestToken 新的token
+	 */
+	updateToken(r) {
+		/(a|s)k_.+/.test(r) || console.error("token 错误!"), this.token = r, this.query.token = r, this.iframeMounted && this._postMessage({
+			type: "transParams",
+			data: {
+				token: r
+			}
+		});
+	}
+	/**
+	 * 卸载iframe
+	 */
+	destroy() {
+		this.container.innerHTML = "";
+	}
+	/**
+	 * 发送消息
+	 * @param {{type: 'warning' | 'success' | 'error' | 'info', content: string}}
+	 */
+	sendMessage(r) {
+		this._postMessage({ type: "message", data: r });
+	}
+	getInfo() {
+		this._postMessage({ type: "getInfo" });
+	}
+	navigate({ page: r, pptId: t, templateId: n }) {
+		if (!$[r]) throw new Error(`页面${r} 不存在`);
+		this._postMessage({
+			type: "nav",
+			data: {
+				page: r,
+				token: this.token,
+				pptId: t,
+				templateId: n
+			}
+		});
+	}
+	changeCreatorData(r, t = !1) {
+		this._postMessage({
+			type: "transParams",
+			data: { creatorData: { ...r, createNow: t } }
+		});
+	}
+	updateTemplate(r) {
+		this._postMessage({
+			type: "changeTemplateById",
+			data: { templateId: r }
+		});
+	}
+	showTemplateDialog(r = "system") {
+		this._postMessage({
+			type: "showTemplateDialog",
+			data: { type: r }
+		});
+	}
+	getCurrentPptInfo() {
+		this._postMessage({
+			type: "getCurrentPptInfo"
+		});
+	}
+	importCSS(r) {
+		this._postMessage({ type: "importCSS", data: { css: r } });
+	}
+}
+window.DocmeeUI = J;

+ 9 - 0
src/api/fanyi.ts

@@ -0,0 +1,9 @@
+import request from '@/utils/request/req';
+
+export function translation(data: any) {
+	return request({
+		url: '/chat/translation',
+		method: 'post',
+		data: data
+	})
+}

+ 9 - 13
src/api/knowledge.ts

@@ -17,8 +17,6 @@ export interface KnowledgeDetailDelReq {
 	docId:string; // 文档id
 }
 
-
-
 export interface SimpleGenerate {
 	model: string,
 	randomness: number,
@@ -27,6 +25,13 @@ export interface SimpleGenerate {
 	text: string
 }
 
+export function getKnowledge() {
+	return request({
+		url: '/knowledge/list',
+		method: 'get',
+	})
+}
+
 export function createKnowledgeReq(params:KnowledgeReq) {
 	return request({
 		url: '/knowledge/save',
@@ -35,17 +40,10 @@ export function createKnowledgeReq(params:KnowledgeReq) {
 	})
 }
 
-export function getKnowledge() {
-	return request({
-		url: '/knowledge/list',
-		method: 'get',
-	})
-}
 export function delKnowledge(params:KnowledgeDelReq) {
 	return request({
-		url: '/knowledge/remove',
+		url: '/knowledge/remove/'+params.kid,
 		method: 'post',
-		data: params,
 	})
 }
 
@@ -59,13 +57,11 @@ export function getKnowledgeDetail(kid: string) {
 
 export function delKnowledgeDetail(params:KnowledgeDetailDelReq) {
 	return request({
-		url: 'knowledge/attach/remove',
+		url: 'knowledge/attach/remove/'+params.kid,
 		method: 'post',
-		data: params,
 	})
 }
 
-
 export function getfragmentList(docId: string) {
 	return request({
 		url: '/knowledge/fragment/list/'+docId,

+ 19 - 1
src/api/mjapi.ts

@@ -353,7 +353,25 @@ export const getLastVersion=  async ()=>{
     return a;
 
 }
-
+export const isCanBase64Model=(model:string)=>{
+    //gpt-4o
+    //customVisionModel
+    let arr=['gpt-4o','gemini','1.5','sonnet','opus','deepseek' ];
+    for( let m of arr){
+        if(model.indexOf(m)>-1) return true
+    }
+    if(model.indexOf('gpt-4o')>-1 || ( model.indexOf('gemini')>-1 && model.indexOf('1.5')>-1 ) ){
+        return true
+    }
+    //if(model.indexOf('sonnet')>-1 ) return true ;
+    let visionArr=['gemini-pro-vision','gpt-4o-2024-08-06','gpt-4o-2024-11-20','gpt-4o','gpt-4o-2024-05-13','gpt-4o-mini','gpt-4o-mini-2024-07-18','gemini-pro-1.5','gpt-4-turbo','gpt-4-turbo-2024-04-09','gpt-4-vision-preview','luma-video','claude-3-5-sonnet-20240620' ,'claude-3-sonnet-20240229','claude-3-opus-20240229', defaultVisionModel() ]
+    if( homeStore.myData.session.customVisionModel ){ 
+        homeStore.myData.session.customVisionModel.split(/[ ,]+/ig).map( (v:string)=>{
+            visionArr.push( v.toLocaleLowerCase() )
+        });
+    }
+    return visionArr.indexOf(model)>-1
+}
 export const canVisionModel= (model:string)=>{
     //['gpt-4-all','gpt-4-v'].indexOf(model)==-1 && model.indexOf('gpt-4-gizmo')==-1
     if( ['gpt-4-all','gpt-4-v','gpt-4v','gpt-3.5-net'].indexOf(model)>-1 ) return true;

+ 12 - 16
src/api/openapi.ts

@@ -149,7 +149,7 @@ export const GptUploader =   ( _url :string, FormData:FormData )=>{
 }
 
 export const whisperUpload = ( FormData:FormData )=>{
-    const url = gptGetUrl('/audio');
+    const url = gptGetUrl('/chat/audio');
     let headers=   {'Content-Type': 'multipart/form-data','Authorization':'Bearer ' + getToken()}
     headers={...headers,...getHeaderAuthorization()}
     return new Promise<any>((resolve, reject) => {
@@ -187,7 +187,6 @@ export const subGPT= async (data:any, chat:Chat.Chat )=>{
 interface subModelType{
     kid: string;
     message:any[]
-    imageContent:any[]
     onMessage:(d:{text:string,isFinish:boolean})=>void
     onError?:(d?:any)=>void
     signal?:AbortSignal
@@ -214,18 +213,15 @@ export const getSystemMessage = (uuid?:number )=>{
     }
     if(  sysTem ) return sysTem;
     let model= gptConfigStore.myData.model;
-    let producer= 'You are ChatGPT, a large language model trained by OpenAI.'
-    if(model.includes('claude-3')) producer=  'You are Claude, a large language model trained by Anthropic.';
-      const DEFAULT_SYSTEM_TEMPLATE = `${producer}
+    let producer= 'Please respond in concise and clear language, prioritizing Chinese.'
+    const DEFAULT_SYSTEM_TEMPLATE = `${producer}
     Current model: ${model}
-    Current time: ${ new Date().toLocaleString()}
-    Latex inline: $x^2$
-    Latex block: $$e=mc^2$$`;
+    Current time: ${ new Date().toLocaleString()}`
     return DEFAULT_SYSTEM_TEMPLATE;
 
 }
 export const subModel= async (opt: subModelType)=>{
-    //
+
     const model= opt.model?? ( gptConfigStore.myData.model);
     let max_tokens= gptConfigStore.myData.max_tokens;
     let temperature= 0.5;
@@ -240,16 +236,14 @@ export const subModel= async (opt: subModelType)=>{
         frequency_penalty = gStore.frequency_penalty??frequency_penalty;
         max_tokens= gStore.max_tokens;
     }
-    if(model=='gpt-4-vision-preview' && max_tokens>2048) max_tokens=2048;
-
+   
     let body ={
             max_tokens ,
             model ,
             temperature,
             top_p,
             presence_penalty ,frequency_penalty,
-            "messages": opt.message,
-            "imageContent": opt.imageContent
+            "messages": opt.message
            ,stream:true
            ,kid:gptConfigStore.myData.kid
         }
@@ -260,6 +254,10 @@ export const subModel= async (opt: subModelType)=>{
         headers={...headers,...getHeaderAuthorization()}
         try {
             let url = "/chat/send"
+            if(gptConfigStore.myData.kid){
+                url = "/knowledge/send"
+            }
+        
          await fetchSSE( gptGetUrl(url),{
             method: 'POST',
             headers: headers,
@@ -428,7 +426,6 @@ export const countTokens= async ( dataSources:Chat.Chat[], input:string ,uuid:nu
     const model =myStore.model;
     const max= getModelMax(model );
     let unit= 1024;
-    if(  model=='gpt-4-1106-preview' || model=='gpt-4-vision-preview' ) unit=1000;
     rz.modelTokens= `${max}k`
     //cl100k_base.encode(input)
 
@@ -456,8 +453,7 @@ const getModelMax=( model:string )=>{
         return 64;
     }else if( model.indexOf('128k')>-1
     || model=='gpt-4-1106-preview'
-    || model=='gpt-4-0125-preview'
-    || model=='gpt-4-vision-preview' ){
+    || model=='gpt-4-0125-preview'){
         return 128;
     }else if( model.indexOf('gpt-4')>-1  ){
         max=8;

+ 17 - 0
src/api/ppt.ts

@@ -0,0 +1,17 @@
+import request from '@/utils/request/req';
+
+// 获取API Token
+export function getApiToken() {
+	return request({
+		url: '/ppt/getApiToken',
+		method: 'get',
+	})
+}
+
+// 成功回调
+export function successCallback() {
+	return request({
+		url: '/ppt/successCallback',
+		method: 'post',
+	})
+}

+ 87 - 0
src/api/store.ts

@@ -0,0 +1,87 @@
+import request from '@/utils/request/req';
+
+export interface RoleReq {
+	name:string; // 名称
+	description:string; //描述
+	avatar: string; //头像地址
+	appUrl:string;//应用地址  
+}
+
+export interface SimpleGenerate {
+	model: string,
+	randomness: number,
+	stability_boost: number,
+	voiceId: string,
+	text: string
+}
+
+export interface Character {
+	id:string;
+	name:string; // 名称
+	description:string; //描述
+	avatar: string; //头像地址
+	appUrl:string;//应用地址  
+  }
+
+export function createRole(params:RoleReq) {
+	return request({
+		url: '/system/voice/add',
+		method: 'post',
+		data: params,
+	})
+}
+
+
+export function simpleGenerateReq(params:SimpleGenerate) {
+	return request({
+		url: '/system/voice/simpleGenerate',
+		method: 'post',
+		data: params,
+	})
+}
+
+export function delRole(id:string) {
+	return request({
+		url: '/system/voice/'+ id,
+		method: 'delete',
+	})
+}
+
+export function getRole() {
+	return request({
+		url: '/system/voice/list',
+		method: 'get',
+	})
+}
+
+/**
+ * 获取应用信息
+ * 
+ * @returns 应用列表
+ * 
+ */
+export function getAppList() {
+	return request({
+		url: '/system/store/appList',
+		method: 'get'
+	})
+}
+
+/**
+ * 收藏声音市场角色
+ *  
+ */
+export function copyRoleList(item: any) {
+	return request({
+		url: '/system/voice/copyRole',
+		method: 'post',
+		data: item
+	})
+}
+
+
+
+
+
+
+

BIN
src/assets/fileType.png


+ 1 - 2
src/components/common/PromptStore/index.vue

@@ -267,9 +267,8 @@ const loading = ref<boolean>(false)
 								<p>{{ feature }}</p>
 							</div>
 						</div>
-						<n-button class="footer" :loading="loading" @click="getPayUrl1(plan.price, plan.name)"><IconSvg icon="add"></IconSvg>{{  $t('setting.recharge') }}</n-button>
+						<n-button class="footer" :loading="loading" @click="getPayUrl(plan.price, plan.name)"><IconSvg icon="add"></IconSvg>{{  $t('setting.recharge') }}</n-button>
 					</div>
-
 				</div>
 			</n-tab-pane>
 			<n-tab-pane :name="$t('setting.charge')">

+ 3 - 3
src/locales/zh-CN.ts

@@ -90,7 +90,7 @@ export default {
     loginSuccess: "登录成功!",
     loginFailed: "登录失败!",
     usernameOrPasswordEmpty: "用户名或者密码不能为空!",
-    emailOrPhone: "邮箱或者电话号码",
+    emailOrPhone: "账号登录",
     enterEmailOrPhone: "请输入邮箱或者电话号码",
     password: "密码",
     enterPassword: "请输入密码",
@@ -241,7 +241,7 @@ export default {
     token: "1000个Token大约相当于750个英文单词或400至500个汉字。在按Token计费的模型中,每使用1000个Token将进行一次扣费。",
     input: "请输入卡密",
     day: '天',
-    siderButton: '购买套餐',
+    siderButton: '应用市场',
     local: '本地',
     online: '在线',
     title: '标题',
@@ -344,7 +344,7 @@ export default {
     ,img2textinfo:'不知如何写提示词?用图生文试试!<br/>提交图片,出提示词'
     ,traning:'翻译中...'
     ,imgcreate:'生成图片'
-    ,imginfo:'其他参数:  <li>1 --no 忽略 --no car 图中不出现车 </li><li>2 --seed 可先获取种子 --seed 123456 </li> <li>3 --chaos 10 混合(范围:0-100)</li> <li>4 --tile 碎片化 </li>  <li>5 --sref 图片url 生成风格一致的图像 <li>6 --cref 图片url 生成<b>角色</b>一致的图像  </li> '
+    ,imginfo:''
     ,tStyle:'风格'
     ,tView:'视角'
     ,tShot:'人物镜头'

+ 60 - 49
src/router/index.ts

@@ -4,10 +4,6 @@ import { createRouter, createWebHashHistory } from 'vue-router'
 import { setupPageGuard } from './permission'
 import { ChatLayout } from '@/views/chat/layout'
 import mjlayout from '@/views/mj/layout.vue'
-import soundLayout from '@/views/sound/layout.vue'
-import sunoLayout from '@/views/suno/layout.vue'
-import lumaLayout from '@/views/luma/layout.vue'
-import knowLayout from '@/views/knowledge/layout.vue'
 
 const routes: RouteRecordRaw[] = [
   {
@@ -54,7 +50,7 @@ const routes: RouteRecordRaw[] = [
   {
     path: '/draw',
     name: 'Rootdraw',
-    component: mjlayout,
+    component: ChatLayout,
     redirect: '/draw/index',
     children: [
       {
@@ -66,29 +62,71 @@ const routes: RouteRecordRaw[] = [
   },
 
   {
-    path: '/sound',
-    name: 'Sound',
-    component: soundLayout,
-    redirect: '/sound/t',
+		path: "/fanyi",
+		name: "Fanyi",
+		component: ChatLayout,
+		redirect: "/fanyi/index",
+		children: [
+			{
+				path: "index",
+				name: "fanyi",
+				component: () => import("@/views/fanyi/index.vue"),
+			},
+		],
+	},
+
+  {
+		path: "/ppt",
+		name: "Ppt",
+		component: ChatLayout,
+		redirect: "/ppt/index",
+		children: [
+			{
+				path: "index",
+				name: "ppt",
+				component: () => import("@/views/ppt/index.vue"),
+			},
+		],
+	},
+
+  {
+    path: '/video',
+    name: 'Video',
+    component: ChatLayout,
+    redirect: '/video/index',
     children: [
       {
-        path: 't',
-        name: 'sound1',
-        component: () => import('@/views/sound/index.vue'),
+        path: '/video/:uuid?',
+        name: 'video',
+        component: () => import('@/views/luma/video.vue'),
       },
     ],
   },
-  
+
+	{
+		path: "/music",
+		name: "Music",
+		component: ChatLayout,
+		redirect: "/music/index",
+		children: [
+			{
+				path: "/music/:uuid?",
+				name: "music",
+				component: () => import("@/views/suno/music.vue"),
+			},
+		],
+	},
+
   {
-    path: '/roleList',
-    name: 'RoleList',
+    path: '/store',
+    name: 'Store',
     component: ChatLayout,
-    redirect: '/roleList/t',
+    redirect: '/store/t',
     children: [
       {
         path: 't',
-        name: 'roleList1',
-        component: () => import('@/views/sound/roleList.vue'),
+        name: 'store',
+        component: () => import('@/views/store/appList.vue'),
       },
     ],
   },
@@ -109,7 +147,7 @@ const routes: RouteRecordRaw[] = [
   {
     path: '/knowledge',
     name: 'Knowledge',
-    component: knowLayout,
+    component: ChatLayout,
     redirect: '/knowledge/t',
     children: [
       {
@@ -123,7 +161,7 @@ const routes: RouteRecordRaw[] = [
   {
     path: '/annex',
     name: 'Annex',
-    component: knowLayout,
+    component: ChatLayout,
     redirect: '/annex/t',
     children: [
       {
@@ -137,7 +175,7 @@ const routes: RouteRecordRaw[] = [
   {
     path: '/fragment',
     name: 'Fragment',
-    component: knowLayout,
+    component: ChatLayout,
     redirect: '/fragment/t',
     children: [
       {
@@ -148,34 +186,6 @@ const routes: RouteRecordRaw[] = [
     ],
   },
 
-  // {
-  //   path: '/music',
-  //   name: 'music',
-  //   component: sunoLayout,
-  //   redirect: '/music/index',
-  //   children: [
-  //     {
-  //       path: '/music/:uuid?',
-  //       name: 'music',
-  //       component: () => import('@/views/suno/music.vue'),
-  //     },
-  //   ],
-  // },
-
-  // {
-  //   path: '/video',
-  //   name: 'video',
-  //   component: lumaLayout,
-  //   redirect: '/video/index',
-  //   children: [
-  //     {
-  //       path: '/video/:uuid?',
-  //       name: 'video',
-  //       component: () => import('@/views/luma/video.vue'),
-  //     },
-  //   ],
-  // },
-
   {
     path: '/404',
     name: '404',
@@ -187,6 +197,7 @@ const routes: RouteRecordRaw[] = [
     name: 'Login',
     component: () => import('@/views/login/index.vue'),
   },
+
   {
     path: '/regist',
     name: 'regist',

+ 1 - 1
src/store/homeStore.ts

@@ -52,7 +52,7 @@ const getGptInt= ():gptConfigType =>{
 }
 
 const  getDefault=()=>{
-const amodel = homeStore.myData.session.amodel??'o1-mini-2024-09-12'
+const amodel = homeStore.myData.session.amodel??'gpt-4o-mini'
 let v:gptConfigType={
     model: amodel,
     modelLabel: '',

+ 2 - 1
src/store/modules/app/helper.ts

@@ -8,12 +8,13 @@ export type Language = 'zh-CN' | 'zh-TW' | 'en-US' | 'ko-KR' | 'ru-RU'
 
 export interface AppState {
   siderCollapsed: boolean
+  isChat: boolean
   theme: Theme
   language: Language
 }
 
 export function defaultSetting(): AppState {
-  return { siderCollapsed: false, theme: 'dark', language: 'zh-CN' }
+  return { siderCollapsed: false,isChat:true, theme: 'dark', language: 'zh-CN' }
 }
 
 export function getLocalSetting(): AppState {

+ 4 - 0
src/store/modules/app/index.ts

@@ -10,6 +10,10 @@ export const useAppStore = defineStore('app-store', {
       this.siderCollapsed = collapsed
       this.recordState()
     },
+    setIsChat(chat: boolean) {
+      this.isChat = chat
+      this.recordState()
+    },
 
     setTheme(theme: Theme) {
       this.theme = theme

+ 1 - 1
src/store/modules/settings/helper.ts

@@ -10,7 +10,7 @@ export interface SettingsState {
 
 export function defaultSetting(): SettingsState {
   return {
-    systemMessage: 'You are ChatGPT, a large language model trained by OpenAI. Follow the user\'s instructions carefully. Respond using markdown.',
+    systemMessage: 'Follow the user\'s instructions carefully. Respond using markdown.',
     temperature: 0.8,
     top_p: 1,
   }

+ 70 - 35
src/styles/lib/highlight.less

@@ -122,7 +122,7 @@ html.dark {
 			background-color: #232627;
 			.user-free{
 				color: #000;
-			}	
+			}
 			.top{
 				.avatar{
 					.circle{
@@ -218,16 +218,25 @@ html.dark {
 			background-color: #232627;
 		}
 		.message-reply{
-			background-color: #000;
+			background-color: #232728;
 		}
 		.message-request{
-			background-color: #0084FF;
+			background-color: #A1DC95;
+
 		}
-		.message-reply,
+
 		.message-request{
 			.markdown-body{
 				p{
-					color: #fff;
+					font-size: 15px;
+					color: #000;
+				}
+			}
+		}
+		.message-reply{
+			.markdown-body{
+				p{
+					color: #FFFFFF;
 				}
 			}
 		}
@@ -292,7 +301,7 @@ html.dark {
 			&:hover{
 				color: #0084ff !important;
 			}
-				
+
 		}
 	}
 	.draw-tabs .n-tabs-bar{
@@ -301,7 +310,7 @@ html.dark {
 	.n-slider-rail{
 		.n-slider-rail__fill{
 			background-color: #0084ff;
-				
+
 		}
 	}
 	.voice-drawer{
@@ -336,7 +345,7 @@ html.dark {
 					&:nth-child(2) {
 						border-left: none;
 						border-right: none;
-	
+
 					}
 					&:nth-child(3) {
 						border-left: none;
@@ -434,8 +443,8 @@ html.dark {
 		background-color: #232627;
 		.card-container{
 			.card-item{
-				background-color: #2b2f30;
-				border: 1px solid #505050;
+				background-color: #232627;
+				border: 1px solid #232627;
 				.ellipsis{
 					color: #aaacac;
 				}
@@ -680,7 +689,7 @@ html {
 
 .n-layout-scroll-container{
 	background-color: #e8eaf1;
-	padding-right: 12px;
+	padding-right: 0px;
 }
 .is-mobile{.n-layout-scroll-container{
 	padding-right: 0;
@@ -759,21 +768,24 @@ position: relative;
 		background-color: #3fdd78;
 		}
 	}
-	.user-name{
+	.user-name {
 		font-family: 'Inter';
 		font-style: normal;
 		font-size: 14px;
-		// color: #FEFEFE;
 		float: left;
-		min-width: 100px;
-	}
+		min-width: 130px; /* 设置最大宽度 */
+		max-width: 160px; /* 设置最大宽度 */
+		overflow: hidden; /* 隐藏超出部分 */
+		white-space: nowrap; /* 防止文本换行 */
+		text-overflow: ellipsis; /* 显示省略号 */
+	  }
 	.user-free{
-		float: right;
-		height: 24px;
-		line-height: 24px;
+		float: left;
+		height: 20px;
+		line-height: 20px;
+		margin-top: 2px;
 		padding: 0 12px;
 		background-color: #3fdd78;
-
 		border-radius: 6px;
 		font-size: 12px;
 		font-weight: 600;
@@ -961,6 +973,7 @@ position: relative;
 		.card-item{
 			height: 152px;
 			border: 1px solid #e5e7eb;
+			box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.2);  /* 添加阴影效果 */
 			h3{
 				font-size: 17px;
 				font-weight: 700;
@@ -979,11 +992,11 @@ position: relative;
 			.button-list{
 				justify-content: flex-end;
 				button{
-					height: 24px;
-					line-height: 24px;
+					height: 28px;
+					line-height: 28px;
 					text-align: center;
 					padding: 0 15px;
-					font-size: 12px;
+					font-size: 13px;
 					margin-left: 10px;
 				}
 			}
@@ -1276,7 +1289,7 @@ position: relative;
 			}
 		}
 	}
-	
+
 	.n-tabs-nav-scroll-content{
 		border: 0 !important;
 		margin-bottom: 20px;
@@ -1309,9 +1322,9 @@ position: relative;
 }
 .table-box{
 	background-color: #fff;
-	padding: 20px 46px 20px 20px;
-	height: calc(100% - 164px);
-	border-bottom-right-radius: 20px;
+	//padding: 20px 46px 20px 20px;
+	height: calc(100%);
+	border-bottom-right-radius: 0px;
 }
 .draw-table-box{
 	.n-data-table-wrapper{
@@ -1321,8 +1334,8 @@ position: relative;
 }
 
 .chat-content {
-	height: calc(100vh - 48px);
-	margin-top: 24px;
+	height: calc(100vh);
+	margin-top: 0px;
 	main{
 		border-top-right-radius: 20px;
 	}
@@ -1583,13 +1596,35 @@ position: relative;
 				box-shadow: none;
 			}
 		}
+		// .hander-login{
+		// 	display: flex; /* 使用 flex 布局 */
+		// 	justify-content: space-between; /* 水平分布,两端对齐 */
+		// 	align-items: center; /* 垂直居中 */
+		// 	margin-top: 8px;
+		// 	margin-left: 80px;
+		// 	width: 60%;
+		// 	button{
+		// 		height: 42px;
+		// 		line-height: 42px;
+		// 		border-radius: 8px;
+		// 		// background-color: #0084FF;
+		// 		background-color: #232627;
+				
+		// 		color: #fff;
+		// 		font-size: 18px;
+		// 		padding: 0;
+		// 		margin-left: 10px;
+		// 		width: 100%;
+		// 		// box-shadow: none;
+		// 	}
+		// }
 	}
 }
 .music-content{
 	background-color: #fff;
-	height: calc(100% - 48px);
-	margin-top: 24px;
-	border-radius: 20px;
+	height: calc(100%);
+	margin-top: 0px;
+	border-radius: 0px;
 	.music-list{
 		background-color: #fff;
 		padding: 15px;
@@ -1601,9 +1636,9 @@ position: relative;
 }
 .video-content{
 	background-color: #fff;
-	height: calc(100% - 48px);
-	margin-top: 24px;
-	border-radius: 20px;
+	height: calc(100%);
+	margin-top: 0px;
+	border-radius: 0px;
 	.p-2{
 		padding: 15px;
 		.upload-video{
@@ -1618,4 +1653,4 @@ position: relative;
 }
 .annex-main{
 	background-color: #fff;
-}
+}

+ 1 - 0
src/views/chat/components/Message/index.vue

@@ -156,6 +156,7 @@ function handleRegenerate2() {
           :as-raw-text="asRawText"
           :chat="chat"
         />
+
         <div class="flex flex-col" v-if="!chat.mjID && chat.model!='dall-e-3' && chat.model!='dall-e-2' ">
           <!-- <button
             v-if="!inversion "

+ 7 - 2
src/views/chat/index.vue

@@ -14,7 +14,7 @@ import { useUsingContext } from './hooks/useUsingContext'
 import { getGpts } from '@/api/chatmsg';
 import {  SvgIcon } from '@/components/common'
 import { useBasicLayout } from '@/hooks/useBasicLayout'
-import { gptConfigStore, gptsUlistStore, homeStore, useChatStore, usePromptStore } from '@/store'
+import { gptConfigStore, gptsUlistStore, homeStore, useChatStore, usePromptStore,useAppStore } from '@/store'
 import { chatSetting, fetchChatAPIProcess, gptsType, mlog, myFetch, my2Fetch } from '@/api'
 import { t } from '@/locales'
 import drawListVue from '../mj/drawList.vue'
@@ -56,6 +56,9 @@ const promptStore = usePromptStore()
 // 使用storeToRefs,保证store修改后,联想部分能够重新渲染
 const { promptList: promptTemplate } = storeToRefs<any>(promptStore)
 
+const appStore = useAppStore()
+const isChat = computed(() => appStore.isChat)
+
 // 未知原因刷新页面,loading 状态不会重置,手动重置
 dataSources.value.forEach((item, index) => {
   if (item.loading)
@@ -513,6 +516,7 @@ const footerClass = computed(() => {
   return classes
 })
 
+
 onMounted(() => {
   scrollToBottom()
   if (inputRef.value && !isMobile.value)
@@ -752,9 +756,10 @@ load()
         </div>
       </div>
     </main>
+    
     <footer :class="footerClass" class="footer-content" v-if="local!=='draw'">
       <div class="w-full max-w-screen-xl m-auto">
-        <aiGptInput @handle-clear="handleClear" @export="handleExport" v-if="['gpt-4-vision-preview','gpt-3.5-turbo-16k'].indexOf(gptConfigStore.myData.model)>-1 || st.inputme "
+        <aiGptInput @handle-clear="handleClear" @export="handleExport" v-if="['gpt-4o-mini','gpt-3.5-turbo-16k'].indexOf(gptConfigStore.myData.model)>-1 || st.inputme "
          v-model:modelValue="prompt" :disabled="buttonDisabled"
          :searchOptions="searchOptions"  :renderOption="renderOption"
           />

+ 0 - 2
src/views/chat/layout/Layout.vue

@@ -33,8 +33,6 @@ if(rt.name =='GPTs'){
   ms.success( t('mj.modleSuccess') );
 }
 
- 
-
 router.replace({ name: 'Chat', params: { uuid: chatStore.active } })
 homeStore.setMyData({local:'Chat'});
 const { isMobile } = useBasicLayout()

+ 138 - 24
src/views/chat/layout/sider/index.vue

@@ -1,6 +1,6 @@
 <script setup lang='ts'>
-import type { CSSProperties } from 'vue'
-import { computed, ref, watch } from 'vue'
+import { computed,defineAsyncComponent , onMounted} from "vue";
+import { ref, watch } from 'vue'
 import { NButton, NLayoutSider, useDialog } from 'naive-ui'
 import List from './List.vue'
 import Footer from './Footer.vue'
@@ -8,6 +8,15 @@ import { useAppStore, useChatStore, homeStore } from '@/store'
 import { useBasicLayout } from '@/hooks/useBasicLayout'
 import { IconSvg, PromptStore, SvgIcon } from '@/components/common'
 import { t } from '@/locales'
+import { defaultSetting,UserInfo } from '@/store/modules/user/helper'
+import { useRouter } from 'vue-router'
+import { loginOut,getUserInfo} from '@/api/user'
+import { removeToken } from '@/store/modules/auth/helper'
+import to from "await-to-js";
+import { getToken } from "@/store/modules/auth/helper";
+import { useRoute } from 'vue-router';
+
+const Setting = defineAsyncComponent(() => import('@/components/common/Setting/index.vue'))
 
 const appStore = useAppStore()
 const chatStore = useChatStore()
@@ -16,8 +25,14 @@ const dialog = useDialog()
 const { isMobile } = useBasicLayout()
 const show = ref(false)
 
+
 const collapsed = computed(() => appStore.siderCollapsed)
 
+
+onMounted(() => {
+  getLoginUserInfo();
+});
+
 function handleAdd() {
   chatStore.addHistory({ title: 'New Chat', uuid: Date.now(), isEdit: false })
   if (isMobile.value)
@@ -50,20 +65,20 @@ const getMobileClass = computed<CSSProperties>(() => {
       height: '100%'
     }
   }
-  if(appStore.theme == 'dark') {
+  if (appStore.theme == 'dark') {
     return {
-        height: 'calc(100% - 48px)',
-        marginTop: '24px',
-        borderTopLeftRadius: '20px',
-        borderBottomLeftRadius: '20px',
-        backgroundColor: '#232627'
+      height: 'calc(100%)',
+      marginTop: '0px',
+      borderTopLeftRadius: '0px',
+      borderBottomLeftRadius: '0px',
+      backgroundColor: '#232627'
     }
-  }else{
+  } else {
     return {
-      height: 'calc(100% - 48px)',
-      marginTop: '24px',
-      borderTopLeftRadius: '20px',
-      borderBottomLeftRadius: '20px',
+      height: 'calc(100%)',
+      marginTop: '0px',
+      borderTopLeftRadius: '0px',
+      borderBottomLeftRadius: '0px',
       backgroundColor: '#fff'
     }
   }
@@ -88,20 +103,66 @@ watch(
     flush: 'post',
   },
 )
+
+const router1 = useRouter()
+const userInfo = ref<UserInfo>(defaultSetting().userInfo)
+const st= ref({'show':false,showImg:false, menu:[],active:'chat'})
+
+const isLogin =computed(  () => {
+  return localStorage.getItem('TOKEN')
+});
+
+
+
+async function longin() {
+    // 跳转到登录页面
+    router1.push('/login')
+}
+
+async function store() {
+    // 跳转到应用商店
+    router1.push('/store')
+}
+
+ async function handleReset() {
+    await loginOut()
+    // 删除用户token
+    removeToken();
+    // 跳转到登录页面
+    router1.push('/login')
+}
+
+
+/**
+ * 获取当前登录用户信息
+ */
+ async function getLoginUserInfo() {
+  // 用户未登录,不需要获取用户信息
+  if(!getToken()){
+      return
+  }
+  const [err, newUserInfo] = await to(getUserInfo());
+      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;
+    userInfo.value.userBalance = newUserInfo.data.user.userBalance;
+    userInfo.value.userName = newUserInfo.data.user.userName;
+    isLogin.value = true
+  }
+}
+
 </script>
 
 <template>
-  <NLayoutSider
-    :collapsed="collapsed"
-    :collapsed-width="0"
-    :width="348"
-    :show-trigger="isMobile ? false : 'arrow-circle'"
-    collapse-mode="transform"
-    bordered
-    v-if="homeStore.myData.local == 'Chat'"
-    :style="getMobileClass"
-    @update-collapsed="handleUpdateCollapsed"
-  >
+  <NLayoutSider :collapsed="collapsed" :collapsed-width="0" :width="348"
+    :show-trigger="isMobile ? false : 'arrow-circle'" collapse-mode="transform" bordered
+    v-if="homeStore.myData.local == 'Chat'" :style="getMobileClass" @update-collapsed="handleUpdateCollapsed">
     <div class="flex flex-col h-full char-sider" :style="mobileSafeArea">
       <main class="flex flex-col flex-1 min-h-0">
         <div class="p-4 top-new-button">
@@ -126,9 +187,62 @@ watch(
       </main>
       <Footer v-if="isMobile"></Footer>
     </div>
+    <div class="nav-bar">
+      <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>
+            <div>
+              <p class="user-name">{{ userInfo.userName ?? '熊猫助手' }}</p>
+              <n-button @click="show = true" style="float: right;" size="small" tertiary>充值</n-button>
+            </div>
+
+            <div> 
+              <span class="user-free">¥ {{ userInfo.userBalance }}元</span>
+            </div>
+          </div>
+
+          <div class="user-bottom" @click="store">
+            <Button block>
+              <!-- 应用市场 -->
+              {{ $t('store.siderButton') }}
+            </Button>
+          </div>
+        </div>
+
+        <div v-show="!isLogin" class="user-bottom" @click="longin">
+          <Button block>
+            {{ $t('store.login') }}
+          </Button>
+        </div>
+
+      </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>
+
+
+    
   </NLayoutSider>
   <template v-if="isMobile">
     <div v-show="!collapsed" class="fixed inset-0 z-40 w-full h-full bg-black/40" @click="handleUpdateCollapsed" />
   </template>
+
+  <Setting v-if="st.show" v-model:visible="st.show" />
   <PromptStore v-model:visible="show"></PromptStore>
+
+
 </template>

+ 155 - 0
src/views/fanyi/components/documentComponent.vue

@@ -0,0 +1,155 @@
+<template>
+	<div class="document-container" v-if="fileList.length === 0">
+		<n-upload action="/api/system/user/edit/avatar" :max=1 directory-dnd :default-file-list="fileList"
+			:headers="headers" @finish="handleFinish">
+			<n-upload-dragger>
+				<div class="document-container-img">
+					<img src="@/assets/fileType.png" alt="">
+				</div>
+				<div>
+					<n-p depth="3" style="margin: 8px 0 0 0">
+						点击或拖拽上传,即享翻译与AI问答
+					</n-p>
+				</div>
+				<n-p depth="3" style="margin: 8px 0 0 0">支持 pdf, word, ppt, xls,txt,epub,srt,xml 多种格式文档</n-p>
+			</n-upload-dragger>
+		</n-upload>
+	</div>
+	<div class="document-containers" v-else>
+		<div class="name-fileList">
+			<div v-for="item in 2" class="file-item" :class="{ 'file-item-selected': selectedFileId === item }"
+				@click="selectedFileId = item" :key="item">
+				<p class="file-name">文件名称{{ item }}</p>
+				<p class="file-id">时间:2025/1/9</p>
+			</div>
+		</div>
+		<div class="name-content">
+			<div :style="{ borderRight: aiAnswer ? '1px dashed #ccc' : 'none' }">
+				<vue-office-pdf :src="pdf" style="height: calc(100vh - 120px)" @rendered="rendered" />
+			</div>
+			<div v-if="aiAnswer">2</div>
+		</div>
+		<div class="name-button">
+			<n-button strong secondary round type="success" style="margin-bottom: 10px;"
+				@click="aiAnswer = !aiAnswer">AI问答</n-button>
+			<n-button strong secondary round type="success">下载文档</n-button>
+		</div>
+	</div>
+</template>
+
+<script setup lang="ts">
+import { ref } from 'vue'
+import { NUpload, UploadFileInfo, useMessage, NUploadDragger, NP, NButton } from 'naive-ui'
+import { getToken } from '@/store/modules/auth/helper'
+import VueOfficePdf from '@vue-office/pdf'
+const message = useMessage()
+const token = getToken()
+const headers = {
+	Authorization: `Bearer ${token}`
+}
+let aiAnswer = ref(false)
+let pdf = ref('http://static.shanhuxueyuan.com/test.pdf')
+const selectedFileId = ref<number | null>(1)
+function rendered(e: any) {
+	console.log(e)
+}
+let fileList = ref<UploadFileInfo[]>([
+	{
+		id: 'avatar',
+		name: '头像预览',
+		status: 'finished',
+		url: 'http://panda-1253683406.cos.ap-guangzhou.myqcloud.com/panda/2024/01/03/0e3600b455914b0dade9943f281be19b.png'
+	},
+])
+
+function handleFinish({
+	event
+}: {
+	file: UploadFileInfo
+	event?: ProgressEvent
+}) {
+	const ext = (event?.target as XMLHttpRequest).response
+	let file = {
+		id: 'avatar',
+		name: '头像预览',
+		status: 'finished',
+		url: ext
+	}
+	fileList.value.push(file as UploadFileInfo)
+	message.success('上传成功!')
+}
+</script>
+
+<style scoped lang="less">
+.document-container {
+	height: calc(100vh - 100px);
+
+	.document-container-img {
+		display: flex;
+		justify-content: center;
+		align-items: center;
+		height: auto;
+		width: 300px;
+		margin: 0 auto;
+	}
+}
+
+.document-containers {
+	height: calc(100vh - 100px);
+	display: flex;
+	justify-content: space-between;
+	border: 1px solid #ccc;
+	border-radius: 10px;
+
+	div {
+		padding: 10px;
+	}
+
+	.name-fileList {
+		width: 300px;
+		height: 100%;
+		border-right: 1px solid #ccc;
+
+		.file-item {
+			border: 1px solid #ccc;
+			margin-bottom: 10px;
+			border-radius: 10px;
+			cursor: pointer;
+		}
+
+		.file-item:hover {
+			background-color: #19bdee;
+		}
+
+		.file-item-selected {
+			background-color: #19bdee;
+			color: white;
+		}
+
+	}
+
+	.name-content {
+		width: 100%;
+		height: 100%;
+		display: flex;
+		justify-content: space-between;
+
+		div:nth-child(1) {
+			flex: 1;
+			padding: 0;
+		}
+
+		div:nth-child(2) {
+			flex: 1;
+			padding: 0 0 0 10px;
+			border-left: 1px dashed #ccc;
+		}
+	}
+
+	.name-button {
+		width: 150px;
+		height: 100%;
+		border-left: 1px solid #ccc;
+	}
+}
+</style>

+ 144 - 0
src/views/fanyi/components/textComponent.vue

@@ -0,0 +1,144 @@
+<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>
+				<div>
+					<n-space vertical>
+						<n-select v-model:value="targetLanguage" :options="options1" />
+					</n-space>
+				</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>
+
+				<n-button @click="handleTranslation" type="primary">翻译</n-button>
+			</div>
+			<n-input v-model:value="prompt" type="textarea" placeholder="请输入文本" :resizable="false" />
+
+		</div>
+		<div class="name-textBox-right">
+			<div class="name-textBox-right-content">
+				{{ translationResult }}
+			</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: '英文' },
+])
+const options1 = ref([
+	{ label: '中文', value: '中文' },
+	{ label: '英文', value: '英文' },
+])
+
+onMounted(() => {
+	getModelList()
+})
+
+async function getModelList() {
+	const res = await modelList()
+	modelListData.value = res.data
+	model.value = modelListData.value[0].modelDescribe
+}
+
+async function handleTranslation() {
+	if (!prompt.value) {
+		return;
+	}
+	const res = await translation({
+		prompt: prompt.value,
+		sourceLanguage: sourceLanguage.value,
+		targetLanguage: targetLanguage.value,
+		model: model.value,
+	})
+	translationResult.value = res
+}
+</script>
+
+<style scoped lang="less">
+.name-textBox {
+	display: flex;
+
+	:deep(.n-input) {
+
+		.n-input__border,
+		.n-input__state-border {
+			border: none !important;
+		}
+
+		textarea {
+			min-height: 500px !important;
+		}
+	}
+
+	.name-textBox-left {
+		flex: 1;
+		height: calc(100vh - 100px);
+		border-radius: 10px;
+
+		padding: 10px;
+
+		.name-selectBox {
+			display: flex;
+			justify-content: space-between;
+			margin-bottom: 10px;
+
+			div {
+				flex: 1;
+			}
+		}
+	}
+
+	.name-textBox-right {
+		flex: 1;
+		min-height: 500px;
+		border-radius: 10px;
+		padding: 10px;
+
+
+		.name-selectBox {
+			margin-bottom: 10px;
+		}
+
+		.name-textBox-right-content {
+			min-height: 500px;
+			border: 1px solid #ccc;
+			border-radius: 10px;
+			padding: 10px;
+			margin-top: 40px;
+		}
+	}
+
+
+
+	div {
+		margin-right: 20px;
+	}
+}
+</style>

+ 26 - 0
src/views/fanyi/index.vue

@@ -0,0 +1,26 @@
+<template>
+	<div class="fanyi-container">
+		<n-card>
+			<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-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'
+
+const activeTab = ref('1')
+</script>
+
+
+<style scoped lang="less"></style>

+ 61 - 0
src/views/fanyi/layout.vue

@@ -0,0 +1,61 @@
+<script setup lang='ts'>
+import { computed } from 'vue'
+import { NLayout, NLayoutContent } from 'naive-ui'
+import { useRouter } from 'vue-router'
+import Permission from '../chat/layout/Permission.vue'
+import { useBasicLayout } from '@/hooks/useBasicLayout'
+import { homeStore, useAppStore, useAuthStore, useChatStore } from '@/store'
+import { aiSider, aiFooter } from '@/views/mj'
+import aiMobileMenu from '@/views/mj/aiMobileMenu.vue';
+
+const router = useRouter()
+const appStore = useAppStore()
+const chatStore = useChatStore()
+const authStore = useAuthStore()
+
+router.replace({ name: 'fanyi', params: { uuid: chatStore.active } })
+homeStore.setMyData({ local: 'fanyi' });
+const { isMobile } = useBasicLayout()
+
+const collapsed = computed(() => appStore.siderCollapsed)
+
+const needPermission = computed(() => !!authStore.session?.auth && !authStore.token)
+
+const getMobileClass = computed(() => {
+    if (isMobile.value)
+        return ['rounded-none', 'shadow-none']
+    return ['shadow-md', 'dark:border-neutral-800'] //'border', 'rounded-md',
+})
+
+const getContainerClass = computed(() => {
+    return [
+        'h-full',
+        { 'abc': !isMobile.value && !collapsed.value },
+    ]
+}) 
+</script>
+
+<template>
+    <div class="dark:bg-[#24272e] transition-all p-0" :class="[isMobile ? 'h55' : 'h-full']">
+        <div class="h-full overflow-hidden" :class="getMobileClass">
+            <NLayout class="z-40 transition" :class="getContainerClass" has-sider
+                :sider-placement="isMobile ? 'left' : 'right'">
+                <aiSider v-if="!isMobile" />
+
+                <NLayoutContent class="h-full">
+                    <RouterView v-slot="{ Component, route }">
+                        <component :is="Component" :key="route.fullPath" />
+                    </RouterView>
+                </NLayoutContent>
+            </NLayout>
+        </div>
+        <Permission :visible="needPermission" />
+    </div>
+    <aiMobileMenu v-if="isMobile" />
+    <aiFooter />
+</template>
+<style>
+.h55 {
+    height: calc(100% - 55px);
+}
+</style>

+ 9 - 7
src/views/knowledge/annex.vue

@@ -143,7 +143,7 @@ const fetchData = async () => {
     if (err) {
       message.error(err.message)
     } else {
-      tableData.value = result;
+      tableData.value = result.rows;
     }
   } catch (error) {
     console.error('Error fetching data:', error);
@@ -154,15 +154,17 @@ const columns = ref(createColumns());
 
 </script>
 <template>
-<br>
-<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>
+
+
 
 <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"

+ 9 - 7
src/views/knowledge/fragment.vue

@@ -72,11 +72,11 @@ const fetchData = async () => {
     try {
         // 发起一个请求
         const [err, result] = await to(getfragmentList(docId.value));
-        console.log("result===", result)
+        console.log("fragmenresult===", result)
         if (err) {
             message.error(err.message)
         } else {
-            tableData.value = result;
+            tableData.value = result.rows;
         }
     } catch (error) {
         console.error('Error fetching data:', error);
@@ -88,14 +88,16 @@ const columns = ref(createColumns());
 
 </script>
 <template>
-    <br>
-    <div style="display: flex; justify-content: flex-start; margin:10px;border-top-left-radius: 20px;" class="know-header">
+  
+
+    <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">
+      </div> 
+
       <n-data-table 
         :columns="columns"
         :data="tableData"

+ 204 - 136
src/views/knowledge/index.vue

@@ -1,6 +1,10 @@
 <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 {
+	NButton, NDataTable, DrawerPlacement, NDrawer,
+	NDrawerContent, NForm, NFormItem, NInput, NDivider,
+	NSpace, useMessage, NGrid, NGi,NSwitch,NInputNumber,NSelect,NSlider 
+} from 'naive-ui';
 import { createKnowledgeReq, getKnowledge, delKnowledge } from '@/api/knowledge';
 import to from 'await-to-js';
 import { useRouter } from 'vue-router';
@@ -13,52 +17,59 @@ const message = useMessage();
 
 // 初始化表单数据对象
 const formValue = ref({
-  id: '', // 知识库id
-  kid: '', // 附件id
-  uid: '', // 用户id  
-  kname: '', // 知识库名称
-  description: '', // 知识库描述 
+	id: '', // 知识库id
+	kid: '', // 附件id
+	uid: '', // 用户id
+	kname: '', // 知识库名称
+	share: '0', // 是否分享
+	description: '', // 知识库描述
 });
 
 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(t('knowledge.createKnowledgeBaseSuccess'));
-    // 重新获取数据,更新表格
-    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(t('knowledge.deleteFailed'));
-  } else {
-    message.success(t('knowledge.deleteSuccess'));
-  }
-  // 重新获取数据,更新表格
-  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 来创建响应式变量
@@ -66,117 +77,174 @@ const active = ref(false);
 const placement = ref<DrawerPlacement>('right');
 
 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;
-    }
-  } 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>
-<br>
-<div style="display: flex; justify-content: flex-start; margin:10px;" class="know-header">
-    <n-button @click="activate('right')" type="primary" :bordered="false" class="success-button">
-       {{ $t('knowledge.createKnowledgeBase') }}
-    </n-button>
-</div>
-
-<div class="flex h-full table-box" style="border-bottom-left-radius: 20px;"> 
-    <main class="flex-1 overflow-hidden h-full">
-      <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="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="600" :placement="placement">
+		<n-drawer-content :title="$t('knowledge.createKnowledgeBase')">
+			{{ $t('knowledge.createYourKnowledgeBase') }}
+			<n-divider />
+			<n-space vertical>
+
+				<n-form ref="formRef" :label-width="80" :model="formValue">
+
+					<n-grid x-gap="12" :cols="2">
+						<n-gi>
+							<n-form-item label="知识库名称">
+								<n-input v-model:value="formValue.kname" placeholder="请输入知识库名称" />
+							</n-form-item>
+						</n-gi>
+				
+						<n-gi>
+							<n-form-item label="分隔符" >
+								<n-input  placeholder="请输入知识分隔符" />
+							</n-form-item>
+						</n-gi>
+
+						<n-gi >
+							<n-form-item label="知识库中检索的条数" >
+								<n-input-number 
+									placeholder="请输入检索条数" />
+							</n-form-item>
+						</n-gi>
+
+						<n-gi >
+							<n-form-item label="文本块大小" path="phone">
+								<n-input-number  placeholder="请输入文本块大小"/>
+					
+							</n-form-item>
+						</n-gi>
+
+
+						<n-gi >
+							<n-form-item label="是否公开">
+								<n-switch size="large" checked-value="1" unchecked-value="0"
+									@update:value="handleUpdateValue" />
+							</n-form-item>
+						</n-gi>
+
+						<n-gi>
+							<n-form-item label="重叠字符" path="formValue.kname">
+								<n-input-number
+									placeholder="请输入重叠字符数" />
+							</n-form-item>
+						</n-gi>
+
+				
+						<n-gi :span="24">
+							<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-gi>
+
+						<n-gi>
+							<n-form-item label="向量模型" path="formValue.description">
+							<n-select
+								placeholder="请选择向量模型"
+							></n-select>
+						    </n-form-item>
+						</n-gi>
+
+						<n-gi >
+							<n-form-item label="提问分割符" path="phone">
+								<n-input placeholder="请输入提问分割符"/>
+							</n-form-item>
+						</n-gi>
+			
+						<n-gi  :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-gi>
+					</n-grid>
+				</n-form>
+
+
+			</n-space>
+		</n-drawer-content>
+	</n-drawer>
 </template>

+ 138 - 139
src/views/login/index.vue

@@ -1,14 +1,19 @@
-<script setup lang='ts'>
-import { onMounted, ref, onUnmounted } from 'vue';
-import { useRouter } from 'vue-router';
-import { useMessage, NButton, NInput, NImage, NModal, NCard } from 'naive-ui';
-import { LoginFrom } from '@/typings/user';
-import { getConfigKey, getMpQrCode, getLoginType, authSystem } from '@/api/user';
-import { useUserStore } from '@/store/modules/user';
+<script setup lang="ts">
+import { onMounted, ref, onUnmounted } from "vue";
+import { useRouter } from "vue-router";
+import { useMessage, NButton, NInput, NImage, NModal, NCard } from "naive-ui";
+import { LoginFrom } from "@/typings/user";
+import {
+	getConfigKey,
+	getMpQrCode,
+	getLoginType,
+	authSystem,
+} from "@/api/user";
+import { useUserStore } from "@/store/modules/user";
 import to from "await-to-js";
-import { useI18n } from 'vue-i18n';
-import { useBasicLayout } from '@/hooks/useBasicLayout'
-const { isMobile } = useBasicLayout()
+import { useI18n } from "vue-i18n";
+import { useBasicLayout } from "@/hooks/useBasicLayout";
+const { isMobile } = useBasicLayout();
 const { t } = useI18n();
 
 const userStore = useUserStore();
@@ -17,27 +22,27 @@ const message = useMessage();
 const user = ref<LoginFrom>(Object.create(null));
 
 // 点击登录
-let loginLoading = ref(false)
+let loginLoading = ref(false);
 async function handleValidateButtonClick(e: MouseEvent) {
 	e.preventDefault();
 	const { username, password } = user.value;
 	if (!validateAccount(username)) {
-		message.error(t('login.accountFormatError'));
+		message.error(t("login.accountFormatError"));
 		return;
 	}
 	if (username && password) {
-		loginLoading.value = true
+		loginLoading.value = true;
 		const [err] = await to(userStore.userLogin(user.value));
 		if (!err) {
-			message.success(t('login.loginSuccess'));
-			await router.push('/');
-			loginLoading.value = false
+			message.success(t("login.loginSuccess"));
+			await router.push("/");
+			loginLoading.value = false;
 		} else {
 			message.error(err.message);
-			loginLoading.value = false
+			loginLoading.value = false;
 		}
 	} else {
-		message.error(t('login.usernameOrPasswordEmpty'));
+		message.error(t("login.usernameOrPasswordEmpty"));
 	}
 }
 
@@ -51,8 +56,8 @@ function validateAccount(account: string) {
 }
 
 const handleRegistBtnClick = async (e: MouseEvent) => {
-	router.push('/regist');
-}
+	router.push("/regist");
+};
 
 const copyright = ref("");
 
@@ -64,35 +69,15 @@ const code = ref("");
 
 // 在组件挂载后执行异步操作
 onMounted(async () => {
-	try {
-		const [err, res] = await to(getConfigKey("copyright"));
-		if (err) {
-			console.error("获取配置失败", err.message);
-		} else {
-			copyright.value = res.msg;
-		}
-
-		const [err1, res1] = await to(getConfigKey("logoImage"));
-		if (err1) {
-			console.error("获取配置失败", err1.message);
-		} else {
-			logo.value = res1.msg;
-		}
-
-		const [err2, res2] = await to(getConfigKey("activate"));
-		if (err2) {
-			console.error("获取配置失败", err1.message);
-		} else {
-			activate.value = res2.msg === 'true';
-		}
-
-	} catch (error) {
-		console.error("获取配置失败", error);
+	const [err, res] = await to(getConfigKey("copyright"));
+	if (err) {
+		console.error("获取配置失败", err.message);
+	} else {
+		copyright.value = res.msg;
 	}
-
 });
 
-const activeTab = ref('login');
+const activeTab = ref("login");
 
 const showModal = ref(false);
 
@@ -104,28 +89,19 @@ let intervalId: string | number | NodeJS.Timer | undefined;
 // 定义轮询间隔时间,例如每3秒轮询一次
 const POLLING_INTERVAL = 3000;
 
-async function handleBeforeLeave(tabName: string) {
-	activeTab.value = tabName;
-	if (tabName == "qr") {
-		//获取二维码信息
-		const [err1, res1] = await to(getMpQrCode());
-		if (err1) {
-			message.error("获取二维码失败: " + err1.message);
-		} else {
-			qrCode.value = res1.data.qrCodeUrl;
-			ticket.value = res1.data.ticket;
-			intervalId = setInterval(slectLoginType, POLLING_INTERVAL);
-		}
-	}
-}
-
-async function sysAuth() {
-	const [err, res] = await to(authSystem(code.value));
-	if (err) {
-		message.error(t('login.activationFailed'));
+async function handleWxLogin() {
+	
+	showModal.value = true
+	//获取二维码信息
+	const [err1, res1] = await to(getMpQrCode());
+	if (err1) {
+		message.error("获取二维码失败: " + err1.message);
 	} else {
-		message.success(t('login.activationSuccess'));
+		qrCode.value = res1.data.qrCodeUrl;
+		ticket.value = res1.data.ticket;
+		intervalId = setInterval(slectLoginType, POLLING_INTERVAL);
 	}
+	
 }
 
 // 1. 定时查询是否登录成功
@@ -138,8 +114,8 @@ async function slectLoginType() {
 			userStore.userQrLogin(res.data.token);
 			clearInterval(intervalId);
 			// 3. 跳转到主页
-			message.success(t('login.loginSuccess'));
-			await router.push('/');
+			message.success(t("login.loginSuccess"));
+			await router.push("/");
 		}
 	}
 }
@@ -154,110 +130,124 @@ onUnmounted(() => {
 
 <template>
 	<div id="app">
-		<br><br><br><br>
+		<br /><br /><br /><br />
 		<div class="flex justify-center mt-8 md:mt-0">
-			<img style="border-radius: 60px; width: 120px; height: 120px;" :src="logo" alt="Robot Icon" class="h-12 w-fit hover:cursor-pointer md:h-16">
+			<img style="border-radius: 60px; width: 120px; height: 120px" :src="logo" alt="Robot Icon"
+				class="h-12 w-fit hover:cursor-pointer md:h-16" />
 		</div>
-		<br>
-		<div
-			class="relative w-full bg-white mt-10 overflow-hidden shadow-xl ring-1 sm:mx-auto sm:h-min sm:max-w-4xl sm:rounded-lg lg:max-w-5xl 2xl:max-w-6xl login-box" :style="{width: isMobile ? '100%' : '880px'}">
+		<br />
+		<div class="relative w-full bg-white mt-10 overflow-hidden shadow-xl ring-1 sm:mx-auto sm:h-min sm:max-w-4xl sm:rounded-lg lg:max-w-5xl 2xl:max-w-6xl login-box"
+			:style="{ width: isMobile ? '100%' : '880px' }">
 			<div class="px-6 pt-4 pb-8 sm:px-10">
 				<main class="mx-auto sm:max-w-4xl lg:max-w-5xl 2xl:max-w-6xl">
-					<!-- <div class="flex justify-start mb-4">
-						<button @click="handleBeforeLeave('login')"
-							:class="{ 'bg-teal-500 text-white': activeTab === 'login', 'bg-white text-gray-700': activeTab !== 'login' }"
-							class="px-4 py-2 font-medium rounded-md hover:bg-teal-600 hover:text-white focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-teal-500 mr-2">
-							{{ $t('login.emailOrPhone') }}
-						</button>
-				
-				
-						<button @click="handleBeforeLeave('qr')"
-							:class="{ 'bg-teal-500 text-white': activeTab === 'qr', 'bg-white text-gray-700': activeTab !== 'qr' }"
-							class="px-4 py-2 font-medium rounded-md hover:bg-teal-600 hover:text-white focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-teal-500">
-							{{ $t('login.activateSystem') }}
-						</button>
-
-					</div> -->
 					<div v-if="activeTab === 'login'">
 						<!-- 登录表单 -->
 						<div class="flex flex-col justify-center my-4 space-y-8">
 							<div class="mx-auto w-full max-w-md">
-								<h2 class="text-3xl font-bold text-center text-gray-900">{{ $t('login.login') }}</h2>
+								<h2 class="text-3xl font-bold text-center text-gray-900">
+									{{ $t("login.login") }}
+								</h2>
 								<p class="mt-2 text-sm text-center text-gray-600 login-desc">
-									{{ $t('login.or') }} <a @click="handleRegistBtnClick"
-										class="font-semibold text-teal-500 hover:text-teal-600">{{ $t('login.register') }}</a> {{ $t('login.andExperience') }}
+									{{ $t("login.or") }}
+									<a @click="handleRegistBtnClick"
+										class="font-semibold text-teal-500 hover:text-teal-600">{{ $t("login.register")
+										}}</a>
+									{{ $t("login.andExperience") }}
 								</p>
 							</div>
 							<div class="mx-auto w-full max-w-sm">
-								<form class="space-y-6" :style="{width: !isMobile ? '580px' : 'calc(100% - 20px)', marginLeft: isMobile ? '10px' : 'calc(50% - 290px)'}">
+								<form class="space-y-6" :style="{
+									width: !isMobile ? '580px' : 'calc(100% - 20px)',
+									marginLeft: isMobile ? '10px' : 'calc(50% - 290px)',
+								}">
 									<div>
-										<label for="email"
-											class="block text-sm font-medium text-gray-700">{{ $t('login.emailOrPhone') }}</label>
+										<label for="email" class="block text-sm font-medium text-gray-700">{{
+											$t("login.emailOrPhone") }}</label>
 										<div class="mt-1">
 											<input id="email" v-model="user.username"
 												:allow-input="(val: string) => { return !/[^A-Za-z0-9_@.]/g.test(val) }"
-												maxlength="32" :placeholder="$t('login.enterEmailOrPhone')" name="email" type="email"
-												autocomplete="email" required
-												class="block w-full px-3 py-2 placeholder-gray-400 border border-gray-300 rounded-md focus:border-teal-500 focus:outline-none focus:ring-teal-500">
+												maxlength="32" :placeholder="$t('login.enterEmailOrPhone')" name="email"
+												type="email" autocomplete="email" required
+												class="block w-full px-3 py-2 placeholder-gray-400 border border-gray-300 rounded-md focus:border-teal-500 focus:outline-none focus:ring-teal-500" />
 										</div>
 									</div>
 									<div>
-										<label for="password" class="block text-sm font-medium text-gray-700">{{ $t('login.password') }}</label>
+										<label for="password" class="block text-sm font-medium text-gray-700">{{
+											$t("login.password") }}</label>
 										<div class="mt-1">
 											<input id="password" maxLength="16" v-model="user.password"
-												:placeholder="$t('login.enterPassword')" name="password" type="password" required
-												class="block w-full px-3 py-2 placeholder-gray-400 border border-gray-300 rounded-md focus:border-teal-500 focus:outline-none focus:ring-teal-500">
+												:placeholder="$t('login.enterPassword')" name="password" type="password"
+												required
+												class="block w-full px-3 py-2 placeholder-gray-400 border border-gray-300 rounded-md focus:border-teal-500 focus:outline-none focus:ring-teal-500" />
 										</div>
 										<a style="color: #0084ff; font-weight: 500" href="#/resetpassword"
-											class="float-right mt-2 text-sm font-semibold text-teal-500 hover:text-teal-600">{{ $t('login.forgotPassword') }}</a>
+											class="float-right mt-2 text-sm font-semibold text-teal-500 hover:text-teal-600">{{
+												$t("login.forgotPassword") }}</a>
 									</div>
 									<div class="footer-login">
-										<n-button :loading="loginLoading" @click="handleValidateButtonClick"
-											class="w-full px-4 py-2 font-medium text-white bg-teal-500 rounded-md hover:bg-teal-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-teal-500">{{ $t('login.login') }}</n-button>
+										<n-button :loading="loginLoading" @click="handleValidateButtonClick">{{
+											$t("login.login") }}</n-button>
+
+										<n-button @click="handleWxLogin">微信登录</n-button>
 									</div>
 								</form>
 							</div>
 						</div>
 					</div>
-					<div v-else-if="activeTab === 'qr'">
-						<!-- 扫码登录 -->
-						<div class="flex flex-col items-center justify-center">
-							<p class="text-sm text-center">{{ $t('login.scanQrCodeLogin') }}</p>
-							<n-image width="320" :src="qrCode" />
-						</div>
-					</div>
+
+					<!-- 扫码登录 -->
+					<n-modal
+						v-model:show="showModal"
+						title="请扫描下方二维码登录"
+						preset="card"
+						draggable
+						:style="{ width: '400px' }"
+ 						 >
+   					 <n-image width="350" :src="qrCode" />
+ 					</n-modal>
 				</main>
 			</div>
 		</div>
 		<div class="footer">
-			<a target="_blank" style="color: #999999; font-size: 14px;" href="https://beian.miit.gov.cn/">
+			<a target="_blank" style="color: #999999; font-size: 14px" href="https://beian.miit.gov.cn/">
 				&nbsp;{{ copyright }}
 			</a>
 		</div>
-		<div v-if="!activate" id="specialDiv">
-  			<p>{{ $t('login.systemNotActivated') }}
-				<n-button size="small" secondary strong @click="showModal = true">{{ $t('login.activate') }}</n-button>
+		<!-- <div v-if="!activate" id="specialDiv">
+			<p>
+				{{ $t("login.systemNotActivated") }}
+				<n-button size="small" secondary strong @click="showModal = true">{{
+					$t("login.activate")
+				}}</n-button>
 			</p>
-		</div>
+		</div> -->
 	</div>
-  <n-modal v-model:show="showModal">
-    <n-card
-      style="width: 600px"
-      :bordered="false"
-      size="huge"
-      role="dialog"
-      aria-modal="true"
-    >
-	      <p class="text-sm text-center" style="margin-right: 10px;">{{ $t('login.enterAuthCode') }}</p>
-			<br>
-			<div style="display: flex; align-items: center; justify-content: center;">
-				<input id="code" v-model="code" maxlength="32" :placeholder="$t('login.activationCode')" 
-				 class="block w-full px-3 py-2 placeholder-gray-400 border border-gray-300 rounded-md focus:border-teal-500 focus:outline-none focus:ring-teal-500"/>
-				<n-button style="margin-left: 10px;" @click="sysAuth()">{{ $t('login.activate') }}</n-button>
-		</div>
-    </n-card>
-  </n-modal>
-
+	<!-- <n-modal v-model:show="showModal">
+		<n-card
+			style="width: 600px"
+			:bordered="false"
+			size="huge"
+			role="dialog"
+			aria-modal="true"
+		>
+			<p class="text-sm text-center" style="margin-right: 10px">
+				{{ $t("login.enterAuthCode") }}
+			</p>
+			<br />
+			<div style="display: flex; align-items: center; justify-content: center">
+				<input
+					id="code"
+					v-model="code"
+					maxlength="32"
+					:placeholder="$t('login.activationCode')"
+					class="block w-full px-3 py-2 placeholder-gray-400 border border-gray-300 rounded-md focus:border-teal-500 focus:outline-none focus:ring-teal-500"
+				/>
+				<n-button style="margin-left: 10px" @click="sysAuth()">{{
+					$t("login.activate")
+				}}</n-button>
+			</div>
+		</n-card>
+	</n-modal> -->
 </template>
 
 <style scoped>
@@ -280,18 +270,27 @@ onUnmounted(() => {
 	margin-top: auto;
 }
 
+.custom-card {
+	width: 500px;
+}
+
 input {
 	color: black !important;
 }
 
 #specialDiv {
-  position: relative; /* 使内部的绝对定位元素相对定位 */
+	position: relative;
+	/* 使内部的绝对定位元素相对定位 */
 }
 
 #specialDiv p {
-  position: absolute; /* 绝对定位 */
-  bottom: 0; /* 将元素对齐到底部 */
-  right: 0; /* 将元素对齐到右边 */
-  margin: 0; /* 去除默认的段落外边距 */
+	position: absolute;
+	/* 绝对定位 */
+	bottom: 0;
+	/* 将元素对齐到底部 */
+	right: 0;
+	/* 将元素对齐到右边 */
+	margin: 0;
+	/* 去除默认的段落外边距 */
 }
-</style>
+</style>

+ 24 - 37
src/views/mj/aiGpt.vue

@@ -4,7 +4,7 @@ import { useRoute } from 'vue-router'
 import { useChat } from '../chat/hooks/useChat'
 import {  homeStore, useChatStore } from '@/store'
 import { getInitChat, mlog, subModel,getSystemMessage , localSaveAny, canVisionModel
-    ,isTTS, subTTS, file2blob, whisperUpload, getHistoryMessage, checkDisableGpt4, chatSetting } from '@/api'
+    ,isTTS, subTTS, file2blob, whisperUpload, getHistoryMessage, checkDisableGpt4, chatSetting, isCanBase64Model, } from '@/api'
 //import { isNumber } from '@/utils/is'
 import { useMessage  } from "naive-ui";
 import { t } from "@/locales";
@@ -64,7 +64,7 @@ watch(()=>homeStore.myData.act, async (n)=>{
 
         let promptMsg = getInitChat(dd.prompt );
         if( dd.fileBase64 && dd.fileBase64.length>0 ){
-            if( !canVisionModel(model)  ) model='gpt-4-vision-preview';
+            if( !canVisionModel(model)  ) model='gpt-4o-mini';
 
             try{
                     let images= await localSaveAny( JSON.stringify( dd.fileBase64)  ) ;
@@ -147,29 +147,17 @@ watch(()=>homeStore.myData.act, async (n)=>{
         //return ;
         let message= [ {  "role": "system", "content": getSystemMessage(  +uuid2) },
                 ...historyMesg ];
-        let imageContent = [];
         if( dd.fileBase64 && dd.fileBase64.length>0 ){
-            if(  model=='gpt-4-vision-preview' ){
+            if(isCanBase64Model(model)){ 
                 let obj={
                         "role": "user",
-                        "content": ""
+                       "content": [] as any
                 }
-                // //"Generate code for a web page that looks exactly like this."
-                //obj.content.push({,      "text": dd.prompt  });
-                imageContent.push(  {
-                        "type": "text",
-                        "text": dd.prompt,
-                    })
+                obj.content.push({ "type": "text",      "text": dd.prompt  });
                 dd.fileBase64.forEach((f:any)=>{
-                    //obj.content.push({ "type": "image_url",  "image_url": {url:f }   });
-                    imageContent.push(
-                        {
-                            "type": "image_url",
-                            "image_url": { url: f }
-                        }
-                    )
+                    obj.content.push({ "type": "image_url",  "image_url": {url:f }   });
                 });
-                message.push(obj);
+                message.push(obj); 
             }else{
                 let cc= dd.prompt;
                 //附件需要时远程的图片链接 或者文件 链接
@@ -187,7 +175,7 @@ watch(()=>homeStore.myData.act, async (n)=>{
             }
         }
 
-        submit(model,message,imageContent,opt);
+        submit(model,message,opt);
 
     }else if(n=='abort'){
        controller.value && controller.value.abort();
@@ -257,7 +245,7 @@ watch(()=>homeStore.myData.act, async (n)=>{
     }
 })
 
-const submit= (model:string, message:any[],imageContent?:any[],opt?:any)=>{
+const submit= (model:string, message:any[],opt?:any)=>{
     mlog('提交Model', model  );
     const chatSet = new chatSetting(   +st.value.uuid  );
     const nGptStore =   chatSet.getGptConfig()  ;
@@ -305,22 +293,21 @@ const submit= (model:string, message:any[],imageContent?:any[],opt?:any)=>{
             });
 
         }else{
-        //controller.signal
-            subModel( {message,model,imageContent
-            ,uuid:st.value.uuid //当前会话
-            ,onMessage:(d)=>{
-                mlog('🐞消息',d);
-                textRz.value.push(d.text);
-            }
-            ,onError:(e:any)=>{
-                mlog('onError',e)
-                let  emsg =   (JSON.stringify(  e.reason? JSON.parse( e.reason ):e,null,2));
-                //if(emsg=='{}' ) emsg= JSON.stringify(e );
-
-                if(e.message!='canceled' && emsg.indexOf('aborted')==-1 ) textRz.value.push("\n"+t('mjchat.failReason')+"\n```\n"+emsg+"\n```\n");
-                goFinish();
-            }
-            ,signal:controller.value.signal,
+            subModel( {message, model,
+                uuid: st.value.uuid //当前会话
+                ,onMessage: (d) => {
+                    mlog('🐞消息', d)
+                    textRz.value.push(d.text)
+                },
+                onError: (e: any) => {
+                    mlog('onError', e)
+                    let emsg = (JSON.stringify(e.reason ? JSON.parse(e.reason) : e, null, 2))
+                    //if(emsg=='{}' ) emsg= JSON.stringify(e );
+                    if (e.message != 'canceled' && emsg.indexOf('aborted') == -1) textRz.value.push("\n" + t('mjchat.failReason') + "\n```\n" + emsg + "\n```\n")
+                    goFinish()
+                },
+                signal: controller.value.signal,
+                kid: ''
             }).then(()=>goFinish() ).catch(e=>{
                 if(e.message!='canceled')  textRz.value.push("\n"+t('mj.fail')+":\n```\n"+(e.reason??JSON.stringify(e,null,2)) +"\n```\n")
                 goFinish();

+ 1 - 1
src/views/mj/aiGptInput.vue

@@ -333,7 +333,7 @@ function handleClear() {
             <template v-else>
               <SvgIcon icon="heroicons:sparkles" />
               <span>{{
-                nGptStore.modelLabel ? nGptStore.modelLabel : "o1-mini-2024-09-12"
+                nGptStore.modelLabel ? nGptStore.modelLabel : "gpt-4o-mini"
               }}</span>
             </template>
             <SvgIcon icon="icon-park-outline:right" />

+ 26 - 3
src/views/mj/aiModel.vue

@@ -17,7 +17,7 @@ const chatSet = new chatSetting( uuid==null?1002:uuid);
 
 const nGptStore = ref(  chatSet.getGptConfig() );
 const message = useMessage()
-onMounted(() => { fetchData()});
+onMounted(() => { fetchData(),fetchDataGetKnowledge() });
 
 
 const config = ref([])
@@ -37,6 +37,29 @@ const fetchData = async () => {
     }
 };
 
+const fetchDataGetKnowledge = async () => {
+    if(getToken()){
+        try {
+       // 发起一个请求
+      const [err, result] = await to(getKnowledge());
+      console.log("result===", result.rows)
+      if (err) {
+        ms.error(err.message)
+      } else {
+        options.value = result.rows.map((item: any) => ({
+            label: item.kname, // 假设后台返回的数据有 'name' 字段
+            value: item.id     // 假设每个数据项都有一个唯一的 'id' 字段
+        }));
+
+        // 请求成功
+        options.value.push({ label: 'please select', value: '' });
+      }
+    } catch (error) {
+      console.error('Error fetching data:', error);
+    }
+    }
+  };
+
 const st= ref({openMore:false });
 const voiceList= computed(()=>{
     let rz=[];
@@ -134,10 +157,10 @@ const reSet=()=>{
     </n-input>
  </section>
 
- <!-- <section class="mb-5 justify-between items-center"  >
+ <section class="mb-5 justify-between items-center"  >
      <div  style="margin-bottom: 8px;">{{ $t('mjchat.knowledgeBase') }} </div>
     <n-select class="change-select" v-model:value="nGptStore.kid" :options="options" @update:value="onSelectChange" size="small"   />
-</section> -->
+</section>
 
  <section class=" flex justify-between items-center"  >
      <div style="margin-bottom: 8px;"> {{ $t('mjchat.historyCnt') }}

+ 21 - 28
src/views/mj/aiSider.vue

@@ -11,6 +11,7 @@ import { loginOut,getUserInfo} from '@/api/user'
 import { UserData } from "@/typings/user"
 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";
 const chatStore = useChatStore()
 //import gallery from '@/views/gallery/index.vue'
@@ -55,6 +56,10 @@ onMounted(() => {
  * 获取当前登录用户信息
  */
  async function getLoginUserInfo() {
+  // 用户未登录,不需要获取用户信息
+  if(!getToken()){
+      return
+  }
   const [err, newUserInfo] = await to(getUserInfo<UserData>());
       if (err) {
        // message.error(err.toString())
@@ -90,7 +95,7 @@ async function longin() {
 
 </script>
 <template>
-<div class="flex-shrink-0 w-[60px] z-[1000]  h-full nav-bar" v-if="!isMobile">
+<!-- <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">
@@ -142,30 +147,18 @@ async function longin() {
 						</n-tooltip>
 					</a>
 
-
-       <!--   <a :href="`#/video`" @click="st.active='video';homeStore.setMyData({act:'video'}) " 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=='video' ? 'active' : '']">
-									<IconSvg icon="video" width="18" height="18"></IconSvg>
-									<span class="text-[18px]">{{ $t('video.menu') }}</span>
-								</div>
-							</template>
-              {{ $t('video.menuinfo') }}
-						</n-tooltip>
-					</a>
-
-          <a :href="`#/wxbot`" @click="st.active='wxbot'" 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=='wxbot' ? 'active' : '']">
-									<SvgIcon icon="mdi-robot" class="text-3xl flex-1"></SvgIcon>
-									<span class="text-[10px]">{{$t('mjtab.bot')}}</span>
-								</div>
-							</template>
-							{{$t('mjtab.botinfo')}}
-						</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>
 
 
@@ -208,9 +201,9 @@ async function longin() {
           </div>
         </div>
     </div>
-</div>
- <Setting v-if="st.show" v-model:visible="st.show" />
- <PromptStore v-model:visible="show"></PromptStore>
+</div> -->
+ <!-- <Setting v-if="st.show" v-model:visible="st.show" />
+ <PromptStore v-model:visible="show"></PromptStore> -->
 
 </template>
 

+ 112 - 0
src/views/ppt/index.vue

@@ -0,0 +1,112 @@
+<template>
+	<div id="ppt-container">
+		<!-- 这里可以是SDK初始化后生成的内容 -->
+	</div>
+</template>
+
+<script setup>
+import { onMounted } from 'vue';
+import { getApiToken, successCallback } from '@/api/ppt';
+import { useMessage } from 'naive-ui';
+
+const message = useMessage();
+
+// 假设SDK文件在public目录下
+const sdkScriptUrl = '/docmee-ui-sdk-iframe.min.js';
+let token = '';
+
+async function getToken() {
+	const res = await getApiToken();
+	if (res.code == 200) {
+		token = res.msg;
+		initPPT();
+	} else {
+		message.error('获取API Token失败');
+	}
+}
+
+onMounted(() => {
+	getToken();
+});
+
+function initPPT() {
+	// 动态加载SDK脚本
+	const script = document.createElement('script');
+	script.src = sdkScriptUrl;
+	script.onload = () => {
+		// 假设SDK提供了一个全局对象或方法来初始化
+		if (window.DocmeeUI) {
+			// 初始化 UI iframe
+			const docmeeUI = new window.DocmeeUI({
+				pptId: null,
+				token: token, // token
+				container: document.querySelector("#ppt-container"), // 挂载 iframe 的容器
+				page: "creator", // 'creator' 创建页面; 'dashboard' PPT列表; 'customTemplate' 自定义模版; 'editor' 编辑页(需要传pptId字段)
+				lang: "zh", // 国际化
+				mode: "light", // light 亮色模式, dark 暗色模式
+				isMobile: false, // 移动端模式
+				background: "linear-gradient(-157deg,#f57bb0, #867dea)", // 自定义背景
+				padding: "40px 20px 0px",
+				async onMessage(message) {
+					console.log("监听事件", message);
+					if (message.type === "invalid-token") {
+						console.log("token 认证错误");
+						// 更换新的 token
+						// let newToken = createApiToken()
+						// docmeeUI.updateToken(newToken)
+					} else if (message.type === "beforeGenerate") {
+						const { subtype, fields } = message.data;
+						if (subtype === "outline") {
+							console.log("即将生成ppt大纲", fields);
+							return true;
+						} else if (subtype === "ppt") {
+							console.log("即将生成ppt", fields);
+							const res = await successCallback();
+							if (res.code == 200) {
+								docmeeUI.sendMessage({
+									type: "success",
+									content: "继续生成PPT",
+								});
+								return true;
+							} else {
+								message.error('生成PPT失败');
+								return false;
+							}
+						}
+					} else if (message.type === "beforeCreateCustomTemplate") {
+						const { file, totalPptCount } = message.data;
+						console.log("用户自定义完整模版,PPT文件:", file.name);
+						if (totalPptCount < 2) {
+							console.log("用户积分不足,不允许制作自定义完整模版");
+							return false;
+						}
+						return true;
+					} else if (message.type == "pageChange") {
+						pageChange(message.data.page);
+					} else if (message.type === "beforeDownload") {
+						const { id, subject } = message.data;
+						return `PPT_${subject}.pptx`;
+					} else if (message.type == "error") {
+						if (message.data.code == 88) {
+							alert("您的次数已用完");
+						} else {
+							alert("发生错误:" + message.data.message);
+						}
+					}
+				},
+			});
+		} else {
+			console.error("DocmeeUI SDK未加载");
+		}
+	};
+	document.body.appendChild(script);
+}
+</script>
+
+<style scoped lang="less">
+#ppt-container {
+	width: 100%;
+	height: calc(100% - 20px);
+	overflow: hidden;
+}
+</style>

+ 61 - 0
src/views/ppt/layout.vue

@@ -0,0 +1,61 @@
+<script setup lang='ts'>
+import { computed } from 'vue'
+import { NLayout, NLayoutContent } from 'naive-ui'
+import { useRouter } from 'vue-router'
+import Permission from '../chat/layout/Permission.vue'
+import { useBasicLayout } from '@/hooks/useBasicLayout'
+import { homeStore, useAppStore, useAuthStore, useChatStore } from '@/store'
+import { aiSider, aiFooter } from '@/views/mj'
+import aiMobileMenu from '@/views/mj/aiMobileMenu.vue';
+
+const router = useRouter()
+const appStore = useAppStore()
+const chatStore = useChatStore()
+const authStore = useAuthStore()
+
+router.replace({ name: 'ppt', params: { uuid: chatStore.active } })
+homeStore.setMyData({ local: 'ppt' });
+const { isMobile } = useBasicLayout()
+
+const collapsed = computed(() => appStore.siderCollapsed)
+
+const needPermission = computed(() => !!authStore.session?.auth && !authStore.token)
+
+const getMobileClass = computed(() => {
+	if (isMobile.value)
+		return ['rounded-none', 'shadow-none']
+	return ['shadow-md', 'dark:border-neutral-800'] //'border', 'rounded-md',
+})
+
+const getContainerClass = computed(() => {
+	return [
+		'h-full',
+		{ 'abc': !isMobile.value && !collapsed.value },
+	]
+})
+</script>
+
+<template>
+	<div class="dark:bg-[#24272e] transition-all p-0" :class="[isMobile ? 'h55' : 'h-full']">
+		<div class="h-full overflow-hidden" :class="getMobileClass">
+			<NLayout class="z-40 transition" :class="getContainerClass" has-sider
+				:sider-placement="isMobile ? 'left' : 'right'">
+				<aiSider v-if="!isMobile" />
+
+				<NLayoutContent class="h-full">
+					<RouterView v-slot="{ Component, route }">
+						<component :is="Component" :key="route.fullPath" />
+					</RouterView>
+				</NLayoutContent>
+			</NLayout>
+		</div>
+		<Permission :visible="needPermission" />
+	</div>
+	<aiMobileMenu v-if="isMobile" />
+	<aiFooter />
+</template>
+<style>
+.h55 {
+	height: calc(100% - 55px);
+}
+</style>

+ 132 - 0
src/views/store/appList.vue

@@ -0,0 +1,132 @@
+<script setup lang="ts">
+import { ref, computed, onMounted } from 'vue';
+import { NButton, NCard, NAvatar, NPagination, useMessage,NDivider,NTabs, NTabPane,NIcon} from 'naive-ui';
+import { getAppList, Character, copyRoleList } from '@/api/store';
+import to from 'await-to-js';
+import { t } from '@/locales';
+import { useRouter } from 'vue-router'
+import { useAppStore, useChatStore, homeStore } from '@/store'
+const allData = ref<Character[]>([]);
+const currentPage = ref(1);
+const pageSize = ref(9);
+const message = useMessage();
+const router = useRouter()
+
+onMounted(async () => {
+  const [err, result] = await to(getAppList());
+  if (err) {
+    message.error(err.message);
+  } else {
+    allData.value = result.data;
+  }
+});
+
+const tableData = computed(() => {
+  const start = (currentPage.value - 1) * pageSize.value;
+  return allData.value.slice(start, start + pageSize.value);
+});
+
+const totalItems = computed(() => {
+  return allData.value ? allData.value.length : 0;
+});
+
+
+const appStore = useAppStore()
+const chatStore = useChatStore()
+
+function playApp(url: string) {
+  appStore.setIsChat(false)
+  // 跳转到应用
+  router.push(url)
+}
+
+async function handleActionButtonClick(item: Character) {
+  const [err] = await to(copyRoleList(item));
+  if (err) {
+    message.error(err.message);
+  } else {
+    message.success(t('voice.collectionSuccessful'));
+  }
+}
+
+</script>
+
+<template>
+  <div class="flex h-full flex-col role-card">
+    <n-tabs type="line" class="tab-bar">
+      <n-tab-pane name="officialRecommend" tab="官网推荐" />
+      <n-tab-pane name="writingAssist" tab="辅助写作" />
+      <n-tab-pane name="socialEntertainment" tab="社交娱乐" />
+      <n-tab-pane name="myFollow" tab="我的关注" />
+    </n-tabs>
+
+    <main class="flex-1 overflow-hidden " style="margin-left: 20px;">
+      <div class="card-container">
+        <n-card v-for="item in tableData" :key="item.id" class="card-item" bordered hoverable>
+          <div class="flex justify-between">
+            <div>
+              <h3>{{ item.name }}</h3>
+              <p class="ellipsis" :title="item.description">{{ item.description || '——' }}</p>
+            </div>
+            <!-- <n-avatar :size="48" :src="item.avatar" /> -->
+           
+            <n-icon size="48">
+              <img :src="item.avatar" alt="Icon" />
+            </n-icon>
+          </div>
+          <n-divider />
+          <div class="flex justify-between mt-4 button-list">
+            <n-button  secondary round type="info" @click="playApp(item.appUrl)">
+              立即体验
+            </n-button>
+            <n-button  secondary round  type="primary">
+              关注
+              <!-- {{ $t('voice.collection') }} -->
+            </n-button>
+          </div>
+        </n-card>
+      </div>
+      <n-pagination :page="currentPage" :page-size="pageSize" :item-count="totalItems"
+        @update:page="currentPage = $event" class="pagination voice-pagination" />
+    </main>
+  </div>
+</template>
+  
+<style scoped>
+.card-container {
+  display: flex;
+  flex-wrap: wrap;
+  justify-content: flex-start; /* 修改为左边对齐 */
+}
+
+.card-item {
+  width: calc(26%);
+  margin: 12px;
+  border-radius: 10px;
+  height: 28vh;
+}
+
+.pagination {
+  position: absolute;
+  right: 10px;
+  bottom: 10px;
+}
+
+.ellipsis {
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  width: 200px; /* Adjust width as needed */
+}
+
+.tab-bar {
+  margin: 20px 0 20px 20px; /* 上下边距20px,左边边距20px */
+  padding: 10px; /* 添加内边距 */
+  border-radius: 8px; /* 添加圆角 */
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); /* 添加阴影 */
+  transition: all 0.3s ease; /* 添加动效过渡 */
+}
+
+
+</style>
+  

+ 280 - 0
src/views/store/index.vue

@@ -0,0 +1,280 @@
+<script setup lang="ts">
+import { ref, computed, onMounted } from 'vue';
+import {
+  NButton, NCard, NAvatar, NPagination, NDrawer, NDrawerContent,
+  NForm, NFormItem, NInput, NDivider, NSpace, NUpload,
+  NProgress, NModal, NSlider, useMessage,NUploadDragger,UploadFileInfo
+} from 'naive-ui';
+import { useRouter } from 'vue-router';
+import { getToken } from '@/store/modules/auth/helper';
+import { createRole, getRole, simpleGenerateReq, delRole } from '@/api/store';
+
+import to from "await-to-js";
+import { t } from '@/locales';
+
+const router = useRouter();
+const message = useMessage();
+const token = getToken();
+const headers = { Authorization: `Bearer ${token}` };
+const active = ref(false);
+const showModal = ref(false);
+const audioUrl = ref('');
+const isPercentage = ref(false);
+const percentage = ref(0);
+const formValue = ref({ name: '', description: '', avatar: '' ,prompt: ''});
+const simpleGenerate = ref({ model: 'reecho-neural-voice-001', randomness: 97, stability_boost: 100, voiceId: '', text: '' });
+const tableData = ref([]);
+const currentPage = ref(1);
+
+const paginatedData = computed(() => {
+  const start = (currentPage.value - 1) * 9;
+  return tableData.value.slice(start, start + 9);
+});
+
+onMounted(fetchData);
+
+function increaseProgress() {
+  isPercentage.value = true;
+  const interval = setInterval(() => {
+    if (percentage.value < 99) {
+      percentage.value += Math.floor(Math.random() * 5) + 1;
+      if (percentage.value > 99) percentage.value = 99;
+    } else {
+      clearInterval(interval);
+    }
+  }, 1000);
+}
+
+async function fetchData() {
+  const [err, result] = await to(getRole());
+  if (!err) tableData.value = result;
+}
+
+/**
+ * 头像上传
+ */
+async function handleUploadFinish({ event }: { event?: ProgressEvent }) {
+  const response = JSON.parse(event?.target?.response);
+  formValue.value.avatar = response.data.url;
+  message.success(t('voice.uploudSuccessful'));
+}
+
+/**
+ * 文件上传
+ */
+ function handleFinish({event,file}: {
+  file: UploadFileInfo
+  event?: ProgressEvent
+}) {
+  const ext = (event?.target as XMLHttpRequest).response
+  // ext 转成json对象
+  const fileData =  JSON.parse(ext);
+  formValue.value.prompt = fileData.data.url
+  message.success(t('voice.uploudSuccessful'));
+}
+
+
+
+
+async function submitForm() {
+  const [err] = await to(createRole(formValue.value));
+  if (!err) {
+    message.success(t('voice.operationSuccessful'));
+    fetchData();
+  }
+}
+
+async function submitSimpleGenerate() {
+  increaseProgress();
+  const [err, result] = await to(simpleGenerateReq(simpleGenerate.value));
+  if (!err) {
+    audioUrl.value = result?.data.audio;
+    message.success(t('voice.operationSuccessful'));
+    isPercentage.value = false;
+  }else{
+    message.error(err.message);
+  }
+}
+
+function handleActionButtonClick(row: any) {
+  simpleGenerate.value.voiceId = row.voiceId;
+  showModal.value = true;
+}
+
+async function handleDelButtonClick(row: any) {
+  const [err] = await to(delRole(row.id));
+  if (!err) message.success(t('voice.operationSuccessful'));
+  // 刷新页面
+  fetchData();
+}
+
+function handlePageChange(page: number) {
+  currentPage.value = page;
+}
+
+function playAudio(url: string) {
+  new Audio(url).play();
+}
+</script>
+
+<template>
+  <div style="display: flex; justify-content: flex-start; margin:10px;" class="sound-button-box top-header">
+    <n-button :bordered="false" @click="active = true" type="primary" style="margin-right: 10px;">{{ $t('voice.createRole') }}</n-button>
+    <n-button :bordered="false" @click="router.push('/roleList/t')" type="warning">{{ $t('voice.soundMarket') }}</n-button>
+  </div>
+ 
+  <div class="flex h-full flex-col role-card table-box">
+    <main class="flex-1 overflow-hidden">
+      <div class="card-container">
+        <n-card v-for="item in paginatedData" :key="item.key" class="card-item" bordered hoverable>
+          <div class="flex justify-between">
+            <div class="card-description">
+              <h3>{{ item.name }}</h3>
+              <p class="ellipsis" :title="item.description">{{ item.description || '——'}}</p>
+            </div>
+            <n-avatar size="large" :src="item.avatar" class="card-avatar" />
+          </div>
+          <n-divider />
+          <div class="flex mt-4 button-list">
+            <n-button :bordered="false" secondary round type="info" @click="playAudio(item.fileUrl)"> {{ $t('voice.play') }}</n-button>
+            <n-button :bordered="false" secondary round type="primary" @click="handleActionButtonClick(item)"> {{ $t('voice.generate') }}</n-button>
+            <n-button :bordered="false" secondary round type="error" @click="handleDelButtonClick(item)">{{ $t('voice.delete') }}</n-button>
+          </div>
+        </n-card>
+        <n-pagination v-model:page="currentPage" :page-size="9" :item-count="tableData.length"
+          @update:page="handlePageChange" class="pagination voice-pagination" />
+      </div>
+    </main>
+  </div>
+
+  <n-drawer v-model:show="active" class="add-role-draw" :width="540" placement="right">
+    <n-drawer-content :title="$t('voice.addRole')">
+      <n-space vertical>
+        <n-form>
+          <n-form-item :label="$t('voice.roleName')">
+            <n-input v-model:value="formValue.name" :placeholder="$t('voice.roleNameDescribe')" />
+          </n-form-item>
+          <n-form-item :label="$t('voice.roleDescribe')">
+            <n-input v-model:value="formValue.description" :placeholder="$t('voice.roleExplain')" />
+          </n-form-item>
+          <n-form-item :label="$t('voice.avatar')">
+            <n-upload action="/api/resource/oss/upload" :max="1" list-type="image-card" :headers="headers" class="role-avatar-upload"
+              @finish="handleUploadFinish">
+              <div style="margin: 30px 0 10px;">
+              <IconSvg icon="add" width="30px" height="30px"></IconSvg>
+              </div>
+              <p style="font-size: 14px">{{ $t('voice.upload') }}</p>
+              </n-upload>
+          </n-form-item>
+
+          <n-form-item :label="$t('voice.audioSamples')">
+                <!-- @before-upload="beforeUpload" -->
+                <n-upload
+                  directory-dnd
+                  action="/api/resource/oss/upload"
+                  name="file"
+                  :headers="headers"
+                  @finish="handleFinish"
+                  class="add-role-upload"
+                  :max="1">
+                      <n-upload-dragger>
+                        <div style="margin-bottom: 12px">
+                          <n-icon size="48" :depth="3">
+                            <archive-icon />
+                          </n-icon>
+                        </div>
+                        <n-text style="font-size: 16px">
+                          {{ $t('voice.prompt1') }}
+                        </n-text>
+                        <br>
+                        <n-p depth="3" style="margin: 8px 0 0 0">
+                          {{ $t('voice.prompt2') }}
+                        </n-p>
+                      </n-upload-dragger>
+                </n-upload>
+              </n-form-item>
+          <n-button :bordered="false" class="add-role-button" @click="submitForm" type="primary">{{ $t('voice.add') }}</n-button>
+        </n-form>
+      </n-space>
+    </n-drawer-content>
+  </n-drawer>
+
+  <n-modal class="voice-drawer" v-model:show="showModal" :title="$t('voice.generate')" :auto-focus="false" preset="card"
+    style="width: 95%; max-width: 540px;">
+    <n-input maxlength="1000" type="textarea" v-model:value="simpleGenerate.text"
+      :placeholder="$t('voice.proposal')" />
+    <n-space vertical>
+      <br>
+      <section class=" flex justify-between items-center">
+        <div> {{ $t('voice.diversity') }}
+        </div>
+        <div class=" flex justify-end items-center w-[80%] max-w-[240px]">
+          <div class=" w-[200px]"><n-slider v-model:value="simpleGenerate.randomness" :step="1" :max="100" /></div>
+          <div class="w-[40px] text-right">{{ simpleGenerate.randomness }}</div>
+        </div>
+      </section>
+      <div class="mb-4 text-[12px] text-gray-300 dark:text-gray-300/20">
+       {{ $t('voice.generateInfo') }}</div>
+
+      <section class=" flex justify-between items-center">
+        <div> {{ $t('voice.stability') }}
+        </div>
+        <div class=" flex justify-end items-center w-[80%] max-w-[240px]">
+          <div class=" w-[200px]"><n-slider v-model:value="simpleGenerate.stability_boost" :step="1" :max="100" /></div>
+          <div class="w-[40px] text-right">{{ simpleGenerate.stability_boost }}</div>
+        </div>
+      </section>
+      <div class="mb-4 text-[12px] text-gray-300 dark:text-gray-300/20 stabilityInfo">
+        </div>
+        {{ $t('voice.stabilityInfo') }}
+
+      <!-- 进度条 -->
+      <n-progress v-if="isPercentage" :percentage="percentage"></n-progress>
+
+      <audio v-if="audioUrl" :src="audioUrl" controls></audio>
+
+    </n-space>
+
+
+
+    <br>
+    <div style="display: flex; justify-content: flex-end">
+      <n-button :bordered="false" @click="submitSimpleGenerate" type="primary" class="addvoicebutton">
+        {{ $t('voice.start') }}
+      </n-button>
+    </div>
+
+
+
+  </n-modal>
+</template>
+
+
+
+<style scoped>
+.card-container {
+  display: flex;
+  flex-wrap: wrap;
+  justify-content: flex-start;
+}
+
+.card-item {
+  width: calc(30%);
+  margin: 10px;
+  border-radius: 10px;
+  height: 23vh;
+}
+
+.pagination {
+  position: absolute;
+  right: 10px;
+  bottom: 10px;
+}
+
+.ellipsis {
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  width: 200px;
+}
+</style>

+ 61 - 0
src/views/store/layout.vue

@@ -0,0 +1,61 @@
+<script setup lang='ts'>
+import { computed } from 'vue'
+import { NLayout, NLayoutContent } from 'naive-ui'
+import { useRouter } from 'vue-router'
+import Permission from '../chat/layout/Permission.vue'
+import { useBasicLayout } from '@/hooks/useBasicLayout'
+import { homeStore, useAppStore, useAuthStore, useChatStore } from '@/store'
+import { aiSider ,aiFooter} from '@/views/mj'
+import aiMobileMenu from '@/views/mj/aiMobileMenu.vue'; 
+
+const router = useRouter()
+const appStore = useAppStore()
+const chatStore = useChatStore()
+const authStore = useAuthStore()
+
+router.replace({ name: 'Sound', params: { uuid: chatStore.active } })
+homeStore.setMyData({local:'sound'});
+const { isMobile } = useBasicLayout()
+
+const collapsed = computed(() => appStore.siderCollapsed)
+
+const needPermission = computed(() => !!authStore.session?.auth && !authStore.token)
+
+const getMobileClass = computed(() => {
+  if (isMobile.value)
+    return ['rounded-none', 'shadow-none' ]
+  return [ 'shadow-md', 'dark:border-neutral-800' ] //'border', 'rounded-md',
+})
+
+const getContainerClass = computed(() => {
+  return [
+    'h-full',
+    { 'abc': !isMobile.value && !collapsed.value },
+  ]
+}) 
+</script>
+
+<template>
+  <div class="dark:bg-[#24272e] transition-all p-0" :class="[isMobile ? 'h55' : 'h-full' ]">
+    <div class="h-full overflow-hidden" :class="getMobileClass">
+      <NLayout class="z-40 transition" :class="getContainerClass" has-sider  :sider-placement="isMobile?'left': 'right'">
+        <aiSider v-if="!isMobile"/>
+       
+        <NLayoutContent class="h-full">
+          <RouterView v-slot="{ Component, route }">
+            <component :is="Component" :key="route.fullPath" />
+          </RouterView>
+        </NLayoutContent>
+         <!-- <Sider /> -->
+      </NLayout>
+    </div>
+    <Permission :visible="needPermission" />
+  </div>
+   <aiMobileMenu v-if="isMobile"   /> 
+  <aiFooter/>
+</template>
+<style  >
+.h55{
+  height: calc(100% - 55px);
+}
+</style>

Некоторые файлы не были показаны из-за большого количества измененных файлов