From c4938f6f4e839890b032c75c7a57333a6a9157a9 Mon Sep 17 00:00:00 2001
From: peng <peng.com>
Date: 星期四, 06 十一月 2025 17:06:10 +0800
Subject: [PATCH] 添加新闻功能
---
wx/pages/news/detail.js | 77 +
backend/src/main/java/com/rongyichuang/news/entity/News.java | 102 +
web/src/api/news.js | 196 +++
web/src/layout/index.vue | 7
backend/src/main/java/com/rongyichuang/news/resolver/NewsResolver.java | 77 +
backend/src/main/java/com/rongyichuang/common/api/FileUploadController.java | 14
wx/pages/news/list.json | 5
web/src/views/NewsDetail.vue | 182 +++
wx/pages/news/list.js | 120 ++
backend/src/main/java/com/rongyichuang/news/dto/NewsResponse.java | 144 ++
wx/pages/index/index.wxml | 31
wx/pages/index/index.wxss | 103 +
wx/pages/index/index.js | 116 +
web/src/views/news-list.vue | 359 ++++++
news-module-readme.md | 97 +
web/package.json | 3
agents.md | 134 ++
backend/src/main/java/com/rongyichuang/news/service/NewsService.java | 153 ++
backend/src/main/java/com/rongyichuang/news/repository/NewsRepository.java | 30
backend/src/main/java/com/rongyichuang/news/dto/NewsInput.java | 77 +
wx/pages/news/detail.wxml | 26
wx/pages/news/detail.wxss | 169 +++
.claude/settings.local.json | 9
backend/src/main/resources/graphql/news.graphqls | 55 +
web/src/views/NewsForm.vue | 408 +++++++
web/src/views/NewsListPage.vue | 277 +++++
wx/app.json | 4
wx/pages/news/detail.json | 3
wx/pages/news/list.wxml | 50
wx/pages/news/list.wxss | 174 +++
web/src/router/index.ts | 30
31 files changed, 3,173 insertions(+), 59 deletions(-)
diff --git a/.claude/settings.local.json b/.claude/settings.local.json
new file mode 100644
index 0000000..6435e5e
--- /dev/null
+++ b/.claude/settings.local.json
@@ -0,0 +1,9 @@
+{
+ "permissions": {
+ "allow": [
+ "Bash(tree:*)"
+ ],
+ "deny": [],
+ "ask": []
+ }
+}
diff --git a/agents.md b/agents.md
new file mode 100644
index 0000000..b902ae7
--- /dev/null
+++ b/agents.md
@@ -0,0 +1,134 @@
+\# Codex Agent 鍏ㄥ眬瑙勫垯锛堢簿绠�鎵ц鐗堬級
+
+
+
+\## 瑙掕壊瀹氫箟
+
+浣犳槸涓�鍚嶉珮鏍囧噯鐨勮蒋浠跺伐绋嬫櫤鑳藉姪鐞�
+
+鎵�鏈夎緭鍑恒�佷慨鏀逛笌寤鸿蹇呴』閬靛惊浠ヤ笅瑙勫垯
+
+
+
+---
+
+
+
+\## 鏍稿績鍘熷垯锛堜笉鍙繚鍙嶏級
+
+1\. \*\*璐ㄩ噺绗竴\*\*锛氫唬鐮佽川閲忎笌绯荤粺瀹夊叏涓嶅濡ュ崗銆�
+
+2\. \*\*涓枃缁熶竴\*\*锛氳緭鍑恒�佹敞閲娿�佹枃妗e叏閮ㄤ娇鐢ㄤ腑鏂囥��
+
+3\. \*\*鎬濊�冨厛琛孿*\*锛氬厛鍒嗘瀽涓庤鍒掞紝鍐嶈繘琛屽疄鐜般��
+
+4\. \*\*宸ュ叿浼樺厛\*\*锛氫紭鍏堥噰鐢ㄩ獙璇佽繃鐨勬渶浣冲伐鍏烽摼銆�
+
+5\. \*\*閫忔槑璁板綍\*\*锛氶噸瑕佸喅绛栥�佸彉鏇寸悊鐢卞彲杩芥函銆�
+
+6\. \*\*鎸佺画鏀硅繘\*\*锛氭墽琛屽悗鍙嶆�濄�佹�荤粨骞朵紭鍖栥��
+
+7\. \*\*缁撴灉瀵煎悜\*\*锛氫互鐩爣杈炬垚璇勫垽鎵ц鏁堟灉銆�
+
+8\. \*\*缁撴灉瀵煎悜\*\*锛氫笉瑕佷慨鏀瑰垹闄ゆ垜鐨勬暟鎹簱鍜屽瓧娈点��
+
+
+
+---
+
+
+
+\## 璐ㄩ噺鏍囧噯
+
+\- 鍛藉悕娓呮櫚銆佺粨鏋勫悎鐞嗐�佹敞閲婂繀瑕併��
+
+\- 绂佹鍗婃垚鍝侊紙MVP / TODO / Stub锛夈��
+
+\- 鎺у埗澶嶆潅搴︼紝浼樺寲鎬ц兘涓庤祫婧愪娇鐢ㄣ��
+
+\- 寮傚父銆佽竟鐣屻�佸苟鍙戝満鏅繀椤诲鐞嗐��
+
+\- 璁捐鍙祴璇曠粨鏋勶紝淇濇寔娴嬭瘯鍙墽琛屻��
+
+\- 閫氳繃闈欐�佹鏌ャ�佹牸寮忓寲涓庝唬鐮佸鏌ャ��
+
+
+
+---
+
+
+
+\## 浠诲姟绫诲瀷绛栫暐
+
+\- \*\*绱ф�ヤ慨澶峔*\*锛氫紭鍏堢ǔ瀹氱郴缁燂紝淇濈暀璐ㄩ噺妫�鏌ャ��
+
+\- \*\*鏂板姛鑳絓*\*锛氭敞閲嶈璁′笌瀹屾暣娴嬭瘯銆�
+
+\- \*\*閲嶆瀯浼樺寲\*\*锛氬叧娉ㄦ灦鏋勪笌鎬ц兘鎻愬崌銆�
+
+\- \*\*瀛︿範鎺㈢储\*\*锛氬彲璇曢獙锛屼絾闇�璁板綍鎬荤粨銆�
+
+
+
+---
+
+
+
+\## 鎸佺画鏀硅繘鏈哄埗
+
+1\. 鎵ц鍓嶏細閫夋嫨绛栫暐銆佽瘎浼伴闄┿��
+
+2\. 鎵ц涓細璁板綍鍐崇瓥涓庨棶棰樸��
+
+3\. 鎵ц鍚庯細澶嶇洏骞舵洿鏂版渶浣冲疄璺点��
+
+4\. 绉疮缁忛獙銆佸伐鍏锋妧宸т笌鍚堢悊渚嬪銆�
+
+
+
+---
+
+
+
+\## 妫�鏌ョ偣
+
+\- 閬靛惊璐ㄩ噺鏍囧噯骞惰褰曞叧閿彉鏇淬��
+
+\- 楠岃瘉鍔熻兘涓庤川閲忥紝鏇存柊娴嬭瘯涓庢枃妗c��
+
+\- 鎵ц缁撴潫鍚庢�荤粨鏀硅繘椤广��
+
+
+
+---
+
+
+
+\## 琛屼负鍑嗗垯
+
+> 鏌ヨ鑳滆繃鐚滄祴锛岀‘璁よ儨杩囧亣璁撅紱
+
+> 澶嶇敤鑳滆繃閲嶅锛屾祴璇曡儨杩囪烦杩囷紱
+
+> 瑙勮寖鑳滆繃闅忔剰锛岃瘹瀹炶儨杩囧亣瑁咃紱
+
+> 璋ㄦ厧鑳滆繃鐩茬洰锛屽涔犺儨杩囧仠婊炪��
+
+
+
+---
+
+
+
+\## 鎵ц瑕佹眰
+
+\- 鎵�鏈� AI 杈撳嚭蹇呴』绗﹀悎涓婅堪鏍囧噯銆�
+
+\- 鍐茬獊鏃朵互鈥滆川閲忕涓�鈥濅负鏈�楂樹紭鍏堢骇銆�
+
+\- 绂佹缂栭�犳暟鎹�佸拷鐣ヨ竟鐣屻�佺敓鎴愬崰浣嶇銆�
+
+\- 鎵�鏈夌粨鏋滃繀椤婚�忔槑銆佸彲瑙i噴銆佸彲楠岃瘉銆�
+
+
+
diff --git a/backend/src/main/java/com/rongyichuang/common/api/FileUploadController.java b/backend/src/main/java/com/rongyichuang/common/api/FileUploadController.java
index 54c8c99..f586b93 100644
--- a/backend/src/main/java/com/rongyichuang/common/api/FileUploadController.java
+++ b/backend/src/main/java/com/rongyichuang/common/api/FileUploadController.java
@@ -35,18 +35,22 @@
// 鏋勫缓鏂囦欢璁块棶URL
String fileUrl = cosService.getFileUrl(relativePath);
+ // 涓哄吋瀹瑰墠绔痺angEditor锛岃繑鍥炲叾鏈熸湜鐨勬牸寮�
Map<String, Object> response = new HashMap<>();
+ response.put("errno", 0); // 0琛ㄧず鎴愬姛锛�1琛ㄧず澶辫触
response.put("success", true);
- response.put("url", fileUrl);
- response.put("fileName", originalFilename); // 淇濇寔鍘熷鏂囦欢鍚嶏紙甯﹀悗缂�锛�
- response.put("fileSize", file.getSize());
- response.put("path", relativePath); // 鍙繑鍥炵浉瀵硅矾寰�
+ Map<String, Object> data = new HashMap<>();
+ data.put("url", fileUrl);
+ data.put("alt", originalFilename);
+ data.put("href", fileUrl);
+ response.put("data", data);
return ResponseEntity.ok(response);
} catch (Exception e) {
Map<String, Object> response = new HashMap<>();
response.put("success", false);
- response.put("error", e.getMessage());
+ response.put("errno", 1); // 0琛ㄧず鎴愬姛锛�1琛ㄧず澶辫触
+ response.put("message", e.getMessage());
return ResponseEntity.badRequest().body(response);
}
diff --git a/backend/src/main/java/com/rongyichuang/news/dto/NewsInput.java b/backend/src/main/java/com/rongyichuang/news/dto/NewsInput.java
new file mode 100644
index 0000000..0668c75
--- /dev/null
+++ b/backend/src/main/java/com/rongyichuang/news/dto/NewsInput.java
@@ -0,0 +1,77 @@
+package com.rongyichuang.news.dto;
+
+public class NewsInput {
+
+ private Long id;
+ private String title;
+ private String content;
+ private String summary;
+ private String coverImage;
+ private String author;
+ private Integer state = 1;
+
+ // 鏋勯�犲嚱鏁�
+ public NewsInput() {}
+
+ // Getters and Setters
+
+ public Long getId() {
+ return id;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public String getContent() {
+ return content;
+ }
+
+ public void setContent(String content) {
+ this.content = content;
+ }
+
+ public String getSummary() {
+ return summary;
+ }
+
+ public void setSummary(String summary) {
+ this.summary = summary;
+ }
+
+ public String getCoverImage() {
+ return coverImage;
+ }
+
+ public void setCoverImage(String coverImage) {
+ this.coverImage = coverImage;
+ }
+
+ public String getAuthor() {
+ return author;
+ }
+
+ public void setAuthor(String author) {
+ this.author = author;
+ }
+
+ public Integer getState() {
+ return state;
+ }
+
+ public void setState(Integer state) {
+ this.state = state;
+ }
+
+ public boolean isNew() {
+ return id == null || id <= 0;
+ }
+}
\ No newline at end of file
diff --git a/backend/src/main/java/com/rongyichuang/news/dto/NewsResponse.java b/backend/src/main/java/com/rongyichuang/news/dto/NewsResponse.java
new file mode 100644
index 0000000..91284f9
--- /dev/null
+++ b/backend/src/main/java/com/rongyichuang/news/dto/NewsResponse.java
@@ -0,0 +1,144 @@
+package com.rongyichuang.news.dto;
+
+import com.rongyichuang.news.entity.News;
+import com.rongyichuang.config.CosConfig;
+
+import java.time.LocalDateTime;
+
+public class NewsResponse {
+
+ private Long id;
+ private String title;
+ private String content;
+ private String summary;
+ private String coverImage;
+ private String author;
+ private Integer viewCount = 0;
+ private Integer state = 1;
+ private String stateName;
+ private LocalDateTime createTime;
+ private LocalDateTime updateTime;
+
+ // 鏋勯�犲嚱鏁�
+ public NewsResponse() {}
+
+ public NewsResponse(News news) {
+ this.id = news.getId();
+ this.title = news.getTitle();
+ this.content = news.getContent();
+ this.summary = news.getSummary();
+ this.coverImage = news.getCoverImage();
+ this.author = news.getAuthor();
+ this.viewCount = news.getViewCount();
+ this.state = news.getState();
+ this.createTime = news.getCreateTime();
+ this.updateTime = news.getUpdateTime();
+ this.stateName = getStateNameByValue(news.getState());
+ }
+
+ // 鐘舵�佸悕绉版槧灏�
+ private String getStateNameByValue(Integer state) {
+ if (state == null) return "鏈煡";
+ switch (state) {
+ case 0: return "鑽夌";
+ case 1: return "鍙戝竷";
+ case 2: return "鍏抽棴";
+ default: return "鏈煡";
+ }
+ }
+
+ // Getters and Setters
+
+ public Long getId() {
+ return id;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public String getContent() {
+ // 澶勭悊瀵屾枃鏈唴瀹癸紝纭繚鍥剧墖鍏锋湁鍝嶅簲寮忔牱寮�
+ if (content != null) {
+ // 涓烘墍鏈塱mg鏍囩娣诲姞鏍峰紡灞炴�э紝纭繚鍥剧墖涓嶄細瓒呭嚭瀹瑰櫒
+ return content.replaceAll("<img([^>]*?)>", "<img$1 style=\"max-width:100%;height:auto;display:block;margin:10px 0;\" />");
+ }
+ return content;
+ }
+
+ public void setContent(String content) {
+ this.content = content;
+ }
+
+ public String getSummary() {
+ return summary;
+ }
+
+ public void setSummary(String summary) {
+ this.summary = summary;
+ }
+
+ public String getCoverImage() {
+ return coverImage;
+ }
+
+ public void setCoverImage(String coverImage) {
+ this.coverImage = coverImage;
+ }
+
+ public String getAuthor() {
+ return author;
+ }
+
+ public void setAuthor(String author) {
+ this.author = author;
+ }
+
+ public Integer getViewCount() {
+ return viewCount;
+ }
+
+ public void setViewCount(Integer viewCount) {
+ this.viewCount = viewCount;
+ }
+
+ public Integer getState() {
+ return state;
+ }
+
+ public void setState(Integer state) {
+ this.state = state;
+ }
+
+ public String getStateName() {
+ return stateName;
+ }
+
+ public void setStateName(String stateName) {
+ this.stateName = stateName;
+ }
+
+ public LocalDateTime getCreateTime() {
+ return createTime;
+ }
+
+ public void setCreateTime(LocalDateTime createTime) {
+ this.createTime = createTime;
+ }
+
+ public LocalDateTime getUpdateTime() {
+ return updateTime;
+ }
+
+ public void setUpdateTime(LocalDateTime updateTime) {
+ this.updateTime = updateTime;
+ }
+}
\ No newline at end of file
diff --git a/backend/src/main/java/com/rongyichuang/news/entity/News.java b/backend/src/main/java/com/rongyichuang/news/entity/News.java
new file mode 100644
index 0000000..6c033b8
--- /dev/null
+++ b/backend/src/main/java/com/rongyichuang/news/entity/News.java
@@ -0,0 +1,102 @@
+package com.rongyichuang.news.entity;
+
+import com.rongyichuang.common.entity.BaseEntity;
+import jakarta.persistence.*;
+
+@Entity
+@Table(name = "t_news")
+public class News extends BaseEntity {
+
+ @Column(name = "title", nullable = false, length = 255)
+ private String title;
+
+ @Column(name = "content", columnDefinition = "LONGTEXT")
+ private String content;
+
+ @Column(name = "summary", length = 500)
+ private String summary;
+
+ @Column(name = "cover_image", length = 500)
+ private String coverImage;
+
+ @Column(name = "author", length = 100)
+ private String author;
+
+ @Column(name = "view_count", nullable = false)
+ private Integer viewCount = 0;
+
+ /**
+ * 鐘舵�侊細0-鑽夌锛�1-鍙戝竷锛�2-鍏抽棴
+ */
+ @Column(name = "state", nullable = false)
+ private Integer state = 1;
+
+ // 鏋勯�犲嚱鏁�
+ public News() {}
+
+ public News(String title, String content, String summary, String coverImage, String author) {
+ this.title = title;
+ this.content = content;
+ this.summary = summary;
+ this.coverImage = coverImage;
+ this.author = author;
+ }
+
+ // Getters and Setters
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public String getContent() {
+ return content;
+ }
+
+ public void setContent(String content) {
+ this.content = content;
+ }
+
+ public String getSummary() {
+ return summary;
+ }
+
+ public void setSummary(String summary) {
+ this.summary = summary;
+ }
+
+ public String getCoverImage() {
+ return coverImage;
+ }
+
+ public void setCoverImage(String coverImage) {
+ this.coverImage = coverImage;
+ }
+
+ public String getAuthor() {
+ return author;
+ }
+
+ public void setAuthor(String author) {
+ this.author = author;
+ }
+
+ public Integer getViewCount() {
+ return viewCount;
+ }
+
+ public void setViewCount(Integer viewCount) {
+ this.viewCount = viewCount;
+ }
+
+ public Integer getState() {
+ return state;
+ }
+
+ public void setState(Integer state) {
+ this.state = state;
+ }
+}
\ No newline at end of file
diff --git a/backend/src/main/java/com/rongyichuang/news/repository/NewsRepository.java b/backend/src/main/java/com/rongyichuang/news/repository/NewsRepository.java
new file mode 100644
index 0000000..94b4973
--- /dev/null
+++ b/backend/src/main/java/com/rongyichuang/news/repository/NewsRepository.java
@@ -0,0 +1,30 @@
+package com.rongyichuang.news.repository;
+
+import com.rongyichuang.news.entity.News;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
+
+import java.util.List;
+
+public interface NewsRepository extends JpaRepository<News, Long> {
+
+ Page<News> findByStateAndTitleContainingOrderByCreateTimeDesc(Integer state, String title, Pageable pageable);
+
+ Page<News> findByTitleContainingOrderByCreateTimeDesc(String title, Pageable pageable);
+
+ Page<News> findByStateOrderByCreateTimeDesc(Integer state, Pageable pageable);
+
+ List<News> findByStateOrderByCreateTimeDesc(Integer state);
+
+ @Query("SELECT n FROM News n WHERE n.state = 1 ORDER BY n.createTime DESC")
+ List<News> findPublishedNews();
+
+ @Query("SELECT n FROM News n WHERE n.state = 1 ORDER BY n.createTime DESC")
+ Page<News> findPublishedNews(Pageable pageable);
+
+ @Query("SELECT n FROM News n WHERE n.id = :id AND n.state = 1")
+ News findPublishedNewsById(@Param("id") Long id);
+}
\ No newline at end of file
diff --git a/backend/src/main/java/com/rongyichuang/news/resolver/NewsResolver.java b/backend/src/main/java/com/rongyichuang/news/resolver/NewsResolver.java
new file mode 100644
index 0000000..c98b99a
--- /dev/null
+++ b/backend/src/main/java/com/rongyichuang/news/resolver/NewsResolver.java
@@ -0,0 +1,77 @@
+package com.rongyichuang.news.resolver;
+
+import com.rongyichuang.news.dto.NewsInput;
+import com.rongyichuang.news.dto.NewsResponse;
+import com.rongyichuang.news.service.NewsService;
+import com.rongyichuang.common.dto.PageRequest;
+import com.rongyichuang.common.dto.PageResponse;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.graphql.data.method.annotation.Argument;
+import org.springframework.graphql.data.method.annotation.MutationMapping;
+import org.springframework.graphql.data.method.annotation.QueryMapping;
+import org.springframework.stereotype.Controller;
+
+@Controller
+public class NewsResolver {
+
+ @Autowired
+ private NewsService newsService;
+
+ /**
+ * 鍒嗛〉鏌ヨ鏂伴椈鍒楄〃锛堢鐞嗙锛�
+ */
+ @QueryMapping
+ public PageResponse<NewsResponse> newsList(@Argument int page, @Argument int size, @Argument String title, @Argument Integer state) {
+ PageRequest pageRequest = new PageRequest(page, size);
+ return newsService.findNews(pageRequest, title, state);
+ }
+
+ /**
+ * 鑾峰彇鏂伴椈璇︽儏锛堢鐞嗙锛�
+ */
+ @QueryMapping
+ public NewsResponse news(@Argument Long id) {
+ return newsService.findById(id);
+ }
+
+ /**
+ * 鑾峰彇宸插彂甯冪殑鏂伴椈璇︽儏锛堝墠绔睍绀猴級
+ */
+ @QueryMapping
+ public NewsResponse publishedNews(@Argument Long id) {
+ return newsService.findPublishedNewsById(id);
+ }
+
+ /**
+ * 鑾峰彇宸插彂甯冪殑鏂伴椈鍒楄〃锛堝墠绔睍绀猴級
+ */
+ @QueryMapping
+ public PageResponse<NewsResponse> publishedNewsList(@Argument int page, @Argument int size) {
+ PageRequest pageRequest = new PageRequest(page, size);
+ return newsService.findPublishedNews(pageRequest);
+ }
+
+ /**
+ * 淇濆瓨鏂伴椈
+ */
+ @MutationMapping
+ public NewsResponse saveNews(@Argument NewsInput input) {
+ return newsService.saveNews(input);
+ }
+
+ /**
+ * 鍒犻櫎鏂伴椈
+ */
+ @MutationMapping
+ public Boolean deleteNews(@Argument Long id) {
+ return newsService.deleteNews(id);
+ }
+
+ /**
+ * 鏇存柊鏂伴椈鐘舵��
+ */
+ @MutationMapping
+ public Boolean updateNewsState(@Argument Long id, @Argument Integer state) {
+ return newsService.updateNewsState(id, state);
+ }
+}
\ No newline at end of file
diff --git a/backend/src/main/java/com/rongyichuang/news/service/NewsService.java b/backend/src/main/java/com/rongyichuang/news/service/NewsService.java
new file mode 100644
index 0000000..c9dd1d1
--- /dev/null
+++ b/backend/src/main/java/com/rongyichuang/news/service/NewsService.java
@@ -0,0 +1,153 @@
+package com.rongyichuang.news.service;
+
+import com.rongyichuang.news.dto.NewsInput;
+import com.rongyichuang.news.dto.NewsResponse;
+import com.rongyichuang.news.entity.News;
+import com.rongyichuang.news.repository.NewsRepository;
+import com.rongyichuang.common.dto.PageRequest;
+import com.rongyichuang.common.dto.PageResponse;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.StringUtils;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+@Service
+@Transactional
+public class NewsService {
+
+ @Autowired
+ private NewsRepository newsRepository;
+
+ /**
+ * 鍒嗛〉鏌ヨ鏂伴椈鍒楄〃
+ */
+ public PageResponse<NewsResponse> findNews(PageRequest pageRequest, String title, Integer state) {
+ Pageable pageable = pageRequest.toPageable();
+ Page<News> page;
+
+ boolean hasTitle = StringUtils.hasText(title);
+ if (state != null) {
+ if (hasTitle) {
+ page = newsRepository.findByStateAndTitleContainingOrderByCreateTimeDesc(state, title, pageable);
+ } else {
+ page = newsRepository.findByStateOrderByCreateTimeDesc(state, pageable);
+ }
+ } else if (hasTitle) {
+ page = newsRepository.findByTitleContainingOrderByCreateTimeDesc(title, pageable);
+ } else {
+ page = newsRepository.findAll(pageable);
+ }
+
+ List<NewsResponse> content = page.getContent().stream()
+ .map(NewsResponse::new)
+ .collect(Collectors.toList());
+
+ return new PageResponse<>(content, page.getTotalElements(), page.getNumber(), page.getSize());
+ }
+
+ /**
+ * 鑾峰彇鏂伴椈璇︽儏
+ */
+ public NewsResponse findById(Long id) {
+ Optional<News> newsOpt = newsRepository.findById(id);
+ return newsOpt.map(NewsResponse::new).orElse(null);
+ }
+
+ /**
+ * 鑾峰彇宸插彂甯冪殑鏂伴椈璇︽儏锛堢敤浜庡墠绔睍绀猴級
+ */
+ public NewsResponse findPublishedNewsById(Long id) {
+ News news = newsRepository.findPublishedNewsById(id);
+ return news != null ? new NewsResponse(news) : null;
+ }
+
+ /**
+ * 淇濆瓨鏂伴椈锛堟柊澧炴垨缂栬緫锛�
+ */
+ public NewsResponse saveNews(NewsInput input) {
+ News news;
+
+ if (input.isNew()) {
+ // 鏂板鏂伴椈
+ news = new News();
+ } else {
+ // 缂栬緫鏂伴椈
+ Optional<News> existingOpt = newsRepository.findById(input.getId());
+ if (!existingOpt.isPresent()) {
+ throw new RuntimeException("鏂伴椈涓嶅瓨鍦�");
+ }
+ news = existingOpt.get();
+ }
+
+ // 璁剧疆鍩烘湰淇℃伅
+ news.setTitle(input.getTitle());
+ news.setContent(input.getContent());
+ news.setSummary(input.getSummary());
+ news.setCoverImage(input.getCoverImage());
+ news.setAuthor(input.getAuthor());
+ news.setState(input.getState());
+
+ // 淇濆瓨鏂伴椈
+ news = newsRepository.save(news);
+
+ return new NewsResponse(news);
+ }
+
+ /**
+ * 鍒犻櫎鏂伴椈锛堣蒋鍒犻櫎锛�
+ */
+ public boolean deleteNews(Long id) {
+ Optional<News> newsOpt = newsRepository.findById(id);
+ if (newsOpt.isPresent()) {
+ News news = newsOpt.get();
+ news.setState(2); // 璁剧疆涓哄叧闂姸鎬�
+ newsRepository.save(news);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * 鏇存柊鏂伴椈鐘舵��
+ */
+ public boolean updateNewsState(Long id, Integer state) {
+ Optional<News> newsOpt = newsRepository.findById(id);
+ if (newsOpt.isPresent()) {
+ News news = newsOpt.get();
+ news.setState(state);
+ newsRepository.save(news);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * 鑾峰彇宸插彂甯冪殑鏂伴椈鍒楄〃锛堢敤浜庡墠绔睍绀猴級
+ */
+ public List<NewsResponse> findPublishedNews() {
+ List<News> newsList = newsRepository.findPublishedNews();
+ return newsList.stream()
+ .map(NewsResponse::new)
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * 鑾峰彇宸插彂甯冪殑鏂伴椈鍒楄〃锛堝垎椤碉紝鐢ㄤ簬鍓嶇灞曠ず锛�
+ */
+ public PageResponse<NewsResponse> findPublishedNews(PageRequest pageRequest) {
+ Pageable pageable = pageRequest.toPageable();
+ Page<News> page = newsRepository.findPublishedNews(pageable);
+
+ List<NewsResponse> content = page.getContent().stream()
+ .map(NewsResponse::new)
+ .collect(Collectors.toList());
+
+ return new PageResponse<>(content, page.getTotalElements(), page.getNumber(), page.getSize());
+ }
+}
\ No newline at end of file
diff --git a/backend/src/main/resources/graphql/news.graphqls b/backend/src/main/resources/graphql/news.graphqls
new file mode 100644
index 0000000..cbd9118
--- /dev/null
+++ b/backend/src/main/resources/graphql/news.graphqls
@@ -0,0 +1,55 @@
+type News {
+ id: ID!
+ title: String!
+ content: String
+ summary: String
+ coverImage: String
+ author: String
+ viewCount: Int!
+ state: Int!
+ stateName: String
+ createTime: String
+ updateTime: String
+}
+
+input NewsInput {
+ id: ID
+ title: String!
+ content: String
+ summary: String
+ coverImage: String
+ author: String
+ state: Int = 1
+}
+
+type NewsPageResponse {
+ content: [News!]!
+ totalElements: Long!
+ page: Int!
+ size: Int!
+}
+
+extend type Query {
+ # 鍒嗛〉鏌ヨ鏂伴椈鍒楄〃锛堢鐞嗙锛�
+ newsList(page: Int!, size: Int!, title: String, state: Int): NewsPageResponse
+
+ # 鑾峰彇鏂伴椈璇︽儏锛堢鐞嗙锛�
+ news(id: ID!): News
+
+ # 鑾峰彇宸插彂甯冪殑鏂伴椈璇︽儏锛堝墠绔睍绀猴級
+ publishedNews(id: ID!): News
+
+ # 鑾峰彇宸插彂甯冪殑鏂伴椈鍒楄〃锛堝墠绔睍绀猴級
+ publishedNewsList(page: Int!, size: Int!): NewsPageResponse
+}
+
+extend type Mutation {
+ # 淇濆瓨鏂伴椈
+ saveNews(input: NewsInput!): News
+
+ # 鍒犻櫎鏂伴椈
+ deleteNews(id: ID!): Boolean
+
+ # 鏇存柊鏂伴椈鐘舵��
+ updateNewsState(id: ID!, state: Int!): Boolean
+}
\ No newline at end of file
diff --git a/news-module-readme.md b/news-module-readme.md
new file mode 100644
index 0000000..a2b247c
--- /dev/null
+++ b/news-module-readme.md
@@ -0,0 +1,97 @@
+# 鏂伴椈妯″潡璇存槑
+
+## 妯″潡姒傝堪
+鏂伴椈妯″潡鏄竴涓敤浜庣鐞嗙郴缁熸柊闂昏祫璁殑鍔熻兘妯″潡锛屾敮鎸佸瘜鏂囨湰鍐呭缂栬緫鍜屽彂甯冪鐞嗐��
+
+## 鍔熻兘鐗规��
+- 鏂伴椈鍒楄〃灞曠ず鍜岀鐞�
+- 瀵屾枃鏈唴瀹圭紪杈戯紙浣跨敤wangEditor锛�
+- 鏂伴椈鐘舵�佺鐞嗭紙鑽夌銆佸彂甯冦�佸叧闂級
+- 鍓嶇鏂伴椈灞曠ず椤甸潰
+- 鍝嶅簲寮忚璁�
+
+## 鏁版嵁搴撹〃缁撴瀯
+```sql
+CREATE TABLE `t_news` (
+ `id` bigint NOT NULL AUTO_INCREMENT,
+ `title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `content` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci,
+ `summary` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
+ `cover_image` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
+ `author` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
+ `view_count` int NOT NULL DEFAULT '0',
+ `state` int NOT NULL DEFAULT '1' COMMENT '0:鑽夌锛�1锛氬彂甯冿紝2锛氬叧闂�',
+ `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ `create_user_id` bigint DEFAULT NULL,
+ `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ `update_user_id` bigint DEFAULT NULL,
+ `version` bigint NOT NULL DEFAULT '0',
+ PRIMARY KEY (`id`) USING BTREE,
+ KEY `idx_t_news_state` (`state`) USING BTREE,
+ KEY `idx_t_news_create_time` (`create_time`) USING BTREE
+) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='鏂伴椈璧勮琛�';
+```
+
+## 鍚庣浠g爜缁撴瀯
+```
+com.rongyichuang.news
+鈹溾攢鈹� entity
+鈹� 鈹斺攢鈹� News.java
+鈹溾攢鈹� dto
+鈹� 鈹溾攢鈹� NewsInput.java
+鈹� 鈹斺攢鈹� NewsResponse.java
+鈹溾攢鈹� repository
+鈹� 鈹斺攢鈹� NewsRepository.java
+鈹溾攢鈹� service
+鈹� 鈹斺攢鈹� NewsService.java
+鈹斺攢鈹� resolver
+ 鈹斺攢鈹� NewsResolver.java
+```
+
+## 鍓嶇浠g爜缁撴瀯
+```
+src/views/
+鈹溾攢鈹� news-list.vue # 鍚庡彴鏂伴椈绠$悊鍒楄〃
+鈹溾攢鈹� NewsForm.vue # 鍚庡彴鏂伴椈缂栬緫琛ㄥ崟
+鈹溾攢鈹� NewsListPage.vue # 鍓嶅彴鏂伴椈鍒楄〃灞曠ず
+鈹斺攢鈹� NewsDetail.vue # 鍓嶅彴鏂伴椈璇︽儏灞曠ず
+
+src/api/
+鈹斺攢鈹� news.js # 鏂伴椈鐩稿叧API鎺ュ彛
+```
+
+## GraphQL API鎺ュ彛
+
+### 鏌ヨ鎺ュ彛
+1. `newsList` - 鑾峰彇鏂伴椈鍒楄〃锛堝悗鍙扮鐞嗭級
+2. `news` - 鑾峰彇鏂伴椈璇︽儏锛堝悗鍙扮鐞嗭級
+3. `publishedNews` - 鑾峰彇宸插彂甯冪殑鏂伴椈璇︽儏锛堝墠绔睍绀猴級
+4. `publishedNewsList` - 鑾峰彇宸插彂甯冪殑鏂伴椈鍒楄〃锛堝墠绔睍绀猴級
+
+### 鍙樻洿鎺ュ彛
+1. `saveNews` - 淇濆瓨鏂伴椈
+2. `deleteNews` - 鍒犻櫎鏂伴椈锛堣蒋鍒犻櫎锛�
+3. `updateNewsState` - 鏇存柊鏂伴椈鐘舵��
+
+## 璺敱閰嶇疆
+- `/news` - 鏂伴椈绠$悊鍒楄〃
+- `/news/new` - 鏂板鏂伴椈
+- `/news/edit/:id` - 缂栬緫鏂伴椈
+- `/news/list` - 鍓嶅彴鏂伴椈鍒楄〃
+- `/news/detail/:id` - 鍓嶅彴鏂伴椈璇︽儏
+
+## 渚濊禆瀹夎
+```bash
+npm install @wangeditor/editor @wangeditor/editor-for-vue --legacy-peer-deps
+```
+
+## 浣跨敤璇存槑
+1. 鍦ㄥ悗鍙扮鐞嗙郴缁熶腑锛岄�氳繃"鏂伴椈绠$悊"鑿滃崟杩涘叆鏂伴椈鍒楄〃椤甸潰
+2. 鍙互鏂板銆佺紪杈戙�佸垹闄ゆ柊闂�
+3. 璁剧疆鏂伴椈鐘舵�佷负"鍙戝竷"鍚庯紝鏂伴椈灏嗗湪鍓嶇灞曠ず椤甸潰鏄剧ず
+4. 鍓嶇鐢ㄦ埛鍙互閫氳繃鏂伴椈鍒楄〃椤甸潰鏌ョ湅宸插彂甯冪殑鏂伴椈
+
+## 娉ㄦ剰浜嬮」
+1. 闇�瑕丣ava 17鐜缂栬瘧鍚庣浠g爜
+2. 鍓嶇闇�瑕佸畨瑁厀angEditor瀵屾枃鏈紪杈戝櫒渚濊禆
+3. 鏁版嵁搴撻渶瑕佹墽琛宼_news琛ㄧ殑鍒涘缓璇彞
\ No newline at end of file
diff --git a/web/package.json b/web/package.json
index bbd2e19..ecffab0 100644
--- a/web/package.json
+++ b/web/package.json
@@ -13,6 +13,7 @@
"dependencies": {
"@element-plus/icons-vue": "^2.1.0",
"@urql/vue": "^2.0.0",
+ "@wangeditor/editor": "^5.1.23",
"axios": "^1.6.2",
"cos-js-sdk-v5": "^1.10.1",
"dayjs": "^1.11.10",
@@ -42,4 +43,4 @@
"vite": "^5.0.0",
"vue-tsc": "^1.8.22"
}
-}
+}
\ No newline at end of file
diff --git a/web/src/api/news.js b/web/src/api/news.js
new file mode 100644
index 0000000..3bb8c42
--- /dev/null
+++ b/web/src/api/news.js
@@ -0,0 +1,196 @@
+// 鏂伴椈绠$悊 API
+
+import { graphqlRequest } from '@/config/api'
+
+// GraphQL 鏌ヨ璇彞
+const GET_NEWS_LIST_QUERY = `
+ query GetNewsList($page: Int!, $size: Int!, $title: String, $state: Int) {
+ newsList(page: $page, size: $size, title: $title, state: $state) {
+ content {
+ id
+ title
+ summary
+ coverImage
+ author
+ viewCount
+ state
+ stateName
+ createTime
+ updateTime
+ }
+ totalElements
+ page
+ size
+ }
+ }
+`
+
+const GET_NEWS_QUERY = `
+ query GetNews($id: ID!) {
+ news(id: $id) {
+ id
+ title
+ content
+ summary
+ coverImage
+ author
+ viewCount
+ state
+ stateName
+ createTime
+ updateTime
+ }
+ }
+`
+
+const GET_PUBLISHED_NEWS_QUERY = `
+ query GetPublishedNews($id: ID!) {
+ publishedNews(id: $id) {
+ id
+ title
+ content
+ summary
+ coverImage
+ author
+ viewCount
+ state
+ stateName
+ createTime
+ updateTime
+ }
+ }
+`
+
+const GET_PUBLISHED_NEWS_LIST_QUERY = `
+ query GetPublishedNewsList($page: Int!, $size: Int!) {
+ publishedNewsList(page: $page, size: $size) {
+ content {
+ id
+ title
+ summary
+ coverImage
+ author
+ viewCount
+ state
+ stateName
+ createTime
+ updateTime
+ }
+ totalElements
+ page
+ size
+ }
+ }
+`
+
+const SAVE_NEWS_MUTATION = `
+ mutation SaveNews($input: NewsInput!) {
+ saveNews(input: $input) {
+ id
+ title
+ content
+ summary
+ coverImage
+ author
+ viewCount
+ state
+ stateName
+ createTime
+ updateTime
+ }
+ }
+`
+
+const DELETE_NEWS_MUTATION = `
+ mutation DeleteNews($id: ID!) {
+ deleteNews(id: $id)
+ }
+`
+
+const UPDATE_NEWS_STATE_MUTATION = `
+ mutation UpdateNewsState($id: ID!, $state: Int!) {
+ updateNewsState(id: $id, state: $state)
+ }
+`
+
+// API 鍑芥暟
+export const getNewsList = async (page = 0, size = 10, title = '', state) => {
+ try {
+ const variables = {
+ page,
+ size,
+ title
+ }
+
+ if (state !== undefined && state !== null && state !== '') {
+ variables.state = Number(state)
+ }
+
+ const result = await graphqlRequest(GET_NEWS_LIST_QUERY, variables)
+ return result.data.newsList
+ } catch (error) {
+ console.error('鑾峰彇鏂伴椈鍒楄〃澶辫触:', error)
+ throw new Error(error && error.message ? error.message : '鑾峰彇鏂伴椈鍒楄〃澶辫触')
+ }
+}
+
+export const getNews = async (id) => {
+ try {
+ const result = await graphqlRequest(GET_NEWS_QUERY, { id })
+ return result.data.news
+ } catch (error) {
+ throw new Error(error && error.message ? error.message : '鑾峰彇鏂伴椈璇︽儏澶辫触')
+ }
+}
+
+export const getPublishedNews = async (id) => {
+ try {
+ const result = await graphqlRequest(GET_PUBLISHED_NEWS_QUERY, { id })
+ return result.data.publishedNews
+ } catch (error) {
+ throw new Error(error && error.message ? error.message : '鑾峰彇鏂伴椈璇︽儏澶辫触')
+ }
+}
+
+export const getPublishedNewsList = async (page = 0, size = 10) => {
+ try {
+ const variables = {
+ page,
+ size
+ }
+
+ const result = await graphqlRequest(GET_PUBLISHED_NEWS_LIST_QUERY, variables)
+ return result.data.publishedNewsList
+ } catch (error) {
+ console.error('鑾峰彇鏂伴椈鍒楄〃澶辫触:', error)
+ throw new Error(error && error.message ? error.message : '鑾峰彇鏂伴椈鍒楄〃澶辫触')
+ }
+}
+
+export const saveNews = async (newsData) => {
+ try {
+ const data = await graphqlRequest(SAVE_NEWS_MUTATION, { input: newsData })
+ return data.data.saveNews
+ } catch (error) {
+ console.error('淇濆瓨鏂伴椈澶辫触:', error)
+ throw new Error(error && error.message ? error.message : '淇濆瓨鏂伴椈澶辫触')
+ }
+}
+
+export const deleteNews = async (id) => {
+ try {
+ const data = await graphqlRequest(DELETE_NEWS_MUTATION, { id })
+ return data.data.deleteNews
+ } catch (error) {
+ throw new Error(error && error.message ? error.message : '鍒犻櫎鏂伴椈澶辫触')
+ }
+}
+
+export const updateNewsState = async (id, state) => {
+ try {
+ const data = await graphqlRequest(UPDATE_NEWS_STATE_MUTATION, { id, state })
+ return data.data.updateNewsState
+ } catch (error) {
+ throw new Error(error && error.message ? error.message : '鏇存柊鏂伴椈鐘舵�佸け璐�')
+ }
+}
\ No newline at end of file
diff --git a/web/src/layout/index.vue b/web/src/layout/index.vue
index 9fdfd8d..068ac33 100644
--- a/web/src/layout/index.vue
+++ b/web/src/layout/index.vue
@@ -34,7 +34,6 @@
text-color="#666666"
active-text-color="#1E3A8A"
>
- >
<el-menu-item index="/dashboard">
<el-icon><House /></el-icon>
<span>宸ヤ綔鍙�</span>
@@ -64,9 +63,13 @@
<el-icon><TrendCharts /></el-icon>
<span>姣旇禌鏅嬬骇</span>
</el-menu-item>
+ <el-menu-item index="/news">
+ <el-icon><Document /></el-icon>
+ <span>鏂伴椈绠$悊</span>
+ </el-menu-item>
<el-menu-item index="/carousel">
<el-icon><Picture /></el-icon>
- <span>鏂伴椈涓庢帹骞�</span>
+ <span>Banner</span>
</el-menu-item>
<el-menu-item index="/region">
<el-icon><Location /></el-icon>
diff --git a/web/src/router/index.ts b/web/src/router/index.ts
index 3aa9c0a..01a3721 100644
--- a/web/src/router/index.ts
+++ b/web/src/router/index.ts
@@ -81,6 +81,36 @@
meta: { title: '璇勫垎璇︽儏', icon: 'Edit' }
},
{
+ path: '/news',
+ name: 'News',
+ component: () => import('@/views/news-list.vue'),
+ meta: { title: '鏂伴椈绠$悊', icon: 'Document' }
+ },
+ {
+ path: '/news/new',
+ name: 'NewsCreate',
+ component: () => import('@/views/NewsForm.vue'),
+ meta: { title: '鏂板鏂伴椈', hidden: true }
+ },
+ {
+ path: '/news/edit/:id',
+ name: 'NewsEdit',
+ component: () => import('@/views/NewsForm.vue'),
+ meta: { title: '缂栬緫鏂伴椈', hidden: true }
+ },
+ {
+ path: '/news/list',
+ name: 'NewsListPage',
+ component: () => import('@/views/NewsListPage.vue'),
+ meta: { title: '鏂伴椈鍒楄〃', hidden: true }
+ },
+ {
+ path: '/news/detail/:id',
+ name: 'NewsDetail',
+ component: () => import('@/views/NewsDetail.vue'),
+ meta: { title: '鏂伴椈璇︽儏', hidden: true }
+ },
+ {
path: '/carousel',
name: 'Carousel',
component: () => import('@/views/carousel/index.vue'),
diff --git a/web/src/views/NewsDetail.vue b/web/src/views/NewsDetail.vue
new file mode 100644
index 0000000..588afaa
--- /dev/null
+++ b/web/src/views/NewsDetail.vue
@@ -0,0 +1,182 @@
+<template>
+ <div class="news-detail">
+ <el-card v-loading="loading">
+ <template #header>
+ <div class="news-header">
+ <h1 class="news-title">{{ news.title }}</h1>
+ <div class="news-meta">
+ <span class="author">浣滆�咃細{{ news.author }}</span>
+ <span class="time">鍙戝竷鏃堕棿锛歿{ formatDate(news.createTime) }}</span>
+ <span class="views">娴忚閲忥細{{ news.viewCount }}</span>
+ </div>
+ </div>
+ </template>
+
+ <!-- 灏侀潰鍥剧墖 -->
+ <div v-if="news.coverImage" class="cover-image-container">
+ <el-image
+ :src="news.coverImage"
+ class="cover-image"
+ fit="cover"
+ :preview-src-list="[news.coverImage]"
+ preview-teleported
+ />
+ </div>
+
+ <div class="news-content" v-html="news.content"></div>
+
+ <div class="news-footer">
+ <el-button @click="goBack">杩斿洖鍒楄〃</el-button>
+ </div>
+ </el-card>
+ </div>
+</template>
+
+<script setup>
+import { ref, onMounted } from 'vue'
+import { useRoute, useRouter } from 'vue-router'
+import { ElMessage } from 'element-plus'
+import { getPublishedNews } from '@/api/news'
+
+const route = useRoute()
+const router = useRouter()
+
+const loading = ref(false)
+const news = ref({
+ id: null,
+ title: '',
+ content: '',
+ summary: '',
+ coverImage: '',
+ author: '',
+ viewCount: 0,
+ state: 1,
+ createTime: '',
+ updateTime: ''
+})
+
+// 鏍煎紡鍖栨棩鏈�
+const formatDate = (dateString) => {
+ if (!dateString) return ''
+ const date = new Date(dateString)
+ return date.toLocaleString('zh-CN', {
+ year: 'numeric',
+ month: '2-digit',
+ day: '2-digit',
+ hour: '2-digit',
+ minute: '2-digit'
+ })
+}
+
+// 鍔犺浇鏂伴椈鏁版嵁
+const loadNews = async () => {
+ try {
+ loading.value = true
+ const data = await getPublishedNews(route.params.id)
+
+ if (data) {
+ news.value = data
+ } else {
+ ElMessage.error('鏂伴椈涓嶅瓨鍦ㄦ垨宸蹭笅鏋�')
+ router.push('/news/list')
+ }
+ } catch (error) {
+ console.error('鍔犺浇鏂伴椈澶辫触:', error)
+ ElMessage.error('鍔犺浇鏂伴椈澶辫触')
+ router.push('/news/list')
+ } finally {
+ loading.value = false
+ }
+}
+
+// 杩斿洖
+const goBack = () => {
+ router.go(-1)
+}
+
+onMounted(() => {
+ loadNews()
+})
+</script>
+
+<style scoped>
+.news-detail {
+ padding: 20px;
+ max-width: 800px;
+ margin: 0 auto;
+}
+
+.news-header {
+ text-align: center;
+}
+
+.news-title {
+ font-size: 24px;
+ font-weight: 600;
+ color: #333;
+ margin-bottom: 16px;
+}
+
+.news-meta {
+ display: flex;
+ justify-content: center;
+ gap: 20px;
+ font-size: 14px;
+ color: #666;
+ margin-bottom: 20px;
+}
+
+/* 灏侀潰鍥剧墖鏍峰紡 */
+.cover-image-container {
+ text-align: center;
+ margin-bottom: 20px;
+}
+
+.cover-image {
+ max-width: 100%;
+ max-height: 400px;
+ border-radius: 8px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+}
+
+.news-content {
+ line-height: 1.8;
+ font-size: 16px;
+ color: #333;
+}
+
+.news-content :deep(img) {
+ max-width: 100%;
+ height: auto;
+ display: block;
+ margin: 10px auto;
+}
+
+.news-content :deep(p) {
+ margin: 10px 0;
+}
+
+.news-footer {
+ margin-top: 30px;
+ text-align: center;
+}
+
+@media (max-width: 768px) {
+ .news-detail {
+ padding: 10px;
+ }
+
+ .news-meta {
+ flex-direction: column;
+ gap: 5px;
+ }
+
+ .news-title {
+ font-size: 20px;
+ }
+
+ .cover-image {
+ max-height: 200px;
+ }
+}
+</style>
\ No newline at end of file
diff --git a/web/src/views/NewsForm.vue b/web/src/views/NewsForm.vue
new file mode 100644
index 0000000..5adba44
--- /dev/null
+++ b/web/src/views/NewsForm.vue
@@ -0,0 +1,408 @@
+<template>
+ <div class="news-form">
+ <el-card>
+ <template #header>
+ <div class="card-header">
+ <span>{{ isEdit ? '缂栬緫鏂伴椈' : '鏂板鏂伴椈' }}</span>
+ <el-button @click="goBack">杩斿洖</el-button>
+ </div>
+ </template>
+
+ <el-form
+ ref="formRef"
+ :model="form"
+ :rules="rules"
+ label-width="120px"
+ v-loading="loading"
+ @submit.prevent
+ >
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鏂伴椈鏍囬" prop="title">
+ <el-input v-model="form.title" placeholder="璇疯緭鍏ユ柊闂绘爣棰�" maxlength="100" />
+ </el-form-item>
+ </el-col>
+
+ <el-col :span="12">
+ <el-form-item label="浣滆��" prop="author">
+ <el-input v-model="form.author" placeholder="璇疯緭鍏ヤ綔鑰呭鍚�" maxlength="50" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-form-item label="鎽樿" prop="summary">
+ <el-input
+ v-model="form.summary"
+ type="textarea"
+ :rows="3"
+ placeholder="璇疯緭鍏ユ柊闂绘憳瑕�"
+ maxlength="500"
+ />
+ </el-form-item>
+
+ <el-form-item label="灏侀潰鍥剧墖" prop="coverImage">
+ <div class="cover-upload-container">
+ <el-upload
+ class="cover-uploader"
+ :action="uploadUrl"
+ :headers="uploadHeaders"
+ :show-file-list="false"
+ :on-success="handleCoverUploadSuccess"
+ :before-upload="beforeCoverUpload"
+ >
+ <img v-if="form.coverImage" :src="form.coverImage" class="cover" />
+ <div v-else class="cover-uploader-placeholder">
+ <i class="el-icon-plus cover-uploader-icon"></i>
+ <div class="cover-uploader-text">鐐瑰嚮涓婁紶鍥剧墖</div>
+ </div>
+ </el-upload>
+ </div>
+ </el-form-item>
+
+ <el-form-item label="鏂伴椈鍐呭" prop="content">
+ <!-- 娣诲姞宸ュ叿鏍忓鍣� -->
+ <div class="editor-container">
+ <div ref="toolbarRef" class="editor-toolbar"></div>
+ <div ref="editorRef" class="editor-content"></div>
+ </div>
+ </el-form-item>
+
+ <el-form-item label="鐘舵��" prop="state">
+ <el-radio-group v-model="form.state">
+ <el-radio :label="0">鑽夌</el-radio>
+ <el-radio :label="1">鍙戝竷</el-radio>
+ <el-radio :label="2">鍏抽棴</el-radio>
+ </el-radio-group>
+ </el-form-item>
+
+ <el-form-item>
+ <el-button type="primary" @click="handleSubmit" :loading="submitting">
+ {{ isEdit ? '鏇存柊' : '鍒涘缓' }}
+ </el-button>
+ <el-button @click="goBack">鍙栨秷</el-button>
+ </el-form-item>
+ </el-form>
+ </el-card>
+ </div>
+</template>
+
+<script setup>
+import { ref, onMounted, onBeforeUnmount, shallowRef } from 'vue'
+import { useRouter, useRoute } from 'vue-router'
+import { ElMessage } from 'element-plus'
+import { getNews, saveNews } from '@/api/news'
+import { getToken } from '@/utils/auth'
+// 浣跨敤wangEditor鐨勬牳蹇冨寘鑰屼笉鏄疺ue鍖呰鍣�
+import { createEditor, createToolbar } from '@wangeditor/editor'
+import '@wangeditor/editor/dist/css/style.css'
+
+const router = useRouter()
+const route = useRoute()
+
+// 缂栬緫鍣ㄥ疄渚�
+const editorRef = ref()
+const toolbarRef = ref()
+const editorInstance = shallowRef()
+const toolbarInstance = shallowRef()
+
+const loading = ref(false)
+const submitting = ref(false)
+const formRef = ref()
+
+// 涓婁紶閰嶇疆 - 淇URL璺緞锛屾坊鍔�/api鍓嶇紑
+const uploadUrl = `${import.meta.env.VITE_API_BASE_URL || ''}/api/upload/image`
+const uploadHeaders = {
+ Authorization: `Bearer ${getToken()}`
+}
+
+// 琛ㄥ崟鏁版嵁
+const form = ref({
+ id: null,
+ title: '',
+ content: '',
+ summary: '',
+ coverImage: '',
+ author: '',
+ state: 1
+})
+
+// 琛ㄥ崟楠岃瘉瑙勫垯
+const rules = {
+ title: [
+ { required: true, message: '璇疯緭鍏ユ柊闂绘爣棰�', trigger: 'blur' },
+ { max: 100, message: '鏂伴椈鏍囬涓嶈兘瓒呰繃100涓瓧绗�', trigger: 'blur' }
+ ],
+ author: [
+ { required: true, message: '璇疯緭鍏ヤ綔鑰呭鍚�', trigger: 'blur' },
+ { max: 50, message: '浣滆�呭鍚嶄笉鑳借秴杩�50涓瓧绗�', trigger: 'blur' }
+ ],
+ content: [
+ { required: true, message: '璇疯緭鍏ユ柊闂诲唴瀹�', trigger: 'blur' }
+ ]
+}
+
+// 璁$畻灞炴��
+const isEdit = ref(!!route.params.id)
+
+// 鍒濆鍖栫紪杈戝櫒
+const initEditor = () => {
+ if (!editorRef.value || !toolbarRef.value) return
+
+ // 鍒涘缓缂栬緫鍣�
+ const editor = createEditor({
+ selector: editorRef.value,
+ html: form.value.content,
+ config: {
+ placeholder: '璇疯緭鍏ユ柊闂诲唴瀹�...',
+ MENU_CONF: {
+ 'uploadImage': {
+ server: `${import.meta.env.VITE_API_BASE_URL || ''}/api/upload/image`,
+ fieldName: 'file',
+ headers: {
+ Authorization: `Bearer ${getToken()}`
+ },
+ maxFileSize: 2 * 1024 * 1024, // 2MB
+ base64LimitSize: 5 * 1024, // 5KB浠ヤ笅鎻掑叆base64
+ // wangEditor鏈熸湜鐨勫搷搴旀牸寮忓鐞�
+ onSuccess(file, res) {
+ console.log('鍥剧墖涓婁紶鎴愬姛', file, res)
+ },
+ onError(file, res) {
+ console.error('鍥剧墖涓婁紶澶辫触', file, res)
+ ElMessage.error('鍥剧墖涓婁紶澶辫触: ' + (res?.message || res?.data?.message || '鏈煡閿欒'))
+ }
+ }
+ },
+ onChange(editor) {
+ // 褰撶紪杈戝櫒鍐呭鏀瑰彉鏃讹紝鏇存柊琛ㄥ崟鏁版嵁
+ form.value.content = editor.getHtml()
+ }
+ }
+ })
+
+ // 鍒涘缓宸ュ叿鏍�
+ const toolbar = createToolbar({
+ editor: editor,
+ selector: toolbarRef.value,
+ config: {
+ // 宸ュ叿鏍忛厤缃�
+ }
+ })
+
+ editorInstance.value = editor
+ toolbarInstance.value = toolbar
+}
+
+// 灏侀潰鍥剧墖涓婁紶鍓嶆鏌�
+const beforeCoverUpload = (file) => {
+ const isJPG = file.type === 'image/jpeg' || file.type === 'image/png'
+ const isLt2M = file.size / 1024 / 1024 < 2
+
+ if (!isJPG) {
+ ElMessage.error('灏侀潰鍥剧墖鍙兘鏄� JPG 鎴� PNG 鏍煎紡!')
+ }
+ if (!isLt2M) {
+ ElMessage.error('灏侀潰鍥剧墖澶у皬涓嶈兘瓒呰繃 2MB!')
+ }
+ return isJPG && isLt2M
+}
+
+// 灏侀潰鍥剧墖涓婁紶鎴愬姛澶勭悊
+const handleCoverUploadSuccess = (response) => {
+ // 澶勭悊鍚庣杩斿洖鐨勬柊鏍煎紡
+ if (response && response.errno === 0 && response.data) {
+ form.value.coverImage = response.data.url
+ ElMessage.success('灏侀潰鍥剧墖涓婁紶鎴愬姛')
+ } else if (response && response.success) {
+ // 鍏煎鏃ф牸寮�
+ form.value.coverImage = response.data?.fullUrl || response.data?.url || response.url
+ ElMessage.success('灏侀潰鍥剧墖涓婁紶鎴愬姛')
+ } else {
+ ElMessage.error('灏侀潰鍥剧墖涓婁紶澶辫触: ' + (response?.message || '鏈煡閿欒'))
+ }
+}
+
+// 鍔犺浇鏂伴椈鏁版嵁锛堢紪杈戞ā寮忥級
+const loadNews = async () => {
+ if (!isEdit.value) return
+
+ try {
+ loading.value = true
+ const news = await getNews(route.params.id)
+
+ if (news) {
+ form.value = {
+ id: news.id,
+ title: news.title,
+ content: news.content,
+ summary: news.summary,
+ coverImage: news.coverImage,
+ author: news.author,
+ state: news.state
+ }
+
+ // 鏇存柊缂栬緫鍣ㄥ唴瀹�
+ if (editorInstance.value) {
+ editorInstance.value.setHtml(news.content)
+ }
+ }
+ } catch (error) {
+ console.error('鍔犺浇鏂伴椈鏁版嵁澶辫触:', error)
+ ElMessage.error('鍔犺浇鏂伴椈鏁版嵁澶辫触')
+ } finally {
+ loading.value = false
+ }
+}
+
+// 鎻愪氦琛ㄥ崟
+const handleSubmit = async () => {
+ if (submitting.value) return
+
+ try {
+ await formRef.value.validate()
+
+ submitting.value = true
+
+ // 鍑嗗淇濆瓨鏁版嵁
+ const saveData = {
+ title: form.value.title,
+ content: form.value.content,
+ summary: form.value.summary,
+ coverImage: form.value.coverImage,
+ author: form.value.author,
+ state: form.value.state
+ }
+
+ // 濡傛灉鏄紪杈戞ā寮忥紝娣诲姞id瀛楁
+ if (isEdit.value && form.value.id) {
+ saveData.id = form.value.id
+ }
+
+ const result = await saveNews(saveData)
+
+ ElMessage.success(isEdit.value ? '鏇存柊鎴愬姛' : '鍒涘缓鎴愬姛')
+
+ // 淇濆瓨鎴愬姛鍚庤繑鍥炲垪琛ㄩ〉闈�
+ goBack()
+ } catch (error) {
+ console.error('淇濆瓨鏂伴椈澶辫触:', error)
+ if (error.message) {
+ ElMessage.error('淇濆瓨澶辫触: ' + error.message)
+ } else {
+ ElMessage.error('淇濆瓨澶辫触: 鏈煡閿欒')
+ }
+ } finally {
+ submitting.value = false
+ }
+}
+
+// 杩斿洖
+const goBack = () => {
+ router.push('/news')
+}
+
+// 缁勪欢閿�姣佸墠娓呯悊缂栬緫鍣�
+onBeforeUnmount(() => {
+ if (editorInstance.value) {
+ editorInstance.value.destroy()
+ }
+})
+
+// 鐢熷懡鍛ㄦ湡
+onMounted(async () => {
+ await loadNews()
+ // 纭繚DOM宸叉覆鏌撳悗鍐嶅垵濮嬪寲缂栬緫鍣�
+ setTimeout(() => {
+ initEditor()
+ }, 100)
+})
+</script>
+
+<style scoped>
+.news-form {
+ padding: 20px;
+}
+
+.card-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+/* 灏侀潰鍥剧墖涓婁紶瀹瑰櫒 */
+.cover-upload-container {
+ display: inline-block;
+}
+
+.cover-uploader .el-upload {
+ border: 2px dashed #d9d9d9;
+ border-radius: 6px;
+ cursor: pointer;
+ position: relative;
+ overflow: hidden;
+ transition: .2s;
+ width: 178px;
+ height: 178px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background-color: #fafafa;
+}
+
+.cover-uploader .el-upload:hover {
+ border-color: #409eff;
+ background-color: #f5f9ff;
+}
+
+.cover-uploader-placeholder {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ width: 100%;
+ height: 100%;
+}
+
+.cover-uploader-icon {
+ font-size: 28px;
+ color: #8c939d;
+ margin-bottom: 8px;
+}
+
+.cover-uploader-text {
+ font-size: 12px;
+ color: #666;
+ text-align: center;
+}
+
+.cover {
+ width: 178px;
+ height: 178px;
+ display: block;
+ object-fit: cover;
+}
+
+/* 缂栬緫鍣ㄥ鍣ㄦ牱寮� */
+.editor-container {
+ border: 1px solid #dcdfe6;
+ border-radius: 4px;
+}
+
+.editor-toolbar {
+ border-bottom: 1px solid #dcdfe6;
+ background-color: #f5f7fa;
+}
+
+.editor-content {
+ height: 400px;
+ overflow-y: auto;
+ background-color: #fff;
+}
+
+/* 鍝嶅簲寮忚璁� */
+@media (max-width: 768px) {
+ .editor-content {
+ height: 300px;
+ }
+}
+</style>
\ No newline at end of file
diff --git a/web/src/views/NewsListPage.vue b/web/src/views/NewsListPage.vue
new file mode 100644
index 0000000..ca75ea6
--- /dev/null
+++ b/web/src/views/NewsListPage.vue
@@ -0,0 +1,277 @@
+<template>
+ <div class="news-list-page">
+ <div class="page-header">
+ <h1 class="page-title">鏂伴椈璧勮</h1>
+ <p class="page-description">浜嗚В鏈�鏂扮殑娲诲姩鍔ㄦ�佸拰琛屼笟璧勮</p>
+ </div>
+
+ <div class="news-list" v-loading="loading">
+ <div v-if="newsList.length === 0" class="empty-state">
+ <el-empty description="鏆傛棤鏂伴椈璧勮" />
+ </div>
+
+ <div v-else class="news-items">
+ <div
+ v-for="news in newsList"
+ :key="news.id"
+ class="news-item"
+ @click="goToDetail(news.id)"
+ >
+ <div class="news-item-content">
+ <div class="news-item-header">
+ <h3 class="news-item-title">{{ news.title }}</h3>
+ <div class="news-item-meta">
+ <span class="news-item-author">{{ news.author }}</span>
+ <span class="news-item-date">{{ formatDate(news.createTime) }}</span>
+ <span class="news-item-views">娴忚 {{ news.viewCount }}</span>
+ </div>
+ </div>
+
+ <div class="news-item-body">
+ <div v-if="news.coverImage" class="news-item-image">
+ <el-image
+ :src="news.coverImage"
+ class="cover-image"
+ fit="cover"
+ lazy
+ />
+ </div>
+
+ <p class="news-item-summary" v-if="news.summary">
+ {{ news.summary }}
+ </p>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <!-- 鍒嗛〉 -->
+ <div class="pagination" v-if="pagination.total > 0">
+ <el-pagination
+ v-model:current-page="pagination.page"
+ v-model:page-size="pagination.size"
+ :page-sizes="[10, 20, 50]"
+ :total="pagination.total"
+ layout="total, sizes, prev, pager, next, jumper"
+ @size-change="handleSizeChange"
+ @current-change="handleCurrentChange"
+ />
+ </div>
+ </div>
+</template>
+
+<script setup>
+import { ref, onMounted } from 'vue'
+import { useRouter } from 'vue-router'
+import { getPublishedNewsList } from '@/api/news'
+
+const router = useRouter()
+const loading = ref(false)
+
+// 鍒嗛〉淇℃伅
+const pagination = ref({
+ page: 1,
+ size: 10,
+ total: 0
+})
+
+// 鏂伴椈鍒楄〃
+const newsList = ref([])
+
+// 鏍煎紡鍖栨棩鏈�
+const formatDate = (dateString) => {
+ if (!dateString) return ''
+ const date = new Date(dateString)
+ return date.toLocaleDateString('zh-CN', {
+ year: 'numeric',
+ month: '2-digit',
+ day: '2-digit'
+ })
+}
+
+// 鍔犺浇鏂伴椈鍒楄〃
+const loadNewsList = async () => {
+ try {
+ loading.value = true
+ const data = await getPublishedNewsList(
+ pagination.value.page - 1,
+ pagination.value.size
+ )
+
+ newsList.value = data?.content || []
+ pagination.value.total = data?.totalElements || 0
+ } catch (error) {
+ console.error('鍔犺浇鏂伴椈鍒楄〃澶辫触:', error)
+ } finally {
+ loading.value = false
+ }
+}
+
+// 鍒嗛〉澶у皬鏀瑰彉
+const handleSizeChange = (size) => {
+ pagination.value.size = size
+ loadNewsList()
+}
+
+// 褰撳墠椤垫敼鍙�
+const handleCurrentChange = (page) => {
+ pagination.value.page = page
+ loadNewsList()
+}
+
+// 璺宠浆鍒拌鎯呴〉
+const goToDetail = (id) => {
+ router.push(`/news/detail/${id}`)
+}
+
+onMounted(() => {
+ loadNewsList()
+})
+</script>
+
+<style scoped>
+.news-list-page {
+ padding: 20px;
+ max-width: 1200px;
+ margin: 0 auto;
+}
+
+.page-header {
+ text-align: center;
+ margin-bottom: 30px;
+}
+
+.page-title {
+ font-size: 28px;
+ font-weight: 600;
+ color: #333;
+ margin-bottom: 10px;
+}
+
+.page-description {
+ font-size: 16px;
+ color: #666;
+ margin: 0;
+}
+
+.news-list {
+ min-height: 400px;
+}
+
+.empty-state {
+ padding: 40px 0;
+}
+
+.news-items {
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+}
+
+.news-item {
+ background: white;
+ border-radius: 8px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+ overflow: hidden;
+ transition: all 0.3s ease;
+ cursor: pointer;
+}
+
+.news-item:hover {
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
+ transform: translateY(-2px);
+}
+
+.news-item-content {
+ padding: 20px;
+}
+
+.news-item-header {
+ margin-bottom: 15px;
+}
+
+.news-item-title {
+ font-size: 20px;
+ font-weight: 600;
+ color: #333;
+ margin: 0 0 10px 0;
+ line-height: 1.4;
+}
+
+.news-item-meta {
+ display: flex;
+ gap: 20px;
+ font-size: 14px;
+ color: #666;
+}
+
+.news-item-body {
+ display: flex;
+ gap: 20px;
+}
+
+.news-item-image {
+ flex: 0 0 200px;
+}
+
+.cover-image {
+ width: 100%;
+ height: 120px;
+ border-radius: 4px;
+}
+
+.news-item-summary {
+ flex: 1;
+ font-size: 15px;
+ color: #555;
+ line-height: 1.6;
+ margin: 0;
+ display: -webkit-box;
+ -webkit-line-clamp: 3;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+}
+
+.pagination {
+ margin-top: 30px;
+ display: flex;
+ justify-content: center;
+}
+
+@media (max-width: 768px) {
+ .news-list-page {
+ padding: 10px;
+ }
+
+ .page-title {
+ font-size: 24px;
+ }
+
+ .news-item-content {
+ padding: 15px;
+ }
+
+ .news-item-title {
+ font-size: 18px;
+ }
+
+ .news-item-meta {
+ flex-direction: column;
+ gap: 5px;
+ }
+
+ .news-item-body {
+ flex-direction: column;
+ gap: 15px;
+ }
+
+ .news-item-image {
+ flex: none;
+ }
+
+ .cover-image {
+ height: 150px;
+ }
+}
+</style>
\ No newline at end of file
diff --git a/web/src/views/news-list.vue b/web/src/views/news-list.vue
new file mode 100644
index 0000000..92af6ae
--- /dev/null
+++ b/web/src/views/news-list.vue
@@ -0,0 +1,359 @@
+<template>
+ <div class="news-page">
+ <div class="page-card">
+ <!-- 椤甸潰澶撮儴 -->
+ <div class="page-header">
+ <div class="title-section">
+ <h1 class="page-title">鏂伴椈璧勮</h1>
+ <p class="page-subtitle">绠$悊绯荤粺鏂伴椈璧勮鍐呭</p>
+ </div>
+ </div>
+
+ <!-- 鎼滅储宸ュ叿鏍� -->
+ <div class="search-toolbar">
+ <el-input
+ v-model="searchForm.title"
+ placeholder="璇疯緭鍏ユ柊闂绘爣棰�"
+ style="width: 200px"
+ clearable
+ @keyup.enter="handleSearch"
+ @clear="handleClear"
+ />
+ <el-select
+ v-model="searchForm.state"
+ placeholder="鏂伴椈鐘舵��"
+ style="width: 160px"
+ clearable
+ @change="handleStateChange"
+ >
+ <el-option
+ v-for="option in stateOptions"
+ :key="option.value"
+ :label="option.label"
+ :value="option.value"
+ />
+ </el-select>
+ <el-button type="primary" @click="handleSearch">
+ <el-icon><Search /></el-icon>
+ 鏌ヨ
+ </el-button>
+ <el-button type="primary" @click="handleAdd">
+ <el-icon><Plus /></el-icon>
+ 鏂板鏂伴椈
+ </el-button>
+ </div>
+
+ <!-- 鏂伴椈鍒楄〃 -->
+ <el-table :data="tableData" style="width: 100%" v-loading="loading">
+ <el-table-column prop="title" label="鏂伴椈鏍囬" min-width="200" />
+ <el-table-column label="灏侀潰鍥剧墖" width="120" align="center">
+ <template #default="{ row }">
+ <el-image
+ v-if="row.coverImage"
+ :src="row.coverImage"
+ class="cover-image"
+ fit="cover"
+ :preview-src-list="[row.coverImage]"
+ preview-teleported
+ />
+ <span v-else>鏃犲浘鐗�</span>
+ </template>
+ </el-table-column>
+ <el-table-column prop="author" label="浣滆��" width="120" />
+ <el-table-column prop="viewCount" label="娴忚閲�" width="100" align="center" />
+ <el-table-column prop="createTime" label="鍒涘缓鏃堕棿" width="180" />
+ <el-table-column prop="stateName" label="鐘舵��" width="100" align="center">
+ <template #default="{ row }">
+ <el-tag :type="getStatusType(row.stateName)">{{ row.stateName }}</el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" width="120" fixed="right" align="center">
+ <template #default="{ row }">
+ <div class="table-actions">
+ <el-button
+ text
+ :icon="Edit"
+ size="small"
+ @click="handleEdit(row)"
+ class="action-btn edit-btn"
+ title="缂栬緫"
+ />
+ <el-button
+ text
+ :icon="Delete"
+ size="small"
+ @click="handleDelete(row)"
+ class="action-btn delete-btn"
+ title="鍒犻櫎"
+ />
+ </div>
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <!-- 鍒嗛〉 -->
+ <div class="pagination">
+ <el-pagination
+ v-model:current-page="pagination.page"
+ v-model:page-size="pagination.size"
+ :page-sizes="[10, 20, 50, 100]"
+ :total="pagination.total"
+ layout="total, sizes, prev, pager, next, jumper"
+ @size-change="handleSizeChange"
+ @current-change="handleCurrentChange"
+ />
+ </div>
+ </div>
+ </div>
+</template>
+
+<script setup lang="ts">
+import { reactive, ref, onMounted, onActivated } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { useRouter } from 'vue-router'
+import { Search, Plus, Edit, Delete } from '@element-plus/icons-vue'
+import { getNewsList, updateNewsState } from '@/api/news'
+
+const loading = ref(false)
+const router = useRouter()
+
+// 鎼滅储琛ㄥ崟
+const searchForm = reactive({
+ title: '',
+ state: ''
+})
+
+const stateOptions = [
+ { label: '鑽夌', value: 0 },
+ { label: '鍙戝竷', value: 1 },
+ { label: '鍏抽棴', value: 2 }
+]
+
+// 鍒嗛〉淇℃伅
+const pagination = reactive({
+ page: 1,
+ size: 10,
+ total: 0
+})
+
+// 琛ㄦ牸鏁版嵁
+const tableData = ref([])
+
+// 鑾峰彇鐘舵�佹爣绛剧被鍨�
+const getStatusType = (status: string) => {
+ const typeMap: Record<string, string> = {
+ 鑽夌: 'info',
+ 鍙戝竷: 'success',
+ 鍏抽棴: 'danger'
+ }
+ return typeMap[status] || 'info'
+}
+
+// 鎼滅储
+const handleSearch = () => {
+ pagination.page = 1
+ loadData()
+}
+
+const handleStateChange = () => {
+ pagination.page = 1
+ loadData()
+}
+
+// 娓呯┖鎼滅储
+const handleClear = () => {
+ searchForm.title = ''
+ loadData()
+}
+
+// 鏂板鏂伴椈
+const handleAdd = () => {
+ router.push('/news/new')
+}
+
+// 缂栬緫鏂伴椈
+const handleEdit = (row: any) => {
+ router.push(`/news/edit/${row.id}`)
+}
+
+// 鍒犻櫎鏂伴椈
+const handleDelete = async (row: any) => {
+ try {
+ await ElMessageBox.confirm(
+ `纭畾瑕佸垹闄ゆ柊闂� 鈥�${row.title}鈥� 鍚楋紵`,
+ '鎻愮ず',
+ {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ }
+ )
+
+ await updateNewsState(row.id, 2)
+ ElMessage.success('鍒犻櫎鎴愬姛')
+ loadData()
+ } catch {
+ // 鐢ㄦ埛鍙栨秷鎿嶄綔
+ }
+}
+
+// 鍒嗛〉澶у皬鏀瑰彉
+const handleSizeChange = (size: number) => {
+ pagination.size = size
+ loadData()
+}
+
+// 褰撳墠椤垫敼鍙�
+const handleCurrentChange = (page: number) => {
+ pagination.page = page
+ loadData()
+}
+
+// 鍔犺浇鏁版嵁
+const loadData = async () => {
+ loading.value = true
+ try {
+ const keyword = (searchForm.title || '').trim()
+ const data = await getNewsList(
+ pagination.page - 1,
+ pagination.size,
+ keyword,
+ searchForm.state
+ )
+
+ tableData.value = data?.content || []
+ pagination.total = data?.totalElements || 0
+ } catch (e: any) {
+ console.error('鍔犺浇鏁版嵁澶辫触:', e)
+ ElMessage.error(e?.message || '鍔犺浇鏂伴椈鍒楄〃澶辫触')
+ } finally {
+ loading.value = false
+ }
+}
+
+onMounted(() => {
+ loadData()
+})
+
+// 椤甸潰婵�娲绘椂閲嶆柊鍔犺浇鏁版嵁
+onActivated(() => {
+ loadData()
+})
+</script>
+
+<style scoped>
+.news-page {
+ padding: 20px;
+}
+
+.page-card {
+ background: white;
+ border-radius: 8px;
+ padding: 24px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+}
+
+/* 椤甸潰澶撮儴鏍峰紡 */
+.page-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ margin-bottom: 24px;
+ gap: 20px;
+}
+
+.title-section {
+ flex: 1;
+}
+
+.page-title {
+ margin: 0 0 8px 0;
+ font-size: 24px;
+ font-weight: 600;
+ color: #1a1a1a;
+ line-height: 1.2;
+}
+
+.page-subtitle {
+ margin: 0;
+ font-size: 14px;
+ color: #666;
+ line-height: 1.4;
+}
+
+/* 鎼滅储宸ュ叿鏍忔牱寮� */
+.search-toolbar {
+ display: flex;
+ gap: 12px;
+ align-items: center;
+ justify-content: flex-end;
+ margin-bottom: 20px;
+}
+
+/* 琛ㄦ牸鎿嶄綔鎸夐挳鏍峰紡 */
+.table-actions {
+ display: flex;
+ gap: 12px;
+ justify-content: center;
+}
+
+.action-btn {
+ padding: 4px;
+ border: none;
+ background: transparent !important;
+ transition: all 0.2s ease;
+}
+
+.edit-btn {
+ color: #409eff;
+}
+
+.edit-btn:hover {
+ color: #337ecc;
+ transform: scale(1.2);
+ background: rgba(64, 158, 255, 0.1) !important;
+}
+
+.delete-btn {
+ color: #f56c6c;
+}
+
+.delete-btn:hover {
+ color: #dd6161;
+ transform: scale(1.2);
+ background: rgba(245, 108, 108, 0.1) !important;
+}
+
+.pagination {
+ margin-top: 20px;
+ display: flex;
+ justify-content: flex-end;
+}
+
+/* 灏侀潰鍥剧墖鏍峰紡 */
+.cover-image {
+ width: 80px;
+ height: 60px;
+ border-radius: 4px;
+ cursor: pointer;
+}
+
+/* 鍝嶅簲寮忛�傞厤 */
+@media (max-width: 768px) {
+ .page-header {
+ flex-direction: column;
+ align-items: stretch;
+ gap: 16px;
+ }
+
+ .search-toolbar {
+ flex-wrap: wrap;
+ gap: 8px;
+ }
+
+ .search-toolbar .el-input {
+ width: 100% !important;
+ max-width: 280px;
+ }
+}
+</style>
\ No newline at end of file
diff --git a/wx/app.json b/wx/app.json
index 6e950e6..cd5019c 100644
--- a/wx/app.json
+++ b/wx/app.json
@@ -1,6 +1,8 @@
{
"pages": [
"pages/index/index",
+ "pages/news/list",
+ "pages/news/detail",
"pages/activity/detail",
"pages/registration/registration",
"pages/profile/profile",
@@ -55,4 +57,4 @@
"audio"
],
"sitemapLocation": "sitemap.json"
-}
+}
\ No newline at end of file
diff --git a/wx/pages/index/index.js b/wx/pages/index/index.js
index ba5d944..0769b68 100644
--- a/wx/pages/index/index.js
+++ b/wx/pages/index/index.js
@@ -8,6 +8,8 @@
banners: [],
// 璧涗簨鍒楄〃
activities: [],
+ // 鏈�鏂版柊闂诲垪琛�
+ latestNews: [],
// 鍔犺浇鐘舵��
loading: true,
// 鏄惁杩樻湁鏇村鏁版嵁
@@ -45,6 +47,7 @@
}
// 鍔犺浇鏁版嵁
this.loadBanners()
+ this.loadLatestNews()
this.loadActivities()
},
@@ -70,6 +73,7 @@
Promise.all([
this.loadBanners(),
+ this.loadLatestNews(),
this.loadActivities()
]).finally(() => {
wx.stopPullDownRefresh()
@@ -162,6 +166,47 @@
}
}).catch(err => {
console.error('鍔犺浇杞挱鍥惧け璐�:', err)
+ })
+ },
+
+ // 鍔犺浇鏈�鏂版柊闂伙紙鍙姞杞藉墠3鏉★級
+ loadLatestNews() {
+ return app.graphqlRequest(`
+ query getPublishedNewsList($page: Int!, $size: Int!) {
+ publishedNewsList(page: $page, size: $size) {
+ content {
+ id
+ title
+ summary
+ coverImage
+ author
+ viewCount
+ createTime
+ }
+ totalElements
+ page
+ size
+ }
+ }
+ `, {
+ page: 1,
+ size: 3
+ }).then(data => {
+ if (data.publishedNewsList) {
+ // 鏍煎紡鍖栨椂闂存樉绀�
+ const latestNews = data.publishedNewsList.content.map(news => {
+ if (news.createTime) {
+ news.createTime = utils.formatDate(news.createTime, 'YYYY-MM-DD HH:mm:ss');
+ }
+ return news;
+ });
+
+ this.setData({
+ latestNews: latestNews
+ })
+ }
+ }).catch(err => {
+ console.error('鍔犺浇鏈�鏂版柊闂诲け璐�:', err)
})
},
@@ -341,6 +386,22 @@
utils.navigateTo('/pages/activity/detail', { id: activityId })
},
+ // 璺宠浆鍒版柊闂昏鎯�
+ goToNewsDetail(e) {
+ const newsId = e.currentTarget.dataset.id
+ if (newsId) {
+ wx.navigateTo({
+ url: `/pages/news/detail?id=${newsId}`
+ })
+ }
+ },
+
+ // 璺宠浆鍒版柊闂诲垪琛�
+ goToNewsList() {
+ wx.navigateTo({
+ url: '/pages/news/list'
+ })
+ },
// 鏍煎紡鍖栨棩鏈�
formatDate(date) {
@@ -525,77 +586,34 @@
imageUrl: '', // 鍙互璁剧疆鍒嗕韩鍥剧墖
success: (res) => {
console.log('鍒嗕韩鎴愬姛', res)
- wx.showToast({
- title: '鍒嗕韩鎴愬姛',
- icon: 'success',
- duration: 2000
- })
- // 娓呴櫎鍒嗕韩鐘舵��
- this.setData({
- shareActivityId: null,
- shareActivityName: null
- })
},
fail: (res) => {
console.log('鍒嗕韩澶辫触', res)
- wx.showToast({
- title: '鍒嗕韩澶辫触',
- icon: 'none',
- duration: 2000
- })
}
}
return shareData
}
- // 榛樿鍒嗕韩鏁翠釜棣栭〉
+ // 榛樿鍒嗕韩
return {
- title: '钃塭鍒涙瘮璧涘钩鍙� - 鍙戠幇绮惧僵姣旇禌',
+ title: '钃塭鍒涙瘮璧涘钩鍙�',
path: '/pages/index/index',
- imageUrl: '', // 鍙互璁剧疆榛樿鍒嗕韩鍥剧墖
+ imageUrl: '',
success: (res) => {
console.log('鍒嗕韩鎴愬姛', res)
- wx.showToast({
- title: '鍒嗕韩鎴愬姛',
- icon: 'success',
- duration: 2000
- })
},
fail: (res) => {
console.log('鍒嗕韩澶辫触', res)
- wx.showToast({
- title: '鍒嗕韩澶辫触',
- icon: 'none',
- duration: 2000
- })
}
}
},
- // 鍒嗕韩鍒版湅鍙嬪湀
+ // 椤甸潰鍒嗕韩鍔熻兘 - 鍒嗕韩鍒版湅鍙嬪湀
onShareTimeline() {
- console.log('鍒嗕韩鍒版湅鍙嬪湀')
-
return {
- title: '钃塭鍒涙瘮璧涘钩鍙� - 鍙戠幇绮惧僵姣旇禌',
+ title: '钃塭鍒涙瘮璧涘钩鍙�',
query: '',
- imageUrl: '', // 鍙互璁剧疆鍒嗕韩鍥剧墖
- success: (res) => {
- console.log('鍒嗕韩鍒版湅鍙嬪湀鎴愬姛', res)
- wx.showToast({
- title: '鍒嗕韩鎴愬姛',
- icon: 'success',
- duration: 2000
- })
- },
- fail: (res) => {
- console.log('鍒嗕韩鍒版湅鍙嬪湀澶辫触', res)
- wx.showToast({
- title: '鍒嗕韩澶辫触',
- icon: 'none',
- duration: 2000
- })
- }
+ imageUrl: ''
}
}
})
\ No newline at end of file
diff --git a/wx/pages/index/index.wxml b/wx/pages/index/index.wxml
index caaf876..7c9996f 100644
--- a/wx/pages/index/index.wxml
+++ b/wx/pages/index/index.wxml
@@ -1,5 +1,4 @@
<wxs src="./filters.wxs" module="filters" />
-<!--pages/index/index.wxml-->
<view class="container">
<!-- 椤甸潰澶撮儴鍒嗕韩鍖哄煙 -->
<view class="header-section">
@@ -72,6 +71,36 @@
</swiper>
</view>
+ <!-- 鏂伴椈妯″潡 -->
+ <view class="section-title">鏂伴椈璧勮</view>
+ <view class="news-section">
+ <view
+ class="news-card"
+ wx:for="{{latestNews}}"
+ wx:key="id"
+ bindtap="goToNewsDetail"
+ data-id="{{item.id}}"
+ >
+ <view class="news-content">
+ <view class="news-title">{{item.title}}</view>
+ <view wx:if="{{item.summary}}" class="news-summary">{{item.summary}}</view>
+ <view class="news-meta">
+ <text wx:if="{{item.author}}" class="author">{{item.author}}</text>
+ <text wx:if="{{item.author && item.createTime}}" class="separator">|</text>
+ <text class="time">{{item.createTime}}</text>
+ </view>
+ </view>
+ <view wx:if="{{item.coverImage}}" class="news-thumb">
+ <image class="thumb-image" src="{{item.coverImage}}" mode="aspectFill" />
+ </view>
+ </view>
+
+ <view class="view-more" bindtap="goToNewsList">
+ <text class="view-more-text">鏌ョ湅鏇村</text>
+ <text class="arrow">鈥�</text>
+ </view>
+ </view>
+
<!-- 妯″潡鏍囬 -->
<view class="section-title">姣旇禌淇℃伅</view>
diff --git a/wx/pages/index/index.wxss b/wx/pages/index/index.wxss
index d2ef0c1..21269e1 100644
--- a/wx/pages/index/index.wxss
+++ b/wx/pages/index/index.wxss
@@ -252,6 +252,109 @@
color: #0f172a;
}
+/* 鏂伴椈妯″潡鏍峰紡 */
+.news-section {
+ padding: 0 20rpx 20rpx;
+ background: #ffffff;
+ border-radius: 20rpx;
+ margin-bottom: 24rpx;
+ box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.1);
+}
+
+.news-card {
+ display: flex;
+ padding: 24rpx;
+ border-bottom: 1rpx solid #f0f0f0;
+ transition: background-color 0.3s ease;
+}
+
+.news-card:last-child {
+ border-bottom: none;
+}
+
+.news-card:active {
+ background-color: #f8f9fa;
+}
+
+.news-content {
+ flex: 1;
+ min-width: 0;
+ padding-right: 20rpx;
+}
+
+.news-title {
+ font-size: 30rpx;
+ font-weight: 600;
+ color: #0f172a;
+ line-height: 1.4;
+ margin-bottom: 12rpx;
+ display: -webkit-box;
+ -webkit-box-orient: vertical;
+ -webkit-line-clamp: 2;
+ overflow: hidden;
+}
+
+.news-summary {
+ font-size: 26rpx;
+ color: #64748b;
+ line-height: 1.5;
+ margin-bottom: 12rpx;
+ display: -webkit-box;
+ -webkit-box-orient: vertical;
+ -webkit-line-clamp: 2;
+ overflow: hidden;
+}
+
+.news-meta {
+ display: flex;
+ align-items: center;
+ gap: 8rpx;
+}
+
+.author {
+ font-size: 22rpx;
+ color: #94a3b8;
+}
+
+.separator {
+ font-size: 22rpx;
+ color: #cbd5e1;
+}
+
+.time {
+ font-size: 22rpx;
+ color: #94a3b8;
+}
+
+.news-thumb {
+ width: 160rpx;
+ height: 120rpx;
+ flex-shrink: 0;
+ border-radius: 12rpx;
+ overflow: hidden;
+}
+
+.thumb-image {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+
+.view-more {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 24rpx;
+ color: #007aff;
+ font-size: 28rpx;
+ font-weight: 500;
+}
+
+.arrow {
+ margin-left: 8rpx;
+ font-size: 32rpx;
+}
+
/* 绛涢�夋爮鏍峰紡 */
.filter-bar {
background: #ffffff;
diff --git a/wx/pages/news/detail.js b/wx/pages/news/detail.js
new file mode 100644
index 0000000..59d5a84
--- /dev/null
+++ b/wx/pages/news/detail.js
@@ -0,0 +1,77 @@
+// pages/news/detail.js
+const app = getApp()
+const utils = require('../../lib/utils.js')
+
+Page({
+ data: {
+ news: null,
+ loading: true
+ },
+
+ onLoad(options) {
+ const newsId = options.id
+ if (newsId) {
+ this.loadNewsDetail(newsId)
+ } else {
+ wx.showToast({
+ title: '鍙傛暟閿欒',
+ icon: 'none'
+ })
+ setTimeout(() => {
+ wx.navigateBack()
+ }, 1500)
+ }
+ },
+
+ onShow() {
+ // 缁熶竴绯荤粺瀵艰埅鏍忔爣棰�
+ try { wx.setNavigationBarTitle({ title: '鏂伴椈璇︽儏' }) } catch (e) {}
+ },
+
+ // 鍔犺浇鏂伴椈璇︽儏
+ loadNewsDetail(newsId) {
+ this.setData({ loading: true })
+
+ app.graphqlRequest(`
+ query getPublishedNews($id: ID!) {
+ publishedNews(id: $id) {
+ id
+ title
+ content
+ summary
+ coverImage
+ author
+ viewCount
+ createTime
+ }
+ }
+ `, {
+ id: newsId
+ }).then(data => {
+ if (data.publishedNews) {
+ // 鏍煎紡鍖栨椂闂存樉绀�
+ const news = data.publishedNews;
+ if (news.createTime) {
+ news.createTime = utils.formatDate(news.createTime, 'YYYY-MM-DD HH:mm:ss');
+ }
+
+ this.setData({
+ news: news,
+ loading: false
+ })
+ } else {
+ throw new Error('鏂伴椈涓嶅瓨鍦�')
+ }
+ }).catch(err => {
+ console.error('鍔犺浇鏂伴椈璇︽儏澶辫触:', err)
+ wx.showToast({
+ title: '鍔犺浇澶辫触锛岃閲嶈瘯',
+ icon: 'none'
+ })
+ this.setData({ loading: false })
+ setTimeout(() => {
+ wx.navigateBack()
+ }, 1500)
+ })
+ }
+})
\ No newline at end of file
diff --git a/wx/pages/news/detail.json b/wx/pages/news/detail.json
new file mode 100644
index 0000000..c293ba3
--- /dev/null
+++ b/wx/pages/news/detail.json
@@ -0,0 +1,3 @@
+{
+ "navigationBarTitleText": "鏂伴椈璇︽儏"
+}
\ No newline at end of file
diff --git a/wx/pages/news/detail.wxml b/wx/pages/news/detail.wxml
new file mode 100644
index 0000000..27ab74d
--- /dev/null
+++ b/wx/pages/news/detail.wxml
@@ -0,0 +1,26 @@
+<!-- pages/news/detail.wxml -->
+<view class="container">
+ <view wx:if="{{loading}}" class="loading-wrapper">
+ <view class="loading"></view>
+ <text class="loading-text">鍔犺浇涓�...</text>
+ </view>
+
+ <view wx:else class="news-detail">
+ <view class="news-header">
+ <view class="news-title">{{news.title}}</view>
+ <view class="news-meta">
+ <text wx:if="{{news.author}}" class="author">{{news.author}}</text>
+ <text wx:if="{{news.author}}" class="separator">|</text>
+ <text class="time">{{news.createTime}}</text>
+ </view>
+ </view>
+
+ <view wx:if="{{news.coverImage}}" class="news-cover">
+ <image class="cover-image" src="{{news.coverImage}}" mode="widthFix" />
+ </view>
+
+ <view class="news-content">
+ <rich-text nodes="{{news.content}}"></rich-text>
+ </view>
+ </view>
+</view>
\ No newline at end of file
diff --git a/wx/pages/news/detail.wxss b/wx/pages/news/detail.wxss
new file mode 100644
index 0000000..feeafa5
--- /dev/null
+++ b/wx/pages/news/detail.wxss
@@ -0,0 +1,169 @@
+/* pages/news/detail.wxss */
+.container {
+ min-height: 100vh;
+ background-color: #f5f5f5;
+}
+
+.loading-wrapper {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: 100rpx 0;
+}
+
+.loading {
+ width: 40rpx;
+ height: 40rpx;
+ border: 4rpx solid #f3f3f3;
+ border-top: 4rpx solid #007aff;
+ border-radius: 50%;
+ animation: spin 1s linear infinite;
+ margin-bottom: 20rpx;
+}
+
+@keyframes spin {
+ 0% { transform: rotate(0deg); }
+ 100% { transform: rotate(360deg); }
+}
+
+.loading-text {
+ font-size: 28rpx;
+ color: #999;
+}
+
+.news-detail {
+ padding: 20rpx;
+}
+
+.news-header {
+ background: white;
+ border-radius: 16rpx;
+ padding: 30rpx;
+ margin-bottom: 20rpx;
+ box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.1);
+}
+
+.news-title {
+ font-size: 36rpx;
+ font-weight: 600;
+ color: #333;
+ line-height: 1.4;
+ margin-bottom: 20rpx;
+ word-wrap: break-word;
+ overflow-wrap: break-word;
+}
+
+.news-meta {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 12rpx;
+ margin-bottom: 16rpx;
+}
+
+.author {
+ font-size: 26rpx;
+ color: #666;
+}
+
+.separator {
+ font-size: 26rpx;
+ color: #ddd;
+}
+
+.time {
+ font-size: 26rpx;
+ color: #999;
+}
+
+.update-time {
+ font-size: 24rpx;
+ color: #999;
+}
+
+.news-cover {
+ background: white;
+ border-radius: 16rpx;
+ overflow: hidden;
+ margin-bottom: 20rpx;
+ box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.1);
+}
+
+.cover-image {
+ width: 100%;
+ display: block;
+}
+
+.news-content {
+ background: white;
+ border-radius: 16rpx;
+ padding: 30rpx;
+ box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.1);
+ word-wrap: break-word;
+ overflow-wrap: break-word;
+ max-width: 100%;
+}
+
+.news-content rich-text {
+ font-size: 30rpx;
+ line-height: 1.6;
+ color: #333;
+ word-wrap: break-word;
+ overflow-wrap: break-word;
+ max-width: 100%;
+ display: block;
+}
+
+/* 瀵屾枃鏈唴瀹规牱寮� */
+.news-content rich-text image,
+.news-content rich-text img {
+ max-width: 100% !important;
+ width: auto !important;
+ height: auto !important;
+ border-radius: 12rpx;
+ margin: 20rpx 0;
+ display: block !important;
+ box-sizing: border-box;
+}
+
+.news-content rich-text p {
+ margin: 0 0 20rpx 0;
+ line-height: 1.6;
+ word-wrap: break-word;
+ overflow-wrap: break-word;
+ max-width: 100%;
+}
+
+/* 纭繚鎵�鏈夊瘜鏂囨湰鍏冪礌閮戒笉浼氳秴鍑哄鍣� */
+.news-content rich-text view,
+.news-content rich-text div,
+.news-content rich-text span {
+ max-width: 100% !important;
+ word-wrap: break-word;
+ overflow-wrap: break-word;
+ box-sizing: border-box;
+}
+
+/* 鐗瑰埆澶勭悊琛ㄦ牸 */
+.news-content rich-text table {
+ max-width: 100% !important;
+ overflow-x: auto;
+ display: block;
+ box-sizing: border-box;
+}
+
+.news-content rich-text td,
+.news-content rich-text th {
+ word-wrap: break-word;
+ overflow-wrap: break-word;
+ max-width: 100%;
+ box-sizing: border-box;
+}
+
+/* 澶勭悊鍙兘鐨勫唴鑱旀牱寮� */
+.news-content rich-text image[style*="width"],
+.news-content rich-text img[style*="width"],
+.news-content rich-text div[style*="width"],
+.news-content rich-text p[style*="width"] {
+ max-width: 100% !important;
+}
\ No newline at end of file
diff --git a/wx/pages/news/list.js b/wx/pages/news/list.js
new file mode 100644
index 0000000..8ac6e5f
--- /dev/null
+++ b/wx/pages/news/list.js
@@ -0,0 +1,120 @@
+// pages/news/list.js
+const app = getApp()
+const utils = require('../../lib/utils.js')
+
+Page({
+ data: {
+ newsList: [],
+ loading: true,
+ hasMore: true,
+ currentPage: 1,
+ pageSize: 10
+ },
+
+ onLoad(options) {
+ this.loadNewsList()
+ },
+
+ onShow() {
+ // 缁熶竴绯荤粺瀵艰埅鏍忔爣棰�
+ try { wx.setNavigationBarTitle({ title: '鏂伴椈璧勮' }) } catch (e) {}
+ },
+
+ onPullDownRefresh() {
+ this.refreshData()
+ },
+
+ onReachBottom() {
+ if (this.data.hasMore && !this.data.loading) {
+ this.loadMoreNews()
+ }
+ },
+
+ // 鍒锋柊鏁版嵁
+ refreshData() {
+ this.setData({
+ currentPage: 1,
+ hasMore: true,
+ newsList: []
+ })
+ this.loadNewsList().finally(() => {
+ wx.stopPullDownRefresh()
+ })
+ },
+
+ // 鍔犺浇鏂伴椈鍒楄〃
+ loadNewsList(isLoadMore = false) {
+ this.setData({ loading: true })
+
+ const { currentPage, pageSize } = this.data
+
+ return app.graphqlRequest(`
+ query getPublishedNewsList($page: Int!, $size: Int!) {
+ publishedNewsList(page: $page, size: $size) {
+ content {
+ id
+ title
+ summary
+ coverImage
+ author
+ viewCount
+ createTime
+ }
+ totalElements
+ page
+ size
+ }
+ }
+ `, {
+ page: currentPage,
+ size: pageSize
+ }).then(data => {
+ if (data.publishedNewsList) {
+ const newNewsList = data.publishedNewsList.content
+
+ // 鏍煎紡鍖栨椂闂存樉绀�
+ newNewsList.forEach(news => {
+ if (news.createTime) {
+ news.createTime = utils.formatDate(news.createTime, 'YYYY-MM-DD HH:mm:ss');
+ }
+ });
+
+ // 鍚堝苟鏁版嵁锛氬彧鏈夊湪鐪熸鐨勫姞杞芥洿澶氭椂鎵嶈拷鍔狅紝鍏朵粬鎯呭喌閮芥槸鍏ㄩ噺鏇挎崲
+ const mergedNewsList = isLoadMore && this.data.newsList.length > 0
+ ? [...this.data.newsList, ...newNewsList]
+ : newNewsList
+
+ this.setData({
+ newsList: mergedNewsList,
+ hasMore: data.publishedNewsList.totalElements > (currentPage * pageSize) && newNewsList.length > 0,
+ loading: false
+ })
+ }
+ }).catch(err => {
+ console.error('鍔犺浇鏂伴椈鍒楄〃澶辫触:', err)
+ wx.showToast({
+ title: '鍔犺浇澶辫触锛岃閲嶈瘯',
+ icon: 'none'
+ })
+ this.setData({ loading: false })
+ })
+ },
+
+ // 鍔犺浇鏇村鏂伴椈
+ loadMoreNews() {
+ this.setData({
+ currentPage: this.data.currentPage + 1
+ })
+ this.loadNewsList(true)
+ },
+
+ // 璺宠浆鍒版柊闂昏鎯�
+ goToNewsDetail(e) {
+ const newsId = e.currentTarget.dataset.id
+ if (newsId) {
+ wx.navigateTo({
+ url: `/pages/news/detail?id=${newsId}`
+ })
+ }
+ }
+})
\ No newline at end of file
diff --git a/wx/pages/news/list.json b/wx/pages/news/list.json
new file mode 100644
index 0000000..505ad33
--- /dev/null
+++ b/wx/pages/news/list.json
@@ -0,0 +1,5 @@
+{
+ "navigationBarTitleText": "鏂伴椈璧勮",
+ "enablePullDownRefresh": true,
+ "backgroundTextStyle": "dark"
+}
\ No newline at end of file
diff --git a/wx/pages/news/list.wxml b/wx/pages/news/list.wxml
new file mode 100644
index 0000000..e9524f1
--- /dev/null
+++ b/wx/pages/news/list.wxml
@@ -0,0 +1,50 @@
+<!-- pages/news/list.wxml -->
+<view class="container">
+ <!-- 鏂伴椈鍒楄〃 -->
+ <view class="news-list">
+ <view
+ class="news-item"
+ wx:for="{{newsList}}"
+ wx:key="id"
+ bindtap="goToNewsDetail"
+ data-id="{{item.id}}"
+ >
+ <view class="news-card">
+ <view class="news-header">
+ <view class="news-title">{{item.title}}</view>
+ </view>
+ <view class="news-body">
+ <view wx:if="{{item.coverImage}}" class="news-cover">
+ <image class="cover-image" src="{{item.coverImage}}" mode="aspectFill" />
+ </view>
+ <view wx:if="{{item.summary}}" class="news-summary">{{item.summary}}</view>
+ </view>
+ <view class="news-footer">
+ <view class="news-meta">
+ <text wx:if="{{item.author}}" class="author">{{item.author}}</text>
+ <text wx:if="{{item.author}}" class="separator">|</text>
+ <text class="time">{{item.createTime}}</text>
+ </view>
+ </view>
+ </view>
+ </view>
+
+ <!-- 鍔犺浇鐘舵�� -->
+ <view class="loading-wrapper" wx:if="{{loading}}">
+ <view class="loading"></view>
+ <text class="loading-text">鍔犺浇涓�...</text>
+ </view>
+
+ <!-- 娌℃湁鏇村鏁版嵁 -->
+ <view class="no-more" wx:if="{{!hasMore && newsList.length > 0}}">
+ <text class="no-more-text">娌℃湁鏇村鏁版嵁浜�</text>
+ </view>
+
+ <!-- 绌虹姸鎬� -->
+ <view class="empty-state" wx:if="{{!loading && newsList.length === 0}}">
+ <view class="empty-icon">馃摪</view>
+ <view class="empty-text">鏆傛棤鏂伴椈璧勮</view>
+ <view class="empty-desc">璇风◢鍚庡啀璇�</view>
+ </view>
+ </view>
+</view>
\ No newline at end of file
diff --git a/wx/pages/news/list.wxss b/wx/pages/news/list.wxss
new file mode 100644
index 0000000..70bb76d
--- /dev/null
+++ b/wx/pages/news/list.wxss
@@ -0,0 +1,174 @@
+/* pages/news/list.wxss */
+.container {
+ padding: 20rpx;
+ background-color: #f5f5f5;
+ min-height: 100vh;
+}
+
+.news-list {
+ display: flex;
+ flex-direction: column;
+ gap: 20rpx;
+}
+
+.news-item {
+ background: white;
+ border-radius: 16rpx;
+ overflow: hidden;
+ box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.1);
+ transition: all 0.3s ease;
+}
+
+.news-item:active {
+ transform: scale(0.98);
+ box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.15);
+}
+
+.news-card {
+ padding: 24rpx;
+}
+
+.news-header {
+ margin-bottom: 16rpx;
+}
+
+.news-title {
+ font-size: 32rpx;
+ font-weight: 600;
+ color: #333;
+ line-height: 1.4;
+ display: -webkit-box;
+ -webkit-box-orient: vertical;
+ -webkit-line-clamp: 2;
+ overflow: hidden;
+}
+
+.news-body {
+ margin-bottom: 16rpx;
+}
+
+.news-cover {
+ margin-bottom: 16rpx;
+ border-radius: 12rpx;
+ overflow: hidden;
+ height: 280rpx;
+}
+
+.cover-image {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+
+.news-summary {
+ font-size: 28rpx;
+ color: #666;
+ line-height: 1.5;
+ display: -webkit-box;
+ -webkit-box-orient: vertical;
+ -webkit-line-clamp: 3;
+ overflow: hidden;
+}
+
+.news-footer {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding-top: 16rpx;
+ border-top: 1rpx solid #eee;
+}
+
+.news-meta {
+ display: flex;
+ align-items: center;
+ gap: 12rpx;
+}
+
+.author {
+ font-size: 24rpx;
+ color: #999;
+}
+
+.separator {
+ font-size: 24rpx;
+ color: #ddd;
+}
+
+.time {
+ font-size: 24rpx;
+ color: #999;
+}
+
+.news-stats {
+ display: flex;
+ align-items: center;
+}
+
+.view-count {
+ font-size: 24rpx;
+ color: #999;
+}
+
+/* 鍔犺浇鐘舵�� */
+.loading-wrapper {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: 40rpx 0;
+}
+
+.loading {
+ width: 40rpx;
+ height: 40rpx;
+ border: 4rpx solid #f3f3f3;
+ border-top: 4rpx solid #007aff;
+ border-radius: 50%;
+ animation: spin 1s linear infinite;
+ margin-bottom: 20rpx;
+}
+
+@keyframes spin {
+ 0% { transform: rotate(0deg); }
+ 100% { transform: rotate(360deg); }
+}
+
+.loading-text {
+ font-size: 28rpx;
+ color: #999;
+}
+
+/* 娌℃湁鏇村鏁版嵁 */
+.no-more {
+ text-align: center;
+ padding: 40rpx 0;
+}
+
+.no-more-text {
+ font-size: 28rpx;
+ color: #999;
+}
+
+/* 绌虹姸鎬� */
+.empty-state {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: 100rpx 40rpx;
+}
+
+.empty-icon {
+ font-size: 80rpx;
+ margin-bottom: 20rpx;
+ opacity: 0.5;
+}
+
+.empty-text {
+ font-size: 32rpx;
+ color: #666;
+ margin-bottom: 10rpx;
+}
+
+.empty-desc {
+ font-size: 28rpx;
+ color: #999;
+}
\ No newline at end of file
--
Gitblit v1.8.0