temp
This commit is contained in:
parent
12e1da5681
commit
c1d6fe181d
Binary file not shown.
Binary file not shown.
@ -1,5 +1,6 @@
|
||||
import Realm, { ObjectSchema } from 'realm'
|
||||
import { BookImageCategory, BookTaskStatus, BookType } from '../../../enum/bookEnum'
|
||||
import { ImageToVideoModels } from '@/define/enum/video'
|
||||
|
||||
export class ImageDefineModel extends Realm.Object<ImageDefineModel> {
|
||||
label: string
|
||||
@ -61,6 +62,7 @@ export class BookTaskModel extends Realm.Object<BookTaskModel> {
|
||||
errorMsg: string | null
|
||||
isAuto: boolean // 是否自动
|
||||
openVideoGenerate: boolean | null // 是否开启视频生成
|
||||
videoCategory: ImageToVideoModels // 图转视频方式
|
||||
updateTime: Date
|
||||
createTime: Date
|
||||
imageCategory: BookImageCategory // 图片出图方式
|
||||
@ -96,6 +98,7 @@ export class BookTaskModel extends Realm.Object<BookTaskModel> {
|
||||
updateTime: 'date',
|
||||
createTime: 'date',
|
||||
imageCategory: 'string',
|
||||
videoCategory: "string",
|
||||
},
|
||||
// 主键为_id
|
||||
primaryKey: 'id'
|
||||
|
||||
@ -1,9 +1,6 @@
|
||||
import Realm, { ObjectSchema } from 'realm'
|
||||
import {
|
||||
BookBackTaskStatus,
|
||||
BookBackTaskType,
|
||||
BookTaskStatus,
|
||||
BookType,
|
||||
MJAction,
|
||||
} from '../../../enum/bookEnum'
|
||||
import { MJImageType } from '../../../enum/mjEnum'
|
||||
@ -65,10 +62,12 @@ export class VideoMessage extends Realm.Object<VideoMessage> {
|
||||
bookTaskDetailId: string;
|
||||
status: string | null;
|
||||
videoUrl: string | null;
|
||||
videoUrls: string[] | null; // 视频地址数组
|
||||
taskId: string | null;
|
||||
runwayOptions: string | null; // 生成视频的一些设置
|
||||
lumaOptions: string | null; // 生成视频的一些设置
|
||||
klingOptions: string | null; // 生成视频的一些设置
|
||||
mjVideoOptions: string | null; // MJ生成视频的一些设置
|
||||
messageData: string | null;
|
||||
static schema: ObjectSchema = {
|
||||
name: 'VideoMessage',
|
||||
@ -87,7 +86,9 @@ export class VideoMessage extends Realm.Object<VideoMessage> {
|
||||
runwayOptions: "string?",
|
||||
lumaOptions: "string?",
|
||||
klingOptions: "string?",
|
||||
messageData: 'string?'
|
||||
mjVideoOptions: "string?",
|
||||
messageData: 'string?',
|
||||
videoUrls: 'string[]'
|
||||
},
|
||||
primaryKey: 'id'
|
||||
}
|
||||
@ -172,6 +173,7 @@ export class BookTaskDetailModel extends Realm.Object<BookTaskDetailModel> {
|
||||
bookTaskId: string
|
||||
videoPath: string | null // 视频地址
|
||||
generateVideoPath: string | null // 生成视频地址
|
||||
subVideoPath: string[] | null // 生成的批次视频的地址
|
||||
audioPath: string | null // 音频地址
|
||||
word: string | null // 文案
|
||||
oldImage: string | null // 旧图片(用于SD的图生图)
|
||||
@ -207,6 +209,7 @@ export class BookTaskDetailModel extends Realm.Object<BookTaskDetailModel> {
|
||||
bookTaskId: { type: 'string', indexed: true },
|
||||
videoPath: 'string?',
|
||||
generateVideoPath: 'string?', // 生成视频地址
|
||||
subVideoPath : "string[]", // 生成的批次视频的地址
|
||||
audioPath: 'string?',
|
||||
word: 'string?',
|
||||
oldImage: 'string?',
|
||||
|
||||
@ -249,6 +249,38 @@ const migration = (oldRealm: Realm, newRealm: Realm) => {
|
||||
newBookTask[i].klingOptions = undefined;
|
||||
}
|
||||
}
|
||||
if (oldRealm.schemaVersion < 40) {
|
||||
const oldBookTask = oldRealm.objects('BookTask')
|
||||
const newBookTask = newRealm.objects('BookTask')
|
||||
for (let i = 0; i < oldBookTask.length; i++) {
|
||||
newBookTask[i].videoCategory = "RUNWAY";
|
||||
newBookTask[i].videoCategory = "RUNWAY";
|
||||
}
|
||||
}
|
||||
if (oldRealm.schemaVersion < 41) {
|
||||
const oldBookTask = oldRealm.objects('BookTaskDetail')
|
||||
const newBookTask = newRealm.objects('BookTaskDetail')
|
||||
for (let i = 0; i < oldBookTask.length; i++) {
|
||||
newBookTask[i].mjVideoOptions = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
if (oldRealm.schemaVersion < 42) {
|
||||
const oldBookTask = oldRealm.objects('VideoMessage')
|
||||
const newBookTask = newRealm.objects('VideoMessage')
|
||||
for (let i = 0; i < oldBookTask.length; i++) {
|
||||
newBookTask[i].videoUrls = [];
|
||||
}
|
||||
}
|
||||
|
||||
if (oldRealm.schemaVersion < 43) {
|
||||
const oldBookTask = oldRealm.objects('VideoMessage')
|
||||
const newBookTask = newRealm.objects('VideoMessage')
|
||||
for (let i = 0; i < oldBookTask.length; i++) {
|
||||
newBookTask[i].subVideoPath = [];
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class BaseRealmService extends BaseService {
|
||||
@ -291,7 +323,7 @@ export class BaseRealmService extends BaseService {
|
||||
VideoMessage
|
||||
],
|
||||
path: this.dbpath,
|
||||
schemaVersion: 39,
|
||||
schemaVersion: 43,
|
||||
migration: migration
|
||||
}
|
||||
this.realm = await Realm.open(config)
|
||||
|
||||
@ -73,6 +73,9 @@ export class BookTaskDetailService extends BaseRealmService {
|
||||
subImagePath: (item.subImagePath as string[])?.map((subImage) => {
|
||||
return JoinPath(define.project_path, subImage)
|
||||
}),
|
||||
subVideoPath: (item.subVideoPath as string[])?.map((subVideo) => {
|
||||
return JoinPath(define.project_path, subVideo)
|
||||
}),
|
||||
characterTags: item.characterTags ? item.characterTags.map((tag) => tag) : null,
|
||||
sceneTags: item.sceneTags ? item.sceneTags.map((tag) => tag) : null,
|
||||
subValue: isEmpty(item.subValue) ? null : JSON.parse(item.subValue),
|
||||
@ -202,7 +205,16 @@ export class BookTaskDetailService extends BaseRealmService {
|
||||
}
|
||||
// 开始修改
|
||||
for (let key in updateData) {
|
||||
bookTaskDetail[key] = updateData[key]
|
||||
|
||||
let newData = updateData[key];
|
||||
|
||||
if (key == "generateVideoPath") {
|
||||
if (!isEmpty(updateData[key])) {
|
||||
newData = path.relative(define.project_path, updateData[key])
|
||||
}
|
||||
}
|
||||
|
||||
bookTaskDetail[key] = newData;
|
||||
}
|
||||
bookTaskDetail.updateTime = new Date()
|
||||
})
|
||||
|
||||
@ -13,6 +13,7 @@ import { TagDefine } from '../../../tagDefine.js'
|
||||
import { ImageStyleDefine } from "../../../../define/iamgeStyleDefine"
|
||||
import { cloneDeep } from 'lodash'
|
||||
import { GeneralResponse } from '../../../../model/generalResponse'
|
||||
import { ImageToVideoModels } from '@/define/enum/video'
|
||||
|
||||
let dbPath = path.resolve(define.db_path, 'book.realm')
|
||||
|
||||
@ -91,6 +92,7 @@ export class BookTaskService extends BaseRealmService {
|
||||
imageFolder: JoinPath(define.project_path, bookTask.imageFolder),
|
||||
cacheImageList: bookTask.cacheImageList ? Array.from(bookTask.cacheImageList).map(item => JoinPath(define.project_path, item)) : [],
|
||||
imageCategory: bookTask.imageCategory ? bookTask.imageCategory : BookImageCategory.MJ, // 默认使用MJ出图
|
||||
videoCategory : bookTask.videoCategory ? bookTask.videoCategory : ImageToVideoModels.MJ_VIDEO
|
||||
} as Book.SelectBookTask;
|
||||
})
|
||||
|
||||
|
||||
@ -196,6 +196,12 @@ const BOOK = {
|
||||
/** Runway图转视频返回前端数据任务 */
|
||||
RUNWAY_IMAGE_TO_VIDEO_RETURN: "RUNWAY_IMAGE_TO_VIDEO_RETURN",
|
||||
|
||||
/** 获取指定的条件的图转视频的数据,包含字批次 */
|
||||
GET_VIDEO_BOOK_INFO_LIST: "GET_VIDEO_BOOK_INFO_LIST",
|
||||
|
||||
/** 获取小说图片和视频生成进度 */
|
||||
GET_BOOK_IMAGE_AND_VIDEO_PROGRESS : "GET_BOOK_IMAGE_AND_VIDEO_PROGRESS"
|
||||
|
||||
//#endregion
|
||||
|
||||
}
|
||||
|
||||
@ -302,6 +302,48 @@ export enum BookTagSelectType {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 根据小说类型值返回对应的中文标签
|
||||
* @param value 小说类型值
|
||||
* @returns 中文标签
|
||||
*/
|
||||
export function GetBookTypeLabel(value: string) {
|
||||
if (value == BookType.MJ_REVERSE) {
|
||||
return 'MJ反推'
|
||||
} else if (value == BookType.SD_REVERSE) {
|
||||
return 'SD反推'
|
||||
} else if (value == BookType.ORIGINAL) {
|
||||
return '原创'
|
||||
} else {
|
||||
return '未知'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据小说图片生成方式返回对应的标签
|
||||
* @param value 图片生成方式类型
|
||||
* @returns 对应的显示标签
|
||||
*/
|
||||
export function GetBookImageCategoryLabel(value: string) {
|
||||
switch (value) {
|
||||
case BookImageCategory.MJ:
|
||||
return 'MJ';
|
||||
case BookImageCategory.SD:
|
||||
return 'SD';
|
||||
case BookImageCategory.ComfyUI:
|
||||
return 'ComfyUI';
|
||||
case BookImageCategory.D3:
|
||||
return 'D3';
|
||||
case BookImageCategory.FLUX_API:
|
||||
return 'FLUX API';
|
||||
case BookImageCategory.FLUX_FORGE:
|
||||
return 'FLUX FORGE';
|
||||
default:
|
||||
return '未知';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 根据Key返回指定的后台任务类型的label
|
||||
* @param key
|
||||
|
||||
@ -1,3 +1,6 @@
|
||||
|
||||
//#region 图转视频类型
|
||||
|
||||
/** 图片转视频的方式 */
|
||||
export enum ImageToVideoModels {
|
||||
/** runway 生成视频 */
|
||||
@ -8,8 +11,54 @@ export enum ImageToVideoModels {
|
||||
KLING = "KLING",
|
||||
/** Pika 生成视频 */
|
||||
PIKA = "PIKA",
|
||||
/** MJ 图转视频 */
|
||||
MJ_VIDEO = "MJ_VIDEO"
|
||||
}
|
||||
|
||||
/**
|
||||
* 图片转视频模型的名称转换
|
||||
* @param model 图片转视频的模型类型
|
||||
* @returns 模型的中文名称
|
||||
*/
|
||||
export const GetImageToVideoModelsLabel = (model: ImageToVideoModels | string) => {
|
||||
switch (model) {
|
||||
case ImageToVideoModels.RUNWAY:
|
||||
return "Runway";
|
||||
case ImageToVideoModels.LUMA:
|
||||
return "Luma";
|
||||
case ImageToVideoModels.KLING:
|
||||
return "可灵";
|
||||
case ImageToVideoModels.PIKA:
|
||||
return "Pika";
|
||||
case ImageToVideoModels.MJ_VIDEO:
|
||||
return "MJ视频";
|
||||
default:
|
||||
return "未知";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取图像转视频模型选项的函数
|
||||
*
|
||||
* 该函数返回一个包含所有可用图像转视频模型的选项数组。
|
||||
* 每个选项包含一个标签(label)和一个值(value)。
|
||||
* 标签通过调用 GetImageToVideoModelsLabel 函数获得,而值则直接使用 ImageToVideoModels 枚举值。
|
||||
*
|
||||
* @returns 图像转视频模型选项数组,每个选项包含 label 和 value 属性
|
||||
*/
|
||||
export const GetImageToVideoModelsOptions = () => {
|
||||
return [
|
||||
{ label: GetImageToVideoModelsLabel(ImageToVideoModels.RUNWAY), value: ImageToVideoModels.RUNWAY },
|
||||
{ label: GetImageToVideoModelsLabel(ImageToVideoModels.LUMA), value: ImageToVideoModels.LUMA },
|
||||
{ label: GetImageToVideoModelsLabel(ImageToVideoModels.KLING), value: ImageToVideoModels.KLING },
|
||||
{ label: GetImageToVideoModelsLabel(ImageToVideoModels.PIKA), value: ImageToVideoModels.PIKA },
|
||||
{ label: GetImageToVideoModelsLabel(ImageToVideoModels.MJ_VIDEO), value: ImageToVideoModels.MJ_VIDEO }
|
||||
]
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
|
||||
//#region 通用
|
||||
|
||||
/** 生成视频的方式 */
|
||||
|
||||
@ -16,6 +16,7 @@ import { BookPrompt } from '../Service/Book/bookPrompt'
|
||||
import { BookGeneral } from '../Service/Book/bookGeneral'
|
||||
import { OperateBookType } from '../../define/enum/bookEnum'
|
||||
import { VideoGlobal } from '../Service/video/videoGlobal'
|
||||
import { BookImageTextToVideoIndex } from "@/main/Service/Book/BookImageTextToVideo/bookImageTextToVideoIndex";
|
||||
let reverseBook = new ReverseBook()
|
||||
let basicReverse = new BasicReverse()
|
||||
let subtitle = new Subtitle()
|
||||
@ -31,6 +32,7 @@ let bookFrame = new BookFrame()
|
||||
let bookPrompt = new BookPrompt();
|
||||
let bookGeneral = new BookGeneral()
|
||||
let videoGlobal = new VideoGlobal()
|
||||
let bookImageTextToVideoIndex = new BookImageTextToVideoIndex();
|
||||
|
||||
export function BookIpc() {
|
||||
// 获取样式图片的子列表
|
||||
@ -369,6 +371,13 @@ export function BookIpc() {
|
||||
/** 修改小说详细分镜的Videomessage */
|
||||
ipcMain.handle(DEFINE_STRING.BOOK.UPDATE_BOOK_TASK_DETAIL_VIDEO_MESSAGE, async (event, bookTaskDetailId, videoMessage) => await videoGlobal.UpdateBookTaskDetailVideoMessage(bookTaskDetailId, videoMessage))
|
||||
|
||||
/** 获取指定的条件的图转视频的数据,包含子批次 */
|
||||
ipcMain.handle(DEFINE_STRING.BOOK.GET_VIDEO_BOOK_INFO_LIST, async (event,
|
||||
condition: BookVideo.BookVideoInfoListQuertCondition) => await bookImageTextToVideoIndex.GetVideoBookInfoList(condition))
|
||||
|
||||
/** 获取小说图片和视频生成进度 */
|
||||
ipcMain.handle(DEFINE_STRING.BOOK.GET_BOOK_IMAGE_AND_VIDEO_PROGRESS, async (event, bookId?: string, bookTaskId?: string) => await bookImageTextToVideoIndex.bookImageTextToVideoInfo.GetBookImageAndVideoProgress(bookId, bookTaskId))
|
||||
|
||||
|
||||
//#endregion
|
||||
}
|
||||
|
||||
@ -1,12 +1,11 @@
|
||||
import { BookType, OperateBookType, TagDefineType } from '../../../define/enum/bookEnum'
|
||||
import { BookType } from '../../../define/enum/bookEnum'
|
||||
import { errorMessage, successMessage } from '../../Public/generalTools'
|
||||
import { BookService } from '../../../define/db/service/Book/bookService'
|
||||
import path from 'path'
|
||||
import { CheckFileOrDirExist, CheckFolderExistsOrCreate, CopyFileOrFolder, DeleteFolderAllFile, GetSubdirectories } from '../../../define/Tools/file'
|
||||
import { CheckFileOrDirExist, DeleteFolderAllFile, GetSubdirectories } from '../../../define/Tools/file'
|
||||
import { GeneralResponse } from '../../../model/generalResponse'
|
||||
import { BookServiceBasic } from '../ServiceBasic/bookServiceBasic'
|
||||
import { BookTask } from './bookTask'
|
||||
import fs from 'fs'
|
||||
import { Book } from '../../../model/book/book'
|
||||
|
||||
export class BookBasic {
|
||||
|
||||
@ -0,0 +1,22 @@
|
||||
import { BookImageTextToVideoInfo } from "./bookImageTextToVideoInfo";
|
||||
|
||||
|
||||
export class BookImageTextToVideoIndex {
|
||||
|
||||
bookImageTextToVideoInfo: BookImageTextToVideoInfo;
|
||||
|
||||
constructor() {
|
||||
this.bookImageTextToVideoInfo = new BookImageTextToVideoInfo();
|
||||
}
|
||||
|
||||
//#region Info
|
||||
|
||||
/** 获取用于视频生成的小说信息列表 根据查询条件返回小说数据,如果指定了bookTaskId,则返回对应小说的单个任务数据 否则返回所有启用了视频生成功能的小说及其任务数据 */
|
||||
GetVideoBookInfoList = async (condition: BookVideo.BookVideoInfoListQuertCondition) => await this.bookImageTextToVideoInfo.GetVideoBookInfoList(condition)
|
||||
|
||||
|
||||
GetBookImageAndVideoProgress = async (bookId?: string, bookTaskId?: string) => await this.bookImageTextToVideoInfo.GetBookImageAndVideoProgress(bookId, bookTaskId);
|
||||
|
||||
//#endregion
|
||||
|
||||
}
|
||||
@ -0,0 +1,257 @@
|
||||
import { errorMessage, successMessage } from "@/main/Public/generalTools";
|
||||
import { BookBasicHandle } from "../bookBasicHandle";
|
||||
import { isEmpty } from "lodash";
|
||||
import { CheckFileOrDirExist } from "@/define/Tools/file";
|
||||
import { Book } from "@/model/book/book";
|
||||
|
||||
// 定义进度数据的类型
|
||||
export interface ProgressData {
|
||||
imageProgress: number;
|
||||
videoProgress: number;
|
||||
totalCount: number;
|
||||
imageRate: number;
|
||||
videoRate: number;
|
||||
}
|
||||
|
||||
// 定义嵌套的 Record 类型
|
||||
export type BookProgressRecord = Record<string, Record<string, ProgressData>>;
|
||||
|
||||
|
||||
|
||||
export class BookImageTextToVideoInfo extends BookBasicHandle {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
//#region GetVideoBookInfoList
|
||||
/**
|
||||
* 获取用于视频生成的小说信息列表
|
||||
* 根据查询条件返回小说数据,如果指定了bookTaskId,则返回对应小说的单个任务数据
|
||||
* 否则返回所有启用了视频生成功能的小说及其任务数据
|
||||
* @param condition 查询条件,包含可选的bookTaskId等参数
|
||||
* @returns 处理结果,成功时返回小说数据,失败时返回错误信息
|
||||
*/
|
||||
GetVideoBookInfoList = async (condition: BookVideo.BookVideoInfoListQuertCondition) => {
|
||||
try {
|
||||
await this.InitBookBasicHandle();
|
||||
|
||||
// 获取小说的所有的数据
|
||||
let bookRes = this.bookService.GetBookData(condition);
|
||||
|
||||
if (bookRes.code !== 1) {
|
||||
return errorMessage('获取小说数据失败,错误信息:' + bookRes.message, 'BookImageTextToVideoInfo_GetVideoBookInfoList');
|
||||
}
|
||||
|
||||
let bookList = bookRes.data.res_book ?? [];
|
||||
if (bookList.length <= 0) {
|
||||
return errorMessage('没有找到对应的小说数据,请先添加小说', 'BookImageTextToVideoInfo_GetVideoBookInfoList');
|
||||
}
|
||||
|
||||
// 有指定的小说批次任务的ID情况下 只返回当前的小说数据
|
||||
if (condition.bookTaskId) {
|
||||
let bookTask = this.bookTaskService.GetBookTaskDataById(condition.bookTaskId);
|
||||
if (bookTask == null) {
|
||||
return errorMessage('没有找到对应的小说批次数据,请先添加小说批次', 'BookImageTextToVideoInfo_GetVideoBookInfoList');
|
||||
}
|
||||
// 再上面的小说数据里面获取数据 然后直接返回就行
|
||||
let bookInfo = bookList.find(book => book.id === bookTask.bookId);
|
||||
if (!bookInfo) {
|
||||
return errorMessage('没有找到对应的小说数据,请先添加小说', 'BookImageTextToVideoInfo_GetVideoBookInfoList');
|
||||
}
|
||||
bookInfo.bookTasks = [bookTask];
|
||||
// 返回
|
||||
return successMessage(bookInfo, '获取小说批次任务数据成功', 'BookImageTextToVideoInfo_GetVideoBookInfoList');
|
||||
}
|
||||
|
||||
// 没有那个数据 将所有的数据进行处理
|
||||
let res = [];
|
||||
for (let i = 0; i < bookList.length; i++) {
|
||||
const element = bookList[i];
|
||||
// 获取小说批次任务数据
|
||||
let bookTaskRes = this.bookTaskService.GetBookTaskData({ bookId: element.id });
|
||||
if (bookTaskRes.code != 1) {
|
||||
continue;
|
||||
}
|
||||
if (bookTaskRes.data.bookTasks.length > 0) {
|
||||
// 检查所有的 bookTasks 里面是不是开启了图转视频功能
|
||||
let videoBookTasks = bookTaskRes.data.bookTasks.filter(task => task.openVideoGenerate);
|
||||
if (videoBookTasks.length <= 0) {
|
||||
continue;
|
||||
} else {
|
||||
element.bookTasks = videoBookTasks;
|
||||
res.push(element);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return successMessage(
|
||||
res,
|
||||
'获取小说批次任务数据成功',
|
||||
'BookImageTextToVideoInfo_GetVideoBookInfoList'
|
||||
);
|
||||
|
||||
} catch (error) {
|
||||
return errorMessage(
|
||||
'初始化BookBasicHandle失败,错误信息如下:' + error.toString(),
|
||||
'BookImageTextToVideoInfo_GetVideoBookInfoList'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
|
||||
//#region GetBookImageAndVideoProgress
|
||||
|
||||
|
||||
/**
|
||||
* 获取小说图片和视频生成的进度信息
|
||||
* 根据提供的参数查询指定小说或小说任务的图片和视频生成进度
|
||||
* @param bookId 可选,小说ID,如果提供则只返回该小说的进度数据
|
||||
* @param bookTaskId 可选,小说任务ID,如果提供则只返回该任务的进度数据
|
||||
* @returns 返回包含进度信息的对象,成功时返回进度数据,失败时返回错误信息
|
||||
*/
|
||||
GetBookImageAndVideoProgress = async (bookId?: string, bookTaskId?: string) => {
|
||||
try {
|
||||
await this.InitBookBasicHandle();
|
||||
|
||||
let bookIds: Record<string, string[]> = {};
|
||||
// 开始处理获取对应的小说ID和关联数据
|
||||
if (!isEmpty(bookId)) {
|
||||
let bookInfo = this.bookService.GetBookDataById(bookId);
|
||||
if (bookInfo == null) {
|
||||
return errorMessage('没有找到对应的小说数据,请先添加小说', 'BookImageTextToVideoInfo_GetBookImageAndVideoProgress');
|
||||
}
|
||||
bookIds[bookInfo.id] = [];
|
||||
} else {
|
||||
// 获取所有的小说数据
|
||||
let bookRes = this.bookService.GetBookData({});
|
||||
if (bookRes.code !== 1) {
|
||||
return errorMessage('获取小说数据失败,错误信息:' + bookRes.message, 'BookImageTextToVideoInfo_GetBookImageAndVideoProgress');
|
||||
}
|
||||
let bookList = bookRes.data.res_book ?? [];
|
||||
if (bookList.length <= 0) {
|
||||
return errorMessage('没有找到对应的小说数据,请先添加小说', 'BookImageTextToVideoInfo_GetBookImageAndVideoProgress');
|
||||
}
|
||||
// 将所有的小说ID添加到bookIds里面
|
||||
for (let i = 0; i < bookList.length; i++) {
|
||||
const element = bookList[i];
|
||||
bookIds[element.id] = [];
|
||||
}
|
||||
}
|
||||
|
||||
// 判断小说任务ID是不是存在 存在的话判断小说任务的ID是不是再bookIds里面 在的话更新bookIds中指定的ID的值数据
|
||||
if (!isEmpty(bookTaskId)) {
|
||||
let bookTaskInfo = this.bookTaskService.GetBookTaskDataById(bookTaskId);
|
||||
if (bookTaskInfo == null) {
|
||||
return errorMessage('没有找到对应的小说批次数据,请先添加小说批次', 'BookImageTextToVideoInfo_GetBookImageAndVideoProgress');
|
||||
}
|
||||
// 判断当前的bookid 是不是再 bookIds里面
|
||||
if (bookTaskInfo.bookId && bookIds[bookTaskInfo.bookId]) {
|
||||
bookIds[bookTaskInfo.bookId] = [bookTaskInfo.id];
|
||||
} else {
|
||||
return errorMessage('当前的小说批次任务ID不属于指定的小说ID', 'BookImageTextToVideoInfo_GetBookImageAndVideoProgress');
|
||||
}
|
||||
} else {
|
||||
// 遍历 bookIds 获取所有的小说批次任务ID
|
||||
for (const bookId of Object.keys(bookIds)) {
|
||||
// 获取小说批次任务数据
|
||||
let bookTaskRes = this.bookTaskService.GetBookTaskData({ bookId: bookId });
|
||||
if (bookTaskRes.code !== 1) {
|
||||
continue;
|
||||
}
|
||||
if (bookTaskRes.data.bookTasks.length > 0) {
|
||||
// 检查所有的 bookTasks 里面是不是开启了图转视频功能
|
||||
let ids = bookTaskRes.data.bookTasks.map(task => task.id);
|
||||
bookIds[bookId] = ids;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 遍历 bookIds 将值数据为空或者长度为0的都删除
|
||||
Object.keys(bookIds).forEach(bookId => {
|
||||
if (isEmpty(bookIds[bookId]) || bookIds[bookId].length <= 0) {
|
||||
delete bookIds[bookId];
|
||||
}
|
||||
});
|
||||
|
||||
// 检查最终结果
|
||||
if (Object.keys(bookIds).length === 0) {
|
||||
return successMessage({}, '没有找到对应的小说数据或者小说批次任务数据', 'BookImageTextToVideoInfo_GetBookImageAndVideoProgress');
|
||||
}
|
||||
|
||||
// 这边开始处理数据
|
||||
let resData = await this.ProgressHandle(bookIds);
|
||||
|
||||
return successMessage(
|
||||
resData,
|
||||
'获取小说图片和视频生成进度成功',
|
||||
'BookImageTextToVideoInfo_GetBookImageAndVideoProgress'
|
||||
);
|
||||
|
||||
} catch (error) {
|
||||
return errorMessage(
|
||||
'获取小说图片和视频生成进度失败,错误信息如下:' + error.toString(),
|
||||
'BookImageTextToVideoInfo_GetBookImageAndVideoProgress'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理进度数据
|
||||
* 遍历小说ID和批次任务ID,计算每个任务的图片和视频生成进度
|
||||
* @param bookIds 小说ID和对应小说任务ID的映射关系,格式为 {bookId: [taskId1, taskId2, ...]}
|
||||
* @returns 返回每本书每个任务的进度数据,包含图片进度、视频进度、总数量和完成率
|
||||
*/
|
||||
private ProgressHandle = async (bookIds: Record<string, string[]>) => {
|
||||
// 这边开始处理数据
|
||||
let resData: BookProgressRecord = {};
|
||||
|
||||
// 遍历 bookIds 获取所有的小说批次任务数据
|
||||
for (const bookId of Object.keys(bookIds)) {
|
||||
let bookTaskIds = bookIds[bookId];
|
||||
if (!bookTaskIds && bookTaskIds.length <= 0) {
|
||||
continue;
|
||||
}
|
||||
resData[bookId] = {};
|
||||
|
||||
// 获取小说批次任务数据
|
||||
for (let i = 0; i < bookTaskIds.length; i++) {
|
||||
const element = bookTaskIds[i];
|
||||
let bookTaskDetails = this.bookTaskDetailService.GetBookTaskData({ bookTaskId: element, bookId: bookId });
|
||||
if (bookTaskDetails.code !== 1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 遍历 bookTaskDetails 获取每个小说批次任务的进度数据
|
||||
let imageProgress = 0;
|
||||
let videoProgress = 0;
|
||||
for (let j = 0; j < bookTaskDetails.data.length; j++) {
|
||||
const bookTaskDetail = bookTaskDetails.data[j] as Book.SelectBookTaskDetail;
|
||||
// 检查图片信息
|
||||
if (!isEmpty(bookTaskDetail.outImagePath) && await CheckFileOrDirExist(bookTaskDetail.outImagePath)) {
|
||||
imageProgress += 1;
|
||||
}
|
||||
// 检查视频信息
|
||||
if (!isEmpty(bookTaskDetail.videoPath) && await CheckFileOrDirExist(bookTaskDetail.videoPath)) {
|
||||
videoProgress += 1;
|
||||
}
|
||||
}
|
||||
// 开始添加数据
|
||||
resData[bookId][element] = {
|
||||
imageProgress: imageProgress,
|
||||
videoProgress: videoProgress,
|
||||
totalCount: bookTaskDetails.data.length,
|
||||
imageRate: bookTaskDetails.data.length > 0 ? (imageProgress / bookTaskDetails.data.length) * 100 : 0,
|
||||
videoRate: bookTaskDetails.data.length > 0 ? (videoProgress / bookTaskDetails.data.length) * 100 : 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return resData;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
}
|
||||
@ -134,7 +134,8 @@ export class ReverseBook {
|
||||
return {
|
||||
...item,
|
||||
outImagePath: isEmpty(item.outImagePath) ? item.outImagePath : item.outImagePath + '?t=' + new Date().getTime(),
|
||||
subImagePath: item.subImagePath && item.subImagePath.length > 0 ? item.subImagePath.map(it => it + '?t=' + new Date().getTime()) : item.subImagePath
|
||||
subImagePath: item.subImagePath && item.subImagePath.length > 0 ? item.subImagePath.map(it => it + '?t=' + new Date().getTime()) : item.subImagePath,
|
||||
subVideoPath: item.subVideoPath && item.subVideoPath.length > 0 ? item.subVideoPath.map(it => it + '?t=' + new Date().getTime()) : item.subVideoPath,
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
39
src/main/Service/Book/bookBasicHandle.ts
Normal file
39
src/main/Service/Book/bookBasicHandle.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { BookService } from "@/define/db/service/Book/bookService"
|
||||
import { BookTaskDetailService } from "@/define/db/service/Book/bookTaskDetailService"
|
||||
import { BookTaskService } from "@/define/db/service/Book/bookTaskService"
|
||||
import { OptionRealmService } from "@/define/db/service/SoftWare/optionRealmService"
|
||||
|
||||
export class BookBasicHandle {
|
||||
bookTaskDetailService!: BookTaskDetailService
|
||||
bookTaskService!: BookTaskService
|
||||
optionRealmService!: OptionRealmService
|
||||
bookService!: BookService
|
||||
|
||||
|
||||
constructor() {
|
||||
// 初始化
|
||||
}
|
||||
|
||||
async InitBookBasicHandle() {
|
||||
// 如果 bookTaskDetailService 已经初始化,则直接返回
|
||||
if (!this.bookTaskDetailService) {
|
||||
this.bookTaskDetailService = await BookTaskDetailService.getInstance()
|
||||
}
|
||||
if (!this.bookTaskService) {
|
||||
this.bookTaskService = await BookTaskService.getInstance()
|
||||
}
|
||||
if (!this.optionRealmService) {
|
||||
this.optionRealmService = await OptionRealmService.getInstance()
|
||||
}
|
||||
if (!this.bookService) {
|
||||
this.bookService = await BookService.getInstance()
|
||||
}
|
||||
}
|
||||
|
||||
async transaction(callback: (realm: any) => void) {
|
||||
await this.InitBookBasicHandle()
|
||||
this.bookService.transaction(() => {
|
||||
callback(this.bookService.realm)
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -3,7 +3,6 @@ import { DownloadFile, GetBaseUrl } from "@/define/Tools/common";
|
||||
import { errorMessage, successMessage } from "@/main/Public/generalTools";
|
||||
import { BookTaskDetail } from "@/model/book/bookTaskDetail";
|
||||
import { GptService } from "@/main/Service/GPT/gpt";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { BookServiceBasic } from "../ServiceBasic/bookServiceBasic";
|
||||
import { isEmpty } from "lodash";
|
||||
import path from "path";
|
||||
|
||||
5
src/model/book/book.d.ts
vendored
5
src/model/book/book.d.ts
vendored
@ -1,3 +1,4 @@
|
||||
import { ImageToVideoModels } from "@/define/enum/video"
|
||||
import { BookBackTaskStatus, BookBackTaskType, BookTaskStatus, BookType, TaskExecuteType, BookRepalceDataType, BookImageCategory } from "../../define/enum/bookEnum"
|
||||
import { MJAction } from "../../define/enum/bookEnum"
|
||||
import { MJImageType } from "../../define/enum/mjEnum"
|
||||
@ -78,6 +79,7 @@ declare namespace Book {
|
||||
isAuto?: boolean // 是否标记全自动
|
||||
subImageFolder?: string[] | null // 子图片文件夹地址,多个
|
||||
openVideoGenerate?: boolean // 是否开启视频生成
|
||||
videoCategory?: ImageToVideoModels
|
||||
}
|
||||
|
||||
// 添加批次任务
|
||||
@ -151,6 +153,7 @@ declare namespace Book {
|
||||
bookTaskId?: string
|
||||
videoPath?: string // 视频地址
|
||||
generateVideoPath?: string // 生成的视频地址
|
||||
subVideoPath?: string[] // 生成的批次视频的地址
|
||||
audioPath?: string // 音频地址
|
||||
draftDepend?: string // 草稿依赖
|
||||
word?: string // 文案
|
||||
@ -178,8 +181,6 @@ declare namespace Book {
|
||||
updateTime?: Date
|
||||
}
|
||||
|
||||
|
||||
|
||||
type QueryBookTaskCondition = {
|
||||
id?: string
|
||||
no?: number
|
||||
|
||||
2
src/model/book/bookTaskDetail.d.ts
vendored
2
src/model/book/bookTaskDetail.d.ts
vendored
@ -20,6 +20,8 @@ declare namespace BookTaskDetail {
|
||||
runwayOptions?: string;
|
||||
lumaOptions?: string;
|
||||
klingOptions?: string;
|
||||
mjVideoOptions?: string;
|
||||
videoUrls?: string[]; // 视频地址数组
|
||||
messageData?: string;
|
||||
}
|
||||
|
||||
|
||||
15
src/model/book/bookVideo.d.ts
vendored
Normal file
15
src/model/book/bookVideo.d.ts
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
declare namespace BookVideo {
|
||||
|
||||
|
||||
/**
|
||||
* 表示用于检索小说转视频信息列表的查询条件的接口。
|
||||
*
|
||||
* @interface BookVideoInfoListQuertCondition
|
||||
* @property {string?} [bookId] - 用于筛选视频的小说ID
|
||||
* @property {string?} [bookTaskId] - 用于筛选视频的小说任务ID
|
||||
*/
|
||||
interface BookVideoInfoListQuertCondition {
|
||||
bookId?: string; // 图书ID
|
||||
bookTaskId?: string; // 图书任务ID
|
||||
}
|
||||
}
|
||||
@ -6,7 +6,9 @@ import { BookType, OperateBookType } from '../../define/enum/bookEnum'
|
||||
import Video from './video'
|
||||
|
||||
const book = {
|
||||
...Video,
|
||||
video: {
|
||||
...Video
|
||||
},
|
||||
// 获取小说操作类型(原创/SD反推/MJ反推)
|
||||
GetBookType: async () => await ipcRenderer.invoke(DEFINE_STRING.BOOK.GET_BOOK_TYPE),
|
||||
|
||||
|
||||
@ -3,7 +3,7 @@ import { ipcRenderer } from "electron"
|
||||
|
||||
const Video = {
|
||||
|
||||
/** 初始化图转视频消息 */
|
||||
/** 初始化小说图片转视频消息 */
|
||||
InitVideoMessage: async (bookTaskDetailId: string) => {
|
||||
return await ipcRenderer.invoke(DEFINE_STRING.BOOK.INIT_VIDEO_MESSAGE, bookTaskDetailId)
|
||||
},
|
||||
@ -11,8 +11,17 @@ const Video = {
|
||||
/** 修改小说详情的VideoMessage */
|
||||
UpdateBookTaskDetailVideoMessage: async (bookTaskDetailId: string, videoMessage: any) => {
|
||||
return await ipcRenderer.invoke(DEFINE_STRING.BOOK.UPDATE_BOOK_TASK_DETAIL_VIDEO_MESSAGE, bookTaskDetailId, videoMessage)
|
||||
}
|
||||
},
|
||||
|
||||
/** 获取指定条件的小说图转视频数据,包含子批次 */
|
||||
GetVideoBookInfoList: async (condition: BookVideo.BookVideoInfoListQuertCondition) => {
|
||||
return await ipcRenderer.invoke(DEFINE_STRING.BOOK.GET_VIDEO_BOOK_INFO_LIST, condition)
|
||||
},
|
||||
|
||||
/** 获取小说图片和视频生成进度 */
|
||||
GetBookImageAndVideoProgress: async (bookId?: string, bookTaskId?: string) => {
|
||||
return await ipcRenderer.invoke(DEFINE_STRING.BOOK.GET_BOOK_IMAGE_AND_VIDEO_PROGRESS, bookId, bookTaskId)
|
||||
}
|
||||
}
|
||||
|
||||
export default Video;
|
||||
11
src/renderer/components.d.ts
vendored
11
src/renderer/components.d.ts
vendored
@ -8,6 +8,8 @@ export {}
|
||||
/* prettier-ignore */
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
NAlert: typeof import('naive-ui')['NAlert']
|
||||
NAvatar: typeof import('naive-ui')['NAvatar']
|
||||
NButton: typeof import('naive-ui')['NButton']
|
||||
NCard: typeof import('naive-ui')['NCard']
|
||||
NCheckbox: typeof import('naive-ui')['NCheckbox']
|
||||
@ -15,12 +17,16 @@ declare module 'vue' {
|
||||
NColorPicker: typeof import('naive-ui')['NColorPicker']
|
||||
NDataTable: typeof import('naive-ui')['NDataTable']
|
||||
NDivider: typeof import('naive-ui')['NDivider']
|
||||
NDrawer: typeof import('naive-ui')['NDrawer']
|
||||
NDrawerContent: typeof import('naive-ui')['NDrawerContent']
|
||||
NDropdown: typeof import('naive-ui')['NDropdown']
|
||||
NDynamicInput: typeof import('naive-ui')['NDynamicInput']
|
||||
NDynamicTags: typeof import('naive-ui')['NDynamicTags']
|
||||
NEmpty: typeof import('naive-ui')['NEmpty']
|
||||
NForm: typeof import('naive-ui')['NForm']
|
||||
NFormItem: typeof import('naive-ui')['NFormItem']
|
||||
NGrid: typeof import('naive-ui')['NGrid']
|
||||
NGridItem: typeof import('naive-ui')['NGridItem']
|
||||
NIcon: typeof import('naive-ui')['NIcon']
|
||||
NImage: typeof import('naive-ui')['NImage']
|
||||
NImageGroup: typeof import('naive-ui')['NImageGroup']
|
||||
@ -28,10 +34,14 @@ declare module 'vue' {
|
||||
NInputNumber: typeof import('naive-ui')['NInputNumber']
|
||||
NLayout: typeof import('naive-ui')['NLayout']
|
||||
NLayoutSider: typeof import('naive-ui')['NLayoutSider']
|
||||
NList: typeof import('naive-ui')['NList']
|
||||
NListItem: typeof import('naive-ui')['NListItem']
|
||||
NLog: typeof import('naive-ui')['NLog']
|
||||
NMenu: typeof import('naive-ui')['NMenu']
|
||||
NModal: typeof import('naive-ui')['NModal']
|
||||
NPopover: typeof import('naive-ui')['NPopover']
|
||||
NProgress: typeof import('naive-ui')['NProgress']
|
||||
NScrollbar: typeof import('naive-ui')['NScrollbar']
|
||||
NSelect: typeof import('naive-ui')['NSelect']
|
||||
NSlider: typeof import('naive-ui')['NSlider']
|
||||
NSpace: typeof import('naive-ui')['NSpace']
|
||||
@ -42,6 +52,7 @@ declare module 'vue' {
|
||||
NTabs: typeof import('naive-ui')['NTabs']
|
||||
NTag: typeof import('naive-ui')['NTag']
|
||||
NText: typeof import('naive-ui')['NText']
|
||||
NThing: typeof import('naive-ui')['NThing']
|
||||
NTooltip: typeof import('naive-ui')['NTooltip']
|
||||
NTree: typeof import('naive-ui')['NTree']
|
||||
NUpload: typeof import('naive-ui')['NUpload']
|
||||
|
||||
@ -8,6 +8,7 @@ import {
|
||||
SettingsOutline,
|
||||
DuplicateOutline,
|
||||
GridOutline,
|
||||
VideocamOutline,
|
||||
RadioOutline,
|
||||
BookOutline
|
||||
} from '@vicons/ionicons5'
|
||||
@ -86,6 +87,13 @@ export const menuDataSource = [
|
||||
type: 'route',
|
||||
icon: 'grid'
|
||||
},
|
||||
{
|
||||
label: '图/文生视频',
|
||||
key: 'image_text_video',
|
||||
routeName: 'image_text_video',
|
||||
type: 'route',
|
||||
icon: 'videocam'
|
||||
},
|
||||
{
|
||||
label: 'API服务',
|
||||
key: 'lai_api',
|
||||
@ -163,6 +171,7 @@ const iconMap = {
|
||||
plane: PaperPlaneOutline,
|
||||
duplicate: DuplicateOutline,
|
||||
grid: GridOutline,
|
||||
videocam: VideocamOutline,
|
||||
api: APIIcon,
|
||||
radio: RadioOutline,
|
||||
settings: SettingsOutline,
|
||||
|
||||
41
src/renderer/src/common/time.ts
Normal file
41
src/renderer/src/common/time.ts
Normal file
@ -0,0 +1,41 @@
|
||||
export function FormatDate(date: Date, onlyDay: boolean = false): string {
|
||||
// 如果传入的是字符串,尝试将其转换为 Date 对象
|
||||
if (typeof date === 'string') {
|
||||
date = new Date(date);
|
||||
}
|
||||
if (!(date instanceof Date) || isNaN(date.getTime())) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// 判断时间是不是 0001-01-01T08:00:00
|
||||
if (date.getFullYear() === 1) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (onlyDay) {
|
||||
return date.getFullYear() + '-' +
|
||||
pad(date.getMonth() + 1) + '-' +
|
||||
pad(date.getDate());
|
||||
}
|
||||
return date.getFullYear() + '-' +
|
||||
pad(date.getMonth() + 1) + '-' +
|
||||
pad(date.getDate()) + ' ' +
|
||||
pad(date.getHours()) + ':' +
|
||||
pad(date.getMinutes()) + ':' +
|
||||
pad(date.getSeconds());
|
||||
}
|
||||
|
||||
|
||||
function pad(number: number) {
|
||||
return (number < 10 ? '0' : '') + number;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 延时多少秒,返回一个Promise
|
||||
* @param time 延时时间,单位毫秒
|
||||
* @returns viod
|
||||
*/
|
||||
export async function TimeDelay(time: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, time));
|
||||
}
|
||||
@ -80,7 +80,7 @@ async function GetBookTaskDetailOption() {
|
||||
videoMessage.value = res.data
|
||||
} else {
|
||||
// 这边初始化
|
||||
let initRes = await window.book.InitVideoMessage(props.bookTaskDetailId)
|
||||
let initRes = await window.book.video.InitVideoMessage(props.bookTaskDetailId)
|
||||
console.log('InitVideoMessage', initRes)
|
||||
if (initRes.code != 1) {
|
||||
message.error(initRes.message)
|
||||
@ -158,7 +158,7 @@ async function SaveSimpleOptions() {
|
||||
}
|
||||
console.log('saveVideoMessageObject', saveVideoMessageObject)
|
||||
|
||||
let res = await window.book.UpdateBookTaskDetailVideoMessage(
|
||||
let res = await window.book.video.UpdateBookTaskDetailVideoMessage(
|
||||
props.bookTaskDetailId,
|
||||
saveVideoMessageObject
|
||||
)
|
||||
|
||||
@ -67,7 +67,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { NProgress, useMessage } from 'naive-ui'
|
||||
import { useReverseManageStore } from '../../../../../stores/reverseManage'
|
||||
import { useSoftwareStore } from '../../../../../stores/software'
|
||||
@ -79,6 +79,9 @@ let promptPercentage = ref(0)
|
||||
let imagePercentage = ref(0)
|
||||
let reversePromptPercentage = ref(0)
|
||||
|
||||
// 定时器引用
|
||||
let intervalId = null
|
||||
|
||||
let reverseManageStore = useReverseManageStore()
|
||||
let softwareStore = useSoftwareStore()
|
||||
let message = useMessage()
|
||||
@ -134,11 +137,19 @@ onMounted(() => {
|
||||
ComputePercentage()
|
||||
|
||||
// 这边开始定时执行计算
|
||||
setInterval(async () => {
|
||||
intervalId = setInterval(async () => {
|
||||
await ComputePercentage()
|
||||
}, 10000)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
// 组件卸载时清除定时器
|
||||
if (intervalId) {
|
||||
clearInterval(intervalId)
|
||||
intervalId = null
|
||||
}
|
||||
})
|
||||
|
||||
function ErrorPosition(type) {
|
||||
let index = -1
|
||||
softwareStore.skipRowIndex = 0
|
||||
|
||||
@ -26,7 +26,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, defineComponent, onUnmounted, toRaw, watch } from 'vue'
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { useMessage, NDivider } from 'naive-ui'
|
||||
import ManageBookDetailButton from './MJReverse/ManageBookDetailButton.vue'
|
||||
import ManageBookReverseTable from './MJReverse/ManageBookReverseTable.vue'
|
||||
|
||||
@ -0,0 +1,217 @@
|
||||
<template>
|
||||
<div class="novel-reader-home">
|
||||
<!-- 移动端遮罩层 -->
|
||||
<div
|
||||
class="mobile-overlay"
|
||||
:class="{ active: isMobileSidebarOpen }"
|
||||
@click="closeMobileSidebar"
|
||||
></div>
|
||||
|
||||
<!-- 主要内容区域 -->
|
||||
<div class="content-container">
|
||||
<!-- 左侧小说列表 -->
|
||||
<NovelSidebar
|
||||
:book-info-list="bookInfoList"
|
||||
:selected-novel-id="selectedNovelId"
|
||||
:is-mobile-sidebar-open="isMobileSidebarOpen"
|
||||
@add-novel="handleAddNovel"
|
||||
@select-novel="handleSelectNovel"
|
||||
/>
|
||||
|
||||
<!-- 右侧章节内容 -->
|
||||
<div class="chapter-content-area">
|
||||
<!-- 移动端顶部导航 -->
|
||||
<MobileHeader
|
||||
:selected-novel-title="selectedNovel?.title"
|
||||
@open-sidebar="openMobileSidebar"
|
||||
/>
|
||||
|
||||
<div v-if="selectedNovel" class="chapter-container">
|
||||
<!-- 章节列表 -->
|
||||
<ChapterList
|
||||
:selected-novel="selectedNovel"
|
||||
@open-chapter="handleOpenChapter"
|
||||
@add-tag="handleAddTag"
|
||||
@remove-tag="handleRemoveTag"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-else class="chapter-container">
|
||||
<EmptyState @add-novel="handleAddNovel" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useMessage } from 'naive-ui'
|
||||
import NovelSidebar from './components/ImageTextVideo/ImageTextVideoBookSidebar.vue'
|
||||
import MobileHeader from './components/ImageTextVideo/ImageTextVideoMobileHeader.vue'
|
||||
import ChapterList from './components/ImageTextVideo/ImageTextVideoTaskList.vue'
|
||||
import EmptyState from './components/ImageTextVideo/ImageTextVideoEmptyState.vue'
|
||||
|
||||
import { useReverseManageStore } from '@/stores/reverseManage'
|
||||
const reverseManageStore = useReverseManageStore()
|
||||
|
||||
const message = useMessage()
|
||||
|
||||
// 选中的小说ID
|
||||
const selectedNovelId = ref('novel-1')
|
||||
|
||||
// 移动端侧边栏状态
|
||||
const isMobileSidebarOpen = ref(false)
|
||||
const bookInfoList = ref([])
|
||||
|
||||
onMounted(async () => {
|
||||
// 初始化小说数据
|
||||
let res = await window.book.video.GetVideoBookInfoList({})
|
||||
console.log('获取小说数据', res)
|
||||
bookInfoList.value = res.data
|
||||
})
|
||||
|
||||
// 选中的小说
|
||||
const selectedNovel = computed(() => {
|
||||
return bookInfoList.value.find((bookInfo) => bookInfo.id === selectedNovelId.value)
|
||||
})
|
||||
|
||||
// 处理选择小说
|
||||
function handleSelectNovel(novelId) {
|
||||
selectedNovelId.value = novelId
|
||||
reverseManageStore.selectBook = bookInfoList.value.find((book) => book.id === novelId)
|
||||
console.log('选择小说:', novelId)
|
||||
// 移动端选择后关闭侧边栏
|
||||
closeMobileSidebar()
|
||||
}
|
||||
|
||||
// 打开移动端侧边栏
|
||||
function openMobileSidebar() {
|
||||
isMobileSidebarOpen.value = true
|
||||
}
|
||||
|
||||
// 关闭移动端侧边栏
|
||||
function closeMobileSidebar() {
|
||||
isMobileSidebarOpen.value = false
|
||||
}
|
||||
|
||||
// 处理添加小说
|
||||
function handleAddNovel() {
|
||||
message.info('功能开发中:添加新小说')
|
||||
}
|
||||
|
||||
// 处理打开章节
|
||||
function handleOpenChapter(chapter) {
|
||||
message.info(`打开章节:${chapter.title}`)
|
||||
}
|
||||
|
||||
// 处理添加标签
|
||||
function handleAddTag(chapterId) {
|
||||
// 简单的提示,实际应该打开输入框
|
||||
const tag = prompt('请输入标签名称:')
|
||||
if (tag && tag.trim()) {
|
||||
const chapter = findChapterById(chapterId)
|
||||
if (chapter) {
|
||||
chapter.tags.push(tag.trim())
|
||||
message.success(`添加标签: ${tag}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理移除标签
|
||||
function handleRemoveTag(chapterId, tag) {
|
||||
const chapter = findChapterById(chapterId)
|
||||
if (chapter) {
|
||||
const index = chapter.tags.indexOf(tag)
|
||||
if (index > -1) {
|
||||
chapter.tags.splice(index, 1)
|
||||
message.success(`移除标签: ${tag}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 查找章节
|
||||
function findChapterById(chapterId) {
|
||||
for (const bookInfo of bookInfoList.value) {
|
||||
const bookTasks = bookInfo.bookTasks.find((ch) => ch.id === chapterId)
|
||||
if (bookTasks) return bookTasks
|
||||
}
|
||||
return null
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.novel-reader-home {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.content-container {
|
||||
display: grid;
|
||||
grid-template-columns: 320px 1fr;
|
||||
height: calc(100vh - 10px);
|
||||
gap: 16px;
|
||||
/* padding: 16px; */
|
||||
}
|
||||
|
||||
/* 右侧内容 */
|
||||
.chapter-content-area {
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.chapter-container {
|
||||
flex: 1;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.mobile-overlay {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 1024px) {
|
||||
.content-container {
|
||||
grid-template-columns: 280px 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.content-container {
|
||||
grid-template-columns: 1fr;
|
||||
height: calc(100vh - 16px);
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.mobile-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
z-index: 999;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: all 0.3s ease;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.mobile-overlay.active {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.chapter-container {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.content-container {
|
||||
height: calc(100vh - 8px);
|
||||
padding: 4px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,311 @@
|
||||
<template>
|
||||
<div class="image-text-video-info-home">
|
||||
<div class="layout-wrapper">
|
||||
<!-- 左右分栏容器 -->
|
||||
<n-layout has-sider class="main-layout">
|
||||
<!-- 左侧表格区域 -->
|
||||
<n-layout-content class="left-panel">
|
||||
<image-text-video-info-task-list
|
||||
:table-data="tableData"
|
||||
:loading="loading"
|
||||
:show-right-panel="showRightPanel"
|
||||
@view-detail="handleViewDetail"
|
||||
@toggle-status="handleToggleStatus"
|
||||
@add-task="handleAdd"
|
||||
@toggle-right-panel="handleToggleRightPanel"
|
||||
/>
|
||||
</n-layout-content>
|
||||
|
||||
<!-- 右侧详细操作区域 -->
|
||||
<n-layout-sider
|
||||
v-if="showRightPanel"
|
||||
:width="450"
|
||||
:min-width="450"
|
||||
:max-width="450"
|
||||
:show-trigger="false"
|
||||
position="right"
|
||||
class="right-panel"
|
||||
>
|
||||
<div class="detail-container">
|
||||
<!-- 空状态 -->
|
||||
<image-text-video-info-empty-state v-if="!selectedTask" />
|
||||
|
||||
<!-- 任务详情 -->
|
||||
<image-text-video-info-task-detail
|
||||
v-else
|
||||
:task="selectedTask"
|
||||
@delete-task="handleDeleteTask"
|
||||
@edit-task="handleEditTask"
|
||||
@config-change="handleConfigChange"
|
||||
/>
|
||||
</div>
|
||||
</n-layout-sider>
|
||||
</n-layout>
|
||||
</div>
|
||||
|
||||
<!-- 抽屉形式的详细信息 -->
|
||||
<n-drawer
|
||||
v-model:show="showDrawer"
|
||||
:width="450"
|
||||
placement="right"
|
||||
:mask-closable="true"
|
||||
:close-on-esc="true"
|
||||
>
|
||||
<n-drawer-content title="任务详情" closable>
|
||||
<!-- 空状态 -->
|
||||
<image-text-video-info-empty-state v-if="!selectedTask" />
|
||||
|
||||
<!-- 任务详情 -->
|
||||
<image-text-video-info-task-detail
|
||||
v-else
|
||||
:task="selectedTask"
|
||||
@delete-task="handleDeleteTask"
|
||||
@edit-task="handleEditTask"
|
||||
@config-change="handleConfigChange"
|
||||
/>
|
||||
</n-drawer-content>
|
||||
</n-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import {
|
||||
NLayout,
|
||||
NLayoutSider,
|
||||
NLayoutContent,
|
||||
NDrawer,
|
||||
NDrawerContent,
|
||||
useMessage
|
||||
} from 'naive-ui'
|
||||
import ImageTextVideoInfoTaskList from './components/ImageTextVideoInfo/ImageTextVideoInfoTaskList.vue'
|
||||
import ImageTextVideoInfoTaskDetail from './components/ImageTextVideoInfo/ImageTextVideoInfoTaskDetail.vue'
|
||||
import ImageTextVideoInfoEmptyState from './components/ImageTextVideoInfo/ImageTextVideoInfoEmptyState.vue'
|
||||
|
||||
import { useReverseManageStore } from '@/stores/reverseManage'
|
||||
|
||||
const reverseManageStore = useReverseManageStore()
|
||||
|
||||
const message = useMessage()
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref([])
|
||||
|
||||
// 加载状态
|
||||
const loading = ref(false)
|
||||
|
||||
// 选中的任务
|
||||
const selectedTask = ref(null)
|
||||
|
||||
// 右侧面板显示控制
|
||||
const showRightPanel = ref(false)
|
||||
|
||||
// 抽屉显示控制
|
||||
const showDrawer = ref(false)
|
||||
|
||||
async function handleInitialize() {
|
||||
// 模拟加载数据
|
||||
loading.value = true
|
||||
try {
|
||||
// 这边开始加载数据
|
||||
let res = await reverseManageStore.GetBookTaskDetail(reverseManageStore.selectBookTask.id)
|
||||
console.log('获取任务数据', res)
|
||||
if (res.code != 1) {
|
||||
message.error(res.message)
|
||||
return
|
||||
}
|
||||
tableData.value = res.data
|
||||
} catch (error) {
|
||||
message.error('加载数据失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
handleInitialize()
|
||||
})
|
||||
|
||||
// 查看详情
|
||||
function handleViewDetail(row) {
|
||||
selectedTask.value = { ...row }
|
||||
// 如果右侧面板隐藏,则显示抽屉
|
||||
if (!showRightPanel.value) {
|
||||
showDrawer.value = true
|
||||
}
|
||||
}
|
||||
|
||||
// 切换右侧面板显示
|
||||
function handleToggleRightPanel(show) {
|
||||
showRightPanel.value = show
|
||||
// 如果隐藏右侧面板且有选中任务,则显示抽屉
|
||||
if (!show && selectedTask.value) {
|
||||
showDrawer.value = true
|
||||
}
|
||||
// 如果显示右侧面板,则隐藏抽屉
|
||||
if (show) {
|
||||
showDrawer.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 切换状态
|
||||
function handleToggleStatus(row) {
|
||||
const task = tableData.value.find((item) => item.id === row.id)
|
||||
if (task) {
|
||||
if (task.status === '进行中') {
|
||||
task.status = '暂停'
|
||||
message.warning(`任务 ${task.name} 已暂停`)
|
||||
} else if (task.status === '暂停') {
|
||||
task.status = '进行中'
|
||||
message.success(`任务 ${task.name} 已启动`)
|
||||
} else {
|
||||
message.error('当前状态不支持切换')
|
||||
}
|
||||
|
||||
// 更新选中的任务
|
||||
if (selectedTask.value && selectedTask.value.id === task.id) {
|
||||
selectedTask.value = { ...task }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 新增任务
|
||||
function handleAdd() {
|
||||
message.info('新增任务功能开发中...')
|
||||
}
|
||||
|
||||
// 编辑任务
|
||||
function handleEditTask(task) {
|
||||
message.info(`编辑任务: ${task.name}`)
|
||||
}
|
||||
|
||||
// 删除任务
|
||||
function handleDeleteTask(task) {
|
||||
const index = tableData.value.findIndex((item) => item.id === task.id)
|
||||
if (index > -1) {
|
||||
tableData.value.splice(index, 1)
|
||||
selectedTask.value = null
|
||||
}
|
||||
}
|
||||
|
||||
// 配置变更
|
||||
function handleConfigChange(task) {
|
||||
// 更新表格数据中的配置
|
||||
const tableTask = tableData.value.find((item) => item.id === task.id)
|
||||
if (tableTask) {
|
||||
tableTask.config = { ...task.config }
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.image-text-video-info-home {
|
||||
width: 100%;
|
||||
height: calc(100vh - 10px);
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.layout-wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-width: 1200px;
|
||||
}
|
||||
|
||||
.main-layout {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
min-width: 1200px;
|
||||
}
|
||||
|
||||
.left-panel {
|
||||
border-right: 1px solid var(--n-border-color);
|
||||
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.05);
|
||||
flex: 1;
|
||||
min-width: 600px;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
/* 当右侧面板隐藏时,左侧面板占满整个宽度 */
|
||||
.left-panel:only-child {
|
||||
border-right: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.right-panel {
|
||||
width: 450px;
|
||||
min-width: 450px;
|
||||
max-width: 450px;
|
||||
flex-shrink: 0;
|
||||
border-left: 1px solid var(--n-border-color);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.detail-container {
|
||||
height: calc(100vh - 20px);
|
||||
padding: 5px 20px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 1400px) {
|
||||
.layout-wrapper {
|
||||
min-width: 1000px;
|
||||
}
|
||||
|
||||
.main-layout {
|
||||
min-width: 1000px;
|
||||
}
|
||||
|
||||
.right-panel {
|
||||
width: 400px;
|
||||
min-width: 400px;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.left-panel {
|
||||
min-width: 500px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.layout-wrapper {
|
||||
min-width: 900px;
|
||||
}
|
||||
|
||||
.main-layout {
|
||||
min-width: 900px;
|
||||
}
|
||||
|
||||
.right-panel {
|
||||
width: 350px;
|
||||
min-width: 350px;
|
||||
max-width: 350px;
|
||||
}
|
||||
|
||||
.left-panel {
|
||||
min-width: 450px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 横向滚动条样式 */
|
||||
.image-text-video-info-home::-webkit-scrollbar {
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.image-text-video-info-home::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.image-text-video-info-home::-webkit-scrollbar-thumb {
|
||||
background: #c1c1c1;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.image-text-video-info-home::-webkit-scrollbar-thumb:hover {
|
||||
background: #a8a8a8;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,210 @@
|
||||
<template>
|
||||
<n-list-item
|
||||
class="book-item"
|
||||
:class="{ selected: isSelected }"
|
||||
@click="$emit('select', bookInfo.id)"
|
||||
>
|
||||
<n-card
|
||||
:bordered="false"
|
||||
class="book-card"
|
||||
:content-style="{
|
||||
padding: '8px 0 0 0',
|
||||
backgroundColor: 'transparent'
|
||||
}"
|
||||
:header-style="{ padding: 0 }"
|
||||
>
|
||||
<template #header>
|
||||
<!-- 标题和按钮 -->
|
||||
<div class="book-header">
|
||||
<span strong class="book-title" :class="{ active: isSelected }" :title="bookInfo.name">
|
||||
{{ bookInfo.name }}
|
||||
</span>
|
||||
<n-dropdown
|
||||
:options="getBookMenuOptions()"
|
||||
class="menu-button"
|
||||
@select="handleBookAction"
|
||||
trigger="click"
|
||||
>
|
||||
<n-button size="small" quaternary circle @click.stop>
|
||||
<template #icon>
|
||||
<n-icon><EllipsisVerticalOutline /></n-icon>
|
||||
</template>
|
||||
</n-button>
|
||||
</n-dropdown>
|
||||
</div>
|
||||
</template>
|
||||
<div class="book-content">
|
||||
<!-- 头像部分 -->
|
||||
<div class="book-avatar">
|
||||
<n-avatar :size="50" :src="bookInfo.cover" color="#7fe7c4" style="border-radius: 8px">
|
||||
<n-icon size="38"><BookOutline /></n-icon>
|
||||
</n-avatar>
|
||||
</div>
|
||||
|
||||
<!-- 描述部分 -->
|
||||
<div class="book-meta">
|
||||
<n-space vertical>
|
||||
<n-space align="center" gap="8px">
|
||||
<n-tag type="warning" size="small" :bordered="false">
|
||||
{{ GetBookTypeLabel(bookInfo.type) }}
|
||||
</n-tag>
|
||||
<n-text strong>{{ bookInfo.bookTasks.length }} 个视频批次</n-text>
|
||||
</n-space>
|
||||
|
||||
<div>
|
||||
<n-text strong>{{ FormatDate(bookInfo.createTime) }}</n-text>
|
||||
</div>
|
||||
</n-space>
|
||||
</div>
|
||||
</div>
|
||||
</n-card>
|
||||
</n-list-item>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { h } from 'vue'
|
||||
import {
|
||||
NListItem,
|
||||
NCard,
|
||||
NAvatar,
|
||||
NIcon,
|
||||
NTag,
|
||||
NButton,
|
||||
NDropdown,
|
||||
NText,
|
||||
useMessage
|
||||
} from 'naive-ui'
|
||||
import {
|
||||
BookOutline,
|
||||
EllipsisVerticalOutline,
|
||||
CreateOutline,
|
||||
TrashOutline,
|
||||
EyeOutline
|
||||
} from '@vicons/ionicons5'
|
||||
import { GetBookTypeLabel } from '@/define/enum/bookEnum'
|
||||
import { FormatDate } from '@/renderer/src/common/time'
|
||||
|
||||
const message = useMessage()
|
||||
|
||||
defineProps({
|
||||
bookInfo: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
isSelected: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
defineEmits(['select'])
|
||||
|
||||
// 获取书籍菜单选项
|
||||
function getBookMenuOptions() {
|
||||
return [
|
||||
{
|
||||
label: '查看详情',
|
||||
key: 'view',
|
||||
icon: () => h(NIcon, null, () => h(EyeOutline))
|
||||
},
|
||||
{
|
||||
label: '编辑信息',
|
||||
key: 'edit',
|
||||
icon: () => h(NIcon, null, () => h(CreateOutline))
|
||||
},
|
||||
{
|
||||
type: 'divider'
|
||||
},
|
||||
{
|
||||
label: '删除',
|
||||
key: 'delete',
|
||||
icon: () => h(NIcon, null, () => h(TrashOutline))
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// 处理菜单操作
|
||||
function handleBookAction(key) {
|
||||
switch (key) {
|
||||
case 'view':
|
||||
message.info('查看书籍详情')
|
||||
break
|
||||
case 'edit':
|
||||
message.info('编辑书籍信息')
|
||||
break
|
||||
case 'delete':
|
||||
message.warning('删除书籍')
|
||||
break
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.book-item {
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
padding: 8px 12px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 8px;
|
||||
background-color: rgba(127, 231, 196, 0.05);
|
||||
border: 1px solid rgba(127, 231, 196, 0.1);
|
||||
}
|
||||
|
||||
.book-item:hover {
|
||||
background-color: var(--n-color-hover);
|
||||
}
|
||||
|
||||
.book-item.selected {
|
||||
background-color: rgba(127, 231, 196, 0.3);
|
||||
border: 1px solid rgba(127, 231, 196, 0.3);
|
||||
}
|
||||
|
||||
.book-card {
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.book-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.book-content {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.book-avatar {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.book-title {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
.book-title.active {
|
||||
color: var(--n-color-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.menu-button {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.book-meta {
|
||||
/* display: flex; */
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,103 @@
|
||||
<template>
|
||||
<n-card
|
||||
class="novel-list-sidebar"
|
||||
:class="{ 'mobile-open': isMobileSidebarOpen }"
|
||||
:bordered="false"
|
||||
:content-style="{ padding: '16px' }"
|
||||
>
|
||||
<template #header>
|
||||
<div class="sidebar-header">
|
||||
<h2>我的书库</h2>
|
||||
<n-button type="primary" size="small" @click="$emit('add-novel')">
|
||||
<template #icon>
|
||||
<n-icon><AddOutline /></n-icon>
|
||||
</template>
|
||||
添加小说
|
||||
</n-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<n-scrollbar style="max-height: calc(100vh - 100px); padding-right: 10px">
|
||||
<n-list>
|
||||
<NovelItem
|
||||
v-for="bookInfo in bookInfoList"
|
||||
:key="bookInfo.id"
|
||||
:book-info="bookInfo"
|
||||
:is-selected="selectedNovelId === bookInfo.id"
|
||||
@select="$emit('select-novel', $event)"
|
||||
/>
|
||||
</n-list>
|
||||
</n-scrollbar>
|
||||
</n-card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { NCard, NButton, NIcon, NScrollbar, NList } from 'naive-ui'
|
||||
import { AddOutline } from '@vicons/ionicons5'
|
||||
import NovelItem from './ImageTextVideoBookItem.vue'
|
||||
|
||||
defineProps({
|
||||
bookInfoList: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
selectedNovelId: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
isMobileSidebarOpen: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
defineEmits(['add-novel', 'select-novel'])
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.novel-list-sidebar {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sidebar-header h2 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.novel-list-sidebar {
|
||||
position: fixed;
|
||||
top: 16px;
|
||||
left: -340px;
|
||||
width: 320px;
|
||||
height: calc(100vh - 32px);
|
||||
z-index: 1000;
|
||||
transition: left 0.3s ease;
|
||||
box-shadow: var(--n-box-shadow-2);
|
||||
}
|
||||
|
||||
.novel-list-sidebar.mobile-open {
|
||||
left: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.novel-list-sidebar {
|
||||
top: 8px;
|
||||
left: -340px;
|
||||
height: calc(100vh - 16px);
|
||||
}
|
||||
|
||||
.novel-list-sidebar.mobile-open {
|
||||
left: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<div class="empty-state">
|
||||
<n-empty size="huge" description="请选择一本小说开始操作">
|
||||
<template #icon>
|
||||
<n-icon><BookOutline /></n-icon>
|
||||
</template>
|
||||
<template #extra>
|
||||
<n-button type="primary" @click="$emit('add-novel')">添加第一本小说</n-button>
|
||||
</template>
|
||||
</n-empty>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { NEmpty, NIcon, NButton } from 'naive-ui'
|
||||
import { BookOutline } from '@vicons/ionicons5'
|
||||
|
||||
defineEmits(['add-novel'])
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.empty-state {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 400px;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<n-card class="mobile-header" :bordered="false">
|
||||
<n-space justify="space-between" align="center">
|
||||
<n-button type="primary" @click="$emit('open-sidebar')">
|
||||
<template #icon>
|
||||
<n-icon><BookOutline /></n-icon>
|
||||
</template>
|
||||
选择小说
|
||||
</n-button>
|
||||
<n-text strong>{{ selectedNovelTitle || '请选择小说' }}</n-text>
|
||||
</n-space>
|
||||
</n-card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { NCard, NSpace, NButton, NIcon, NText } from 'naive-ui'
|
||||
import { BookOutline } from '@vicons/ionicons5'
|
||||
|
||||
defineProps({
|
||||
selectedNovelTitle: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
defineEmits(['open-sidebar'])
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.mobile-header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.mobile-header {
|
||||
display: block;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,198 @@
|
||||
<template>
|
||||
<n-card
|
||||
class="bookTask-card"
|
||||
hoverable
|
||||
@click="handleOpenBookTask"
|
||||
:header-style="{
|
||||
padding: '8px 16px'
|
||||
}"
|
||||
:content-style="{
|
||||
padding: '0 16px 16px 16px',
|
||||
borderRadius: '8px'
|
||||
}"
|
||||
>
|
||||
<template #header>
|
||||
<n-space justify="space-between" align="center">
|
||||
<n-text strong>{{ bookTask.name }}</n-text>
|
||||
<n-dropdown :options="getChapterMenuOptions(bookTask)" @select="handleChapterAction">
|
||||
<n-button size="small" quaternary circle>
|
||||
<template #icon>
|
||||
<n-icon><EllipsisVerticalOutline /></n-icon>
|
||||
</template>
|
||||
</n-button>
|
||||
</n-dropdown>
|
||||
</n-space>
|
||||
</template>
|
||||
|
||||
<!-- 章节信息 -->
|
||||
<n-space vertical size="small">
|
||||
<!-- 标签 -->
|
||||
<n-space size="small" wrap>
|
||||
<n-tag
|
||||
v-for="tag in bookTask.tags"
|
||||
:key="tag"
|
||||
size="small"
|
||||
type="primary"
|
||||
@close="handleRemoveTag(bookTask.id, tag)"
|
||||
>
|
||||
{{ tag }}
|
||||
</n-tag>
|
||||
</n-space>
|
||||
|
||||
<n-space size="small">
|
||||
<n-text depth="3" style="font-size: 12px">{{ FormatDate(bookTask.updateTime) }}</n-text>
|
||||
</n-space>
|
||||
|
||||
<!-- 阅读进度 -->
|
||||
<div>
|
||||
<n-space justify="space-between" style="margin-bottom: 4px">
|
||||
<n-text depth="3" style="font-size: 12px">转视频进度</n-text>
|
||||
<n-text depth="3" style="font-size: 12px">{{ progress }}%</n-text>
|
||||
</n-space>
|
||||
<n-progress
|
||||
type="line"
|
||||
:percentage="progress"
|
||||
:show-indicator="false"
|
||||
color="#7fe7c4"
|
||||
rail-color="#f3f4f6"
|
||||
:height="6"
|
||||
/>
|
||||
</div>
|
||||
</n-space>
|
||||
</n-card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { h, toRef, computed, onMounted } from 'vue'
|
||||
import {
|
||||
NCard,
|
||||
NSpace,
|
||||
NText,
|
||||
NDropdown,
|
||||
NButton,
|
||||
NIcon,
|
||||
NTag,
|
||||
NProgress,
|
||||
useMessage,
|
||||
useDialog
|
||||
} from 'naive-ui'
|
||||
import {
|
||||
EllipsisVerticalOutline,
|
||||
AddOutline,
|
||||
CreateOutline,
|
||||
TrashOutline,
|
||||
EyeOutline
|
||||
} from '@vicons/ionicons5'
|
||||
import { GetBookImageCategoryLabel } from '@/define/enum/bookEnum'
|
||||
import { GetImageToVideoModelsLabel } from '@/define/enum/video'
|
||||
import { FormatDate } from '@/renderer/src/common/time'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useReverseManageStore } from '@/stores/reverseManage'
|
||||
|
||||
const reverseManageStore = useReverseManageStore()
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
let props = defineProps({
|
||||
bookTask: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
const message = useMessage()
|
||||
const dialog = useDialog()
|
||||
const bookTask = toRef(props, 'bookTask')
|
||||
|
||||
const progress = computed(() => {
|
||||
debugger
|
||||
return bookTask.value.imageVideoProgress ? bookTask.value.imageVideoProgress.videoRate : 0
|
||||
})
|
||||
|
||||
// 处理bookTask的tags的方法
|
||||
function tagsHandle() {
|
||||
bookTask.value.tags = [
|
||||
GetBookImageCategoryLabel(props.bookTask.imageCategory),
|
||||
GetImageToVideoModelsLabel(props.bookTask.videoCategory)
|
||||
]
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
tagsHandle()
|
||||
})
|
||||
|
||||
// 处理打开bookTask事件
|
||||
function handleOpenBookTask() {
|
||||
message.info(`正在打开任务: ${bookTask.value.name}`)
|
||||
reverseManageStore.selectBookTask = bookTask.value
|
||||
router.push({ name: 'image_text_video_info', params: { id: bookTask.value.id } })
|
||||
// 打印当前的url
|
||||
console.log('当前URL:', window.location.href)
|
||||
}
|
||||
|
||||
// 处理移除标签事件
|
||||
function handleRemoveTag(bookTaskId, tag) {
|
||||
console.log('移除标签:', bookTaskId, tag)
|
||||
message.info(`已移除标签: ${tag}`)
|
||||
// 这里可以添加具体的移除标签逻辑
|
||||
}
|
||||
|
||||
// 获取章节菜单选项
|
||||
function getChapterMenuOptions(bookTask) {
|
||||
return [
|
||||
{
|
||||
label: '阅读',
|
||||
key: 'read',
|
||||
icon: () => h(NIcon, null, () => h(EyeOutline))
|
||||
},
|
||||
{
|
||||
label: '编辑',
|
||||
key: 'edit',
|
||||
icon: () => h(NIcon, null, () => h(CreateOutline))
|
||||
},
|
||||
{
|
||||
type: 'divider'
|
||||
},
|
||||
{
|
||||
label: '删除',
|
||||
key: 'delete',
|
||||
icon: () => h(NIcon, null, () => h(TrashOutline))
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// 处理章节操作
|
||||
function handleChapterAction(key, option) {
|
||||
console.log('章节操作:', key)
|
||||
switch (key) {
|
||||
case 'read':
|
||||
message.info('正在打开阅读界面...')
|
||||
break
|
||||
case 'edit':
|
||||
message.info('正在打开编辑界面...')
|
||||
break
|
||||
case 'delete':
|
||||
dialog.warning({
|
||||
title: '确认删除',
|
||||
content: '确定要删除这个章节吗?此操作不可恢复。',
|
||||
positiveText: '删除',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: () => {
|
||||
message.success('章节已删除')
|
||||
}
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.bookTask-card {
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.bookTask-card:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,110 @@
|
||||
<template>
|
||||
<n-card title="批次目录" :bordered="false" :style="{ minWidth: '380px' }">
|
||||
<template #header-extra>
|
||||
<n-space align="center">
|
||||
<n-text depth="3">共 {{ selectedNovel.bookTasks.length }} 个视频批次</n-text>
|
||||
<n-divider vertical />
|
||||
<n-tooltip trigger="hover">
|
||||
<template #trigger>
|
||||
<n-button text type="primary" size="small">
|
||||
<template #icon>
|
||||
<n-icon><AddOutline /></n-icon>
|
||||
</template>
|
||||
新增视频批次
|
||||
</n-button>
|
||||
</template>
|
||||
点击添加选择一个视频批次,从聚合推文同步过来!!
|
||||
</n-tooltip>
|
||||
</n-space>
|
||||
</template>
|
||||
|
||||
<div class="task-grid">
|
||||
<div v-for="bookTask in selectedBook.bookTasks" :key="bookTask.id" class="task-item">
|
||||
<ChapterCard :bookTask="bookTask" />
|
||||
</div>
|
||||
</div>
|
||||
</n-card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import {
|
||||
NCard,
|
||||
NSpace,
|
||||
NText,
|
||||
NDivider,
|
||||
NGrid,
|
||||
NGridItem,
|
||||
NButton,
|
||||
NTooltip,
|
||||
NIcon
|
||||
} from 'naive-ui'
|
||||
import ChapterCard from './ImageTextVideoTaskCard.vue'
|
||||
import { AddOutline } from '@vicons/ionicons5'
|
||||
import { TimeDelay } from '@/define/Tools/time'
|
||||
|
||||
const props = defineProps({
|
||||
selectedNovel: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
const selectedBook = ref(props.selectedNovel)
|
||||
|
||||
async function progressHandle() {
|
||||
// 这里可以添加一些处理逻辑
|
||||
let res = await window.book.video.GetBookImageAndVideoProgress(props.selectedNovel.id, null)
|
||||
console.log('获取批次进度', res)
|
||||
|
||||
let bookProgress = res.data[props.selectedNovel.id]
|
||||
for (let i = 0; i < selectedBook.value.bookTasks.length; i++) {
|
||||
const element = selectedBook.value.bookTasks[i]
|
||||
element.imageVideoProgress = bookProgress[element.id] || {
|
||||
imageProgress: 0,
|
||||
videoProgress: 0,
|
||||
totalCount: 0,
|
||||
imageRate: 0,
|
||||
videoRate: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await progressHandle()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.task-grid {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
}
|
||||
|
||||
/* 大屏幕 (l: 1024px+) - 固定宽度,自动换行 */
|
||||
@media (min-width: 1024px) {
|
||||
.task-grid {
|
||||
grid-template-columns: repeat(auto-fill, 320px);
|
||||
justify-content: start;
|
||||
}
|
||||
}
|
||||
|
||||
/* 中等屏幕 (m: 768px-1023px) - 自适应 */
|
||||
@media (min-width: 768px) and (max-width: 1023px) {
|
||||
.task-grid {
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
/* 小屏幕 (s: <768px) - 单列自适应 */
|
||||
@media (max-width: 767px) {
|
||||
.task-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.task-item {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,72 @@
|
||||
<template>
|
||||
<n-card title="基本信息" class="info-card">
|
||||
<n-descriptions :column="2" label-placement="left" class="descriptions">
|
||||
<n-descriptions-item label="任务名称">
|
||||
{{ task.name }}
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="任务状态">
|
||||
<n-tag :type="getStatusType(task.status)">
|
||||
{{ task.status }}
|
||||
</n-tag>
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="创建时间">
|
||||
{{ formatDate(task.createTime) }}
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="更新时间">
|
||||
{{ formatDate(task.updateTime) }}
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="任务描述" :span="2">
|
||||
{{ task.description || '暂无描述' }}
|
||||
</n-descriptions-item>
|
||||
</n-descriptions>
|
||||
</n-card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
NCard,
|
||||
NDescriptions,
|
||||
NDescriptionsItem,
|
||||
NTag
|
||||
} from 'naive-ui'
|
||||
|
||||
// 定义 props
|
||||
const props = defineProps({
|
||||
task: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
// 获取状态类型
|
||||
function getStatusType(status) {
|
||||
const statusMap = {
|
||||
进行中: 'info',
|
||||
已完成: 'success',
|
||||
暂停: 'warning',
|
||||
失败: 'error'
|
||||
}
|
||||
return statusMap[status] || 'default'
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
function formatDate(dateString) {
|
||||
if (!dateString) return '-'
|
||||
return new Date(dateString).toLocaleString('zh-CN')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.info-card {
|
||||
margin-bottom: 16px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.info-card:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.descriptions :deep(.n-descriptions-item-label) {
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<n-card title="参数配置" class="info-card">
|
||||
<n-form :model="task.config" label-placement="left" label-width="120px">
|
||||
<n-form-item label="输出格式">
|
||||
<n-select
|
||||
v-model:value="task.config.outputFormat"
|
||||
:options="outputFormatOptions"
|
||||
@update:value="handleConfigChange"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item label="质量设置">
|
||||
<n-slider
|
||||
v-model:value="task.config.quality"
|
||||
:min="1"
|
||||
:max="10"
|
||||
:step="1"
|
||||
:marks="{ 1: '低', 5: '中', 10: '高' }"
|
||||
@update:value="handleConfigChange"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item label="自动处理">
|
||||
<n-switch
|
||||
v-model:value="task.config.autoProcess"
|
||||
@update:value="handleConfigChange"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</n-card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import {
|
||||
NCard,
|
||||
NForm,
|
||||
NFormItem,
|
||||
NSelect,
|
||||
NSlider,
|
||||
NSwitch
|
||||
} from 'naive-ui'
|
||||
|
||||
// 定义 emits
|
||||
const emit = defineEmits(['config-change'])
|
||||
|
||||
// 定义 props
|
||||
const props = defineProps({
|
||||
task: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
// 输出格式选项
|
||||
const outputFormatOptions = [
|
||||
{ label: 'MP4', value: 'mp4' },
|
||||
{ label: 'AVI', value: 'avi' },
|
||||
{ label: 'MOV', value: 'mov' },
|
||||
{ label: 'WMV', value: 'wmv' }
|
||||
]
|
||||
|
||||
// 配置变更
|
||||
function handleConfigChange() {
|
||||
emit('config-change')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.info-card {
|
||||
margin-bottom: 16px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.info-card:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<div class="empty-state">
|
||||
<n-empty description="请选择左侧表格中的任务查看详细信息">
|
||||
<template #icon>
|
||||
<n-icon size="48" color="#d4d4d8">
|
||||
<DocumentTextOutline />
|
||||
</n-icon>
|
||||
</template>
|
||||
</n-empty>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { NEmpty, NIcon } from 'naive-ui'
|
||||
import { DocumentTextOutline } from '@vicons/ionicons5'
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.empty-state {
|
||||
height: calc(100vh - 50px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<n-card title="进度信息" class="info-card">
|
||||
<n-space vertical>
|
||||
<n-space justify="space-between">
|
||||
<n-text>完成进度</n-text>
|
||||
<n-text>{{ task.progress }}%</n-text>
|
||||
</n-space>
|
||||
<n-progress
|
||||
type="line"
|
||||
:percentage="task.progress"
|
||||
color="#18a058"
|
||||
rail-color="#f3f4f6"
|
||||
:height="8"
|
||||
/>
|
||||
</n-space>
|
||||
</n-card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
NCard,
|
||||
NSpace,
|
||||
NText,
|
||||
NProgress
|
||||
} from 'naive-ui'
|
||||
|
||||
// 定义 props
|
||||
const props = defineProps({
|
||||
task: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.info-card {
|
||||
margin-bottom: 16px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.info-card:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,118 @@
|
||||
<template>
|
||||
<div class="task-detail">
|
||||
<div class="detail-header">
|
||||
<n-space justify="space-between" align="center">
|
||||
<n-text strong style="font-size: 18px">任务详情</n-text>
|
||||
<n-button-group>
|
||||
<n-button type="primary" @click="handleEdit">
|
||||
<template #icon>
|
||||
<n-icon><CreateOutline /></n-icon>
|
||||
</template>
|
||||
编辑
|
||||
</n-button>
|
||||
<n-button type="error" @click="handleDelete">
|
||||
<template #icon>
|
||||
<n-icon><TrashOutline /></n-icon>
|
||||
</template>
|
||||
删除
|
||||
</n-button>
|
||||
</n-button-group>
|
||||
</n-space>
|
||||
</div>
|
||||
|
||||
<n-divider />
|
||||
|
||||
<!-- 基本信息 -->
|
||||
<image-text-video-info-basic-info :task="task" />
|
||||
|
||||
<!-- 进度信息 -->
|
||||
<image-text-video-info-progress :task="task" />
|
||||
|
||||
<!-- 参数配置 -->
|
||||
<image-text-video-info-config :task="task" @config-change="handleConfigChange" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import {
|
||||
NSpace,
|
||||
NText,
|
||||
NButton,
|
||||
NButtonGroup,
|
||||
NIcon,
|
||||
NDivider,
|
||||
useMessage,
|
||||
useDialog
|
||||
} from 'naive-ui'
|
||||
import {
|
||||
CreateOutline,
|
||||
TrashOutline
|
||||
} from '@vicons/ionicons5'
|
||||
import ImageTextVideoInfoBasicInfo from './ImageTextVideoInfoBasicInfo.vue'
|
||||
import ImageTextVideoInfoProgress from './ImageTextVideoInfoProgress.vue'
|
||||
import ImageTextVideoInfoConfig from './ImageTextVideoInfoConfig.vue'
|
||||
|
||||
const message = useMessage()
|
||||
const dialog = useDialog()
|
||||
|
||||
// 定义 emits
|
||||
const emit = defineEmits(['delete-task', 'edit-task', 'config-change'])
|
||||
|
||||
// 定义 props
|
||||
const props = defineProps({
|
||||
task: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
// 编辑任务
|
||||
function handleEdit() {
|
||||
emit('edit-task', props.task)
|
||||
message.info(`编辑任务: ${props.task.name}`)
|
||||
}
|
||||
|
||||
// 删除任务
|
||||
function handleDelete() {
|
||||
dialog.warning({
|
||||
title: '确认删除',
|
||||
content: `确定要删除任务 "${props.task.name}" 吗?此操作不可恢复。`,
|
||||
positiveText: '删除',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: () => {
|
||||
emit('delete-task', props.task)
|
||||
message.success('任务已删除')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 配置变更
|
||||
function handleConfigChange() {
|
||||
emit('config-change', props.task)
|
||||
message.info('配置已更新')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.task-detail {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.detail-header {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
.task-detail::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.task-detail::-webkit-scrollbar-track {
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.task-detail::-webkit-scrollbar-thumb {
|
||||
border-radius: 3px;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,535 @@
|
||||
<template>
|
||||
<div class="table-container">
|
||||
<div class="table-header">
|
||||
<n-space justify="space-between" align="center">
|
||||
<n-text strong>任务列表</n-text>
|
||||
<n-space align="center">
|
||||
<n-space align="center" size="small">
|
||||
<n-text>分页</n-text>
|
||||
<n-switch v-model:value="paginationEnabled" @update:value="handlePaginationToggle" />
|
||||
</n-space>
|
||||
<n-space align="center" size="small">
|
||||
<n-text>右侧面板</n-text>
|
||||
<n-switch v-model:value="showRightPanel" @update:value="handleRightPanelToggle" />
|
||||
</n-space>
|
||||
<n-button type="primary" size="small" @click="handleAdd">
|
||||
<template #icon>
|
||||
<n-icon><AddOutline /></n-icon>
|
||||
</template>
|
||||
新增任务
|
||||
</n-button>
|
||||
</n-space>
|
||||
</n-space>
|
||||
</div>
|
||||
|
||||
<n-data-table
|
||||
:columns="columns"
|
||||
:data="tableData"
|
||||
:loading="loading"
|
||||
:pagination="paginationEnabled ? paginationReactive : false"
|
||||
:row-key="(row) => row.id"
|
||||
:scroll-x="1400"
|
||||
:max-height="tableMaxHeight"
|
||||
:min-height="tableMinHeight"
|
||||
class="data-table"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, h, computed, onMounted, onUnmounted, watch } from 'vue'
|
||||
import {
|
||||
NSpace,
|
||||
NText,
|
||||
NButton,
|
||||
NIcon,
|
||||
NDataTable,
|
||||
NTag,
|
||||
NSwitch,
|
||||
NImage,
|
||||
NInput,
|
||||
useMessage
|
||||
} from 'naive-ui'
|
||||
import { AddOutline, EyeOutline, PlayOutline, PauseOutline } from '@vicons/ionicons5'
|
||||
import { define } from '@/define/define'
|
||||
import ImageTextVideoInfoVideoConfig from './ImageTextVideoInfoVideoConfig.vue'
|
||||
import ImageTextVideoInfoVideoListInfo from './ImageTextVideoInfoVideoListInfo.vue'
|
||||
|
||||
const message = useMessage()
|
||||
|
||||
// 定义 emits
|
||||
const emit = defineEmits([
|
||||
'view-detail',
|
||||
'toggle-status',
|
||||
'add-task',
|
||||
'update-prompt',
|
||||
'update-method',
|
||||
'update-note',
|
||||
'toggle-right-panel'
|
||||
])
|
||||
|
||||
// 定义 props
|
||||
const props = defineProps({
|
||||
tableData: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
showRightPanel: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
})
|
||||
|
||||
// 分页开关状态
|
||||
const paginationEnabled = ref(true)
|
||||
|
||||
// 右侧面板开关状态
|
||||
const showRightPanel = ref(props.showRightPanel)
|
||||
|
||||
// 监听 props 变化
|
||||
watch(
|
||||
() => props.showRightPanel,
|
||||
(newVal) => {
|
||||
showRightPanel.value = newVal
|
||||
}
|
||||
)
|
||||
|
||||
// 窗口高度响应式
|
||||
const windowHeight = ref(window.innerHeight)
|
||||
|
||||
// 计算表格最大高度和最小高度
|
||||
const tableMaxHeight = computed(() => {
|
||||
// 计算可用高度:窗口高度 - 顶部预留空间 - 表格头部高度 - 分页高度 - 底部边距
|
||||
const topReserved = 60 // 顶部预留空间
|
||||
const headerHeight = 48 // 表格头部高度
|
||||
const paginationHeight = paginationEnabled.value ? 40 : 0 // 分页高度:开启时60px,关闭时10px边距
|
||||
const bottomMargin = 15 // 底部边距
|
||||
|
||||
const availableHeight =
|
||||
windowHeight.value - topReserved - headerHeight - paginationHeight - bottomMargin
|
||||
|
||||
// 根据分页状态设置不同的最小高度
|
||||
const minHeight = paginationEnabled.value ? 300 : 400
|
||||
|
||||
return Math.max(availableHeight, minHeight)
|
||||
})
|
||||
|
||||
const tableMinHeight = computed(() => {
|
||||
// 分页开启时最小高度300px,关闭时最小高度400px(因为要显示更多数据)
|
||||
return paginationEnabled.value ? 300 : 400
|
||||
})
|
||||
|
||||
// 监听窗口大小变化
|
||||
const handleResize = () => {
|
||||
windowHeight.value = window.innerHeight
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('resize', handleResize)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', handleResize)
|
||||
})
|
||||
|
||||
// 分页配置
|
||||
const paginationReactive = reactive({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
showSizePicker: true,
|
||||
pageSizes: [10, 20, 50],
|
||||
onChange: (page) => {
|
||||
paginationReactive.page = page
|
||||
},
|
||||
onUpdatePageSize: (pageSize) => {
|
||||
paginationReactive.pageSize = pageSize
|
||||
paginationReactive.page = 1
|
||||
}
|
||||
})
|
||||
|
||||
// 通用的无padding列样式类名
|
||||
const noPaddingColumnClass = 'no-padding-column'
|
||||
|
||||
// 表格列定义
|
||||
const columns = [
|
||||
{
|
||||
title: '任务名称',
|
||||
key: 'name',
|
||||
width: 120,
|
||||
fixed: 'left'
|
||||
},
|
||||
{
|
||||
title: '图片',
|
||||
key: 'image',
|
||||
minWidth: 80,
|
||||
width: 160,
|
||||
maxWidth: 160,
|
||||
className: noPaddingColumnClass,
|
||||
render(row) {
|
||||
if (row.outImagePath) {
|
||||
return h(
|
||||
'div',
|
||||
{
|
||||
style: {
|
||||
height: '130px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}
|
||||
},
|
||||
[
|
||||
h(NImage, {
|
||||
src: row.outImagePath,
|
||||
height: 130,
|
||||
objectFit: 'cover',
|
||||
style: {
|
||||
borderRadius: '4px',
|
||||
maxWidth: '160px',
|
||||
height: '130px'
|
||||
},
|
||||
fallbackSrc: define.zhanwei_image,
|
||||
showToolbar: false,
|
||||
showToolbarTooltip: false
|
||||
})
|
||||
]
|
||||
)
|
||||
} else {
|
||||
return h(
|
||||
'div',
|
||||
{
|
||||
style: {
|
||||
height: '130px',
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: '#f5f5f5',
|
||||
borderRadius: '4px',
|
||||
color: '#999',
|
||||
fontSize: '12px'
|
||||
}
|
||||
},
|
||||
'图片不存在'
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '图片链接',
|
||||
key: 'image_url',
|
||||
width: 150,
|
||||
className: noPaddingColumnClass,
|
||||
render(row) {
|
||||
return h(
|
||||
'div',
|
||||
{
|
||||
style: {
|
||||
height: '130px',
|
||||
width: '95%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: '4px'
|
||||
}
|
||||
},
|
||||
[
|
||||
h(NInput, {
|
||||
value: row.videoMessage?.imageUrl || '',
|
||||
type: 'textarea',
|
||||
placeholder: '请输入图片链接...',
|
||||
resizable: false,
|
||||
style: {
|
||||
width: '100%',
|
||||
height: '100%'
|
||||
},
|
||||
inputProps: {
|
||||
style: {
|
||||
height: '100%',
|
||||
resize: 'none'
|
||||
},
|
||||
spellcheck: false
|
||||
},
|
||||
onUpdateValue: (value) => {
|
||||
row.note = value
|
||||
emit('update-note', row.id, value)
|
||||
}
|
||||
})
|
||||
]
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '视频配置',
|
||||
key: 'videoConfig',
|
||||
minWidth: 300,
|
||||
className: noPaddingColumnClass,
|
||||
render(row) {
|
||||
return h(ImageTextVideoInfoVideoConfig, {
|
||||
taskId: row.id,
|
||||
videoMessage: row?.videoMessage,
|
||||
onUpdateMethod: (taskId, method) => {
|
||||
row.videoMethod = method
|
||||
emit('update-method', taskId, method)
|
||||
},
|
||||
onUpdatePrompt: (taskId, prompt) => {
|
||||
row.videoPrompt = prompt
|
||||
emit('update-prompt', taskId, prompt)
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '视频信息',
|
||||
key: 'videoInfo',
|
||||
minWidth: 120,
|
||||
width: 160,
|
||||
className: noPaddingColumnClass,
|
||||
render(row) {
|
||||
if (row.generateVideoPath) {
|
||||
return h(
|
||||
'div',
|
||||
{
|
||||
style: {
|
||||
height: '130px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}
|
||||
},
|
||||
[
|
||||
h('video', {
|
||||
src: row.generateVideoPath,
|
||||
controls: true,
|
||||
style: {
|
||||
height: '130px',
|
||||
maxWidth: '160px',
|
||||
objectFit: 'cover',
|
||||
borderRadius: '4px'
|
||||
}
|
||||
})
|
||||
]
|
||||
)
|
||||
} else {
|
||||
return h(
|
||||
'div',
|
||||
{
|
||||
style: {
|
||||
height: '130px',
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: '#f5f5f5',
|
||||
borderRadius: '4px',
|
||||
color: '#999',
|
||||
fontSize: '12px'
|
||||
}
|
||||
},
|
||||
'暂无视频'
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '视频选择',
|
||||
key: 'videoSelection',
|
||||
minWidth: 200,
|
||||
width: 200,
|
||||
className: noPaddingColumnClass,
|
||||
render(row) {
|
||||
return h(ImageTextVideoInfoVideoListInfo, {
|
||||
taskData: row,
|
||||
videoList: row.subVideoPath || []
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 160,
|
||||
fixed: 'right',
|
||||
render(row) {
|
||||
return h(
|
||||
NSpace,
|
||||
{ size: 'small' },
|
||||
{
|
||||
default: () => [
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
size: 'small',
|
||||
type: 'primary',
|
||||
onClick: () => handleViewDetail(row)
|
||||
},
|
||||
{
|
||||
default: () => '查看',
|
||||
icon: () => h(NIcon, null, { default: () => h(EyeOutline) })
|
||||
}
|
||||
),
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
size: 'small',
|
||||
type: row.status === '进行中' ? 'warning' : 'success',
|
||||
onClick: () => handleToggleStatus(row)
|
||||
},
|
||||
{
|
||||
default: () => (row.status === '进行中' ? '暂停' : '启动'),
|
||||
icon: () =>
|
||||
h(NIcon, null, {
|
||||
default: () => (row.status === '进行中' ? h(PauseOutline) : h(PlayOutline))
|
||||
})
|
||||
}
|
||||
)
|
||||
]
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
// 获取状态类型
|
||||
function getStatusType(status) {
|
||||
const statusMap = {
|
||||
进行中: 'info',
|
||||
已完成: 'success',
|
||||
暂停: 'warning',
|
||||
失败: 'error'
|
||||
}
|
||||
return statusMap[status] || 'default'
|
||||
}
|
||||
|
||||
// 查看详情
|
||||
function handleViewDetail(row) {
|
||||
emit('view-detail', row)
|
||||
message.info(`查看任务: ${row.name}`)
|
||||
}
|
||||
|
||||
// 切换状态
|
||||
function handleToggleStatus(row) {
|
||||
emit('toggle-status', row)
|
||||
}
|
||||
|
||||
// 新增任务
|
||||
function handleAdd() {
|
||||
emit('add-task')
|
||||
message.info('新增任务功能开发中...')
|
||||
}
|
||||
|
||||
// 处理分页开关切换
|
||||
function handlePaginationToggle(value) {
|
||||
if (value) {
|
||||
// 开启分页
|
||||
paginationReactive.page = 1 // 重置到第一页
|
||||
message.info('已开启分页,表格高度已调整')
|
||||
} else {
|
||||
// 关闭分页,显示全部数据
|
||||
message.info('已关闭分页,显示全部数据,表格高度已调整')
|
||||
}
|
||||
}
|
||||
|
||||
// 处理右侧面板开关切换
|
||||
function handleRightPanelToggle(value) {
|
||||
showRightPanel.value = value
|
||||
emit('toggle-right-panel', value)
|
||||
if (value) {
|
||||
message.info('右侧面板已显示')
|
||||
} else {
|
||||
message.info('右侧面板已隐藏,点击查看按钮可在抽屉中查看详情')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.table-container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.table-header {
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid var(--n-border-color);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 表格样式优化 */
|
||||
.data-table {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.data-table :deep(.n-data-table) {
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.data-table :deep(.n-data-table-th) {
|
||||
font-weight: 600;
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
.data-table :deep(.n-data-table-td) {
|
||||
border-bottom: 1px solid var(--n-border-color);
|
||||
}
|
||||
|
||||
.data-table :deep(.n-data-table__body) {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.data-table :deep(.n-data-table__body)::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.data-table :deep(.n-data-table__body)::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.data-table :deep(.n-data-table__body)::-webkit-scrollbar-thumb {
|
||||
background: #c1c1c1;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.data-table :deep(.n-data-table__body)::-webkit-scrollbar-thumb:hover {
|
||||
background: #a8a8a8;
|
||||
}
|
||||
|
||||
/* 按钮样式 */
|
||||
.data-table :deep(.n-button) {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.data-table :deep(.n-button:hover) {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* 进度条样式 */
|
||||
.data-table :deep(.n-progress) {
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
/* 通用的无padding列样式 */
|
||||
.data-table :deep(.no-padding-column) {
|
||||
padding: 2px !important;
|
||||
}
|
||||
|
||||
.data-table :deep(.no-padding-column .n-data-table-td__content) {
|
||||
padding: 2px !important;
|
||||
}
|
||||
|
||||
/* 解决图片下方多余间隔问题 */
|
||||
.data-table :deep(.no-padding-column .n-image) {
|
||||
display: block;
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
.data-table :deep(.no-padding-column .n-image img) {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,138 @@
|
||||
<template>
|
||||
<div class="video-config-container">
|
||||
<!-- 转视频方式选择和进度条 -->
|
||||
<div class="method-section">
|
||||
<div class="method-row">
|
||||
<n-select
|
||||
v-model:value="videoMessage.videoType"
|
||||
:options="videoMethodOptions"
|
||||
placeholder="选择转视频方式"
|
||||
size="small"
|
||||
@update:value="handleMethodChange"
|
||||
class="method-select"
|
||||
/>
|
||||
<n-tag type="primary" size="small" class="method-progress">
|
||||
{{ videoMessage.status ?? 'wait' }}
|
||||
</n-tag>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 视频提示词输入 -->
|
||||
<div class="prompt-section">
|
||||
<n-input
|
||||
v-model:value="videoMessage.prompt"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入视频提示词..."
|
||||
:autosize="{ minRows: 3, maxRows: 6 }"
|
||||
@update:value="handlePromptChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, onMounted, toRef } from 'vue'
|
||||
import { NSelect, NInput, NTag } from 'naive-ui'
|
||||
import { GetImageToVideoModelsOptions } from '@/define/enum/video'
|
||||
|
||||
// 定义 emits
|
||||
const emit = defineEmits(['update-method', 'update-prompt'])
|
||||
|
||||
// 定义 props
|
||||
const props = defineProps({
|
||||
videoMessage: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
taskId: {
|
||||
type: [String, Number],
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
// 转视频方式选项
|
||||
const videoMethodOptions = GetImageToVideoModelsOptions()
|
||||
|
||||
const videoMessage = ref({})
|
||||
|
||||
// 处理方式变更
|
||||
function handleMethodChange(value) {
|
||||
emit('update-method', props.taskId, value)
|
||||
}
|
||||
|
||||
// 处理提示词变更
|
||||
function handlePromptChange(value) {
|
||||
emit('update-prompt', props.taskId, value)
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
if (props.videoMessage) {
|
||||
videoMessage.value = props.videoMessage
|
||||
} else {
|
||||
videoMessage.value = {
|
||||
videoType: 'RUNWAY',
|
||||
prompt: ''
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.video-config-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 130px; /* 与图片行高度一致 */
|
||||
gap: 6px;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.method-section {
|
||||
flex-shrink: 0;
|
||||
height: 28px; /* 固定选择框高度 */
|
||||
}
|
||||
|
||||
.method-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.method-select {
|
||||
width: 120px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.prompt-section {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0; /* 确保可以缩小 */
|
||||
}
|
||||
|
||||
.prompt-section :deep(.n-input) {
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.prompt-section :deep(.n-input .n-input__input-el) {
|
||||
resize: none;
|
||||
line-height: 1.4;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.prompt-section :deep(.n-input .n-input-wrapper) {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.prompt-section :deep(.n-input .n-input__textarea) {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.prompt-section :deep(.n-input .n-input__textarea-el) {
|
||||
height: 100%;
|
||||
resize: none;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,731 @@
|
||||
<template>
|
||||
<div>
|
||||
<!-- 视频选择缩略图 -->
|
||||
<div class="video-thumbnail-container">
|
||||
<div v-if="videoList.length === 0" class="no-video-thumbnail">
|
||||
<div class="empty-state">
|
||||
<n-text depth="3">暂无视频</n-text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="thumbnail-grid">
|
||||
<div
|
||||
v-for="(video, index) in videoList.slice(0, 3)"
|
||||
:key="index"
|
||||
class="thumbnail-item"
|
||||
@click="handleShowModal"
|
||||
>
|
||||
<video :src="video" class="thumbnail-video" muted preload="metadata" />
|
||||
|
||||
<!-- 如果有更多视频,在最后一个缩略图上显示数量 -->
|
||||
<div v-if="index === 2 && videoList.length > 3" class="more-count">
|
||||
+{{ videoList.length - 3 }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 视频弹窗 -->
|
||||
<n-modal
|
||||
v-model:show="showModal"
|
||||
:mask-closable="false"
|
||||
preset="card"
|
||||
style="width: 90%; max-width: 1200px"
|
||||
:title="'视频信息详情:' + taskData.name"
|
||||
size="huge"
|
||||
:segmented="true"
|
||||
>
|
||||
<n-alert type="info" closable :show-icon="false" :style="{
|
||||
marginBottom : '16px'
|
||||
}">
|
||||
修改选择的视频之后,请点击 保存视频选择 按钮将操作进行保存生效!!!
|
||||
</n-alert>
|
||||
|
||||
<div v-if="taskData" class="video-modal-content">
|
||||
<!-- 选中的视频和任务信息 -->
|
||||
<div class="selected-video-section">
|
||||
<div class="selected-video-left">
|
||||
<div class="current-video-container">
|
||||
<video
|
||||
v-if="currentSelectedVideo"
|
||||
:src="currentSelectedVideo"
|
||||
controls
|
||||
class="current-video-player"
|
||||
preload="metadata"
|
||||
/>
|
||||
<div v-else class="no-selected-video">
|
||||
<n-text depth="3">请选择一个视频</n-text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="task-info-right">
|
||||
<n-card
|
||||
title="📋 任务详情"
|
||||
size="small"
|
||||
:bordered="true"
|
||||
:style="{
|
||||
height: '100%'
|
||||
}"
|
||||
>
|
||||
<template #header-extra>
|
||||
<n-button size="small" type="primary" @click="handleSaveVideoSelection">
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<Save />
|
||||
</n-icon>
|
||||
</template>
|
||||
保存视频选择
|
||||
</n-button>
|
||||
</template>
|
||||
|
||||
<div class="info-section">
|
||||
<div class="info-item">
|
||||
<div class="info-label">
|
||||
<n-icon size="16" class="info-icon">
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M11,16.5L6.5,12L7.91,10.59L11,13.67L16.59,8.09L18,9.5L11,16.5Z"
|
||||
/>
|
||||
</svg>
|
||||
</n-icon>
|
||||
<n-text strong>分镜名称</n-text>
|
||||
</div>
|
||||
<div class="info-value">
|
||||
<n-text>{{ taskData.name }}</n-text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-item" v-if="taskData.id">
|
||||
<div class="info-label">
|
||||
<n-icon size="16" class="info-icon">
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M2,4C2,2.89 2.9,2 4,2H7V4H4V7H2V4M22,4V7H20V4H17V2H20C21.1,2 22,2.89 22,4M20,20V17H22V20C22,21.11 21.1,22 20,22H17V20H20M2,17V20C2,21.11 2.9,22 4,22H7V20H4V17H2M12,8A4,4 0 0,1 16,12A4,4 0 0,1 12,16A4,4 0 0,1 8,12A4,4 0 0,1 12,8M12,10A2,2 0 0,0 10,12A2,2 0 0,0 12,14A2,2 0 0,0 14,12A2,2 0 0,0 12,10Z"
|
||||
/>
|
||||
</svg>
|
||||
</n-icon>
|
||||
<n-text strong>任务ID</n-text>
|
||||
</div>
|
||||
<div class="info-value">
|
||||
<n-text depth="2" code>{{ videoMessage.taskId }}</n-text>
|
||||
<n-button
|
||||
size="tiny"
|
||||
type="primary"
|
||||
ghost
|
||||
@click="copyTaskId"
|
||||
class="copy-button"
|
||||
style="margin-left: 8px"
|
||||
>
|
||||
<template #icon>
|
||||
<n-icon size="12">
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"
|
||||
/>
|
||||
</svg>
|
||||
</n-icon>
|
||||
</template>
|
||||
复制
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-item" v-if="videoMessage.status">
|
||||
<div class="info-label">
|
||||
<n-icon size="16" class="info-icon">
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M12,17A5,5 0 0,1 7,12A5,5 0 0,1 12,7A5,5 0 0,1 17,12A5,5 0 0,1 12,17Z"
|
||||
/>
|
||||
</svg>
|
||||
</n-icon>
|
||||
<n-text strong>状态</n-text>
|
||||
</div>
|
||||
<div class="info-value">
|
||||
<n-tag :type="getStatusTagType(videoMessage.status)" size="small" round>
|
||||
{{ videoMessage.status }}
|
||||
</n-tag>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-item" v-if="videoMessage.videoType">
|
||||
<div class="info-label">
|
||||
<n-icon size="16" class="info-icon">
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M17,10.5V7A1,1 0 0,0 16,6H4A1,1 0 0,0 3,7V17A1,1 0 0,0 4,18H16A1,1 0 0,0 17,17V13.5L21,17.5V6.5L17,10.5Z"
|
||||
/>
|
||||
</svg>
|
||||
</n-icon>
|
||||
<n-text strong>转视频类型</n-text>
|
||||
</div>
|
||||
<div class="info-value">
|
||||
<n-tag type="info" size="small" round>
|
||||
{{ GetImageToVideoModelsLabel(videoMessage.videoType) }}
|
||||
</n-tag>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-item" v-if="videoList.length > 0">
|
||||
<div class="info-label">
|
||||
<n-icon size="16" class="info-icon">
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M4,6H2V20A2,2 0 0,0 4,22H18V20H4V6M20,2H8A2,2 0 0,0 6,4V16A2,2 0 0,0 8,18H20A2,2 0 0,0 22,16V4A2,2 0 0,0 20,2M12,4.5L17,9L12,13.5V10.5H8V7.5H12V4.5Z"
|
||||
/>
|
||||
</svg>
|
||||
</n-icon>
|
||||
<n-text strong>视频总数</n-text>
|
||||
</div>
|
||||
<div class="info-value">
|
||||
<n-tag type="success" size="small" round :bordered="false">
|
||||
{{ videoList.length }} 个
|
||||
</n-tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</n-card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="video-grid">
|
||||
<n-grid :cols="gridCols" :x-gap="12" :y-gap="12">
|
||||
<n-grid-item v-for="(video, index) in videoList" :key="index">
|
||||
<n-card
|
||||
size="small"
|
||||
:bordered="true"
|
||||
class="video-card"
|
||||
:class="{ selected: currentSelectedVideo === video }"
|
||||
@click="selectVideo(video)"
|
||||
>
|
||||
<template #header>
|
||||
<n-text>视频 {{ index + 1 }}</n-text>
|
||||
</template>
|
||||
|
||||
<div class="video-container">
|
||||
<video :src="video" class="video-player" preload="metadata" />
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<n-space vertical size="small">
|
||||
<div v-if="video.duration">
|
||||
<n-text depth="3">时长:{{ video.duration }}s</n-text>
|
||||
</div>
|
||||
<div v-if="video.size">
|
||||
<n-text depth="3">大小:{{ video.size }}</n-text>
|
||||
</div>
|
||||
<div v-if="video.createdAt">
|
||||
<n-text depth="3">创建时间:{{ formatDate(video.createdAt) }}</n-text>
|
||||
</div>
|
||||
</n-space>
|
||||
</template>
|
||||
</n-card>
|
||||
</n-grid-item>
|
||||
</n-grid>
|
||||
</div>
|
||||
|
||||
<div v-if="videoList.length === 0" class="no-video">
|
||||
<n-text depth="3">暂无视频信息</n-text>
|
||||
</div>
|
||||
</div>
|
||||
<n-empty description="当前分镜没有转视频相关的信息,请先进行转视频操作!" v-else> </n-empty>
|
||||
</n-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { NModal, NText, NCard, NGrid, NGridItem, NSpace, NTag, NIcon, NButton } from 'naive-ui'
|
||||
import { GetImageToVideoModelsLabel } from '@/define/enum/video'
|
||||
import { isEmpty } from 'lodash'
|
||||
|
||||
import { Save } from '@vicons/ionicons5'
|
||||
import { TimeDelay } from '@/define/Tools/time'
|
||||
|
||||
// 定义 props
|
||||
const props = defineProps({
|
||||
taskData: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
videoList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
})
|
||||
|
||||
// 弹窗显示状态
|
||||
const showModal = ref(false)
|
||||
|
||||
const videoMessage = ref({})
|
||||
|
||||
const message = useMessage()
|
||||
|
||||
// 当前选中的视频
|
||||
const currentSelectedVideo = ref(props.taskData.generateVideoPath)
|
||||
|
||||
// 窗口宽度
|
||||
const windowWidth = ref(window.innerWidth)
|
||||
|
||||
// 响应式列数
|
||||
const gridCols = computed(() => {
|
||||
if (windowWidth.value <= 480) {
|
||||
return 1 // 手机:1列
|
||||
} else if (windowWidth.value <= 768) {
|
||||
return 2 // 平板:2列
|
||||
} else if (windowWidth.value <= 1024) {
|
||||
return 3 // 小桌面:3列
|
||||
} else {
|
||||
return 4 // 大桌面:4列
|
||||
}
|
||||
})
|
||||
|
||||
// 监听窗口大小变化
|
||||
const handleResize = () => {
|
||||
windowWidth.value = window.innerWidth
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('resize', handleResize)
|
||||
videoMessage.value = props.taskData.videoMessage || {}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', handleResize)
|
||||
})
|
||||
|
||||
// 显示弹窗
|
||||
function handleShowModal() {
|
||||
// 默认选中第一个视频
|
||||
if (props.videoList.length > 0 && isEmpty(props.taskData.generateVideoPath)) {
|
||||
currentSelectedVideo.value = props.videoList[0]
|
||||
} else {
|
||||
currentSelectedVideo.value = props.taskData.generateVideoPath
|
||||
}
|
||||
showModal.value = true
|
||||
}
|
||||
|
||||
// 选择视频
|
||||
function selectVideo(video) {
|
||||
currentSelectedVideo.value = video
|
||||
}
|
||||
|
||||
async function handleSaveVideoSelection() {
|
||||
if (isEmpty(currentSelectedVideo.value)) {
|
||||
message.warning('请选择一个视频')
|
||||
return
|
||||
}
|
||||
|
||||
// 显示加载状态
|
||||
const loadingMessage = message.loading('正在保存视频选择...', {
|
||||
duration: 1000
|
||||
})
|
||||
try {
|
||||
// 清理视频路径,去除时间戳参数
|
||||
const cleanVideoPath = currentSelectedVideo.value?.split('?t=')[0] || currentSelectedVideo.value
|
||||
|
||||
// 验证任务ID
|
||||
if (!props.taskData?.id) {
|
||||
message.error('任务ID不存在')
|
||||
return
|
||||
}
|
||||
|
||||
// 调用数据库更新方法
|
||||
const res = await window.db.UpdateBookTaskDetailData(props.taskData.id, {
|
||||
generateVideoPath: cleanVideoPath
|
||||
})
|
||||
|
||||
// 检查返回结果
|
||||
if (res.code == 1) {
|
||||
message.success('视频选择已保存成功')
|
||||
|
||||
// 更新本地数据
|
||||
if (props.taskData) {
|
||||
props.taskData.generateVideoPath = cleanVideoPath + '?t=' + new Date().getTime()
|
||||
}
|
||||
} else {
|
||||
message.error('保存视频选择失败: ' + res.message)
|
||||
return
|
||||
}
|
||||
} catch (error) {
|
||||
message.error(`保存视频选择失败: ${error.message || error}`)
|
||||
} finally {
|
||||
// 关闭加载状态
|
||||
loadingMessage.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
// 获取状态标签类型
|
||||
function getStatusTagType(status) {
|
||||
const statusMap = {
|
||||
进行中: 'info',
|
||||
已完成: 'success',
|
||||
完成: 'success',
|
||||
暂停: 'warning',
|
||||
失败: 'error',
|
||||
错误: 'error',
|
||||
等待: 'default'
|
||||
}
|
||||
return statusMap[status] || 'primary'
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
function 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',
|
||||
second: '2-digit'
|
||||
})
|
||||
}
|
||||
|
||||
// 复制任务ID
|
||||
async function copyTaskId() {
|
||||
try {
|
||||
const taskId = videoMessage.value.taskId
|
||||
if (!taskId) {
|
||||
message.warning('任务ID不存在')
|
||||
return
|
||||
}
|
||||
|
||||
await navigator.clipboard.writeText(taskId)
|
||||
message.success('任务ID已复制到剪贴板')
|
||||
} catch (err) {
|
||||
console.error('复制失败:', err)
|
||||
message.error('复制失败,请手动复制')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 视频缩略图样式 */
|
||||
.video-thumbnail-container {
|
||||
height: 130px;
|
||||
width: 100%;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.no-video-thumbnail {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 4px;
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.thumbnail-grid {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.thumbnail-item {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
border: 1px solid #e0e0e0;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.thumbnail-item:hover {
|
||||
border-color: #18a058;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.thumbnail-video {
|
||||
width: 60px;
|
||||
height: 45px;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.more-count {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* 视频弹窗样式 */
|
||||
.video-modal-content {
|
||||
max-height: 75vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* 选中的视频和任务信息区域 */
|
||||
.selected-video-section {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
margin-bottom: 30px;
|
||||
padding: 20px;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.selected-video-left {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.current-video-container {
|
||||
width: 100%;
|
||||
aspect-ratio: 16/9;
|
||||
background-color: #000;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.current-video-player {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.no-selected-video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #f5f5f5;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.task-info-right {
|
||||
flex: 1;
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
.task-detail-card :deep(.n-card-header__main) {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.info-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 16px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid #18a058;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.info-item:hover {
|
||||
background: #e8f5e8;
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
.info-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.info-icon {
|
||||
color: #18a058;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.copy-button {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.copy-button:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
text-align: center;
|
||||
padding: 12px 8px;
|
||||
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||
border-radius: 8px;
|
||||
color: white;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.stat-item:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.stat-item:nth-child(2) {
|
||||
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
||||
}
|
||||
|
||||
.video-grid {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.video-card {
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.video-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.video-card.selected {
|
||||
border-color: #18a058;
|
||||
box-shadow: 0 0 0 2px rgba(24, 160, 88, 0.2);
|
||||
}
|
||||
|
||||
.video-card.selected :deep(.n-card-header) {
|
||||
background-color: #f0f9ff;
|
||||
}
|
||||
|
||||
.video-container {
|
||||
width: 100%;
|
||||
aspect-ratio: 16/9;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.video-player {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.no-video {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 480px) {
|
||||
.selected-video-section {
|
||||
flex-direction: column;
|
||||
padding: 12px;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.task-info-right {
|
||||
min-width: unset;
|
||||
}
|
||||
|
||||
.video-grid {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
min-width: unset;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.selected-video-section {
|
||||
flex-direction: column;
|
||||
padding: 16px;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.task-info-right {
|
||||
min-width: unset;
|
||||
}
|
||||
|
||||
.video-grid {
|
||||
margin-top: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.selected-video-section {
|
||||
gap: 16px;
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.task-info-right {
|
||||
min-width: 280px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1400px) {
|
||||
.selected-video-section {
|
||||
padding: 24px;
|
||||
gap: 24px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -25,7 +25,7 @@ import MJImagePackage from './MJImagePackage.vue'
|
||||
import { MJImageType } from '@/define/enum/mjEnum'
|
||||
import { isEmpty } from 'lodash'
|
||||
import { GetMJUrlOptions } from '@/define/api/apiUrlDefine'
|
||||
import { GetImageProxyUrlOptions, ImagePackageProxyOptions } from '@/define/data/settingData'
|
||||
import { GetImageProxyUrlOptions } from '@/define/data/settingData'
|
||||
|
||||
let softwareStore = useSoftwareStore()
|
||||
let optionStore = useOptionStore()
|
||||
|
||||
@ -103,6 +103,16 @@ const routes = [
|
||||
name: 'manage_book',
|
||||
component: () => import('./components/Book/ManageBookDetail.vue')
|
||||
},
|
||||
{
|
||||
path: '/image_text_video',
|
||||
name: 'image_text_video',
|
||||
component: () => import('./components/ImageTextVideo/ImageTextVideoHome.vue')
|
||||
},
|
||||
{
|
||||
path: '/image_text_video_info/:id',
|
||||
name: 'image_text_video_info',
|
||||
component: () => import('./components/ImageTextVideo/ImageTextVideoInfoHome.vue')
|
||||
},
|
||||
{
|
||||
path: '/test_options',
|
||||
name: 'test_options',
|
||||
|
||||
@ -266,7 +266,8 @@ export const useReverseManageStore = defineStore('reverseManage', {
|
||||
...item,
|
||||
outImagePath: item.outImagePath ? item.outImagePath + '?t=' + new Date().getTime() : undefined,
|
||||
subImagePath: item.subImagePath ? item.subImagePath.map(item => item + '?t=' + new Date().getTime()) : [],
|
||||
oldImage: item.oldImage ? item.oldImage + '?t=' + new Date().getTime() : undefined
|
||||
oldImage: item.oldImage ? item.oldImage + '?t=' + new Date().getTime() : undefined,
|
||||
generateVideoPath: item.generateVideoPath ? item.generateVideoPath + '?t=' + new Date().getTime() : undefined,
|
||||
}
|
||||
})
|
||||
// 这边开始修改数据,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user