acomplish replacing Vue-cli with Vite

This commit is contained in:
GeekMaster
2025-05-26 15:56:18 +08:00
parent b1ddcef593
commit 76a3ada85f
44 changed files with 2811 additions and 9576 deletions

78
web/auto-imports.d.ts vendored Normal file
View File

@@ -0,0 +1,78 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
// biome-ignore lint: disable
export {}
declare global {
const EffectScope: typeof import('vue')['EffectScope']
const computed: typeof import('vue')['computed']
const createApp: typeof import('vue')['createApp']
const customRef: typeof import('vue')['customRef']
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
const defineComponent: typeof import('vue')['defineComponent']
const effectScope: typeof import('vue')['effectScope']
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
const getCurrentScope: typeof import('vue')['getCurrentScope']
const h: typeof import('vue')['h']
const inject: typeof import('vue')['inject']
const isProxy: typeof import('vue')['isProxy']
const isReactive: typeof import('vue')['isReactive']
const isReadonly: typeof import('vue')['isReadonly']
const isRef: typeof import('vue')['isRef']
const markRaw: typeof import('vue')['markRaw']
const nextTick: typeof import('vue')['nextTick']
const onActivated: typeof import('vue')['onActivated']
const onBeforeMount: typeof import('vue')['onBeforeMount']
const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave']
const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate']
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
const onDeactivated: typeof import('vue')['onDeactivated']
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
const onMounted: typeof import('vue')['onMounted']
const onRenderTracked: typeof import('vue')['onRenderTracked']
const onRenderTriggered: typeof import('vue')['onRenderTriggered']
const onScopeDispose: typeof import('vue')['onScopeDispose']
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
const onUnmounted: typeof import('vue')['onUnmounted']
const onUpdated: typeof import('vue')['onUpdated']
const onWatcherCleanup: typeof import('vue')['onWatcherCleanup']
const provide: typeof import('vue')['provide']
const reactive: typeof import('vue')['reactive']
const readonly: typeof import('vue')['readonly']
const ref: typeof import('vue')['ref']
const resolveComponent: typeof import('vue')['resolveComponent']
const shallowReactive: typeof import('vue')['shallowReactive']
const shallowReadonly: typeof import('vue')['shallowReadonly']
const shallowRef: typeof import('vue')['shallowRef']
const toRaw: typeof import('vue')['toRaw']
const toRef: typeof import('vue')['toRef']
const toRefs: typeof import('vue')['toRefs']
const toValue: typeof import('vue')['toValue']
const triggerRef: typeof import('vue')['triggerRef']
const unref: typeof import('vue')['unref']
const useAttrs: typeof import('vue')['useAttrs']
const useCssModule: typeof import('vue')['useCssModule']
const useCssVars: typeof import('vue')['useCssVars']
const useFetch: typeof import('@vueuse/core')['useFetch']
const useId: typeof import('vue')['useId']
const useLink: typeof import('vue-router')['useLink']
const useModel: typeof import('vue')['useModel']
const useMouse: typeof import('@vueuse/core')['useMouse']
const useRoute: typeof import('vue-router')['useRoute']
const useRouter: typeof import('vue-router')['useRouter']
const useSlots: typeof import('vue')['useSlots']
const useTemplateRef: typeof import('vue')['useTemplateRef']
const watch: typeof import('vue')['watch']
const watchEffect: typeof import('vue')['watchEffect']
const watchPostEffect: typeof import('vue')['watchPostEffect']
const watchSyncEffect: typeof import('vue')['watchSyncEffect']
}
// for type re-export
declare global {
// @ts-ignore
export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
import('vue')
}

View File

@@ -1,5 +0,0 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}

View File

@@ -1,20 +0,0 @@
{
"compilerOptions": {
"target": "es5",
"module": "esnext",
"baseUrl": "./",
"jsx": "preserve",
"moduleResolution": "node",
"paths": {
"@/*": [
"src/*"
]
},
"lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
]
}
}

7312
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,8 +2,9 @@
"name": "geekai-web",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"dev": "vite --host",
"build": "vite build",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore"
},
@@ -43,11 +44,11 @@
"vant": "^4.5.0",
"vue": "^3.2.13",
"vue-router": "^4.0.15",
"unplugin-auto-import": "^0.18.5",
"vue-waterfall-plugin-next": "^2.6.5"
},
"devDependencies": {
"@babel/core": "7.18.6",
"@babel/eslint-parser": "^7.12.16",
"@vitejs/plugin-vue": "^5.2.4",
"autoprefixer": "^10.4.20",
"postcss": "^8.4.49",
@@ -55,25 +56,5 @@
"stylus-loader": "^7.0.0",
"tailwindcss": "^3.4.17",
"vite": "^5.4.10"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/vue3-essential",
"eslint:recommended"
],
"parserOptions": {
"parser": "@babel/eslint-parser"
},
"rules": {}
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead",
"not ie 11"
]
}
}

View File

@@ -1,4 +1,4 @@
module.exports = {
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},

View File

@@ -119,8 +119,6 @@ console.log(
</script>
<style lang="stylus">
@import '@/assets/iconfont/iconfont.css'
html, body {
margin: 0;
padding: 0;

View File

@@ -324,7 +324,6 @@ const hangUp = async () => {
defineExpose({ connect, hangUp })
</script>
<style scoped lang="stylus">
@import "@/assets/css/realtime.styl"
<style lang="stylus" scoped>
@import "../assets/css/realtime.styl"
</style>

View File

@@ -22,7 +22,7 @@
</template>
<script setup>
import nodata from "@/assets/img/no-data.png";
import nodata from '@/assets/img/no-data.png'
// eslint-disable-next-line no-undef
const props = defineProps({
@@ -30,9 +30,9 @@ const props = defineProps({
type: Array,
default: [],
},
});
})
</script>
<style scoped lang="stylus">
@import "~@/assets/css/running-job-list.styl"
<style lang="stylus" scoped>
@import "../assets/css/running-job-list.styl"
</style>

View File

@@ -4,7 +4,12 @@
<div class="flex h-20">
<ul class="scrollbar-type-nav">
<li :class="{ active: typeId === '' }" @click="getAppList('')">全部分类</li>
<li v-for="item in appTypes" :key="item.id" :class="{ active: typeId === item.id }" @click="getAppList(item.id)">
<li
v-for="item in appTypes"
:key="item.id"
:class="{ active: typeId === item.id }"
@click="getAppList(item.id)"
>
<div class="image" v-if="item.icon">
<el-image :src="item.icon" fit="cover" />
</div>
@@ -27,37 +32,25 @@
<div class="info-text">{{ scope.item.hello_msg }}</div>
</div>
<div class="btn">
<el-button size="small" class="sm-btn-theme" @click="useRole(scope.item)">使用</el-button>
<el-button size="small" class="sm-btn-theme" @click="useRole(scope.item)"
>使用</el-button
>
<el-tooltip content="从工作区移除" placement="top" v-if="hasRole(scope.item.key)">
<el-button size="small" type="danger" @click="updateRole(scope.item, 'remove')">移除</el-button>
<el-button size="small" type="danger" @click="updateRole(scope.item, 'remove')"
>移除</el-button
>
</el-tooltip>
<el-tooltip content="添加到工作区" placement="top" v-else>
<el-button size="small" style="--el-color-primary: #009999" @click="updateRole(scope.item, 'add')">添加</el-button>
<el-button
size="small"
style="--el-color-primary: #009999"
@click="updateRole(scope.item, 'add')"
>添加</el-button
>
</el-tooltip>
</div>
</div>
</div>
<!-- <div class="app-item">-->
<!-- <el-image :src="scope.item.icon" fit="cover"/>-->
<!-- <div class="title">-->
<!-- <span class="name">{{ scope.item.name }}</span>-->
<!-- <div class="opt">-->
<!-- <div v-if="hasRole(scope.item.key)">-->
<!-- <el-button size="small" type="success" @click="useRole(scope.item)">使用</el-button>-->
<!-- <el-button size="small" type="danger" @click="updateRole(scope.item,'remove')">移除</el-button>-->
<!-- </div>-->
<!-- <el-button v-else size="small"-->
<!-- style="&#45;&#45;el-color-primary:#009999"-->
<!-- @click="updateRole(scope.item, 'add')">-->
<!-- <el-icon>-->
<!-- <Plus/>-->
<!-- </el-icon>-->
<!-- <span>添加应用</span>-->
<!-- </el-button>-->
<!-- </div>-->
<!-- </div>-->
<!-- <div class="hello-msg" ref="elements">{{ scope.item.intro }}</div>-->
<!-- </div>-->
</template>
</ItemList>
<div v-else style="width: 100%">
@@ -69,112 +62,112 @@
</template>
<script setup>
import nodata from "@/assets/img/no-data.png";
import { onMounted, ref } from "vue";
import { ElMessage } from "element-plus";
import { httpGet, httpPost } from "@/utils/http";
import { checkSession } from "@/store/cache";
import { arrayContains, removeArrayItem, substr } from "@/utils/libs";
import { useRouter } from "vue-router";
import { useSharedStore } from "@/store/sharedata";
import ItemList from "@/components/ItemList.vue";
import nodata from '@/assets/img/no-data.png'
import ItemList from '@/components/ItemList.vue'
import { checkSession } from '@/store/cache'
import { useSharedStore } from '@/store/sharedata'
import { httpGet, httpPost } from '@/utils/http'
import { arrayContains, removeArrayItem, substr } from '@/utils/libs'
import { ElMessage } from 'element-plus'
import { onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
const listBoxHeight = window.innerHeight - 133;
const listBoxHeight = window.innerHeight - 133
const typeId = ref("");
const appTypes = ref([]);
const list = ref([]);
const roles = ref([]);
const store = useSharedStore();
const typeId = ref('')
const appTypes = ref([])
const list = ref([])
const roles = ref([])
const store = useSharedStore()
onMounted(() => {
getAppType();
getAppList();
getRoles();
});
getAppType()
getAppList()
getRoles()
})
const getRoles = () => {
checkSession()
.then((user) => {
roles.value = user.chat_roles;
roles.value = user.chat_roles
})
.catch((e) => {
console.log(e.message);
});
};
console.log(e.message)
})
}
const getAppType = () => {
httpGet("/api/app/type/list")
httpGet('/api/app/type/list')
.then((res) => {
appTypes.value = res.data;
appTypes.value = res.data
})
.catch((e) => {
ElMessage.error("获取分类失败:" + e.message);
});
};
ElMessage.error('获取分类失败:' + e.message)
})
}
const getAppList = (tid = "") => {
typeId.value = tid;
httpGet("/api/app/list", { tid })
const getAppList = (tid = '') => {
typeId.value = tid
httpGet('/api/app/list', { tid })
.then((res) => {
const items = res.data;
const items = res.data
// 处理 hello message
for (let i = 0; i < items.length; i++) {
items[i].intro = substr(items[i].hello_msg, 80);
items[i].intro = substr(items[i].hello_msg, 80)
}
list.value = items;
list.value = items
})
.catch((e) => {
ElMessage.error("获取应用失败:" + e.message);
});
};
ElMessage.error('获取应用失败:' + e.message)
})
}
const updateRole = (row, opt) => {
checkSession()
.then(() => {
const title = ref("");
if (opt === "add") {
title.value = "添加应用";
const exists = arrayContains(roles.value, row.key);
const title = ref('')
if (opt === 'add') {
title.value = '添加应用'
const exists = arrayContains(roles.value, row.key)
if (exists) {
return;
return
}
roles.value.push(row.key);
roles.value.push(row.key)
} else {
title.value = "移除应用";
const exists = arrayContains(roles.value, row.key);
title.value = '移除应用'
const exists = arrayContains(roles.value, row.key)
if (!exists) {
return;
return
}
roles.value = removeArrayItem(roles.value, row.key);
roles.value = removeArrayItem(roles.value, row.key)
}
httpPost("/api/app/update", { keys: roles.value })
httpPost('/api/app/update', { keys: roles.value })
.then(() => {
ElMessage.success({
message: title.value + "成功!",
message: title.value + '成功!',
duration: 1000,
});
})
})
.catch((e) => {
ElMessage.error(title.value + "失败:" + e.message);
});
ElMessage.error(title.value + '失败:' + e.message)
})
})
.catch(() => {
store.setShowLoginDialog(true);
});
};
store.setShowLoginDialog(true)
})
}
const hasRole = (roleKey) => {
return arrayContains(roles.value, roleKey, (v1, v2) => v1 === v2);
};
return arrayContains(roles.value, roleKey, (v1, v2) => v1 === v2)
}
const router = useRouter();
const router = useRouter()
const useRole = (role) => {
router.push(`/chat?role_id=${role.id}`);
};
router.push(`/chat?role_id=${role.id}`)
}
</script>
<style lang="stylus">
@import "@/assets/css/chat-app.styl"
@import "@/assets/css/custom-scroll.styl"
<style lang="stylus" scoped>
@import "../assets/css/chat-app.styl"
@import "../assets/css/custom-scroll.styl"
</style>

View File

@@ -264,7 +264,12 @@
<welcome @send="autofillPrompt" />
</div>
<div v-for="item in chatData" :key="item.id" v-else>
<chat-prompt v-if="item.type === 'prompt'" :data="item" :list-style="listStyle" @edit="editUserPrompt" />
<chat-prompt
v-if="item.type === 'prompt'"
:data="item"
:list-style="listStyle"
@edit="editUserPrompt"
/>
<chat-reply
v-else-if="item.type === 'reply'"
:data="item"
@@ -1191,54 +1196,57 @@ const stopGenerate = function () {
// 重新生成
const reGenerate = function () {
// 恢复发送按钮状态
canSend.value = true;
showStopGenerate.value = false;
canSend.value = true
showStopGenerate.value = false
// 查找最后的用户消息和AI回复并删除
if (chatData.value.length >= 2) {
// 从后往前找如果最后一条是AI回复再往前一条是用户消息
if (chatData.value[chatData.value.length - 1].type === 'reply') {
// 删除AI回复
chatData.value.pop();
chatData.value.pop()
// 如果此时最后一条是用户消息,也删除它
if (chatData.value.length > 0 && chatData.value[chatData.value.length - 1].type === 'prompt') {
if (
chatData.value.length > 0 &&
chatData.value[chatData.value.length - 1].type === 'prompt'
) {
// 保存用户消息内容,填入输入框
const userPrompt = chatData.value[chatData.value.length - 1].content;
const userPrompt = chatData.value[chatData.value.length - 1].content
// 删除用户消息
chatData.value.pop();
chatData.value.pop()
// 填入输入框
prompt.value = userPrompt;
prompt.value = userPrompt
}
}
}
// 将光标定位到输入框并聚焦
nextTick(() => {
if (inputRef.value) {
inputRef.value.focus();
inputRef.value.focus()
// 触发输入事件以更新文本高度
onInput({ keyCode: null });
onInput({ keyCode: null })
}
});
})
}
// 编辑用户消息
const editUserPrompt = function (messageId) {
// 找到要编辑的消息及其索引
let messageIndex = -1;
let messageContent = '';
let messageIndex = -1
let messageContent = ''
for (let i = 0; i < chatData.value.length; i++) {
if (chatData.value[i].id === messageId) {
messageIndex = i;
messageContent = chatData.value[i].content;
break;
messageIndex = i
messageContent = chatData.value[i].content
break
}
}
if (messageIndex === -1) return;
if (messageIndex === -1) return
// 弹出编辑对话框
ElMessageBox.prompt('', '编辑消息', {
confirmButtonText: '确定',
@@ -1246,51 +1254,53 @@ const editUserPrompt = function (messageId) {
inputValue: messageContent,
inputType: 'textarea',
customClass: 'edit-prompt-dialog',
roundButton: true
}).then(({ value }) => {
if (value.trim() === '') {
ElMessage.warning('消息内容不能为空');
return;
}
// 更新用户消息
chatData.value[messageIndex].content = value;
// 移除该消息之后的所有消息
chatData.value = chatData.value.slice(0, messageIndex + 1);
// 添加空回复消息
const _role = getRoleById(roleId.value);
chatData.value.push({
chat_id: chatId,
role_id: roleId.value,
type: 'reply',
id: randString(32),
icon: _role['icon'],
content: '',
});
disableInput(false);
// 发送编辑后的消息
store.socket.conn.send(
JSON.stringify({
channel: 'chat',
type: 'text',
body: {
role_id: roleId.value,
model_id: modelID.value,
chat_id: chatId.value,
content: value,
tools: toolSelected.value,
stream: stream.value,
edit_message: true
},
roundButton: true,
})
.then(({ value }) => {
if (value.trim() === '') {
ElMessage.warning('消息内容不能为空')
return
}
// 更新用户消息
chatData.value[messageIndex].content = value
// 移除该消息之后的所有消息
chatData.value = chatData.value.slice(0, messageIndex + 1)
// 添加空回复消息
const _role = getRoleById(roleId.value)
chatData.value.push({
chat_id: chatId,
role_id: roleId.value,
type: 'reply',
id: randString(32),
icon: _role['icon'],
content: '',
})
);
}).catch(() => {
// 取消编辑
});
disableInput(false)
// 发送编辑后的消息
store.socket.conn.send(
JSON.stringify({
channel: 'chat',
type: 'text',
body: {
role_id: roleId.value,
model_id: modelID.value,
chat_id: chatId.value,
content: value,
tools: toolSelected.value,
stream: stream.value,
edit_message: true,
},
})
)
})
.catch(() => {
// 取消编辑
})
}
const chatName = ref('')
@@ -1374,11 +1384,11 @@ const realtimeChat = () => {
</script>
<style scoped lang="stylus">
@import "@/assets/css/chat-plus.styl"
@import '../assets/css/chat-plus.styl'
</style>
<style lang="stylus">
@import '@/assets/css/markdown/vue.css';
@import '../assets/css/markdown/vue.css';
.notice-dialog {
.el-dialog__header {
padding-bottom 0

View File

@@ -288,16 +288,16 @@
<script setup>
import nodata from '@/assets/img/no-data.png'
import { nextTick, onMounted, onUnmounted, ref } from 'vue'
import { Delete, InfoFilled, Picture } from '@element-plus/icons-vue'
import { httpGet, httpPost } from '@/utils/http'
import { ElMessage, ElMessageBox } from 'element-plus'
import Clipboard from 'clipboard'
import BackTop from '@/components/BackTop.vue'
import TaskList from '@/components/TaskList.vue'
import { checkSession, getSystemInfo } from '@/store/cache'
import { useSharedStore } from '@/store/sharedata'
import TaskList from '@/components/TaskList.vue'
import BackTop from '@/components/BackTop.vue'
import { showMessageError, showMessageOK } from '@/utils/dialog'
import { httpGet, httpPost } from '@/utils/http'
import { Delete, InfoFilled } from '@element-plus/icons-vue'
import Clipboard from 'clipboard'
import { ElMessage, ElMessageBox } from 'element-plus'
import { onMounted, onUnmounted, ref } from 'vue'
import { LazyImg, Waterfall } from 'vue-waterfall-plugin-next'
import 'vue-waterfall-plugin-next/dist/style.css'
@@ -585,7 +585,7 @@ const changeModel = (model) => {
}
</script>
<style lang="stylus">
@import '@/assets/css/image-dall.styl';
@import '@/assets/css/custom-scroll.styl';
<style lang="stylus" scoped>
@import '../assets/css/image-dall.styl';
@import '../assets/css/custom-scroll.styl';
</style>

View File

@@ -297,6 +297,6 @@ const loginSuccess = () => {
</script>
<style lang="stylus" scoped>
@import "@/assets/css/custom-scroll.styl"
@import "@/assets/css/home.styl"
@import "../assets/css/custom-scroll.styl"
@import "../assets/css/home.styl"
</style>

View File

@@ -204,7 +204,10 @@
</el-icon>
</el-tooltip>
<div class="flex-row justify-start items-center">
<span>如需自定义比例在绘画指令最后加一个空格然后加上指令(宽高比) --ar w:h 例如: 1 cat --ar 21:9 </span>
<span
>如需自定义比例在绘画指令最后加一个空格然后加上指令(宽高比) --ar w:h
例如: 1 cat --ar 21:9
</span>
</div>
</div>
</div>
@@ -330,8 +333,11 @@
</el-tooltip>
</div>
<div class="flex-row justify-start items-center">
<span>如需自定义比例在绘画指令最后加一个空格然后加上指令(宽高比) --ar w:h 例如: 1 cat --ar 21:9 </span>
</div>
<span
>如需自定义比例在绘画指令最后加一个空格然后加上指令(宽高比) --ar w:h
例如: 1 cat --ar 21:9
</span>
</div>
</div>
</div>
@@ -545,7 +551,10 @@
</el-icon>
</el-tooltip>
<div class="flex-row justify-start items-center">
<span>如需自定义比例在绘画指令最后加一个空格然后加上指令(宽高比) --ar w:h 例如: 1 cat --ar 21:9 </span>
<span
>如需自定义比例在绘画指令最后加一个空格然后加上指令(宽高比) --ar w:h
例如: 1 cat --ar 21:9
</span>
</div>
</div>
</div>
@@ -1213,8 +1222,8 @@ const generate = () => {
return ElMessage.error('换脸操作需要上传两张图片')
}
const regex = /(^|\s)--ar\s+(\d+:\d+)/;
const match = regex.exec(params.value.prompt);
const regex = /(^|\s)--ar\s+(\d+:\d+)/
const match = regex.exec(params.value.prompt)
if (match) {
params.value.rate = match[2]
}
@@ -1349,6 +1358,6 @@ const generatePrompt = () => {
</script>
<style lang="stylus">
@import '@/assets/css/image-mj.styl';
@import '@/assets/css/custom-scroll.styl';
@import '../assets/css/image-mj.styl';
@import '../assets/css/custom-scroll.styl';
</style>

View File

@@ -471,21 +471,21 @@
</template>
<script setup>
import { nextTick, onMounted, onUnmounted, ref } from 'vue'
import { Delete, DocumentCopy, InfoFilled, Orange } from '@element-plus/icons-vue'
import nodata from '@/assets/img/no-data.png'
import { Delete, InfoFilled, Orange } from '@element-plus/icons-vue'
import { onMounted, onUnmounted, ref } from 'vue'
import { httpGet, httpPost } from '@/utils/http'
import { ElMessage, ElMessageBox } from 'element-plus'
import Clipboard from 'clipboard'
import BackTop from '@/components/BackTop.vue'
import SdTaskView from '@/components/SdTaskView.vue'
import TaskList from '@/components/TaskList.vue'
import { checkSession, getSystemInfo } from '@/store/cache'
import { useRouter } from 'vue-router'
import { getSessionId } from '@/store/session'
import { useSharedStore } from '@/store/sharedata'
import TaskList from '@/components/TaskList.vue'
import BackTop from '@/components/BackTop.vue'
import { showMessageError } from '@/utils/dialog'
import SdTaskView from '@/components/SdTaskView.vue'
import { httpGet, httpPost } from '@/utils/http'
import Clipboard from 'clipboard'
import { ElMessage, ElMessageBox } from 'element-plus'
import { useRouter } from 'vue-router'
import { LazyImg, Waterfall } from 'vue-waterfall-plugin-next'
import 'vue-waterfall-plugin-next/dist/style.css'
@@ -753,6 +753,6 @@ const generatePrompt = () => {
</script>
<style lang="stylus">
@import '@/assets/css/image-sd.styl';
@import '@/assets/css/custom-scroll.styl';
@import '../assets/css/image-sd.styl';
@import '../assets/css/custom-scroll.styl';
</style>

View File

@@ -11,11 +11,7 @@
</el-radio-group>
</div>
</div>
<div
class="waterfall"
:style="{ height: listBoxHeight + 'px' }"
id="waterfall-box"
>
<div class="waterfall" :style="{ height: listBoxHeight + 'px' }" id="waterfall-box">
<Waterfall
v-if="imgType === 'mj'"
id="waterfall-mj"
@@ -55,11 +51,7 @@
class="pt-3 flex justify-center items-center border-t border-t-gray-600 border-opacity-50"
>
<div class="opt">
<el-tooltip
class="box-item"
content="复制提示词"
placement="top"
>
<el-tooltip class="box-item" content="复制提示词" placement="top">
<el-button
type="info"
circle
@@ -70,16 +62,8 @@
</el-button>
</el-tooltip>
<el-tooltip
class="box-item"
content="画同款"
placement="top"
>
<el-button
type="primary"
circle
@click="drawSameMj(item)"
>
<el-tooltip class="box-item" content="画同款" placement="top">
<el-button type="primary" circle @click="drawSameMj(item)">
<i class="iconfont icon-palette"></i>
</el-button>
</el-tooltip>
@@ -129,11 +113,7 @@
class="pt-3 flex justify-center items-center border-t border-t-gray-600 border-opacity-50"
>
<div class="opt">
<el-tooltip
class="box-item"
content="复制提示词"
placement="top"
>
<el-tooltip class="box-item" content="复制提示词" placement="top">
<el-button
type="info"
circle
@@ -144,16 +124,8 @@
</el-button>
</el-tooltip>
<el-tooltip
class="box-item"
content="画同款"
placement="top"
>
<el-button
type="primary"
circle
@click="drawSameSd(item)"
>
<el-tooltip class="box-item" content="画同款" placement="top">
<el-button type="primary" circle @click="drawSameSd(item)">
<i class="iconfont icon-palette"></i>
</el-button>
</el-tooltip>
@@ -203,11 +175,7 @@
class="pt-3 flex justify-center items-center border-t border-t-gray-600 border-opacity-50"
>
<div class="opt">
<el-tooltip
class="box-item"
content="复制提示词"
placement="top"
>
<el-tooltip class="box-item" content="复制提示词" placement="top">
<el-button
type="info"
circle
@@ -261,7 +229,7 @@
<el-image-viewer
@close="
() => {
previewURL = '';
previewURL = ''
}
"
v-if="previewURL !== ''"
@@ -271,142 +239,139 @@
</template>
<script setup>
import nodata from "@/assets/img/no-data.png";
import { nextTick, onMounted, onUnmounted, ref } from "vue";
import { DocumentCopy, Picture } from "@element-plus/icons-vue";
import { httpGet } from "@/utils/http";
import { ElMessage } from "element-plus";
import Clipboard from "clipboard";
import { useRouter } from "vue-router";
import BackTop from "@/components/BackTop.vue";
import SdTaskView from "@/components/SdTaskView.vue";
import { LazyImg, Waterfall } from "vue-waterfall-plugin-next";
import "vue-waterfall-plugin-next/dist/style.css";
import { useSharedStore } from "@/store/sharedata";
import BackTop from '@/components/BackTop.vue'
import SdTaskView from '@/components/SdTaskView.vue'
import { useSharedStore } from '@/store/sharedata'
import { httpGet } from '@/utils/http'
import Clipboard from 'clipboard'
import { ElMessage } from 'element-plus'
import { nextTick, onMounted, onUnmounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import { LazyImg, Waterfall } from 'vue-waterfall-plugin-next'
import 'vue-waterfall-plugin-next/dist/style.css'
const store = useSharedStore();
const waterfallOptions = store.waterfallOptions;
const store = useSharedStore()
const waterfallOptions = store.waterfallOptions
const data = ref({
mj: [],
sd: [],
dall: [],
});
const loading = ref(true);
const isOver = ref(false);
const imgType = ref("mj"); // 图片类别
const listBoxHeight = window.innerHeight - 124;
const showTaskDialog = ref(false);
const item = ref({});
const previewURL = ref("");
})
const loading = ref(true)
const isOver = ref(false)
const imgType = ref('mj') // 图片类别
const listBoxHeight = window.innerHeight - 124
const showTaskDialog = ref(false)
const item = ref({})
const previewURL = ref('')
const previewImg = (item) => {
previewURL.value = item.img_url;
};
previewURL.value = item.img_url
}
const page = ref(0);
const pageSize = ref(15);
const page = ref(0)
const pageSize = ref(15)
// 获取下一页数据
const getNext = () => {
if (isOver.value) {
return;
return
}
loading.value = true;
page.value = page.value + 1;
let url = "";
loading.value = true
page.value = page.value + 1
let url = ''
switch (imgType.value) {
case "mj":
url = "/api/mj/imgWall";
break;
case "sd":
url = "/api/sd/imgWall";
break;
case "dall":
url = "/api/dall/imgWall";
break;
case 'mj':
url = '/api/mj/imgWall'
break
case 'sd':
url = '/api/sd/imgWall'
break
case 'dall':
url = '/api/dall/imgWall'
break
}
httpGet(`${url}?page=${page.value}&page_size=${pageSize.value}`)
.then((res) => {
if (!res.data.items || res.data.items.length === 0) {
isOver.value = true;
loading.value = false;
return;
isOver.value = true
loading.value = false
return
}
// 生成缩略图
const imageList = res.data.items;
const imageList = res.data.items
for (let i = 0; i < imageList.length; i++) {
imageList[i]["img_thumb"] =
imageList[i]["img_url"] + "?imageView2/4/w/300/h/0/q/75";
imageList[i]['img_thumb'] = imageList[i]['img_url'] + '?imageView2/4/w/300/h/0/q/75'
}
if (data.value[imgType.value].length === 0) {
data.value[imgType.value] = imageList;
return;
data.value[imgType.value] = imageList
return
}
if (imageList.length < pageSize.value) {
isOver.value = true;
isOver.value = true
}
data.value[imgType.value] = data.value[imgType.value].concat(imageList);
data.value[imgType.value] = data.value[imgType.value].concat(imageList)
})
.catch((e) => {
ElMessage.error("获取图片失败:" + e.message);
loading.value = false;
});
};
ElMessage.error('获取图片失败:' + e.message)
loading.value = false
})
}
getNext();
getNext()
const clipboard = ref(null);
const clipboard = ref(null)
onMounted(() => {
clipboard.value = new Clipboard(".copy-prompt-wall");
clipboard.value.on("success", () => {
ElMessage.success("复制成功!");
});
clipboard.value = new Clipboard('.copy-prompt-wall')
clipboard.value.on('success', () => {
ElMessage.success('复制成功!')
})
clipboard.value.on("error", () => {
ElMessage.error("复制失败!");
});
});
clipboard.value.on('error', () => {
ElMessage.error('复制失败!')
})
})
onUnmounted(() => {
clipboard.value.destroy();
});
clipboard.value.destroy()
})
const changeImgType = () => {
console.log(imgType.value);
document.getElementById("waterfall-box").scrollTo(0, 0);
page.value = 0;
console.log(imgType.value)
document.getElementById('waterfall-box').scrollTo(0, 0)
page.value = 0
data.value = {
mj: [],
sd: [],
dall: [],
};
loading.value = true;
isOver.value = false;
nextTick(() => getNext());
};
}
loading.value = true
isOver.value = false
nextTick(() => getNext())
}
const showTask = (row) => {
item.value = row;
showTaskDialog.value = true;
};
item.value = row
showTaskDialog.value = true
}
const router = useRouter();
const router = useRouter()
const drawSameSd = (row) => {
router.push({
name: "image-sd",
name: 'image-sd',
params: { copyParams: JSON.stringify(row.params) },
});
};
})
}
const drawSameMj = (row) => {
router.push({ name: "image-mj", params: { prompt: row.prompt } });
};
router.push({ name: 'image-mj', params: { prompt: row.prompt } })
}
</script>
<style lang="stylus">
@import '@/assets/css/images-wall.styl';
@import '@/assets/css/custom-scroll.styl';
@import '../assets/css/images-wall.styl';
@import '../assets/css/custom-scroll.styl';
</style>

