!DOCTYPE html html lang=zh-CN head meta charset=UTF-8 meta name=viewport content=width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no title全能图片生成title link href=httpsfonts.googleapis.comcss2family=Interwght@300;400;500;600;700;800&display=swap rel=stylesheet link rel=stylesheet href=httpscdnjs.cloudflare.comajaxlibsfont-awesome6.4.0cssall.min.css style root { --bg #07070a; --bg-elevated #0f0f14; --bg-card #13131a; --bg-hover #1a1a24; --border rgba(255,255,255,0.05); --border-hover rgba(255,255,255,0.1); --text #f0f0f5; --text-sec #8a8a9a; --text-dim #4a4a5a; --accent #7c3aed; --accent-bright #a78bfa; --success #22c55e; --error #ef4444; } { margin0; padding0; box-sizingborder-box; } html, body { font-family'Inter',-apple-system,sans-serif; backgroundvar(--bg); colorvar(--text); min-height100vh; overflow-xhidden; -webkit-tap-highlight-colortransparent; } .bg-glow { positionfixed; inset0; pointer-eventsnone; z-index0; overflowhidden; } .bg-glowbefore { content''; positionabsolute; width600px; height600px; backgroundradial-gradient(circle, rgba(124,58,237,0.12) 0%, transparent 70%); top-200px; right-150px; animationfloat 18s infinite ease-in-out; } .bg-glowafter { content''; positionabsolute; width500px; height500px; backgroundradial-gradient(circle, rgba(167,139,250,0.08) 0%, transparent 70%); bottom-180px; left-100px; animationfloat 22s infinite ease-in-out reverse; } @keyframes float { 0%,100%{transformtranslate(0,0) scale(1)} 50%{transformtranslate(30px,-20px) scale(1.08)} } .header { displayflex; align-itemscenter; justify-contentspace-between; padding16px 28px; border-bottom1px solid var(--border); backgroundrgba(7,7,10,0.85); backdrop-filterblur(24px) saturate(1.2); positionsticky; top0; z-index100; } .brand { displayflex; align-itemscenter; gap12px; } .brand-icon { width36px; height36px; border-radius10px; backgroundlinear-gradient(135deg,var(--accent),#c084fc); displayflex; align-itemscenter; justify-contentcenter; color#fff; font-size16px; box-shadow0 4px 20px rgba(124,58,237,0.3); } .brand-text { font-size17px; font-weight700; letter-spacing-0.3px; backgroundlinear-gradient(135deg,var(--text),var(--accent-bright)); -webkit-background-cliptext; -webkit-text-fill-colortransparent; } .brand-sub { font-size11px; colorvar(--text-dim); font-weight500; margin-top-2px; } .nav-actions { displayflex; align-itemscenter; gap10px; } .nav-btn { width38px; height38px; border-radius10px; border1px solid var(--border); backgroundvar(--bg-card); colorvar(--text-sec); displayflex; align-itemscenter; justify-contentcenter; cursorpointer; font-size15px; transitionall .25s cubic-bezier(.4,0,.2,1); positionrelative; } .nav-btnhover { colorvar(--text); border-colorvar(--border-hover); backgroundvar(--bg-hover); transformtranslateY(-1px); } .nav-btn .badge { positionabsolute; top-4px; right-4px; width16px; height16px; border-radius50%; backgroundvar(--accent); color#fff; font-size9px; displayflex; align-itemscenter; justify-contentcenter; font-weight700; } .main-wrap { max-width1280px; margin0 auto; padding28px; displaygrid; grid-template-columns400px 1fr; gap28px; positionrelative; z-index1; } .ctrl-col { displayflex; flex-directioncolumn; gap20px; } .ctrl-card { backgroundvar(--bg-card); border1px solid var(--border); border-radius20px; padding24px; transitionall .3s ease; } .ctrl-cardhover { border-colorvar(--border-hover); } .mode-tabs { displayflex; gap4px; padding4px; backgroundvar(--bg); border-radius14px; border1px solid var(--border); } .mode-tab { flex1; padding12px; text-aligncenter; font-size14px; font-weight600; colorvar(--text-sec); cursorpointer; border-radius10px; bordernone; backgroundtransparent; transitionall .25s ease; displayflex; align-itemscenter; justify-contentcenter; gap8px; } .mode-tabhover { colorvar(--text); } .mode-tab.active { color#fff; backgroundlinear-gradient(135deg,var(--accent),#9333ea); box-shadow0 4px 16px rgba(124,58,237,0.25); } .provider-bar { displayflex; align-itemscenter; gap8px; margin-top12px; padding-top12px; border-top1px solid var(--border); } .provider-label { font-size12px; font-weight600; colorvar(--text-sec); text-transformuppercase; letter-spacing0.5px; } .provider-chip { padding8px 16px; border-radius10px; border1.5px solid var(--border); backgroundvar(--bg); colorvar(--text-sec); cursorpointer; font-size12px; font-weight600; transitionall .25s ease; } .provider-chiphover { border-colorvar(--border-hover); colorvar(--text); } .provider-chip.active { border-colorvar(--accent); colorvar(--accent-bright); backgroundrgba(124,58,237,0.08); box-shadow0 2px 8px rgba(124,58,237,0.1); } .upload-area { border2px dashed var(--border); border-radius16px; padding36px 24px; text-aligncenter; cursorpointer; transitionall .3s ease; positionrelative; overflowhidden; } .upload-areabefore { content''; positionabsolute; inset0; backgroundlinear-gradient(135deg,rgba(124,58,237,0.03),rgba(147,51,234,0.03)); opacity0; transitionopacity .3s; } .upload-areahover { border-colorvar(--accent); } .upload-areahoverbefore { opacity1; } .upload-area { positionrelative; z-index1; } .upload-area i { font-size32px; colorvar(--accent); margin-bottom12px; displayblock; } .upload-area .u-title { font-size14px; font-weight500; colorvar(--text-sec); } .upload-area .u-hint { font-size12px; colorvar(--text-dim); margin-top4px; } .upload-input { displaynone; } .preview-box { positionrelative; margin-top16px; displaynone; border-radius14px; overflowhidden; border1px solid var(--border); } .preview-box.active { displayblock; animationfadeIn .4s ease; } .preview-box img { width100%; max-height200px; object-fitcover; displayblock; } .preview-del { positionabsolute; top10px; right10px; width32px; height32px; border-radius50%; bordernone; backgroundrgba(0,0,0,.6); backdrop-filterblur(8px); color#fff; cursorpointer; displayflex; align-itemscenter; justify-contentcenter; font-size12px; transitionall .2s; } .preview-delhover { backgroundvar(--error); transformscale(1.1); } .prompt-wrap { positionrelative; } .prompt-area { width100%; min-height120px; padding16px; backgroundvar(--bg); border1.5px solid var(--border); border-radius16px; colorvar(--text); font-size14px; line-height1.7; resizevertical; outlinenone; font-familyinherit; transitionall .3s ease; } .prompt-areafocus { border-colorvar(--accent); box-shadow0 0 0 3px rgba(124,58,237,0.1), inset 0 1px 2px rgba(0,0,0,0.1); } .prompt-areaplaceholder { colorvar(--text-dim); } .prompt-footer { displayflex; justify-contentspace-between; align-itemscenter; margin-top10px; padding0 4px; } .prompt-count { font-size11px; colorvar(--text-dim); font-weight500; } .prompt-clear { font-size11px; colorvar(--text-dim); cursorpointer; backgroundnone; bordernone; displayflex; align-itemscenter; gap4px; transitioncolor .2s; } .prompt-clearhover { colorvar(--text-sec); } .ratio-section { margin-top16px; } .ratio-label { font-size12px; font-weight600; colorvar(--text-sec); text-transformuppercase; letter-spacing0.8px; margin-bottom12px; displayflex; align-itemscenter; gap6px; } .gen-wrap { margin-top8px; } .gen-btn { width100%; padding16px; border-radius16px; bordernone; backgroundlinear-gradient(135deg,var(--accent),#9333ea); color#fff; font-size15px; font-weight700; cursorpointer; displayflex; align-itemscenter; justify-contentcenter; gap10px; transitionall .3s cubic-bezier(.4,0,.2,1); positionrelative; overflowhidden; box-shadow0 4px 20px rgba(124,58,237,0.25); } .gen-btnbefore { content''; positionabsolute; inset0; backgroundlinear-gradient(135deg,transparent,rgba(255,255,255,0.1),transparent); transformtranslateX(-100%); transitiontransform .6s; } .gen-btnhover { transformtranslateY(-2px); box-shadow0 8px 30px rgba(124,58,237,0.35); } .gen-btnhoverbefore { transformtranslateX(100%); } .gen-btnactive { transformtranslateY(0); } .gen-btn .spin { displaynone; width18px; height18px; border2px solid rgba(255,255,255,.3); border-top-color#fff; border-radius50%; animationspin .8s linear infinite; } .gen-btn.loading .spin { displayblock; } .gen-btn.loading .btn-txt { displaynone; } @keyframes spin { to{transformrotate(360deg)} } .placeholder-card { backgroundvar(--bg-card); border1px solid var(--border); border-radius20px; overflowhidden; transitionall .4s ease; animationcardIn .5s ease; } @keyframes cardIn { from{opacity0; transformtranslateY(24px) scale(.98)} to{opacity1; transformtranslateY(0) scale(1)} } .ph-frame { positionrelative; padding-top75%; overflowhidden; backgroundlinear-gradient(135deg,rgba(124,58,237,.1),rgba(147,51,234,.06)); displayflex; align-itemscenter; justify-contentcenter; } .ph-content { positionabsolute; inset0; displayflex; flex-directioncolumn; align-itemscenter; justify-contentcenter; gap16px; padding20px; } .ph-pulse { width48px; height48px; border-radius50%; backgroundlinear-gradient(135deg,var(--accent),var(--accent-bright)); opacity.6; animationpulseGlow 2s infinite ease-in-out; } @keyframes pulseGlow { 0%,100%{transformscale(1); opacity.6; box-shadow0 0 0 0 rgba(124,58,237,0.4);} 50%{transformscale(1.1); opacity.9; box-shadow0 0 30px rgba(124,58,237,0.3);} } .ph-text { font-size13px; colorvar(--text-sec); font-weight500; } .ph-progress { width80%; height4px; backgroundvar(--bg); border-radius2px; overflowhidden; } .ph-progress-bar { height100%; width0%; border-radius2px; backgroundlinear-gradient(90deg,var(--accent),var(--accent-bright)); animationprogressMove 3s ease-in-out infinite; } @keyframes progressMove { 0%{width0%; opacity.5;} 50%{width70%; opacity1;} 100%{width100%; opacity.5;} } .ph-info { padding18px; } .ph-prompt { font-size13px; colorvar(--text-sec); line-height1.6; display-webkit-box; -webkit-line-clamp2; -webkit-box-orientvertical; overflowhidden; } .ph-meta { displayflex; justify-contentspace-between; align-itemscenter; margin-top12px; padding-top12px; border-top1px solid var(--border); } .ph-tag { font-size10px; font-weight600; colorvar(--text-dim); text-transformuppercase; letter-spacing0.5px; padding4px 10px; border-radius6px; backgroundvar(--bg); } .ph-status { font-size11px; colorvar(--accent-bright); font-weight500; } .ph-status.done { colorvar(--success); } .ph-status.err { colorvar(--error); } .img-card { backgroundvar(--bg-card); border1px solid var(--border); border-radius20px; overflowhidden; transitionall .4s cubic-bezier(.4,0,.2,1); animationcardIn .5s cubic-bezier(.4,0,.2,1); } .img-cardhover { border-colorvar(--border-hover); transformtranslateY(-4px); box-shadow0 20px 40px rgba(0,0,0,.4), 0 0 0 1px rgba(124,58,237,.1); } .img-frame { positionrelative; padding-top75%; overflowhidden; backgroundlinear-gradient(135deg,rgba(124,58,237,.06),rgba(147,51,234,.04)); } .img-frame img { positionabsolute; inset0; width100%; height100%; object-fitcover; transitiontransform .6s cubic-bezier(.4,0,.2,1); } .img-cardhover .img-frame img { transformscale(1.06); } .img-overlay { positionabsolute; inset0; backgroundlinear-gradient(to top, rgba(0,0,0,.7) 0%, transparent 60%); opacity0; transitionopacity .35s ease; displayflex; align-itemsflex-end; justify-contentcenter; padding20px; gap10px; } .img-cardhover .img-overlay { opacity1; } .ov-btn { width40px; height40px; border-radius12px; bordernone; backgroundrgba(255,255,255,.12); backdrop-filterblur(12px); color#fff; cursorpointer; displayflex; align-itemscenter; justify-contentcenter; font-size14px; transitionall .25s ease; } .ov-btnhover { backgroundrgba(255,255,255,.25); transformtranslateY(-2px); } .img-info { padding18px; } .img-prompt { font-size13px; colorvar(--text-sec); line-height1.6; display-webkit-box; -webkit-line-clamp2; -webkit-box-orientvertical; overflowhidden; } .img-footer { displayflex; justify-contentspace-between; align-itemscenter; margin-top12px; padding-top12px; border-top1px solid var(--border); } .img-tag { font-size10px; font-weight600; colorvar(--text-dim); text-transformuppercase; letter-spacing0.5px; padding4px 10px; border-radius6px; backgroundvar(--bg); } .img-time { font-size11px; colorvar(--text-dim); } .empty-state { displayflex; flex-directioncolumn; align-itemscenter; justify-contentcenter; padding80px 40px; text-aligncenter; backgroundvar(--bg-card); border1px solid var(--border); border-radius20px; min-height400px; } .empty-illu { width80px; height80px; border-radius24px; backgroundlinear-gradient(135deg,rgba(124,58,237,.15),rgba(147,51,234,.1)); displayflex; align-itemscenter; justify-contentcenter; margin-bottom24px; } .empty-illu i { font-size28px; colorvar(--accent); opacity.7; } .empty-state h3 { font-size18px; font-weight700; margin-bottom8px; } .empty-state p { font-size14px; colorvar(--text-dim); max-width280px; line-height1.7; } .result-col { displayflex; flex-directioncolumn; gap20px; } .result-header { displayflex; align-itemscenter; justify-contentspace-between; padding-bottom16px; border-bottom1px solid var(--border); } .result-title { font-size20px; font-weight700; letter-spacing-0.5px; } .result-meta { displayflex; align-itemscenter; gap16px; } .meta-chip { displayflex; align-itemscenter; gap6px; padding6px 14px; border-radius20px; backgroundvar(--bg-card); border1px solid var(--border); font-size12px; colorvar(--text-sec); font-weight500; } .meta-chip i { font-size11px; } .meta-chip .val { colorvar(--text); font-weight700; } .img-grid { displaygrid; grid-template-columnsrepeat(auto-fill,minmax(280px,1fr)); gap20px; } .drawer-backdrop { positionfixed; inset0; backgroundrgba(0,0,0,.6); backdrop-filterblur(4px); z-index250; opacity0; visibilityhidden; transitionall .3s ease; } .drawer-backdrop.show { opacity1; visibilityvisible; } .drawer { positionfixed; top0; right-420px; width420px; height100vh; backgroundvar(--bg-elevated); border-left1px solid var(--border); z-index300; transitionright .4s cubic-bezier(.16,1,.3,1); displayflex; flex-directioncolumn; overflow-yauto; } .drawer.open { right0; } .drawer-head { padding24px; border-bottom1px solid var(--border); displayflex; align-itemscenter; justify-contentspace-between; positionsticky; top0; backgroundvar(--bg-elevated); z-index2; } .drawer-head h2 { font-size18px; font-weight700; } .drawer-close { width36px; height36px; border-radius10px; bordernone; backgroundvar(--bg-card); colorvar(--text-sec); cursorpointer; displayflex; align-itemscenter; justify-contentcenter; font-size14px; transitionall .2s; } .drawer-closehover { backgroundvar(--bg-hover); colorvar(--text); } .drawer-body { padding24px; displayflex; flex-directioncolumn; gap24px; } .form-group { displayflex; flex-directioncolumn; gap8px; } .form-label { font-size12px; font-weight600; colorvar(--text-sec); text-transformuppercase; letter-spacing0.8px; } .form-input { width100%; padding12px 16px; backgroundvar(--bg); border1.5px solid var(--border); border-radius12px; colorvar(--text); font-size14px; outlinenone; font-familyinherit; transitionall .25s ease; } .form-inputfocus { border-colorvar(--accent); box-shadow0 0 0 3px rgba(124,58,237,.1); } .form-inputplaceholder { colorvar(--text-dim); } select.form-input { appearancenone; background-imageurl(dataimagesvg+xml,%3Csvg xmlns='httpwww.w3.org2000svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%238a8a9a' stroke-width='2'%3E%3Cpath d='M6 9l6 6 6-6'%3E%3Csvg%3E); background-repeatno-repeat; background-positionright 14px center; padding-right40px; } select.form-input option { background#0f0f14 !important; color#f0f0f5 !important; padding10px 16px; } .modal { positionfixed; inset0; z-index400; displayflex; align-itemscenter; justify-contentcenter; opacity0; visibilityhidden; transitionall .3s ease; } .modal.show { opacity1; visibilityvisible; } .modal-bg { positionabsolute; inset0; backgroundrgba(0,0,0,.88); backdrop-filterblur(12px); } .modal-close { positionabsolute; top24px; right24px; width44px; height44px; border-radius50%; bordernone; backgroundrgba(255,255,255,.08); color#fff; font-size18px; cursorpointer; z-index2; displayflex; align-itemscenter; justify-contentcenter; transitionall .2s; backdrop-filterblur(8px); } .modal-closehover { backgroundrgba(255,255,255,.15); transformrotate(90deg); } .modal-content { positionrelative; z-index1; max-width90vw; max-height90vh; } .modal-img { max-width100%; max-height80vh; border-radius16px; box-shadow0 30px 80px rgba(0,0,0,.6); } .modal-actions { displayflex; gap12px; justify-contentcenter; margin-top20px; flex-wrapwrap; } .modal-btn { padding12px 24px; border-radius12px; border1px solid rgba(255,255,255,.12); backgroundrgba(255,255,255,.06); color#fff; font-size13px; font-weight600; cursorpointer; displayflex; align-itemscenter; gap8px; transitionall .2s; backdrop-filterblur(8px); } .modal-btnhover { backgroundrgba(255,255,255,.12); border-colorrgba(255,255,255,.2); } .toast-wrap { positionfixed; bottom28px; left50%; transformtranslateX(-50%); z-index500; displayflex; flex-directioncolumn; gap8px; } .toast { padding14px 24px; border-radius14px; backgroundvar(--bg-elevated); border1px solid var(--border); colorvar(--text); font-size13px; font-weight500; displayflex; align-itemscenter; gap10px; box-shadow0 16px 48px rgba(0,0,0,.4); animationtoastIn .35s cubic-bezier(.4,0,.2,1); backdrop-filterblur(12px); } @keyframes toastIn { from{opacity0; transformtranslate(-50%,20px)} to{opacity1; transformtranslate(-50%,0)} } .toast.ok { border-colorrgba(34,197,94,.3); } .toast.err { border-colorrgba(239,68,68,.3); } .toast i { font-size16px; } .toast.ok i { colorvar(--success); } .toast.err i { colorvar(--error); } .hist-drawer { width380px; right-380px; } .hist-drawer.open { right0; } .hist-list { padding20px; displayflex; flex-directioncolumn; gap12px; } .hist-item { displayflex; gap14px; padding14px; border-radius14px; backgroundvar(--bg-card); border1px solid var(--border); cursorpointer; transitionall .25s ease; positionrelative; } .hist-itemhover { border-colorvar(--border-hover); backgroundvar(--bg-hover); } .hist-thumb { width60px; height60px; border-radius10px; object-fitcover; flex-shrink0; backgroundlinear-gradient(135deg,rgba(124,58,237,.15),rgba(147,51,234,.1)); } .hist-info { flex1; min-width0; displayflex; flex-directioncolumn; justify-contentcenter; } .hist-prompt { font-size13px; colorvar(--text); line-height1.5; display-webkit-box; -webkit-line-clamp2; -webkit-box-orientvertical; overflowhidden; } .hist-meta { font-size11px; colorvar(--text-dim); margin-top6px; } .hist-actions { displayflex; gap6px; flex-shrink0; align-itemscenter; } .hist-actions .ov-btn { width28px; height28px; border-radius8px; font-size11px; } .hist-progress-wrap { flex1; height3px; backgroundvar(--bg); border-radius2px; overflowhidden; margin-top6px; } .hist-progress-bar { height100%; backgroundlinear-gradient(90deg,var(--accent),var(--accent-bright)); transitionwidth .3s ease; } .hist-spin { width20px; height20px; border2px solid rgba(255,255,255,.2); border-top-colorvar(--accent); border-radius50%; animationspin .8s linear infinite; } .wechat-modal .modal-content { max-width360px; width90%; backgroundvar(--bg-elevated); border-radius24px; border1px solid var(--border); text-aligncenter; padding32px 24px; } .wechat-modal img { width100%; border-radius12px; margin20px 0; displayblock; border1px solid var(--border); } .preview-grid { displaygrid; grid-template-columnsrepeat(4,1fr); gap10px; margin-top12px; } @media (max-width640px) { .preview-grid { grid-template-columnsrepeat(2,1fr); } } .preview-item { positionrelative; border-radius12px; overflowhidden; border1px solid var(--border); aspect-ratio1; backgroundvar(--bg); } .preview-item img { width100%; height100%; object-fitcover; displayblock; } .preview-item .preview-idx { positionabsolute; top6px; left6px; width20px; height20px; border-radius50%; backgroundvar(--accent); color#fff; font-size10px; font-weight700; displayflex; align-itemscenter; justify-contentcenter; z-index2; } .preview-item .preview-del { positionabsolute; top6px; right6px; width22px; height22px; border-radius50%; bordernone; backgroundrgba(0,0,0,.6); color#fff; cursorpointer; displayflex; align-itemscenter; justify-contentcenter; font-size10px; z-index2; transitionall .2s; } .preview-item .preview-delhover { backgroundvar(--error); transformscale(1.1); } .url-input-wrap { displayflex; flex-directioncolumn; gap10px; } .url-add-row { displayflex; gap8px; } .url-add-row .form-input { flex1; } .url-add-btn { width40px; height40px; border-radius10px; border1.5px solid var(--border); backgroundvar(--bg); colorvar(--accent); cursorpointer; displayflex; align-itemscenter; justify-contentcenter; font-size14px; transitionall .25s ease; } .url-add-btnhover { border-colorvar(--accent); backgroundrgba(124,58,237,0.08); } .url-item { displayflex; align-itemscenter; gap8px; padding10px 14px; border-radius10px; backgroundvar(--bg); border1.5px solid var(--border); } .url-item img { width40px; height40px; border-radius6px; object-fitcover; flex-shrink0; backgroundvar(--bg-card); } .url-item span { flex1; font-size12px; colorvar(--text-sec); overflowhidden; text-overflowellipsis; white-spacenowrap; } .url-item button { width24px; height24px; border-radius50%; bordernone; backgroundrgba(239,68,68,.1); colorvar(--error); cursorpointer; displayflex; align-itemscenter; justify-contentcenter; font-size10px; } @media (max-width1024px) { .main-wrap { grid-template-columns1fr; } .ctrl-col { max-width600px; margin0 auto; width100%; } .drawer, .hist-drawer { width100%; right-100%; } } @media (max-width640px) { .header { padding12px 16px; } .main-wrap { padding16px; gap20px; } .ctrl-card { padding18px; border-radius16px; } .img-grid { grid-template-columnsrepeat(2,1fr); gap12px; } .img-card { border-radius14px; } .result-header { flex-directioncolumn; align-itemsflex-start; gap12px; } .result-meta { width100%; justify-contentspace-between; } .empty-state { padding60px 20px; min-height300px; } } @media (max-width400px) { .img-grid { grid-template-columns1fr; } } @keyframes fadeIn { from{opacity0} to{opacity1} } style base target=_blank head body div class=bg-glowdiv header class=header div class=brand div class=brand-iconi class=fas fa-wand-magic-sparklesidiv divdiv class=brand-text全能图片生成divdiv class=brand-subAI 图片生成工具 · 纯本地存储divdiv div div class=nav-actions button class=nav-btn id=histBtn title=历史记录i class=fas fa-clock-rotate-leftispan class=badge id=histBadge style=displaynone0spanbutton button class=nav-btn id=setBtn title=设置i class=fas fa-slidersibutton div header div class=main-wrap div class=ctrl-col div class=ctrl-card style=padding16px; div class=mode-tabs button class=mode-tab active data-tab=generatei class=fas fa-paint-brushi图片生成button button class=mode-tab data-tab=editi class=fas fa-pen-to-squarei图片编辑button div div class=provider-bar span class=provider-label通道span button class=provider-chip active id=chipOneapi onclick=switchProvider('oneapi')全能图片button button class=provider-chip id=chipGrsai onclick=switchProvider('grsai')全能图片备用button div div div class=ctrl-card id=uploadCard style=displaynone; div id=oneapiUploadArea div class=upload-area id=uploadArea i class=fas fa-cloud-arrow-upi div class=u-title点击上传参考图片div div class=u-hint支持 PNG、JPG、WEBP · ≤10MBdiv input type=file class=upload-input id=fileInput accept=image div div class=preview-box id=previewBox img id=previewImg alt= button class=preview-del id=previewDeli class=fas fa-timesibutton div div div id=grsaiUploadAreaWrap style=displaynone; div class=upload-area id=grsaiUploadArea style=positionrelative;overflowhidden; i class=fas fa-cloud-arrow-upi div class=u-title点击上传参考图片div div class=u-hint支持 PNG、JPG、WEBP · ≤10MB · 最多4张div input type=file id=grsaiFileInput accept=image multiple style=positionabsolute;inset0;opacity0;cursorpointer;width100%;height100%;z-index10; div div class=preview-grid id=grsaiPreviewGriddiv div style=margin-top12px; padding-top12px; border-top1px solid var(--border); label class=form-labeli class=fas fa-link style=font-size10px;i或粘贴网络图片URLlabel div id=urlListdiv div class=url-add-row input type=text class=form-input id=urlInput placeholder=httpsexample.comimage.png button class=url-add-btn id=urlAddBtni class=fas fa-plusibutton div div div div div class=ctrl-card div class=prompt-wrap textarea class=prompt-area id=promptInput placeholder=描述你想要的画面,越详细效果越好...textarea div class=prompt-footer span class=prompt-count id=promptCount0 字span button class=prompt-clear id=promptCleari class=fas fa-eraser style=font-size10px;i清空button div div div class=ratio-section div class=ratio-labeli class=fas fa-crop-simple style=font-size10px;i画面比例div select class=form-input id=ratioSelect option value=2048x2048 selected11 正方形 · 2048×2048option option value=2560x1440169 宽屏 · 2560×1440 (2K QHD)option option value=1440x2560916 竖屏 · 1440×2560option option value=2048x136032 横屏 · 2048×1360option option value=1360x204823 竖屏 · 1360×2048option option value=2048x153643 横屏 · 2048×1536option option value=1536x204834 竖屏 · 1536×2048option option value=2560x1088219 带鱼屏 · 2560×1088option select div div div class=gen-wrap button class=gen-btn id=genBtn div class=spindiv span class=btn-txti class=fas fa-bolti span id=btnLabel开始生成spanspan button div div div class=result-col div class=result-header h2 class=result-title生成结果h2 div class=result-meta div class=meta-chipi class=fas fa-imagesispan class=val id=metaCount0span 张div div class=meta-chipi class=fas fa-coinsi费用 span class=val id=metaCost¥0.00spandiv div div div class=img-grid id=imgGrid div class=empty-state div class=empty-illui class=fas fa-wand-magic-sparklesidiv h3开始创作h3 p在左侧输入提示词,选择比例,点击生成即可开始 AI 绘图brspan style=colorvar(--text-dim);font-size12px;图片将自动保存到本地设备spanp div div div div div class=drawer-backdrop id=setBackdropdiv div class=drawer id=setDrawer div class=drawer-head h2i class=fas fa-sliders style=margin-right10px;colorvar(--accent);i设置h2 button class=drawer-close id=setClosei class=fas fa-timesibutton div div class=drawer-body div class=form-grouplabel class=form-label服务商labelselect class=form-input id=providerSelectoption value=oneapi全能图片optionoption value=grsai全能图片备用optionselectdiv div class=form-grouplabel class=form-label全能图片 Keylabelinput type=password class=form-input id=apiKeyOneapi placeholder=请输入全能图片 API Keydiv div class=form-grouplabel class=form-label全能图片备用 Keylabelinput type=password class=form-input id=apiKeyGrsai placeholder=请输入全能图片备用 API Keydiv div class=form-group style=displaynone;label class=form-labelAPI 地址labelinput type=text class=form-input id=apiUrl placeholder=httpsxxx.xxx.comdiv div class=form-group style=displaynone;label class=form-label模型labelselect class=form-input id=modelSelectoption value=gpt-image-2gpt-image-2optionoption value=dall-e-3dall-e-3optionselectdiv div class=form-group style=displaynone;label class=form-label单价 (元张)labelinput type=number class=form-input id=priceInput value=0.2 step=0.01div div style=padding12px;border-radius10px;backgroundrgba(34,197,94,0.08);border1px solid rgba(34,197,94,0.2); div style=font-size12px;colorvar(--success);font-weight600;i class=fas fa-shield-halved style=margin-right6px;i纯本地模式div div style=font-size11px;colorvar(--text-sec);margin-top4px;API Key 和历史记录均保存在当前设备,br换设备或清理浏览器数据会丢失。div div div div div class=drawer hist-drawer id=histDrawer div class=drawer-head h2i class=fas fa-clock-rotate-left style=margin-right10px;colorvar(--accent);i历史记录h2 button class=drawer-close id=histClosei class=fas fa-timesibutton div div class=hist-list id=histListdiv div div class=modal id=modal div class=modal-bg id=modalBgdiv button class=modal-close id=modalClosei class=fas fa-timesibutton div class=modal-content img class=modal-img id=modalImg src= alt= div class=modal-actions button class=modal-btn id=modalDowni class=fas fa-downloadi下载图片button button class=modal-btn id=modalCopyi class=fas fa-copyi复制提示词button button class=modal-btn id=modalRef style=backgroundrgba(124,58,237,0.15);border-colorrgba(124,58,237,0.3);i class=fas fa-imagei引用参考button div div div div class=modal wechat-modal id=wechatModal div class=modal-bg onclick=closeWechatTip()div div class=modal-content i class=fas fa-hand-pointer style=font-size40px; colorvar(--accent); margin-bottom8px;i h3 style=margin-bottom8px;微信内保存提示h3 p style=colorvar(--text-sec); font-size14px; line-height1.6; 微信内置浏览器限制自动下载br 请strong style=colorvar(--text);长按下方图片strong选择保存到相册br 或点击右上角 ··· 在浏览器打开 p img id=wechatImg src= alt= button class=gen-btn onclick=closeWechatTip() style=width100%;我知道了button div div div class=toast-wrap id=toastWrapdiv script ==================== IndexedDB 本地存储模块 ==================== const DB_NAME = 'gpt-image-local-v1'; const STORE_NAME = 'images'; let dbInstance = null; const imageCache = new Map(); id - { objectUrl, record } function openDB() { return new Promise((resolve, reject) = { if(dbInstance) return resolve(dbInstance); const req = indexedDB.open(DB_NAME, 1); req.onerror = () = reject(req.error); req.onsuccess = () = { dbInstance = req.result; resolve(dbInstance); }; req.onupgradeneeded = (e) = { const db = e.target.result; if(!db.objectStoreNames.contains(STORE_NAME)){ db.createObjectStore(STORE_NAME, { keyPath 'id' }); } }; }); } async function saveImageRecord(id, blob, prompt, size, model, time) { const db = await openDB(); return new Promise((resolve, reject) = { const tx = db.transaction(STORE_NAME, 'readwrite'); const store = tx.objectStore(STORE_NAME); const req = store.put({ id, blob, prompt, size, model, time }); req.onsuccess = () = resolve(); req.onerror = () = reject(req.error); }); } async function deleteImageRecord(id) { const db = await openDB(); return new Promise((resolve, reject) = { const tx = db.transaction(STORE_NAME, 'readwrite'); const store = tx.objectStore(STORE_NAME); const req = store.delete(id); req.onsuccess = () = resolve(); req.onerror = () = reject(req.error); }); } async function loadAllRecords() { const db = await openDB(); return new Promise((resolve, reject) = { const tx = db.transaction(STORE_NAME, 'readonly'); const store = tx.objectStore(STORE_NAME); const req = store.getAll(); req.onsuccess = () = resolve(req.result []); req.onerror = () = reject(req.error); }); } function dataURLtoBlob(dataurl) { const arr = dataurl.split(','); const mime = arr[0].match((.);)[1]; const bstr = atob(arr[1]); let n = bstr.length; const u8arr = new Uint8Array(n); while(n--) u8arr[n] = bstr.charCodeAt(n); return new Blob([u8arr], { type mime }); } async function urlToBlob(url) { if(url.startsWith('data')) return dataURLtoBlob(url); const r = await fetch(url, { mode 'cors' }); return r.blob(); } ==================== 状态 ==================== const state = { tab'generate', provider'oneapi', apiKeyOneapi localStorage.getItem('gi2_key_oneapi') '', apiKeyGrsai localStorage.getItem('gi2_key_grsai') '', apiUrl'', model'gpt-image-2', price0.2, filenull, urls[], images[], loadingfalse, selectedSize'2048x2048', abortControllers[], generatingTasks [], }; function getDefaultUrl(provider){ const p = provider state.provider; return p==='grsai''httpsgrsai.dakka.com.cn''httpsone-api.bltcy.top'; } function getApiKey(){return state.provider==='grsai'state.apiKeyGrsaistate.apiKeyOneapi;} const $ = id=document.getElementById(id); function isWechat(){ return MicroMessengeri.test(navigator.userAgent); } ==================== 初始化 ==================== async function init(){ await openDB(); await loadHistoryFromDB(); if($('providerSelect')) $('providerSelect').value=state.provider; $('apiKeyOneapi').value=state.apiKeyOneapi; $('apiKeyGrsai').value=state.apiKeyGrsai; const defaultUrl = getDefaultUrl(); $('apiUrl').value = defaultUrl; state.apiUrl = defaultUrl; localStorage.setItem('gi2_url', defaultUrl); $('modelSelect').value=state.model; $('priceInput').value=state.price; $('ratioSelect').value = state.selectedSize; document.querySelectorAll('.provider-chip').forEach(c=c.classList.remove('active')); if(state.provider==='oneapi'){if($('chipOneapi')) $('chipOneapi').classList.add('active');} else{if($('chipGrsai')) $('chipGrsai').classList.add('active');} if(state.images.length){renderImages(); updateMeta();} renderHistory(); updateBadge(); bindEvents(); } async function loadHistoryFromDB(){ try{ const records = await loadAllRecords(); 按时间倒序 records.sort((a,b)=new Date(b.time)-new Date(a.time)); 重建内存缓存 records.forEach(r={ const url = URL.createObjectURL(r.blob); imageCache.set(r.id, { url, record r }); }); 重建 state.images(只存元数据) state.images = records.map(r=({ id r.id, prompt r.prompt, size r.size, model r.model, time r.time })); }catch(e){ console.error('加载本地历史失败', e); } } function getImageSrc(id){ const cached = imageCache.get(id); return cached cached.url ''; } ==================== 事件绑定 ==================== function bindEvents(){ document.querySelectorAll('.mode-tab').forEach(t={ t.addEventListener('click',()=switchTab(t.dataset.tab)); }); if($('providerSelect')) $('providerSelect').addEventListener('change',e={ switchProvider(e.target.value); const newUrl = getDefaultUrl(); $('apiUrl').value = newUrl; state.apiUrl = newUrl; }); $('apiKeyOneapi').addEventListener('change',e={ state.apiKeyOneapi=e.target.value; localStorage.setItem('gi2_key_oneapi', state.apiKeyOneapi); }); $('apiKeyGrsai').addEventListener('change',e={ state.apiKeyGrsai=e.target.value; localStorage.setItem('gi2_key_grsai', state.apiKeyGrsai); }); $('apiUrl').addEventListener('change',e={state.apiUrl=e.target.value;}); $('modelSelect').addEventListener('change',e={state.model=e.target.value;}); $('priceInput').addEventListener('change',e={state.price=parseFloat(e.target.value)0.2; updateMeta();}); $('ratioSelect').addEventListener('change', e={ state.selectedSize = e.target.value; }); $('uploadArea').addEventListener('click',()={$('fileInput').click();}); $('fileInput').addEventListener('change',e={ if(e.target.files && e.target.files[0]) handleFile(e.target.files[0]); }); $('uploadArea').addEventListener('dragover',e={e.preventDefault(); $('uploadArea').style.borderColor='var(--accent)';}); $('uploadArea').addEventListener('dragleave',()={$('uploadArea').style.borderColor='';}); $('uploadArea').addEventListener('drop',e={e.preventDefault(); $('uploadArea').style.borderColor=''; if(e.dataTransfer.files[0]) handleFile(e.dataTransfer.files[0]);}); $('previewDel').addEventListener('click',e={e.stopPropagation(); clearOneapiFile();}); if($('urlAddBtn')) $('urlAddBtn').addEventListener('click',addUrl); if($('urlInput')) $('urlInput').addEventListener('keydown',e={if(e.key==='Enter') addUrl();}); if($('grsaiFileInput')){ $('grsaiFileInput').addEventListener('change',e={ if(e.target.files && e.target.files.length) handleGrsaiFiles(e.target.files); e.target.value = ''; }); } $('genBtn').addEventListener('click',generate); $('promptClear').addEventListener('click',()={$('promptInput').value=''; $('promptCount').textContent='0 字'; $('promptInput').focus();}); $('promptInput').addEventListener('input',e={$('promptCount').textContent=e.target.value.length+' 字';}); $('modalClose').addEventListener('click',closeModal); $('modalBg').addEventListener('click',closeModal); $('modalDown').addEventListener('click',dlCurrent); $('modalCopy').addEventListener('click',cpCurrent); $('modalRef').addEventListener('click',refCurrent); $('setBtn').addEventListener('click',()={ $('setDrawer').classList.add('open'); $('setBackdrop').classList.add('show'); }); $('setClose').addEventListener('click',closeSet); $('setBackdrop').addEventListener('click',()={closeSet(); closeHist();}); $('histBtn').addEventListener('click',()={ $('histDrawer').classList.add('open'); $('setBackdrop').classList.add('show'); }); $('histClose').addEventListener('click',closeHist); document.addEventListener('keydown',e={ if(e.key==='Escape'){closeModal(); closeSet(); closeHist(); closeWechatTip();} if((e.ctrlKeye.metaKey)&&e.key==='Enter') generate(); }); } ==================== 微信提示 ==================== function showWechatTip(id){ const src = getImageSrc(id); if(!src) return; $('wechatImg').src = src; $('wechatModal').classList.add('show'); document.body.style.overflow = 'hidden'; } function closeWechatTip(){ $('wechatModal').classList.remove('show'); document.body.style.overflow = ''; } ==================== 引用参考图 ==================== function addToReference(id){ const cached = imageCache.get(id); if(!cached){ toast('图片未加载','err'); return; } const blob = cached.record.blob; if(state.provider === 'grsai'){ if(state.urls.length = 4){toast('参考图已满(最多4张)','err'); return;} const refUrl = URL.createObjectURL(blob); 注意:GRSAI 的 urls 数组里放的是字符串(base64 或 http url),blob url 也可以直接传给服务商吗? 实际上 blob url 只在当前页面有效,无法传给后端。我们需要转成 base64 或 File 这里转为 base64 加入 const reader = new FileReader(); reader.onload = e = { const base64 = e.target.result; if(state.urls.includes(base64)){toast('该图已在参考图中','err'); return;} state.urls.push(base64); renderGrsaiPreviews(); renderUrls(); if(state.tab !== 'edit') switchTab('edit'); toast('已添加到参考图','ok'); }; reader.readAsDataURL(blob); } else { if(state.file){toast('OneAPI只能上传1张参考图,请先清除当前图片','err'); return;} const file = new File([blob], 'ref.png', {type blob.type 'imagepng'}); handleFile(file); if(state.tab !== 'edit') switchTab('edit'); toast('已添加到参考图','ok'); } } function refCurrent(){ if(curId) addToReference(curId); } ==================== 自动下载 ==================== function autoDownload(blob, filename){ const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; a.style.display = 'none'; document.body.appendChild(a); a.click(); setTimeout(() = { document.body.removeChild(a); URL.revokeObjectURL(url); }, 100); } ==================== 删除历史 ==================== function deleteHistory(id){ if(!confirm('确定删除这条记录?本地文件不会删除。')) return; if(imageCache.has(id)){ URL.revokeObjectURL(imageCache.get(id).url); imageCache.delete(id); } deleteImageRecord(id); state.images = state.images.filter(i = i.id !== id); renderImages(); renderHistory(); updateBadge(); updateMeta(); toast('已删除记录','ok'); } function switchProvider(provider){ state.provider = provider; if($('providerSelect')) $('providerSelect').value = provider; const newUrl = getDefaultUrl(); state.apiUrl = newUrl; if($('apiUrl')) $('apiUrl').value = newUrl; document.querySelectorAll('.provider-chip').forEach(c=c.classList.remove('active')); if(provider==='oneapi') $('chipOneapi').classList.add('active'); else $('chipGrsai').classList.add('active'); toast('已切换至 ' + (provider==='oneapi''全能图片''全能图片备用'), 'ok'); if(state.tab==='edit') updateUploadArea(); } function updateUploadArea(){ if(state.provider==='grsai'){ $('oneapiUploadArea').style.display='none'; $('grsaiUploadAreaWrap').style.display='block'; }else{ $('oneapiUploadArea').style.display='block'; $('grsaiUploadAreaWrap').style.display='none'; } } function switchTab(tab){ state.tab=tab; document.querySelectorAll('.mode-tab').forEach(t=t.classList.toggle('active',t.dataset.tab===tab)); $('uploadCard').style.display=tab==='edit''block''none'; $('btnLabel').textContent=tab==='generate''开始生成''开始编辑'; if(tab==='generate'){ clearOneapiFile(); clearGrsaiData(); }else{ updateUploadArea(); } toast(tab==='generate''已切换生成模式''已切换编辑模式','ok'); } function handleFile(file){ if(!file!file.type.startsWith('image')){toast('请上传图片文件','err'); return;} const MAX_SIZE = 10 1024 1024; if(file.size MAX_SIZE){ toast(`图片超过10MB(${(file.size10241024).toFixed(1)}MB),请压缩后重试`,'err'); return; } state.file = file; const r = new FileReader(); r.onload = e = { $('previewImg').src = e.target.result; $('previewBox').classList.add('active'); $('uploadArea').style.display = 'none'; }; r.onerror = () = toast('图片读取失败','err'); r.readAsDataURL(file); } function clearOneapiFile(){ state.file = null; $('fileInput').value = ''; $('previewBox').classList.remove('active'); $('uploadArea').style.display = 'block'; } function clearGrsaiData(){ state.urls = []; if($('urlInput')) $('urlInput').value = ''; if($('urlList')) $('urlList').innerHTML = ''; if($('grsaiPreviewGrid')) $('grsaiPreviewGrid').innerHTML = ''; if($('grsaiUploadArea')) $('grsaiUploadArea').style.display = 'block'; } function handleGrsaiFiles(fileList){ const files = Array.from(fileList).filter(f=f.type.startsWith('image')f.name.match(.(jpgjpegpngwebpgifbmp)$i)); if(!files.length){toast('请上传图片文件','err'); return;} if(state.urls.length + files.length 4){toast('最多4张参考图','err'); return;} const MAX_SIZE = 10 1024 1024; let added = 0; files.forEach(file = { if(file.size MAX_SIZE){ toast(`「${file.name}」超过10MB,已跳过`,'err'); return; } const reader = new FileReader(); reader.onload = e = { state.urls.push(e.target.result); added++; renderGrsaiPreviews(); renderUrls(); toast(`已添加「${file.name}」(${state.urls.length}4)`,'ok'); }; reader.onerror = () = toast(`「${file.name}」读取失败`,'err'); reader.readAsDataURL(file); }); } function renderGrsaiPreviews(){ const grid = $('grsaiPreviewGrid'); if(!grid) return; const fileUrls = state.urls.filter(u = u.startsWith('dataimage')); if(!fileUrls.length){grid.innerHTML=''; $('grsaiUploadArea').style.display='block'; return;} grid.innerHTML = fileUrls.map((url, i) = ` div class=preview-item span class=preview-idx${i+1}span button class=preview-del onclick=removeGrsaiFile(${i}) title=删除i class=fas fa-timesibutton img src=${url} div `).join(''); if(state.urls.length = 4) $('grsaiUploadArea').style.display='none'; else $('grsaiUploadArea').style.display='block'; } function removeGrsaiFile(idx){ let count = 0; for(let i=0; istate.urls.length; i++){ if(state.urls[i].startsWith('dataimage')){ if(count === idx){ state.urls.splice(i, 1); break; } count++; } } renderGrsaiPreviews(); } function addUrl(){ const input = $('urlInput'); const url = input.value.trim(); if(!url){toast('请输入图片URL','err'); return;} if(!url.startsWith('http')){toast('URL格式错误','err'); return;} if(state.urls.length = 4){toast('最多4张参考图','err'); return;} if(state.urls.includes(url)){toast('该URL已添加','err'); return;} state.urls.push(url); input.value = ''; renderUrls(); toast(`已添加 ${state.urls.length}4`,'ok'); } function removeUrl(idx){ state.urls.splice(idx, 1); renderUrls(); } function renderUrls(){ const list = $('urlList'); if(!list) return; if(!state.urls.length){list.innerHTML=''; return;} list.innerHTML = state.urls.map((url, i) = { const isBase64 = url.startsWith('dataimage'); const display = isBase64 '[本地图片]' url.slice(0,40)+'...'; return ` div class=url-item img src=${url} alt= onerror=this.style.display='none';this.parentElement.querySelector('.url-fallback').style.display='flex'; div class=url-fallback style=displaynone;width40px;height40px;border-radius6px;backgroundvar(--bg-card);align-itemscenter;justify-contentcenter;colorvar(--text-dim);font-size16px;i class=fas fa-imageidiv span${display}span button onclick=removeUrl(${i}) title=删除i class=fas fa-timesibutton div `}).join(''); } ==================== 生成主流程 ==================== function generate(){ const prompt=$('promptInput').value.trim(); const key=getApiKey().trim(); if(!key){toast(`请填写 ${state.provider==='grsai''备用''主'}通道 API Key`,'err'); openSet(); return;} if(!prompt){toast('请输入提示词','err'); $('promptInput').focus(); return;} if(state.tab==='edit'){ if(state.provider==='grsai'){ if(state.urls.length===0){toast('请上传至少1张参考图','err'); return;} }else{ if(!state.file){toast('请上传图片','err'); return;} } } const base=$('apiUrl').value.trim()getDefaultUrl(); const model=$('modelSelect').value; const size=state.selectedSize; const batchId=Date.now(); state.loading=true; setLoad(true); const grid=$('imgGrid'); if(grid.querySelector('.empty-state')) grid.innerHTML=''; const placeholders=[]; for(let i=0;i1;i++){ const phId=`ph-${batchId}-${i}`; const ph=createPlaceholder(phId, prompt, size, i+1, 1); grid.insertBefore(ph, grid.firstChild); placeholders.push(ph); } const task = { id placeholders[0].id, prompt, size, model, status'running', progress5, startTimeDate.now(), providerstate.provider }; state.generatingTasks.unshift(task); renderHistory(); const ctrl=new AbortController(); state.abortControllers.push(ctrl); const phId=placeholders[0].id; const placeholderEl = placeholders[0]; if(state.provider==='grsai'){ runTaskGrsai(prompt, key, base, size, ctrl, placeholderEl, phId); }else{ runTaskOneapi(prompt, key, base, model, size, ctrl, placeholderEl, phId); } setLoad(false); toast('开始生成...','ok'); } function updateTaskStatus(phId, updates){ const task = state.generatingTasks.find(t = t.id === phId); if(task){ Object.assign(task, updates); renderHistory(); } } function removeTask(phId){ state.generatingTasks = state.generatingTasks.filter(t = t.id !== phId); renderHistory(); } function createPlaceholder(id, prompt, size, index, total){ const div=document.createElement('div'); div.className='placeholder-card'; div.id=id; div.innerHTML=`div class=ph-framediv class=ph-contentdiv class=ph-pulsedivdiv class=ph-textAI 绘制中...divdiv class=ph-progressdiv class=ph-progress-bardivdivdivdivdiv class=ph-infodiv class=ph-prompt${esc(prompt)}divdiv class=ph-metaspan class=ph-tag${size'2048x2048'}spanspan class=ph-status id=st-${id}准备中...spandivdiv`; return div; } ==================== OneAPI 任务 ==================== function runTaskOneapi(prompt, key, base, model, size, ctrl, placeholderEl, phId){ const t0=Date.now(); const statusEl=placeholderEl.querySelector(`#st-${phId}`); if(statusEl) statusEl.textContent='绘制中...'; updateTaskStatus(phId, {status'running', progress10}); let reqPromise; if(state.tab==='generate'){ reqPromise=fetch(`${base}v1imagesgenerations`,{ method'POST', signalctrl.signal, headers{'Authorization'`Bearer ${key}`,'Content-Type''applicationjson'}, bodyJSON.stringify({model,prompt,size}) }); }else{ const fd=new FormData(); fd.append('image', state.file); fd.append('prompt', prompt); fd.append('model', model); fd.append('size', size); reqPromise=fetch(`${base}v1imagesedits`,{method'POST',signalctrl.signal,headers{'Authorization'`Bearer ${key}`},bodyfd}); } reqPromise.then(async resp={ if(ctrl.signal.aborted) return; let data; const ct=resp.headers.get('content-type')''; if(ct.includes('applicationjson')){data=await resp.json();}else{const text=await resp.text(); throw new Error(`非JSON${resp.status}`);} if(!resp.ok) throw new Error(`API ${resp.status}${data.error.messageJSON.stringify(data).slice(0,100)}`); if(!data.data!Array.isArray(data.data)data.data.length===0) throw new Error('无图片数据'); const item=data.data[0]; const imgUrl=item.url(item.b64_json`dataimagepng;base64,${item.b64_json}`''); if(!imgUrl) throw new Error('图片URL缺失'); updateTaskStatus(phId, {status'succeeded', progress100}); const time=((Date.now()-t0)1000).toFixed(1); 核心:转 blob - 存本地 - 自动下载 let blob; try{ blob = await urlToBlob(imgUrl); }catch(e){ console.error('获取blob失败', e); } const id = Date.now()+Math.floor(Math.random()1000); if(blob){ await saveImageRecord(id, blob, prompt, size, model, new Date().toISOString()); const objectUrl = URL.createObjectURL(blob); imageCache.set(id, { url objectUrl, record {id, blob, prompt, size, model, time new Date().toISOString()} }); if(isWechat()){ showWechatTip(id); } else { autoDownload(blob, `img-${id}.png`); toast('已自动保存到本地下载文件夹','ok'); } } const img={id, prompt, size, model, time new Date().toISOString()}; state.images.unshift(img); if(state.images.length50){ const old = state.images.pop(); if(imageCache.has(old.id)){ URL.revokeObjectURL(imageCache.get(old.id).url); imageCache.delete(old.id); } deleteImageRecord(old.id).catch(()={}); } removeTask(phId); replacePlaceholder(placeholderEl, img, time); renderHistory(); updateMeta(); updateBadge(); }).catch(err={ let msg=ctrl.signal.aborted'已取消'(err.message'未知错误'); if(msg.includes('413') msg.includes('Payload') msg.includes('too large')) msg = '图片太大,请压缩后重试'; const st=placeholderEl.querySelector(`#st-${phId}`); if(st){st.textContent=msg; st.className='ph-status err';} updateTaskStatus(phId, {status'failed', progress0}); console.error('任务失败',err); }); } ==================== GRSAI 任务 ==================== async function runTaskGrsai(prompt, key, base, size, ctrl, placeholderEl, phId){ const t0=Date.now(); const statusEl=placeholderEl.querySelector(`#st-${phId}`); try{ if(statusEl) statusEl.textContent='提交中...'; updateTaskStatus(phId, {status'running', progress5}); const submitResp=await fetch(`${base}v1drawcompletions`,{ method'POST', signalctrl.signal, headers{'Authorization'`Bearer ${key}`,'Content-Type''applicationjson'}, bodyJSON.stringify({ model'gpt-image-2', prompt, sizesize, urls state.urls.length 0 state.urls undefined, webHook'-1' }) }); const submitData=await submitResp.json(); if(submitData.code!==0) throw new Error(submitData.msg'提交失败'); const taskId=submitData.data.id; while(true){ if(ctrl.signal.aborted) return; await new Promise(r=setTimeout(r,2000)); if(ctrl.signal.aborted) return; const resultResp=await fetch(`${base}v1drawresult`,{ method'POST', headers{'Authorization'`Bearer ${key}`,'Content-Type''applicationjson'}, bodyJSON.stringify({idtaskId}) }); const resultData=await resultResp.json(); if(resultData.code!==0) continue; const d=resultData.data; if(d.status==='succeeded'){ const imgUrl=d.results.[0].urld.url; if(!imgUrl) throw new Error('无图片URL'); const time=((Date.now()-t0)1000).toFixed(1); updateTaskStatus(phId, {status'succeeded', progress100}); let blob; try{ blob = await urlToBlob(imgUrl); }catch(e){ console.error('获取blob失败', e); } const id = Date.now()+Math.floor(Math.random()1000); if(blob){ await saveImageRecord(id, blob, prompt, size, 'gpt-image-2', new Date().toISOString()); const objectUrl = URL.createObjectURL(blob); imageCache.set(id, { url objectUrl, record {id, blob, prompt, size, model'gpt-image-2', time new Date().toISOString()} }); if(isWechat()){ showWechatTip(id); } else { autoDownload(blob, `img-${id}.png`); toast('已自动保存到本地下载文件夹','ok'); } } const img={id, prompt, size, model'gpt-image-2', time new Date().toISOString()}; state.images.unshift(img); if(state.images.length50){ const old = state.images.pop(); if(imageCache.has(old.id)){ URL.revokeObjectURL(imageCache.get(old.id).url); imageCache.delete(old.id); } deleteImageRecord(old.id).catch(()={}); } removeTask(phId); replacePlaceholder(placeholderEl, img, time); renderHistory(); updateMeta(); updateBadge(); return; }else if(d.status==='failed'){ throw new Error(d.failure_reasond.error'生成失败'); }else if(d.status==='running'){ if(statusEl){statusEl.textContent=`绘制中 ${d.progress0}%`;} updateTaskStatus(phId, {status'running', progress d.progress 50}); } } }catch(err){ let msg=ctrl.signal.aborted'已取消'(err.message'未知错误'); if(msg.includes('413') msg.includes('Payload') msg.includes('too large')) msg = '图片太大,请压缩后重试'; if(statusEl){statusEl.textContent=msg; statusEl.className='ph-status err';} updateTaskStatus(phId, {status'failed', progress0}); console.error('GRSAI任务失败',err); } } ==================== 渲染 ==================== function replacePlaceholder(el, img, time){ if(!el) return; const src = getImageSrc(img.id) ''; el.className='img-card'; el.id=`img-${img.id}`; el.innerHTML=`div class=img-frameimg src=${src} alt= loading=lazy onerror=this.style.display='none'div class=img-overlaybutton class=ov-btn onclick=preview(${img.id}) title=预览i class=fas fa-expandibuttonbutton class=ov-btn onclick=dlImg(${img.id}) title=下载i class=fas fa-downloadibuttonbutton class=ov-btn onclick=cpPrompt('${esc(img.prompt)}') title=复制提示词i class=fas fa-copyibuttonbutton class=ov-btn onclick=addToReference(${img.id}) title=引用到参考图 style=backgroundrgba(124,58,237,0.25);i class=fas fa-imageibuttondivdivdiv class=img-infodiv class=img-prompt${esc(img.prompt)}divdiv class=img-footerspan class=img-tag${img.size'2048x2048'}spanspan class=img-time${time}sspandivdiv`; } function renderImages(){ if(!state.images.length){ $('imgGrid').innerHTML=`div class=empty-statediv class=empty-illui class=fas fa-wand-magic-sparklesidivh3开始创作h3p在左侧输入提示词,选择比例,点击生成即可开始 AI 绘图brspan style=colorvar(--text-dim);font-size12px;图片将自动保存到本地设备spanpdiv`; $('metaCount').textContent='0'; $('metaCost').textContent='¥0.00'; return; } $('metaCount').textContent=state.images.length; $('metaCost').textContent='¥'+(state.images.lengthstate.price).toFixed(2); $('imgGrid').innerHTML=state.images.map(img={ const src = getImageSrc(img.id) ''; return ` div class=img-card div class=img-frameimg src=${src} alt= loading=lazy onerror=this.style.display='none'div class=img-overlaybutton class=ov-btn onclick=preview(${img.id}) title=预览i class=fas fa-expandibuttonbutton class=ov-btn onclick=dlImg(${img.id}) title=下载i class=fas fa-downloadibuttonbutton class=ov-btn onclick=cpPrompt('${esc(img.prompt)}') title=复制提示词i class=fas fa-copyibuttonbutton class=ov-btn onclick=addToReference(${img.id}) title=引用到参考图 style=backgroundrgba(124,58,237,0.25);i class=fas fa-imageibuttondivdiv div class=img-infodiv class=img-prompt${esc(img.prompt)}divdiv class=img-footerspan class=img-tag${img.size'2048x2048'}spanspan class=img-time${fmtTime(img.time)}spandivdiv div `}).join(''); } function renderHistory(){ let html = ''; if(state.generatingTasks.length){ html += state.generatingTasks.map(task = ` div class=hist-item style=opacity0.85; div style=width60px;height60px;border-radius10px;backgroundlinear-gradient(135deg,rgba(124,58,237,.2),rgba(147,51,234,.15));displayflex;align-itemscenter;justify-contentcenter;flex-shrink0; div class=hist-spindiv div div class=hist-info style=flex1; div class=hist-prompt${esc(task.prompt)}div div style=displayflex;align-itemscenter;gap8px;margin-top6px; span style=font-size11px;colorvar(--accent-bright);font-weight500;white-spacenowrap; ${task.status==='running'(task.progress10`绘制中 ${task.progress}%`'绘制中...')'处理中...'} span div class=hist-progress-wrap div class=hist-progress-bar style=width${task.progress5}%div div div div div `).join(''); } if(state.images.length){ html += state.images.map(img = { const src = getImageSrc(img.id) ''; return ` div class=hist-item onclick=loadHist(${img.id}) img class=hist-thumb src=${src} alt= onerror=this.style.display='none' div class=hist-info div class=hist-prompt${esc(img.prompt)}div div class=hist-meta${fmtTime(img.time)} · ${img.size'2048x2048'}div div div class=hist-actions button class=ov-btn onclick=event.stopPropagation(); dlImg(${img.id}) title=下载i class=fas fa-downloadibutton button class=ov-btn onclick=event.stopPropagation(); addToReference(${img.id}) title=引用 style=backgroundrgba(124,58,237,0.2);i class=fas fa-imageibutton button class=ov-btn onclick=event.stopPropagation(); deleteHistory(${img.id}) title=删除 style=colorvar(--error);i class=fas fa-trashibutton div div `}).join(''); } if(!html){ $('histList').innerHTML='div style=text-aligncenter;padding40px;colorvar(--text-dim)i class=fas fa-inbox style=font-size28px;displayblock;margin-bottom10px;i暂无记录div'; } else { $('histList').innerHTML = html; } } function updateMeta(){$('metaCost').textContent='¥'+(state.images.lengthstate.price).toFixed(2);} function updateBadge(){const badge=$('histBadge'); if(state.images.length){badge.textContent=state.images.length; badge.style.display='flex';}else badge.style.display='none';} function loadHist(id){const img=state.images.find(i=i.id===id); if(img){$('promptInput').value=img.prompt; $('promptCount').textContent=img.prompt.length+' 字'; closeHist(); toast('已加载历史提示词','ok');}} function openSet(){$('setDrawer').classList.add('open'); $('setBackdrop').classList.add('show');} function closeSet(){$('setDrawer').classList.remove('open'); if(!$('histDrawer').classList.contains('open')) $('setBackdrop').classList.remove('show');} function closeHist(){$('histDrawer').classList.remove('open'); if(!$('setDrawer').classList.contains('open')) $('setBackdrop').classList.remove('show');} let curId=null, curPrompt=''; function preview(id){ const src = getImageSrc(id); if(!src) return; const img = state.images.find(i=i.id===id); curId = id; curPrompt = img img.prompt ''; $('modalImg').src = src; $('modal').classList.add('show'); document.body.style.overflow='hidden'; } function closeModal(){$('modal').classList.remove('show'); document.body.style.overflow=''; curId=null; curPrompt='';} function dlImg(id){ const cached = imageCache.get(id); if(!cached){ toast('图片未找到','err'); return; } if(isWechat()){ showWechatTip(id); return; } autoDownload(cached.record.blob, `img-${id}.png`); toast('下载成功','ok'); } function dlCurrent(){ if(curId) dlImg(curId); } function cpPrompt(t){navigator.clipboard.writeText(t).then(()=toast('提示词已复制','ok')).catch(()=toast('复制失败','err'));} function cpCurrent(){ if(curPrompt) cpPrompt(curPrompt); } function setLoad(v){const btn=$('genBtn'); btn.classList.toggle('loading',v);} function toast(msg,type='ok'){ const t=document.createElement('div'); t.className=`toast ${type}`; t.innerHTML=`i class=fas fa-${type==='ok''check-circle''exclamation-circle'}ispan${msg}span`; $('toastWrap').appendChild(t); setTimeout(()={t.style.opacity='0'; t.style.transform='translate(-50%,16px)'; setTimeout(()=t.remove(),300);},3000); } function esc(t){const d=document.createElement('div'); d.textContent=t; return d.innerHTML;} function fmtTime(ts){if(!ts)return'刚刚';const d=new Date(ts),n=new Date(),diff=(n-d)1000;if(diff60)return'刚刚';if(diff3600)return Math.floor(diff60)+'分前';if(diff86400)return Math.floor(diff3600)+'时前';return `${d.getMonth()+1}${d.getDate()}`;} init(); script body html