| | |
| | | `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, |
| | | `title` VARCHAR(128) NOT NULL COMMENT '标题', |
| | | `cover_img` VARCHAR(512) DEFAULT NULL COMMENT '封面图', |
| | | `category` VARCHAR(32) NOT NULL COMMENT '类别:平台介绍、营销模式、营销动作', |
| | | `category` VARCHAR(32) NOT NULL COMMENT '类别:platform(平台介绍)、mode(营销模式)、action(营销动作)', |
| | | `content_type` TINYINT NOT NULL COMMENT '类型:1图文 2视频', |
| | | `content` MEDIUMTEXT COMMENT '图文内容(富文本HTML)', |
| | | `video_url` VARCHAR(512) DEFAULT NULL COMMENT '视频地址', |
| New file |
| | |
| | | package com.tcm.training.admin.app.projectintroduction.controller; |
| | | |
| | | import com.tcm.training.admin.app.projectintroduction.service.AppProjectIntroductionService; |
| | | import com.tcm.training.admin.common.ApiResponse; |
| | | import com.tcm.training.admin.live.vo.AppProjectIntroductionDetailVO; |
| | | import com.tcm.training.admin.live.vo.AppProjectIntroductionItemVO; |
| | | import org.springframework.validation.annotation.Validated; |
| | | import org.springframework.web.bind.annotation.GetMapping; |
| | | import org.springframework.web.bind.annotation.PathVariable; |
| | | import org.springframework.web.bind.annotation.RequestMapping; |
| | | import org.springframework.web.bind.annotation.RequestParam; |
| | | import org.springframework.web.bind.annotation.RestController; |
| | | |
| | | import java.util.List; |
| | | |
| | | /** |
| | | * APP项目介绍接口 |
| | | */ |
| | | @Validated |
| | | @RestController |
| | | @RequestMapping({"/api/app/project-introduction", "/api/v1/app/project-introduction"}) |
| | | public class AppProjectIntroductionController { |
| | | |
| | | private final AppProjectIntroductionService appProjectIntroductionService; |
| | | |
| | | public AppProjectIntroductionController(AppProjectIntroductionService appProjectIntroductionService) { |
| | | this.appProjectIntroductionService = appProjectIntroductionService; |
| | | } |
| | | |
| | | /** |
| | | * 获取项目介绍列表(可按类别筛选) |
| | | * @param category 类别:platform(平台介绍)、mode(营销模式)、action(营销动作),不传则查询全部 |
| | | */ |
| | | @GetMapping("/list") |
| | | public ApiResponse<List<AppProjectIntroductionItemVO>> list(@RequestParam(required = false) String category) { |
| | | return ApiResponse.ok(appProjectIntroductionService.listByCategory(category)); |
| | | } |
| | | |
| | | /** |
| | | * 获取项目介绍详情 |
| | | * @param id 项目介绍ID |
| | | */ |
| | | @GetMapping("/detail/{id}") |
| | | public ApiResponse<AppProjectIntroductionDetailVO> detail(@PathVariable Long id) { |
| | | AppProjectIntroductionDetailVO detail = appProjectIntroductionService.getDetail(id); |
| | | if (detail == null) { |
| | | return ApiResponse.fail(404, "内容不存在"); |
| | | } |
| | | return ApiResponse.ok(detail); |
| | | } |
| | | } |
| New file |
| | |
| | | package com.tcm.training.admin.app.projectintroduction.service; |
| | | |
| | | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
| | | import com.tcm.training.admin.live.entity.ProjectIntroduction; |
| | | import com.tcm.training.admin.live.mapper.ProjectIntroductionMapper; |
| | | import com.tcm.training.admin.live.vo.AppProjectIntroductionDetailVO; |
| | | import com.tcm.training.admin.live.vo.AppProjectIntroductionItemVO; |
| | | import org.springframework.stereotype.Service; |
| | | import org.springframework.transaction.annotation.Transactional; |
| | | |
| | | import java.util.ArrayList; |
| | | import java.util.List; |
| | | |
| | | @Service |
| | | public class AppProjectIntroductionService { |
| | | |
| | | private final ProjectIntroductionMapper projectIntroductionMapper; |
| | | |
| | | public AppProjectIntroductionService(ProjectIntroductionMapper projectIntroductionMapper) { |
| | | this.projectIntroductionMapper = projectIntroductionMapper; |
| | | } |
| | | |
| | | /** |
| | | * 获取APP项目介绍列表(按类别筛选) |
| | | */ |
| | | public List<AppProjectIntroductionItemVO> listByCategory(String category) { |
| | | LambdaQueryWrapper<ProjectIntroduction> wrapper = new LambdaQueryWrapper<>(); |
| | | wrapper.eq(ProjectIntroduction::getStatus, 1) |
| | | .eq(ProjectIntroduction::getDeleted, 0); |
| | | |
| | | if (category != null && !category.isEmpty()) { |
| | | wrapper.eq(ProjectIntroduction::getCategory, category); |
| | | } |
| | | |
| | | wrapper.orderByDesc(ProjectIntroduction::getCreatedAt); |
| | | |
| | | List<ProjectIntroduction> list = projectIntroductionMapper.selectList(wrapper); |
| | | |
| | | List<AppProjectIntroductionItemVO> result = new ArrayList<>(); |
| | | for (ProjectIntroduction item : list) { |
| | | AppProjectIntroductionItemVO vo = new AppProjectIntroductionItemVO(); |
| | | vo.setId(item.getId()); |
| | | vo.setTitle(item.getTitle()); |
| | | vo.setCoverImg(item.getCoverImg()); |
| | | vo.setCategory(item.getCategory()); |
| | | vo.setContentType(item.getContentType()); |
| | | vo.setClickCount(item.getClickCount()); |
| | | result.add(vo); |
| | | } |
| | | |
| | | return result; |
| | | } |
| | | |
| | | /** |
| | | * 获取APP项目介绍详情,并增加点击量 |
| | | */ |
| | | @Transactional |
| | | public AppProjectIntroductionDetailVO getDetail(Long id) { |
| | | ProjectIntroduction entity = projectIntroductionMapper.selectOne( |
| | | new LambdaQueryWrapper<ProjectIntroduction>() |
| | | .eq(ProjectIntroduction::getId, id) |
| | | .eq(ProjectIntroduction::getStatus, 1) |
| | | .eq(ProjectIntroduction::getDeleted, 0) |
| | | ); |
| | | |
| | | if (entity == null) { |
| | | return null; |
| | | } |
| | | |
| | | // 增加点击量 |
| | | entity.setClickCount(entity.getClickCount() + 1); |
| | | projectIntroductionMapper.updateById(entity); |
| | | |
| | | // 转换为VO |
| | | AppProjectIntroductionDetailVO vo = new AppProjectIntroductionDetailVO(); |
| | | vo.setId(entity.getId()); |
| | | vo.setTitle(entity.getTitle()); |
| | | vo.setCoverImg(entity.getCoverImg()); |
| | | vo.setCategory(entity.getCategory()); |
| | | vo.setContentType(entity.getContentType()); |
| | | vo.setContent(entity.getContent()); |
| | | vo.setVideoUrl(entity.getVideoUrl()); |
| | | vo.setVideoFileName(entity.getVideoFileName()); |
| | | vo.setClickCount(entity.getClickCount()); |
| | | |
| | | return vo; |
| | | } |
| | | } |
| New file |
| | |
| | | package com.tcm.training.admin.live.vo; |
| | | |
| | | import lombok.Getter; |
| | | import lombok.NoArgsConstructor; |
| | | import lombok.Setter; |
| | | |
| | | @Getter |
| | | @Setter |
| | | @NoArgsConstructor |
| | | public class AppProjectIntroductionDetailVO { |
| | | |
| | | private Long id; |
| | | |
| | | /** |
| | | * 标题 |
| | | */ |
| | | private String title; |
| | | |
| | | /** |
| | | * 封面图 |
| | | */ |
| | | private String coverImg; |
| | | |
| | | /** |
| | | * 类别:平台介绍、营销模式、营销动作 |
| | | */ |
| | | private String category; |
| | | |
| | | /** |
| | | * 类型:1图文 2视频 |
| | | */ |
| | | private Integer contentType; |
| | | |
| | | /** |
| | | * 图文内容(富文本HTML) |
| | | */ |
| | | private String content; |
| | | |
| | | /** |
| | | * 视频地址 |
| | | */ |
| | | private String videoUrl; |
| | | |
| | | /** |
| | | * 视频文件名 |
| | | */ |
| | | private String videoFileName; |
| | | |
| | | /** |
| | | * 点击量 |
| | | */ |
| | | private Integer clickCount; |
| | | } |
| New file |
| | |
| | | package com.tcm.training.admin.live.vo; |
| | | |
| | | import lombok.Getter; |
| | | import lombok.NoArgsConstructor; |
| | | import lombok.Setter; |
| | | |
| | | @Getter |
| | | @Setter |
| | | @NoArgsConstructor |
| | | public class AppProjectIntroductionItemVO { |
| | | |
| | | private Long id; |
| | | |
| | | /** |
| | | * 标题 |
| | | */ |
| | | private String title; |
| | | |
| | | /** |
| | | * 封面图 |
| | | */ |
| | | private String coverImg; |
| | | |
| | | /** |
| | | * 类别:平台介绍、营销模式、营销动作 |
| | | */ |
| | | private String category; |
| | | |
| | | /** |
| | | * 类型:1图文 2视频 |
| | | */ |
| | | private Integer contentType; |
| | | |
| | | /** |
| | | * 点击量 |
| | | */ |
| | | private Integer clickCount; |
| | | } |
| | |
| | | }; |
| | | |
| | | const categoryOptions = [ |
| | | { label: '平台介绍', value: '平台介绍' }, |
| | | { label: '营销模式', value: '营销模式' }, |
| | | { label: '营销动作', value: '营销动作' }, |
| | | { label: '平台介绍', value: 'platform' }, |
| | | { label: '营销模式', value: 'mode' }, |
| | | { label: '营销动作', value: 'action' }, |
| | | ]; |
| | | |
| | | // Category映射:英文标识 -> 中文显示 |
| | | const categoryMap: Record<string, string> = { |
| | | platform: '平台介绍', |
| | | mode: '营销模式', |
| | | action: '营销动作', |
| | | }; |
| | | |
| | | const categoryText = (value?: string) => categoryMap[value || ''] || value || '-'; |
| | | |
| | | const contentTypeOptions = [ |
| | | { label: '图文', value: 1 }, |
| | |
| | | title: '类别', |
| | | dataIndex: 'category', |
| | | width: 120, |
| | | render: (value) => categoryText(value), |
| | | }, |
| | | { |
| | | title: '类型', |
| | |
| | | import './style.less'; |
| | | |
| | | const categoryOptions = [ |
| | | { label: '平台介绍', value: '平台介绍' }, |
| | | { label: '营销模式', value: '营销模式' }, |
| | | { label: '营销动作', value: '营销动作' }, |
| | | { label: '平台介绍', value: 'platform' }, |
| | | { label: '营销模式', value: 'mode' }, |
| | | { label: '营销动作', value: 'action' }, |
| | | ]; |
| | | |
| | | const contentTypeOptions = [ |
| | |
| | | </view> |
| | | </view> |
| | | |
| | | <view v-else class="entry-content"> |
| | | <image class="entry-image" src="/static/image/project/jieshao.png" mode="widthFix" |
| | | hover-class="entry-image--pressed" hover-stay-time="80" @tap="goList('platform', '平台介绍')"></image> |
| | | <image class="entry-image" src="/static/image/project/moshi.png" mode="widthFix" |
| | | hover-class="entry-image--pressed" hover-stay-time="80" @tap="goList('mode', '营销模式')"></image> |
| | | <image class="entry-image" src="/static/image/project/dongzuo.png" mode="widthFix" |
| | | hover-class="entry-image--pressed" hover-stay-time="80" @tap="goList('action', '营销动作')"></image> |
| | | <view v-else class="content"> |
| | | <image |
| | | class="entry-image" |
| | | src="/static/image/project/jieshao.png" |
| | | mode="widthFix" |
| | | hover-class="entry-image--pressed" |
| | | hover-stay-time="80" |
| | | @tap="goList('platform', '平台介绍')"> |
| | | </image> |
| | | <image |
| | | class="entry-image" |
| | | src="/static/image/project/moshi.png" |
| | | mode="widthFix" |
| | | hover-class="entry-image--pressed" |
| | | hover-stay-time="80" |
| | | @tap="goList('mode', '营销模式')"> |
| | | </image> |
| | | <image |
| | | class="entry-image" |
| | | src="/static/image/project/dongzuo.png" |
| | | mode="widthFix" |
| | | hover-class="entry-image--pressed" |
| | | hover-stay-time="80" |
| | | @tap="goList('action', '营销动作')"> |
| | | </image> |
| | | </view> |
| | | </view> |
| | | </template> |
| | |
| | | <template> |
| | | <view class="container"> |
| | | <!-- 状态栏占位 --> |
| | | <view class="status-bar-placeholder" :style="{ height: statusBarHeight + 'px' }"></view> |
| | | |
| | | <!-- 导航栏 --> |
| | | <view class="nav-bar"> |
| | | <image class="back-icon" src="/static/image/home/left.png" mode="aspectFit" @tap="goBack"></image> |
| | | <text class="nav-title">详情</text> |
| | | <text class="nav-title">{{ detail.title || '详情' }}</text> |
| | | <view class="nav-right-placeholder"></view> |
| | | </view> |
| | | |
| | | <view v-if="isVideoType" class="video-container"> |
| | | <video class="video-player" :src="detail.videoUrl" controls :autoplay="false"></video> |
| | | </view> |
| | | <!-- 内容区域 --> |
| | | <view class="content"> |
| | | <view v-if="loading" class="status-text">加载中...</view> |
| | | <view v-else-if="!detail.id" class="status-text">内容不存在</view> |
| | | <view v-else> |
| | | <!-- 标题 --> |
| | | <view class="detail-title">{{ detail.title }}</view> |
| | | |
| | | <view v-else-if="detail.coverImage" class="cover-container"> |
| | | <image class="cover-image" :src="detail.coverImage" mode="aspectFill"></image> |
| | | </view> |
| | | <!-- 视频内容 --> |
| | | <view v-if="detail.contentType === 2 && detail.videoUrl" class="video-wrap"> |
| | | <video |
| | | :src="detail.videoUrl" |
| | | class="video-player" |
| | | controls |
| | | :show-center-play-btn="true" |
| | | :enable-progress-gesture="true" |
| | | ></video> |
| | | </view> |
| | | |
| | | <view class="content-area"> |
| | | <text class="content-title">{{ detail.title || '详情' }}</text> |
| | | <text v-if="isVideoType && videoSubtitle" class="content-subtitle">{{ videoSubtitle }}</text> |
| | | <rich-text v-if="!isVideoType" :nodes="contentNodes" class="rich-content"></rich-text> |
| | | <view v-if="!detail.id" class="status-text">暂无详情内容</view> |
| | | <!-- 图文内容 --> |
| | | <view v-if="detail.contentType === 1 && detail.content" class="html-content"> |
| | | <rich-text :nodes="detail.content"></rich-text> |
| | | </view> |
| | | |
| | | <!-- 点击量 --> |
| | | <view class="meta-info"> |
| | | <text class="meta-text">阅读 {{ detail.clickCount || 0 }}</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup lang="ts"> |
| | | import { computed, onMounted, ref } from 'vue' |
| | | import { onLoad } from '@dcloudio/uni-app' |
| | | import { getProjectContentDetail } from './static-content.js' |
| | | import { ref, onMounted } from 'vue'; |
| | | import { getProjectIntroductionDetail } from './service' |
| | | |
| | | interface ProjectDetail { |
| | | id?: number | null |
| | | title: string |
| | | subtitle: string |
| | | coverImage: string |
| | | contentType?: number | null |
| | | content: string |
| | | videoUrl: string |
| | | interface ProjectIntroductionDetail { |
| | | id?: number | string |
| | | title?: string |
| | | coverImg?: string |
| | | category?: string |
| | | contentType?: number |
| | | content?: string |
| | | videoUrl?: string |
| | | videoFileName?: string |
| | | clickCount?: number |
| | | } |
| | | |
| | | const statusBarHeight = ref(0) |
| | | const detail = ref<ProjectDetail>(createDefaultDetail()) |
| | | const statusBarHeight = ref(0); |
| | | const loading = ref(false) |
| | | const detail = ref<ProjectIntroductionDetail>({}) |
| | | const projectId = ref('') |
| | | |
| | | function createDefaultDetail(): ProjectDetail { |
| | | return { |
| | | id: null, |
| | | title: '', |
| | | subtitle: '', |
| | | coverImage: '', |
| | | contentType: null, |
| | | content: '', |
| | | videoUrl: '', |
| | | } |
| | | } |
| | | |
| | | const isVideoType = computed(() => Number(detail.value.contentType) === 2) |
| | | |
| | | const videoSubtitle = computed(() => String(detail.value.subtitle || '').trim()) |
| | | |
| | | const contentNodes = computed(() => buildRichTextNodes(detail.value.content, '暂无详情内容')) |
| | | |
| | | /** |
| | | * 页面加载时初始化。 |
| | | */ |
| | | onMounted(() => { |
| | | uni.getSystemInfo({ |
| | | success: (res) => { |
| | | statusBarHeight.value = res.statusBarHeight || 0 |
| | | statusBarHeight.value = res.statusBarHeight || 0; |
| | | } |
| | | }) |
| | | }) |
| | | }); |
| | | |
| | | onLoad((options) => { |
| | | const type = options?.type ? decodeURIComponent(options.type) : '' |
| | | const id = options?.id ? decodeURIComponent(options.id) : '' |
| | | const matched = getProjectContentDetail(type, id) |
| | | detail.value = matched ? { ...createDefaultDetail(), ...matched } : createDefaultDetail() |
| | | }) |
| | | // 获取页面参数 |
| | | const pages = getCurrentPages() |
| | | const currentPage = pages[pages.length - 1] as any |
| | | const options = currentPage?.options || {} |
| | | |
| | | projectId.value = options.id || '' |
| | | |
| | | if (projectId.value) { |
| | | loadProjectIntroductionDetail() |
| | | } |
| | | }); |
| | | |
| | | /** |
| | | * 将富文本内容转换为 `rich-text` 可直接渲染的节点。 |
| | | * 返回上一页 |
| | | */ |
| | | function buildRichTextNodes(content?: string, fallbackText?: string) { |
| | | if (typeof content === 'string' && content.trim()) { |
| | | return content.replace(/<img/gi, |
| | | '<img style="max-width:100%;height:auto;margin:0 auto;display:block"') |
| | | } |
| | | if (typeof fallbackText === 'string' && fallbackText.trim()) { |
| | | return `<p>${fallbackText.trim()}</p>` |
| | | } |
| | | return '<p></p>' |
| | | function goBack() { |
| | | uni.navigateBack() |
| | | } |
| | | |
| | | /** |
| | | * 返回上一页;没有历史栈时回到项目中心。 |
| | | * 加载项目介绍详情。 |
| | | */ |
| | | function goBack() { |
| | | const currentPages = getCurrentPages() |
| | | if (currentPages.length > 1) { |
| | | uni.navigateBack() |
| | | return |
| | | async function loadProjectIntroductionDetail() { |
| | | loading.value = true |
| | | try { |
| | | const response = await getProjectIntroductionDetail(projectId.value) |
| | | if (response?.code === 0 && response.data) { |
| | | detail.value = response.data |
| | | } else { |
| | | detail.value = {} |
| | | } |
| | | } catch { |
| | | detail.value = {} |
| | | } finally { |
| | | loading.value = false |
| | | } |
| | | uni.switchTab({ |
| | | url: '/pages/good/good' |
| | | }) |
| | | } |
| | | </script> |
| | | |
| | | <style> |
| | | page, |
| | | html { |
| | | background-color: #F5F6F8; |
| | | page, html { |
| | | background-color: #fff; |
| | | } |
| | | </style> |
| | | |
| | | <style lang="scss" scoped> |
| | | .container { |
| | | min-height: 100vh; |
| | | background-color: #F5F6F8; |
| | | } |
| | | |
| | | .status-bar-placeholder { |
| | | width: 100%; |
| | | background-color: #fff; |
| | | } |
| | | |
| | | .nav-bar { |
| | | height: 88rpx; |
| | | display: flex; |
| | | align-items: center; |
| | | padding: 0 24rpx; |
| | | justify-content: space-between; |
| | | padding: 0 32rpx; |
| | | background-color: #fff; |
| | | position: relative; |
| | | } |
| | | |
| | | .back-icon { |
| | | width: 40rpx; |
| | | height: 40rpx; |
| | | position: absolute; |
| | | left: 24rpx; |
| | | z-index: 10; |
| | | flex-shrink: 0; |
| | | } |
| | | |
| | | .nav-title { |
| | | flex: 1; |
| | | text-align: center; |
| | | font-size: 36rpx; |
| | | font-weight: 500; |
| | | font-weight: 600; |
| | | color: #333; |
| | | overflow: hidden; |
| | | text-overflow: ellipsis; |
| | | white-space: nowrap; |
| | | padding: 0 20rpx; |
| | | } |
| | | |
| | | .video-container { |
| | | .nav-right-placeholder { |
| | | width: 40rpx; |
| | | height: 40rpx; |
| | | flex-shrink: 0; |
| | | } |
| | | |
| | | .content { |
| | | padding: 24rpx 32rpx; |
| | | } |
| | | |
| | | .detail-title { |
| | | font-size: 40rpx; |
| | | font-weight: 600; |
| | | color: #1a1a1a; |
| | | line-height: 56rpx; |
| | | margin-bottom: 24rpx; |
| | | } |
| | | |
| | | .video-wrap { |
| | | width: 100%; |
| | | height: 402rpx; |
| | | margin-bottom: 32rpx; |
| | | } |
| | | |
| | | .video-player { |
| | | width: 100%; |
| | | height: 100%; |
| | | height: 400rpx; |
| | | border-radius: 12rpx; |
| | | overflow: hidden; |
| | | } |
| | | |
| | | .cover-container { |
| | | width: 100%; |
| | | height: 402rpx; |
| | | } |
| | | |
| | | .cover-image { |
| | | width: 100%; |
| | | height: 100%; |
| | | } |
| | | |
| | | .content-area { |
| | | padding: 32rpx; |
| | | background-color: #F5F6F8; |
| | | } |
| | | |
| | | .content-title { |
| | | font-size: 32rpx; |
| | | font-weight: 500; |
| | | color: #333; |
| | | .html-content { |
| | | font-size: 30rpx; |
| | | line-height: 48rpx; |
| | | } |
| | | |
| | | .content-subtitle { |
| | | display: block; |
| | | margin-top: 12rpx; |
| | | font-size: 12px; |
| | | font-family: 'PingFang SC', 'PingFang-SC', sans-serif; |
| | | color: #5B6475; |
| | | line-height: 1.5; |
| | | } |
| | | |
| | | .rich-content { |
| | | margin-top: 24rpx; |
| | | width: 100%; |
| | | font-size: 28rpx; |
| | | line-height: 1.8; |
| | | color: #333; |
| | | margin-bottom: 32rpx; |
| | | |
| | | :deep(img) { |
| | | max-width: 100% !important; |
| | | height: auto !important; |
| | | display: block; |
| | | margin: 20rpx 0; |
| | | } |
| | | |
| | | :deep(p) { |
| | | margin: 16rpx 0; |
| | | } |
| | | |
| | | :deep(video) { |
| | | width: 100% !important; |
| | | height: auto !important; |
| | | } |
| | | } |
| | | |
| | | .meta-info { |
| | | padding: 24rpx 0; |
| | | border-top: 1rpx solid #f0f0f0; |
| | | margin-top: 32rpx; |
| | | } |
| | | |
| | | .meta-text { |
| | | font-size: 26rpx; |
| | | color: #999; |
| | | } |
| | | |
| | | .status-text { |
| | | margin-top: 24rpx; |
| | | font-size: 26rpx; |
| | | line-height: 40rpx; |
| | | color: #86909C; |
| | | text-align: center; |
| | | font-size: 28rpx; |
| | | color: #999; |
| | | padding: 80rpx 0; |
| | | } |
| | | </style> |
| | |
| | | <template> |
| | | <view class="container"> |
| | | <!-- 状态栏占位 --> |
| | | <view class="status-bar-placeholder" :style="{ height: statusBarHeight + 'px' }"></view> |
| | | |
| | | <!-- 导航栏 --> |
| | | <view class="nav-bar"> |
| | | <image class="back-icon" src="/static/image/home/left.png" mode="aspectFit" @tap="goBack"></image> |
| | | <text class="nav-title">{{ pageTitle }}</text> |
| | | <text class="nav-title">{{ title }}</text> |
| | | <view class="nav-right-placeholder"></view> |
| | | </view> |
| | | |
| | | <!-- 内容区域 --> |
| | | <view class="content"> |
| | | <view v-if="articleList.length" class="article-list"> |
| | | <view class="article-item" v-for="item in articleList" :key="item.id" @tap="goDetail(item)"> |
| | | <image class="article-img" :src="item.coverImage" mode="aspectFill"></image> |
| | | <view v-if="isVideoType(item)" class="play-btn"> |
| | | <text class="play-icon">▶</text> |
| | | </view> |
| | | <view class="article-item" v-for="item in articleList" :key="item.id" @tap="goToDetail(item.id)"> |
| | | <image class="article-img" :src="item.coverImg || defaultCoverImage" mode="aspectFill"></image> |
| | | <view class="article-shadow"> |
| | | <text class="article-text">{{ item.title }}</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | <view v-else-if="loading" class="status-text">加载中...</view> |
| | | <view v-else class="status-text">暂无内容</view> |
| | | </view> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup lang="ts"> |
| | | import { onLoad } from '@dcloudio/uni-app' |
| | | import { ref } from 'vue' |
| | | import { PROJECT_CONTENT_MAP } from './static-content.js' |
| | | import { ref, onMounted } from 'vue'; |
| | | import { onPullDownRefresh } from '@dcloudio/uni-app' |
| | | import { getProjectIntroductionList } from './service' |
| | | |
| | | interface ProjectListItem { |
| | | id: number |
| | | interface ProjectIntroductionItem { |
| | | id: number | string |
| | | title: string |
| | | coverImage: string |
| | | contentType: number |
| | | coverImg?: string |
| | | category?: string |
| | | contentType?: number |
| | | clickCount?: number |
| | | } |
| | | |
| | | const statusBarHeight = ref(0) |
| | | const pageTitle = ref('项目中心') |
| | | const listType = ref('') |
| | | const articleList = ref<ProjectListItem[]>([]) |
| | | const statusBarHeight = ref(0); |
| | | const loading = ref(false) |
| | | const articleList = ref<ProjectIntroductionItem[]>([]) |
| | | const defaultCoverImage = '/static/image/home/zixun.png' |
| | | const title = ref('') |
| | | const category = ref('') |
| | | |
| | | onLoad((options) => { |
| | | /** |
| | | * 页面加载时初始化状态栏高度并拉取项目介绍列表。 |
| | | */ |
| | | onMounted(() => { |
| | | uni.getSystemInfo({ |
| | | success: (res) => { |
| | | statusBarHeight.value = res.statusBarHeight || 0 |
| | | statusBarHeight.value = res.statusBarHeight || 0; |
| | | } |
| | | }) |
| | | const title = options?.title ? decodeURIComponent(options.title) : '' |
| | | const type = options?.type ? decodeURIComponent(options.type) : '' |
| | | pageTitle.value = title || '项目中心' |
| | | listType.value = type |
| | | articleList.value = PROJECT_CONTENT_MAP[type] || [] |
| | | }); |
| | | |
| | | // 获取页面参数 |
| | | const pages = getCurrentPages() |
| | | const currentPage = pages[pages.length - 1] as any |
| | | const options = currentPage?.options || {} |
| | | |
| | | title.value = decodeURIComponent(options.title || '项目介绍') |
| | | category.value = options.type || '' |
| | | |
| | | loadProjectIntroductionList() |
| | | }); |
| | | |
| | | /** |
| | | * 返回上一页 |
| | | */ |
| | | function goBack() { |
| | | uni.navigateBack() |
| | | } |
| | | |
| | | /** |
| | | * 下拉刷新项目介绍列表 |
| | | */ |
| | | onPullDownRefresh(async () => { |
| | | try { |
| | | await loadProjectIntroductionList() |
| | | } finally { |
| | | uni.stopPullDownRefresh() |
| | | } |
| | | }) |
| | | |
| | | /** |
| | | * 视频类型在封面上展示播放按钮。 |
| | | * 拉取项目介绍列表数据。 |
| | | */ |
| | | function isVideoType(item: ProjectListItem) { |
| | | return Number(item.contentType) === 2 |
| | | } |
| | | |
| | | /** |
| | | * 进入项目中心详情页,图文和视频共用同一详情结构。 |
| | | */ |
| | | function goDetail(item: ProjectListItem) { |
| | | uni.navigateTo({ |
| | | url: `/pages/good/projectDetail?type=${encodeURIComponent(listType.value)}&id=${item.id}` |
| | | }) |
| | | } |
| | | |
| | | /** |
| | | * 返回项目中心主页面。 |
| | | */ |
| | | function goBack() { |
| | | const currentPages = getCurrentPages() |
| | | if (currentPages.length > 1) { |
| | | uni.navigateBack() |
| | | return |
| | | async function loadProjectIntroductionList() { |
| | | loading.value = true |
| | | try { |
| | | const response = await getProjectIntroductionList(category.value) |
| | | if (response?.code !== 0 || !Array.isArray(response.data)) { |
| | | articleList.value = [] |
| | | return |
| | | } |
| | | articleList.value = response.data.filter((item: ProjectIntroductionItem) => !!item?.id) |
| | | } catch { |
| | | articleList.value = [] |
| | | } finally { |
| | | loading.value = false |
| | | } |
| | | uni.switchTab({ |
| | | url: '/pages/good/good' |
| | | }) |
| | | } |
| | | |
| | | /** |
| | | * 跳转项目介绍详情页。 |
| | | */ |
| | | const goToDetail = (id?: number | string | null) => { |
| | | uni.navigateTo({ |
| | | url: id |
| | | ? `/pages/good/projectDetail?id=${id}` |
| | | : '/pages/good/projectDetail' |
| | | }); |
| | | }; |
| | | </script> |
| | | |
| | | <style> |
| | |
| | | .container { |
| | | min-height: 100vh; |
| | | background-color: #fff; |
| | | } |
| | | |
| | | .status-bar-placeholder { |
| | | width: 100%; |
| | | } |
| | | |
| | | .nav-bar { |
| | |
| | | |
| | | .nav-right-placeholder { |
| | | width: 40rpx; |
| | | height: 40rpx; |
| | | flex-shrink: 0; |
| | | } |
| | | |
| | | .content { |
| | | margin-top: 32rpx; |
| | | padding: 0 32rpx; |
| | | padding-bottom: 40rpx; |
| | | } |
| | | |
| | | .status-text { |
| | | text-align: center; |
| | | font-size: 24rpx; |
| | | color: #C0C4CC; |
| | | padding: 80rpx 0; |
| | | padding: 24rpx 32rpx; |
| | | } |
| | | |
| | | .article-list { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | flex-wrap: wrap; |
| | | flex-direction: column; |
| | | gap: 24rpx; |
| | | } |
| | | |
| | | .article-item { |
| | | width: 328rpx; |
| | | height: 328rpx; |
| | | border-radius: 16rpx; |
| | | position: relative; |
| | | width: 100%; |
| | | height: 320rpx; |
| | | border-radius: 16rpx; |
| | | overflow: hidden; |
| | | margin-bottom: 24rpx; |
| | | } |
| | | |
| | | .article-img { |
| | | width: 100%; |
| | | height: 100%; |
| | | } |
| | | |
| | | .play-btn { |
| | | position: absolute; |
| | | left: 50%; |
| | | top: 50%; |
| | | width: 80rpx; |
| | | height: 80rpx; |
| | | margin-left: -40rpx; |
| | | margin-top: -40rpx; |
| | | border-radius: 50%; |
| | | background: rgba(0, 0, 0, 0.5); |
| | | border: 2rpx solid rgba(255, 255, 255, 0.9); |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | z-index: 2; |
| | | } |
| | | |
| | | .play-icon { |
| | | color: #fff; |
| | | font-size: 32rpx; |
| | | line-height: 1; |
| | | margin-left: 4rpx; |
| | | display: block; |
| | | } |
| | | |
| | | .article-shadow { |
| | | position: absolute; |
| | | bottom: 0; |
| | | left: 0; |
| | | bottom: 0; |
| | | width: 100%; |
| | | height: 64rpx; |
| | | background: linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.8) 100%); |
| | | height: 160rpx; |
| | | background: linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.6) 100%); |
| | | display: flex; |
| | | align-items: center; |
| | | padding: 0 16rpx; |
| | | align-items: flex-end; |
| | | padding: 24rpx; |
| | | box-sizing: border-box; |
| | | z-index: 1; |
| | | } |
| | | |
| | | .article-text { |
| | | color: #FFFFFF; |
| | | font-size: 24rpx; |
| | | white-space: nowrap; |
| | | font-size: 32rpx; |
| | | font-weight: 600; |
| | | color: #ffffff; |
| | | line-height: 44rpx; |
| | | overflow: hidden; |
| | | text-overflow: ellipsis; |
| | | width: 100%; |
| | | display: -webkit-box; |
| | | -webkit-line-clamp: 2; |
| | | -webkit-box-orient: vertical; |
| | | } |
| | | |
| | | .status-text { |
| | | text-align: center; |
| | | font-size: 28rpx; |
| | | color: #999; |
| | | padding: 80rpx 0; |
| | | } |
| | | </style> |
| | |
| | | // 返回格式:{ code: 200, data: true/false } |
| | | return res?.data === true |
| | | } |
| | | |
| | | /** |
| | | * 获取项目介绍列表。 |
| | | * @param {string} category - 类别:platform(平台介绍)、mode(营销模式)、action(营销动作) |
| | | */ |
| | | export const getProjectIntroductionList = async (category) => { |
| | | const url = category |
| | | ? `/api/app/project-introduction/list?category=${category}` |
| | | : '/api/app/project-introduction/list' |
| | | return publicGet(url) |
| | | } |
| | | |
| | | /** |
| | | * 获取项目介绍详情。 |
| | | * @param {string|number} id - 项目介绍ID |
| | | */ |
| | | export const getProjectIntroductionDetail = async (id) => { |
| | | return publicGet(`/api/app/project-introduction/detail/${id}`) |
| | | } |