View File

@@ -84,6 +84,7 @@
import FooterBar from '@/components/FooterBar.vue'
import ThemeChange from '@/components/ThemeChange.vue'
import { checkSession, getLicenseInfo, getSystemInfo } from '@/store/cache'
import { removeUserToken } from '@/store/session'
import { httpGet } from '@/utils/http'
import { isMobile } from '@/utils/libs'
import { ElMessage } from 'element-plus'
@@ -211,12 +212,12 @@ const logout = function () {
removeUserToken()
router.push('/login')
})
.catch(() => {
ElMessage.error('注销失败')
.catch((e) => {
ElMessage.error('注销失败' + e.message)
})
}
</script>
<style lang="stylus" scoped>
@import "@/assets/css/index.styl"
@import '../assets/css/index.styl'
</style>

View File

@@ -7,7 +7,8 @@
<div class="info">
我们非常欢迎您把此应用分享给您身边的朋友分享成功注册后您和被邀请人都将获得
<strong>{{ invitePower }}</strong>
算力额度作为奖励 你可以保存下面的二维码或者直接复制分享您的专属推广链接发送给微信好友
算力额度作为奖励
你可以保存下面的二维码或者直接复制分享您的专属推广链接发送给微信好友
</div>
<div class="invite-qrcode">
@@ -16,7 +17,9 @@
<div class="invite-url">
<span>{{ inviteURL }}</span>
<el-button type="primary" plain class="copy-link" :data-clipboard-text="inviteURL">复制链接</el-button>
<el-button type="primary" plain class="copy-link" :data-clipboard-text="inviteURL"
>复制链接</el-button
>
</div>
</div>
@@ -88,79 +91,79 @@
</template>
<script setup>
import { onMounted, ref } from "vue";
import QRCode from "qrcode";
import { httpGet } from "@/utils/http";
import { ElMessage } from "element-plus";
import Clipboard from "clipboard";
import InviteList from "@/components/InviteList.vue";
import { checkSession, getSystemInfo } from "@/store/cache";
import { useSharedStore } from "@/store/sharedata";
import InviteList from '@/components/InviteList.vue'
import { checkSession, getSystemInfo } from '@/store/cache'
import { useSharedStore } from '@/store/sharedata'
import { httpGet } from '@/utils/http'
import Clipboard from 'clipboard'
import { ElMessage } from 'element-plus'
import QRCode from 'qrcode'
import { onMounted, ref } from 'vue'
const inviteURL = ref("");
const qrImg = ref("/images/wx.png");
const invitePower = ref(0);
const hits = ref(0);
const regNum = ref(0);
const rate = ref(0);
const isLogin = ref(false);
const store = useSharedStore();
const inviteURL = ref('')
const qrImg = ref('/images/wx.png')
const invitePower = ref(0)
const hits = ref(0)
const regNum = ref(0)
const rate = ref(0)
const isLogin = ref(false)
const store = useSharedStore()
onMounted(() => {
initData();
initData()
// 复制链接
const clipboard = new Clipboard(".copy-link");
clipboard.on("success", () => {
ElMessage.success("复制成功!");
});
const clipboard = new Clipboard('.copy-link')
clipboard.on('success', () => {
ElMessage.success('复制成功!')
})
clipboard.on("error", () => {
ElMessage.error("复制失败!");
});
});
clipboard.on('error', () => {
ElMessage.error('复制失败!')
})
})
const initData = () => {
checkSession()
.then(() => {
isLogin.value = true;
httpGet("/api/invite/code")
isLogin.value = true
httpGet('/api/invite/code')
.then((res) => {
const text = `${location.protocol}//${location.host}/register?invite_code=${res.data.code}`;
hits.value = res.data["hits"];
regNum.value = res.data["reg_num"];
const text = `${location.protocol}//${location.host}/register?invite_code=${res.data.code}`
hits.value = res.data['hits']
regNum.value = res.data['reg_num']
if (hits.value > 0) {
rate.value = ((regNum.value / hits.value) * 100).toFixed(2);
rate.value = ((regNum.value / hits.value) * 100).toFixed(2)
}
QRCode.toDataURL(text, { width: 400, height: 400, margin: 2 }, (error, url) => {
if (error) {
console.error(error);
console.error(error)
} else {
qrImg.value = url;
qrImg.value = url
}
});
inviteURL.value = text;
})
inviteURL.value = text
})
.catch((e) => {
ElMessage.error("获取邀请码失败:" + e.message);
});
ElMessage.error('获取邀请码失败:' + e.message)
})
getSystemInfo()
.then((res) => {
invitePower.value = res.data["invite_power"];
invitePower.value = res.data['invite_power']
})
.catch((e) => {
ElMessage.error("获取系统配置失败:" + e.message);
});
ElMessage.error('获取系统配置失败:' + e.message)
})
})
.catch(() => {
store.setShowLoginDialog(true);
});
};
store.setShowLoginDialog(true)
})
}
</script>
<style lang="stylus" scoped>
@import "@/assets/css/custom-scroll.styl"
@import '../assets/css/custom-scroll.styl'
.page-invitation {
display: flex;
justify-content: center;

View File

@@ -22,7 +22,9 @@
<el-col :span="8" v-for="item in rates" :key="item.value">
<div
class="flex-col items-center"
:class="item.value === params.aspect_ratio ? 'grid-content active' : 'grid-content'"
:class="
item.value === params.aspect_ratio ? 'grid-content active' : 'grid-content'
"
@click="changeRate(item)"
>
<el-image class="icon proportion" :src="item.img" fit="cover"></el-image>
@@ -35,8 +37,17 @@
<!-- 模型选择 -->
<div class="param-line">
<el-form-item label="模型选择">
<el-select v-model="params.model" placeholder="请选择模型" @change="updateModelPower">
<el-option v-for="item in models" :key="item.value" :label="item.text" :value="item.value" />
<el-select
v-model="params.model"
placeholder="请选择模型"
@change="updateModelPower"
>
<el-option
v-for="item in models"
:key="item.value"
:label="item.text"
:value="item.value"
/>
</el-select>
</el-form-item>
</div>
@@ -44,7 +55,11 @@
<!-- 视频时长 -->
<div class="param-line">
<el-form-item label="视频时长">
<el-select v-model="params.duration" placeholder="请选择时长" @change="updateModelPower">
<el-select
v-model="params.duration"
placeholder="请选择时长"
@change="updateModelPower"
>
<el-option label="5秒" value="5" />
<el-option label="10秒" value="10" />
</el-select>
@@ -54,7 +69,11 @@
<!-- 生成模式 -->
<div class="param-line">
<el-form-item label="生成模式">
<el-select v-model="params.mode" placeholder="请选择模式" @change="updateModelPower">
<el-select
v-model="params.mode"
placeholder="请选择模式"
@change="updateModelPower"
>
<el-option label="标准模式" value="std" />
<el-option label="专业模式" value="pro" />
</el-select>
@@ -94,7 +113,11 @@
<!-- 仅在simple模式下显示详细配置 -->
<div class="camera-control mt-2" v-if="params.camera_control.type === 'simple'">
<el-form-item label="水平移动">
<el-slider v-model="params.camera_control.config.horizontal" :min="-10" :max="10" />
<el-slider
v-model="params.camera_control.config.horizontal"
:min="-10"
:max="10"
/>
</el-form-item>
<el-form-item label="垂直移动">
<el-slider v-model="params.camera_control.config.vertical" :min="-10" :max="10" />
@@ -126,7 +149,10 @@
<div class="text">使用文字描述想要生成视频的内容</div>
</el-tab-pane>
<el-tab-pane label="图生视频" name="image2video">
<div class="text">以某张图片为底稿参考来创作视频生成类似风格或类型视频支持 PNG /JPG/JPEG 格式图片</div>
<div class="text">
以某张图片为底稿参考来创作视频生成类似风格或类型视频支持 PNG /JPG/JPEG
格式图片
</div>
</el-tab-pane>
</el-tabs>
</div>
@@ -142,7 +168,13 @@
placeholder="请在此输入视频提示词,您也可以点击下面的提示词助手生成视频提示词"
/>
<el-row class="text-info">
<el-button class="generate-btn" @click="generatePrompt" :loading="isGenerating" size="small" color="#5865f2">
<el-button
class="generate-btn"
@click="generatePrompt"
:loading="isGenerating"
size="small"
color="#5865f2"
>
<i class="iconfont icon-chuangzuo"></i>
生成专业视频提示词
</el-button>
@@ -152,10 +184,18 @@
<div v-else class="image2video">
<div class="image-upload img-inline">
<div class="upload-box img-uploader video-img-box">
<el-icon v-if="params.image" @click="removeImage('start')" class="removeimg"><CircleCloseFilled /></el-icon>
<el-icon v-if="params.image" @click="removeImage('start')" class="removeimg"
><CircleCloseFilled
/></el-icon>
<h4>起始帧</h4>
<el-upload class="uploader img-uploader" :auto-upload="true" :show-file-list="false" :http-request="uploadStartImage" accept=".jpg,.png,.jpeg">
<el-upload
class="uploader img-uploader"
:auto-upload="true"
:show-file-list="false"
:http-request="uploadStartImage"
accept=".jpg,.png,.jpeg"
>
<img v-if="params.image" :src="params.image" class="preview" />
<el-icon v-else class="upload-icon"><Plus /></el-icon>
@@ -165,9 +205,17 @@
<i class="iconfont icon-exchange" @click="switchReverse"></i>
</div>
<div class="upload-box img-uploader video-img-box">
<el-icon v-if="params.image_tail" @click="removeImage('end')" class="removeimg"><CircleCloseFilled /></el-icon>
<el-icon v-if="params.image_tail" @click="removeImage('end')" class="removeimg"
><CircleCloseFilled
/></el-icon>
<h4>结束帧</h4>
<el-upload class="uploader" :auto-upload="true" :show-file-list="false" :http-request="uploadEndImage" accept=".jpg,.png,.jpeg">
<el-upload
class="uploader"
:auto-upload="true"
:show-file-list="false"
:http-request="uploadEndImage"
accept=".jpg,.png,.jpeg"
>
<img v-if="params.image_tail" :src="params.image_tail" class="preview" />
<el-icon v-else class="upload-icon"><Plus /></el-icon>
</el-upload>
@@ -186,7 +234,12 @@
</div>
</div>
<div class="param-line pt">
<el-input v-model="params.prompt" type="textarea" :autosize="{ minRows: 4, maxRows: 6 }" placeholder="描述视频画面细节" />
<el-input
v-model="params.prompt"
type="textarea"
:autosize="{ minRows: 4, maxRows: 6 }"
placeholder="描述视频画面细节"
/>
</div>
</div>
@@ -238,8 +291,19 @@
<div class="left">
<div class="container">
<div v-if="item.progress === 100">
<video class="video" :src="item.video_url" preload="auto" loop="loop" muted="muted">您的浏览器不支持视频播放</video>
<button class="play flex justify-center items-center" @click="previewVideo(item)">
<video
class="video"
:src="item.video_url"
preload="auto"
loop="loop"
muted="muted"
>
您的浏览器不支持视频播放
</video>
<button
class="play flex justify-center items-center"
@click="previewVideo(item)"
>
<img src="/images/play.svg" alt="" />
</button>
</div>
@@ -255,7 +319,9 @@
<el-tag class="mr-1">{{ item.raw_data.duration }}</el-tag>
<el-tag class="mr-1">{{ item.raw_data.mode }}</el-tag>
</div>
<div class="failed" v-if="item.progress === 101">任务执行失败{{ item.err_msg }}任务提示词{{ item.prompt }}</div>
<div class="failed" v-if="item.progress === 101">
任务执行失败{{ item.err_msg }}任务提示词{{ item.prompt }}
</div>
<div class="prompt" v-else>
{{ substr(item.prompt, 1000) }}
</div>
@@ -275,9 +341,18 @@
</button> -->
<el-tooltip content="下载视频" placement="top">
<button class="btn btn-icon" @click="downloadVideo(item)" :disabled="item.downloading">
<button
class="btn btn-icon"
@click="downloadVideo(item)"
:disabled="item.downloading"
>
<i class="iconfont icon-download" v-if="!item.downloading"></i>
<el-image src="/images/loading.gif" class="downloading" fit="cover" v-else />
<el-image
src="/images/loading.gif"
class="downloading"
fit="cover"
v-else
/>
</button>
</el-tooltip>
@@ -298,7 +373,12 @@
</div>
</div>
<el-empty :image-size="100" :image="nodata" description="没有任何作品,赶紧去创作吧!" v-else />
<el-empty
:image-size="100"
:image="nodata"
description="没有任何作品,赶紧去创作吧!"
v-else
/>
<div class="pagination">
<el-pagination
@@ -318,7 +398,13 @@
</div>
<!-- 视频预览对话框 -->
<black-dialog v-model:show="previewVisible" title="视频预览" hide-footer @cancal="previewVisible = false" width="auto">
<black-dialog
v-model:show="previewVisible"
title="视频预览"
hide-footer
@cancal="previewVisible = false"
width="auto"
>
<video
v-if="currentVideo"
:src="currentVideo"
@@ -336,47 +422,44 @@
</template>
<script setup>
import failed from "@/assets/img/failed.png";
import TaskList from "@/components/TaskList.vue";
import { ref, reactive, onMounted, onUnmounted, watch } from "vue";
import { Plus, Delete, InfoFilled, ChromeFilled, DocumentCopy, Download, WarnTriangleFilled, CircleCloseFilled } from "@element-plus/icons-vue";
import { httpGet, httpPost, httpDownload } from "@/utils/http";
import { ElMessage, ElMessageBox } from "element-plus";
import { checkSession, getSystemInfo } from "@/store/cache";
import Clipboard from "clipboard";
import BlackDialog from "@/components/ui/BlackDialog.vue";
import BlackSwitch from "@/components/ui/BlackSwitch.vue";
import { closeLoading, showLoading, showMessageError, showMessageOK } from "@/utils/dialog";
import { replaceImg, substr } from "@/utils/libs";
import Generating from "@/components/ui/Generating.vue";
import nodata from "@/assets/img/no-data.png";
import nodata from '@/assets/img/no-data.png'
import BlackDialog from '@/components/ui/BlackDialog.vue'
import Generating from '@/components/ui/Generating.vue'
import { checkSession, getSystemInfo } from '@/store/cache'
import { closeLoading, showLoading, showMessageError, showMessageOK } from '@/utils/dialog'
import { httpDownload, httpGet, httpPost } from '@/utils/http'
import { replaceImg, substr } from '@/utils/libs'
import { CircleCloseFilled, InfoFilled, Plus } from '@element-plus/icons-vue'
import Clipboard from 'clipboard'
import { ElMessage, ElMessageBox } from 'element-plus'
import { onMounted, onUnmounted, reactive, ref } from 'vue'
const models = ref([
{
text: "可灵 1.6",
value: "kling-v1-6",
text: '可灵 1.6',
value: 'kling-v1-6',
},
{
text: "可灵 1.5",
value: "kling-v1-5",
text: '可灵 1.5',
value: 'kling-v1-5',
},
{
text: "可灵 1.0",
value: "kling-v1",
text: '可灵 1.0',
value: 'kling-v1',
},
]);
])
// 参数设置
const params = reactive({
task_type: "text2video",
task_type: 'text2video',
model: models.value[0].value,
prompt: "",
negative_prompt: "",
prompt: '',
negative_prompt: '',
cfg_scale: 0.7,
mode: "std",
aspect_ratio: "16:9",
duration: "5",
mode: 'std',
aspect_ratio: '16:9',
duration: '5',
camera_control: {
type: "",
type: '',
config: {
horizontal: 0,
vertical: 0,
@@ -386,142 +469,142 @@ const params = reactive({
zoom: 0,
},
},
image: "",
image_tail: "",
});
image: '',
image_tail: '',
})
const rates = [
{ css: "square", value: "1:1", text: "1:1", img: "/images/mj/rate_1_1.png" },
{ css: 'square', value: '1:1', text: '1:1', img: '/images/mj/rate_1_1.png' },
{
css: "size16-9",
value: "16:9",
text: "16:9",
img: "/images/mj/rate_16_9.png",
css: 'size16-9',
value: '16:9',
text: '16:9',
img: '/images/mj/rate_16_9.png',
},
{
css: "size9-16",
value: "9:16",
text: "9:16",
img: "/images/mj/rate_9_16.png",
css: 'size9-16',
value: '9:16',
text: '9:16',
img: '/images/mj/rate_9_16.png',
},
];
]
// 切换图片比例
const changeRate = (item) => {
params.aspect_ratio = item.value;
};
params.aspect_ratio = item.value
}
const generating = ref(false);
const isGenerating = ref(false);
const powerCost = ref(10);
const availablePower = ref(100);
const taskFilter = ref("all");
const loading = ref(false);
const list = ref([]);
const noData = ref(true);
const page = ref(1);
const pageSize = ref(10);
const total = ref(0);
const taskPulling = ref(true);
const pullHandler = ref(null);
const previewVisible = ref(false);
const currentVideo = ref("");
const showCameraControl = ref(false);
const keLingPowers = ref({});
const isLogin = ref(false);
const generating = ref(false)
const isGenerating = ref(false)
const powerCost = ref(10)
const availablePower = ref(100)
const taskFilter = ref('all')
const loading = ref(false)
const list = ref([])
const noData = ref(true)
const page = ref(1)
const pageSize = ref(10)
const total = ref(0)
const taskPulling = ref(true)
const pullHandler = ref(null)
const previewVisible = ref(false)
const currentVideo = ref('')
const showCameraControl = ref(false)
const keLingPowers = ref({})
const isLogin = ref(false)
// 动态更新模型消耗的算力
const updateModelPower = () => {
showCameraControl.value = params.model === "kling-v1-5" && params.mode === "pro";
powerCost.value = keLingPowers.value[`${params.model}_${params.mode}_${params.duration}`] || {};
};
showCameraControl.value = params.model === 'kling-v1-5' && params.mode === 'pro'
powerCost.value = keLingPowers.value[`${params.model}_${params.mode}_${params.duration}`] || {}
}
// tab切换
const tabChange = (tab) => {
params.task_type = tab;
};
params.task_type = tab
}
const uploadStartImage = async (file) => {
const formData = new FormData();
formData.append("file", file.file);
const formData = new FormData()
formData.append('file', file.file)
try {
showLoading("图片上传中...");
const res = await httpPost("/api/upload", formData);
params.image = res.data.url;
ElMessage.success("上传成功");
closeLoading();
showLoading('图片上传中...')
const res = await httpPost('/api/upload', formData)
params.image = res.data.url
ElMessage.success('上传成功')
closeLoading()
} catch (e) {
showMessageError("上传失败: " + e.message);
closeLoading();
showMessageError('上传失败: ' + e.message)
closeLoading()
}
};
}
//移除图片
const removeImage = (type) => {
if (type === "start") {
params.image = "";
} else if (type === "end") {
params.image_tail = "";
if (type === 'start') {
params.image = ''
} else if (type === 'end') {
params.image_tail = ''
}
};
}
//图片交换方法
const switchReverse = () => {
[params.image, params.image_tail] = [params.image_tail, params.image];
};
;[params.image, params.image_tail] = [params.image_tail, params.image]
}
const uploadEndImage = async (file) => {
const formData = new FormData();
formData.append("file", file.file);
const formData = new FormData()
formData.append('file', file.file)
try {
const res = await httpPost("/api/upload", formData);
params.image_tail = res.data.url;
ElMessage.success("上传成功");
const res = await httpPost('/api/upload', formData)
params.image_tail = res.data.url
ElMessage.success('上传成功')
} catch (e) {
showMessageError("上传失败: " + e.message);
showMessageError('上传失败: ' + e.message)
}
};
}
const generatePrompt = async () => {
if (isGenerating.value) return;
if (isGenerating.value) return
if (!params.prompt) {
return showMessageError("请输入视频描述");
return showMessageError('请输入视频描述')
}
isGenerating.value = true;
isGenerating.value = true
try {
const res = await httpPost("/api/prompt/video", { prompt: params.prompt });
params.prompt = res.data;
const res = await httpPost('/api/prompt/video', { prompt: params.prompt })
params.prompt = res.data
} catch (e) {
showMessageError("生成失败: " + e.message);
showMessageError('生成失败: ' + e.message)
} finally {
isGenerating.value = false;
isGenerating.value = false
}
};
}
const generate = async () => {
//增加防抖
if (generating.value) return;
if (generating.value) return
if (!params.prompt?.trim()) {
return ElMessage.error("请输入视频描述");
return ElMessage.error('请输入视频描述')
}
// 提示词长度不能超过 500
if (params.prompt.length > 500) {
return ElMessage.error("视频描述不能超过 500 个字符");
return ElMessage.error('视频描述不能超过 500 个字符')
}
if (params.task_type === "image2video" && !params.image) {
return ElMessage.error("请上传起始帧图片");
if (params.task_type === 'image2video' && !params.image) {
return ElMessage.error('请上传起始帧图片')
}
generating.value = true;
generating.value = true
// 处理图片链接
if (params.image) {
params.image = replaceImg(params.image);
params.image = replaceImg(params.image)
}
if (params.image_tail) {
params.image_tail = replaceImg(params.image_tail);
params.image_tail = replaceImg(params.image_tail)
}
try {
await httpPost("/api/video/keling/create", params);
showMessageOK("任务创建成功");
await httpPost('/api/video/keling/create', params)
showMessageOK('任务创建成功')
// 新增重置
page.value = 1;
page.value = 1
list.value.unshift({
progress: 0,
prompt: params.prompt,
@@ -531,132 +614,132 @@ const generate = async () => {
duration: params.duration,
mode: params.mode,
},
});
taskPulling.value = true;
})
taskPulling.value = true
} catch (e) {
showMessageError("创建失败: " + e.message);
showMessageError('创建失败: ' + e.message)
} finally {
generating.value = false;
generating.value = false
}
};
}
const fetchData = (_page) => {
if (_page) {
page.value = _page;
page.value = _page
}
httpGet("/api/video/list", {
httpGet('/api/video/list', {
page: page.value,
page_size: pageSize.value,
type: "keling",
task_type: taskFilter.value === "all" ? "" : taskFilter.value,
type: 'keling',
task_type: taskFilter.value === 'all' ? '' : taskFilter.value,
})
.then((res) => {
total.value = res.data.total;
let needPull = false;
const items = [];
total.value = res.data.total
let needPull = false
const items = []
for (let v of res.data.items) {
if (v.progress === 0 || v.progress === 102) {
needPull = true;
needPull = true
}
items.push({
...v,
downloading: false,
});
})
}
loading.value = false;
taskPulling.value = needPull;
loading.value = false
taskPulling.value = needPull
if (JSON.stringify(list.value) !== JSON.stringify(items)) {
list.value = items;
list.value = items
}
noData.value = list.value.length === 0;
noData.value = list.value.length === 0
})
.catch(() => {
loading.value = false;
noData.value = true;
});
};
loading.value = false
noData.value = true
})
}
const previewVideo = (task) => {
currentVideo.value = task.video_url;
previewVisible.value = true;
};
currentVideo.value = task.video_url
previewVisible.value = true
}
const downloadVideo = async (task) => {
try {
const res = await httpDownload(`/api/download?url=${replaceImg(task.video_url)}`);
const blob = new Blob([res.data]);
const link = document.createElement("a");
link.href = URL.createObjectURL(blob);
link.download = `video_${task.id}.mp4`;
link.click();
URL.revokeObjectURL(link.href);
const res = await httpDownload(`/api/download?url=${replaceImg(task.video_url)}`)
const blob = new Blob([res.data])
const link = document.createElement('a')
link.href = URL.createObjectURL(blob)
link.download = `video_${task.id}.mp4`
link.click()
URL.revokeObjectURL(link.href)
} catch (e) {
showMessageError("下载失败: " + e.message);
showMessageError('下载失败: ' + e.message)
}
};
}
// 删除任务
const removeJob = (item) => {
ElMessageBox.confirm("此操作将会删除任务相关文件,继续操作码?", "删除提示", {
confirmButtonText: "确认",
cancelButtonText: "取消",
type: "warning",
ElMessageBox.confirm('此操作将会删除任务相关文件,继续操作码?', '删除提示', {
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
httpGet("/api/video/remove", { id: item.id })
httpGet('/api/video/remove', { id: item.id })
.then(() => {
ElMessage.success("任务删除成功");
fetchData(page.value);
ElMessage.success('任务删除成功')
fetchData(page.value)
})
.catch((e) => {
ElMessage.error("任务删除失败:" + e.message);
});
ElMessage.error('任务删除失败:' + e.message)
})
})
.catch(() => {});
};
.catch(() => {})
}
const clipboard = ref(null);
const clipboard = ref(null)
// 生命周期钩子
onMounted(() => {
checkSession()
.then((u) => {
isLogin.value = true;
availablePower.value = u.power;
fetchData(1);
isLogin.value = true
availablePower.value = u.power
fetchData(1)
// 设置轮询
pullHandler.value = setInterval(() => {
if (taskPulling.value) {
fetchData(page.value);
fetchData(page.value)
}
}, 5000);
}, 5000)
})
.catch((e) => {
console.log(e);
});
console.log(e)
})
clipboard.value = new Clipboard(".copy-prompt");
clipboard.value.on("success", () => {
ElMessage.success("复制成功!");
});
clipboard.value.on("error", () => {
ElMessage.error("复制失败!");
});
clipboard.value = new Clipboard('.copy-prompt')
clipboard.value.on('success', () => {
ElMessage.success('复制成功!')
})
clipboard.value.on('error', () => {
ElMessage.error('复制失败!')
})
getSystemInfo().then((res) => {
keLingPowers.value = res.data.keling_powers;
updateModelPower();
});
});
keLingPowers.value = res.data.keling_powers
updateModelPower()
})
})
onUnmounted(() => {
clipboard.value.destroy();
clipboard.value.destroy()
if (pullHandler.value) {
clearInterval(pullHandler.value);
clearInterval(pullHandler.value)
}
});
})
</script>
<style lang="stylus" scoped>
@import "@/assets/css/keling.styl"
@import '../assets/css/keling.styl'
</style>

