v 3.4.2
1. 新增 图/文转视频 菜单界面,专注实现图/文转视频(目前只集成了 MJ VIDEO) 1. 全新的界面排列,小说列表和批次任务更加分明 2. 添加转视频进度,在主界面即可看到转视频的比例 3. 单独的界面去处理图转视频,避免表格数据过多繁琐 4. 新增分页显示,界面加载更快,也可切换不分页,需要更多的事件等待加载 5. 单独操作面板,参数修改处理更加清晰,支持多种模式显示,右侧固定或抽屉模式 6. 批量设置转视频配置,可以批量修改分类 7. 友好的选择视频界面 2. 重写 软件导出剪映,修复若干草稿导出问题 1. 修复导出剪映文案和图片对齐会有些许对不上,时长越长越明显 2. 修复导出草稿关键帧部分问题 3. 导出的文案通过分镜自动导入,不再需要手动选择SRT 3. 美化 生成草稿界面 弹窗,优化部分逻辑 1. 删除选择SRT文件,SRT根据聚合推文中导入的SRT自动生成草稿 2. 只需选择配音文件即可,配音文件和导入的SRT请自行对应 3. 背景音乐不在内部设置,自行选择文件夹或者是MP3、WAV文件 4. 背景音乐选择文件夹则读取文件夹,随机获取一个 5. 背景音乐选择指定的音乐文件则使用选择的
This commit is contained in:
parent
c1d6fe181d
commit
82ec437b5d
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "laitool",
|
"name": "laitool",
|
||||||
"version": "3.4.1",
|
"version": "3.4.2",
|
||||||
"description": "An AI tool for image processing, video processing, and other functions.",
|
"description": "An AI tool for image processing, video processing, and other functions.",
|
||||||
"main": "./out/main/index.js",
|
"main": "./out/main/index.js",
|
||||||
"author": "laitool.cn",
|
"author": "laitool.cn",
|
||||||
@ -89,6 +89,7 @@
|
|||||||
"resources/image/zhanwei.png",
|
"resources/image/zhanwei.png",
|
||||||
"resources/scripts/model/**",
|
"resources/scripts/model/**",
|
||||||
"resources/scripts/Lai.exe",
|
"resources/scripts/Lai.exe",
|
||||||
|
"resources/scripts/xiangbei_jianying_main.exe",
|
||||||
"resources/scripts/discordScript.js",
|
"resources/scripts/discordScript.js",
|
||||||
"resources/tmp/**",
|
"resources/tmp/**",
|
||||||
"resources/icon.ico"
|
"resources/icon.ico"
|
||||||
|
|||||||
@ -121,6 +121,10 @@ elif sys.argv[1] == "-ka":
|
|||||||
shotSplit.get_fram(sys.argv[2], sys.argv[3], sys.argv[4])
|
shotSplit.get_fram(sys.argv[2], sys.argv[3], sys.argv[4])
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
elif sys.argv[1] == "-df":
|
||||||
|
shotSplit.get_fram(sys.argv[2], sys.argv[3], sys.argv[4])
|
||||||
|
pass
|
||||||
|
|
||||||
# # 智能分镜。字幕识别
|
# # 智能分镜。字幕识别
|
||||||
# elif sys.argv[1] == "-a":
|
# elif sys.argv[1] == "-a":
|
||||||
# print("开始算法分镜:" + sys.argv[2] + " -- 输出文件夹:" + sys.argv[3])
|
# print("开始算法分镜:" + sys.argv[2] + " -- 输出文件夹:" + sys.argv[3])
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
BIN
resources/scripts/xiangbei_jianying_main.exe
Normal file
BIN
resources/scripts/xiangbei_jianying_main.exe
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
@ -6,6 +6,7 @@ let apiUrl = [
|
|||||||
isPackage: false,
|
isPackage: false,
|
||||||
mj_url: {
|
mj_url: {
|
||||||
imagine: 'https://api.laitool.cc/mj/submit/imagine',
|
imagine: 'https://api.laitool.cc/mj/submit/imagine',
|
||||||
|
video: 'https://api.laitool.cc/mj/submit/video',
|
||||||
describe: 'https://api.laitool.cc/mj/submit/describe',
|
describe: 'https://api.laitool.cc/mj/submit/describe',
|
||||||
update_file: 'https://api.laitool.cc/mj/submit/upload-discord-images',
|
update_file: 'https://api.laitool.cc/mj/submit/upload-discord-images',
|
||||||
once_get_task: 'https://api.laitool.cc/mj/task/${id}/fetch'
|
once_get_task: 'https://api.laitool.cc/mj/task/${id}/fetch'
|
||||||
@ -22,6 +23,7 @@ let apiUrl = [
|
|||||||
isPackage: false,
|
isPackage: false,
|
||||||
mj_url: {
|
mj_url: {
|
||||||
imagine: 'https://laitool.net/mj/submit/imagine',
|
imagine: 'https://laitool.net/mj/submit/imagine',
|
||||||
|
video: 'https://laitool.net/mj/submit/video',
|
||||||
describe: 'https://laitool.net/mj/submit/describe',
|
describe: 'https://laitool.net/mj/submit/describe',
|
||||||
update_file: 'https://laitool.net/mj/submit/upload-discord-images',
|
update_file: 'https://laitool.net/mj/submit/upload-discord-images',
|
||||||
once_get_task: 'https://laitool.net/mj/task/${id}/fetch'
|
once_get_task: 'https://laitool.net/mj/task/${id}/fetch'
|
||||||
|
|||||||
@ -1,10 +1,24 @@
|
|||||||
export const SoftwareData = {
|
export const SoftwareData = {
|
||||||
"version": "V3.4.1",
|
"version": "V3.4.2",
|
||||||
"date": "2025-07-08",
|
"date": "2025-08-08",
|
||||||
"notes": [
|
"notes": [
|
||||||
"1. 适配 MJ V7 版本的 oref 参数",
|
"1. 新增图/文转视频菜单界面,专注实现图/文转视频(目前只集成了 MJ VIDEO)",
|
||||||
"2. 恢复超级单证中文版推理模式",
|
" • 全新的界面排列,小说列表和批次任务更加分明",
|
||||||
"3. 出图进度添加本地图片文件是否存在的判断",
|
" • 添加转视频进度,在主界面即可看到转视频的比例",
|
||||||
"4. 新增 推理模式 Laitool提示词专家-全能优化版"
|
" • 单独的界面去处理图转视频,避免表格数据过多繁琐",
|
||||||
|
" • 新增分页显示,界面加载更快,也可切换不分页",
|
||||||
|
" • 单独操作面板,参数修改处理更加清晰,支持多种模式显示",
|
||||||
|
" • 批量设置转视频配置,可以批量修改分类",
|
||||||
|
" • 友好的选择视频界面",
|
||||||
|
"2. 重写软件导出剪映,修复若干草稿导出问题",
|
||||||
|
" • 修复导出剪映文案和图片对齐问题,解决时长越长越明显的对不上问题",
|
||||||
|
" • 修复导出草稿关键帧部分问题",
|
||||||
|
" • 导出的文案通过分镜自动导入,不再需要手动选择SRT",
|
||||||
|
"3. 美化生成草稿界面弹窗,优化部分逻辑",
|
||||||
|
" • 删除选择SRT文件,SRT根据聚合推文中导入的SRT自动生成草稿",
|
||||||
|
" • 只需选择配音文件即可,配音文件和导入的SRT请自行对应",
|
||||||
|
" • 背景音乐不在内部设置,自行选择文件夹或者是MP3、WAV文件",
|
||||||
|
" • 背景音乐选择文件夹则读取文件夹,随机获取一个",
|
||||||
|
" • 背景音乐选择指定的音乐文件则使用选择的"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,6 +16,8 @@ export class BookBackTaskList extends Realm.Object<BookBackTaskList> {
|
|||||||
startTime: number
|
startTime: number
|
||||||
endTime: number
|
endTime: number
|
||||||
messageName?: string
|
messageName?: string
|
||||||
|
taskId?: string // 任务ID,可能是视频生成任务的ID
|
||||||
|
taskMessage?: string // 任务消息,可能是视频生成任务的消息
|
||||||
|
|
||||||
static schema: ObjectSchema = {
|
static schema: ObjectSchema = {
|
||||||
name: 'BookBackTaskList',
|
name: 'BookBackTaskList',
|
||||||
@ -33,7 +35,9 @@ export class BookBackTaskList extends Realm.Object<BookBackTaskList> {
|
|||||||
updateTime: 'date',
|
updateTime: 'date',
|
||||||
startTime: 'int',
|
startTime: 'int',
|
||||||
endTime: 'int',
|
endTime: 'int',
|
||||||
messageName: 'string?'
|
messageName: 'string?',
|
||||||
|
taskId: 'string?', // 任务ID,可能是视频生成任务的ID
|
||||||
|
taskMessage: 'string?', // 任务消息,可能是视频生成任务的消息
|
||||||
},
|
},
|
||||||
primaryKey: 'id'
|
primaryKey: 'id'
|
||||||
}
|
}
|
||||||
|
|||||||
@ -260,6 +260,32 @@ export class BookBackTaskListService extends BaseRealmService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新后台任务的数据
|
||||||
|
* @param taskId 任务ID
|
||||||
|
* @param backTaskParam 需要更新的任务数据部分字段
|
||||||
|
*/
|
||||||
|
UpdateBackTaskData(taskId: string, backTaskParam: Partial<TaskModal.Task>): void {
|
||||||
|
this.transaction(() => {
|
||||||
|
// 根据ID获取后台任务
|
||||||
|
let backTask = this.realm.objectForPrimaryKey('BookBackTaskList', taskId)
|
||||||
|
// 检查任务是否存在
|
||||||
|
if (backTask == null) {
|
||||||
|
throw new Error('更新后台任务数据失败,未找到对应的任务')
|
||||||
|
}
|
||||||
|
// 遍历需要更新的字段
|
||||||
|
for (const key in backTaskParam) {
|
||||||
|
// 跳过ID字段,防止主键被修改
|
||||||
|
if (key == "id") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// 更新对应字段的值
|
||||||
|
backTask[key] = backTaskParam[key]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 删除满足条件的数据,包含 id、bookId、bookTaskId
|
* 删除满足条件的数据,包含 id、bookId、bookTaskId
|
||||||
* 上面的条件,至少要有一个
|
* 上面的条件,至少要有一个
|
||||||
|
|||||||
@ -281,6 +281,15 @@ const migration = (oldRealm: Realm, newRealm: Realm) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (oldRealm.schemaVersion < 44) {
|
||||||
|
const oldBookTask = oldRealm.objects('BookBackTaskList')
|
||||||
|
const newBookTask = newRealm.objects('BookBackTaskList')
|
||||||
|
for (let i = 0; i < oldBookTask.length; i++) {
|
||||||
|
newBookTask[i].taskId = undefined;
|
||||||
|
newBookTask[i].taskMessage = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class BaseRealmService extends BaseService {
|
export class BaseRealmService extends BaseService {
|
||||||
@ -323,7 +332,7 @@ export class BaseRealmService extends BaseService {
|
|||||||
VideoMessage
|
VideoMessage
|
||||||
],
|
],
|
||||||
path: this.dbpath,
|
path: this.dbpath,
|
||||||
schemaVersion: 43,
|
schemaVersion: 44,
|
||||||
migration: migration
|
migration: migration
|
||||||
}
|
}
|
||||||
this.realm = await Realm.open(config)
|
this.realm = await Realm.open(config)
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import { FfmpegOptions } from '../../../../main/Service/ffmpegOptions.js'
|
|||||||
import { version } from '../../../../../package.json'
|
import { version } from '../../../../../package.json'
|
||||||
import { Book } from '../../../../model/book/book.js'
|
import { Book } from '../../../../model/book/book.js'
|
||||||
import { GeneralResponse } from '../../../../model/generalResponse.js'
|
import { GeneralResponse } from '../../../../model/generalResponse.js'
|
||||||
|
import { ImageToVideoModels } from '@/define/enum/video.js'
|
||||||
|
|
||||||
export class BookService extends BaseRealmService {
|
export class BookService extends BaseRealmService {
|
||||||
static instance: BookService | null = null
|
static instance: BookService | null = null
|
||||||
@ -210,6 +211,8 @@ export class BookService extends BaseRealmService {
|
|||||||
throw new Error('未知的小说类型')
|
throw new Error('未知的小说类型')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let videoCategory = ImageToVideoModels.MJ_VIDEO;
|
||||||
|
|
||||||
this.realm.write(() => {
|
this.realm.write(() => {
|
||||||
book.version = version
|
book.version = version
|
||||||
this.realm.create('Book', book)
|
this.realm.create('Book', book)
|
||||||
@ -233,6 +236,7 @@ export class BookService extends BaseRealmService {
|
|||||||
createTime: new Date(),
|
createTime: new Date(),
|
||||||
version: version,
|
version: version,
|
||||||
imageCategory: imageCategory,
|
imageCategory: imageCategory,
|
||||||
|
videoCategory: videoCategory,
|
||||||
openVideoGenerate: false
|
openVideoGenerate: false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -11,6 +11,7 @@ const { v4: uuidv4 } = require('uuid')
|
|||||||
import { Book } from "../../../../model/book/book"
|
import { Book } from "../../../../model/book/book"
|
||||||
import { GeneralResponse } from '../../../../model/generalResponse.js'
|
import { GeneralResponse } from '../../../../model/generalResponse.js'
|
||||||
import { BookTaskDetail } from '@/model/book/bookTaskDetail'
|
import { BookTaskDetail } from '@/model/book/bookTaskDetail'
|
||||||
|
import { ValidateJson } from '@/define/Tools/validate'
|
||||||
|
|
||||||
let dbPath = path.resolve(define.db_path, 'book.realm')
|
let dbPath = path.resolve(define.db_path, 'book.realm')
|
||||||
|
|
||||||
@ -73,8 +74,21 @@ export class BookTaskDetailService extends BaseRealmService {
|
|||||||
subImagePath: (item.subImagePath as string[])?.map((subImage) => {
|
subImagePath: (item.subImagePath as string[])?.map((subImage) => {
|
||||||
return JoinPath(define.project_path, subImage)
|
return JoinPath(define.project_path, subImage)
|
||||||
}),
|
}),
|
||||||
subVideoPath: (item.subVideoPath as string[])?.map((subVideo) => {
|
subVideoPath: (item.subVideoPath as string[]).map((subVideo) => subVideo.toString()),
|
||||||
return JoinPath(define.project_path, subVideo)
|
subVideoPathObject: (item.subVideoPath as string[])?.map((subVideo) => {
|
||||||
|
if (isEmpty(subVideo)) {
|
||||||
|
return {};
|
||||||
|
} else {
|
||||||
|
if (!ValidateJson(subVideo)) {
|
||||||
|
return {};
|
||||||
|
} else {
|
||||||
|
let obj = JSON.parse(subVideo);
|
||||||
|
if (!isEmpty(obj.localPath)) {
|
||||||
|
obj.localPath = JoinPath(define.project_path, obj.localPath) + '?t=' + new Date().getTime();
|
||||||
|
}
|
||||||
|
return obj
|
||||||
|
}
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
characterTags: item.characterTags ? item.characterTags.map((tag) => tag) : null,
|
characterTags: item.characterTags ? item.characterTags.map((tag) => tag) : null,
|
||||||
sceneTags: item.sceneTags ? item.sceneTags.map((tag) => tag) : null,
|
sceneTags: item.sceneTags ? item.sceneTags.map((tag) => tag) : null,
|
||||||
@ -257,6 +271,7 @@ export class BookTaskDetailService extends BaseRealmService {
|
|||||||
*/
|
*/
|
||||||
UpdateBookTaskDetailVideoMessage(bookTaskDetailId: string, videoMessage: BookTaskDetail.VideoMessage): void {
|
UpdateBookTaskDetailVideoMessage(bookTaskDetailId: string, videoMessage: BookTaskDetail.VideoMessage): void {
|
||||||
this.transaction(() => {
|
this.transaction(() => {
|
||||||
|
console.log("开始更新小说分镜的视频消息", bookTaskDetailId, videoMessage)
|
||||||
let bookTaskDetail = this.realm.objectForPrimaryKey('BookTaskDetail', bookTaskDetailId)
|
let bookTaskDetail = this.realm.objectForPrimaryKey('BookTaskDetail', bookTaskDetailId)
|
||||||
let videoMessageRes = this.realm.objectForPrimaryKey('VideoMessage', bookTaskDetailId)
|
let videoMessageRes = this.realm.objectForPrimaryKey('VideoMessage', bookTaskDetailId)
|
||||||
if (bookTaskDetail.videoMessage == null) {
|
if (bookTaskDetail.videoMessage == null) {
|
||||||
|
|||||||
@ -193,14 +193,20 @@ const BOOK = {
|
|||||||
/** 修改小说分镜的VideoMessage */
|
/** 修改小说分镜的VideoMessage */
|
||||||
UPDATE_BOOK_TASK_DETAIL_VIDEO_MESSAGE: "UPDATE_BOOK_TASK_DETAIL_VIDEO_MESSAGE",
|
UPDATE_BOOK_TASK_DETAIL_VIDEO_MESSAGE: "UPDATE_BOOK_TASK_DETAIL_VIDEO_MESSAGE",
|
||||||
|
|
||||||
|
/** 重新下载视频任务 */
|
||||||
|
RELOAD_VIDEO_TASK_INFO: "RELOAD_VIDEO_TASK_INFO",
|
||||||
|
|
||||||
/** Runway图转视频返回前端数据任务 */
|
/** Runway图转视频返回前端数据任务 */
|
||||||
RUNWAY_IMAGE_TO_VIDEO_RETURN: "RUNWAY_IMAGE_TO_VIDEO_RETURN",
|
RUNWAY_IMAGE_TO_VIDEO_RETURN: "RUNWAY_IMAGE_TO_VIDEO_RETURN",
|
||||||
|
|
||||||
|
/** MJ VIDEO 图转视频返回前端数据任务 */
|
||||||
|
MJ_VIDEO_TO_VIDEO_RETURN: "MJ_VIDEO_TO_VIDEO_RETURN",
|
||||||
|
|
||||||
/** 获取指定的条件的图转视频的数据,包含字批次 */
|
/** 获取指定的条件的图转视频的数据,包含字批次 */
|
||||||
GET_VIDEO_BOOK_INFO_LIST: "GET_VIDEO_BOOK_INFO_LIST",
|
GET_VIDEO_BOOK_INFO_LIST: "GET_VIDEO_BOOK_INFO_LIST",
|
||||||
|
|
||||||
/** 获取小说图片和视频生成进度 */
|
/** 获取小说图片和视频生成进度 */
|
||||||
GET_BOOK_IMAGE_AND_VIDEO_PROGRESS : "GET_BOOK_IMAGE_AND_VIDEO_PROGRESS"
|
GET_BOOK_IMAGE_AND_VIDEO_PROGRESS: "GET_BOOK_IMAGE_AND_VIDEO_PROGRESS"
|
||||||
|
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
|
|||||||
@ -17,4 +17,7 @@ export const SYSTEM = {
|
|||||||
|
|
||||||
/** 选择多个指定文件后缀的文件 */
|
/** 选择多个指定文件后缀的文件 */
|
||||||
SELECT_MULTIPLE_FILE: "SELECT_MULTIPLE_FILE",
|
SELECT_MULTIPLE_FILE: "SELECT_MULTIPLE_FILE",
|
||||||
|
|
||||||
|
/** 选择文件夹或指定后缀的文件 */
|
||||||
|
SELECT_FOLDER_OR_FILE: "SELECT_FOLDER_OR_FILE",
|
||||||
}
|
}
|
||||||
@ -100,6 +100,10 @@ export enum BookBackTaskType {
|
|||||||
LUMA_VIDEO = 'luma_video',
|
LUMA_VIDEO = 'luma_video',
|
||||||
// kling 生成视频
|
// kling 生成视频
|
||||||
KLING_VIDEO = 'kling_video',
|
KLING_VIDEO = 'kling_video',
|
||||||
|
// MJ Video
|
||||||
|
MJ_VIDEO = 'mj_video',
|
||||||
|
// MJ VIDEO EXTEND 视频拓展
|
||||||
|
MJ_VIDEO_EXTEND = 'mj_video_extend'
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -5,9 +5,10 @@ export enum OptionType {
|
|||||||
STRING = 'string',
|
STRING = 'string',
|
||||||
NUMBER = 'number',
|
NUMBER = 'number',
|
||||||
BOOLEAN = 'boolean',
|
BOOLEAN = 'boolean',
|
||||||
JOSN = 'json'
|
JSON = 'json'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export enum OptionKeyName {
|
export enum OptionKeyName {
|
||||||
|
|
||||||
//#region 文案处理
|
//#region 文案处理
|
||||||
@ -79,7 +80,17 @@ export enum OptionKeyName {
|
|||||||
/**
|
/**
|
||||||
* ComfyUI 工作流设置
|
* ComfyUI 工作流设置
|
||||||
*/
|
*/
|
||||||
ComfyUI_WorkFlowSetting = "ComfyUI_WorkFlowSetting"
|
ComfyUI_WorkFlowSetting = "ComfyUI_WorkFlowSetting",
|
||||||
|
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
//#region Image To Video
|
||||||
|
|
||||||
|
/** 是否显示右侧的Image To Video 操作面板 */
|
||||||
|
ImageToVideo_ShowRightPanel = 'ImageToVideo_ShowRightPanel',
|
||||||
|
|
||||||
|
/** 是否显示分页 */
|
||||||
|
ImageToVideo_ShowPagination = 'ImageToVideo_ShowPagination',
|
||||||
|
|
||||||
//#endregion
|
//#endregion
|
||||||
}
|
}
|
||||||
@ -69,6 +69,8 @@ export enum ResponseMessageType {
|
|||||||
RUNWAY_VIDEO = "RUNWAY_VIDEO",// Runway生成视频
|
RUNWAY_VIDEO = "RUNWAY_VIDEO",// Runway生成视频
|
||||||
LUMA_VIDEO = "LUMA_VIDEO",// Luma生成视频
|
LUMA_VIDEO = "LUMA_VIDEO",// Luma生成视频
|
||||||
KLING_VIDEO = "KLING_VIDEO",// Kling生成视频
|
KLING_VIDEO = "KLING_VIDEO",// Kling生成视频
|
||||||
|
MJ_VIDEO = "MJ_VIDEO",// MJ生成视频
|
||||||
|
MJ_VIDEO_EXTEND = "MJ_VIDEO_EXTEND",// MJ生成视频拓展
|
||||||
VIDEO_SUCESS = "VIDEO_SUCESS" //视频生成成功
|
VIDEO_SUCESS = "VIDEO_SUCESS" //视频生成成功
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
|
|
||||||
//#region 图转视频类型
|
//#region 图转视频类型
|
||||||
|
|
||||||
|
import { BookBackTaskType } from "./bookEnum";
|
||||||
|
|
||||||
/** 图片转视频的方式 */
|
/** 图片转视频的方式 */
|
||||||
export enum ImageToVideoModels {
|
export enum ImageToVideoModels {
|
||||||
/** runway 生成视频 */
|
/** runway 生成视频 */
|
||||||
@ -12,7 +14,27 @@ export enum ImageToVideoModels {
|
|||||||
/** Pika 生成视频 */
|
/** Pika 生成视频 */
|
||||||
PIKA = "PIKA",
|
PIKA = "PIKA",
|
||||||
/** MJ 图转视频 */
|
/** MJ 图转视频 */
|
||||||
MJ_VIDEO = "MJ_VIDEO"
|
MJ_VIDEO = "MJ_VIDEO",
|
||||||
|
/** MJ 视频拓展 */
|
||||||
|
MJ_VIDEO_EXTEND = "MJ_VIDEO_EXTEND"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const MappingTaskTypeToVideoModel = (type: BookBackTaskType | string) => {
|
||||||
|
switch (type) {
|
||||||
|
case BookBackTaskType.LUMA_VIDEO:
|
||||||
|
return ImageToVideoModels.LUMA;
|
||||||
|
case BookBackTaskType.RUNWAY_VIDEO:
|
||||||
|
return ImageToVideoModels.RUNWAY;
|
||||||
|
case BookBackTaskType.KLING_VIDEO:
|
||||||
|
return ImageToVideoModels.KLING;
|
||||||
|
case BookBackTaskType.MJ_VIDEO:
|
||||||
|
return ImageToVideoModels.MJ_VIDEO;
|
||||||
|
case BookBackTaskType.MJ_VIDEO_EXTEND:
|
||||||
|
return ImageToVideoModels.MJ_VIDEO_EXTEND;
|
||||||
|
default:
|
||||||
|
return "UNKNOWN"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -48,11 +70,11 @@ export const GetImageToVideoModelsLabel = (model: ImageToVideoModels | string) =
|
|||||||
*/
|
*/
|
||||||
export const GetImageToVideoModelsOptions = () => {
|
export const GetImageToVideoModelsOptions = () => {
|
||||||
return [
|
return [
|
||||||
|
{ label: GetImageToVideoModelsLabel(ImageToVideoModels.MJ_VIDEO), value: ImageToVideoModels.MJ_VIDEO },
|
||||||
{ label: GetImageToVideoModelsLabel(ImageToVideoModels.RUNWAY), value: ImageToVideoModels.RUNWAY },
|
{ label: GetImageToVideoModelsLabel(ImageToVideoModels.RUNWAY), value: ImageToVideoModels.RUNWAY },
|
||||||
{ label: GetImageToVideoModelsLabel(ImageToVideoModels.LUMA), value: ImageToVideoModels.LUMA },
|
{ label: GetImageToVideoModelsLabel(ImageToVideoModels.LUMA), value: ImageToVideoModels.LUMA },
|
||||||
{ label: GetImageToVideoModelsLabel(ImageToVideoModels.KLING), value: ImageToVideoModels.KLING },
|
{ label: GetImageToVideoModelsLabel(ImageToVideoModels.KLING), value: ImageToVideoModels.KLING },
|
||||||
{ label: GetImageToVideoModelsLabel(ImageToVideoModels.PIKA), value: ImageToVideoModels.PIKA },
|
{ label: GetImageToVideoModelsLabel(ImageToVideoModels.PIKA), value: ImageToVideoModels.PIKA },
|
||||||
{ label: GetImageToVideoModelsLabel(ImageToVideoModels.MJ_VIDEO), value: ImageToVideoModels.MJ_VIDEO }
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -127,3 +149,61 @@ export enum KlingMode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
|
//#region MJ Video
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 对视频任务进行操作。不为空时,index、taskId必填
|
||||||
|
*/
|
||||||
|
export enum MJVideoAction {
|
||||||
|
Extend = "extend",
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 首帧图片,扩展时可为空
|
||||||
|
*/
|
||||||
|
export enum MJVideoImageType {
|
||||||
|
Base64 = "base64",
|
||||||
|
Url = "url",
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MJ Video的动作幅度
|
||||||
|
*/
|
||||||
|
export enum MJVideoMotion {
|
||||||
|
High = "high",
|
||||||
|
Low = "low",
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 获取MJ视频动作幅度的标签
|
||||||
|
*
|
||||||
|
* @param model MJ视频动作幅度枚举值或字符串
|
||||||
|
* @returns 返回对应的中英文标签
|
||||||
|
*/
|
||||||
|
export function GetMJVideoMotionLabel(model: MJVideoMotion | string) {
|
||||||
|
switch (model) {
|
||||||
|
case MJVideoMotion.High:
|
||||||
|
return "高 (High)";
|
||||||
|
case MJVideoMotion.Low:
|
||||||
|
return "低 (Low)";
|
||||||
|
default:
|
||||||
|
return "无效"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取MJ视频动作幅度的选项列表
|
||||||
|
*
|
||||||
|
* @returns 返回包含标签和值的选项数组,用于下拉选择框等UI组件
|
||||||
|
*/
|
||||||
|
export function GetMJVideoMotionOptions() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: GetMJVideoMotionLabel(MJVideoMotion.Low), value: MJVideoMotion.Low
|
||||||
|
}, {
|
||||||
|
label: GetMJVideoMotionLabel(MJVideoMotion.High), value: MJVideoMotion.High
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
//#endregion
|
||||||
|
|||||||
@ -371,6 +371,9 @@ export function BookIpc() {
|
|||||||
/** 修改小说详细分镜的Videomessage */
|
/** 修改小说详细分镜的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.UPDATE_BOOK_TASK_DETAIL_VIDEO_MESSAGE, async (event, bookTaskDetailId, videoMessage) => await videoGlobal.UpdateBookTaskDetailVideoMessage(bookTaskDetailId, videoMessage))
|
||||||
|
|
||||||
|
/** 重新下载视频任务 */
|
||||||
|
ipcMain.handle(DEFINE_STRING.BOOK.RELOAD_VIDEO_TASK_INFO, async (_, bookTaskDetailId) => await bookImageTextToVideoIndex.ReloadVideoTaskInfo(bookTaskDetailId))
|
||||||
|
|
||||||
/** 获取指定的条件的图转视频的数据,包含子批次 */
|
/** 获取指定的条件的图转视频的数据,包含子批次 */
|
||||||
ipcMain.handle(DEFINE_STRING.BOOK.GET_VIDEO_BOOK_INFO_LIST, async (event,
|
ipcMain.handle(DEFINE_STRING.BOOK.GET_VIDEO_BOOK_INFO_LIST, async (event,
|
||||||
condition: BookVideo.BookVideoInfoListQuertCondition) => await bookImageTextToVideoIndex.GetVideoBookInfoList(condition))
|
condition: BookVideo.BookVideoInfoListQuertCondition) => await bookImageTextToVideoIndex.GetVideoBookInfoList(condition))
|
||||||
|
|||||||
@ -34,5 +34,8 @@ function SystemIpc() {
|
|||||||
|
|
||||||
/** 选择多个指定文件后缀的文件 */
|
/** 选择多个指定文件后缀的文件 */
|
||||||
ipcMain.handle(DEFINE_STRING.SYSTEM.SELECT_MULTIPLE_FILE, async (event, value: string[]) => await electronInterface.SelectMultipleFile(value))
|
ipcMain.handle(DEFINE_STRING.SYSTEM.SELECT_MULTIPLE_FILE, async (event, value: string[]) => await electronInterface.SelectMultipleFile(value))
|
||||||
|
|
||||||
|
/** 选择文件夹或指定后缀的文件 */
|
||||||
|
ipcMain.handle(DEFINE_STRING.SYSTEM.SELECT_FOLDER_OR_FILE, async (event, value?: string[]) => await electronInterface.SelectFolderOrFile(value))
|
||||||
}
|
}
|
||||||
export { SystemIpc }
|
export { SystemIpc }
|
||||||
|
|||||||
@ -0,0 +1,64 @@
|
|||||||
|
import { errorMessage, successMessage } from "@/main/Public/generalTools";
|
||||||
|
import { BookBasicHandle } from "../bookBasicHandle";
|
||||||
|
import { ImageToVideoModels } from "@/define/enum/video";
|
||||||
|
import { MJVideoService } from "../../video/mjVideo";
|
||||||
|
import { isEmpty } from "lodash";
|
||||||
|
import { GeneralResponse } from "@/model/generalResponse";
|
||||||
|
|
||||||
|
|
||||||
|
export class BookImageTextToVideoCategory extends BookBasicHandle {
|
||||||
|
mjVideoService: MJVideoService
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.mjVideoService = new MJVideoService();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重新加载视频任务信息
|
||||||
|
*
|
||||||
|
* 根据小说分镜的ID重新获取视频任务的信息。该方法会检查小说分镜数据是否存在,
|
||||||
|
* 视频消息数据是否存在,视频任务ID是否存在,然后根据视频类型调用相应的服务
|
||||||
|
* 重新加载视频任务。
|
||||||
|
*
|
||||||
|
* @param bookTaskDetailId - 小说分镜的ID
|
||||||
|
* @returns 成功时返回任务信息,失败时返回错误信息
|
||||||
|
* @throws 如果重新加载过程中发生错误
|
||||||
|
*/
|
||||||
|
async ReloadVideoTaskInfo(bookTaskDetailId: string) {
|
||||||
|
try {
|
||||||
|
await this.InitBookBasicHandle()
|
||||||
|
let bookTaskDetail = this.bookTaskDetailService.GetBookTaskDetailDataById(bookTaskDetailId);
|
||||||
|
if (bookTaskDetail == null) {
|
||||||
|
return errorMessage('没有找到对应的小说分镜数据,请先添加小说分镜', 'BookImageTextToVideoCategory_ReloadVideoTaskInfo');
|
||||||
|
}
|
||||||
|
|
||||||
|
let videoMessage = bookTaskDetail.videoMessage;
|
||||||
|
if (videoMessage == null) {
|
||||||
|
return errorMessage('没有找到对应的小说分镜的视频消息数据,请先添加视频消息', 'BookImageTextToVideoCategory_ReloadVideoTaskInfo');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isEmpty(videoMessage.taskId)) {
|
||||||
|
return errorMessage('没有找到对应的小说分镜的视频任务ID,请先添加视频任务', 'BookImageTextToVideoCategory_ReloadVideoTaskInfo');
|
||||||
|
}
|
||||||
|
|
||||||
|
let res: GeneralResponse.ErrorItem | GeneralResponse.SuccessItem;
|
||||||
|
switch (videoMessage.videoType) {
|
||||||
|
case ImageToVideoModels.MJ_VIDEO:
|
||||||
|
res = await this.mjVideoService.ReloadMJVideoTask(bookTaskDetail, videoMessage.taskId);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return errorMessage('不支持的视频类型,请检查视频类型', 'BookImageTextToVideoCategory_ReloadVideoTaskInfo');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查返回结果
|
||||||
|
if (res.code != 1) {
|
||||||
|
return errorMessage(res.message, 'BookImageTextToVideoCategory_ReloadVideoTaskInfo');
|
||||||
|
}
|
||||||
|
return successMessage(res.data, res.message, 'BookImageTextToVideoCategory_ReloadVideoTaskInfo');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
return errorMessage('重新下载视频任务失败,错误信息:' + error.message, 'BookImageTextToVideoCategory_ReloadVideoTaskInfo');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -1,12 +1,15 @@
|
|||||||
import { BookImageTextToVideoInfo } from "./bookImageTextToVideoInfo";
|
import { BookImageTextToVideoInfo } from "./bookImageTextToVideoInfo";
|
||||||
|
import { BookImageTextToVideoCategory } from "./bookImageTextToVideoCategory";
|
||||||
|
|
||||||
|
|
||||||
export class BookImageTextToVideoIndex {
|
export class BookImageTextToVideoIndex {
|
||||||
|
|
||||||
bookImageTextToVideoInfo: BookImageTextToVideoInfo;
|
bookImageTextToVideoInfo: BookImageTextToVideoInfo;
|
||||||
|
bookImageTextToVideoCategory: BookImageTextToVideoCategory
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.bookImageTextToVideoInfo = new BookImageTextToVideoInfo();
|
this.bookImageTextToVideoInfo = new BookImageTextToVideoInfo();
|
||||||
|
this.bookImageTextToVideoCategory = new BookImageTextToVideoCategory();
|
||||||
}
|
}
|
||||||
|
|
||||||
//#region Info
|
//#region Info
|
||||||
@ -15,8 +18,15 @@ export class BookImageTextToVideoIndex {
|
|||||||
GetVideoBookInfoList = async (condition: BookVideo.BookVideoInfoListQuertCondition) => await this.bookImageTextToVideoInfo.GetVideoBookInfoList(condition)
|
GetVideoBookInfoList = async (condition: BookVideo.BookVideoInfoListQuertCondition) => await this.bookImageTextToVideoInfo.GetVideoBookInfoList(condition)
|
||||||
|
|
||||||
|
|
||||||
|
/** 获取小说图片和视频生成的进度信息 根据提供的参数查询指定小说或小说任务的图片和视频生成进度 */
|
||||||
GetBookImageAndVideoProgress = async (bookId?: string, bookTaskId?: string) => await this.bookImageTextToVideoInfo.GetBookImageAndVideoProgress(bookId, bookTaskId);
|
GetBookImageAndVideoProgress = async (bookId?: string, bookTaskId?: string) => await this.bookImageTextToVideoInfo.GetBookImageAndVideoProgress(bookId, bookTaskId);
|
||||||
|
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
|
//#region Category
|
||||||
|
|
||||||
|
ReloadVideoTaskInfo = async (bookTaskDetailId: string) => await this.bookImageTextToVideoCategory.ReloadVideoTaskInfo(bookTaskDetailId);
|
||||||
|
|
||||||
|
//#endregion
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -234,7 +234,7 @@ export class BookImageTextToVideoInfo extends BookBasicHandle {
|
|||||||
imageProgress += 1;
|
imageProgress += 1;
|
||||||
}
|
}
|
||||||
// 检查视频信息
|
// 检查视频信息
|
||||||
if (!isEmpty(bookTaskDetail.videoPath) && await CheckFileOrDirExist(bookTaskDetail.videoPath)) {
|
if (!isEmpty(bookTaskDetail.generateVideoPath) && await CheckFileOrDirExist(bookTaskDetail.generateVideoPath)) {
|
||||||
videoProgress += 1;
|
videoProgress += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -135,7 +135,6 @@ export class ReverseBook {
|
|||||||
...item,
|
...item,
|
||||||
outImagePath: isEmpty(item.outImagePath) ? item.outImagePath : item.outImagePath + '?t=' + new Date().getTime(),
|
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,
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -2,13 +2,14 @@ import { BookService } from "@/define/db/service/Book/bookService"
|
|||||||
import { BookTaskDetailService } from "@/define/db/service/Book/bookTaskDetailService"
|
import { BookTaskDetailService } from "@/define/db/service/Book/bookTaskDetailService"
|
||||||
import { BookTaskService } from "@/define/db/service/Book/bookTaskService"
|
import { BookTaskService } from "@/define/db/service/Book/bookTaskService"
|
||||||
import { OptionRealmService } from "@/define/db/service/SoftWare/optionRealmService"
|
import { OptionRealmService } from "@/define/db/service/SoftWare/optionRealmService"
|
||||||
|
import { BookBackTaskListService } from "@/define/db/service/Book/bookBackTaskListService"
|
||||||
|
|
||||||
export class BookBasicHandle {
|
export class BookBasicHandle {
|
||||||
bookTaskDetailService!: BookTaskDetailService
|
bookTaskDetailService!: BookTaskDetailService
|
||||||
bookTaskService!: BookTaskService
|
bookTaskService!: BookTaskService
|
||||||
optionRealmService!: OptionRealmService
|
optionRealmService!: OptionRealmService
|
||||||
bookService!: BookService
|
bookService!: BookService
|
||||||
|
bookBackTaskListService!: BookBackTaskListService
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
// 初始化
|
// 初始化
|
||||||
@ -28,6 +29,9 @@ export class BookBasicHandle {
|
|||||||
if (!this.bookService) {
|
if (!this.bookService) {
|
||||||
this.bookService = await BookService.getInstance()
|
this.bookService = await BookService.getInstance()
|
||||||
}
|
}
|
||||||
|
if (!this.bookBackTaskListService) {
|
||||||
|
this.bookBackTaskListService = await BookBackTaskListService.getInstance()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async transaction(callback: (realm: any) => void) {
|
async transaction(callback: (realm: any) => void) {
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import { BookServiceBasic } from "../ServiceBasic/bookServiceBasic";
|
|||||||
import { ValidateJson } from "../../../define/Tools/validate";
|
import { ValidateJson } from "../../../define/Tools/validate";
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
import { TimeStringToMilliseconds } from "../../../define/Tools/time";
|
import { TimeStringToMilliseconds } from "../../../define/Tools/time";
|
||||||
|
import { ImageToVideoModels } from "@/define/enum/video";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 小说批次相关的操作
|
* 小说批次相关的操作
|
||||||
@ -123,6 +124,7 @@ export class BookTask {
|
|||||||
let name = 'output_' + no.toString().padStart(5, '0');
|
let name = 'output_' + no.toString().padStart(5, '0');
|
||||||
let imageFolder = path.join(define.project_path, `${bookTask.bookId}/tmp/${name}`);
|
let imageFolder = path.join(define.project_path, `${bookTask.bookId}/tmp/${name}`);
|
||||||
let imageCategory = global.config.defaultImageMode ?? BookImageCategory.MJ;
|
let imageCategory = global.config.defaultImageMode ?? BookImageCategory.MJ;
|
||||||
|
let videoCategory = ImageToVideoModels.MJ_VIDEO;
|
||||||
let book = await this.bookServiceBasic.GetBookDataById(bookTask.bookId)
|
let book = await this.bookServiceBasic.GetBookDataById(bookTask.bookId)
|
||||||
if (!isEmpty(bookTask.imageCategory)) {
|
if (!isEmpty(bookTask.imageCategory)) {
|
||||||
imageCategory = bookTask.imageCategory;
|
imageCategory = bookTask.imageCategory;
|
||||||
@ -136,6 +138,10 @@ export class BookTask {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(!isEmpty(bookTask.videoCategory)){
|
||||||
|
videoCategory = bookTask.videoCategory;
|
||||||
|
}
|
||||||
|
|
||||||
let newBookTask = {
|
let newBookTask = {
|
||||||
id: uuidv4(),
|
id: uuidv4(),
|
||||||
bookId: bookTask.bookId,
|
bookId: bookTask.bookId,
|
||||||
@ -157,6 +163,7 @@ export class BookTask {
|
|||||||
prefixPrompt: addNewBookTask.prefixPrompt ??= undefined,
|
prefixPrompt: addNewBookTask.prefixPrompt ??= undefined,
|
||||||
suffixPrompt: addNewBookTask.suffixPrompt ?? undefined,
|
suffixPrompt: addNewBookTask.suffixPrompt ?? undefined,
|
||||||
imageCategory: imageCategory,
|
imageCategory: imageCategory,
|
||||||
|
videoCategory : videoCategory,
|
||||||
subImageFolder: [],
|
subImageFolder: [],
|
||||||
draftSrtStyle: undefined,
|
draftSrtStyle: undefined,
|
||||||
backgroundMusic: bookTask.backgroundMusic ??= undefined,
|
backgroundMusic: bookTask.backgroundMusic ??= undefined,
|
||||||
|
|||||||
@ -18,6 +18,7 @@ import util from 'util';
|
|||||||
import { spawn, exec } from 'child_process';
|
import { spawn, exec } from 'child_process';
|
||||||
import { SendMessageToRenderer } from "../globalService";
|
import { SendMessageToRenderer } from "../globalService";
|
||||||
import { TaskModal } from "@/model/task";
|
import { TaskModal } from "@/model/task";
|
||||||
|
import compressing from "compressing";
|
||||||
const execAsync = util.promisify(exec);
|
const execAsync = util.promisify(exec);
|
||||||
|
|
||||||
export class BookVideo {
|
export class BookVideo {
|
||||||
@ -25,12 +26,68 @@ export class BookVideo {
|
|||||||
bookServiceBasic: BookServiceBasic
|
bookServiceBasic: BookServiceBasic
|
||||||
jianyingService: JianyingService
|
jianyingService: JianyingService
|
||||||
bookSetting: BookSetting
|
bookSetting: BookSetting
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.setting = new Setting(global)
|
this.setting = new Setting(global)
|
||||||
this.bookServiceBasic = new BookServiceBasic()
|
this.bookServiceBasic = new BookServiceBasic()
|
||||||
this.jianyingService = new JianyingService()
|
this.jianyingService = new JianyingService()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 安全执行外部脚本的方法
|
||||||
|
* @param scriptPath 脚本路径
|
||||||
|
* @param configPath 配置文件路径
|
||||||
|
* @returns Promise<{stdout: string, stderr: string}>
|
||||||
|
*/
|
||||||
|
private async executeScript(scriptPath: string, configPath: string): Promise<{ stdout: string, stderr: string }> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
// 设置环境变量
|
||||||
|
const env = {
|
||||||
|
...process.env,
|
||||||
|
PYTHONIOENCODING: 'utf-8',
|
||||||
|
PYTHONLEGACYWINDOWSSTDIO: 'utf-8',
|
||||||
|
LANG: 'zh_CN.UTF-8',
|
||||||
|
PYTHONUTF8: '1'
|
||||||
|
};
|
||||||
|
|
||||||
|
// 使用spawn方式执行,更好地控制进程
|
||||||
|
const child = spawn(scriptPath, [configPath.replaceAll("\\", '/')], {
|
||||||
|
env: env,
|
||||||
|
cwd: path.dirname(scriptPath),
|
||||||
|
stdio: ['pipe', 'pipe', 'pipe']
|
||||||
|
});
|
||||||
|
|
||||||
|
let stdout = '';
|
||||||
|
let stderr = '';
|
||||||
|
|
||||||
|
child.stdout?.on('data', (data) => {
|
||||||
|
stdout += data.toString('utf8');
|
||||||
|
});
|
||||||
|
|
||||||
|
child.stderr?.on('data', (data) => {
|
||||||
|
stderr += data.toString('utf8');
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on('close', (code) => {
|
||||||
|
if (code === 0) {
|
||||||
|
resolve({ stdout, stderr });
|
||||||
|
} else {
|
||||||
|
reject(new Error(`脚本执行失败,退出代码: ${code}, 错误信息: ${stderr}`));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on('error', (error) => {
|
||||||
|
reject(new Error(`无法启动脚本: ${error.message}`));
|
||||||
|
});
|
||||||
|
|
||||||
|
// 设置超时
|
||||||
|
setTimeout(() => {
|
||||||
|
child.kill();
|
||||||
|
reject(new Error('脚本执行超时'));
|
||||||
|
}, 300000); // 5分钟超时
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
//#region 引用主小说相关数据
|
//#region 引用主小说相关数据
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -100,7 +157,10 @@ export class BookVideo {
|
|||||||
* @param book 小说数据
|
* @param book 小说数据
|
||||||
* @param bookTask 对应的小说任务数据
|
* @param bookTask 对应的小说任务数据
|
||||||
*/
|
*/
|
||||||
private async GenerateConfigFile(book: Book.SelectBook, bookTask: Book.SelectBookTask): Promise<void> {
|
private async GenerateConfigFile(book: Book.SelectBook, bookTask: Book.SelectBookTask): Promise<{
|
||||||
|
draftName: string;
|
||||||
|
configJsonPath: string;
|
||||||
|
}> {
|
||||||
try {
|
try {
|
||||||
// 先修改通用设置
|
// 先修改通用设置
|
||||||
let saveProjectRes = await this.setting.ModifySampleSetting(JSON.stringify({
|
let saveProjectRes = await this.setting.ModifySampleSetting(JSON.stringify({
|
||||||
@ -111,7 +171,6 @@ export class BookVideo {
|
|||||||
throw new Error("修改通用设置失败")
|
throw new Error("修改通用设置失败")
|
||||||
}
|
}
|
||||||
// 开始生成配置文件
|
// 开始生成配置文件
|
||||||
|
|
||||||
let configPath = path.join(book.bookFolderPath, `scripts/${bookTask.name}_config.json`);
|
let configPath = path.join(book.bookFolderPath, `scripts/${bookTask.name}_config.json`);
|
||||||
await CheckFolderExistsOrCreate(path.dirname(configPath));
|
await CheckFolderExistsOrCreate(path.dirname(configPath));
|
||||||
|
|
||||||
@ -120,17 +179,71 @@ export class BookVideo {
|
|||||||
bookTaskId: bookTask.id
|
bookTaskId: bookTask.id
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let musicPath: string | undefined = undefined;
|
||||||
|
// 处理背景音乐
|
||||||
|
if (!isEmpty(bookTask.backgroundMusic)) {
|
||||||
|
|
||||||
|
// 判断文件或者是文件夹是不是存在
|
||||||
|
if (!CheckFileOrDirExist(bookTask.backgroundMusic)) {
|
||||||
|
throw new Error("背景音乐文件夹或文件不存在,请检查");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断背景音乐是文件夹还是文件 文件的话 就直接赋值 文件夹的话 随机一个文件
|
||||||
|
let isFolder = await fs.promises.stat(bookTask.backgroundMusic).then(stat => stat.isDirectory()).catch(() => false);
|
||||||
|
if (!isFolder) {
|
||||||
|
musicPath = bookTask.backgroundMusic;
|
||||||
|
} else {
|
||||||
|
let files = await GetFilesWithExtensions(bookTask.backgroundMusic, [".mp3", ".wav"]);
|
||||||
|
if (files.length <= 0) {
|
||||||
|
throw new Error("背景音乐文件夹下面未存在有效的音频文件");
|
||||||
|
} else {
|
||||||
|
const randomIndex = Math.floor(Math.random() * files.length);
|
||||||
|
musicPath = files[randomIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理草稿文件
|
||||||
|
let draft_name = `${book.name}_${bookTask.name}`;
|
||||||
|
let draft_path = path.join(global.config.draft_path, draft_name);
|
||||||
|
await fs.promises.rm(draft_path, { recursive: true, force: true });
|
||||||
|
await compressing.zip.uncompress(define.draft_temp_path, path.join(global.config.draft_path, draft_name));
|
||||||
|
let draftPath = path.join(draft_path, "draft_content.json");
|
||||||
|
|
||||||
|
// 处理关键帧数据
|
||||||
|
let key_frame_setting_str = await fs.promises.readFile(define.clip_setting, 'utf-8');
|
||||||
|
if (!ValidateJson(key_frame_setting_str)) {
|
||||||
|
throw new Error("关键帧配置文件格式错误,请检查");
|
||||||
|
}
|
||||||
|
let key_frame_setting = JSON.parse(key_frame_setting_str)
|
||||||
|
let key_frame = key_frame_setting.key_frame;
|
||||||
|
|
||||||
|
// 判断关键帧配置是不是存在。不存在直接结束
|
||||||
|
if (key_frame == null) {
|
||||||
|
throw new Error("没有找到关键帧配置,请检查");
|
||||||
|
}
|
||||||
|
let newKeyFrame = {
|
||||||
|
...key_frame,
|
||||||
|
is_fixed_speed: key_frame.isFixedSpeed ? key_frame.isFixedSpeed : false,
|
||||||
|
}
|
||||||
|
|
||||||
let configData = {
|
let configData = {
|
||||||
srt_time_information: [],
|
srt_time_information: [],
|
||||||
video_config: {
|
video_config: {
|
||||||
srt_path: bookTask.srtPath,
|
srt_path: bookTask.srtPath,
|
||||||
audio_path: bookTask.audioPath,
|
audio_path: bookTask.audioPath,
|
||||||
draft_srt_style: bookTask.draftSrtStyle ? bookTask.draftSrtStyle : "0",
|
draft_srt_style: bookTask.draftSrtStyle ? bookTask.draftSrtStyle : "0",
|
||||||
background_music: bookTask.backgroundMusic,
|
background_music: musicPath,
|
||||||
friendly_reminder: bookTask.friendlyReminder ? bookTask.friendlyReminder : "0",
|
friendly_reminder: bookTask.friendlyReminder ? bookTask.friendlyReminder : "0",
|
||||||
|
draft_content_json_path: draftPath,
|
||||||
|
key_frame_info: newKeyFrame,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 用于判断是不是开启视频合成,是不是给视频路径
|
||||||
|
let openVideo = bookTask.openVideoGenerate ?? false;
|
||||||
|
|
||||||
for (let i = 0; i < bookTaskDetail.length; i++) {
|
for (let i = 0; i < bookTaskDetail.length; i++) {
|
||||||
const element = bookTaskDetail[i];
|
const element = bookTaskDetail[i];
|
||||||
let frameData = {
|
let frameData = {
|
||||||
@ -150,6 +263,7 @@ export class BookVideo {
|
|||||||
prompt_json: '',
|
prompt_json: '',
|
||||||
name: element.name + '.png',
|
name: element.name + '.png',
|
||||||
outImagePath: element.outImagePath,
|
outImagePath: element.outImagePath,
|
||||||
|
generateVideoPath: openVideo ? element.generateVideoPath : null,
|
||||||
subImagePath: element.subImagePath,
|
subImagePath: element.subImagePath,
|
||||||
scene_tags: [],
|
scene_tags: [],
|
||||||
imageLock: element.imageLock,
|
imageLock: element.imageLock,
|
||||||
@ -158,9 +272,14 @@ export class BookVideo {
|
|||||||
configData.srt_time_information.push(frameData)
|
configData.srt_time_information.push(frameData)
|
||||||
}
|
}
|
||||||
// 完毕,将数据写出
|
// 完毕,将数据写出
|
||||||
await fs.promises.writeFile(configPath, JSON.stringify(configData));
|
await fs.promises.writeFile(configPath, JSON.stringify(configData), 'utf-8');
|
||||||
|
let configJsonPath = path.join(book.bookFolderPath, 'scripts/config.json');
|
||||||
// 复制一个到config.json中
|
// 复制一个到config.json中
|
||||||
await CopyFileOrFolder(configPath, path.join(book.bookFolderPath, 'scripts/config.json'));
|
await CopyFileOrFolder(configPath, configJsonPath);
|
||||||
|
return {
|
||||||
|
draftName: draft_name,
|
||||||
|
configJsonPath: configJsonPath
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
@ -227,40 +346,84 @@ export class BookVideo {
|
|||||||
element.imageFolder, draft_name);
|
element.imageFolder, draft_name);
|
||||||
result.push(draft_name);
|
result.push(draft_name);
|
||||||
} else {
|
} else {
|
||||||
await this.GenerateConfigFile(book, element);
|
let { draftName, configJsonPath } = await this.GenerateConfigFile(book, element);
|
||||||
// 数据处理完毕,开始输出
|
|
||||||
let clipDraft = new ClipDraft(global, [element.name, {
|
// 开始调用 exe 执行 草稿的导出
|
||||||
srt_path: operateBookType == OperateBookType.ASSIGNBOOKTASK ? book.srtPath : element.srtPath,
|
let jianyingExePath = path.join(define.scripts_path, "xiangbei_jianying_main.exe");
|
||||||
audio_path: operateBookType == OperateBookType.ASSIGNBOOKTASK ? book.audioPath : element.audioPath,
|
if (!CheckFileOrDirExist(jianyingExePath)) {
|
||||||
draft_srt_style: operateBookType == OperateBookType.ASSIGNBOOKTASK ? (book.draftSrtStyle ? book.draftSrtStyle : '0') : (element.draftSrtStyle ? element.draftSrtStyle : "0"),
|
throw new Error("没有找到导出剪映的执行文件,请检查");
|
||||||
background_music: operateBookType == OperateBookType.ASSIGNBOOKTASK ? book.backgroundMusic : element.backgroundMusic,
|
|
||||||
friendly_reminder: operateBookType == OperateBookType.ASSIGNBOOKTASK ? (book.friendlyReminder ? book.bookFolderPath : '0') : (element.friendlyReminder ? element.friendlyReminder : "0"),
|
|
||||||
}])
|
|
||||||
let res = await clipDraft.addDraft();
|
|
||||||
if (res.code == 0) {
|
|
||||||
throw new Error(res.message)
|
|
||||||
}
|
|
||||||
result.push(res.draft_name);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let bookTaskDetails = await this.bookServiceBasic.GetBookTaskDetailData({
|
// 开始执行exe
|
||||||
bookTaskId: element.id
|
try {
|
||||||
|
// 首先尝试使用spawn方法执行
|
||||||
|
const output = await this.executeScript(jianyingExePath, configJsonPath);
|
||||||
|
|
||||||
|
// 检查stderr是否真的是错误
|
||||||
|
if (output.stderr && (output.stderr.includes('Error') || output.stderr.includes('failed') || output.stderr.includes('UnicodeEncodeError'))) {
|
||||||
|
throw new Error(output.stderr);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出成功
|
||||||
|
let stdout = output.stdout;
|
||||||
|
// 将导出的日志写道文件里面
|
||||||
|
let exportLogPath = path.join(book.bookFolderPath, `scripts/JianYingExportLog/${draftName}_export_log_${new Date().getTime()}.txt`);
|
||||||
|
await CheckFolderExistsOrCreate(path.dirname(exportLogPath));
|
||||||
|
await fs.promises.writeFile(exportLogPath, stdout, 'utf-8');
|
||||||
|
|
||||||
|
// 导出成功 将草稿名字返回
|
||||||
|
result.push(draftName);
|
||||||
|
} catch (execError) {
|
||||||
|
// 如果spawn方法失败,尝试使用原来的execAsync方法作为备用
|
||||||
|
try {
|
||||||
|
const env = {
|
||||||
|
...process.env,
|
||||||
|
PYTHONIOENCODING: 'utf-8',
|
||||||
|
PYTHONLEGACYWINDOWSSTDIO: 'utf-8',
|
||||||
|
LANG: 'zh_CN.UTF-8',
|
||||||
|
PYTHONUTF8: '1'
|
||||||
|
};
|
||||||
|
|
||||||
|
// 尝试最简单的执行方式,不使用chcp
|
||||||
|
const simpleCommand = `"${jianyingExePath}" "${configJsonPath}"`;
|
||||||
|
|
||||||
|
const output = await execAsync(simpleCommand, {
|
||||||
|
maxBuffer: 1024 * 1024 * 10,
|
||||||
|
encoding: 'utf-8',
|
||||||
|
env: env,
|
||||||
|
cwd: path.dirname(jianyingExePath),
|
||||||
|
timeout: 300000
|
||||||
});
|
});
|
||||||
let repalceObject: ReplaceOnject[] = []
|
|
||||||
|
|
||||||
for (let i = 0; i < bookTaskDetails.length; i++) {
|
if (output.stderr && (output.stderr.includes('Error') || output.stderr.includes('failed') || output.stderr.includes('UnicodeEncodeError'))) {
|
||||||
const bookTaskDetail = bookTaskDetails[i];
|
throw new Error(output.stderr);
|
||||||
if (!isEmpty(bookTaskDetail.generateVideoPath) && await CheckFileOrDirExist(bookTaskDetail.generateVideoPath)) {
|
}
|
||||||
repalceObject.push({
|
|
||||||
materialName: path.basename(bookTaskDetail.outImagePath),
|
// 导出成功
|
||||||
videoPath: bookTaskDetail.generateVideoPath,
|
let stdout = output.stdout;
|
||||||
imagePath: bookTaskDetail.outImagePath
|
// 将导出的日志写道文件里面
|
||||||
})
|
let exportLogPath = path.join(book.bookFolderPath, `scripts/JianYingExportLog/${draftName}_export_log_${new Date().getTime()}.txt`);
|
||||||
|
await CheckFolderExistsOrCreate(path.dirname(exportLogPath));
|
||||||
|
await fs.promises.writeFile(exportLogPath, stdout, 'utf-8');
|
||||||
|
|
||||||
|
// 导出成功 将草稿名字返回
|
||||||
|
result.push(draftName);
|
||||||
|
} catch (fallbackError) {
|
||||||
|
// 记录详细的错误信息到文件
|
||||||
|
const errorLogPath = path.join(book.bookFolderPath, `scripts/JianYingExportLog/error_${draftName}_${new Date().getTime()}.txt`);
|
||||||
|
await CheckFolderExistsOrCreate(path.dirname(errorLogPath));
|
||||||
|
const errorInfo = {
|
||||||
|
spawnError: execError.message,
|
||||||
|
execAsyncError: fallbackError.message,
|
||||||
|
scriptPath: jianyingExePath,
|
||||||
|
configPath: configJsonPath,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
};
|
||||||
|
await fs.promises.writeFile(errorLogPath, JSON.stringify(errorInfo, null, 2), 'utf-8');
|
||||||
|
|
||||||
|
throw new Error(`所有执行方法都失败了。详细错误已记录到: ${errorLogPath}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 这边操作草稿,修改数据(把图片替换为视频)
|
|
||||||
if (repalceObject && repalceObject.length > 0) {
|
|
||||||
await this.jianyingService.ReplaceDraftMaterialImageToVideo(book.name + "_" + element.name, repalceObject);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 所有的草稿都添加完毕之后开始返回
|
// 所有的草稿都添加完毕之后开始返回
|
||||||
|
|||||||
@ -19,6 +19,7 @@ class MJApi {
|
|||||||
mjSimpleSetting: MJSettingModel.MjSimpleSettingModel
|
mjSimpleSetting: MJSettingModel.MjSimpleSettingModel
|
||||||
bootType: string
|
bootType: string
|
||||||
imagineUrl: string
|
imagineUrl: string
|
||||||
|
videoUrl: string
|
||||||
fetchTaskUrl: string
|
fetchTaskUrl: string
|
||||||
describeUrl: string
|
describeUrl: string
|
||||||
|
|
||||||
@ -56,6 +57,51 @@ class MJApi {
|
|||||||
return randomAccountId
|
return randomAccountId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async InitMJAPISetting() {
|
||||||
|
let defaultApiUrl = GetMJUrlOptions("api");
|
||||||
|
// 获取自定义的API的地址
|
||||||
|
let customApiUrl = await this.optionServices.GetOptionByKey(OptionKeyName.MJ_CustomAPISetting);
|
||||||
|
if (customApiUrl.code == 0) {
|
||||||
|
throw new Error("加载MJ设置失败,失败原因如下:" + customApiUrl.message)
|
||||||
|
}
|
||||||
|
if (!(customApiUrl.data == null || isEmpty(customApiUrl.data.value) || !ValidateJson(customApiUrl.data.value))) {
|
||||||
|
let customApiUrlData = ValidateJsonAndParse(customApiUrl.data.value) as any[];
|
||||||
|
customApiUrlData.forEach((item: any) => {
|
||||||
|
let baseUrl = item.baseUrl.replace(/\/$/, '')
|
||||||
|
defaultApiUrl.push({
|
||||||
|
label: item.name,
|
||||||
|
value: item.id,
|
||||||
|
isPackage: true,
|
||||||
|
mj_url: {
|
||||||
|
imagine: baseUrl + '/mj/submit/imagine',
|
||||||
|
video: baseUrl + '/mj/submit/video',
|
||||||
|
describe: baseUrl + '/mj/submit/describe',
|
||||||
|
update_file: baseUrl + '/mj/submit/upload-discord-images',
|
||||||
|
once_get_task: baseUrl + '/mj/task/${id}/fetch',
|
||||||
|
query_url: null
|
||||||
|
} as any,
|
||||||
|
buy_url: null
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
let apiUrlIndex = defaultApiUrl.findIndex(item => item.value == this.mj_globalSetting.mj_apiSetting.mjApiUrl);
|
||||||
|
if (apiUrlIndex == -1) {
|
||||||
|
throw new Error('没有找到MJ API对应的请求URL,请检查配置');
|
||||||
|
}
|
||||||
|
let apiUrlItem = defaultApiUrl[apiUrlIndex];
|
||||||
|
if (apiUrlItem.mj_url == null) {
|
||||||
|
throw new Error('没有找到MJ API对应的请求URL,请检查配置');
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
imagineUrl: apiUrlItem.mj_url.imagine,
|
||||||
|
videoUrl: apiUrlItem.mj_url.video,
|
||||||
|
describeUrl: apiUrlItem.mj_url.describe,
|
||||||
|
fetchTaskUrl: apiUrlItem.mj_url.once_get_task
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 初始化MJ设置
|
* 初始化MJ设置
|
||||||
*/
|
*/
|
||||||
@ -77,6 +123,7 @@ class MJApi {
|
|||||||
this.bootType = this.mjSimpleSetting.selectRobot == MJRobotType.NIJI ? "NIJI_JOURNEY" : "MID_JOURNEY"
|
this.bootType = this.mjSimpleSetting.selectRobot == MJRobotType.NIJI ? "NIJI_JOURNEY" : "MID_JOURNEY"
|
||||||
if (this.mjSimpleSetting.type == MJImageType.REMOTE_MJ) {
|
if (this.mjSimpleSetting.type == MJImageType.REMOTE_MJ) {
|
||||||
this.imagineUrl = define.remotemj_api + 'mj/submit/imagine'
|
this.imagineUrl = define.remotemj_api + 'mj/submit/imagine'
|
||||||
|
this.videoUrl = undefined; // 远程MJ不支持视频
|
||||||
this.describeUrl = define.remotemj_api + 'mj/submit/describe'
|
this.describeUrl = define.remotemj_api + 'mj/submit/describe'
|
||||||
this.fetchTaskUrl = define.remotemj_api + 'mj/task/${id}/fetch'
|
this.fetchTaskUrl = define.remotemj_api + 'mj/task/${id}/fetch'
|
||||||
} else if (this.mjSimpleSetting.type == MJImageType.LOCAL_MJ) {
|
} else if (this.mjSimpleSetting.type == MJImageType.LOCAL_MJ) {
|
||||||
@ -90,6 +137,7 @@ class MJApi {
|
|||||||
localRemoteBaseUrl = localRemoteBaseUrl.slice(0, -1)
|
localRemoteBaseUrl = localRemoteBaseUrl.slice(0, -1)
|
||||||
}
|
}
|
||||||
this.imagineUrl = localRemoteBaseUrl + ":" + localRemotePort + '/mj/submit/imagine'
|
this.imagineUrl = localRemoteBaseUrl + ":" + localRemotePort + '/mj/submit/imagine'
|
||||||
|
this.videoUrl = undefined; // 本地代理模式不支持视频
|
||||||
this.describeUrl = localRemoteBaseUrl + ":" + localRemotePort + '/mj/submit/describe'
|
this.describeUrl = localRemoteBaseUrl + ":" + localRemotePort + '/mj/submit/describe'
|
||||||
this.fetchTaskUrl = localRemoteBaseUrl + ":" + localRemotePort + '/mj/task/${id}/fetch'
|
this.fetchTaskUrl = localRemoteBaseUrl + ":" + localRemotePort + '/mj/task/${id}/fetch'
|
||||||
} else if (this.mjSimpleSetting.type == MJImageType.PACKAGE_MJ) {
|
} else if (this.mjSimpleSetting.type == MJImageType.PACKAGE_MJ) {
|
||||||
@ -109,6 +157,7 @@ class MJApi {
|
|||||||
isPackage: true,
|
isPackage: true,
|
||||||
mj_url: {
|
mj_url: {
|
||||||
imagine: baseUrl + '/mj/submit/imagine',
|
imagine: baseUrl + '/mj/submit/imagine',
|
||||||
|
video: undefined, // 生图包不支持视频
|
||||||
describe: baseUrl + '/mj/submit/describe',
|
describe: baseUrl + '/mj/submit/describe',
|
||||||
update_file: baseUrl + '/mj/submit/upload-discord-images',
|
update_file: baseUrl + '/mj/submit/upload-discord-images',
|
||||||
once_get_task: baseUrl + '/mj/task/${id}/fetch',
|
once_get_task: baseUrl + '/mj/task/${id}/fetch',
|
||||||
@ -128,47 +177,16 @@ class MJApi {
|
|||||||
throw new Error('没有找到MJ API对应的请求URL,请检查配置');
|
throw new Error('没有找到MJ API对应的请求URL,请检查配置');
|
||||||
}
|
}
|
||||||
this.imagineUrl = apiUrlItem.mj_url.imagine
|
this.imagineUrl = apiUrlItem.mj_url.imagine
|
||||||
|
this.videoUrl = apiUrlItem.mj_url.video
|
||||||
this.describeUrl = apiUrlItem.mj_url.describe
|
this.describeUrl = apiUrlItem.mj_url.describe
|
||||||
this.fetchTaskUrl = apiUrlItem.mj_url.once_get_task
|
this.fetchTaskUrl = apiUrlItem.mj_url.once_get_task
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
let defaultApiUrl = GetMJUrlOptions("api");
|
let { imagineUrl, videoUrl, describeUrl, fetchTaskUrl } = await this.InitMJAPISetting();
|
||||||
// 获取自定义的API的地址
|
this.imagineUrl = imagineUrl
|
||||||
let customApiUrl = await this.optionServices.GetOptionByKey(OptionKeyName.MJ_CustomAPISetting);
|
this.videoUrl = videoUrl
|
||||||
if (customApiUrl.code == 0) {
|
this.describeUrl = describeUrl
|
||||||
throw new Error("加载MJ设置失败,失败原因如下:" + customApiUrl.message)
|
this.fetchTaskUrl = fetchTaskUrl
|
||||||
}
|
|
||||||
if (!(customApiUrl.data == null || isEmpty(customApiUrl.data.value) || !ValidateJson(customApiUrl.data.value))) {
|
|
||||||
let customApiUrlData = ValidateJsonAndParse(customApiUrl.data.value) as any[];
|
|
||||||
customApiUrlData.forEach((item: any) => {
|
|
||||||
let baseUrl = item.baseUrl.replace(/\/$/, '')
|
|
||||||
defaultApiUrl.push({
|
|
||||||
label: item.name,
|
|
||||||
value: item.id,
|
|
||||||
isPackage: true,
|
|
||||||
mj_url: {
|
|
||||||
imagine: baseUrl + '/mj/submit/imagine',
|
|
||||||
describe: baseUrl + '/mj/submit/describe',
|
|
||||||
update_file: baseUrl + '/mj/submit/upload-discord-images',
|
|
||||||
once_get_task: baseUrl + '/mj/task/${id}/fetch',
|
|
||||||
query_url: null
|
|
||||||
},
|
|
||||||
buy_url: null
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
let apiUrlIndex = defaultApiUrl.findIndex(item => item.value == this.mj_globalSetting.mj_apiSetting.mjApiUrl);
|
|
||||||
if (apiUrlIndex == -1) {
|
|
||||||
throw new Error('没有找到MJ API对应的请求URL,请检查配置');
|
|
||||||
}
|
|
||||||
let apiUrlItem = defaultApiUrl[apiUrlIndex];
|
|
||||||
if (apiUrlItem.mj_url == null) {
|
|
||||||
throw new Error('没有找到MJ API对应的请求URL,请检查配置');
|
|
||||||
}
|
|
||||||
this.imagineUrl = apiUrlItem.mj_url.imagine
|
|
||||||
this.describeUrl = apiUrlItem.mj_url.describe
|
|
||||||
this.fetchTaskUrl = apiUrlItem.mj_url.once_get_task
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
63
src/main/Service/Options/optionSerialization.ts
Normal file
63
src/main/Service/Options/optionSerialization.ts
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import { OptionType } from '@/define/enum/option'
|
||||||
|
import { OptionModel } from '@/model/option/option'
|
||||||
|
import { isEmpty } from 'lodash'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将字符串转换为指定类型的值
|
||||||
|
* @param value 要转换的字符串值
|
||||||
|
* @param type 目标类型 ('string'|'number'|'boolean'|'json')
|
||||||
|
* @returns 转换后的值
|
||||||
|
*/
|
||||||
|
export function convertStringToType<T>(value: string, type: OptionType, checkString?: string): T {
|
||||||
|
let checkErrorString = '请到 ' + checkString + ' 检查设置!'
|
||||||
|
// 如果值为空,直接报错
|
||||||
|
if (value === undefined || value === null || value === '') {
|
||||||
|
throw new Error('当前值为空!' + checkString ? checkErrorString : '')
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
switch (type.toLowerCase()) {
|
||||||
|
case 'string':
|
||||||
|
return value as unknown as T
|
||||||
|
case 'number':
|
||||||
|
const num = Number(value)
|
||||||
|
if (isNaN(num)) {
|
||||||
|
throw new Error(
|
||||||
|
`Cannot convert "${value}" to number, ${checkString ? checkErrorString : ''}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return num as unknown as T
|
||||||
|
case 'boolean':
|
||||||
|
return (value.toLowerCase() === 'true' || value === '1') as unknown as T
|
||||||
|
case 'json':
|
||||||
|
try {
|
||||||
|
return JSON.parse(value) as T
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error(`Invalid JSON string: ${value}, ${checkString ? checkErrorString : ''}`)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw new Error(`Unsupported type: ${type}`)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将选项对象的值转换为指定类型
|
||||||
|
* @param option 选项对象
|
||||||
|
* @param defaultValue 默认值,当值为空时返回的默认值
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const optionSerialization = <T>(
|
||||||
|
option: OptionModel.OptionItem | null,
|
||||||
|
checkString?: string
|
||||||
|
): T => {
|
||||||
|
if (option == null) {
|
||||||
|
throw new Error('未找到选项对象,请检查所有的选项设置是否存在!')
|
||||||
|
}
|
||||||
|
if (option.value == null || option.value == undefined || isEmpty(option.value)) {
|
||||||
|
throw new Error('option value is null')
|
||||||
|
}
|
||||||
|
return convertStringToType<T>(option.value, option.type as OptionType, checkString)
|
||||||
|
}
|
||||||
@ -99,7 +99,7 @@ export class OptionServices {
|
|||||||
if (ValidateJson(CW_AISettingData)) {
|
if (ValidateJson(CW_AISettingData)) {
|
||||||
return successMessage(JSON.parse(CW_AISettingData), "数据已存在,无需再次同步或初始化", "OptionOptions.InitCopyWritingAISetting")
|
return successMessage(JSON.parse(CW_AISettingData), "数据已存在,无需再次同步或初始化", "OptionOptions.InitCopyWritingAISetting")
|
||||||
} else {
|
} else {
|
||||||
this.optionRealmService.ModifyOptionByKey(OptionKeyName.CW_AISetting, JSON.stringify(aiSetting), OptionType.JOSN);
|
this.optionRealmService.ModifyOptionByKey(OptionKeyName.CW_AISetting, JSON.stringify(aiSetting), OptionType.JSON);
|
||||||
return successMessage(aiSetting, "数据已存在,但是数据格式不正确,已重新初始化", "OptionOptions.InitCopyWritingAISetting")
|
return successMessage(aiSetting, "数据已存在,但是数据格式不正确,已重新初始化", "OptionOptions.InitCopyWritingAISetting")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -112,16 +112,16 @@ export class OptionServices {
|
|||||||
let softwareData = software.toJSON()[0]
|
let softwareData = software.toJSON()[0]
|
||||||
let SynchronizeAISetting = softwareData["aiSetting"] as string
|
let SynchronizeAISetting = softwareData["aiSetting"] as string
|
||||||
if (ValidateJson(SynchronizeAISetting)) {
|
if (ValidateJson(SynchronizeAISetting)) {
|
||||||
this.optionRealmService.ModifyOptionByKey(OptionKeyName.CW_AISetting, SynchronizeAISetting, OptionType.JOSN);
|
this.optionRealmService.ModifyOptionByKey(OptionKeyName.CW_AISetting, SynchronizeAISetting, OptionType.JSON);
|
||||||
return successMessage(JSON.parse(SynchronizeAISetting), "同步旧文案处理AI设置数据成功", "OptionOptions.InitCopyWritingAISetting")
|
return successMessage(JSON.parse(SynchronizeAISetting), "同步旧文案处理AI设置数据成功", "OptionOptions.InitCopyWritingAISetting")
|
||||||
} else {
|
} else {
|
||||||
this.optionRealmService.ModifyOptionByKey(OptionKeyName.CW_AISetting, JSON.stringify(aiSetting), OptionType.JOSN);
|
this.optionRealmService.ModifyOptionByKey(OptionKeyName.CW_AISetting, JSON.stringify(aiSetting), OptionType.JSON);
|
||||||
return successMessage(aiSetting, "旧的文案处理AI设置无效,已重新重置", "OptionOptions.InitCopyWritingAISetting")
|
return successMessage(aiSetting, "旧的文案处理AI设置无效,已重新重置", "OptionOptions.InitCopyWritingAISetting")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 新设置
|
// 新设置
|
||||||
this.optionRealmService.ModifyOptionByKey(OptionKeyName.CW_AISetting, JSON.stringify(aiSetting), OptionType.JOSN);
|
this.optionRealmService.ModifyOptionByKey(OptionKeyName.CW_AISetting, JSON.stringify(aiSetting), OptionType.JSON);
|
||||||
return successMessage(aiSetting, '初始化文案处理AI设置成功', 'OptionOptions.SynchronizeAISettingOldData')
|
return successMessage(aiSetting, '初始化文案处理AI设置成功', 'OptionOptions.SynchronizeAISettingOldData')
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
return errorMessage(
|
return errorMessage(
|
||||||
|
|||||||
@ -102,4 +102,60 @@ export default class ElectronInterface {
|
|||||||
return errorMessage('选择文件错误,错误信息如下:' + error.message, 'SystemIpc_SelectMultipleFile');
|
return errorMessage('选择文件错误,错误信息如下:' + error.message, 'SystemIpc_SelectMultipleFile');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 选择文件夹或指定后缀的文件
|
||||||
|
* @param extensions 文件后缀列表(可选)
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
public async SelectFolderOrFile(extensions?: string[]): Promise<GeneralResponse.SuccessItem | GeneralResponse.ErrorItem> {
|
||||||
|
try {
|
||||||
|
// 使用消息框让用户选择类型
|
||||||
|
const choice = await dialog.showMessageBox({
|
||||||
|
type: 'question',
|
||||||
|
title: '选择类型',
|
||||||
|
message: '请选择要选择的类型:',
|
||||||
|
buttons: ['选择文件', '选择文件夹', '取消'],
|
||||||
|
defaultId: 0,
|
||||||
|
cancelId: 2
|
||||||
|
});
|
||||||
|
|
||||||
|
if (choice.response === 2) {
|
||||||
|
throw new Error('用户取消选择');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (choice.response === 0) {
|
||||||
|
// 选择文件
|
||||||
|
const result = await dialog.showOpenDialog({
|
||||||
|
properties: ['openFile'],
|
||||||
|
filters: extensions && extensions.length > 0 ? [
|
||||||
|
{ name: 'Audio Files', extensions },
|
||||||
|
{ name: 'All Files', extensions: ['*'] }
|
||||||
|
] : [{ name: 'All Files', extensions: ['*'] }],
|
||||||
|
title: '选择文件'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.filePaths.length === 0) {
|
||||||
|
throw new Error('没有选择文件');
|
||||||
|
}
|
||||||
|
|
||||||
|
return successMessage(result.filePaths[0], '选择文件成功', 'SystemIpc_SelectFolderOrFile');
|
||||||
|
} else {
|
||||||
|
// 选择文件夹
|
||||||
|
const result = await dialog.showOpenDialog({
|
||||||
|
properties: ['openDirectory'],
|
||||||
|
title: '选择文件夹'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.filePaths.length === 0) {
|
||||||
|
throw new Error('没有选择文件夹');
|
||||||
|
}
|
||||||
|
|
||||||
|
return successMessage(result.filePaths[0], '选择文件夹成功', 'SystemIpc_SelectFolderOrFile');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('选择文件或文件夹错误:', error);
|
||||||
|
return errorMessage('选择文件或文件夹错误,错误信息如下:' + error.message, 'SystemIpc_SelectFolderOrFile');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -379,6 +379,8 @@ export class TaskManager {
|
|||||||
case BookBackTaskType.RUNWAY_VIDEO:
|
case BookBackTaskType.RUNWAY_VIDEO:
|
||||||
case BookBackTaskType.LUMA_VIDEO:
|
case BookBackTaskType.LUMA_VIDEO:
|
||||||
case BookBackTaskType.KLING_VIDEO:
|
case BookBackTaskType.KLING_VIDEO:
|
||||||
|
case BookBackTaskType.MJ_VIDEO:
|
||||||
|
case BookBackTaskType.MJ_VIDEO_EXTEND:
|
||||||
this.AddImageToVideo(task);
|
this.AddImageToVideo(task);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|||||||
@ -137,6 +137,8 @@ export class KlingService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async FetchKlingVideoResult(bookTaskDetail: Book.SelectBookTaskDetail, task: TaskModal.Task, taskId: string, baseUrl: string, gptApiKey: string, useTransfer: boolean = false) {
|
async FetchKlingVideoResult(bookTaskDetail: Book.SelectBookTaskDetail, task: TaskModal.Task, taskId: string, baseUrl: string, gptApiKey: string, useTransfer: boolean = false) {
|
||||||
while (true) {
|
while (true) {
|
||||||
try {
|
try {
|
||||||
@ -243,5 +245,4 @@ export class KlingService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
515
src/main/Service/video/mjVideo.ts
Normal file
515
src/main/Service/video/mjVideo.ts
Normal file
@ -0,0 +1,515 @@
|
|||||||
|
import { TaskModal } from "@/model/task";
|
||||||
|
import { BookBasicHandle } from "../Book/bookBasicHandle";
|
||||||
|
import MJApi from "@/main/Service/MJ/mjApi"
|
||||||
|
import { ValidateJson } from "@/define/Tools/validate";
|
||||||
|
import { BookTaskDetail } from "@/model/book/bookTaskDetail";
|
||||||
|
import { ImageToVideoModels, MJVideoMotion, VideoStatus } from "@/define/enum/video";
|
||||||
|
import { GetImageBase64 } from "@/define/Tools/image";
|
||||||
|
import axios from "axios";
|
||||||
|
import { SendMessageToRenderer } from "../globalService";
|
||||||
|
import { ResponseMessageType } from "@/define/enum/softwareEnum";
|
||||||
|
import { Book } from "@/model/book/book";
|
||||||
|
import { cloneDeep, isEmpty } from "lodash";
|
||||||
|
import { BookBackTaskStatus, BookTaskStatus } from "@/define/enum/bookEnum";
|
||||||
|
import { errorMessage, successMessage } from "@/main/Public/generalTools";
|
||||||
|
import { DEFINE_STRING } from "@/define/define_string";
|
||||||
|
import path from "path";
|
||||||
|
import { CheckFolderExistsOrCreate, CopyFileOrFolder } from "@/define/Tools/file";
|
||||||
|
import { DownloadFile } from "@/define/Tools/common";
|
||||||
|
import { define } from "@/define/define";
|
||||||
|
import { c } from "naive-ui";
|
||||||
|
|
||||||
|
export class MJVideoService extends BookBasicHandle {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
//#region InitMJSetting
|
||||||
|
/**
|
||||||
|
* 初始化MJ设置
|
||||||
|
* @returns 返回MJ全局设置和视频URL
|
||||||
|
*/
|
||||||
|
async InitMJSetting() {
|
||||||
|
try {
|
||||||
|
// 创建MJ API实例
|
||||||
|
let mjApi = new MJApi();
|
||||||
|
// 初始化MJ设置
|
||||||
|
await mjApi.InitMJSetting();
|
||||||
|
|
||||||
|
let { imagineUrl, videoUrl, describeUrl, fetchTaskUrl } = await mjApi.InitMJAPISetting();
|
||||||
|
// 返回全局设置和视频URL
|
||||||
|
return {
|
||||||
|
mj_globalSetting: mjApi.mj_globalSetting,
|
||||||
|
videoUrl: videoUrl,
|
||||||
|
fetchTaskUrl: fetchTaskUrl
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// 如果初始化失败,抛出错误
|
||||||
|
throw new Error(`初始化MJ设置失败,${error.message},请检查MJ配置`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
|
||||||
|
//#region MJImageToVideo
|
||||||
|
/**
|
||||||
|
* MJ图片转视频处理方法
|
||||||
|
* 将指定的图片通过Midjourney API转换为视频
|
||||||
|
* @param task 任务对象,包含小说任务详情ID等信息
|
||||||
|
* @returns Promise<void>
|
||||||
|
* @throws 当初始化失败、参数错误或API调用失败时抛出异常
|
||||||
|
*/
|
||||||
|
async MJImageToVideo(task: TaskModal.Task): Promise<void> {
|
||||||
|
try {
|
||||||
|
await this.InitBookBasicHandle();
|
||||||
|
let bookTaskDetail = this.bookTaskDetailService.GetBookTaskDetailDataById(task.bookTaskDetailId);
|
||||||
|
if (bookTaskDetail == null) {
|
||||||
|
throw new Error("未找到对应的小说批次任务分镜数据,请检查");
|
||||||
|
}
|
||||||
|
|
||||||
|
let videoMessage = bookTaskDetail.videoMessage;
|
||||||
|
if (videoMessage == null) {
|
||||||
|
throw new Error("小说批次任务分镜数据的转视频配置为空,请检查");
|
||||||
|
}
|
||||||
|
|
||||||
|
let mjVideoOptionsString = bookTaskDetail.videoMessage.mjVideoOptions;
|
||||||
|
if (!ValidateJson(mjVideoOptionsString)) {
|
||||||
|
throw new Error("MJ 图转视频 参数错误,请检查");
|
||||||
|
}
|
||||||
|
let mjVideoOptions: BookTaskDetail.MjVideoOptions = JSON.parse(mjVideoOptionsString);
|
||||||
|
|
||||||
|
let imageUrl = videoMessage.imageUrl?.trim() || mjVideoOptions.image?.trim() || "";
|
||||||
|
let prompt = videoMessage.prompt?.trim();
|
||||||
|
let motion: MJVideoMotion = mjVideoOptions.motion === MJVideoMotion.High
|
||||||
|
? MJVideoMotion.High
|
||||||
|
: MJVideoMotion.Low;
|
||||||
|
|
||||||
|
let raw = mjVideoOptions.raw || false;
|
||||||
|
|
||||||
|
// 判断 图片是不是网络图片,不是网络图片的话判断当前图片再本地是不是存在,存在的话讲图片转为 base64
|
||||||
|
if (!imageUrl.startsWith("http")) {
|
||||||
|
imageUrl = await GetImageBase64(imageUrl.split("?t=")[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断是不是有 有效的提示词 有的话 判断是不是视频原始 是的话 在提示词后面添加 --raw
|
||||||
|
if (!isEmpty(prompt) && raw) {
|
||||||
|
prompt = prompt + " --raw";
|
||||||
|
}
|
||||||
|
|
||||||
|
let body = {
|
||||||
|
prompt: prompt,
|
||||||
|
image: imageUrl,
|
||||||
|
motion: motion,
|
||||||
|
}
|
||||||
|
|
||||||
|
let useTransfer = false;
|
||||||
|
|
||||||
|
let { mj_globalSetting, videoUrl, fetchTaskUrl } = await this.InitMJSetting();
|
||||||
|
console.log("MJImageToVideo", mj_globalSetting, videoUrl);
|
||||||
|
|
||||||
|
|
||||||
|
let apiKey = mj_globalSetting.mj_apiSetting.apiKey;
|
||||||
|
// 开始请求
|
||||||
|
let res = await axios.post(videoUrl, body, {
|
||||||
|
headers: {
|
||||||
|
"Authorization": apiKey
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log("MJImageToVideo response", res.data);
|
||||||
|
let resData = res.data;
|
||||||
|
let id = resData.result;
|
||||||
|
|
||||||
|
// 修改Task, 将数据写入
|
||||||
|
this.bookBackTaskListService.UpdateBackTaskData(task.id, {
|
||||||
|
taskId: id,
|
||||||
|
taskMessage: JSON.stringify(resData),
|
||||||
|
});
|
||||||
|
|
||||||
|
// 修改videoMessage数据
|
||||||
|
videoMessage.taskId = id;
|
||||||
|
videoMessage.status = VideoStatus.WAIT;
|
||||||
|
videoMessage.messageData = JSON.stringify(resData);
|
||||||
|
videoMessage.msg = "";
|
||||||
|
delete videoMessage.imageUrl;
|
||||||
|
this.bookTaskDetailService.UpdateBookTaskDetailVideoMessage(task.bookTaskDetailId, videoMessage);
|
||||||
|
|
||||||
|
// 返回前端数据
|
||||||
|
SendMessageToRenderer({
|
||||||
|
code: 1,
|
||||||
|
id: task.bookTaskDetailId,
|
||||||
|
message: "MJ Video 合成任务提交成功",
|
||||||
|
type: ResponseMessageType.MJ_VIDEO,
|
||||||
|
data: JSON.stringify(videoMessage)
|
||||||
|
}, task.messageName);
|
||||||
|
|
||||||
|
await this.FetchMJVideoResult(bookTaskDetail, task, id, fetchTaskUrl, apiKey, useTransfer)
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`MJ 图转视频 失败,失败信息入下:${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
//#region FetchMJVideoResult
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取MJ视频生成结果
|
||||||
|
* 通过轮询方式检查Midjourney视频生成任务的状态,直到任务完成或失败
|
||||||
|
* @param bookTaskDetail 小说任务详情对象,包含视频消息等信息
|
||||||
|
* @param task 任务对象,包含任务ID、消息名称等信息
|
||||||
|
* @param taskId Midjourney返回的任务ID,用于查询任务状态
|
||||||
|
* @param fetchTaskUrl 查询任务状态的API地址模板
|
||||||
|
* @param apiKey API密钥,用于身份验证
|
||||||
|
* @param useTransfer 是否使用传输模式,默认为false
|
||||||
|
* @returns Promise<void>
|
||||||
|
* @throws 当任务失败或API调用异常时抛出错误
|
||||||
|
*/
|
||||||
|
async FetchMJVideoResult(bookTaskDetail: Book.SelectBookTaskDetail, task: TaskModal.Task, taskId: string, fetchTaskUrl: string, apiKey: string, useTransfer: boolean = false) {
|
||||||
|
while (true) {
|
||||||
|
try {
|
||||||
|
|
||||||
|
let fetchUrl = fetchTaskUrl.replace("${id}", taskId);
|
||||||
|
|
||||||
|
let res = await axios.get(fetchUrl, {
|
||||||
|
headers: {
|
||||||
|
"Authorization": apiKey
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
let resData = res.data;
|
||||||
|
|
||||||
|
let status = resData.status.toLowerCase();
|
||||||
|
let code = status == 'failure' || status == 'cancel' ? 0 : 1
|
||||||
|
let progress = resData.progress && resData.progress.length > 0
|
||||||
|
? parseInt(resData.progress.slice(0, -1))
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
if (code == 0) {
|
||||||
|
// 失败
|
||||||
|
let videoMessage = cloneDeep(bookTaskDetail.videoMessage);
|
||||||
|
|
||||||
|
videoMessage.status = VideoStatus.FAIL;
|
||||||
|
videoMessage.msg = resData.failReason;
|
||||||
|
videoMessage.taskId = taskId;
|
||||||
|
videoMessage.messageData = JSON.stringify(resData);
|
||||||
|
|
||||||
|
delete videoMessage.imageUrl;
|
||||||
|
|
||||||
|
// 修改 videoMessage数据
|
||||||
|
this.bookTaskDetailService.UpdateBookTaskDetailVideoMessage(bookTaskDetail.id, videoMessage);
|
||||||
|
|
||||||
|
// 修改TASK
|
||||||
|
this.bookBackTaskListService.UpdateBackTaskData(task.id, {
|
||||||
|
taskId: taskId,
|
||||||
|
taskMessage: JSON.stringify(resData),
|
||||||
|
})
|
||||||
|
|
||||||
|
// 返回前端数据
|
||||||
|
SendMessageToRenderer({
|
||||||
|
code: 0,
|
||||||
|
id: bookTaskDetail.id,
|
||||||
|
message: "MJ VIDEO 合成视频失败,错误信息如下:" + resData.failReason,
|
||||||
|
type: ResponseMessageType.MJ_VIDEO,
|
||||||
|
data: JSON.stringify(videoMessage)
|
||||||
|
}, task.messageName);
|
||||||
|
throw new Error("MJ Video 合成视频失败,错误信息如下:" + resData.failReason);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// 请求成功 但是需要判断状态和返回的进度
|
||||||
|
if (progress == 100 && status == 'success') {
|
||||||
|
let videoMessage = cloneDeep(bookTaskDetail.videoMessage);
|
||||||
|
videoMessage.status = VideoStatus.SUCCESS;
|
||||||
|
videoMessage.taskId = taskId;
|
||||||
|
if (resData.videoUrls && resData.videoUrls.length > 0) {
|
||||||
|
videoMessage.videoUrls = [];
|
||||||
|
resData.videoUrls.forEach((item: any) => {
|
||||||
|
videoMessage.videoUrls.push(item.url);
|
||||||
|
})
|
||||||
|
videoMessage.videoUrl = videoMessage.videoUrls[0];
|
||||||
|
}
|
||||||
|
videoMessage.messageData = JSON.stringify(resData);
|
||||||
|
delete videoMessage.imageUrl;
|
||||||
|
|
||||||
|
this.bookTaskDetailService.UpdateBookTaskDetailVideoMessage(task.bookTaskDetailId, videoMessage);
|
||||||
|
|
||||||
|
this.bookTaskService.UpdetedBookTaskData(task.bookTaskId, {
|
||||||
|
status: BookTaskStatus.IMAGE_TO_VIDEO_SUCCESS,
|
||||||
|
})
|
||||||
|
|
||||||
|
this.bookBackTaskListService.UpdateBackTaskData(task.id, {
|
||||||
|
status: BookBackTaskStatus.DONE,
|
||||||
|
taskId: taskId,
|
||||||
|
taskMessage: JSON.stringify(resData),
|
||||||
|
})
|
||||||
|
|
||||||
|
SendMessageToRenderer({
|
||||||
|
code: 1,
|
||||||
|
id: bookTaskDetail.id,
|
||||||
|
message: "MJ VIDEO 合成视频完成",
|
||||||
|
type: ResponseMessageType.MJ_VIDEO,
|
||||||
|
data: JSON.stringify(videoMessage)
|
||||||
|
}, task.messageName);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 再执行中
|
||||||
|
let videoMessage = cloneDeep(bookTaskDetail.videoMessage);
|
||||||
|
videoMessage.status = VideoStatus.PROCESSING;
|
||||||
|
videoMessage.taskId = taskId;
|
||||||
|
videoMessage.messageData = JSON.stringify(resData);
|
||||||
|
delete videoMessage.imageUrl;
|
||||||
|
this.bookTaskDetailService.UpdateBookTaskDetailVideoMessage(task.bookTaskDetailId, videoMessage);
|
||||||
|
|
||||||
|
SendMessageToRenderer({
|
||||||
|
code: 1,
|
||||||
|
id: bookTaskDetail.id,
|
||||||
|
message: "MJ VIDEO 合成任务正在合成中",
|
||||||
|
type: ResponseMessageType.MJ_VIDEO,
|
||||||
|
data: JSON.stringify(videoMessage)
|
||||||
|
}, task.messageName);
|
||||||
|
|
||||||
|
// 没有成功 等待二十秒后继续执行
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 20000));
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
//#region MJVideoExtend
|
||||||
|
|
||||||
|
async MJVideoExtend(task: TaskModal.Task): Promise<void> {
|
||||||
|
try {
|
||||||
|
await this.InitBookBasicHandle();
|
||||||
|
|
||||||
|
let bookTaskDetail = this.bookTaskDetailService.GetBookTaskDetailDataById(task.bookTaskDetailId);
|
||||||
|
if (bookTaskDetail == null) {
|
||||||
|
throw new Error("未找到对应的小说批次任务分镜数据,请检查");
|
||||||
|
}
|
||||||
|
|
||||||
|
let videoMessage = bookTaskDetail.videoMessage;
|
||||||
|
if (videoMessage == null) {
|
||||||
|
throw new Error("小说批次任务分镜数据的转视频配置为空,请检查");
|
||||||
|
}
|
||||||
|
|
||||||
|
let mjVideoOptionsString = bookTaskDetail.videoMessage.mjVideoOptions;
|
||||||
|
if (!ValidateJson(mjVideoOptionsString)) {
|
||||||
|
throw new Error("MJ 图转视频 参数错误,请检查");
|
||||||
|
}
|
||||||
|
let mjVideoOptions: BookTaskDetail.MjVideoOptions = JSON.parse(mjVideoOptionsString);
|
||||||
|
|
||||||
|
let { mj_globalSetting, videoUrl, fetchTaskUrl } = await this.InitMJSetting();
|
||||||
|
|
||||||
|
console.log("MJVideoExtend", mj_globalSetting, videoUrl);
|
||||||
|
|
||||||
|
let prompt = videoMessage.prompt?.trim();
|
||||||
|
let motion: MJVideoMotion = mjVideoOptions.motion === MJVideoMotion.High
|
||||||
|
? MJVideoMotion.High
|
||||||
|
: MJVideoMotion.Low;
|
||||||
|
let action = 'extend';
|
||||||
|
let index = mjVideoOptions.index;
|
||||||
|
let taskId = mjVideoOptions.taskId;
|
||||||
|
let raw = mjVideoOptions.raw || false;
|
||||||
|
|
||||||
|
if (index == undefined || index < 0 || index > 3 || index == null) {
|
||||||
|
throw new Error("MJ视频拓展参数错误,index必须大于等于0且小于等于3,请检查");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isEmpty(taskId)) {
|
||||||
|
throw new Error("MJ视频拓展参数错误,taskId不能为空,请检查");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isEmpty(prompt)) {
|
||||||
|
if (raw) {
|
||||||
|
prompt = prompt + " --raw";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let body = {
|
||||||
|
prompt,
|
||||||
|
motion,
|
||||||
|
action,
|
||||||
|
index,
|
||||||
|
taskId,
|
||||||
|
}
|
||||||
|
let apiKey = mj_globalSetting.mj_apiSetting.apiKey;
|
||||||
|
let res = await axios.post(videoUrl, body, {
|
||||||
|
headers: {
|
||||||
|
"Authorization": apiKey
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log("MJVideoExtend response", res.data);
|
||||||
|
let resData = res.data;
|
||||||
|
|
||||||
|
let id = resData.result;
|
||||||
|
|
||||||
|
// 修改Task, 将数据写入
|
||||||
|
this.bookBackTaskListService.UpdateBackTaskData(task.id, {
|
||||||
|
taskId: id,
|
||||||
|
taskMessage: JSON.stringify(resData),
|
||||||
|
});
|
||||||
|
|
||||||
|
// 修改videoMessage数据
|
||||||
|
videoMessage.taskId = id;
|
||||||
|
videoMessage.status = VideoStatus.WAIT;
|
||||||
|
videoMessage.messageData = JSON.stringify(resData);
|
||||||
|
videoMessage.msg = "";
|
||||||
|
delete videoMessage.imageUrl;
|
||||||
|
this.bookTaskDetailService.UpdateBookTaskDetailVideoMessage(task.bookTaskDetailId, videoMessage);
|
||||||
|
|
||||||
|
// 返回前端数据
|
||||||
|
// 返回前端数据
|
||||||
|
SendMessageToRenderer({
|
||||||
|
code: 1,
|
||||||
|
id: task.bookTaskDetailId,
|
||||||
|
message: "MJ Video EXTENT 任务提交成功",
|
||||||
|
type: ResponseMessageType.MJ_VIDEO_EXTEND,
|
||||||
|
data: JSON.stringify(videoMessage)
|
||||||
|
}, task.messageName);
|
||||||
|
|
||||||
|
let useTransfer = false;
|
||||||
|
await this.FetchMJVideoResult(bookTaskDetail, task, id, fetchTaskUrl, apiKey, useTransfer)
|
||||||
|
} catch (error) {
|
||||||
|
console.error("MJVideoExtend Error:", error);
|
||||||
|
throw new Error(`MJ视频拓展初始化失败,错误信息:${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
//#region ReloadMJVideoTask
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重新加载MJ视频任务
|
||||||
|
* 重新获取Midjourney视频任务的状态和结果,下载视频文件到本地并更新数据库
|
||||||
|
* @param bookTaskDetail 小说任务详情对象,包含视频消息、任务ID等信息
|
||||||
|
* @param taskId Midjourney返回的任务ID,用于查询任务状态
|
||||||
|
* @returns Promise<any> 返回成功或失败的消息对象
|
||||||
|
* @throws 当任务状态不正确、文件下载失败或数据更新异常时抛出错误
|
||||||
|
*
|
||||||
|
* 功能说明:
|
||||||
|
* 1. 验证任务状态必须为success且进度为100%
|
||||||
|
* 2. 重新获取视频URL并更新数据库状态
|
||||||
|
* 3. 下载主视频文件到本地指定目录
|
||||||
|
* 4. 下载所有子视频文件并更新路径信息
|
||||||
|
* 5. 更新小说任务状态为图转视频成功
|
||||||
|
*/
|
||||||
|
async ReloadMJVideoTask(bookTaskDetail: Book.SelectBookTaskDetail, taskId: string) {
|
||||||
|
await this.InitBookBasicHandle();
|
||||||
|
|
||||||
|
let { mj_globalSetting, videoUrl, fetchTaskUrl } = await this.InitMJSetting();
|
||||||
|
let apiKey = mj_globalSetting.mj_apiSetting.apiKey;
|
||||||
|
let fetchUrl = fetchTaskUrl.replace("${id}", taskId);
|
||||||
|
|
||||||
|
let res = await axios.get(fetchUrl, {
|
||||||
|
headers: {
|
||||||
|
"Authorization": apiKey
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
let resData = res.data;
|
||||||
|
if (res.status == 204) {
|
||||||
|
return errorMessage("当前分镜的视频任务状态为 204,没有数据返回,可能是任务不存在或者已被删除,请检查!");
|
||||||
|
}
|
||||||
|
let status = resData.status.toLowerCase();
|
||||||
|
let progress = resData.progress && resData.progress.length > 0
|
||||||
|
? parseInt(resData.progress.slice(0, -1))
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
if (status != 'success' || progress != 100) {
|
||||||
|
return errorMessage("当前分镜的视频任务状态不为 success 或者进度不是 100%,不可重新加载!");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开始处理数据返回
|
||||||
|
let videoMessage = cloneDeep(bookTaskDetail.videoMessage);
|
||||||
|
|
||||||
|
videoMessage.status = VideoStatus.SUCCESS;
|
||||||
|
videoMessage.taskId = taskId;
|
||||||
|
videoMessage.msg = "";
|
||||||
|
if (resData.videoUrls && resData.videoUrls.length > 0) {
|
||||||
|
videoMessage.videoUrls = [];
|
||||||
|
resData.videoUrls.forEach((item: any) => {
|
||||||
|
videoMessage.videoUrls.push(item.url);
|
||||||
|
})
|
||||||
|
videoMessage.videoUrl = videoMessage.videoUrls[0];
|
||||||
|
}
|
||||||
|
videoMessage.messageData = JSON.stringify(resData);
|
||||||
|
delete videoMessage.imageUrl;
|
||||||
|
|
||||||
|
this.bookTaskDetailService.UpdateBookTaskDetailVideoMessage(bookTaskDetail.id, videoMessage);
|
||||||
|
|
||||||
|
this.bookTaskService.UpdetedBookTaskData(bookTaskDetail.bookTaskId, {
|
||||||
|
status: BookTaskStatus.IMAGE_TO_VIDEO_SUCCESS,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 这边开始下载视频
|
||||||
|
let book = this.bookService.GetBookDataById(bookTaskDetail.bookId);
|
||||||
|
if (book == null) {
|
||||||
|
return errorMessage("重新加载视频任务失败,未找到对应的小说数据,请检查");
|
||||||
|
}
|
||||||
|
|
||||||
|
let remoteVideoUrl = bookTaskDetail.videoMessage.videoUrl;
|
||||||
|
let remoteVideoUrls = bookTaskDetail.videoMessage.videoUrls || [];
|
||||||
|
if (isEmpty(remoteVideoUrl)) {
|
||||||
|
return errorMessage("重新加载视频任务失败,未找到对应的小说分镜视频地址,请检查");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开始下载 remoteVideoUrl 并且修改对应的数据
|
||||||
|
let videoPath = path.join(book.bookFolderPath, `data/video/temp/${bookTaskDetail.name}_${new Date().getTime()}.mp4`);
|
||||||
|
await CheckFolderExistsOrCreate(path.dirname(videoPath));
|
||||||
|
await DownloadFile(remoteVideoUrl, videoPath);
|
||||||
|
|
||||||
|
let targetPath = path.join(book.bookFolderPath, `data/video/${bookTaskDetail.name}.mp4`);
|
||||||
|
await CopyFileOrFolder(videoPath, targetPath);
|
||||||
|
// 开始修改信息
|
||||||
|
this.bookTaskDetailService.UpdateBookTaskDetail(bookTaskDetail.id, {
|
||||||
|
generateVideoPath: targetPath,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 开始处理 remoteVideoUrls
|
||||||
|
if (remoteVideoUrls && remoteVideoUrls.length > 0) {
|
||||||
|
let tempVideoUrls = bookTaskDetail.subVideoPath || [];
|
||||||
|
let newVideoUrls: Array<string> = []
|
||||||
|
|
||||||
|
for (let i = 0; i < remoteVideoUrls.length; i++) {
|
||||||
|
let tempVideoUrl = remoteVideoUrls[i];
|
||||||
|
let tmepVideoPath = path.join(book.bookFolderPath, `data/video/temp/${bookTaskDetail.name}_${i}_${new Date().getTime()}.mp4`);
|
||||||
|
await CheckFolderExistsOrCreate(path.dirname(tmepVideoPath));
|
||||||
|
await DownloadFile(tempVideoUrl, tmepVideoPath);
|
||||||
|
// 开始修改信息
|
||||||
|
// 将信息添加到里面
|
||||||
|
let a = {
|
||||||
|
localPath: path.relative(define.project_path, tmepVideoPath),
|
||||||
|
remotePath: tempVideoUrl,
|
||||||
|
taskId: bookTaskDetail.videoMessage.taskId,
|
||||||
|
index: i,
|
||||||
|
type: ImageToVideoModels.MJ_VIDEO
|
||||||
|
}
|
||||||
|
newVideoUrls.push(JSON.stringify(a));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开始处理数据
|
||||||
|
// 将原有的视频路径合并到新数组中
|
||||||
|
newVideoUrls.push(...tempVideoUrls);
|
||||||
|
|
||||||
|
this.bookTaskDetailService.UpdateBookTaskDetail(bookTaskDetail.id, {
|
||||||
|
subVideoPath: newVideoUrls,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
let newBookTaskDetail = this.bookTaskDetailService.GetBookTaskDetailDataById(bookTaskDetail.id);
|
||||||
|
if (newBookTaskDetail == null) {
|
||||||
|
return errorMessage("重新加载视频任务失败,未找到对应的小说批次任务分镜数据,请检查");
|
||||||
|
}
|
||||||
|
return successMessage(newBookTaskDetail, "重新加载视频任务完成!");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import { ImageToVideoModels, KlingMode, RunawayModel, RunwaySeconds, VideoModel, VideoStatus } from "@/define/enum/video";
|
import { ImageToVideoModels, KlingMode, MappingTaskTypeToVideoModel, MJVideoMotion, RunawayModel, RunwaySeconds, VideoModel, VideoStatus } from "@/define/enum/video";
|
||||||
import { DownloadFile, GetBaseUrl } from "@/define/Tools/common";
|
import { DownloadFile, GetBaseUrl } from "@/define/Tools/common";
|
||||||
import { errorMessage, successMessage } from "@/main/Public/generalTools";
|
import { errorMessage, successMessage } from "@/main/Public/generalTools";
|
||||||
import { BookTaskDetail } from "@/model/book/bookTaskDetail";
|
import { BookTaskDetail } from "@/model/book/bookTaskDetail";
|
||||||
@ -16,6 +16,7 @@ import { KlingService } from "./kling";
|
|||||||
import { LumaService } from "./luma";
|
import { LumaService } from "./luma";
|
||||||
import { CheckFolderExistsOrCreate, CopyFileOrFolder } from "@/define/Tools/file";
|
import { CheckFolderExistsOrCreate, CopyFileOrFolder } from "@/define/Tools/file";
|
||||||
import { ResponseMessageType } from "@/define/enum/softwareEnum";
|
import { ResponseMessageType } from "@/define/enum/softwareEnum";
|
||||||
|
import { MJVideoService } from "./mjVideo";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 小说图生视频的基础配置
|
* 小说图生视频的基础配置
|
||||||
@ -26,22 +27,21 @@ export class VideoGlobal {
|
|||||||
runwayService: RunwayService
|
runwayService: RunwayService
|
||||||
lumaService: LumaService
|
lumaService: LumaService
|
||||||
klingService: KlingService
|
klingService: KlingService
|
||||||
|
mjVideoService: MJVideoService
|
||||||
constructor() {
|
constructor() {
|
||||||
this.gptService = new GptService();
|
this.gptService = new GptService();
|
||||||
this.bookServiceBasic = new BookServiceBasic();
|
this.bookServiceBasic = new BookServiceBasic();
|
||||||
this.runwayService = new RunwayService();
|
this.runwayService = new RunwayService();
|
||||||
this.lumaService = new LumaService();
|
this.lumaService = new LumaService();
|
||||||
this.klingService = new KlingService();
|
this.klingService = new KlingService();
|
||||||
|
this.mjVideoService = new MJVideoService();
|
||||||
}
|
}
|
||||||
|
|
||||||
//#region 初始化分镜的视频配置
|
//#region 初始化分镜的视频配置
|
||||||
|
|
||||||
/**
|
async InitVideoMessageData(bookTaskDetailId: string) {
|
||||||
* 初始化分镜的视频配置
|
|
||||||
*/
|
|
||||||
async InitVideoMessage(bookTaskDetailId: string) {
|
|
||||||
try {
|
try {
|
||||||
let defaultVideoMode = global.config.defaultVideoMode ?? ImageToVideoModels.RUNWAY;
|
let defaultVideoMode = global.config.defaultVideoMode ?? ImageToVideoModels.MJ_VIDEO;
|
||||||
let { gptUrl, gptApiKey } = await this.gptService.RefreshGptSetting();
|
let { gptUrl, gptApiKey } = await this.gptService.RefreshGptSetting();
|
||||||
console.log("gptUrl", gptUrl, "gptApiKey", gptApiKey);
|
console.log("gptUrl", gptUrl, "gptApiKey", gptApiKey);
|
||||||
|
|
||||||
@ -59,7 +59,6 @@ export class VideoGlobal {
|
|||||||
seconds: RunwaySeconds.FIVE,
|
seconds: RunwaySeconds.FIVE,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
let options = JSON.stringify(optionObject);
|
|
||||||
|
|
||||||
let lumaOptions: BookTaskDetail.lumaOptions = {
|
let lumaOptions: BookTaskDetail.lumaOptions = {
|
||||||
user_prompt: "",
|
user_prompt: "",
|
||||||
@ -79,6 +78,19 @@ export class VideoGlobal {
|
|||||||
duration: RunwaySeconds.FIVE,
|
duration: RunwaySeconds.FIVE,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mjVideoOptions: BookTaskDetail.MjVideoOptions = {
|
||||||
|
action: undefined,
|
||||||
|
image: !isEmpty(bookTaskDetail.outImagePath) ? path.relative(define.project_path, bookTaskDetail.outImagePath) : "", // 或者根据 Image 类型的定义提供默认值
|
||||||
|
index: undefined,
|
||||||
|
motion: MJVideoMotion.High, // 根据 Motion 类型的定义提供默认值
|
||||||
|
noStorage: false,
|
||||||
|
notifyHook: undefined,
|
||||||
|
prompt: null,
|
||||||
|
state: undefined,
|
||||||
|
taskId: undefined,
|
||||||
|
raw: false
|
||||||
|
}
|
||||||
|
|
||||||
let videoMessage: BookTaskDetail.VideoMessage = {
|
let videoMessage: BookTaskDetail.VideoMessage = {
|
||||||
id: bookTaskDetailId,
|
id: bookTaskDetailId,
|
||||||
msg: "",
|
msg: "",
|
||||||
@ -87,13 +99,27 @@ export class VideoGlobal {
|
|||||||
style: "",
|
style: "",
|
||||||
imageUrl: !isEmpty(bookTaskDetail.outImagePath) ? path.relative(define.project_path, bookTaskDetail.outImagePath) : "",
|
imageUrl: !isEmpty(bookTaskDetail.outImagePath) ? path.relative(define.project_path, bookTaskDetail.outImagePath) : "",
|
||||||
bookTaskDetailId: bookTaskDetailId,
|
bookTaskDetailId: bookTaskDetailId,
|
||||||
runwayOptions: options,
|
runwayOptions: JSON.stringify(optionObject),
|
||||||
lumaOptions: JSON.stringify(lumaOptions),
|
lumaOptions: JSON.stringify(lumaOptions),
|
||||||
klingOptions: JSON.stringify(klingOptions),
|
klingOptions: JSON.stringify(klingOptions),
|
||||||
|
mjVideoOptions: JSON.stringify(mjVideoOptions),
|
||||||
status: VideoStatus.WAIT,
|
status: VideoStatus.WAIT,
|
||||||
model: VideoModel.IMAGE_TO_VIDEO
|
model: VideoModel.IMAGE_TO_VIDEO
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return { optionObject, lumaOptions, klingOptions, mjVideoOptions, videoMessage };
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化分镜的视频配置
|
||||||
|
*/
|
||||||
|
async InitVideoMessage(bookTaskDetailId: string) {
|
||||||
|
try {
|
||||||
|
let { optionObject, lumaOptions, klingOptions, mjVideoOptions, videoMessage } = await this.InitVideoMessageData(bookTaskDetailId);
|
||||||
|
|
||||||
await this.bookServiceBasic.UpdateBookTaskDetail(bookTaskDetailId, {
|
await this.bookServiceBasic.UpdateBookTaskDetail(bookTaskDetailId, {
|
||||||
videoMessage: videoMessage,
|
videoMessage: videoMessage,
|
||||||
})
|
})
|
||||||
@ -107,7 +133,7 @@ export class VideoGlobal {
|
|||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
|
|
||||||
//#region 秀嘎视频消息
|
//#region 修改视频消息
|
||||||
/**
|
/**
|
||||||
* 修改小说详情信息的VideoMessage
|
* 修改小说详情信息的VideoMessage
|
||||||
* @param bookTaskDetailId
|
* @param bookTaskDetailId
|
||||||
@ -140,36 +166,79 @@ export class VideoGlobal {
|
|||||||
await this.klingService.KlingImageToVideo(task, gptUrl, gptApiKey, useTransfer);
|
await this.klingService.KlingImageToVideo(task, gptUrl, gptApiKey, useTransfer);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case BookBackTaskType.MJ_VIDEO:
|
||||||
|
// MJ视频的处理
|
||||||
|
await this.mjVideoService.MJImageToVideo(task);
|
||||||
|
break;
|
||||||
|
case BookBackTaskType.MJ_VIDEO_EXTEND:
|
||||||
|
await this.mjVideoService.MJVideoExtend(task);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
throw new Error("暂不支持的视频类型");
|
throw new Error("暂不支持的视频类型");
|
||||||
|
|
||||||
}
|
}
|
||||||
|
// return ;
|
||||||
|
|
||||||
// 执行完毕,开始下载视频
|
// 执行完毕,开始下载视频
|
||||||
let bookTaskDetail = await this.bookServiceBasic.GetBookTaskDetailDataById(task.bookTaskDetailId);
|
let bookTaskDetail = await this.bookServiceBasic.GetBookTaskDetailDataById(task.bookTaskDetailId);
|
||||||
let book = await this.bookServiceBasic.GetBookDataById(task.bookId);
|
let book = await this.bookServiceBasic.GetBookDataById(task.bookId);
|
||||||
|
|
||||||
let videoUrl = bookTaskDetail.videoMessage.videoUrl;
|
let videoUrl = bookTaskDetail.videoMessage.videoUrl;
|
||||||
|
let videoUrls = bookTaskDetail.videoMessage.videoUrls || [];
|
||||||
if (isEmpty(videoUrl)) {
|
if (isEmpty(videoUrl)) {
|
||||||
throw new Error("生成的视频地址为空,请检查");
|
throw new Error("生成的视频地址为空,请检查");
|
||||||
}
|
}
|
||||||
// 开始下载
|
// 开始下载 videoUrl
|
||||||
let videoPath = path.join(book.bookFolderPath, `data/video/temp/${bookTaskDetail.name}_${new Date().getTime()}.mp4`);
|
let videoPath = path.join(book.bookFolderPath, `data/video/temp/${bookTaskDetail.name}_${new Date().getTime()}.mp4`);
|
||||||
await CheckFolderExistsOrCreate(path.dirname(videoPath));
|
await CheckFolderExistsOrCreate(path.dirname(videoPath));
|
||||||
await DownloadFile(videoUrl, videoPath);
|
await DownloadFile(videoUrl, videoPath);
|
||||||
await CopyFileOrFolder(videoPath, path.join(book.bookFolderPath, `data/video/${bookTaskDetail.name}.mp4`));
|
|
||||||
|
let targetPath = path.join(book.bookFolderPath, `data/video/${bookTaskDetail.name}.mp4`);
|
||||||
|
await CopyFileOrFolder(videoPath, targetPath);
|
||||||
// 开始修改信息
|
// 开始修改信息
|
||||||
await this.bookServiceBasic.UpdateBookTaskDetail(bookTaskDetail.id, {
|
await this.bookServiceBasic.UpdateBookTaskDetail(bookTaskDetail.id, {
|
||||||
generateVideoPath: path.relative(define.project_path, videoPath),
|
generateVideoPath: targetPath,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 开始下载 videoUrls
|
||||||
|
if (videoUrls.length > 0) {
|
||||||
|
let tempVideoUrls = bookTaskDetail.subVideoPath || [];
|
||||||
|
let newVideoUrls: string[] = []
|
||||||
|
|
||||||
|
for (let i = 0; i < videoUrls.length; i++) {
|
||||||
|
let tempVideoUrl = videoUrls[i];
|
||||||
|
let tmepVideoPath = path.join(book.bookFolderPath, `data/video/temp/${bookTaskDetail.name}_${i}_${new Date().getTime()}.mp4`);
|
||||||
|
await CheckFolderExistsOrCreate(path.dirname(tmepVideoPath));
|
||||||
|
await DownloadFile(tempVideoUrl, tmepVideoPath);
|
||||||
|
// 开始修改信息
|
||||||
|
// 将信息添加到里面
|
||||||
|
let a = {
|
||||||
|
localPath: path.relative(define.project_path, tmepVideoPath),
|
||||||
|
remotePath: tempVideoUrl,
|
||||||
|
taskId: bookTaskDetail.videoMessage.taskId,
|
||||||
|
index: i,
|
||||||
|
type: MappingTaskTypeToVideoModel(task.type),
|
||||||
|
}
|
||||||
|
newVideoUrls.push(JSON.stringify(a));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开始处理数据
|
||||||
|
// 将原有的视频路径合并到新数组中
|
||||||
|
newVideoUrls.push(...tempVideoUrls);
|
||||||
|
|
||||||
|
await this.bookServiceBasic.UpdateBookTaskDetail(bookTaskDetail.id, {
|
||||||
|
subVideoPath: newVideoUrls,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
let newBookTaskDetail = await this.bookServiceBasic.GetBookTaskDetailDataById(task.bookTaskDetailId);
|
||||||
// 讲数据返回前端
|
// 讲数据返回前端
|
||||||
SendMessageToRenderer({
|
SendMessageToRenderer({
|
||||||
code: 1,
|
code: 1,
|
||||||
id: task.bookTaskDetailId,
|
id: task.bookTaskDetailId,
|
||||||
message: "视频生成成功",
|
message: "视频生成成功",
|
||||||
type: ResponseMessageType.VIDEO_SUCESS,
|
type: ResponseMessageType.VIDEO_SUCESS,
|
||||||
data: videoPath + "?t=" + new Date().getTime()
|
data: JSON.stringify(newBookTaskDetail)
|
||||||
}, task.messageName);
|
}, task.messageName);
|
||||||
console.log("视频生成成功", videoPath);
|
console.log("视频生成成功", videoPath);
|
||||||
|
|
||||||
@ -190,6 +259,7 @@ export class VideoGlobal {
|
|||||||
taskId: "",
|
taskId: "",
|
||||||
msg: message
|
msg: message
|
||||||
})
|
})
|
||||||
|
|
||||||
SendMessageToRenderer({
|
SendMessageToRenderer({
|
||||||
code: 0,
|
code: 0,
|
||||||
id: task.bookTaskDetailId,
|
id: task.bookTaskDetailId,
|
||||||
|
|||||||
@ -21,7 +21,7 @@ import { DiscordIpc, RemoveDiscordIpc } from './IPCEvent/discordIpc.js'
|
|||||||
import { Logger } from './logger.js'
|
import { Logger } from './logger.js'
|
||||||
import { RegisterIpc } from './IPCEvent/index'
|
import { RegisterIpc } from './IPCEvent/index'
|
||||||
|
|
||||||
import { InitRemoteMjSettingType } from './initFunc'
|
import { InitRemoteMjSettingType, InitData as a } from './initFunc'
|
||||||
|
|
||||||
let tools = new Tools()
|
let tools = new Tools()
|
||||||
let imageGenerate = new ImageGenerate(global)
|
let imageGenerate = new ImageGenerate(global)
|
||||||
@ -30,6 +30,7 @@ let softWareServiceBasic = new SoftWareServiceBasic()
|
|||||||
|
|
||||||
async function InitData(gl) {
|
async function InitData(gl) {
|
||||||
await InitRemoteMjSettingType()
|
await InitRemoteMjSettingType()
|
||||||
|
await a()
|
||||||
let res = await setting.getSettingDafultData()
|
let res = await setting.getSettingDafultData()
|
||||||
gl.config = res
|
gl.config = res
|
||||||
return res
|
return res
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
import { isEmpty } from "lodash";
|
import { isEmpty } from "lodash";
|
||||||
import { errorMessage, successMessage } from "./Public/generalTools";
|
import { errorMessage, successMessage } from "./Public/generalTools";
|
||||||
import { SoftWareServiceBasic } from "./Service/ServiceBasic/softwareServiceBasic";
|
import { SoftWareServiceBasic } from "./Service/ServiceBasic/softwareServiceBasic";
|
||||||
|
import { OptionKeyName, OptionType } from "@/define/enum/option";
|
||||||
|
import { OptionServices } from "./Service/Options/optionServices";
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -23,3 +25,41 @@ export async function InitRemoteMjSettingType() {
|
|||||||
errorMessage("初始化远程MJ的设置类型失败," + error.toString(), "InitRemoteMjSettingType")
|
errorMessage("初始化远程MJ的设置类型失败," + error.toString(), "InitRemoteMjSettingType")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化数据函数
|
||||||
|
* @description 用于初始化应用程序所需的选项数据
|
||||||
|
*/
|
||||||
|
export async function InitData() {
|
||||||
|
// 初始化 Options 数据
|
||||||
|
// 循环 initObject 进行添加,在添加之前需要判断数据是不是存在,存在的话不进行处理,直接跳过,只有当不存在的时候在添加
|
||||||
|
let optionService = new OptionServices();
|
||||||
|
// 遍历初始化对象数组
|
||||||
|
for (let i = 0; i < initObject.length; i++) {
|
||||||
|
const item = initObject[i];
|
||||||
|
// 通过键名获取选项数据
|
||||||
|
let res = await optionService.GetOptionByKey(item.key);
|
||||||
|
if (res.code == 1 && res.data == null) {
|
||||||
|
// 不存在,进行添加
|
||||||
|
await optionService.ModifyOptionByKey(item.key, item.value, item.type);
|
||||||
|
} else {
|
||||||
|
// 存在,跳过
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const initObject = [
|
||||||
|
{
|
||||||
|
table: "Options",
|
||||||
|
key: OptionKeyName.ImageToVideo_ShowRightPanel,
|
||||||
|
value: "true",
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
table: "Options",
|
||||||
|
key: OptionKeyName.ImageToVideo_ShowPagination,
|
||||||
|
value: "true",
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
}
|
||||||
|
]
|
||||||
9
src/model/book/book.d.ts
vendored
9
src/model/book/book.d.ts
vendored
@ -145,6 +145,14 @@ declare namespace Book {
|
|||||||
isSelect?: boolean
|
isSelect?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface subVideoPathModel {
|
||||||
|
localPath: string,
|
||||||
|
remotePath: string,
|
||||||
|
taskId: string,
|
||||||
|
index?: number,
|
||||||
|
type: ImageToVideoModels
|
||||||
|
}
|
||||||
|
|
||||||
type SelectBookTaskDetail = {
|
type SelectBookTaskDetail = {
|
||||||
id?: string
|
id?: string
|
||||||
no?: number
|
no?: number
|
||||||
@ -154,6 +162,7 @@ declare namespace Book {
|
|||||||
videoPath?: string // 视频地址
|
videoPath?: string // 视频地址
|
||||||
generateVideoPath?: string // 生成的视频地址
|
generateVideoPath?: string // 生成的视频地址
|
||||||
subVideoPath?: string[] // 生成的批次视频的地址
|
subVideoPath?: string[] // 生成的批次视频的地址
|
||||||
|
subVideoPathObject?: subVideoPath[] //生成视频的完成结构显示
|
||||||
audioPath?: string // 音频地址
|
audioPath?: string // 音频地址
|
||||||
draftDepend?: string // 草稿依赖
|
draftDepend?: string // 草稿依赖
|
||||||
word?: string // 文案
|
word?: string // 文案
|
||||||
|
|||||||
42
src/model/book/bookTaskDetail.d.ts
vendored
42
src/model/book/bookTaskDetail.d.ts
vendored
@ -1,4 +1,4 @@
|
|||||||
import { ImageToVideoModels, KlingMode, RunawayModel, RunwaySeconds, VideoModel, VideoStatus } from "@/define/enum/video";
|
import { ImageToVideoModels, KlingMode, MJVideoAction, MJVideoMotion, RunawayModel, RunwaySeconds, VideoModel, VideoStatus } from "@/define/enum/video";
|
||||||
|
|
||||||
declare namespace BookTaskDetail {
|
declare namespace BookTaskDetail {
|
||||||
|
|
||||||
@ -73,5 +73,45 @@ declare namespace BookTaskDetail {
|
|||||||
callback_url?: string; // 回调地址,可选,生成视频完成后,会向该地址发送通知
|
callback_url?: string; // 回调地址,可选,生成视频完成后,会向该地址发送通知
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface MjVideoOptions {
|
||||||
|
/**
|
||||||
|
* 对视频任务进行操作。不为空时,index、taskId必填
|
||||||
|
*/
|
||||||
|
action?: MJVideoAction;
|
||||||
|
/**
|
||||||
|
* 首帧图片,扩展时可为空
|
||||||
|
*/
|
||||||
|
image: string;
|
||||||
|
/**
|
||||||
|
* 执行的视频索引号
|
||||||
|
*/
|
||||||
|
index?: number;
|
||||||
|
/**
|
||||||
|
* 运动变化
|
||||||
|
*/
|
||||||
|
motion: MJVideoMotion;
|
||||||
|
/**
|
||||||
|
* True时,返回官方链接
|
||||||
|
*/
|
||||||
|
noStorage?: boolean;
|
||||||
|
/**
|
||||||
|
* 回调地址
|
||||||
|
*/
|
||||||
|
notifyHook?: string;
|
||||||
|
/** 提示词 */
|
||||||
|
prompt?: null | string;
|
||||||
|
|
||||||
|
state?: string;
|
||||||
|
/**
|
||||||
|
* 需要操作的视频父任务ID
|
||||||
|
*/
|
||||||
|
taskId?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否减少视频的创意
|
||||||
|
*/
|
||||||
|
raw?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
//#endregion
|
//#endregion
|
||||||
}
|
}
|
||||||
2
src/model/task.d.ts
vendored
2
src/model/task.d.ts
vendored
@ -16,6 +16,8 @@ declare namespace TaskModal {
|
|||||||
startTime?: number
|
startTime?: number
|
||||||
endTime?: number,
|
endTime?: number,
|
||||||
messageName?: string
|
messageName?: string
|
||||||
|
taskId?: string // 任务ID,可能是第三方服务的任务ID
|
||||||
|
taskMessage?: string // 任务消息,可能是第三方服务的任务消息
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TaskCondition {
|
interface TaskCondition {
|
||||||
|
|||||||
@ -13,6 +13,11 @@ const Video = {
|
|||||||
return await ipcRenderer.invoke(DEFINE_STRING.BOOK.UPDATE_BOOK_TASK_DETAIL_VIDEO_MESSAGE, bookTaskDetailId, videoMessage)
|
return await ipcRenderer.invoke(DEFINE_STRING.BOOK.UPDATE_BOOK_TASK_DETAIL_VIDEO_MESSAGE, bookTaskDetailId, videoMessage)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** 重新下载视频任务 */
|
||||||
|
ReloadVideoTaskInfo: async (bookTaskDetailId: string) => {
|
||||||
|
return await ipcRenderer.invoke(DEFINE_STRING.BOOK.RELOAD_VIDEO_TASK_INFO, bookTaskDetailId)
|
||||||
|
},
|
||||||
|
|
||||||
/** 获取指定条件的小说图转视频数据,包含子批次 */
|
/** 获取指定条件的小说图转视频数据,包含子批次 */
|
||||||
GetVideoBookInfoList: async (condition: BookVideo.BookVideoInfoListQuertCondition) => {
|
GetVideoBookInfoList: async (condition: BookVideo.BookVideoInfoListQuertCondition) => {
|
||||||
return await ipcRenderer.invoke(DEFINE_STRING.BOOK.GET_VIDEO_BOOK_INFO_LIST, condition)
|
return await ipcRenderer.invoke(DEFINE_STRING.BOOK.GET_VIDEO_BOOK_INFO_LIST, condition)
|
||||||
|
|||||||
@ -22,5 +22,8 @@ const system = {
|
|||||||
/** 选择多个指定文件后缀的文件 */
|
/** 选择多个指定文件后缀的文件 */
|
||||||
SelectMultipleFile: (value: string[]) => ipcRenderer.invoke(DEFINE_STRING.SYSTEM.SELECT_MULTIPLE_FILE, value),
|
SelectMultipleFile: (value: string[]) => ipcRenderer.invoke(DEFINE_STRING.SYSTEM.SELECT_MULTIPLE_FILE, value),
|
||||||
|
|
||||||
|
/** 选择文件夹或指定后缀的文件 */
|
||||||
|
SelectFolderOrFile: (value?: string[]) => ipcRenderer.invoke(DEFINE_STRING.SYSTEM.SELECT_FOLDER_OR_FILE, value),
|
||||||
|
|
||||||
}
|
}
|
||||||
export { system }
|
export { system }
|
||||||
|
|||||||
10
src/renderer/components.d.ts
vendored
10
src/renderer/components.d.ts
vendored
@ -9,7 +9,6 @@ export {}
|
|||||||
declare module 'vue' {
|
declare module 'vue' {
|
||||||
export interface GlobalComponents {
|
export interface GlobalComponents {
|
||||||
NAlert: typeof import('naive-ui')['NAlert']
|
NAlert: typeof import('naive-ui')['NAlert']
|
||||||
NAvatar: typeof import('naive-ui')['NAvatar']
|
|
||||||
NButton: typeof import('naive-ui')['NButton']
|
NButton: typeof import('naive-ui')['NButton']
|
||||||
NCard: typeof import('naive-ui')['NCard']
|
NCard: typeof import('naive-ui')['NCard']
|
||||||
NCheckbox: typeof import('naive-ui')['NCheckbox']
|
NCheckbox: typeof import('naive-ui')['NCheckbox']
|
||||||
@ -17,16 +16,12 @@ declare module 'vue' {
|
|||||||
NColorPicker: typeof import('naive-ui')['NColorPicker']
|
NColorPicker: typeof import('naive-ui')['NColorPicker']
|
||||||
NDataTable: typeof import('naive-ui')['NDataTable']
|
NDataTable: typeof import('naive-ui')['NDataTable']
|
||||||
NDivider: typeof import('naive-ui')['NDivider']
|
NDivider: typeof import('naive-ui')['NDivider']
|
||||||
NDrawer: typeof import('naive-ui')['NDrawer']
|
|
||||||
NDrawerContent: typeof import('naive-ui')['NDrawerContent']
|
|
||||||
NDropdown: typeof import('naive-ui')['NDropdown']
|
NDropdown: typeof import('naive-ui')['NDropdown']
|
||||||
NDynamicInput: typeof import('naive-ui')['NDynamicInput']
|
NDynamicInput: typeof import('naive-ui')['NDynamicInput']
|
||||||
NDynamicTags: typeof import('naive-ui')['NDynamicTags']
|
NDynamicTags: typeof import('naive-ui')['NDynamicTags']
|
||||||
NEmpty: typeof import('naive-ui')['NEmpty']
|
NEmpty: typeof import('naive-ui')['NEmpty']
|
||||||
NForm: typeof import('naive-ui')['NForm']
|
NForm: typeof import('naive-ui')['NForm']
|
||||||
NFormItem: typeof import('naive-ui')['NFormItem']
|
NFormItem: typeof import('naive-ui')['NFormItem']
|
||||||
NGrid: typeof import('naive-ui')['NGrid']
|
|
||||||
NGridItem: typeof import('naive-ui')['NGridItem']
|
|
||||||
NIcon: typeof import('naive-ui')['NIcon']
|
NIcon: typeof import('naive-ui')['NIcon']
|
||||||
NImage: typeof import('naive-ui')['NImage']
|
NImage: typeof import('naive-ui')['NImage']
|
||||||
NImageGroup: typeof import('naive-ui')['NImageGroup']
|
NImageGroup: typeof import('naive-ui')['NImageGroup']
|
||||||
@ -34,14 +29,10 @@ declare module 'vue' {
|
|||||||
NInputNumber: typeof import('naive-ui')['NInputNumber']
|
NInputNumber: typeof import('naive-ui')['NInputNumber']
|
||||||
NLayout: typeof import('naive-ui')['NLayout']
|
NLayout: typeof import('naive-ui')['NLayout']
|
||||||
NLayoutSider: typeof import('naive-ui')['NLayoutSider']
|
NLayoutSider: typeof import('naive-ui')['NLayoutSider']
|
||||||
NList: typeof import('naive-ui')['NList']
|
|
||||||
NListItem: typeof import('naive-ui')['NListItem']
|
|
||||||
NLog: typeof import('naive-ui')['NLog']
|
NLog: typeof import('naive-ui')['NLog']
|
||||||
NMenu: typeof import('naive-ui')['NMenu']
|
NMenu: typeof import('naive-ui')['NMenu']
|
||||||
NModal: typeof import('naive-ui')['NModal']
|
|
||||||
NPopover: typeof import('naive-ui')['NPopover']
|
NPopover: typeof import('naive-ui')['NPopover']
|
||||||
NProgress: typeof import('naive-ui')['NProgress']
|
NProgress: typeof import('naive-ui')['NProgress']
|
||||||
NScrollbar: typeof import('naive-ui')['NScrollbar']
|
|
||||||
NSelect: typeof import('naive-ui')['NSelect']
|
NSelect: typeof import('naive-ui')['NSelect']
|
||||||
NSlider: typeof import('naive-ui')['NSlider']
|
NSlider: typeof import('naive-ui')['NSlider']
|
||||||
NSpace: typeof import('naive-ui')['NSpace']
|
NSpace: typeof import('naive-ui')['NSpace']
|
||||||
@ -52,7 +43,6 @@ declare module 'vue' {
|
|||||||
NTabs: typeof import('naive-ui')['NTabs']
|
NTabs: typeof import('naive-ui')['NTabs']
|
||||||
NTag: typeof import('naive-ui')['NTag']
|
NTag: typeof import('naive-ui')['NTag']
|
||||||
NText: typeof import('naive-ui')['NText']
|
NText: typeof import('naive-ui')['NText']
|
||||||
NThing: typeof import('naive-ui')['NThing']
|
|
||||||
NTooltip: typeof import('naive-ui')['NTooltip']
|
NTooltip: typeof import('naive-ui')['NTooltip']
|
||||||
NTree: typeof import('naive-ui')['NTree']
|
NTree: typeof import('naive-ui')['NTree']
|
||||||
NUpload: typeof import('naive-ui')['NUpload']
|
NUpload: typeof import('naive-ui')['NUpload']
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import TextCommon from "./text";
|
|||||||
*/
|
*/
|
||||||
async function SaveCWAISimpleSetting() {
|
async function SaveCWAISimpleSetting() {
|
||||||
let optionStore = useOptionStore();
|
let optionStore = useOptionStore();
|
||||||
let saveRes = await window.options.ModifyOptionByKey(OptionKeyName.CW_AISimpleSetting, JSON.stringify(optionStore.CW_AISimpleSetting), OptionType.JOSN);
|
let saveRes = await window.options.ModifyOptionByKey(OptionKeyName.CW_AISimpleSetting, JSON.stringify(optionStore.CW_AISimpleSetting), OptionType.JSON);
|
||||||
if (saveRes.code == 0) {
|
if (saveRes.code == 0) {
|
||||||
throw new Error(saveRes.message);
|
throw new Error(saveRes.message);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -78,7 +78,7 @@ async function InitTTSGlobalSetting() {
|
|||||||
let saveRes = await window.options.ModifyOptionByKey(
|
let saveRes = await window.options.ModifyOptionByKey(
|
||||||
OptionKeyName.TTS_GlobalSetting,
|
OptionKeyName.TTS_GlobalSetting,
|
||||||
JSON.stringify(initData),
|
JSON.stringify(initData),
|
||||||
OptionType.JOSN
|
OptionType.JSON
|
||||||
)
|
)
|
||||||
if (saveRes.code == 0) {
|
if (saveRes.code == 0) {
|
||||||
window.api.showGlobalMessageDialog(saveRes)
|
window.api.showGlobalMessageDialog(saveRes)
|
||||||
@ -108,7 +108,7 @@ async function InitFluxModelList(isMust: boolean = false): Promise<Array<{ label
|
|||||||
let saveRes = await window.options.ModifyOptionByKey(
|
let saveRes = await window.options.ModifyOptionByKey(
|
||||||
OptionKeyName.FLUX_APIModelList,
|
OptionKeyName.FLUX_APIModelList,
|
||||||
JSON.stringify(initData),
|
JSON.stringify(initData),
|
||||||
OptionType.JOSN
|
OptionType.JSON
|
||||||
)
|
)
|
||||||
if (saveRes.code == 0) {
|
if (saveRes.code == 0) {
|
||||||
window.api.showGlobalMessageDialog(saveRes)
|
window.api.showGlobalMessageDialog(saveRes)
|
||||||
@ -124,7 +124,7 @@ async function InitFluxModelList(isMust: boolean = false): Promise<Array<{ label
|
|||||||
let saveRes = await window.options.ModifyOptionByKey(
|
let saveRes = await window.options.ModifyOptionByKey(
|
||||||
OptionKeyName.FLUX_APIModelList,
|
OptionKeyName.FLUX_APIModelList,
|
||||||
JSON.stringify(initData),
|
JSON.stringify(initData),
|
||||||
OptionType.JOSN
|
OptionType.JSON
|
||||||
)
|
)
|
||||||
if (saveRes.code == 0) {
|
if (saveRes.code == 0) {
|
||||||
window.api.showGlobalMessageDialog(saveRes)
|
window.api.showGlobalMessageDialog(saveRes)
|
||||||
@ -163,7 +163,7 @@ async function InitComfyUISetting() {
|
|||||||
let saveRes = await window.options.ModifyOptionByKey(
|
let saveRes = await window.options.ModifyOptionByKey(
|
||||||
OptionKeyName.ComfyUI_SimpleSetting,
|
OptionKeyName.ComfyUI_SimpleSetting,
|
||||||
JSON.stringify(initSimpleSetting),
|
JSON.stringify(initSimpleSetting),
|
||||||
OptionType.JOSN
|
OptionType.JSON
|
||||||
)
|
)
|
||||||
if (saveRes.code == 0) {
|
if (saveRes.code == 0) {
|
||||||
window.api.showGlobalMessageDialog(saveRes)
|
window.api.showGlobalMessageDialog(saveRes)
|
||||||
@ -190,7 +190,7 @@ async function InitComfyUISetting() {
|
|||||||
let saveRes = await window.options.ModifyOptionByKey(
|
let saveRes = await window.options.ModifyOptionByKey(
|
||||||
OptionKeyName.ComfyUI_WorkFlowSetting,
|
OptionKeyName.ComfyUI_WorkFlowSetting,
|
||||||
JSON.stringify(initWorkFlowSetting),
|
JSON.stringify(initWorkFlowSetting),
|
||||||
OptionType.JOSN
|
OptionType.JSON
|
||||||
)
|
)
|
||||||
if (saveRes.code == 0) {
|
if (saveRes.code == 0) {
|
||||||
window.api.showGlobalMessageDialog(saveRes)
|
window.api.showGlobalMessageDialog(saveRes)
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import { useOptionStore } from "@/stores/option";
|
|||||||
*/
|
*/
|
||||||
async function SaveTTSGlobalSetting() {
|
async function SaveTTSGlobalSetting() {
|
||||||
let optionStore = useOptionStore();
|
let optionStore = useOptionStore();
|
||||||
let saveRes = await window.options.ModifyOptionByKey(OptionKeyName.TTS_GlobalSetting, JSON.stringify(optionStore.TTS_GlobalSetting), OptionType.JOSN);
|
let saveRes = await window.options.ModifyOptionByKey(OptionKeyName.TTS_GlobalSetting, JSON.stringify(optionStore.TTS_GlobalSetting), OptionType.JSON);
|
||||||
if (saveRes.code == 0) {
|
if (saveRes.code == 0) {
|
||||||
throw new Error(saveRes.message);
|
throw new Error(saveRes.message);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,7 +9,11 @@
|
|||||||
require-mark-placement="right-hanging"
|
require-mark-placement="right-hanging"
|
||||||
>
|
>
|
||||||
<n-form-item label="书名" path="name">
|
<n-form-item label="书名" path="name">
|
||||||
<n-input v-model:value="reverseManageStore.selectBook.name" placeholder="请输入书名" />
|
<n-input
|
||||||
|
v-model:value="reverseManageStore.selectBook.name"
|
||||||
|
placeholder="请输入书名"
|
||||||
|
:disabled="!reverseManageStore.selectBook"
|
||||||
|
/>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item label="视频文件" path="oldVideoPath">
|
<n-form-item label="视频文件" path="oldVideoPath">
|
||||||
<n-input
|
<n-input
|
||||||
|
|||||||
@ -80,8 +80,10 @@ import {
|
|||||||
NSelect
|
NSelect
|
||||||
} from 'naive-ui'
|
} from 'naive-ui'
|
||||||
import { useReverseManageStore } from '../../../../../../stores/reverseManage'
|
import { useReverseManageStore } from '../../../../../../stores/reverseManage'
|
||||||
|
import { useSoftwareStore } from '../../../../../../stores/software'
|
||||||
import { AddBookTaskCopyData } from '../../../../../../define/enum/bookEnum'
|
import { AddBookTaskCopyData } from '../../../../../../define/enum/bookEnum'
|
||||||
let reverseManageStore = useReverseManageStore()
|
let reverseManageStore = useReverseManageStore()
|
||||||
|
let softwareStore = useSoftwareStore()
|
||||||
let message = useMessage()
|
let message = useMessage()
|
||||||
let formRef = ref(null)
|
let formRef = ref(null)
|
||||||
let creatObj = ref({
|
let creatObj = ref({
|
||||||
|
|||||||
@ -184,6 +184,8 @@ async function ClipDraft(e) {
|
|||||||
closeOnEsc: false,
|
closeOnEsc: false,
|
||||||
title: `生成草稿前检查 ${bookTask.value.name}`,
|
title: `生成草稿前检查 ${bookTask.value.name}`,
|
||||||
maskClosable: false,
|
maskClosable: false,
|
||||||
|
showIcon: false,
|
||||||
|
style: 'width: 800px; max-width: 90vw',
|
||||||
content: () =>
|
content: () =>
|
||||||
h(ManageBookTaskGenerateInformation, {
|
h(ManageBookTaskGenerateInformation, {
|
||||||
bookTask: bookTask.value,
|
bookTask: bookTask.value,
|
||||||
@ -202,6 +204,8 @@ async function GenerateVideo(e) {
|
|||||||
closeOnEsc: false,
|
closeOnEsc: false,
|
||||||
title: `合成视频前检查 ${bookTask.value.name}`,
|
title: `合成视频前检查 ${bookTask.value.name}`,
|
||||||
maskClosable: false,
|
maskClosable: false,
|
||||||
|
showIcon: false,
|
||||||
|
style: 'width: 800px; max-width: 90vw',
|
||||||
content: () =>
|
content: () =>
|
||||||
h(ManageBookTaskGenerateInformation, {
|
h(ManageBookTaskGenerateInformation, {
|
||||||
bookTask: bookTask.value,
|
bookTask: bookTask.value,
|
||||||
|
|||||||
@ -1,116 +1,230 @@
|
|||||||
<template>
|
<template>
|
||||||
|
<n-card class="form-card" size="large">
|
||||||
<n-form
|
<n-form
|
||||||
ref="formRef"
|
ref="formRef"
|
||||||
:model="bookTask"
|
:model="bookTask"
|
||||||
:rules="rules"
|
:rules="rules"
|
||||||
label-width="auto"
|
label-width="auto"
|
||||||
require-mark-placement="right-hanging"
|
require-mark-placement="right-hanging"
|
||||||
:size="size"
|
:size="'medium'"
|
||||||
:style="{
|
label-placement="top"
|
||||||
maxWidth: '640px'
|
class="generate-form"
|
||||||
}"
|
|
||||||
>
|
>
|
||||||
<n-form-item label="选择srt地址" path="srtPath">
|
<!-- 音频文件选择 -->
|
||||||
|
<n-form-item label="配音文件 (MP3/WAV)" path="audioPath">
|
||||||
|
<div class="file-input-group">
|
||||||
<n-input
|
<n-input
|
||||||
style="width: 300px; margin-right: 5px"
|
|
||||||
v-model:value="bookTask.srtPath"
|
|
||||||
type="text"
|
|
||||||
placeholder="SRT字幕地址"
|
|
||||||
/>
|
|
||||||
<n-button color="#e5a84b" @click="SelectSrtFile">
|
|
||||||
<n-icon :size="20">
|
|
||||||
<folder-open />
|
|
||||||
</n-icon>
|
|
||||||
</n-button>
|
|
||||||
</n-form-item>
|
|
||||||
<n-form-item label="选择配音" path="audioPath">
|
|
||||||
<n-input
|
|
||||||
style="width: 300px; margin-right: 5px"
|
|
||||||
v-model:value="bookTask.audioPath"
|
v-model:value="bookTask.audioPath"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="配音地址(mp3或wav)"
|
placeholder="请选择配音文件..."
|
||||||
|
readonly
|
||||||
|
class="file-input"
|
||||||
/>
|
/>
|
||||||
<n-button color="#e5a84b" @click="SelectMusicFile">
|
<n-button type="primary" @click="SelectMusicFile" class="file-button">
|
||||||
<n-icon :size="20">
|
<template #icon>
|
||||||
<folder-open />
|
<n-icon size="18">
|
||||||
|
<FolderOpen />
|
||||||
</n-icon>
|
</n-icon>
|
||||||
|
</template>
|
||||||
|
选择文件
|
||||||
</n-button>
|
</n-button>
|
||||||
</n-form-item>
|
|
||||||
<n-form-item label="选择背景音乐文件夹(随机匹配,自动合成视频必选)" path="backgroundMusic">
|
|
||||||
<n-select
|
|
||||||
style="width: 300px"
|
|
||||||
v-model:value="bookTask.backgroundMusic"
|
|
||||||
filterable
|
|
||||||
placeholder="选择样式"
|
|
||||||
:options="backgroundMusicOptions"
|
|
||||||
clearable
|
|
||||||
/>
|
|
||||||
</n-form-item>
|
|
||||||
<n-form-item label="选择剪映草稿" path="backgroundMusic" v-if="optionType != 'video'">
|
|
||||||
<div>
|
|
||||||
<div style="color: red">
|
|
||||||
注意:选择的草稿主轨道的图片数量要和当前的相同,会生成新的草稿并且只会替换图片,保留其余的数据
|
|
||||||
</div>
|
</div>
|
||||||
|
</n-form-item>
|
||||||
|
|
||||||
|
<!-- 背景音乐选择 -->
|
||||||
|
<n-form-item label="背景音乐" path="backgroundMusic">
|
||||||
|
<div class="file-input-group">
|
||||||
|
<n-input
|
||||||
|
v-model:value="bookTask.backgroundMusic"
|
||||||
|
type="text"
|
||||||
|
placeholder="请选择背景音乐文件夹或音频文件..."
|
||||||
|
readonly
|
||||||
|
class="file-input"
|
||||||
|
/>
|
||||||
|
<n-button type="primary" @click="SelectBackgroundMusic" class="file-button">
|
||||||
|
<template #icon>
|
||||||
|
<n-icon size="18">
|
||||||
|
<FolderOpen />
|
||||||
|
</n-icon>
|
||||||
|
</template>
|
||||||
|
选择文件/文件夹
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
</n-form-item>
|
||||||
|
|
||||||
|
<!-- 剪映草稿选择 -->
|
||||||
|
<n-form-item
|
||||||
|
label="剪映草稿模板"
|
||||||
|
path="draftDepend"
|
||||||
|
v-if="optionType != 'video'"
|
||||||
|
:show-feedback="false"
|
||||||
|
>
|
||||||
|
<div class="draft-section">
|
||||||
<n-select
|
<n-select
|
||||||
style="width: 300px"
|
|
||||||
v-model:value="bookTask.draftDepend"
|
v-model:value="bookTask.draftDepend"
|
||||||
filterable
|
filterable
|
||||||
placeholder="选择样式"
|
placeholder="选择剪映草稿模板"
|
||||||
:options="draftSelect"
|
:options="draftSelect"
|
||||||
clearable
|
clearable
|
||||||
|
class="select-input"
|
||||||
/>
|
/>
|
||||||
|
<n-alert type="warning" :show-icon="true" class="draft-warning">
|
||||||
|
注意:选择的草稿主轨道的图片数量要和当前的相同,会生成新的草稿并且只会替换图片,保留其余的数据,依赖的草稿必须是剪映
|
||||||
|
5.9 级以下版本,不支持 6.0 及以上版本
|
||||||
|
</n-alert>
|
||||||
</div>
|
</div>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item path="backgroundMusic">
|
|
||||||
<n-button :disabled="type == 'book'" type="info" @click="UseBookVideoDataToBookTask"
|
<!-- 操作按钮组 -->
|
||||||
>应用主小说相关数据</n-button
|
<n-form-item>
|
||||||
|
<n-space size="medium" class="button-group">
|
||||||
|
<n-button
|
||||||
|
:disabled="type == 'book'"
|
||||||
|
type="info"
|
||||||
|
@click="UseBookVideoDataToBookTask"
|
||||||
|
class="action-button"
|
||||||
>
|
>
|
||||||
<n-button type="info" style="margin-left: 10px" @click="SaveVideoData">保存数据</n-button>
|
<template #icon>
|
||||||
|
<n-icon size="16">
|
||||||
|
<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>
|
||||||
|
</template>
|
||||||
|
应用主小说数据
|
||||||
|
</n-button>
|
||||||
|
|
||||||
|
<n-button type="primary" @click="SaveVideoData" class="action-button save-button">
|
||||||
|
<template #icon>
|
||||||
|
<n-icon size="16">
|
||||||
|
<svg viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M15,9H5V5H15M12,19A3,3 0 0,1 9,16A3,3 0 0,1 12,13A3,3 0 0,1 15,16A3,3 0 0,1 12,19M17,3H5C3.89,3 3,3.9 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V7L17,3Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</n-icon>
|
||||||
|
</template>
|
||||||
|
保存配置
|
||||||
|
</n-button>
|
||||||
|
</n-space>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<div v-if="type == 'bookTask'" style="color: red">
|
|
||||||
注意:在 生成草稿/合成视频 前要先保存数据
|
<!-- 提示信息 -->
|
||||||
|
<n-alert
|
||||||
|
:type="type == 'bookTask' ? 'info' : 'warning'"
|
||||||
|
:show-icon="true"
|
||||||
|
class="notice-alert"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<span class="alert-title">重要提示</span>
|
||||||
|
</template>
|
||||||
|
<div v-if="type == 'bookTask'">在生成草稿/合成视频前要先保存数据</div>
|
||||||
|
<div v-else>
|
||||||
|
在生成草稿/合成视频前要先保存数据,当前会生成选择的草稿/视频,全部会使用上面的参数
|
||||||
</div>
|
</div>
|
||||||
<div v-else style="color: red">
|
</n-alert>
|
||||||
注意:在 生成草稿/合成视频 前要先保存数据,当前会生成选择的 草稿/视频,全部会使用上面的参数
|
|
||||||
</div>
|
<!-- 主要操作按钮 -->
|
||||||
<n-form-item style="display: flex; justify-content: flex-end">
|
<n-form-item class="main-action-item">
|
||||||
|
<div class="main-action-container">
|
||||||
<n-button
|
<n-button
|
||||||
v-if="optionType == 'video'"
|
v-if="optionType == 'video'"
|
||||||
type="info"
|
type="success"
|
||||||
style="margin-left: 10px"
|
size="large"
|
||||||
@click="AddGenerateVideoTask"
|
@click="AddGenerateVideoTask"
|
||||||
>添加合成视频任务</n-button
|
class="main-action-button"
|
||||||
>
|
>
|
||||||
<n-button v-else type="info" style="margin-left: 10px" @click="AddJianyingDraft"
|
<template #icon>
|
||||||
>生成草稿</n-button
|
<n-icon size="20">
|
||||||
|
<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>
|
||||||
|
</template>
|
||||||
|
添加合成视频任务
|
||||||
|
</n-button>
|
||||||
|
|
||||||
|
<n-button
|
||||||
|
v-else
|
||||||
|
type="success"
|
||||||
|
size="large"
|
||||||
|
@click="AddJianyingDraft"
|
||||||
|
class="main-action-button"
|
||||||
>
|
>
|
||||||
|
<template #icon>
|
||||||
|
<n-icon size="20">
|
||||||
|
<svg viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M18,20H6V4H13V9H18V20Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</n-icon>
|
||||||
|
</template>
|
||||||
|
生成剪映草稿
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
</n-form>
|
</n-form>
|
||||||
|
</n-card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { useMessage, NForm, NFormItem, NButton, NIcon, NInput, NSelect } from 'naive-ui'
|
import {
|
||||||
|
useMessage,
|
||||||
|
useDialog,
|
||||||
|
NForm,
|
||||||
|
NFormItem,
|
||||||
|
NButton,
|
||||||
|
NIcon,
|
||||||
|
NInput,
|
||||||
|
NSelect,
|
||||||
|
NCard,
|
||||||
|
NSpace,
|
||||||
|
NAlert
|
||||||
|
} from 'naive-ui'
|
||||||
import { FolderOpen } from '@vicons/ionicons5'
|
import { FolderOpen } from '@vicons/ionicons5'
|
||||||
import { BookTaskStatus, OperateBookType } from '../../../../../../define/enum/bookEnum'
|
import { BookTaskStatus, OperateBookType } from '../../../../../../define/enum/bookEnum'
|
||||||
import { useReverseManageStore } from '../../../../../../stores/reverseManage'
|
import { useReverseManageStore } from '../../../../../../stores/reverseManage'
|
||||||
|
import { useSoftwareStore } from '@/stores/software'
|
||||||
|
|
||||||
let props = defineProps({
|
const props = defineProps({
|
||||||
bookTask: undefined,
|
bookTask: {
|
||||||
type: undefined,
|
type: Object,
|
||||||
selectBookTask: [],
|
required: true
|
||||||
optionType: undefined
|
},
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
selectBookTask: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
optionType: {
|
||||||
|
type: String,
|
||||||
|
default: 'draft'
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
let bookTask = ref(props.bookTask)
|
const bookTask = ref(props.bookTask)
|
||||||
let type = ref(props.type)
|
const type = ref(props.type)
|
||||||
|
const optionType = ref(props.optionType)
|
||||||
|
|
||||||
let optionType = ref(props.optionType)
|
const message = useMessage()
|
||||||
|
const draftSelect = ref([])
|
||||||
|
const reverseManageStore = useReverseManageStore()
|
||||||
|
const softwareStore = useSoftwareStore()
|
||||||
|
|
||||||
let backgroundMusicOptions = ref([])
|
const dialog = useDialog()
|
||||||
let message = useMessage()
|
|
||||||
let draftSelect = ref([])
|
|
||||||
let reverseManageStore = useReverseManageStore()
|
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
// 获取草稿文件列表
|
// 获取草稿文件列表
|
||||||
@ -123,21 +237,6 @@ onMounted(async () => {
|
|||||||
draftSelect.value.push(obj)
|
draftSelect.value.push(obj)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
// 获取初始化的背景音乐列表
|
|
||||||
await window.api.GetBackgroundMusicConfigList((value) => {
|
|
||||||
if (value.code == 0) {
|
|
||||||
message.error(value.message)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
for (let i = 0; i < value.value.length; i++) {
|
|
||||||
const element = value.value[i]
|
|
||||||
let obj = {
|
|
||||||
label: element.name,
|
|
||||||
value: element.id
|
|
||||||
}
|
|
||||||
backgroundMusicOptions.value.push(obj)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -166,6 +265,21 @@ async function SelectMusicFile() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 选择背景音乐(支持文件夹、MP3、WAV文件)
|
||||||
|
*/
|
||||||
|
async function SelectBackgroundMusic() {
|
||||||
|
// 显示选择对话框让用户选择类型
|
||||||
|
let res = await window.system.SelectFolderOrFile(['mp3', 'wav'])
|
||||||
|
console.log('选择的背景音乐:', res)
|
||||||
|
if (res.code == 0) {
|
||||||
|
message.error(res.message)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
bookTask.value.backgroundMusic = res.data
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 应用主小说相关数据
|
* 应用主小说相关数据
|
||||||
*/
|
*/
|
||||||
@ -273,6 +387,10 @@ async function AddGenerateVideoTask() {
|
|||||||
* 添加草稿
|
* 添加草稿
|
||||||
*/
|
*/
|
||||||
async function AddJianyingDraft() {
|
async function AddJianyingDraft() {
|
||||||
|
softwareStore.spin.spinning = true
|
||||||
|
softwareStore.spin.tip = '正在生成剪映草稿。。。'
|
||||||
|
|
||||||
|
try {
|
||||||
let res = undefined
|
let res = undefined
|
||||||
if (props.type == 'book') {
|
if (props.type == 'book') {
|
||||||
res = await window.book.AddJianyingDraft(props.selectBookTask, OperateBookType.ASSIGNBOOKTASK)
|
res = await window.book.AddJianyingDraft(props.selectBookTask, OperateBookType.ASSIGNBOOKTASK)
|
||||||
@ -303,9 +421,173 @@ async function AddJianyingDraft() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
window.api.showGlobalMessageDialog(res)
|
window.api.showGlobalMessageDialog(res)
|
||||||
|
} finally {
|
||||||
|
softwareStore.spin.spinning = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
let rules = ref({
|
const rules = ref({
|
||||||
srtPath: [{ required: true, message: '请选择背景音乐', trigger: ['input', 'blur', 'change'] }],
|
srtPath: [{ required: true, message: '请选择字幕文件', trigger: ['input', 'blur', 'change'] }],
|
||||||
audioPath: [{ required: true, message: '请选择音频文件', trigger: ['input', 'blur', 'change'] }]
|
audioPath: [{ required: true, message: '请选择音频文件', trigger: ['input', 'blur', 'change'] }]
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.form-card {
|
||||||
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
|
||||||
|
border-radius: 16px;
|
||||||
|
border: none;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
animation: fadeInUp 0.6s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.generate-form {
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-input-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-input {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-button {
|
||||||
|
flex-shrink: 0;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 8px rgba(52, 152, 219, 0.2);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-button:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 12px rgba(52, 152, 219, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-input {
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.draft-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.draft-warning {
|
||||||
|
border-radius: 8px;
|
||||||
|
border: none;
|
||||||
|
box-shadow: 0 2px 8px rgba(230, 126, 34, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-group {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button {
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 8px 20px;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-button {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notice-alert {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: none;
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-title {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-action-item {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-action-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-action-button {
|
||||||
|
padding: 12px 32px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
border: none;
|
||||||
|
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
min-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-action-button:hover {
|
||||||
|
transform: translateY(-3px);
|
||||||
|
box-shadow: 0 8px 25px rgba(102, 126, 234, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-action-button:active {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.file-input-group {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-group {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-action-button {
|
||||||
|
width: 100%;
|
||||||
|
min-width: unset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 动画效果 */
|
||||||
|
@keyframes fadeInUp {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(30px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@ -62,7 +62,6 @@ let props = defineProps({
|
|||||||
let videoType = ref(ImageToVideoModels)
|
let videoType = ref(ImageToVideoModels)
|
||||||
let reverseManageStore = useReverseManageStore()
|
let reverseManageStore = useReverseManageStore()
|
||||||
let message = useMessage()
|
let message = useMessage()
|
||||||
let softwareStore = useSoftwareStore()
|
|
||||||
let runwayRef = ref(null)
|
let runwayRef = ref(null)
|
||||||
|
|
||||||
let videoMessage = ref({})
|
let videoMessage = ref({})
|
||||||
@ -71,7 +70,6 @@ let lumaOptions = ref({})
|
|||||||
let klingOptions = ref({})
|
let klingOptions = ref({})
|
||||||
|
|
||||||
async function GetBookTaskDetailOption() {
|
async function GetBookTaskDetailOption() {
|
||||||
debugger
|
|
||||||
let res = await window.db.GetBookTaskDetailProperty(props.bookTaskDetailId, 'videoMessage')
|
let res = await window.db.GetBookTaskDetailProperty(props.bookTaskDetailId, 'videoMessage')
|
||||||
if (res.code != 1) {
|
if (res.code != 1) {
|
||||||
message.error(res.message)
|
message.error(res.message)
|
||||||
@ -184,7 +182,6 @@ async function AddImageToVideoTask() {
|
|||||||
} else if (videoMessage.value.videoType == ImageToVideoModels.KLING) {
|
} else if (videoMessage.value.videoType == ImageToVideoModels.KLING) {
|
||||||
type = BookBackTaskType.KLING_VIDEO
|
type = BookBackTaskType.KLING_VIDEO
|
||||||
}
|
}
|
||||||
debugger
|
|
||||||
// 添加任务
|
// 添加任务
|
||||||
let res = await window.task.AddBookBackTask(
|
let res = await window.task.AddBookBackTask(
|
||||||
reverseManageStore.selectBook.id,
|
reverseManageStore.selectBook.id,
|
||||||
|
|||||||
@ -68,10 +68,16 @@ export default defineComponent({
|
|||||||
|
|
||||||
async function GetBookByCondition() {
|
async function GetBookByCondition() {
|
||||||
try {
|
try {
|
||||||
|
// 只在第一次加载或者当前没有选中书籍时才重置
|
||||||
|
if (!reverseManageStore.selectBook || !reverseManageStore.selectBook.id) {
|
||||||
|
reverseManageStore.resetSelectBook()
|
||||||
|
}
|
||||||
|
|
||||||
let res = await reverseManageStore.GetBookDataFromDB({
|
let res = await reverseManageStore.GetBookDataFromDB({
|
||||||
page: paginationReactive.page,
|
page: paginationReactive.page,
|
||||||
pageSize: paginationReactive.pageSize
|
pageSize: paginationReactive.pageSize
|
||||||
})
|
})
|
||||||
|
console.log('获取小说任务数据:', res)
|
||||||
if (res.code == 0) {
|
if (res.code == 0) {
|
||||||
message.error(res.message)
|
message.error(res.message)
|
||||||
return
|
return
|
||||||
@ -82,8 +88,8 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
pagination.value.pageCount = res_count
|
pagination.value.pageCount = res_count
|
||||||
|
|
||||||
|
// 开始获取批次任务 - 只有当前有选中的书籍时才尝试加载任务
|
||||||
// 开始获取批次任务
|
if (reverseManageStore.selectBook && reverseManageStore.selectBook.id) {
|
||||||
let bookTask = await reverseManageStore.GetBookTaskDataFromDB({
|
let bookTask = await reverseManageStore.GetBookTaskDataFromDB({
|
||||||
bookId: reverseManageStore.selectBook.id
|
bookId: reverseManageStore.selectBook.id
|
||||||
})
|
})
|
||||||
@ -91,6 +97,7 @@ export default defineComponent({
|
|||||||
message.error(bookTask.message)
|
message.error(bookTask.message)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error('获取小说数据失败')
|
message.error('获取小说数据失败')
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div style="margin-bottom: 5px; margin-left: 5px; display: flex; align-items: center">
|
<div style="margin-bottom: 5px; margin-left: 5px; display: flex; align-items: center">
|
||||||
<n-button size="small" strong secondary type="default">{{
|
<n-button size="small" strong secondary type="default">{{
|
||||||
reverseManageStore.selectBook.name
|
reverseManageStore.selectBook?.name || '未选择小说'
|
||||||
}}</n-button>
|
}}</n-button>
|
||||||
<n-button
|
<n-button
|
||||||
style="margin-left: 5px"
|
style="margin-left: 5px"
|
||||||
@ -76,6 +76,7 @@ import { ResponseMessageType } from '../../../../define/enum/softwareEnum'
|
|||||||
import AddBookTask from './Components/ManageBook/AddBookTask.vue'
|
import AddBookTask from './Components/ManageBook/AddBookTask.vue'
|
||||||
import ManageBookTaskGenerateInformation from './Components/ManageBook/ManageBookTaskGenerateInformation.vue'
|
import ManageBookTaskGenerateInformation from './Components/ManageBook/ManageBookTaskGenerateInformation.vue'
|
||||||
import { isEmpty } from 'lodash'
|
import { isEmpty } from 'lodash'
|
||||||
|
import { TimeDelay } from '@/define/Tools/time'
|
||||||
|
|
||||||
let reverseManageStore = useReverseManageStore()
|
let reverseManageStore = useReverseManageStore()
|
||||||
let softwareStore = useSoftwareStore()
|
let softwareStore = useSoftwareStore()
|
||||||
@ -174,6 +175,10 @@ function createOptions(row) {
|
|||||||
label: `高清 ${row.name}`,
|
label: `高清 ${row.name}`,
|
||||||
key: 'hd'
|
key: 'hd'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: `转视频 ${row.name}`,
|
||||||
|
key: 'image_to_video'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: `生成草稿 ${row.name}`,
|
label: `生成草稿 ${row.name}`,
|
||||||
key: 'draft'
|
key: 'draft'
|
||||||
@ -262,6 +267,36 @@ async function hdImageFunc(id, operateBookType) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 开启图转视频任务 然后跳转到对应的任务详情页
|
||||||
|
async function ImageToVideo(value) {
|
||||||
|
dialog.warning({
|
||||||
|
title: '开启图转视频任务',
|
||||||
|
content: `确定要将图像转换为视频吗?开启之后会在 图/文转视频 任务列表 中显示该任务,若是已经开启的,将直接跳转到图转视频界面,是否继续?`,
|
||||||
|
positiveText: '确定',
|
||||||
|
negativeText: '取消',
|
||||||
|
onPositiveClick: async () => {
|
||||||
|
let res = await window.db.UpdateBookTaskData(value.id, {
|
||||||
|
openVideoGenerate: true
|
||||||
|
})
|
||||||
|
if (res.code == 0) {
|
||||||
|
message.error(res.message)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 任务开启成功
|
||||||
|
message.success('图像转换为视频任务开启成功,即将跳转到图转视频界面!')
|
||||||
|
await TimeDelay(1000)
|
||||||
|
reverseManageStore.selectBookTask = value
|
||||||
|
// 跳转到图转视频任务详情页
|
||||||
|
router.push({
|
||||||
|
name: 'image_text_video'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onNegativeClick: () => {
|
||||||
|
message.info('已取消图像转换为视频任务')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 生成草稿
|
* 生成草稿
|
||||||
*/
|
*/
|
||||||
@ -270,6 +305,8 @@ async function ClipDraft(value) {
|
|||||||
closeOnEsc: false,
|
closeOnEsc: false,
|
||||||
title: `生成草稿前检查 ${value.name}`,
|
title: `生成草稿前检查 ${value.name}`,
|
||||||
maskClosable: false,
|
maskClosable: false,
|
||||||
|
style: 'width: 800px; max-width: 90vw',
|
||||||
|
showIcon: false,
|
||||||
content: () =>
|
content: () =>
|
||||||
h(ManageBookTaskGenerateInformation, {
|
h(ManageBookTaskGenerateInformation, {
|
||||||
bookTask: value,
|
bookTask: value,
|
||||||
@ -287,6 +324,8 @@ async function GenerateVideo(value) {
|
|||||||
closeOnEsc: false,
|
closeOnEsc: false,
|
||||||
title: `合成视频前检查 ${value.name}`,
|
title: `合成视频前检查 ${value.name}`,
|
||||||
maskClosable: false,
|
maskClosable: false,
|
||||||
|
showIcon: false,
|
||||||
|
style: 'width: 800px; max-width: 90vw',
|
||||||
content: () =>
|
content: () =>
|
||||||
h(ManageBookTaskGenerateInformation, {
|
h(ManageBookTaskGenerateInformation, {
|
||||||
bookTask: value,
|
bookTask: value,
|
||||||
@ -372,6 +411,9 @@ async function handleSelect(key) {
|
|||||||
case 'hd':
|
case 'hd':
|
||||||
await HDImage(selectRow.value)
|
await HDImage(selectRow.value)
|
||||||
break
|
break
|
||||||
|
case 'image_to_video':
|
||||||
|
await ImageToVideo(selectRow.value)
|
||||||
|
break
|
||||||
case 'draft':
|
case 'draft':
|
||||||
await ClipDraft(selectRow.value)
|
await ClipDraft(selectRow.value)
|
||||||
break
|
break
|
||||||
@ -466,7 +508,7 @@ async function HDImageAll() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function AddBookDialog() {
|
async function AddBookDialog() {
|
||||||
message.info('新增' + reverseManageStore.selectBook.id)
|
message.info('新增' + (reverseManageStore.selectBook?.id || '未知小说'))
|
||||||
dialog.create({
|
dialog.create({
|
||||||
title: '新增小说批次任务',
|
title: '新增小说批次任务',
|
||||||
showIcon: false,
|
showIcon: false,
|
||||||
@ -487,8 +529,10 @@ async function DraftAll() {
|
|||||||
// 草稿弹窗
|
// 草稿弹窗
|
||||||
dialog.info({
|
dialog.info({
|
||||||
closeOnEsc: false,
|
closeOnEsc: false,
|
||||||
title: `生成草稿前检查 ${reverseManageStore.selectBook.name}`,
|
title: `生成草稿前检查 ${reverseManageStore.selectBook?.name || '未知小说'}`,
|
||||||
maskClosable: false,
|
maskClosable: false,
|
||||||
|
showIcon: false,
|
||||||
|
style: 'width: 800px; max-width: 90vw',
|
||||||
content: () =>
|
content: () =>
|
||||||
h(ManageBookTaskGenerateInformation, {
|
h(ManageBookTaskGenerateInformation, {
|
||||||
bookTask: reverseManageStore.selectBook,
|
bookTask: reverseManageStore.selectBook,
|
||||||
@ -509,8 +553,10 @@ async function VideoAll() {
|
|||||||
// 草稿弹窗
|
// 草稿弹窗
|
||||||
dialog.info({
|
dialog.info({
|
||||||
closeOnEsc: false,
|
closeOnEsc: false,
|
||||||
title: `生成草稿前检查 ${reverseManageStore.selectBook.name}`,
|
title: `生成草稿前检查 ${reverseManageStore.selectBook?.name || '未知小说'}`,
|
||||||
maskClosable: false,
|
maskClosable: false,
|
||||||
|
showIcon: false,
|
||||||
|
style: 'width: 800px; max-width: 90vw',
|
||||||
content: () =>
|
content: () =>
|
||||||
h(ManageBookTaskGenerateInformation, {
|
h(ManageBookTaskGenerateInformation, {
|
||||||
bookTask: reverseManageStore.selectBook,
|
bookTask: reverseManageStore.selectBook,
|
||||||
|
|||||||
234
src/renderer/src/components/Common/TextEllipsis.md
Normal file
234
src/renderer/src/components/Common/TextEllipsis.md
Normal file
@ -0,0 +1,234 @@
|
|||||||
|
# TextEllipsis 通用文本省略组件
|
||||||
|
|
||||||
|
一个智能的文本省略显示组件,当内容超出指定宽度时自动显示tooltip,否则直接显示完整内容。
|
||||||
|
|
||||||
|
## 功能特性
|
||||||
|
|
||||||
|
- 🎯 **智能检测**:自动检测内容是否超出最大宽度
|
||||||
|
- 🏷️ **多组件支持**:支持 `n-text` 和 `n-tag` 组件
|
||||||
|
- 📱 **响应式**:支持动态宽度调整
|
||||||
|
- 🎨 **高度可定制**:支持自定义样式、属性和tooltip配置
|
||||||
|
- ⚡ **性能优化**:只在必要时显示tooltip
|
||||||
|
|
||||||
|
## 基本用法
|
||||||
|
|
||||||
|
### 1. 文本组件 (n-text)
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<!-- 基础用法 -->
|
||||||
|
<TextEllipsis
|
||||||
|
text="这是一段很长的文本内容,可能会超出指定的宽度"
|
||||||
|
max-width="150px"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 自定义文本属性 -->
|
||||||
|
<TextEllipsis
|
||||||
|
text="重要信息"
|
||||||
|
component="n-text"
|
||||||
|
:component-props="{ strong: true, depth: '1' }"
|
||||||
|
max-width="100px"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 标签组件 (n-tag)
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<!-- 基础标签 -->
|
||||||
|
<TextEllipsis
|
||||||
|
text="very-long-task-id-12345678"
|
||||||
|
component="n-tag"
|
||||||
|
:component-props="{ type: 'primary', size: 'small' }"
|
||||||
|
max-width="120px"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 圆角标签 -->
|
||||||
|
<TextEllipsis
|
||||||
|
text="状态标签"
|
||||||
|
component="n-tag"
|
||||||
|
:component-props="{ type: 'success', size: 'small', round: true }"
|
||||||
|
max-width="80px"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
## API 参数
|
||||||
|
|
||||||
|
### Props
|
||||||
|
|
||||||
|
| 参数 | 类型 | 默认值 | 说明 |
|
||||||
|
|------|------|--------|------|
|
||||||
|
| `text` | `String` | `''` | 显示的文本内容(必填) |
|
||||||
|
| `maxWidth` | `String` | `'120px'` | 最大宽度 |
|
||||||
|
| `component` | `String` | `'n-text'` | 组件类型:`'n-text'` 或 `'n-tag'` |
|
||||||
|
| `componentProps` | `Object` | `{}` | 传递给组件的属性 |
|
||||||
|
| `customClass` | `String` | `''` | 自定义CSS类名 |
|
||||||
|
| `showArrow` | `Boolean` | `false` | tooltip是否显示箭头 |
|
||||||
|
| `trigger` | `String` | `'hover'` | tooltip触发方式 |
|
||||||
|
| `placement` | `String` | `'top'` | tooltip显示位置 |
|
||||||
|
| `tooltipMaxWidth` | `String` | `'300px'` | tooltip最大宽度 |
|
||||||
|
| `forceTooltip` | `Boolean` | `false` | 强制显示tooltip |
|
||||||
|
| `disableTooltip` | `Boolean` | `false` | 禁用tooltip |
|
||||||
|
|
||||||
|
### 暴露的方法
|
||||||
|
|
||||||
|
| 方法 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `checkOverflow()` | 手动检查内容是否溢出 |
|
||||||
|
| `isOverflow` | 获取当前溢出状态 |
|
||||||
|
|
||||||
|
## 高级用法
|
||||||
|
|
||||||
|
### 1. 响应式宽度
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<TextEllipsis
|
||||||
|
:text="dynamicText"
|
||||||
|
:max-width="windowWidth < 768 ? '100px' : '200px'"
|
||||||
|
component="n-tag"
|
||||||
|
:component-props="{ type: 'info' }"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
|
||||||
|
const windowWidth = ref(window.innerWidth)
|
||||||
|
const dynamicText = ref('动态内容')
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
window.addEventListener('resize', () => {
|
||||||
|
windowWidth.value = window.innerWidth
|
||||||
|
})
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 强制tooltip模式
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<!-- 总是显示tooltip,即使内容不超出 -->
|
||||||
|
<TextEllipsis
|
||||||
|
text="短文本"
|
||||||
|
max-width="200px"
|
||||||
|
:force-tooltip="true"
|
||||||
|
component="n-tag"
|
||||||
|
:component-props="{ type: 'warning' }"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 禁用tooltip
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<!-- 只显示省略号,不显示tooltip -->
|
||||||
|
<TextEllipsis
|
||||||
|
text="很长的文本内容"
|
||||||
|
max-width="100px"
|
||||||
|
:disable-tooltip="true"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 自定义tooltip配置
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<TextEllipsis
|
||||||
|
text="自定义tooltip配置的文本"
|
||||||
|
max-width="120px"
|
||||||
|
:show-arrow="true"
|
||||||
|
trigger="click"
|
||||||
|
placement="bottom"
|
||||||
|
tooltip-max-width="400px"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 实际应用场景
|
||||||
|
|
||||||
|
### 1. 任务ID显示
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<TextEllipsis
|
||||||
|
:text="task.id"
|
||||||
|
component="n-tag"
|
||||||
|
:component-props="{ type: 'primary', size: 'small' }"
|
||||||
|
max-width="120px"
|
||||||
|
custom-class="task-id-tag"
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 文件路径显示
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<TextEllipsis
|
||||||
|
:text="file.path"
|
||||||
|
component="n-tag"
|
||||||
|
:component-props="{ type: 'default', size: 'small' }"
|
||||||
|
max-width="200px"
|
||||||
|
custom-class="file-path-tag"
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 用户名显示
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<TextEllipsis
|
||||||
|
:text="user.name"
|
||||||
|
component="n-text"
|
||||||
|
:component-props="{ strong: true }"
|
||||||
|
max-width="150px"
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 状态标签
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<TextEllipsis
|
||||||
|
:text="status.message"
|
||||||
|
component="n-tag"
|
||||||
|
:component-props="{
|
||||||
|
type: status.type,
|
||||||
|
size: 'small',
|
||||||
|
round: true
|
||||||
|
}"
|
||||||
|
max-width="100px"
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. **性能优化**:组件会自动检测内容溢出,但频繁的文本变化可能影响性能
|
||||||
|
2. **样式继承**:组件会继承父容器的字体样式来准确计算宽度
|
||||||
|
3. **响应式**:当容器宽度变化时,需要手动调用 `checkOverflow()` 方法
|
||||||
|
4. **浏览器兼容**:使用了现代浏览器的 API,确保目标浏览器支持
|
||||||
|
|
||||||
|
## 样式定制
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<style scoped>
|
||||||
|
/* 自定义样式 */
|
||||||
|
.my-custom-text {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #007bff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.my-custom-tag :deep(.n-tag) {
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<TextEllipsis
|
||||||
|
text="自定义样式文本"
|
||||||
|
max-width="120px"
|
||||||
|
custom-class="my-custom-text"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
211
src/renderer/src/components/Common/TextEllipsis.vue
Normal file
211
src/renderer/src/components/Common/TextEllipsis.vue
Normal file
@ -0,0 +1,211 @@
|
|||||||
|
<template>
|
||||||
|
<div class="text-ellipsis-wrapper" :style="{ maxWidth: maxWidth }">
|
||||||
|
<!-- 如果内容超出限制则显示 tooltip -->
|
||||||
|
<n-tooltip v-if="isOverflow" :show-arrow="showArrow" :trigger="trigger" :placement="placement">
|
||||||
|
<template #trigger>
|
||||||
|
<n-tag
|
||||||
|
v-if="component === 'n-tag'"
|
||||||
|
v-bind="componentProps"
|
||||||
|
:class="['text-ellipsis-content', customClass]"
|
||||||
|
:style="ellipsisStyle"
|
||||||
|
>
|
||||||
|
{{ text }}
|
||||||
|
</n-tag>
|
||||||
|
<n-text
|
||||||
|
v-else
|
||||||
|
v-bind="componentProps"
|
||||||
|
:class="['text-ellipsis-content', customClass]"
|
||||||
|
:style="ellipsisStyle"
|
||||||
|
>
|
||||||
|
{{ text }}
|
||||||
|
</n-text>
|
||||||
|
</template>
|
||||||
|
<!-- tooltip 内容 -->
|
||||||
|
<div class="tooltip-content" :style="{ maxWidth: tooltipMaxWidth }">
|
||||||
|
{{ text }}
|
||||||
|
</div>
|
||||||
|
</n-tooltip>
|
||||||
|
|
||||||
|
<!-- 如果内容未超出限制则直接显示 -->
|
||||||
|
<n-tag
|
||||||
|
v-else-if="component === 'n-tag'"
|
||||||
|
v-bind="componentProps"
|
||||||
|
:class="['text-ellipsis-content', customClass]"
|
||||||
|
>
|
||||||
|
{{ text }}
|
||||||
|
</n-tag>
|
||||||
|
<n-text
|
||||||
|
v-else
|
||||||
|
v-bind="componentProps"
|
||||||
|
:class="['text-ellipsis-content', customClass]"
|
||||||
|
>
|
||||||
|
{{ text }}
|
||||||
|
</n-text>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, onMounted, nextTick, watch } from 'vue'
|
||||||
|
import { NTooltip, NText, NTag } from 'naive-ui'
|
||||||
|
|
||||||
|
// 定义 props
|
||||||
|
const props = defineProps({
|
||||||
|
// 显示的文本内容
|
||||||
|
text: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
// 最大宽度
|
||||||
|
maxWidth: {
|
||||||
|
type: String,
|
||||||
|
default: '120px'
|
||||||
|
},
|
||||||
|
// 组件类型:'n-text' 或 'n-tag'
|
||||||
|
component: {
|
||||||
|
type: String,
|
||||||
|
default: 'n-text',
|
||||||
|
validator: (value) => ['n-text', 'n-tag'].includes(value)
|
||||||
|
},
|
||||||
|
// 组件的额外属性
|
||||||
|
componentProps: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
// 自定义 CSS 类名
|
||||||
|
customClass: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
// tooltip 是否显示箭头
|
||||||
|
showArrow: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
// tooltip 触发方式
|
||||||
|
trigger: {
|
||||||
|
type: String,
|
||||||
|
default: 'hover',
|
||||||
|
validator: (value) => ['hover', 'click', 'focus', 'manual'].includes(value)
|
||||||
|
},
|
||||||
|
// tooltip 显示位置
|
||||||
|
placement: {
|
||||||
|
type: String,
|
||||||
|
default: 'top',
|
||||||
|
validator: (value) => [
|
||||||
|
'top', 'top-start', 'top-end',
|
||||||
|
'right', 'right-start', 'right-end',
|
||||||
|
'bottom', 'bottom-start', 'bottom-end',
|
||||||
|
'left', 'left-start', 'left-end'
|
||||||
|
].includes(value)
|
||||||
|
},
|
||||||
|
// tooltip 最大宽度
|
||||||
|
tooltipMaxWidth: {
|
||||||
|
type: String,
|
||||||
|
default: '300px'
|
||||||
|
},
|
||||||
|
// 是否强制显示 tooltip
|
||||||
|
forceTooltip: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
// 是否禁用 tooltip
|
||||||
|
disableTooltip: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 是否内容溢出
|
||||||
|
const isOverflow = ref(false)
|
||||||
|
|
||||||
|
// 省略号样式
|
||||||
|
const ellipsisStyle = computed(() => ({
|
||||||
|
maxWidth: props.maxWidth,
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
display: 'inline-flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
verticalAlign: 'middle'
|
||||||
|
}))
|
||||||
|
|
||||||
|
// 检查内容是否溢出
|
||||||
|
const checkOverflow = async () => {
|
||||||
|
if (props.disableTooltip) {
|
||||||
|
isOverflow.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.forceTooltip) {
|
||||||
|
isOverflow.value = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 简化的溢出检测:基于文本长度和宽度的估算
|
||||||
|
const maxWidthValue = parseInt(props.maxWidth.replace('px', ''))
|
||||||
|
const estimatedCharWidth = 14 // 假设每个字符约14px宽度
|
||||||
|
const estimatedTextWidth = props.text.length * estimatedCharWidth
|
||||||
|
|
||||||
|
isOverflow.value = estimatedTextWidth > maxWidthValue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听文本和最大宽度变化
|
||||||
|
watch([() => props.text, () => props.maxWidth, () => props.forceTooltip, () => props.disableTooltip], () => {
|
||||||
|
checkOverflow()
|
||||||
|
}, { immediate: true })
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
checkOverflow()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 暴露方法给父组件
|
||||||
|
defineExpose({
|
||||||
|
checkOverflow,
|
||||||
|
isOverflow: computed(() => isOverflow.value)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.text-ellipsis-wrapper {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-ellipsis-content {
|
||||||
|
cursor: default;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-ellipsis-content:deep(.n-tag) {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-ellipsis-content:deep(.n-tag__content) {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-ellipsis-content:deep(.n-text) {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-content {
|
||||||
|
word-wrap: break-word;
|
||||||
|
word-break: break-all;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 当组件为 n-tag 时的特殊样式 */
|
||||||
|
.text-ellipsis-content.n-tag {
|
||||||
|
max-width: inherit;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -87,7 +87,7 @@ async function InitCopyWritingData() {
|
|||||||
let saveRes = await window.options.ModifyOptionByKey(
|
let saveRes = await window.options.ModifyOptionByKey(
|
||||||
OptionKeyName.CW_AISimpleSetting,
|
OptionKeyName.CW_AISimpleSetting,
|
||||||
JSON.stringify(optionStore.CW_AISimpleSetting),
|
JSON.stringify(optionStore.CW_AISimpleSetting),
|
||||||
OptionType.JOSN
|
OptionType.JSON
|
||||||
)
|
)
|
||||||
if (saveRes.code == 0) {
|
if (saveRes.code == 0) {
|
||||||
throw new Error('初始化文案处理界面数据失败,错误信息:' + saveRes.message)
|
throw new Error('初始化文案处理界面数据失败,错误信息:' + saveRes.message)
|
||||||
|
|||||||
@ -193,7 +193,7 @@ async function SaveData() {
|
|||||||
let res = await window.options.ModifyOptionByKey(
|
let res = await window.options.ModifyOptionByKey(
|
||||||
OptionKeyName.CW_AISimpleSetting,
|
OptionKeyName.CW_AISimpleSetting,
|
||||||
JSON.stringify(optionStore.CW_AISimpleSetting),
|
JSON.stringify(optionStore.CW_AISimpleSetting),
|
||||||
OptionType.JOSN
|
OptionType.JSON
|
||||||
)
|
)
|
||||||
if (res.code == 0) {
|
if (res.code == 0) {
|
||||||
window.api.showGlobalMessageDialog(res)
|
window.api.showGlobalMessageDialog(res)
|
||||||
|
|||||||
@ -6,14 +6,19 @@
|
|||||||
:options="menuOptions"
|
:options="menuOptions"
|
||||||
:render-icon="renderIcon"
|
:render-icon="renderIcon"
|
||||||
:expand-icon="expandIcon"
|
:expand-icon="expandIcon"
|
||||||
|
:value="currentMenuKey"
|
||||||
default-value="mainHome"
|
default-value="mainHome"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
import { generateMenuOptions, renderMenuIcon, expandIcon } from '@/renderer/src/common/homeMenu'
|
import { generateMenuOptions, renderMenuIcon, expandIcon } from '@/renderer/src/common/homeMenu'
|
||||||
|
|
||||||
|
// 获取当前路由
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
// Props
|
// Props
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
collapsed: {
|
collapsed: {
|
||||||
@ -29,6 +34,42 @@ const props = defineProps({
|
|||||||
// Emits
|
// Emits
|
||||||
const emit = defineEmits(['openBackTask'])
|
const emit = defineEmits(['openBackTask'])
|
||||||
|
|
||||||
|
// 计算当前菜单key
|
||||||
|
const currentMenuKey = computed(() => {
|
||||||
|
// 根据当前路由名称获取对应的菜单key
|
||||||
|
const routeName = route.name
|
||||||
|
console.log('当前路由名称:', routeName)
|
||||||
|
|
||||||
|
// 路由名称到菜单key的映射
|
||||||
|
const routeToMenuKeyMap = {
|
||||||
|
'mainHome': 'mainHome',
|
||||||
|
'gptCopywriting': 'gptCopywriting',
|
||||||
|
'sdoriginal': 'sdoriginal',
|
||||||
|
'getframe': 'backward_frame',
|
||||||
|
'copywriting': 'copywriting',
|
||||||
|
'pushBackPrompt': 'push_back',
|
||||||
|
'regenerate': 'regenerate',
|
||||||
|
'VideoGenerate': 'VideoGenerate',
|
||||||
|
'book_management': 'book_management',
|
||||||
|
'image_text_video': 'image_text_video',
|
||||||
|
'image_text_video_info': 'image_text_video',
|
||||||
|
'lai_api': 'lai_api',
|
||||||
|
'TTS_Services': 'TTS_Services',
|
||||||
|
'global_setting': 'global_setting',
|
||||||
|
'clip_setting': 'clip_setting',
|
||||||
|
'videogeneratesetting': 'videogeneratesetting',
|
||||||
|
'sd_setting': 'sd_setting',
|
||||||
|
'mj_setting': 'mj_setting',
|
||||||
|
'toolbox': 'toolbox',
|
||||||
|
'image-upload': 'toolbox',
|
||||||
|
'image-compress': 'toolbox'
|
||||||
|
}
|
||||||
|
|
||||||
|
const menuKey = routeToMenuKeyMap[routeName] || 'mainHome'
|
||||||
|
console.log('计算出的菜单key:', menuKey)
|
||||||
|
return menuKey
|
||||||
|
})
|
||||||
|
|
||||||
// 计算菜单选项
|
// 计算菜单选项
|
||||||
const menuOptions = computed(() => {
|
const menuOptions = computed(() => {
|
||||||
return generateMenuOptions(props.showOriginal, () => {
|
return generateMenuOptions(props.showOriginal, () => {
|
||||||
|
|||||||
@ -29,10 +29,12 @@
|
|||||||
<div v-if="selectedNovel" class="chapter-container">
|
<div v-if="selectedNovel" class="chapter-container">
|
||||||
<!-- 章节列表 -->
|
<!-- 章节列表 -->
|
||||||
<ChapterList
|
<ChapterList
|
||||||
|
:key="forceRenderKey"
|
||||||
:selected-novel="selectedNovel"
|
:selected-novel="selectedNovel"
|
||||||
@open-chapter="handleOpenChapter"
|
@open-chapter="handleOpenChapter"
|
||||||
@add-tag="handleAddTag"
|
@add-tag="handleAddTag"
|
||||||
@remove-tag="handleRemoveTag"
|
@remove-tag="handleRemoveTag"
|
||||||
|
@refresh-data="refreshData"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -46,7 +48,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted, provide } from 'vue'
|
||||||
import { useMessage } from 'naive-ui'
|
import { useMessage } from 'naive-ui'
|
||||||
import NovelSidebar from './components/ImageTextVideo/ImageTextVideoBookSidebar.vue'
|
import NovelSidebar from './components/ImageTextVideo/ImageTextVideoBookSidebar.vue'
|
||||||
import MobileHeader from './components/ImageTextVideo/ImageTextVideoMobileHeader.vue'
|
import MobileHeader from './components/ImageTextVideo/ImageTextVideoMobileHeader.vue'
|
||||||
@ -54,6 +56,7 @@ import ChapterList from './components/ImageTextVideo/ImageTextVideoTaskList.vue'
|
|||||||
import EmptyState from './components/ImageTextVideo/ImageTextVideoEmptyState.vue'
|
import EmptyState from './components/ImageTextVideo/ImageTextVideoEmptyState.vue'
|
||||||
|
|
||||||
import { useReverseManageStore } from '@/stores/reverseManage'
|
import { useReverseManageStore } from '@/stores/reverseManage'
|
||||||
|
import { isEmpty } from 'lodash'
|
||||||
const reverseManageStore = useReverseManageStore()
|
const reverseManageStore = useReverseManageStore()
|
||||||
|
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
@ -61,16 +64,48 @@ const message = useMessage()
|
|||||||
// 选中的小说ID
|
// 选中的小说ID
|
||||||
const selectedNovelId = ref('novel-1')
|
const selectedNovelId = ref('novel-1')
|
||||||
|
|
||||||
|
// 强制重新渲染的key
|
||||||
|
const forceRenderKey = ref(0)
|
||||||
|
|
||||||
// 移动端侧边栏状态
|
// 移动端侧边栏状态
|
||||||
const isMobileSidebarOpen = ref(false)
|
const isMobileSidebarOpen = ref(false)
|
||||||
const bookInfoList = ref([])
|
const bookInfoList = ref([])
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
// 初始化小说数据
|
// 初始化小说数据
|
||||||
|
await refreshData()
|
||||||
|
handleSelectNovel()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 刷新数据函数
|
||||||
|
async function refreshData() {
|
||||||
|
try {
|
||||||
let res = await window.book.video.GetVideoBookInfoList({})
|
let res = await window.book.video.GetVideoBookInfoList({})
|
||||||
console.log('获取小说数据', res)
|
console.log('获取小说数据', res)
|
||||||
bookInfoList.value = res.data
|
bookInfoList.value = res.data
|
||||||
})
|
debugger
|
||||||
|
|
||||||
|
// 刷新数据后,重新设置当前选中的小说以触发响应式更新
|
||||||
|
if (selectedNovelId.value && !isEmpty(selectedNovelId.value)) {
|
||||||
|
// 如果当前有选中的小说,重新设置以更新数据
|
||||||
|
const currentId = selectedNovelId.value
|
||||||
|
handleSelectNovel(currentId)
|
||||||
|
} else {
|
||||||
|
// 如果没有选中的小说,也要更新 store 中的数据
|
||||||
|
const selectedBook = bookInfoList.value.find((book) => book.id === selectedNovelId.value)
|
||||||
|
if (selectedBook) {
|
||||||
|
reverseManageStore.selectBook = selectedBook
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 强制重新渲染整个组件
|
||||||
|
forceRenderKey.value++
|
||||||
|
console.log('强制重新渲染,key:', forceRenderKey.value)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('刷新数据失败:', error)
|
||||||
|
message.error('刷新数据失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 选中的小说
|
// 选中的小说
|
||||||
const selectedNovel = computed(() => {
|
const selectedNovel = computed(() => {
|
||||||
@ -79,9 +114,24 @@ const selectedNovel = computed(() => {
|
|||||||
|
|
||||||
// 处理选择小说
|
// 处理选择小说
|
||||||
function handleSelectNovel(novelId) {
|
function handleSelectNovel(novelId) {
|
||||||
|
if (isEmpty(novelId)) {
|
||||||
|
novelId = reverseManageStore.selectBook?.id || ''
|
||||||
|
}
|
||||||
|
if (isEmpty(novelId)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
selectedNovelId.value = novelId
|
selectedNovelId.value = novelId
|
||||||
reverseManageStore.selectBook = bookInfoList.value.find((book) => book.id === novelId)
|
const selectedBook = bookInfoList.value.find((book) => book.id === novelId)
|
||||||
|
if (selectedBook) {
|
||||||
|
reverseManageStore.selectBook = selectedBook
|
||||||
console.log('选择小说:', novelId)
|
console.log('选择小说:', novelId)
|
||||||
|
|
||||||
|
// 强制重新渲染以确保UI更新
|
||||||
|
forceRenderKey.value++
|
||||||
|
console.log('选择小说后强制重新渲染,key:', forceRenderKey.value)
|
||||||
|
} else {
|
||||||
|
console.warn('未找到对应的小说数据:', novelId)
|
||||||
|
}
|
||||||
// 移动端选择后关闭侧边栏
|
// 移动端选择后关闭侧边栏
|
||||||
closeMobileSidebar()
|
closeMobileSidebar()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,14 +6,18 @@
|
|||||||
<!-- 左侧表格区域 -->
|
<!-- 左侧表格区域 -->
|
||||||
<n-layout-content class="left-panel">
|
<n-layout-content class="left-panel">
|
||||||
<image-text-video-info-task-list
|
<image-text-video-info-task-list
|
||||||
:table-data="tableData"
|
v-if="!loading && reverseManageStore.selectBookTaskDetail"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
:show-right-panel="showRightPanel"
|
:show-right-panel="showRightPanel"
|
||||||
|
:show-pagination="showPagination"
|
||||||
@view-detail="handleViewDetail"
|
@view-detail="handleViewDetail"
|
||||||
@toggle-status="handleToggleStatus"
|
|
||||||
@add-task="handleAdd"
|
|
||||||
@toggle-right-panel="handleToggleRightPanel"
|
@toggle-right-panel="handleToggleRightPanel"
|
||||||
/>
|
/>
|
||||||
|
<div v-else class="loading-container">
|
||||||
|
<n-spin size="large">
|
||||||
|
<template #description> 正在加载任务数据... </template>
|
||||||
|
</n-spin>
|
||||||
|
</div>
|
||||||
</n-layout-content>
|
</n-layout-content>
|
||||||
|
|
||||||
<!-- 右侧详细操作区域 -->
|
<!-- 右侧详细操作区域 -->
|
||||||
@ -34,7 +38,6 @@
|
|||||||
<image-text-video-info-task-detail
|
<image-text-video-info-task-detail
|
||||||
v-else
|
v-else
|
||||||
:task="selectedTask"
|
:task="selectedTask"
|
||||||
@delete-task="handleDeleteTask"
|
|
||||||
@edit-task="handleEditTask"
|
@edit-task="handleEditTask"
|
||||||
@config-change="handleConfigChange"
|
@config-change="handleConfigChange"
|
||||||
/>
|
/>
|
||||||
@ -59,9 +62,7 @@
|
|||||||
<image-text-video-info-task-detail
|
<image-text-video-info-task-detail
|
||||||
v-else
|
v-else
|
||||||
:task="selectedTask"
|
:task="selectedTask"
|
||||||
@delete-task="handleDeleteTask"
|
|
||||||
@edit-task="handleEditTask"
|
@edit-task="handleEditTask"
|
||||||
@config-change="handleConfigChange"
|
|
||||||
/>
|
/>
|
||||||
</n-drawer-content>
|
</n-drawer-content>
|
||||||
</n-drawer>
|
</n-drawer>
|
||||||
@ -69,13 +70,14 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted, onUnmounted } from 'vue'
|
||||||
import {
|
import {
|
||||||
NLayout,
|
NLayout,
|
||||||
NLayoutSider,
|
NLayoutSider,
|
||||||
NLayoutContent,
|
NLayoutContent,
|
||||||
NDrawer,
|
NDrawer,
|
||||||
NDrawerContent,
|
NDrawerContent,
|
||||||
|
NSpin,
|
||||||
useMessage
|
useMessage
|
||||||
} from 'naive-ui'
|
} from 'naive-ui'
|
||||||
import ImageTextVideoInfoTaskList from './components/ImageTextVideoInfo/ImageTextVideoInfoTaskList.vue'
|
import ImageTextVideoInfoTaskList from './components/ImageTextVideoInfo/ImageTextVideoInfoTaskList.vue'
|
||||||
@ -83,14 +85,16 @@ import ImageTextVideoInfoTaskDetail from './components/ImageTextVideoInfo/ImageT
|
|||||||
import ImageTextVideoInfoEmptyState from './components/ImageTextVideoInfo/ImageTextVideoInfoEmptyState.vue'
|
import ImageTextVideoInfoEmptyState from './components/ImageTextVideoInfo/ImageTextVideoInfoEmptyState.vue'
|
||||||
|
|
||||||
import { useReverseManageStore } from '@/stores/reverseManage'
|
import { useReverseManageStore } from '@/stores/reverseManage'
|
||||||
|
import { OptionKeyName } from '@/define/enum/option'
|
||||||
|
import { optionSerialization } from '@/main/Service/Options/optionSerialization'
|
||||||
|
import { DEFINE_STRING } from '@/define/define_string'
|
||||||
|
import { ResponseMessageType } from '@/define/enum/softwareEnum'
|
||||||
|
import { VideoStatus } from '@/define/enum/video'
|
||||||
|
|
||||||
const reverseManageStore = useReverseManageStore()
|
const reverseManageStore = useReverseManageStore()
|
||||||
|
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
|
|
||||||
// 表格数据
|
|
||||||
const tableData = ref([])
|
|
||||||
|
|
||||||
// 加载状态
|
// 加载状态
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
|
||||||
@ -100,32 +104,165 @@ const selectedTask = ref(null)
|
|||||||
// 右侧面板显示控制
|
// 右侧面板显示控制
|
||||||
const showRightPanel = ref(false)
|
const showRightPanel = ref(false)
|
||||||
|
|
||||||
|
// 是否显示分页
|
||||||
|
const showPagination = ref(true)
|
||||||
|
|
||||||
// 抽屉显示控制
|
// 抽屉显示控制
|
||||||
const showDrawer = ref(false)
|
const showDrawer = ref(false)
|
||||||
|
|
||||||
async function handleInitialize() {
|
async function handleInitialize() {
|
||||||
// 模拟加载数据
|
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
// 这边开始加载数据
|
console.log('初始化 ImageTextVideoHome 组件')
|
||||||
|
|
||||||
|
// 检查必要的 store 数据
|
||||||
|
if (!reverseManageStore.selectBookTask?.id) {
|
||||||
|
console.error('未找到选中的任务,无法继续初始化')
|
||||||
|
message.error('未选择任务,请返回重新选择')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('当前选中任务:', reverseManageStore.selectBookTask)
|
||||||
|
|
||||||
|
// 加载 option 设置 中的是不是显示右侧面板
|
||||||
|
let showRightPanelOptionRes = await window.options.GetOptionByKey(
|
||||||
|
OptionKeyName.ImageToVideo_ShowRightPanel
|
||||||
|
)
|
||||||
|
if (showRightPanelOptionRes.code != 1) {
|
||||||
|
message.error(showRightPanelOptionRes.message || '加载设置失败')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
showRightPanel.value = optionSerialization(showRightPanelOptionRes.data)
|
||||||
|
|
||||||
|
// 加载 option 设置 中的是不是显示分页
|
||||||
|
let showPaginationOptionRes = await window.options.GetOptionByKey(
|
||||||
|
OptionKeyName.ImageToVideo_ShowPagination
|
||||||
|
)
|
||||||
|
if (showPaginationOptionRes.code != 1) {
|
||||||
|
message.error(showPaginationOptionRes.message || '加载设置失败')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
showPagination.value = optionSerialization(showPaginationOptionRes.data)
|
||||||
|
|
||||||
|
// 加载表格使用数据
|
||||||
|
console.log('开始加载任务详情数据...')
|
||||||
let res = await reverseManageStore.GetBookTaskDetail(reverseManageStore.selectBookTask.id)
|
let res = await reverseManageStore.GetBookTaskDetail(reverseManageStore.selectBookTask.id)
|
||||||
console.log('获取任务数据', res)
|
console.log('获取任务数据', res)
|
||||||
if (res.code != 1) {
|
if (res.code != 1) {
|
||||||
message.error(res.message)
|
message.error(res.message)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
tableData.value = res.data
|
reverseManageStore.selectBookTaskDetail = res.data
|
||||||
|
console.log('任务详情数据加载完成:', res.data)
|
||||||
|
|
||||||
|
await initVideoMessage(res.data)
|
||||||
|
console.log('视频消息初始化完成')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error('加载数据失败')
|
console.error('加载数据失败:', error)
|
||||||
|
message.error('加载数据失败: ' + error.message)
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
// 检查所有的当前的分镜 判断 是不是又 videoMessage,没有的话 创建初始的
|
||||||
handleInitialize()
|
async function initVideoMessage(bookTaskDetail) {
|
||||||
|
if (bookTaskDetail.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for (const task of bookTaskDetail) {
|
||||||
|
if (!task.videoMessage) {
|
||||||
|
// 初始化 videoMessage
|
||||||
|
let initRes = await window.book.video.InitVideoMessage(task.id)
|
||||||
|
if (initRes.code !== 1) {
|
||||||
|
message.error(`初始化任务 ${task.name} 的视频信息失败: ${initRes.message}`)
|
||||||
|
} else {
|
||||||
|
task.videoMessage = initRes.data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
console.log('ImageTextVideoInfoHome onMounted 开始执行')
|
||||||
|
try {
|
||||||
|
await handleInitialize()
|
||||||
|
handleIpcTaskListChange()
|
||||||
|
console.log('ImageTextVideoInfoHome onMounted 执行完成')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('ImageTextVideoInfoHome onMounted 执行失败:', error)
|
||||||
|
message.error('组件初始化失败: ' + error.message)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
// 清理事件监听
|
||||||
|
window.api.removeEventListen([DEFINE_STRING.BOOK.MJ_VIDEO_TO_VIDEO_RETURN])
|
||||||
|
})
|
||||||
|
|
||||||
|
function handleIpcTaskListChange() {
|
||||||
|
// 监听SD出图返回的数据
|
||||||
|
window.api.setEventListen([DEFINE_STRING.BOOK.MJ_VIDEO_TO_VIDEO_RETURN], (value) => {
|
||||||
|
try {
|
||||||
|
if (value.type == ResponseMessageType.MJ_VIDEO) {
|
||||||
|
// 正在执行中
|
||||||
|
let videoMessage = JSON.parse(value.data)
|
||||||
|
console.log('收到 mj video视频处理进度', videoMessage)
|
||||||
|
let findIndex = reverseManageStore.selectBookTaskDetail.findIndex(
|
||||||
|
(item) => item.id === value.id
|
||||||
|
)
|
||||||
|
if (findIndex !== -1) {
|
||||||
|
reverseManageStore.selectBookTaskDetail[findIndex].videoMessage.status =
|
||||||
|
videoMessage.status
|
||||||
|
reverseManageStore.selectBookTaskDetail[findIndex].videoMessage.taskId =
|
||||||
|
videoMessage.taskId
|
||||||
|
reverseManageStore.selectBookTaskDetail[findIndex].videoMessage.msg = videoMessage.msg
|
||||||
|
reverseManageStore.selectBookTaskDetail[findIndex].videoMessage.messageData =
|
||||||
|
videoMessage.messageData
|
||||||
|
}
|
||||||
|
} else if (value.type == ResponseMessageType.MJ_VIDEO_EXTEND) {
|
||||||
|
// 正在执行中
|
||||||
|
let videoMessage = JSON.parse(value.data)
|
||||||
|
console.log('收到 mj video extend 视频处理进度', videoMessage)
|
||||||
|
let findIndex = reverseManageStore.selectBookTaskDetail.findIndex(
|
||||||
|
(item) => item.id === value.id
|
||||||
|
)
|
||||||
|
if (findIndex !== -1) {
|
||||||
|
reverseManageStore.selectBookTaskDetail[findIndex].videoMessage.status =
|
||||||
|
videoMessage.status
|
||||||
|
reverseManageStore.selectBookTaskDetail[findIndex].videoMessage.taskId =
|
||||||
|
videoMessage.taskId
|
||||||
|
reverseManageStore.selectBookTaskDetail[findIndex].videoMessage.msg = videoMessage.msg
|
||||||
|
reverseManageStore.selectBookTaskDetail[findIndex].videoMessage.messageData =
|
||||||
|
videoMessage.messageData
|
||||||
|
}
|
||||||
|
} else if (value.type == ResponseMessageType.VIDEO_SUCESS) {
|
||||||
|
// 执行返回 返回全部的最新数据
|
||||||
|
let bookTaskDetail = JSON.parse(value.data)
|
||||||
|
console.log('视频处理完成', bookTaskDetail)
|
||||||
|
let findIndex = reverseManageStore.selectBookTaskDetail.findIndex(
|
||||||
|
(item) => item.id === bookTaskDetail.id
|
||||||
|
)
|
||||||
|
if (findIndex !== -1) {
|
||||||
|
reverseManageStore.selectBookTaskDetail[findIndex] = bookTaskDetail
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error('未知的返回类型', value.type, value)
|
||||||
|
let findIndex = reverseManageStore.selectBookTaskDetail.findIndex(
|
||||||
|
(item) => item.id === value.id
|
||||||
|
)
|
||||||
|
if (findIndex != -1) {
|
||||||
|
reverseManageStore.selectBookTaskDetail[findIndex].videoMessage.status = VideoStatus.FAIL
|
||||||
|
reverseManageStore.selectBookTaskDetail[findIndex].videoMessage.msg = value.message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('处理IPC数据时发生错误:', error)
|
||||||
|
message.error('处理数据时发生错误,' + error.message)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// 查看详情
|
// 查看详情
|
||||||
function handleViewDetail(row) {
|
function handleViewDetail(row) {
|
||||||
selectedTask.value = { ...row }
|
selectedTask.value = { ...row }
|
||||||
@ -147,55 +284,6 @@ function handleToggleRightPanel(show) {
|
|||||||
showDrawer.value = false
|
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>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@ -249,6 +337,14 @@ function handleConfigChange(task) {
|
|||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.loading-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
/* 响应式设计 */
|
/* 响应式设计 */
|
||||||
@media (max-width: 1400px) {
|
@media (max-width: 1400px) {
|
||||||
.layout-wrapper {
|
.layout-wrapper {
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
<n-card
|
<n-card
|
||||||
class="bookTask-card"
|
class="bookTask-card"
|
||||||
hoverable
|
hoverable
|
||||||
@click="handleOpenBookTask"
|
@dblclick="handleOpenBookTask"
|
||||||
:header-style="{
|
:header-style="{
|
||||||
padding: '8px 16px'
|
padding: '8px 16px'
|
||||||
}"
|
}"
|
||||||
@ -15,7 +15,7 @@
|
|||||||
<n-space justify="space-between" align="center">
|
<n-space justify="space-between" align="center">
|
||||||
<n-text strong>{{ bookTask.name }}</n-text>
|
<n-text strong>{{ bookTask.name }}</n-text>
|
||||||
<n-dropdown :options="getChapterMenuOptions(bookTask)" @select="handleChapterAction">
|
<n-dropdown :options="getChapterMenuOptions(bookTask)" @select="handleChapterAction">
|
||||||
<n-button size="small" quaternary circle>
|
<n-button size="small" quaternary circle @click.stop>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<n-icon><EllipsisVerticalOutline /></n-icon>
|
<n-icon><EllipsisVerticalOutline /></n-icon>
|
||||||
</template>
|
</template>
|
||||||
@ -28,13 +28,7 @@
|
|||||||
<n-space vertical size="small">
|
<n-space vertical size="small">
|
||||||
<!-- 标签 -->
|
<!-- 标签 -->
|
||||||
<n-space size="small" wrap>
|
<n-space size="small" wrap>
|
||||||
<n-tag
|
<n-tag v-for="tag in bookTask.tags" :key="tag" size="small" type="primary">
|
||||||
v-for="tag in bookTask.tags"
|
|
||||||
:key="tag"
|
|
||||||
size="small"
|
|
||||||
type="primary"
|
|
||||||
@close="handleRemoveTag(bookTask.id, tag)"
|
|
||||||
>
|
|
||||||
{{ tag }}
|
{{ tag }}
|
||||||
</n-tag>
|
</n-tag>
|
||||||
</n-space>
|
</n-space>
|
||||||
@ -47,7 +41,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<n-space justify="space-between" style="margin-bottom: 4px">
|
<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">转视频进度</n-text>
|
||||||
<n-text depth="3" style="font-size: 12px">{{ progress }}%</n-text>
|
<n-text depth="3" style="font-size: 12px">{{ Math.floor(progress) }}%</n-text>
|
||||||
</n-space>
|
</n-space>
|
||||||
<n-progress
|
<n-progress
|
||||||
type="line"
|
type="line"
|
||||||
@ -78,16 +72,17 @@ import {
|
|||||||
} from 'naive-ui'
|
} from 'naive-ui'
|
||||||
import {
|
import {
|
||||||
EllipsisVerticalOutline,
|
EllipsisVerticalOutline,
|
||||||
AddOutline,
|
|
||||||
CreateOutline,
|
|
||||||
TrashOutline,
|
TrashOutline,
|
||||||
EyeOutline
|
EnterOutline,
|
||||||
|
CloseCircleOutline,
|
||||||
|
DocumentTextOutline
|
||||||
} from '@vicons/ionicons5'
|
} from '@vicons/ionicons5'
|
||||||
import { GetBookImageCategoryLabel } from '@/define/enum/bookEnum'
|
import { GetBookImageCategoryLabel, OperateBookType } from '@/define/enum/bookEnum'
|
||||||
import { GetImageToVideoModelsLabel } from '@/define/enum/video'
|
import { GetImageToVideoModelsLabel } from '@/define/enum/video'
|
||||||
import { FormatDate } from '@/renderer/src/common/time'
|
import { FormatDate } from '@/renderer/src/common/time'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { useReverseManageStore } from '@/stores/reverseManage'
|
import { useReverseManageStore } from '@/stores/reverseManage'
|
||||||
|
import ManageBookTaskGenerateInformation from '@/renderer/src/components/Book/Components/ManageBook/ManageBookTaskGenerateInformation.vue'
|
||||||
|
|
||||||
const reverseManageStore = useReverseManageStore()
|
const reverseManageStore = useReverseManageStore()
|
||||||
|
|
||||||
@ -100,6 +95,8 @@ let props = defineProps({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['refresh-data'])
|
||||||
|
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const bookTask = toRef(props, 'bookTask')
|
const bookTask = toRef(props, 'bookTask')
|
||||||
@ -130,60 +127,125 @@ function handleOpenBookTask() {
|
|||||||
console.log('当前URL:', window.location.href)
|
console.log('当前URL:', window.location.href)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理移除标签事件
|
// 处理删除关闭视频生成任务
|
||||||
function handleRemoveTag(bookTaskId, tag) {
|
async function handleCloseVideoGenerate() {
|
||||||
console.log('移除标签:', bookTaskId, tag)
|
dialog.warning({
|
||||||
message.info(`已移除标签: ${tag}`)
|
title: '确认关闭视频生成',
|
||||||
// 这里可以添加具体的移除标签逻辑
|
content: `确定要关闭任务 "${bookTask.value.name}" 的视频生成吗?此操作会讲当前图/文生视频的批次删除,不影响聚合推文。并且已有的视频不会被导出,是否继续操作!!`,
|
||||||
|
positiveText: '确认关闭',
|
||||||
|
negativeText: '取消',
|
||||||
|
onPositiveClick: async () => {
|
||||||
|
try {
|
||||||
|
let res = await window.db.UpdateBookTaskData(bookTask.value.id, {
|
||||||
|
openVideoGenerate: !bookTask.value.openVideoGenerate
|
||||||
|
})
|
||||||
|
if (res.code == 0) {
|
||||||
|
message.error(res.message)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
message.success(`已关闭任务 "${bookTask.value.name}" 的视频生成`)
|
||||||
|
// 可能需要更新任务状态或重新加载数据
|
||||||
|
emit('refresh-data', bookTask.value.id)
|
||||||
|
} catch (error) {
|
||||||
|
message.error(`关闭视频生成失败: ${error.message}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取章节菜单选项
|
// 处理导出剪映草稿
|
||||||
function getChapterMenuOptions(bookTask) {
|
async function handleExportDraft() {
|
||||||
return [
|
dialog.info({
|
||||||
{
|
closeOnEsc: false,
|
||||||
label: '阅读',
|
title: `生成草稿前检查 ${bookTask.value.name}`,
|
||||||
key: 'read',
|
maskClosable: false,
|
||||||
icon: () => h(NIcon, null, () => h(EyeOutline))
|
style: 'width: 800px; max-width: 90vw',
|
||||||
},
|
showIcon: false,
|
||||||
{
|
content: () =>
|
||||||
label: '编辑',
|
h(ManageBookTaskGenerateInformation, {
|
||||||
key: 'edit',
|
bookTask: bookTask.value,
|
||||||
icon: () => h(NIcon, null, () => h(CreateOutline))
|
type: 'bookTask',
|
||||||
},
|
optionType: 'draft'
|
||||||
{
|
})
|
||||||
type: 'divider'
|
})
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '删除',
|
|
||||||
key: 'delete',
|
|
||||||
icon: () => h(NIcon, null, () => h(TrashOutline))
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理章节操作
|
// 处理章节操作
|
||||||
function handleChapterAction(key, option) {
|
function handleChapterAction(key, option) {
|
||||||
console.log('章节操作:', key)
|
console.log('章节操作:', key)
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case 'read':
|
case 'open-info':
|
||||||
message.info('正在打开阅读界面...')
|
handleOpenBookTask()
|
||||||
break
|
break
|
||||||
case 'edit':
|
case 'export-draft':
|
||||||
message.info('正在打开编辑界面...')
|
handleExportDraft()
|
||||||
break
|
break
|
||||||
case 'delete':
|
case 'close-generate':
|
||||||
|
handleCloseVideoGenerate()
|
||||||
|
break
|
||||||
|
case 'delete-book-task':
|
||||||
dialog.warning({
|
dialog.warning({
|
||||||
title: '确认删除',
|
title: '确认删除',
|
||||||
content: '确定要删除这个章节吗?此操作不可恢复。',
|
content: `确定要删除任务 "${bookTask.value.name}" 吗?继续操作会删除此任务且会同步删除聚合推文中的批次任务,若只是删除视频生成任务,请使用“关闭视频生成”按钮,是否继续?`,
|
||||||
positiveText: '删除',
|
positiveText: '确认删除',
|
||||||
negativeText: '取消',
|
negativeText: '取消',
|
||||||
onPositiveClick: () => {
|
onPositiveClick: async () => {
|
||||||
message.success('章节已删除')
|
try {
|
||||||
|
let res = await window.book.DeleteBookTask(bookTask.value.id, OperateBookType.BOOKTASK)
|
||||||
|
console.log('删除任务结果:', res)
|
||||||
|
if (res.code == 1) {
|
||||||
|
message.success('任务已删除')
|
||||||
|
// 触发事件通知父组件重新加载数据
|
||||||
|
emit('refresh-data', bookTask.value.id)
|
||||||
|
} else {
|
||||||
|
message.error(res.message)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
message.error(`删除任务失败: ${error.message}`)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onNegativeClick: () => {
|
||||||
|
message.info('已取消删除操作')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取章节菜单选项
|
||||||
|
function getChapterMenuOptions(bookTask) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: '进入主界面',
|
||||||
|
key: 'open-info',
|
||||||
|
icon: () => h(NIcon, null, () => h(EnterOutline))
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '导出剪映草稿',
|
||||||
|
key: 'export-draft',
|
||||||
|
icon: () => h(NIcon, null, () => h(DocumentTextOutline))
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '关闭视频生成',
|
||||||
|
key: 'close-generate',
|
||||||
|
icon: () =>
|
||||||
|
h('div', { style: 'display: flex; align-items: center;' }, [
|
||||||
|
h(NIcon, null, () => h(CloseCircleOutline))
|
||||||
|
])
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'divider'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: () => h('span', { style: 'color: #e74c3c' }, '删除任务'),
|
||||||
|
key: 'delete-book-task',
|
||||||
|
icon: () =>
|
||||||
|
h('div', { style: 'color: #e74c3c; display: flex; align-items: center;' }, [
|
||||||
|
h(NIcon, null, () => h(TrashOutline))
|
||||||
|
])
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@ -1,12 +1,22 @@
|
|||||||
<template>
|
<template>
|
||||||
<n-card title="批次目录" :bordered="false" :style="{ minWidth: '380px' }">
|
<n-card :bordered="false" :style="{ minWidth: '380px' }">
|
||||||
|
<template #header>
|
||||||
|
<n-tooltip :show="needTooltip ? undefined : false" trigger="hover" placement="top">
|
||||||
|
<template #trigger>
|
||||||
|
<span>{{ title }}</span>
|
||||||
|
</template>
|
||||||
|
<template v-if="needTooltip">
|
||||||
|
{{ fullTitle }}
|
||||||
|
</template>
|
||||||
|
</n-tooltip>
|
||||||
|
</template>
|
||||||
<template #header-extra>
|
<template #header-extra>
|
||||||
<n-space align="center">
|
<n-space align="center">
|
||||||
<n-text depth="3">共 {{ selectedNovel.bookTasks.length }} 个视频批次</n-text>
|
<n-text depth="3">共 {{ selectedNovel.bookTasks.length }} 个视频批次</n-text>
|
||||||
<n-divider vertical />
|
<n-divider vertical />
|
||||||
<n-tooltip trigger="hover">
|
<n-tooltip trigger="hover">
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<n-button text type="primary" size="small">
|
<n-button text type="primary" size="small" @click="addNewVideoBatch">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<n-icon><AddOutline /></n-icon>
|
<n-icon><AddOutline /></n-icon>
|
||||||
</template>
|
</template>
|
||||||
@ -20,28 +30,17 @@
|
|||||||
|
|
||||||
<div class="task-grid">
|
<div class="task-grid">
|
||||||
<div v-for="bookTask in selectedBook.bookTasks" :key="bookTask.id" class="task-item">
|
<div v-for="bookTask in selectedBook.bookTasks" :key="bookTask.id" class="task-item">
|
||||||
<ChapterCard :bookTask="bookTask" />
|
<ChapterCard :bookTask="bookTask" @refresh-data="handleTaskDeleted" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</n-card>
|
</n-card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import {
|
import { NCard, NSpace, NText, NDivider, NButton, NTooltip, NIcon } from 'naive-ui'
|
||||||
NCard,
|
|
||||||
NSpace,
|
|
||||||
NText,
|
|
||||||
NDivider,
|
|
||||||
NGrid,
|
|
||||||
NGridItem,
|
|
||||||
NButton,
|
|
||||||
NTooltip,
|
|
||||||
NIcon
|
|
||||||
} from 'naive-ui'
|
|
||||||
import ChapterCard from './ImageTextVideoTaskCard.vue'
|
import ChapterCard from './ImageTextVideoTaskCard.vue'
|
||||||
import { AddOutline } from '@vicons/ionicons5'
|
import { AddOutline } from '@vicons/ionicons5'
|
||||||
import { TimeDelay } from '@/define/Tools/time'
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
selectedNovel: {
|
selectedNovel: {
|
||||||
@ -50,8 +49,37 @@ const props = defineProps({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['refresh-data'])
|
||||||
|
|
||||||
const selectedBook = ref(props.selectedNovel)
|
const selectedBook = ref(props.selectedNovel)
|
||||||
|
|
||||||
|
// 处理任务删除事件
|
||||||
|
function handleTaskDeleted(taskId) {
|
||||||
|
console.log('任务已删除:', taskId)
|
||||||
|
// 向上传递事件到 ImageTextVideoHome
|
||||||
|
emit('refresh-data')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 完整标题(用于tooltip显示)
|
||||||
|
const fullTitle = computed(() => {
|
||||||
|
return `视频批次列表 - ${selectedBook.value.name}`
|
||||||
|
})
|
||||||
|
|
||||||
|
// 截断标题(用于card显示)
|
||||||
|
const title = computed(() => {
|
||||||
|
const bookName = selectedBook.value.name
|
||||||
|
const maxLength = 12 // 设置最大显示长度
|
||||||
|
const truncatedName =
|
||||||
|
bookName.length > maxLength ? bookName.substring(0, maxLength) + '...' : bookName
|
||||||
|
return `视频批次列表 - ${truncatedName}`
|
||||||
|
})
|
||||||
|
|
||||||
|
// 判断是否需要显示tooltip
|
||||||
|
const needTooltip = computed(() => {
|
||||||
|
return selectedBook.value.name.length > 12
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取批次进度
|
||||||
async function progressHandle() {
|
async function progressHandle() {
|
||||||
// 这里可以添加一些处理逻辑
|
// 这里可以添加一些处理逻辑
|
||||||
let res = await window.book.video.GetBookImageAndVideoProgress(props.selectedNovel.id, null)
|
let res = await window.book.video.GetBookImageAndVideoProgress(props.selectedNovel.id, null)
|
||||||
|
|||||||
@ -1,34 +1,84 @@
|
|||||||
<template>
|
<template>
|
||||||
<n-card title="基本信息" class="info-card">
|
<n-card class="info-card" size="small">
|
||||||
<n-descriptions :column="2" label-placement="left" class="descriptions">
|
<template #header>
|
||||||
<n-descriptions-item label="任务名称">
|
<div class="card-header">
|
||||||
{{ task.name }}
|
<n-icon size="20" class="header-icon">
|
||||||
</n-descriptions-item>
|
<svg viewBox="0 0 24 24">
|
||||||
<n-descriptions-item label="任务状态">
|
<path
|
||||||
<n-tag :type="getStatusType(task.status)">
|
fill="currentColor"
|
||||||
{{ task.status }}
|
d="M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M18,20H6V4H13V9H18V20Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</n-icon>
|
||||||
|
<span class="header-title">基本信息</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="info-container">
|
||||||
|
<div class="info-grid">
|
||||||
|
<div class="info-item">
|
||||||
|
<div class="info-label">
|
||||||
|
<n-icon size="16" class="label-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>
|
||||||
|
<span>任务名称</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-value">
|
||||||
|
<n-text strong>{{ task.name }}</n-text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-item">
|
||||||
|
<div class="info-label">
|
||||||
|
<n-icon size="16" class="label-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>
|
||||||
|
<span>任务状态</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-value">
|
||||||
|
<n-tag
|
||||||
|
:type="task.videoMessage?.status == 'error' ? 'error' : 'primary'"
|
||||||
|
size="small"
|
||||||
|
round
|
||||||
|
>
|
||||||
|
{{ task.videoMessage?.status }}
|
||||||
</n-tag>
|
</n-tag>
|
||||||
</n-descriptions-item>
|
</div>
|
||||||
<n-descriptions-item label="创建时间">
|
</div>
|
||||||
{{ formatDate(task.createTime) }}
|
|
||||||
</n-descriptions-item>
|
<div class="info-item description-item" v-if="task.videoMessage?.msg">
|
||||||
<n-descriptions-item label="更新时间">
|
<div class="info-label">
|
||||||
{{ formatDate(task.updateTime) }}
|
<n-icon size="16" class="label-icon">
|
||||||
</n-descriptions-item>
|
<svg viewBox="0 0 24 24">
|
||||||
<n-descriptions-item label="任务描述" :span="2">
|
<path
|
||||||
{{ task.description || '暂无描述' }}
|
fill="currentColor"
|
||||||
</n-descriptions-item>
|
d="M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M18,20H6V4H13V9H18V20M16,11H8V13H16V11M16,15H8V17H16V15Z"
|
||||||
</n-descriptions>
|
/>
|
||||||
|
</svg>
|
||||||
|
</n-icon>
|
||||||
|
<span>任务描述</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-value description-value">
|
||||||
|
<n-text>{{ task.videoMessage?.msg }}</n-text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</n-card>
|
</n-card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import {
|
import { NCard, NTag, NIcon, NText } from 'naive-ui'
|
||||||
NCard,
|
|
||||||
NDescriptions,
|
|
||||||
NDescriptionsItem,
|
|
||||||
NTag
|
|
||||||
} from 'naive-ui'
|
|
||||||
|
|
||||||
// 定义 props
|
// 定义 props
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@ -37,36 +87,144 @@ const props = defineProps({
|
|||||||
required: true
|
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>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.info-card {
|
.info-card {
|
||||||
margin-bottom: 16px;
|
margin-bottom: 12px;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-card:hover {
|
.info-card:hover {
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.1);
|
||||||
|
transform: translateY(-1px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.descriptions :deep(.n-descriptions-item-label) {
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-icon {
|
||||||
|
color: #18a058;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-title {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 16px;
|
||||||
|
color: var(--n-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-container {
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 12px;
|
||||||
|
background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #e8e8e8;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 3px;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(to bottom, #18a058, #36ad6a);
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 3px 12px rgba(24, 160, 88, 0.12);
|
||||||
|
border-color: #18a058;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item:hover::before {
|
||||||
|
width: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description-item {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-icon {
|
||||||
|
color: #18a058;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-value {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 24px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description-value {
|
||||||
|
align-items: flex-start;
|
||||||
|
line-height: 1.6;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.info-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-title {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 深色模式适配 */
|
||||||
|
.info-card :deep(.n-card) {
|
||||||
|
background: var(--n-card-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item {
|
||||||
|
background: var(--n-body-color);
|
||||||
|
border-color: var(--n-border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 状态标签动画 */
|
||||||
|
.info-item :deep(.n-tag) {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item:hover :deep(.n-tag) {
|
||||||
|
transform: scale(1.05);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,46 +1,252 @@
|
|||||||
<template>
|
<template>
|
||||||
<n-card title="参数配置" class="info-card">
|
<n-card class="video-info-card" size="small">
|
||||||
<n-form :model="task.config" label-placement="left" label-width="120px">
|
<template #header>
|
||||||
<n-form-item label="输出格式">
|
<div class="card-header">
|
||||||
|
<n-icon size="20" class="header-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>
|
||||||
|
<span class="header-title">{{ getTypeTitle() }}</span>
|
||||||
<n-select
|
<n-select
|
||||||
v-model:value="task.config.outputFormat"
|
v-model:value="selectedVideoType"
|
||||||
:options="outputFormatOptions"
|
:options="GetImageToVideoModelsOptions()"
|
||||||
@update:value="handleConfigChange"
|
size="small"
|
||||||
|
style="width: 120px"
|
||||||
|
@update:value="handleVideoTypeChange"
|
||||||
/>
|
/>
|
||||||
</n-form-item>
|
</div>
|
||||||
<n-form-item label="质量设置">
|
</template>
|
||||||
<n-slider
|
|
||||||
v-model:value="task.config.quality"
|
<!-- Runway 类型 -->
|
||||||
:min="1"
|
<div v-if="selectedVideoType == 'RUNWAY'" class="info-content">
|
||||||
:max="10"
|
<div class="info-section">
|
||||||
:step="1"
|
<div class="info-item">
|
||||||
:marks="{ 1: '低', 5: '中', 10: '高' }"
|
<div class="info-label">
|
||||||
@update:value="handleConfigChange"
|
<n-icon size="16">
|
||||||
|
<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"
|
||||||
/>
|
/>
|
||||||
</n-form-item>
|
</svg>
|
||||||
<n-form-item label="自动处理">
|
</n-icon>
|
||||||
<n-switch
|
<span>模型</span>
|
||||||
v-model:value="task.config.autoProcess"
|
</div>
|
||||||
@update:value="handleConfigChange"
|
<div class="info-value">
|
||||||
|
<n-tag size="small" type="info">{{ runwayOptions?.model || 'gen-3' }}</n-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-item">
|
||||||
|
<div class="info-label">
|
||||||
|
<n-icon size="16">
|
||||||
|
<svg viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M12,2L13.09,8.26L22,9L13.09,9.74L12,16L10.91,9.74L2,9L10.91,8.26L12,2Z"
|
||||||
/>
|
/>
|
||||||
</n-form-item>
|
</svg>
|
||||||
</n-form>
|
</n-icon>
|
||||||
|
<span>时长</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-value">
|
||||||
|
<n-text>{{ runwayOptions?.options?.seconds || 5 }}秒</n-text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-item" v-if="runwayOptions?.style">
|
||||||
|
<div class="info-label">
|
||||||
|
<n-icon size="16">
|
||||||
|
<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>
|
||||||
|
<span>风格</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-value">
|
||||||
|
<n-text>{{ runwayOptions.style }}</n-text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Luma 类型 -->
|
||||||
|
<div v-else-if="selectedVideoType === 'LUMA'" class="info-content">
|
||||||
|
<div class="info-section">
|
||||||
|
<div class="info-item">
|
||||||
|
<div class="info-label">
|
||||||
|
<n-icon size="16">
|
||||||
|
<svg viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M19,3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5C21,3.89 20.1,3 19,3M19,5V19H5V5H19Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</n-icon>
|
||||||
|
<span>宽高比</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-value">
|
||||||
|
<n-tag size="small" type="info">{{ lumaOptions?.aspect_ratio || '16:9' }}</n-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-item">
|
||||||
|
<div class="info-label">
|
||||||
|
<n-icon size="16">
|
||||||
|
<svg viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M12,2L13.09,8.26L22,9L13.09,9.74L12,16L10.91,9.74L2,9L10.91,8.26L12,2Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</n-icon>
|
||||||
|
<span>扩展提示词</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-value">
|
||||||
|
<n-tag size="small" :type="lumaOptions?.expand_prompt ? 'success' : 'warning'">
|
||||||
|
{{ lumaOptions?.expand_prompt ? '是' : '否' }}
|
||||||
|
</n-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-item">
|
||||||
|
<div class="info-label">
|
||||||
|
<n-icon size="16">
|
||||||
|
<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>
|
||||||
|
<span>循环模式</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-value">
|
||||||
|
<n-tag size="small" :type="lumaOptions?.loop ? 'success' : 'default'">
|
||||||
|
{{ lumaOptions?.loop ? '开启' : '关闭' }}
|
||||||
|
</n-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Kling 类型 -->
|
||||||
|
<div v-else-if="selectedVideoType === 'KLING'" class="info-content">
|
||||||
|
<div class="info-section">
|
||||||
|
<div class="info-item">
|
||||||
|
<div class="info-label">
|
||||||
|
<n-icon size="16">
|
||||||
|
<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>
|
||||||
|
<span>模型</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-value">
|
||||||
|
<n-tag size="small" type="info">{{ klingOptions?.model || 'kling-v1' }}</n-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-item">
|
||||||
|
<div class="info-label">
|
||||||
|
<n-icon size="16">
|
||||||
|
<svg viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M12,2L13.09,8.26L22,9L13.09,9.74L12,16L10.91,9.74L2,9L10.91,8.26L12,2Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</n-icon>
|
||||||
|
<span>模式</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-value">
|
||||||
|
<n-tag size="small" :type="klingOptions?.mode === 'pro' ? 'success' : 'info'">
|
||||||
|
{{ klingOptions?.mode === 'pro' ? '高表现' : '高性能' }}
|
||||||
|
</n-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-item">
|
||||||
|
<div class="info-label">
|
||||||
|
<n-icon size="16">
|
||||||
|
<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>
|
||||||
|
<span>时长</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-value">
|
||||||
|
<n-text>{{ klingOptions?.duration || 5 }}秒</n-text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-item" v-if="klingOptions?.cfg_scale">
|
||||||
|
<div class="info-label">
|
||||||
|
<n-icon size="16">
|
||||||
|
<svg viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M19,3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5C21,3.89 20.1,3 19,3M19,5V19H5V5H19Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</n-icon>
|
||||||
|
<span>提示词相关性</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-value">
|
||||||
|
<n-text>{{ klingOptions.cfg_scale }}</n-text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Midjourney 类型 -->
|
||||||
|
<div v-else-if="selectedVideoType === 'MJ_VIDEO'" class="info-content">
|
||||||
|
<ImageTextVideoInfoMJVideoInfo :task="task" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 未知类型或无数据 -->
|
||||||
|
<div v-else class="info-content">
|
||||||
|
<div class="empty-state">
|
||||||
|
<n-icon size="48" depth="3">
|
||||||
|
<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 depth="3">暂无视频配置信息</n-text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</n-card>
|
</n-card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
|
import { NCard, NTag, NIcon, NText, NSelect, useMessage } from 'naive-ui'
|
||||||
import {
|
import {
|
||||||
NCard,
|
GetImageToVideoModelsLabel,
|
||||||
NForm,
|
GetImageToVideoModelsOptions,
|
||||||
NFormItem,
|
ImageToVideoModels
|
||||||
NSelect,
|
} from '@/define/enum/video'
|
||||||
NSlider,
|
import { useReverseManageStore } from '@/stores/reverseManage'
|
||||||
NSwitch
|
import ImageTextVideoInfoMJVideoInfo from './ImageTextVideoInfoMJVideo/ImageTextVideoInfoMJVideoInfo.vue'
|
||||||
} from 'naive-ui'
|
|
||||||
|
|
||||||
// 定义 emits
|
const message = useMessage()
|
||||||
const emit = defineEmits(['config-change'])
|
const reverseManageStore = useReverseManageStore()
|
||||||
|
|
||||||
// 定义 props
|
// 定义 props
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@ -50,27 +256,240 @@ const props = defineProps({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// 输出格式选项
|
// 定义 emits
|
||||||
const outputFormatOptions = [
|
const emit = defineEmits(['update-video-type'])
|
||||||
{ label: 'MP4', value: 'mp4' },
|
|
||||||
{ label: 'AVI', value: 'avi' },
|
|
||||||
{ label: 'MOV', value: 'mov' },
|
|
||||||
{ label: 'WMV', value: 'wmv' }
|
|
||||||
]
|
|
||||||
|
|
||||||
// 配置变更
|
const selectedVideoType = computed(() => {
|
||||||
function handleConfigChange() {
|
return props.task?.videoMessage?.videoType || ImageToVideoModels.MJ_VIDEO
|
||||||
emit('config-change')
|
})
|
||||||
|
|
||||||
|
// 当前选中的视频类型
|
||||||
|
// const selectedVideoType = ref(videoType.value)
|
||||||
|
|
||||||
|
// 处理视频类型变化
|
||||||
|
async function handleVideoTypeChange(value) {
|
||||||
|
try {
|
||||||
|
let res = await window.book.video.UpdateBookTaskDetailVideoMessage(props.task.id, {
|
||||||
|
videoType: value
|
||||||
|
})
|
||||||
|
if (res.code !== 1) {
|
||||||
|
message.error(`保存失败: ${res.message}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新本地数据,触发 selectedVideoType 的响应式更新
|
||||||
|
if (props.task.videoMessage) {
|
||||||
|
props.task.videoMessage.videoType = value
|
||||||
|
} else {
|
||||||
|
props.task.videoMessage = { videoType: value }
|
||||||
|
}
|
||||||
|
|
||||||
|
message.success(`视频类型已更改为:${GetImageToVideoModelsLabel(value)}`)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('更新视频类型失败:', error)
|
||||||
|
message.error('更新视频类型失败,请重试')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const videoMessage = computed(() => {
|
||||||
|
return props.task?.videoMessage || {}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 计算属性:解析不同类型的配置
|
||||||
|
const runwayOptions = computed(() => {
|
||||||
|
if (videoMessage.value?.runwayOptions) {
|
||||||
|
try {
|
||||||
|
return JSON.parse(videoMessage.value.runwayOptions)
|
||||||
|
} catch {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {}
|
||||||
|
})
|
||||||
|
|
||||||
|
const lumaOptions = computed(() => {
|
||||||
|
if (videoMessage.value?.lumaOptions) {
|
||||||
|
try {
|
||||||
|
return JSON.parse(videoMessage.value.lumaOptions)
|
||||||
|
} catch {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {}
|
||||||
|
})
|
||||||
|
|
||||||
|
const klingOptions = computed(() => {
|
||||||
|
if (videoMessage.value?.klingOptions) {
|
||||||
|
try {
|
||||||
|
return JSON.parse(videoMessage.value.klingOptions)
|
||||||
|
} catch {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {}
|
||||||
|
})
|
||||||
|
|
||||||
|
const mjVideoOptions = computed(() => {
|
||||||
|
if (videoMessage.value?.mjVideoOptions) {
|
||||||
|
try {
|
||||||
|
return JSON.parse(videoMessage.value.mjVideoOptions)
|
||||||
|
} catch {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取类型标题
|
||||||
|
function getTypeTitle() {
|
||||||
|
const titles = {
|
||||||
|
[ImageToVideoModels.RUNWAY]: 'Runway 视频配置',
|
||||||
|
[ImageToVideoModels.LUMA]: 'Luma 视频配置',
|
||||||
|
[ImageToVideoModels.KLING]: 'Kling 视频配置',
|
||||||
|
[ImageToVideoModels.MJ_VIDEO]: 'Midjourney 视频配置',
|
||||||
|
[ImageToVideoModels.PIKA]: 'PIKA 视频配置'
|
||||||
|
}
|
||||||
|
return titles[selectedVideoType.value] || '视频配置信息'
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.info-card {
|
.video-info-card {
|
||||||
margin-bottom: 16px;
|
margin-bottom: 12px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-info-card:hover {
|
||||||
|
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.1);
|
||||||
|
/* transform: translateY(-1px); */
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-icon {
|
||||||
|
color: #18a058;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-title {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 16px;
|
||||||
|
color: var(--n-text-color);
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-content {
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-section {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 12px;
|
||||||
|
background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #e8e8e8;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 3px;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(to bottom, #18a058, #36ad6a);
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 3px 12px rgba(24, 160, 88, 0.12);
|
||||||
|
border-color: #18a058;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item:hover::before {
|
||||||
|
width: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #606266;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-label n-icon {
|
||||||
|
color: #18a058;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-value {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 24px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 40px 20px;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.info-section {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-title {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 深色模式适配 */
|
||||||
|
.video-info-card :deep(.n-card) {
|
||||||
|
background: var(--n-card-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item {
|
||||||
|
background: var(--n-body-color);
|
||||||
|
border-color: var(--n-border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 标签动画 */
|
||||||
|
.info-item :deep(.n-tag) {
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-card:hover {
|
.info-item:hover :deep(.n-tag) {
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
transform: scale(1.05);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -0,0 +1,344 @@
|
|||||||
|
<template>
|
||||||
|
<n-space vertical :size="12">
|
||||||
|
<n-alert type="success" :show-icon="false" title="适配拓展说明">
|
||||||
|
<div>
|
||||||
|
视频生成时长从 5
|
||||||
|
秒开始,但并非仅限于此。视频制作完成后,您可以在当前界面为选定的适配进行延长!
|
||||||
|
</div>
|
||||||
|
<br />
|
||||||
|
<div>您可以随意将视频延长最多 4 倍,直至达到 21 秒(即可用的最大长度)。</div>
|
||||||
|
</n-alert>
|
||||||
|
<!-- 父任务ID -->
|
||||||
|
<n-form-item label="父任务ID" required :show-feedback="false">
|
||||||
|
<div
|
||||||
|
style="
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
width: 100%;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<n-input
|
||||||
|
v-model:value="videoMessage.mjVideoOptionsObject.taskId"
|
||||||
|
placeholder="请输入需要操作的视频任务ID"
|
||||||
|
size="small"
|
||||||
|
:disabled="loading"
|
||||||
|
@update-value="(value) => handleVideoMessageChange('taskId', value)"
|
||||||
|
style="flex: 1"
|
||||||
|
/>
|
||||||
|
<n-tooltip trigger="hover" placement="top" style="max-width: 300px">
|
||||||
|
<template #trigger>
|
||||||
|
<n-button size="small" quaternary circle>
|
||||||
|
<template #icon>
|
||||||
|
<n-icon>
|
||||||
|
<HelpCircleOutline />
|
||||||
|
</n-icon>
|
||||||
|
</template>
|
||||||
|
</n-button>
|
||||||
|
</template>
|
||||||
|
<div style="line-height: 1.5">
|
||||||
|
<div style="font-weight: bold; margin-bottom: 8px">父任务ID说明:</div>
|
||||||
|
<div>点击按钮可以选择当前分镜的父任务ID,转视频类别为 MJ_VIDEO 的任务。</div>
|
||||||
|
<div style="margin-top: 8px; font-weight: bold">
|
||||||
|
也可手动输入 MJ_VIDEO 的任务ID,手动输入时,视频索引也需手动数据!!
|
||||||
|
</div>
|
||||||
|
<div style="margin-top: 8px; color: #f56c6c">
|
||||||
|
注意:如果当前任务不是 MJ_VIDEO 类型,则无法选择父任务。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</n-tooltip>
|
||||||
|
<n-button
|
||||||
|
size="small"
|
||||||
|
type="primary"
|
||||||
|
:disabled="loading"
|
||||||
|
ghost
|
||||||
|
@click="handleSelectParentTask"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<n-icon size="16">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
viewBox="0 0 512 512"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M428.8 137.6h-86.177a115.52 115.52 0 0 0 2.176-22.4c0-47.914-35.072-83.2-92-83.2c-45.314 0-57.002 48.537-75.707 78.784c-7.735 12.413-16.994 23.317-25.851 33.253l-.131.146l-.129.148C135.662 161.807 127.764 168 120.8 168h-2.679c-5.747-4.952-13.536-8-22.12-8H32c-17.673 0-32 12.894-32 28.8v230.4C0 435.106 14.327 448 32 448h64c8.584 0 16.373-3.048 22.12-8h2.679c28.688 0 67.137 40 127.2 40h21.299c62.542 0 98.8-38.658 99.94-91.145c12.482-17.813 18.491-40.785 15.985-62.791A93.148 93.148 0 0 0 393.152 304H428.8c45.435 0 83.2-37.584 83.2-83.2c0-45.099-38.101-83.2-83.2-83.2zm0 118.4h-91.026c12.837 14.669 14.415 42.825-4.95 61.05c11.227 19.646 1.687 45.624-12.925 53.625c6.524 39.128-10.076 61.325-50.6 61.325H248c-45.491 0-77.21-35.913-120-39.676V215.571c25.239-2.964 42.966-21.222 59.075-39.596c11.275-12.65 21.725-25.3 30.799-39.875C232.355 112.712 244.006 80 252.8 80c23.375 0 44 8.8 44 35.2c0 35.2-26.4 53.075-26.4 70.4h158.4c18.425 0 35.2 16.5 35.2 35.2c0 18.975-16.225 35.2-35.2 35.2zM88 384c0 13.255-10.745 24-24 24s-24-10.745-24-24s10.745-24 24-24s24 10.745 24 24z"
|
||||||
|
fill="currentColor"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</n-icon>
|
||||||
|
</template>
|
||||||
|
选择父任务
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
</n-form-item>
|
||||||
|
|
||||||
|
<!-- 视频索引 -->
|
||||||
|
<n-form-item :show-label="false" :show-require-mark="false" :show-feedback="false">
|
||||||
|
<div class="motion-control">
|
||||||
|
<div class="motion-label">
|
||||||
|
<span>视频索引 (Index)</span>
|
||||||
|
|
||||||
|
<n-tooltip trigger="hover" placement="top" style="max-width: 300px">
|
||||||
|
<template #trigger>
|
||||||
|
<n-button size="small" quaternary circle>
|
||||||
|
<template #icon>
|
||||||
|
<n-icon>
|
||||||
|
<HelpCircleOutline />
|
||||||
|
</n-icon>
|
||||||
|
</template>
|
||||||
|
</n-button>
|
||||||
|
</template>
|
||||||
|
<div style="line-height: 1.5">
|
||||||
|
<div style="font-weight: bold; margin-bottom: 8px">视频索引说明:</div>
|
||||||
|
<div style="margin-bottom: 6px">选择要进行视频拓展的具体视频索引,范围为 0-3。</div>
|
||||||
|
<div style="margin-bottom: 6px"><strong>索引 0:</strong> 第一个生成的视频</div>
|
||||||
|
<div style="margin-bottom: 6px"><strong>索引 1:</strong> 第二个生成的视频</div>
|
||||||
|
<div style="margin-bottom: 6px"><strong>索引 2:</strong> 第三个生成的视频</div>
|
||||||
|
<div><strong>索引 3:</strong> 第四个生成的视频</div>
|
||||||
|
<br />
|
||||||
|
<div>选择对应的索引后,会对该索引的视频进行拓展操作。</div>
|
||||||
|
</div>
|
||||||
|
</n-tooltip>
|
||||||
|
</div>
|
||||||
|
<n-input-number
|
||||||
|
v-model:value="videoMessage.mjVideoOptionsObject.index"
|
||||||
|
:min="0"
|
||||||
|
:max="3"
|
||||||
|
placeholder="1-4"
|
||||||
|
size="small"
|
||||||
|
:disabled="loading"
|
||||||
|
style="width: 100px"
|
||||||
|
@update:value="(value) => handleVideoMessageChange('index', value)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</n-form-item>
|
||||||
|
|
||||||
|
<!-- 提示词 -->
|
||||||
|
<n-form-item label="提示词(可选,不填写由MJ自行处理)" :show-feedback="false">
|
||||||
|
<n-input
|
||||||
|
v-model:value="videoMessage.prompt"
|
||||||
|
type="textarea"
|
||||||
|
placeholder="请输入视频生成提示词"
|
||||||
|
size="small"
|
||||||
|
:autosize="{ minRows: 2, maxRows: 4 }"
|
||||||
|
:disabled="loading"
|
||||||
|
@change="(value) => handleVideoMessageChange('prompt', value)"
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
|
||||||
|
<!-- 运动变化 -->
|
||||||
|
<n-form-item :show-label="false" :show-require-mark="false" :show-feedback="false">
|
||||||
|
<div class="motion-control">
|
||||||
|
<div class="motion-label">
|
||||||
|
<span>运动变化 (Motion)</span>
|
||||||
|
|
||||||
|
<n-tooltip trigger="hover" placement="top" style="max-width: 300px">
|
||||||
|
<template #trigger>
|
||||||
|
<n-button size="small" quaternary circle>
|
||||||
|
<template #icon>
|
||||||
|
<n-icon>
|
||||||
|
<HelpCircleOutline />
|
||||||
|
</n-icon>
|
||||||
|
</template>
|
||||||
|
</n-button>
|
||||||
|
</template>
|
||||||
|
<div style="line-height: 1.5">
|
||||||
|
<div style="font-weight: bold; margin-bottom: 8px">运动变化程度说明:</div>
|
||||||
|
<div style="margin-bottom: 6px">
|
||||||
|
生成视频时,您有两个运动设置选项可供选择:"低运动"和"高运动"。请使用网站上的相应按钮,或在视频提示末尾添加
|
||||||
|
<code>--motion low</code> 或参数。 <code>--motion high</code>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-bottom: 6px">
|
||||||
|
<strong>低运动(默认):</strong
|
||||||
|
>更有可能产生静止场景、低摄像机运动、慢动作或细微的角色动作。
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong>高运动:</strong
|
||||||
|
>更有可能导致大的摄像机运动和更大的角色运动,但也可能产生不切实际或有故障的动作。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</n-tooltip>
|
||||||
|
</div>
|
||||||
|
<n-select
|
||||||
|
v-model:value="videoMessage.mjVideoOptionsObject.motion"
|
||||||
|
:options="GetMJVideoMotionOptions()"
|
||||||
|
placeholder="选择运动变化程度"
|
||||||
|
@update-value="(value) => handleVideoMessageChange('motion', value)"
|
||||||
|
size="small"
|
||||||
|
:disabled="loading"
|
||||||
|
class="motion-select"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</n-form-item>
|
||||||
|
|
||||||
|
<!-- 减少创意 -->
|
||||||
|
<n-form-item :show-label="false" :show-require-mark="false" :show-feedback="false">
|
||||||
|
<div class="motion-control">
|
||||||
|
<div class="motion-label">
|
||||||
|
<span>视频原始 (Raw)</span>
|
||||||
|
|
||||||
|
<n-tooltip trigger="hover" placement="top" style="max-width: 300px">
|
||||||
|
<template #trigger>
|
||||||
|
<n-button size="small" quaternary circle>
|
||||||
|
<template #icon>
|
||||||
|
<n-icon>
|
||||||
|
<HelpCircleOutline />
|
||||||
|
</n-icon>
|
||||||
|
</template>
|
||||||
|
</n-button>
|
||||||
|
</template>
|
||||||
|
<div style="line-height: 1.5">
|
||||||
|
<div style="font-weight: bold; margin-bottom: 8px">视频原始开关说明:</div>
|
||||||
|
<div style="margin-bottom: 6px">
|
||||||
|
为了更精确地控制视频创作的运动,使用
|
||||||
|
<code>--raw</code> 视频提示中的参数会很有帮助。此功能的作用类似于图像的Raw 模式。
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
当您添加 <code>--raw</code>,它会减少 Midjourney
|
||||||
|
通常添加的额外创意天赋,从而使您的提示文本对结果产生更大的影响。
|
||||||
|
</div>
|
||||||
|
<br />
|
||||||
|
<div style="color: red">注意:视频原始只有在有提示词的时候才会生效</div>
|
||||||
|
</div>
|
||||||
|
</n-tooltip>
|
||||||
|
</div>
|
||||||
|
<n-switch
|
||||||
|
v-model:value="videoMessage.mjVideoOptionsObject.raw"
|
||||||
|
size="small"
|
||||||
|
@update-value="(value) => handleVideoMessageChange('raw', value)"
|
||||||
|
:disabled="loading"
|
||||||
|
>
|
||||||
|
<template #checked>是</template>
|
||||||
|
<template #unchecked>否</template>
|
||||||
|
</n-switch>
|
||||||
|
</div>
|
||||||
|
</n-form-item>
|
||||||
|
|
||||||
|
<!-- 执行按钮 -->
|
||||||
|
<n-form-item>
|
||||||
|
<n-button type="primary" size="small" :loading="loading" @click="handleExtend" block>
|
||||||
|
<template #icon>
|
||||||
|
<n-icon>
|
||||||
|
<svg viewBox="0 0 24 24">
|
||||||
|
<path fill="currentColor" d="M8,5.14V19.14L19,12.14L8,5.14Z" />
|
||||||
|
</svg>
|
||||||
|
</n-icon>
|
||||||
|
</template>
|
||||||
|
执行视频拓展
|
||||||
|
</n-button>
|
||||||
|
</n-form-item>
|
||||||
|
</n-space>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import {
|
||||||
|
NIcon,
|
||||||
|
NInput,
|
||||||
|
NInputNumber,
|
||||||
|
NSelect,
|
||||||
|
NButton,
|
||||||
|
NSpace,
|
||||||
|
NFormItem,
|
||||||
|
NTooltip,
|
||||||
|
useMessage
|
||||||
|
} from 'naive-ui'
|
||||||
|
|
||||||
|
import { HelpCircleOutline } from '@vicons/ionicons5'
|
||||||
|
import { GetMJVideoMotionOptions, ImageToVideoModels } from '@/define/enum/video'
|
||||||
|
import { isEmpty } from 'lodash'
|
||||||
|
import { BookBackTaskType, TaskExecuteType } from '@/define/enum/bookEnum'
|
||||||
|
import { DEFINE_STRING } from '@/define/define_string'
|
||||||
|
|
||||||
|
const message = useMessage()
|
||||||
|
|
||||||
|
// 定义 props
|
||||||
|
const props = defineProps({
|
||||||
|
videoMessage: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
loading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
task: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 定义 emits
|
||||||
|
const emit = defineEmits(['video-message-change', 'extend', 'select-parent-task'])
|
||||||
|
|
||||||
|
// 修改 videoMessage 的通用函数
|
||||||
|
function handleVideoMessageChange(key, value = undefined) {
|
||||||
|
emit('video-message-change', key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理视频拓展
|
||||||
|
async function handleExtend() {
|
||||||
|
console.log('执行视频拓展', props.videoMessage, props.task)
|
||||||
|
|
||||||
|
let taskId = props.videoMessage.mjVideoOptionsObject.taskId
|
||||||
|
let videoIndex = props.videoMessage.mjVideoOptionsObject.index
|
||||||
|
|
||||||
|
if (isEmpty(taskId) || videoIndex == undefined) {
|
||||||
|
message.error('请先选择父任务ID和视频索引!')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开始添加任务
|
||||||
|
// 开始提交任务
|
||||||
|
let type = BookBackTaskType.MJ_VIDEO_EXTEND
|
||||||
|
|
||||||
|
// 添加任务
|
||||||
|
let res = await window.task.AddBookBackTask(
|
||||||
|
props.task.bookId,
|
||||||
|
type,
|
||||||
|
TaskExecuteType.AUTO,
|
||||||
|
props.task.bookTaskId,
|
||||||
|
props.task.id,
|
||||||
|
DEFINE_STRING.BOOK.MJ_VIDEO_TO_VIDEO_RETURN
|
||||||
|
)
|
||||||
|
if (res.code != 1) {
|
||||||
|
message.error(res.message)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
message.success('添加视频拓展任务成功!')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选择父任务的处理函数
|
||||||
|
function handleSelectParentTask() {
|
||||||
|
if (props.task.videoMessage?.videoType !== ImageToVideoModels.MJ_VIDEO) {
|
||||||
|
message.error('当前任务不是 MJ_VIDEO 类型,无法选择父任务!')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
emit('select-parent-task')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.motion-control {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.motion-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-self: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.motion-select {
|
||||||
|
width: 100px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,263 @@
|
|||||||
|
<template>
|
||||||
|
<n-space vertical :size="12">
|
||||||
|
<!-- 首帧图片 -->
|
||||||
|
<n-form-item label="图片链接(图片地址)" required :show-feedback="false">
|
||||||
|
<div class="input-with-preview">
|
||||||
|
<n-input
|
||||||
|
v-model:value="videoMessage.imageUrl"
|
||||||
|
placeholder="请输入图片链接"
|
||||||
|
@change="handleVideoMessageChange('imageUrl')"
|
||||||
|
size="small"
|
||||||
|
:disabled="loading"
|
||||||
|
class="image-input"
|
||||||
|
/>
|
||||||
|
<n-image
|
||||||
|
v-if="videoMessage.imageUrl"
|
||||||
|
:src="videoMessage.imageUrl"
|
||||||
|
:height="60"
|
||||||
|
:fallback-src="'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICA8cmVjdCB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgZmlsbD0iI2Y1ZjVmNSIvPgogIDx0ZXh0IHg9IjUwIiB5PSI1MCIgZm9udC1mYW1pbHk9IkFyaWFsLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjEyIiBmaWxsPSIjOTk5IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBkeT0iMC4zZW0iPuWbvueJh+WKoOi9veWksei0pTwvdGV4dD4KPC9zdmc+'"
|
||||||
|
object-fit="contain"
|
||||||
|
class="preview-image"
|
||||||
|
@error="handleImageError"
|
||||||
|
/>
|
||||||
|
<div v-else class="preview-placeholder">
|
||||||
|
<n-text depth="3" class="placeholder-text">图片预览</n-text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</n-form-item>
|
||||||
|
|
||||||
|
<!-- 提示词 -->
|
||||||
|
<n-form-item label="提示词(可选,不填写由MJ自行处理)" :show-feedback="false">
|
||||||
|
<n-input
|
||||||
|
v-model:value="videoMessage.prompt"
|
||||||
|
type="textarea"
|
||||||
|
placeholder="请输入视频生成提示词"
|
||||||
|
size="small"
|
||||||
|
:autosize="{ minRows: 2, maxRows: 4 }"
|
||||||
|
:disabled="loading"
|
||||||
|
@change="handleVideoMessageChange('prompt')"
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
|
||||||
|
<!-- 运动变化 -->
|
||||||
|
<n-form-item :show-label="false" :show-require-mark="false" :show-feedback="false">
|
||||||
|
<div class="motion-control">
|
||||||
|
<div class="motion-label">
|
||||||
|
<span>运动变化 (Motion)</span>
|
||||||
|
|
||||||
|
<n-tooltip trigger="hover" placement="top" style="max-width: 300px">
|
||||||
|
<template #trigger>
|
||||||
|
<n-button size="small" quaternary circle>
|
||||||
|
<template #icon>
|
||||||
|
<n-icon>
|
||||||
|
<HelpCircleOutline />
|
||||||
|
</n-icon>
|
||||||
|
</template>
|
||||||
|
</n-button>
|
||||||
|
</template>
|
||||||
|
<div style="line-height: 1.5">
|
||||||
|
<div style="font-weight: bold; margin-bottom: 8px">运动变化程度说明:</div>
|
||||||
|
<div style="margin-bottom: 6px">
|
||||||
|
生成视频时,您有两个运动设置选项可供选择:"低运动"和"高运动"。请使用网站上的相应按钮,或在视频提示末尾添加
|
||||||
|
<code>--motion low</code> 或参数。 <code>--motion high</code>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-bottom: 6px">
|
||||||
|
<strong>低运动(默认):</strong
|
||||||
|
>更有可能产生静止场景、低摄像机运动、慢动作或细微的角色动作。
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong>高运动:</strong
|
||||||
|
>更有可能导致大的摄像机运动和更大的角色运动,但也可能产生不切实际或有故障的动作。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</n-tooltip>
|
||||||
|
</div>
|
||||||
|
<n-select
|
||||||
|
v-model:value="videoMessage.mjVideoOptionsObject.motion"
|
||||||
|
:options="GetMJVideoMotionOptions()"
|
||||||
|
placeholder="选择运动变化程度"
|
||||||
|
@update-value="(value) => handleVideoMessageChange('motion', value)"
|
||||||
|
size="small"
|
||||||
|
:disabled="loading"
|
||||||
|
class="motion-select"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</n-form-item>
|
||||||
|
|
||||||
|
<!-- 减少创意 -->
|
||||||
|
<n-form-item :show-label="false" :show-require-mark="false" :show-feedback="false">
|
||||||
|
<div class="motion-control">
|
||||||
|
<div class="motion-label">
|
||||||
|
<span>视频原始 (Raw)</span>
|
||||||
|
|
||||||
|
<n-tooltip trigger="hover" placement="top" style="max-width: 300px">
|
||||||
|
<template #trigger>
|
||||||
|
<n-button size="small" quaternary circle>
|
||||||
|
<template #icon>
|
||||||
|
<n-icon>
|
||||||
|
<HelpCircleOutline />
|
||||||
|
</n-icon>
|
||||||
|
</template>
|
||||||
|
</n-button>
|
||||||
|
</template>
|
||||||
|
<div style="line-height: 1.5">
|
||||||
|
<div style="font-weight: bold; margin-bottom: 8px">视频原始开关说明:</div>
|
||||||
|
<div style="margin-bottom: 6px">
|
||||||
|
为了更精确地控制视频创作的运动,使用
|
||||||
|
<code>--raw</code> 视频提示中的参数会很有帮助。此功能的作用类似于图像的Raw 模式。
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
当您添加 <code>--raw</code>,它会减少 Midjourney
|
||||||
|
通常添加的额外创意天赋,从而使您的提示文本对结果产生更大的影响。
|
||||||
|
</div>
|
||||||
|
<br />
|
||||||
|
<div style="color: red;">注意:视频原始只有在有提示词的时候才会生效</div>
|
||||||
|
</div>
|
||||||
|
</n-tooltip>
|
||||||
|
</div>
|
||||||
|
<n-switch
|
||||||
|
v-model:value="videoMessage.mjVideoOptionsObject.raw"
|
||||||
|
size="small"
|
||||||
|
@update-value="handleVideoMessageChange('raw')"
|
||||||
|
:disabled="loading"
|
||||||
|
>
|
||||||
|
<template #checked>是</template>
|
||||||
|
<template #unchecked>否</template>
|
||||||
|
</n-switch>
|
||||||
|
</div>
|
||||||
|
</n-form-item>
|
||||||
|
|
||||||
|
<!-- 生成按钮 -->
|
||||||
|
<n-form-item>
|
||||||
|
<n-button type="primary" size="small" :loading="loading" @click="handleImageToVideo" block>
|
||||||
|
<template #icon>
|
||||||
|
<n-icon>
|
||||||
|
<svg viewBox="0 0 24 24">
|
||||||
|
<path fill="currentColor" d="M8,5.14V19.14L19,12.14L8,5.14Z" />
|
||||||
|
</svg>
|
||||||
|
</n-icon>
|
||||||
|
</template>
|
||||||
|
生成视频
|
||||||
|
</n-button>
|
||||||
|
</n-form-item>
|
||||||
|
</n-space>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import {
|
||||||
|
NIcon,
|
||||||
|
NInput,
|
||||||
|
NSelect,
|
||||||
|
NSwitch,
|
||||||
|
NButton,
|
||||||
|
NSpace,
|
||||||
|
NFormItem,
|
||||||
|
NImage,
|
||||||
|
NText,
|
||||||
|
NTooltip,
|
||||||
|
useMessage
|
||||||
|
} from 'naive-ui'
|
||||||
|
|
||||||
|
import { HelpCircleOutline } from '@vicons/ionicons5'
|
||||||
|
import { GetMJVideoMotionOptions } from '@/define/enum/video'
|
||||||
|
import { isEmpty } from 'lodash'
|
||||||
|
|
||||||
|
const message = useMessage()
|
||||||
|
|
||||||
|
// 定义 props
|
||||||
|
const props = defineProps({
|
||||||
|
videoMessage: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
loading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 定义 emits
|
||||||
|
const emit = defineEmits(['video-message-change', 'image-to-video'])
|
||||||
|
|
||||||
|
// 处理图片加载错误
|
||||||
|
function handleImageError() {
|
||||||
|
message.warning('图片加载失败,请检查图片链接是否有效')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修改 videoMessage 的通用函数
|
||||||
|
function handleVideoMessageChange(key, value = undefined) {
|
||||||
|
emit('video-message-change', key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理图生视频
|
||||||
|
function handleImageToVideo() {
|
||||||
|
if (isEmpty(props.videoMessage.imageUrl)) {
|
||||||
|
message.error('请输入首帧图片链接')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (props.videoMessage.mjVideoOptionsObject.motion == undefined) {
|
||||||
|
message.error('请选择运动变化程度')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
emit('image-to-video')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.input-with-preview {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-input {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-image {
|
||||||
|
width: auto;
|
||||||
|
height: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-placeholder {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
border: 1px solid #e0e0e6;
|
||||||
|
background-color: #fafafa;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.motion-control {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.motion-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-self: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.motion-select {
|
||||||
|
width: 100px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder-text {
|
||||||
|
font-size: 10px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,199 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mj-video-container">
|
||||||
|
<n-tabs v-model:value="activeTab" type="segment" size="small">
|
||||||
|
<!-- 图生视频 Tab -->
|
||||||
|
<n-tab-pane name="image-to-video" tab="图生视频">
|
||||||
|
<ImageTextVideoInfoMJVideoImageToVideo
|
||||||
|
:video-message="videoMessage"
|
||||||
|
:loading="loading"
|
||||||
|
@video-message-change="handleVideoMessageChange"
|
||||||
|
@image-to-video="handleImageToVideo"
|
||||||
|
/>
|
||||||
|
</n-tab-pane>
|
||||||
|
|
||||||
|
<!-- 视频拓展 Tab -->
|
||||||
|
<n-tab-pane name="video-extend" tab="视频拓展">
|
||||||
|
<ImageTextVideoInfoMJVideoExtend
|
||||||
|
:video-message="videoMessage"
|
||||||
|
:loading="loading"
|
||||||
|
:task="props.task"
|
||||||
|
@video-message-change="handleVideoMessageChange"
|
||||||
|
@select-parent-task="handleSelectParentTask"
|
||||||
|
/>
|
||||||
|
</n-tab-pane>
|
||||||
|
</n-tabs>
|
||||||
|
|
||||||
|
<!-- 选择父任务的 Modal 弹窗 -->
|
||||||
|
<n-modal
|
||||||
|
v-model:show="showParentTaskModal"
|
||||||
|
:mask-closable="false"
|
||||||
|
preset="card"
|
||||||
|
style="width: 90%; max-width: 1200px"
|
||||||
|
title="选择父任务"
|
||||||
|
size="huge"
|
||||||
|
:content-style="{ padding: '8px 16px' }"
|
||||||
|
:segmented="true"
|
||||||
|
>
|
||||||
|
<ImageTextVideoInfoMJVideoSelectParentTask
|
||||||
|
:taskData="props.task"
|
||||||
|
:videoList="subVideoPathObject"
|
||||||
|
@close="showParentTaskModal = false"
|
||||||
|
/>
|
||||||
|
</n-modal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { NTabs, NTabPane, NModal, useMessage } from 'naive-ui'
|
||||||
|
|
||||||
|
import { ImageToVideoModels, MJVideoMotion } from '@/define/enum/video'
|
||||||
|
import { ValidateJsonAndParse } from '@/define/Tools/validate'
|
||||||
|
import { BookBackTaskType, TaskExecuteType } from '@/define/enum/bookEnum'
|
||||||
|
import { DEFINE_STRING } from '@/define/define_string'
|
||||||
|
import { isEmpty } from 'lodash'
|
||||||
|
import ImageTextVideoInfoMJVideoSelectParentTask from './ImageTextVideoInfoMJVideoSelectParentTask.vue'
|
||||||
|
import ImageTextVideoInfoMJVideoImageToVideo from './ImageTextVideoInfoMJVideoImageToVideo.vue'
|
||||||
|
import ImageTextVideoInfoMJVideoExtend from './ImageTextVideoInfoMJVideoExtend.vue'
|
||||||
|
|
||||||
|
const message = useMessage()
|
||||||
|
|
||||||
|
// 定义 props
|
||||||
|
const props = defineProps({
|
||||||
|
task: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 响应式状态
|
||||||
|
const activeTab = ref('image-to-video')
|
||||||
|
const loading = ref(false)
|
||||||
|
const showParentTaskModal = ref(false)
|
||||||
|
|
||||||
|
// 图生视频表单
|
||||||
|
const videoMessage = computed(() => {
|
||||||
|
let videoMessage = props.task?.videoMessage || {}
|
||||||
|
let mjVideoOptionsString = videoMessage.mjVideoOptions || '{}'
|
||||||
|
let mjVideoOptions = ValidateJsonAndParse(mjVideoOptionsString)
|
||||||
|
|
||||||
|
mjVideoOptions.image = videoMessage.imageUrl ?? ''
|
||||||
|
if (mjVideoOptions.motion == undefined) {
|
||||||
|
mjVideoOptions.motion = MJVideoMotion.Low // 默认运动变化为低
|
||||||
|
}
|
||||||
|
if (mjVideoOptions.raw == undefined) {
|
||||||
|
mjVideoOptions.raw = true // 默认不启用原始模式
|
||||||
|
}
|
||||||
|
videoMessage.mjVideoOptionsObject = mjVideoOptions
|
||||||
|
return videoMessage
|
||||||
|
})
|
||||||
|
|
||||||
|
const subVideoPathObject = computed(() => {
|
||||||
|
return props.task?.subVideoPathObject || []
|
||||||
|
})
|
||||||
|
|
||||||
|
// 处理图生视频
|
||||||
|
async function handleImageToVideo() {
|
||||||
|
debugger
|
||||||
|
if (isEmpty(videoMessage.value.imageUrl)) {
|
||||||
|
message.error('请输入首帧图片链接')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (videoMessage.value.mjVideoOptionsObject.motion == undefined) {
|
||||||
|
message.error('请选择运动变化程度')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开始提交任务
|
||||||
|
let type = BookBackTaskType.MJ_VIDEO
|
||||||
|
|
||||||
|
// 添加任务
|
||||||
|
let res = await window.task.AddBookBackTask(
|
||||||
|
props.task.bookId,
|
||||||
|
type,
|
||||||
|
TaskExecuteType.AUTO,
|
||||||
|
props.task.bookTaskId,
|
||||||
|
props.task.id,
|
||||||
|
DEFINE_STRING.BOOK.MJ_VIDEO_TO_VIDEO_RETURN
|
||||||
|
)
|
||||||
|
if (res.code != 1) {
|
||||||
|
message.error(res.message)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
message.success('添加图转视频任务到队列成功')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选择父任务的处理函数
|
||||||
|
async function handleSelectParentTask() {
|
||||||
|
if (props.task.videoMessage?.videoType !== ImageToVideoModels.MJ_VIDEO) {
|
||||||
|
message.error('当前任务不是 MJ_VIDEO 类型,无法选择父任务!')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('选择父任务,当前任务数据:', props.task, subVideoPathObject.value)
|
||||||
|
showParentTaskModal.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 做一个修改 videoMessage 的通用函数
|
||||||
|
async function handleVideoMessageChange(key, value = undefined) {
|
||||||
|
let updateObject = {}
|
||||||
|
|
||||||
|
switch (key) {
|
||||||
|
case 'imageUrl':
|
||||||
|
updateObject.imageUrl = videoMessage.value.imageUrl
|
||||||
|
updateObject.mjVideoOptions = JSON.stringify({
|
||||||
|
...videoMessage.value.mjVideoOptionsObject,
|
||||||
|
image: videoMessage.value.imageUrl
|
||||||
|
})
|
||||||
|
break
|
||||||
|
case 'prompt':
|
||||||
|
updateObject.prompt = videoMessage.value.prompt
|
||||||
|
break
|
||||||
|
case 'motion':
|
||||||
|
updateObject.mjVideoOptions = JSON.stringify({
|
||||||
|
...videoMessage.value.mjVideoOptionsObject,
|
||||||
|
motion: value
|
||||||
|
})
|
||||||
|
break
|
||||||
|
case 'index':
|
||||||
|
updateObject.mjVideoOptions = JSON.stringify({
|
||||||
|
...videoMessage.value.mjVideoOptionsObject,
|
||||||
|
index: value
|
||||||
|
})
|
||||||
|
break
|
||||||
|
case 'taskId':
|
||||||
|
updateObject.mjVideoOptions = JSON.stringify({
|
||||||
|
...videoMessage.value.mjVideoOptionsObject,
|
||||||
|
taskId: value
|
||||||
|
})
|
||||||
|
break
|
||||||
|
case 'raw':
|
||||||
|
updateObject.mjVideoOptions = JSON.stringify({
|
||||||
|
...videoMessage.value.mjVideoOptionsObject,
|
||||||
|
raw: videoMessage.value.mjVideoOptionsObject.raw
|
||||||
|
})
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
message.error(`未知的修改键: ${key}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开始修改
|
||||||
|
let res = await window.book.video.UpdateBookTaskDetailVideoMessage(
|
||||||
|
videoMessage.value.bookTaskDetailId,
|
||||||
|
updateObject
|
||||||
|
)
|
||||||
|
|
||||||
|
if (res.code !== 1) {
|
||||||
|
message.error(`修改失败: ${res.message}, Key: ${key}`)
|
||||||
|
console.error('修改失败:', res)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.mj-video-container {
|
||||||
|
padding: 16px 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,849 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<n-alert
|
||||||
|
type="info"
|
||||||
|
closable
|
||||||
|
:show-icon="false"
|
||||||
|
:style="{
|
||||||
|
marginBottom: '16px'
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
修改选择的视频任务之后,会将对应的选择的视频的<strong>任务ID</strong>、<strong>视频索引</strong>更新到
|
||||||
|
<strong>Midjourney 视频配置</strong>
|
||||||
|
中的 <strong>视频拓展</strong> 中!!
|
||||||
|
</div>
|
||||||
|
</n-alert>
|
||||||
|
|
||||||
|
<div v-if="taskData" class="video-modal-content">
|
||||||
|
<!-- 选中的视频和任务信息 -->
|
||||||
|
<div class="selected-video-section">
|
||||||
|
<div class="selected-video-left">
|
||||||
|
<n-card
|
||||||
|
title="🎬 视频预览"
|
||||||
|
size="small"
|
||||||
|
:bordered="true"
|
||||||
|
:style="{
|
||||||
|
height: '100%'
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
</n-card>
|
||||||
|
</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="handleSaveMJVideoTaskSelection">
|
||||||
|
<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">
|
||||||
|
<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">
|
||||||
|
<TextEllipsis
|
||||||
|
v-if="currentVideoInfo"
|
||||||
|
:text="currentVideoInfo.taskId"
|
||||||
|
component="n-tag"
|
||||||
|
:component-props="{ type: 'primary', size: 'small' }"
|
||||||
|
max-width="120px"
|
||||||
|
/>
|
||||||
|
<n-text v-else depth="3">-</n-text>
|
||||||
|
<n-button
|
||||||
|
v-if="currentVideoInfo"
|
||||||
|
size="tiny"
|
||||||
|
type="primary"
|
||||||
|
ghost
|
||||||
|
@click="copyToClipboard(currentVideoInfo.taskId)"
|
||||||
|
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">
|
||||||
|
<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 v-if="currentVideoInfo" type="info" size="small">
|
||||||
|
{{ currentVideoInfo.index }}
|
||||||
|
</n-tag>
|
||||||
|
<n-text v-else depth="3">-</n-text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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="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 v-if="currentVideoInfo" type="success" size="small" round>
|
||||||
|
{{ GetImageToVideoModelsLabel(currentVideoInfo.type) || '未知类型' }}
|
||||||
|
</n-tag>
|
||||||
|
<n-text v-else depth="3">-</n-text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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="M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M18,20H6V4H13V9H18V20Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</n-icon>
|
||||||
|
<n-text strong>本地路径</n-text>
|
||||||
|
</div>
|
||||||
|
<div class="info-value">
|
||||||
|
<TextEllipsis
|
||||||
|
v-if="currentVideoInfo && currentVideoInfo.localPath"
|
||||||
|
:text="currentVideoInfo.localPath"
|
||||||
|
component="n-tag"
|
||||||
|
:component-props="{ type: 'success', size: 'small' }"
|
||||||
|
max-width="150px"
|
||||||
|
/>
|
||||||
|
<n-text v-else depth="3">-</n-text>
|
||||||
|
<n-button
|
||||||
|
v-if="currentVideoInfo && currentVideoInfo.localPath"
|
||||||
|
size="tiny"
|
||||||
|
type="success"
|
||||||
|
ghost
|
||||||
|
@click="copyToClipboard(currentVideoInfo.localPath)"
|
||||||
|
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">
|
||||||
|
<div class="info-label">
|
||||||
|
<n-icon size="16" class="info-icon">
|
||||||
|
<svg viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M17,7H22V17H17V19A1,1 0 0,0 18,20H20V22H17.5C16.95,22 16,21.55 16,21C16,21.55 15.05,22 14.5,22H12V20H14A1,1 0 0,0 15,19V5A1,1 0 0,0 14,4H12V2H14.5C15.05,2 16,2.45 16,3C16,2.45 16.95,2 17.5,2H20V4H18A1,1 0 0,0 17,5V7M2,7H13V9H4V15H13V17H2V7M8,10A1,1 0 0,1 9,11A1,1 0 0,1 8,12A1,1 0 0,1 7,11A1,1 0 0,1 8,10Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</n-icon>
|
||||||
|
<n-text strong>远端地址</n-text>
|
||||||
|
</div>
|
||||||
|
<div class="info-value">
|
||||||
|
<TextEllipsis
|
||||||
|
v-if="currentVideoInfo && currentVideoInfo.remotePath"
|
||||||
|
:text="currentVideoInfo.remotePath"
|
||||||
|
component="n-tag"
|
||||||
|
:component-props="{ type: 'warning', size: 'small' }"
|
||||||
|
max-width="150px"
|
||||||
|
/>
|
||||||
|
<n-text v-else depth="3">-</n-text>
|
||||||
|
<n-button
|
||||||
|
v-if="currentVideoInfo && currentVideoInfo.remotePath"
|
||||||
|
size="tiny"
|
||||||
|
type="warning"
|
||||||
|
ghost
|
||||||
|
@click="copyToClipboard(currentVideoInfo.remotePath)"
|
||||||
|
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">
|
||||||
|
<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" :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.localPath }"
|
||||||
|
@click="selectVideo(video)"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<n-text>视频 {{ index + 1 }}</n-text>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #header-extra>
|
||||||
|
<n-space justify="center" align="center">
|
||||||
|
<n-tooltip :show-arrow="false" trigger="hover">
|
||||||
|
<template #trigger>
|
||||||
|
<n-tag type="primary" size="small" class="video-status-tag task-id-tag">
|
||||||
|
{{ video.taskId }}
|
||||||
|
</n-tag>
|
||||||
|
</template>
|
||||||
|
{{ video.taskId }}
|
||||||
|
</n-tooltip>
|
||||||
|
<n-tag type="info" size="small" class="video-status-tag">
|
||||||
|
{{ video.index }}
|
||||||
|
</n-tag>
|
||||||
|
</n-space>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="video-container">
|
||||||
|
<video :src="video.localPath" class="video-player" preload="metadata" />
|
||||||
|
</div>
|
||||||
|
</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 />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||||
|
import {
|
||||||
|
NModal,
|
||||||
|
NAlert,
|
||||||
|
NText,
|
||||||
|
NCard,
|
||||||
|
NGrid,
|
||||||
|
NGridItem,
|
||||||
|
NSpace,
|
||||||
|
NTag,
|
||||||
|
NIcon,
|
||||||
|
NButton,
|
||||||
|
NTooltip,
|
||||||
|
NEmpty,
|
||||||
|
useMessage
|
||||||
|
} from 'naive-ui'
|
||||||
|
import { GetImageToVideoModelsLabel, ImageToVideoModels } from '@/define/enum/video'
|
||||||
|
|
||||||
|
import TextEllipsis from '@/renderer/src/components/Common/TextEllipsis.vue'
|
||||||
|
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 videoList = computed(() => {
|
||||||
|
console.log('未过滤的当前视频列表', props.videoList)
|
||||||
|
// 过滤掉没有本地路径的视频
|
||||||
|
return props.videoList.filter(
|
||||||
|
(video) =>
|
||||||
|
!isEmpty(video.localPath) &&
|
||||||
|
(video.type == ImageToVideoModels.MJ_VIDEO ||
|
||||||
|
video.type == ImageToVideoModels.MJ_VIDEO_EXTEND)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const videoMessage = ref({})
|
||||||
|
|
||||||
|
const message = useMessage()
|
||||||
|
|
||||||
|
// 当前选中的视频
|
||||||
|
const currentSelectedVideo = ref(props.taskData.generateVideoPath)
|
||||||
|
|
||||||
|
const currentVideoInfo = ref(null)
|
||||||
|
|
||||||
|
// 窗口宽度
|
||||||
|
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 || {}
|
||||||
|
videoList.value = videoList.value.filter((t) => !isEmpty(t.localPath))
|
||||||
|
if (videoList.value.length > 0) {
|
||||||
|
// 默认选择第一个视频
|
||||||
|
selectVideo(videoList.value[0])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('resize', handleResize)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 选择视频
|
||||||
|
function selectVideo(video) {
|
||||||
|
currentSelectedVideo.value = video.localPath
|
||||||
|
currentVideoInfo.value = video
|
||||||
|
if (!isEmpty(currentVideoInfo.value.localPath)) {
|
||||||
|
currentVideoInfo.value.localPath = currentVideoInfo.value.localPath.split('?t=')[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存当前任务选择的按钮事件
|
||||||
|
async function handleSaveMJVideoTaskSelection() {
|
||||||
|
try {
|
||||||
|
if (currentVideoInfo.value == null || isEmpty(currentSelectedVideo.value)) {
|
||||||
|
message.error('请选择一个视频')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let taskId = currentVideoInfo.value.taskId
|
||||||
|
let videoIndex = currentVideoInfo.value.index
|
||||||
|
|
||||||
|
console.log('当前选中的视频信息', taskId, videoIndex, currentSelectedVideo.value)
|
||||||
|
if (isEmpty(taskId) || videoIndex == null) {
|
||||||
|
message.error('当前选中的视频的 taskId 或 videoIndex 为空,请检查视频信息')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开始修改实际的数据
|
||||||
|
let updateObject = {
|
||||||
|
mjVideoOptions: JSON.stringify({
|
||||||
|
...videoMessage.value.mjVideoOptionsObject,
|
||||||
|
taskId: taskId,
|
||||||
|
index: videoIndex
|
||||||
|
})
|
||||||
|
}
|
||||||
|
console.log('更新的实际数据', updateObject)
|
||||||
|
|
||||||
|
let res = await window.book.video.UpdateBookTaskDetailVideoMessage(
|
||||||
|
props.taskData.id,
|
||||||
|
updateObject
|
||||||
|
)
|
||||||
|
|
||||||
|
// 检查返回结果
|
||||||
|
if (res.code == 1) {
|
||||||
|
message.success('视频选择已保存成功')
|
||||||
|
|
||||||
|
videoMessage.value.mjVideoOptionsObject.taskId = taskId
|
||||||
|
videoMessage.value.mjVideoOptionsObject.index = videoIndex
|
||||||
|
} else {
|
||||||
|
message.error('保存视频选择失败: ' + res.message)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
message.error('保存视频选择失败: ' + error.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 复制到剪贴板
|
||||||
|
async function copyToClipboard(text) {
|
||||||
|
try {
|
||||||
|
if (isEmpty(text)) {
|
||||||
|
message.warning('复制的内容为空')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await navigator.clipboard.writeText(text)
|
||||||
|
message.success('已复制到剪贴板')
|
||||||
|
} 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: var(--n-color-embedded);
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid var(--n-border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-video-left {
|
||||||
|
flex: 3;
|
||||||
|
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: var(--n-color-embedded-popover);
|
||||||
|
color: var(--n-text-color-disabled);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-info-right {
|
||||||
|
flex: 2;
|
||||||
|
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: var(--n-color-embedded);
|
||||||
|
border-radius: 8px;
|
||||||
|
border-left: 4px solid #18a058;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
border: 1px solid var(--n-border-color);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 5px;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(to bottom, #18a058, #36ad6a);
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item:hover {
|
||||||
|
background: var(--n-color-embedded-popover);
|
||||||
|
transform: translateX(4px);
|
||||||
|
border-color: #18a058;
|
||||||
|
border-left-color: #0c7a43;
|
||||||
|
box-shadow: 0 2px 8px rgba(24, 160, 88, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item:hover::before {
|
||||||
|
width: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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: var(--n-color-embedded-popover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-container {
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 16/9;
|
||||||
|
background-color: var(--n-color-embedded-popover);
|
||||||
|
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: var(--n-text-color-disabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Task ID Tag 样式 */
|
||||||
|
.task-id-tag {
|
||||||
|
max-width: 120px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
/* display: inline-block; */
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-id-tag :deep(.n-tag__content) {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -1,45 +0,0 @@
|
|||||||
<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>
|
|
||||||
@ -3,62 +3,31 @@
|
|||||||
<div class="detail-header">
|
<div class="detail-header">
|
||||||
<n-space justify="space-between" align="center">
|
<n-space justify="space-between" align="center">
|
||||||
<n-text strong style="font-size: 18px">任务详情</n-text>
|
<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>
|
</n-space>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<n-divider />
|
<n-divider />
|
||||||
|
|
||||||
<!-- 基本信息 -->
|
<!-- 不同的模式切换 -->
|
||||||
<image-text-video-info-basic-info :task="task" />
|
|
||||||
|
|
||||||
<!-- 进度信息 -->
|
<!-- 基本信息 -->
|
||||||
<image-text-video-info-progress :task="task" />
|
<image-text-video-info-basic-info :task="task" :video-message="videoMessage" />
|
||||||
|
|
||||||
<!-- 参数配置 -->
|
<!-- 参数配置 -->
|
||||||
<image-text-video-info-config :task="task" @config-change="handleConfigChange" />
|
<image-text-video-info-config :task="task" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import {
|
import {
|
||||||
NSpace,
|
NSpace,
|
||||||
NText,
|
NText,
|
||||||
NButton,
|
NDivider
|
||||||
NButtonGroup,
|
|
||||||
NIcon,
|
|
||||||
NDivider,
|
|
||||||
useMessage,
|
|
||||||
useDialog
|
|
||||||
} from 'naive-ui'
|
} from 'naive-ui'
|
||||||
import {
|
|
||||||
CreateOutline,
|
|
||||||
TrashOutline
|
|
||||||
} from '@vicons/ionicons5'
|
|
||||||
import ImageTextVideoInfoBasicInfo from './ImageTextVideoInfoBasicInfo.vue'
|
import ImageTextVideoInfoBasicInfo from './ImageTextVideoInfoBasicInfo.vue'
|
||||||
import ImageTextVideoInfoProgress from './ImageTextVideoInfoProgress.vue'
|
|
||||||
import ImageTextVideoInfoConfig from './ImageTextVideoInfoConfig.vue'
|
import ImageTextVideoInfoConfig from './ImageTextVideoInfoConfig.vue'
|
||||||
|
|
||||||
const message = useMessage()
|
|
||||||
const dialog = useDialog()
|
|
||||||
|
|
||||||
// 定义 emits
|
|
||||||
const emit = defineEmits(['delete-task', 'edit-task', 'config-change'])
|
|
||||||
|
|
||||||
// 定义 props
|
// 定义 props
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
task: {
|
task: {
|
||||||
@ -67,52 +36,19 @@ const props = defineProps({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// 编辑任务
|
const videoMessage = ref(props.task.videoMessage)
|
||||||
function handleEdit() {
|
|
||||||
emit('edit-task', props.task)
|
|
||||||
message.info(`编辑任务: ${props.task.name}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 删除任务
|
onMounted(() => {
|
||||||
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>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.task-detail {
|
.task-detail {
|
||||||
height: 100%;
|
height: calc(100% - 20px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-header {
|
.detail-header {
|
||||||
margin-bottom: 16px;
|
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>
|
</style>
|
||||||
|
|||||||
@ -2,7 +2,15 @@
|
|||||||
<div class="table-container">
|
<div class="table-container">
|
||||||
<div class="table-header">
|
<div class="table-header">
|
||||||
<n-space justify="space-between" align="center">
|
<n-space justify="space-between" align="center">
|
||||||
<n-text strong>任务列表</n-text>
|
<div>
|
||||||
|
<n-button type="default" size="small" @click="handleBackToTaskList">
|
||||||
|
<template #icon>
|
||||||
|
<n-icon><ArrowBackOutline /></n-icon>
|
||||||
|
</template>
|
||||||
|
返回任务列表
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<n-space align="center">
|
<n-space align="center">
|
||||||
<n-space align="center" size="small">
|
<n-space align="center" size="small">
|
||||||
<n-text>分页</n-text>
|
<n-text>分页</n-text>
|
||||||
@ -12,7 +20,7 @@
|
|||||||
<n-text>右侧面板</n-text>
|
<n-text>右侧面板</n-text>
|
||||||
<n-switch v-model:value="showRightPanel" @update:value="handleRightPanelToggle" />
|
<n-switch v-model:value="showRightPanel" @update:value="handleRightPanelToggle" />
|
||||||
</n-space>
|
</n-space>
|
||||||
<n-button type="primary" size="small" @click="handleAdd">
|
<n-button type="primary" size="small" @click="message.info('新增任务功能开发中...')">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<n-icon><AddOutline /></n-icon>
|
<n-icon><AddOutline /></n-icon>
|
||||||
</template>
|
</template>
|
||||||
@ -24,11 +32,11 @@
|
|||||||
|
|
||||||
<n-data-table
|
<n-data-table
|
||||||
:columns="columns"
|
:columns="columns"
|
||||||
:data="tableData"
|
:data="reverseManageStore.selectBookTaskDetail"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
:pagination="paginationEnabled ? paginationReactive : false"
|
:pagination="paginationEnabled ? paginationReactive : false"
|
||||||
:row-key="(row) => row.id"
|
:row-key="(row) => row.id"
|
||||||
:scroll-x="1400"
|
:scroll-x="1200"
|
||||||
:max-height="tableMaxHeight"
|
:max-height="tableMaxHeight"
|
||||||
:min-height="tableMinHeight"
|
:min-height="tableMinHeight"
|
||||||
class="data-table"
|
class="data-table"
|
||||||
@ -37,7 +45,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, h, computed, onMounted, onUnmounted, watch } from 'vue'
|
import { ref, reactive, h, computed, onMounted, onUnmounted, watch, toRef } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
import {
|
import {
|
||||||
NSpace,
|
NSpace,
|
||||||
NText,
|
NText,
|
||||||
@ -48,32 +57,31 @@ import {
|
|||||||
NSwitch,
|
NSwitch,
|
||||||
NImage,
|
NImage,
|
||||||
NInput,
|
NInput,
|
||||||
useMessage
|
NEmpty,
|
||||||
|
NSelect,
|
||||||
|
useMessage,
|
||||||
|
useDialog
|
||||||
} from 'naive-ui'
|
} from 'naive-ui'
|
||||||
import { AddOutline, EyeOutline, PlayOutline, PauseOutline } from '@vicons/ionicons5'
|
import { AddOutline, EyeOutline, ArrowBackOutline } from '@vicons/ionicons5'
|
||||||
import { define } from '@/define/define'
|
import { define } from '@/define/define'
|
||||||
import ImageTextVideoInfoVideoConfig from './ImageTextVideoInfoVideoConfig.vue'
|
import ImageTextVideoInfoVideoConfig from './ImageTextVideoInfoVideoConfig.vue'
|
||||||
import ImageTextVideoInfoVideoListInfo from './ImageTextVideoInfoVideoListInfo.vue'
|
import ImageTextVideoInfoVideoListInfo from './ImageTextVideoInfoVideoListInfo.vue'
|
||||||
|
import ImageTextVideoInfoTaskOptions from './ImageTextVideoInfoTaskOptions.vue'
|
||||||
|
import { OptionKeyName, OptionType } from '@/define/enum/option'
|
||||||
|
import { useReverseManageStore } from '@/stores/reverseManage'
|
||||||
|
import { GetImageToVideoModelsOptions } from '@/define/enum/video'
|
||||||
|
|
||||||
|
const reverseManageStore = useReverseManageStore()
|
||||||
|
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
|
const dialog = useDialog()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
// 定义 emits
|
// 定义 emits
|
||||||
const emit = defineEmits([
|
const emit = defineEmits(['toggle-right-panel', 'view-detail'])
|
||||||
'view-detail',
|
|
||||||
'toggle-status',
|
|
||||||
'add-task',
|
|
||||||
'update-prompt',
|
|
||||||
'update-method',
|
|
||||||
'update-note',
|
|
||||||
'toggle-right-panel'
|
|
||||||
])
|
|
||||||
|
|
||||||
// 定义 props
|
// 定义 props
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
tableData: {
|
|
||||||
type: Array,
|
|
||||||
default: () => []
|
|
||||||
},
|
|
||||||
loading: {
|
loading: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false
|
||||||
@ -81,16 +89,26 @@ const props = defineProps({
|
|||||||
showRightPanel: {
|
showRightPanel: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true
|
default: true
|
||||||
|
},
|
||||||
|
showPagination: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// 分页开关状态
|
// 分页开关状态 - 使用本地ref + watch监听props变化
|
||||||
const paginationEnabled = ref(true)
|
const paginationEnabled = ref(props.showPagination)
|
||||||
|
|
||||||
// 右侧面板开关状态
|
// 右侧面板开关状态 - 使用本地ref + watch监听props变化
|
||||||
const showRightPanel = ref(props.showRightPanel)
|
const showRightPanel = ref(props.showRightPanel)
|
||||||
|
|
||||||
// 监听 props 变化
|
// 批量设置视频类型
|
||||||
|
const batchVideoType = ref(null)
|
||||||
|
|
||||||
|
// 视频类型选项
|
||||||
|
const videoTypeOptions = GetImageToVideoModelsOptions()
|
||||||
|
|
||||||
|
// 监听 props 变化,同步更新本地状态
|
||||||
watch(
|
watch(
|
||||||
() => props.showRightPanel,
|
() => props.showRightPanel,
|
||||||
(newVal) => {
|
(newVal) => {
|
||||||
@ -98,6 +116,13 @@ watch(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.showPagination,
|
||||||
|
(newVal) => {
|
||||||
|
paginationEnabled.value = newVal
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
// 窗口高度响应式
|
// 窗口高度响应式
|
||||||
const windowHeight = ref(window.innerHeight)
|
const windowHeight = ref(window.innerHeight)
|
||||||
|
|
||||||
@ -207,10 +232,11 @@ const columns = [
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
backgroundColor: '#f5f5f5',
|
backgroundColor: 'var(--n-color-embedded-popover)',
|
||||||
borderRadius: '4px',
|
borderRadius: '4px',
|
||||||
color: '#999',
|
color: 'var(--n-text-color-disabled)',
|
||||||
fontSize: '12px'
|
fontSize: '12px',
|
||||||
|
border: '1px dashed var(--n-border-color)'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'图片不存在'
|
'图片不存在'
|
||||||
@ -254,8 +280,8 @@ const columns = [
|
|||||||
spellcheck: false
|
spellcheck: false
|
||||||
},
|
},
|
||||||
onUpdateValue: (value) => {
|
onUpdateValue: (value) => {
|
||||||
row.note = value
|
row.videoMessage.imageUrl = value
|
||||||
emit('update-note', row.id, value)
|
handleSaveBookTaskDetailVideoMessage(row, row.id, 'imageUrl', value)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
@ -263,7 +289,29 @@ const columns = [
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '视频配置',
|
title: () =>
|
||||||
|
h(
|
||||||
|
'div',
|
||||||
|
{
|
||||||
|
style: {
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
gap: '16px'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[
|
||||||
|
h('span', '视频配置'),
|
||||||
|
h(NSelect, {
|
||||||
|
value: batchVideoType.value,
|
||||||
|
size: 'small',
|
||||||
|
style: { width: '120px' },
|
||||||
|
options: videoTypeOptions,
|
||||||
|
placeholder: '批量设置',
|
||||||
|
onUpdateValue: handleBatchVideoTypeChange
|
||||||
|
})
|
||||||
|
]
|
||||||
|
),
|
||||||
key: 'videoConfig',
|
key: 'videoConfig',
|
||||||
minWidth: 300,
|
minWidth: 300,
|
||||||
className: noPaddingColumnClass,
|
className: noPaddingColumnClass,
|
||||||
@ -271,13 +319,11 @@ const columns = [
|
|||||||
return h(ImageTextVideoInfoVideoConfig, {
|
return h(ImageTextVideoInfoVideoConfig, {
|
||||||
taskId: row.id,
|
taskId: row.id,
|
||||||
videoMessage: row?.videoMessage,
|
videoMessage: row?.videoMessage,
|
||||||
onUpdateMethod: (taskId, method) => {
|
onUpdateMethod: async (taskId, value) => {
|
||||||
row.videoMethod = method
|
await handleSaveBookTaskDetailVideoMessage(row, taskId, 'videoType', value)
|
||||||
emit('update-method', taskId, method)
|
|
||||||
},
|
},
|
||||||
onUpdatePrompt: (taskId, prompt) => {
|
onUpdatePrompt: async (taskId, prompt) => {
|
||||||
row.videoPrompt = prompt
|
await handleSaveBookTaskDetailVideoMessage(row, taskId, 'prompt', prompt)
|
||||||
emit('update-prompt', taskId, prompt)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -322,14 +368,15 @@ const columns = [
|
|||||||
width: '100%',
|
width: '100%',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center'
|
||||||
backgroundColor: '#f5f5f5',
|
|
||||||
borderRadius: '4px',
|
|
||||||
color: '#999',
|
|
||||||
fontSize: '12px'
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'暂无视频'
|
[
|
||||||
|
h(NEmpty, {
|
||||||
|
description: '暂无视频',
|
||||||
|
size: 'small'
|
||||||
|
})
|
||||||
|
]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -343,85 +390,134 @@ const columns = [
|
|||||||
render(row) {
|
render(row) {
|
||||||
return h(ImageTextVideoInfoVideoListInfo, {
|
return h(ImageTextVideoInfoVideoListInfo, {
|
||||||
taskData: row,
|
taskData: row,
|
||||||
videoList: row.subVideoPath || []
|
videoList: row.subVideoPathObject || []
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '操作',
|
title: '操作',
|
||||||
key: 'actions',
|
key: 'actions',
|
||||||
width: 160,
|
width: 120,
|
||||||
fixed: 'right',
|
fixed: 'right',
|
||||||
render(row) {
|
render(row) {
|
||||||
return h(
|
return h(ImageTextVideoInfoTaskOptions, {
|
||||||
NSpace,
|
bookTaskDetail: row,
|
||||||
{ size: 'small' },
|
onViewDetail: (bookTaskDetail) => handleViewDetail(bookTaskDetail)
|
||||||
{
|
|
||||||
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) {
|
function handleViewDetail(row) {
|
||||||
emit('view-detail', row)
|
emit('view-detail', row)
|
||||||
message.info(`查看任务: ${row.name}`)
|
message.info(`查看任务: ${row.name}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 切换状态
|
// 返回任务列表
|
||||||
function handleToggleStatus(row) {
|
function handleBackToTaskList() {
|
||||||
emit('toggle-status', row)
|
router.push('/image_text_video')
|
||||||
|
message.info('返回任务列表')
|
||||||
}
|
}
|
||||||
|
|
||||||
// 新增任务
|
// 批量设置视频类型
|
||||||
function handleAdd() {
|
async function handleBatchVideoTypeChange(value) {
|
||||||
emit('add-task')
|
if (!value) return
|
||||||
message.info('新增任务功能开发中...')
|
|
||||||
|
try {
|
||||||
|
// 获取当前显示的所有任务
|
||||||
|
const tasks = reverseManageStore.selectBookTaskDetail
|
||||||
|
if (!tasks || tasks.length === 0) {
|
||||||
|
message.warning('没有可设置的任务')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加一个提示的dialog 判断用户选择 是/否
|
||||||
|
const selectedOption = videoTypeOptions.find((option) => option.value === value)
|
||||||
|
const optionLabel = selectedOption ? selectedOption.label : value
|
||||||
|
|
||||||
|
dialog.warning({
|
||||||
|
title: '批量设置确认',
|
||||||
|
content: `确定要将所有 ${tasks.length} 个任务的视频类型设置为 "${optionLabel}" 吗?此操作不可撤销。`,
|
||||||
|
positiveText: '确定',
|
||||||
|
negativeText: '取消',
|
||||||
|
onPositiveClick: async () => {
|
||||||
|
try {
|
||||||
|
// 批量更新所有任务的视频类型
|
||||||
|
const updatePromises = tasks.map((task) =>
|
||||||
|
handleSaveBookTaskDetailVideoMessage(task, task.id, 'videoType', value)
|
||||||
|
)
|
||||||
|
|
||||||
|
// 修改所有批次的 videoType
|
||||||
|
await Promise.all(updatePromises)
|
||||||
|
|
||||||
|
// 修改 bookTask 中的 videoCategory
|
||||||
|
let res = await window.db.UpdateBookTaskData(reverseManageStore.selectBookTask.id, {
|
||||||
|
videoCategory: value
|
||||||
|
})
|
||||||
|
|
||||||
|
if (res.code !== 1) {
|
||||||
|
message.error(`修改小说批次任务的出图方式失败: ${res.message}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
message.success(`已批量设置 ${tasks.length} 个任务的视频类型为: ${optionLabel}`)
|
||||||
|
|
||||||
|
// 重置选择
|
||||||
|
batchVideoType.value = null
|
||||||
|
} catch (error) {
|
||||||
|
message.error(`批量设置失败: ${error.message}`)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onNegativeClick: () => {
|
||||||
|
// 取消时重置选择
|
||||||
|
batchVideoType.value = null
|
||||||
|
message.info('已取消批量设置')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
message.error(`批量设置失败: ${error.message}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修改视频提示词里面指定的信息
|
||||||
|
async function handleSaveBookTaskDetailVideoMessage(row, taskId, key, value) {
|
||||||
|
try {
|
||||||
|
let res = await window.book.video.UpdateBookTaskDetailVideoMessage(taskId, {
|
||||||
|
[key]: value
|
||||||
|
})
|
||||||
|
if (res.code !== 1) {
|
||||||
|
message.error(`保存失败: ${res.message}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
row.videoMessage[key] = value
|
||||||
|
// 修改store中的数据
|
||||||
|
let index = reverseManageStore.selectBookTaskDetail.findIndex((item) => item.id === taskId)
|
||||||
|
|
||||||
|
if (index !== -1) {
|
||||||
|
reverseManageStore.selectBookTaskDetail[index].videoMessage[key] = value
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
message.error(`保存失败: ${error.message}`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理分页开关切换
|
// 处理分页开关切换
|
||||||
function handlePaginationToggle(value) {
|
async function handlePaginationToggle(value) {
|
||||||
|
// 通用修改
|
||||||
|
// 将当前的变化写道数据库里面 后续直接加载
|
||||||
|
let res = await window.options.ModifyOptionByKey(
|
||||||
|
OptionKeyName.ImageToVideo_ShowPagination,
|
||||||
|
value,
|
||||||
|
OptionType.BOOLEAN
|
||||||
|
)
|
||||||
|
|
||||||
|
if (res.code != 1) {
|
||||||
|
message.error(res.message || '切换失败')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断数据 给出不同的提示
|
||||||
if (value) {
|
if (value) {
|
||||||
// 开启分页
|
// 开启分页
|
||||||
paginationReactive.page = 1 // 重置到第一页
|
paginationReactive.page = 1 // 重置到第一页
|
||||||
@ -433,8 +529,21 @@ function handlePaginationToggle(value) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 处理右侧面板开关切换
|
// 处理右侧面板开关切换
|
||||||
function handleRightPanelToggle(value) {
|
async function handleRightPanelToggle(value) {
|
||||||
|
// 将当前的变化写道数据库里面 后续直接加载
|
||||||
|
let res = await window.options.ModifyOptionByKey(
|
||||||
|
OptionKeyName.ImageToVideo_ShowRightPanel,
|
||||||
|
value,
|
||||||
|
OptionType.BOOLEAN
|
||||||
|
)
|
||||||
|
|
||||||
|
if (res.code != 1) {
|
||||||
|
message.error(res.message || '切换失败')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 更新本地状态
|
||||||
showRightPanel.value = value
|
showRightPanel.value = value
|
||||||
|
// 发布事件通知父组件
|
||||||
emit('toggle-right-panel', value)
|
emit('toggle-right-panel', value)
|
||||||
if (value) {
|
if (value) {
|
||||||
message.info('右侧面板已显示')
|
message.info('右侧面板已显示')
|
||||||
@ -500,20 +609,6 @@ function handleRightPanelToggle(value) {
|
|||||||
background: #a8a8a8;
|
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列样式 */
|
/* 通用的无padding列样式 */
|
||||||
.data-table :deep(.no-padding-column) {
|
.data-table :deep(.no-padding-column) {
|
||||||
padding: 2px !important;
|
padding: 2px !important;
|
||||||
|
|||||||
@ -0,0 +1,100 @@
|
|||||||
|
<template>
|
||||||
|
<n-space size="small" justify="center">
|
||||||
|
<n-button size="small" type="primary" text @click="handleViewDetail">
|
||||||
|
<template #icon>
|
||||||
|
<n-icon>
|
||||||
|
<EyeOutline />
|
||||||
|
</n-icon>
|
||||||
|
</template>
|
||||||
|
查看
|
||||||
|
</n-button>
|
||||||
|
<n-button size="small" type="warning" text @click="handleReloadTask">
|
||||||
|
<template #icon>
|
||||||
|
<n-icon>
|
||||||
|
<RefreshOutline />
|
||||||
|
</n-icon>
|
||||||
|
</template>
|
||||||
|
重新加载
|
||||||
|
</n-button>
|
||||||
|
</n-space>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { NSpace, NButton, NIcon, useDialog, useMessage } from 'naive-ui'
|
||||||
|
import { EyeOutline, RefreshOutline } from '@vicons/ionicons5'
|
||||||
|
import { useSoftwareStore } from '@/stores/software'
|
||||||
|
import { useReverseManageStore } from '@/stores/reverseManage'
|
||||||
|
|
||||||
|
const dialog = useDialog()
|
||||||
|
const message = useMessage()
|
||||||
|
|
||||||
|
const softwareStore = useSoftwareStore()
|
||||||
|
const reverseManageStore = useReverseManageStore()
|
||||||
|
|
||||||
|
// 定义 emits
|
||||||
|
const emit = defineEmits(['view-detail', 'reload-task'])
|
||||||
|
|
||||||
|
// 定义 props
|
||||||
|
const props = defineProps({
|
||||||
|
bookTaskDetail: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 查看详情
|
||||||
|
function handleViewDetail() {
|
||||||
|
emit('view-detail', props.bookTaskDetail)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重新加载任务
|
||||||
|
function handleReloadTask() {
|
||||||
|
dialog.warning({
|
||||||
|
title: '确认重新加载',
|
||||||
|
content: `确定要重新加载任务 "${props.bookTaskDetail.name}" 吗?\n\n重新加载会将当前的任务重新获取,只能再当前任务视频下载失败时才能使用。否则会导致重复的视频显示,请确认是否继续!`,
|
||||||
|
positiveText: '确定',
|
||||||
|
negativeText: '取消',
|
||||||
|
onPositiveClick: async () => {
|
||||||
|
softwareStore.spin.spinning = true
|
||||||
|
softwareStore.spin.tip = '正在重新加载视频任务...'
|
||||||
|
try {
|
||||||
|
console.log(props.bookTaskDetail.videoMessage)
|
||||||
|
if (
|
||||||
|
props.bookTaskDetail.videoMessage &&
|
||||||
|
props.bookTaskDetail.videoMessage.taskId &&
|
||||||
|
props.bookTaskDetail.videoMessage.videoType
|
||||||
|
) {
|
||||||
|
let res = await window.book.video.ReloadVideoTaskInfo(props.bookTaskDetail.id)
|
||||||
|
console.log('重新加载任务结果:', res)
|
||||||
|
if (res.code != 1) {
|
||||||
|
message.error(res.message)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开始修改数据
|
||||||
|
let findIndex = reverseManageStore.selectBookTaskDetail.findIndex(
|
||||||
|
(x) => x.id == res.data.id
|
||||||
|
)
|
||||||
|
if (findIndex != -1) {
|
||||||
|
reverseManageStore.selectBookTaskDetail[findIndex] = res.data
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
message.error('未找到当前分镜的视频配置,不可重新加载!!')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
message.error('重新加载任务失败,' + error.message)
|
||||||
|
} finally {
|
||||||
|
softwareStore.spin.spinning = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onNegativeClick: () => {
|
||||||
|
// 用户取消,什么都不做
|
||||||
|
message.info('用户取消操作')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* 可以在这里添加组件特定的样式 */
|
||||||
|
</style>
|
||||||
@ -4,23 +4,37 @@
|
|||||||
<div class="method-section">
|
<div class="method-section">
|
||||||
<div class="method-row">
|
<div class="method-row">
|
||||||
<n-select
|
<n-select
|
||||||
v-model:value="videoMessage.videoType"
|
:value="props.videoMessage?.videoType || ''"
|
||||||
:options="videoMethodOptions"
|
:options="videoMethodOptions"
|
||||||
placeholder="选择转视频方式"
|
placeholder="选择转视频方式"
|
||||||
size="small"
|
size="small"
|
||||||
@update:value="handleMethodChange"
|
@update:value="handleMethodChange"
|
||||||
class="method-select"
|
class="method-select"
|
||||||
/>
|
/>
|
||||||
<n-tag type="primary" size="small" class="method-progress">
|
<n-tooltip v-if="props.videoMessage?.status == 'fail'">
|
||||||
{{ videoMessage.status ?? 'wait' }}
|
<template #trigger>
|
||||||
|
<n-tag type="error" size="small" class="method-progress">
|
||||||
|
{{ props.videoMessage?.status }}
|
||||||
</n-tag>
|
</n-tag>
|
||||||
|
</template>
|
||||||
|
{{ props.videoMessage?.msg ?? '' }}
|
||||||
|
</n-tooltip>
|
||||||
|
<n-tag v-else type="primary" size="small" class="method-progress">
|
||||||
|
{{ props.videoMessage?.status ?? 'wait' }}
|
||||||
|
</n-tag>
|
||||||
|
<n-tooltip :show-arrow="false" trigger="hover">
|
||||||
|
<template #trigger>
|
||||||
|
<span class="task-id-span">{{ props.videoMessage?.taskId ?? '' }}</span>
|
||||||
|
</template>
|
||||||
|
{{ props.videoMessage?.taskId ?? '' }}
|
||||||
|
</n-tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 视频提示词输入 -->
|
<!-- 视频提示词输入 -->
|
||||||
<div class="prompt-section">
|
<div class="prompt-section">
|
||||||
<n-input
|
<n-input
|
||||||
v-model:value="videoMessage.prompt"
|
:value="props.videoMessage?.prompt || ''"
|
||||||
type="textarea"
|
type="textarea"
|
||||||
:rows="3"
|
:rows="3"
|
||||||
placeholder="请输入视频提示词..."
|
placeholder="请输入视频提示词..."
|
||||||
@ -32,8 +46,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch, onMounted, toRef } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { NSelect, NInput, NTag } from 'naive-ui'
|
import { NSelect, NInput, NTag, NTooltip } from 'naive-ui'
|
||||||
import { GetImageToVideoModelsOptions } from '@/define/enum/video'
|
import { GetImageToVideoModelsOptions } from '@/define/enum/video'
|
||||||
|
|
||||||
// 定义 emits
|
// 定义 emits
|
||||||
@ -54,29 +68,18 @@ const props = defineProps({
|
|||||||
// 转视频方式选项
|
// 转视频方式选项
|
||||||
const videoMethodOptions = GetImageToVideoModelsOptions()
|
const videoMethodOptions = GetImageToVideoModelsOptions()
|
||||||
|
|
||||||
const videoMessage = ref({})
|
|
||||||
|
|
||||||
// 处理方式变更
|
// 处理方式变更
|
||||||
function handleMethodChange(value) {
|
function handleMethodChange(value) {
|
||||||
emit('update-method', props.taskId, value)
|
emit('update-method', props.videoMessage?.bookTaskDetailId, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理提示词变更
|
// 处理提示词变更
|
||||||
function handlePromptChange(value) {
|
function handlePromptChange(value) {
|
||||||
emit('update-prompt', props.taskId, value)
|
emit('update-prompt', props.videoMessage?.bookTaskDetailId, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化
|
// 初始化
|
||||||
onMounted(() => {
|
onMounted(() => {})
|
||||||
if (props.videoMessage) {
|
|
||||||
videoMessage.value = props.videoMessage
|
|
||||||
} else {
|
|
||||||
videoMessage.value = {
|
|
||||||
videoType: 'RUNWAY',
|
|
||||||
prompt: ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@ -135,4 +138,18 @@ onMounted(() => {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
resize: none;
|
resize: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.task-id-span {
|
||||||
|
display: inline-block;
|
||||||
|
max-width: 80px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
padding: 2px 6px;
|
||||||
|
background-color: var(--primary-color-suppl);
|
||||||
|
color: var(--primary-color);
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
<div class="video-thumbnail-container">
|
<div class="video-thumbnail-container">
|
||||||
<div v-if="videoList.length === 0" class="no-video-thumbnail">
|
<div v-if="videoList.length === 0" class="no-video-thumbnail">
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
<n-text depth="3">暂无视频</n-text>
|
<n-empty description="暂可选视频" size="small" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -15,7 +15,7 @@
|
|||||||
class="thumbnail-item"
|
class="thumbnail-item"
|
||||||
@click="handleShowModal"
|
@click="handleShowModal"
|
||||||
>
|
>
|
||||||
<video :src="video" class="thumbnail-video" muted preload="metadata" />
|
<video :src="video.localPath" class="thumbnail-video" muted preload="metadata" />
|
||||||
|
|
||||||
<!-- 如果有更多视频,在最后一个缩略图上显示数量 -->
|
<!-- 如果有更多视频,在最后一个缩略图上显示数量 -->
|
||||||
<div v-if="index === 2 && videoList.length > 3" class="more-count">
|
<div v-if="index === 2 && videoList.length > 3" class="more-count">
|
||||||
@ -34,10 +34,18 @@
|
|||||||
:title="'视频信息详情:' + taskData.name"
|
:title="'视频信息详情:' + taskData.name"
|
||||||
size="huge"
|
size="huge"
|
||||||
:segmented="true"
|
:segmented="true"
|
||||||
|
:content-style="{
|
||||||
|
padding: '8px 16px'
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<n-alert
|
||||||
|
type="info"
|
||||||
|
closable
|
||||||
|
:show-icon="false"
|
||||||
|
:style="{
|
||||||
|
marginBottom: '16px'
|
||||||
|
}"
|
||||||
>
|
>
|
||||||
<n-alert type="info" closable :show-icon="false" :style="{
|
|
||||||
marginBottom : '16px'
|
|
||||||
}">
|
|
||||||
修改选择的视频之后,请点击 保存视频选择 按钮将操作进行保存生效!!!
|
修改选择的视频之后,请点击 保存视频选择 按钮将操作进行保存生效!!!
|
||||||
</n-alert>
|
</n-alert>
|
||||||
|
|
||||||
@ -202,15 +210,31 @@
|
|||||||
size="small"
|
size="small"
|
||||||
:bordered="true"
|
:bordered="true"
|
||||||
class="video-card"
|
class="video-card"
|
||||||
:class="{ selected: currentSelectedVideo === video }"
|
:class="{ selected: currentSelectedVideo === video.localPath }"
|
||||||
@click="selectVideo(video)"
|
@click="selectVideo(video)"
|
||||||
>
|
>
|
||||||
<template #header>
|
<template #header>
|
||||||
<n-text>视频 {{ index + 1 }}</n-text>
|
<n-text>视频 {{ index + 1 }}</n-text>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template #header-extra>
|
||||||
|
<n-space justify="center" align="center">
|
||||||
|
<n-tooltip :show-arrow="false" trigger="hover">
|
||||||
|
<template #trigger>
|
||||||
|
<n-tag type="primary" size="small" class="video-status-tag task-id-tag">
|
||||||
|
{{ video.taskId }}
|
||||||
|
</n-tag>
|
||||||
|
</template>
|
||||||
|
{{ video.taskId }}
|
||||||
|
</n-tooltip>
|
||||||
|
<n-tag type="info" size="small" class="video-status-tag">
|
||||||
|
{{ video.index }}
|
||||||
|
</n-tag>
|
||||||
|
</n-space>
|
||||||
|
</template>
|
||||||
|
|
||||||
<div class="video-container">
|
<div class="video-container">
|
||||||
<video :src="video" class="video-player" preload="metadata" />
|
<video :src="video.localPath" class="video-player" preload="metadata" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
@ -235,19 +259,32 @@
|
|||||||
<n-text depth="3">暂无视频信息</n-text>
|
<n-text depth="3">暂无视频信息</n-text>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<n-empty description="当前分镜没有转视频相关的信息,请先进行转视频操作!" v-else> </n-empty>
|
<n-empty description="当前分镜没有转视频相关的信息,请先进行转视频操作!" v-else />
|
||||||
</n-modal>
|
</n-modal>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||||
import { NModal, NText, NCard, NGrid, NGridItem, NSpace, NTag, NIcon, NButton } from 'naive-ui'
|
import {
|
||||||
|
NModal,
|
||||||
|
NAlert,
|
||||||
|
NText,
|
||||||
|
NCard,
|
||||||
|
NGrid,
|
||||||
|
NGridItem,
|
||||||
|
NSpace,
|
||||||
|
NTag,
|
||||||
|
NIcon,
|
||||||
|
NButton,
|
||||||
|
NTooltip,
|
||||||
|
NEmpty,
|
||||||
|
useMessage
|
||||||
|
} from 'naive-ui'
|
||||||
import { GetImageToVideoModelsLabel } from '@/define/enum/video'
|
import { GetImageToVideoModelsLabel } from '@/define/enum/video'
|
||||||
import { isEmpty } from 'lodash'
|
import { isEmpty } from 'lodash'
|
||||||
|
|
||||||
import { Save } from '@vicons/ionicons5'
|
import { Save } from '@vicons/ionicons5'
|
||||||
import { TimeDelay } from '@/define/Tools/time'
|
|
||||||
|
|
||||||
// 定义 props
|
// 定义 props
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@ -261,6 +298,11 @@ const props = defineProps({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const videoList = computed(() => {
|
||||||
|
// 过滤掉没有本地路径的视频
|
||||||
|
return props.videoList.filter((video) => !isEmpty(video.localPath))
|
||||||
|
})
|
||||||
|
|
||||||
// 弹窗显示状态
|
// 弹窗显示状态
|
||||||
const showModal = ref(false)
|
const showModal = ref(false)
|
||||||
|
|
||||||
@ -295,6 +337,7 @@ const handleResize = () => {
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
window.addEventListener('resize', handleResize)
|
window.addEventListener('resize', handleResize)
|
||||||
videoMessage.value = props.taskData.videoMessage || {}
|
videoMessage.value = props.taskData.videoMessage || {}
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
@ -304,8 +347,8 @@ onUnmounted(() => {
|
|||||||
// 显示弹窗
|
// 显示弹窗
|
||||||
function handleShowModal() {
|
function handleShowModal() {
|
||||||
// 默认选中第一个视频
|
// 默认选中第一个视频
|
||||||
if (props.videoList.length > 0 && isEmpty(props.taskData.generateVideoPath)) {
|
if (videoList.value.length > 0 && isEmpty(props.taskData.generateVideoPath)) {
|
||||||
currentSelectedVideo.value = props.videoList[0]
|
currentSelectedVideo.value = videoList.value[0].localPath
|
||||||
} else {
|
} else {
|
||||||
currentSelectedVideo.value = props.taskData.generateVideoPath
|
currentSelectedVideo.value = props.taskData.generateVideoPath
|
||||||
}
|
}
|
||||||
@ -314,7 +357,7 @@ function handleShowModal() {
|
|||||||
|
|
||||||
// 选择视频
|
// 选择视频
|
||||||
function selectVideo(video) {
|
function selectVideo(video) {
|
||||||
currentSelectedVideo.value = video
|
currentSelectedVideo.value = video.localPath
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleSaveVideoSelection() {
|
async function handleSaveVideoSelection() {
|
||||||
@ -412,7 +455,7 @@ async function copyTaskId() {
|
|||||||
/* 视频缩略图样式 */
|
/* 视频缩略图样式 */
|
||||||
.video-thumbnail-container {
|
.video-thumbnail-container {
|
||||||
height: 130px;
|
height: 130px;
|
||||||
width: 100%;
|
width: 99%;
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -430,10 +473,11 @@ async function copyTaskId() {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
background-color: #f5f5f5;
|
background-color: var(--n-color-embedded);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
color: #999;
|
color: var(--n-text-color-disabled);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
border: 1px solid var(--n-border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.thumbnail-grid {
|
.thumbnail-grid {
|
||||||
@ -492,8 +536,9 @@ async function copyTaskId() {
|
|||||||
gap: 20px;
|
gap: 20px;
|
||||||
margin-bottom: 30px;
|
margin-bottom: 30px;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
background-color: #f8f9fa;
|
background-color: var(--n-color-embedded);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
border: 1px solid var(--n-border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.selected-video-left {
|
.selected-video-left {
|
||||||
@ -524,8 +569,9 @@ async function copyTaskId() {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
background-color: #f5f5f5;
|
background-color: var(--n-color-embedded-popover);
|
||||||
color: #999;
|
color: var(--n-text-color-disabled);
|
||||||
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-info-right {
|
.task-info-right {
|
||||||
@ -548,15 +594,36 @@ async function copyTaskId() {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
background: #f8f9fa;
|
background: var(--n-color-embedded);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
border-left: 4px solid #18a058;
|
border-left: 4px solid #18a058;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
|
border: 1px solid var(--n-border-color);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 5px;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(to bottom, #18a058, #36ad6a);
|
||||||
|
transition: width 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-item:hover {
|
.info-item:hover {
|
||||||
background: #e8f5e8;
|
background: var(--n-color-embedded-popover);
|
||||||
transform: translateX(4px);
|
transform: translateX(4px);
|
||||||
|
border-color: #18a058;
|
||||||
|
border-left-color: #0c7a43;
|
||||||
|
box-shadow: 0 2px 8px rgba(24, 160, 88, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item:hover::before {
|
||||||
|
width: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-label {
|
.info-label {
|
||||||
@ -634,13 +701,13 @@ async function copyTaskId() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.video-card.selected :deep(.n-card-header) {
|
.video-card.selected :deep(.n-card-header) {
|
||||||
background-color: #f0f9ff;
|
background-color: var(--n-color-embedded-popover);
|
||||||
}
|
}
|
||||||
|
|
||||||
.video-container {
|
.video-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
aspect-ratio: 16/9;
|
aspect-ratio: 16/9;
|
||||||
background-color: #f5f5f5;
|
background-color: var(--n-color-embedded-popover);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -657,7 +724,7 @@ async function copyTaskId() {
|
|||||||
.no-video {
|
.no-video {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 40px;
|
padding: 40px;
|
||||||
color: #999;
|
color: var(--n-text-color-disabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 响应式设计 */
|
/* 响应式设计 */
|
||||||
@ -728,4 +795,19 @@ async function copyTaskId() {
|
|||||||
gap: 24px;
|
gap: 24px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Task ID Tag 样式 */
|
||||||
|
.task-id-tag {
|
||||||
|
max-width: 120px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
/* display: inline-block; */
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-id-tag :deep(.n-tag__content) {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -184,7 +184,7 @@ async function handleDelete() {
|
|||||||
let res = await window.options.ModifyOptionByKey(
|
let res = await window.options.ModifyOptionByKey(
|
||||||
OptionKeyName.MJ_CustomAPISetting,
|
OptionKeyName.MJ_CustomAPISetting,
|
||||||
JSON.stringify(configList.value),
|
JSON.stringify(configList.value),
|
||||||
OptionType.JOSN
|
OptionType.JSON
|
||||||
)
|
)
|
||||||
if (res.code != 1) {
|
if (res.code != 1) {
|
||||||
message.error('删除配置失败,' + res.message)
|
message.error('删除配置失败,' + res.message)
|
||||||
@ -225,7 +225,7 @@ const handleSave = (e) => {
|
|||||||
let res = await window.options.ModifyOptionByKey(
|
let res = await window.options.ModifyOptionByKey(
|
||||||
OptionKeyName.MJ_CustomAPISetting,
|
OptionKeyName.MJ_CustomAPISetting,
|
||||||
JSON.stringify(configList.value),
|
JSON.stringify(configList.value),
|
||||||
OptionType.JOSN
|
OptionType.JSON
|
||||||
)
|
)
|
||||||
if (res.code != 1) {
|
if (res.code != 1) {
|
||||||
throw new Error(res.message)
|
throw new Error(res.message)
|
||||||
|
|||||||
@ -196,7 +196,7 @@ async function handleDelete() {
|
|||||||
let res = await window.options.ModifyOptionByKey(
|
let res = await window.options.ModifyOptionByKey(
|
||||||
OptionKeyName.MJ_CustomPackageSetting,
|
OptionKeyName.MJ_CustomPackageSetting,
|
||||||
JSON.stringify(configList.value),
|
JSON.stringify(configList.value),
|
||||||
OptionType.JOSN
|
OptionType.JSON
|
||||||
)
|
)
|
||||||
if (res.code != 1) {
|
if (res.code != 1) {
|
||||||
message.error('删除配置失败,' + res.message)
|
message.error('删除配置失败,' + res.message)
|
||||||
@ -238,7 +238,7 @@ const handleSave = (e) => {
|
|||||||
let res = await window.options.ModifyOptionByKey(
|
let res = await window.options.ModifyOptionByKey(
|
||||||
OptionKeyName.MJ_CustomPackageSetting,
|
OptionKeyName.MJ_CustomPackageSetting,
|
||||||
JSON.stringify(configList.value),
|
JSON.stringify(configList.value),
|
||||||
OptionType.JOSN
|
OptionType.JSON
|
||||||
)
|
)
|
||||||
if (res.code != 1) {
|
if (res.code != 1) {
|
||||||
throw new Error(res.message)
|
throw new Error(res.message)
|
||||||
|
|||||||
@ -138,7 +138,7 @@ async function saveWorkflow() {
|
|||||||
let res = await window.options.ModifyOptionByKey(
|
let res = await window.options.ModifyOptionByKey(
|
||||||
OptionKeyName.ComfyUI_WorkFlowSetting,
|
OptionKeyName.ComfyUI_WorkFlowSetting,
|
||||||
JSON.stringify(optionStore.ComfyUI_WorkFlowSetting),
|
JSON.stringify(optionStore.ComfyUI_WorkFlowSetting),
|
||||||
OptionType.JOSN
|
OptionType.JSON
|
||||||
)
|
)
|
||||||
|
|
||||||
if (res.code == 1) {
|
if (res.code == 1) {
|
||||||
|
|||||||
@ -135,7 +135,7 @@ async function SaveComfyUISimpleSetting() {
|
|||||||
let res = await window.options.ModifyOptionByKey(
|
let res = await window.options.ModifyOptionByKey(
|
||||||
OptionKeyName.ComfyUI_SimpleSetting,
|
OptionKeyName.ComfyUI_SimpleSetting,
|
||||||
JSON.stringify(optionStore.ComfyUI_SimpleSetting),
|
JSON.stringify(optionStore.ComfyUI_SimpleSetting),
|
||||||
OptionType.JOSN
|
OptionType.JSON
|
||||||
)
|
)
|
||||||
if (res.code == 1) {
|
if (res.code == 1) {
|
||||||
message.success('保存设置成功')
|
message.success('保存设置成功')
|
||||||
@ -269,7 +269,7 @@ const handleRemove = (row) => {
|
|||||||
let res = await window.options.ModifyOptionByKey(
|
let res = await window.options.ModifyOptionByKey(
|
||||||
OptionKeyName.ComfyUI_WorkFlowSetting,
|
OptionKeyName.ComfyUI_WorkFlowSetting,
|
||||||
JSON.stringify(optionStore.ComfyUI_WorkFlowSetting),
|
JSON.stringify(optionStore.ComfyUI_WorkFlowSetting),
|
||||||
OptionType.JOSN
|
OptionType.JSON
|
||||||
)
|
)
|
||||||
|
|
||||||
if (res.code == 0) {
|
if (res.code == 0) {
|
||||||
@ -280,7 +280,7 @@ const handleRemove = (row) => {
|
|||||||
res = await window.options.ModifyOptionByKey(
|
res = await window.options.ModifyOptionByKey(
|
||||||
OptionKeyName.ComfyUI_SimpleSetting,
|
OptionKeyName.ComfyUI_SimpleSetting,
|
||||||
JSON.stringify(optionStore.ComfyUI_SimpleSetting),
|
JSON.stringify(optionStore.ComfyUI_SimpleSetting),
|
||||||
OptionType.JOSN
|
OptionType.JSON
|
||||||
)
|
)
|
||||||
|
|
||||||
if (res.code == 1) {
|
if (res.code == 1) {
|
||||||
|
|||||||
49
src/renderer/src/components/Test/TextEllipsisTest.vue
Normal file
49
src/renderer/src/components/Test/TextEllipsisTest.vue
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
<template>
|
||||||
|
<div style="padding: 20px;">
|
||||||
|
<h3>TextEllipsis 组件测试</h3>
|
||||||
|
|
||||||
|
<div style="margin: 20px 0;">
|
||||||
|
<h4>测试 n-tag 组件 (120px 宽度)</h4>
|
||||||
|
<TextEllipsis
|
||||||
|
text="这是一个很长的任务ID用来测试省略号效果12345678901234567890"
|
||||||
|
component="n-tag"
|
||||||
|
:component-props="{ type: 'primary', size: 'small' }"
|
||||||
|
max-width="120px"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin: 20px 0;">
|
||||||
|
<h4>测试 n-text 组件 (100px 宽度)</h4>
|
||||||
|
<TextEllipsis
|
||||||
|
text="这是一个很长的文本用来测试省略号效果"
|
||||||
|
component="n-text"
|
||||||
|
max-width="100px"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin: 20px 0;">
|
||||||
|
<h4>短文本不显示tooltip</h4>
|
||||||
|
<TextEllipsis
|
||||||
|
text="短文本"
|
||||||
|
component="n-tag"
|
||||||
|
:component-props="{ type: 'success', size: 'small' }"
|
||||||
|
max-width="120px"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin: 20px 0;">
|
||||||
|
<h4>强制显示tooltip</h4>
|
||||||
|
<TextEllipsis
|
||||||
|
text="短文本"
|
||||||
|
component="n-tag"
|
||||||
|
:component-props="{ type: 'warning', size: 'small' }"
|
||||||
|
max-width="120px"
|
||||||
|
:force-tooltip="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import TextEllipsis from '../Common/TextEllipsis.vue'
|
||||||
|
</script>
|
||||||
@ -1,10 +1,11 @@
|
|||||||
import { messageDark, useMessage } from 'naive-ui'
|
import { messageDark, useMessage } from 'naive-ui'
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { errorMessage, successMessage } from '../main/Public/generalTools'
|
import { errorMessage, successMessage } from '../main/Public/generalTools'
|
||||||
import { BookTaskStatus } from '../define/enum/bookEnum'
|
import { BookImageCategory, BookTaskStatus } from '../define/enum/bookEnum'
|
||||||
import { Book } from '../model/book/book'
|
import { Book } from '../model/book/book'
|
||||||
import { GeneralResponse } from '../model/generalResponse'
|
import { GeneralResponse } from '../model/generalResponse'
|
||||||
import { PresetModel } from '../model/preset'
|
import { PresetModel } from '../model/preset'
|
||||||
|
import { ImageToVideoModels } from '@/define/enum/video'
|
||||||
|
|
||||||
// 系统相关设置
|
// 系统相关设置
|
||||||
export const useReverseManageStore = defineStore('reverseManage', {
|
export const useReverseManageStore = defineStore('reverseManage', {
|
||||||
@ -12,36 +13,37 @@ export const useReverseManageStore = defineStore('reverseManage', {
|
|||||||
bookType: [],
|
bookType: [],
|
||||||
bookData: [], // 当前显示的所有小说数据
|
bookData: [], // 当前显示的所有小说数据
|
||||||
selectBook: {
|
selectBook: {
|
||||||
id: null,
|
id: undefined,
|
||||||
name: null,
|
name: undefined,
|
||||||
bookFolderPath: null,
|
bookFolderPath: undefined,
|
||||||
type: null,
|
type: undefined,
|
||||||
oldVideoPath: null,
|
oldVideoPath: undefined,
|
||||||
srtPath: null,
|
srtPath: undefined,
|
||||||
audioPath: null,
|
audioPath: undefined,
|
||||||
imageFolder: null,
|
imageFolder: undefined,
|
||||||
subtitlePosition: null
|
subtitlePosition: undefined
|
||||||
} as Book.SelectBook, // 当前选中的小说
|
} as Book.SelectBook, // 当前选中的小说
|
||||||
|
|
||||||
bookTaskData: [] as Book.SelectBookTask[], // 当前显示的所有小说任务数据
|
bookTaskData: [] as Book.SelectBookTask[], // 当前显示的所有小说任务数据
|
||||||
|
|
||||||
selectBookTask: {
|
selectBookTask: {
|
||||||
no: null,
|
no: undefined,
|
||||||
id: null,
|
id: undefined,
|
||||||
bookId: null,
|
bookId: undefined,
|
||||||
name: null,
|
name: undefined,
|
||||||
generateVideoPath: null,
|
generateVideoPath: undefined,
|
||||||
srtPath: null,
|
srtPath: undefined,
|
||||||
audioPath: null,
|
audioPath: undefined,
|
||||||
draftSrtStyle: null, // 草稿字幕样式
|
draftSrtStyle: undefined, // 草稿字幕样式
|
||||||
backgroundMusic: null, // 背景音乐ID
|
backgroundMusic: undefined, // 背景音乐ID
|
||||||
friendlyReminder: null, // 友情提示
|
friendlyReminder: undefined, // 友情提示
|
||||||
imageFolder: null,
|
imageFolder: undefined,
|
||||||
styleList: null,
|
styleList: undefined,
|
||||||
prefix: null,
|
prefix: undefined,
|
||||||
imageCategory: null,
|
imageCategory: undefined,
|
||||||
|
videoCategory: undefined,
|
||||||
status: BookTaskStatus.WAIT,
|
status: BookTaskStatus.WAIT,
|
||||||
errorMsg: null,
|
errorMsg: undefined,
|
||||||
openVideoGenerate: false
|
openVideoGenerate: false
|
||||||
} as Book.SelectBookTask// 当前选中的小说任务
|
} as Book.SelectBookTask// 当前选中的小说任务
|
||||||
,
|
,
|
||||||
@ -62,6 +64,22 @@ export const useReverseManageStore = defineStore('reverseManage', {
|
|||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
|
|
||||||
|
resetSelectBook() {
|
||||||
|
|
||||||
|
this.selectBook = {
|
||||||
|
id: undefined,
|
||||||
|
name: undefined,
|
||||||
|
bookFolderPath: undefined,
|
||||||
|
type: undefined,
|
||||||
|
oldVideoPath: undefined,
|
||||||
|
srtPath: undefined,
|
||||||
|
audioPath: undefined,
|
||||||
|
imageFolder: undefined,
|
||||||
|
subtitlePosition: undefined
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
//#region 更新小说批次任务数据
|
//#region 更新小说批次任务数据
|
||||||
/** 更新小说批次任务数据 */
|
/** 更新小说批次任务数据 */
|
||||||
UpdatedBookTaskData(bookTaskId: string | string[], data: Book.SelectBookTask) {
|
UpdatedBookTaskData(bookTaskId: string | string[], data: Book.SelectBookTask) {
|
||||||
@ -101,6 +119,7 @@ export const useReverseManageStore = defineStore('reverseManage', {
|
|||||||
if (res.data.res_book.length <= 0) {
|
if (res.data.res_book.length <= 0) {
|
||||||
throw new Error('没有找到对应的小说数据,请先添加小说')
|
throw new Error('没有找到对应的小说数据,请先添加小说')
|
||||||
}
|
}
|
||||||
|
debugger
|
||||||
this.SetBookData(res.data.res_book)
|
this.SetBookData(res.data.res_book)
|
||||||
this.selectBook = res.data.res_book[0]
|
this.selectBook = res.data.res_book[0]
|
||||||
return successMessage(res.data)
|
return successMessage(res.data)
|
||||||
@ -119,26 +138,29 @@ export const useReverseManageStore = defineStore('reverseManage', {
|
|||||||
if (res.code == 0) {
|
if (res.code == 0) {
|
||||||
throw new Error(res.message)
|
throw new Error(res.message)
|
||||||
}
|
}
|
||||||
|
console.log('获取小说批次任务数据:', res);
|
||||||
if (res.data.bookTasks.length > 0) {
|
if (res.data.bookTasks.length > 0) {
|
||||||
this.bookTaskData = res.data.bookTasks
|
this.bookTaskData = res.data.bookTasks
|
||||||
this.selectBookTask = res.data.bookTasks[0]
|
this.selectBookTask = res.data.bookTasks[0]
|
||||||
} else {
|
} else {
|
||||||
this.selectBookTask = {
|
this.selectBookTask = {
|
||||||
no: null,
|
no: undefined,
|
||||||
id: null,
|
id: undefined,
|
||||||
bookId: null,
|
bookId: undefined,
|
||||||
name: null,
|
name: undefined,
|
||||||
generateVideoPath: null,
|
generateVideoPath: undefined,
|
||||||
srtPath: null,
|
srtPath: undefined,
|
||||||
audioPath: null,
|
audioPath: undefined,
|
||||||
draftSrtStyle: null, // 草稿字幕样式
|
draftSrtStyle: undefined, // 草稿字幕样式
|
||||||
backgroundMusic: null, // 背景音乐ID
|
backgroundMusic: undefined, // 背景音乐ID
|
||||||
friendlyReminder: null, // 友情提示
|
friendlyReminder: undefined, // 友情提示
|
||||||
imageFolder: null,
|
imageFolder: undefined,
|
||||||
styleList: null,
|
imageCategory: BookImageCategory.MJ,
|
||||||
prefix: null,
|
videoCategory: ImageToVideoModels.MJ_VIDEO,
|
||||||
|
styleList: undefined,
|
||||||
|
prefix: undefined,
|
||||||
status: BookTaskStatus.WAIT,
|
status: BookTaskStatus.WAIT,
|
||||||
errorMsg: null
|
errorMsg: undefined
|
||||||
} as Book.SelectBookTask
|
} as Book.SelectBookTask
|
||||||
throw new Error('没有找到对应的子批次数据,请先创建')
|
throw new Error('没有找到对应的子批次数据,请先创建')
|
||||||
}
|
}
|
||||||
@ -259,6 +281,7 @@ export const useReverseManageStore = defineStore('reverseManage', {
|
|||||||
try {
|
try {
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
let detailRes = await window.book.GetBookTaskDetail(bookTaskId)
|
let detailRes = await window.book.GetBookTaskDetail(bookTaskId)
|
||||||
|
console.log('获取小说任务详细数据222222222:', detailRes);
|
||||||
let bookTaskDetail = []
|
let bookTaskDetail = []
|
||||||
if (detailRes.code == 1) {
|
if (detailRes.code == 1) {
|
||||||
bookTaskDetail = detailRes.data.map(item => {
|
bookTaskDetail = detailRes.data.map(item => {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user