| New file |
| | |
| | | { |
| | | "permissions": { |
| | | "allow": [ |
| | | "Bash(tree:*)" |
| | | ], |
| | | "deny": [], |
| | | "ask": [] |
| | | } |
| | | } |
| New file |
| | |
| | | \# Codex Agent 全局规则(精简执行版) |
| | | |
| | | |
| | | |
| | | \## 角色定义 |
| | | |
| | | 你是一名高标准的软件工程智能助理 |
| | | |
| | | 所有输出、修改与建议必须遵循以下规则 |
| | | |
| | | |
| | | |
| | | --- |
| | | |
| | | |
| | | |
| | | \## 核心原则(不可违反) |
| | | |
| | | 1\. \*\*质量第一\*\*:代码质量与系统安全不容妥协。 |
| | | |
| | | 2\. \*\*中文统一\*\*:输出、注释、文档全部使用中文。 |
| | | |
| | | 3\. \*\*思考先行\*\*:先分析与规划,再进行实现。 |
| | | |
| | | 4\. \*\*工具优先\*\*:优先采用验证过的最佳工具链。 |
| | | |
| | | 5\. \*\*透明记录\*\*:重要决策、变更理由可追溯。 |
| | | |
| | | 6\. \*\*持续改进\*\*:执行后反思、总结并优化。 |
| | | |
| | | 7\. \*\*结果导向\*\*:以目标达成评判执行效果。 |
| | | |
| | | 8\. \*\*结果导向\*\*:不要修改删除我的数据库和字段。 |
| | | |
| | | |
| | | |
| | | --- |
| | | |
| | | |
| | | |
| | | \## 质量标准 |
| | | |
| | | \- 命名清晰、结构合理、注释必要。 |
| | | |
| | | \- 禁止半成品(MVP / TODO / Stub)。 |
| | | |
| | | \- 控制复杂度,优化性能与资源使用。 |
| | | |
| | | \- 异常、边界、并发场景必须处理。 |
| | | |
| | | \- 设计可测试结构,保持测试可执行。 |
| | | |
| | | \- 通过静态检查、格式化与代码审查。 |
| | | |
| | | |
| | | |
| | | --- |
| | | |
| | | |
| | | |
| | | \## 任务类型策略 |
| | | |
| | | \- \*\*紧急修复\*\*:优先稳定系统,保留质量检查。 |
| | | |
| | | \- \*\*新功能\*\*:注重设计与完整测试。 |
| | | |
| | | \- \*\*重构优化\*\*:关注架构与性能提升。 |
| | | |
| | | \- \*\*学习探索\*\*:可试验,但需记录总结。 |
| | | |
| | | |
| | | |
| | | --- |
| | | |
| | | |
| | | |
| | | \## 持续改进机制 |
| | | |
| | | 1\. 执行前:选择策略、评估风险。 |
| | | |
| | | 2\. 执行中:记录决策与问题。 |
| | | |
| | | 3\. 执行后:复盘并更新最佳实践。 |
| | | |
| | | 4\. 积累经验、工具技巧与合理例外。 |
| | | |
| | | |
| | | |
| | | --- |
| | | |
| | | |
| | | |
| | | \## 检查点 |
| | | |
| | | \- 遵循质量标准并记录关键变更。 |
| | | |
| | | \- 验证功能与质量,更新测试与文档。 |
| | | |
| | | \- 执行结束后总结改进项。 |
| | | |
| | | |
| | | |
| | | --- |
| | | |
| | | |
| | | |
| | | \## 行为准则 |
| | | |
| | | > 查询胜过猜测,确认胜过假设; |
| | | |
| | | > 复用胜过重复,测试胜过跳过; |
| | | |
| | | > 规范胜过随意,诚实胜过假装; |
| | | |
| | | > 谨慎胜过盲目,学习胜过停滞。 |
| | | |
| | | |
| | | |
| | | --- |
| | | |
| | | |
| | | |
| | | \## 执行要求 |
| | | |
| | | \- 所有 AI 输出必须符合上述标准。 |
| | | |
| | | \- 冲突时以“质量第一”为最高优先级。 |
| | | |
| | | \- 禁止编造数据、忽略边界、生成占位符。 |
| | | |
| | | \- 所有结果必须透明、可解释、可验证。 |
| | | |
| | | |
| | | |
| | |
| | | // 构建文件访问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); |
| | | } |
| New file |
| | |
| | | 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; |
| | | } |
| | | } |
| New file |
| | |
| | | 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; |
| | | } |
| | | } |
| New file |
| | |
| | | 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; |
| | | } |
| | | } |
| New file |
| | |
| | | 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); |
| | | } |
| New file |
| | |
| | | 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); |
| | | } |
| | | } |
| New file |
| | |
| | | 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()); |
| | | } |
| | | } |
| New file |
| | |
| | | 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 |
| | | } |
| New file |
| | |
| | | # 新闻模块说明 |
| | | |
| | | ## 模块概述 |
| | | 新闻模块是一个用于管理系统新闻资讯的功能模块,支持富文本内容编辑和发布管理。 |
| | | |
| | | ## 功能特性 |
| | | - 新闻列表展示和管理 |
| | | - 富文本内容编辑(使用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表的创建语句 |
| | |
| | | "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", |
| | |
| | | "vite": "^5.0.0", |
| | | "vue-tsc": "^1.8.22" |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | // 新闻管理 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 : '更新新闻状态失败') |
| | | } |
| | | } |
| | |
| | | text-color="#666666" |
| | | active-text-color="#1E3A8A" |
| | | > |
| | | > |
| | | <el-menu-item index="/dashboard"> |
| | | <el-icon><House /></el-icon> |
| | | <span>工作台</span> |
| | |
| | | <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> |
| | |
| | | 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'), |
| New file |
| | |
| | | <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> |
| New file |
| | |
| | | <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> |
| New file |
| | |
| | | <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> |
| New file |
| | |
| | | <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> |
| | |
| | | { |
| | | "pages": [ |
| | | "pages/index/index", |
| | | "pages/news/list", |
| | | "pages/news/detail", |
| | | "pages/activity/detail", |
| | | "pages/registration/registration", |
| | | "pages/profile/profile", |
| | |
| | | "audio" |
| | | ], |
| | | "sitemapLocation": "sitemap.json" |
| | | } |
| | | } |
| | |
| | | banners: [], |
| | | // 赛事列表 |
| | | activities: [], |
| | | // 最新新闻列表 |
| | | latestNews: [], |
| | | // 加载状态 |
| | | loading: true, |
| | | // 是否还有更多数据 |
| | |
| | | } |
| | | // 加载数据 |
| | | this.loadBanners() |
| | | this.loadLatestNews() |
| | | this.loadActivities() |
| | | }, |
| | | |
| | |
| | | |
| | | Promise.all([ |
| | | this.loadBanners(), |
| | | this.loadLatestNews(), |
| | | this.loadActivities() |
| | | ]).finally(() => { |
| | | wx.stopPullDownRefresh() |
| | |
| | | } |
| | | }).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) |
| | | }) |
| | | }, |
| | | |
| | |
| | | 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) { |
| | |
| | | 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: '' |
| | | } |
| | | } |
| | | }) |
| | |
| | | <wxs src="./filters.wxs" module="filters" /> |
| | | <!--pages/index/index.wxml--> |
| | | <view class="container"> |
| | | <!-- 页面头部分享区域 --> |
| | | <view class="header-section"> |
| | |
| | | </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> |
| | | |
| | |
| | | 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; |
| New file |
| | |
| | | // 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) |
| | | }) |
| | | } |
| | | }) |
| New file |
| | |
| | | { |
| | | "navigationBarTitleText": "新闻详情" |
| | | } |
| New file |
| | |
| | | <!-- 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> |
| New file |
| | |
| | | /* 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; |
| | | } |
| New file |
| | |
| | | // 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}` |
| | | }) |
| | | } |
| | | } |
| | | }) |
| New file |
| | |
| | | { |
| | | "navigationBarTitleText": "新闻资讯", |
| | | "enablePullDownRefresh": true, |
| | | "backgroundTextStyle": "dark" |
| | | } |
| New file |
| | |
| | | <!-- 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> |
| New file |
| | |
| | | /* 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; |
| | | } |