替换 vite 架构

This commit is contained in:
GeekMaster
2025-05-26 14:14:29 +08:00
parent 94a5187e75
commit b1ddcef593
20 changed files with 937 additions and 782 deletions

View File

@@ -1,14 +1,14 @@
VUE_APP_API_HOST=http://localhost:5678 VITE_API_HOST=http://localhost:5678
VUE_APP_WS_HOST=ws://localhost:5678 VITE_WS_HOST=ws://localhost:5678
VUE_APP_USER=18888888888 VITE_USER=18888888888
VUE_APP_PASS=12345678 VITE_PASS=12345678
VUE_APP_ADMIN_USER=admin VITE_ADMIN_USER=admin
VUE_APP_ADMIN_PASS=admin123 VITE_ADMIN_PASS=admin123
VUE_APP_KEY_PREFIX=GeekAI_DEV_ VITE_KEY_PREFIX=GeekAI_DEV_
VUE_APP_TITLE="Geek-AI 创作系统" VITE_TITLE="Geek-AI 创作系统"
VUE_APP_VERSION=v4.2.3 VITE_VERSION=v4.2.4
VUE_APP_DOCS_URL=https://docs.geekai.me VITE_DOCS_URL=https://docs.geekai.me
VUE_APP_GITHUB_URL=https://github.com/yangjian102621/geekai VITE_GITHUB_URL=https://github.com/yangjian102621/geekai
VUE_APP_GITEE_URL=https://gitee.com/blackfox/geekai VITE_GITEE_URL=https://gitee.com/blackfox/geekai
VUE_APP_GITCODE_URL=https://gitcode.com/yangjian102621/geekai VITE_GITCODE_URL=https://gitcode.com/yangjian102621/geekai

View File

@@ -1,8 +1,8 @@
VUE_APP_API_HOST= VITE_API_HOST=
VUE_APP_WS_HOST= VITE_WS_HOST=
VUE_APP_KEY_PREFIX=GeekAI_ VITE_KEY_PREFIX=GeekAI_
VUE_APP_VERSION=v4.2.3 VITE_VERSION=v4.2.4
VUE_APP_DOCS_URL=https://docs.geekai.me VITE_DOCS_URL=https://docs.geekai.me
VUE_APP_GITHUB_URL=https://github.com/yangjian102621/geekai VITE_GITHUB_URL=https://github.com/yangjian102621/geekai
VUE_APP_GITEE_URL=https://gitee.com/blackfox/geekai VITE_GITEE_URL=https://gitee.com/blackfox/geekai
VUE_APP_GITCODE_URL=https://gitcode.com/yangjian102621/geekai VITE_GITCODE_URL=https://gitcode.com/yangjian102621/geekai

View File