View File

@@ -145,7 +145,6 @@ onMounted(() => {
}
})
.catch((e) => {
console.warn(e)
agreementContent.value =
'用户在使用本服务前应当阅读并同意本协议。本协议内容包括协议正文及所有本平台已经发布的或将来可能发布的各类规则。所有规则为本协议不可分割的组成部分,与协议正文具有同等法律效力。'
})
@@ -161,7 +160,6 @@ onMounted(() => {
}
})
.catch((e) => {
console.warn(e)
privacyContent.value =
'我们非常重视用户的隐私和个人信息保护。您在使用我们的产品与服务时,我们可能会收集和使用您的相关信息。我们希望通过本《隐私政策》向您说明我们在收集和使用您相关信息时对应的处理规则。'
})
@@ -278,7 +276,7 @@ const openPrivacy = () => {
</script>
<style lang="stylus" scoped>
@import "@/assets/css/login.styl"
@import '../assets/css/login.styl'
.agreement-box
margin-bottom: 10px

View File

@@ -383,5 +383,5 @@ const generatePrompt = () => {
</script>
<style lang="stylus" scoped>
@import "@/assets/css/luma.styl"
@import "../assets/css/luma.styl"
</style>

View File

@@ -109,15 +109,15 @@
</template>
<script setup>
import { nextTick, onMounted, ref } from 'vue'
import { Markmap } from 'markmap-view'
import { Transformer } from 'markmap-lib'
import { checkSession, getSystemInfo } from '@/store/cache'
import { httpGet, httpPost } from '@/utils/http'
import { ElMessage } from 'element-plus'
import { Download } from '@element-plus/icons-vue'
import { Toolbar } from 'markmap-toolbar'
import { useSharedStore } from '@/store/sharedata'
import { httpGet, httpPost } from '@/utils/http'
import { Download } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import { Transformer } from 'markmap-lib'
import { Toolbar } from 'markmap-toolbar'
import { Markmap } from 'markmap-view'
import { nextTick, onMounted, ref } from 'vue'
const leftBoxHeight = ref(window.innerHeight - 105)
//const rightBoxHeight = ref(window.innerHeight - 115);
@@ -263,9 +263,9 @@ const getModelById = (modelId) => {
}
// download SVG to png file
const downloadImage = async() => {
const downloadImage = async () => {
// 先自适应思维导图到可视化区域
await markMap.value.fit()
await markMap.value.fit()
const svgElement = document.getElementById('markmap')
const serializer = new XMLSerializer()
@@ -275,7 +275,7 @@ const downloadImage = async() => {
image.src = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(source)
// 分辨率倍数,越高图片越清晰,但文件越大
const scale = 4
const scale = 4
const canvas = document.createElement('canvas')
canvas.width = svgElement.offsetWidth * scale
canvas.height = svgElement.offsetHeight * scale
@@ -298,6 +298,6 @@ const downloadImage = async() => {
</script>
<style lang="stylus">
@import "@/assets/css/mark-map.styl"
@import "@/assets/css/custom-scroll.styl"
@import '../assets/css/mark-map.styl'
@import '../assets/css/custom-scroll.styl'
</style>

View File

@@ -292,7 +292,7 @@ const payCallback = (success) => {
}
</script>
<style lang="stylus">
@import "@/assets/css/custom-scroll.styl"
@import "@/assets/css/member.styl"
<style lang="stylus" scoped>
@import "../assets/css/custom-scroll.styl"
@import "../assets/css/member.styl"
</style>

View File

@@ -4,7 +4,12 @@
<div class="inner">
<div class="list-box">
<div class="handle-box">
<el-input v-model="query.model" placeholder="模型" class="handle-input mr10" clearable></el-input>
<el-input
v-model="query.model"
placeholder="模型"
class="handle-input mr10"
clearable
></el-input>
<el-date-picker
v-model="query.date"
type="daterange"
@@ -23,21 +28,27 @@
<el-table-column prop="model" label="模型" width="130px" />
<el-table-column prop="type" label="类型">
<template #default="scope">
<el-tag size="small" :type="tagColors[scope.row.type]">{{ scope.row.type_str }}</el-tag>
<el-tag size="small" :type="tagColors[scope.row.type]">{{
scope.row.type_str
}}</el-tag>
</template>
</el-table-column>
<el-table-column label="数额">
<template #default="scope">
<div>
<el-text type="success" v-if="scope.row.mark === 1">+{{ scope.row.amount }}</el-text>
<el-text type="danger" v-if="scope.row.mark === 0">-{{ scope.row.amount }}</el-text>
<el-text type="success" v-if="scope.row.mark === 1"
>+{{ scope.row.amount }}</el-text
>
<el-text type="danger" v-if="scope.row.mark === 0"
>-{{ scope.row.amount }}</el-text
>
</div>
</template>
</el-table-column>
<el-table-column prop="balance" label="余额" />
<el-table-column label="发生时间" width="160px">
<template #default="scope">
<span>{{ dateFormat(scope.row["created_at"]) }}</span>
<span>{{ dateFormat(scope.row['created_at']) }}</span>
</template>
</el-table-column>
<el-table-column prop="remark" label="备注" />
@@ -64,48 +75,48 @@
</template>
<script setup>
import nodata from "@/assets/img/no-data.png";
import nodata from '@/assets/img/no-data.png'
import { onMounted, ref } from "vue";
import { dateFormat } from "@/utils/libs";
import { Search } from "@element-plus/icons-vue";
import Clipboard from "clipboard";
import { ElMessage } from "element-plus";
import { httpPost } from "@/utils/http";
import { checkSession } from "@/store/cache";
import { checkSession } from '@/store/cache'
import { httpPost } from '@/utils/http'
import { dateFormat } from '@/utils/libs'
import { Search } from '@element-plus/icons-vue'
import Clipboard from 'clipboard'
import { ElMessage } from 'element-plus'
import { onMounted, ref } from 'vue'
const items = ref([]);
const total = ref(0);
const page = ref(1);
const pageSize = ref(20);
const loading = ref(false);
const listBoxHeight = window.innerHeight - 87;
const items = ref([])
const total = ref(0)
const page = ref(1)
const pageSize = ref(20)
const loading = ref(false)
const listBoxHeight = window.innerHeight - 87
const query = ref({
model: "",
model: '',
date: [],
});
const tagColors = ref(["primary", "success", "primary", "danger", "info", "warning"]);
})
const tagColors = ref(['primary', 'success', 'primary', 'danger', 'info', 'warning'])
onMounted(() => {
checkSession()
.then(() => {
fetchData();
fetchData()
})
.catch(() => {});
const clipboard = new Clipboard(".copy-order-no");
clipboard.on("success", () => {
ElMessage.success("复制成功");
});
.catch(() => {})
const clipboard = new Clipboard('.copy-order-no')
clipboard.on('success', () => {
ElMessage.success('复制成功')
})
clipboard.on("error", () => {
ElMessage.error("复制失败");
});
});
clipboard.on('error', () => {
ElMessage.error('复制失败')
})
})
// 获取数据
const fetchData = () => {
loading.value = true;
httpPost("/api/powerLog/list", {
loading.value = true
httpPost('/api/powerLog/list', {
model: query.value.model,
date: query.value.date,
page: page.value,
@@ -113,22 +124,22 @@ const fetchData = () => {
})
.then((res) => {
if (res.data) {
items.value = res.data.items;
total.value = res.data.total;
page.value = res.data.page;
pageSize.value = res.data.page_size;
items.value = res.data.items
total.value = res.data.total
page.value = res.data.page
pageSize.value = res.data.page_size
}
loading.value = false;
loading.value = false
})
.catch((e) => {
loading.value = false;
ElMessage.error("获取数据失败" + e.message);
});
};
loading.value = false
ElMessage.error('获取数据失败' + e.message)
})
}
</script>
<style lang="stylus" scoped>
@import "@/assets/css/custom-scroll.styl"
@import "../assets/css/custom-scroll.styl"
.power-log {
color #ffffff
.inner {

View File

@@ -10,12 +10,27 @@
<el-tab-pane label="手机注册" name="mobile" v-if="enableMobile">
<el-form-item>
<div class="form-title">手机号码</div>
<el-input placeholder="请输入手机号码" size="large" v-model="data.mobile" maxlength="11" autocomplete="off"> </el-input>
<el-input
placeholder="请输入手机号码"
size="large"
v-model="data.mobile"
maxlength="11"
autocomplete="off"
>
</el-input>
</el-form-item>
<el-form-item>
<div class="form-title">验证码</div>
<div class="flex w100">
<el-input placeholder="请输入验证码" size="large" maxlength="30" class="code-input" v-model="data.code" autocomplete="off"> </el-input>
<el-input
placeholder="请输入验证码"
size="large"
maxlength="30"
class="code-input"
v-model="data.code"
autocomplete="off"
>
</el-input>
<send-msg size="large" :receiver="data.mobile" type="mobile" />
</div>
@@ -24,12 +39,26 @@
<el-tab-pane label="邮箱注册" name="email" v-if="enableEmail">
<el-form-item class="block">
<div class="form-title">邮箱</div>
<el-input placeholder="请输入邮箱地址" size="large" v-model="data.email" autocomplete="off"> </el-input>
<el-input
placeholder="请输入邮箱地址"
size="large"
v-model="data.email"
autocomplete="off"
>
</el-input>
</el-form-item>
<el-form-item class="block">
<div class="form-title">验证码</div>
<div class="flex w100">
<el-input placeholder="请输入验证码" size="large" maxlength="30" class="code-input" v-model="data.code" autocomplete="off"> </el-input>
<el-input
placeholder="请输入验证码"
size="large"
maxlength="30"
class="code-input"
v-model="data.code"
autocomplete="off"
>
</el-input>
<send-msg size="large" :receiver="data.email" type="email" />
</div>
@@ -39,7 +68,13 @@
<el-form-item class="block">
<div class="form-title">用户名</div>
<el-input placeholder="请输入用户名" size="large" v-model="data.username" autocomplete="off"> </el-input>
<el-input
placeholder="请输入用户名"
size="large"
v-model="data.username"
autocomplete="off"
>
</el-input>
</el-form-item>
</el-tab-pane>
</el-tabs>
@@ -47,35 +82,67 @@
<el-form-item class="block">
<div class="form-title">密码</div>
<el-input placeholder="请输入密码(8-16位)" maxlength="16" size="large" v-model="data.password" show-password autocomplete="off"> </el-input>
<el-input
placeholder="请输入密码(8-16位)"
maxlength="16"
size="large"
v-model="data.password"
show-password
autocomplete="off"
>
</el-input>
</el-form-item>
<el-form-item class="block">
<div class="form-title">重复密码</div>
<el-input placeholder="请再次输入密码(8-16位)" size="large" maxlength="16" v-model="data.repass" show-password autocomplete="off"> </el-input>
<el-input
placeholder="请再次输入密码(8-16位)"
size="large"
maxlength="16"
v-model="data.repass"
show-password
autocomplete="off"
>
</el-input>
</el-form-item>
<el-form-item class="block">
<div class="form-title">邀请码</div>
<el-input placeholder="请输入邀请码(可选)" size="large" v-model="data.invite_code" autocomplete="off"> </el-input>
<el-input
placeholder="请输入邀请码(可选)"
size="large"
v-model="data.invite_code"
autocomplete="off"
>
</el-input>
</el-form-item>
<el-form-item label="" prop="agreement" :class="{'agreement-error': agreementError}">
<div class="agreement-box" :class="{'shake': isShaking}">
<el-form-item
label=""
prop="agreement"
:class="{ 'agreement-error': agreementError }"
>
<div class="agreement-box" :class="{ shake: isShaking }">
<el-checkbox v-model="data.agreement" @change="handleAgreementChange">
我已阅读并同意
<span class="agreement-link" @click.stop.prevent="openAgreement">用户协议</span>
<span class="agreement-link" @click.stop.prevent="openAgreement"
>用户协议</span
>
<span class="agreement-link" @click.stop.prevent="openPrivacy">隐私政策</span>
<span class="agreement-link" @click.stop.prevent="openPrivacy"
>隐私政策</span
>
</el-checkbox>
</div>
</el-form-item>
<el-row class="btn-row" :gutter="20">
<el-col :span="24">
<el-button class="login-btn" type="primary" size="large" @click="submitRegister">注册</el-button>
<el-button class="login-btn" type="primary" size="large" @click="submitRegister"
>注册</el-button
>
</el-col>
</el-row>
</el-form>
@@ -91,7 +158,9 @@
</div>
<div class="mt-3">
<el-button type="primary" @click="router.push('/')"><i class="iconfont icon-home mr-1"></i> 返回首页</el-button>
<el-button type="primary" @click="router.push('/')"
><i class="iconfont icon-home mr-1"></i> 返回首页</el-button
>
</div>
</template>
</el-result>
@@ -103,198 +172,202 @@
</template>
<script setup>
import { ref } from "vue";
import AccountTop from "@/components/AccountTop.vue";
import AccountBg from "@/components/AccountBg.vue";
import AccountBg from '@/components/AccountBg.vue'
import AccountTop from '@/components/AccountTop.vue'
import { ref } from 'vue'
import { httpGet, httpPost } from "@/utils/http";
import { ElMessage, ElMessageBox } from "element-plus";
import { useRouter } from "vue-router";
import MarkdownIt from "markdown-it";
import { httpGet, httpPost } from '@/utils/http'
import { ElMessage, ElMessageBox } from 'element-plus'
import MarkdownIt from 'markdown-it'
import { useRouter } from 'vue-router'
import SendMsg from "@/components/SendMsg.vue";
import { arrayContains, isMobile } from "@/utils/libs";
import { setUserToken } from "@/store/session";
import { validateEmail, validateMobile } from "@/utils/validate";
import { showMessageError, showMessageOK } from "@/utils/dialog";
import { getLicenseInfo, getSystemInfo } from "@/store/cache";
import Captcha from "@/components/Captcha.vue";
import Captcha from '@/components/Captcha.vue'
import SendMsg from '@/components/SendMsg.vue'
import { getLicenseInfo, getSystemInfo } from '@/store/cache'
import { setUserToken } from '@/store/session'
import { showMessageError, showMessageOK } from '@/utils/dialog'
import { arrayContains, isMobile } from '@/utils/libs'
import { validateEmail, validateMobile } from '@/utils/validate'
const router = useRouter();
const title = ref("");
const logo = ref("");
const router = useRouter()
const title = ref('')
const logo = ref('')
const data = ref({
username: "",
mobile: "",
email: "",
password: "",
code: "",
repass: "",
invite_code: router.currentRoute.value.query["invite_code"],
username: '',
mobile: '',
email: '',
password: '',
code: '',
repass: '',
invite_code: router.currentRoute.value.query['invite_code'],
agreement: false,
});
})
const enableMobile = ref(false);
const enableEmail = ref(false);
const enableUser = ref(false);
const enableRegister = ref(true);
const activeName = ref("mobile");
const wxImg = ref("/images/wx.png");
const licenseConfig = ref({});
const enableVerify = ref(false);
const captchaRef = ref(null);
const agreementError = ref(false);
const isShaking = ref(false);
const enableMobile = ref(false)
const enableEmail = ref(false)
const enableUser = ref(false)
const enableRegister = ref(true)
const activeName = ref('mobile')
const wxImg = ref('/images/wx.png')
const licenseConfig = ref({})
const enableVerify = ref(false)
const captchaRef = ref(null)
const agreementError = ref(false)
const isShaking = ref(false)
// 初始化markdown解析器
const md = new MarkdownIt({
html: true,
linkify: true,
typographer: true
});
typographer: true,
})
const agreementContent = ref('');
const privacyContent = ref('');
const agreementContent = ref('')
const privacyContent = ref('')
// 记录邀请码点击次数
if (data.value.invite_code) {
httpGet("/api/invite/hits", { code: data.value.invite_code });
httpGet('/api/invite/hits', { code: data.value.invite_code })
}
getSystemInfo()
.then((res) => {
if (res.data) {
title.value = res.data.title;
logo.value = res.data.logo;
const registerWays = res.data["register_ways"];
title.value = res.data.title
logo.value = res.data.logo
const registerWays = res.data['register_ways']
if (arrayContains(registerWays, "username")) {
enableUser.value = true;
activeName.value = "username";
if (arrayContains(registerWays, 'username')) {
enableUser.value = true
activeName.value = 'username'
}
if (arrayContains(registerWays, "email")) {
enableEmail.value = true;
activeName.value = "email";
if (arrayContains(registerWays, 'email')) {
enableEmail.value = true
activeName.value = 'email'
}
if (arrayContains(registerWays, "mobile")) {
enableMobile.value = true;
activeName.value = "mobile";
if (arrayContains(registerWays, 'mobile')) {
enableMobile.value = true
activeName.value = 'mobile'
}
// 是否启用注册
enableRegister.value = res.data["enabled_register"];
enableRegister.value = res.data['enabled_register']
// 使用后台上传的客服微信二维码
if (res.data["wechat_card_url"] !== "") {
wxImg.value = res.data["wechat_card_url"];
if (res.data['wechat_card_url'] !== '') {
wxImg.value = res.data['wechat_card_url']
}
enableVerify.value = res.data["enabled_verify"];
enableVerify.value = res.data['enabled_verify']
}
})
.catch((e) => {
ElMessage.error("获取系统配置失败:" + e.message);
});
ElMessage.error('获取系统配置失败:' + e.message)
})
// 获取用户协议
httpGet('/api/config/get?key=agreement')
.then((res) => {
if (res.data && res.data.content) {
agreementContent.value = res.data.content;
agreementContent.value = res.data.content
} else {
agreementContent.value = '# 用户协议\n\n用户在使用本服务前应当阅读并同意本协议。本协议内容包括协议正文及所有本平台已经发布的或将来可能发布的各类规则。所有规则为本协议不可分割的组成部分与协议正文具有同等法律效力。';
agreementContent.value =
'# 用户协议\n\n用户在使用本服务前应当阅读并同意本协议。本协议内容包括协议正文及所有本平台已经发布的或将来可能发布的各类规则。所有规则为本协议不可分割的组成部分与协议正文具有同等法律效力。'
}
})
.catch((e) => {
console.warn(e);
agreementContent.value = '# 用户协议\n\n用户在使用本服务前应当阅读并同意本协议。本协议内容包括协议正文及所有本平台已经发布的或将来可能发布的各类规则。所有规则为本协议不可分割的组成部分与协议正文具有同等法律效力。';
});
console.warn(e)
agreementContent.value =
'# 用户协议\n\n用户在使用本服务前应当阅读并同意本协议。本协议内容包括协议正文及所有本平台已经发布的或将来可能发布的各类规则。所有规则为本协议不可分割的组成部分与协议正文具有同等法律效力。'
})
// 获取隐私政策
httpGet('/api/config/get?key=privacy')
.then((res) => {
if (res.data && res.data.content) {
privacyContent.value = res.data.content;
privacyContent.value = res.data.content
} else {
privacyContent.value = '# 隐私政策\n\n我们非常重视用户的隐私和个人信息保护。您在使用我们的产品与服务时我们可能会收集和使用您的相关信息。我们希望通过本《隐私政策》向您说明我们在收集和使用您相关信息时对应的处理规则。';
privacyContent.value =
'# 隐私政策\n\n我们非常重视用户的隐私和个人信息保护。您在使用我们的产品与服务时我们可能会收集和使用您的相关信息。我们希望通过本《隐私政策》向您说明我们在收集和使用您相关信息时对应的处理规则。'
}
})
.catch((e) => {
console.warn(e);
privacyContent.value = '# 隐私政策\n\n我们非常重视用户的隐私和个人信息保护。您在使用我们的产品与服务时我们可能会收集和使用您的相关信息。我们希望通过本《隐私政策》向您说明我们在收集和使用您相关信息时对应的处理规则。';
});
console.warn(e)
privacyContent.value =
'# 隐私政策\n\n我们非常重视用户的隐私和个人信息保护。您在使用我们的产品与服务时我们可能会收集和使用您的相关信息。我们希望通过本《隐私政策》向您说明我们在收集和使用您相关信息时对应的处理规则。'
})
getLicenseInfo()
.then((res) => {
licenseConfig.value = res.data;
licenseConfig.value = res.data
})
.catch((e) => {
showMessageError("获取 License 配置:" + e.message);
});
showMessageError('获取 License 配置:' + e.message)
})
// 注册操作
const submitRegister = () => {
if (activeName.value === "username" && data.value.username === "") {
return showMessageError("请输入用户名");
if (activeName.value === 'username' && data.value.username === '') {
return showMessageError('请输入用户名')
}
if (activeName.value === "mobile" && !validateMobile(data.value.mobile)) {
return showMessageError("请输入合法的手机号");
if (activeName.value === 'mobile' && !validateMobile(data.value.mobile)) {
return showMessageError('请输入合法的手机号')
}
if (activeName.value === "email" && !validateEmail(data.value.email)) {
return showMessageError("请输入合法的邮箱地址");
if (activeName.value === 'email' && !validateEmail(data.value.email)) {
return showMessageError('请输入合法的邮箱地址')
}
if (data.value.password.length < 8) {
return showMessageError("密码的长度为8-16个字符");
return showMessageError('密码的长度为8-16个字符')
}
if (data.value.repass !== data.value.password) {
return showMessageError("两次输入密码不一致");
return showMessageError('两次输入密码不一致')
}
if ((activeName.value === "mobile" || activeName.value === "email") && data.value.code === "") {
return showMessageError("请输入验证码");
if ((activeName.value === 'mobile' || activeName.value === 'email') && data.value.code === '') {
return showMessageError('请输入验证码')
}
if (!data.value.agreement) {
agreementError.value = true;
isShaking.value = true;
agreementError.value = true
isShaking.value = true
setTimeout(() => {
isShaking.value = false;
}, 500);
showMessageError("请先阅读并同意用户协议和隐私政策");
return;
isShaking.value = false
}, 500)
showMessageError('请先阅读并同意用户协议和隐私政策')
return
}
// 如果是用户名和密码登录,那么需要加载验证码
if (enableVerify.value && activeName.value === "username") {
captchaRef.value.loadCaptcha();
if (enableVerify.value && activeName.value === 'username') {
captchaRef.value.loadCaptcha()
} else {
doSubmitRegister({});
doSubmitRegister({})
}
};
}
const doSubmitRegister = (verifyData) => {
data.value.key = verifyData.key;
data.value.dots = verifyData.dots;
data.value.x = verifyData.x;
data.value.reg_way = activeName.value;
httpPost("/api/user/register", data.value)
data.value.key = verifyData.key
data.value.dots = verifyData.dots
data.value.x = verifyData.x
data.value.reg_way = activeName.value
httpPost('/api/user/register', data.value)
.then((res) => {
setUserToken(res.data.token);
showMessageOK("注册成功,即将跳转到对话主界面...");
setUserToken(res.data.token)
showMessageOK('注册成功,即将跳转到对话主界面...')
if (isMobile()) {
router.push("/mobile/index");
router.push('/mobile/index')
} else {
router.push("/chat");
router.push('/chat')
}
})
.catch((e) => {
showMessageError("注册失败," + e.message);
});
};
showMessageError('注册失败,' + e.message)
})
}
const handleAgreementChange = () => {
agreementError.value = !data.value.agreement;
};
agreementError.value = !data.value.agreement
}
const openAgreement = () => {
// 使用弹窗显示用户协议内容支持Markdown格式
@@ -304,10 +377,10 @@ const openAgreement = () => {
{
confirmButtonText: '我已阅读',
dangerouslyUseHTMLString: true,
callback: () => {}
callback: () => {},
}
);
};
)
}
const openPrivacy = () => {
// 使用弹窗显示隐私政策内容支持Markdown格式
@@ -317,14 +390,14 @@ const openPrivacy = () => {
{
confirmButtonText: '我已阅读',
dangerouslyUseHTMLString: true,
callback: () => {}
callback: () => {},
}
);
};
)
}
</script>
<style lang="stylus" scoped>
@import "@/assets/css/login.styl"
@import '../assets/css/login.styl'
:deep(.back){
margin-bottom: 10px;
}
@@ -350,7 +423,7 @@ const openPrivacy = () => {
.el-checkbox__input
.el-checkbox__inner
border-color: #F56C6C !important
.shake
animation: shake 0.5s cubic-bezier(.36,.07,.19,.97) both
@@ -389,7 +462,8 @@ const openPrivacy = () => {
line-height: 1.5;
}
.markdown-content ul, .markdown-content ol {
.markdown-content ul,
.markdown-content ol {
padding-left: 20px;
margin-bottom: 10px;
}

View File

@@ -10,11 +10,7 @@
<el-tab-pane label="手机号验证" name="mobile">
<el-form-item>
<div class="form-title">手机号码</div>
<el-input
v-model="form.mobile"
size="large"
placeholder="请输入手机号"
/>
<el-input v-model="form.mobile" size="large" placeholder="请输入手机号" />
</el-form-item>
<el-form-item>
<div class="form-title">验证码</div>
@@ -26,11 +22,7 @@
placeholder="请输入验证码"
class="code-input"
/>
<send-msg
size="large"
:receiver="form.mobile"
type="mobile"
/>
<send-msg size="large" :receiver="form.mobile" type="mobile" />
</div>
</el-form-item>
</el-tab-pane>
@@ -38,11 +30,7 @@
<el-form-item>
<div class="form-title">邮箱</div>
<el-input
v-model="form.email"
placeholder="请输入邮箱"
size="large"
/>
<el-input v-model="form.email" placeholder="请输入邮箱" size="large" />
</el-form-item>
<el-form-item>
<div class="form-title">验证码</div>
@@ -54,11 +42,7 @@
placeholder="请输入验证码"
class="code-input"
/>
<send-msg
size="large"
:receiver="form.email"
type="email"
/>
<send-msg size="large" :receiver="form.email" type="email" />
</div>
</el-form-item>
</el-tab-pane>
@@ -85,12 +69,7 @@
/>
</el-form-item>
<el-form-item>
<el-button
class="login-btn"
size="large"
type="primary"
@click="save"
>
<el-button class="login-btn" size="large" type="primary" @click="save">
重置密码
</el-button>
</el-form-item>
@@ -103,50 +82,49 @@
</template>
<script setup>
import { ref } from "vue";
import SendMsg from "@/components/SendMsg.vue";
import AccountTop from "@/components/AccountTop.vue";
import AccountTop from '@/components/AccountTop.vue'
import SendMsg from '@/components/SendMsg.vue'
import { ref } from 'vue'
import { ElMessage } from "element-plus";
import { httpPost } from "@/utils/http";
import AccountBg from "@/components/AccountBg.vue";
import { validateEmail, validateMobile } from "@/utils/validate";
import AccountBg from '@/components/AccountBg.vue'
import { httpPost } from '@/utils/http'
import { ElMessage } from 'element-plus'
const form = ref({
mobile: "",
email: "",
type: "mobile",
code: "",
password: "",
repass: ""
});
mobile: '',
email: '',
type: 'mobile',
code: '',
password: '',
repass: '',
})
const save = () => {
if (form.value.code === "") {
return ElMessage.error("请输入验证码");
if (form.value.code === '') {
return ElMessage.error('请输入验证码')
}
if (form.value.password.length < 8) {
return ElMessage.error("密码长度必须大于8位");
return ElMessage.error('密码长度必须大于8位')
}
if (form.value.repass !== form.value.password) {
return ElMessage.error("两次输入密码不一致");
return ElMessage.error('两次输入密码不一致')
}
httpPost("/api/user/resetPass", form.value)
httpPost('/api/user/resetPass', form.value)
.then(() => {
ElMessage.success({
message: "重置密码成功",
duration: 1000
});
message: '重置密码成功',
duration: 1000,
})
})
.catch((e) => {
ElMessage.error("重置密码失败:" + e.message);
});
};
ElMessage.error('重置密码失败:' + e.message)
})
}
</script>
<style lang="stylus">
@import "@/assets/css/login.styl"
<style lang="stylus" scoped>
@import "../assets/css/login.styl"
::v-deep(.el-tabs__item.is-active, .el-tabs__item:hover){
color: var(--common-text-color) !important;
}

