8884e100f9
Overlay 190 upstream items (V8.22→8.31) preserving trade-pro theme, registry tests, and design docs. Re-applied full patch set on identical anchors (discovery/env_for/blocks.css/type-mapping/locale-override/ theme_color/frontend entries/test_theme_pro_v3 mods) plus compose pins. Verified: theme suites 35/35; dual-baseline full run 415 passed vs pristine 386+29 ours, FAILED-set diff EMPTY (same 10 Windows env fails). Live: container 8.31.1-dev.1 healthy, hesupipe 192 entries intact, trade-pro build 203 pages 0 errors. Co-Authored-By: Claude Code <noreply@anthropic.com>
405 lines
84 KiB
HTML
405 lines
84 KiB
HTML
<!doctype html>
|
||
<html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||
<title>SiteForge · 网站工作台</title><meta name="description" content="统一管理网站、内容、设计、迁移和构建。">
|
||
<link rel="icon" href="/favicon.ico"><style>:root{--bg:#f6f8fb;--surface:#fff;--ink:#152538;--muted:#697a8c;--line:#e5eaf1;--blue:#245ac6;--blue-hover:#164da9;--blue-soft:#edf3ff;--green:#178264;--amber:#b67c2a;--shadow:0 4px 22px rgba(25,52,95,.045)}*{box-sizing:border-box}html{scroll-behavior:smooth}body{margin:0;background:var(--bg);color:var(--ink);font:14px/1.55 Inter,"Segoe UI","PingFang SC","Microsoft YaHei",system-ui,sans-serif}button,input,textarea,select{font:inherit}button{cursor:pointer}a{text-decoration:none;color:inherit}button:focus-visible,a:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible,summary:focus-visible{outline:3px solid #91b9fc;outline-offset:2px}.app-shell{display:grid;grid-template-columns:242px minmax(0,1fr);min-height:100vh}.nav-side{background:#fff;border-right:1px solid var(--line);height:100vh;position:sticky;top:0;display:flex;flex-direction:column;padding:26px 14px 16px;overflow:auto;z-index:15}.brand{display:flex;align-items:center;gap:12px;padding:2px 12px 29px;border-bottom:1px solid var(--line);margin-bottom:11px}.brand-icon{display:grid;place-items:center;width:39px;height:39px;border-radius:12px;background:#2057bc;color:#fff;font-size:24px;font-weight:750;box-shadow:0 7px 18px #306fbc2b}.brand strong,.brand small{display:block;line-height:1.35}.brand strong{font-size:19px;letter-spacing:-.8px}.brand small{font-size:11px;color:#8491a3;letter-spacing:.2px;margin-top:1px}.nav-group{margin:8px 0 12px}.nav-label{font-size:11px;letter-spacing:.3px;color:#94a0ae;font-weight:700;margin:15px 15px 9px}.nav-item{display:flex;align-items:center;gap:13px;width:100%;border:0;background:transparent;color:#556779;border-radius:9px;padding:11px 13px;text-align:left;font-weight:600;transition:background .15s}.nav-item:hover{background:#f4f7fb;color:#183b75}.nav-item.active{background:var(--blue-soft);color:#1e55b1}.nav-ico{width:20px;min-width:20px;text-align:center;font-size:19px;line-height:1.1;font-weight:400;color:#758397}.nav-item.active .nav-ico{color:#245ac6}.site-mini{margin:0 12px 9px;display:block;max-width:191px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#40536c;font-size:12px;font-weight:650}.advanced{border-top:1px solid var(--line);margin-top:8px;padding-top:16px}.advanced summary{cursor:pointer;list-style:none;display:flex;align-items:center;gap:12px;padding:10px 14px;color:#66778b;font-weight:600;border-radius:9px}.advanced summary::-webkit-details-marker{display:none}.advanced summary:hover{background:#f5f8fc}.chevron{margin-left:auto}.advanced[open] .chevron{transform:rotate(180deg)}.nav-sub{display:block;padding:9px 10px 9px 48px;color:#64748a;font-size:12px;border-radius:7px}.nav-sub:hover{background:#f4f7fb;color:var(--blue)}.side-foot{display:flex;gap:10px;align-items:center;border-top:1px solid var(--line);padding:19px 13px 3px;margin-top:auto}.side-indicator{width:8px;height:8px;border-radius:100%;background:#25ae80}.side-foot strong,.side-foot small{display:block}.side-foot strong{font-size:12px;color:#4f6073}.side-foot small{font-size:11px;color:#9aa7b4;margin-top:2px}.content-shell{min-width:0;display:flex;flex-direction:column}.topbar{height:76px;background:rgba(255,255,255,.96);border-bottom:1px solid var(--line);display:flex;align-items:center;justify-content:space-between;padding:0 36px;gap:16px;position:sticky;top:0;z-index:9}.top-left,.top-right{display:flex;align-items:center;gap:16px}.breadcrumbs{color:#99a4b1;font-size:13px;white-space:nowrap}.breadcrumbs strong{color:#273a50}.crumb-divider{margin:0 9px;color:#cbd4dd}.site-picker-wrap{display:flex;align-items:center;gap:10px;font-size:11px;font-weight:650;color:#8a97a8}.site-picker-wrap select{min-width:180px;max-width:230px;height:38px;padding:0 11px;border:1px solid var(--line);border-radius:8px;color:#3a4c60;background:#fff;font-size:12px}.btn{display:inline-flex;align-items:center;justify-content:center;gap:7px;min-height:39px;border-radius:9px;padding:9px 16px;border:1px solid #dfe6ef;background:#fff;color:#394b5f;font-weight:650;transition:all .15s}.btn:hover{border-color:#b3c8e4;background:#fafcff}.btn-primary{background:var(--blue);border-color:var(--blue);color:#fff;box-shadow:0 4px 12px #225bc328}.btn-primary:hover{background:var(--blue-hover);color:#fff;border-color:var(--blue-hover)}.btn-muted{background:#f5f7fa}.btn-tiny{font-size:12px;min-height:33px;padding:7px 11px}.btn-link{background:none;border:0;color:var(--blue);padding:6px;font-weight:650}.logout,.hamburger{border:0;background:transparent;color:#7c8b9d;font-size:21px;padding:7px;border-radius:7px}.logout:hover,.hamburger:hover{background:#eef3f9}.hamburger{display:none}.main-content{width:min(1190px,100%);padding:37px 37px 70px;margin:0 auto;flex:1}.app-footer{padding:16px 38px;border-top:1px solid var(--line);font-size:11px;color:#a1adba;display:flex;justify-content:space-between;gap:20px}.eyebrow,.overline{font-size:11px;letter-spacing:1.2px;text-transform:uppercase;color:var(--blue);font-weight:750;margin:0 0 10px}.page-heading{display:flex;align-items:start;justify-content:space-between;gap:22px;margin-bottom:26px}.page-heading h1{font-size:28px;letter-spacing:-.8px;line-height:1.2;margin:0 0 10px;font-weight:740}.page-heading p{font-size:13px;color:var(--muted);margin:0;max-width:680px}.section-heading{display:flex;align-items:center;justify-content:space-between;gap:15px;margin:32px 0 16px}.section-heading h2{font-size:17px;letter-spacing:-.3px;margin:0}.section-heading p{color:var(--muted);font-size:12px;margin:5px 0 0}.stats{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:16px;margin:26px 0}.stat{border:1px solid var(--line);background:white;border-radius:12px;padding:21px 23px;box-shadow:var(--shadow);position:relative}.stat .value{font-size:29px;letter-spacing:-1px;font-weight:740;display:block;margin:6px 0 4px}.stat small{color:var(--muted);font-size:12px}.stat .glyph{position:absolute;right:17px;top:16px;background:#f2f5fa;border-radius:9px;width:30px;height:30px;display:grid;place-items:center;color:#6a809c}.panel{background:var(--surface);border:1px solid var(--line);border-radius:13px;padding:23px;box-shadow:var(--shadow);min-width:0}.panel h2,.panel h3{margin:0 0 6px}.panel h2{font-size:17px}.panel h3{font-size:15px}.panel-desc{color:var(--muted);font-size:12px;margin:0 0 19px}.panel-head{display:flex;align-items:start;justify-content:space-between;gap:15px;margin-bottom:20px}.panel-head p{color:var(--muted);font-size:12px;margin:5px 0 0}.grid-2{display:grid;grid-template-columns:1.6fr 1fr;gap:18px}.grid-equal{grid-template-columns:repeat(2,minmax(0,1fr))}.quick-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:15px}.quick-card{background:#fff;border:1px solid var(--line);border-radius:12px;padding:20px;display:flex;align-items:start;gap:13px;text-align:left;min-height:138px;transition:transform .16s,box-shadow .16s}.quick-card:hover,.site-card:hover,.theme-card:hover{transform:translateY(-2px);box-shadow:0 8px 24px #1f478417}.quick-ico{width:42px;height:42px;flex:0 0 42px;display:grid;place-items:center;background:#edf3fd;color:#285bb4;border-radius:10px;font-size:23px}.quick-card h3{margin:2px 0 6px;font-size:15px}.quick-card p{font-size:12px;line-height:1.6;color:var(--muted);margin:0}.quick-card .quick-more{font-size:12px;color:var(--blue);display:block;font-weight:700;margin-top:12px}.site-list{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px}.site-card{padding:21px;border-radius:12px;border:1px solid var(--line);background:#fff;display:flex;gap:17px;align-items:center;text-align:left;transition:all .15s;min-height:127px;position:relative}.site-icon{width:56px;height:56px;display:grid;place-items:center;border-radius:13px;background:#dceafa;color:#204d9a;font-size:24px;font-weight:800;flex:0 0 56px}.site-icon.industrial{background:#e4edeb;color:#2f7561}.site-icon.minimal{background:#f2eee9;color:#8b6348}.site-icon.automotive{background:#e9edf7;color:#333e70}.site-card h3{font-size:15px;margin:0 0 5px}.site-card p{margin:0;color:var(--muted);font-size:12px;line-height:1.6}.site-badges{display:flex;gap:8px;margin-top:9px;font-size:11px;color:#7c8b9d}.arrow{margin-left:auto;font-size:20px;color:#9eb0c8}.pill{display:inline-block;font-size:11px;font-weight:700;color:#3d70bb;background:#eaf2ff;padding:4px 9px;border-radius:6px}.pill.success{color:#187350;background:#e6f6ed}.pill.warn{color:#9a6615;background:#fff3dc}.pill.gray{color:#64748b;background:#f2f4f7}.wizard-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px}.wizard-step{border:1px solid var(--line);background:#fff;border-radius:10px;padding:16px;min-height:135px}.wizard-index{display:grid;place-items:center;width:27px;height:27px;border-radius:50%;font-size:12px;font-weight:800;background:#eff3fa;color:#70819a;margin-bottom:11px}.wizard-step.done .wizard-index{background:#e4f7ed;color:#17805b}.wizard-step.next{border-color:#adc9f8;background:#fafcff}.wizard-step.next .wizard-index{background:#e8f0fe;color:#265ac1}.wizard-step h3{font-size:13px;margin:0 0 5px}.wizard-step p{margin:0;color:#8290a0;font-size:11px}.wizard-step button{padding:0;margin-top:9px;font-weight:700;font-size:11px;border:0;background:none;color:var(--blue)}.task-note{background:#edf4ff;border-radius:11px;border:1px solid #dce9fd;padding:16px 19px;display:flex;align-items:center;justify-content:space-between;gap:18px;margin-bottom:24px}.task-note strong{font-size:13px}.task-note p{margin:4px 0 0;font-size:12px;color:#667c99}.empty{background:#fff;border:1px dashed #d6dfeb;border-radius:12px;padding:42px;text-align:center}.empty .empty-symbol{font-size:30px;color:#a0b5d6}.empty h3{font-size:17px;margin:14px 0 6px}.empty p{font-size:12px;color:var(--muted);margin:0 0 18px}.table-wrap{width:100%;overflow-x:auto;border:1px solid var(--line);border-radius:11px;background:#fff}.data-table{width:100%;border-collapse:collapse;font-size:12px}.data-table th{text-align:left;background:#fafbfe;padding:13px 15px;color:#768699;font-weight:650;white-space:nowrap}.data-table td{padding:14px 15px;border-top:1px solid #eef1f6;vertical-align:middle}.data-table td strong{color:#2a3a51;font-size:13px}.data-table .action-cell{white-space:nowrap}.muted{color:var(--muted)}.toolbar{display:flex;gap:12px;align-items:center;margin:0 0 20px}.toolbar input{max-width:300px}.toolbar select{max-width:175px}.input,input,textarea,select{border:1px solid #d9e1ed;border-radius:8px;background:#fff;padding:10px 12px;color:#324357;min-width:0}input:focus,textarea:focus,select:focus{border-color:#4d8ce8;outline:none;box-shadow:0 0 0 3px #4d8ce820}.field-grid,.form-fields{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:17px}.field-grid label,.form-fields>label{display:grid;gap:7px;color:#3d5067;font-size:12px;font-weight:650}.full{grid-column:1 / -1}.field-grid textarea,.form-fields textarea{width:100%;resize:vertical}.form-actions{display:flex;justify-content:flex-start;gap:10px;margin-top:20px}.optional{font-size:10px;color:#9ca7b7;font-weight:400}.choice-cards{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:14px}.theme-card{border:1px solid var(--line);background:#fff;border-radius:12px;overflow:hidden;transition:all .15s}.theme-preview{height:126px;position:relative;padding:17px;display:flex;align-items:flex-end;font-size:22px;font-weight:750;letter-spacing:-.9px}.theme-preview.automotive{background:linear-gradient(125deg,#142b4b,#3b527c);color:#e5d8aa}.theme-preview.industrial{background:linear-gradient(125deg,#d4e2df,#899f9c);color:#254f49}.theme-preview.corporate{background:linear-gradient(125deg,#e4eefe,#9ebff1);color:#17437d}.theme-preview.minimal{background:linear-gradient(125deg,#f4efe8,#dcd3c8);color:#6f5944}.theme-desc{padding:16px}.theme-desc h3{margin:0 0 7px;font-size:13px}.theme-desc p{margin:0 0 12px;font-size:11px;color:var(--muted)}.help-hint{background:#f7f9fc;border:1px solid #e6ecf4;border-radius:10px;padding:14px 18px;color:#61748a;font-size:12px}.help-hint strong{color:#304960}.import-box{border:2px dashed #cbdcf6;background:#fafcff;border-radius:12px;padding:24px;margin:18px 0}.import-box input{width:100%;margin:11px 0}.message{font-size:12px;border-radius:8px;padding:14px;background:#f6f8fb;white-space:pre-wrap;overflow:auto;max-height:240px;color:#496075}.build-row{display:flex;gap:15px;align-items:center;border-top:1px solid var(--line);padding:15px 0}.build-row:first-child{border:0}.build-meta{flex:1;min-width:0}.build-meta h3{margin:0 0 4px;font-size:13px}.build-meta p{font-size:11px;color:var(--muted);margin:0}.inline-actions{display:flex;gap:8px;flex-wrap:wrap}.modal{border:1px solid var(--line);border-radius:15px;max-height:90vh;max-width:calc(100vw - 25px);width:570px;box-shadow:0 24px 80px #0e213d30;padding:0;overflow:auto}.modal::backdrop{background:#101e3385}.modal-inner{padding:29px}.modal-top{display:flex;justify-content:space-between;align-items:start;gap:14px;margin-bottom:24px}.modal-top h2{font-size:23px;letter-spacing:-.6px;margin:0 0 5px}.subtle{margin:0;color:var(--muted);font-size:12px}.x-btn{font-size:26px;background:none;border:0;color:#91a0b0}.modal-actions{display:flex;justify-content:flex-end;gap:9px;border-top:1px solid var(--line);margin-top:23px;padding-top:21px}.form-expand{border:1px solid var(--line);border-radius:8px;padding:14px}.form-expand summary{cursor:pointer;font-weight:650;color:#5e748c}.form-expand .form-fields{margin-top:12px}.toast{position:fixed;right:25px;bottom:22px;z-index:90;background:#273f59;color:#fff;padding:13px 18px;border-radius:11px;box-shadow:0 8px 36px #0002;font-size:13px;max-width:450px;visibility:hidden;opacity:0;transform:translateY(13px);transition:all .2s}.toast.show{visibility:visible;opacity:1;transform:translateY(0)}.toast.bad{background:#a83b47}.skeleton{padding:40px;color:#8ea0b2}.mobile-backdrop{display:none}@media(max-width:1180px){.choice-cards{grid-template-columns:repeat(2,minmax(0,1fr))}.wizard-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.main-content{padding:30px 25px 65px}.topbar{padding:0 25px}.site-list{grid-template-columns:1fr}}@media(max-width:880px){.app-shell{display:block}.nav-side{position:fixed;top:0;left:0;transform:translateX(-100%);width:252px;box-shadow:10px 0 40px #192f4830;transition:transform .2s}.nav-side.open{transform:translateX(0)}.hamburger{display:block}.mobile-backdrop.open{display:block;position:fixed;inset:0;background:#10203864;z-index:12}.topbar{padding:0 16px}.site-picker-wrap>span{display:none}.stats{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-2,.grid-equal{grid-template-columns:1fr}.quick-grid{grid-template-columns:1fr}.quick-card{min-height:100px}.site-picker-wrap select{max-width:190px;min-width:110px}}@media(max-width:570px){.topbar{height:66px}.breadcrumbs{display:none}.top-right{gap:7px}.site-picker-wrap select{max-width:150px;width:148px}.top-right .btn{font-size:11px;padding:8px 10px}.main-content{padding:25px 16px 45px}.page-heading{flex-direction:column}.page-heading h1{font-size:23px}.stats{gap:10px}.stat{padding:17px}.stat .value{font-size:25px}.wizard-grid{grid-template-columns:1fr 1fr}.wizard-step{padding:12px;min-height:135px}.site-list,.field-grid,.form-fields,.choice-cards{grid-template-columns:1fr}.full{grid-column:1}.toolbar{flex-wrap:wrap}.toolbar input,.toolbar select{max-width:none;width:100%}.task-note{align-items:start;flex-direction:column}.app-footer{padding:13px 16px}.app-footer span:last-child{display:none}}@media(prefers-reduced-motion:reduce){*,*:before,*:after{transition:none!important;scroll-behavior:auto!important}}
|
||
|
||
/* V8.12: task progress and dangerous actions, consistent focus and mobile sizing */
|
||
.btn-danger{border:1px solid #f1c3c7;color:#a92840;background:#fff5f6}.btn-danger:hover{background:#feecee}.btn:disabled{opacity:.55;cursor:not-allowed}.build-row{min-width:0}.build-meta{overflow-wrap:anywhere}.site-card:focus-visible,.quick-card:focus-visible{outline:3px solid #80aaf6;outline-offset:2px}@media(max-width:570px){.build-row{align-items:flex-start;flex-wrap:wrap}.build-row>.inline-actions{width:100%;padding-left:0}.site-picker-wrap select{max-width:130px}}
|
||
|
||
/* V8.27 guided creation and actionable journey. Keep the original admin styling. */
|
||
[hidden]{display:none!important}
|
||
.sf-create-dialog .modal-inner{max-width:650px}.sf-create-dialog .modal-top h2{margin-bottom:4px}
|
||
.sf-create-progress{list-style:none;display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px;margin:2px 0 22px;padding:0}
|
||
.sf-create-progress li{border-bottom:3px solid #dde6f1;padding:9px 2px 11px;color:#8391a3;text-align:center;font-size:12px}
|
||
.sf-create-progress li[aria-current=step]{border-color:var(--blue);color:var(--blue);font-weight:750}
|
||
.sf-create-step h3{font-size:19px;margin:10px 0}.sf-guide-desc{font-size:13px;color:var(--muted);line-height:1.7;margin:6px 0 24px}
|
||
.sf-required{font-size:11px;color:#aa4c3f}.sf-create-summary{border:1px solid #dbe5f0;border-radius:12px;background:#f8fafc;padding:13px 18px;margin:15px 0 22px}
|
||
.sf-create-summary p{margin:10px 0;overflow-wrap:anywhere}.sf-guide-safety{border-radius:10px;background:#eff5ff;color:#4a6282;padding:13px 15px;line-height:1.7;margin:18px 0}
|
||
.sf-create-error{color:#ab2537;background:#fff2f2;border:1px solid #f4cbd0;padding:11px;border-radius:8px}
|
||
.sf-journey-prompt,.sf-journey-intro{display:flex;align-items:center;justify-content:space-between;gap:22px;background:white;border:1px solid var(--line);border-radius:14px;padding:20px 24px;margin:16px 0}
|
||
.sf-journey-prompt p,.sf-journey-intro p{line-height:1.65;color:var(--muted);margin:7px 0 0}.sf-journey-intro h2{font-size:24px;margin:5px 0}.sf-journey-eyebrow{color:var(--muted);font-size:12px}
|
||
.sf-journey-progress{display:grid;text-align:center;border-radius:12px;background:#eff5ff;padding:13px 20px;min-width:130px}
|
||
.sf-journey-progress strong{font-size:28px;color:var(--blue)}.sf-journey-progress span{font-size:11px;color:var(--muted)}
|
||
.sf-journey-list{margin:16px 0 22px;padding:0;list-style:none;display:grid;gap:10px}
|
||
.sf-journey-step{display:grid;grid-template-columns:38px minmax(0,1fr) auto;gap:18px;align-items:center;background:white;border:1px solid var(--line);border-radius:13px;padding:17px 19px}
|
||
.sf-journey-step.is-next{border-color:#aac1f2;box-shadow:0 0 0 2px #eff5ff}
|
||
.sf-journey-number{width:34px;height:34px;border:1px solid #dde5ef;border-radius:50%;background:#f4f7fa;display:grid;place-items:center;color:#6b788b;font-weight:700}
|
||
.sf-journey-step.is-done .sf-journey-number{background:#e7f7f0;color:#16835a;border-color:#cbebde}
|
||
.sf-journey-title{display:flex;align-items:center;gap:12px;flex-wrap:wrap}
|
||
.sf-journey-title strong{font-size:15px}.sf-journey-title span{color:#6b7b8d;font-size:11px}
|
||
.sf-journey-step p{color:var(--muted);font-size:12px;line-height:1.65;margin:6px 0 0}
|
||
.sf-journey-after,.sf-journey-notice{background:white;border:1px solid var(--line);border-radius:13px;padding:20px 22px;margin:16px 0}
|
||
.sf-journey-after h2{font-size:17px;margin:0 0 14px}.sf-glossary-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}
|
||
.sf-glossary-grid p{margin:0;padding:12px;background:#f7f9fc;border-radius:9px}.sf-glossary-grid strong{display:block;margin-bottom:6px}.sf-glossary-grid span{display:block;color:var(--muted);font-size:12px;line-height:1.7}
|
||
.sf-journey-notice{background:#fffdf6;color:#705d35;line-height:1.8}
|
||
@media(max-width:680px){.sf-journey-step{grid-template-columns:34px minmax(0,1fr);gap:11px}.sf-journey-step>.btn{grid-column:2;width:max-content}.sf-journey-intro,.sf-journey-prompt{flex-direction:column;align-items:stretch}.sf-glossary-grid{grid-template-columns:1fr}.sf-journey-progress{max-width:190px}}
|
||
</style><style>.demo-flag{position:fixed;z-index:999;top:6px;left:50%;transform:translateX(-50%);font-size:11px;line-height:1.4;color:#755422;background:#fff2d5;border:1px solid #f2d6a1;border-radius:20px;padding:4px 14px;pointer-events:none;max-width:90vw;text-align:center}@media(max-width:700px){.demo-flag{top:auto;bottom:8px}}</style></head>
|
||
<body>
|
||
<div class="app-shell">
|
||
<aside class="nav-side" id="sideMenu">
|
||
<a href="/" class="brand" aria-label="SiteForge 工作台"><span class="brand-icon">S</span><span><strong>SiteForge</strong><small>网站生产工厂</small></span></a>
|
||
<div class="nav-group"><div class="nav-label">工作空间</div>
|
||
<button data-view="home" class="nav-item active"><span class="nav-ico">▦</span><span>工作台</span></button>
|
||
<button data-view="sites" class="nav-item"><span class="nav-ico">▤</span><span>我的网站</span></button>
|
||
</div>
|
||
<div class="nav-group" id="websiteGroup"><div class="nav-label">当前网站</div><div id="sideSiteName" class="site-mini">请先选择网站</div>
|
||
<button data-view="overview" class="nav-item"><span class="nav-ico">◎</span><span>网站概览</span></button>
|
||
<button data-view="guide" class="nav-item"><span class="nav-ico">✓</span><span>建站操作指引</span></button>
|
||
<button data-view="content" class="nav-item"><span class="nav-ico">▧</span><span>内容与页面</span></button>
|
||
<button data-view="design" class="nav-item"><span class="nav-ico">◈</span><span>外观与设计</span></button>
|
||
<button data-view="import" class="nav-item"><span class="nav-ico">⇩</span><span>导入与迁移</span></button>
|
||
<button data-view="build" class="nav-item"><span class="nav-ico">↗</span><span>构建与交付</span></button>
|
||
<button data-view="languages" class="nav-item"><span class="nav-ico">文</span><span>多语言管理</span></button>
|
||
<button data-view="settings" class="nav-item"><span class="nav-ico">⚙</span><span>网站设置</span></button>
|
||
</div>
|
||
<details class="advanced"><summary><span class="nav-ico">⌘</span>高级工具 <span class="chevron">⌄</span></summary>
|
||
<a href="/production-v8" class="nav-sub">数据导入与模板管理</a>
|
||
<a href="/operations" class="nav-sub">迁移复核与发布审批</a>
|
||
<a href="/theme-gallery" class="nav-sub">完整主题预览</a>
|
||
<a href="/factory-studio" class="nav-sub">高级网站设计</a>
|
||
<a href="/classic" class="nav-sub">旧版管理功能</a>
|
||
</details>
|
||
<div class="side-foot"><span class="side-indicator"></span><span><strong>系统运行中</strong><small id="appVersion">读取版本…</small></span></div>
|
||
</aside>
|
||
<div class="content-shell">
|
||
<header class="topbar">
|
||
<div class="top-left"><button class="hamburger" id="menuToggle" aria-label="展开菜单" aria-expanded="false">☰</button><div class="breadcrumbs"><span>工作空间</span><span class="crumb-divider">/</span><strong id="breadcrumbTitle">工作台</strong></div></div>
|
||
<div class="top-right"><label class="site-picker-wrap"><span>正在管理</span><select id="sitePicker" aria-label="选择当前网站"><option value="">请选择网站</option></select></label><button id="newSiteTop" class="btn btn-primary">+ 创建网站</button><button class="logout" id="logoutBtn" title="退出登录" aria-label="退出登录">↪</button></div>
|
||
</header>
|
||
<main class="main-content" id="mainContent" tabindex="-1"><div class="skeleton">正在读取工作空间…</div></main>
|
||
<footer class="app-footer"><span>SiteForge · 让网站制作有清晰的下一步</span><span>正式发布前请完成内容与 SEO 检查</span></footer>
|
||
</div>
|
||
</div>
|
||
<div class="mobile-backdrop" id="mobileBackdrop"></div><div id="toast" class="toast" role="status" aria-live="polite"></div>
|
||
<dialog id="siteModal" class="modal sf-create-dialog"><form id="siteForm" class="modal-inner" novalidate>
|
||
<div class="modal-top"><div><p class="overline">开始建立网站</p><h2>创建新网站</h2><p class="subtle" id="createProgressText" aria-live="polite">第 1 步,共 3 步 · 选择建站方式</p></div><button type="button" data-close="siteModal" class="x-btn" aria-label="关闭创建窗口">×</button></div>
|
||
<ol class="sf-create-progress" aria-label="创建网站进度"><li data-create-indicator="0" aria-current="step">基本信息</li><li data-create-indicator="1">选择风格</li><li data-create-indicator="2">确认创建</li></ol>
|
||
<section class="sf-create-step" data-create-step="0"><h3>你想制作什么网站?</h3><p class="sf-guide-desc">先选目的,再填名称,后续内容都能修改。</p>
|
||
<div class="form-fields"><label class="full">网站名称 <span class="sf-required">必填</span><input name="name" required maxlength="150" placeholder="例如:HESU Auto 全球官网"></label>
|
||
<label class="full">建站方式<select name="mode"><option value="new">全新制作网站</option><option value="migration">迁移已有 WordPress 网站</option><option value="hybrid">在旧内容基础上重新设计</option></select></label></div></section>
|
||
<section class="sf-create-step" data-create-step="1" hidden><h3>选择一种网站风格</h3><p class="sf-guide-desc">这里只是起始模板,建好网站仍可以更换。</p>
|
||
<div class="form-fields"><label class="full">行业主题<select name="theme"><option value="automotive">汽车与出行</option><option value="industrial">工业与制造</option><option value="corporate" selected>企业品牌</option><option value="minimal">极简展示</option></select></label>
|
||
<label class="full">一句话介绍 <span class="optional">选填</span><input name="tagline" placeholder="例如:全球汽车出口与服务"></label></div></section>
|
||
<section class="sf-create-step" data-create-step="2" hidden><h3>确认网站信息</h3><p class="sf-guide-desc">创建只会生成一个网站项目,不会自动发布到互联网。</p>
|
||
<div class="sf-create-summary" id="createSummary"></div>
|
||
<div class="form-fields"><label class="full">正式域名 <span class="optional">可稍后设置</span><input name="domain" type="url" placeholder="https://www.example.com"></label></div>
|
||
<div class="sf-guide-safety">创建完成后建议先添加真实内容,再编辑页面,最后预览、检查和发布。</div></section>
|
||
<p id="createError" class="sf-create-error" role="alert" hidden></p>
|
||
<div class="modal-actions"><button type="button" data-close="siteModal" class="btn btn-muted">取消</button><button type="button" id="createBack" class="btn btn-muted" hidden>上一步</button><button type="button" id="createNext" class="btn btn-primary">下一步:选择风格 →</button><button type="submit" id="createSubmit" class="btn btn-primary" hidden>确认创建网站</button></div>
|
||
</form></dialog>
|
||
<dialog id="entryModal" class="modal"><form id="entryForm" class="modal-inner"><div class="modal-top"><div><p class="overline">SITE CONTENT</p><h2 id="entryModalTitle">添加内容</h2><p class="subtle">按业务填写信息即可。详细布局可以进入可视化编辑器修改。</p></div><button type="button" data-close="entryModal" class="x-btn" aria-label="关闭">×</button></div><input type="hidden" name="entry_id"><div class="form-fields"><label>内容类型<select name="type"><option value="page">网站页面</option><option value="product">产品</option><option value="vehicle">车型</option><option value="post">新闻文章</option><option value="brand">品牌</option><option value="case">案例</option><option value="service">服务项目</option></select></label><label>状态<select name="status"><option value="published">可参与网站构建</option><option value="draft">内容草稿</option></select></label><label class="full">标题<input name="title" required maxlength="250" placeholder="输入页面或产品标题"></label><label class="full">页面地址 <span class="optional">新内容可留空</span><input name="route" placeholder="/products/example/"></label><label class="full">简短介绍<textarea name="summary" rows="2" placeholder="显示在列表和摘要中的介绍"></textarea></label><label class="full">正文内容 <span class="optional">可选</span><textarea name="body" rows="5" placeholder="填写业务正文,复杂排版请使用可视化设计器"></textarea></label><details class="form-expand full"><summary>SEO 设置(进阶)</summary><div class="form-fields"><label class="full">SEO 标题<input name="seo_title" placeholder="默认使用内容标题"></label><label class="full">Meta 描述<textarea name="seo_description" rows="2"></textarea></label></div></details></div><div class="modal-actions"><button type="button" data-close="entryModal" class="btn btn-muted">取消</button><button type="submit" class="btn btn-primary">保存内容</button></div></form></dialog>
|
||
<div class="demo-flag">离线交互演示 · 不会保存到服务器,也不会修改真实网站</div>
|
||
<script>
|
||
window.history.pushState=()=>{};window.history.replaceState=()=>{};
|
||
window.mockSites=[];window.mockCreateCount=0;
|
||
window.fetch=async function(url,options={}){
|
||
const path=String(url).split('?')[0];const method=options.method||'GET';let result={};
|
||
if(path==='/api/auth/me')result={csrf:'DEMO'};
|
||
else if(path==='/api/health')result={version:'8.27.0-dev.1 · 演示模式'};
|
||
else if(path==='/api/sites' && method==='GET')result=window.mockSites;
|
||
else if(path==='/api/sites' && method==='POST'){
|
||
const data=JSON.parse(options.body);window.mockCreateCount++;
|
||
result={id:'demo-id',name:data.name,theme:data.theme,mode:data.mode,domain:data.domain,tagline:data.tagline,updated_at:'2026-01-01T00:00:00Z',entries_count:1,build_count:0};window.mockSites=[result];
|
||
}else if(path==='/api/sites/demo-id')result=window.mockSites[0];
|
||
else if(path==='/api/sites/demo-id/entries')result=[{id:'home',source:'starter',status:'published',route:'/',type:'page',title:'首页'}];
|
||
else if(path==='/api/sites/demo-id/builds')result=[];
|
||
else if(path==='/api/enterprise/sites/demo-id/jobs')result=[];
|
||
else if(path==='/api/production/sites/demo-id/delivery-status')result={jobs:[],history:[],configured:{}};
|
||
else return {status:404,ok:false,json:async()=>({detail:'演示未接入真实服务器:'+path})};
|
||
return {status:method==='POST'?201:200,ok:true,json:async()=>result};
|
||
};
|
||
</script>
|
||
<script>/* V8.27: pure journey model. Display only server-observed milestones as complete. */
|
||
(function(root,factory){
|
||
const api=factory();
|
||
if(typeof module==='object'&&module.exports)module.exports=api;
|
||
else root.SiteForgeJourney=api;
|
||
})(typeof window!=='undefined'?window:this,function(){
|
||
'use strict';
|
||
const successful=new Set(['complete','completed','done','success']);
|
||
const active=new Set(['queued','running','retrying']);
|
||
function parse(t){const d=Date.parse(t||'');return Number.isFinite(d)?d:null}
|
||
function plan(input){
|
||
const site=input?.site||null, entries=input?.entries||[], builds=input?.builds||[], jobs=input?.jobs||[], history=input?.delivery?.history||[];
|
||
if(!site)return {ready:false,steps:[],next:null,completed:0,hasBuild:false,live:false};
|
||
const userEntries=entries.filter(e=>e.source!=='starter' && e.status!=='draft');
|
||
const manuallyEditedHome=entries.some(e=>e.route==='/'&&e.source!=='starter'&&e.status!=='draft');
|
||
const hasContent=userEntries.length>0||manuallyEditedHome;
|
||
const lastBuild=builds.filter(x=>successful.has(x.status)).sort((a,b)=>(parse(b.created_at)||0)-(parse(a.created_at)||0))[0]||null;
|
||
const anyRunning=jobs.some(x=>active.has(x.state));
|
||
const changeTimes=[site.updated_at,...entries.filter(e=>e.status!=='draft').map(e=>e.updated_at)].map(parse).filter(x=>x!==null);
|
||
const changedAfterBuild=!!lastBuild&&changeTimes.some(x=>{const built=parse(lastBuild.created_at);return built!==null&&x>built+1000});
|
||
const hasBuild=!!lastBuild;
|
||
const live=history.some(x=>x.target==='production' && !!x.release_id && !x.is_rollback);
|
||
const domain=Boolean((site.domain||'').trim());
|
||
const steps=[
|
||
{id:'site',label:'创建网站',hint:'网站已经建立,基础资料可以随时修改。',view:'settings',done:true,optional:false},
|
||
{id:'content',label:'准备网站内容',hint:hasContent?'已有人工添加的正式内容。':'添加页面、产品或导入旧站内容。新建时自带的示例首页不代表内容已准备好。',view:site.mode==='migration'?'import':'content',done:hasContent,optional:false},
|
||
{id:'design',label:'调整网站样式',hint:'可视化修改页面、页眉、导航和手机布局;设计需人工确认。',view:'design',done:false,optional:true},
|
||
{id:'build',label:'生成并预览网站',hint:anyRunning?'后台正在生成,请在构建页面查看结果。':!hasBuild?'生成静态页面后,再检查预览与质量报告。':changedAfterBuild?'已有构建版本,但内容可能已更新,请检查是否需要重新构建。':'已有构建版本;仍需人工检查预览、图片和 SEO。',view:'build',done:hasBuild&&!changedAfterBuild,optional:false},
|
||
{id:'review',label:'审核并正式上线',hint:live?'存在正式发布记录,请核对目标网址是否正确显示。':'正式上线前需要发布条件检查、独立审批与服务器部署。',view:'build',done:live,optional:false}
|
||
];
|
||
const next=steps.find(s=>!s.optional&&!s.done)||steps[4];
|
||
return {ready:true,steps,next,completed:steps.filter(s=>!s.optional&&s.done).length,hasBuild,changedAfterBuild,live,domain,anyRunning};
|
||
}
|
||
return {plan};
|
||
});
|
||
</script>
|
||
<script>'use strict';
|
||
/* SiteForge simplified workspace: native APIs, no parallel business backend. */
|
||
const $=(s,root=document)=>root.querySelector(s);
|
||
const esc=x=>String(x??'').replace(/[&<>"']/g,ch=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[ch]));
|
||
const themeName={automotive:'汽车与出行',industrial:'工业与制造',corporate:'企业品牌',minimal:'极简展示'};
|
||
const typeName={page:'页面',product:'产品',vehicle:'车型',post:'新闻',brand:'品牌',case:'案例',service:'服务'};
|
||
const fmtDate=t=>{if(!t)return '—';const d=new Date(t);return Number.isNaN(d.getTime())?String(t).slice(0,16):d.toLocaleDateString('zh-CN',{year:'numeric',month:'2-digit',day:'2-digit'})};
|
||
const sfSiteKey='siteforge:selected-site';
|
||
const sfRemember=id=>{try{if(id)localStorage.setItem(sfSiteKey,id)}catch{}};
|
||
const state={sites:[],site:null,entries:[],builds:[],jobs:[],view:'home',csrf:'',search:'',typ:'',loading:false,job:null,selectionToken:0,building:false,delivery:{jobs:[],history:[],configured:{}},language:{group:null,sites:[],units:[]},languageGroups:[],languageEntries:[]};
|
||
const headings={home:'工作台',sites:'我的网站',guide:'建站操作指引',overview:'网站概览',content:'内容与页面',design:'外观与设计',import:'导入与迁移',build:'构建与交付',languages:'多语言管理',settings:'网站设置'};
|
||
async function api(url,method='GET',data=null,extraHeaders={}){
|
||
const headers={...extraHeaders};if(method!=='GET'&&method!=='HEAD')headers['X-SiteForge-CSRF']=state.csrf;
|
||
let body=data;if(data!=null && !(data instanceof FormData)){headers['Content-Type']='application/json';body=JSON.stringify(data)}
|
||
const res=await fetch(url,{method,headers,body,credentials:'same-origin'});
|
||
if(res.status===401){location.href='/login';throw Error('登录已过期,请重新登录')}
|
||
let value;try{value=await res.json()}catch{throw Error('服务器响应异常(HTTP '+res.status+')')}
|
||
if(!res.ok){const ex=Error(typeof value.detail==='string'?value.detail:JSON.stringify(value.detail||value));ex.status=res.status;throw ex}return value;
|
||
}
|
||
function toast(msg,bad=false){const el=$('#toast');el.textContent=msg;el.classList.toggle('bad',bad);el.classList.add('show');clearTimeout(el.timer);el.timer=setTimeout(()=>el.classList.remove('show'),4000)}
|
||
function notifyError(error){console.error(error);toast(error?.message||String(error),true)}
|
||
function htmlHeading(title,desc,actions=''){return `<div class="page-heading"><div><p class="overline">SITEFORGE / ${esc(headings[state.view]||'WORKSPACE')}</p><h1>${esc(title)}</h1><p>${esc(desc)}</p></div><div class="inline-actions">${actions}</div></div>`}
|
||
function actionButton(text,action=''){return `<button type="button" class="btn btn-primary" data-action="${action}">${esc(text)}</button>`}
|
||
function stat(title,num,icon){return `<div class="stat"><span class="glyph">${icon}</span><small>${title}</small><strong class="value">${esc(num)}</strong></div>`}
|
||
function emptyState(title,description,button=''){return `<div class="empty"><div class="empty-symbol">◇</div><h3>${esc(title)}</h3><p>${esc(description)}</p>${button}</div>`}
|
||
function nav(button){return `data-action="navigate" data-view="${button}"`}
|
||
// Business navigation is a shareable, browser-history-aware URL; selection is still authorized by /api/sites.
|
||
const siteUrl=(view,site)=>{const q=new URLSearchParams();if(site)q.set('site',site);if(view&&(view!=='home'||site))q.set('view',view);return '/'+(q.size?'?'+q.toString():'')};
|
||
function rememberView(replace=false){const url=siteUrl(state.view,state.site?.id);history[replace?'replaceState':'pushState']({site:state.site?.id||null,view:state.view},'',url)}
|
||
function activeJobs(){return state.jobs.filter(j=>['queued','running','retrying'].includes(j.state))}
|
||
function buildStatusLabel(stateCode){return ({queued:'等待 Worker',running:'正在生成',retrying:'重试中',done:'已完成',failed:'失败',cancelled:'已取消'})[stateCode]||stateCode||'未知'}
|
||
function buildReadyStep(){return state.entries.some(e=>e.status!=='draft'&&e.type!=='page')||state.entries.some(e=>e.status!=='draft'&&e.type==='page')}
|
||
|
||
function siteCard(s){return `<button class="site-card" data-action="select-site" data-id="${esc(s.id)}"><span class="site-icon ${esc(s.theme)}">${esc((s.name||'S').slice(0,1))}</span><span><h3>${esc(s.name)}</h3><p>${esc(s.domain||'尚未设置正式域名')}</p><span class="site-badges"><span>${esc(themeName[s.theme]||'企业网站')}</span><span>·</span><span>${Number(s.entries_count)||0} 条内容</span><span>·</span><span>${Number(s.build_count)||0} 次构建</span></span></span><span class="arrow">›</span></button>`}
|
||
function overviewShortcut(view,icon,title,desc){return `<button class="quick-card" ${nav(view)}><span class="quick-ico">${icon}</span><span><h3>${title}</h3><p>${desc}</p><span class="quick-more">前往操作 →</span></span></button>`}
|
||
function home(){const sites=state.sites;const total=sites.reduce((n,s)=>n+(+s.entries_count||0),0);const builds=sites.reduce((n,s)=>n+(+s.build_count||0),0);
|
||
return htmlHeading('欢迎回来','管理和制作企业网站,从这里开始。',actionButton('+ 创建新网站','new-site'))+
|
||
`<div class="stats">${stat('网站项目',sites.length,'▤')}${stat('内容总数',total,'▧')}${stat('构建版本',builds,'↗')}${stat('可用主题',4,'◈')}</div>`+
|
||
`<div class="section-heading"><div><h2>您想先做什么?</h2><p>按使用目的进入对应页面,不需要了解技术术语。</p></div></div><div class="quick-grid">`+
|
||
overviewShortcut('sites','+','创建或管理网站','从零搭建网站,或者接着制作已有网站。')+
|
||
overviewShortcut('import','⇩','迁移旧站 / 导入数据','导入 WordPress 或 Excel 产品资料。')+
|
||
overviewShortcut('design','◈','更换网站设计','选择行业主题,编辑网页与全站外观。')+
|
||
`</div><div class="section-heading"><div><h2>最近的网站</h2><p>点击网站继续制作。</p></div><button class="btn-link" ${nav('sites')}>查看全部网站 →</button></div>`+
|
||
(sites.length?`<div class="site-list">${sites.slice(0,4).map(siteCard).join('')}</div>`:emptyState('还没有网站项目','创建一个网站,选择主题后即可开始添加页面、产品和新闻。',actionButton('+ 创建第一个网站','new-site')));
|
||
}
|
||
function sites(){return htmlHeading('我的网站','统一管理所有网站项目,每个网站的数据、主题和构建记录彼此独立。',actionButton('+ 创建网站','new-site'))+
|
||
(state.sites.length?`<div class="site-list">${state.sites.map(siteCard).join('')}</div>`:emptyState('暂时没有网站','选择行业主题,开始制作您的第一个网站。',actionButton('创建网站','new-site')));
|
||
}
|
||
function currentSite(){return !!state.site}
|
||
function noSite(){return htmlHeading('请先选择一个网站','选择现有网站或创建新网站,才能继续进行内容编辑、迁移和构建。')+emptyState('尚未选择网站','点击右上角选择网站,或直接创建一个新的企业网站。',actionButton('+ 创建网站','new-site'))}
|
||
function latestDone(){return state.builds.find(b=>['done','complete','success','completed'].includes(b.status))||null}
|
||
function overview(){
|
||
const s=state.site;if(!s)return noSite();
|
||
const journey=window.SiteForgeJourney?.plan({site:s,entries:state.entries,builds:state.builds,jobs:state.jobs,delivery:state.delivery});
|
||
const next=journey?.next||{label:'查看操作指引',hint:'检查各步骤是否已经完成。',view:'guide'};
|
||
const contentCount=state.entries.filter(e=>e.source!=='starter').length;
|
||
return htmlHeading(s.name,'所有网站操作都从这里开始;保存内容、生成网站和正式上线是不同的步骤。',`<button type="button" class="btn btn-primary" data-action="navigate" data-view="guide">查看完整操作指引 →</button>`)+
|
||
`<section class="sf-journey-intro"><div><span class="sf-journey-eyebrow">建议下一步</span><h2>${esc(next.label)}</h2><p>${esc(next.hint)}</p></div><button type="button" class="btn btn-primary" data-action="navigate" data-view="${esc(next.view)}">现在去做 →</button></section>`+
|
||
`<div class="stats">${stat('人工添加的内容',contentCount,'▧')}${stat('静态构建版本',state.builds.length,'↗')}${stat('当前主题',themeName[s.theme]||'企业品牌','◈')}${stat('正式域名',s.domain?'已填写':'未设置','◎')}</div>`+
|
||
`<div class="section-heading"><div><h2>最常用的三项操作</h2><p>不需要了解技术细节,选择你想完成的事情即可。</p></div></div>`+
|
||
`<div class="quick-grid">${overviewShortcut('content','▧','修改文字和产品','填写网站内容,修改已经创建的页面。')}${overviewShortcut('design','◈','调整网页外观','编辑图片、版式、导航和手机端。')}${overviewShortcut('build','↗','生成网站并预览','先检查生成效果,再进入审批和上线。')}</div>`+
|
||
`<div class="sf-journey-notice" role="note"><strong>关于上线</strong>:页面编辑和保存不会自动修改线上网站。上线仍需生成静态版本、检查、独立审批和部署。</div>`;
|
||
}
|
||
function guide(){
|
||
if(!currentSite())return noSite();
|
||
const journey=window.SiteForgeJourney?.plan({site:state.site,entries:state.entries,builds:state.builds,jobs:state.jobs,delivery:state.delivery});
|
||
if(!journey)return htmlHeading('建站操作指引','操作引导暂时无法加载,请刷新网页。');
|
||
const checked=journey.completed;
|
||
const rows=journey.steps.map((step,i)=>{
|
||
const label=step.optional?'可选:随时完善':step.done?'已记录完成':step.id===journey.next.id?'建议现在进行':'等待前面的步骤';
|
||
return `<li class="sf-journey-step ${step.done?'is-done':''} ${step.id===journey.next.id?'is-next':''}">
|
||
<span class="sf-journey-number" aria-hidden="true">${step.done?'✓':i+1}</span>
|
||
<div><div class="sf-journey-title"><strong>${esc(step.label)}</strong><span>${esc(label)}</span></div>
|
||
<p>${esc(step.hint)}</p></div>
|
||
<button type="button" class="btn ${step.id===journey.next.id?'btn-primary':'btn-muted'}" data-action="navigate" data-view="${esc(step.view)}">${step.id===journey.next.id?'开始操作':'查看 / 修改'} →</button>
|
||
</li>`;
|
||
}).join('');
|
||
return htmlHeading('建站操作指引','按照实际操作顺序完成网站制作,系统只根据已保存的数据与任务记录显示进度。')+
|
||
`<section class="sf-journey-intro"><div><span class="sf-journey-eyebrow">当前项目</span><h2>${esc(state.site.name)}</h2><p>${esc(journey.next.hint)}</p></div>
|
||
<div class="sf-journey-progress"><strong>${checked}/4</strong><span>已记录的必要步骤</span></div></section>
|
||
<ol class="sf-journey-list">${rows}</ol>
|
||
<section class="sf-journey-after"><h2>保存、构建和上线有什么不同?</h2><div class="sf-glossary-grid">
|
||
<p><strong>保存内容</strong><span>把修改写入管理系统,外部访客还看不到新版。</span></p>
|
||
<p><strong>生成网站</strong><span>制作一份可预览、可下载的静态网站版本。</span></p>
|
||
<p><strong>检查与审批</strong><span>核对页面、图片、网址和 SEO,并取得相应审批。</span></p>
|
||
<p><strong>正式上线</strong><span>把批准的版本部署到正式服务器,再核对公网访问。</span></p></div></section>
|
||
<section class="sf-journey-notice" role="note"><strong>重要说明</strong>:${journey.live?'系统发现正式发布记录,但无法仅凭该记录确认当前公网网站可访问,请人工检查实际域名。':'当前没有可确认的正式发布记录;生成 ZIP 或点击“发布设计”都不代表网站已上线。'}${journey.changedAfterBuild?' 网站内容在最近一次构建后可能有变化,建议重新构建并预览。':''}${!journey.domain?' 正式域名尚未填写,不影响预览。':''}</section>`;
|
||
}
|
||
function content(){if(!currentSite())return noSite();let arr=state.entries.filter(e=>(!state.typ||e.type===state.typ)&&(!state.search||`${e.title} ${e.route}`.toLowerCase().includes(state.search.toLowerCase())));
|
||
return htmlHeading('内容与页面','所有网页、产品、车型和新闻都在这里维护,保存后重新构建即可更新网站。',actionButton('+ 添加内容','new-entry'))+
|
||
`<div class="panel"><div class="panel-head"><div><h2>内容列表</h2><p>共 ${state.entries.length} 条内容。点击“设计页面”可进行可视化排版。</p></div></div><div class="toolbar"><input id="contentSearch" aria-label="搜索内容" placeholder="搜索标题或页面路径…" value="${esc(state.search)}"><select id="contentType" aria-label="按类型筛选"><option value="">全部类型</option>${Object.entries(typeName).map(([key,value])=>`<option value="${key}" ${state.typ===key?'selected':''}>${value}</option>`).join('')}</select></div>`+
|
||
(arr.length?`<div class="table-wrap"><table class="data-table"><thead><tr><th>名称</th><th>内容类型</th><th>页面地址</th><th>状态</th><th>操作</th></tr></thead><tbody>${arr.slice(0,200).map(e=>`<tr><td><strong>${esc(e.title)}</strong></td><td>${esc(typeName[e.type]||e.type)}</td><td class="muted">${esc(e.route)}</td><td><span class="pill ${e.status==='draft'?'warn':'success'}">${e.status==='draft'?'草稿':'已保存'}</span></td><td class="action-cell"><button class="btn btn-tiny" data-action="edit-entry" data-id="${esc(e.id)}">修改内容</button> <a class="btn btn-tiny" href="/editor/${encodeURIComponent(state.site.id)}/${encodeURIComponent(e.id)}">设计页面 ↗</a></td></tr>`).join('')}</tbody></table></div>`:emptyState('没有找到内容','可以清除筛选条件,或者添加新内容。',actionButton('+ 添加内容','new-entry')))+
|
||
`${arr.length>200?'<p class="muted">当前显示前 200 条,请使用搜索缩小范围。</p>':''}</div>`;
|
||
}
|
||
function design(){if(!currentSite())return noSite();const s=state.site;const cards=Object.entries(themeName).map(([id,name])=>`<div class="theme-card"><div class="theme-preview ${id}">${name}</div><div class="theme-desc"><h3>${name} ${s.theme===id?'<span class="pill success">当前使用</span>':''}</h3><p>${({automotive:'适合汽车出口、车型及品牌展示',industrial:'适合制造业、产品与技术参数',corporate:'适合集团公司、服务和案例',minimal:'适合作品展示和品牌形象'})[id]}</p><button class="btn btn-tiny ${s.theme===id?'btn-muted':'btn-primary'}" ${s.theme===id?'disabled':''} data-action="set-theme" data-theme="${id}">${s.theme===id?'正在使用':'使用此主题'}</button></div></div>`).join('');
|
||
return htmlHeading('外观与设计','先选一套网站风格,再打开可视化编辑器修改图文、导航和手机端布局。')+
|
||
`<div class="section-heading"><div><h2>第一步 · 选择行业主题</h2><p>更换主题不会删除您已经录入的业务内容和 URL。</p></div><a href="/theme-gallery" class="btn btn-tiny">查看完整主题预览 ↗</a></div><div class="choice-cards">${cards}</div>`+
|
||
`<div class="section-heading"><div><h2>第二步 · 设计网页</h2><p>挑选要修改的页面;可在设计器中编辑导航、页眉、页脚及各设备布局。</p></div></div><div class="panel"><div class="panel-head"><div><h2>选择要设计的页面</h2><p>修改先保存为草稿,正式发布设计后才会参与下一次静态构建。</p></div></div><div class="grid-2 grid-equal">${state.entries.filter(e=>e.type==='page').slice(0,8).map(e=>`<a class="site-card" href="/editor/${encodeURIComponent(s.id)}/${encodeURIComponent(e.id)}"><span class="site-icon">▤</span><span><h3>${esc(e.title)}</h3><p>${esc(e.route)}</p></span><span class="arrow">›</span></a>`).join('')||'<div class="muted">尚无页面,请先添加内容。</div>'}</div></div><div class="help-hint" style="margin-top:19px"><strong>提示</strong>:网站设置用于公司信息和域名;可视化编辑器用于调整页面外观。两者不会混在同一表单里。</div>`;
|
||
}
|
||
function imports(){if(!currentSite())return noSite();return htmlHeading('导入与迁移','根据您已有的资料选择操作方式:Excel 产品、WordPress 网站,或者外部 API / 数据库。')+
|
||
`<div class="quick-grid"><a class="quick-card" href="/production-v8"><span class="quick-ico">▤</span><span><h3>导入 Excel / CSV</h3><p>推荐先预览字段、检查冲突,再确认提交。</p><span class="quick-more">安全导入与批次回滚 →</span></span></a><a class="quick-card" href="/production-v8"><span class="quick-ico">⇄</span><span><h3>迁移 WordPress 旧站</h3><p>迁移会话、文章采集、图片与 SEO 检查。</p><span class="quick-more">打开迁移工作台 ↗</span></span></a><a class="quick-card" href="/production-v8"><span class="quick-ico">⌘</span><span><h3>API / XML / MySQL</h3><p>连接外部业务系统,进行字段映射和分批导入。</p><span class="quick-more">进入高级导入 ↗</span></span></a></div>`+
|
||
`<div class="panel"><h2>推荐:先预演再导入</h2><p>在数据导入工作台检查字段匹配、页面路径冲突及品牌分类关系,再分批提交。</p><a class="btn btn-primary" href="/production-v8">打开安全导入工作台 →</a><details class="form-expand" style="margin-top:20px"><summary>高级:直接写入网站(不可批次回滚)</summary><p class="panel-desc">仅用于确认内容无误的小文件,建议先备份;复杂迁移请使用上方工作台。</p><form id="importForm"><div class="import-box"><label for="importFile"><strong>选择要导入的数据文件</strong></label><input type="file" id="importFile" name="file" accept=".csv,.xlsx,.xls,.json,.xml" required><p class="muted">支持 CSV、Excel、JSON、WordPress XML;单文件最多 15 MB。</p></div><div class="form-actions"><button class="btn btn-primary" type="submit">确认导入数据</button><a href="/production-v8" class="btn">需要预演与批次回滚?</a></div></form><div id="importSummary" class="message" style="margin-top:15px" hidden></div></details></div>`+
|
||
`<div class="help-hint" style="margin-top:17px"><strong>迁移旧网站请注意:</strong>如果需要保留旧 URL、原有 SEO、复杂插件字段和全部图片,请优先使用 WordPress 迁移工作台,而不是仅上传一个内容文件。</div>`;
|
||
}
|
||
function builds(){if(!currentSite())return noSite();const latest=latestDone();const pending=activeJobs();const first=pending[0];
|
||
return htmlHeading('构建与交付','网站构建由独立 Worker 执行。离开页面或刷新后仍可查看任务状态。',`<button type="button" class="btn btn-primary" data-action="build-now" ${pending.length||state.building?'disabled':''}>${pending.length?'已有构建在运行':'↗ 开始构建'}</button>`)+
|
||
`<div class="task-note"><div><strong>${first?'当前:'+buildStatusLabel(first.state):latest?'已有构建版本,可以预览':'还没有已完成的网站构建'}</strong><p>${first?'当前任务 '+esc(first.id.slice(0,12))+';Worker 在后台执行,您可以关闭此页面。':'内容或设计有更新时,重新生成网站版本。'}</p></div><div class="inline-actions"><button class="btn" data-action="check-readiness">交付前检查</button><a class="btn" href="/operations">任务与发布审批 ↗</a></div></div>`+
|
||
`<div class="panel"><div class="panel-head"><div><h2>后台任务</h2><p>可以查看任务状态。仅取消当前仍在队列或运行中的构建。</p></div><button class="btn btn-tiny" data-action="refresh-builds">刷新状态</button></div><div id="jobNotice" class="message" ${state.job?'':'hidden'}>${esc(state.job||'')}</div>`+
|
||
(state.jobs.length?state.jobs.slice(0,8).map(j=>`<div class="build-row"><span class="quick-ico">${j.state==='done'?'✓':j.state==='failed'?'!':'◷'}</span><div class="build-meta"><h3>${esc(buildStatusLabel(j.state))}</h3><p>${esc(fmtDate(j.created_at))} · 任务 ${esc(j.id.slice(0,12))}${j.state==='failed'&&j.error?' · '+esc(j.error):''}</p></div>${['queued','running','retrying'].includes(j.state)?`<button class="btn btn-tiny btn-danger" data-action="cancel-job" data-id="${esc(j.id)}">取消任务</button>`:''}</div>`).join(''):'<p class="muted">当前网站还没有后台构建任务。</p>')+`</div>`+
|
||
`<div class="panel" style="margin-top:18px"><div class="panel-head"><div><h2>构建版本</h2><p>建议先预览和核对质量报告,再下载 ZIP。预览不代表正式发布。</p></div></div>`+
|
||
(state.builds.length?state.builds.slice(0,20).map(b=>`<div class="build-row"><span class="quick-ico">${['done','completed','success','complete'].includes(b.status)?'✓':'◷'}</span><div class="build-meta"><h3>${esc(fmtDate(b.created_at))} · ${esc(b.total_pages||'—')} 个页面</h3><p>版本 ${esc(b.id.slice(0,12))} · ${esc(b.status||'未知')}</p></div><div class="inline-actions">${['done','completed','success','complete'].includes(b.status)?`<a class="btn btn-tiny" href="/preview/${encodeURIComponent(state.site.id)}/${encodeURIComponent(b.id)}/" target="_blank" rel="noopener">预览 ↗</a><button class="btn btn-tiny" data-action="report" data-id="${esc(b.id)}">质量报告</button><a class="btn btn-tiny btn-primary" href="/api/sites/${encodeURIComponent(state.site.id)}/builds/${encodeURIComponent(b.id)}/download">下载 ZIP</a>`:'<span class="muted">尚未完成,暂不能下载</span>'}</div></div>`).join(''):emptyState('尚无完成版本','准备好网站数据后,在上方提交构建任务。'))+
|
||
`<div id="reportContent"></div></div>`+
|
||
`<div class="panel" style="margin-top:18px"><div class="panel-head"><div><h2>正式发布与回滚</h2><p>先完成质量检查与独立审批,Worker 负责上传、校验和切换版本。没有配置目标服务器时不能发布。</p></div><a href="/operations" class="btn btn-tiny">审批与复核工具 ↗</a></div>
|
||
<div class="field-grid"><label>选择已完成构建<select id="deliveryBuild">${state.builds.filter(b=>b.status==='complete').map(b=>`<option value="${esc(b.id)}">${esc(b.id.slice(0,12))} · ${esc(fmtDate(b.created_at))}</option>`).join('')}</select></label>
|
||
<label>目标环境<select id="deliveryTarget"><option value="staging">测试环境(Staging)</option><option value="production">正式环境(Production)</option></select></label></div>
|
||
<div class="form-actions"><button class="btn" data-action="release-preflight">检查发布条件</button><button class="btn" data-action="release-approval">提交独立审批</button><button class="btn btn-primary" data-action="release-deploy">提交发布任务</button></div>
|
||
<div id="deliveryNotice" class="message" aria-live="polite" hidden></div>
|
||
<h3>最近发布任务</h3>${(state.delivery.jobs||[]).length?state.delivery.jobs.slice(0,8).map(j=>`<div class="build-row"><div class="build-meta"><strong>${j.kind==='remote_rollback'?'回滚':'部署'} · ${esc(buildStatusLabel(j.state))}</strong><p>${esc(j.id.slice(0,12))} · ${esc(j.created_at)}${j.error?' · '+esc(j.error):''}</p></div>${['queued','running','retrying'].includes(j.state)?`<button class="btn btn-tiny btn-danger" data-action="cancel-job" data-id="${esc(j.id)}">取消</button>`:''}</div>`).join(''):'<p class="muted">暂无发布任务。</p>'}
|
||
<h3>已发布版本</h3>${(state.delivery.history||[]).length?state.delivery.history.slice(0,8).map(r=>`<div class="build-row"><div class="build-meta"><strong>${esc(r.target)} · ${esc(r.release_id)}</strong><p>构建 ${esc(r.build_id.slice(0,12))} · ${esc(r.deployed_at)}</p></div><button class="btn btn-tiny" data-action="release-rollback" data-target="${esc(r.target)}" data-id="${esc(r.release_id)}">回滚到此版本</button></div>`).join(''):'<p class="muted">当前没有远程部署记录。</p>'}</div>`;
|
||
}
|
||
function settings(){if(!currentSite())return noSite();const s=state.site;return htmlHeading('网站设置','这里只管理公司资料、域名与站点属性。网页样式请到“外观与设计”修改。')+
|
||
`<div class="panel"><h2>基本资料</h2><p class="panel-desc">没有域名也可以制作与预览网站,正式上线前再填写即可。</p><form id="settingsForm"><div class="field-grid"><label class="full">网站名称<input name="name" required value="${esc(s.name)}"></label><label class="full">正式域名<input name="domain" placeholder="https://www.example.com" value="${esc(s.domain||'')}"></label><label class="full">企业介绍 / 网站标语<textarea name="tagline" rows="3">${esc(s.tagline||'')}</textarea></label><label class="full">Logo 图片地址<input name="logo" value="${esc(s.logo||'')}"></label><label>网站类型<select name="mode"><option value="new" ${s.mode==='new'?'selected':''}>新建网站</option><option value="migration" ${s.mode==='migration'?'selected':''}>旧站迁移</option><option value="hybrid" ${s.mode==='hybrid'?'selected':''}>混合建站</option></select></label><label>主题色<input type="color" name="accent" value="${esc(s.accent||'#245ac6')}" style="padding:4px;height:43px"></label></div><div class="form-actions"><button class="btn btn-primary" type="submit">保存网站设置</button></div></form></div>`;
|
||
}
|
||
function languages(){
|
||
if(!currentSite())return noSite();
|
||
const d=state.language||{group:null,sites:[],units:[]};const g=d.group;
|
||
const groups=state.languageGroups||[];
|
||
const header=htmlHeading('多语言管理','多个语言版本分别拥有独立的网站、域名和内容。系统只关联已审核的翻译,不会自动翻译企业事实。','<button class="btn" data-action="language-refresh">刷新语言状态</button>');
|
||
if(!g)return header+`<div class="panel"><h2>第一步 · 建立语言组</h2><p class="panel-desc">例如“集团全球网站”包含中文版、英文版、法文版。创建语言组后将网站加入对应语言。</p><form id="langGroupForm"><div class="field-grid"><label>语言组名称<input name="name" required minlength="2" placeholder="HESU 国际站"></label><label>默认语言<select name="default_locale"><option value="en">English (en)</option><option value="zh-CN">简体中文 (zh-CN)</option><option value="fr-FR">Français (fr-FR)</option><option value="ar">العربية (ar)</option></select></label><label>当前网站的实际语言<input name="current_locale" value="zh-CN" required placeholder="例如 zh-CN 或 en"></label></div><div class="form-actions"><button class="btn btn-primary">创建语言组并关联当前网站</button></div></form></div>`+
|
||
(groups.length?`<div class="panel"><h2>加入已有语言组</h2><p class="panel-desc">仅可关联您有管理权限、且已配置独立 HTTPS 域名的网站。</p><form id="langJoinForm"><div class="field-grid"><label>语言组<select name="group_id">${groups.map(x=>`<option value="${esc(x.id)}">${esc(x.name)}</option>`).join('')}</select></label><label>当前网站的语言<input name="locale" value="zh-CN" required placeholder="zh-CN / en / fr-FR"></label></div><div class="form-actions"><button class="btn btn-primary">加入语言组</button></div></form></div>`:'')+
|
||
`<div class="help-hint"><strong>上线前提示:</strong>网站设置中的正式域名应为 https://example.com 形式;不同语言网站不能共用一个域名。当前域名:${esc(state.site.domain||'未设置')}</div>`;
|
||
const groupSites=d.sites||[]; const uniqueUnits=[...new Map((d.units||[]).map(u=>[u.id,u])).values()];
|
||
const canJoin=state.sites.filter(x=>!groupSites.some(m=>m.site_id===x.id));
|
||
return header+`<div class="panel"><div class="panel-head"><div><h2>${esc(g.name)}</h2><p>当前站语言 ${esc(g.locale)} · 默认语言 ${esc(g.default_locale)} · ${groupSites.length} 个语言站点</p></div></div><div class="table-wrap"><table class="data-table"><thead><tr><th>语言</th><th>网站</th><th>正式域名</th></tr></thead><tbody>${groupSites.map(x=>`<tr><td>${esc(x.locale)}</td><td>${esc(x.name)}</td><td>${esc(x.domain)}</td></tr>`).join('')}</tbody></table></div></div>`+
|
||
`<div class="panel"><h2>第二步 · 添加其他语言网站</h2><form id="langJoinForm"><div class="field-grid"><input type="hidden" name="group_id" value="${esc(g.group_id)}"><label>已有网站<select name="site_id">${canJoin.map(x=>`<option value="${esc(x.id)}">${esc(x.name)}</option>`).join('')}</select></label><label>对应语言<input name="locale" required placeholder="例如 en、fr-FR"></label></div><div class="form-actions"><button class="btn btn-primary" ${!canJoin.length?'disabled':''}>加入语言站</button></div></form><p class="muted">先通过“我的网站”创建另一语言站并设置 HTTPS 域名,再加入语言组。</p></div>`+
|
||
`<div class="panel"><h2>第三步 · 建立翻译内容关系</h2><p class="panel-desc">相同产品、文章或页面可以使用不同 URL。用一个内容标识关联它们,只有正式发布且审核通过的内容才会生成 hreflang。</p><form id="langUnitForm"><div class="field-grid"><label class="full">翻译组标识<input name="reference_key" required placeholder="例如 product:rox-01 或 about-page"></label></div><div class="form-actions"><button class="btn btn-primary">创建内容翻译组</button></div></form></div>`+
|
||
`<div class="panel"><h2>第四步 · 关联各语言的实际内容</h2><form id="langLinkForm"><div class="field-grid"><label>翻译内容组<select name="unit_id">${uniqueUnits.map(x=>`<option value="${esc(x.id)}">${esc(x.reference_key)}</option>`).join('')}</select></label><label>语言网站<select id="langMemberSite" name="site_id">${groupSites.map(x=>`<option value="${esc(x.site_id)}" ${x.site_id===state.site.id?'selected':''}>${esc(x.locale)} · ${esc(x.name)}</option>`).join('')}</select></label><label class="full">对应内容<select id="langMemberEntry" name="entry_id">${(state.languageEntries||[]).map(x=>`<option value="${esc(x.id)}">${esc(x.title)} (${esc(x.route)}) · ${x.status==='published'?'正式':'草稿'}</option>`).join('')}</select></label><label>审核状态<select name="state"><option value="pending">待翻译(仅允许草稿)</option><option value="approved">翻译已确认(仅允许正式内容)</option></select></label></div><div class="form-actions"><button class="btn btn-primary" ${uniqueUnits.length?'':'disabled'}>保存关联</button></div></form></div>`+
|
||
`<div class="panel"><h2>已有语言关系</h2><p class="panel-desc">没有已审核的双向内容关系,就不会输出假语言链接。</p>${d.units.length?`<div class="table-wrap"><table class="data-table"><thead><tr><th>内容组</th><th>语言</th><th>内容 / URL</th><th>状态</th></tr></thead><tbody>${d.units.map(u=>`<tr><td>${esc(u.reference_key)}</td><td>${esc(u.locale)}</td><td>${esc(u.title)}<br><small>${esc(u.route)}</small></td><td>${u.state==='approved'?'已审核':'待翻译'}</td></tr>`).join('')}</tbody></table></div>`:'<p class="muted">还没有关联内容。</p>'}</div>`;
|
||
}
|
||
async function loadLanguage(){const sid=state.site?.id;if(!sid)return;try{
|
||
const [data,groups]=await Promise.all([api('/api/i18n/sites/'+encodeURIComponent(sid)),api('/api/i18n/groups')]);
|
||
if(state.site?.id!==sid)return;state.language=data;state.languageGroups=groups;await loadLanguageEntries(sid,false);if(state.view==='languages')rerender();
|
||
}catch(e){notifyError(e)}}
|
||
async function loadLanguageEntries(siteId,redraw=true){if(!siteId)return;try{const rows=await api('/api/sites/'+encodeURIComponent(siteId)+'/entries');state.languageEntries=rows;if(redraw){const target=$('#langMemberEntry');if(target)target.innerHTML=rows.map(x=>`<option value="${esc(x.id)}">${esc(x.title)} (${esc(x.route)}) · ${x.status==='published'?'正式':'草稿'}</option>`).join('')}}catch(e){notifyError(e);state.languageEntries=[]}}
|
||
async function submitLanguageGroup(ev){ev.preventDefault();if(!state.site)return;try{const d=Object.fromEntries(new FormData(ev.target));const locale=d.current_locale;delete d.current_locale;const group=await api('/api/i18n/groups','POST',d);await api('/api/i18n/groups/'+group.id+'/sites','POST',{site_id:state.site.id,locale});toast('语言组已创建');await loadLanguage()}catch(e){notifyError(e)}}
|
||
async function submitLanguageJoin(ev){ev.preventDefault();try{const d=Object.fromEntries(new FormData(ev.target));await api('/api/i18n/groups/'+d.group_id+'/sites','POST',{site_id:d.site_id||state.site.id,locale:d.locale});toast('语言站已关联');await loadLanguage()}catch(e){notifyError(e)}}
|
||
async function submitLanguageUnit(ev){ev.preventDefault();const d=Object.fromEntries(new FormData(ev.target));try{await api('/api/i18n/groups/'+state.language.group.group_id+'/units','POST',d);toast('翻译组已创建');await loadLanguage()}catch(e){notifyError(e)}}
|
||
async function submitLanguageLink(ev){ev.preventDefault();const d=Object.fromEntries(new FormData(ev.target));try{await api('/api/i18n/groups/'+state.language.group.group_id+'/units/'+encodeURIComponent(d.unit_id)+'/members','POST',{site_id:d.site_id,entry_id:d.entry_id,state:d.state});toast('内容翻译关系已保存');await loadLanguage()}catch(e){notifyError(e)}}
|
||
function rerender(){const area=$('#mainContent');const selected=state.view;$('#breadcrumbTitle').textContent=headings[selected]||'工作台';$('#sideSiteName').textContent=state.site?.name||'请先选择网站';
|
||
document.querySelectorAll('[data-view].nav-item').forEach(el=>{const yes=el.dataset.view===selected;el.classList.toggle('active',yes);yes?el.setAttribute('aria-current','page'):el.removeAttribute('aria-current')});
|
||
area.innerHTML=({home,sites,overview,guide,content,design,import:imports,build:builds,languages,settings}[selected]||home)();
|
||
}
|
||
async function loadSites(){state.sites=await api('/api/sites');const picker=$('#sitePicker'),prev=state.site?.id||'';picker.innerHTML='<option value="">选择网站…</option>'+state.sites.map(s=>`<option value="${esc(s.id)}">${esc(s.name)}</option>`).join('');picker.value=state.sites.some(s=>s.id===prev)?prev:''}
|
||
async function readDelivery(siteId){try{return await api(`/api/production/sites/${encodeURIComponent(siteId)}/delivery-status`)}catch(e){return {jobs:[],history:[],configured:{}}}}
|
||
async function readJobs(siteId){try{return await api(`/api/enterprise/sites/${encodeURIComponent(siteId)}/jobs`)}catch(e){if(![403,404].includes(Number(e.status)))console.warn('加载任务状态失败',e);return []}}
|
||
async function selectSite(id,go='overview',historyMode='push'){
|
||
const token=++state.selectionToken;
|
||
if(!id){state.site=null;state.entries=[];state.builds=[];state.jobs=[];state.delivery={jobs:[],history:[],configured:{}};state.job=null;state.view=go==='home'?'home':'sites';$('#sitePicker').value='';rerender();if(historyMode!=='none')rememberView(historyMode==='replace');return}
|
||
if(!state.sites.some(x=>x.id===id)){toast('该网站不在您的可见列表中',true);return}
|
||
try{const [site,entries,builds,jobs,delivery]=await Promise.all([api(`/api/sites/${encodeURIComponent(id)}`),api(`/api/sites/${encodeURIComponent(id)}/entries`),api(`/api/sites/${encodeURIComponent(id)}/builds`),readJobs(id),readDelivery(id)]);
|
||
if(token!==state.selectionToken)return;
|
||
state.site=site;sfRemember(id);state.entries=entries;state.builds=builds;state.jobs=jobs;state.delivery=delivery;state.job=null;state.view=go;state.search='';state.typ='';$('#sitePicker').value=id;rerender();if(historyMode!=='none')rememberView(historyMode==='replace');if(go==='languages')loadLanguage();closeMobile();window.scrollTo({top:0,behavior:'instant'});
|
||
}catch(e){if(token===state.selectionToken)notifyError(e)}
|
||
}
|
||
function navigate(to,historyMode='push'){if(!state.site&&!['home','sites'].includes(to)){state.view='sites';rerender();toast('请先选择网站项目');return}state.view=to;rerender();closeMobile();if(historyMode!=='none')rememberView(historyMode==='replace');if(to==='build')refreshBuilds();if(to==='languages')loadLanguage();window.scrollTo({top:0,behavior:'instant'})}
|
||
async function refreshBuilds(){const id=state.site?.id;if(!id)return;try{const previouslyRunning=activeJobs().length>0;const [jobs,builds,delivery]=await Promise.all([readJobs(id),api(`/api/sites/${encodeURIComponent(id)}/builds`),readDelivery(id)]);if(state.site?.id!==id)return;state.jobs=jobs;state.builds=builds;state.delivery=delivery;if(previouslyRunning&&!activeJobs().length){const last=jobs[0];state.job=last?.state==='done'?'后台构建已完成,请预览并查看质量报告。':last?.state==='failed'?'本次构建失败,请在任务中心检查原因。':last?.state==='cancelled'?'本次构建已取消。':null}if(state.view==='build')rerender()}catch(e){notifyError(e)}}
|
||
function openModal(name){const node=$('#'+name);node.showModal();node.querySelector('input:not([type=hidden]),select')?.focus()}
|
||
function closeModal(name){$('#'+name)?.close()}
|
||
function openEntry(id){const form=$('#entryForm');form.reset();form.elements.entry_id.value='';$('#entryModalTitle').textContent=id?'修改内容':'添加内容';if(id){const e=state.entries.find(x=>x.id===id);if(!e)return toast('这条内容不存在',true);for(const field of ['title','route','summary','body','seo_title','seo_description','type','status']){if(form.elements[field])form.elements[field].value=e[field]||''}form.elements.entry_id.value=id}openModal('entryModal')}
|
||
async function saveEntry(ev){ev.preventDefault();if(!state.site)return;const el=ev.currentTarget;const data=Object.fromEntries(new FormData(el));const id=data.entry_id;delete data.entry_id;
|
||
if(!data.route?.trim())delete data.route;
|
||
try{await api(`/api/sites/${state.site.id}/entries${id?'/'+encodeURIComponent(id):''}`,id?'PATCH':'POST',data);closeModal('entryModal');toast(id?'内容已更新':'内容已添加');await selectSite(state.site.id,'content')}catch(e){notifyError(e)}}
|
||
let createStep=0;
|
||
function createValidate(){
|
||
const f=$('#siteForm'),name=f.elements.name.value.trim(),domain=f.elements.domain.value.trim();
|
||
if(!name){return '请填写网站名称。'}
|
||
if(name.length>150)return '网站名称过长,请缩短后再继续。';
|
||
if(domain&&!/^https:\/\/[^\s/]+(?:[\/?#][^\s]*)?$/i.test(domain))return '正式域名请填写完整的 https:// 地址,或先留空。';
|
||
return '';
|
||
}
|
||
function renderCreateStep(){
|
||
const form=$('#siteForm');if(!form)return;
|
||
form.querySelectorAll('[data-create-step]').forEach(el=>{el.hidden=Number(el.dataset.createStep)!==createStep});
|
||
form.querySelectorAll('[data-create-indicator]').forEach(el=>{const current=Number(el.dataset.createIndicator)===createStep;if(current)el.setAttribute('aria-current','step');else el.removeAttribute('aria-current')});
|
||
$('#createProgressText').textContent=`第 ${createStep+1} 步,共 3 步 · ${['填写信息','选择风格','确认创建'][createStep]}`;
|
||
$('#createBack').hidden=createStep===0;
|
||
$('#createNext').hidden=createStep===2;
|
||
$('#createNext').textContent=createStep===0?'下一步:选择风格 →':'下一步:确认信息 →';
|
||
$('#createSubmit').hidden=createStep!==2;
|
||
$('#createError').hidden=true;
|
||
if(createStep===2){const data=Object.fromEntries(new FormData(form));$('#createSummary').innerHTML=`<p><strong>网站名称:</strong>${esc(data.name)}</p><p><strong>网站类型:</strong>${esc(({new:'全新制作网站',migration:'迁移旧站',hybrid:'旧站内容重新设计'})[data.mode]||data.mode)}</p><p><strong>网站风格:</strong>${esc(themeName[data.theme]||'企业品牌')}</p>`}
|
||
}
|
||
function openCreateWizard(){const form=$('#siteForm');form.reset();createStep=0;renderCreateStep();openModal('siteModal')}
|
||
async function saveSite(ev){ev.preventDefault();const error=createValidate();if(error){$('#createError').textContent=error;$('#createError').hidden=false;return}
|
||
if(createStep!==2){createStep=2;renderCreateStep();return}
|
||
const button=$('#createSubmit');if(button.disabled)return;button.disabled=true;button.textContent='正在创建…';
|
||
try{const data=Object.fromEntries(new FormData(ev.currentTarget));const s=await api('/api/sites','POST',data);closeModal('siteModal');toast('网站已建立,下一步请完善真实内容');await loadSites();await selectSite(s.id,'guide')}
|
||
catch(e){notifyError(e);$('#createError').textContent=e.message||'创建失败,请重试';$('#createError').hidden=false}
|
||
finally{button.disabled=false;button.textContent='确认创建网站'}
|
||
}
|
||
|
||
async function setTheme(theme){if(!state.site||theme===state.site.theme)return;if(!confirm(`切换到「${themeName[theme]}」?网站内容及已有 URL 将保持不变,外观会在下次构建后更新。`))return;try{await api(`/api/sites/${state.site.id}`,'PATCH',{theme});toast('主题已切换,重新构建后生效');await selectSite(state.site.id,'design')}catch(e){notifyError(e)}}
|
||
async function saveSettings(ev){ev.preventDefault();try{await api(`/api/sites/${state.site.id}`,'PATCH',Object.fromEntries(new FormData(ev.currentTarget)));toast('网站设置已保存');await loadSites();await selectSite(state.site.id,'settings')}catch(e){notifyError(e)}}
|
||
async function importFile(ev){ev.preventDefault();try{if(!state.site)return;const form=new FormData(ev.currentTarget);if(!form.get('file')?.name)throw Error('请选择文件');if(!confirm('将文件内容直接写入当前网站,重复路径默认跳过。需要预演和批次回滚请先使用高级导入。确认继续?'))return;const result=await api(`/api/sites/${state.site.id}/import`,'POST',form);const box=$('#importSummary');box.hidden=false;box.textContent=`导入完成:新增 ${result.imported||0} 条,更新 ${result.updated||0} 条,跳过 ${result.skipped||0} 条。错误 ${result.errors?.length||0} 条。`;toast('导入已完成');await refreshSiteKeepView()}catch(e){notifyError(e)}}
|
||
async function refreshSiteKeepView(){if(!state.site)return;const [site,entries,builds]=await Promise.all([api('/api/sites/'+state.site.id),api('/api/sites/'+state.site.id+'/entries'),api('/api/sites/'+state.site.id+'/builds')]);state.site=site;state.entries=entries;state.builds=builds}
|
||
async function cancelJob(jobId){if(!state.site)return;if(!confirm('确定取消这个构建任务?已经完成的静态版本不会被删除。'))return;try{const id=state.site.id;await api(`/api/enterprise/sites/${encodeURIComponent(id)}/jobs/${encodeURIComponent(jobId)}/cancel`,'POST');toast('已发送取消请求');await refreshBuilds()}catch(e){notifyError(e)}}
|
||
async function buildNow(){if(!state.site)return toast('请先选择网站',true);if(state.building||activeJobs().length)return toast('已有构建在执行,请先查看任务状态');
|
||
const id=state.site.id;state.building=true;
|
||
try{navigate('build');state.job='正在提交构建任务…';rerender();const job=await api(`/api/sites/${encodeURIComponent(id)}/builds`,'POST',{mirror_images:false},{'Idempotency-Key':crypto.randomUUID()});
|
||
if(state.site?.id!==id)return;state.job=`任务 ${job.id?.slice(0,12)||'已提交'} 已进入队列,可以离开页面;重新打开“构建与交付”查看结果。`;toast('已提交构建任务');await refreshBuilds();
|
||
}catch(e){notifyError(e)}finally{state.building=false;if(state.site?.id===id&&state.view==='build')rerender()}}
|
||
function selectedReleaseBuild(){const id=$('#deliveryBuild')?.value;if(!id)throw Error('没有可发布的构建版本,请先完成网站构建');return id}
|
||
function showDeliveryMessage(message){const el=$('#deliveryNotice');if(el){el.hidden=false;el.textContent=message}toast(message)}
|
||
async function releasePreflight(){if(!state.site)return;try{const id=selectedReleaseBuild(),s=state.site.id;const r=await api(`/api/production/sites/${encodeURIComponent(s)}/preflight/${encodeURIComponent(id)}`);if(state.site?.id!==s)return;showDeliveryMessage(r.ready?'已通过发布前置检查:'+r.message:'尚不能发布:'+r.message)}catch(e){notifyError(e)}}
|
||
async function releaseApproval(){if(!state.site)return;try{const id=selectedReleaseBuild(),s=state.site.id;const r=await api(`/api/factory/sites/${encodeURIComponent(s)}/release-requests`,'POST',{build_id:id});showDeliveryMessage('审批申请已创建,请由另一名有审核权限的人员批准。申请编号:'+r.id)}catch(e){notifyError(e)}}
|
||
async function releaseDeploy(){if(!state.site)return;try{const id=selectedReleaseBuild(),s=state.site.id,target=$('#deliveryTarget')?.value;if(!state.delivery.configured?.[target])throw Error('尚未配置此目标服务器,请管理员配置目标发布方式(Agent/SFTP/S3)、服务器凭据与 HTTPS 验证地址');if(!confirm(`确定将构建 ${id.slice(0,12)} 提交到${target==='production'?'正式':'测试'}环境吗?必须已有独立审批。`))return;const r=await api(`/api/production/sites/${encodeURIComponent(s)}/deploy`,'POST',{build_id:id,target},{'Idempotency-Key':crypto.randomUUID()});showDeliveryMessage(`发布任务 ${r.id} 已排队。Worker 会先暂存和校验,再激活新版本。`);await refreshBuilds()}catch(e){notifyError(e)}}
|
||
async function releaseRollback(target,release){if(!state.site)return;if(!confirm(`确定在 ${target} 环境恢复历史版本 ${release} 吗?请确认其内容仍符合当前业务要求。`))return;try{const r=await api(`/api/production/sites/${encodeURIComponent(state.site.id)}/rollback`,'POST',{target,release_id:release},{'Idempotency-Key':crypto.randomUUID()});showDeliveryMessage(`回滚任务 ${r.id} 已排队,请在任务列表查看结果。`);await refreshBuilds()}catch(e){notifyError(e)}}
|
||
async function checkReadiness(){if(!state.site)return;const siteId=state.site.id;
|
||
try{const result=await api(`/api/factory/sites/${encodeURIComponent(siteId)}/migration/readiness`);
|
||
if(state.site?.id!==siteId)return;
|
||
const box=$('#reportContent');if(!box)return;
|
||
const gates=result.gates||[];
|
||
box.innerHTML=`<div class="panel" style="margin-top:18px"><h3>正式交付前检查</h3><p class="muted">检查旧站 URL、SEO 和静态文件风险;通过检查不等于已经上线。</p><p class="${result.can_publish?'pill success':'pill warn'}">${result.can_publish?'没有发现发布阻断项':'存在需要先处理的问题'}</p>${gates.length?'<ul class="readiness-list">'+gates.map(g=>`<li><strong>${g.level==='block'?'必须修复':'需要复核'}</strong> ${esc(g.message)}</li>`).join('')+'</ul>':'<p>当前没有需要处理的检查项。仍需人工确认内容、域名与询盘表单。</p>'}<div class="form-actions"><a class="btn" href="/operations">进入迁移复核与发布审批 →</a></div></div>`;
|
||
box.scrollIntoView({behavior:'smooth',block:'nearest'});
|
||
}catch(e){notifyError(e)}
|
||
}
|
||
async function showReport(buildId){try{const r=await api(`/api/sites/${state.site.id}/builds/${buildId}/report`);const target=$('#reportContent');if(!target)return;target.innerHTML=`<div class="panel" style="margin-top:16px"><h3>质量检查结果</h3><p class="muted">本报告检查静态内容与资源,不保证搜索引擎排名。</p><div class="stats">${stat('HTML 页面',r.pages||0,'▤')}${stat('重定向',r.redirects||0,'⇄')}${stat('已本地化图片',r.mirrored_images||0,'▧')}${stat('提示数量',(r.warnings||[]).length,'!')}</div>${r.warnings?.length?`<div class="message">${r.warnings.slice(0,20).map(x=>esc(x)).join('\n')}</div>`:'<span class="pill success">当前检查没有警告</span>'}</div>`;target.scrollIntoView({behavior:'smooth',block:'nearest'})}catch(e){notifyError(e)}}
|
||
function closeMobile(){$('#sideMenu').classList.remove('open');$('#mobileBackdrop').classList.remove('open');$('#menuToggle').setAttribute('aria-expanded','false')}
|
||
async function init(){try{const me=await api('/api/auth/me');state.csrf=me.csrf;await loadSites();const health=await api('/api/health');$('#appVersion').textContent='版本 '+health.version;
|
||
// Restore URL first, then a previously selected site *only* after verifying /api/sites.
|
||
const params=new URLSearchParams(location.search),requested=params.get('site');
|
||
const target=params.get('view');const allowed=new Set(['home','sites','overview','guide','content','design','import','build','languages','settings']);
|
||
let stored='';try{stored=localStorage.getItem(sfSiteKey)||''}catch{}
|
||
const chosen=[requested,stored].find(id=>id&&state.sites.some(x=>x.id===id));
|
||
if(chosen){await selectSite(chosen,allowed.has(target)?target:'overview','replace')}
|
||
else {state.view=target==='sites'?'sites':'home';rerender();rememberView(true)}
|
||
}catch(e){notifyError(e);$('#mainContent').innerHTML=emptyState('后台加载失败',e.message,'<button class="btn btn-primary" onclick="location.reload()">重新加载</button>')}}
|
||
// Delegated UI interactions keep navigation and form actions centrally auditable.
|
||
document.addEventListener('click',async ev=>{const control=ev.target.closest('[data-action]');if(!control)return;const action=control.dataset.action;try{if(action==='navigate')navigate(control.dataset.view);if(action==='language-refresh')await loadLanguage();if(action==='new-site')openCreateWizard();if(action==='select-site')await selectSite(control.dataset.id);if(action==='new-entry')openEntry();if(action==='edit-entry')openEntry(control.dataset.id);if(action==='set-theme')await setTheme(control.dataset.theme);if(action==='build-now')await buildNow();if(action==='refresh-builds')await refreshBuilds();if(action==='check-readiness')await checkReadiness();if(action==='cancel-job')await cancelJob(control.dataset.id);if(action==='report')await showReport(control.dataset.id);if(action==='release-preflight')await releasePreflight();if(action==='release-approval')await releaseApproval();if(action==='release-deploy')await releaseDeploy();if(action==='release-rollback')await releaseRollback(control.dataset.target,control.dataset.id)}catch(e){notifyError(e)}});
|
||
document.addEventListener('click',ev=>{const btn=ev.target.closest('[data-close]');if(btn)closeModal(btn.dataset.close)});
|
||
$('#siteForm').addEventListener('submit',saveSite);$('#entryForm').addEventListener('submit',saveEntry);
|
||
document.addEventListener('submit',ev=>{if(ev.target.id==='settingsForm')saveSettings(ev);if(ev.target.id==='importForm')importFile(ev);if(ev.target.id==='langGroupForm')submitLanguageGroup(ev);if(ev.target.id==='langJoinForm')submitLanguageJoin(ev);if(ev.target.id==='langUnitForm')submitLanguageUnit(ev);if(ev.target.id==='langLinkForm')submitLanguageLink(ev)});
|
||
document.addEventListener('input',ev=>{if(ev.target.id==='contentSearch'){state.search=ev.target.value;const cursor=ev.target.selectionStart;const type=state.typ;rerender();const el=$('#contentSearch');el.focus();el.setSelectionRange(cursor,cursor);state.typ=type}});
|
||
document.addEventListener('change',ev=>{if(ev.target.id==='contentType'){state.typ=ev.target.value;rerender()}if(ev.target.id==='langMemberSite')loadLanguageEntries(ev.target.value)});
|
||
$('#sitePicker').addEventListener('change',ev=>selectSite(ev.target.value));
|
||
window.addEventListener('popstate',async()=>{const p=new URLSearchParams(location.search);const site=p.get('site'),view=p.get('view')||'home';const allowed=['home','sites','overview','guide','content','design','import','build','languages','settings'];if(site&&state.sites.some(s=>s.id===site))await selectSite(site,allowed.includes(view)?view:'overview','none');else{state.selectionToken++;state.site=null;state.entries=[];state.jobs=[];state.builds=[];state.view=view==='sites'?'sites':'home';rerender()}});
|
||
setInterval(()=>{if(!document.hidden&&state.view==='build'&&activeJobs().length)refreshBuilds()},5000);
|
||
$('#newSiteTop').addEventListener('click',openCreateWizard);
|
||
$('#createNext').addEventListener('click',()=>{const error=createValidate();if(error){$('#createError').textContent=error;$('#createError').hidden=false;return}createStep=Math.min(2,createStep+1);renderCreateStep()});
|
||
$('#createBack').addEventListener('click',()=>{createStep=Math.max(0,createStep-1);renderCreateStep()});
|
||
$('#logoutBtn').addEventListener('click',async()=>{try{await api('/api/auth/logout','POST');location.href='/login'}catch(e){notifyError(e)}});
|
||
$('#menuToggle').addEventListener('click',()=>{const open=$('#sideMenu').classList.toggle('open');$('#mobileBackdrop').classList.toggle('open',open);$('#menuToggle').setAttribute('aria-expanded',String(open))});
|
||
$('#mobileBackdrop').addEventListener('click',closeMobile);
|
||
document.addEventListener('keydown',ev=>{if(ev.key==='Escape')closeMobile()});
|
||
document.addEventListener('DOMContentLoaded',init);
|
||
</script>
|
||
</body></html>
|