@@ -50,14 +50,11 @@
"@babel/eslint-parser": "^7.12.16", "@babel/eslint-parser": "^7.12.16",
"@vitejs/plugin-vue": "^5.2.4", "@vitejs/plugin-vue": "^5.2.4",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"eslint": "^7.32.0",
"eslint-plugin-vue": "^8.0.3",
"postcss": "^8.4.49", "postcss": "^8.4.49",
"stylus": "^0.58.1", "stylus": "^0.58.1",
"stylus-loader": "^7.0.0", "stylus-loader": "^7.0.0",
"tailwindcss": "^3.4.17", "tailwindcss": "^3.4.17",
"vite": "^6.3.5", "vite": "^5.4.10"
"webpack": "^5.90.3"
}, },
"eslintConfig": { "eslintConfig": {
"root": true, "root": true,

View File

@@ -5,93 +5,93 @@
</template> </template>
<script setup> <script setup>
import { ElConfigProvider } from "element-plus"; import { checkSession, getClientId, getSystemInfo } from '@/store/cache'
import { onMounted, ref, watch } from "vue"; import { getUserToken } from '@/store/session'
import { checkSession, getClientId, getSystemInfo } from "@/store/cache"; import { useSharedStore } from '@/store/sharedata'
import { isChrome, isMobile } from "@/utils/libs"; import { showMessageInfo } from '@/utils/dialog'
import { showMessageInfo } from "@/utils/dialog"; import { isChrome, isMobile } from '@/utils/libs'
import { useSharedStore } from "@/store/sharedata"; import { ElConfigProvider } from 'element-plus'
import { getUserToken } from "@/store/session"; import { onMounted, ref, watch } from 'vue'
const debounce = (fn, delay) => { const debounce = (fn, delay) => {
let timer; let timer
return (...args) => { return (...args) => {
if (timer) { if (timer) {
clearTimeout(timer); clearTimeout(timer)
} }
timer = setTimeout(() => { timer = setTimeout(() => {
fn(...args); fn(...args)
}, delay); }, delay)
}; }
}; }
const _ResizeObserver = window.ResizeObserver; const _ResizeObserver = window.ResizeObserver
window.ResizeObserver = class ResizeObserver extends _ResizeObserver { window.ResizeObserver = class ResizeObserver extends _ResizeObserver {
constructor(callback) { constructor(callback) {
callback = debounce(callback, 200); callback = debounce(callback, 200)
super(callback); super(callback)
} }
}; }
const store = useSharedStore(); const store = useSharedStore()
onMounted(() => { onMounted(() => {
// 获取系统参数 // 获取系统参数
getSystemInfo().then((res) => { getSystemInfo().then((res) => {
const link = document.createElement("link"); const link = document.createElement('link')
link.rel = "shortcut icon"; link.rel = 'shortcut icon'
link.href = res.data.logo; link.href = res.data.logo
document.head.appendChild(link); document.head.appendChild(link)
}); })
if (!isChrome() && !isMobile()) { if (!isChrome() && !isMobile()) {
showMessageInfo("建议使用 Chrome 浏览器以获得最佳体验。"); showMessageInfo('建议使用 Chrome 浏览器以获得最佳体验。')
} }
checkSession() checkSession()
.then(() => { .then(() => {
store.setIsLogin(true); store.setIsLogin(true)
}) })
.catch(() => {}); .catch(() => {})
// 设置主题 // 设置主题
document.documentElement.setAttribute("data-theme", store.theme); document.documentElement.setAttribute('data-theme', store.theme)
}); })
watch( watch(
() => store.isLogin, () => store.isLogin,
(val) => { (val) => {
if (val) { if (val) {
connect(); connect()
} }
} }
); )
const handler = ref(0); const handler = ref(0)
// 初始化 websocket 连接 // 初始化 websocket 连接
const connect = () => { const connect = () => {
let host = process.env.VUE_APP_WS_HOST; let host = import.meta.env.VITE_WS_HOST
if (host === "") { if (host === '') {
if (location.protocol === "https:") { if (location.protocol === 'https:') {
host = "wss://" + location.host; host = 'wss://' + location.host
} else { } else {
host = "ws://" + location.host; host = 'ws://' + location.host
} }
} }
const clientId = getClientId(); const clientId = getClientId()
const _socket = new WebSocket(host + `/api/ws?client_id=${clientId}`, ["token", getUserToken()]); const _socket = new WebSocket(host + `/api/ws?client_id=${clientId}`, ['token', getUserToken()])
_socket.addEventListener("open", () => { _socket.addEventListener('open', () => {
console.log("WebSocket 已连接"); console.log('WebSocket 已连接')
handler.value = setInterval(() => { handler.value = setInterval(() => {
if (_socket.readyState === WebSocket.OPEN) { if (_socket.readyState === WebSocket.OPEN) {
_socket.send(JSON.stringify({ type: "ping" })); _socket.send(JSON.stringify({ type: 'ping' }))
} }
}, 5000); }, 5000)
}); })
_socket.addEventListener("close", () => { _socket.addEventListener('close', () => {
clearInterval(handler.value); clearInterval(handler.value)
connect(); connect()
}); })
store.setSocket(_socket); store.setSocket(_socket)
}; }
// 打印 banner // 打印 banner
const banner = ` const banner = `
@@ -104,20 +104,23 @@ const banner = `
'88. .88' 888 .o 888 .o 888 88b. .8' 888. 888 '88. .88' 888 .o 888 .o 888 88b. .8' 888. 888
Y8bood8P' Y8bod8P' Y8bod8P' o888o o888o o88o o8888o o888o Y8bood8P' Y8bod8P' Y8bod8P' o888o o888o o88o o8888o o888o
`; `
console.log("%c" + banner + "", "color: purple;font-size: 18px;"); console.log('%c' + banner + '', 'color: purple;font-size: 18px;')
console.log("%c感谢大家为 GeekAI 做出的卓越贡献!", "color: green;font-size: 40px;font-family: '微软雅黑';");
console.log( console.log(
"%c项目源码https://github.com/yangjian102621/geekai %c 您的 star 对我们非常重要!", '%c感谢大家为 GeekAI 做出的卓越贡献!',
"color: green;font-size: 40px;font-family: '微软雅黑';"
)
console.log(
'%c项目源码https://github.com/yangjian102621/geekai %c 您的 star 对我们非常重要!',
"color: green;font-size: 20px;font-family: '微软雅黑';", "color: green;font-size: 20px;font-family: '微软雅黑';",
"color: red;font-size: 20px;font-family: '微软雅黑';" "color: red;font-size: 20px;font-family: '微软雅黑';"
); )
console.log("%c 愿你出走半生,归来仍是少年!大奉武夫许七安,前来凿阵!", "color: #7c39ed;font-size: 18px;font-family: '微软雅黑';");
</script> </script>
<style lang="stylus"> <style lang="stylus">
@import '@/assets/iconfont/iconfont.css'
html, body { html, body {
margin: 0; margin: 0;
padding: 0; padding: 0;
@@ -173,6 +176,4 @@ html, body {
background #D6FBCC background #D6FBCC
color #07C160 color #07C160
} }
@import '@/assets/iconfont/iconfont.css'
</style> </style>

View File

@@ -17,45 +17,45 @@
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref } from "vue"; import { getLicenseInfo, getSystemInfo } from '@/store/cache'
import { showMessageError } from "@/utils/dialog"; import { showMessageError } from '@/utils/dialog'
import { getLicenseInfo, getSystemInfo } from "@/store/cache"; import { ref } from 'vue'
const title = ref(""); const title = ref('')
const version = ref(process.env.VUE_APP_VERSION); const version = ref(import.meta.env.VITE_VERSION)
const gitURL = ref(process.env.VUE_APP_GITHUB_URL); const gitURL = ref(import.meta.env.VITE_GITHUB_URL)
const copyRight = ref(""); const copyRight = ref('')
const icp = ref(""); const icp = ref('')
const license = ref({}); const license = ref({})
const props = defineProps({ const props = defineProps({
textColor: { textColor: {
type: String, type: String,
default: "#ffffff" default: '#ffffff',
} },
}); })
// 获取系统配置 // 获取系统配置
getSystemInfo() getSystemInfo()
.then((res) => { .then((res) => {
title.value = res.data.title ?? process.env.VUE_APP_TITLE; title.value = res.data.title ?? import.meta.env.VITE_TITLE
copyRight.value = copyRight.value =
(res.data.copyright ? res.data.copyright : "极客学长") + (res.data.copyright ? res.data.copyright : '极客学长') +
" © 2023 - " + ' © 2023 - ' +
new Date().getFullYear() + new Date().getFullYear() +
" All rights reserved"; ' All rights reserved'
icp.value = res.data.icp; icp.value = res.data.icp
}) })
.catch((e) => { .catch((e) => {
showMessageError("获取系统配置失败:" + e.message); showMessageError('获取系统配置失败:' + e.message)
}); })
getLicenseInfo() getLicenseInfo()
.then((res) => { .then((res) => {
license.value = res.data; license.value = res.data
}) })
.catch((e) => { .catch((e) => {
showMessageError("获取 License 失败:" + e.message); showMessageError('获取 License 失败:' + e.message)
}); })
</script> </script>
<style scoped lang="stylus"> <style scoped lang="stylus">

View File

@@ -13,7 +13,14 @@
</div> </div>
<div class="block"> <div class="block">
<el-input placeholder="请输入密码(8-16位)" maxlength="16" size="large" v-model="data.password" show-password autocomplete="off"> <el-input
placeholder="请输入密码(8-16位)"
maxlength="16"
size="large"
v-model="data.password"
show-password
autocomplete="off"
>
<template #prefix> <template #prefix>
<el-icon> <el-icon>
<Lock /> <Lock />
@@ -24,7 +31,9 @@
<el-row class="btn-row" :gutter="20"> <el-row class="btn-row" :gutter="20">
<el-col :span="24"> <el-col :span="24">
<el-button class="login-btn" type="primary" size="large" @click="submitLogin"> </el-button> <el-button class="login-btn" type="primary" size="large" @click="submitLogin"
> </el-button
>
</el-col> </el-col>
</el-row> </el-row>
@@ -33,7 +42,9 @@
还没有账号 还没有账号
<el-button size="small" @click="login = false">注册</el-button> <el-button size="small" @click="login = false">注册</el-button>
<el-button type="info" class="forget" size="small" @click="showResetPass = true">忘记密码</el-button> <el-button type="info" class="forget" size="small" @click="showResetPass = true"
>忘记密码</el-button
>
</div> </div>
<div v-if="wechatLoginURL !== ''"> <div v-if="wechatLoginURL !== ''">
<el-divider> <el-divider>
@@ -42,7 +53,11 @@
<div class="c-login flex justify-center"> <div class="c-login flex justify-center">
<div class="p-2 w-full"> <div class="p-2 w-full">
<a :href="wechatLoginURL"> <a :href="wechatLoginURL">
<el-button type="success" class="w-full" size="large" @click="setRoute(router.currentRoute.value.path)" <el-button
type="success"
class="w-full"
size="large"
@click="setRoute(router.currentRoute.value.path)"
><i class="iconfont icon-wechat mr-2"></i> 微信登录 ><i class="iconfont icon-wechat mr-2"></i> 微信登录
</el-button> </el-button>
</a> </a>
@@ -58,7 +73,13 @@
<el-tabs v-model="activeName" class="demo-tabs"> <el-tabs v-model="activeName" class="demo-tabs">
<el-tab-pane label="手机注册" name="mobile" v-if="enableMobile"> <el-tab-pane label="手机注册" name="mobile" v-if="enableMobile">
<div class="block"> <div class="block">
<el-input placeholder="手机号码" size="large" v-model="data.mobile" maxlength="11" autocomplete="off"> <el-input
placeholder="手机号码"
size="large"
v-model="data.mobile"
maxlength="11"
autocomplete="off"
>
<template #prefix> <template #prefix>
<el-icon> <el-icon>
<Iphone /> <Iphone />
@@ -69,7 +90,13 @@
<div class="block"> <div class="block">
<el-row :gutter="10"> <el-row :gutter="10">
<el-col :span="12"> <el-col :span="12">
<el-input placeholder="验证码" size="large" maxlength="30" v-model="data.code" autocomplete="off"> <el-input
placeholder="验证码"
size="large"
maxlength="30"
v-model="data.code"
autocomplete="off"
>
<template #prefix> <template #prefix>
<el-icon> <el-icon>
<Checked /> <Checked />
@@ -96,7 +123,13 @@
<div class="block"> <div class="block">
<el-row :gutter="10"> <el-row :gutter="10">
<el-col :span="12"> <el-col :span="12">
<el-input placeholder="验证码" size="large" maxlength="30" v-model="data.code" autocomplete="off"> <el-input
placeholder="验证码"
size="large"
maxlength="30"
v-model="data.code"
autocomplete="off"
>
<template #prefix> <template #prefix>
<el-icon> <el-icon>
<Checked /> <Checked />
@@ -112,7 +145,12 @@
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="用户名注册" name="username" v-if="enableUser"> <el-tab-pane label="用户名注册" name="username" v-if="enableUser">
<div class="block"> <div class="block">
<el-input placeholder="用户名" size="large" v-model="data.username" autocomplete="off"> <el-input
placeholder="用户名"
size="large"
v-model="data.username"
autocomplete="off"
>
<template #prefix> <template #prefix>
<el-icon> <el-icon>
<Iphone /> <Iphone />
@@ -124,7 +162,14 @@
</el-tabs> </el-tabs>
<div class="block"> <div class="block">
<el-input placeholder="请输入密码(8-16位)" maxlength="16" size="large" v-model="data.password" show-password autocomplete="off"> <el-input
placeholder="请输入密码(8-16位)"
maxlength="16"
size="large"
v-model="data.password"
show-password
autocomplete="off"
>
<template #prefix> <template #prefix>
<el-icon> <el-icon>
<Lock /> <Lock />
@@ -134,7 +179,14 @@
</div> </div>
<div class="block"> <div class="block">
<el-input placeholder="重复密码(8-16位)" size="large" maxlength="16" v-model="data.repass" show-password autocomplete="off"> <el-input
placeholder="重复密码(8-16位)"
size="large"
maxlength="16"
v-model="data.repass"
show-password
autocomplete="off"
>
<template #prefix> <template #prefix>
<el-icon> <el-icon>
<Lock /> <Lock />
@@ -144,7 +196,12 @@
</div> </div>
<div class="block"> <div class="block">
<el-input placeholder="邀请码(可选)" size="large" v-model="data.invite_code" autocomplete="off"> <el-input
placeholder="邀请码(可选)"
size="large"
v-model="data.invite_code"
autocomplete="off"
>
<template #prefix> <template #prefix>
<el-icon> <el-icon>
<Message /> <Message />
@@ -154,7 +211,9 @@
</div> </div>
<div class="w-full"> <div class="w-full">
<el-button class="login-btn w-full" type="primary" size="large" @click="submitRegister"> </el-button> <el-button class="login-btn w-full" type="primary" size="large" @click="submitRegister"
> </el-button
>
</div> </div>
<div class="text text-sm flex justify-center items-center w-full pt-3"> <div class="text text-sm flex justify-center items-center w-full pt-3">
@@ -188,193 +247,193 @@
</template> </template>
<script setup> <script setup>
import { onMounted, ref, watch } from "vue"; import Captcha from '@/components/Captcha.vue'
import { httpGet, httpPost } from "@/utils/http"; import ResetPass from '@/components/ResetPass.vue'
import { ElMessage } from "element-plus"; import SendMsg from '@/components/SendMsg.vue'
import { setUserToken } from "@/store/session"; import { getSystemInfo } from '@/store/cache'
import { validateEmail, validateMobile } from "@/utils/validate"; import { setUserToken } from '@/store/session'
import { Checked, Close, Iphone, Lock, Message } from "@element-plus/icons-vue"; import { useSharedStore } from '@/store/sharedata'
import SendMsg from "@/components/SendMsg.vue"; import { setRoute } from '@/store/system'
import { arrayContains } from "@/utils/libs"; import { httpGet, httpPost } from '@/utils/http'
import { getSystemInfo } from "@/store/cache"; import { arrayContains } from '@/utils/libs'
import Captcha from "@/components/Captcha.vue"; import { validateEmail, validateMobile } from '@/utils/validate'
import ResetPass from "@/components/ResetPass.vue"; import { Checked, Iphone, Lock, Message } from '@element-plus/icons-vue'
import { setRoute } from "@/store/system"; import { ElMessage } from 'element-plus'
import { useRouter } from "vue-router"; import { onMounted, ref, watch } from 'vue'
import { useSharedStore } from "@/store/sharedata"; import { useRouter } from 'vue-router'
// eslint-disable-next-line no-undef // eslint-disable-next-line no-undef
const props = defineProps({ const props = defineProps({
show: Boolean, show: Boolean,
}); })
const showDialog = ref(false); const showDialog = ref(false)
watch( watch(
() => props.show, () => props.show,
(newValue) => { (newValue) => {
showDialog.value = newValue; showDialog.value = newValue
} }
); )
const login = ref(true); const login = ref(true)
const data = ref({ const data = ref({
username: process.env.VUE_APP_USER, username: import.meta.env.VITE_USER,
password: process.env.VUE_APP_PASS, password: import.meta.env.VITE_PASS,
mobile: "", mobile: '',
email: "", email: '',
repass: "", repass: '',
code: "", code: '',
invite_code: "", invite_code: '',
}); })
const enableMobile = ref(false); const enableMobile = ref(false)
const enableEmail = ref(false); const enableEmail = ref(false)
const enableUser = ref(false); const enableUser = ref(false)
const enableRegister = ref(true); const enableRegister = ref(true)
const wechatLoginURL = ref(""); const wechatLoginURL = ref('')
const activeName = ref(""); const activeName = ref('')
const wxImg = ref("/images/wx.png"); const wxImg = ref('/images/wx.png')
const captchaRef = ref(null); const captchaRef = ref(null)
// eslint-disable-next-line no-undef // eslint-disable-next-line no-undef
const emits = defineEmits(["hide", "success"]); const emits = defineEmits(['hide', 'success'])
const action = ref("login"); const action = ref('login')
const enableVerify = ref(false); const enableVerify = ref(false)
const showResetPass = ref(false); const showResetPass = ref(false)
const router = useRouter(); const router = useRouter()
const store = useSharedStore(); const store = useSharedStore()
onMounted(() => { onMounted(() => {
const returnURL = `${location.protocol}//${location.host}/login/callback?action=login`; const returnURL = `${location.protocol}//${location.host}/login/callback?action=login`
httpGet("/api/user/clogin?return_url=" + returnURL) httpGet('/api/user/clogin?return_url=' + returnURL)
.then((res) => { .then((res) => {
wechatLoginURL.value = res.data.url; wechatLoginURL.value = res.data.url
}) })
.catch((e) => { .catch((e) => {
console.log(e.message); console.log(e.message)
}); })
getSystemInfo() getSystemInfo()
.then((res) => { .then((res) => {
if (res.data) { if (res.data) {
const registerWays = res.data["register_ways"]; const registerWays = res.data['register_ways']
if (arrayContains(registerWays, "username")) { if (arrayContains(registerWays, 'username')) {
enableUser.value = true; enableUser.value = true
activeName.value = "username"; activeName.value = 'username'
} }
if (arrayContains(registerWays, "email")) { if (arrayContains(registerWays, 'email')) {
enableEmail.value = true; enableEmail.value = true
activeName.value = "email"; activeName.value = 'email'
} }
if (arrayContains(registerWays, "mobile")) { if (arrayContains(registerWays, 'mobile')) {
enableMobile.value = true; enableMobile.value = true
activeName.value = "mobile"; activeName.value = 'mobile'
} }
// 是否启用注册 // 是否启用注册
enableRegister.value = res.data["enabled_register"]; enableRegister.value = res.data['enabled_register']
// 使用后台上传的客服微信二维码 // 使用后台上传的客服微信二维码
if (res.data["wechat_card_url"] !== "") { if (res.data['wechat_card_url'] !== '') {
wxImg.value = 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) => { .catch((e) => {
ElMessage.error("获取系统配置失败:" + e.message); ElMessage.error('获取系统配置失败:' + e.message)
}); })
}); })
const submit = (verifyData) => { const submit = (verifyData) => {
if (action.value === "login") { if (action.value === 'login') {
doLogin(verifyData); doLogin(verifyData)
} else if (action.value === "register") { } else if (action.value === 'register') {
doRegister(verifyData); doRegister(verifyData)
} }
}; }
// 登录操作 // 登录操作
const submitLogin = () => { const submitLogin = () => {
if (!data.value.username) { if (!data.value.username) {
return ElMessage.error("请输入用户名"); return ElMessage.error('请输入用户名')
} }
if (!data.value.password) { if (!data.value.password) {
return ElMessage.error("请输入密码"); return ElMessage.error('请输入密码')
} }
if (enableVerify.value) { if (enableVerify.value) {
captchaRef.value.loadCaptcha(); captchaRef.value.loadCaptcha()
action.value = "login"; action.value = 'login'
} else { } else {
doLogin({}); doLogin({})
} }
}; }
const doLogin = (verifyData) => { const doLogin = (verifyData) => {
data.value.key = verifyData.key; data.value.key = verifyData.key
data.value.dots = verifyData.dots; data.value.dots = verifyData.dots
data.value.x = verifyData.x; data.value.x = verifyData.x
httpPost("/api/user/login", data.value) httpPost('/api/user/login', data.value)
.then((res) => { .then((res) => {
setUserToken(res.data.token); setUserToken(res.data.token)
store.setIsLogin(true); store.setIsLogin(true)
ElMessage.success("登录成功!"); ElMessage.success('登录成功!')
emits("hide"); emits('hide')
emits("success"); emits('success')
}) })
.catch((e) => { .catch((e) => {
ElMessage.error("登录失败," + e.message); ElMessage.error('登录失败,' + e.message)
}); })
}; }
// 注册操作 // 注册操作
const submitRegister = () => { const submitRegister = () => {
if (activeName.value === "username" && data.value.username === "") { if (activeName.value === 'username' && data.value.username === '') {
return ElMessage.error("请输入用户名"); return ElMessage.error('请输入用户名')
} }
if (activeName.value === "mobile" && !validateMobile(data.value.mobile)) { if (activeName.value === 'mobile' && !validateMobile(data.value.mobile)) {
return ElMessage.error("请输入合法的手机号"); return ElMessage.error('请输入合法的手机号')
} }
if (activeName.value === "email" && !validateEmail(data.value.email)) { if (activeName.value === 'email' && !validateEmail(data.value.email)) {
return ElMessage.error("请输入合法的邮箱地址"); return ElMessage.error('请输入合法的邮箱地址')
} }
if (data.value.password.length < 8) { if (data.value.password.length < 8) {
return ElMessage.error("密码的长度为8-16个字符"); return ElMessage.error('密码的长度为8-16个字符')
} }
if (data.value.repass !== data.value.password) { if (data.value.repass !== data.value.password) {
return ElMessage.error("两次输入密码不一致"); return ElMessage.error('两次输入密码不一致')
} }
if ((activeName.value === "mobile" || activeName.value === "email") && data.value.code === "") { if ((activeName.value === 'mobile' || activeName.value === 'email') && data.value.code === '') {
return ElMessage.error("请输入验证码"); return ElMessage.error('请输入验证码')
} }
if (enableVerify.value && activeName.value === "username") { if (enableVerify.value && activeName.value === 'username') {
captchaRef.value.loadCaptcha(); captchaRef.value.loadCaptcha()
action.value = "register"; action.value = 'register'
} else { } else {
doRegister({}); doRegister({})
} }
}; }
const doRegister = (verifyData) => { const doRegister = (verifyData) => {
data.value.key = verifyData.key; data.value.key = verifyData.key
data.value.dots = verifyData.dots; data.value.dots = verifyData.dots
data.value.x = verifyData.x; data.value.x = verifyData.x
data.value.reg_way = activeName.value; data.value.reg_way = activeName.value
httpPost("/api/user/register", data.value) httpPost('/api/user/register', data.value)
.then((res) => { .then((res) => {
setUserToken(res.data.token); setUserToken(res.data.token)
ElMessage.success({ ElMessage.success({
message: "注册成功!", message: '注册成功!',
onClose: () => { onClose: () => {
emits("hide"); emits('hide')
emits("success"); emits('success')
}, },
duration: 1000, duration: 1000,
}); })
}) })
.catch((e) => { .catch((e) => {
ElMessage.error("注册失败," + e.message); ElMessage.error('注册失败,' + e.message)
}); })
}; }
</script> </script>
<style lang="stylus"> <style lang="stylus">

View File

@@ -38,11 +38,7 @@
<div class="call-controls"> <div class="call-controls">
<el-tooltip content="长按发送语音" placement="top"> <el-tooltip content="长按发送语音" placement="top">
<ripple-button> <ripple-button>
<button <button class="call-button answer" @mousedown="startRecording" @mouseup="stopRecording">
class="call-button answer"
@mousedown="startRecording"
@mouseup="stopRecording"
>
<i class="iconfont icon-mic-bold"></i> <i class="iconfont icon-mic-bold"></i>
</button> </button>
</ripple-button> </ripple-button>
@@ -58,159 +54,155 @@
</template> </template>
<script setup> <script setup>
import RippleButton from "@/components/ui/RippleButton.vue"; import RippleButton from '@/components/ui/RippleButton.vue'
import { ref, onMounted, onUnmounted } from "vue"; import { WavRecorder, WavStreamPlayer } from '@/lib/wavtools/index.js'
import { RealtimeClient } from "@openai/realtime-api-beta"; import { getUserToken } from '@/store/session'
import { WavRecorder, WavStreamPlayer } from "@/lib/wavtools/index.js"; import { instructions } from '@/utils/conversation_config.js'
import { instructions } from "@/utils/conversation_config.js"; import { showMessageError } from '@/utils/dialog'
import { WavRenderer } from "@/utils/wav_renderer"; import { WavRenderer } from '@/utils/wav_renderer'
import { showMessageError } from "@/utils/dialog"; import { RealtimeClient } from '@openai/realtime-api-beta'
import { getUserToken } from "@/store/session"; import { onMounted, onUnmounted, ref } from 'vue'
// eslint-disable-next-line no-unused-vars,no-undef // eslint-disable-next-line no-unused-vars,no-undef
const props = defineProps({ const props = defineProps({
height: { height: {
type: String, type: String,
default: "100vh" default: '100vh',
} },
}); })
// eslint-disable-next-line no-undef // eslint-disable-next-line no-undef
const emits = defineEmits(["close"]); const emits = defineEmits(['close'])
/********************** connection animation code *************************/ /********************** connection animation code *************************/
const fullText = "正在接通中..."; const fullText = '正在接通中...'
const connectingText = ref(""); const connectingText = ref('')
let index = 0; let index = 0
const typeText = () => { const typeText = () => {
if (index < fullText.length) { if (index < fullText.length) {
connectingText.value += fullText[index]; connectingText.value += fullText[index]
index++; index++
setTimeout(typeText, 200); // 每300毫秒显示一个字 setTimeout(typeText, 200) // 每300毫秒显示一个字
} else { } else {
setTimeout(() => { setTimeout(() => {
connectingText.value = ""; connectingText.value = ''
index = 0; index = 0
typeText(); typeText()
}, 1000); // 等待1秒后重新开始 }, 1000) // 等待1秒后重新开始
} }
}; }
/*************************** end of code ****************************************/ /*************************** end of code ****************************************/
/********************** conversation process code ***************************/ /********************** conversation process code ***************************/
const leftVoiceActive = ref(false); const leftVoiceActive = ref(false)
const rightVoiceActive = ref(false); const rightVoiceActive = ref(false)
const animateVoice = () => { const animateVoice = () => {
leftVoiceActive.value = Math.random() > 0.5; leftVoiceActive.value = Math.random() > 0.5
rightVoiceActive.value = Math.random() > 0.5; rightVoiceActive.value = Math.random() > 0.5
}; }
const wavRecorder = ref(new WavRecorder({ sampleRate: 24000 })); const wavRecorder = ref(new WavRecorder({ sampleRate: 24000 }))
const wavStreamPlayer = ref(new WavStreamPlayer({ sampleRate: 24000 })); const wavStreamPlayer = ref(new WavStreamPlayer({ sampleRate: 24000 }))
let host = process.env.VUE_APP_WS_HOST; let host = import.meta.env.VITE_WS_HOST
if (host === "") { if (host === '') {
if (location.protocol === "https:") { if (location.protocol === 'https:') {
host = "wss://" + location.host; host = 'wss://' + location.host
} else { } else {
host = "ws://" + location.host; host = 'ws://' + location.host
} }
} }
const client = ref( const client = ref(
new RealtimeClient({ new RealtimeClient({
url: `${host}/api/realtime`, url: `${host}/api/realtime`,
apiKey: getUserToken(), apiKey: getUserToken(),
dangerouslyAllowAPIKeyInBrowser: true dangerouslyAllowAPIKeyInBrowser: true,
}) })
); )
// // Set up client instructions and transcription // // Set up client instructions and transcription
client.value.updateSession({ client.value.updateSession({
instructions: instructions, instructions: instructions,
turn_detection: null, turn_detection: null,
input_audio_transcription: { model: "whisper-1" }, input_audio_transcription: { model: 'whisper-1' },
voice: "alloy" voice: 'alloy',
}); })
// set voice wave canvas // set voice wave canvas
const clientCanvasRef = ref(null); const clientCanvasRef = ref(null)
const serverCanvasRef = ref(null); const serverCanvasRef = ref(null)
const isConnected = ref(false); const isConnected = ref(false)
const isRecording = ref(false); const isRecording = ref(false)
const backgroundAudio = ref(null); const backgroundAudio = ref(null)
const hangUpAudio = ref(null); const hangUpAudio = ref(null)
function sleep(ms) { function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms)); return new Promise((resolve) => setTimeout(resolve, ms))
} }
const connect = async () => { const connect = async () => {
if (isConnected.value) { if (isConnected.value) {
return; return
} }
// 播放背景音乐 // 播放背景音乐
if (backgroundAudio.value) { if (backgroundAudio.value) {
backgroundAudio.value.play().catch((error) => { backgroundAudio.value.play().catch((error) => {
console.error("播放失败,可能是浏览器的自动播放策略导致的:", error); console.error('播放失败,可能是浏览器的自动播放策略导致的:', error)
}); })
} }
// 模拟拨号延时 // 模拟拨号延时
await sleep(3000); await sleep(3000)
try { try {
await client.value.connect(); await client.value.connect()
await wavRecorder.value.begin(); await wavRecorder.value.begin()
await wavStreamPlayer.value.connect(); await wavStreamPlayer.value.connect()
console.log("对话连接成功!"); console.log('对话连接成功!')
if (!client.value.isConnected()) { if (!client.value.isConnected()) {
return; return
} }
isConnected.value = true; isConnected.value = true
backgroundAudio.value?.pause(); backgroundAudio.value?.pause()
backgroundAudio.value.currentTime = 0; backgroundAudio.value.currentTime = 0
client.value.sendUserMessageContent([ client.value.sendUserMessageContent([
{ {
type: "input_text", type: 'input_text',
text: "你好,我是极客学长!" text: '你好,我是极客学长!',
} },
]); ])
if (client.value.getTurnDetectionType() === "server_vad") { if (client.value.getTurnDetectionType() === 'server_vad') {
await wavRecorder.value.record((data) => await wavRecorder.value.record((data) => client.value.appendInputAudio(data.mono))
client.value.appendInputAudio(data.mono)
);
} }
} catch (e) { } catch (e) {
console.error(e); console.error(e)
} }
}; }
// 开始语音输入 // 开始语音输入
const startRecording = async () => { const startRecording = async () => {
if (isRecording.value) { if (isRecording.value) {
return; return
} }
isRecording.value = true; isRecording.value = true
try { try {
const trackSampleOffset = await wavStreamPlayer.value.interrupt(); const trackSampleOffset = await wavStreamPlayer.value.interrupt()
if (trackSampleOffset?.trackId) { if (trackSampleOffset?.trackId) {
const { trackId, offset } = trackSampleOffset; const { trackId, offset } = trackSampleOffset
client.value.cancelResponse(trackId, offset); client.value.cancelResponse(trackId, offset)
} }
await wavRecorder.value.record((data) => await wavRecorder.value.record((data) => client.value.appendInputAudio(data.mono))
client.value.appendInputAudio(data.mono)
);
} catch (e) { } catch (e) {
console.error(e); console.error(e)
} }
}; }
// 结束语音输入 // 结束语音输入
const stopRecording = async () => { const stopRecording = async () => {
try { try {
isRecording.value = false; isRecording.value = false
await wavRecorder.value.pause(); await wavRecorder.value.pause()
client.value.createResponse(); client.value.createResponse()
} catch (e) { } catch (e) {
console.error(e); console.error(e)
} }
}; }
// const changeTurnEndType = async (value) => { // const changeTurnEndType = async (value) => {
// if (value === 'none' && wavRecorder.value.getStatus() === 'recording') { // if (value === 'none' && wavRecorder.value.getStatus() === 'recording') {
@@ -228,108 +220,108 @@ const stopRecording = async () => {
// 初始化 WaveRecorder 组件和 RealtimeClient 事件处理 // 初始化 WaveRecorder 组件和 RealtimeClient 事件处理
const initialize = async () => { const initialize = async () => {
// Set up render loops for the visualization canvas // Set up render loops for the visualization canvas
let isLoaded = true; let isLoaded = true
const render = () => { const render = () => {
if (isLoaded) { if (isLoaded) {
if (clientCanvasRef.value) { if (clientCanvasRef.value) {
const canvas = clientCanvasRef.value; const canvas = clientCanvasRef.value
if (!canvas.width || !canvas.height) { if (!canvas.width || !canvas.height) {
canvas.width = canvas.offsetWidth; canvas.width = canvas.offsetWidth
canvas.height = canvas.offsetHeight; canvas.height = canvas.offsetHeight
} }
const ctx = canvas.getContext("2d"); const ctx = canvas.getContext('2d')
if (ctx) { if (ctx) {
ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.clearRect(0, 0, canvas.width, canvas.height)
const result = wavRecorder.value.recording const result = wavRecorder.value.recording
? wavRecorder.value.getFrequencies("voice") ? wavRecorder.value.getFrequencies('voice')
: { values: new Float32Array([0]) }; : { values: new Float32Array([0]) }
WavRenderer.drawBars(canvas, ctx, result.values, "#0099ff", 10, 0, 8); WavRenderer.drawBars(canvas, ctx, result.values, '#0099ff', 10, 0, 8)
} }
} }
if (serverCanvasRef.value) { if (serverCanvasRef.value) {
const canvas = serverCanvasRef.value; const canvas = serverCanvasRef.value
if (!canvas.width || !canvas.height) { if (!canvas.width || !canvas.height) {
canvas.width = canvas.offsetWidth; canvas.width = canvas.offsetWidth
canvas.height = canvas.offsetHeight; canvas.height = canvas.offsetHeight
} }
const ctx = canvas.getContext("2d"); const ctx = canvas.getContext('2d')
if (ctx) { if (ctx) {
ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.clearRect(0, 0, canvas.width, canvas.height)
const result = wavStreamPlayer.value.analyser const result = wavStreamPlayer.value.analyser
? wavStreamPlayer.value.getFrequencies("voice") ? wavStreamPlayer.value.getFrequencies('voice')
: { values: new Float32Array([0]) }; : { values: new Float32Array([0]) }
WavRenderer.drawBars(canvas, ctx, result.values, "#009900", 10, 0, 8); WavRenderer.drawBars(canvas, ctx, result.values, '#009900', 10, 0, 8)
} }
} }
requestAnimationFrame(render); requestAnimationFrame(render)
} }
}; }
render(); render()
client.value.on("error", (event) => { client.value.on('error', (event) => {
showMessageError(event.error); showMessageError(event.error)
}); })
client.value.on("realtime.event", (re) => { client.value.on('realtime.event', (re) => {
if (re.event.type === "error") { if (re.event.type === 'error') {
showMessageError(re.event.error); showMessageError(re.event.error)
} }
}); })
client.value.on("conversation.interrupted", async () => { client.value.on('conversation.interrupted', async () => {
const trackSampleOffset = await wavStreamPlayer.value.interrupt(); const trackSampleOffset = await wavStreamPlayer.value.interrupt()
if (trackSampleOffset?.trackId) { if (trackSampleOffset?.trackId) {
const { trackId, offset } = trackSampleOffset; const { trackId, offset } = trackSampleOffset
client.value.cancelResponse(trackId, offset); client.value.cancelResponse(trackId, offset)
} }
}); })
client.value.on("conversation.updated", async ({ item, delta }) => { client.value.on('conversation.updated', async ({ item, delta }) => {
// console.log('item updated', item, delta) // console.log('item updated', item, delta)
if (delta?.audio) { if (delta?.audio) {
wavStreamPlayer.value.add16BitPCM(delta.audio, item.id); wavStreamPlayer.value.add16BitPCM(delta.audio, item.id)
} }
}); })
}; }
const voiceInterval = ref(null); const voiceInterval = ref(null)
onMounted(() => { onMounted(() => {
initialize(); initialize()
// 启动聊天进行中的动画 // 启动聊天进行中的动画
voiceInterval.value = setInterval(animateVoice, 200); voiceInterval.value = setInterval(animateVoice, 200)
typeText(); typeText()
}); })
onUnmounted(() => { onUnmounted(() => {
clearInterval(voiceInterval.value); clearInterval(voiceInterval.value)
client.value.reset(); client.value.reset()
}); })
// 挂断通话 // 挂断通话
const hangUp = async () => { const hangUp = async () => {
try { try {
isConnected.value = false; isConnected.value = false
// 停止播放拨号音乐 // 停止播放拨号音乐
if (backgroundAudio.value?.currentTime) { if (backgroundAudio.value?.currentTime) {
backgroundAudio.value?.pause(); backgroundAudio.value?.pause()
backgroundAudio.value.currentTime = 0; backgroundAudio.value.currentTime = 0
} }
// 断开客户端的连接 // 断开客户端的连接
client.value.reset(); client.value.reset()
// 中断语音输入和输出服务 // 中断语音输入和输出服务
await wavRecorder.value.end(); await wavRecorder.value.end()
await wavStreamPlayer.value.interrupt(); await wavStreamPlayer.value.interrupt()
} catch (e) { } catch (e) {
console.error(e); console.error(e)
} finally { } finally {
// 播放挂断音乐 // 播放挂断音乐
hangUpAudio.value?.play(); hangUpAudio.value?.play()
emits("close"); emits('close')
} }
}; }
// eslint-disable-next-line no-undef // eslint-disable-next-line no-undef
defineExpose({ connect, hangUp }); defineExpose({ connect, hangUp })
</script> </script>
<style scoped lang="stylus"> <style scoped lang="stylus">

View File

@@ -58,63 +58,63 @@
</div> </div>
</template> </template>
<script setup> <script setup>
import {onMounted, ref} from "vue"; import { getSystemInfo } from '@/store/cache'
import {ElMessage} from "element-plus"; import { ElMessage } from 'element-plus'
import {getSystemInfo} from "@/store/cache"; import { onMounted, ref } from 'vue'
const title = ref(process.env.VUE_APP_TITLE); const title = ref(import.meta.env.VITE_TITLE)
const version = ref(process.env.VUE_APP_VERSION); const version = ref(import.meta.env.VITE_VERSION)
const samples = ref([ const samples = ref([
"用小学生都能听懂的术语解释什么是量子纠缠", '用小学生都能听懂的术语解释什么是量子纠缠',
"能给一位6岁男孩的生日会提供一些创造性的建议吗", '能给一位6岁男孩的生日会提供一些创造性的建议吗',
"如何用 Go 语言实现支持代理 Http client 请求?" '如何用 Go 语言实现支持代理 Http client 请求?',
]); ])
const plugins = ref([ const plugins = ref([
{ {
value: "今日早报", value: '今日早报',
text: "今日早报:获取当天全球的热门新闻事件列表" text: '今日早报:获取当天全球的热门新闻事件列表',
}, },
{ {
value: "微博热搜", value: '微博热搜',
text: "微博热搜:新浪微博热搜榜,微博当日热搜榜单" text: '微博热搜:新浪微博热搜榜,微博当日热搜榜单',
}, },
{ {
value: "今日头条", value: '今日头条',
text: "今日头条:给用户推荐当天的头条新闻,周榜热文" text: '今日头条:给用户推荐当天的头条新闻,周榜热文',
} },
]); ])
const capabilities = ref([ const capabilities = ref([
{ {
text: "轻松扮演翻译专家程序员AI 女友,文案高手...", text: '轻松扮演翻译专家程序员AI 女友,文案高手...',
value: "" value: '',
}, },
{ {
text: "国产大语言模型支持百度文心科大讯飞ChatGLM...", text: '国产大语言模型支持百度文心科大讯飞ChatGLM...',
value: "" value: '',
}, },
{ {
text: "绘画马斯克开拖拉机20世纪中国农村。3:2", text: '绘画马斯克开拖拉机20世纪中国农村。3:2',
value: "绘画马斯克开拖拉机20世纪中国农村。3:2" value: '绘画马斯克开拖拉机20世纪中国农村。3:2',
} },
]); ])
onMounted(() => { onMounted(() => {
getSystemInfo() getSystemInfo()
.then((res) => { .then((res) => {
title.value = res.data.title; title.value = res.data.title
}) })
.catch((e) => { .catch((e) => {
ElMessage.error("获取系统配置失败:" + e.message); ElMessage.error('获取系统配置失败:' + e.message)
}); })
}); })
const emits = defineEmits(["send"]); const emits = defineEmits(['send'])
const send = (text) => { const send = (text) => {
emits("send", text); emits('send', text)
}; }
</script> </script>
<style scoped lang="stylus"> <style scoped lang="stylus">
.welcome { .welcome {

View File

@@ -12,14 +12,22 @@
<div class="breadcrumb"> <div class="breadcrumb">
<el-breadcrumb :separator-icon="ArrowRight"> <el-breadcrumb :separator-icon="ArrowRight">
<el-breadcrumb-item v-for="item in breadcrumb" :key="item.title">{{ item.title }}</el-breadcrumb-item> <el-breadcrumb-item v-for="item in breadcrumb" :key="item.title">{{
item.title
}}</el-breadcrumb-item>
</el-breadcrumb> </el-breadcrumb>
</div> </div>
<div class="header-right"> <div class="header-right">
<div class="header-user-con"> <div class="header-user-con">
<!-- 切换主题 --> <!-- 切换主题 -->
<el-switch style="margin-right: 10px" v-model="dark" inline-prompt :active-action-icon="Moon" <el-switch
:inactive-action-icon="Sunny" @change="changeTheme"/> style="margin-right: 10px"
v-model="dark"
inline-prompt
:active-action-icon="Moon"
:inactive-action-icon="Sunny"
@change="changeTheme"
/>
<!-- 用户名下拉菜单 --> <!-- 用户名下拉菜单 -->
<el-dropdown class="user-name" :hide-on-click="true" trigger="click"> <el-dropdown class="user-name" :hide-on-click="true" trigger="click">
<span class="el-dropdown-link"> <span class="el-dropdown-link">
@@ -30,7 +38,9 @@
</span> </span>
<template #dropdown> <template #dropdown>
<el-dropdown-menu> <el-dropdown-menu>
<el-dropdown-item><i class="iconfont icon-version"></i> 当前版本{{ version }}</el-dropdown-item> <el-dropdown-item
><i class="iconfont icon-version"></i> 当前版本{{ version }}</el-dropdown-item
>
<el-dropdown-item divided @click="logout"> <el-dropdown-item divided @click="logout">
<i class="iconfont icon-logout"></i> <i class="iconfont icon-logout"></i>
<span>退出登录</span> <span>退出登录</span>
@@ -43,103 +53,103 @@
</div> </div>
</template> </template>
<script setup> <script setup>
import {onMounted, ref, watch} from "vue"; import { removeAdminToken } from '@/store/session'
import {getMenuItems, useSidebarStore} from "@/store/sidebar"; import { useSharedStore } from '@/store/sharedata'
import {useRouter} from "vue-router"; import { getMenuItems, useSidebarStore } from '@/store/sidebar'
import {ArrowDown, ArrowRight, Expand, Fold, Moon, Sunny} from "@element-plus/icons-vue"; import { httpGet } from '@/utils/http'
import {httpGet} from "@/utils/http"; import { ArrowDown, ArrowRight, Expand, Fold, Moon, Sunny } from '@element-plus/icons-vue'
import {ElMessage} from "element-plus"; import { ElMessage } from 'element-plus'
import {removeAdminToken} from "@/store/session"; import { onMounted, ref, watch } from 'vue'
import {useSharedStore} from "@/store/sharedata"; import { useRouter } from 'vue-router'
const version = ref(process.env.VUE_APP_VERSION); const version = ref(import.meta.env.VITE_VERSION)
const avatar = ref("/images/user-info.jpg"); const avatar = ref('/images/user-info.jpg')
const sidebar = useSidebarStore(); const sidebar = useSidebarStore()
const router = useRouter(); const router = useRouter()
const breadcrumb = ref([]); const breadcrumb = ref([])
const store = useSharedStore(); const store = useSharedStore()
const dark = ref(store.theme === "dark"); const dark = ref(store.theme === 'dark')
const theme = ref(store.theme); const theme = ref(store.theme)
watch( watch(
() => store.theme, () => store.theme,
(val) => { (val) => {
theme.value = val; theme.value = val
} }
); )
const changeTheme = () => { const changeTheme = () => {
store.setTheme(dark.value ? "dark" : "light"); store.setTheme(dark.value ? 'dark' : 'light')
}; }
router.afterEach((to) => { router.afterEach((to) => {
initBreadCrumb(to.path); initBreadCrumb(to.path)
}); })
onMounted(() => { onMounted(() => {
initBreadCrumb(router.currentRoute.value.path); initBreadCrumb(router.currentRoute.value.path)
}); })
// 初始化面包屑导航 // 初始化面包屑导航
const initBreadCrumb = (path) => { const initBreadCrumb = (path) => {
breadcrumb.value = [{ title: "首页" }]; breadcrumb.value = [{ title: '首页' }]
const items = getMenuItems(); const items = getMenuItems()
if (items) { if (items) {
let bk = false; let bk = false
for (let i = 0; i < items.length; i++) { for (let i = 0; i < items.length; i++) {
if (items[i].index === path) { if (items[i].index === path) {
breadcrumb.value.push({ breadcrumb.value.push({
title: items[i].title, title: items[i].title,
path: items[i].index, path: items[i].index,
}); })
break; break
} }
if (bk) { if (bk) {
break; break
} }
if (items[i]["subs"]) { if (items[i]['subs']) {
const subs = items[i]["subs"]; const subs = items[i]['subs']
for (let j = 0; j < subs.length; j++) { for (let j = 0; j < subs.length; j++) {
if (subs[j].index === path) { if (subs[j].index === path) {
breadcrumb.value.push({ breadcrumb.value.push({
title: items[i].title, title: items[i].title,
path: items[i].index, path: items[i].index,
}); })
breadcrumb.value.push({ breadcrumb.value.push({
title: subs[j].title, title: subs[j].title,
path: subs[j].index, path: subs[j].index,
}); })
bk = true; bk = true
break; break
} }
} }
} }
} }
} }
}; }
// 侧边栏折叠 // 侧边栏折叠
const collapseChange = () => { const collapseChange = () => {
sidebar.handleCollapse(); sidebar.handleCollapse()
}; }
onMounted(() => { onMounted(() => {
if (document.body.clientWidth < 1024) { if (document.body.clientWidth < 1024) {
collapseChange(); collapseChange()
} }
}); })
const logout = function () { const logout = function () {
httpGet("/api/admin/logout") httpGet('/api/admin/logout')
.then(() => { .then(() => {
removeAdminToken(); removeAdminToken()
router.replace("/admin/login"); router.replace('/admin/login')
}) })
.catch((e) => { .catch((e) => {
ElMessage.error("注销失败: " + e.message); ElMessage.error('注销失败: ' + e.message)
}); })
}; }
</script> </script>
<style scoped lang="stylus"> <style scoped lang="stylus">
.admin-header { .admin-header {

View File

@@ -1,39 +1,39 @@
import {randString} from "@/utils/libs"; import { removeAdminInfo } from '@/store/cache'
import Storage from "good-storage"; import { randString } from '@/utils/libs'
import {removeAdminInfo} from "@/store/cache"; import Storage from 'good-storage'
/** /**
* storage handler * storage handler
*/ */
const UserTokenKey = process.env.VUE_APP_KEY_PREFIX + "Authorization"; const UserTokenKey = import.meta.env.VITE_KEY_PREFIX + 'Authorization'
const AdminTokenKey = process.env.VUE_APP_KEY_PREFIX + "Admin-Authorization" const AdminTokenKey = import.meta.env.VITE_KEY_PREFIX + 'Admin-Authorization'
export function getSessionId() { export function getSessionId() {
return randString(42) return randString(42)
} }
export function getUserToken() { export function getUserToken() {
return Storage.get(UserTokenKey) ?? "" return Storage.get(UserTokenKey) ?? ''
} }
export function setUserToken(token) { export function setUserToken(token) {
// 刷新 session 缓存 // 刷新 session 缓存
Storage.set(UserTokenKey, token) Storage.set(UserTokenKey, token)
} }
export function removeUserToken() { export function removeUserToken() {
Storage.remove(UserTokenKey) Storage.remove(UserTokenKey)
} }
export function getAdminToken() { export function getAdminToken() {
return Storage.get(AdminTokenKey) ?? "" return Storage.get(AdminTokenKey) ?? ''
} }
export function setAdminToken(token) { export function setAdminToken(token) {
Storage.set(AdminTokenKey, token) Storage.set(AdminTokenKey, token)
} }
export function removeAdminToken() { export function removeAdminToken() {
Storage.remove(AdminTokenKey) Storage.remove(AdminTokenKey)
removeAdminInfo() removeAdminInfo()
} }

View File

@@ -5,50 +5,50 @@
// * @Author yangjian102621@163.com // * @Author yangjian102621@163.com
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
import Storage from "good-storage"; import Storage from 'good-storage'
export function GetFileIcon(ext) { export function GetFileIcon(ext) {
const files = { const files = {
".docx": "doc.png", '.docx': 'doc.png',
".doc": "doc.png", '.doc': 'doc.png',
".xls": "xls.png", '.xls': 'xls.png',
".xlsx": "xls.png", '.xlsx': 'xls.png',
".csv": "xls.png", '.csv': 'xls.png',
".ppt": "ppt.png", '.ppt': 'ppt.png',
".pptx": "ppt.png", '.pptx': 'ppt.png',
".md": "md.png", '.md': 'md.png',
".pdf": "pdf.png", '.pdf': 'pdf.png',
".sql": "sql.png", '.sql': 'sql.png',
".mp3": "mp3.png", '.mp3': 'mp3.png',
".wav": "mp3.png", '.wav': 'mp3.png',
".mp4": "mp4.png", '.mp4': 'mp4.png',
".avi": "mp4.png", '.avi': 'mp4.png',
} }
if (files[ext]) { if (files[ext]) {
return '/images/ext/' + files[ext] return '/images/ext/' + files[ext]
} }
return '/images/ext/file.png' return '/images/ext/file.png'
} }
// 获取文件类型 // 获取文件类型
export function GetFileType (ext) { export function GetFileType(ext) {
return ext.replace(".", "").toUpperCase() return ext.replace('.', '').toUpperCase()
} }
// 将文件大小转成字符 // 将文件大小转成字符
export function FormatFileSize(bytes) { export function FormatFileSize(bytes) {
if (bytes === 0) return '0 Bytes'; if (bytes === 0) return '0 Bytes'
const k = 1024; const k = 1024
const sizes = ['Bytes', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB']; const sizes = ['Bytes', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB']
const i = Math.floor(Math.log(bytes) / Math.log(k)); const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
} }
export function setRoute(path) { export function setRoute(path) {
Storage.set(process.env.VUE_APP_KEY_PREFIX + 'ROUTE_',path) Storage.set(import.meta.env.VITE_KEY_PREFIX + 'ROUTE_', path)
} }
export function getRoute() { export function getRoute() {
return Storage.get(process.env.VUE_APP_KEY_PREFIX + 'ROUTE_') return Storage.get(import.meta.env.VITE_KEY_PREFIX + 'ROUTE_')
} }

View File

@@ -168,7 +168,7 @@ const routerViewKey = ref(0)
const showConfigDialog = ref(false) const showConfigDialog = ref(false)
const license = ref({ de_copy: true }) const license = ref({ de_copy: true })
const showLoginDialog = ref(false) const showLoginDialog = ref(false)
const githubURL = ref(process.env.VUE_APP_GITHUB_URL) const githubURL = ref(import.meta.env.VITE_GITHUB_URL)
/** /**
* 从路径名中提取第一个路径段 * 从路径名中提取第一个路径段

View File

@@ -102,9 +102,9 @@ const slogan = ref('')
const license = ref({ de_copy: true }) const license = ref({ de_copy: true })
const isLogin = ref(false) const isLogin = ref(false)
const docsURL = ref(process.env.VUE_APP_DOCS_URL) const docsURL = ref(import.meta.env.VITE_DOCS_URL)
const githubURL = ref(process.env.VUE_APP_GITHUB_URL) const githubURL = ref(import.meta.env.VITE_GITHUB_URL)
const giteeURL = ref(process.env.VUE_APP_GITEE_URL) const giteeURL = ref(import.meta.env.VITE_GITEE_URL)
const navs = ref([]) const navs = ref([])
const iconMap = ref({ const iconMap = ref({

View File

@@ -92,8 +92,8 @@ const enableVerify = ref(false)
const captchaRef = ref(null) const captchaRef = ref(null)
const ruleFormRef = ref(null) const ruleFormRef = ref(null)
const ruleForm = reactive({ const ruleForm = reactive({
username: process.env.VUE_APP_USER, username: import.meta.env.VITE_USER,
password: process.env.VUE_APP_PASS, password: import.meta.env.VITE_PASS,
agreement: false, agreement: false,
}) })
const rules = { const rules = {

View File

@@ -15,11 +15,25 @@
<div class="prompt-container"> <div class="prompt-container">
<div class="input-container"> <div class="input-container">
<div class="upload-icon" v-if="images.length < 2"> <div class="upload-icon" v-if="images.length < 2">
<el-upload class="avatar-uploader" :auto-upload="true" :show-file-list="false" :http-request="upload" accept=".jpg,.png,.jpeg"> <el-upload
class="avatar-uploader"
:auto-upload="true"
:show-file-list="false"
:http-request="upload"
accept=".jpg,.png,.jpeg"
>
<i class="iconfont icon-image"></i> <i class="iconfont icon-image"></i>
</el-upload> </el-upload>
</div> </div>
<textarea class="prompt-input" :rows="row" v-model="formData.prompt" maxlength="2000" placeholder="请输入提示词或者上传图片" autofocus> </textarea> <textarea
class="prompt-input"
:rows="row"
v-model="formData.prompt"
maxlength="2000"
placeholder="请输入提示词或者上传图片"
autofocus
>
</textarea>
<div class="send-icon" @click="create"> <div class="send-icon" @click="create">
<i class="iconfont icon-send"></i> <i class="iconfont icon-send"></i>
</div> </div>
@@ -44,7 +58,11 @@
</div> </div>
</div> </div>
<el-container class="video-container" v-loading="loading" element-loading-background="rgba(100,100,100,0.3)"> <el-container
class="video-container"
v-loading="loading"
element-loading-background="rgba(100,100,100,0.3)"
>
<h2 class="h-title text-2xl mb-5 mt-2">你的作品</h2> <h2 class="h-title text-2xl mb-5 mt-2">你的作品</h2>
<div class="list-box" v-if="!noData"> <div class="list-box" v-if="!noData">
@@ -53,7 +71,15 @@
<div class="left"> <div class="left">
<div class="container"> <div class="container">
<div v-if="item.progress === 100"> <div v-if="item.progress === 100">
<video class="video" :src="replaceImg(item.video_url)" preload="auto" loop="loop" muted="muted">您的浏览器不支持视频播放</video> <video
class="video"
:src="replaceImg(item.video_url)"
preload="auto"
loop="loop"
muted="muted"
>
您的浏览器不支持视频播放
</video>
<button class="play flex justify-center items-center" @click="play(item)"> <button class="play flex justify-center items-center" @click="play(item)">
<img src="/images/play.svg" alt="" /> <img src="/images/play.svg" alt="" />
</button> </button>
@@ -63,7 +89,9 @@
</div> </div>
</div> </div>
<div class="center"> <div class="center">
<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>{{ item.prompt }}</div> <div class="prompt" v-else>{{ item.prompt }}</div>
</div> </div>
<div class="right" v-if="item.progress === 100"> <div class="right" v-if="item.progress === 100">
@@ -100,7 +128,12 @@
</div> </div>
</div> </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"> <div class="pagination">
<el-pagination <el-pagination
@@ -116,8 +149,22 @@
/> />
</div> </div>
</el-container> </el-container>
<black-dialog v-model:show="showDialog" title="预览视频" hide-footer @cancal="showDialog = false" width="auto"> <black-dialog
<video style="max-width: 90vw; max-height: 90vh" :src="currentVideoUrl" preload="auto" :autoplay="true" loop="loop" muted="muted" v-show="showDialog"> v-model:show="showDialog"
title="预览视频"
hide-footer
@cancal="showDialog = false"
width="auto"
>
<video
style="max-width: 90vw; max-height: 90vh"
:src="currentVideoUrl"
preload="auto"
:autoplay="true"
loop="loop"
muted="muted"
v-show="showDialog"
>
您的浏览器不支持视频播放 您的浏览器不支持视频播放
</video> </video>
</black-dialog> </black-dialog>
@@ -125,215 +172,214 @@
</template> </template>
<script setup> <script setup>
import nodata from "@/assets/img/no-data.png"; import nodata from '@/assets/img/no-data.png'
import { onMounted, onUnmounted, reactive, ref } from "vue"; import BlackDialog from '@/components/ui/BlackDialog.vue'
import { CircleCloseFilled } from "@element-plus/icons-vue"; import Generating from '@/components/ui/Generating.vue'
import { httpDownload, httpPost, httpGet } from "@/utils/http"; import { checkSession } from '@/store/cache'
import { checkSession } from "@/store/cache"; import { closeLoading, showLoading, showMessageError, showMessageOK } from '@/utils/dialog'
import { closeLoading, showLoading, showMessageError, showMessageOK } from "@/utils/dialog"; import { httpDownload, httpGet, httpPost } from '@/utils/http'
import { replaceImg } from "@/utils/libs"; import { replaceImg } from '@/utils/libs'
import { ElMessage, ElMessageBox } from "element-plus"; import { CircleCloseFilled } from '@element-plus/icons-vue'
import BlackSwitch from "@/components/ui/BlackSwitch.vue"; import Clipboard from 'clipboard'
import Generating from "@/components/ui/Generating.vue"; import { ElMessage, ElMessageBox } from 'element-plus'
import BlackDialog from "@/components/ui/BlackDialog.vue"; import { onMounted, onUnmounted, reactive, ref } from 'vue'
import Clipboard from "clipboard"; const showDialog = ref(false)
const showDialog = ref(false); const currentVideoUrl = ref('')
const currentVideoUrl = ref(""); const row = ref(1)
const row = ref(1); const images = ref([])
const images = ref([]);
const formData = reactive({ const formData = reactive({
prompt: "", prompt: '',
expand_prompt: false, expand_prompt: false,
loop: false, loop: false,
first_frame_img: "", first_frame_img: '',
end_frame_img: "", end_frame_img: '',
}); })
const loading = ref(false); const loading = ref(false)
const list = ref([]); const list = ref([])
const noData = ref(true); const noData = ref(true)
const page = ref(1); const page = ref(1)
const pageSize = ref(10); const pageSize = ref(10)
const total = ref(0); const total = ref(0)
const taskPulling = ref(true); const taskPulling = ref(true)
const clipboard = ref(null); const clipboard = ref(null)
const pullHandler = ref(null); const pullHandler = ref(null)
onMounted(() => { onMounted(() => {
checkSession().then(() => { checkSession().then(() => {
fetchData(1); fetchData(1)
// 设置轮询 // 设置轮询
pullHandler.value = setInterval(() => { pullHandler.value = setInterval(() => {
if (taskPulling.value) { if (taskPulling.value) {
fetchData(1); fetchData(1)
} }
}, 5000); }, 5000)
}); })
clipboard.value = new Clipboard(".copy-prompt"); clipboard.value = new Clipboard('.copy-prompt')
clipboard.value.on("success", () => { clipboard.value.on('success', () => {
ElMessage.success("复制成功!"); ElMessage.success('复制成功!')
}); })
}); })
onUnmounted(() => { onUnmounted(() => {
clipboard.value.destroy(); clipboard.value.destroy()
if (pullHandler.value) { if (pullHandler.value) {
clearInterval(pullHandler.value); clearInterval(pullHandler.value)
} }
}); })
const download = (item) => { const download = (item) => {
const url = replaceImg(item.video_url); const url = replaceImg(item.video_url)
const downloadURL = `${process.env.VUE_APP_API_HOST}/api/download?url=${url}`; const downloadURL = `${import.meta.env.VITE_API_HOST}/api/download?url=${url}`
const urlObj = new URL(url); const urlObj = new URL(url)
const fileName = urlObj.pathname.split("/").pop(); const fileName = urlObj.pathname.split('/').pop()
item.downloading = true; item.downloading = true
httpDownload(downloadURL) httpDownload(downloadURL)
.then((response) => { .then((response) => {
const blob = new Blob([response.data]); const blob = new Blob([response.data])
const link = document.createElement("a"); const link = document.createElement('a')
link.href = URL.createObjectURL(blob); link.href = URL.createObjectURL(blob)
link.download = fileName; link.download = fileName
document.body.appendChild(link); document.body.appendChild(link)
link.click(); link.click()
document.body.removeChild(link); document.body.removeChild(link)
URL.revokeObjectURL(link.href); URL.revokeObjectURL(link.href)
item.downloading = false; item.downloading = false
}) })
.catch(() => { .catch(() => {
showMessageError("下载失败"); showMessageError('下载失败')
item.downloading = false; item.downloading = false
}); })
}; }
const play = (item) => { const play = (item) => {
currentVideoUrl.value = replaceImg(item.video_url); currentVideoUrl.value = replaceImg(item.video_url)
showDialog.value = true; showDialog.value = true
}; }
const removeJob = (item) => { const removeJob = (item) => {
ElMessageBox.confirm("此操作将会删除任务相关文件,继续操作码?", "删除提示", { ElMessageBox.confirm('此操作将会删除任务相关文件,继续操作码?', '删除提示', {
confirmButtonText: "确认", confirmButtonText: '确认',
cancelButtonText: "取消", cancelButtonText: '取消',
type: "warning", type: 'warning',
}) })
.then(() => { .then(() => {
httpGet("/api/video/remove", { id: item.id }) httpGet('/api/video/remove', { id: item.id })
.then(() => { .then(() => {
ElMessage.success("任务删除成功"); ElMessage.success('任务删除成功')
fetchData(); fetchData()
}) })
.catch((e) => { .catch((e) => {
ElMessage.error("任务删除失败:" + e.message); ElMessage.error('任务删除失败:' + e.message)
}); })
}) })
.catch(() => {}); .catch(() => {})
}; }
const publishJob = (item) => { const publishJob = (item) => {
httpGet("/api/video/publish", { id: item.id, publish: item.publish }) httpGet('/api/video/publish', { id: item.id, publish: item.publish })
.then(() => { .then(() => {
ElMessage.success("操作成功"); ElMessage.success('操作成功')
}) })
.catch((e) => { .catch((e) => {
ElMessage.error("操作失败:" + e.message); ElMessage.error('操作失败:' + e.message)
}); })
}; }
const upload = (file) => { const upload = (file) => {
const formData = new FormData(); const formData = new FormData()
formData.append("file", file.file, file.name); formData.append('file', file.file, file.name)
showLoading("正在上传文件..."); showLoading('正在上传文件...')
httpPost("/api/upload", formData) httpPost('/api/upload', formData)
.then((res) => { .then((res) => {
images.value.push(res.data.url); images.value.push(res.data.url)
ElMessage.success({ message: "上传成功", duration: 500 }); ElMessage.success({ message: '上传成功', duration: 500 })
closeLoading(); closeLoading()
}) })
.catch((e) => { .catch((e) => {
ElMessage.error("图片上传失败:" + e.message); ElMessage.error('图片上传失败:' + e.message)
closeLoading(); closeLoading()
}); })
}; }
const remove = (img) => { const remove = (img) => {
images.value = images.value.filter((item) => item !== img); images.value = images.value.filter((item) => item !== img)
}; }
const switchReverse = () => { const switchReverse = () => {
images.value = images.value.reverse(); images.value = images.value.reverse()
}; }
const fetchData = (_page) => { const fetchData = (_page) => {
if (_page) { if (_page) {
page.value = _page; page.value = _page
} }
httpGet("/api/video/list", { httpGet('/api/video/list', {
page: page.value, page: page.value,
page_size: pageSize.value, page_size: pageSize.value,
type: "luma", type: 'luma',
}) })
.then((res) => { .then((res) => {
total.value = res.data.total; total.value = res.data.total
let needPull = false; let needPull = false
const items = []; const items = []
for (let v of res.data.items) { for (let v of res.data.items) {
if (v.progress === 0 || v.progress === 102) { if (v.progress === 0 || v.progress === 102) {
needPull = true; needPull = true
} }
items.push(v); items.push(v)
} }
loading.value = false; loading.value = false
taskPulling.value = needPull; taskPulling.value = needPull
if (JSON.stringify(list.value) !== JSON.stringify(items)) { 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(() => { .catch(() => {
loading.value = false; loading.value = false
noData.value = true; noData.value = true
}); })
}; }
const create = () => { const create = () => {
const len = images.value.length; const len = images.value.length
if (len) { if (len) {
formData.first_frame_img = replaceImg(images.value[0]); formData.first_frame_img = replaceImg(images.value[0])
if (len === 2) { if (len === 2) {
formData.end_frame_img = replaceImg(images.value[1]); formData.end_frame_img = replaceImg(images.value[1])
} }
} }
httpPost("/api/video/luma/create", formData) httpPost('/api/video/luma/create', formData)
.then(() => { .then(() => {
fetchData(1); fetchData(1)
taskPulling.value = true; taskPulling.value = true
showMessageOK("创建任务成功"); showMessageOK('创建任务成功')
}) })
.catch((e) => { .catch((e) => {
showMessageError("创建任务失败:" + e.message); showMessageError('创建任务失败:' + e.message)
}); })
}; }
const generatePrompt = () => { const generatePrompt = () => {
if (formData.prompt === "") { if (formData.prompt === '') {
return showMessageError("请输入原始提示词"); return showMessageError('请输入原始提示词')
} }
showLoading("正在生成视频脚本..."); showLoading('正在生成视频脚本...')
httpPost("/api/prompt/video", { prompt: formData.prompt }) httpPost('/api/prompt/video', { prompt: formData.prompt })
.then((res) => { .then((res) => {
formData.prompt = res.data; formData.prompt = res.data
closeLoading(); closeLoading()
}) })
.catch((e) => { .catch((e) => {
showMessageError("生成提示词失败:" + e.message); showMessageError('生成提示词失败:' + e.message)
closeLoading(); closeLoading()
}); })
}; }
</script> </script>
<style lang="stylus" scoped> <style lang="stylus" scoped>

View File

@@ -1,6 +1,11 @@
<template> <template>
<div> <div>
<div class="member custom-scroll" v-loading="loading" element-loading-background="rgba(255,255,255,.3)" :element-loading-text="loadingText"> <div
class="member custom-scroll"
v-loading="loading"
element-loading-background="rgba(255,255,255,.3)"
:element-loading-text="loadingText"
>
<div class="inner"> <div class="inner">
<div class="user-profile"> <div class="user-profile">
<user-profile :key="profileKey" /> <user-profile :key="profileKey" />
@@ -26,7 +31,9 @@
<div class="product-box"> <div class="product-box">
<div class="info" v-if="orderPayInfoText !== ''"> <div class="info" v-if="orderPayInfoText !== ''">
<el-alert type="success" show-icon :closable="false" effect="dark"> <strong>说明:</strong> {{ vipInfoText }} </el-alert> <el-alert type="success" show-icon :closable="false" effect="dark">
<strong>说明:</strong> {{ vipInfoText }}
</el-alert>
</div> </div>
<el-row v-if="list.length > 0" :gutter="20" class="list-box"> <el-row v-if="list.length > 0" :gutter="20" class="list-box">
@@ -61,7 +68,12 @@
</div> </div>
<div class="pay-way"> <div class="pay-way">
<span type="primary" v-for="payWay in payWays" @click="pay(item, payWay)" :key="payWay"> <span
type="primary"
v-for="payWay in payWays"
@click="pay(item, payWay)"
:key="payWay"
>
<el-button v-if="payWay.pay_type === 'alipay'" color="#15A6E8" circle> <el-button v-if="payWay.pay_type === 'alipay'" color="#15A6E8" circle>
<i class="iconfont icon-alipay"></i> <i class="iconfont icon-alipay"></i>
</el-button> </el-button>
@@ -97,14 +109,33 @@
</div> </div>
</div> </div>
<password-dialog v-if="isLogin" :show="showPasswordDialog" @hide="showPasswordDialog = false" /> <password-dialog
<bind-mobile v-if="isLogin" :show="showBindMobileDialog" @hide="showBindMobileDialog = false" /> v-if="isLogin"
:show="showPasswordDialog"
@hide="showPasswordDialog = false"
/>
<bind-mobile
v-if="isLogin"
:show="showBindMobileDialog"
@hide="showBindMobileDialog = false"
/>
<bind-email v-if="isLogin" :show="showBindEmailDialog" @hide="showBindEmailDialog = false" /> <bind-email v-if="isLogin" :show="showBindEmailDialog" @hide="showBindEmailDialog = false" />
<third-login v-if="isLogin" :show="showThirdLoginDialog" @hide="showThirdLoginDialog = false" /> <third-login
v-if="isLogin"
:show="showThirdLoginDialog"
@hide="showThirdLoginDialog = false"
/>
<redeem-verify v-if="isLogin" :show="showRedeemVerifyDialog" @hide="redeemCallback" /> <redeem-verify v-if="isLogin" :show="showRedeemVerifyDialog" @hide="redeemCallback" />
</div> </div>
<el-dialog v-model="showDialog" :show-close="false" :close-on-click-modal="false" hide-footer width="auto" class="pay-dialog"> <el-dialog
v-model="showDialog"
:show-close="false"
:close-on-click-modal="false"
hide-footer
width="auto"
class="pay-dialog"
>
<div v-if="qrImg !== ''"> <div v-if="qrImg !== ''">
<div class="product-info"> <div class="product-info">
请使用微信扫码支付<span class="price">{{ price }}</span> 请使用微信扫码支付<span class="price">{{ price }}</span>
@@ -120,145 +151,145 @@
</template> </template>
<script setup> <script setup>
import nodata from "@/assets/img/no-data.png"; import nodata from '@/assets/img/no-data.png'
import { onMounted, ref } from "vue"; import BindEmail from '@/components/BindEmail.vue'
import { ElMessage } from "element-plus"; import BindMobile from '@/components/BindMobile.vue'
import { httpGet, httpPost } from "@/utils/http"; import PasswordDialog from '@/components/PasswordDialog.vue'
import { checkSession, getSystemInfo } from "@/store/cache"; import RedeemVerify from '@/components/RedeemVerify.vue'
import UserProfile from "@/components/UserProfile.vue"; import ThirdLogin from '@/components/ThirdLogin.vue'
import PasswordDialog from "@/components/PasswordDialog.vue"; import UserOrder from '@/components/UserOrder.vue'
import BindMobile from "@/components/BindMobile.vue"; import UserProfile from '@/components/UserProfile.vue'
import RedeemVerify from "@/components/RedeemVerify.vue"; import { checkSession, getSystemInfo } from '@/store/cache'
import UserOrder from "@/components/UserOrder.vue"; import { useSharedStore } from '@/store/sharedata'
import { useSharedStore } from "@/store/sharedata"; import { httpGet, httpPost } from '@/utils/http'
import BindEmail from "@/components/BindEmail.vue"; import { ElMessage } from 'element-plus'
import ThirdLogin from "@/components/ThirdLogin.vue"; import QRCode from 'qrcode'
import QRCode from "qrcode"; import { onMounted, ref } from 'vue'
const list = ref([]); const list = ref([])
const vipImg = ref("/images/menu/member.png"); const vipImg = ref('/images/menu/member.png')
const enableReward = ref(false); // 是否启用众筹功能 const enableReward = ref(false) // 是否启用众筹功能
const rewardImg = ref("/images/reward.png"); const rewardImg = ref('/images/reward.png')
const showPasswordDialog = ref(false); const showPasswordDialog = ref(false)
const showBindMobileDialog = ref(false); const showBindMobileDialog = ref(false)
const showBindEmailDialog = ref(false); const showBindEmailDialog = ref(false)
const showRedeemVerifyDialog = ref(false); const showRedeemVerifyDialog = ref(false)
const showThirdLoginDialog = ref(false); const showThirdLoginDialog = ref(false)
const user = ref(null); const user = ref(null)
const isLogin = ref(false); const isLogin = ref(false)
const orderTimeout = ref(1800); const orderTimeout = ref(1800)
const loading = ref(true); const loading = ref(true)
const loadingText = ref("加载中..."); const loadingText = ref('加载中...')
const orderPayInfoText = ref(""); const orderPayInfoText = ref('')
const payWays = ref([]); const payWays = ref([])
const vipInfoText = ref(""); const vipInfoText = ref('')
const store = useSharedStore(); const store = useSharedStore()
const profileKey = ref(0); const profileKey = ref(0)
const userOrderKey = ref(0); const userOrderKey = ref(0)
const showDialog = ref(false); const showDialog = ref(false)
const qrImg = ref(""); const qrImg = ref('')
const price = ref(0); const price = ref(0)
onMounted(() => { onMounted(() => {
checkSession() checkSession()
.then((_user) => { .then((_user) => {
user.value = _user; user.value = _user
isLogin.value = true; isLogin.value = true
}) })
.catch(() => { .catch(() => {
store.setShowLoginDialog(true); store.setShowLoginDialog(true)
}); })
httpGet("/api/product/list") httpGet('/api/product/list')
.then((res) => { .then((res) => {
list.value = res.data; list.value = res.data
loading.value = false; loading.value = false
}) })
.catch((e) => { .catch((e) => {
ElMessage.error("获取产品套餐失败:" + e.message); ElMessage.error('获取产品套餐失败:' + e.message)
}); })
getSystemInfo() getSystemInfo()
.then((res) => { .then((res) => {
rewardImg.value = res.data["reward_img"]; rewardImg.value = res.data['reward_img']
enableReward.value = res.data["enabled_reward"]; enableReward.value = res.data['enabled_reward']
orderPayInfoText.value = res.data["order_pay_info_text"]; orderPayInfoText.value = res.data['order_pay_info_text']
if (res.data["order_pay_timeout"] > 0) { if (res.data['order_pay_timeout'] > 0) {
orderTimeout.value = res.data["order_pay_timeout"]; orderTimeout.value = res.data['order_pay_timeout']
} }
vipInfoText.value = res.data["vip_info_text"]; vipInfoText.value = res.data['vip_info_text']
}) })
.catch((e) => { .catch((e) => {
ElMessage.error("获取系统配置失败:" + e.message); ElMessage.error('获取系统配置失败:' + e.message)
}); })
httpGet("/api/payment/payWays") httpGet('/api/payment/payWays')
.then((res) => { .then((res) => {
payWays.value = res.data; payWays.value = res.data
}) })
.catch((e) => { .catch((e) => {
ElMessage.error("获取支付方式失败:" + e.message); ElMessage.error('获取支付方式失败:' + e.message)
}); })
}); })
const pay = (product, payWay) => { const pay = (product, payWay) => {
if (!isLogin.value) { if (!isLogin.value) {
store.setShowLoginDialog(true); store.setShowLoginDialog(true)
return; return
} }
loading.value = true; loading.value = true
loadingText.value = "正在生成支付订单..."; loadingText.value = '正在生成支付订单...'
let host = process.env.VUE_APP_API_HOST; let host = import.meta.env.VITE_API_HOST
if (host === "") { if (host === '') {
host = `${location.protocol}//${location.host}`; host = `${location.protocol}//${location.host}`
} }
httpPost(`${process.env.VUE_APP_API_HOST}/api/payment/doPay`, { httpPost(`${import.meta.env.VITE_API_HOST}/api/payment/doPay`, {
product_id: product.id, product_id: product.id,
pay_way: payWay.pay_way, pay_way: payWay.pay_way,
pay_type: payWay.pay_type, pay_type: payWay.pay_type,
user_id: user.value.id, user_id: user.value.id,
host: host, host: host,
device: "jump", device: 'jump',
}) })
.then((res) => { .then((res) => {
showDialog.value = true; showDialog.value = true
loading.value = false; loading.value = false
if (payWay.pay_way === "wechat") { if (payWay.pay_way === 'wechat') {
price.value = Number(product.discount); price.value = Number(product.discount)
QRCode.toDataURL(res.data, { width: 300, height: 300, margin: 2 }, (error, url) => { QRCode.toDataURL(res.data, { width: 300, height: 300, margin: 2 }, (error, url) => {
if (error) { if (error) {
console.error(error); console.error(error)
} else { } else {
qrImg.value = url; qrImg.value = url
} }
}); })
} else { } else {
window.open(res.data, "_blank"); window.open(res.data, '_blank')
} }
}) })
.catch((e) => { .catch((e) => {
setTimeout(() => { setTimeout(() => {
ElMessage.error("生成支付订单失败:" + e.message); ElMessage.error('生成支付订单失败:' + e.message)
loading.value = false; loading.value = false
}, 500); }, 500)
}); })
}; }
const redeemCallback = (success) => { const redeemCallback = (success) => {
showRedeemVerifyDialog.value = false; showRedeemVerifyDialog.value = false
if (success) { if (success) {
profileKey.value += 1; profileKey.value += 1
} }
}; }
const payCallback = (success) => { const payCallback = (success) => {
showDialog.value = false; showDialog.value = false
if (success) { if (success) {
profileKey.value += 1; profileKey.value += 1
userOrderKey.value += 1; userOrderKey.value += 1
} }
}; }
</script> </script>
<style lang="stylus"> <style lang="stylus">

View File

@@ -46,7 +46,7 @@
placement="right" placement="right"
:width="200" :width="200"
trigger="hover" trigger="hover"
content="描述您想要的音乐风格(例如原声流行音乐”)。Sunos 模特无法识别艺术家的名字但能够理解音乐流派和氛围" content="描述您想要的音乐风格(例如"原声流行音乐"Sunos 模特无法识别艺术家的名字但能够理解音乐流派和氛围"
> >
<template #reference> <template #reference>
<el-icon> <el-icon>
@@ -270,23 +270,23 @@
<script setup> <script setup>
import nodata from "@/assets/img/no-data.png"; import nodata from "@/assets/img/no-data.png";
import { nextTick, onMounted, onUnmounted, ref, watch } from "vue"; import MusicPlayer from "@/components/MusicPlayer.vue";
import { Delete, InfoFilled } from "@element-plus/icons-vue"; import BlackDialog from "@/components/ui/BlackDialog.vue";
import BlackInput from "@/components/ui/BlackInput.vue";
import BlackSelect from "@/components/ui/BlackSelect.vue"; import BlackSelect from "@/components/ui/BlackSelect.vue";
import BlackSwitch from "@/components/ui/BlackSwitch.vue"; import BlackSwitch from "@/components/ui/BlackSwitch.vue";
import BlackInput from "@/components/ui/BlackInput.vue";
import MusicPlayer from "@/components/MusicPlayer.vue";
import { compact } from "lodash";
import { httpDownload, httpGet, httpPost } from "@/utils/http";
import { closeLoading, showLoading, showMessageError, showMessageOK } from "@/utils/dialog";
import { checkSession } from "@/store/cache";
import { ElMessage, ElMessageBox } from "element-plus";
import { formatTime, replaceImg } from "@/utils/libs";
import Clipboard from "clipboard";
import BlackDialog from "@/components/ui/BlackDialog.vue";
import Compressor from "compressorjs";
import Generating from "@/components/ui/Generating.vue"; import Generating from "@/components/ui/Generating.vue";
import { checkSession } from "@/store/cache";
import { useSharedStore } from "@/store/sharedata"; 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([ const models = ref([
@@ -455,7 +455,7 @@ const merge = (item) => {
// 下载歌曲 // 下载歌曲
const download = (item) => { const download = (item) => {
const url = replaceImg(item.audio_url); const url = replaceImg(item.audio_url);
const downloadURL = `${process.env.VUE_APP_API_HOST}/api/download?url=${url}`; const downloadURL = `${import.meta.env.VITE_API_HOST}/api/download?url=${url}`;
// parse filename // parse filename
const urlObj = new URL(url); const urlObj = new URL(url);
const fileName = urlObj.pathname.split("/").pop(); const fileName = urlObj.pathname.split("/").pop();

View File

@@ -68,8 +68,8 @@ import { useRouter } from 'vue-router'
const router = useRouter() const router = useRouter()
const title = ref('Geek-AI Console') const title = ref('Geek-AI Console')
const username = ref(process.env.VUE_APP_ADMIN_USER) const username = ref(import.meta.env.VITE_ADMIN_USER)
const password = ref(process.env.VUE_APP_ADMIN_PASS) const password = ref(import.meta.env.VITE_ADMIN_PASS)
const logo = ref('') const logo = ref('')
const enableVerify = ref(false) const enableVerify = ref(false)
const captchaRef = ref(null) const captchaRef = ref(null)

View File

@@ -93,7 +93,7 @@ import { showNotify } from 'vant'
import { computed, onMounted, ref } from 'vue' import { computed, onMounted, ref } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
const title = ref(process.env.VUE_APP_TITLE) const title = ref(import.meta.env.VITE_TITLE)
const router = useRouter() const router = useRouter()
const isLogin = ref(false) const isLogin = ref(false)
const apps = ref([]) const apps = ref([])

View File

@@ -1,27 +1,46 @@
import { defineConfig } from 'vite'; import vue from '@vitejs/plugin-vue'
import vue from '@vitejs/plugin-vue'; import path from 'path'
import path from 'path'; import { defineConfig, loadEnv } from 'vite'
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig(({ mode }) => {
plugins: [vue()], const env = loadEnv(mode, process.cwd())
resolve: { const apiHost = env.VITE_API_HOST || 'http://localhost:5678'
alias: {
'@': path.resolve(__dirname, 'src'), return {
}, plugins: [vue()],
}, resolve: {
server: { alias: {
port: 8888, '@': path.resolve(__dirname, './src'),
proxy: {
'/api': {
target: process.env.VUE_APP_API_HOST || 'http://localhost:8080', // Fallback if env var is not set
changeOrigin: true,
ws: true,
},
'/static/upload/': {
target: process.env.VUE_APP_API_HOST || 'http://localhost:8080', // Fallback if env var is not set
changeOrigin: true,
}, },
}, },
}, css: {
}); preprocessorOptions: {
stylus: {
additionalData: `@import "@/assets/css/index.styl";`,
},
},
},
optimizeDeps: {
include: ['stylus'],
},
server: {
port: 8888,
...(process.env.NODE_ENV === 'development'
? {
proxy: {
'/api': {
target: apiHost,
changeOrigin: true,
ws: true,
},
'/static/upload/': {
target: apiHost,
changeOrigin: true,
},
},
}
: {}),
},
}
})