View File

@@ -11,7 +11,9 @@
<el-avatar :size="32" :src="song.user?.avatar" />
</span>
<span class="nickname">{{ song.user?.nickname }}</span>
<button class="btn btn-icon" @click="play"><i class="iconfont icon-play"></i> {{ song.play_times }}</button>
<button class="btn btn-icon" @click="play">
<i class="iconfont icon-play"></i> {{ song.play_times }}
</button>
<el-tooltip content="复制歌曲链接" placement="top">
<button class="btn btn-icon copy-link" :data-clipboard-text="getShareURL(song)">
@@ -37,58 +39,58 @@
</template>
<script setup>
import { onMounted, onUnmounted, ref } from "vue";
import { useRouter } from "vue-router";
import { httpGet } from "@/utils/http";
import { showMessageError } from "@/utils/dialog";
import { dateFormat } from "@/utils/libs";
import Clipboard from "clipboard";
import { ElMessage } from "element-plus";
import MusicPlayer from "@/components/MusicPlayer.vue";
import MusicPlayer from '@/components/MusicPlayer.vue'
import { showMessageError } from '@/utils/dialog'
import { httpGet } from '@/utils/http'
import { dateFormat } from '@/utils/libs'
import Clipboard from 'clipboard'
import { ElMessage } from 'element-plus'
import { onMounted, onUnmounted, ref } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter();
const id = router.currentRoute.value.params.id;
const song = ref({ title: "" });
const playList = ref([]);
const playerRef = ref(null);
const router = useRouter()
const id = router.currentRoute.value.params.id
const song = ref({ title: '' })
const playList = ref([])
const playerRef = ref(null)
httpGet("/api/suno/detail", { song_id: id })
httpGet('/api/suno/detail', { song_id: id })
.then((res) => {
song.value = res.data;
playList.value = [song.value];
document.title = song.value?.title + " | By " + song.value?.user.nickname + " | Suno音乐";
song.value = res.data
playList.value = [song.value]
document.title = song.value?.title + ' | By ' + song.value?.user.nickname + ' | Suno音乐'
})
.catch((e) => {
showMessageError("获取歌曲详情失败:" + e.message);
});
showMessageError('获取歌曲详情失败:' + e.message)
})
const clipboard = ref(null);
const clipboard = ref(null)
onMounted(() => {
clipboard.value = new Clipboard(".copy-link");
clipboard.value.on("success", () => {
ElMessage.success("复制歌曲链接成功!");
});
clipboard.value = new Clipboard('.copy-link')
clipboard.value.on('success', () => {
ElMessage.success('复制歌曲链接成功!')
})
clipboard.value.on("error", () => {
ElMessage.error("复制失败!");
});
});
clipboard.value.on('error', () => {
ElMessage.error('复制失败!')
})
})
onUnmounted(() => {
clipboard.value.destroy();
});
clipboard.value.destroy()
})
// 播放歌曲
const play = () => {
playerRef.value.play();
};
playerRef.value.play()
}
const winHeight = ref(window.innerHeight - 50);
const winHeight = ref(window.innerHeight - 50)
const getShareURL = (item) => {
return `${location.protocol}//${location.host}/song/${item.id}`;
};
return `${location.protocol}//${location.host}/song/${item.id}`
}
</script>
<style lang="stylus" scoped>
@import "@/assets/css/song.styl"
@import '../assets/css/song.styl'
</style>

View File

@@ -5,27 +5,48 @@
<el-tooltip content="定义模式" placement="top">
<black-switch v-model:value="custom" size="large" />
</el-tooltip>
<el-tooltip content="请上传6-60秒的原始音频检测到人声的音频将仅设为私人音频。" placement="bottom-end">
<el-upload class="avatar-uploader" :auto-upload="true" :show-file-list="false" :http-request="uploadAudio" accept=".wav,.mp3">
<el-tooltip
content="请上传6-60秒的原始音频检测到人声的音频将仅设为私人音频。"
placement="bottom-end"
>
<el-upload
class="avatar-uploader"
:auto-upload="true"
:show-file-list="false"
:http-request="uploadAudio"
accept=".wav,.mp3"
>
<el-button class="upload-music" round type="primary">
<i class="iconfont icon-upload"></i>
<span>上传音乐</span>
</el-button>
</el-upload>
</el-tooltip>
<black-select v-model:value="data.model" :options="models" placeholder="请选择模型" style="width: 100px" />
<black-select
v-model:value="data.model"
:options="models"
placeholder="请选择模型"
style="width: 100px"
/>
</div>
<div class="params">
<div class="pure-music">
<span class="switch"><black-switch v-model:value="data.instrumental" size="default" /></span>
<span class="switch"
><black-switch v-model:value="data.instrumental" size="default"
/></span>
<span class="text">纯音乐</span>
</div>
<div v-if="custom">
<div class="item-group" v-if="!data.instrumental">
<div class="label">
<span class="text">歌词</span>
<el-popover placement="right" :width="200" trigger="hover" content="自己写歌词或寻求 AI 的帮助。使用两节歌词8 行)可获得最佳效果。">
<el-popover
placement="right"
:width="200"
trigger="hover"
content="自己写歌词或寻求 AI 的帮助。使用两节歌词8 行)可获得最佳效果。"
>
<template #reference>
<el-icon>
<InfoFilled />
@@ -34,7 +55,12 @@
</el-popover>
</div>
<div class="item" v-loading="isGenerating" element-loading-text="正在生成歌词...">
<black-input v-model:value="data.lyrics" type="textarea" :rows="10" :placeholder="promptPlaceholder" />
<black-input
v-model:value="data.lyrics"
type="textarea"
:rows="10"
:placeholder="promptPlaceholder"
/>
<button class="btn btn-lyric" @click="createLyric">生成歌词</button>
</div>
</div>
@@ -46,7 +72,7 @@
placement="right"
:width="200"
trigger="hover"
content="描述您想要的音乐风格(例如"原声流行音乐"Sunos 模特无法识别艺术家的名字但能够理解音乐流派和氛围"
content="描述您想要的音乐风格(例如原声流行音乐)。Sunos 模特无法识别艺术家的名字但能够理解音乐流派和氛围"
>
<template #reference>
<el-icon>
@@ -56,12 +82,20 @@
</el-popover>
</div>
<div class="item">
<black-input v-model:value="data.tags" type="textarea" :maxlength="120" :rows="3" placeholder="请输入音乐风格,多个风格之间用英文逗号隔开..." />
<black-input
v-model:value="data.tags"
type="textarea"
:maxlength="120"
:rows="3"
placeholder="请输入音乐风格,多个风格之间用英文逗号隔开..."
/>
</div>
<div class="tag-select">
<div class="inner">
<span class="tag" @click="selectTag(tag)" v-for="tag in tags" :key="tag.value">{{ tag.label }}</span>
<span class="tag" @click="selectTag(tag)" v-for="tag in tags" :key="tag.value">{{
tag.label
}}</span>
</div>
</div>
</div>
@@ -69,7 +103,12 @@
<div class="item-group">
<div class="label">
<span class="text">歌曲名称</span>
<el-popover placement="right" :width="200" trigger="hover" content="给你的歌曲起一个标题,以便于分享、发现和组织。">
<el-popover
placement="right"
:width="200"
trigger="hover"
content="给你的歌曲起一个标题,以便于分享、发现和组织。"
>
<template #reference>
<el-icon>
<InfoFilled />
@@ -78,7 +117,12 @@
</el-popover>
</div>
<div class="item">
<black-input v-model:value="data.title" type="textarea" :rows="1" placeholder="请输入歌曲名称..." />
<black-input
v-model:value="data.title"
type="textarea"
:rows="1"
placeholder="请输入歌曲名称..."
/>
</div>
</div>
</div>
@@ -100,14 +144,24 @@
</el-popover>
</div>
<div class="item">
<black-input v-model:value="data.prompt" type="textarea" :rows="10" placeholder="例如:一首关于爱情的摇滚歌曲..." />
<black-input
v-model:value="data.prompt"
type="textarea"
:rows="10"
placeholder="例如:一首关于爱情的摇滚歌曲..."
/>
</div>
</div>
<div class="ref-song" v-if="refSong">
<div class="label">
<span class="text">续写</span>
<el-popover placement="right" :width="200" trigger="hover" content="输入额外的歌词,根据您之前的歌词来扩展歌曲。">
<el-popover
placement="right"
:width="200"
trigger="hover"
content="输入额外的歌词,根据您之前的歌词来扩展歌曲。"
>
<template #reference>
<el-icon>
<InfoFilled />
@@ -122,7 +176,9 @@
<span class="title">{{ refSong.title }}</span>
<el-button type="info" @click="removeRefSong" size="small" :icon="Delete" circle />
</div>
<div class="extend-secs"> <input v-model="refSong.extend_secs" type="text" /> 秒开始续写</div>
<div class="extend-secs">
<input v-model="refSong.extend_secs" type="text" /> 秒开始续写
</div>
</div>
</div>
@@ -134,7 +190,11 @@
</div>
</div>
</div>
<div class="right-box h-dvh" v-loading="loading" element-loading-background="rgba(100,100,100,0.3)">
<div
class="right-box h-dvh"
v-loading="loading"
element-loading-background="rgba(100,100,100,0.3)"
>
<div class="list-box" v-if="!noData">
<div v-for="item in list" :key="item.id">
<div class="item" v-if="item.progress === 100">
@@ -150,7 +210,9 @@
<div class="center">
<div class="title">
<a :href="'/song/' + item.song_id" target="_blank">{{ item.title }}</a>
<span class="model" v-if="item.major_model_version">{{ item.major_model_version }}</span>
<span class="model" v-if="item.major_model_version">{{
item.major_model_version
}}</span>
<span class="model" v-if="item.type === 4">用户上传</span>
<span class="model" v-if="item.type === 3">
<i class="iconfont icon-mp3"></i>
@@ -171,7 +233,11 @@
<button class="btn btn-publish">
<span class="text">发布</span>
<black-switch v-model:value="item.publish" @change="publishJob(item)" size="small" />
<black-switch
v-model:value="item.publish"
@change="publishJob(item)"
size="small"
/>
</button>
<el-tooltip content="下载歌曲" placement="top">
@@ -228,7 +294,12 @@
</div>
</div>
</div>
<el-empty :image-size="100" :image="nodata" description="没有任何作品,赶紧去创作吧!" v-else />
<el-empty
:image-size="100"
:image="nodata"
description="没有任何作品,赶紧去创作吧!"
v-else
/>
<div class="pagination">
<el-pagination
@@ -245,11 +316,22 @@
</div>
<div class="music-player" v-if="showPlayer">
<music-player :songs="playList" ref="playerRef" :show-close="true" @close="showPlayer = false" />
<music-player
:songs="playList"
ref="playerRef"
:show-close="true"
@close="showPlayer = false"
/>
</div>
</div>
<black-dialog v-model:show="showDialog" title="修改歌曲" @cancal="showDialog = false" @confirm="updateSong" :width="500 + 'px'">
<black-dialog
v-model:show="showDialog"
title="修改歌曲"
@cancal="showDialog = false"
@confirm="updateSong"
:width="500 + 'px'"
>
<form class="form">
<div class="form-item">
<div class="label">歌曲名称</div>
@@ -258,7 +340,13 @@
<div class="form-item">
<div class="label">封面图片</div>
<el-upload class="avatar-uploader" :auto-upload="true" :show-file-list="false" :http-request="uploadCover" accept=".png,.jpg,.jpeg,.bmp">
<el-upload
class="avatar-uploader"
:auto-upload="true"
:show-file-list="false"
:http-request="uploadCover"
accept=".png,.jpg,.jpeg,.bmp"
>
<el-avatar :src="editData.cover" shape="square" :size="100" />
</el-upload>
</div>
@@ -268,386 +356,386 @@
</template>
<script setup>
import nodata from "@/assets/img/no-data.png";
import nodata from '@/assets/img/no-data.png'
import MusicPlayer from "@/components/MusicPlayer.vue";
import BlackDialog from "@/components/ui/BlackDialog.vue";
import BlackInput from "@/components/ui/BlackInput.vue";
import BlackSelect from "@/components/ui/BlackSelect.vue";
import BlackSwitch from "@/components/ui/BlackSwitch.vue";
import Generating from "@/components/ui/Generating.vue";
import { checkSession } from "@/store/cache";
import { useSharedStore } from "@/store/sharedata";
import { closeLoading, showLoading, showMessageError, showMessageOK } from "@/utils/dialog";
import { httpDownload, httpGet, httpPost } from "@/utils/http";
import { formatTime, replaceImg } from "@/utils/libs";
import { Delete, InfoFilled } from "@element-plus/icons-vue";
import Clipboard from "clipboard";
import Compressor from "compressorjs";
import { ElMessage, ElMessageBox } from "element-plus";
import { compact } from "lodash";
import { nextTick, onMounted, onUnmounted, ref, watch } from "vue";
import MusicPlayer from '@/components/MusicPlayer.vue'
import BlackDialog from '@/components/ui/BlackDialog.vue'
import BlackInput from '@/components/ui/BlackInput.vue'
import BlackSelect from '@/components/ui/BlackSelect.vue'
import BlackSwitch from '@/components/ui/BlackSwitch.vue'
import Generating from '@/components/ui/Generating.vue'
import { checkSession } from '@/store/cache'
import { useSharedStore } from '@/store/sharedata'
import { closeLoading, showLoading, showMessageError, showMessageOK } from '@/utils/dialog'
import { httpDownload, httpGet, httpPost } from '@/utils/http'
import { formatTime, replaceImg } from '@/utils/libs'
import { Delete, InfoFilled } from '@element-plus/icons-vue'
import Clipboard from 'clipboard'
import Compressor from 'compressorjs'
import { ElMessage, ElMessageBox } from 'element-plus'
import { compact } from 'lodash'
import { nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
const custom = ref(false);
const custom = ref(false)
const models = ref([
{ label: "v3.0", value: "chirp-v3-0" },
{ label: "v3.5", value: "chirp-v3-5" },
{ label: "v4.0", value: "chirp-v4" },
{ label: "v4.5", value: "chirp-auk" },
]);
{ label: 'v3.0', value: 'chirp-v3-0' },
{ label: 'v3.5', value: 'chirp-v3-5' },
{ label: 'v4.0', value: 'chirp-v4' },
{ label: 'v4.5', value: 'chirp-auk' },
])
const tags = ref([
{ label: "女声", value: "female vocals" },
{ label: "男声", value: "male vocals" },
{ label: "流行", value: "pop" },
{ label: "摇滚", value: "rock" },
{ label: "硬摇滚", value: "hard rock" },
{ label: "电音", value: "electronic" },
{ label: "金属", value: "metal" },
{ label: "重金属", value: "heavy metal" },
{ label: "节拍", value: "beat" },
{ label: "弱拍", value: "upbeat" },
{ label: "合成器", value: "synth" },
{ label: "吉他", value: "guitar" },
{ label: "钢琴", value: "piano" },
{ label: "小提琴", value: "violin" },
{ label: "贝斯", value: "bass" },
{ label: "嘻哈", value: "hip hop" },
]);
{ label: '女声', value: 'female vocals' },
{ label: '男声', value: 'male vocals' },
{ label: '流行', value: 'pop' },
{ label: '摇滚', value: 'rock' },
{ label: '硬摇滚', value: 'hard rock' },
{ label: '电音', value: 'electronic' },
{ label: '金属', value: 'metal' },
{ label: '重金属', value: 'heavy metal' },
{ label: '节拍', value: 'beat' },
{ label: '弱拍', value: 'upbeat' },
{ label: '合成器', value: 'synth' },
{ label: '吉他', value: 'guitar' },
{ label: '钢琴', value: 'piano' },
{ label: '小提琴', value: 'violin' },
{ label: '贝斯', value: 'bass' },
{ label: '嘻哈', value: 'hip hop' },
])
const data = ref({
model: "chirp-auk",
tags: "",
lyrics: "",
prompt: "",
title: "",
model: 'chirp-auk',
tags: '',
lyrics: '',
prompt: '',
title: '',
instrumental: false,
ref_task_id: "",
ref_task_id: '',
extend_secs: 0,
ref_song_id: "",
});
const loading = ref(false);
const noData = ref(true);
const playList = ref([]);
const playerRef = ref(null);
const showPlayer = ref(false);
const list = ref([]);
const taskPulling = ref(true);
const tastPullHandler = ref(null);
const btnText = ref("开始创作");
const refSong = ref(null);
const showDialog = ref(false);
const editData = ref({ title: "", cover: "", id: 0 });
const promptPlaceholder = ref("请在这里输入你自己写的歌词...");
const store = useSharedStore();
const clipboard = ref(null);
ref_song_id: '',
})
const loading = ref(false)
const noData = ref(true)
const playList = ref([])
const playerRef = ref(null)
const showPlayer = ref(false)
const list = ref([])
const taskPulling = ref(true)
const tastPullHandler = ref(null)
const btnText = ref('开始创作')
const refSong = ref(null)
const showDialog = ref(false)
const editData = ref({ title: '', cover: '', id: 0 })
const promptPlaceholder = ref('请在这里输入你自己写的歌词...')
const store = useSharedStore()
const clipboard = ref(null)
onMounted(() => {
clipboard.value = new Clipboard(".copy-link");
clipboard.value.on("success", () => {
ElMessage.success("复制歌曲链接成功!");
});
clipboard.value = new Clipboard('.copy-link')
clipboard.value.on('success', () => {
ElMessage.success('复制歌曲链接成功!')
})
clipboard.value.on("error", () => {
ElMessage.error("复制失败!");
});
clipboard.value.on('error', () => {
ElMessage.error('复制失败!')
})
checkSession()
.then(() => {
fetchData(1);
fetchData(1)
tastPullHandler.value = setInterval(() => {
if (taskPulling.value) {
fetchData(1);
fetchData(1)
}
}, 5000);
}, 5000)
})
.catch(() => {});
});
.catch(() => {})
})
onUnmounted(() => {
clipboard.value.destroy();
clipboard.value.destroy()
if (tastPullHandler.value) {
clearInterval(tastPullHandler.value);
clearInterval(tastPullHandler.value)
}
});
})
const page = ref(1);
const pageSize = ref(10);
const total = ref(0);
const page = ref(1)
const pageSize = ref(10)
const total = ref(0)
const fetchData = (_page) => {
if (_page) {
page.value = _page;
page.value = _page
}
loading.value = true;
httpGet("/api/suno/list", { page: page.value, page_size: pageSize.value })
loading.value = true
httpGet('/api/suno/list', { page: page.value, page_size: pageSize.value })
.then((res) => {
total.value = res.data.total;
let needPull = false;
const items = [];
total.value = res.data.total
let needPull = false
const items = []
for (let v of res.data.items) {
if (v.progress === 100) {
v.major_model_version = v["raw_data"]["major_model_version"];
v.major_model_version = v['raw_data']['major_model_version']
}
if (v.progress === 0 || v.progress === 102) {
needPull = true;
needPull = true
}
items.push(v);
items.push(v)
}
loading.value = false;
taskPulling.value = needPull;
loading.value = false
taskPulling.value = needPull
// 如果任务有变化,则刷新任务列表
if (JSON.stringify(list.value) !== JSON.stringify(items)) {
list.value = items;
list.value = items
}
noData.value = list.value.length === 0;
noData.value = list.value.length === 0
})
.catch((e) => {
loading.value = false;
noData.value = true;
showMessageError("获取作品列表失败:" + e.message);
});
};
loading.value = false
noData.value = true
showMessageError('获取作品列表失败:' + e.message)
})
}
// 创建新的歌曲
const create = () => {
data.value.type = custom.value ? 2 : 1;
data.value.ref_task_id = refSong.value ? refSong.value.task_id : "";
data.value.ref_song_id = refSong.value ? refSong.value.song_id : "";
data.value.extend_secs = refSong.value ? refSong.value.extend_secs : 0;
data.value.type = custom.value ? 2 : 1
data.value.ref_task_id = refSong.value ? refSong.value.task_id : ''
data.value.ref_song_id = refSong.value ? refSong.value.song_id : ''
data.value.extend_secs = refSong.value ? refSong.value.extend_secs : 0
if (refSong.value) {
if (data.value.extend_secs > refSong.value.duration) {
return showMessageError("续写开始时间不能超过原歌曲长度");
return showMessageError('续写开始时间不能超过原歌曲长度')
}
} else if (custom.value) {
if (data.value.lyrics === "") {
return showMessageError("请输入歌词");
if (data.value.lyrics === '') {
return showMessageError('请输入歌词')
}
if (data.value.title === "") {
return showMessageError("请输入歌曲标题");
if (data.value.title === '') {
return showMessageError('请输入歌曲标题')
}
} else {
if (data.value.prompt === "") {
return showMessageError("请输入歌曲描述");
if (data.value.prompt === '') {
return showMessageError('请输入歌曲描述')
}
}
httpPost("/api/suno/create", data.value)
httpPost('/api/suno/create', data.value)
.then(() => {
fetchData(1);
taskPulling.value = true;
showMessageOK("创建任务成功");
fetchData(1)
taskPulling.value = true
showMessageOK('创建任务成功')
})
.catch((e) => {
showMessageError("创建任务失败:" + e.message);
});
};
showMessageError('创建任务失败:' + e.message)
})
}
// 拼接歌曲
const merge = (item) => {
httpPost("/api/suno/create", { song_id: item.song_id, type: 3 })
httpPost('/api/suno/create', { song_id: item.song_id, type: 3 })
.then(() => {
fetchData(1);
taskPulling.value = true;
showMessageOK("创建任务成功");
fetchData(1)
taskPulling.value = true
showMessageOK('创建任务成功')
})
.catch((e) => {
showMessageError("合并歌曲失败:" + e.message);
});
};
showMessageError('合并歌曲失败:' + e.message)
})
}
// 下载歌曲
const download = (item) => {
const url = replaceImg(item.audio_url);
const downloadURL = `${import.meta.env.VITE_API_HOST}/api/download?url=${url}`;
const url = replaceImg(item.audio_url)
const downloadURL = `${import.meta.env.VITE_API_HOST}/api/download?url=${url}`
// parse filename
const urlObj = new URL(url);
const fileName = urlObj.pathname.split("/").pop();
item.downloading = true;
const urlObj = new URL(url)
const fileName = urlObj.pathname.split('/').pop()
item.downloading = true
httpDownload(downloadURL)
.then((response) => {
const blob = new Blob([response.data]);
const link = document.createElement("a");
link.href = URL.createObjectURL(blob);
link.download = fileName;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(link.href);
item.downloading = false;
const blob = new Blob([response.data])
const link = document.createElement('a')
link.href = URL.createObjectURL(blob)
link.download = fileName
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(link.href)
item.downloading = false
})
.catch(() => {
showMessageError("下载失败");
item.downloading = false;
});
};
showMessageError('下载失败')
item.downloading = false
})
}
const uploadAudio = (file) => {
const formData = new FormData();
formData.append("file", file.file, file.name);
showLoading("正在上传文件...");
const formData = new FormData()
formData.append('file', file.file, file.name)
showLoading('正在上传文件...')
// 执行上传操作
httpPost("/api/upload", formData)
httpPost('/api/upload', formData)
.then((res) => {
httpPost("/api/suno/create", {
httpPost('/api/suno/create', {
audio_url: res.data.url,
title: res.data.name,
type: 4,
})
.then(() => {
fetchData(1);
showMessageOK("歌曲上传成功");
closeLoading();
fetchData(1)
showMessageOK('歌曲上传成功')
closeLoading()
})
.catch((e) => {
showMessageError("歌曲上传失败:" + e.message);
closeLoading();
});
removeRefSong();
ElMessage.success({ message: "上传成功", duration: 500 });
showMessageError('歌曲上传失败:' + e.message)
closeLoading()
})
removeRefSong()
ElMessage.success({ message: '上传成功', duration: 500 })
})
.catch((e) => {
ElMessage.error("文件传失败:" + e.message);
});
};
ElMessage.error('文件传失败:' + e.message)
})
}
// 续写歌曲
const extend = (item) => {
refSong.value = item;
refSong.value.extend_secs = item.duration;
data.value.title = item.title;
custom.value = true;
btnText.value = "续写歌曲";
promptPlaceholder.value = "输入额外的歌词,根据您之前的歌词来扩展歌曲...";
};
refSong.value = item
refSong.value.extend_secs = item.duration
data.value.title = item.title
custom.value = true
btnText.value = '续写歌曲'
promptPlaceholder.value = '输入额外的歌词,根据您之前的歌词来扩展歌曲...'
}
// 更细歌曲
const update = (item) => {
showDialog.value = true;
editData.value.title = item.title;
editData.value.cover = item.cover_url;
editData.value.id = item.id;
};
showDialog.value = true
editData.value.title = item.title
editData.value.cover = item.cover_url
editData.value.id = item.id
}
const updateSong = () => {
if (editData.value.title === "" || editData.value.cover === "") {
return showMessageError("歌曲标题和封面不能为空");
if (editData.value.title === '' || editData.value.cover === '') {
return showMessageError('歌曲标题和封面不能为空')
}
httpPost("/api/suno/update", editData.value)
httpPost('/api/suno/update', editData.value)
.then(() => {
showMessageOK("更新歌曲成功");
showDialog.value = false;
fetchData();
showMessageOK('更新歌曲成功')
showDialog.value = false
fetchData()
})
.catch((e) => {
showMessageError("更新歌曲失败:" + e.message);
});
};
showMessageError('更新歌曲失败:' + e.message)
})
}
watch(
() => custom.value,
(newValue) => {
if (!newValue) {
removeRefSong();
removeRefSong()
}
}
);
)
const removeRefSong = () => {
refSong.value = null;
btnText.value = "开始创作";
promptPlaceholder.value = "请在这里输入你自己写的歌词...";
};
refSong.value = null
btnText.value = '开始创作'
promptPlaceholder.value = '请在这里输入你自己写的歌词...'
}
const play = (item) => {
playList.value = [item];
showPlayer.value = true;
nextTick(() => playerRef.value.play());
};
playList.value = [item]
showPlayer.value = true
nextTick(() => playerRef.value.play())
}
const selectTag = (tag) => {
if (data.value.tags.length + tag.value.length >= 119) {
return;
return
}
data.value.tags = compact([...data.value.tags.split(","), tag.value]).join(",");
};
data.value.tags = compact([...data.value.tags.split(','), tag.value]).join(',')
}
const removeJob = (item) => {
ElMessageBox.confirm("此操作将会删除任务相关文件,继续操作码?", "删除提示", {
confirmButtonText: "确认",
cancelButtonText: "取消",
type: "warning",
ElMessageBox.confirm('此操作将会删除任务相关文件,继续操作码?', '删除提示', {
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
httpGet("/api/suno/remove", { id: item.id })
httpGet('/api/suno/remove', { id: item.id })
.then(() => {
ElMessage.success("任务删除成功");
fetchData();
ElMessage.success('任务删除成功')
fetchData()
})
.catch((e) => {
ElMessage.error("任务删除失败:" + e.message);
});
ElMessage.error('任务删除失败:' + e.message)
})
})
.catch(() => {});
};
.catch(() => {})
}
const publishJob = (item) => {
httpGet("/api/suno/publish", { id: item.id, publish: item.publish })
httpGet('/api/suno/publish', { id: item.id, publish: item.publish })
.then(() => {
ElMessage.success("操作成功");
ElMessage.success('操作成功')
})
.catch((e) => {
ElMessage.error("操作失败:" + e.message);
});
};
ElMessage.error('操作失败:' + e.message)
})
}
const getShareURL = (item) => {
return `${location.protocol}//${location.host}/song/${item.song_id}`;
};
return `${location.protocol}//${location.host}/song/${item.song_id}`
}
const uploadCover = (file) => {
// 压缩图片并上传
new Compressor(file.file, {
quality: 0.6,
success(result) {
const formData = new FormData();
formData.append("file", result, result.name);
showLoading("图片上传中...");
const formData = new FormData()
formData.append('file', result, result.name)
showLoading('图片上传中...')
// 执行上传操作
httpPost("/api/upload", formData)
httpPost('/api/upload', formData)
.then((res) => {
editData.value.cover = res.data.url;
ElMessage.success({ message: "上传成功", duration: 500 });
closeLoading();
editData.value.cover = res.data.url
ElMessage.success({ message: '上传成功', duration: 500 })
closeLoading()
})
.catch((e) => {
ElMessage.error("图片上传失败:" + e.message);
closeLoading();
});
ElMessage.error('图片上传失败:' + e.message)
closeLoading()
})
},
error(err) {
console.log(err.message);
console.log(err.message)
},
});
};
})
}
const isGenerating = ref(false);
const isGenerating = ref(false)
const createLyric = () => {
if (data.value.lyrics === "") {
return showMessageError("请输入歌词描述");
if (data.value.lyrics === '') {
return showMessageError('请输入歌词描述')
}
isGenerating.value = true;
httpPost("/api/prompt/lyric", { prompt: data.value.lyrics })
isGenerating.value = true
httpPost('/api/prompt/lyric', { prompt: data.value.lyrics })
.then((res) => {
const lines = res.data.split("\n");
data.value.title = lines.shift().replace(/\*/g, "");
lines.shift();
data.value.lyrics = lines.join("\n");
isGenerating.value = false;
const lines = res.data.split('\n')
data.value.title = lines.shift().replace(/\*/g, '')
lines.shift()
data.value.lyrics = lines.join('\n')
isGenerating.value = false
})
.catch((e) => {
showMessageError("歌词生成失败:" + e.message);
isGenerating.value = false;
});
};
showMessageError('歌词生成失败:' + e.message)
isGenerating.value = false
})
}
</script>
<style lang="stylus" scoped>
@import "@/assets/css/suno.styl"
@import '../assets/css/suno.styl'
</style>

