hookehuyr

feat(分享海报): 新增通用分享海报组件并集成至课程详情页

- 添加 SharePoster 组件,支持通过 Canvas 合成课程海报
- 在课程详情页底部操作栏添加分享按钮并接入海报弹窗
- 海报包含封面图、二维码及课程信息,支持长按保存
- 添加 html2canvas 和 qrcode 依赖用于海报生成
- 更新 README 文档说明组件使用方式
......@@ -63,3 +63,15 @@ https://oa-dev.onwall.cn/f/mlaj
- 原因:父组件 `handleSearch` 对“相同关键字”进行了拦截,导致路由参数变化不执行请求。
- 修复:移除拦截逻辑;无论关键字是否变化均触发搜索(防抖控制频率)。
- 位置:`/src/views/courses/CourseListPage.vue``handleSearch`
- 分享海报弹窗(通用组件)
- 入口:课程详情页底部操作栏“分享”按钮。
- 组件:`/src/components/ui/SharePoster.vue`(支持复用),`v-model:show` 控制显隐,`course` 传入课程信息,`qr_url` 可指定二维码内容地址(默认取当前页面 URL)。
- 布局:上部封面图;下部信息区左侧二维码,右侧课程标题、副标题、精简介绍与日期范围。
- 样式:优先使用 TailwindCSS 布局;组件内部使用 Less 做层级嵌套的样式补充。
- 图片规则:当封面图域名为 `cdn.ipadbiz.cn` 时,自动追加 `?imageMogr2/thumbnail/200x/strip/quality/70` 压缩参数。
- 接入位置:`/src/views/courses/CourseDetailPage.vue`,导入并渲染 `<SharePoster v-model:show="show_share_poster" :course="course" />`
- Canvas 合成:弹窗打开时使用 Canvas 直接合成海报(封面图、二维码、文案),生成 `dataURL` 并以 `<img>` 展示,用户可直接长按图片保存到手机(无需额外按钮)。
- 依赖:`pnpm add qrcode`(在 Canvas 内本地生成二维码,避免跨域图片导致画布污染)。
- 跨域:通过 `crossorigin="anonymous"` 加载封面,并追加时间戳防缓存;若封面跨域不允许,则显示降级卡片,仍可长按截图保存。
- 文案:使用中文字体并自动换行限制行数,末行超出追加省略号。
......
......@@ -34,8 +34,10 @@
"@vue-office/pptx": "^1.0.1",
"browser-md5-file": "^1.1.1",
"dayjs": "^1.11.13",
"html2canvas": "^1.4.1",
"lodash": "^4.17.21",
"pdf-vue3": "^1.0.12",
"qrcode": "^1.5.4",
"swiper": "^11.2.6",
"uuid": "^11.1.0",
"vant": "^4.9.19",
......
This diff could not be displayed because it is too large.
......@@ -31,6 +31,7 @@ declare module 'vue' {
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
SearchBar: typeof import('./components/ui/SearchBar.vue')['default']
SharePoster: typeof import('./components/ui/SharePoster.vue')['default']
SummerCampCard: typeof import('./components/ui/SummerCampCard.vue')['default']
TaskCalendar: typeof import('./components/ui/TaskCalendar.vue')['default']
TermsPopup: typeof import('./components/ui/TermsPopup.vue')['default']
......
This diff is collapsed. Click to expand it.
......@@ -197,7 +197,16 @@
<!-- Bottom Action Bar -->
<div class="fixed bottom-16 left-0 right-0 bg-white shadow-lg p-3 flex justify-between items-center">
<div class="flex space-x-4">
<!-- <button class="flex flex-col items-center text-gray-500 text-xs">
<button class="flex flex-col items-center text-gray-500 text-xs transition-transform duration-300"
@click="toggleFavorite" :class="{ 'animate-favorite': isFavorite }">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 transition-transform duration-300"
:fill="isFavorite ? 'red' : 'none'" viewBox="0 0 24 24" :stroke="isFavorite ? 'red' : 'currentColor'">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
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" />
</svg>
收藏
</button>
<button class="flex flex-col items-center text-gray-500 text-xs" @click="open_consult_dialog">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
......@@ -209,21 +218,12 @@
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
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"
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"
/>
</svg>
分享
</button> -->
<button class="flex flex-col items-center text-gray-500 text-xs transition-transform duration-300"
@click="toggleFavorite" :class="{ 'animate-favorite': isFavorite }">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 transition-transform duration-300"
:fill="isFavorite ? 'red' : 'none'" viewBox="0 0 24 24" :stroke="isFavorite ? 'red' : 'currentColor'">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
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" />
</svg>
收藏
咨询
</button>
<button class="flex flex-col items-center text-gray-500 text-xs" @click="open_consult_dialog">
<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"
......@@ -235,10 +235,10 @@
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
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"
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"
/>
</svg>
咨询
分享
</button>
</div>
<div class="flex items-center">
......@@ -364,6 +364,8 @@
</div>
</div>
</van-popup>
<!-- 分享海报弹窗 -->
<!-- <SharePoster v-if="course" v-model:show="show_share_poster" :course="course" /> -->
<van-back-top right="5vw" bottom="25vh" offset="600" />
</AppLayout>
</template>
......@@ -383,6 +385,7 @@ import { sharePage } from '@/composables/useShare.js'
import AppLayout from '@/components/layout/AppLayout.vue'
import FrostedGlass from '@/components/ui/FrostedGlass.vue'
import SharePoster from '@/components/ui/SharePoster.vue'
// 导入接口
import { getCourseDetailAPI, getGroupCommentListAPI, addGroupCommentAPI } from "@/api/course";
......@@ -507,6 +510,22 @@ const isPurchased = ref(false)
const isReviewed = ref(false)
const showReviewPopup = ref(false)
// 分享海报弹窗状态
/**
* @type {import('vue').Ref<boolean>}
* 展示分享海报弹窗的显隐状态
*/
const show_share_poster = ref(false)
/**
* @function open_share_poster
* @description 打开分享海报弹窗
* @returns {void}
*/
const open_share_poster = () => {
show_share_poster.value = true
}
// 处理富文本点击事件,实现图片预览
const handleIntroduceClick = (event) => {
const target = event.target;
......