|
@@ -287,7 +287,10 @@
|
|
|
|
|
|
|
|
<!-- Group 2: Promotion Tools (paid users only) -->
|
|
<!-- Group 2: Promotion Tools (paid users only) -->
|
|
|
<view v-if="userStore.isLoggedIn && userStore.isVip && userStore.referralCode" class="menu-section">
|
|
<view v-if="userStore.isLoggedIn && userStore.isVip && userStore.referralCode" class="menu-section">
|
|
|
- <view class="menu-section__header">推广工具</view>
|
|
|
|
|
|
|
+ <view class="menu-section__header">
|
|
|
|
|
+ <text>推广工具</text>
|
|
|
|
|
+ <text class="menu-section__more" @click="goCommission">查看详细 ›</text>
|
|
|
|
|
+ </view>
|
|
|
<view class="promo-card">
|
|
<view class="promo-card">
|
|
|
<view class="promo-card__code-area">
|
|
<view class="promo-card__code-area">
|
|
|
<text class="promo-card__prefix">推广码</text>
|
|
<text class="promo-card__prefix">推广码</text>
|
|
@@ -326,7 +329,7 @@
|
|
|
</view>
|
|
</view>
|
|
|
|
|
|
|
|
<!-- Hidden canvas for poster generation -->
|
|
<!-- Hidden canvas for poster generation -->
|
|
|
- <canvas canvas-id="posterCanvas" id="posterCanvas" class="poster-canvas" />
|
|
|
|
|
|
|
+ <canvas type="2d" id="posterCanvas" class="poster-canvas" />
|
|
|
</view>
|
|
</view>
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
@@ -380,6 +383,12 @@ const isExpired = computed(() => {
|
|
|
|
|
|
|
|
function formatDate(dateStr) {
|
|
function formatDate(dateStr) {
|
|
|
if (!dateStr) return ''
|
|
if (!dateStr) return ''
|
|
|
|
|
+ // Handle Jackson array format: [2026, 6, 8, 23, 59, 59]
|
|
|
|
|
+ if (Array.isArray(dateStr)) {
|
|
|
|
|
+ const [y, m, d] = dateStr
|
|
|
|
|
+ return `${y}-${String(m).padStart(2, '0')}-${String(d).padStart(2, '0')}`
|
|
|
|
|
+ }
|
|
|
|
|
+ // Handle ISO string: "2026-06-08T23:59:59"
|
|
|
return dateStr.slice(0, 10)
|
|
return dateStr.slice(0, 10)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -472,134 +481,170 @@ function generatePoster() {
|
|
|
uni.showLoading({ title: '生成海报…', mask: true })
|
|
uni.showLoading({ title: '生成海报…', mask: true })
|
|
|
const baseUrl = 'http://127.0.0.1:8186'
|
|
const baseUrl = 'http://127.0.0.1:8186'
|
|
|
|
|
|
|
|
- Promise.all([
|
|
|
|
|
- downloadImage(userStore.avatarUrl),
|
|
|
|
|
- profileApi.qrcode().then(res => {
|
|
|
|
|
- const url = res.qrCodeUrl.startsWith('http') ? res.qrCodeUrl : baseUrl + res.qrCodeUrl
|
|
|
|
|
- return downloadImage(url)
|
|
|
|
|
- })
|
|
|
|
|
- ]).then(([avatarPath, qrPath]) => {
|
|
|
|
|
- const ctx = uni.createCanvasContext('posterCanvas')
|
|
|
|
|
- const W = 375, H = 670
|
|
|
|
|
-
|
|
|
|
|
- // Background
|
|
|
|
|
- const grd = ctx.createLinearGradient(0, 0, 0, H)
|
|
|
|
|
- grd.addColorStop(0, '#0c0a1a')
|
|
|
|
|
- grd.addColorStop(1, '#1a1040')
|
|
|
|
|
- ctx.fillStyle = grd
|
|
|
|
|
- ctx.fillRect(0, 0, W, H)
|
|
|
|
|
-
|
|
|
|
|
- // Top glow
|
|
|
|
|
- const topGlow = ctx.createLinearGradient(0, 0, 0, 220)
|
|
|
|
|
- topGlow.addColorStop(0, 'rgba(90, 50, 180, 0.35)')
|
|
|
|
|
- topGlow.addColorStop(1, 'rgba(90, 50, 180, 0)')
|
|
|
|
|
- ctx.fillStyle = topGlow
|
|
|
|
|
- ctx.fillRect(0, 0, W, 220)
|
|
|
|
|
-
|
|
|
|
|
- // Gold accent line
|
|
|
|
|
- ctx.fillStyle = '#C9A84C'
|
|
|
|
|
- ctx.fillRect(W * 0.32, 28, W * 0.36, 1.5)
|
|
|
|
|
-
|
|
|
|
|
- // Brand title
|
|
|
|
|
- ctx.font = '13px sans-serif'
|
|
|
|
|
- ctx.fillStyle = 'rgba(201, 168, 76, 0.7)'
|
|
|
|
|
- ctx.textAlign = 'center'
|
|
|
|
|
- ctx.fillText('数字能量学', W / 2, 52)
|
|
|
|
|
-
|
|
|
|
|
- // Avatar (circular)
|
|
|
|
|
- const avatarSize = 76
|
|
|
|
|
- ctx.save()
|
|
|
|
|
- ctx.beginPath()
|
|
|
|
|
- ctx.arc(W / 2, 128, avatarSize / 2, 0, 2 * Math.PI)
|
|
|
|
|
- ctx.closePath()
|
|
|
|
|
- ctx.fillStyle = '#ffffff'
|
|
|
|
|
- ctx.fill()
|
|
|
|
|
- if (avatarPath) {
|
|
|
|
|
- ctx.clip()
|
|
|
|
|
- ctx.drawImage(avatarPath, (W - avatarSize) / 2, 128 - avatarSize / 2, avatarSize, avatarSize)
|
|
|
|
|
- }
|
|
|
|
|
- ctx.restore()
|
|
|
|
|
-
|
|
|
|
|
- // Nickname
|
|
|
|
|
- ctx.font = 'bold 18px sans-serif'
|
|
|
|
|
- ctx.fillStyle = '#FFFFFF'
|
|
|
|
|
- ctx.textAlign = 'center'
|
|
|
|
|
- ctx.fillText(userStore.nickname || '数字能量探索者', W / 2, 192)
|
|
|
|
|
-
|
|
|
|
|
- // Role badge
|
|
|
|
|
- const roleText = userStore.vipType === 'practitioner' ? '能量师' : userStore.vipType === 'annual' ? 'C端会员' : '探索者'
|
|
|
|
|
- ctx.font = '11px sans-serif'
|
|
|
|
|
- ctx.fillStyle = userStore.vipType === 'practitioner' ? '#C9A84C' : 'rgba(255,255,255,0.5)'
|
|
|
|
|
- ctx.fillText(roleText, W / 2, 214)
|
|
|
|
|
-
|
|
|
|
|
- // Divider
|
|
|
|
|
- ctx.fillStyle = 'rgba(255,255,255,0.07)'
|
|
|
|
|
- ctx.fillRect(30, 240, W - 60, 1)
|
|
|
|
|
-
|
|
|
|
|
- // Section title
|
|
|
|
|
- ctx.font = '12px sans-serif'
|
|
|
|
|
- ctx.fillStyle = 'rgba(255,255,255,0.38)'
|
|
|
|
|
- ctx.fillText('推荐体验', W / 2, 270)
|
|
|
|
|
-
|
|
|
|
|
- // QR code background
|
|
|
|
|
- const qrSize = 160
|
|
|
|
|
- const qrX = (W - qrSize) / 2
|
|
|
|
|
- const qrY = 288
|
|
|
|
|
- ctx.fillStyle = '#FFFFFF'
|
|
|
|
|
- ctx.fillRect(qrX, qrY, qrSize, qrSize)
|
|
|
|
|
- if (qrPath) {
|
|
|
|
|
- ctx.drawImage(qrPath, qrX + 8, qrY + 8, qrSize - 16, qrSize - 16)
|
|
|
|
|
|
|
+ // Get canvas node via selector query (Canvas 2D API)
|
|
|
|
|
+ const query = uni.createSelectorQuery()
|
|
|
|
|
+ query.select('#posterCanvas').fields({ node: true, size: true }).exec((res) => {
|
|
|
|
|
+ if (!res || !res[0]) {
|
|
|
|
|
+ uni.hideLoading()
|
|
|
|
|
+ uni.showToast({ title: '生成失败,请重试', icon: 'none' })
|
|
|
|
|
+ return
|
|
|
}
|
|
}
|
|
|
|
|
+ const canvas = res[0].node
|
|
|
|
|
+ const ctx = canvas.getContext('2d')
|
|
|
|
|
+ const W = 375, H = 670
|
|
|
|
|
+ const dpr = uni.getSystemInfoSync().pixelRatio || 2
|
|
|
|
|
+ canvas.width = W * dpr
|
|
|
|
|
+ canvas.height = H * dpr
|
|
|
|
|
+ ctx.scale(dpr, dpr)
|
|
|
|
|
+
|
|
|
|
|
+ // Download images first
|
|
|
|
|
+ Promise.all([
|
|
|
|
|
+ downloadImage(userStore.avatarUrl),
|
|
|
|
|
+ profileApi.qrcode().then(res => {
|
|
|
|
|
+ const url = res.qrCodeUrl.startsWith('http') ? res.qrCodeUrl : baseUrl + res.qrCodeUrl
|
|
|
|
|
+ return downloadImage(url)
|
|
|
|
|
+ })
|
|
|
|
|
+ ]).then(([avatarPath, qrPath]) => {
|
|
|
|
|
+ // Load images into Canvas 2D Image objects
|
|
|
|
|
+ const imgPromises = []
|
|
|
|
|
+ let avatarImg = null
|
|
|
|
|
+ let qrImg = null
|
|
|
|
|
+
|
|
|
|
|
+ if (avatarPath) {
|
|
|
|
|
+ imgPromises.push(new Promise((resolve) => {
|
|
|
|
|
+ const img = canvas.createImage()
|
|
|
|
|
+ img.onload = () => { avatarImg = img; resolve() }
|
|
|
|
|
+ img.onerror = () => resolve()
|
|
|
|
|
+ img.src = avatarPath
|
|
|
|
|
+ }))
|
|
|
|
|
+ }
|
|
|
|
|
+ if (qrPath) {
|
|
|
|
|
+ imgPromises.push(new Promise((resolve) => {
|
|
|
|
|
+ const img = canvas.createImage()
|
|
|
|
|
+ img.onload = () => { qrImg = img; resolve() }
|
|
|
|
|
+ img.onerror = () => resolve()
|
|
|
|
|
+ img.src = qrPath
|
|
|
|
|
+ }))
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- // Hint
|
|
|
|
|
- ctx.font = '11px sans-serif'
|
|
|
|
|
- ctx.fillStyle = 'rgba(255,255,255,0.32)'
|
|
|
|
|
- ctx.fillText('扫码开启你的数字能量之旅', W / 2, qrY + qrSize + 24)
|
|
|
|
|
-
|
|
|
|
|
- // Bottom
|
|
|
|
|
- ctx.font = '10px sans-serif'
|
|
|
|
|
- ctx.fillStyle = 'rgba(255,255,255,0.18)'
|
|
|
|
|
- ctx.fillText('数字能量学 · 数字能量智能体', W / 2, H - 22)
|
|
|
|
|
-
|
|
|
|
|
- ctx.draw(false)
|
|
|
|
|
-
|
|
|
|
|
- setTimeout(() => {
|
|
|
|
|
- uni.canvasToTempFilePath({
|
|
|
|
|
- canvasId: 'posterCanvas',
|
|
|
|
|
- quality: 1,
|
|
|
|
|
- success: (res) => {
|
|
|
|
|
- uni.saveImageToPhotosAlbum({
|
|
|
|
|
- filePath: res.tempFilePath,
|
|
|
|
|
- success: () => {
|
|
|
|
|
- uni.hideLoading()
|
|
|
|
|
- uni.showToast({ title: '海报已保存到相册', icon: 'success' })
|
|
|
|
|
- },
|
|
|
|
|
- fail: (e) => {
|
|
|
|
|
- uni.hideLoading()
|
|
|
|
|
- if (e.errMsg && e.errMsg.includes('auth deny')) {
|
|
|
|
|
- uni.showModal({
|
|
|
|
|
- title: '保存海报',
|
|
|
|
|
- content: '需要相册权限才能保存海报到本地',
|
|
|
|
|
- confirmText: '去设置',
|
|
|
|
|
- success: (r) => { if (r.confirm) uni.openSetting() }
|
|
|
|
|
- })
|
|
|
|
|
- } else {
|
|
|
|
|
- uni.showToast({ title: '保存失败,请重试', icon: 'none' })
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- })
|
|
|
|
|
- },
|
|
|
|
|
- fail: () => {
|
|
|
|
|
- uni.hideLoading()
|
|
|
|
|
- uni.showToast({ title: '生成失败,请重试', icon: 'none' })
|
|
|
|
|
|
|
+ Promise.all(imgPromises).then(() => {
|
|
|
|
|
+ // ─── Background ───
|
|
|
|
|
+ const grd = ctx.createLinearGradient(0, 0, 0, H)
|
|
|
|
|
+ grd.addColorStop(0, '#0c0a1a')
|
|
|
|
|
+ grd.addColorStop(1, '#1a1040')
|
|
|
|
|
+ ctx.fillStyle = grd
|
|
|
|
|
+ ctx.fillRect(0, 0, W, H)
|
|
|
|
|
+
|
|
|
|
|
+ // Top glow
|
|
|
|
|
+ const topGlow = ctx.createLinearGradient(0, 0, 0, 220)
|
|
|
|
|
+ topGlow.addColorStop(0, 'rgba(90, 50, 180, 0.35)')
|
|
|
|
|
+ topGlow.addColorStop(1, 'rgba(90, 50, 180, 0)')
|
|
|
|
|
+ ctx.fillStyle = topGlow
|
|
|
|
|
+ ctx.fillRect(0, 0, W, 220)
|
|
|
|
|
+
|
|
|
|
|
+ // Gold accent line
|
|
|
|
|
+ ctx.fillStyle = '#C9A84C'
|
|
|
|
|
+ ctx.fillRect(W * 0.32, 28, W * 0.36, 1.5)
|
|
|
|
|
+
|
|
|
|
|
+ // Brand title
|
|
|
|
|
+ ctx.font = '13px sans-serif'
|
|
|
|
|
+ ctx.fillStyle = 'rgba(201, 168, 76, 0.7)'
|
|
|
|
|
+ ctx.textAlign = 'center'
|
|
|
|
|
+ ctx.fillText('数字能量学', W / 2, 52)
|
|
|
|
|
+
|
|
|
|
|
+ // Avatar (circular)
|
|
|
|
|
+ const avatarSize = 76
|
|
|
|
|
+ ctx.save()
|
|
|
|
|
+ ctx.beginPath()
|
|
|
|
|
+ ctx.arc(W / 2, 128, avatarSize / 2, 0, 2 * Math.PI)
|
|
|
|
|
+ ctx.closePath()
|
|
|
|
|
+ ctx.fillStyle = '#ffffff'
|
|
|
|
|
+ ctx.fill()
|
|
|
|
|
+ if (avatarImg) {
|
|
|
|
|
+ ctx.clip()
|
|
|
|
|
+ ctx.drawImage(avatarImg, (W - avatarSize) / 2, 128 - avatarSize / 2, avatarSize, avatarSize)
|
|
|
|
|
+ }
|
|
|
|
|
+ ctx.restore()
|
|
|
|
|
+
|
|
|
|
|
+ // Nickname
|
|
|
|
|
+ ctx.font = 'bold 18px sans-serif'
|
|
|
|
|
+ ctx.fillStyle = '#FFFFFF'
|
|
|
|
|
+ ctx.textAlign = 'center'
|
|
|
|
|
+ ctx.fillText(userStore.nickname || '数字能量探索者', W / 2, 192)
|
|
|
|
|
+
|
|
|
|
|
+ // Role badge
|
|
|
|
|
+ const roleText = userStore.vipType === 'practitioner' ? '能量师' : userStore.vipType === 'annual' ? 'C端会员' : '探索者'
|
|
|
|
|
+ ctx.font = '11px sans-serif'
|
|
|
|
|
+ ctx.fillStyle = userStore.vipType === 'practitioner' ? '#C9A84C' : 'rgba(255,255,255,0.5)'
|
|
|
|
|
+ ctx.fillText(roleText, W / 2, 214)
|
|
|
|
|
+
|
|
|
|
|
+ // Divider
|
|
|
|
|
+ ctx.fillStyle = 'rgba(255,255,255,0.07)'
|
|
|
|
|
+ ctx.fillRect(30, 240, W - 60, 1)
|
|
|
|
|
+
|
|
|
|
|
+ // Section title
|
|
|
|
|
+ ctx.font = '12px sans-serif'
|
|
|
|
|
+ ctx.fillStyle = 'rgba(255,255,255,0.38)'
|
|
|
|
|
+ ctx.fillText('推荐体验', W / 2, 270)
|
|
|
|
|
+
|
|
|
|
|
+ // QR code background
|
|
|
|
|
+ const qrSize = 160
|
|
|
|
|
+ const qrX = (W - qrSize) / 2
|
|
|
|
|
+ const qrY = 288
|
|
|
|
|
+ ctx.fillStyle = '#FFFFFF'
|
|
|
|
|
+ ctx.fillRect(qrX, qrY, qrSize, qrSize)
|
|
|
|
|
+ if (qrImg) {
|
|
|
|
|
+ ctx.drawImage(qrImg, qrX + 8, qrY + 8, qrSize - 16, qrSize - 16)
|
|
|
}
|
|
}
|
|
|
- }, getCurrentInstance())
|
|
|
|
|
- }, 150)
|
|
|
|
|
|
|
|
|
|
- }).catch(e => {
|
|
|
|
|
- console.error('generatePoster error:', e)
|
|
|
|
|
- uni.hideLoading()
|
|
|
|
|
- uni.showToast({ title: '生成失败,请重试', icon: 'none' })
|
|
|
|
|
|
|
+ // Hint
|
|
|
|
|
+ ctx.font = '11px sans-serif'
|
|
|
|
|
+ ctx.fillStyle = 'rgba(255,255,255,0.32)'
|
|
|
|
|
+ ctx.fillText('扫码开启你的数字能量之旅', W / 2, qrY + qrSize + 24)
|
|
|
|
|
+
|
|
|
|
|
+ // Bottom
|
|
|
|
|
+ ctx.font = '10px sans-serif'
|
|
|
|
|
+ ctx.fillStyle = 'rgba(255,255,255,0.18)'
|
|
|
|
|
+ ctx.fillText('数字能量学 · 数字能量智能体', W / 2, H - 22)
|
|
|
|
|
+
|
|
|
|
|
+ // Canvas 2D draws immediately — no ctx.draw() needed
|
|
|
|
|
+ // Export to temp file (pass canvas object, not canvasId)
|
|
|
|
|
+ uni.canvasToTempFilePath({
|
|
|
|
|
+ canvas,
|
|
|
|
|
+ quality: 1,
|
|
|
|
|
+ success: (res) => {
|
|
|
|
|
+ uni.saveImageToPhotosAlbum({
|
|
|
|
|
+ filePath: res.tempFilePath,
|
|
|
|
|
+ success: () => {
|
|
|
|
|
+ uni.hideLoading()
|
|
|
|
|
+ uni.showToast({ title: '海报已保存到相册', icon: 'success' })
|
|
|
|
|
+ },
|
|
|
|
|
+ fail: (e) => {
|
|
|
|
|
+ uni.hideLoading()
|
|
|
|
|
+ if (e.errMsg && e.errMsg.includes('auth deny')) {
|
|
|
|
|
+ uni.showModal({
|
|
|
|
|
+ title: '保存海报',
|
|
|
|
|
+ content: '需要相册权限才能保存海报到本地',
|
|
|
|
|
+ confirmText: '去设置',
|
|
|
|
|
+ success: (r) => { if (r.confirm) uni.openSetting() }
|
|
|
|
|
+ })
|
|
|
|
|
+ } else {
|
|
|
|
|
+ uni.showToast({ title: '保存失败,请重试', icon: 'none' })
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ },
|
|
|
|
|
+ fail: () => {
|
|
|
|
|
+ uni.hideLoading()
|
|
|
|
|
+ uni.showToast({ title: '生成失败,请重试', icon: 'none' })
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ })
|
|
|
|
|
+ }).catch(e => {
|
|
|
|
|
+ console.error('generatePoster error:', e)
|
|
|
|
|
+ uni.hideLoading()
|
|
|
|
|
+ uni.showToast({ title: '生成失败,请重试', icon: 'none' })
|
|
|
|
|
+ })
|
|
|
})
|
|
})
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -619,7 +664,11 @@ function goTags() {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function goAbout() {
|
|
function goAbout() {
|
|
|
- uni.navigateTo({ url: '/pages/about/index' })
|
|
|
|
|
|
|
+ uni.showToast({ title: '功能开发中', icon: 'none' })
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function goCommission() {
|
|
|
|
|
+ uni.navigateTo({ url: '/pages/commission/index' })
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function handleLogout() {
|
|
function handleLogout() {
|
|
@@ -1363,6 +1412,18 @@ $danger: var(--color-danger);
|
|
|
text-transform: uppercase;
|
|
text-transform: uppercase;
|
|
|
letter-spacing: 0.5px;
|
|
letter-spacing: 0.5px;
|
|
|
padding: 14px 16px 6px;
|
|
padding: 14px 16px 6px;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &__more {
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+ color: #3B82F6;
|
|
|
|
|
+ text-transform: none;
|
|
|
|
|
+ letter-spacing: 0;
|
|
|
|
|
+ cursor: pointer;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -1467,11 +1528,11 @@ $danger: var(--color-danger);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.poster-canvas {
|
|
.poster-canvas {
|
|
|
-position: fixed;
|
|
|
|
|
- left: -9999px;
|
|
|
|
|
- top: -9999px;
|
|
|
|
|
- width: 375px;
|
|
|
|
|
- height: 670px;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ position: fixed;
|
|
|
|
|
+ left: -9999px;
|
|
|
|
|
+ top: -9999px;
|
|
|
|
|
+ width: 375px;
|
|
|
|
|
+ height: 670px;
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
</style>
|
|
</style>
|