随着科技的发展,人工智能将会是以后的主流方向,ai绘图、chatgpt高科技先后出台,都获得了火爆的发展,因此想借着势头开发一款主打ai绘图的app+小程序
一、前期准备
1、选定技术框架
因为后面需要多平台发布,而尽可能减少开发成本,因此选定了uniapp框架作为开发框架,前期目标是 android、ios以及 微信小程序三个平台,android平台名称:易绘,IOS端名称:易绘ai作画,微信小程序端:栾青易绘ai绘画。
2、ai绘图的api选择
有两个方向,一个是百度云,一个是腾讯云,各有优劣,价格方面腾讯云更优惠,但百度推出的时间更早。因为前期接入的是百度云的sdk,所以还是选择了百度云sdk。
3、功能整合
主打ai绘图(用文字生成图片),次功能头像制作、老照片修复处理,真人动漫化头像,二次元图片,壁纸美图。开放了一部分功能免费使用,以留住用户。
二、ai绘图
<template>
<view>
<uni-nav-bar :statusBar="true" :border="true" :fixed="true" :leftWidth="500">
<template v-slot:left class="uni_flex_row_align_left">
<view v-if="isLogined" @click="gotoTaskList">
<image src="https://blog.csdn.net/xsy341341/article/details/@/static/icon/icon_task.png" style="width: 40rpx;height: 40rpx;margin-left: 10rpx;"></image>
<text class="uni_15px ml6 uni_color_666">任务列表</text>
<text class="ml4 mr2">(</text>
<text style="color: #f43533;">{{taskNum}}</text>
<text class="ml2">)</text>
</view>
<view v-else class="uni_14px uni_font_regular ml14 uni_color_666">
用文字绘出梦想,用梦想成就未来
</view>
</template>
</uni-nav-bar>
<!-- 主绘制面板视图 -->
<view class="page_root">
<view style="width: 690rpx;height: 15rpx;"></view>
<!-- 输入框内容 START -->
<view class="input_box">
<!-- 文本输入框 -->
<textarea :maxlength="maxNum" placeholder="输入你的奇思妙想,AI智能为你作画" v-model="inputContent"></textarea>
<!-- 信息操作栏 START -->
<view class="input_action_bar">
<view @click="startVoice">
<uni-icons type="mic-filled" color="#9686FB" size="19"></uni-icons>
<text style="color: #9686FB;">语音输入</text>
</view>
<view class="uni_flex_row_align_right">
<view v-if="inputContent">
<text class="num g1">{{inputContent.length || 0}}</text>
<text class="num g2 ml4 mr4">/</text>
<text class="num g2">{{maxNum}}</text>
</view>
<view v-else @click="gotoSchool">
<uni-icons type="help" size="21" color="#9686FB"></uni-icons>
<text style="color: #9686FB;">教程帮助</text>
</view>
<view v-if="inputContent"></view>
<!-- 清除 -->
<uni-icons type="clear" v-if="inputContent" color="#9686FB" size="19" @click="onClear" click="ml10"></uni-icons>
<!-- 复制 -->
<uni-icons type="paperclip" v-if="inputContent" color="#9686FB" size="19" @click="onCopy"></uni-icons>
</view>
</view>
<!-- 信息操作栏 START -->
</view>
<!-- 输入框内容 END -->
<!-- 示例 START -->
<view v-if="exampleObj.id">
<!-- 示例内容 -->
<text @click="onClickExample">示例:{{exampleObj.content}}</text>
<!-- 刷新示例数据 -->
<uni-icons :animation="animationData" type="loop" color="#9686FB" size="18" @click="queryExample"></uni-icons>
</view>
<!-- 示例 END -->
<!-- 关键字模块 START -->
<view class="mt18 uni_flex_row_between_center">
<view class="uni_flex_row_align_left">
<image src="https://blog.csdn.net/xsy341341/static/icon/icon_keyword.png" style="width: 36rpx; height: 36rpx;"></image>
<text class="ml4 uni_16px uni_color_666">可以试试</text>
</view>
<view class="uni_flex_row_align_right">
<uni-icons type="loop" color="#9686FB" size="17" @click="queryStyles"></uni-icons>
<text @click="queryStyles">换一批</text>
</view>
</view>
<view class="uni_flex_warp uni_flex_row_between mt8">
<view v-for="(keyword,id) in keywords" v-bind:key="id" @click="onClickKeyword(keyword)">{{keyword.name}}</view>
</view>
<view class="uni_flex_row_align_left mt20">
<text class="uni_16px uni_color_666">高级设置</text>
<switch color="#9686FB" :checked="isShowHeightSetting" @change="setSettingOfShowHeight"></switch>
</view>
<!-- 画作风格 START -->
<view v-if="isShowHeightSetting">
<view class="uni_flex_row_align_left">
<image src="https://blog.csdn.net/xsy341341/static/icon/icon_style.png" style="width: 34rpx;height: 34rpx;"></image>
<text class="ml4 uni_15px uni_color_666">绘图风格</text>
<text class="ml4 uni_11px uni_color_bbb">(可选,默认不限风格)</text>
</view>
<view @click="showPickerDataObj(true, 'style')">
<text class="uni_14px uni_font_regular uni_color_app1">{{imageStyle.tag || '请选择风格'}}</text>
<uni-icons type="right" color="#9686FB" class="ml6"></uni-icons>
</view>
</view>
<!-- 画作风格 END -->
<!-- 生成张数 START -->
<view v-if="isShowHeightSetting">
<view class="uni_flex_row_align_left">
<image src="https://blog.csdn.net/xsy341341/static/icon/icon_number.png" style="width: 34rpx;height: 34rpx;"></image>
<text class="ml4 uni_15px uni_color_666">图片生成张数</text>
<text class="ml4 uni_11px uni_color_bbb">(可选,默认1张)</text>
</view>
<view @click="showPickerDataObj(true, 'number')">
<text class="uni_14px uni_font_regular uni_color_app1">{{createAiNumber + ' 张' || '请选择张数'}}</text>
<uni-icons type="right" color="#9686FB" class="ml6"></uni-icons>
</view>
</view>
<!-- 生成张数 END -->
<!-- 画作分辨率 START -->
<view v-if="isShowHeightSetting">
<view class="uni_flex_row_align_left">
<image src="https://blog.csdn.net/xsy341341/static/icon/icon_ratio.png" style="width: 34rpx;height: 34rpx;"></image>
<text class="ml4 uni_15px uni_color_666">分辨率</text>
<text class="ml4 uni_11px uni_color_bbb">(可选,默认1024*1536)</text>
</view>
<view @click="showPickerDataObj(true, 'ratio')">
<text class="uni_14px uni_font_regular uni_color_app1">{{imageRatio.tag || '请选择分辨率'}}</text>
<uni-icons type="right" color="#9686FB" class="ml6"></uni-icons>
</view>
</view>
<!-- 画作分辨率 END -->
<!-- 开始绘画按钮 -->
<view class="uni_flex_row_align_center mt30">
<view @click="onCheckStatusAndShowDialog">
<text style="letter-spacing: 2rpx;">{{isLogined ? 'Ai 绘画创作':'登录后使用Ai绘画功能'}}</text>
</view>
<uni-badge :text="freeNum" absolute="rightTop" size="normal" :offset="[5,5]">
<view v-if="freeNum>0" @click="showTestDialog(true)">
免费体验
</view>
<view v-else class="start_test_btn_unable">
暂不可用
</view>
</uni-badge>
</view>
<!-- 客服信息 -->
<view class="customer uni_12px mt10 uni_color_999">
<text @click="onCopy('729913920')">QQ交流学习群:729913920 (点击复制)</text>
</view>
</view>
<!-- 数据选择弹框 -->
<uni-popup type="bottom" ref="pickerDataObj">
<view class="popup_dialog_style">
<view class="popup_dialog_title">
<text @click="showPickerDataObj(false)">取消</text>
<text v-if="dialogType === 'number'">请选择画作生成张数</text>
<text v-else-if="dialogType === 'style'">请选择画作风格</text>
<text v-else-if="dialogType === 'ratio'">请选择画作分辨率</text>
<text @click="onPickerData('',true)">确定</text>
</view>
<view v-if="dialogType === 'number'">
<text
v-if="dialogType === 'number'"
v-for="(dateItem, index) in dialogData" v-bind:key="index"
:class="tempImageNumber === dateItem ? 'style_checked':''"
@click="onPickerData(dateItem)">
{{dateItem}}张
</text>
</view>
<view v-if="dialogType === 'style'">
<text
v-for="(ratio,index) in styles" v-bind:key="index"
:class="tempImageStyle.id === ratio.id ? 'style_checked':''"
@click="onPickerData(ratio)">
{{ratio.tag}}
</text>
</view>
<view v-if="dialogType === 'ratio'">
<text
v-for="(ratio,index) in ratios" v-bind:key="index"
:class="tempImageRatio.id === ratio.id ? 'style_checked':''"
@click="onPickerData(ratio)">
{{ratio.tag}}
</text>
</view>
</view>
</uni-popup>
<!-- 开始AI绘图任务后的状态展示弹窗 -->
<uni-popup ref="taskStatusDialog" type="center">
<view class="task_status_dialog">
<image src="https://blog.csdn.net/xsy341341/static/img_ai_status_dialog.gif" style="width: 400rpx;height: 160rpx;"></image>
<view class="uni_14px uni_font_regular">{{aiRequestStatusText}}</view>
<view class="uni_flex_row_align_center uni_flex_warp mt10 ml10 mr10">
<view v-for="(wait,wid) in waitTime" v-bind:key="wid" class="white_point"></view>
</view>
<view class="mt4 uni_16px uni_font_medium">{{waitTime.length}}s</view>
</view>
</uni-popup>
<uni-popup ref="dialogTest" type="bottom">
<view class="test_dialog uni_flex_col_align_center">
<view class="uni_font_medium uni_17px uni_color_333 mt10 mb15">免费体验说明</view>
<text style="letter-spacing: 2rpx;">
全系统今日总共还
<text class="uni_font_regular uni_color_666">剩余{{freeNum}}次免费体验机会,</text>
系统每天晚上20点下放随机数量的免费体验次数,当天用完即止,每个账号可先到先得免费使用一次ai绘图功能(注:为避免占用,每个账号每天有且只有一次免费体验机会)
</text>
<text @click="onConfirmTest">立即免费创作</text>
<text @click="showTestDialog(false)">取消</text>
</view>
</uni-popup>
<!-- 提示信息弹窗 -->
<uni-popup ref="message" type="message">
<uni-popup-message type="error" :message="messageText" :duration="3200"></uni-popup-message>
</uni-popup>
<uni-popup ref="confirmDialog" type="center">
<view class="uni_flex_col_align_center">
<view class="confirm_dialog_bg mb10">
<view class="uni_18px uni_color_000 uni_font_medium mt10 mb15">温馨提示</view>
<view style="letter-spacing: 2rpx;">
<text>您正在使用易绘的Ai绘画功能,此功能按次收费,本次操作将从您的易绘账号上</text>
<text class="uni_16px uni_font_regular ml2 uni_color_red">扣除 {{price * createAiNumber}} 钻石</text>
<text class="ml2">({{createAiNumber}}张图) 是否已知悉并确认?</text>
</view>
<!-- <view v-if="createAiNumber <= 1">您正在使用Ai绘画功能,AI绘图功能单次需扣除 {{price}} 钻石,是否确定?</view> -->
<!-- <view v-else>您正在使用Ai绘画功能,当前您设置了同时生成{{createAiNumber}}张图片,本次需扣除 {{price * createAiNumber}} 钻石,是否确定并开始绘画?</view> -->
<text @click="onClickStartButton('pay')">确认并开始绘画</text>
<text v-if="couponNum>0" @click="onClickStartButton('coupon')">
使用免费券抵扣<text class="uni_12px uni_color_fff uni_font_light">({{couponNum}}张)</text>
</text>
<!-- <text @click="showTestDialog(false)">取消绘画</text> -->
</view>
<uni-icons type="close" size="45" color="#ffffff" @click="showAiConfirmDialog(false)"></uni-icons>
</view>
</uni-popup>
</view>
</template>
<script>
import api from '../../common/network/api/api.js';
import dataBase from '../../common/database.js';
import UI from '../../common/UI.js';
import handle from '../../common/handle.js';
export default {
data() {
return {
maxNum: 100,
inputContent: '', // 输入的描述内容
errorTip: '', // 错误提示
exampleObj: {}, // 示例
tempImageRatio: {},
tempImageStyle: {},
tempImageNumber: 1,
imageStyle: {},
imageRatio: {},
keywords: [], // 关键字列表
// simpleKeyWords:[], // 简单列表的关键字
styles: [], // 风格列表
ratios: [] ,// 分辨率列表
countAiTask: undefined, // ai绘图状态弹窗的计时器
waitTime: [], // ai绘图状态弹窗的 等待时间计时
waiting: '', // 预估等待时间
ableReload: true, // 是否可以重新加载
aiRequestStatusText: '', // AI请求
messageTimeoutTask: undefined, // 临时消息的计时器对象
aiStatus: 0, // AI绘图状态 0|队列中 1|已完成
aiId: '', // 唯一ID
taskNum: 0,
createAiNumber: 1, // 申请AI绘画的图片数量
isLogined: false,
price: 0,
dialogType: '',
dialogData: [],
animationData:{},
freeNum: 0, // 系统免费ai体验次数
isShowHeightSetting: false,
}
},
onShow() {
this.init();
this.getSettingOfShowHeight();
console.error("环境:",process.env.NODE_ENV)
},
onLoad() {
uni.$on('networkResume',()=>{
this.init();
});
},
onUnload() {
uni.$off('networkResume');
this.showTaskStatusDialog(false);
},
methods: {
getSettingOfShowHeight(){
let temp = dataBase.queryStorage('heightSetting') || false;
this.isShowHeightSetting = temp;
return temp;
},
setSettingOfShowHeight(e){
this.isShowHeightSetting = e.detail.value;
dataBase.insertStorage('heightSetting',e.detail.value);
},
showAiConfirmDialog(show){
if(show)
this.$refs.confirmDialog.open();
else
this.$refs.confirmDialog.close();
},
// 确定体验
onConfirmTest(){
this.showTestDialog(false);
},
showTestDialog(show){
if(show){
this.onCheckStatusAndShowDialog('free');
// this.$refs.dialogTest.open();
}else{
this.$refs.dialogTest.close();
}
},
init(){
// 允许重新加载
this.ableReload = true;
this.isLogined = dataBase.queryLoginStatus();
this.queryStyles(); // 查询风格/分辨率等
this.queryExample(); // 查询示例
if(this.isLogined){
this.queryTaskNum(); // 查询任务数据
api.doPost({action:'queryUnreadMessageNumber'}).then(res=>{
let unread = res.data.unread;
unread > 0 ? uni.showTabBarRedDot({index: 4}) : uni.hideTabBarRedDot({index: 4});
});
}
},
gotoSchool(){
console.error("前往学院");
uni.navigateTo({
url:'/pages_sub/center/pages/school/school'
});
},
// 前往任务列表页面
gotoTaskList(){
uni.navigateTo({
url:'/pages_sub/ai/pages/ai-task-list/ai-task-list'
});
},
// 查询任务数量
queryTaskNum(){
api.doPost({action:'queryAITaskNumOrList'}).then(res=>{
this.taskNum = res.data.num;
this.couponNum = res.data.freeConpon;
});
},
// 点击主类关键字
onClickKeyword(item){
if(this.inputContent && this.inputContent.length > 0){
let lastWord = this.inputContent.substring(this.inputContent.length - 1, this.inputContent.length);
if(lastWord === ','){
this.inputContent = this.inputContent.substring(0, this.inputContent.length - 1);
}
}
this.inputContent += (this.inputContent ? ',':'') + item.name;
// 限制字数
if(this.inputContent.length > this.maxNum){
this.inputContent = this.inputContent.substring(0, this.maxNum);
}
},
// 选择风格
onPickerData(style, confirm){
if(confirm){
this.showPickerDataObj(false);
switch(this.dialogType){
case 'number': this.createAiNumber = this.tempImageNumber; break;
case 'style': this.imageStyle = this.tempImageStyle; break;
case 'ratio': this.imageRatio = this.tempImageRatio; break;
}
}else{
switch(this.dialogType){
case 'number': this.tempImageNumber = style; break;
case 'style': this.tempImageStyle = style; break;
case 'ratio': this.tempImageRatio = style; break;
}
}
},
// 显示和隐藏任务进度弹窗
showTaskStatusDialog(show){
const $that = this;
if(this.countAiTask){
clearInterval(this.countAiTask);
}
if(show){
this.waitTime = [];
$that.countAiTask = setInterval(()=>{
$that.waitTime.push("");
// 是否可以重新加载
if($that.ableReload){
$that.queryAiResult();
}
}, 2000);
$that.$refs.taskStatusDialog.open();
}else{
$that.inputContent = '';
$that.$refs.taskStatusDialog.close();
}
},
// 检查条件以及展示价格确认
onCheckStatusAndShowDialog(mode){
const $that = this;
// 未登录
if(!this.isLogined){
uni.showModal({
title: '提示',
content: '需要登录才能使用此功能,是否前往登录?',
confirmText: '前往登录',
cancelText: '暂不登录',
complete(res) {
if(res.confirm){
uni.navigateTo({
url:'/pages_sub/login/pages/login/login'
});
return "NO";
}
}
});
return "NO";
}
if(!this.inputContent){
this.messageText = '请在下方输入画作的文字描述'
this.$refs.message.open();
return "NO";
}
if(this.price > 0 && mode !== 'free'){
// 开始ai的确认弹窗
this.showAiConfirmDialog(true);
return "NO";
}
$that.onClickStartButton(mode);
return "OK"
},
// 开始AI绘画
onClickStartButton(mode){
// 隐藏确认对话框
this.showAiConfirmDialog(false);
uni.showLoading({
title:'正在提交云端',
});
let data = {
action:'queryAiTask',
text: this.inputContent,
ratio: this.imageRatio.tag,
style: this.imageStyle.tag,
num:this.createAiNumber,
mode: mode // 免费 付费
};
api.doPost(data).then(res=>{
uni.hideLoading();
if(res.code === 208){
handle.gotoRecharge(res.message);
return;
}
this.showTaskStatusDialog(true);
this.aiStatus = res.data.status;
this.aiId = res.data.aid;
this.aiRequestStatusText = '已成功连接云端';
// 队列中
if(this.aiStatus === 0){
this.waiting = res.data.waiting;
this.aiRequestStatusText = "正在刻画绘图...,预估时长【"+res.data.waiting+"】";
this.queryTaskNum();
}
// 已完成
else{
this.aiRequestStatusText = '已完成AI绘图作画任务';
this.showTaskStatusDialog(false);
console.error("AI绘图完成",res.data);
uni.navigateTo({
url:'/pages_sub/ai/pages/preview-ai-img/preview-ai-img?data='+JSON.stringify(res.data)
});
}
},err=>{
uni.hideLoading();
});
},
// 单纯查询ai绘图状态
queryAiResult(){
this.ableReload = false;
console.error("AI任务查询开始");
api.doPost({action:'queryAiResult',aid:this.aiId}).then(res=>{
console.error("AI任务查询结果:",res);
let code = res.code;
if(code === 200){
this.showTaskStatusDialog(false);
uni.navigateTo({
url:'/pages_sub/ai/pages/preview-ai-img/preview-ai-img?data='+JSON.stringify(res.data)
});
}else{
this.ableReload = true;
}
}, err=>{
console.error("AI任务查询失败:",err);
this.ableReload = false;
this.showTaskStatusDialog(false);
}).catch(e=>{
console.error("AI任务发生异常:",e);
});
},
// 清除内容
onClear(){
this.inputContent = '';
},
// 复制内容
onCopy(text){
uni.setClipboardData({
data: this.inputContent,
success() {
uni.showToast({
title:'复制成功~',
icon:'success'
});
}
});
},
// 显示/关闭数据选择弹窗
showPickerDataObj(show, type){
if(show){
this.dialogType = type;
switch(this.dialogType){
case 'number': this.dialogData = [1,2,3,4,5,6]; break;
case 'style': this.dialogData = this.styles; break;
case 'ratio': this.dialogData = this.ratios; break;
}
this.$refs.pickerDataObj.open();
}else{
this.$refs.pickerDataObj.close();
}
},
// 点击示例文字时,会把示例内容带入到输入框里
onClickExample(){
const $that = this;
// 判断示例是否存在内容
if(this.exampleObj.content){
// 如果输入框内已有内容,就弹出对话框提示用户是否确定要用示例替换
if(this.inputContent){
uni.showModal({
title:'温馨提示',
content:"是否确定要用示例的内容「替换」输入框描述内容?",
confirmText: '确认替换',
cancelText: '取消',
complete(res) {
if(res.confirm){
$that.inputContent = $that.exampleObj.content;
}
}
})
}else{
this.inputContent = this.exampleObj.content;
}
}
},
// 加载/刷新示例
queryExample(){
let data = {
action:'queryExample',
}
// 判断是否有id
if(this.exampleObj && this.exampleObj.id){
data.id = this.exampleObj.id;
}
api.doPost(data).then(res=>{
this.exampleObj = res.data;
});
},
// 查询风格列表
queryStyles(){
api.doPost({action:'queryStyles'}).then(res=>{
this.styles = res.data.styles || [];
this.ratios = res.data.ratios || [];
this.keywords = res.data.keywords || [];
this.price = res.data.price || 0;
this.freeNum = res.data.freeNum;
if(this.styles && this.styles.length > 0){
this.imageStyle = this.styles[0];
this.tempImageStyle = this.styles[0];
}
if(this.ratios && this.ratios.length > 0){
this.imageRatio = this.ratios[0];
this.tempImageRatio = this.ratios[0];
}
});
},
startVoice() {
if(!this.isLogined){
UI.gotoLoginWithComfirmDialog();
return;
}
// #ifdef APP-PLUS
// 开始语音识别
const $that = this;
plus.speech.startRecognize({
engine: 'baidu'
}, (e => {
if (e) {
$that.inputContent += e;
// 限制字数
if($that.inputContent.length > $that.maxNum){
$that.inputContent = $that.inputContent.substring(0, $that.maxNum);
}
}
}), (e => {
if (e.code === 1310722) {
uni.showToast({
title: '抱歉,没有听清,请您提高音量再试一遍吧',
icon: 'none'
})
}
}))
// #endif
}
}
}
</script>
<style>
@import url('index.css');
</style>
三、成品展示图
下载链接: Android apk
苹果链接: AppStore链接
三、登录页
<template>
<view class="page pageStyle">
<view class="uni_flex_col_align_center flexX margin-top-100">
<view class="uni_flex_col_align_center margin-bottom-50">
<image src="https://blog.csdn.net/static/app_logo200.png" class="iconimg"></image>
<view class="app_name uni_font_medium uni_25px">{{appName}}</view>
<view class="app_version uni-font-regular">{{version}}</view>
</view>
<!-- #ifdef MP-WEIXIN -->
<button @click="getuserinfo" withCredentials="true" class="btn_func uni_flex_row_align_center uni_font_medium">微信授权登陆</button>
<view @click="finishPage">暂不登录</view>
<!-- #endif -->
<!-- #ifdef APP-PLUS -->
<view v-if="isShowWX" @click="doLoginAction('app-wx')">
<image src="https://blog.csdn.net/pages_sub/login/static/icon_share_wx.png"></image>
<text style="letter-spacing: 4rpx;">微信授权登录</text>
</view>
<view v-if="isCanPhoneNumberLogin" @click="doLoginAction('mobile')">
<uni-icons type="phone-filled" color="#E00300" size="20"></uni-icons>
<view style="margin-left: 15rpx;">手机号一键登录</view>
</view>
<view @click="doLoginAction('apple')" v-if="isApple">
<image src="https://blog.csdn.net/pages_sub/login/static/icon_apple.png"></image>
<view>通过 Apple 登录</view>
</view>
<view @click="finishPage">暂不登录</view>
<!-- #endif -->
<!-- #ifdef H5 -->
<!-- 下载按钮 -->
<view>
<view @click="download(1)">
<image src="https://mp-4600ec1f-16af-433c-a045-130f8b08315f.cdn.bspapp.com/cloudstorage/3122fd08-63b0-42cd-91f1-f11421e72389.png"></image>
<text style="letter-spacing: 2rpx;">下载App (安卓)</text>
</view>
<view @click="download(2)">
<image src="https://mp-4600ec1f-16af-433c-a045-130f8b08315f.cdn.bspapp.com/cloudstorage/a8074492-ae50-42ad-ba0e-5399354ad9c6.png"></image>
<text style="letter-spacing: 2rpx;">下载App (苹果)</text>
</view>
<view class="uni_flex_col_align_center mt15">
<image src="https://luanqing.oss-cn-shanghai.aliyuncs.com/icon/yihui/min_code_yihui.jpeg"></image>
<text style="letter-spacing: 2rpx;">微信小程序码</text>
</view>
</view>
<!-- #endif -->
</view>
<!-- #ifdef APP-PLUS -->
<view class="right uni_flex_col_align_center flexS">
<view class="uni_flex_row_align_center uni_14px uni-font-regular">
<checkbox color="#8256f6" :checked="agreeState" @click="doAgree"></checkbox>
<view class="uni-font-light">已阅读并同意</view>
<view @click="gotoUserAgreen">《用户服务协议》</view>
<view @click="gotoPrivacy">《隐私政策》</view>
</view>
<view class="uni_13px mt6">上海栾青网络科技有限公司出品</view>
<view class="uni_13px mb15">Copyright © 2023</view>
</view>
<!-- #endif -->
</view>
</template>
<script>
// #ifdef APP-PLUS
const univerifyManager = uni.getUniverifyManager()
// #endif
import api from '../../../../common/network/api/api.js';
import dataBase from '../../../../common/database.js';
export default{
data(){
return{
agreeState: false,
version:"v1.0",
appName: '易绘',
fromSource: 'index', // 来源,如果是index,则登录后返回主页,否则返回上一级
code:undefined,
isShowWX:true,
isApple:false,
isCanPhoneNumberLogin: false,
}
},
onLoad(opt) {
// #ifdef MP-WEIXIN
const $that = this;
this.fromSource = opt.fromSource || 'index';
uni.login({
success(res) {
$that.code = res.code;
},
fail(res) {
},
})
// #endif
},
onShow() {
const $that = this;
$that.version = 'v'+ uni.getSystemInfoSync().appVersion;
// #ifdef APP-PLUS
if(plus.runtime.isApplicationExist({pname:'com.tencent.mm',action:'weixin://'})){
console.log("微信应用已安装");
$that.isShowWX = true;
}else{
console.log("微信应用未安装");
$that.isShowWX = false;
}
uni.preLogin({
provider:'univerify',
success: (suc) => {
this.isCanPhoneNumberLogin = true;
if(univerifyManager){
univerifyManager.preLogin();
}
},
});
this.isApple = uni.getSystemInfoSync().platform == 'ios';
// #endif
},
methods:{
download(type){
if(type === 1){
window.location.href = dataBase.appDownloadUrl;
}else if(type === 2){
window.location.href = 'https://apps.apple.com/cn/app/%E6%98%93%E7%BB%98ai%E4%BD%9C%E7%94%BB/id1670258950';
}
},
finishPage(){
uni.navigateBack({delta:1});
},
// 登录动作中枢
doLoginAction(type){
if(!this.agreeState){
const $that = this;
uni.showModal({
title:'温馨提示',
content:'您是否已阅读并同意 《用户服务协议》、 《隐私政策》?',
confirmText:'同意并登录',
cancelText:'拒绝',
complete: (res) => {
if(res.confirm){
$that.agreeState = true;
$that.doLoginAction(type);
}
}
});
return;
}
switch(type){
case 'apple': this.login4ApplePhone(); break;
case 'app-wx': this.login4AppWx(); break;
case 'mobile': this.login4Mobild(); break;
}
},
gotoUserAgreen(){
uni.navigateTo({
url:'/pages_sub/center/pages/webview/webview?url='+dataBase.appAgreement+'&title=用户协议'
});
},
gotoPrivacy(){
uni.navigateTo({
url:'/pages_sub/center/pages/webview/webview?url='+dataBase.appPrivacy+'&title=隐私协议'
});
},
doAgree(){
this.agreeState = !this.agreeState;
},
// 手机号一键登录
login4Mobild(){
const $that = this;
// 一键登录必须是手机使用流量的前提下才能获取到手机号码,用Wi-Fi联网时无法获取到手机号码,同时如果是双卡手机,获取到的手机号码是默认移动数据的那个手机卡的号码。
// #ifdef APP-PLUS
// 预登录 START
// 1.提高一键登录的加载速度
// 2.判断一键登录环境是否可用
univerifyManager.preLogin();
// 调用一键登录弹框
univerifyManager.login({
univerifyStyle: {
fullScreen:false,
icon: {
path:'/static/app_logo200.png'
},
privacyTerms:{
defaultCheckBoxState: false,
checkBoxSize: 14,
privacyItems:[
{
"url": dataBase.appAgreement,
"title": "用户服务协议"
},
{
"url": dataBase.appPrivacy,
"title": "隐私协议"
}
]
},
authButton:{
normalColor:"#9686FB",
disabledColor:"#AAAAAA"
},
otherLoginButton: {
visible: true, // 是否显示其他登录按钮,默认值:true
normalColor: "", // 其他登录按钮正常状态背景颜色 默认值:透明
highlightColor: "", // 其他登录按钮按下状态背景颜色 默认值:透明
textColor: "#656565", // 其他登录按钮文字颜色 默认值:#656565
title: "其他登录方式", // 其他登录方式按钮文字 默认值:“其他登录方式”
borderColor: "", //边框颜色 默认值:透明(仅iOS支持)
borderRadius: "0px" // 其他登录按钮圆角 默认值:"24px" (按钮高度的一半)
},
},
success (auth) {
uniCloud.callFunction({
name:'getPhoneNumber',
data:{ openid: auth.authResult.openid, access_token: auth.authResult.access_token},
success(cloudRes) {
let {code, phoneNumber} = cloudRes.result;
console.error("手机号:",phoneNumber);
if(code === 200){
const data = {
name: '手机用户',
unionId: phoneNumber
};
$that.submitLoginData(data);
univerifyManager.close();
}else{
univerifyManager.close();
}
},
fail: (err) => {
console.error("云函数失败:",err);
}
})
},
fail(res) {
// 点击其他登录方式
if(res.code === 30002){
univerifyManager.close();
}
}
})
// #endif
},
// 苹果登录
login4ApplePhone(){
const $that = this;
uni.login({
provider: 'apple',
success(loginRes){
// 登录成功
uni.getUserInfo({
provider: 'apple',
success(res) {
// 苹果登录成功:{"errMsg":"getUserInfo:ok","userInfo":{"openId":"001465.6449e1ad2e46401488d67bae89d79c8a.1706","fullName":{"familyName":"许","giveName":"仕永","givenName":"仕永"},"email":"835588741@qq.com","authorizationCode":"c9acf4dd2a2a9491a93d158a7669b4165.0.mruwv.b0hncISXQbHMGinz5_f3vg","identityToken":"eyJraWQiOiJZdXlYb1kiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodHRwczovL2FwcGxlaWQuYXBwbGUuY29tIiwiYXVkIjoiY29tLmx1YW5xaW5nLmJhYnkiLCJleHAiOjE2MjA5MjU2MTIsImlhdCI6MTYyMDgzOTIxMiwic3ViIjoiMDAxNDY1LjY0NDllMWFkMmU0NjQwMTQ4OGQ2N2JhZTg5ZDc5YzhhLjE3MDYiLCJjX2hhc2giOiJqVG9MWHFKd3BBemxxSlNmV2xjWFBBIiwiZW1haWwiOiI4MzU1ODg3NDFAcXEuY29tIiwiZW1haWxfdmVyaWZpZWQiOiJ0cnVlIiwiYXV0aF90aW1lIjoxNjIwODM5MjEyLCJub25jZV9zdXBwb3J0ZWQiOnRydWUsInJlYWxfdXNlcl9zdGF0dXMiOjJ9.nAHGpGDpjALPmz-vReHjIAV3IyrYJTrLxHmLDqTd2AShYH6XZW3Qh-27aAgbWjg8_3w4Ex22Tpv8_NhKNTAIrHjCNMN5jnw6WkXL4-utKTZi4mlX4p_WiPpkZSa7e0cJZYKrN5b3UEiI1Vf6yu8b5TeKz4E7cg1Iq6RfFNmXLYJmt92WRtwSPexinTySjVvZmxGZ-_7nnh-TowHXjedLBsUsdB7oRlp1xSRpRQm78YKnibwmF2__iejPTmKL_WwOTXXBsg4NrF5h7rFS0Z7jGvA8WPziJxoaeHDP_j1Iw2pcmfzMLA7FhSXlkcYd38To2Wv01kOma0fLlfIm8JovIg","realUserStatus":2}} at pages/user/login/login.vue:93 __ERROR
console.error("苹果登录成功:",res?.userInfo?.fullName?.giveName || "苹果用户",res?.userInfo?.openId);
// 获取用户信息成功
const data = {
name: res?.userInfo?.fullName?.giveName || "苹果用户",
unionId: res?.userInfo?.openId
};
$that.submitLoginData(data);
}
})
},
});
},
// App端微信
login4AppWx(){
const $that = this;
uni.login({
provider: 'weixin',
success: function (loginRes) {
uni.getUserInfo({
success(res) {
const data = {
avatar:res.userInfo.avatarUrl,
name:res.userInfo.nickName,
gender:res.userInfo.gender,
unionId: res.userInfo.unionId,
};
$that.submitLoginData(data);
},
})
},
});
},
// 提交登录信息给后端
submitLoginData(data){
data.action = "submitLoginData";
api.doPost(data).then(res=>{
let temp = res.data;
dataBase.insertUUIDAtStorage(temp.uuid);
dataBase.insertTokenAtStorage(temp.token);
this.finishPage();
});
},
// 小程序专用 2
getuserinfo(){
let $that = this;
let code = $that.code;
uni.showToast({
title:'正在请求中',
icon:'none'
});
uni.getUserProfile({
desc:'用于登录获取昵称头像',
success(res) {
console.error("获取的用户资料:",JSON.stringify(res));
const data = {
action:'submitLoginDataMP',
code:code,
avatar:res.userInfo.avatarUrl,
name:res.userInfo.nickName,
gender:res.userInfo.gender,
};
api.doPost(data).then(response=>{
dataBase.insertTokenAtStorage(response.data.token);
dataBase.insertUUIDAtStorage(response.data.uuid);
$that.finishPage();
});
}
})
},
}
}
</script>
<style>
@import url("login.css");
</style>
四、头像制作
<template>
<view>
<uni-nav-bar :statusBar="true" :border="false" :fixed="true" :leftWidth="400" :rightWidth="200">
<template v-slot:left class="uni_flex_row_align_left uni_14px uni_font_medium">
<text @click="importImageBg">导入图片</text>
<text @click="onSave2PhoneStorage">
保存本地
<!-- <text style="color: #E00300;">免费</text> -->
</text>
</template>
<!-- #ifndef MP -->
<template v-slot:right>
<view @click="onClickHelp(true)">
<uni-icons type="help-filled" size="22" color="#A2A3A4"></uni-icons>
<view class="uni_12px uni_color_999">帮助</view>
</view>
</template>
<!-- #endif -->
</uni-nav-bar>
<view v-if="isShowCanvas" style="background-color: #ffffff;position: sticky;top: 140rpx;z-index: 999;">
<canvas canvas- @touchstart="onTouchStart" @touchmove="onTouchMove"></canvas>
</view>
<!-- 缩放、旋转操作栏 START -->
<view v-if="moveElement.url_mp || moveElement.url" class="uni_flex_row_align_center mt10">
<view @click="onImgTagRotate(-1)">
<image src="https://blog.csdn.net/xsy341341/static/icon/icon_left.png"></image>
</view>
<view @click="onImgTagScan(-1)">缩小</view>
<!-- #ifdef MP -->
<!-- <image :src="https://blog.csdn.net/xsy341341/article/details/moveElement.url_mp" mode="widthFix"></image> -->
<!-- #endif -->
<view @click="onImgTagRemove">移除</view>
<!-- #ifndef MP -->
<!-- <image :src="https://blog.csdn.net/xsy341341/article/details/moveElement.url" mode="widthFix"></image> -->
<!-- #endif -->
<view @click="onImgTagScan(1)">放大</view>
<view @click="onImgTagRotate(1)">
<image src="https://blog.csdn.net/xsy341341/static/icon/icon_right.png"></image>
</view>
</view>
<!-- 缩放、旋转操作栏 END -->
<view class="uni_flex_row_align_left ml15 mt25">
<image src="https://blog.csdn.net/static/icon/icon_color_piacker.png" style="width: 30rpx;height: 30rpx;"></image>
<text class="uni_14px uni_color_222 ml8 uni_font_medium">背景颜色</text>
</view>
<view class="uni_flex_warp uni_flex_row_between ml15 mr15 mt12">
<view v-for="(color,id) in colorList" v-bind:key="id" :style="'background-color:#'+color" @click="onClickColorItem(color)"></view>
</view>
<view v-if="false">
<view class="uni_flex_row_align_left uni_font_regular uni_17px">
<text class="mr2">#</text>
<input maxlength="6" v-model="customColor" placeholder="请输入6位十六进制码" />
</view>
<view @click="onClickColorItem()">
<text class="uni_green_209B5C uni_14px uni_font_medium">使用自定义</text>
<view :style="'background-color:#'+customColor"></view>
</view>
</view>
<view class="uni_flex_row_align_left ml15 mt25">
<image src="https://blog.csdn.net/static/icon/icon_img_tag.png" style="width: 30rpx;height: 30rpx;"></image>
<text class="uni_14px uni_color_222 ml8 uni_font_medium">装饰点缀图案</text>
</view>
<!-- 点缀的图案 -->
<swiper :indicator-dots="true" indicator-active-color="#8256f6">
<swiper-item v-for="(page,pid) in imgTag" v-bind:key="pid" style="width: 690rpx;">
<view class="uni_flex_warp">
<view v-for="(tag, tid) in page" v-bind:key="tid" @click="onPickerImgTag(tag,tid)">
<image :src="https://blog.csdn.net/xsy341341/article/details/tag.url"></image>
</view>
</view>
</swiper-item>
</swiper>
<!-- <view v-if="false">
<text class="uni_13px">三步自定义头像制作</text>
<text class="mt4 uni_12px">1.选择背景,添加头像的背景主图</text>
<text class="mt2 uni_12px">2.点击添加装饰点缀的图案</text>
<text class="mt2 uni_12px">3.点击保存,即可保存到手机(相册查看)</text>
</view> -->
<uni-popup ref="showHelpDialog" type="top" :isMaskClick="false">
<view class="popup_help_view">
<view class="page_tip_view mt40">
<text class="uni_13px">三步自定义头像制作</text>
<text class="mt4 uni_12px">1.选择背景,添加头像的背景主图</text>
<text class="mt2 uni_12px">2.点击添加装饰点缀的图案</text>
<text class="mt2 uni_12px">3.点击保存,即可保存到手机(相册查看)</text>
</view>
<view style="height: 40rpx;"></view>
<view @click="onClickHelp(false)">好的</view>
</view>
</uni-popup>
</view>
</template>
<script>
import api from '../../common/network/api/api';
import dataBase from '../../common/database.js';
export default {
data() {
return {
isShowCanvas: true,
// tagImages:[],
drawTag:[], // 绘制在画布上的点缀图案列表
imgTag:[], // 可选的点缀图案
canvasBG:'', // 画布的背景主图
moveElement:{}, // 当前选中的元素
canvasBgColor:'#ffffff', // 画布的背景颜色上色
customColor:'FFFFFF', // 自定义颜色吗
canvasSize: 231,
colorList:["9686FB","6BADFF","77C38F","FFE16B","FF9F6B","FF6B6B"],
}
},
onShow() {
this.queryTags();
this.isLogin = dataBase.queryLoginStatus();
if(this.isLogin){
api.doPost({action:'queryUnreadMessageNumber'}).then(res=>{
let unread = res.data.unread;
unread > 0 ? uni.showTabBarRedDot({index: 4}) : uni.hideTabBarRedDot({index: 4});
});
}
},
methods: {
onImgTagRemove(){
let id = -1;
this.drawTag.forEach((item,itemid)=>{
if(item.id === this.moveElement.id){
id = itemid;
}
});
if(id != -1){
this.drawTag.splice(id,1);
}
this.moveElement = {};
this.drawCanvas();
},
// 保存到本地手机存储
onSave2PhoneStorage(item){
uni.canvasToTempFilePath({canvasId:'canvasId',success:(res)=>{
// 保存到手机本地存储
uni.saveImageToPhotosAlbum({filePath: res.tempFilePath,success:(res)=>{
uni.showToast({
title:'已下载到手机(可在相册查看)',
icon:'none'
});
}});
}});
},
// 导入头像主图
importImageBg(){
// #ifndef H5
uni.chooseImage({
count:1,
crop: {width: 1000,height:1000,quality:100},
success: (res) => {
this.canvasBG = res.tempFilePaths[0];
this.drawCanvas();
}
})
// #endif
// #ifdef H5
uni.chooseFile({
count:1,
extension:['.png','.jpg'],
complete: (res) => {
console.error("选择成功:",res);
}
});
// #endif
},
// 显示帮助弹窗
onClickHelp(show){
this.isShowCanvas = !show;
if(show){
this.$refs.showHelpDialog.open();
}else{
this.$refs.showHelpDialog.close();
}
},
// 查询图案标签列表
queryTags(){
let tagData = dataBase.queryStorage("app_tag_lsit");
if(tagData && tagData.date > (new Date().getTime())){
this.imgTag = tagData.list;
}else{
api.doPost({action:'queryImageTag'}).then(res=>{
let temp = res.data || [];
let pageSize = 18;
let pageNum = temp.length / 18 + (temp.length % 18 > 0 ? 1:0);
pageNum = Math.floor(pageNum);
this.imgTag = [];
for(let i = 0 ; i < pageNum; i++){
let sub;
if(i < pageNum - 1){
sub = temp.slice(i * pageSize, pageSize);
}else{
sub = temp.slice(i * pageSize, i * pageSize + temp.length % 18);
}
this.imgTag.push(sub);
}
let insertData = { list: this.imgTag, date: (new Date().getTime() + 18000000) };
dataBase.insertStorage('app_tag_lsit', insertData);
})
}
},
// 缩放
onImgTagScan(mode){
if(this.moveElement && this.moveElement.url){
this.moveElement.width += (5 * mode);
this.moveElement.height += (5 * mode);
}
this.drawCanvas();
},
// 旋转
onImgTagRotate(mode){
if(this.moveElement && this.moveElement.url){
if(this.moveElement.degree >= 360){
this.moveElement.degree = mode === -1 ? 350:10;
}else if(this.moveElement.degree <= 0){
this.moveElement.degree = mode === -1 ? 360:10;
}else{
this.moveElement.degree += (10 * mode);
}
}
this.drawCanvas();
},
// 选择饰品图标
onPickerImgTag(item,id){
item.id = id;
item.x = 0;
item.y = 0;
item.width = 50;
item.height = 50;
item.degree = 360;
this.drawTag.push(item);
this.drawCanvas();
},
// 点击颜色卡
onClickColorItem(color){
if(color){
this.customColor = color;
this.canvasBgColor = "#"+color;
this.drawCanvas();
}else{
if(!this.customColor || this.customColor.length < 6){
this.customColor = "FFFFFF";
uni.showToast({
title:"自定义颜色码不合法,请输入6位的十六进制颜色码",
icon:'none'
});
return;
}else{
this.canvasBgColor = "#"+this.customColor;
this.drawCanvas();
}
}
},
// 绘制的中枢核心方法
drawCanvas(){
const ctx = uni.createCanvasContext('canvasId');
// 填充背景色
ctx.setFillStyle(this.canvasBgColor);
ctx.fillRect(0, 0, this.canvasSize, this.canvasSize);
// 绘制背景图
if(this.canvasBG){
ctx.drawImage(this.canvasBG, this.bgLeft, this.bgTop, this.canvasSize, this.canvasSize);
}
// 饰品点缀图以及旋转逻辑处理
this.drawTag.forEach(item=>{
if(item.url){
// 旋转的处理代码
if(item.degree > 0){
ctx.translate(item.x + item.width / 2, item.y + item.height / 2);
ctx.rotate(item.degree * Math.PI / 180);
ctx.translate((item.x + item.width / 2) * -1, (item.y + item.height / 2) * -1);
}
if(this.moveElement && item.id === this.moveElement.id){
ctx.setStrokeStyle('red');
ctx.strokeRect(item.x - 2 , item.y - 2 , item.width + 2, item.height + 2)
}
// 绘制饰品点缀图
//#ifdef MP
ctx.drawImage(item.url_mp, item.x, item.y, item.width, item.height);
//#endif
//#ifndef MP
ctx.drawImage(item.url, item.x, item.y, item.width, item.height);
//#endif
}
});
ctx.draw();
},
// 触摸开始,用于定位点击的是哪个tag装饰
onTouchStart(e){
let x = e.touches[0].x;
let y = e.touches[0].y;
x = x < 0 ? 0 : x;
y = y < 0 ? 0 : y;
x = x > this.canvasSize ? this.canvasSize : x;
y = y > this.canvasSize ? this.canvasSize : y;
let cur = this.drawTag.find(item=>{
return ((x >= item.x && y >= item.y) && (x <= (item.x + item.width) && y <= (item.y + item.height)));
});
if(cur){
this.moveElement = cur;
}
this.drawCanvas();
},
// 移动进行中,拖动生效
onTouchMove(e){
let x = e.touches[0].x;
let y = e.touches[0].y;
x = x < 0 ? 0 : x;
y = y < 0 ? 0 : y;
x = x - this.moveElement.width;
y = y - this.moveElement.height;
// let offset = 20;
// x = x > this.canvasSize - this.moveElement.width - offset ? this.canvasSize - this.moveElement.width - offset : x;
// y = y > this.canvasSize - this.moveElement.height - offset ? this.canvasSize - this.moveElement.height - offset: y;
this.moveElement.x = x;
this.moveElement.y = y;
this.drawCanvas();
},
}
}
</script>
<style>
@import url('index.css');
</style>