优化 MJ 转视频
优化视频的显示 添加图片的上传
This commit is contained in:
parent
8bc60256ba
commit
d94e21b3b2
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "laitool-pro",
|
"name": "laitool-pro",
|
||||||
"productName": "来推 Pro",
|
"productName": "来推 Pro",
|
||||||
"version": "v3.4.5",
|
"version": "v3.4.6",
|
||||||
"description": "来推 Pro - 一款集音频处理、文案生成、图片生成、视频生成等功能于一体的多合一AI工具软件。",
|
"description": "来推 Pro - 一款集音频处理、文案生成、图片生成、视频生成等功能于一体的多合一AI工具软件。",
|
||||||
"main": "./out/main/index.js",
|
"main": "./out/main/index.js",
|
||||||
"author": "xiangbei",
|
"author": "xiangbei",
|
||||||
|
|||||||
@ -9,7 +9,7 @@ const fspromises = fs.promises
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 判断文件或目录是否存在
|
* 判断文件或目录是否存在
|
||||||
* @param {*} path 文件或目录的路径
|
* @param {*} filePath 文件或目录的路径
|
||||||
* @returns true表示存在,false表示不存在
|
* @returns true表示存在,false表示不存在
|
||||||
*/
|
*/
|
||||||
export async function CheckFileOrDirExist(filePath) {
|
export async function CheckFileOrDirExist(filePath) {
|
||||||
|
|||||||
@ -67,7 +67,7 @@ export class BookTaskDetailService extends RealmBaseService {
|
|||||||
oldImage: JoinPath(projectPath, item.oldImage),
|
oldImage: JoinPath(projectPath, item.oldImage),
|
||||||
outImagePath: JoinPath(projectPath, item.outImagePath),
|
outImagePath: JoinPath(projectPath, item.outImagePath),
|
||||||
subImagePath: (item.subImagePath as string[])?.map((subImage) => {
|
subImagePath: (item.subImagePath as string[])?.map((subImage) => {
|
||||||
return JoinPath(projectPath, subImage)
|
return JoinPath(projectPath, subImage) + '?t=' + new Date().getTime()
|
||||||
}),
|
}),
|
||||||
subVideoPath: (item.subVideoPath as string[]).map((subVideo) => subVideo.toString()),
|
subVideoPath: (item.subVideoPath as string[]).map((subVideo) => subVideo.toString()),
|
||||||
subVideoPathObject: (item.subVideoPath as string[])?.map((subVideo) => {
|
subVideoPathObject: (item.subVideoPath as string[])?.map((subVideo) => {
|
||||||
|
|||||||
@ -52,6 +52,7 @@ export const GetImageToVideoModelsLabel = (model: ImageToVideoModels | string) =
|
|||||||
case ImageToVideoModels.PIKA:
|
case ImageToVideoModels.PIKA:
|
||||||
return 'Pika'
|
return 'Pika'
|
||||||
case ImageToVideoModels.MJ_VIDEO:
|
case ImageToVideoModels.MJ_VIDEO:
|
||||||
|
case ImageToVideoModels.MJ_VIDEO_EXTEND:
|
||||||
return t('MJ视频')
|
return t('MJ视频')
|
||||||
default:
|
default:
|
||||||
return '未知'
|
return '未知'
|
||||||
|
|||||||
@ -48,5 +48,17 @@ export function bookImageIpc() {
|
|||||||
await bookHandle.DownloadImageUrlAndSplit(bookTaskDetailId, imageUrl)
|
await bookHandle.DownloadImageUrlAndSplit(bookTaskDetailId, imageUrl)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/** 同步主图文件到批次任务 */
|
||||||
|
ipcMain.handle(
|
||||||
|
DEFINE_STRING.BOOK.SYNC_MAIN_IMAGE_FOR_BOOK_TASK,
|
||||||
|
async (_, bookTaskId: string) => await bookHandle.SyncMainImageForBookTask(bookTaskId)
|
||||||
|
)
|
||||||
|
|
||||||
|
/** 同步子图文件到批次任务 */
|
||||||
|
ipcMain.handle(
|
||||||
|
DEFINE_STRING.BOOK.SYNC_SUB_IMAGE_FOR_BOOK_TASK,
|
||||||
|
async (_, bookTaskId: string) => await bookHandle.SyncSubImageForBookTask(bookTaskId)
|
||||||
|
)
|
||||||
|
|
||||||
//#endregion
|
//#endregion
|
||||||
}
|
}
|
||||||
|
|||||||
@ -133,6 +133,13 @@ function SystemIpc() {
|
|||||||
async (_, filePath: string) => await electronInterface.ReadTextFile(filePath)
|
async (_, filePath: string) => await electronInterface.ReadTextFile(filePath)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/** 上传图片到LaiTool云端 */
|
||||||
|
ipcMain.handle(
|
||||||
|
DEFINE_STRING.SYSTEM.UPLOAD_IMAGE_TO_LAITOOL,
|
||||||
|
async (_, imagePath: string, type: "video" | "image") =>
|
||||||
|
await electronInterface.UploadImageToLaiTool(imagePath, type)
|
||||||
|
)
|
||||||
|
|
||||||
//#endregion
|
//#endregion
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -136,6 +136,12 @@ const BOOK = {
|
|||||||
|
|
||||||
/** 下载图片并拆分处理应用到分镜 */
|
/** 下载图片并拆分处理应用到分镜 */
|
||||||
DOWNLOAD_IMAGE_URL_AND_SPLIT: 'DOWNLOAD_IMAGE_URL_AND_SPLIT',
|
DOWNLOAD_IMAGE_URL_AND_SPLIT: 'DOWNLOAD_IMAGE_URL_AND_SPLIT',
|
||||||
|
|
||||||
|
/** 同步主图文件到批次任务 */
|
||||||
|
SYNC_MAIN_IMAGE_FOR_BOOK_TASK: 'SYNC_MAIN_IMAGE_FOR_BOOK_TASK',
|
||||||
|
|
||||||
|
/** 同步子图文件到批次任务 */
|
||||||
|
SYNC_SUB_IMAGE_FOR_BOOK_TASK: 'SYNC_SUB_IMAGE_FOR_BOOK_TASK',
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
//#region 导出相关
|
//#region 导出相关
|
||||||
|
|||||||
@ -51,6 +51,9 @@ const SYSTEM = {
|
|||||||
/** 读取文本文件内容 */
|
/** 读取文本文件内容 */
|
||||||
READ_TEXT_FILE: 'READ_TEXT_FILE',
|
READ_TEXT_FILE: 'READ_TEXT_FILE',
|
||||||
|
|
||||||
|
/** 上传图片到LaiTool云端 */
|
||||||
|
UPLOAD_IMAGE_TO_LAITOOL: 'UPLOAD_IMAGE_TO_LAITOOL',
|
||||||
|
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
/** 打开指定的url */
|
/** 打开指定的url */
|
||||||
|
|||||||
3
src/define/model/book/bookTaskDetail.d.ts
vendored
3
src/define/model/book/bookTaskDetail.d.ts
vendored
@ -137,6 +137,9 @@ declare namespace BookTaskDetail {
|
|||||||
*/
|
*/
|
||||||
endImageUrl?: string
|
endImageUrl?: string
|
||||||
|
|
||||||
|
/** 视频拓展时的尾帧图片 */
|
||||||
|
extendEndImageUrl?: string
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 是否首尾循环
|
* 是否首尾循环
|
||||||
*/
|
*/
|
||||||
|
|||||||
36
src/define/model/book/bookVideo.d.ts
vendored
36
src/define/model/book/bookVideo.d.ts
vendored
@ -10,4 +10,40 @@ declare namespace BookVideo {
|
|||||||
bookId?: string // 小说ID
|
bookId?: string // 小说ID
|
||||||
bookTaskId?: string // 小说任务ID
|
bookTaskId?: string // 小说任务ID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 视频生成请求参数接口
|
||||||
|
*
|
||||||
|
* @interface VideoGenerateRequest
|
||||||
|
* @property {string | null} [prompt] - 生成提示词,可选
|
||||||
|
* @property {'low' | 'high'} motion - 运动幅度,必需
|
||||||
|
* @property {'vid_1.1_i2v_480' | 'vid_1.1_i2v_720'} videoType - 视频类型,必需
|
||||||
|
* @property {'url' | 'base64'} image - 首帧图片格式,扩展时可为空,必需
|
||||||
|
* @property {string} [endImage] - 尾帧图片,可选
|
||||||
|
* @property {boolean} [loop] - 是否循环播放,可选
|
||||||
|
* @property {1 | 2 | 4} [batchSize] - 批次大小,可选,默认值为4
|
||||||
|
* @property {'extend'} [action] - 对视频任务进行操作,不为空时index、taskId必填,可选
|
||||||
|
* @property {number} [index] - 执行的视频索引号,范围0-3,可选
|
||||||
|
* @property {string} [taskId] - 需要操作的视频父任务ID,可选
|
||||||
|
* @property {string} [state] - 状态,可选
|
||||||
|
* @property {string} [notifyHook] - 回调地址,可选
|
||||||
|
* @property {boolean} [noStorage] - True时返回官方链接,可选
|
||||||
|
*/
|
||||||
|
interface MJVideoGenerateRequest {
|
||||||
|
prompt?: string | null
|
||||||
|
motion: 'low' | 'high'
|
||||||
|
videoType: 'vid_1.1_i2v_480' | 'vid_1.1_i2v_720'
|
||||||
|
image?: string
|
||||||
|
endImage?: string
|
||||||
|
loop?: boolean
|
||||||
|
batchSize?: 1 | 2 | 4
|
||||||
|
action?: 'extend'
|
||||||
|
index?: number // 0-3
|
||||||
|
taskId?: string
|
||||||
|
state?: string
|
||||||
|
notifyHook?: string
|
||||||
|
noStorage?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
16
src/define/model/image.d.ts
vendored
Normal file
16
src/define/model/image.d.ts
vendored
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
declare namespace ImageModel {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件上传的请求体
|
||||||
|
*/
|
||||||
|
interface FileUploadRequest {
|
||||||
|
/** 文件内容 - Base64编码或二进制数据 */
|
||||||
|
file: string
|
||||||
|
/** 文件名称 - 包含扩展名的完整文件名 */
|
||||||
|
fileName: string
|
||||||
|
/** 文件内容类型 - MIME类型,如 'image/jpeg', 'image/png' 等 */
|
||||||
|
contentType: string
|
||||||
|
/** 文件类型分类 - 用于服务器端分类处理,如 'image', 'video' 等 */
|
||||||
|
type?: string
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,6 +2,7 @@ export default {
|
|||||||
//#region 通用
|
//#region 通用
|
||||||
"是": "Yes",
|
"是": "Yes",
|
||||||
"否": 'No',
|
"否": 'No',
|
||||||
|
"空": 'Empty',
|
||||||
"操作": 'Action',
|
"操作": 'Action',
|
||||||
"清空": 'Clear',
|
"清空": 'Clear',
|
||||||
"生成": 'Generate',
|
"生成": 'Generate',
|
||||||
@ -227,6 +228,10 @@ export default {
|
|||||||
"初始视频消息失败,未找到指定小说批次任务的分镜数据": "Failed to initialize video message, storyboard data for specified novel batch task not found",
|
"初始视频消息失败,未找到指定小说批次任务的分镜数据": "Failed to initialize video message, storyboard data for specified novel batch task not found",
|
||||||
'根据ID获取小说批次任务信息失败,ID不能为空!': 'Failed to retrieve novel batch task information by ID, ID cannot be empty!',
|
'根据ID获取小说批次任务信息失败,ID不能为空!': 'Failed to retrieve novel batch task information by ID, ID cannot be empty!',
|
||||||
"未找到对应的小说批次任务信息,请检查!": "Corresponding novel batch task information not found, please check!",
|
"未找到对应的小说批次任务信息,请检查!": "Corresponding novel batch task information not found, please check!",
|
||||||
|
"同步主图文件失败,{error}": "Synchronization of main image file failed, {error}",
|
||||||
|
"同步分镜主图文件成功,同步成功数 {count},同步分镜名称 {name}": "Storyboard main image files synchronized successfully, {count} images synchronized, storyboard names: {name}",
|
||||||
|
"同步分镜子图文件成功,同步成功数 {count},同步分镜名称 {name}": "Storyboard sub-image files synchronized successfully, {count} images synchronized, storyboard names: {name}",
|
||||||
|
"同步分镜子图文件失败,{error}": "Synchronization of sub-image files failed, {error}",
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
//#region 小说分镜
|
//#region 小说分镜
|
||||||
@ -298,6 +303,12 @@ export default {
|
|||||||
"反推提示词的ID不能为空": "Reverse prompt ID cannot be empty",
|
"反推提示词的ID不能为空": "Reverse prompt ID cannot be empty",
|
||||||
"全部翻译完成": "All translation completed",
|
"全部翻译完成": "All translation completed",
|
||||||
"翻译失败,{error}": "Translation failed, {error}",
|
"翻译失败,{error}": "Translation failed, {error}",
|
||||||
|
"检查所有的分镜信息,检查没有主图的分镜,判断对应的图片输出文件夹中是否有和分镜同名的图片,若有,则同步到对应分镜的主图中,若没有,则跳过!": "Check all storyboard information, check storyboards without main images, determine if there are images with the same name as the storyboard in the corresponding image output folder, if so, sync to the main image of the corresponding storyboard, if not, skip!",
|
||||||
|
'检查所有的分镜信息,检查没有主图的分镜,判断对应的图片输出文件夹中是否有和分镜同名的图片,若有,则同步到对应分镜的主图中,若没有,则跳过!\n\n 注意:该操作不会覆盖已经有主图的分镜!\n\n 该操作适用于手动将图片放入输出文件夹后,同步到分镜中!点击右侧"打开文件夹"打开当前批次的图片输出文件夹': 'Check all storyboard information, check storyboards without main images, determine if there are images with the same name as the storyboard in the corresponding image output folder, if so, sync to the main image of the corresponding storyboard, if not, skip!\n\n Note: This operation will not overwrite storyboards that already have main images!\n\n This operation is suitable for manually placing images into the output folder and then syncing to the storyboard! Click "Open Folder" on the right to open the current batch\'s image output folder',
|
||||||
|
"检查所有的分镜信息,判断分镜的子图文件夹中的图片是否都在分镜的子图列表中,若没有,则添加进去!": "Check all storyboard information, determine if the images in the storyboard's sub-image folder are all in the storyboard's sub-image list, if not, add them!",
|
||||||
|
'检查所有的分镜信息,判断分镜的子图文件夹中的图片是否都在分镜的子图列表中,若没有,则添加进去!\n\n 注意:该操作不会删除分镜中已经有的子图信息!而是根据文件中的图片信息添加到分镜的子图数据中\n\n 该操作适用于手动将子图放入子图文件夹后,同步到分镜中!点击右侧"打开文件夹"打开当前批次的图片输出文件夹': 'Check all storyboard information, determine if the images in the storyboard\'s sub-image folder are all in the storyboard\'s sub-image list, if not, add them!\n\n Note: This operation will not delete existing sub-image information in the storyboard! Instead, it adds image information from the folder to the storyboard\'s sub-image data\n\n This operation is suitable for manually placing sub-images into the sub-image folder and then syncing to the storyboard! Click "Open Folder" on the right to open the current batch\'s image output folder',
|
||||||
|
'正在同步主图信息,请稍后...': 'Syncing main image information, please wait...',
|
||||||
|
'同步主图信息失败,{error}': 'Failed to sync main image information, {error}',
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
//#region 出图
|
//#region 出图
|
||||||
@ -550,6 +561,8 @@ export default {
|
|||||||
"将图片上传到 LaiTool 图床,支持多种图片格式,获得可分享的链接": "Upload images to LaiTool image host, supports multiple image formats, get shareable links",
|
"将图片上传到 LaiTool 图床,支持多种图片格式,获得可分享的链接": "Upload images to LaiTool image host, supports multiple image formats, get shareable links",
|
||||||
"搜索工具...": "Search tools...",
|
"搜索工具...": "Search tools...",
|
||||||
"上传图片到LaiTool图床,获取图片链接": "Upload images to LaiTool image host, get image links",
|
"上传图片到LaiTool图床,获取图片链接": "Upload images to LaiTool image host, get image links",
|
||||||
|
"上传成功": "Upload successful",
|
||||||
|
"上传后返回的链接为空!": "Returned link after upload is empty!",
|
||||||
"上传失败,{error}": "Upload failed, {error}",
|
"上传失败,{error}": "Upload failed, {error}",
|
||||||
'未找到机器ID,请重启软件后重试!!': 'Machine ID not found, please restart software and try again!!',
|
'未找到机器ID,请重启软件后重试!!': 'Machine ID not found, please restart software and try again!!',
|
||||||
'图片处理完毕,开始上传文件...': 'Image processing completed, starting file upload...',
|
'图片处理完毕,开始上传文件...': 'Image processing completed, starting file upload...',
|
||||||
@ -1326,6 +1339,7 @@ export default {
|
|||||||
'初始化小说分镜模块失败,{error}': 'Failed to initialize novel storyboard module, {error}',
|
'初始化小说分镜模块失败,{error}': 'Failed to initialize novel storyboard module, {error}',
|
||||||
'正在初始化小说分镜模块,{bookName}_{bookTaskName}': 'Initializing novel storyboard module, {bookName}_{bookTaskName}',
|
'正在初始化小说分镜模块,{bookName}_{bookTaskName}': 'Initializing novel storyboard module, {bookName}_{bookTaskName}',
|
||||||
'分镜出图的进度:{progress}% ': 'Storyboard generation progress: {progress}% ',
|
'分镜出图的进度:{progress}% ': 'Storyboard generation progress: {progress}% ',
|
||||||
|
"视频生成的进度:{progress}% ": "Video generation progress: {progress}% ",
|
||||||
'正在删除小说数据...': 'Deleting novel data...',
|
'正在删除小说数据...': 'Deleting novel data...',
|
||||||
'是否删除该小说,删除后数据将无法恢复,请确认是否继续!': 'Are you sure you want to delete this novel? Data cannot be recovered after deletion, please confirm whether to continue!',
|
'是否删除该小说,删除后数据将无法恢复,请确认是否继续!': 'Are you sure you want to delete this novel? Data cannot be recovered after deletion, please confirm whether to continue!',
|
||||||
'正在重置小说数据。。。': 'Resetting novel data...',
|
'正在重置小说数据。。。': 'Resetting novel data...',
|
||||||
@ -1777,6 +1791,7 @@ export default {
|
|||||||
'复制失败:存在未生成的文本,请先生成文本!': 'Copy failed: Ungenerated text exists, please generate text first!',
|
'复制失败:存在未生成的文本,请先生成文本!': 'Copy failed: Ungenerated text exists, please generate text first!',
|
||||||
'复制成功': 'Copy successful',
|
'复制成功': 'Copy successful',
|
||||||
'复制失败,请在左边的显示生成文本中进行手动复制,失败信息如下:{error}': 'Copy failed, please manually copy in the display generated text on the left, error details: {error}',
|
'复制失败,请在左边的显示生成文本中进行手动复制,失败信息如下:{error}': 'Copy failed, please manually copy in the display generated text on the left, error details: {error}',
|
||||||
|
"复制失败,复制内容为空": "Copy failed, copied content is empty",
|
||||||
'复制失败:{error}': 'Copy failed: {error}',
|
'复制失败:{error}': 'Copy failed: {error}',
|
||||||
"复制失败,请手动复制:{data}": "Copy failed, please copy manually: {data}",
|
"复制失败,请手动复制:{data}": "Copy failed, please copy manually: {data}",
|
||||||
'取消复制': 'Cancel Copy',
|
'取消复制': 'Cancel Copy',
|
||||||
|
|||||||
@ -2,6 +2,7 @@ export default {
|
|||||||
//#region 通用
|
//#region 通用
|
||||||
"是": '是',
|
"是": '是',
|
||||||
"否": '否',
|
"否": '否',
|
||||||
|
"空": '空',
|
||||||
"操作": '操作',
|
"操作": '操作',
|
||||||
"清空": '清空',
|
"清空": '清空',
|
||||||
"生成": '生成',
|
"生成": '生成',
|
||||||
@ -227,6 +228,10 @@ export default {
|
|||||||
"初始视频消息失败,未找到指定小说批次任务的分镜数据": "初始视频消息失败,未找到指定小说批次任务的分镜数据",
|
"初始视频消息失败,未找到指定小说批次任务的分镜数据": "初始视频消息失败,未找到指定小说批次任务的分镜数据",
|
||||||
'根据ID获取小说批次任务信息失败,ID不能为空!': '根据ID获取小说批次任务信息失败,ID不能为空!',
|
'根据ID获取小说批次任务信息失败,ID不能为空!': '根据ID获取小说批次任务信息失败,ID不能为空!',
|
||||||
"未找到对应的小说批次任务信息,请检查!": "未找到对应的小说批次任务信息,请检查!",
|
"未找到对应的小说批次任务信息,请检查!": "未找到对应的小说批次任务信息,请检查!",
|
||||||
|
"同步主图文件失败,{error}": "同步主图文件失败,{error}",
|
||||||
|
"同步分镜主图文件成功,同步成功数 {count},同步分镜名称 {name}": "同步分镜主图文件成功,同步成功数 {count},同步分镜名称 {name}",
|
||||||
|
"同步分镜子图文件成功,同步成功数 {count},同步分镜名称 {name}": "同步分镜子图文件成功,同步成功数 {count},同步分镜名称 {name}",
|
||||||
|
"同步分镜子图文件失败,{error}": "同步分镜子图文件失败,{error}",
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
//#region 小说分镜
|
//#region 小说分镜
|
||||||
@ -298,6 +303,12 @@ export default {
|
|||||||
"反推提示词的ID不能为空": "反推提示词的ID不能为空",
|
"反推提示词的ID不能为空": "反推提示词的ID不能为空",
|
||||||
"全部翻译完成": "全部翻译完成",
|
"全部翻译完成": "全部翻译完成",
|
||||||
"翻译失败,{error}": "翻译失败,{error}",
|
"翻译失败,{error}": "翻译失败,{error}",
|
||||||
|
"检查所有的分镜信息,检查没有主图的分镜,判断对应的图片输出文件夹中是否有和分镜同名的图片,若有,则同步到对应分镜的主图中,若没有,则跳过!": "检查所有的分镜信息,检查没有主图的分镜,判断对应的图片输出文件夹中是否有和分镜同名的图片,若有,则同步到对应分镜的主图中,若没有,则跳过!",
|
||||||
|
'检查所有的分镜信息,检查没有主图的分镜,判断对应的图片输出文件夹中是否有和分镜同名的图片,若有,则同步到对应分镜的主图中,若没有,则跳过!\n\n 注意:该操作不会覆盖已经有主图的分镜!\n\n 该操作适用于手动将图片放入输出文件夹后,同步到分镜中!点击右侧"打开文件夹"打开当前批次的图片输出文件夹': '检查所有的分镜信息,检查没有主图的分镜,判断对应的图片输出文件夹中是否有和分镜同名的图片,若有,则同步到对应分镜的主图中,若没有,则跳过!\n\n 注意:该操作不会覆盖已经有主图的分镜!\n\n 该操作适用于手动将图片放入输出文件夹后,同步到分镜中!点击右侧"打开文件夹"打开当前批次的图片输出文件夹',
|
||||||
|
"检查所有的分镜信息,判断分镜的子图文件夹中的图片是否都在分镜的子图列表中,若没有,则添加进去!": "检查所有的分镜信息,判断分镜的子图文件夹中的图片是否都在分镜的子图列表中,若没有,则添加进去!",
|
||||||
|
'检查所有的分镜信息,判断分镜的子图文件夹中的图片是否都在分镜的子图列表中,若没有,则添加进去!\n\n 注意:该操作不会删除分镜中已经有的子图信息!而是根据文件中的图片信息添加到分镜的子图数据中\n\n 该操作适用于手动将子图放入子图文件夹后,同步到分镜中!点击右侧"打开文件夹"打开当前批次的图片输出文件夹': '检查所有的分镜信息,判断分镜的子图文件夹中的图片是否都在分镜的子图列表中,若没有,则添加进去!\n\n 注意:该操作不会删除分镜中已经有的子图信息!而是根据文件中的图片信息添加到分镜的子图数据中\n\n 该操作适用于手动将子图放入子图文件夹后,同步到分镜中!点击右侧"打开文件夹"打开当前批次的图片输出文件夹',
|
||||||
|
'正在同步主图信息,请稍后...': '正在同步主图信息,请稍后...',
|
||||||
|
'同步主图信息失败,{error}': '同步主图信息失败,{error}',
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
//#region 出图
|
//#region 出图
|
||||||
@ -550,6 +561,8 @@ export default {
|
|||||||
"将图片上传到 LaiTool 图床,支持多种图片格式,获得可分享的链接": "将图片上传到 LaiTool 图床,支持多种图片格式,获得可分享的链接",
|
"将图片上传到 LaiTool 图床,支持多种图片格式,获得可分享的链接": "将图片上传到 LaiTool 图床,支持多种图片格式,获得可分享的链接",
|
||||||
"搜索工具...": "搜索工具...",
|
"搜索工具...": "搜索工具...",
|
||||||
"上传图片到LaiTool图床,获取图片链接": "上传图片到LaiTool图床,获取图片链接",
|
"上传图片到LaiTool图床,获取图片链接": "上传图片到LaiTool图床,获取图片链接",
|
||||||
|
"上传成功": "上传成功",
|
||||||
|
"上传后返回的链接为空!": "上传后返回的链接为空!",
|
||||||
"上传失败,{error}": "上传失败,{error}",
|
"上传失败,{error}": "上传失败,{error}",
|
||||||
'未找到机器ID,请重启软件后重试!!': '未找到机器ID,请重启软件后重试!!',
|
'未找到机器ID,请重启软件后重试!!': '未找到机器ID,请重启软件后重试!!',
|
||||||
'图片处理完毕,开始上传文件...': '图片处理完毕,开始上传文件...',
|
'图片处理完毕,开始上传文件...': '图片处理完毕,开始上传文件...',
|
||||||
@ -1326,6 +1339,7 @@ export default {
|
|||||||
'初始化小说分镜模块失败,{error}': '初始化小说分镜模块失败,{error}',
|
'初始化小说分镜模块失败,{error}': '初始化小说分镜模块失败,{error}',
|
||||||
'正在初始化小说分镜模块,{bookName}_{bookTaskName}': '正在初始化小说分镜模块,{bookName}_{bookTaskName}',
|
'正在初始化小说分镜模块,{bookName}_{bookTaskName}': '正在初始化小说分镜模块,{bookName}_{bookTaskName}',
|
||||||
'分镜出图的进度:{progress}% ': '分镜出图的进度:{progress}% ',
|
'分镜出图的进度:{progress}% ': '分镜出图的进度:{progress}% ',
|
||||||
|
"视频生成的进度:{progress}% ": "视频生成的进度:{progress}% ",
|
||||||
'正在删除小说数据...': '正在删除小说数据...',
|
'正在删除小说数据...': '正在删除小说数据...',
|
||||||
'是否删除该小说,删除后数据将无法恢复,请确认是否继续!': '是否删除该小说,删除后数据将无法恢复,请确认是否继续!',
|
'是否删除该小说,删除后数据将无法恢复,请确认是否继续!': '是否删除该小说,删除后数据将无法恢复,请确认是否继续!',
|
||||||
'正在重置小说数据。。。': '正在重置小说数据。。。',
|
'正在重置小说数据。。。': '正在重置小说数据。。。',
|
||||||
@ -1777,6 +1791,7 @@ export default {
|
|||||||
'复制失败:存在未生成的文本,请先生成文本!': '复制失败:存在未生成的文本,请先生成文本!',
|
'复制失败:存在未生成的文本,请先生成文本!': '复制失败:存在未生成的文本,请先生成文本!',
|
||||||
'复制成功': '复制成功',
|
'复制成功': '复制成功',
|
||||||
'复制失败,请在左边的显示生成文本中进行手动复制,失败信息如下:{error}': '复制失败,请在左边的显示生成文本中进行手动复制,失败信息如下:{error}',
|
'复制失败,请在左边的显示生成文本中进行手动复制,失败信息如下:{error}': '复制失败,请在左边的显示生成文本中进行手动复制,失败信息如下:{error}',
|
||||||
|
"复制失败,复制内容为空": "复制失败,复制内容为空",
|
||||||
'复制失败:{error}': '复制失败:{error}',
|
'复制失败:{error}': '复制失败:{error}',
|
||||||
"复制失败,请手动复制:{data}": "复制失败,请手动复制:{data}",
|
"复制失败,请手动复制:{data}": "复制失败,请手动复制:{data}",
|
||||||
'取消复制': '取消复制',
|
'取消复制': '取消复制',
|
||||||
|
|||||||
@ -55,5 +55,13 @@ export class BookImageEntrance {
|
|||||||
DownloadImageUrlAndSplit = async (bookTaskDetailId: string, imageUrl: string) =>
|
DownloadImageUrlAndSplit = async (bookTaskDetailId: string, imageUrl: string) =>
|
||||||
await this.bookImageHandle.DownloadImageUrlAndSplit(bookTaskDetailId, imageUrl)
|
await this.bookImageHandle.DownloadImageUrlAndSplit(bookTaskDetailId, imageUrl)
|
||||||
|
|
||||||
|
/** 同步主图文件到批次任务 */
|
||||||
|
SyncMainImageForBookTask = async (bookTaskId: string) =>
|
||||||
|
await this.bookImageHandle.SyncMainImageForBookTask(bookTaskId)
|
||||||
|
|
||||||
|
/** 同步子图文件到批次任务 */
|
||||||
|
SyncSubImageForBookTask = async (bookTaskId: string) =>
|
||||||
|
await this.bookImageHandle.SyncSubImageForBookTask(bookTaskId)
|
||||||
|
|
||||||
//#endregion
|
//#endregion
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,7 +5,8 @@ import {
|
|||||||
CheckFileOrDirExist,
|
CheckFileOrDirExist,
|
||||||
CheckFolderExistsOrCreate,
|
CheckFolderExistsOrCreate,
|
||||||
CopyFileOrFolder,
|
CopyFileOrFolder,
|
||||||
DownloadImageFromUrl
|
DownloadImageFromUrl,
|
||||||
|
GetFilesWithExtensions
|
||||||
} from '@/define/Tools/file'
|
} from '@/define/Tools/file'
|
||||||
import { getProjectPath } from '../../option/optionCommonService'
|
import { getProjectPath } from '../../option/optionCommonService'
|
||||||
import { errorMessage, successMessage } from '@/public/generalTools'
|
import { errorMessage, successMessage } from '@/public/generalTools'
|
||||||
@ -741,4 +742,277 @@ export class BookImageHandle extends BookBasicHandle {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 同步主图文件到批次任务
|
||||||
|
*
|
||||||
|
* 该方法会检查指定批次任务的所有分镜信息,并将图片输出文件夹中存在的分镜图片文件
|
||||||
|
* 同步到对应的分镜数据中。只有当分镜在数据库中没有主图记录或主图文件不存在时,
|
||||||
|
* 才会进行同步操作。
|
||||||
|
*
|
||||||
|
* 处理流程:
|
||||||
|
* 1. 验证批次任务和图片输出文件夹的有效性
|
||||||
|
* 2. 获取批次任务下的所有分镜详情
|
||||||
|
* 3. 遍历每个分镜,检查是否已有有效的主图文件
|
||||||
|
* 4. 如果分镜没有主图或主图文件不存在,则在图片文件夹中查找对应的图片文件
|
||||||
|
* 5. 找到对应图片后,更新分镜的主图路径和生成状态信息
|
||||||
|
* 6. 返回同步结果,包含成功同步的分镜信息
|
||||||
|
*
|
||||||
|
* 注意事项:
|
||||||
|
* - 只处理 .png 格式的图片文件
|
||||||
|
* - 图片文件命名规则:分镜名称.png
|
||||||
|
* - 不会覆盖已存在且有效的主图文件
|
||||||
|
* - 同步后会自动设置生成状态为成功完成
|
||||||
|
*
|
||||||
|
* @param {string} bookTaskId - 小说批次任务ID
|
||||||
|
* @returns {Promise<GeneralResponse.SuccessItem | GeneralResponse.ErrorItem>}
|
||||||
|
* 成功时返回同步结果数组,包含每个分镜的ID、名称和主图路径
|
||||||
|
* 失败时返回错误信息
|
||||||
|
*
|
||||||
|
* @throws {Error} 当找不到批次任务、图片文件夹不存在或分镜数据为空时抛出错误
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // 同步批次任务的所有主图文件
|
||||||
|
* const result = await bookImageHandle.SyncMainImageForBookTask("task-123");
|
||||||
|
* if (result.code === 1) {
|
||||||
|
* console.log("同步成功:", result.data);
|
||||||
|
* // result.data 包含: [{ id: "detail-1", name: "第一章", outImagePath: "..." }]
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
public async SyncMainImageForBookTask(
|
||||||
|
bookTaskId: string
|
||||||
|
): Promise<GeneralResponse.SuccessItem | GeneralResponse.ErrorItem> {
|
||||||
|
try {
|
||||||
|
await this.InitBookBasicHandle()
|
||||||
|
|
||||||
|
// 获取批次任务信息
|
||||||
|
const bookTask = await this.bookTaskService.GetBookTaskDataById(bookTaskId, true)
|
||||||
|
|
||||||
|
// 获取图片输出文件夹
|
||||||
|
const imageFolder = bookTask.imageFolder
|
||||||
|
if (!imageFolder || isEmpty(imageFolder)) {
|
||||||
|
throw new Error(t('没有找到对应的小说批次任务的图片输出地址,请检查'))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查图片文件夹是否存在
|
||||||
|
if (!(await CheckFileOrDirExist(imageFolder))) {
|
||||||
|
throw new Error(t("目的文件/文件夹不存在,{data}", {
|
||||||
|
data: imageFolder
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取所有分镜详情
|
||||||
|
const bookTaskDetails = await this.bookTaskDetailService.GetBookTaskDetailDataByCondition({
|
||||||
|
bookTaskId: bookTaskId
|
||||||
|
})
|
||||||
|
if (bookTaskDetails.length <= 0) {
|
||||||
|
throw new Error(t('没有找到对应的小说批次任务的分镜数据,请检查'))
|
||||||
|
}
|
||||||
|
let result: any = []
|
||||||
|
|
||||||
|
const projectPath = await getProjectPath()
|
||||||
|
|
||||||
|
for (const detail of bookTaskDetails) {
|
||||||
|
// 跳过主图已经存在并且对应的文件也存在的分镜
|
||||||
|
if (!isEmpty(detail.outImagePath) && await CheckFileOrDirExist(detail.outImagePath as string)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按照分镜名称获取默认的图片文件,并检查对应的文件是不是存在
|
||||||
|
let foundImagePath = path.join(imageFolder, detail.name as string + '.png');
|
||||||
|
const foundImagePathExist = await CheckFileOrDirExist(foundImagePath)
|
||||||
|
if (!foundImagePathExist) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对应的图片存在,小改小说分镜信息
|
||||||
|
await this.bookTaskDetailService.ModifyBookTaskDetailById(detail.id as string, {
|
||||||
|
outImagePath: path.relative(projectPath, foundImagePath),
|
||||||
|
} as any)
|
||||||
|
|
||||||
|
// 修改出图信息
|
||||||
|
let mjMessage = {
|
||||||
|
status: 'success',
|
||||||
|
progress: 100,
|
||||||
|
category: ImageGenerateMode.MJ_API,
|
||||||
|
messageId: '',
|
||||||
|
action: MJAction.IMAGINE
|
||||||
|
}
|
||||||
|
this.bookTaskDetailService.UpdateBookTaskDetailMjMessage(detail.id as string, mjMessage)
|
||||||
|
// 成功 构建返回数据
|
||||||
|
result.push({
|
||||||
|
id: detail.id,
|
||||||
|
name: detail.name,
|
||||||
|
outImagePath: foundImagePath + '?t=' + new Date().getTime()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return successMessage(
|
||||||
|
result,
|
||||||
|
t("同步分镜主图文件成功,同步成功数 {count},同步分镜名称 {name}", {
|
||||||
|
count: result.length,
|
||||||
|
name: result.length == 0 ? t("空") : result.map((item: any) => item.name).join(',')
|
||||||
|
}),
|
||||||
|
'BookImage_SyncMainImageForBookTask'
|
||||||
|
)
|
||||||
|
} catch (error: any) {
|
||||||
|
return errorMessage(
|
||||||
|
t('同步主图文件失败,{error}', {
|
||||||
|
error: (error as Error).message
|
||||||
|
}),
|
||||||
|
'BookImage_SyncMainImageForBookTask'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 同步子图文件到批次任务
|
||||||
|
*
|
||||||
|
* 该方法会检查指定批次任务的所有分镜信息,扫描每个分镜对应的子图文件夹,
|
||||||
|
* 将文件夹中存在但数据库中没有记录的图片文件添加到分镜的子图列表中。
|
||||||
|
*
|
||||||
|
* 处理流程:
|
||||||
|
* 1. 验证批次任务和图片输出文件夹的有效性
|
||||||
|
* 2. 获取批次任务下的所有分镜详情
|
||||||
|
* 3. 遍历每个分镜,构建对应的子图文件夹路径(imageFolder/subImage/分镜名称)
|
||||||
|
* 4. 扫描子图文件夹中的所有 .png 图片文件
|
||||||
|
* 5. 比较文件夹中的图片与数据库中已有的子图记录
|
||||||
|
* 6. 将新发现的图片添加到分镜的子图列表中
|
||||||
|
* 7. 更新数据库并返回同步结果
|
||||||
|
*
|
||||||
|
* 注意事项:
|
||||||
|
* - 只处理 .png 格式的图片文件
|
||||||
|
* - 不会删除数据库中已有的子图记录,只做增量添加
|
||||||
|
* - 子图文件夹路径固定为:图片输出目录/subImage/分镜名称
|
||||||
|
* - 路径比较时会移除时间戳参数,确保准确匹配
|
||||||
|
*
|
||||||
|
* @param {string} bookTaskId - 小说批次任务ID
|
||||||
|
* @returns {Promise<GeneralResponse.SuccessItem | GeneralResponse.ErrorItem>}
|
||||||
|
* 成功时返回同步结果数组,包含每个分镜的ID、名称和更新后的子图路径列表
|
||||||
|
* 失败时返回错误信息
|
||||||
|
*
|
||||||
|
* @throws {Error} 当找不到批次任务、图片文件夹不存在或分镜数据为空时抛出错误
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // 同步批次任务的所有子图文件
|
||||||
|
* const result = await bookImageHandle.SyncSubImageForBookTask("task-123");
|
||||||
|
* if (result.code === 1) {
|
||||||
|
* console.log("同步成功:", result.data);
|
||||||
|
* // result.data 包含: [{ id: "detail-1", name: "第一章", subImagePath: [...] }]
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
public async SyncSubImageForBookTask(
|
||||||
|
bookTaskId: string
|
||||||
|
): Promise<GeneralResponse.SuccessItem | GeneralResponse.ErrorItem> {
|
||||||
|
try {
|
||||||
|
// 初始化基础处理器
|
||||||
|
await this.InitBookBasicHandle()
|
||||||
|
|
||||||
|
// 获取批次任务信息
|
||||||
|
const bookTask = await this.bookTaskService.GetBookTaskDataById(bookTaskId, true)
|
||||||
|
|
||||||
|
// 获取并验证图片输出文件夹路径
|
||||||
|
const imageFolder = bookTask.imageFolder
|
||||||
|
if (!imageFolder || isEmpty(imageFolder)) {
|
||||||
|
throw new Error(t('没有找到对应的小说批次任务的图片输出地址,请检查'))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查图片文件夹是否存在
|
||||||
|
if (!(await CheckFileOrDirExist(imageFolder))) {
|
||||||
|
throw new Error(t("目的文件/文件夹不存在,{data}", {
|
||||||
|
data: imageFolder
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取该批次任务下的所有分镜详情
|
||||||
|
const bookTaskDetails = await this.bookTaskDetailService.GetBookTaskDetailDataByCondition({
|
||||||
|
bookTaskId: bookTaskId
|
||||||
|
})
|
||||||
|
|
||||||
|
// 验证是否有分镜数据
|
||||||
|
if (bookTaskDetails.length <= 0) {
|
||||||
|
throw new Error(t('没有找到对应的小说批次任务的分镜数据,请检查'))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化结果数组和项目根路径
|
||||||
|
const result: { id: string; name: string, subImagePath: string[] }[] = []
|
||||||
|
let projectPath = await getProjectPath()
|
||||||
|
|
||||||
|
// 遍历每个分镜进行子图同步
|
||||||
|
for (const detail of bookTaskDetails) {
|
||||||
|
// 构建子图文件夹路径:图片输出目录/subImage/分镜名称
|
||||||
|
let subImageFolderPath = path.join(imageFolder, 'subImage', detail.name as string)
|
||||||
|
|
||||||
|
// 检查子图文件夹是否存在,不存在则跳过该分镜
|
||||||
|
if (!(await CheckFileOrDirExist(subImageFolderPath))) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取子图文件夹中的所有 .png 图片文件
|
||||||
|
let imageFiles = await GetFilesWithExtensions(subImageFolderPath, ['.png']);
|
||||||
|
if (imageFiles.length === 0) {
|
||||||
|
continue // 没有图片文件则跳过
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取当前分镜在数据库中已有的子图列表
|
||||||
|
const currentSubImages = detail.subImagePath || []
|
||||||
|
|
||||||
|
// 将数据库中已有的子图路径转换为绝对路径,便于与文件夹中的图片进行比较
|
||||||
|
const currentSubImagePaths = currentSubImages
|
||||||
|
.map(item => item.replace(/\?t=\d+$/, '')) // 移除时间戳参数 (?t=数字)
|
||||||
|
.map(item => path.resolve(projectPath, item)) // 相对路径转换为绝对路径
|
||||||
|
|
||||||
|
// 找出新图片:在文件夹中存在但在数据库记录中不存在的图片
|
||||||
|
let newImages: string[] = [];
|
||||||
|
for (let i = 0; i < imageFiles.length; i++) {
|
||||||
|
const imageFile = imageFiles[i];
|
||||||
|
const absoluteImagePath = path.resolve(imageFile);
|
||||||
|
|
||||||
|
// 如果这个图片文件不在已有的子图列表中,则添加到新图片列表
|
||||||
|
if (!currentSubImagePaths.includes(absoluteImagePath)) {
|
||||||
|
newImages.push(imageFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有新图片需要添加,跳过该分镜
|
||||||
|
if (newImages.length === 0) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 合并已有子图和新发现的图片,形成完整的子图列表
|
||||||
|
const allSubImages = [...currentSubImages, ...newImages]
|
||||||
|
|
||||||
|
// 更新数据库中分镜的子图列表(存储相对路径)
|
||||||
|
await this.bookTaskDetailService.ModifyBookTaskDetailById(detail.id as string, {
|
||||||
|
subImagePath: allSubImages.map((item) => path.relative(projectPath, item))
|
||||||
|
} as any)
|
||||||
|
|
||||||
|
// 将同步结果添加到返回数组(返回绝对路径+时间戳)
|
||||||
|
result.push({
|
||||||
|
id: detail.id as string,
|
||||||
|
name: detail.name as string,
|
||||||
|
subImagePath: allSubImages.map((item) => item + '?t=' + new Date().getTime())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回成功结果,包含统计信息和详细数据
|
||||||
|
return successMessage(
|
||||||
|
result,
|
||||||
|
t("同步分镜子图文件成功,同步成功数 {count},同步分镜名称 {name}", {
|
||||||
|
count: result.length,
|
||||||
|
name: result.length == 0 ? t("空") : result.map((item: any) => item.name).join(',')
|
||||||
|
}),
|
||||||
|
'BookImage_SyncSubImageForBookTask'
|
||||||
|
)
|
||||||
|
|
||||||
|
} catch (error: any) {
|
||||||
|
// 捕获并返回错误信息
|
||||||
|
return errorMessage(
|
||||||
|
t('同步子图文件失败,{error}', {
|
||||||
|
error: (error as Error).message
|
||||||
|
}),
|
||||||
|
'BookImage_SyncSubImageForBookTask'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -308,6 +308,8 @@ export class BookVideoServiceHandle extends BookBasicHandle {
|
|||||||
switch (task.type) {
|
switch (task.type) {
|
||||||
case BookBackTaskType.MJ_VIDEO:
|
case BookBackTaskType.MJ_VIDEO:
|
||||||
return await videoHandle.MJImageToVideo(task)
|
return await videoHandle.MJImageToVideo(task)
|
||||||
|
case BookBackTaskType.MJ_VIDEO_EXTEND:
|
||||||
|
return await videoHandle.MJVideoExtendToVideo(task)
|
||||||
default:
|
default:
|
||||||
throw new Error('未知的视频生成方式,请检查')
|
throw new Error('未知的视频生成方式,请检查')
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,10 @@ import fs from 'fs/promises'
|
|||||||
import { errorMessage, successMessage } from '../../../public/generalTools'
|
import { errorMessage, successMessage } from '../../../public/generalTools'
|
||||||
import { ErrorItem, SuccessItem } from '@/define/model/generalResponse'
|
import { ErrorItem, SuccessItem } from '@/define/model/generalResponse'
|
||||||
import { t } from '@/i18n'
|
import { t } from '@/i18n'
|
||||||
|
import { GetImageBase64, GetMimeType } from '@/define/Tools/image'
|
||||||
|
import axios from 'axios'
|
||||||
|
import { define } from '@/define/define'
|
||||||
|
import { isEmpty } from 'lodash'
|
||||||
|
|
||||||
/** 打开指定的文件夹的方法 */
|
/** 打开指定的文件夹的方法 */
|
||||||
export type OpenFolderParams = {
|
export type OpenFolderParams = {
|
||||||
@ -318,4 +322,89 @@ export default class ElectronInterface {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传图片到LaiTool云端
|
||||||
|
*
|
||||||
|
* 该方法将本地图片文件上传到LaiTool云端服务器,返回可用于分享的网络链接。
|
||||||
|
* 支持常见的图片格式,并可指定上传类型用于不同的业务场景。
|
||||||
|
*
|
||||||
|
* @param {string} imagePath - 本地图片文件的绝对路径
|
||||||
|
* @param {"video" | "image"} type - 上传类型分类
|
||||||
|
* - "image": 普通图片上传,用于一般图片分享
|
||||||
|
* - "video": 视频相关图片上传,用于视频生成、MJ垫图等功能
|
||||||
|
*
|
||||||
|
* @returns {Promise<ErrorItem | SuccessItem>} 返回上传结果
|
||||||
|
* - 成功时:返回包含图片网络链接等信息的SuccessItem对象
|
||||||
|
* - 失败时:返回包含错误信息的ErrorItem对象
|
||||||
|
*
|
||||||
|
* @throws {Error} 当以下情况发生时会抛出错误:
|
||||||
|
* - 机器码获取失败或为空
|
||||||
|
* - 指定的图片文件不存在
|
||||||
|
* - 图片文件格式不支持
|
||||||
|
* - 网络请求失败
|
||||||
|
* - 服务器返回错误响应
|
||||||
|
*
|
||||||
|
* @description 注意事项:
|
||||||
|
* 1. 每日上传限制:50次/天
|
||||||
|
* 2. 支持格式:jpg, jpeg, png, gif, bmp, webp 等常见图片格式
|
||||||
|
* 3. 上传后的图片会在LaiTool服务器保留,请确保图片内容合规
|
||||||
|
* 4. 返回的网络链接为全球可访问的永久链接
|
||||||
|
* 5. 需要有效的机器码和网络连接
|
||||||
|
*/
|
||||||
|
public async UploadImageToLaiTool(
|
||||||
|
imagePath: string,
|
||||||
|
type: "video" | "image"
|
||||||
|
): Promise<ErrorItem | SuccessItem> {
|
||||||
|
try {
|
||||||
|
// 检查机器码是不是存在
|
||||||
|
if (!global.machineId || isEmpty(global.machineId)) {
|
||||||
|
throw new Error(t('获取机器码失败,请重启软件或者检查对应权限!!'))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查文件是不是存在
|
||||||
|
if (!(await CheckFileOrDirExist(imagePath))) {
|
||||||
|
throw new Error(t('文件不存在'))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取图片文件的base64
|
||||||
|
let fileBase64 = await GetImageBase64(imagePath)
|
||||||
|
let contentType = GetMimeType(imagePath);
|
||||||
|
let body: ImageModel.FileUploadRequest = {
|
||||||
|
file: fileBase64,
|
||||||
|
fileName: path.basename(imagePath),
|
||||||
|
contentType: contentType
|
||||||
|
}
|
||||||
|
if (type == "video") {
|
||||||
|
body.type = "video"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开始上传
|
||||||
|
let res = await axios.post(define.lms_url + `/lms/FileUpload/FileUpload/${global.machineId}`, body)
|
||||||
|
let resData = res.data;
|
||||||
|
if (resData.code !== 1) {
|
||||||
|
throw new Error(resData.message || t('未知错误'))
|
||||||
|
}
|
||||||
|
|
||||||
|
let url = resData.data.url;
|
||||||
|
if (url == null || isEmpty(url)) {
|
||||||
|
throw new Error(t('上传后返回的链接为空'))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 上传成功,返回结果
|
||||||
|
return successMessage(
|
||||||
|
resData.data,
|
||||||
|
t('上传成功'),
|
||||||
|
'SystemIpc_UploadImageToLaiTool'
|
||||||
|
)
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('上传图片错误:', error)
|
||||||
|
return errorMessage(
|
||||||
|
t("上传失败,{error}", {
|
||||||
|
error: error.message
|
||||||
|
}),
|
||||||
|
'SystemIpc_UploadImageToLaiTool'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,4 +12,9 @@ export class VideoHandle {
|
|||||||
MJImageToVideo(task: TaskModal.Task) {
|
MJImageToVideo(task: TaskModal.Task) {
|
||||||
return this.mjVideoService.MJImageToVideo(task)
|
return this.mjVideoService.MJImageToVideo(task)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** MJ视频扩展生成视频处理方法 将指定的视频通过Midjourney API进行扩展生成新视频 */
|
||||||
|
MJVideoExtendToVideo(task: TaskModal.Task) {
|
||||||
|
return this.mjVideoService.MJVideoExtendToVideo(task)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,6 +22,7 @@ import path from 'path'
|
|||||||
import { CheckFolderExistsOrCreate, CopyFileOrFolder } from '@/define/Tools/file'
|
import { CheckFolderExistsOrCreate, CopyFileOrFolder } from '@/define/Tools/file'
|
||||||
import { DownloadFile } from '@/define/Tools/common'
|
import { DownloadFile } from '@/define/Tools/common'
|
||||||
import { getProjectPath } from '../option/optionCommonService'
|
import { getProjectPath } from '../option/optionCommonService'
|
||||||
|
import { define } from '@/define/define'
|
||||||
|
|
||||||
export class MJVideoService extends MJApiService {
|
export class MJVideoService extends MJApiService {
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -95,31 +96,25 @@ export class MJVideoService extends MJApiService {
|
|||||||
throw new Error(t("不支持的图片链接,仅支持网络图片链接!"))
|
throw new Error(t("不支持的图片链接,仅支持网络图片链接!"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 在提示词后面添加 --raw
|
// 判断有没有提示词,若有提示词并且raw为true,则在提示词后面添加 --raw
|
||||||
if (!isEmpty(prompt) && raw) {
|
if (!isEmpty(prompt) && raw) {
|
||||||
prompt = prompt + ' --raw'
|
prompt = prompt + ' --raw'
|
||||||
}
|
}
|
||||||
prompt = imageUrl + ' ' + prompt
|
|
||||||
|
|
||||||
// 添加 批次信息
|
let body: BookVideo.MJVideoGenerateRequest = {
|
||||||
prompt = prompt + ' --bs ' + batchSize
|
|
||||||
|
|
||||||
if (loop) {
|
|
||||||
prompt = prompt + ' --end loop'
|
|
||||||
} else {
|
|
||||||
if (!isEmpty(endImageUrl)) {
|
|
||||||
prompt = prompt + ' --end ' + endImageUrl
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let body: {
|
|
||||||
prompt: string
|
|
||||||
motion?: MJVideoMotion
|
|
||||||
videoType: MJVideoType
|
|
||||||
} = {
|
|
||||||
prompt: prompt,
|
prompt: prompt,
|
||||||
motion: motion,
|
motion: motion,
|
||||||
videoType: videoType
|
videoType: videoType,
|
||||||
|
image: imageUrl,
|
||||||
|
batchSize: batchSize
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loop) {
|
||||||
|
body.loop = loop
|
||||||
|
} else {
|
||||||
|
if (endImageUrl && !isEmpty(endImageUrl)) {
|
||||||
|
body.endImage = endImageUrl
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 开始请求
|
// 开始请求
|
||||||
@ -178,6 +173,131 @@ export class MJVideoService extends MJApiService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//#region MJVideoExtendToVideo
|
||||||
|
|
||||||
|
async MJVideoExtendToVideo(task: TaskModal.Task) {
|
||||||
|
try {
|
||||||
|
await this.InitMJBasic()
|
||||||
|
await this.InitMJSetting(ImageGenerateMode.MJ_API)
|
||||||
|
|
||||||
|
// 检查是否支持视频功能
|
||||||
|
if (this.videoUrl == null || isEmpty(this.videoUrl)) {
|
||||||
|
throw new Error(t('当前Midjourney模式不支持视频生成功能,请更换为MJ API或本地代理模式后重试!'))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查 token
|
||||||
|
if (this.token == null || isEmpty(this.token)) {
|
||||||
|
throw new Error(
|
||||||
|
t("对应Midjourney模式的Token不能为空,请前往 {settingPath} 中配置", { settingPath: t("设置 -> MJ设置") })
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开始处理小说数据
|
||||||
|
let bookTaskDetail = await this.bookTaskDetailService.GetBookTaskDetailDataById(
|
||||||
|
task.bookTaskDetailId as string, true
|
||||||
|
)
|
||||||
|
|
||||||
|
// 获取视频配置信息
|
||||||
|
let videoMessage = bookTaskDetail.videoMessage
|
||||||
|
if (videoMessage == null || videoMessage == undefined) {
|
||||||
|
throw new Error(t("小说批次任务的分镜数据的转视频配置为空,请检查"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取 MJ Video 的options
|
||||||
|
let mjVideoOptionsString = bookTaskDetail.videoMessage?.mjVideoOptions as string
|
||||||
|
if (!ValidateJson(mjVideoOptionsString)) {
|
||||||
|
throw new Error(t("当前分镜数据的MJ图转视频参数为空或参数校验失败,请检查"))
|
||||||
|
}
|
||||||
|
let mjVideoOptions: BookTaskDetail.MjVideoOptions = JSON.parse(mjVideoOptionsString)
|
||||||
|
|
||||||
|
let prompt = videoMessage.prompt?.trim()
|
||||||
|
let motion: MJVideoMotion =
|
||||||
|
mjVideoOptions.motion === MJVideoMotion.High ? MJVideoMotion.High : MJVideoMotion.Low
|
||||||
|
|
||||||
|
let videoType = mjVideoOptions.videoType ?? MJVideoType.HD
|
||||||
|
|
||||||
|
let batchSize = mjVideoOptions.batchSize ?? MJVideoBatchSize.FOUR
|
||||||
|
|
||||||
|
// 暂不支持尾帧
|
||||||
|
// let extendEndImageUrl = mjVideoOptions.extendEndImageUrl?.trim() || ''
|
||||||
|
|
||||||
|
let raw = mjVideoOptions.raw ?? false
|
||||||
|
|
||||||
|
if (mjVideoOptions.taskId == null || isEmpty(mjVideoOptions.taskId) || mjVideoOptions.index == null) {
|
||||||
|
throw new Error(t("请先选择父任务ID和视频索引!"))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 开始构建请求体
|
||||||
|
// 判断有没有提示词,若有提示词并且raw为true,则在提示词后面添加 --raw
|
||||||
|
if (!isEmpty(prompt) && raw) {
|
||||||
|
prompt = prompt + ' --raw'
|
||||||
|
}
|
||||||
|
|
||||||
|
let body: BookVideo.MJVideoGenerateRequest = {
|
||||||
|
motion: motion,
|
||||||
|
videoType: videoType,
|
||||||
|
batchSize: batchSize,
|
||||||
|
action: 'extend',
|
||||||
|
taskId: mjVideoOptions.taskId,
|
||||||
|
index: mjVideoOptions.index
|
||||||
|
}
|
||||||
|
if (!isEmpty(prompt)) {
|
||||||
|
body.prompt = prompt
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开始请求
|
||||||
|
let res = await axios.post(this.videoUrl, body, {
|
||||||
|
headers: {
|
||||||
|
Authorization: this.token
|
||||||
|
}
|
||||||
|
})
|
||||||
|
console.log('MJVideoExtendToVideo response', res.data)
|
||||||
|
|
||||||
|
let resData = res.data
|
||||||
|
let id = resData.result
|
||||||
|
|
||||||
|
// 修改Task, 将数据写入
|
||||||
|
this.taskListService.UpdateBackTaskData(task.id as string, {
|
||||||
|
taskId: id as string,
|
||||||
|
taskMessage: JSON.stringify(resData)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 修改videoMessage
|
||||||
|
videoMessage.taskId = id
|
||||||
|
videoMessage.status = VideoStatus.WAIT
|
||||||
|
videoMessage.messageData = JSON.stringify(resData)
|
||||||
|
videoMessage.msg = ''
|
||||||
|
delete videoMessage.imageUrl // 不要修改原本的图片地址
|
||||||
|
|
||||||
|
// 添加任务成功 返回前端任务事件
|
||||||
|
SendReturnMessage(
|
||||||
|
{
|
||||||
|
code: 1,
|
||||||
|
id: task.bookTaskDetailId as string,
|
||||||
|
message: t('已成功提交Midjourney图转视频任务,任务ID:{taskId}', { taskId: id }),
|
||||||
|
type: ResponseMessageType.MJ_VIDEO,
|
||||||
|
data: JSON.stringify(videoMessage)
|
||||||
|
},
|
||||||
|
task.messageName as string
|
||||||
|
)
|
||||||
|
|
||||||
|
// 开始循环查询任务状态
|
||||||
|
await this.FetchMJVideoResult(bookTaskDetail, task, id)
|
||||||
|
return successMessage(
|
||||||
|
t('Midjourney图转视频任务执行完成。'),
|
||||||
|
'MJVideoService_MJImageToVideo'
|
||||||
|
)
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(
|
||||||
|
t('Midjourney图转视频任务执行失败,失败信息如下:{error}', { error: (error as Error).message })
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//#endregion
|
||||||
|
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
//#region FetchMJVideoResult
|
//#region FetchMJVideoResult
|
||||||
@ -217,7 +337,7 @@ export class MJVideoService extends MJApiService {
|
|||||||
task: TaskModal.Task,
|
task: TaskModal.Task,
|
||||||
taskId: string,
|
taskId: string,
|
||||||
useTransfer: boolean = false
|
useTransfer: boolean = false
|
||||||
) {
|
): Promise<void> {
|
||||||
console.log(useTransfer)
|
console.log(useTransfer)
|
||||||
while (true) {
|
while (true) {
|
||||||
let fetchUrl = this.fetchTaskUrl.replace('${id}', taskId)
|
let fetchUrl = this.fetchTaskUrl.replace('${id}', taskId)
|
||||||
@ -310,8 +430,9 @@ export class MJVideoService extends MJApiService {
|
|||||||
taskMessage: JSON.stringify(resData)
|
taskMessage: JSON.stringify(resData)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
let mjId = resData.properties?.messageHash ?? resData.id
|
||||||
// 下载 视频
|
// 下载 视频
|
||||||
await this.DownloadMJVideo(videoMessage.videoUrls || [], task, bookTaskDetail)
|
await this.DownloadMJVideo(videoMessage.videoUrls || [], task, bookTaskDetail, mjId)
|
||||||
|
|
||||||
SendReturnMessage(
|
SendReturnMessage(
|
||||||
{
|
{
|
||||||
@ -365,7 +486,8 @@ export class MJVideoService extends MJApiService {
|
|||||||
async DownloadMJVideo(
|
async DownloadMJVideo(
|
||||||
videoUrls: string[],
|
videoUrls: string[],
|
||||||
task: TaskModal.Task,
|
task: TaskModal.Task,
|
||||||
bookTaskDetail: Book.SelectBookTaskDetail
|
bookTaskDetail: Book.SelectBookTaskDetail,
|
||||||
|
preffix: string
|
||||||
) {
|
) {
|
||||||
// 处理完成 开始下载指定的图片
|
// 处理完成 开始下载指定的图片
|
||||||
|
|
||||||
@ -388,15 +510,36 @@ export class MJVideoService extends MJApiService {
|
|||||||
bookTask.imageFolder as string,
|
bookTask.imageFolder as string,
|
||||||
`video/subVideo/${bookTaskDetail.name}/${new Date().getTime()}_${i}.mp4`
|
`video/subVideo/${bookTaskDetail.name}/${new Date().getTime()}_${i}.mp4`
|
||||||
)
|
)
|
||||||
|
|
||||||
|
let remoteUrl = videoUrl
|
||||||
|
|
||||||
|
// 开始处理下载 mj 官方的图片不支持转存
|
||||||
|
if (global.machineId && !isEmpty(global.machineId) && !videoUrl.startsWith('https://cdn.midjourney.com')) {
|
||||||
|
// 转存一下视频文件
|
||||||
|
// 获取当前url的文件名
|
||||||
|
let fileName = preffix + "_" + path.basename(videoUrl)
|
||||||
|
let transferRes = await axios.post(define.lms_url + `/lms/FileUpload/UrlUpload/${global.machineId}`, {
|
||||||
|
url: videoUrl,
|
||||||
|
fileName: fileName
|
||||||
|
})
|
||||||
|
if (transferRes.status == 200 && transferRes.data.code == 1) {
|
||||||
|
remoteUrl = transferRes.data.data.url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isEmpty(remoteUrl)) {
|
||||||
|
remoteUrl = videoUrl
|
||||||
|
}
|
||||||
|
|
||||||
await CheckFolderExistsOrCreate(path.dirname(videoPath))
|
await CheckFolderExistsOrCreate(path.dirname(videoPath))
|
||||||
await DownloadFile(videoUrl, videoPath)
|
await DownloadFile(remoteUrl, videoPath)
|
||||||
|
|
||||||
// 处理返回数据信息
|
// 处理返回数据信息
|
||||||
// 开始修改信息
|
// 开始修改信息
|
||||||
// 将信息添加到里面
|
// 将信息添加到里面
|
||||||
let a = {
|
let a = {
|
||||||
localPath: path.relative(project_path, videoPath),
|
localPath: path.relative(project_path, videoPath),
|
||||||
remotePath: videoUrl,
|
remotePath: remoteUrl,
|
||||||
taskId: bookTaskDetail.videoMessage?.taskId,
|
taskId: bookTaskDetail.videoMessage?.taskId,
|
||||||
index: i,
|
index: i,
|
||||||
type: MappingTaskTypeToVideoModel(task.type as string)
|
type: MappingTaskTypeToVideoModel(task.type as string)
|
||||||
|
|||||||
@ -58,6 +58,14 @@ export const bookImagePreload = {
|
|||||||
DEFINE_STRING.BOOK.DOWNLOAD_IMAGE_URL_AND_SPLIT,
|
DEFINE_STRING.BOOK.DOWNLOAD_IMAGE_URL_AND_SPLIT,
|
||||||
bookTaskDetailId,
|
bookTaskDetailId,
|
||||||
imageUrl
|
imageUrl
|
||||||
)
|
),
|
||||||
|
|
||||||
|
/** 同步主图文件到批次任务 */
|
||||||
|
SyncMainImageForBookTask: async (bookTaskId: string) =>
|
||||||
|
await ipcRenderer.invoke(DEFINE_STRING.BOOK.SYNC_MAIN_IMAGE_FOR_BOOK_TASK, bookTaskId),
|
||||||
|
|
||||||
|
/** 同步子图文件到批次任务 */
|
||||||
|
SyncSubImageForBookTask: async (bookTaskId: string) =>
|
||||||
|
await ipcRenderer.invoke(DEFINE_STRING.BOOK.SYNC_SUB_IMAGE_FOR_BOOK_TASK, bookTaskId)
|
||||||
// #endregion
|
// #endregion
|
||||||
}
|
}
|
||||||
|
|||||||
@ -46,6 +46,10 @@ const system = {
|
|||||||
ReadTextFile: (filePath: string) =>
|
ReadTextFile: (filePath: string) =>
|
||||||
ipcRenderer.invoke(DEFINE_STRING.SYSTEM.READ_TEXT_FILE, filePath),
|
ipcRenderer.invoke(DEFINE_STRING.SYSTEM.READ_TEXT_FILE, filePath),
|
||||||
|
|
||||||
|
/** 上传图片到LaiTool云端 */
|
||||||
|
UploadImageToLaiTool: (imagePath: string, type: "video" | "image") =>
|
||||||
|
ipcRenderer.invoke(DEFINE_STRING.SYSTEM.UPLOAD_IMAGE_TO_LAITOOL, imagePath, type),
|
||||||
|
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
//#region 系统相关
|
//#region 系统相关
|
||||||
|
|||||||
3
src/renderer/components.d.ts
vendored
3
src/renderer/components.d.ts
vendored
@ -50,6 +50,7 @@ declare module 'vue' {
|
|||||||
InputDialogContent: typeof import('./src/components/common/InputDialogContent.vue')['default']
|
InputDialogContent: typeof import('./src/components/common/InputDialogContent.vue')['default']
|
||||||
JianyingGenerateInformation: typeof import('./src/components/Original/BookTaskDetail/JianyingGenerateInformation.vue')['default']
|
JianyingGenerateInformation: typeof import('./src/components/Original/BookTaskDetail/JianyingGenerateInformation.vue')['default']
|
||||||
JianyingKeyFrameSetting: typeof import('./src/components/Setting/JianyingKeyFrameSetting.vue')['default']
|
JianyingKeyFrameSetting: typeof import('./src/components/Setting/JianyingKeyFrameSetting.vue')['default']
|
||||||
|
LanguageSwitcher: typeof import('./src/components/common/LanguageSwitcher.vue')['default']
|
||||||
LoadingComponent: typeof import('./src/components/common/LoadingComponent.vue')['default']
|
LoadingComponent: typeof import('./src/components/common/LoadingComponent.vue')['default']
|
||||||
ManageAISetting: typeof import('./src/components/CopyWriting/ManageAISetting.vue')['default']
|
ManageAISetting: typeof import('./src/components/CopyWriting/ManageAISetting.vue')['default']
|
||||||
MediaToVideoInfoBasicInfo: typeof import('./src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoBasicInfo.vue')['default']
|
MediaToVideoInfoBasicInfo: typeof import('./src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoBasicInfo.vue')['default']
|
||||||
@ -121,7 +122,6 @@ declare module 'vue' {
|
|||||||
OpenInBrowserRound: typeof import('./src/components/common/Icon/OpenInBrowserRound.vue')['default']
|
OpenInBrowserRound: typeof import('./src/components/common/Icon/OpenInBrowserRound.vue')['default']
|
||||||
OriginalAddBook: typeof import('./src/components/Original/MainHome/OriginalAddBook.vue')['default']
|
OriginalAddBook: typeof import('./src/components/Original/MainHome/OriginalAddBook.vue')['default']
|
||||||
OriginalAddBookTask: typeof import('./src/components/Original/MainHome/OriginalAddBookTask.vue')['default']
|
OriginalAddBookTask: typeof import('./src/components/Original/MainHome/OriginalAddBookTask.vue')['default']
|
||||||
OriginalBookTaskCard: typeof import('./src/components/Original/MainHome/OriginalBookTaskCard.vue')['default']
|
|
||||||
OriginalEmptyState: typeof import('./src/components/Original/MainHome/OriginalEmptyState.vue')['default']
|
OriginalEmptyState: typeof import('./src/components/Original/MainHome/OriginalEmptyState.vue')['default']
|
||||||
OriginalMobileHeader: typeof import('./src/components/Original/MainHome/OriginalMobileHeader.vue')['default']
|
OriginalMobileHeader: typeof import('./src/components/Original/MainHome/OriginalMobileHeader.vue')['default']
|
||||||
OriginalModifyBookTask: typeof import('./src/components/Original/MainHome/OriginalModifyBookTask.vue')['default']
|
OriginalModifyBookTask: typeof import('./src/components/Original/MainHome/OriginalModifyBookTask.vue')['default']
|
||||||
@ -149,6 +149,7 @@ declare module 'vue' {
|
|||||||
TopMenuButtons: typeof import('./src/components/Original/BookTaskDetail/TopMenuButtons.vue')['default']
|
TopMenuButtons: typeof import('./src/components/Original/BookTaskDetail/TopMenuButtons.vue')['default']
|
||||||
UploadRound: typeof import('./src/components/common/Icon/UploadRound.vue')['default']
|
UploadRound: typeof import('./src/components/common/Icon/UploadRound.vue')['default']
|
||||||
UserAnalysis: typeof import('./src/components/Original/Analysis/UserAnalysis.vue')['default']
|
UserAnalysis: typeof import('./src/components/Original/Analysis/UserAnalysis.vue')['default']
|
||||||
|
VideoDisplay: typeof import('./src/components/common/VideoDisplay.vue')['default']
|
||||||
WechatGroup: typeof import('./src/components/SoftHome/WechatGroup.vue')['default']
|
WechatGroup: typeof import('./src/components/SoftHome/WechatGroup.vue')['default']
|
||||||
WordGroup: typeof import('./src/components/Original/Copywriter/WordGroup.vue')['default']
|
WordGroup: typeof import('./src/components/Original/Copywriter/WordGroup.vue')['default']
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { t } from '@/i18n'
|
|||||||
import { CloudUploadOutline } from '@vicons/ionicons5'
|
import { CloudUploadOutline } from '@vicons/ionicons5'
|
||||||
|
|
||||||
// 工具分类
|
// 工具分类
|
||||||
export const categories = [
|
export const getCategories = () => [
|
||||||
{ key: 'media', label: t('媒体工具'), color: '#2080f0' }
|
{ key: 'media', label: t('媒体工具'), color: '#2080f0' }
|
||||||
// { key: 'document', label: '文档处理', color: '#18a058' },
|
// { key: 'document', label: '文档处理', color: '#18a058' },
|
||||||
// { key: 'development', label: '开发工具', color: '#f0a020' },
|
// { key: 'development', label: '开发工具', color: '#f0a020' },
|
||||||
@ -14,7 +14,7 @@ export const categories = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
// 工具数据
|
// 工具数据
|
||||||
export const toolsData = [
|
export const getToolsData = () => [
|
||||||
// 媒体工具
|
// 媒体工具
|
||||||
{
|
{
|
||||||
id: 'image-converter',
|
id: 'image-converter',
|
||||||
@ -251,3 +251,7 @@ export const toolsData = [
|
|||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
]
|
]
|
||||||
|
|
||||||
|
// 兼容性:保持原有的导出名称,但使用函数
|
||||||
|
export const categories = getCategories()
|
||||||
|
export const toolsData = getToolsData()
|
||||||
@ -54,7 +54,7 @@
|
|||||||
<div style="font-size: 14px; font-weight: 500">{{ subCategory.name }}</div>
|
<div style="font-size: 14px; font-weight: 500">{{ subCategory.name }}</div>
|
||||||
</template>
|
</template>
|
||||||
<div style="font-size: 12px; color: #909399; line-height: 1.4">
|
<div style="font-size: 12px; color: #909399; line-height: 1.4">
|
||||||
{{ subCategory.remark }}
|
{{ subCategory.description }}
|
||||||
</div>
|
</div>
|
||||||
</n-card>
|
</n-card>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -132,6 +132,74 @@
|
|||||||
</div>
|
</div>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
|
|
||||||
|
<!-- 尾帧图片链接 -->
|
||||||
|
<n-form-item :label="t('尾帧图片链接(可选)')" :show-feedback="false">
|
||||||
|
<div class="input-with-preview">
|
||||||
|
<n-input
|
||||||
|
v-model:value="videoMessage.mjVideoOptionsObject.extendEndImageUrl"
|
||||||
|
:placeholder="t('请输入图片链接')"
|
||||||
|
@change="
|
||||||
|
handleVideoMessageChange(
|
||||||
|
'extendEndImageUrl',
|
||||||
|
videoMessage.mjVideoOptionsObject.extendEndImageUrl
|
||||||
|
)
|
||||||
|
"
|
||||||
|
size="small"
|
||||||
|
:disabled="loading"
|
||||||
|
class="image-input"
|
||||||
|
>
|
||||||
|
<template #suffix>
|
||||||
|
<n-button
|
||||||
|
size="tiny"
|
||||||
|
quaternary
|
||||||
|
@click="
|
||||||
|
handleUploadImage(
|
||||||
|
videoMessage.mjVideoOptionsObject.extendEndImageUrl,
|
||||||
|
'video',
|
||||||
|
'extendEndImageUrl'
|
||||||
|
)
|
||||||
|
"
|
||||||
|
:disabled="loading"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<n-icon size="20">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<g
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M4 17v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-2"></path>
|
||||||
|
<path d="M7 9l5-5l5 5"></path>
|
||||||
|
<path d="M12 4v12"></path>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</n-icon>
|
||||||
|
</template>
|
||||||
|
</n-button>
|
||||||
|
</template>
|
||||||
|
</n-input>
|
||||||
|
<n-image
|
||||||
|
v-if="videoMessage.mjVideoOptionsObject.extendEndImageUrl"
|
||||||
|
:src="videoMessage.mjVideoOptionsObject.extendEndImageUrl"
|
||||||
|
:height="60"
|
||||||
|
:fallback-src="'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICA8cmVjdCB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgZmlsbD0iI2Y1ZjVmNSIvPgogIDx0ZXh0IHg9IjUwIiB5PSI1MCIgZm9udC1mYW1pbHk9IkFyaWFsLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjEyIiBmaWxsPSIjOTk5IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBkeT0iMC4zZW0iPuWbvueJh+WKoOi9veWksei0pTwvdGV4dD4KPC9zdmc+'"
|
||||||
|
object-fit="contain"
|
||||||
|
class="preview-image"
|
||||||
|
@error="handleImageError(videoMessage.mjVideoOptionsObject.extendEndImageUrl)"
|
||||||
|
/>
|
||||||
|
<div v-else class="preview-placeholder">
|
||||||
|
<n-text depth="3" class="placeholder-text">{{ t('图片预览') }}</n-text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</n-form-item>
|
||||||
|
|
||||||
<!-- 提示词 -->
|
<!-- 提示词 -->
|
||||||
<n-form-item :label="t('提示词(可选)')" :show-feedback="false">
|
<n-form-item :label="t('提示词(可选)')" :show-feedback="false">
|
||||||
<n-input
|
<n-input
|
||||||
@ -398,6 +466,10 @@ import {
|
|||||||
NSpace,
|
NSpace,
|
||||||
NFormItem,
|
NFormItem,
|
||||||
NTooltip,
|
NTooltip,
|
||||||
|
NSwitch,
|
||||||
|
NAlert,
|
||||||
|
NImage,
|
||||||
|
NText,
|
||||||
useMessage
|
useMessage
|
||||||
} from 'naive-ui'
|
} from 'naive-ui'
|
||||||
|
|
||||||
@ -412,9 +484,15 @@ import { isEmpty } from 'lodash'
|
|||||||
import { BookBackTaskType, TaskExecuteType } from '@/define/enum/bookEnum'
|
import { BookBackTaskType, TaskExecuteType } from '@/define/enum/bookEnum'
|
||||||
import { DEFINE_STRING } from '@/define/ipcDefineString'
|
import { DEFINE_STRING } from '@/define/ipcDefineString'
|
||||||
import { t } from '@/i18n'
|
import { t } from '@/i18n'
|
||||||
|
import { AddOneTask } from '@/renderer/src/common/task'
|
||||||
|
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
|
|
||||||
|
// 处理图片加载错误
|
||||||
|
function handleImageError(imageUrl) {
|
||||||
|
message.warning(t('图片加载失败,请检查图片链接是否有效!'))
|
||||||
|
}
|
||||||
|
|
||||||
// 定义 props
|
// 定义 props
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
videoMessage: {
|
videoMessage: {
|
||||||
@ -432,7 +510,7 @@ const props = defineProps({
|
|||||||
})
|
})
|
||||||
|
|
||||||
// 定义 emits
|
// 定义 emits
|
||||||
const emit = defineEmits(['video-message-change', 'extend', 'select-parent-task'])
|
const emit = defineEmits(['video-message-change', 'extend', 'select-parent-task', 'image-upload'])
|
||||||
|
|
||||||
// 修改 videoMessage 的通用函数
|
// 修改 videoMessage 的通用函数
|
||||||
function handleVideoMessageChange(key, value = undefined) {
|
function handleVideoMessageChange(key, value = undefined) {
|
||||||
@ -456,14 +534,14 @@ async function handleExtend() {
|
|||||||
let type = BookBackTaskType.MJ_VIDEO_EXTEND
|
let type = BookBackTaskType.MJ_VIDEO_EXTEND
|
||||||
|
|
||||||
// 添加任务
|
// 添加任务
|
||||||
let res = await window.task.AddBookBackTask(
|
let res = await AddOneTask({
|
||||||
props.task.bookId,
|
bookId: props.task.bookId,
|
||||||
type,
|
type: type,
|
||||||
TaskExecuteType.AUTO,
|
executeType: TaskExecuteType.AUTO,
|
||||||
props.task.bookTaskId,
|
bookTaskId: props.task.bookTaskId,
|
||||||
props.task.id,
|
bookTaskDetailId: props.task.id,
|
||||||
DEFINE_STRING.BOOK.MJ_VIDEO_TO_VIDEO_RETURN
|
messageName: DEFINE_STRING.BOOK.MJ_VIDEO_TO_VIDEO_RETURN
|
||||||
)
|
})
|
||||||
if (res.code != 1) {
|
if (res.code != 1) {
|
||||||
message.error(res.message)
|
message.error(res.message)
|
||||||
return
|
return
|
||||||
@ -480,9 +558,43 @@ function handleSelectParentTask() {
|
|||||||
|
|
||||||
emit('select-parent-task')
|
emit('select-parent-task')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 处理图片上传
|
||||||
|
function handleUploadImage(currentImageUrl, type, property) {
|
||||||
|
emit('image-upload', currentImageUrl, type, property)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<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 {
|
.motion-control {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@ -10,7 +10,38 @@
|
|||||||
size="small"
|
size="small"
|
||||||
:disabled="loading"
|
:disabled="loading"
|
||||||
class="image-input"
|
class="image-input"
|
||||||
/>
|
>
|
||||||
|
<template #suffix>
|
||||||
|
<n-button
|
||||||
|
size="tiny"
|
||||||
|
quaternary
|
||||||
|
@click="handleUploadImage(videoMessage.imageUrl, 'video', 'imageUrl')"
|
||||||
|
:disabled="loading"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<n-icon size="20">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<g
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M4 17v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-2"></path>
|
||||||
|
<path d="M7 9l5-5l5 5"></path>
|
||||||
|
<path d="M12 4v12"></path>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</n-icon>
|
||||||
|
</template>
|
||||||
|
</n-button>
|
||||||
|
</template>
|
||||||
|
</n-input>
|
||||||
<n-image
|
<n-image
|
||||||
v-if="videoMessage.imageUrl"
|
v-if="videoMessage.imageUrl"
|
||||||
:src="videoMessage.imageUrl"
|
:src="videoMessage.imageUrl"
|
||||||
@ -38,7 +69,44 @@
|
|||||||
size="small"
|
size="small"
|
||||||
:disabled="loading"
|
:disabled="loading"
|
||||||
class="image-input"
|
class="image-input"
|
||||||
/>
|
>
|
||||||
|
<template #suffix>
|
||||||
|
<n-button
|
||||||
|
size="tiny"
|
||||||
|
quaternary
|
||||||
|
@click="
|
||||||
|
handleUploadImage(
|
||||||
|
videoMessage.mjVideoOptionsObject.endImageUrl,
|
||||||
|
'video',
|
||||||
|
'endImageUrl'
|
||||||
|
)
|
||||||
|
"
|
||||||
|
:disabled="loading"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<n-icon size="20">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<g
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M4 17v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-2"></path>
|
||||||
|
<path d="M7 9l5-5l5 5"></path>
|
||||||
|
<path d="M12 4v12"></path>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</n-icon>
|
||||||
|
</template>
|
||||||
|
</n-button>
|
||||||
|
</template>
|
||||||
|
</n-input>
|
||||||
<n-image
|
<n-image
|
||||||
v-if="videoMessage.mjVideoOptionsObject.endImageUrl"
|
v-if="videoMessage.mjVideoOptionsObject.endImageUrl"
|
||||||
:src="videoMessage.mjVideoOptionsObject.endImageUrl"
|
:src="videoMessage.mjVideoOptionsObject.endImageUrl"
|
||||||
@ -390,7 +458,7 @@ const props = defineProps({
|
|||||||
})
|
})
|
||||||
|
|
||||||
// 定义 emits
|
// 定义 emits
|
||||||
const emit = defineEmits(['video-message-change', 'image-to-video'])
|
const emit = defineEmits(['video-message-change', 'image-to-video', 'image-upload'])
|
||||||
|
|
||||||
// 处理图片加载错误
|
// 处理图片加载错误
|
||||||
function handleImageError(imageUrl) {
|
function handleImageError(imageUrl) {
|
||||||
@ -403,6 +471,11 @@ function handleVideoMessageChange(key, value = undefined) {
|
|||||||
emit('video-message-change', key, value)
|
emit('video-message-change', key, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 处理上传图片
|
||||||
|
async function handleUploadImage(filePath, type, property) {
|
||||||
|
emit('image-upload', filePath, type, property)
|
||||||
|
}
|
||||||
|
|
||||||
// 处理图生视频
|
// 处理图生视频
|
||||||
function handleImageToVideo() {
|
function handleImageToVideo() {
|
||||||
if (isEmpty(props.videoMessage.imageUrl)) {
|
if (isEmpty(props.videoMessage.imageUrl)) {
|
||||||
|
|||||||
@ -8,18 +8,20 @@
|
|||||||
:loading="loading"
|
:loading="loading"
|
||||||
@video-message-change="handleVideoMessageChange"
|
@video-message-change="handleVideoMessageChange"
|
||||||
@image-to-video="handleImageToVideo"
|
@image-to-video="handleImageToVideo"
|
||||||
|
@image-upload="handleUploadImage"
|
||||||
/>
|
/>
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
|
|
||||||
<!-- 视频拓展 Tab -->
|
<!-- 视频拓展 Tab -->
|
||||||
<n-tab-pane name="video-extend" :tab="t('视频拓展')">
|
<n-tab-pane name="video-extend" :tab="t('视频拓展')">
|
||||||
<DisabledWrapper :un-use="true">
|
<DisabledWrapper :un-use="false">
|
||||||
<ImageTextVideoInfoMJVideoExtend
|
<ImageTextVideoInfoMJVideoExtend
|
||||||
:video-message="videoMessage"
|
:video-message="videoMessage"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
:task="props.task"
|
:task="props.task"
|
||||||
@video-message-change="handleVideoMessageChange"
|
@video-message-change="handleVideoMessageChange"
|
||||||
@select-parent-task="handleSelectParentTask"
|
@select-parent-task="handleSelectParentTask"
|
||||||
|
@image-upload="handleUploadImage"
|
||||||
/>
|
/>
|
||||||
</DisabledWrapper>
|
</DisabledWrapper>
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
@ -59,8 +61,13 @@ import ImageTextVideoInfoMJVideoImageToVideo from './MediaToVideoInfoMJVideoImag
|
|||||||
import ImageTextVideoInfoMJVideoExtend from './MediaToVideoInfoMJVideoExtend.vue'
|
import ImageTextVideoInfoMJVideoExtend from './MediaToVideoInfoMJVideoExtend.vue'
|
||||||
import { AddOneTask } from '@/renderer/src/common/task'
|
import { AddOneTask } from '@/renderer/src/common/task'
|
||||||
import { t } from '@/i18n'
|
import { t } from '@/i18n'
|
||||||
|
import { useSoftwareStore } from '@/renderer/src/stores'
|
||||||
|
import { useFile } from '@/renderer/src/hooks/useFile'
|
||||||
|
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
|
const dialog = useDialog()
|
||||||
|
const softwareStore = useSoftwareStore()
|
||||||
|
const { UploadImageToLaiTool } = useFile()
|
||||||
|
|
||||||
// 定义 props
|
// 定义 props
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@ -81,14 +88,21 @@ const videoMessage = computed(() => {
|
|||||||
let mjVideoOptionsString = videoMessage.mjVideoOptions || '{}'
|
let mjVideoOptionsString = videoMessage.mjVideoOptions || '{}'
|
||||||
let mjVideoOptions = ValidateJsonAndParse(mjVideoOptionsString)
|
let mjVideoOptions = ValidateJsonAndParse(mjVideoOptionsString)
|
||||||
|
|
||||||
mjVideoOptions.image = videoMessage.imageUrl ?? ''
|
// 确保 mjVideoOptions 是一个干净的对象,避免保留上一个任务的数据
|
||||||
if (mjVideoOptions.motion == undefined) {
|
const cleanMjVideoOptions = {
|
||||||
mjVideoOptions.motion = MJVideoMotion.Low // 默认运动变化为低
|
taskId: mjVideoOptions.taskId || '',
|
||||||
|
index: mjVideoOptions.index || 0,
|
||||||
|
videoType: mjVideoOptions.videoType || '',
|
||||||
|
motion: mjVideoOptions.motion || MJVideoMotion.Low,
|
||||||
|
raw: mjVideoOptions.raw !== undefined ? mjVideoOptions.raw : true,
|
||||||
|
batchSize: mjVideoOptions.batchSize || 1,
|
||||||
|
image: videoMessage.imageUrl || mjVideoOptions.image || '',
|
||||||
|
endImageUrl: mjVideoOptions.endImageUrl || '',
|
||||||
|
extendEndImageUrl: mjVideoOptions.extendEndImageUrl || '',
|
||||||
|
loop: mjVideoOptions.loop !== undefined ? mjVideoOptions.loop : false
|
||||||
}
|
}
|
||||||
if (mjVideoOptions.raw == undefined) {
|
|
||||||
mjVideoOptions.raw = true // 默认不启用原始模式
|
videoMessage.mjVideoOptionsObject = cleanMjVideoOptions
|
||||||
}
|
|
||||||
videoMessage.mjVideoOptionsObject = mjVideoOptions
|
|
||||||
return videoMessage
|
return videoMessage
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -156,6 +170,12 @@ async function handleVideoMessageChange(key, value = undefined) {
|
|||||||
endImageUrl: value
|
endImageUrl: value
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
|
case 'extendEndImageUrl':
|
||||||
|
updateObject.mjVideoOptions = JSON.stringify({
|
||||||
|
...videoMessage.value.mjVideoOptionsObject,
|
||||||
|
extendEndImageUrl: value
|
||||||
|
})
|
||||||
|
break
|
||||||
case 'prompt':
|
case 'prompt':
|
||||||
updateObject.prompt = videoMessage.value.prompt
|
updateObject.prompt = videoMessage.value.prompt
|
||||||
break
|
break
|
||||||
@ -221,6 +241,49 @@ async function handleVideoMessageChange(key, value = undefined) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 处理上传图片
|
||||||
|
async function handleUploadImage(filePath, type, property) {
|
||||||
|
debugger
|
||||||
|
let da = dialog.warning({
|
||||||
|
title: t('操作确认'),
|
||||||
|
content: () =>
|
||||||
|
h(
|
||||||
|
'div',
|
||||||
|
{
|
||||||
|
style: { whiteSpace: 'pre-line' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
default: () =>
|
||||||
|
t(
|
||||||
|
'是否上传图片文件到LaiTool云端?\n\n上传后会返回一个全球可分享的网络链接地址,但是每日限制五十次上传。上传后的图片可用于MJ垫图,转视频等功能。\n\n注意:上传后的图片会再Laitool服务器留存,若介意请勿上传。'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
),
|
||||||
|
positiveText: t('继续'),
|
||||||
|
negativeText: t('取消'),
|
||||||
|
onPositiveClick: async () => {
|
||||||
|
da?.destroy()
|
||||||
|
try {
|
||||||
|
softwareStore.spin.spinning = true
|
||||||
|
softwareStore.spin.tip = t('开始上传文件...')
|
||||||
|
|
||||||
|
let url = await UploadImageToLaiTool(filePath, type)
|
||||||
|
if (property == 'endImageUrl' || property == 'extendEndImageUrl')
|
||||||
|
videoMessage.value.mjVideoOptionsObject[property] = url
|
||||||
|
else videoMessage.value[property] = url
|
||||||
|
await handleVideoMessageChange(property, url)
|
||||||
|
} catch (error) {
|
||||||
|
message.error(error.message)
|
||||||
|
} finally {
|
||||||
|
softwareStore.spin.spinning = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onNegativeClick: () => {
|
||||||
|
message.info(t('取消操作'))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@ -57,8 +57,6 @@ import {
|
|||||||
NDataTable,
|
NDataTable,
|
||||||
NSwitch,
|
NSwitch,
|
||||||
NImage,
|
NImage,
|
||||||
NInput,
|
|
||||||
NEmpty,
|
|
||||||
NSelect,
|
NSelect,
|
||||||
useMessage,
|
useMessage,
|
||||||
useDialog
|
useDialog
|
||||||
@ -68,6 +66,7 @@ import { define } from '@/define/define'
|
|||||||
import ImageTextVideoInfoVideoConfig from './MediaToVideoInfoVideoConfig.vue'
|
import ImageTextVideoInfoVideoConfig from './MediaToVideoInfoVideoConfig.vue'
|
||||||
import ImageTextVideoInfoVideoListInfo from './MediaToVideoInfoVideoListInfo.vue'
|
import ImageTextVideoInfoVideoListInfo from './MediaToVideoInfoVideoListInfo.vue'
|
||||||
import ImageTextVideoInfoTaskOptions from './MediaToVideoInfoTaskOptions.vue'
|
import ImageTextVideoInfoTaskOptions from './MediaToVideoInfoTaskOptions.vue'
|
||||||
|
import VideoDisplay from '@/renderer/src/components/common/VideoDisplay.vue'
|
||||||
import { OptionKeyName, OptionType } from '@/define/enum/option'
|
import { OptionKeyName, OptionType } from '@/define/enum/option'
|
||||||
import { useBookStore } from '@/renderer/src/stores'
|
import { useBookStore } from '@/renderer/src/stores'
|
||||||
import { GetImageToVideoModelsOptions } from '@/define/enum/video'
|
import { GetImageToVideoModelsOptions } from '@/define/enum/video'
|
||||||
@ -246,50 +245,6 @@ const columns = [
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: t('图片链接'),
|
|
||||||
key: 'image_url',
|
|
||||||
width: 150,
|
|
||||||
className: noPaddingColumnClass,
|
|
||||||
render(row) {
|
|
||||||
return h(
|
|
||||||
'div',
|
|
||||||
{
|
|
||||||
style: {
|
|
||||||
height: '130px',
|
|
||||||
width: '95%',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
padding: '4px'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[
|
|
||||||
h(NInput, {
|
|
||||||
value: row.videoMessage?.imageUrl || '',
|
|
||||||
type: 'textarea',
|
|
||||||
placeholder: t('请输入图片链接...'),
|
|
||||||
resizable: false,
|
|
||||||
style: {
|
|
||||||
width: '100%',
|
|
||||||
height: '100%'
|
|
||||||
},
|
|
||||||
inputProps: {
|
|
||||||
style: {
|
|
||||||
height: '100%',
|
|
||||||
resize: 'none'
|
|
||||||
},
|
|
||||||
spellcheck: false
|
|
||||||
},
|
|
||||||
onUpdateValue: (value) => {
|
|
||||||
row.videoMessage.imageUrl = value
|
|
||||||
handleSaveBookTaskDetailVideoMessage(row, row.id, 'imageUrl', value)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: () =>
|
title: () =>
|
||||||
h(
|
h(
|
||||||
@ -337,50 +292,13 @@ const columns = [
|
|||||||
width: 160,
|
width: 160,
|
||||||
className: noPaddingColumnClass,
|
className: noPaddingColumnClass,
|
||||||
render(row) {
|
render(row) {
|
||||||
if (row.generateVideoPath) {
|
return h(VideoDisplay, {
|
||||||
return h(
|
videoPath: row.generateVideoPath,
|
||||||
'div',
|
containerHeight: '130px',
|
||||||
{
|
containerWidth: '100%',
|
||||||
style: {
|
autoSize: true,
|
||||||
height: '130px',
|
emptyDescription: t('暂无选择视频')
|
||||||
display: 'flex',
|
})
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[
|
|
||||||
h('video', {
|
|
||||||
src: row.generateVideoPath,
|
|
||||||
controls: true,
|
|
||||||
style: {
|
|
||||||
height: '130px',
|
|
||||||
maxWidth: '160px',
|
|
||||||
objectFit: 'cover',
|
|
||||||
borderRadius: '4px'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
]
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
return h(
|
|
||||||
'div',
|
|
||||||
{
|
|
||||||
style: {
|
|
||||||
height: '130px',
|
|
||||||
width: '100%',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[
|
|
||||||
h(NEmpty, {
|
|
||||||
description: t('暂无选择视频'),
|
|
||||||
size: 'small'
|
|
||||||
})
|
|
||||||
]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@ -22,12 +22,53 @@
|
|||||||
<n-tag v-else type="primary" size="small" class="method-progress">
|
<n-tag v-else type="primary" size="small" class="method-progress">
|
||||||
{{ props.videoMessage?.status ?? 'wait' }}
|
{{ props.videoMessage?.status ?? 'wait' }}
|
||||||
</n-tag>
|
</n-tag>
|
||||||
<n-tooltip :show-arrow="false" trigger="hover">
|
|
||||||
<template #trigger>
|
<n-text
|
||||||
<span class="task-id-span">{{ props.videoMessage?.taskId ?? '' }}</span>
|
v-if="progress"
|
||||||
</template>
|
code
|
||||||
{{ props.videoMessage?.taskId ?? '' }}
|
:style="{
|
||||||
</n-tooltip>
|
fontSize: '14px'
|
||||||
|
}"
|
||||||
|
>{{ progress }}
|
||||||
|
</n-text>
|
||||||
|
|
||||||
|
<div v-if="props.videoMessage?.taskId" class="task-id-container">
|
||||||
|
<n-tooltip>
|
||||||
|
<template #trigger>
|
||||||
|
<n-text
|
||||||
|
code
|
||||||
|
:style="{
|
||||||
|
fontSize: '14px',
|
||||||
|
maxWidth: '150px',
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
cursor: 'pointer'
|
||||||
|
}"
|
||||||
|
>{{ props.videoMessage?.taskId }}
|
||||||
|
</n-text>
|
||||||
|
</template>
|
||||||
|
<span>{{ props.videoMessage?.taskId }}</span>
|
||||||
|
</n-tooltip>
|
||||||
|
<n-button
|
||||||
|
size="tiny"
|
||||||
|
type="primary"
|
||||||
|
ghost
|
||||||
|
@click="CopyData(props.videoMessage?.taskId)"
|
||||||
|
class="copy-button"
|
||||||
|
>
|
||||||
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -47,9 +88,13 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { onMounted } from 'vue'
|
import { onMounted } from 'vue'
|
||||||
import { NSelect, NInput, NTag, NTooltip } from 'naive-ui'
|
import { NSelect, NInput, NTag, NTooltip, NText, NButton, NIcon, useMessage } from 'naive-ui'
|
||||||
import { GetImageToVideoModelsOptions } from '@/define/enum/video'
|
import { GetImageToVideoModelsOptions, ImageToVideoModels } from '@/define/enum/video'
|
||||||
import { t } from '@/i18n'
|
import { t } from '@/i18n'
|
||||||
|
import { useSimple } from '@/renderer/src/hooks/useSimple'
|
||||||
|
|
||||||
|
const message = useMessage()
|
||||||
|
const { CopyData } = useSimple()
|
||||||
|
|
||||||
// 定义 emits
|
// 定义 emits
|
||||||
const emit = defineEmits(['update-method', 'update-prompt'])
|
const emit = defineEmits(['update-method', 'update-prompt'])
|
||||||
@ -69,6 +114,19 @@ const props = defineProps({
|
|||||||
// 转视频方式选项
|
// 转视频方式选项
|
||||||
const videoMethodOptions = GetImageToVideoModelsOptions()
|
const videoMethodOptions = GetImageToVideoModelsOptions()
|
||||||
|
|
||||||
|
const progress = computed(() => {
|
||||||
|
let p = '0%'
|
||||||
|
let messageData = props.videoMessage?.messageData || '{}'
|
||||||
|
let messageObject = JSON.parse(messageData)
|
||||||
|
let videoType = props.videoMessage?.videoType || undefined
|
||||||
|
if (videoType == ImageToVideoModels.MJ_VIDEO) {
|
||||||
|
p = messageObject.progress ?? '0%'
|
||||||
|
} else {
|
||||||
|
p = '0%'
|
||||||
|
}
|
||||||
|
return p
|
||||||
|
})
|
||||||
|
|
||||||
// 处理方式变更
|
// 处理方式变更
|
||||||
function handleMethodChange(value) {
|
function handleMethodChange(value) {
|
||||||
emit('update-method', props.videoMessage?.bookTaskDetailId, value)
|
emit('update-method', props.videoMessage?.bookTaskDetailId, value)
|
||||||
@ -140,17 +198,19 @@ onMounted(() => {})
|
|||||||
resize: none;
|
resize: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-id-span {
|
/* 任务ID容器样式 */
|
||||||
display: inline-block;
|
.task-id-container {
|
||||||
max-width: 80px;
|
display: flex;
|
||||||
overflow: hidden;
|
align-items: center;
|
||||||
text-overflow: ellipsis;
|
gap: 8px;
|
||||||
white-space: nowrap;
|
height: 100%;
|
||||||
padding: 2px 6px;
|
}
|
||||||
background-color: var(--primary-color-suppl);
|
|
||||||
color: var(--primary-color);
|
/* 复制按钮样式 */
|
||||||
border-radius: 3px;
|
.copy-button {
|
||||||
font-size: 12px;
|
padding: 4px !important;
|
||||||
cursor: pointer;
|
min-width: auto !important;
|
||||||
|
width: 24px !important;
|
||||||
|
height: 24px !important;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -403,7 +403,7 @@ async function copyTaskId() {
|
|||||||
try {
|
try {
|
||||||
const taskId = videoMessage.value.taskId
|
const taskId = videoMessage.value.taskId
|
||||||
if (!taskId) {
|
if (!taskId) {
|
||||||
message.warning(t('task_id_is_empty'))
|
message.warning(t('任务ID不能为空,请检查!'))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -8,19 +8,20 @@
|
|||||||
:loading="loading"
|
:loading="loading"
|
||||||
:single-line="false"
|
:single-line="false"
|
||||||
:row-key="rowKey"
|
:row-key="rowKey"
|
||||||
:scroll-x="1600"
|
:scroll-x="bookStore.selectBookTask.openVideoGenerate ? 1700 : 1600"
|
||||||
:max-height="tableHeight"
|
:max-height="tableHeight"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { useBookStore, usePresetStore, useSoftwareStore } from '@/renderer/src/stores'
|
import { useBookStore, useSoftwareStore } from '@/renderer/src/stores'
|
||||||
import DatatableAfterGpt from './DatatableAfterGpt.vue'
|
import DatatableAfterGpt from './DatatableAfterGpt.vue'
|
||||||
import DatatableCharacterAndSceneAndStyle from './DatatableCharacterAndSceneAndStyle.vue'
|
import DatatableCharacterAndSceneAndStyle from './DatatableCharacterAndSceneAndStyle.vue'
|
||||||
import DataTableGptPrompt from './DataTableGptPrompt.vue'
|
import DataTableGptPrompt from './DataTableGptPrompt.vue'
|
||||||
import { DEFINE_STRING } from '@/define/ipcDefineString'
|
import { DEFINE_STRING } from '@/define/ipcDefineString'
|
||||||
import DatatableGenerateImage from './DatatableGenerateImage.vue'
|
import DatatableGenerateImage from './DatatableGenerateImage.vue'
|
||||||
|
import VideoDisplay from '@/renderer/src/components/common/VideoDisplay.vue'
|
||||||
|
|
||||||
import DataTableAction from './DataTableAction.vue'
|
import DataTableAction from './DataTableAction.vue'
|
||||||
import DatatableHeaderImage from './DatatableHeaderImage.vue'
|
import DatatableHeaderImage from './DatatableHeaderImage.vue'
|
||||||
@ -124,7 +125,17 @@ const columns = computed(() => {
|
|||||||
key: 'generateVideoPath',
|
key: 'generateVideoPath',
|
||||||
className: 'empty-margin',
|
className: 'empty-margin',
|
||||||
width: '200',
|
width: '200',
|
||||||
minWidth: bookStore.selectBookTask.openVideoGenerate ? 130 : 0
|
minWidth: bookStore.selectBookTask.openVideoGenerate ? 130 : 0,
|
||||||
|
render(row, index) {
|
||||||
|
return h(VideoDisplay, {
|
||||||
|
videoPath: row.generateVideoPath,
|
||||||
|
containerHeight: softwareStore.originalDatatableHeight + 'px',
|
||||||
|
containerWidth: '100%',
|
||||||
|
autoSize: true,
|
||||||
|
emptyDescription: t('暂无视频'),
|
||||||
|
emptySize: 'small'
|
||||||
|
})
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// 出图
|
// 出图
|
||||||
|
|||||||
@ -9,7 +9,7 @@
|
|||||||
:options="getImageCategoryOptions()"
|
:options="getImageCategoryOptions()"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<n-dropdown
|
<TooltipDropdown
|
||||||
trigger="click"
|
trigger="click"
|
||||||
:options="blockOptions"
|
:options="blockOptions"
|
||||||
@select="dropdownSelectHandle"
|
@select="dropdownSelectHandle"
|
||||||
@ -24,12 +24,12 @@
|
|||||||
</template>
|
</template>
|
||||||
{{ t('更多操作') }}
|
{{ t('更多操作') }}
|
||||||
</n-button>
|
</n-button>
|
||||||
</n-dropdown>
|
</TooltipDropdown>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed, h } from 'vue'
|
||||||
import { useMessage, useDialog, NIcon, NSelect, NButton, NDropdown, NDataTable } from 'naive-ui'
|
import { useMessage, useDialog, NIcon, NSelect, NButton, NDropdown, NDataTable } from 'naive-ui'
|
||||||
import { ChevronDown, ChevronUp } from '@vicons/ionicons5'
|
import { ChevronDown, ChevronUp } from '@vicons/ionicons5'
|
||||||
|
|
||||||
@ -39,7 +39,7 @@ import { isEmpty } from 'lodash'
|
|||||||
import { useMD } from '@/renderer/src/hooks/useMD'
|
import { useMD } from '@/renderer/src/hooks/useMD'
|
||||||
import { t } from '@/i18n'
|
import { t } from '@/i18n'
|
||||||
|
|
||||||
const { showErrorDialog } = useMD()
|
const { showErrorDialog, showSuccessDialog } = useMD()
|
||||||
|
|
||||||
let props = defineProps({
|
let props = defineProps({
|
||||||
style: undefined
|
style: undefined
|
||||||
@ -84,6 +84,98 @@ async function handleUpdateValue(value) {
|
|||||||
message.success(t('修改成功'))
|
message.success(t('修改成功'))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 同步主图
|
||||||
|
async function handleSyncMainImage() {
|
||||||
|
let da = dialog.warning({
|
||||||
|
title: t('操作提示'),
|
||||||
|
content: () =>
|
||||||
|
h(
|
||||||
|
'div',
|
||||||
|
{
|
||||||
|
style: { whiteSpace: 'pre-line' }
|
||||||
|
},
|
||||||
|
t(
|
||||||
|
'检查所有的分镜信息,检查没有主图的分镜,判断对应的图片输出文件夹中是否有和分镜同名的图片,若有,则同步到对应分镜的主图中,若没有,则跳过!\n\n 注意:该操作不会覆盖已经有主图的分镜!\n\n 该操作适用于手动将图片放入输出文件夹后,同步到分镜中!点击右侧"打开文件夹"打开当前批次的图片输出文件夹'
|
||||||
|
)
|
||||||
|
),
|
||||||
|
positiveText: t('继续'),
|
||||||
|
negativeText: t('取消'),
|
||||||
|
onPositiveClick: async () => {
|
||||||
|
try {
|
||||||
|
da?.destroy()
|
||||||
|
softwareStore.spin.spinning = true
|
||||||
|
softwareStore.spin.tip = t('正在同步主图信息,请稍后...')
|
||||||
|
let res = await window.book.SyncMainImageForBookTask(bookStore.selectBookTask.id)
|
||||||
|
console.log('同步主图结果', res)
|
||||||
|
if (res.code != 1) {
|
||||||
|
throw new Error(res.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修改数据
|
||||||
|
for (let i = 0; i < res.data.length; i++) {
|
||||||
|
const element = res.data[i]
|
||||||
|
let findIndex = bookStore.selectBookTaskDetail.findIndex((item) => item.id == element.id)
|
||||||
|
if (findIndex != -1) {
|
||||||
|
bookStore.selectBookTaskDetail[findIndex].outImagePath = element.outImagePath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showSuccessDialog(t('成功'), res.message)
|
||||||
|
} catch (error) {
|
||||||
|
showErrorDialog(t('失败'), t(error.message))
|
||||||
|
} finally {
|
||||||
|
softwareStore.spin.spinning = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 同步子图到分镜
|
||||||
|
async function handleSyncSubMainImage() {
|
||||||
|
let da = dialog.warning({
|
||||||
|
title: t('操作提示'),
|
||||||
|
content: () =>
|
||||||
|
h(
|
||||||
|
'div',
|
||||||
|
{
|
||||||
|
style: { whiteSpace: 'pre-line' }
|
||||||
|
},
|
||||||
|
t(
|
||||||
|
'检查所有的分镜信息,判断分镜的子图文件夹中的图片是否都在分镜的子图列表中,若没有,则添加进去!\n\n 注意:该操作不会删除分镜中已经有的子图信息!而是根据文件中的图片信息添加到分镜的子图数据中\n\n 该操作适用于手动将子图放入子图文件夹后,同步到分镜中!点击右侧"打开文件夹"打开当前批次的图片输出文件夹'
|
||||||
|
)
|
||||||
|
),
|
||||||
|
positiveText: t('继续'),
|
||||||
|
negativeText: t('取消'),
|
||||||
|
onPositiveClick: async () => {
|
||||||
|
try {
|
||||||
|
da?.destroy()
|
||||||
|
softwareStore.spin.spinning = true
|
||||||
|
softwareStore.spin.tip = t('正在同步子图信息,请稍后...')
|
||||||
|
let res = await window.book.SyncSubImageForBookTask(bookStore.selectBookTask.id)
|
||||||
|
console.log('同步子图结果', res)
|
||||||
|
if (res.code != 1) {
|
||||||
|
throw new Error(res.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修改数据
|
||||||
|
for (let i = 0; i < res.data.length; i++) {
|
||||||
|
const element = res.data[i]
|
||||||
|
let findIndex = bookStore.selectBookTaskDetail.findIndex((item) => item.id == element.id)
|
||||||
|
if (findIndex != -1) {
|
||||||
|
bookStore.selectBookTaskDetail[findIndex].subImagePath = [...element.subImagePath]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showSuccessDialog(t('成功'), res.message)
|
||||||
|
} catch (error) {
|
||||||
|
showErrorDialog(t('失败'), t(error.message))
|
||||||
|
} finally {
|
||||||
|
softwareStore.spin.spinning = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// 处理一键锁定 一键解锁的操作
|
// 处理一键锁定 一键解锁的操作
|
||||||
async function handleImageLockOperation(type) {
|
async function handleImageLockOperation(type) {
|
||||||
let title = type == 'lock' ? t('一键锁定图片提示') : t('一键解锁图片提示')
|
let title = type == 'lock' ? t('一键锁定图片提示') : t('一键解锁图片提示')
|
||||||
@ -156,6 +248,13 @@ async function dropdownSelectHandle(key) {
|
|||||||
case 'oneToFour':
|
case 'oneToFour':
|
||||||
await OneToFourBookTask()
|
await OneToFourBookTask()
|
||||||
break
|
break
|
||||||
|
case 'syncMainImage':
|
||||||
|
await handleSyncMainImage()
|
||||||
|
break
|
||||||
|
case 'syncSubImage':
|
||||||
|
await handleSyncSubMainImage()
|
||||||
|
break
|
||||||
|
|
||||||
default:
|
default:
|
||||||
message.error(t('未知操作'))
|
message.error(t('未知操作'))
|
||||||
break
|
break
|
||||||
@ -260,6 +359,28 @@ async function OneToFourBookTask() {
|
|||||||
// 计算菜单选项,根据当前图像类别动态生成
|
// 计算菜单选项,根据当前图像类别动态生成
|
||||||
const blockOptions = computed(() => {
|
const blockOptions = computed(() => {
|
||||||
const baseOptions = [
|
const baseOptions = [
|
||||||
|
{
|
||||||
|
label: t('同步主图文件'),
|
||||||
|
tooltip: t(
|
||||||
|
'检查所有的分镜信息,检查没有主图的分镜,判断对应的图片输出文件夹中是否有和分镜同名的图片,若有,则同步到对应分镜的主图中,若没有,则跳过!'
|
||||||
|
),
|
||||||
|
key: 'syncMainImage'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'divider',
|
||||||
|
key: 'd1'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('同步子图文件'),
|
||||||
|
key: 'syncSubImage',
|
||||||
|
tooltip: t(
|
||||||
|
'检查所有的分镜信息,判断分镜的子图文件夹中的图片是否都在分镜的子图列表中,若没有,则添加进去!'
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'divider',
|
||||||
|
key: 'd1'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: t('一键锁定'),
|
label: t('一键锁定'),
|
||||||
key: 'lock'
|
key: 'lock'
|
||||||
|
|||||||
@ -1,27 +1,27 @@
|
|||||||
<template>
|
<template>
|
||||||
<div style="display: flex; align-items: center; font-size: 12px; font-weight: bold">
|
<div style="display: flex; align-items: center; font-size: 12px; font-weight: bold">
|
||||||
<n-space>
|
<n-space>
|
||||||
<div style="display: flex; align-items: center">
|
<n-space align="center" size="small">
|
||||||
<div>{{ t('音频文件') }}</div>
|
<div>{{ t('音频文件') }}</div>
|
||||||
<span class="clickable-link" @click="OpenFile(audioPath)">{{
|
<span class="clickable-link" @click="OpenFile(audioPath)">{{
|
||||||
getBasename(audioPath)
|
getBasename(audioPath)
|
||||||
}}</span>
|
}}</span>
|
||||||
</div>
|
</n-space>
|
||||||
|
|
||||||
<div style="display: flex; align-items: center">
|
<n-space align="center" size="small">
|
||||||
<div>{{ t('SRT文件') }}</div>
|
<div>{{ t('SRT文件') }}</div>
|
||||||
<span class="clickable-link" @click="OpenFile(srtPath)">{{ getBasename(srtPath) }}</span>
|
<span class="clickable-link" @click="OpenFile(srtPath)">{{ getBasename(srtPath) }}</span>
|
||||||
</div>
|
</n-space>
|
||||||
|
|
||||||
<div style="display: flex; align-items: center">
|
<n-space align="center" size="small">
|
||||||
<div>{{ t('出图文件夹') }}</div>
|
<div>{{ t('出图文件夹') }}</div>
|
||||||
<span class="clickable-link" @click="OpenFolder(imageFolderPath)">{{
|
<span class="clickable-link" @click="OpenFolder(imageFolderPath)">{{
|
||||||
getBasename(imageFolderPath)
|
getBasename(imageFolderPath)
|
||||||
}}</span>
|
}}</span>
|
||||||
</div>
|
</n-space>
|
||||||
|
|
||||||
<!-- 推理提示词进度 -->
|
<!-- 推理提示词进度 -->
|
||||||
<div style="display: flex; align-items: center">
|
<n-space align="center" size="small">
|
||||||
<div>{{ t('提示词') }}</div>
|
<div>{{ t('提示词') }}</div>
|
||||||
<n-progress
|
<n-progress
|
||||||
type="line"
|
type="line"
|
||||||
@ -31,11 +31,12 @@
|
|||||||
@click="ErrorPosition('gptPrompt')"
|
@click="ErrorPosition('gptPrompt')"
|
||||||
rail-color="#bbb"
|
rail-color="#bbb"
|
||||||
/>
|
/>
|
||||||
</div>
|
</n-space>
|
||||||
|
|
||||||
<!-- 反推提示词进度 -->
|
<!-- 反推提示词进度 -->
|
||||||
<div
|
<n-space
|
||||||
style="display: flex; align-items: center"
|
align="center"
|
||||||
|
size="small"
|
||||||
v-if="
|
v-if="
|
||||||
bookStore.selectBook.type == 'mj_reverse' || bookStore.selectBook.type == 'sd_reverse'
|
bookStore.selectBook.type == 'mj_reverse' || bookStore.selectBook.type == 'sd_reverse'
|
||||||
"
|
"
|
||||||
@ -50,11 +51,11 @@
|
|||||||
@click="ErrorPosition('reverseGptPrompt')"
|
@click="ErrorPosition('reverseGptPrompt')"
|
||||||
rail-color="#bbb"
|
rail-color="#bbb"
|
||||||
/>
|
/>
|
||||||
</div>
|
</n-space>
|
||||||
|
|
||||||
<!-- 出图进度 -->
|
<!-- 出图进度 -->
|
||||||
<div style="display: flex; align-items: center">
|
<n-space align="center" size="small">
|
||||||
<div style="display: flex; align-items: center">
|
<n-space align="center">
|
||||||
<div>{{ t('出图') }}</div>
|
<div>{{ t('出图') }}</div>
|
||||||
<n-progress
|
<n-progress
|
||||||
type="line"
|
type="line"
|
||||||
@ -64,15 +65,15 @@
|
|||||||
@click="ErrorPosition('image')"
|
@click="ErrorPosition('image')"
|
||||||
rail-color="#bbb"
|
rail-color="#bbb"
|
||||||
/>
|
/>
|
||||||
</div>
|
</n-space>
|
||||||
</div>
|
</n-space>
|
||||||
</n-space>
|
</n-space>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { NProgress, useMessage } from 'naive-ui'
|
import { NProgress, NSpace, useMessage } from 'naive-ui'
|
||||||
import { useSoftwareStore, useBookStore, useThemeStore } from '@/renderer/src/stores'
|
import { useSoftwareStore, useBookStore, useThemeStore } from '@/renderer/src/stores'
|
||||||
import { isEmpty } from 'lodash'
|
import { isEmpty } from 'lodash'
|
||||||
import { getBasename } from '@/renderer/src/common/file'
|
import { getBasename } from '@/renderer/src/common/file'
|
||||||
|
|||||||
@ -1,249 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="subprojects-cards">
|
|
||||||
<div
|
|
||||||
v-for="subproject in bookStore.selectBook.children"
|
|
||||||
:key="subproject.id"
|
|
||||||
class="subproject-card-wrapper"
|
|
||||||
>
|
|
||||||
<div class="subproject-card">
|
|
||||||
<div class="subproject-content">
|
|
||||||
<div class="subproject-left">
|
|
||||||
<div
|
|
||||||
:style="{
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyItems: 'center'
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<h5 class="subproject-name" :title="subproject.name">{{ subproject.name }}</h5>
|
|
||||||
</div>
|
|
||||||
<div class="subproject-actions">
|
|
||||||
<n-tooltip trigger="hover" placement="top">
|
|
||||||
<template #trigger>
|
|
||||||
<n-button
|
|
||||||
size="small"
|
|
||||||
quaternary
|
|
||||||
circle
|
|
||||||
@click.stop="enterBookTaskDetail(subproject)"
|
|
||||||
>
|
|
||||||
<n-icon :size="20"><Enter /></n-icon>
|
|
||||||
</n-button>
|
|
||||||
</template>
|
|
||||||
{{ t('进入小说批次信息') }}
|
|
||||||
</n-tooltip>
|
|
||||||
|
|
||||||
<n-tooltip trigger="hover" placement="top">
|
|
||||||
<template #trigger>
|
|
||||||
<n-button
|
|
||||||
size="small"
|
|
||||||
type="error"
|
|
||||||
quaternary
|
|
||||||
circle
|
|
||||||
@click="deleteProject(subproject)"
|
|
||||||
>
|
|
||||||
<n-icon :size="20"><Trash /></n-icon>
|
|
||||||
</n-button>
|
|
||||||
</template>
|
|
||||||
{{ t('删除子项目') }}
|
|
||||||
</n-tooltip>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="subproject-tags">
|
|
||||||
<n-tooltip trigger="hover" placement="top">
|
|
||||||
<template #trigger>
|
|
||||||
<n-tag size="small" :type="getImageCategoryLabel(subproject.imageCategory).type">
|
|
||||||
{{ getImageCategoryLabel(subproject.imageCategory).label }}
|
|
||||||
</n-tag>
|
|
||||||
</template>
|
|
||||||
{{ t('批次出图方式') }}
|
|
||||||
</n-tooltip>
|
|
||||||
<n-tooltip trigger="hover" placement="top">
|
|
||||||
<template #trigger>
|
|
||||||
<n-tag size="small" :type="GetBookBackTaskStatusLabel(subproject.status).type">
|
|
||||||
{{ GetBookBackTaskStatusLabel(subproject.status).label }}
|
|
||||||
</n-tag>
|
|
||||||
</template>
|
|
||||||
{{ t('状态') }}
|
|
||||||
</n-tooltip>
|
|
||||||
<n-tooltip trigger="hover" placement="top">
|
|
||||||
<template #trigger>
|
|
||||||
<n-tag size="small" type="info">
|
|
||||||
{{ getName(subproject.name) }}
|
|
||||||
</n-tag>
|
|
||||||
</template>
|
|
||||||
{{ t('批次号') }}
|
|
||||||
</n-tooltip>
|
|
||||||
</div>
|
|
||||||
<div class="subproject-date">
|
|
||||||
{{
|
|
||||||
t('创建时间 {time}', {
|
|
||||||
time: formatDateToString(subproject.createTime)
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { getImageCategoryLabel } from '@/define/data/imageData'
|
|
||||||
import { GetBookBackTaskStatusLabel } from '@/define/enum/bookEnum'
|
|
||||||
import { formatDateToString } from '@/renderer/src/common/time'
|
|
||||||
import { toRGBA } from '@/renderer/src/common/color'
|
|
||||||
import {
|
|
||||||
useBookStore,
|
|
||||||
useThemeStore,
|
|
||||||
useSoftwareStore,
|
|
||||||
usePresetStore
|
|
||||||
} from '@/renderer/src/stores'
|
|
||||||
import { Trash, Enter } from '@vicons/ionicons5'
|
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
import { getShowTagsData } from '@/renderer/src/common/book'
|
|
||||||
import { isEmpty } from 'lodash'
|
|
||||||
import { t } from '@/i18n'
|
|
||||||
|
|
||||||
const bookStore = useBookStore()
|
|
||||||
const themeStore = useThemeStore()
|
|
||||||
const softwareStore = useSoftwareStore()
|
|
||||||
const presetStore = usePresetStore()
|
|
||||||
const router = useRouter()
|
|
||||||
const message = useMessage()
|
|
||||||
const dialog = useDialog()
|
|
||||||
|
|
||||||
const emit = defineEmits(['update:bookTask'])
|
|
||||||
|
|
||||||
function getName(name) {
|
|
||||||
if (!isEmpty(name)) {
|
|
||||||
let sp = name.split('_')
|
|
||||||
return sp[sp.length - 1]
|
|
||||||
} else {
|
|
||||||
return t('未命名')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 进入小说批次信息
|
|
||||||
async function enterBookTaskDetail(subproject) {
|
|
||||||
softwareStore.spin.spinning = true
|
|
||||||
softwareStore.spin.text = t('正在加载小说批次信息...')
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
|
||||||
// 这边加载数据
|
|
||||||
let res = await window.book.GetBookTaskDetailDataByCondition({
|
|
||||||
bookTaskId: subproject.id
|
|
||||||
})
|
|
||||||
if (res.code != 1) {
|
|
||||||
message.error(res.message)
|
|
||||||
softwareStore.spin.spinning = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
console.log('小说批次信息', res.data)
|
|
||||||
bookStore.selectBookTaskDetail = res.data
|
|
||||||
bookStore.selectBookTask = { ...subproject }
|
|
||||||
|
|
||||||
// 加载标签信息
|
|
||||||
let tagRes = await getShowTagsData({
|
|
||||||
isShow: true
|
|
||||||
})
|
|
||||||
// 做一下数据的处理
|
|
||||||
presetStore.showCharacterPresetArray = tagRes.character
|
|
||||||
presetStore.showScenePresetArray = tagRes.scene
|
|
||||||
presetStore.showStylePresetArray = tagRes.style
|
|
||||||
|
|
||||||
router.push('/original-book-detail/' + subproject.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function deleteProject(subproject) {
|
|
||||||
dialog.warning({
|
|
||||||
title: t('操作确认'),
|
|
||||||
positiveText: t('确定'),
|
|
||||||
negativeText: t('取消'),
|
|
||||||
content: t(
|
|
||||||
'确定要删除小说任务吗?该操作不可逆,会将对应的子任务数据和分镜数据全部删除,请谨慎操作!'
|
|
||||||
),
|
|
||||||
onPositiveClick: async () => {
|
|
||||||
let res = await window.book.DeleteBookTaskByIds([subproject.id])
|
|
||||||
console.log('删除小说任务', res)
|
|
||||||
if (res.code == 1) {
|
|
||||||
// 加载任务列表
|
|
||||||
emit('update:bookTask', res.data)
|
|
||||||
// 删除成功
|
|
||||||
message.success(t('删除小说任务并刷新数据成功'))
|
|
||||||
} else {
|
|
||||||
message.error(res.message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
/* 子项目卡片列表 */
|
|
||||||
.subprojects-cards {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 16px;
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 子项目卡片包装器 */
|
|
||||||
.subproject-card-wrapper {
|
|
||||||
width: 250px;
|
|
||||||
flex-grow: 0;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 子项目卡片 */
|
|
||||||
.subproject-card {
|
|
||||||
padding: 14px;
|
|
||||||
border-radius: 8px;
|
|
||||||
background-color: v-bind('toRGBA(themeStore.menuPrimaryColor, 0.6)');
|
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subproject-card:hover {
|
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
|
||||||
transform: translateY(-2px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.subproject-content {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subproject-left {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subproject-name {
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 500;
|
|
||||||
margin: 0 0 6px 0;
|
|
||||||
white-space: nowrap; /* 防止文本换行 */
|
|
||||||
overflow: hidden; /* 隐藏溢出内容 */
|
|
||||||
text-overflow: ellipsis; /* 显示省略号 */
|
|
||||||
max-width: 120px; /* 设置最大宽度,根据实际需求调整 */
|
|
||||||
}
|
|
||||||
|
|
||||||
.subproject-tags {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 4px;
|
|
||||||
margin-bottom: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subproject-date {
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subproject-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 4px;
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -2,7 +2,7 @@
|
|||||||
<n-card
|
<n-card
|
||||||
class="task-card"
|
class="task-card"
|
||||||
hoverable
|
hoverable
|
||||||
@dblclick="handleOpenTask"
|
@dblclick="handleDBOpenTask"
|
||||||
:header-style="{
|
:header-style="{
|
||||||
padding: '12px 16px',
|
padding: '12px 16px',
|
||||||
background: themeStore.menuPrimaryShadow,
|
background: themeStore.menuPrimaryShadow,
|
||||||
@ -16,7 +16,6 @@
|
|||||||
<template #header>
|
<template #header>
|
||||||
<n-space justify="space-between" align="center">
|
<n-space justify="space-between" align="center">
|
||||||
<!-- 左侧:TASK图标 + 任务名称 -->
|
<!-- 左侧:TASK图标 + 任务名称 -->
|
||||||
|
|
||||||
<div class="task-icon">
|
<div class="task-icon">
|
||||||
<n-icon
|
<n-icon
|
||||||
size="24"
|
size="24"
|
||||||
@ -151,9 +150,13 @@
|
|||||||
</template>
|
</template>
|
||||||
<template #default>
|
<template #default>
|
||||||
{{
|
{{
|
||||||
t('分镜出图的进度:{progress}% ', {
|
props.type == 'mediaToVideo'
|
||||||
progress: progress
|
? t('视频生成的进度:{progress}% ', {
|
||||||
})
|
progress: progress
|
||||||
|
})
|
||||||
|
: t('分镜出图的进度:{progress}% ', {
|
||||||
|
progress: progress
|
||||||
|
})
|
||||||
}}
|
}}
|
||||||
</template>
|
</template>
|
||||||
</n-tooltip>
|
</n-tooltip>
|
||||||
@ -277,7 +280,9 @@ function handleButtonClick(event, action, ...args) {
|
|||||||
// 计算进度
|
// 计算进度
|
||||||
const progress = computed(() => {
|
const progress = computed(() => {
|
||||||
// 这里可以根据实际业务逻辑计算进度
|
// 这里可以根据实际业务逻辑计算进度
|
||||||
return Math.floor(props.bookTask?.imageVideoProgress?.imageRate ?? 0) // 示例:随机进度
|
return props.type == 'mediaToVideo'
|
||||||
|
? Math.floor(props.bookTask?.imageVideoProgress?.videoRate ?? 0)
|
||||||
|
: Math.floor(props.bookTask?.imageVideoProgress?.imageRate ?? 0)
|
||||||
})
|
})
|
||||||
|
|
||||||
// 处理任务操作
|
// 处理任务操作
|
||||||
@ -306,9 +311,23 @@ async function handleTaskAction(key) {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
async function handleDBOpenTask() {
|
||||||
|
switch (props.type) {
|
||||||
|
case 'origin':
|
||||||
|
handleOpenTask()
|
||||||
|
break
|
||||||
|
case 'mediaToVideo':
|
||||||
|
handleOpenVideoInfo()
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
message.error(t('未知的任务类型'))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 处理打开任务
|
// 处理打开任务
|
||||||
async function handleOpenTask() {
|
async function handleOpenTask() {
|
||||||
|
console.log('打开任务', props.bookTask, props.type)
|
||||||
// 如果刚刚点击了操作按钮,则不执行双击事件
|
// 如果刚刚点击了操作按钮,则不执行双击事件
|
||||||
if (isActionClicked.value) {
|
if (isActionClicked.value) {
|
||||||
return
|
return
|
||||||
|
|||||||
@ -10,12 +10,7 @@
|
|||||||
>
|
>
|
||||||
{{ displayText }}
|
{{ displayText }}
|
||||||
</n-tag>
|
</n-tag>
|
||||||
<n-text
|
<n-text v-else v-bind="componentProps" :class="[customClass]" :style="ellipsisStyle">
|
||||||
v-else
|
|
||||||
v-bind="componentProps"
|
|
||||||
:class="[customClass]"
|
|
||||||
:style="ellipsisStyle"
|
|
||||||
>
|
|
||||||
{{ displayText }}
|
{{ displayText }}
|
||||||
</n-text>
|
</n-text>
|
||||||
</template>
|
</template>
|
||||||
@ -23,7 +18,12 @@
|
|||||||
<!-- 正常的省略显示模式 -->
|
<!-- 正常的省略显示模式 -->
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<!-- 如果内容超出限制则显示 tooltip -->
|
<!-- 如果内容超出限制则显示 tooltip -->
|
||||||
<n-tooltip v-if="isOverflow" :show-arrow="showArrow" :trigger="trigger" :placement="placement">
|
<n-tooltip
|
||||||
|
v-if="isOverflow"
|
||||||
|
:show-arrow="showArrow"
|
||||||
|
:trigger="trigger"
|
||||||
|
:placement="placement"
|
||||||
|
>
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<n-tag
|
<n-tag
|
||||||
v-if="component === 'n-tag'"
|
v-if="component === 'n-tag'"
|
||||||
@ -290,28 +290,37 @@ defineExpose({
|
|||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
min-height: 28px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-ellipsis-content {
|
.text-ellipsis-content {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
display: inline-block;
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
max-width: inherit;
|
max-width: inherit;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
min-height: 24px;
|
||||||
|
padding: 2px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-ellipsis-content:deep(.n-tag) {
|
.text-ellipsis-content:deep(.n-tag) {
|
||||||
display: inline-flex;
|
display: inline-flex !important;
|
||||||
align-items: center;
|
align-items: center !important;
|
||||||
|
min-height: 24px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-ellipsis-content:deep(.n-tag__content) {
|
.text-ellipsis-content:deep(.n-tag__content) {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
display: inline-block;
|
display: flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
line-height: 1.4 !important;
|
||||||
|
min-height: 20px !important;
|
||||||
|
padding: 2px 6px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-ellipsis-content:deep(.n-text) {
|
.text-ellipsis-content:deep(.n-text) {
|
||||||
@ -319,8 +328,10 @@ defineExpose({
|
|||||||
text-overflow: ellipsis !important;
|
text-overflow: ellipsis !important;
|
||||||
white-space: nowrap !important;
|
white-space: nowrap !important;
|
||||||
max-width: inherit !important;
|
max-width: inherit !important;
|
||||||
display: inline-block !important;
|
display: inline-flex !important;
|
||||||
|
align-items: center !important;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
padding: 2px 6px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip-content {
|
.tooltip-content {
|
||||||
@ -335,5 +346,6 @@ defineExpose({
|
|||||||
max-width: inherit;
|
max-width: inherit;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
min-height: 24px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
173
src/renderer/src/components/common/VideoDisplay.vue
Normal file
173
src/renderer/src/components/common/VideoDisplay.vue
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="video-display-container"
|
||||||
|
:style="{
|
||||||
|
height: containerHeight,
|
||||||
|
width: containerWidth || '100%',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
overflow: 'hidden'
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<video
|
||||||
|
v-if="videoPath"
|
||||||
|
ref="videoRef"
|
||||||
|
:src="videoPath"
|
||||||
|
controls
|
||||||
|
:style="videoStyle"
|
||||||
|
@loadedmetadata="handleVideoLoaded"
|
||||||
|
/>
|
||||||
|
<n-empty v-else :description="emptyDescription" :size="emptySize" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { NEmpty } from 'naive-ui'
|
||||||
|
import { t } from '@/i18n'
|
||||||
|
|
||||||
|
// 定义 props
|
||||||
|
const props = defineProps({
|
||||||
|
// 视频路径
|
||||||
|
videoPath: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
// 容器高度
|
||||||
|
containerHeight: {
|
||||||
|
type: String,
|
||||||
|
default: '130px'
|
||||||
|
},
|
||||||
|
// 容器宽度
|
||||||
|
containerWidth: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
// 是否自适应大小 - 自动撑满容器同时保持比例
|
||||||
|
autoSize: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
// 视频宽度(仅在autoSize为false时生效)
|
||||||
|
videoWidth: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
// 视频高度(仅在autoSize为false时生效)
|
||||||
|
videoHeight: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
// 视频最大宽度
|
||||||
|
maxWidth: {
|
||||||
|
type: String,
|
||||||
|
default: '160px'
|
||||||
|
},
|
||||||
|
// 视频最大高度
|
||||||
|
maxHeight: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
// 视频填充方式
|
||||||
|
objectFit: {
|
||||||
|
type: String,
|
||||||
|
default: 'cover', // 使用cover确保填满容器
|
||||||
|
validator: (value) => ['fill', 'contain', 'cover', 'none', 'scale-down'].includes(value)
|
||||||
|
},
|
||||||
|
// 圆角大小
|
||||||
|
borderRadius: {
|
||||||
|
type: String,
|
||||||
|
default: '4px'
|
||||||
|
},
|
||||||
|
// 空状态描述文字
|
||||||
|
emptyDescription: {
|
||||||
|
type: String,
|
||||||
|
default: () => t('暂无选择视频')
|
||||||
|
},
|
||||||
|
// 空状态大小
|
||||||
|
emptySize: {
|
||||||
|
type: String,
|
||||||
|
default: 'small'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const videoRef = ref(null)
|
||||||
|
const videoNaturalWidth = ref(0)
|
||||||
|
const videoNaturalHeight = ref(0)
|
||||||
|
const isVideoLoaded = ref(false)
|
||||||
|
|
||||||
|
// 处理视频加载完成
|
||||||
|
const handleVideoLoaded = () => {
|
||||||
|
if (videoRef.value) {
|
||||||
|
videoNaturalWidth.value = videoRef.value.videoWidth
|
||||||
|
videoNaturalHeight.value = videoRef.value.videoHeight
|
||||||
|
isVideoLoaded.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算视频样式
|
||||||
|
const videoStyle = computed(() => {
|
||||||
|
if (!props.autoSize) {
|
||||||
|
// 非自适应模式,使用原有逻辑
|
||||||
|
return {
|
||||||
|
width: props.videoWidth || 'auto',
|
||||||
|
height: props.videoHeight || props.containerHeight,
|
||||||
|
objectFit: props.objectFit,
|
||||||
|
borderRadius: props.borderRadius
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自适应模式下的逻辑
|
||||||
|
if (!isVideoLoaded.value || !videoNaturalWidth.value || !videoNaturalHeight.value) {
|
||||||
|
// 视频未加载完成时的默认样式
|
||||||
|
return {
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
objectFit: 'contain',
|
||||||
|
borderRadius: props.borderRadius
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取容器尺寸(移除px单位)
|
||||||
|
const containerWidthNum = parseFloat(props.containerWidth || '100')
|
||||||
|
const containerHeightNum = parseFloat(props.containerHeight || '130')
|
||||||
|
|
||||||
|
// 计算视频和容器的宽高比
|
||||||
|
const videoAspectRatio = videoNaturalWidth.value / videoNaturalHeight.value
|
||||||
|
const containerAspectRatio = containerWidthNum / containerHeightNum
|
||||||
|
|
||||||
|
let finalWidth, finalHeight
|
||||||
|
|
||||||
|
if (videoAspectRatio > containerAspectRatio) {
|
||||||
|
// 视频比容器更宽,以容器宽度为准
|
||||||
|
finalWidth = '100%'
|
||||||
|
finalHeight = 'auto'
|
||||||
|
} else {
|
||||||
|
// 视频比容器更高,以容器高度为准
|
||||||
|
finalWidth = 'auto'
|
||||||
|
finalHeight = '100%'
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
width: finalWidth,
|
||||||
|
height: finalHeight,
|
||||||
|
objectFit: 'contain', // 保持比例,完整显示
|
||||||
|
borderRadius: props.borderRadius
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.video-display-container {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-display-container video {
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-display-container video:hover {
|
||||||
|
transform: scale(1.02);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
0
src/renderer/src/composables/useReactiveI18n.ts
Normal file
0
src/renderer/src/composables/useReactiveI18n.ts
Normal file
23
src/renderer/src/hooks/useFile.ts
Normal file
23
src/renderer/src/hooks/useFile.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { useMessage } from 'naive-ui'
|
||||||
|
|
||||||
|
export function useFile() {
|
||||||
|
|
||||||
|
const message = useMessage()
|
||||||
|
|
||||||
|
async function UploadImageToLaiTool(imagePath: string, type: "video" | "image") {
|
||||||
|
|
||||||
|
// 开始上传
|
||||||
|
let res = await window.system.UploadImageToLaiTool(imagePath, type)
|
||||||
|
if (res.code != 1) {
|
||||||
|
message.error(res.message)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let url = res.data.url;
|
||||||
|
return url;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
UploadImageToLaiTool
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -3,7 +3,12 @@ import { t } from "@/i18n"
|
|||||||
export function useMD() {
|
export function useMD() {
|
||||||
let dialog = useDialog()
|
let dialog = useDialog()
|
||||||
|
|
||||||
/** 调用一个错误的dialog */
|
/**
|
||||||
|
* 显示错误对话框
|
||||||
|
* @param {string} title - 对话框标题
|
||||||
|
* @param {string} content - 对话框内容
|
||||||
|
* @description 调用一个错误类型的对话框,用于显示错误信息或警告用户操作失败
|
||||||
|
*/
|
||||||
function showErrorDialog(title, content) {
|
function showErrorDialog(title, content) {
|
||||||
dialog.error({
|
dialog.error({
|
||||||
title: title,
|
title: title,
|
||||||
@ -13,7 +18,12 @@ export function useMD() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 调用一个成功的dialog */
|
/**
|
||||||
|
* 显示成功对话框
|
||||||
|
* @param {string} title - 对话框标题
|
||||||
|
* @param {string} content - 对话框内容
|
||||||
|
* @description 调用一个成功类型的对话框,用于显示操作成功信息或确认用户操作完成
|
||||||
|
*/
|
||||||
function showSuccessDialog(title, content) {
|
function showSuccessDialog(title, content) {
|
||||||
dialog.success({
|
dialog.success({
|
||||||
title: title,
|
title: title,
|
||||||
|
|||||||
30
src/renderer/src/hooks/useSimple.ts
Normal file
30
src/renderer/src/hooks/useSimple.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { t } from "@/i18n"
|
||||||
|
import { isEmpty } from "lodash"
|
||||||
|
|
||||||
|
|
||||||
|
export function useSimple() {
|
||||||
|
|
||||||
|
const message = useMessage()
|
||||||
|
|
||||||
|
// 复制任务ID
|
||||||
|
async function CopyData(data: string) {
|
||||||
|
try {
|
||||||
|
if (!data || isEmpty(data)) {
|
||||||
|
message.warning(t("复制失败,复制内容为空"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await navigator.clipboard.writeText(data)
|
||||||
|
message.success(t('复制成功'))
|
||||||
|
} catch (err) {
|
||||||
|
message.error(
|
||||||
|
t('复制失败:{error}', {
|
||||||
|
error: (err as Error).message
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
CopyData
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import { createRouter, createWebHashHistory } from 'vue-router'
|
import { createRouter, createWebHashHistory } from 'vue-router'
|
||||||
import Home from '../views/Home.vue'
|
import Home from '../views/Home.vue'
|
||||||
import { toolsData } from '@renderer/common/toolData'
|
import { getToolsData } from '@renderer/common/toolData'
|
||||||
import { t } from '@/i18n'
|
import { t } from '@/i18n'
|
||||||
|
|
||||||
// 创建404页面组件(可以是单独文件,这里用组件选项简化)
|
// 创建404页面组件(可以是单独文件,这里用组件选项简化)
|
||||||
@ -79,7 +79,7 @@ const dynamicRoutes = () => {
|
|||||||
let defaultRoutes = routes
|
let defaultRoutes = routes
|
||||||
|
|
||||||
// 检查tooldata添加路由
|
// 检查tooldata添加路由
|
||||||
toolsData.forEach((tool) => {
|
getToolsData().forEach((tool) => {
|
||||||
if (tool.action?.type == 'route') {
|
if (tool.action?.type == 'route') {
|
||||||
const route = {
|
const route = {
|
||||||
path: tool.action.route,
|
path: tool.action.route,
|
||||||
|
|||||||
0
src/renderer/src/stores/locale.ts
Normal file
0
src/renderer/src/stores/locale.ts
Normal file
@ -230,9 +230,6 @@ async function InitServerGptOptions() {
|
|||||||
// 开始处理
|
// 开始处理
|
||||||
for (let i = 0; i < prompt.data.data.length; i++) {
|
for (let i = 0; i < prompt.data.data.length; i++) {
|
||||||
const element = prompt.data.data[i]
|
const element = prompt.data.data[i]
|
||||||
if (!element.remark) {
|
|
||||||
element.remark = ''
|
|
||||||
}
|
|
||||||
|
|
||||||
let findIndex = pc.findIndex((item) => item.id == element.promptTypeId)
|
let findIndex = pc.findIndex((item) => item.id == element.promptTypeId)
|
||||||
if (findIndex != -1) {
|
if (findIndex != -1) {
|
||||||
|
|||||||
@ -51,7 +51,7 @@
|
|||||||
<ToolGrid :tools="filteredTools" @tool-click="handleToolClick" />
|
<ToolGrid :tools="filteredTools" @tool-click="handleToolClick" />
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
<n-tab-pane
|
<n-tab-pane
|
||||||
v-for="category in categories"
|
v-for="category in getCategories()"
|
||||||
:key="category.key"
|
:key="category.key"
|
||||||
:name="category.key"
|
:name="category.key"
|
||||||
:tab="category.label"
|
:tab="category.label"
|
||||||
@ -65,7 +65,7 @@
|
|||||||
<div class="stats-section">
|
<div class="stats-section">
|
||||||
<n-space>
|
<n-space>
|
||||||
<n-statistic :label="t('工具总数')" :value="totalTools" />
|
<n-statistic :label="t('工具总数')" :value="totalTools" />
|
||||||
<n-statistic :label="t('分类数量')" :value="categories.length" />
|
<n-statistic :label="t('分类数量')" :value="getCategories().length" />
|
||||||
</n-space>
|
</n-space>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -76,7 +76,7 @@ import { ref, computed, onMounted } from 'vue'
|
|||||||
import { NInput, NSelect, NButton, NSpace, NTabs, NTabPane, NStatistic, NIcon } from 'naive-ui'
|
import { NInput, NSelect, NButton, NSpace, NTabs, NTabPane, NStatistic, NIcon } from 'naive-ui'
|
||||||
import { SearchOutline, RefreshOutline } from '@vicons/ionicons5'
|
import { SearchOutline, RefreshOutline } from '@vicons/ionicons5'
|
||||||
import ToolGrid from '@/renderer/src/components/ToolBox/ToolGrid.vue'
|
import ToolGrid from '@/renderer/src/components/ToolBox/ToolGrid.vue'
|
||||||
import { toolsData, categories } from '@/renderer/src/common/toolData'
|
import { getToolsData, getCategories } from '@/renderer/src/common/toolData'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { t } from '@/i18n'
|
import { t } from '@/i18n'
|
||||||
|
|
||||||
@ -91,19 +91,19 @@ let router = useRouter()
|
|||||||
|
|
||||||
// 计算属性
|
// 计算属性
|
||||||
const categoryOptions = computed(() =>
|
const categoryOptions = computed(() =>
|
||||||
categories.map((cat) => ({ label: cat.label, value: cat.key }))
|
getToolsData().map((cat) => ({ label: cat.label, value: cat.key }))
|
||||||
)
|
)
|
||||||
|
|
||||||
const tagOptions = computed(() => {
|
const tagOptions = computed(() => {
|
||||||
const allTags = new Set()
|
const allTags = new Set()
|
||||||
toolsData.forEach((tool) => {
|
getCategories().forEach((tool) => {
|
||||||
tool.tags?.forEach((tag) => allTags.add(tag))
|
tool.tags?.forEach((tag) => allTags.add(tag))
|
||||||
})
|
})
|
||||||
return Array.from(allTags).map((tag) => ({ label: tag, value: tag }))
|
return Array.from(allTags).map((tag) => ({ label: tag, value: tag }))
|
||||||
})
|
})
|
||||||
|
|
||||||
const filteredTools = computed(() => {
|
const filteredTools = computed(() => {
|
||||||
let tools = toolsData
|
let tools = getToolsData()
|
||||||
|
|
||||||
// 关键词搜索
|
// 关键词搜索
|
||||||
if (searchKeyword.value) {
|
if (searchKeyword.value) {
|
||||||
@ -134,11 +134,11 @@ const filteredTools = computed(() => {
|
|||||||
return tools
|
return tools
|
||||||
})
|
})
|
||||||
|
|
||||||
const totalTools = computed(() => toolsData.length)
|
const totalTools = computed(() => getToolsData().length)
|
||||||
|
|
||||||
// 方法
|
// 方法
|
||||||
function getToolsByCategory(categoryKey) {
|
function getToolsByCategory(categoryKey) {
|
||||||
return toolsData.filter((tool) => tool.category === categoryKey)
|
return getToolsData().filter((tool) => tool.category === categoryKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleToolClick(tool) {
|
function handleToolClick(tool) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user