!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