diff --git a/.vscode/settings.json b/.vscode/settings.json index 829a7d0c..61677b95 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -72,5 +72,7 @@ "[json]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, - "cSpell.words": ["Chatdoc"] + "[vue]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + } } diff --git a/README.md b/README.md index cb413a40..f6ca2896 100644 --- a/README.md +++ b/README.md @@ -65,3 +65,9 @@ Artistic QR Code: ![](https://cdn.acedata.cloud/q3ivan.png) + +Suno Music: + +![](https://cdn.acedata.cloud/mewx2.png) + + diff --git a/change/@acedatacloud-nexior-a671d8ab-f592-4823-b028-e17f4bee63d4.json b/change/@acedatacloud-nexior-a671d8ab-f592-4823-b028-e17f4bee63d4.json new file mode 100644 index 00000000..c65dd1b4 --- /dev/null +++ b/change/@acedatacloud-nexior-a671d8ab-f592-4823-b028-e17f4bee63d4.json @@ -0,0 +1,7 @@ +{ + "type": "major", + "comment": "init suno", + "packageName": "@acedatacloud/nexior", + "email": "office@acedata.cloud", + "dependentChangeType": "patch" +} diff --git a/package.json b/package.json index d321ad18..ddb1c7b8 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "@fortawesome/free-regular-svg-icons": "^6.2.1", "@fortawesome/free-solid-svg-icons": "^6.2.1", "@fortawesome/vue-fontawesome": "^3.0.2", + "@icon-park/vue-next": "^1.3.6", "axios": "^1.6.0", "codemirror": "^6.0.1", "copy-to-clipboard": "^3.3.3", diff --git a/src/assets/base.scss b/src/assets/base.scss new file mode 100644 index 00000000..b5c61c95 --- /dev/null +++ b/src/assets/base.scss @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/src/assets/img/disk.png b/src/assets/img/disk.png new file mode 100644 index 00000000..49be6360 Binary files /dev/null and b/src/assets/img/disk.png differ diff --git a/src/assets/img/index.ts b/src/assets/img/index.ts new file mode 100644 index 00000000..23117df2 --- /dev/null +++ b/src/assets/img/index.ts @@ -0,0 +1,3 @@ +import OpticalDisk from './disk.png'; + +export { OpticalDisk }; diff --git a/src/components/common/HelpEntry.vue b/src/components/common/HelpEntry.vue index 8b758f98..375280ff 100644 --- a/src/components/common/HelpEntry.vue +++ b/src/components/common/HelpEntry.vue @@ -13,7 +13,10 @@ - + - + @@ -57,7 +57,7 @@ import { defineComponent } from 'vue'; import Pagination from '@/components/common/Pagination.vue'; import CopyToClipboard from '@/components/common/CopyToClipboard.vue'; -import { ElRow, ElCol, ElTable, ElTableColumn, ElCard, ElSwitch } from 'element-plus'; +import { ElRow, ElCol, ElTable, ElTableColumn, ElCard } from 'element-plus'; import { userOperator } from '@/operators'; import { IUser } from '@/models'; @@ -74,7 +74,6 @@ export default defineComponent({ Pagination, CopyToClipboard, ElRow, - ElSwitch, ElCol, ElTable, ElTableColumn, diff --git a/src/pages/index/Index.vue b/src/pages/index/Index.vue index 8bc8a9d6..41bca09c 100644 --- a/src/pages/index/Index.vue +++ b/src/pages/index/Index.vue @@ -142,7 +142,6 @@
- - + +
@@ -279,6 +278,15 @@ export default defineComponent({ icon: 'fa-solid fa-qrcode' } ] + : []), + ...(this.site?.features?.suno?.enabled + ? [ + { + title: this.$t('index.title.suno'), + subtitle: this.$t('index.subtitle.suno'), + icon: 'fa-solid fa-music' + } + ] : []) ]; } @@ -364,9 +372,9 @@ export default defineComponent({ font-weight: bold; margin-bottom: 50px; text-align: left; - -webkit-text-fill-color: transparent; - background-clip: text; - background-image: linear-gradient(90deg, #277186, #7752ff 40%, #5f98fa 60%, #44beff); + // -webkit-text-fill-color: transparent; + // background-clip: text; + // background-image: linear-gradient(90deg, #277186, #7752ff 40%, #5f98fa 60%, #44beff); } h3.subtitle { font-size: 30px; diff --git a/src/pages/site/Index.vue b/src/pages/site/Index.vue index 00df849f..615f93ef 100644 --- a/src/pages/site/Index.vue +++ b/src/pages/site/Index.vue @@ -174,7 +174,7 @@ diff --git a/src/pages/suno/Index.vue b/src/pages/suno/Index.vue new file mode 100644 index 00000000..bb67e98e --- /dev/null +++ b/src/pages/suno/Index.vue @@ -0,0 +1,197 @@ + + + + + diff --git a/src/plugins/font-awesome.ts b/src/plugins/font-awesome.ts index 4fc27976..12f6d413 100644 --- a/src/plugins/font-awesome.ts +++ b/src/plugins/font-awesome.ts @@ -10,6 +10,8 @@ import { } from '@fortawesome/free-regular-svg-icons'; import { faDiscord as faBrandsDiscord, faWeixin as faBrandsWeixin } from '@fortawesome/free-brands-svg-icons'; import { + // 添加suno图标svg + faMusic as faSolidMusic, faLanguage as faSolidLanguage, faUser as faSolidUser, faSeedling as faSolidSeedling, @@ -70,7 +72,7 @@ import { faWandMagic as faSolidWandMagic, faAngleDown as faSolidAngleDown } from '@fortawesome/free-solid-svg-icons'; - +library.add(faSolidMusic); library.add(faRegularCopy); library.add(faRegularMessage); library.add(faSolidAngleDown); diff --git a/src/router/constants.ts b/src/router/constants.ts index 527c03b3..0acbc6d2 100644 --- a/src/router/constants.ts +++ b/src/router/constants.ts @@ -19,6 +19,9 @@ export const ROUTE_CHATDOC_CONVERSATION_NEW = 'chatdoc-conversation-new'; export const ROUTE_CHATDOC_SETTING = 'chatdoc-setting'; export const ROUTE_CHATDOC_MANAGE = 'chatdoc-knowledge'; +export const ROUTE_SUNO_INDEX = 'suno-index'; +export const ROUTE_SUNO_HISTORY = 'suno-history'; + export const ROUTE_PROFILE_INDEX = 'profile-index'; export const ROUTE_CONSOLE_ROOT = 'console-root'; diff --git a/src/router/index.ts b/src/router/index.ts index 8a1ec17d..dd3a79b3 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -6,6 +6,7 @@ import midjourney from './midjourney'; import distribution from './distribution'; import chatdoc from './chatdoc'; import qrart from './qrart'; +import suno from './suno'; import site from './site'; import profile from './profile'; @@ -30,6 +31,7 @@ const routes = [ auth, chat, qrart, + suno, midjourney, distribution, site, diff --git a/src/router/suno.ts b/src/router/suno.ts new file mode 100644 index 00000000..01341565 --- /dev/null +++ b/src/router/suno.ts @@ -0,0 +1,16 @@ +import { ROUTE_SUNO_INDEX } from './constants'; + +export default { + path: '/suno', + meta: { + auth: true + }, + component: () => import('@/layouts/Main.vue'), + children: [ + { + path: '', + name: ROUTE_SUNO_INDEX, + component: () => import('@/pages/suno/Index.vue') + } + ] +}; diff --git a/src/store/common/models.ts b/src/store/common/models.ts index 9d8ac1ea..2f4f362d 100644 --- a/src/store/common/models.ts +++ b/src/store/common/models.ts @@ -3,6 +3,7 @@ import { IMidjourneyState } from '../midjourney/models'; import { IChatState } from '../chat/models'; import { IChatdocState } from '../chatdoc/models'; import { IQrartState } from '../qrart/models'; +import { ISunoState } from '../suno/models'; export interface ISetting {} @@ -24,4 +25,5 @@ export interface IRootState extends ICommonState { chat: IChatState; chatdoc: IChatdocState; qrart: IQrartState; + suno: ISunoState; } diff --git a/src/store/index.ts b/src/store/index.ts index 6d2244fe..3a6e7aa6 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -4,11 +4,13 @@ import midjourney from './midjourney'; import chat from './chat'; import chatdoc from './chatdoc'; import qrart from './qrart'; +import suno from './suno'; import root from './common'; import persistChat from './chat/persist'; import persistMidjourney from './midjourney/persist'; import persistChatdoc from './chatdoc/persist'; import persistQrart from './qrart/persist'; +import persistSuno from './suno/persist'; import persistRoot from './common/persist'; const store = createStore({ @@ -17,11 +19,12 @@ const store = createStore({ midjourney: midjourney, chat: chat, qrart: qrart, + suno: suno, chatdoc: chatdoc }, plugins: [ createPersistedState({ - paths: [...persistRoot, ...persistChat, ...persistMidjourney, ...persistChatdoc, ...persistQrart] + paths: [...persistRoot, ...persistChat, ...persistMidjourney, ...persistChatdoc, ...persistQrart, ...persistSuno] }) ] }); diff --git a/src/store/suno/actions.ts b/src/store/suno/actions.ts new file mode 100644 index 00000000..9d023755 --- /dev/null +++ b/src/store/suno/actions.ts @@ -0,0 +1,145 @@ +import { applicationOperator, sunoOperator, serviceOperator } from '@/operators'; +import { ISunoState } from './models'; +import { ActionContext } from 'vuex'; +import { log } from '@/utils/log'; +import { IRootState } from '../common/models'; +import { IApplication, ICredential, ISunoConfig, ISunoTask, IService, Song } from '@/models'; +import { Status } from '@/models/common'; +import { SUNO_SERVICE_ID } from '@/constants'; + +export const resetAll = ({ commit }: ActionContext): void => { + commit('resetAll'); +}; + +export const setCredential = async ({ commit }: any, payload: ICredential): Promise => { + log(setCredential, 'set credential', payload); + commit('setCredential', payload); +}; + +export const setConfig = ({ commit }: any, payload: ISunoConfig) => { + commit('setConfig', payload); +}; + +export const setService = async ({ commit }: any, payload: IService): Promise => { + log(setService, 'set service', payload); + commit('setService', payload); +}; + +export const setApplication = ({ commit }: any, payload: IApplication[]) => { + commit('setApplication', payload); +}; + +export const getApplication = async ({ + commit, + state, + rootState +}: ActionContext): Promise => { + log(getApplication, 'start to get application for suno'); + return new Promise(async (resolve, reject) => { + state.status.getApplication = Status.Request; + applicationOperator + .getAll({ + user_id: rootState?.user?.id, + service_id: SUNO_SERVICE_ID + }) + .then((response) => { + state.status.getApplication = Status.Success; + commit('setApplication', response.data.items[0]); + const credential = response.data.items?.[0]?.credentials?.find( + (credential) => credential?.host === window.location.origin + ); + commit('setCredential', credential); + resolve(response.data.items[0]); + }) + .catch((error) => { + state.status.getApplication = Status.Error; + reject(error); + }); + }); +}; + +export const setTasks = ({ commit }: any, payload: any) => { + commit('setTasks', payload); +}; + +export const setTasksItems = ({ commit }: any, payload: ISunoTask[]) => { + commit('setTasksItems', payload); +}; + +export const setTasksTotal = ({ commit }: any, payload: number) => { + commit('setTasksTotal', payload); +}; + +export const setTasksActive = ({ commit }: any, payload: ISunoTask) => { + commit('setTasksActive', payload); +}; + +export const setAudio = ({ commit }: any, payload: any) => { + commit('setAudio', payload); +}; + +export const getService = async ({ commit, state }: ActionContext): Promise => { + return new Promise(async (resolve, reject) => { + log(getService, 'start to get service for suno'); + state.status.getService = Status.Request; + serviceOperator + .get(SUNO_SERVICE_ID) + .then((response) => { + state.status.getService = Status.Success; + commit('setService', response.data); + resolve(response.data); + }) + .catch((error) => { + state.status.getService = Status.Error; + reject(error); + }); + }); +}; + +export const getTasks = async ( + { commit, state }: ActionContext, + { offset, limit }: { offset?: number; limit?: number } +): Promise => { + return new Promise(async (resolve, reject) => { + log(getTasks, 'start to get tasks', offset, limit); + const credential = state.credential; + const token = credential?.token; + if (!token) { + return reject('no token'); + } + sunoOperator + .tasks( + { + applicationId: state.application?.id + }, + { + token + } + ) + .then((response) => { + log(getTasks, 'get imagine tasks success', response.data.items); + commit('setTasksItems', response.data.items); + commit('setTasksTotal', response.data.count); + resolve(response.data.items); + }) + .catch((error) => { + return reject(error); + }); + }); +}; + +export default { + setService, + getService, + resetAll, + setCredential, + setConfig, + setApplication, + getApplication, + setTasks, + setTasksItems, + setTasksTotal, + setTasksActive, + getTasks, + setAudio +}; diff --git a/src/store/suno/index.ts b/src/store/suno/index.ts new file mode 100644 index 00000000..a3ac7f42 --- /dev/null +++ b/src/store/suno/index.ts @@ -0,0 +1,14 @@ +import { Module } from 'vuex'; +import { ISunoState } from './models'; +import actions from './actions'; +import mutations from './mutations'; +import state from './state'; + +export const suno: Module = { + namespaced: true, + state, + mutations, + actions +}; + +export default suno; diff --git a/src/store/suno/models.ts b/src/store/suno/models.ts new file mode 100644 index 00000000..17990540 --- /dev/null +++ b/src/store/suno/models.ts @@ -0,0 +1,40 @@ +import { IApplication, ICredential, IService, ISunoAudio, Status } from '@/models'; +import { ISunoConfig, ISunoTask, Song } from '@/models'; + +export interface ISunoState { + application: IApplication | undefined; + service: IService | undefined; + credential: ICredential | undefined; + config: ISunoConfig | undefined; + tasks: + | { + items: ISunoTask[] | undefined; + total: number | undefined; + active: ISunoTask | undefined; + } + | undefined; + audio: ISunoAudio | undefined; + status: { + getService: Status; + getApplication: Status; + getTasks: Status; + }; +} + +// export interface PlayerState { +// audio?: HTMLAudioElement | undefined; +// loopType?: number; +// volume?: number; +// playList?: Song[] | undefined; +// showPlayList?: boolean; +// id?: string; +// url?: string; +// song?: Song | undefined; +// isPlaying?: boolean; +// isPause?: boolean; +// sliderInput?: boolean; +// ended?: boolean; +// muted?: boolean; +// currentTime?: number; +// duration?: number; +// } diff --git a/src/store/suno/mutations.ts b/src/store/suno/mutations.ts new file mode 100644 index 00000000..410f1768 --- /dev/null +++ b/src/store/suno/mutations.ts @@ -0,0 +1,169 @@ +import { IApplication, ICredential, ISunoConfig, ISunoTask, IService } from '@/models'; +import { ISunoState } from './models'; +// import { Song, SongUrl } from '@/models'; + +// const KEYS = { +// volume: 'PLAYER-VOLUME' +// }; +export const resetAll = (state: ISunoState): void => { + state.service = undefined; + state.application = undefined; + state.config = undefined; + state.credential = undefined; + state.tasks = undefined; +}; + +export const setService = (state: ISunoState, payload: IService): void => { + state.service = payload; +}; + +export const setCredential = (state: ISunoState, payload: ICredential): void => { + state.credential = payload; +}; + +export const setApplication = (state: ISunoState, payload: IApplication): void => { + state.application = payload; +}; + +export const setConfig = (state: ISunoState, payload: ISunoConfig): void => { + state.config = payload; +}; + +export const setTasksItems = (state: ISunoState, payload: ISunoTask[]): void => { + const newPayload = { + ...state.tasks, + items: payload + } as typeof state.tasks; + state.tasks = newPayload; +}; + +export const setTasksTotal = (state: ISunoState, payload: number): void => { + const newPayload = { + ...state.tasks, + total: payload + } as typeof state.tasks; + state.tasks = newPayload; +}; + +export const setTasksActive = (state: ISunoState, payload: ISunoTask): void => { + const newPayload = { + ...state.tasks, + active: payload + } as typeof state.tasks; + state.tasks = newPayload; +}; + +export const setAudio = (state: ISunoState, payload: any): void => { + state.audio = payload; +}; + +export const setTasks = (state: ISunoState, payload: any): void => { + state.tasks = payload; +}; +// // 歌曲播放的mutations方法 +// // 播放器相关的 mutations +// export const setAudio = (state: RootState, audio: HTMLAudioElement): void => { +// state.player.audio = audio; +// }; +// // 设置循环模式 +// export const setLoopType = (state: RootState, loopType: number): void => { +// state.player.loopType = loopType; +// }; +// // 设置音量 +// export const setVolume = (state: RootState, volume: number): void => { +// state.player.volume = volume; +// state.player.audio.volume = volume / 100; +// localStorage.setItem(KEYS.volume, volume.toString()); +// }; +// // 设置播放列表 +// export const setPlayList = (state: RootState, playList: Song[]): void => { +// state.player.playList = playList; +// }; +// // 设置是否展示播放列表 +// export const setShowPlayList = (state: RootState, showPlayList: boolean): void => { +// state.player.showPlayList = showPlayList; +// }; +// // 设置歌曲ID +// export const setId = (state: RootState, id: string): void => { +// state.player.id = id; +// }; +// // 设置歌曲URL +// export const setUrl = (state: RootState, url: string): void => { +// state.player.url = url; +// }; +// // 设置歌曲 +// export const setSong = (state: RootState, song: Song): void => { +// state.player.song = song; +// }; +// // 设置歌曲正在播放 +// export const setIsPlaying = (state: RootState, isPlaying: boolean): void => { +// state.player.isPlaying = isPlaying; +// }; +// // 设置歌曲正在暂停 +// export const setIsPause = (state: RootState, isPause: boolean): void => { +// state.player.isPause = isPause; +// }; +// // 设置歌曲播放进度 +// export const setSliderInput = (state: RootState, sliderInput: boolean): void => { +// state.player.sliderInput = sliderInput; +// }; +// // 设置歌曲播放结束 +// export const setEnded = (state: RootState, ended: boolean): void => { +// state.player.ended = ended; +// }; +// // 设置歌曲静音 +// export const setMuted = (state: RootState, muted: boolean): void => { +// state.player.muted = muted; +// state.player.audio.muted = muted; +// }; +// // 设置歌曲播放当前时间 +// export const setCurrentTime = (state: RootState, currentTime: number): void => { +// state.player.currentTime = currentTime; +// }; +// // 设置歌曲总时长 +// export const setDuration = (state: RootState, duration: number): void => { +// state.player.duration = duration; +// }; +// // 将歌曲添加到播放列表 +// export const pushPlayList = (state: RootState, { replace, list }: { replace: boolean; list: Song[] }): void => { +// if (replace) { +// state.player.playList = list; +// } else { +// list.forEach((song) => { +// if (state.player.playList.filter((s) => s.id == song.id).length <= 0) { +// state.player.playList.push(song); +// } +// }); +// } +// }; +// // 清空播放列表 +// export const clearPlayList = (state: RootState): void => { +// state.player.url = ''; +// state.player.id = '0'; +// state.player.song = {} as Song; +// state.player.isPlaying = false; +// state.player.isPause = false; +// state.player.sliderInput = false; +// state.player.ended = false; +// state.player.muted = false; +// state.player.currentTime = 0; +// state.player.playList = []; +// state.player.showPlayList = false; +// state.player.audio.load(); +// setTimeout(() => { +// state.player.duration = 0; +// }, 500); +// }; + +export default { + setTasks, + setApplication, + setConfig, + setCredential, + setService, + setTasksActive, + setTasksItems, + setTasksTotal, + setAudio, + resetAll +}; diff --git a/src/store/suno/persist.ts b/src/store/suno/persist.ts new file mode 100644 index 00000000..0c3fc2d5 --- /dev/null +++ b/src/store/suno/persist.ts @@ -0,0 +1 @@ +export default ['suno.config', 'suno.credential', 'suno.application', 'suno.tasks']; diff --git a/src/store/suno/state.ts b/src/store/suno/state.ts new file mode 100644 index 00000000..fd6fac0b --- /dev/null +++ b/src/store/suno/state.ts @@ -0,0 +1,20 @@ +import { ISunoState } from './models'; +import { Status, Song } from '@/models'; + +export default (): ISunoState => { + return { + service: undefined, + application: undefined, + tasks: undefined, + audio: { + volume: 100 + }, + credential: undefined, + config: undefined, + status: { + getService: Status.None, + getApplication: Status.None, + getTasks: Status.None + } + }; +}; diff --git a/src/utils/number.ts b/src/utils/number.ts new file mode 100644 index 00000000..3d97cc28 --- /dev/null +++ b/src/utils/number.ts @@ -0,0 +1,26 @@ +export function useNumberFormat(number: number): string | number { + if (number > 100000000) { + return Number((number / 100000000).toFixed(1)) + ' 亿'; + } + + if (number > 10000000) { + return Number((number / 10000000).toFixed(1)) + ' 千万'; + } + + if (number > 10000) { + return Number((number / 10000).toFixed(1)) + ' 万'; + } + + return number; +} + +export function useFormatDuring(during: number) { + const s = Math.floor(during) % 60; + during = Math.floor(during / 60); + const i = during % 60; + + const ii = i < 10 ? `0${i}` : i; + const ss = s < 10 ? `0${s}` : s; + + return ii + ':' + ss; +} diff --git a/transmart.config.js b/transmart.config.js index b38db378..c416018b 100644 --- a/transmart.config.js +++ b/transmart.config.js @@ -4,7 +4,7 @@ module.exports = { cacheEnabled: true, localePath: 'src/i18n', openAIApiKey: process.env.VITE_OPENAI_API_KEY, - openAIApiModel: 'gpt-3.5-turbo-1106', + openAIApiModel: 'gpt-4o-mini', openAIApiUrl: 'https://api.acedata.cloud', openAIApiUrlPath: '/openai/chat/completions', modelContextLimit: 3000, diff --git a/yarn.lock b/yarn.lock index 594877bb..882bb8fb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -303,6 +303,11 @@ resolved "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz" integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== +"@icon-park/vue-next@^1.3.6": + version "1.3.6" + resolved "https://registry.yarnpkg.com/@icon-park/vue-next/-/vue-next-1.3.6.tgz#29c07b427002f498101dea3a1edbb6a61481e4b5" + integrity sha512-AZaCcjRPU9vTNVfcrG03CYJM+uv0SXx5lC4njanlRorFo+TV/x0ZTYGYGpTR/l6ek6QmFu9THmrgqKl7i/8yHg== + "@intlify/core-base@9.1.9": version "9.1.9" resolved "https://registry.npmjs.org/@intlify/core-base/-/core-base-9.1.9.tgz"