hookehuyr

feat(SharePoster): 优化海报生成样式并移除分享按钮

- 为海报添加圆角边框和阴影效果
- 调整海报内部元素布局和样式
- 移除课程详情页的分享按钮功能
......@@ -14,7 +14,8 @@
</div>
<!-- 海报区域:直接使用 Canvas 合成的图片,支持长按保存 -->
<div class="PosterCard bg-white rounded-xl shadow-md overflow-hidden mx-auto" ref="card_ref">
<!-- 当已生成海报图时,容器不再应用卡片边框与阴影,避免双重边框视觉效果;降级展示仍保留卡片样式 -->
<div :class="poster_img_src ? 'PosterCard mx-auto' : 'PosterCard bg-white rounded-xl shadow-md overflow-hidden mx-auto'" ref="card_ref">
<img v-if="poster_img_src" :src="poster_img_src" alt="分享海报" class="w-full h-auto object-contain block" />
<!-- 生成失败或尚未生成时的降级展示(可长按截图保存) -->
<div v-else>
......@@ -231,6 +232,32 @@ function wrap_text(ctx, text, max_w, font, line_h, max_lines) {
}
/**
* @function rounded_rect_path
* @description 绘制圆角矩形路径(仅定义 path,不进行填充或描边)。
* @param {CanvasRenderingContext2D} ctx 画布上下文
* @param {number} x 起始x
* @param {number} y 起始y
* @param {number} w 宽度
* @param {number} h 高度
* @param {number} r 圆角半径
* @returns {void}
*/
function rounded_rect_path(ctx, x, y, w, h, r) {
const rr = Math.max(0, Math.min(r, Math.min(w, h) / 2))
ctx.beginPath()
ctx.moveTo(x + rr, y)
ctx.lineTo(x + w - rr, y)
ctx.quadraticCurveTo(x + w, y, x + w, y + rr)
ctx.lineTo(x + w, y + h - rr)
ctx.quadraticCurveTo(x + w, y + h, x + w - rr, y + h)
ctx.lineTo(x + rr, y + h)
ctx.quadraticCurveTo(x, y + h, x, y + h - rr)
ctx.lineTo(x, y + rr)
ctx.quadraticCurveTo(x, y, x + rr, y)
ctx.closePath()
}
/**
* @function compose_poster
* @description 以 Canvas 合成海报:上部封面、左侧二维码、右侧文本信息;生成 dataURL 供长按保存。
* @returns {Promise<void>}
......@@ -247,9 +274,17 @@ async function compose_poster() {
const width = 750
const aspect_ratio = 1 // 高度 = 宽度 * 1
const height = Math.round(width * aspect_ratio)
// 调整封面与信息区比例:封面 2/3,高度留出更多信息区避免裁切
const cover_h = Math.round(height * 2 / 3)
const info_h = height - cover_h
// 卡片外边距,用于投影显示空间;卡片圆角与投影参数
const card_margin = 12
const card_x = card_margin
const card_y = card_margin
const card_w = width - card_margin * 2
const card_h = height - card_margin * 2
const card_radius = 16
// 调整封面与信息区比例:封面 2/3,高度留出更多信息区避免裁切(基于卡片内容高度)
const cover_h = Math.round(card_h * 2 / 3)
const info_h = card_h - cover_h
const padding = 32
const info_body_h = info_h - padding * 2
// 二维码尺寸不超过信息区有效高度,保留少量安全边距,防止底部被裁切
......@@ -260,7 +295,7 @@ async function compose_poster() {
const measurer = document.createElement('canvas')
const mctx = measurer.getContext('2d')
const text_max_w = width - padding * 2 - qr_size - 20
const text_max_w = card_w - padding * 2 - qr_size - 20
// 仅测量标题与副标题,并在信息区垂直居中排版
const title_lines = wrap_text(mctx, title_text.value, text_max_w, title_font, 44, 1)
......@@ -274,26 +309,46 @@ async function compose_poster() {
const ctx = canvas.getContext('2d')
ctx.scale(dpr, dpr)
// 背景
// 卡片阴影与背景(白色圆角卡片 + 灰色边框)
ctx.save()
rounded_rect_path(ctx, card_x, card_y, card_w, card_h, card_radius)
ctx.shadowColor = 'rgba(0,0,0,0.12)'
ctx.shadowBlur = 12
ctx.shadowOffsetX = 0
ctx.shadowOffsetY = 4
ctx.fillStyle = '#ffffff'
ctx.fillRect(0, 0, width, height)
ctx.fill()
ctx.restore()
// 卡片边框
ctx.save()
rounded_rect_path(ctx, card_x, card_y, card_w, card_h, card_radius)
ctx.strokeStyle = '#e5e7eb' // gray-200
ctx.lineWidth = 2
ctx.stroke()
ctx.restore()
// 卡片内容裁剪区域
ctx.save()
rounded_rect_path(ctx, card_x, card_y, card_w, card_h, card_radius)
ctx.clip()
// 封面图(对象填充)
const cover_img = await load_image(cover_src.value)
if (cover_img) {
const scale = Math.max(width / cover_img.width, cover_h / cover_img.height)
const scale = Math.max(card_w / cover_img.width, cover_h / cover_img.height)
const dw = cover_img.width * scale
const dh = cover_img.height * scale
const dx = (width - dw) / 2
const dy = (cover_h - dh) / 2
const dx = card_x + (card_w - dw) / 2
const dy = card_y + (cover_h - dh) / 2
ctx.drawImage(cover_img, dx, dy, dw, dh)
} else {
ctx.fillStyle = '#f3f4f6' // gray-100
ctx.fillRect(0, 0, width, cover_h)
ctx.fillRect(card_x, card_y, card_w, cover_h)
ctx.fillStyle = '#9ca3af' // gray-400
ctx.textAlign = 'center'
ctx.font = 'normal 20px -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei"'
ctx.fillText('封面加载失败', width / 2, cover_h / 2 + 10)
ctx.fillText('封面加载失败', card_x + card_w / 2, card_y + cover_h / 2 + 10)
ctx.textAlign = 'left'
}
......@@ -314,11 +369,11 @@ async function compose_poster() {
qctx.font = `${Math.round(16 * dpr)}px -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei"`
qctx.fillText('二维码生成失败', Math.round(12 * dpr), Math.round((qr_size * dpr) / 2))
}
ctx.drawImage(qr_canvas, padding, cover_h + padding, qr_size, qr_size)
ctx.drawImage(qr_canvas, card_x + padding, card_y + cover_h + padding, qr_size, qr_size)
// 文案(右侧,仅标题 + 副标题,整体居中)
let tx = padding + qr_size + 20
let ty = cover_h + padding + text_offset_y
let tx = card_x + padding + qr_size + 20
let ty = card_y + cover_h + padding + text_offset_y
ctx.fillStyle = '#1f2937' // gray-800
ctx.font = title_font
title_lines.forEach(line => { ctx.fillText(line, tx, ty + 34); ty += 44 })
......@@ -327,6 +382,9 @@ async function compose_poster() {
ctx.font = subtitle_font
subtitle_lines.forEach(line => { ctx.fillText(line, tx, ty + 24); ty += 32 })
// 恢复裁剪
ctx.restore()
// 生成 dataURL
try {
const data_url = canvas.toDataURL('image/png')
......
......@@ -224,7 +224,7 @@
</svg>
咨询
</button>
<button class="flex flex-col items-center text-gray-500 text-xs" @click="open_share_poster">
<!-- <button class="flex flex-col items-center text-gray-500 text-xs" @click="open_share_poster">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
......@@ -240,7 +240,7 @@
/>
</svg>
分享
</button>
</button> -->
</div>
<div class="flex items-center">
<div v-if="!course?.is_buy" class="mr-2">
......