peng
2025-11-06 c4938f6f4e839890b032c75c7a57333a6a9157a9
添加新闻功能
8个文件已修改
23个文件已添加
3232 ■■■■■ 已修改文件
.claude/settings.local.json 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
agents.md 134 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
backend/src/main/java/com/rongyichuang/common/api/FileUploadController.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
backend/src/main/java/com/rongyichuang/news/dto/NewsInput.java 77 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
backend/src/main/java/com/rongyichuang/news/dto/NewsResponse.java 144 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
backend/src/main/java/com/rongyichuang/news/entity/News.java 102 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
backend/src/main/java/com/rongyichuang/news/repository/NewsRepository.java 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
backend/src/main/java/com/rongyichuang/news/resolver/NewsResolver.java 77 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
backend/src/main/java/com/rongyichuang/news/service/NewsService.java 153 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
backend/src/main/resources/graphql/news.graphqls 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
news-module-readme.md 97 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
web/package.json 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
web/src/api/news.js 196 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
web/src/layout/index.vue 7 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
web/src/router/index.ts 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
web/src/views/NewsDetail.vue 182 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
web/src/views/NewsForm.vue 408 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
web/src/views/NewsListPage.vue 277 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
web/src/views/news-list.vue 359 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
wx/app.json 4 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
wx/pages/index/index.js 116 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
wx/pages/index/index.wxml 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
wx/pages/index/index.wxss 103 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
wx/pages/news/detail.js 77 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
wx/pages/news/detail.json 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
wx/pages/news/detail.wxml 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
wx/pages/news/detail.wxss 169 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
wx/pages/news/list.js 120 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
wx/pages/news/list.json 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
wx/pages/news/list.wxml 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
wx/pages/news/list.wxss 174 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.claude/settings.local.json
New file
@@ -0,0 +1,9 @@
{
  "permissions": {
    "allow": [
      "Bash(tree:*)"
    ],
    "deny": [],
    "ask": []
  }
}
agents.md
New file
@@ -0,0 +1,134 @@
\# Codex Agent 全局规则(精简执行版)
\## 角色定义
你是一名高标准的软件工程智能助理
所有输出、修改与建议必须遵循以下规则
---
\## 核心原则(不可违反)
1\. \*\*质量第一\*\*:代码质量与系统安全不容妥协。
2\. \*\*中文统一\*\*:输出、注释、文档全部使用中文。
3\. \*\*思考先行\*\*:先分析与规划,再进行实现。
4\. \*\*工具优先\*\*:优先采用验证过的最佳工具链。
5\. \*\*透明记录\*\*:重要决策、变更理由可追溯。
6\. \*\*持续改进\*\*:执行后反思、总结并优化。
7\. \*\*结果导向\*\*:以目标达成评判执行效果。
8\. \*\*结果导向\*\*:不要修改删除我的数据库和字段。
---
\## 质量标准
\- 命名清晰、结构合理、注释必要。
\- 禁止半成品(MVP / TODO / Stub)。
\- 控制复杂度,优化性能与资源使用。
\- 异常、边界、并发场景必须处理。
\- 设计可测试结构,保持测试可执行。
\- 通过静态检查、格式化与代码审查。
---
\## 任务类型策略
\- \*\*紧急修复\*\*:优先稳定系统,保留质量检查。
\- \*\*新功能\*\*:注重设计与完整测试。
\- \*\*重构优化\*\*:关注架构与性能提升。
\- \*\*学习探索\*\*:可试验,但需记录总结。
---
\## 持续改进机制
1\. 执行前:选择策略、评估风险。
2\. 执行中:记录决策与问题。
3\. 执行后:复盘并更新最佳实践。
4\. 积累经验、工具技巧与合理例外。
---
\## 检查点
\- 遵循质量标准并记录关键变更。
\- 验证功能与质量,更新测试与文档。
\- 执行结束后总结改进项。
---
\## 行为准则
> 查询胜过猜测,确认胜过假设;
> 复用胜过重复,测试胜过跳过;
> 规范胜过随意,诚实胜过假装;
> 谨慎胜过盲目,学习胜过停滞。
---
\## 执行要求
\- 所有 AI 输出必须符合上述标准。
\- 冲突时以“质量第一”为最高优先级。
\- 禁止编造数据、忽略边界、生成占位符。
\- 所有结果必须透明、可解释、可验证。
backend/src/main/java/com/rongyichuang/common/api/FileUploadController.java
@@ -35,18 +35,22 @@
            // 构建文件访问URL
            String fileUrl = cosService.getFileUrl(relativePath);
            
            // 为兼容前端wangEditor,返回其期望的格式
            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);
        }
backend/src/main/java/com/rongyichuang/news/dto/NewsInput.java
New file
@@ -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;
    }
}
backend/src/main/java/com/rongyichuang/news/dto/NewsResponse.java
New file
@@ -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) {
            // 为所有img标签添加样式属性,确保图片不会超出容器
            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;
    }
}
backend/src/main/java/com/rongyichuang/news/entity/News.java
New file
@@ -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;
    }
}
backend/src/main/java/com/rongyichuang/news/repository/NewsRepository.java
New file
@@ -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);
}
backend/src/main/java/com/rongyichuang/news/resolver/NewsResolver.java
New file
@@ -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);
    }
}
backend/src/main/java/com/rongyichuang/news/service/NewsService.java
New file
@@ -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());
    }
}
backend/src/main/resources/graphql/news.graphqls
New file
@@ -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
}
news-module-readme.md
New file
@@ -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='新闻资讯表';
```
## 后端代码结构
```
com.rongyichuang.news
├── entity
│   └── News.java
├── dto
│   ├── NewsInput.java
│   └── NewsResponse.java
├── repository
│   └── NewsRepository.java
├── service
│   └── NewsService.java
└── resolver
    └── NewsResolver.java
```
## 前端代码结构
```
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. 需要Java 17环境编译后端代码
2. 前端需要安装wangEditor富文本编辑器依赖
3. 数据库需要执行t_news表的创建语句
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"
  }
}
}
web/src/api/news.js
New file
@@ -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 : '更新新闻状态失败')
  }
}
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>
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'),
web/src/views/NewsDetail.vue
New file
@@ -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>
web/src/views/NewsForm.vue
New file
@@ -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的核心包而不是Vue包装器
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>
web/src/views/NewsListPage.vue
New file
@@ -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>
web/src/views/news-list.vue
New file
@@ -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>
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"
}
}
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: '蓉e创比赛平台 - 发现精彩比赛',
      title: '蓉e创比赛平台',
      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: '蓉e创比赛平台 - 发现精彩比赛',
      title: '蓉e创比赛平台',
      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: ''
    }
  }
})
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>
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;
wx/pages/news/detail.js
New file
@@ -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)
    })
  }
})
wx/pages/news/detail.json
New file
@@ -0,0 +1,3 @@
{
  "navigationBarTitleText": "新闻详情"
}
wx/pages/news/detail.wxml
New file
@@ -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>
wx/pages/news/detail.wxss
New file
@@ -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;
}
wx/pages/news/list.js
New file
@@ -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}`
      })
    }
  }
})
wx/pages/news/list.json
New file
@@ -0,0 +1,5 @@
{
  "navigationBarTitleText": "新闻资讯",
  "enablePullDownRefresh": true,
  "backgroundTextStyle": "dark"
}
wx/pages/news/list.wxml
New file
@@ -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>
wx/pages/news/list.wxss
New file
@@ -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;
}