// 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 }); }, }, });