From 3bd8b516f729df3d32bc48fa7121ed425d55b574 Mon Sep 17 00:00:00 2001
From: hejianhao <15708179461@qq.com>
Date: 星期四, 20 八月 2026 14:10:44 +0800
Subject: [PATCH] feat0806: APP项目中心数据渲染
---
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/vo/AppProjectIntroductionItemVO.java | 38 ++
tcm-training-admin-backend/sql/20260806_admin.sql | 2
traditionalApp/pages/good/projectDetail.vue | 275 ++++++++++---------
tcm-training-admin/src/pages/live/project-introduction/manage.tsx | 6
traditionalApp/pages/good/service.js | 19 +
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/projectintroduction/controller/AppProjectIntroductionController.java | 51 +++
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/projectintroduction/service/AppProjectIntroductionService.java | 88 ++++++
traditionalApp/pages/good/good.vue | 32 +
tcm-training-admin/src/pages/live/project-introduction/index.tsx | 16
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/vo/AppProjectIntroductionDetailVO.java | 53 +++
traditionalApp/pages/good/projectList.vue | 213 +++++++-------
11 files changed, 548 insertions(+), 245 deletions(-)
diff --git a/tcm-training-admin-backend/sql/20260806_admin.sql b/tcm-training-admin-backend/sql/20260806_admin.sql
index c0e0256..f7b148f 100644
--- a/tcm-training-admin-backend/sql/20260806_admin.sql
+++ b/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 '视频地址',
diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/projectintroduction/controller/AppProjectIntroductionController.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/projectintroduction/controller/AppProjectIntroductionController.java
new file mode 100644
index 0000000..28dcd70
--- /dev/null
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/projectintroduction/controller/AppProjectIntroductionController.java
@@ -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);
+ }
+}
diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/projectintroduction/service/AppProjectIntroductionService.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/projectintroduction/service/AppProjectIntroductionService.java
new file mode 100644
index 0000000..643a5eb
--- /dev/null
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/projectintroduction/service/AppProjectIntroductionService.java
@@ -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;
+ }
+}
diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/vo/AppProjectIntroductionDetailVO.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/vo/AppProjectIntroductionDetailVO.java
new file mode 100644
index 0000000..5136357
--- /dev/null
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/vo/AppProjectIntroductionDetailVO.java
@@ -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;
+}
diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/vo/AppProjectIntroductionItemVO.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/vo/AppProjectIntroductionItemVO.java
new file mode 100644
index 0000000..413ae85
--- /dev/null
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/vo/AppProjectIntroductionItemVO.java
@@ -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;
+}
diff --git a/tcm-training-admin/src/pages/live/project-introduction/index.tsx b/tcm-training-admin/src/pages/live/project-introduction/index.tsx
index b7bab07..ab4df30 100644
--- a/tcm-training-admin/src/pages/live/project-introduction/index.tsx
+++ b/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: '类型',
diff --git a/tcm-training-admin/src/pages/live/project-introduction/manage.tsx b/tcm-training-admin/src/pages/live/project-introduction/manage.tsx
index 62be956..e4433ba 100644
--- a/tcm-training-admin/src/pages/live/project-introduction/manage.tsx
+++ b/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 = [
diff --git a/traditionalApp/pages/good/good.vue b/traditionalApp/pages/good/good.vue
index 4b2bc55..4ce09a8 100644
--- a/traditionalApp/pages/good/good.vue
+++ b/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>
diff --git a/traditionalApp/pages/good/projectDetail.vue b/traditionalApp/pages/good/projectDetail.vue
index 5da54c0..ffe90d3 100644
--- a/traditionalApp/pages/good/projectDetail.vue
+++ b/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>
+ <!-- 内容区域 -->
+ <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>
diff --git a/traditionalApp/pages/good/projectList.vue b/traditionalApp/pages/good/projectList.vue
index 639a8e3..f77f154 100644
--- a/traditionalApp/pages/good/projectList.vue
+++ b/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 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>
@@ -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>
diff --git a/traditionalApp/pages/good/service.js b/traditionalApp/pages/good/service.js
index 053153e..61d3197 100644
--- a/traditionalApp/pages/good/service.js
+++ b/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}`)
+}
--
Gitblit v1.7.1