功能升级

This commit is contained in:
严浪
2025-12-04 23:07:06 +08:00
parent 3c24e263f3
commit c10ab633fd
11 changed files with 3761 additions and 2377 deletions
+243 -1
View File
@@ -1 +1,243 @@
{}
{
"胜多负少": {
"nodes": [
{
"id": -1,
"type": "KSampler",
"pos": [
980,
197
],
"size": [
315,
262
],
"flags": {},
"order": 4,
"mode": 0,
"inputs": [
{
"label": "模型",
"localized_name": "模型",
"name": "model",
"type": "MODEL",
"link": null
},
{
"label": "正面条件",
"localized_name": "正面条件",
"name": "positive",
"type": "CONDITIONING",
"link": null
},
{
"label": "负面条件",
"localized_name": "负面条件",
"name": "negative",
"type": "CONDITIONING",
"link": null
},
{
"label": "Latent",
"localized_name": "Latent图像",
"name": "latent_image",
"type": "LATENT",
"link": null
},
{
"localized_name": "随机种",
"name": "seed",
"type": "INT",
"widget": {
"name": "seed"
},
"link": null
},
{
"localized_name": "步数",
"name": "steps",
"type": "INT",
"widget": {
"name": "steps"
},
"link": null
},
{
"localized_name": "CFG",
"name": "cfg",
"type": "FLOAT",
"widget": {
"name": "cfg"
},
"link": null
},
{
"localized_name": "采样器",
"name": "sampler_name",
"type": "COMBO",
"widget": {
"name": "sampler_name"
},
"link": null
},
{
"localized_name": "调度器",
"name": "scheduler",
"type": "COMBO",
"widget": {
"name": "scheduler"
},
"link": null
},
{
"localized_name": "降噪",
"name": "denoise",
"type": "FLOAT",
"widget": {
"name": "denoise"
},
"link": null
}
],
"outputs": [
{
"label": "Latent",
"localized_name": "Latent",
"name": "LATENT",
"type": "LATENT",
"slot_index": 0,
"links": []
}
],
"properties": {
"cnr_id": "comfy-core",
"ver": "0.3.56",
"Node name for S&R": "KSampler"
},
"widgets_values": [
156680208700286,
"randomize",
20,
8,
"euler",
"normal",
1
],
"index": 0
},
{
"id": -1,
"type": "VAEDecode",
"pos": [
1326,
199
],
"size": [
210,
46
],
"flags": {},
"order": 5,
"mode": 0,
"inputs": [
{
"label": "Latent",
"localized_name": "Latent",
"name": "samples",
"type": "LATENT",
"link": null
},
{
"label": "VAE",
"localized_name": "vae",
"name": "vae",
"type": "VAE",
"link": null
}
],
"outputs": [
{
"label": "图像",
"localized_name": "图像",
"name": "IMAGE",
"type": "IMAGE",
"slot_index": 0,
"links": []
}
],
"properties": {
"cnr_id": "comfy-core",
"ver": "0.3.56",
"Node name for S&R": "VAEDecode"
},
"index": 1
}
],
"links": [
[
null,
0,
0,
0,
4,
"MODEL"
],
[
null,
0,
0,
1,
6,
"CONDITIONING"
],
[
null,
0,
0,
2,
7,
"CONDITIONING"
],
[
null,
0,
0,
3,
5,
"LATENT"
],
[
0,
0,
1,
0,
19,
"LATENT"
],
[
null,
2,
1,
1,
4,
"VAE"
]
],
"external": [
[
1,
0,
"IMAGE"
]
],
"widgets_values": [
156680208700286,
"randomize",
20,
8,
"euler",
"normal",
1
]
}
}
+310 -285
View File
@@ -1,29 +1,29 @@
import { app } from "/scripts/app.js";
import { ComfyWidgets } from "/scripts/widgets.js";
import { GroupNodeConfig,GroupNodeHandler } from "/extensions/core/groupNode.js";
import { $el,ComfyDialog } from "../../../scripts/ui.js";
import { GroupNodeConfig, GroupNodeHandler } from "/extensions/core/groupNode.js";
import { $el, ComfyDialog } from "../../../scripts/ui.js";
import { api } from "../../../scripts/api.js";
// Displays input text on a node
const CONVERTED_TYPE = "converted-widget";
const type2paramType={
"STRING": "text",
"combo":"select",
"number":"number",
"toggle":"toggle",
"BOOLEAN":"switch",
"text":"text",
"string":"text",
"customtext":"customtext"
const type2paramType = {
"STRING": "text",
"combo": "select",
"number": "number",
"toggle": "toggle",
"BOOLEAN": "switch",
"text": "text",
"string": "text",
"customtext": "customtext"
}
var update_comfyui_button = null;
var switch_comfyui_button = null;
var fetch_updates_button = null;
var update_all_button = null;
let share_option = 'all';
let manager_instance =null;
let manager_instance = null;
$el("style", {
textContent: `
textContent: `
/*表单样式*/
.lam_app_form {
@@ -135,143 +135,155 @@ $el("style", {
}
`,
parent: document.body,
parent: document.body,
})
let initData={}
let initData = {}
class ManagerMenuDialog extends ComfyDialog {
createControlsLeft() {
let self = this;
let authorIds=initData?.base?.authorIds || []
let commands=initData?.commands||[]
let authorEls=[]
for(let id of authorIds){
authorEls.push($el('ol',{style:{marginRight:'10px'}},[
$el('span',{textContent:id}),
$el('button',{textContent:'删除',dataset:{authorId:id},$:(el) => {
el.onclick = () => {
initData.base.authorIds=authorIds.filter(id=>id!=el.dataset.authorId)
manager_instance.initElement();
let self = this;
let authorIds = initData?.base?.authorIds || []
let commands = initData?.commands || []
let authorEls = []
for (let id of authorIds) {
authorEls.push($el('ol', { style: { marginRight: '10px' } }, [
$el('span', { textContent: id }),
$el('button', {
textContent: '删除', dataset: { authorId: id }, $: (el) => {
el.onclick = () => {
initData.base.authorIds = authorIds.filter(id => id != el.dataset.authorId)
manager_instance.initElement();
}
}
}}),
}),
]))
}
let commonEls=[]
for(let command in commands){
commonEls.push($el('ol',{style:{marginRight:'10px'}},[
$el('span',{textContent:command}),
$el('button',{textContent:'删除',dataset:{command:command},$:(el) => {
el.onclick = () => {
delete initData.commands[command]
manager_instance.initElement();
let commonEls = []
for (let command in commands) {
commonEls.push($el('ol', { style: { marginRight: '10px' } }, [
$el('span', { textContent: command }),
$el('button', {
textContent: '删除', dataset: { command: command }, $: (el) => {
el.onclick = () => {
delete initData.commands[command]
manager_instance.initElement();
}
}
}}),
}),
]))
}
let from =$el("from", [
$el("div.txtb", [
$el("label", { textContent: "应用标题:" }),
$el("input", {
name: "appTitle",
value: initData?.base?.appTitle,
onchange: (e) => {
initData.base.appTitle = e.target.value;
},
}),
]),
$el("div.txtb", [
$el("label", { textContent: "应用logo:" }),
$el("input", {
name: "appLogo",
value: initData?.base?.appLogo,
onchange: (e) => {
initData.base.appLogo = e.target.value;
},
}),
]),
$el("div.txtb", [
$el("label", { textContent: "免费次数(0为无限):" }),
$el("input", {
name: "freeSize",
value: initData?.base?.freeSize,
onchange: (e) => {
let value=e.target.value
if(isNaN(value)){
value=0
}else{
value=parseInt(value)
}
initData.base.freeSize = value;
},
}),
]),
$el("div.txtb", [
$el("label", { textContent: "授权编号:" },[
$el('button',{textContent:'添加',$:(el) => {
let from = $el("from", [
$el("div.txtb", [
$el("label", { textContent: "应用标题:" }),
$el("input", {
name: "appTitle",
value: initData?.base?.appTitle,
onchange: (e) => {
initData.base.appTitle = e.target.value;
},
}),
]),
$el("div.txtb", [
$el("label", { textContent: "应用logo:" }),
$el("input", {
name: "appLogo",
value: initData?.base?.appLogo,
onchange: (e) => {
initData.base.appLogo = e.target.value;
},
}),
]),
$el("div.txtb", [
$el("label", { textContent: "免费次数(0为无限):" }),
$el("input", {
name: "freeSize",
value: initData?.base?.freeSize,
onchange: (e) => {
let value = e.target.value
if (isNaN(value)) {
value = 0
} else {
value = parseInt(value)
}
initData.base.freeSize = value;
},
}),
]),
$el("div.txtb", [
$el("label", { textContent: "授权编号:" }, [
$el('button', {
textContent: '添加', $: (el) => {
el.onclick = () => {
//随机字符串
let authorId = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
initData.base.authorIds.push(authorId)
manager_instance.initElement();
}
}})
]),
$el("ul.scroll-container",authorEls)
}
})
]),
$el("div.txtb", [
$el("label", { textContent: "应用管理:" }),
$el("ul.scroll-container",commonEls)
]),
])
return [$el('div.lam_app_form',[from,
$el('span.btn',{textContent:'提交',$:(el) => {
el.onclick = () => {
updateInitData()
$el("ul.scroll-container", authorEls)
]),
$el("div.txtb", [
$el("label", { textContent: "应用管理:" }),
$el("ul.scroll-container", commonEls)
]),
])
return [$el('div.lam_app_form', [from,
$el('span.btn', {
textContent: '提交', $: (el) => {
el.onclick = () => {
updateInitData()
}
}
}}),
$el('span.btn',{textContent:'应用预览',onclick:()=>{
openAppPage('default')
}}),
$el('span.btn',{textContent:'画板预览',onclick:()=>{
openAppPage()
}})
}),
$el('span.btn', {
textContent: '应用预览', onclick: () => {
openAppPage('default')
}
}),
$el('span.btn', {
textContent: '画板预览', onclick: () => {
openAppPage()
}
})
])];
}
constructor() {
super();
constructor() {
super();
const close_button = $el("button", { id: "cm-close-button", type: "button", textContent: "Close", onclick: () => this.close() });
const content =
$el("div.comfy-modal-content",
[
$el("tr.cm-title", {}, [
$el("font", {size:6, color:"white"}, [`应用管理`])]
),
$el("br", {}, []),
$el("div.cm-menu-container",
[
...this.createControlsLeft()
]),
const content =
$el("div.comfy-modal-content",
[
$el("tr.cm-title", {}, [
$el("font", { size: 6, color: "white" }, [`应用管理`])]
),
$el("br", {}, []),
$el("div.cm-menu-container",
[
...this.createControlsLeft()
]),
$el("br", {}, []),
close_button,
]
);
$el("br", {}, []),
close_button,
]
);
content.style.width = '100%';
content.style.height = '80%';
this.element = $el("div.comfy-modal", { id:'cm-manager-dialog', parent: document.body,style:{maxWidth:'700px',height:'800px'} }, [ content ]);
}
initElement(){
this.element.children[0].children[2].innerHTML=''
this.element.children[0].children[2].appendChild( ...this.createControlsLeft())
content.style.width = '100%';
content.style.height = '80%';
this.element = $el("div.comfy-modal", { id: 'cm-manager-dialog', parent: document.body, style: { maxWidth: '700px', height: '800px' } }, [content]);
}
show() {
initElement() {
this.element.children[0].children[2].innerHTML = ''
this.element.children[0].children[2].appendChild(...this.createControlsLeft())
}
show() {
this.initElement()
this.element.style.display = "block";
}
this.element.style.display = "block";
}
}
async function openAppPage(type) {
try {
@@ -279,7 +291,7 @@ async function openAppPage(type) {
if (resp.status === 200) {
let data = await resp.json();
//打开新标签页面
window.open('/wechatauth/'+(type=='default'?'app':'index')+'?openId='+data.data);
window.open('/wechatauth/' + (type == 'default' ? 'app' : 'index') + '?openId=' + data.data);
return true;
}
throw new Error(resp.data.msg);
@@ -288,14 +300,14 @@ async function openAppPage(type) {
}
}
async function getAppInit(){
async function getAppInit() {
try {
const resp = await api.fetchApi(`/lam/getInitData`);
if (resp.status === 200) {
let data = await resp.json();
if(data.success){
if (data.success) {
console.log(data)
initData=data.data
initData = data.data
manager_instance.show();
}
return true;
@@ -308,11 +320,12 @@ async function getAppInit(){
async function updateInitData() {
try {
let data={appTitle:initData.base.appTitle,
appLogo:initData.base.appLogo,
freeSize:initData.base.freeSize,
authorIds:initData.base.authorIds,
commands:initData.commands
let data = {
appTitle: initData.base.appTitle,
appLogo: initData.base.appLogo,
freeSize: initData.base.freeSize,
authorIds: initData.base.authorIds,
commands: initData.commands
}
const response = await api.fetchApi("/lam/updateAppData", {
method: "POST",
@@ -333,8 +346,8 @@ async function updateInitData() {
async function setAppParams(data) {
try {
let workflow= await app.graphToPrompt();
data['workflow']=workflow['output']
let workflow = await app.graphToPrompt();
data['workflow'] = workflow['output']
const response = await api.fetchApi("/lam/setAppParams", {
method: "POST",
headers: {
@@ -351,61 +364,67 @@ async function setAppParams(data) {
console.error(error);
}
}
function add_param(w,appNode){
let typeEl=null;
if(w.type=='number'||w.type=='slider'){
typeEl=$el('td',[
$el('select',{value:w.type,$:(el) =>{el.onchange=()=>{
appNode.properties['paramList'].find(obj => obj.id === el.parentNode.parentNode.dataset.id).type=el.value
}}},[
$el('option',{selected:w.type=='number',textContent:'数值输入',value:'number'}),
$el('option',{selected:w.type=='slider',textContent:'数值滑条',value:'slider'}),
function add_param(w, appNode) {
let typeEl = null;
if (w.type == 'number' || w.type == 'slider') {
typeEl = $el('td', [
$el('select', {
value: w.type, $: (el) => {
el.onchange = () => {
appNode.properties['paramList'].find(obj => obj.id === el.parentNode.parentNode.dataset.id).type = el.value
}
}
}, [
$el('option', { selected: w.type == 'number', textContent: '数值输入', value: 'number' }),
$el('option', { selected: w.type == 'slider', textContent: '数值滑条', value: 'slider' }),
])
])
}else{
typeEl=$el('td',{textContent:w.type})
} else {
typeEl = $el('td', { textContent: w.type })
}
let maskEl=null;
let maskd=appNode.properties['paramList'].find(obj => obj.id==w?.maskKey)
if(w.type=='image'){
maskEl=$el('td',[
$el('span',{textContent:maskd?'已关联:'+maskd.zhName:'关联遮罩',$:(el) => {
el.onclick = () => {
let thIndex=appNode.properties['paramList'].findIndex(obj => obj.id==el.parentNode.parentNode.dataset.id)
let imgs=appNode.properties['paramList'].filter(obj => obj.type === 'image'&&obj.id!=el.parentNode.parentNode.dataset.id)
let maskKey=appNode.properties['paramList'][thIndex]?.maskKey
if(imgs.length>0){
if(!maskKey){
appNode.properties['paramList'][thIndex]['maskKey']=imgs[0].id
el.innerHTML='已关联:'+imgs[0].zhName
}else{
if(imgs.length==1){
appNode.properties['paramList'][thIndex]['maskKey']=null
el.innerHTML='关联遮罩'
}else{
let imgIndex=imgs.findIndex(obj => obj.id === maskKey)
if(imgIndex==-1||imgIndex+1>=imgs.length){
appNode.properties['paramList'][thIndex]['maskKey']=null
el.innerHTML='关联遮罩'
}else{
appNode.properties['paramList'][thIndex]['maskKey']=imgs[imgIndex+1].id
el.innerHTML='已关联:'+imgs[imgIndex+1].zhName
let maskEl = null;
let maskd = appNode.properties['paramList'].find(obj => obj.id == w?.maskKey)
if (w.type == 'image') {
maskEl = $el('td', [
$el('span', {
textContent: maskd ? '已关联:' + maskd.zhName : '关联遮罩', $: (el) => {
el.onclick = () => {
let thIndex = appNode.properties['paramList'].findIndex(obj => obj.id == el.parentNode.parentNode.dataset.id)
let imgs = appNode.properties['paramList'].filter(obj => obj.type === 'image' && obj.id != el.parentNode.parentNode.dataset.id)
let maskKey = appNode.properties['paramList'][thIndex]?.maskKey
if (imgs.length > 0) {
if (!maskKey) {
appNode.properties['paramList'][thIndex]['maskKey'] = imgs[0].id
el.innerHTML = '已关联:' + imgs[0].zhName
} else {
if (imgs.length == 1) {
appNode.properties['paramList'][thIndex]['maskKey'] = null
el.innerHTML = '关联遮罩'
} else {
let imgIndex = imgs.findIndex(obj => obj.id === maskKey)
if (imgIndex == -1 || imgIndex + 1 >= imgs.length) {
appNode.properties['paramList'][thIndex]['maskKey'] = null
el.innerHTML = '关联遮罩'
} else {
appNode.properties['paramList'][thIndex]['maskKey'] = imgs[imgIndex + 1].id
el.innerHTML = '已关联:' + imgs[imgIndex + 1].zhName
}
}
}
} else {
el.innerHTML = '关联遮罩'
alert('请先添加遮罩图片参数')
}
}else{
el.innerHTML='关联遮罩'
alert('请先添加遮罩图片参数')
}
}}
}
})
])
}else{
maskEl=$el('td')
} else {
maskEl = $el('td')
}
return $el('tr',{
return $el('tr', {
dataset: {
id: w.id,
name: w.name,
@@ -413,68 +432,76 @@ function add_param(w,appNode){
type: w.type,
value: w.value
}
},[
$el('td',{textContent:w.name}),
$el('td',{},[
$el('input',{name:w.name,value:w.zhName,$:(el) =>{el.onchange=()=>{
appNode.properties['paramList'].find(obj => obj.id === el.parentNode.parentNode.dataset.id).zhName=el.value
}}})
}, [
$el('td', { textContent: w.name }),
$el('td', {}, [
$el('input', {
name: w.name, value: w.zhName, $: (el) => {
el.onchange = () => {
appNode.properties['paramList'].find(obj => obj.id === el.parentNode.parentNode.dataset.id).zhName = el.value
}
}
})
]),
typeEl,
maskEl,
$el('td',{},[$el('button',{textContent:'删除',$:(el) => {
el.onclick = () => {
appNode.properties['paramList'].splice(appNode.properties['paramList'].findIndex(obj => obj.id === el.parentNode.parentNode.dataset.id),1)
el.parentNode.parentNode.parentNode.removeChild(el.parentNode.parentNode)
}}
$el('td', {}, [$el('button', {
textContent: '删除', $: (el) => {
el.onclick = () => {
appNode.properties['paramList'].splice(appNode.properties['paramList'].findIndex(obj => obj.id === el.parentNode.parentNode.dataset.id), 1)
el.parentNode.parentNode.parentNode.removeChild(el.parentNode.parentNode)
}
}
}
)]),
])
}
app.registerExtension({
name: "AppParams",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
var names=["AppParams"]
if (names.indexOf(nodeData.name)>=0) {
var names = ["AppParams"]
if (names.indexOf(nodeData.name) >= 0) {
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function() {
nodeType.prototype.onNodeCreated = function () {
const r = onNodeCreated?.apply(this, arguments);
this.setProperty("paramList", [])
const list = $el("table.lam_app_params_list",[
$el('tr',[
$el('td',{textContent:'参数名'}),
$el('td',{textContent:'中文名'}),
$el('td',{textContent:'组件类型'}),
$el('td',{textContent:'绘制遮罩'}),
$el('td',{textContent:'操作'}),
const list = $el("table.lam_app_params_list", [
$el('tr', [
$el('td', { textContent: '参数名' }),
$el('td', { textContent: '中文名' }),
$el('td', { textContent: '组件类型' }),
$el('td', { textContent: '绘制遮罩' }),
$el('td', { textContent: '操作' }),
])
]);
let params=this.addDOMWidget('styles',"list",$el('div.lam_app_param_div',[$el('button',{
textContent:'应用预览',
style:{},
onclick:()=>{
let params = this.addDOMWidget('styles', "list", $el('div.lam_app_param_div', [$el('button', {
textContent: '应用预览',
style: {},
onclick: () => {
let appType = this.widgets[this.widgets.findIndex(obj => obj.name === 'appType')];
openAppPage(appType.value)
}}),$el('button',{
textContent:'参数应用',
style:{},
onclick:()=>{
let paramList=this.properties['paramList']
let appName = this.widgets[this.widgets.findIndex(obj => obj.name === 'appName')];
let appType = this.widgets[this.widgets.findIndex(obj => obj.name === 'appType')];
let appDesc = this.widgets[this.widgets.findIndex(obj => obj.name === 'appDesc')];
if(!appName.value){
alert('请输入应用名称');
return ;
}
if(paramList.length<=0){
alert('请至少选择一个参数');
return ;
}
let data={appName:appName.value,appType:appType.value,appDesc:appDesc.value,paramList:paramList}
setAppParams(data)
}}),list]));
}
}), $el('button', {
textContent: '参数应用',
style: {},
onclick: () => {
let paramList = this.properties['paramList']
let appName = this.widgets[this.widgets.findIndex(obj => obj.name === 'appName')];
let appType = this.widgets[this.widgets.findIndex(obj => obj.name === 'appType')];
let appDesc = this.widgets[this.widgets.findIndex(obj => obj.name === 'appDesc')];
if (!appName.value) {
alert('请输入应用名称');
return;
}
if (paramList.length <= 0) {
alert('请至少选择一个参数');
return;
}
let data = { appName: appName.value, appType: appType.value, appDesc: appDesc.value, paramList: paramList }
setAppParams(data)
}
}), list]));
this.setSize([500, 600]);
return r;
};
@@ -483,10 +510,10 @@ app.registerExtension({
loadedGraphNode(node, _) {
if (node.type === "AppParams") {
node.properties['paramList'].forEach(el => {
node.widgets[3].element.children[2].appendChild(add_param(el,node))
node.widgets[3].element.children[2].appendChild(add_param(el, node))
});
}
},
},
});
async function addConvertToGroupOptions() {
@@ -494,9 +521,9 @@ async function addConvertToGroupOptions() {
const getNodeMenuOptions = LGraphCanvas.prototype.getNodeMenuOptions;
LGraphCanvas.prototype.getNodeMenuOptions = function (node) {
const options = getNodeMenuOptions.apply(this, arguments);
if (!GroupNodeHandler.isGroupNode(node)&&node.type!='AppParams'&&node.widgets){
if (!GroupNodeHandler.isGroupNode(node) && node.type != 'AppParams' && node.widgets) {
let toInput = [];
let wNames=node.inputs.filter(obj => obj.widget !== undefined && obj.link === null).map(obj => obj.name);
let wNames = node.inputs.filter(obj => obj.widget !== undefined && obj.link === null).map(obj => obj.name);
for (const w of node.widgets) {
if (w.options?.forceInput || !wNames.includes(w.name)) {
continue;
@@ -506,72 +533,72 @@ async function addConvertToGroupOptions() {
content: `添加 ${w.label} 到输入参数`,
callback: async () => {
console.log(w)
let nodes=graph.computeExecutionOrder(false)
let appNode=nodes[nodes.findIndex(obj => obj.type === 'AppParams')]
if(!appNode){
let nodes = graph.computeExecutionOrder(false)
let appNode = nodes[nodes.findIndex(obj => obj.type === 'AppParams')]
if (!appNode) {
alert('请先添加AppParams节点');
return;
}
let appTypeEl = appNode.widgets[appNode.widgets.findIndex(obj => obj.name === 'appType')];
if(!w.type in type2paramType){
if (!w.type in type2paramType) {
return;
}
let paramData={id:'n'+node.id+'_'+w.name,keys:[''+node.id,'inputs',w.name],name:w.name,zhName:w.label,type:w.type,default:w.value,isRequired:false}
if(w.type=='number'){
paramData['min']=w.options.min
paramData['max']=w.options.max
paramData['step']=w.options.round
}else if(w.type=='toggle'||w.type=='boolean'||w.type=='BOOLEAN'){
paramData['type']='switch'
paramData['onVal']=true
paramData['offVal']=false
}else if(w.type=='STRING'){
paramData['type']='text'
}else if(w.type=='combo'){
paramData['type']='select'
let values=w.options.values
paramData['options']={}
for(let i=0;i<values.length;i++){
paramData['options'][values[i]]=values[i]
let paramData = { id: 'n' + node.id + '_' + w.name, keys: ['' + node.id, 'inputs', w.name], name: w.name, zhName: w.label, type: w.type, default: w.value, isRequired: false }
if (w.type == 'number') {
paramData['min'] = w.options.min
paramData['max'] = w.options.max
paramData['step'] = w.options.round
} else if (w.type == 'toggle' || w.type == 'boolean' || w.type == 'BOOLEAN') {
paramData['type'] = 'switch'
paramData['onVal'] = true
paramData['offVal'] = false
} else if (w.type == 'STRING') {
paramData['type'] = 'text'
} else if (w.type == 'combo') {
paramData['type'] = 'select'
let values = w.options.values
paramData['options'] = {}
for (let i = 0; i < values.length; i++) {
paramData['options'][values[i]] = values[i]
}
}
if(node.type=="LamLoadPathImage"&&w.name=='image_path'){
paramData['type']='image'
paramData['default']=''
}else if(node.type=="LamLoadImageBase64"&&w.name=='image'){
paramData['type']='base64img'
paramData['isRequired']=true
}else if(w.name=='seed'){
paramData['type']='seed'
if(appTypeEl.value=='paint-board'){
paramData['default']=-1
if (node.type == "LamLoadPathImage" && w.name == 'image_path') {
paramData['type'] = 'image'
paramData['default'] = ''
} else if (node.type == "LamLoadImageBase64" && w.name == 'image') {
paramData['type'] = 'base64img'
paramData['isRequired'] = true
} else if (w.name == 'seed') {
paramData['type'] = 'seed'
if (appTypeEl.value == 'paint-board') {
paramData['default'] = -1
}
}else if(w.name=='batch_size'){
paramData['type']='number'
}else if(w.name=='denoise'){
paramData['type']='slider'
if(appTypeEl.value=='paint-board'){
paramData['min']=0
paramData['max']=100
paramData['step']=1
paramData['original']=1
} else if (w.name == 'batch_size') {
paramData['type'] = 'number'
} else if (w.name == 'denoise') {
paramData['type'] = 'slider'
if (appTypeEl.value == 'paint-board') {
paramData['min'] = 0
paramData['max'] = 100
paramData['step'] = 1
paramData['original'] = 1
}
paramData['default']=parseFloat(paramData['default'].toFixed(2))
}
paramData['default'] = parseFloat(paramData['default'].toFixed(2))
}
let index=appNode.properties['paramList'].findIndex(obj => obj.id === paramData.id)
if(index>=0){
let index = appNode.properties['paramList'].findIndex(obj => obj.id === paramData.id)
if (index >= 0) {
return;
}
appNode.properties['paramList'].push(paramData)
let tr=add_param(paramData,appNode)
let tr = add_param(paramData, appNode)
appNode.widgets[3].element.children[2].appendChild(tr)
}
});
}
}
const index = options.findIndex((o) => o?.content === "Outputs") + 1 || options.length - 1;
toInput.length>0&&options.splice(index + 1, null, {
toInput.length > 0 && options.splice(index + 1, null, {
content: `设置应用参数`,
submenu: {
options: toInput
@@ -581,12 +608,12 @@ async function addConvertToGroupOptions() {
return options;
};
let cmGroup = new (await import("../../scripts/ui/components/buttonGroup.js")).ComfyButtonGroup(
new(await import("../../scripts/ui/components/button.js")).ComfyButton({
let cmGroup = new (await import("../../scripts/ui/components/buttonGroup.js")).ComfyButtonGroup(
new (await import("../../scripts/ui/components/button.js")).ComfyButton({
icon: "puzzle",
action: () => {
if(!manager_instance){
manager_instance= new ManagerMenuDialog();
if (!manager_instance) {
manager_instance = new ManagerMenuDialog();
}
getAppInit()
},
@@ -596,7 +623,7 @@ async function addConvertToGroupOptions() {
}).element
);
app.menu?.settingsGroup.element.before(cmGroup.element);
}
const id = "Lam.AppParams";
@@ -612,12 +639,10 @@ const ext = {
// // Store this so we can mutate it later with group nodes
// globalDefs = defs;
// },
nodeCreated(node) {
if (!GroupNodeHandler.isGroupNode(node)) {
}
},
// nodeCreated(node) {
// if (!GroupNodeHandler.isGroupNode(node)) {
// }
// },
};
app.registerExtension(ext);
+67
View File
@@ -0,0 +1,67 @@
import { app } from "/scripts/app.js";
function adaptWidgetsToConnection(node) {
if (!node.outputs || node.outputs.length === 0) {
return
}
console.log("到这里==================")
}
function setupParameterNode(nodeType) {
const onAdded = nodeType.prototype.onAdded
nodeType.prototype.onAdded = function () {
onAdded?.apply(this, arguments)
adaptWidgetsToConnection(this)
}
const onAfterGraphConfigured = nodeType.prototype.onAfterGraphConfigured
nodeType.prototype.onAfterGraphConfigured = function () {
onAfterGraphConfigured?.apply(this, arguments)
adaptWidgetsToConnection(this)
}
const onConnectOutput = nodeType.prototype.onConnectOutput
nodeType.prototype.onConnectOutput = function (slot, type, input, target_node, target_slot) {
if (!input.widget) {
return false
} else if (onConnectOutput) {
result = onConnectOutput.apply(this, arguments)
return result
}
return true
}
const onConnectionsChange = nodeType.prototype.onConnectionsChange
nodeType.prototype.onConnectionsChange = function (_, index, connected) {
if (!app.configuringGraph) {
adaptWidgetsToConnection(this)
}
onConnectionsChange?.apply(this, arguments)
}
}
const id = "Lam.Switch";
const ext = {
name: id,
setup() {
//addConvertToGroupOptions();
//添加按钮可以
},
async beforeConfigureGraph(graphData, missingNodeTypes) {
//加载配置
},
beforeRegisterNodeDef(nodeType /*typeof LGraphNode*/, nodeData /*ComfyObjectInfo*/, app) {
if (nodeData.name === "LamSwitchStart") {
setupParameterNode(nodeType)
}
},
addCustomNodeDefs(defs) {
// 将其存储起来,以便稍后使用组节点对其进行修改
},
nodeCreated(node) {
//节点创建
},
};
app.registerExtension(ext);
+212 -946
View File
File diff suppressed because it is too large Load Diff
+581 -122
View File
@@ -1,130 +1,589 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>简单画板</title>
<!--<link rel="stylesheet" href="https://unpkg.com/element-plus@2.4.4/dist/index.css"/>-->
<link rel="stylesheet" href="static/assets/index.css" />
<!--<script src="https://unpkg.com/vue@3.3.12/dist/vue.global.js"></script>-->
<script src="static/assets/vue.global.js"></script>
<!--<script src="https://unpkg.com/vue-i18n@9.8.0/dist/vue-i18n.global.js"></script>-->
<script src="static/assets/vue-i18n.global.js"></script>
<!--<script src="https://unpkg.com/element-plus@2.4.4/dist/index.full.min.js"></script>-->
<script src="static/assets/index.full.min.js"></script>
<!--<script src="https://unpkg.com/@element-plus/icons-vue@2.3.1/dist/index.iife.min.js"></script>-->
<script src="static/assets/index.iife.min.js"></script>
<!--<script src="https://unpkg.com/clipboard@2.0.11/dist/clipboard.min.js"></script>-->
<script src="static/assets/clipboard.min.js"></script>
<!--<script src="https://unpkg.com/axios@1.6.2/dist/axios.min.js"></script>-->
<script src="static/assets/axios.min.js"></script>
<script src="static/assets/fabric.min.js"></script>
</head>
<body>
<div id="app" class="image-comparator">
<section class="aigc-preview" >
<el-carousel class="aigc-preview__img" :interval="5000" arrow="always" :autoplay="false">
<el-carousel-item v-for="item in 4" :key="item">
<el-image class="aigc-preview__img" src="../view?filename=ComfyUI_03170_.png&type=input&subfolder=" fit="contain" ></el-image>
</el-carousel-item>
</el-carousel>
</section>
<!-- 核心标签代码 -->
<div class="container" @mouseenter="startDrag" @mousemove="onDrag" @mouseup="endDrag" @mouseleave="endDrag">
<div class="image-wrapper"
:style="{ clipPath: `inset(0 ${100 - (dividerPosition / containerWidth * 100)}% 0 0)` }">
<el-image src="https://fuss10.elemecdn.com/9/bb/e27858e973f5d7d3904835f46abbdjpeg.jpeg" class="image" fit="contain"/>
</div>
<div class="divider" :style="{ left: `${dividerPosition}px` }"></div>
<div class="image-wrapper" :style="{ clipPath: `inset(0 0 0 ${(dividerPosition / containerWidth * 100)}%)` }">
<el-image src="https://fuss10.elemecdn.com/d/e6/c4d93a3805b3ce3f323f7974e6f78jpeg.jpeg" class="image" fit="contain"/>
</div>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>360°全景图播放器</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #1a2a6c, #b21f1f, #fdbb2d);
color: white;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
padding: 20px;
overflow-x: hidden;
}
.container {
max-width: 1200px;
width: 100%;
text-align: center;
}
header {
margin-bottom: 30px;
}
h1 {
font-size: 2.5rem;
margin-bottom: 10px;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
}
.subtitle {
font-size: 1.2rem;
opacity: 0.9;
margin-bottom: 20px;
}
.player-container {
position: relative;
width: 100%;
height: 70vh;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.4);
margin-bottom: 30px;
}
#panorama {
width: 100%;
height: 100%;
background-color: #111;
}
.loading {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 1.2rem;
color: white;
z-index: 10;
background: rgba(0, 0, 0, 0.7);
padding: 15px 30px;
border-radius: 8px;
}
.error-message {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 1.2rem;
color: #ff6b6b;
z-index: 10;
background: rgba(0, 0, 0, 0.7);
padding: 15px 30px;
border-radius: 8px;
display: none;
}
.controls {
display: flex;
justify-content: center;
gap: 15px;
margin-bottom: 30px;
flex-wrap: wrap;
}
.control-btn {
background: rgba(255, 255, 255, 0.2);
border: none;
color: white;
padding: 12px 24px;
border-radius: 50px;
font-size: 1rem;
cursor: pointer;
transition: all 0.3s ease;
backdrop-filter: blur(5px);
display: flex;
align-items: center;
gap: 8px;
}
.control-btn:hover {
background: rgba(255, 255, 255, 0.3);
transform: translateY(-2px);
}
.control-btn:active {
transform: translateY(1px);
}
.control-btn.active {
background: rgba(253, 187, 45, 0.7);
}
.instructions {
background: rgba(255, 255, 255, 0.1);
padding: 20px;
border-radius: 12px;
max-width: 800px;
margin: 0 auto 30px;
backdrop-filter: blur(5px);
}
.instructions h2 {
margin-bottom: 15px;
font-size: 1.5rem;
}
.instructions ul {
text-align: left;
list-style-type: none;
padding: 0;
}
.instructions li {
margin-bottom: 10px;
padding-left: 25px;
position: relative;
}
.instructions li:before {
content: "•";
color: #fdbb2d;
font-size: 1.5rem;
position: absolute;
left: 0;
top: -5px;
}
.image-selector {
display: flex;
gap: 15px;
justify-content: center;
flex-wrap: wrap;
margin-bottom: 30px;
}
.image-option {
width: 120px;
height: 80px;
border-radius: 8px;
overflow: hidden;
cursor: pointer;
opacity: 0.7;
transition: all 0.3s ease;
border: 2px solid transparent;
position: relative;
}
.image-option:hover {
opacity: 0.9;
transform: scale(1.05);
}
.image-option.active {
opacity: 1;
border-color: #fdbb2d;
transform: scale(1.05);
}
.image-option img {
width: 100%;
height: 100%;
object-fit: cover;
}
.image-option .placeholder {
width: 100%;
height: 100%;
background: linear-gradient(45deg, #666, #999);
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 0.8rem;
}
footer {
margin-top: 20px;
font-size: 0.9rem;
opacity: 0.8;
}
@media (max-width: 768px) {
h1 {
font-size: 2rem;
}
.player-container {
height: 50vh;
}
.control-btn {
padding: 10px 20px;
font-size: 0.9rem;
}
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>360°全景图播放器</h1>
<p class="subtitle">沉浸式体验全景视觉盛宴</p>
</header>
<div class="player-container">
<div id="panorama"></div>
<div class="loading" id="loading">加载全景图...</div>
<div class="error-message" id="errorMessage">图片加载失败,请尝试其他全景图</div>
</div>
<div class="controls">
<button class="control-btn" id="autoRotate">
<span>自动旋转</span>
</button>
<button class="control-btn" id="fullscreen">
<span>全屏模式</span>
</button>
<button class="control-btn" id="resetView">
<span>重置视角</span>
</button>
</div>
<div class="image-selector">
<div class="image-option active" data-src="/api/view?filename=ComfyUI_00004_.png&subfolder=&type=output&rand=0.4623513290192227">
<div class="placeholder">示例全景图1</div>
</div>
<div class="image-option" data-src="https://threejs.org/examples/textures/25258776a2_o.jpg">
<div class="placeholder">示例全景图2</div>
</div>
<div class="image-option" data-src="https://threejs.org/examples/textures/3583869462_03c4660a5d_o.jpg">
<div class="placeholder">示例全景图3</div>
</div>
<div class="image-option" data-src="https://threejs.org/examples/textures/landscape.png">
<div class="placeholder">示例全景图4</div>
</div>
</div>
<div class="instructions">
<h2>使用说明</h2>
<ul>
<li>使用鼠标拖拽来旋转视角,探索全景环境</li>
<li>使用鼠标滚轮来放大或缩小视图</li>
<li>点击"自动旋转"按钮启用/禁用自动旋转</li>
<li>点击"全屏模式"按钮进入全屏沉浸体验</li>
<li>点击"重置视角"按钮回到初始视角</li>
<li>从下方选择不同的全景图进行切换</li>
<li><strong>注意:</strong>示例图片来自threejs.org,可能需要科学上网才能访问</li>
</ul>
</div>
<div class="instructions">
<h2>本地图片使用说明</h2>
<ul>
<li>如需使用本地图片,请将图片文件放在项目目录中</li>
<li>修改 data-src 属性为本地图片路径,例如:data-src="images/panorama1.jpg"</li>
<li>确保图片是等距柱状投影格式(360°全景图)</li>
<li>推荐图片尺寸:4096×2048 或 8192×4096</li>
</ul>
</div>
<footer>
<p>© 2023 全景图播放器 | 使用 Three.js 构建</p>
</footer>
</div>
</body>
<script>
const app = Vue.createApp({
data() {
return {
containerWidth: 500,
dividerPosition: 250,
dragging: false,
containerRect: null,
};
},
methods: {
updateContainerWidth() {
this.containerWidth = document.querySelector(".container").clientWidth;
},
startDrag(event) {
this.dragging = true;
this.containerRect = document.querySelector(".container").getBoundingClientRect();
},
onDrag(event) {
if (this.dragging && this.containerRect) {
let newPosition = event.clientX - this.containerRect.left;
if (newPosition < 0) newPosition = 0;
if (newPosition > this.containerRect.width)
newPosition = this.containerRect.width;
this.dividerPosition = newPosition;
}
},
endDrag() {
this.dragging = false;
this.containerRect = null;
},
},
mounted() {
this.updateContainerWidth();
window.addEventListener("resize", this.updateContainerWidth);
},
beforeDestroy() {
window.removeEventListener("resize", this.updateContainerWidth);
},
});
app.use(ElementPlus);
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component);
}
app.mount("#app");
</script>
<style scoped lang="less">
.image-comparator {
width: 90%;
/*max-width: 600px;*/
margin: 0 auto;
<!-- 引入Three.js库 -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
.container {
position: relative;
width: 100%;
height: 600px;
overflow: hidden;
}
<script>
// 全景图播放器类
class PanoramaViewer {
constructor(containerId) {
this.container = document.getElementById(containerId);
this.scene = null;
this.camera = null;
this.renderer = null;
this.sphere = null;
this.textureLoader = new THREE.TextureLoader();
.image-wrapper {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
overflow: hidden;
}
// 控制参数
this.isUserInteracting = false;
this.onPointerDownMouseX = 0;
this.onPointerDownMouseY = 0;
this.onPointerDownLon = 0;
this.onPointerDownLat = 0;
this.lon = 0;
this.lat = 0;
this.phi = 0;
this.theta = 0;
this.target = new THREE.Vector3();
this.autoRotate = false;
this.autoRotateSpeed = 0.3;
.image {
width: 100%;
height: 100%;
object-fit: cover;
}
this.init();
}
.divider {
position: absolute;
top: 0;
bottom: 0;
width: 2px;
background-color: black;
cursor: ew-resize;
z-index: 10;
}
}
</style>
</html>
init() {
// 创建场景
this.scene = new THREE.Scene();
// 创建相机
this.camera = new THREE.PerspectiveCamera(75, this.container.clientWidth / this.container.clientHeight, 0.1, 1000);
// 创建渲染器
this.renderer = new THREE.WebGLRenderer({ antialias: true });
this.renderer.setSize(this.container.clientWidth, this.container.clientHeight);
this.renderer.setPixelRatio(window.devicePixelRatio);
this.container.appendChild(this.renderer.domElement);
// 创建球体几何
const geometry = new THREE.SphereGeometry(500, 60, 40);
geometry.scale(-1, 1, 1); // 反转球体,使纹理在内部
// 创建默认材质(等待图片加载)
const material = new THREE.MeshBasicMaterial({
color: 0x222222,
side: THREE.DoubleSide
});
this.sphere = new THREE.Mesh(geometry, material);
this.scene.add(this.sphere);
// 设置相机位置
this.camera.position.set(0, 0, 0.1);
// 添加事件监听
this.addEventListeners();
// 开始动画循环
this.animate();
// 窗口大小变化处理
window.addEventListener('resize', () => this.onWindowResize());
}
loadPanorama(imageUrl) {
document.getElementById('loading').style.display = 'block';
document.getElementById('errorMessage').style.display = 'none';
// 设置超时时间
const timeout = setTimeout(() => {
this.showError('图片加载超时,请检查网络连接或尝试其他图片');
}, 10000);
this.textureLoader.load(
imageUrl,
(texture) => {
clearTimeout(timeout);
texture.minFilter = THREE.LinearFilter;
// 创建新材质
const material = new THREE.MeshBasicMaterial({
map: texture,
side: THREE.DoubleSide
});
this.sphere.material = material;
document.getElementById('loading').style.display = 'none';
},
(progress) => {
// 加载进度
const percent = Math.round((progress.loaded / progress.total) * 100);
document.getElementById('loading').textContent = `加载中... ${percent}%`;
},
(error) => {
clearTimeout(timeout);
console.error('加载全景图失败:', error);
this.showError('图片加载失败,请尝试其他全景图');
}
);
}
showError(message) {
document.getElementById('loading').style.display = 'none';
document.getElementById('errorMessage').textContent = message;
document.getElementById('errorMessage').style.display = 'block';
}
addEventListeners() {
const domElement = this.renderer.domElement;
// 鼠标按下事件
domElement.addEventListener('mousedown', (event) => {
event.preventDefault();
this.isUserInteracting = true;
this.onPointerDownMouseX = event.clientX;
this.onPointerDownMouseY = event.clientY;
this.onPointerDownLon = this.lon;
this.onPointerDownLat = this.lat;
});
// 鼠标移动事件
domElement.addEventListener('mousemove', (event) => {
if (this.isUserInteracting) {
this.lon = (this.onPointerDownMouseX - event.clientX) * 0.1 + this.onPointerDownLon;
this.lat = (event.clientY - this.onPointerDownMouseY) * 0.1 + this.onPointerDownLat;
}
});
// 鼠标释放事件
domElement.addEventListener('mouseup', () => {
this.isUserInteracting = false;
});
// 鼠标离开事件
domElement.addEventListener('mouseleave', () => {
this.isUserInteracting = false;
});
// 鼠标滚轮事件(缩放)
domElement.addEventListener('wheel', (event) => {
event.preventDefault();
// 调整相机FOV实现缩放效果
this.camera.fov += event.deltaY * 0.05;
this.camera.fov = THREE.MathUtils.clamp(this.camera.fov, 30, 110);
this.camera.updateProjectionMatrix();
});
// 触摸事件支持
domElement.addEventListener('touchstart', (event) => {
event.preventDefault();
if (event.touches.length === 1) {
this.isUserInteracting = true;
this.onPointerDownMouseX = event.touches[0].pageX;
this.onPointerDownMouseY = event.touches[0].pageY;
this.onPointerDownLon = this.lon;
this.onPointerDownLat = this.lat;
}
});
domElement.addEventListener('touchmove', (event) => {
event.preventDefault();
if (event.touches.length === 1 && this.isUserInteracting) {
this.lon = (this.onPointerDownMouseX - event.touches[0].pageX) * 0.1 + this.onPointerDownLon;
this.lat = (event.touches[0].pageY - this.onPointerDownMouseY) * 0.1 + this.onPointerDownLat;
}
});
domElement.addEventListener('touchend', () => {
this.isUserInteracting = false;
});
}
onWindowResize() {
this.camera.aspect = this.container.clientWidth / this.container.clientHeight;
this.camera.updateProjectionMatrix();
this.renderer.setSize(this.container.clientWidth, this.container.clientHeight);
}
resetView() {
this.lon = 0;
this.lat = 0;
this.camera.fov = 75;
this.camera.updateProjectionMatrix();
}
toggleAutoRotate() {
this.autoRotate = !this.autoRotate;
}
animate() {
requestAnimationFrame(() => this.animate());
if (this.autoRotate && !this.isUserInteracting) {
this.lon += this.autoRotateSpeed;
}
this.lat = Math.max(-85, Math.min(85, this.lat));
this.phi = THREE.MathUtils.degToRad(90 - this.lat);
this.theta = THREE.MathUtils.degToRad(this.lon);
this.target.x = 500 * Math.sin(this.phi) * Math.cos(this.theta);
this.target.y = 500 * Math.cos(this.phi);
this.target.z = 500 * Math.sin(this.phi) * Math.sin(this.theta);
this.camera.lookAt(this.target);
this.renderer.render(this.scene, this.camera);
}
}
// 初始化全景查看器
let panoramaViewer;
document.addEventListener('DOMContentLoaded', function () {
// 创建全景查看器
panoramaViewer = new PanoramaViewer('panorama');
// 加载第一个全景图
const firstImage = document.querySelector('.image-option.active').getAttribute('data-src');
panoramaViewer.loadPanorama(firstImage);
// 绑定控制按钮事件
document.getElementById('autoRotate').addEventListener('click', function () {
panoramaViewer.toggleAutoRotate();
this.classList.toggle('active');
this.innerHTML = panoramaViewer.autoRotate ?
'<span>停止旋转</span>' : '<span>自动旋转</span>';
});
document.getElementById('fullscreen').addEventListener('click', function () {
const container = document.querySelector('.player-container');
if (!document.fullscreenElement) {
container.requestFullscreen().catch(err => {
console.error(`全屏请求错误: ${err.message}`);
});
this.innerHTML = '<span>退出全屏</span>';
} else {
document.exitFullscreen();
this.innerHTML = '<span>全屏模式</span>';
}
});
document.getElementById('resetView').addEventListener('click', function () {
panoramaViewer.resetView();
});
// 绑定全景图切换事件
const imageOptions = document.querySelectorAll('.image-option');
imageOptions.forEach(option => {
option.addEventListener('click', function () {
// 更新活动状态
imageOptions.forEach(opt => opt.classList.remove('active'));
this.classList.add('active');
// 切换全景图
const imageUrl = this.getAttribute('data-src');
panoramaViewer.loadPanorama(imageUrl);
});
});
// 全屏变化事件
document.addEventListener('fullscreenchange', function () {
const fullscreenBtn = document.getElementById('fullscreen');
if (document.fullscreenElement) {
fullscreenBtn.innerHTML = '<span>退出全屏</span>';
} else {
fullscreenBtn.innerHTML = '<span>全屏模式</span>';
}
});
});
</script>
</body>
</html>
+932 -1023
View File
File diff suppressed because it is too large Load Diff
+300
View File
@@ -0,0 +1,300 @@
/* Extracted from pictureBook.html */
.aigc-layout {
/* Add specific styles if needed, previously inline or default */
}
.head-title-image {
height: 100%;
width: auto;
}
.contact-admin-btn {
color: aliceblue;
}
.qr-code-img {
width: 100%;
}
.role-remark-input,
.narration-input,
.prompt-input {
background-color: #101a2d;
}
.prompt-image {
width: 70px;
height: auto;
}
.preview-image {
width: 100%;
}
.mask-edit-btn {
display: -webkit-box;
}
.brush-slider {
width: 40%;
margin: 0 10px;
}
.mask-btn {
margin: 0 10px;
}
.scroll-container {
overflow-y: scroll;
height: 78vh;
}
.mid-text {
font-weight: bold;
font-size: medium;
}
.el-message-box .box-title {
width: 100%;
text-align: center;
font-size: large;
color: aliceblue;
}
.el-message-box .qrcode {
width: 100%;
height: auto;
}
.el-message-box .el-input-group {
box-shadow: 0 0 0 1px var(--el-input-border-color, var(--el-border-color)) inset;
border-radius: var(--el-input-border-radius, var(--el-border-radius-base));
}
.el-message-box input {
text-align: center;
}
.el-message-box .el-input-group__append {
color: aliceblue;
}
.el-message-box {
margin-top: 4px;
padding: 20px 16px;
color: rgb(253 248 248 / 80%);
background-color: #606266;
border-radius: 24px;
font-size: 13px;
font-weight: lighter;
border: 1px solid #606266;
}
.head-title {
color: aliceblue;
font-size: x-large;
height: 52px;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
}
.el-card__body,
.el-card__footer {
padding: 0;
}
.el-upload-dragger,
.el-upload-dragger.is-dragover {
padding: 0;
background-color: rgba(255, 255, 255, .0);
border: none;
border-radius: 6px;
box-sizing: border-box;
text-align: center;
cursor: pointer;
position: relative;
overflow: hidden;
}
.image-view .el-upload--picture-card {
display: none;
}
.el-upload-list--picture-card .el-upload-list__item,
.el-upload--picture-card {
width: 100px;
height: 100px;
}
.el-menu--horizontal {
--el-menu-horizontal-height: 100px;
}
.el-form-item__label,
.el-checkbox__label,
.el-radio__label,
.el-input__inner,
.el-statistic__head,
.el-statistic__number {
color: rgba(255, 255, 255, .8);
}
.el-input__wrapper {
font-weight: lighter;
position: relative;
background-color: rgba(255, 255, 255, .05);
color: rgba(255, 255, 255, .8);
border-radius: 9px;
font-size: 13px;
border: 1px solid transparent;
}
.el-textarea__inner {
font-weight: lighter;
position: relative;
background-color: rgba(255, 255, 255, .05);
color: rgba(255, 255, 255, .8);
border-radius: 9px;
font-size: 13px;
border: 1px solid transparent;
}
.el-card,
.el-card__body,
.el-card__header,
.el-card__footer {
color: rgba(255, 255, 255, .8);
background-color: rgba(255, 255, 255, .0);
border: 0px solid transparent;
}
.el-input-group__append,
.el-switch__core,
.el-input-number__decrease,
.el-input-number__increase,
.el-upload--picture-card {
background-color: rgba(255, 255, 255, .05);
}
.el-textarea .el-input__count {
color: rgba(255, 255, 255, .8);
background-color: rgba(255, 255, 255, 0);
}
.el-icon {
color: rgba(255, 255, 255, .8);
}
.el-popper.is-pure {
padding: 13px 0;
background: rgba(255, 255, 255, .1);
color: #fff;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
box-shadow: 0 4px 40px rgba(2, 10, 29, .4);
border-radius: 9px;
}
.el-select-dropdown__item {
padding: 5px 20px 5px 12px;
position: relative;
background: transparent;
color: #fff;
display: block;
min-height: 32px;
padding: 5px 12px;
color: rgba(250, 250, 250, 0.88);
font-weight: normal;
font-size: 14px;
line-height: 1.5714285714285714;
box-sizing: border-box;
cursor: pointer;
border-radius: 4px;
}
.el-select-dropdown__item.hover,
.el-select-dropdown__item:hover {
background: rgba(255, 255, 255, .05);
}
.el-select-dropdown__item.selected {
color: #fff;
}
.el-select-dropdown__item.selected:after {
width: 16px;
height: 16px;
position: absolute;
content: "";
background: url(https://staticsns.cdn.bcebos.com/amis/2024-1/1704594047813/selected.png);
background-size: 100% 100%;
right: 8px;
top: 8px;
}
.el-button.is-disabled,
.el-button.is-disabled:focus,
.el-button.is-disabled:hover {
position: relative;
background: linear-gradient(to bottom right, rgb(229, 250, 87), rgb(64, 252, 67));
font-size: 16px;
font-weight: 900;
text-align: center;
line-height: 19.6px;
border-radius: 48px;
color: #000;
cursor: not-allowed;
}
.el-carousel__container {
height: 100%;
width: 100%;
}
/* Auth Dialog Styles */
.auth-dialog-content {
text-align: center;
}
.auth-title {
width: 100%;
text-align: center;
font-size: large;
color: aliceblue;
margin-bottom: 10px;
}
.auth-qrcode {
width: 100%;
height: auto;
margin-bottom: 10px;
}
.auth-input-container {
margin-bottom: 10px;
}
.auth-input {
text-align: center;
}
.auth-msg {
color: #ffb3b3;
margin-top: 10px;
}
.book-menu {
height: 30px;
}
.book-content {
height: 100%;
overflow-y: auto;
}
.story-content-item {
z-index: 0;
}
+275
View File
@@ -0,0 +1,275 @@
.el-message-box {
div.box-title {
width: 100%;
text-align: center;
font-size: large;
color: aliceblue;
}
.qrcode {
width: 100%;
height: auto;
}
.el-input-group {
box-shadow: 0 0 0 1px var(--el-input-border-color, var(--el-border-color)) inset;
border-radius: var(--el-input-border-radius, var(--el-border-radius-base));
}
input {
text-align: center;
}
.el-input-group__append {
color: aliceblue;
}
margin-top: 4px;
padding: 20px 16px;
color: rgb(253 248 248 / 80%);
background-color: #606266;
border-radius: 24px;
font-size: 13px;
font-weight: lighter;
border: 1px solid #606266;
}
.head-title {
color: aliceblue;
font-size: x-large;
height: 52px;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
}
.el-card__body,
.el-card__footer {
padding: 0;
}
.el-upload-dragger,
.el-upload-dragger.is-dragover {
padding: 0;
background-color: rgba(255, 255, 255, .0);
border: none;
border-radius: 6px;
box-sizing: border-box;
text-align: center;
cursor: pointer;
position: relative;
overflow: hidden;
}
.image-view .el-upload--picture-card {
display: none;
}
.el-upload-list--picture-card .el-upload-list__item,
.el-upload--picture-card {
width: 100px;
height: 100px;
}
.el-menu--horizontal {
--el-menu-horizontal-height: 100px;
}
.el-form-item__label,
.el-checkbox__label,
.el-radio__label,
.el-input__inner,
.el-statistic__head,
.el-statistic__number {
color: rgba(255, 255, 255, .8);
}
.el-input__wrapper {
font-weight: lighter;
position: relative;
background-color: rgba(255, 255, 255, .05);
color: rgba(255, 255, 255, .8);
border-radius: 9px;
font-size: 13px;
border: 1px solid transparent;
}
.el-textarea__inner {
font-weight: lighter;
position: relative;
background-color: rgba(255, 255, 255, .05);
color: rgba(255, 255, 255, .8);
border-radius: 9px;
font-size: 13px;
border: 1px solid transparent;
}
.el-card,
.el-card__body,
.el-card__header,
.el-card__footer {
color: rgba(255, 255, 255, .8);
background-color: rgba(255, 255, 255, .0);
border: 0px solid transparent;
}
.el-input-group__append,
.el-switch__core,
.el-input-number__decrease,
.el-input-number__increase,
.el-upload--picture-card {
background-color: rgba(255, 255, 255, .05);
}
.el-textarea .el-input__count {
color: rgba(255, 255, 255, .8);
background-color: rgba(255, 255, 255, 0);
}
.el-icon {
color: rgba(255, 255, 255, .8);
}
.el-popper.is-pure {
padding: 13px 0;
background: rgba(255, 255, 255, .1);
color: #fff;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
box-shadow: 0 4px 40px rgba(2, 10, 29, .4);
border-radius: 9px;
}
.el-select-dropdown__item {
padding: 5px 20px 5px 12px;
position: relative;
background: transparent;
color: #fff;
display: block;
min-height: 32px;
padding: 5px 12px;
color: rgba(250, 250, 250, 0.88);
font-weight: normal;
font-size: 14px;
line-height: 1.5714285714285714;
box-sizing: border-box;
cursor: pointer;
border-radius: 4px;
}
.el-select-dropdown__item.hover,
.el-select-dropdown__item:hover {
background: rgba(255, 255, 255, .05);
}
.el-select-dropdown__item.selected {
color: #fff;
}
.el-select-dropdown__item.selected:after {
width: 16px;
height: 16px;
position: absolute;
content: "";
background: url(https://staticsns.cdn.bcebos.com/amis/2024-1/1704594047813/selected.png);
background-size: 100% 100%;
right: 8px;
top: 8px;
}
.el-button.is-disabled,
.el-button.is-disabled:focus,
.el-button.is-disabled:hover {
position: relative;
background: linear-gradient(to bottom right, rgb(229, 250, 87), rgb(64, 252, 67));
font-size: 16px;
font-weight: 900;
text-align: center;
line-height: 19.6px;
border-radius: 48px;
color: #000;
cursor: not-allowed;
}
.el-carousel__container {
height: 100%;
width: 100%;
}
/* New classes extracted from app.html */
.logo-image {
height: 100%;
width: auto;
}
.admin-contact-text {
color: aliceblue;
}
.qr-code-img {
width: 100%;
}
.preview-carousel {
display: block;
height: 100%;
}
.full-size {
width: 100%;
height: 100%;
}
.text-white {
color: #fff;
}
.scroll-container {
overflow-y: scroll;
height: 78vh;
}
.w-100 {
width: 100%;
}
.bold-medium-text {
font-weight: bold;
font-size: medium;
}
.flex-box {
display: -webkit-box;
display: flex;
}
.slider-custom {
width: 40%;
margin: 0 10px;
}
.margin-h-10 {
margin: 0 10px;
}
.btn-gradient {
background: linear-gradient(to bottom right, rgb(224 224 224), rgb(244 85 85));
}
.full-height {
height: 100%;
}
.auth-container {
text-align: center;
}
.auth-input {
margin-top: 20px;
}
.auth-message {
color: #ffb3b3;
margin-top: 10px;
}
+607
View File
@@ -0,0 +1,607 @@
function formatTime(seconds) {
const days = Math.floor(seconds / (3600 * 24));
seconds -= days * 3600 * 24;
const hours = Math.floor(seconds / 3600);
seconds -= hours * 3600;
const minutes = Math.floor(seconds / 60);
seconds -= minutes * 60;
const formattedDays = days > 0 ? days + ':' : '';
const formattedHours = hours.toString().padStart(2, '0') + ':';
const formattedMinutes = minutes.toString().padStart(2, '0') + ':';
const formattedSeconds = seconds.toString().padStart(2, '0') + '';
return formattedDays + formattedHours + formattedMinutes + formattedSeconds;
}
function getParams(url) {
const regex = /[?&]([^=#]+)=([^&#]*)/g;
const params = {};
let match;
while (match = regex.exec(url)) {
params[decodeURIComponent(match[1])] = decodeURIComponent(match[2]);
}
return params;
}
let openId = window.APP_CONFIG.openId;
const app = Vue.createApp({
data() {
return {
commandList: [],
activeKey: '',
paramList: [],
brushSize: 20,
canvas: null,
dialogVisible: false,
dialogImageUrl: '',
dialogImageName: '',
maskKey: '',
historyList: [],
queue_remaining: 0,
page_number: 1,
noMore: false,
loading: false,
disabled: false,
isNotData: false,
isConnect: false,
isSuccess: false,
authVisible: false,
authCode: '',
authMessage: '',
form: {
openId: '',
command: '',
status: "prepare",
prompt_id: '',
percenStatus: 'warning',
percentage: 0,
output: { images: [], texts: [] },
nodeIds: [],
doneNodeIds: []
}
}
},
mounted: function () {
if (openId) {
this.initMain();
} else {
this.authUser();
}
},
watch: {
loading(newVal, oldVal) {
this.disabled = (newVal || this.noMore)
},
noMore(newVal, oldVal) {
this.disabled = (newVal || this.loading)
},
},
created() {
},
methods: {
initMain() {
this.loadData();
document.getElementById("app").style.display = "block";
this.initWebSocket();
this.getHistorys();
},
authUser(msg = '') {
this.authMessage = msg;
this.authVisible = true;
},
submitAuth() {
let that = this;
if (this.authCode.length != 6) {
this.authMessage = '验证码长度不正确!';
return;
}
this.getUserOpenId(this.authCode);
},
isShowAdmin() {
if ('pywebview' in window) {
if (pywebview.api.isLogin()) {
return true;
}
}
return false;
},
initWebSocket() {
var that = this;
var websocket = null;
//var socketId = "ws-" + new Date().getTime();
// 判断当前浏览器是否支持WebSocket, springboot是项目名
if ('WebSocket' in window) {
var host = window.location.host;
websocket = new WebSocket("ws://" + host + '/ws?clientId=' + openId);
} else {
console.error("不支持WebSocket");
}
// 连接发生错误的回调方法
websocket.onerror = function (e) {
console.error("WebSocket连接发生错误");
};
// 连接成功建立的回调方法
websocket.onopen = function () {
// 获取所有在线用户
console.log("WebSocket连接成功");
that.isConnect = true;
}
// 接收到消息的回调方法
websocket.onmessage = async function (event) {
if (event.data == null || event.data == "" || event.data instanceof Blob) {
return;
}
var messageJson = eval("(" + event.data + ")");
if (messageJson['type'] == 'crystools.monitor') {
return
}
console.log('messageJson', messageJson)
if (messageJson['type'] == "status") {
that.queue_remaining = messageJson['data']['status']['exec_info']['queue_remaining'];
} else if (messageJson['type'] == "execution_start") {
if (that.form.prompt_id == messageJson['data']['prompt_id']) {
that.form.percenStatus = 'success';
}
} else if (messageJson['type'] == "execution_cached") {
if (that.form.prompt_id == messageJson['data']['prompt_id']) {
that.form.doneNodeIds = messageJson['data']['nodes'];
}
} else if (messageJson['type'] == "executing") {
if (that.form.prompt_id == messageJson['data']['prompt_id']) {
let node = messageJson['data']['node'];
if (that.form.executing) {
that.form.doneNodeIds.push(that.form.executing);
}
if (!messageJson['data']['node']) {
that.form.status = 'prepare';
that.form.percenStatus = 'warning';
that.form.percentage = 0;
that.form.executing = '';
that.form.doneNodeIds = [];
that.form.nodeIds = [];
if (that.isSuccess) {
that.isSuccess = false
setTimeout(() => {
that.page_number = 1;
that.getHistorys();
}, 1000)
}
} else {
that.form.executing = node;
}
}
} else if (messageJson['type'] == 'progress') {
if (that.form.prompt_id == messageJson['data']['prompt_id']) {
let max = messageJson['data']['max'];
let value = messageJson['data']['value'];
let node = messageJson['data']['node'];
let percentage = 0;
if (that.form.executing == node) {
percentage = parseFloat(value / max);
}
that.form.percentage = parseInt((that.form.doneNodeIds.length + percentage) / that.form.nodeIds.length * 100);
}
} else if (messageJson['type'] == 'execution_success') {
if (that.form.prompt_id == messageJson['data']['prompt_id']) {
that.isSuccess = true;
}
} else if (messageJson['type'] == 'executed') {
if (that.form.prompt_id == messageJson['data']['prompt_id']) {
let node_output = 'output' in messageJson['data'] ? messageJson['data']['output'] : {};
let images = [];
let texts = [];
if ('outputImg' in node_output) {
for (let img of node_output?.outputImg) {
let url = '../view?filename=' + img.filename + '&type=' + img.type + '&subfolder=' + img.subfolder + '&rand=' + Math.random();
images.push(url)
}
} else if ("wechat_text" in node_output) {
texts.push(...node_output.wechat_text);
}
if (images.length > 0 || texts.length > 0) {
that.form.output = { images: images, texts: texts };
}
}
}
}
// 连接关闭的回调方法
websocket.onclose = function () {
console.error("WebSocket连接关闭");
that.openCloseMsg();
}
},
openCloseMsg() {
var that = this;
that.isConnect = false;
ElementPlus.ElMessage({
showClose: true,
duration: 0,
message: '服务器断开连接,关闭消息尝试重连',
type: 'error',
onClose: () => {
that.initWebSocket();
}
})
},
loadData() {
var that = this;
axios.get('../wechatauth/getCommands')
.then(function (response) {
console.log(response);
if (response.status == 200) {
that.commandList = response.data;
that.clickTab(Object.keys(response.data)[0])
}
})
.catch(function (error) {
console.log(error);
ElementPlus.ElMessage.error(error);
})
},
clickTab(key) {
this.activeKey = key;
if (this.commandList[key]) {
this.paramList = this.commandList[key].params;
for (let paramKey in this.paramList) {
this.paramList[paramKey]['value'] = this.paramList[paramKey]?.default;
if (this.paramList[paramKey].type == 'image') {
this.paramList[paramKey]['fileList'] = [];
}
}
}
},
clearValue(param) {
param.value = '';
},
refreshSeed(param) {
param.value = Math.floor(Math.random() * 1000000000000000);
},
handlePictureCardPreview(uploadFile, maskKey) {
this.dialogImageUrl = uploadFile.url;
this.dialogImageName = uploadFile.name;
this.dialogVisible = true;
this.maskKey = maskKey;
if (this.maskKey) {
this.initCanvas(uploadFile.url);
} else {
this.canvas = null;
}
},
handleRemove(uploadFile, paramKey) {
this.paramList[paramKey]['value'] = '';
},
beforeAvatarUpload(rawFile) {
// 判断文件格式不为图片的
if (rawFile.type !== 'image/png' && rawFile.type !== 'image/jpg' && rawFile.type !== 'image/jpeg') {
ElementPlus.ElMessage.error('请上传图片文件!');
return false
}
// else if (rawFile.size / 1024 / 1024 > 2) {
// ElementPlus.ElMessage.error('Avatar picture size can not exceed 2MB!')
// return false
// }
return true
},
updateSuccess(data, paramKey) {
this.paramList[paramKey]['value'] = data.subfolder ? (data.type + '/' + data.name) : (data.type + '/' + data.subfolder + '/' + data.name)
},
changeSize(value) {
this.canvas.freeDrawingBrush.width = parseInt(value);
},
clearCanvas() {
this.canvas.clear();
this.initCanvas(this.dialogImageUrl);
},
saveCanvas() {
let that = this;
// 清除背景图
that.canvas.backgroundImage = null;
that.canvas.backgroundColor = 'rgba(0,0,0,0)'
that.canvas.renderAll();
that.canvas.lowerCanvasEl.toBlob(function (blob) {
let formData = new FormData();
formData.append("image", blob, 'mask_' + openId + that.dialogImageName);
formData.append("overwrite", "true");
formData.append("type", "input");
axios.post('../upload/image', formData)
.then(function (response) {
let data = response.data;
data['url'] = '../view?filename=' + data.name + '&type=input&subfolder=&rand=' + Math.random();
that.paramList[that.maskKey]['fileList'] = [data];
that.paramList[that.maskKey]['value'] = data.subfolder ? (data.type + '/' + data.name) : (data.type + '/' + data.subfolder + '/' + data.name);
that.initCanvas(that.dialogImageUrl);
})
.catch(function (error) {
console.log(error);
ElementPlus.ElMessage.error(error);
})
}, "image/png");
},
initCanvas(backgroundUrl) {
var that = this;
fabric.Image.fromURL(backgroundUrl, (img) => {
if (that.canvas == null) {
that.canvas = new fabric.Canvas("canvas", {
backgroundColor: "#FFFFFF",
isDrawingMode: true,
});
let freeDrawingBrush = new fabric.PencilBrush(that.canvas);
// Set initial brush settings
freeDrawingBrush.width = that.brushSize;
freeDrawingBrush.color = '#000000';
// Enable free drawing mode on canvas
that.canvas.isDrawingMode = true;
that.canvas.freeDrawingBrush = freeDrawingBrush;
}
let parentWidth = document.querySelector(".mask_edit_btn").clientWidth;
let bili = img.width / parentWidth;
let imgHeight = img.height / bili;
that.canvas.setWidth(parentWidth);
that.canvas.setHeight(imgHeight);
that.canvas.setBackgroundImage(img, that.canvas.renderAll.bind(that.canvas), {
scaleX: that.canvas.width / img.width,
scaleY: that.canvas.height / img.height,
});
that.canvas.renderAll();
});
},
getUserOpenId(code) {
let that = this;
let formData = new FormData();
formData.append("code", code);
axios.post('../wechatauth/getUserOpenId', formData)
.then(function (response) {
if (response.data.success) {
openId = response.data.openId;
that.authVisible = false; // Close dialog
that.initMain();
if ('pywebview' in window) {
pywebview.api.setOpenId(openId);
}
} else {
that.authMessage = '授权码校验失败,请先扫码关注获取授权码!';
}
})
.catch(function (error) {
console.log(error);
that.authMessage = error.message || 'Unknown error';
})
},
cancelTask() {
let that = this;
let formData = new FormData();
formData.append("openId", openId);
formData.append("prompt_id", that.form.prompt_id)
axios.post('../wechatauth/cancelTask', formData)
.then(function (response) {
if (response.data.success) {
that.form.prompt_id = '';
that.form.nodeIds = [];
that.form.status = 'prepare';
that.form.percenStatus = 'warning';
that.form.percentage = 0;
} else {
ElementPlus.ElMessage.error(response.data.msg);
}
})
.catch(function (error) {
console.log(error);
ElementPlus.ElMessage.error(error);
})
},
startTask() {
let that = this;
if (that.form.status !== 'prepare') {
ElementPlus.ElMessage.info('任务<' + that.form.command + '>正在运行,请勿重复操作!')
return;
}
let formData = new FormData();
formData.append("openId", openId);
formData.append("command", this.activeKey);
formData.append("status", "prepare");
that.form.openId = openId;
that.form.command = this.activeKey;
that.form.status = "prepare";
for (let paramKey in this.paramList) {
if (this.paramList[paramKey]?.value) {
formData.append(paramKey, this.paramList[paramKey]?.value);
}
}
axios.post('../wechatauth/addTask', formData)
.then(function (response) {
console.log(response);
if (response.data.success) {
that.form.status = 'waiting';
that.form.prompt_id = response.data.prompt_id;
that.form.nodeIds = response.data.nodeIds;
} else {
ElementPlus.ElMessage.error(response.data.msg);
}
})
.catch(function (error) {
console.log(error);
ElementPlus.ElMessage.error(error);
})
},
confirmSetParam(params) {
let that = this;
this.clickTab(params.command);
for (let paramKey in this.paramList) {
if (params[paramKey]) {
if (typeof params[paramKey] == 'string' && params[paramKey] == 'undefined') {
this.paramList[paramKey].value = '';
} else if (this.paramList[paramKey].type == 'slider'
|| this.paramList[paramKey].type == 'number') {
this.paramList[paramKey].value = parseFloat(params[paramKey]);
} else {
if (this.paramList[paramKey].type == 'image') {
let ps = params[paramKey].split('//');
let url = '../view?filename=' + ps[1] + '&type=' + ps[0] + '&subfolder=&rand=' + Math.random();
//获取文件名称
this.paramList[paramKey].fileList = [{ url: url, name: ps[1] }];
}
this.paramList[paramKey].value = params[paramKey];
}
}
}
},
async handleDownload(imgs, name) {
let that = this;
var zip = new JSZip();
for (let img of imgs) {
let fileName = img.filename;
let { data } = await axios({
method: 'get',
url: img.url,
responseType: 'blob',
});
zip.file(fileName, data);
}
zip.generateAsync({ type: 'blob' }).then(function (content) {
let url = window.URL.createObjectURL(content);
that.downloadFile(url, name);
});
},
downloadFile(url, name) {
let a = document.createElement('a');
a.href = url;
a.download = name;
a.click();
},
loadHistory() {
let that = this;
if (this.isNotData) {
this.noMore = true;
setTimeout(() => {
this.noMore = false;
}, 1000)
return;
}
that.page_number += 1;
that.getHistorys();
},
setRunTask(dataStr, prompt_id) {
let that = this;
let params = JSON.parse(dataStr)
this.confirmSetParam(params)
axios.get('../api/queue')
.then(function (response) {
let queue_pending = response.data.queue_pending;
let queue_running = response.data.queue_running;
for (let data of queue_running) {
if (data[1] == prompt_id) {
that.form.status = 'waiting';
that.form.prompt_id = prompt_id;
that.form.percenStatus = 'success';
that.form.nodeIds = Object.keys(data[2]);
return;
}
}
for (let data of queue_pending) {
if (data[1] == prompt_id) {
that.form.status = 'waiting';
that.form.prompt_id = prompt_id;
that.form.nodeIds = Object.keys(data[2]);
return;
}
}
}).catch(function (error) {
console.log(error);
ElementPlus.ElMessage.error(error);
})
},
getHistorys() {
let that = this;
that.loading = true;
axios.get('../wechatauth/getHistorys?openId=' + openId + '&page_number=' + that.page_number)
.then(function (response) {
console.log(response);
if (that.page_number == 1) {
that.isNotData = false;
that.historyList = [];
}
let datas = response.data;
if (datas.length > 0 && datas[0][5] == 'waiting') {
that.setRunTask(datas[0][3], datas[0][4])
}
for (let res of datas) {
let command = JSON.parse(res[3])
if (res[5] == 'waiting') {
continue
} else if (res[5] == 'wcomplete') {
command['status'] = 'wcomplete'
let outputs = JSON.parse(res[8])
let images = [];
let texts = [];
let files = [];
for (let node_id in outputs) {
let node_output = outputs[node_id]
if (Object.keys(node_output).indexOf("outputImg") >= 0) {
for (let img of node_output?.outputImg) {
let url = '../view?filename=' + img.filename + '&type=' + img.type + '&subfolder=' + img.subfolder + '&rand=' + Math.random();
images.push(url)
img['url'] = url;
files.push(img);
}
} else if (Object.keys(node_output).indexOf("wechat_text") >= 0) {
texts.push(...node_output.wechat_text);
}
}
that.historyList.push({ 'prompt_id': res[4], 'command': command, 'images': images, 'files': files, 'status': res[5], 'texts': texts })
}
}
if (datas.length < 10) {
that.isNotData = true;
}
that.loading = false;
}).catch(function (error) {
console.log(error);
ElementPlus.ElMessage.error(error);
})
}
},
setup() {
}
})
// use ElementPlus and ElementPlusIconsVue
app.use(ElementPlus)
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
// use VueI18n
const browserLanguage = navigator.language || navigator.userLanguage; // zh-CN en-US
const languageTag = browserLanguage.split('-')[0]; // zh en
const i18n = VueI18n.createI18n({
legacy: false, // you must set `false`, to use Composition API
locale: languageTag, // set locale
fallbackLocale: 'en', // set fallback locale
messages: {},
})
app.use(i18n)
function loadLocaleMessages(locale) {
return axios.get(`static/locales/${locale}.json`).then(response => {
return response.data;
});
}
async function loadLocales() {
const locales = ['en', 'zh'];
await Promise.all(
locales.map(async (locale) => {
const messages = await loadLocaleMessages(locale);
i18n.global.setLocaleMessage(locale, messages);
})
);
}
loadLocales().then(() => {
app.mount('#app');
});
+150
View File
@@ -0,0 +1,150 @@
import cv2
import numpy as np
import math
import time
import os
import torch
def panorama_to_spherical(img):
"""
改进的正向变换函数,使用双线性插值
"""
rows, cols, c = img.shape
# 优化:大幅提高中间图像分辨率
# 原逻辑 R = cols / 2 / pi 导致垂直分辨率损失约70%
# 新逻辑:直接使用原图高度作为半径,保证垂直细节不丢失
R = rows
D = R * 2
cx = R
cy = R
# 创建目标图像的坐标网格
j_coords, i_coords = np.mgrid[0:D, 0:D]
# 计算极坐标参数
r = np.sqrt((i_coords - cx)**2 + (j_coords - cy)**2)
theta = np.arctan2(j_coords - cy, i_coords - cx)
theta = np.where(theta < 0, theta + 2 * math.pi, theta) # 调整角度到[0, 2π]
# 计算源图像坐标
xp = (theta / (2 * math.pi) * cols).astype(np.float32)
yp = ((1 - r / R) * rows).astype(np.float32) # 修正yp计算
# 使用remap进行高质量变换
valid_mask = r <= R
# 为了避免边缘伪影,可以不强制设为-1,而是让它自然插值(或者保持-1)
# 这里保持原逻辑,但使用更高质量插值
xp[~valid_mask] = -1
yp[~valid_mask] = -1
# 使用 Lanczos 插值以获得更好的质量
new_img = cv2.remap(img, xp, yp, cv2.INTER_LANCZOS4, borderMode=cv2.BORDER_CONSTANT, borderValue=0)
return new_img
# 更高质量的还原方案
def spherical_to_panorama(transformed_img, original_rows, original_cols):
"""
使用向量化计算和Lanczos插值的高质量还原
"""
D, D_c, c = transformed_img.shape
# 优化:根据中间图的实际尺寸计算半径,自适应不同分辨率
R = D // 2
cx = R
cy = R
# 向量化生成网格
x = np.arange(original_cols, dtype=np.float32)
y = np.arange(original_rows, dtype=np.float32)
X, Y = np.meshgrid(x, y)
# 计算极坐标参数
# r = (1 - y / original_rows) * R
r = (1.0 - Y / original_rows) * R
# theta = x / original_cols * 2 * math.pi
theta = X / original_cols * 2 * math.pi
# 极坐标转笛卡尔坐标
# 必须转换为 float32 才能用于 cv2.remap
map_x = (cx + r * np.cos(theta)).astype(np.float32)
map_y = (cy + r * np.sin(theta)).astype(np.float32)
# 使用 Lanczos 插值(更高质量)
restored_img = cv2.remap(transformed_img, map_x, map_y,
interpolation=cv2.INTER_LANCZOS4,
borderMode=cv2.BORDER_CONSTANT,
borderValue=0)
return restored_img
class LamSpherical2Panorama:
@classmethod
def INPUT_TYPES(s):
return {"required":
{
"images": ("IMAGE", ),
"width": ("INT", {"default": 4096, "min": 64, "max": 9999999}),
"height": ("INT", {"default": 2048, "min": 64, "max": 9999999}),
},
}
CATEGORY = "lam"
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("图片",)
FUNCTION = "spherical2Panorama"
def spherical2Panorama(self, images,width,height):
imaget_np=images.numpy()
imgt_shape=imaget_np.shape
prd_images=[]
for i in range(imgt_shape[0]):
imaget=np.uint8(imaget_np[i]*255)
imaget = cv2.cvtColor(imaget, cv2.COLOR_BGR2RGB)
transformed_img = spherical_to_panorama(imaget,height,width)
fin_color = cv2.cvtColor(transformed_img, cv2.COLOR_BGR2RGB)
prd_images.append(torch.from_numpy(np.array(fin_color).astype(np.float32) / 255.0))
return (torch.stack(prd_images),)
class LamPanorama2Spherical:
@classmethod
def INPUT_TYPES(s):
return {"required":
{"images": ("IMAGE", ),
},
}
CATEGORY = "lam"
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("图片",)
FUNCTION = "panorama2Spherical"
def panorama2Spherical(self, images):
imaget_np=images.numpy()
imgt_shape=imaget_np.shape
prd_images=[]
for i in range(imgt_shape[0]):
imaget=np.uint8(imaget_np[i]*255)
imaget = cv2.cvtColor(imaget, cv2.COLOR_BGR2RGB)
transformed_img = panorama_to_spherical(imaget)
fin_color = cv2.cvtColor(transformed_img, cv2.COLOR_BGR2RGB)
prd_images.append(torch.from_numpy(np.array(fin_color).astype(np.float32) / 255.0))
return (torch.stack(prd_images),)
NODE_CLASS_MAPPINGS = {
"LamPanorama2Spherical": LamPanorama2Spherical,
"LamSpherical2Panorama": LamSpherical2Panorama
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LamPanorama2Spherical": "全景图转球面图",
"LamSpherical2Panorama": "球面图转全景图"
}
+84
View File
@@ -0,0 +1,84 @@
from .src.utils.uitls import AlwaysEqualProxy
class LamSwitchStart:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"base": (AlwaysEqualProxy("*"),),
},
"hidden": {
"switchs": "STRING", #隐藏参数
}
}
RETURN_TYPES = (AlwaysEqualProxy("*"),)
RETURN_NAMES = ('base',)
FUNCTION = "switch_start"
OUTPUT_NODE = False
CATEGORY = "lam"
def switch_start(self,base,switchs=''):
return (base,)
class LamSwitchMiddle:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"base": (AlwaysEqualProxy("*"),),
"new": (AlwaysEqualProxy("*"),{"lazy":True}),
},
"hidden": {
"unique_id": "UNIQUE_ID", #节点编号
"prompt": "PROMPT", #流程节点信息
}
}
RETURN_TYPES = (AlwaysEqualProxy("*"),)
RETURN_NAMES = ('base',)
FUNCTION = "switch_middle"
OUTPUT_NODE = False
CATEGORY = "lam"
def check_lazy_status(self,base,new,unique_id,prompt):
needed = []
start_id = prompt[unique_id]['inputs']['base'][0]
index = 0
while prompt[start_id]['class_type']!='LamSwitchStart':
start_id=prompt[start_id]['inputs']['base'][0]
index+=1
switchsStr = prompt[start_id]['inputs']['switchs']
switchs = switchsStr.split(',')
if switchs[index]=='1':
needed.append('new')
return needed
def switch_middle(self,base,new,unique_id,prompt):
start_id = prompt[unique_id]['inputs']['base'][0]
index = 0
while prompt[start_id]['class_type']!='LamSwitchStart':
start_id=prompt[start_id]['inputs']['base'][0]
index+=1
switchsStr = prompt[start_id]['inputs']['switchs']
switchs = switchsStr.split(',')
if switchs[index]=='1':
return (new,)
else:
return (base,)
NODE_CLASS_MAPPINGS = {
"LamSwitchStart": LamSwitchStart,
"LamSwitchMiddle":LamSwitchMiddle
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LamSwitchStart": "开关控制",
"LamSwitchMiddle": "开关分段"
}