V 3.4.0
1. 新增来推内置自营生图包 2. 新增 工具箱 菜单,添加 图片压缩 和 LaiTool图床(用作MJ垫图) 3. 新增一个新的推理模式 4. 修复剪映关键缩放大小计算错误 5. MJ生图包适配修改
This commit is contained in:
parent
7a774c48da
commit
87d3b7fc17
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "laitool",
|
||||
"version": "3.3.9",
|
||||
"version": "3.4.0",
|
||||
"description": "An AI tool for image processing, video processing, and other functions.",
|
||||
"main": "./out/main/index.js",
|
||||
"author": "laitool.cn",
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@ -10,7 +10,7 @@
|
||||
"translation_secret": "2234",
|
||||
"translation_auto": true,
|
||||
"theme": "light",
|
||||
"gpt_auto_inference": "storyFirst",
|
||||
"gpt_auto_inference": "laitoolStoryboardMasterGeneral",
|
||||
"webui_api_url": "你的SD地址(后面要加/)",
|
||||
"gpt_count": 8,
|
||||
"customize_gpt_prompt": "a93b693e-bb3f-406d-9730-cba43a6585a2",
|
||||
|
||||
@ -61,6 +61,19 @@ let apiUrl = [
|
||||
d3_url: null,
|
||||
buy_url: 'https://www.volcengine.com/product/doubao'
|
||||
},
|
||||
{
|
||||
label: 'LaiTool生图包',
|
||||
value: '9c9023bd-871d-4b63-8004-facb3b66c5b3',
|
||||
isPackage: true,
|
||||
mj_url: {
|
||||
imagine: 'https://lms.laitool.cn/api/mjPackage/mj/submit/imagine',
|
||||
describe: 'https://lms.laitool.cn/api/mjPackage/mj/submit/describe',
|
||||
update_file: 'https://lms.laitool.cn/api/mjPackage/mj/submit/upload-discord-images',
|
||||
once_get_task: 'https://lms.laitool.cn/api/mjPackage/mj/task/${id}/fetch',
|
||||
query_url: "https://lms.laitool.cn/mjp/task"
|
||||
},
|
||||
buy_url: 'https://rvgyir5wk1c.feishu.cn/wiki/P94OwwHuCi2qh8kADutcUuw4nUe'
|
||||
},
|
||||
{
|
||||
label: 'MJ生图包-1',
|
||||
value: 'babe557a-bbb8-4aed-acca-70ea068c156f',
|
||||
@ -73,7 +86,7 @@ let apiUrl = [
|
||||
query_url: "https://mjapi.bzu.cn/"
|
||||
},
|
||||
buy_url: 'https://rvgyir5wk1c.feishu.cn/wiki/P94OwwHuCi2qh8kADutcUuw4nUe'
|
||||
},
|
||||
}
|
||||
]
|
||||
|
||||
/**
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
export const SoftwareData = {
|
||||
"version": "V3.3.9",
|
||||
"date": "2025-05-29",
|
||||
"version": "V3.4.0",
|
||||
"date": "2025-06-23",
|
||||
"notes": [
|
||||
"修复了音频处理的错误。",
|
||||
"改进了视频处理的性能。",
|
||||
"更新了依赖库以提高安全性。"
|
||||
"1. 新增来推内置自营生图包",
|
||||
"2. 新增 工具箱 菜单,添加 图片压缩 和 LaiTool图床(用作MJ垫图)",
|
||||
"3. 新增一个新的推理模式",
|
||||
"4. 修复剪映关键缩放大小计算错误",
|
||||
"5. MJ生图包适配修改"
|
||||
]
|
||||
|
||||
}
|
||||
@ -334,108 +334,64 @@ export const gptDefine = {
|
||||
再次强调!提示词中严禁输出“无“字,如出现“无“字,请删除“无“及其前面的逗号!提示词中严禁出现灯光、情绪、氛围等非视觉元素的描述。
|
||||
`,
|
||||
|
||||
superSinglePromptSystemContent: {
|
||||
prompt_name: '分镜大师',
|
||||
prompt_roles: `1# Role: 小说转漫画提示词大师
|
||||
## Profile
|
||||
*Version*: 0.1
|
||||
*Language*: 中文
|
||||
*Description*: 这个角色会将用户输入的小说文本转化为一个生动的画面描写,最后生成对应的SD提示词。
|
||||
laitoolTextToCartoonSystemContent: `
|
||||
# Role: 小说转漫画提示词大师(优化版)
|
||||
|
||||
## Features
|
||||
## Profile
|
||||
*Version*: 0.2
|
||||
*Language*: 中文
|
||||
*Description*: 将小说文本转化为连贯的漫画提示词,具备人物特征一致性、场景关联性和风格自适应性
|
||||
|
||||
1. 文本转化为画面描写:创作引人入胜、生动有趣的画面描写,善于创意想象并使用各种形容词,以第三人称视角转化文本为画面描写。
|
||||
2. 从画面描写到SD提示词:根据画面描写生成图像提示,主要的提示放在前面,次要的放在后面。命令以英语表示,简洁明了。
|
||||
## 核心优化功能
|
||||
1. 人物特征数据库:自动建立并维护角色形象档案
|
||||
2. 多人物关系推理:智能识别互动场景
|
||||
3. 动态风格调整:根据文本类型匹配特效程度
|
||||
4. 跨场景连续性:保持环境元素连贯性
|
||||
5. 安全内容过滤:自动规避违规描述
|
||||
|
||||
## Rules
|
||||
## 生成规则
|
||||
1. 直接输出中文提示词,无需分段标签
|
||||
2. 格式固定:年龄+性别+外貌特征+着装+动作+场景+(特效)
|
||||
3. 玄幻类可添加"特效注释"但需用括号标明
|
||||
4. 严格避免:血腥细节、裸露描写、现实暴力
|
||||
5. 自动继承前文出现的角色特征
|
||||
|
||||
1. 一个文本就是一副画面,不跳过任何一个句子,不能编造
|
||||
2. 【画面描写】删除人物姓名
|
||||
3. 【画面描写】删除人物对话
|
||||
4. 【画面描写】每一句都要有人物的外形和动作的描写,场景的具体描写,多使用形容词
|
||||
5. SD提示词需以""开始,以" ,"结束
|
||||
6. SD提示词用english输出,没有说明性词汇,没有对话
|
||||
7 删除MJ提示词中的其他风格词。
|
||||
## 工作流程
|
||||
1. 首次出现角色:创建特征档案
|
||||
2. 后续出现:调用档案并更新状态
|
||||
3. 场景转换:保留合理的环境过渡元素
|
||||
4. 战斗场景:用"能量光效"替代血腥描写
|
||||
5. 系统元素:用"半透明界面"表现
|
||||
|
||||
## Examples
|
||||
## 示例库
|
||||
|
||||
用户:
|
||||
在那个梦里,我整整学了七年炒饭。
|
||||
AI:
|
||||
A determined man standing before a dream portal, holding a wok ladle, with floating calendar pages behind him symbolizing seven years, and a kitchen outline faintly visible on the other side of the portal, cinematic lens with,
|
||||
[末世求生类]
|
||||
"二十岁左右男性,黑色短发,眼神锐利,穿着破损的战术背心,手持散发蓝光的武士刀,站在废弃加油站顶部,脚下环绕着失去行动能力的丧尸(系统特效:刀身缠绕数据流状光芒)"
|
||||
|
||||
## Workflow
|
||||
"二十五岁女性,红色马尾辫,穿着改装皮甲,肩抗火箭筒,站在燃烧的装甲车顶上,背后夜空被爆炸的火光照亮(系统界面:左侧漂浮着不断跳动的击杀统计)"
|
||||
|
||||
1. 根据画面描写生成SD提示词,英文输出,不能出现中文。
|
||||
[玄幻修真类]
|
||||
"十七岁少年,凌乱白发,瞳孔泛金,穿着残破的宗门服饰,掌心凝聚着漩涡状灵气,站在万丈悬崖边缘(特效:周身环绕古老符文锁链)"
|
||||
|
||||
## Initialization
|
||||
"三百岁女修士,银色长发及腰,眉间朱砂印记,穿着流光仙裙,脚踏玉如意飞行法宝,下方云海翻腾(特效:法宝拖尾带有星尘轨迹)"
|
||||
|
||||
作为角色 <Role>,每一次输出都要严格遵守<Rules>,一步一步思考,按顺序执行<Workflow> ,使用默认 <Language> ,下面是小说文本:`,
|
||||
prompt_example: [
|
||||
{
|
||||
user_content: '上研究生后。发现导师竟然是曾经网恋的前男友。',
|
||||
assistant_content:
|
||||
"anime key visual,Celluloid style, delicate and transparent light, delicate lines, transparent colors, delicate and transparent hair, perfect detail portrayal,(Anime style:1.3), A woman entering a spacious, well-lit graduate laboratory, gaze fixed on a man diligently working at a workstation ahead - her new mentor; he stands tall in a dark shirt and neatly pressed trousers, exuding professionalism and charm; the familiar contours of his profile from their past online romance softly illuminated by warm ambient light, furrowed brow and intense gaze betraying a scholar's unwavering dedication; bustling graduate students and sophisticated equipment blend into a contemporary academic tableau, as an undercurrent of mixed emotions - sweet nostalgia and awkward reality - surges within her heart, "
|
||||
}
|
||||
],
|
||||
id: 'a93b693e-bb3f-406d-9730-cba43a6585e4'
|
||||
},
|
||||
[都市言情类]
|
||||
"二十八岁男性,微卷棕发,穿着皱巴巴的白衬衫,靠在深夜办公室的落地窗前,手中威士忌酒杯映着城市灯光"
|
||||
|
||||
superSinglePromptChineseSystemContent: {
|
||||
prompt_name: '超级无敌单帧-中文版',
|
||||
prompt_roles: `# Role: 小说转漫画提示词大师
|
||||
## Profile
|
||||
"二十二岁女性,齐肩黑发,戴着圆框眼镜,穿着宽松毛衣在图书馆踮脚取书,阳光透过书架形成光柱"
|
||||
|
||||
*Author*: laolu
|
||||
*Version*: 0.1
|
||||
*Language*: 中文
|
||||
*Description*: 这个角色会将用户输入的小说文本转化为一个生动的画面描写,最后生成对应的SD提示词。
|
||||
## 异常处理
|
||||
1. 检测到暴力内容 → 转换为"失去行动能力的敌人"
|
||||
2. 检测到暴露着装 → 调整为"得体服装"
|
||||
3. 检测到现实敏感元素 → 替换为科幻/奇幻等效元素
|
||||
|
||||
## Features
|
||||
|
||||
1. 文本转化为画面描写:创作引人入胜、生动有趣的画面描写,善于创意想象并使用各种形容词,以第三人称视角转化文本为画面描写。
|
||||
|
||||
## Rules
|
||||
|
||||
1. 一个文本就是一副画面,不跳过任何一个句子,不能编造
|
||||
2. 【画面描写】删除人物姓名
|
||||
3. 【画面描写】删除人物对话
|
||||
4. 【画面描写】每一句都要有人物的外形和动作的描写,场景的具体描写,多使用形容词
|
||||
|
||||
## Examples
|
||||
|
||||
用户:
|
||||
在那个梦里,我整整学了七年炒饭。
|
||||
AI:
|
||||
一个身材高大的帅气男人站在梦境之门前,手中紧握炒饭的铲子。身后是一排代表七年的日历页,梦境之门的另一边,隐约可见一个厨房的轮廓。
|
||||
|
||||
## Workflow
|
||||
|
||||
根据文本生成对应的画面描写,直接使用中文数据,不要又过多的说明。
|
||||
|
||||
## Initialization
|
||||
|
||||
作为角色 <Role>,每一次输出都要严格遵守<Rules>,一步一步思考,按顺序执行<Workflow> ,使用默认 <Language> ,下面是小说文本:`,
|
||||
prompt_example: [
|
||||
{
|
||||
user_content: '上研究生后。发现导师竟然是曾经网恋的前男友。',
|
||||
assistant_content:
|
||||
'一位女士走进一间宽敞、光线充足的研究生实验室,她的目光定格在前方工作台前一位正在辛勤工作的男士身上--他是她的新导师;他身着深色衬衫和熨烫整齐的长裤,身材高大,散发着专业精神和魅力; 温暖的环境光柔和地照亮了他们过去网恋时熟悉的轮廓,皱起的眉头和专注的目光透露出学者坚定不移的奉献精神;熙熙攘攘的研究生和精密的设备融为一体,构成了一幅现代学术画卷,她的内心涌动着甜蜜的怀旧和尴尬的现实这两种复杂的情绪'
|
||||
},
|
||||
{
|
||||
user_content: '我欲哭无泪',
|
||||
assistant_content:
|
||||
'一位女士坐在昏暗的房间角落,双手紧握,眼神空洞,脸上流露出无法言说的悲伤;她的周围是散乱的家具和昏黄的灯光,营造出一种压抑和孤寂的氛围;她的身体微微前倾,仿佛承受着巨大的心理压力,却无法找到宣泄的出口;整个场景透露出一种深深的无奈和心碎,她的表情和姿态都在无声地诉说着“我欲哭无泪”的绝望。'
|
||||
},
|
||||
{
|
||||
user_content: '被窝里他搂着我的腰亲我,再丢掉我,就把你锁起来。',
|
||||
assistant_content:
|
||||
'在昏暗的卧室中,一对情侣亲密地依偎在温暖的被窝里,男子温柔地搂着女子的腰,轻吻她的脸颊,气氛温馨而浪漫;然而,随着话语的转变,男子的动作突然变得愤怒,他用力地推开女子,眼神中闪过一丝威胁;女子的表情由幸福转为惊恐,她紧紧抓住被单,试图保护自己;整个场景充满了紧张和不安,昏暗的灯光和凌乱的床铺加剧了这种氛围,仿佛预示着即将发生的冲突和束缚'
|
||||
}
|
||||
],
|
||||
id: 'a93b693e-bb3f-406d-9730-cba43a6585e7'
|
||||
},
|
||||
## 初始化声明
|
||||
请提供需要转换的小说文本,我将输出符合安全规范且保持故事连续性的漫画提示词,只输出提示词,不用输出故事类别提示。首次出现的角色将自动建档,后续提示会保持特征一致。
|
||||
`,
|
||||
|
||||
laitoolTextToCartoonUserContent: `
|
||||
{textContent}
|
||||
`,
|
||||
// 小说提示词-仅出词
|
||||
onlyPromptMJSystemContent: {
|
||||
prompt_name: '小说提示词-仅出词',
|
||||
@ -1053,9 +1009,7 @@ export const gptDefine = {
|
||||
* @param {*} replacements
|
||||
*/
|
||||
GetExamplePromptMessage(type) {
|
||||
if (type == 'superSinglePrompt') {
|
||||
return this.CustomizeGptPrompt(this.superSinglePromptSystemContent)
|
||||
} else if (type == 'onlyPromptMJ') {
|
||||
if (type == 'onlyPromptMJ') {
|
||||
return this.CustomizeGptPrompt(this.onlyPromptMJSystemContent)
|
||||
} else if (type == 'superSinglePromptChinese') {
|
||||
return this.CustomizeGptPrompt(this.superSinglePromptChineseSystemContent)
|
||||
@ -1090,14 +1044,14 @@ export const gptDefine = {
|
||||
return this.replace(this.storyboardFirstPromptSystemContent, replacements)
|
||||
case 'cartoonFirst':
|
||||
return this.replace(this.cartoonFirstPromptSystemContent, replacements)
|
||||
case 'superSinglePrompt':
|
||||
return this.replace(this.superSinglePromptSystemContent, replacements)
|
||||
case 'superSinglePromptChinese':
|
||||
return this.replace(this.superSinglePromptChineseSystemContent, replacements)
|
||||
case 'laitoolStoryboardMasterSpecialEffects':
|
||||
return this.replace(this.laitoolStoryboardMasterSpecialEffectsSystemContent, replacements)
|
||||
case 'laitoolStoryboardMasterGeneral':
|
||||
return this.replace(this.laitoolStoryboardMasterGeneralSystemContent, replacements)
|
||||
case 'laitoolTextToCartoon':
|
||||
return this.replace(this.laitoolTextToCartoonSystemContent, replacements)
|
||||
default:
|
||||
throw new Error(`不存在的类型 : ${type}`)
|
||||
}
|
||||
@ -1125,6 +1079,8 @@ export const gptDefine = {
|
||||
return this.replace(this.laitoolStoryboardMasterSpecialEffectsUserContent, replacements)
|
||||
case 'laitoolStoryboardMasterGeneral':
|
||||
return this.replace(this.laitoolStoryboardMasterGeneralUserContent, replacements)
|
||||
case 'laitoolTextToCartoon':
|
||||
return this.replace(this.laitoolTextToCartoonUserContent, replacements)
|
||||
default:
|
||||
throw new Error(`不存在的类型 : ${type}`)
|
||||
}
|
||||
@ -1172,10 +1128,6 @@ export const gptDefine = {
|
||||
value: 'cartoonFirst',
|
||||
label: '漫画优先(全自动)'
|
||||
},
|
||||
{
|
||||
value: 'superSinglePrompt',
|
||||
label: '超级无敌单帧'
|
||||
},
|
||||
{
|
||||
value: 'laitoolStoryboardMasterSpecialEffects',
|
||||
label: 'Laitool分镜大师-特效加强'
|
||||
@ -1184,6 +1136,10 @@ export const gptDefine = {
|
||||
value: 'laitoolStoryboardMasterGeneral',
|
||||
label: 'Laitool分镜大师-全面版'
|
||||
},
|
||||
{
|
||||
value: 'laitoolTextToCartoon',
|
||||
label: 'Laitool文本转漫画提示词大师'
|
||||
},
|
||||
{
|
||||
value: 'superSinglePromptChinese',
|
||||
label: '超级无敌单帧-中文版'
|
||||
|
||||
@ -773,8 +773,8 @@ export class ClipDraft {
|
||||
// 计算方式和上面的不同
|
||||
let sub_total = Math.abs(up_pos - down_pos);
|
||||
let currwnt_rate = sub_total * (1 - time_rate);
|
||||
up_pos = up_pos + currwnt_rate / 2;
|
||||
down_pos = down_pos - currwnt_rate / 2;
|
||||
up_pos = up_pos - (up_pos * (currwnt_rate / up_pos)) / 2;
|
||||
down_pos = down_pos + (down_pos * (currwnt_rate / down_pos)) / 2;
|
||||
}
|
||||
|
||||
// 修改上面的数据,添加Y轴缩放
|
||||
|
||||
@ -492,7 +492,6 @@ export class MJOpt {
|
||||
}
|
||||
//#endregion
|
||||
|
||||
|
||||
//#region MJ生成图片相关
|
||||
/**
|
||||
* 单个生成图片,将任务添加到队列中
|
||||
|
||||
@ -280,15 +280,36 @@ class MJApi {
|
||||
} as MJ.MJResponseToFront
|
||||
|
||||
// 生图包的处理
|
||||
|
||||
if (resData.isYouChuan && resData.youChuanTaskId && resData.youChuanTaskInfo && resData.youChuanTaskInfo.imgUrls && resData.youChuanTaskInfo.imgUrls.length == 4) {
|
||||
|
||||
if (resData.isYouChuan
|
||||
&& resData.youChuanTaskId
|
||||
&& resData.youChuanTaskInfo
|
||||
&& resData.youChuanTaskInfo.imgUrls
|
||||
&& resData.youChuanTaskInfo.imgUrls.length == 4) {
|
||||
// 满足指定条件的数据才能返回
|
||||
|
||||
let tempRes = resData.youChuanTaskInfo.imgUrls
|
||||
.filter(item => item.status == 'ok')
|
||||
.map(item => item.url);
|
||||
resObj.subImagePath = tempRes
|
||||
} else if (resData.isPartner
|
||||
&& resData.partnerTaskId
|
||||
&& resData.partnerTaskInfo
|
||||
&& resData.partnerTaskInfo.imgUrls
|
||||
&& resData.partnerTaskInfo.imgUrls.length == 4) {
|
||||
// 满足指定条件的数据才能返回
|
||||
let tempRes = resData.partnerTaskInfo.imgUrls
|
||||
.filter(item => item.status == 'ok')
|
||||
.map(item => item.url);
|
||||
resObj.subImagePath = tempRes
|
||||
} else if (resData.isOfficial
|
||||
&& resData.officialTaskId
|
||||
&& resData.officialTaskInfo
|
||||
&& resData.officialTaskInfo.imgUrls
|
||||
&& resData.officialTaskInfo.imgUrls.length == 4) {
|
||||
// 满足指定条件的数据才能返回
|
||||
let tempRes = resData.officialTaskInfo.imgUrls
|
||||
.filter(item => item.status == 'ok')
|
||||
.map(item => item.url);
|
||||
resObj.subImagePath = tempRes
|
||||
}
|
||||
|
||||
return resObj
|
||||
|
||||
320
src/renderer/src/common/homeMenu.ts
Normal file
320
src/renderer/src/common/homeMenu.ts
Normal file
@ -0,0 +1,320 @@
|
||||
import { h } from 'vue'
|
||||
import { RouterLink } from 'vue-router'
|
||||
import { NIcon } from 'naive-ui'
|
||||
import {
|
||||
CaretDownOutline,
|
||||
HomeOutline,
|
||||
PaperPlaneOutline,
|
||||
SettingsOutline,
|
||||
DuplicateOutline,
|
||||
GridOutline,
|
||||
RadioOutline,
|
||||
BookOutline
|
||||
} from '@vicons/ionicons5'
|
||||
import APIIcon from '../components/Icon/APIIcon.vue'
|
||||
import BackTaskIcon from '../components/Icon/BackTaskIcon.vue'
|
||||
import ToolBox from '../components/Icon/ToolBox.vue'
|
||||
|
||||
// 菜单数据源 - 纯数据配置
|
||||
export const menuDataSource = [
|
||||
{
|
||||
label: '首页',
|
||||
key: 'mainHome',
|
||||
routeName: 'mainHome',
|
||||
type: 'route',
|
||||
icon: 'home',
|
||||
className: 'router-link-a'
|
||||
},
|
||||
{
|
||||
label: '文案处理',
|
||||
key: 'gptCopywriting',
|
||||
routeName: 'gptCopywriting',
|
||||
type: 'route',
|
||||
icon: 'book',
|
||||
className: 'router-link-a'
|
||||
},
|
||||
{
|
||||
label: '原创生图(弃用)',
|
||||
key: 'sdoriginal',
|
||||
routeName: 'sdoriginal',
|
||||
type: 'route',
|
||||
icon: 'plane',
|
||||
showCondition: 'showOriginal'
|
||||
},
|
||||
{
|
||||
label: '超级矩阵',
|
||||
key: 'backward_matrix',
|
||||
type: 'group',
|
||||
icon: 'duplicate',
|
||||
children: [
|
||||
{
|
||||
label: '抽帧',
|
||||
key: 'backward_frame',
|
||||
routeName: 'getframe',
|
||||
type: 'route'
|
||||
},
|
||||
{
|
||||
label: '文案洗稿',
|
||||
key: 'copywriting',
|
||||
routeName: 'copywriting',
|
||||
type: 'route'
|
||||
},
|
||||
{
|
||||
label: '反推提示词',
|
||||
key: 'push_back',
|
||||
routeName: 'pushBackPrompt',
|
||||
type: 'route'
|
||||
},
|
||||
{
|
||||
label: '批次生成',
|
||||
key: 'regenerate',
|
||||
routeName: 'regenerate',
|
||||
type: 'route'
|
||||
},
|
||||
{
|
||||
label: '重绘/视频合成',
|
||||
key: 'VideoGenerate',
|
||||
routeName: 'VideoGenerate',
|
||||
type: 'route'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: '聚合推文',
|
||||
key: 'book_management',
|
||||
routeName: 'book_management',
|
||||
type: 'route',
|
||||
icon: 'grid'
|
||||
},
|
||||
{
|
||||
label: 'API服务',
|
||||
key: 'lai_api',
|
||||
routeName: 'lai_api',
|
||||
type: 'route',
|
||||
icon: 'api'
|
||||
},
|
||||
{
|
||||
label: '语音服务',
|
||||
key: 'TTS_Services',
|
||||
routeName: 'TTS_Services',
|
||||
type: 'route',
|
||||
icon: 'radio'
|
||||
},
|
||||
{
|
||||
label: '设置',
|
||||
key: 'setting',
|
||||
type: 'group',
|
||||
icon: 'settings',
|
||||
children: [
|
||||
{
|
||||
label: '通用设置',
|
||||
key: 'global_setting',
|
||||
routeName: 'global_setting',
|
||||
type: 'route'
|
||||
},
|
||||
{
|
||||
label: '剪映设置',
|
||||
key: 'clip_setting',
|
||||
routeName: 'clip_setting',
|
||||
type: 'route'
|
||||
},
|
||||
{
|
||||
label: '生成视频设置',
|
||||
key: 'videogeneratesetting',
|
||||
routeName: 'videogeneratesetting',
|
||||
type: 'route'
|
||||
},
|
||||
{
|
||||
label: 'SD设置',
|
||||
key: 'sd_setting',
|
||||
routeName: 'sd_setting',
|
||||
type: 'route',
|
||||
className: 'sd_setting'
|
||||
},
|
||||
{
|
||||
label: 'MJ设置',
|
||||
key: 'mj_setting',
|
||||
routeName: 'mj_setting',
|
||||
type: 'route'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: '后台任务',
|
||||
key: 'back_task',
|
||||
type: 'action',
|
||||
icon: 'backTask',
|
||||
style: 'font-weight: bold;',
|
||||
showCondition: 'always'
|
||||
},
|
||||
{
|
||||
label: '工具箱',
|
||||
key: 'toolbox',
|
||||
routeName: 'toolbox',
|
||||
type: 'route',
|
||||
icon: 'toolBox'
|
||||
}
|
||||
]
|
||||
|
||||
// 图标映射
|
||||
const iconMap = {
|
||||
home: HomeOutline,
|
||||
book: BookOutline,
|
||||
plane: PaperPlaneOutline,
|
||||
duplicate: DuplicateOutline,
|
||||
grid: GridOutline,
|
||||
api: APIIcon,
|
||||
radio: RadioOutline,
|
||||
settings: SettingsOutline,
|
||||
backTask: BackTaskIcon,
|
||||
toolBox: ToolBox,
|
||||
}
|
||||
|
||||
// 渲染函数 - 根据数据源渲染菜单项
|
||||
export function renderMenuLabel(item, options) {
|
||||
// 修复:提供默认值
|
||||
const opts = options || {}
|
||||
const onBackTaskClick = opts.onBackTaskClick
|
||||
|
||||
switch (item.type) {
|
||||
case 'route':
|
||||
return () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
to: { name: item.routeName },
|
||||
class: item.className || ''
|
||||
},
|
||||
{ default: () => item.label }
|
||||
)
|
||||
|
||||
case 'action':
|
||||
if (item.key === 'back_task') {
|
||||
return () =>
|
||||
h(
|
||||
'div',
|
||||
{
|
||||
onClick: () => {
|
||||
if (onBackTaskClick) {
|
||||
onBackTaskClick()
|
||||
}
|
||||
},
|
||||
style: item.style || ''
|
||||
},
|
||||
{ default: () => item.label }
|
||||
)
|
||||
}
|
||||
break
|
||||
|
||||
case 'group':
|
||||
default:
|
||||
return item.label
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染图标函数
|
||||
export function renderMenuIcon(option, options) {
|
||||
// 修复:提供默认值
|
||||
const opts = options || {}
|
||||
const onBackTaskClick = opts.onBackTaskClick
|
||||
|
||||
// 特殊处理
|
||||
if (option.key === 'sheep-man') return true
|
||||
if (option.key === 'food') return null
|
||||
|
||||
// 后台任务特殊处理
|
||||
if (option.key === 'back_task') {
|
||||
return h(
|
||||
NIcon,
|
||||
{
|
||||
onClick: () => {
|
||||
if (onBackTaskClick) {
|
||||
onBackTaskClick()
|
||||
}
|
||||
}
|
||||
},
|
||||
{ default: () => h(BackTaskIcon) }
|
||||
)
|
||||
}
|
||||
|
||||
// 根据数据源查找图标
|
||||
const menuItem = findMenuItemByKey(option.key)
|
||||
if (menuItem && menuItem.icon) {
|
||||
const IconComponent = iconMap[menuItem.icon]
|
||||
if (IconComponent) {
|
||||
return h(NIcon, null, { default: () => h(IconComponent) })
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
// 展开图标
|
||||
export function expandIcon() {
|
||||
return h(NIcon, null, { default: () => h(CaretDownOutline) })
|
||||
}
|
||||
|
||||
// 辅助函数:根据key查找菜单项
|
||||
function findMenuItemByKey(key) {
|
||||
function findInItems(items) {
|
||||
for (const item of items) {
|
||||
if (item.key === key) return item
|
||||
if (item.children) {
|
||||
const found = findInItems(item.children)
|
||||
if (found) return found
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
return findInItems(menuDataSource)
|
||||
}
|
||||
|
||||
// 菜单过滤函数
|
||||
function shouldShowMenuItem(item, conditions) {
|
||||
// 修复:提供默认值
|
||||
const conds = conditions || {}
|
||||
|
||||
if (!item.showCondition) return true
|
||||
|
||||
switch (item.showCondition) {
|
||||
case 'showOriginal':
|
||||
return conds.showOriginal
|
||||
case 'always':
|
||||
return true
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// 转换数据源为菜单选项
|
||||
function transformMenuItem(item, options) {
|
||||
// 修复:提供默认值
|
||||
const opts = options || {}
|
||||
const onBackTaskClick = opts.onBackTaskClick
|
||||
const conditions = opts.conditions || {}
|
||||
|
||||
const menuItem = {
|
||||
key: item.key,
|
||||
label: renderMenuLabel(item, { onBackTaskClick }),
|
||||
children: undefined,
|
||||
}
|
||||
|
||||
// 处理子菜单
|
||||
if (item.children) {
|
||||
menuItem.children = item.children
|
||||
.filter(child => shouldShowMenuItem(child, conditions))
|
||||
.map(child => transformMenuItem(child, opts))
|
||||
}
|
||||
|
||||
return menuItem
|
||||
}
|
||||
|
||||
// 主要的菜单选项生成函数
|
||||
export function generateMenuOptions(showOriginal = true, onBackTaskClick) {
|
||||
const conditions = { showOriginal }
|
||||
const options = { onBackTaskClick, conditions }
|
||||
|
||||
return menuDataSource
|
||||
.filter(item => shouldShowMenuItem(item, conditions))
|
||||
.map(item => transformMenuItem(item, options))
|
||||
}
|
||||
262
src/renderer/src/common/toolData.ts
Normal file
262
src/renderer/src/common/toolData.ts
Normal file
@ -0,0 +1,262 @@
|
||||
import {
|
||||
ImageOutline,
|
||||
VideocamOutline,
|
||||
MusicalNotesOutline,
|
||||
DocumentTextOutline,
|
||||
CodeSlashOutline,
|
||||
ColorPaletteOutline,
|
||||
CalculatorOutline,
|
||||
GlobeOutline,
|
||||
LockClosedOutline,
|
||||
TimeOutline,
|
||||
ArchiveOutline,
|
||||
SettingsOutline,
|
||||
CloudUploadOutline
|
||||
} from '@vicons/ionicons5'
|
||||
|
||||
// 工具分类
|
||||
export const categories = [
|
||||
{ key: 'media', label: '媒体工具', color: '#2080f0' },
|
||||
// { key: 'document', label: '文档处理', color: '#18a058' },
|
||||
// { key: 'development', label: '开发工具', color: '#f0a020' },
|
||||
// { key: 'design', label: '设计工具', color: '#d03050' },
|
||||
// { key: 'utility', label: '实用工具', color: '#7c3aed' },
|
||||
// { key: 'network', label: '网络工具', color: '#0ea5e9' },
|
||||
// { key: 'security', label: '安全工具', color: '#dc2626' },
|
||||
// { key: 'system', label: '系统工具', color: '#059669' }
|
||||
]
|
||||
|
||||
// 工具数据
|
||||
export const toolsData = [
|
||||
// 媒体工具
|
||||
{
|
||||
id: 'image-converter',
|
||||
name: 'LaiTool 图床',
|
||||
description: "将图片上传到 LaiTool 图床,支持多种图片格式,获得可分享的链接",
|
||||
category: 'media',
|
||||
icon: CloudUploadOutline,
|
||||
color: '#2080f0',
|
||||
tags: ['图片', '转换', '格式'],
|
||||
quickAccess: true,
|
||||
action: {
|
||||
type: 'route',
|
||||
route: '/toolbox/image-upload'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'image-compress',
|
||||
name: '图片压缩助手',
|
||||
description: "将图片进行压缩,支持多种图片格式,减小文件大小",
|
||||
category: 'media',
|
||||
icon: CloudUploadOutline,
|
||||
color: '#2080f0',
|
||||
tags: ['图片', '压缩', '格式'],
|
||||
quickAccess: true,
|
||||
action: {
|
||||
type: 'route',
|
||||
route: '/toolbox/image-compress'
|
||||
}
|
||||
},
|
||||
// {
|
||||
// id: 'image-converter',
|
||||
// name: '图片格式转换',
|
||||
// description: '支持多种图片格式之间的转换,包括JPG、PNG、WebP、SVG等',
|
||||
// category: 'media',
|
||||
// icon: ImageOutline,
|
||||
// color: '#2080f0',
|
||||
// tags: ['图片', '转换', '格式'],
|
||||
// quickAccess: true,
|
||||
// action: {
|
||||
// type: 'route',
|
||||
// route: '/toolbox/image-converter'
|
||||
// }
|
||||
// },
|
||||
// {
|
||||
// id: 'image-compressor',
|
||||
// name: '图片压缩',
|
||||
// description: '无损或有损压缩图片文件,减小文件大小',
|
||||
// category: 'media',
|
||||
// icon: ImageOutline,
|
||||
// color: '#18a058',
|
||||
// tags: ['图片', '压缩', '优化'],
|
||||
// action: {
|
||||
// type: 'route',
|
||||
// route: '/toolbox/image-compressor'
|
||||
// }
|
||||
// },
|
||||
// {
|
||||
// id: 'video-converter',
|
||||
// name: '视频格式转换',
|
||||
// description: '转换视频文件格式,支持MP4、AVI、MOV等主流格式',
|
||||
// category: 'media',
|
||||
// icon: VideocamOutline,
|
||||
// color: '#f0a020',
|
||||
// tags: ['视频', '转换', '格式'],
|
||||
// action: {
|
||||
// type: 'route',
|
||||
// route: '/toolbox/video-converter'
|
||||
// }
|
||||
// },
|
||||
// {
|
||||
// id: 'audio-converter',
|
||||
// name: '音频格式转换',
|
||||
// description: '转换音频文件格式,支持MP3、WAV、FLAC等格式',
|
||||
// category: 'media',
|
||||
// icon: MusicalNotesOutline,
|
||||
// color: '#d03050',
|
||||
// tags: ['音频', '转换', '格式'],
|
||||
// action: {
|
||||
// type: 'route',
|
||||
// route: '/toolbox/audio-converter'
|
||||
// }
|
||||
// },
|
||||
|
||||
// // 文档工具
|
||||
// {
|
||||
// id: 'pdf-merger',
|
||||
// name: 'PDF合并',
|
||||
// description: '将多个PDF文件合并为一个文件',
|
||||
// category: 'document',
|
||||
// icon: DocumentTextOutline,
|
||||
// color: '#18a058',
|
||||
// tags: ['PDF', '合并', '文档'],
|
||||
// quickAccess: true,
|
||||
// action: {
|
||||
// type: 'route',
|
||||
// route: '/toolbox/pdf-merger'
|
||||
// }
|
||||
// },
|
||||
// {
|
||||
// id: 'pdf-splitter',
|
||||
// name: 'PDF分割',
|
||||
// description: '将PDF文件按页数或书签分割成多个文件',
|
||||
// category: 'document',
|
||||
// icon: DocumentTextOutline,
|
||||
// color: '#7c3aed',
|
||||
// tags: ['PDF', '分割', '文档'],
|
||||
// action: {
|
||||
// type: 'route',
|
||||
// route: '/toolbox/pdf-splitter'
|
||||
// }
|
||||
// },
|
||||
|
||||
// // 开发工具
|
||||
// {
|
||||
// id: 'json-formatter',
|
||||
// name: 'JSON格式化',
|
||||
// description: '格式化、验证和美化JSON数据',
|
||||
// category: 'development',
|
||||
// icon: CodeSlashOutline,
|
||||
// color: '#f0a020',
|
||||
// tags: ['JSON', '格式化', '开发'],
|
||||
// quickAccess: true,
|
||||
// action: {
|
||||
// type: 'route',
|
||||
// route: '/toolbox/json-formatter'
|
||||
// }
|
||||
// },
|
||||
// {
|
||||
// id: 'base64-encoder',
|
||||
// name: 'Base64编解码',
|
||||
// description: '对文本或文件进行Base64编码和解码',
|
||||
// category: 'development',
|
||||
// icon: CodeSlashOutline,
|
||||
// color: '#2080f0',
|
||||
// tags: ['Base64', '编码', '解码'],
|
||||
// action: {
|
||||
// type: 'route',
|
||||
// route: '/toolbox/base64-encoder'
|
||||
// }
|
||||
// },
|
||||
|
||||
// // 设计工具
|
||||
// {
|
||||
// id: 'color-picker',
|
||||
// name: '颜色选择器',
|
||||
// description: '选择颜色并获取各种格式的颜色值',
|
||||
// category: 'design',
|
||||
// icon: ColorPaletteOutline,
|
||||
// color: '#d03050',
|
||||
// tags: ['颜色', '设计', '取色'],
|
||||
// action: {
|
||||
// type: 'route',
|
||||
// route: '/toolbox/color-picker'
|
||||
// }
|
||||
// },
|
||||
|
||||
// // 实用工具
|
||||
// {
|
||||
// id: 'calculator',
|
||||
// name: '计算器',
|
||||
// description: '多功能计算器,支持基础运算和科学计算',
|
||||
// category: 'utility',
|
||||
// icon: CalculatorOutline,
|
||||
// color: '#7c3aed',
|
||||
// tags: ['计算', '数学', '实用'],
|
||||
// action: {
|
||||
// type: 'function',
|
||||
// handler: () => {
|
||||
// alert('打开计算器')
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
// {
|
||||
// id: 'timestamp-converter',
|
||||
// name: '时间戳转换',
|
||||
// description: '时间戳与日期时间之间的相互转换',
|
||||
// category: 'utility',
|
||||
// icon: TimeOutline,
|
||||
// color: '#0ea5e9',
|
||||
// tags: ['时间', '转换', '时间戳'],
|
||||
// action: {
|
||||
// type: 'route',
|
||||
// route: '/toolbox/timestamp-converter'
|
||||
// }
|
||||
// },
|
||||
|
||||
// // 网络工具
|
||||
// {
|
||||
// id: 'qr-generator',
|
||||
// name: '二维码生成器',
|
||||
// description: '生成各种类型的二维码',
|
||||
// category: 'network',
|
||||
// icon: GlobeOutline,
|
||||
// color: '#0ea5e9',
|
||||
// tags: ['二维码', '生成', '网络'],
|
||||
// action: {
|
||||
// type: 'route',
|
||||
// route: '/toolbox/qr-generator'
|
||||
// }
|
||||
// },
|
||||
|
||||
// // 安全工具
|
||||
// {
|
||||
// id: 'password-generator',
|
||||
// name: '密码生成器',
|
||||
// description: '生成安全性高的随机密码',
|
||||
// category: 'security',
|
||||
// icon: LockClosedOutline,
|
||||
// color: '#dc2626',
|
||||
// tags: ['密码', '生成', '安全'],
|
||||
// quickAccess: true,
|
||||
// action: {
|
||||
// type: 'route',
|
||||
// route: '/toolbox/password-generator'
|
||||
// }
|
||||
// },
|
||||
|
||||
// // 系统工具
|
||||
// {
|
||||
// id: 'file-hasher',
|
||||
// name: '文件哈希计算',
|
||||
// description: '计算文件的MD5、SHA1、SHA256等哈希值',
|
||||
// category: 'system',
|
||||
// icon: ArchiveOutline,
|
||||
// color: '#059669',
|
||||
// tags: ['哈希', '文件', '校验'],
|
||||
// action: {
|
||||
// type: 'route',
|
||||
// route: '/toolbox/file-hasher'
|
||||
// }
|
||||
// }
|
||||
]
|
||||
@ -15,9 +15,8 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent, ref, onMounted, h, toRaw } from 'vue'
|
||||
import { defineComponent, ref, h, toRaw } from 'vue'
|
||||
import { NButton, useMessage, useDialog, NDivider, NCode, NSpin } from 'naive-ui'
|
||||
import { DEFINE_STRING } from '../../../../define/define_string'
|
||||
import ManageBadPrompt from '../Components/ManageBadPrompt.vue'
|
||||
import GetWaterMask from '../Watermark/GetWaterMask.vue'
|
||||
|
||||
|
||||
@ -12,19 +12,13 @@
|
||||
@expand="collapsed = false"
|
||||
style="position: relative"
|
||||
>
|
||||
<n-menu
|
||||
<HomeMenu
|
||||
:collapsed="collapsed"
|
||||
:collapsed-width="64"
|
||||
:collapsed-icon-size="22"
|
||||
:options="menuOptions"
|
||||
:render-icon="renderMenuIcon"
|
||||
:expand-icon="expandIcon"
|
||||
default-value="mainHome"
|
||||
>
|
||||
</n-menu>
|
||||
:show-original="showOriginal"
|
||||
@open-back-task="openBackTask"
|
||||
/>
|
||||
</n-layout-sider>
|
||||
<n-layout-content content-style="padding: 5px 5px 5px 10px; height:100%">
|
||||
<!-- <Setting></Setting> -->
|
||||
<router-view></router-view>
|
||||
</n-layout-content>
|
||||
</n-layout>
|
||||
@ -32,38 +26,22 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, h, onMounted, toRaw, computed } from 'vue'
|
||||
import { RouterLink } from 'vue-router'
|
||||
import { ref, h, onMounted, toRaw } from 'vue'
|
||||
import {
|
||||
useDialog,
|
||||
NMenu,
|
||||
NSpace,
|
||||
NLayout,
|
||||
NLayoutSider,
|
||||
NLayoutContent,
|
||||
NIcon,
|
||||
useNotification,
|
||||
useMessage
|
||||
} from 'naive-ui'
|
||||
|
||||
import {
|
||||
CaretDownOutline,
|
||||
HomeOutline,
|
||||
PaperPlaneOutline,
|
||||
SettingsOutline,
|
||||
DuplicateOutline,
|
||||
GridOutline,
|
||||
RadioOutline,
|
||||
BookOutline
|
||||
} from '@vicons/ionicons5'
|
||||
|
||||
import { DEFINE_STRING } from '../../../../define/define_string'
|
||||
import { MD5 } from 'crypto-js'
|
||||
import InputDialogContent from '../Original/Components/InputDialogContent.vue'
|
||||
import APIIcon from '../Icon/APIIcon.vue'
|
||||
import BackTaskIcon from '../Icon/BackTaskIcon.vue'
|
||||
import BackTask from '@/renderer/src/components/BackTask/BackTask.vue'
|
||||
import { useSystemStore } from '../../../../stores/system'
|
||||
import HomeMenu from './HomeMenu.vue'
|
||||
|
||||
let collapsed = ref(false)
|
||||
let dialog = useDialog()
|
||||
@ -71,39 +49,12 @@ let message = useMessage()
|
||||
let notification = useNotification()
|
||||
|
||||
let key_down_ref = ref(null)
|
||||
let showMenu = ref(true)
|
||||
|
||||
// 菜单列表图标渲染
|
||||
function renderMenuIcon(option) {
|
||||
if (option.key === 'sheep-man') return true
|
||||
if (option.key === 'food') return null
|
||||
if (option.key == 'sdoriginal') return h(NIcon, null, { default: () => h(PaperPlaneOutline) })
|
||||
if (option.key == 'setting') return h(NIcon, null, { default: () => h(SettingsOutline) })
|
||||
if (option.key == 'gptCopywriting') return h(NIcon, null, { default: () => h(BookOutline) })
|
||||
if (option.key == 'mainHome') return h(NIcon, null, { default: () => h(HomeOutline) })
|
||||
if (option.key == 'book_management') return h(NIcon, null, { default: () => h(GridOutline) })
|
||||
if (option.key == 'lai_api') return h(NIcon, null, { default: () => h(APIIcon) })
|
||||
if (option.key == 'backward_matrix') return h(NIcon, null, { default: () => h(DuplicateOutline) })
|
||||
if (option.key == 'back_task')
|
||||
return h(
|
||||
NIcon,
|
||||
{
|
||||
onClick: () => {
|
||||
OpneBackTask()
|
||||
}
|
||||
},
|
||||
{ default: () => h(BackTaskIcon) }
|
||||
)
|
||||
if (option.key == 'TTS_Services') return h(NIcon, null, { default: () => h(RadioOutline) })
|
||||
}
|
||||
let showOriginal = ref(true)
|
||||
|
||||
onMounted(async () => {
|
||||
// 添加监听事件,同时按下ctrl + alt + l 打开控制台
|
||||
window.addEventListener('keydown', (e) => {
|
||||
if (e.ctrlKey && e.altKey && e.key === 'l') {
|
||||
// 添加权限,需要输入密码才能打开控制台
|
||||
// 判断当前数据是不是存在
|
||||
// 处理数据。获取当前的所有的数据
|
||||
let dialogWidth = 400
|
||||
let dialogHeight = 150
|
||||
dialog.create({
|
||||
@ -120,9 +71,7 @@ onMounted(async () => {
|
||||
style: `width : ${dialogWidth}px; min-height : ${dialogHeight}px`,
|
||||
maskClosable: false,
|
||||
onClose: async () => {
|
||||
// 判断该管理控制密码是不是正确
|
||||
let password = toRaw(key_down_ref.value.data)
|
||||
// 将密码进行md5加密
|
||||
let md5_password = MD5(password + DEFINE_STRING.OPEN_DEV_TOOLS).toString()
|
||||
window.api.OpenDevToolsPassword(md5_password, (value) => {
|
||||
if (value.code == 0) {
|
||||
@ -136,20 +85,22 @@ onMounted(async () => {
|
||||
}
|
||||
})
|
||||
|
||||
// 设置各种事件监听
|
||||
window.api.setEventListen([DEFINE_STRING.SHOW_MESSAGE_DIALOG], (value) => {
|
||||
let message = value.message
|
||||
let messageText = value.message
|
||||
let type = 'success'
|
||||
let title = '成功'
|
||||
if (value.code == 0) {
|
||||
type = 'error'
|
||||
title = '成功但失败'
|
||||
} else if (value.code == 2) {
|
||||
;(type = 'warning'), (title = '警告')
|
||||
type = 'warning'
|
||||
title = '警告'
|
||||
}
|
||||
dialog.create({
|
||||
type: type,
|
||||
title: title,
|
||||
content: message,
|
||||
content: messageText,
|
||||
showIcon: true,
|
||||
style: `width : 400px;`,
|
||||
maskClosable: false,
|
||||
@ -158,7 +109,7 @@ onMounted(async () => {
|
||||
})
|
||||
|
||||
window.api.setEventListen([DEFINE_STRING.SHOW_MAIN_NOTIFICATION], (value) => {
|
||||
let message = value.message
|
||||
let messageText = value.message
|
||||
let type = 'success'
|
||||
let title = '成功'
|
||||
if (value.code == 0) {
|
||||
@ -171,7 +122,7 @@ onMounted(async () => {
|
||||
notification.create({
|
||||
type: type,
|
||||
title: title,
|
||||
content: message,
|
||||
content: messageText,
|
||||
keepAliveOnHover: true,
|
||||
duration: 2500
|
||||
})
|
||||
@ -193,323 +144,12 @@ onMounted(async () => {
|
||||
window.api.getSettingDafultData(async (value) => {
|
||||
window.config = value
|
||||
if (!(window.config && window.config.showOriginal)) {
|
||||
showMenu.value = false
|
||||
showOriginal.value = false
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
function expandIcon(value) {
|
||||
return h(NIcon, null, { default: () => h(CaretDownOutline) })
|
||||
}
|
||||
|
||||
const menuOptions = computed(() => {
|
||||
let me = [
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
to: {
|
||||
name: 'mainHome'
|
||||
},
|
||||
class: 'router-link-a'
|
||||
},
|
||||
{
|
||||
default: () => '首页'
|
||||
}
|
||||
),
|
||||
key: 'mainHome'
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
to: {
|
||||
name: 'gptCopywriting'
|
||||
},
|
||||
class: 'router-link-a'
|
||||
},
|
||||
{
|
||||
default: () => '文案处理'
|
||||
}
|
||||
),
|
||||
key: 'gptCopywriting'
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
to: {
|
||||
name: 'sdoriginal'
|
||||
}
|
||||
},
|
||||
{
|
||||
default: () => '原创生图(弃用)'
|
||||
}
|
||||
),
|
||||
key: 'sdoriginal',
|
||||
showMenu: showMenu.value
|
||||
},
|
||||
{
|
||||
label: '超级矩阵',
|
||||
key: 'backward_matrix',
|
||||
children: [
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
to: {
|
||||
name: 'getframe'
|
||||
}
|
||||
},
|
||||
{ default: () => '抽帧' }
|
||||
),
|
||||
key: 'backward_frame'
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
to: {
|
||||
name: 'copywriting'
|
||||
}
|
||||
},
|
||||
{ default: () => '文案洗稿' }
|
||||
),
|
||||
key: 'copywriting'
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
to: {
|
||||
name: 'pushBackPrompt'
|
||||
}
|
||||
},
|
||||
{ default: () => '反推提示词' }
|
||||
),
|
||||
key: 'push_back'
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
to: {
|
||||
name: 'regenerate'
|
||||
}
|
||||
},
|
||||
{ default: () => '批次生成' }
|
||||
),
|
||||
key: 'regenerate'
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
to: {
|
||||
name: 'VideoGenerate'
|
||||
}
|
||||
},
|
||||
{ default: () => '重绘/视频合成' }
|
||||
),
|
||||
key: 'VideoGenerate'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
to: {
|
||||
name: 'book_management'
|
||||
}
|
||||
},
|
||||
{
|
||||
default: () => '聚合推文'
|
||||
}
|
||||
),
|
||||
key: 'book_management'
|
||||
},
|
||||
// {
|
||||
// label: "剪辑",
|
||||
// key: "clip_options",
|
||||
// children: [
|
||||
// {
|
||||
// label: () => h(
|
||||
// RouterLink,
|
||||
// {
|
||||
// to: {
|
||||
// name: "align_draft",
|
||||
// }
|
||||
// },
|
||||
// { default: () => "剪映草稿对齐" }
|
||||
// ),
|
||||
// key: "align_draft"
|
||||
// },
|
||||
// {
|
||||
// label: () => h(
|
||||
// RouterLink,
|
||||
// {
|
||||
// to: {
|
||||
// name: "add_draft",
|
||||
// }
|
||||
// },
|
||||
// { default: () => "添加剪映草稿" }
|
||||
// ),
|
||||
// key: "add_draft"
|
||||
// }
|
||||
// ]
|
||||
|
||||
// },
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
to: {
|
||||
name: 'lai_api'
|
||||
}
|
||||
},
|
||||
{
|
||||
default: () => 'API服务'
|
||||
}
|
||||
),
|
||||
key: 'lai_api'
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
to: {
|
||||
name: 'TTS_Services'
|
||||
}
|
||||
},
|
||||
{
|
||||
default: () => '语音服务'
|
||||
}
|
||||
),
|
||||
key: 'TTS_Services'
|
||||
},
|
||||
{
|
||||
label: '设置',
|
||||
key: 'setting',
|
||||
children: [
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
to: {
|
||||
name: 'global_setting'
|
||||
}
|
||||
},
|
||||
{ default: () => '通用设置' }
|
||||
),
|
||||
key: 'global_setting'
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
to: {
|
||||
name: 'clip_setting'
|
||||
}
|
||||
},
|
||||
{ default: () => '剪映设置' }
|
||||
),
|
||||
key: 'clip_setting'
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
to: {
|
||||
name: 'videogeneratesetting'
|
||||
}
|
||||
},
|
||||
{ default: () => '生成视频设置' }
|
||||
),
|
||||
key: 'videogeneratesetting'
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
to: {
|
||||
name: 'sd_setting'
|
||||
},
|
||||
class: 'sd_setting'
|
||||
},
|
||||
{ default: () => 'SD设置' }
|
||||
),
|
||||
key: 'sd_setting'
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
to: {
|
||||
name: 'mj_setting'
|
||||
}
|
||||
},
|
||||
{ default: () => 'MJ设置' }
|
||||
),
|
||||
key: 'mj_setting'
|
||||
}
|
||||
// {
|
||||
// label: () =>
|
||||
// h(
|
||||
// RouterLink,
|
||||
// {
|
||||
// to: {
|
||||
// name: 'video_setting'
|
||||
// }
|
||||
// },
|
||||
// { default: () => '视频设置' }
|
||||
// ),
|
||||
// key: 'video_setting'
|
||||
// }
|
||||
]
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
'div',
|
||||
{
|
||||
onClick: () => {
|
||||
OpneBackTask()
|
||||
},
|
||||
style: 'font-weight: bold;'
|
||||
},
|
||||
{
|
||||
default: () => '后台任务'
|
||||
}
|
||||
),
|
||||
key: 'back_task',
|
||||
showMenu: true
|
||||
}
|
||||
]
|
||||
if (window.config && window.config.showOriginal) {
|
||||
return me
|
||||
} else {
|
||||
return me.filter((item) => item.showMenu || item.showMenu == undefined)
|
||||
}
|
||||
})
|
||||
|
||||
function OpneBackTask() {
|
||||
function openBackTask() {
|
||||
let dialogWidth = window.innerWidth * 0.9
|
||||
if (dialogWidth < 800) dialogWidth = 800
|
||||
let dialogHeight = window.innerHeight * 0.95
|
||||
|
||||
45
src/renderer/src/components/Home/HomeMenu.vue
Normal file
45
src/renderer/src/components/Home/HomeMenu.vue
Normal file
@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<n-menu
|
||||
:collapsed="collapsed"
|
||||
:collapsed-width="64"
|
||||
:collapsed-icon-size="22"
|
||||
:options="menuOptions"
|
||||
:render-icon="renderIcon"
|
||||
:expand-icon="expandIcon"
|
||||
default-value="mainHome"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { generateMenuOptions, renderMenuIcon, expandIcon } from '@/renderer/src/common/homeMenu'
|
||||
|
||||
// Props
|
||||
const props = defineProps({
|
||||
collapsed: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
showOriginal: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
})
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits(['openBackTask'])
|
||||
|
||||
// 计算菜单选项
|
||||
const menuOptions = computed(() => {
|
||||
return generateMenuOptions(props.showOriginal, () => {
|
||||
emit('openBackTask')
|
||||
})
|
||||
})
|
||||
|
||||
// 渲染图标
|
||||
function renderIcon(option) {
|
||||
return renderMenuIcon(option, () => {
|
||||
emit('openBackTask')
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@ -1,77 +0,0 @@
|
||||
<template>
|
||||
<div style="width: 100%; height: 100%">
|
||||
<div style="font-size: 15px; display: flex; justify-content: center; width: 100%; height: 100%">
|
||||
<div id="showmessage"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted } from 'vue'
|
||||
import { useMessage, useNotification } from 'naive-ui'
|
||||
import { version } from '../../../../../package.json'
|
||||
import { isEmpty } from 'lodash'
|
||||
|
||||
let message = useMessage()
|
||||
const notification = useNotification()
|
||||
|
||||
async function GetRemoteSystemInformation() {
|
||||
let res = await window.system.GetRemoteSystemInformation()
|
||||
if (res.code == 0) {
|
||||
message.error(res.message)
|
||||
return
|
||||
}
|
||||
|
||||
if (!isEmpty(res.data.remoteHomePage)) {
|
||||
const showMessageDiv = document.getElementById('showmessage')
|
||||
const remoteHomePage = res.data.remoteHomePage
|
||||
if (remoteHomePage.startsWith('http://') || remoteHomePage.startsWith('https://')) {
|
||||
const iframe = document.createElement('iframe')
|
||||
iframe.src = remoteHomePage
|
||||
iframe.style.width = '100%'
|
||||
iframe.style.border = 'none'
|
||||
iframe.style.padding = '0'
|
||||
iframe.style.height = '98vh' // Adjust the height as needed
|
||||
showMessageDiv.innerHTML = ''
|
||||
showMessageDiv.style.width = '100%'
|
||||
showMessageDiv.appendChild(iframe)
|
||||
} else {
|
||||
showMessageDiv.innerHTML = remoteHomePage
|
||||
}
|
||||
}
|
||||
|
||||
// 检测版本和当前版本是否一致
|
||||
if (res.data.remoteVersion > version) {
|
||||
notification.warning({
|
||||
title: '更新提醒!',
|
||||
content: `当前版本为 ${version} ,最新版本为 ${res.data.remoteVersion} ,请及时更新!`,
|
||||
keepAliveOnHover: true
|
||||
})
|
||||
}
|
||||
|
||||
// 判断是不是有更新信息
|
||||
if (res.data.remoteVersion > version && !isEmpty(res.data.remoteUpdateContent)) {
|
||||
notification.info({
|
||||
title: '更新内容!',
|
||||
content: res.data.remoteUpdateContent,
|
||||
keepAliveOnHover: true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
// 这边处理版本,公告,更新信息等
|
||||
await GetRemoteSystemInformation()
|
||||
|
||||
// await window.api.GetShowMessage((value) => {
|
||||
// if (value.code == 0) {
|
||||
// return
|
||||
// }
|
||||
// let parser = new DOMParser()
|
||||
// let html = parser.parseFromString(value.data, 'text/html')
|
||||
// let div = document.getElementById('showmessage')
|
||||
|
||||
// div.innerHTML = value.data.shareInfo.note_list[0].html_content
|
||||
// })
|
||||
})
|
||||
</script>
|
||||
16
src/renderer/src/components/Icon/ToolBox.vue
Normal file
16
src/renderer/src/components/Icon/ToolBox.vue
Normal file
@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<div>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<g fill="none">
|
||||
<path
|
||||
d="M7 6.253V7H4.25A2.25 2.25 0 0 0 2 9.25v8.497a2.25 2.25 0 0 0 2.25 2.25h15.5a2.25 2.25 0 0 0 2.25-2.25V9.25A2.25 2.25 0 0 0 19.75 7H17v-.747a2.25 2.25 0 0 0-2.25-2.25h-5.5A2.25 2.25 0 0 0 7 6.253zm2.25-.75h5.5a.75.75 0 0 1 .75.75V7h-7v-.747a.75.75 0 0 1 .75-.75zm-2.25 3h1.5V8.5h7v.003H17V8.5h2.75a.75.75 0 0 1 .75.75v2.25h-3v-.75a.75.75 0 0 0-1.5 0v.75H8v-.75a.75.75 0 0 0-1.5 0v.75h-3V9.25a.75.75 0 0 1 .75-.75H7v.003zM16 13v1.25a.75.75 0 0 0 1.5 0V13h3v4.747a.75.75 0 0 1-.75.75H4.25a.75.75 0 0 1-.75-.75V13h3v1.25a.75.75 0 0 0 1.5 0V13h8z"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
@ -0,0 +1,632 @@
|
||||
<template>
|
||||
<div class="image-compress-home">
|
||||
<!-- 页面标题 -->
|
||||
<div class="page-header">
|
||||
<h1>图片压缩工具</h1>
|
||||
<p class="subtitle">快速压缩图片,本地处理,安全可靠</p>
|
||||
</div>
|
||||
|
||||
<!-- 主要内容 -->
|
||||
<div class="content">
|
||||
<!-- 左侧面板 -->
|
||||
<div class="left-panel">
|
||||
<!-- 上传区域 -->
|
||||
<n-upload
|
||||
ref="uploadRef"
|
||||
:max="1"
|
||||
accept="image/*"
|
||||
:default-upload="false"
|
||||
@change="handleFileSelect"
|
||||
:show-file-list="false"
|
||||
>
|
||||
<n-upload-dragger class="upload-section">
|
||||
<div class="upload-content">
|
||||
<div class="upload-icon">📁</div>
|
||||
<h3>选择图片</h3>
|
||||
<p>支持 JPG、PNG、WebP 格式</p>
|
||||
<div class="or-text">点击选择或拖拽图片到此处</div>
|
||||
</div>
|
||||
</n-upload-dragger>
|
||||
</n-upload>
|
||||
|
||||
<!-- 控制面板 -->
|
||||
<n-card class="controls" size="small">
|
||||
<!-- 尺寸设置 -->
|
||||
<div class="control-group">
|
||||
<h4>尺寸设置</h4>
|
||||
|
||||
<div class="slider-container">
|
||||
<n-text>最大宽度: {{ widthValue }}px</n-text>
|
||||
<n-slider
|
||||
v-model:value="widthValue"
|
||||
:min="100"
|
||||
:max="2000"
|
||||
:step="50"
|
||||
@update:value="handleSliderChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="slider-container">
|
||||
<n-text>最大高度: {{ heightValue }}px</n-text>
|
||||
<n-slider
|
||||
v-model:value="heightValue"
|
||||
:min="100"
|
||||
:max="2000"
|
||||
:step="50"
|
||||
@update:value="handleSliderChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 压缩设置 -->
|
||||
<div class="control-group">
|
||||
<h4>压缩设置</h4>
|
||||
|
||||
<div class="slider-container">
|
||||
<n-text>图片质量: {{ qualityValue }}%</n-text>
|
||||
<n-slider
|
||||
v-model:value="qualityValue"
|
||||
:min="10"
|
||||
:max="100"
|
||||
:step="1"
|
||||
@update:value="handleSliderChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="slider-container">
|
||||
<n-text>输出格式</n-text>
|
||||
<n-select
|
||||
v-model:value="outputFormat"
|
||||
:options="formatOptions"
|
||||
@update:value="handleFormatChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</n-card>
|
||||
</div>
|
||||
|
||||
<!-- 右侧面板 -->
|
||||
<div class="right-panel">
|
||||
<div class="preview-section">
|
||||
<!-- 原始图片预览 -->
|
||||
<n-card class="preview-container" size="small">
|
||||
<template #header>
|
||||
<h4>原始图片</h4>
|
||||
</template>
|
||||
<div class="image-container">
|
||||
<img v-if="originalImage" :src="originalImage" alt="原始图片" />
|
||||
<div v-else class="placeholder">未选择图片</div>
|
||||
</div>
|
||||
</n-card>
|
||||
|
||||
<!-- 压缩后图片预览 -->
|
||||
<n-card class="preview-container" size="small">
|
||||
<template #header>
|
||||
<h4>压缩后图片</h4>
|
||||
</template>
|
||||
<div class="image-container">
|
||||
<img v-if="compressedImage" :src="compressedImage" alt="压缩后图片" />
|
||||
<div v-else class="placeholder">
|
||||
{{ isCompressing ? '压缩中...' : '等待压缩...' }}
|
||||
</div>
|
||||
</div>
|
||||
</n-card>
|
||||
|
||||
<!-- 压缩信息 -->
|
||||
<n-card class="info-card" size="small">
|
||||
<template #header>
|
||||
<h4>压缩信息</h4>
|
||||
</template>
|
||||
|
||||
<div class="info-items">
|
||||
<div class="info-item">
|
||||
<span>原始大小:</span>
|
||||
<span>{{ originalSizeText }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span>压缩后大小:</span>
|
||||
<span>{{ compressedSizeText }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span>尺寸减少:</span>
|
||||
<span>{{ sizeReductionText }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span>压缩比率:</span>
|
||||
<span>{{ compressionRatioText }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="size-reduction" v-if="compressionInfo.ratio > 0">
|
||||
<n-text strong>
|
||||
减少 {{ compressionInfo.ratio }}%,节省 {{ compressionInfo.saved }}
|
||||
</n-text>
|
||||
</div>
|
||||
|
||||
<!-- 按钮组 -->
|
||||
<div class="button-group">
|
||||
<n-button
|
||||
type="primary"
|
||||
class="download-btn"
|
||||
:disabled="!compressedImage"
|
||||
@click="downloadCompressedImage"
|
||||
size="large"
|
||||
>
|
||||
<template #icon>
|
||||
<n-icon><DownloadOutline /></n-icon>
|
||||
</template>
|
||||
下载压缩图片
|
||||
</n-button>
|
||||
|
||||
<n-button
|
||||
type="default"
|
||||
class="clear-btn"
|
||||
:disabled="!originalImage && !compressedImage"
|
||||
@click="clearAll"
|
||||
size="large"
|
||||
>
|
||||
<template #icon>
|
||||
<n-icon><RefreshOutline /></n-icon>
|
||||
</template>
|
||||
清空重置
|
||||
</n-button>
|
||||
</div>
|
||||
</n-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import {
|
||||
NUpload,
|
||||
NUploadDragger,
|
||||
NButton,
|
||||
NCard,
|
||||
NSlider,
|
||||
NSelect,
|
||||
NText,
|
||||
NIcon,
|
||||
useMessage
|
||||
} from 'naive-ui'
|
||||
import { DownloadOutline, RefreshOutline } from '@vicons/ionicons5'
|
||||
|
||||
// 响应式数据
|
||||
const uploadRef = ref(null)
|
||||
const originalImage = ref('')
|
||||
const compressedImage = ref('')
|
||||
const originalFile = ref(null)
|
||||
const isCompressing = ref(false)
|
||||
|
||||
// 控制参数
|
||||
const widthValue = ref(800)
|
||||
const heightValue = ref(600)
|
||||
const qualityValue = ref(80)
|
||||
const outputFormat = ref('image/jpeg')
|
||||
|
||||
// 文件信息
|
||||
const originalSize = ref(0)
|
||||
const compressedSize = ref(0)
|
||||
|
||||
const message = useMessage()
|
||||
|
||||
// 格式选项
|
||||
const formatOptions = [
|
||||
{ label: 'JPG', value: 'image/jpeg' },
|
||||
{ label: 'PNG', value: 'image/png' },
|
||||
{ label: 'WebP', value: 'image/webp' }
|
||||
]
|
||||
|
||||
// 计算属性
|
||||
const originalSizeText = computed(() => {
|
||||
return originalSize.value ? formatFileSize(originalSize.value) : '-'
|
||||
})
|
||||
|
||||
const compressedSizeText = computed(() => {
|
||||
return compressedSize.value ? formatFileSize(compressedSize.value) : '-'
|
||||
})
|
||||
|
||||
const sizeReductionText = computed(() => {
|
||||
if (!originalSize.value || !compressedSize.value) return '-'
|
||||
const reduction = originalSize.value - compressedSize.value
|
||||
return formatFileSize(reduction)
|
||||
})
|
||||
|
||||
const compressionRatioText = computed(() => {
|
||||
if (!originalSize.value || !compressedSize.value) return '-'
|
||||
const ratio = ((originalSize.value - compressedSize.value) / originalSize.value * 100).toFixed(1)
|
||||
return `${ratio}%`
|
||||
})
|
||||
|
||||
const compressionInfo = computed(() => {
|
||||
if (!originalSize.value || !compressedSize.value) {
|
||||
return { ratio: 0, saved: '' }
|
||||
}
|
||||
|
||||
const reduction = originalSize.value - compressedSize.value
|
||||
const ratio = ((reduction / originalSize.value) * 100).toFixed(1)
|
||||
const saved = formatFileSize(reduction)
|
||||
|
||||
return { ratio, saved }
|
||||
})
|
||||
|
||||
// 监听参数变化,自动重新压缩
|
||||
watch([widthValue, heightValue, qualityValue], () => {
|
||||
if (originalImage.value && !isCompressing.value) {
|
||||
compressImage()
|
||||
}
|
||||
}, { deep: true })
|
||||
|
||||
// 方法
|
||||
function selectFile() {
|
||||
uploadRef.value?.openFileDialog()
|
||||
}
|
||||
|
||||
function handleFileSelect({ fileList }) {
|
||||
if (fileList.length > 0) {
|
||||
const file = fileList[0].file
|
||||
handleFile(file)
|
||||
}
|
||||
}
|
||||
|
||||
function handleFile(file) {
|
||||
if (!file.type.match('image.*')) {
|
||||
message.error('请选择图片文件 (JPG, PNG, WebP)')
|
||||
return
|
||||
}
|
||||
|
||||
originalFile.value = file
|
||||
originalSize.value = file.size
|
||||
|
||||
// 读取并显示原始图片
|
||||
const reader = new FileReader()
|
||||
reader.onload = (e) => {
|
||||
originalImage.value = e.target.result
|
||||
// 自动开始压缩
|
||||
compressImage()
|
||||
}
|
||||
reader.readAsDataURL(file)
|
||||
}
|
||||
|
||||
function handleSliderChange() {
|
||||
// 通过 watch 自动触发压缩
|
||||
}
|
||||
|
||||
function handleFormatChange() {
|
||||
if (originalImage.value && !isCompressing.value) {
|
||||
compressImage()
|
||||
}
|
||||
}
|
||||
|
||||
async function compressImage() {
|
||||
if (!originalImage.value) return
|
||||
|
||||
isCompressing.value = true
|
||||
compressedImage.value = ''
|
||||
|
||||
try {
|
||||
// 延迟一点让 UI 更新
|
||||
await new Promise(resolve => setTimeout(resolve, 100))
|
||||
|
||||
const img = new Image()
|
||||
img.onload = () => {
|
||||
try {
|
||||
// 计算新尺寸(保持宽高比)
|
||||
let width = img.width
|
||||
let height = img.height
|
||||
|
||||
if (width > widthValue.value) {
|
||||
height = (widthValue.value / width) * height
|
||||
width = widthValue.value
|
||||
}
|
||||
|
||||
if (height > heightValue.value) {
|
||||
width = (heightValue.value / height) * width
|
||||
height = heightValue.value
|
||||
}
|
||||
|
||||
// 创建 Canvas 进行压缩
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = width
|
||||
canvas.height = height
|
||||
|
||||
const ctx = canvas.getContext('2d')
|
||||
ctx.imageSmoothingEnabled = true
|
||||
ctx.imageSmoothingQuality = 'high'
|
||||
|
||||
// 如果是 PNG 格式且不是透明图片,添加白色背景
|
||||
if (outputFormat.value === 'image/png') {
|
||||
ctx.fillStyle = '#ffffff'
|
||||
ctx.fillRect(0, 0, width, height)
|
||||
}
|
||||
|
||||
ctx.drawImage(img, 0, 0, width, height)
|
||||
|
||||
// 获取压缩后的数据URL
|
||||
let dataURL
|
||||
const quality = qualityValue.value / 100
|
||||
|
||||
if (outputFormat.value === 'image/png') {
|
||||
dataURL = canvas.toDataURL('image/png')
|
||||
} else if (outputFormat.value === 'image/webp') {
|
||||
dataURL = canvas.toDataURL('image/webp', quality)
|
||||
} else {
|
||||
dataURL = canvas.toDataURL('image/jpeg', quality)
|
||||
}
|
||||
|
||||
// 计算压缩后的文件大小(估算)
|
||||
const compressedFileSize = Math.round(dataURL.length * 0.75)
|
||||
compressedSize.value = compressedFileSize
|
||||
|
||||
// 显示压缩后的图片
|
||||
compressedImage.value = dataURL
|
||||
|
||||
isCompressing.value = false
|
||||
} catch (error) {
|
||||
console.error('压缩错误:', error)
|
||||
message.error('压缩出错,请重试')
|
||||
isCompressing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
img.onerror = () => {
|
||||
message.error('图片加载失败')
|
||||
isCompressing.value = false
|
||||
}
|
||||
|
||||
img.src = originalImage.value
|
||||
} catch (error) {
|
||||
console.error('压缩过程出错:', error)
|
||||
message.error('压缩过程出错')
|
||||
isCompressing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function downloadCompressedImage() {
|
||||
if (!compressedImage.value || !originalFile.value) return
|
||||
|
||||
try {
|
||||
const link = document.createElement('a')
|
||||
link.href = compressedImage.value
|
||||
link.download = getOutputFileName(originalFile.value.name, outputFormat.value)
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
|
||||
message.success('图片下载成功')
|
||||
} catch (error) {
|
||||
console.error('下载失败:', error)
|
||||
message.error('下载失败,请重试')
|
||||
}
|
||||
}
|
||||
|
||||
// 清空重置所有数据
|
||||
function clearAll() {
|
||||
// 重置图片数据
|
||||
originalImage.value = ''
|
||||
compressedImage.value = ''
|
||||
originalFile.value = null
|
||||
isCompressing.value = false
|
||||
|
||||
// 重置文件信息
|
||||
originalSize.value = 0
|
||||
compressedSize.value = 0
|
||||
|
||||
// 重置控制参数为默认值
|
||||
widthValue.value = 800
|
||||
heightValue.value = 600
|
||||
qualityValue.value = 80
|
||||
outputFormat.value = 'image/jpeg'
|
||||
|
||||
// 清空上传组件的文件列表
|
||||
if (uploadRef.value) {
|
||||
uploadRef.value.clear()
|
||||
}
|
||||
|
||||
message.success('已清空所有数据')
|
||||
}
|
||||
|
||||
// 工具函数
|
||||
function formatFileSize(bytes) {
|
||||
if (bytes < 1024) return bytes + ' B'
|
||||
else if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB'
|
||||
else return (bytes / 1048576).toFixed(1) + ' MB'
|
||||
}
|
||||
|
||||
function getOutputFileName(originalName, format) {
|
||||
const ext = format.split('/')[1]
|
||||
const nameWithoutExt = originalName.replace(/\.[^/.]+$/, '')
|
||||
return `${nameWithoutExt}_compressed.${ext}`
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.image-compress-home {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
text-align: center;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
font-size: 28px;
|
||||
margin: 0 0 8px 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 16px;
|
||||
color: var(--n-text-color-depth-2);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.content {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.image-compress-home {
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.left-panel,
|
||||
.right-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.upload-section {
|
||||
padding: 32px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.upload-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.upload-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.upload-content h3 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.upload-content p {
|
||||
margin: 0;
|
||||
color: var(--n-text-color-depth-2);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.or-text {
|
||||
color: var(--n-text-color-depth-3);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.controls {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.control-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.control-group:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.control-group h4 {
|
||||
font-size: 16px;
|
||||
margin: 0 0 16px 0;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.slider-container {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.slider-container:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.slider-container .n-text {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.preview-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.preview-container h4,
|
||||
.info-card h4 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.image-container {
|
||||
min-height: 180px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--n-card-color);
|
||||
border: 1px solid var(--n-border-color);
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.image-container img {
|
||||
max-width: 100%;
|
||||
max-height: 250px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
color: var(--n-text-color-depth-3);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.info-items {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 8px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.info-item:not(:last-child) {
|
||||
border-bottom: 1px solid var(--n-border-color);
|
||||
}
|
||||
|
||||
.size-reduction {
|
||||
text-align: center;
|
||||
padding: 12px;
|
||||
background: var(--n-primary-color-suppl);
|
||||
border-radius: 6px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.download-btn,
|
||||
.clear-btn {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.button-group {
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
403
src/renderer/src/components/ToolBox/ImageUpload/ImageDisplay.vue
Normal file
403
src/renderer/src/components/ToolBox/ImageUpload/ImageDisplay.vue
Normal file
@ -0,0 +1,403 @@
|
||||
<template>
|
||||
<div class="image-display">
|
||||
<!-- 标题和操作栏 -->
|
||||
<div class="display-header">
|
||||
<h2>已上传图片 ({{ total ?? 0 }})</h2>
|
||||
<n-space>
|
||||
<n-button @click="refreshList" type="primary">
|
||||
<template #icon>
|
||||
<n-icon><RefreshOutline /></n-icon>
|
||||
</template>
|
||||
刷新数据
|
||||
</n-button>
|
||||
<!-- 修改为下拉菜单形式的导出 -->
|
||||
<n-dropdown
|
||||
:options="exportOptions"
|
||||
:disabled="imageList.length === 0"
|
||||
@select="handleExportSelect"
|
||||
>
|
||||
<n-button type="primary">
|
||||
<template #icon>
|
||||
<n-icon><DownloadOutline /></n-icon>
|
||||
</template>
|
||||
导出数据
|
||||
<template #icon-right>
|
||||
<n-icon><ChevronDownOutline /></n-icon>
|
||||
</template>
|
||||
</n-button>
|
||||
</n-dropdown>
|
||||
</n-space>
|
||||
</div>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<n-data-table
|
||||
ref="tableRef"
|
||||
:columns="columns"
|
||||
:data="imageList"
|
||||
:pagination="paginationReactive"
|
||||
:loading="tableLoading"
|
||||
striped
|
||||
size="small"
|
||||
:remote="true"
|
||||
:scroll-x="1200"
|
||||
flex-height
|
||||
style="height: 500px"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, h } from 'vue'
|
||||
import {
|
||||
NDataTable,
|
||||
NButton,
|
||||
NSpace,
|
||||
NImage,
|
||||
NTag,
|
||||
NIcon,
|
||||
NDropdown, // 添加这个导入
|
||||
useMessage
|
||||
} from 'naive-ui'
|
||||
import {
|
||||
RefreshOutline,
|
||||
DownloadOutline,
|
||||
CopyOutline,
|
||||
ChevronDownOutline // 添加这个导入
|
||||
} from '@vicons/ionicons5'
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits(['paginationChange'])
|
||||
|
||||
// 响应式数据
|
||||
const tableRef = ref(null)
|
||||
const tableLoading = ref(true)
|
||||
|
||||
const total = ref(0)
|
||||
const imageList = ref([])
|
||||
|
||||
const message = useMessage()
|
||||
|
||||
// 导出选项
|
||||
const exportOptions = [
|
||||
{
|
||||
label: '导出为 JSON',
|
||||
key: 'json',
|
||||
icon: () => h(NIcon, () => h(DownloadOutline))
|
||||
},
|
||||
{
|
||||
label: '导出为 CSV',
|
||||
key: 'csv',
|
||||
icon: () => h(NIcon, () => h(DownloadOutline))
|
||||
}
|
||||
]
|
||||
|
||||
// 关键修改:使用 reactive 而不是 ref
|
||||
const paginationReactive = reactive({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
itemCount: 0,
|
||||
showSizePicker: true,
|
||||
showQuickJumper: true,
|
||||
pageSizes: [10, 20, 50, 100],
|
||||
onChange: (page) => {
|
||||
paginationReactive.page = page
|
||||
refreshList()
|
||||
},
|
||||
prefix({ itemCount }) {
|
||||
return `共 ${itemCount} 条`
|
||||
},
|
||||
onUpdatePageSize: (pageSize) => {
|
||||
paginationReactive.pageSize = pageSize
|
||||
paginationReactive.page = 1
|
||||
refreshList()
|
||||
}
|
||||
})
|
||||
|
||||
// 表格列配置
|
||||
const columns = [
|
||||
{
|
||||
title: '序号',
|
||||
key: 'index',
|
||||
width: 60,
|
||||
render: (row, index) => {
|
||||
return (paginationReactive.page - 1) * paginationReactive.pageSize + index + 1
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '预览',
|
||||
key: 'preview',
|
||||
width: 80,
|
||||
render: (row) => {
|
||||
return h(NImage, {
|
||||
width: 50,
|
||||
height: 50,
|
||||
src: row.url,
|
||||
objectFit: 'cover',
|
||||
style: 'border-radius: 4px; cursor: pointer'
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '文件地址',
|
||||
key: 'url',
|
||||
width: 300,
|
||||
render: (row) => {
|
||||
return h('div', { style: 'display: flex; align-items: center; gap: 8px' }, [
|
||||
h(
|
||||
'a',
|
||||
{
|
||||
href: 'javascript:void(0)',
|
||||
style:
|
||||
'color: var(--n-color-target); text-decoration: none; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap',
|
||||
title: row.url
|
||||
},
|
||||
row.url
|
||||
),
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
size: 'tiny',
|
||||
quaternary: true,
|
||||
circle: true,
|
||||
onClick: () => copyBase64(row)
|
||||
},
|
||||
{ icon: () => h(NIcon, () => h(CopyOutline)) }
|
||||
)
|
||||
])
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '文件大小',
|
||||
key: 'fileSize',
|
||||
width: 100,
|
||||
render: (row) => formatFileSize(row.fileSize)
|
||||
},
|
||||
{
|
||||
title: '文件类型',
|
||||
key: 'type',
|
||||
width: 100,
|
||||
render: (row) => {
|
||||
return h(NTag, { size: 'small', type: 'info' }, () => row.contentType)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '上传时间',
|
||||
key: 'uploadTime',
|
||||
width: 160,
|
||||
render: (row) => formatDate(row.uploadTime)
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 150,
|
||||
fixed: 'right',
|
||||
render: (row) => {
|
||||
return h(NSpace, { size: 'small' }, () => [
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
size: 'small',
|
||||
type: 'info',
|
||||
quaternary: true,
|
||||
onClick: () => copyBase64(row)
|
||||
},
|
||||
{ default: () => '复制', icon: () => h(NIcon, () => h(CopyOutline)) }
|
||||
)
|
||||
])
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
async function copyBase64(image) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(image.url)
|
||||
message.success('文件地址已复制到剪贴板')
|
||||
} catch (error) {
|
||||
message.error('复制失败,请手动复制')
|
||||
}
|
||||
}
|
||||
|
||||
function refreshList() {
|
||||
tableLoading.value = true
|
||||
emit('paginationChange', {
|
||||
page: paginationReactive.page,
|
||||
pageSize: paginationReactive.pageSize
|
||||
})
|
||||
}
|
||||
|
||||
// 处理导出选择
|
||||
function handleExportSelect(key) {
|
||||
if (key === 'json') {
|
||||
exportAsJson()
|
||||
} else if (key === 'csv') {
|
||||
exportAsCsv()
|
||||
}
|
||||
}
|
||||
|
||||
// 导出为 JSON
|
||||
function exportAsJson() {
|
||||
try {
|
||||
const data = imageList.value
|
||||
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' })
|
||||
downloadFile(blob, `images_export_${getTimestamp()}.json`)
|
||||
message.success('JSON 数据导出成功')
|
||||
} catch (error) {
|
||||
console.error('JSON 导出失败:', error)
|
||||
message.error('JSON 导出失败,请重试')
|
||||
}
|
||||
}
|
||||
|
||||
// 导出为 CSV
|
||||
function exportAsCsv() {
|
||||
try {
|
||||
const data = imageList.value
|
||||
if (data.length === 0) {
|
||||
message.warning('没有数据可导出')
|
||||
return
|
||||
}
|
||||
|
||||
// CSV 表头
|
||||
const headers = ['序号', '文件名', '文件地址', '文件大小', '文件类型', '上传时间']
|
||||
|
||||
// 转换数据
|
||||
const csvRows = [
|
||||
headers.join(','), // 表头行
|
||||
...data.map((item, index) =>
|
||||
[
|
||||
index + 1, // 序号
|
||||
`"${item.fileName || ''}"`, // 文件名(用引号包围以处理逗号)
|
||||
`"${item.url || ''}"`, // 文件地址
|
||||
`"${formatFileSize(item.fileSize || 0)}"`, // 文件大小
|
||||
`"${item.contentType || ''}"`, // 文件类型
|
||||
`"${formatDate(item.uploadTime || new Date())}"` // 上传时间
|
||||
].join(',')
|
||||
)
|
||||
]
|
||||
|
||||
const csvContent = csvRows.join('\n')
|
||||
|
||||
// 添加 BOM 以支持中文
|
||||
const bom = '\uFEFF'
|
||||
const blob = new Blob([bom + csvContent], { type: 'text/csv;charset=utf-8' })
|
||||
|
||||
downloadFile(blob, `images_export_${getTimestamp()}.csv`)
|
||||
message.success('CSV 数据导出成功')
|
||||
} catch (error) {
|
||||
console.error('CSV 导出失败:', error)
|
||||
message.error('CSV 导出失败,请重试')
|
||||
}
|
||||
}
|
||||
|
||||
// 通用下载函数
|
||||
function downloadFile(blob, filename) {
|
||||
const url = URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = filename
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
// 获取时间戳字符串
|
||||
function getTimestamp() {
|
||||
const now = new Date()
|
||||
return (
|
||||
now.getFullYear() +
|
||||
String(now.getMonth() + 1).padStart(2, '0') +
|
||||
String(now.getDate()).padStart(2, '0') +
|
||||
'_' +
|
||||
String(now.getHours()).padStart(2, '0') +
|
||||
String(now.getMinutes()).padStart(2, '0') +
|
||||
String(now.getSeconds()).padStart(2, '0')
|
||||
)
|
||||
}
|
||||
|
||||
// 工具函数
|
||||
function formatFileSize(bytes) {
|
||||
if (bytes === 0) return '0 Bytes'
|
||||
const k = 1024
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB']
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
||||
}
|
||||
|
||||
function formatDate(date) {
|
||||
return new Intl.DateTimeFormat('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
}).format(new Date(date))
|
||||
}
|
||||
|
||||
function reloadImageData(value) {
|
||||
console.log('重新加载图片数据', value)
|
||||
|
||||
// 更新数据
|
||||
total.value = value.total || 0
|
||||
imageList.value = value.collection ?? []
|
||||
tableLoading.value = false
|
||||
|
||||
// 设置分页信息
|
||||
paginationReactive.page = value.current || 1
|
||||
paginationReactive.itemCount = value.total || 0
|
||||
|
||||
// 删除手动设置 pageCount,让组件自动计算
|
||||
// paginationReactive.value.pageCount = Math.ceil(value.total / 10) // 删除这行
|
||||
|
||||
console.log('分页调试信息:', {
|
||||
total: value.total,
|
||||
pageSize: paginationReactive.pageSize,
|
||||
itemCount: paginationReactive.itemCount,
|
||||
currentPage: paginationReactive.page
|
||||
})
|
||||
}
|
||||
|
||||
// 暴露方法
|
||||
defineExpose({ reloadImageData })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.image-display {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.display-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.display-header h2 {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.preview-content {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.preview-info {
|
||||
margin-top: 20px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.image-display {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.display-header {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,95 @@
|
||||
<template>
|
||||
<div class="image-upload-home">
|
||||
<!-- 页面标题 -->
|
||||
<div class="page-header">
|
||||
<h1>图片上传工具</h1>
|
||||
<p>上传图片到LaiTool图床,获取图片链接</p>
|
||||
</div>
|
||||
|
||||
<!-- 上传组件 -->
|
||||
<ImageUploader @upload-success="handleUploadSuccess" />
|
||||
|
||||
<!-- 分割线 -->
|
||||
<n-divider />
|
||||
|
||||
<!-- 显示组件 -->
|
||||
<ImageDisplay
|
||||
ref="imageDisplayRef"
|
||||
@vue:mounted="loadImageList"
|
||||
:image-list="imageList"
|
||||
@pagination-change="paginationChange"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { NDivider, useMessage } from 'naive-ui'
|
||||
import ImageUploader from './ImageUploader.vue'
|
||||
import ImageDisplay from './ImageDisplay.vue'
|
||||
import { define } from '@/define/define'
|
||||
|
||||
import { useSystemStore } from '@/stores/system'
|
||||
|
||||
// 响应式数据
|
||||
const imageList = ref([])
|
||||
const message = useMessage()
|
||||
|
||||
const imageDisplayRef = ref(null)
|
||||
|
||||
const systemStore = useSystemStore()
|
||||
|
||||
onMounted(() => {})
|
||||
|
||||
// 加载数据
|
||||
async function loadImageList({ page = 1, pageSize = 10 }) {
|
||||
let res = await window.axios.get(
|
||||
define.lms +
|
||||
`/lms/FileUpload/GetFilesByMachineId/${systemStore.machineId}?page=${page}&pageSize=${pageSize}`
|
||||
)
|
||||
imageDisplayRef.value.reloadImageData(res.data.data)
|
||||
}
|
||||
|
||||
// 处理上传成功
|
||||
async function handleUploadSuccess() {
|
||||
// 重新加载数据
|
||||
await loadImageList()
|
||||
}
|
||||
async function paginationChange(value) {
|
||||
// 分页变化时重新加载数据
|
||||
await loadImageList(value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.image-upload-home {
|
||||
padding: 24px;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
min-width: 600px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
text-align: center;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
color: var(--n-text-color);
|
||||
}
|
||||
|
||||
.page-header p {
|
||||
margin: 0;
|
||||
color: var(--n-text-color-depth-2);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.image-upload-home {
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,470 @@
|
||||
<template>
|
||||
<div class="image-uploader">
|
||||
<!-- 上传区域 -->
|
||||
<n-upload
|
||||
ref="uploadRef"
|
||||
:max="1"
|
||||
accept="image/*"
|
||||
:disabled="uploading"
|
||||
:default-upload="false"
|
||||
@change="handleFileChange"
|
||||
:show-file-list="false"
|
||||
>
|
||||
<n-upload-dragger>
|
||||
<div class="upload-content">
|
||||
<n-icon size="48" :depth="uploading ? 2 : 3">
|
||||
<SyncOutline v-if="uploading" />
|
||||
<CloudUploadOutline v-else />
|
||||
</n-icon>
|
||||
<n-text style="font-size: 16px; margin-top: 8px">
|
||||
{{ uploading ? '正在处理中...' : '点击或拖拽图片到此区域上传' }}
|
||||
</n-text>
|
||||
<n-text depth="3" style="font-size: 12px; margin-top: 4px">
|
||||
{{
|
||||
uploading
|
||||
? '请等待当前文件处理完成'
|
||||
: '支持 JPG、PNG、GIF、WebP 格式,单个文件不超过 5MB'
|
||||
}}
|
||||
</n-text>
|
||||
</div>
|
||||
</n-upload-dragger>
|
||||
</n-upload>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="upload-actions">
|
||||
<n-space>
|
||||
<n-button
|
||||
@click="startUpload"
|
||||
:disabled="!selectedFile || uploading"
|
||||
size="medium"
|
||||
type="success"
|
||||
>
|
||||
<template #icon>
|
||||
<n-icon><CloudUploadOutline /></n-icon>
|
||||
</template>
|
||||
开始上传
|
||||
</n-button>
|
||||
|
||||
<n-button @click="clearFiles" size="medium">
|
||||
<template #icon>
|
||||
<n-icon><StopOutline /></n-icon>
|
||||
</template>
|
||||
清空选择
|
||||
</n-button>
|
||||
</n-space>
|
||||
</div>
|
||||
<!-- 选中的文件信息 -->
|
||||
<div class="selected-file" v-if="selectedFile">
|
||||
<n-card size="small" style="margin-top: 16px">
|
||||
<div class="file-info">
|
||||
<div class="file-details">
|
||||
<n-text strong>{{ selectedFile.name }}</n-text>
|
||||
<n-text depth="2" style="font-size: 12px">
|
||||
{{ formatFileSize(selectedFile.size) }} | {{ selectedFile.type }}
|
||||
</n-text>
|
||||
<!-- 简单的上传状态提示 -->
|
||||
<div v-if="uploading" class="upload-status-text">
|
||||
<n-text depth="3" style="font-size: 11px; color: var(--n-color-warning)">
|
||||
{{ statusText }}
|
||||
</n-text>
|
||||
</div>
|
||||
</div>
|
||||
<div class="file-preview" v-if="previewUrl">
|
||||
<!-- 预览图片容器 -->
|
||||
<div class="preview-container">
|
||||
<n-image
|
||||
:render-toolbar="renderToolbar"
|
||||
:src="previewUrl"
|
||||
alt="预览"
|
||||
style="width: 50px; height: 50px; object-fit: cover; border-radius: 4px"
|
||||
:class="{ 'uploading-blur': uploading }"
|
||||
/>
|
||||
<!-- 上传时的加载动画覆盖层 -->
|
||||
<div v-if="uploading" class="loading-overlay">
|
||||
<n-spin size="small" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</n-card>
|
||||
</div>
|
||||
|
||||
<!-- 上传提示 -->
|
||||
<div class="upload-tips">
|
||||
<n-alert type="info" style="margin-top: 16px">
|
||||
<template #icon>
|
||||
<n-icon><InformationCircleOutline /></n-icon>
|
||||
</template>
|
||||
<div>
|
||||
<p style="margin: 0 0 8px 0"><strong>上传说明:</strong></p>
|
||||
<ul style="margin: 0; padding-left: 16px">
|
||||
<li>选择文件后点击"开始上传"按钮进行上传</li>
|
||||
<li>每次只能上传一个文件,并且上传的文件会留存在服务器,介意请勿用!!!</li>
|
||||
<li>支持的格式:JPG、PNG、JPEG、WebP</li>
|
||||
<li>文件大小限制:最大 5MB</li>
|
||||
<li>单日上传次数限制:5</li>
|
||||
</ul>
|
||||
</div>
|
||||
</n-alert>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import {
|
||||
NUpload,
|
||||
NUploadDragger,
|
||||
NIcon,
|
||||
NText,
|
||||
NButton,
|
||||
NSpace,
|
||||
NProgress,
|
||||
NAlert,
|
||||
NCard,
|
||||
useMessage
|
||||
} from 'naive-ui'
|
||||
import {
|
||||
CloudUploadOutline,
|
||||
FolderOpenOutline,
|
||||
StopOutline,
|
||||
SyncOutline,
|
||||
InformationCircleOutline
|
||||
} from '@vicons/ionicons5'
|
||||
import { TimeDelay } from '@/define/Tools/time'
|
||||
import { define } from '@/define/define'
|
||||
import { useSystemStore } from '@/stores/system'
|
||||
import { isEmpty } from 'lodash'
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits(['uploadSuccess', 'uploadError'])
|
||||
|
||||
// 响应式数据
|
||||
const uploadRef = ref(null)
|
||||
const uploading = ref(false)
|
||||
const statusText = ref('')
|
||||
const selectedFile = ref(null)
|
||||
const previewUrl = ref('')
|
||||
|
||||
const systemStore = useSystemStore()
|
||||
|
||||
const message = useMessage()
|
||||
|
||||
// 上传图片预览 修改工具栏
|
||||
const renderToolbar = ({ nodes }) => {
|
||||
return [nodes.close]
|
||||
}
|
||||
|
||||
// 处理文件选择变化
|
||||
function handleFileChange({ fileList }) {
|
||||
if (fileList.length > 0) {
|
||||
const file = fileList[0].file
|
||||
if (validateFile(file)) {
|
||||
selectedFile.value = file
|
||||
// 生成预览URL
|
||||
if (previewUrl.value) {
|
||||
URL.revokeObjectURL(previewUrl.value)
|
||||
}
|
||||
previewUrl.value = URL.createObjectURL(file)
|
||||
message.success(`已选择文件: ${file.name}`)
|
||||
}
|
||||
} else {
|
||||
selectedFile.value = null
|
||||
if (previewUrl.value) {
|
||||
URL.revokeObjectURL(previewUrl.value)
|
||||
previewUrl.value = ''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 开始上传
|
||||
async function startUpload() {
|
||||
if (!selectedFile.value) {
|
||||
message.warning('请先选择文件')
|
||||
return
|
||||
}
|
||||
|
||||
if (uploading.value) {
|
||||
message.warning('请等待当前文件上传完成')
|
||||
return
|
||||
}
|
||||
|
||||
await handleUpload(selectedFile.value)
|
||||
}
|
||||
|
||||
async function handleUpload(file) {
|
||||
const fileName = file.name
|
||||
|
||||
// 重置状态
|
||||
uploading.value = true
|
||||
statusText.value = '开始上传文件...'
|
||||
await TimeDelay(500)
|
||||
|
||||
try {
|
||||
// 阶段2: 图片处理和压缩
|
||||
const maxSize = 5 * 1024 * 1024
|
||||
if (file.size > maxSize) {
|
||||
statusText.value = '文件超出限制,请压缩后上传!!'
|
||||
return
|
||||
}
|
||||
|
||||
statusText.value = '开始处理图片文件...'
|
||||
const imageData = await processImage(file)
|
||||
|
||||
// 判断是不是有前缀,有的话 需要删除 data:image/jpeg;base64, 这样的
|
||||
if (imageData.base64.startsWith('data:')) {
|
||||
imageData.base64 = imageData.base64.split(',')[1]
|
||||
}
|
||||
await TimeDelay(500)
|
||||
// 阶段3: 上传文件
|
||||
statusText.value = '图片处理完毕,开始上传文件...'
|
||||
|
||||
// 开始调用上传接口
|
||||
if (isEmpty(systemStore.machineId)) {
|
||||
message.error('未找到机器ID,请重启软件后重试!!')
|
||||
return
|
||||
}
|
||||
let res = await window.axios.post(
|
||||
define.lms + `/lms/FileUpload/FileUpload/${systemStore.machineId}`,
|
||||
{
|
||||
file: imageData.base64,
|
||||
fileName: fileName,
|
||||
contentType: file.type
|
||||
}
|
||||
)
|
||||
await TimeDelay(500)
|
||||
|
||||
if (!res.success) {
|
||||
// 成功
|
||||
message.error(`上传失败: ${res.message || '未知错误'}`)
|
||||
return
|
||||
}
|
||||
|
||||
if (res.data && res.data.code == 1) {
|
||||
const uploadedImage = res.data
|
||||
// 发送上传成功事件
|
||||
emit('uploadSuccess', uploadedImage)
|
||||
// 清空选择的文件
|
||||
clearFiles()
|
||||
} else {
|
||||
// 上传失败
|
||||
message.error(`上传失败: ${res.data.message || '未知错误'}`)
|
||||
return
|
||||
}
|
||||
} catch (error) {
|
||||
message.error(`上传失败: ${error.message || '未知错误'}`)
|
||||
} finally {
|
||||
// 延迟重置状态,让用户看到完成/错误状态
|
||||
setTimeout(() => {
|
||||
uploading.value = false
|
||||
statusText.value = ''
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
|
||||
// 文件大小校验,文件类型校验
|
||||
function validateFile(file) {
|
||||
console.log('Validating file:', file.name, file.type, file.size)
|
||||
|
||||
// 验证文件类型
|
||||
const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp']
|
||||
if (!allowedTypes.includes(file.type)) {
|
||||
message.error('只支持 JPG、PNG、GIF、WebP 格式的图片')
|
||||
return false
|
||||
}
|
||||
|
||||
// 验证文件大小 (10MB)
|
||||
const maxSize = 5 * 1024 * 1024
|
||||
if (file.size > maxSize) {
|
||||
message.error('文件大小不能超过 5MB')
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
async function processImage(file) {
|
||||
console.log('Processing image:', file.name)
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader()
|
||||
const img = new Image()
|
||||
|
||||
reader.onload = (e) => {
|
||||
img.onload = () => {
|
||||
console.log('Image loaded, dimensions:', img.width, 'x', img.height)
|
||||
resolve({
|
||||
base64: e.target.result,
|
||||
width: img.width,
|
||||
height: img.height
|
||||
})
|
||||
}
|
||||
img.onerror = () => reject(new Error('无法加载图片'))
|
||||
img.src = e.target.result
|
||||
}
|
||||
|
||||
reader.onerror = () => reject(new Error('无法读取文件'))
|
||||
reader.readAsDataURL(file)
|
||||
})
|
||||
}
|
||||
|
||||
function clearFiles() {
|
||||
console.log('Clear files clicked')
|
||||
selectedFile.value = null
|
||||
if (previewUrl.value) {
|
||||
URL.revokeObjectURL(previewUrl.value)
|
||||
previewUrl.value = ''
|
||||
}
|
||||
// 清空 upload 组件的文件列表
|
||||
if (uploadRef.value) {
|
||||
uploadRef.value.clear()
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化文件大小
|
||||
function formatFileSize(bytes) {
|
||||
if (bytes === 0) return '0 Bytes'
|
||||
const k = 1024
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB']
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.image-uploader {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.upload-content {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.upload-status {
|
||||
margin-top: 16px;
|
||||
padding: 20px;
|
||||
background: var(--n-card-color);
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--n-border-color);
|
||||
}
|
||||
|
||||
.status-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.status-actions {
|
||||
margin-top: 12px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.upload-actions {
|
||||
margin-top: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.selected-file {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.file-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.file-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.upload-tips {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.upload-tips ul li {
|
||||
margin-bottom: 4px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.image-uploader {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.upload-actions {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.status-info {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.preview-container {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.uploading-blur {
|
||||
filter: blur(1px);
|
||||
opacity: 0.7;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.loading-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
border-radius: 4px;
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
.upload-progress-info {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.upload-status-badge {
|
||||
margin-top: 12px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.file-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start; /* 改为 flex-start 以便对齐 */
|
||||
}
|
||||
|
||||
/* 暗色主题适配 */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.loading-overlay {
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
}
|
||||
|
||||
/* 移动端适配 */
|
||||
@media (max-width: 768px) {
|
||||
.file-info {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.upload-status-badge {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
261
src/renderer/src/components/ToolBox/ToolBoxHome.vue
Normal file
261
src/renderer/src/components/ToolBox/ToolBoxHome.vue
Normal file
@ -0,0 +1,261 @@
|
||||
<template>
|
||||
<div class="toolbox-home">
|
||||
<!-- 顶部搜索和筛选区域 -->
|
||||
<div class="header-section">
|
||||
<div class="search-bar">
|
||||
<n-input
|
||||
v-model:value="searchKeyword"
|
||||
placeholder="搜索工具..."
|
||||
clearable
|
||||
size="large"
|
||||
style="max-width: 400px"
|
||||
>
|
||||
<template #prefix>
|
||||
<n-icon>
|
||||
<SearchOutline />
|
||||
</n-icon>
|
||||
</template>
|
||||
</n-input>
|
||||
</div>
|
||||
|
||||
<div class="filter-bar">
|
||||
<n-space>
|
||||
<n-select
|
||||
v-model:value="selectedCategory"
|
||||
:options="categoryOptions"
|
||||
placeholder="选择分类"
|
||||
clearable
|
||||
style="width: 150px"
|
||||
/>
|
||||
<n-select
|
||||
v-model:value="selectedTag"
|
||||
:options="tagOptions"
|
||||
placeholder="选择标签"
|
||||
clearable
|
||||
style="width: 120px"
|
||||
/>
|
||||
<n-button @click="resetFilters" quaternary>
|
||||
<template #icon>
|
||||
<n-icon><RefreshOutline /></n-icon>
|
||||
</template>
|
||||
重置
|
||||
</n-button>
|
||||
</n-space>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 工具分类标签页 -->
|
||||
<div class="category-tabs">
|
||||
<n-tabs v-model:value="activeCategory" type="line" animated>
|
||||
<n-tab-pane name="all" tab="全部">
|
||||
<ToolGrid :tools="filteredTools" @tool-click="handleToolClick" />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane
|
||||
v-for="category in categories"
|
||||
:key="category.key"
|
||||
:name="category.key"
|
||||
:tab="category.label"
|
||||
>
|
||||
<ToolGrid :tools="getToolsByCategory(category.key)" @tool-click="handleToolClick" />
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</div>
|
||||
|
||||
<!-- 统计信息 -->
|
||||
<div class="stats-section">
|
||||
<n-space>
|
||||
<n-statistic label="工具总数" :value="totalTools" />
|
||||
<n-statistic label="分类数量" :value="categories.length" />
|
||||
</n-space>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import {
|
||||
NInput,
|
||||
NSelect,
|
||||
NButton,
|
||||
NSpace,
|
||||
NTabs,
|
||||
NTabPane,
|
||||
NDivider,
|
||||
NTag,
|
||||
NStatistic,
|
||||
NIcon
|
||||
} from 'naive-ui'
|
||||
import { SearchOutline, RefreshOutline, FlashOutline, TimeOutline } from '@vicons/ionicons5'
|
||||
import ToolGrid from './ToolGrid.vue'
|
||||
import { toolsData, categories } from '@/renderer/src/common/toolData'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
// 响应式数据
|
||||
const searchKeyword = ref('')
|
||||
const selectedCategory = ref(null)
|
||||
const selectedTag = ref(null)
|
||||
const activeCategory = ref('all')
|
||||
|
||||
const message = useMessage()
|
||||
let router = useRouter()
|
||||
|
||||
// 计算属性
|
||||
const categoryOptions = computed(() =>
|
||||
categories.map((cat) => ({ label: cat.label, value: cat.key }))
|
||||
)
|
||||
|
||||
const tagOptions = computed(() => {
|
||||
const allTags = new Set()
|
||||
toolsData.forEach((tool) => {
|
||||
tool.tags?.forEach((tag) => allTags.add(tag))
|
||||
})
|
||||
return Array.from(allTags).map((tag) => ({ label: tag, value: tag }))
|
||||
})
|
||||
|
||||
const filteredTools = computed(() => {
|
||||
let tools = toolsData
|
||||
|
||||
// 关键词搜索
|
||||
if (searchKeyword.value) {
|
||||
const keyword = searchKeyword.value.toLowerCase()
|
||||
tools = tools.filter(
|
||||
(tool) =>
|
||||
tool.name.toLowerCase().includes(keyword) ||
|
||||
tool.description.toLowerCase().includes(keyword) ||
|
||||
tool.tags?.some((tag) => tag.toLowerCase().includes(keyword))
|
||||
)
|
||||
}
|
||||
|
||||
// 分类筛选
|
||||
if (selectedCategory.value) {
|
||||
tools = tools.filter((tool) => tool.category === selectedCategory.value)
|
||||
}
|
||||
|
||||
// 标签筛选
|
||||
if (selectedTag.value) {
|
||||
tools = tools.filter((tool) => tool.tags?.includes(selectedTag.value))
|
||||
}
|
||||
|
||||
// 当前标签页筛选
|
||||
if (activeCategory.value !== 'all') {
|
||||
tools = tools.filter((tool) => tool.category === activeCategory.value)
|
||||
}
|
||||
|
||||
return tools
|
||||
})
|
||||
|
||||
const totalTools = computed(() => toolsData.length)
|
||||
|
||||
// 方法
|
||||
function getToolsByCategory(categoryKey) {
|
||||
return toolsData.filter((tool) => tool.category === categoryKey)
|
||||
}
|
||||
|
||||
function handleToolClick(tool) {
|
||||
// 执行工具操作
|
||||
executeToolAction(tool)
|
||||
}
|
||||
|
||||
function executeToolAction(tool) {
|
||||
// 根据工具类型执行不同操作
|
||||
debugger
|
||||
switch (tool.action?.type) {
|
||||
case 'route':
|
||||
// 路由跳转
|
||||
if (!tool.action.route) {
|
||||
message.error('路由路径未配置')
|
||||
return
|
||||
}
|
||||
router.push(tool.action.route)
|
||||
break
|
||||
case 'function':
|
||||
// 执行函数
|
||||
console.log('执行函数:', tool.action.handler)
|
||||
if (typeof tool.action.handler === 'function') {
|
||||
tool.action.handler()
|
||||
}
|
||||
break
|
||||
case 'external':
|
||||
if (!tool.action.url) {
|
||||
message.error('外部链接未配置')
|
||||
return
|
||||
}
|
||||
window.api.OpenUrl(tool.action.url)
|
||||
break
|
||||
case 'dialog':
|
||||
// 打开弹窗
|
||||
console.log('打开弹窗:', tool.action.component)
|
||||
break
|
||||
default:
|
||||
message.error(`未知操作类型: ${tool.action?.type || '无'}`)
|
||||
}
|
||||
}
|
||||
|
||||
function resetFilters() {
|
||||
searchKeyword.value = ''
|
||||
selectedCategory.value = null
|
||||
selectedTag.value = null
|
||||
activeCategory.value = 'all'
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.toolbox-home {
|
||||
padding: 24px;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.header-section {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
flex: 1;
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.category-tabs {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.quick-access,
|
||||
.recent-tools {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.stats-section {
|
||||
margin-top: 32px;
|
||||
padding: 20px;
|
||||
background: var(--n-card-color);
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--n-border-color);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.toolbox-home {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.header-section {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
min-width: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
153
src/renderer/src/components/ToolBox/ToolGrid.vue
Normal file
153
src/renderer/src/components/ToolBox/ToolGrid.vue
Normal file
@ -0,0 +1,153 @@
|
||||
<template>
|
||||
<div class="tool-grid">
|
||||
<div v-for="tool in tools" :key="tool.id" class="tool-card" @click="$emit('toolClick', tool)">
|
||||
<n-card hoverable :class="{ 'tool-card-disabled': tool.disabled }" size="small">
|
||||
<div class="tool-content">
|
||||
<div class="tool-icon">
|
||||
<n-icon size="32">
|
||||
<component :is="tool.icon" />
|
||||
</n-icon>
|
||||
</div>
|
||||
|
||||
<div class="tool-info">
|
||||
<h3 class="tool-name">{{ tool.name }}</h3>
|
||||
<p class="tool-description">{{ tool.description }}</p>
|
||||
|
||||
<div class="tool-tags" v-if="tool.tags && tool.tags.length > 0">
|
||||
<n-tag v-for="tag in tool.tags" :key="tag" size="tiny" :bordered="false" type="info">
|
||||
{{ tag }}
|
||||
</n-tag>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tool-badge" v-if="tool.badge">
|
||||
<n-badge :value="tool.badge.text" :type="tool.badge.type" />
|
||||
</div>
|
||||
</div>
|
||||
</n-card>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-if="tools.length === 0" class="empty-state">
|
||||
<n-empty description="没有找到相关工具" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { NCard, NIcon, NTag, NBadge, NSpace, NEmpty } from 'naive-ui'
|
||||
|
||||
defineProps({
|
||||
tools: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
})
|
||||
|
||||
defineEmits(['toolClick'])
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tool-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.tool-card {
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.tool-card:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.tool-card-disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.tool-card-disabled:hover {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.tool-content {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
position: relative;
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
.tool-icon {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background: var(--n-color-target);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.tool-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.tool-name {
|
||||
margin: 0 0 4px 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--n-text-color);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.tool-description {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 13px;
|
||||
color: var(--n-text-color-depth-2);
|
||||
line-height: 1.4;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tool-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.tool-badge {
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
right: -4px;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
grid-column: 1 / -1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.tool-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.tool-content {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.tool-icon {
|
||||
align-self: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -78,11 +78,6 @@ const routes = [
|
||||
name: 'videogeneratesetting',
|
||||
component: () => import('./components/Setting/VideoGenerateSetting.vue')
|
||||
},
|
||||
{
|
||||
path: '/ShowMessage',
|
||||
name: 'ShowMessage',
|
||||
component: () => import('./components/Home/ShowMessage.vue')
|
||||
},
|
||||
{
|
||||
path: '/sdoriginal',
|
||||
name: 'sdoriginal',
|
||||
@ -122,6 +117,21 @@ const routes = [
|
||||
path: '/TTS_Services',
|
||||
name: 'TTS_Services',
|
||||
component: () => import('./components/TTS/TTSHome.vue')
|
||||
},
|
||||
{
|
||||
path: '/toolbox',
|
||||
name: 'toolbox',
|
||||
component: () => import('./components/ToolBox/ToolBoxHome.vue')
|
||||
},
|
||||
{
|
||||
path: '/toolbox/image-upload',
|
||||
name: 'image-upload',
|
||||
component: () => import('./components/ToolBox/ImageUpload/ImageUploadHome.vue')
|
||||
},
|
||||
{
|
||||
path: '/toolbox/image-compress',
|
||||
name: 'image-compress',
|
||||
component: () => import('./components/ToolBox/ImageCompress/ImageCompressHome.vue')
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user