View File

@@ -391,7 +391,7 @@ const remove = function (row) {
</script>
<style lang="stylus" scoped>
@import "@/assets/css/admin/form.styl";
@import "../../assets/css/admin/form.styl";
.model-list {
.handle-box {

View File

@@ -4,10 +4,7 @@
<div class="content-box" :class="{ 'content-collapse': sidebar.collapse }">
<admin-header />
<admin-tags />
<div
:class="'content ' + theme"
:style="{ height: contentHeight + 'px' }"
>
<div :class="'content ' + theme" :style="{ height: contentHeight + 'px' }">
<router-view v-slot="{ Component }">
<transition name="move" mode="out-in">
<keep-alive :include="tags.nameList">
@@ -20,41 +17,41 @@
</div>
</template>
<script setup>
import {useSidebarStore} from "@/store/sidebar";
import {useTagsStore} from "@/store/tags";
import AdminHeader from "@/components/admin/AdminHeader.vue";
import AdminSidebar from "@/components/admin/AdminSidebar.vue";
import AdminTags from "@/components/admin/AdminTags.vue";
import {useRouter} from "vue-router";
import {checkAdminSession} from "@/store/cache";
import {ref, watch} from "vue";
import {useSharedStore} from "@/store/sharedata";
import AdminHeader from '@/components/admin/AdminHeader.vue'
import AdminSidebar from '@/components/admin/AdminSidebar.vue'
import AdminTags from '@/components/admin/AdminTags.vue'
import { checkAdminSession } from '@/store/cache'
import { useSharedStore } from '@/store/sharedata'
import { useSidebarStore } from '@/store/sidebar'
import { useTagsStore } from '@/store/tags'
import { ref, watch } from 'vue'
import { useRouter } from 'vue-router'
const sidebar = useSidebarStore();
const tags = useTagsStore();
const isLogin = ref(false);
const contentHeight = window.innerHeight - 80;
const store = useSharedStore();
const theme = ref(store.theme);
const sidebar = useSidebarStore()
const tags = useTagsStore()
const isLogin = ref(false)
const contentHeight = window.innerHeight - 80
const store = useSharedStore()
const theme = ref(store.theme)
// 获取会话信息
const router = useRouter();
const router = useRouter()
checkAdminSession()
.then(() => {
isLogin.value = true;
isLogin.value = true
})
.catch(() => {
router.replace("/admin/login");
});
router.replace('/admin/login')
})
watch(
() => store.theme,
() => store.theme,
(val) => {
theme.value = val;
theme.value = val
}
);
)
</script>
<style scoped lang="stylus">
@import '@/assets/css/main.styl';
<style lang="stylus" scoped>
@import "../../assets/css/main.styl";
</style>

View File

@@ -133,7 +133,7 @@ const doLogin = function (verifyData) {
background #8d4bbb
// background-image url("~@/assets/img/transparent-bg.png")
// background-repeat:repeat;
background-image url("~@/assets/img/admin-login-bg.jpg")
background-image url("@/assets/img/admin-login-bg.jpg")
background-size cover
background-position center
background-repeat no-repeat

View File

@@ -5,7 +5,13 @@
</div>
<el-row>
<el-table :data="items" :row-key="(row) => row.id" table-layout="auto" border style="width: 100%">
<el-table
:data="items"
:row-key="(row) => row.id"
table-layout="auto"
border
style="width: 100%"
>
<el-table-column prop="name" label="菜单名称">
<template #default="scope">
<span class="sort" :data-id="scope.row.id">
@@ -51,7 +57,12 @@
<template #label>
<div class="label-title">
开放注册
<el-tooltip effect="dark" content="可以填写 iconfont 图标名称也可以自己上传图片" raw-content placement="right">
<el-tooltip
effect="dark"
content="可以填写 iconfont 图标名称也可以自己上传图片"
raw-content
placement="right"
>
<el-icon>
<InfoFilled />
</el-icon>
@@ -89,49 +100,49 @@
</template>
<script setup>
import { onMounted, reactive, ref } from "vue";
import { httpGet, httpPost } from "@/utils/http";
import { ElMessage } from "element-plus";
import { dateFormat, removeArrayItem } from "@/utils/libs";
import { InfoFilled, Plus, UploadFilled } from "@element-plus/icons-vue";
import { Sortable } from "sortablejs";
import Compressor from "compressorjs";
import { httpGet, httpPost } from '@/utils/http'
import { dateFormat, removeArrayItem } from '@/utils/libs'
import { InfoFilled, Plus, UploadFilled } from '@element-plus/icons-vue'
import Compressor from 'compressorjs'
import { ElMessage } from 'element-plus'
import { Sortable } from 'sortablejs'
import { onMounted, reactive, ref } from 'vue'
// 变量定义
const items = ref([]);
const item = ref({});
const showDialog = ref(false);
const title = ref("");
const items = ref([])
const item = ref({})
const showDialog = ref(false)
const title = ref('')
const rules = reactive({
name: [{ required: true, message: "请输入菜单名称", trigger: "change" }],
icon: [{ required: true, message: "请上传菜单图标", trigger: "change" }],
url: [{ required: true, message: "请输入菜单地址", trigger: "change" }],
});
const loading = ref(true);
const formRef = ref(null);
name: [{ required: true, message: '请输入菜单名称', trigger: 'change' }],
icon: [{ required: true, message: '请上传菜单图标', trigger: 'change' }],
url: [{ required: true, message: '请输入菜单地址', trigger: 'change' }],
})
const loading = ref(true)
const formRef = ref(null)
const fetchData = () => {
// 获取数据
httpGet("/api/admin/menu/list")
httpGet('/api/admin/menu/list')
.then((res) => {
if (res.data) {
// 初始化数据
const arr = res.data;
const arr = res.data
for (let i = 0; i < arr.length; i++) {
arr[i].last_used_at = dateFormat(arr[i].last_used_at);
arr[i].last_used_at = dateFormat(arr[i].last_used_at)
}
items.value = arr;
items.value = arr
}
loading.value = false;
loading.value = false
})
.catch(() => {
ElMessage.error("获取数据失败");
});
};
ElMessage.error('获取数据失败')
})
}
onMounted(() => {
const drawBodyWrapper = document.querySelector(".el-table__body tbody");
fetchData();
const drawBodyWrapper = document.querySelector('.el-table__body tbody')
fetchData()
// 初始化拖动排序插件
Sortable.create(drawBodyWrapper, {
@@ -139,80 +150,82 @@ onMounted(() => {
animation: 500,
onEnd({ newIndex, oldIndex, from }) {
if (oldIndex === newIndex) {
return;
return
}
const sortedData = Array.from(from.children).map((row) => row.querySelector(".sort").getAttribute("data-id"));
const ids = [];
const sorts = [];
const sortedData = Array.from(from.children).map((row) =>
row.querySelector('.sort').getAttribute('data-id')
)
const ids = []
const sorts = []
sortedData.forEach((id, index) => {
ids.push(parseInt(id));
sorts.push(index + 1);
items.value[index].sort_num = index + 1;
});
ids.push(parseInt(id))
sorts.push(index + 1)
items.value[index].sort_num = index + 1
})
httpPost("/api/admin/menu/sort", { ids: ids, sorts: sorts }).catch((e) => {
ElMessage.error("排序失败" + e.message);
});
httpPost('/api/admin/menu/sort', { ids: ids, sorts: sorts }).catch((e) => {
ElMessage.error('排序失败' + e.message)
})
},
});
});
})
})
const add = function () {
title.value = "新增菜单";
showDialog.value = true;
item.value = {};
};
title.value = '新增菜单'
showDialog.value = true
item.value = {}
}
const edit = function (row) {
title.value = "修改菜单";
showDialog.value = true;
item.value = row;
};
title.value = '修改菜单'
showDialog.value = true
item.value = row
}
const save = function () {
formRef.value.validate((valid) => {
if (valid) {
showDialog.value = false;
showDialog.value = false
if (!item.value.id) {
item.value.sort_num = items.value.length + 1;
item.value.sort_num = items.value.length + 1
}
httpPost("/api/admin/menu/save", item.value)
httpPost('/api/admin/menu/save', item.value)
.then(() => {
ElMessage.success("操作成功");
fetchData();
ElMessage.success('操作成功')
fetchData()
})
.catch((e) => {
ElMessage.error("操作失败" + e.message);
});
ElMessage.error('操作失败' + e.message)
})
} else {
return false;
return false
}
});
};
})
}
const enable = (row) => {
httpPost("/api/admin/menu/enable", { id: row.id, enabled: row.enabled })
httpPost('/api/admin/menu/enable', { id: row.id, enabled: row.enabled })
.then(() => {
ElMessage.success("操作成功");
ElMessage.success('操作成功')
})
.catch((e) => {
ElMessage.error("操作失败" + e.message);
});
};
ElMessage.error('操作失败' + e.message)
})
}
const remove = function (row) {
httpGet("/api/admin/menu/remove?id=" + row.id)
httpGet('/api/admin/menu/remove?id=' + row.id)
.then(() => {
ElMessage.success("删除成功");
ElMessage.success('删除成功')
items.value = removeArrayItem(items.value, row, (v1, v2) => {
return v1.id === v2.id;
});
return v1.id === v2.id
})
})
.catch((e) => {
ElMessage.error("删除失败" + e.message);
});
};
ElMessage.error('删除失败' + e.message)
})
}
// 图片上传
const uploadImg = (file) => {
@@ -220,28 +233,28 @@ const uploadImg = (file) => {
new Compressor(file.file, {
quality: 0.6,
success(result) {
const formData = new FormData();
formData.append("file", result, result.name);
const formData = new FormData()
formData.append('file', result, result.name)
// 执行上传操作
httpPost("/api/admin/upload", formData)
httpPost('/api/admin/upload', formData)
.then((res) => {
item.value.icon = res.data.url;
ElMessage.success("上传成功");
item.value.icon = res.data.url
ElMessage.success('上传成功')
})
.catch((e) => {
ElMessage.error("上传失败:" + e.message);
});
ElMessage.error('上传失败:' + e.message)
})
},
error(e) {
ElMessage.error("上传失败:" + e.message);
ElMessage.error('上传失败:' + e.message)
},
});
};
})
}
</script>
<style lang="stylus">
@import "@/assets/css/admin/form.styl"
@import "@/assets/css/main.styl"
<style lang="stylus" scoped>
@import "../../assets/css/admin/form.styl"
@import "../../assets/css/main.styl"
.menu {
.handle-box {

View File

@@ -567,38 +567,34 @@
</div>
</el-tab-pane>
<el-tab-pane label="修复数据" name="fixData">
<!-- <el-tab-pane label="修复数据" name="fixData">
<div class="container">
<p class="text">
有些版本升级的时候更新了数据库的结构比如字段名字改了需要把之前的字段的值转移到其他字段这些无法通过简单的
SQL 语句可以实现的需要手动写程序修正数据
</p>
<!-- <p class="text">当前版本 v4.1.4 需要修正用户数据增加了 mobile email 字段需要把之前用手机号或者邮箱注册的用户的 username 字段数据初始化到 mobile 或者 email 字段另外需要把订单的支付渠道从名字称修正为 key</p>-->
<!-- <el-text type="danger">请注意在修复数据前请先备份好数据库以免数据丢失</el-text>-->
<div class="mt-3">
<el-button type="primary" @click="fixData">立即修复</el-button>
</div>
</div>
</el-tab-pane>
</el-tab-pane> -->
</el-tabs>
</div>
</template>
<script setup>
import { onMounted, reactive, ref } from 'vue'
import { httpGet, httpPost } from '@/utils/http'
import Compressor from 'compressorjs'
import { ElMessage, ElMessageBox } from 'element-plus'
import { CloseBold, InfoFilled, Select, UploadFilled } from '@element-plus/icons-vue'
import MdEditor from 'md-editor-v3'
import 'md-editor-v3/lib/style.css'
import Menu from '@/views/admin/Menu.vue'
import { copyObj, dateFormat } from '@/utils/libs'
import ItemsInput from '@/components/ui/ItemsInput.vue'
import { useSharedStore } from '@/store/sharedata'
import { httpGet, httpPost } from '@/utils/http'
import { copyObj, dateFormat } from '@/utils/libs'
import Menu from '@/views/admin/Menu.vue'
import { CloseBold, InfoFilled, Select, UploadFilled } from '@element-plus/icons-vue'
import Compressor from 'compressorjs'
import { ElMessage } from 'element-plus'
import MdEditor from 'md-editor-v3'
import 'md-editor-v3/lib/style.css'
import { onMounted, reactive, ref } from 'vue'
const activeName = ref('basic')
const system = ref({ models: [] })
@@ -826,29 +822,29 @@ const onUploadImg = (files, callback) => {
})
}
const fixData = () => {
ElMessageBox.confirm('在修复数据前,请先备份好数据库,以免数据丢失!是否继续操作?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(() => {
loading.value = true
httpGet('/api/admin/config/fixData')
.then(() => {
ElMessage.success('数据修复成功')
loading.value = false
})
.catch((e) => {
loading.value = false
ElMessage.error('数据修复失败:' + e.message)
})
})
}
// const fixData = () => {
// ElMessageBox.confirm('在修复数据前,请先备份好数据库,以免数据丢失!是否继续操作?', '警告', {
// confirmButtonText: '确定',
// cancelButtonText: '取消',
// type: 'warning',
// }).then(() => {
// loading.value = true
// httpGet('/api/admin/config/fixData')
// .then(() => {
// ElMessage.success('数据修复成功')
// loading.value = false
// })
// .catch((e) => {
// loading.value = false
// ElMessage.error('数据修复失败:' + e.message)
// })
// })
// }
</script>
<style lang="stylus" scoped>
@import "@/assets/css/admin/form.styl"
@import "@/assets/css/main.styl"
@import '../../assets/css/admin/form.styl'
@import '../../assets/css/main.styl'
.system-config {
display flex
justify-content center

View File

@@ -186,7 +186,7 @@
class="chat-dialog"
style="--el-dialog-width: 60%"
>
<div class="chat-box chat-page">
<div class="chat-box chat-page p-2">
<div v-for="item in messages" :key="item.id">
<chat-prompt v-if="item.type === 'prompt'" :data="item" />
<chat-reply v-else-if="item.type === 'reply'" :read-only="true" :data="item" />
@@ -206,6 +206,8 @@ import { Search } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import hl from 'highlight.js'
import 'highlight.js/styles/a11y-dark.css'
import MarkdownIt from 'markdown-it'
import mathjaxPlugin from 'markdown-it-mathjax3'
import { onMounted, ref } from 'vue'
// 变量定义
@@ -316,8 +318,7 @@ const removeMessage = function (row) {
})
}
const mathjaxPlugin = require('markdown-it-mathjax3')
const md = require('markdown-it')({
const md = MarkdownIt({
breaks: true,
html: true,
linkify: true,

View File

@@ -1,10 +1,28 @@
<template>
<div class="container media-page">
<el-tabs v-model="activeName" @tab-change="handleChange">
<el-tab-pane v-for="media in mediaTypes" :key="media.name" :label="media.label" :name="media.name" v-loading="data[media.name].loading">
<el-tab-pane
v-for="media in mediaTypes"
:key="media.name"
:label="media.label"
:name="media.name"
v-loading="data[media.name].loading"
>
<div class="handle-box">
<el-input v-model="data[media.name].query.username" placeholder="用户名" class="handle-input mr10" @keyup="search($event, media.name)" clearable />
<el-input v-model="data[media.name].query.prompt" placeholder="提示词" class="handle-input mr10" @keyup="search($event, media.name)" clearable />
<el-input
v-model="data[media.name].query.username"
placeholder="用户名"
class="handle-input mr10"
@keyup="search($event, media.name)"
clearable
/>
<el-input
v-model="data[media.name].query.prompt"
placeholder="提示词"
class="handle-input mr10"
@keyup="search($event, media.name)"
clearable
/>
<el-date-picker
v-model="data[media.name].query.created_at"
type="daterange"
@@ -28,7 +46,10 @@
<div class="duration">
{{ formatTime(scope.row.duration) }}
</div>
<button class="play" @click="playMusic(scope.row)">
<button
class="play flex justify-center items-center"
@click="playMusic(scope.row)"
>
<img src="/images/play.svg" alt="" />
</button>
</div>
@@ -37,12 +58,27 @@
<template #default="scope" v-if="media.previewComponent === 'VideoPreview'">
<div class="container">
<div v-if="scope.row.progress === 100">
<video class="video" :src="replaceImg(scope.row.video_url)" preload="auto" loop="loop" muted="muted">您的浏览器不支持视频播放</video>
<button class="play" @click="playVideo(scope.row)">
<video
class="video"
:src="replaceImg(scope.row.video_url)"
preload="auto"
loop="loop"
muted="muted"
>
您的浏览器不支持视频播放
</video>
<button
class="play flex justify-center items-center"
@click="playVideo(scope.row)"
>
<img src="/images/play.svg" alt="" />
</button>
</div>
<el-image :src="scope.row.cover_url" fit="cover" v-else-if="scope.row.progress > 100" />
<el-image
:src="scope.row.cover_url"
fit="cover"
v-else-if="scope.row.progress > 100"
/>
<generating message="正在生成视频" v-else />
</div>
</template>
@@ -59,13 +95,21 @@
<el-table-column prop="play_times" label="播放次数" />
<el-table-column label="歌词">
<template #default="scope">
<el-button size="small" type="primary" plain @click="showLyric(scope.row)">查看歌词</el-button>
<el-button size="small" type="primary" plain @click="showLyric(scope.row)"
>查看歌词</el-button
>
</template>
</el-table-column>
</template>
<el-table-column label="提示词" v-if="media.previewComponent === 'VideoPreview'">
<template #default="scope">
<el-popover placement="top-start" title="提示词" :width="300" trigger="hover" :content="scope.row.prompt">
<el-popover
placement="top-start"
title="提示词"
:width="300"
trigger="hover"
:content="scope.row.prompt"
>
<template #reference>
<span>{{ substr(scope.row.prompt, 20) }}</span>
</template>
@@ -74,7 +118,7 @@
</el-table-column>
<el-table-column label="创建时间">
<template #default="scope">
<span>{{ dateFormat(scope.row["created_at"]) }}</span>
<span>{{ dateFormat(scope.row['created_at']) }}</span>
</template>
</el-table-column>
<el-table-column label="失败原因">
@@ -96,7 +140,10 @@
</el-table-column>
<el-table-column label="操作" width="180">
<template #default="scope">
<el-popconfirm title="确定要删除当前记录吗?" @confirm="remove(scope.row, media.name)">
<el-popconfirm
title="确定要删除当前记录吗?"
@confirm="remove(scope.row, media.name)"
>
<template #reference>
<el-button size="small" type="danger">删除</el-button>
</template>
@@ -123,13 +170,25 @@
</el-tab-pane>
</el-tabs>
<el-dialog v-model="showVideoDialog" title="视频预览">
<video style="width: 100%; max-height: 90vh" :src="currentVideoUrl" preload="auto" :autoplay="true" loop="loop" muted="muted">
<video
style="width: 100%; max-height: 90vh"
:src="currentVideoUrl"
preload="auto"
:autoplay="true"
loop="loop"
muted="muted"
>
您的浏览器不支持视频播放
</video>
</el-dialog>
<div class="music-player" v-if="showPlayer">
<music-player :songs="playList" ref="playerRef" :show-close="true" @close="showPlayer = false" />
<music-player
:songs="playList"
ref="playerRef"
:show-close="true"
@close="showPlayer = false"
/>
</div>
<el-dialog v-model="showLyricDialog" title="歌词">
@@ -139,18 +198,19 @@
</template>
<script setup>
import { nextTick, onMounted, ref } from "vue";
import { httpGet, httpPost } from "@/utils/http";
import { ElMessage } from "element-plus";
import { dateFormat, formatTime, replaceImg, substr } from "@/utils/libs";
import { Search } from "@element-plus/icons-vue";
import MusicPlayer from "@/components/MusicPlayer.vue";
import Generating from "@/components/ui/Generating.vue";
import MusicPlayer from '@/components/MusicPlayer.vue'
import Generating from '@/components/ui/Generating.vue'
import { httpGet, httpPost } from '@/utils/http'
import { dateFormat, formatTime, replaceImg, substr } from '@/utils/libs'
import { Search } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import MarkdownIt from 'markdown-it'
import { nextTick, onMounted, ref } from 'vue'
const data = ref({
suno: {
items: [],
query: { prompt: "", username: "", created_at: [], page: 1, page_size: 15 },
query: { prompt: '', username: '', created_at: [], page: 1, page_size: 15 },
total: 0,
page: 1,
pageSize: 10,
@@ -158,7 +218,7 @@ const data = ref({
},
luma: {
items: [],
query: { prompt: "", username: "", created_at: [], page: 1, page_size: 15 },
query: { prompt: '', username: '', created_at: [], page: 1, page_size: 15 },
total: 0,
page: 1,
pageSize: 10,
@@ -166,169 +226,169 @@ const data = ref({
},
keling: {
items: [],
query: { prompt: "", username: "", created_at: [], page: 1, page_size: 15 },
query: { prompt: '', username: '', created_at: [], page: 1, page_size: 15 },
total: 0,
page: 1,
pageSize: 10,
loading: true,
},
});
})
const mediaTypes = [
{
name: "suno",
label: "Suno音乐",
name: 'suno',
label: 'Suno音乐',
fetchData: () => fetchSunoData(),
previewComponent: "MusicPreview",
previewComponent: 'MusicPreview',
},
{
name: "luma",
label: "Luma视频",
name: 'luma',
label: 'Luma视频',
fetchData: () => fetchLumaData(),
previewComponent: "VideoPreview",
previewComponent: 'VideoPreview',
},
{
name: "keling",
label: "可灵视频",
name: 'keling',
label: '可灵视频',
fetchData: () => fetchKelingData(),
previewComponent: "VideoPreview",
previewComponent: 'VideoPreview',
},
];
const activeName = ref("suno");
const playList = ref([]);
const playerRef = ref(null);
const showPlayer = ref(false);
const showLyricDialog = ref(false);
const lyrics = ref("");
const showVideoDialog = ref(false);
const currentVideoUrl = ref("");
]
const activeName = ref('suno')
const playList = ref([])
const playerRef = ref(null)
const showPlayer = ref(false)
const showLyricDialog = ref(false)
const lyrics = ref('')
const showVideoDialog = ref(false)
const currentVideoUrl = ref('')
onMounted(() => {
fetchSunoData();
});
fetchSunoData()
})
const handleChange = (tab) => {
switch (tab) {
case "suno":
fetchSunoData();
break;
case "luma":
fetchLumaData();
break;
case "keling":
fetchKelingData();
break;
case 'suno':
fetchSunoData()
break
case 'luma':
fetchLumaData()
break
case 'keling':
fetchKelingData()
break
}
};
}
// 搜索对话
const search = (evt, tab) => {
if (evt.keyCode === 13) {
handleChange(tab);
handleChange(tab)
}
};
}
// 获取数据
const fetchSunoData = () => {
const d = data.value.suno;
d.query.page = d.page;
d.query.page_size = d.pageSize;
httpPost("/api/admin/media/suno", d.query)
const d = data.value.suno
d.query.page = d.page
d.query.page_size = d.pageSize
httpPost('/api/admin/media/suno', d.query)
.then((res) => {
if (res.data) {
d.items = res.data.items;
d.total = res.data.total;
d.page = res.data.page;
d.pageSize = res.data.page_size;
d.items = res.data.items
d.total = res.data.total
d.page = res.data.page
d.pageSize = res.data.page_size
}
d.loading = false;
d.loading = false
})
.catch((e) => {
ElMessage.error("获取数据失败" + e.message);
});
};
ElMessage.error('获取数据失败' + e.message)
})
}
const fetchLumaData = () => {
const d = data.value.luma;
d.query.page = d.page;
d.query.page_size = d.pageSize;
d.query.type = "luma";
httpPost("/api/admin/media/videos", d.query)
const d = data.value.luma
d.query.page = d.page
d.query.page_size = d.pageSize
d.query.type = 'luma'
httpPost('/api/admin/media/videos', d.query)
.then((res) => {
if (res.data) {
d.items = res.data.items;
d.total = res.data.total;
d.page = res.data.page;
d.pageSize = res.data.page_size;
d.items = res.data.items
d.total = res.data.total
d.page = res.data.page
d.pageSize = res.data.page_size
}
d.loading = false;
d.loading = false
})
.catch((e) => {
ElMessage.error("获取数据失败" + e.message);
});
};
ElMessage.error('获取数据失败' + e.message)
})
}
const fetchKelingData = () => {
const d = data.value.keling;
d.query.page = d.page;
d.query.page_size = d.pageSize;
d.query.type = "keling";
httpPost("/api/admin/media/videos", d.query)
const d = data.value.keling
d.query.page = d.page
d.query.page_size = d.pageSize
d.query.type = 'keling'
httpPost('/api/admin/media/videos', d.query)
.then((res) => {
if (res.data) {
d.items = res.data.items;
d.total = res.data.total;
d.page = res.data.page;
d.pageSize = res.data.page_size;
d.items = res.data.items
d.total = res.data.total
d.page = res.data.page
d.pageSize = res.data.page_size
}
d.loading = false;
d.loading = false
})
.catch((e) => {
ElMessage.error("获取数据失败" + e.message);
});
};
ElMessage.error('获取数据失败' + e.message)
})
}
const remove = function (row, tab) {
httpGet(`/api/admin/media/remove?id=${row.id}&tab=${tab}`)
.then(() => {
ElMessage.success("删除成功");
handleChange(tab);
ElMessage.success('删除成功')
handleChange(tab)
})
.catch((e) => {
ElMessage.error("删除失败" + e.message);
ElMessage.error('删除失败' + e.message)
})
.finally(() => {
nextTick(() => {
// data.value[tab].page = 1;
// data.value[tab].pageSize = 10;
const mediaType = mediaTypes.find((type) => type.name === tab);
const mediaType = mediaTypes.find((type) => type.name === tab)
if (mediaType && mediaType.fetchData) {
mediaType.fetchData();
mediaType.fetchData()
}
});
});
};
})
})
}
const playMusic = (item) => {
playList.value = [item];
showPlayer.value = true;
nextTick(() => playerRef.value.play());
};
playList.value = [item]
showPlayer.value = true
nextTick(() => playerRef.value.play())
}
const playVideo = (item) => {
currentVideoUrl.value = replaceImg(item.video_url);
showVideoDialog.value = true;
};
currentVideoUrl.value = replaceImg(item.video_url)
showVideoDialog.value = true
}
const md = require("markdown-it")({
const md = MarkdownIt({
breaks: true,
html: true,
linkify: true,
});
})
const showLyric = (item) => {
showLyricDialog.value = true;
lyrics.value = md.render(item.prompt);
};
showLyricDialog.value = true
lyrics.value = md.render(item.prompt)
}
</script>
<style lang="stylus" scoped>

