Files
msh-system/msh_single_uniapp/pages/ai-generate/image.vue

569 lines
24 KiB
Vue
Raw Normal View History

<template>
<view class="image-container" @touchstart="onTouchStart" @touchmove="onTouchMove" @touchend="onTouchEnd">
<view class="media-wrapper">
<swiper class="image-swiper" :current="currentIndex" :circular="true" :autoplay="false" @change="onSwiperChange">
<block v-for="(img, idx) in images" :key="idx">
<swiper-item>
<view class="image-slide" @click="onImageTap(idx)">
<view class="zoom-wrapper" :style="{ transform: 'scale(' + ((scaleMap[idx] || 1)) + ')' }">
<image class="display-image"
:src="img.displayUrl"
mode="aspectFill"
lazy-load
@load="onImageLoad(idx)"
@error="onImageError(idx)" />
</view>
</view>
</swiper-item>
</block>
</swiper>
</view>
<view class="top-controls" :style="{ paddingTop: statusBarHeight + 'px' }">
<view class="left-controls">
<view class="back-btn" @click="onBack">
<text class="icon-back"></text>
</view>
</view>
</view>
<view class="bottom-info">
<view class="article-status" v-if="isLoadingArticle || articleError">
<view class="loading-indicator" v-if="isLoadingArticle">
<text class="loading-text">正在加载图片详情...</text>
</view>
<view class="error-message" v-if="articleError">
<text class="error-text">{{ articleError }}</text>
</view>
</view>
<view class="creator-info">
<image class="creator-avatar" :src="creatorAvatar" @click="onCreatorTap" />
<view class="creator-details">
<text class="creator-name">{{ creatorName }}</text>
<view :class="['follow-btn', isFollowing ? 'following' : '']" @click="onFollow">
{{ isFollowing ? '已关注' : '关注' }}
</view>
</view>
</view>
<view class="image-info" v-if="imageDescription">
<view :class="['image-description', isDescriptionExpanded ? 'expanded' : '']" @click="onToggleDescription">
{{ imageDescription }}
</view>
<view class="expand-toggle" @click="onToggleDescription" v-if="imageDescription.length > 50">
<text class="iconfont" :class="isDescriptionExpanded ? 'icon-xiangshang' : 'icon-xiangxia'"></text>
</view>
</view>
<view class="interaction-area">
<view class="like-section">
<view :class="['like-btn', isLiked ? 'liked' : '']" @click="onLike">
<text class="heart-icon">{{ isLiked ? '❤️' : '🤍' }}</text>
</view>
<text class="like-count">{{ likeCount }}</text>
</view>
<view class="action-buttons">
<view class="consult-btn" @click="onConsult">立即咨询</view>
<view class="action-btn" @click="onCreateSimilar">做同款</view>
</view>
</view>
<view class="ai-notice">
<text class="ai-notice-text">内容由AI生成</text>
</view>
<view class="page-indicator">
<text class="indicator-text">{{ currentIndex + 1 }} / {{ images.length }}</text>
</view>
</view>
<view class="floating-actions">
<view class="floating-btn download-btn" @click="onDownload">
<text class="floating-label">下载</text>
</view>
<view class="floating-btn share-btn" @click="onShare">
<text class="floating-label">分享</text>
</view>
<view class="floating-btn compare-btn" @click="onCompare">
<text class="floating-label">对比</text>
</view>
</view>
</view>
</template>
<script>
import api from '@/api/models-api.js';
import Cache from '@/utils/cache';
import { mapGetters } from 'vuex';
export default {
data() {
return {
images: [],
currentIndex: 0,
currentTime: '00:25',
creatorName: '开心海浪',
creatorAvatar: '/static/images/creator-avatar.png',
isFollowing: false,
imageDescription: '',
isDescriptionExpanded: false,
isLiked: false,
likeCount: 6830,
isLoadingArticle: false,
articleError: null,
articleData: null,
currentArticleId: null,
touchStart: null,
touchDistance: 0,
scaleMap: {},
minScale: 1,
maxScale: 3
,statusBarHeight: 0
};
},
computed: mapGetters(['chatUrl']),
onLoad(options) {
const articleId = options.id;
this.setData({ currentArticleId: articleId });
const sys = uni.getSystemInfoSync();
const sbh = sys.statusBarHeight;
this.statusBarHeight = (sbh && sbh > 0) ? sbh : (sys.platform === 'ios' ? 44 : 24);
this.statusBarHeight +=30;
const cached = Cache.getItem('imageDetail_' + articleId);
if (cached) {
this.initFromData(cached);
}
if (articleId) {
this.loadArticleDetail(articleId);
}
},
methods: {
setData(data) {
let that = this;
Object.keys(data).forEach(key => { that[key] = data[key]; });
},
isImageUrl(url) {
if (!url) return false;
const exts = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg'];
const u = (url.split('?')[0].split('#')[0] || '').toLowerCase();
return exts.some(ext => u.endsWith(ext));
},
// 处理头像URL添加前缀
getAvatarUrl(avatarUrl) {
if (!avatarUrl || avatarUrl === '/static/images/avatar-default.png') {
return '/static/images/avatar-default.png';
}
// 如果已经是完整URL直接返回
if (avatarUrl.startsWith('http://') || avatarUrl.startsWith('https://')) {
return avatarUrl;
}
// 如果是相对路径,添加前缀
const prefix = 'https://uthink2025.oss-cn-shanghai.aliyuncs.com/';
// 如果URL已经以/开头,去掉开头的/
const cleanUrl = avatarUrl.startsWith('/') ? avatarUrl.substring(1) : avatarUrl;
return prefix + cleanUrl;
},
initFromData(data) {
console.log("===initFromData===",data);
const imgs = [];
const vUrl = data.videoUrl || '';
const iUrl = data.imageInput || data.image_input || '';
if (this.isImageUrl(vUrl)) {
imgs.push({ displayUrl: vUrl, originalUrl: vUrl });
}
if (iUrl) {
imgs.push({ displayUrl: iUrl, originalUrl: iUrl });
}
if (!imgs.length) {
// 尝试从 imageOutput 获取图片
let imageOutput = data.imageOutput || '';
if (imageOutput) {
if (typeof imageOutput === 'string') {
try {
const parsed = JSON.parse(imageOutput);
if (Array.isArray(parsed)) {
parsed.forEach(u => imgs.push({ displayUrl: u, originalUrl: u }));
} else {
imgs.push({ displayUrl: parsed, originalUrl: parsed });
}
} catch (e) {
imgs.push({ displayUrl: imageOutput, originalUrl: imageOutput });
}
} else if (Array.isArray(imageOutput)) {
imageOutput.forEach(u => imgs.push({ displayUrl: u, originalUrl: u }));
}
}
if (!imgs.length) {
const list = data.images || data.image_urls || [];
if (Array.isArray(list) && list.length) {
list.forEach(u => imgs.push({ displayUrl: u, originalUrl: u }));
} else {
const u = data.cover || '';
if (u) imgs.push({ displayUrl: u, originalUrl: u });
}
}
}
const like = data.likeCount || data.visit || 0;
// 获取作者头像
const authorAvatar = this.getAvatarUrl(data.authorAvatar || data.avatar || '');
// 获取描述信息,优先使用 prompt然后是 title、content
const description = data.prompt || data.title || data.content || data.synopsis || '';
// 获取作者名称
const authorName = data.authorName || data.author || this.creatorName;
this.setData({
images: imgs,
likeCount: like,
imageDescription: description,
creatorName: authorName,
creatorAvatar: authorAvatar
});
},
loadArticleDetail(articleId) {
this.setData({ isLoadingArticle: true, articleError: null });
api.getArticleById(articleId).then(response => {
const data = response.data || response;
this.articleData = data;
Cache.setItem({ name: 'imageDetail_' + articleId, value: data, expires: 3600 * 1000 });
this.initFromData(data);
this.setData({ isLoadingArticle: false });
}).catch(error => {
this.setData({ articleError: error.message || '获取图片详情失败', isLoadingArticle: false });
uni.showToast({ title: '获取图片详情失败', icon: 'none', duration: 2000 });
});
},
onImageLoad(idx) {
this.scaleMap[idx] = 1;
},
onImageError(idx) {
uni.showToast({ title: '图片加载失败', icon: 'none' });
},
onImageTap(idx) {
const urls = this.images.map(i => i.originalUrl);
uni.previewImage({ urls, current: idx });
},
onSwiperChange(e) {
const i = e.detail.current || 0;
this.setData({ currentIndex: i });
},
onTouchStart(e) {
if (!e.touches || e.touches.length < 1) return;
if (e.touches.length === 2) {
const d = this.distance(e.touches[0], e.touches[1]);
this.touchDistance = d;
} else {
this.touchStart = e.touches[0];
}
},
onTouchMove(e) {
if (e.touches && e.touches.length === 2) {
const d = this.distance(e.touches[0], e.touches[1]);
const delta = d - this.touchDistance;
const cur = this.scaleMap[this.currentIndex] || 1;
let next = cur + delta / 300;
if (next < this.minScale) next = this.minScale;
if (next > this.maxScale) next = this.maxScale;
this.scaleMap[this.currentIndex] = next;
this.touchDistance = d;
}
},
onTouchEnd() {
this.touchStart = null;
this.touchDistance = 0;
},
distance(p1, p2) {
const dx = p1.clientX - p2.clientX;
const dy = p1.clientY - p2.clientY;
return Math.sqrt(dx * dx + dy * dy);
},
onDownload() {
if (!this.images.length) {
uni.showToast({ title: '无可下载图片', icon: 'none' });
return;
}
const url = this.images[this.currentIndex].originalUrl;
uni.showLoading({ title: '下载中...', mask: true });
uni.downloadFile({
url,
success: (res) => {
if (res.statusCode === 200) {
this.compressAndSave(res.tempFilePath);
} else {
uni.hideLoading();
uni.showToast({ title: '下载失败', icon: 'none' });
}
},
fail: () => {
uni.hideLoading();
uni.showToast({ title: '下载失败,请重试', icon: 'none' });
}
});
},
compressAndSave(path) {
uni.compressImage({
src: path,
quality: 80,
success: (res) => {
const filePath = res.tempFilePath || path;
uni.saveImageToPhotosAlbum({
filePath,
success: () => {
uni.hideLoading();
uni.showToast({ title: '已保存到相册', icon: 'success', duration: 2000 });
},
fail: (error) => {
uni.hideLoading();
if ((error.errMsg || '').includes('auth')) {
uni.showModal({
title: '需要相册权限',
content: '请在设置中开启相册权限',
confirmText: '去设置',
success: (modalRes) => { if (modalRes.confirm) uni.openSetting(); }
});
} else {
uni.showToast({ title: '保存失败', icon: 'none' });
}
}
});
},
fail: () => {
uni.saveImageToPhotosAlbum({ filePath: path });
}
});
},
onShare() {
uni.showActionSheet({
itemList: ['分享到微信', '分享到朋友圈', '复制链接'],
success: (res) => {
if (res.tapIndex === 2) this.copyLink();
}
});
},
onCompare() {
// 跳转到设计效果页
const params = {
id: this.currentArticleId || ''
};
// 如果有图片数据传递图片URL
if (this.images && this.images.length > 0) {
if (this.images.length >= 2) {
// 如果有两张图片,第一张作为改造前,第二张作为改造后
params.beforeImage = encodeURIComponent(this.images[0].originalUrl || this.images[0].displayUrl);
params.afterImage = encodeURIComponent(this.images[1].originalUrl || this.images[1].displayUrl);
} else if (this.images.length === 1) {
// 如果只有一张图片,作为改造后
params.afterImage = encodeURIComponent(this.images[0].originalUrl || this.images[0].displayUrl);
}
}
// 传递提示词
if (this.imageDescription) {
params.promptText = encodeURIComponent(this.imageDescription);
} else if (this.articleData && this.articleData.prompt) {
params.promptText = encodeURIComponent(this.articleData.prompt);
}
const queryString = Object.keys(params)
.filter(key => params[key])
.map(key => `${key}=${params[key]}`)
.join('&');
uni.navigateTo({
url: `/pages/ai-generate/effect?${queryString}`,
fail: (err) => {
console.error('Navigate to effect page failed:', err);
uni.showToast({
title: '跳转失败',
icon: 'none'
});
}
});
},
onBack() {
uni.navigateBack();
},
copyLink() {
const shareUrl = this.currentArticleId
? `https://yourapp.com/pages/ai-generate/image?id=${this.currentArticleId}`
: 'https://yourapp.com/pages/ai-generate/image';
uni.setClipboardData({ data: shareUrl, success: () => { uni.showToast({ title: '链接已复制', icon: 'success' }); } });
},
onToggleDescription() {
this.isDescriptionExpanded = !this.isDescriptionExpanded;
},
onCreatorTap() {
uni.showToast({ title: `查看${this.creatorName}的主页`, icon: 'none' });
},
onFollow() {
const isFollowing = !this.isFollowing;
this.setData({ isFollowing });
uni.showToast({ title: isFollowing ? '关注成功' : '取消关注', icon: 'success' });
},
onLike() {
const isLiked = !this.isLiked;
const likeCount = isLiked ? this.likeCount + 1 : this.likeCount - 1;
this.setData({ isLiked, likeCount });
if (isLiked) uni.vibrateShort();
},
onCreateSimilar() {
const description = this.imageDescription || '';
const encodedDescription = encodeURIComponent(description);
// 获取imageInput作为参考图
let referenceImage = '';
if (this.articleData && this.articleData.imageInput) {
referenceImage = this.articleData.imageInput;
} else if (this.images && this.images.length > 0) {
// 如果没有imageInput使用第一张图片
referenceImage = this.images[0].originalUrl || this.images[0].displayUrl;
}
const params = {
description: encodedDescription,
from: 'image',
articleId: this.currentArticleId || ''
};
if (referenceImage) {
params.referenceImage = encodeURIComponent(referenceImage);
}
const queryString = Object.keys(params)
.filter(key => params[key])
.map(key => `${key}=${params[key]}`)
.join('&');
uni.navigateTo({
url: `/pages/ai-generate/index?${queryString}`
});
},
onConsult() {
// #ifdef MP-WEIXIN
// 微信小程序环境,打开微信客服
wx.openCustomerServiceChat({
extInfo: {
url: 'https://work.weixin.qq.com/kfid/your_kfid' // 替换为实际的客服链接
},
corpId: 'your_corp_id', // 替换为实际的企业ID
success: (res) => {
console.log('打开客服成功', res);
},
fail: (err) => {
console.error('打开客服失败', err);
// 降级方案:跳转到客服页面
const url = this.chatUrl;
if (url) {
uni.navigateTo({ url: `/pages/users/web_page/index?webUel=${encodeURIComponent(url)}&title=客服` });
} else {
uni.showToast({ title: '暂无客服', icon: 'none' });
}
}
});
// #endif
// #ifndef MP-WEIXIN
// 非微信环境,使用原有逻辑
const url = this.chatUrl;
if (!url) {
uni.showToast({ title: '暂无客服链接', icon: 'none' });
return;
}
uni.navigateTo({ url: `/pages/users/web_page/index?webUel=${encodeURIComponent(url)}&title=客服` });
// #endif
},
onShareAppMessage() {
let title = '精彩图片分享';
if (this.articleData && this.articleData.title) {
title = this.articleData.title;
if (title.length > 28) title = title.substring(0, 25) + '...';
} else if (this.creatorName) {
title = `${this.creatorName}的作品`;
}
let imageUrl = (this.images[0] && this.images[0].originalUrl) || '/static/images/video-share.png';
const path = this.currentArticleId ? `/pages/ai-generate/image?id=${this.currentArticleId}` : '/pages/ai-generate/assets';
return { title, path, imageUrl };
},
onShareTimeline() {
let title = '精彩图片分享';
if (this.articleData && this.articleData.title) {
title = this.articleData.title;
if (title.length > 20) title = title.substring(0, 17) + '...';
} else if (this.creatorName) {
title = `${this.creatorName}的作品`;
}
let imageUrl = (this.images[0] && this.images[0].originalUrl) || '/static/images/video-share.png';
return { title, imageUrl };
},
onPullDownRefresh() {
if (this.currentArticleId) this.loadArticleDetail(this.currentArticleId);
setTimeout(() => { uni.stopPullDownRefresh(); }, 800);
}
}
};
</script>
<style>
.image-container {
width: 100vw;
height: 100vh;
background: #000000;
position: relative;
overflow: hidden;
}
.media-wrapper { position: absolute; top: 0; left: 0; right: 0; bottom: 0; }
.image-swiper { width: 100%; height: 100%; }
.image-swiper swiper-item { height: 100%; }
.image-slide { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; }
.zoom-wrapper { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; transition: transform 0.1s linear; }
.display-image { width: 100%; height: 100%; }
.top-controls { position: absolute; top: 0; left: 0; right: 0; height: 48px; display: flex; justify-content: space-between; align-items: flex-end; padding: 0 16px 16px; background: linear-gradient(180deg, rgba(0,0,0,0.6) 0%, transparent 100%); z-index: 10; }
.left-controls { display: flex; align-items: center; }
.back-btn { width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; margin-right: 12px; }
.icon-back { font-size: 24px; color: #ffffff; font-weight: bold; }
.time-display { font-size: 14px; color: #ffffff; font-weight: 500; }
.bottom-info { position: absolute; bottom: 40rpx; left: 0; right: 0; padding: 11px; background: linear-gradient(0deg, rgba(0,0,0,0.8) 0%, transparent 100%); z-index: 10; }
.article-status { margin-bottom: 16px; padding: 12px 16px; background: rgba(0, 0, 0, 0.6); border-radius: 8px; }
.loading-text { color: #ffffff; font-size: 14px; opacity: 0.8; }
.error-text { color: #ff6b6b; font-size: 14px; }
.creator-info { display: flex; align-items: center; margin-bottom: 12px; }
.creator-avatar { width: 40px; height: 40px; border-radius: 20px; margin-right: 12px; border: 2px solid rgba(255, 255, 255, 0.3); }
.creator-details { flex: 1; display: flex; align-items: center; justify-content: space-between; }
.creator-name { font-size: 16px; color: #ffffff; font-weight: 500; }
.follow-btn { padding: 6px 16px; background: #ffffff; color: #333333; border-radius: 16px; font-size: 14px; font-weight: 500; transition: all 0.3s ease; }
.follow-btn.following { background: rgba(255, 255, 255, 0.3); color: #ffffff; }
.image-info { margin-bottom: 16px; }
.image-description { font-size: 14px; color: #ffffff; line-height: 1.4; opacity: 0.9; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; line-clamp: 2; overflow: hidden; text-overflow: ellipsis; word-break: break-all; cursor: pointer; }
.image-description.expanded { -webkit-line-clamp: unset; line-clamp: unset; overflow: visible; }
.expand-toggle { display: flex; justify-content: center; align-items: center; margin-top: 8px; padding: 4px 0; cursor: pointer; }
.expand-toggle .iconfont { font-size: 16px; color: rgba(255, 255, 255, 0.7); transition: all 0.3s ease; }
.expand-toggle:active .iconfont { color: rgba(255, 255, 255, 1); transform: scale(1.1); }
.interaction-area { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; }
.like-section { display: flex; align-items: center; }
.like-btn { width: 44px; height: 44px; display: flex; align-items: center; justify-content: center; margin-right: 8px; transition: transform 0.2s ease; }
.like-btn:active { transform: scale(1.2); }
.heart-icon { font-size: 24px; }
.like-count { font-size: 16px; color: #ffffff; font-weight: 500; }
.action-buttons { display: flex; align-items: center; gap: 12px; }
.floating-actions { position: fixed; right: 16px; bottom: 35%; display: flex; flex-direction: column; gap: 20px; z-index: 100; }
.floating-btn { width: 42px; height: 42px; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 4px; background: linear-gradient(135deg, rgba(0, 0, 0, 0.75) 0%, rgba(0, 0, 0, 0.25) 100%); border: 2px solid rgba(255, 255, 255, 0.15); border-radius: 50%; backdrop-filter: blur(20px); transition: all 0.35s cubic-bezier(0.4, 0, 0.2, 1); box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4), 0 4px 8px rgba(0, 0, 0, 0.2); }
.download-btn { background: linear-gradient(135deg, rgba(76, 175, 80, 0.2) 0%, rgba(56, 142, 60, 0.4) 100%); border-color: rgba(139, 195, 74, 0.3); }
.share-btn { background: linear-gradient(135deg, rgba(33, 150, 243, 0.2) 0%, rgba(25, 118, 210, 0.4) 100%); border-color: rgba(100, 181, 246, 0.3); }
.floating-label { font-size: 12px; color: #ffffff; font-weight: 500; letter-spacing: 0.3px; text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); }
.consult-btn { padding: 12px 24px; background: rgba(255, 255, 255, 0.15); color: #ffffff; border: 1px solid rgba(255, 255, 255, 0.5); border-radius: 24px; font-size: 14px; font-weight: 500; transition: transform 0.2s ease, background 0.2s ease; }
.consult-btn:active { transform: scale(0.95); background: rgba(255, 255, 255, 0.25); }
.action-btn { padding: 12px 24px; background: #ffffff; color: #333333; border-radius: 24px; font-size: 14px; font-weight: 500; transition: transform 0.2s ease; }
.action-btn:active { transform: scale(0.95); }
.ai-notice { position: absolute; bottom: 180rpx; left: 14.5%; transform: translateX(-50%); display: flex; align-items: center; justify-content: center; gap: 8rpx; padding: 12rpx 24rpx; background: rgba(0, 0, 0, 0.3); border: 1rpx solid rgba(66, 202, 77, 0.5); border-radius: 40rpx; backdrop-filter: blur(10rpx); z-index: 100; }
.ai-notice { position: fixed; width: 100px;
height: 30px; top: 99px; left: 72%; transform: none; }
.ai-notice-text { font-size: 20rpx; color: #42ca4d; line-height: 1.4; }
.page-indicator { display: flex; justify-content: center; align-items: center; }
.indicator-text { font-size: 12px; color: rgba(255,255,255,0.8); }
@keyframes likeAnimation { 0% { transform: scale(1); } 50% { transform: scale(1.3); } 100% { transform: scale(1); } }
.like-btn.liked .heart-icon { animation: likeAnimation 0.6s ease; }
</style>