hookehuyr

feat(分享海报): 启用分享按钮并优化海报布局和日期显示

- 启用课程详情页的分享按钮功能
- 优化分享海报的布局和间距
- 新增日期格式化功能并显示课程日期范围
- 添加底部"扫码了解详情"提示文案
...@@ -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">
......