diff --git a/backend-java/src/main/java/com/competition/modules/pub/controller/ContentReviewController.java b/backend-java/src/main/java/com/competition/modules/pub/controller/ContentReviewController.java index 3cbd105..9c30c0d 100644 --- a/backend-java/src/main/java/com/competition/modules/pub/controller/ContentReviewController.java +++ b/backend-java/src/main/java/com/competition/modules/pub/controller/ContentReviewController.java @@ -8,6 +8,7 @@ import com.competition.modules.ugc.entity.UgcReviewLog; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; +import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.*; import java.util.List; @@ -36,10 +37,16 @@ public class ContentReviewController { @RequestParam(required = false) String keyword, @RequestParam(required = false) String startTime, @RequestParam(required = false) String endTime, + @RequestParam(required = false) String reviewStartTime, + @RequestParam(required = false) String reviewEndTime, @RequestParam(required = false) String sortBy, - @RequestParam(required = false) Boolean isRecommended) { + @RequestParam(required = false) String isRecommended) { + Boolean recommended = null; + if (StringUtils.hasText(isRecommended)) { + recommended = "1".equals(isRecommended) || "true".equalsIgnoreCase(isRecommended); + } return Result.success(publicContentReviewService.getWorkQueue( - page, pageSize, status, keyword, startTime, endTime, sortBy, isRecommended)); + page, pageSize, status, keyword, startTime, endTime, reviewStartTime, reviewEndTime, sortBy, recommended)); } @GetMapping("/works/{id}") diff --git a/backend-java/src/main/java/com/competition/modules/pub/service/PublicContentReviewService.java b/backend-java/src/main/java/com/competition/modules/pub/service/PublicContentReviewService.java index b7b8cab..3037c1d 100644 --- a/backend-java/src/main/java/com/competition/modules/pub/service/PublicContentReviewService.java +++ b/backend-java/src/main/java/com/competition/modules/pub/service/PublicContentReviewService.java @@ -1,9 +1,9 @@ package com.competition.modules.pub.service; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; -import com.competition.common.enums.WorkStatus; import com.competition.common.exception.BusinessException; import com.competition.common.result.PageResult; import com.competition.modules.sys.entity.SysUser; @@ -18,8 +18,11 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; +import java.time.LocalDate; import java.time.LocalDateTime; import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; @Slf4j @Service @@ -30,15 +33,30 @@ public class PublicContentReviewService { private final UgcReviewLogMapper ugcReviewLogMapper; private final SysUserMapper sysUserMapper; + /** 与 UgcWork.status 整型及写库逻辑一致 */ + private static final int ST_DRAFT = 0; + private static final int ST_PENDING = 1; + private static final int ST_PROCESSING = 2; + private static final int ST_COMPLETED = 3; + private static final int ST_CATALOGED = 4; + private static final int ST_DUBBED = 5; + private static final int ST_REJECTED = -1; + private static final int ST_TAKEN_DOWN = -2; + /** - * 获取各状态统计 + * 获取各状态统计(作品审核页:待审核数 + 今日审核维度) */ public Map getStats() { Map stats = new LinkedHashMap<>(); - stats.put("pending_review", countByStatus("pending_review")); - stats.put("published", countByStatus("published")); - stats.put("rejected", countByStatus(WorkStatus.REJECTED.getValue())); - stats.put("taken_down", countByStatus(WorkStatus.TAKEN_DOWN.getValue())); + stats.put("pending", countByStatusInt(ST_PENDING)); + stats.put("pending_review", countByStatusInt(ST_PENDING)); + stats.put("published", countPublishedStatuses()); + stats.put("rejected", countByStatusInt(ST_REJECTED)); + stats.put("taken_down", countByStatusInt(ST_TAKEN_DOWN)); + LocalDateTime dayStart = LocalDate.now().atStartOfDay(); + stats.put("todayReviewed", countTodayReviewLogs(dayStart)); + stats.put("todayApproved", countTodayLogsByAction("approve", dayStart)); + stats.put("todayRejected", countTodayLogsByAction("reject", dayStart)); return stats; } @@ -47,14 +65,31 @@ public class PublicContentReviewService { */ public PageResult> getWorkQueue(int page, int pageSize, String status, String keyword, String startTime, String endTime, + String reviewStartTime, String reviewEndTime, String sortBy, Boolean isRecommended) { LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.eq(UgcWork::getIsDeleted, 0); if (StringUtils.hasText(status)) { - wrapper.eq(UgcWork::getStatus, status); + List codes = parseStatusFilter(status); + if (codes.isEmpty()) { + wrapper.eq(UgcWork::getStatus, Integer.MIN_VALUE); + } else { + wrapper.in(UgcWork::getStatus, codes); + } } if (StringUtils.hasText(keyword)) { - wrapper.like(UgcWork::getTitle, keyword); + List userIds = sysUserMapper.selectList(new LambdaQueryWrapper() + .select(SysUser::getId) + .like(SysUser::getNickname, keyword)) + .stream() + .map(SysUser::getId) + .collect(Collectors.toList()); + wrapper.and(w -> { + w.like(UgcWork::getTitle, keyword); + if (!userIds.isEmpty()) { + w.or().in(UgcWork::getUserId, userIds); + } + }); } if (StringUtils.hasText(startTime)) { wrapper.ge(UgcWork::getCreateTime, LocalDateTime.parse(startTime)); @@ -62,15 +97,17 @@ public class PublicContentReviewService { if (StringUtils.hasText(endTime)) { wrapper.le(UgcWork::getCreateTime, LocalDateTime.parse(endTime)); } + if (StringUtils.hasText(reviewStartTime)) { + wrapper.ge(UgcWork::getReviewTime, LocalDateTime.parse(reviewStartTime)); + } + if (StringUtils.hasText(reviewEndTime)) { + wrapper.le(UgcWork::getReviewTime, LocalDateTime.parse(reviewEndTime)); + } if (isRecommended != null) { wrapper.eq(UgcWork::getIsRecommended, isRecommended); } - if ("oldest".equals(sortBy)) { - wrapper.orderByAsc(UgcWork::getCreateTime); - } else { - wrapper.orderByDesc(UgcWork::getCreateTime); - } + applyWorkQueueSort(wrapper, sortBy); IPage result = ugcWorkMapper.selectPage(new Page<>(page, pageSize), wrapper); @@ -220,12 +257,20 @@ public class PublicContentReviewService { */ public Map getManagementStats() { Map stats = new LinkedHashMap<>(); - stats.put("total", ugcWorkMapper.selectCount( - new LambdaQueryWrapper().eq(UgcWork::getIsDeleted, 0))); - stats.put("pendingReview", countByStatus("pending_review")); - stats.put("published", countByStatus("published")); - stats.put("rejected", countByStatus(WorkStatus.REJECTED.getValue())); - stats.put("takenDown", countByStatus(WorkStatus.TAKEN_DOWN.getValue())); + // 与作品管理列表默认筛选 status=published,taken_down 一致(已上架/已编目/已配音 + 已下架),不含草稿/待审等 + stats.put("total", countPublishedStatuses() + countByStatusInt(ST_TAKEN_DOWN)); + LocalDateTime dayStart = LocalDate.now().atStartOfDay(); + // 与作品管理「今日新增」Tab 列表一致:当日创建且为已上架(3/4/5)或已下架(-2) + stats.put("todayNew", ugcWorkMapper.selectCount( + new LambdaQueryWrapper() + .eq(UgcWork::getIsDeleted, 0) + .ge(UgcWork::getCreateTime, dayStart) + .in(UgcWork::getStatus, ST_COMPLETED, ST_CATALOGED, ST_DUBBED, ST_TAKEN_DOWN))); + stats.put("totalViews", sumAllViewCounts()); + stats.put("pendingReview", countByStatusInt(ST_PENDING)); + stats.put("published", countPublishedStatuses()); + stats.put("rejected", countByStatusInt(ST_REJECTED)); + stats.put("takenDown", countByStatusInt(ST_TAKEN_DOWN)); stats.put("recommended", ugcWorkMapper.selectCount( new LambdaQueryWrapper() .eq(UgcWork::getIsDeleted, 0) @@ -248,13 +293,121 @@ public class PublicContentReviewService { return PageResult.from(result); } - private long countByStatus(String status) { + private void applyWorkQueueSort(LambdaQueryWrapper wrapper, String sortBy) { + if ("hot".equals(sortBy)) { + wrapper.orderByDesc(UgcWork::getLikeCount); + } else if ("views".equals(sortBy)) { + wrapper.orderByDesc(UgcWork::getViewCount); + } else if ("oldest".equals(sortBy)) { + wrapper.orderByAsc(UgcWork::getCreateTime); + } else if ("latest".equals(sortBy) || !StringUtils.hasText(sortBy)) { + wrapper.orderByDesc(UgcWork::getPublishTime); + wrapper.orderByDesc(UgcWork::getCreateTime); + } else { + wrapper.orderByDesc(UgcWork::getCreateTime); + } + } + + /** + * 解析筛选参数:单状态或逗号分隔多状态(如 published,taken_down) + */ + private List parseStatusFilter(String status) { + String[] parts = status.split(","); + return Stream.of(parts) + .map(String::trim) + .filter(StringUtils::hasText) + .flatMap(token -> resolveStatusCodes(token).stream()) + .distinct() + .collect(Collectors.toList()); + } + + private List resolveStatusCodes(String token) { + switch (token) { + case "pending_review": + return List.of(ST_PENDING); + case "published": + return List.of(ST_COMPLETED, ST_CATALOGED, ST_DUBBED); + case "rejected": + return List.of(ST_REJECTED); + case "taken_down": + return List.of(ST_TAKEN_DOWN); + case "draft": + return List.of(ST_DRAFT); + case "processing": + return List.of(ST_PROCESSING); + default: + return Collections.emptyList(); + } + } + + private long countByStatusInt(int status) { return ugcWorkMapper.selectCount( new LambdaQueryWrapper() .eq(UgcWork::getIsDeleted, 0) .eq(UgcWork::getStatus, status)); } + private long countPublishedStatuses() { + return ugcWorkMapper.selectCount( + new LambdaQueryWrapper() + .eq(UgcWork::getIsDeleted, 0) + .in(UgcWork::getStatus, ST_COMPLETED, ST_CATALOGED, ST_DUBBED)); + } + + private long countTodayReviewLogs(LocalDateTime dayStart) { + return ugcReviewLogMapper.selectCount( + new LambdaQueryWrapper() + .eq(UgcReviewLog::getTargetType, "work") + .in(UgcReviewLog::getAction, "approve", "reject") + .ge(UgcReviewLog::getCreateTime, dayStart)); + } + + private long countTodayLogsByAction(String action, LocalDateTime dayStart) { + return ugcReviewLogMapper.selectCount( + new LambdaQueryWrapper() + .eq(UgcReviewLog::getTargetType, "work") + .eq(UgcReviewLog::getAction, action) + .ge(UgcReviewLog::getCreateTime, dayStart)); + } + + private long sumAllViewCounts() { + List> maps = ugcWorkMapper.selectMaps( + new QueryWrapper() + .select("IFNULL(SUM(view_count),0) AS sv") + .eq("is_deleted", 0)); + if (maps.isEmpty() || maps.get(0).get("sv") == null) { + return 0L; + } + return ((Number) maps.get(0).get("sv")).longValue(); + } + + /** + * 将作品状态整型转为前端使用的字符串码(与 WorkReview / WorkManagement 一致) + */ + private String mapWorkStatusToString(Integer code) { + if (code == null) { + return "draft"; + } + switch (code) { + case ST_DRAFT: + return "draft"; + case ST_PENDING: + return "pending_review"; + case ST_PROCESSING: + return "processing"; + case ST_COMPLETED: + case ST_CATALOGED: + case ST_DUBBED: + return "published"; + case ST_REJECTED: + return "rejected"; + case ST_TAKEN_DOWN: + return "taken_down"; + default: + return "draft"; + } + } + private void createLog(Long workId, String action, String reason, String note, Long operatorId) { UgcReviewLog logEntry = new UgcReviewLog(); logEntry.setTargetType("work"); @@ -274,7 +427,9 @@ public class PublicContentReviewService { vo.put("title", work.getTitle()); vo.put("coverUrl", work.getCoverUrl()); vo.put("description", work.getDescription()); - vo.put("status", work.getStatus()); + Integer statusCode = work.getStatus(); + vo.put("statusCode", statusCode); + vo.put("status", mapWorkStatusToString(statusCode)); vo.put("visibility", work.getVisibility()); vo.put("isRecommended", work.getIsRecommended()); vo.put("viewCount", work.getViewCount()); @@ -285,7 +440,7 @@ public class PublicContentReviewService { vo.put("publishTime", work.getPublishTime()); vo.put("createTime", work.getCreateTime()); - // 补充作者信息 + // 补充作者信息(user 与 creator 同构,便于前端组件复用) if (work.getUserId() != null) { SysUser user = sysUserMapper.selectById(work.getUserId()); if (user != null) { @@ -295,6 +450,7 @@ public class PublicContentReviewService { userInfo.put("nickname", user.getNickname()); userInfo.put("avatar", user.getAvatar()); vo.put("user", userInfo); + vo.put("creator", userInfo); } } return vo; diff --git a/backend-java/src/main/java/com/competition/modules/pub/service/PublicGalleryService.java b/backend-java/src/main/java/com/competition/modules/pub/service/PublicGalleryService.java index 412e49e..d7c10a5 100644 --- a/backend-java/src/main/java/com/competition/modules/pub/service/PublicGalleryService.java +++ b/backend-java/src/main/java/com/competition/modules/pub/service/PublicGalleryService.java @@ -4,7 +4,6 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; -import com.competition.common.enums.PublishStatus; import com.competition.common.enums.Visibility; import com.competition.common.exception.BusinessException; import com.competition.common.result.PageResult; @@ -26,6 +25,11 @@ import java.util.*; @RequiredArgsConstructor public class PublicGalleryService { + /** 与 PublicContentReviewService、UgcWork.status 一致:已上架可对外的作品 */ + private static final int ST_COMPLETED = 3; + private static final int ST_CATALOGED = 4; + private static final int ST_DUBBED = 5; + private final UgcWorkMapper ugcWorkMapper; private final UgcWorkPageMapper ugcWorkPageMapper; private final SysUserMapper sysUserMapper; @@ -36,7 +40,7 @@ public class PublicGalleryService { public PageResult> getGalleryList(int page, int pageSize, Long tagId, String category, String sortBy, String keyword) { LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); - wrapper.eq(UgcWork::getStatus, PublishStatus.PUBLISHED.getValue()) + wrapper.in(UgcWork::getStatus, ST_COMPLETED, ST_CATALOGED, ST_DUBBED) .eq(UgcWork::getVisibility, Visibility.PUBLIC.getValue()) .eq(UgcWork::getIsDeleted, 0); if (StringUtils.hasText(keyword)) { @@ -70,7 +74,7 @@ public class PublicGalleryService { public List> getRecommended() { LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.eq(UgcWork::getIsRecommended, true) - .eq(UgcWork::getStatus, PublishStatus.PUBLISHED.getValue()) + .in(UgcWork::getStatus, ST_COMPLETED, ST_CATALOGED, ST_DUBBED) .eq(UgcWork::getVisibility, Visibility.PUBLIC.getValue()) .eq(UgcWork::getIsDeleted, 0) .orderByDesc(UgcWork::getPublishTime) @@ -111,6 +115,7 @@ public class PublicGalleryService { if (user != null) { userInfo = new LinkedHashMap<>(); userInfo.put("id", user.getId()); + userInfo.put("username", user.getUsername()); userInfo.put("nickname", user.getNickname()); userInfo.put("avatar", user.getAvatar()); } @@ -130,6 +135,7 @@ public class PublicGalleryService { result.put("publishTime", work.getPublishTime()); result.put("pages", pages); result.put("user", userInfo); + result.put("creator", userInfo); return result; } @@ -139,7 +145,7 @@ public class PublicGalleryService { public PageResult> getUserPublicWorks(Long userId, int page, int pageSize) { LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.eq(UgcWork::getUserId, userId) - .eq(UgcWork::getStatus, PublishStatus.PUBLISHED.getValue()) + .in(UgcWork::getStatus, ST_COMPLETED, ST_CATALOGED, ST_DUBBED) .eq(UgcWork::getVisibility, Visibility.PUBLIC.getValue()) .eq(UgcWork::getIsDeleted, 0) .orderByDesc(UgcWork::getPublishTime); @@ -171,9 +177,11 @@ public class PublicGalleryService { if (user != null) { Map userInfo = new LinkedHashMap<>(); userInfo.put("id", user.getId()); + userInfo.put("username", user.getUsername()); userInfo.put("nickname", user.getNickname()); userInfo.put("avatar", user.getAvatar()); vo.put("user", userInfo); + vo.put("creator", userInfo); } } return vo; diff --git a/docs/design/public/ugc-platform-upgrade.md b/docs/design/public/ugc-platform-upgrade.md index e7d9fa7..cea5035 100644 --- a/docs/design/public/ugc-platform-upgrade.md +++ b/docs/design/public/ugc-platform-upgrade.md @@ -3,7 +3,7 @@ > 所属端:用户端(公众端)+ 超管端 > 状态:需求已确认,待拆分开发计划 > 创建日期:2026-03-27 -> 最后更新:2026-03-27 +> 最后更新:2026-04-09 --- @@ -118,6 +118,8 @@ UserWork(用户作品) └── isDeleted ``` +**实现说明(与库表对齐)**:当前后端表 `t_ugc_work.status` 为 **整型**(如 `0` 草稿、`1` 待审、`3` 生成完成、`4` 已编目、`5` 已配音、`-1` 已拒绝、`-2` 已下架)。产品文档中的 **`published`(已发布/可对外展示)** 在查询与 API 筛选语义上对应 **`status ∈ {3, 4, 5}`**,且需配合 `visibility=public`、未删除;公众端广场与推荐接口按此过滤。勿与竞赛/作业等业务里使用的字符串型 `PublishStatus` 混用于 UGC 作品表。 + ### 2.3 社区互动模型(新增) ``` diff --git a/docs/design/super-admin/content-management.md b/docs/design/super-admin/content-management.md index ffa3fc7..fcfb2bd 100644 --- a/docs/design/super-admin/content-management.md +++ b/docs/design/super-admin/content-management.md @@ -3,7 +3,7 @@ > 所属端:超管端 > 状态:P0 已实现并优化 > 创建日期:2026-03-27 -> 最后更新:2026-03-31 +> 最后更新:2026-04-09 --- @@ -357,6 +357,7 @@ POST /api/content-review/reports/:id/handle — 处理举报 - [x] 撤销机制:已通过/已拒绝的作品支持撤销恢复为待审核(操作列常驻按钮+二次确认) - [x] 操作日志:详情 Drawer 底部展示审核操作时间线(通过/拒绝/下架/恢复/撤销) - [x] 体验优化:默认筛选待审核、表格加描述预览列+审核时间列、详情加「上一个/下一个」导航(审核完自动跳下一个)、统计卡片点击筛选、筛选下拉自动查询 +- [x] 统计 Tab 与列表一致:「今日已审/今日通过/今日拒绝」按当日 `review_time` 区间查询(`reviewStartTime`/`reviewEndTime`);「待审核」仅 `pending_review` #### 作品管理 - [x] 基础功能:统计卡片、筛选(关键词+状态+排序)、作品表格、推荐/下架/恢复操作 @@ -365,6 +366,7 @@ POST /api/content-review/reports/:id/handle — 处理举报 - [x] 详情 Drawer:补全作品描述、标签、绘本预览、操作按钮(推荐/下架/恢复)、操作日志 - [x] 推荐联动:推荐作品在公众端广场顶部「编辑推荐」横栏展示,下架时自动取消推荐 - [x] 体验优化:统计卡片可点击筛选、表格加描述预览列、取消推荐二次确认、筛选自动查询 +- [x] 统计 Tab 与列表一致:「今日新增」按当日 `create_time` 区间(`startTime`/`endTime`)且含已上架+已下架;「总作品数」为全量;「累计浏览」列表按浏览量排序;「已下架」仅下架状态 #### 标签管理 - [x] 基础功能:标签 CRUD、启用/禁用、删除保护(已使用不可删) @@ -380,10 +382,12 @@ POST /api/content-review/reports/:id/handle — 处理举报 POST /api/content-review/works/batch-approve — 批量通过 POST /api/content-review/works/batch-reject — 批量拒绝 POST /api/content-review/works/:id/revoke — 撤销审核 -GET /api/content-review/works (新增参数) — sortBy 排序 + isRecommended 筛选 +GET /api/content-review/works (新增参数) — sortBy 排序 + isRecommended 筛选;startTime/endTime 按创建时间;reviewStartTime/reviewEndTime 按审核时间 GET /api/public/gallery/recommended — 推荐作品列表(公众端) POST /api/tags/batch-sort — 标签批量排序 ``` +**公众端广场与 `status`(实现约定)**:`t_ugc_work.status` 为整型;管理端筛选参数里的 `published` 语义对应 **`status ∈ {3, 4, 5}`**(生成完成 / 已编目 / 已配音)。公众端 `GET /public/gallery`、`/public/gallery/recommended` 仅查询上述状态且 `visibility=public`、未删除;**不得**复用赛事/作业等模块的 `PublishStatus.PUBLISHED`(字符串 `"published"`)与整型列比较。 + #### 数据库变更 - `work_tags` 表新增 `color` 字段(VARCHAR(20),标签颜色) diff --git a/frontend/src/views/content/WorkManagement.vue b/frontend/src/views/content/WorkManagement.vue index 1dbe71b..d5ccc27 100644 --- a/frontend/src/views/content/WorkManagement.vue +++ b/frontend/src/views/content/WorkManagement.vue @@ -24,9 +24,15 @@
- + - + @@ -45,7 +51,7 @@ - 搜索 + 搜索 重置 @@ -81,7 +87,7 @@ {{ record.isRecommended ? '取消推荐' : '推荐' }} 下架 - 恢复 + 恢复上架 @@ -210,6 +216,9 @@ import { import request from '@/utils/request' import dayjs from 'dayjs' +/** 供 a-form :model 使用,避免无 model 时 @finish/提交不触发 */ +const filterFormModel = reactive>({}) + const loading = ref(false) const dataSource = ref([]) const pagination = reactive({ current: 1, pageSize: 10, total: 0, showSizeChanger: true, showTotal: (t: number) => `共 ${t} 条` }) @@ -268,33 +277,52 @@ const fetchStats = async () => { const fetchList = async () => { loading.value = true try { - const isRecommendedFilter = filterStatus.value === 'recommended' - const res: any = await request.get('/content-review/works', { - params: { - page: pagination.current, - pageSize: pagination.pageSize, - status: isRecommendedFilter ? 'published' : (filterStatus.value || 'published,taken_down'), - keyword: keyword.value || undefined, - sortBy: sortBy.value, - isRecommended: isRecommendedFilter ? '1' : undefined, - }, - }) + const dayStart = dayjs().startOf('day').format('YYYY-MM-DDTHH:mm:ss') + const dayEnd = dayjs().endOf('day').format('YYYY-MM-DDTHH:mm:ss') + const params: Record = { + page: pagination.current, + pageSize: pagination.pageSize, + keyword: keyword.value || undefined, + } + const key = activeStatKey.value + // 统计卡片优先:与设计「点击卡片筛选列表」一致 + if (key === 'todayNew') { + params.startTime = dayStart + params.endTime = dayEnd + params.status = 'published,taken_down' + params.sortBy = sortBy.value + } else if (key === 'takenDown') { + params.status = 'taken_down' + params.sortBy = sortBy.value + } else if (key === 'totalViews') { + params.status = 'published,taken_down' + params.sortBy = 'views' + } else if (key === 'total') { + params.status = 'published,taken_down' + params.sortBy = sortBy.value + } else { + const isRecommendedFilter = filterStatus.value === 'recommended' + params.sortBy = sortBy.value + if (isRecommendedFilter) { + params.status = 'published' + params.isRecommended = '1' + } else { + params.status = filterStatus.value || 'published,taken_down' + } + } + const res: any = await request.get('/content-review/works', { params }) dataSource.value = res.list pagination.total = res.total } catch { message.error('获取失败') } finally { loading.value = false } } -// #7 统计卡片点击筛选 +// #7 统计卡片点击筛选(与 fetchList 中 activeStatKey 分支联动,不再改写 filterStatus) const handleStatClick = (key: string) => { if (activeStatKey.value === key) { activeStatKey.value = '' - filterStatus.value = '' } else { activeStatKey.value = key - if (key === 'takenDown') filterStatus.value = 'taken_down' - else if (key === 'total' || key === 'todayNew') filterStatus.value = 'published' - else filterStatus.value = '' } pagination.current = 1 fetchList() @@ -384,7 +412,7 @@ const handleRestore = async (record: any) => { const handleRestoreInDrawer = async () => { const id = detailData.value?.id if (!id) return - try { await request.post(`/content-review/works/${id}/restore`); message.success('已恢复'); fetchList(); fetchStats(); showDetail(id) } + try { await request.post(`/content-review/works/${id}/restore`); message.success('已恢复上架'); fetchList(); fetchStats(); showDetail(id) } catch { message.error('操作失败') } } diff --git a/frontend/src/views/content/WorkReview.vue b/frontend/src/views/content/WorkReview.vue index 89bdecb..145db11 100644 --- a/frontend/src/views/content/WorkReview.vue +++ b/frontend/src/views/content/WorkReview.vue @@ -19,7 +19,7 @@
- + 全部 @@ -29,11 +29,17 @@ - + - 搜索 + 搜索 重置 @@ -212,6 +218,9 @@ import { import request from '@/utils/request' import dayjs from 'dayjs' +/** 供 a-form :model 使用,避免无 model 时 @finish/提交不触发 */ +const filterFormModel = reactive>({}) + const loading = ref(false) const dataSource = ref([]) const pagination = reactive({ current: 1, pageSize: 10, total: 0, showSizeChanger: true, showTotal: (t: number) => `共 ${t} 条` }) @@ -233,8 +242,8 @@ const statsItems = computed(() => [ { key: 'rejected', label: '今日拒绝', count: stats.value.todayRejected, icon: CloseCircleOutlined, color: '#ef4444', bgColor: 'rgba(239,68,68,0.1)' }, ]) -const statusColor: Record = { pending_review: 'orange', published: 'green', rejected: 'red', taken_down: 'default' } -const statusText: Record = { pending_review: '待审核', published: '已通过', rejected: '已拒绝', taken_down: '已下架', draft: '草稿' } +const statusColor: Record = { pending_review: 'orange', processing: 'blue', published: 'green', rejected: 'red', taken_down: 'default', draft: 'default' } +const statusText: Record = { pending_review: '待审核', processing: '生成中', published: '已通过', rejected: '已拒绝', taken_down: '已下架', draft: '草稿' } const logActionText: Record = { approve: '审核通过', reject: '审核拒绝', takedown: '下架', restore: '恢复', revoke: '撤销审核' } const logActionColor: Record = { approve: 'green', reject: 'red', takedown: 'gray', restore: 'blue', revoke: 'orange' } const formatDate = (d: string) => d ? dayjs(d).format('YYYY-MM-DD HH:mm') : '-' @@ -348,16 +357,40 @@ const fetchStats = async () => { const fetchList = async () => { loading.value = true try { - const res: any = await request.get('/content-review/works', { - params: { page: pagination.current, pageSize: pagination.pageSize, status: searchStatus.value || undefined, keyword: searchKeyword.value || undefined }, - }) + const dayStart = dayjs().startOf('day').format('YYYY-MM-DDTHH:mm:ss') + const dayEnd = dayjs().endOf('day').format('YYYY-MM-DDTHH:mm:ss') + const params: Record = { + page: pagination.current, + pageSize: pagination.pageSize, + keyword: searchKeyword.value || undefined, + } + const af = activeFilter.value + // 统计卡片:今日维度按审核时间 review_time,与后端 today* 统计口径一致 + if (af === 'pending_review') { + params.status = 'pending_review' + } else if (af === 'today') { + params.status = 'published,rejected,taken_down' + params.reviewStartTime = dayStart + params.reviewEndTime = dayEnd + } else if (af === 'approved') { + params.status = 'published' + params.reviewStartTime = dayStart + params.reviewEndTime = dayEnd + } else if (af === 'rejected') { + params.status = 'rejected' + params.reviewStartTime = dayStart + params.reviewEndTime = dayEnd + } else { + params.status = searchStatus.value || undefined + } + const res: any = await request.get('/content-review/works', { params }) dataSource.value = res.list pagination.total = res.total } catch { message.error('获取失败') } finally { loading.value = false } } -// #6 今日已审点击 → 展示今日审核过的(通过+拒绝) +// #6 今日已审 / 今日通过 / 今日拒绝:与 fetchList 中 activeFilter + reviewStartTime/End 联动 const handleStatClick = (key: string) => { if (activeFilter.value === key) { activeFilter.value = '' @@ -365,6 +398,7 @@ const handleStatClick = (key: string) => { } else { activeFilter.value = key if (key === 'pending_review') searchStatus.value = 'pending_review' + else if (key === 'today') searchStatus.value = '' else if (key === 'approved') searchStatus.value = 'published' else if (key === 'rejected') searchStatus.value = 'rejected' else searchStatus.value = ''