feat: 完成小说漫剧历史记录模块开发

This commit is contained in:
2026-02-24 21:07:31 +08:00
parent 05d3cc539d
commit 5f2ff36e2c
7 changed files with 276 additions and 26 deletions

View File

@@ -56,3 +56,11 @@ export const getNovelComicHistory = (params = {}) => {
}
})
}
// 删除历史记录
export const deleteNovelComicHistory = (id) => {
return request({
url: `/novel-comic/history/${id}`,
method: 'DELETE'
})
}

View File

@@ -17,7 +17,7 @@ export const getMyWorksByType = (workType, params = {}) => {
return api.get('/works/my-works', {
params: {
page: params.page || 0,
size: params.size || 1000,
size: params.size || 20,
includeProcessing: true,
workType: workType // TEXT_TO_VIDEO, IMAGE_TO_VIDEO, STORYBOARD_VIDEO, STORYBOARD_IMAGE
}

View File

@@ -448,7 +448,7 @@ const isVerticalVideo = computed(() => {
})
const page = ref(1)
const pageSize = ref(100)
const pageSize = ref(20)
const loading = ref(false)
const hasMore = ref(true)
const items = ref([])
@@ -502,10 +502,10 @@ const setupVirtualObserver = () => {
if (changed) visibleItemIds.value = next
},
{
root: contentAreaRef.value,
rootMargin: '75% 0px', // 上下 75% 视口高度的缓冲区
threshold: 0
}
root: contentAreaRef.value,
rootMargin: '50% 0px', // 上下 50% 视口高度的缓冲区
threshold: 0
}
)
// 观察已注册的元素
@@ -665,6 +665,8 @@ const loadList = async () => {
if (response.data.success) {
const data = response.data.data || []
const totalPages = response.data.totalPages || 0
const currentPage = response.data.currentPage || 0
// 转换数据格式
const transformedData = data
@@ -673,7 +675,11 @@ const loadList = async () => {
if (page.value === 1) items.value = []
items.value = items.value.concat(transformedData)
hasMore.value = data.length === pageSize.value
// 使用后端返回的总页数来判断是否还有更多数据
hasMore.value = currentPage < totalPages - 1
console.log('[MyWorks] 分页信息 - 当前页:', currentPage, '总页数:', totalPages, '是否有更多:', hasMore.value)
// 检查是否有处理中的任务,如果有则启动轮询
checkAndStartPolling()

View File

@@ -9,8 +9,8 @@
<div class="left-panel-content">
<CreationTabs active="novel-comic" />
<div class="form-card">
<h3 class="form-card-title">创作设置</h3>
<div>
<h3 class="form-section-title">创作设置</h3>
<!-- 主题选填 -->
<div class="form-group">
@@ -22,7 +22,7 @@
v-model:value="form.theme"
placeholder="例如:都市奇幻、校园恋爱、科幻冒险..."
:maxlength="100"
show-count
@input="checkThemeLength"
/>
</div>
@@ -37,7 +37,7 @@
placeholder="描述故事发生的世界观、时代背景、环境设定等..."
:rows="4"
:maxlength="2000"
show-count
@input="checkStoryBackgroundLength"
/>
</div>
@@ -84,7 +84,7 @@
placeholder="输入完整的故事文案/剧本内容AI 将基于此生成漫剧..."
:rows="8"
:maxlength="10000"
show-count
@input="checkStoryScriptLength"
/>
</div>
@@ -163,35 +163,64 @@
</div>
<!-- 历史记录 -->
<div class="history-card" v-if="historyList.length > 0">
<div class="history-card">
<h3 class="form-card-title">历史记录</h3>
<div class="history-list">
<div class="history-list" v-if="historyList.length > 0">
<div
v-for="item in historyList"
:key="item.id || item.taskId"
class="history-item"
@click="viewHistoryItem(item)"
>
<div class="history-item-top">
<span class="history-status" :class="getHistoryStatusClass(item.status)">
{{ getHistoryStatusText(item.status) }}
</span>
<span class="history-date">{{ formatDate(item.createdAt) }}</span>
<span class="history-date">{{ formatFriendlyDate(item.createdAt) }}</span>
</div>
<div class="history-theme" v-if="item.theme">{{ item.theme }}</div>
<div class="history-preview-text">
{{ truncateText(item.storyBackground || item.storyScript || '', 80) }}
</div>
<!-- 展开详情 -->
<div class="history-expand" v-if="expandedItemId === item.id">
<div class="history-detail-section">
<div class="history-detail-label">故事背景</div>
<div class="history-detail-content">{{ item.storyBackground || '-' }}</div>
</div>
<div class="history-detail-section">
<div class="history-detail-label">故事文案</div>
<div class="history-detail-content">{{ item.storyScript || '-' }}</div>
</div>
</div>
<div class="history-item-actions">
<a-button size="small" @click.stop="toggleExpand(item)">
{{ expandedItemId === item.id ? '收起' : '展开' }}
</a-button>
<a-button size="small" @click.stop="reuseHistory(item)">复用设置</a-button>
<a-button size="small" type="primary" v-if="item.status === 'SUCCESS' || item.status === 'COMPLETED'" @click.stop="viewHistoryItem(item)">
<CopyOutlined /> 查看结果
</a-button>
<a-button size="small" danger @click.stop="handleDeleteHistory(item)">
删除
</a-button>
</div>
</div>
</div>
<!-- 空状态 -->
<div class="history-empty" v-else-if="!loadingHistory">
<div class="empty-illustration">📝</div>
<p class="empty-title">暂无历史记录</p>
<p class="empty-desc">完成创作后记录将显示在这里</p>
</div>
<!-- 加载中 -->
<div class="history-loading" v-else>
<LoadingOutlined class="is-loading" />
<span>加载中...</span>
</div>
<!-- 加载更多 -->
<div class="history-load-more" v-if="hasMoreHistory">
<div class="history-load-more" v-if="hasMoreHistory && historyList.length > 0">
<a-button size="small" :loading="loadingHistory" @click="loadMoreHistory">加载更多</a-button>
</div>
</div>
@@ -225,11 +254,11 @@
<script setup>
import { ref, computed, onMounted } from 'vue'
import { message } from 'ant-design-vue'
import { message, Modal } from 'ant-design-vue'
import { useRouter } from 'vue-router'
import { LoadingOutlined, CopyOutlined, CustomerServiceOutlined, CloseOutlined } from '@ant-design/icons-vue'
import { useUserStore } from '@/stores/user'
import { createNovelComicTask, getNovelComicTaskStatus, getNovelComicHistory } from '@/api/novelComic'
import { createNovelComicTask, getNovelComicTaskStatus, getNovelComicHistory, deleteNovelComicHistory } from '@/api/novelComic'
import { getUserSubscriptionInfo } from '@/api/payments'
import CreationHeader from '@/components/CreationHeader.vue'
import CreationTabs from '@/components/CreationTabs.vue'
@@ -362,6 +391,8 @@ const handleSubmit = async () => {
// 异步任务:开始轮询状态
polling.value = true
startPolling(taskId)
// 刷新历史记录
loadHistory()
} else {
taskResult.value = {
status: 'FAILED',
@@ -469,11 +500,35 @@ const resetForm = () => {
taskResult.value = null
}
// 字数限制检查
const THEME_MAX_LENGTH = 100
const STORY_BACKGROUND_MAX_LENGTH = 2000
const STORY_SCRIPT_MAX_LENGTH = 10000
const checkThemeLength = (e) => {
if (e.target.value.length >= THEME_MAX_LENGTH) {
message.warning(`主题最多 ${THEME_MAX_LENGTH} 个字`)
}
}
const checkStoryBackgroundLength = (e) => {
if (e.target.value.length >= STORY_BACKGROUND_MAX_LENGTH) {
message.warning(`故事背景最多 ${STORY_BACKGROUND_MAX_LENGTH} 个字`)
}
}
const checkStoryScriptLength = (e) => {
if (e.target.value.length >= STORY_SCRIPT_MAX_LENGTH) {
message.warning(`故事文案最多 ${STORY_SCRIPT_MAX_LENGTH} 个字`)
}
}
// ===== 历史记录 =====
const historyList = ref([])
const loadingHistory = ref(false)
const historyPage = ref(0)
const hasMoreHistory = ref(false)
const expandedItemId = ref(null)
const loadHistory = async (page = 0) => {
loadingHistory.value = true
@@ -488,7 +543,6 @@ const loadHistory = async (page = 0) => {
historyList.value.push(...items)
}
historyPage.value = page
// 判断是否还有更多
const totalElements = data.data.totalElements || 0
hasMoreHistory.value = historyList.value.length < totalElements
}
@@ -530,11 +584,47 @@ const formatDate = (dateStr) => {
return `${month}-${day} ${hour}:${min}`
}
const formatFriendlyDate = (dateStr) => {
if (!dateStr) return ''
const date = new Date(dateStr)
const now = new Date()
const diff = now - date
const oneDay = 24 * 60 * 60 * 1000
const oneHour = 60 * 60 * 1000
const oneMinute = 60 * 1000
if (diff < oneMinute) {
return '刚刚'
} else if (diff < oneHour) {
const minutes = Math.floor(diff / oneMinute)
return `${minutes} 分钟前`
} else if (diff < oneDay) {
const hours = Math.floor(diff / oneHour)
return `${hours} 小时前`
} else if (diff < 2 * oneDay) {
return '昨天'
} else if (diff < 7 * oneDay) {
const days = Math.floor(diff / oneDay)
return `${days} 天前`
} else {
return formatDate(dateStr)
}
}
const truncateText = (text, maxLen) => {
if (!text) return '暂无描述'
return text.length > maxLen ? text.slice(0, maxLen) + '...' : text
}
// 展开/收起历史记录
const toggleExpand = (item) => {
if (expandedItemId.value === item.id) {
expandedItemId.value = null
} else {
expandedItemId.value = item.id
}
}
// 查看历史结果
const viewHistoryItem = (item) => {
const s = (item.status || '').toUpperCase()
@@ -560,6 +650,34 @@ const reuseHistory = (item) => {
message.success('已填充历史设置')
}
// 删除历史记录
const handleDeleteHistory = (item) => {
Modal.confirm({
title: '确认删除',
content: `确定要删除这条历史记录吗?`,
okText: '删除',
cancelText: '取消',
okType: 'danger',
onOk: async () => {
try {
const response = await deleteNovelComicHistory(item.id)
if (response.data?.success) {
message.success('删除成功')
historyList.value = historyList.value.filter(i => i.id !== item.id)
if (expandedItemId.value === item.id) {
expandedItemId.value = null
}
} else {
message.error(response.data?.message || '删除失败')
}
} catch (error) {
console.error('删除历史记录失败:', error)
message.error('删除失败,请重试')
}
}
})
}
onMounted(() => {
checkMembership()
loadHistory()
@@ -640,7 +758,6 @@ onMounted(() => {
gap: 20px;
}
.form-card,
.result-card {
background: var(--bg-surface);
border-radius: 20px;
@@ -649,7 +766,7 @@ onMounted(() => {
box-shadow: 0 2px 12px rgba(124, 58, 237, 0.04);
}
.form-card-title {
.form-section-title {
font-size: 1.1rem;
font-weight: 700;
margin: 0 0 24px 0;
@@ -658,7 +775,7 @@ onMounted(() => {
padding-bottom: 12px;
}
.form-card-title::after {
.form-section-title::after {
content: '';
position: absolute;
bottom: 0;
@@ -1101,6 +1218,75 @@ onMounted(() => {
.history-item-actions {
display: flex;
gap: 8px;
margin-top: 12px;
}
/* 展开区域 */
.history-expand {
margin-top: 12px;
padding-top: 12px;
border-top: 1px dashed var(--border-subtle);
}
.history-detail-section {
margin-bottom: 12px;
}
.history-detail-section:last-child {
margin-bottom: 0;
}
.history-detail-label {
font-size: 12px;
font-weight: 600;
color: var(--text-tertiary);
margin-bottom: 4px;
}
.history-detail-content {
font-size: 13px;
color: var(--text-secondary);
line-height: 1.6;
background: var(--bg-base);
padding: 10px 12px;
border-radius: 10px;
word-break: break-word;
}
/* 空状态和加载中 */
.history-empty,
.history-loading {
text-align: center;
padding: 40px 20px;
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
}
.history-empty .empty-illustration,
.history-loading .empty-illustration {
font-size: 48px;
opacity: 0.6;
}
.history-empty .empty-title,
.history-loading .empty-title {
font-size: 16px;
font-weight: 600;
color: var(--text-secondary);
margin: 0;
}
.history-empty .empty-desc,
.history-loading .empty-desc {
font-size: 13px;
color: var(--text-tertiary);
margin: 0;
}
.history-loading .is-loading {
font-size: 28px;
}
.history-load-more {

View File

@@ -265,6 +265,46 @@ public class NovelComicApiController {
return ResponseEntity.ok(response);
}
/**
* 删除历史记录
* 不需要事务,不需要先查询,直接删除并通过影响行数判断
*/
@DeleteMapping("/history/{id}")
public ResponseEntity<Map<String, Object>> deleteHistory(
@PathVariable Long id,
@RequestHeader("Authorization") String token) {
Map<String, Object> response = new HashMap<>();
try {
String username = extractUsernameFromToken(token);
if (username == null) {
response.put("success", false);
response.put("message", "用户未登录");
return ResponseEntity.status(401).body(response);
}
int deleted = novelComicTaskRepository.deleteByIdAndUsername(id, username);
if (deleted > 0) {
logger.info("删除历史记录成功: id={}, user={}", id, username);
response.put("success", true);
response.put("message", "删除成功");
return ResponseEntity.ok(response);
} else {
response.put("success", false);
response.put("message", "记录不存在或无权限删除");
return ResponseEntity.status(404).body(response);
}
} catch (Exception e) {
logger.error("删除历史记录异常: id={}", id, e);
response.put("success", false);
response.put("message", "删除失败: " + e.getMessage());
return ResponseEntity.status(500).body(response);
}
}
/**
* 更新数据库中的任务记录
*/

View File

@@ -96,7 +96,7 @@ public class UserWorkApiController {
public ResponseEntity<Map<String, Object>> getMyWorks(
@RequestHeader(value = "Authorization", required = false) String token,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "1000") int size,
@RequestParam(defaultValue = "20") int size,
@RequestParam(defaultValue = "true") boolean includeProcessing,
@RequestParam(required = false) String workType) {
@@ -110,9 +110,10 @@ public class UserWorkApiController {
return ResponseEntity.status(401).body(response);
}
// 输入验证 - 移除size上限限制
// 输入验证 - 设置合理的size上限
if (page < 0) page = 0;
if (size <= 0) size = 1000; // 不设上限默认1000条
if (size <= 0) size = 20;
if (size > 100) size = 100; // 设置上限最多100条
// 解析作品类型
CommonTaskType filterType = null;

View File

@@ -5,6 +5,9 @@ import java.util.Optional;
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.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import com.example.demo.model.NovelComicTask;
@@ -14,5 +17,11 @@ public interface NovelComicTaskRepository extends JpaRepository<NovelComicTask,
Optional<NovelComicTask> findByTaskId(String taskId);
Optional<NovelComicTask> findByIdAndUsername(Long id, String username);
Page<NovelComicTask> findByUsernameOrderByCreatedAtDesc(String username, Pageable pageable);
@Modifying
@Query("DELETE FROM NovelComicTask t WHERE t.id = :id AND t.username = :username")
int deleteByIdAndUsername(@Param("id") Long id, @Param("username") String username);
}