fix: 公众端广场推荐与超管统计Tab查询对齐设计文档

Made-with: Cursor
This commit is contained in:
zhonghua 2026-04-09 16:29:33 +08:00
parent 67de13c29a
commit 7240c543fc
7 changed files with 300 additions and 61 deletions

View File

@ -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}")

View File

@ -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<String, Long> getStats() {
Map<String, Long> 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<Map<String, Object>> getWorkQueue(int page, int pageSize, String status,
String keyword, String startTime, String endTime,
String reviewStartTime, String reviewEndTime,
String sortBy, Boolean isRecommended) {
LambdaQueryWrapper<UgcWork> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(UgcWork::getIsDeleted, 0);
if (StringUtils.hasText(status)) {
wrapper.eq(UgcWork::getStatus, status);
List<Integer> 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<Long> userIds = sysUserMapper.selectList(new LambdaQueryWrapper<SysUser>()
.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<UgcWork> result = ugcWorkMapper.selectPage(new Page<>(page, pageSize), wrapper);
@ -220,12 +257,20 @@ public class PublicContentReviewService {
*/
public Map<String, Object> getManagementStats() {
Map<String, Object> stats = new LinkedHashMap<>();
stats.put("total", ugcWorkMapper.selectCount(
new LambdaQueryWrapper<UgcWork>().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<UgcWork>()
.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<UgcWork>()
.eq(UgcWork::getIsDeleted, 0)
@ -248,13 +293,121 @@ public class PublicContentReviewService {
return PageResult.from(result);
}
private long countByStatus(String status) {
private void applyWorkQueueSort(LambdaQueryWrapper<UgcWork> 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<Integer> 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<Integer> 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<UgcWork>()
.eq(UgcWork::getIsDeleted, 0)
.eq(UgcWork::getStatus, status));
}
private long countPublishedStatuses() {
return ugcWorkMapper.selectCount(
new LambdaQueryWrapper<UgcWork>()
.eq(UgcWork::getIsDeleted, 0)
.in(UgcWork::getStatus, ST_COMPLETED, ST_CATALOGED, ST_DUBBED));
}
private long countTodayReviewLogs(LocalDateTime dayStart) {
return ugcReviewLogMapper.selectCount(
new LambdaQueryWrapper<UgcReviewLog>()
.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<UgcReviewLog>()
.eq(UgcReviewLog::getTargetType, "work")
.eq(UgcReviewLog::getAction, action)
.ge(UgcReviewLog::getCreateTime, dayStart));
}
private long sumAllViewCounts() {
List<Map<String, Object>> maps = ugcWorkMapper.selectMaps(
new QueryWrapper<UgcWork>()
.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;

View File

@ -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<Map<String, Object>> getGalleryList(int page, int pageSize, Long tagId,
String category, String sortBy, String keyword) {
LambdaQueryWrapper<UgcWork> 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<Map<String, Object>> getRecommended() {
LambdaQueryWrapper<UgcWork> 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<Map<String, Object>> getUserPublicWorks(Long userId, int page, int pageSize) {
LambdaQueryWrapper<UgcWork> 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<String, Object> 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;

View File

@ -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 社区互动模型(新增)
```

View File

@ -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),标签颜色)

View File

@ -24,9 +24,15 @@
<!-- 筛选 -->
<div class="filter-bar">
<a-form layout="inline" @finish="handleSearch">
<a-form layout="inline" :model="filterFormModel" @finish="handleSearch">
<a-form-item label="作品/作者">
<a-input v-model:value="keyword" placeholder="作品名称或作者" allow-clear style="width: 180px" />
<a-input
v-model:value="keyword"
placeholder="作品名称或作者"
allow-clear
style="width: 180px"
@press-enter="handleSearch"
/>
</a-form-item>
<a-form-item label="状态">
<a-select v-model:value="filterStatus" style="width: 120px" @change="handleSearch">
@ -45,7 +51,7 @@
</a-form-item>
<a-form-item>
<a-space>
<a-button type="primary" html-type="submit"><template #icon><SearchOutlined /></template>搜索</a-button>
<a-button type="primary" @click="handleSearch"><template #icon><SearchOutlined /></template>搜索</a-button>
<a-button @click="handleReset"><template #icon><ReloadOutlined /></template>重置</a-button>
</a-space>
</a-form-item>
@ -81,7 +87,7 @@
{{ record.isRecommended ? '取消推荐' : '推荐' }}
</a-button>
<a-button v-if="record.status === 'published'" type="link" danger size="small" @click="openTakedown(record)">下架</a-button>
<a-button v-else type="link" size="small" style="color: #10b981" @click="handleRestore(record)">恢复</a-button>
<a-button v-else type="link" size="small" style="color: #10b981" @click="handleRestore(record)">恢复上架</a-button>
</a-space>
</template>
</template>
@ -210,6 +216,9 @@ import {
import request from '@/utils/request'
import dayjs from 'dayjs'
/** 供 a-form :model 使用,避免无 model 时 @finish/提交不触发 */
const filterFormModel = reactive<Record<string, unknown>>({})
const loading = ref(false)
const dataSource = ref<any[]>([])
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<string, unknown> = {
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('操作失败') }
}

View File

@ -19,7 +19,7 @@
<!-- 筛选 -->
<div class="filter-bar">
<a-form layout="inline" @finish="handleSearch">
<a-form layout="inline" :model="filterFormModel" @finish="handleSearch">
<a-form-item label="审核状态">
<a-select v-model:value="searchStatus" style="width: 120px" @change="handleSearch">
<a-select-option value="">全部</a-select-option>
@ -29,11 +29,17 @@
</a-select>
</a-form-item>
<a-form-item label="作者">
<a-input v-model:value="searchKeyword" placeholder="作者昵称" allow-clear style="width: 150px" />
<a-input
v-model:value="searchKeyword"
placeholder="作者昵称"
allow-clear
style="width: 150px"
@press-enter="handleSearch"
/>
</a-form-item>
<a-form-item>
<a-space>
<a-button type="primary" html-type="submit"><template #icon><SearchOutlined /></template>搜索</a-button>
<a-button type="primary" @click="handleSearch"><template #icon><SearchOutlined /></template>搜索</a-button>
<a-button @click="handleReset"><template #icon><ReloadOutlined /></template>重置</a-button>
</a-space>
</a-form-item>
@ -212,6 +218,9 @@ import {
import request from '@/utils/request'
import dayjs from 'dayjs'
/** 供 a-form :model 使用,避免无 model 时 @finish/提交不触发 */
const filterFormModel = reactive<Record<string, unknown>>({})
const loading = ref(false)
const dataSource = ref<any[]>([])
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<string, string> = { pending_review: 'orange', published: 'green', rejected: 'red', taken_down: 'default' }
const statusText: Record<string, string> = { pending_review: '待审核', published: '已通过', rejected: '已拒绝', taken_down: '已下架', draft: '草稿' }
const statusColor: Record<string, string> = { pending_review: 'orange', processing: 'blue', published: 'green', rejected: 'red', taken_down: 'default', draft: 'default' }
const statusText: Record<string, string> = { pending_review: '待审核', processing: '生成中', published: '已通过', rejected: '已拒绝', taken_down: '已下架', draft: '草稿' }
const logActionText: Record<string, string> = { approve: '审核通过', reject: '审核拒绝', takedown: '下架', restore: '恢复', revoke: '撤销审核' }
const logActionColor: Record<string, string> = { 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<string, unknown> = {
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 = ''