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