// index.js
// 获取应用实例
import {
VueLikePage
} from "../../utils/page";
import {
CDN_URL,
API_BASE_URL,
VIDEO_BASE_URL,
app
} from "../../config/index";
VueLikePage([], {
data: {
cdn_url: "",
baseUrl: API_BASE_URL + "/",
url_link: "",
id: "1",
type: "",
loadCompele: false,
filePath: "",
projectid: '',
isEditing: false,
info: {
resourceImg: {},
banner: {},
sceneTitleImg: {},
recordTitleImg: {},
rescan: {},
activeSceneBdImg: {},
},
isZoom: false,
ipsImgList: [],
selectedIp: null,
selectedIpIndex: -1,
ipScaleX: 1,
ipScaleY: 1,
ipRotate: 0,
ipConfirmed: false,
ipLeft: 0,
ipTop: 0,
positionInitialized: false,
canvasWidth: 375,
canvasHeight: 600,
widgetVisible: true,
zoomScrollLeft: 0,
scaleOrientation: '',
baseUniformScale: 1,
confirmedIps: [],
// 1:贴纸,2:标题,3:日期
tabIndex: 1,
rgb: 'rgba(13, 121, 217, 1)', //初始值
rgbIndex: 1, //0,1,2,3
pick: false,
titleDatas: [],
title: '',
//日期
pickerValue: [0, 0, 0], // 年、月、日的选中索引
years: [], // 年份数组
months: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], // 月份数组
days: [], // 日期数组(根据年月动态生成)
selectedDate: '' // 最终选中的日期
},
methods: {
selectConfirm(e){
this.confirmIp()
// 拿到item,从confirmedIps去掉当前item
const selectedItem = e.currentTarget.dataset.item
console.log(selectedItem)
this.setData({
confirmedIps:this.data.confirmedIps.filter(i=>i.id!==selectedItem.id),
// 再重新还原到选中状态 设置
selectedIp: selectedItem.selectedIp,
selectedIpIndex: selectedItem.selectedIpIndex,
tabIndex: selectedItem.typeIndex, // 判断是贴图还是标题还是日期
rgb: selectedItem.rgb,
imgUrl:selectedItem.typeIndex==1? selectedItem?.imgUrl:'',
title: selectedItem.typeIndex==2?selectedItem?.title:'',
date: selectedItem.typeIndex==3?selectedItem?.date:'',
ipScaleX: selectedItem.scaleX,
ipScaleY: selectedItem.scaleY,
ipRotate: selectedItem.rotate,
ipLeft: selectedItem.left,
ipTop: selectedItem.top,
positionInitialized:true
})
// console.log(this.data)
},
loadDate() {
const now = new Date();
const currentYear = now.getFullYear();
const years = [];
for (let i = currentYear - 10; i <= currentYear + 10; i++) {
years.push(i);
}
this.setData({
years
});
// 2. 初始化当前日期(默认选中今天)
const currentMonth = now.getMonth() + 1; // 月份从0开始,需+1
const currentDay = now.getDate();
// 计算当前年/月/日在数组中的索引
const yearIndex = years.indexOf(currentYear);
const monthIndex = currentMonth - 1;
// 初始化日期数组(根据当前年月)
this.setDays(currentYear, currentMonth);
// 设置默认选中值
this.setData({
pickerValue: [yearIndex, monthIndex, currentDay - 1],
selectedDate: `${currentYear}-${this.formatNum(currentMonth)}-${this.formatNum(currentDay)}`
});
},
// 格式化数字(补0,比如1→01)
formatNum(num) {
return num < 10 ? `0${num}` : num;
},
// 根据年、月动态生成日期数组(处理2月、小月/大月)
setDays(year, month) {
// 计算当月最后一天
const lastDay = new Date(year, month, 0).getDate();
const days = [];
for (let i = 1; i <= lastDay; i++) {
days.push(i);
}
this.setData({
days
});
},
// 日期选择器滚动变化时触发
onDateChange(e) {
const [yearIndex, monthIndex, dayIndex] = e.detail.value;
const {
years,
months,
days
} = this.data;
// 获取选中的年、月、日
const selectedYear = years[yearIndex];
const selectedMonth = months[monthIndex];
const selectedDay = days[dayIndex];
// 重新计算日期数组(防止切换年月后,日期超出当月范围,比如31号切到小月)
this.setDays(selectedYear, selectedMonth);
// 更新选中日期和picker值(日期索引可能变化,需重新校准)
const newDayIndex = Math.min(dayIndex, this.data.days.length - 1);
this.setData({
pickerValue: [yearIndex, monthIndex, newDayIndex],
selectedDate: `${selectedYear}-${this.formatNum(selectedMonth)}-${this.formatNum(this.data.days[newDayIndex])}`
});
},
// 输入时实时更新
onTitleInput(e) {
this.setData({
title: e.detail.value
})
console.log(123, this.data.title)
},
saveTitle(e) {
const currentTitle = this.data.title.trim()
// 防空判断
if (!currentTitle) {
wx.showToast({
title: '请输入标题',
icon: 'none'
})
return
}
// 追加
this.setData({
titleDatas: [...this.data.titleDatas, currentTitle],
title: '',
})
this.selectIp(e)
console.log(this.data.titleDatas)
},
// 显示取色器
toPick: function () {
this.setData({
pick: !this.data.pick,
rgbIndex:5
})
},
//取色结果回调
pickColor(e) {
let rgb = e.detail.color;
this.setData({
rgb: rgb
})
},
//设置颜色
setColor(e) {
let rgb = e.currentTarget.dataset.rgb;
let index = e.currentTarget.dataset.index;
console.log(rgb)
this.setData({
rgb: rgb,
rgbIndex: index
})
},
// 切换tab
handleTabTap(e) {
const index = e.currentTarget.dataset.index;
console.log(index, '11111')
this.setData({
tabIndex: index
}
)
},
zoom() {
this.setData({
isZoom: !this.data.isZoom,
selectedIp: null,
selectedIpIndex: -1,
ipScaleX: 1,
ipScaleY: 1,
ipRotate: 0,
ipConfirmed: false,
confirmedIps: [],
zoomScrollLeft: 0
});
},
onLoad: function (options) {
let {
rdw,
id,
type,
projectid
} = options;
if (!projectid) {
projectid = 'ZHS2409020-1';
}
this.initIpsList(projectid);
this.getData(projectid);
let link = "";
if (type == "0") {
link = `${VIDEO_BASE_URL}4dvedio/${rdw}.mp4`;
} else {
// link = `${VIDEO_BASE_URL}4dpic/${rdw}.jpg`;
// test
link = 'https://pic.616pic.com/phototwo/00/06/02/618e27a7290161785.jpg'
this.loadDate()
}
this.setData({
url_link: link,
id,
type,
projectid,
cdn_url: CDN_URL + "/" + projectid,
});
this.downloadF((data) => {
this.setData({
filePath: data,
});
});
},
initIpsList(projectid) {
const list = [];
for (let i = 1; i <= 34; i++) {
list.push({
name: String(i),
imgUrl: `${CDN_URL}/${projectid}/ip/${i}.png`,
});
}
this.setData({
ipsImgList: list,
});
},
loadcompele() {
this.setData({
loadCompele: true,
});
},
cancel() {
if (this.data.isEditing) {
this.setData({
isEditing: false,
});
} else {
// wx.reLaunch({
// url: "/pages/work/index",
// });
wx.navigateBack();
}
},
edit() {
this.setData({
isEditing: !this.data.isEditing,
});
},
_wrapText(ctx, text, maxWidth) {
const chars = text.split('');
const lines = [];
let currentLine = '';
for (let char of chars) {
const testLine = currentLine + char;
if (ctx.measureText(testLine).width <= maxWidth || currentLine === '') {
currentLine = testLine;
} else {
lines.push(currentLine);
currentLine = char;
}
}
if (currentLine) lines.push(currentLine);
return lines;
},
downloadF(cb = () => {}) {
let link = this.data.url_link,
m_type = "";
if (this.data.type == "1") {
m_type = "jpeg";
} else {
m_type = "video";
}
wx.downloadFile({
url: link,
success: (res) => {
if (res.statusCode == "404") {
return app.showAlert("作品暂未生成,请稍后再试", () => {
wx.navigateBack();
});
}
//判断是否为数组
let typeType =
Object.prototype.toString.call(res.header["Content-Type"]) ==
"[object String]" ?
res.header["Content-Type"] :
res.header["Content-Type"][0];
//判断不是xml文件
if (typeType.indexOf(m_type) > -1) {
cb(res.tempFilePath);
}
},
fail: () => {
app.showAlert("作品暂未生成,请稍后再试");
},
});
this.setData({
showModal: false,
});
},
drawRoundRect(ctx, x, y, width, height, radius) {
ctx.beginPath();
// 左上圆角
ctx.moveTo(x + radius, y);
ctx.arcTo(x + width, y, x + width, y + height, radius);
// 右上圆角
ctx.arcTo(x + width, y + height, x, y + height, radius);
// 右下圆角
ctx.arcTo(x, y + height, x, y, radius);
// 左下圆角
ctx.arcTo(x, y, x + width, y, radius);
ctx.closePath(); // 闭合路径
},
async generateImage() {
wx.showLoading({
title: '生成中...',
mask: true
});
try {
const query = wx.createSelectorQuery();
query.select('.w_video').boundingClientRect();
if (this.data.selectedIp) {
query.select('.ip-overlay').boundingClientRect();
}
query.selectAll('.confirmed-overlay').boundingClientRect();
const res = await new Promise(resolve => query.exec(resolve));
const container = res[0];
const overlay = this.data.selectedIp ? res[1] : null;
const confirmedRects = this.data.selectedIp ? (res[2] || []) : (res[1] || []);
if (!container) throw new Error('Cannot find container');
let width = container.width;
let height = container.height;
if (this.data.isZoom) {
const imgRes = await new Promise((resolve, reject) => {
wx.getImageInfo({
src: this.data.url_link,
success: resolve,
fail: reject
})
});
width = imgRes.width;
height = imgRes.height;
}
// Limit canvas size to avoid incomplete rendering on some devices
const dpr = wx.getSystemInfoSync().pixelRatio;
const maxCanvasSize = 4096;
let scale = 1;
if (width * dpr > maxCanvasSize || height * dpr > maxCanvasSize) {
scale = Math.min(maxCanvasSize / (width * dpr), maxCanvasSize / (height * dpr));
}
const canvasWidth = width * scale;
const canvasHeight = height * scale;
await this._resetWidget(canvasWidth, canvasHeight);
const widget = this.selectComponent('#widget');
const mainUrl = this.data.url_link;
let wxml = '';
if (this.data.isZoom) {
wxml = `
`;
} else {
const bgUrl = this.data.info.resourceImg.bg ? (this.data.cdn_url + this.data.info.resourceImg.bg) : '';
wxml = `
${(this.data.type != '0') ? `` : ''}
`;
}
const style = {
container: {
width: canvasWidth,
height: canvasHeight,
position: 'relative',
overflow: 'hidden'
},
bg: {
width: canvasWidth,
height: canvasHeight,
position: 'absolute',
left: 0,
top: 0
},
main: {
width: canvasWidth,
height: canvasHeight,
position: 'absolute',
left: 0,
top: 0
}
};
const ctx = widget.ctx;
const use2d = widget.data.use2dCanvas;
const ratio = canvasWidth / container.width;
await widget.renderToCanvas({
wxml,
style
});
// Draw confirmed overlays
for (let i = 0; i < this.data.confirmedIps.length; i++) {
const ov = this.data.confirmedIps[i];
const rect = confirmedRects[i];
if (!ov || !rect) continue;
const cx = ((rect.left - container.left + rect.width / 2) + (this.data.isZoom ? this.data.zoomScrollLeft : 0)) * ratio;
const cy = (rect.top - container.top + rect.height / 2) * ratio;
const overlayWidth = rect.width * ratio;
const overlayHeight = rect.height * ratio;
// 贴纸
console.log(ov)
if (ov.typeIndex == 1) {
const stickerInfo = await new Promise((resolve, reject) => {
wx.getImageInfo({
src: ov.imgUrl,
success: resolve,
fail: reject
})
});
let imgToDraw = stickerInfo.path;
if (use2d) {
const canvas = widget.canvas;
const img = canvas.createImage();
await new Promise((resolve, reject) => {
img.onload = resolve;
img.onerror = reject;
img.src = stickerInfo.path;
});
imgToDraw = img;
}
ctx.save();
ctx.translate(cx, cy);
ctx.rotate(ov.rotate * Math.PI / 180);
ctx.scale(ov.scaleX, ov.scaleY);
ctx.drawImage(imgToDraw, -overlayWidth / 2, -overlayHeight / 2, overlayWidth, overlayHeight);
ctx.restore();
} else if (ov.typeIndex == 2 || ov.typeIndex == 3) {
const text = ov.typeIndex == 2 ? ov.title.trim() : ov.date.trim();
if (!text) continue;
console.log(text)
ctx.save();
ctx.translate(cx, cy);
ctx.rotate((ov.rotate || 0) * Math.PI / 180);
ctx.scale(ov.scaleX || 1, ov.scaleY || 1);
// ── 参数定义 ──
const fontSizeRpx = 40;
const fontSizePx = fontSizeRpx * ratio * (container.width / 750);
const maxWidth = canvasWidth;
const minWidth = 80 * ratio;
const minHeight = 45 * ratio;
const lineHeight = 30;
const paddingRpx = 10;
const padding = paddingRpx * ratio;
const borderRadius = 40 * ratio;
ctx.font = ` ${fontSizePx}px cexwz`;
ctx.fillStyle = ov.rgb || '#000000';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
// 计算换行
let lines = [text];
let textWidth = ctx.measureText(text).width;
if (textWidth > maxWidth) {
lines = this._wrapText(ctx, text, maxWidth);
textWidth = Math.max(...lines.map(line => ctx.measureText(line).width));
}
// 应用 min-width
const contentWidth = Math.max(textWidth, minWidth);
// 计算总内容高度(文字行高总和)
const contentHeight = lines.length * lineHeight;
// 最终容器高度(文字高度 + 上下 padding + min-height)
const finalHeight = Math.max(contentHeight + padding, minHeight);
console.log(minHeight)
// 最终容器宽度(文字宽度 + 左右 padding)
const finalWidth = contentWidth + padding * 2;
// 绘制圆角矩形背景
ctx.fillStyle = 'rgba(255, 255, 255, 0.70)';
ctx.beginPath();
this.drawRoundRect(
ctx,
-finalWidth / 2,
-finalHeight / 2,
finalWidth,
finalHeight,
20 // 圆角半径
);
ctx.fill();
// ── 绘制文字(在背景之上) ──
ctx.fillStyle = ov.rgb || '#000000'; // 恢复文字颜色
let yOffset = -contentHeight / 2 + lineHeight / 2; // 文字从容器中间开始
for (const line of lines) {
ctx.fillText(line, 0, yOffset);
yOffset += lineHeight;
}
ctx.restore();
}
if (!use2d) {
await new Promise(resolve => ctx.draw(true, resolve));
}
}
// Draw active overlay if exists
if (this.data.selectedIp && overlay) {
const stickerUrl = this.data.selectedIp.imgUrl;
const stickerInfo = await new Promise((resolve, reject) => {
wx.getImageInfo({
src: stickerUrl,
success: resolve,
fail: reject
})
});
let imgToDraw = stickerInfo.path;
if (use2d) {
const canvas = widget.canvas;
const img = canvas.createImage();
await new Promise((resolve, reject) => {
img.onload = resolve;
img.onerror = reject;
img.src = stickerInfo.path;
});
imgToDraw = img;
}
const cx = ((overlay.left - container.left + overlay.width / 2) + (this.data.isZoom ? this.data.zoomScrollLeft : 0)) * ratio;
const cy = (overlay.top - container.top + overlay.height / 2) * ratio;
const overlayWidth = overlay.width * ratio;
const overlayHeight = overlay.height * ratio;
ctx.save();
ctx.translate(cx, cy);
ctx.rotate(this.data.ipRotate * Math.PI / 180);
ctx.scale(this.data.ipScaleX, this.data.ipScaleY);
ctx.drawImage(imgToDraw, -overlayWidth / 2, -overlayHeight / 2, overlayWidth, overlayHeight);
ctx.restore();
if (!use2d) {
await new Promise(resolve => ctx.draw(true, resolve));
}
}
const {
tempFilePath
} = await widget.canvasToTempFilePath();
// Save
wx.saveImageToPhotosAlbum({
filePath: tempFilePath,
success: () => {
wx.showModal({
title: "提示",
content: "已保存到相册,快去分享吧",
showCancel: false,
});
},
fail: (e) => {
if (!(e.errMsg.indexOf("cancel") > -1)) {
wx.showModal({
title: "提示",
content: "保存失败,请检查是否开启相册保存权限",
showCancel: false,
});
}
}
});
} catch (e) {
console.error(e);
wx.showToast({
title: '生成失败',
icon: 'none'
});
} finally {
wx.hideLoading();
}
},
async _resetWidget(width, height) {
this.setData({
widgetVisible: false
});
await new Promise(r => setTimeout(r, 50));
this.setData({
canvasWidth: width,
canvasHeight: height,
widgetVisible: true
});
await new Promise(r => setTimeout(r, 120));
},
onZoomScroll(e) {
this.setData({
zoomScrollLeft: e.detail.scrollLeft || 0
});
},
saveAlbum() {
let type = this.data.type;
if (this.data.projectid == 'ZHS2409020-1' && type !== '0') {
if (this.data.selectedIp && !this.data.ipConfirmed) {
wx.showToast({
title: '请先确认标签',
icon: 'none'
})
return;
}
this.generateImage();
return;
}
wx.showLoading({
title: "保存中…",
mask: true,
});
if (this.data.filePath) {
let api =
type == "0" ? "saveVideoToPhotosAlbum" : "saveImageToPhotosAlbum";
wx[api]({
filePath: this.data.filePath,
success() {
wx.showModal({
title: "提示",
content: "已保存到相册,快去分享吧",
showCancel: false,
});
},
fail: (e) => {
if (!(e.errMsg.indexOf("cancel") > -1)) {
wx.showModal({
title: "提示",
content: "保存失败,请检查是否开启相册保存权限,可在「右上角」 - 「设置」里查看",
showCancel: false,
});
}
},
complete: () => {
wx.hideLoading();
},
});
}
},
// 横琴是ZHS2409020-1,替换 ZHS2305758-1
getData(prjId = "ZHS2305758-1") {
wx.showLoading({
title: "资源加载中",
});
this.setData({
cdn_url: CDN_URL + "/" + prjId,
});
wx.request({
url: `${VIDEO_BASE_URL}project/4dage-sxb/${prjId}/config.json`,
success: ({
data: {
title,
...rest
}
}) => {
this.setData({
info: rest,
},
() => {
wx.hideLoading();
}
);
wx.setNavigationBarTitle({
title: title,
});
},
});
},
selectIp(e) {
const index = e.currentTarget.dataset.index ;
const item = this.data.ipsImgList[index];
console.log(index,item)
// 这里日期和标题选择的index都没用,但是也需要传,相当于限制了添加上限为ips数组长度
if (!item) return;
if (this.data.selectedIp && !this.data.ipConfirmed) {
this.setData({
selectedIpIndex: index,
selectedIp: item
});
} else {
this.setData({
selectedIpIndex: index,
selectedIp: item,
ipScaleX: 1,
ipScaleY: 1,
ipRotate: 0,
ipConfirmed: false,
positionInitialized: false,
}, () => {
this.getOverlayRect();
const query = wx.createSelectorQuery();
query.select('.w_video').boundingClientRect();
query.select('.ip-overlay').boundingClientRect();
query.exec((res) => {
const container = res[0];
const overlay = res[1];
console.log(container, overlay, 'index')
if (container && overlay) {
this.setData({
ipLeft: overlay.left - container.left,
ipTop: overlay.top - container.top,
positionInitialized: true
});
}
});
});
}
},
dragStart(e) {
if (this.data.ipConfirmed) return;
const touch = e.touches[0];
this.setData({
dragStartX: touch.clientX,
dragStartY: touch.clientY,
startIpLeft: this.data.ipLeft,
startIpTop: this.data.ipTop
});
},
dragMove(e) {
if (this.data.ipConfirmed) return;
const touch = e.touches[0];
const dx = touch.clientX - this.data.dragStartX;
const dy = touch.clientY - this.data.dragStartY;
this.setData({
ipLeft: this.data.startIpLeft + dx,
ipTop: this.data.startIpTop + dy
});
},
getOverlayRect() {
const query = wx.createSelectorQuery();
query.select('.ip-overlay').boundingClientRect(rect => {
if (rect) {
this.setData({
centerX: rect.left + rect.width / 2,
centerY: rect.top + rect.height / 2
});
}
}).exec();
},
rotateStart(e) {
this.getOverlayRect();
const touch = e.touches[0];
const dx = touch.clientX - this.data.centerX;
const dy = touch.clientY - this.data.centerY;
const startAngle = Math.atan2(dy, dx) * 180 / Math.PI;
this.setData({
startRotateAngle: startAngle,
baseIpRotate: this.data.ipRotate
});
},
rotateMove(e) {
const touch = e.touches[0];
const dx = touch.clientX - this.data.centerX;
const dy = touch.clientY - this.data.centerY;
const currentAngle = Math.atan2(dy, dx) * 180 / Math.PI;
const diff = currentAngle - this.data.startRotateAngle;
let nextRotate = this.data.baseIpRotate + diff;
this.setData({
ipRotate: nextRotate
});
},
scaleStart(e) {
const touch = e.touches[0];
this.setData({
startX: touch.clientX,
startY: touch.clientY,
baseUniformScale: (this.data.ipScaleX + this.data.ipScaleY) / 2
});
},
scaleMove(e) {
const touch = e.touches[0];
const dy = touch.clientY - this.data.startY;
const factor = 0.005;
let nextScale = this.data.baseUniformScale + (-dy) * factor;
if (nextScale < 0.2) nextScale = 0.2;
if (nextScale > 4) nextScale = 4;
this.setData({
ipScaleX: nextScale,
ipScaleY: nextScale
});
},
rotateIp() {
// 兼容旧的点击事件,如果不需要可以删除,但保留也不会出错
if (!this.data.selectedIp) return;
const nextRotate = (this.data.ipRotate + 15) % 360;
this.setData({
ipRotate: nextRotate,
});
},
scaleIp() {
if (!this.data.selectedIp) return;
let nextScaleX = this.data.ipScaleX + 0.25;
let nextScaleY = this.data.ipScaleY + 0.25;
if (nextScaleX > 2.5 || nextScaleY > 2.5) {
nextScaleX = 1;
nextScaleY = 1;
}
this.setData({
ipScaleX: nextScaleX,
ipScaleY: nextScaleY,
});
},
deleteIp() {
this.setData({
selectedIp: null,
selectedIpIndex: -1,
ipScaleX: 1,
ipScaleY: 1,
ipRotate: 0,
ipConfirmed: false,
});
},
confirmIp() {
if (!this.data.selectedIp) return;
const overlayItem = {
typeIndex: this.data.tabIndex, // 判断是贴图还是标题还是日期
id: Date.now(),
rgb: this.data.rgb,
imgUrl: this.data.selectedIp.imgUrl,
title: this.data.titleDatas[this.data.titleDatas.length - 1],
date: this.data.selectedDate,
scaleX: this.data.ipScaleX,
scaleY: this.data.ipScaleY,
rotate: this.data.ipRotate,
left: this.data.ipLeft,
top: this.data.ipTop,
selectedIp:this.data.selectedIp,
selectedIpIndex:this.data.selectedIpIndex
};
this.setData({
confirmedIps: [...this.data.confirmedIps, overlayItem],
selectedIp: null,
selectedIpIndex: -1,
ipScaleX: 1,
ipScaleY: 1,
ipRotate: 0,
ipConfirmed: false,
positionInitialized: false
});
},
},
});