Quellcode durchsuchen

增加后台管理,支持docker部署

ageer vor 11 Monaten
Ursprung
Commit
017465621a

+ 8 - 53
Dockerfile

@@ -1,56 +1,11 @@
-# build front-end
-FROM node:lts-alpine AS frontend
+# 使用alpine版本的nginx作为基础镜像
+FROM nginx:alpine
 
-RUN npm install pnpm -g
+# 将dist文件中的内容复制到nginx的html目录下的web目录中
+COPY dist/ /usr/share/nginx/html/web/
 
-WORKDIR /app
+# 用本地的nginx.conf配置来替换nginx镜像里的默认配置
+COPY nginx.conf /etc/nginx/nginx.conf
 
-COPY ./package.json /app
-
-COPY ./pnpm-lock.yaml /app
-
-RUN pnpm install
-
-COPY . /app
-
-RUN pnpm run build
-
-# build backend
-FROM node:lts-alpine as backend
-
-RUN npm install pnpm -g
-
-WORKDIR /app
-
-COPY /service/package.json /app
-
-COPY /service/pnpm-lock.yaml /app
-
-RUN pnpm install
-
-COPY /service /app
-
-RUN pnpm build
-
-# service
-FROM node:lts-alpine
-
-RUN npm install pnpm -g
-
-WORKDIR /app
-
-COPY /service/package.json /app
-
-COPY /service/pnpm-lock.yaml /app
-
-RUN pnpm install --production && rm -rf /root/.npm /root/.pnpm-store /usr/local/share/.cache /tmp/*
-
-COPY /service /app
-
-COPY --from=frontend /app/dist /app/public
-
-COPY --from=backend /app/build /app/build
-
-EXPOSE 3002
-
-CMD ["pnpm", "run", "prod"]
+# 暴露8081端口
+EXPOSE 8081

+ 1 - 1
index.html

@@ -4,7 +4,7 @@
 	<meta charset="UTF-8">
 	<link rel="icon" type="image/svg+xml" href="/favicon.svg">
 	<meta content="yes" name="apple-mobile-web-app-capable"/>
-	<link rel="apple-touch-icon" href="/favicon.ico">
+	<link rel="apple-touch-icon" href="/favicon.svg">
 	<meta name="viewport"
 		content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, viewport-fit=cover" />
 	<title>熊猫助手</title>

+ 35 - 0
nginx.conf

@@ -0,0 +1,35 @@
+worker_processes 1;
+
+events {
+    worker_connections 1024;
+}
+
+http {
+    include       mime.types;
+    default_type  application/octet-stream;
+
+    sendfile        on;
+    keepalive_timeout  65;
+
+    server {
+        listen       8081;
+        server_name  localhost;
+
+        location / {
+            root   /usr/share/nginx/html/web;
+            index  index.html index.htm;
+            try_files $uri $uri/ /index.html;
+        }
+
+        location /api/{
+            proxy_pass http://ruoyi-server:6039/; 
+            # 避免出现反代https域名出现502错误
+            proxy_ssl_server_name on;
+        }
+
+        error_page   500 502 503 504  /50x.html;
+        location = /50x.html {
+            root   /usr/share/nginx/html;
+        }
+    }
+}

+ 2 - 2
package.json

@@ -1,6 +1,6 @@
 {
-  "name": "chatgpt-web-midjourney-proxy",
-  "version": "2.16.2",
+  "name": "ruoyi-web",
+  "version": "1.2.0",
   "private": false,
   "description": "ChatGPT Web Midjourney Proxy",
   "author": "Dooy <ydlhero@gmail.com>",

+ 1 - 1
public/favicon.svg

@@ -1 +1 @@
-<svg id="openai-symbol" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path d="M29.71,13.09A8.09,8.09,0,0,0,20.34,2.68a8.08,8.08,0,0,0-13.7,2.9A8.08,8.08,0,0,0,2.3,18.9,8,8,0,0,0,3,25.45a8.08,8.08,0,0,0,8.69,3.87,8,8,0,0,0,6,2.68,8.09,8.09,0,0,0,7.7-5.61,8,8,0,0,0,5.33-3.86A8.09,8.09,0,0,0,29.71,13.09Zm-12,16.82a6,6,0,0,1-3.84-1.39l.19-.11,6.37-3.68a1,1,0,0,0,.53-.91v-9l2.69,1.56a.08.08,0,0,1,.05.07v7.44A6,6,0,0,1,17.68,29.91ZM4.8,24.41a6,6,0,0,1-.71-4l.19.11,6.37,3.68a1,1,0,0,0,1,0l7.79-4.49V22.8a.09.09,0,0,1,0,.08L13,26.6A6,6,0,0,1,4.8,24.41ZM3.12,10.53A6,6,0,0,1,6.28,7.9v7.57a1,1,0,0,0,.51.9l7.75,4.47L11.85,22.4a.14.14,0,0,1-.09,0L5.32,18.68a6,6,0,0,1-2.2-8.18Zm22.13,5.14-7.78-4.52L20.16,9.6a.08.08,0,0,1,.09,0l6.44,3.72a6,6,0,0,1-.9,10.81V16.56A1.06,1.06,0,0,0,25.25,15.67Zm2.68-4-.19-.12-6.36-3.7a1,1,0,0,0-1.05,0l-7.78,4.49V9.2a.09.09,0,0,1,0-.09L19,5.4a6,6,0,0,1,8.91,6.21ZM11.08,17.15,8.38,15.6a.14.14,0,0,1-.05-.08V8.1a6,6,0,0,1,9.84-4.61L18,3.6,11.61,7.28a1,1,0,0,0-.53.91ZM12.54,14,16,12l3.47,2v4L16,20l-3.47-2Z"/></svg>
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1714855236562" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1647" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M513 19.7c-272.9 0-495 222.1-495 495 0 86 22.4 170.6 64.7 244.9 8.6 15.1-4.4 43.8-18.3 74.2-17.9 39.5-25.1 59.9-17.7 68.5 2.2 2.9 7.3 4.4 15.5 4.4 14.5 0 35.7-4.7 56.3-9.2 21.6-4.8 42.1-9.3 56.9-9.3 8.7 0 14.5 1.5 18.3 4.8 89.1 75.2 202.5 116.7 319.3 116.7 272.9 0 495-222 495-495 0-272.9-222-495-495-495z m0 922.7c-100.9 0-198.9-35.8-275.8-100.8-16.1-13.6-37-20.6-61.8-20.6-10.8 0-22.1 1.3-33.7 3.2 11.9-32.1 17.8-65.8-0.5-97.9-36.6-64.1-55.9-137.3-55.9-211.6C85.3 278.9 277.2 87 513 87c235.9 0 427.8 191.9 427.8 427.7 0 235.8-191.9 427.7-427.8 427.7z" fill="#333333" p-id="1648"></path><path d="M445.2 323.5c18.5 0 33.6 15.1 33.6 33.6v315.2c0 18.5-15.1 33.6-33.6 33.6s-33.6-15.1-33.6-33.6V357.1c-0.1-18.5 15.1-33.6 33.6-33.6zM580.9 373.6c18.5 0 33.6 15.1 33.6 33.6v215c0 18.5-15.1 33.6-33.6 33.6s-33.6-15.1-33.6-33.6v-215c0-18.5 15.2-33.6 33.6-33.6zM716.7 449.3c18.5 0 33.6 15.1 33.6 33.6v63.5c0 18.5-15.1 33.6-33.6 33.6s-33.6-15.1-33.6-33.6v-63.5c0-18.4 15.1-33.6 33.6-33.6zM309.4 449.3c18.5 0 33.6 15.1 33.6 33.6v63.5c0 18.5-15.1 33.6-33.6 33.6s-33.6-15.1-33.6-33.6v-63.5c-0.1-18.4 15.1-33.6 33.6-33.6z" fill="#333333" p-id="1649"></path></svg>

+ 26 - 0
src/api/model.ts

@@ -0,0 +1,26 @@
+import request from '@/utils/request/req';
+
+/**
+ * 查询未隐藏模型
+ */
+export function getmodelList() {
+	return request({
+		url: '/system/model/modelList',
+		method: 'get',
+	})
+}
+
+/**
+ * 查询所有模型
+ */
+export function list() {
+	return request({
+		url: '/system/model/list',
+		method: 'get',
+	})
+}
+
+
+
+
+

+ 129 - 0
src/api/suno.ts

@@ -0,0 +1,129 @@
+import { gptServerStore,homeStore,useAuthStore } from "@/store";
+import { mlog } from "./mjapi";
+import { sunoStore,SunoMedia } from "./sunoStore";  
+
+const getUrl=(url:string)=>{
+    if(url.indexOf('http')==0) return url;
+    if(gptServerStore.myData.SUNO_SERVER){
+        return `${ gptServerStore.myData.SUNO_SERVER}${url}`;
+    }
+    return `/sunoapi${url}`;
+}
+function getHeaderAuthorization(){
+    let headers={}
+    if( homeStore.myData.vtoken ){
+        const  vtokenh={ 'x-vtoken':  homeStore.myData.vtoken ,'x-ctoken':  homeStore.myData.ctoken};
+        headers= {...headers, ...vtokenh}
+    }
+    if(!gptServerStore.myData.SUNO_KEY){
+        const authStore = useAuthStore()
+        if( authStore.token ) {
+            const bmi= { 'x-ptoken':  authStore.token };
+            headers= {...headers, ...bmi }
+            return headers;
+        }
+        return headers
+    }
+    const bmi={
+        'Authorization': 'Bearer ' +gptServerStore.myData.SUNO_KEY
+    }
+    headers= {...headers, ...bmi }
+    return headers
+}
+export function sleep(time: number) {
+  return new Promise((resolve) => setTimeout(resolve, time));
+}
+export const lyricsFetch= async ( lid:string)=>{
+    for(let i=0;i<50;i++){
+        let dt:any = await sunoFetch(`/lyrics/${lid}`);
+        mlog("ddd",dt )
+        let time= (i+1)
+        if(time>20) time=20;
+        if(dt.status=='complete') return dt ;
+        await sleep( time*1000 )
+        
+    }
+    return null;
+   
+}
+
+export function randStyle(): string {
+    const s: string[] = ["acoustic", "aggressive", "anthemic", "atmospheric", "bouncy", "chill", "dark", "dreamy", "electronic", "emotional", "epic", "experimental", "futuristic", "groovy", "heartfelt", "infectious", "melodic", "mellow", "powerful", "psychedelic", "romantic", "smooth", "syncopated", "uplifting", ""];
+    const l: string[] = ["afrobeat", "anime", "ballad", "bedroom pop", "bluegrass", "blues", "classical", "country", "cumbia", "dance", "dancepop", "delta blues", "electropop", "disco", "dream pop", "drum and bass", "edm", "emo", "folk", "funk", "future bass", "gospel", "grunge", "grime", "hip hop", "house", "indie", "j-pop", "jazz", "k-pop", "kids music", "metal", "new jack swing", "new wave", "opera", "pop", "punk", "raga", "rap", "reggae", "reggaeton", "rock", "rumba", "salsa", "samba", "sertanejo", "soul", "synthpop", "swing", "synthwave", "techno", "trap", "uk garage"];
+    
+    const randomS: string = s[Math.floor(Math.random() * s.length)];
+    const randomL: string = l[Math.floor(Math.random() * l.length)];
+    // const randomS2: string = s[Math.floor(Math.random() * s.length)];
+    // const randomL2: string = l[Math.floor(Math.random() * l.length)];
+
+    return randomS + " " + randomL ;
+}
+
+export const FeedTask= async (ids:string[])=>{
+    const sunoS = new sunoStore();
+    if(ids.length<=0) return;
+    
+    let d:any[] = await sunoFetch('/feed/'+ ids.join(','));
+    mlog('FeedTask',d )
+    d.forEach( (item:SunoMedia) =>{
+         sunoS.save( item)
+        if(item.status== "complete"){
+            ids= ids.filter(v=>v!=item.id )
+        }
+    });
+    homeStore.setMyData({act:'FeedTask'});
+    await sleep(5*1000 );
+    FeedTask(ids)
+
+}
+
+
+export const sunoFetch=(url:string,data?:any,opt2?:any )=>{
+    mlog('sunoFetch', url  );
+    let headers= {'Content-Type':'application/json'}
+    if(opt2 && opt2.headers ) headers= opt2.headers;
+
+    headers={...headers,...getHeaderAuthorization()}
+   
+    return new Promise<any>((resolve, reject) => {
+        let opt:RequestInit ={method:'GET'};
+       
+        opt.headers= headers ;
+        if(opt2?.upFile ){
+             opt.method='POST';
+             opt.body=data as FormData ;
+        }
+        else if(data) {
+            opt.body= JSON.stringify(data) ;
+            opt.method='POST';
+        }
+        fetch(getUrl(url),  opt )
+        .then( async (d) =>{
+            if (!d.ok) { 
+                let msg = '发生错误: '+ d.status
+                try{ 
+                  let bjson:any  = await d.json();
+                  msg = '('+ d.status+')发生错误: '+(bjson?.error?.message??'' ) 
+                }catch( e ){ 
+                }
+                homeStore.myData.ms &&  homeStore.myData.ms.error(msg )
+                throw new Error( msg );
+            }
+     
+            d.json().then(d=> resolve(d)).catch(e=>{ 
+            
+                homeStore.myData.ms &&  homeStore.myData.ms.error('发生错误'+ e )
+                reject(e) 
+            }
+        )})
+        .catch(e=>{ 
+            if (e.name === 'TypeError' && e.message === 'Failed to fetch') {
+                homeStore.myData.ms &&  homeStore.myData.ms.error('跨域|CORS error'  )
+            }
+            else homeStore.myData.ms &&  homeStore.myData.ms.error('发生错误:'+e )
+            mlog('e', e.stat )
+            reject(e)
+        })
+    })
+
+}

+ 61 - 0
src/api/sunoStore.ts

