]> acesimba.cloud Git - StationBuilder.git/commitdiff
合并 Codex 设计稿前端(忠实一致,不含后端):扩展菜单 / AI 欢迎面板 / 站点编辑 5 步(含页面配置)/ 手动配置资产前端
authorroot <root@localhost.localdomain>
Wed, 9 Sep 2026 03:31:43 +0000 (11:31 +0800)
committerroot <root@localhost.localdomain>
Wed, 9 Sep 2026 03:31:43 +0000 (11:31 +0800)
app/globals.css
app/page.tsx

index 2dec9a8ea70a94be630329049d3be222c10337af..b427f591459ae8c91cc7e6bedf48a8eb6047ece5 100644 (file)
@@ -40,8 +40,7 @@
 .ai-dialog .ai-close{border:0;background:transparent;color:#fff;font-size:20px;line-height:1;width:28px;height:28px;border-radius:6px}
 .ai-dialog .ai-close:hover{background:#ffffff33}
 .ai-messages{flex:1;overflow-y:auto;padding:12px;display:flex;flex-direction:column;gap:10px;background:#f7f8f9}
-.ai-empty{margin:auto;text-align:center;color:#9aa1a5;font-size:11px;line-height:1.7;padding:0 14px}
-.ai-empty b{color:#168abf}
+.ai-welcome{width:100%;margin:auto 0;display:flex;flex-direction:column;gap:10px;color:#323233}.ai-welcome-intro{display:flex;align-items:center;gap:10px;padding-bottom:2px}.ai-welcome-intro>span{width:32px;height:32px;border-radius:4px;background:#e5f4fb;color:#008cd6;display:grid;place-items:center;flex:0 0 auto}.ai-welcome-intro>div{min-width:0}.ai-welcome-intro b{display:block;font-size:14px;line-height:1.4}.ai-welcome-intro p{margin:2px 0 0;color:#777;font-size:12px;line-height:1.5}.ai-capability-list{border:1px solid #dfe3e6;background:#fff}.ai-capability-list>div{min-height:52px;padding:8px 10px;display:flex;align-items:center;gap:10px;border-bottom:1px solid #edf0f2}.ai-capability-list>div:last-child{border-bottom:0}.ai-capability-list i{width:22px;height:22px;border-radius:50%;background:#e5f4fb;color:#008cd6;display:grid;place-items:center;flex:0 0 auto;font-size:12px;font-style:normal;font-weight:600}.ai-capability-list span{display:flex;flex-direction:column;gap:2px;min-width:0}.ai-capability-list b{font-size:13px;line-height:1.35;font-weight:500}.ai-capability-list small{color:#777;font-size:11px;line-height:1.35}.ai-welcome-tip{padding:8px 10px;background:#eef7fb;border-left:3px solid #008cd6;color:#666;font-size:11px;line-height:1.5}.ai-welcome-tip b{color:#323233}
 .ai-bubble{display:flex;gap:8px;align-items:flex-start;max-width:88%}
 .ai-bubble .ai-avatar{width:26px;height:26px;border-radius:50%;flex:0 0 auto;display:grid;place-items:center;font-size:11px;background:#e3edf3;color:#168abf}
 .ai-bubble.me{flex-direction:row-reverse;align-self:flex-end}
@@ -295,6 +294,7 @@ html,body{font-family:Roboto,"Noto Sans SC","Microsoft YaHei",Arial,sans-serif;f
 .menu-section-items button{height:50px;padding:0 20px 0 40px;display:flex;align-items:center;font-size:17px;line-height:50px;color:#323233}
 .menu-section-items button:hover{background:#dcecf5;color:var(--company-primary)}
 .menu-section-items button.active{background:#c3e4f6;color:var(--company-primary);font-weight:500}
+.extension-menu-items{padding-bottom:2px}.extension-menu-item{height:50px;padding:0 20px 0 40px;display:flex;align-items:center;font-size:17px;line-height:50px;color:#323233;font-weight:500;cursor:default}.extension-submenu-title{justify-content:space-between;cursor:pointer}.extension-submenu-title>.anticon{font-size:11px;margin-right:1px}.extension-third-menu>div{height:50px;padding:0 20px 0 60px;display:flex;align-items:center;color:#323233;font-size:17px;line-height:50px;font-weight:500;cursor:default}.extension-menu-section .extension-menu-item:hover,.extension-menu-section .extension-third-menu>div:hover{background:transparent;color:inherit}
 .company-side-footer{position:relative;margin:auto 16px 16px;padding:14px 4px 0;border-top:1px solid #ddd;display:flex;flex-direction:column;gap:8px;font-size:11px}
 .company-side-footer>b{color:#515a6e}.company-side-footer>button{height:26px;padding:0;border:0;background:transparent;display:flex;align-items:center;justify-content:space-between;color:#89898a;text-align:left}
 .company-model-menu{position:absolute;left:0;right:0;bottom:30px;padding:6px;background:#fff;border:1px solid #d9d9d9;box-shadow:0 4px 12px #00000018}
@@ -302,6 +302,12 @@ html,body{font-family:Roboto,"Noto Sans SC","Microsoft YaHei",Arial,sans-serif;f
 .sticky-actions{left:200px}
 @media(max-width:720px){.company-side-nav{display:none}.app-shell{padding-left:0}.sticky-actions{left:0}}
 
+.site-edit-switcher{display:flex;align-items:center;gap:10px;color:var(--company-muted);font-size:14px}.site-edit-switcher select{width:220px;height:34px;border:1px solid var(--company-border);background:#fff;border-radius:2px;padding:0 30px 0 10px;color:#323233}.site-edit-switcher select:focus{outline:1px solid var(--company-primary);border-color:var(--company-primary)}
+.site-edit-tabs{height:64px;background:#fff;border-top:1px solid var(--company-border);border-bottom:1px solid var(--company-border);display:flex;padding:0 24px;gap:0}.site-edit-tabs>button{height:64px;flex:1;min-width:0;border:0;border-bottom:3px solid transparent;background:#fff;color:#6f6f70;display:flex;align-items:center;gap:10px;padding:0 20px;font-size:14px;text-align:left}.site-edit-tabs>button i{font-style:normal;width:26px;height:26px;border:1px solid #c9cfd3;border-radius:50%;display:grid;place-items:center;background:#fff}.site-edit-tabs>button:hover{color:var(--company-primary);background:#fafcfd}.site-edit-tabs>button.active{color:var(--company-primary);border-bottom-color:var(--company-primary);font-weight:600}.site-edit-tabs>button.active i{background:var(--company-primary);border-color:var(--company-primary);color:#fff}.site-step-blank{min-height:calc(100vh - 286px);background:#fff}
+.asset-config-shell{min-width:0}.asset-config-tabs{height:66px;display:flex;align-items:flex-end;border:1px solid var(--company-border);border-bottom:0;background:#fff;padding:0 18px;gap:34px}.asset-config-tabs>button{height:65px;min-width:210px;padding:10px 4px 8px;border:0;border-bottom:3px solid transparent;background:transparent;text-align:left;display:flex;flex-direction:column;justify-content:center;gap:4px;color:#777}.asset-config-tabs>button span{font-size:16px;font-weight:600}.asset-config-tabs>button small{font-size:12px;color:#999}.asset-config-tabs>button.active{color:var(--company-primary);border-bottom-color:var(--company-primary)}.asset-config-tabs>button.active small{color:#666}.quick-asset-config{border-top:1px solid var(--company-border);padding-top:14px}.manual-asset-list{border-top:1px solid var(--company-border);padding-top:14px}.manual-asset-list>.panel{margin:0}.manual-asset-filter{padding:0 0 14px;margin-bottom:0}.asset-category{border-top:1px solid var(--company-border);margin:0 -12px}.asset-category>header{height:46px;display:flex;align-items:center;gap:9px;padding:0 14px;background:#f7f7f7}.asset-category>header strong{font-size:14px}.asset-category>header span{color:#888;font-size:12px}.asset-category .company-table{margin:0}.asset-category .company-table th:first-child,.asset-category .company-table td:first-child{padding-left:38px}.asset-group-dialog{width:540px}.dialog-form{display:flex;flex-direction:column;gap:18px;margin:20px 0}.dialog-form input,.dialog-form select{height:38px;border:1px solid var(--company-border);padding:0 10px;background:#fff}
+.manual-asset-editor{border-top:1px solid var(--company-border);padding-top:0;background:#fff}.asset-editor-title{height:64px;border:1px solid var(--company-border);border-top:0;display:flex;align-items:center;padding:0 16px;gap:18px}.asset-editor-title>.text-button{margin:0;padding-right:18px;border-right:1px solid #ddd}.asset-editor-title>div{flex:1;display:flex;align-items:baseline;gap:12px}.asset-editor-title h2{font-size:18px;margin:0}.asset-editor-title span{color:#888}.asset-editor-title>.button{margin:0}.asset-editor-workbench{min-height:620px;border:1px solid var(--company-border);border-top:0;display:grid;grid-template-columns:250px 1fr}.asset-editor-side{border-right:1px solid var(--company-border);background:#fafafa}.asset-editor-side>.field{padding:16px;border-bottom:1px solid var(--company-border)}.asset-editor-side>.field input{background:#fff}.asset-batch-entry{width:100%;height:54px;border:0;border-bottom:1px solid var(--company-border);background:#fff;text-align:left;padding:0 16px;font-size:15px}.asset-batch-entry:hover{color:var(--company-primary);background:#f5fbfe}.asset-template-panel{padding:16px;display:flex;flex-direction:column;gap:10px}.asset-template-panel>strong{font-size:16px}.asset-template-panel>select{height:34px;border:1px solid var(--company-border);background:#fff;padding:0 8px}.asset-template-panel>button{min-height:58px;border:1px solid #dce2e6;background:#fff;display:grid;grid-template-columns:24px 1fr;grid-template-rows:1fr 1fr;text-align:left;padding:8px;column-gap:6px}.asset-template-panel>button>span{grid-row:1/3;color:var(--company-primary);align-self:center}.asset-template-panel>button>b{font-size:13px}.asset-template-panel>button>small{font-size:11px;color:#888}.asset-template-panel>button:hover{border-color:var(--company-primary)}.asset-tree-workspace{min-width:0;background:#f7f8f9}.asset-editor-toolbar{height:52px;border-bottom:1px solid var(--company-border);background:#fff;padding:0 12px;display:flex;align-items:center;justify-content:space-between}.asset-editor-toolbar>div{display:flex;align-items:center;gap:8px}.asset-editor-toolbar .button{margin:0;min-width:auto}.asset-tree-search{width:210px;height:30px;border:1px solid var(--company-border);background:#fff;display:flex;align-items:center;padding:0 8px;color:#999}.asset-tree-search input{border:0;outline:0;width:100%;padding-left:7px}.asset-tree-canvas{min-height:567px;padding:28px;overflow:auto}.asset-tree-root-card{width:220px;min-height:54px;margin:0 auto 24px;border:1px solid #b9c3c9;border-left:4px solid var(--company-primary);background:#fff;box-shadow:0 2px 6px #00000014;padding:9px 12px;display:flex;flex-direction:column;gap:3px;position:relative}.asset-tree-root-card:after{content:"";position:absolute;left:50%;bottom:-25px;height:24px;border-left:1px solid #9ecbe0}.asset-tree-root-card small{color:#888}.asset-node-tree{display:flex;justify-content:center;align-items:flex-start;gap:18px}.asset-tree-branch{position:relative;display:flex;flex-direction:column;align-items:center}.asset-node-card{width:190px;min-height:50px;border:1px solid #cbd3d8;background:#fff;box-shadow:0 2px 5px #00000014;display:grid;grid-template-columns:24px 1fr 22px;grid-template-rows:1fr 1fr;text-align:left;padding:7px 8px;column-gap:5px;position:relative}.asset-node-card>span{grid-row:1/3;color:var(--company-primary);align-self:center}.asset-node-card>b{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.asset-node-card>small{color:#888;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.asset-node-card>em{grid-column:3;grid-row:1/3;align-self:center;justify-self:end;font-style:normal;width:20px;height:20px;border-radius:50%;background:#eee;display:grid;place-items:center;color:#777}.asset-node-card:hover,.asset-node-card.selected{border-color:var(--company-primary);box-shadow:0 0 0 1px var(--company-primary)}.asset-tree-children{margin-top:28px;padding-top:0;display:flex;align-items:flex-start;justify-content:center;gap:14px;position:relative}.asset-tree-children:before{content:"";position:absolute;left:50%;top:-28px;height:28px;border-left:1px solid #9ecbe0}.asset-tree-children>.asset-tree-branch:before{content:"";position:absolute;left:50%;top:-14px;height:14px;border-left:1px solid #9ecbe0}.asset-tree-children>.asset-tree-branch:not(:only-child):after{content:"";position:absolute;top:-14px;left:-7px;right:-7px;border-top:1px solid #9ecbe0}.asset-tree-empty{width:420px;margin:60px auto;text-align:center;display:flex;flex-direction:column;gap:8px;color:#888}.asset-tree-empty b{color:#555}.node-drawer-tabs{height:45px;border-bottom:1px solid var(--company-border);display:flex;margin:-8px 0 20px}.node-drawer-tabs button{border:0;border-bottom:3px solid transparent;background:#fff;padding:0 18px;font-size:14px}.node-drawer-tabs button.active{color:var(--company-primary);border-bottom-color:var(--company-primary)}.node-drawer-form{display:flex;flex-direction:column;gap:18px}.node-drawer-form input,.node-drawer-form select,.node-drawer-form textarea{width:100%;border:1px solid var(--company-border);padding:8px;background:#fff}.node-drawer-form input,.node-drawer-form select{height:38px}.node-drawer-form textarea{min-height:110px}.node-config-list>header,.node-config-list>div{display:grid;grid-template-columns:1fr 1fr 90px;gap:8px;margin-bottom:8px}.node-config-list.binding>header,.node-config-list.binding>div{grid-template-columns:1fr 1fr}.node-config-list>header{color:#777;font-size:12px}.node-config-list input{height:36px;border:1px solid var(--company-border);padding:0 8px}.node-config-list>.button{margin:6px 0 0}.batch-create-dialog{width:min(760px,calc(100vw - 40px))}.batch-create-dialog .node-drawer-tabs{margin:10px 0 18px}.batch-table>header,.batch-table>div{display:grid;grid-template-columns:1.4fr 90px 1.4fr 32px;gap:8px;margin-bottom:8px}.batch-table>header{color:#777;font-size:12px}.batch-table input{height:36px;border:1px solid var(--company-border);padding:0 8px}.batch-table>.button{margin:6px 0 10px}.manual-excel-upload{height:180px;border:1px dashed #aebbc3;background:#fafcfd;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;cursor:pointer}.manual-excel-upload>b{font-size:28px;color:var(--company-primary)}.manual-excel-upload>span{font-size:15px}.manual-excel-upload>small{color:#888}
+@media(max-width:1100px){.asset-editor-workbench{grid-template-columns:220px 1fr}.asset-node-card{width:160px}.asset-tree-children{flex-direction:column}.asset-tree-children>.asset-tree-branch:after{display:none}.asset-config-tabs{gap:18px}.asset-config-tabs>button{min-width:180px}}
+
 /* Resource center list pages */
 .resource-list-panel>.panel-body{padding:0 16px 12px;overflow-x:auto}
 .resource-table{min-width:980px;table-layout:auto}.resource-table th,.resource-table td{vertical-align:middle}.resource-table th:last-child,.resource-table td:last-child{width:64px;text-align:center}
@@ -509,3 +515,20 @@ html,body{font-family:Roboto,"Noto Sans SC","Microsoft YaHei",Arial,sans-serif;f
 @media(max-width:1120px){.wise-powered-divider,.wise-powered-logo{display:none}.wise-avatar{margin-right:8px}}
 @media(max-width:700px){.wise-header-secondary{display:none!important}.wise-product-switch>strong{display:none}.wise-header-actions>button:not(.wise-avatar){width:36px;flex-basis:36px}}
 @media(max-width:560px){.wise-logo-link{display:none}.wise-product-switch{margin-left:4px}.wise-header-actions>button:nth-child(1),.wise-header-actions>button:nth-child(2){display:none}}
+
+/* 当前站点资产组编辑器:直接进入详情,不再经过资产组列表。 */
+.asset-editor-loading{min-height:680px;border:1px solid var(--company-border);border-top:0;background:#fff;display:flex;align-items:center;justify-content:center;gap:14px;color:#666}.asset-editor-loading.error{color:#c4382c}
+.manual-asset-editor{border-top:1px solid var(--company-border);background:#fff}.asset-editor-title{height:58px;border:1px solid var(--company-border);border-top:0;padding:0 20px}.asset-editor-title>div{display:flex;align-items:baseline;gap:18px}.asset-editor-title h2{font-size:20px;font-weight:600;color:#222}.asset-editor-title span{font-size:13px;color:#777}.asset-editor-workbench{min-height:680px;height:calc(100vh - 306px);grid-template-columns:232px minmax(720px,1fr);border-color:var(--company-border)}
+.asset-editor-side{background:#fff;border-right:1px solid var(--company-border)}.asset-editor-side>.field{padding:14px 20px 15px}.asset-editor-side>.field>span{font-size:15px;font-weight:600;color:#222;margin-bottom:9px}.asset-editor-side>.field input{height:36px;border:1px solid #cfd5da;padding:0 9px}.asset-batch-entry{height:72px;padding:0 20px;font-size:15px;color:#222}.asset-batch-entry:hover{color:var(--company-primary);background:#f1f9fd}.asset-template-panel{padding:14px 20px;gap:12px}.asset-template-panel>strong{font-size:16px;font-weight:600;color:#222}.asset-template-panel>select{height:34px}.asset-template-panel>button{min-height:50px;border:0;background:transparent;grid-template-columns:24px 1fr;padding:7px 6px}.asset-template-panel>button:hover{background:#eef8fd;border:0}.asset-template-panel>button>span{font-size:18px}.asset-template-panel>button>small{color:#8a8a8a}
+.asset-tree-workspace{background:#fff;display:flex;flex-direction:column;min-height:0}.asset-editor-toolbar{height:49px;min-height:49px;padding:0 14px;position:relative}.asset-editor-toolbar>div{gap:2px}.asset-tool-group,.asset-canvas-tools{display:flex;align-items:center}.asset-tool-button.ant-btn{width:34px;min-width:34px;height:32px;padding:0;border-radius:2px;color:#7f8a91;font-size:17px}.asset-tool-button.ant-btn:not(:disabled){color:var(--company-primary)}.asset-tool-button.ant-btn:hover:not(:disabled){color:#0078b9;background:#eef8fd}.asset-tool-button.primary.ant-btn{color:var(--company-primary)}.asset-tool-divider{height:22px;border-left:1px solid #d9dde0;margin:0 5px}.asset-view-switch{position:absolute;left:50%;transform:translateX(-50%);height:32px;border:1px solid #cfd5da;display:flex!important;gap:0!important}.asset-view-switch button{width:38px;border:0;border-right:1px solid #cfd5da;background:#fff;color:#7b8790;font-size:16px}.asset-view-switch button:last-child{border-right:0}.asset-view-switch button.active{color:var(--company-primary);background:#edf8fd}.asset-tree-search{width:172px;height:30px;margin-left:8px;color:#8b969d}.asset-tree-search input{font-size:12px}
+.asset-tree-canvas{flex:1;min-height:625px;padding:44px;overflow:auto;background:#fbfbfb;cursor:grab;touch-action:none;user-select:none}.asset-tree-canvas.dragging{cursor:grabbing}.asset-tree-stage{width:max-content;min-width:100%;min-height:100%;display:flex;align-items:center;justify-content:center;transform-origin:center center;transition:transform .12s ease}.dragging .asset-tree-stage{transition:none}.asset-node-tree{display:flex;flex-direction:column;justify-content:center;align-items:flex-start;gap:26px;min-width:max-content}.asset-tree-branch{display:flex;flex-direction:row;align-items:center;position:relative}.asset-node-card{width:174px;min-height:42px;grid-template-columns:24px 1fr 22px;grid-template-rows:1fr 1fr;padding:6px 7px;border:0;border-radius:2px;box-shadow:0 2px 7px #00000025;background:#fff;cursor:pointer}.asset-node-card>span{font-size:18px}.asset-node-card>b{font-size:13px;font-weight:500;color:#5d6469}.asset-node-card>small{font-size:11px;color:#92999d}.asset-node-card>button{grid-column:3;grid-row:1/3;align-self:center;justify-self:end;width:19px;height:19px;border:0;border-radius:50%;padding:0;background:#c4c8ca;color:#fff;display:grid;place-items:center;font-size:10px}.asset-node-card>button:hover{background:var(--company-primary)}.asset-node-card:hover,.asset-node-card.selected{border:0;box-shadow:0 0 0 1px var(--company-primary),0 2px 7px #00000025}.asset-node-card.matched{box-shadow:0 0 0 2px #f5a623,0 2px 7px #00000025}.asset-tree-children{margin:0 0 0 34px;padding:0 0 0 34px;display:flex;flex-direction:column;align-items:flex-start;justify-content:center;gap:17px;position:relative}.asset-tree-children:before{left:0;top:21px;bottom:21px;height:auto;border-left:1px solid #159bd3}.asset-tree-children>.asset-tree-branch:before{content:"";position:absolute;left:-34px;top:50%;width:34px;height:0;border-top:1px solid #159bd3}.asset-tree-children>.asset-tree-branch:not(:only-child):after{display:none}.asset-tree-branch>.asset-node-card+.asset-tree-children:after{content:"";position:absolute;left:-34px;top:50%;width:34px;border-top:1px solid #159bd3}.asset-tree-empty{margin:80px auto;color:#8a8a8a}.asset-tree-empty b{font-weight:600}
+.asset-node-list{flex:1;min-height:625px;overflow:auto;background:#fff}.asset-node-list-head,.asset-node-list>button{height:43px;display:grid;grid-template-columns:minmax(260px,1.5fr) 120px minmax(180px,1fr) 90px;align-items:center;text-align:left;border:0;border-bottom:1px solid #e4e7e9;padding:0 16px;background:#fff;color:#555}.asset-node-list-head{height:40px;background:#f4f6f7;font-size:12px;font-weight:600;color:#616b71}.asset-node-list>button:hover,.asset-node-list>button.selected{background:#eef8fd}.asset-node-list>button.selected{box-shadow:inset 3px 0 var(--company-primary)}.asset-node-list>button>span:first-child{color:#333}
+@media(max-width:1100px){.asset-editor-workbench{grid-template-columns:210px minmax(660px,1fr)}.asset-editor-title span{display:none}.asset-tree-search{width:130px}.asset-zoom-value{display:none}}
+
+/* Asset 节点配置抽屉:对齐原资产设定的三 Tab 信息架构。 */
+.asset-node-drawer .ant-drawer-header{min-height:56px;padding:0 24px;border-bottom:1px solid #d7dade}.asset-node-drawer .ant-drawer-title{font-size:16px;font-weight:500;color:#222}.asset-node-drawer .ant-drawer-body{padding:0;overflow:auto}.asset-node-drawer .ant-drawer-footer{height:56px;padding:11px 24px;border-top:1px solid #d7dade;background:#fff}.node-drawer-footer{display:flex;justify-content:flex-end;align-items:center;gap:8px}.node-drawer-footer .button{margin:0}
+.asset-node-drawer .node-drawer-tabs{height:49px;margin:0;padding-left:20px;gap:0;border-bottom:1px solid #d7dade}.asset-node-drawer .node-drawer-tabs button{height:49px;padding:0 20px;border-right:1px solid #d7dade;border-bottom:3px solid transparent;color:#777}.asset-node-drawer .node-drawer-tabs button:first-child{padding-left:0;margin-right:20px}.asset-node-drawer .node-drawer-tabs button.active{color:#555;border-bottom-color:var(--company-primary)}
+.node-config-page{padding:24px 24px 48px;display:flex;flex-direction:column;gap:38px}.node-config-section>h3{height:22px;line-height:22px;margin:0 0 24px;padding-left:17px;border-left:4px solid var(--company-primary);font-size:16px;font-weight:600;color:#333}.node-config-section>h4,.binding-parameter-head h4{margin:0 0 12px;font-size:16px;color:#3f4549}.node-id-row{display:grid;grid-template-columns:105px 1fr;align-items:center;max-width:420px;margin:0 0 20px 47px;color:#777}.node-id-row b{font-weight:400;color:#56606a}.node-basic-grid{display:grid;grid-template-columns:1fr 1fr;gap:18px 36px}.node-basic-grid .field,.node-property-grid .field{display:grid;grid-template-columns:105px minmax(0,1fr);align-items:center;gap:12px}.node-basic-grid .field>span,.node-property-grid .field>span{font-size:14px;font-weight:400;color:#666;text-align:right}.node-basic-grid .field input,.node-basic-grid .field select,.node-property-grid .field input{width:100%;height:34px;padding:0 10px;border:1px solid #ced5da}.node-basic-grid .field:nth-child(5){grid-column:1/-1}.node-image-row{display:flex;align-items:flex-start;gap:12px;margin-left:20px}.node-image-preview{width:108px;height:108px;border:1px solid #cbd2d7;background:#fafafa;display:grid;place-items:center;overflow:hidden;color:#9da8ae;font-size:36px;cursor:pointer}.node-image-preview img{width:100%;height:100%;object-fit:contain}.node-image-row>div{display:flex;flex-direction:column;align-items:flex-start;gap:5px}.node-upload-link{color:var(--company-primary);font-size:14px;cursor:pointer}.node-image-row small{font-size:12px;color:#aaa}.node-image-row .text-button{margin:3px 0 0}.node-config-section .ant-qrcode{margin-left:20px;padding:0;background:#fff}
+.node-property-grid{display:grid;grid-template-columns:1fr 1fr;gap:18px 36px}.node-property-grid .field{min-width:0}.node-property-grid .field:nth-last-child(2){grid-column:auto}.node-property-grid input[type="date"]{color:#666}
+.binding-page{gap:28px}.edge-component-table{border:1px solid #d5dade;max-height:200px;overflow:auto}.edge-component-table>header,.edge-component-table>div{min-height:45px;display:grid;grid-template-columns:1.2fr 1fr 1fr 1.2fr 88px;align-items:center;padding:0 8px;border-bottom:1px solid #dfe3e6}.edge-component-table>header{min-height:38px;background:#f4f5f6;border-bottom:2px solid var(--company-primary);font-size:12px;font-weight:600;color:#444}.edge-component-table>div:last-child{border-bottom:0}.edge-component-table>div.active{background:#f1f9fd}.edge-component-table b{font-weight:400}.edge-component-table>div>span:last-child{display:flex;justify-content:flex-end;gap:7px}.edge-component-table button{width:28px;height:28px;border:0;background:transparent;color:var(--company-primary);font-size:17px}.edge-component-table button:hover{background:#e8f6fc}.binding-component-tabs{height:42px;display:flex;align-items:flex-end;justify-content:center;margin-bottom:12px}.binding-component-tabs button{height:34px;padding:0 20px;border:0;border-right:1px solid #d7dade;background:#fff;color:#a0a6aa}.binding-component-tabs button:last-child{border-right:0}.binding-component-tabs button.active{color:var(--company-primary);font-weight:600}.binding-parameter-head{display:flex;align-items:center;justify-content:space-between;border:1px solid #d5dade;border-bottom:0;padding:8px 12px}.binding-parameter-head h4{margin:0}.binding-parameter-head .button{height:30px;margin:0}.binding-parameter-table{border:1px solid #d5dade;overflow:auto}.binding-parameter-table>header,.binding-parameter-table>div:not(.binding-empty){min-width:760px;display:grid;grid-template-columns:1fr 1.2fr 70px 64px 1fr 1.2fr 42px;align-items:center;gap:6px;padding:0 8px}.binding-parameter-table>header{height:38px;background:#f4f5f6;border-bottom:2px solid var(--company-primary);font-size:12px;font-weight:600}.binding-parameter-table>div:not(.binding-empty){min-height:44px;border-bottom:1px solid #e1e4e6}.binding-parameter-table input{width:100%;height:30px;border:1px solid #d3d8dc;padding:0 6px}.binding-parameter-table .trash{color:#d84b3f}.binding-empty{height:120px;display:grid;place-items:center;color:#999}.binding-import-backdrop{z-index:1300}.binding-import-dialog{width:min(620px,calc(100vw - 48px))}
+@media(max-width:1200px){.asset-node-drawer .ant-drawer-content-wrapper{width:78vw!important}.node-basic-grid,.node-property-grid{grid-template-columns:1fr}.node-basic-grid .field:nth-child(5){grid-column:auto}}@media(max-width:760px){.asset-node-drawer .ant-drawer-content-wrapper{width:100vw!important}.node-basic-grid .field,.node-property-grid .field{grid-template-columns:90px minmax(0,1fr)}.node-config-page{padding:20px 16px}.edge-component-table>header,.edge-component-table>div{grid-template-columns:1.2fr 1fr 1fr 72px}.edge-component-table>header>span:nth-child(4),.edge-component-table>div>span:nth-child(4){display:none}}
index aa509bf55a87665f7efc49ec6774921121e5be90..32342e91162c8caec3b0f7610cb85b4cc222dbe9 100644 (file)
@@ -1,17 +1,19 @@
 "use client";
 
 import { useEffect, useMemo, useRef, useState, type PointerEvent as ReactPointerEvent } from "react";
-import { Button as AntButton, Drawer, Dropdown, Tag } from "antd";
-import { CaretDownOutlined, CaretRightOutlined, DeleteOutlined, DownloadOutlined, EditOutlined, EllipsisOutlined, EyeOutlined, RobotOutlined } from "@ant-design/icons";
+import { Button as AntButton, Drawer, Dropdown, QRCode, Tag } from "antd";
+import { AimOutlined, ApartmentOutlined, BarsOutlined, BlockOutlined, CaretDownOutlined, CaretRightOutlined, ControlOutlined, CopyOutlined, DeleteOutlined, DownloadOutlined, EditOutlined, EllipsisOutlined, EyeOutlined, LinkOutlined, PlusOutlined, PlusSquareOutlined, RobotOutlined, SearchOutlined, SnippetsOutlined, UploadOutlined, ZoomInOutlined, ZoomOutOutlined } from "@ant-design/icons";
 
-type PageKey = "login" | "projects" | "basic" | "assets" | "functions" | "generate" | "result" | "preview" | "domains" | "domainDetail" | "styleLibrary" | "deviceTemplates" | "toolSettings" | "domainSettings";
+type PageKey = "login" | "projects" | "siteEdit" | "basic" | "assets" | "pageConfig" | "functions" | "generate" | "result" | "preview" | "domains" | "domainDetail" | "styleLibrary" | "deviceTemplates" | "toolSettings" | "domainSettings";
 type ModalKey = "" | "feedback" | "download" | "history" | "prompt" | "completePrompt";
 
 const meta: Record<PageKey, { label: string; icon: string; subtitle: string }> = {
   login:{label:"内部登录",icon:"◇",subtitle:"使用公司账号登录"},
   projects:{label:"站点项目",icon:"▦",subtitle:"创建、查找和管理站点生成项目"},
+  siteEdit:{label:"站点编辑",icon:"",subtitle:"维护已建站点的基本属性与参考资料"},
   basic:{label:"站点基本属性",icon:"▤",subtitle:"描述站点业务背景,信息越完整,生成结果越符合预期"},
-  assets:{label:"Asset 资产设置",icon:"▧",subtitle:"配置设备类型、数量和资产层级,为页面和模拟数据提供基础"},
+  assets:{label:"站点资产设置",icon:"▧",subtitle:"通过快速配置或手动配置建立场站资产结构"},
+  pageConfig:{label:"页面配置",icon:"",subtitle:""},
   functions:{label:"功能与菜单",icon:"☷",subtitle:"选择 MU 标准页面,定义 Cube 扩展页面及菜单结构"},
   generate:{label:"生成确认",icon:"↻",subtitle:"确认生成范围,调用 Cube Skill 生成页面和模拟数据"},
   result:{label:"站点结果",icon:"◫",subtitle:"集中查看、预览、校验并下载站点交付物"},
@@ -55,6 +57,11 @@ type PageStyle = { id:number; name:string; created_at?:string };
 type DomainAsset = { id?:number; device_template_id:number; device_name:string; count:number; rule:string; sort_order:number };
 type DomainPage = { id:number; name:string; description:string; json_text:string; status:string; gen_filename?:string|null; prompt?:string|null; source?:string|null; sort_order:number };
 type DomainStepKey = "basic" | "assets" | "functions" | "opsflow" | "knowledge" | "generate" | "expert";
+type SiteEditSection = "basic" | "assets" | "pageConfig" | "functions" | "generate";
+const SITE_EDIT_STEPS:SiteEditSection[] = ["basic","assets","pageConfig","functions","generate"];
+type ManualAssetBinding = {source:string;name?:string;point:string;collect_type?:string;group?:string;device_name?:string;unit?:string;precision?:string;mapping?:string;description?:string};
+type ManualAssetNode = { id:string; name:string; alias?:string; asset_type?:string; image_data?:string; node_type:"group"|"device"; template_name:string; description:string; properties:Array<{name:string;value:string;unit:string}>; bindings:ManualAssetBinding[]; children:ManualAssetNode[] };
+type ManualAssetGroup = { id:number; site_id:number; name:string; category:string; template_name:string; structure:ManualAssetNode[]; created_at:string; updated_at:string };
 
 // ---- helpers ----
 function Status({children}:{children:React.ReactNode}){const v=String(children);const tone=/不可用|失败|异常|failed/.test(v)?"bad":/完成|正常|成功|通过|推荐|可用|已配置|已生成|已上传|generated/.test(v)?"ok":/生成中|校验中|告警|running/.test(v)?"warn":"muted";return <Tag className={"status "+tone}>{children}</Tag>}
@@ -120,6 +127,8 @@ function countChars(v:string){ return Array.from(v||"").length; }
 export default function Home(){
   const [page,setPage]=useState<PageKey>("projects");
   const [projectNavOpen,setProjectNavOpen]=useState(true);
+  const [extensionNavOpen,setExtensionNavOpen]=useState(true);
+  const [reportNavOpen,setReportNavOpen]=useState(true);
   const [resourceNavOpen,setResourceNavOpen]=useState(true);
   const [systemNavOpen,setSystemNavOpen]=useState(true);
   const [toast,setToast]=useState("");
@@ -138,6 +147,7 @@ export default function Home(){
   });
   const [fabPos,setFabPos]=useState<{x:number;y:number}|null>(()=>{ try{ const s=localStorage.getItem("ai-fab-pos"); return s?JSON.parse(s):null; }catch{ return null; } });
   const fabRef=useRef<HTMLButtonElement|null>(null);
+  const routeAppliedRef=useRef(false);
   const dragState=useRef<{active:boolean;startX:number;startY:number;origX:number;origY:number;moved:boolean}|null>(null);
   const lastPosRef=useRef<{x:number;y:number}|null>(null);
   const onFabPointerDown=(e:ReactPointerEvent)=>{
@@ -188,6 +198,8 @@ export default function Home(){
   const [newDomainName,setNewDomainName]=useState("");
 
   const [view,setView]=useState<"table"|"card">("table");
+  const [siteEditSection,setSiteEditSection]=useState<SiteEditSection>("basic");
+  const [assetConfigMode,setAssetConfigMode]=useState<"quick"|"manual">("quick");
   const [functionTab,setFunctionTab]=useState<"mu"|"cube"|"menu">("mu");
   const [previewTab,setPreviewTab]=useState<"info"|"json"|"feedback">("info");
   const [feedbackCube,setFeedbackCube]=useState<Cube|null>(null);
@@ -389,11 +401,22 @@ export default function Home(){
 
   const go=async(target:PageKey, sid?:number|"new")=>{
     try{
+      if(target==="siteEdit")setSiteEditSection("basic");
       // 显式要求新建站点:始终创建空白站点,避免复用/误建
       if(sid==="new"){ await createSite(); setPage(target); window.history.replaceState({}, "", "?page="+target); return; }
-      const curId = sid ?? siteId;
+      let curId = sid ?? siteId;
+      if(target==="siteEdit" && !curId){
+        const first = sites[0];
+        if(!first){
+          setPage("projects");
+          window.history.replaceState({}, "", "?page=projects");
+          notify("暂无可编辑站点,请先新建站点");
+          return;
+        }
+        curId = first.id;
+      }
       // 详情类页面必须依附于已选站点:无站点时不自动建站,回站点列表并提示
-      const needsSite = ["basic","assets","functions","generate","result","preview"].includes(target);
+      const needsSite = ["siteEdit","basic","assets","pageConfig","functions","generate","result","preview"].includes(target);
       if(needsSite && !curId){
         setPage("projects");
         window.history.replaceState({}, "", "?page=projects");
@@ -410,6 +433,16 @@ export default function Home(){
     }
   };
 
+  useEffect(()=>{
+    if(loading||routeAppliedRef.current)return;
+    routeAppliedRef.current=true;
+    const params=new URLSearchParams(window.location.search);
+    const target=params.get("page") as PageKey|null;
+    if(!target||target==="projects"||!Object.prototype.hasOwnProperty.call(meta,target))return;
+    const rawId=Number(params.get("id"));
+    void go(target,Number.isInteger(rawId)&&rawId>0?rawId:undefined);
+  },[loading]);
+
   const saveSiteBasic=async(patch:Partial<SiteDetail>)=>{
     if(!site) return;
     const next = {...site, ...patch};
@@ -496,9 +529,11 @@ export default function Home(){
 
   if(page==="login")return <Login onLogin={()=>go("projects")}/>;
 
-  const projectArea = (["projects","basic","assets","functions","generate","result","preview"] as PageKey[]).includes(page);
+  const projectArea = (["projects","siteEdit","basic","assets","pageConfig","functions","generate","result","preview"] as PageKey[]).includes(page);
   const resourceArea = (["domains","domainDetail","deviceTemplates","styleLibrary"] as PageKey[]).includes(page);
   const systemArea = (["toolSettings","domainSettings"] as PageKey[]).includes(page);
+  const siteEditMeta = meta[siteEditSection];
+  const manualAssetActive = assetConfigMode==="manual" && (page==="assets" || (page==="siteEdit"&&siteEditSection==="assets"));
 
   return <div className="app-shell">
     <header className="topbar wise-header">
@@ -524,7 +559,24 @@ export default function Home(){
       <nav className="side-menu" aria-label="StationBuilder 主菜单">
         <section className={projectArea?"menu-section current":"menu-section"}>
           <button className="menu-section-title" aria-expanded={projectNavOpen} onClick={()=>setProjectNavOpen(v=>!v)}><span>站点管理</span>{projectNavOpen?<CaretDownOutlined/>:<CaretRightOutlined/>}</button>
-          {projectNavOpen&&<div className="menu-section-items"><button className={projectArea?"active":""} onClick={()=>go("projects")}><span>站点项目</span></button></div>}
+          {projectNavOpen&&<div className="menu-section-items">
+            <button className={page==="projects"?"active":""} onClick={()=>go("projects")}><span>站点项目</span></button>
+            <button className={page==="siteEdit"?"active":""} onClick={()=>go("siteEdit",sites[0]?.id)}><span>站点编辑</span></button>
+          </div>}
+        </section>
+        <section className="menu-section extension-menu-section" aria-label="扩展页面菜单">
+          <button className="menu-section-title" aria-expanded={extensionNavOpen} onClick={()=>setExtensionNavOpen(v=>!v)}><span>扩展页面</span>{extensionNavOpen?<CaretDownOutlined/>:<CaretRightOutlined/>}</button>
+          {extensionNavOpen&&<div className="menu-section-items extension-menu-items">
+            <div className="extension-menu-item">仪表盘</div>
+            <div className="extension-submenu">
+              <button className="extension-menu-item extension-submenu-title" aria-expanded={reportNavOpen} onClick={()=>setReportNavOpen(v=>!v)}><span>报表</span>{reportNavOpen?<CaretDownOutlined/>:<CaretRightOutlined/>}</button>
+              {reportNavOpen&&<div className="extension-third-menu">
+                <div>仪表盘报表</div>
+                <div>文档报表</div>
+              </div>}
+            </div>
+            <div className="extension-menu-item">数据源</div>
+          </div>}
         </section>
         <section className={resourceArea?"menu-section current":"menu-section"}>
           <button className="menu-section-title" aria-expanded={resourceNavOpen} onClick={()=>setResourceNavOpen(v=>!v)}><span>方案中心</span>{resourceNavOpen?<CaretDownOutlined/>:<CaretRightOutlined/>}</button>
@@ -543,7 +595,7 @@ export default function Home(){
         </section>
       </nav>
     </aside>
-    <aside className="side-nav"><div className="module-switch"><button className={(["projects","basic","assets","functions","generate","result","preview"] as PageKey[]).includes(page)?"active":""} onClick={()=>go("projects")}><b>▦</b><span>StationBuilder<small>站点生成工具</small></span></button><button className={(["domains","domainDetail","deviceTemplates"] as PageKey[]).includes(page)?"active":""} onClick={()=>{setDomainFilter(null); go("domains");}}><b>◆</b><span>领域方案库<small>行业标准方案</small></span></button><button className={page==="styleLibrary"?"active":""} onClick={()=>go("styleLibrary")}><b>❖</b><span>页面样式库<small>页面视觉样式</small></span></button></div>{page==="styleLibrary"?<><small>样式库导航</small><button className={page==="styleLibrary"?"active":""} onClick={()=>go("styleLibrary")}><b>❖</b>全部页面样式</button></>:(["domains","domainDetail","deviceTemplates"] as PageKey[]).includes(page)?<><small>方案库导航</small><button className={page==="domains"?"active":""} onClick={()=>{setDomainFilter(null); go("domains");}}><b>▦</b>领域方案</button><button className={page==="deviceTemplates"?"active":""} onClick={()=>go("deviceTemplates")}><b>⬡</b>设备模版</button><small>领域分类</small>
+    <aside className="side-nav"><div className="module-switch"><button className={(["projects","basic","assets","pageConfig","functions","generate","result","preview"] as PageKey[]).includes(page)?"active":""} onClick={()=>go("projects")}><b>▦</b><span>StationBuilder<small>站点生成工具</small></span></button><button className={(["domains","domainDetail","deviceTemplates"] as PageKey[]).includes(page)?"active":""} onClick={()=>{setDomainFilter(null); go("domains");}}><b>◆</b><span>领域方案库<small>行业标准方案</small></span></button><button className={page==="styleLibrary"?"active":""} onClick={()=>go("styleLibrary")}><b>❖</b><span>页面样式库<small>页面视觉样式</small></span></button></div>{page==="styleLibrary"?<><small>样式库导航</small><button className={page==="styleLibrary"?"active":""} onClick={()=>go("styleLibrary")}><b>❖</b>全部页面样式</button></>:(["domains","domainDetail","deviceTemplates"] as PageKey[]).includes(page)?<><small>方案库导航</small><button className={page==="domains"?"active":""} onClick={()=>{setDomainFilter(null); go("domains");}}><b>▦</b>领域方案</button><button className={page==="deviceTemplates"?"active":""} onClick={()=>go("deviceTemplates")}><b>⬡</b>设备模版</button><small>领域分类</small>
 {domainOptions.map((d:any)=>(
   <div key={d.id} className={"domain-nav-row"+(page==="domains"&&domainFilter===d.id?" active":"")}>
     <button className={(page==="domains"&&domainFilter===d.id)?"active":""} onClick={()=>{setDomainFilter(d.id); go("domains");}}><b>◆</b><span className="domain-nav-name">{d.name}</span></button>
@@ -558,7 +610,7 @@ export default function Home(){
   </div>
 ) : (
   <div className="domain-nav-add-cta"><button className="domain-nav-add" onClick={()=>{setAddingDomain(true);setNewDomainName("");}}><b>+</b>新增领域</button></div>
-)}</>:<><small>站点列表</small><button className={page==="projects"?"active":""} onClick={()=>go("projects")}><b>▦</b>站点项目</button><small>站点详情</small><select className="site-switcher" value={siteId ?? ""} onChange={e=>{const v=e.target.value; if(v){ const t=(page==="projects"||["domains","domainDetail","styleLibrary","deviceTemplates"].includes(page))?"basic":page; go(t, Number(v)); }}}><option value="">选择站点…</option>{sites.map(s=><option key={s.id} value={s.id}>{s.name||"未命名站点"}</option>)}</select><small>生成向导</small>{(["basic","assets","functions","generate"] as PageKey[]).map((item,index)=><button key={item} className={page===item?"active":""} onClick={()=>go(item)}><em>{index+1}</em>{meta[item].label}</button>)}<small>方案交付</small><button className={page==="result"?"active":""} onClick={()=>go("result")}><b>◫</b>站点结果</button><button className={page==="preview"?"active":""} onClick={()=>go("preview")}><b>▣</b>页面预览</button></>}<div className="side-help">
+)}</>:<><small>站点列表</small><button className={page==="projects"?"active":""} onClick={()=>go("projects")}><b>▦</b>站点项目</button><small>站点详情</small><select className="site-switcher" value={siteId ?? ""} onChange={e=>{const v=e.target.value; if(v){ const t=(page==="projects"||["domains","domainDetail","styleLibrary","deviceTemplates"].includes(page))?"basic":page; go(t, Number(v)); }}}><option value="">选择站点…</option>{sites.map(s=><option key={s.id} value={s.id}>{s.name||"未命名站点"}</option>)}</select><small>生成向导</small>{SITE_EDIT_STEPS.map((item,index)=><button key={item} className={page===item?"active":""} onClick={()=>go(item)}><em>{index+1}</em>{meta[item].label}</button>)}<small>方案交付</small><button className={page==="result"?"active":""} onClick={()=>go("result")}><b>◫</b>站点结果</button><button className={page==="preview"?"active":""} onClick={()=>go("preview")}><b>▣</b>页面预览</button></>}<div className="side-help">
     <b>内部工具 · v0.3</b>
     <span className="model-trigger" onClick={() => setModelOpen(o => !o)}>
       Wordbuddy AI · {model}<span className="caret">▾</span>
@@ -580,7 +632,7 @@ export default function Home(){
       </>
     )}
   </div></aside>
-    <main className="workspace"><div className="crumb">iStation StationBuilder / <b>{["domains","domainDetail","styleLibrary","deviceTemplates","toolSettings","domainSettings"].includes(page)?meta[page].label:page==="projects"?"站点列表":`站点详情 / ${meta[page].label}`}</b></div><div className="page-heading"><div><h1>{meta[page].label}</h1><p>{meta[page].subtitle}</p></div><div>{page==="projects"&&<Button onClick={()=>go("basic","new")}>+ 新建站点</Button>}{page==="domains"&&openDomainTemplateId==null&&<Button onClick={()=>setOpenDomainTemplateId("create")}>+ 新增领域方案</Button>}{page==="domains"&&openDomainTemplateId!=null&&<Button secondary onClick={()=>setOpenDomainTemplateId(null)}>← 返回方案列表</Button>}{domainFilter!=null&&page==="domains"&&openDomainTemplateId==null&&<Button secondary onClick={()=>{setDomainFilter(null);}}>清除领域筛选:{domainOptions.find(d=>d.id===domainFilter)?.name||""}</Button>}{page==="domainSettings"&&<Button onClick={()=>{setNewDomainName("");setAddingDomain(true);}}>+ 新增领域</Button>}</div></div>{(["basic","assets","functions","generate"] as PageKey[]).includes(page)&&<Steps page={page} go={go}/>}<div className="content">
+    <main className="workspace"><div className="crumb">iStation StationBuilder / <b>{["domains","domainDetail","styleLibrary","deviceTemplates","toolSettings","domainSettings"].includes(page)?meta[page].label:page==="projects"?"站点列表":page==="siteEdit"?`站点管理 / 站点编辑 / ${siteEditMeta.label}`:`站点详情 / ${meta[page].label}`}</b></div><div className="page-heading"><div><h1>{page==="siteEdit"?siteEditMeta.label:meta[page].label}</h1><p>{page==="siteEdit"?siteEditMeta.subtitle:meta[page].subtitle}</p></div><div>{page==="projects"&&<Button onClick={()=>go("basic","new")}>+ 新建站点</Button>}{page==="siteEdit"&&<label className="site-edit-switcher"><span>当前站点</span><select value={siteId??""} onChange={async e=>{const id=Number(e.target.value);await loadSite(id);window.history.replaceState({},"",`?page=siteEdit&id=${id}`);}}>{sites.map(item=><option key={item.id} value={item.id}>{item.name||"未命名站点"}</option>)}</select></label>}{page==="domains"&&openDomainTemplateId==null&&<Button onClick={()=>setOpenDomainTemplateId("create")}>+ 新增领域方案</Button>}{page==="domains"&&openDomainTemplateId!=null&&<Button secondary onClick={()=>setOpenDomainTemplateId(null)}>← 返回方案列表</Button>}{domainFilter!=null&&page==="domains"&&openDomainTemplateId==null&&<Button secondary onClick={()=>{setDomainFilter(null);}}>清除领域筛选:{domainOptions.find(d=>d.id===domainFilter)?.name||""}</Button>}{page==="domainSettings"&&<Button onClick={()=>{setNewDomainName("");setAddingDomain(true);}}>+ 新增领域</Button>}</div></div>{page==="siteEdit"&&<SiteEditTabs section={siteEditSection} onChange={setSiteEditSection}/>} {SITE_EDIT_STEPS.includes(page as SiteEditSection)&&<Steps page={page} go={go}/>}<div className="content">
       {page==="projects"&&<Projects view={view} setView={setView} sites={sites} loading={loading} go={go} loadSite={loadSite} notify={notify} onDelete={deleteSites}/>}
       {(page==="domains"||page==="domainDetail")&&(openDomainTemplateId!=null
         ? <DomainDetail id={openDomainTemplateId==="create"?null:openDomainTemplateId} go={go} notify={notify} onBack={()=>setOpenDomainTemplateId(null)} onCreated={setOpenDomainTemplateId} viewCompletePrompt={viewDtCompletePrompt} viewCompleteAssetsPrompt={viewDtCompleteAssetsPrompt} viewCompleteCubesPrompt={viewDtCompleteCubesPrompt} pageStyles={pageStyles}/>
@@ -589,25 +641,39 @@ export default function Home(){
       {page==="deviceTemplates"&&<CompanyDeviceTemplates notify={notify} onViewCpPrompt={viewDeviceCpCompletePrompt}/>}
       {page==="toolSettings"&&<InternalToolSettings model={model} onModelChange={setModel} notify={notify}/>}
       {page==="domainSettings"&&<DomainSettings domains={domainOptions} notify={notify} onDeleted={handleDomainDeleted}/>}
-      {page==="basic"&&<Basic site={site} domains={domains} onChange={saveSiteBasic} onAddDomain={addDomain} onUpload={uploadFile} saving={saving} notify={notify} onViewCompletePrompt={viewCompletePrompt}/>}
-      {page==="assets"&&<Assets site={site} onChange={saveAssets} saving={saving} notify={notify} onViewCompletePrompt={viewCompleteAssetsPrompt}/>}
+      {(page==="basic"||(page==="siteEdit"&&siteEditSection==="basic"))&&<Basic site={site} domains={domains} onChange={saveSiteBasic} onAddDomain={addDomain} onUpload={uploadFile} saving={saving} notify={notify} onViewCompletePrompt={viewCompletePrompt}/>}
+      {page==="assets"&&<Assets site={site} onChange={saveAssets} saving={saving} notify={notify} onViewCompletePrompt={viewCompleteAssetsPrompt} mode={assetConfigMode} onModeChange={setAssetConfigMode}/>}
+      {page==="siteEdit"&&siteEditSection==="assets"&&<Assets site={site} onChange={saveAssets} saving={saving} notify={notify} onViewCompletePrompt={viewCompleteAssetsPrompt} mode={assetConfigMode} onModeChange={setAssetConfigMode}/>}
+      {(page==="pageConfig"||(page==="siteEdit"&&siteEditSection==="pageConfig"))&&<div className="site-step-blank" aria-label="页面配置内容区域"/>}
       {page==="functions"&&<Functions site={site} muTree={muTree} onMuChange={saveMu} onSiteChange={setSite} pageStyles={pageStyles} notify={notify} onViewCompletePrompt={viewCompleteCubesPrompt}/>}
+      {page==="siteEdit"&&siteEditSection==="functions"&&<Functions site={site} muTree={muTree} onMuChange={saveMu} onSiteChange={setSite} pageStyles={pageStyles} notify={notify} onViewCompletePrompt={viewCompleteCubesPrompt}/>}
       {page==="generate"&&<Generate site={site} onSiteChange={setSite} notify={notify} viewPrompt={viewPrompt} viewSitePrompt={viewSitePrompt}/>}
+      {page==="siteEdit"&&siteEditSection==="generate"&&<Generate site={site} onSiteChange={setSite} notify={notify} viewPrompt={viewPrompt} viewSitePrompt={viewSitePrompt}/>}
       {page==="result"&&<Result site={site} go={go} setModal={setModal} viewPrompt={viewPrompt} notify={notify} openFeedback={openFeedback} viewSitePrompt={viewSitePrompt}/>}
       {page==="preview"&&<Preview site={site} tab={previewTab} setTab={setPreviewTab} setModal={setModal} notify={notify}/>}
-    </div>{(["basic","assets","functions"] as PageKey[]).includes(page)&&<div className="sticky-actions"><Button secondary onClick={()=>go(page==="basic"?"projects":page==="assets"?"basic":"assets")}>上一步</Button><div><Button secondary onClick={()=>notify("草稿已保存")}>保存草稿</Button><Button onClick={()=>go(page==="basic"?"assets":page==="assets"?"functions":"generate")}>下一步</Button></div></div>}</main>
+    </div>{SITE_EDIT_STEPS.slice(0,-1).includes(page as SiteEditSection)&&<div className="sticky-actions"><Button secondary onClick={()=>{const i=SITE_EDIT_STEPS.indexOf(page as SiteEditSection);go(i>0?SITE_EDIT_STEPS[i-1]:"projects")}}>上一步</Button><div><Button secondary onClick={()=>notify("草稿已保存")}>保存草稿</Button><Button onClick={()=>{const i=SITE_EDIT_STEPS.indexOf(page as SiteEditSection);go(SITE_EDIT_STEPS[i+1])}}>下一步</Button></div></div>}{page==="siteEdit"&&<div className="sticky-actions"><Button secondary onClick={()=>{const i=SITE_EDIT_STEPS.indexOf(siteEditSection);if(i>0)setSiteEditSection(SITE_EDIT_STEPS[i-1]);else go("projects")}}>上一步</Button><div><Button secondary onClick={()=>notify("草稿已保存")}>保存草稿</Button>{siteEditSection!=="generate"&&<Button onClick={()=>{const i=SITE_EDIT_STEPS.indexOf(siteEditSection);setSiteEditSection(SITE_EDIT_STEPS[i+1])}}>下一步</Button>}</div></div>}</main>
     <footer className="site-footer">陕ICP备2026022239号-2</footer>
     {addingDomain&&page==="domainSettings"&&<div className="dlg-backdrop" onMouseDown={()=>{setAddingDomain(false);setNewDomainName("");}}><div className="dlg-box domain-create-dialog" onMouseDown={e=>e.stopPropagation()}><h2>新增领域</h2><p>创建后可在领域方案和站点基本信息中直接选择。</p><input autoFocus value={newDomainName} maxLength={6} placeholder="请输入领域名称(最多 6 个字)" onChange={e=>setNewDomainName(e.target.value)} onKeyDown={async e=>{if(e.key==="Enter")await doAddDomain();if(e.key==="Escape"){setAddingDomain(false);setNewDomainName("");}}}/><div className="dlg-actions"><Button secondary onClick={()=>{setAddingDomain(false);setNewDomainName("");}}>取消</Button><Button disabled={!newDomainName.trim()} onClick={doAddDomain}>创建</Button></div></div></div>}
     {toast&&<div className="toast"><b>✓</b>{toast}</div>}{modal&&<Modal type={modal} close={()=>setModal("")} notify={notify} promptData={promptData} feedbackCube={feedbackCube} refineCube={refineCube} refining={refining} refineLog={refineLog}/>}
-    {!aiOpen&&<button ref={fabRef} className="ai-fab" title="AI 助手 · 拖动可移动位置,双击复位" aria-label="AI 助手"
+    {!aiOpen&&!manualAssetActive&&<button ref={fabRef} className="ai-fab" title="AI 助手 · 拖动可移动位置,双击复位" aria-label="AI 助手"
       style={fabPos?{left:fabPos.x,top:fabPos.y,right:"auto",bottom:"auto"}:undefined}
       onPointerDown={onFabPointerDown} onPointerMove={onFabPointerMove} onPointerUp={onFabPointerUp} onDoubleClick={resetFab}>
       <img src="/t/stationbuilder/ai-icon.png" alt="AI 助手" className="ai-fab-icon" draggable={false} />
     </button>}
-    {aiOpen&&<div className="ai-dialog">
+    {aiOpen&&!manualAssetActive&&<div className="ai-dialog">
       <header><span className="ai-dialog-logo">✦</span><div className="ai-dialog-title"><b>StationBuilder AI</b><small>模型 · {model}</small></div><button className="ai-close" onClick={()=>setAiOpen(false)}>×</button></header>
       <div className="ai-messages">
-        {aiMessages.length===0&&<div className="ai-empty">我是由 Wordbuddy <b>{model}</b> 驱动的站点助手,问我和站点生成相关的问题吧。</div>}
+        {aiMessages.length===0&&<div className="ai-welcome">
+          <div className="ai-welcome-intro"><span>✦</span><div><b>我可以帮你建设场站</b><p>进入对应页面使用 AI 功能,或直接在下方提问。</p></div></div>
+          <div className="ai-capability-list">
+            <div><i>1</i><span><b>完善站点需求</b><small>站点编辑 → 站点基本属性</small></span></div>
+            <div><i>2</i><span><b>推荐设备、数量与命名规则</b><small>站点编辑 → 站点资产设置</small></span></div>
+            <div><i>3</i><span><b>规划并生成 Cube 页面</b><small>站点编辑 → 功能与菜单</small></span></div>
+            <div><i>4</i><span><b>检查结果并按意见修改页面</b><small>站点编辑 → 生成确认/页面预览</small></span></div>
+            <div><i>5</i><span><b>补全领域方案与设备采集点</b><small>方案中心 → 领域方案/设备模板</small></span></div>
+          </div>
+          <div className="ai-welcome-tip"><b>你可以问:</b>当前站点还缺少哪些设备或页面?</div>
+        </div>}
         {aiMessages.map((m,i)=><div key={i} className={"ai-bubble "+(m.role==="user"?"me":"bot")}><span className="ai-avatar">{m.role==="user"?"W":"✦"}</span><div className="ai-text">{m.content||(m.role==="assistant"&&aiBusy?"…":"")}</div></div>)}
         <div ref={aiEndRef}/>
       </div>
@@ -620,7 +686,11 @@ export default function Home(){
 }
 
 function Login({onLogin}:{onLogin:()=>void}){return <div className="login-page"><div className="login-top"><div className="brand-mark">⬡</div><b>iStation</b><span>StationBuilder</span></div><div className="login-card"><div className="login-logo">⬡</div><h1>iStation StationBuilder</h1><p>面向 iStation 团队的 AI 站点与领域方案生成工具</p><button className="sso-button" onClick={onLogin}><span className="avatar">W</span><b>使用公司账号登录</b><i>→</i></button><small>仅限 iStation 团队内部成员访问</small></div><footer>© 2026 Advantech · iStation Team · v0.3</footer></div>}
-function Steps({page,go}:{page:PageKey;go:(p:PageKey)=>void}){const list:PageKey[]=["basic","assets","functions","generate"];const current=list.indexOf(page);return <div className="steps">{list.map((item,index)=><button key={item} onClick={()=>index<=current&&go(item)} className={index===current?"active":index<current?"done":""}><i>{index<current?"✓":index+1}</i><span><small>STEP {index+1}</small>{meta[item].label}</span>{index<3&&<b>›</b>}</button>)}</div>}
+function Steps({page,go}:{page:PageKey;go:(p:PageKey)=>void}){const current=SITE_EDIT_STEPS.indexOf(page as SiteEditSection);return <div className="steps">{SITE_EDIT_STEPS.map((item,index)=><button key={item} onClick={()=>index<=current&&go(item)} className={index===current?"active":index<current?"done":""}><i>{index<current?"✓":index+1}</i><span><small>STEP {index+1}</small>{meta[item].label}</span>{index<SITE_EDIT_STEPS.length-1&&<b>›</b>}</button>)}</div>}
+
+function SiteEditTabs({section,onChange}:{section:SiteEditSection;onChange:(section:SiteEditSection)=>void}){
+  return <nav className="site-edit-tabs" aria-label="站点编辑功能">{SITE_EDIT_STEPS.map((item,index)=><button key={item} className={section===item?"active":""} aria-current={section===item?"page":undefined} onClick={()=>onChange(item)}><i>{index+1}</i><span>{meta[item].label}</span></button>)}</nav>;
+}
 
 function Projects({view,setView,sites,loading,go,loadSite,notify,onDelete}:{view:"table"|"card";setView:(v:"table"|"card")=>void;sites:SiteSummary[];loading:boolean;go:(p:PageKey, sid?:number|"new")=>void;loadSite:(id:number)=>Promise<void>;notify:(m:string)=>void;onDelete:(ids:number[])=>Promise<void>}){
   const counts = useMemo(()=>{
@@ -644,7 +714,7 @@ function Projects({view,setView,sites,loading,go,loadSite,notify,onDelete}:{view
   const toggleAll = ()=> setSelected(allSelected ? new Set() : new Set(filtered.map(s=>s.id)));
   const batchDelete = async ()=>{ await onDelete([...selected]); setSelected(new Set()); };
   const statusText=(status:string)=>status==="generated"?"生成完成":status==="partial"?"部分失败":status==="running"?"生成中":"草稿";
-  const openProject=(row:SiteSummary)=>go(row.status==="generated"?"result":"basic",row.id);
+  const openProject=(row:SiteSummary)=>go("siteEdit",row.id);
   if(loading) return <Panel><div className="empty-state">加载中…</div></Panel>;
   return <>{confirmEl}
     <div className="company-status-tabs" role="tablist" aria-label="站点状态筛选">
@@ -742,7 +812,7 @@ function Basic({site,domains,onChange,onAddDomain,onUpload,saving,notify,onViewC
   </Drawer></>;
 }
 
-function Assets({site,onChange,saving,notify,onViewCompletePrompt}:{site:SiteDetail|null;onChange:(assets:Asset[])=>void;saving:boolean;notify:(m:string)=>void;onViewCompletePrompt:()=>void}){
+function Assets({site,onChange,saving,notify,onViewCompletePrompt,mode,onModeChange}:{site:SiteDetail|null;onChange:(assets:Asset[])=>void;saving:boolean;notify:(m:string)=>void;onViewCompletePrompt:()=>void;mode:"quick"|"manual";onModeChange:(mode:"quick"|"manual")=>void}){
   const [completing,setCompleting]=useState(false);
   const [completeLog,setCompleteLog]=useState("");
   const completeConsoleRef=useRef<HTMLPreElement>(null);
@@ -787,7 +857,12 @@ function Assets({site,onChange,saving,notify,onViewCompletePrompt}:{site:SiteDet
   };
   const add=()=>onChange([...rows,{type:"",count:0,rule:"",sort_order:rows.length}]);
   const remove=(i:number)=>onChange(rows.filter((_,idx)=>idx!==i));
-  return <div className="two-column">
+  return <div className="asset-config-shell">
+    <div className="asset-config-tabs" role="tablist" aria-label="资产配置方式">
+      <button role="tab" aria-selected={mode==="quick"} className={mode==="quick"?"active":""} onClick={()=>onModeChange("quick")}><span>快速资产配置</span><small>按设备类型、数量和命名规则快速生成</small></button>
+      <button role="tab" aria-selected={mode==="manual"} className={mode==="manual"?"active":""} onClick={()=>onModeChange("manual")}><span>手动配置</span><small>按资产组逐层维护节点与数据关系</small></button>
+    </div>
+    {mode==="manual"?<ManualAssetConfiguration site={site} notify={notify}/>:<div className="two-column quick-asset-config">
     <div>
       <div className="kpi-row">{[[rows.length,"设备类型","类"],[total,"设备实例","个"],[Math.max(2, rows.filter(r=>r.type).length+1),"资产层级","层"]].map(x=><div key={x[1]}><span>{x[1]}</span><b>{x[0]}</b><small>{x[2]}</small></div>)}{saving&&<div><span>保存</span><Status>保存中…</Status></div>}</div>
       <Panel title="设备资产配置" tools={<Button onClick={add}>+ 添加设备类型</Button>}>
@@ -821,9 +896,171 @@ function Assets({site,onChange,saving,notify,onViewCompletePrompt}:{site:SiteDet
       </Panel>
       {saving&&<Panel><div className="empty-state">正在保存…</div></Panel>}
     </div>
+  </div>}
   </div>;
 }
 
+function newAssetNodeId(){ return "node-"+Date.now().toString(36)+Math.random().toString(36).slice(2,7); }
+
+function cloneAssetNode(node:ManualAssetNode):ManualAssetNode{
+  return {...node,id:newAssetNodeId(),properties:node.properties.map(item=>({...item})),bindings:node.bindings.map(item=>({...item})),children:node.children.map(cloneAssetNode)};
+}
+
+function findAssetNode(nodes:ManualAssetNode[],id:string):ManualAssetNode|null{
+  for(const node of nodes){ if(node.id===id)return node; const child=findAssetNode(node.children,id); if(child)return child; }
+  return null;
+}
+
+function updateAssetNode(nodes:ManualAssetNode[],id:string,next:ManualAssetNode):ManualAssetNode[]{
+  return nodes.map(node=>node.id===id?next:{...node,children:updateAssetNode(node.children,id,next)});
+}
+
+function appendAssetNode(nodes:ManualAssetNode[],parentId:string|null,node:ManualAssetNode):ManualAssetNode[]{
+  if(!parentId)return [...nodes,node];
+  return nodes.map(item=>item.id===parentId?{...item,children:[...item.children,node]}:{...item,children:appendAssetNode(item.children,parentId,node)});
+}
+
+function removeAssetNode(nodes:ManualAssetNode[],id:string):ManualAssetNode[]{
+  return nodes.filter(node=>node.id!==id).map(node=>({...node,children:removeAssetNode(node.children,id)}));
+}
+
+function findAssetParentId(nodes:ManualAssetNode[],id:string,parentId:string|null=null):string|null{
+  for(const node of nodes){if(node.id===id)return parentId;const found=findAssetParentId(node.children,id,node.id);if(found!==null)return found;}
+  return null;
+}
+
+function ManualAssetConfiguration({site,notify}:{site:SiteDetail;notify:(message:string)=>void}){
+  const [group,setGroup]=useState<ManualAssetGroup|null>(null);
+  const [loading,setLoading]=useState(true);
+  const [loadError,setLoadError]=useState("");
+
+  const seedChildren=()=>site.assets.filter(item=>item.type).map(item=>({
+    id:newAssetNodeId(),name:item.type,node_type:"group" as const,template_name:item.type,description:`${item.type}设备集合`,properties:[],bindings:[],
+    children:Array.from({length:Math.max(0,Number(item.count)||0)},(_,index)=>({id:newAssetNodeId(),name:formatInstances(item.rule,Math.max(0,Number(item.count)||0)).split("、")[index]||`${item.type}-${String(index+1).padStart(2,"0")}`,node_type:"device" as const,template_name:item.type,description:"",properties:[],bindings:[],children:[]}))
+  }));
+  const seedStructure=():ManualAssetNode[]=>[{id:newAssetNodeId(),name:site.name||"未命名站点",node_type:"group",template_name:"站点资产根节点",description:"当前站点的资产结构根节点",properties:[],bindings:[],children:seedChildren()}];
+
+  const loadGroup=async()=>{
+    if(!site.id)return;
+    setLoading(true);
+    setLoadError("");
+    try{
+      let rows=await fetchDb(`/sites/${site.id}/asset-groups`) as ManualAssetGroup[];
+      if(rows.length===0){
+        const created=await fetchDb(`/sites/${site.id}/asset-groups`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:`${site.name||"未命名站点"}资产组`,category:site.domain||"站点资产",template_name:"通用资产模板",structure:seedStructure()})}) as ManualAssetGroup;
+        rows=[created];
+      }
+      // 手动配置只维护当前站点所属的唯一资产组;旧版本若已有多条,取最近更新的一条。
+      const current=rows[0];
+      setGroup({...current,name:current.name?.trim()||`${site.name||"未命名站点"}资产组`,structure:current.structure.length?current.structure:seedStructure()});
+    }catch(e:any){const message="加载资产组失败:"+(e?.message||e);setLoadError(message);notify(message);}
+    finally{setLoading(false);}
+  };
+  useEffect(()=>{setGroup(null);loadGroup();},[site.id]);
+
+  const saveGroup=async(group:ManualAssetGroup)=>{
+    const saved=await fetchDb(`/sites/${site.id}/asset-groups/${group.id}`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify(group)}) as ManualAssetGroup;
+    setGroup(saved); notify("资产组已保存"); return saved;
+  };
+
+  if(loading)return <div className="asset-editor-loading"><span>正在打开当前站点资产组…</span></div>;
+  if(loadError||!group)return <div className="asset-editor-loading error"><span>{loadError||"资产组不可用"}</span><Button secondary onClick={loadGroup}>重新加载</Button></div>;
+  return <ManualAssetEditor key={group.id} group={group} siteName={site.name} notify={notify} onSave={saveGroup}/>;
+}
+
+function flattenAssetNodes(nodes:ManualAssetNode[],depth=0):Array<{node:ManualAssetNode;depth:number}>{
+  return nodes.flatMap(node=>[{node,depth},...flattenAssetNodes(node.children,depth+1)]);
+}
+
+const assetPropertyFields=["型号","价值","地址","安装位置","负责人","品牌","供应商","经销商","出厂日期","投产日期","质保截止日期","NFC ID","备注","设备运行状态计算阈值"] as const;
+function assetPropertyValue(node:ManualAssetNode,name:string){return node.properties.find(item=>item.name===name)?.value||"";}
+function setAssetProperty(node:ManualAssetNode,name:string,value:string):ManualAssetNode{
+  const exists=node.properties.some(item=>item.name===name);
+  return {...node,properties:exists?node.properties.map(item=>item.name===name?{...item,value}:item):[...node.properties,{name,value,unit:""}]};
+}
+
+function ManualAssetEditor({group,siteName,onSave,notify}:{group:ManualAssetGroup;siteName:string;onSave:(group:ManualAssetGroup)=>Promise<ManualAssetGroup>;notify:(message:string)=>void}){
+  const [draft,setDraft]=useState(group);
+  const [selectedId,setSelectedId]=useState<string|null>(group.structure[0]?.id||null);
+  const [nodeOpen,setNodeOpen]=useState(false);
+  const [nodeTab,setNodeTab]=useState<"basic"|"properties"|"binding">("basic");
+  const [nodeDraft,setNodeDraft]=useState<ManualAssetNode|null>(null);
+  const [batchOpen,setBatchOpen]=useState(false);
+  const [batchTab,setBatchTab]=useState<"online"|"excel">("online");
+  const [batchRows,setBatchRows]=useState([{template_name:"进水提升泵",count:2,rule:"beng-ts-##"}]);
+  const [clipboard,setClipboard]=useState<ManualAssetNode|null>(null);
+  const [search,setSearch]=useState("");
+  const [viewMode,setViewMode]=useState<"tree"|"list">("tree");
+  const [zoom,setZoom]=useState(1);
+  const [pan,setPan]=useState({x:0,y:0});
+  const [dragging,setDragging]=useState(false);
+  const [saving,setSaving]=useState(false);
+  const [templateCategory,setTemplateCategory]=useState("site");
+  const [creatingNodeId,setCreatingNodeId]=useState<string|null>(null);
+  const [bindingComponent,setBindingComponent]=useState("HealthStatus");
+  const [bindingImportOpen,setBindingImportOpen]=useState(false);
+  const selected=selectedId?findAssetNode(draft.structure,selectedId):null;
+  const openNode=(node:ManualAssetNode)=>{setCreatingNodeId(null);setSelectedId(node.id);setNodeDraft({...node,alias:node.alias||`${node.name}-alias`,asset_type:node.asset_type||(node.node_type==="device"?"Machine":"Group"),properties:node.properties.map(item=>({...item})),bindings:node.bindings.map(item=>({...item})),children:node.children});setBindingComponent(node.bindings[0]?.source||"HealthStatus");setNodeTab("basic");setNodeOpen(true);};
+  const addNode=(parent:ManualAssetNode|null=selected,templateName=draft.template_name)=>{
+    const node:ManualAssetNode={id:newAssetNodeId(),name:"新建资产节点",alias:"new-asset-alias",asset_type:"Machine",node_type:"device",template_name:templateName,description:"",properties:[],bindings:[],children:[]};
+    setDraft(prev=>({...prev,structure:appendAssetNode(prev.structure,parent?.id||null,node)}));setCreatingNodeId(node.id);setSelectedId(node.id);setNodeDraft(node);setBindingComponent("HealthStatus");setNodeTab("basic");setNodeOpen(true);
+  };
+  const addSibling=()=>{const parentId=selectedId?findAssetParentId(draft.structure,selectedId):null;const node:ManualAssetNode={id:newAssetNodeId(),name:"新建同级节点",alias:"new-peer-alias",asset_type:"Machine",node_type:"device",template_name:draft.template_name,description:"",properties:[],bindings:[],children:[]};setDraft(prev=>({...prev,structure:appendAssetNode(prev.structure,parentId,node)}));setCreatingNodeId(node.id);setSelectedId(node.id);setNodeDraft(node);setBindingComponent("HealthStatus");setNodeTab("basic");setNodeOpen(true);};
+  const pasteNode=()=>{if(!clipboard){notify("请先复制一个节点");return;}const node=cloneAssetNode(clipboard);setDraft(prev=>({...prev,structure:appendAssetNode(prev.structure,selectedId,node)}));setSelectedId(node.id);notify("节点已粘贴");};
+  const deleteNode=()=>{if(!selectedId){notify("请先选择节点");return;}setDraft(prev=>({...prev,structure:removeAssetNode(prev.structure,selectedId)}));setSelectedId(null);notify("节点已移除,保存后生效");};
+  const saveNode=()=>{if(!nodeDraft)return;if(!nodeDraft.name.trim()||!nodeDraft.alias?.trim()||!nodeDraft.template_name.trim()||!nodeDraft.asset_type?.trim()){notify("请填写名称、编号/别名、模板和类型");return;}setDraft(prev=>({...prev,structure:updateAssetNode(prev.structure,nodeDraft.id,nodeDraft)}));setCreatingNodeId(null);setNodeOpen(false);notify("节点信息已更新");};
+  const cancelNode=()=>{if(creatingNodeId){setDraft(prev=>({...prev,structure:removeAssetNode(prev.structure,creatingNodeId)}));setSelectedId(null);}setCreatingNodeId(null);setNodeOpen(false);setNodeDraft(null);};
+  const readNodeImage=(file:File)=>{if(file.size>2*1024*1024){notify("图片不能超过 2 MB");return;}const reader=new FileReader();reader.onload=()=>setNodeDraft(prev=>prev?{...prev,image_data:String(reader.result||"")}:prev);reader.readAsDataURL(file);};
+  const addBatchRow=()=>setBatchRows(prev=>[...prev,{template_name:"",count:1,rule:"device-##"}]);
+  const createBatch=()=>{
+    let structure=draft.structure;
+    for(const row of batchRows.filter(item=>item.template_name&&item.count>0)){
+      const parent:ManualAssetNode={id:newAssetNodeId(),name:row.template_name,node_type:"group",template_name:row.template_name,description:"批量创建",properties:[],bindings:[],children:Array.from({length:row.count},(_,index)=>({id:newAssetNodeId(),name:row.rule.includes("#")?row.rule.replace(/#+/,match=>String(index+1).padStart(match.length,"0")):`${row.rule}${String(index+1).padStart(2,"0")}`,node_type:"device",template_name:row.template_name,description:"",properties:[],bindings:[],children:[]}))};
+      structure=appendAssetNode(structure,selectedId,parent);
+    }
+    setDraft(prev=>({...prev,structure}));setBatchOpen(false);notify("批量节点已加入资产树");
+  };
+  const importBatchFile=async(file:File)=>{
+    try{
+      const XLSX=await import("xlsx"); const wb=XLSX.read(await file.arrayBuffer()); const sheet=wb.Sheets[wb.SheetNames[0]]; const data=XLSX.utils.sheet_to_json<Record<string,unknown>>(sheet);
+      const rows=data.map(item=>({template_name:String(item["设备类型"]||item["资产模板"]||item["名称"]||""),count:Number(item["数量"]||1),rule:String(item["命名规则"]||"device-##")})).filter(item=>item.template_name);
+      if(!rows.length)throw new Error("未找到“设备类型/资产模板、数量、命名规则”列"); setBatchRows(rows); setBatchTab("online"); notify(`已读取 ${rows.length} 行配置`);
+    }catch(e:any){notify("读取文件失败:"+(e?.message||e));}
+  };
+  const saveAll=async()=>{setSaving(true);try{const saved=await onSave(draft);setDraft(saved);}catch(e:any){notify("保存失败:"+(e?.message||e));}finally{setSaving(false);}};
+  const copySelected=(deep:boolean)=>{if(!selected){notify("请先选择节点");return;}setClipboard(deep?selected:{...selected,children:[]});notify(deep?"已深度复制节点及其下级":"已复制节点");};
+  const editSelected=(tab:"basic"|"properties"|"binding"="basic")=>{if(!selected){notify("请先选择节点");return;}openNode(selected);setNodeTab(tab);};
+  const bindTemplate=(templateName:string)=>{if(!selected){notify("请先选择节点");return;}const next={...selected,template_name:templateName};setDraft(prev=>({...prev,structure:updateAssetNode(prev.structure,selected.id,next)}));notify(`已应用${templateName}`);};
+  const bindingComponents=Array.from(new Set(["HealthStatus","Motor DE H","Motor NDE H",...(nodeDraft?.bindings.map(item=>item.source).filter(Boolean)||[])]));
+  const bindEdgeComponent=(component:string)=>{if(!nodeDraft)return;const exists=nodeDraft.bindings.some(item=>item.source===component);const bindings=exists?nodeDraft.bindings.map(item=>item.source===component?{...item,collect_type:item.collect_type||"IoT Edge",group:item.group||"Default",device_name:item.device_name||`${nodeDraft.name}-device`}:item):[...nodeDraft.bindings,{source:component,name:component,point:"",collect_type:"IoT Edge",group:"Default",device_name:`${nodeDraft.name}-device`,unit:"",precision:"2",mapping:"",description:""}];setNodeDraft({...nodeDraft,bindings});setBindingComponent(component);notify(`已绑定边缘组件 ${component}`);};
+  const updateBinding=(index:number,patch:Partial<ManualAssetBinding>)=>{if(!nodeDraft)return;const targetIndexes=nodeDraft.bindings.map((item,i)=>item.source===bindingComponent?i:-1).filter(i=>i>=0);const target=targetIndexes[index];if(target===undefined)return;setNodeDraft({...nodeDraft,bindings:nodeDraft.bindings.map((item,i)=>i===target?{...item,...patch}:item)});};
+  const removeBinding=(index:number)=>{if(!nodeDraft)return;const targetIndexes=nodeDraft.bindings.map((item,i)=>item.source===bindingComponent?i:-1).filter(i=>i>=0);const target=targetIndexes[index];setNodeDraft({...nodeDraft,bindings:nodeDraft.bindings.filter((_,i)=>i!==target)});};
+  const importBindings=async(file:File)=>{if(!nodeDraft)return;try{const XLSX=await import("xlsx");const wb=XLSX.read(await file.arrayBuffer());const sheet=wb.Sheets[wb.SheetNames[0]];const data=XLSX.utils.sheet_to_json<Record<string,unknown>>(sheet);const rows:ManualAssetBinding[]=data.map(item=>({source:bindingComponent,name:String(item["名称"]||item["参数名称"]||""),point:String(item["采集点"]||item["点位"]||""),unit:String(item["单位"]||""),precision:String(item["精度"]??"2"),mapping:String(item["映射"]||""),description:String(item["描述"]||"")})).filter(item=>item.name||item.point);if(!rows.length)throw new Error("未找到名称或采集点数据");setNodeDraft({...nodeDraft,bindings:[...nodeDraft.bindings.filter(item=>item.source!==bindingComponent),...rows]});setBindingImportOpen(false);notify(`已导入 ${rows.length} 个点位参数`);}catch(e:any){notify("导入失败:"+(e?.message||e));}};
+  const visibleRows=flattenAssetNodes(draft.structure).filter(({node})=>!search||node.name.toLowerCase().includes(search.toLowerCase())||node.template_name.toLowerCase().includes(search.toLowerCase()));
+  const fitCanvas=()=>{setZoom(1);setPan({x:0,y:0});};
+  return <div className="manual-asset-editor">
+    <div className="asset-editor-title"><div><h2>资产组详情</h2><span>当前站点:{siteName||"未命名站点"}</span></div><Button disabled={saving} onClick={saveAll}>{saving?"保存中…":"保存"}</Button></div>
+    <div className="asset-editor-workbench">
+      <aside className="asset-editor-side"><Field label="站点资产组"><input value={draft.name} onChange={e=>setDraft(prev=>({...prev,name:e.target.value}))}/></Field><button className="asset-batch-entry" onClick={()=>setBatchOpen(true)}>批量创建</button><div className="asset-template-panel"><strong>资产模板</strong><select aria-label="资产模板分类" value={templateCategory} onChange={e=>setTemplateCategory(e.target.value)}><option value="site">场站设备类别</option><option value="unbound">不绑定设备的类别</option></select>{(templateCategory==="site"?["通用资产模板","污水处理设备模板","泵站设备模板"]:["不绑定设备的模板"]).map(template=><button key={template} onClick={()=>addNode(selected,template)}><span>{templateCategory==="site"?"◆":"◇"}</span><b>{template}</b><small>点击添加到当前选中节点</small></button>)}</div></aside>
+      <section className="asset-tree-workspace"><div className="asset-editor-toolbar"><div className="asset-tool-group"><AntButton type="text" className="asset-tool-button" title="复制节点" aria-label="复制节点" disabled={!selected} icon={<CopyOutlined/>} onClick={()=>copySelected(false)}/><AntButton type="text" className="asset-tool-button" title="深度复制节点及其下级" aria-label="深度复制节点" disabled={!selected} icon={<SnippetsOutlined/>} onClick={()=>copySelected(true)}/><AntButton type="text" className="asset-tool-button" title="节点属性设置" aria-label="节点属性设置" disabled={!selected} icon={<ControlOutlined/>} onClick={()=>editSelected("properties")}/><AntButton type="text" className="asset-tool-button" title="删除节点" aria-label="删除节点" danger disabled={!selected} icon={<DeleteOutlined/>} onClick={deleteNode}/><span className="asset-tool-divider"/><AntButton type="text" className="asset-tool-button primary" title="添加同级节点" aria-label="添加同级节点" disabled={!selected} icon={<PlusSquareOutlined/>} onClick={addSibling}/><AntButton type="text" className="asset-tool-button primary" title="添加子节点" aria-label="添加子节点" icon={<PlusOutlined/>} onClick={()=>addNode(selected)}/><Dropdown trigger={["click"]} menu={{items:[{key:"通用资产模板",label:"应用通用资产模板"},{key:"污水处理设备模板",label:"应用污水处理设备模板"},{key:"泵站设备模板",label:"应用泵站设备模板"}],onClick:({key})=>bindTemplate(key)}}><AntButton type="text" className="asset-tool-button primary" title="应用资产模板" aria-label="应用资产模板" disabled={!selected} icon={<BlockOutlined/>}/></Dropdown></div><div className="asset-view-switch" role="group" aria-label="视图切换"><button className={viewMode==="tree"?"active":""} title="资产树视图" onClick={()=>setViewMode("tree")}><ApartmentOutlined/></button><button className={viewMode==="list"?"active":""} title="列表视图" onClick={()=>setViewMode("list")}><BarsOutlined/></button></div><div className="asset-canvas-tools"><AntButton type="text" className="asset-tool-button" title="缩小" aria-label="缩小" icon={<ZoomOutOutlined/>} onClick={()=>setZoom(value=>Math.max(.55,Number((value-.1).toFixed(2))))}/><AntButton type="text" className="asset-tool-button" title="放大" aria-label="放大" icon={<ZoomInOutlined/>} onClick={()=>setZoom(value=>Math.min(1.6,Number((value+.1).toFixed(2))))}/><AntButton type="text" className="asset-tool-button" title="适合窗口" aria-label="适合窗口" icon={<AimOutlined/>} onClick={fitCanvas}/><label className="asset-tree-search"><SearchOutlined/><input value={search} onChange={e=>setSearch(e.target.value)} placeholder="搜索资产节点"/></label></div></div>{viewMode==="tree"?<div className={dragging?"asset-tree-canvas dragging":"asset-tree-canvas"} onPointerDown={(e:ReactPointerEvent<HTMLDivElement>)=>{if((e.target as HTMLElement).closest("button,input,[role='button']"))return;setDragging(true);e.currentTarget.setPointerCapture(e.pointerId);}} onPointerMove={(e:ReactPointerEvent<HTMLDivElement>)=>{if(dragging)setPan(value=>({x:value.x+e.movementX,y:value.y+e.movementY}));}} onPointerUp={(e:ReactPointerEvent<HTMLDivElement>)=>{setDragging(false);e.currentTarget.releasePointerCapture(e.pointerId);}}><div className="asset-tree-stage" style={{transform:`translate(${pan.x}px, ${pan.y}px) scale(${zoom})`}}><div className="asset-node-tree">{draft.structure.map(node=><AssetTreeBranch key={node.id} node={node} selectedId={selectedId} search={search} onSelect={setSelectedId} onOpen={openNode} onAddChild={addNode}/>)}</div></div></div>:<div className="asset-node-list"><div className="asset-node-list-head"><span>资产名称</span><span>节点类型</span><span>资产模板</span><span>下级节点</span></div>{visibleRows.length?visibleRows.map(({node,depth})=><button key={node.id} className={selectedId===node.id?"selected":""} onClick={()=>openNode(node)}><span style={{paddingLeft:depth*22}}>{node.node_type==="group"?"◇":"◆"} {node.name}</span><span>{node.node_type==="group"?"资产分组":"设备资产"}</span><span>{node.template_name||"—"}</span><span>{node.children.length}</span></button>):<div className="asset-tree-empty"><b>没有匹配的资产节点</b><span>请更换搜索关键词。</span></div>}</div>}</section>
+    </div>
+    <Drawer className="asset-node-drawer" title="节点配置" width="58vw" open={nodeOpen} onClose={cancelNode} footer={<div className="node-drawer-footer"><Button secondary onClick={cancelNode}>取消</Button><Button disabled={!nodeDraft?.name.trim()||!nodeDraft?.alias?.trim()||!nodeDraft?.template_name.trim()||!nodeDraft?.asset_type?.trim()} onClick={saveNode}>确定</Button></div>}>
+      <div className="node-drawer-tabs"><button className={nodeTab==="basic"?"active":""} onClick={()=>setNodeTab("basic")}>基础信息设定</button><button className={nodeTab==="properties"?"active":""} onClick={()=>setNodeTab("properties")}>属性设定</button><button className={nodeTab==="binding"?"active":""} onClick={()=>setNodeTab("binding")}>数据绑定</button></div>
+      {nodeDraft&&nodeTab==="basic"&&<div className="node-config-page"><section className="node-config-section"><h3>基础设定</h3><div className="node-id-row"><span>节点ID</span><b>{nodeDraft.id.replace("node-","").slice(0,10)}</b></div><div className="node-basic-grid"><Field label="名称" required><input value={nodeDraft.name} onChange={e=>setNodeDraft({...nodeDraft,name:e.target.value})}/></Field><Field label="编号/别名" required><input value={nodeDraft.alias||""} onChange={e=>setNodeDraft({...nodeDraft,alias:e.target.value})}/></Field><Field label="模板" required><select value={nodeDraft.template_name} onChange={e=>setNodeDraft({...nodeDraft,template_name:e.target.value})}><option>通用资产模板</option><option>污水处理设备模板</option><option>泵站设备模板</option><option>不绑定设备的模板</option></select></Field><Field label="类型" required><input value={nodeDraft.asset_type||""} onChange={e=>setNodeDraft({...nodeDraft,asset_type:e.target.value})}/></Field><Field label="节点描述"><input value={nodeDraft.description} placeholder="请输入" onChange={e=>setNodeDraft({...nodeDraft,description:e.target.value})}/></Field></div></section><section className="node-config-section"><h3>图片</h3><div className="node-image-row"><label className="node-image-preview"><input type="file" accept="image/jpeg,image/png,image/bmp,image/svg+xml" hidden onChange={e=>{const file=e.target.files?.[0];if(file)readNodeImage(file);}}/>{nodeDraft.image_data?<img src={nodeDraft.image_data} alt="节点图片"/>:<BlockOutlined/>}</label><div><label className="node-upload-link"><input type="file" accept="image/jpeg,image/png,image/bmp,image/svg+xml" hidden onChange={e=>{const file=e.target.files?.[0];if(file)readNodeImage(file);}}/><UploadOutlined/> 图片上传</label><small>支持格式 jpeg/jpg/png/bmp/svg,文件不超过 2 MB</small>{nodeDraft.image_data&&<button className="text-button" onClick={()=>setNodeDraft({...nodeDraft,image_data:""})}>移除图片</button>}</div></div></section><section className="node-config-section"><h3>节点二维码</h3><QRCode size={112} value={`stationbuilder://site/${group.site_id}/asset/${nodeDraft.id}`} bordered={false}/></section></div>}
+      {nodeDraft&&nodeTab==="properties"&&<div className="node-config-page"><section className="node-config-section"><h3>基础属性</h3><div className="node-property-grid">{assetPropertyFields.map(name=><Field key={name} label={name}><input type={name.includes("日期")?"date":"text"} value={assetPropertyValue(nodeDraft,name)||(name==="设备运行状态计算阈值"?"5":"")} placeholder={name==="设备运行状态计算阈值"?"5":"请输入"} onChange={e=>setNodeDraft(setAssetProperty(nodeDraft,name,e.target.value))}/></Field>)}</div></section></div>}
+      {nodeDraft&&nodeTab==="binding"&&<div className="node-config-page binding-page"><section className="node-config-section"><h3>边缘端绑定</h3><h4>组件列表</h4><div className="edge-component-table"><header><span>边缘组件</span><span>采集类型</span><span>群组</span><span>设备名称</span><span>操作</span></header>{bindingComponents.map(component=>{const item=nodeDraft.bindings.find(binding=>binding.source===component);return <div key={component} className={bindingComponent===component?"active":""}><b>{component}</b><span>{item?.collect_type||"—"}</span><span>{item?.group||"—"}</span><span>{item?.device_name||"—"}</span><span><button title="编辑点位" onClick={()=>setBindingComponent(component)}><EditOutlined/></button><button title="绑定边缘设备" onClick={()=>bindEdgeComponent(component)}><LinkOutlined/></button></span></div>})}</div></section><section className="node-config-section"><h3>点位绑定</h3><div className="binding-component-tabs">{bindingComponents.map(component=><button key={component} className={bindingComponent===component?"active":""} onClick={()=>setBindingComponent(component)}>{component}</button>)}</div><div className="binding-parameter-head"><h4>参数列表</h4><Button onClick={()=>setBindingImportOpen(true)}>+ 批量导入</Button></div><div className="binding-parameter-table"><header><span>名称</span><span>采集点</span><span>单位</span><span>精度</span><span>映射</span><span>描述</span><span>操作</span></header>{nodeDraft.bindings.filter(item=>item.source===bindingComponent).map((item,index)=><div key={`${bindingComponent}-${index}`}><input value={item.name||""} placeholder="参数名称" onChange={e=>updateBinding(index,{name:e.target.value})}/><input value={item.point} placeholder="采集点" onChange={e=>updateBinding(index,{point:e.target.value})}/><input value={item.unit||""} onChange={e=>updateBinding(index,{unit:e.target.value})}/><input value={item.precision||""} onChange={e=>updateBinding(index,{precision:e.target.value})}/><input value={item.mapping||""} onChange={e=>updateBinding(index,{mapping:e.target.value})}/><input value={item.description||""} onChange={e=>updateBinding(index,{description:e.target.value})}/><button className="trash" title="删除参数" onClick={()=>removeBinding(index)}><DeleteOutlined/></button></div>)}{!nodeDraft.bindings.some(item=>item.source===bindingComponent)&&<div className="binding-empty">暂无数据,请先在组件列表中绑定设备或批量导入点位。</div>}</div></section></div>}
+    </Drawer>
+    {bindingImportOpen&&<div className="dlg-backdrop binding-import-backdrop" onMouseDown={()=>setBindingImportOpen(false)}><div className="dlg-box binding-import-dialog" onMouseDown={e=>e.stopPropagation()}><h2>批量导入点位参数</h2><p className="hint">导入到组件:{bindingComponent}</p><label className="manual-excel-upload"><input type="file" accept=".xlsx,.xls,.csv" hidden onChange={e=>{const file=e.target.files?.[0];if(file)importBindings(file);}}/><b>⇧</b><span>点击选择点位文件</span><small>表头支持:名称、采集点、单位、精度、映射、描述</small></label><div className="dlg-actions"><Button secondary onClick={()=>setBindingImportOpen(false)}>取消</Button></div></div></div>}
+    {batchOpen&&<div className="dlg-backdrop" onMouseDown={()=>setBatchOpen(false)}><div className="dlg-box batch-create-dialog" onMouseDown={e=>e.stopPropagation()}><h2>批量创建资产节点</h2><div className="node-drawer-tabs"><button className={batchTab==="online"?"active":""} onClick={()=>setBatchTab("online")}>在线配置</button><button className={batchTab==="excel"?"active":""} onClick={()=>setBatchTab("excel")}>Excel 导入</button></div>{batchTab==="online"?<div className="batch-table"><header><span>资产模板</span><span>数量</span><span>命名规则</span><span></span></header>{batchRows.map((row,index)=><div key={index}><input value={row.template_name} onChange={e=>setBatchRows(prev=>prev.map((item,i)=>i===index?{...item,template_name:e.target.value}:item))}/><input type="number" min={1} value={row.count} onChange={e=>setBatchRows(prev=>prev.map((item,i)=>i===index?{...item,count:Number(e.target.value)}:item))}/><input value={row.rule} onChange={e=>setBatchRows(prev=>prev.map((item,i)=>i===index?{...item,rule:e.target.value}:item))}/><button className="trash" onClick={()=>setBatchRows(prev=>prev.filter((_,i)=>i!==index))}>×</button></div>)}<Button secondary onClick={addBatchRow}>+ 添加一行</Button></div>:<label className="manual-excel-upload"><input type="file" accept=".xlsx,.xls,.csv" hidden onChange={e=>{const file=e.target.files?.[0];if(file)importBatchFile(file);}}/><b>⇧</b><span>点击选择资产配置文件</span><small>表头支持:设备类型或资产模板、数量、命名规则</small></label>}<div className="dlg-actions"><Button secondary onClick={()=>setBatchOpen(false)}>取消</Button><Button disabled={batchTab!=="online"||!batchRows.some(item=>item.template_name&&item.count>0)} onClick={createBatch}>创建节点</Button></div></div></div>}
+  </div>;
+}
+
+function AssetTreeBranch({node,selectedId,search,onSelect,onOpen,onAddChild}:{node:ManualAssetNode;selectedId:string|null;search:string;onSelect:(id:string)=>void;onOpen:(node:ManualAssetNode)=>void;onAddChild:(node:ManualAssetNode)=>void}){
+  const [expanded,setExpanded]=useState(true);
+  const matched=!!search&&(node.name.toLowerCase().includes(search.toLowerCase())||node.template_name.toLowerCase().includes(search.toLowerCase()));
+  return <div className="asset-tree-branch"><div role="button" tabIndex={0} className={["asset-node-card",selectedId===node.id?"selected":"",matched?"matched":""].filter(Boolean).join(" ")} onClick={()=>onOpen(node)} onKeyDown={e=>{if(e.key==="Enter")onOpen(node);}}><span>{node.node_type==="group"?"◇":"◆"}</span><b>{node.name}</b><small>{node.template_name}</small>{node.children.length?<button title={expanded?"收起下级节点":"展开下级节点"} aria-label={expanded?`收起${node.name}`:`展开${node.name}`} onClick={e=>{e.stopPropagation();onSelect(node.id);setExpanded(value=>!value);}}>{expanded?"−":"+"}</button>:<button title="添加子节点" aria-label={`为${node.name}添加子节点`} onClick={e=>{e.stopPropagation();onSelect(node.id);onAddChild(node);}}><PlusOutlined/></button>}</div>{expanded&&node.children.length>0&&<div className="asset-tree-children">{node.children.map(child=><AssetTreeBranch key={child.id} node={child} selectedId={selectedId} search={search} onSelect={onSelect} onOpen={onOpen} onAddChild={onAddChild}/>)}</div>}</div>;
+}
+
 function Functions({site,muTree,onMuChange,onSiteChange,pageStyles,notify,onViewCompletePrompt}:{site:SiteDetail|null;muTree:MuNode[];onMuChange:(s:string[])=>void;onSiteChange:(s:SiteDetail)=>void;pageStyles:PageStyle[];notify:(m:string)=>void;onViewCompletePrompt:()=>void}){
   const [tab,setTab]=useState<"mu"|"cube"|"menu">("mu");
   const leaves = useMemo(()=>buildMuLeaves(muTree),[muTree]);