一、前言
上篇讲述了 零基础手把手教你如何使用Laf免费玩转Midjourney,下面将讲解如何结合前端完成终极体验!
二、项目搭建
前端技术栈:vue + element plus
1.创建vue项目
这里使用vue脚手架创建项目,搭建项目步骤可参考官网 创建一个项目 | Vue CLIhttps://cli.vuejs.org/zh/guide/creating-a-project.html
2.安装element plus依赖并引入
安装步骤可参考官网 安装 | Element Plushttps://element-plus.gitee.io/zh-CN/guide/installation.html安装完成后在main.js中引入组件
import ElementPlus from 'element-plus'
import * as ElIcons from '@element-plus/icons-vue'
import 'element-plus/dist/index.css'
import locale from 'element-plus/lib/locale/lang/zh-cn'
const app = createApp(App)
for (const name in ElIcons) {
app.component(name, ElIcons[name]);
}
app.use(ElementPlus, { locale })
app.use(store)
app.use(router)
app.mount('#app')
3.代码实现
修改“vue.config.js”文件中的配置,其中“target”为Laf接口访问前缀
module.exports = {
transpileDependencies: true,
lintOnSave: false,
productionSourceMap: false,
devServer: {
proxy: {
'/api': {
target: 'https://xxx.laf.dev',
changeOrigin: true,
ws: false,
pathRewrite: {
'^/api': ''
}
}
}
}
}
接着,完成页面布局和逻辑业务实现,以“home.vue”为例:
<template>
<div class="page">
<el-container>
<el-header ref="elHeader">
<p>欢迎体验Midjourney绘画功能~</p>
<p>绘画指令:[/mj 提示词],例如:/mj a dog</p>
<p>放大指令:[/mj u1-u4],例如:/mj u2</p>
<p>变换指令:[/mj v1-v4],例如:/mj v3</p>
<p class="tips">注意:提示词最好用英文!</p>
</el-header>
<el-main :style="{ height: scrollerHeight + 'px' }">
<el-scrollbar class="msg-list">
<el-row v-for="(item, key) in msgList" :key="key" class="msg-item">
<el-col>
<img v-if="item.type == 1" class="img_1" src="../assets/images/ic_chatgpt.png" />
<el-card :style="{ 'margin-left': (item.type == 2 ? 'auto' : '0') }">
<div class="txt">
<!-- <img class="img" v-if="item.content.indexOf('http') > -1" :src="item.content" /> -->
<el-image v-if="item.content.indexOf('http') > -1" :src="item.content"
:preview-src-list="[item.content]" fit="cover" />
<text v-else>{{ item.content }}</text>
</div>
<div class="time">{{ item.time }}</div>
</el-card>
<img v-if="item.type == 2" class="img_2" src="../assets/images/me.png" />
</el-col>
</el-row>
</el-scrollbar>
</el-main>
<el-footer ref="elFooter">
<el-input v-model="queryInfo.param.prompt" placeholder="/mj 提示词" clearable size="large"
:formatter="(value) => `/mj ${value}`.replace(/\B(?=(\d{3})+(?!\d))/g, ',')"
:parser="(value) => value.replace(/\/mj\s?|(,*)/g, '')" :disabled="isDisabled">
<template #append>
<el-button type="primary" :disabled="isDisabled" @click="sendMsg()">
<el-icon>
<Position />
</el-icon>发送
</el-button>
</template>
</el-input>
</el-footer>
</el-container>
</div>
</template>
<script>
import topBar from '@/components/topBar.vue'
var _this;
export default {
name: 'home',
components: {
topBar
},
data() {
return {
// 容器高度初始化为0
scrollerHeight: 0,
// 查询参数信息
queryInfo: {
type: null,
param: {
msg_Id: null,
prompt: '',
question: null,
index: null,
id: null,
url: null
}
},
// 信息集合
msgList: [],
msgIndex: 0,
isDisabled: false,
// 图片信息
imgInfo: null,
// 操作类型 1-绘图,2-放大,3-变换
operateType: 0,
// 是否重试
isRetry: true
}
},
created() {
_this = this;
// 先读取缓存中的会话信息,没有则新创建一条
const _msgList = localStorage.getItem('msgList');
if (_msgList) {
_this.msgList = JSON.parse(_msgList);
} else {
_this.insertMsg(1, '亲,欢迎加入Midjourney的绘画体验,请输入指令.....');
}
const _imgInfo = localStorage.getItem('imgInfo');
if (_imgInfo) {
_this.imgInfo = JSON.parse(_imgInfo);
}
},
mounted: function () {
_this.calculateHeight();
// 当浏览器窗口大小发生变化时,重新计算容器高度
window.addEventListener('resize', () => {
_this.calculateHeight();
});
},
methods: {
// 计算容器高度
calculateHeight() {
// 获取顶部元素
let elHeader = _this.$refs.elHeader;
// 获取底部元素
let elFooter = _this.$refs.elFooter;
if (elHeader && elFooter) {
_this.scrollerHeight = document.documentElement.clientHeight - elHeader.$el.offsetHeight - elFooter.$el.offsetHeight - 1;
}
},
//发送消息
sendMsg() {
const _prompt = _this.queryInfo.param.prompt.replace(/\/mj\s?|(,*)/g, '').toLowerCase();
if (!_prompt) {
_this.$message.toast({
message: '请输入提示词!'
});
return;
}
if (!_this.imgInfo && (_prompt.indexOf('u1') > -1 || _prompt.indexOf('u2') > -1 || _prompt.indexOf('u3') > -1 || _prompt.indexOf('u4') > -1
|| _prompt.indexOf('v1') > -1 || _prompt.indexOf('v2') > -1 || _prompt.indexOf('v3') > -1 || _prompt.indexOf('v4') > -1)) {
_this.$message.toast({
message: '请先输入绘图指令!'
});
return;
}
let optStr = '';
_this.queryInfo.param.question = _prompt;
// 放大图片
if (_prompt.indexOf('u1') > -1 || _prompt.indexOf('u2') > -1 || _prompt.indexOf('u3') > -1 || _prompt.indexOf('u4') > -1) {
optStr = '放大';
_this.operateType = 2;
_this.queryInfo.type = 'upscale';
if (_prompt.indexOf('u1') > -1) {
_this.queryInfo.param.index = 1;
}
if (_prompt.indexOf('u2') > -1) {
_this.queryInfo.param.index = 2;
}
if (_prompt.indexOf('u3') > -1) {
_this.queryInfo.param.index = 3;
}
if (_prompt.indexOf('u4') > -1) {
_this.queryInfo.param.index = 4;
}
_this.queryInfo.param.id = _this.imgInfo.id;
_this.queryInfo.param.url = _this.imgInfo.attachments[0].url;
}
//变化图片
else if (_prompt.indexOf('v1') > -1 || _prompt.indexOf('v2') > -1 || _prompt.indexOf('v3') > -1 || _prompt.indexOf('v4') > -1) {
optStr = '变化';
_this.operateType = 3;
_this.queryInfo.type = 'variation';
if (_prompt.indexOf('v1') > -1) {
_this.queryInfo.param.index = 1;
}
if (_prompt.indexOf('v2') > -1) {
_this.queryInfo.param.index = 2;
}
if (_prompt.indexOf('v3') > -1) {
_this.queryInfo.param.index = 3;
}
if (_prompt.indexOf('v4') > -1) {
_this.queryInfo.param.index = 4;
}
_this.queryInfo.param.id = _this.imgInfo.id;
_this.queryInfo.param.url = _this.imgInfo.attachments[0].url;
}
//生成图片
else {
optStr = '生成';
_this.operateType = 1;
_this.queryInfo.type = 'imagine';
_this.queryInfo.param.msg_Id = 'amj_' + Math.ceil(Math.random() * 1000000000000);
}
_this.queryInfo.param.prompt = '';
_this.insertMsg(2, _prompt);
_this.isDisabled = true;
_this.isRetry = true;
console.log('请求参数', JSON.stringify(_this.queryInfo));
_this.$requests
.post("/api/mj-func", _this.queryInfo)
.then((res) => {
console.log('请求结果', res);
if (res != null) {
_this.$message.toast({
message: res.msg,
type: "success",
});
// 请求成功后,开始倒计时60秒
let seconds = 60;
_this.insertMsg(1, '请求成功,' + seconds + ' 秒后将为您获取' + optStr + '后的图片。');
let timer = setInterval(function () {
seconds--;
if (seconds == 0) {
console.log('执行查询');
clearInterval(timer);
_this.msgList[_this.msgList.length - 1].content = '图片获取中,请稍后......';
localStorage.setItem('msgList', JSON.stringify(_this.msgList));
_this.retrieveMessages();
return;
}
_this.msgList[_this.msgList.length - 1].content = '请求成功,' + seconds + ' 秒后将为您获取' + optStr + '后的图片。';
localStorage.setItem('msgList', JSON.stringify(_this.msgList));
}, 1000);
}
});
},
// 查询消息
retrieveMessages() {
_this.queryInfo.type = 'retrieveMessages';
console.log('查询参数', JSON.stringify(_this.queryInfo));
_this.$requests
.post("/api/mj-func", _this.queryInfo)
.then((res) => {
console.log('查询结果', res);
_this.isDisabled = _this.isRetry ? true : false;
if (res != null) {
_this.isDisabled = false;
_this.$message.toast({
message: res.msg,
type: "success",
});
if (res.data) {
_this.insertMsg(1, res.data.pic);
// 生成图片后,存储变量
if (_this.operateType == 1) {
_this.imgInfo = res.data;
localStorage.setItem('imgInfo', JSON.stringify(_this.imgInfo));
}
}
}
else {
if (_this.isRetry) {
_this.isRetry = false;
// 重试,开始倒计时180秒
let seconds = 180;
let timer = setInterval(function () {
seconds--;
if (seconds == 0) {
console.log('重试->执行查询');
clearInterval(timer);
_this.msgList[_this.msgList.length - 1].content = '图片获取中,请稍后......';
localStorage.setItem('msgList', JSON.stringify(_this.msgList));
_this.retrieveMessages();
return;
}
_this.msgList[_this.msgList.length - 1].content = '图片获取失败,' + seconds + ' 秒后将为您重新获取。';
localStorage.setItem('msgList', JSON.stringify(_this.msgList));
}, 1000);
}
else {
_this.insertMsg(1, '十分抱歉,图片获取失败,请重新输入指令.....');
}
}
});
},
// 插入会话
insertMsg(type, content) {
_this.msgIndex = _this.msgIndex + 1;
_this.msgList.push({
id: _this.msgIndex,
type: type || 0,//类型 1-机器人,2-自己
content: content || '',
time: _this.$date.fmtDateTime(null, 3)
});
// 将会话写入缓存
localStorage.setItem('msgList', JSON.stringify(_this.msgList));
}
}
}
</script>
<style lang="scss">
.page {
.el-header {
line-height: 20px;
text-align: left;
background-color: var(--el-color-primary-light-7);
color: var(--el-text-color-primary);
height: auto;
padding: 8px 15px;
p {
font-size: 13px;
color: #303133;
}
.tips {
color: red;
}
}
.el-main {
padding: 12px 15px;
.msg-list {
.msg-item:last-child {
margin-bottom: 0;
}
.msg-item {
margin-bottom: 10px;
text-align: left;
.el-col {
display: flex;
}
.img {
width: 100%;
}
.img_1 {
width: 36px;
height: 36px;
margin-right: 8px;
}
.img_2 {
width: 36px;
height: 36px;
margin-left: 8px;
}
.el-card__body {
padding: 8px 15px;
font-size: 14px;
color: #303133;
min-width: 130px;
}
.time {
margin-top: 5px;
text-align: right;
color: #909399;
font-size: 12px;
}
}
}
}
.el-footer {
line-height: 60px;
background-color: #ecf5ff;
color: var(--el-text-color-primary);
.el-input-group__append {
background-color: #409EFF;
color: #ffffff;
box-shadow: none;
}
}
}
</style>
说明:post请求时,第一个url参数需要使用自己在Laf中创建的云函数的名称
在src目录下创建utils文件夹,用于存放以上关联的js,具体代码如下:
日期格式化(date.js)
//时间处理
const date = {
/**
*
* @param {时间} dateTime
* @param {类型} type 默认:年月日
* type=1 年-月-日
* type=2 年.月.日
* type=3 年-月-日 时:分:秒
* type=4 年.月.日 时:分:秒
* @returns
*/
fmtDateTime(dateTime, type) {
if (dateTime == '' || dateTime == null) {
dateTime = new Date();
} else {
// dateTime = dateTime.substr(0, 4) + "/" + dateTime.substr(5, 2) + "/" + dateTime.substr(8, 2) + " " + dateTime.substr(11, 8);
dateTime = new Date(dateTime);
}
var y = dateTime.getFullYear();
var m = dateTime.getMonth() + 1;
var d = dateTime.getDate();
var h = dateTime.getHours() > 9 ? dateTime.getHours().toString() : '0' + dateTime.getHours();
var mm = dateTime.getMinutes() > 9 ? dateTime.getMinutes().toString() : '0' + dateTime.getMinutes();
var ss = dateTime.getSeconds() > 9 ? dateTime.getSeconds().toString() : '0' + dateTime.getSeconds();
if (type === 1) {
return y + '-' + (m < 10 ? ('0' + m) : m) + '-' + (d < 10 ? ('0' + d) : d);
}
else if (type === 2) {
return y + '.' + (m < 10 ? ('0' + m) : m) + '.' + (d < 10 ? ('0' + d) : d);
}
else if (type === 3) {
return y + '-' + (m < 10 ? ('0' + m) : m) + '-' + (d < 10 ? ('0' + d) : d) + " " + h + ":" + mm + ":" + ss;
}
else if (type === 4) {
return y + '.' + (m < 10 ? ('0' + m) : m) + '.' + (d < 10 ? ('0' + d) : d) + " " + h + ":" + mm + ":" + ss;
}
return y + '年' + (m < 10 ? ('0' + m) : m) + '月' + (d < 10 ? ('0' + d) : d) + '日';
}
}
export default date
消息弹框提示(message.js)
import { ElMessage } from "element-plus";
const message = {
//信息提示
toast(obj) {
return ElMessage({
message: obj.message || 'this is a message.',
type: obj.type || 'warning',//success warning info error
duration: obj.duration || 3000,
showClose: obj.showClose || false
});
}
}
export default message
加载动画(loading.js)
import { ElLoading } from 'element-plus';
// 定义一个请求次数的变量,用来记录当前页面总共请求的次数
let loadingRequestCount = 0;
// 初始化loading
let loadingInstance;
// 编写一个显示loading的函数 并且记录请求次数 ++
const showLoading = (target) => {
if (loadingRequestCount === 0) {
// element的服务方式 target 我这边取的是表格class
// 类似整个表格loading和在表格配置v-loading一样的效果,这么做是全局实现了,不用每个页面单独去v-loading
loadingInstance = ElLoading.service({ target });
}
loadingRequestCount++
}
// 编写一个隐藏loading的函数,并且记录请求次数 --
const hideLoading = () => {
if (loadingRequestCount <= 0) return
loadingRequestCount--
if (loadingRequestCount === 0) {
loadingInstance.close();
}
}
export {
showLoading,
hideLoading
}
http请求(requests.js)
import axios from 'axios'
import message from './message';
import { showLoading, hideLoading } from '@/utils/loading'
// 创建 axios 实例
const requests = axios.create({
// baseURL: 'http://localhost:8088',
withCredentials: true,
headers: {
// 'Content-Type': 'application/json',
// 'content-type' : 'application/x-www-form-urlencoded',
// 'auth': 'test'
},
timeout: 30 * 1000 // 请求超时时间 30秒
})
// 错误处理函数
const err = (error) => {
// 响应拦截进来隐藏loading效果,此处采用延时处理是合并loading请求效果,避免多次请求loading关闭又开启
setTimeout(() => {
hideLoading()
}, 200);
if (error.response) {
const data = error.response.data;
if (error.response.status === 403) {
message.toast({
message: data.message || data.msg
});
}
if (error.response.status === 401) {
message.toast({
type: 'warning',
message: '你没有权限。'
});
}
}
return Promise.reject(error)
}
// requests interceptor(请求拦截器)
requests.interceptors.request.use(config => {
// 请求拦截进来调用显示loading效果
showLoading(".el-main");
const token = localStorage.getItem('token')
if (token) {
config.headers['auth'] = token // 让每个请求携带自定义 token
}
return config
}, err)
// requests interceptor(接收拦截器)
requests.interceptors.response.use((response) => {
// 响应拦截进来隐藏loading效果,此处采用延时处理是合并loading请求效果,避免多次请求loading关闭又开启
setTimeout(() => {
hideLoading()
}, 200);
const res = response.data;
if (res.code == 200) {
return Promise.resolve(res).catch((e) => { });
} else {
message.toast({
message: res.msg
});
return Promise.reject(res).catch((e) => { });
}
}, err)
export default {
requests
}
同时,需要修改“main.js”,进行引入,配置全局变量
import message from './utils/message'
import requests from './utils/requests'
import date from './utils/date'
//配置全局属性
app.config.globalProperties.$requests = requests.requests
app.config.globalProperties.$message = message
app.config.globalProperties.$date = date
“App.vue”页面中样式如下:
<template>
<router-view />
</template>
<style lang="scss">
body {
padding: 0;
margin: 0;
}
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
}
p {
margin: 0;
padding: 0;
}
.el-message{
width: max-content;
}
</style>
以为全部完成后,使用命令npm run serve运行项目,界面效果如下:
在线体验可点击 Midjourney绘画
三、发布项目并部署到Laf云平台
1.打包项目
本地开发完成后,没有服务器部署到线上怎么办?不用担心,Laf都已经为我们考虑好了,使用命令npm run build打包项目
2.项目托管
进入【Laf云开发】-【存储】,新建“Bucket”空间,如下图:
完成后,点击上方“上传”按钮,将本地打包后生成的dist文件夹下的文件全部上传,如下图:
再点击右上方“开启网站托管”按钮,生成“当前域名”,即可进行外部访问了。
关于网站托管详细介绍,可访问官网 静态网站托管介绍 | laf 云开发https://doc.laf.run/guide/website-hosting/
以上即为使用Laf接入Midjourney后整合前端的完整演示,如有疑问,欢迎提出!