feat(分享海报): 启用分享按钮并优化海报布局和日期显示
- 启用课程详情页的分享按钮功能 - 优化分享海报的布局和间距 - 新增日期格式化功能并显示课程日期范围 - 添加底部"扫码了解详情"提示文案
Showing
2 changed files
with
68 additions
and
17 deletions
| ... | @@ -23,19 +23,21 @@ | ... | @@ -23,19 +23,21 @@ |
| 23 | <div class="PosterCover"> | 23 | <div class="PosterCover"> |
| 24 | <img :src="cover_src" alt="课程封面" class="w-full h-full object-cover" crossorigin="anonymous" /> | 24 | <img :src="cover_src" alt="课程封面" class="w-full h-full object-cover" crossorigin="anonymous" /> |
| 25 | </div> | 25 | </div> |
| 26 | - <!-- 下部信息区:左二维码 + 右文案 --> | 26 | + <!-- 下部信息区:左二维码 + 右文案 --> |
| 27 | - <div class="PosterInfo p-4"> | 27 | + <div class="PosterInfo p-4"> |
| 28 | <div class="flex items-start"> | 28 | <div class="flex items-start"> |
| 29 | <!-- 左侧二维码 --> | 29 | <!-- 左侧二维码 --> |
| 30 | <div class="PosterQR mr-4"> | 30 | <div class="PosterQR mr-4"> |
| 31 | <img :src="qr_src" alt="课程二维码" class="w-24 h-24 rounded" crossorigin="anonymous" /> | 31 | <img :src="qr_src" alt="课程二维码" class="w-24 h-24 rounded" crossorigin="anonymous" /> |
| 32 | </div> | 32 | </div> |
| 33 | <!-- 右侧文案 --> | 33 | <!-- 右侧文案 --> |
| 34 | - <div class="flex-1"> | 34 | + <div class="flex-1 flex flex-col space-y-2 -mt-1"> |
| 35 | - <div class="text-lg font-semibold text-gray-800 truncate">{{ title_text }}</div> | 35 | + <div class="text-lg font-semibold text-gray-800 truncate leading-tight -mt-0.5">{{ title_text }}</div> |
| 36 | - <div class="text-sm text-gray-500 mt-1 truncate" v-if="subtitle_text">{{ subtitle_text }}</div> | 36 | + <div class="text-sm text-gray-500 truncate" v-if="subtitle_text">{{ subtitle_text }}</div> |
| 37 | + <div class="text-lg text-gray-400 truncate" v-if="date_range_text">{{ date_range_text }}</div> | ||
| 37 | </div> | 38 | </div> |
| 38 | </div> | 39 | </div> |
| 40 | + <div class="mt-4 text-green-600 text-base">扫码了解详情</div> | ||
| 39 | </div> | 41 | </div> |
| 40 | </div> | 42 | </div> |
| 41 | </div> | 43 | </div> |
| ... | @@ -139,11 +141,31 @@ const intro_text = computed(() => { | ... | @@ -139,11 +141,31 @@ const intro_text = computed(() => { |
| 139 | return text | 141 | return text |
| 140 | }) | 142 | }) |
| 141 | 143 | ||
| 144 | +/** | ||
| 145 | + * @function format_date | ||
| 146 | + * @description 将时间字符串转为 `YYYY-MM-DD` 格式;无法解析则返回原字符串。 | ||
| 147 | + * @param {string} s 时间字符串 | ||
| 148 | + * @returns {string} 处理后的日期字符串 | ||
| 149 | + */ | ||
| 150 | +function format_date(s) { | ||
| 151 | + if (!s) return '' | ||
| 152 | + const t = String(s).trim() | ||
| 153 | + if (t.length >= 10) return t.slice(0, 10) | ||
| 154 | + const d = new Date(t) | ||
| 155 | + if (!isNaN(d.getTime())) { | ||
| 156 | + const y = d.getFullYear() | ||
| 157 | + const m = String(d.getMonth() + 1).padStart(2, '0') | ||
| 158 | + const dd = String(d.getDate()).padStart(2, '0') | ||
| 159 | + return `${y}-${m}-${dd}` | ||
| 160 | + } | ||
| 161 | + return t | ||
| 162 | +} | ||
| 163 | + | ||
| 142 | /** 日期范围(若有) */ | 164 | /** 日期范围(若有) */ |
| 143 | const date_range_text = computed(() => { | 165 | const date_range_text = computed(() => { |
| 144 | - const s = props.course?.start_at || '' | 166 | + const s = props.course?.course_start_time || props.course?.start_at || '' |
| 145 | - const e = props.course?.end_at || '' | 167 | + const e = props.course?.course_end_time || props.course?.end_at || '' |
| 146 | - if (s && e) return `${s} 至 ${e}` | 168 | + if (s && e) return `${format_date(s)} 至 ${format_date(e)}` |
| 147 | return '' | 169 | return '' |
| 148 | }) | 170 | }) |
| 149 | 171 | ||
| ... | @@ -292,16 +314,26 @@ async function compose_poster() { | ... | @@ -292,16 +314,26 @@ async function compose_poster() { |
| 292 | 314 | ||
| 293 | const title_font = 'bold 36px -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei"' | 315 | const title_font = 'bold 36px -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei"' |
| 294 | const subtitle_font = 'normal 24px -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei"' | 316 | const subtitle_font = 'normal 24px -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei"' |
| 317 | + const date_font = 'normal 28px -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei"' | ||
| 318 | + const footnote_font = 'normal 24px -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei"' | ||
| 295 | 319 | ||
| 296 | const measurer = document.createElement('canvas') | 320 | const measurer = document.createElement('canvas') |
| 297 | const mctx = measurer.getContext('2d') | 321 | const mctx = measurer.getContext('2d') |
| 298 | const text_max_w = card_w - padding * 2 - qr_size - 20 | 322 | const text_max_w = card_w - padding * 2 - qr_size - 20 |
| 299 | 323 | ||
| 300 | - // 仅测量标题与副标题,并在信息区垂直居中排版 | 324 | + // 测量标题/副标题/日期,并在信息区顶部对齐排版(脚注单独贴底绘制) |
| 301 | const title_lines = wrap_text(mctx, title_text.value, text_max_w, title_font, 44, 1) | 325 | const title_lines = wrap_text(mctx, title_text.value, text_max_w, title_font, 44, 1) |
| 302 | const subtitle_lines = subtitle_text.value ? wrap_text(mctx, subtitle_text.value, text_max_w, subtitle_font, 32, 1) : [] | 326 | const subtitle_lines = subtitle_text.value ? wrap_text(mctx, subtitle_text.value, text_max_w, subtitle_font, 32, 1) : [] |
| 303 | - const total_text_h = (title_lines.length ? 44 : 0) + (subtitle_lines.length ? 32 : 0) | 327 | + const date_lines = date_range_text.value ? wrap_text(mctx, date_range_text.value, text_max_w, date_font, 40, 1) : [] |
| 304 | - const text_offset_y = Math.max(0, Math.floor((info_body_h - total_text_h) / 2)) | 328 | + // 文本行间距:用于增大标题/副标题/日期的间隔 |
| 329 | + const line_gap = 10 | ||
| 330 | + const lines_count = (title_lines.length ? 1 : 0) + (subtitle_lines.length ? 1 : 0) + (date_lines.length ? 1 : 0) | ||
| 331 | + const gaps_count = Math.max(0, lines_count - 1) | ||
| 332 | + const total_text_h = (title_lines.length ? 44 : 0) | ||
| 333 | + + (subtitle_lines.length ? 32 : 0) | ||
| 334 | + + (date_lines.length ? 40 : 0) | ||
| 335 | + + gaps_count * line_gap | ||
| 336 | + const text_offset_y = 0 | ||
| 305 | const dpr = Math.max(1, Math.min(2, window.devicePixelRatio || 1)) | 337 | const dpr = Math.max(1, Math.min(2, window.devicePixelRatio || 1)) |
| 306 | const canvas = document.createElement('canvas') | 338 | const canvas = document.createElement('canvas') |
| 307 | canvas.width = Math.round(width * dpr) | 339 | canvas.width = Math.round(width * dpr) |
| ... | @@ -371,16 +403,35 @@ async function compose_poster() { | ... | @@ -371,16 +403,35 @@ async function compose_poster() { |
| 371 | } | 403 | } |
| 372 | ctx.drawImage(qr_canvas, card_x + padding, card_y + cover_h + padding, qr_size, qr_size) | 404 | ctx.drawImage(qr_canvas, card_x + padding, card_y + cover_h + padding, qr_size, qr_size) |
| 373 | 405 | ||
| 374 | - // 文案(右侧,仅标题 + 副标题,整体居中) | 406 | + // 文案(右侧,顶部对齐) |
| 375 | let tx = card_x + padding + qr_size + 20 | 407 | let tx = card_x + padding + qr_size + 20 |
| 376 | - let ty = card_y + cover_h + padding + text_offset_y | 408 | + let ty = card_y + cover_h + padding |
| 377 | ctx.fillStyle = '#1f2937' // gray-800 | 409 | ctx.fillStyle = '#1f2937' // gray-800 |
| 378 | ctx.font = title_font | 410 | ctx.font = title_font |
| 379 | - title_lines.forEach(line => { ctx.fillText(line, tx, ty + 34); ty += 44 }) | 411 | + // 增加标题与后续文本的间距 |
| 412 | + title_lines.forEach(line => { ctx.fillText(line, tx, ty + 34); ty += 44 + line_gap }) | ||
| 380 | 413 | ||
| 381 | ctx.fillStyle = '#6b7280' // gray-500 | 414 | ctx.fillStyle = '#6b7280' // gray-500 |
| 382 | ctx.font = subtitle_font | 415 | ctx.font = subtitle_font |
| 383 | - subtitle_lines.forEach(line => { ctx.fillText(line, tx, ty + 24); ty += 32 }) | 416 | + // 增加副标题与后续文本的间距 |
| 417 | + subtitle_lines.forEach(line => { ctx.fillText(line, tx, ty + 24); ty += 32 + line_gap }) | ||
| 418 | + | ||
| 419 | + ctx.fillStyle = '#9ca3af' // gray-400 | ||
| 420 | + ctx.font = date_font | ||
| 421 | + // 日期作为最后一行,不再额外叠加间距 | ||
| 422 | + date_lines.forEach(line => { ctx.fillText(line, tx, ty + 30); ty += 40 }) | ||
| 423 | + | ||
| 424 | + // 脚注:底部绿色提示文案(贴底显示) | ||
| 425 | + ctx.fillStyle = '#10b981' // green-500 | ||
| 426 | + ctx.font = footnote_font | ||
| 427 | + let foot_y = card_y + cover_h + info_h - padding - 14 | ||
| 428 | + // 保证脚注与上方文本至少保留最小间距 | ||
| 429 | + const min_gap = 24 | ||
| 430 | + const last_text_bottom_y = ty | ||
| 431 | + if (foot_y - last_text_bottom_y < min_gap) { | ||
| 432 | + foot_y = last_text_bottom_y + min_gap | ||
| 433 | + } | ||
| 434 | + ctx.fillText('扫码了解详情', tx, foot_y) | ||
| 384 | 435 | ||
| 385 | // 恢复裁剪 | 436 | // 恢复裁剪 |
| 386 | ctx.restore() | 437 | ctx.restore() | ... | ... |
| ... | @@ -224,7 +224,7 @@ | ... | @@ -224,7 +224,7 @@ |
| 224 | </svg> | 224 | </svg> |
| 225 | 咨询 | 225 | 咨询 |
| 226 | </button> | 226 | </button> |
| 227 | - <!-- <button class="flex flex-col items-center text-gray-500 text-xs" @click="open_share_poster"> | 227 | + <button class="flex flex-col items-center text-gray-500 text-xs" @click="open_share_poster"> |
| 228 | <svg | 228 | <svg |
| 229 | xmlns="http://www.w3.org/2000/svg" | 229 | xmlns="http://www.w3.org/2000/svg" |
| 230 | class="h-6 w-6" | 230 | class="h-6 w-6" |
| ... | @@ -240,7 +240,7 @@ | ... | @@ -240,7 +240,7 @@ |
| 240 | /> | 240 | /> |
| 241 | </svg> | 241 | </svg> |
| 242 | 分享 | 242 | 分享 |
| 243 | - </button> --> | 243 | + </button> |
| 244 | </div> | 244 | </div> |
| 245 | <div class="flex items-center"> | 245 | <div class="flex items-center"> |
| 246 | <div v-if="!course?.is_buy" class="mr-2"> | 246 | <div v-if="!course?.is_buy" class="mr-2"> | ... | ... |
-
Please register or login to post a comment