feat: 完成小说漫剧历史记录模块开发
This commit is contained in:
@@ -56,3 +56,11 @@ export const getNovelComicHistory = (params = {}) => {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 删除历史记录
|
||||
export const deleteNovelComicHistory = (id) => {
|
||||
return request({
|
||||
url: `/novel-comic/history/${id}`,
|
||||
method: 'DELETE'
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新数据库中的任务记录
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user