View File

@@ -2,46 +2,43 @@
<div class="app-background">
<div class="container mobile-chat-list">
<van-nav-bar
:title="title"
left-text="新建会话"
@click-left="showPicker = true"
custom-class="navbar"
:title="title"
left-text="新建会话"
@click-left="showPicker = true"
custom-class="navbar"
>
<template #right>
<van-icon name="delete-o" @click="clearAllChatHistory"/>
<van-icon name="delete-o" @click="clearAllChatHistory" />
</template>
</van-nav-bar>
<div class="content">
<van-search
v-model="chatName"
input-align="center"
placeholder="请输入会话标题"
custom-class="van-search"
@input="search"
v-model="chatName"
input-align="center"
placeholder="请输入会话标题"
custom-class="van-search"
@input="search"
/>
<van-list
v-model:error="error"
v-model:loading="loading"
:finished="finished"
error-text="请求失败点击重新加载"
finished-text="没有更多了"
@load="onLoad"
v-model:error="error"
v-model:loading="loading"
:finished="finished"
error-text="请求失败点击重新加载"
finished-text="没有更多了"
@load="onLoad"
>
<van-swipe-cell v-for="item in chats" :key="item.id">
<van-cell @click="changeChat(item)">
<div class="chat-list-item">
<van-image
:src="item.icon"
round
/>
<van-image :src="item.icon" round />
<div class="van-ellipsis">{{ item.title }}</div>
</div>
</van-cell>
<template #right>
<van-button square text="修改" type="primary" @click="editChat(item)"/>
<van-button square text="删除" type="danger" @click="removeChat(item)"/>
<van-button square text="修改" type="primary" @click="editChat(item)" />
<van-button square text="删除" type="danger" @click="removeChat(item)" />
</template>
</van-swipe-cell>
</van-list>
@@ -50,43 +47,43 @@
<van-popup v-model:show="showPicker" position="bottom" class="popup">
<van-picker
:columns="columns"
title="选择模型和角色"
@change="onChange"
@cancel="showPicker = false"
@confirm="newChat"
:columns="columns"
title="选择模型和角色"
@change="onChange"
@cancel="showPicker = false"
@confirm="newChat"
>
<template #option="item">
<div class="picker-option">
<van-image
v-if="item.icon"
:src="item.icon"
fit="cover"
round
/>
<van-image v-if="item.icon" :src="item.icon" fit="cover" round />
<span>{{ item.text }}</span>
</div>
</template>
</van-picker>
</van-popup>
<van-dialog v-model:show="showEditChat" title="修改对话标题" show-cancel-button class="dialog" @confirm="saveTitle">
<van-field v-model="tmpChatTitle" label="" placeholder="请输入对话标题" class="field"/>
<van-dialog
v-model:show="showEditChat"
title="修改对话标题"
show-cancel-button
class="dialog"
@confirm="saveTitle"
>
<van-field v-model="tmpChatTitle" label="" placeholder="请输入对话标题" class="field" />
</van-dialog>
</div>
</template>
<script setup>
import {ref} from "vue";
import {httpGet, httpPost} from "@/utils/http";
import {showConfirmDialog, showFailToast, showSuccessToast} from "vant";
import {checkSession} from "@/store/cache";
import {router} from "@/router";
import {removeArrayItem, showLoginDialog} from "@/utils/libs";
import { router } from '@/router'
import { checkSession } from '@/store/cache'
import { httpGet, httpPost } from '@/utils/http'
import { removeArrayItem, showLoginDialog } from '@/utils/libs'
import { showConfirmDialog, showFailToast, showSuccessToast } from 'vant'
import { ref } from 'vue'
const title = ref("会话列表")
const chatName = ref("")
const title = ref('会话列表')
const chatName = ref('')
const chats = ref([])
const allChats = ref([])
const loading = ref(false)
@@ -100,105 +97,118 @@ const showPicker = ref(false)
const columns = ref([roles.value, models.value])
const showEditChat = ref(false)
const item = ref({})
const tmpChatTitle = ref("")
const tmpChatTitle = ref('')
checkSession().then((user) => {
loginUser.value = user
isLogin.value = true
// 加载角色列表
httpGet(`/api/app/list/user`).then((res) => {
if (res.data) {
const items = res.data
for (let i = 0; i < items.length; i++) {
// console.log(items[i])
roles.value.push({
text: items[i].name,
value: items[i].id,
icon: items[i].icon,
helloMsg: items[i].hello_msg,
model_id: items[i].model_id
})
}
}
}).catch(() => {
showFailToast("加载聊天角色失败")
checkSession()
.then((user) => {
loginUser.value = user
isLogin.value = true
// 加载角色列表
httpGet(`/api/app/list/user`)
.then((res) => {
if (res.data) {
const items = res.data
for (let i = 0; i < items.length; i++) {
// console.log(items[i])
roles.value.push({
text: items[i].name,
value: items[i].id,
icon: items[i].icon,
helloMsg: items[i].hello_msg,
model_id: items[i].model_id,
})
}
}
})
.catch(() => {
showFailToast('加载聊天角色失败')
})
// 加载模型
httpGet('/api/model/list?enable=1')
.then((res) => {
if (res.data) {
const items = res.data
for (let i = 0; i < items.length; i++) {
models.value.push({ text: items[i].name, value: items[i].id })
}
}
})
.catch((e) => {
showFailToast('加载模型失败: ' + e.message)
})
})
.catch(() => {
loading.value = false
finished.value = true
// 加载模型
httpGet('/api/model/list?enable=1').then(res => {
if (res.data) {
const items = res.data
for (let i = 0; i < items.length; i++) {
models.value.push({text: items[i].name, value: items[i].id})
}
}
}).catch(e => {
showFailToast("加载模型失败: " + e.message)
// 加载角色列表
httpGet('/api/app/list/user')
.then((res) => {
if (res.data) {
const items = res.data
for (let i = 0; i < items.length; i++) {
// console.log(items[i])
roles.value.push({
text: items[i].name,
value: items[i].id,
icon: items[i].icon,
helloMsg: items[i].hello_msg,
})
}
}
})
.catch(() => {
showFailToast('加载聊天角色失败')
})
// 加载模型
httpGet('/api/model/list')
.then((res) => {
if (res.data) {
const items = res.data
for (let i = 0; i < items.length; i++) {
models.value.push({ text: items[i].name, value: items[i].id })
}
}
})
.catch((e) => {
showFailToast('加载模型失败: ' + e.message)
})
})
}).catch(() => {
loading.value = false
finished.value = true
// 加载角色列表
httpGet('/api/app/list/user').then((res) => {
if (res.data) {
const items = res.data
for (let i = 0; i < items.length; i++) {
// console.log(items[i])
roles.value.push({
text: items[i].name,
value: items[i].id,
icon: items[i].icon,
helloMsg: items[i].hello_msg
})
}
}
}).catch(() => {
showFailToast("加载聊天角色失败")
})
// 加载模型
httpGet('/api/model/list').then(res => {
if (res.data) {
const items = res.data
for (let i = 0; i < items.length; i++) {
models.value.push({text: items[i].name, value: items[i].id})
}
}
}).catch(e => {
showFailToast("加载模型失败: " + e.message)
})
})
const onLoad = () => {
checkSession().then(() => {
httpGet("/api/chat/list?user_id=" + loginUser.value.id).then((res) => {
if (res.data) {
chats.value = res.data;
allChats.value = res.data;
finished.value = true
}
loading.value = false;
}).catch(() => {
error.value = true
showFailToast("加载会话列表失败")
checkSession()
.then(() => {
httpGet('/api/chat/list?user_id=' + loginUser.value.id)
.then((res) => {
if (res.data) {
chats.value = res.data
allChats.value = res.data
finished.value = true
}
loading.value = false
})
.catch(() => {
error.value = true
showFailToast('加载会话列表失败')
})
})
}).catch(() => {})
};
.catch(() => {})
}
const search = () => {
if (chatName.value === '') {
chats.value = allChats.value
return
}
const items = [];
const items = []
for (let i = 0; i < allChats.value.length; i++) {
if (allChats.value[i].title.toLowerCase().indexOf(chatName.value.toLowerCase()) !== -1) {
items.push(allChats.value[i]);
items.push(allChats.value[i])
}
}
chats.value = items;
chats.value = items
}
const clearAllChatHistory = () => {
@@ -208,17 +218,21 @@ const clearAllChatHistory = () => {
showConfirmDialog({
title: '操作提示',
message: '确定要删除所有的会话记录吗?'
}).then(() => {
httpGet("/api/chat/clear").then(() => {
showSuccessToast('所有聊天记录已清空')
chats.value = [];
}).catch(e => {
showFailToast("操作失败:" + e.message)
})
}).catch(() => {
// on cancel
message: '确定要删除所有的会话记录吗?',
})
.then(() => {
httpGet('/api/chat/clear')
.then(() => {
showSuccessToast('所有聊天记录已清空')
chats.value = []
})
.catch((e) => {
showFailToast('操作失败:' + e.message)
})
})
.catch(() => {
// on cancel
})
}
const newChat = (item) => {
@@ -227,7 +241,9 @@ const newChat = (item) => {
}
showPicker.value = false
const options = item.selectedOptions
router.push(`/mobile/chat/session?title=新对话&role_id=${options[0].value}&model_id=${options[1].value}`)
router.push(
`/mobile/chat/session?title=新对话&role_id=${options[0].value}&model_id=${options[1].value}`
)
}
const changeChat = (chat) => {
@@ -240,40 +256,42 @@ const editChat = (row) => {
tmpChatTitle.value = row.title
}
const saveTitle = () => {
httpPost('/api/chat/update', {chat_id: item.value.chat_id, title: tmpChatTitle.value}).then(() => {
showSuccessToast("操作成功!");
item.value.title = tmpChatTitle.value;
}).catch(e => {
showFailToast("操作失败:" + e.message);
})
httpPost('/api/chat/update', { chat_id: item.value.chat_id, title: tmpChatTitle.value })
.then(() => {
showSuccessToast('操作成功!')
item.value.title = tmpChatTitle.value
})
.catch((e) => {
showFailToast('操作失败:' + e.message)
})
}
const removeChat = (item) => {
httpGet('/api/chat/remove?chat_id=' + item.chat_id).then(() => {
chats.value = removeArrayItem(chats.value, item, function (e1, e2) {
return e1.id === e2.id
httpGet('/api/chat/remove?chat_id=' + item.chat_id)
.then(() => {
chats.value = removeArrayItem(chats.value, item, function (e1, e2) {
return e1.id === e2.id
})
})
.catch((e) => {
showFailToast('操作失败:' + e.message)
})
}).catch(e => {
showFailToast('操作失败:' + e.message);
})
}
const onChange = (item) => {
const selectedValues = item.selectedOptions
if (selectedValues[0].model_id) {
for (let i = 0; i < columns.value[1].length; i++) {
columns.value[1][i].disabled = columns.value[1][i].value !== selectedValues[0].model_id;
columns.value[1][i].disabled = columns.value[1][i].value !== selectedValues[0].model_id
}
} else {
for (let i = 0; i < columns.value[1].length; i++) {
columns.value[1][i].disabled = false;
columns.value[1][i].disabled = false
}
}
}
</script>
<style lang="stylus" scoped>
@import "@/assets/css/mobile/chat-list.styl"
</style>
@import '../../assets/css/mobile/chat-list.styl'
</style>

View File

@@ -601,6 +601,6 @@ const onChange = (item) => {
}
</script>
<style lang="stylus">
@import "@/assets/css/mobile/chat-session.styl"
<style lang="stylus" scoped>
@import "../../assets/css/mobile/chat-session.styl"
</style>

View File

@@ -1,37 +1,36 @@
<template>
<van-config-provider :theme="theme">
<div class="mobile-home">
<router-view/>
<router-view />
<van-tabbar route v-model="active">
<van-tabbar-item to="/mobile/index" name="home" icon="home-o">首页</van-tabbar-item>
<van-tabbar-item to="/mobile/chat" name="chat" icon="chat-o">对话</van-tabbar-item>
<van-tabbar-item to="/mobile/image" name="image" icon="photo-o">绘图</van-tabbar-item>
<van-tabbar-item to="/mobile/profile" name="profile" icon="user-o">我的
</van-tabbar-item>
<van-tabbar-item to="/mobile/profile" name="profile" icon="user-o">我的 </van-tabbar-item>
</van-tabbar>
</div>
</van-config-provider>
</template>
<script setup>
import {ref, watch} from "vue";
import {useSharedStore} from "@/store/sharedata";
import { useSharedStore } from '@/store/sharedata'
import { ref, watch } from 'vue'
const active = ref('home')
const store = useSharedStore()
const theme = ref(store.theme)
watch(() => store.theme, (val) => {
theme.value = val
})
watch(
() => store.theme,
(val) => {
theme.value = val
}
)
</script>
<style lang="stylus">
@import '@/assets/iconfont/iconfont.css';
@import '../../assets/iconfont/iconfont.css';
.mobile-home {
.container {
.van-nav-bar {
@@ -53,4 +52,4 @@ watch(() => store.theme, (val) => {
position fixed
width 100%
}
</style>
</style>

View File

@@ -3,29 +3,65 @@
<van-form @submit="generate">
<van-cell-group inset>
<div>
<van-field v-model="selectedModel" is-link label="生图模型" placeholder="选择生图模型" @click="showModelPicker = true" />
<van-field
v-model="selectedModel"
is-link
label="生图模型"
placeholder="选择生图模型"
@click="showModelPicker = true"
/>
<van-popup v-model:show="showModelPicker" position="bottom" teleport="#app">
<van-picker :columns="models" @cancel="showModelPicker = false" @confirm="modelConfirm" />
<van-picker
:columns="models"
@cancel="showModelPicker = false"
@confirm="modelConfirm"
/>
</van-popup>
</div>
<div>
<van-field v-model="quality" is-link label="图片质量" placeholder="选择图片质量" @click="showQualityPicker = true" />
<van-field
v-model="quality"
is-link
label="图片质量"
placeholder="选择图片质量"
@click="showQualityPicker = true"
/>
<van-popup v-model:show="showQualityPicker" position="bottom" teleport="#app">
<van-picker :columns="qualities" @cancel="showQualityPicker = false" @confirm="qualityConfirm" />
<van-picker
:columns="qualities"
@cancel="showQualityPicker = false"
@confirm="qualityConfirm"
/>
</van-popup>
</div>
<div>
<van-field v-model="size" is-link label="图片尺寸" placeholder="选择图片尺寸" @click="showSizePicker = true" />
<van-field
v-model="size"
is-link
label="图片尺寸"
placeholder="选择图片尺寸"
@click="showSizePicker = true"
/>
<van-popup v-model:show="showSizePicker" position="bottom" teleport="#app">
<van-picker :columns="sizes" @cancel="showSizePicker = false" @confirm="sizeConfirm" />
</van-popup>
</div>
<div>
<van-field v-model="style" is-link label="图片样式" placeholder="选择图片样式" @click="showStylePicker = true" />
<van-field
v-model="style"
is-link
label="图片样式"
placeholder="选择图片样式"
@click="showStylePicker = true"
/>
<van-popup v-model:show="showStylePicker" position="bottom" teleport="#app">
<van-picker :columns="styles" @cancel="showStylePicker = false" @confirm="styleConfirm" />
<van-picker
:columns="styles"
@cancel="showStylePicker = false"
@confirm="styleConfirm"
/>
</van-popup>
</div>
@@ -61,7 +97,14 @@
<div v-if="item.progress > 0">
<van-image src="/images/img-holder.png"></van-image>
<div class="progress">
<van-circle v-model:current-rate="item.progress" :rate="item.progress" :speed="100" :text="item.progress + '%'" :stroke-width="60" size="90px" />
<van-circle
v-model:current-rate="item.progress"
:rate="item.progress"
:speed="100"
:text="item.progress + '%'"
:stroke-width="60"
size="90px"
/>
</div>
</div>
@@ -97,11 +140,19 @@
<div class="title">任务失败</div>
<div class="opt">
<van-button size="small" @click="showErrMsg(item)">详情</van-button>
<van-button type="danger" @click="removeImage($event, item)" size="small">删除</van-button>
<van-button type="danger" @click="removeImage($event, item)" size="small"
>删除</van-button
>
</div>
</div>
<div class="job-item" v-else>
<van-image :src="item['img_url']" :class="item['can_opt'] ? '' : 'upscale'" lazy-load @click="imageView(item)" fit="cover">
<van-image
:src="item['img_url']"
:class="item['can_opt'] ? '' : 'upscale'"
lazy-load
@click="imageView(item)"
fit="cover"
>
<template v-slot:loading>
<van-loading type="spinner" size="20" />
</template>
@@ -109,7 +160,12 @@
<div class="remove">
<el-button type="danger" :icon="Delete" @click="removeImage($event, item)" circle />
<el-button type="warning" v-if="item.publish" @click="publishImage($event, item, false)" circle>
<el-button
type="warning"
v-if="item.publish"
@click="publishImage($event, item, false)"
circle
>
<i class="iconfont icon-cancel-share"></i>
</el-button>
<el-button type="success" v-else @click="publishImage($event, item, true)" circle>
@@ -124,324 +180,343 @@
</van-grid>
</van-list>
</div>
<button style="display: none" class="copy-prompt-dall" :data-clipboard-text="prompt" id="copy-btn-dall">复制</button>
<button
style="display: none"
class="copy-prompt-dall"
:data-clipboard-text="prompt"
id="copy-btn-dall"
>
复制
</button>
</div>
</template>
<script setup>
import { onMounted, onUnmounted, ref } from "vue";
import { Delete } from "@element-plus/icons-vue";
import { httpGet, httpPost } from "@/utils/http";
import Clipboard from "clipboard";
import { checkSession, getSystemInfo } from "@/store/cache";
import { useRouter } from "vue-router";
import { getSessionId } from "@/store/session";
import { showConfirmDialog, showDialog, showFailToast, showImagePreview, showNotify, showSuccessToast, showToast } from "vant";
import { showLoginDialog } from "@/utils/libs";
import { useSharedStore } from "@/store/sharedata";
import { checkSession, getSystemInfo } from '@/store/cache'
import { getSessionId } from '@/store/session'
import { useSharedStore } from '@/store/sharedata'
import { httpGet, httpPost } from '@/utils/http'
import { showLoginDialog } from '@/utils/libs'
import { Delete } from '@element-plus/icons-vue'
import Clipboard from 'clipboard'
import {
showConfirmDialog,
showDialog,
showFailToast,
showImagePreview,
showNotify,
showSuccessToast,
showToast,
} from 'vant'
import { onMounted, onUnmounted, ref } from 'vue'
import { useRouter } from 'vue-router'
const listBoxHeight = ref(window.innerHeight - 40);
const mjBoxHeight = ref(window.innerHeight - 150);
const isLogin = ref(false);
const listBoxHeight = ref(window.innerHeight - 40)
const mjBoxHeight = ref(window.innerHeight - 150)
const isLogin = ref(false)
window.onresize = () => {
listBoxHeight.value = window.innerHeight - 40;
mjBoxHeight.value = window.innerHeight - 150;
};
listBoxHeight.value = window.innerHeight - 40
mjBoxHeight.value = window.innerHeight - 150
}
const qualities = [
{ text: "标准", value: "standard" },
{ text: "高清", value: "hd" },
];
{ text: '标准', value: 'standard' },
{ text: '高清', value: 'hd' },
]
const fluxSizes = [
{ text: "1024x1024", value: "1024x1024" },
{ text: "1024x768", value: "1024x768" },
{ text: "768x1024", value: "768x1024" },
{ text: "1280x960", value: "1280x960" },
{ text: "960x1280", value: "960x1280" },
{ text: "1366x768", value: "1366x768" },
{ text: "768x1366", value: "768x1366" },
];
{ text: '1024x1024', value: '1024x1024' },
{ text: '1024x768', value: '1024x768' },
{ text: '768x1024', value: '768x1024' },
{ text: '1280x960', value: '1280x960' },
{ text: '960x1280', value: '960x1280' },
{ text: '1366x768', value: '1366x768' },
{ text: '768x1366', value: '768x1366' },
]
const dalleSizes = [
{ text: "1024x1024", value: "1024x1024" },
{ text: "1792x1024", value: "1792x1024" },
{ text: "1024x1792", value: "1024x1792" },
];
{ text: '1024x1024', value: '1024x1024' },
{ text: '1792x1024', value: '1792x1024' },
{ text: '1024x1792', value: '1024x1792' },
]
let sizes = dalleSizes;
let sizes = dalleSizes
const styles = [
{ text: "生动", value: "vivid" },
{ text: "自然", value: "natural" },
];
{ text: '生动', value: 'vivid' },
{ text: '自然', value: 'natural' },
]
const params = ref({
quality: qualities[0].value,
size: sizes[0].value,
style: styles[0].value,
prompt: "",
});
const quality = ref(qualities[0].text);
const size = ref(sizes[0].text);
const style = ref(styles[0].text);
prompt: '',
})
const quality = ref(qualities[0].text)
const size = ref(sizes[0].text)
const style = ref(styles[0].text)
const showQualityPicker = ref(false);
const showStylePicker = ref(false);
const showSizePicker = ref(false);
const showModelPicker = ref(false);
const showQualityPicker = ref(false)
const showStylePicker = ref(false)
const showSizePicker = ref(false)
const showModelPicker = ref(false)
const runningJobs = ref([]);
const finishedJobs = ref([]);
const allowPulling = ref(true); // 是否允许轮询
const tastPullHandler = ref(null);
const router = useRouter();
const power = ref(0);
const dallPower = ref(0); // 画一张 DALL 图片消耗算力
const runningJobs = ref([])
const finishedJobs = ref([])
const allowPulling = ref(true) // 是否允许轮询
const tastPullHandler = ref(null)
const router = useRouter()
const power = ref(0)
const dallPower = ref(0) // 画一张 DALL 图片消耗算力
const userId = ref(0);
const store = useSharedStore();
const clipboard = ref(null);
const prompt = ref("");
const models = ref([]);
const selectedModel = ref(null);
const userId = ref(0)
const store = useSharedStore()
const clipboard = ref(null)
const prompt = ref('')
const models = ref([])
const selectedModel = ref(null)
onMounted(() => {
initData();
clipboard.value = new Clipboard(".copy-prompt-dall");
clipboard.value.on("success", () => {
showNotify({ type: "success", message: "复制成功", duration: 1000 });
});
clipboard.value.on("error", () => {
showNotify({ type: "danger", message: "复制失败", duration: 2000 });
});
initData()
clipboard.value = new Clipboard('.copy-prompt-dall')
clipboard.value.on('success', () => {
showNotify({ type: 'success', message: '复制成功', duration: 1000 })
})
clipboard.value.on('error', () => {
showNotify({ type: 'danger', message: '复制失败', duration: 2000 })
})
getSystemInfo()
.then((res) => {
dallPower.value = res.data.dall_power;
dallPower.value = res.data.dall_power
})
.catch((e) => {
showNotify({ type: "danger", message: "获取系统配置失败:" + e.message });
});
showNotify({ type: 'danger', message: '获取系统配置失败:' + e.message })
})
// 获取模型列表
httpGet("/api/dall/models")
httpGet('/api/dall/models')
.then((res) => {
for (let i = 0; i < res.data.length; i++) {
models.value.push({ text: res.data[i].name, value: res.data[i].id, name: res.data[i].value });
models.value.push({
text: res.data[i].name,
value: res.data[i].id,
name: res.data[i].value,
})
}
selectedModel.value = models.value[0]?.text;
params.value.model_id = models.value[0]?.value;
selectedModel.value = models.value[0]?.text
params.value.model_id = models.value[0]?.value
})
.catch((e) => {
showMessageError("获取模型列表失败:" + e.message);
});
});
showMessageError('获取模型列表失败:' + e.message)
})
})
onUnmounted(() => {
clipboard.value.destroy();
clipboard.value.destroy()
if (tastPullHandler.value) {
clearInterval(tastPullHandler.value);
clearInterval(tastPullHandler.value)
}
});
})
const initData = () => {
checkSession()
.then((user) => {
power.value = user["power"];
isLogin.value = true;
fetchRunningJobs();
fetchFinishJobs(1);
power.value = user['power']
isLogin.value = true
fetchRunningJobs()
fetchFinishJobs(1)
tastPullHandler.value = setInterval(() => {
if (allowPulling.value) {
fetchRunningJobs();
fetchRunningJobs()
}
}, 5000);
}, 5000)
})
.catch(() => {
loading.value = false;
});
};
loading.value = false
})
}
const fetchRunningJobs = () => {
// 获取运行中的任务
httpGet(`/api/dall/jobs?finish=0`)
.then((res) => {
if (runningJobs.value.length !== res.data.items.length) {
fetchFinishJobs(1);
fetchFinishJobs(1)
}
if (res.data.items.length === 0) {
allowPulling.value = false;
allowPulling.value = false
}
runningJobs.value = res.data.items;
runningJobs.value = res.data.items
})
.catch((e) => {
showNotify({ type: "danger", message: "获取任务失败:" + e.message });
});
};
showNotify({ type: 'danger', message: '获取任务失败:' + e.message })
})
}
const loading = ref(false);
const finished = ref(false);
const error = ref(false);
const page = ref(0);
const pageSize = ref(10);
const loading = ref(false)
const finished = ref(false)
const error = ref(false)
const page = ref(0)
const pageSize = ref(10)
// 获取已完成的任务
const fetchFinishJobs = (page) => {
loading.value = true;
loading.value = true
httpGet(`/api/dall/jobs?finish=1&page=${page}&page_size=${pageSize.value}`)
.then((res) => {
const jobs = res.data.items;
const jobs = res.data.items
if (jobs.length < pageSize.value) {
finished.value = true;
finished.value = true
}
const _jobs = [];
const _jobs = []
for (let i = 0; i < jobs.length; i++) {
if (jobs[i].progress === -1) {
jobs[i]["thumb_url"] = jobs[i]["img_url"] + "?imageView2/1/w/480/h/600/q/75";
jobs[i]['thumb_url'] = jobs[i]['img_url'] + '?imageView2/1/w/480/h/600/q/75'
}
_jobs.push(jobs[i]);
_jobs.push(jobs[i])
}
if (page === 1) {
finishedJobs.value = _jobs;
finishedJobs.value = _jobs
} else {
finishedJobs.value = finishedJobs.value.concat(_jobs);
finishedJobs.value = finishedJobs.value.concat(_jobs)
}
loading.value = false;
loading.value = false
})
.catch((e) => {
loading.value = false;
showNotify({ type: "danger", message: "获取任务失败:" + e.message });
});
};
loading.value = false
showNotify({ type: 'danger', message: '获取任务失败:' + e.message })
})
}
const onLoad = () => {
page.value += 1;
fetchFinishJobs(page.value);
};
page.value += 1
fetchFinishJobs(page.value)
}
// 创建绘图任务
const promptRef = ref(null);
const promptRef = ref(null)
const generate = () => {
if (!isLogin.value) {
return showLoginDialog(router);
return showLoginDialog(router)
}
if (params.value.prompt === "") {
promptRef.value.focus();
return showToast("请输入绘画提示词!");
if (params.value.prompt === '') {
promptRef.value.focus()
return showToast('请输入绘画提示词!')
}
if (!params.value.seed) {
params.value.seed = -1;
params.value.seed = -1
}
params.value.session_id = getSessionId();
httpPost("/api/dall/image", params.value)
params.value.session_id = getSessionId()
httpPost('/api/dall/image', params.value)
.then(() => {
showSuccessToast("绘画任务推送成功,请耐心等待任务执行...");
power.value -= dallPower.value;
allowPulling.value = true;
showSuccessToast('绘画任务推送成功,请耐心等待任务执行...')
power.value -= dallPower.value
allowPulling.value = true
runningJobs.value.push({
progress: 0,
});
})
})
.catch((e) => {
showFailToast("任务推送失败:" + e.message);
});
};
showFailToast('任务推送失败:' + e.message)
})
}
const showPrompt = (item) => {
prompt.value = item.prompt;
prompt.value = item.prompt
showConfirmDialog({
title: "绘画提示词",
title: '绘画提示词',
message: item.prompt,
confirmButtonText: "复制",
cancelButtonText: "关闭",
confirmButtonText: '复制',
cancelButtonText: '关闭',
})
.then(() => {
document.querySelector("#copy-btn-dall").click();
document.querySelector('#copy-btn-dall').click()
})
.catch(() => {});
};
.catch(() => {})
}
const showErrMsg = (item) => {
showDialog({
title: "错误详情",
message: item["err_msg"],
title: '错误详情',
message: item['err_msg'],
}).then(() => {
// on close
});
};
})
}
const removeImage = (event, item) => {
event.stopPropagation();
event.stopPropagation()
showConfirmDialog({
title: "标题",
message: "此操作将会删除任务和图片,继续操作码?",
title: '标题',
message: '此操作将会删除任务和图片,继续操作码?',
})
.then(() => {
httpGet("/api/dall/remove", { id: item.id, user_id: item.user_id })
httpGet('/api/dall/remove', { id: item.id, user_id: item.user_id })
.then(() => {
showSuccessToast("任务删除成功");
fetchFinishJobs(1);
showSuccessToast('任务删除成功')
fetchFinishJobs(1)
})
.catch((e) => {
showFailToast("任务删除失败:" + e.message);
});
showFailToast('任务删除失败:' + e.message)
})
})
.catch(() => {
showToast("您取消了操作");
});
};
showToast('您取消了操作')
})
}
// 发布图片到作品墙
const publishImage = (event, item, action) => {
event.stopPropagation();
let text = "图片发布";
event.stopPropagation()
let text = '图片发布'
if (action === false) {
text = "取消发布";
text = '取消发布'
}
httpGet("/api/dall/publish", { id: item.id, action: action, user_id: item.user_id })
httpGet('/api/dall/publish', { id: item.id, action: action, user_id: item.user_id })
.then(() => {
showSuccessToast(text + "成功");
item.publish = action;
showSuccessToast(text + '成功')
item.publish = action
})
.catch((e) => {
showFailToast(text + "失败:" + e.message);
});
};
showFailToast(text + '失败:' + e.message)
})
}
const imageView = (item) => {
showImagePreview([item["img_url"]]);
};
showImagePreview([item['img_url']])
}
const qualityConfirm = (item) => {
params.value.quality = item.selectedOptions[0].value;
quality.value = item.selectedOptions[0].text;
showQualityPicker.value = false;
};
params.value.quality = item.selectedOptions[0].value
quality.value = item.selectedOptions[0].text
showQualityPicker.value = false
}
const styleConfirm = (item) => {
params.value.style = item.selectedOptions[0].value;
style.value = item.selectedOptions[0].text;
showStylePicker.value = false;
};
params.value.style = item.selectedOptions[0].value
style.value = item.selectedOptions[0].text
showStylePicker.value = false
}
const sizeConfirm = (item) => {
params.value.size = item.selectedOptions[0].value;
size.value = item.selectedOptions[0].text;
showSizePicker.value = false;
};
params.value.size = item.selectedOptions[0].value
size.value = item.selectedOptions[0].text
showSizePicker.value = false
}
const modelConfirm = (item) => {
params.value.model_id = item.selectedOptions[0].value;
selectedModel.value = item.selectedOptions[0].text;
showModelPicker.value = false;
if (item.selectedOptions[0].name.startsWith("dall")) {
sizes = dalleSizes;
params.value.model_id = item.selectedOptions[0].value
selectedModel.value = item.selectedOptions[0].text
showModelPicker.value = false
if (item.selectedOptions[0].name.startsWith('dall')) {
sizes = dalleSizes
} else {
sizes = fluxSizes;
sizes = fluxSizes
}
};
}
</script>
<style lang="stylus">
@import "@/assets/css/mobile/image-sd.styl"
@import '../../../assets/css/mobile/image-sd.styl'
</style>

