feat(分享海报): 新增通用分享海报组件并集成至课程详情页
- 添加 SharePoster 组件,支持通过 Canvas 合成课程海报 - 在课程详情页底部操作栏添加分享按钮并接入海报弹窗 - 海报包含封面图、二维码及课程信息,支持长按保存 - 添加 html2canvas 和 qrcode 依赖用于海报生成 - 更新 README 文档说明组件使用方式
Showing
6 changed files
with
49 additions
and
15 deletions
| ... | @@ -63,3 +63,15 @@ https://oa-dev.onwall.cn/f/mlaj | ... | @@ -63,3 +63,15 @@ https://oa-dev.onwall.cn/f/mlaj |
| 63 | - 原因:父组件 `handleSearch` 对“相同关键字”进行了拦截,导致路由参数变化不执行请求。 | 63 | - 原因:父组件 `handleSearch` 对“相同关键字”进行了拦截,导致路由参数变化不执行请求。 |
| 64 | - 修复:移除拦截逻辑;无论关键字是否变化均触发搜索(防抖控制频率)。 | 64 | - 修复:移除拦截逻辑;无论关键字是否变化均触发搜索(防抖控制频率)。 |
| 65 | - 位置:`/src/views/courses/CourseListPage.vue` 的 `handleSearch`。 | 65 | - 位置:`/src/views/courses/CourseListPage.vue` 的 `handleSearch`。 |
| 66 | + | ||
| 67 | + - 分享海报弹窗(通用组件) | ||
| 68 | + - 入口:课程详情页底部操作栏“分享”按钮。 | ||
| 69 | + - 组件:`/src/components/ui/SharePoster.vue`(支持复用),`v-model:show` 控制显隐,`course` 传入课程信息,`qr_url` 可指定二维码内容地址(默认取当前页面 URL)。 | ||
| 70 | + - 布局:上部封面图;下部信息区左侧二维码,右侧课程标题、副标题、精简介绍与日期范围。 | ||
| 71 | + - 样式:优先使用 TailwindCSS 布局;组件内部使用 Less 做层级嵌套的样式补充。 | ||
| 72 | + - 图片规则:当封面图域名为 `cdn.ipadbiz.cn` 时,自动追加 `?imageMogr2/thumbnail/200x/strip/quality/70` 压缩参数。 | ||
| 73 | + - 接入位置:`/src/views/courses/CourseDetailPage.vue`,导入并渲染 `<SharePoster v-model:show="show_share_poster" :course="course" />`。 | ||
| 74 | + - Canvas 合成:弹窗打开时使用 Canvas 直接合成海报(封面图、二维码、文案),生成 `dataURL` 并以 `<img>` 展示,用户可直接长按图片保存到手机(无需额外按钮)。 | ||
| 75 | + - 依赖:`pnpm add qrcode`(在 Canvas 内本地生成二维码,避免跨域图片导致画布污染)。 | ||
| 76 | + - 跨域:通过 `crossorigin="anonymous"` 加载封面,并追加时间戳防缓存;若封面跨域不允许,则显示降级卡片,仍可长按截图保存。 | ||
| 77 | + - 文案:使用中文字体并自动换行限制行数,末行超出追加省略号。 | ... | ... |
| ... | @@ -34,8 +34,10 @@ | ... | @@ -34,8 +34,10 @@ |
| 34 | "@vue-office/pptx": "^1.0.1", | 34 | "@vue-office/pptx": "^1.0.1", |
| 35 | "browser-md5-file": "^1.1.1", | 35 | "browser-md5-file": "^1.1.1", |
| 36 | "dayjs": "^1.11.13", | 36 | "dayjs": "^1.11.13", |
| 37 | + "html2canvas": "^1.4.1", | ||
| 37 | "lodash": "^4.17.21", | 38 | "lodash": "^4.17.21", |
| 38 | "pdf-vue3": "^1.0.12", | 39 | "pdf-vue3": "^1.0.12", |
| 40 | + "qrcode": "^1.5.4", | ||
| 39 | "swiper": "^11.2.6", | 41 | "swiper": "^11.2.6", |
| 40 | "uuid": "^11.1.0", | 42 | "uuid": "^11.1.0", |
| 41 | "vant": "^4.9.19", | 43 | "vant": "^4.9.19", | ... | ... |
pnpm-lock.yaml
0 → 100644
This diff could not be displayed because it is too large.
| ... | @@ -31,6 +31,7 @@ declare module 'vue' { | ... | @@ -31,6 +31,7 @@ declare module 'vue' { |
| 31 | RouterLink: typeof import('vue-router')['RouterLink'] | 31 | RouterLink: typeof import('vue-router')['RouterLink'] |
| 32 | RouterView: typeof import('vue-router')['RouterView'] | 32 | RouterView: typeof import('vue-router')['RouterView'] |
| 33 | SearchBar: typeof import('./components/ui/SearchBar.vue')['default'] | 33 | SearchBar: typeof import('./components/ui/SearchBar.vue')['default'] |
| 34 | + SharePoster: typeof import('./components/ui/SharePoster.vue')['default'] | ||
| 34 | SummerCampCard: typeof import('./components/ui/SummerCampCard.vue')['default'] | 35 | SummerCampCard: typeof import('./components/ui/SummerCampCard.vue')['default'] |
| 35 | TaskCalendar: typeof import('./components/ui/TaskCalendar.vue')['default'] | 36 | TaskCalendar: typeof import('./components/ui/TaskCalendar.vue')['default'] |
| 36 | TermsPopup: typeof import('./components/ui/TermsPopup.vue')['default'] | 37 | TermsPopup: typeof import('./components/ui/TermsPopup.vue')['default'] | ... | ... |
src/components/ui/SharePoster.vue
0 → 100644
This diff is collapsed. Click to expand it.
| ... | @@ -197,7 +197,16 @@ | ... | @@ -197,7 +197,16 @@ |
| 197 | <!-- Bottom Action Bar --> | 197 | <!-- Bottom Action Bar --> |
| 198 | <div class="fixed bottom-16 left-0 right-0 bg-white shadow-lg p-3 flex justify-between items-center"> | 198 | <div class="fixed bottom-16 left-0 right-0 bg-white shadow-lg p-3 flex justify-between items-center"> |
| 199 | <div class="flex space-x-4"> | 199 | <div class="flex space-x-4"> |
| 200 | - <!-- <button class="flex flex-col items-center text-gray-500 text-xs"> | 200 | + <button class="flex flex-col items-center text-gray-500 text-xs transition-transform duration-300" |
| 201 | + @click="toggleFavorite" :class="{ 'animate-favorite': isFavorite }"> | ||
| 202 | + <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 transition-transform duration-300" | ||
| 203 | + :fill="isFavorite ? 'red' : 'none'" viewBox="0 0 24 24" :stroke="isFavorite ? 'red' : 'currentColor'"> | ||
| 204 | + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" | ||
| 205 | + d="M4.318 6.318 a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682 a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318 a4.5 4.5 0 00-6.364 0z" /> | ||
| 206 | + </svg> | ||
| 207 | + 收藏 | ||
| 208 | + </button> | ||
| 209 | + <button class="flex flex-col items-center text-gray-500 text-xs" @click="open_consult_dialog"> | ||
| 201 | <svg | 210 | <svg |
| 202 | xmlns="http://www.w3.org/2000/svg" | 211 | xmlns="http://www.w3.org/2000/svg" |
| 203 | class="h-6 w-6" | 212 | class="h-6 w-6" |
| ... | @@ -209,21 +218,12 @@ | ... | @@ -209,21 +218,12 @@ |
| 209 | stroke-linecap="round" | 218 | stroke-linecap="round" |
| 210 | stroke-linejoin="round" | 219 | stroke-linejoin="round" |
| 211 | stroke-width="2" | 220 | stroke-width="2" |
| 212 | - d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.368 2.684 3 3 0 00-5.368-2.684z" | 221 | + d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" |
| 213 | /> | 222 | /> |
| 214 | </svg> | 223 | </svg> |
| 215 | - 分享 | 224 | + 咨询 |
| 216 | - </button> --> | ||
| 217 | - <button class="flex flex-col items-center text-gray-500 text-xs transition-transform duration-300" | ||
| 218 | - @click="toggleFavorite" :class="{ 'animate-favorite': isFavorite }"> | ||
| 219 | - <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 transition-transform duration-300" | ||
| 220 | - :fill="isFavorite ? 'red' : 'none'" viewBox="0 0 24 24" :stroke="isFavorite ? 'red' : 'currentColor'"> | ||
| 221 | - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" | ||
| 222 | - d="M4.318 6.318 a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682 a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318 a4.5 4.5 0 00-6.364 0z" /> | ||
| 223 | - </svg> | ||
| 224 | - 收藏 | ||
| 225 | </button> | 225 | </button> |
| 226 | - <button class="flex flex-col items-center text-gray-500 text-xs" @click="open_consult_dialog"> | 226 | + <button class="flex flex-col items-center text-gray-500 text-xs" @click="open_share_poster"> |
| 227 | <svg | 227 | <svg |
| 228 | xmlns="http://www.w3.org/2000/svg" | 228 | xmlns="http://www.w3.org/2000/svg" |
| 229 | class="h-6 w-6" | 229 | class="h-6 w-6" |
| ... | @@ -235,10 +235,10 @@ | ... | @@ -235,10 +235,10 @@ |
| 235 | stroke-linecap="round" | 235 | stroke-linecap="round" |
| 236 | stroke-linejoin="round" | 236 | stroke-linejoin="round" |
| 237 | stroke-width="2" | 237 | stroke-width="2" |
| 238 | - d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" | 238 | + d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.368 2.684 3 3 0 00-5.368-2.684z" |
| 239 | /> | 239 | /> |
| 240 | </svg> | 240 | </svg> |
| 241 | - 咨询 | 241 | + 分享 |
| 242 | </button> | 242 | </button> |
| 243 | </div> | 243 | </div> |
| 244 | <div class="flex items-center"> | 244 | <div class="flex items-center"> |
| ... | @@ -364,6 +364,8 @@ | ... | @@ -364,6 +364,8 @@ |
| 364 | </div> | 364 | </div> |
| 365 | </div> | 365 | </div> |
| 366 | </van-popup> | 366 | </van-popup> |
| 367 | + <!-- 分享海报弹窗 --> | ||
| 368 | + <!-- <SharePoster v-if="course" v-model:show="show_share_poster" :course="course" /> --> | ||
| 367 | <van-back-top right="5vw" bottom="25vh" offset="600" /> | 369 | <van-back-top right="5vw" bottom="25vh" offset="600" /> |
| 368 | </AppLayout> | 370 | </AppLayout> |
| 369 | </template> | 371 | </template> |
| ... | @@ -383,6 +385,7 @@ import { sharePage } from '@/composables/useShare.js' | ... | @@ -383,6 +385,7 @@ import { sharePage } from '@/composables/useShare.js' |
| 383 | 385 | ||
| 384 | import AppLayout from '@/components/layout/AppLayout.vue' | 386 | import AppLayout from '@/components/layout/AppLayout.vue' |
| 385 | import FrostedGlass from '@/components/ui/FrostedGlass.vue' | 387 | import FrostedGlass from '@/components/ui/FrostedGlass.vue' |
| 388 | +import SharePoster from '@/components/ui/SharePoster.vue' | ||
| 386 | 389 | ||
| 387 | // 导入接口 | 390 | // 导入接口 |
| 388 | import { getCourseDetailAPI, getGroupCommentListAPI, addGroupCommentAPI } from "@/api/course"; | 391 | import { getCourseDetailAPI, getGroupCommentListAPI, addGroupCommentAPI } from "@/api/course"; |
| ... | @@ -507,6 +510,22 @@ const isPurchased = ref(false) | ... | @@ -507,6 +510,22 @@ const isPurchased = ref(false) |
| 507 | const isReviewed = ref(false) | 510 | const isReviewed = ref(false) |
| 508 | const showReviewPopup = ref(false) | 511 | const showReviewPopup = ref(false) |
| 509 | 512 | ||
| 513 | +// 分享海报弹窗状态 | ||
| 514 | +/** | ||
| 515 | + * @type {import('vue').Ref<boolean>} | ||
| 516 | + * 展示分享海报弹窗的显隐状态 | ||
| 517 | + */ | ||
| 518 | +const show_share_poster = ref(false) | ||
| 519 | + | ||
| 520 | +/** | ||
| 521 | + * @function open_share_poster | ||
| 522 | + * @description 打开分享海报弹窗 | ||
| 523 | + * @returns {void} | ||
| 524 | + */ | ||
| 525 | +const open_share_poster = () => { | ||
| 526 | + show_share_poster.value = true | ||
| 527 | +} | ||
| 528 | + | ||
| 510 | // 处理富文本点击事件,实现图片预览 | 529 | // 处理富文本点击事件,实现图片预览 |
| 511 | const handleIntroduceClick = (event) => { | 530 | const handleIntroduceClick = (event) => { |
| 512 | const target = event.target; | 531 | const target = event.target; | ... | ... |
-
Please register or login to post a comment