hejianhao
2026-08-20 3bd8b516f729df3d32bc48fa7121ed425d55b574
feat0806: APP项目中心数据渲染
4个文件已添加
7个文件已修改
789 ■■■■■ 已修改文件
tcm-training-admin-backend/sql/20260806_admin.sql 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/projectintroduction/controller/AppProjectIntroductionController.java 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/projectintroduction/service/AppProjectIntroductionService.java 88 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/vo/AppProjectIntroductionDetailVO.java 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/vo/AppProjectIntroductionItemVO.java 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin/src/pages/live/project-introduction/index.tsx 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin/src/pages/live/project-introduction/manage.tsx 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/pages/good/good.vue 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/pages/good/projectDetail.vue 275 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/pages/good/projectList.vue 209 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/pages/good/service.js 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/sql/20260806_admin.sql
@@ -106,7 +106,7 @@
  `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 '视频地址',
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/projectintroduction/controller/AppProjectIntroductionController.java
New file
@@ -0,0 +1,51 @@
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);
    }
}
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/projectintroduction/service/AppProjectIntroductionService.java
New file
@@ -0,0 +1,88 @@
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;
    }
}
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/vo/AppProjectIntroductionDetailVO.java
New file
@@ -0,0 +1,53 @@
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;
}
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/vo/AppProjectIntroductionItemVO.java
New file
@@ -0,0 +1,38 @@
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;
}
tcm-training-admin/src/pages/live/project-introduction/index.tsx
@@ -27,10 +27,19 @@
};
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 },
@@ -135,6 +144,7 @@
      title: '类别',
      dataIndex: 'category',
      width: 120,
      render: (value) => categoryText(value),
    },
    {
      title: '类型',
tcm-training-admin/src/pages/live/project-introduction/manage.tsx
@@ -13,9 +13,9 @@
import './style.less';
const categoryOptions = [
  { label: '平台介绍', value: '平台介绍' },
  { label: '营销模式', value: '营销模式' },
  { label: '营销动作', value: '营销动作' },
  { label: '平台介绍', value: 'platform' },
  { label: '营销模式', value: 'mode' },
  { label: '营销动作', value: 'action' },
];
const contentTypeOptions = [
traditionalApp/pages/good/good.vue
@@ -22,13 +22,31 @@
            </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>
traditionalApp/pages/good/projectDetail.vue
@@ -1,204 +1,225 @@
<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 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-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 v-else-if="detail.coverImage" class="cover-container">
            <image class="cover-image" :src="detail.coverImage" mode="aspectFill"></image>
                <!-- 图文内容 -->
                <view v-if="detail.contentType === 1 && detail.content" class="html-content">
                    <rich-text :nodes="detail.content"></rich-text>
        </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 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() {
    const currentPages = getCurrentPages()
    if (currentPages.length > 1) {
        uni.navigateBack()
        return
    }
    uni.switchTab({
        url: '/pages/good/good'
    })
/**
 * 加载项目介绍详情。
 */
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
    }
}
</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>
traditionalApp/pages/good/projectList.vue
@@ -1,88 +1,120 @@
<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 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()
}
/**
 * 下拉刷新项目介绍列表
 */
onPullDownRefresh(async () => {
    try {
        await loadProjectIntroductionList()
    } finally {
        uni.stopPullDownRefresh()
    }
})
/**
 * 拉取项目介绍列表数据。
 */
async function loadProjectIntroductionList() {
    loading.value = true
    try {
        const response = await getProjectIntroductionList(category.value)
        if (response?.code !== 0 || !Array.isArray(response.data)) {
            articleList.value = []
        return
    }
    uni.switchTab({
        url: '/pages/good/good'
    })
        articleList.value = response.data.filter((item: ProjectIntroductionItem) => !!item?.id)
    } catch {
        articleList.value = []
    } finally {
        loading.value = false
}
}
/**
 * 跳转项目介绍详情页。
 */
const goToDetail = (id?: number | string | null) => {
    uni.navigateTo({
        url: id
            ? `/pages/good/projectDetail?id=${id}`
            : '/pages/good/projectDetail'
    });
};
</script>
<style>
@@ -95,10 +127,6 @@
.container {
    min-height: 100vh;
    background-color: #fff;
}
.status-bar-placeholder {
    width: 100%;
}
.nav-bar {
@@ -125,86 +153,63 @@
.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>
traditionalApp/pages/good/service.js
@@ -20,3 +20,22 @@
    // 返回格式:{ 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}`)
}