View File

@@ -5,7 +5,10 @@
<div class="text-line">
<van-row :gutter="10">
<van-col :span="4" v-for="item in rates" :key="item.value">
<div :class="item.value === params.rate ? 'rate active' : 'rate'" @click="changeRate(item)">
<div
:class="item.value === params.rate ? 'rate active' : 'rate'"
@click="changeRate(item)"
>
<div class="icon">
<van-image :src="item.img" fit="cover"></van-image>
</div>
@@ -18,7 +21,10 @@
<div class="text-line">
<van-row :gutter="10">
<van-col :span="8" v-for="item in models" :key="item.value">
<div :class="item.value === params.model ? 'model active' : 'model'" @click="changeModel(item)">
<div
:class="item.value === params.model ? 'model active' : 'model'"
@click="changeModel(item)"
>
<div class="icon">
<van-image :src="item.img" fit="cover"></van-image>
</div>
@@ -32,7 +38,12 @@
<div class="text-line">
<van-field label="创意度">
<template #input>
<van-slider v-model.number="params.chaos" :max="100" :step="1" @update:model-value="showToast('当前值' + params.chaos)" />
<van-slider
v-model.number="params.chaos"
:max="100"
:step="1"
@update:model-value="showToast('当前值' + params.chaos)"
/>
</template>
</van-field>
</div>
@@ -40,7 +51,12 @@
<div class="text-line">
<van-field label="风格化">
<template #input>
<van-slider v-model.number="params.stylize" :max="1000" :step="1" @update:model-value="showToast('当前值' + params.stylize)" />
<van-slider
v-model.number="params.stylize"
:max="1000"
:step="1"
@update:model-value="showToast('当前值' + params.stylize)"
/>
</template>
</van-field>
</div>
@@ -85,14 +101,27 @@
<div class="text-line">
<van-field label="垫图权重">
<template #input>
<van-slider v-model.number="params.iw" :max="1" :step="0.01" @update:model-value="showToast('当前值' + params.iw)" />
<van-slider
v-model.number="params.iw"
:max="1"
:step="0.01"
@update:model-value="showToast('当前值' + params.iw)"
/>
</template>
</van-field>
</div>
<div class="tip-text">提示只有于 niji6 v6 模型支持一致性功能如果选择其他模型此功能将会生成失败</div>
<div class="tip-text">
提示只有于 niji6 v6 模型支持一致性功能如果选择其他模型此功能将会生成失败
</div>
<van-cell-group>
<van-field v-model="params.cref" center clearable label="角色一致性" placeholder="请输入图片URL或者上传图片">
<van-field
v-model="params.cref"
center
clearable
label="角色一致性"
placeholder="请输入图片URL或者上传图片"
>
<template #button>
<van-uploader @click="beforeUpload('cref')" :after-read="uploadImg">
<van-button size="mini" type="primary" icon="plus" />
@@ -102,7 +131,13 @@
</van-cell-group>
<van-cell-group>
<van-field v-model="params.sref" center clearable label="风格一致性" placeholder="请输入图片URL或者上传图片">
<van-field
v-model="params.sref"
center
clearable
label="风格一致性"
placeholder="请输入图片URL或者上传图片"
>
<template #button>
<van-uploader @click="beforeUpload('sref')" :after-read="uploadImg">
<van-button size="mini" type="primary" icon="plus" />
@@ -114,13 +149,20 @@
<div class="text-line">
<van-field label="一致性权重">
<template #input>
<van-slider v-model.number="params.cw" :max="100" :step="1" @update:model-value="showToast('当前值' + params.cw)" />
<van-slider
v-model.number="params.cw"
:max="100"
:step="1"
@update:model-value="showToast('当前值' + params.cw)"
/>
</template>
</van-field>
</div>
</van-tab>
<van-tab title="融图" name="blend">
<div class="tip-text">请上传两张以上的图片最多不超过五张超过五张图片请使用图生图功能</div>
<div class="tip-text">
请上传两张以上的图片最多不超过五张超过五张图片请使用图生图功能
</div>
<div class="text-line">
<van-uploader v-model="imgList" :after-read="uploadImg" />
</div>
@@ -137,13 +179,24 @@
<div class="text-line">
<van-collapse v-model="activeColspan">
<van-collapse-item title="反向提示词" name="neg_prompt">
<van-field v-model="params.neg_prompt" rows="3" maxlength="2000" autosize type="textarea" placeholder="不想出现在图片上的元素(例如:树,建筑)" />
<van-field
v-model="params.neg_prompt"
rows="3"
maxlength="2000"
autosize
type="textarea"
placeholder="不想出现在图片上的元素(例如:树,建筑)"
/>
</van-collapse-item>
</van-collapse>
</div>
<div class="text-line pt-6">
<el-tag>绘图消耗{{ mjPower }}算力U/V 操作消耗{{ mjActionPower }}算力当前算力{{ power }}</el-tag>
<el-tag
>绘图消耗{{ mjPower }}算力U/V 操作消耗{{ mjActionPower }}算力当前算力{{
power
}}</el-tag
>
</div>
<div class="text-line">
@@ -164,7 +217,14 @@
<div v-if="item.progress > 0">
<van-image src="/images/img-holder.png"></van-image>
<div class="progress">
<van-circle v-model:current-rate="item.progress" :rate="item.progress" :speed="100" :text="item.progress + '%'" :stroke-width="60" size="90px" />
<van-circle
v-model:current-rate="item.progress"
:rate="item.progress"
:speed="100"
:text="item.progress + '%'"
:stroke-width="60"
size="90px"
/>
</div>
</div>
@@ -204,7 +264,13 @@
</div>
</div>
<div class="job-item" v-else>
<van-image :src="item['thumb_url']" :class="item['can_opt'] ? '' : 'upscale'" lazy-load @click="imageView(item)" fit="cover">
<van-image
:src="item['thumb_url']"
:class="item['can_opt'] ? '' : 'upscale'"
lazy-load
@click="imageView(item)"
fit="cover"
>
<template v-slot:loading>
<van-loading type="spinner" size="20" />
</template>
@@ -220,16 +286,29 @@
<van-grid-item><a @click="upscale(2, item)" class="opt-btn">U2</a></van-grid-item>
<van-grid-item><a @click="upscale(3, item)" class="opt-btn">U3</a></van-grid-item>
<van-grid-item><a @click="upscale(4, item)" class="opt-btn">U4</a></van-grid-item>
<van-grid-item><a @click="variation(1, item)" class="opt-btn">V1</a></van-grid-item>
<van-grid-item><a @click="variation(2, item)" class="opt-btn">V2</a></van-grid-item>
<van-grid-item><a @click="variation(3, item)" class="opt-btn">V3</a></van-grid-item>
<van-grid-item><a @click="variation(4, item)" class="opt-btn">V4</a></van-grid-item>
<van-grid-item
><a @click="variation(1, item)" class="opt-btn">V1</a></van-grid-item
>
<van-grid-item
><a @click="variation(2, item)" class="opt-btn">V2</a></van-grid-item
>
<van-grid-item
><a @click="variation(3, item)" class="opt-btn">V3</a></van-grid-item
>
<van-grid-item
><a @click="variation(4, item)" class="opt-btn">V4</a></van-grid-item
>
</van-grid>
</div>
<div class="remove">
<el-button type="danger" :icon="Delete" @click="removeImage(item)" circle />
<el-button type="warning" v-if="item.publish" @click="publishImage(item, false)" circle>
<el-button
type="warning"
v-if="item.publish"
@click="publishImage(item, false)"
circle
>
<i class="iconfont icon-cancel-share"></i>
</el-button>
<el-button type="success" v-else @click="publishImage(item, true)" circle>
@@ -245,44 +324,54 @@
</van-list>
</div>
<button style="display: none" class="copy-prompt" :data-clipboard-text="prompt" id="copy-btn">复制</button>
<button style="display: none" class="copy-prompt" :data-clipboard-text="prompt" id="copy-btn">
复制
</button>
</div>
</template>
<script setup>
import { nextTick, onMounted, onUnmounted, ref } from "vue";
import { showConfirmDialog, showFailToast, showImagePreview, showNotify, showSuccessToast, showToast, showDialog } from "vant";
import { httpGet, httpPost } from "@/utils/http";
import Compressor from "compressorjs";
import { getSessionId } from "@/store/session";
import { checkSession, getSystemInfo } from "@/store/cache";
import { useRouter } from "vue-router";
import { Delete } from "@element-plus/icons-vue";
import { showLoginDialog } from "@/utils/libs";
import Clipboard from "clipboard";
import { useSharedStore } from "@/store/sharedata";
import { checkSession, getSystemInfo } from '@/store/cache'
import { getSessionId } from '@/store/session'
import { useSharedStore } from '@/store/sharedata'
import { httpGet, httpPost } from '@/utils/http'
import { showLoginDialog } from '@/utils/libs'
import { Delete } from '@element-plus/icons-vue'
import Clipboard from 'clipboard'
import Compressor from 'compressorjs'
import {
showConfirmDialog,
showDialog,
showFailToast,
showImagePreview,
showNotify,
showSuccessToast,
showToast,
} from 'vant'
import { nextTick, onMounted, onUnmounted, ref } from 'vue'
import { useRouter } from 'vue-router'
const activeColspan = ref([""]);
const activeColspan = ref([''])
const rates = [
{ css: "square", value: "1:1", text: "1:1", img: "/images/mj/rate_1_1.png" },
{ css: "size2-3", value: "2:3", text: "2:3", img: "/images/mj/rate_3_4.png" },
{ css: "size3-4", value: "3:4", text: "3:4", img: "/images/mj/rate_3_4.png" },
{ css: "size4-3", value: "4:3", text: "4:3", img: "/images/mj/rate_4_3.png" },
{ css: "size16-9", value: "16:9", text: "16:9", img: "/images/mj/rate_16_9.png" },
{ css: "size9-16", value: "9:16", text: "9:16", img: "/images/mj/rate_9_16.png" },
];
{ css: 'square', value: '1:1', text: '1:1', img: '/images/mj/rate_1_1.png' },
{ css: 'size2-3', value: '2:3', text: '2:3', img: '/images/mj/rate_3_4.png' },
{ css: 'size3-4', value: '3:4', text: '3:4', img: '/images/mj/rate_3_4.png' },
{ css: 'size4-3', value: '4:3', text: '4:3', img: '/images/mj/rate_4_3.png' },
{ css: 'size16-9', value: '16:9', text: '16:9', img: '/images/mj/rate_16_9.png' },
{ css: 'size9-16', value: '9:16', text: '9:16', img: '/images/mj/rate_9_16.png' },
]
const models = [
{ text: "MJ-6.0", value: " --v 6", img: "/images/mj/mj-v6.png" },
{ text: "MJ-5.2", value: " --v 5.2", img: "/images/mj/mj-v5.2.png" },
{ text: "Niji5", value: " --niji 5", img: "/images/mj/mj-niji.png" },
{ text: "Niji5 可爱", value: " --niji 5 --style cute", img: "/images/mj/nj1.jpg" },
{ text: "Niji5 风景", value: " --niji 5 --style scenic", img: "/images/mj/nj2.jpg" },
{ text: "Niji6", value: " --niji 6", img: "/images/mj/nj3.jpg" },
];
const imgList = ref([]);
{ text: 'MJ-6.0', value: ' --v 6', img: '/images/mj/mj-v6.png' },
{ text: 'MJ-5.2', value: ' --v 5.2', img: '/images/mj/mj-v5.2.png' },
{ text: 'Niji5', value: ' --niji 5', img: '/images/mj/mj-niji.png' },
{ text: 'Niji5 可爱', value: ' --niji 5 --style cute', img: '/images/mj/nj1.jpg' },
{ text: 'Niji5 风景', value: ' --niji 5 --style scenic', img: '/images/mj/nj2.jpg' },
{ text: 'Niji6', value: ' --niji 6', img: '/images/mj/nj3.jpg' },
]
const imgList = ref([])
const params = ref({
task_type: "image",
task_type: 'image',
rate: rates[0].value,
model: models[0].value,
chaos: 0,
@@ -291,229 +380,229 @@ const params = ref({
img_arr: [],
raw: false,
iw: 0,
prompt: "",
neg_prompt: "",
prompt: '',
neg_prompt: '',
tile: false,
quality: 0,
cref: "",
sref: "",
cref: '',
sref: '',
cw: 0,
});
const userId = ref(0);
const router = useRouter();
const runningJobs = ref([]);
const finishedJobs = ref([]);
const power = ref(0);
const activeName = ref("txt2img");
const isLogin = ref(false);
const prompt = ref("");
const store = useSharedStore();
const clipboard = ref(null);
const taskPulling = ref(true);
const tastPullHandler = ref(null);
const downloadPulling = ref(false);
const downloadPullHandler = ref(null);
})
const userId = ref(0)
const router = useRouter()
const runningJobs = ref([])
const finishedJobs = ref([])
const power = ref(0)
const activeName = ref('txt2img')
const isLogin = ref(false)
const prompt = ref('')
const store = useSharedStore()
const clipboard = ref(null)
const taskPulling = ref(true)
const tastPullHandler = ref(null)
const downloadPulling = ref(false)
const downloadPullHandler = ref(null)
onMounted(() => {
clipboard.value = new Clipboard(".copy-prompt");
clipboard.value.on("success", () => {
showNotify({ type: "success", message: "复制成功", duration: 1000 });
});
clipboard.value.on("error", () => {
showNotify({ type: "danger", message: "复制失败", duration: 2000 });
});
clipboard.value = new Clipboard('.copy-prompt')
clipboard.value.on('success', () => {
showNotify({ type: 'success', message: '复制成功', duration: 1000 })
})
clipboard.value.on('error', () => {
showNotify({ type: 'danger', message: '复制失败', duration: 2000 })
})
checkSession()
.then((user) => {
power.value = user["power"];
userId.value = user.id;
isLogin.value = true;
fetchRunningJobs();
fetchFinishJobs(1);
power.value = user['power']
userId.value = user.id
isLogin.value = true
fetchRunningJobs()
fetchFinishJobs(1)
tastPullHandler.value = setInterval(() => {
if (taskPulling.value) {
fetchRunningJobs();
fetchRunningJobs()
}
}, 5000);
}, 5000)
downloadPullHandler.value = setInterval(() => {
if (downloadPulling.value) {
page.value = 1;
fetchFinishJobs(1);
page.value = 1
fetchFinishJobs(1)
}
}, 5000);
}, 5000)
})
.catch(() => {
// router.push('/login')
});
});
})
})
onUnmounted(() => {
clipboard.value.destroy();
clipboard.value.destroy()
if (tastPullHandler.value) {
clearInterval(tastPullHandler.value);
clearInterval(tastPullHandler.value)
}
if (downloadPullHandler.value) {
clearInterval(downloadPullHandler.value);
clearInterval(downloadPullHandler.value)
}
});
})
const mjPower = ref(1);
const mjActionPower = ref(1);
const mjPower = ref(1)
const mjActionPower = ref(1)
getSystemInfo()
.then((res) => {
mjPower.value = res.data["mj_power"];
mjActionPower.value = res.data["mj_action_power"];
mjPower.value = res.data['mj_power']
mjActionPower.value = res.data['mj_action_power']
})
.catch((e) => {
showNotify({ type: "danger", message: "获取系统配置失败:" + e.message });
});
showNotify({ type: 'danger', message: '获取系统配置失败:' + e.message })
})
// 获取运行中的任务
const fetchRunningJobs = (userId) => {
if (!isLogin.value) {
return;
return
}
httpGet(`/api/mj/jobs?finish=0&user_id=${userId}`)
.then((res) => {
const jobs = res.data.items;
const _jobs = [];
const jobs = res.data.items
const _jobs = []
for (let i = 0; i < jobs.length; i++) {
if (jobs[i].progress === -1) {
showNotify({
message: `任务执行失败:${jobs[i]["err_msg"]}`,
type: "danger",
});
if (jobs[i].type === "image") {
power.value += mjPower.value;
message: `任务执行失败:${jobs[i]['err_msg']}`,
type: 'danger',
})
if (jobs[i].type === 'image') {
power.value += mjPower.value
} else {
power.value += mjActionPower.value;
power.value += mjActionPower.value
}
continue;
continue
}
_jobs.push(jobs[i]);
_jobs.push(jobs[i])
}
if (runningJobs.value.length !== _jobs.length) {
page.value = 1;
downloadPulling.value = true;
fetchFinishJobs(1);
page.value = 1
downloadPulling.value = true
fetchFinishJobs(1)
}
if (_jobs.length === 0) {
taskPulling.value = false;
taskPulling.value = false
}
runningJobs.value = _jobs;
runningJobs.value = _jobs
})
.catch((e) => {
showNotify({ type: "danger", message: "获取任务失败:" + e.message });
});
};
showNotify({ type: 'danger', message: '获取任务失败:' + e.message })
})
}
const loading = ref(false);
const finished = ref(false);
const error = ref(false);
const page = ref(0);
const pageSize = ref(10);
const loading = ref(false)
const finished = ref(false)
const error = ref(false)
const page = ref(0)
const pageSize = ref(10)
const fetchFinishJobs = (page) => {
if (!isLogin.value) {
return;
return
}
loading.value = true;
loading.value = true
// 获取已完成的任务
httpGet(`/api/mj/jobs?finish=1&page=${page}&page_size=${pageSize.value}`)
.then((res) => {
const jobs = res.data.items;
let hasDownload = false;
const jobs = res.data.items
let hasDownload = false
for (let i = 0; i < jobs.length; i++) {
if (jobs[i].type === "upscale" || jobs[i].type === "swapFace") {
jobs[i]["thumb_url"] = jobs[i]["img_url"] + "?imageView2/1/w/480/h/600/q/75";
if (jobs[i].type === 'upscale' || jobs[i].type === 'swapFace') {
jobs[i]['thumb_url'] = jobs[i]['img_url'] + '?imageView2/1/w/480/h/600/q/75'
} else {
jobs[i]["thumb_url"] = jobs[i]["img_url"] + "?imageView2/1/w/480/h/480/q/75";
jobs[i]['thumb_url'] = jobs[i]['img_url'] + '?imageView2/1/w/480/h/480/q/75'
}
if (jobs[i]["img_url"] === "" && jobs[i].progress === 100) {
hasDownload = true;
if (jobs[i]['img_url'] === '' && jobs[i].progress === 100) {
hasDownload = true
}
if (jobs[i].type !== "upscale" && jobs[i].progress === 100) {
jobs[i]["can_opt"] = true;
if (jobs[i].type !== 'upscale' && jobs[i].progress === 100) {
jobs[i]['can_opt'] = true
}
}
if (page === 1) {
downloadPulling.value = hasDownload;
downloadPulling.value = hasDownload
}
if (jobs.length < pageSize.value) {
finished.value = true;
finished.value = true
}
if (page === 1) {
finishedJobs.value = jobs;
finishedJobs.value = jobs
} else {
finishedJobs.value = finishedJobs.value.concat(jobs);
finishedJobs.value = finishedJobs.value.concat(jobs)
}
nextTick(() => (loading.value = false));
nextTick(() => (loading.value = false))
})
.catch((e) => {
loading.value = false;
error.value = true;
showFailToast("获取任务失败:" + e.message);
});
};
loading.value = false
error.value = true
showFailToast('获取任务失败:' + e.message)
})
}
const onLoad = () => {
page.value += 1;
fetchFinishJobs(page.value);
};
page.value += 1
fetchFinishJobs(page.value)
}
// 切换图片比例
const changeRate = (item) => {
params.value.rate = item.value;
};
params.value.rate = item.value
}
// 切换模型
const changeModel = (item) => {
params.value.model = item.value;
};
params.value.model = item.value
}
const imgKey = ref("");
const imgKey = ref('')
const beforeUpload = (key) => {
imgKey.value = key;
};
imgKey.value = key
}
// 图片上传
const uploadImg = (file) => {
file.status = "uploading";
file.status = 'uploading'
// 压缩图片并上传
new Compressor(file.file, {
quality: 0.6,
success(result) {
const formData = new FormData();
formData.append("file", result, result.name);
const formData = new FormData()
formData.append('file', result, result.name)
// 执行上传操作
httpPost("/api/upload", formData)
httpPost('/api/upload', formData)
.then((res) => {
file.url = res.data.url;
if (imgKey.value !== "") {
file.url = res.data.url
if (imgKey.value !== '') {
// 单张图片上传
params.value[imgKey.value] = res.data.url;
imgKey.value = "";
params.value[imgKey.value] = res.data.url
imgKey.value = ''
}
file.status = "done";
file.status = 'done'
})
.catch((e) => {
file.status = "failed";
file.message = "上传失败";
showFailToast("图片上传失败:" + e.message);
});
file.status = 'failed'
file.message = '上传失败'
showFailToast('图片上传失败:' + e.message)
})
},
error(err) {
console.log(err.message);
console.log(err.message)
},
});
};
})
}
const send = (url, index, item) => {
httpPost(url, {
@@ -525,126 +614,126 @@ const send = (url, index, item) => {
prompt: item.prompt,
})
.then(() => {
showSuccessToast("任务推送成功,请耐心等待任务执行...");
power.value -= mjActionPower.value;
showSuccessToast('任务推送成功,请耐心等待任务执行...')
power.value -= mjActionPower.value
runningJobs.value.push({
progress: 0,
});
})
})
.catch((e) => {
showFailToast("任务推送失败:" + e.message);
});
};
showFailToast('任务推送失败:' + e.message)
})
}
// 图片放大任务
const upscale = (index, item) => {
send("/api/mj/upscale", index, item);
};
send('/api/mj/upscale', index, item)
}
// 图片变换任务
const variation = (index, item) => {
send("/api/mj/variation", index, item);
};
send('/api/mj/variation', index, item)
}
const generate = () => {
if (!isLogin.value) {
return showLoginDialog(router);
return showLoginDialog(router)
}
if (params.value.prompt === "" && params.value.task_type === "image") {
return showFailToast("请输入绘画提示词!");
if (params.value.prompt === '' && params.value.task_type === 'image') {
return showFailToast('请输入绘画提示词!')
}
if (params.value.model.indexOf("niji") !== -1 && params.value.raw) {
return showFailToast("动漫模型不允许启用原始模式");
if (params.value.model.indexOf('niji') !== -1 && params.value.raw) {
return showFailToast('动漫模型不允许启用原始模式')
}
params.value.session_id = getSessionId();
params.value.img_arr = imgList.value.map((img) => img.url);
httpPost("/api/mj/image", params.value)
params.value.session_id = getSessionId()
params.value.img_arr = imgList.value.map((img) => img.url)
httpPost('/api/mj/image', params.value)
.then(() => {
showToast("绘画任务推送成功,请耐心等待任务执行");
power.value -= mjPower.value;
taskPulling.value = true;
showToast('绘画任务推送成功,请耐心等待任务执行')
power.value -= mjPower.value
taskPulling.value = true
runningJobs.value.push({
progress: 0,
});
})
})
.catch((e) => {
showFailToast("任务推送失败:" + e.message);
});
};
showFailToast('任务推送失败:' + e.message)
})
}
const removeImage = (item) => {
showConfirmDialog({
title: "删除提示",
message: "此操作将会删除任务和图片,继续操作码?",
title: '删除提示',
message: '此操作将会删除任务和图片,继续操作码?',
})
.then(() => {
httpGet("/api/mj/remove", { id: item.id, user_id: item.user_id })
httpGet('/api/mj/remove', { id: item.id, user_id: item.user_id })
.then(() => {
showSuccessToast("任务删除成功");
fetchFinishJobs(1);
showSuccessToast('任务删除成功')
fetchFinishJobs(1)
})
.catch((e) => {
showFailToast("任务删除失败:" + e.message);
});
showFailToast('任务删除失败:' + e.message)
})
})
.catch(() => {
showToast("您取消了操作");
});
};
showToast('您取消了操作')
})
}
// 发布图片到作品墙
const publishImage = (item, action) => {
let text = "图片发布";
let text = '图片发布'
if (action === false) {
text = "取消发布";
text = '取消发布'
}
httpGet("/api/mj/publish", { id: item.id, action: action, user_id: item.user_id })
httpGet('/api/mj/publish', { id: item.id, action: action, user_id: item.user_id })
.then(() => {
showSuccessToast(text + "成功");
item.publish = action;
showSuccessToast(text + '成功')
item.publish = action
})
.catch((e) => {
showFailToast(text + "失败:" + e.message);
});
};
showFailToast(text + '失败:' + e.message)
})
}
const showPrompt = (item) => {
prompt.value = item.prompt;
prompt.value = item.prompt
showConfirmDialog({
title: "绘画提示词",
title: '绘画提示词',
message: item.prompt,
confirmButtonText: "复制",
cancelButtonText: "关闭",
confirmButtonText: '复制',
cancelButtonText: '关闭',
})
.then(() => {
document.querySelector("#copy-btn").click();
document.querySelector('#copy-btn').click()
})
.catch(() => {});
};
.catch(() => {})
}
const showErrMsg = (item) => {
showDialog({
title: "错误详情",
message: item["err_msg"],
title: '错误详情',
message: item['err_msg'],
}).then(() => {
// on close
});
};
})
}
const imageView = (item) => {
showImagePreview([item["img_url"]]);
};
showImagePreview([item['img_url']])
}
// 切换菜单
const tabChange = (tab) => {
if (tab === "txt2img" || tab === "img2img") {
params.value.task_type = "image";
if (tab === 'txt2img' || tab === 'img2img') {
params.value.task_type = 'image'
} else {
params.value.task_type = tab;
params.value.task_type = tab
}
};
}
</script>
<style lang="stylus">
@import "@/assets/css/mobile/image-mj.styl"
@import "../../../assets/css/mobile/image-mj.styl"
</style>

