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

569 lines
24 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>