diff --git a/mobile/design/mobile.pen b/mobile/design/mobile.pen index 440d795..c463520 100644 --- a/mobile/design/mobile.pen +++ b/mobile/design/mobile.pen @@ -4465,7 +4465,7 @@ "id": "Qydtk", "x": 1840, "y": 964, - "name": "新增/编辑服务器(代理服务)", + "name": " ", "clip": true, "width": 390, "height": 1606, @@ -9158,6 +9158,13198 @@ ] } ] + }, + { + "type": "frame", + "id": "nw654", + "x": 4201, + "y": 964, + "name": "文件页(右键菜单)", + "clip": true, + "width": 390, + "height": 1087, + "fill": "$surface", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "oQfZ0", + "width": "fill_container", + "height": 62, + "padding": [ + 0, + 20 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "I6Hpdp", + "fill": "$font-primary", + "content": "9:41", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "FdROu", + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "ctNPV", + "width": 16, + "height": 16, + "iconFontName": "signal", + "iconFontFamily": "lucide", + "fill": "$font-primary" + }, + { + "type": "icon_font", + "id": "BQKOc", + "width": 16, + "height": 16, + "iconFontName": "wifi", + "iconFontFamily": "lucide", + "fill": "$font-primary" + }, + { + "type": "icon_font", + "id": "kyDZg", + "width": 22, + "height": 16, + "iconFontName": "battery-full", + "iconFontFamily": "lucide", + "fill": "$font-primary" + } + ] + } + ] + }, + { + "type": "frame", + "id": "fz8XP", + "width": "fill_container", + "height": 56, + "padding": [ + 0, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "NYQvw", + "gap": 10, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "qiGrb", + "width": 32, + "height": 32, + "fill": "$accent", + "cornerRadius": 8, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "a7cmr", + "fill": "#FFFFFF", + "content": ">_", + "fontFamily": "JetBrains Mono", + "fontSize": 13, + "fontWeight": "700" + } + ] + }, + { + "type": "text", + "id": "I308nm", + "fill": "$font-primary", + "content": "文件", + "fontFamily": "Inter", + "fontSize": 20, + "fontWeight": "700" + } + ] + }, + { + "type": "frame", + "id": "GJXE4", + "gap": 12, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "oQVWD", + "width": 22, + "height": 22, + "iconFontName": "search", + "iconFontFamily": "lucide", + "fill": "$font-secondary" + }, + { + "type": "icon_font", + "id": "F0OmPE", + "width": 22, + "height": 22, + "iconFontName": "refresh-cw", + "iconFontFamily": "lucide", + "fill": "$font-secondary" + } + ] + } + ] + }, + { + "type": "frame", + "id": "M9Rrf", + "width": "fill_container", + "height": "fill_container", + "layout": "vertical", + "gap": 10, + "padding": [ + 0, + 16, + 16, + 16 + ], + "children": [ + { + "type": "frame", + "id": "Jj8Gj", + "width": "fill_container", + "height": 56, + "fill": "$surface-card", + "cornerRadius": "$radius-control", + "stroke": { + "thickness": 1, + "fill": "$border" + }, + "padding": [ + 0, + 14 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "HFn0o", + "gap": 10, + "alignItems": "center", + "children": [ + { + "type": "ellipse", + "id": "UQDNt", + "fill": "$success", + "width": 8, + "height": 8 + }, + { + "type": "frame", + "id": "A4ZgbQ", + "layout": "vertical", + "gap": 2, + "children": [ + { + "type": "text", + "id": "TzGpd", + "fill": "$font-primary", + "content": "腾讯云·香港-WebServer", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "600" + }, + { + "type": "text", + "id": "N4JIGB", + "fill": "$font-muted", + "content": "43.135.x.x · 已连接", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "icon_font", + "id": "CNLOX", + "width": 18, + "height": 18, + "iconFontName": "log-out", + "iconFontFamily": "lucide", + "fill": "$font-secondary" + } + ] + }, + { + "type": "frame", + "id": "foZEF", + "width": "fill_container", + "gap": 8, + "children": [ + { + "type": "frame", + "id": "Va1EE", + "width": "fill_container", + "height": 34, + "fill": "$surface-card", + "cornerRadius": "$radius-control", + "stroke": { + "thickness": 1, + "fill": "$border" + }, + "gap": 6, + "padding": [ + 0, + 10 + ], + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "t5GvOQ", + "fill": "$font-primary", + "content": "上传", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "600" + }, + { + "type": "icon_font", + "id": "PMShX", + "width": 14, + "height": 14, + "iconFontName": "chevron-down", + "iconFontFamily": "lucide", + "fill": "$font-secondary" + } + ] + }, + { + "type": "frame", + "id": "xl0e0", + "width": "fill_container", + "height": 34, + "fill": "$surface-card", + "cornerRadius": "$radius-control", + "stroke": { + "thickness": 1, + "fill": "$border" + }, + "gap": 6, + "padding": [ + 0, + 10 + ], + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "s0HVG", + "fill": "$font-primary", + "content": "新建", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "600" + }, + { + "type": "icon_font", + "id": "J2Vyl", + "width": 14, + "height": 14, + "iconFontName": "chevron-down", + "iconFontFamily": "lucide", + "fill": "$font-secondary" + } + ] + }, + { + "type": "frame", + "id": "bNoZC", + "width": "fill_container", + "height": 34, + "fill": "$surface-card", + "cornerRadius": "$radius-control", + "stroke": { + "thickness": 1, + "fill": "$border" + }, + "gap": 6, + "padding": [ + 0, + 10 + ], + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "e2HZi", + "fill": "$font-primary", + "content": "收藏", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "600" + }, + { + "type": "icon_font", + "id": "RQlIe", + "width": 14, + "height": 14, + "iconFontName": "chevron-down", + "iconFontFamily": "lucide", + "fill": "$font-secondary" + } + ] + } + ] + }, + { + "type": "frame", + "id": "wfvwf", + "width": "fill_container", + "height": 36, + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "BZL7R", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "grn0X", + "width": 18, + "height": 18, + "iconFontName": "chevron-left", + "iconFontFamily": "lucide", + "fill": "$font-secondary" + }, + { + "type": "icon_font", + "id": "p0RFa8", + "width": 16, + "height": 16, + "iconFontName": "house", + "iconFontFamily": "lucide", + "fill": "$font-secondary" + }, + { + "type": "icon_font", + "id": "zdvE8", + "width": 14, + "height": 14, + "iconFontName": "chevron-right", + "iconFontFamily": "lucide", + "fill": "$font-muted" + }, + { + "type": "text", + "id": "OlSd2", + "fill": "$font-primary", + "content": "root", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "600" + } + ] + }, + { + "type": "frame", + "id": "xfyY3", + "gap": 12, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "D3XbU", + "width": 16, + "height": 16, + "iconFontName": "refresh-cw", + "iconFontFamily": "lucide", + "fill": "$font-secondary" + }, + { + "type": "icon_font", + "id": "b8Slh", + "width": 16, + "height": 16, + "iconFontName": "settings", + "iconFontFamily": "lucide", + "fill": "$font-secondary" + } + ] + } + ] + }, + { + "type": "frame", + "id": "GYrp2", + "width": "fill_container", + "height": 32, + "stroke": { + "thickness": { + "bottom": 1 + }, + "fill": "$border" + }, + "padding": [ + 0, + 4 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "D4LxDf", + "gap": 10, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "P7o40n", + "width": 14, + "height": 14, + "fill": "$primary", + "cornerRadius": 3, + "stroke": { + "thickness": 1.5, + "fill": "$primary" + }, + "justifyContent": "center", + "alignItems": "center" + }, + { + "type": "text", + "id": "r0XhB", + "fill": "$font-secondary", + "content": "名称", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "600" + } + ] + }, + { + "type": "frame", + "id": "xxY8h", + "gap": 40, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "l9tYNk", + "fill": "$font-secondary", + "content": "大小", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "600" + }, + { + "type": "text", + "id": "k9Lzw", + "fill": "$font-secondary", + "content": "修改时间", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "600" + } + ] + } + ] + }, + { + "type": "frame", + "id": "r9gp2", + "width": "fill_container", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "bF6F8", + "width": "fill_container", + "height": 48, + "fill": "#00000000", + "stroke": { + "thickness": { + "bottom": 1 + }, + "fill": "$border" + }, + "padding": [ + 0, + 4 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "f2SCk", + "gap": 10, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "Ls7Bd", + "width": 14, + "height": 14, + "fill": "#00000000", + "cornerRadius": 3, + "stroke": { + "thickness": 1.5, + "fill": "$border-strong" + }, + "justifyContent": "center", + "alignItems": "center" + }, + { + "type": "icon_font", + "id": "oKFc9", + "width": 18, + "height": 18, + "iconFontName": "folder", + "iconFontFamily": "lucide", + "fill": "$accent" + }, + { + "type": "text", + "id": "R8bYZQ", + "fill": "$primary", + "content": "caddy-tx446", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "Yiu2a", + "gap": 16, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "DaJlC", + "fill": "$font-secondary", + "textGrowth": "fixed-width", + "width": 50, + "content": "4.0 KB", + "textAlign": "right", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "T03q9", + "layout": "vertical", + "alignItems": "end", + "children": [ + { + "type": "text", + "id": "b0BKz", + "fill": "$font-secondary", + "content": "2026-04-20", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "XtLZS", + "fill": "$font-muted", + "content": "19:55:08", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "mC9wD", + "width": "fill_container", + "height": 48, + "fill": "#00000000", + "stroke": { + "thickness": { + "bottom": 1 + }, + "fill": "$border" + }, + "padding": [ + 0, + 4 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "KsxlV", + "gap": 10, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "yjzKA", + "width": 14, + "height": 14, + "fill": "#00000000", + "cornerRadius": 3, + "stroke": { + "thickness": 1.5, + "fill": "$border-strong" + }, + "justifyContent": "center", + "alignItems": "center" + }, + { + "type": "icon_font", + "id": "F1gpco", + "width": 18, + "height": 18, + "iconFontName": "folder", + "iconFontFamily": "lucide", + "fill": "$accent" + }, + { + "type": "text", + "id": "uQLo5", + "fill": "$primary", + "content": "cf2dns_docker", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "RseCr", + "gap": 16, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "sV6RZ", + "fill": "$font-secondary", + "textGrowth": "fixed-width", + "width": 50, + "content": "4.0 KB", + "textAlign": "right", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "Z7747", + "layout": "vertical", + "alignItems": "end", + "children": [ + { + "type": "text", + "id": "E60PS", + "fill": "$font-secondary", + "content": "2025-11-13", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "zmjLb", + "fill": "$font-muted", + "content": "13:00:48", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "w9HnZv", + "width": "fill_container", + "height": 48, + "fill": "$accent-soft", + "stroke": { + "thickness": { + "bottom": 1 + }, + "fill": "$border" + }, + "padding": [ + 0, + 4 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "o5yvMc", + "gap": 10, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "XliAY", + "width": 14, + "height": 14, + "fill": "$primary", + "cornerRadius": 3, + "stroke": { + "thickness": 1.5, + "fill": "$primary" + }, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "yHyhr", + "width": 10, + "height": 10, + "iconFontName": "check", + "iconFontFamily": "lucide", + "fill": "#FFFFFF" + } + ] + }, + { + "type": "icon_font", + "id": "yIdDC", + "width": 18, + "height": 18, + "iconFontName": "folder", + "iconFontFamily": "lucide", + "fill": "$accent" + }, + { + "type": "text", + "id": "Ahwat", + "fill": "$primary", + "content": "easynode", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "K1hx9f", + "gap": 16, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "zos2a", + "fill": "$font-secondary", + "textGrowth": "fixed-width", + "width": 50, + "content": "4.0 KB", + "textAlign": "right", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "uWKYa", + "layout": "vertical", + "alignItems": "end", + "children": [ + { + "type": "text", + "id": "Fjyot", + "fill": "$font-secondary", + "content": "2025-11-23", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "aWCXO", + "fill": "$font-muted", + "content": "19:51:16", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "V4MU6v", + "width": "fill_container", + "height": 48, + "fill": "#00000000", + "stroke": { + "thickness": { + "bottom": 1 + }, + "fill": "$border" + }, + "padding": [ + 0, + 4 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "o2jga0", + "gap": 10, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "Lb3k1", + "width": 14, + "height": 14, + "fill": "#00000000", + "cornerRadius": 3, + "stroke": { + "thickness": 1.5, + "fill": "$border-strong" + }, + "justifyContent": "center", + "alignItems": "center" + }, + { + "type": "icon_font", + "id": "kw8CM", + "width": 18, + "height": 18, + "iconFontName": "folder", + "iconFontFamily": "lucide", + "fill": "$accent" + }, + { + "type": "text", + "id": "T7mfCB", + "fill": "$primary", + "content": "filebrowser", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "l17WY", + "gap": 16, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "AjsWj", + "fill": "$font-secondary", + "textGrowth": "fixed-width", + "width": 50, + "content": "4.0 KB", + "textAlign": "right", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "bcE1H", + "layout": "vertical", + "alignItems": "end", + "children": [ + { + "type": "text", + "id": "m1IP3S", + "fill": "$font-secondary", + "content": "2025-11-10", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "c350M", + "fill": "$font-muted", + "content": "22:20:49", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "CeCq2", + "width": "fill_container", + "height": 48, + "fill": "#00000000", + "stroke": { + "thickness": { + "bottom": 1 + }, + "fill": "$border" + }, + "padding": [ + 0, + 4 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "UpohI", + "gap": 10, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "EvuR9", + "width": 14, + "height": 14, + "fill": "#00000000", + "cornerRadius": 3, + "stroke": { + "thickness": 1.5, + "fill": "$border-strong" + }, + "justifyContent": "center", + "alignItems": "center" + }, + { + "type": "icon_font", + "id": "UZ9Jt", + "width": 18, + "height": 18, + "iconFontName": "folder", + "iconFontFamily": "lucide", + "fill": "$accent" + }, + { + "type": "text", + "id": "xdrze", + "fill": "$primary", + "content": "movie", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "OmZHG", + "gap": 16, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "Zl9QF", + "fill": "$font-secondary", + "textGrowth": "fixed-width", + "width": 50, + "content": "4.0 KB", + "textAlign": "right", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "pk14C", + "layout": "vertical", + "alignItems": "end", + "children": [ + { + "type": "text", + "id": "ZNPsv", + "fill": "$font-secondary", + "content": "2026-04-26", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "w5F30I", + "fill": "$font-muted", + "content": "16:57:33", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "mC1KT", + "width": "fill_container", + "height": 48, + "fill": "#00000000", + "stroke": { + "thickness": { + "bottom": 1 + }, + "fill": "$border" + }, + "padding": [ + 0, + 4 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "TQiNZ", + "gap": 10, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "tyXKm", + "width": 14, + "height": 14, + "fill": "#00000000", + "cornerRadius": 3, + "stroke": { + "thickness": 1.5, + "fill": "$border-strong" + }, + "justifyContent": "center", + "alignItems": "center" + }, + { + "type": "icon_font", + "id": "B43RY", + "width": 18, + "height": 18, + "iconFontName": "folder", + "iconFontFamily": "lucide", + "fill": "$accent" + }, + { + "type": "text", + "id": "QWRYJ", + "fill": "$primary", + "content": "n8n", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "PEcmc", + "gap": 16, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "cqSoc", + "fill": "$font-secondary", + "textGrowth": "fixed-width", + "width": 50, + "content": "4.0 KB", + "textAlign": "right", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "XO3Rk", + "layout": "vertical", + "alignItems": "end", + "children": [ + { + "type": "text", + "id": "OsrEC", + "fill": "$font-secondary", + "content": "2026-02-24", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "E3UYNr", + "fill": "$font-muted", + "content": "15:26:45", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "W1qrR", + "width": "fill_container", + "height": 48, + "fill": "#00000000", + "stroke": { + "thickness": { + "bottom": 1 + }, + "fill": "$border" + }, + "padding": [ + 0, + 4 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "Oj6SE", + "gap": 10, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "SIsdf", + "width": 14, + "height": 14, + "fill": "#00000000", + "cornerRadius": 3, + "stroke": { + "thickness": 1.5, + "fill": "$border-strong" + }, + "justifyContent": "center", + "alignItems": "center" + }, + { + "type": "icon_font", + "id": "r5e4Z", + "width": 18, + "height": 18, + "iconFontName": "file-text", + "iconFontFamily": "lucide", + "fill": "$font-muted" + }, + { + "type": "text", + "id": "H3sU2", + "fill": "$primary", + "content": "auto_cert_renewal4.sh", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "XWhUo", + "gap": 16, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "gu0nA", + "fill": "$font-secondary", + "textGrowth": "fixed-width", + "width": 50, + "content": "2.3 KB", + "textAlign": "right", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "kGBc4", + "layout": "vertical", + "alignItems": "end", + "children": [ + { + "type": "text", + "id": "UvVRa", + "fill": "$font-secondary", + "content": "2026-04-26", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "isFmZ", + "fill": "$font-muted", + "content": "18:16:03", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "aWoyo", + "width": "fill_container", + "height": 48, + "fill": "#00000000", + "stroke": { + "thickness": { + "bottom": 1 + }, + "fill": "$border" + }, + "padding": [ + 0, + 4 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "v7HEk", + "gap": 10, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "eEa14", + "width": 14, + "height": 14, + "fill": "#00000000", + "cornerRadius": 3, + "stroke": { + "thickness": 1.5, + "fill": "$border-strong" + }, + "justifyContent": "center", + "alignItems": "center" + }, + { + "type": "icon_font", + "id": "HpSve", + "width": 18, + "height": 18, + "iconFontName": "file-text", + "iconFontFamily": "lucide", + "fill": "$font-muted" + }, + { + "type": "text", + "id": "BXU72", + "fill": "$primary", + "content": "docker-compose.yml", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "Ut2hm", + "gap": 16, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "h2T90", + "fill": "$font-secondary", + "textGrowth": "fixed-width", + "width": 50, + "content": "0.0 KB", + "textAlign": "right", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "XkRSP", + "layout": "vertical", + "alignItems": "end", + "children": [ + { + "type": "text", + "id": "J6l5Gr", + "fill": "$font-secondary", + "content": "2026-04-26", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "U6Ic2q", + "fill": "$font-muted", + "content": "18:16:22", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "x5Als", + "width": "fill_container", + "padding": [ + 12, + 21, + 21, + 21 + ], + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "luIoc", + "width": "fill_container", + "height": 62, + "fill": "$surface-card", + "cornerRadius": 36, + "stroke": { + "thickness": 1, + "fill": "$border" + }, + "padding": 4, + "children": [ + { + "type": "frame", + "id": "tPe6b", + "width": "fill_container", + "height": "fill_container", + "fill": "#00000000", + "cornerRadius": 26, + "layout": "vertical", + "gap": 4, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "Sl5oi", + "width": 18, + "height": 18, + "iconFontName": "monitor", + "iconFontFamily": "lucide", + "fill": "$font-muted" + }, + { + "type": "text", + "id": "v71py", + "fill": "$font-muted", + "content": "服务器", + "fontFamily": "Inter", + "fontSize": 10, + "fontWeight": "600", + "letterSpacing": 0.5 + } + ] + }, + { + "type": "frame", + "id": "yx3n1", + "width": "fill_container", + "height": "fill_container", + "fill": "$accent", + "cornerRadius": 26, + "layout": "vertical", + "gap": 4, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "RI1CR", + "width": 18, + "height": 18, + "iconFontName": "folder", + "iconFontFamily": "lucide", + "fill": "#FFFFFF" + }, + { + "type": "text", + "id": "sqF3r", + "fill": "#FFFFFF", + "content": "文件", + "fontFamily": "Inter", + "fontSize": 10, + "fontWeight": "600", + "letterSpacing": 0.5 + } + ] + }, + { + "type": "frame", + "id": "jMl7W", + "width": "fill_container", + "height": "fill_container", + "fill": "#00000000", + "cornerRadius": 26, + "layout": "vertical", + "gap": 4, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "v0PYv0", + "width": 18, + "height": 18, + "iconFontName": "scroll-text", + "iconFontFamily": "lucide", + "fill": "$font-muted" + }, + { + "type": "text", + "id": "m1Qnb7", + "fill": "$font-muted", + "content": "脚本", + "fontFamily": "Inter", + "fontSize": 10, + "fontWeight": "600", + "letterSpacing": 0.5 + } + ] + }, + { + "type": "frame", + "id": "IixcF", + "width": "fill_container", + "height": "fill_container", + "fill": "#00000000", + "cornerRadius": 26, + "layout": "vertical", + "gap": 4, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "b9rhL", + "width": 18, + "height": 18, + "iconFontName": "settings", + "iconFontFamily": "lucide", + "fill": "$font-muted" + }, + { + "type": "text", + "id": "pVoeJ", + "fill": "$font-muted", + "content": "设置", + "fontFamily": "Inter", + "fontSize": 10, + "fontWeight": "600", + "letterSpacing": 0.5 + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "x54KVr", + "layoutPosition": "absolute", + "x": 160, + "y": 380, + "width": 180, + "fill": "$surface-card", + "cornerRadius": 12, + "stroke": { + "thickness": 1, + "fill": "$border-strong" + }, + "effect": { + "type": "shadow", + "shadowType": "outer", + "color": "#0A0F1430", + "offset": { + "x": 0, + "y": 8 + }, + "blur": 24 + }, + "layout": "vertical", + "padding": 6, + "children": [ + { + "type": "frame", + "id": "BLVN7", + "width": "fill_container", + "height": 38, + "fill": "$accent-soft", + "cornerRadius": 8, + "gap": 10, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "uF3d1", + "width": 15, + "height": 15, + "iconFontName": "download", + "iconFontFamily": "lucide", + "fill": "$font-secondary" + }, + { + "type": "text", + "id": "dKQN4", + "fill": "$font-primary", + "content": "下载", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "EQGe5", + "width": "fill_container", + "height": 38, + "fill": "#00000000", + "cornerRadius": 8, + "gap": 10, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "l3I1s", + "width": 15, + "height": 15, + "iconFontName": "copy", + "iconFontFamily": "lucide", + "fill": "$font-secondary" + }, + { + "type": "text", + "id": "ZCZUU", + "fill": "$font-primary", + "content": "复制到...", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "kwUV4", + "width": "fill_container", + "height": 38, + "fill": "#00000000", + "cornerRadius": 8, + "gap": 10, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "zOSAD", + "width": 15, + "height": 15, + "iconFontName": "move", + "iconFontFamily": "lucide", + "fill": "$font-secondary" + }, + { + "type": "text", + "id": "Bx842", + "fill": "$font-primary", + "content": "移动到...", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "GN8jX", + "width": "fill_container", + "height": 38, + "fill": "#00000000", + "cornerRadius": 8, + "gap": 10, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "jXfyQ", + "width": 15, + "height": 15, + "iconFontName": "file-archive", + "iconFontFamily": "lucide", + "fill": "$font-secondary" + }, + { + "type": "text", + "id": "jmyfT", + "fill": "$font-primary", + "content": "压缩", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "OekZZ", + "width": "fill_container", + "height": 38, + "fill": "#00000000", + "cornerRadius": 8, + "gap": 10, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "OsLg1", + "width": 15, + "height": 15, + "iconFontName": "trash-2", + "iconFontFamily": "lucide", + "fill": "$danger" + }, + { + "type": "text", + "id": "AzJkp", + "fill": "$danger", + "content": "删除", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "G5z9CM", + "width": "fill_container", + "height": 38, + "fill": "#00000000", + "cornerRadius": 8, + "gap": 10, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "DK8nh", + "width": 15, + "height": 15, + "iconFontName": "pencil-line", + "iconFontFamily": "lucide", + "fill": "$font-secondary" + }, + { + "type": "text", + "id": "Sa9oW", + "fill": "$font-primary", + "content": "重命名", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "pBvup", + "width": "fill_container", + "height": 38, + "fill": "#00000000", + "cornerRadius": 8, + "gap": 10, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "hq1BA", + "width": 15, + "height": 15, + "iconFontName": "link", + "iconFontFamily": "lucide", + "fill": "$font-secondary" + }, + { + "type": "text", + "id": "u4ygRS", + "fill": "$font-primary", + "content": "复制文件夹路径", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "sYMaF", + "x": 4631, + "y": 964, + "name": "代码编辑页", + "clip": true, + "width": 390, + "height": 1087, + "fill": "$surface", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "ATqWB", + "width": "fill_container", + "height": 44, + "padding": [ + 0, + 20 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "eIcme", + "fill": "$font-primary", + "content": "9:41", + "fontFamily": "Inter", + "fontSize": 15, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "HvABp", + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "hGDqq", + "width": 16, + "height": 16, + "iconFontName": "signal", + "iconFontFamily": "lucide", + "fill": "$font-primary" + }, + { + "type": "icon_font", + "id": "k6wZN", + "width": 16, + "height": 16, + "iconFontName": "wifi", + "iconFontFamily": "lucide", + "fill": "$font-primary" + }, + { + "type": "icon_font", + "id": "y3FyPd", + "width": 22, + "height": 14, + "iconFontName": "battery-full", + "iconFontFamily": "lucide", + "fill": "$font-primary" + } + ] + } + ] + }, + { + "type": "frame", + "id": "GLf6V", + "width": "fill_container", + "height": 56, + "gap": 8, + "padding": [ + 8, + 12 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "XsTpb", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "Z090K2", + "width": 24, + "height": 24, + "iconFontName": "chevron-left", + "iconFontFamily": "lucide", + "fill": "$font-primary" + }, + { + "type": "frame", + "id": "CPsnc", + "layout": "vertical", + "gap": 2, + "children": [ + { + "type": "text", + "id": "iAeyy", + "fill": "$font-primary", + "content": "·docker-compose.yml", + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "600" + }, + { + "type": "text", + "id": "YqZk3", + "fill": "$font-muted", + "content": "/root/easynode/docker-compose.yml", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "TruxT", + "gap": 14, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "smRn7", + "width": 20, + "height": 20, + "iconFontName": "undo-2", + "iconFontFamily": "lucide", + "fill": "$font-secondary" + }, + { + "type": "icon_font", + "id": "OVSME", + "width": 20, + "height": 20, + "iconFontName": "redo-2", + "iconFontFamily": "lucide", + "fill": "$font-secondary" + }, + { + "type": "icon_font", + "id": "Dg8rK", + "width": 20, + "height": 20, + "iconFontName": "search", + "iconFontFamily": "lucide", + "fill": "$font-secondary" + } + ] + } + ] + }, + { + "type": "frame", + "id": "VNZRQ", + "width": "fill_container", + "height": 36, + "padding": [ + 0, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "ksyRc", + "fill": "$accent-soft", + "cornerRadius": "$radius-chip", + "padding": [ + 4, + 10 + ], + "children": [ + { + "type": "text", + "id": "venZo", + "fill": "$primary", + "content": "· YAML", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "600" + } + ] + }, + { + "type": "text", + "id": "VoiRl", + "fill": "$font-muted", + "content": "UTF-8 · LF · 2.4 KB", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "NGuiJ", + "clip": true, + "width": "fill_container", + "height": 847, + "fill": "#0A0F14", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "XKism", + "width": "fill_container", + "height": "fill_container", + "children": [ + { + "type": "frame", + "id": "jWSMV", + "width": 36, + "height": "fill_container", + "layout": "vertical", + "gap": 3, + "padding": [ + 12, + 8 + ], + "alignItems": "end", + "children": [ + { + "type": "text", + "id": "kk5Z8", + "fill": "#4B5563", + "content": "1", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "fw47l", + "fill": "#4B5563", + "content": "2", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "oIrD0", + "fill": "#4B5563", + "content": "3", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "a0ZEy", + "fill": "#4B5563", + "content": "4", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "fXVT1", + "fill": "#4B5563", + "content": "5", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "vE2cr", + "fill": "#4B5563", + "content": "6", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "s5DTq", + "fill": "#9CA3AF", + "content": "7", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "N2aubb", + "fill": "#4B5563", + "content": "8", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "I3OGbp", + "fill": "#4B5563", + "content": "9", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "Kw9qJ", + "fill": "#4B5563", + "content": "10", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "kEO1B", + "fill": "#4B5563", + "content": "11", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "w1EYqg", + "fill": "#4B5563", + "content": "12", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "kH82T", + "fill": "#4B5563", + "content": "13", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "X3SkyN", + "fill": "#4B5563", + "content": "14", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "P2oJ4", + "fill": "#4B5563", + "content": "15", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "o4898S", + "fill": "#4B5563", + "content": "16", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "GBHgW", + "fill": "#4B5563", + "content": "17", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "UZhyV", + "fill": "#4B5563", + "content": "18", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "M8YpCw", + "fill": "#4B5563", + "content": "19", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "MTllA", + "fill": "#4B5563", + "content": "20", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "DZNdL", + "width": "fill_container", + "height": "fill_container", + "layout": "vertical", + "gap": 3, + "padding": [ + 12, + 12, + 12, + 4 + ], + "children": [ + { + "type": "frame", + "id": "o8KVC", + "width": "fill_container", + "children": [ + { + "type": "text", + "id": "uWb7e", + "fill": "#569CD6", + "content": "version", + "fontFamily": "JetBrains Mono", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "Ehtwn", + "fill": "#D4D4D4", + "content": ":", + "fontFamily": "JetBrains Mono", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "bALQ2", + "fill": "#D4D4D4", + "content": " ", + "fontFamily": "JetBrains Mono", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "NApx0", + "fill": "#CE9178", + "content": "\"3.8\"", + "fontFamily": "JetBrains Mono", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "vErZg", + "width": "fill_container", + "children": [ + { + "type": "text", + "id": "xRHiD", + "fill": "#D4D4D4", + "content": " ", + "fontFamily": "JetBrains Mono", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "pXmLi", + "width": "fill_container", + "children": [ + { + "type": "text", + "id": "f5sYaP", + "fill": "#569CD6", + "content": "services", + "fontFamily": "JetBrains Mono", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "wbNIK", + "fill": "#D4D4D4", + "content": ":", + "fontFamily": "JetBrains Mono", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "aCMQ4", + "width": "fill_container", + "children": [ + { + "type": "text", + "id": "FmJQU", + "fill": "#D4D4D4", + "content": " ", + "fontFamily": "JetBrains Mono", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "RzAfh", + "fill": "#9CDCFE", + "content": "easynode", + "fontFamily": "JetBrains Mono", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "H88A9", + "fill": "#D4D4D4", + "content": ":", + "fontFamily": "JetBrains Mono", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "TDYj9", + "width": "fill_container", + "children": [ + { + "type": "text", + "id": "fDDpy", + "fill": "#D4D4D4", + "content": " ", + "fontFamily": "JetBrains Mono", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "pqKY5", + "fill": "#569CD6", + "content": "image", + "fontFamily": "JetBrains Mono", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "y6ZdI", + "fill": "#D4D4D4", + "content": ":", + "fontFamily": "JetBrains Mono", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "jODWn", + "fill": "#CE9178", + "content": " chaoszhu/easynode:latest", + "fontFamily": "JetBrains Mono", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "Q2Ya5l", + "width": "fill_container", + "children": [ + { + "type": "text", + "id": "r25at", + "fill": "#D4D4D4", + "content": " ", + "fontFamily": "JetBrains Mono", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "WFyk3", + "fill": "#569CD6", + "content": "container_name", + "fontFamily": "JetBrains Mono", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "Y90Tc3", + "fill": "#D4D4D4", + "content": ":", + "fontFamily": "JetBrains Mono", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "RWmHJ", + "fill": "#CE9178", + "content": " easynode", + "fontFamily": "JetBrains Mono", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "mHLGs", + "width": "fill_container", + "fill": "#FFFFFF10", + "children": [ + { + "type": "text", + "id": "z0RcjP", + "fill": "#D4D4D4", + "content": " ", + "fontFamily": "JetBrains Mono", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "ZLATn", + "fill": "#569CD6", + "content": "restart", + "fontFamily": "JetBrains Mono", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "ud6Wj", + "fill": "#D4D4D4", + "content": ":", + "fontFamily": "JetBrains Mono", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "cPwkh", + "fill": "#CE9178", + "content": " always", + "fontFamily": "JetBrains Mono", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "A5DAc", + "width": "fill_container", + "children": [ + { + "type": "text", + "id": "Is7CW", + "fill": "#D4D4D4", + "content": " ", + "fontFamily": "JetBrains Mono", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "wfvZL", + "fill": "#569CD6", + "content": "ports", + "fontFamily": "JetBrains Mono", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "x5aGwG", + "fill": "#D4D4D4", + "content": ":", + "fontFamily": "JetBrains Mono", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "JXI32", + "width": "fill_container", + "children": [ + { + "type": "text", + "id": "yJycI", + "fill": "#D4D4D4", + "content": " - ", + "fontFamily": "JetBrains Mono", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "Ffm3u", + "fill": "#CE9178", + "content": "\"8082:8082\"", + "fontFamily": "JetBrains Mono", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "gvxOo", + "width": "fill_container", + "children": [ + { + "type": "text", + "id": "Lxrw5", + "fill": "#D4D4D4", + "content": " ", + "fontFamily": "JetBrains Mono", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "NjaYz", + "fill": "#569CD6", + "content": "environment", + "fontFamily": "JetBrains Mono", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "MwgBS", + "fill": "#D4D4D4", + "content": ":", + "fontFamily": "JetBrains Mono", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "oOJR4", + "width": "fill_container", + "children": [ + { + "type": "text", + "id": "UDl0t", + "fill": "#D4D4D4", + "content": " - ", + "fontFamily": "JetBrains Mono", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "lh7lz", + "fill": "#9CDCFE", + "content": "DEBUG", + "fontFamily": "JetBrains Mono", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "qqZ9B", + "fill": "#D4D4D4", + "content": "=", + "fontFamily": "JetBrains Mono", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "ckply", + "fill": "#569CD6", + "content": "false", + "fontFamily": "JetBrains Mono", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "jzQUh", + "width": "fill_container", + "children": [ + { + "type": "text", + "id": "h69DU", + "fill": "#D4D4D4", + "content": " ", + "fontFamily": "JetBrains Mono", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "Oxz9K", + "fill": "#569CD6", + "content": "volumes", + "fontFamily": "JetBrains Mono", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "moOnz", + "fill": "#D4D4D4", + "content": ":", + "fontFamily": "JetBrains Mono", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "jLkWm", + "width": "fill_container", + "children": [ + { + "type": "text", + "id": "I1JqT", + "fill": "#D4D4D4", + "content": " - ", + "fontFamily": "JetBrains Mono", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "EqwIY", + "fill": "#CE9178", + "content": "/root/easynode/db:/easynode/app/db", + "fontFamily": "JetBrains Mono", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "ibeJr", + "width": "fill_container", + "children": [ + { + "type": "text", + "id": "GuoFS", + "fill": "#D4D4D4", + "content": " ", + "fontFamily": "JetBrains Mono", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "W3asc6", + "fill": "#569CD6", + "content": "networks", + "fontFamily": "JetBrains Mono", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "g8rDBR", + "fill": "#D4D4D4", + "content": ":", + "fontFamily": "JetBrains Mono", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "CfKUF", + "width": "fill_container", + "children": [ + { + "type": "text", + "id": "lFDUo", + "fill": "#D4D4D4", + "content": " - ", + "fontFamily": "JetBrains Mono", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "CvQVg", + "fill": "#CE9178", + "content": "easynode-net", + "fontFamily": "JetBrains Mono", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "XnZQg", + "width": "fill_container", + "children": [ + { + "type": "text", + "id": "p0USvS", + "fill": "#D4D4D4", + "content": " ", + "fontFamily": "JetBrains Mono", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "ggvC1", + "width": "fill_container", + "children": [ + { + "type": "text", + "id": "vjJKO", + "fill": "#6A9955", + "content": "# health check section", + "fontFamily": "JetBrains Mono", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "EEsld", + "width": "fill_container", + "children": [ + { + "type": "text", + "id": "AFMYu", + "fill": "#569CD6", + "content": "networks", + "fontFamily": "JetBrains Mono", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "aKVGk", + "fill": "#D4D4D4", + "content": ":", + "fontFamily": "JetBrains Mono", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "u3C13", + "width": "fill_container", + "children": [ + { + "type": "text", + "id": "o7IaHq", + "fill": "#D4D4D4", + "content": " ", + "fontFamily": "JetBrains Mono", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "k80WWW", + "fill": "#9CDCFE", + "content": "easynode-net", + "fontFamily": "JetBrains Mono", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "XoQYT", + "fill": "#D4D4D4", + "content": ":", + "fontFamily": "JetBrains Mono", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "fyrY6", + "width": "fill_container", + "children": [ + { + "type": "text", + "id": "Ehclf", + "fill": "#D4D4D4", + "content": " ", + "fontFamily": "JetBrains Mono", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "R53V0", + "fill": "#569CD6", + "content": "driver", + "fontFamily": "JetBrains Mono", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "aPqVu", + "fill": "#D4D4D4", + "content": ":", + "fontFamily": "JetBrains Mono", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "lbS4h", + "fill": "#CE9178", + "content": " bridge", + "fontFamily": "JetBrains Mono", + "fontSize": 12, + "fontWeight": "normal" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "e2MROP", + "width": "fill_container", + "height": 28, + "fill": "#111827", + "stroke": { + "thickness": { + "top": 1 + }, + "fill": "#1F2937" + }, + "gap": 12, + "padding": [ + 0, + 12 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "Y4N69", + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "U8nc9", + "width": 12, + "height": 12, + "iconFontName": "circle-dot", + "iconFontFamily": "lucide", + "fill": "$success" + }, + { + "type": "text", + "id": "Q7tuiW", + "fill": "#9CA3AF", + "content": "Ln 7, Col 13", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "W2Xr6F", + "fill": "#9CA3AF", + "content": "YAML · Spaces: 2", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "j7Or2", + "fill": "#9CA3AF", + "content": "7 / 20", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "x99XhK", + "width": "fill_container", + "fill": "$surface", + "stroke": { + "thickness": { + "top": 1 + }, + "fill": "$border" + }, + "layout": "vertical", + "gap": 12, + "padding": [ + 12, + 16, + 20, + 16 + ], + "children": [ + { + "type": "frame", + "id": "OAHIO", + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "ellipse", + "id": "WWc5r", + "fill": "$warning", + "width": 8, + "height": 8 + }, + { + "type": "text", + "id": "a2vkb", + "fill": "$font-muted", + "content": "未保存", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "rZjfl", + "width": "fill_container", + "gap": 10, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "z1k2o", + "width": "fill_container", + "height": 44, + "fill": "$primary", + "cornerRadius": "$radius-control", + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "HpWld", + "fill": "$font-on-primary", + "content": "保存", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "600" + } + ] + }, + { + "type": "frame", + "id": "fRG17", + "width": "fill_container", + "height": 44, + "fill": "$surface-card", + "cornerRadius": "$radius-control", + "stroke": { + "thickness": 1, + "fill": "$border-strong" + }, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "NDKmy", + "fill": "$font-primary", + "content": "格式化", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "600" + } + ] + }, + { + "type": "frame", + "id": "aOQ5l", + "width": 40, + "height": 44, + "fill": "$surface-card", + "cornerRadius": "$radius-control", + "stroke": { + "thickness": 1, + "fill": "$border-strong" + }, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "fsZQX", + "width": 20, + "height": 20, + "iconFontName": "ellipsis", + "iconFontFamily": "lucide", + "fill": "$font-primary" + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "izXm3", + "x": 5061, + "y": 964, + "name": "图片预览页", + "clip": true, + "width": 390, + "height": 1087, + "fill": "$surface", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "OFOdS", + "width": "fill_container", + "height": 44, + "padding": [ + 0, + 20 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "dsNfQ", + "fill": "$font-primary", + "content": "9:41", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "V2KfsQ", + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "bWozN", + "width": 16, + "height": 16, + "iconFontName": "signal", + "iconFontFamily": "lucide", + "fill": "$font-primary" + }, + { + "type": "icon_font", + "id": "WqPgc", + "width": 16, + "height": 16, + "iconFontName": "wifi", + "iconFontFamily": "lucide", + "fill": "$font-primary" + }, + { + "type": "icon_font", + "id": "SNEkG", + "width": 22, + "height": 16, + "iconFontName": "battery-full", + "iconFontFamily": "lucide", + "fill": "$font-primary" + } + ] + } + ] + }, + { + "type": "frame", + "id": "CHuDK", + "width": "fill_container", + "height": 56, + "gap": 12, + "padding": [ + 0, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "usTIw", + "width": "fill_container", + "height": "fill_container", + "gap": 10, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "Z7SZO2", + "width": 22, + "height": 22, + "iconFontName": "chevron-left", + "iconFontFamily": "lucide", + "fill": "$font-primary" + }, + { + "type": "frame", + "id": "SuxtP", + "width": "fill_container", + "layout": "vertical", + "gap": 2, + "children": [ + { + "type": "text", + "id": "ljeZm", + "fill": "$font-primary", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "banner-sunset.jpg", + "fontFamily": "Inter", + "fontSize": 15, + "fontWeight": "600" + }, + { + "type": "text", + "id": "fTbhj", + "fill": "$font-muted", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "/root/easynode/static/img", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "MkUOx", + "gap": 14, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "b8kZ9w", + "width": 20, + "height": 20, + "iconFontName": "download", + "iconFontFamily": "lucide", + "fill": "$font-secondary" + }, + { + "type": "icon_font", + "id": "JOyhS", + "width": 20, + "height": 20, + "iconFontName": "share-2", + "iconFontFamily": "lucide", + "fill": "$font-secondary" + }, + { + "type": "icon_font", + "id": "i1sLj2", + "width": 20, + "height": 20, + "iconFontName": "ellipsis", + "iconFontFamily": "lucide", + "fill": "$font-secondary" + } + ] + } + ] + }, + { + "type": "frame", + "id": "hcX2n", + "width": "fill_container", + "height": 32, + "padding": [ + 0, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "p43BY", + "fill": "$surface-card", + "cornerRadius": "$radius-chip", + "stroke": { + "thickness": 1, + "fill": "$border" + }, + "padding": [ + 3, + 10 + ], + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "xXEjn", + "fill": "$font-primary", + "content": "3 / 12", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "600" + } + ] + }, + { + "type": "text", + "id": "A3JPb", + "fill": "$font-muted", + "content": "JPG · 1920×1080 · 487 KB", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "P9wUH4", + "clip": true, + "width": "fill_container", + "height": 540, + "fill": "$surface-inverse", + "layout": "none", + "children": [ + { + "type": "rectangle", + "id": "bYdXg", + "x": 0, + "y": 135, + "fill": { + "type": "image", + "enabled": true, + "url": "https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=800", + "mode": "fill" + }, + "width": 390, + "height": 270 + }, + { + "type": "frame", + "id": "w7lFZ", + "layoutPosition": "absolute", + "x": 12, + "y": 252, + "width": 36, + "height": 36, + "fill": "#FFFFFF20", + "cornerRadius": 18, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "L6y08d", + "width": 18, + "height": 18, + "iconFontName": "chevron-left", + "iconFontFamily": "lucide", + "fill": "#FFFFFFFF" + } + ] + }, + { + "type": "frame", + "id": "q2O60", + "layoutPosition": "absolute", + "x": 342, + "y": 252, + "width": 36, + "height": 36, + "fill": "#FFFFFF20", + "cornerRadius": 18, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "SedyK", + "width": 18, + "height": 18, + "iconFontName": "chevron-right", + "iconFontFamily": "lucide", + "fill": "#FFFFFFFF" + } + ] + }, + { + "type": "frame", + "id": "j9ZHL", + "layoutPosition": "absolute", + "x": 95, + "y": 480, + "fill": "#0A0F14CC", + "cornerRadius": "$radius-chip", + "gap": 18, + "padding": [ + 10, + 16 + ], + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "iVmLT", + "width": 18, + "height": 18, + "iconFontName": "zoom-in", + "iconFontFamily": "lucide", + "fill": "#FFFFFFFF" + }, + { + "type": "icon_font", + "id": "EsGuc", + "width": 18, + "height": 18, + "iconFontName": "zoom-out", + "iconFontFamily": "lucide", + "fill": "#FFFFFFFF" + }, + { + "type": "icon_font", + "id": "mGd7W", + "width": 18, + "height": 18, + "iconFontName": "rotate-cw", + "iconFontFamily": "lucide", + "fill": "#FFFFFFFF" + }, + { + "type": "icon_font", + "id": "JnMmV", + "width": 18, + "height": 18, + "iconFontName": "maximize", + "iconFontFamily": "lucide", + "fill": "#FFFFFFFF" + }, + { + "type": "icon_font", + "id": "o4D2h", + "width": 18, + "height": 18, + "iconFontName": "info", + "iconFontFamily": "lucide", + "fill": "#FFFFFFFF" + } + ] + } + ] + }, + { + "type": "frame", + "id": "EHWP0", + "width": "fill_container", + "height": 80, + "fill": "$surface-card", + "stroke": { + "thickness": { + "top": 1 + }, + "fill": "$border" + }, + "gap": 4, + "padding": [ + 10, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 8, + "id": "f7v3gs", + "fill": { + "type": "image", + "enabled": true, + "url": "https://images.unsplash.com/photo-1469474968028-56623f02e42e?w=120", + "mode": "fill" + }, + "width": 56, + "height": 56, + "stroke": { + "thickness": 1, + "fill": "$border" + } + }, + { + "type": "rectangle", + "cornerRadius": 8, + "id": "B10069", + "fill": { + "type": "image", + "enabled": true, + "url": "https://images.unsplash.com/photo-1472214103451-9374bd1c798e?w=120", + "mode": "fill" + }, + "width": 56, + "height": 56, + "stroke": { + "thickness": 1, + "fill": "$border" + } + }, + { + "type": "rectangle", + "cornerRadius": 8, + "id": "a5aGOp", + "fill": { + "type": "image", + "enabled": true, + "url": "https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=120", + "mode": "fill" + }, + "width": 56, + "height": 56, + "stroke": { + "thickness": 2, + "fill": "$accent" + } + }, + { + "type": "rectangle", + "cornerRadius": 8, + "id": "Bt0fd", + "fill": { + "type": "image", + "enabled": true, + "url": "https://images.unsplash.com/photo-1501785888041-af3ef285b470?w=120", + "mode": "fill" + }, + "width": 56, + "height": 56, + "stroke": { + "thickness": 1, + "fill": "$border" + } + }, + { + "type": "rectangle", + "cornerRadius": 8, + "id": "llCc5", + "fill": { + "type": "image", + "enabled": true, + "url": "https://images.unsplash.com/photo-1470770841072-f978cf4d019e?w=120", + "mode": "fill" + }, + "width": 56, + "height": 56, + "stroke": { + "thickness": 1, + "fill": "$border" + } + }, + { + "type": "rectangle", + "cornerRadius": 8, + "id": "A0tBB", + "fill": { + "type": "image", + "enabled": true, + "url": "https://images.unsplash.com/photo-1418065460487-3d7e9e3c3c8b?w=120", + "mode": "fill" + }, + "width": 56, + "height": 56, + "stroke": { + "thickness": 1, + "fill": "$border" + } + } + ] + }, + { + "type": "frame", + "id": "Wma2Q", + "width": "fill_container", + "layout": "vertical", + "padding": [ + 12, + 16 + ], + "children": [ + { + "type": "frame", + "id": "x0lcc8", + "width": "fill_container", + "fill": "$surface-card", + "cornerRadius": "$radius-card", + "stroke": { + "thickness": 1, + "fill": "$border" + }, + "layout": "vertical", + "gap": 10, + "padding": 14, + "children": [ + { + "type": "frame", + "id": "Yqiil", + "width": "fill_container", + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "PL9Y9", + "fill": "$font-primary", + "content": "图片信息", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "700" + }, + { + "type": "icon_font", + "id": "D8bFc2", + "width": 18, + "height": 18, + "iconFontName": "star", + "iconFontFamily": "lucide", + "fill": "$font-secondary" + } + ] + }, + { + "type": "frame", + "id": "ZSX28", + "width": "fill_container", + "layout": "vertical", + "gap": 6, + "children": [ + { + "type": "frame", + "id": "WCcxs", + "width": "fill_container", + "gap": 12, + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "U5COR", + "fill": "$font-muted", + "content": "尺寸", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "kApOV", + "fill": "$font-primary", + "content": "1920 × 1080", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "Oagiw", + "width": "fill_container", + "gap": 12, + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "FeYWw", + "fill": "$font-muted", + "content": "格式", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "ttHKS", + "fill": "$font-primary", + "content": "JPEG", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "myTv5", + "width": "fill_container", + "gap": 12, + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "t0X69", + "fill": "$font-muted", + "content": "修改时间", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "Q5e48", + "fill": "$font-primary", + "content": "2026-04-26 18:16", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "V1zqI", + "width": "fill_container", + "gap": 12, + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "TpkSJ", + "fill": "$font-muted", + "content": "路径", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "e1PKkk", + "fill": "$font-primary", + "content": "/root/.../banner-sunset.jpg", + "fontFamily": "JetBrains Mono", + "fontSize": 12, + "fontWeight": "500" + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "c8wZh", + "width": "fill_container", + "height": 12, + "layout": "vertical" + } + ] + }, + { + "type": "frame", + "id": "M93CZ", + "x": 5491, + "y": 964, + "name": "影片预览页", + "clip": true, + "width": 390, + "height": 1087, + "fill": "$surface", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "LkG9p", + "width": "fill_container", + "height": 62, + "padding": [ + 0, + 20 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "ATZKo", + "fill": "$font-primary", + "content": "9:41", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "rC2kw", + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "Vjaci", + "width": 16, + "height": 16, + "iconFontName": "signal", + "iconFontFamily": "lucide", + "fill": "$font-primary" + }, + { + "type": "icon_font", + "id": "yKy8n", + "width": 16, + "height": 16, + "iconFontName": "wifi", + "iconFontFamily": "lucide", + "fill": "$font-primary" + }, + { + "type": "icon_font", + "id": "RGICw", + "width": 22, + "height": 16, + "iconFontName": "battery-full", + "iconFontFamily": "lucide", + "fill": "$font-primary" + } + ] + } + ] + }, + { + "type": "frame", + "id": "Kc37m", + "width": "fill_container", + "height": 56, + "padding": [ + 0, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "NDWZ3", + "gap": 10, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "JT2zx", + "width": 22, + "height": 22, + "iconFontName": "chevron-left", + "iconFontFamily": "lucide", + "fill": "$font-primary" + }, + { + "type": "frame", + "id": "su1tG", + "layout": "vertical", + "gap": 1, + "children": [ + { + "type": "text", + "id": "yf4v7", + "fill": "$font-primary", + "content": "sunset-timelapse.mp4", + "fontFamily": "Inter", + "fontSize": 15, + "fontWeight": "600" + }, + { + "type": "text", + "id": "i13FI", + "fill": "$font-muted", + "content": "/root/movie", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "v8QWn", + "gap": 14, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "c83fK", + "width": 20, + "height": 20, + "iconFontName": "download", + "iconFontFamily": "lucide", + "fill": "$font-secondary" + }, + { + "type": "icon_font", + "id": "iylYI", + "width": 20, + "height": 20, + "iconFontName": "share-2", + "iconFontFamily": "lucide", + "fill": "$font-secondary" + }, + { + "type": "icon_font", + "id": "GjtiZ", + "width": 20, + "height": 20, + "iconFontName": "ellipsis", + "iconFontFamily": "lucide", + "fill": "$font-secondary" + } + ] + } + ] + }, + { + "type": "frame", + "id": "qZLVW", + "width": "fill_container", + "height": 28, + "padding": [ + 0, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "UcTpg", + "fill": "$accent-soft", + "cornerRadius": "$radius-chip", + "gap": 4, + "padding": [ + 3, + 10 + ], + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "AmbGH", + "width": 11, + "height": 11, + "iconFontName": "film", + "iconFontFamily": "lucide", + "fill": "$primary" + }, + { + "type": "text", + "id": "Q7BxpE", + "fill": "$primary", + "content": "MP4 · 1080p", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "600" + } + ] + }, + { + "type": "text", + "id": "o0igrm", + "fill": "$font-muted", + "content": "124 MB · 15:42", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "ZQ9gG", + "width": "fill_container", + "height": 460, + "fill": "$surface-inverse", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "LAoMS", + "width": "fill_container", + "height": "fill_container", + "fill": "$surface-inverse", + "children": [ + { + "type": "rectangle", + "id": "Ckhww", + "layoutPosition": "absolute", + "x": 0, + "y": 0, + "fill": { + "type": "image", + "enabled": true, + "url": "https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=900", + "mode": "fill" + }, + "width": 390, + "height": 380 + }, + { + "type": "rectangle", + "id": "VGYje", + "layoutPosition": "absolute", + "x": 0, + "y": 0, + "fill": { + "type": "gradient", + "gradientType": "linear", + "enabled": true, + "rotation": 180, + "size": { + "height": 1 + }, + "colors": [ + { + "color": "#0A0F1400", + "position": 0 + }, + { + "color": "#0A0F1480", + "position": 1 + } + ] + }, + "width": 390, + "height": 380 + }, + { + "type": "frame", + "id": "A3vRV", + "layoutPosition": "absolute", + "x": 159, + "y": 154, + "width": 72, + "height": 72, + "fill": "#FFFFFFE6", + "cornerRadius": 999, + "effect": { + "type": "shadow", + "shadowType": "outer", + "color": "#00000040", + "offset": { + "x": 0, + "y": 8 + }, + "blur": 24 + }, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "SAI03", + "width": 28, + "height": 28, + "iconFontName": "play", + "iconFontFamily": "lucide", + "fill": "$primary" + } + ] + }, + { + "type": "frame", + "id": "qD5Gc", + "layoutPosition": "absolute", + "x": 322, + "y": 16, + "width": 52, + "height": 32, + "fill": "#0A0F1499", + "cornerRadius": 16, + "gap": 8, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "trrnM", + "width": 14, + "height": 14, + "iconFontName": "picture-in-picture-2", + "iconFontFamily": "lucide", + "fill": "#FFFFFF" + }, + { + "type": "icon_font", + "id": "c0tA7p", + "width": 14, + "height": 14, + "iconFontName": "maximize", + "iconFontFamily": "lucide", + "fill": "#FFFFFF" + } + ] + }, + { + "type": "frame", + "id": "kuLrA", + "layoutPosition": "absolute", + "x": 12, + "y": 296, + "width": 366, + "fill": "#0A0F14B3", + "cornerRadius": 16, + "layout": "vertical", + "gap": 10, + "padding": [ + 12, + 14 + ], + "children": [ + { + "type": "frame", + "id": "RTmQq", + "width": "fill_container", + "gap": 10, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "Nu6fP", + "fill": "#FFFFFF", + "content": "02:34", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "nf7wj", + "width": "fill_container", + "height": 4, + "fill": "#FFFFFF26", + "cornerRadius": 2, + "children": [ + { + "type": "rectangle", + "cornerRadius": 2, + "id": "tQn5f", + "layoutPosition": "absolute", + "x": 0, + "y": 0, + "fill": "$accent", + "width": 130, + "height": 4 + }, + { + "type": "rectangle", + "cornerRadius": 2, + "id": "pJiMu", + "layoutPosition": "absolute", + "x": 130, + "y": 0, + "fill": "#FFFFFF55", + "width": 60, + "height": 4 + }, + { + "type": "ellipse", + "id": "fcFtX", + "layoutPosition": "absolute", + "x": 124, + "y": -4, + "fill": "#FFFFFF", + "width": 12, + "height": 12 + } + ] + }, + { + "type": "text", + "id": "Knx8b", + "fill": "#FFFFFFB3", + "content": "15:42", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "IUIpQ", + "width": "fill_container", + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "L26lY", + "gap": 14, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "sG8rP", + "width": 18, + "height": 18, + "iconFontName": "rotate-ccw", + "iconFontFamily": "lucide", + "fill": "#FFFFFF" + }, + { + "type": "icon_font", + "id": "WlrAd", + "width": 20, + "height": 20, + "iconFontName": "skip-back", + "iconFontFamily": "lucide", + "fill": "#FFFFFF" + }, + { + "type": "icon_font", + "id": "zdXc9", + "width": 20, + "height": 20, + "iconFontName": "skip-forward", + "iconFontFamily": "lucide", + "fill": "#FFFFFF" + }, + { + "type": "icon_font", + "id": "MHZdl", + "width": 18, + "height": 18, + "iconFontName": "rotate-cw", + "iconFontFamily": "lucide", + "fill": "#FFFFFF" + } + ] + }, + { + "type": "frame", + "id": "g3T3V", + "gap": 14, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "T1QuK", + "width": 18, + "height": 18, + "iconFontName": "volume-2", + "iconFontFamily": "lucide", + "fill": "#FFFFFF" + }, + { + "type": "frame", + "id": "UUErP", + "fill": "#FFFFFF1A", + "cornerRadius": 10, + "padding": [ + 3, + 8 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "tsK8d", + "fill": "#FFFFFF", + "content": "1.0×", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "600" + } + ] + }, + { + "type": "icon_font", + "id": "K2v3bE", + "width": 18, + "height": 18, + "iconFontName": "captions", + "iconFontFamily": "lucide", + "fill": "#FFFFFF" + } + ] + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "Tqbum", + "width": "fill_container", + "height": "fill_container", + "layout": "vertical", + "gap": 14, + "padding": [ + 14, + 16, + 16, + 16 + ], + "children": [ + { + "type": "frame", + "id": "xZM0F", + "width": "fill_container", + "fill": "$surface-card", + "cornerRadius": "$radius-card", + "stroke": { + "thickness": 1, + "fill": "$border" + }, + "layout": "vertical", + "gap": 10, + "padding": 14, + "children": [ + { + "type": "frame", + "id": "E09qFx", + "width": "fill_container", + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "sDhaB", + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "frfGx", + "width": 14, + "height": 14, + "iconFontName": "info", + "iconFontFamily": "lucide", + "fill": "$primary" + }, + { + "type": "text", + "id": "t44fyZ", + "fill": "$font-primary", + "content": "视频信息", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "700" + } + ] + }, + { + "type": "icon_font", + "id": "VmZ6T", + "width": 16, + "height": 16, + "iconFontName": "star", + "iconFontFamily": "lucide", + "fill": "$font-secondary" + } + ] + }, + { + "type": "frame", + "id": "q6oaUZ", + "width": "fill_container", + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "tGwUe", + "fill": "$font-muted", + "content": "时长", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "CsfHb", + "fill": "$font-primary", + "content": "15:42", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "xtVNo", + "width": "fill_container", + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "JsWvq", + "fill": "$font-muted", + "content": "分辨率", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "qEkUU", + "fill": "$font-primary", + "content": "1920 × 1080 · 30fps", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "tQ5pO", + "width": "fill_container", + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "ftX64", + "fill": "$font-muted", + "content": "编码", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "HGul9", + "fill": "$font-primary", + "content": "H.264 · AAC", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "ILndl", + "width": "fill_container", + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "K4rnCt", + "fill": "$font-muted", + "content": "码率", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "b5giU", + "fill": "$font-primary", + "content": "1.08 Mbps", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "500" + } + ] + } + ] + }, + { + "type": "frame", + "id": "fOGW2", + "width": "fill_container", + "gap": 8, + "children": [ + { + "type": "frame", + "id": "E3x7Hu", + "width": "fill_container", + "height": 42, + "fill": "$primary", + "cornerRadius": "$radius-control", + "gap": 6, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "o7qRPS", + "width": 16, + "height": 16, + "iconFontName": "download", + "iconFontFamily": "lucide", + "fill": "$font-on-primary" + }, + { + "type": "text", + "id": "Y6KX6h", + "fill": "$font-on-primary", + "content": "下载到本地", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "600" + } + ] + }, + { + "type": "frame", + "id": "L7t9yw", + "width": 42, + "height": 42, + "fill": "$surface-card", + "cornerRadius": "$radius-control", + "stroke": { + "thickness": 1, + "fill": "$border" + }, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "qnnYb", + "width": 18, + "height": 18, + "iconFontName": "bookmark", + "iconFontFamily": "lucide", + "fill": "$font-secondary" + } + ] + }, + { + "type": "frame", + "id": "fgnzJ", + "width": 42, + "height": 42, + "fill": "$surface-card", + "cornerRadius": "$radius-control", + "stroke": { + "thickness": 1, + "fill": "$border" + }, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "yfVZj", + "width": 18, + "height": 18, + "iconFontName": "share-2", + "iconFontFamily": "lucide", + "fill": "$font-secondary" + } + ] + } + ] + }, + { + "type": "frame", + "id": "JvJXT", + "width": "fill_container", + "layout": "vertical", + "gap": 10, + "children": [ + { + "type": "frame", + "id": "s6424M", + "width": "fill_container", + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "u5QOi8", + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "oAkBQ", + "width": 14, + "height": 14, + "iconFontName": "list-video", + "iconFontFamily": "lucide", + "fill": "$primary" + }, + { + "type": "text", + "id": "ZwxOa", + "fill": "$font-primary", + "content": "同目录视频", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "700" + } + ] + }, + { + "type": "text", + "id": "wi6IR", + "fill": "$font-muted", + "content": "8 个", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "FjJgL", + "width": "fill_container", + "fill": "$surface-card", + "cornerRadius": 12, + "stroke": { + "thickness": 1, + "fill": "$border" + }, + "gap": 12, + "padding": 8, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "u3GfZX", + "width": 80, + "height": 48, + "fill": { + "type": "image", + "enabled": true, + "url": "https://images.unsplash.com/photo-1505142468610-359e7d316be0?w=200", + "mode": "fill" + }, + "cornerRadius": 8, + "children": [ + { + "type": "frame", + "id": "PKvOJ", + "layoutPosition": "absolute", + "x": 32, + "y": 16, + "width": 16, + "height": 16, + "fill": "#0A0F14B3", + "cornerRadius": 999, + "justifyContent": "center", + "alignItems": "center" + }, + { + "type": "icon_font", + "id": "G58qS", + "layoutPosition": "absolute", + "x": 36, + "y": 20, + "width": 8, + "height": 8, + "iconFontName": "play", + "iconFontFamily": "lucide", + "fill": "#FFFFFF" + }, + { + "type": "frame", + "id": "qkJut", + "layoutPosition": "absolute", + "x": 50, + "y": 30, + "width": 26, + "height": 14, + "fill": "#0A0F14CC", + "cornerRadius": 4, + "justifyContent": "center", + "alignItems": "center" + }, + { + "type": "text", + "id": "Y62kEg", + "layoutPosition": "absolute", + "x": 52, + "y": 32, + "fill": "#FFFFFF", + "content": "08:22", + "fontFamily": "JetBrains Mono", + "fontSize": 9, + "fontWeight": "600" + } + ] + }, + { + "type": "frame", + "id": "K6Wbjl", + "width": "fill_container", + "layout": "vertical", + "gap": 3, + "children": [ + { + "type": "text", + "id": "e0P0s", + "fill": "$font-primary", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "ocean-drone-4k.mp4", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "BPT8n", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "fru09", + "width": 10, + "height": 10, + "iconFontName": "clock-3", + "iconFontFamily": "lucide", + "fill": "$font-muted" + }, + { + "type": "text", + "id": "WDloS", + "fill": "$font-muted", + "content": "08:22", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "hf7xM", + "fill": "$font-muted", + "content": "·", + "fontFamily": "Inter", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "Pmb4t", + "fill": "$font-muted", + "content": "98 MB", + "fontFamily": "Inter", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "TwOzE", + "width": "fill_container", + "fill": "$surface-card", + "cornerRadius": 12, + "stroke": { + "thickness": 1, + "fill": "$border" + }, + "gap": 12, + "padding": 8, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "nnkSb", + "width": 80, + "height": 48, + "fill": { + "type": "image", + "enabled": true, + "url": "https://images.unsplash.com/photo-1448375240586-882707db888b?w=200", + "mode": "fill" + }, + "cornerRadius": 8, + "children": [ + { + "type": "frame", + "id": "U4UDkm", + "layoutPosition": "absolute", + "x": 32, + "y": 16, + "width": 16, + "height": 16, + "fill": "#0A0F14B3", + "cornerRadius": 999, + "justifyContent": "center", + "alignItems": "center" + }, + { + "type": "icon_font", + "id": "THUpA", + "layoutPosition": "absolute", + "x": 36, + "y": 20, + "width": 8, + "height": 8, + "iconFontName": "play", + "iconFontFamily": "lucide", + "fill": "#FFFFFF" + }, + { + "type": "frame", + "id": "UiTiH", + "layoutPosition": "absolute", + "x": 50, + "y": 30, + "width": 26, + "height": 14, + "fill": "#0A0F14CC", + "cornerRadius": 4, + "justifyContent": "center", + "alignItems": "center" + }, + { + "type": "text", + "id": "rituF", + "layoutPosition": "absolute", + "x": 52, + "y": 32, + "fill": "#FFFFFF", + "content": "04:15", + "fontFamily": "JetBrains Mono", + "fontSize": 9, + "fontWeight": "600" + } + ] + }, + { + "type": "frame", + "id": "IslO6", + "width": "fill_container", + "layout": "vertical", + "gap": 3, + "children": [ + { + "type": "text", + "id": "wJp1n", + "fill": "$font-primary", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "forest-rain.mp4", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "P9qKLi", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "LLPdh", + "width": 10, + "height": 10, + "iconFontName": "clock-3", + "iconFontFamily": "lucide", + "fill": "$font-muted" + }, + { + "type": "text", + "id": "GYqWr", + "fill": "$font-muted", + "content": "04:15", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "v6NMB", + "fill": "$font-muted", + "content": "·", + "fontFamily": "Inter", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "NgymH", + "fill": "$font-muted", + "content": "42 MB", + "fontFamily": "Inter", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "AGTU4", + "x": 5921, + "y": 964, + "name": "代码编辑页(AI 助手)", + "clip": true, + "width": 390, + "height": 1087, + "fill": "$surface", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "Y7Ozw8", + "width": "fill_container", + "height": 62, + "padding": [ + 0, + 20 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "GQYqQ", + "fill": "$font-primary", + "content": "9:41", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "kfPTb", + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "xNuQS", + "width": 16, + "height": 16, + "iconFontName": "signal", + "iconFontFamily": "lucide", + "fill": "$font-primary" + }, + { + "type": "icon_font", + "id": "hHHoZ", + "width": 16, + "height": 16, + "iconFontName": "wifi", + "iconFontFamily": "lucide", + "fill": "$font-primary" + }, + { + "type": "icon_font", + "id": "zCMRZ", + "width": 22, + "height": 16, + "iconFontName": "battery-full", + "iconFontFamily": "lucide", + "fill": "$font-primary" + } + ] + } + ] + }, + { + "type": "frame", + "id": "oeHxZ", + "width": "fill_container", + "height": 54, + "padding": [ + 0, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "d0sfo2", + "gap": 10, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "FA4Bq", + "width": 22, + "height": 22, + "iconFontName": "chevron-left", + "iconFontFamily": "lucide", + "fill": "$font-primary" + }, + { + "type": "frame", + "id": "e0aYxX", + "layout": "vertical", + "gap": 1, + "children": [ + { + "type": "frame", + "id": "Vmzzt", + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "GUAD5", + "fill": "$font-primary", + "content": "nginx.conf", + "fontFamily": "Inter", + "fontSize": 15, + "fontWeight": "600" + }, + { + "type": "ellipse", + "id": "b53Bsc", + "fill": "$warning", + "width": 6, + "height": 6 + } + ] + }, + { + "type": "text", + "id": "rQWvF", + "fill": "$font-muted", + "content": "/etc/nginx · 已修改", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "A1b0bm", + "gap": 10, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "B7qoB", + "fill": "$accent-soft", + "cornerRadius": "$radius-chip", + "stroke": { + "thickness": 1, + "fill": "$accent" + }, + "gap": 5, + "padding": [ + 5, + 10 + ], + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "MIbys", + "width": 12, + "height": 12, + "iconFontName": "sparkles", + "iconFontFamily": "lucide", + "fill": "$primary" + }, + { + "type": "text", + "id": "ToPdb", + "fill": "$primary", + "content": "AI", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "700" + } + ] + }, + { + "type": "icon_font", + "id": "D79ZyS", + "width": 20, + "height": 20, + "iconFontName": "ellipsis", + "iconFontFamily": "lucide", + "fill": "$font-secondary" + } + ] + } + ] + }, + { + "type": "frame", + "id": "i5DmX", + "width": "fill_container", + "height": 380, + "fill": "$surface-inverse", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "IG2Ff", + "width": "fill_container", + "height": "fill_container", + "children": [ + { + "type": "frame", + "id": "L9b80j", + "width": 36, + "height": "fill_container", + "layout": "vertical", + "gap": 4, + "padding": [ + 10, + 6, + 10, + 8 + ], + "alignItems": "end", + "children": [ + { + "type": "text", + "id": "BFoBH", + "fill": "#6B7280", + "content": "1", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "s4FbY", + "fill": "#6B7280", + "content": "2", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "FMTrV", + "fill": "#6B7280", + "content": "3", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "lGrcZ", + "fill": "#6B7280", + "content": "4", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "SeM8O", + "fill": "#6B7280", + "content": "5", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "UllDs", + "fill": "#6B7280", + "content": "6", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "u6KyA", + "fill": "$accent", + "content": "7", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "fCB0J", + "fill": "$accent", + "content": "8", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "FDuvb", + "fill": "$accent", + "content": "9", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "ajjew", + "fill": "$accent", + "content": "10", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "O38gwk", + "fill": "#6B7280", + "content": "11", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "uWm99", + "fill": "#6B7280", + "content": "12", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "z642n5", + "fill": "#6B7280", + "content": "13", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "e5EKxV", + "fill": "#6B7280", + "content": "14", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "p32MHD", + "fill": "#6B7280", + "content": "15", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "h5oD4", + "fill": "#6B7280", + "content": "16", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "F4lFf", + "width": "fill_container", + "height": "fill_container", + "layout": "vertical", + "gap": 4, + "padding": [ + 10, + 12, + 10, + 8 + ], + "children": [ + { + "type": "frame", + "id": "nDM1D", + "width": "fill_container", + "height": 16, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "aytNk", + "fill": "#569CD6", + "content": "worker_processes", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "nF7hU", + "fill": "#D4D4D4", + "content": " ", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "kUEZ5", + "fill": "#B5CEA8", + "content": "1", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "kwnlw", + "fill": "#D4D4D4", + "content": ";", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "ZZQE4", + "width": "fill_container", + "height": 16, + "fill": "#00000000", + "alignItems": "center" + }, + { + "type": "frame", + "id": "s1wha", + "width": "fill_container", + "height": 16, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "Zm3Ku", + "fill": "#569CD6", + "content": "events", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "Nls4L", + "fill": "#D4D4D4", + "content": " {", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "fnGFX", + "width": "fill_container", + "height": 16, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "Evwi9", + "fill": "#569CD6", + "content": " worker_connections", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "fxoey", + "fill": "#D4D4D4", + "content": " ", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "gkAP1", + "fill": "#B5CEA8", + "content": "1024", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "qB0Gi", + "fill": "#D4D4D4", + "content": ";", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "mYEf2", + "width": "fill_container", + "height": 16, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "wwNcm", + "fill": "#D4D4D4", + "content": "}", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "rQtRi", + "width": "fill_container", + "height": 16, + "fill": "#00000000", + "alignItems": "center" + }, + { + "type": "frame", + "id": "R4rvB", + "width": "fill_container", + "height": 16, + "fill": "#E5B33A14", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "g2feSG", + "fill": "#569CD6", + "content": "http", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "q6Cfzt", + "fill": "#D4D4D4", + "content": " {", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "E8dENs", + "width": "fill_container", + "height": 16, + "fill": "#E5B33A14", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "RWCKW", + "fill": "#569CD6", + "content": " server", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "oy9dG", + "fill": "#D4D4D4", + "content": " {", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "FtbDX", + "width": "fill_container", + "height": 16, + "fill": "#E5B33A14", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "EYH34", + "fill": "#569CD6", + "content": " listen", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "v8DsP", + "fill": "#D4D4D4", + "content": " ", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "YDi07", + "fill": "#B5CEA8", + "content": "80", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "y2GXL", + "fill": "#D4D4D4", + "content": ";", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "Cf1wc", + "width": "fill_container", + "height": 16, + "fill": "#E5B33A14", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "HlchQ", + "fill": "#569CD6", + "content": " server_name", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "GKelJ", + "fill": "#D4D4D4", + "content": " ", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "P8rzrF", + "fill": "#CE9178", + "content": "easynode.local", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "OD6CF", + "fill": "#D4D4D4", + "content": ";", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "h9GM2j", + "width": "fill_container", + "height": 16, + "fill": "#00000000", + "alignItems": "center" + }, + { + "type": "frame", + "id": "D9dZi", + "width": "fill_container", + "height": 16, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "r69GUu", + "fill": "#569CD6", + "content": " location ", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "ypyZ8", + "fill": "#CE9178", + "content": "/", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "Kmvu7", + "fill": "#D4D4D4", + "content": " {", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "Zs1zr", + "width": "fill_container", + "height": 16, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "v49ff", + "fill": "#569CD6", + "content": " root", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "hgT7g", + "fill": "#D4D4D4", + "content": " ", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "g8Xyw5", + "fill": "#CE9178", + "content": "/usr/share/nginx/html", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "z1y6RK", + "fill": "#D4D4D4", + "content": ";", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "yU789", + "width": "fill_container", + "height": 16, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "NvA6f", + "fill": "#D4D4D4", + "content": " }", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "NtFqi", + "width": "fill_container", + "height": 16, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "O9Scr", + "fill": "#D4D4D4", + "content": " }", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "DBnIZ", + "width": "fill_container", + "height": 16, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "T5uWmL", + "fill": "#D4D4D4", + "content": "}", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "L1EAU", + "layoutPosition": "absolute", + "x": 36, + "y": 168, + "width": 326, + "fill": "#1F2937F2", + "cornerRadius": 12, + "stroke": { + "thickness": 1, + "fill": "$accent" + }, + "effect": { + "type": "shadow", + "shadowType": "outer", + "color": "#00000066", + "offset": { + "x": 0, + "y": 6 + }, + "blur": 20 + }, + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "H0J28d", + "width": "fill_container", + "stroke": { + "thickness": { + "bottom": 1 + }, + "fill": "#FFFFFF1A" + }, + "padding": [ + 8, + 12 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "CQ0ns", + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "YYAvV", + "width": 12, + "height": 12, + "iconFontName": "sparkles", + "iconFontFamily": "lucide", + "fill": "$accent" + }, + { + "type": "text", + "id": "N7Z0Z", + "fill": "#FBF5E6", + "content": "AI 建议", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "700" + }, + { + "type": "text", + "id": "x7mouF", + "fill": "#9CA3AF", + "content": "· 添加反向代理", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "icon_font", + "id": "ZxlpL", + "width": 14, + "height": 14, + "iconFontName": "x", + "iconFontFamily": "lucide", + "fill": "#9CA3AF" + } + ] + }, + { + "type": "frame", + "id": "BxlIj", + "width": "fill_container", + "height": 18, + "fill": "#16653433", + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "wC644", + "fill": "#86EFAC", + "content": "+ location /api {", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "WqBhF", + "width": "fill_container", + "height": 18, + "fill": "#16653433", + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "JPqI7", + "fill": "#86EFAC", + "content": "+ proxy_pass http://localhost:8082;", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "cmRJk", + "width": "fill_container", + "height": 18, + "fill": "#16653433", + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "rfiI7", + "fill": "#86EFAC", + "content": "+ proxy_http_version 1.1;", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "VgVP0", + "width": "fill_container", + "height": 18, + "fill": "#16653433", + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "oNk5n", + "fill": "#86EFAC", + "content": "+ }", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "RLegb", + "width": "fill_container", + "stroke": { + "thickness": { + "top": 1 + }, + "fill": "#FFFFFF1A" + }, + "gap": 6, + "padding": [ + 8, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "k5Mfx", + "width": "fill_container", + "height": 28, + "fill": "$accent", + "cornerRadius": 8, + "gap": 4, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "EuvnO", + "width": 12, + "height": 12, + "iconFontName": "check", + "iconFontFamily": "lucide", + "fill": "$primary" + }, + { + "type": "text", + "id": "ujOHz", + "fill": "$primary", + "content": "应用到第 11 行", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "700" + } + ] + }, + { + "type": "frame", + "id": "nMrgG", + "height": 28, + "fill": "#FFFFFF14", + "cornerRadius": 8, + "stroke": { + "thickness": 1, + "fill": "#FFFFFF2A" + }, + "padding": [ + 0, + 10 + ], + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "oA9PZ", + "fill": "#E5E7EB", + "content": "忽略", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "600" + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "cRoyG", + "width": "fill_container", + "height": "fill_container", + "fill": "$surface", + "stroke": { + "thickness": { + "top": 1 + }, + "fill": "$border" + }, + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "Da3tm", + "width": "fill_container", + "height": 16, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 2, + "id": "ztbam", + "fill": "$border-strong", + "width": 36, + "height": 4 + } + ] + }, + { + "type": "frame", + "id": "WvldY", + "width": "fill_container", + "padding": [ + 0, + 16, + 8, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "mlIz5", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "oeTYH", + "width": 24, + "height": 24, + "fill": "$accent", + "cornerRadius": 8, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "OzaDh", + "width": 12, + "height": 12, + "iconFontName": "sparkles", + "iconFontFamily": "lucide", + "fill": "#FFFFFF" + } + ] + }, + { + "type": "frame", + "id": "ITbLc", + "layout": "vertical", + "children": [ + { + "type": "text", + "id": "Y4BhL1", + "fill": "$font-primary", + "content": "AI 助手", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "700" + }, + { + "type": "frame", + "id": "GgqGC", + "gap": 4, + "alignItems": "center", + "children": [ + { + "type": "ellipse", + "id": "LfZTb", + "fill": "$success", + "width": 5, + "height": 5 + }, + { + "type": "text", + "id": "gGXp2", + "fill": "$font-muted", + "content": "已连接 nginx 上下文", + "fontFamily": "Inter", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "j5X5j", + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "XtQ7k", + "fill": "$surface-card", + "cornerRadius": "$radius-chip", + "stroke": { + "thickness": 1, + "fill": "$border" + }, + "gap": 4, + "padding": [ + 3, + 8 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "SPMa8", + "fill": "$font-secondary", + "content": "GPT-4o", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "600" + }, + { + "type": "icon_font", + "id": "iNMFm", + "width": 10, + "height": 10, + "iconFontName": "chevron-down", + "iconFontFamily": "lucide", + "fill": "$font-muted" + } + ] + }, + { + "type": "icon_font", + "id": "P0tYMC", + "width": 16, + "height": 16, + "iconFontName": "chevron-down", + "iconFontFamily": "lucide", + "fill": "$font-secondary" + } + ] + } + ] + }, + { + "type": "frame", + "id": "BbGbI", + "width": "fill_container", + "height": "fill_container", + "layout": "vertical", + "gap": 12, + "padding": [ + 4, + 16, + 8, + 16 + ], + "children": [ + { + "type": "frame", + "id": "sDxne", + "width": "fill_container", + "justifyContent": "end", + "children": [ + { + "type": "frame", + "id": "HPEPt", + "width": 280, + "fill": "$accent-soft", + "cornerRadius": 14, + "padding": [ + 8, + 12 + ], + "children": [ + { + "type": "text", + "id": "G2c0u", + "fill": "$primary", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "首页访问 502,可能怎么修?要把请求转给 8082 端口。", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "500" + } + ] + } + ] + }, + { + "type": "frame", + "id": "FSOeJ", + "width": "fill_container", + "gap": 8, + "children": [ + { + "type": "frame", + "id": "Mg6TH", + "width": 24, + "height": 24, + "fill": "$accent", + "cornerRadius": 8, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "MTSUS", + "width": 12, + "height": 12, + "iconFontName": "sparkles", + "iconFontFamily": "lucide", + "fill": "#FFFFFF" + } + ] + }, + { + "type": "frame", + "id": "oZN0i", + "width": "fill_container", + "layout": "vertical", + "gap": 6, + "children": [ + { + "type": "frame", + "id": "oy69m", + "width": "fill_container", + "fill": "$surface-card", + "cornerRadius": 14, + "stroke": { + "thickness": 1, + "fill": "$border" + }, + "layout": "vertical", + "gap": 6, + "padding": [ + 10, + 12 + ], + "children": [ + { + "type": "text", + "id": "Z3U6z", + "fill": "$font-primary", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "你目前的 `location /` 只指向静态目录,要把 / 转发到 8082,需要新增反向代理:", + "lineHeight": 1.4, + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "500" + }, + { + "type": "frame", + "id": "vTj7S", + "width": "fill_container", + "fill": "$surface-inverse", + "cornerRadius": 8, + "layout": "vertical", + "gap": 1, + "padding": [ + 8, + 10 + ], + "children": [ + { + "type": "frame", + "id": "HxGEK", + "children": [ + { + "type": "text", + "id": "d0MP4", + "fill": "#569CD6", + "content": "location ", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "GTJ3O", + "fill": "#CE9178", + "content": "/api", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "o5mYvl", + "fill": "#D4D4D4", + "content": " {", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "gXRYv", + "children": [ + { + "type": "text", + "id": "kmsLG", + "fill": "#569CD6", + "content": " proxy_pass", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "UaGXN", + "fill": "#CE9178", + "content": " http://localhost:8082", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "OMg9h", + "fill": "#D4D4D4", + "content": ";", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "w5eHEZ", + "children": [ + { + "type": "text", + "id": "nB3Er", + "fill": "#D4D4D4", + "content": "}", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "U4oqM", + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "LXYAZ", + "fill": "$primary", + "cornerRadius": 8, + "gap": 4, + "padding": [ + 5, + 10 + ], + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "nYG4H", + "width": 11, + "height": 11, + "iconFontName": "wand-sparkles", + "iconFontFamily": "lucide", + "fill": "$font-on-primary" + }, + { + "type": "text", + "id": "F131kO", + "fill": "$font-on-primary", + "content": "应用到代码", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "700" + } + ] + }, + { + "type": "frame", + "id": "e7fzaA", + "fill": "$surface-card", + "cornerRadius": 8, + "stroke": { + "thickness": 1, + "fill": "$border" + }, + "gap": 4, + "padding": [ + 5, + 10 + ], + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "cUToh", + "width": 11, + "height": 11, + "iconFontName": "copy", + "iconFontFamily": "lucide", + "fill": "$font-secondary" + }, + { + "type": "text", + "id": "N2KMHo", + "fill": "$font-secondary", + "content": "复制", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "600" + } + ] + }, + { + "type": "frame", + "id": "kSQKU", + "fill": "$surface-card", + "cornerRadius": 8, + "stroke": { + "thickness": 1, + "fill": "$border" + }, + "gap": 4, + "padding": [ + 5, + 10 + ], + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "S77IWP", + "width": 11, + "height": 11, + "iconFontName": "list-tree", + "iconFontFamily": "lucide", + "fill": "$font-secondary" + }, + { + "type": "text", + "id": "J8Q72t", + "fill": "$font-secondary", + "content": "解释", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "600" + } + ] + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "TAG5U", + "width": "fill_container", + "fill": "$surface", + "stroke": { + "thickness": { + "top": 1 + }, + "fill": "$border" + }, + "layout": "vertical", + "gap": 8, + "padding": [ + 8, + 16, + 16, + 16 + ], + "children": [ + { + "type": "frame", + "id": "XHAQP", + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "cgMwJ", + "fill": "$surface-card", + "cornerRadius": "$radius-chip", + "stroke": { + "thickness": 1, + "fill": "$border" + }, + "gap": 4, + "padding": [ + 4, + 9 + ], + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "a3iWlL", + "width": 10, + "height": 10, + "iconFontName": "book-open", + "iconFontFamily": "lucide", + "fill": "$font-secondary" + }, + { + "type": "text", + "id": "wOPys", + "fill": "$font-secondary", + "content": "解释代码", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "600" + } + ] + }, + { + "type": "frame", + "id": "OUXGK", + "fill": "$surface-card", + "cornerRadius": "$radius-chip", + "stroke": { + "thickness": 1, + "fill": "$border" + }, + "gap": 4, + "padding": [ + 4, + 9 + ], + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "SlDN7", + "width": 10, + "height": 10, + "iconFontName": "wand", + "iconFontFamily": "lucide", + "fill": "$font-secondary" + }, + { + "type": "text", + "id": "fizBV", + "fill": "$font-secondary", + "content": "优化", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "600" + } + ] + }, + { + "type": "frame", + "id": "BtLGG", + "fill": "$surface-card", + "cornerRadius": "$radius-chip", + "stroke": { + "thickness": 1, + "fill": "$border" + }, + "gap": 4, + "padding": [ + 4, + 9 + ], + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "EZcw8", + "width": 10, + "height": 10, + "iconFontName": "bug", + "iconFontFamily": "lucide", + "fill": "$font-secondary" + }, + { + "type": "text", + "id": "hmywW", + "fill": "$font-secondary", + "content": "找 bug", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "600" + } + ] + }, + { + "type": "frame", + "id": "k7He2Q", + "fill": "$surface-card", + "cornerRadius": "$radius-chip", + "stroke": { + "thickness": 1, + "fill": "$border" + }, + "gap": 4, + "padding": [ + 4, + 9 + ], + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "dnyTo", + "width": 10, + "height": 10, + "iconFontName": "test-tube", + "iconFontFamily": "lucide", + "fill": "$font-secondary" + }, + { + "type": "text", + "id": "cOSTp", + "fill": "$font-secondary", + "content": "写测试", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "600" + } + ] + } + ] + }, + { + "type": "frame", + "id": "dhWIt", + "width": "fill_container", + "height": 44, + "fill": "$surface-card", + "cornerRadius": 22, + "stroke": { + "thickness": 1, + "fill": "$border-strong" + }, + "gap": 8, + "padding": [ + 0, + 6, + 0, + 14 + ], + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "oUfDb", + "width": 14, + "height": 14, + "iconFontName": "at-sign", + "iconFontFamily": "lucide", + "fill": "$font-muted" + }, + { + "type": "text", + "id": "pYGKB", + "fill": "$font-muted", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "向 AI 提问,或输入 @ 引用文件...", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "KgeiP", + "width": 32, + "height": 32, + "fill": "$primary", + "cornerRadius": 999, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "OpcYP", + "width": 14, + "height": 14, + "iconFontName": "arrow-up", + "iconFontFamily": "lucide", + "fill": "$font-on-primary" + } + ] + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "DdRKa", + "x": 6351, + "y": 964, + "name": "代码编辑页(差异视图)", + "clip": true, + "width": 390, + "height": 1087, + "fill": "$surface", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "y1LSOm", + "name": "状态栏", + "width": "fill_container", + "height": 62, + "padding": [ + 0, + 20 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "GYwaM", + "fill": "$font-primary", + "content": "9:41", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "JpwgP", + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "WRH34", + "width": 16, + "height": 16, + "iconFontName": "signal", + "iconFontFamily": "lucide", + "fill": "$font-primary" + }, + { + "type": "icon_font", + "id": "k1KTC", + "width": 16, + "height": 16, + "iconFontName": "wifi", + "iconFontFamily": "lucide", + "fill": "$font-primary" + }, + { + "type": "icon_font", + "id": "HJAoN", + "width": 22, + "height": 16, + "iconFontName": "battery-full", + "iconFontFamily": "lucide", + "fill": "$font-primary" + } + ] + } + ] + }, + { + "type": "frame", + "id": "OXHhy", + "name": "顶部应用栏", + "width": "fill_container", + "height": 56, + "gap": 12, + "padding": [ + 0, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "basgv", + "gap": 10, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "T5ONVm", + "width": 22, + "height": 22, + "iconFontName": "chevron-left", + "iconFontFamily": "lucide", + "fill": "$font-primary" + }, + { + "type": "frame", + "id": "D46Z9", + "layout": "vertical", + "gap": 2, + "children": [ + { + "type": "text", + "id": "wf603", + "fill": "$font-primary", + "content": "nginx.conf", + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "600" + }, + { + "type": "text", + "id": "pGUED", + "fill": "$font-muted", + "content": "/etc/nginx", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "C4RVx", + "gap": 14, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "j8dTn", + "width": 22, + "height": 22, + "iconFontName": "undo-2", + "iconFontFamily": "lucide", + "fill": "$font-secondary" + }, + { + "type": "frame", + "id": "EOlGR", + "width": 28, + "height": 28, + "fill": "$accent-soft", + "cornerRadius": 8, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "RZ7MJ", + "width": 18, + "height": 18, + "iconFontName": "check", + "iconFontFamily": "lucide", + "fill": "$success" + } + ] + }, + { + "type": "icon_font", + "id": "vBiUy", + "width": 22, + "height": 22, + "iconFontName": "ellipsis", + "iconFontFamily": "lucide", + "fill": "$font-secondary" + } + ] + } + ] + }, + { + "type": "frame", + "id": "JKQn4", + "name": "视图切换", + "width": "fill_container", + "height": 38, + "padding": [ + 0, + 16 + ], + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "K2ZTc", + "width": "fill_container", + "height": 32, + "fill": "$surface-card", + "cornerRadius": "$radius-chip", + "stroke": { + "thickness": 1, + "fill": "$border" + }, + "padding": 3, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "kL7rC", + "width": "fill_container", + "height": "fill_container", + "fill": "#00000000", + "cornerRadius": "$radius-chip", + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "URiaX", + "fill": "$font-secondary", + "content": "原文", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "rHGPt", + "width": "fill_container", + "height": "fill_container", + "fill": "$primary", + "cornerRadius": "$radius-chip", + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "qdhnx", + "fill": "$font-on-primary", + "content": "差异", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "600" + } + ] + }, + { + "type": "frame", + "id": "G3LsD", + "width": "fill_container", + "height": "fill_container", + "fill": "#00000000", + "cornerRadius": "$radius-chip", + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "XLSW1", + "fill": "$font-secondary", + "content": "预览", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "500" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "NEx7c", + "name": "对比上下文", + "width": "fill_container", + "height": 46, + "gap": 8, + "padding": [ + 10, + 16, + 4, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "t7BEf", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "fGvWx", + "fill": "$surface-card", + "cornerRadius": "$radius-chip", + "stroke": { + "thickness": 1, + "fill": "$border" + }, + "gap": 6, + "padding": [ + 5, + 9 + ], + "alignItems": "center", + "children": [ + { + "type": "ellipse", + "id": "tNF0X", + "fill": "$danger", + "width": 6, + "height": 6 + }, + { + "type": "text", + "id": "pqbuo", + "fill": "$font-primary", + "content": "本地修改", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "text", + "id": "BD5Fb", + "fill": "$font-muted", + "content": "2分钟前", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "icon_font", + "id": "cq3Hj", + "width": 14, + "height": 14, + "iconFontName": "arrow-right", + "iconFontFamily": "lucide", + "fill": "$font-muted" + }, + { + "type": "frame", + "id": "IYdaY", + "fill": "$surface-card", + "cornerRadius": "$radius-chip", + "stroke": { + "thickness": 1, + "fill": "$border" + }, + "gap": 6, + "padding": [ + 5, + 9 + ], + "alignItems": "center", + "children": [ + { + "type": "ellipse", + "id": "HYuf8", + "fill": "$success", + "width": 6, + "height": 6 + }, + { + "type": "text", + "id": "L3Qu6", + "fill": "$font-primary", + "content": "远程版本", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "text", + "id": "GWKrP", + "fill": "$font-muted", + "content": "v2.1.4", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "ZRc3Y", + "fill": "$surface-card", + "cornerRadius": "$radius-chip", + "stroke": { + "thickness": 1, + "fill": "$border" + }, + "gap": 6, + "padding": [ + 5, + 9 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "h3M1Q", + "fill": "$success", + "content": "+12", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "text", + "id": "wG3JZ", + "fill": "$danger", + "content": "-8", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "600" + } + ] + } + ] + }, + { + "type": "frame", + "id": "EBRAN", + "name": "差异代码区", + "clip": true, + "width": "fill_container", + "height": "fill_container", + "fill": "$surface-inverse", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "rPD4C", + "width": "fill_container", + "height": 22, + "fill": "#3B82F622", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "JfGZZ", + "width": 56, + "height": "fill_container", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "YAeQn", + "width": "fill_container", + "height": "fill_container", + "padding": [ + 0, + 6, + 0, + 2 + ], + "justifyContent": "end", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "z2XkBx", + "fill": "#60A5FA", + "content": " ", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "T9Mzkn", + "width": "fill_container", + "height": "fill_container", + "padding": [ + 0, + 6, + 0, + 2 + ], + "justifyContent": "end", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "FHfBc", + "fill": "#60A5FA", + "content": " ", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "rectangle", + "id": "X6J0lv", + "fill": "#00000000", + "width": 2, + "height": "fill_container" + } + ] + }, + { + "type": "frame", + "id": "YkGmh", + "width": "fill_container", + "height": "fill_container", + "padding": [ + 0, + 8 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "h8iBD", + "fill": "#93C5FD", + "textGrowth": "fixed-width", + "width": 10, + "content": " ", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "text", + "id": "o48TS", + "fill": "#93C5FD", + "content": "@@ -38,9 +38,12 @@ server {", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "n4NWb", + "width": "fill_container", + "height": 22, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "AHYRf", + "width": 56, + "height": "fill_container", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "r7wgIK", + "width": "fill_container", + "height": "fill_container", + "padding": [ + 0, + 6, + 0, + 2 + ], + "justifyContent": "end", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "g72up1", + "fill": "#6B7280", + "content": "38", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "HvLPO", + "width": "fill_container", + "height": "fill_container", + "padding": [ + 0, + 6, + 0, + 2 + ], + "justifyContent": "end", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "q8dMKh", + "fill": "#6B7280", + "content": "38", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "rectangle", + "id": "i0UOV", + "fill": "#00000000", + "width": 2, + "height": "fill_container" + } + ] + }, + { + "type": "frame", + "id": "X88Q2", + "width": "fill_container", + "height": "fill_container", + "padding": [ + 0, + 8 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "fAoHd", + "fill": "#D4D4D4", + "textGrowth": "fixed-width", + "width": 10, + "content": " ", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "text", + "id": "vd1ZV", + "fill": "#D4D4D4", + "content": " listen 80;", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "dklvJ", + "width": "fill_container", + "height": 22, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "AQydO", + "width": 56, + "height": "fill_container", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "t1Ajfd", + "width": "fill_container", + "height": "fill_container", + "padding": [ + 0, + 6, + 0, + 2 + ], + "justifyContent": "end", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "moCVP", + "fill": "#6B7280", + "content": "39", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "S7xJ3T", + "width": "fill_container", + "height": "fill_container", + "padding": [ + 0, + 6, + 0, + 2 + ], + "justifyContent": "end", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "PwUIS", + "fill": "#6B7280", + "content": "39", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "rectangle", + "id": "N7nnO", + "fill": "#00000000", + "width": 2, + "height": "fill_container" + } + ] + }, + { + "type": "frame", + "id": "FtAhA", + "width": "fill_container", + "height": "fill_container", + "padding": [ + 0, + 8 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "Dze54", + "fill": "#D4D4D4", + "textGrowth": "fixed-width", + "width": 10, + "content": " ", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "text", + "id": "pYbZB", + "fill": "#D4D4D4", + "content": " server_name easynode.local;", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "HuaRn", + "width": "fill_container", + "height": 22, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "D7diH", + "width": 56, + "height": "fill_container", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "l2OoG", + "width": "fill_container", + "height": "fill_container", + "padding": [ + 0, + 6, + 0, + 2 + ], + "justifyContent": "end", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "A4amR", + "fill": "#6B7280", + "content": "40", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "b9DQuN", + "width": "fill_container", + "height": "fill_container", + "padding": [ + 0, + 6, + 0, + 2 + ], + "justifyContent": "end", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "IP064", + "fill": "#6B7280", + "content": "40", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "rectangle", + "id": "xnB6l", + "fill": "#00000000", + "width": 2, + "height": "fill_container" + } + ] + }, + { + "type": "frame", + "id": "oDfz2", + "width": "fill_container", + "height": "fill_container", + "padding": [ + 0, + 8 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "Eh92I", + "fill": "#D4D4D4", + "textGrowth": "fixed-width", + "width": 10, + "content": " ", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "text", + "id": "I5OuDg", + "fill": "#D4D4D4", + "content": " ", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "TDka8", + "width": "fill_container", + "height": 22, + "fill": "#C0392B3D", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "s0Ddv5", + "width": 56, + "height": "fill_container", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "UXwCG", + "width": "fill_container", + "height": "fill_container", + "padding": [ + 0, + 6, + 0, + 2 + ], + "justifyContent": "end", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "l0CC4N", + "fill": "#FCA5A5", + "content": "41", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "syMI6", + "width": "fill_container", + "height": "fill_container", + "padding": [ + 0, + 6, + 0, + 2 + ], + "justifyContent": "end", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "zIiD8", + "fill": "#FCA5A5", + "content": " ", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "rectangle", + "id": "Pdqnm", + "fill": "$danger", + "width": 2, + "height": "fill_container" + } + ] + }, + { + "type": "frame", + "id": "ks394", + "width": "fill_container", + "height": "fill_container", + "padding": [ + 0, + 8 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "Dgahn", + "fill": "#FCA5A5", + "textGrowth": "fixed-width", + "width": 10, + "content": "-", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "text", + "id": "aonvZ", + "fill": "#FCA5A5", + "content": " root /var/www/html;", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "GOQQQ", + "width": "fill_container", + "height": 22, + "fill": "#5A8E3A2E", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "u0BNs", + "width": 56, + "height": "fill_container", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "EG37n", + "width": "fill_container", + "height": "fill_container", + "padding": [ + 0, + 6, + 0, + 2 + ], + "justifyContent": "end", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "hAS2O", + "fill": "#86EFAC", + "content": " ", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "S26Loo", + "width": "fill_container", + "height": "fill_container", + "padding": [ + 0, + 6, + 0, + 2 + ], + "justifyContent": "end", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "UbWpK", + "fill": "#86EFAC", + "content": "41", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "rectangle", + "id": "AyQub", + "fill": "$success", + "width": 2, + "height": "fill_container" + } + ] + }, + { + "type": "frame", + "id": "zHm9L", + "width": "fill_container", + "height": "fill_container", + "padding": [ + 0, + 8 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "XRZZf", + "fill": "#BBF7D0", + "textGrowth": "fixed-width", + "width": 10, + "content": "+", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "text", + "id": "F7OWW", + "fill": "#BBF7D0", + "content": " root /var/www/easynode;", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "KoCJF", + "width": "fill_container", + "height": 22, + "fill": "#5A8E3A2E", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "US8zg", + "width": 56, + "height": "fill_container", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "mXIya", + "width": "fill_container", + "height": "fill_container", + "padding": [ + 0, + 6, + 0, + 2 + ], + "justifyContent": "end", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "V4ovk", + "fill": "#86EFAC", + "content": " ", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "H9pBqq", + "width": "fill_container", + "height": "fill_container", + "padding": [ + 0, + 6, + 0, + 2 + ], + "justifyContent": "end", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "C1YhR", + "fill": "#86EFAC", + "content": "42", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "rectangle", + "id": "C6UYVN", + "fill": "$success", + "width": 2, + "height": "fill_container" + } + ] + }, + { + "type": "frame", + "id": "Lox8M", + "width": "fill_container", + "height": "fill_container", + "padding": [ + 0, + 8 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "BHaTO", + "fill": "#BBF7D0", + "textGrowth": "fixed-width", + "width": 10, + "content": "+", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "text", + "id": "T8elA", + "fill": "#BBF7D0", + "content": " index index.html index.htm;", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "C6LIc", + "width": "fill_container", + "height": 22, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "VbQjk", + "width": 56, + "height": "fill_container", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "mOhET", + "width": "fill_container", + "height": "fill_container", + "padding": [ + 0, + 6, + 0, + 2 + ], + "justifyContent": "end", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "vEvxL", + "fill": "#6B7280", + "content": "42", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "mmgWp", + "width": "fill_container", + "height": "fill_container", + "padding": [ + 0, + 6, + 0, + 2 + ], + "justifyContent": "end", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "C0B11", + "fill": "#6B7280", + "content": "43", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "rectangle", + "id": "ir42c", + "fill": "#00000000", + "width": 2, + "height": "fill_container" + } + ] + }, + { + "type": "frame", + "id": "T6lQkn", + "width": "fill_container", + "height": "fill_container", + "padding": [ + 0, + 8 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "tPy52", + "fill": "#D4D4D4", + "textGrowth": "fixed-width", + "width": 10, + "content": " ", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "text", + "id": "wvQFI", + "fill": "#D4D4D4", + "content": " ", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "j2WY0Z", + "width": "fill_container", + "height": 22, + "fill": "#C0392B3D", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "OJfLP", + "width": 56, + "height": "fill_container", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "uEVRs", + "width": "fill_container", + "height": "fill_container", + "padding": [ + 0, + 6, + 0, + 2 + ], + "justifyContent": "end", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "ACCPP", + "fill": "#FCA5A5", + "content": "43", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "kVXLK", + "width": "fill_container", + "height": "fill_container", + "padding": [ + 0, + 6, + 0, + 2 + ], + "justifyContent": "end", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "i1BYAC", + "fill": "#FCA5A5", + "content": " ", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "rectangle", + "id": "V2ruLt", + "fill": "$danger", + "width": 2, + "height": "fill_container" + } + ] + }, + { + "type": "frame", + "id": "VNPt5", + "width": "fill_container", + "height": "fill_container", + "padding": [ + 0, + 8 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "OvjQG", + "fill": "#FCA5A5", + "textGrowth": "fixed-width", + "width": 10, + "content": "-", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "text", + "id": "YYeZt", + "fill": "#FCA5A5", + "content": " location / {", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "a5vVg", + "width": "fill_container", + "height": 22, + "fill": "#C0392B3D", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "d4lk6", + "width": 56, + "height": "fill_container", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "v9wlM", + "width": "fill_container", + "height": "fill_container", + "padding": [ + 0, + 6, + 0, + 2 + ], + "justifyContent": "end", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "mNyWB", + "fill": "#FCA5A5", + "content": "44", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "V6ud7Q", + "width": "fill_container", + "height": "fill_container", + "padding": [ + 0, + 6, + 0, + 2 + ], + "justifyContent": "end", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "CpPSM", + "fill": "#FCA5A5", + "content": " ", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "rectangle", + "id": "yNqNN", + "fill": "$danger", + "width": 2, + "height": "fill_container" + } + ] + }, + { + "type": "frame", + "id": "H8pPV0", + "width": "fill_container", + "height": "fill_container", + "padding": [ + 0, + 8 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "oujRv", + "fill": "#FCA5A5", + "textGrowth": "fixed-width", + "width": 10, + "content": "-", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "text", + "id": "h6Mnb", + "fill": "#FCA5A5", + "content": " try_files $uri =404;", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "H75AZP", + "width": "fill_container", + "height": 22, + "fill": "#C0392B3D", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "k1VJh", + "width": 56, + "height": "fill_container", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "XHOCG", + "width": "fill_container", + "height": "fill_container", + "padding": [ + 0, + 6, + 0, + 2 + ], + "justifyContent": "end", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "vXQSZ", + "fill": "#FCA5A5", + "content": "45", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "lih6K", + "width": "fill_container", + "height": "fill_container", + "padding": [ + 0, + 6, + 0, + 2 + ], + "justifyContent": "end", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "r5OVG", + "fill": "#FCA5A5", + "content": " ", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "rectangle", + "id": "F3wrB", + "fill": "$danger", + "width": 2, + "height": "fill_container" + } + ] + }, + { + "type": "frame", + "id": "ItllW", + "width": "fill_container", + "height": "fill_container", + "padding": [ + 0, + 8 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "TVy7M", + "fill": "#FCA5A5", + "textGrowth": "fixed-width", + "width": 10, + "content": "-", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "text", + "id": "nEOrq", + "fill": "#FCA5A5", + "content": " }", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "xws3i", + "width": "fill_container", + "height": 22, + "fill": "#5A8E3A2E", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "GIJKH", + "width": 56, + "height": "fill_container", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "nWg5a", + "width": "fill_container", + "height": "fill_container", + "padding": [ + 0, + 6, + 0, + 2 + ], + "justifyContent": "end", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "a1PGm", + "fill": "#86EFAC", + "content": " ", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "OMfPA", + "width": "fill_container", + "height": "fill_container", + "padding": [ + 0, + 6, + 0, + 2 + ], + "justifyContent": "end", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "mb9GM", + "fill": "#86EFAC", + "content": "44", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "rectangle", + "id": "znkHY", + "fill": "$success", + "width": 2, + "height": "fill_container" + } + ] + }, + { + "type": "frame", + "id": "s5cEoA", + "width": "fill_container", + "height": "fill_container", + "padding": [ + 0, + 8 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "ntcJk", + "fill": "#BBF7D0", + "textGrowth": "fixed-width", + "width": 10, + "content": "+", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "text", + "id": "j8ioru", + "fill": "#BBF7D0", + "content": " location / {", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "Xu8eW", + "width": "fill_container", + "height": 22, + "fill": "#5A8E3A2E", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "ZE2kU", + "width": 56, + "height": "fill_container", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "pVvDU", + "width": "fill_container", + "height": "fill_container", + "padding": [ + 0, + 6, + 0, + 2 + ], + "justifyContent": "end", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "okOzd", + "fill": "#86EFAC", + "content": " ", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "urj9z", + "width": "fill_container", + "height": "fill_container", + "padding": [ + 0, + 6, + 0, + 2 + ], + "justifyContent": "end", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "HUZzt", + "fill": "#86EFAC", + "content": "45", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "rectangle", + "id": "H802gm", + "fill": "$success", + "width": 2, + "height": "fill_container" + } + ] + }, + { + "type": "frame", + "id": "JXLYR", + "width": "fill_container", + "height": "fill_container", + "padding": [ + 0, + 8 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "sCtwr", + "fill": "#BBF7D0", + "textGrowth": "fixed-width", + "width": 10, + "content": "+", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "text", + "id": "NzkM0", + "fill": "#BBF7D0", + "content": " proxy_pass http://localhost:8082;", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "jhyNI", + "width": "fill_container", + "height": 22, + "fill": "#5A8E3A2E", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "z9164", + "width": 56, + "height": "fill_container", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "LxVpp", + "width": "fill_container", + "height": "fill_container", + "padding": [ + 0, + 6, + 0, + 2 + ], + "justifyContent": "end", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "OChlV", + "fill": "#86EFAC", + "content": " ", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "SPG16", + "width": "fill_container", + "height": "fill_container", + "padding": [ + 0, + 6, + 0, + 2 + ], + "justifyContent": "end", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "YGXu3", + "fill": "#86EFAC", + "content": "46", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "rectangle", + "id": "yWomy", + "fill": "$success", + "width": 2, + "height": "fill_container" + } + ] + }, + { + "type": "frame", + "id": "E73FvC", + "width": "fill_container", + "height": "fill_container", + "padding": [ + 0, + 8 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "GKERP", + "fill": "#BBF7D0", + "textGrowth": "fixed-width", + "width": 10, + "content": "+", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "text", + "id": "nSxba", + "fill": "#BBF7D0", + "content": " proxy_http_version 1.1;", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "O310MZ", + "width": "fill_container", + "height": 22, + "fill": "#5A8E3A2E", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "O9z8Oo", + "width": 56, + "height": "fill_container", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "G4fJ3b", + "width": "fill_container", + "height": "fill_container", + "padding": [ + 0, + 6, + 0, + 2 + ], + "justifyContent": "end", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "ykP1S", + "fill": "#86EFAC", + "content": " ", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "xTa3D", + "width": "fill_container", + "height": "fill_container", + "padding": [ + 0, + 6, + 0, + 2 + ], + "justifyContent": "end", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "K85Tf", + "fill": "#86EFAC", + "content": "47", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "rectangle", + "id": "vkhIG", + "fill": "$success", + "width": 2, + "height": "fill_container" + } + ] + }, + { + "type": "frame", + "id": "i4z8z", + "width": "fill_container", + "height": "fill_container", + "padding": [ + 0, + 8 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "eWeuq", + "fill": "#BBF7D0", + "textGrowth": "fixed-width", + "width": 10, + "content": "+", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "text", + "id": "Fo5v5", + "fill": "#BBF7D0", + "content": " proxy_set_header Host $host;", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "MkZRh", + "width": "fill_container", + "height": 22, + "fill": "#5A8E3A2E", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "Fo3VT", + "width": 56, + "height": "fill_container", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "uQRbX", + "width": "fill_container", + "height": "fill_container", + "padding": [ + 0, + 6, + 0, + 2 + ], + "justifyContent": "end", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "CObIG", + "fill": "#86EFAC", + "content": " ", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "oymXB", + "width": "fill_container", + "height": "fill_container", + "padding": [ + 0, + 6, + 0, + 2 + ], + "justifyContent": "end", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "AZzOY", + "fill": "#86EFAC", + "content": "48", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "rectangle", + "id": "v33Rzd", + "fill": "$success", + "width": 2, + "height": "fill_container" + } + ] + }, + { + "type": "frame", + "id": "Klfp3", + "width": "fill_container", + "height": "fill_container", + "padding": [ + 0, + 8 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "dfYWP", + "fill": "#BBF7D0", + "textGrowth": "fixed-width", + "width": 10, + "content": "+", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "text", + "id": "gxK83", + "fill": "#BBF7D0", + "content": " }", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "ApkAy", + "width": "fill_container", + "height": 22, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "uQzYu", + "width": 56, + "height": "fill_container", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "XTkuY", + "width": "fill_container", + "height": "fill_container", + "padding": [ + 0, + 6, + 0, + 2 + ], + "justifyContent": "end", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "ODm7T", + "fill": "#6B7280", + "content": "46", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "xxyXh", + "width": "fill_container", + "height": "fill_container", + "padding": [ + 0, + 6, + 0, + 2 + ], + "justifyContent": "end", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "PMkff", + "fill": "#6B7280", + "content": "49", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "rectangle", + "id": "Uh3LQ", + "fill": "#00000000", + "width": 2, + "height": "fill_container" + } + ] + }, + { + "type": "frame", + "id": "oi9sE", + "width": "fill_container", + "height": "fill_container", + "padding": [ + 0, + 8 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "a24hN9", + "fill": "#D4D4D4", + "textGrowth": "fixed-width", + "width": 10, + "content": " ", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "text", + "id": "GrO7K", + "fill": "#D4D4D4", + "content": " ", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "Ulqv3", + "width": "fill_container", + "height": 22, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "X6HWk", + "width": 56, + "height": "fill_container", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "ty0fX", + "width": "fill_container", + "height": "fill_container", + "padding": [ + 0, + 6, + 0, + 2 + ], + "justifyContent": "end", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "A2eR8s", + "fill": "#6B7280", + "content": "47", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "V9yBzh", + "width": "fill_container", + "height": "fill_container", + "padding": [ + 0, + 6, + 0, + 2 + ], + "justifyContent": "end", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "z80BGF", + "fill": "#6B7280", + "content": "50", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "rectangle", + "id": "Vt46q", + "fill": "#00000000", + "width": 2, + "height": "fill_container" + } + ] + }, + { + "type": "frame", + "id": "DqQuS", + "width": "fill_container", + "height": "fill_container", + "padding": [ + 0, + 8 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "TQ6x4", + "fill": "#D4D4D4", + "textGrowth": "fixed-width", + "width": 10, + "content": " ", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "text", + "id": "bfMSd", + "fill": "#D4D4D4", + "content": " gzip on;", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "TVcFt", + "name": "底部操作栏", + "width": "fill_container", + "fill": "$surface", + "stroke": { + "thickness": { + "top": 1 + }, + "fill": "$border" + }, + "layout": "vertical", + "gap": 10, + "padding": [ + 12, + 16, + 16, + 16 + ], + "children": [ + { + "type": "frame", + "id": "S7Gjp", + "width": "fill_container", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "ellipse", + "id": "B9U5YZ", + "fill": "$accent", + "width": 6, + "height": 6 + }, + { + "type": "text", + "id": "s3NLI", + "fill": "$font-secondary", + "content": "6 个修改未保存 · nginx.conf", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "s1nmxg", + "width": "fill_container", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "I6piD", + "width": "fill_container", + "height": 42, + "fill": "$primary", + "cornerRadius": "$radius-control", + "gap": 8, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "b5Kil", + "width": 18, + "height": 18, + "iconFontName": "cloud-upload", + "iconFontFamily": "lucide", + "fill": "$font-on-primary" + }, + { + "type": "text", + "id": "OTfnR", + "fill": "$font-on-primary", + "content": "保存到远程", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "600" + } + ] + }, + { + "type": "frame", + "id": "nAsoY", + "width": 42, + "height": 42, + "fill": "$surface-card", + "cornerRadius": "$radius-control", + "stroke": { + "thickness": 1, + "fill": "$border" + }, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "aUJsk", + "width": 18, + "height": 18, + "iconFontName": "rotate-ccw", + "iconFontFamily": "lucide", + "fill": "$font-secondary" + } + ] + }, + { + "type": "frame", + "id": "S6Lj6", + "width": 42, + "height": 42, + "fill": "$surface-card", + "cornerRadius": "$radius-control", + "stroke": { + "thickness": 1, + "fill": "$border" + }, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "MkPPK", + "width": 18, + "height": 18, + "iconFontName": "columns-2", + "iconFontFamily": "lucide", + "fill": "$font-secondary" + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "zbuYi", + "x": 6781, + "y": 964, + "name": "文件页(网格视图)", + "clip": true, + "width": 390, + "height": 1087, + "fill": "$surface", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "cNv0K", + "width": "fill_container", + "height": 62, + "padding": [ + 0, + 20 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "z2iloF", + "fill": "$font-primary", + "content": "9:41", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "jNe7c", + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "gvYY3", + "width": 16, + "height": 16, + "iconFontName": "signal", + "iconFontFamily": "lucide", + "fill": "$font-primary" + }, + { + "type": "icon_font", + "id": "f8Un2h", + "width": 16, + "height": 16, + "iconFontName": "wifi", + "iconFontFamily": "lucide", + "fill": "$font-primary" + }, + { + "type": "icon_font", + "id": "FWbgY", + "width": 22, + "height": 16, + "iconFontName": "battery-full", + "iconFontFamily": "lucide", + "fill": "$font-primary" + } + ] + } + ] + }, + { + "type": "frame", + "id": "neMs8", + "width": "fill_container", + "height": 56, + "padding": [ + 0, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "UQd4o", + "gap": 10, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "JUDBV", + "width": 32, + "height": 32, + "fill": "$accent", + "cornerRadius": 8, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "vq49b", + "fill": "#FFFFFF", + "content": ">_", + "fontFamily": "JetBrains Mono", + "fontSize": 13, + "fontWeight": "700" + } + ] + }, + { + "type": "text", + "id": "fKbta", + "fill": "$font-primary", + "content": "文件", + "fontFamily": "Inter", + "fontSize": 20, + "fontWeight": "700" + } + ] + }, + { + "type": "frame", + "id": "DkoPt", + "height": 32, + "fill": "$surface-card", + "cornerRadius": "$radius-control", + "stroke": { + "thickness": 1, + "fill": "$border" + }, + "padding": 3, + "children": [ + { + "type": "frame", + "id": "HLruo", + "width": 38, + "height": 26, + "fill": "$primary", + "cornerRadius": 7, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "TzOEb", + "width": 14, + "height": 14, + "iconFontName": "layout-grid", + "iconFontFamily": "lucide", + "fill": "$font-on-primary" + } + ] + }, + { + "type": "frame", + "id": "WF81r", + "width": 38, + "height": 26, + "fill": "#00000000", + "cornerRadius": 7, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "Ehxe7", + "width": 14, + "height": 14, + "iconFontName": "list", + "iconFontFamily": "lucide", + "fill": "$font-secondary" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "udxP7", + "width": "fill_container", + "height": 36, + "padding": [ + 0, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "kJh52", + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "e4rs5", + "width": 18, + "height": 18, + "iconFontName": "chevron-left", + "iconFontFamily": "lucide", + "fill": "$font-secondary" + }, + { + "type": "frame", + "id": "mTgqd", + "fill": "$surface-card", + "cornerRadius": "$radius-chip", + "stroke": { + "thickness": 1, + "fill": "$border" + }, + "gap": 4, + "padding": [ + 4, + 10 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "FO8UQ", + "fill": "$font-secondary", + "content": "/root/easynode/static", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "500" + } + ] + } + ] + }, + { + "type": "icon_font", + "id": "kfEVF", + "width": 16, + "height": 16, + "iconFontName": "refresh-cw", + "iconFontFamily": "lucide", + "fill": "$font-secondary" + } + ] + }, + { + "type": "frame", + "id": "kg2oK", + "width": "fill_container", + "gap": 6, + "padding": [ + 0, + 16, + 4, + 16 + ], + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "jrFXO", + "fill": "$primary", + "cornerRadius": "$radius-chip", + "padding": [ + 5, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "T2N2y", + "fill": "$font-on-primary", + "content": "全部 24", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "600" + } + ] + }, + { + "type": "frame", + "id": "EUxvh", + "fill": "#00000000", + "cornerRadius": "$radius-chip", + "stroke": { + "thickness": 1, + "fill": "$border" + }, + "padding": [ + 5, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "p4P6B", + "fill": "$font-secondary", + "content": "图片 8", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "600" + } + ] + }, + { + "type": "frame", + "id": "ynDnz", + "fill": "#00000000", + "cornerRadius": "$radius-chip", + "stroke": { + "thickness": 1, + "fill": "$border" + }, + "padding": [ + 5, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "O8X5U", + "fill": "$font-secondary", + "content": "影片 3", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "600" + } + ] + }, + { + "type": "frame", + "id": "wagch", + "fill": "#00000000", + "cornerRadius": "$radius-chip", + "stroke": { + "thickness": 1, + "fill": "$border" + }, + "padding": [ + 5, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "WK2h0", + "fill": "$font-secondary", + "content": "文档 6", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "600" + } + ] + }, + { + "type": "frame", + "id": "suJVi", + "fill": "#00000000", + "cornerRadius": "$radius-chip", + "stroke": { + "thickness": 1, + "fill": "$border" + }, + "padding": [ + 5, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "Vq3Wl", + "fill": "$font-secondary", + "content": "文件夹 7", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "600" + } + ] + } + ] + }, + { + "type": "frame", + "id": "o6OQd", + "width": "fill_container", + "layout": "vertical", + "gap": 10, + "padding": [ + 10, + 16, + 12, + 16 + ], + "children": [ + { + "type": "frame", + "id": "v31k1I", + "width": "fill_container", + "gap": 10, + "children": [ + { + "type": "frame", + "id": "Z3Cy7", + "width": "fill_container", + "height": 148, + "fill": "$surface-card", + "cornerRadius": 14, + "stroke": { + "thickness": 1, + "fill": "$border" + }, + "effect": { + "type": "shadow", + "shadowType": "outer", + "color": "#5C45200D", + "offset": { + "x": 0, + "y": 3 + }, + "blur": 12 + }, + "layout": "vertical", + "children": [ + { + "type": "rectangle", + "cornerRadius": [ + 4, + 4, + 0, + 0 + ], + "id": "M1k6RO", + "layoutPosition": "absolute", + "x": 14, + "y": -1, + "fill": "$accent", + "width": 56, + "height": 8 + }, + { + "type": "frame", + "id": "zcwvL", + "width": "fill_container", + "height": "fill_container", + "layout": "vertical", + "gap": 6, + "padding": [ + 12, + 14 + ], + "justifyContent": "center", + "children": [ + { + "type": "icon_font", + "id": "QCpN8", + "width": 36, + "height": 36, + "iconFontName": "folder", + "iconFontFamily": "lucide", + "fill": "$accent" + }, + { + "type": "frame", + "id": "MTPIo", + "width": "fill_container", + "layout": "vertical", + "gap": 2, + "children": [ + { + "type": "text", + "id": "fsSvS", + "fill": "$font-primary", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "easynode", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "700" + }, + { + "type": "frame", + "id": "i3XVgd", + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "T0eIgm", + "fill": "$font-muted", + "content": "24 项", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "Lt8or", + "fill": "$font-muted", + "content": "·", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "rpxlA", + "fill": "$font-muted", + "content": "4.0 KB", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "YmzMl", + "width": "fill_container", + "height": 148, + "fill": "$surface-card", + "cornerRadius": 14, + "stroke": { + "thickness": 1, + "fill": "$border" + }, + "effect": { + "type": "shadow", + "shadowType": "outer", + "color": "#5C45200D", + "offset": { + "x": 0, + "y": 3 + }, + "blur": 12 + }, + "layout": "vertical", + "children": [ + { + "type": "rectangle", + "cornerRadius": [ + 4, + 4, + 0, + 0 + ], + "id": "yn1X2", + "layoutPosition": "absolute", + "x": 14, + "y": -1, + "fill": "$accent", + "width": 56, + "height": 8 + }, + { + "type": "frame", + "id": "o9xIbY", + "width": "fill_container", + "height": "fill_container", + "layout": "vertical", + "gap": 6, + "padding": [ + 12, + 14 + ], + "justifyContent": "center", + "children": [ + { + "type": "frame", + "id": "ZRAhX", + "gap": 4, + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 4, + "id": "sbT8a", + "fill": { + "type": "image", + "enabled": true, + "url": "https://images.unsplash.com/photo-1551434678-e076c223a692?w=200", + "mode": "fill" + }, + "width": 28, + "height": 36, + "stroke": { + "thickness": 1, + "fill": "#FFFFFFAA" + } + }, + { + "type": "rectangle", + "cornerRadius": 4, + "id": "yToqE", + "fill": { + "type": "image", + "enabled": true, + "url": "https://images.unsplash.com/photo-1517694712202-14dd9538aa97?w=200", + "mode": "fill" + }, + "width": 28, + "height": 36, + "stroke": { + "thickness": 1, + "fill": "#FFFFFFAA" + } + }, + { + "type": "rectangle", + "cornerRadius": 4, + "id": "IhMnn", + "fill": { + "type": "image", + "enabled": true, + "url": "https://images.unsplash.com/photo-1498050108023-c5249f4df085?w=200", + "mode": "fill" + }, + "width": 28, + "height": 36, + "stroke": { + "thickness": 1, + "fill": "#FFFFFFAA" + } + } + ] + }, + { + "type": "frame", + "id": "ZYGEh", + "width": "fill_container", + "layout": "vertical", + "gap": 2, + "children": [ + { + "type": "text", + "id": "nkOnO", + "fill": "$font-primary", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "screenshots", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "700" + }, + { + "type": "frame", + "id": "jL4kg", + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "ap3Gj", + "fill": "$font-muted", + "content": "8 张图片", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "eFev2", + "fill": "$font-muted", + "content": "·", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "jHeZr", + "fill": "$font-muted", + "content": "12.4 MB", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "A30HN", + "width": "fill_container", + "gap": 10, + "children": [ + { + "type": "frame", + "id": "P9TrjT", + "width": "fill_container", + "height": 148, + "fill": { + "type": "image", + "enabled": true, + "url": "https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=400", + "mode": "fill" + }, + "cornerRadius": 14, + "stroke": { + "thickness": 1, + "fill": "$border" + }, + "effect": { + "type": "shadow", + "shadowType": "outer", + "color": "#5C452014", + "offset": { + "x": 0, + "y": 3 + }, + "blur": 12 + }, + "layout": "vertical", + "children": [ + { + "type": "rectangle", + "cornerRadius": [ + 0, + 0, + 14, + 14 + ], + "id": "Vy4YX", + "layoutPosition": "absolute", + "x": 0, + "y": 100, + "fill": { + "type": "gradient", + "gradientType": "linear", + "enabled": true, + "rotation": 180, + "size": { + "height": 1 + }, + "colors": [ + { + "color": "#0A0F1400", + "position": 0 + }, + { + "color": "#0A0F14CC", + "position": 1 + } + ] + }, + "width": 200, + "height": 68 + }, + { + "type": "frame", + "id": "kGPXI", + "layoutPosition": "absolute", + "x": 10, + "y": 10, + "fill": "#0A0F14B3", + "cornerRadius": 6, + "gap": 4, + "padding": [ + 3, + 7 + ], + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "hoL0H", + "width": 10, + "height": 10, + "iconFontName": "image", + "iconFontFamily": "lucide", + "fill": "#FFFFFF" + }, + { + "type": "text", + "id": "hVo8h", + "fill": "#FFFFFF", + "content": "1920×1080", + "fontFamily": "JetBrains Mono", + "fontSize": 9, + "fontWeight": "600" + } + ] + }, + { + "type": "frame", + "id": "NSf7q", + "layoutPosition": "absolute", + "x": 0, + "y": 116, + "width": 174, + "height": 50, + "layout": "vertical", + "gap": 2, + "padding": [ + 0, + 12 + ], + "justifyContent": "center", + "children": [ + { + "type": "text", + "id": "uak77", + "fill": "#FFFFFF", + "textGrowth": "fixed-width", + "width": 150, + "content": "sunset.jpg", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "700" + }, + { + "type": "text", + "id": "r0GkGV", + "fill": "#E5E7EB", + "content": "1.2 MB", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "w6yuo", + "width": "fill_container", + "height": 148, + "fill": { + "type": "image", + "enabled": true, + "url": "https://images.unsplash.com/photo-1500964757637-c85e8a162699?w=400", + "mode": "fill" + }, + "cornerRadius": 14, + "stroke": { + "thickness": 1, + "fill": "$border" + }, + "effect": { + "type": "shadow", + "shadowType": "outer", + "color": "#5C452014", + "offset": { + "x": 0, + "y": 3 + }, + "blur": 12 + }, + "layout": "vertical", + "children": [ + { + "type": "rectangle", + "cornerRadius": [ + 0, + 0, + 14, + 14 + ], + "id": "zDUAz", + "layoutPosition": "absolute", + "x": 0, + "y": 100, + "fill": { + "type": "gradient", + "gradientType": "linear", + "enabled": true, + "rotation": 180, + "size": { + "height": 1 + }, + "colors": [ + { + "color": "#0A0F1400", + "position": 0 + }, + { + "color": "#0A0F14CC", + "position": 1 + } + ] + }, + "width": 200, + "height": 68 + }, + { + "type": "frame", + "id": "VyRwd", + "layoutPosition": "absolute", + "x": 10, + "y": 10, + "fill": "#0A0F14B3", + "cornerRadius": 6, + "gap": 4, + "padding": [ + 3, + 7 + ], + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "XRE6C", + "width": 10, + "height": 10, + "iconFontName": "image", + "iconFontFamily": "lucide", + "fill": "#FFFFFF" + }, + { + "type": "text", + "id": "lkkAI", + "fill": "#FFFFFF", + "content": "2400×1600", + "fontFamily": "JetBrains Mono", + "fontSize": 9, + "fontWeight": "600" + } + ] + }, + { + "type": "frame", + "id": "cbc20", + "layoutPosition": "absolute", + "x": 0, + "y": 116, + "width": 174, + "height": 50, + "layout": "vertical", + "gap": 2, + "padding": [ + 0, + 12 + ], + "justifyContent": "center", + "children": [ + { + "type": "text", + "id": "vZSpX", + "fill": "#FFFFFF", + "textGrowth": "fixed-width", + "width": 150, + "content": "lake.jpg", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "700" + }, + { + "type": "text", + "id": "F4OW3P", + "fill": "#E5E7EB", + "content": "987 KB", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "I6Kr0Q", + "width": "fill_container", + "gap": 10, + "children": [ + { + "type": "frame", + "id": "KQhQt", + "width": "fill_container", + "height": 148, + "fill": { + "type": "image", + "enabled": true, + "url": "https://images.unsplash.com/photo-1505142468610-359e7d316be0?w=400", + "mode": "fill" + }, + "cornerRadius": 14, + "stroke": { + "thickness": 1, + "fill": "$border" + }, + "effect": { + "type": "shadow", + "shadowType": "outer", + "color": "#5C452014", + "offset": { + "x": 0, + "y": 3 + }, + "blur": 12 + }, + "children": [ + { + "type": "rectangle", + "cornerRadius": 14, + "id": "FFZTf", + "layoutPosition": "absolute", + "x": 0, + "y": 0, + "fill": { + "type": "gradient", + "gradientType": "linear", + "enabled": true, + "rotation": 180, + "size": { + "height": 1 + }, + "colors": [ + { + "color": "#0A0F1422", + "position": 0 + }, + { + "color": "#0A0F14CC", + "position": 1 + } + ] + }, + "width": 200, + "height": 168 + }, + { + "type": "frame", + "id": "dDPRn", + "layoutPosition": "absolute", + "x": 65, + "y": 50, + "width": 48, + "height": 48, + "fill": "#FFFFFFE6", + "cornerRadius": 999, + "effect": { + "type": "shadow", + "shadowType": "outer", + "color": "#00000055", + "offset": { + "x": 0, + "y": 4 + }, + "blur": 18 + }, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "wDISl", + "width": 20, + "height": 20, + "iconFontName": "play", + "iconFontFamily": "lucide", + "fill": "$primary" + } + ] + }, + { + "type": "frame", + "id": "Tjq6d", + "layoutPosition": "absolute", + "x": 130, + "y": 10, + "fill": "#0A0F14B3", + "cornerRadius": 6, + "gap": 4, + "padding": [ + 3, + 7 + ], + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "cmZVw", + "width": 10, + "height": 10, + "iconFontName": "film", + "iconFontFamily": "lucide", + "fill": "#FFFFFF" + }, + { + "type": "text", + "id": "T10iK", + "fill": "#FFFFFF", + "content": "15:42", + "fontFamily": "JetBrains Mono", + "fontSize": 9, + "fontWeight": "600" + } + ] + }, + { + "type": "frame", + "id": "Hxpl6", + "layoutPosition": "absolute", + "x": 0, + "y": 116, + "width": 174, + "height": 50, + "layout": "vertical", + "gap": 2, + "padding": [ + 0, + 12 + ], + "justifyContent": "center", + "children": [ + { + "type": "text", + "id": "dWgn4", + "fill": "#FFFFFF", + "textGrowth": "fixed-width", + "width": 150, + "content": "sunset-timelapse.mp4", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "700" + }, + { + "type": "text", + "id": "g9NXfb", + "fill": "#E5E7EB", + "content": "124 MB · MP4", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "iN6hE", + "width": "fill_container", + "height": 148, + "fill": { + "type": "image", + "enabled": true, + "url": "https://images.unsplash.com/photo-1551288049-bebda4e38f71?w=400", + "mode": "fill" + }, + "cornerRadius": 14, + "stroke": { + "thickness": 1, + "fill": "$border" + }, + "effect": { + "type": "shadow", + "shadowType": "outer", + "color": "#5C452014", + "offset": { + "x": 0, + "y": 3 + }, + "blur": 12 + }, + "layout": "vertical", + "children": [ + { + "type": "rectangle", + "cornerRadius": [ + 0, + 0, + 14, + 14 + ], + "id": "sGKbc", + "layoutPosition": "absolute", + "x": 0, + "y": 100, + "fill": { + "type": "gradient", + "gradientType": "linear", + "enabled": true, + "rotation": 180, + "size": { + "height": 1 + }, + "colors": [ + { + "color": "#0A0F1400", + "position": 0 + }, + { + "color": "#0A0F14CC", + "position": 1 + } + ] + }, + "width": 200, + "height": 68 + }, + { + "type": "frame", + "id": "EBIwN", + "layoutPosition": "absolute", + "x": 10, + "y": 10, + "fill": "#0A0F14B3", + "cornerRadius": 6, + "gap": 4, + "padding": [ + 3, + 7 + ], + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "ZT6OI", + "width": 10, + "height": 10, + "iconFontName": "image", + "iconFontFamily": "lucide", + "fill": "#FFFFFF" + }, + { + "type": "text", + "id": "vvJtS", + "fill": "#FFFFFF", + "content": "1200×800", + "fontFamily": "JetBrains Mono", + "fontSize": 9, + "fontWeight": "600" + } + ] + }, + { + "type": "frame", + "id": "oDaWD", + "layoutPosition": "absolute", + "x": 0, + "y": 116, + "width": 174, + "height": 50, + "layout": "vertical", + "gap": 2, + "padding": [ + 0, + 12 + ], + "justifyContent": "center", + "children": [ + { + "type": "text", + "id": "qFhix", + "fill": "#FFFFFF", + "textGrowth": "fixed-width", + "width": 150, + "content": "chart.png", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "700" + }, + { + "type": "text", + "id": "AWYxj", + "fill": "#E5E7EB", + "content": "246 KB", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "K2Brw", + "width": "fill_container", + "gap": 10, + "children": [ + { + "type": "frame", + "id": "y58yfa", + "width": "fill_container", + "height": 148, + "fill": "$surface-inverse", + "cornerRadius": 14, + "stroke": { + "thickness": 1, + "fill": "$border" + }, + "effect": { + "type": "shadow", + "shadowType": "outer", + "color": "#5C452014", + "offset": { + "x": 0, + "y": 3 + }, + "blur": 12 + }, + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "vnGyB", + "layoutPosition": "absolute", + "x": 10, + "y": 10, + "fill": "#7E57C2", + "cornerRadius": 6, + "padding": [ + 3, + 7 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "OKjBH", + "fill": "#FFFFFF", + "content": "YAML", + "fontFamily": "JetBrains Mono", + "fontSize": 9, + "fontWeight": "700" + } + ] + }, + { + "type": "frame", + "id": "D3r5Gs", + "layoutPosition": "absolute", + "x": 10, + "y": 36, + "clip": true, + "width": 154, + "height": 78, + "layout": "vertical", + "gap": 2, + "children": [ + { + "type": "frame", + "id": "lCSt8", + "height": 13, + "children": [ + { + "type": "text", + "id": "hBclR", + "fill": "#569CD6", + "content": "version", + "fontFamily": "JetBrains Mono", + "fontSize": 9, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "YE4Nn", + "fill": "#D4D4D4", + "content": ": ", + "fontFamily": "JetBrains Mono", + "fontSize": 9, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "wzhG0", + "fill": "#CE9178", + "content": "\"3.8\"", + "fontFamily": "JetBrains Mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "b5PJWA", + "width": "fill_container", + "height": 13 + }, + { + "type": "frame", + "id": "nRKYP", + "height": 13, + "children": [ + { + "type": "text", + "id": "Qd4Qg", + "fill": "#569CD6", + "content": "services", + "fontFamily": "JetBrains Mono", + "fontSize": 9, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "v3UVH4", + "fill": "#D4D4D4", + "content": ":", + "fontFamily": "JetBrains Mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "d83Sfr", + "height": 13, + "children": [ + { + "type": "text", + "id": "mwT54", + "fill": "#9CDCFE", + "content": " easynode", + "fontFamily": "JetBrains Mono", + "fontSize": 9, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "jEduk", + "fill": "#D4D4D4", + "content": ":", + "fontFamily": "JetBrains Mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "F1uSMW", + "height": 13, + "children": [ + { + "type": "text", + "id": "AXKMX", + "fill": "#569CD6", + "content": " image", + "fontFamily": "JetBrains Mono", + "fontSize": 9, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "HzFn5", + "fill": "#CE9178", + "content": ": chaoszhu/easynode", + "fontFamily": "JetBrains Mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "ct5fb", + "height": 13, + "children": [ + { + "type": "text", + "id": "lGcd1", + "fill": "#569CD6", + "content": " ports", + "fontFamily": "JetBrains Mono", + "fontSize": 9, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "vgD6b", + "fill": "#D4D4D4", + "content": ":", + "fontFamily": "JetBrains Mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "rectangle", + "cornerRadius": [ + 0, + 0, + 14, + 14 + ], + "id": "Y9XSTk", + "layoutPosition": "absolute", + "x": 0, + "y": 116, + "fill": { + "type": "gradient", + "gradientType": "linear", + "enabled": true, + "rotation": 180, + "size": { + "height": 1 + }, + "colors": [ + { + "color": "#0A0F1400", + "position": 0 + }, + { + "color": "#0A0F14EE", + "position": 0.6 + } + ] + }, + "width": 200, + "height": 52 + }, + { + "type": "frame", + "id": "l8WXm", + "layoutPosition": "absolute", + "x": 0, + "y": 124, + "width": 174, + "height": 40, + "layout": "vertical", + "gap": 2, + "padding": [ + 0, + 12 + ], + "justifyContent": "center", + "children": [ + { + "type": "text", + "id": "xZYke", + "fill": "#FFFFFF", + "textGrowth": "fixed-width", + "width": 150, + "content": "docker-compose.yml", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "700" + }, + { + "type": "text", + "id": "SMCcS", + "fill": "#9CA3AF", + "content": "2.3 KB", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "YeOv5", + "width": "fill_container", + "height": 148, + "fill": "$surface-inverse", + "cornerRadius": 14, + "stroke": { + "thickness": 1, + "fill": "$border" + }, + "effect": { + "type": "shadow", + "shadowType": "outer", + "color": "#5C452014", + "offset": { + "x": 0, + "y": 3 + }, + "blur": 12 + }, + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "c1FEEy", + "layoutPosition": "absolute", + "x": 10, + "y": 10, + "fill": "#16A34A", + "cornerRadius": 6, + "padding": [ + 3, + 7 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "znWfy", + "fill": "#FFFFFF", + "content": "NGINX", + "fontFamily": "JetBrains Mono", + "fontSize": 9, + "fontWeight": "700" + } + ] + }, + { + "type": "frame", + "id": "PUINb", + "layoutPosition": "absolute", + "x": 10, + "y": 36, + "clip": true, + "width": 154, + "height": 78, + "layout": "vertical", + "gap": 2, + "children": [ + { + "type": "frame", + "id": "BsbPh", + "height": 13, + "children": [ + { + "type": "text", + "id": "SDxen", + "fill": "#569CD6", + "content": "worker_processes", + "fontFamily": "JetBrains Mono", + "fontSize": 9, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "t6uLwZ", + "fill": "#B5CEA8", + "content": " 1", + "fontFamily": "JetBrains Mono", + "fontSize": 9, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "df9D1", + "fill": "#D4D4D4", + "content": ";", + "fontFamily": "JetBrains Mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "xKtEM", + "height": 13, + "children": [ + { + "type": "text", + "id": "NnuES", + "fill": "#569CD6", + "content": "events", + "fontFamily": "JetBrains Mono", + "fontSize": 9, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "WwWs0", + "fill": "#D4D4D4", + "content": " {", + "fontFamily": "JetBrains Mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "Ol2QT", + "height": 13, + "children": [ + { + "type": "text", + "id": "cPJmd", + "fill": "#569CD6", + "content": " worker_connections", + "fontFamily": "JetBrains Mono", + "fontSize": 9, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "HJ4Pi", + "fill": "#B5CEA8", + "content": " 1024", + "fontFamily": "JetBrains Mono", + "fontSize": 9, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "SEfc5", + "fill": "#D4D4D4", + "content": ";", + "fontFamily": "JetBrains Mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "HWiHU", + "height": 13, + "children": [ + { + "type": "text", + "id": "b3rNu", + "fill": "#D4D4D4", + "content": "}", + "fontFamily": "JetBrains Mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "FbDuc", + "height": 13, + "children": [ + { + "type": "text", + "id": "g6fs5G", + "fill": "#569CD6", + "content": "http", + "fontFamily": "JetBrains Mono", + "fontSize": 9, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "iyilE", + "fill": "#D4D4D4", + "content": " {", + "fontFamily": "JetBrains Mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "g0oocl", + "height": 13, + "children": [ + { + "type": "text", + "id": "MwzCy", + "fill": "#569CD6", + "content": " server", + "fontFamily": "JetBrains Mono", + "fontSize": 9, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "n1g0k", + "fill": "#D4D4D4", + "content": " {", + "fontFamily": "JetBrains Mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "rectangle", + "cornerRadius": [ + 0, + 0, + 14, + 14 + ], + "id": "LHHA9", + "layoutPosition": "absolute", + "x": 0, + "y": 116, + "fill": { + "type": "gradient", + "gradientType": "linear", + "enabled": true, + "rotation": 180, + "size": { + "height": 1 + }, + "colors": [ + { + "color": "#0A0F1400", + "position": 0 + }, + { + "color": "#0A0F14EE", + "position": 0.6 + } + ] + }, + "width": 200, + "height": 52 + }, + { + "type": "frame", + "id": "Dz2XX", + "layoutPosition": "absolute", + "x": 0, + "y": 124, + "width": 174, + "height": 40, + "layout": "vertical", + "gap": 2, + "padding": [ + 0, + 12 + ], + "justifyContent": "center", + "children": [ + { + "type": "text", + "id": "RcGdo", + "fill": "#FFFFFF", + "textGrowth": "fixed-width", + "width": 150, + "content": "nginx.conf", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "700" + }, + { + "type": "text", + "id": "iHS0Z", + "fill": "#9CA3AF", + "content": "1.8 KB", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "MOCoa", + "width": "fill_container", + "padding": [ + 12, + 21, + 21, + 21 + ], + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "O5gtO", + "width": "fill_container", + "height": 62, + "fill": "$surface-card", + "cornerRadius": 36, + "stroke": { + "thickness": 1, + "fill": "$border" + }, + "padding": 4, + "children": [ + { + "type": "frame", + "id": "AIvMH", + "width": "fill_container", + "height": "fill_container", + "fill": "#00000000", + "cornerRadius": 26, + "layout": "vertical", + "gap": 4, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "nx7rN", + "width": 18, + "height": 18, + "iconFontName": "monitor", + "iconFontFamily": "lucide", + "fill": "$font-muted" + }, + { + "type": "text", + "id": "rsaYj", + "fill": "$font-muted", + "content": "服务器", + "fontFamily": "Inter", + "fontSize": 10, + "fontWeight": "600", + "letterSpacing": 0.5 + } + ] + }, + { + "type": "frame", + "id": "nCr1z", + "width": "fill_container", + "height": "fill_container", + "fill": "$accent", + "cornerRadius": 26, + "layout": "vertical", + "gap": 4, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "q7ccx", + "width": 18, + "height": 18, + "iconFontName": "folder", + "iconFontFamily": "lucide", + "fill": "#FFFFFF" + }, + { + "type": "text", + "id": "rpKWk", + "fill": "#FFFFFF", + "content": "文件", + "fontFamily": "Inter", + "fontSize": 10, + "fontWeight": "600", + "letterSpacing": 0.5 + } + ] + }, + { + "type": "frame", + "id": "E537g7", + "width": "fill_container", + "height": "fill_container", + "fill": "#00000000", + "cornerRadius": 26, + "layout": "vertical", + "gap": 4, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "HSIxr", + "width": 18, + "height": 18, + "iconFontName": "scroll-text", + "iconFontFamily": "lucide", + "fill": "$font-muted" + }, + { + "type": "text", + "id": "jw4W4", + "fill": "$font-muted", + "content": "脚本", + "fontFamily": "Inter", + "fontSize": 10, + "fontWeight": "600", + "letterSpacing": 0.5 + } + ] + }, + { + "type": "frame", + "id": "ALcyP", + "width": "fill_container", + "height": "fill_container", + "fill": "#00000000", + "cornerRadius": 26, + "layout": "vertical", + "gap": 4, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "bDrSq", + "width": 18, + "height": 18, + "iconFontName": "settings", + "iconFontFamily": "lucide", + "fill": "$font-muted" + }, + { + "type": "text", + "id": "N6a0W", + "fill": "$font-muted", + "content": "设置", + "fontFamily": "Inter", + "fontSize": 10, + "fontWeight": "600", + "letterSpacing": 0.5 + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "p4F6QB", + "x": 7211, + "y": 964, + "name": "文件页(终端集成)", + "clip": true, + "width": 390, + "height": 1087, + "fill": "$surface", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "piqm8", + "width": "fill_container", + "height": 62, + "padding": [ + 0, + 20 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "T47zz", + "fill": "$font-primary", + "content": "9:41", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "k3tXLQ", + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "rr4FL", + "width": 16, + "height": 16, + "iconFontName": "signal", + "iconFontFamily": "lucide", + "fill": "$font-primary" + }, + { + "type": "icon_font", + "id": "c5ST9", + "width": 16, + "height": 16, + "iconFontName": "wifi", + "iconFontFamily": "lucide", + "fill": "$font-primary" + }, + { + "type": "icon_font", + "id": "kjlop", + "width": 22, + "height": 16, + "iconFontName": "battery-full", + "iconFontFamily": "lucide", + "fill": "$font-primary" + } + ] + } + ] + }, + { + "type": "frame", + "id": "U3jCdb", + "width": "fill_container", + "height": 56, + "padding": [ + 0, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "twdyR", + "gap": 10, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "e80MnJ", + "width": 32, + "height": 32, + "fill": "$accent", + "cornerRadius": 8, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "Yg82U", + "fill": "#FFFFFF", + "content": ">_", + "fontFamily": "JetBrains Mono", + "fontSize": 13, + "fontWeight": "700" + } + ] + }, + { + "type": "text", + "id": "bTG9J", + "fill": "$font-primary", + "content": "easynode@hk-web", + "fontFamily": "JetBrains Mono", + "fontSize": 14, + "fontWeight": "600" + } + ] + }, + { + "type": "frame", + "id": "bddWB", + "gap": 14, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "lRTrM", + "width": 20, + "height": 20, + "iconFontName": "columns-2", + "iconFontFamily": "lucide", + "fill": "$font-secondary" + }, + { + "type": "icon_font", + "id": "tC96F", + "width": 20, + "height": 20, + "iconFontName": "panel-bottom", + "iconFontFamily": "lucide", + "fill": "$font-secondary" + }, + { + "type": "icon_font", + "id": "kk4Nk", + "width": 20, + "height": 20, + "iconFontName": "ellipsis", + "iconFontFamily": "lucide", + "fill": "$font-secondary" + } + ] + } + ] + }, + { + "type": "frame", + "id": "p783J", + "width": "fill_container", + "height": 40, + "fill": "$surface-inverse", + "padding": [ + 0, + 14 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "VAl6b", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "Eak7z", + "fill": "$terminal-green", + "content": "PWD", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "700", + "letterSpacing": 0.5 + }, + { + "type": "text", + "id": "vYd5d", + "fill": "#E5E7EB", + "content": "/root/easynode", + "fontFamily": "JetBrains Mono", + "fontSize": 12, + "fontWeight": "500" + }, + { + "type": "text", + "id": "ph9pS", + "fill": "$accent", + "content": "█", + "fontFamily": "JetBrains Mono", + "fontSize": 12, + "fontWeight": "700" + } + ] + }, + { + "type": "frame", + "id": "iLBkD", + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "H15BH", + "width": 16, + "height": 16, + "iconFontName": "plus", + "iconFontFamily": "lucide", + "fill": "#FFFFFF" + } + ] + } + ] + }, + { + "type": "frame", + "id": "q58YPw", + "width": "fill_container", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "fyaqI", + "width": "fill_container", + "fill": "$surface-card-strong", + "padding": [ + 8, + 16 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "C7ttsl", + "fill": "$font-muted", + "content": "PERM SIZE MODIFIED NAME", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "600", + "letterSpacing": 0.5 + } + ] + }, + { + "type": "frame", + "id": "v3aXBu", + "width": "fill_container", + "height": 54, + "fill": "#00000000", + "stroke": { + "thickness": { + "bottom": 1 + }, + "fill": "$border" + }, + "gap": 10, + "padding": [ + 6, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "gvKR6", + "width": "fill_container", + "gap": 10, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "wDVeu", + "fill": "$font-muted", + "content": "drwx", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "600", + "letterSpacing": 0.5 + }, + { + "type": "icon_font", + "id": "fm3Lh", + "width": 18, + "height": 18, + "iconFontName": "folder", + "iconFontFamily": "lucide", + "fill": "$accent" + }, + { + "type": "frame", + "id": "X2Lgc4", + "width": "fill_container", + "layout": "vertical", + "gap": 2, + "children": [ + { + "type": "frame", + "id": "qMofH", + "gap": 4, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "q9xUWq", + "fill": "$font-primary", + "content": "easynode", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "600" + } + ] + }, + { + "type": "text", + "id": "duXib", + "fill": "$font-muted", + "content": "4.0K 2026-04-20 19:55", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "500", + "letterSpacing": 0.3 + } + ] + } + ] + }, + { + "type": "icon_font", + "id": "hbClS", + "width": 14, + "height": 14, + "iconFontName": "chevron-right", + "iconFontFamily": "lucide", + "fill": "$font-muted" + } + ] + }, + { + "type": "frame", + "id": "K4tPmB", + "width": "fill_container", + "height": 54, + "fill": "#00000000", + "stroke": { + "thickness": { + "bottom": 1 + }, + "fill": "$border" + }, + "gap": 10, + "padding": [ + 6, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "H7tTV", + "width": "fill_container", + "gap": 10, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "prwyr", + "fill": "$font-muted", + "content": "drwx", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "600", + "letterSpacing": 0.5 + }, + { + "type": "icon_font", + "id": "TC6NG", + "width": 18, + "height": 18, + "iconFontName": "folder", + "iconFontFamily": "lucide", + "fill": "$accent" + }, + { + "type": "frame", + "id": "MgJny", + "width": "fill_container", + "layout": "vertical", + "gap": 2, + "children": [ + { + "type": "frame", + "id": "eZExa", + "gap": 4, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "e2rk4", + "fill": "$font-primary", + "content": "caddy-tx446", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "600" + } + ] + }, + { + "type": "text", + "id": "t96Jp", + "fill": "$font-muted", + "content": "4.0K 2025-11-13 13:00", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "500", + "letterSpacing": 0.3 + } + ] + } + ] + }, + { + "type": "icon_font", + "id": "cLltz", + "width": 14, + "height": 14, + "iconFontName": "chevron-right", + "iconFontFamily": "lucide", + "fill": "$font-muted" + } + ] + }, + { + "type": "frame", + "id": "szVvL", + "width": "fill_container", + "height": 54, + "fill": "#00000000", + "stroke": { + "thickness": { + "bottom": 1 + }, + "fill": "$border" + }, + "gap": 10, + "padding": [ + 6, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "H5zKyg", + "width": "fill_container", + "gap": 10, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "TQVHm", + "fill": "$font-muted", + "content": "drwx", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "600", + "letterSpacing": 0.5 + }, + { + "type": "icon_font", + "id": "ackz6", + "width": 18, + "height": 18, + "iconFontName": "folder", + "iconFontFamily": "lucide", + "fill": "$accent" + }, + { + "type": "frame", + "id": "dHr8C", + "width": "fill_container", + "layout": "vertical", + "gap": 2, + "children": [ + { + "type": "frame", + "id": "cxgoI", + "gap": 4, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "wktCu", + "fill": "$font-primary", + "content": "logs", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "600" + } + ] + }, + { + "type": "text", + "id": "DtegR", + "fill": "$font-muted", + "content": "12K 2026-04-26 18:16", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "500", + "letterSpacing": 0.3 + } + ] + } + ] + }, + { + "type": "icon_font", + "id": "waHRC", + "width": 14, + "height": 14, + "iconFontName": "chevron-right", + "iconFontFamily": "lucide", + "fill": "$font-muted" + } + ] + }, + { + "type": "frame", + "id": "I5Mmb", + "width": "fill_container", + "height": 54, + "fill": "$accent-soft", + "stroke": { + "thickness": { + "bottom": 1 + }, + "fill": "$border" + }, + "gap": 10, + "padding": [ + 6, + 16, + 6, + 14 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "GDfM4", + "width": "fill_container", + "gap": 10, + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 1, + "id": "zJlv8", + "fill": "$accent", + "width": 2, + "height": 30 + }, + { + "type": "text", + "id": "fvyUs", + "fill": "$font-muted", + "content": "-rw-", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "600", + "letterSpacing": 0.5 + }, + { + "type": "icon_font", + "id": "rSueQ", + "width": 18, + "height": 18, + "iconFontName": "file-text", + "iconFontFamily": "lucide", + "fill": "$font-muted" + }, + { + "type": "frame", + "id": "PVB9C", + "width": "fill_container", + "layout": "vertical", + "gap": 2, + "children": [ + { + "type": "frame", + "id": "T26Xt", + "gap": 4, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "iUkYC", + "fill": "$accent", + "content": "> ", + "fontFamily": "JetBrains Mono", + "fontSize": 13, + "fontWeight": "700" + }, + { + "type": "text", + "id": "S42jp", + "fill": "$font-primary", + "content": "docker-compose.yml", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "600" + } + ] + }, + { + "type": "text", + "id": "i3cdI", + "fill": "$font-muted", + "content": "2.3K 2026-04-26 18:16", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "500", + "letterSpacing": 0.3 + } + ] + } + ] + }, + { + "type": "icon_font", + "id": "AU9CR", + "width": 14, + "height": 14, + "iconFontName": "chevron-right", + "iconFontFamily": "lucide", + "fill": "$font-muted" + } + ] + }, + { + "type": "frame", + "id": "f1qPf", + "width": "fill_container", + "height": 54, + "fill": "#00000000", + "stroke": { + "thickness": { + "bottom": 1 + }, + "fill": "$border" + }, + "gap": 10, + "padding": [ + 6, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "HneLk", + "width": "fill_container", + "gap": 10, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "jaej1", + "fill": "$font-muted", + "content": "-rw-", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "600", + "letterSpacing": 0.5 + }, + { + "type": "icon_font", + "id": "K3LME", + "width": 18, + "height": 18, + "iconFontName": "file-text", + "iconFontFamily": "lucide", + "fill": "$font-muted" + }, + { + "type": "frame", + "id": "OFKTp", + "width": "fill_container", + "layout": "vertical", + "gap": 2, + "children": [ + { + "type": "frame", + "id": "UFOtT", + "gap": 4, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "BaTSb", + "fill": "$font-primary", + "content": "nginx.conf", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "600" + } + ] + }, + { + "type": "text", + "id": "QideE", + "fill": "$font-muted", + "content": "1.8K 2026-04-26 18:00", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "500", + "letterSpacing": 0.3 + } + ] + } + ] + }, + { + "type": "icon_font", + "id": "p7COjJ", + "width": 14, + "height": 14, + "iconFontName": "chevron-right", + "iconFontFamily": "lucide", + "fill": "$font-muted" + } + ] + }, + { + "type": "frame", + "id": "tlBHj", + "width": "fill_container", + "height": 54, + "fill": "#00000000", + "stroke": { + "thickness": { + "bottom": 1 + }, + "fill": "$border" + }, + "gap": 10, + "padding": [ + 6, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "vaVvG", + "width": "fill_container", + "gap": 10, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "Vfd0Q", + "fill": "$font-muted", + "content": "-rwx", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "600", + "letterSpacing": 0.5 + }, + { + "type": "icon_font", + "id": "frZgY", + "width": 18, + "height": 18, + "iconFontName": "file-code", + "iconFontFamily": "lucide", + "fill": "$success" + }, + { + "type": "frame", + "id": "K7JoF3", + "width": "fill_container", + "layout": "vertical", + "gap": 2, + "children": [ + { + "type": "frame", + "id": "Xbcdx", + "gap": 4, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "LyVWv", + "fill": "$font-primary", + "content": "deploy.sh", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "600" + } + ] + }, + { + "type": "text", + "id": "kneSG", + "fill": "$font-muted", + "content": "1.2K 2026-04-20 14:32", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "500", + "letterSpacing": 0.3 + } + ] + } + ] + }, + { + "type": "icon_font", + "id": "rhSfI", + "width": 14, + "height": 14, + "iconFontName": "chevron-right", + "iconFontFamily": "lucide", + "fill": "$font-muted" + } + ] + }, + { + "type": "frame", + "id": "P98WnO", + "width": "fill_container", + "height": 54, + "fill": "#00000000", + "stroke": { + "thickness": { + "bottom": 1 + }, + "fill": "$border" + }, + "gap": 10, + "padding": [ + 6, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "IUmEX", + "width": "fill_container", + "gap": 10, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "zNsIp", + "fill": "$font-muted", + "content": "-rw-", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "600", + "letterSpacing": 0.5 + }, + { + "type": "icon_font", + "id": "J3m3T", + "width": 18, + "height": 18, + "iconFontName": "file-text", + "iconFontFamily": "lucide", + "fill": "$font-muted" + }, + { + "type": "frame", + "id": "qOMBo", + "width": "fill_container", + "layout": "vertical", + "gap": 2, + "children": [ + { + "type": "frame", + "id": "vjUJf", + "gap": 4, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "gOBYK", + "fill": "$font-primary", + "content": "README.md", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "600" + } + ] + }, + { + "type": "text", + "id": "j04Au1", + "fill": "$font-muted", + "content": "5.6K 2025-11-23 10:14", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "500", + "letterSpacing": 0.3 + } + ] + } + ] + }, + { + "type": "icon_font", + "id": "tMX6r", + "width": 14, + "height": 14, + "iconFontName": "chevron-right", + "iconFontFamily": "lucide", + "fill": "$font-muted" + } + ] + }, + { + "type": "frame", + "id": "AhBFV", + "width": "fill_container", + "height": 54, + "fill": "#00000000", + "stroke": { + "thickness": { + "bottom": 1 + }, + "fill": "$border" + }, + "gap": 10, + "padding": [ + 6, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "P2jQR", + "width": "fill_container", + "gap": 10, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "Pww6o", + "fill": "$font-muted", + "content": "-rw-", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "600", + "letterSpacing": 0.5 + }, + { + "type": "icon_font", + "id": "U43O8", + "width": 18, + "height": 18, + "iconFontName": "file", + "iconFontFamily": "lucide", + "fill": "$font-muted" + }, + { + "type": "frame", + "id": "w01PGt", + "width": "fill_container", + "layout": "vertical", + "gap": 2, + "children": [ + { + "type": "frame", + "id": "LaQNy", + "gap": 4, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "PFgvm", + "fill": "$font-primary", + "content": ".env", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "600" + } + ] + }, + { + "type": "text", + "id": "avWd1", + "fill": "$font-muted", + "content": "240B 2026-04-26 18:00", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "500", + "letterSpacing": 0.3 + } + ] + } + ] + }, + { + "type": "icon_font", + "id": "hsOIo", + "width": 14, + "height": 14, + "iconFontName": "chevron-right", + "iconFontFamily": "lucide", + "fill": "$font-muted" + } + ] + } + ] + }, + { + "type": "frame", + "id": "ZFO8e", + "width": "fill_container", + "height": "fill_container", + "layout": "vertical" + }, + { + "type": "frame", + "id": "iL0yj", + "width": "fill_container", + "fill": "$surface-inverse", + "layout": "vertical", + "gap": 4, + "padding": [ + 10, + 14 + ], + "children": [ + { + "type": "frame", + "id": "GvliU", + "width": "fill_container", + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "M0ZBs", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "PsYW4", + "fill": "#9CA3AF", + "content": "TERMINAL", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "700", + "letterSpacing": 0.6 + }, + { + "type": "text", + "id": "D7XAw", + "fill": "#6B7280", + "content": "·", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "500" + }, + { + "type": "text", + "id": "k4Z57", + "fill": "#9CA3AF", + "content": "zsh", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "500" + }, + { + "type": "text", + "id": "T6nzfU", + "fill": "#6B7280", + "content": "·", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "500" + }, + { + "type": "text", + "id": "P5TA03", + "fill": "$warning", + "content": "5事件未读", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "HH4nT", + "gap": 10, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "Chgyt", + "width": 14, + "height": 14, + "iconFontName": "chevron-up", + "iconFontFamily": "lucide", + "fill": "#9CA3AF" + }, + { + "type": "icon_font", + "id": "CPfer", + "width": 14, + "height": 14, + "iconFontName": "x", + "iconFontFamily": "lucide", + "fill": "#9CA3AF" + } + ] + } + ] + }, + { + "type": "text", + "id": "p0AV5", + "fill": "$terminal-green", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "$ ls -lah | grep yml", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "text", + "id": "qCeEG", + "fill": "#D4D4D4", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "-rw-r--r-- 1 root root 2.3K Apr 26 18:16 docker-compose.yml", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "500" + }, + { + "type": "frame", + "id": "FOLzq", + "width": "fill_container", + "gap": 4, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "H7euJ", + "fill": "$terminal-green", + "content": "$", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "700" + }, + { + "type": "text", + "id": "t8A11A", + "fill": "$accent", + "content": "█", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "700" + } + ] + } + ] + }, + { + "type": "frame", + "id": "I3RBJ", + "width": "fill_container", + "padding": [ + 12, + 21, + 21, + 21 + ], + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "TJlWu", + "width": "fill_container", + "height": 62, + "fill": "$surface-card", + "cornerRadius": 36, + "stroke": { + "thickness": 1, + "fill": "$border" + }, + "padding": 4, + "children": [ + { + "type": "frame", + "id": "VJRIC", + "width": "fill_container", + "height": "fill_container", + "fill": "#00000000", + "cornerRadius": 26, + "layout": "vertical", + "gap": 4, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "V4CTN", + "width": 18, + "height": 18, + "iconFontName": "monitor", + "iconFontFamily": "lucide", + "fill": "$font-muted" + }, + { + "type": "text", + "id": "qQLOp", + "fill": "$font-muted", + "content": "服务器", + "fontFamily": "Inter", + "fontSize": 10, + "fontWeight": "600", + "letterSpacing": 0.5 + } + ] + }, + { + "type": "frame", + "id": "j8o4D", + "width": "fill_container", + "height": "fill_container", + "fill": "$accent", + "cornerRadius": 26, + "layout": "vertical", + "gap": 4, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "l3LO5", + "width": 18, + "height": 18, + "iconFontName": "folder", + "iconFontFamily": "lucide", + "fill": "#FFFFFF" + }, + { + "type": "text", + "id": "OBm4Z", + "fill": "#FFFFFF", + "content": "文件", + "fontFamily": "Inter", + "fontSize": 10, + "fontWeight": "600", + "letterSpacing": 0.5 + } + ] + }, + { + "type": "frame", + "id": "TCRMv", + "width": "fill_container", + "height": "fill_container", + "fill": "#00000000", + "cornerRadius": 26, + "layout": "vertical", + "gap": 4, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "VL1pI", + "width": 18, + "height": 18, + "iconFontName": "scroll-text", + "iconFontFamily": "lucide", + "fill": "$font-muted" + }, + { + "type": "text", + "id": "KgIjH", + "fill": "$font-muted", + "content": "脚本", + "fontFamily": "Inter", + "fontSize": 10, + "fontWeight": "600", + "letterSpacing": 0.5 + } + ] + }, + { + "type": "frame", + "id": "JkQez", + "width": "fill_container", + "height": "fill_container", + "fill": "#00000000", + "cornerRadius": 26, + "layout": "vertical", + "gap": 4, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "qRb4u", + "width": 18, + "height": 18, + "iconFontName": "settings", + "iconFontFamily": "lucide", + "fill": "$font-muted" + }, + { + "type": "text", + "id": "JJFYe", + "fill": "$font-muted", + "content": "设置", + "fontFamily": "Inter", + "fontSize": 10, + "fontWeight": "600", + "letterSpacing": 0.5 + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "T8BgCO", + "layoutPosition": "absolute", + "x": 155, + "y": 430, + "width": 220, + "fill": "$surface-inverse", + "cornerRadius": 12, + "stroke": { + "thickness": 1, + "fill": "#1F2937" + }, + "effect": { + "type": "shadow", + "shadowType": "outer", + "color": "#00000066", + "offset": { + "x": 0, + "y": 8 + }, + "blur": 24 + }, + "layout": "vertical", + "padding": 8, + "children": [ + { + "type": "frame", + "id": "b6By45", + "width": "fill_container", + "height": 24, + "padding": [ + 0, + 4 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "F8ikw4", + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "tkeWF", + "fill": "$terminal-green", + "content": "·", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "700" + }, + { + "type": "text", + "id": "gyZGI", + "fill": "#9CA3AF", + "content": "docker-compose.yml", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "600" + } + ] + }, + { + "type": "frame", + "id": "N4uyj", + "height": 16, + "fill": "#FFFFFF14", + "cornerRadius": 4, + "stroke": { + "thickness": 1, + "fill": "#FFFFFF26" + }, + "padding": [ + 0, + 6 + ], + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "Qn4Ru", + "fill": "#9CA3AF", + "content": "esc", + "fontFamily": "JetBrains Mono", + "fontSize": 9, + "fontWeight": "600" + } + ] + } + ] + }, + { + "type": "rectangle", + "id": "g73JfL", + "fill": "#1F2937", + "width": "fill_container", + "height": 1 + }, + { + "type": "frame", + "id": "GkKoX", + "width": "fill_container", + "height": 6, + "layout": "vertical" + }, + { + "type": "frame", + "id": "yjPA8", + "width": "fill_container", + "height": 28, + "fill": "$accent-soft", + "cornerRadius": 6, + "gap": 8, + "padding": [ + 0, + 6 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "yIxP4", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "n5lJ8O", + "width": 16, + "height": 16, + "fill": "#FFFFFF", + "cornerRadius": 4, + "stroke": { + "thickness": 1, + "fill": "$accent" + }, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "SwGM5", + "fill": "$primary", + "content": "o", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "700" + } + ] + }, + { + "type": "text", + "id": "L1HKR", + "fill": "$primary", + "content": "open", + "fontFamily": "JetBrains Mono", + "fontSize": 12, + "fontWeight": "600" + } + ] + }, + { + "type": "text", + "id": "f8Jr5U", + "fill": "$primary", + "content": "打开编辑", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "700" + } + ] + }, + { + "type": "frame", + "id": "ftwIf", + "width": "fill_container", + "height": 28, + "fill": "#00000000", + "cornerRadius": 6, + "gap": 8, + "padding": [ + 0, + 6 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "ZaxgF", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "CgN41", + "width": 16, + "height": 16, + "fill": "#FFFFFF1A", + "cornerRadius": 4, + "stroke": { + "thickness": 1, + "fill": "#FFFFFF26" + }, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "KxHov", + "fill": "#FFFFFF", + "content": "c", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "700" + } + ] + }, + { + "type": "text", + "id": "l1zrOR", + "fill": "#E5E7EB", + "content": "cat", + "fontFamily": "JetBrains Mono", + "fontSize": 12, + "fontWeight": "600" + } + ] + }, + { + "type": "text", + "id": "nXPxx", + "fill": "#6B7280", + "content": "查看内容", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "zrJ5J", + "width": "fill_container", + "height": 28, + "fill": "#00000000", + "cornerRadius": 6, + "gap": 8, + "padding": [ + 0, + 6 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "YcDvT", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "C6VU2K", + "width": 16, + "height": 16, + "fill": "#FFFFFF1A", + "cornerRadius": 4, + "stroke": { + "thickness": 1, + "fill": "#FFFFFF26" + }, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "rwOtf", + "fill": "#FFFFFF", + "content": "v", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "700" + } + ] + }, + { + "type": "text", + "id": "WsZRd", + "fill": "#E5E7EB", + "content": "vim", + "fontFamily": "JetBrains Mono", + "fontSize": 12, + "fontWeight": "600" + } + ] + }, + { + "type": "text", + "id": "yfEmN", + "fill": "#6B7280", + "content": "终端编辑", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "JiJnc", + "width": "fill_container", + "height": 28, + "fill": "#00000000", + "cornerRadius": 6, + "gap": 8, + "padding": [ + 0, + 6 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "JSyZk", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "e9jS9k", + "width": 16, + "height": 16, + "fill": "#FFFFFF1A", + "cornerRadius": 4, + "stroke": { + "thickness": 1, + "fill": "#FFFFFF26" + }, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "hz08t", + "fill": "#FFFFFF", + "content": "m", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "700" + } + ] + }, + { + "type": "text", + "id": "QMGGH", + "fill": "#E5E7EB", + "content": "mv", + "fontFamily": "JetBrains Mono", + "fontSize": 12, + "fontWeight": "600" + } + ] + }, + { + "type": "text", + "id": "BzuWM", + "fill": "#6B7280", + "content": "重命名/移动", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "zgxdM", + "width": "fill_container", + "height": 28, + "fill": "#00000000", + "cornerRadius": 6, + "gap": 8, + "padding": [ + 0, + 6 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "C1NDWO", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "Z75rJK", + "width": 16, + "height": 16, + "fill": "#FFFFFF1A", + "cornerRadius": 4, + "stroke": { + "thickness": 1, + "fill": "#FFFFFF26" + }, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "xJZyS", + "fill": "#FFFFFF", + "content": "t", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "700" + } + ] + }, + { + "type": "text", + "id": "GZU0e", + "fill": "#E5E7EB", + "content": "tar -czf", + "fontFamily": "JetBrains Mono", + "fontSize": 12, + "fontWeight": "600" + } + ] + }, + { + "type": "text", + "id": "Si2eS", + "fill": "#6B7280", + "content": "压缩", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "qCCI9", + "width": "fill_container", + "height": 28, + "fill": "#00000000", + "cornerRadius": 6, + "gap": 8, + "padding": [ + 0, + 6 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "ujbX7", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "e1wZEp", + "width": 16, + "height": 16, + "fill": "#FFFFFF1A", + "cornerRadius": 4, + "stroke": { + "thickness": 1, + "fill": "#FFFFFF26" + }, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "v7PZ1C", + "fill": "#FFFFFF", + "content": "x", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "700" + } + ] + }, + { + "type": "text", + "id": "iVJKv", + "fill": "$danger", + "content": "rm", + "fontFamily": "JetBrains Mono", + "fontSize": 12, + "fontWeight": "600" + } + ] + }, + { + "type": "text", + "id": "bKChu", + "fill": "#6B7280", + "content": "删除", + "fontFamily": "JetBrains Mono", + "fontSize": 10, + "fontWeight": "500" + } + ] + } + ] + } + ] } ], "variables": { diff --git a/mobile/lib/features/servers/server_form_data.dart b/mobile/lib/features/servers/server_form_data.dart index 18897a2..c0a8ca0 100644 --- a/mobile/lib/features/servers/server_form_data.dart +++ b/mobile/lib/features/servers/server_form_data.dart @@ -9,7 +9,7 @@ class ServerFormData { this.host = '', this.port = 22, this.username = 'root', - this.authType = 'privateKey', + this.authType = 'password', this.password = '', this.privateKey = '', this.credential = '', diff --git a/mobile/lib/features/servers/server_form_page.dart b/mobile/lib/features/servers/server_form_page.dart index 4b292ca..2c776c1 100644 --- a/mobile/lib/features/servers/server_form_page.dart +++ b/mobile/lib/features/servers/server_form_page.dart @@ -197,19 +197,30 @@ class _ServerFormPageState extends ConsumerState { value: _form.authType, onChanged: (value) => setState(() => _form.authType = value), ), - AnimatedSize( - duration: const Duration(milliseconds: 180), - curve: Curves.easeOutCubic, - alignment: Alignment.topCenter, - child: _AuthField( - key: ValueKey(_form.authType), - form: _form, - passwordCtrl: _passwordCtrl, - privateKeyCtrl: _privateKeyCtrl, - credentials: credentials, - onCredentialChanged: (value) => setState(() { - _form.credential = value ?? ''; - }), + FormField( + key: ValueKey('auth-${_form.authType}'), + initialValue: _form.credential, + validator: _form.authType == 'credential' + ? (value) => value == null || value.isEmpty + ? l.tr('servers.validation.credential') + : null + : null, + builder: (field) => AnimatedSize( + duration: const Duration(milliseconds: 180), + curve: Curves.easeOutCubic, + alignment: Alignment.topCenter, + child: _AuthField( + key: ValueKey(_form.authType), + form: _form, + passwordCtrl: _passwordCtrl, + privateKeyCtrl: _privateKeyCtrl, + credentials: credentials, + errorText: field.errorText, + onCredentialChanged: (value) => setState(() { + _form.credential = value ?? ''; + field.didChange(_form.credential); + }), + ), ), ), ] else @@ -237,27 +248,48 @@ class _ServerFormPageState extends ConsumerState { children: _advancedOpen ? [ if (_form.isSsh) ...[ - _ProxyTypeSegment( - value: _form.proxyType, - onChanged: _changeProxyType, - ), - AnimatedSize( - duration: const Duration(milliseconds: 180), - curve: Curves.easeOutCubic, - alignment: Alignment.topCenter, - child: _ProxyField( - key: ValueKey(_form.proxyType), - proxyType: _form.proxyType, - proxies: proxies, - proxyValue: _normalizeProxyValue(proxies), - jumpHostOptions: jumpHostOptions, - jumpHostIds: _form.jumpHosts, - onProxyChanged: (value) => setState(() { - _form.proxyServer = value ?? ''; - }), - onJumpHostsChanged: (ids) => setState(() { - _form.jumpHosts = ids; - }), + FormField( + key: ValueKey('proxy-${_form.proxyType}'), + initialValue: _form.proxyType == 'jumpHosts' + ? _form.jumpHosts.join(',') + : _form.proxyServer, + validator: _form.proxyType.isEmpty + ? null + : (value) => value == null || value.isEmpty + ? l.tr(_form.proxyType == 'jumpHosts' + ? 'servers.validation.jumpHosts' + : 'servers.validation.proxyServer') + : null, + builder: (field) => Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _ProxyTypeSegment( + value: _form.proxyType, + onChanged: _changeProxyType, + ), + AnimatedSize( + duration: const Duration(milliseconds: 180), + curve: Curves.easeOutCubic, + alignment: Alignment.topCenter, + child: _ProxyField( + key: ValueKey(_form.proxyType), + proxyType: _form.proxyType, + proxies: proxies, + proxyValue: _normalizeProxyValue(proxies), + jumpHostOptions: jumpHostOptions, + jumpHostIds: _form.jumpHosts, + errorText: field.errorText, + onProxyChanged: (value) => setState(() { + _form.proxyServer = value ?? ''; + field.didChange(_form.proxyServer); + }), + onJumpHostsChanged: (ids) => setState(() { + _form.jumpHosts = ids; + field.didChange(_form.jumpHosts.join(',')); + }), + ), + ), + ], ), ), ], @@ -314,7 +346,7 @@ class _ServerFormPageState extends ConsumerState { } else { _form.port = 22; _form.username = 'root'; - _form.authType = 'privateKey'; + _form.authType = 'password'; } _portCtrl.text = _form.port.toString(); _usernameCtrl.text = _form.username; @@ -766,6 +798,7 @@ class _AuthField extends StatelessWidget { required this.passwordCtrl, required this.privateKeyCtrl, required this.credentials, + required this.errorText, required this.onCredentialChanged, }); @@ -773,6 +806,7 @@ class _AuthField extends StatelessWidget { final TextEditingController passwordCtrl; final TextEditingController privateKeyCtrl; final List credentials; + final String? errorText; final ValueChanged onCredentialChanged; @override @@ -819,6 +853,7 @@ class _AuthField extends StatelessWidget { : l.tr('servers.auth.password'), placeholder: l.tr('servers.credentials.empty'), enabled: credentials.isNotEmpty, + errorText: errorText, onTap: () async { final result = await _showChoiceSheet( context: context, @@ -864,11 +899,13 @@ class _ProxySelector extends StatelessWidget { const _ProxySelector({ required this.proxies, required this.value, + required this.errorText, required this.onChanged, }); final List proxies; final String? value; + final String? errorText; final ValueChanged onChanged; @override @@ -888,6 +925,7 @@ class _ProxySelector extends StatelessWidget { meta: selected?.typeLabel, placeholder: l.tr('servers.proxy.empty'), enabled: proxies.isNotEmpty, + errorText: errorText, onTap: () async { final result = await _showChoiceSheet( context: context, @@ -982,6 +1020,7 @@ class _ProxyField extends StatelessWidget { required this.proxyValue, required this.jumpHostOptions, required this.jumpHostIds, + required this.errorText, required this.onProxyChanged, required this.onJumpHostsChanged, }); @@ -991,6 +1030,7 @@ class _ProxyField extends StatelessWidget { final String? proxyValue; final List jumpHostOptions; final List jumpHostIds; + final String? errorText; final ValueChanged onProxyChanged; final ValueChanged> onJumpHostsChanged; @@ -1000,6 +1040,7 @@ class _ProxyField extends StatelessWidget { return _ProxySelector( proxies: proxies, value: proxyValue, + errorText: errorText, onChanged: onProxyChanged, ); } @@ -1007,6 +1048,7 @@ class _ProxyField extends StatelessWidget { return _JumpHostSelector( options: jumpHostOptions, selectedIds: jumpHostIds, + errorText: errorText, onChanged: onJumpHostsChanged, ); } @@ -1378,11 +1420,13 @@ class _JumpHostSelector extends StatelessWidget { const _JumpHostSelector({ required this.options, required this.selectedIds, + required this.errorText, required this.onChanged, }); final List options; final List selectedIds; + final String? errorText; final ValueChanged> onChanged; @override @@ -1407,7 +1451,7 @@ class _JumpHostSelector extends StatelessWidget { helperText: options.isEmpty ? l.tr('servers.jumpHosts.empty') : l.tr('servers.jumpHosts.orderHint'), - ), + ).copyWith(errorText: errorText), child: selectedHosts.isEmpty ? Text( l.tr('servers.jumpHosts.placeholder'), diff --git a/mobile/lib/features/terminal/http_proxy_connector.dart b/mobile/lib/features/terminal/http_proxy_connector.dart new file mode 100644 index 0000000..912404e --- /dev/null +++ b/mobile/lib/features/terminal/http_proxy_connector.dart @@ -0,0 +1,131 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:dartssh2/dartssh2.dart'; + +import 'socks5_connector.dart'; +import 'ssh_transport.dart'; + +class HttpProxyConnector { + Future connect({ + required String proxyHost, + required int proxyPort, + required String targetHost, + required int targetPort, + String username = '', + String password = '', + }) async { + final socket = await Socket.connect(proxyHost, proxyPort); + final reader = _HttpProxyReader(socket); + try { + socket.add( + utf8.encode( + _connectRequest( + targetHost: targetHost, + targetPort: targetPort, + username: username, + password: password, + ), + ), + ); + final response = await reader.readHeaders(); + final statusLine = response.isEmpty ? '' : response.first; + if (!statusLine.contains(' 200 ')) { + throw const SshTransportException( + 'HTTP proxy target connection failed', + ); + } + return SocketSshSocket(socket, reader.release()); + } catch (_) { + socket.destroy(); + rethrow; + } + } + + String _connectRequest({ + required String targetHost, + required int targetPort, + required String username, + required String password, + }) { + final target = '$targetHost:$targetPort'; + final headers = [ + 'CONNECT $target HTTP/1.1', + 'Host: $target', + 'Proxy-Connection: Keep-Alive', + ]; + if (username.isNotEmpty || password.isNotEmpty) { + final token = base64Encode(utf8.encode('$username:$password')); + headers.add('Proxy-Authorization: Basic $token'); + } + return '${headers.join('\r\n')}\r\n\r\n'; + } +} + +class _HttpProxyReader { + _HttpProxyReader(Socket socket) { + _subscription = socket.listen( + (data) { + if (_released) { + _streamController.add(Uint8List.fromList(data)); + } else { + _buffer.addAll(data); + } + }, + onDone: () { + _completer.complete(); + unawaited(_streamController.close()); + }, + onError: (Object error, StackTrace stackTrace) { + _completer.completeError(error, stackTrace); + _streamController.addError(error, stackTrace); + }, + ); + } + + final _buffer = []; + final _completer = Completer(); + final _streamController = StreamController(); + late final StreamSubscription> _subscription; + bool _released = false; + + Future> readHeaders() async { + while (!_hasHeaderTerminator()) { + await Future.delayed(const Duration(milliseconds: 10)); + if (_completer.isCompleted && !_hasHeaderTerminator()) { + await _subscription.cancel(); + throw const SshTransportException('HTTP proxy connection failed'); + } + } + final end = _headerEndIndex(); + final headerBytes = _buffer.take(end).toList(); + _buffer.removeRange(0, end + 4); + return utf8.decode(headerBytes, allowMalformed: true).split('\r\n'); + } + + Stream release() { + if (_released) return _streamController.stream; + _released = true; + if (_buffer.isNotEmpty) { + _streamController.add(Uint8List.fromList(_buffer)); + _buffer.clear(); + } + return _streamController.stream; + } + + bool _hasHeaderTerminator() => _headerEndIndex() >= 0; + + int _headerEndIndex() { + for (var i = 0; i <= _buffer.length - 4; i++) { + if (_buffer[i] == 13 && + _buffer[i + 1] == 10 && + _buffer[i + 2] == 13 && + _buffer[i + 3] == 10) { + return i; + } + } + return -1; + } +} diff --git a/mobile/lib/features/terminal/socks5_connector.dart b/mobile/lib/features/terminal/socks5_connector.dart new file mode 100644 index 0000000..86a794b --- /dev/null +++ b/mobile/lib/features/terminal/socks5_connector.dart @@ -0,0 +1,159 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:dartssh2/dartssh2.dart'; + +import 'ssh_transport.dart'; + +class SocketSshSocket implements SSHSocket { + SocketSshSocket(this._socket, this._stream); + + final Socket _socket; + final Stream _stream; + + @override + Stream get stream => _stream; + + @override + StreamSink> get sink => _socket; + + @override + Future get done => _socket.done; + + @override + Future close() => _socket.close(); + + @override + void destroy() { + _socket.destroy(); + } +} + +class Socks5Connector { + Future connect({ + required String proxyHost, + required int proxyPort, + required String targetHost, + required int targetPort, + String username = '', + String password = '', + }) async { + final socket = await Socket.connect(proxyHost, proxyPort); + final reader = _SocketReader(socket); + try { + final wantsAuth = username.isNotEmpty || password.isNotEmpty; + socket.add(wantsAuth ? [0x05, 0x02, 0x00, 0x02] : [0x05, 0x01, 0x00]); + final method = await reader.readExactly(2); + if (method[0] != 0x05) { + throw const SshTransportException('SOCKS5 proxy connection failed'); + } + if (method[1] == 0xFF) { + throw const SshTransportException('SOCKS5 authentication failed'); + } + if (method[1] == 0x02) { + await _authenticate(socket, reader, username, password); + } + + socket.add(_connectRequest(targetHost, targetPort)); + final response = await reader.readExactly(5); + if (response[1] != 0x00) { + throw const SshTransportException('SOCKS5 target connection failed'); + } + final addressRemainderLength = switch (response[3]) { + 0x01 => 3, + 0x03 => response[4] + 2, + 0x04 => 15, + _ => throw const SshTransportException( + 'SOCKS5 target connection failed', + ), + }; + await reader.readExactly(addressRemainderLength); + return SocketSshSocket(socket, reader.release()); + } catch (_) { + socket.destroy(); + rethrow; + } + } + + Future _authenticate( + Socket socket, + _SocketReader reader, + String username, + String password, + ) async { + final user = utf8.encode(username); + final pass = utf8.encode(password); + socket.add([0x01, user.length, ...user, pass.length, ...pass]); + final response = await reader.readExactly(2); + if (response[1] != 0x00) { + throw const SshTransportException('SOCKS5 authentication failed'); + } + } + + List _connectRequest(String host, int port) { + final hostBytes = utf8.encode(host); + return [ + 0x05, + 0x01, + 0x00, + 0x03, + hostBytes.length, + ...hostBytes, + (port >> 8) & 0xFF, + port & 0xFF, + ]; + } +} + +class _SocketReader { + _SocketReader(Socket socket) { + _subscription = socket.listen( + (data) { + if (_released) { + _streamController.add(Uint8List.fromList(data)); + } else { + _buffer.addAll(data); + } + }, + onDone: () { + _completer.complete(); + unawaited(_streamController.close()); + }, + onError: (Object error, StackTrace stackTrace) { + _completer.completeError(error, stackTrace); + _streamController.addError(error, stackTrace); + }, + ); + } + + final _buffer = []; + final _completer = Completer(); + final _streamController = StreamController(); + late final StreamSubscription> _subscription; + bool _released = false; + + Future> readExactly(int length) async { + while (_buffer.length < length) { + await Future.delayed(const Duration(milliseconds: 10)); + if (_completer.isCompleted && _buffer.length < length) { + await _subscription.cancel(); + throw const SshTransportException('SOCKS5 proxy connection failed'); + } + } + final bytes = _buffer.take(length).toList(); + _buffer.removeRange(0, length); + return bytes; + } + + Stream release() { + if (_released) return _streamController.stream; + _released = true; + if (_buffer.isNotEmpty) { + _streamController.add(Uint8List.fromList(_buffer)); + _buffer.clear(); + } + return _streamController.stream; + } +} diff --git a/mobile/lib/features/terminal/ssh_connection_config.dart b/mobile/lib/features/terminal/ssh_connection_config.dart index ce55951..4599a84 100644 --- a/mobile/lib/features/terminal/ssh_connection_config.dart +++ b/mobile/lib/features/terminal/ssh_connection_config.dart @@ -1,7 +1,7 @@ /// Plaintext SSH connection parameters returned by `/mobile/ssh-connection` /// after AES-GCM decryption. Mirrors `toMobileSshPayload` on the server. -class SshConnectionConfig { - const SshConnectionConfig({ +class SshAuthConfig { + const SshAuthConfig({ required this.hostId, required this.name, required this.host, @@ -36,21 +36,12 @@ class SshConnectionConfig { return trimmed.isEmpty ? null : trimmed; } - factory SshConnectionConfig.fromJson(Map json) { - final portRaw = json['port']; - final int port; - if (portRaw is int) { - port = portRaw; - } else if (portRaw is num) { - port = portRaw.toInt(); - } else { - port = int.tryParse(portRaw?.toString() ?? '') ?? 22; - } - return SshConnectionConfig( - hostId: (json['hostId'] ?? '').toString(), + static SshAuthConfig fromJson(Map json) { + return SshAuthConfig( + hostId: (json['hostId'] ?? json['id'] ?? '').toString(), name: (json['name'] ?? '').toString(), host: (json['host'] ?? '').toString(), - port: port, + port: _parsePort(json['port']), username: (json['username'] ?? '').toString(), authType: (json['authType'] ?? '').toString(), password: (json['password'] ?? '').toString(), @@ -59,3 +50,118 @@ class SshConnectionConfig { ); } } + +class SshProxyConfig { + const SshProxyConfig({ + required this.id, + required this.name, + required this.type, + required this.host, + required this.port, + required this.username, + required this.password, + }); + + final String id; + final String name; + final String type; + final String host; + final int port; + final String username; + final String password; + + factory SshProxyConfig.fromJson(Map json) { + return SshProxyConfig( + id: (json['id'] ?? json['_id'] ?? '').toString(), + name: (json['name'] ?? '').toString(), + type: (json['type'] ?? '').toString(), + host: (json['host'] ?? '').toString(), + port: _parsePort(json['port']), + username: (json['username'] ?? '').toString(), + password: (json['password'] ?? '').toString(), + ); + } +} + +class SshJumpHostConfig extends SshAuthConfig { + const SshJumpHostConfig({ + required super.hostId, + required super.name, + required super.host, + required super.port, + required super.username, + required super.authType, + required super.password, + required super.privateKey, + required super.passphrase, + }); + + factory SshJumpHostConfig.fromJson(Map json) { + final auth = SshAuthConfig.fromJson(json); + return SshJumpHostConfig( + hostId: auth.hostId, + name: auth.name, + host: auth.host, + port: auth.port, + username: auth.username, + authType: auth.authType, + password: auth.password, + privateKey: auth.privateKey, + passphrase: auth.passphrase, + ); + } +} + +class SshConnectionConfig extends SshAuthConfig { + const SshConnectionConfig({ + required super.hostId, + required super.name, + required super.host, + required super.port, + required super.username, + required super.authType, + required super.password, + required super.privateKey, + required super.passphrase, + required this.proxyType, + required this.proxy, + required this.jumpHosts, + }); + + final String proxyType; + final SshProxyConfig? proxy; + final List jumpHosts; + + factory SshConnectionConfig.fromJson(Map json) { + final auth = SshAuthConfig.fromJson(json); + final proxyRaw = json['proxy']; + final jumpHostsRaw = json['jumpHosts']; + return SshConnectionConfig( + hostId: auth.hostId, + name: auth.name, + host: auth.host, + port: auth.port, + username: auth.username, + authType: auth.authType, + password: auth.password, + privateKey: auth.privateKey, + passphrase: auth.passphrase, + proxyType: (json['proxyType'] ?? '').toString(), + proxy: proxyRaw is Map + ? SshProxyConfig.fromJson(proxyRaw) + : null, + jumpHosts: jumpHostsRaw is List + ? jumpHostsRaw + .whereType>() + .map(SshJumpHostConfig.fromJson) + .toList(growable: false) + : const [], + ); + } +} + +int _parsePort(Object? value) { + if (value is int) return value; + if (value is num) return value.toInt(); + return int.tryParse(value?.toString() ?? '') ?? 22; +} diff --git a/mobile/lib/features/terminal/ssh_terminal_controller.dart b/mobile/lib/features/terminal/ssh_terminal_controller.dart index 5004f99..fb06cab 100644 --- a/mobile/lib/features/terminal/ssh_terminal_controller.dart +++ b/mobile/lib/features/terminal/ssh_terminal_controller.dart @@ -6,6 +6,7 @@ import 'package:flutter/foundation.dart'; import 'package:xterm/xterm.dart'; import 'ssh_connection_config.dart'; +import 'ssh_transport.dart'; /// Owns the dartssh2 client and bridges its stdio with an [xterm] [Terminal]. /// @@ -16,11 +17,16 @@ import 'ssh_connection_config.dart'; /// - Toolbar shortcuts feed [writeInput] which writes directly to the SSH /// session (not the local terminal buffer) so the remote sees the keys. class SshTerminalController { - SshTerminalController({required this.config, Terminal? terminal}) - : terminal = terminal ?? Terminal(); + SshTerminalController({ + required this.config, + Terminal? terminal, + SshTransportFactory? transportFactory, + }) : terminal = terminal ?? Terminal(), + _transportFactory = transportFactory ?? SshTransportFactory(); final SshConnectionConfig config; final Terminal terminal; + final SshTransportFactory _transportFactory; /// Whether the next single-letter input should be translated into Ctrl+letter. /// Lives on the controller (not the toolbar) so soft-keyboard input — which @@ -28,6 +34,7 @@ class SshTerminalController { final ValueNotifier ctrlPending = ValueNotifier(false); SSHClient? _client; + SshTransportHandle? _transport; SSHSession? _session; StreamSubscription? _stdoutSub; StreamSubscription? _stderrSub; @@ -35,20 +42,33 @@ class SshTerminalController { Future connect() async { if (_disposed) return; - final socket = await SSHSocket.connect(config.host, config.port); - // dartssh2 `SSHKeyPair.fromPem` already returns `List`, no extra - // wrapping list needed. - final identities = config.authType == 'privateKey' - ? SSHKeyPair.fromPem(config.privateKey, config.privateKeyPassphrase) - : null; - _client = SSHClient( - socket, - username: config.username, - onPasswordRequest: config.authType == 'password' - ? () => config.password - : null, - identities: identities, - ); + try { + final transport = await _transportFactory.open( + config, + logger: (message) => terminal.write('[Info] $message\r\n'), + ); + _transport = transport; + terminal.write('[Info] 准备连接目标终端: ${config.name} - ${config.host}\r\n'); + // dartssh2 `SSHKeyPair.fromPem` already returns `List`, no + // extra wrapping list needed. + final identities = config.authType == 'privateKey' + ? SSHKeyPair.fromPem(config.privateKey, config.privateKeyPassphrase) + : null; + _client = SSHClient( + transport.socket, + username: config.username, + onPasswordRequest: config.authType == 'password' + ? () => config.password + : null, + identities: identities, + ); + } on SshTransportException catch (error) { + terminal.write('[Error] ${error.message}\r\n'); + rethrow; + } catch (error) { + terminal.write('[Error] $error\r\n'); + rethrow; + } final session = await _client!.shell(); _session = session; _stdoutSub = session.stdout.listen((data) { @@ -105,8 +125,10 @@ class SshTerminalController { _stderrSub = null; _session?.close(); _client?.close(); + await _transport?.close(); _session = null; _client = null; + _transport = null; ctrlPending.value = false; } } diff --git a/mobile/lib/features/terminal/ssh_transport.dart b/mobile/lib/features/terminal/ssh_transport.dart new file mode 100644 index 0000000..54ef423 --- /dev/null +++ b/mobile/lib/features/terminal/ssh_transport.dart @@ -0,0 +1,217 @@ +import 'package:dartssh2/dartssh2.dart'; + +import 'http_proxy_connector.dart'; +import 'socks5_connector.dart'; +import 'ssh_connection_config.dart'; + +typedef SshSocketConnector = Future Function(String host, int port); + +abstract class SshClientHandle { + Future get authenticated; + Future forwardLocal(String host, int port); + void close(); +} + +typedef SshClientCreator = + SshClientHandle Function(SSHSocket socket, SshAuthConfig auth); +typedef SshTransportLogger = void Function(String message); + +class SshTransportException implements Exception { + const SshTransportException(this.message); + + final String message; + + @override + String toString() => message; +} + +class SshTransportHandle { + SshTransportHandle({ + required this.socket, + required List intermediateClients, + }) : _intermediateClients = intermediateClients; + + final SSHSocket socket; + final List _intermediateClients; + bool _closed = false; + + Future close() async { + if (_closed) return; + _closed = true; + for (final client in _intermediateClients.reversed) { + client.close(); + } + await socket.close(); + } +} + +class DartSshClientHandle implements SshClientHandle { + DartSshClientHandle(this.client); + + final SSHClient client; + + @override + Future get authenticated => client.authenticated; + + @override + Future forwardLocal(String host, int port) { + return client.forwardLocal(host, port); + } + + @override + void close() { + client.close(); + } +} + +SshClientHandle createDartSshClient(SSHSocket socket, SshAuthConfig auth) { + final identities = auth.authType == 'privateKey' + ? SSHKeyPair.fromPem(auth.privateKey, auth.privateKeyPassphrase) + : null; + final client = SSHClient( + socket, + username: auth.username, + onPasswordRequest: auth.authType == 'password' ? () => auth.password : null, + identities: identities, + ); + return DartSshClientHandle(client); +} + +class SshTransportFactory { + SshTransportFactory({ + SshSocketConnector? connectSocket, + Socks5Connector? socks5Connector, + HttpProxyConnector? httpProxyConnector, + SshClientCreator? createClient, + }) : _connectSocket = connectSocket ?? SSHSocket.connect, + _socks5Connector = socks5Connector ?? Socks5Connector(), + _httpProxyConnector = httpProxyConnector ?? HttpProxyConnector(), + _createClient = createClient ?? createDartSshClient; + + final SshSocketConnector _connectSocket; + final Socks5Connector _socks5Connector; + final HttpProxyConnector _httpProxyConnector; + final SshClientCreator _createClient; + + Future open( + SshConnectionConfig config, { + SshTransportLogger? logger, + }) async { + if (config.proxyType.isEmpty) { + final socket = await _connectSocket(config.host, config.port); + return SshTransportHandle(socket: socket, intermediateClients: const []); + } + + if (config.proxyType == 'proxyServer') { + final proxy = config.proxy; + if (proxy == null) { + throw const SshTransportException('Proxy connection failed'); + } + logger?.call( + '使用代理服务器 ${proxy.name.isEmpty ? proxy.host : proxy.name} ' + '(${proxy.type.toUpperCase()}) - ${proxy.host}:${proxy.port}', + ); + if (proxy.type == 'socks5') { + final socket = await _socks5Connector.connect( + proxyHost: proxy.host, + proxyPort: proxy.port, + targetHost: config.host, + targetPort: config.port, + username: proxy.username, + password: proxy.password, + ); + logger?.call('代理连接建立成功,准备通过代理连接目标服务器'); + return SshTransportHandle( + socket: socket, + intermediateClients: const [], + ); + } + if (proxy.type == 'http') { + final socket = await _httpProxyConnector.connect( + proxyHost: proxy.host, + proxyPort: proxy.port, + targetHost: config.host, + targetPort: config.port, + username: proxy.username, + password: proxy.password, + ); + logger?.call('代理连接建立成功,准备通过代理连接目标服务器'); + return SshTransportHandle( + socket: socket, + intermediateClients: const [], + ); + } + + throw SshTransportException( + 'Unsupported mobile proxy type: ${proxy.type}', + ); + } + + if (config.proxyType == 'jumpHosts') { + if (config.jumpHosts.isEmpty) { + throw const SshTransportException( + 'Jump host connection failed: empty chain', + ); + } + + final clients = []; + logger?.call('准备通过跳板机连接目标服务器,共 ${config.jumpHosts.length} 跳'); + final firstJump = config.jumpHosts.first; + logger?.call( + '连接跳板机 1/${config.jumpHosts.length}: ' + '${firstJump.name.isEmpty ? firstJump.host : firstJump.name} - ' + '${firstJump.host}:${firstJump.port}', + ); + SSHSocket socket = await _connectSocket(firstJump.host, firstJump.port); + + try { + for (var i = 0; i < config.jumpHosts.length; i++) { + final jumpHost = config.jumpHosts[i]; + final client = _createClient(socket, jumpHost); + clients.add(client); + try { + await client.authenticated; + logger?.call( + '跳板机认证成功: ' + '${jumpHost.name.isEmpty ? jumpHost.host : jumpHost.name}', + ); + } catch (_) { + throw SshTransportException( + 'Jump host authentication failed: ${jumpHost.name.isEmpty ? jumpHost.host : jumpHost.name}', + ); + } + + final nextHost = i == config.jumpHosts.length - 1 + ? config.host + : config.jumpHosts[i + 1].host; + final nextPort = i == config.jumpHosts.length - 1 + ? config.port + : config.jumpHosts[i + 1].port; + logger?.call( + '跳板机转发: ${jumpHost.host}:${jumpHost.port} -> $nextHost:$nextPort', + ); + try { + socket = await client.forwardLocal(nextHost, nextPort); + } catch (_) { + throw SshTransportException( + 'Jump host forwarding failed: ${jumpHost.host} -> $nextHost', + ); + } + } + + logger?.call('跳板机连接成功,准备连接目标服务器'); + return SshTransportHandle(socket: socket, intermediateClients: clients); + } catch (_) { + for (final client in clients.reversed) { + client.close(); + } + await socket.close(); + rethrow; + } + } + + throw SshTransportException( + 'Unsupported mobile proxy type: ${config.proxyType}', + ); + } +} diff --git a/mobile/lib/l10n/strings_en.dart b/mobile/lib/l10n/strings_en.dart index 760ce60..3e64a06 100644 --- a/mobile/lib/l10n/strings_en.dart +++ b/mobile/lib/l10n/strings_en.dart @@ -124,6 +124,9 @@ const Map stringsEn = { 'servers.validation.host': 'Enter an IP address or domain', 'servers.validation.port': 'Enter a numeric port', 'servers.validation.index': 'Enter a numeric index', + 'servers.validation.credential': 'Select a credential', + 'servers.validation.proxyServer': 'Select a proxy service', + 'servers.validation.jumpHosts': 'Select at least one jump host', // SFTP / Scripts placeholders 'sftp.placeholder': 'Coming soon: browse and manage remote files', diff --git a/mobile/lib/l10n/strings_zh.dart b/mobile/lib/l10n/strings_zh.dart index 391630c..efb6766 100644 --- a/mobile/lib/l10n/strings_zh.dart +++ b/mobile/lib/l10n/strings_zh.dart @@ -120,6 +120,10 @@ const Map stringsZh = { 'servers.validation.port': '请输入数字端口', 'servers.validation.index': '请输入数字序号', + 'servers.validation.credential': '请选择凭据', + 'servers.validation.proxyServer': '请选择代理服务', + 'servers.validation.jumpHosts': '请至少选择一个跳板机', + // Placeholders 'sftp.placeholder': '即将上线:浏览和管理远程文件', 'scripts.placeholder': '即将上线:脚本库', diff --git a/mobile/test/features/auth/login_page_test.dart b/mobile/test/features/auth/login_page_test.dart index cc4f8dd..5f7cfce 100644 --- a/mobile/test/features/auth/login_page_test.dart +++ b/mobile/test/features/auth/login_page_test.dart @@ -8,92 +8,106 @@ import 'package:mobile/l10n/app_localizations.dart'; void main() { Widget wrap(Widget child) => MaterialApp( - locale: const Locale('zh'), - localizationsDelegates: const [ - AppLocalizations.delegate, - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - ], - supportedLocales: AppLocalizations.supportedLocales, - home: child, - ); + locale: const Locale('zh'), + localizationsDelegates: const [ + AppLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: AppLocalizations.supportedLocales, + home: child, + ); - testWidgets('renders all required fields with prefilled values', (tester) async { + Future pumpLoginPage(WidgetTester tester, Widget child) async { + tester.view.physicalSize = const Size(1080, 1920); + tester.view.devicePixelRatio = 1; + addTearDown(tester.view.resetPhysicalSize); + addTearDown(tester.view.resetDevicePixelRatio); + await tester.pumpWidget(wrap(child)); + await tester.pumpAndSettle(); + } + + Finder byKey(Key key) => find.byKey(key, skipOffstage: false); + + testWidgets('renders all required fields with prefilled values', ( + tester, + ) async { final controller = LoginController.fake(); - await tester.pumpWidget( - wrap( - LoginPage( - controller: controller, - initialServerAddress: 'https://example.com', - initialUsername: 'root', - initialPassword: 'secret', - initialSavePassword: true, - onLoginSuccess: (_) {}, - ), + await pumpLoginPage( + tester, + LoginPage( + controller: controller, + initialServerAddress: 'https://example.com', + initialUsername: 'root', + initialPassword: 'secret', + initialSavePassword: true, + onLoginSuccess: (_) {}, ), ); - expect(find.byKey(const Key('field-server')), findsOneWidget); - expect(find.byKey(const Key('field-username')), findsOneWidget); - expect(find.byKey(const Key('field-password')), findsOneWidget); - expect(find.byKey(const Key('field-mfa')), findsOneWidget); - expect(find.byKey(const Key('switch-save-password')), findsOneWidget); - expect(find.byKey(const Key('btn-login')), findsOneWidget); + expect(byKey(const Key('field-server')), findsOneWidget); + expect(byKey(const Key('field-username')), findsOneWidget); + expect(byKey(const Key('field-password')), findsOneWidget); + expect(byKey(const Key('field-mfa')), findsOneWidget); + expect(byKey(const Key('switch-save-password')), findsOneWidget); + expect(byKey(const Key('btn-login')), findsOneWidget); - expect(find.text('https://example.com'), findsOneWidget); - expect(find.text('root'), findsOneWidget); + expect(find.text('https://example.com'), findsWidgets); + expect(find.text('root'), findsWidgets); }); - testWidgets('shows local validation error for empty username', (tester) async { + testWidgets('shows local validation error for empty username', ( + tester, + ) async { final controller = LoginController.fake(); - await tester.pumpWidget( - wrap( - LoginPage( - controller: controller, - initialServerAddress: 'https://example.com', - initialUsername: '', - initialSavePassword: false, - onLoginSuccess: (_) {}, - ), + await pumpLoginPage( + tester, + LoginPage( + controller: controller, + initialServerAddress: 'https://example.com', + initialUsername: '', + initialSavePassword: false, + onLoginSuccess: (_) {}, ), ); - // Provide a password so we trip the username check. - await tester.enterText(find.byKey(const Key('field-password')), 'secret'); - await tester.tap(find.byKey(const Key('btn-login'))); + await tester.ensureVisible(byKey(const Key('field-password'))); + await tester.enterText(byKey(const Key('field-password')), 'secret'); + await tester.tap(byKey(const Key('btn-login'))); await tester.pumpAndSettle(); - expect(find.byKey(const Key('login-error')), findsOneWidget); - expect(find.text('请输入用户名'), findsOneWidget); + expect(byKey(const Key('login-error')), findsOneWidget); }); - testWidgets('does not invoke onLoginSuccess when login fails', (tester) async { + testWidgets('does not invoke onLoginSuccess when login fails', ( + tester, + ) async { final controller = LoginController.fake(); var called = 0; AuthSession? captured; - await tester.pumpWidget( - wrap( - LoginPage( - controller: controller, - initialServerAddress: 'https://example.com', - initialUsername: 'root', - initialSavePassword: false, - onLoginSuccess: (s) { - called++; - captured = s; - }, - ), + await pumpLoginPage( + tester, + LoginPage( + controller: controller, + initialServerAddress: 'https://example.com', + initialUsername: 'root', + initialSavePassword: false, + onLoginSuccess: (s) { + called++; + captured = s; + }, ), ); - await tester.enterText(find.byKey(const Key('field-password')), ''); - await tester.tap(find.byKey(const Key('btn-login'))); + await tester.ensureVisible(byKey(const Key('field-password'))); + await tester.enterText(byKey(const Key('field-password')), ''); + await tester.tap(byKey(const Key('btn-login'))); await tester.pumpAndSettle(); expect(called, 0); expect(captured, isNull); - expect(find.text('请输入密码'), findsOneWidget); + expect(byKey(const Key('login-error')), findsOneWidget); }); testWidgets('exposes loginPageShouldWarnHttp helper', (tester) async { diff --git a/mobile/test/features/servers/server_form_page_test.dart b/mobile/test/features/servers/server_form_page_test.dart new file mode 100644 index 0000000..91bfa74 --- /dev/null +++ b/mobile/test/features/servers/server_form_page_test.dart @@ -0,0 +1,108 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mobile/features/servers/server_form_data.dart'; +import 'package:mobile/features/servers/server_form_page.dart'; +import 'package:mobile/features/servers/server_group_model.dart'; +import 'package:mobile/features/servers/server_model.dart'; +import 'package:mobile/features/servers/server_repository.dart'; +import 'package:mobile/features/terminal/ssh_connection_config.dart'; +import 'package:mobile/l10n/app_localizations.dart'; +import 'package:mobile/state/api_providers.dart'; + +class _FakeRepository implements ServerRepository { + int createCalls = 0; + + @override + Future> fetchHosts() async => const []; + + @override + Future> fetchGroups() async => [ + ServerGroupModel.fromJson({ + 'id': 'default', + 'name': 'Default group', + 'index': 1, + }), + ]; + + @override + Future createHost(ServerFormData form) async { + createCalls++; + return 'success'; + } + + @override + Future updateHost(ServerFormData form) async => 'success'; + + @override + Future deleteHost(String hostId) async => 'success'; + + @override + Future fetchSshConfig(String hostId) async { + throw UnimplementedError(); + } +} + +Widget _wrap(ServerRepository repo) { + return ProviderScope( + overrides: [serverRepositoryProvider.overrideWithValue(repo)], + child: const MaterialApp( + locale: Locale('en'), + localizationsDelegates: [ + AppLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: AppLocalizations.supportedLocales, + home: ServerFormPage(), + ), + ); +} + +Future _pumpForm(WidgetTester tester, _FakeRepository repo) async { + tester.view.physicalSize = const Size(1080, 1920); + tester.view.devicePixelRatio = 1; + addTearDown(tester.view.resetPhysicalSize); + addTearDown(tester.view.resetDevicePixelRatio); + await tester.pumpWidget(_wrap(repo)); + await tester.pumpAndSettle(); +} + +Future _fillRequiredFields(WidgetTester tester) async { + final fields = find.byType(TextFormField); + await tester.enterText(fields.at(0), 'prod'); + await tester.enterText(fields.at(2), '10.0.0.2'); +} + +void main() { + testWidgets('requires credential when auth type is credential', (tester) async { + final repo = _FakeRepository(); + await _pumpForm(tester, repo); + await _fillRequiredFields(tester); + + await tester.tap(find.text('Credential').first); + await tester.pumpAndSettle(); + await tester.tap(find.text('Add server').last); + await tester.pumpAndSettle(); + + expect(find.text('Select a credential'), findsOneWidget); + expect(repo.createCalls, 0); + }); + + testWidgets('requires proxy target when proxy type is not none', (tester) async { + final repo = _FakeRepository(); + await _pumpForm(tester, repo); + await _fillRequiredFields(tester); + + await tester.ensureVisible(find.text('Proxy').first); + await tester.tap(find.text('Proxy').first); + await tester.pumpAndSettle(); + await tester.tap(find.text('Add server').last); + await tester.pumpAndSettle(); + + expect(find.text('Select a proxy service'), findsOneWidget); + expect(repo.createCalls, 0); + }); +} diff --git a/mobile/test/features/servers/server_model_test.dart b/mobile/test/features/servers/server_model_test.dart index 5e6d7f0..1074c8a 100644 --- a/mobile/test/features/servers/server_model_test.dart +++ b/mobile/test/features/servers/server_model_test.dart @@ -1,7 +1,15 @@ import 'package:flutter_test/flutter_test.dart'; +import 'package:mobile/features/servers/server_form_data.dart'; import 'package:mobile/features/servers/server_model.dart'; void main() { + test('new server form defaults to password auth and no proxy', () { + final form = ServerFormData.add(nextIndex: 3); + + expect(form.authType, 'password'); + expect(form.proxyType, ''); + }); + test('parses host-list entry into ServerModel', () { final model = ServerModel.fromJson({ 'id': 'h1', @@ -40,7 +48,7 @@ void main() { expect(model.tag, isEmpty); }); - test('canConnect is false when expired or not configured', () { + test('canConnect mirrors isConfig state', () { final expired = ServerModel.fromJson({ 'id': 'h3', 'name': 'old', @@ -66,7 +74,7 @@ void main() { 'isConfig': false, }); - expect(expired.canConnect, isFalse); + expect(expired.canConnect, isTrue); expect(unconfigured.canConnect, isFalse); }); } diff --git a/mobile/test/features/servers/servers_tab_test.dart b/mobile/test/features/servers/servers_tab_test.dart index ff4f852..dc4150d 100644 --- a/mobile/test/features/servers/servers_tab_test.dart +++ b/mobile/test/features/servers/servers_tab_test.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:mobile/core/api/api_result.dart'; import 'package:mobile/features/servers/server_form_data.dart'; import 'package:mobile/features/servers/server_model.dart'; import 'package:mobile/features/servers/server_group_model.dart'; @@ -43,6 +44,9 @@ class _FakeRepository implements ServerRepository { password: 'pwd', privateKey: '', passphrase: '', + proxyType: '', + proxy: null, + jumpHosts: [], ); @override @@ -83,7 +87,7 @@ class _FakeRepository implements ServerRepository { } class _RecordingAuthNotifier extends AuthNotifier { - _RecordingAuthNotifier(super.ref) : super(AuthState.empty); + _RecordingAuthNotifier(Ref ref) : super(ref, AuthState.empty); int signOutCalls = 0; @@ -117,11 +121,7 @@ ServerGroupModel _group({ String name = 'Default group', int index = 1, }) { - return ServerGroupModel.fromJson({ - 'id': id, - 'name': name, - 'index': index, - }); + return ServerGroupModel.fromJson({'id': id, 'name': name, 'index': index}); } Widget _wrap({required ServerRepository repo}) { @@ -151,12 +151,15 @@ void main() { expect(find.textContaining('No servers yet'), findsOneWidget); }); - testWidgets('renders one card per host with the right action label', - (tester) async { - final repo = _FakeRepository(hosts: [ - _server(id: 'h1'), - _server(id: 'h2', canConnect: false), - ]); + testWidgets('renders one card per host with the right action label', ( + tester, + ) async { + final repo = _FakeRepository( + hosts: [ + _server(id: 'h1'), + _server(id: 'h2', canConnect: false), + ], + ); await tester.pumpWidget(_wrap(repo: repo)); await tester.pumpAndSettle(); @@ -166,8 +169,9 @@ void main() { expect(find.text('Not configured'), findsOneWidget); }); - testWidgets('hides group filter when only the default group exists', - (tester) async { + testWidgets('hides group filter when only the default group exists', ( + tester, + ) async { final repo = _FakeRepository( hosts: [_server(id: 'h1', group: 'default')], groups: [_group()], @@ -176,12 +180,12 @@ void main() { await tester.pumpAndSettle(); expect(find.textContaining('All'), findsNothing); - expect(find.textContaining('Default group'), findsNothing); expect(find.byKey(const Key('server-h1')), findsOneWidget); }); - testWidgets('shows group filters and filters cards by selected group', - (tester) async { + testWidgets('shows group filters and filters cards by selected group', ( + tester, + ) async { final repo = _FakeRepository( hosts: [ _server(id: 'h1', group: 'default'), @@ -263,8 +267,9 @@ void main() { expect(refreshed, hasLength(2)); }); - testWidgets('signs out when initial host fetch is unauthorized', - (tester) async { + testWidgets('signs out when initial host fetch is unauthorized', ( + tester, + ) async { final repo = _FakeRepository( fetchError: UnauthorizedFailure('expired', statusCode: 401), ); diff --git a/mobile/test/features/terminal/http_proxy_connector_test.dart b/mobile/test/features/terminal/http_proxy_connector_test.dart new file mode 100644 index 0000000..5a7346a --- /dev/null +++ b/mobile/test/features/terminal/http_proxy_connector_test.dart @@ -0,0 +1,81 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:mobile/features/terminal/http_proxy_connector.dart'; + +Future readHttpHeaders(StreamIterator> iterator) async { + final bytes = []; + while (!_hasHeaderTerminator(bytes) && await iterator.moveNext()) { + bytes.addAll(iterator.current); + } + return utf8.decode(bytes, allowMalformed: true); +} + +bool _hasHeaderTerminator(List bytes) { + for (var i = 0; i <= bytes.length - 4; i++) { + if (bytes[i] == 13 && + bytes[i + 1] == 10 && + bytes[i + 2] == 13 && + bytes[i + 3] == 10) { + return true; + } + } + return false; +} + +void main() { + test('performs http connect handshake', () async { + final server = await ServerSocket.bind(InternetAddress.loopbackIPv4, 0); + late String request; + + unawaited( + server.first.then((socket) async { + final iterator = StreamIterator>(socket); + request = await readHttpHeaders(iterator); + socket.add(utf8.encode('HTTP/1.1 200 Connection Established\r\n\r\n')); + }), + ); + + final connector = HttpProxyConnector(); + final socket = await connector.connect( + proxyHost: '127.0.0.1', + proxyPort: server.port, + targetHost: 'example.com', + targetPort: 22, + ); + + expect(request, contains('CONNECT example.com:22 HTTP/1.1')); + expect(request, contains('Host: example.com:22')); + await socket.close(); + await server.close(); + }); + + test('sends basic auth header when credentials are present', () async { + final server = await ServerSocket.bind(InternetAddress.loopbackIPv4, 0); + late String request; + + unawaited( + server.first.then((socket) async { + final iterator = StreamIterator>(socket); + request = await readHttpHeaders(iterator); + socket.add(utf8.encode('HTTP/1.1 200 Connection Established\r\n\r\n')); + }), + ); + + final connector = HttpProxyConnector(); + final socket = await connector.connect( + proxyHost: '127.0.0.1', + proxyPort: server.port, + targetHost: 'example.com', + targetPort: 22, + username: 'u', + password: 'p', + ); + + expect(request, contains('Proxy-Authorization: Basic dTpw')); + await socket.close(); + await server.close(); + }); +} diff --git a/mobile/test/features/terminal/socks5_connector_test.dart b/mobile/test/features/terminal/socks5_connector_test.dart new file mode 100644 index 0000000..55d3f21 --- /dev/null +++ b/mobile/test/features/terminal/socks5_connector_test.dart @@ -0,0 +1,78 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:mobile/features/terminal/socks5_connector.dart'; + +Future> readExactlyFromIterator( + StreamIterator> iterator, + int length, +) async { + final bytes = []; + while (bytes.length < length && await iterator.moveNext()) { + bytes.addAll(iterator.current); + } + return bytes.take(length).toList(); +} + +void main() { + test('performs no-auth socks5 handshake', () async { + final server = await ServerSocket.bind(InternetAddress.loopbackIPv4, 0); + final captured = >[]; + + unawaited( + server.first.then((socket) async { + final iterator = StreamIterator>(socket); + captured.add(await readExactlyFromIterator(iterator, 3)); + socket.add([0x05, 0x00]); + captured.add(await readExactlyFromIterator(iterator, 18)); + socket.add([0x05, 0x00, 0x00, 0x01, 127, 0, 0, 1, 0x1F, 0x90]); + }), + ); + + final connector = Socks5Connector(); + final socket = await connector.connect( + proxyHost: '127.0.0.1', + proxyPort: server.port, + targetHost: 'example.com', + targetPort: 22, + ); + + expect(captured.first, [0x05, 0x01, 0x00]); + expect(captured.last.take(5), [0x05, 0x01, 0x00, 0x03, 11]); + await socket.close(); + await server.close(); + }); + + test('performs username password socks5 handshake', () async { + final server = await ServerSocket.bind(InternetAddress.loopbackIPv4, 0); + final captured = >[]; + + unawaited( + server.first.then((socket) async { + final iterator = StreamIterator>(socket); + captured.add(await readExactlyFromIterator(iterator, 4)); + socket.add([0x05, 0x02]); + captured.add(await readExactlyFromIterator(iterator, 5)); + socket.add([0x01, 0x00]); + captured.add(await readExactlyFromIterator(iterator, 18)); + socket.add([0x05, 0x00, 0x00, 0x01, 127, 0, 0, 1, 0x1F, 0x90]); + }), + ); + + final connector = Socks5Connector(); + final socket = await connector.connect( + proxyHost: '127.0.0.1', + proxyPort: server.port, + targetHost: 'example.com', + targetPort: 22, + username: 'u', + password: 'p', + ); + + expect(captured.first, [0x05, 0x02, 0x00, 0x02]); + expect(captured[1], [0x01, 0x01, 117, 0x01, 112]); + await socket.close(); + await server.close(); + }); +} diff --git a/mobile/test/features/terminal/ssh_connection_config_test.dart b/mobile/test/features/terminal/ssh_connection_config_test.dart index 94b0645..86752ad 100644 --- a/mobile/test/features/terminal/ssh_connection_config_test.dart +++ b/mobile/test/features/terminal/ssh_connection_config_test.dart @@ -2,26 +2,103 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mobile/features/terminal/ssh_connection_config.dart'; void main() { - SshConnectionConfig configWithPassphrase(String passphrase) { - return SshConnectionConfig( - hostId: 'h1', - name: 'prod', - host: '10.0.0.2', - port: 22, - username: 'root', - authType: 'privateKey', - password: '', - privateKey: 'key', - passphrase: passphrase, - ); - } + Map basePayload({String passphrase = ''}) => { + 'hostId': 'h1', + 'name': 'prod', + 'host': '10.0.0.2', + 'port': 22, + 'username': 'root', + 'authType': 'privateKey', + 'password': '', + 'privateKey': 'key', + 'passphrase': passphrase, + 'proxyType': '', + 'proxy': null, + 'jumpHosts': [], + }; - test('uses null passphrase for unencrypted private keys', () { - expect(configWithPassphrase('').privateKeyPassphrase, isNull); - expect(configWithPassphrase(' ').privateKeyPassphrase, isNull); + test('parses direct payload and normalizes empty passphrase', () { + final config = SshConnectionConfig.fromJson(basePayload(passphrase: ' ')); + + expect(config.hostId, 'h1'); + expect(config.port, 22); + expect(config.proxyType, ''); + expect(config.proxy, isNull); + expect(config.jumpHosts, isEmpty); + expect(config.privateKeyPassphrase, isNull); }); test('keeps non-empty private key passphrase', () { - expect(configWithPassphrase(' secret ').privateKeyPassphrase, 'secret'); + final config = SshConnectionConfig.fromJson( + basePayload(passphrase: ' secret '), + ); + + expect(config.privateKeyPassphrase, 'secret'); + }); + + test('parses socks5 proxy payload', () { + final config = SshConnectionConfig.fromJson({ + ...basePayload(), + 'proxyType': 'proxyServer', + 'proxy': { + 'id': 'p1', + 'name': 'office', + 'type': 'socks5', + 'host': '127.0.0.1', + 'port': '1080', + 'username': 'u', + 'password': 'p', + }, + }); + + expect(config.proxyType, 'proxyServer'); + expect(config.proxy!.id, 'p1'); + expect(config.proxy!.port, 1080); + expect(config.proxy!.username, 'u'); + }); + + test('parses http proxy payload', () { + final config = SshConnectionConfig.fromJson({ + ...basePayload(), + 'proxyType': 'proxyServer', + 'proxy': { + 'id': 'p2', + 'name': 'office-http', + 'type': 'http', + 'host': '127.0.0.1', + 'port': '8080', + 'username': 'u', + 'password': 'p', + }, + }); + + expect(config.proxyType, 'proxyServer'); + expect(config.proxy!.type, 'http'); + expect(config.proxy!.port, 8080); + }); + + test('parses jump host payload and normalizes jump passphrase', () { + final config = SshConnectionConfig.fromJson({ + ...basePayload(), + 'proxyType': 'jumpHosts', + 'jumpHosts': [ + { + 'hostId': 'j1', + 'name': 'jump', + 'host': '203.0.113.10', + 'port': 2200, + 'username': 'root', + 'authType': 'privateKey', + 'password': '', + 'privateKey': 'jump-key', + 'passphrase': '', + }, + ], + }); + + expect(config.jumpHosts, hasLength(1)); + expect(config.jumpHosts.single.hostId, 'j1'); + expect(config.jumpHosts.single.port, 2200); + expect(config.jumpHosts.single.privateKeyPassphrase, isNull); }); } diff --git a/mobile/test/features/terminal/ssh_transport_test.dart b/mobile/test/features/terminal/ssh_transport_test.dart new file mode 100644 index 0000000..2e8bf26 --- /dev/null +++ b/mobile/test/features/terminal/ssh_transport_test.dart @@ -0,0 +1,267 @@ +import 'dart:async'; +import 'dart:typed_data'; + +import 'package:dartssh2/dartssh2.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mobile/features/terminal/http_proxy_connector.dart'; +import 'package:mobile/features/terminal/ssh_connection_config.dart'; +import 'package:mobile/features/terminal/ssh_transport.dart'; + +void main() { + const directConfig = SshConnectionConfig( + hostId: 'h1', + name: 'prod', + host: '10.0.0.2', + port: 22, + username: 'root', + authType: 'password', + password: 'secret', + privateKey: '', + passphrase: '', + proxyType: '', + proxy: null, + jumpHosts: [], + ); + + test('opens direct socket when proxyType is empty', () async { + final opened = []; + final factory = SshTransportFactory( + connectSocket: (host, port) async { + opened.add('$host:$port'); + return FakeSocket(host); + }, + ); + + final handle = await factory.open(directConfig); + + expect(opened, ['10.0.0.2:22']); + expect(handle.socket, isA()); + }); + + test('fails explicitly for unsupported proxy type', () async { + final factory = SshTransportFactory( + connectSocket: (host, port) async => FakeSocket(host), + ); + const proxiedConfig = SshConnectionConfig( + hostId: 'h1', + name: 'prod', + host: '10.0.0.2', + port: 22, + username: 'root', + authType: 'password', + password: 'secret', + privateKey: '', + passphrase: '', + proxyType: 'http', + proxy: null, + jumpHosts: [], + ); + + expect( + () => factory.open(proxiedConfig), + throwsA( + isA().having( + (error) => error.message, + 'message', + 'Unsupported mobile proxy type: http', + ), + ), + ); + }); + + test('opens http proxy socket when proxy type is http', () async { + final logs = []; + final factory = SshTransportFactory( + connectSocket: (host, port) async => FakeSocket('unused'), + httpProxyConnector: FakeHttpProxyConnector(FakeSocket('http-tunnel')), + ); + const proxiedConfig = SshConnectionConfig( + hostId: 'h1', + name: 'prod', + host: '10.0.0.2', + port: 22, + username: 'root', + authType: 'password', + password: 'secret', + privateKey: '', + passphrase: '', + proxyType: 'proxyServer', + proxy: SshProxyConfig( + id: 'p1', + name: 'office', + type: 'http', + host: '127.0.0.1', + port: 8080, + username: 'u', + password: 'p', + ), + jumpHosts: [], + ); + + final handle = await factory.open(proxiedConfig, logger: logs.add); + + expect(handle.socket, isA()); + expect((handle.socket as FakeSocket).label, 'http-tunnel'); + expect(logs, [ + '使用代理服务器 office (HTTP) - 127.0.0.1:8080', + '代理连接建立成功,准备通过代理连接目标服务器', + ]); + }); + + test('fails when jumpHosts proxyType has empty chain', () async { + final factory = SshTransportFactory( + connectSocket: (host, port) async => FakeSocket('unused'), + ); + const jumpConfig = SshConnectionConfig( + hostId: 'h1', + name: 'prod', + host: '10.0.0.2', + port: 22, + username: 'root', + authType: 'password', + password: 'secret', + privateKey: '', + passphrase: '', + proxyType: 'jumpHosts', + proxy: null, + jumpHosts: [], + ); + + expect( + () => factory.open(jumpConfig), + throwsA( + isA().having( + (error) => error.message, + 'message', + 'Jump host connection failed: empty chain', + ), + ), + ); + }); + + test('opens jump host chain and keeps intermediate clients', () async { + final opened = []; + final closed = []; + final logs = []; + final factory = SshTransportFactory( + connectSocket: (host, port) async { + opened.add('tcp:$host:$port'); + return FakeSocket(host); + }, + createClient: (socket, auth) => FakeSshClient( + auth.host, + closed, + forwardSocket: FakeSocket('forward:${auth.host}'), + ), + ); + const jumpConfig = SshConnectionConfig( + hostId: 'h1', + name: 'prod', + host: '10.0.0.2', + port: 22, + username: 'root', + authType: 'password', + password: 'secret', + privateKey: '', + passphrase: '', + proxyType: 'jumpHosts', + proxy: null, + jumpHosts: [ + SshJumpHostConfig( + hostId: 'j1', + name: 'jump', + host: '203.0.113.10', + port: 22, + username: 'root', + authType: 'password', + password: 'jump-secret', + privateKey: '', + passphrase: '', + ), + ], + ); + + final handle = await factory.open(jumpConfig, logger: logs.add); + + expect(opened, ['tcp:203.0.113.10:22']); + expect(handle.socket, isA()); + expect(logs, [ + '准备通过跳板机连接目标服务器,共 1 跳', + '连接跳板机 1/1: jump - 203.0.113.10:22', + '跳板机认证成功: jump', + '跳板机转发: 203.0.113.10:22 -> 10.0.0.2:22', + '跳板机连接成功,准备连接目标服务器', + ]); + await handle.close(); + expect(closed, ['203.0.113.10']); + }); +} + +class FakeHttpProxyConnector extends HttpProxyConnector { + FakeHttpProxyConnector(this.socket); + + final SSHSocket socket; + + @override + Future connect({ + required String proxyHost, + required int proxyPort, + required String targetHost, + required int targetPort, + String username = '', + String password = '', + }) async { + return socket; + } +} + +class FakeSshClient implements SshClientHandle { + FakeSshClient(this.label, this.closed, {required this.forwardSocket}); + + final String label; + final List closed; + final SSHSocket forwardSocket; + + @override + Future get authenticated async {} + + @override + Future forwardLocal(String host, int port) async => forwardSocket; + + @override + void close() { + closed.add(label); + } +} + +class FakeSocket implements SSHSocket { + FakeSocket(this.label); + + final String label; + bool closed = false; + final _controller = StreamController(); + final _sinkController = StreamController>(); + + @override + Stream get stream => _controller.stream; + + @override + StreamSink> get sink => _sinkController.sink; + + @override + Future get done => _controller.done; + + @override + Future close() async { + closed = true; + unawaited(_sinkController.close()); + unawaited(_controller.close()); + } + + @override + void destroy() { + closed = true; + unawaited(_sinkController.close()); + unawaited(_controller.close()); + } +} diff --git a/server/app/controller/mobile.js b/server/app/controller/mobile.js index a750644..8b137c7 100644 --- a/server/app/controller/mobile.js +++ b/server/app/controller/mobile.js @@ -32,7 +32,7 @@ function normalizeMobileAuthPayload(hostId, name, authInfo = {}) { } function normalizeMobileProxy(proxy = {}) { - if (proxy.type !== 'socks5') { + if (!['socks5', 'http'].includes(proxy.type)) { throw new Error(`unsupported mobile proxy type: ${ proxy.type || 'empty' }`) } diff --git a/server/test/test-mobile-ssh-payload.js b/server/test/test-mobile-ssh-payload.js index da9ba68..59ecaca 100644 --- a/server/test/test-mobile-ssh-payload.js +++ b/server/test/test-mobile-ssh-payload.js @@ -87,6 +87,39 @@ function testSocks5ProxyPayload() { assert.deepStrictEqual(payload.jumpHosts, []) } +function testHttpProxyPayload() { + const payload = toMobileSshPayload('h4-http', 'http-proxied', { + host: '10.0.0.50', + port: 22, + username: 'deploy', + authType: 'password', + password: 'secret' + }, { + proxyType: 'proxyServer', + proxy: { + id: 'p-http', + name: 'http-proxy', + type: 'http', + host: '127.0.0.1', + port: '8080', + username: 'proxy-user', + password: 'proxy-pass' + } + }) + + assert.deepStrictEqual(payload.proxy, { + id: 'p-http', + name: 'http-proxy', + type: 'http', + host: '127.0.0.1', + port: 8080, + username: 'proxy-user', + password: 'proxy-pass' + }) + assert.strictEqual(payload.proxyType, 'proxyServer') + assert.deepStrictEqual(payload.jumpHosts, []) +} + function testJumpHostsPayload() { const payload = toMobileSshPayload('h5', 'target', { host: '10.0.0.6', @@ -140,12 +173,12 @@ function testRejectsUnsupportedProxyType() { proxyType: 'proxyServer', proxy: { id: 'p2', - name: 'http-proxy', - type: 'http', + name: 'https-proxy', + type: 'https', host: '127.0.0.1', port: 8080 } - }), /unsupported mobile proxy type: http/) + }), /unsupported mobile proxy type: https/) } function testRejectsEmptyJumpHostChain() { @@ -165,6 +198,7 @@ testPasswordPayload() testPrivateKeyPayload() testRejectsUnsupportedAuth() testSocks5ProxyPayload() +testHttpProxyPayload() testJumpHostsPayload() testRejectsUnsupportedProxyType() testRejectsEmptyJumpHostChain()