@@ -0,0 +1,61 @@
+import { ss } from '@/utils/storage'
+ 
+export type SunoMedia = {
+    id: string;
+    video_url: string;
+    audio_url: string;
+    image_url: string;
+    image_large_url: string;
+    is_video_pending: boolean;
+    major_model_version: string;
+    model_name: string;
+    metadata: {
+        tags?: string;
+        prompt: string;
+        gpt_description_prompt?: string ;
+        audio_prompt_id?: string ;
+        history?: string ;
+        concat_history?: string ;
+        type: string;
+        duration: number;
+        refund_credits: boolean;
+        stream: boolean;
+        error_type?: string ;
+        error_message?: string ;
+    };
+    is_liked: boolean;
+    user_id: string;
+    display_name: string;
+    handle: string;
+    is_handle_updated: boolean;
+    is_trashed: boolean;
+    reaction?: any; // You might want to define a proper type for this
+    created_at: string;
+    status: string;
+    title: string;
+    play_count: number;
+    upvote_count: number;
+    is_public: boolean;
+};
+export class sunoStore{
+  //private id: string;
+  private localKey='suno-store';
+  public save(obj:SunoMedia ){
+    if(!obj.id ) throw "id must";
+    let arr=  this.getObjs();
+    let i= arr.findIndex( v=>v.id==obj.id );
+    if(i>-1) arr[i]= obj;
+    else arr.push(obj);
+     ss.set(this.localKey, arr );
+    return this;
+  } 
+  public findIndex(id:string){ 
+    return this.getObjs().findIndex( v=>v.id== id )
+  }
+
+  public getObjs():SunoMedia[]{
+     const obj = ss.get( this.localKey ) as  undefined| SunoMedia[];
+     if(!obj) return [];
+     return obj;
+  }
+}

+ 102 - 106
src/components/common/PromptStore/index.vue

@@ -1,11 +1,18 @@
 <script setup lang='ts'>
-import { computed, ref, onUnmounted, watch } from 'vue'
-import { NCard, NTag, NSpace, NModal, NTabs, NButton, NTabPane, NImage, NTable} from 'naive-ui'
+import { computed, ref, onUnmounted, watch, h, onMounted } from 'vue'
+import { NCard, NTag, NSpace, NModal, NTabs, NButton, NTabPane, NImage, NDataTable } from 'naive-ui'
 import { SvgIcon } from '@/components/common'
 import { useMessage } from 'naive-ui'
 import { payUrl, getOrderInfo } from '@/api/pay'
+import { list } from '@/api/model'
+
 import to from "await-to-js";
 
+onMounted(() => {
+	// 查询模型未隐藏信息
+	modeList()
+});
+
 interface Props {
 	visible: boolean,
 	title: string
@@ -83,17 +90,100 @@ watch(showMeVisible, (newValue, oldValue) => {
 	}
 });
 
+const pagination = ref({
+	page: 1,
+	pageSize: 10,
+	showSizePicker: true,
+	pageSizes: [10, 20, 30, 40],
+	onChange: (page: number) => {
+		pagination.value.page = page;
+	},
+	onUpdatePageSize: (pageSize: number) => {
+		pagination.value.pageSize = pageSize;
+		pagination.value.page = 1;
+	}
+});
+
+const createColumns = () => {
+	return [
+		...(false
+			? [{
+				title: '主键',
+				key: 'id',
+				width: 80,
+				ellipsis: true,
+			}]
+			: []),
+		{
+			title: '模型名称',
+			key: 'modelDescribe'
+		},
+		{
+			title: '价格',
+			key: 'modelPrice'
+		},
+		{
+			title: '计费方式',
+			key: 'modelType',
+			render: (row: any) => {
+				let text, type;
+				switch (row.modelType) {
+					case "1":
+						text = 'token计费';
+						type = 'success'; // 绿色标签
+						break;
+					case "2":
+						text = '次数计费';
+						type = 'info'; // 蓝色标签
+						break;
+					default:
+						text = '未知计费方式';
+						type = 'default'; // 默认灰色标签
+				}
+				// 直接使用导入的 NTag 组件,设置相应的属性
+				return h(NTag, {
+					type: type,
+					size: 'medium',
+					round: true
+				}, {
+					default: () => text
+				});
+			}
+		},
+		{
+			title: '备注',
+			key: 'remark'
+		},
+
+	]
+}
+const columns = ref(createColumns());
+
+const tableData = ref([]);
+
+const modeList = async () => {
+	try {
+		// 发起一个请求
+		const [err, result] = await to(list());
 
+		if (err) {
+			message.error(err.message)
+		} else {
+			tableData.value = result.rows
+		}
+	} catch (error) {
+		console.error('Error fetching data:', error);
+	}
+};
 </script>
 
+
+
 <template>
 	<NModal v-model:show="show" :auto-focus="false" preset="card" style="max-width: 1100px;">
 		<n-tabs type="line" size="large" :tabs-padding="20" pane-style="padding: 20px;">
 			<n-tab-pane name=" 订阅计划">
-				<div style=" display: flex; overflow: auto;
-					justifyContent: center;
-					alignItems: center;">
-
+				<div style=" display: flex; overflow: auto;">
 					<n-card title="初级套餐" hoverable :bordered="false" :segmented="{
 						content: true,
 						footer: 'soft'