View File

@@ -3,9 +3,20 @@
<van-form @submit="generate">
<van-cell-group inset>
<div>
<van-field v-model="params.sampler" is-link readonly label="采样方法" placeholder="选择采样方法" @click="showSamplerPicker = true" />
<van-field
v-model="params.sampler"
is-link
readonly
label="采样方法"
placeholder="选择采样方法"
@click="showSamplerPicker = true"
/>
<van-popup v-model:show="showSamplerPicker" position="bottom" teleport="#app">
<van-picker :columns="samplers" @cancel="showSamplerPicker = false" @confirm="samplerConfirm" />
<van-picker
:columns="samplers"
@cancel="showSamplerPicker = false"
@confirm="samplerConfirm"
/>
</van-popup>
</div>
@@ -24,17 +35,30 @@
<van-field v-model.number="params.steps" label="迭代步数" placeholder="">
<template #right-icon>
<van-icon name="info-o" @click="showInfo('值越大则代表细节越多同时也意味着出图速度越慢一般推荐20-30')" />
<van-icon
name="info-o"
@click="showInfo('值越大则代表细节越多同时也意味着出图速度越慢一般推荐20-30')"
/>
</template>
</van-field>
<van-field v-model.number="params.cfg_scale" label="引导系数" placeholder="">
<template #right-icon>
<van-icon name="info-o" @click="showInfo('提示词引导系数,图像在多大程度上服从提示词,较低值会产生更有创意的结果')" />
<van-icon
name="info-o"
@click="
showInfo('提示词引导系数,图像在多大程度上服从提示词,较低值会产生更有创意的结果')
"
/>
</template>
</van-field>
<van-field v-model.number="params.seed" label="随机因子" placeholder="">
<template #right-icon>
<van-icon name="info-o" @click="showInfo('随机数种子,相同的种子会得到相同的结果,设置为 -1 则每次随机生成种子')" />
<van-icon
name="info-o"
@click="
showInfo('随机数种子,相同的种子会得到相同的结果,设置为 -1 则每次随机生成种子')
"
/>
</template>
</van-field>
@@ -46,9 +70,20 @@
<div v-if="params.hd_fix">
<div>
<van-field v-model="params.hd_scale_alg" is-link readonly label="放大算法" placeholder="选择放大算法" @click="showUpscalePicker = true" />
<van-field
v-model="params.hd_scale_alg"
is-link
readonly
label="放大算法"
placeholder="选择放大算法"
@click="showUpscalePicker = true"
/>
<van-popup v-model:show="showUpscalePicker" position="bottom" teleport="#app">
<van-picker :columns="upscaleAlgArr" @cancel="showUpscalePicker = false" @confirm="upscaleConfirm" />
<van-picker
:columns="upscaleAlgArr"
@cancel="showUpscalePicker = false"
@confirm="upscaleConfirm"
/>
</van-popup>
</div>
@@ -57,10 +92,18 @@
<van-field label="重绘幅度">
<template #input>
<van-slider v-model.number="params.hd_redraw_rate" :max="1" :step="0.1" @update:model-value="showToast('当前值' + params.hd_redraw_rate)" />
<van-slider
v-model.number="params.hd_redraw_rate"
:max="1"
:step="0.1"
@update:model-value="showToast('当前值' + params.hd_redraw_rate)"
/>
</template>
<template #right-icon>
<van-icon name="info-o" @click="showInfo('决定算法对图像内容的影响程度,较大的值将得到越有创意的图像')" />
<van-icon
name="info-o"
@click="showInfo('决定算法对图像内容的影响程度,较大的值将得到越有创意的图像')"
/>
</template>
</van-field>
</div>
@@ -76,7 +119,14 @@
<van-collapse v-model="activeColspan">
<van-collapse-item title="反向提示词" name="neg_prompt">
<van-field v-model="params.neg_prompt" rows="3" maxlength="2000" autosize type="textarea" placeholder="不想出现在图片上的元素(例如:树,建筑)" />
<van-field
v-model="params.neg_prompt"
rows="3"
maxlength="2000"
autosize
type="textarea"
placeholder="不想出现在图片上的元素(例如:树,建筑)"
/>
</van-collapse-item>
</van-collapse>
@@ -103,7 +153,14 @@
<div v-if="item.progress > 0">
<van-image src="/images/img-holder.png"></van-image>
<div class="progress">
<van-circle v-model:current-rate="item.progress" :rate="item.progress" :speed="100" :text="item.progress + '%'" :stroke-width="60" size="90px" />
<van-circle
v-model:current-rate="item.progress"
:rate="item.progress"
:speed="100"
:text="item.progress + '%'"
:stroke-width="60"
size="90px"
/>
</div>
</div>
@@ -139,11 +196,19 @@
<div class="title">任务失败</div>
<div class="opt">
<van-button size="small" @click="showErrMsg(item)">详情</van-button>
<van-button type="danger" @click="removeImage($event, item)" size="small">删除</van-button>
<van-button type="danger" @click="removeImage($event, item)" size="small"
>删除</van-button
>
</div>
</div>
<div class="job-item" v-else>
<van-image :src="item['img_url']" :class="item['can_opt'] ? '' : 'upscale'" lazy-load @click="imageView(item)" fit="cover">
<van-image
:src="item['img_url']"
:class="item['can_opt'] ? '' : 'upscale'"
lazy-load
@click="imageView(item)"
fit="cover"
>
<template v-slot:loading>
<van-loading type="spinner" size="20" />
</template>
@@ -151,7 +216,12 @@
<div class="remove">
<el-button type="danger" :icon="Delete" @click="removeImage($event, item)" circle />
<el-button type="warning" v-if="item.publish" @click="publishImage($event, item, false)" circle>
<el-button
type="warning"
v-if="item.publish"
@click="publishImage($event, item, false)"
circle
>
<i class="iconfont icon-cancel-share"></i>
</el-button>
<el-button type="success" v-else @click="publishImage($event, item, true)" circle>
@@ -166,49 +236,64 @@
</van-grid>
</van-list>
</div>
<button style="display: none" class="copy-prompt-sd" :data-clipboard-text="prompt" id="copy-btn-sd">复制</button>
<button
style="display: none"
class="copy-prompt-sd"
:data-clipboard-text="prompt"
id="copy-btn-sd"
>
复制
</button>
</div>
</template>
<script setup>
import { onMounted, onUnmounted, ref } from "vue";
import { Delete } from "@element-plus/icons-vue";
import { httpGet, httpPost } from "@/utils/http";
import Clipboard from "clipboard";
import { checkSession, getSystemInfo } from "@/store/cache";
import { useRouter } from "vue-router";
import { getSessionId } from "@/store/session";
import { showConfirmDialog, showDialog, showFailToast, showImagePreview, showNotify, showSuccessToast, showToast } from "vant";
import { showLoginDialog } from "@/utils/libs";
import { useSharedStore } from "@/store/sharedata";
import { checkSession, getSystemInfo } from '@/store/cache'
import { getSessionId } from '@/store/session'
import { useSharedStore } from '@/store/sharedata'
import { httpGet, httpPost } from '@/utils/http'
import { showLoginDialog } from '@/utils/libs'
import { Delete } from '@element-plus/icons-vue'
import Clipboard from 'clipboard'
import {
showConfirmDialog,
showDialog,
showFailToast,
showImagePreview,
showNotify,
showSuccessToast,
showToast,
} from 'vant'
import { onMounted, onUnmounted, ref } from 'vue'
import { useRouter } from 'vue-router'
const listBoxHeight = ref(window.innerHeight - 40);
const mjBoxHeight = ref(window.innerHeight - 150);
const isLogin = ref(false);
const activeColspan = ref([""]);
const listBoxHeight = ref(window.innerHeight - 40)
const mjBoxHeight = ref(window.innerHeight - 150)
const isLogin = ref(false)
const activeColspan = ref([''])
window.onresize = () => {
listBoxHeight.value = window.innerHeight - 40;
mjBoxHeight.value = window.innerHeight - 150;
};
listBoxHeight.value = window.innerHeight - 40
mjBoxHeight.value = window.innerHeight - 150
}
const samplers = ref([
{ text: "Euler a", value: "Euler a" },
{ text: "DPM++ 2S a Karras", value: "DPM++ 2S a Karras" },
{ text: "DPM++ 2M Karras", value: "DPM++ 2M Karras" },
{ text: "DPM++ 2M SDE Karras", value: "DPM++ 2M SDE Karras" },
{ text: "DPM++ 2M Karras", value: "DPM++ 2M Karras" },
{ text: "DPM++ 3M SDE Karras", value: "DPM++ 3M SDE Karras" },
]);
const showSamplerPicker = ref(false);
{ text: 'Euler a', value: 'Euler a' },
{ text: 'DPM++ 2S a Karras', value: 'DPM++ 2S a Karras' },
{ text: 'DPM++ 2M Karras', value: 'DPM++ 2M Karras' },
{ text: 'DPM++ 2M SDE Karras', value: 'DPM++ 2M SDE Karras' },
{ text: 'DPM++ 2M Karras', value: 'DPM++ 2M Karras' },
{ text: 'DPM++ 3M SDE Karras', value: 'DPM++ 3M SDE Karras' },
])
const showSamplerPicker = ref(false)
const upscaleAlgArr = ref([
{ text: "Latent", value: "Latent" },
{ text: "ESRGAN_4x", value: "ESRGAN_4x" },
{ text: "ESRGAN 4x+", value: "ESRGAN 4x+" },
{ text: "SwinIR_4x", value: "SwinIR_4x" },
{ text: "LDSR", value: "LDSR" },
]);
const showUpscalePicker = ref(false);
{ text: 'Latent', value: 'Latent' },
{ text: 'ESRGAN_4x', value: 'ESRGAN_4x' },
{ text: 'ESRGAN 4x+', value: 'ESRGAN 4x+' },
{ text: 'SwinIR_4x', value: 'SwinIR_4x' },
{ text: 'LDSR', value: 'LDSR' },
])
const showUpscalePicker = ref(false)
const params = ref({
width: 1024,
@@ -222,260 +307,261 @@ const params = ref({
hd_scale: 2,
hd_scale_alg: upscaleAlgArr.value[0].value,
hd_steps: 0,
prompt: "",
neg_prompt: "nsfw, paintings,low quality,easynegative,ng_deepnegative ,lowres,bad anatomy,bad hands,bad feet",
});
prompt: '',
neg_prompt:
'nsfw, paintings,low quality,easynegative,ng_deepnegative ,lowres,bad anatomy,bad hands,bad feet',
})
const runningJobs = ref([]);
const finishedJobs = ref([]);
const allowPulling = ref(true); // 是否允许轮询
const tastPullHandler = ref(null);
const router = useRouter();
const runningJobs = ref([])
const finishedJobs = ref([])
const allowPulling = ref(true) // 是否允许轮询
const tastPullHandler = ref(null)
const router = useRouter()
// 检查是否有画同款的参数
const _params = router.currentRoute.value.params["copyParams"];
const _params = router.currentRoute.value.params['copyParams']
if (_params) {
params.value = JSON.parse(_params);
params.value = JSON.parse(_params)
}
const power = ref(0);
const sdPower = ref(0); // 画一张 SD 图片消耗算力
const power = ref(0)
const sdPower = ref(0) // 画一张 SD 图片消耗算力
const userId = ref(0);
const store = useSharedStore();
const clipboard = ref(null);
const prompt = ref("");
const userId = ref(0)
const store = useSharedStore()
const clipboard = ref(null)
const prompt = ref('')
onMounted(() => {
initData();
clipboard.value = new Clipboard(".copy-prompt-sd");
clipboard.value.on("success", () => {
showNotify({ type: "success", message: "复制成功", duration: 1000 });
});
clipboard.value.on("error", () => {
showNotify({ type: "danger", message: "复制失败", duration: 2000 });
});
initData()
clipboard.value = new Clipboard('.copy-prompt-sd')
clipboard.value.on('success', () => {
showNotify({ type: 'success', message: '复制成功', duration: 1000 })
})
clipboard.value.on('error', () => {
showNotify({ type: 'danger', message: '复制失败', duration: 2000 })
})
getSystemInfo()
.then((res) => {
sdPower.value = res.data.sd_power;
params.value.neg_prompt = res.data.sd_neg_prompt;
sdPower.value = res.data.sd_power
params.value.neg_prompt = res.data.sd_neg_prompt
})
.catch((e) => {
showNotify({ type: "danger", message: "获取系统配置失败:" + e.message });
});
});
showNotify({ type: 'danger', message: '获取系统配置失败:' + e.message })
})
})
onUnmounted(() => {
clipboard.value.destroy();
clipboard.value.destroy()
if (tastPullHandler.value) {
clearInterval(tastPullHandler.value);
clearInterval(tastPullHandler.value)
}
});
})
const initData = () => {
checkSession()
.then((user) => {
power.value = user["power"];
userId.value = user.id;
isLogin.value = true;
fetchRunningJobs();
fetchFinishJobs(1);
power.value = user['power']
userId.value = user.id
isLogin.value = true
fetchRunningJobs()
fetchFinishJobs(1)
tastPullHandler.value = setInterval(() => {
if (allowPulling.value) {
fetchRunningJobs();
fetchRunningJobs()
}
}, 5000);
}, 5000)
})
.catch(() => {
loading.value = false;
});
};
loading.value = false
})
}
const fetchRunningJobs = () => {
// 获取运行中的任务
httpGet(`/api/sd/jobs?finish=0`)
.then((res) => {
const jobs = res.data.items;
const _jobs = [];
const jobs = res.data.items
const _jobs = []
for (let i = 0; i < jobs.length; i++) {
if (jobs[i].progress === -1) {
showNotify({
message: `任务ID${jobs[i]["task_id"]} 原因:${jobs[i]["err_msg"]}`,
type: "danger",
});
power.value += sdPower.value;
continue;
message: `任务ID${jobs[i]['task_id']} 原因:${jobs[i]['err_msg']}`,
type: 'danger',
})
power.value += sdPower.value
continue
}
_jobs.push(jobs[i]);
_jobs.push(jobs[i])
}
if (runningJobs.value.length !== _jobs.length) {
fetchFinishJobs(1);
fetchFinishJobs(1)
}
if (runningJobs.value.length === 0) {
allowPulling.value = false;
allowPulling.value = false
}
runningJobs.value = _jobs;
runningJobs.value = _jobs
})
.catch((e) => {
showNotify({ type: "danger", message: "获取任务失败:" + e.message });
});
};
showNotify({ type: 'danger', message: '获取任务失败:' + e.message })
})
}
const loading = ref(false);
const finished = ref(false);
const error = ref(false);
const page = ref(0);
const pageSize = ref(10);
const loading = ref(false)
const finished = ref(false)
const error = ref(false)
const page = ref(0)
const pageSize = ref(10)
// 获取已完成的任务
const fetchFinishJobs = (page) => {
loading.value = true;
loading.value = true
httpGet(`/api/sd/jobs?finish=1&page=${page}&page_size=${pageSize.value}`)
.then((res) => {
const jobs = res.data.items;
const jobs = res.data.items
if (jobs.length < pageSize.value) {
finished.value = true;
finished.value = true
}
const _jobs = [];
const _jobs = []
for (let i = 0; i < jobs.length; i++) {
if (jobs[i].progress === -1) {
jobs[i]["thumb_url"] = jobs[i]["img_url"] + "?imageView2/1/w/480/h/600/q/75";
jobs[i]['thumb_url'] = jobs[i]['img_url'] + '?imageView2/1/w/480/h/600/q/75'
}
_jobs.push(jobs[i]);
_jobs.push(jobs[i])
}
if (page === 1) {
finishedJobs.value = _jobs;
finishedJobs.value = _jobs
} else {
finishedJobs.value = finishedJobs.value.concat(_jobs);
finishedJobs.value = finishedJobs.value.concat(_jobs)
}
loading.value = false;
loading.value = false
})
.catch((e) => {
loading.value = false;
showNotify({ type: "danger", message: "获取任务失败:" + e.message });
});
};
loading.value = false
showNotify({ type: 'danger', message: '获取任务失败:' + e.message })
})
}
const onLoad = () => {
page.value += 1;
fetchFinishJobs(page.value);
};
page.value += 1
fetchFinishJobs(page.value)
}
// 创建绘图任务
const promptRef = ref(null);
const promptRef = ref(null)
const generate = () => {
if (!isLogin.value) {
return showLoginDialog(router);
return showLoginDialog(router)
}
if (params.value.prompt === "") {
promptRef.value.focus();
return showToast("请输入绘画提示词!");
if (params.value.prompt === '') {
promptRef.value.focus()
return showToast('请输入绘画提示词!')
}
if (!params.value.seed) {
params.value.seed = -1;
params.value.seed = -1
}
params.value.session_id = getSessionId();
httpPost("/api/sd/image", params.value)
params.value.session_id = getSessionId()
httpPost('/api/sd/image', params.value)
.then(() => {
showSuccessToast("绘画任务推送成功,请耐心等待任务执行...");
power.value -= sdPower.value;
allowPulling.value = true;
showSuccessToast('绘画任务推送成功,请耐心等待任务执行...')
power.value -= sdPower.value
allowPulling.value = true
runningJobs.value.push({
progress: 0,
});
})
})
.catch((e) => {
showFailToast("任务推送失败:" + e.message);
});
};
showFailToast('任务推送失败:' + e.message)
})
}
const showPrompt = (item) => {
prompt.value = item.prompt;
prompt.value = item.prompt
showConfirmDialog({
title: "绘画提示词",
title: '绘画提示词',
message: item.prompt,
confirmButtonText: "复制",
cancelButtonText: "关闭",
confirmButtonText: '复制',
cancelButtonText: '关闭',
})
.then(() => {
document.querySelector("#copy-btn-sd").click();
document.querySelector('#copy-btn-sd').click()
})
.catch(() => {});
};
.catch(() => {})
}
const showErrMsg = (item) => {
showDialog({
title: "错误详情",
message: item["err_msg"],
title: '错误详情',
message: item['err_msg'],
}).then(() => {
// on close
});
};
})
}
const removeImage = (event, item) => {
event.stopPropagation();
event.stopPropagation()
showConfirmDialog({
title: "标题",
message: "此操作将会删除任务和图片,继续操作码?",
title: '标题',
message: '此操作将会删除任务和图片,继续操作码?',
})
.then(() => {
httpGet("/api/sd/remove", { id: item.id, user_id: item.user })
httpGet('/api/sd/remove', { id: item.id, user_id: item.user })
.then(() => {
showSuccessToast("任务删除成功");
fetchFinishJobs(1);
showSuccessToast('任务删除成功')
fetchFinishJobs(1)
})
.catch((e) => {
showFailToast("任务删除失败:" + e.message);
});
showFailToast('任务删除失败:' + e.message)
})
})
.catch(() => {
showToast("您取消了操作");
});
};
showToast('您取消了操作')
})
}
// 发布图片到作品墙
const publishImage = (event, item, action) => {
event.stopPropagation();
let text = "图片发布";
event.stopPropagation()
let text = '图片发布'
if (action === false) {
text = "取消发布";
text = '取消发布'
}
httpGet("/api/sd/publish", { id: item.id, action: action, user_id: item.user })
httpGet('/api/sd/publish', { id: item.id, action: action, user_id: item.user })
.then(() => {
showSuccessToast(text + "成功");
item.publish = action;
showSuccessToast(text + '成功')
item.publish = action
})
.catch((e) => {
showFailToast(text + "失败:" + e.message);
});
};
showFailToast(text + '失败:' + e.message)
})
}
const imageView = (item) => {
showImagePreview([item["img_url"]]);
};
showImagePreview([item['img_url']])
}
const samplerConfirm = (item) => {
params.value.sampler = item.selectedOptions[0].text;
showSamplerPicker.value = false;
};
params.value.sampler = item.selectedOptions[0].text
showSamplerPicker.value = false
}
const upscaleConfirm = (item) => {
params.value.hd_scale_alg = item.selectedOptions[0].text;
showUpscalePicker.value = false;
};
params.value.hd_scale_alg = item.selectedOptions[0].text
showUpscalePicker.value = false
}
const showInfo = (message) => {
showDialog({
title: "参数说明",
title: '参数说明',
message: message,
}).then(() => {
// on close
});
};
})
}
</script>
<style lang="stylus">
@import "@/assets/css/mobile/image-sd.styl"
<style lang="stylus" scoped>
@import "../../../assets/css/mobile/image-sd.styl"
</style>

View File

@@ -1,7 +1,7 @@
module.exports = {
content: ["./index.html", "./src/**/*.{vue,js,ts,jsx,tsx}"],
export default {
content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
theme: {
extend: {},
},
plugins: [],
};
}

View File

@@ -1,31 +1,42 @@
import vue from '@vitejs/plugin-vue'
import path from 'path'
import AutoImport from 'unplugin-auto-import/vite'
import { defineConfig, loadEnv } from 'vite'
// https://vitejs.dev/config/
// https://vite.dev/config/
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd())
// 加载环境变量
const env = loadEnv(mode, process.cwd(), '')
const apiHost = env.VITE_API_HOST || 'http://localhost:5678'
return {
plugins: [vue()],
plugins: [
vue(),
AutoImport({
imports: [
'vue',
'vue-router',
{
'@vueuse/core': ['useMouse', 'useFetch'],
},
],
dts: true, // 生成 TypeScript 声明文件
}),
],
base: env.VITE_BASE_URL,
build: {
outDir: 'dist', // 构建输出目录
},
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
'~@': path.resolve(__dirname, './src'),
},
},
css: {
preprocessorOptions: {
stylus: {
additionalData: `@import "@/assets/css/index.styl";`,
},
},
},
optimizeDeps: {
include: ['stylus'],
},
server: {
port: 8888,
port: 8888, // 设置你想要的端口号
open: false, // 可选:启动服务器时自动打开浏览器
...(process.env.NODE_ENV === 'development'
? {
proxy: {

View File

@@ -1,44 +0,0 @@
const { defineConfig } = require('@vue/cli-service')
const path = require('path')
let webpack = require('webpack')
module.exports = defineConfig({
transpileDependencies: true,
lintOnSave: false, //关闭eslint校验
productionSourceMap: false, //在生产模式中禁用 Source Map既可以减少包大小也可以加密源码
configureWebpack: {
// disable performance hints
performance: {
hints: false,
},
plugins: [new webpack.optimize.MinChunkSizePlugin({ minChunkSize: 10000 })],
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
},
},
},
publicPath: '/',
outputDir: 'dist',
crossorigin: 'anonymous',
devServer: {
client: {
overlay: false, // 关闭错误覆盖层
},
allowedHosts: 'all',
port: 8888,
proxy: {
'/api': {
target: process.env.VUE_APP_API_HOST,
changeOrigin: true,
ws: true,
},
'/static/upload/': {
target: process.env.VUE_APP_API_HOST,
changeOrigin: true,
},
},
},
})