@@ -192,19 +282,7 @@ watch(showMeVisible, (newValue, oldValue) => {
 									<SvgIcon icon="icon-park-twotone:correct" />
 								</template>
 							</n-tag>
-						
-							<!-- <n-tag type="success" :bordered="false">
-								10次退还卡
-								<template #icon>
-									<SvgIcon icon="icon-park-twotone:correct" />
-								</template>
-							</n-tag>
-							<n-tag type="success" :bordered="false">
-								50次免费mj绘图
-								<template #icon>
-									<SvgIcon icon="icon-park-twotone:correct" />
-								</template>
-							</n-tag> -->
+					
 						</n-space>
 
 						<template #footer>
@@ -260,18 +338,6 @@ watch(showMeVisible, (newValue, oldValue) => {
 								</template>
 							</n-tag>
 
-							<!-- <n-tag type="success" :bordered="false">
-								30次退还卡
-								<template #icon>
-									<SvgIcon icon="icon-park-twotone:correct" />
-								</template>
-							</n-tag>
-							<n-tag type="success" :bordered="false">
-								200次免费mj绘图
-								<template #icon>
-									<SvgIcon icon="icon-park-twotone:correct" />
-								</template>
-							</n-tag> -->
 						</n-space>
 					
 						<template #footer>
@@ -285,81 +351,11 @@ watch(showMeVisible, (newValue, oldValue) => {
 				</div>
 			</n-tab-pane>
 			<n-tab-pane name="收费标准">
-				<div style=" display: flex; overflow: auto;
-					justifyContent: center;
-					alignItems: center;">
-						<n-table :bordered="false" :single-line="false">
-							
-							<thead>
-							<tr>
-								<th>模型名称(通常1000个Token约等于750个英文单词或者400~500个汉字)</th>
-								<th>价格</th>
-								<th>说明</th>
-							</tr>
-							</thead>
-							<tbody>
-							<tr>
-								<td>gpt-3.5-turbo-1106</td>
-								<td>0.05元/1K tokens</td>
-								<td>GPT3.5最新模型,用于文本生成、对话系统、内容摘要等</td>
-							</tr>
-				
-							<tr>
-								<td>gpt-4-1106-preview</td>
-								<td>0.2元/1K tokens</td>
-								<td>最新版GPT-4,相对GPT-3.5更先进、拥有更多的参数和更强大的语言处理能力</td>
-							</tr>
-
-							<tr>
-								<td>gpt-4-1106-vision-preview</td>
-								<td>0.2元/次</td>
-								<td> GPT-4 的一个包含视觉处理能力的预览版本,结合了视觉信息处理的能力</td>
-							</tr>	
-
-							<tr>
-								<td>gpt-4-all</td>
-								<td>0.2元/次</td>
-								<td>同时拥有联网查询,高级数据分析,画图 DALL.E功能,GPT 会自动识别并调取相关能力工具</td>
-							</tr>
-
-				
-							<tr>
-								<td>gpt-4-gizmo</td>
-								<td>0.2元/次</td>
-								<td>gpts商店中的模型,使用方式:gpt-4-gizmo-g-xxx</td>
-							</tr>
-
-							<tr>
-								<td>claude-3</td>
-								<td>0.2元/次</td>
-								<td>Claude模型的最新版本,具有最先进的语言处理技术</td>
-							</tr>
-
-							<tr>
-								<td>dall·e 3</td>
-								<td>0.3元/次</td>
-								<td>DALL·E 是一个专注于图像生成的模型</td>
-
-							</tr>
-							<tr>
-								<td>dall·e 3(1790px)</td>
-								<td>0.5元/次</td>
-								<td>DALL·E 是一个专注于图像生成的模型</td>
-							</tr>
-							<tr>
-								<td>midjourney</td>
-								<td>0.3元/次</td>
-								<td>高级图像生成和处理模型,擅长创建逼真的视觉效果</td>
-							</tr>
-							
-							<tr>
-								<td>stable-diffusion</td>
-								<td>0.1元/次</td>
-								<td>高级图像生成和处理模型,擅长创建逼真的视觉效果</td>
-							</tr>
-			
-							</tbody>
-						</n-table>
+				<span style="color: red;">1000个Token大约相当于750个英文单词或400至500个汉字。在按Token计费的模型中,每使用1000个Token将进行一次扣费。</span>
+				<div class="flex h-full">
+					<main class="flex-1 overflow-hidden h-full">
+						<n-data-table :columns="columns" :data="tableData" :pagination="pagination" />
+					</main>
 				</div>
 			</n-tab-pane>
 		</n-tabs>

+ 123 - 2
src/locales/en-US.ts

@@ -122,7 +122,7 @@ export default {
   "pan_up": "Up",
   "pan_down": "Down",
   "up2": "HD 2x",
-  "up4": "HD 4x" , 
+  "up4": "HD 4x" ,
 
   "thinking": "Thinking...",
   "noReUpload": "Cannot re-upload",
@@ -144,7 +144,7 @@ export default {
     "noSuppertModel": "Refresh, this model is not currently supported!",
     "failOcr": "Recognition failed",
     "remain": "Remain:",
-  
+
   "totalUsage": "Total subscription amount",
   "disableGpt4": "GPT4 disabled",
   "setTextInfo": "OpenAI API Key error, click here to retry",
@@ -279,8 +279,129 @@ export default {
     "add2more": "Please add two or more images",
     "no1m": "Image size cannot exceed 1M",
     "imgExt": "Images support only jpg, gif, png, jpeg formats"
+    ,"setSync": "Synchronize Midjourney and Suno"
+  },
+
+	draw: {
+		qualityList: {
+			general: "General",
+			clear: "Clear",
+			hd: "HD",
+			ultraHd: "Ultra HD",
+		},
+		styleList: {
+			cyberpunk: "Cyberpunk",
+			star: "Star",
+			anime: "Anime",
+			japaneseComicsManga: "Japanese Comics/Manga",
+			inkWashPaintingStyle: "Ink Wash Painting Style",
+			original: "Original",
+			landscape: "Landscape",
+			illustration: "Illustration",
+			manga: "Manga",
+			modernOrganic: "Modern Organic",
+			genesis: "Genesis",
+			posterstyle: "Poster Style",
+			surrealism: "Surrealism",
+			sketch: "Sketch",
+			realism: "Realism",
+			watercolorPainting: "Watercolor Painting",
+			cubism: "Cubism",
+			blackAndWhite: "Black and White",
+			fmPhotography: "Film Photography Style",
+			cinematic: "Cinematic",
+			clearFacialFeatures: "Clear Facial Features",
+		},
+		viewList: {
+			wideView: "Wide View",
+			birdView: "Bird's Eye View",
+			topView: "Top View",
+			upview: "Upview",
+			frontView: "Front View",
+			headshot: "Headshot",
+			ultrawideshot: "Ultrawide Shot",
+			mediumShot: "Medium Shot (MS)",
+			longShot: "Long Shot (LS)",
+			depthOfField: "Depth of Field (DOF)",
+		},
+		shotList: {
+			faceShot: "Face Shot (VCU)",
+			bigCloseUp: "Big Close-Up (BCU)",
+			closeUp: "Close-Up (CU)",
+			waistShot: "Waist Shot (WS)",
+			kneeShot: "Knee Shot (KS)",
+			fullLengthShot: "Full Length Shot (FLS)",
+			extraLongShot: "Extra Long Shot (ELS)",
+		},
+		stylesList: {
+			styleLow: "Style Low",
+			styleMed: "Style Medium",
+			styleHigh: "Style High",
+			styleVeryHigh: "Style Very High",
+		},
+		lightList: {
+			coldLight: "Cold Light",
+			warmLight: "Warm Light",
+			hardLighting: "Hard Lighting",
+			dramaticLight: "Dramatic Light",
+			reflectionLight: "Reflection Light",
+			mistyFoggy: "Misty/Foggy",
+			naturalLight: "Natural Light",
+			sunLight: "Sun Light",
+			moody: "Moody",
+		},
+		versionList: {
+			mjV6: "MJ V6",
+			mjV52: "MJ V5.2",
+			mjV51: "MJ V5.1",
+			nijiV6: "Niji V6",
+			nijiV5: "Niji V5",
+			nijiV4: "Niji V4",
+			nijiJourney: "Niji Journey",
+		},
+		botList: {
+			midjourneyBot: "Midjourney Bot",
+			nijiJourney: "Niji Journey",
+		},
+		dimensionsList: {
+			square: "Square (1:1)",
+			portrait: "Portrait (2:3)",
+			landscape: "Landscape (3:2)",
+		},
+	}
+
+  ,suno:{
+    "description": "Description",
+    "custom": "Custom",
+    "style": "Song Style",
+    "stylepls": "Song Name, e.g., Pop Music",
+    "emputy": "No content available",
+    "noly": "No lyrics available",
+    "inputly": "Please enter the song name or lyrics",
+    "doingly": "In progress, please wait.",
+    "doingly2": "Fetching lyrics...",
+    "title": "Song Name",
+    "titlepls": "Song Name, e.g., Vacation",
+    "desc": "Song Description",
+    "descpls": "Song description, e.g., Original pop music about vacation",
+    "noneedly": "No lyrics needed",
+    "rank": "Random selection",
+    "ly": "Lyrics",
+    "lypls": "Lyrics: with a certain format",
+    "generate": "Compose Song",
+    "generately": "Generate Lyrics",
+    "nodata": "Please compose first to have a list of songs",
+
+    "menu": "Music",
+    "menuinfo": "Suno Music Creation",
+    "server": "Suno API Endpoint",
+    "serverabout": "Suno Related",
+    "setOpenKeyPlaceholder": "Related KEY for Suno API; optional"
   }
 
 
 
+
+
+
 }

+ 130 - 14
src/locales/fr-FR.ts

@@ -74,7 +74,7 @@ export default {
         httpsProxy: 'Proxy HTTPS',
         balance: 'Solde de l\'API',
         monthlyUsage: 'Utilisation Mensuelle',
-    },    
+    },
     store: {
         siderButton: 'Prompt Boutique',
         local: 'Local',
@@ -92,7 +92,7 @@ export default {
         importRepeatContent: 'Contenu ignoré de manière répétée : {msg}',
         onlineImportWarning: 'Remarque : Veuillez vérifier la source du fichier JSON !',
         downloadError: 'Veuillez vérifier l\'état du réseau et la validité du fichier JSON',
-    },    
+    },
     "mj": {
         "setOpen": "Lié à OpenAI",
         "setOpenPlaceholder": "Doit inclure http(s)://",
@@ -105,7 +105,7 @@ export default {
         "setUploaderUrl": "Adresse de Téléchargement:",
         "setBtSave": "Enregistrer",
         "setBtBack": "Restaurer les Paramètres par Défaut",
-    
+
         "redraw": "Redessiner",
         "fail1": "S'il vous plaît soyez patient, ça charge.",
         "success1": "Image rafraîchie avec succès !",
@@ -120,8 +120,8 @@ export default {
         "pan_up": "Haut",
         "pan_down": "Bas",
         "up2": "HD 2x",
-        "up4": "HD 4x" , 
-    
+        "up4": "HD 4x" ,
+
         "thinking": "Réflexion...",
         "noReUpload": "Impossible de réimporter",
         "uploading": "Téléchargement...",
@@ -133,20 +133,20 @@ export default {
         "czoom": "Personnalisé",
         "customTitle": "Zoom personnalisé",
         "zoominfo": "Modifier la valeur du zoom, de 1.0 à 2.0, la valeur par défaut est réglée sur 1.8",
-    
+
         "modleSuccess": "Modèle chargé avec succès",
         "setingSuccess": "Paramètres réussis",
-    
+
         "tokenInfo1": "Jetons restants = Longueur du modèle - Réglage du rôle - Contexte (historique des conversations) - Nombre de réponses - Entrée actuelle",
         "tokenInfo2": "Laissez le réglage du rôle vide et le système fournira un réglage par défaut.",
         "noSuppertModel": "Actualiser, ce modèle n'est actuellement pas pris en charge !",
         "failOcr": "Échec de la reconnaissance",
         "remain": "Reste :",
-    
+
         "totalUsage": "Montant total de l'abonnement",
         "disableGpt4": "GPT4 désactivé",
         "setTextInfo": "Erreur de clé API OpenAI, cliquez ici pour réessayer",
-    
+
         "attr1": "Attribut",
         "ulink": "Lien de l'image",
         "copyFail": "Copie échouée",
@@ -164,7 +164,7 @@ export default {
         "mPlay": "Lire",
         "mCanel": "Annuler",
         "mSent": "Envoyer",
-    
+
         "findVersion": "Découvrir la version mise à jour",
         "yesLastVersion": "Déjà sur la dernière version",
         "infoStar": 'Ce projet est open source sur <a class="text-blue-600 dark:text-blue-500" href="https://github.com/Dooy/chatgpt-web-midjourney-proxy\" target="_blank">GitHub</a>, gratuit et basé sur la licence MIT sans aucune forme de paiement ! </p><p>Si vous trouvez ce projet utile, veuillez lui donner une étoile sur GitHub, merci !',
@@ -190,7 +190,7 @@ export default {
         "micRec": "Commencer l'enregistrement, s'il vous plaît parlez ! Il s'arrêtera automatiquement s'il n'y a pas de son pendant 2 secondes.",
         "micRecEnd": "L'enregistrement est terminé"
 
-    },    
+    },
     "mjset": {
         "server": "Serveur",
         "about": "À Propos",
@@ -203,7 +203,7 @@ export default {
         "drawinfo": "Dessin AI avec le Moteur Midjourney",
         "gallery": "Galerie",
         "galleryInfo": "Ma Galerie"
-    },    
+    },
     "mjchat": {
         "loading": "Chargement de l'Image",
         "openurl": "Ouvrir le lien directement",
@@ -277,7 +277,123 @@ export default {
         "no2add": "Ne pas ajouter d'images en double",
         "add2more": "Veuillez ajouter deux images ou plus",
         "no1m": "La taille de l'image ne peut pas dépasser 1 Mo",
-        "imgExt": "Les images ne supportent que les formats jpg, gif, png, jpeg"
+        "imgExt": "Les images ne supportent que les formats jpg, gif, png, jpeg",
+        "setSync": "Synchroniser Midjourney et Suno",
     },
+	draw: {
+		qualityList: {
+			general: "General",
+			clear: "Clear",
+			hd: "HD",
+			ultraHd: "Ultra HD",
+		},
+		styleList: {
+			cyberpunk: "Cyberpunk",
+			star: "Star",
+			anime: "Anime",
+			japaneseComicsManga: "Japanese Comics/Manga",
+			inkWashPaintingStyle: "Ink Wash Painting Style",
+			original: "Original",
+			landscape: "Landscape",
+			illustration: "Illustration",
+			manga: "Manga",
+			modernOrganic: "Modern Organic",
+			genesis: "Genesis",
+			posterstyle: "Poster Style",
+			surrealism: "Surrealism",
+			sketch: "Sketch",
+			realism: "Realism",
+			watercolorPainting: "Watercolor Painting",
+			cubism: "Cubism",
+			blackAndWhite: "Black and White",
+			fmPhotography: "Film Photography Style",
+			cinematic: "Cinematic",
+			clearFacialFeatures: "Clear Facial Features",
+		},
+		viewList: {
+			wideView: "Wide View",
+			birdView: "Bird's Eye View",
+			topView: "Top View",
+			upview: "Upview",
+			frontView: "Front View",
+			headshot: "Headshot",
+			ultrawideshot: "Ultrawide Shot",
+			mediumShot: "Medium Shot (MS)",
+			longShot: "Long Shot (LS)",
+			depthOfField: "Depth of Field (DOF)",
+		},
+		shotList: {
+			faceShot: "Face Shot (VCU)",
+			bigCloseUp: "Big Close-Up (BCU)",
+			closeUp: "Close-Up (CU)",
+			waistShot: "Waist Shot (WS)",
+			kneeShot: "Knee Shot (KS)",
+			fullLengthShot: "Full Length Shot (FLS)",
+			extraLongShot: "Extra Long Shot (ELS)",
+		},
+		stylesList: {
+			styleLow: "Style Low",
+			styleMed: "Style Medium",
+			styleHigh: "Style High",
+			styleVeryHigh: "Style Very High",
+		},
+		lightList: {
+			coldLight: "Cold Light",
+			warmLight: "Warm Light",
+			hardLighting: "Hard Lighting",
+			dramaticLight: "Dramatic Light",
+			reflectionLight: "Reflection Light",
+			mistyFoggy: "Misty/Foggy",
+			naturalLight: "Natural Light",
+			sunLight: "Sun Light",
+			moody: "Moody",
+		},
+		versionList: {
+			mjV6: "MJ V6",
+			mjV52: "MJ V5.2",
+			mjV51: "MJ V5.1",
+			nijiV6: "Niji V6",
+			nijiV5: "Niji V5",
+			nijiV4: "Niji V4",
+			nijiJourney: "Niji Journey",
+		},
+		botList: {
+			midjourneyBot: "Midjourney Bot",
+			nijiJourney: "Niji Journey",
+		},
+		dimensionsList: {
+			square: "Square (1:1)",
+			portrait: "Portrait (2:3)",
+			landscape: "Landscape (3:2)",
+		},
+	}
+  ,suno:{
+    "description": "Mode de description",
+    "custom": "Mode professionnel",
+    "style": "Style de chanson",
+    "stylepls": "Nom de la chanson, par exemple : Musique pop",
+    "emputy": "Aucun contenu disponible",
+    "noly": "Pas de paroles disponibles",
+    "inputly": "Veuillez saisir le nom de la chanson ou les paroles",
+    "doingly": "En cours, veuillez patienter.",
+    "doingly2": "Récupération des paroles...",
+    "title": "Nom de la chanson",
+    "titlepls": "Nom de la chanson, par exemple : Vacances",
+    "desc": "Description de la chanson",
+    "descpls": "Description de la chanson, par exemple : Musique pop originale sur les vacances",
+    "noneedly": "Pas besoin de paroles",
+    "rank": "Sélection aléatoire",
+    "ly": "Paroles",
+    "lypls": "Paroles : avec un certain format",
+    "generate": "Composer une chanson",
+    "generately": "Générer des paroles",
+    "nodata": "Veuillez composer d'abord pour obtenir une liste de chansons",
+
+    "menu": "Musique",
+    "menuinfo": "Création musicale Suno",
+    "server": "Point de terminaison de l'API Suno",
+    "serverabout": "Lié à Suno",
+    "setOpenKeyPlaceholder": "Clé associée pour l'API Suno ; facultatif"
+
+   }
   }
-  

+ 120 - 3
src/locales/ko-KR.ts

@@ -144,7 +144,7 @@ export default {
     "totalUsage": "총 구독 금액",
     "disableGpt4": "GPT4 비활성화됨",
     "setTextInfo": "OpenAI API 키 오류, 여기를 클릭하여 다시 시도",
-    
+
     "attr1": "첨부",
     "ulink": "원본 이미지 링크",
     "copyFail": "복사 실패",
@@ -170,7 +170,7 @@ export default {
 
     "wsrvClose": "닫기 wsrv",
     "wsrvOpen": "열기 wsrv",
-    
+
     "temperature": "랜덤성",
     "temperatureInfo": "(temperature) 값이 증가함에 따라 응답이 더 랜덤해집니다",
     "top_p": "상위 확률 샘플링",
@@ -275,6 +275,123 @@ export default {
     ,"no2add": "이미지를 중복해서 추가하지 마십시오."
     ,"add2more": "두 장 이상의 이미지를 추가하십시오."
     ,"no1m": "이미지 크기는 1M를 초과할 수 없습니다."
+    ,"setSync": "Midjourney와 Suno를 동기화하십시오"
     ,"imgExt": "이미지는 jpg, gif, png, jpeg 형식만 지원됩니다."
-  }
+    
+  },
+	draw: {
+		qualityList: {
+			general: "General",
+			clear: "Clear",
+			hd: "HD",
+			ultraHd: "Ultra HD",
+		},
+		styleList: {
+			cyberpunk: "Cyberpunk",
+			star: "Star",
+			anime: "Anime",
+			japaneseComicsManga: "Japanese Comics/Manga",
+			inkWashPaintingStyle: "Ink Wash Painting Style",
+			original: "Original",
+			landscape: "Landscape",
+			illustration: "Illustration",
+			manga: "Manga",
+			modernOrganic: "Modern Organic",
+			genesis: "Genesis",
+			posterstyle: "Poster Style",
+			surrealism: "Surrealism",
+			sketch: "Sketch",
+			realism: "Realism",
+			watercolorPainting: "Watercolor Painting",
+			cubism: "Cubism",
+			blackAndWhite: "Black and White",
+			fmPhotography: "Film Photography Style",
+			cinematic: "Cinematic",
+			clearFacialFeatures: "Clear Facial Features",
+		},
+		viewList: {
+			wideView: "Wide View",
+			birdView: "Bird's Eye View",
+			topView: "Top View",
+			upview: "Upview",
+			frontView: "Front View",
+			headshot: "Headshot",
+			ultrawideshot: "Ultrawide Shot",
+			mediumShot: "Medium Shot (MS)",
+			longShot: "Long Shot (LS)",
+			depthOfField: "Depth of Field (DOF)",
+		},
+		shotList: {
+			faceShot: "Face Shot (VCU)",
+			bigCloseUp: "Big Close-Up (BCU)",
+			closeUp: "Close-Up (CU)",
+			waistShot: "Waist Shot (WS)",
+			kneeShot: "Knee Shot (KS)",
+			fullLengthShot: "Full Length Shot (FLS)",
+			extraLongShot: "Extra Long Shot (ELS)",
+		},
+		stylesList: {
+			styleLow: "Style Low",
+			styleMed: "Style Medium",
+			styleHigh: "Style High",
+			styleVeryHigh: "Style Very High",
+		},
+		lightList: {
+			coldLight: "Cold Light",
+			warmLight: "Warm Light",
+			hardLighting: "Hard Lighting",
+			dramaticLight: "Dramatic Light",
+			reflectionLight: "Reflection Light",
+			mistyFoggy: "Misty/Foggy",
+			naturalLight: "Natural Light",
+			sunLight: "Sun Light",
+			moody: "Moody",
+		},
+		versionList: {
+			mjV6: "MJ V6",
+			mjV52: "MJ V5.2",
+			mjV51: "MJ V5.1",
+			nijiV6: "Niji V6",
+			nijiV5: "Niji V5",
+			nijiV4: "Niji V4",
+			nijiJourney: "Niji Journey",
+		},
+		botList: {
+			midjourneyBot: "Midjourney Bot",
+			nijiJourney: "Niji Journey",
+		},
+		dimensionsList: {
+			square: "Square (1:1)",
+			portrait: "Portrait (2:3)",
+			landscape: "Landscape (3:2)",
+		},
+	}
+  ,suno:{
+    "description": "설명 모드",
+    "custom": "전문가 모드",
+    "style": "노래 스타일",
+    "stylepls": "노래 이름, 예: 팝 음악",
+    "emputy": "내용 없음",
+    "noly": "가사 없음",
+    "inputly": "노래 이름 또는 가사를 입력하세요",
+    "doingly": "진행 중입니다. 잠시 기다려주세요.",
+    "doingly2": "가사 가져오는 중...",
+    "title": "노래 제목",
+    "titlepls": "노래 이름, 예: 휴가",
+    "desc": "노래 설명",
+    "descpls": "노래 설명, 예: 휴가에 관한 오리지널 팝 음악",
+    "noneedly": "가사 필요 없음",
+    "rank": "랜덤 선택",
+    "ly": "가사",
+    "lypls": "가사: 일정한 형식으로",
+    "generate": "노래 만들기",
+    "generately": "가사 생성",
+    "nodata": "곡 목록을 보려면 먼저 곡을 작성하세요",
+    
+    "menu": "음악",
+    "menuinfo": "Suno 음악 생성",
+    "server": "Suno API 엔드포인트",
+    "serverabout": "Suno 관련",
+    "setOpenKeyPlaceholder": "Suno API에 대한 관련 키; 선택 사항"
+   }
 }

+ 122 - 5
src/locales/ru-RU.ts

@@ -106,7 +106,7 @@ export default {
     "setBtSave": "Сохранить",
     "setBtBack": "Восстановить по умолчанию",
 
-   
+
   "redraw": "Частичная Перерисовка",
   "fail1": "Пожалуйста, будьте терпеливы, идет загрузка.",
   "success1": "Изображение успешно обновлено!",
@@ -186,7 +186,7 @@ export default {
   "typing": "Печать",
   "authErro": "Ошибка авторизации",
   "authBt": "Пожалуйста, введите пароль доступа к авторизации снова",
-  
+
   "micWhisper": "Распознавание шепота",
   "micAsr": "Мгновенное распознавание",
   "micRec": "Начать запись, пожалуйста, говорите! Запись автоматически остановится, если 2 секунды не будет звука.",
@@ -278,7 +278,124 @@ export default {
     "no2add": "Не добавляйте одно и то же изображение повторно",
     "add2more": "Добавьте как минимум два изображения",
     "no1m": "Размер изображения не должен превышать 1 Мб",
-    "imgExt": "Формат изображения должен быть jpg, gif, png, jpeg"
-  
-  }
+    "imgExt": "Формат изображения должен быть jpg, gif, png, jpeg",
+    "setSync": "Синхронизировать Midjourney и Suno"
+
+  },
+	draw: {
+		qualityList: {
+			general: "General",
+			clear: "Clear",
+			hd: "HD",
+			ultraHd: "Ultra HD",
+		},
+		styleList: {
+			cyberpunk: "Cyberpunk",
+			star: "Star",
+			anime: "Anime",
+			japaneseComicsManga: "Japanese Comics/Manga",
+			inkWashPaintingStyle: "Ink Wash Painting Style",
+			original: "Original",
+			landscape: "Landscape",
+			illustration: "Illustration",
+			manga: "Manga",
+			modernOrganic: "Modern Organic",
+			genesis: "Genesis",
+			posterstyle: "Poster Style",
+			surrealism: "Surrealism",
+			sketch: "Sketch",
+			realism: "Realism",
+			watercolorPainting: "Watercolor Painting",
+			cubism: "Cubism",
+			blackAndWhite: "Black and White",
+			fmPhotography: "Film Photography Style",
+			cinematic: "Cinematic",
+			clearFacialFeatures: "Clear Facial Features",
+		},
+		viewList: {
+			wideView: "Wide View",
+			birdView: "Bird's Eye View",
+			topView: "Top View",
+			upview: "Upview",
+			frontView: "Front View",
+			headshot: "Headshot",
+			ultrawideshot: "Ultrawide Shot",
+			mediumShot: "Medium Shot (MS)",
+			longShot: "Long Shot (LS)",
+			depthOfField: "Depth of Field (DOF)",
+		},
+		shotList: {
+			faceShot: "Face Shot (VCU)",
+			bigCloseUp: "Big Close-Up (BCU)",
+			closeUp: "Close-Up (CU)",
+			waistShot: "Waist Shot (WS)",
+			kneeShot: "Knee Shot (KS)",
+			fullLengthShot: "Full Length Shot (FLS)",
+			extraLongShot: "Extra Long Shot (ELS)",
+		},
+		stylesList: {
+			styleLow: "Style Low",
+			styleMed: "Style Medium",
+			styleHigh: "Style High",
+			styleVeryHigh: "Style Very High",
+		},
+		lightList: {
+			coldLight: "Cold Light",
+			warmLight: "Warm Light",
+			hardLighting: "Hard Lighting",
+			dramaticLight: "Dramatic Light",
+			reflectionLight: "Reflection Light",
+			mistyFoggy: "Misty/Foggy",
+			naturalLight: "Natural Light",
+			sunLight: "Sun Light",
+			moody: "Moody",
+		},
+		versionList: {
+			mjV6: "MJ V6",
+			mjV52: "MJ V5.2",
+			mjV51: "MJ V5.1",
+			nijiV6: "Niji V6",
+			nijiV5: "Niji V5",
+			nijiV4: "Niji V4",
+			nijiJourney: "Niji Journey",
+		},
+		botList: {
+			midjourneyBot: "Midjourney Bot",
+			nijiJourney: "Niji Journey",
+		},
+		dimensionsList: {
+			square: "Square (1:1)",
+			portrait: "Portrait (2:3)",
+			landscape: "Landscape (3:2)",
+		},
+	}
+   ,suno:{
+    "description": "Режим описания",
+    "custom": "Профессиональный режим",
+    "style": "Стиль песни",
+    "stylepls": "Название песни, например, Поп-музыка",
+    "emputy": "Нет доступного содержимого",
+    "noly": "Текст песни недоступен",
+    "inputly": "Пожалуйста, введите название песни или текст",
+    "doingly": "В процессе, пожалуйста, подождите.",
+    "doingly2": "Получение текста...",
+    "title": "Название песни",
+    "titlepls": "Название песни, например, Каникулы",
+    "desc": "Описание песни",
+    "descpls": "Описание песни, например, Оригинальная поп-музыка о каникулах",
+    "noneedly": "Текст песни не требуется",
+    "rank": "Случайный выбор",
+    "ly": "Текст песни",
+    "lypls": "Текст песни: с определенным форматом",
+    "generate": "Создать песню",
+    "generately": "Сгенерировать текст",
+    "nodata": "Пожалуйста, сначала создайте песню, чтобы получить список песен",
+
+    "menu": "Музыка",
+    "menuinfo": "Создание музыки Suno",
+    "server": "Конечная точка API Suno",
+    "serverabout": "Связанные с Suno",
+    "setOpenKeyPlaceholder": "Связанный ключ для API Suno; необязательно"
+    
+   }
 }

+ 132 - 18
src/locales/tr-TR.ts

@@ -51,7 +51,7 @@ export default {
         clearHistoryConfirm: 'Bu sohbet geçmişini silmek istediğinizden emin misiniz?',
         preview: 'Önizleme',
         showRawText: 'Ham metin olarak göster',
-    },    
+    },
     setting: {
         setting: 'Ayarlar',
         general: 'Genel',
@@ -74,7 +74,7 @@ export default {
         httpsProxy: 'HTTPS Proxy',
         balance: 'API Bakiyesi',
         monthlyUsage: 'Aylık Kullanım',
-    },       
+    },
     store: {
         siderButton: 'Prompt Mağazası',
         local: 'Yerel',
@@ -92,7 +92,7 @@ export default {
         importRepeatContent: 'İçerik tekrarlı olarak atlandı: {msg}',
         onlineImportWarning: 'Not: Lütfen JSON dosyası kaynağını kontrol edin!',
         downloadError: 'Lütfen ağ durumunu ve JSON dosyasının geçerliliğini kontrol edin',
-    },      
+    },
     "mj": {
         "setOpen": "OpenAI İlişkilendirilmiş",
         "setOpenPlaceholder": "http(s):// içermelidir",
@@ -105,7 +105,7 @@ export default {
         "setUploaderUrl": "Yükleme Adresi:",
         "setBtSave": "Kaydet",
         "setBtBack": "Varsayılanı Geri Yükle",
-    
+
         "redraw": "Yeniden Çiz",
         "fail1": "Lütfen sabırlı olun, yükleniyor.",
         "success1": "Resim başarıyla yenilendi!",
@@ -121,7 +121,7 @@ export default {
         "pan_down": "Aşağı",
         "up2": "HD 2x",
         "up4": "HD 4x" ,
-    
+
         "thinking": "Düşünüyor...",
         "noReUpload": "Yeniden yüklenemiyor",
         "uploading": "Yükleniyor...",
@@ -133,20 +133,20 @@ export default {
         "czoom": "Özel",
         "customTitle": "Özel zoom",
         "zoominfo": "Zoom değerini değiştirin, 1.0 ile 2.0 arasında, varsayılan 1.8 olarak ayarlanmıştır",
-    
+
         "modleSuccess": "Model başarıyla yüklendi",
         "setingSuccess": "Ayarlar başarılı",
-    
+
         "tokenInfo1": "Kalan Jetonlar = Model Uzunluğu - Rol Ayarı - Bağlam (Sohbet Geçmişi) - Yanıt Sayısı - Mevcut Giriş",
         "tokenInfo2": "Rol ayarı boş bırakılırsa, sistem varsayılan bir tane sağlar.",
         "noSuppertModel": "Yenile, bu model şu anda desteklenmiyor!",
         "failOcr": "Tanıma başarısız",
         "remain": "Kalan:",
-    
+
         "totalUsage": "Toplam abonelik miktarı",
         "disableGpt4": "GPT4 devre dışı",
         "setTextInfo": "OpenAI API Anahtarı hatası, buraya tıklayarak yeniden deneyin",
-    
+
         "attr1": "Attr",
         "ulink": "Resim Bağlantısı",
         "copyFail": "Kopyalama Başarısız",
@@ -164,7 +164,7 @@ export default {
         "mPlay": "Oynat",
         "mCanel": "İptal",
         "mSent": "Gönder",
-    
+
         "findVersion": "Güncellenmiş sürümü keşfet",
         "yesLastVersion": "Zaten en son sürümde",
         "infoStar": 'Bu proje <a class="text-blue-600 dark:text-blue-500" href="https://github.com/Dooy/chatgpt-web-midjourney-proxy\" target="_blank">GitHub</a> üzerinde açık kaynaklı, ücretsiz ve MIT lisansına dayanmaktadır, herhangi bir ödeme şekli yoktur! </p><p>Bu projeyi yararlı bulursanız, lütfen GitHub üzerinde yıldız verin, teşekkür ederim!',
@@ -172,7 +172,7 @@ export default {
         "setBtSaveSys": "Sisteme kaydet",
         "wsrvClose": "wsrv'yi kapat",
         "wsrvOpen": "wsrv'yi aç",
-        
+
         "temperature": "Rastlantısallık",
         "temperatureInfo": "(temperature) değeri arttıkça yanıtlar daha rastlantısal hale gelir",
         "top_p": "Üst Olasılık Örnekleme",
@@ -183,13 +183,13 @@ export default {
         "frequency_penaltyInfo": "(frequency_penalty) değeri arttıkça, tekrarlanan kelimelerin azaltılma olasılığı daha yüksektir"
         ,"tts_voice": "TTS Ses Karakteri",
         "typing": "Yazıyor",
-        "authErro": "Yetkilendirme başarısız", 
+        "authErro": "Yetkilendirme başarısız",
         "authBt": "Lütfen yetkilendirme erişim şifresini yeniden girin",
         "micWhisper": "Fısıltı konuşma tanıma",
         "micAsr": "Anında tanıma",
         "micRec": "Kayıt başlat, lütfen konuşun! 2 saniye boyunca ses yoksa otomatik olarak duracaktır.",
         "micRecEnd": "Kayıt sona erdi"
-    },        
+    },
     "mjset": {
         "server": "Sunucu",
         "about": "Hakkında",
@@ -202,7 +202,7 @@ export default {
         "drawinfo": "Midjourney Motoru ile Yapay Zeka Çizimi",
         "gallery": "Galeri",
         "galleryInfo": "Benim Galerim"
-    },        
+    },
     "mjchat": {
         "loading": "Resim Yükleniyor",
         "openurl": "Bağlantıyı Doğrudan Aç",
@@ -276,8 +276,122 @@ export default {
         "no2add": "Çift resim ekleme",
         "add2more": "Lütfen iki veya daha fazla resim ekleyin",
         "no1m": "Resim boyutu 1M'yi aşamaz",
-        "imgExt": "Resimler sadece jpg, gif, png, jpeg formatlarını destekler"
-    }
-    
+        "imgExt": "Resimler sadece jpg, gif, png, jpeg formatlarını destekler",
+        "setSync": "Midjourney ve Suno'yu senkronize et"
+    },
+	draw: {
+		qualityList: {
+			general: "General",
+			clear: "Clear",
+			hd: "HD",
+			ultraHd: "Ultra HD",
+		},
+		styleList: {
+			cyberpunk: "Cyberpunk",
+			star: "Star",
+			anime: "Anime",
+			japaneseComicsManga: "Japanese Comics/Manga",
+			inkWashPaintingStyle: "Ink Wash Painting Style",
+			original: "Original",
+			landscape: "Landscape",
+			illustration: "Illustration",
+			manga: "Manga",
+			modernOrganic: "Modern Organic",
+			genesis: "Genesis",
+			posterstyle: "Poster Style",
+			surrealism: "Surrealism",
+			sketch: "Sketch",
+			realism: "Realism",
+			watercolorPainting: "Watercolor Painting",
+			cubism: "Cubism",
+			blackAndWhite: "Black and White",
+			fmPhotography: "Film Photography Style",
+			cinematic: "Cinematic",
+			clearFacialFeatures: "Clear Facial Features",
+		},
+		viewList: {
+			wideView: "Wide View",
+			birdView: "Bird's Eye View",
+			topView: "Top View",
+			upview: "Upview",
+			frontView: "Front View",
+			headshot: "Headshot",
+			ultrawideshot: "Ultrawide Shot",
+			mediumShot: "Medium Shot (MS)",
+			longShot: "Long Shot (LS)",
+			depthOfField: "Depth of Field (DOF)",
+		},
+		shotList: {
+			faceShot: "Face Shot (VCU)",
+			bigCloseUp: "Big Close-Up (BCU)",
+			closeUp: "Close-Up (CU)",
+			waistShot: "Waist Shot (WS)",
+			kneeShot: "Knee Shot (KS)",
+			fullLengthShot: "Full Length Shot (FLS)",
+			extraLongShot: "Extra Long Shot (ELS)",
+		},
+		stylesList: {
+			styleLow: "Style Low",
+			styleMed: "Style Medium",
+			styleHigh: "Style High",
+			styleVeryHigh: "Style Very High",
+		},
+		lightList: {
+			coldLight: "Cold Light",
+			warmLight: "Warm Light",
+			hardLighting: "Hard Lighting",
+			dramaticLight: "Dramatic Light",
+			reflectionLight: "Reflection Light",
+			mistyFoggy: "Misty/Foggy",
+			naturalLight: "Natural Light",
+			sunLight: "Sun Light",
+			moody: "Moody",
+		},
+		versionList: {
+			mjV6: "MJ V6",
+			mjV52: "MJ V5.2",
+			mjV51: "MJ V5.1",
+			nijiV6: "Niji V6",
+			nijiV5: "Niji V5",
+			nijiV4: "Niji V4",
+			nijiJourney: "Niji Journey",
+		},
+		botList: {
+			midjourneyBot: "Midjourney Bot",
+			nijiJourney: "Niji Journey",
+		},
+		dimensionsList: {
+			square: "Square (1:1)",
+			portrait: "Portrait (2:3)",
+			landscape: "Landscape (3:2)",
+		},
+	}
+  ,suno:{
+    "description": "Açıklama Modu",
+    "custom": "Profesyonel Mod",
+    "style": "Şarkı Tarzı",
+    "stylepls": "Şarkı Adı, örneğin: Pop Müzik",
+    "emputy": "Mevcut içerik yok",
+    "noly": "Söz yok",
+    "inputly": "Lütfen şarkı adını veya sözleri girin",
+    "doingly": "Devam ediyor, lütfen bekleyin.",
+    "doingly2": "Sözler getiriliyor...",
+    "title": "Şarkı Adı",
+    "titlepls": "Şarkı Adı, örneğin: Tatil",
+    "desc": "Şarkı Açıklaması",
+    "descpls": "Şarkı açıklaması, örneğin: Tatil hakkında orijinal pop müziği",
+    "noneedly": "Söz gerekli değil",
+    "rank": "Rastgele seçim",
+    "ly": "Sözler",
+    "lypls": "Sözler: belirli bir formatta",
+    "generate": "Şarkı Oluştur",
+    "generately": "Sözler Oluştur",
+    "nodata": "Lütfen önce şarkı oluşturun ki şarkı listesi olsun",
+    "menu": "Müzik",
+    "menuinfo": "Suno Müzik Oluşturma",
+    "server": "Suno API Uç Noktası",
+    "serverabout": "Suno İlgili",
+    "setOpenKeyPlaceholder": "Suno API için İlgili Anahtar; isteğe bağlı"
+   }
+
   }
-  

+ 119 - 3
src/locales/vi-VN.ts

@@ -170,7 +170,7 @@ export default {
 
     "wsrvClose": "Đóng wsrv",
     "wsrvOpen": "Mở wsrv",
-    
+
     "temperature": "Ngẫu nhiên",
     "temperatureInfo": "Khi giá trị (temperature) tăng, các phản hồi trở nên ngẫu nhiên hơn",
     "top_p": "Lấy Mẫu Xác Suất Cao Nhất",
@@ -276,6 +276,122 @@ export default {
     "no2add": "Vui lòng không thêm hình ảnh giống nhau",
     "add2more": "Vui lòng thêm ít nhất hai hình ảnh",
     "no1m": "Kích thước hình ảnh không quá 1M",
-    "imgExt": "Chỉ hỗ trợ định dạng jpg, gif, png, jpeg cho hình ảnh"
-    }
+    "imgExt": "Chỉ hỗ trợ định dạng jpg, gif, png, jpeg cho hình ảnh",
+    "setSync": "Đồng bộ hóa Midjourney và Suno"
+  },
+	draw: {
+		qualityList: {
+			general: "General",
+			clear: "Clear",
+			hd: "HD",
+			ultraHd: "Ultra HD",
+		},
+		styleList: {
+			cyberpunk: "Cyberpunk",
+			star: "Star",
+			anime: "Anime",
+			japaneseComicsManga: "Japanese Comics/Manga",
+			inkWashPaintingStyle: "Ink Wash Painting Style",
+			original: "Original",
+			landscape: "Landscape",
+			illustration: "Illustration",
+			manga: "Manga",
+			modernOrganic: "Modern Organic",
+			genesis: "Genesis",
+			posterstyle: "Poster Style",
+			surrealism: "Surrealism",
+			sketch: "Sketch",
+			realism: "Realism",
+			watercolorPainting: "Watercolor Painting",
+			cubism: "Cubism",
+			blackAndWhite: "Black and White",
+			fmPhotography: "Film Photography Style",
+			cinematic: "Cinematic",
+			clearFacialFeatures: "Clear Facial Features",
+		},
+		viewList: {
+			wideView: "Wide View",
+			birdView: "Bird's Eye View",
+			topView: "Top View",
+			upview: "Upview",
+			frontView: "Front View",
+			headshot: "Headshot",
+			ultrawideshot: "Ultrawide Shot",
+			mediumShot: "Medium Shot (MS)",
+			longShot: "Long Shot (LS)",
+			depthOfField: "Depth of Field (DOF)",
+		},
+		shotList: {
+			faceShot: "Face Shot (VCU)",
+			bigCloseUp: "Big Close-Up (BCU)",
+			closeUp: "Close-Up (CU)",
+			waistShot: "Waist Shot (WS)",
+			kneeShot: "Knee Shot (KS)",
+			fullLengthShot: "Full Length Shot (FLS)",
+			extraLongShot: "Extra Long Shot (ELS)",
+		},
+		stylesList: {
+			styleLow: "Style Low",
+			styleMed: "Style Medium",
+			styleHigh: "Style High",
+			styleVeryHigh: "Style Very High",
+		},
+		lightList: {
+			coldLight: "Cold Light",
+			warmLight: "Warm Light",
+			hardLighting: "Hard Lighting",
+			dramaticLight: "Dramatic Light",
+			reflectionLight: "Reflection Light",
+			mistyFoggy: "Misty/Foggy",
+			naturalLight: "Natural Light",
+			sunLight: "Sun Light",
+			moody: "Moody",
+		},
+		versionList: {
+			mjV6: "MJ V6",
+			mjV52: "MJ V5.2",
+			mjV51: "MJ V5.1",
+			nijiV6: "Niji V6",
+			nijiV5: "Niji V5",
+			nijiV4: "Niji V4",
+			nijiJourney: "Niji Journey",
+		},
+		botList: {
+			midjourneyBot: "Midjourney Bot",
+			nijiJourney: "Niji Journey",
+		},
+		dimensionsList: {
+			square: "Square (1:1)",
+			portrait: "Portrait (2:3)",
+			landscape: "Landscape (3:2)",
+		},
+	}
+  ,suno:{
+    "description": "Chế độ mô tả",
+    "custom": "Chế độ chuyên nghiệp",
+    "style": "Phong cách bài hát",
+    "stylepls": "Tên bài hát, ví dụ: Nhạc Pop",
+    "emputy": "Không có nội dung",
+    "noly": "Không có lời bài hát",
+    "inputly": "Vui lòng nhập tên bài hát hoặc lời bài hát",
+    "doingly": "Đang tiến hành, vui lòng đợi.",
+    "doingly2": "Đang lấy lời bài hát...",
+    "title": "Tên bài hát",
+    "titlepls": "Tên bài hát, ví dụ: Kỳ nghỉ",
+    "desc": "Mô tả bài hát",
+    "descpls": "Mô tả bài hát, ví dụ: Nhạc pop gốc về kỳ nghỉ",
+    "noneedly": "Không cần lời bài hát",
+    "rank": "Lựa chọn ngẫu nhiên",
+    "ly": "Lời bài hát",
+    "lypls": "Lời bài hát: với một định dạng nhất định",
+    "generate": "Sáng tác bài hát",
+    "generately": "Tạo lời bài hát",
+    "nodata": "Vui lòng sáng tạo trước để có danh sách bài hát",
+
+    "menu": "Âm nhạc",
+    "menuinfo": "Sáng tạo âm nhạc Suno",
+    "server": "Điểm cuối API Suno",
+    "serverabout": "Liên quan đến Suno",
+    "setOpenKeyPlaceholder": "Khóa liên quan cho API Suno; tùy chọn"
+   }
 }

+ 37 - 4
src/locales/zh-CN.ts

@@ -76,7 +76,7 @@ export default {
     monthlyUsage: '本月使用量',
   },
   store: {
-    siderButton: '进入市场选购',
+    siderButton: '购买套餐',
     local: '本地',
     online: '在线',
     title: '标题',
@@ -99,7 +99,7 @@ export default {
     server:'服务端'
     ,about:'关于'
     ,model:'模型'
-    ,sysname:'熊猫助手'
+    ,sysname:'AI绘图'
   }
 
   ,mjtab:{
@@ -166,7 +166,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:'其他参数:  <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 --cw 0 只参考五官, 100 参考五官、头发、服装等  </li>'
     ,tStyle:'风格'
     ,tView:'视角'
     ,tShot:'人物镜头'
@@ -183,6 +183,7 @@ export default {
     ,add2more:'请添加两张以上图片'
     ,no1m:'图片大小不能超过{m}M'
     ,imgExt:'图片仅支持jpg,gif,png,jpeg格式'
+    ,setSync:'同步Midjourney、Suno设置'
   },
   mj:{
     setOpen:'OpenAI 相关',
@@ -261,7 +262,7 @@ export default {
     ,findVersion:'发现更新版本'
     ,yesLastVersion:'已是最新版本'
     ,infoStar:'此项目开源于 <a  class="text-blue-600 dark:text-blue-500" href="https://github.com/Dooy/chatgpt-web-midjourney-proxy" target="_blank"> GitHub </a>,免费且基于 MIT 协议,没有任何形式的付费行为! </p><p>如果你觉得此项目对你有帮助,请在 GitHub 帮我点个 Star,谢谢!'
-    ,setBtSaveChat:'保存会话'
+    ,setBtSaveChat:'保存会话'
     ,setBtSaveSys: '保存至系统'
 
     ,wsrvClose:'关闭 wsrv'
@@ -287,6 +288,8 @@ export default {
     ,micRec:'开始录音,请说话!2秒内无声音将自动关闭'
     ,micRecEnd:'录音已结束'
 
+    
+
   },
 
 	draw: {
@@ -377,4 +380,34 @@ export default {
 		},
 	}
 
+  ,suno:{
+    description:"描述模式"
+    ,custom:"定制模式"
+    ,style:'歌曲风格'
+    ,stylepls:'歌曲名称比如:流行音乐'
+    ,emputy:'暂无内容'
+    ,noly:'无歌词'
+    ,inputly:'请输入歌曲名称或歌词'
+    ,doingly:"正在执行请稍后."
+    ,doingly2: "正在获取歌词..."
+    ,title:'歌曲名称'
+    ,titlepls:'歌曲名称比如:假期'
+    ,desc:'歌曲描述'
+    ,descpls:'歌曲描述 比如:关于假期的原声流行音乐'
+    ,noneedly:'无需歌词'
+    ,rank:'随机获取'
+    ,ly:'歌词'
+    ,lypls:'歌词:有一定的格式'
+    ,generate:'创作歌曲'
+    ,generately:'生成歌词'
+    ,nodata:'请先创作才有歌曲列表'
+
+    ,menu:'音乐'
+    ,menuinfo:'Suno 音乐创作'
+    ,server:'Suno 接口地址'
+    ,serverabout:'Suno 相关'
+    ,setOpenKeyPlaceholder:'Suno API 的相关KEY;可不填'
+  }
+
+
 }

+ 32 - 2
src/locales/zh-TW.ts

@@ -167,7 +167,7 @@ export default {
     "findVersion": "發現更新版本",
     "yesLastVersion": "已是最新版本",
     "infoStar": "此專案在 <a class=\"text-blue-600 dark:text-blue-500\" href=\"https://github.com/Dooy/chatgpt-web-midjourney-proxy\" target=\"_blank\">GitHub</a> 上以 MIT 協議開源,免費且沒有任何付費行為! </p><p>如果你覺得這個專案對你有幫助,請在 GitHub 上給它一顆星,謝謝!",
-    "setBtSaveChat": "保存對話",
+    "setBtSaveChat": "保存對話",
     "setBtSaveSys": "保存至系統",
     "wsrvClose": "關閉 wsrv",
     "wsrvOpen": "開啟 wsrv",
@@ -272,7 +272,8 @@ export default {
     "no2add": "請勿重複添加圖片",
     "add2more": "請添加兩張以上圖片",
     "no1m": "圖片大小不能超過1M",
-    "imgExt": "圖片僅支持jpg,gif,png,jpeg格式"
+    "imgExt": "圖片僅支持jpg,gif,png,jpeg格式",
+    "setSync": "同步Midjourney和Suno",
   },
 	draw: {
 		qualityList: {
@@ -361,4 +362,33 @@ export default {
 			landscape: "風景 (3:2)",
 		},
 	}
+  ,suno:{
+    "description": "描述模式",
+    "custom": "專業模式",
+    "style": "歌曲風格",
+    "stylepls": "歌曲名稱,例如:流行音樂",
+    "emputy": "暫無內容",
+    "noly": "無歌詞",
+    "inputly": "請輸入歌曲名稱或歌詞",
+    "doingly": "正在進行中,請稍候。",
+    "doingly2": "正在獲取歌詞...",
+    "title": "歌曲名稱",
+    "titlepls": "歌曲名稱,例如:假期",
+    "desc": "歌曲描述",
+    "descpls": "歌曲描述,例如:關於假期的原聲流行音樂",
+    "noneedly": "無需歌詞",
+    "rank": "隨機獲取",
+    "ly": "歌詞",
+    "lypls": "歌詞:有一定的格式",
+    "generate": "創作歌曲",
+    "generately": "生成歌詞",
+    "nodata": "請先創作才有歌曲列表",
+
+    "menu": "音樂",
+    "menuinfo": "Suno 音樂創作",
+    "server": "Suno API 端點",
+    "serverabout": "Suno 相關",
+    "setOpenKeyPlaceholder": "Suno API 的相關KEY;可不填"
+
+   }
 }

+ 8 - 49
src/router/index.ts

@@ -4,6 +4,7 @@ import { createRouter, createWebHashHistory } from 'vue-router'
 import { setupPageGuard } from './permission'
 import { ChatLayout } from '@/views/chat/layout'
 import mjlayout from '@/views/mj/layout.vue'
+import sunoLayout from '@/views/suno/layout.vue'
 
 const routes: RouteRecordRaw[] = [
   {
@@ -61,57 +62,15 @@ const routes: RouteRecordRaw[] = [
   },
 
   {
-    path: '/sound',
-    name: 'Sound',
-    component: ChatLayout,
-    redirect: '/sound/t',
-    children: [
-      {
-        path: 't',
-        name: 'sound1',
-        component: () => import('@/views/sound/index.vue'),
-      },
-    ],
-  },
-
-  {
-    path: '/knowledge',
-    name: 'Knowledge',
-    component: ChatLayout,
-    redirect: '/knowledge/t',
-    children: [
-      {
-        path: 't',
-        name: 'knowledge1',
-        component: () => import('@/views/knowledge/index.vue'),
-      },
-    ],
-  },
-
-  {
-    path: '/annex',
-    name: 'Annex',
-    component: ChatLayout,
-    redirect: '/annex/t',
-    children: [
-      {
-        path: 't',
-        name: 'annex1',
-        component: () => import('@/views/knowledge/annex.vue'),
-      },
-    ],
-  },
-
-  {
-    path: '/fragment',
-    name: 'Fragment',
-    component: ChatLayout,
-    redirect: '/fragment/t',
+    path: '/music',
+    name: 'music',
+    component: sunoLayout,
+    redirect: '/music/index',
     children: [
       {
-        path: 't',
-        name: 'fragment1',
-        component: () => import('@/views/knowledge/fragment.vue'),
+        path: '/music/:uuid?',
+        name: 'music',
+        component: () => import('@/views/suno/music.vue'),
       },
     ],
   },

+ 28 - 15
src/store/homeStore.ts

@@ -5,10 +5,15 @@ import { ss } from '@/utils/storage'
 export const homeStore = reactive({
     myData:{
         act:'',//动作
+        act2:'',//动作
         actData:{} //动作类别 
         ,local:'' //当前所处的版本
         ,session:{} as any
         ,isLoader:false
+        ,vtoken:'' //turnstile token
+        ,ctoken:'' //cookie
+        ,isClient: typeof window !== 'undefined' && window.__TAURI__
+        ,ms:{} as any
        
     }
     
@@ -20,6 +25,12 @@ export const homeStore = reactive({
                 this.myData.actData=''
             }, 2000 );
         }
+        if( Object.keys(v).indexOf('act2')>-1){ 
+            setTimeout(()=> {
+                this.myData.act2=''
+                this.myData.actData=''
+            }, 500 );
+        }
     }
  
 })
@@ -30,8 +41,6 @@ export interface gptConfigType{
     userModel?:string //自定义
     talkCount:number //联系对话
     systemMessage:string //自定义系统提示语
-    kid:string //知识库id
-    kName:string //知识库名称
     gpts?:gptsType
     uuid?:number
     temperature?:number // 随机性 : 值越大,回复越随机
@@ -53,19 +62,17 @@ const getGptInt= ():gptConfigType =>{
 const  getDefault=()=>{
 const amodel = homeStore.myData.session.amodel??'gpt-3.5-turbo'
 let v:gptConfigType={
-    model: amodel,
-    max_tokens: 1024,
-    userModel: '',
-    talkCount: 10,
-    systemMessage: '',
-    temperature: 0.5,
-    top_p: 1,
-    presence_penalty: 0,
-    frequency_penalty: 0,
-    tts_voice: "alloy",
-    kid: '',
-    kName: ''
-}
+        model: amodel,
+        max_tokens:1024,
+        userModel:'',
+        talkCount:10,
+        systemMessage:'',
+        temperature:0.5,
+        top_p:1,
+        presence_penalty:0,
+        frequency_penalty:0,
+        tts_voice:"alloy"
+    }
     return v ;
 }
 export const gptConfigStore= reactive({
@@ -91,6 +98,9 @@ export interface gptServerType{
     MJ_API_SECRET:string
     UPLOADER_URL:string
     MJ_CDN_WSRV?:boolean //wsrv.nl
+    SUNO_SERVER:string
+    SUNO_KEY:string
+    IS_SET_SYNC?:boolean
 
 }
 
@@ -101,7 +111,10 @@ let v:gptServerType={
         MJ_SERVER:'',
         UPLOADER_URL:'',
         MJ_API_SECRET:'',
+        SUNO_KEY:'',
+        SUNO_SERVER:'',
         MJ_CDN_WSRV:false
+        ,IS_SET_SYNC:true
     }
     return v ;
 }

+ 39 - 21
src/views/login/index.vue

@@ -129,32 +129,50 @@ const handleRegistBtnClick = async (e: MouseEvent) => {
 			</main>
 		</div>
 	</div>
-	<div 
-			style="position:absolute;top:100%;text-align:center;bottom:0;margin:0 auto;width:100%;color: #999999"
-		>
+
+	<div class="footer">
+		<a target="_blank" style="color: #999999; font-size: 14px; display: inline-block; vertical-align: middle;"
+			href="https://beian.miit.gov.cn/">
 			<img src='http://cdn.beiruijk.com/0be25a8d779aee40433aaca76c5f6ce.jpg'
-					style="display: inline-block; vertical-align: middle;"/>
-			<a
-				target="_blank"
-				style="color: #999999;font-size: 14px; display: inline-block; vertical-align: middle;"
-				href="https://beian.miit.gov.cn/"
-			> &nbsp;鄂ICP备2023007672号-1</a>
-			<span style="color: #999999;font-size: 14px">&nbsp; @2023 熊猫助手</span>
+				style="display: inline-block; vertical-align: middle;" />
+			&nbsp;Copyright © 本网站由:熊猫智能科技有限公司运营 粤ICP备202410086号
+		</a>
 	</div>
+
 </div>
 
 </template>
 
 <style scoped>
-	#app {
-		background-image: url('@/assets/background.jpg');
-		background-size: cover;
-		background-repeat: no-repeat;
-	}
-	.forgot {
-		top: 1px;
-		right: 6px;
-		font-size: 12px;
-		color: var(--font-gray);
-	}
+#app {
+	display: flex;
+	flex-direction: column;
+	min-height: 100vh;
+	/* 确保至少为视口的高度 */
+	background-image: url('@/assets/background.jpg');
+	background-size: cover;
+	background-repeat: no-repeat;
+}
+
+
+.footer {
+	display: flex;
+	flex-direction: column;
+	justify-content: center;
+	/* 垂直居中 */
+	align-items: center;
+	/* 水平居中 */
+	text-align: center;
+	color: #999999;
+	width: 100%;
+	margin-top: auto;
+	/* 自动将页脚推到底部 */
+}
+
+.forgot {
+	top: 1px;
+	right: 6px;
+	font-size: 12px;
+	color: var(--font-gray);
+}
 </style>

+ 87 - 10
src/views/mj/aiDrawInputItem.vue

@@ -7,12 +7,14 @@ import { useBasicLayout } from '@/hooks/useBasicLayout'
 const { isMobile } = useBasicLayout()
 import AiMsg from './aiMsg.vue'
 //import aiFace from './aiFace.vue'
-import { mlog, train, upImg ,getMjAll } from '@/api'
+import { mlog, train, upImg ,getMjAll, mjFetch } from '@/api'
 //import {copyText3} from "@/utils/format";
 import { homeStore ,useChatStore} from "@/store";
 const chatStore = useChatStore()
 import {t} from "@/locales"
+import axios from "@/utils/request/axios";
 //import { upImg } from "./mj";
+import { getToken } from '@/store/modules/auth/helper'
 
 const vf=[{s:'width: 100%; height: 100%;',label:'1:1'}
 ,{s:'width: 100%; height: 75%;',label:'4:3'}
@@ -23,7 +25,7 @@ const vf=[{s:'width: 100%; height: 100%;',label:'1:1'}
 
 const f=ref({bili:-1, quality:'',view:'',light:'',shot:'',style:'', styles:'',version:'--v 6.0',sref:'',cref:'',cw:'',});
 const st =ref({text:'',isDisabled:false,isLoad:false
-    ,fileBase64:[],bot:'',showFace:false
+    ,fileBase64:[],bot:'',showFace:false,upType:''
 });
 const farr= [
 { k:'style',v:t('mjchat.tStyle') }
@@ -38,7 +40,7 @@ const farr= [
 const drawlocalized = computed(() => {
 	let localizedConfig = {};
 	Object.keys(config).forEach((key) => {
-		localizedConfig[key] = config[key].map((option) => {
+		localizedConfig[key] = config[key].map((option: { labelKey: any; }) => {
 			// 假设 labelKey 如 "draw.qualityList.general"
 			let path = option.labelKey; // 直接使用 labelKey 作为路径
 			return {
@@ -54,6 +56,7 @@ const drawlocalized = computed(() => {
 const msgRef = ref()
 const fsRef= ref()
 const fsRef2 = ref()
+const fsRef3 = ref()
 const $emit=defineEmits(['drawSent','close']);
 const props = defineProps({buttonDisabled:Boolean});
 
@@ -251,12 +254,78 @@ const clearAll=()=>{
   f.value.sref='';
 }
 
-//const config=
+const uploader=(type:string)=>{
+    st.value.upType= type;
+    fsRef3.value.click();
+}
+const selectFile3=  (input:any)=>{
+    // ms.loading('上传中...');
+    upImg(input.target.files[0]).then( async(d)=>{
+        mlog('selectFile3>> ',d );
+        let data={
+            action:'img2txt',
+            data:{
+                "base64Array":d
+            }
+        }
+        const blob = base64ToBlob(data.data.base64Array);
+        const file = blobToFile(blob, 'image.png'); // 指定文件名和扩展名
+        // 创建FormData
+        const formData = new FormData();
+        formData.append('file', file);
+        console.log("formData========",formData)
+        try{
+            const result = await axios.post('/resource/oss/upload', formData, {
+            headers: {
+                    'Content-Type': 'multipart/form-data',
+                    'Authorization': 'Bearer ' + getToken(),
+                },
+            });
+            fsRef3.value.value='';
+            if(result.data.code== 200){
+                if( st.value.upType=='cref'){
+                    f.value.cref= result.data.data.url;
+                }else{
+                    f.value.sref= result.data.data.url;
+                }
+                ms.success( t('mj.uploadSuccess'));
+            }else{
+                ms.error( t(result.data.msg));  
+            }
+            mlog('selectFile3>> ',d );
+
+        }catch(e ){
+            msgRef.value.showError(e)
+        }
+
+    })
+    .catch(e=>msgRef.value.showError(e))
+}
+
+// 将base64字符串转换为Blob对象
+function base64ToBlob(base64: string): Blob {
+  const byteString = window.atob(base64.split(',')[1]);
+  const mimeString = base64.split(',')[0].split(':')[1].split(';')[0];
+  const ab = new ArrayBuffer(byteString.length);
+  const ia = new Uint8Array(ab);
+  for (let i = 0; i < byteString.length; i++) {
+    ia[i] = byteString.charCodeAt(i);
+  }
+  return new Blob([ab], { type: mimeString });
+}
+
+// 将Blob对象转换为File对象
+function blobToFile(blob: Blob, fileName: string): File {
+  const file = new File([blob], fileName, { type: blob.type });
+  return file;
+}
+
 </script>
 <template>
 <AiMsg ref="msgRef" />
 <input type="file"  @change="selectFile"  ref="fsRef" style="display: none" accept="image/jpeg, image/jpg, image/png, image/gif"/>
 <input type="file"  @change="selectFile2" ref="fsRef2" style="display: none" accept="image/jpeg, image/jpg, image/png, image/gif"/>
+<input type="file"  @change="selectFile3" ref="fsRef3" style="display: none" accept="image/jpeg, image/jpg, image/png, image/gif"/>
 
 <div class="overflow-y-auto bg-[#fafbfc] px-4 dark:bg-[#18181c] h-full ">
 
@@ -288,21 +357,29 @@ const clearAll=()=>{
         <div>{{ v.v }}</div>
         <n-select v-model:value="f[v.k]" :options="drawlocalized[v.k+'List']" size="small"  class="!w-[60%]" :clearable="true" />
 	</section>
-    <template v-if="!isMobile"> 
+    <!-- <template  >  </template> -->
         <section class="mb-4 flex justify-between items-center"  >
         <div  >cw(0-100)</div>
         <NInputNumber :min="0" :max="100" v-model:value="f.cw" class="!w-[60%]" size="small" clearable placeholder="0-100 角色参考程度" />
         </section >
     
         <section class="mb-4 flex justify-between items-center"  >
-        <div class="w-[60px]">sref</div>
-        <NInput v-model:value="f.sref" size="small" placeholder="图片url 生成风格一致的图像" clearable />
+        <div class="w-[45px]">sref</div>
+            <NInput v-model:value="f.sref" size="small" placeholder="图片url 生成风格一致的图像" clearable >
+                 <template #suffix>
+                    <SvgIcon icon="ri:upload-line"  class="cursor-pointer" @click="uploader('sref')"></SvgIcon>
+                </template>
+            </NInput>
         </section>
         <section class="mb-4 flex justify-between items-center"  >
-        <div class="w-[60px]">cref</div>
-        <NInput  v-model:value="f.cref" size="small" placeholder="图片url 生成角色一致的图像" clearable/>
+        <div class="w-[45px]">cref</div>
+            <NInput  v-model:value="f.cref" size="small" placeholder="图片url 生成角色一致的图像" clearable>
+                <template #suffix>
+                    <SvgIcon icon="ri:upload-line" class="cursor-pointer"  @click="uploader('cref')"></SvgIcon>
+                </template>
+            </NInput>
         </section>
-    </template>
+   
     
     <div class="mb-1">
      <n-input    type="textarea"  v-model:value="st.text"   :placeholder="$t('mjchat.prompt')" round clearable maxlength="2000" show-count

+ 31 - 21
src/views/mj/aiModel.vue

@@ -4,19 +4,36 @@ import { ref ,computed,watch, onMounted} from "vue";
 import {gptConfigStore, homeStore,useChatStore} from '@/store'
 import { mlog,chatSetting } from "@/api";
 import { t } from "@/locales";
+import to from "await-to-js";
+import { getmodelList } from '@/api/model'
 
 const emit = defineEmits(['close']);
 const chatStore = useChatStore();
 const uuid = chatStore.active;
-//mlog('uuid', uuid );
+
 const chatSet = new chatSetting( uuid==null?1002:uuid);
 
 const nGptStore = ref(  chatSet.getGptConfig() );
+const message = useMessage()
+onMounted(() => { fetchData() });
+
+
+const config = ref([])
+const fetchData = async () => {
+    try {
+       // 发起一个请求
+      const [err, result] = await to(getmodelList());
+    
+      if (err) {
+        message.error(err.message)
+      } else {
+         config.value = result.data;
+      }
+    } catch (error) {
+      console.error('Error fetching data:', error);
+    }
+};
 
-const config = ref({
-model:[ 'gpt-4-0125-preview','gpt-3.5-turbo','gpt-4-all','claude-3-sonnet-20240229','stable-diffusion','suno-v3']
-,maxToken:2048
-});
 const st= ref({openMore:false });
 const voiceList= computed(()=>{
     let rz=[];
@@ -25,14 +42,11 @@ const voiceList= computed(()=>{
 });
 const modellist = computed(() => { //
     let rz =[ ];
-    for(let o of config.value.model){
-        rz.push({label:o,value:o})
+    for(let o of config.value){
+        rz.push({label:o.modelDescribe,value:o.modelName})
     }
     if(gptConfigStore.myData.userModel){
         let arr = gptConfigStore.myData.userModel.split(/[ ,]+/ig);
-        //  let uniqueArray  = arr.filter((value, index, self) => {
-        //     return self.indexOf(value) === index;
-        // });
         for(let o of arr ){
              rz.push({label:o,value:o})
         }
@@ -69,6 +83,10 @@ const saveChat=(type:string)=>{
      emit('close');
 }
 
+const onSelectChange = (newValue: any) => {
+    const option = modellist.value.find(optionValue => optionValue.value === newValue);
+    nGptStore.value.modelLabel = option.label;
+};
 
 watch(()=>nGptStore.value.model,(n)=>{
     nGptStore.value.gpts=undefined;
@@ -89,29 +107,21 @@ const reSet=()=>{
     nGptStore.value= gptConfigStore.myData;
 }
 
-onMounted(() => {
-    //gptConfigStore.myData= chatSet.getGptConfig();
-});
-
-
-//
-//const f= ref({model:gptConfigStore.myData.model});
 </script>
 <template>
 <section class="mb-4 flex justify-between items-center"  >
      <div ><span class="text-red-500">*</span>  {{ $t('mjset.model') }}</div>
-    <n-select v-model:value="nGptStore.model" :options="modellist" size="small"  class="!w-[50%]"   />
+    <n-select v-model:value="nGptStore.model" :options="modellist" @update:value="onSelectChange" size="small"  class="!w-[50%]"   />
 </section>
 
 <section class="mb-4 flex justify-between items-center"  >
     <n-input   :placeholder="$t('mjchat.modlePlaceholder')" v-model:value="nGptStore.userModel">
       <template #prefix>
-        {{ $t('mjchat.myModle') }}
+        {{ $t('mjchat.myModle') }} 
       </template>
     </n-input>
  </section>
 
-
  <section class=" flex justify-between items-center"  >
      <div> {{ $t('mjchat.historyCnt') }}
      </div>
@@ -196,4 +206,4 @@ onMounted(() => {
     <NButton type="primary" @click="save">{{ $t('mj.setBtSaveSys') }}</NButton> -->
     <NButton type="primary" @click="saveChat('no')">{{ $t('common.save') }}</NButton>
  </section>
-</template>
+</template>

+ 39 - 68
src/views/mj/aiSider.vue

@@ -1,37 +1,26 @@
 <script setup lang="ts">
 import { computed,defineAsyncComponent ,ref} from "vue";
-import { SvgIcon ,HoverButton} from '@/components/common'
+import { SvgIcon} from '@/components/common'
 import { useBasicLayout } from '@/hooks/useBasicLayout'
 const { isMobile } = useBasicLayout()
-import { NAvatar,NTooltip } from 'naive-ui'
-import { homeStore, useUserStore,useChatStore } from '@/store'
+import { NTooltip } from 'naive-ui'
+import { homeStore,useChatStore } from '@/store'
 const chatStore = useChatStore()
 //import gallery from '@/views/gallery/index.vue'
 
 const Setting = defineAsyncComponent(() => import('@/components/common/Setting/index.vue'))
-const userStore = useUserStore()
+
 
 const st= ref({'show':false,showImg:false, menu:[],active:'chat'})
 
 
-const userInfo = computed(() => userStore.userInfo)
 import { router } from '@/router'
-import { mlog } from "@/api";
 
 const goHome =computed(  () => {
-  //router.push('/')
+
   return router.currentRoute.value.name
 });
-// const go=(n:string)=>{
-//   if('chat'==n){
-//         router.push('/chat/'+ chatStore.active??'1002')
-//     }
-//     if('draw'==n){
-//         router.push('/draw/'+ chatStore.active??'1002')
-//         st.value.show=true;
-//     }
-// }
-//mlog('g', goHome() );
+
 const chatId= computed(()=>chatStore.active??'1002' );
 </script>
 <template>
@@ -40,7 +29,7 @@ const chatId= computed(()=>chatStore.active??'1002' );
         <div class="flex flex-col space-y-4 flex-1">
             <a :href="`#/chat/${chatId}`"    @click="st.active='chat'" class="router-link-active router-link-exact-active h-12 w-12 cursor-pointer rounded-xl bg-white duration-300 dark:bg-[#34373c] hover:bg-[#bbb] dark:hover:bg-[#555]">
                 <n-tooltip placement="right" trigger="hover">
-                  <template #trigger> 
+                  <template #trigger>
                     <div  class="flex h-full justify-center items-center py-1 flex-col " :class="[ goHome =='Chat' ? 'active' : '']">
                     <SvgIcon icon="ri:wechat-line" class="text-3xl  flex-1"></SvgIcon>
                      <span class="text-[10px]">{{$t('mjtab.chat')}}</span>
@@ -48,16 +37,16 @@ const chatId= computed(()=>chatStore.active??'1002' );
                  </template>
                 AI Chat
                 </n-tooltip>
-            </a> 
+            </a>
             <a   @click="homeStore.setMyData({act:'showgpts'}) " class=" router-link-exact-active h-12 w-12 cursor-pointer rounded-xl bg-white duration-300 dark:bg-[#34373c] hover:bg-[#bbb] dark:hover:bg-[#555]">
                 <n-tooltip placement="right" trigger="hover">
-                  <template #trigger> 
+                  <template #trigger>
                     <div  class="flex h-full justify-center items-center   py-1 flex-col" >
                     <SvgIcon icon="ri:apps-fill" class="text-3xl flex-1"></SvgIcon>
-                     <span class="text-[10px]">GPTs</span>
-                    </div> 
+                     <span class="text-[10px]">应用</span>
+                    </div>
                   </template>
-                    ChatGPT Store 
+                    应用商店
                 </n-tooltip>
             </a>
 
@@ -65,65 +54,47 @@ const chatId= computed(()=>chatStore.active??'1002' );
 
             <a :href="`#/draw/${chatId}`" @click="st.active='draw'" class=" router-link-exact-active h-12 w-12 cursor-pointer rounded-xl bg-white duration-300 dark:bg-[#34373c] hover:bg-[#bbb] dark:hover:bg-[#555]">
                 <n-tooltip placement="right" trigger="hover">
-                  <template #trigger> 
+                  <template #trigger>
                     <div  class="flex h-full justify-center items-center   py-1 flex-col" :class="[goHome=='draw' ? 'active' : '']">
                     <SvgIcon icon="ic:outline-palette" class="text-3xl flex-1"></SvgIcon>
                      <span class="text-[10px]">{{$t('mjtab.draw')}}</span>
-                    </div> 
+                    </div>
                   </template>
                     {{$t('mjtab.drawinfo')}}
                 </n-tooltip>
             </a>
 
-             <a   @click="homeStore.setMyData({act:'gallery'}) " class=" router-link-exact-active h-12 w-12 cursor-pointer rounded-xl bg-white duration-300 dark:bg-[#34373c] hover:bg-[#bbb] dark:hover:bg-[#555]">
-                <n-tooltip placement="right" trigger="hover">
-                  <template #trigger> 
-                    <div  class="flex h-full justify-center items-center   py-1 flex-col" >
-                    <SvgIcon icon="material-symbols:imagesmode-outline" class="text-3xl flex-1"></SvgIcon>
-                     <span class="text-[10px]">{{$t('mjtab.gallery')}}</span>
-                    </div> 
-                  </template>
-                    {{ $t('mjtab.galleryInfo') }}
-                </n-tooltip>
-            </a>
-
-            <!-- <section  class=" router-link-exact-active h-12 w-12 cursor-pointer rounded-xl bg-white duration-300 dark:bg-[#34373c] hover:bg-[#bbb] dark:hover:bg-[#555]"
-             >
-                <n-tooltip placement="right" trigger="hover">
-                  <template #trigger> 
-                    <div  class="flex  h-full justify-center items-center py-1 flex-col ">
-                      <SvgIcon icon="mingcute:grid-2-line" class="text-3xl flex-1"></SvgIcon>
-                      <span class="text-[10px]">画廊</span>
-                    </div>  
-                  </template>
-                    画廊:看看别人是如何画的
-                </n-tooltip>                
-            </section> -->
+					<a   @click="homeStore.setMyData({act:'gallery'}) " class=" router-link-exact-active h-12 w-12 cursor-pointer rounded-xl bg-white duration-300 dark:bg-[#34373c] hover:bg-[#bbb] dark:hover:bg-[#555]">
+						<n-tooltip placement="right" trigger="hover">
+							<template #trigger>
+								<div  class="flex h-full justify-center items-center   py-1 flex-col" >
+									<SvgIcon icon="material-symbols:imagesmode-outline" class="text-3xl flex-1"></SvgIcon>
+									<span class="text-[10px]">{{$t('mjtab.gallery')}}</span>
+								</div>
+							</template>
+							{{ $t('mjtab.galleryInfo') }}
+						</n-tooltip>
+					</a>
 
-             
+<!--            <a :href="`#/music`" @click="st.active='music'" class=" router-link-exact-active h-12 w-12 cursor-pointer rounded-xl bg-white duration-300 dark:bg-[#34373c] hover:bg-[#bbb] dark:hover:bg-[#555]"-->
+<!--             >-->
+<!--                <n-tooltip placement="right" trigger="hover">-->
+<!--                  <template #trigger> -->
+<!--                    <div  class="flex  h-full justify-center items-center py-1 flex-col " :class="[ goHome =='music' ? 'active' : '']">-->
+<!--                      <SvgIcon icon="arcticons:wynk-music" class="text-3xl flex-1"></SvgIcon>-->
+<!--                      <span class="text-[10px]">音乐</span>-->
+<!--                    </div>  -->
+<!--                  </template>-->
+<!--                    音乐创作-->
+<!--                </n-tooltip>                -->
+<!--            </a>-->
 
         </div>
-        <!-- <div class="flex flex-col  space-y-2 "> 
-
-            
-            <NAvatar  size="large"  round  :src="userInfo.avatar"   v-if="userInfo.avatar"
-             class=" cursor-pointer"  />
-            
-            <HoverButton>
-                <div class="text-xl text-[#4f555e] dark:text-white flex h-full justify-center items-center "  @click="st.show = true">
-                    <SvgIcon icon="ri:settings-4-line" />
-                </div>
-            </HoverButton>
-        </div> -->
+
     </div>
 </div>
  <Setting v-if="st.show" v-model:visible="st.show" />
 
- <!-- <n-drawer v-model:show="st.showImg" :placement="isMobile?'bottom':'right'"  :class="isMobile?['!h-[90vh]']: ['!w-[80vw]']" style="--n-body-padding:0">
-    <n-drawer-content title="GPT store" closable>
-      sdsd 
-    </n-drawer-content>
-</n-drawer> -->
+
 </template>
 
- 

+ 63 - 0
src/views/suno/layout.vue

@@ -0,0 +1,63 @@
+<script setup lang='ts'>
+import { computed } from 'vue'
+import { NLayout, NLayoutContent } from 'naive-ui'
+import { useRouter } from 'vue-router'
+import player from './player.vue';
+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: 'music', params: { uuid: chatStore.active } })
+homeStore.setMyData({local:'music'});
+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/>
+  <player/>
+</template>
+<style  >
+.h55{
+  height: calc(100% - 55px);
+}
+</style>

+ 186 - 0
src/views/suno/mcInput.vue

@@ -0,0 +1,186 @@
+<script setup lang="ts">
+import { ref,computed ,onMounted} from 'vue';
+import { NTabs ,NTabPane ,NInput,NSwitch ,NTooltip, NTag ,NButton, useMessage} from "naive-ui";
+import { SvgIcon } from '@/components/common';
+import { mlog } from '@/api';
+import { sunoFetch ,lyricsFetch, randStyle, FeedTask} from '@/api/suno';
+import { t } from '@/locales';
+import { homeStore } from '@/store';
+
+const st = ref({type:'custom',isLoading:false})
+const des= ref( {
+  "gpt_description_prompt": "",
+  "make_instrumental": false,
+  "mv": "chirp-v3-0",
+  "prompt": ""
+});
+const cs= ref({
+  "prompt": "",
+  "mv": "chirp-v3-0",
+  "title": "",
+  "tags": "",
+  "continue_at": 120,
+  "continue_clip_id": ""
+
+});
+
+const canPost = computed(() => {
+   // return true; 
+    if( st.value.isLoading ) return false;
+    if( st.value.type=='custom'){
+        return cs.value.tags && cs.value.title
+    }
+    if( st.value.type=='description' ){
+        mlog('des: ', des.value.gpt_description_prompt , des.value.make_instrumental )
+        return cs.value.title &&( des.value.gpt_description_prompt || des.value.make_instrumental)
+    }
+    return true
+})
+
+const ms = useMessage();
+onMounted(() => {
+    homeStore.setMyData({ms:ms})
+});
+//生成歌词
+const generateLyrics= ()=>{
+    //generate/lyrics
+    let prompt = cs.value.prompt || cs.value.title;
+    if (!prompt){
+        ms.error(   t('suno.inputly') )
+        return 
+    }
+    if(st.value.isLoading) {
+         ms.info( t('suno.doingly'));
+        return;
+    }
+    st.value.isLoading =true;
+    ms.info( t('suno.doingly2') );
+    sunoFetch(  '/generate/lyrics/' ,  {prompt}).then(async (r:any )=>{
+        mlog('lyrics', r);
+        let dz:any = await lyricsFetch( r.id );
+         mlog('lyrics rz =>', dz );
+        if(dz!=null){  
+
+            cs.value.prompt= dz.text;
+            cs.value.title= dz.title;
+        }
+        st.value.isLoading =false;
+
+    }).catch(()=>  st.value.isLoading =false );
+    if( !cs.value.tags ) cs.value.tags= randStyle()
+}
+
+const generate= async ()=>{
+    st.value.isLoading =false;
+    let ids:string[]= ["0d435185-d440-42c8-982a-50205e1cf17d","43e095ba-5f08-4920-bb3d-89dd0defe0b7"];
+    ids=["d359a0aa-adf1-4298-9074-005573d7cc84","12e3d62f-8fcc-497b-8365-194657582519"]
+
+    if(st.value.type=='custom'){ 
+        if(des.value.make_instrumental) cs.value.prompt='';
+        let r:any= await sunoFetch(  '/generate' ,  cs.value ) 
+        st.value.isLoading =false;
+
+       ids=r.clips.map((r:any)=>r.id);
+        mlog('ids ', ids );
+    }else{
+        des.value.prompt=cs.value.title;
+        let r:any= await sunoFetch(  '/generate/description-mode' ,  des.value )  
+        st.value.isLoading =false; 
+        ids=r.clips.map((r:any)=>r.id);
+    }
+    FeedTask(ids)
+}
+
+</script>
+<template>
+<div class="p-2"> 
+    <n-tabs type="segment" animated  v-model:value="st.type">
+        <!-- <n-tab-pane name="start" tab=""> 
+
+        </n-tab-pane> -->
+        <!-- <NText depth="3" class="text-center">{{ $t('suno.mic') }}</NText> -->
+        <n-tab-pane name="description" :tab="$t('suno.description')">
+            <div class="pt-1">
+                <n-input :placeholder="$t('suno.titlepls')" v-model:value="cs.title">
+                <template #prefix>
+                     <span>{{$t('suno.title')}}:</span>
+                </template>
+                </n-input>
+            </div>
+            <div  class="pt-4 flex justify-between">
+                <div>{{$t('suno.desc')}}:</div>
+                <div> 
+                    <n-switch v-model:value="des.make_instrumental" size="small">
+                        <template #checked>
+                         {{ $t('suno.noneedly') }}
+                        </template>
+                        <template #unchecked>
+                         {{ $t('suno.noneedly') }}
+                        </template>
+                    </n-switch>
+                </div>
+            </div>
+            <div  class="pt-1"> 
+            <n-input v-model:value="des.gpt_description_prompt" :disabled="des.make_instrumental"
+                :placeholder="$t('suno.descpls')"  type="textarea"  size="small"   
+                :autosize="{ minRows: 3, maxRows: 12  }"  />
+            </div>
+        </n-tab-pane>
+
+        <n-tab-pane name="custom" :tab="$t('suno.custom')">
+            <div class="pt-1">
+                <n-input :placeholder="$t('suno.titlepls')" v-model:value="cs.title">
+                <template #prefix>
+                     <span>{{$t('suno.title')}}:</span>
+                </template>
+                </n-input>
+            </div>
+             <div class="pt-4">
+                <n-input :placeholder="$t('suno.stylepls')" v-model:value="cs.tags">
+                    <template #prefix>
+                        <span>{{$t('suno.style')}}:</span>
+                    </template>
+                    <template #suffix>
+                        <n-tooltip placement="right" trigger="hover">
+                            <template #trigger>
+                                <div class="cursor-pointer" @click="cs.tags= randStyle()"><SvgIcon  icon="fa:random" class="w-4 h-4" /></div>
+                            </template>
+                            <div>{{$t('suno.rank')}}</div>
+                            
+                        </n-tooltip>
+                    </template>
+                </n-input>
+            </div>
+
+            <div  class="pt-4 flex justify-between">
+                <div>{{$t('suno.ly')}} :</div>
+                <div> 
+                    <n-switch v-model:value="des.make_instrumental" size="small">
+                        <template #checked>
+                        {{ $t('suno.noneedly') }}
+                        </template>
+                        <template #unchecked>
+                         {{ $t('suno.noneedly') }}
+                        </template>
+                    </n-switch>
+                </div>
+            </div>
+            <div  class="pt-1"> 
+            <n-input v-model:value="cs.prompt" :disabled="des.make_instrumental"
+                :placeholder="$t('suno.lypls')" type="textarea"  size="small"   
+                :autosize="{ minRows: 3, maxRows: 12  }"  />
+            </div>
+        </n-tab-pane>
+    </n-tabs>
+
+    <div class="pt-4">
+        <div class="flex justify-between items-start">
+            <div>
+                  <NTag v-if="st.type=='custom'" type="success" size="small" round  ><span class="cursor-pointer" @click="generateLyrics()" >{{ $t('suno.generately') }}</span></NTag>
+            </div>
+            <NButton type="primary" :disabled="!canPost" @click="generate()"><SvgIcon icon="ri:music-fill"  /> {{$t('suno.generate')}}</NButton> 
+        </div>
+    </div>
+</div>
+
+</template>

+ 110 - 0
src/views/suno/mcList.vue

@@ -0,0 +1,110 @@
+<script setup lang="ts">
+import { ref, watch } from 'vue'
+import { SvgIcon } from '@/components/common';
+//import {   FeedTask} from '@/api/suno';
+import {   sunoStore, SunoMedia} from '@/api/sunoStore';  
+
+import playui from './playui.vue';
+import { homeStore } from '@/store';
+import { mlog } from '@/api';
+import {NEmpty, NImage } from "naive-ui"
+import { FeedTask } from '@/api/suno';
+
+const list= ref<SunoMedia[]>([]);
+const csuno= new sunoStore()
+const st= ref({playid:''});
+const initLoad=()=>{
+    let arr = csuno.getObjs();
+    list.value= arr.reverse()
+}
+
+const getNowCls=(v:any)=>{
+    if(v.id==st.value.playid ){
+        return ['bg-gray-200','dark:bg-black']
+    }
+    return [];
+}
+const goPlay=(v:SunoMedia)=>{
+    st.value.playid=v.id
+    homeStore.setMyData({act:'goPlay',actData:v})
+    if(v.status!='complete'){
+        FeedTask([v.id ])
+    }
+}
+
+const sp= ref({v:10, max:0 ,status:'',idDrop:false });
+ 
+watch(()=>homeStore.myData.act, (n)=>{
+     if(n=='FeedTask'){
+         initLoad()
+     }
+     if(n=='playEned'){
+        //
+        let  i= list.value.findIndex((v)=>v.id==st.value.playid)
+        i++;
+        mlog('playEned,',i, list.value.length )
+        if(i<list.value.length) setTimeout(()=>goPlay(list.value[i]),1000)  
+     }
+});
+
+const update = (v:any )=>{
+     sp.value=v
+      
+}
+initLoad();
+</script>
+<template>
+<div v-if="list.length>0">
+    <div  v-for="item in list" :class="getNowCls( item )" class="flex relative  justify-between items-start p-2 hover:dark:bg-black hover:bg-gray-200 border-b-[1px] border-gray-500/10 ">
+        
+        <playui @update="update" v-if="st.playid==item.id"  class="absolute top-[-4px] left-0 w-full  z-10" ></playui>
+        <div class="w-[60px] h-[60px] relative  cursor-pointer"  @click="goPlay( item )">
+            
+           <template v-if="item.status=='complete'">
+                <n-image  lazy  width="100"  :src="item.image_url" preview-disabled  >
+                    <template #placeholder>
+                        <div class="w-full h-full justify-center items-center flex"  >
+                        <SvgIcon icon="line-md:downloading-loop" class="text-[40px] text-green-300"   ></SvgIcon>
+                        </div>
+                    </template>
+                </n-image>
+                <div class="absolute top-0 right-0 w-full h-full flex justify-center items-center" v-if="st.playid==item.id">
+                    <SvgIcon icon="mdi:pause-circle-outline" class="text-[40px] text-[#fff]" v-if="sp.status=='pause'"></SvgIcon>
+                    <SvgIcon icon="mdi:play-circle-outline" class="text-[40px] text-[#fff]" v-else></SvgIcon>
+                </div>
+            </template>
+            <template v-else>
+                <n-image  lazy  width="100"  :src="item.image_url" preview-disabled  />
+                <div class="absolute top-0 right-0  w-full h-full justify-center items-center flex"  >
+                    <SvgIcon icon="line-md:downloading-loop" class="text-[40px] text-green-300"   ></SvgIcon>
+                </div>
+            </template>
+             
+
+            
+        </div> 
+        <div class="flex-1  pl-2"> 
+            <div class="flex justify-between line-clamp-1 w-full cursor-pointer"  @click="goPlay( item )">
+                <h3>{{item.title}}</h3>
+                <div class="opacity-80"  >{{item.metadata.tags}}</div>
+            </div>
+            <div class="opacity-60 line-clamp-1 w-full text-[12px] cursor-pointer"  @click="goPlay( item )" v-if="item.metadata && item.metadata.prompt">
+             {{item.metadata.prompt}}
+            </div>
+            <div class="opacity-60 line-clamp-1 w-full text-[12px] cursor-pointer"  @click="goPlay( item )" v-else>
+             {{$t('suno.noly')}}
+              </div>
+            <div class="text-right text-[14px] flex justify-end items-center space-x-2  ">
+                <div class="text-[10px] flex items-center border-[1px] border-gray-500/30 px-0.5 list-none rounded-md" v-if="item.metadata && item.metadata.duration"> {{item.metadata.duration.toFixed(1)}}s</div>
+                <SvgIcon icon="mdi:play-circle-outline" class="cursor-pointer"  @click="goPlay( item )" />
+                <a :href="item.audio_url" download  target="_blank"><SvgIcon icon="mdi:download" class="cursor-pointer"/></a>
+            </div>
+           
+        </div>
+    </div>
+</div>
+<div class="w-full h-full flex justify-center items-center" v-else>
+    <NEmpty :description="$t('suno.nodata')"></NEmpty>
+</div>
+
+</template>

+ 40 - 0
src/views/suno/mcplayer.vue

@@ -0,0 +1,40 @@
+<script setup lang="ts">
+import {watch,ref  } from 'vue'
+import {SunoMedia} from '@/api/sunoStore';
+import { homeStore } from '@/store';
+import { NImage,NEmpty } from 'naive-ui';
+import {SvgIcon} from '@/components/common'
+
+const pObj= ref<SunoMedia>()
+watch(()=>homeStore.myData.act, (n)=>{
+    if(n=='goPlay'){
+        let data = homeStore.myData.actData 
+        pObj.value = data as SunoMedia
+        
+
+    }
+})
+</script>
+<template>
+<div v-if="pObj">
+    <div class="w-full  relative h-[300px]">
+        <NImage :src="pObj.image_large_url" class="w-full h-full">
+             <template #placeholder>
+                      <div class="w-full h-full justify-center items-center flex"  >
+                       <SvgIcon icon="line-md:downloading-loop" class="text-[60px] text-green-300"   ></SvgIcon>
+                      </div>
+                </template>
+        </NImage>
+        <div class="absolute bottom-0 right-0 p-2 text-white text-right"> 
+            <h2 class=" text-xl">{{ pObj.title }}</h2>
+            <div class="">{{$t('suno.style')}}:{{ pObj.metadata.tags }}</div>
+        </div>
+    </div>
+   
+    <pre class=" text-wrap p-2">{{ pObj.metadata.prompt }}</pre>
+</div>
+<div class=" flex w-full h-full justify-center items-center" v-else >
+    <n-empty :description="$t('suno.emputy')" ></n-empty>
+</div>
+
+</template>

+ 21 - 0
src/views/suno/music.vue

@@ -0,0 +1,21 @@
+<script setup lang="ts">
+import McInput from './mcInput.vue';
+import mcList from './mcList.vue';
+import mcplayer from './mcplayer.vue';
+
+</script>
+
+<template>
+
+<div class="flex w-full h-full   ">
+    <div class="w-[300px] h-full  overflow-y-auto ">
+        <McInput />
+    </div>
+    <div class=" flex-1  h-full bg-[#fafbfc] pt-2 dark:bg-[#18181c] overflow-y-auto " >
+        <mcList/>
+    </div>
+    <div class="w-[300px]  h-full overflow-y-auto ">
+        <mcplayer/>
+    </div>
+</div>
+</template>

+ 71 - 0
src/views/suno/player.vue

@@ -0,0 +1,71 @@
+<script setup lang="ts">
+import { mlog } from '@/api';
+import { SunoMedia } from '@/api/sunoStore';
+import { homeStore } from '@/store';
+import { watch,ref  } from 'vue';
+
+const st= ref({isLoad:0, url:''});
+const pObj= ref<SunoMedia>()
+const player = new window.Audio(); 
+const loadPay=()=>{
+    if(  !pObj.value ) return 
+    mlog('pObj', pObj.value.audio_url )
+    player.src = pObj.value.audio_url;
+    player.addEventListener('ended', () => {
+        st.value.isLoad=0;
+        homeStore.setMyData({act:'playEned',actData:{a:'ended'}})
+        mlog('ended')
+    });
+    player.addEventListener('play', () => {
+        mlog('play')
+        st.value.isLoad=1;
+        homeStore.setMyData({act:'playStatus',actData:{a:'play',d:{ currentTime: player.currentTime, duration: player.duration }}})
+    }) 
+    player.addEventListener('pause', function() {
+         st.value.isLoad=2;
+          mlog('pause')
+         homeStore.setMyData({act:'playStatus',actData:{a:'pause'}})
+    });
+    player.addEventListener('timeupdate', function(e) {
+        // 音频播放位置变化时的操作
+        //mlog('timeupdate'  ,player.currentTime ,player.duration );
+        let a= 'timeupdate';
+        if(player.currentTime==player.duration){
+            st.value.isLoad=2;
+            a='pause'
+        }
+        homeStore.setMyData({act2:'playStatus' ,actData:{a ,d:{ currentTime: player.currentTime, duration: player.duration }}})
+    });
+    player.load();
+    player.play();
+    
+}
+
+const goPlay=()=>{
+    if(player.src!=pObj.value?.audio_url){
+        if(st.value.isLoad==1 ) player.pause();
+        loadPay()
+    }else{
+        mlog('goPlay',  st.value.isLoad );
+         if(st.value.isLoad==1 ) player.pause();
+         else player.play();
+    }
+}
+watch(()=>homeStore.myData.act, (n)=>{
+    if(n=='goPlay'){
+        let data = homeStore.myData.actData
+        mlog('goPlay' , data );
+        pObj.value = data as SunoMedia
+        goPlay();
+
+    }
+    if(n=='playUpdate'){
+         let data:any  = homeStore.myData.actData
+         mlog('playUpdate' , data );
+         if( data ) player.currentTime = data.v as number
+         //player.set
+    }
+})
+</script>
+<template>
+</template>

+ 55 - 0
src/views/suno/playui.vue

@@ -0,0 +1,55 @@
+<script setup lang="ts">
+import { mlog } from '@/api';
+import { homeStore } from '@/store';
+import { ref ,watch} from 'vue'
+import { NSlider } from 'naive-ui';
+const sp= ref({v:10, max:0 ,status:'',idDrop:false });
+const updatev=(v:number)=>{
+    homeStore.setMyData({act:'playUpdate',actData:{ v}})
+}
+
+const $emit= defineEmits(['update'] );
+ 
+
+watch(()=>homeStore.myData.act2, (n)=>{
+    if(n=='playStatus'){
+        if( sp.value.idDrop ) return
+       let data:any = homeStore.myData.actData
+        mlog('playStatus' , data );
+        if(data && data.d && data.d.duration  ){
+            sp.value.max = data.d.duration
+            sp.value.v = data.d.currentTime ;// parseInt( data.d.currentTime ) 
+           
+        }
+        if( data )   sp.value.status = data.a 
+        $emit('update', sp.value)
+       
+
+    }
+})
+watch(()=>homeStore.myData.act, (n)=>{
+     if(n=='playStatus'){
+         let data:any = homeStore.myData.actData
+         if(data)   sp.value.status = data.a 
+          $emit('update', sp.value)
+     }
+});
+
+</script>
+<template>
+<div class="sss"    style="--n-rail-height:2px">
+<n-slider :on-dragend="()=>sp.idDrop=false" :on-dragstart="()=>sp.idDrop=true" 
+            class="w-full" v-model:value="sp.v" :step="1" v-if="sp.max" :max="sp.max" 
+            :on-update:value="updatev"
+            :format-tooltip="(v)=>v.toFixed(1)+'s'" />
+</div>
+</template>
+<style>
+.sss .n-slider .n-slider-rail{
+    height: 2px!important;
+}
+.sss .n-slider-handle{
+height: 12px!important;
+width: 12px!important;
+ }
+</style>