From 3c57c167f2680630d8261c1e07dcfec5cbb59f9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=85=E5=BF=B5?= Date: Thu, 22 Oct 2026 10:42:19 +0800 Subject: [PATCH] chore: import SiteForge Factory V5 source (baseline) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 导入 SiteForge Factory V5 基线源码: - FastAPI + SQLite + Docker Compose(Web + Worker)架构 - 四套商业主题 × 14 页面模板(1.1.0,含历史版本快照) - 四种建站模式骨架 + 迁移审计 + 发布门禁 - 52 项 pytest 测试 - 补全计划主文档 docs/superpowers/plans/ Co-Authored-By: Claude Code --- .dockerignore | 14 + .env.example | 9 + .gitignore | 17 + Dockerfile | 17 + README.md | 123 +++ app/__init__.py | 0 app/adapters.py | 100 ++ app/assembly.py | 126 +++ app/backup.py | 35 + app/blueprints.py | 230 +++++ app/builder.py | 412 ++++++++ app/commercial_readiness.py | 48 + app/core.py | 109 +++ app/db.py | 93 ++ app/enterprise.py | 338 +++++++ app/factory_api.py | 334 +++++++ app/importers.py | 239 +++++ app/main.py | 772 +++++++++++++++ app/meta.py | 6 + app/migration_audit.py | 245 +++++ app/quality.py | 56 ++ app/relations.py | 50 + app/responsive.py | 25 + app/reusables.py | 53 + app/security.py | 104 ++ app/static/admin.css | 1 + app/static/admin.js | 96 ++ app/static/blocks.css | 13 + app/static/commercial.css | 27 + app/static/editor.css | 4 + app/static/editor.js | 90 ++ app/static/enterprise-ui.js | 58 ++ app/static/factory-v2.css | 1 + app/static/factory-v2.js | 65 ++ app/static/factory.css | 27 + app/static/pro-base.css | 21 + app/static/pro.js | 26 + app/static/site-runtime.js | 31 + app/static_site.py | 56 ++ app/table_loader.py | 38 + app/templates/admin.html | 15 + app/templates/editor.html | 26 + app/templates/factory-v2.html | 12 + app/templates/login.html | 5 + app/templates/theme-gallery.html | 15 + app/theme_pro.py | 174 ++++ app/theme_store.py | 171 ++++ app/wordpress_compat.py | 83 ++ app/worker.py | 60 ++ compose.public.yaml | 26 + compose.yaml | 62 ++ deploy/Caddyfile | 9 + docs/CHANGELOG_v0.3.md | 17 + docs/COMMERCIAL_THEME_V5.md | 57 ++ docs/IMPLEMENTATION_V4.md | 22 + docs/PRODUCTION_ROADMAP.md | 17 + docs/RELEASE_STATUS_V2.md | 29 + docs/SiteForge_V2_Architecture.md | 905 ++++++++++++++++++ ...-10-22-siteforge-completion-master-plan.md | 133 +++ examples/content.json | 20 + examples/declarative-theme-1.0.0.zip | Bin 0 -> 4090 bytes examples/products.csv | 4 + examples/professional-theme-1.0.0.zip | Bin 0 -> 2315 bytes integrations/art-design-pro/README.md | 28 + .../src/views/siteforge/EnterpriseCenter.vue | 33 + .../src/views/siteforge/ThemeCenter.vue | 34 + .../src/views/siteforge/WebsiteFactory.vue | 71 ++ pytest.ini | 3 + requirements-dev.txt | 3 + requirements.txt | 14 + scripts/check.ps1 | 25 + scripts/init_config.py | 19 + scripts/install_into_art_design_pro.py | 40 + scripts/make_offline_preview.py | 30 + scripts/make_theme_demo.py | 71 ++ scripts/restore_backup.py | 56 ++ scripts/split_theme_pages.py | 24 + tests/test_commercial_v5.py | 77 ++ tests/test_core_v4.py | 196 ++++ tests/test_enterprise.py | 134 +++ tests/test_factory.py | 269 ++++++ tests/test_factory_v2.py | 160 ++++ tests/test_theme_pro_v3.py | 122 +++ themes/_professional_components.html | 16 + themes/automotive/manifest.json | 54 ++ themes/automotive/page-presets.json | 285 ++++++ themes/automotive/pages/about.html | 2 + themes/automotive/pages/article.html | 1 + themes/automotive/pages/brand.html | 1 + themes/automotive/pages/case.html | 1 + themes/automotive/pages/category.html | 2 + themes/automotive/pages/contact.html | 7 + themes/automotive/pages/faq.html | 2 + themes/automotive/pages/home.html | 4 + themes/automotive/pages/listing.html | 2 + themes/automotive/pages/notfound.html | 1 + themes/automotive/pages/product.html | 3 + themes/automotive/pages/search.html | 2 + themes/automotive/pages/service.html | 2 + themes/automotive/pages/standard.html | 2 + themes/automotive/pro.css | 8 + themes/automotive/pro.html | 10 + themes/automotive/site.html | 13 + themes/automotive/theme.css | 1 + .../automotive/versions/0.3.0/manifest.json | 38 + .../versions/0.3.0/page-presets.json | 245 +++++ themes/automotive/versions/0.3.0/site.html | 13 + themes/automotive/versions/0.3.0/theme.css | 1 + .../automotive/versions/1.0.0/manifest.json | 43 + .../versions/1.0.0/page-presets.json | 285 ++++++ .../versions/1.0.0/pages/about.html | 2 + .../versions/1.0.0/pages/article.html | 1 + .../versions/1.0.0/pages/brand.html | 1 + .../automotive/versions/1.0.0/pages/case.html | 1 + .../versions/1.0.0/pages/category.html | 2 + .../versions/1.0.0/pages/contact.html | 7 + .../automotive/versions/1.0.0/pages/faq.html | 2 + .../automotive/versions/1.0.0/pages/home.html | 4 + .../versions/1.0.0/pages/listing.html | 2 + .../versions/1.0.0/pages/notfound.html | 1 + .../versions/1.0.0/pages/product.html | 3 + .../versions/1.0.0/pages/search.html | 2 + .../versions/1.0.0/pages/service.html | 2 + .../versions/1.0.0/pages/standard.html | 2 + themes/automotive/versions/1.0.0/pro.css | 3 + themes/automotive/versions/1.0.0/pro.html | 10 + themes/automotive/versions/1.0.0/site.html | 13 + themes/automotive/versions/1.0.0/theme.css | 1 + themes/automotive/visual/hero.svg | 1 + themes/corporate/manifest.json | 54 ++ themes/corporate/page-presets.json | 285 ++++++ themes/corporate/pages/about.html | 2 + themes/corporate/pages/article.html | 1 + themes/corporate/pages/brand.html | 1 + themes/corporate/pages/case.html | 1 + themes/corporate/pages/category.html | 2 + themes/corporate/pages/contact.html | 7 + themes/corporate/pages/faq.html | 2 + themes/corporate/pages/home.html | 4 + themes/corporate/pages/listing.html | 2 + themes/corporate/pages/notfound.html | 1 + themes/corporate/pages/product.html | 3 + themes/corporate/pages/search.html | 2 + themes/corporate/pages/service.html | 2 + themes/corporate/pages/standard.html | 2 + themes/corporate/pro.css | 7 + themes/corporate/pro.html | 10 + themes/corporate/site.html | 12 + themes/corporate/theme.css | 1 + themes/corporate/versions/0.3.0/manifest.json | 38 + .../versions/0.3.0/page-presets.json | 245 +++++ themes/corporate/versions/0.3.0/site.html | 12 + themes/corporate/versions/0.3.0/theme.css | 1 + themes/corporate/versions/1.0.0/manifest.json | 43 + .../versions/1.0.0/page-presets.json | 285 ++++++ .../corporate/versions/1.0.0/pages/about.html | 2 + .../versions/1.0.0/pages/article.html | 1 + .../corporate/versions/1.0.0/pages/brand.html | 1 + .../corporate/versions/1.0.0/pages/case.html | 1 + .../versions/1.0.0/pages/category.html | 2 + .../versions/1.0.0/pages/contact.html | 7 + .../corporate/versions/1.0.0/pages/faq.html | 2 + .../corporate/versions/1.0.0/pages/home.html | 4 + .../versions/1.0.0/pages/listing.html | 2 + .../versions/1.0.0/pages/notfound.html | 1 + .../versions/1.0.0/pages/product.html | 3 + .../versions/1.0.0/pages/search.html | 2 + .../versions/1.0.0/pages/service.html | 2 + .../versions/1.0.0/pages/standard.html | 2 + themes/corporate/versions/1.0.0/pro.css | 2 + themes/corporate/versions/1.0.0/pro.html | 10 + themes/corporate/versions/1.0.0/site.html | 12 + themes/corporate/versions/1.0.0/theme.css | 1 + themes/corporate/visual/hero.svg | 1 + themes/industrial/manifest.json | 54 ++ themes/industrial/page-presets.json | 285 ++++++ themes/industrial/pages/about.html | 2 + themes/industrial/pages/article.html | 1 + themes/industrial/pages/brand.html | 1 + themes/industrial/pages/case.html | 1 + themes/industrial/pages/category.html | 2 + themes/industrial/pages/contact.html | 7 + themes/industrial/pages/faq.html | 2 + themes/industrial/pages/home.html | 4 + themes/industrial/pages/listing.html | 2 + themes/industrial/pages/notfound.html | 1 + themes/industrial/pages/product.html | 3 + themes/industrial/pages/search.html | 2 + themes/industrial/pages/service.html | 2 + themes/industrial/pages/standard.html | 2 + themes/industrial/pro.css | 7 + themes/industrial/pro.html | 10 + themes/industrial/site.html | 12 + themes/industrial/theme.css | 1 + .../industrial/versions/0.3.0/manifest.json | 38 + .../versions/0.3.0/page-presets.json | 245 +++++ themes/industrial/versions/0.3.0/site.html | 12 + themes/industrial/versions/0.3.0/theme.css | 1 + .../industrial/versions/1.0.0/manifest.json | 43 + .../versions/1.0.0/page-presets.json | 285 ++++++ .../versions/1.0.0/pages/about.html | 2 + .../versions/1.0.0/pages/article.html | 1 + .../versions/1.0.0/pages/brand.html | 1 + .../industrial/versions/1.0.0/pages/case.html | 1 + .../versions/1.0.0/pages/category.html | 2 + .../versions/1.0.0/pages/contact.html | 7 + .../industrial/versions/1.0.0/pages/faq.html | 2 + .../industrial/versions/1.0.0/pages/home.html | 4 + .../versions/1.0.0/pages/listing.html | 2 + .../versions/1.0.0/pages/notfound.html | 1 + .../versions/1.0.0/pages/product.html | 3 + .../versions/1.0.0/pages/search.html | 2 + .../versions/1.0.0/pages/service.html | 2 + .../versions/1.0.0/pages/standard.html | 2 + themes/industrial/versions/1.0.0/pro.css | 2 + themes/industrial/versions/1.0.0/pro.html | 10 + themes/industrial/versions/1.0.0/site.html | 12 + themes/industrial/versions/1.0.0/theme.css | 1 + themes/industrial/visual/hero.svg | 1 + themes/minimal/manifest.json | 54 ++ themes/minimal/page-presets.json | 273 ++++++ themes/minimal/pages/about.html | 2 + themes/minimal/pages/article.html | 1 + themes/minimal/pages/brand.html | 1 + themes/minimal/pages/case.html | 1 + themes/minimal/pages/category.html | 2 + themes/minimal/pages/contact.html | 7 + themes/minimal/pages/faq.html | 2 + themes/minimal/pages/home.html | 3 + themes/minimal/pages/listing.html | 2 + themes/minimal/pages/notfound.html | 1 + themes/minimal/pages/product.html | 3 + themes/minimal/pages/search.html | 2 + themes/minimal/pages/service.html | 2 + themes/minimal/pages/standard.html | 2 + themes/minimal/pro.css | 8 + themes/minimal/pro.html | 9 + themes/minimal/site.html | 8 + themes/minimal/theme.css | 1 + themes/minimal/versions/0.3.0/manifest.json | 38 + .../minimal/versions/0.3.0/page-presets.json | 233 +++++ themes/minimal/versions/0.3.0/site.html | 8 + themes/minimal/versions/0.3.0/theme.css | 1 + themes/minimal/versions/1.0.0/manifest.json | 43 + .../minimal/versions/1.0.0/page-presets.json | 273 ++++++ .../minimal/versions/1.0.0/pages/about.html | 2 + .../minimal/versions/1.0.0/pages/article.html | 1 + .../minimal/versions/1.0.0/pages/brand.html | 1 + themes/minimal/versions/1.0.0/pages/case.html | 1 + .../versions/1.0.0/pages/category.html | 2 + .../minimal/versions/1.0.0/pages/contact.html | 7 + themes/minimal/versions/1.0.0/pages/faq.html | 2 + themes/minimal/versions/1.0.0/pages/home.html | 3 + .../minimal/versions/1.0.0/pages/listing.html | 2 + .../versions/1.0.0/pages/notfound.html | 1 + .../minimal/versions/1.0.0/pages/product.html | 3 + .../minimal/versions/1.0.0/pages/search.html | 2 + .../minimal/versions/1.0.0/pages/service.html | 2 + .../versions/1.0.0/pages/standard.html | 2 + themes/minimal/versions/1.0.0/pro.css | 2 + themes/minimal/versions/1.0.0/pro.html | 9 + themes/minimal/versions/1.0.0/site.html | 8 + themes/minimal/versions/1.0.0/theme.css | 1 + themes/minimal/visual/hero.svg | 1 + 264 files changed, 11682 insertions(+) create mode 100644 .dockerignore create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 app/__init__.py create mode 100644 app/adapters.py create mode 100644 app/assembly.py create mode 100644 app/backup.py create mode 100644 app/blueprints.py create mode 100644 app/builder.py create mode 100644 app/commercial_readiness.py create mode 100644 app/core.py create mode 100644 app/db.py create mode 100644 app/enterprise.py create mode 100644 app/factory_api.py create mode 100644 app/importers.py create mode 100644 app/main.py create mode 100644 app/meta.py create mode 100644 app/migration_audit.py create mode 100644 app/quality.py create mode 100644 app/relations.py create mode 100644 app/responsive.py create mode 100644 app/reusables.py create mode 100644 app/security.py create mode 100644 app/static/admin.css create mode 100644 app/static/admin.js create mode 100644 app/static/blocks.css create mode 100644 app/static/commercial.css create mode 100644 app/static/editor.css create mode 100644 app/static/editor.js create mode 100644 app/static/enterprise-ui.js create mode 100644 app/static/factory-v2.css create mode 100644 app/static/factory-v2.js create mode 100644 app/static/factory.css create mode 100644 app/static/pro-base.css create mode 100644 app/static/pro.js create mode 100644 app/static/site-runtime.js create mode 100644 app/static_site.py create mode 100644 app/table_loader.py create mode 100644 app/templates/admin.html create mode 100644 app/templates/editor.html create mode 100644 app/templates/factory-v2.html create mode 100644 app/templates/login.html create mode 100644 app/templates/theme-gallery.html create mode 100644 app/theme_pro.py create mode 100644 app/theme_store.py create mode 100644 app/wordpress_compat.py create mode 100644 app/worker.py create mode 100644 compose.public.yaml create mode 100644 compose.yaml create mode 100644 deploy/Caddyfile create mode 100644 docs/CHANGELOG_v0.3.md create mode 100644 docs/COMMERCIAL_THEME_V5.md create mode 100644 docs/IMPLEMENTATION_V4.md create mode 100644 docs/PRODUCTION_ROADMAP.md create mode 100644 docs/RELEASE_STATUS_V2.md create mode 100644 docs/SiteForge_V2_Architecture.md create mode 100644 docs/superpowers/plans/2026-10-22-siteforge-completion-master-plan.md create mode 100644 examples/content.json create mode 100644 examples/declarative-theme-1.0.0.zip create mode 100644 examples/products.csv create mode 100644 examples/professional-theme-1.0.0.zip create mode 100644 integrations/art-design-pro/README.md create mode 100644 integrations/art-design-pro/src/views/siteforge/EnterpriseCenter.vue create mode 100644 integrations/art-design-pro/src/views/siteforge/ThemeCenter.vue create mode 100644 integrations/art-design-pro/src/views/siteforge/WebsiteFactory.vue create mode 100644 pytest.ini create mode 100644 requirements-dev.txt create mode 100644 requirements.txt create mode 100644 scripts/check.ps1 create mode 100644 scripts/init_config.py create mode 100644 scripts/install_into_art_design_pro.py create mode 100644 scripts/make_offline_preview.py create mode 100644 scripts/make_theme_demo.py create mode 100644 scripts/restore_backup.py create mode 100644 scripts/split_theme_pages.py create mode 100644 tests/test_commercial_v5.py create mode 100644 tests/test_core_v4.py create mode 100644 tests/test_enterprise.py create mode 100644 tests/test_factory.py create mode 100644 tests/test_factory_v2.py create mode 100644 tests/test_theme_pro_v3.py create mode 100644 themes/_professional_components.html create mode 100644 themes/automotive/manifest.json create mode 100644 themes/automotive/page-presets.json create mode 100644 themes/automotive/pages/about.html create mode 100644 themes/automotive/pages/article.html create mode 100644 themes/automotive/pages/brand.html create mode 100644 themes/automotive/pages/case.html create mode 100644 themes/automotive/pages/category.html create mode 100644 themes/automotive/pages/contact.html create mode 100644 themes/automotive/pages/faq.html create mode 100644 themes/automotive/pages/home.html create mode 100644 themes/automotive/pages/listing.html create mode 100644 themes/automotive/pages/notfound.html create mode 100644 themes/automotive/pages/product.html create mode 100644 themes/automotive/pages/search.html create mode 100644 themes/automotive/pages/service.html create mode 100644 themes/automotive/pages/standard.html create mode 100644 themes/automotive/pro.css create mode 100644 themes/automotive/pro.html create mode 100644 themes/automotive/site.html create mode 100644 themes/automotive/theme.css create mode 100644 themes/automotive/versions/0.3.0/manifest.json create mode 100644 themes/automotive/versions/0.3.0/page-presets.json create mode 100644 themes/automotive/versions/0.3.0/site.html create mode 100644 themes/automotive/versions/0.3.0/theme.css create mode 100644 themes/automotive/versions/1.0.0/manifest.json create mode 100644 themes/automotive/versions/1.0.0/page-presets.json create mode 100644 themes/automotive/versions/1.0.0/pages/about.html create mode 100644 themes/automotive/versions/1.0.0/pages/article.html create mode 100644 themes/automotive/versions/1.0.0/pages/brand.html create mode 100644 themes/automotive/versions/1.0.0/pages/case.html create mode 100644 themes/automotive/versions/1.0.0/pages/category.html create mode 100644 themes/automotive/versions/1.0.0/pages/contact.html create mode 100644 themes/automotive/versions/1.0.0/pages/faq.html create mode 100644 themes/automotive/versions/1.0.0/pages/home.html create mode 100644 themes/automotive/versions/1.0.0/pages/listing.html create mode 100644 themes/automotive/versions/1.0.0/pages/notfound.html create mode 100644 themes/automotive/versions/1.0.0/pages/product.html create mode 100644 themes/automotive/versions/1.0.0/pages/search.html create mode 100644 themes/automotive/versions/1.0.0/pages/service.html create mode 100644 themes/automotive/versions/1.0.0/pages/standard.html create mode 100644 themes/automotive/versions/1.0.0/pro.css create mode 100644 themes/automotive/versions/1.0.0/pro.html create mode 100644 themes/automotive/versions/1.0.0/site.html create mode 100644 themes/automotive/versions/1.0.0/theme.css create mode 100644 themes/automotive/visual/hero.svg create mode 100644 themes/corporate/manifest.json create mode 100644 themes/corporate/page-presets.json create mode 100644 themes/corporate/pages/about.html create mode 100644 themes/corporate/pages/article.html create mode 100644 themes/corporate/pages/brand.html create mode 100644 themes/corporate/pages/case.html create mode 100644 themes/corporate/pages/category.html create mode 100644 themes/corporate/pages/contact.html create mode 100644 themes/corporate/pages/faq.html create mode 100644 themes/corporate/pages/home.html create mode 100644 themes/corporate/pages/listing.html create mode 100644 themes/corporate/pages/notfound.html create mode 100644 themes/corporate/pages/product.html create mode 100644 themes/corporate/pages/search.html create mode 100644 themes/corporate/pages/service.html create mode 100644 themes/corporate/pages/standard.html create mode 100644 themes/corporate/pro.css create mode 100644 themes/corporate/pro.html create mode 100644 themes/corporate/site.html create mode 100644 themes/corporate/theme.css create mode 100644 themes/corporate/versions/0.3.0/manifest.json create mode 100644 themes/corporate/versions/0.3.0/page-presets.json create mode 100644 themes/corporate/versions/0.3.0/site.html create mode 100644 themes/corporate/versions/0.3.0/theme.css create mode 100644 themes/corporate/versions/1.0.0/manifest.json create mode 100644 themes/corporate/versions/1.0.0/page-presets.json create mode 100644 themes/corporate/versions/1.0.0/pages/about.html create mode 100644 themes/corporate/versions/1.0.0/pages/article.html create mode 100644 themes/corporate/versions/1.0.0/pages/brand.html create mode 100644 themes/corporate/versions/1.0.0/pages/case.html create mode 100644 themes/corporate/versions/1.0.0/pages/category.html create mode 100644 themes/corporate/versions/1.0.0/pages/contact.html create mode 100644 themes/corporate/versions/1.0.0/pages/faq.html create mode 100644 themes/corporate/versions/1.0.0/pages/home.html create mode 100644 themes/corporate/versions/1.0.0/pages/listing.html create mode 100644 themes/corporate/versions/1.0.0/pages/notfound.html create mode 100644 themes/corporate/versions/1.0.0/pages/product.html create mode 100644 themes/corporate/versions/1.0.0/pages/search.html create mode 100644 themes/corporate/versions/1.0.0/pages/service.html create mode 100644 themes/corporate/versions/1.0.0/pages/standard.html create mode 100644 themes/corporate/versions/1.0.0/pro.css create mode 100644 themes/corporate/versions/1.0.0/pro.html create mode 100644 themes/corporate/versions/1.0.0/site.html create mode 100644 themes/corporate/versions/1.0.0/theme.css create mode 100644 themes/corporate/visual/hero.svg create mode 100644 themes/industrial/manifest.json create mode 100644 themes/industrial/page-presets.json create mode 100644 themes/industrial/pages/about.html create mode 100644 themes/industrial/pages/article.html create mode 100644 themes/industrial/pages/brand.html create mode 100644 themes/industrial/pages/case.html create mode 100644 themes/industrial/pages/category.html create mode 100644 themes/industrial/pages/contact.html create mode 100644 themes/industrial/pages/faq.html create mode 100644 themes/industrial/pages/home.html create mode 100644 themes/industrial/pages/listing.html create mode 100644 themes/industrial/pages/notfound.html create mode 100644 themes/industrial/pages/product.html create mode 100644 themes/industrial/pages/search.html create mode 100644 themes/industrial/pages/service.html create mode 100644 themes/industrial/pages/standard.html create mode 100644 themes/industrial/pro.css create mode 100644 themes/industrial/pro.html create mode 100644 themes/industrial/site.html create mode 100644 themes/industrial/theme.css create mode 100644 themes/industrial/versions/0.3.0/manifest.json create mode 100644 themes/industrial/versions/0.3.0/page-presets.json create mode 100644 themes/industrial/versions/0.3.0/site.html create mode 100644 themes/industrial/versions/0.3.0/theme.css create mode 100644 themes/industrial/versions/1.0.0/manifest.json create mode 100644 themes/industrial/versions/1.0.0/page-presets.json create mode 100644 themes/industrial/versions/1.0.0/pages/about.html create mode 100644 themes/industrial/versions/1.0.0/pages/article.html create mode 100644 themes/industrial/versions/1.0.0/pages/brand.html create mode 100644 themes/industrial/versions/1.0.0/pages/case.html create mode 100644 themes/industrial/versions/1.0.0/pages/category.html create mode 100644 themes/industrial/versions/1.0.0/pages/contact.html create mode 100644 themes/industrial/versions/1.0.0/pages/faq.html create mode 100644 themes/industrial/versions/1.0.0/pages/home.html create mode 100644 themes/industrial/versions/1.0.0/pages/listing.html create mode 100644 themes/industrial/versions/1.0.0/pages/notfound.html create mode 100644 themes/industrial/versions/1.0.0/pages/product.html create mode 100644 themes/industrial/versions/1.0.0/pages/search.html create mode 100644 themes/industrial/versions/1.0.0/pages/service.html create mode 100644 themes/industrial/versions/1.0.0/pages/standard.html create mode 100644 themes/industrial/versions/1.0.0/pro.css create mode 100644 themes/industrial/versions/1.0.0/pro.html create mode 100644 themes/industrial/versions/1.0.0/site.html create mode 100644 themes/industrial/versions/1.0.0/theme.css create mode 100644 themes/industrial/visual/hero.svg create mode 100644 themes/minimal/manifest.json create mode 100644 themes/minimal/page-presets.json create mode 100644 themes/minimal/pages/about.html create mode 100644 themes/minimal/pages/article.html create mode 100644 themes/minimal/pages/brand.html create mode 100644 themes/minimal/pages/case.html create mode 100644 themes/minimal/pages/category.html create mode 100644 themes/minimal/pages/contact.html create mode 100644 themes/minimal/pages/faq.html create mode 100644 themes/minimal/pages/home.html create mode 100644 themes/minimal/pages/listing.html create mode 100644 themes/minimal/pages/notfound.html create mode 100644 themes/minimal/pages/product.html create mode 100644 themes/minimal/pages/search.html create mode 100644 themes/minimal/pages/service.html create mode 100644 themes/minimal/pages/standard.html create mode 100644 themes/minimal/pro.css create mode 100644 themes/minimal/pro.html create mode 100644 themes/minimal/site.html create mode 100644 themes/minimal/theme.css create mode 100644 themes/minimal/versions/0.3.0/manifest.json create mode 100644 themes/minimal/versions/0.3.0/page-presets.json create mode 100644 themes/minimal/versions/0.3.0/site.html create mode 100644 themes/minimal/versions/0.3.0/theme.css create mode 100644 themes/minimal/versions/1.0.0/manifest.json create mode 100644 themes/minimal/versions/1.0.0/page-presets.json create mode 100644 themes/minimal/versions/1.0.0/pages/about.html create mode 100644 themes/minimal/versions/1.0.0/pages/article.html create mode 100644 themes/minimal/versions/1.0.0/pages/brand.html create mode 100644 themes/minimal/versions/1.0.0/pages/case.html create mode 100644 themes/minimal/versions/1.0.0/pages/category.html create mode 100644 themes/minimal/versions/1.0.0/pages/contact.html create mode 100644 themes/minimal/versions/1.0.0/pages/faq.html create mode 100644 themes/minimal/versions/1.0.0/pages/home.html create mode 100644 themes/minimal/versions/1.0.0/pages/listing.html create mode 100644 themes/minimal/versions/1.0.0/pages/notfound.html create mode 100644 themes/minimal/versions/1.0.0/pages/product.html create mode 100644 themes/minimal/versions/1.0.0/pages/search.html create mode 100644 themes/minimal/versions/1.0.0/pages/service.html create mode 100644 themes/minimal/versions/1.0.0/pages/standard.html create mode 100644 themes/minimal/versions/1.0.0/pro.css create mode 100644 themes/minimal/versions/1.0.0/pro.html create mode 100644 themes/minimal/versions/1.0.0/site.html create mode 100644 themes/minimal/versions/1.0.0/theme.css create mode 100644 themes/minimal/visual/hero.svg diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..67697e5 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,14 @@ +.git +.venv +.env +.env.* +!.env.example +storage +__pycache__ +.pytest_cache +*.pyc +*.zip +screenshots +*.log + +demo_storage diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..e04ba44 --- /dev/null +++ b/.env.example @@ -0,0 +1,9 @@ +# 复制为 .env 后更换密码和密钥;不要提交 .env 到版本库 +SITEFORGE_PORT=8787 +SITEFORGE_ADMIN_PASSWORD=REPLACE_WITH_STRONG_PASSWORD +SITEFORGE_SESSION_SECRET=REPLACE_WITH_64_RANDOM_CHARACTERS +SITEFORGE_COOKIE_SECURE=false +SITEFORGE_ALLOWED_HOSTS=localhost,127.0.0.1 + +# 只有启用 compose.public.yaml 时才需要填写,先正确设置 DNS 记录 +SITEFORGE_DOMAIN=siteforge.example.com diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ed7d327 --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +.venv/ +storage/ +__pycache__/ +.pytest_cache/ +*.pyc +*.log +.DS_Store +.env +.env.* +!.env.example +secrets/ + +demo_storage/ + +# 本地测试产物 +smoke-*.png +.playwright-mcp/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..3ed8dac --- /dev/null +++ b/Dockerfile @@ -0,0 +1,17 @@ +FROM python:3.12-slim +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + PIP_DISABLE_PIP_VERSION_CHECK=1 \ + SITEFORGE_DATA_DIR=/data +WORKDIR /opt/siteforge +COPY requirements.txt ./ +RUN pip install --no-cache-dir -r requirements.txt && \ + groupadd -g 10001 siteforge && useradd -u 10001 -g 10001 -s /usr/sbin/nologin siteforge && \ + mkdir -p /data /published && chown -R siteforge:siteforge /data /published +COPY --chown=siteforge:siteforge app/ ./app/ +COPY --chown=siteforge:siteforge themes/ ./themes/ +COPY --chown=siteforge:siteforge examples/ ./examples/ +USER siteforge +EXPOSE 8787 +HEALTHCHECK --interval=30s --timeout=5s --start-period=25s --retries=3 CMD python -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8787/api/health', timeout=3)" +CMD ["python","-m","uvicorn","app.main:app","--host","0.0.0.0","--port","8787","--proxy-headers"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..d9e57e7 --- /dev/null +++ b/README.md @@ -0,0 +1,123 @@ +# SiteForge Factory V5 — 四套商业视觉主题升级版 + +> **独立可部署源代码项目**。V5 基于 V4 基于 V3,重点开发需求中的前四个模块:完整行业主题、可视化编辑、数据自动装配、旧站迁移审计。**这是可测试的功能整合版,不等于已经通过生产级安全、真实 CMS 无损迁移和全设备视觉验收。** + +## V5 本轮核心升级 + +四套主题新增商业版视觉体系、内置本地 SVG 示例视觉素材、内容与 Logo 优先展示、响应式细节、交互优化和**独立商业交付检查报告**,主题版本升级为 `1.1.0` 并保留旧版快照。后台 `/theme-gallery` 与正式静态网站共用主题样式资源。详细功能与验收限制请阅读 [docs/COMMERCIAL_THEME_V5.md](docs/COMMERCIAL_THEME_V5.md)。 + +> 这是一套可以用于客户项目试点与二次精修的商业主题交付候选版,**不是已经完成真实企业资料、表单和法律/SEO 验收的正式上线成品**。 + +## 快速开始(Windows + Docker Desktop) + +1. 安装 Docker Desktop,解压项目,在解压根目录打开 PowerShell。 +2. 首次初始化(只执行一次,密码显示在终端并自行妥善保存): + +```powershell +docker run --rm -v "${PWD}:/project" -w /project python:3.12-slim python scripts/init_config.py +``` + +3. 启动管理网站及后台任务 Worker: + +```powershell +docker compose up -d --build +docker compose ps +``` + +4. 浏览器打开 `http://localhost:8787/`,使用初始化后的 `admin` 密码登录。 + +进入 **`/factory-studio`** 查看网站工厂、主题装配、迁移审计。编辑器地址从网站管理工作台进入。主题预览 **`/theme-gallery`**。 + +V5 的 Compose 项目名为 `siteforge-v5`,默认使用单独的 Docker 数据卷,以免意外覆盖 V3。**如果你已经有 V4 数据,先做备份并设计独立迁移步骤,不要通过更改数据卷名直接强行复用。** + +## 能实际做的四条工作流程 + +### A. 无旧站:从零新建网站 + +1. 登录,进入 `/factory-studio`,创建网站并选择 Automotive / Industrial / Corporate / Minimal 主题。 +2. 在网站结构里初始化页面;填写公司介绍、Logo、邮箱、产品、新闻、案例及联系信息。 +3. 在编辑器中选择全页自由排版模式,添加 Hero、图文、动态内容、**响应式多列容器**等组件;列内继续添加组件,支持跨列移动和复用组件。 +4. 选择主题版本,预览,构建并下载静态站 ZIP,或进入现有审批发布流程。 + +### B. 数据生产:Excel、CSV、JSON 自动装配 + +1. 打开 `/factory-studio` 的“内容与自动装配”,上传 XLSX / CSV / JSON。 +2. 系统建议车型、品牌、分类、动力形式、图库、技术参数、SEO 字段的映射;可以手动调整。 +3. 先“预览数据和错误”,确认 URL 冲突、空标题与未映射字段,再执行导入。 +4. 预览“品牌关系”并将缺失的品牌页面创建为空资料页,不伪造企业文案。 +5. 切换主题重新构建,保留已经导入的 URL / SEO / 品牌和参数数据。 + +### C. 旧站迁移:WordPress 与其他来源 + +- WordPress WXR/XML:文章和页面、基础 SEO、公开自定义字段;V4/V5 继续提取特色图片、WXR 媒体 URL、品牌/产品分类、部分 WooCommerce 简单商品属性,以及 Elementor 已知标题/正文/图片组件。**未知 Elementor 组件、ACF 复杂字段、变体/订单及短代码均保留原始数据并产生复核提示。** +- 其他来源:现有 CSV/JSON/XLSX 导入,以及安全受限的公开 HTML 采集。没有对任意 PHP/MySQL/CMS 数据库格式进行通用自动转换。 +- 旧站基线:上传原网站的 URL/SEO CSV、JSON、sitemap XML 或 WXR 文件。CSV 列可包含 `url,seo_title,description,canonical,robots,h1,status`。 +- 系统对 **实际生成的 HTML** 检查缺失 URL、title/description/canonical/robots/H1 差异及静态资源、内部链接。阻断项必须修复,警告必须经具有审核权限的人员填写理由才能通过发布门禁。 + +### D. 主题开发与维护 + +四套内置主题各有 14 种独立模板、响应式 CSS、菜单、产品参数、产品多图切换/大图预览、搜索筛选、页脚,以及可配置的联系表单入口。主题 ZIP 支持受限声明式 CSS/区块预设的版本安装和兼容性检查。**不允许安装执行任意 Jinja/JS/Python 的 ZIP**,避免任意代码执行。 + +联系表单只有在后台设置正式的 HTTPS 外部表单接收地址时才会呈现。SiteForge 默认不会提供虚假的“发送成功”;表单服务的收件、隐私、防垃圾邮件和运维需要自行配置。 + +## 技术实现概要 + +- FastAPI / SQLite / Docker Compose Web + Worker;这是 V3 继承的运行架构,**尚未更换 PostgreSQL/Redis**。 +- 专业主题引擎:四套主题的 `pro.html`、`pro.css` 和各 14 种页面模板。 +- 编辑器:允许 `layout`(1-4 列、最多两层)及列内嵌套安全组件;支持整页替换、全主题预览、PC/平板/手机切换、图片上传、内联文本、按设备隐藏/间距、组件库保存复用。 +- 内容装配:`app/assembly.py`、`app/table_loader.py`、`app/relations.py`。 +- WordPress 特殊数据:`app/wordpress_compat.py`,不执行 Elementor/PHP 代码。 +- 迁移清单/构建差异/人工复核:`app/migration_audit.py`,API `/api/audit/...`。 +- 多站点权限、内容历史、基础审批、静态生成、ZIP / 本地发布延续 V3。 + +## 关键 API + +| API | 功能 | +|---|---| +| `GET /api/factory/themes/{id}/compatibility` | 检查主题资源及 14 种页面结构 | +| `POST /api/factory/sites/{site}/mapping/parse-file` | Excel、CSV、JSON 字段解析,不直接写入数据库 | +| `POST /api/factory/sites/{site}/mapping/run` | 字段映射预览/写入 | +| `GET /api/factory/sites/{site}/relationships` | 产品与品牌分类关系检查 | +| `POST /api/factory/sites/{site}/relationships/assemble` | 创建缺失的空品牌页 | +| `GET/POST /api/sites/{site}/components` | 可复用组件库 | +| `PUT /api/sites/{site}/pages/{page}/blueprint` | 保存区块和整页设计模式 `layout_mode` | +| `PUT /api/sites/{site}/contact-settings` | 联系邮箱和外部 HTTPS 表单配置 | +| `POST /api/audit/sites/{site}/import-baseline` | 导入旧站 URL + SEO 清单 | +| `GET /api/audit/sites/{site}/builds/{build}/report` | 实际构建文件差异报告 | +| `GET /api/audit/sites/{site}/builds/{build}/report.csv` | 下载差异 CSV | +| `POST /api/audit/sites/{site}/builds/{build}/review-warning` | 有理由地审核单条警告 | + +全部管理 API 均需要登录;写入 API 需要 CSRF 校验、站点角色权限。为避免不受控的远程页面运行风险,迁移清单和 WXR 采用离线解析。 + +## 自检 + +```bash +pip install -r requirements-dev.txt +python -m pytest -q +python -m compileall -q app +node --check app/static/editor.js +node --check app/static/factory-v2.js +node --check app/static/pro.js +``` + +Windows 也可以在安装开发依赖后运行 `powershell -ExecutionPolicy Bypass -File scripts/check.ps1`。2026-10-22 使用 Python 3.12 完成回归,当前 **54 项自动化测试全部通过**;四套演示主题的浏览器 E2E 与 Docker 容器仍应在目标部署环境重新验收,不能用源码测试通过代替上线验收。 + +运行依赖使用已经回归验证的精确版本。升级 FastAPI、Starlette、Pydantic 或 HTTPX 时应作为独立变更处理,并重新运行完整测试,避免 Docker 重建时悄然跨越框架兼容边界。 + +## V5 运行安全配置 + +- `SITEFORGE_ALLOWED_HOSTS` 是逗号分隔的后台访问域名白名单;本地 Compose 默认只允许 localhost。公网部署文件会自动加入 `SITEFORGE_DOMAIN`。 +- 管理员密码与团队密码都使用 scrypt 派生后比较;仍建议通过 Docker Secret 或 `SITEFORGE_ADMIN_PASSWORD_FILE` 提供管理员密码。 +- Worker 对临时失败的构建默认最多尝试 3 次,并采用短延迟退避;可通过 `SITEFORGE_JOB_MAX_ATTEMPTS` 调整。 +- 安全响应头允许主题中心进行同源 iframe 预览,但禁止第三方网站嵌入管理后台。 + +## 明确未实现的部分 + +1. 任意外部 CMS 的自动无损迁移;完整 ACF Repeaters、Elementor 全部 widget、WooCommerce 变体/订单/支付、任意 SQL 备份的自动解释。 +2. 任意上传完整可执行代码主题;上传仅限安全声明式主题,独立复杂主题需要开发者审核并放入代码仓库。 +3. 真正任意元素绝对定位、所有断点分别绘制的 Figma 级编辑器;V4 提供可控的列布局与嵌套组件。 +4. 内置表单邮件/CRM 收件服务;需配置受控外部 HTTPS 服务。 +5. Art Design Pro **官方完整工程替换**;当前默认还是 SiteForge Web 管理后台。 +6. PostgreSQL/Redis、多云/CDN 一键发布、正式渗透测试、高可用、监控、真实 Google Search Console 性能保障。 + +**建议首次试点:** 使用一个完整 WordPress 导出(WXR + 媒体备份)以及一份实际车型 Excel,分别做旧站迁移和新站创建,对所有链接、内容、SEO 与手机视图逐项验收,再考虑生产上线。 diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/adapters.py b/app/adapters.py new file mode 100644 index 0000000..40bb662 --- /dev/null +++ b/app/adapters.py @@ -0,0 +1,100 @@ +"""CMS adapter framework: bounded public-site detection and WP REST import. + +Private/paid/SSO content is intentionally not accessed by this adapter. Other +platforms should provide a compatible adapter for their authorised APIs. +""" +from dataclasses import dataclass +from urllib.parse import urljoin,urlparse +from bs4 import BeautifulSoup +import httpx +from .core import ensure_public_url, route_path, save_entry +from . import db + +USER_AGENT='SiteForge/1.0 (+authorized-migration)' + +@dataclass(frozen=True) +class SiteAnalysis: + url:str + platform:str + title:str + rest_api:str|None + hints:list[str] + +def _get(client,url): + ensure_public_url(url) + r=client.get(url,follow_redirects=False) + if r.is_redirect:raise ValueError('重定向目标需由管理员核实后再次扫描') + r.raise_for_status() + if len(r.content)>4_000_000:raise ValueError('响应内容超过限制') + return r + +def analyze(url): + ensure_public_url(url) + with httpx.Client(timeout=httpx.Timeout(13,connect=5),trust_env=False,headers={'User-Agent':USER_AGENT}) as client: + r=_get(client,url) + soup=BeautifulSoup(r.text,'html.parser') + generator=soup.find('meta',attrs={'name':'generator'}) + gen=(generator.get('content','') if generator else '').lower() + rest=soup.find('link',rel='https://api.w.org/') + api_url=rest.get('href') if rest else None + platform='WordPress' if 'wordpress' in gen or api_url or 'wp-content/' in r.text[:200000] else ('Drupal' if 'drupal' in gen else 'Joomla' if 'joomla' in gen else 'Unknown / Custom') + hints=[] + if soup.find('meta',attrs={'name':'robots'}):hints.append('检测到 robots meta') + if soup.find('link',rel='canonical'):hints.append('检测到 Canonical') + if soup.find('script',attrs={'type':'application/ld+json'}):hints.append('检测到结构化数据') + if not api_url and platform=='WordPress':api_url=urljoin(url,'/wp-json/') + return SiteAnalysis(url,platform,soup.title.get_text(' ',strip=True)[:200] if soup.title else '',api_url,hints).__dict__ + +def import_wordpress_rest(site_id,origin,max_items=150,on_conflict='skip'): + """Import public published posts/pages from a WP REST API, preserving URL paths. + + Special Elementor/ACF/Yoast fields require additional adapter support. + """ + if max_items<1 or max_items>1000:raise ValueError('允许导入 1-1000 条公开内容') + if on_conflict not in ('skip','update'):raise ValueError('无效的冲突策略') + root=origin.rstrip('/')+'/' + ensure_public_url(root) + result={'imported':0,'updated':0,'skipped':0,'errors':[]} + with httpx.Client(timeout=httpx.Timeout(18,connect=6),trust_env=False,headers={'User-Agent':USER_AGENT}) as client: + for kind in ('pages','posts'): + batch_size=min(100,max_items) + remaining=max_items-(result['imported']+result['updated']+result['skipped']) + if remaining<=0:break + page=1 + while remaining>0 and page<=20: + api=urljoin(root,f'wp-json/wp/v2/{kind}?per_page={batch_size}&page={page}&_embed=1') + try: + response=_get(client,api) + content=response.json() + if not isinstance(content,list):raise ValueError('API 返回内容不是数组') + except httpx.HTTPStatusError as exc: + if exc.response.status_code in (400,404):break + result['errors'].append({'source':kind,'page':page,'error':str(exc)[:150]});break + except (ValueError,httpx.HTTPError) as exc: + result['errors'].append({'source':kind,'page':page,'error':str(exc)[:150]});break + if not content:break + for item in content: + try: + permalink=item.get('link') or '' + parsed=urlparse(permalink) + if not parsed.path:raise ValueError('未返回页面 URL') + if parsed.hostname!=urlparse(root).hostname:raise ValueError('返回了不同域名的链接,需手动映射') + route=route_path(parsed.path) + title=BeautifulSoup(item.get('title',{}).get('rendered',''),'html.parser').get_text(' ',strip=True) + excerpt=BeautifulSoup(item.get('excerpt',{}).get('rendered',''),'html.parser').get_text(' ',strip=True) + seo=item.get('yoast_head_json') or {} + data={'type':'post' if kind=='posts' else 'page', 'route':route,'title':title, + 'summary':excerpt,'body':item.get('content',{}).get('rendered',''), + 'seo_title':seo.get('title',''),'seo_description':seo.get('description',''), + 'canonical':seo.get('canonical',''),'meta':{'wp_id':item.get('id'),'wp_slug':item.get('slug'),'wp_modified':item.get('modified')}} + previous=db.one('SELECT id FROM entries WHERE site_id=? AND route=?',(site_id,route)) + if previous and on_conflict=='skip':result['skipped']+=1;continue + save_entry(site_id,data,existing_id=previous['id'] if previous else None,source='wp-rest') + result['updated' if previous else 'imported']+=1 + except Exception as exc: + result['errors'].append({'item':item.get('id'),'error':str(exc)[:180]}) + remaining-=1 + if remaining<=0:break + if len(content)100:raise ValueError('字段映射规则不合法') + for dest,src in rules.items(): + if not re.fullmatch(r'[\w\-]{1,60}',dest,re.UNICODE):raise ValueError('目标字段名称不合法') + if not isinstance(src,str) or src not in columns:raise ValueError(f'来源字段不存在: {src}') + if 'title' not in rules:raise ValueError('必须映射标题字段') + + +def transform(rows,rules,kind='product',prefix=None): + if kind not in ('product','post','page','brand','vehicle','service','case','other'):raise ValueError('不支持的内容类型') + columns=set().union(*(r.keys() for r in rows)) if rows else set() + validate_rules(rules,columns) + seen=set();out=[];errors=[] + for index,row in enumerate(rows): + vals={} + for target,src in rules.items(): + raw=row.get(src) + vals[target]=(json.dumps(raw,ensure_ascii=False) if isinstance(raw,(dict,list)) else str(raw if raw is not None else '')).strip() + title=vals.get('title','') + if not title: + errors.append({'row':index+1,'error':'标题为空'});continue + route=vals.get('route','') + if not route: + route=f'/{prefix or kind}/{slug_of(title)}/' + try:route=route_path(route) + except ValueError as exc: + errors.append({'row':index+1,'error':f'路径非法:{exc}'});continue + if route in seen: + errors.append({'row':index+1,'error':'导入文件内 URL 重复:'+route});continue + seen.add(route) + fields={k:v for k,v in vals.items() if k not in SYSTEM_FIELDS} + if fields.get('gallery'): + try: + values=json.loads(fields['gallery']) + if isinstance(values,list):fields['gallery']=[str(x.get('url','') if isinstance(x,dict) else x) for x in values if x] + except ValueError: + fields['gallery']=[x.strip() for x in fields['gallery'].split(';') if x.strip()][:40] + out.append({'type':kind,'title':title,'route':route,'summary':vals.get('summary',''), + 'body':vals.get('body',''),'cover':vals.get('cover',''), + 'seo_title':vals.get('seo_title',''),'seo_description':vals.get('seo_description',''), + 'canonical':vals.get('canonical',''),'meta':{'fields':fields}}) + return out,errors + + +def dry_run(site_id,rows,rules,kind='product',prefix=None): + if len(rows)>1000:raise ValueError('单批最多1000条内容') + items,errors=transform(rows,rules,kind,prefix) + existing={r['route'] for r in db.rows('SELECT route FROM entries WHERE site_id=?',(site_id,))} + collisions=[r['route'] for r in items if r['route'] in existing] + return {'input':len(rows),'mapped':len(items),'errors':errors,'conflicts':collisions, + 'unmapped_columns':sorted(set().union(*(x.keys() for x in rows)) - set(rules.values())) if rows else [], + 'sample':items[:10], 'items':items} + + +def commit(site_id,rows,rules,kind='product',on_conflict='skip',prefix=None): + if on_conflict not in ('skip','replace'):raise ValueError('冲突策略只能为 skip 或 replace') + report=dry_run(site_id,rows,rules,kind,prefix) + created=0;updated=0;skipped=0;errors=list(report['errors']) + for idx,item in enumerate(report['items'],1): + try: + existing=db.one('SELECT id FROM entries WHERE site_id=? AND route=?',(site_id,item['route'])) + if existing and on_conflict=='skip':skipped+=1;continue + save_entry(site_id,item,existing_id=existing['id'] if existing else None,source='mapped-import') + if existing:updated+=1 + else:created+=1 + except Exception as exc: + errors.append({'row':idx,'error':str(exc)[:250]}) + summary={'created':created,'updated':updated,'skipped':skipped,'errors':errors,'mapped':report['mapped']} + from .core import uid + db.insert('INSERT INTO import_batches VALUES (?,?,?,?,?,?)',(uid(),site_id,'',f'content-map:{kind}',json.dumps(summary,ensure_ascii=False),db.now())) + return summary diff --git a/app/backup.py b/app/backup.py new file mode 100644 index 0000000..9db28eb --- /dev/null +++ b/app/backup.py @@ -0,0 +1,35 @@ +"""Consistent SQLite + asset backup. Run: docker compose exec siteforge python -m app.backup""" +import os +import sqlite3 +import tempfile +import zipfile +from pathlib import Path +from datetime import datetime, timezone +from . import db + + +def backup(): + db.init() + storage=Path(db.DATA).resolve() + path=storage/'backups' + path.mkdir(exist_ok=True,parents=True) + name='siteforge-backup-'+datetime.now(timezone.utc).strftime('%Y%m%d-%H%M%S')+'.zip' + dest=path/name + # SQLite online backup is consistent even when the DB uses WAL mode. + with tempfile.TemporaryDirectory() as tmp: + snapshot=Path(tmp)/'siteforge.sqlite3' + with sqlite3.connect(db.DB_FILE) as source, sqlite3.connect(snapshot) as target: + source.backup(target) + with zipfile.ZipFile(dest,'w',compression=zipfile.ZIP_DEFLATED,compresslevel=5) as output: + output.write(snapshot,'siteforge.sqlite3') + for subdir in ('media','legacy','builds','archives'): + root=storage/subdir + if not root.is_dir():continue + for file in root.rglob('*'): + if file.is_file() and not file.is_symlink(): + output.write(file, file.relative_to(storage).as_posix()) + print(f'备份成功: {dest}') + print('注意:备份文件仍在 Docker 数据卷内。请使用 docker compose cp 或其他方式再复制到外部设备。') + return dest + +if __name__=='__main__':backup() diff --git a/app/blueprints.py b/app/blueprints.py new file mode 100644 index 0000000..30b7066 --- /dev/null +++ b/app/blueprints.py @@ -0,0 +1,230 @@ +"""Declarative theme/page blueprints, editor schema, and safe content bindings. +No eval and no arbitrary template code can be submitted by visitors. +""" +import copy +import json +import re +from html import escape +from urllib.parse import urlparse +from .core import sanitize, route_path + +BLOCKS = { + 'hero': {'name':'封面横幅','icon':'✦','fields':['title','subtitle','description','image','button','href','align'], 'defaults':{'title':'让品牌从这里出发','subtitle':'GLOBAL EXPERIENCE','description':'在这里呈现企业最重要的价值。','button':'探索产品','href':'/product/','align':'left'}}, + 'richtext':{'name':'富文本','icon':'≡','fields':['title','body'],'defaults':{'title':'关于我们','body':'

在这里输入图文内容。

'}}, + 'features':{'name':'核心优势','icon':'▦','fields':['title','items'],'defaults':{'title':'我们的优势','items':[{'title':'专业服务','text':'从咨询到交付'},{'title':'品质保障','text':'严格的质量流程'},{'title':'全球网络','text':'连接不同市场'}]}}, + 'stats':{'name':'数字指标','icon':'#','fields':['title','items'],'defaults':{'title':'我们的实力','items':[{'value':'50,000+','label':'服务案例'},{'value':'70+','label':'市场覆盖'},{'value':'30+','label':'合作伙伴'}]}}, + 'collection':{'name':'动态内容列表','icon':'▣','fields':['title','subtitle','source','limit','columns','button','href'],'defaults':{'title':'精选产品','subtitle':'为客户提供适合的解决方案','source':'product','limit':6,'columns':3,'button':'查看全部','href':'/product/'}}, + 'image_text':{'name':'图文组合','icon':'◧','fields':['title','subtitle','description','image','image_side','button','href'],'defaults':{'title':'从采购到交付','subtitle':'OUR APPROACH','description':'展示业务流程和服务理念。','image':'','image_side':'right','button':'了解更多','href':'/about/'}}, + 'gallery':{'name':'图片画廊','icon':'▧','fields':['title','images','columns'],'defaults':{'title':'精彩瞬间','images':[],'columns':3}}, + 'faq':{'name':'常见问题','icon':'?','fields':['title','items'],'defaults':{'title':'常见问题','items':[{'question':'如何开始合作?','answer':'您可以通过联系我们页面提交需求。'}]}}, + 'cta':{'name':'行动号召','icon':'↗','fields':['title','subtitle','button','href'],'defaults':{'title':'开启新的合作','subtitle':'告诉我们您的需求,我们将提供适合的方案。','button':'联系我们','href':'/contact/'}}, + 'attributes':{'name':'参数信息表','icon':'▤','fields':['title'],'defaults':{'title':'产品参数'}}, + 'layout':{'name':'多列自由布局','icon':'▥','fields':['columns','gap'], 'defaults':{'columns':[{'width':1,'blocks':[]},{'width':1,'blocks':[]}],'gap':24}}, + 'spacer':{'name':'上下留白','icon':'↕','fields':['height'],'defaults':{'height':48}}, +} +BINDABLE = ['site.name','site.tagline','site.logo','page.title','page.summary','page.cover','page.body'] +CONTENT_MODELS={ + 'product': [{'name':'brand','label':'品牌','type':'text'},{'name':'model','label':'型号','type':'text'},{'name':'category','label':'分类','type':'text'},{'name':'specs','label':'技术参数','type':'textarea'}], + 'vehicle':[{'name':'brand','label':'品牌','type':'text'},{'name':'energy','label':'能源类型','type':'text'},{'name':'range','label':'续航里程','type':'text'}], + 'post':[{'name':'author','label':'作者','type':'text'},{'name':'publish_date','label':'发布日期','type':'date'}], + 'case':[{'name':'location','label':'项目地点','type':'text'},{'name':'industry','label':'行业','type':'text'}], + 'brand':[{'name':'country','label':'所属地区','type':'text'},{'name':'website','label':'官方网站','type':'text'}] +} +PAGE_TEMPLATES={ + 'home':{'name':'首页','description':'全站形象与动态内容展示'}, + 'about':{'name':'关于我们','description':'企业介绍、优势与里程碑'}, + 'contact':{'name':'联系我们','description':'联系方式与业务咨询'}, + 'listing':{'name':'产品或文章列表','description':'内容查询、列表与分类展示'}, + 'product':{'name':'产品详情','description':'产品简介、图片、技术参数'}, + 'article':{'name':'新闻文章','description':'正文、作者与相关内容'}, + 'brand':{'name':'品牌详情','description':'品牌介绍、品牌产品'}, + 'standard':{'name':'标准内容页','description':'自由组合的自定义页面'}, + 'notfound':{'name':'404','description':'访问异常页面'}, + 'category':{'name':'分类详情','description':'按具体分类查看内容'}, + 'case':{'name':'案例详情','description':'项目介绍、项目图片和成果'}, + 'service':{'name':'服务详情','description':'业务服务介绍'}, + 'faq':{'name':'常见问题','description':'问题与回答'}, +} + +def sample_blocks(kind): + presets={ + 'home':[('hero',{}),('stats',{}),('collection',{}),('features',{}),('cta',{})], + 'about':[('image_text',{'title':'认识我们'}),('stats',{}),('features',{})], + 'contact':[('richtext',{'title':'联系我们','body':'

请填写企业联系方式与地址。

'}),('cta',{'title':'欢迎联系'})], + 'listing':[('collection',{'title':'所有产品','limit':24})], + 'product':[('image_text',{'title':'产品介绍','description':'{{page.summary}}','image':'{{page.cover}}'}),('attributes',{'title':'技术参数'}),('richtext',{'title':'产品详情','body':'{{page.body}}'}),('cta',{})], + 'article':[('richtext',{'title':'{{page.title}}','body':'{{page.body}}'})], + 'brand':[('image_text',{'title':'{{page.title}}','description':'{{page.summary}}','image':'{{page.cover}}'}),('collection',{'title':'品牌产品','source':'product'})], + 'standard':[('richtext',{'title':'{{page.title}}','body':'{{page.body}}'})], + 'notfound':[('cta',{'title':'页面暂时无法访问','href':'/','button':'返回首页'})], + 'category':[('collection',{'source':'product','limit':24})], + 'case':[('image_text',{'title':'{{page.title}}','image':'{{page.cover}}','description':'{{page.summary}}'}),('richtext',{'title':'项目介绍','body':'{{page.body}}'})], + 'service':[('richtext',{'title':'{{page.title}}','body':'{{page.body}}'}),('features',{})], + 'faq':[('faq',{})] + } + return [new_block(k,overrides) for k,overrides in presets.get(kind,presets['standard'])] + +def new_block(kind, overrides=None): + import uuid + if kind not in BLOCKS:raise ValueError('未知区块类型: '+str(kind)) + return {'id':uuid.uuid4().hex[:10], 'type':kind, 'visible':True, **copy.deepcopy(BLOCKS[kind]['defaults']), **(overrides or {})} + +def validate_blocks(blocks): + """Validate nested no-code rows; cap depth, blocks and payload size. + + Children must always be safe declared block types. No arbitrary HTML or JS. + """ + found=set() + total=[0] + def normalize(rows,depth=0): + if not isinstance(rows,list) or len(rows)>60:raise ValueError('区块必须是数组,最多 60 个') + clean=[] + for block in rows: + if not isinstance(block,dict) or block.get('type') not in BLOCKS:raise ValueError('包含不支持的区块类型') + import uuid + total[0]+=1 + if total[0]>120:raise ValueError('页面最多 120 个区块(含列内组件)') + ident=str(block.get('id') or uuid.uuid4().hex[:10]).strip() + if not re.fullmatch('[a-zA-Z0-9_-]{4,50}',ident) or ident in found:raise ValueError('区块 ID 缺失或重复') + found.add(ident) + spec=BLOCKS[block['type']] + allowed=set(spec['fields'])|{'id','type','visible','style'} + normalized={key:val for key,val in block.items() if key in allowed} + normalized['id']=ident + if 'style' in normalized: + style=normalized['style'] + if not isinstance(style,dict):raise ValueError('样式必须为对象') + normalized['style']={k:v for k,v in style.items() if k in ('background','text_color','padding','tablet_padding','mobile_padding','hide_mobile','hide_tablet') and isinstance(v,(str,int,float,bool))} + if block['type']=='layout': + if depth>=2:raise ValueError('多列容器最多嵌套两层') + cols=block.get('columns') + if not isinstance(cols,list) or not 1<=len(cols)<=4:raise ValueError('一行需要1-4列') + sanitized=[] + for col in cols: + if not isinstance(col,dict):raise ValueError('列配置无效') + width=col.get('width',1) + if not isinstance(width,(int,float)) or not .5<=width<=4:raise ValueError('列宽必须在0.5-4之间') + sanitized.append({'width':float(width),'blocks':normalize(col.get('blocks',[]),depth+1)}) + normalized['columns']=sanitized + normalized['gap']=max(0,min(64,int(block.get('gap',24)))) + if len(json.dumps(normalized,ensure_ascii=False))>100_000:raise ValueError('单一区块数据过大') + clean.append(normalized) + return clean + return normalize(blocks) + +def resolve_text(value, site, page): + if not isinstance(value,str):return str(value or '') + def repl(m): + group,name=m.group(1),m.group(2) + if group=='site' and name in ('name','tagline','logo'): return str(site.get(name) or '') + if group=='page' and name in ('title','summary','cover','body'):return str(page.get(name) or '') + return '' + return re.sub(r'\{\{\s*(site|page)\.([a-z_]+)\s*\}\}', repl,value) + +def safe_media(raw): + value=(raw or '').strip() + p=urlparse(value) + if p.scheme and p.scheme not in ('http','https'):return '' + if value.startswith('//') or any(c in value for c in ('"','<','>','\n','\r')):return '' + return escape(value.lstrip('/') if value.startswith('/') else value,quote=True) + +def safe_href(raw): + value=(raw or '').strip() + p=urlparse(value) + if p.scheme and p.scheme not in ('http','https','mailto','tel'):return '/' + if value.startswith('//') or any(c in value for c in ('"','<','>','\n','\r')):return '/' + if not p.scheme: + try:return escape(route_path(value).lstrip('/') or './',quote=True) + except ValueError:return '/' + return escape(value,quote=True) + +def render_blocks(blocks, site, page, entries): + """One trusted rendering implementation for public HTML and editor previews.""" + parts=[] + for block in validate_blocks(blocks): + if block.get('visible') is False:continue + typ=block['type']; resolve=lambda key,default='':resolve_text(block.get(key,default),site,page) + title=escape(resolve('title')); subtitle=escape(resolve('subtitle'));desc=escape(resolve('description')) + raw_style=block.get('style') or {}; s=[] + for key,var in [('background','--sf-bg'),('text_color','--sf-fg')]: + val=str(raw_style.get(key,'')).strip() + if re.fullmatch(r'#[a-fA-F0-9]{6}',val):s.append(f'{var}:{val}') + if 'padding' in raw_style: + try:pad=max(0,min(120,int(raw_style['padding'])));s.append(f'--sf-pad:{pad}px') + except (ValueError,TypeError):pass + style=' style="'+escape(';'.join(s),quote=True)+'"' if s else '' + start=f'
' + end='
' + if typ=='hero': + image=safe_media(resolve('image')) + art=f'
{title}
' if image else '
✧
' + parts.append(start+'
'+art+'
'+end) + elif typ=='richtext': + body=sanitize(resolve('body')) + parts.append(start+f'

{title}

{body}
'+end) + elif typ=='features': + items=block.get('items') if isinstance(block.get('items'),list) else [] + cards=''.join(f'
{i+1:02}

{escape(str(item.get("title","")))}

{escape(str(item.get("text","")))}

' for i,item in enumerate(items[:12]) if isinstance(item,dict)) + parts.append(start+f'

{title}

{cards}
'+end) + elif typ=='stats': + items=block.get('items') if isinstance(block.get('items'),list) else [] + cards=''.join(f'
{escape(str(item.get("value","")))}{escape(str(item.get("label","")))}
' for item in items[:12] if isinstance(item,dict)) + parts.append(start+f'

{title}

{cards}
'+end) + elif typ=='collection': + source=str(block.get('source') or 'product');source=source if source in ('product','post','page','service','case','brand','vehicle','other','all') else 'product' + bound=[r for r in entries if r.get('route')!=page.get('route') and (source=='all' or r.get('type')==source)] + if page.get('type')=='brand' and source in ('product','vehicle'): + def brand_of(record): + try:return str((json.loads(record.get('meta_json') or '{}').get('fields') or {}).get('brand','')).casefold().strip() + except (ValueError,TypeError,AttributeError):return '' + bound=[r for r in bound if brand_of(r)==str(page.get('title','')).casefold().strip()] + count=max(1,min(48,int(block.get('limit') or 6)));cols=max(2,min(4,int(block.get('columns') or 3))) + cards=''.join(f'
'+(f'{escape(r.get(' if safe_media(r.get('cover')) else '
'+escape(r.get('title','')[:2])+'
')+f'
{escape(r.get("type",""))}

{escape(r.get("title",""))}

{escape((r.get("summary") or "")[:140])}

查看详情 →
' for r in bound[:count]) + parts.append(start+f'
{cards if cards else "

当前暂无相关内容,请导入或添加数据。

"}
'+end) + elif typ=='image_text': + im=safe_media(resolve('image')); img=f'{title}' if im else '
✧
' + side=' sf-reverse' if block.get('image_side')=='left' else '' + parts.append(start+f''+end) + elif typ=='attributes': + raw=page.get('meta_json') or '{}' + try: meta=json.loads(raw) if isinstance(raw,str) else raw + except (ValueError,TypeError):meta={} + props=(meta.get('fields') if isinstance(meta.get('fields'),dict) else meta) or {} + specs=(site.get('schemas') or {}).get(page.get('type'),[]) + candidates=[(field['name'],field.get('label') or field['name']) for field in specs] + if not candidates: + candidates=[(k,k.replace('_',' ').title()) for k in props.keys() if k not in ('blocks','page_template','wp_metadata','wp_post_id','wp_type','fields') and isinstance(props[k],(str,int,float,bool))] + pairs=''.join(f'
{escape(str(label))}{escape(str(props[key]))}
' for key,label in candidates if props.get(key) not in (None,'')) + parts.append(start+f'

{title}

{pairs if pairs else "

参数尚未填写

"}
'+end) + elif typ=='gallery': + pics=block.get('images') if isinstance(block.get('images'),list) else [] + imgs=''.join(f'{escape(str(img.get(' for img in pics[:24] if isinstance(img,dict) and safe_media(str(img.get('url') or ''))) + parts.append(start+f'

{title}

'+end) + elif typ=='faq': + items=block.get('items') if isinstance(block.get('items'),list) else [] + body=''.join(f'
{escape(str(item.get("question","")))}

{escape(str(item.get("answer","")))}

' for item in items[:20] if isinstance(item,dict)) + parts.append(start+f'

{title}

{body}
'+end) + elif typ=='cta': + parts.append(start+f'
LET’S CONNECT

{title}

{subtitle}

{escape(resolve("button","联系我们"))} ↗
'+end) + elif typ=='layout': + columns=block['columns'];gap=block.get('gap',24) + weights=' '.join(f'{col["width"]}fr' for col in columns) + html_cols=[] + for col in columns: + html_cols.append('
'+render_blocks(col['blocks'],site,page,entries)+'
') + parts.append(start+f'
'+''.join(html_cols)+'
'+end) + elif typ=='spacer': + try:height=max(8,min(160,int(block.get('height',48)))) + except (ValueError,TypeError):height=48 + parts.append(f'
') + return ''.join(parts) + + +def theme_blocks(theme,kind,version=None): + from pathlib import Path + from . import theme_store + path=theme_store.resolve(theme,version)/'page-presets.json' + if not path.is_file():return sample_blocks(kind) + data=json.loads(path.read_text(encoding='utf-8')) + # IDs must be unique per created page. + return [new_block(b['type'],{k:v for k,v in b.items() if k not in ('type','id')}) for b in data.get(kind, data['standard'])] diff --git a/app/builder.py b/app/builder.py new file mode 100644 index 0000000..e99e1fa --- /dev/null +++ b/app/builder.py @@ -0,0 +1,412 @@ +"""Static site assembly, audit, ZIP export, immutable build versions.""" +import json +import re +import shutil +import hashlib +import mimetypes +import zipfile +from pathlib import Path +from urllib.parse import urlparse, urljoin +from html import escape +from xml.sax.saxutils import escape as xml_escape +from bs4 import BeautifulSoup +import httpx +from jinja2 import Environment, FileSystemLoader, select_autoescape +from markupsafe import Markup +from . import db, blueprints, theme_store, static_site, responsive, theme_pro +from .core import uid, route_path, ensure_public_url, sanitize, THEMES + +ROOT=Path(__file__).resolve().parent.parent +THEME_DIR=ROOT/'themes' +ENV=Environment(loader=FileSystemLoader(str(THEME_DIR)),autoescape=select_autoescape(['html','xml'])) + +TYPE_NAMES={'page':'页面','post':'新闻','product':'产品','service':'服务','case':'案例','brand':'品牌','vehicle':'车型','other':'内容'} + +def output_path(dist,route): + path=route_path(route) + if path=='/':return dist/'index.html' + clean=path.lstrip('/') + # Treat explicit .html/.htm as files and other endpoints as directory/index. + if clean.lower().endswith(('.html','.htm')): + out=dist/clean + else: + out=dist/clean/'index.html' + target=out.resolve() + if not target.is_relative_to(dist.resolve()):raise ValueError('非法生成目标目录') + return out + +def link(path): + return (path or '/').lstrip('/') or './' + +def public_url(site,route): + origin=site['domain'].strip().rstrip('/') + if not origin.startswith(('https://','http://')):return '' + return origin+route + +def render_blocks(blocks): + parts=[] + if not isinstance(blocks,list):return '' + for block in blocks[:30]: + if not isinstance(block,dict):continue + typ=str(block.get('type','text')) + title=escape(str(block.get('title',''))) + sub=escape(str(block.get('subtitle',''))) + body=sanitize(str(block.get('body',''))) + if typ=='hero': + parts.append(f'
{sub}

{title}

{escape(str(block.get("description","")))}

') + elif typ=='features': + features=block.get('items',[]) + cards=''.join(f'
0{i+1}

{escape(str(x.get("title","")))}

{escape(str(x.get("text","")))}

' for i,x in enumerate(features[:12]) if isinstance(x,dict)) + parts.append(f'

{title}

{cards}
') + elif typ=='text': + parts.append(f'

{title}

{body}
') + elif typ=='stats': + vals=''.join(f'
{escape(str(x.get("value","")))}{escape(str(x.get("label","")))}
' for x in block.get('items',[])[:8] if isinstance(x,dict)) + parts.append(f'
{vals}
') + elif typ=='cta': + target=block.get('href','/contact/') + try:target=link(route_path(target)) + except ValueError:target='./' + parts.append(f'
') + return ''.join(parts) + +def url_fix(link_value,origin_host,known_routes): + """Convert imported internal absolute URLs to static-site-relative links.""" + if not link_value:return link_value + parsed=urlparse(link_value) + if parsed.scheme in ('mailto','tel'):return link_value + if parsed.scheme in ('https','http') and parsed.hostname==origin_host and parsed.path in known_routes: + return link(parsed.path) + (('?' + parsed.query) if parsed.query else '') + if not parsed.scheme and link_value.startswith('/') and parsed.path in known_routes: + return link(parsed.path) + return link_value + +def rewrite_internal_links(body,origin_host,known_routes): + soup=BeautifulSoup(body or '', 'html.parser') + for a in soup.find_all('a',href=True): + a['href']=url_fix(a['href'],origin_host,known_routes) + return str(soup) + +def copy_media(site_id,dist): + src=db.DATA/'media'/site_id + target=dist/'media' + if src.exists():shutil.copytree(src,target,dirs_exist_ok=True) + legacy=db.DATA/'legacy'/site_id + if legacy.exists():shutil.copytree(legacy,dist,dirs_exist_ok=True) + +def normalize_local_images(html): + soup=BeautifulSoup(html or '', 'html.parser') + for tag in soup.find_all('img',src=True): + src=tag['src'] + if src.startswith('/') and not src.startswith('//'): + tag['src']=src.lstrip('/') + return str(soup) + +def mirror_images(html,dist,origin_host,client,cache,warnings): + soup=BeautifulSoup(html or '', 'html.parser') + for tag in soup.find_all('img',src=True): + tag['src']=mirror_image_url(tag['src'],dist,origin_host,client,cache,warnings) + return str(soup) + +def mirror_image_url(raw,dist,origin_host,client,cache,warnings): + if not raw:return raw + parsed=urlparse(raw) + if parsed.scheme not in ('http','https'): + if raw.startswith('/'): + return raw.lstrip('/') + return raw + if raw in cache:return cache[raw] + try: + ensure_public_url(raw) + with client.stream('GET',raw,follow_redirects=False) as response: + if response.status_code in (301,302,303,307,308): + raise ValueError('图片重定向需人工检查') + response.raise_for_status() + mime=response.headers.get('content-type','').split(';')[0].lower() + allowed={'image/jpeg':'.jpg','image/png':'.png','image/webp':'.webp','image/gif':'.gif','image/svg+xml':'.svg','image/avif':'.avif'} + if mime not in allowed:raise ValueError(f'图片类型不支持:{mime}') + buf=bytearray() + for chunk in response.iter_bytes(): + buf.extend(chunk) + if len(buf)>8_000_000:raise ValueError('图片超过 8MB') + # SVG can have scripts; disallow external SVG for safety. + if mime=='image/svg+xml':raise ValueError('SVG 外部资源需人工审查') + orig=parsed.path.lstrip('/') + # Original path is preserved for same-domain assets with safe extensions. + if parsed.hostname==origin_host and orig and re.fullmatch(r'[\w\-./%]+',orig) and not any(p in ('.','..') for p in orig.split('/')) and Path(orig).suffix.lower() in allowed.values(): + rel=orig + else: + rel='media/remote-'+hashlib.sha256(raw.encode()).hexdigest()[:16]+allowed[mime] + dst=dist/rel + if not dst.resolve().is_relative_to(dist.resolve()):raise ValueError('图片路径越界') + dst.parent.mkdir(parents=True,exist_ok=True) + dst.write_bytes(buf) + cache[raw]=rel + return rel + except Exception as exc: + warnings.append(f'外部图片未本地化: {raw[:100]} ({str(exc)[:120]})') + return raw + +def emit_nginx(dist,redirects,slashless): + # Include snippets to be inserted inside the real site's `server {}` block. + lines=['# SiteForge 生成:将本文件内容合并到 Nginx server {} 内','index index.html;','error_page 404 /404.html;', + 'location / { try_files $uri $uri/ =404; }'] + for old,new,code in redirects: + lines.append(f'location = {old} {{ return {int(code)} {new}; }}') + for route in slashless: + lines.append(f'location = {route} {{ try_files {route}/index.html =404; }}') + (dist/'nginx-snippet.conf').write_text('\n'.join(lines)+'\n',encoding='utf-8') + +def build(site_id,mirror=False): + site=db.one('SELECT * FROM sites WHERE id=?',(site_id,)) + if not site:raise ValueError('网站不存在') + if site['theme'] not in theme_store.ids():raise ValueError('无效的主题') + records=db.rows("SELECT * FROM entries WHERE site_id=? AND status='published' ORDER BY type,title",(site_id,)) + redirects=db.rows('SELECT * FROM redirects WHERE site_id=?',(site_id,)) + settings=json.loads(site['settings_json'] or '{}') + site['schemas']={k:v for k,v in blueprints.CONTENT_MODELS.items()} + for row in db.rows('SELECT content_type,fields_json FROM content_schemas WHERE site_id=?',(site_id,)): + site['schemas'][row['content_type']]=json.loads(row['fields_json']) + site['email']=settings.get('email','') + site['footer_text']=settings.get('footer_text','') + routes={e['route'] for e in records} + issues=[];warnings=[];slashless=[] + if '/' not in routes:warnings.append('缺少 / 首页:已自动生成默认首页') + if not site['domain'].startswith(('http://','https://')):warnings.append('尚未配置完整域名:未生成绝对 canonical') + route_set=set(routes) + reserved={'/sitemap.xml','/robots.txt','/404.html'} + for e in records: + if e['route'] in reserved:issues.append('页面路由与系统文件冲突: '+e['route']) + if e['route']!='/' and not e['route'].endswith('/') and not e['route'].endswith(('.html','.htm')): + slashless.append(e['route']) + if any(r['old_path'] in route_set for r in redirects):issues.append('重定向来源和有效页面冲突') + if issues:raise ValueError('构建失败:'+';'.join(issues)) + bid=uid() + dist=db.DATA/'builds'/site_id/bid/'dist' + dist.mkdir(parents=True,exist_ok=True) + copy_media(site_id,dist) + (dist/'assets').mkdir(exist_ok=True) + theme_pin=theme_store.pinned(site_id) + theme_dir=theme_store.resolve(site['theme'],theme_pin['version'] if theme_pin and theme_pin['theme_id']==site['theme'] else None) + shutil.copy2(theme_dir/'theme.css',dist/'assets'/'theme.css') + shutil.copy2(ROOT/'app'/'static'/'blocks.css',dist/'assets'/'blocks.css') + shutil.copy2(ROOT/'app'/'static'/'pro-base.css',dist/'assets'/'pro-base.css') + shutil.copy2(ROOT/'app'/'static'/'commercial.css',dist/'assets'/'commercial.css') + visuals=theme_dir/'visual' + if visuals.is_dir(): + shutil.copytree(visuals,dist/'assets'/'visual',dirs_exist_ok=True) + shutil.copy2(ROOT/'app'/'static'/'pro.js',dist/'assets'/'pro.js') + if (theme_dir/'pro.css').exists(): + shutil.copy2(theme_dir/'pro.css',dist/'assets'/'pro.css') + else: + (dist/'assets'/'pro.css').write_text('',encoding='utf-8') + accent=site['accent'] if re.fullmatch(r'#[0-9a-fA-F]{6}',site['accent'] or '') else '#2563eb' + tokens=settings.get('design_tokens') or {} + allowed_fonts={'system':'system-ui,-apple-system,BlinkMacSystemFont,sans-serif', + 'serif':'Georgia,Times New Roman,serif', + 'modern':'Arial,Helvetica,sans-serif'} + font=allowed_fonts.get(tokens.get('font'),'system-ui,-apple-system,sans-serif') + radius=int(max(0,min(30,tokens.get('radius',12)))) + space=int(max(32,min(140,tokens.get('section_spacing',72)))) + width=int(max(900,min(1600,tokens.get('container_width',1200)))) + scale=float(max(.75,min(1.5,tokens.get('heading_scale',1)))) + button={'rounded':f'{radius}px','pill':'999px','square':'0px'}.get(tokens.get('button_style'),f'{radius}px') + theme_css=(f':root{{--accent:{accent};--sf-font:{font};--sf-radius:{radius}px;' + f'--sf-space:{space}px;--sf-width:{width}px;--sf-heading-scale:{scale};}}' + f'body{{font-family:var(--sf-font)}} .sf-canvas .sf-section,.sf-canvas .section{{padding-block:var(--sf-space)}}' + f'.container,.sf-container{{max-width:var(--sf-width)}}' + f'.btn,.primary,.request,.button,.nav-btn{{background-color:{accent};border-radius:{button}}}' + f'body .sf-canvas h1{{font-size:clamp(2.5rem,5vw,calc(4.8rem * var(--sf-heading-scale)))}}') + (dist/'assets'/'custom.css').write_text(theme_css+responsive.site_css(records),encoding='utf-8') + # For installed theme packages, Jinja loads only the trusted included template. + professional_template=(theme_dir/'pro.html').exists() + if professional_template and site['theme'] not in theme_pro.THEMES: + shutil.copy2(theme_dir/'theme.css',dist/'assets'/'theme-overrides.css') + template=ENV.get_template(site['theme']+'/site.html') if site['theme'] in THEMES and theme_dir.resolve()==(THEME_DIR/site['theme']).resolve() else Environment(loader=FileSystemLoader(str(theme_dir)),autoescape=True).get_template('site.html') + orig=urlparse(settings.get('origin') or site['domain']) + origin_host=orig.hostname + local_records=[dict(e) for e in records] + remote_cache={} + try: + with httpx.Client(timeout=httpx.Timeout(10,connect=4),trust_env=False,headers={'User-Agent':'SiteForgeAssetMirror/0.1'}) as client: + for e in local_records: + e['body']=normalize_local_images(rewrite_internal_links(e['body'],origin_host,route_set)) + if mirror: + e['body']=mirror_images(e['body'],dist,origin_host,client,remote_cache,warnings) + if e['cover']: + e['cover']=mirror_image_url(e['cover'],dist,origin_host,client,remote_cache,warnings) + if mirror and site['logo']: + site['logo']=mirror_image_url(site['logo'],dist,origin_host,client,remote_cache,warnings) + except Exception as exc: + warnings.append('资源镜像操作异常: '+str(exc)) + # Derive brand and category landing pages from actual content, not fabricated business data. + # Existing imported URLs always take priority; derived routes never rewrite source records. + derived=[] + product_records=[x for x in local_records if x.get('type') in ('product','vehicle')] + for cat in theme_pro.categories(product_records): + path='/categories/'+theme_pro.slug(cat)+'/' + if path not in route_set: + derived.append({'id':'derived-category','route':path,'title':cat,'category':cat,'type':'page', + 'summary':'分类产品与相关信息','body':'','cover':'','meta_json':'{}','seo_title':'','seo_description':'','canonical':'','robots':'index,follow'}) + route_set.add(path) + for brand in theme_pro.brands(local_records): + if brand['url'] not in route_set: + derived.append({'id':'derived-brand','route':brand['url'],'title':brand['name'],'type':'brand', + 'summary':'相关品牌及产品','body':'','cover':'','meta_json':'{}','seo_title':'','seo_description':'','canonical':'','robots':'index,follow'}) + route_set.add(brand['url']) + # Detail pages, including custom-type documents. + totals=0 + def render_one(page,mode='detail',listing_items=None): + nonlocal totals + page=page or {'title':site['name'],'route':'/','summary':site['tagline'],'body':'','cover':'','seo_title':'','seo_description':'','robots':'index,follow','canonical':'','type':'page','meta_json':'{}'} + meta=json.loads(page.get('meta_json') or '{}') + if not isinstance(meta,dict):meta={} + defined_blocks=meta.get('blocks',[]) + use_blueprint=isinstance(defined_blocks,list) and len(defined_blocks)>0 + blocks_html=blueprints.render_blocks(defined_blocks,site,page,local_records) if use_blueprint else '' + # Starter/scaffold preset blocks are layout defaults, not additional content; + # the full theme already renders the corresponding page family. Keep edited + # blocks, even on starter pages, so the editor remains effective. + if professional_template and use_blueprint and meta.get('layout_mode')!='replace' and page.get('source') in ('starter','site-bootstrap','scaffold'): + kind=theme_pro.infer_kind(page,mode) + try: + def signature(rows): + return [{k:v for k,v in row.items() if k!='id'} for row in rows] + # Starter layouts may belong to any previously selected theme. + is_default=any(signature(defined_blocks)==signature(blueprints.theme_blocks(t,kind)) for t in theme_pro.THEMES) + if not is_default and page.get('source')=='site-bootstrap' and kind=='listing': + # Bootstrap customizes collection title/source only; native listing + # already renders that content without the duplicate block. + is_default=all(b.get('type') in ('collection','spacer') for b in defined_blocks) + if is_default:blocks_html='' + except (ValueError,TypeError,KeyError):pass + canon=page.get('canonical') or public_url(site,page['route']) + if professional_template: + # Version-resolved trusted templates; uploaded themes cannot execute arbitrary code. + if site['theme'] in theme_pro.THEMES and theme_dir.resolve()==(THEME_DIR/site['theme']).resolve(): + html=theme_pro.render(site['theme'],site,page,local_records,mode,listing_items,blocks_html) + else: + env=Environment(loader=FileSystemLoader([str(theme_dir),str(THEME_DIR)]),autoescape=select_autoescape(['html'])) + ctx=theme_pro.make_context(site,page,local_records,mode,listing_items) + ctx.update(custom_blocks=Markup(blocks_html),has_custom=bool(blocks_html),noindex=(ctx['kind']=='notfound'),theme_overrides=True,designer_replace=meta.get('layout_mode')=='replace') + html=env.get_template('pro.html').render(**ctx) + else: + html=template.render(site=site,page=page,mode=mode,content=Markup(page.get('body') or ''), + blocks=Markup(blocks_html),blueprint_mode=use_blueprint,entries=local_records,listing_items=listing_items or [], + type_names=TYPE_NAMES,nav=settings.get('nav') or [e for e in local_records if e['type']=='page' and e['route'] in ('/','/about/','/about-us/','/contact/','/contact-us/','/services/','/products/')], + link=link, asset=lambda x: x.lstrip('/') if x.startswith('/') else x, canonical=canon, seo_title=page.get('seo_title') or page['title'], + seo_description=page.get('seo_description') or page.get('summary') or site['tagline']) + html=static_site.shell(html,site,page,local_records,settings.get('nav') or []) + target=output_path(dist,page['route']) + if target.exists():raise ValueError('多个路由写入同一文件: '+page['route']) + target.parent.mkdir(parents=True,exist_ok=True) + target.write_text(html,encoding='utf-8') + totals+=1 + for e in local_records:render_one(e,'home' if e['route']=='/' else 'detail') + for e in derived:render_one(e,'detail') + if '/' not in routes:render_one(None,'home') + for typ in sorted({e['type'] for e in local_records if e['type']!='page'} | {'product'}): + list_route='/'+typ+'/' + if list_route in route_set:continue + itemlist=[e for e in local_records if e['type']==typ] + render_one({'type':'page','route':list_route,'title':TYPE_NAMES.get(typ,typ)+'展示','summary':'Explore our '+typ,'meta_json':'{}'},'listing',itemlist) + route_set.add(list_route) + notfound={'title':'404 · 页面不存在','route':'/404.html','summary':'','body':'','cover':'','meta_json':'{}','robots':'noindex,follow'} + if professional_template: + if site['theme'] in theme_pro.THEMES and theme_dir.resolve()==(THEME_DIR/site['theme']).resolve(): + missing_html=theme_pro.render(site['theme'],site,notfound,local_records,'notfound') + else: + env=Environment(loader=FileSystemLoader([str(theme_dir),str(THEME_DIR)]),autoescape=select_autoescape(['html'])) + ctx=theme_pro.make_context(site,notfound,local_records,'notfound') + ctx.update(custom_blocks=Markup(''),has_custom=False,noindex=True,theme_overrides=True) + missing_html=env.get_template('pro.html').render(**ctx) + else: + missing_html=template.render(site=site,page=notfound,mode='notfound',content='',blocks='',blueprint_mode=False,entries=local_records,listing_items=[],type_names=TYPE_NAMES,nav=[],link=link,seo_title='404',seo_description='Page not found',canonical='') + (dist/'404.html').write_text(missing_html,encoding='utf-8') + # A complete static search page is packaged for every theme, not a live API. + static_site.write_assets(dist,local_records) + if '/search/' not in route_set: + search_file=dist/'search'/'index.html' + search_file.parent.mkdir(parents=True,exist_ok=True) + search_entry={'title':'站内搜索','route':'/search/','type':'page','summary':'搜索本站内容','body':'','meta_json':'{}','robots':'noindex,follow'} + if professional_template: + if site['theme'] in theme_pro.THEMES and theme_dir.resolve()==(THEME_DIR/site['theme']).resolve(): + search_html=theme_pro.render(site['theme'],site,search_entry,local_records) + else: + env=Environment(loader=FileSystemLoader([str(theme_dir),str(THEME_DIR)]),autoescape=select_autoescape(['html'])) + ctx=theme_pro.make_context(site,search_entry,local_records) + ctx.update(custom_blocks=Markup(''),has_custom=False,noindex=True,theme_overrides=True) + search_html=env.get_template('pro.html').render(**ctx) + else: + search_html=static_site.search_page(site) + search_file.write_text(search_html,encoding='utf-8') + route_set.add('/search/') + noindex_routes={e['route'] for e in records if 'noindex' in (e['robots'] or '').lower()} | {'/search/'} + canonical_routes=sorted([x for x in route_set if not x.startswith('/404') and x not in noindex_routes]) + if site['domain'].startswith(('https://','http://')): + urls='\n'.join(''+xml_escape(public_url(site,r))+'' for r in canonical_routes) + sitemap='\n'+urls+'' + (dist/'sitemap.xml').write_text(sitemap,encoding='utf-8') + (dist/'robots.txt').write_text('User-agent: *\nAllow: /\n'+(f'Sitemap: {site["domain"].rstrip("/")}/sitemap.xml\n' if site['domain'].startswith('http') else ''),encoding='utf-8') + rlist=[] + for r in redirects: + old,new=route_path(r['old_path']),r['new_path'] + if not (new.startswith('/') or new.startswith('https://')):raise ValueError('非法重定向目标') + rlist.append((old,new,int(r['status_code']))) + emit_nginx(dist,rlist,slashless) + (dist/'.htaccess').write_text('Options -MultiViews\nRewriteEngine On\n'+''.join(f'RewriteRule ^{re.escape(old.lstrip("/"))}$ {new} [R={code},L,NE]\n' for old,new,code in rlist),encoding='utf-8') + # Evaluate packaged files; external src references are reported (not silently asserted migrated). + external=[];missing=[];broken_links=[];legacy_links=[];canonical_mismatches=[] + target_host=urlparse(site['domain']).hostname + for e in records: + c=urlparse(e['canonical'] or '') + if c.hostname and target_host and c.hostname != target_host: + canonical_mismatches.append(e['route']) + for f in dist.rglob('*.html'): + soup=BeautifulSoup(f.read_text(encoding='utf-8'),'html.parser') + for a in soup.select('a[href]'): + href=a.get('href','') + p=urlparse(href) + if p.scheme in ('http','https'): + if p.hostname==origin_host and p.path not in route_set and href not in legacy_links: + legacy_links.append(href) + elif href and href not in ('.','./','index.html') and not href.startswith(('#','mailto:','tel:','javascript:')): + candidate=p.path + if candidate and not candidate.startswith('/'): + candidate='/'+candidate + if candidate and candidate not in route_set and candidate not in ('/index.html',) and not (dist/candidate.lstrip('/')).is_file() and candidate not in broken_links: + broken_links.append(candidate) + for img in soup.select('img[src]'): + src=img.get('src','') + if src.startswith(('http://','https://')): + if src not in external:external.append(src) + else: + rel=src.lstrip('/') + if rel and not (dist/rel).is_file() and rel not in missing:missing.append(rel) + if external:warnings.append(f'{len(external)} 个外部图片 URL 尚未打包(可开启镜像重建)') + if broken_links:warnings.append(f'{len(broken_links)} 个内部链接无对应页面或文件') + if legacy_links:warnings.append(f'{len(legacy_links)} 个指向源站但未能映射的链接,需人工核验') + if canonical_mismatches:warnings.append(f'{len(canonical_mismatches)} 个页面 canonical 域名与新站域名不一致') + if missing:warnings.append(f'{len(missing)} 个本地引用图片缺失') + if slashless:warnings.append(f'{len(slashless)} 个无后缀、无尾斜杠 URL 需要按照生成的 Nginx 规则部署') + report={'site':site['name'],'version':bid,'pages':totals,'theme':site['theme'],'theme_version':theme_pin['version'] if theme_pin else theme_store.latest(site['theme']), + 'warnings':warnings,'errors':issues,'external_assets':external[:200], + 'missing_local_images':missing[:200],'broken_links':broken_links[:200],'legacy_links':legacy_links[:200],'canonical_mismatches':canonical_mismatches[:200],'slashless_routes':slashless, + 'redirects':len(redirects),'mirrored_images':len(remote_cache), + 'quality_status':'warning' if warnings else 'passed','blueprint_blocks':sum(len(json.loads(x.get('meta_json') or '{}').get('blocks',[])) for x in local_records)} + # Separate static quality from readiness for commercial hand-off. + from . import commercial_readiness + handoff=commercial_readiness.inspect(dist) + report['commercial_readiness']={'ready_for_review':handoff['ready_for_review'], + 'commercially_certified':False,'blockers':len(handoff['blockers']), + 'warnings':len(handoff['warnings'])} + (dist/'commercial-readiness.json').write_text(json.dumps(handoff,ensure_ascii=False,indent=2),encoding='utf-8') + (dist/'siteforge-report.json').write_text(json.dumps(report,ensure_ascii=False,indent=2),encoding='utf-8') + archive_dir=db.DATA/'archives';archive_dir.mkdir(parents=True,exist_ok=True) + archive=archive_dir/(site_id+'-'+bid+'.zip') + with zipfile.ZipFile(archive,'w',compression=zipfile.ZIP_DEFLATED,compresslevel=6) as z: + for file in dist.rglob('*'): + if file.is_file():z.write(file,file.relative_to(dist)) + db.insert('INSERT INTO builds (id,site_id,created_at,status,total_pages,report_json,zip_path) VALUES (?,?,?,?,?,?,?)', + (bid,site_id,db.now(),'complete',totals,json.dumps(report,ensure_ascii=False),str(archive))) + return report diff --git a/app/commercial_readiness.py b/app/commercial_readiness.py new file mode 100644 index 0000000..ff2a80d --- /dev/null +++ b/app/commercial_readiness.py @@ -0,0 +1,48 @@ +"""Commercial-delivery readiness report, separate from technical/SEO pass. + +It does NOT certify a site's business claims, copyright clearance, legal compliance, +WCAG conformance or any real-world server/SEO outcome. It reports missing items. +""" +from __future__ import annotations +from pathlib import Path +from bs4 import BeautifulSoup +import json + +REQUIRED=('index.html','about/index.html','contact/index.html','products/index.html','brands/index.html','news/index.html','cases/index.html','404.html') +DEMO_MARKERS=('example.com','example.test','这是可替换的','虚构示例','演示内容','演示产品','此为 SiteForge','请在后台配置') + +def inspect(dist:Path)->dict: + dist=Path(dist) + blockers=[];warnings=[];metrics={} + for f in REQUIRED: + if not (dist/f).is_file():blockers.append({'code':'page_missing','path':f,'message':'缺少标准栏目页面'}) + homepage=dist/'index.html' + if homepage.is_file(): + soup=BeautifulSoup(homepage.read_text(encoding='utf-8'),'html.parser') + logo=soup.select_one('header img.pro-brand-logo') + if not logo:warnings.append({'code':'brand_logo','message':'尚未替换成企业正式 Logo'}) + if not soup.find('meta',attrs={'name':'description'}):blockers.append({'code':'seo_description','message':'首页缺少 SEO 描述标签'}) + if len(soup.select('h1'))!=1:blockers.append({'code':'home_h1','message':'首页 H1 数量不符合预期'}) + else:soup=None + htmls=list(dist.rglob('*.html')) + texts=' '.join(p.read_text(encoding='utf-8',errors='replace') for p in htmls) + metrics['html_pages']=len(htmls) + metrics['artwork_fallbacks']=sum(x.count('class="pro-art ') for x in [texts]) + metrics['pages_with_body']=sum(bool(BeautifulSoup(p.read_text(encoding='utf-8'),'html.parser').select_one('.pro-rich')) for p in htmls) + if metrics['artwork_fallbacks']: + warnings.append({'code':'placeholder_images','count':metrics['artwork_fallbacks'],'message':'部分页面仍使用示意图,正式发布前请替换为真实且有使用权的图片'}) + if any(marker in texts for marker in DEMO_MARKERS): + warnings.append({'code':'demo_content','message':'检测到演示或默认文案,正式发布前请逐页核对'}) + if not (dist/'assets'/'commercial.css').is_file(): + warnings.append({'code':'commercial_style','message':'当前构建未包含商业视觉样式(可能是历史主题版本)'}) + contact=dist/'contact/index.html' + if contact.is_file(): + con=BeautifulSoup(contact.read_text(encoding='utf-8'),'html.parser') + if not con.select_one('form.pro-inquiry') and not con.select_one('a[href^="mailto:"]'): + blockers.append({'code':'contact_method','message':'联系页既无已配置表单,也无有效电子邮箱入口'}) + elif not con.select_one('form.pro-inquiry'): + warnings.append({'code':'form_not_configured','message':'尚未配置 HTTPS 询盘接收服务;目前仅提供邮件联系'}) + if not any((dist/p).is_file() for p in ('privacy/index.html','privacy-policy/index.html')): + warnings.append({'code':'legal_pages','message':'请根据目标国家/地区法律审查是否需要隐私政策与 Cookie 声明'}) + return {'ready_for_review':not blockers,'commercially_certified':False,'blockers':blockers,'warnings':warnings, + 'metrics':metrics,'note':'仅为本地结构化交付清单,不等于合法合规、内容准确、无障碍、性能或生产上线认证'} diff --git a/app/core.py b/app/core.py new file mode 100644 index 0000000..a36d7c2 --- /dev/null +++ b/app/core.py @@ -0,0 +1,109 @@ +"""Shared site/route/content rules, with content sanitization.""" +import ipaddress +import json +import re +import socket +import uuid +from urllib.parse import urlparse, unquote +import bleach +from . import db + +TYPES = ('page','product','post','service','case','brand','vehicle','other') +THEMES = ('automotive','industrial','corporate','minimal') +ALLOWED_TAGS = ['p','br','b','i','u','em','strong','h1','h2','h3','h4','h5','h6','ul','ol','li','blockquote','pre','code','a','img','figure','figcaption','table','thead','tbody','tr','th','td','hr','div','span'] +ALLOWED_ATTRS = {'a':['href','title','target','rel'], 'img':['src','alt','title','loading','width','height'], 'td':['colspan','rowspan'], 'th':['colspan','rowspan']} + +def uid(): + return uuid.uuid4().hex[:12] + +def sanitize(html): + return bleach.clean(html or '', tags=ALLOWED_TAGS, attributes=ALLOWED_ATTRS, protocols=['http','https','mailto'], strip=True) + +def route_path(value, default='/'): + s = (value or default).strip() + if s.startswith(('http://','https://')): + s = urlparse(s).path or '/' + # Query strings cannot be represented as ordinary static file routes. + s = s.split('?',1)[0].split('#',1)[0] + if not s.startswith('/'): + s = '/' + s + if '\\' in s or '\x00' in s or any(part in ('.','..') for part in unquote(s).split('/')): + raise ValueError('不允许路径穿越或反斜杠') + if s.startswith(('/api/','/preview/','/__siteforge/')): + raise ValueError('该路径已保留给系统') + if len(s)>500: + raise ValueError('URL 路径过长') + return s + +def slug_of(title): + s = re.sub(r'[^\w\-]+','-', title.strip().lower(), flags=re.UNICODE).strip('-') + return s[:70] or uid() + +def clean_hex(s): + if not re.fullmatch(r'#[0-9a-fA-F]{6}', s or ''): + raise ValueError('品牌色需要 #RRGGBB 格式') + return s + +def ensure_public_url(url): + parsed=urlparse(url) + if parsed.scheme not in ('https','http') or not parsed.hostname or parsed.username or parsed.password: + raise ValueError('仅支持公开的 HTTP/HTTPS URL') + hostname=parsed.hostname.lower() + if hostname in ('localhost','localhost.localdomain') or hostname.endswith(('.local','.internal')): + raise ValueError('禁止访问内网域名') + try: + addrs=socket.getaddrinfo(hostname, parsed.port or (443 if parsed.scheme=='https' else 80), type=socket.SOCK_STREAM) + for info in addrs: + ip=ipaddress.ip_address(info[4][0]) + if not ip.is_global: + raise ValueError('不允许访问内网、回环或保留 IP 地址') + except socket.gaierror: + raise ValueError('无法解析域名') + return url + +def save_entry(site_id, data, existing_id=None, source='manual', replace_route=False): + if not db.one('SELECT id FROM sites WHERE id=?',(site_id,)): + raise ValueError('网站不存在') + kind=str(data.get('type') or 'page').lower() + if kind not in TYPES: + kind='other' + title=str(data.get('title') or '').strip() + if not title: + raise ValueError('内容标题不能为空') + route=route_path(data.get('route'), '/' if kind=='page' and title.lower() in ('home','首页') else f'/{kind}/{slug_of(title)}/') + if route=='/' and kind!='page': + raise ValueError('首页必须是普通页面类型') + old=db.one('SELECT * FROM entries WHERE site_id=? AND route=?',(site_id,route)) + if old and old['id'] != existing_id: + if replace_route: + existing_id=old['id'] + else: + raise ValueError(f'URL 冲突:{route}') + eid=existing_id or uid() + prior=db.one('SELECT * FROM entries WHERE site_id=? AND id=?',(site_id,eid)) if existing_id else None + if existing_id and not prior: + raise ValueError('内容不存在') + meta=data.get('meta', {}) + if isinstance(meta,str): + try: meta=json.loads(meta) + except json.JSONDecodeError: meta={} + if not isinstance(meta,dict): meta={} + if 'blocks' in meta: + from .blueprints import validate_blocks + meta['blocks']=validate_blocks(meta['blocks']) + vals=(kind,title,route,slug_of(title),str(data.get('summary') or ''),sanitize(str(data.get('body') or '')), + str(data.get('cover') or ''),str(data.get('seo_title') or ''),str(data.get('seo_description') or ''), + str(data.get('canonical') or ''),str(data.get('robots') or 'index,follow'),json.dumps(meta,ensure_ascii=False), + str(data.get('status') or 'published')) + if prior: + db.execute('''UPDATE entries SET type=?,title=?,route=?,slug=?,summary=?,body=?,cover=?,seo_title=?,seo_description=?,canonical=?,robots=?,meta_json=?,status=?,updated_at=? WHERE site_id=? AND id=?''',(*vals,db.now(),site_id,eid)) + else: + db.insert('''INSERT INTO entries (type,title,route,slug,summary,body,cover,seo_title,seo_description,canonical,robots,meta_json,status,created_at,updated_at,site_id,id,source) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)''',(*vals,db.now(),db.now(),site_id,eid,source)) + return eid + +def entry_dict(e): + v=dict(e) + try:v['meta']=json.loads(v.pop('meta_json')) + except (ValueError,TypeError):v['meta']={} + return v diff --git a/app/db.py b/app/db.py new file mode 100644 index 0000000..d4cf5e5 --- /dev/null +++ b/app/db.py @@ -0,0 +1,93 @@ +"""Local SQLite data store for SiteForge MVP. One database, hard site_id isolation.""" +import os +import sqlite3 +import json +from pathlib import Path +from datetime import datetime, timezone + +ROOT = Path(__file__).resolve().parent.parent +DATA = Path(os.getenv('SITEFORGE_DATA_DIR', str(ROOT / 'storage'))).resolve() +DB_FILE = DATA / 'siteforge.sqlite3' + +def now(): + return datetime.now(timezone.utc).isoformat(timespec='seconds') + +def connect(): + DATA.mkdir(parents=True, exist_ok=True) + c = sqlite3.connect(DB_FILE) + c.row_factory = sqlite3.Row + c.execute('PRAGMA foreign_keys = ON') + c.execute('PRAGMA busy_timeout=5000') + return c + +def init(): + with connect() as c: + c.executescript(''' + PRAGMA journal_mode=WAL; + CREATE TABLE IF NOT EXISTS sites ( + id TEXT PRIMARY KEY, name TEXT NOT NULL, domain TEXT NOT NULL DEFAULT '', + tagline TEXT NOT NULL DEFAULT '', theme TEXT NOT NULL DEFAULT 'corporate', + accent TEXT NOT NULL DEFAULT '#2563eb', logo TEXT NOT NULL DEFAULT '', + mode TEXT NOT NULL DEFAULT 'new', + settings_json TEXT NOT NULL DEFAULT '{}', + created_at TEXT NOT NULL, updated_at TEXT NOT NULL + ); + CREATE TABLE IF NOT EXISTS entries ( + id TEXT PRIMARY KEY, site_id TEXT NOT NULL REFERENCES sites(id) ON DELETE CASCADE, + type TEXT NOT NULL, title TEXT NOT NULL, route TEXT NOT NULL, slug TEXT NOT NULL, + summary TEXT NOT NULL DEFAULT '', body TEXT NOT NULL DEFAULT '', + cover TEXT NOT NULL DEFAULT '', seo_title TEXT NOT NULL DEFAULT '', + seo_description TEXT NOT NULL DEFAULT '', canonical TEXT NOT NULL DEFAULT '', + robots TEXT NOT NULL DEFAULT 'index,follow', + meta_json TEXT NOT NULL DEFAULT '{}', status TEXT NOT NULL DEFAULT 'published', + source TEXT NOT NULL DEFAULT 'manual', created_at TEXT NOT NULL, updated_at TEXT NOT NULL, + UNIQUE(site_id, route) + ); + CREATE INDEX IF NOT EXISTS idx_entries_site ON entries(site_id, type, status); + CREATE TABLE IF NOT EXISTS redirects ( + id TEXT PRIMARY KEY, site_id TEXT NOT NULL REFERENCES sites(id) ON DELETE CASCADE, + old_path TEXT NOT NULL, new_path TEXT NOT NULL, status_code INTEGER NOT NULL DEFAULT 301, + UNIQUE(site_id, old_path) + ); + CREATE TABLE IF NOT EXISTS builds ( + id TEXT PRIMARY KEY, site_id TEXT NOT NULL REFERENCES sites(id) ON DELETE CASCADE, + created_at TEXT NOT NULL, status TEXT NOT NULL, total_pages INTEGER NOT NULL DEFAULT 0, + report_json TEXT NOT NULL DEFAULT '{}', zip_path TEXT NOT NULL DEFAULT '' + ); + CREATE INDEX IF NOT EXISTS idx_builds_site ON builds(site_id, created_at); + CREATE TABLE IF NOT EXISTS presets ( + id TEXT PRIMARY KEY,name TEXT NOT NULL,theme TEXT NOT NULL, + settings_json TEXT NOT NULL,home_blocks_json TEXT NOT NULL,created_at TEXT NOT NULL + ); + CREATE TABLE IF NOT EXISTS content_schemas ( + site_id TEXT NOT NULL REFERENCES sites(id) ON DELETE CASCADE, + content_type TEXT NOT NULL, fields_json TEXT NOT NULL DEFAULT '[]', + PRIMARY KEY(site_id,content_type) + ); + CREATE TABLE IF NOT EXISTS imports ( + id TEXT PRIMARY KEY, site_id TEXT NOT NULL REFERENCES sites(id) ON DELETE CASCADE, + created_at TEXT NOT NULL, source TEXT NOT NULL, + result_json TEXT NOT NULL DEFAULT '{}' + ); + ''') + +def rows(sql, params=()): + with connect() as c: + return [dict(r) for r in c.execute(sql, params).fetchall()] + +def one(sql, params=()): + with connect() as c: + r=c.execute(sql, params).fetchone() + return dict(r) if r else None + +def execute(sql, params=()): + with connect() as c: + cur = c.execute(sql, params) + return cur.rowcount + +def insert(sql, params=()): + with connect() as c: + c.execute(sql, params) + +if __name__ == '__main__': + init() diff --git a/app/enterprise.py b/app/enterprise.py new file mode 100644 index 0000000..a8d1d4e --- /dev/null +++ b/app/enterprise.py @@ -0,0 +1,338 @@ +"""SiteForge team accounts, RBAC, content history, audit, jobs, migration baseline and release governance. + +This module is deliberately database-backed and independent of the frontend. It +extends the existing site factory without allowing editors to bypass permissions. +""" +import csv +import hashlib +import hmac +import io +import json +import os +import secrets +import sqlite3 +import uuid +from pathlib import Path +from urllib.parse import urlparse +from fastapi import APIRouter, HTTPException, Request, UploadFile, File, Body +from fastapi.responses import Response +from pydantic import BaseModel, Field +from . import db +from .core import route_path + +router = APIRouter(prefix='/api/enterprise', tags=['Enterprise']) +ROLES = ('owner','editor','reviewer','viewer') +ACCESS = {'viewer':0, 'reviewer':1, 'editor':2, 'owner':3, 'superadmin':4} + +def setup(): + with db.connect() as c: + c.executescript(''' + CREATE TABLE IF NOT EXISTS team_users ( + id TEXT PRIMARY KEY, username TEXT UNIQUE NOT NULL, password_hash TEXT NOT NULL, + display_name TEXT NOT NULL, disabled INTEGER NOT NULL DEFAULT 0, + created_at TEXT NOT NULL); + CREATE TABLE IF NOT EXISTS team_memberships ( + user_id TEXT NOT NULL REFERENCES team_users(id) ON DELETE CASCADE, + site_id TEXT NOT NULL REFERENCES sites(id) ON DELETE CASCADE, + role TEXT NOT NULL, PRIMARY KEY(user_id,site_id)); + CREATE TABLE IF NOT EXISTS audit_events ( + id TEXT PRIMARY KEY, actor TEXT NOT NULL, site_id TEXT, operation TEXT NOT NULL, + path TEXT NOT NULL, status INTEGER NOT NULL, created_at TEXT NOT NULL); + CREATE INDEX IF NOT EXISTS idx_audit_time ON audit_events(created_at); + CREATE TABLE IF NOT EXISTS content_revisions ( + id TEXT PRIMARY KEY, site_id TEXT NOT NULL, entry_id TEXT NOT NULL, + actor TEXT NOT NULL, action TEXT NOT NULL, snapshot_json TEXT NOT NULL, + created_at TEXT NOT NULL); + CREATE INDEX IF NOT EXISTS idx_revision_entry ON content_revisions(site_id,entry_id,created_at); + CREATE TABLE IF NOT EXISTS build_jobs ( + id TEXT PRIMARY KEY, site_id TEXT NOT NULL REFERENCES sites(id) ON DELETE CASCADE, + kind TEXT NOT NULL, state TEXT NOT NULL, attempts INTEGER NOT NULL DEFAULT 0, + error TEXT NOT NULL DEFAULT '', result_json TEXT NOT NULL DEFAULT '{}', + created_by TEXT NOT NULL, created_at TEXT NOT NULL, started_at TEXT, finished_at TEXT, + retry_at TEXT); + CREATE INDEX IF NOT EXISTS idx_build_jobs_state ON build_jobs(state,created_at); + CREATE TABLE IF NOT EXISTS migration_baseline ( + site_id TEXT NOT NULL REFERENCES sites(id) ON DELETE CASCADE, + route TEXT NOT NULL, seo_title TEXT NOT NULL DEFAULT '', + description TEXT NOT NULL DEFAULT '', canonical TEXT NOT NULL DEFAULT '', + status INTEGER NOT NULL DEFAULT 200, PRIMARY KEY(site_id,route)); + CREATE TABLE IF NOT EXISTS release_history ( + id TEXT PRIMARY KEY, site_id TEXT NOT NULL REFERENCES sites(id) ON DELETE CASCADE, + build_id TEXT NOT NULL, actor TEXT NOT NULL, action TEXT NOT NULL, + created_at TEXT NOT NULL); + ''') + # Lightweight forward migration for V2-V5 databases created before + # delayed job retries were introduced. + columns={row['name'] for row in c.execute('PRAGMA table_info(build_jobs)').fetchall()} + if 'retry_at' not in columns: + c.execute('ALTER TABLE build_jobs ADD COLUMN retry_at TEXT') + +def uid(): return uuid.uuid4().hex[:16] + +def hash_password(password, salt=None): + if len(password)<12: raise ValueError('密码至少12位') + salt=salt or secrets.token_bytes(16) + digest=hashlib.scrypt(password.encode(),salt=salt,n=2**14,r=8,p=1,dklen=32) + return f'scrypt$16384$8$1${salt.hex()}${digest.hex()}' + +def check_password(password, encoded): + try: + algorithm,n,r,p,salt,expected=encoded.split('$') + if algorithm!='scrypt':return False + result=hashlib.scrypt(password.encode(),salt=bytes.fromhex(salt),n=int(n),r=int(r),p=int(p),dklen=32) + return hmac.compare_digest(result,bytes.fromhex(expected)) + except (ValueError,TypeError,MemoryError):return False + +def actor(request): + return request.state.actor if hasattr(request.state,'actor') else 'system' + +def level(request, site_id=None): + user=getattr(request.state,'account_id',None) + if not user: return ACCESS['superadmin'] if getattr(request.state,'is_superadmin',False) else -1 + if not site_id:return ACCESS['viewer'] + item=db.one('SELECT role FROM team_memberships WHERE site_id=? AND user_id=?',(site_id,user)) + return ACCESS.get(item['role'],-1) if item else -1 + +def require(request, site_id=None, role='viewer'): + if level(request,site_id)20000:raise HTTPException(413,'URL 基线最多20000条') + routes=set();rows=[] + for item in payload: + route=route_path(item.route) + if route in routes:raise HTTPException(400,'存在重复 URL:'+route) + routes.add(route) + rows.append((site_id,route,item.seo_title,item.description,item.canonical,item.status)) + with db.connect() as c: + c.execute('DELETE FROM migration_baseline WHERE site_id=?',(site_id,)) + c.executemany('INSERT INTO migration_baseline VALUES (?,?,?,?,?,?)',rows) + return {'saved':len(rows)} + +@router.post('/sites/{site_id}/baseline/snapshot') +def baseline_snapshot(request:Request,site_id:str): + require(request,site_id,'editor') + entries=db.rows('SELECT route,seo_title,seo_description,canonical FROM entries WHERE site_id=?',(site_id,)) + with db.connect() as c: + c.execute('DELETE FROM migration_baseline WHERE site_id=?',(site_id,)) + c.executemany('INSERT INTO migration_baseline VALUES (?,?,?,?,?,?)', + [(site_id,x['route'],x['seo_title'],x['seo_description'],x['canonical'],200) for x in entries]) + return {'captured':len(entries)} + +def diff_baseline(site_id): + baseline=db.rows('SELECT * FROM migration_baseline WHERE site_id=?',(site_id,)) + entries={x['route']:x for x in db.rows('SELECT route,seo_title,seo_description,canonical FROM entries WHERE site_id=?',(site_id,))} + redirects={x['old_path']:x['new_path'] for x in db.rows('SELECT old_path,new_path FROM redirects WHERE site_id=?',(site_id,))} + problems=[] + for old in baseline: + new=entries.get(old['route']) + if not new: + problems.append({'route':old['route'],'field':'url','severity':'error','from':'200','to':f'redirect→{redirects[old["route"]]}' if old['route'] in redirects else 'missing'}) + continue + for oldcol,newcol in [('seo_title','seo_title'),('description','seo_description'),('canonical','canonical')]: + if old[oldcol] and old[oldcol]!=new[newcol]: + problems.append({'route':old['route'],'field':oldcol,'severity':'warning','from':old[oldcol],'to':new[newcol]}) + return {'baseline_count':len(baseline),'current_count':len(entries),'issues':problems, + 'errors':sum(x['severity']=='error' for x in problems),'warnings':sum(x['severity']=='warning' for x in problems), + 'passed':not any(x['severity']=='error' for x in problems)} + +@router.get('/sites/{site_id}/baseline/report') +def baseline_report(request:Request,site_id:str): + require(request,site_id,'viewer') + return diff_baseline(site_id) + +@router.get('/sites/{site_id}/baseline/export.csv') +def baseline_export(request:Request,site_id:str): + require(request,site_id,'viewer') + records=db.rows('SELECT route,seo_title,description,canonical,status FROM migration_baseline WHERE site_id=? ORDER BY route',(site_id,)) + dest=io.StringIO(); writer=csv.DictWriter(dest,fieldnames=['route','seo_title','description','canonical','status']);writer.writeheader();writer.writerows(records) + return Response('\ufeff'+dest.getvalue(),media_type='text/csv',headers={'Content-Disposition':'attachment; filename=siteforge-seo-baseline.csv'}) + +@router.get('/sites/{site_id}/releases') +def release_list(request:Request,site_id:str): + require(request,site_id,'viewer') + return db.rows('SELECT * FROM release_history WHERE site_id=? ORDER BY created_at DESC',(site_id,)) + +@router.post('/sites/{site_id}/releases/{build_id}/rollback') +def release_rollback(request:Request,site_id:str,build_id:str): + require(request,site_id,'owner') + from .main import publish_local + response=publish_local(site_id,build_id) + db.insert('INSERT INTO release_history VALUES (?,?,?,?,?,?)',(uid(),site_id,build_id,actor(request),'rollback',db.now())) + return response + +# Design tokens are site-specific and independent of the reusable theme package. +DESIGN_DEFAULTS = {'font':'system','heading_scale':1.0,'radius':12,'section_spacing':72,'container_width':1200,'button_style':'rounded','header_style':'standard'} + +@router.get('/sites/{site_id}/design-tokens') +def get_tokens(request:Request,site_id:str): + require(request,site_id,'viewer') + row=db.one('SELECT settings_json FROM sites WHERE id=?',(site_id,)) + if not row:raise HTTPException(404) + settings=json.loads(row['settings_json']) + return {**DESIGN_DEFAULTS,**settings.get('design_tokens',{})} + +@router.put('/sites/{site_id}/design-tokens') +def save_tokens(request:Request,site_id:str,payload:dict=Body(...)): + require(request,site_id,'editor') + tokens={**DESIGN_DEFAULTS} + if set(payload)-set(tokens):raise HTTPException(400,'存在不支持的设计变量') + tokens.update(payload) + if tokens['font'] not in ('system','serif','modern'):raise HTTPException(400,'不支持字体') + if tokens['button_style'] not in ('rounded','square','pill'):raise HTTPException(400,'不支持按钮形状') + if tokens['header_style'] not in ('standard','compact'):raise HTTPException(400,'不支持导航布局') + for field,lo,hi in [('heading_scale',0.75,1.5),('radius',0,30),('section_spacing',32,140),('container_width',900,1600)]: + if not isinstance(tokens[field],(float,int)) or not lo<=tokens[field]<=hi:raise HTTPException(400,'设计变量超出范围: '+field) + record=db.one('SELECT settings_json FROM sites WHERE id=?',(site_id,)) + if not record:raise HTTPException(404) + settings=json.loads(record['settings_json']);settings['design_tokens']=tokens + db.execute('UPDATE sites SET settings_json=?,updated_at=? WHERE id=?',(json.dumps(settings,ensure_ascii=False),db.now(),site_id)) + return tokens + +@router.get('/sites/{site_id}/builds/{build_id}/quality') +def quality_report(request:Request,site_id:str,build_id:str): + require(request,site_id,'viewer') + if not db.one('SELECT id FROM builds WHERE site_id=? AND id=?',(site_id,build_id)):raise HTTPException(404) + from .quality import report + return report(site_id,build_id) + +@router.post('/sites/{site_id}/analyze') +def scan_source(request:Request,site_id:str,payload:dict=Body(...)): + require(request,site_id,'editor') + from .adapters import analyze + try:return analyze(str(payload.get('url',''))) + except Exception as exc:raise HTTPException(400,str(exc)[:300]) + +@router.post('/sites/{site_id}/wordpress-rest') +def wordpress_rest(request:Request,site_id:str,payload:dict=Body(...)): + require(request,site_id,'editor') + from .adapters import import_wordpress_rest + try:return import_wordpress_rest(site_id,str(payload.get('origin','')),int(payload.get('max_items',150)),str(payload.get('on_conflict','skip'))) + except Exception as exc:raise HTTPException(400,str(exc)[:300]) diff --git a/app/factory_api.py b/app/factory_api.py new file mode 100644 index 0000000..63d7e96 --- /dev/null +++ b/app/factory_api.py @@ -0,0 +1,334 @@ +"""Factory V5 endpoints: themes, mappings, build comparisons and release readiness.""" +import json,csv,io +from pathlib import Path +from fastapi import APIRouter, HTTPException, Request, UploadFile, File, Body +from fastapi.responses import HTMLResponse, FileResponse +from pydantic import BaseModel,Field +from . import db,theme_store,assembly,enterprise,builder + +router=APIRouter(prefix='/api/factory',tags=['Website Factory v2']) + +def setup(): + theme_store.setup();assembly.setup() + with db.connect() as c: + c.executescript('''CREATE TABLE IF NOT EXISTS release_requests ( + id TEXT PRIMARY KEY, site_id TEXT NOT NULL REFERENCES sites(id) ON DELETE CASCADE, + build_id TEXT NOT NULL, requested_by TEXT NOT NULL, + reviewed_by TEXT NOT NULL DEFAULT '', state TEXT NOT NULL DEFAULT 'pending', + review_note TEXT NOT NULL DEFAULT '', requested_at TEXT NOT NULL,reviewed_at TEXT); + CREATE INDEX IF NOT EXISTS idx_release_request_site ON release_requests(site_id,build_id); + ''') + +@router.get('/themes') +def all_themes(request:Request): + return theme_store.catalog() + +@router.post('/themes/install') +async def install_theme(request:Request,file:UploadFile=File(...)): + if not request.state.is_superadmin:raise HTTPException(403,'仅系统管理员可安装主题') + try:return theme_store.install(await file.read(8_000_001)) + except (ValueError,UnicodeError,KeyError,json.JSONDecodeError) as exc:raise HTTPException(400,str(exc)) + +@router.get('/themes/{theme_id}/compatibility') +def theme_compatibility(request:Request,theme_id:str,version:str=''): + return theme_store.compatibility(theme_id,version or None) + +@router.get('/themes/{theme_id}/versions') +def versions(request:Request,theme_id:str): + if theme_id in theme_store.BUILTIN: + archive=theme_store.BASE/theme_id/'versions' + historical=[x.name for x in archive.iterdir() if x.is_dir()] if archive.exists() else [] + return [{'theme_id':theme_id,'version':v,'builtin':True} for v in [theme_store.latest(theme_id)]+sorted(historical,reverse=True)] + return db.rows('SELECT * FROM theme_versions WHERE theme_id=? ORDER BY installed_at DESC',(theme_id,)) + +@router.put('/sites/{site_id}/theme') +def choose_theme(request:Request,site_id:str,payload:dict=Body(...)): + enterprise.require(request,site_id,'editor') + try:return theme_store.pin(site_id,str(payload.get('theme','')),payload.get('version') or None) + except ValueError as exc:raise HTTPException(400,str(exc)) + +@router.get('/sites/{site_id}/theme') +def site_theme(request:Request,site_id:str): + enterprise.require(request,site_id,'viewer') + site=db.one('SELECT theme FROM sites WHERE id=?',(site_id,)) + if not site:raise HTTPException(404) + pin=theme_store.pinned(site_id) + return {'theme':site['theme'],'version':pin['version'] if pin else theme_store.latest(site['theme'])} + +@router.get('/sites/{site_id}/relationships') +def preview_relationships(request:Request,site_id:str): + enterprise.require(request,site_id,'viewer') + from . import relations + return relations.preview(site_id) + +@router.post('/sites/{site_id}/relationships/assemble') +def assemble_relationships(request:Request,site_id:str): + enterprise.require(request,site_id,'editor') + from . import relations + try:return relations.commit(site_id) + except ValueError as exc:raise HTTPException(409,str(exc)) + +@router.get('/sites/{site_id}/mapping-profiles') +def profiles(request:Request,site_id:str): + enterprise.require(request,site_id,'viewer') + return db.rows('SELECT * FROM mapping_profiles WHERE site_id=? ORDER BY updated_at DESC',(site_id,)) + +@router.post('/sites/{site_id}/mapping-profiles') +def save_profile(request:Request,site_id:str,payload:dict=Body(...)): + enterprise.require(request,site_id,'editor') + from .core import uid + name=str(payload.get('name','')).strip()[:100] + if not name:raise HTTPException(400,'请输入映射方案名称') + rules=payload.get('rules',{}) + if not isinstance(rules,dict) or 'title' not in rules:raise HTTPException(400,'请设置标题字段映射') + kind=str(payload.get('content_type','product')) + id=uid() + db.insert('INSERT INTO mapping_profiles VALUES (?,?,?,?,?,?,?)',(id,site_id,name,kind,json.dumps(rules,ensure_ascii=False),db.now(),db.now())) + return {'id':id,'name':name,'rules':rules} + +class MapRows(BaseModel): + rows:list[dict]=Field(max_length=1000) + rules:dict[str,str]=Field(default_factory=dict) + content_type:str='product' + prefix:str|None=None + on_conflict:str='skip' + dry_run:bool=True + +@router.post('/sites/{site_id}/mapping/parse-file') +async def parse_mapping_file(request:Request,site_id:str,file:UploadFile=File(...)): + enterprise.require(request,site_id,'viewer') + from .table_loader import parse_table + try: + decoded=parse_table(file.filename or '',await file.read(12_000_001)) + return {**decoded,'suggested':assembly.suggest(decoded['columns'])} + except (ValueError,UnicodeDecodeError,json.JSONDecodeError) as exc: + raise HTTPException(400,str(exc)) + +@router.post('/sites/{site_id}/mapping/suggest') +def suggested_map(request:Request,site_id:str,payload:dict=Body(...)): + enterprise.require(request,site_id,'viewer') + columns=payload.get('columns',[]) + if not isinstance(columns,list) or len(columns)>150:raise HTTPException(400,'字段数量过多') + return {'rules':assembly.suggest(columns)} + +@router.post('/sites/{site_id}/mapping/run') +def map_rows(request:Request,site_id:str,payload:MapRows): + enterprise.require(request,site_id,'viewer' if payload.dry_run else 'editor') + try: + return assembly.dry_run(site_id,payload.rows,payload.rules,payload.content_type,payload.prefix) if payload.dry_run else assembly.commit(site_id,payload.rows,payload.rules,payload.content_type,payload.on_conflict,payload.prefix) + except ValueError as exc:raise HTTPException(400,str(exc)) + +@router.get('/sites/{site_id}/migration/readiness') +def readiness(request:Request,site_id:str,build_id:str=''): + enterprise.require(request,site_id,'viewer') + site=db.one('SELECT id,mode FROM sites WHERE id=?',(site_id,)) + if not site:raise HTTPException(404) + base=enterprise.diff_baseline(site_id) + builds=db.rows('SELECT * FROM builds WHERE site_id=? ORDER BY created_at DESC LIMIT 1',(site_id,)) + latest=db.one('SELECT * FROM builds WHERE site_id=? AND id=?',(site_id,build_id)) if build_id else (builds[0] if builds else None) + report=json.loads(latest['report_json']) if latest else None + gates=[] + if not latest:gates.append({'level':'block','message':'还没有完成静态网站构建'}) + if site['mode']=='migration' and base['baseline_count']==0:gates.append({'level':'block','message':'迁移项目尚未导入原站 URL 基线'}) + if site['mode']=='migration' and base['errors']:gates.append({'level':'block','message':f'仍有 {base["errors"]} 个旧站 URL 没有原路径页面'}) + if base['warnings']:gates.append({'level':'review','message':f'{base["warnings"]} 项 SEO 差异待核查'}) + if report: + for key in ('missing_local_images','broken_links'): + if report.get(key):gates.append({'level':'block','message':f'{key} 有 {len(report[key])} 项需处理'}) + if report.get('external_assets'):gates.append({'level':'review','message':'仍存在未本地化的远程资源'}) + if latest and site['mode']=='migration' and base['baseline_count']: + from . import migration_audit + detail=migration_audit.audit(site_id,latest['id']) + if detail['errors']: + gates.append({'level':'block','message':f'构建文件与原站基线存在 {detail["errors"]} 个阻断问题'}) + if detail['warnings']: + gates.append({'level':'review','message':f'生成 HTML 存在 {detail["warnings"]} 项源站内容/SEO 差异'}) + if detail.get('unreviewed_warnings'): + gates.append({'level':'block','message':f'还有 {detail["unreviewed_warnings"]} 项差异未经审核,暂不允许发布'}) + return {'site_id':site_id,'build_id':latest['id'] if latest else None,'baseline':base, + 'report':report,'gates':gates,'can_publish':not any(x['level']=='block' for x in gates), + 'must_review':any(x['level']=='review' for x in gates)} + +@router.get('/sites/{site_id}/assets/inventory') +def inventory(request:Request,site_id:str): + enterprise.require(request,site_id,'viewer') + records=db.rows('SELECT route,cover,body,meta_json FROM entries WHERE site_id=?',(site_id,)) + from bs4 import BeautifulSoup + assets={} + for row in records: + for url in [row['cover']]+[x.get('src') for x in BeautifulSoup(row['body'] or '','html.parser').select('img[src]')]: + if url:assets.setdefault(url,[]).append(row['route']) + legacy=db.DATA/'legacy'/site_id + media=db.DATA/'media'/site_id + existing=[str(p.relative_to(d)) for d in (legacy,media) if d.exists() for p in d.rglob('*') if p.is_file()] + return {'references':len(assets),'files':len(existing),'assets':[{'url':a,'used_by':routes} for a,routes in sorted(assets.items())][:5000]} + +@router.post('/sites/{site_id}/bootstrap') +def bootstrap_site(request:Request,site_id:str): + """Create full navigation/landing-page structure while preserving existing URLs. + + Does not fabricate business descriptions or products; placeholder blocks stay + visibly editable and static empty collections clearly identify missing data. + """ + enterprise.require(request,site_id,'editor') + from . import blueprints + from .core import save_entry + site=db.one('SELECT theme,name FROM sites WHERE id=?',(site_id,)) + if not site:raise HTTPException(404) + pages=[('about','关于我们','/about/','企业介绍与经营理念'), + ('contact','联系我们','/contact/','业务咨询与联系方式'), + ('listing','产品中心','/products/','按分类浏览产品', 'product'), + ('listing','品牌中心','/brands/','了解我们的合作品牌','brand'), + ('listing','新闻资讯','/news/','企业资讯与行业动态','post'), + ('listing','项目案例','/cases/','精选客户项目','case'), + ('standard','服务与解决方案','/services/','业务流程与服务能力'), + ('standard','常见问题','/faq/','了解合作流程')] + created=[];skipped=[] + for page in pages: + kind,title,route,description=page[:4] + if db.one('SELECT id FROM entries WHERE site_id=? AND route=?',(site_id,route)): + skipped.append(route);continue + blocks=blueprints.theme_blocks(site['theme'],kind) + if kind=='listing' and len(page)>4: + for b in blocks: + if b['type']=='collection':b['source']=page[4];b['title']=title;b['limit']=36 + if route=='/faq/':blocks=[blueprints.new_block('faq')] + save_entry(site_id,{'type':'page','title':title,'route':route,'summary':description, + 'meta':{'page_template':kind,'blocks':blocks}},source='site-bootstrap') + created.append(route) + nav=[{'title':v,'route':r} for v,r in [('首页','/'),('产品中心','/products/'),('品牌','/brands/'),('新闻','/news/'),('案例','/cases/'),('关于我们','/about/'),('联系我们','/contact/')]] + raw=db.one('SELECT settings_json FROM sites WHERE id=?',(site_id,)) + settings=json.loads(raw['settings_json'] or '{}') + if not settings.get('nav'): + settings['nav']=nav + db.execute('UPDATE sites SET settings_json=?,updated_at=? WHERE id=?',(json.dumps(settings,ensure_ascii=False),db.now(),site_id)) + return {'created':created,'skipped':skipped,'navigation':settings.get('nav'), + 'message':'页面已建立;企业介绍、联系方式、产品等实际业务内容仍需补充。'} + +@router.get('/sites/{site_id}/release-requests') +def requests_list(request:Request,site_id:str): + enterprise.require(request,site_id,'viewer') + return db.rows('SELECT * FROM release_requests WHERE site_id=? ORDER BY requested_at DESC',(site_id,)) + +@router.post('/sites/{site_id}/release-requests') +def request_release(request:Request,site_id:str,payload:dict=Body(...)): + enterprise.require(request,site_id,'editor') + build_id=str(payload.get('build_id','')) + if not db.one('SELECT id FROM builds WHERE site_id=? AND id=?',(site_id,build_id)): + raise HTTPException(404,'构建版本不存在') + status=readiness(request,site_id,build_id) + if not status['can_publish']: + raise HTTPException(409,'迁移/质量门禁尚未通过,不允许提交发布审批') + from . import quality + qa=quality.report(site_id,build_id) + if not qa['passed']:raise HTTPException(409,f'构建质量检测包含 {qa["errors"]} 个错误') + from .core import uid + rid=uid();db.insert('INSERT INTO release_requests(id,site_id,build_id,requested_by,requested_at) VALUES (?,?,?,?,?)', + (rid,site_id,build_id,request.state.actor,db.now())) + return {'id':rid,'state':'pending','build_id':build_id,'review_required':True} + +@router.post('/sites/{site_id}/release-requests/{request_id}/review') +def review_release(request:Request,site_id:str,request_id:str,payload:dict=Body(...)): + enterprise.require(request,site_id,'reviewer') + item=db.one('SELECT * FROM release_requests WHERE id=? AND site_id=?',(request_id,site_id)) + if not item:raise HTTPException(404) + if item['state']!='pending':raise HTTPException(409,'该请求已处理') + if item['requested_by']==request.state.actor:raise HTTPException(403,'审批人不能审核自己提交的发布申请') + state=payload.get('state') + if state not in ('approved','rejected'):raise HTTPException(400,'必须选择 approved 或 rejected') + note=str(payload.get('note') or '')[:500] + db.execute('UPDATE release_requests SET state=?,reviewed_by=?,review_note=?,reviewed_at=? WHERE id=? AND site_id=?', + (state,request.state.actor,note,db.now(),request_id,site_id)) + return {'id':request_id,'state':state,'reviewed_by':request.state.actor} + + +@router.get('/themes/{theme_id}/package') +def download_theme_package(request:Request,theme_id:str,version:str=''): + if not request.state.is_superadmin:raise HTTPException(403,'只有系统管理员可以导出主题包') + import io,zipfile + from fastapi.responses import Response + try:folder=theme_store.resolve(theme_id,version or None) + except ValueError as exc:raise HTTPException(404,str(exc)) + buffer=io.BytesIO() + with zipfile.ZipFile(buffer,'w',compression=zipfile.ZIP_DEFLATED) as archive: + manifest=json.loads((folder/'manifest.json').read_text(encoding='utf-8')) + manifest['layout_family']=theme_id if theme_id in theme_store.BUILTIN else manifest.get('layout_family','corporate') + archive.writestr('manifest.json',json.dumps(manifest,ensure_ascii=False,indent=2)) + for part in ('theme.css','page-presets.json'): + archive.write(folder/part,arcname=part) + for image_name in ('preview.png','preview.jpg','preview.webp'): + if (folder/image_name).exists():archive.write(folder/image_name,arcname=image_name) + return Response(buffer.getvalue(),media_type='application/zip',headers={'Content-Disposition':f'attachment; filename="{theme_id}-{version or theme_store.latest(theme_id)}.zip"'}) + +@router.get('/themes/{theme_id}/preview.css') +def theme_preview_css(theme_id:str,version:str=''): + from fastapi.responses import Response + try:dir=theme_store.resolve(theme_id,version or None) + except ValueError as exc:raise HTTPException(404,str(exc)) + return Response((dir/'theme.css').read_text(encoding='utf-8'),media_type='text/css') + +@router.get('/themes/{theme_id}/preview.pro.css') +def theme_preview_pro_css(theme_id:str,version:str=''): + from fastapi.responses import Response + try:folder=theme_store.resolve(theme_id,version or None) + except ValueError as exc:raise HTTPException(404,str(exc)) + asset=folder/'pro.css' + return Response(asset.read_text(encoding='utf-8') if asset.exists() else '',media_type='text/css') + +@router.get('/themes/{theme_id}/visual/hero.svg') +def theme_preview_hero(theme_id:str,version:str=''): + from fastapi.responses import Response + try:directory=theme_store.resolve(theme_id,version or None) + except ValueError as exc:raise HTTPException(404,str(exc)) + art=directory/'visual'/'hero.svg' + if not art.is_file():raise HTTPException(404,'该主题版本没有内置视觉素材') + return Response(art.read_bytes(),media_type='image/svg+xml',headers={'Cache-Control':'public,max-age=300'}) + +@router.get('/themes/{theme_id}/preview',response_class=HTMLResponse) +def theme_preview(theme_id:str,version:str='',page_type:str='home'): + from jinja2 import Environment,FileSystemLoader,select_autoescape + from markupsafe import Markup + from html import escape + from . import blueprints,static_site + try:directory=theme_store.resolve(theme_id,version or None) + except ValueError as exc:raise HTTPException(404,str(exc)) + if page_type not in blueprints.PAGE_TEMPLATES: + raise HTTPException(400, '不支持的预览页面类型') + manifest=json.loads((directory/'manifest.json').read_text(encoding='utf-8')) + site={'id':'preview','theme':theme_id,'name':manifest.get('name','品牌企业'), + 'tagline':'Independent design · Integrated content · Global delivery', + 'domain':'','email':'hello@example.com','logo':'','accent':'#346aca'} + page={'title':{'home':'创造有影响力的网站','about':'关于我们','contact':'联系我们','listing':'产品中心','product':'旗舰产品 A','article':'最新企业新闻','brand':'合作品牌','standard':'我们的服务','notfound':'404 · 页面不存在','case':'项目案例','service':'专业服务','faq':'常见问题','category':'演示分类'}[page_type], + 'route':'/' if page_type=='home' else ('/404.html' if page_type=='notfound' else ('/categories/demo/' if page_type=='category' else '/preview-example/')),'type':{'product':'product','article':'post','brand':'brand','case':'case','service':'service'}.get(page_type,'page'), + 'summary':'从完整主题出发,连接产品、品牌和客户。', + 'cover':'','body':'','robots':'index,follow','meta_json':json.dumps({'page_template':page_type})} + records=[{'title':t,'type':'product','route':'/product/example-'+str(i)+'/','summary':'展示产品与解决方案', + 'cover':'','meta_json':'{}','status':'published'} for i,t in enumerate(('旗舰产品 A','核心产品 B','定制解决方案 C'))] + site['schemas']={} + blocks=blueprints.theme_blocks(theme_id,page_type,version or None) + block_html=blueprints.render_blocks(blocks,site,page,records) + from . import theme_pro + if (directory/'pro.html').exists(): + # Online previews and published pages use the identical professional renderer. + if theme_id in theme_pro.THEMES and directory.resolve()==(theme_store.BASE/theme_id).resolve(): + html=theme_pro.render(theme_id,site,page,records,'home' if page_type=='home' else ('notfound' if page_type=='notfound' else 'detail'),[],'') + else: + env=Environment(loader=FileSystemLoader([str(directory),str(theme_store.BASE)]),autoescape=select_autoescape(['html'])) + context=theme_pro.make_context(site,page,records,'home' if page_type=='home' else ('notfound' if page_type=='notfound' else 'detail'),[]) + context.update(custom_blocks=Markup(''),has_custom=False,noindex=page_type=='notfound',theme_overrides=True) + html=env.get_template('pro.html').render(**context) + html=html.replace('assets/pro-base.css','/ui/pro-base.css').replace('assets/pro.js','/ui/pro.js').replace('assets/commercial.css','/ui/commercial.css') + html=html.replace('assets/pro.css',f'/api/factory/themes/{theme_id}/preview.pro.css?version={version or theme_store.latest(theme_id)}') + html=html.replace('assets/theme-overrides.css',f'/api/factory/themes/{theme_id}/preview.css?version={version or theme_store.latest(theme_id)}') + else: + env=Environment(loader=FileSystemLoader(str(directory)),autoescape=select_autoescape(['html'])) + html=env.get_template('site.html').render(site=site,page=page,mode='home' if page_type=='home' else ('notfound' if page_type=='notfound' else 'detail'),content=Markup(''), + blocks=Markup(block_html),blueprint_mode=True,entries=records,listing_items=[], + type_names={'product':'产品','page':'页面'},nav=[],link=lambda x:x.lstrip('/') or './', + asset=lambda x:x,canonical='',seo_title='主题预览 · '+manifest.get('name',''), + seo_description='SiteForge 主题模板预览') + html=static_site.shell(html,site,page,records,[]) + html=html.replace('assets/theme.css',f'/api/factory/themes/{theme_id}/preview.css?version={version or theme_store.latest(theme_id)}') + html=html.replace('assets/blocks.css','/ui/blocks.css').replace('assets/factory.css','/ui/factory.css') + html=html.replace('assets/custom.css','/ui/factory.css').replace('assets/site-runtime.js','/ui/site-runtime.js') + return HTMLResponse(html) diff --git a/app/importers.py b/app/importers.py new file mode 100644 index 0000000..523cf17 --- /dev/null +++ b/app/importers.py @@ -0,0 +1,239 @@ +"""Data-import adapters for CSV/JSON/XLSX/WordPress WXR/public HTML crawl.""" +import csv +import io +import json +import re +import time +from urllib.parse import urlparse, urljoin, urldefrag +from collections import deque +from defusedxml import ElementTree as SafeET +from bs4 import BeautifulSoup +import httpx +from openpyxl import load_workbook +from .core import route_path, save_entry, ensure_public_url, slug_of +from . import db,wordpress_compat + +KEY_ALIASES={ + 'title':('title','name','标题','名称','产品名称','post_title'), + 'type':('type','post_type','类型','内容类型'), + 'route':('route','url','path','permalink','link','路径','链接'), + 'summary':('summary','excerpt','description','摘要','简介'), + 'body':('body','content','text','details','正文','内容'), + 'cover':('cover','image','image_url','featured_image','图片','封面'), + 'seo_title':('seo_title','meta_title','SEO标题'), + 'seo_description':('seo_description','meta_description','SEO描述'), + 'canonical':('canonical',), + 'status':('status',), +} + +def normalize(rec): + r={str(k).strip().lower():v for k,v in rec.items()} + clean={} + used=set() + for canonical,aliases in KEY_ALIASES.items(): + for alias in aliases: + if alias.lower() in r: + clean[canonical]=r[alias.lower()] if r[alias.lower()] is not None else '' + used.add(alias.lower());break + extra={k:v for k,v in r.items() if k not in used} + embedded=r.get('meta') if isinstance(r.get('meta'),dict) else {} + clean['meta']={**embedded,**{k:v for k,v in extra.items() if k not in ('id','meta')}} + if not clean.get('type'):clean['type']='product' + return clean + +def import_records(site_id, records, origin='file', on_conflict='skip'): + if on_conflict not in ('skip','update'): + raise ValueError('无效的冲突策略') + result={'imported':0,'updated':0,'skipped':0,'errors':[]} + for index,rec in enumerate(records,1): + try: + data=normalize(rec) + if not data.get('title'):raise ValueError('缺少 title/name') + kind=data.get('type') or 'product' + if not data.get('route'): + data['route']=f'/{kind}/{slug_of(str(data["title"]))}/' + if '?' in str(data['route']) and not str(data['route']).startswith('http'): + raise ValueError('带查询参数的动态 URL 需要专项映射,不能直接转换为静态路由') + route=route_path(data['route']) + existing=db.one('SELECT id FROM entries WHERE site_id=? AND route=?',(site_id,route)) + if existing and on_conflict=='skip': + result['skipped']+=1;continue + save_entry(site_id,data,existing_id=existing['id'] if existing else None,source=origin) + result['updated' if existing else 'imported']+=1 + except Exception as exc: + if len(result['errors'])<80: + result['errors'].append({'row':index,'message':str(exc)[:250]}) + return result + +def import_data_file(site_id, filename, content, conflict): + lower=filename.lower() + if lower.endswith('.csv'): + txt=content.decode('utf-8-sig') + records=list(csv.DictReader(io.StringIO(txt))) + elif lower.endswith('.json'): + parsed=json.loads(content.decode('utf-8-sig')) + if isinstance(parsed,dict):parsed=parsed.get('entries',parsed.get('items',[])) + if not isinstance(parsed,list):raise ValueError('JSON 应当为数据数组或包含 entries/items 数组') + records=parsed + elif lower.endswith('.xlsx'): + wb=load_workbook(io.BytesIO(content),read_only=True,data_only=True) + ws=wb.active + values=ws.iter_rows(values_only=True) + headers=[str(x or '').strip() for x in next(values)] + records=(dict(zip(headers,row)) for row in values) + elif lower.endswith('.xml'): + return import_wxr(site_id,content,conflict) + else: + raise ValueError('仅支持 .csv/.json/.xlsx/.xml') + return import_records(site_id,records,filename,conflict) + +def import_wxr(site_id,content,conflict='skip'): + root=SafeET.fromstring(content) + ns={'content':'http://purl.org/rss/1.0/modules/content/', + 'excerpt':'http://wordpress.org/export/1.2/excerpt/', + 'wp':'http://wordpress.org/export/1.2/'} + # Support alternate WXR versions (e.g. 1.1) + raw_content=None; raw_wp=None; raw_excerpt=None + for e in root.iter(): + if e.tag.startswith('{'): + uri=e.tag.split('}')[0][1:] + if uri.endswith('/modules/content/'):raw_content=uri + if 'wordpress.org/export/' in uri and uri.endswith('/') :raw_wp=uri + if uri.endswith('/modules/excerpt/'):raw_excerpt=uri + if raw_content:ns['content']=raw_content + if raw_wp:ns['wp']=raw_wp + if raw_excerpt:ns['excerpt']=raw_excerpt + entries=[] + media_index=wordpress_compat.wp_media_map(root) + for it in root.findall('./channel/item'): + typ=(it.findtext('wp:post_type',default='post',namespaces=ns) or 'post') + status=(it.findtext('wp:status',default='publish',namespaces=ns) or 'publish') + if typ in ('attachment','nav_menu_item','revision') or status not in ('publish','private','draft'): + continue + slug=it.findtext('wp:post_name',namespaces=ns) or slug_of(it.findtext('title') or 'page') + title=it.findtext('title') or slug + link=it.findtext('link') or '' + path=urlparse(link).path if link.startswith(('http://','https://')) else '' + if not path: path=f'/{slug}/' if typ=='page' else f'/{typ}/{slug}/' + wpmeta={} + for pm in it.findall('wp:postmeta',ns): + key=pm.findtext('wp:meta_key',namespaces=ns) + if key:wpmeta[key]=pm.findtext('wp:meta_value',default='',namespaces=ns) + body=it.findtext('content:encoded',default='',namespaces=ns) + taxonomy=wordpress_compat.wp_taxonomy(it) + # ACF stores human-readable fields as plain postmeta keys, while + # underscore-prefixed field references point to the ACF field schema. + # Preserve *all* raw metadata, but expose only safe scalar values as + # structured content fields for the visual parameter component. + fields={k:v[:10000] for k,v in wpmeta.items() + if k and not k.startswith('_') and not k.lower().startswith('field_') + and isinstance(v,str) and len(v)<=10000} + for source,dest in (('_sku','sku'),('_regular_price','regular_price'), + ('_sale_price','sale_price'),('_price','price'), + ('_stock_status','stock_status'),('_weight','weight')): + if wpmeta.get(source):fields[dest]=str(wpmeta[source])[:1000] + fields.update(taxonomy) + wp_featured=media_index.get(wpmeta.get('_thumbnail_id',''),'') + gallery_ids=[x.strip() for x in wpmeta.get('_product_image_gallery','').split(',') if x.strip()] + gallery=[media_index[x] for x in gallery_ids if x in media_index] + if gallery:fields['gallery']=gallery + warnings=[] + if wpmeta.get('_elementor_data'): + elementor=wordpress_compat.elementor_to_html(wpmeta['_elementor_data']) + if not body.strip() and elementor['html']: + body=elementor['html'];warnings.append('Elementor 仅转换已支持的文本与图片组件,原始布局需人工检查') + warnings.extend(elementor['warnings']) + warnings.append('检测到 Elementor 页面构建器 JSON;原生布局不能直接转换为 SiteForge 区块,需人工对照检查。') + if any(k.startswith('_product_attributes') for k in wpmeta): + warnings.append('WooCommerce 序列化商品属性待专项解析,原始 metadata 已保留。') + body=body or '' + entries.append({'type':typ if typ in ('page','post','product','service','case','brand','vehicle') else 'other', + 'title':title,'route':path,'body':body,'cover':wp_featured, + 'summary':it.findtext('excerpt:encoded',default='',namespaces=ns), + 'seo_title':wpmeta.get('_yoast_wpseo_title') or wpmeta.get('rank_math_title') or '', + 'seo_description':wpmeta.get('_yoast_wpseo_metadesc') or wpmeta.get('rank_math_description') or '', + 'canonical':wpmeta.get('_yoast_wpseo_canonical') or wpmeta.get('rank_math_canonical_url') or '', + 'robots':'noindex,follow' if wpmeta.get('_yoast_wpseo_meta-robots-noindex')=='1' or wpmeta.get('rank_math_robots')=='noindex' else 'index,follow', + 'status':'published' if status=='publish' else 'draft', + 'meta':{'elementor_widgets':elementor['widgets'] if wpmeta.get('_elementor_data') else 0,'wp_post_id':it.findtext('wp:post_id',default='',namespaces=ns),'wp_type':typ,'wp_metadata':wpmeta,'fields':fields,'migration_warnings':warnings}}) + return import_records(site_id,entries,'wordpress-wxr',conflict) + +def safe_get(client,url,max_bytes=3_000_000): + """Validate each hop to prevent trivial redirect-to-private host SSRF.""" + for _ in range(4): + ensure_public_url(url) + with client.stream('GET',url, follow_redirects=False) as response: + if response.status_code in (301,302,303,307,308): + loc=response.headers.get('location') + if not loc: raise ValueError('重定向缺少 Location') + url=urljoin(url,loc);continue + response.raise_for_status() + if 'text/html' not in response.headers.get('content-type','text/html'): + raise ValueError('页面不是 HTML') + buff=bytearray() + for part in response.iter_bytes(): + buff.extend(part) + if len(buff)>max_bytes:raise ValueError('HTML 页面超过 3 MB') + return bytes(buff),str(response.url) + raise ValueError('页面重定向次数过多') + +def extract_page(html,url): + s=BeautifulSoup(html,'html.parser') + title=s.title.get_text(' ',strip=True) if s.title else '' + description=s.find('meta',attrs={'name':re.compile('^description$',re.I)}) + canonical=s.find('link',attrs={'rel':lambda val: bool(val) and ('canonical' in val if isinstance(val,list) else 'canonical' in val)}) + robots=s.find('meta',attrs={'name':re.compile('^robots$',re.I)}) + h1=s.find('h1') + heading=h1.get_text(' ',strip=True) if h1 else title + main=s.find('main') or s.find('article') or s.body + body='' + if main: + for tag in main.select('script,style,nav,footer,header,aside,form,iframe,svg'): + tag.decompose() + for image in main.find_all('img'): + src=image.get('src') or image.get('data-src') + if src:image['src']=urljoin(url,src) + for a in main.find_all('a',href=True): + if a['href'].startswith(('http','/','.')):a['href']=urljoin(url,a['href']) + body=str(main) + links=[] + for a in s.find_all('a',href=True): + abs_url=urljoin(url,a['href']) + if urlparse(abs_url).scheme in ('http','https'): + links.append(urldefrag(abs_url)[0]) + return ({'type':'page' if urlparse(url).path=='/' else 'post', + 'title':heading or 'Untitled','route':urlparse(url).path or '/', + 'body':body,'summary':description.get('content','') if description else '', + 'seo_title':title,'seo_description':description.get('content','') if description else '', + 'canonical':canonical.get('href','') if canonical else '', + 'robots':robots.get('content','index,follow') if robots else 'index,follow'},links) + +def crawl_site(site_id,start,max_pages=25,conflict='skip'): + ensure_public_url(start) + max_pages=max(1,min(int(max_pages),60)) + root=urlparse(start) + host=root.hostname + queue=deque([start]);visited=set();pages=[];errors=[] + # Strict same-host traversal, not open web crawling. + with httpx.Client(timeout=httpx.Timeout(12,connect=5),headers={'User-Agent':'SiteForgeLocalMigration/0.1'},trust_env=False) as client: + while queue and len(visited)=5: + enterprise.save_revision(pieces[2],pieces[4],identity['actor'],'before_'+request.method.lower()) + response=await call_next(request) + if not public: + if request.method in ('POST','PUT','PATCH','DELETE'): + site_match=__import__('re').match(r'^/api/(?:(?:enterprise|factory)/)?sites/([^/]+)',path) + enterprise.log_event(request.state.actor,path,response.status_code,request.method, + site_match.group(1) if site_match else None) + if request.method=='GET' and path=='/api/sites' and not request.state.is_superadmin and response.status_code==200: + # Apply the same site-scope filter at the top-level list boundary. + import json as _json + permitted=enterprise.db.rows('SELECT site_id FROM team_memberships WHERE user_id=?',(request.state.account_id,)) + ids={r['site_id'] for r in permitted} + from starlette.responses import StreamingResponse + raw=b'' + async for part in response.body_iterator:raw+=part + data=_json.loads(raw) + response=JSONResponse([x for x in data if x['id'] in ids]) + response.headers['X-Content-Type-Options']='nosniff' + # The authenticated theme gallery embeds same-origin previews. Block + # cross-origin framing without breaking that internal workflow. + response.headers['X-Frame-Options']='SAMEORIGIN' + response.headers['Referrer-Policy']='strict-origin-when-cross-origin' + response.headers['Permissions-Policy']='camera=(), microphone=(), geolocation=(), payment=(), usb=()' + response.headers['Content-Security-Policy']=( + "default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'self'; " + "script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; " + "img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' https:; " + "form-action 'self' https:" + ) + response.headers['Cache-Control']='no-store' if path.startswith('/api/auth/') else response.headers.get('Cache-Control','no-cache') + return response + + +@app.get('/theme-gallery',response_class=HTMLResponse) +def theme_gallery(): + return (ROOT/'app'/'templates'/'theme-gallery.html').read_text(encoding='utf-8') + +@app.get('/factory-studio',response_class=HTMLResponse) +def factory_studio(): + return (ROOT/'app'/'templates'/'factory-v2.html').read_text(encoding='utf-8') + +@app.get('/login',response_class=HTMLResponse) +def login_page(): + return (ROOT/'app'/'templates'/'login.html').read_text(encoding='utf-8') + + +class LoginIn(BaseModel): + password: str + username: str = "admin" + + +@app.post('/api/auth/login') +def login(payload:LoginIn, request:Request): + remote = request.client.host if request.client else 'unknown' + security.check_login_rate(remote) + if payload.username=='admin': + authenticated=security.check_password(payload.password) + token=security.sign_session() if authenticated else None + else: + user=enterprise.user_by_name(payload.username) + authenticated=bool(user and not user['disabled'] and enterprise.check_password(payload.password,user['password_hash'])) + token=security.sign_user_session(user['id']) if authenticated else None + if not authenticated: + security.note_login_failure(remote) + raise HTTPException(401, '账号或密码不正确') + security.note_login_success(remote) + response = JSONResponse({'ok':True}) + response.set_cookie(security.COOKIE, token, max_age=security.MAX_AGE, + httponly=True, secure=security.COOKIE_SECURE, samesite='strict', path='/') + response.headers['Cache-Control'] = 'no-store' + return response + + +@app.get('/api/auth/me') +def whoami(request:Request): + session=security.read_session(request.cookies.get(security.COOKIE)) + return {'authenticated':True,'user':request.state.actor,'superadmin':request.state.is_superadmin,'csrf':security.csrf(session),'version':APP_VERSION} + + +@app.post('/api/auth/logout') +def logout(): + response=JSONResponse({'ok':True}) + response.delete_cookie(security.COOKIE,path='/') + return response + + +class SiteIn(BaseModel): + name:str = Field(min_length=1,max_length=150) + domain:str = '' + tagline:str = '' + theme:str = 'corporate' + accent:str = '#2563eb' + logo:str = '' + mode:str = 'new' + origin:str = '' + preset_id:str = '' + +class SitePatch(BaseModel): + name:str|None=None + domain:str|None=None + tagline:str|None=None + theme:str|None=None + accent:str|None=None + logo:str|None=None + mode:str|None=None + origin:str|None=None + +class CrawlIn(BaseModel): + url:str + max_pages:int=20 + on_conflict:str='skip' + +class BuildIn(BaseModel): + mirror_images:bool=False + +class RedirectIn(BaseModel): + old_path:str + new_path:str + status_code:int=301 + +def get_site(site_id): + site=db.one('SELECT * FROM sites WHERE id=?',(site_id,)) + if not site:raise HTTPException(404,'找不到网站') + site['settings']=json.loads(site.pop('settings_json')) + return site + +def get_entry(site_id,eid): + val=db.one('SELECT * FROM entries WHERE site_id=? AND id=?',(site_id,eid)) + if not val:raise HTTPException(404,'找不到内容') + return entry_dict(val) + +def fail(exc): + if isinstance(exc,HTTPException):raise exc + raise HTTPException(400,detail=str(exc)[:500]) + +@app.get('/',response_class=HTMLResponse) +def home(): + return (ROOT/'app'/'templates'/'admin.html').read_text(encoding='utf-8') + +@app.get('/editor/{site_id}/{entry_id}',response_class=HTMLResponse) +def visual_editor(site_id:str,entry_id:str): + get_entry(site_id,entry_id) + return (ROOT/'app'/'templates'/'editor.html').read_text(encoding='utf-8') + +@app.get('/api/health') +def health():return {'status':'ok','name':APP_NAME,'version':APP_VERSION} + +@app.get('/api/themes') +def themes(): + manifests=[] + for theme in THEMES: + meta=json.loads((ROOT/'themes'/theme/'manifest.json').read_text(encoding='utf-8')) + manifests.append(meta) + return manifests + +@app.get('/api/editor/blocks') +def editor_blocks(): + return {'blocks':blueprints.BLOCKS,'bindings':blueprints.BINDABLE,'page_templates':blueprints.PAGE_TEMPLATES} + +@app.get('/api/sites') +def list_sites(): + arr=db.rows('SELECT * FROM sites ORDER BY updated_at DESC') + for s in arr: + s.pop('settings_json',None) + s['entries_count']=db.one('SELECT count(*) n FROM entries WHERE site_id=?',(s['id'],))['n'] + s['build_count']=db.one('SELECT count(*) n FROM builds WHERE site_id=?',(s['id'],))['n'] + return arr + +@app.post('/api/sites',status_code=201) +def create_site(payload:SiteIn): + if payload.theme not in theme_store.ids():raise HTTPException(400,'没有该模板') + try:clean_hex(payload.accent) + except Exception as exc:fail(exc) + site_id=uid() + settings={'origin':payload.origin,'nav':[],'email':'','footer_text':''} + homeblocks=blueprints.theme_blocks(payload.theme,'home') + if payload.preset_id: + p=db.one('SELECT * FROM presets WHERE id=?',(payload.preset_id,)) + if not p:raise HTTPException(404,'模板预设不存在') + payload.theme=p['theme'];settings.update(json.loads(p['settings_json'])) + homeblocks=json.loads(p['home_blocks_json']) + with db.connect() as c: + c.execute('''INSERT INTO sites(id,name,domain,tagline,theme,accent,logo,mode,settings_json,created_at,updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?)''', + (site_id,payload.name,payload.domain,payload.tagline,payload.theme,payload.accent,payload.logo,payload.mode,json.dumps(settings,ensure_ascii=False),db.now(),db.now())) + save_entry(site_id,{'type':'page','title':'首页','route':'/','summary':payload.tagline, + 'seo_title':payload.name,'meta':{'blocks':homeblocks}},source='starter') + return get_site(site_id) + +@app.get('/api/sites/{site_id}') +def site_detail(site_id:str): + site=get_site(site_id) + site['counts']={r['type']:r['n'] for r in db.rows('SELECT type,count(*) n FROM entries WHERE site_id=? GROUP BY type',(site_id,))} + site['builds']=db.rows('SELECT id,created_at,total_pages,status FROM builds WHERE site_id=? ORDER BY created_at DESC LIMIT 10',(site_id,)) + return site + +@app.patch('/api/sites/{site_id}') +def patch_site(site_id:str,patch:SitePatch): + old=get_site(site_id) + data=patch.model_dump(exclude_unset=True) + if 'theme' in data and data['theme'] not in theme_store.ids():raise HTTPException(400,'没有该模板') + if 'accent' in data: + try:clean_hex(data['accent']) + except Exception as exc:fail(exc) + settings=old['settings'] + if 'origin' in data:settings['origin']=data.pop('origin') or '' + valid=['name','domain','tagline','theme','accent','logo','mode'] + fields=[k for k in data if k in valid] + with db.connect() as c: + for k in fields:c.execute(f'UPDATE sites SET {k}=? WHERE id=?',(data[k],site_id)) + c.execute('UPDATE sites SET settings_json=?,updated_at=? WHERE id=?',(json.dumps(settings,ensure_ascii=False),db.now(),site_id)) + return get_site(site_id) + +@app.delete('/api/sites/{site_id}') +def delete_site(site_id:str): + get_site(site_id) + db.execute('DELETE FROM sites WHERE id=?',(site_id,)) + for relative in (db.DATA/'media'/site_id,db.DATA/'builds'/site_id): + if relative.exists():shutil.rmtree(relative) + for z in (db.DATA/'archives').glob(site_id+'-*.zip') if (db.DATA/'archives').exists() else []: + z.unlink() + return {'deleted':True} + +@app.post('/api/sites/{site_id}/clone',status_code=201) +def clone_site(site_id:str,settings:dict[str,Any]=Body(default_factory=dict)): + original=get_site(site_id) + clone=create_site(SiteIn(name=settings.get('name') or original['name']+' 副本',theme=original['theme'], + domain=settings.get('domain',''),tagline=original['tagline'],accent=original['accent'],logo=original['logo'])) + entries=db.rows('SELECT * FROM entries WHERE site_id=?',(site_id,)) + for old in entries: + e=entry_dict(old) + try:save_entry(clone['id'],e,source='clone',replace_route=True) + except Exception:pass + return get_site(clone['id']) + +@app.get('/api/sites/{site_id}/entries') +def list_entries(site_id:str,typ:str='',q:str=''): + get_site(site_id) + sql='SELECT * FROM entries WHERE site_id=?';params=[site_id] + if typ:sql+=' AND type=?';params.append(typ) + if q:sql+=' AND (title LIKE ? OR route LIKE ?)';params.extend(['%'+q+'%','%'+q+'%']) + sql+=' ORDER BY CASE WHEN route="/" THEN 0 ELSE 1 END, type, updated_at DESC' + return [entry_dict(x) for x in db.rows(sql,params)] + +@app.post('/api/sites/{site_id}/entries',status_code=201) +def create_entry(site_id:str,payload:dict[str,Any]): + try: + eid=save_entry(site_id,payload) + return get_entry(site_id,eid) + except Exception as exc:fail(exc) + +@app.patch('/api/sites/{site_id}/entries/{entry_id}') +def update_entry(site_id:str,entry_id:str,payload:dict[str,Any]): + previous=get_entry(site_id,entry_id) + data={**previous,**payload} + try: + save_entry(site_id,data,existing_id=entry_id) + return get_entry(site_id,entry_id) + except Exception as exc:fail(exc) + +@app.delete('/api/sites/{site_id}/entries/{entry_id}') +def delete_entry(site_id:str,entry_id:str): + get_entry(site_id,entry_id) + db.execute('DELETE FROM entries WHERE site_id=? AND id=?',(site_id,entry_id)) + return {'deleted':True} + +def infer_page_template(entry): + route=str(entry.get('route','')).lower() + typ=entry.get('type') + if route=='/':return 'home' + if typ in ('product','vehicle'):return 'product' + if typ=='post':return 'article' + if typ=='brand':return 'brand' + if any(k in route for k in ('/about', '/company', '/our-story')):return 'about' + if any(k in route for k in ('/contact', '/inquiry')):return 'contact' + return 'standard' + +@app.post('/api/sites/{site_id}/scaffold') +def scaffold_standard_pages(site_id:str): + """Add commonly needed enterprise pages while never overwriting existing URLs.""" + site=get_site(site_id) + defaults=[('about','关于我们','/about/','企业介绍、经营理念与核心服务能力'), + ('contact','联系我们','/contact/','联系方式、业务咨询与合作对接'), + ('standard','服务与解决方案','/services/','适合不同客户需求的业务与服务')] + created=[];skipped=[] + for kind,title,route,summary in defaults: + if db.one('SELECT id FROM entries WHERE site_id=? AND route=?',(site_id,route)): + skipped.append(route);continue + meta={'page_template':kind,'blocks':blueprints.theme_blocks(site['theme'],kind,(theme_store.pinned(site_id) or {}).get('version'))} + save_entry(site_id,{'type':'page','title':title,'route':route,'summary':summary,'meta':meta},source='scaffold') + created.append(route) + return {'created':created,'skipped':skipped,'note':'不覆盖已有页面或修改任何原始 URL'} + +@app.post('/api/sites/{site_id}/apply-theme') +def apply_theme_templates(site_id:str,payload:dict=Body(default_factory=dict)): + """Re-assemble existing content with the selected theme without modifying URLs/SEO.""" + site=get_site(site_id) + mode=payload.get('mode','missing') + if mode not in ('missing','overwrite'):raise HTTPException(400,'无效的套版模式') + entries=[entry_dict(x) for x in db.rows('SELECT * FROM entries WHERE site_id=?',(site_id,))] + changed=0;skipped=0 + for entry in entries: + if mode=='missing' and entry['meta'].get('blocks'): + skipped+=1;continue + kind=infer_page_template(entry) + entry['meta']={**entry['meta'],'page_template':kind,'blocks':blueprints.theme_blocks(site['theme'],kind,(theme_store.pinned(site_id) or {}).get('version'))} + save_entry(site_id,entry,existing_id=entry['id']) + changed+=1 + return {'updated':changed,'skipped':skipped,'total':len(entries),'theme':site['theme'],'mode':mode, + 'note':'只修改页面模板/区块,不改变正文原数据、URL、SEO 和 Canonical'} + +@app.get('/api/sites/{site_id}/schemas') +def site_schemas(site_id:str): + get_site(site_id) + rows=db.rows('SELECT content_type,fields_json FROM content_schemas WHERE site_id=?',(site_id,)) + result={k:v for k,v in blueprints.CONTENT_MODELS.items()} + for row in rows:result[row['content_type']]=json.loads(row['fields_json']) + return result + +@app.put('/api/sites/{site_id}/schemas/{kind}') +def update_schema(site_id:str,kind:str,payload:list[dict]=Body(...)): + get_site(site_id) + if kind not in TYPES:raise HTTPException(400,'未知内容类型') + if len(payload)>40:raise HTTPException(400,'最多允许40个自定义字段') + clean=[];names=set() + for field in payload: + name=str(field.get('name') or '') + if not re.fullmatch('[a-zA-Z_][a-zA-Z0-9_]{0,50}',name) or name in names:raise HTTPException(400,'字段名称不合法或重复') + typ=field.get('type','text') + if typ not in ('text','textarea','number','date','url','boolean'):raise HTTPException(400,'不支持该字段类型') + names.add(name);clean.append({'name':name,'label':str(field.get('label') or name)[:80],'type':typ}) + with db.connect() as conn: + conn.execute('INSERT INTO content_schemas(site_id,content_type,fields_json) VALUES (?,?,?) ON CONFLICT(site_id,content_type) DO UPDATE SET fields_json=excluded.fields_json',(site_id,kind,json.dumps(clean,ensure_ascii=False))) + return {'content_type':kind,'fields':clean} + +@app.get('/api/sites/{site_id}/pages/{entry_id}/blueprint') +def read_blueprint(site_id:str,entry_id:str): + entry=get_entry(site_id,entry_id) + kind=entry['meta'].get('page_template') or ('home' if entry['route']=='/' else 'product' if entry['type'] in ('product','vehicle') else 'article' if entry['type']=='post' else 'standard') + return {'entry_id':entry_id,'page_template':kind,'layout_mode':entry['meta'].get('layout_mode','append'),'blocks':entry['meta'].get('blocks',[]),'entry':entry} + +class BlueprintPayload(BaseModel): + blocks:list[dict] + page_template:str='standard' + layout_mode:str='append' + +@app.put('/api/sites/{site_id}/pages/{entry_id}/blueprint') +def save_blueprint(site_id:str,entry_id:str,payload:BlueprintPayload): + entry=get_entry(site_id,entry_id) + if payload.page_template not in blueprints.PAGE_TEMPLATES:raise HTTPException(400,'不支持的页面模板') + try:validated=blueprints.validate_blocks(payload.blocks) + except ValueError as exc:raise HTTPException(400,str(exc)) + if payload.layout_mode not in ('append','replace'):raise HTTPException(400,'不支持的页面设计模式') + meta={**entry['meta'],'page_template':payload.page_template,'layout_mode':payload.layout_mode,'blocks':validated} + save_entry(site_id,{**entry,'meta':meta},existing_id=entry_id) + return {'saved':True,'page_template':payload.page_template,'layout_mode':payload.layout_mode,'blocks':validated} + +@app.post('/api/sites/{site_id}/pages/{entry_id}/blueprint/preset') +def reset_blueprint(site_id:str,entry_id:str,payload:dict=Body(default_factory=dict)): + entry=get_entry(site_id,entry_id) + kind=payload.get('page_template','home' if entry['route']=='/' else 'standard') + if kind not in blueprints.PAGE_TEMPLATES:raise HTTPException(400,'未知的页面模板') + site=get_site(site_id) + blocks=blueprints.theme_blocks(site['theme'],kind,(theme_store.pinned(site_id) or {}).get('version')) + meta={**entry['meta'],'blocks':blocks,'page_template':kind} + save_entry(site_id,{**entry,'meta':meta},existing_id=entry_id) + return {'page_template':kind,'blocks':blocks} + +@app.post('/api/sites/{site_id}/pages/{entry_id}/editor-preview',response_class=HTMLResponse) +def editor_preview(site_id:str,entry_id:str,payload:BlueprintPayload): + site=get_site(site_id);page=get_entry(site_id,entry_id) + try:blocks=blueprints.validate_blocks(payload.blocks) + except (ValueError,TypeError) as exc:raise HTTPException(400,str(exc)) + site['schemas']={k:v for k,v in blueprints.CONTENT_MODELS.items()} + for row in db.rows('SELECT content_type,fields_json FROM content_schemas WHERE site_id=?',(site_id,)): + site['schemas'][row['content_type']]=json.loads(row['fields_json']) + content=blueprints.render_blocks(blocks,site,page,[entry_dict(x) for x in db.rows("SELECT * FROM entries WHERE site_id=? AND status='published'",(site_id,))]) + # The editor now uses the same trusted theme renderer as production builds. + # CSS assets are inlined because srcdoc is sandboxed and has no /assets mount. + from . import theme_pro, theme_store, responsive + import re as _re + from markupsafe import Markup + from jinja2 import Environment,FileSystemLoader,select_autoescape + settings=site.get('settings') or {} + site['settings_json']=json.dumps(settings,ensure_ascii=False) + records=db.rows("SELECT * FROM entries WHERE site_id=? AND status='published'",(site_id,)) + page['meta_json']=json.dumps({**page['meta'],'layout_mode':payload.layout_mode},ensure_ascii=False) + data=theme_store.pinned(site_id) + theme_dir=theme_store.resolve(site['theme'],data['version'] if data else None) + if site['theme'] in theme_pro.THEMES and theme_dir.resolve()==(ROOT/'themes'/site['theme']).resolve(): + html=theme_pro.render(site['theme'],site,page,records,blocks_html=content) + elif (theme_dir/'pro.html').exists(): + env=Environment(loader=FileSystemLoader([str(theme_dir),str(ROOT/'themes')]),autoescape=select_autoescape(['html'])) + ctx=theme_pro.make_context(site,page,records) + ctx.update(custom_blocks=Markup(content),has_custom=bool(content),noindex=False, + designer_replace=payload.layout_mode=='replace',theme_overrides=True) + html=env.get_template('pro.html').render(**ctx) + else: + html=''+content+'' + styles='\n'.join([ + (ROOT/'app'/'static'/'pro-base.css').read_text(encoding='utf-8'), + (ROOT/'app'/'static'/'commercial.css').read_text(encoding='utf-8'), + (ROOT/'app'/'static'/'blocks.css').read_text(encoding='utf-8'), + (ROOT/'app'/'static'/'factory.css').read_text(encoding='utf-8'), + (theme_dir/'pro.css').read_text(encoding='utf-8') if (theme_dir/'pro.css').exists() else '', + (theme_dir/'theme.css').read_text(encoding='utf-8'), + responsive.editor_css(blocks)]) + html=_re.sub(r']*>', '', html) + html=_re.sub(r']*>', '', html) + html=html.replace('','') + bridge = r'''''' + return HTMLResponse(html.replace('',bridge+'')) + +@app.put('/api/sites/{site_id}/contact-settings') +def update_contact_settings(request:Request,site_id:str,payload:dict=Body(...)): + enterprise.require(request,site_id,'owner') + site=get_site(site_id) + email=str(payload.get('email') or '').strip()[:180] + endpoint=str(payload.get('form_endpoint') or '').strip() + if email and not re.fullmatch(r'[^@\s]+@[^@\s]+\.[^@\s]+',email): + raise HTTPException(400,'联系邮箱格式不正确') + if endpoint and (not endpoint.startswith('https://') or any(c in endpoint for c in ('\"','<','>','\n','\r')) or len(endpoint)>500): + raise HTTPException(400,'表单服务地址必须是有效的 HTTPS URL') + settings={**site['settings'],'email':email,'form_endpoint':endpoint, + 'footer_text':str(payload.get('footer_text') or '')[:600]} + db.execute('UPDATE sites SET settings_json=?,updated_at=? WHERE id=?',(json.dumps(settings,ensure_ascii=False),db.now(),site_id)) + return {'saved':True,'email':email,'form_endpoint':endpoint,'footer_text':settings['footer_text'], + 'note':'外部表单服务必须另外开通、配置隐私与防垃圾验证,SiteForge不保存表单数据'} + +@app.put('/api/sites/{site_id}/navigation') +def update_navigation(site_id:str,items:list[dict]=Body(...)): + site=get_site(site_id) + if len(items)>25:raise HTTPException(400,'最多25个导航项目') + nav=[] + for item in items: + label=str(item.get('title') or '').strip()[:60] + path=route_path(item.get('route') or '/') + if not label:raise HTTPException(400,'导航标题不能为空') + nav.append({'title':label,'route':path}) + settings={**site['settings'],'nav':nav} + db.execute('UPDATE sites SET settings_json=?,updated_at=? WHERE id=?',(json.dumps(settings,ensure_ascii=False),db.now(),site_id)) + return {'nav':nav} + +@app.post('/api/sites/{site_id}/import') +async def import_file(site_id:str,file:UploadFile=File(...),on_conflict:str=Form('skip')): + get_site(site_id) + content=await file.read(15_000_001) + if len(content)>15_000_000:raise HTTPException(413,'导入文件不能超过 15MB') + try:result=importers.import_data_file(site_id,file.filename or '',content,on_conflict) + except Exception as exc:fail(exc) + db.insert('INSERT INTO imports(id,site_id,created_at,source,result_json) VALUES (?,?,?,?,?)', + (uid(),site_id,db.now(),file.filename or 'upload',json.dumps(result,ensure_ascii=False))) + return result + +@app.post('/api/sites/{site_id}/crawl') +def crawl(site_id:str,payload:CrawlIn): + get_site(site_id) + try:result=importers.crawl_site(site_id,payload.url,payload.max_pages,payload.on_conflict) + except Exception as exc:fail(exc) + db.insert('INSERT INTO imports(id,site_id,created_at,source,result_json) VALUES (?,?,?,?,?)', + (uid(),site_id,db.now(),'crawler: '+payload.url,json.dumps(result,ensure_ascii=False))) + old=get_site(site_id);settings=old['settings'];settings['origin']=payload.url + db.execute('UPDATE sites SET settings_json=?,updated_at=? WHERE id=?',(json.dumps(settings),db.now(),site_id)) + return result + +@app.get('/api/sites/{site_id}/imports') +def imports(site_id:str): + get_site(site_id) + return db.rows('SELECT * FROM imports WHERE site_id=? ORDER BY created_at DESC LIMIT 25',(site_id,)) + +@app.post('/api/sites/{site_id}/media') +async def upload_image(site_id:str,file:UploadFile=File(...)): + get_site(site_id) + allowed={'.png','image/png','.jpg','image/jpeg','.jpeg','.webp','image/webp','.gif','image/gif','.avif','image/avif'} + ext=Path(file.filename or '').suffix.lower() + if ext not in ('.png','.jpg','.jpeg','.webp','.gif','.avif'): + raise HTTPException(400,'仅支持 PNG/JPG/WebP/GIF/AVIF 图片') + data=await file.read(8_000_001) + if len(data)>8_000_000:raise HTTPException(413,'图片不能超过 8 MB') + if not (data.startswith(b'\x89PNG') or data.startswith(b'\xff\xd8') or data.startswith(b'GIF8') or data[4:8]==b'ftyp' or (len(data)>12 and data[8:12]==b'WEBP')): + raise HTTPException(400,'不是有效图片格式') + folder=db.DATA/'media'/site_id;folder.mkdir(parents=True,exist_ok=True) + dest=folder/(uid()+ext);dest.write_bytes(data) + return {'url':'media/'+dest.name,'size':len(data)} + +@app.get('/api/sites/{site_id}/media') +def media_list(site_id:str): + get_site(site_id) + folder=db.DATA/'media'/site_id + return [{'url':'media/'+p.name,'name':p.name,'bytes':p.stat().st_size} for p in sorted(folder.glob('*')) if p.is_file()] if folder.exists() else [] + +@app.get('/local-media/{site_id}/{filename}') +def media_view(site_id:str,filename:str): + get_site(site_id) + if not re.fullmatch(r'[0-9a-f]{12}\.(png|jpg|jpeg|webp|gif|avif)',filename):raise HTTPException(404) + path=db.DATA/'media'/site_id/filename + if not path.is_file():raise HTTPException(404) + return FileResponse(path) + +@app.post('/api/sites/{site_id}/redirects') +def add_redirect(site_id:str,payload:RedirectIn): + get_site(site_id) + if payload.status_code not in (301,302,307,308):raise HTTPException(400,'无效的 HTTP 重定向状态') + old=route_path(payload.old_path) + if payload.new_path==old:raise HTTPException(400,'无法重定向到自身') + if not (payload.new_path.startswith('/') or payload.new_path.startswith('https://')):raise HTTPException(400,'无效目标') + try: + db.insert('INSERT INTO redirects(id,site_id,old_path,new_path,status_code) VALUES (?,?,?,?,?)', + (uid(),site_id,old,payload.new_path,payload.status_code)) + except Exception as exc:fail(exc) + return {'created':True} + +@app.get('/api/sites/{site_id}/redirects') +def list_redirects(site_id:str): + get_site(site_id) + return db.rows('SELECT * FROM redirects WHERE site_id=? ORDER BY old_path',(site_id,)) + +@app.delete('/api/sites/{site_id}/redirects/{redirect_id}') +def remove_redirect(site_id:str,redirect_id:str): + get_site(site_id) + db.execute('DELETE FROM redirects WHERE site_id=? AND id=?',(site_id,redirect_id)) + return {'deleted':True} + +@app.post('/api/sites/{site_id}/builds') +def new_build(site_id:str,payload:BuildIn): + get_site(site_id) + try:return builder.build(site_id,payload.mirror_images) + except Exception as exc:fail(exc) + +@app.get('/api/sites/{site_id}/builds') +def list_builds(site_id:str): + get_site(site_id) + return db.rows('SELECT id,created_at,status,total_pages,report_json FROM builds WHERE site_id=? ORDER BY created_at DESC',(site_id,)) + +@app.get('/api/sites/{site_id}/builds/{build_id}/report') +def build_report(site_id:str,build_id:str): + record=db.one('SELECT * FROM builds WHERE site_id=? AND id=?',(site_id,build_id)) + if not record:raise HTTPException(404) + return json.loads(record['report_json']) + +@app.get('/api/sites/{site_id}/builds/{build_id}/download') +def download_build(site_id:str,build_id:str): + record=db.one('SELECT * FROM builds WHERE site_id=? AND id=?',(site_id,build_id)) + if not record:raise HTTPException(404) + archive=Path(record['zip_path']) + if not archive.is_file():raise HTTPException(404,'构建压缩包丢失') + return FileResponse(archive,filename=f'siteforge-{site_id}-{build_id}.zip',media_type='application/zip') + +@app.get('/preview/{site_id}/{build_id}/{path:path}') +def preview_path(site_id:str,build_id:str,path:str): + b=db.one('SELECT id FROM builds WHERE site_id=? AND id=?',(site_id,build_id)) + if not b:raise HTTPException(404) + dist=(db.DATA/'builds'/site_id/build_id/'dist').resolve() + target=dist/(path or 'index.html') + if target.is_dir():target=target/'index.html' + if not target.exists() and not Path(path).suffix:target=dist/path/'index.html' + if not target.resolve().is_relative_to(dist):raise HTTPException(404) + if not target.is_file():raise HTTPException(404) + if target.suffix.lower() in ('.html','.htm'): + html=target.read_text(encoding='utf-8') + html=html.replace('',f'') + return HTMLResponse(html) + return FileResponse(target) + +@app.get('/preview/{site_id}/{build_id}') +def preview_root(site_id:str,build_id:str): + return RedirectResponse(f'/preview/{site_id}/{build_id}/') + +@app.get('/api/presets') +def get_presets(): + return db.rows('SELECT id,name,theme,created_at FROM presets ORDER BY created_at DESC') + +@app.post('/api/sites/{site_id}/save-preset') +def save_preset(site_id:str,payload:dict=Body(default_factory=dict)): + s=get_site(site_id) + homepage=db.one('SELECT meta_json FROM entries WHERE site_id=? AND route="/"',(site_id,)) + meta=json.loads(homepage['meta_json'] or '{}') if homepage else {} + blocks=meta.get('blocks',[]) + pid=uid() + name=str(payload.get('name') or s['name']+' 模板')[:150] + db.insert('INSERT INTO presets(id,name,theme,settings_json,home_blocks_json,created_at) VALUES (?,?,?,?,?,?)', + (pid,name,s['theme'],json.dumps(s['settings'],ensure_ascii=False),json.dumps(blocks,ensure_ascii=False),db.now())) + return {'id':pid,'name':name,'theme':s['theme']} + + +@app.post('/api/sites/{site_id}/media-archive') +async def import_media_archive(site_id:str,file:UploadFile=File(...)): + """Upload a WordPress wp-content/uploads ZIP. Reject executable formats and unsafe paths.""" + import zipfile + import stat + get_site(site_id) + if not (file.filename or '').lower().endswith('.zip'): + raise HTTPException(400,'请上传 .zip 媒体压缩包') + data=await file.read(80_000_001) + if len(data)>80_000_000:raise HTTPException(413,'压缩包最大 80MB') + count=0;skipped=0;size=0 + allowed={'.png','.jpg','.jpeg','.gif','.webp','.avif','.pdf','.woff','.woff2','.ttf','.ico'} + target=db.DATA/'legacy'/site_id + try: + with zipfile.ZipFile(io.BytesIO(data)) as z: + infos=z.infolist() + if len(infos)>4000:raise ValueError('压缩包文件数过多') + for member in infos: + size+=member.file_size + if size>250_000_000:raise ValueError('解压后的数据总量超过 250MB') + for member in infos: + name=member.filename.replace('\\','/').lstrip('/') + parts=name.split('/') + if member.is_dir():continue + if any(p in ('.','..') for p in parts) or ':' in name or name.startswith('~'): + skipped+=1;continue + if stat.S_ISLNK(member.external_attr>>16):skipped+=1;continue + if Path(name).suffix.lower() not in allowed or member.file_size>25_000_000: + skipped+=1;continue + if 'wp-content' in parts: + name='/'.join(parts[parts.index('wp-content'):]) + elif 'uploads' in parts: + name='wp-content/'+'/'.join(parts[parts.index('uploads'):]) + else: + name='media/imported/'+name + dst=target/name + if not dst.resolve().is_relative_to(target.resolve()):skipped+=1;continue + dst.parent.mkdir(parents=True,exist_ok=True) + with z.open(member) as source, open(dst,'wb') as out: + shutil.copyfileobj(source,out) + count+=1 + except Exception as exc:fail(exc) + return {'imported':count,'skipped':skipped,'bytes_uncompressed':size, + 'message':'旧媒体将按原路径打包进下次构建;单独的源站页面和 URL 仍需核验'} + +@app.post('/api/sites/{site_id}/builds/{build_id}/publish-local') +def publish_local(site_id:str,build_id:str): + """Opt-in atomic local release; requires SITEFORGE_PUBLISH_ROOT env.""" + import os + root=os.environ.get('SITEFORGE_PUBLISH_ROOT','').strip() + if not root:raise HTTPException(403,'本地部署未启用,请设置 SITEFORGE_PUBLISH_ROOT') + get_site(site_id) + build_record=db.one('SELECT * FROM builds WHERE site_id=? AND id=?',(site_id,build_id)) + if not build_record:raise HTTPException(404,'构建版本不存在') + site=get_site(site_id) + if site['mode']=='migration': + report=enterprise.diff_baseline(site_id) + if report['baseline_count']==0:raise HTTPException(409,'迁移网站尚未导入旧 URL 基线,禁止直接发布') + if report['errors']:raise HTTPException(409,f'旧站 URL 基线检查未通过:缺失 {report["errors"]} 项') + # Four-eyes rule is enabled by default in the V2 Docker configuration. + if __import__('os').environ.get('SITEFORGE_REQUIRE_RELEASE_APPROVAL','false').lower() in ('1','true','yes'): + approval=db.one('SELECT id FROM release_requests WHERE site_id=? AND build_id=? AND state=? ORDER BY reviewed_at DESC LIMIT 1', + (site_id,build_id,'approved')) + if not approval:raise HTTPException(409,'当前构建版本尚未获得独立审批,禁止发布') + # Migration gate is re-evaluated on publish, not only at approval time. + if site['mode']=='migration' and enterprise.diff_baseline(site_id)['errors']: + raise HTTPException(409,'迁移基线再次检测未通过') + src=db.DATA/'builds'/site_id/build_id/'dist' + if not src.is_dir():raise HTTPException(404,'构建文件缺失') + parent=Path(root).resolve()/site_id + releases=parent/'releases';releases.mkdir(parents=True,exist_ok=True) + dest=releases/build_id + if not dest.exists():shutil.copytree(src,dest) + current=parent/'current' + if os.name=='nt': + previous=parent/'previous' + if previous.exists():shutil.rmtree(previous) + if current.exists():current.rename(previous) + shutil.copytree(dest,current) + else: + tmp=parent/('current-tmp-'+uid()) + tmp.symlink_to(dest,target_is_directory=True) + os.replace(tmp,current) + enterprise.db.insert('INSERT INTO release_history VALUES (?,?,?,?,?,?)',(enterprise.uid(),site_id,build_id,'system','publish',db.now())) + return {'published':True,'root':str(current),'release':build_id,'message':'请确认 Nginx/Caddy 的站点根目录指向 current;DNS/CDN 不会被自动修改'} diff --git a/app/meta.py b/app/meta.py new file mode 100644 index 0000000..49e58c5 --- /dev/null +++ b/app/meta.py @@ -0,0 +1,6 @@ +"""Application identity shared by the API, health checks and UI clients.""" + +APP_NAME = "SiteForge" +APP_VERSION = "5.0.0" +API_TITLE = "SiteForge 网站工厂" +API_DESCRIPTION = "Docker-first visual website factory" diff --git a/app/migration_audit.py b/app/migration_audit.py new file mode 100644 index 0000000..4b2180c --- /dev/null +++ b/app/migration_audit.py @@ -0,0 +1,245 @@ +"""Offline, reproducible website migration inventory and built-artifact comparison. + +No network calls. The source manifest is treated as untrusted input and never as +an instruction. Unsupported dynamic URLs are reported instead of rewritten. +""" +from __future__ import annotations +import csv, hashlib, io, json, re +from pathlib import Path +from urllib.parse import urlsplit +from bs4 import BeautifulSoup +from defusedxml import ElementTree as ET +from fastapi import APIRouter, Body, File, HTTPException, Request, UploadFile +from . import db, enterprise, builder +from .core import route_path + +router=APIRouter(prefix='/api/audit',tags=['Migration Audit V4']) +MAX_BASELINE=20_000 + +def setup(): + with db.connect() as c: + c.executescript('''CREATE TABLE IF NOT EXISTS source_audit_fields( + site_id TEXT NOT NULL, route TEXT NOT NULL, + robots TEXT NOT NULL DEFAULT '', h1 TEXT NOT NULL DEFAULT '', + original_url TEXT NOT NULL DEFAULT '', + PRIMARY KEY(site_id,route), + FOREIGN KEY(site_id) REFERENCES sites(id) ON DELETE CASCADE); + CREATE TABLE IF NOT EXISTS source_audit_assets( + site_id TEXT NOT NULL, url TEXT NOT NULL, + source_route TEXT NOT NULL DEFAULT '', + PRIMARY KEY(site_id,url,source_route), + FOREIGN KEY(site_id) REFERENCES sites(id) ON DELETE CASCADE); + CREATE TABLE IF NOT EXISTS source_audit_exceptions( + site_id TEXT NOT NULL, build_id TEXT NOT NULL, issue_id TEXT NOT NULL, + reviewer TEXT NOT NULL, explanation TEXT NOT NULL, decided_at TEXT NOT NULL, + PRIMARY KEY(site_id,build_id,issue_id)); + ''') + +def _route(value:str): + text=str(value or '').strip() + if not text:raise ValueError('缺少页面路径') + u=urlsplit(text) + if u.query:raise ValueError('需要单独处理带查询参数的动态 URL') + if u.scheme and u.scheme not in ('https','http'):raise ValueError('不支持的 URL 协议') + return route_path(u.path or '/') + +def _str(row,*keys): + for key in keys: + if row.get(key) is not None and str(row[key]).strip():return str(row[key]).strip() + return '' + +def _normalize(row): + original=_str(row,'url','loc','original_url','link','route','path') + route=_route(original) + status=int(row.get('status') or 200) + if status not in (200,301,302,307,308,404,410):raise ValueError('不支持的 HTTP 状态') + return {'route':route,'original_url':original,'seo_title':_str(row,'seo_title','title','meta_title'), + 'description':_str(row,'description','seo_description','meta_description'), + 'canonical':_str(row,'canonical'),'robots':_str(row,'robots'), + 'h1':_str(row,'h1'),'status':status} + +def _wp_item(element): + def get(local,default=''): + for ch in element: + if ch.tag.rsplit('}',1)[-1]==local:return (ch.text or default) + return default + def get_content(local): + for ch in element: + if ch.tag.rsplit('}',1)[-1]==local:return ch.text or '' + return '' + meta={} + for child in element: + if child.tag.rsplit('}',1)[-1]=='postmeta': + key=value='' + for part in child: + if part.tag.endswith('meta_key'):key=part.text or '' + elif part.tag.endswith('meta_value'):value=part.text or '' + if key:meta[key]=value + return (get('post_type','post'),get('status','publish'),get('link'),get('title'),get_content('encoded'),meta) + +def from_file(filename:str,blob:bytes): + if len(blob)>16_000_000:raise ValueError('源站清单不能超过 16 MB') + name=filename.lower();rows=[];assets=[];warnings=[] + if name.endswith('.csv'): + rows=list(csv.DictReader(io.StringIO(blob.decode('utf-8-sig')))) + elif name.endswith('.json'): + data=json.loads(blob.decode('utf-8-sig')) + rows=data.get('urls',data.get('pages',[])) if isinstance(data,dict) else data + if not isinstance(rows,list):raise ValueError('JSON 应包含 pages/urls 数组') + elif name.endswith('.xml'): + root=ET.fromstring(blob) + root_name=root.tag.rsplit('}',1)[-1] + if root_name=='urlset': + rows=[{'url':next((c.text or '' for c in node if c.tag.rsplit('}',1)[-1]=='loc'),'')} for node in root] + elif root_name=='rss': + for item in root.findall('./channel/item'): + typ,status,url,title,body,meta=_wp_item(item) + if typ=='attachment': + if url:assets.append((url,'')) + continue + if status!='publish' or typ in ('revision','nav_menu_item'):continue + if not url: + warnings.append('WXR 某条记录缺少真实链接,无法推断原路径:'+title[:70]);continue + rows.append({'url':url,'title':meta.get('_yoast_wpseo_title') or meta.get('rank_math_title') or title, + 'description':meta.get('_yoast_wpseo_metadesc') or meta.get('rank_math_description') or '', + 'canonical':meta.get('_yoast_wpseo_canonical') or meta.get('rank_math_canonical_url') or '', + 'robots':'noindex,follow' if meta.get('_yoast_wpseo_meta-robots-noindex')=='1' else '', + 'h1':title}) + soup=BeautifulSoup(body,'html.parser') + for img in soup.select('img[src]'): + assets.append((img.get('src',''),url)) + else:raise ValueError('XML 需要 WordPress WXR 或 sitemap.xml') + else:raise ValueError('仅支持 .csv / .json / WordPress WXR.xml / sitemap.xml') + if not 1<=len(rows)<=MAX_BASELINE:raise ValueError('URL 清单需要1至20000条') + out=[];errors=[];seen=set() + for i,row in enumerate(rows,1): + try: + if not isinstance(row,dict):raise ValueError('记录不是对象') + normalized=_normalize(row) + if normalized['route'] in seen:raise ValueError('原路径重复') + seen.add(normalized['route']);out.append(normalized) + except Exception as exc:errors.append({'row':i,'message':str(exc)[:180]}) + return {'records':out,'assets':assets,'errors':errors,'warnings':warnings,'total':len(rows)} + +def save(site_id,manifest,replace=True): + if manifest['errors']:raise ValueError('URL 清单存在格式错误,请修正后再导入') + with db.connect() as c: + if replace: + c.execute('DELETE FROM migration_baseline WHERE site_id=?',(site_id,)) + c.execute('DELETE FROM source_audit_fields WHERE site_id=?',(site_id,)) + c.execute('DELETE FROM source_audit_assets WHERE site_id=?',(site_id,)) + for row in manifest['records']: + c.execute('''INSERT INTO migration_baseline(site_id,route,seo_title,description,canonical,status) + VALUES(?,?,?,?,?,?) ON CONFLICT(site_id,route) DO UPDATE SET + seo_title=excluded.seo_title,description=excluded.description,canonical=excluded.canonical,status=excluded.status''', + (site_id,row['route'],row['seo_title'],row['description'],row['canonical'],row['status'])) + c.execute('''INSERT INTO source_audit_fields(site_id,route,robots,h1,original_url) VALUES(?,?,?,?,?) + ON CONFLICT(site_id,route) DO UPDATE SET robots=excluded.robots,h1=excluded.h1,original_url=excluded.original_url''', + (site_id,row['route'],row['robots'],row['h1'],row['original_url'])) + for url,origin in manifest['assets'][:MAX_BASELINE]: + if not url or len(url)>1000:continue + c.execute('INSERT OR IGNORE INTO source_audit_assets VALUES(?,?,?)',(site_id,url,origin[:500])) + return {'imported':len(manifest['records']),'asset_references':len(manifest['assets']),'warnings':manifest['warnings']} + +def _tag(soup,name,attrs=None): + node=soup.find(name,attrs=attrs or {}) + return node.get('content','').strip() if node and name=='meta' else node.get_text(' ',strip=True) if node else '' + +def audit(site_id,build_id): + # Actual generated bytes, not source entry metadata, are the authority. + root=(db.DATA/'builds'/site_id/build_id/'dist').resolve() + if not root.is_dir() or not db.one('SELECT id FROM builds WHERE id=? AND site_id=?',(build_id,site_id)): + raise ValueError('构建版本不存在') + baseline=db.rows('''SELECT b.*,x.robots,x.h1,x.original_url FROM migration_baseline b + LEFT JOIN source_audit_fields x ON b.site_id=x.site_id AND b.route=x.route + WHERE b.site_id=? ORDER BY b.route''',(site_id,)) + if not baseline:return {'baseline_count':0,'errors':1,'warnings':0,'passed':False, + 'issues':[{'severity':'error','field':'baseline','message':'未导入源站 URL 清单'}]} + redirects={r['old_path']:(r['new_path'],r['status_code']) for r in db.rows('SELECT old_path,new_path,status_code FROM redirects WHERE site_id=?',(site_id,))} + issues=[];matched=0 + for old in baseline: + route=old['route'];expect_status=old['status']; target=builder.output_path(root,route) + if expect_status in (301,302,307,308,404,410): + if expect_status in (301,302,307,308): + if route not in redirects: + issues.append({'route':route,'field':'redirect','severity':'error','message':'历史重定向未配置'}) + elif redirects[route][1]!=expect_status: + issues.append({'route':route,'field':'redirect_status','severity':'error','message':'重定向状态码与旧站不一致'}) + elif target.is_file(): + issues.append({'route':route,'field':'http_status','severity':'error','message':'旧站的不存在/已删除页面被错误地生成了内容'}) + continue + if not target.is_file(): + issues.append({'route':route,'field':'url','severity':'error','message':'旧站正常页面在生成网站中缺失'}) + continue + matched+=1 + soup=BeautifulSoup(target.read_text(encoding='utf-8',errors='replace'),'html.parser') + title=_tag(soup,'title') + desc_node=soup.find('meta',attrs={'name':re.compile('^description$',re.I)}) + description=(desc_node.get('content','') if desc_node else '').strip() + canonical_node=soup.find('link',rel=lambda value:value and 'canonical' in value) + canonical=(canonical_node.get('href','') if canonical_node else '').strip() + robot_node=soup.find('meta',attrs={'name':re.compile('^robots$',re.I)}) + robots=(robot_node.get('content','') if robot_node else '').strip().lower() + h1=_tag(soup,'h1') + for source,current,label in [('seo_title',title,'title'),('description',description,'description'),('canonical',canonical,'canonical'),('robots',robots,'robots'),('h1',h1,'h1')]: + if old.get(source) and ' '.join(str(old[source]).split())!=' '.join(str(current).split()): + issues.append({'route':route,'field':label,'severity':'warning','message':'旧新页面字段不一致', + 'before':str(old[source])[:160],'after':str(current)[:160]}) + other=__import__('app.quality',fromlist=['report']).report(site_id,build_id) + for q in other['issues']: + if q['severity']=='error':issues.append({'route':q['file'],'field':q['type'],'severity':'error','message':str(q.get('target','构建质量问题'))[:180]}) + decisions={x['issue_id']:x for x in db.rows('SELECT * FROM source_audit_exceptions WHERE site_id=? AND build_id=?',(site_id,build_id))} + for issue in issues: + token=json.dumps([issue.get(k,'') for k in ('route','field','message','before','after')],ensure_ascii=False,sort_keys=True) + issue['issue_id']=hashlib.sha256(token.encode()).hexdigest()[:24] + approved=decisions.get(issue['issue_id']) + issue['reviewed']=bool(approved) if issue['severity']=='warning' else False + if approved: + issue['reviewer']=approved['reviewer'] + issue['review_reason']=approved['explanation'] + outstanding=sum(i['severity']=='warning' and not i['reviewed'] for i in issues) + return {'baseline_count':len(baseline),'matched_pages':matched, + 'errors':sum(x['severity']=='error' for x in issues), + 'warnings':sum(x['severity']=='warning' for x in issues), + 'passed':not any(x['severity']=='error' for x in issues),'unreviewed_warnings':outstanding, + 'quality':{'files':other['files'],'errors':other['errors'],'warnings':other['warnings']}, + 'issues':issues[:2000]} + +@router.post('/sites/{site_id}/import-baseline') +async def import_baseline(request:Request,site_id:str,file:UploadFile=File(...)): + enterprise.require(request,site_id,'editor') + data=await file.read(16_000_001) + try: + manifest=from_file(file.filename or '',data) + if manifest['errors']: + return {'imported':0,'errors':manifest['errors'][:200],'total':manifest['total'],'warnings':manifest['warnings']} + return save(site_id,manifest) + except (ValueError,UnicodeDecodeError) as exc:raise HTTPException(400,str(exc)) + +@router.get('/sites/{site_id}/builds/{build_id}/report') +def report(request:Request,site_id:str,build_id:str): + enterprise.require(request,site_id,'viewer') + try:return audit(site_id,build_id) + except ValueError as exc:raise HTTPException(404,str(exc)) + +@router.post('/sites/{site_id}/builds/{build_id}/review-warning') +def approve_warning(request:Request,site_id:str,build_id:str,payload:dict=Body(...)): + enterprise.require(request,site_id,'reviewer') + issue_id=str(payload.get('issue_id','')) + reason=str(payload.get('reason','')).strip() + if len(reason)<10 or len(reason)>2000:raise HTTPException(400,'请填写至少10个字符的复核原因及处理依据') + evidence=audit(site_id,build_id) + issue=next((x for x in evidence['issues'] if x['issue_id']==issue_id and x['severity']=='warning'),None) + if not issue:raise HTTPException(404,'只能复核此构建版本当前存在的警告') + db.insert('INSERT INTO source_audit_exceptions(site_id,build_id,issue_id,reviewer,explanation,decided_at) VALUES(?,?,?,?,?,?) ON CONFLICT(site_id,build_id,issue_id) DO UPDATE SET reviewer=excluded.reviewer,explanation=excluded.explanation,decided_at=excluded.decided_at', + (site_id,build_id,issue_id,request.state.actor,reason,db.now())) + return {'approved':True,'issue_id':issue_id,'reviewer':request.state.actor} + +@router.get('/sites/{site_id}/builds/{build_id}/report.csv') +def report_csv(request:Request,site_id:str,build_id:str): + from fastapi.responses import Response + enterprise.require(request,site_id,'viewer') + result=audit(site_id,build_id) + dest=io.StringIO();writer=csv.DictWriter(dest,fieldnames=['severity','route','field','message','before','after'],extrasaction='ignore') + writer.writeheader();writer.writerows(result['issues']) + return Response('\ufeff'+dest.getvalue(),media_type='text/csv',headers={'Content-Disposition':'attachment; filename="migration-audit.csv"'}) diff --git a/app/quality.py b/app/quality.py new file mode 100644 index 0000000..0f079ee --- /dev/null +++ b/app/quality.py @@ -0,0 +1,56 @@ +"""Offline static-build quality gate. No external network requests or speculative SEO scores.""" +from pathlib import Path +from urllib.parse import urlsplit,unquote +from bs4 import BeautifulSoup +from . import db + +def report(site_id, build_id): + root=(db.DATA/'builds'/site_id/build_id/'dist').resolve() + if not root.is_dir():raise FileNotFoundError('构建版本不存在') + files=list(root.rglob('*.html')) + site=db.one('SELECT domain FROM sites WHERE id=?',(site_id,)) or {} + hostname=urlsplit(site.get('domain') or '').hostname + issues=[];seen_titles={};seen_descriptions={} + for p in files: + rel=p.relative_to(root).as_posix() + soup=BeautifulSoup(p.read_text(encoding='utf-8',errors='replace'),'html.parser') + title=(soup.title.get_text(' ',strip=True) if soup.title else '') + h1=soup.find_all('h1') + if not title:issues.append({'file':rel,'type':'missing_title','severity':'error'}) + if len(h1)!=1:issues.append({'file':rel,'type':'h1_count','severity':'warning','count':len(h1)}) + if title and title in seen_titles and p.name!='404.html':issues.append({'file':rel,'type':'duplicate_title','severity':'warning','other':seen_titles[title]}) + seen_titles[title]=rel + description=soup.find('meta',attrs={'name':'description'}) + desc=description.get('content','').strip() if description else '' + if not desc:issues.append({'file':rel,'type':'missing_description','severity':'warning'}) + if desc and desc in seen_descriptions:issues.append({'file':rel,'type':'duplicate_description','severity':'warning','other':seen_descriptions[desc]}) + seen_descriptions[desc]=rel + for img in soup.find_all('img'): + if not img.get('alt'):issues.append({'file':rel,'type':'missing_image_alt','severity':'warning','src':img.get('src','')[:180]}) + for node in soup.find_all(['a','img','script','link']): + attr='href' if node.name in ('a','link') else 'src' + target=node.get(attr) + if not target:continue + parsed=urlsplit(target) + if parsed.scheme in ('http','https'): + if parsed.hostname!=hostname:continue + path=parsed.path + elif parsed.scheme or target.startswith('//'):continue + elif target.startswith('#'):continue + else:path=parsed.path + if not path:continue + if node.name=='link' and node.get('rel') and 'canonical' in node.get('rel'):continue + from_root=path.startswith('/') + # HTML uses , which makes relative paths site-root relative. + base=soup.find('base',href=True) + if base and base.get('href')=='/':from_root=True + raw=unquote(path.lstrip('/')) + if not from_root:raw=(p.relative_to(root).parent/raw).as_posix() + test=(root/raw) + if target.endswith('/') or not Path(raw).suffix:test=test/'index.html' + if not test.resolve().is_relative_to(root) or not test.is_file(): + if node.name=='link' and ('alternate' in (node.get('rel') or [])):continue + issues.append({'file':rel,'type':'broken_internal_reference','severity':'error','target':target[:220]}) + return {'files':len(files),'errors':sum(i['severity']=='error' for i in issues), + 'warnings':sum(i['severity']=='warning' for i in issues), + 'passed':not any(i['severity']=='error' for i in issues),'issues':issues[:500]} diff --git a/app/relations.py b/app/relations.py new file mode 100644 index 0000000..f695930 --- /dev/null +++ b/app/relations.py @@ -0,0 +1,50 @@ +"""Content assembly: stable, site-scoped brand/category relationships. + +Only materializes associations supported by source data. Generated brand shells +are clearly marked and contain no fabricated copy. +""" +from collections import defaultdict +from . import db +from .core import save_entry +from .theme_pro import fields,slug + +KINDS=('product','vehicle') + +def preview(site_id): + records=db.rows("SELECT * FROM entries WHERE site_id=? AND type IN ('product','vehicle','brand')",(site_id,)) + brand_pages={r['title'].casefold():r for r in records if r['type']=='brand'} + routes={r['route'] for r in db.rows('SELECT route FROM entries WHERE site_id=?',(site_id,))} + brands=defaultdict(list);cats=defaultdict(list);issues=[] + for row in records: + if row['type'] not in KINDS:continue + attrs=fields(row) + brand=str(attrs.get('brand') or '').strip();category=str(attrs.get('category') or '').strip() + if brand: brands[brand].append(row['route']) + else:issues.append({'route':row['route'],'severity':'review','message':'未填写品牌'}) + if category:cats[category].append(row['route']) + else:issues.append({'route':row['route'],'severity':'review','message':'未填写分类'}) + if not row['cover']:issues.append({'route':row['route'],'severity':'review','message':'缺少产品封面图'}) + candidates=[] + for name,products in brands.items(): + if name.casefold() in brand_pages:continue + url=f'/brands/{slug(name)}/' + if url in routes: + issues.append({'route':url,'severity':'block','message':'品牌自动生成路径与已有页面冲突'});continue + candidates.append({'brand':name,'route':url,'products':len(products)}) + return {'brands':[{ 'name':k,'items':len(v)} for k,v in brands.items()], + 'categories':[{'name':k,'items':len(v)} for k,v in cats.items()], + 'brand_pages_missing':candidates,'issues':issues, + 'products':sum(len(x) for x in brands.values()), + 'note':'仅根据已有产品字段建立关系;缺少真实品牌介绍/图片时不会自动编造。'} + +def commit(site_id): + result=preview(site_id) + if any(issue['severity']=='block' for issue in result['issues']): + raise ValueError('存在路径冲突,请先处理后再装配') + created=[] + for brand in result['brand_pages_missing']: + save_entry(site_id,{'type':'brand','title':brand['brand'],'route':brand['route'], + 'summary':'','body':'','status':'published','meta':{'fields':{'source':'auto-assembled'},'source_auto_assembled':True}}, + source='relations-assembly') + created.append(brand['route']) + return {'created_brand_pages':created,'count':len(created),'preview':result} diff --git a/app/responsive.py b/app/responsive.py new file mode 100644 index 0000000..8675ae7 --- /dev/null +++ b/app/responsive.py @@ -0,0 +1,25 @@ +"""Safe per-device block CSS using validated data-block identifiers.""" +import re,json + +def editor_css(blocks): + tablet=[];mobile=[] + for b in blocks: + uid=b.get('id','') + if not re.fullmatch('[a-zA-Z0-9_-]{4,50}',uid):continue + s=b.get('style') or {} + try: + tp=min(120,max(0,int(s.get('tablet_padding',50)))) + mp=min(120,max(0,int(s.get('mobile_padding',28)))) + except (TypeError,ValueError):tp,mp=50,28 + tablet.append(f'.sf-section[data-block="{uid}"]{{--sf-pad:{tp}px}}') + mobile.append(f'.sf-section[data-block="{uid}"]{{--sf-pad:{mp}px}}') + if s.get('hide_tablet') is True:tablet.append(f'.sf-section[data-block="{uid}"]{{display:none}}') + if s.get('hide_mobile') is True:mobile.append(f'.sf-section[data-block="{uid}"]{{display:none}}') + return '@media(max-width:820px){'+''.join(tablet)+'}@media(max-width:540px){'+''.join(mobile)+'}' + +def site_css(entries): + items=[] + for entry in entries: + meta=json.loads(entry.get('meta_json') or '{}') + items.extend(meta.get('blocks',[]) if isinstance(meta.get('blocks'),list) else []) + return editor_css(items) diff --git a/app/reusables.py b/app/reusables.py new file mode 100644 index 0000000..31a3060 --- /dev/null +++ b/app/reusables.py @@ -0,0 +1,53 @@ +"""Site-scoped reusable editor component library with versioned snapshots.""" +import json +from fastapi import APIRouter,Body,HTTPException,Request +from . import db,enterprise,blueprints +from .core import uid + +router=APIRouter(prefix='/api/sites',tags=['Reusable Components']) + +def setup(): + with db.connect() as c: + c.executescript('''CREATE TABLE IF NOT EXISTS reusable_components( + id TEXT PRIMARY KEY,site_id TEXT NOT NULL REFERENCES sites(id) ON DELETE CASCADE, + name TEXT NOT NULL,version INTEGER NOT NULL DEFAULT 1,block_json TEXT NOT NULL, + updated_at TEXT NOT NULL); + CREATE INDEX IF NOT EXISTS idx_reuse_site ON reusable_components(site_id); + ''') + +@router.get('/{site_id}/components') +def all_components(request:Request,site_id:str): + enterprise.require(request,site_id,'viewer') + return [{**r,'block':json.loads(r.pop('block_json'))} for r in db.rows( + 'SELECT * FROM reusable_components WHERE site_id=? ORDER BY updated_at DESC',(site_id,))] + +@router.post('/{site_id}/components') +def save_component(request:Request,site_id:str,payload:dict=Body(...)): + enterprise.require(request,site_id,'editor') + name=str(payload.get('name','')).strip()[:100] + if not name:raise HTTPException(400,'组件名称不能为空') + try:block=blueprints.validate_blocks([payload.get('block')])[0] + except (ValueError,TypeError) as exc:raise HTTPException(400,str(exc)) + cid=uid() + db.insert('INSERT INTO reusable_components VALUES (?,?,?,?,?,?)', + (cid,site_id,name,1,json.dumps(block,ensure_ascii=False),db.now())) + return {'id':cid,'name':name,'version':1,'block':block} + +@router.put('/{site_id}/components/{component_id}') +def update_component(request:Request,site_id:str,component_id:str,payload:dict=Body(...)): + enterprise.require(request,site_id,'editor') + current=db.one('SELECT * FROM reusable_components WHERE site_id=? AND id=?',(site_id,component_id)) + if not current:raise HTTPException(404) + if payload.get('expected_version')!=current['version']:raise HTTPException(409,'组件已被修改,请重新加载') + try:block=blueprints.validate_blocks([payload.get('block')])[0] + except (ValueError,TypeError) as exc:raise HTTPException(400,str(exc)) + db.execute('UPDATE reusable_components SET name=?,block_json=?,version=?,updated_at=? WHERE id=? AND site_id=?', + (str(payload.get('name') or current['name'])[:100],json.dumps(block,ensure_ascii=False),current['version']+1,db.now(),component_id,site_id)) + return {'id':component_id,'version':current['version']+1} + +@router.delete('/{site_id}/components/{component_id}') +def delete_component(request:Request,site_id:str,component_id:str): + enterprise.require(request,site_id,'editor') + if not db.execute('DELETE FROM reusable_components WHERE site_id=? AND id=?',(site_id,component_id)): + raise HTTPException(404) + return {'deleted':True} diff --git a/app/security.py b/app/security.py new file mode 100644 index 0000000..7b34b19 --- /dev/null +++ b/app/security.py @@ -0,0 +1,104 @@ +"""One-owner authentication for the SiteForge Docker Web edition. + +Session cookie is signed and HttpOnly. JSON mutations additionally require a +session-bound CSRF header; this is intentionally a small-team / single-admin +MVP, not a multi-tenant identity provider. +""" +import hashlib +import hmac +import os +import secrets +import time +from collections import defaultdict +from pathlib import Path +from itsdangerous import URLSafeTimedSerializer, BadSignature, SignatureExpired +from fastapi import HTTPException + +COOKIE = 'siteforge_session' +MAX_AGE = 8 * 3600 +SERIALIZER = None +PASSWORD_HASH = b'' +PASSWORD_SALT = b'' +CSRF_KEY = b'' +COOKIE_SECURE = False +FAILURES = defaultdict(list) + + +def configure(): + global SERIALIZER, PASSWORD_HASH, PASSWORD_SALT, CSRF_KEY, COOKIE_SECURE + password_file = os.environ.get('SITEFORGE_ADMIN_PASSWORD_FILE', '').strip() + if password_file: + password = Path(password_file).read_text(encoding='utf-8').strip('\r\n') + else: + password = os.environ.get('SITEFORGE_ADMIN_PASSWORD', '') + secret = os.environ.get('SITEFORGE_SESSION_SECRET', '') + weak = {'password', 'admin123456', 'change-me', 'replace-me', 'changeme', '123456789', 'REPLACE_WITH_STRONG_PASSWORD'} + if len(password) < 12 or password in weak: + raise RuntimeError('请设置至少 12 位的 SITEFORGE_ADMIN_PASSWORD,或配置 SITEFORGE_ADMIN_PASSWORD_FILE') + if len(secret) < 32 or secret in weak or secret == 'REPLACE_WITH_64_RANDOM_CHARACTERS': + raise RuntimeError('请配置至少 32 位的随机 SITEFORGE_SESSION_SECRET') + SERIALIZER = URLSafeTimedSerializer(secret_key=secret, salt='siteforge-web-v02') + # Keep the deployment secret out of memory after startup and make each + # verification deliberately expensive, consistent with team accounts. + PASSWORD_SALT = secrets.token_bytes(16) + PASSWORD_HASH = hashlib.scrypt(password.encode('utf-8'),salt=PASSWORD_SALT,n=2**14,r=8,p=1,dklen=32) + CSRF_KEY = hashlib.sha256(('siteforge-csrf-v02:'+secret).encode()).digest() + COOKIE_SECURE = os.environ.get('SITEFORGE_COOKIE_SECURE', 'false').lower() in ('1', 'true', 'yes') + FAILURES.clear() + + +def check_password(password): + try: + candidate=hashlib.scrypt(password.encode('utf-8'),salt=PASSWORD_SALT,n=2**14,r=8,p=1,dklen=32) + return hmac.compare_digest(candidate,PASSWORD_HASH) + except (ValueError,TypeError,MemoryError): + return False + + +def sign_session(): + session_id = secrets.token_urlsafe(32) + return SERIALIZER.dumps({'sub': 'admin', 'sid': session_id, 'v': 2}) + + +def sign_user_session(user_id): + return SERIALIZER.dumps({'sub':'user','user_id':user_id,'sid':secrets.token_urlsafe(32),'v':3}) + +def read_session(cookie): + if not cookie or SERIALIZER is None: + return None + try: + value = SERIALIZER.loads(cookie, max_age=MAX_AGE) + if (value.get('sub'),value.get('v')) not in (('admin',2),('user',3)): + return None + return value + except (BadSignature, SignatureExpired, ValueError, TypeError): + return None + + +def csrf(session): + return hmac.new(CSRF_KEY, session['sid'].encode(), hashlib.sha256).hexdigest() + + +def verify_csrf(session, supplied): + return bool(supplied) and hmac.compare_digest(csrf(session), supplied) + + +def check_login_rate(remote): + now = time.monotonic() + # Drop expired buckets so spoofed source addresses cannot grow memory + # without bound on a long-running process. + for key in list(FAILURES): + recent=[t for t in FAILURES[key] if now-t < 900] + if recent:FAILURES[key]=recent + else:FAILURES.pop(key,None) + FAILURES[remote] = [t for t in FAILURES[remote] if now-t < 900] + if len(FAILURES[remote]) >= 8: + raise HTTPException(429, '登录尝试次数过多,请稍后重试') + + +def note_login_failure(remote): + FAILURES[remote].append(time.monotonic()) + + +def note_login_success(remote): + FAILURES.pop(remote, None) diff --git a/app/static/admin.css b/app/static/admin.css new file mode 100644 index 0000000..bac1c08 --- /dev/null +++ b/app/static/admin.css @@ -0,0 +1 @@ +:root{--navy:#121b2a;--navylight:#1c2b40;--blue:#2563eb;--ink:#18263a;--grey:#798699;--border:#e5e9f0;--bg:#f7f9fc;--shadow:0 10px 28px #1e355b0b}*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--ink);font:14px/1.55 Inter,"Segoe UI",Arial,"Microsoft Yahei",sans-serif}button,input,textarea,select{font:inherit}button{cursor:pointer}a{color:inherit;text-decoration:none}.workspace{min-height:100vh;display:grid;grid-template-columns:250px minmax(0,1fr)}.sidebar{background:#152033;color:#fff;padding:27px 15px;display:flex;flex-direction:column;min-height:100vh;position:sticky;top:0;height:100vh}.identity{display:flex;gap:11px;align-items:center;padding:0 14px 34px;border-bottom:1px solid #ffffff19}.logo{width:42px;height:42px;border-radius:12px;background:linear-gradient(140deg,#5b96f9,#3551bd);display:grid;place-items:center;font-size:25px;font-weight:850;position:relative}.logo span{position:absolute;right:-4px;top:-9px;font-size:12px;color:#75bfff}.identity strong{font-size:20px;letter-spacing:-.4px}.identity small,.sidebar-bottom small{display:block;color:#8291ad;font-size:9px;letter-spacing:1.4px;margin-top:2px}.side-caption{display:flex;justify-content:space-between;color:#8e9db6;font-size:10px;font-weight:800;letter-spacing:1.3px;text-transform:uppercase;margin:29px 13px 12px}.side-caption button{color:#9dbbe8;border:0;background:none;font-size:24px;line-height:12px}.side-link{width:100%;border:0;background:transparent;color:#bdc8da;display:flex;align-items:center;gap:14px;padding:13px 16px;border-radius:9px;text-align:left;font-weight:650}.side-link>span{font-size:20px;width:23px;text-align:center;font-weight:400}.side-link:hover,.side-link.selected{background:#2b3b54;color:white}.site-nav{display:flex;flex-direction:column;gap:3px;overflow:auto;padding-bottom:20px}.site-nav button{border:0;background:transparent;color:#aabbd2;padding:11px 14px;display:flex;gap:11px;align-items:center;text-align:left;width:100%;border-radius:8px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.site-nav button.active,.site-nav button:hover{background:#243550;color:white}.site-dot{display:grid;place-items:center;flex-shrink:0;background:#385d82;width:25px;height:25px;border-radius:7px;font-size:10px;font-weight:800}.sidebar-bottom{display:flex;align-items:center;gap:10px;margin-top:auto;padding:23px 12px 4px;border-top:1px solid #ffffff1a}.sidebar-bottom strong{font-size:11px}.status-dot{width:10px;height:10px;background:#43c78d;border-radius:50%;box-shadow:0 0 0 4px #43c78d18}.shell{min-width:0}.topbar{height:80px;border-bottom:1px solid var(--border);background:white;display:flex;justify-content:space-between;align-items:center;padding:0 40px;gap:25px}.breadcrumb{display:flex;gap:13px;font-size:12px;color:#9aa3b2}.breadcrumb strong{color:#2b3850;font-size:13px}.crumb-sep{color:#d1d8e0}.top-actions{display:flex;gap:9px;align-items:center}.env-label{font-size:10px;letter-spacing:1px;color:#2eaf7c;margin-right:15px;font-weight:750}.main{padding:40px;max-width:1650px;margin:auto}.eyebrow{color:#5181db;font-size:10px;letter-spacing:2px;font-weight:800}.page-head{display:flex;align-items:end;justify-content:space-between;gap:20px;margin:0 0 30px}.page-head h1{font-size:30px;letter-spacing:-1px;line-height:1.25;margin:8px 0}.page-head p{color:var(--grey);margin:0;font-size:13px}.btn{border-radius:8px;border:1px solid transparent;padding:10px 17px;font-weight:700;display:inline-flex;gap:9px;align-items:center;justify-content:center;white-space:nowrap;transition:filter .2s,transform .2s}.btn:hover{filter:brightness(.97);transform:translateY(-1px)}.btn.primary{background:#3266dc;color:white;box-shadow:0 4px 13px #356ce020}.btn.light{color:#365078;background:white;border-color:#dce3ef}.btn.danger{background:#fff2f0;color:#cf4040;border-color:#ffd5d0}.btn.small{padding:9px 13px;font-size:12px}.btn.tiny{padding:5px 9px;font-size:11px}.cards{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:22px}.site-card{background:#fff;border:1px solid var(--border);border-radius:14px;box-shadow:var(--shadow);overflow:hidden;cursor:pointer;transition:transform .2s}.site-card:hover{transform:translateY(-4px)}.card-preview{height:156px;display:flex;align-items:center;padding:23px;position:relative;overflow:hidden}.preview-auto{background:linear-gradient(115deg,#0a1723,#2b455c);color:white}.preview-ind{background:linear-gradient(115deg,#073951,#1e839e);color:white}.preview-corp{background:linear-gradient(115deg,#ecf1ff,#bfc9ec);color:#2c3982}.preview-min{background:linear-gradient(115deg,#dad5c9,#f6f2eb);color:#202020}.card-preview .brandmark{position:relative;z-index:3;font-size:24px;font-weight:850;max-width:70%;line-height:1.12}.preview-min .brandmark{font-family:Georgia,serif;font-size:30px;font-weight:400}.card-preview:after{content:'';position:absolute;border:23px solid #ffffff18;border-radius:50%;height:200px;width:200px;right:-30px;bottom:-145px;transform:rotate(-18deg)}.preview-ind:after{border-radius:12px;transform:rotate(35deg);right:0}.preview-corp:after{background:#f8f8ff88;border:0;right:20px;top:-70px}.card-tag{position:absolute;top:14px;right:16px;background:#fff4;padding:4px 9px;border-radius:15px;font-size:10px;z-index:2}.site-card-body{padding:20px 22px}.site-card-body h3{margin:0;font-size:17px}.site-card-body p{color:#8794a4;font-size:12px;white-space:nowrap;text-overflow:ellipsis;overflow:hidden;margin:7px 0 20px}.site-card-meta{display:flex;justify-content:space-between;align-items:center;border-top:1px solid var(--border);padding-top:16px;font-size:11px;color:#748296}.add-card{min-height:300px;border:2px dashed #cfdbed;display:flex;align-items:center;justify-content:center;flex-direction:column;border-radius:14px;background:#f7faff;cursor:pointer}.add-circle{font-size:35px;color:#5890e1;background:white;border-radius:12px;width:61px;height:61px;display:grid;place-items:center;margin-bottom:12px}.add-card h3{margin:7px}.add-card p{color:var(--grey);font-size:12px}.metrics-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:15px;margin:0 0 24px}.metric{padding:21px;background:white;border:1px solid var(--border);border-radius:12px}.metric small{display:block;color:#8d9bad;font-size:11px}.metric strong{display:block;font-size:26px;margin:5px 0}.metric span{color:#89a1ba;font-size:11px}.tabs{display:flex;gap:5px;border-bottom:1px solid #dee5f0;margin:30px 0 26px;overflow:auto}.tab{white-space:nowrap;border:0;background:none;color:#6c7e94;padding:13px 18px;font-size:13px;font-weight:700;border-bottom:3px solid transparent}.tab.current{color:#285ed5;border-bottom-color:#285ed5}.panel{background:white;border:1px solid var(--border);border-radius:13px;padding:26px;box-shadow:var(--shadow);margin-bottom:20px}.panel-title{display:flex;align-items:center;justify-content:space-between;gap:20px;margin-bottom:21px}.panel-title h3{font-size:17px;margin:0}.panel-title p{font-size:12px;color:#8e9caf;margin:4px 0}.panel-title .muted{font-size:12px;color:var(--grey)}.setting-grid,.form-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:19px}.setting-grid label,.form-grid label,.field{display:flex;flex-direction:column;gap:7px;color:#394960;font-size:12px;font-weight:680}.full{grid-column:1 / -1}input,textarea,select{background:white;border:1px solid #dce3ed;border-radius:8px;padding:11px 12px;min-width:0;color:#29374d;outline:none;width:100%}input:focus,textarea:focus,select:focus{border-color:#639af2;box-shadow:0 0 0 3px #639af215}textarea{resize:vertical}label small{font-weight:400;color:#95a0af}.panel-footer{margin-top:24px;display:flex;gap:10px;align-items:center;justify-content:flex-end}.data-table{width:100%;border-collapse:collapse;font-size:12px}.data-table th{text-align:left;padding:14px 12px;color:#8f9bad;font-size:10px;letter-spacing:.8px;background:#f8faff}.data-table td{padding:15px 12px;border-top:1px solid #edf0f5}.data-table tr:hover td{background:#fcfdff}.mono{font:11px/1.5 ui-monospace,SFMono-Regular,Consolas,monospace;color:#5c7189}.pill{display:inline-block;background:#edf4ff;color:#4b72aa;font-size:10px;border-radius:20px;padding:4px 9px}.pill.green{background:#e7f8ef;color:#218450}.pill.yellow{background:#fff6dc;color:#a27a24}.row-actions{display:flex;gap:7px}.table-wrapper{overflow:auto}.toolbar{display:flex;gap:12px;align-items:center;margin-bottom:17px;flex-wrap:wrap}.toolbar input{max-width:300px}.toolbar select{width:auto;min-width:120px}.template-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:18px}.template-card{border:1px solid #dce5ed;background:#fff;border-radius:12px;overflow:hidden}.template-art{height:180px;padding:28px;display:flex;flex-direction:column;justify-content:center}.template-art span{font-size:11px;opacity:.75;letter-spacing:2px}.template-art strong{font-size:35px;line-height:1.1;max-width:250px}.template-art.min strong{font-family:Georgia,serif;font-weight:400;font-size:41px}.template-card .detail{padding:17px 20px;display:flex;align-items:center;justify-content:space-between;gap:12px}.template-card .detail h3{margin:0;font-size:15px}.template-card .detail p{margin:4px 0 0;color:#8a9aad;font-size:11px}.blueprint-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:16px}.feature-option{border:1px solid var(--border);border-radius:12px;padding:21px;min-height:130px}.feature-option b{display:block;font-size:14px;margin:12px 0 5px}.feature-option p{margin:0;font-size:12px;color:var(--grey)}.feature-icon{font-size:25px;color:#5b86cd}.split{display:grid;grid-template-columns:1fr 1fr;gap:18px}.tip{padding:13px 15px;border-radius:8px;background:#f1f5fd;color:#64799c;font-size:12px}.tip.warning{background:#fff8e8;color:#927034}.pre{white-space:pre-wrap;overflow-wrap:anywhere;font:12px/1.65 Consolas,monospace;color:#607188;background:#f8faff;border:1px solid #edf0f5;border-radius:9px;padding:14px}.report-good{color:#1b9e6c}.report-warn{color:#ac7d28}.report-bad{color:#c54545}.empty{text-align:center;padding:54px 20px;color:#8b99a9}.empty div{font-size:36px;color:#adc4e1;margin-bottom:10px}.empty h3{color:#354760;margin:5px}.empty p{font-size:12px}.dialog{border:1px solid #dce5ef;border-radius:16px;padding:0;width:min(620px,95vw);max-height:90vh;box-shadow:0 24px 80px #13264338}.dialog.wide{width:min(860px,95vw)}.dialog::backdrop{background:#15223780;backdrop-filter:blur(3px)}.dialog form{padding:27px 32px}.dialog-head{display:flex;justify-content:space-between;margin-bottom:24px}.dialog-head h2{margin:7px 0 5px;font-size:23px}.dialog-head p{color:var(--grey);font-size:12px;margin:0}.icon-btn{border:0;background:#f1f4fa;width:31px;height:31px;border-radius:8px;font-size:21px;color:#789}.dialog-actions{display:flex;justify-content:flex-end;gap:10px;padding-top:22px;margin-top:24px;border-top:1px solid #e5eaf0}.inline-input{display:flex;gap:7px}.inline-input input{flex:1}.editor-tools{display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:14px;background:#f8faff;border-radius:9px}.editor-tools strong{font-size:12px}.editor-tools small{width:100%;color:#9aa7b5}.toast{position:fixed;bottom:25px;right:25px;background:#233651;color:white;border-radius:10px;padding:15px 20px;box-shadow:0 10px 40px #1a233e35;opacity:0;transform:translateY(15px);pointer-events:none;transition:all .25s;z-index:99;max-width:450px}.toast.show{opacity:1;transform:translateY(0)}.loading{padding:40px;text-align:center;color:#8fa0b6}.steps{display:grid;grid-template-columns:repeat(4,1fr);gap:12px}.step{padding:18px;background:#f6f9ff;border:1px solid #e0eaf7;border-radius:10px}.step span{display:block;color:#4274c4;font-size:11px;letter-spacing:1px}.step strong{display:block;margin-top:9px}.step p{font-size:11px;color:#8091a7;margin:6px 0 0}@media(max-width:1200px){.cards{grid-template-columns:repeat(2,1fr)}.metrics-grid{grid-template-columns:repeat(2,1fr)}.topbar{padding:0 24px}.main{padding:24px}.sidebar{width:230px}.workspace{grid-template-columns:230px minmax(0,1fr)}}@media(max-width:850px){.workspace{grid-template-columns:1fr}.sidebar{height:auto;min-height:auto;position:static;display:block;padding:12px 14px}.identity{padding:5px 0 12px}.sidebar .side-caption,.sidebar .side-link,.sidebar .site-nav,.sidebar-bottom{display:none}.topbar{height:66px}.env-label{display:none}.top-actions .light{display:none}.main{padding:20px}.setting-grid,.split,.template-grid,.blueprint-grid,.steps{grid-template-columns:1fr}}@media(max-width:560px){.cards,.metrics-grid,.form-grid{grid-template-columns:1fr}.full{grid-column:1}.page-head{flex-direction:column;align-items:start}.site-card{min-height:0}.top-actions .btn{font-size:10px}.dialog form{padding:18px}.card-preview{height:130px}} diff --git a/app/static/admin.js b/app/static/admin.js new file mode 100644 index 0000000..af4993c --- /dev/null +++ b/app/static/admin.js @@ -0,0 +1,96 @@ +'use strict'; +const $=(q,el=document)=>el.querySelector(q); +const E=s=>String(s??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); +const ATTR=s=>E(s); +const cfg={automotive:{label:'汽车与出行',desc:'高级质感 · 深色车型展厅',prev:'auto',title:'Move beyond.'},industrial:{label:'工业制造',desc:'专业理性 · 工业产品展示',prev:'ind',title:'Built to last.'},corporate:{label:'企业品牌',desc:'现代明亮 · 集团企业官网',prev:'corp',title:'Ideas become impact.'},minimal:{label:'极简设计',desc:'编辑式排版 · 极简作品展示',prev:'min',title:'Less, but better.'}}; +let csrfToken=''; +const state={sites:[],siteId:null,site:null,entries:[],builds:[],tab:'overview',presets:[],query:'',typ:''}; +const api=async(path,opt={})=>{ + if(!['GET','HEAD','OPTIONS'].includes((opt.method||'GET').toUpperCase())) {opt.headers={...(opt.headers||{}),'X-SiteForge-CSRF':csrfToken}} + const res=await fetch(path,opt);if(res.status===401){location.assign('/login');throw new Error('登录已过期')}let data;try{data=await res.json()}catch{data={detail:'请求失败,响应不是 JSON'}} + if(!res.ok)throw new Error(typeof data.detail==='string'?data.detail:JSON.stringify(data.detail||data));return data; +}; +const json=(method,data)=>({method,headers:{'Content-Type':'application/json'},body:JSON.stringify(data)}); +const notice=(s,bad=false)=>{const t=$('#toast');t.innerText=s;t.style.background=bad?'#a03e45':'#243952';t.classList.add('show');clearTimeout(t._timer);t._timer=setTimeout(()=>t.classList.remove('show'),4300)}; +const onError=e=>{console.error(e);notice(e.message||String(e),true)}; +const simpleDate=s=>s?new Date(s).toLocaleString('zh-CN',{year:'numeric',month:'2-digit',day:'2-digit',hour:'2-digit',minute:'2-digit'}):'—'; +const b=(text,fn,extra='')=>``; +const tabs=[['overview','工作概览'],['entries','内容管理'],['templates','模板与设计'],['design','可视化页面设计'],['models','内容字段模型'],['import','旧站 / 数据导入'],['seo','SEO / URL'],['builds','构建与导出']]; +function topTitle(name){$('#topTitle').textContent=name} +function hero(title,desc,buttons='',eyebrow='SITEFORGE / WEBSITE FACTORY'){return `
${E(eyebrow)}

${E(title)}

${E(desc)}

${buttons}
`} +function themeCard(key,action){let t=cfg[key];return `
THEME / ${E(key.toUpperCase())}${E(t.title)}

${E(t.label)}

${E(t.desc)}

${action}
`} +const App={ + async init(){try{const me=await api('/api/auth/me');csrfToken=me.csrf;state.sites=await api('/api/sites');state.presets=await api('/api/presets');App.renderSiteNav();App.openDashboard()}catch(e){onError(e)}}, + async logout(){try{await api('/api/auth/logout',{method:'POST'});location.assign('/login')}catch(e){onError(e)}}, + async reload(){state.sites=await api('/api/sites');App.renderSiteNav();if(state.siteId){state.site=await api('/api/sites/'+state.siteId);state.entries=await api('/api/sites/'+state.siteId+'/entries');state.builds=await api('/api/sites/'+state.siteId+'/builds')}}, + renderSiteNav(){$('#siteNav').innerHTML=state.sites.map(s=>``).join('')||'
暂无网站项目
'}, + openDashboard(){state.siteId=null;topTitle('网站总览');App.renderSiteNav();document.querySelectorAll('.side-link').forEach(x=>x.classList.remove('selected'));$('#nav-dashboard').classList.add('selected'); + let n=state.sites.length, counts=state.sites.reduce((x,s)=>x+s.entries_count,0),builds=state.sites.reduce((x,s)=>x+s.build_count,0); + $('#view').innerHTML=hero('网站生产工作台','从零创建、迁移旧站、导入内容、更换模板,统一生产独立静态网站。',``,'WELCOME TO YOUR FACTORY')+ + `
网站总数${n}独立站点项目
内容条目${counts}页面、产品、文章等
构建版本${builds}可预览与下载
可用主题4支持每站独立风格
`+ + `

我的网站

选择任意站点,继续编辑、导入数据或构建版本

`+ + state.sites.map(s=>`
${E(cfg[s.theme]?.label||'企业站')}
${E(s.name)}

${E(s.name)}

${E(s.domain||'待绑定域名')} · ${E(s.tagline||'尚未填写站点简介')}

${s.entries_count} 条内容${s.build_count} 次构建 →
`).join('')+`
+

创建新网站

旧站迁移 / 新建站点 / 导入内容

`+ + (!state.sites.length?`

第一次体验 SiteForge?

生成演示网站,并立即构建一个可独立下载的纯静态网站。

`:''); + }, + async openSite(id,tab='overview'){try{state.siteId=id;state.tab=tab;await App.reload();topTitle(state.site.name);App.renderSiteNav();App.renderSite()}catch(e){onError(e)}}, + renderSite(){const s=state.site;let head=hero(s.name,s.domain||'独立网站 · 尚未设置域名',``,'WEBSITE PROJECT / '+s.theme.toUpperCase()); + let navigation=`
${tabs.map(([id,name])=>``).join('')}
`; + $('#view').innerHTML=head+`
页面 / 内容${state.entries.length}统一内容数据中心
主题${E(cfg[s.theme]?.label)}可随时切换
构建次数${state.builds.length}独立构建版本
SEO 模式${s.mode==='migration'?'原址保留':'灵活新建'}路由独立管理
`+navigation+`
`;App.renderTab(); + }, + changeTab(tab){state.tab=tab;App.renderSite()}, + renderTab(){let html='';switch(state.tab){case'overview':html=App.overview();break;case'entries':html=App.entriesView();break;case'templates':html=App.templateView();break;case'design':html=App.designView();break;case'models':html=App.modelsView();break;case'import':html=App.importView();break;case'seo':html=App.seoView();break;case'builds':html=App.buildView();break;}$('#tab-content').innerHTML=html;if(state.tab==='import')App.loadMedia();if(state.tab==='seo')App.loadRedirects();if(state.tab==='models')App.loadModels()}, + overview(){let s=state.site, latest=state.builds[0];return `

网站生产流程

每次修改内容、主题或域名后都可以重新构建

STEP 01创建网站

选择主题,完成独立站点初始化

STEP 02添加或导入内容

手动录入、旧站采集、文件批量导入

STEP 03主题与 SEO

自定义视觉样式、路由及页面元信息

STEP 04构建并导出

生成静态 HTML 文件,预览并下载 ZIP

网站基本资料

设置域名、品牌和原网站来源

${App.settingsForm()}

最近构建版本

构建文件与管理后台彼此独立

${latest?`
静态构建完成

${latest.total_pages} 个 HTML 页面

${simpleDate(latest.created_at)}

`:`
⌁

还没有构建版本

保存内容并点击「构建网站」,即可生成首个网站。

`}
`}, + settingsForm(){let s=state.site;return `
`}, + async saveSettings(ev){ev.preventDefault();try{let data=Object.fromEntries(new FormData(ev.target));state.site=await api('/api/sites/'+state.siteId,json('PATCH',data));await App.reload();notice('已保存站点设置');App.renderSite()}catch(e){onError(e)}}, + entriesView(){let arr=state.entries.filter(e=>(!state.typ||e.type===state.typ)&&(!state.query||e.title.toLowerCase().includes(state.query.toLowerCase())||e.route.toLowerCase().includes(state.query.toLowerCase())));return `

内容库

页面 / 产品 / 文章 / 自定义内容,支持独立 URL 和 SEO

${arr.length} 条结果
${arr.map(e=>``).join('')}
标题类型路由状态更新时间操作
${E(e.title)}
${E(e.summary||'')}
${E(e.type)}${E(e.route)}${e.status==='published'?'已发布':'草稿'}${simpleDate(e.updated_at)}
页面设计
${arr.length?'':'

尚无匹配内容

新建内容或使用导入中心批量导入

'}
`}, + filterContent(){const active=document.activeElement?.id;const caret=active==='searchEntry'?$('#searchEntry').selectionStart:null;state.query=$('#searchEntry')?.value||'';state.typ=$('#typeFilter')?.value||'';$('#tab-content').innerHTML=App.entriesView();if(active==='searchEntry'){const input=$('#searchEntry');input.focus();if(caret!==null)input.setSelectionRange(caret,caret)}}, + async updateNavigation(ev){ev.preventDefault();try{const raw=$('#navRoutes').value.split('\n').map(x=>x.trim()).filter(Boolean);const entries=raw.map(line=>{const sep=line.indexOf('|');if(sep<0)throw new Error('每行必须为:导航文字 | /路径/');return {title:line.slice(0,sep).trim(),route:line.slice(sep+1).trim()}});let resp=await api('/api/sites/'+state.siteId+'/navigation',json('PUT',entries));state.site=await api('/api/sites/'+state.siteId);notice('导航已保存:'+resp.nav.length+'项')}catch(e){onError(e)}}, + async scaffold(){if(!confirm('创建关于我们、联系我们和服务页面?已有同路径页面不会覆盖。'))return;try{let r=await api('/api/sites/'+state.siteId+'/scaffold',{method:'POST'});await App.reload();App.renderSite();notice('创建 '+r.created.length+' 个页面,跳过 '+r.skipped.length+' 个')}catch(e){onError(e)}}, + modelsView(){return `

内容字段模型

自定义产品、车型、品牌、文章等数据字段。字段保存在每个站点自己的配置内,不影响其他站点。

正在读取字段定义……

`}, + async loadModels(){try{state.models=await api('/api/sites/'+state.siteId+'/schemas');App.renderModelFields()}catch(e){onError(e)}}, + renderModelFields(){let el=$('#modelFields');if(!el||!state.models)return;let kind=$('#modelKind').value;let defs=state.models[kind]||[];el.innerHTML=`${defs.map(d=>App.modelRow(d)).join('')}
字段键名显示名称字段类型
`}, + modelRow(d={}){return ``}, + addModelField(){if(!$('#fieldRows'))return;$('#fieldRows').insertAdjacentHTML('beforeend',App.modelRow())}, + async saveModelFields(){try{const fields=[...document.querySelectorAll('.field-row')].map(tr=>({name:tr.querySelector('.field-key').value.trim(),label:tr.querySelector('.field-label').value.trim(),type:tr.querySelector('.field-type').value}));let kind=$('#modelKind').value;let result=await api('/api/sites/'+state.siteId+'/schemas/'+kind,json('PUT',fields));state.models[kind]=result.fields;notice('已保存 '+kind+' 字段模型')}catch(e){onError(e)}}, + designView(){const s=state.site;return `

Visual Studio · 页面可视化工作台

拖动区块顺序、修改属性、自动绑定内容,并在同一个界面预览 PC 和移动端。

${state.entries.filter(e=>e.type==='page'||e.type==='product'||e.type==='post').slice(0,36).map(e=>`
${E(e.type)}

${E(e.title)}

${E(e.route)}

打开可视化编辑器 ↗
`).join('')}

页面编辑后点击构建,系统生成对应的独立静态页面,不会丢失原有 URL 和 SEO。

顶部导航配置

每行:标题 | /原始路径/,不填写则按网站已有栏目自动生成导航。

`}, + async applyTemplates(mode){if(!confirm(mode==='overwrite'?'将用当前主题的默认布局覆盖所有页面区块,内容和SEO不变,确定继续?':'将给尚未设置布局的页面自动套用当前主题,确定继续?'))return;try{let r=await api('/api/sites/'+state.siteId+'/apply-theme',json('POST',{mode}));await App.reload();notice('已装配 '+r.updated+' 个页面,跳过 '+r.skipped+' 个');App.renderSite()}catch(e){onError(e)}}, + templateView(){let s=state.site;return `

主题样式库

切换后内容、原有 URL 和 SEO 数据不变,重新构建立即产生全新视觉版本

${Object.keys(cfg).map(t=>themeCard(t,``)).join('')}

模板工厂

保存当前主题与首页区块配置,以后创建其他网站时可以直接套用。

v0.3 已提供页面区块拖拽排序、PC / 手机预览、动态内容绑定。完全自由定位的像素级编辑仍不支持。
`}, + importView(){return `

批量导入文件

支持 WordPress XML、CSV、Excel XLSX、JSON

建议 CSV 字段:type, title, route, summary, body, cover, seo_title, seo_description。

旧站公开页面采集

支持 WordPress、普通 HTML 与其他 CMS 的公开内容

公开爬取无法保证包含私有字段、插件数据或复杂动态页面。推荐尽量使用原站完整数据库/导出文件。

素材中心

上传图片,生成本地媒体路径,内容页可直接引用。

上传文件 URL 将返回类似 media/abc123.jpg,静态站构建时自动携带。
`}, + seoView(){return `

SEO 保留策略

不因改版随意改变页面 URL

↗
页面 URL Registry

所有现有 URL 由内容库的 route 精确定义;冲突直接禁止保存。

◇
SEO Metadata

编辑内容可以设置 Title、Description、Canonical 和 Robots。

▤
Sitemap + 服务器规则

构建时生成 sitemap.xml、robots.txt、Nginx snippet 和 .htaccess。

301 / 302 重定向管理

原 URL 不能保留时,人工审批后配置对应跳转。

`}, + buildView(){return `

静态构建中心

将当前内容、模板和 SEO 路由组合成完全独立的 HTML 文件。

▦
静态 HTML / CSS / JS

无需 PHP、WordPress 或中心 CMS,就能独立部署访问。

▰
可回溯构建版本

每次构建产生独立 ZIP 和版本报告,不会覆盖旧版本。

历史构建

选择版本查看预览、下载 ZIP、检查 SEO/媒体报告

${state.builds.map(x=>{let r=JSON.parse(x.report_json||'{}');return ``}).join('')}
版本 ID构建时间页面数量报告操作
${x.id}${simpleDate(x.created_at)}${x.total_pages}${r.quality_status==='warning'?'有告警':'已通过基础检查'}
预览 ↗ZIP ↓
${state.builds.length?'':'
⌘

尚无构建记录

完成内容编辑后,点击上方按钮生成版本。

'}
`}, + async openTemplates(){state.siteId=null;topTitle('模板资源库');App.renderSiteNav();document.querySelectorAll('.side-link').forEach(x=>x.classList.remove('selected'));$('#nav-template').classList.add('selected');$('#view').innerHTML=hero('模板与主题资源库','从四套行业视觉风格开始,逐步沉淀企业自己的模板库。',``)+`
${Object.keys(cfg).map(t=>themeCard(t,``)).join('')}

已保存的模板预设

由已有网站保存,复用主题及首页区块结构。

${state.presets.length?state.presets.map(p=>`
${E(p.name)}

${E(cfg[p.theme]?.label||p.theme)} · ${simpleDate(p.created_at)}

`).join(''):'

暂未保存企业自定义模板

从任意网站的「模板与设计」中保存为模板。

'}
`}, + openNewSite(theme='',preset=''){const f=$('#siteForm');f.reset();if(theme)f.elements.theme.value=theme;$('#presetSelector').innerHTML=''+state.presets.map(p=>``).join('');if(preset)f.elements.preset_id.value=preset;$('#siteDialog').showModal()}, + closeDialog(id){$('#'+id).close()}, + async createSite(ev){ev.preventDefault();try{let values=Object.fromEntries(new FormData(ev.target));let site=await api('/api/sites',json('POST',values));App.closeDialog('siteDialog');notice('网站已创建,可以开始添加内容');await App.openSite(site.id)}catch(e){onError(e)}}, + openEntry(id=''){let f=$('#entryForm');f.reset();let e=state.entries.find(x=>x.id===id);f.elements.entry_id.value=id;$('#entryHeading').textContent=id?'编辑内容':'新建内容';if(e){for(let key of ['title','route','summary','body','cover','seo_title','seo_description','canonical','type','status','robots'])if(f.elements[key])f.elements[key].value=e[key]??'';f.elements.meta.value=JSON.stringify(e.meta||{},null,2)}else{f.elements.meta.value='{}';f.elements.type.value='page';f.elements.status.value='published'}$('#entryDialog').showModal();App.loadEntryFields()}, + async loadEntryFields(){try{state.models=await api('/api/sites/'+state.siteId+'/schemas');App.renderEntryFields()}catch(e){onError(e)}}, + renderEntryFields(){let el=$('#dynamicContentFields');if(!el)return;let kind=$('#entryType').value;let list=(state.models||{})[kind]||[];let parsed={};try{parsed=JSON.parse($('#entryMeta').value||'{}')}catch{};const values=parsed.fields||parsed;el.innerHTML=list.length?`

业务字段 · ${E(kind)}

来自当前网站的字段模型,保存时同步写入内容数据。

${list.map(f=>``).join('')}
`:''}, + addBlock(type){const el=$('#entryMeta');let parsed={};try{parsed=JSON.parse(el.value||'{}')}catch{return notice('当前 JSON 格式不合法,先修复后再添加区块',true)}if(!Array.isArray(parsed.blocks))parsed.blocks=[];let obj=({hero:{type:'hero',title:'核心主标题',subtitle:'OUR VISION',description:'可编辑的首页主视觉描述'},features:{type:'features',title:'核心优势',items:[{title:'专业实力',text:'填写优势简介'},{title:'可靠交付',text:'填写优势简介'},{title:'服务网络',text:'填写优势简介'}]},stats:{type:'stats',items:[{value:'30+',label:'合作品牌'},{value:'70+',label:'服务市场'}]},cta:{type:'cta',title:'期待与您合作',subtitle:'联系我们,了解更多服务',button:'联系我们',href:'/contact/'}})[type];parsed.blocks.push(obj);el.value=JSON.stringify(parsed,null,2);notice('已添加区块,请编辑 JSON 中的标题与内容')}, + async saveEntry(ev){ev.preventDefault();try{let v=Object.fromEntries(new FormData(ev.target)),id=v.entry_id;delete v.entry_id;v.meta=JSON.parse(v.meta||'{}');const fieldInputs=[...document.querySelectorAll('#dynamicContentFields .sf-data-field')];if(fieldInputs.length){let fields={...(v.meta.fields||{})};for(const input of fieldInputs)fields[input.dataset.key]=input.value;v.meta.fields=fields}if(!id&&!v.route)delete v.route;let endpoint='/api/sites/'+state.siteId+'/entries'+(id?'/'+id:'');await api(endpoint,json(id?'PATCH':'POST',v));App.closeDialog('entryDialog');await App.openSite(state.siteId,'entries');notice(id?'已更新内容':'已创建内容')}catch(e){onError(e)}}, + async deleteEntry(id){if(!confirm('确定删除此内容吗?删除之后重新构建才会影响线上静态网站。'))return;try{await api('/api/sites/'+state.siteId+'/entries/'+id,{method:'DELETE'});await App.openSite(state.siteId,'entries');notice('内容已删除')}catch(e){onError(e)}}, + async setTheme(theme){if(!confirm('切换到「'+cfg[theme].label+'」?内容和 URL 保持不变,需重新构建才能更新站点。'))return;try{await api('/api/sites/'+state.siteId,json('PATCH',{theme}));await App.openSite(state.siteId,'templates');notice('主题已更换,请重新构建预览')}catch(e){onError(e)}}, + async savePreset(){let name=$('#presetName')?.value?.trim();if(!name)return notice('请输入新模板名称',true);try{let result=await api('/api/sites/'+state.siteId+'/save-preset',json('POST',{name}));state.presets=await api('/api/presets');notice('已保存模板:'+result.name)}catch(e){onError(e)}}, + async cloneSite(){let name=prompt('新站名称:',state.site.name+' 副本');if(!name)return;try{let newsite=await api('/api/sites/'+state.siteId+'/clone',json('POST',{name}));notice('已克隆全部内容到新网站');await App.openSite(newsite.id)}catch(e){onError(e)}}, + async importFile(ev){ev.preventDefault();let btn=$('button[type=submit]',ev.target);btn.disabled=true;btn.innerText='导入中...';try{let form=new FormData(ev.target);let result=await api('/api/sites/'+state.siteId+'/import',{method:'POST',body:form});await App.openSite(state.siteId,'import');notice(`导入完成:新增 ${result.imported},更新 ${result.updated},跳过 ${result.skipped},错误 ${result.errors?.length||0}`);if(result.errors?.length)alert(JSON.stringify(result.errors.slice(0,15),null,2))}catch(e){onError(e)}finally{btn.disabled=false;btn.innerText='开始批量导入'}}, + async crawl(ev){ev.preventDefault();if(!confirm('确认对该公开网站进行限定范围采集?请仅采集你拥有或获授权迁移的站点。'))return;let btn=$('button[type=submit]',ev.target);btn.disabled=true;btn.innerText='正在采集...';try{const input=Object.fromEntries(new FormData(ev.target));input.max_pages=+input.max_pages;const r=await api('/api/sites/'+state.siteId+'/crawl',json('POST',input));await App.openSite(state.siteId,'import');notice(`采集完成:发现 ${r.discovered} 页,新增 ${r.imported} 页,页面获取失败 ${r.fetch_errors?.length||0}`)}catch(e){onError(e)}finally{btn.disabled=false;btn.innerText='扫描并导入公开页面'}}, + async uploadMediaArchive(file){if(!file)return;try{let form=new FormData();form.append('file',file);notice('正在解压媒体文件...');let r=await api('/api/sites/'+state.siteId+'/media-archive',{method:'POST',body:form});notice(`媒体导入:${r.imported} 文件,跳过 ${r.skipped} 文件`)}catch(e){onError(e)}}, + async uploadMedia(file){if(!file)return;try{let fd=new FormData();fd.append('file',file);let r=await api('/api/sites/'+state.siteId+'/media',{method:'POST',body:fd});notice('图片已上传:'+r.url);App.loadMedia()}catch(e){onError(e)}}, + async uploadCover(file){if(!file)return;try{let fd=new FormData();fd.append('file',file);let r=await api('/api/sites/'+state.siteId+'/media',{method:'POST',body:fd});$('#coverInput').value=r.url;notice('封面上传成功')}catch(e){onError(e)}}, + async loadMedia(){if(state.tab!=='import')return;try{let data=await api('/api/sites/'+state.siteId+'/media');$('#mediaGrid').innerHTML=data.map(x=>`
`).join('')||'

暂无本地素材

'}catch(e){onError(e)}}, + async createRedirect(ev){ev.preventDefault();let value=Object.fromEntries(new FormData(ev.target));value.status_code=+value.status_code;try{await api('/api/sites/'+state.siteId+'/redirects',json('POST',value));ev.target.reset();notice('已创建重定向规则');App.loadRedirects()}catch(e){onError(e)}}, + async loadRedirects(){if(state.tab!=='seo')return;try{let data=await api('/api/sites/'+state.siteId+'/redirects');$('#redirectList').innerHTML=data.length?'

已有重定向

'+data.map(x=>`

${E(x.old_path)} ${x.status_code} → ${E(x.new_path)}

`).join(''):'

当前没有重定向规则。

'}catch(e){onError(e)}}, + async deleteRedirect(id){if(!confirm('确认删除这条规则?'))return;try{await api('/api/sites/'+state.siteId+'/redirects/'+id,{method:'DELETE'});App.loadRedirects()}catch(e){onError(e)}}, + async publishLocal(id){if(!confirm('将此版本发布到 Docker 持久化目录?请确认配置好独立站点服务器。'))return;try{const x=await api('/api/sites/'+state.siteId+'/builds/'+id+'/publish-local',{method:'POST'});notice('已发布:'+x.release+'(发布文件位于 Docker 数据卷)')}catch(e){onError(e)}}, + async build(){let s=state.siteId;if(!s)return;let mirror=$('#mirrorChoice')?.checked||false;notice('开始构建,请检查完成后的质量报告...');try{let report=await api('/api/sites/'+s+'/builds',json('POST',{mirror_images:mirror}));await App.openSite(s,'builds');notice(`构建成功:${report.pages} 个页面 · ${report.warnings.length} 项警告`);App.showReport(report.version)}catch(e){onError(e)}}, + async showReport(id){try{const r=await api('/api/sites/'+state.siteId+'/builds/'+id+'/report');let box=$('#reportBox');if(!box){alert(JSON.stringify(r,null,2));return}box.innerHTML=`

质量报告:${E(id)}

本报告只覆盖当前实现的基础结构与资源检查,不代表 SEO 排名保证。

静态 HTML 数${r.pages}
重定向数${r.redirects}
已镜像图片${r.mirrored_images}
质量告警${r.warnings.length}
${r.warnings.length?`
${r.warnings.map(x=>`• ${E(x)}`).join('
')}
`:'
✓ 当前规则下未发现告警
'}

详细报告

${E(JSON.stringify(r,null,2))}
`;box.scrollIntoView({behavior:'smooth',block:'nearest'})}catch(e){onError(e)}}, + async seedDemo(){try{const s=await api('/api/sites',json('POST',{name:'HESU Global Motors · 演示站',tagline:'全球汽车采购、出口与交付服务',theme:'automotive',domain:'https://demo.example.com',mode:'new'}));let base='/api/sites/'+s.id+'/entries';for(let v of [ + {type:'page',title:'关于我们',route:'/about/',summary:'专注全球汽车出口与本地交付',body:'

这里是示例公司介绍。请在内容管理中替换为你的真实资料。

',seo_title:'About us'}, + {type:'product',title:'ROX 01',route:'/product/rox-01/',summary:'豪华全地形 SUV · 示例数据,可在后台编辑',body:'

产品概览

此条目为演示内容,不代表实际库存或车型参数。

'}, + {type:'product',title:'BYD ATTO 3',route:'/product/byd-atto-3/',summary:'纯电 SUV · 示例车型信息',body:'

可以从 WordPress、Excel 或 JSON 重新导入真正的车型数据。

'}, + {type:'post',title:'网站动态示例',route:'/news/first-news/',summary:'一个独立的新网站如何快速发布',body:'

编辑内容、切换主题、点击构建,即可导出静态网站。

'} +])await api(base,json('POST',v));await App.openSite(s.id);await App.build();notice('演示网站已创建并成功构建,可直接预览或下载 ZIP')}catch(e){onError(e)}}, +}; +window.App=App; +$('#siteForm').addEventListener('submit',App.createSite); +$('#entryForm').addEventListener('submit',App.saveEntry); +window.addEventListener('DOMContentLoaded',()=>App.init()); diff --git a/app/static/blocks.css b/app/static/blocks.css new file mode 100644 index 0000000..cdf0cbf --- /dev/null +++ b/app/static/blocks.css @@ -0,0 +1,13 @@ +/* Portable SiteForge blocks, loaded with every theme (all styles scoped to sf-). */ +.sf-section{background:var(--sf-bg,transparent);color:var(--sf-fg,inherit);padding:var(--sf-pad,82px) 0;margin:0}.sf-section:nth-of-type(even){background:var(--sf-bg,#f4f6f9)}.sf-container{max-width:1200px;margin:auto;padding:0 32px}.sf-section h1,.sf-section h2,.sf-section h3,.sf-section p{color:inherit;line-height:1.3}.sf-section h1{font-size:clamp(38px,5vw,76px);font-weight:790;letter-spacing:-.052em}.sf-section h2{font-size:clamp(27px,3.2vw,44px);font-weight:760;letter-spacing:-.037em}.sf-kicker{display:block;font-size:12px;letter-spacing:.17em;text-transform:uppercase;font-weight:740;color:var(--accent,#3569d3);margin-bottom:18px}.sf-hero-grid,.sf-image-text{display:grid;grid-template-columns:1.08fr .92fr;gap:68px;align-items:center}.sf-hero-copy p{font-size:18px;line-height:1.75;max-width:520px;opacity:.75}.sf-hero-img,.sf-image-side{overflow:hidden;border-radius:9px;min-height:380px;background:linear-gradient(130deg,#d5e1ee,#eef3f9);display:grid;place-items:center}.sf-hero-img img,.sf-image-side img{width:100%;height:100%;max-height:580px;object-fit:cover}.sf-hero-abstract{font-size:110px;color:#a7bdd4}.sf-button{display:inline-flex;align-items:center;justify-content:center;gap:28px;background:var(--accent,#315ee2);color:white!important;padding:16px 26px;border-radius:6px;font-size:14px;text-decoration:none;font-weight:720;margin-top:24px}.sf-section-head{display:flex;align-items:end;justify-content:space-between;margin-bottom:32px;gap:24px}.sf-section-head h2{margin:0}.sf-section-head>a,.sf-link{color:var(--accent,#3569d3);font-weight:700;text-decoration:none}.sf-prose{font-size:16px;line-height:1.85;max-width:930px}.sf-prose p{line-height:1.85}.sf-features,.sf-collection,.sf-stats{display:grid;gap:20px}.sf-features{grid-template-columns:repeat(3,minmax(0,1fr))}.sf-feature{border:1px solid #e0e6eb;padding:32px 28px;border-radius:8px;background:#fff;color:#253247}.sf-feature>span{font-size:12px;font-weight:800;color:var(--accent,#346add)}.sf-feature h3{font-size:22px;margin:30px 0 8px}.sf-feature p{font-size:14px;line-height:1.7;color:#647181}.sf-stats{grid-template-columns:repeat(4,1fr);padding:40px 0;border-top:1px solid #b7c0cd66;border-bottom:1px solid #b7c0cd66}.sf-stats>div{display:flex;flex-direction:column;gap:8px}.sf-stats strong{font-size:clamp(28px,4vw,53px);letter-spacing:-.04em}.sf-stats span{font-size:12px;opacity:.65;letter-spacing:.04em}.sf-collection{grid-template-columns:repeat(var(--sf-cols,3),minmax(0,1fr))}.sf-item{display:block;text-decoration:none;color:#233044;background:#fff;border:1px solid #e8edf1;border-radius:8px;overflow:hidden;transition:transform .2s,box-shadow .2s}.sf-item:hover{transform:translateY(-4px);box-shadow:0 18px 38px #344d7018}.sf-item-img{height:210px;overflow:hidden;background:#e4eaf0}.sf-item-img img{height:100%;width:100%;object-fit:cover}.sf-placeholder{display:grid;place-items:center;width:100%;height:100%;min-height:190px;background:linear-gradient(130deg,#e2eaf2,#c9d7e6);font-size:72px;color:#8399b2;font-weight:800}.sf-large{min-height:360px}.sf-item-copy{padding:25px}.sf-item-copy small{text-transform:uppercase;letter-spacing:.13em;color:var(--accent,#376edc)}.sf-item-copy h3{font-size:22px;margin:12px 0 7px}.sf-item-copy p{font-size:14px;line-height:1.6;color:#7a8796;min-height:42px}.sf-item-copy span{font-size:12px;color:var(--accent,#376edc);font-weight:800}.sf-image-text>div>p{font-size:17px;line-height:1.8;max-width:520px;opacity:.7}.sf-reverse>div:first-child{order:2}.sf-gallery{display:grid;grid-template-columns:repeat(3,1fr);gap:15px}.sf-gallery img{width:100%;height:230px;object-fit:cover;border-radius:6px}.sf-faq{max-width:920px}.sf-faq details{border-bottom:1px solid #dce3eb;padding:22px 0}.sf-faq summary{font-size:17px;cursor:pointer;font-weight:700}.sf-faq p{color:#647181;line-height:1.75}.sf-cta-wrap{display:flex;justify-content:space-between;align-items:center;gap:24px}.sf-cta-wrap h2{margin:0}.sf-cta-wrap p{font-size:16px;opacity:.75}.sf-section a{cursor:pointer}.sf-muted{opacity:.65} +@media(max-width:820px){.sf-container{padding:0 20px}.sf-section{padding:50px 0}.sf-hero-grid,.sf-image-text{grid-template-columns:1fr;gap:26px}.sf-reverse>div:first-child{order:initial}.sf-hero-img,.sf-image-side{min-height:250px}.sf-features,.sf-collection{grid-template-columns:repeat(2,minmax(0,1fr))}.sf-stats{grid-template-columns:repeat(2,1fr)}.sf-cta-wrap{align-items:flex-start;flex-direction:column}} +@media(max-width:540px){.sf-features,.sf-collection,.sf-gallery{grid-template-columns:1fr}.sf-section-head{display:block}.sf-section-head>a{display:block;margin-top:13px}.sf-item-img{height:220px}.sf-hero-grid{gap:22px}.sf-hero-copy p{font-size:16px}.sf-stats{gap:28px}} + +.sf-attributes{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:0 32px;border-top:1px solid #e5eaf0}.sf-attribute{display:flex;justify-content:space-between;align-items:center;gap:18px;border-bottom:1px solid #e5eaf0;padding:19px 10px;font-size:14px}.sf-attribute span{color:#748497}.sf-attribute strong{font-weight:720;text-align:right}@media(max-width:640px){.sf-attributes{grid-template-columns:1fr}} + +/* SiteForge responsive no-code grid container */ +.sf-layout-grid{display:grid;grid-template-columns:var(--sf-layout-cols,1fr 1fr);gap:var(--sf-layout-gap,24px);align-items:stretch} +.sf-layout-column{min-width:0}.sf-layout-column>.sf-section{padding:0;margin:0 0 18px}.sf-layout-column .sf-container{width:100%;padding:0} +.sf-layout-column>.sf-section:last-child{margin-bottom:0} +@media(max-width:820px){.sf-layout-grid{grid-template-columns:repeat(2,minmax(0,1fr))!important}} +@media(max-width:540px){.sf-layout-grid{grid-template-columns:minmax(0,1fr)!important}} diff --git a/app/static/commercial.css b/app/static/commercial.css new file mode 100644 index 0000000..8f49cf6 --- /dev/null +++ b/app/static/commercial.css @@ -0,0 +1,27 @@ +/* SiteForge commercial theme UX layer: accessibility, layout rhythm, media, responsive navigation. */ +:root{--sf-shadow:0 20px 55px rgba(17,33,55,.08);--sf-shadow-hover:0 26px 65px rgba(17,33,55,.15)} +html{scroll-padding-top:104px}body{font-feature-settings:"kern";text-rendering:optimizeLegibility} +a,button,input,select,textarea{-webkit-tap-highlight-color:transparent} +a:focus-visible,button:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible{outline:3px solid var(--pro-accent);outline-offset:4px} +.pro-wrap{width:100%;max-width:min(var(--pro-wide),100%)}.pro-header{isolation:isolate;box-shadow:0 1px 0 rgba(15,35,60,.035)}.pro-head-inner{gap:clamp(15px,2vw,29px)}.pro-logo{min-width:0;flex-shrink:0;gap:12px}.pro-brand-logo{max-width:155px;max-height:46px;width:auto;height:auto;object-fit:contain}.pro-logo-name{overflow-wrap:anywhere}.pro-navigation{gap:clamp(11px,1.6vw,24px)}.pro-header-cta{white-space:nowrap} +.pro-mobile-toggle,.pro-catalog-toggle,.pro-nav-disclose{border:0;background:transparent;color:inherit;line-height:1.2} +.pro-mobile-toggle{min-height:44px;min-width:44px;border:1px solid currentColor;border-radius:6px;font-size:19px}.pro-nav-group{position:relative;display:flex;align-items:center;gap:2px} +.pro-nav-disclose{padding:9px;font-size:13px}.pro-nav-children{position:absolute;top:100%;left:0;min-width:215px;max-width:330px;display:grid;gap:2px;padding:13px;box-shadow:var(--sf-shadow);border-radius:10px;z-index:45;background:var(--pro-background);border:1px solid var(--pro-border);color:var(--pro-text)} +.pro-nav-children[hidden],.pro-mega[hidden]{display:none!important}.pro-nav-children a{display:block;font-size:13px;white-space:normal;padding:10px;border-radius:5px}.pro-nav-children a:hover{background:var(--pro-surface)} +.pro-catalog-toggle{padding:11px 3px;white-space:nowrap;font-size:12px;font-weight:700}.pro-mega{position:absolute;top:100%;left:50%;transform:translateX(-50%);width:min(100%,var(--pro-wide));box-shadow:var(--sf-shadow);z-index:41;display:grid;grid-template-columns:215px 1fr;background:var(--pro-background);color:var(--pro-text);border:1px solid var(--pro-border);border-radius:0 0 15px 15px;padding:29px;align-items:start}.pro-mega>span{font-size:12px;letter-spacing:.06em}.pro-mega>div{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:9px 22px;overflow:visible}.pro-mega>div a{white-space:normal;padding:10px 5px;border-bottom:1px solid var(--pro-border)} +.pro-home-hero{overflow:hidden}.pro-home-hero .pro-home-grid{min-height:660px;gap:5%}.pro-home-hero .pro-hero-copy{position:relative;z-index:2}.pro-home-hero .pro-hero-copy h1{font-size:clamp(46px,5.7vw,92px);line-height:1.06;letter-spacing:-.064em;max-width:15ch}.pro-home-hero .pro-hero-copy h1 em{font-style:normal;display:block}.pro-home-hero .pro-hero-copy p{font-size:clamp(15px,1.3vw,19px);line-height:1.75;max-width:44ch}.pro-home-hero .pro-hero-art{min-width:0;min-height:425px;position:relative;isolation:isolate}.pro-home-hero .pro-hero-art>img{width:100%;height:100%;object-fit:cover;position:absolute;inset:0}.pro-hero-art:has(.pro-abstract-car),.pro-hero-art:has(.pro-technical),.pro-hero-art:has(.pro-corporate-shape){background-position:center;background-size:cover;background-repeat:no-repeat}.pro-abstract-car,.pro-technical,.pro-corporate-shape{position:relative;z-index:1;pointer-events:none} +.pro-hero-actions{display:flex;flex-wrap:wrap;align-items:center;gap:15px}.pro-button,.pro-header-cta,.pro-outline-button{min-height:48px;transition:transform .24s ease,box-shadow .24s ease,opacity .24s ease}.pro-button:hover,.pro-header-cta:hover,.pro-outline-button:hover{transform:translateY(-2px);box-shadow:0 10px 22px rgba(0,0,0,.10)} +.pro-section{padding-top:clamp(76px,7vw,112px)!important;padding-bottom:clamp(76px,7vw,112px)!important}.pro-section-head{gap:16px}.pro-section-head h2{font-size:clamp(32px,3.5vw,53px);max-width:22ch}.pro-section-head .pro-text-link{align-self:end} +.pro-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:20px}.pro-card{min-width:0;display:flex;flex-direction:column;transition:transform .28s ease,box-shadow .28s ease;border:1px solid var(--pro-border);background:var(--pro-background);overflow:hidden}.pro-card:hover{transform:translateY(-6px);box-shadow:var(--sf-shadow-hover)}.pro-card-cover{aspect-ratio:1.42;overflow:hidden;background:var(--pro-surface);position:relative}.pro-card-cover .pro-photo{width:100%;height:100%;object-fit:cover;transition:transform .45s ease}.pro-card:hover .pro-photo{transform:scale(1.045)}.pro-card-body{display:flex;flex-direction:column;flex:1}.pro-card-body h3{font-size:clamp(19px,1.6vw,24px);line-height:1.26}.pro-card-body p{font-size:14px}.pro-card-link{margin-top:auto;padding-top:14px;font-size:13px;font-weight:700} +.pro-art{min-height:100%;width:100%;position:relative;overflow:hidden;display:grid;place-items:center;isolation:isolate}.pro-art-ring{width:60%;aspect-ratio:1;border:1px solid currentColor;opacity:.13;border-radius:50%;transform:rotate(20deg);box-shadow:0 0 0 23px #ffffff10,0 0 0 57px #ffffff07}.pro-art-center{font-size:clamp(38px,5vw,80px);position:absolute;font-weight:800;letter-spacing:-.065em;opacity:.65}.pro-art-label{position:absolute;bottom:19px;left:22px;font-size:10px;letter-spacing:.22em;opacity:.7} +.pro-breadcrumb{display:flex;gap:11px;flex-wrap:wrap;font-size:12px;align-items:center}.pro-breadcrumb a{text-decoration:underline;text-underline-offset:4px}.pro-product-hero{align-items:start;gap:clamp(28px,5vw,80px)}.pro-product-media{aspect-ratio:1.12;background:var(--pro-surface);overflow:hidden}.pro-product-media>img,.pro-product-media>.pro-art{width:100%;height:100%;object-fit:contain}.pro-product-summary{padding:clamp(12px,3vw,50px) 0}.pro-product-summary h1{font-size:clamp(36px,4.3vw,72px)}.pro-detail-layout{border-top:1px solid var(--pro-border);padding-top:50px}.pro-specs>div{display:grid;grid-template-columns:minmax(100px,40%) 1fr;gap:10px;align-items:baseline;padding:15px 0;border-bottom:1px solid var(--pro-border)}.pro-specs strong{overflow-wrap:anywhere}.pro-gallery-thumbs button{flex-basis:96px;height:76px}.pro-gallery-lightbox{backdrop-filter:blur(6px)} +.pro-filter-panel{display:flex;align-items:end;flex-wrap:wrap;gap:16px;background:var(--pro-surface);border:1px solid var(--pro-border);padding:20px;margin-bottom:25px;border-radius:var(--pro-radius)}.pro-filter-panel label{display:grid;gap:7px;min-width:180px;flex:1}.pro-filter-panel input,.pro-filter-panel select{width:100%;background:var(--pro-background);border:1px solid var(--pro-border);padding:12px 14px;color:var(--pro-text);border-radius:6px}.pro-filter-count{white-space:nowrap;font-size:13px;padding:12px 0}.pro-card[hidden]{display:none!important}.pro-filter-empty{padding:38px;border:1px dashed var(--pro-border);text-align:center;grid-column:1/-1} +.pro-inquiry input,.pro-inquiry textarea{min-height:46px}.pro-inquiry textarea{min-height:150px}.pro-inquiry-wrap{gap:clamp(32px,7vw,110px)}.pro-inquiry .pro-button{border:0;justify-self:start}.pro-footer-links a{padding-block:5px;display:inline-block} +.pro-end-cta{overflow:hidden}.pro-end-cta h2{font-size:clamp(30px,4vw,64px)} +.pro-rich{line-height:1.85}.pro-rich img{border-radius:var(--pro-radius);margin-block:24px}.pro-rich table{width:100%;display:block;overflow:auto;border-collapse:collapse}.pro-rich td,.pro-rich th{border:1px solid var(--pro-border);padding:10px 14px}.pro-rich a{text-decoration:underline;text-underline-offset:3px} +@media(min-width:1025px){.pro-navigation>.pro-nav-group:hover .pro-nav-children:not([hidden]){visibility:visible}} +@media(max-width:1150px){.pro-head-inner{gap:15px}.pro-navigation{gap:12px}.pro-logo-name{font-size:15px}.pro-header-cta{padding:13px 16px}} +@media(max-width:1024px){.pro-head-inner{min-height:72px;flex-wrap:wrap}.pro-mobile-toggle{display:inline-flex;align-items:center;justify-content:center;margin-left:auto}.pro-navigation{display:none;width:100%;flex-basis:100%;flex-direction:column;align-items:stretch;gap:0;order:5;margin:0;padding:8px 0 20px;border-top:1px solid var(--pro-border)}.pro-navigation.is-open{display:flex}.pro-navigation>a,.pro-nav-group>a{padding:14px 8px;font-size:15px}.pro-nav-group{display:flex;flex-wrap:wrap;justify-content:space-between}.pro-nav-children{position:static;flex-basis:100%;max-width:none;width:100%;box-shadow:none;border-radius:0;border-left:2px solid var(--pro-accent);margin:0 0 12px 8px}.pro-nav-disclose{min-width:44px;min-height:44px}.pro-catalog-toggle{margin-left:auto}.pro-mega{top:100%;display:block;padding:20px}.pro-mega>div{grid-template-columns:repeat(2,minmax(0,1fr));margin-top:12px}.pro-home-hero .pro-home-grid{min-height:560px}.pro-home-hero .pro-hero-art{min-height:325px}.pro-grid{grid-template-columns:repeat(2,minmax(0,1fr))}} +@media(max-width:700px){.pro-wrap{padding-left:20px;padding-right:20px}.pro-utility{display:none}.pro-head-inner{min-height:66px}.pro-logo-name{max-width:140px;font-size:14px}.pro-brand-logo{max-width:125px}.pro-header-cta{display:none}.pro-catalog-toggle{font-size:11px;padding-inline:7px;margin-left:0}.pro-home-hero .pro-home-grid{display:flex;flex-direction:column;gap:36px;padding-top:48px;padding-bottom:48px}.pro-home-hero .pro-hero-copy h1{font-size:clamp(43px,10vw,68px)}.pro-home-hero .pro-hero-copy p{font-size:15px}.pro-home-hero .pro-hero-art{width:100%;min-height:290px}.pro-home-hero .pro-hero-bottom{display:none}.pro-section{padding-top:68px!important;padding-bottom:68px!important}.pro-section-head{align-items:flex-start}.pro-grid{grid-template-columns:1fr;gap:18px}.pro-card-cover{aspect-ratio:1.45}.pro-product-hero,.pro-detail-layout,.pro-info-grid,.pro-inquiry-wrap{display:grid;grid-template-columns:1fr;gap:30px}.pro-product-summary{padding:0}.pro-specs>div{grid-template-columns:1fr 1fr}.pro-mega{top:100%;max-height:65vh;overflow-y:auto}.pro-mega>div{grid-template-columns:1fr}.pro-filter-panel{display:grid;grid-template-columns:1fr;gap:12px}.pro-filter-panel label{min-width:0}.pro-footer-top{display:grid;grid-template-columns:1fr;gap:27px}.pro-footer-bottom{gap:12px;flex-wrap:wrap}} +@media(max-width:410px){.pro-logo-name{max-width:115px}.pro-logo-symbol{font-size:28px}.pro-catalog-toggle{font-size:10px}.pro-mobile-toggle{min-width:42px}} +@media(prefers-reduced-motion:reduce){html{scroll-behavior:auto}*,*:before,*:after{animation-duration:.01ms!important;transition-duration:.01ms!important;scroll-behavior:auto!important}} diff --git a/app/static/editor.css b/app/static/editor.css new file mode 100644 index 0000000..6c10797 --- /dev/null +++ b/app/static/editor.css @@ -0,0 +1,4 @@ +:root{--ink:#263448;--muted:#8a99ae;--blue:#406dce;--line:#e8edf3;--panel:#fff;--bg:#f2f5f9;font-family:Inter,"Segoe UI","Microsoft YaHei",Arial,sans-serif}*{box-sizing:border-box}body{margin:0;color:var(--ink);background:var(--bg)}button,input,select,textarea{font:inherit}button{cursor:pointer}.studio{height:100vh;display:flex;flex-direction:column;min-height:650px}.studio-top{height:72px;min-height:72px;background:#fff;border-bottom:1px solid var(--line);display:flex;align-items:center;padding:0 24px;gap:16px;z-index:2}.brand{display:flex;align-items:center;gap:9px}.brand>a{font-size:29px;line-height:1;background:#1e314e;color:white;text-decoration:none;width:40px;height:40px;border-radius:12px;display:grid;place-items:center}.brand b{display:block;font-size:16px;letter-spacing:-.45px}.brand small{font-size:8px;font-weight:750;color:var(--muted);letter-spacing:1.6px}.divide{height:26px;border-left:1px solid #d9e0ea}.file-name{font-size:13px;white-space:nowrap;max-width:280px;overflow:hidden;text-overflow:ellipsis}.file-name small{color:#c0c9d3}.file-name strong{font-weight:700}.top-center{display:flex;gap:3px;background:#f2f5f9;padding:4px;border-radius:9px;margin:auto}.icon-btn{border:0;border-radius:6px;background:transparent;padding:6px 11px;font-size:20px;color:#718299}.icon-btn.on{background:#fff;color:#355dbb;box-shadow:0 2px 8px #28466716}.save-status{color:#91a0ae;font-size:11px;white-space:nowrap}.save-status.dirty{color:#be8530}.ui-btn{border-radius:8px;padding:11px 15px;font-weight:650;font-size:12px;border:1px solid #e5eaf0;white-space:nowrap}.ui-btn.ghost{background:#fff;color:#344764}.ui-btn.primary{background:#315bba;color:#fff;border-color:#315bba}.ui-btn:hover,.small-btn:hover{filter:brightness(.96)}.studio-body{display:grid;grid-template-columns:278px minmax(0,1fr) 324px;min-height:0;flex:1}.left-pane,.right-pane{background:#fff;overflow-y:auto;display:flex;flex-direction:column}.left-pane{border-right:1px solid var(--line)}.right-pane{border-left:1px solid var(--line)}.pane-heading{padding:27px 24px 17px;display:flex;justify-content:space-between;align-items:center}.overline{font-size:9px;font-weight:800;color:#7894c4;letter-spacing:1.4px}.pane-heading h3{margin:4px 0 0;font-size:19px;letter-spacing:-.4px}.chip{font-size:10px;background:#eff4fb;color:#6d87ad;padding:4px 8px;border-radius:5px;white-space:nowrap}.left-pane .field{margin:0 19px 14px}.field label,.prop label{display:block;font-size:11px;font-weight:750;color:#758499;margin-bottom:7px}.field select,.prop input,.prop textarea,.prop select{width:100%;border:1px solid #dce5ed;border-radius:8px;padding:10px 12px;background:white;color:#2b3c53;outline:0;font-size:12px}.field select:focus,.prop input:focus,.prop textarea:focus,.prop select:focus{border-color:#5589d9;box-shadow:0 0 0 3px #5f90de12}.section-label{font-size:10px;font-weight:800;color:#9da9b8;letter-spacing:.7px;padding:17px 22px 11px}.block-list{padding:0 13px}.block-row{display:flex;align-items:center;gap:9px;padding:11px 10px;margin-bottom:7px;background:#f8fafc;border:1px solid #e6ecf3;border-radius:10px;cursor:pointer}.block-row.selected{border-color:#729cf0;background:#eff5ff;box-shadow:0 0 0 1px #e9f0ff}.block-row.dragging{opacity:.35}.drag-handle{font-size:17px;color:#aab9cc;cursor:grab}.block-icon{display:grid;place-items:center;background:white;width:30px;height:30px;border-radius:7px;color:#5b76b2;font-size:17px;border:1px solid #e8edf4}.block-copy{flex:1;min-width:0}.block-copy strong{display:block;font-size:12px}.block-copy small{display:block;font-size:10px;color:#9aa7b8;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.row-actions{display:flex;gap:2px}.row-actions button{border:0;background:transparent;font-size:12px;color:#9aa5b4;padding:4px}.palette{padding:0 19px 20px;display:grid;grid-template-columns:1fr 1fr;gap:9px}.palette button{display:flex;gap:7px;align-items:center;border:1px dashed #cdd9e6;background:#fcfdff;color:#566b83;border-radius:9px;padding:11px 9px;text-align:left;font-size:11px;font-weight:700}.palette button span{color:#416fcd;font-size:17px}.sidebar-foot{font-size:10px;line-height:1.8;color:#98a5b6;margin:auto 20px 22px;padding-top:24px}.preview-workspace{min-width:0;display:flex;flex-direction:column;background:#eef1f5}.preview-head{height:50px;min-height:50px;display:flex;align-items:center;gap:10px;padding:0 26px;border-bottom:1px solid #e1e7f0;color:#64758b;font-size:11px}.preview-head code{font-size:10px;padding:4px 7px;border-radius:5px;background:#e2e8f0;color:#445b78}.live-dot{height:7px;width:7px;border-radius:50%;background:#36c993;box-shadow:0 0 0 4px #36c99318}.preview-note{margin-left:auto;color:#9ba9b8}.stage{flex:1;overflow:auto;padding:25px;display:flex;align-items:flex-start;justify-content:center;background:radial-gradient(circle at 20px 20px,#dce3eb 1px,transparent 1px);background-size:20px 20px}.device-frame{background:white;width:100%;max-width:1440px;min-height:700px;border:1px solid #dce3eb;box-shadow:0 15px 45px #273b5217;border-radius:5px;transition:width .2s}.device-frame.tablet{width:780px;flex-shrink:0}.device-frame.phone{width:390px;flex-shrink:0}iframe{display:block;border:0;width:100%;height:calc(100vh - 183px);min-height:700px}.inspector{padding:2px 22px;flex:1}.no-selection{margin-top:20px;background:#f5f8fb;border-radius:11px;padding:22px;font-size:12px;line-height:1.8;color:#8294a8}.prop{margin:0 0 19px}.prop textarea{min-height:96px;resize:vertical;line-height:1.5}.prop input[type=color]{height:42px;padding:4px}.prop-tip{display:block;margin-top:6px;color:#9da9b8;font-size:10px;line-height:1.5}.inspector-title{display:flex;align-items:center;justify-content:space-between;margin-bottom:21px}.inspector-title h3{font-size:17px;margin:0}.toggle{display:flex;align-items:center;gap:8px;color:#697f98;font-size:12px}.toggle input{width:17px;height:17px}.subheading{font-size:11px;font-weight:800;margin:23px 0 13px;color:#788ba3;border-top:1px solid #e8edf3;padding-top:20px}.small-btn{background:#f2f6fa;border:1px solid #e2e8ee;padding:8px 12px;border-radius:8px;color:#5675a1;font-size:11px;font-weight:700}.red-btn{background:#fff0f0;color:#bb5055;border-color:#ffe1e1}.right-foot{margin:22px;padding:19px;background:#f5f8fc;border:1px solid #e7edf5;border-radius:10px;font-size:11px}.right-foot div{font-weight:760}.right-foot p{line-height:1.8;color:#96a3b3;margin:7px 0 0}.right-foot code{color:#486fb6}.toast{position:fixed;bottom:25px;right:28px;z-index:10;background:#1c3555;color:white;padding:13px 19px;border-radius:9px;box-shadow:0 8px 30px #1e314e30;display:none;font-size:12px}.toast.show{display:block} +@media(max-width:1200px){.studio-body{grid-template-columns:240px minmax(0,1fr) 290px}.preview-note{display:none}.studio-top{gap:8px}.file-name{max-width:180px}}@media(max-width:950px){.studio-body{grid-template-columns:210px minmax(0,1fr)}.right-pane{position:fixed;right:0;top:72px;bottom:0;width:310px;z-index:3;box-shadow:-8px 0 22px #0001}.studio-top .ui-btn.ghost{display:none}}@media(max-width:650px){.studio-body{grid-template-columns:1fr}.left-pane{display:none}.right-pane{display:none}.studio-top{padding:0 12px}.file-name{display:none}.save-status{display:none}} + +.layout-editor-col{border:1px solid #dfe5ed;border-radius:12px;padding:10px;margin:12px 0;background:#f8fafc}.layout-editor-title{display:flex;align-items:center;gap:7px;flex-wrap:wrap;justify-content:space-between}.layout-editor-title input{width:56px}.layout-editor-title button,.layout-child button{border:0;background:none;color:#54647a;cursor:pointer}.layout-child{background:white;border:1px solid #e5eaf3;border-radius:9px;margin:8px 0;padding:9px}.layout-child label{display:block;font-size:11px;color:#475569;margin:7px 0}.layout-child input,.layout-child select{display:block;width:100%;margin-top:3px;padding:7px;border:1px solid #e1e6ee;border-radius:6px}.layout-child-toolbar{display:flex;flex-wrap:wrap;gap:4px}.layout-child-toolbar button{background:#e8effa;border:0;padding:5px 7px;border-radius:6px;cursor:pointer;font-size:11px} diff --git a/app/static/editor.js b/app/static/editor.js new file mode 100644 index 0000000..9175761 --- /dev/null +++ b/app/static/editor.js @@ -0,0 +1,90 @@ +'use strict'; +const $=id=>document.querySelector(id); +const html=s=>String(s??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); +const pathParts=location.pathname.split('/').filter(Boolean); +const siteId=pathParts[1],initialPage=pathParts[2]; +let csrf='',site=null,pages=[],schema=null,reusables=[],pageId=initialPage,blocks=[],kind='standard',layoutMode='append',selected=-1,dirty=false,timer=null; +const api=async(p,opt={})=>{if(!['GET','HEAD','OPTIONS'].includes((opt.method||'GET').toUpperCase()))opt.headers={...(opt.headers||{}),'X-SiteForge-CSRF':csrf};let r=await fetch(p,opt);if(r.status===401){location.assign('/login');throw Error('会话已过期')}let t=r.headers.get('content-type')||'';let body=t.includes('json')?await r.json():await r.text();if(!r.ok)throw Error(body.detail||body);return body}; +const req=(method,data)=>({method,headers:{'Content-Type':'application/json'},body:JSON.stringify(data)}); +const toast=(s,err=false)=>{const t=$('#toast');t.textContent=s;t.style.background=err?'#a23c44':'#1c3555';t.classList.add('show');setTimeout(()=>t.classList.remove('show'),3400)}; +const activePage=()=>pages.find(e=>e.id===pageId); +const Studio={ + async init(){try{csrf=(await api('/api/auth/me')).csrf;site=await api(`/api/sites/${siteId}`);pages=await api(`/api/sites/${siteId}/entries`);schema=await api('/api/editor/blocks');$('#siteName').textContent=site.name;const select=$('#pageSelect');select.innerHTML=pages.map(e=>``).join('');for(let [k,v] of Object.entries(schema.page_templates))$('#pageTemplate').innerHTML+=``;Studio.renderPalette();await Studio.loadReusables();await Studio.choosePage(pageId||pages[0]?.id)}catch(e){toast('初始化失败:'+e.message,true);console.error(e)}}, + async choosePage(id){if(dirty&&!confirm('当前页面尚未保存,确定切换?')){$('#pageSelect').value=pageId;return}pageId=id;$('#pageSelect').value=id;const data=await api(`/api/sites/${siteId}/pages/${id}/blueprint`);blocks=data.blocks.map(b=>structuredClone(b));kind=data.page_template;layoutMode=data.layout_mode||'append';$('#layoutMode').value=layoutMode;selected=blocks.length?0:-1;$('#pageTemplate').value=kind;$('#pageName').textContent=data.entry.title;$('#routeDisplay').textContent=data.entry.route;dirty=false;Studio.draw();Studio.refreshPreview()}, + async loadReusables(){try{reusables=await api(`/api/sites/${siteId}/components`);$('#reusablePalette').innerHTML=reusables.map(x=>``).join('')||'暂无保存的组件'}catch(e){console.warn(e)}}, + async saveReusable(){const block=blocks[selected];if(!block)return toast('请先选择一个区块',true);const name=prompt('为此组件命名',block.title||schema.blocks[block.type]?.name);if(!name?.trim())return;try{await api(`/api/sites/${siteId}/components`,req('POST',{name,block}));await Studio.loadReusables();toast('组件已保存到本站组件库')}catch(e){toast(e.message,true)}}, + insertReusable(id){const saved=reusables.find(x=>x.id===id);if(!saved)return;const copy=structuredClone(saved.block);const refreshIds=item=>{item.id=crypto.randomUUID().replace(/-/g,'').slice(0,10);if(item.type==='layout')(item.columns||[]).forEach(c=>(c.blocks||[]).forEach(refreshIds))};refreshIds(copy);blocks.push(copy);selected=blocks.length-1;Studio.modified();Studio.draw();}, + renderPalette(){let p=$('#palette');p.innerHTML=Object.entries(schema.blocks).map(([key,b])=>``).join('')}, + draw(){let list=$('#blockList');$('#blockCount').textContent=blocks.length+' 区块';list.innerHTML=blocks.map((b,i)=>`
⠿
${html(schema.blocks[b.type]?.icon||'▣')}
${html(schema.blocks[b.type]?.name||b.type)}${html(b.title||b.subtitle||b.type)}
`).join('')||'
当前没有区块,点击下方按钮添加。
';Studio.inspector()}, + select(i){selected=i;Studio.draw()}, + add(type){let data=structuredClone(schema.blocks[type].defaults);data.id=crypto.randomUUID().replace(/-/g,'').slice(0,10);data.type=type;data.visible=true;blocks.push(data);selected=blocks.length-1;Studio.modified();Studio.draw()}, + duplicate(i){let b=structuredClone(blocks[i]);b.id=crypto.randomUUID().replace(/-/g,'').slice(0,10);blocks.splice(i+1,0,b);selected=i+1;Studio.modified();Studio.draw()}, + remove(i){if(!confirm('确定删除这个区块?'))return;blocks.splice(i,1);selected=Math.min(i,blocks.length-1);Studio.modified();Studio.draw()}, + drag(e,i){e.dataTransfer.setData('text/plain',String(i));e.dataTransfer.effectAllowed='move';e.currentTarget.classList.add('dragging')}, + drop(e,i){e.preventDefault();let src=Number(e.dataTransfer.getData('text/plain'));if(!Number.isInteger(src)||src<0||src>=blocks.length||src===i)return;let [b]=blocks.splice(src,1);blocks.splice(i,0,b);selected=i;Studio.modified();Studio.draw()}, + field(key,value){const b=blocks[selected];if(!b)return;b[key]=value;Studio.modified();Studio.draw()}, + fieldQuiet(key,value){const b=blocks[selected];if(!b)return;b[key]=value;Studio.modified();const row=$('#blockList').children[selected];if(row)row.querySelector('small').textContent=b.title||b.subtitle||b.type}, + jsonField(key,value){try{let data=JSON.parse(value);if(!Array.isArray(data))throw Error('必须为数组');Studio.fieldQuiet(key,data);$('#jsonError').textContent='✓ JSON 格式有效';$('#jsonError').style.color='#238460'}catch(e){$('#jsonError').textContent='JSON 格式有误:'+e.message;$('#jsonError').style.color='#bf5a58'}}, + styleField(key,value){const b=blocks[selected];b.style={...(b.style||{}),[key]:value};Studio.modified()}, + inspector(){let target=$('#inspector');let b=blocks[selected];$('#blockTypeLabel').textContent=b?schema.blocks[b.type].name:'页面';if(!b){target.innerHTML='
请从左侧选择区块并编辑属性。
';return}const spec=schema.blocks[b.type];let out=`

${html(spec.name)}

`; + for(const field of spec.fields){let value=b[field]??'',label=({title:'标题',subtitle:'副标题',description:'简介',image:'图片地址',body:'正文 HTML',button:'按钮文字',href:'跳转路径',source:'绑定内容类型',limit:'显示条数',columns:'每行列数',items:'重复条目',images:'画廊图片',image_side:'图片方向',align:'标题对齐',height:'间隔高度'})[field]||field; + out+=`
`; + if(field==='items'||field==='images')out+=`请输入 JSON 数组,可编辑多个项目`; + else if(field==='source')out+=`网站已有内容将自动绑定并生成卡片。`; + else if(field==='image_side'||field==='align')out+=``; + else if(field==='body'||field==='description')out+=``; + else if(['limit','columns','height'].includes(field))out+=``; + else out+=``; + if(['title','subtitle','description','image','body'].includes(field))out+='可输入 {{site.name}}、{{page.title}} 等变量。'; + out+='
'; + } + if(b.type==='layout'){ + out+=`
列内组件 · 无代码布局

一行最多四列,拖动整个容器可改变行顺序。列在手机上自动变为单列。

`; + b.columns.forEach((column,ci)=>{ + out+=`
第${ci+1}列
`; + column.blocks.forEach((child,j)=>{out+=`
${html(schema.blocks[child.type]?.name||child.type)}`; + for(const key of (schema.blocks[child.type]?.fields||[]).filter(x=>['title','subtitle','description','image','body','href','button'].includes(x))){out+=``} + if(b.columns.length>1){out+=``} + out+='
'; + }); + out+=`
${['richtext','image_text','collection','cta','features'].map(t=>``).join('')}
`; + }); + out+=''; + } + out+=`
区块设计(按设备覆盖)
`; + out+='
'; + target.innerHTML=out; + }, + modified(){dirty=true;$('#saveStatus').textContent='未保存更改';$('#saveStatus').classList.add('dirty');clearTimeout(timer);timer=setTimeout(()=>Studio.refreshPreview(),500)}, + async refreshPreview(){if(!pageId)return;try{let frame=$('#sitePreview');let doc=await api(`/api/sites/${siteId}/pages/${pageId}/editor-preview`,req('POST',{blocks,page_template:kind,layout_mode:layoutMode}));frame.srcdoc=doc}catch(e){toast('预览失败:'+e.message,true)}}, + device(type){$('#deviceFrame').className='device-frame '+(type==='desktop'?'':type);for(let name of ['Desktop','Tablet','Phone'])$('#device'+name).classList.toggle('on',name.toLowerCase()===type)}, + changeTemplateKind(t){kind=t;Studio.modified()}, + setLayoutMode(v){layoutMode=v;Studio.modified()}, + async resetPreset(){if(!confirm('使用预设区块将覆盖当前页面布局,是否继续?'))return;try{let result=await api(`/api/sites/${siteId}/pages/${pageId}/blueprint/preset`,req('POST',{page_template:kind}));blocks=result.blocks;selected=0;dirty=false;Studio.draw();Studio.refreshPreview();toast('已加载页面预设并保存')}catch(e){toast(e.message,true)}}, + async save(){try{const result=await api(`/api/sites/${siteId}/pages/${pageId}/blueprint`,req('PUT',{blocks,page_template:kind,layout_mode:layoutMode}));blocks=result.blocks;dirty=false;$('#saveStatus').textContent='已保存';$('#saveStatus').classList.remove('dirty');toast('页面蓝图已保存,重新构建后会生成正式静态页面')}catch(e){toast('保存失败:'+e.message,true)}}, + addColumn(){const b=blocks[selected];if(!b||b.type!=='layout'||b.columns.length>=4)return;b.columns.push({width:1,blocks:[]});Studio.modified();Studio.inspector()}, + removeColumn(i){const b=blocks[selected];if(!b||b.type!=='layout'||b.columns.length<=1)return;if(b.columns[i].blocks.length&&!confirm('这一列含有组件,确认删除?'))return;b.columns.splice(i,1);Studio.modified();Studio.inspector()}, + setColumnWidth(i,value){const b=blocks[selected];if(!b||b.type!=='layout')return;b.columns[i].width=Math.max(.5,Math.min(4,Number(value)||1));Studio.modified()}, + addChild(i,type){const b=blocks[selected];if(!b||b.type!=='layout')return;const item=structuredClone(schema.blocks[type].defaults);item.type=type;item.id=crypto.randomUUID().replace(/-/g,'').slice(0,10);item.visible=true;b.columns[i].blocks.push(item);Studio.modified();Studio.inspector()}, + removeChild(i,j){const b=blocks[selected];if(!b||b.type!=='layout')return;b.columns[i].blocks.splice(j,1);Studio.modified();Studio.inspector()}, + childField(i,j,key,value){const b=blocks[selected];if(!b||b.type!=='layout')return;b.columns[i].blocks[j][key]=value;Studio.modified()}, + moveChild(ci,ri,target){const b=blocks[selected];if(!b||b.type!=='layout'||target<0||target>=b.columns.length)return;let [component]=b.columns[ci].blocks.splice(ri,1);b.columns[target].blocks.push(component);Studio.modified();Studio.inspector()}, + async uploadImage(file){if(!file)return;try{const form=new FormData();form.append('file',file);let r=await api(`/api/sites/${siteId}/media`,{method:'POST',body:form});Studio.field('image',r.url);toast('图片已上传,保存页面后参与构建')}catch(e){toast(e.message,true)}}, + async buildSite(){if(dirty){await Studio.save();if(dirty)return}try{toast('开始构建网站……');let result=await api(`/api/sites/${siteId}/builds`,req('POST',{mirror_images:false}));toast('构建完成:'+result.pages+' 个页面');window.open(`/preview/${siteId}/${result.version}/`,'_blank','noopener')}catch(e){toast('构建失败:'+e.message,true)}}, +}; +window.Studio=Studio; +window.addEventListener('DOMContentLoaded',()=>Studio.init()); +window.addEventListener('beforeunload',e=>{if(dirty){e.preventDefault();e.returnValue='';}}); + +window.addEventListener('message',event=>{ + const frame=document.querySelector('#sitePreview'); + if(!frame||event.source!==frame.contentWindow)return; + const data=event.data||{}; + if(data.source!=='siteforge-inline-editor')return; + const findChild=rows=>{for(const row of rows){if(row.id===data.blockId)return {block:row,parent:row};if(row.type==='layout'){for(const col of row.columns||[]){const child=findChild(col.blocks||[]);if(child)return {block:child.block,parent:row};}}}return null}; + const found=findChild(blocks),b=found?.block; + if(!b||!['title','description'].includes(data.field)||typeof data.value!=='string')return; + b[data.field]=data.value.slice(0,500); + selected=blocks.indexOf(found.parent);Studio.modified();Studio.draw(); + toast('已修改文字,点击保存页面写入数据库'); +}); diff --git a/app/static/enterprise-ui.js b/app/static/enterprise-ui.js new file mode 100644 index 0000000..a39f13e --- /dev/null +++ b/app/static/enterprise-ui.js @@ -0,0 +1,58 @@ +'use strict'; +// Enterprise pages extend the existing reliable Docker Web console without requiring a CDN. +tabs.push(['design-system','设计规范'],['migration-qa','迁移验收'],['operations','任务 / 版本'],['permissions','成员 / 审计']); +const sfEnterpriseApi=async (path,method='GET',data)=>api(path,method==='GET'?{}:json(method,data)); +const sfRoot='/api/enterprise'; +const _oldRenderTab=App.renderTab.bind(App); +App.renderTab=function(){ + if(!['design-system','migration-qa','operations','permissions'].includes(state.tab))return _oldRenderTab(); + const el=document.getElementById('tab-content'); + el.innerHTML='

正在读取站点工作数据...

'; + if(state.tab==='design-system')EnterpriseUI.design().catch(onError); + if(state.tab==='migration-qa')EnterpriseUI.migration().catch(onError); + if(state.tab==='operations')EnterpriseUI.operations().catch(onError); + if(state.tab==='permissions')EnterpriseUI.permissions().catch(onError); +}; +const EnterpriseUI={ + section(name,caption='',actions=''){return `

${E(name)}

${E(caption)}

${actions}
`}, + async design(){ + const x=await sfEnterpriseApi(`${sfRoot}/sites/${state.siteId}/design-tokens`); + const fields=[['font','全站字体','select',['system','serif','modern']],['heading_scale','标题比例','number'],['radius','圆角半径(px)','number'],['section_spacing','板块上下间距(px)','number'],['container_width','内容最大宽度(px)','number'],['button_style','按钮形状','select',['rounded','pill','square']],['header_style','导航密度','select',['standard','compact']]]; + document.getElementById('tab-content').innerHTML=`
${this.section('全站设计规范','同一个主题的不同网站可拥有完全不同的字体、间距、圆角和排版节奏。')}
${fields.map(([k,name,type,options])=>``).join('')}
`; + }, + async saveDesign(event){event.preventDefault();try{ + const raw=Object.fromEntries(new FormData(event.target)); + for(const k of ['heading_scale','radius','section_spacing','container_width'])raw[k]=Number(raw[k]); + await sfEnterpriseApi(`${sfRoot}/sites/${state.siteId}/design-tokens`,'PUT',raw); + notice('设计规范已保存,重新构建即可查看新效果'); + }catch(err){onError(err)}}, + async migration(){ + const r=await sfEnterpriseApi(`${sfRoot}/sites/${state.siteId}/baseline/report`); + const badge=r.errors?`${r.errors} 项 URL 错误`:'无 URL 缺失'; + document.getElementById('tab-content').innerHTML=`
${this.section('旧站 URL / SEO 基线验收','迁移模式发布前必须建立旧站基线,杜绝静默漏页。',``)}
旧 URL 清单${r.baseline_count}
当前页面${r.current_count}
URL 缺失${r.errors}
SEO 差异${r.warnings}

注意:最可靠的做法是在导入旧站后、换模板和调整内容前建立基线。也可调用 API 批量导入旧站已知 URL 清单。

${r.issues.map(x=>`
${E(x.route)} · ${E(x.field)}
${E(x.from)} → ${E(x.to)}
`).join('')||'
暂无 URL 或 SEO 差异。
'}
`; + }, + async snapshot(){if(!confirm('这会用当前站点的 URL 与 SEO 覆盖旧基线。推荐在旧站导入完成后、进行改版前操作。是否继续?'))return; + try{const r=await sfEnterpriseApi(`${sfRoot}/sites/${state.siteId}/baseline/snapshot`,'POST',{});notice(`已记录 ${r.captured} 个原始页面`);await this.migration()}catch(err){onError(err)}}, + async operations(){ + const jobs=await sfEnterpriseApi(`${sfRoot}/sites/${state.siteId}/jobs`); + const releases=await sfEnterpriseApi(`${sfRoot}/sites/${state.siteId}/releases`); + const builds=state.builds; + document.getElementById('tab-content').innerHTML=`
${this.section('异步构建任务','后台 Worker 负责执行耗时构建,关闭浏览器也不影响任务。',``)}${jobs.map(j=>``).join('')||''}
任务 ID状态尝试次数创建时间
${E(j.id)}${E(j.state)}${j.attempts}${simpleDate(j.created_at)}
暂无任务
${this.section('构建版本与质量检测','检查 HTML / 站内链接 / 图片 ALT / SEO,再决定是否发布。')}
${builds.slice(0,15).map(b=>`
${E(b.id)} · ${b.total_pages} 页

${simpleDate(b.created_at)}

`).join('')||'

暂无构建,请先提交任务。

'}
${this.section('本地发布记录','发布记录保留全部构建版本,方便核对和恢复。')}${releases.slice(0,30).map(r=>`

${E(r.action)} · ${E(r.build_id)} · ${simpleDate(r.created_at)}

`).join('')||'

暂无发布历史

'}
`; + }, + async queueBuild(){try{const j=await sfEnterpriseApi(`${sfRoot}/sites/${state.siteId}/jobs`,'POST',{kind:'build'});notice('构建任务已提交 '+j.id);await this.operations()}catch(err){onError(err)}}, + async quality(id){try{const r=await sfEnterpriseApi(`${sfRoot}/sites/${state.siteId}/builds/${id}/quality`);const el=document.getElementById('sf-quality-result');if(el)el.innerHTML=`

检查结果 · ${r.files} 页 · ${r.errors} 错误 · ${r.warnings} 警告

${r.issues.slice(0,60).map(i=>`

${E(i.file)} ${E(i.type)} ${E(i.target||'')}

`).join('')||'通过基础静态质量检查'}
`}catch(err){onError(err)}}, + async rollback(id){if(!confirm('确认将此构建版本设为当前本地发布版本?迁移站发布前必须通过 URL 基线检查。'))return; + try{const result=await sfEnterpriseApi(`${sfRoot}/sites/${state.siteId}/releases/${id}/rollback`,'POST',{});notice(result.published?'发布成功':'发布失败');await this.operations()}catch(err){onError(err)}}, + async permissions(){ + const logs=await sfEnterpriseApi(`${sfRoot}/audit?site_id=${state.siteId}&limit=50`); + let members=[];try{members=await sfEnterpriseApi(`${sfRoot}/sites/${state.siteId}/members`)}catch{} + let users=[];try{users=await sfEnterpriseApi(`${sfRoot}/team/users`)}catch{} + document.getElementById('tab-content').innerHTML=`
${this.section('网站团队与角色','Owner 可以管理成员,Editor 可以修改内容,Reviewer 和 Viewer 为只读角色。')}
${members.map(m=>`
${E(m.display_name||m.username)}

${E(m.username)} · ${E(m.role)}

`).join('')||'

尚未添加团队成员。系统管理员拥有全部权限。

'}
${users.length?`
`:''}
${this.section('操作审计日志','记录修改 API 的操作者、时间、路径与响应状态。')}
${logs.map(l=>`
${E(l.actor)} · ${E(l.operation)} · ${l.status}

${E(l.path)} · ${simpleDate(l.created_at)}

`).join('')||'

暂无操作日志

'}
`; + }, + async assign(event){event.preventDefault();try{await sfEnterpriseApi(`${sfRoot}/sites/${state.siteId}/members`,'PUT',Object.fromEntries(new FormData(event.target)));notice('权限已更新');await this.permissions()}catch(err){onError(err)}}, + async team(){try{const users=await sfEnterpriseApi(`${sfRoot}/team/users`);state.siteId=null;topTitle('团队账号'); + document.getElementById('view').innerHTML=hero('团队与权限','创建协作者,再为每个网站分配独立角色。')+`
${this.section('新增团队账号','至少12位密码,使用 scrypt 加盐哈希存储。')}
${this.section('现有成员','站点权限可在各个网站的“成员 / 审计”中配置。')}${users.map(u=>`
${E(u.username)}

${E(u.display_name)} · ${u.disabled?'停用':'启用'}

`).join('')||'

尚无协作者。

'}
`; + }catch(err){onError(err)}}, + async addUser(event){event.preventDefault();try{await sfEnterpriseApi(`${sfRoot}/team/users`,'POST',Object.fromEntries(new FormData(event.target)));notice('团队账号创建成功');await this.team()}catch(err){onError(err)}} +}; +window.EnterpriseUI=EnterpriseUI; diff --git a/app/static/factory-v2.css b/app/static/factory-v2.css new file mode 100644 index 0000000..f1dada8 --- /dev/null +++ b/app/static/factory-v2.css @@ -0,0 +1 @@ +:root{font-family:Inter,"Microsoft Yahei",-apple-system,BlinkMacSystemFont,Arial,sans-serif;color:#1b314c;background:#f5f7fb;font-size:14px}*{box-sizing:border-box}body{margin:0}button,select,input,textarea{font:inherit}button{cursor:pointer}.shell{display:flex;min-height:100vh}.sidebar{flex:0 0 236px;background:#101e35;color:#c6d2e5;padding:24px 15px;display:flex;flex-direction:column;position:sticky;top:0;height:100vh}.identity{text-decoration:none;display:flex;gap:11px;align-items:center;color:#fff;padding:5px 12px 26px;border-bottom:1px solid #ffffff18}.identity .mark{background:#4b88ff;color:#fff;font-weight:900;border-radius:12px;width:42px;height:42px;display:grid;place-items:center;font-size:15px}.identity strong{display:block;font-size:20px;letter-spacing:-.8px}.identity small{color:#8e9eb9;font-size:9px;letter-spacing:1.5px}.side-title{padding:32px 13px 13px;font-size:10px;color:#7e91b0;letter-spacing:1.7px;font-weight:700}#navtabs{display:grid;gap:5px}#navtabs button{background:transparent;border:0;color:#aebbd0;text-align:left;padding:13px 15px;border-radius:9px;font-weight:620}#navtabs button.active,#navtabs button:hover{background:#2c4671;color:#fff}.side-bottom{margin-top:auto;padding:16px 12px;display:grid;gap:12px;border-top:1px solid #ffffff12}.side-bottom a{color:#dce6f5;text-decoration:none}.side-bottom small{color:#778ba7}.work{flex:1;min-width:0}.top{min-height:76px;background:#fff;border-bottom:1px solid #e2e9f2;display:flex;justify-content:space-between;align-items:center;padding:12px 32px;gap:22px}.top-crumb{font-size:13px;color:#8190a7}.top-crumb span{color:#20354e;font-weight:800}.top-controls{display:flex;gap:10px;align-items:center}.top-controls label{display:flex;align-items:center;gap:9px;font-size:12px;color:#667790}.top select{min-width:205px;max-width:280px;padding:10px;border:1px solid #e1e8f2;background:#f9fbfe;border-radius:8px;color:#1b314c}.primary,.outline{display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:10px 16px;border-radius:8px;text-decoration:none;font-size:13px;font-weight:700;border:1px solid #dce5ef;white-space:nowrap}.primary{background:#2772e5;color:#fff;border-color:#2772e5}.outline{background:#fff;color:#304a68}.primary:hover{background:#145ec2}.outline:hover{background:#eaf2ff}.content{max-width:1450px;margin:0 auto;padding:30px 36px}.heading{display:flex;justify-content:space-between;gap:20px;align-items:center;margin:12px 0 30px}.heading h1{margin:9px 0 7px;font-size:30px;letter-spacing:-.7px}.heading p{color:#75869c;margin:0}.eyebrow{text-transform:uppercase;color:#5585d6;letter-spacing:2.2px;font-size:10px;font-weight:750}.head-actions{display:flex;gap:9px}.cards{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:17px;margin-bottom:22px}.card{padding:22px;background:white;border:1px solid #e8edf4;border-radius:15px;box-shadow:0 6px 24px #18375108}.card .overline{color:#8998ad;font-size:11px}.card strong{display:block;margin:9px 0 4px;font-size:27px;font-weight:820;letter-spacing:-1px}.card small{color:#7185a0}.columns{display:grid;grid-template-columns:1.3fr .7fr;gap:20px}.section{background:white;border:1px solid #e5ecf5;border-radius:15px;padding:24px;margin-bottom:20px}.section h2{font-size:18px;margin:0 0 16px}.section p{line-height:1.7;color:#6b8095}.section .toolbar{display:flex;gap:10px;justify-content:space-between;align-items:center;margin-bottom:16px;flex-wrap:wrap}.gridthemes{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:18px}.theme-card{border:1px solid #e4eaf2;border-radius:13px;overflow:hidden;background:#fff}.theme-visual{height:154px;position:relative;overflow:hidden;padding:17px}.theme-visual .mocknav{height:16px;width:85%;border-radius:4px;background:#ffffffbb;margin-bottom:20px}.theme-visual .mocktitle{font-size:30px;line-height:1.05;letter-spacing:-1.2px;font-weight:900;max-width:170px}.theme-visual .mockbtn{width:65px;height:9px;margin-top:17px;background:#ffffff9c;border-radius:4px}.theme-info{padding:19px}.theme-info h3{margin:0 0 8px;font-size:16px}.theme-info p{font-size:12px;min-height:38px;margin:0 0 14px}.theme-info .actions{display:flex;gap:8px}.theme-info button{padding:9px;font-size:12px}.muted{color:#8b9ab0;font-size:12px}.badge{padding:5px 9px;background:#eaf3ff;color:#3579cb;font-size:11px;border-radius:100px}.badge.warn{background:#fff2da;color:#b87725}.badge.danger{background:#fee4e5;color:#bb3d4c}.stack{display:grid;gap:13px}.row{display:flex;gap:14px;align-items:center;justify-content:space-between;padding:13px 0;border-bottom:1px solid #edf1f6}.row>div{min-width:0}.row strong{display:block;margin-bottom:5px}.row small{color:#8393a6}.row:last-child{border-bottom:0}input:not([type=file]),textarea,select{padding:11px;border:1px solid #d9e3ef;border-radius:8px;background:#fff;color:#1e324b}input:not([type=file]):focus,textarea:focus,select:focus{outline:2px solid #98c2ff}label.field{display:grid;gap:7px;font-size:12px;color:#536780;font-weight:680;margin-bottom:12px}.field textarea{min-height:130px;resize:vertical}.field input,.field select{width:100%}table{width:100%;border-collapse:collapse;text-align:left}th{color:#8492a7;font-size:11px;font-weight:650;border-bottom:1px solid #e5ebf3;padding:12px 8px}td{font-size:12px;color:#2b4059;padding:12px 8px;border-bottom:1px solid #edf2f8}table td:first-child{font-weight:700}.scroll{overflow-x:auto}.error{color:#b63c4b}.success{color:#18896f}#alert{display:none;border-radius:9px;padding:13px 18px;margin-bottom:18px;background:#fff2db;color:#aa651c}#alert.on{display:block}dialog{border:0;border-radius:15px;box-shadow:0 24px 90px #1837513d;width:min(500px,95vw);padding:27px}dialog::backdrop{background:#0c25466e}dialog form{display:grid;gap:14px}dialog label{display:grid;gap:7px}dialog input,dialog select{width:100%}.form-actions{display:flex;justify-content:flex-end;gap:9px;margin-top:12px}.steps{display:grid;grid-template-columns:repeat(4,1fr);gap:10px}.step{padding:18px;background:#f4f7fb;border-radius:9px}.step b{display:block;margin:8px 0}.step small{color:#75879e}.split{display:grid;grid-template-columns:1fr 1fr;gap:20px}.rule{display:grid;grid-template-columns:1fr 1fr;gap:12px;align-items:center;margin:8px 0}.rule select{width:100%}.loading{padding:30px;color:#8493a6}@media(max-width:1000px){.cards{grid-template-columns:repeat(2,1fr)}.gridthemes{grid-template-columns:repeat(2,1fr)}.columns{grid-template-columns:1fr}.sidebar{flex-basis:190px}.top{flex-wrap:wrap}}@media(max-width:680px){.sidebar{display:none}.top,.content{padding:16px}.cards,.gridthemes,.split,.steps{grid-template-columns:1fr}.top-controls{flex-wrap:wrap}.heading{display:block}.head-actions{margin-top:17px}} diff --git a/app/static/factory-v2.js b/app/static/factory-v2.js new file mode 100644 index 0000000..d5164dd --- /dev/null +++ b/app/static/factory-v2.js @@ -0,0 +1,65 @@ +'use strict'; +const $=s=>document.querySelector(s), H=s=>String(s??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); +const state={sites:[],themes:[],site:null,tab:'overview',csrf:'',importRows:[],fields:[],mapping:{},kind:'product'}; +const toast=(s,error=false)=>{let el=$('#alert');el.textContent=s;el.className='on';el.style.background=error?'#ffe6e6':'#e3f7ed';el.style.color=error?'#b13445':'#176e50';setTimeout(()=>el.className='',5500)}; +async function api(url,method='GET',data=null){const opt={method,credentials:'same-origin',headers:{}};if(!['GET','HEAD'].includes(method))opt.headers['X-SiteForge-CSRF']=state.csrf; + if(data instanceof FormData)opt.body=data;else if(data!==null){opt.headers['Content-Type']='application/json';opt.body=JSON.stringify(data)} + const r=await fetch(url,opt);if(r.status===401){location.href='/login';throw Error('请登录')};let body=(r.headers.get('content-type')||'').includes('json')?await r.json():await r.text();if(!r.ok)throw Error(body.detail||body);return body;} +const siteUrl=path=>`/api/sites/${state.site.id}${path}`; +const factoryUrl=path=>`/api/factory/sites/${state.site.id}${path}`; +function selected(){if(!state.site){$('#panel').innerHTML='

创建第一个网站

选择主题后即可开始添加新内容,或导入旧网站的数据。

';return false}return true;} +function setTab(tab){state.tab=tab;$$('#navtabs button').forEach(b=>b.classList.toggle('active',b.dataset.tab===tab));render().catch(e=>toast(e.message,true))} +function $$(s){return [...document.querySelectorAll(s)]} +function setHeading(h,desc){$('#heading').textContent=h;$('#description').textContent=desc;$('#currentTab').textContent=h;} +async function init(){try{state.csrf=(await api('/api/auth/me')).csrf;await refresh();document.querySelectorAll('#navtabs button').forEach(btn=>btn.addEventListener('click',()=>setTab(btn.dataset.tab))); + $('#sitePicker').onchange=()=>{state.site=state.sites.find(s=>s.id===$('#sitePicker').value);render().catch(e=>toast(e.message,true))};$('#reload').onclick=refresh;$('#newSite').onclick=openCreate;$('#closeCreate').onclick=()=>$('#createDialog').close();$('#createForm').onsubmit=create; + $('#buildQuick').onclick=()=>buildNow();await render()}catch(e){toast('初始化失败:'+e.message,true)}} +async function refresh(){state.sites=await api('/api/sites');state.themes=await api('/api/factory/themes');let old=state.site?.id;state.site=state.sites.find(x=>x.id===old)||state.sites[0]||null; + $('#sitePicker').innerHTML=state.sites.map(s=>``).join('');if(state.site)$('#sitePicker').value=state.site.id; + $('#newTheme').innerHTML=state.themes.map(t=>``).join('');} +function openCreate(){$('#createDialog').showModal()} +async function create(e){e.preventDefault();try{let d=Object.fromEntries(new FormData(e.target));let result=await api('/api/sites','POST',d);state.site=result;await api(`/api/factory/sites/${result.id}/bootstrap`,'POST',{});$('#createDialog').close();toast('新网站已创建并生成基础页面结构');await refresh();await render()}catch(err){toast(err.message,true)}} +async function render(){$('#openEditor').href=state.site?`/editor/${state.site.id}`:'#';if(!selected())return; + const maps={overview:['网站总览','查看生产进度、页面数量和建站方式'],themes:['主题与版本','安装主题、切换主题、固定版本以及批量套版'],content:['内容与自动装配','导入 CSV/JSON、自动识别字段、预览映射并建立新内容'],migration:['迁移与 SEO 验收','核对旧站 URL、媒体资源和 SEO 差异'],build:['构建与发布','生成独立 HTML 站点、检测问题、下载与版本管理']};setHeading(...maps[state.tab]); + const fn={overview:overview,themes:themes,content:content,migration:migration,build:buildPanel}[state.tab];$('#panel').innerHTML='
加载中……
';await fn();} +async function overview(){let [entries,builds,source]=await Promise.all([api(siteUrl('/entries')),api(siteUrl('/builds')),api(factoryUrl('/theme'))]);let s=state.site; + $('#panel').innerHTML=`
当前站点${H(s.name)}${H(s.mode==='migration'?'旧站迁移':'新站搭建')} · ${H(s.theme)}
内容总数${entries.length}页面/产品/品牌/文章
构建版本${builds.length}静态网站历史记录
主题版本${H(source.version)}已固定版本可追踪
+

网站工厂生产流程

01网站结构首页、栏目页和导航
02内容自动装配导入新数据或旧内容
03设计编辑切换主题、可视化编辑
04静态站交付构建、验收、发布
+

网站页面

${entries.slice(0,25).map(x=>``).join('')}
页面/内容类型URL操作
${H(x.title)}${H(x.type)}${H(x.route)}设计页面 ↗

联系信息与表单服务

邮箱会出现在主题的联系页与页脚。外部表单地址为空时,不显示假提交按钮。

常用功能

当没有真实产品时,主题不会虚构业务数据;可先完善基础页面,再逐步导入。

`;} +async function bootstrap(){try{let r=await api(factoryUrl('/bootstrap'),'POST',{});toast(`新增 ${r.created.length} 个基础页面;不会覆盖原 URL`);await render()}catch(e){toast(e.message,true)}} +async function themes(){let cur=await api(factoryUrl('/theme'));const schemes={automotive:['#172840','#cfab79','MOVE BEYOND'],industrial:['#dcebf4','#14517d','BUILD FURTHER'],corporate:['#e7e9fb','#535bb4','CREATE IMPACT'],minimal:['#e9e5dc','#3d3b35','LESS IS MORE']}; + $('#panel').innerHTML=`

可安装、可版本化的主题包

当前主题:${H(cur.theme)} · v${H(cur.version)}
${state.themes.map(t=>{let id=t.id||t.theme_id,p=schemes[id]||['#e9eff8','#246c9e','YOUR BRAND'];return `
${H(p[2])}

${H(t.name)} v${H(t.version)}

${H(t.description)}

全部页面预览首页预览
`}).join('')}
+

安装新主题 ZIP

主题包由 manifest.json、theme.css、page-presets.json 组成。只能安装受控的页面组件和 CSS,不执行上传的 JavaScript 或模板逻辑。

批量重装配现有内容

会按页面类型给已有内容应用当前主题;保留现有 URL、SEO 和正文。选择「只补缺失」可避免覆盖已手工设计的区块。

`;} +async function chooseTheme(id,version){if(!confirm(`为当前网站选择主题 ${id} v${version}?将保留网站内容。`))return;try{await api(factoryUrl('/theme'),'PUT',{theme:id,version});await refresh();toast('主题切换完成。需要重新构建才能更新正式网站');await render()}catch(e){toast(e.message,true)}} +async function showVersions(id){let list=await api(`/api/factory/themes/${id}/versions`);$('#themeVersions').textContent='已安装版本:'+list.map(x=>x.version).join(' / ')} +async function checkTheme(id,version){try{const r=await api(`/api/factory/themes/${id}/compatibility?version=${encodeURIComponent(version)}`);$('#themeVersions').textContent=`${r.valid?'✓ 主题结构可用':'✗ 存在阻断'} · ${r.checked_page_types||0} 类页面 · ${[...(r.errors||[]),...(r.warnings||[])].join(';')||'无结构警告'}。正式交付前仍需实际浏览器验收。`;}catch(e){toast(e.message,true)}} +async function uploadTheme(){let f=$('#themePackage').files?.[0];if(!f)return toast('请选择主题 ZIP',true);let data=new FormData();data.append('file',f);try{let r=await api('/api/factory/themes/install','POST',data);toast(`已安装:${r.name} v${r.version}`);await refresh();await render()}catch(e){toast(e.message,true)}} +async function applyTheme(mode){if(mode==='overwrite'&&!confirm('覆盖已有页面设计(内容和 SEO 不变),继续吗?'))return;try{let r=await api(siteUrl('/apply-theme'),'POST',{mode});toast(`套版完成:更新 ${r.updated} 个页面`)}catch(e){toast(e.message,true)}} +async function content(){let entries=await api(siteUrl('/entries'));$('#panel').innerHTML=`

新内容 / 旧内容导入装配

上传 Excel、JSON 数组或 CSV;系统自动建议字段对应关系,例如「车型名称 → 标题」「品牌 → 产品品牌」。映射会经过预览与冲突检查。

本站内容统计

${['page','product','vehicle','brand','post','case','service'].map(t=>`
${H(({page:'页面',product:'产品',vehicle:'车型',brand:'品牌',post:'新闻',case:'案例',service:'服务'})[t])}${entries.filter(e=>e.type===t).length}
`).join('')}

其他导入方式(WordPress XML/REST、Excel、媒体 ZIP)可以在原版站点管理工作台使用,使用相同的数据存储。

打开完整内容管理 →
`;} +function parseCSV(text){let rows=[],row=[],cell='',quoted=false;for(let i=0;ix.trim()))rows.push(row);row=[];cell=''}else cell+=ch;}row.push(cell);if(row.some(x=>x.trim()))rows.push(row);let cols=rows.shift()?.map(s=>s.trim())||[];return rows.map(r=>Object.fromEntries(cols.map((c,i)=>[c,r[i]||''])))} +async function readData(){let f=$('#dataFile').files?.[0];if(!f)return toast('请选择 Excel、CSV 或 JSON 文件',true); + try{let data=new FormData();data.append('file',f);const parsed=await api(factoryUrl('/mapping/parse-file'),'POST',data);state.importRows=parsed.rows;state.fields=parsed.columns;state.mapping=parsed.suggested;drawMapping();} + catch(e){toast(e.message,true)}} +async function previewRelations(){try{const r=await api(factoryUrl('/relationships'));$('#relationReport').innerHTML=`
产品-品牌关联检查

已有品牌 ${r.brands.length} 个、分类 ${r.categories.length} 个;缺少独立品牌页 ${r.brand_pages_missing.length} 个

${H(r.brand_pages_missing.map(x=>x.brand).join(' / ')||'无')}

${r.issues.slice(0,10).map(x=>`

${H(x.route)} · ${H(x.message)}

`).join('')}
`;}catch(e){toast(e.message,true)}} +async function assembleRelations(){if(!confirm('依据真实产品品牌字段创建缺少的品牌页面?不会自动生成虚构文案。'))return;try{const r=await api(factoryUrl('/relationships/assemble'),'POST',{});toast(`已创建 ${r.count} 个品牌页面`);await previewRelations()}catch(e){toast(e.message,true)}} + +function drawMapping(){let fields=['title','route','summary','body','cover','brand','category','model','energy','range','gallery','specs','sku','price','seo_title','seo_description','canonical'];let labels={title:'标题(必填)',route:'旧站 URL',summary:'摘要',body:'详情正文',cover:'封面',brand:'品牌',category:'分类',seo_title:'SEO标题',seo_description:'SEO描述',canonical:'Canonical',model:'型号',energy:'动力形式',range:'续航',gallery:'图库(JSON数组或分号分隔)',specs:'技术参数',sku:'SKU',price:'价格'}; + $('#mappingUI').innerHTML=`

字段映射(已读取 ${state.importRows.length} 行)

${fields.map(t=>`
${H(labels[t])}
`).join('')}
`;} +function gather(){let rules={};$$('.mapfield').forEach(x=>{if(x.value)rules[x.dataset.target]=x.value});return {rows:state.importRows,rules,content_type:$('#mapKind').value}} +async function mappingPreview(){try{let r=await api(factoryUrl('/mapping/run'),'POST',{...gather(),dry_run:true});$('#mappingReport').innerHTML=`
预览:${r.mapped} 条可映射

错误 ${r.errors.length} 条,URL 冲突 ${r.conflicts.length} 条

${r.sample.map(x=>``).join('')}
标题新 URL内容类型
${H(x.title)}${H(x.route)}${H(x.type)}
${r.errors.length?'

'+H(JSON.stringify(r.errors.slice(0,6)))+'

':''}
`}catch(e){toast(e.message,true)}} +async function mappingCommit(){if(!confirm('确认将已映射的数据写入当前网站?'))return;try{let r=await api(factoryUrl('/mapping/run'),'POST',{...gather(),dry_run:false,on_conflict:'skip'});toast(`新增 ${r.created} 条,跳过 ${r.skipped} 条,错误 ${r.errors.length} 条`);await render()}catch(e){toast(e.message,true)}} +async function migration(){let [data,assets,builds]=await Promise.all([api(factoryUrl('/migration/readiness')),api(factoryUrl('/assets/inventory')),api(siteUrl('/builds'))]); + let audit=builds.length?await api(`/api/audit/sites/${state.site.id}/builds/${builds[0].id}/report`).catch(()=>null):null;const gate=data.gates||[];$('#panel').innerHTML=`
旧站 URL 基线${data.baseline.baseline_count}应保持有效的历史路径
缺失 URL${data.baseline.errors}发布阻断项
SEO 差异${data.baseline.warnings}需要人工核查
资源引用${assets.references}实际已识别的内容图片

迁移验收门禁

${gate.length?gate.map(g=>`
${g.level==='block'?'阻止发布':'需人工复核'}

${H(g.message)}

`).join(''):'

当前已检测范围内未发现阻断项。

'}
资源清单 JSON

提示:完整旧站 URL 基线应来自迁移前的原网站清单,不要用迁移后不完整的网站替代。

实际生成 HTML 与旧站对比

${audit?`

旧站 ${audit.baseline_count} 条 / 已匹配 ${audit.matched_pages||0} 条;阻断 ${audit.errors} 项;待复核 ${audit.unreviewed_warnings||0} 项

下载差异 CSV
${audit.issues.slice(0,40).map(i=>``).join('')}
问题路径操作
${H(i.field)} · ${H(i.message)}${H(i.route)}${i.severity==='warning'&&!i.reviewed?``:i.reviewed?'已复核':'必须修复'}
`:'

请先构建新站,再与旧站基线对比。

'}

前 12 项差异

${data.baseline.issues.slice(0,12).map(x=>`
${H(x.route)}${H(x.field)} · ${H(x.severity)}
`).join('')||'

没有基线差异。

'}
`;} +async function saveContactSettings(){try{await api(siteUrl('/contact-settings'),'PUT',{email:$('#configEmail').value,form_endpoint:$('#configForm').value,footer_text:$('#configFooter').value});toast('网站联系配置已保存');await refresh()}catch(e){toast(e.message,true)}} +async function approveAuditWarning(bid,issue){const reason=prompt('填写核查原因及处理依据(至少10字,审核后对本构建生效):','');if(!reason)return;try{await api(`/api/audit/sites/${state.site.id}/builds/${bid}/review-warning`,'POST',{issue_id:issue,reason});toast('已记录审查决定');await render()}catch(e){toast(e.message,true)}} +async function uploadBaseline(){const file=$('#baselineFile')?.files?.[0];if(!file)return toast('请选择原站清单文件',true);const data=new FormData();data.append('file',file); + try{let r=await api(`/api/audit/sites/${state.site.id}/import-baseline`,'POST',data);if(r.errors?.length){$('#baselineImportResult').textContent=JSON.stringify(r.errors.slice(0,15));toast('源站清单存在错误,未写入基线',true);return}toast(`已导入 ${r.imported} 个旧站 URL`);await render()}catch(e){toast(e.message,true)}} +async function openLatestAudit(){try{let builds=await api(siteUrl('/builds'));if(!builds.length)return toast('请先构建网站',true);window.open(`/api/audit/sites/${state.site.id}/builds/${builds[0].id}/report`,'_blank','noopener')}catch(e){toast(e.message,true)}} +async function snapshot(){if(!confirm('仅适用于尚未迁移的测试网站。真实迁移应导入旧站原始 URL 清单!继续吗?'))return;try{let r=await api(`/api/enterprise/sites/${state.site.id}/baseline/snapshot`,'POST',{});toast('已建立 '+r.captured+' 条当前内容基线');await render()}catch(e){toast(e.message,true)}} +async function buildPanel(){let [builds,ready,releaseRequests]=await Promise.all([api(siteUrl('/builds')),api(factoryUrl('/migration/readiness')),api(factoryUrl('/release-requests'))]);$('#panel').innerHTML=`

静态构建版本

${ready.can_publish?'可进入发布审查':'发布前有阻断项'}
${builds.slice(0,15).map(b=>``).join('')}
版本页面数生成时间交付
${H(b.id)}${b.total_pages}${H(b.created_at)}预览 · 下载 · ·

交付说明

静态网站包含 HTML、CSS、JavaScript、独立搜索索引、站点地图和 Nginx 重定向示例配置。

通过实际发布前,需要校验 SEO 基线、静态资源和功能;当前本地发布仅适用于事先配置的发布目录,公网 DNS/CDN 连接器仍需另行开发。

${ready.gates.map(g=>`
${H(g.level)}

${H(g.message)}

`).join('')}

发布审批记录

${releaseRequests.slice(0,8).map(r=>`
${H(r.build_id)}${H(r.requested_by)} · ${H(r.state)}
${r.state==='pending'?` `:''}
`).join('')||'

暂无发布审批记录。

'}
`;} +async function requestRelease(build){try{let r=await api(factoryUrl('/release-requests'),'POST',{build_id:build});toast('审批申请已提交:'+r.id);await render()}catch(e){toast(e.message,true)}} +async function reviewRelease(id,status){try{let note=prompt('请输入审批说明(可留空)','')||'';await api(factoryUrl('/release-requests/'+id+'/review'),'POST',{state:status,note});toast('审批状态已更新');await render()}catch(e){toast(e.message,true)}} +async function publishRelease(build){if(!confirm('确认将此版本发布到预先配置的本地持久化站点目录?'))return;try{let r=await api(siteUrl('/builds/'+build+'/publish-local'),'POST',{});toast('已发布:'+r.release);await render()}catch(e){toast(e.message,true)}} +async function buildNow(){if(!state.site)return;try{toast('构建中……');let r=await api(siteUrl('/builds'),'POST',{mirror_images:false});toast(`构建成功,${r.pages} 个页面`);await render();window.open(`/preview/${state.site.id}/${r.version}/`,'_blank','noopener')}catch(e){toast(e.message,true)}} +window.addEventListener('DOMContentLoaded',init); +window.requestRelease=requestRelease;window.reviewRelease=reviewRelease;window.publishRelease=publishRelease;window.bootstrap=bootstrap;window.chooseTheme=chooseTheme;window.checkTheme=checkTheme;window.showVersions=showVersions;window.uploadTheme=uploadTheme;window.applyTheme=applyTheme;window.readData=readData;window.uploadBaseline=uploadBaseline;window.approveAuditWarning=approveAuditWarning;window.saveContactSettings=saveContactSettings;window.openLatestAudit=openLatestAudit;window.previewRelations=previewRelations;window.assembleRelations=assembleRelations;window.mappingPreview=mappingPreview;window.mappingCommit=mappingCommit;window.snapshot=snapshot;window.buildNow=buildNow;window.setTab=setTab;window.openCreate=openCreate; diff --git a/app/static/factory.css b/app/static/factory.css new file mode 100644 index 0000000..154ffb3 --- /dev/null +++ b/app/static/factory.css @@ -0,0 +1,27 @@ +/* SiteForge trusted global shell. Site themes remain visually independent. */ +:root{--sf-shell:#12243d;--sf-shell-accent:#dcae71;--sf-shell-soft:#edf2f8;--sf-shell-fg:#173049} +html{scroll-behavior:smooth}body{overflow-x:hidden}body>.header,body>.utility,body>.top,body>header,body>footer{display:none!important} +.sf-theme-industrial{--sf-shell:#114e79;--sf-shell-accent:#92d5ed;--sf-shell-soft:#ebf6fa;--sf-shell-fg:#0c4267} +.sf-theme-corporate{--sf-shell:#232d62;--sf-shell-accent:#b8bdff;--sf-shell-soft:#f3f3fa;--sf-shell-fg:#333468} +.sf-theme-minimal{--sf-shell:#1e1e1c;--sf-shell-accent:#ded6c5;--sf-shell-soft:#f6f4ef;--sf-shell-fg:#252520} +.sf-site-head{position:relative;z-index:25;background:var(--sf-shell);color:#fff;font-family:system-ui,-apple-system,sans-serif} +.sf-head-inner{max-width:1240px;margin:0 auto;min-height:78px;padding:0 28px;display:flex;align-items:center;gap:28px} +.sf-logo{display:flex;align-items:center;gap:11px;font-size:18px;text-decoration:none!important;color:#fff!important;white-space:nowrap} +.sf-logo-mark{width:36px;height:36px;display:grid;place-items:center;background:var(--sf-shell-accent);color:var(--sf-shell);font-size:22px;font-weight:900;border-radius:9px} +.sf-primary-links{display:flex;gap:23px;margin-left:auto;align-items:center} +.sf-primary-links a,.sf-mega a{color:#fff!important;text-decoration:none!important;font-size:13px;font-weight:650;white-space:nowrap} +.sf-primary-links a:hover{color:var(--sf-shell-accent)!important} +.sf-head-cta{font-size:13px;font-weight:750;text-decoration:none;background:var(--sf-shell-accent);color:var(--sf-shell)!important;padding:13px 18px;border-radius:4px;white-space:nowrap} +.sf-menu-toggle{display:none;margin-left:auto;color:white;border:1px solid #ffffff55;background:none;border-radius:4px;padding:8px 12px;font-size:19px;cursor:pointer} +.sf-mega{display:flex;gap:18px;max-width:1240px;margin:auto;padding:0 28px 9px;overflow-x:auto} +.sf-mega a{display:block;min-width:120px;border-top:1px solid #ffffff2e;padding:12px 8px}.sf-mega small{display:block;color:#ffffff99;max-width:160px;overflow:hidden;text-overflow:ellipsis;font-size:10px;margin-top:6px} +.sf-site-foot{background:var(--sf-shell);color:white!important;font:14px/1.7 system-ui,-apple-system,sans-serif;padding:72px 28px 26px} +.sf-foot-grid{max-width:1200px;margin:auto;display:grid;grid-template-columns:2fr 1fr 1fr;gap:70px}.sf-foot-grid h2{font-size:25px;color:white;margin:0 0 17px}.sf-foot-grid h4{font-size:13px;color:var(--sf-shell-accent);letter-spacing:.07em;margin:0 0 17px}.sf-foot-grid p{color:#ffffffae;max-width:380px}.sf-foot-grid a{display:block;width:fit-content;color:#fff!important;text-decoration:none;margin-bottom:8px}.sf-foot-lower{border-top:1px solid #ffffff33;max-width:1200px;margin:45px auto 0;padding-top:20px;display:flex;justify-content:space-between;color:#ffffff9e;font-size:12px} +.sf-filters{display:flex;align-items:center;flex-wrap:wrap;gap:12px;margin:0 0 30px}.sf-filters input,.sf-filters select{padding:11px 13px;border-radius:6px;border:1px solid #cfd9e4;background:#fff;color:#263a51;max-width:280px}.sf-filter-empty{padding:22px;color:#778598} +.sf-search-page{min-height:76vh;max-width:1060px;padding:90px 24px;margin:auto;font-family:system-ui,sans-serif;color:var(--sf-shell-fg)}.sf-search-page h1{font-size:clamp(35px,5vw,62px);margin:20px 0}.sf-search-page>label{display:block;margin:18px 0 8px}.sf-search-page input{display:block;width:100%;padding:17px;font-size:18px;border:1px solid #b7c6d7;border-radius:8px}.sf-search-page #sf-search-count{margin:22px 0;color:#607589}.sf-search-result{padding:22px 0;border-bottom:1px solid #dae1ea}.sf-search-result a{color:var(--sf-shell-fg);font-size:21px;font-weight:750;text-decoration:none}.sf-search-result small{display:block;color:#7992a7}.sf-search-result p{margin:7px 0;color:#657487} +@media(max-width:1050px){.sf-head-inner{gap:13px}.sf-primary-links{gap:12px}.sf-primary-links a{font-size:12px}.sf-head-cta{padding:10px}} +@media(max-width:850px){.sf-head-inner{min-height:68px}.sf-menu-toggle{display:block}.sf-head-cta{display:none}.sf-primary-links{display:none;position:absolute;top:68px;left:0;right:0;margin:0;background:var(--sf-shell);padding:20px 28px;flex-direction:column;align-items:stretch;box-shadow:0 20px 24px #0002}.sf-primary-links.is-open{display:flex}.sf-mega{display:none}.sf-foot-grid{grid-template-columns:1fr 1fr;gap:35px}} +@media(max-width:560px){.sf-foot-grid{grid-template-columns:1fr}.sf-foot-lower{display:block}.sf-site-foot{padding-top:48px}.sf-head-inner{padding:0 16px}} + +.sf-page-intro{padding:80px 28px;background:var(--sf-shell-soft);color:var(--sf-shell-fg);font-family:system-ui,sans-serif}.sf-page-intro>div{max-width:1200px;margin:auto}.sf-page-intro p{font-size:11px;font-weight:850;letter-spacing:.19em;text-transform:uppercase;color:var(--sf-shell-fg);opacity:.58}.sf-page-intro h1{font-size:clamp(38px,5vw,67px);letter-spacing:-.04em;line-height:1.08;margin:13px 0}.sf-page-intro span{font-size:16px;opacity:.72} +.sf-item[hidden]{display:none!important} diff --git a/app/static/pro-base.css b/app/static/pro-base.css new file mode 100644 index 0000000..c2c4f61 --- /dev/null +++ b/app/static/pro-base.css @@ -0,0 +1,21 @@ +/* SiteForge V3 professional theme primitives. Each commercial theme adds its own art direction. */ +:root{--pro-text:#19283a;--pro-muted:#687587;--pro-border:#dfe5ea;--pro-background:#fff;--pro-surface:#f5f7fa;--pro-accent:#365cb7;--pro-hero:#15263d;--pro-hero-fg:#fff;--pro-radius:12px;--pro-headings:system-ui,-apple-system,sans-serif;--pro-font:system-ui,-apple-system,sans-serif;--pro-wide:1270px} +*,*:before,*:after{box-sizing:border-box}html{scroll-behavior:smooth}body{margin:0;background:var(--pro-background);color:var(--pro-text);font-family:var(--pro-font);-webkit-font-smoothing:antialiased;overflow-x:hidden}button,input,select{font:inherit}button,a{touch-action:manipulation}button{cursor:pointer}a{text-decoration:none;color:inherit}a:hover{color:var(--pro-accent)}img{max-width:100%;display:block}h1,h2,h3,h4,p{margin-top:0}h1,h2,h3{font-family:var(--pro-headings);line-height:1.09}h1{letter-spacing:-.052em}h2{letter-spacing:-.042em}h3{letter-spacing:-.028em}p{line-height:1.72}.pro-wrap{max-width:var(--pro-wide);margin:0 auto;padding-left:32px;padding-right:32px}.pro-skip{position:absolute;left:-9999px;z-index:90;background:#fff;color:#000;padding:12px}.pro-skip:focus{left:10px;top:10px}.pro-eyebrow{font-weight:750;font-size:11px;letter-spacing:.17em;text-transform:uppercase;display:block;line-height:1.6;color:var(--pro-accent)}.pro-muted{opacity:.7}.pro-utility{min-height:30px;background:var(--pro-hero);color:#c5d2db;font-size:10px;letter-spacing:.16em}.pro-utility .pro-wrap{display:flex;justify-content:space-between;padding-block:8px}.pro-utility i{font-style:normal;color:var(--pro-accent)}.pro-header{background:var(--pro-background);color:var(--pro-text);border-bottom:1px solid var(--pro-border);position:relative;z-index:35}.pro-head-inner{min-height:84px;display:flex;align-items:center;gap:28px}.pro-logo{display:flex;align-items:center;gap:12px;min-width:170px}.pro-logo-symbol{font-size:32px;line-height:1;font-weight:800;letter-spacing:-.07em;color:var(--pro-accent)}.pro-logo-name{max-width:220px;font-weight:820;font-size:17px;line-height:1.1;letter-spacing:-.035em}.pro-logo-name small{display:block;font-weight:650;font-size:9px;letter-spacing:.19em;margin-top:7px;color:var(--pro-muted)}.pro-navigation{display:flex;align-items:center;gap:24px;margin-left:auto}.pro-navigation a{font-size:13px;font-weight:650;white-space:nowrap;transition:opacity .2s}.pro-navigation a:hover{opacity:.6}.pro-header-cta,.pro-button{display:inline-flex;align-items:center;justify-content:center;gap:22px;background:var(--pro-accent);color:#fff!important;border-radius:var(--pro-radius);font-size:13px;letter-spacing:.01em;font-weight:750;padding:16px 24px;white-space:nowrap;transition:transform .2s,background .2s}.pro-header-cta:hover,.pro-button:hover{transform:translateY(-2px)}.pro-mobile-toggle{display:none}.pro-mega{display:flex;align-items:center;gap:22px;min-height:37px;border-top:1px solid var(--pro-border);padding-block:10px;font-size:11px}.pro-mega>span{font-weight:750;color:var(--pro-muted);white-space:nowrap}.pro-mega>div{display:flex;gap:16px;overflow:auto;scrollbar-width:none}.pro-mega a{white-space:nowrap;font-weight:650}.pro-search-link{font-size:19px!important}.pro-home-hero{position:relative;overflow:hidden}.pro-home-grid{display:grid;grid-template-columns:1.02fr .98fr;align-items:center;gap:5%;min-height:580px;padding-top:85px;padding-bottom:85px}.pro-hero-copy{position:relative;z-index:2}.pro-hero-copy h1{font-size:clamp(48px,6.2vw,91px);line-height:1.03;margin:20px 0 25px;max-width:730px}.pro-hero-copy h1 em{font-style:normal;display:block;color:var(--pro-accent)}.pro-hero-copy p{font-size:17px;max-width:495px;color:var(--pro-muted)}.pro-hero-actions{display:flex;align-items:center;flex-wrap:wrap;gap:25px;margin-top:34px}.pro-outline-button{font-weight:700;border-bottom:1px solid currentColor;padding:12px 0}.pro-hero-art{width:100%;height:435px;position:relative;overflow:hidden;border-radius:var(--pro-radius)}.pro-hero-art>img{width:100%;height:100%;object-fit:cover}.pro-hero-bottom{display:flex;justify-content:space-between;font-size:10px;letter-spacing:.18em;padding-block:22px;border-top:1px solid #ffffff32}.pro-section{padding:106px 0}.pro-section-head{display:flex;align-items:end;justify-content:space-between;gap:25px;margin-bottom:34px}.pro-section-head h2{font-size:clamp(36px,4vw,57px);margin:12px 0 0}.pro-text-link{font-size:13px;font-weight:750;display:inline-flex;gap:18px;align-items:center;border-bottom:1px solid;padding-bottom:7px;color:var(--pro-accent)}.pro-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:22px}.pro-card{display:block;min-width:0;background:var(--pro-background);color:var(--pro-text);border:1px solid var(--pro-border);overflow:hidden;border-radius:var(--pro-radius);transition:transform .25s,box-shadow .25s}.pro-card:hover{transform:translateY(-6px);box-shadow:0 24px 45px #0f25431b}.pro-card-cover{width:100%;height:244px;overflow:hidden;background:var(--pro-surface)}.pro-photo{width:100%;height:100%;object-fit:cover;transition:transform .5s}.pro-card:hover .pro-photo{transform:scale(1.045)}.pro-art{width:100%;height:100%;overflow:hidden;display:grid;place-items:center;position:relative;background:linear-gradient(130deg,#d8e1ed,#f3f6f9)}.pro-art-ring{width:55%;aspect-ratio:1;border:28px solid #ffffff5f;border-radius:50%;box-shadow:0 0 0 42px #ffffff30;transform:rotate(-25deg)}.pro-art-center{position:absolute;font-family:var(--pro-headings);font-weight:800;font-size:76px;letter-spacing:-.08em;color:#45647d90}.pro-art-label{position:absolute;bottom:16px;left:18px;letter-spacing:.16em;font-size:9px;color:#435773}.pro-card-body{padding:25px 25px 28px}.pro-card-meta{display:flex;justify-content:space-between;gap:20px;font-size:10px;font-weight:750;letter-spacing:.1em;text-transform:uppercase;color:var(--pro-accent)}.pro-card h3{font-size:23px;margin:14px 0 9px}.pro-card p{font-size:13px;color:var(--pro-muted);margin-bottom:24px;min-height:44px}.pro-card-link{font-size:12px;font-weight:800;display:flex;justify-content:space-between;color:var(--pro-text);padding-top:14px;border-top:1px solid var(--pro-border)}.pro-empty{grid-column:1/-1;padding:45px;border:1px dashed var(--pro-border);background:var(--pro-surface);color:var(--pro-muted)}.pro-statement{background:var(--pro-surface)}.pro-statement-layout{display:grid;grid-template-columns:1fr 2fr;gap:60px}.pro-statement h2{font-size:clamp(36px,4.5vw,63px);max-width:760px}.pro-statement p{max-width:520px;color:var(--pro-muted)}.pro-brand-pills{display:flex;flex-wrap:wrap;gap:12px}.pro-brand-pills a{padding:18px 28px;border:1px solid var(--pro-border);border-radius:var(--pro-radius);font-weight:750}.pro-brand-pills a:hover{border-color:var(--pro-accent)}.pro-inner-hero{background:var(--pro-surface);padding:92px 0 65px;position:relative;overflow:hidden}.pro-inner-hero h1{max-width:870px;font-size:clamp(43px,6vw,85px);margin:18px 0}.pro-inner-hero p{max-width:650px;color:var(--pro-muted);font-size:17px}.pro-breadcrumb{font-size:12px;color:var(--pro-muted);margin-top:25px;display:flex;gap:12px;align-items:center;flex-wrap:wrap}.pro-breadcrumb strong{color:var(--pro-text)}.pro-list-section{padding:65px 0 110px}.pro-filter-panel{display:grid;grid-template-columns:minmax(220px,2fr) 1fr 1fr auto;align-items:end;gap:14px;margin-bottom:35px}.pro-filter-panel label>span{display:block;font-size:11px;margin-bottom:8px;color:var(--pro-muted);font-weight:750}.pro-filter-panel input,.pro-filter-panel select{display:block;width:100%;padding:14px 15px;border-radius:var(--pro-radius);border:1px solid var(--pro-border);background:var(--pro-background);color:var(--pro-text);outline-offset:2px}.pro-filter-count{font-size:12px;padding-bottom:17px;color:var(--pro-muted)}.pro-filter-scope .pro-card[hidden]{display:none}.pro-product-layout{padding-top:38px}.pro-product-layout .pro-breadcrumb{margin-bottom:38px}.pro-product-hero{display:grid;grid-template-columns:1.18fr .82fr;gap:6%;align-items:center}.pro-product-media{height:460px;overflow:hidden;background:var(--pro-surface);border-radius:var(--pro-radius)}.pro-product-media .pro-photo{height:100%}.pro-product-summary h1{font-size:clamp(45px,5vw,72px);margin:16px 0}.pro-product-summary p{font-size:16px;color:var(--pro-muted)}.pro-product-brand{font-weight:700;color:var(--pro-text)!important}.pro-detail-layout{border-top:1px solid var(--pro-border);display:grid;grid-template-columns:1fr 1fr;gap:8%;margin-top:80px;padding-top:55px}.pro-detail-layout h2{font-size:32px;margin-top:13px}.pro-specs{margin-top:25px}.pro-specs>div{display:grid;grid-template-columns:1fr 1fr;gap:22px;padding:16px 0;border-bottom:1px solid var(--pro-border)}.pro-specs span{color:var(--pro-muted)}.pro-rich{line-height:1.85;font-size:16px;overflow-wrap:anywhere}.pro-rich img{border-radius:var(--pro-radius);margin:25px 0}.pro-rich a{text-decoration:underline}.pro-rich h2{margin-top:30px}.pro-lead{font-size:20px;color:var(--pro-muted)}.pro-brand-hero-grid{display:grid;grid-template-columns:1.1fr .9fr;gap:7%;align-items:center}.pro-brand-media{height:300px;overflow:hidden;border-radius:var(--pro-radius)}.pro-article-hero{text-align:center;padding:110px 0 75px;background:var(--pro-surface)}.pro-article-width{max-width:900px}.pro-article-hero h1{font-size:clamp(40px,5vw,75px);margin:15px 0 25px}.pro-article-hero p{color:var(--pro-muted);font-size:18px}.pro-article-hero time{font-size:12px;color:var(--pro-muted)}.pro-article-content{max-width:930px;padding-top:70px;padding-bottom:90px}.pro-article-cover{width:100%;max-height:530px;object-fit:cover;margin-bottom:40px;border-radius:var(--pro-radius)}.pro-article-content>.pro-text-link{margin-top:35px}.pro-case-detail{max-width:1020px}.pro-case-detail>img{width:100%;max-height:620px;object-fit:cover}.pro-case-meta{display:flex;gap:35px;margin:35px 0;color:var(--pro-muted);font-size:13px}.pro-case-meta strong{display:block;color:var(--pro-text);font-size:18px;margin-top:7px}.pro-info-grid{display:grid;grid-template-columns:.8fr 1.2fr;gap:12%}.pro-info-grid h2{font-size:clamp(31px,4.5vw,54px);margin-top:15px}.pro-contact-email{font-size:20px;color:var(--pro-accent);font-weight:750;word-break:break-word}.pro-info-photo{max-height:450px;object-fit:cover;margin-top:35px;border-radius:var(--pro-radius)}.pro-custom-content .sf-section{background:transparent}.pro-custom-content .sf-section:nth-of-type(even){background:var(--pro-surface)}.pro-end-cta{padding:76px 0;background:var(--pro-hero);color:#fff}.pro-end-cta .pro-wrap{display:flex;align-items:center;justify-content:space-between;gap:40px}.pro-end-cta h2{font-size:clamp(32px,4vw,53px);margin:13px 0}.pro-end-cta p{color:#c5c9d1}.pro-end-cta a{background:var(--pro-accent);color:#fff;font-weight:800;padding:19px 30px;border-radius:var(--pro-radius);white-space:nowrap}.pro-404{text-align:center;padding-block:125px}.pro-404>span{font-size:12px;letter-spacing:.3em}.pro-404 h1{font-size:clamp(150px,20vw,320px);margin:10px 0;letter-spacing:-.08em}.pro-404 p{font-size:18px;color:var(--pro-muted)}.pro-404 a{margin-top:22px}.pro-footer{background:#15212d;color:#fff;padding:75px 0 22px}.pro-footer-top{display:grid;grid-template-columns:1.45fr 1fr 1fr;gap:55px}.pro-footer h2{font-size:32px;margin:12px 0}.pro-footer p{max-width:350px;font-size:13px;color:#afbbc8}.pro-footer .pro-eyebrow{color:#b6c3d2}.pro-footer-links ul{margin:0;padding:0;list-style:none;display:grid;grid-template-columns:1fr 1fr;gap:15px}.pro-footer-links a{font-size:13px;color:#d6e0e8}.pro-footer-bottom{border-top:1px solid #ffffff29;margin-top:60px;padding-top:20px;font-size:10px;letter-spacing:.12em;color:#adb9c3;display:flex;justify-content:space-between}.pro-anchor{display:block;height:0}.pro-category-tiles{display:grid;grid-template-columns:repeat(3,1fr);gap:10px}.pro-category-tiles a{padding:35px 28px;min-height:155px;background:var(--pro-surface);display:flex;flex-direction:column;justify-content:space-between}.pro-category-tiles span{font-size:11px;color:var(--pro-muted)}.pro-category-tiles strong{font-size:24px}.pro-category-tiles b{align-self:end}.sf-site-head .sf-head-inner,.sf-site-head .sf-primary-links{display:none} +@media(max-width:1100px){.pro-head-inner{gap:17px}.pro-navigation{gap:14px}.pro-navigation a{font-size:12px}.pro-home-grid{min-height:500px}.pro-hero-copy h1{font-size:60px}.pro-hero-art{height:350px}} +@media(max-width:860px){.pro-wrap{padding-left:22px;padding-right:22px}.pro-utility{display:none}.pro-head-inner{min-height:70px;flex-wrap:wrap;gap:12px}.pro-mobile-toggle{display:block;margin-left:auto;border:1px solid var(--pro-border);padding:8px 12px;background:transparent;color:var(--pro-text);border-radius:5px}.pro-navigation{display:none;order:10;width:100%;flex-direction:column;align-items:stretch;gap:0;padding-bottom:12px}.pro-navigation.is-open{display:flex}.pro-navigation a{padding:11px 2px;border-bottom:1px solid var(--pro-border)}.pro-header-cta{display:none}.pro-mega{display:none}.pro-home-grid{grid-template-columns:1fr;gap:35px;padding-top:60px;padding-bottom:60px}.pro-hero-art{height:350px}.pro-hero-copy h1{font-size:clamp(45px,9vw,73px)}.pro-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.pro-product-hero,.pro-detail-layout,.pro-info-grid,.pro-brand-hero-grid{grid-template-columns:1fr;gap:35px}.pro-product-media{height:380px}.pro-filter-panel{grid-template-columns:1fr 1fr}.pro-filter-panel label:first-child{grid-column:1/-1}.pro-end-cta .pro-wrap{align-items:start;flex-direction:column}.pro-section{padding:72px 0}.pro-footer-top{grid-template-columns:1fr 1fr}.pro-footer-top>div:first-child{grid-column:1/-1}.pro-category-tiles{grid-template-columns:repeat(2,1fr)}} +@media(max-width:560px){.pro-wrap{padding-left:18px;padding-right:18px}.pro-logo-name{font-size:15px}.pro-hero-copy h1{font-size:43px}.pro-hero-art{height:255px}.pro-hero-bottom{display:none}.pro-home-grid{min-height:0}.pro-grid{grid-template-columns:1fr}.pro-section-head{align-items:start;flex-direction:column}.pro-section-head h2{font-size:37px}.pro-card-cover{height:230px}.pro-filter-panel{grid-template-columns:1fr}.pro-filter-panel label:first-child{grid-column:auto}.pro-filter-count{padding-bottom:4px}.pro-statement-layout,.pro-footer-top{grid-template-columns:1fr;gap:25px}.pro-footer-top>div:first-child{grid-column:auto}.pro-footer-bottom{flex-direction:column;gap:8px}.pro-inner-hero{padding:60px 0}.pro-product-media{height:270px}.pro-product-summary h1{font-size:43px}.pro-category-tiles{grid-template-columns:1fr}.pro-end-cta h2{font-size:38px}.pro-minimal-meta{display:block!important}} + +/* V2 compatibility stylesheet hides direct headers/footers; full themes restore their own. */ +body>header.sf-site-head.pro-header,body>footer.sf-site-foot.pro-footer{display:block!important} + +/* Accessible desktop mega-navigation and mobile disclosure */ +.pro-catalog-toggle{font-size:12px;font-weight:750;border:1px solid var(--pro-border);background:transparent;color:inherit;border-radius:var(--pro-radius);padding:10px 14px;white-space:nowrap} +.pro-mega[hidden]{display:none!important}.pro-mega.is-open{display:flex}.pro-mega>div{padding-block:3px} +@media(max-width:1060px){.pro-catalog-toggle{display:none}} +@media(max-width:860px){.pro-mega.is-open{display:block!important}.pro-mega.is-open>div{display:flex;flex-wrap:wrap;gap:14px;padding:12px 0}.pro-mega>span{white-space:normal}} + +/* V4: responsive nested navigation, gallery and opt-in inquiry integration */ +.pro-nav-group{position:relative;display:flex;align-items:center;gap:3px}.pro-nav-disclose{border:0;background:none;color:inherit;font-size:12px;padding:8px}.pro-nav-children[hidden]{display:none!important}.pro-nav-children{position:absolute;left:0;top:100%;z-index:45;min-width:230px;max-width:360px;padding:10px;background:var(--pro-background);border:1px solid var(--pro-border);box-shadow:0 18px 35px #0002;border-radius:var(--pro-radius);display:flex;flex-direction:column}.pro-nav-children a{padding:10px 12px;white-space:nowrap}.pro-nav-children .pro-nav-leaf{padding-left:28px;color:var(--pro-muted)} +.pro-product-gallery{min-width:0}.pro-gallery-thumbs{display:flex;gap:8px;margin-top:12px;overflow:auto;scrollbar-width:thin;padding-bottom:5px}.pro-gallery-thumbs button{flex:0 0 86px;height:67px;border:2px solid transparent;padding:3px;border-radius:8px;background:var(--pro-surface)}.pro-gallery-thumbs button:focus-visible,.pro-gallery-thumbs button[aria-pressed=true]{border-color:var(--pro-accent)}.pro-gallery-thumbs img{width:100%;height:100%;object-fit:cover;border-radius:4px}.pro-gallery-lightbox{position:fixed;inset:0;background:rgba(10,16,30,.92);z-index:1000;display:grid;place-items:center;padding:6vw}.pro-gallery-lightbox[hidden]{display:none}.pro-gallery-lightbox img{max-width:92vw;max-height:80vh;object-fit:contain}.pro-gallery-lightbox button{position:absolute;right:25px;top:25px;border:1px solid #fff8;background:#222;color:#fff;padding:10px 15px;border-radius:5px} +.pro-inquiry-wrap{display:grid;grid-template-columns:1fr 1fr;gap:70px}.pro-inquiry{display:grid;gap:17px}.pro-inquiry label{display:grid;gap:7px;font-weight:650}.pro-inquiry input,.pro-inquiry textarea{padding:13px;border:1px solid var(--pro-border);border-radius:var(--pro-radius);background:var(--pro-background);color:var(--pro-text);width:100%}.pro-inquiry textarea{min-height:140px}.pro-inquiry .pro-honeypot{position:absolute;left:-10000px;width:1px;height:1px}.pro-inquiry p{font-size:12px;color:var(--pro-muted)} +@media(max-width:860px){.pro-nav-group{display:block}.pro-nav-children{position:static;max-width:none;width:100%;box-shadow:none;border:0;border-left:2px solid var(--pro-accent);padding-left:8px}.pro-nav-group>a{display:inline-block}.pro-inquiry-wrap{grid-template-columns:1fr;gap:20px}} diff --git a/app/static/pro.js b/app/static/pro.js new file mode 100644 index 0000000..29312a1 --- /dev/null +++ b/app/static/pro.js @@ -0,0 +1,26 @@ +'use strict';(()=>{ + const menu=document.querySelector('[data-pro-menu]'); const nav=document.querySelector('#pro-navigation'); + if(menu&&nav){menu.addEventListener('click',()=>{const open=menu.getAttribute('aria-expanded')!=='true';menu.setAttribute('aria-expanded',String(open));nav.classList.toggle('is-open',open);menu.setAttribute('aria-label',open?'关闭导航':'打开导航');});} + const megaButton=document.querySelector('[data-pro-catalog]'), mega=document.querySelector('[data-pro-mega]'); + if(megaButton&&mega){function setMega(open){mega.hidden=!open;mega.classList.toggle('is-open',open);megaButton.setAttribute('aria-expanded',String(open));} + megaButton.addEventListener('click',()=>setMega(mega.hidden)); + document.addEventListener('keydown',e=>{if(e.key==='Escape')setMega(false);}); + document.addEventListener('click',e=>{if(!mega.hidden&&!mega.contains(e.target)&&!megaButton.contains(e.target))setMega(false);}); + } + document.querySelectorAll('.pro-nav-disclose').forEach(button=>{button.addEventListener('click',()=>{let panel=button.parentElement.querySelector('.pro-nav-children');let visible=button.getAttribute('aria-expanded')==='true';button.setAttribute('aria-expanded',String(!visible));panel.hidden=visible;});}); + document.querySelectorAll('.pro-gallery-thumbs').forEach(container=>{const main=container.parentElement.querySelector('.pro-product-media img');const buttons=[...container.querySelectorAll('[data-gallery-image]')]; + buttons.forEach(b=>{b.addEventListener('click',()=>{buttons.forEach(x=>x.setAttribute('aria-pressed',String(x===b)));if(main){main.src=b.dataset.galleryImage;main.alt=b.querySelector('img')?.alt||'';}});}); + if(!main)return;main.tabIndex=0;main.style.cursor='zoom-in';const open=()=>{const pane=document.createElement('div');pane.className='pro-gallery-lightbox';pane.setAttribute('role','dialog');pane.setAttribute('aria-label','产品大图预览');let img=document.createElement('img');img.src=main.src;img.alt=main.alt;const close=document.createElement('button');close.type='button';close.textContent='关闭 ×';pane.append(img,close);document.body.append(pane);const done=()=>{pane.remove();document.removeEventListener('keydown',esc);main.focus();};const esc=e=>{if(e.key==='Escape')done();};document.addEventListener('keydown',esc);close.addEventListener('click',done);pane.addEventListener('click',e=>{if(e.target===pane)done();});close.focus();};main.addEventListener('click',open);main.addEventListener('keydown',e=>{if(e.key==='Enter')open();}); + }); + document.querySelectorAll('.pro-filter-scope').forEach(scope=>{ + const panel=scope.previousElementSibling;if(!panel||!panel.classList.contains('pro-filter-panel'))return; + const cards=Array.from(scope.querySelectorAll('.pro-card')); + const blank=document.createElement('div');blank.className='pro-filter-empty';blank.hidden=true;blank.textContent='没有找到符合条件的内容,请调整关键词或筛选条件。';scope.append(blank); + const q=panel.querySelector('[data-pro-filter-search]'),b=panel.querySelector('[data-pro-filter-brand]'),c=panel.querySelector('[data-pro-filter-category]'),count=panel.querySelector('[data-pro-filter-count]'); + function options(sel,key){const vals=[...new Set(cards.map(x=>x.dataset[key]||'').filter(Boolean))].sort();vals.forEach(x=>sel.add(new Option(x,x)));if(!vals.length)sel.closest('label').hidden=true;} + options(b,'brand');options(c,'category'); + function run(){const str=q.value.trim().toLocaleLowerCase();let visible=0; + cards.forEach(el=>{const hit=el.textContent.toLocaleLowerCase().includes(str)&&(!b.value||el.dataset.brand===b.value)&&(!c.value||el.dataset.category===c.value);el.hidden=!hit;visible+=Number(hit);});count.textContent=`共 ${visible} 条`;blank.hidden=visible!==0;} + [q,b,c].forEach(x=>x.addEventListener(x===q?'input':'change',run));run(); + }); +})(); diff --git a/app/static/site-runtime.js b/app/static/site-runtime.js new file mode 100644 index 0000000..e4715e8 --- /dev/null +++ b/app/static/site-runtime.js @@ -0,0 +1,31 @@ +'use strict'; +(()=>{ + const $=s=>document.querySelector(s), $$=s=>[...document.querySelectorAll(s)]; + const menu=$('.sf-menu-toggle');if(menu){menu.addEventListener('click',()=>{let open=menu.getAttribute('aria-expanded')!=='true';menu.setAttribute('aria-expanded',String(open));$('#sf-primary-links')?.classList.toggle('is-open',open);});} + // Filter actual static content cards, no API is needed at runtime. + $$('.sf-collection').forEach(grid=>{ + const cards=$$('.sf-item',grid); + if(cards.length<2)return; + const toolbar=document.createElement('div');toolbar.className='sf-filters'; + const search=document.createElement('input');search.type='search';search.placeholder='筛选当前列表';search.setAttribute('aria-label','筛选当前列表'); + const status=document.createElement('span');status.setAttribute('aria-live','polite');status.style.fontSize='12px'; + const brand=document.createElement('select');brand.setAttribute('aria-label','按品牌筛选'); + const category=document.createElement('select');category.setAttribute('aria-label','按分类筛选'); + function fill(el,field,all){el.append(new Option(all,''));const vals=[...new Set(cards.map(x=>x.dataset[field]||'').filter(Boolean))].sort();vals.forEach(v=>el.append(new Option(v,v)));if(!vals.length)el.hidden=true;} + fill(brand,'brand','全部品牌');fill(category,'category','全部分类'); + toolbar.append(search,brand,category,status);grid.parentNode.insertBefore(toolbar,grid); + function apply(){let val=search.value.trim().toLocaleLowerCase();let visible=0;cards.forEach(card=>{let hit=card.textContent.toLocaleLowerCase().includes(val)&&(!brand.value||card.dataset.brand===brand.value)&&(!category.value||card.dataset.category===category.value);card.hidden=!hit;if(hit)visible++;});status.textContent=`共 ${visible} 项`;} + search.addEventListener('input',apply);brand.addEventListener('change',apply);category.addEventListener('change',apply);apply(); + }); + const input=$('#sf-search'),root=$('#sf-search-results'),count=$('#sf-search-count'); + if(input&&root){ + let data=[];const params=new URLSearchParams(location.search);input.value=params.get('q')||''; + let debounce=null; + const render=()=>{let q=input.value.trim().toLocaleLowerCase();let found=q?data.filter(d=>`${d.title} ${d.summary} ${d.brand} ${d.type}`.toLocaleLowerCase().includes(q)).slice(0,100):data.slice(0,30); + root.replaceChildren();count.textContent=`${found.length} 条结果${!q?'(显示前30条)':''}`; + found.forEach(d=>{let article=document.createElement('article'),a=document.createElement('a'),small=document.createElement('small'),p=document.createElement('p');article.className='sf-search-result';a.textContent=d.title;a.href=d.url.replace(/^\//,'');small.textContent=d.type;p.textContent=d.summary;article.append(a,small,p);root.append(article);}); + };const base=document.querySelector('base')?.href||location.href; + fetch(new URL('search-index.json',base)).then(r=>{if(!r.ok)throw Error('index unavailable');return r.json();}).then(v=>{data=v;render()}).catch(()=>{count.textContent='搜索索引加载失败,请检查站点文件部署路径';}); + input.addEventListener('input',()=>{clearTimeout(debounce);debounce=setTimeout(render,100)}); + } +})(); diff --git a/app/static_site.py b/app/static_site.py new file mode 100644 index 0000000..6dd8e1b --- /dev/null +++ b/app/static_site.py @@ -0,0 +1,56 @@ +"""Trusted shared site shell for the four visual theme designs and installed themes. +Client-side search/filter works without a server after static export. +""" +from html import escape as e +import json +from pathlib import Path +from . import db + +ASSETS=Path(__file__).resolve().parent/'static' + +def shell(html,site,page,records,nav): + theme=site.get('theme','corporate') + label=lambda key: e(str(key or ''),quote=True) + links=[] + if not nav: + nav=[{'title':'首页','route':'/'},{'title':'产品中心','route':'/product/'},{'title':'品牌','route':'/brand/'}, + {'title':'新闻资讯','route':'/post/'},{'title':'成功案例','route':'/case/'},{'title':'关于我们','route':'/about/'},{'title':'联系我们','route':'/contact/'}] + seen=set() + for n in nav[:25]: + route=n.get('route','/') + if route in seen:continue + seen.add(route) + links.append(f'{label(n.get("title"))}') + # Themes can vary greatly in typography, but share an accessible responsive site shell. + name=label(site['name']); tag=label(site.get('tagline','')) + panels=''.join(f'{label(x["title"])}{label(x["summary"][:86])}' for x in records if x.get('type') in ('brand','service') and x.get('status')=='published')[:12000] + header=f'''
+ + + 合作咨询 ↗
{('
'+panels+'
') if panels else ''}
''' + foot=f'''

{name}

{tag}

快速导航

{''.join(links[:6])}

联系我们

{label(site.get('email') or '请在管理后台填写联系邮箱')}

提交咨询 →
© {name}GLOBAL SOURCING · GLOBAL DELIVERY
''' + # Every generated landing/detail page needs exactly one visible page heading. + # Blueprints for information pages start with h2 blocks by default. + if page.get('route') != '/' and '
' in html and '

EXPLORE / {label(page.get("type","PAGE")).upper()}

{label(page.get("title", ""))}

{label(page.get("summary", ""))}
' + html=html.replace('
',intro+'
',1) + html=html.replace('',''+header,1) + html=html.replace('',foot+'',1) + html=html.replace('','',1) + return html + + +def write_assets(dist,records): + from shutil import copy2 + copy2(ASSETS/'factory.css',dist/'assets'/'factory.css') + copy2(ASSETS/'site-runtime.js',dist/'assets'/'site-runtime.js') + search=[{'title':r['title'],'url':r['route'],'summary':(r.get('summary') or '')[:360], + 'type':r['type'], 'brand':str((json.loads(r.get('meta_json') or '{}').get('fields') or {}).get('brand',''))} + for r in records if r.get('route') and r['route']!='/'] + (dist/'search-index.json').write_text(json.dumps(search,ensure_ascii=False),encoding='utf-8') + + +def search_page(site): + from html import escape + return f'''站内搜索 - {escape(site['name'])} +
← 返回首页

搜索网站内容

''' diff --git a/app/table_loader.py b/app/table_loader.py new file mode 100644 index 0000000..70fc1de --- /dev/null +++ b/app/table_loader.py @@ -0,0 +1,38 @@ +"""Decode Excel/CSV/JSON source files for a no-side-effect mapping preview.""" +import csv,io,json +from openpyxl import load_workbook + +MAX_ROWS=1000 + +def parse_table(name,data): + if len(data)>12_000_000:raise ValueError('文件超过12 MB') + lower=name.lower() + if lower.endswith('.csv'): + rows=list(csv.DictReader(io.StringIO(data.decode('utf-8-sig')))) + elif lower.endswith('.json'): + rows=json.loads(data.decode('utf-8-sig')) + if isinstance(rows,dict):rows=rows.get('rows',rows.get('entries',rows.get('items',[]))) + elif lower.endswith('.xlsx'): + wb=load_workbook(io.BytesIO(data),read_only=True,data_only=True) + try: + ws=wb.active;iterator=ws.iter_rows(values_only=True) + header=next(iterator,None) + if header is None:raise ValueError('Excel 工作表为空') + titles=[str(x or '').strip() for x in header] + if not all(titles) or len(set(titles))!=len(titles):raise ValueError('Excel 列标题不能为空或重复') + rows=[] + for i,values in enumerate(iterator): + if i>=MAX_ROWS:raise ValueError('一次最多导入1000条记录') + if any(x is not None and str(x).strip() for x in values): + rows.append(dict(zip(titles,values))) + finally:wb.close() + else:raise ValueError('支持 CSV、JSON 或 XLSX 文件') + if not isinstance(rows,list) or not 1<=len(rows)<=MAX_ROWS or any(not isinstance(r,dict) for r in rows): + raise ValueError('内容必须包含1-1000条对象记录') + keys=list(dict.fromkeys(str(k) for r in rows for k in r)) + if not keys or len(keys)>150:raise ValueError('字段数量不合法') + # Date/datetime -> string to avoid JSON encoder serialization exceptions. + normalized=[] + for row in rows: + normalized.append({str(k):v.isoformat() if hasattr(v,'isoformat') else v for k,v in row.items()}) + return {'filename':name,'columns':keys,'count':len(rows),'rows':normalized} diff --git a/app/templates/admin.html b/app/templates/admin.html new file mode 100644 index 0000000..08595c2 --- /dev/null +++ b/app/templates/admin.html @@ -0,0 +1,15 @@ +SiteForge · 智能网站工厂 +
+ +
● ENTERPRISE EDITION
+
正在初始化 SiteForge...
+
+
+
+
CREATE WEBSITE

创建全新网站

选择模板并创建站点,后续可继续导入旧内容或添加新数据。

+
CONTENT EDITOR

新增页面 / 内容

所有字段均属于当前网站,修改后重新构建即可更新静态文件。

首页区块快捷添加基础区块使用 JSON 配置;进入「可视化页面设计」可进行拖动排序和属性调整。
+ diff --git a/app/templates/editor.html b/app/templates/editor.html new file mode 100644 index 0000000..4924b3a --- /dev/null +++ b/app/templates/editor.html @@ -0,0 +1,26 @@ +SiteForge Studio · 可视化建站 + +
+
+
◈
SiteForgeVISUAL STUDIO
+
加载中 / 页面编辑器
+
+ 未修改 +
+
+ +
实时预览/正在展示正式主题中的完整页面,编辑后需保存并重新构建
+ +
+
+
diff --git a/app/templates/factory-v2.html b/app/templates/factory-v2.html new file mode 100644 index 0000000..a36a2c7 --- /dev/null +++ b/app/templates/factory-v2.html @@ -0,0 +1,12 @@ +SiteForge 3 · 网站生产中心 +
+ +
SiteForge / 网站总览
+
MANAGE · ASSEMBLE · DEPLOY

网站生产工作台

将新内容或旧网站数据与主题组合,构建独立可部署的网站。

可视化编辑器 ↗
+
+
+

创建网站

不需要旧网站,也能直接选择主题、添加内容、构建整套官网。

+ diff --git a/app/templates/login.html b/app/templates/login.html new file mode 100644 index 0000000..74f660e --- /dev/null +++ b/app/templates/login.html @@ -0,0 +1,5 @@ +登录 · SiteForge 网站工厂
S✳

欢迎使用 SiteForge

登录网站生产工作台,管理网站、内容、主题和静态构建版本。

SiteForge Web · Enterprise Edition v1.0
diff --git a/app/templates/theme-gallery.html b/app/templates/theme-gallery.html new file mode 100644 index 0000000..d0f0811 --- /dev/null +++ b/app/templates/theme-gallery.html @@ -0,0 +1,15 @@ +SiteForge · 完整主题预览中心 + +
SF SiteForge · 主题预览中心返回网站工厂 →
+
+ +
预览失败,请确认已登录。
+
+
预览使用真实页面模板与演示数据。实际网站构建后将使用你的品牌信息和内容。
+ diff --git a/app/theme_pro.py b/app/theme_pro.py new file mode 100644 index 0000000..2ba173c --- /dev/null +++ b/app/theme_pro.py @@ -0,0 +1,174 @@ +"""Full, version-aware page-family theme renderer. + +All content is escaped by Jinja's HTML autoescaping. Templates packaged with the +application are trusted; uploaded declarative themes cannot supply executable Jinja. +""" +from __future__ import annotations +import hashlib +import datetime +import json +import re +from html import escape +from pathlib import Path +from urllib.parse import quote +from jinja2 import Environment, FileSystemLoader, select_autoescape +from markupsafe import Markup +from .core import sanitize, route_path + +THEMES=('automotive','industrial','corporate','minimal') +ROOT=Path(__file__).resolve().parent.parent +LOADER=Environment(loader=FileSystemLoader(str(ROOT/'themes')),autoescape=select_autoescape(['html','xml'])) + +TITLES={'product':'产品中心','vehicle':'车型中心','post':'新闻资讯','case':'项目案例','brand':'品牌中心','service':'服务与解决方案'} + +def meta(item): + try: + raw=item.get('meta_json') or '{}' + val=json.loads(raw) if isinstance(raw,str) else raw + return val if isinstance(val,dict) else {} + except (TypeError,ValueError):return {} + +def fields(item): + v=meta(item).get('fields') or {} + return v if isinstance(v,dict) else {} + +def text(item,key,limit=500): + return str(item.get(key) or '')[:limit] + +def slug(value): + # Conservative URL generation for newly derived pages; never rewrite imported URLs. + raw=str(value).strip().casefold() + ascii_slug=re.sub(r'[^a-z0-9-]+','-',raw).strip('-')[:54] + return ascii_slug or ('term-'+hashlib.sha256(raw.encode()).hexdigest()[:12]) + +def all_groups(records): + return {typ:[e for e in records if e.get('type')==typ] for typ in TITLES} + +def categories(records): + return sorted({str(fields(e).get('category') or '').strip() for e in records if e.get('type') in ('product','vehicle') and str(fields(e).get('category') or '').strip()},key=str.casefold) + +def brands(records): + found={} + for entry in records: + if entry.get('type')=='brand':found[entry['title'].casefold()]={'name':entry['title'],'url':entry['route']} + for entry in records: + if entry.get('type') not in ('product','vehicle'):continue + name=str(fields(entry).get('brand') or '').strip() + if name and name.casefold() not in found:found[name.casefold()]={'name':name,'url':'/brands/'+slug(name)+'/'} + return sorted(found.values(),key=lambda e:e['name'].casefold()) + +def infer_kind(page,mode='detail'): + path=page.get('route') or '/' + typ=page.get('type') or 'page' + explicit=str(meta(page).get('page_template') or '').strip().lower() + if mode=='home' or path=='/':return 'home' + if mode=='notfound':return 'notfound' + if path=='/search/':return 'search' + if path.startswith('/categories/'):return 'category' + if mode=='listing' or explicit=='listing' or path in ('/products/','/brands/','/news/','/cases/','/product/','/post/','/brand/','/case/','/service/','/vehicle/'): + return 'listing' + if typ in ('product','vehicle'):return 'product' + if typ=='post':return 'article' + if typ=='brand':return 'brand' + if typ=='case':return 'case' + if typ=='service':return 'service' + if explicit in ('home','about','contact','listing','product','article','brand','standard','notfound','category','case','service','faq'):return explicit + if path.startswith('/about'):return 'about' + if path.startswith('/contact'):return 'contact' + if path.startswith('/faq'):return 'faq' + if path.startswith('/services'):return 'service' + return 'standard' + +def safe_href(route): + if not isinstance(route,str):return '/' + if route.startswith(('mailto:','tel:','https://','http://')):return route + try:return route_path(route) + except ValueError:return '/' + +def safe_image(value): + if not value or not isinstance(value,str):return '' + value=value.strip() + if value.startswith('//') or any(c in value for c in ('<','>','"','\n','\r')):return '' + if value.startswith(('https://','http://')):return value + if value.startswith('/') or not value.startswith(('data:','javascript:')):return '/'+value.lstrip('/') + return '' + +def media_gallery(page): + """Accept JSON gallery arrays or explicit semicolon/newline separated URLs.""" + attrs=fields(page) + raw=attrs.get('gallery') or attrs.get('images') or meta(page).get('gallery') or [] + if isinstance(raw,str): + try: raw=json.loads(raw) + except (ValueError,TypeError):raw=re.split(r'[;\n]+',raw) + if not isinstance(raw,list):raw=[] + results=[] + for item in [page.get('cover')]+raw: + uri=item.get('url','') if isinstance(item,dict) else item + safe=safe_image(uri) + if safe and safe not in results:results.append(safe) + if len(results)>=20:break + return results + +def select_listing(page,kind,groups,all_records,requested): + if kind=='category': + category=text(page,'category') or text(page,'title') + return [e for e in groups['product']+groups['vehicle'] if str(fields(e).get('category') or '').casefold()==category.casefold()] + path=(page.get('route') or '').lower() + title=text(page,'title').lower() + template=meta(page) + source=next((b.get('source') for b in template.get('blocks',[]) if isinstance(b,dict) and b.get('type')=='collection' and b.get('source')),'') + category= 'post' if ('news' in path or '新闻' in title) else ('brand' if ('brand' in path or '品牌' in title) else ('case' if ('case' in path or '案例' in title) else ('service' if ('service' in path or '服务' in title) else 'product'))) + category=source if source in groups else category + if requested and kind=='listing' and not source and path.startswith(('/product/','/post/','/brand/','/case/','/service/')): + category={'/product/':'product','/post/':'post','/brand/':'brand','/case/':'case','/service/':'service/'} .get(path,category) + if category=='product':return groups['product']+groups['vehicle'] + return groups.get(category,[]) + +def make_context(site,page,records,mode='detail',listing_items=None): + pg=dict(page or {}) + kind=infer_kind(pg,mode) + groups=all_groups(records) + listing=select_listing(pg,kind,groups,records,listing_items) + if listing_items and mode=='listing' and not (pg.get('route') or '').startswith('/categories/'): + listing=list(listing_items) + detail_fields=fields(pg) + spec=[{'label':str(k).replace('_',' ').capitalize(),'value':str(v)} for k,v in detail_fields.items() if isinstance(v,(str,int,float)) and v not in ('',None) and k not in ('brand','category','model','description','image','cover','url')] + ownbrand=str(detail_fields.get('brand') or pg.get('title') or '').strip() + related=[x for x in groups['product']+groups['vehicle'] if str(fields(x).get('brand') or '').casefold()==ownbrand.casefold()] if kind=='brand' else [] + settings=json.loads(site.get('settings_json') or '{}') if isinstance(site.get('settings_json'),str) else site.get('settings_json',{}) or {} + nav=settings.get('nav') or [ + {'title':'首页','route':'/'},{'title':'产品','route':'/products/'}, + {'title':'品牌','route':'/brands/'},{'title':'新闻','route':'/news/'}, + {'title':'案例','route':'/cases/'},{'title':'关于','route':'/about/'}, + {'title':'联系','route':'/contact/'}] + def nav_item(row,depth=0): + output={'title':str(row.get('title',''))[:50],'route':safe_href(row.get('route') or '/')} + children=row.get('children',[]) if depth<2 else [] + output['children']=[nav_item(c,depth+1) for c in children[:20] if isinstance(c,dict)] if isinstance(children,list) else [] + return output + nav=[nav_item(x) for x in nav if isinstance(x,dict)][:20] + assets={'categories':[{'name':c,'url':'/categories/'+slug(c)+'/'} for c in categories(records)], + 'brands':brands(records)} + title=(pg.get('title') or site.get('name') or '') + canonical=(pg.get('canonical') or (site.get('domain','').rstrip('/')+pg.get('route','/') if site.get('domain','').startswith(('http://','https://')) else '')) + form_endpoint=settings.get('form_endpoint') or '' + form_endpoint=form_endpoint if isinstance(form_endpoint,str) and form_endpoint.startswith('https://') and not any(x in form_endpoint for x in ('<','>','\"','\n')) else '' + return {'year':datetime.datetime.now(datetime.timezone.utc).year,'theme_color':{'automotive':'#0e1c2c','industrial':'#113f59','corporate':'#f5f6fa','minimal':'#f8f7f3'}.get(site.get('theme'),'#ffffff'),'gallery':media_gallery(pg),'form_endpoint':form_endpoint,'site':site,'page':pg,'kind':kind,'records':records,'groups':groups,'listing':listing, + 'showcase':groups['product'][:6]+groups['vehicle'][:max(0,6-len(groups['product'][:6]))], + 'features':groups['service'][:3], 'stories':groups['post'][:3], 'cases':groups['case'][:3], + 'brands':assets['brands'],'categories':assets['categories'], 'nav':nav, 'specs':spec, + 'related':related,'fields':detail_fields,'content':Markup(sanitize(pg.get('body') or '')), + 'title':title, 'seo_title':pg.get('seo_title') or title,'seo_description':pg.get('seo_description') or pg.get('summary') or site.get('tagline') or '', + 'canonical':canonical,'safe_href':safe_href,'safe_image':safe_image, + 'fields_of':fields,'labels':TITLES,'footer_email':settings.get('email') or site.get('email') or '', + 'footer_text':settings.get('footer_text') or site.get('tagline') or '', + 'route':pg.get('route','/'), + 'pretty_type':TITLES.get(pg.get('type'),'内容'), + 'empty':len(listing)==0, + } + +def render(theme,site,page,records,mode='detail',listing_items=None,blocks_html=''): + if theme not in THEMES:raise ValueError('不是内置专业主题') + ctx=make_context(site,page,records,mode,listing_items) + ctx.update(custom_blocks=Markup(blocks_html or ''),has_custom=bool(blocks_html),noindex=(ctx['kind']=='notfound'),designer_replace=meta(page).get('layout_mode')=='replace') + return LOADER.get_template(theme+'/pro.html').render(**ctx) diff --git a/app/theme_store.py b/app/theme_store.py new file mode 100644 index 0000000..7a09881 --- /dev/null +++ b/app/theme_store.py @@ -0,0 +1,171 @@ +"""Versioned *declarative* theme packages. Never execute user uploaded templates. + +Uploads can customize styles and built-in page blueprint presets. The trusted +Jinja template is copied from the built-in base. Arbitrary HTML/Jinja/JS in +uploaded archives is deliberately not supported. +""" +from __future__ import annotations +import io, json, re, zipfile, shutil, time +from pathlib import Path +from . import db + +BASE=Path(__file__).resolve().parent.parent/'themes' +THEME_ROOT=db.DATA/'theme_packages' +BUILTIN=('automotive','industrial','corporate','minimal') +THEME_ID=re.compile(r'^[a-z][a-z0-9-]{2,39}$') +VERSION=re.compile(r'^\d+\.\d+\.\d+$') + +class ThemeError(ValueError):pass + +def setup(): + with db.connect() as c: + c.executescript('''CREATE TABLE IF NOT EXISTS theme_versions ( + theme_id TEXT NOT NULL, version TEXT NOT NULL, name TEXT NOT NULL, + description TEXT NOT NULL DEFAULT '', installed_at TEXT NOT NULL, + PRIMARY KEY(theme_id,version)); + CREATE TABLE IF NOT EXISTS site_theme_pins ( + site_id TEXT PRIMARY KEY REFERENCES sites(id) ON DELETE CASCADE, + theme_id TEXT NOT NULL, version TEXT NOT NULL); + ''') + +def catalog(): + builtin=[] + for tid in BUILTIN: + obj=json.loads((BASE/tid/'manifest.json').read_text(encoding='utf-8')) + builtin.append({**obj,'builtin':True,'installed':True}) + custom=[dict(r,builtin=False,installed=True) for r in db.rows('SELECT * FROM theme_versions ORDER BY installed_at DESC')] + return builtin+custom + +def ids(): + return set(BUILTIN)|{r['theme_id'] for r in db.rows('SELECT DISTINCT theme_id FROM theme_versions')} + +def latest(tid): + if tid in BUILTIN: + return json.loads((BASE/tid/'manifest.json').read_text(encoding='utf-8')).get('version','1.0.0') + items=db.rows('SELECT version FROM theme_versions WHERE theme_id=?',(tid,)) + if not items:raise ThemeError('主题不存在') + return sorted((r['version'] for r in items),key=lambda s:tuple(map(int,s.split('.'))))[-1] + +def resolve(tid,version=None): + if tid in BUILTIN: + current=latest(tid) + if not version or version==current:return BASE/tid + if not VERSION.fullmatch(version):raise ThemeError('非法主题版本') + previous=(BASE/tid/'versions'/version).resolve() + if not previous.is_relative_to((BASE/tid/'versions').resolve()) or not previous.is_dir(): + raise ThemeError('内置主题的历史版本不存在') + return previous + ver=version or latest(tid) + if not db.one('SELECT theme_id FROM theme_versions WHERE theme_id=? AND version=?',(tid,ver)): + raise ThemeError('主题版本不存在') + result=(THEME_ROOT/tid/ver).resolve() + if not result.is_relative_to(THEME_ROOT.resolve()) or not result.is_dir():raise ThemeError('主题目录丢失') + return result + +def pin(site_id,tid,ver=None): + target=resolve(tid,ver) + ver=ver or latest(tid) + with db.connect() as c: + c.execute('INSERT INTO site_theme_pins VALUES (?,?,?) ON CONFLICT(site_id) DO UPDATE SET theme_id=excluded.theme_id,version=excluded.version',(site_id,tid,ver)) + c.execute('UPDATE sites SET theme=?,updated_at=? WHERE id=?',(tid,db.now(),site_id)) + return {'theme':tid,'version':ver,'path':str(target)} + +def pinned(site_id): + return db.one('SELECT theme_id,version FROM site_theme_pins WHERE site_id=?',(site_id,)) + +def compatibility(theme_id,version=None): + """Theme build/renderer compatibility, not a subjective visual quality score.""" + from . import blueprints + try:folder=resolve(theme_id,version) + except ThemeError as exc:return {'valid':False,'errors':[str(exc)],'warnings':[]} + issues=[];warnings=[] + for resource in ('manifest.json','theme.css','page-presets.json','pro.html','pro.css'): + if not (folder/resource).is_file():issues.append('缺少 '+resource) + if not issues: + try:manifest=json.loads((folder/'manifest.json').read_text(encoding='utf-8')) + except (ValueError,UnicodeError) as exc:issues.append('主题清单解析失败:'+str(exc));manifest={} + if manifest.get('id')!=theme_id:issues.append('清单 ID 与文件夹不匹配') + try:presets=json.loads((folder/'page-presets.json').read_text(encoding='utf-8')) + except (ValueError,UnicodeError):presets={};issues.append('页面预设 JSON 不合法') + missing=[kind for kind in blueprints.PAGE_TEMPLATES if kind not in presets] + if missing:warnings.append('缺少可编辑页面预设:'+','.join(missing)) + layout_family=manifest.get('layout_family',theme_id) + family=layout_family if layout_family in BUILTIN else 'corporate' + page_root=(folder/'pages') if (folder/'pages').exists() else BASE/family/'pages' + for kind in ('home','about','contact','listing','product','brand','article','case','service','faq','category','search','standard','notfound'): + if not (page_root/(kind+'.html')).is_file():issues.append('缺少实际页面布局:'+kind) + return {'valid':not issues,'theme':theme_id,'version':version or (latest(theme_id) if not issues else version), + 'checked_page_types':14,'errors':issues,'warnings':warnings, + 'note':'技术兼容性检查不会代替企业内容准确性和真实设备视觉验收'} + +def install(blob:bytes): + if len(blob)>8_000_000:raise ThemeError('主题包最大 8 MB') + try:archive=zipfile.ZipFile(io.BytesIO(blob)) + except zipfile.BadZipFile:raise ThemeError('不是 ZIP 压缩包') + names={n.filename for n in archive.infolist()} + permitted={'manifest.json','theme.css','page-presets.json','preview.png','preview.jpg','preview.webp'} + if not {'manifest.json','theme.css','page-presets.json'}.issubset(names):raise ThemeError('缺少主题清单、样式或页面预设') + if len(names)>6 or any(name not in permitted for name in names):raise ThemeError('主题包只能包含受支持的声明式文件,不允许模板脚本') + for f in archive.infolist(): + if f.file_size>1_500_000 or f.is_dir():raise ThemeError('主题资源体积或结构不符合要求') + manifest=json.loads(archive.read('manifest.json')) + tid=manifest.get('id',''); ver=manifest.get('version','') + if not THEME_ID.fullmatch(tid) or not VERSION.fullmatch(ver) or tid in BUILTIN:raise ThemeError('主题 ID 或 SemVer 不合法,内置主题不能覆盖') + if db.one('SELECT theme_id FROM theme_versions WHERE theme_id=? AND version=?',(tid,ver)): + raise ThemeError('同版本主题已存在,请升级版本号') + if str(manifest.get('layout_family') or 'corporate') not in BUILTIN: + raise ThemeError('layout_family 只能选择已审查的基础布局') + title=str(manifest.get('name',''))[:100]; description=str(manifest.get('description',''))[:400] + if not title:raise ThemeError('主题名称不能为空') + presets=json.loads(archive.read('page-presets.json')) + if not isinstance(presets,dict):raise ThemeError('页面模板预设必须为对象') + from . import blueprints + for kind,blocks in presets.items(): + if kind not in blueprints.PAGE_TEMPLATES:raise ThemeError('未知的页面模板:'+kind) + if not isinstance(blocks,list):raise ThemeError('页面预设内容必须是区块数组') + for b in blocks: + if not isinstance(b,dict):raise ThemeError('页面预设区块格式不正确') + blueprints.validate_blocks([blueprints.new_block(b['type'],b) for b in blocks]) + css=archive.read('theme.css').decode('utf-8') + # Style packages are declarative: no network requests or executable CSS extensions. + if len(css)>180000 or re.search(r'@import\b|url\s*\(|expression\s*\(|behavior\s*:|12: warnings.append('Elementor 嵌套超过安全限制');return + if not isinstance(elements,list):return + for item in elements[:300]: + if not isinstance(item,dict):continue + typ=item.get('widgetType') or '' + settings=item.get('settings') or {} + if not isinstance(settings,dict):settings={} + if typ: + count[0]+=1 + if typ not in WIDGETS: + if len(warnings)<60:warnings.append('未转换的 Elementor 组件:'+str(typ)[:65]) + elif typ=='heading': + size=settings.get('header_size','h2');size=size if size in ('h1','h2','h3','h4','h5','h6') else 'h2' + parts.append(f'<{size}>{escape(str(settings.get("title") or ""))}') + elif typ=='text-editor': + parts.append(sanitize(str(settings.get('editor') or ''))) + elif typ in ('image','image-box'): + image=settings.get('image') or {} + url=image.get('url','') if isinstance(image,dict) else '' + if isinstance(url,str) and url.startswith(('https://','http://','/')): + parts.append(f'

{escape(str(settings.get(

') + if typ=='image-box' and settings.get('title_text'): + parts.append('

'+escape(str(settings['title_text']))+'

') + if typ=='image-box' and settings.get('description_text'): + parts.append('

'+escape(str(settings['description_text']))+'

') + elif typ=='button': + href=settings.get('link') or {} + dest=href.get('url','') if isinstance(href,dict) else '' + if isinstance(dest,str) and dest.startswith(('https://','http://','/')): + parts.append(f'{escape(str(settings.get("text") or "更多信息"))}') + elif typ=='html': + parts.append(sanitize(str(settings.get('html') or ''))) + elif typ in ('video','divider','spacer','icon-box'): + if len(warnings)<60:warnings.append(f'{typ} 布局/嵌入需人工复核') + walk(item.get('elements') or [],depth+1) + walk(data) + return {'html':'\n'.join(parts),'warnings':warnings,'widgets':count[0]} + +def wp_taxonomy(item): + data={} + for node in item: + if node.tag.rsplit('}',1)[-1]!='category':continue + domain=node.get('domain','') + name=(node.text or '').strip() + if not name:continue + if domain in ('category','product_cat','vehicle_cat'):data['category']=name + elif domain in ('product_brand','brand','pa_brand'):data['brand']=name + elif domain=='product_type':data['product_type']=name + elif domain.startswith('pa_'):data[domain[3:]]=name + return data + +def wp_media_map(root): + """WXR attachment ID -> original URL, useful for featured and gallery images.""" + media={} + for item in root.findall('./channel/item'): + typ=post_id=url='' + for node in item: + local=node.tag.rsplit('}',1)[-1] + if local=='post_type':typ=node.text or '' + elif local=='post_id':post_id=node.text or '' + elif local=='attachment_url':url=node.text or '' + if typ=='attachment' and post_id and url and url.startswith(('http://','https://')): + media[post_id]=url + return media diff --git a/app/worker.py b/app/worker.py new file mode 100644 index 0000000..406b365 --- /dev/null +++ b/app/worker.py @@ -0,0 +1,60 @@ +"""Dedicated Docker worker: database-backed queue, serialized claim, retry + restart recovery.""" +import json +import os +import signal +import time +from datetime import datetime, timedelta, timezone +from . import db, enterprise, builder + +running=True + +def stop(*_): + global running + running=False + +def claim(): + with db.connect() as c: + c.execute('BEGIN IMMEDIATE') + job=c.execute("""SELECT id FROM build_jobs + WHERE state='queued' AND (retry_at IS NULL OR retry_at<=?) + ORDER BY created_at,id LIMIT 1""",(db.now(),)).fetchone() + if not job:return None + c.execute("UPDATE build_jobs SET state='running',attempts=attempts+1,started_at=?,retry_at=NULL WHERE id=?",(db.now(),job['id'])) + return dict(c.execute('SELECT * FROM build_jobs WHERE id=?',(job['id'],)).fetchone()) + +def run_once(): + job=claim() + if job is None:return None + try: + if job['kind']=='build': result=builder.build(job['site_id'],False) + else:raise ValueError('Unsupported job') + db.execute("UPDATE build_jobs SET state='done',result_json=?,finished_at=? WHERE id=?", + (json.dumps(result,ensure_ascii=False),db.now(),job['id'])) + except Exception as exc: + maximum=max(1,int(os.getenv('SITEFORGE_JOB_MAX_ATTEMPTS','3'))) + if job['attempts'] V5 是在 V4 原有后台、内容模型、编辑器与构建流程上迭代的**行业网站主题交付候选版**。完成页面功能及技术回归,不代表现已取得各类网站的商用法律、运营或视觉验收。 + +## 本轮实际开发 + +- 四个独立主题 Automotive、Industrial、Corporate、Minimal,继续保留各自 14 类页面模板。 +- 页面视觉重新整理:商业版字体排版、节奏、卡片、详情与参数布局、按钮交互、移动端适配;四套不同视觉方向。 +- 主题自带本地 SVG 视觉示意素材,可离线浏览;上传企业正式图片或 Logo 后,会优先展示真实素材。内置插画不是实际产品图片,请正式交付前替换。 +- 全站 Logo 优先使用网站实际配置。导航多级菜单、品牌与分类目录、产品画廊、筛选、对比页面结构等保持数据驱动。 +- 移动端导航与筛选空状态、键盘焦点、减少动态效果偏好等交互改进。 +- 站点 OG 标题/摘要与主题色,继承站点已有 Title/SEO/Canonical/Robots,不自行修改迁移来源 URL。 +- 建立商业交付报告 `commercial-readiness.json`,提示演示内容、占位图、Logo、询盘渠道及隐私说明等待人工验证事项。报告也在 `siteforge-report.json` 包含摘要。这与静态质量检测分开,**不会虚构已合规或已上线**。 +- 新增主题 `1.1.0`。保留 V4 主题 `1.0.0` 的完整模板快照供已锁定旧主题的站点使用;更旧的 `0.3.0` 仍保留。 +- 修正后台主题预览的商业 CSS 与本地 SVG 资源加载,使其与构建所用主题文件一致。 + +## 四套主题的业务定位 + +| 主题 | 风格与重点 | 常见栏目 | +| --- | --- | --- | +| Automotive | 深蓝与暖金、大幅车辆展示、车型与品牌分区 | 首页/车型/品牌/参数/新闻/案例/联系 | +| Industrial | 浅蓝与工程网格、产品分级、规格表 | 首页/分类/产品/规格/案例/技术文章/联系 | +| Corporate | 现代紫白体系、业务能力与项目展示 | 首页/服务/关于/案例/新闻/联系 | +| Minimal | 中性色编辑式布局、作品陈列 | 首页/作品/服务/简介/联系 | + +## 快速使用 + +1. Docker Desktop 启动后,在项目目录初始化 `.env`,然后运行 `docker compose up -d --build`。 +2. `http://localhost:8787/` 登录。`/theme-gallery` 查看主题,`/factory-studio` 创建网站。 +3. 在网站项目中初始化完整栏目,添加企业资料、Logo、正式邮箱、产品与文章。 +4. 选择主题版本 `1.1.0`、预览、生成静态站并在 ZIP 中查看 `commercial-readiness.json` 与 `siteforge-report.json`。 +5. 完成真实设备、内容、图片版权、SEO、表单、法律页面与性能检查后再上线。 + +## 商业交付前人工验收清单 + +- [ ] 企业 Logo、品牌颜色、产品主图、图库和版权授权材料均已填入;不再使用演示素材。 +- [ ] 首页、关于、产品、品牌、新闻、案例、联系与 404 页面逐页核对;不允许空栏或虚构数据。 +- [ ] PC 1440px/1024px、手机 390px/360px 等设备检查,尤其是中文/英文长标题和多级菜单。 +- [ ] 真实询盘接收渠道已配置并实际发送验证;如连接第三方表单,需要其隐私和防垃圾邮件机制。 +- [ ] Title、Description、Canonical、Robots、Sitemap 和历史 URL 与旧站基线一致(仅迁移项目)。 +- [ ] 图片与 PDF、内部链接、搜索、筛选、参数/画廊和键盘操作实际验证。 +- [ ] 业务文案和业绩指标经过企业审核,隐私政策/法律页按目标市场要求配置。 +- [ ] 部署环境的 HTTPS、缓存、日志、备份、回滚、安全与压力测试已通过。 + +## 当前不在本轮保证范围 + +不保证任意 WordPress/Elementor/ACF/WooCommerce 无损迁移;不提供任意代码主题安全安装、内置邮件服务器、完整 Art Design Pro 官方仓库整合、PostgreSQL/Redis、多云一键发布,也未完成正式生产环境合规或多浏览器全面测试。构建通过只是技术成功,`commercially_certified` 固定为 `false`。 + +## 测试方法 + +```bash +python -m pytest -q +PYTHONPATH=. SITEFORGE_DEMO_OUT=/tmp/siteforge-demo python scripts/make_theme_demo.py +python scripts/make_offline_preview.py /tmp/siteforge-demo /tmp/siteforge-preview.html +``` + +静态演示使用虚构内容,包含各主题的本地 `visual/hero.svg`;不依赖第三方图片服务。 diff --git a/docs/IMPLEMENTATION_V4.md b/docs/IMPLEMENTATION_V4.md new file mode 100644 index 0000000..0e66401 --- /dev/null +++ b/docs/IMPLEMENTATION_V4.md @@ -0,0 +1,22 @@ +# V4 功能验收矩阵 + +| 用户期望 | 本次已实现 | 继续开发/验收 | +|---|---|---| +| 四套完整行业主题 | 原 V3 的四套主题共 56 组独立模板;V4 追加多级导航、产品图库 Lightbox、可配置 HTTPS 表单和主题兼容性检查 | 专业素材、真实表单接收服务、复杂车型筛选逻辑与视觉优化 | +| 可视化组件设计 | 支持编辑器响应式多列布局、列内区块、全页自由布局/追加模式、完整主题预览、可复用组件库 | 自由网格画布、高级排版控制、多人实时协作 | +| 内容智能装配 | Excel/CSV/JSON 解析预览、字段别名、车型/能源/续航/图库/参数绑定、关系报告与品牌空页面装配 | 多值关系、多语言和复杂字段映射、AI 语义匹配需人工确认 | +| WordPress 迁移 | WXR/REST 等原有路径 + V4 特殊字段转换、SEO 清单、静态构建差异和人工复核机制 | SQL 全库深度适配、复杂插件无损解析及真实站验收 | +| 发布前审计 | WXR/sitemap/CSV/JSON 基线;实际 HTML 路由、title、description、canonical、robots、H1 和质量检查;差异 CSV;单条人工接受理由 | 真实 HTTP 状态 / CDN 规则与线上 Google Search Console 指标监控 | + +## 源数据验证标准 + +- 未抓取/导出的路径不能自动假设不存在。 +- HTTP 200 的源路径必须有对应 HTML,缺少会阻断发布。 +- SEO 源数据缺失时只报告已知差异,不虚构旧站 metadata。 +- 所有警告必须审核接受或修复后重新构建;结构阻断项不允许直接接受例外。 +- 复杂 Elementor、ACF、WooCommerce 数据保留原始字段,部分安全转换并列出人工核查项。 +- 构建产物中不得出现未识别的本地资源/内链错误;外部资源应按实际站点审核是否本地化。 + +## 运行安全界限 + +本项目不提供 Docker 容器启动成功的保证。不要将默认测试端口直接暴露公网;请配置 HTTPS、限流、备份、生产安全审计。管理中心与客户静态网站独立部署。 diff --git a/docs/PRODUCTION_ROADMAP.md b/docs/PRODUCTION_ROADMAP.md new file mode 100644 index 0000000..df6c60f --- /dev/null +++ b/docs/PRODUCTION_ROADMAP.md @@ -0,0 +1,17 @@ +# SiteForge 企业正式生产化清单 + +本项目实现了单机 Docker Web 形式的网站生产核心功能。要达到大型企业多团队多环境长期运行标准,还应完成如下工程工作(不属于本次已验证能力): + +1. **数据库和队列**:从当前 SQLite WAL 单实例升级到 PostgreSQL(Alembic 迁移、多租户行级访问控制、连接池)和 Redis/RabbitMQ + Worker;构建任务幂等、重试退避、死信队列、任务超时和容量控制。 +2. **账号与认证**:接入 OIDC / SSO、MFA、账号生命周期、细粒度角色审批、管理员紧急停用和会话撤销。当前 Cookie 签名在有效期内仍需账户禁用验证。 +3. **审计不可抵赖**:当前审计日志与主库共存,应写入追加型独立日志、配置审计保留策略和异地日志汇聚。 +4. **主题市场与沙箱**:严格的主题 Manifest 版本约束、组件兼容性、预览沙箱、第三方主题供应链签名、模板回滚和主题资产扫描。 +5. **高级可视化编辑**:页面级断点独立控制、样式变量冲突检测、组件属性 Schema、草稿分支、多人冲突锁、预览审批,避免用户自定义不受控的 HTML/JS。 +6. **大型 CMS 迁移**:WordPress 数据库/ACF/Elementor/WooCommerce 扩展适配器、Drupal、Joomla、Shopify/Headless CMS 权限 API、分页重试和导入幂等;开放页面爬虫不可替代私有源数据。 +7. **静态 QA**:旧站抓取 URL 完整性(包含状态码/Canonical/Hreflang/HTTP 缓存/结构化数据)、手机/桌面视觉差异、在线域名上线后的真实 301/404 响应检查,Search Console 监测。 +8. **安全**:运行时镜像与 Python 包漏洞扫描、供应链锁文件、SSRF DNS pinning / 网络 egress ACL、文件解析沙箱、病毒扫描、限流共享 Redis、CSP/安全响应头、DNS/CDN 变更审批。 +9. **发布管道**:Nginx/Caddy/对象存储/CDN 多提供商部署器、同步版本清单、预部署审核、流量切换、回滚、不同环境变量密钥管理。 +10. **运维 SLO**:Prometheus/OpenTelemetry、集中日志、异常报警、备份异地与还原演练、容量规划、真实 Docker/Kubernetes 压测及 RPO/RTO 目标。 +11. **Art Design Pro 官方集成**:取得有权使用的 Art Design Pro 版本源码,按对应版本路由/权限系统接入并在 Node 构建环境完整测试、验证其许可证要求。 + +建议实施顺序:正式试点环境 → 真实 WP 站迁移基线验收 → PostgreSQL/队列 → Art 前端正式整合 → 安全审计 → 大型网站灰度发布。 diff --git a/docs/RELEASE_STATUS_V2.md b/docs/RELEASE_STATUS_V2.md new file mode 100644 index 0000000..3ee176b --- /dev/null +++ b/docs/RELEASE_STATUS_V2.md @@ -0,0 +1,29 @@ +# SiteForge V2 六大优先级 · 开发交付与验收状态 + +最后核对:2026-10-21。本文件用“已实现/基础实现/未实现/未验证”分别记录,不把开发代码视为已通过生产验收。 + +| 用户要求 | 当前交付的可用能力 | 尚需继续的功能 | 状态 | +|---|---|---|---| +| ① 完整主题 | 四套独立设计、9 类页面预设、导航/响应式页脚、静态搜索和筛选、上传声明式主题 ZIP、版本管理、预览、站点固定版本 | 更完善的商业设计资产、更多独立布局组件、高级导航交互、行业专题详情定制 | 基础实现,视觉未商业验收 | +| ② 可视化编辑 | 区块拖拽排序/增删/复制、图片上传、预览双击文字编辑、PC/平板/手机布局差异、设计 Token、构建实时生效 | 任意布局嵌套/多层栅格、任意元素拖放、实时协同、完整撤销/重做、复杂动画 | 结构化区块编辑已实现 | +| ③ 内容智能装配 | 产品/车型/品牌映射、字段别名识别、规则保存、CSV/JSON 映射预演、冲突报告、实际导入、内容关联列表和详情 | 多层关联复杂自动识别、AI 语义识别、全量 Excel/数据库字段映射后台工作流和数据质量分析 | 规则式自动装配已实现 | +| ④ 完整旧站迁移 | WP WXR/REST、公开爬取、CSV/XLSX/JSON、ACF/Woo 部分字段、原 URL 基线、SEO 差异/404/资源质量检查、发布门禁 | Elementor 深度重构、完整 Woo 业务数据、非 WP 专有 CMS/数据库深度迁移、真实网站逐页验收 | 基础迁移,**不保证无损** | +| ⑤ 正式 Art Design Pro | Vue3 业务页面与官方工程离线接入脚本;完整可用原生 Web 控制台 | 官方完整工程及依赖集成、前端生产构建、路由与登录权限同构、官方 UI E2E | **尚未正式完成** | +| ⑥ 企业级发布运维 | 登录 RBAC、操作审计、异步 Worker、双人审批、本地发布、版本回滚、Docker Compose 数据卷 | PostgreSQL/Redis、多实例高可用、远程发布/HTTPS/CDN、监控告警、压测/渗透/容灾演练 | 基础交付,**未完成生产化** | + +## 本次可以跑通的闭环 + +- 从空白新建站点 → 初始化标准栏目 → 添加/导入内容 → 选择主题 → 设计区块 → 构建 → 预览 → 导出静态站。 +- 旧站内容按字段导入 → URL 和 SEO 基线 → 更换主题 → 检查是否丢页 → 审批 → 本地发布。 +- 一个账号仅能管理授权网站;不同账号申请与审批。新增版本后原内容保持可追溯。 + +## 生产发布前的强制验收 + +- [ ] 真实 Docker 环境完成 Compose 构建与 Web/Worker 启停验证。 +- [ ] Art Design Pro 官方代码整合并通过 `pnpm build`、浏览器验收(若坚持用它作为唯一后台)。 +- [ ] 数据卷、数据库升级方案、异地备份和恢复演练。 +- [ ] 真实旧站全量 URL/SEO/资源清单与新站差异报告无未处理阻断项。 +- [ ] 登录/权限/CSRF/SSRF/上传文件安全检查和渗透测试。 +- [ ] CDN/HTTPS/多域名发布、回滚、容量和性能验收。 + +**结论:这是可继续扩展的 V2 开发集成版本,不等于六项要求全部生产级完成。** diff --git a/docs/SiteForge_V2_Architecture.md b/docs/SiteForge_V2_Architecture.md new file mode 100644 index 0000000..e542499 --- /dev/null +++ b/docs/SiteForge_V2_Architecture.md @@ -0,0 +1,905 @@ +# SiteForge 智能网站生产工厂 + +> **完整产品需求与技术架构方案(PRD + Architecture + Implementation Plan)** +> 版本:V2.0(对 V1.0“通用多站点迁移与静态建站平台”的定位升级) +> 日期:2026-10-21 +> 状态:产品与研发规划版 +> 适用:产品经理、设计师、前后端研发、测试、运维、Claude 等编程助手 + +--- + +## 0. 核心定义与产品定位 + +**SiteForge 不只是旧网站迁移工具,而是“网站工厂(Website Factory)”:以模板、数据、页面结构和站点配置为原材料,经过装配、构建、验证和发布,持续生产完全独立的网站。** + +网站可以: + +1. **旧站改造**:从 WordPress、其他 CMS、自研 PHP 或普通网站导入既有内容,选用全新主题,保留旧站 SEO 和 URL,重建设计不同的静态网站。 +2. **模板建站**:没有旧站,选择行业模板,填入企业资料、Logo、产品、文章和媒体文件,直接形成新站。 +3. **数据建站**:从 Excel/CSV/JSON/XML、API、数据库或图片资料批量导入新内容,自动建立内容类型和页面,匹配模板生成网站。 +4. **自由建站**:从空白项目开始创建导航、页面、区块、数据集合、主题和页面规则;最终保存为可复用的新模板。 +5. **混合建站**:上述方式可自由组合,例如“旧站文章 + 新 Excel 产品 + 新模板 + 自定义页面”。 + +统一原则:**内容和视觉彻底分离,迁移和新建使用同一个内容模型、页面装配器、静态构建器、质量检测及发布系统。** + +### 0.1 产品成功标准 + +- 非迁移用户也能完整创建、编辑、构建并发布一个新网站。 +- 旧站迁移不是复制旧视觉,而是按“内容映射 → 新主题 → 全新网站”装配。 +- 同一份数据可以切换不同主题、重复构建;主题切换不得无故删除内容。 +- 一份设计可以保存为模板;模板升级不能未经确认覆盖站点定制内容。 +- 多网站独立运行、互不影响;输出静态文件后生产站不依赖中心平台在线。 +- 迁移站保留需要保留的 URL、SEO、链接和媒体访问行为;全新站可自由制定 URL/SEO 策略。 +- 导出物有真实 HTML/CSS/JS/媒体、必要的路由/重定向/服务器配置,而非只提供开发框架项目。 + +### 0.2 非目标与边界 + +- 不是承诺对任何公开 URL 一键获得不可见的后台、私有数据或完整业务代码。 +- 不是将所有交互功能硬转为静态 HTML;账号、支付、订单、实时库存、复杂搜索等需要独立 API/服务。 +- 不是初期一次性建成可对任意复杂页面精准拖拽设计的通用 Webflow 类编辑器。 +- 不保证迁移后搜索排名零波动;系统负责风险降低、可追溯验证和监测。 + +--- + +## 1. 四大建站入口与混合生产工作流 + +| 模式 | 起始条件 | 用户主要步骤 | 输出 | +|---|---|---|---| +| A 旧站重建 | 有旧网站 | 识别旧站 → 导入内容/SEO/URL → 选主题 → 映射 → 预览 → 构建 → 发布 | 新设计的独立网站 | +| B 模板新建 | 无旧站 | 选行业模板 → 填公司信息 → 导入或新增内容 → 配置栏目 → 预览 → 发布 | 全新网站 | +| C 数据驱动 | 有 Excel/API/数据库 | 导入数据 → 字段映射 → 生成内容集合 → 选模板 → 自动建立列表/详情页 → 发布 | 数据驱动批量页面网站 | +| D 空白搭建 | 需要自主设计 | 创建站点 → 可视化组装页面 → 配置设计变量/数据 → 预览 → 发布 → 沉淀为模板 | 定制网站与可复用模板 | + +### 1.1 “继续生产”能力 + +网站创建完后可继续:新增文章和产品、批量导入新数据、替换 Banner、维护多语言、增删栏目、修改 SEO、调整主题样式、升级组件、局部/全站重建、回滚到旧版本。**迁移任务完成不等于站点项目结束。** + +### 1.2 三个关键产品动作 + +- **Create Site**:从空白、模板或克隆既有站点结构新建。 +- **Import Content**:随时将旧站、文件、API 等数据导入现有站点,映射、去重、合并和校验。 +- **Build & Publish**:将某一明确的数据版本 + 主题版本 + 站点配置打包成可追溯静态发布版本。 + +不能把“导入网站”与“创建网站”绑定成一对一:一个站点允许多个数据源,一个数据源也可按授权复用到多个站点。 + +--- + +## 2. 总体架构:三条输入管线、一个生产流水线 + +```mermaid +flowchart TB + subgraph Inputs[网站生产输入] + I1[旧站导入:WP/CMS/PHP/HTML] + I2[新内容导入:Excel/CSV/JSON/API/媒体] + I3[从零创建:内容编辑器/页面编辑器] + end + subgraph Core[SiteForge 网站工厂控制中心] + C1[通用内容中心 / 媒体中心] + C2[模板市场 / 主题设计系统] + C3[页面编辑器 / 页面装配规则] + C4[网站蓝图 Site Blueprint] + C5[URL / SEO / 多语言规则] + C6[任务引擎 / 版本 / 权限] + end + subgraph Pipeline[网站生产流水线] + P1[数据校验与字段映射] + P2[站点装配与路由解析] + P3[静态构建 HTML/CSS/JS] + P4[预览 / SEO / 功能 / 链接检查] + P5[发布 / ZIP 导出 / 回滚] + end + I1 --> P1 + I2 --> P1 + I3 --> C1 + P1 --> C1 + C1 --> C4 + C2 --> C4 + C3 --> C4 + C5 --> C4 + C6 --> C4 + C4 --> P2 --> P3 --> P4 --> P5 + P5 --> S[独立运行的网站 A / B / C ...] +``` + +### 2.1 控制面与生产面分离 + +控制面:SiteForge 后台、API、数据/媒体资产、模板、采集、构建任务、发布/审核。生产面:各网站独立的静态文件/CDN/Nginx 与可选独立业务 API。**线上网站运行不要求 SiteForge API 或数据库常驻可用。** + +### 2.2 “网站”并不是“主题” + +一个网站由以下因素组合: + +``` +Website Release = + Site Configuration + + Site Blueprint (页面/栏目/菜单/区块/路由规则) + + Theme & Component Version + + Content Snapshot + + Asset Snapshot + + SEO & Redirect Rules + + Environment & Build Settings +``` + +这是保证可重复构建、主题更换和版本回滚的核心公式。 + +--- + +## 3. 关键领域模型(必须从第一天设计好) + +### 3.1 Site(站点) + +唯一站点 ID、站点类型、语言/地区、域名、环境、数据源、主题引用、权限、发布策略。 + +### 3.2 Site Blueprint(网站蓝图) + +蓝图描述**网站结构而不是内容本身**: + +- 页面树:主页、关于我们、联系方式、服务、产品、新闻、品牌、活动等。 +- 导航树:顶部主导航、Mega Menu、页脚、移动导航。 +- 页面类型:普通静态页、内容集合列表、集合详情、分类聚合、专题页。 +- 路由规则:固定页 slug、列表分页、产品 URL 格式、语言前缀。 +- SEO 规则:title 模板、canonical、sitemap/noindex、结构化数据类型。 +- 区块编排:Banner、产品网格、优势、表单、统计、FAQ、CTA 等。 +- 设计变量:品牌色、字体、布局宽度、间距、圆角、响应式断点。 +- 模板绑定:哪个页面类型使用哪套布局及组件。 +- 功能扩展:搜索、询盘、表单、在线聊天、外部 API 等。 + +站点蓝图可以从模板初始化,也可以由编辑器独立创建;保存之后属于站点自身,主题升级不得悄悄覆盖人工更改。 + +### 3.3 Content Type(内容类型)与 Entry(内容条目) + +内容中心不能硬编码只支持“文章”和“产品”,应支持动态类型: + +- 默认类型:Page、Post、Product、Category、Brand、Service、Case、FAQ、Team、Download。 +- 自定义内容类型:如汽车的 Vehicle、工业管材的 Pipe、房地产的 Property。 +- 字段定义:短文本、富文本、数字、布尔、日期、枚举、关系、数组、媒体、SEO 字段、多语言字段、引用。 +- 校验规则:是否必填、唯一性、格式、单位、可搜索/筛选、跨字段规则。 +- 数据状态:draft / review / published / archived;版本和来源可追踪。 + +### 3.4 Theme(主题)/ Template(页面模板)/ Component(组件) + +- **主题 Theme**:完整设计风格及组件集合、页面模板、响应式规则、设计 Token。 +- **页面模板 Page Template**:例如首页、产品列表、产品详情、文章详情、专题落地页。 +- **组件 Component**:Hero、图片网格、产品卡、参数表、图文交错、导航栏、页脚等。 +- **主题实例 Theme Instance**:某站点在主题基础上的参数覆盖与自定义区块配置。 + +### 3.5 Route Registry(路由注册表) + +一切最终发布 URL 的唯一事实源:`site_id + method + normalized_path + locale + route_behavior`;必须记录来源及冲突处理,不能仅由标题推断路径。 + +--- + +## 4. 模板工厂:不仅选模板,还要生产模板 + +### 4.1 模板分类 + +- 行业模板:汽车出口、工业品、制造业、外贸 B2B、企业介绍、服务类、资讯类。 +- 页面模板:首页、列表、详情、关于、联系、新闻、案例、FAQ、落地专题等。 +- 区块库:Hero、统计、产品参数、网格、时间线、客户评价、团队、展会等。 +- 布局方案:顶部导航、侧边导航、全宽、多列、自适应等。 + +### 4.2 模板选择体验 + +每个主题展示 PC/移动端预览、适合行业、支持的内容类型、内置页面数量、菜单形态和可编辑范围。选择主题后运行 `compatibilityCheck`,提示当前内容是否满足主题必需字段,并提供字段映射或替代组件选项。 + +### 4.3 模板来源 + +1. 平台预置主题。 +2. 开发人员以标准主题 SDK 编写并注册主题。 +3. 在设计器中制作页面,保存为模板或区块。 +4. 从现有网站**复制结构/设计配置**作为新项目起点,不自动复制私有数据。 + +### 4.4 防止主题与数据耦合 + +主题只能根据语义接口取数据,例如 `products.featured`、`company.name`、`posts.latest`。不能把某个具体产品、域名或 WordPress ID 硬编码进主题组件。 + +### 4.5 模板升级 + +主题使用不可变版本号。站点自行选择是否升级;升级前进行数据兼容性检测和区块差异预览;不支持自动合并的站点私有修改应手动解决。 + +--- + +## 5. 可视化建站编辑器(分级实现) + +### 5.1 编辑器 V1:区块式编辑 + +支持:增删页面、排列标准区块、替换组件样式、设定图片/文案/数据引用、编辑导航与页脚、调整设计 Token、切换 PC/平板/手机预览、草稿/审核/发布。 + +### 5.2 编辑器 V2:结构化可视化编排 + +支持:嵌套容器、列布局、响应式属性、条件显示、组件插槽、集合绑定、重复列表、页面模板编辑和全站设计系统管理。 + +### 5.3 编辑器 V3(可选):高级自由设计 + +高级拖拽、交互动效、定制脚本、组件市场等;需严格控制任意 HTML/JS 注入与主题兼容问题。 + +**不建议 V1 开发任意像素拖拽引擎。** 先用区块式编辑器建立高复用度和稳定构建体验;高级主题仍允许开发者编写独立代码。 + +### 5.4 主题自由度分级 + +- L1 配置型:换色、Logo、素材和文案即可上线。 +- L2 区块型:调整首页和内页的组件排列及内容映射。 +- L3 高级定制型:独立开发主题,完全自定义 HTML、样式、交互和布局。 + +这避免为了统一拖拽而让不同网站“长得一样”。 + +--- + +## 6. 内容工厂:手动、批量、API 与旧站多源输入 + +### 6.1 数据输入管道 + +| 输入渠道 | 典型数据 | 数据处理 | +|---|---|---| +| 后台手动新增 | 文章、产品、服务、页面 | 表单编辑和字段校验 | +| Excel / CSV | 车型、价格、参数、批量产品 | 预览字段、列映射、类型推断、增量更新 | +| JSON / XML | 第三方结构化数据 | schema 映射、字段转换、关系解析 | +| API / Webhook | ERP、CRM、内部商品系统 | 授权连接、定时/手动同步、冲突策略 | +| SQL / 备份 | 原数据库内容 | 隔离解析、适配映射、数据脱敏 | +| WordPress XML/API | 文章、产品、分类、meta | WordPress 插件与字段适配器 | +| 通用网站抓取 | 公开页面、SEO、图片、菜单 | 页面发现、JS 渲染、去噪和人工确认 | +| 媒体 ZIP / 对象存储 | 图片、PDF、视频 | 去重、批量命名和资源引用映射 | + +### 6.2 通用导入工作台 + +选择数据源 → 预览 → 识别内容类型 → 字段映射 → 数据清洗 → 去重/冲突策略 → 导入模拟运行(dry-run) → 确认入库 → 验证关联与资源 → 保留导入快照。 + +支持“仅新增”“按唯一键更新”“新增+更新”“人工审核冲突”;**默认不得直接覆盖人工编辑过的字段**。保留来源字段值、最新导入值、人工覆盖值及优先级。 + +### 6.3 内容与页面的区别 + +- 录入 1000 个车型,不意味着手工建 1000 个页面。 +- 车型都是 `Vehicle` 内容条目;系统采用一个 `vehicle-detail` 页面模板批量生成 1000 个对应 URL。 +- 新增车型后自动生成新详情页并更新分类列表、筛选数据、站点地图与内部链接。 + +### 6.4 媒体中心 + +- 原图存储,派生多尺寸格式;保留 alt、文件名、来源 URL 和使用关系。 +- 旧站媒体需维护原路径映射,必要时保留原 URL 的资源响应。 +- 批量上传、图库、资源去重、视频外链、PDF 和下载附件。 +- 资源删除时检查使用关系;不能造成已上线页面引用失效。 + +--- + +## 7. 网站装配引擎(全产品最核心的技术) + +### 7.1 输入 + +`site_blueprint.json + theme@version + content_snapshot + asset_manifest + route_registry + SEO_policy` + +### 7.2 核心过程 + +1. 验证主题与内容类型是否兼容。 +2. 解析页面树与导航,构造页面上下文。 +3. 读取数据绑定配置,生成列表和详情集合。 +4. 根据 URL/语言规则生成页面路径,检测重复与冲突。 +5. 按页面模板渲染静态 HTML,并生成 CSS/JS。 +6. 生成 sitemap、robots、结构化数据和必要的重定向配置。 +7. 执行页面链接、SEO、媒体、性能及功能验证。 +8. 形成不可变构建产物、版本元数据和差异报告。 + +### 7.3 装配示例:汽车站 + +- 公司信息绑定 `company.*`。 +- 首页精选车型绑定 `vehicles where featured=true`。 +- 品牌列表绑定 `brands`,每个品牌生成 `/brands/{slug}/`。 +- 车型详情绑定 `vehicles`,生成 `/models/{slug}/` 或从旧站注册表获取原有 URL。 +- 新闻内容绑定 `posts`,自动生成新闻列表和详情。 +- PC 和移动端页面由同一语义内容驱动,但主题可采用不同响应式布局。 + +### 7.4 蓝图 JSON 示例 + +```json +{ + "siteKey": "export-auto-demo", + "mode": "migration-redesign", + "theme": {"id": "automotive-premium", "version": "1.4.0"}, + "locale": "en", + "designTokens": {"primaryColor": "#1765B2", "contentWidth": "1280px"}, + "pages": [ + {"kind": "static", "path": "/", "template": "home"}, + {"kind": "collection", "path": "/products/", "template": "product-list", "contentType": "vehicle"}, + {"kind": "entry", "template": "product-detail", "contentType": "vehicle", "routeSource": "registry"} + ], + "navigation": {"header": ["/", "/products/", "/about-us/", "/contact/"]}, + "seoPolicy": "preserve-imported" +} +``` + +### 7.5 主题数据绑定示例 + +```json +{ + "template": "home", + "slots": { + "hero": {"type": "Hero", "bindings": {"title": "site.homeHero.title", "image": "site.homeHero.image"}}, + "featured": {"type": "VehicleGrid", "source": {"contentType": "vehicle", "filter": {"featured": true}, "limit": 8}}, + "news": {"type": "LatestPosts", "source": {"contentType": "post", "limit": 3}} + } +} +``` + +规则应由受控 schema/查询 DSL 解释,**禁止把任意用户输入作为数据库查询或 JS/SQL 执行**。 + +--- + +## 8. 旧站迁移子系统(Website Factory 的一个入口) + +### 8.1 兼容范围与优先顺序 + +P0:WordPress(REST API + WXR XML + SQL + `wp-content`)、HTML 静态站、普通 PHP 公开站。P1:Drupal、Joomla、自定义 CMS、Vue/React 渲染站。P2:电商、会员、复杂业务站专用适配器。 + +### 8.2 自动扫描 + +抓取 robots、sitemap、内部链接、菜单、页面、分页、媒体和 head SEO;识别 CMS、页面类型、动态表单、语言、状态码和 canonical;得到带来源、置信度和异常标记的扫描清单。 + +### 8.3 迁移独有工作流 + +`Old Site → Snapshot → Extract → Normalize → Map To Theme → Route/SEO Preserve → Preview/QA → Cutover` + +1. 建立旧站不可变快照,记录内容、URL、响应状态、SEO、媒体。 +2. 能取得权限时优先组合 API/备份/网页数据,不能取得权限时抓取公开内容并标记未知项。 +3. 将旧站类型映射到统一类型,例如 WooCommerce product → Product / ACF car → Vehicle。 +4. 让用户选择新的视觉主题,映射旧数据字段到新页面组件。 +5. 保留旧 URL 注册表、历史 301、canonical 和 SEO 元数据;异常需明示审核。 +6. 禁止新主题自动重写旧路由。新增加的内容可按新建路由规则生成。 +7. 预览视觉不同的新站并以旧站数据基线自动验收。 +8. 发布时注意 DNS、CDN、缓存、预览 noindex、必要 API 以及回滚。 +9. 上线后追踪 404、搜索抓取和流量变化;上线前后还可进行增量同步直至冻结旧站写入。 + +### 8.4 关键事实 + +公开页面采集无法保证后台未展示字段完整;网站登录、支付、购物车及实时功能不能纯静态复制;要在扫描报告中明确“可自动”“需授权”“需开发扩展”“不迁移”四种处置结果。 + +--- + +## 9. 新站创建子系统(与迁移同等优先级) + +### 9.1 模板新建流程 + +1. 输入网站名称、主语言、品牌信息、目标行业、域名(可延后绑定)。 +2. 选择模板并预览 PC/移动端。 +3. 从模板生成站点蓝图、页面树、导航和内容类型定义。 +4. 进入引导式公司资料表:名称、Logo、联系方式、社媒、地址、企业简介。 +5. 手工或批量添加产品、文章、品牌、图片;主题根据字段绑定渲染页面。 +6. 修改页面区块、主色、字体和导航,设置联系表单目标。 +7. 自动生成页面、站点地图与 SEO 默认值;检查内容完整性。 +8. 预览、发布、绑定域名;未来继续从后台更新。 + +### 9.2 空白新建流程 + +1. 创建站点与内容类型。 +2. 配置导航/页脚和布局框架。 +3. 通过区块编辑器建立页面和模板。 +4. 建立动态集合页及详情路由规则。 +5. 绑定内容和媒体,预览并发布。 +6. 经审核将该站的可复用结构导出为主题/站点模板。 + +### 9.3 导入内容建站流程 + +1. 上传 `vehicles.xlsx`,识别标题、品牌、图片、价格、参数等。 +2. 创建或选择 `Vehicle` 内容类型,建立字段映射、唯一键和数据校验。 +3. 选择适合的产品站主题;若字段不足,提示补齐,而不是凭空生成。 +4. 由车型集合创建列表、详情、品牌聚合页,生成对应 URL。 +5. 配置通用企业资料、SEO、导航,发布成完整新站。 + +### 9.4 主题替换机制 + +切换主题时预检查必需字段、页面类型、交互能力、导航结构及 URL 兼容性。允许在临时预览环境试装,保留旧主题和站点蓝图版本;确认后切换,不删除原内容和媒体。 + +--- + +## 10. URL 与 SEO:区分“迁移”和“新建”两种政策 + +### 10.1 迁移站(Preserve Mode) + +- 以实际旧站最终有效访问行为为基线,保留用户可见有效 URL、状态码和所需重定向链。 +- Title、Description、Canonical、Robots、Hreflang、JSON-LD、Open Graph、图片 alt、内链和 sitemap 做差异比对。 +- 旧 URL 到新构建产物一对一登记,包含中文路径、大小写、尾部斜杠、`.html`、分页、文件、查询参数规则。 +- 404 处理必须区分原本无效和新迁移导致;如果页面已批准下线,应设置合适的 301/410 行为。 +- 允许设计变化,但不能因使用新模板悄悄改变原 SEO/URL。 + +### 10.2 新站(Generate Mode) + +- 按站点定义 slug 策略和 URL 规则;允许人工指定覆盖。 +- 使用 SEO 默认模板,例如 `{product.name} | {site.name}`,并提示检查重复标题/空描述。 +- 自动生成 robots、sitemap、Open Graph、必要的 JSON-LD 和 canonical。 +- 多语言模式需完整管理 locale URL、翻译关联和 hreflang。 +- 预览域名必须鉴权并禁止搜索索引。 + +### 10.3 混合模式 + +来源于旧站的内容使用 Preserve URL,新增内容使用 Generate URL;注册表统一检查冲突。**不能简单设置一个整站 `preserve=true` 就忽略新内容路径生成。** + +### 10.4 质量门禁 + +迁移站关键 URL 和 SEO 的未批准差异必须阻止发布;新站的门禁主要是 URL 冲突、重复规范地址、无效内链、关键必填字段缺失和资源失效。 + +--- + +## 11. 管理后台信息架构(建议菜单) + +``` +SiteForge +├── 工作台 Dashboard +│ ├── 网站数量 / 最近发布 / 失败任务 / 风险 +│ └── 快速操作:新建网站 / 导入内容 / 创建模板 +├── 网站工厂 +│ ├── 所有网站 +│ ├── 创建网站向导(空白 / 模板 / 旧站 / 数据) +│ ├── 网站蓝图 / 页面树 / 菜单 / 域名 +│ └── 预览 / 历史版本 / 克隆结构 +├── 内容中心 +│ ├── 内容类型设计器 +│ ├── 文章 / 产品 / 自定义集合 +│ ├── 公司资料 / 全局数据 +│ ├── 数据导入与同步 +│ └── 媒体资源库 +├── 设计中心 +│ ├── 模板库 / 主题库 +│ ├── 页面模板 / 区块库 +│ ├── 页面编辑器 +│ ├── 全站设计 Token +│ └── 自定义主题注册与版本 +├── 迁移中心 +│ ├── 网站技术扫描 +│ ├── WordPress / CMS 导入 +│ ├── 字段映射 / 旧站快照 +│ ├── URL / SEO 基线 +│ └── 迁移差异报告 +├── 构建与发布 +│ ├── 构建任务 / 预览 +│ ├── 自动验收与报告 +│ ├── 发布审批 / 目标服务器 +│ ├── ZIP 导出 / 历史版本 +│ └── 回滚 / CDN 缓存刷新 +└── 系统设置 + ├── 用户 / 团队 / 角色 + ├── API 密钥 / 数据源凭证 + ├── 适配器 / 扩展服务 + ├── 审计日志 / 告警 + └── 存储 / 域名 / 环境 +``` + +### 11.1 新建网站首页建议 + +四个同级入口卡片:**旧站改造、使用模板新建、导入数据建站、空白建站**。不要把“迁移”置于唯一入口,也不要把“选择模板”藏在迁移流程中。 + +### 11.2 网站内部工作台 + +概览 → 内容 → 页面 → 设计 → SEO/路由 → 预览 → 发布 → 设置。便于独立运营每个网站,不要求管理员理解数据仓库和构建细节。 + +--- + +## 12. 推荐技术选型与研发边界 + +| 部分 | 推荐 | 备注 | +|---|---|---| +| 后台界面 | Vue 3 + TypeScript + Element Plus / Art Design Pro | 多站点、内容编辑、区块页面编辑器 | +| 核心 API | Python FastAPI + Pydantic + SQLAlchemy | 复用 Python 采集、数据处理和内部工程经验 | +| 异步任务 | Celery + Redis(或 Dramatiq) | 采集、媒体、导入、同步、质量任务 | +| 数据库 | PostgreSQL | JSONB 扩展字段、关系表和审计日志 | +| 采集服务 | Python httpx/Scrapy + Playwright | SSR/动态页面扫描与插件适配 | +| 模板及静态生成 | Astro + TypeScript/Node Worker | 主题组件、静态页面编译、增量/全量构建 | +| 资产存储 | S3 兼容对象存储/OSS + 本地缓存 | 原图、附件、站点版本和静态包 | +| 部署 | Nginx / 对象存储 + CDN / SSH 或 Git 工作流 | 独立站交付和可回滚 | +| 任务与日志 | PostgreSQL + Redis + 集中日志 | 可追踪的导入和发布作业 | +| 运行 | Docker Compose 首选 | 后期再按规模拆分服务 | + +**为什么不强制全部使用一种语言**:数据采集/规范化适合 Python;静态主题的现代前端生态适合 TypeScript/Astro。通过版本化 JSON 契约连接两个 Worker,避免牺牲网站的设计自由度。若团队对 NestJS 更熟,也可替换 API 层,不影响领域模型和构建协议。 + +### 12.1 部署架构 + +- API、PostgreSQL、Redis、导入 Worker、构建 Worker、管理前端、对象存储等分别通过容器管理。 +- 每个网站生成独立不可变 `release_id` 的产物并可单独导出、上传和回滚。 +- 为不同域名、地区和站点提供独立构建与部署配置。 +- 预览站点与生产站点隔离,预览授权、禁止索引。 + +--- + +## 13. 数据库详细方案 + +### 13.1 数据库分域 + +**组织与权限**:`organizations`、`users`、`memberships`、`roles`、`permissions`。 + +**网站与蓝图**:`sites`、`site_environments`、`site_blueprints`、`site_settings`、`page_nodes`、`navigation_menus`、`navigation_items`、`site_domains`。 + +**内容模型**:`content_types`、`field_definitions`、`content_entries`、`entry_versions`、`entry_relations`、`content_localizations`、`global_entries`。 + +**媒体**:`media_assets`、`media_variants`、`media_references`、`media_source_urls`。 + +**模板与组件**:`themes`、`theme_versions`、`page_templates`、`component_definitions`、`component_instances`、`design_tokens`、`site_theme_bindings`。 + +**数据输入与适配器**:`data_sources`、`adapter_definitions`、`import_jobs`、`import_items`、`field_mappings`、`sync_policies`、`source_snapshots`、`source_identity_map`。 + +**URL 与 SEO**:`routes`、`redirect_rules`、`seo_records`、`seo_snapshots`、`link_checks`、`route_diffs`。 + +**构建发布**:`build_jobs`、`build_artifacts`、`releases`、`deployments`、`quality_reports`、`audit_events`。 + +### 13.2 SQL 示意 + +```sql +CREATE TABLE sites ( + id UUID PRIMARY KEY, + organization_id UUID NOT NULL, + name TEXT NOT NULL, + site_key TEXT NOT NULL, + creation_mode TEXT NOT NULL CHECK (creation_mode IN + ('blank','template','migration','data_import','clone_structure')), + default_locale TEXT NOT NULL DEFAULT 'en', + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + UNIQUE (organization_id, site_key) +); + +CREATE TABLE content_types ( + id UUID PRIMARY KEY, + site_id UUID NOT NULL REFERENCES sites(id), + type_key TEXT NOT NULL, + schema_json JSONB NOT NULL, + UNIQUE (site_id, type_key) +); + +CREATE TABLE content_entries ( + id UUID PRIMARY KEY, + site_id UUID NOT NULL REFERENCES sites(id), + content_type_id UUID NOT NULL REFERENCES content_types(id), + source_identity TEXT, + slug TEXT, + locale TEXT NOT NULL DEFAULT 'en', + status TEXT NOT NULL DEFAULT 'draft', + data_json JSONB NOT NULL, + version INTEGER NOT NULL DEFAULT 1, + updated_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +CREATE TABLE site_blueprints ( + id UUID PRIMARY KEY, + site_id UUID NOT NULL REFERENCES sites(id), + revision INTEGER NOT NULL, + blueprint_json JSONB NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + UNIQUE (site_id, revision) +); + +CREATE TABLE routes ( + id UUID PRIMARY KEY, + site_id UUID NOT NULL REFERENCES sites(id), + locale TEXT NOT NULL, + path TEXT NOT NULL, + behavior TEXT NOT NULL, -- render | redirect | gone | proxy + target_entry_id UUID NULL REFERENCES content_entries(id), + target_url TEXT NULL, + source_kind TEXT NOT NULL, -- imported | generated | manual + UNIQUE (site_id, locale, path) +); + +CREATE TABLE build_jobs ( + id UUID PRIMARY KEY, + site_id UUID NOT NULL REFERENCES sites(id), + blueprint_revision INTEGER NOT NULL, + theme_version TEXT NOT NULL, + input_manifest JSONB NOT NULL, + status TEXT NOT NULL, + started_at TIMESTAMPTZ, + finished_at TIMESTAMPTZ +); +``` + +数据库 DDL 只是结构示意;正式实现应补全租户隔离策略、外键/索引、关系类型、软删、版本表、迁移脚本和审计字段。`source_identity_map` 不应在跨数据源之间简单依赖 slug 去重。 + +### 13.3 数据治理约束 + +- 以 `organization_id + site_id` 强制作用域隔离,查询接口必须实现授权校验。 +- 导入前保存 raw source;标准化后保留原来源与变更来源。 +- 使用内容版本快照支持回滚和差异查看。 +- 记录手动字段覆盖,不允许重导入无声覆盖。 +- 涉及多语言、复杂关系、复用媒体时,不用一张任意 JSONB 表取代所有关系模型。 + +--- + +## 14. 核心 API 契约(示意) + +```text +POST /api/sites # 建立网站;body.creationMode +GET /api/sites # 站点清单 +GET /api/sites/{siteId} +POST /api/sites/{siteId}/clone-structure +GET /api/themes # 主题库 +GET /api/themes/{themeId}/versions +POST /api/sites/{siteId}/theme/preview # 试装主题/兼容性检查 +POST /api/sites/{siteId}/theme/activate +GET /api/sites/{siteId}/blueprint +PUT /api/sites/{siteId}/blueprint # 乐观锁 revision +GET /api/sites/{siteId}/content-types +POST /api/sites/{siteId}/content-types +GET /api/sites/{siteId}/entries +POST /api/sites/{siteId}/entries +PATCH /api/sites/{siteId}/entries/{entryId} +POST /api/sites/{siteId}/imports # 文件/API/CMS/旧站导入 +POST /api/imports/{jobId}/dry-run +POST /api/imports/{jobId}/commit +GET /api/imports/{jobId}/conflicts +GET /api/sites/{siteId}/routes +GET /api/sites/{siteId}/seo-audit +POST /api/sites/{siteId}/builds +GET /api/builds/{buildId} +POST /api/sites/{siteId}/releases/{releaseId}/publish +POST /api/sites/{siteId}/deployments/{deployId}/rollback +GET /api/sites/{siteId}/quality-reports +``` + +### 14.1 任务状态机 + +`created → queued → running → needs_review | failed | completed`;构建验收后再从 `completed` 经 `approved → published`,允许取消、重试、失败恢复,但每个状态转换须有审计记录。 + +### 14.2 构建契约 + +Node/Astro 构建 Worker 只接受经过签名/授权访问的不可变输入 manifest,输出静态包路径、构建报告、内容 hash、资源清单和验收结果;不要将数据库直接暴露给静态构建主题代码。 + +--- + +## 15. 质量验收体系(按建站模式执行) + +### 15.1 通用验收 + +- 所有必需页面可访问、无冲突路由、没有意外 404/5xx。 +- 内容、分类、关联、图片和附件链接完整;过滤 XSS 等不可信脚本。 +- PC / 移动端核心页面无溢出、错位和严重布局问题。 +- sitemap、robots、canonical 与发布目标域名一致。 +- 站点预览不被搜索引擎收录,正式站按配置开放索引。 +- 表单、询盘、导航、搜索等启用功能经过实际测试。 +- 发布包可在隔离环境部署并独立访问;能回滚。 + +### 15.2 迁移专用验收 + +- 旧站已发现的有效 URL 都有一致访问或批准例外;关键路径的未批准变化阻断发布。 +- SEO 标题、描述、canonical、robots、hreflang 等做旧/新对照。 +- 产品/文章/页面数与旧站基线差异有解释;不可获取内容明确列出。 +- 资源与重定向规则逐项校验;上线后持续关注抓取和错误。 + +### 15.3 新站专用验收 + +- 必填企业信息、Logo、联系路径和站点语言完整。 +- 集合列表与详情页数量正确,新增内容可以重新生成网页。 +- SEO 自动生成规则没有导致大量重复标题或无意义内容。 +- 所选模板适用内容字段,缺失信息不能通过臆造补全。 + +### 15.4 验收报告分级 + +`BLOCKER`(必须解决)/ `WARNING`(可人工批准)/ `INFO`。不要把“页面加载成功”视作“内容迁移完整”。 + +--- + +## 16. 构建、部署、静态导出与持续更新 + +### 16.1 全量与局部构建 + +V1 可先全量构建保证正确性;V2 根据内容变化计算影响依赖:更新车型 → 对应详情、分类、品牌列表、首页精选和 sitemap 等重建。依赖图要可追踪,以免漏刷引用页。 + +### 16.2 交付格式 + +``` +release-2026-10-21-a1b2/ +├── public/ +│ ├── index.html +│ ├── about-us/index.html +│ ├── products/index.html +│ ├── products/example/index.html +│ ├── assets/... +│ ├── sitemap.xml +│ └── robots.txt +├── server/ +│ ├── nginx.conf.example +│ └── redirects.json +├── manifest.json +├── quality-report.json +└── deployment-notes.md +``` + +旧站存在 `.html`、尾斜杠与目录结构差异时,按照路由注册表选择适当静态目录或 Web 服务器规则;**输出文件路径本身不代表 HTTP 访问语义自动等同**,必须经过实际服务器验收。 + +### 16.3 部署策略 + +- 预览 / staging / production 三级环境,可选择简化为 preview + production。 +- 产物不可变、版本化;原子切换部署;媒体缓存和 HTML 缓存采用不同策略。 +- 支持 ZIP 下载、SSH/SFTP、Git CI、对象存储 + CDN;不同站点独立域名和缓存。 +- 发布操作与回滚操作留存操作人、时间、版本和差异。 + +### 16.4 外部服务扩展 + +表单、询盘、搜索、评论、登录等由独立功能适配器负责;主题仅通过公开的组件/客户端 SDK 调用,避免各站重复实现且不把平台后台变成生产站依赖。 + +--- + +## 17. 安全、权限、成本与可维护性 + +- **采集 SSRF**:阻断内部 IP、云元数据地址、敏感主机及重定向跳转;抓取速率限制、URL 白名单、robots 与站点使用权限审查。 +- **文件安全**:ZIP 解压路径穿越、恶意 HTML/JS/PHP、上传扩展名、MIME、体积限制与资源扫描。 +- **数据隔离**:租户、站点、内容、媒体、API 凭证严格隔离,导出权限与发布权限分离。 +- **主题安全**:主题依赖锁定、审核第三方组件、禁止把未审核用户表达式转换为运行代码。 +- **SEO 安全**:导入外站数据时不能盲目继承失效域名 canonical/跳转目标;必须判别并显示冲突。 +- **构建安全**:构建 Worker 沙箱、资源/CPU/时间限制、审计命令、脱离机密生产凭证。 +- **版本安全**:导入快照、内容版本、主题版本、发布版本皆可追溯与恢复。 +- **数据备份**:数据库备份、媒体/源快照、构建物及密钥轮换政策。 + +--- + +## 18. 开发路线图:从最小“网站工厂”开始 + +### 阶段 0 — 架构与样本验证(建议 2–3 周) + +成果:Site Blueprint schema、内容类型/字段协议、主题 manifest、路由引擎规则、导入/构建 JSON 契约、站点项目 UI 原型;准备 3 个试点样本(WordPress 迁移、新站模板、新数据批量导入)。 + +### 阶段 1 — MVP 网站工厂(建议 6–10 周) + +**必须同时具备“新建”和“迁移”两条路径。** + +- 站点项目管理、主题目录(至少两套不同设计)、站点蓝图。 +- 内容模型(页面/文章/产品/分类、可扩展字段)、媒体上传与基础编辑。 +- 模板新建、CSV/Excel 导入、WordPress 核心内容导入。 +- 页面装配/静态构建、基本 URL/SEO 模式、预览与 ZIP 导出。 +- 自动验收、单站版本管理。 + +验收:同一系统能生产一个无旧站的新网站和一个 WP 迁移重建网站;两站视觉结构不同且可独立访问。 + +### 阶段 2 — 可规模化多站点运营(建议 6–10 周) + +- 通用网站爬虫与插件适配器、字段映射工作台。 +- 主题兼容性检测、区块式编辑器、页面/菜单可视化设置。 +- 增量导入、媒体处理、版本化部署与一键回滚。 +- URL/SEO 完整旧新站差异报告、批量质量门禁。 +- 多站点、多权限、多环境和站点克隆结构。 + +验收:至少一个非 WordPress 站可迁移,模板与数据独立升级可运行。 + +### 阶段 3 — 完整模板工厂与自动化(按实际业务决定) + +- 可视化页面模板编辑、组合式区块库。 +- 自定义主题 SDK 和内部主题市场、模板版本升级迁移。 +- API 数据同步、任务计划、依赖图增量构建。 +- AI 内容分类、字段匹配和页面布局建议(人工审核)。 +- 更多 CMS / ERP 适配器和复杂组件。 + +> 时间均为粗略研发规划区间,不是工期承诺;应基于团队人数、模板复杂度、实际旧站数据和试点验收调整。 + +--- + +## 19. 研发团队协作与目录建议 + +```text +siteforge/ +├── apps/ +│ ├── admin-web/ # Vue 管理后台 +│ └── api/ # FastAPI 主服务 +├── workers/ +│ ├── imports/ # 导入与数据清洗 +│ ├── crawler/ # 旧站抓取和 CMS 适配 +│ ├── media/ # 媒体处理 +│ ├── builder/ # Astro 构建服务 +│ └── qa/ # URL/SEO/功能验收 +├── packages/ +│ ├── blueprint-schema/ # 站点蓝图协议 +│ ├── content-schema/ # 内容与字段协议 +│ ├── adapter-sdk/ # 数据源接入接口 +│ ├── theme-sdk/ # 主题数据绑定/组件接口 +│ ├── route-engine/ # 路由规则与映射 +│ └── build-contract/ # 输入/输出 manifest +├── themes/ +│ ├── corporate-clean/ +│ ├── automotive-premium/ +│ └── industrial-b2b/ +├── migrations/ # 数据库演进 +├── docs/ # PRD、ADR、API、模板规范 +├── tests/ # 完整端到端样本 +└── infra/ # Docker、Nginx、CDN、CI +``` + +### 19.1 设计/数据/研发协作界面 + +- 设计师交付主题原型与组件规范。 +- 研发把组件封装成可配置主题,声明需要哪些数据字段。 +- 内容人员录入内容和映射字段,不需修改代码。 +- 项目负责人检查预览并发布。 +- 可选开发人员继续以 SDK 开发特殊功能,保持独立品牌差异。 + +### 19.2 文档最小集合 + +PRD、站点蓝图 JSON Schema、Theme Manifest、组件数据绑定规范、适配器 SDK、数据模型 ERD、API OpenAPI、任务状态机、部署/回滚手册、验收测试用例。 + +--- + +## 20. 首批试点与详细验收用例 + +### 试点 A:旧 WordPress 汽车网站 → 新汽车主题 + +提供 WordPress XML + SQL/媒体(若可用)+ 网站 URL;输出文章/车型/品牌/分类和 SEO;选择全新汽车主题;旧站现存核心 URL 不改变;首页/品牌/车型详情样式与旧站不同;确认可离线发布。 + +### 试点 B:新建工业品网站(无任何旧站) + +选择工业主题 → 添加公司 Logo/简介/联系方式 → 批量上传管材 Excel 和图片 → 自动生成产品分类与详情 → 配置联系页面 → 预览 → 以纯静态文件发布。 + +### 试点 C:已有网站 + 新数据混合 + +在 A 项目后续导入新一批产品 CSV:仅新增/更新相应产品,不覆盖旧文章与人工编辑;新增页面按新规则生成路由;原迁移 URL 不改变;系统更新受影响页面与 sitemap。 + +### 试点 D:空白页面 → 保存为模板 → 复用到第二个网站 + +从空白建立官网页面树和首页组件结构,绑定内容字段,保存为可复用模板;新项目使用该模板和另一套公司资料生成不同网站,不复制试点的私有内容。 + +**上述四个试点是网站工厂成功与否的核心验收,不允许用“只完成 WP 迁移”代替。** + +--- + +## 21. 风险与关键决策 + +| 风险/问题 | 对策 | +|---|---| +| 模板不适配导入数据 | 主题声明字段和页面能力,预检查并引导补齐字段 | +| “一键搬家”遗漏旧数据 | 多源交叉提取、数据覆盖率报告、人工审核和异常列表 | +| 新主题导致旧 URL 改变 | URL 注册表独立于主题、迁移模式发布门禁 | +| 模板升级覆盖独立设计 | 主题版本锁定、兼容检测、差异预览、独立站点覆盖层 | +| 内容修改无法及时更新静态站 | 事件触发构建、预览与人工或自动发布策略 | +| 大站生成耗时过长 | 静态构建分批、缓存、增量依赖、构建 Worker 并发 | +| 复杂业务功能无法静态化 | 独立服务/API 适配器、功能清单和迁移边界确认 | +| AI 生成错误产品参数或公司事实 | 不凭空补全、标记来源和置信度、人工审阅 | +| 运营长期绑定平台无法退出 | 保证完整静态包、资源与服务器规则可导出 | + +### 21.1 推荐冻结的架构决策 + +1. Website Factory 为总产品;Migration Center 仅是子模块。 +2. 模板和内容分离;Site Blueprint 负责装配。 +3. 四种同级建站入口;导入在网站创建后随时可用。 +4. “旧站 SEO 保真”与“新站 SEO 生成”是两种独立策略。 +5. 线上输出优先真正静态文件;额外动态能力作为可选扩展。 +6. 站点、内容、媒体、主题、蓝图、构建物独立版本化。 +7. 第一阶段不追求任意像素自由拖拽,保留独立主题开发能力。 + +--- + +## 22. 可立即拆解的第一轮开发任务 + +### Sprint A:基础项目与协议 + +- 建立仓库、开发环境、CI、PostgreSQL、Redis、身份与网站项目模型。 +- 完成 Site Blueprint / Theme Manifest / Content Type Schema 草案及验证器。 +- 创建模板库 UI、四入口建站首页和站点工作台初版。 + +### Sprint B:内容 + 模板 + 构建闭环 + +- 手动创建产品、文章、基本页面和媒体。 +- 实现两种设计风格不同的主题,完成数据绑定和列表/详情页生成。 +- 输入规范 manifest,Astro 全量构建、预览站点和 ZIP 导出。 + +### Sprint C:数据导入 + WordPress 迁移 + +- CSV/Excel 字段映射、dry-run、幂等更新、异常行报告。 +- WordPress WXR/API 导入和图片资源迁移。 +- 旧站 URL/SEO 基线及 Preserve Mode。 + +### Sprint D:上线级质量闭环 + +- URL/SEO/媒体/内链验收、构建版本、发布审批和回滚。 +- 四项端到端试点验收;填写已知限制和改进清单。 + +--- + +## 23. 最终交付物清单 + +- SiteForge 管理后台(含四种建站入口)。 +- 通用内容中心、动态内容类型、媒体库、导入工作台。 +- 模板/主题/组件中心、站点蓝图、区块式页面配置。 +- WordPress 适配器、通用爬虫、Excel/CSV/JSON 导入能力。 +- 静态装配与构建引擎,包含 ZIP 导出与独立服务器部署。 +- 旧站路由与 SEO 比对、新站 SEO 自动生成、自动验收和质量报告。 +- 多网站环境/发布/回滚/权限/审计能力。 +- 至少三套不同业务风格的参考主题与必要主题 SDK 文档。 +- 四个试点通过报告、自动化测试、开发部署手册和数据恢复演练。 + +--- + +## 24. 一句话总结 + +**SiteForge 是“有内容就能造网站、没有内容也能从零造网站、有旧网站就能搬内容换设计、造完还能持续管理和再次生成”的通用网站工厂。** + +旧站迁移、内容管理、主题市场、页面设计、SEO、静态构建与发布,都是围绕“生产网站”这一核心能力服务的工序,而不是独立产品的拼接。 diff --git a/docs/superpowers/plans/2026-10-22-siteforge-completion-master-plan.md b/docs/superpowers/plans/2026-10-22-siteforge-completion-master-plan.md new file mode 100644 index 0000000..3512847 --- /dev/null +++ b/docs/superpowers/plans/2026-10-22-siteforge-completion-master-plan.md @@ -0,0 +1,133 @@ +# SiteForge 网站工厂补全 — 主计划(Master Plan) + +> **For agentic workers:** 本文档是路线图级主计划。每个 Phase 会各自展开为独立的任务级实施计划(TDD、bite-sized steps),按 superpowers:writing-plans 流程单独编写和评审后再执行。 + +**Goal:** 把现有 SiteForge V5(四种建站模式骨架已完成 ~70%)补全为需求定义的"智能网站生产工厂":四种模式名实相符、可组合、可随时修改、可一键交付。 + +**Architecture:** 全部在现有 FastAPI + SQLite + 声明式主题/区块架构上增量演进,不推倒重来。每个 Phase 独立交付可测试的软件,完成后系统始终可用。 + +**Tech Stack:** Python 3.12 / FastAPI / SQLite(Phase 5 前不换)/ Jinja2 / 原生 JS / Docker Compose + +--- + +## 前提与决策点(实施前需用户确认) + +| # | 决策 | 推荐 | 影响 | +|---|---|---|---| +| D1 | 多语言方案 | **站点级复制 + hreflang 互链**(一个语言一个 site,跨站关联),不改 entries 表结构 | 单站多语言混存需重构 route 唯一约束和全部主题模板,代价高且当前无此需求证据 | +| D2 | 数据库连接器首发 | **MySQL 只读**(WordPress 老站生态主流) | 决定 Phase 2 连接器实现顺序 | +| D3 | PostgreSQL 迁移时机 | **推迟到 Phase 5**(试点验证后再做) | 提前做会拖慢所有功能迭代 | +| D4 | Phase 顺序 | 1→2→3→4→5 | Phase 1 最快见效(纯编排已有 API) | + +## 工程前提(阻塞项,最先做) + +- [ ] **P0-1: `git init`** —— 当前项目不是 git 仓库,任何实施前必须先有版本控制(.gitignore 已存在,需补 `storage/`、`.env`、`*.png` 截图) +- [ ] **P0-2: 修复实测发现的 3 个缺陷**: + - compose.yaml 双服务同标签构建冲突:`docker compose up -d --build` 失败(已验证规避法:先 `docker compose build siteforge`)。修法:worker 服务加 `build: false` 或 image 复用策略 + - worker 容器 unhealthy:worker 服务加 `healthcheck: disable: true`(继承镜像 HEALTHCHECK 探测 8787 所致) + - favicon.ico 404:补一个内置 favicon 路由 +- [ ] **P0-3: CI 自检脚本**(`scripts/ci.sh`:pytest + compileall + node --check,与 README 自检命令一致) + +--- + +## Phase 1 — 模式 A 补全:迁移向导("输入旧站地址"一站式) + +**解决缺口:** 模式 A 现在要手动 5 步;需求是输入地址后系统自动编排。无表结构变更,纯编排层,最快见效。 + +**Files:** +- Create: `app/migration_wizard.py`(向导状态机 + 编排逻辑) +- Create: `app/static/wizard.js`(前端步骤面板,挂在 factory-v2) +- Modify: `app/factory_api.py`(挂载向导路由) +- Modify: `app/db.py`(wizard_state 表) +- Test: `tests/test_migration_wizard.py` + +**功能拆解:** +1. `POST /api/factory/sites/{site}/wizard/start` `{url}` → 调 `adapters.analyze()` 识别平台 → 返回推荐路径(WP REST / WXR 上传 / 爬虫 / 基线上传) +2. 向导状态机(pending → analyzed → importing → baseline → media → done),每步可重试、可跳过,状态落库 +3. WP REST 全量分页导入(现有 1000 条上限 → 10000,走 worker 队列异步 + 进度查询) +4. 媒体自动发现:导入完成后自动生成 assets inventory(现有 API),向导里展示"待本地化图片清单"并提供构建时镜像开关 +5. sitemap.xml 自动拉取生成基线(当前基线只能手动上传文件;加一个"从 URL 拉取 sitemap"入口,受 ensure_public_url 约束) +6. 爬虫容量 60 → 200 页,带 robots.txt 检查 + +**验收标准:** 输入一个真实 WordPress 站地址,向导引导完成 内容导入→基线→媒体清单→构建→审计报告 全流程,中途退出可恢复状态。 + +--- + +## Phase 2 — 模式 C 补全:数据源连接器 + 自动匹配 + +**解决缺口:** XML 不支持、数据库/API 连接完全没有、"自动识别类型/自动匹配模板"没有。 + +**Files:** +- Modify: `app/table_loader.py`(+XML 行解析) +- Create: `app/connectors.py`(MySQL/PG 只读连接器框架 + 凭据加密存储) +- Create: `app/content_sense.py`(列名/值特征打分 → 内容类型 + 行业主题推荐) +- Modify: `app/importers.py`、`app/factory_api.py`(连接器端点、批量导入走 worker) +- Modify: `app/worker.py`(新 job kind: `import`) +- Test: `tests/test_connectors.py`、`tests/test_content_sense.py` + +**功能拆解:** +1. 通用 XML 数据导入(任意扁平/两层 XML → 行记录,走现有 mapping 管道) +2. MySQL 只读连接器:连接配置(主机/库/账号)用 Fernet 加密存库;只允许 `SELECT`,白名单表,单表 ≤10000 行,超时与行数硬上限;预览表结构 → 选表选列 → 进 mapping 管道(复用 dry-run/commit) +3. PostgreSQL 连接器(同框架第二实现) +4. `content_sense`:对解析后的 columns+样本行打分(品牌/价格/续航→vehicle;作者/日期→post;案例地点→case),输出推荐 content_type 与推荐主题(automotive/industrial/corporate/minimal 关键词匹配),前端"智能推荐"面板一键采纳 +5. 大批量:mapping commit 改为分批(500/批)入 worker 队列,前端进度条;上限 1000 → 50000 + +**验收标准:** 上传一份 5000 行车型 Excel → 系统推荐 vehicle 类型 + automotive 主题 → 异步导入完成 → 构建出含分类/品牌聚合页的完整站点。连一个测试 MySQL 库选表导入同样走通。 + +--- + +## Phase 3 — 模式 D 补全:编辑器区块扩充 + 整站模板 + +**解决缺口:** 区块仅 12 种;"保存为新模板"只有首页级。 + +**Files:** +- Modify: `app/blueprints.py`(新区块类型:carousel / video(白名单 embed) / tabs / pricing / map-static / form-link) +- Modify: `app/static/editor.js` + `app/static/blocks.css`(新区块编辑与渲染) +- Create: `app/site_templates.py`(整站模板存取:多页面 blocks + 导航 + 设置 + 主题 + 设计令牌) +- Modify: `app/db.py`(site_templates 表)、`app/factory_api.py`(导出/应用端点) +- Test: `tests/test_site_templates.py`、tests/test_factory.py 扩展 + +**功能拆解:** +1. 6 个新区块(全部声明式、白名单校验,video 仅允许 https YouTube/Vimeo embed URL,map 用静态图片不引第三方 JS——延续"不执行用户代码"红线) +2. 整站模板:`POST /api/factory/sites/{site}/export-template` 把整站设计打包 JSON;建站时可选整站模板初始化(区别于现有首页级 preset) +3. 模板兼容性检查(复用 theme compatibility 思路:区块类型是否被当前版本支持) +4. (可选,需确认)草稿模式:entry status=draft 不进构建 —— 改动面大,默认不做 + +**验收标准:** 从零设计一个完整站点(多页+导航+设计令牌)→ 一键存为整站模板 → 新建站点套用该模板 → 构建结果与原站视觉一致。新 taxonomy 区块在编辑器和构建产物中渲染正确。 + +--- + +## Phase 4 — 横向刚需:多语言 + 发布连接器 + +**解决缺口:** 出口外贸场景的多语言(D1 方案);重新发布的"一键上线"。 + +**Files:** +- Create: `app/multilang.py`(站点语言属性 + hreflang 互链 + 一键克隆翻译站) +- Create: `app/publish_targets.py`(SFTP 与 S3 兼容目标:凭据加密、连通性测试、发布/回滚记录) +- Modify: `app/db.py`(sites.locale、site_relations、publish_targets、publish_history 表) +- Modify: `app/builder.py`(sitemap 输出 hreflang alternates) +- Modify: `app/static/factory-v2.js`(语言管理面板、发布目标面板) +- Test: `tests/test_multilang.py`、`tests/test_publish_targets.py` + +**功能拆解:** +1. 站点 locale 字段 + 语言变体关系组;克隆为翻译站(复制结构、内容置为待翻译占位,不机器翻译、不伪造) +2. 构建时各语言站 sitemap 互相输出 hreflang;语言切换器区块(白名单) +3. SFTP 发布目标:host/账号/路径,密码加密存储;"发布到此目标"= 上传当前 release 目录(原子:先传 `.new` 再 rename),发布历史落库,失败回滚提示 +4. S3 兼容目标(endpoint/key/secret/bucket/prefix),同样走审批门禁后才可发布 +5. 发布动作复用现有 release_requests 审批流(未审批的构建禁止推送到外部目标) + +**验收标准:** 一个中文站克隆出英文变体站,两站 sitemap 含正确 hreflang;任一构建版本经审批后一键 SFTP 推送到测试服务器并可访问。 + +--- + +## Phase 5 — 生产化加固(引用 docs/PRODUCTION_ROADMAP.md,单独立项) + +PostgreSQL/Redis、SSO/MFA、审计外置、E2E 视觉回归、供应链与渗透测试、监控 SLO。**不在本主计划展开**,功能补全(Phase 1-4)验证业务闭环后再启动,避免过早优化。 + +--- + +## 执行方式 + +- 每个 Phase 开始前:按 superpowers:writing-plans 写任务级详细计划(含 TDD 步骤与完整代码)→ 评审 → 按 subagent-driven 或 inline 方式执行 +- 每个 Phase 结束:`scripts/ci.sh` 全绿 + Docker 实测启动 + 浏览器验收(复用本次会话已验证的冒烟流程) +- 顺序建议:P0(0.5 天)→ Phase 1(最快见效)→ Phase 2 → Phase 3 → Phase 4;Phase 2/3 可并行(不同子系统) diff --git a/examples/content.json b/examples/content.json new file mode 100644 index 0000000..19a13d0 --- /dev/null +++ b/examples/content.json @@ -0,0 +1,20 @@ +{ + "entries": [ + { + "type": "page", + "title": "About Us", + "route": "/about-us/", + "summary": "Learn more about our global business.", + "body": "

Who we are

Replace this example content with your real company introduction.

", + "seo_title": "About Us", + "seo_description": "Our company introduction." + }, + { + "type": "service", + "title": "Global Shipping", + "route": "/services/global-shipping/", + "summary": "Integrated shipping and delivery coordination.", + "body": "

Describe your real shipping services here.

" + } + ] +} diff --git a/examples/declarative-theme-1.0.0.zip b/examples/declarative-theme-1.0.0.zip new file mode 100644 index 0000000000000000000000000000000000000000..faa02f7f0d0d647511008411faaf0957e26d8e0e GIT binary patch literal 4090 zcmZ{nWmFX4wuT27qy&bPZt0X7N{Jy=x{;D@hM{F(04eDbL>i<+nvpJHsG&@bJsfetb5Ph`^UHTUf=s`ul26?(S3>sqy_*0gaGwtMwV7z1!7`x0Dvl70D$!G z*2C7*#nA!gE9?sM@_ep0p@k<#*LlhJHowI%*$Usfa6`4!zRCK_lzSuhSK9bM5cO?k z>~|%SD03irgmlr~O8K(?L1n$or(MrWYR9ejs1TGU3OYJ2a0GT(JM`hUnI&zJX%_Yp zlvD~at&*&kh5FJa-=L1*CwENAVOu5Ts=n_!Lx?xDUM++tym=_b^-!&uXIRnTN|9W6PDM~Io~ zom#CH{g#@~!aDEuD{(6cBd|(^du2E87&2+EJ4Ndgy=_wZmT+15eOubtUkJNclqay0 z85k($=&Jw3^E@HhdV9uxN?{V6jl+*mnWHi(@o0M7rm*xuol$X$UOoZx4WS4g11TH- zu<}clXf<@la(9$=p}kZsod-M9wzMR|rb6+-j9$GQj7;Kp7tKIO$E9qc=6FKTqF^!; z)|2b%aqJr;JMAG6A%4rDJ3mn0dBN& z@%a$}03i7Q03iB%GGAv04+mj880>@TSXya2&DxDXEUF?~i@+YBEYHdP16#vv70Abx zilQb0rdXP(USZ?R1 z#_&73Q^o5zVO?PGAlY#FD@@|?@s+^wD! z8iZa85?S`vR9J)uv* zX)3v2MQ9F_^0FCwhp)yMP9gqKbZE!9idhJlx)EQF56dy1ps3_VU~GM}yYQ~Layv4} zjE+}02Jy{lh#>ICnNU&j>N-)&rJpyge6^npSu~G>oWBYA>9HYCKE{dEMB+-{_ncnp`|sogTS!fKZZD1B{tVwE17_YV;jWZ&AN(P z#XuB0&#*0Zs(RFFlD{X`i8N9tWddGj+8oTQWFI%ddxJEPZmb*(qHmJk-D@}X-#5^C zYi+bxg=&LlW(w+et`B+yDKAb8zTEDLod2Heg67!$aCJ|v9M&sVmLaeGa|9I={32Wg zq1Nx%DrEbt7?^ARvQ28*OZil~NVQJxm41aj^EbmzM0dQ=o~3xGvD(*62SHJ9Ra@4; z$sj#boY3%BVxoi!jvRf(uIsZ;QFQg0dg|^cghDdsGD((rl9agLlkoruIXr?w<2JXo za=I>4rLz;D!Ac`r47BPw&{$}sNhqA4+f}NwkZ6;Q-FN;s6YNFG^TY;vKeMM>qt(&f z6CHSd&GMbK3jRjT3q#`l#Wcv)dOH76GM&4&0J)@<8|XlcXS*AC`e)8$iU_5#x9O5- zMh}m+_ALl@&?8yR0c!H6)e(u(#0*8*QVN8!8H#%M9NpIewnTR)P1EN{sfGu39B8#) z+7<+}o#ymmJ7M49W8~e+=dIuD9a@%+{uF{^sXWin1Kyt)tw)`RpE;gCH+r<+i@1^T zKA6IR`LyDvx4y>^jRi1b&^Mg0WRBDhsZw*V(UlfTv-wU|S>9R|* z__B^NOK|{2mu%XpFT}wT4_=)TBF^<^SXntuES{r*e2`N$ynjkrY;`~)fk?B0=6lvA zl_~8BD`^54o4GM=(pTwE80OpjJ9FHJS8S`I^Dbj+zPS|42bt)nd&MRH_j(^l$_WFR2G5`~5gugX z8Aqo10=wmUGVIYu_#$wNzv`U=loUY%3 z?-%k=B1X1;dEt}aaiS}MP9t=VdQlHjmFvdYGtw;8ui-?$nN0<<@p=f4v`}_3kto~jrOY_ciF0&u$`Y6aVz0)=C zc0FHC@m4>-`T7NI|7|L@pwS@CRjk@ve1Rmo$0}+M%r8flV1ZZz?qHrC*BWw8(DQ$U zYuvtuUj91An1`T{ExBjLRM00~A^cg;6;`UR|@Pp>{P+Ao&6G{O;hvyF% zrwubUoT#rzimn(%D-k@3>0$mwz#3@}sYc_!H+<<~XB)IPn}RpCja{brY;r+K zJ#@@-Q92^M9l<9{b>%IM2q+gnka;-7Mqc)&EbQ*6=g%pu)x^&g1WtSN`oVB|qD@LO zWkxYUbn+Pn*+j5!Mxeks@A5n6_Hwj0Ov*KD5%udV_o7S0WJE=tefb*P_{5&4fVI&M zI^xKrQ8B}X%;JCFDsFOwLB6(}mU!v)%sr6qjNj7M4i0?#i1}KrdvA4x_Wo_97sSnC ze`5K@9HgzR%vVtGCT|K)r+aP|0j)wyod!x zLVXnr>AHIf3d_>R@Wc&U-~C%E&{s3Ev_X}!$l(J3%(MUi#otumZR_M9BdU#Q(gYG8lKi;-blSdWN&V3%gV)M?R2WC>TwMdaeHx* zi4ghSf9QhH_y7fIinaDVDQkPRt#cc7xJLgaMK1k8VRMzeS@-haa&pXnce<0R$Og{U zD%@BMFIYNKw3)kd)PHW<$d&;>$ZyyPE?xTLTQfGCM-FJ{_?4QpG;LGa{3p-IR3={Rf zI7@yYTScs;V#(NN&hO%uoKz;6kn~cNdNIw}y{3fMNz~u3X2P#Jx7;9J?ZCweZmM3@ z6?}iB^8VumGXT_MWch(QKbh=I!()*Y5UY}yaFzFrXG$}WPT55Cn*NBH{~l3Lm%D^< zZQjEsux6cCb`+6*_9LszgliEs_!q{H;qlvePtM56F35AwqvGT{-0DmcwNl4xX7Zpg zySJ-3B_H1Zm=?rn+HKdn*EKCe*VixdceKn6n2>d(q;?_|iLUFxCybWM-219w6|Zcn zDk!v#@wG`0K1(FxXdf|a%SwAL+_90wUtr_Y9TVZ6zH5XfkWYDk*6P|9l;k(FiC}Q1 zVfg&|PR-}=gzuB=WYNBUqk#&H?rsU^@ZJiHTZ*AisbLGt_C)3fI34vZsXveIu+9D= zmZgD^NZ0t+N8X~f21I2blM+wY1rw;``t>S{H2Yp+WiZP7bke2#LL`qYmtn_T z$H8}p8+WGO_2XD|``al|i@4^8<2}zfT!u}4Y8YJab2dqg5!c8xd`m6k4Aw&RuE+65 z2c>R*0F?(LBKsa!-Cc|8XP2D1ZC&J8fbEOipLd?zix!+T#kypTIeSK8{hBg9pnVd| zE!mexXQLvHbEe&sqVoCL!94~WmaE{<;Sj4+tXj6#%PFM@xOHo0gf{z)B)M5s+IZtV zAJ&)$T~{}7V)>xM)vNIIulZM(Fg8fd7SJpXLXHk6bM_Zs4pDS8zH{n%+o$l6HTE@E zAp~FK!hoWeo|%~or1qQToy!jy5c5gRs$=Co?%knf-pfWw*Hg?ww2>93frXmiTE*Ip zXautrQo+;1i!SkI;w5T01WN%ob&|FTo?s;L#6Q>uI^~L43*|@_P_61sSFGBH>~KeT z4X!}uIC!7%6BAKz#jzXx%(n9*B|A!wl}G7o^DX|E-{j<}IU#AhmP0kU+rjIWW~_rX z_eg3EQfFw91rCC++oh2>DK#Toxe04)1@N3$S;(1 zLG+x&qWpnp8Ifn01woVdl5W!E)9LE!>PyS&v+p3dJBy#Jzpngl`+#d;lG^?Bo4D_$ zE;w2df+!xgVCgkoTr#`a1q%Btq-%aIlj0M^?5x#Vk^S`H<>C{QKlQ)I@7J;YV% zAC5H=V3<6W3-dUOHU45?MxZ$q0C9qV$cCwx2hfpI;RWS?RFy%hsqw|UZ-VAFm2xNq z)M3GgO;RN~r}?Ye3KwZtA4gPDI5p~WO403Bv z`}B@MXlv)B!1szB9=f>VE|DeugzAgrVM->!#PHK`(qu*dA|Lx7Ub;_laH;YBe`x+| zxBo1}Uy%M^2>tKU{{(OUEd&5c!<+t!|Alh@F8fdJ{y#F*Uj+O=Ua$KU|K2~o;Qj4} Lzpps?Kc@cz5K@|^ literal 0 HcmV?d00001 diff --git a/examples/products.csv b/examples/products.csv new file mode 100644 index 0000000..1758ffb --- /dev/null +++ b/examples/products.csv @@ -0,0 +1,4 @@ +type,title,route,summary,seo_title,seo_description,cover +product,Electric SUV Model A,/vehicles/model-a/,Modern electric SUV,Model A | Electric SUV,Discover Model A, +product,Pickup Truck Model B,/vehicles/model-b/,Practical pickup for global markets,Model B | Pickup,Explore Model B, +product,Premium SUV Model C,/vehicles/model-c/,Comfortable and versatile premium SUV,Model C | SUV,Explore Model C, diff --git a/examples/professional-theme-1.0.0.zip b/examples/professional-theme-1.0.0.zip new file mode 100644 index 0000000000000000000000000000000000000000..cbc9ed8efe8dddf1c2ce37ed87ea778808f0da8f GIT binary patch literal 2315 zcmZ`*c{CL48XwCr)MU-RW^83k!icf&`;aUdduEJb7!n${5EZWMyFr6yH1=c(*~^|~ z=tGvtxCo_0c25hvq zI_3Hm0^91f@n;KqS5|I?fA#rt)I)ytyh3*Ro6ptbh@;ixsF<6bM{b0isv6Uw;D(Kp zs=ZfXQv%>gZP{wyr*?!<^82~N!+MymTVV``7#~s9_-?X3{@dpFPvl}TD!(%It8KL< zKD1WBqfN;=NkciOfiSdt=gW`ON%5!Z!dpji?Onx&U~8jIXWpX+#hRSb?<;{g3X9aB zQ4Q7kXIe_2mDDiybuwy_?_s>RtyPlqQi$cCWS^v0_L&k`BvEyKR{{>iFjYVpzLqW} zG+W`RWMY7i8d9|1_y#y%lLw($@4*w{NaJDI%e^8l^Lfhu4O}le+cEQ5H~HYvd}h13 zT@~g9J}FkuIsa^#XK;W(h@i%q?d97fwtTv)-dUhk{^EMMsH&9&txSjK^Gm^P?M^1U zzY%TqR2poH4<_Ir0Af!Q=ix>$9r@D#c_A z{9CG7prieM1&RDK-hbpg7>yJ3U!O##B?tWkWakTc}HS! z%QZ_cIfg9zw|aT^n#KDhYo>y;57)e;pmTz#Yg%!Oi=YRItnSOf3GO#~O2?O`3k;WQ z<^ucTdj+ybFfI;Uk{90~WyXc{C1uL$G=+M$JdgGs8rdq%{XjD7#?l5@RL&kf-z$4` zb|tcRwto41kZ)#BWW=FJ(0ZB3r%VnbVb#)8Hb zI2+doHmb-!$Y4;iK!&^}q?yA!9v;n3-x#nlN)(9THJ&umwQ4mE=h9X{ zmTSMsEVzh=#Aa7(=TmQ=u{d_|xw^L0vC9anDHWAy>u;C-%DmSQYY&@>V-1-00#Z88 z@q5T+9eGA}L1KOST@9vqIq$F$N-H5ssjI(AD=VE)+3D6H;Z%>@D(cD+`h1cC~r$*cTgY6 zd0^T}G(k7Yx9YsSqWxv6y;Gfpc*(5Z6J1d#_VKq)IgmgqYb?ZcR~>#RDa|Ho8h*P= zJICKK88+%)#i4m%?$MJW~&u%JJ z&^y(7kJDs3%`l`N#!txyhikYvIHpXZPAkd{&RAR7*o<7NgWJ^A)~a2ux<1atc8e%e zLF8MP$$>b@O3V0H@~`I5%S%Y+7Zv5-j`hfJ0PHLo7MVZo6u&}$ u-An%l0s!|I)=$Czv!#B8|2pD-;0vt(HL#r}kmb)Wrqif9y^TPBzWxAN*$rv{ literal 0 HcmV?d00001 diff --git a/integrations/art-design-pro/README.md b/integrations/art-design-pro/README.md new file mode 100644 index 0000000..a62db44 --- /dev/null +++ b/integrations/art-design-pro/README.md @@ -0,0 +1,28 @@ +# Art Design Pro 官方工程对接(可执行安装器) + +SiteForge V2 随包提供三个 Vue 3 + Element Plus 业务页面:`WebsiteFactory.vue`、`ThemeCenter.vue`、`EnterpriseCenter.vue`。 + +> **重要:** 由于本次代码执行环境无法访问 GitHub 与 npm,无法直接打包或编译 Art Design Pro 官方完整仓库。以下对接脚本已实现源码复制和路由注册逻辑,但尚未针对官方实际项目执行 `pnpm build`。默认可运行的 `/factory-studio` Web 管理台不依赖这些文件。 + +## 在联网设备对接 + +1. 依官方文档克隆 `https://github.com/Daymychen/art-design-pro`,使用符合要求的 Node 版本和 pnpm。 +2. 在 SiteForge 根目录执行: + + ```bash + python scripts/install_into_art_design_pro.py /path/to/art-design-pro + python scripts/install_into_art_design_pro.py /path/to/art-design-pro --apply + ``` + +3. 使用同域反向代理: + + - Art 前端 `/` 为网站工厂管理 UI + - `/api` → SiteForge FastAPI + - `/editor`、`/preview`、`/ui`、`/local-media`、`/login`、`/factory-studio` → SiteForge FastAPI + +4. 在 Art 工程目录执行 `pnpm install && pnpm build`,访问 `/siteforge/factory`、`/siteforge/themes`、`/siteforge/enterprise`。 +5. SiteForge API 仍使用自己的服务端 Session、CSRF、站点级 RBAC;前端的隐藏菜单不构成授权。`/api/auth/me` 必须在同一 Cookie 域名可访问。正式发布需统一登录体验,并补齐 Art 当前版本的路由/权限约定。 + +安装脚本不会覆盖已有配置而不备份:原 `asyncRoutes.ts` 会先保存 `.ts.siteforge-backup`,同名 Vue 文件另存 `.vue.siteforge-backup`。重复执行不会重复插入路由。 + +官方路由结构和本项目的适配规则以 [Art Design Pro 官方路由文档](https://www.artd.pro/docs/zh/guide/essentials/route.html) 为准。 diff --git a/integrations/art-design-pro/src/views/siteforge/EnterpriseCenter.vue b/integrations/art-design-pro/src/views/siteforge/EnterpriseCenter.vue new file mode 100644 index 0000000..facef24 --- /dev/null +++ b/integrations/art-design-pro/src/views/siteforge/EnterpriseCenter.vue @@ -0,0 +1,33 @@ + + + diff --git a/integrations/art-design-pro/src/views/siteforge/ThemeCenter.vue b/integrations/art-design-pro/src/views/siteforge/ThemeCenter.vue new file mode 100644 index 0000000..e437650 --- /dev/null +++ b/integrations/art-design-pro/src/views/siteforge/ThemeCenter.vue @@ -0,0 +1,34 @@ + + + diff --git a/integrations/art-design-pro/src/views/siteforge/WebsiteFactory.vue b/integrations/art-design-pro/src/views/siteforge/WebsiteFactory.vue new file mode 100644 index 0000000..1f5947d --- /dev/null +++ b/integrations/art-design-pro/src/views/siteforge/WebsiteFactory.vue @@ -0,0 +1,71 @@ + + + diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..c7b23ec --- /dev/null +++ b/pytest.ini @@ -0,0 +1,3 @@ +[pytest] +pythonpath = . +testpaths = tests diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..4596645 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,3 @@ +-r requirements.txt +pytest==8.4.2 +httpx2==2.16.0 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..61a533d --- /dev/null +++ b/requirements.txt @@ -0,0 +1,14 @@ +# Runtime versions are pinned so Docker rebuilds do not silently select a new +# framework generation. Update these deliberately and run scripts/check.ps1. +fastapi==0.141.1 +starlette==1.4.1 +pydantic==2.13.4 +uvicorn[standard]==0.52.1 +jinja2==3.1.6 +python-multipart==0.0.32 +httpx==0.28.1 +beautifulsoup4==4.15.0 +bleach==6.4.0 +defusedxml==0.7.1 +openpyxl==3.1.5 +itsdangerous==2.2.0 diff --git a/scripts/check.ps1 b/scripts/check.ps1 new file mode 100644 index 0000000..2c4821d --- /dev/null +++ b/scripts/check.ps1 @@ -0,0 +1,25 @@ +param( + [string]$Python = "python" +) + +$ErrorActionPreference = "Stop" + +& $Python -m pip check +if ($LASTEXITCODE -ne 0) { throw "Python dependency check failed" } + +& $Python -m compileall -q app +if ($LASTEXITCODE -ne 0) { throw "Python compilation failed" } + +& $Python -m pytest -q --basetemp=.pytest_cache/check-tmp +if ($LASTEXITCODE -ne 0) { throw "Pytest failed" } + +if (Get-Command node -ErrorAction SilentlyContinue) { + foreach ($file in @("app/static/admin.js", "app/static/editor.js", "app/static/factory-v2.js", "app/static/pro.js")) { + & node --check $file + if ($LASTEXITCODE -ne 0) { throw "JavaScript syntax check failed: $file" } + } +} else { + Write-Warning "Node.js is unavailable; JavaScript syntax checks were skipped." +} + +Write-Host "SiteForge V5 checks passed." diff --git a/scripts/init_config.py b/scripts/init_config.py new file mode 100644 index 0000000..47527b7 --- /dev/null +++ b/scripts/init_config.py @@ -0,0 +1,19 @@ +"""Generate a local .env using only Docker's Python image (no host Python needed).""" +import os +import secrets +from pathlib import Path + +root=Path(__file__).resolve().parent.parent +config=root/'.env' +if config.exists(): + print('已存在 .env:为了防止覆盖现有密码和密钥,本次不做修改。') + raise SystemExit(0) +password=secrets.token_urlsafe(24) +secret=secrets.token_urlsafe(48) +config.write_text(f'''# SiteForge Web Docker v0.3 - 不要上传至代码仓库\nSITEFORGE_PORT=8787\nSITEFORGE_ADMIN_PASSWORD={password}\nSITEFORGE_SESSION_SECRET={secret}\nSITEFORGE_COOKIE_SECURE=false\nSITEFORGE_DOMAIN=siteforge.example.com\n''', encoding='utf-8') +try:os.chmod(config,0o600) +except OSError:pass +print('SiteForge 已生成 .env 配置。') +print('首次登录管理员密码:'+password) +print('请将密码保存在安全的位置,忘记后可编辑 .env 的密码并重启容器。') +print('本机启动命令:docker compose up -d --build') diff --git a/scripts/install_into_art_design_pro.py b/scripts/install_into_art_design_pro.py new file mode 100644 index 0000000..c749a34 --- /dev/null +++ b/scripts/install_into_art_design_pro.py @@ -0,0 +1,40 @@ +"""Offline installer for SiteForge components into a user-provided official Art Design Pro checkout. + +Run with --dry-run first. It does NOT download, modify auth schemes, or +pretend to have compiled the official repository in this sandbox. +""" +import argparse,shutil,re +from pathlib import Path + +SOURCE=Path(__file__).resolve().parents[1]/'integrations'/'art-design-pro'/'src'/'views'/'siteforge' +MARKER='// SITEFORGE_FACTORY_V2_REGISTERED' +ROUTES='''\n // SITEFORGE_FACTORY_V2_REGISTERED\n {\n path: '/siteforge', name: 'SiteForge', component: '/index/index',\n meta: {title:'网站工厂',icon:'ri:layout-grid-line'},\n children: [\n {path:'factory',name:'SiteForgeFactory',component:'/siteforge/WebsiteFactory',meta:{title:'网站项目'}},\n {path:'themes',name:'SiteForgeThemes',component:'/siteforge/ThemeCenter',meta:{title:'主题与版本'}},\n {path:'enterprise',name:'SiteForgeEnterprise',component:'/siteforge/EnterpriseCenter',meta:{title:'迁移与交付'}},\n ]\n },\n''' + +def install(root:Path,dry_run:bool): + base=root/'src'/'router'/'routes'/'asyncRoutes.ts' + target=root/'src'/'views'/'siteforge' + if not base.is_file() or not (root/'package.json').is_file():raise ValueError('不是受支持的 Art Design Pro 源码目录') + original=base.read_text(encoding='utf-8') + pat=r'(export\s+const\s+asyncRoutes\s*:[^=]*=\s*\[)' + if MARKER not in original: + modified,count=re.subn(pat,lambda m:m.group(1)+ROUTES,original,count=1) + if count!=1:raise ValueError('无法定位 asyncRoutes 定义;该官方版本的路由结构可能不同,请人工集成') + else:modified=original + print(f'官方工程: {root}\n文件目标: {target}\n待复制页面: {len(list(SOURCE.glob("*.vue")))}\n路由需要更新: {original!=modified}') + if dry_run:return + target.mkdir(parents=True,exist_ok=True) + for src in SOURCE.glob('*.vue'): + dest=target/src.name + if dest.exists():shutil.copy2(dest,dest.with_suffix('.vue.siteforge-backup')) + shutil.copy2(src,dest) + if modified!=original: + shutil.copy2(base,base.with_suffix('.ts.siteforge-backup')) + base.write_text(modified,encoding='utf-8') + print('业务页面与菜单已写入;下一步请配置 /api、/editor、/preview 代理并执行 pnpm build 完成验收。') + +if __name__=='__main__': + parser=argparse.ArgumentParser() + parser.add_argument('official_repo',type=Path) + parser.add_argument('--apply',action='store_true',help='执行源代码变更;不传则只检查') + args=parser.parse_args() + install(args.official_repo.resolve(),dry_run=not args.apply) diff --git a/scripts/make_offline_preview.py b/scripts/make_offline_preview.py new file mode 100644 index 0000000..1c8ba08 --- /dev/null +++ b/scripts/make_offline_preview.py @@ -0,0 +1,30 @@ +"""Self-contained HTML preview from the actual four built static sites. +It inlines production CSS; navigation/build/SEO remains in the accompanying ZIP. +""" +import json,re,base64 +from pathlib import Path +root=Path(__import__('sys').argv[1]).resolve() if len(__import__('sys').argv)>1 else Path(__file__).resolve().parent.parent/'demo_sites' +out=Path(__import__('sys').argv[2]).resolve() if len(__import__('sys').argv)>2 else Path(__file__).resolve().parent.parent/'theme-preview.html' +styles=['theme.css','blocks.css','factory.css','pro-base.css','pro.css','commercial.css','custom.css'] +data={} +for theme in ('automotive','industrial','corporate','minimal'): + base=root/theme + pages={'home':'index.html','products':'products/index.html','detail':'products/1/index.html','news':'news/index.html','case':'cases/1/index.html','about':'about/index.html','contact':'contact/index.html'} + data[theme]={} + for name,file in pages.items(): + src=(base/file).read_text(encoding='utf-8') + for css in styles: + text=(base/'assets'/css).read_text(encoding='utf-8') + visual=base/'assets'/'visual'/'hero.svg' + if visual.is_file(): + text=text.replace('./visual/hero.svg','data:image/svg+xml;base64,'+base64.b64encode(visual.read_bytes()).decode()) + src=src.replace(f'','') + src=src.replace('','') + src=re.sub(r'','',src) + data[theme][name]=src +payload=json.dumps(data,ensure_ascii=False).replace('SiteForge V5 · 四套商业视觉主题 · 真实构建预览 +
SiteForge V5 · 专业主题效果预览直接打开本文件 · 不需要 Docker · 展示真实构建的 HTML/CSS

演示包含虚构企业与产品数据。此单文件用于查看实际生成页面的视觉布局;完整网站的导航、搜索和筛选请在 Docker 或静态 Web 服务器部署演示站后体验。

+'''.replace('__PAYLOAD__',payload) +out.write_text(html,encoding='utf-8') +print(str(out),out.stat().st_size,'bytes') diff --git a/scripts/make_theme_demo.py b/scripts/make_theme_demo.py new file mode 100644 index 0000000..3273574 --- /dev/null +++ b/scripts/make_theme_demo.py @@ -0,0 +1,71 @@ +"""Build four fully static demonstration sites from the real V3 builder. +All sample names and content here are clearly fictitious demonstration data. +""" +import json, os, shutil +from pathlib import Path +BASE=Path(__file__).resolve().parent.parent +DATA=Path(os.environ.get('SITEFORGE_DATA_DIR',str(BASE/'demo_storage'))) +os.environ['SITEFORGE_DATA_DIR']=str(DATA) +from app import db,theme_store,builder,quality +from app.core import uid,save_entry +from app.factory_api import bootstrap_site + +def put(sid,typ,title,route,summary='',body='',category='',brand='',extra=None): + fields={'category':category,'brand':brand} + if extra:fields.update(extra) + return save_entry(sid,{'type':typ,'title':title,'route':route,'summary':summary,'body':body, + 'meta':{'fields':fields}},source='demo') + +def build_demo(): + db.init();theme_store.setup() + demos={ + 'automotive':('NOVA MOBILITY','探索更广阔的出行可能','汽车与出行演示主题','Cars'), + 'industrial':('ARC INDUSTRIES','面向现代工程的产品与服务','工业制造演示主题','Materials'), + 'corporate':('NORTHLINE GROUP','以专业的服务连接企业与未来','企业品牌演示主题','Services'), + 'minimal':('MONO STUDIO','少而精,让每一项作品更清晰','极简设计演示主题','Objects'), + } + out=Path(os.environ.get('SITEFORGE_DEMO_OUT',str(BASE/'demo_sites')));out.mkdir(parents=True,exist_ok=True) + results={} + for theme,(name,tagline,about,term) in demos.items(): + sid=uid() + settings={'nav':[{'title':x,'route':r} for x,r in [('首页','/'),('产品','/products/'),('品牌','/brands/'),('新闻','/news/'),('案例','/cases/'),('关于','/about/'),('联系','/contact/')]], + 'email':'hello@example.com','footer_text':'此为 SiteForge 主题演示网站。公司、产品及内容均为虚构示例。'} + db.insert('INSERT INTO sites VALUES (?,?,?,?,?,?,?,?,?,?,?)', + (sid,name,'https://'+theme+'.example.test',tagline,theme,'#315f92','', 'new',json.dumps(settings,ensure_ascii=False),db.now(),db.now())) + theme_store.pin(sid,theme,'1.1.0') + put(sid,'page','首页','/',tagline) + for i,(route,title,description) in enumerate([ + ('/about/','关于我们','企业介绍、发展理念、合作能力与团队故事。'), + ('/contact/','联系我们','欢迎通过邮件与我们的演示团队取得联系。'), + ('/products/','产品中心','查看系列、类别及产品资料。'), + ('/brands/','品牌中心','探索与产品相关的品牌。'), + ('/news/','新闻资讯','了解最新消息与团队动态。'), + ('/cases/','项目案例','精选项目展示。'), + ('/services/','服务与解决方案','从咨询到交付的全链路服务。'), + ('/faq/','常见问题','更多问题可以联系我们。')]): + put(sid,'page',title,route,description,'

本页为演示内容。您可以在网站工厂中直接修改和替换。

') + for i,(prod,cat,brand) in enumerate([ + (f'{term} Alpha','旗舰系列','NOVA'),(f'{term} Beta','旗舰系列','NOVA'), + (f'{term} Gamma','畅销系列','ARC'),(f'{term} Delta','畅销系列','ARC'), + (f'{term} Epsilon','定制系列','MONO'),(f'{term} Zeta','定制系列','MONO'), + (f'{term} Eta','专业系列','NOVA'),(f'{term} Theta','专业系列','ARC')]): + put(sid,'product',prod,f'/products/{i+1}/',f'{prod} — 针对多样应用的演示产品。', + '

产品说明

此处可以展示产品特点、应用场景和可用规格。

',cat,brand, + {'model':'MX-'+str(200+i),'material':'Example / Demo','version':'2026','energy':'EV' if theme=='automotive' else 'Standard'}) + for name_b in ('NOVA','ARC','MONO'): + put(sid,'brand',name_b,'/brands/'+name_b.lower()+'/',f'{name_b} 品牌产品与应用。') + for i in range(3): + put(sid,'post',f'企业动态 · {i+1:02d}',f'/news/{i+1}/','在这里发布新闻资讯与品牌动态。', + '

这是可替换的新闻正文内容,可包含图片、表格和链接。

',extra={'publish_date':f'2026-10-{11+i:02}'}) + put(sid,'case',f'精选项目 · {i+1:02d}',f'/cases/{i+1}/','展示项目背景、过程与成果。', + '

本页仅为项目结构示例,并非实际完成的工程项目。

',extra={'location':'Demo location','industry':'Demo industry'}) + put(sid,'service','供应与交付方案','/services/solution/','灵活、可配置的供应交付服务。') + r=builder.build(sid) + src=db.DATA/'builds'/sid/r['version']/'dist' + dst=out/theme + if dst.exists():shutil.rmtree(dst) + shutil.copytree(src,dst) + results[theme]={'page_count':r['pages'],'warnings':r['warnings'],'quality':quality.report(sid,r['version'])} + (out/'BUILD_REPORT.json').write_text(json.dumps(results,ensure_ascii=False,indent=2),encoding='utf-8') + print(json.dumps({k:{'pages':v['page_count'],'warnings':len(v['warnings']),'quality_errors':v['quality']['errors']} for k,v in results.items()},ensure_ascii=False,indent=2)) +if __name__=='__main__':build_demo() diff --git a/scripts/restore_backup.py b/scripts/restore_backup.py new file mode 100644 index 0000000..6982d93 --- /dev/null +++ b/scripts/restore_backup.py @@ -0,0 +1,56 @@ +"""Offline restore of SiteForge backups. STOP API and worker containers first! + +Usage: python scripts/restore_backup.py /path/to/backup.zip --confirmed-offline +The application must be stopped to prevent WAL writes during restore. +""" +import argparse +import os +import shutil +import sqlite3 +import stat +import tempfile +import zipfile +from pathlib import Path + +def restore(path, target, confirmed=False): + if not confirmed:raise RuntimeError('停止 API 与 worker 后,使用 --confirmed-offline 才能恢复') + target=Path(target).resolve();archive=Path(path).resolve() + if not archive.exists():raise FileNotFoundError(archive) + with zipfile.ZipFile(archive) as z: + infos=z.infolist() + if len(infos)>250000:raise ValueError('备份文件数量过大') + if sum(i.file_size for i in infos)>30_000_000_000:raise ValueError('解压数据总量过大') + for item in infos: + p=Path(item.filename) + if p.is_absolute() or any(x in ('..','.') for x in p.parts) or not p.parts: + raise ValueError('备份包含不安全路径') + if stat.S_ISLNK(item.external_attr>>16):raise ValueError('不允许符号链接') + if 'siteforge.sqlite3' not in z.namelist():raise ValueError('缺少数据库文件') + with tempfile.TemporaryDirectory(dir=target.parent) as tmp: + z.extractall(tmp) + restored=Path(tmp)/'siteforge.sqlite3' + with sqlite3.connect(restored) as db: + if db.execute('PRAGMA integrity_check').fetchone()[0]!='ok': + raise ValueError('备份数据库完整性校验失败') + target.mkdir(parents=True,exist_ok=True) + # Keep a copy of the old DB outside the restored data volume. + old_db=target/'siteforge.sqlite3' + if old_db.exists(): + dest=target.parent/'siteforge-pre-restore.sqlite3' + shutil.copy2(old_db,dest) + for special in ('siteforge.sqlite3-wal','siteforge.sqlite3-shm'): + (target/special).unlink(missing_ok=True) + for src in Path(tmp).rglob('*'): + if src.is_file(): + dest=target/src.relative_to(tmp) + dest.parent.mkdir(parents=True,exist_ok=True) + shutil.copy2(src,dest) + print('恢复完成。请检查文件权限后再启动 SiteForge。') + +if __name__=='__main__': + parser=argparse.ArgumentParser() + parser.add_argument('archive') + parser.add_argument('--data',default=os.getenv('SITEFORGE_DATA_DIR','/data')) + parser.add_argument('--confirmed-offline',action='store_true') + args=parser.parse_args() + restore(args.archive,args.data,args.confirmed_offline) diff --git a/scripts/split_theme_pages.py b/scripts/split_theme_pages.py new file mode 100644 index 0000000..4a55bc9 --- /dev/null +++ b/scripts/split_theme_pages.py @@ -0,0 +1,24 @@ +"""One-time developer script: break V3 full themes into independently maintainable Jinja page templates.""" +from pathlib import Path +import re +base=Path(__file__).resolve().parent.parent/'themes' +types={'home':['home'],'search':['search'],'listing':['listing','category'],'product':['product'], + 'brand':['brand'],'article':['article'],'case':['case'],'info':['about','service','contact','faq','standard'],'notfound':['notfound']} +markers=[("{% if kind=='home' %}",'home'),("{% elif kind=='search' %}",'search'),("{% elif kind in ['listing','category'] %}",'listing'),("{% elif kind=='product' %}",'product'),("{% elif kind=='brand' %}",'brand'),("{% elif kind=='article' %}",'article'),("{% elif kind=='case' %}",'case'),("{% elif kind in ('about','service','contact','faq','standard') %}",'info'),("{% elif kind=='notfound' %}",'notfound')] +for name in ('automotive','industrial','corporate','minimal'): + p=base/name/'pro.html' + t=p.read_text(encoding='utf-8') + beginning=t.index(markers[0][0]); ending=t.index('{% endif %}\n{% if has_custom',beginning) + common=t[:beginning] + remainder=t[ending+len('{% endif %}'):] # preserve custom blocks and footer + dir=base/name/'pages';dir.mkdir(parents=True,exist_ok=True) + pieces={} + for i,(marker,key) in enumerate(markers): + start=t.index(marker,beginning)+len(marker) + finish=t.index(markers[i+1][0],start) if i+1Test

Test

') + report=commercial_readiness.inspect(root) + assert not report['ready_for_review'] + assert report['commercially_certified'] is False + assert any(x['code']=='page_missing' for x in report['blockers']) + +def test_online_theme_preview_loads_same_css_and_hero(factory): + preview=factory.get('/api/factory/themes/automotive/preview',params={'version':'1.1.0','page_type':'home'}) + assert preview.status_code==200 + assert '/ui/commercial.css' in preview.text + sheet=factory.get('/ui/commercial.css') + assert sheet.status_code==200 and 'pro-filter-panel' in sheet.text + art=factory.get('/api/factory/themes/automotive/visual/hero.svg',params={'version':'1.1.0'}) + assert art.status_code==200 and 'image/svg+xml' in art.headers['content-type'] + assert factory.get('/api/factory/themes/automotive/visual/hero.svg',params={'version':'1.0.0'}).status_code==404 + +def test_custom_declarative_theme_inherits_trusted_visuals(factory): + import io,zipfile + from test_factory_v2 import zip_theme + blob=io.BytesIO(zip_theme('art-compatible','2.0.0')) + response=factory.post('/api/factory/themes/install',files={'file':('theme.zip',blob.getvalue(),'application/zip')}) + assert response.status_code==200,response.text + folder=theme_store.resolve('art-compatible','2.0.0') + assert (folder/'visual/hero.svg').is_file() diff --git a/tests/test_core_v4.py b/tests/test_core_v4.py new file mode 100644 index 0000000..6c0b2aa --- /dev/null +++ b/tests/test_core_v4.py @@ -0,0 +1,196 @@ +"""Regression tests for V4: theme layouts, migration audit, assembly, WXR. +No external HTTP/DNS or Docker engine required. +""" +import io,json,zipfile +from pathlib import Path +from html import escape +from openpyxl import Workbook +from app import db,blueprints,theme_store,migration_audit,wordpress_compat +from test_factory_v2 import factory,site + + +def test_nested_grid_validates_and_builds(factory): + sid=site(factory) + home=next(x for x in factory.get(f'/api/sites/{sid}/entries').json() if x['route']=='/') + layout=blueprints.new_block('layout',{'columns':[ + {'width':2,'blocks':[blueprints.new_block('richtext',{'title':'品牌故事','body':'

我们的企业介绍

'})]}, + {'width':1,'blocks':[blueprints.new_block('cta',{'title':'欢迎合作','href':'/contact/'})]}]}) + result=factory.put(f'/api/sites/{sid}/pages/{home["id"]}/blueprint',json={'blocks':[layout], + 'page_template':'home','layout_mode':'replace'}) + assert result.status_code==200,result.text + assert result.json()['layout_mode']=='replace' + preview=factory.post(f'/api/sites/{sid}/pages/{home["id"]}/editor-preview',json={'blocks':[layout],'page_template':'home','layout_mode':'replace'}) + assert preview.status_code==200,preview.text + assert '品牌故事' in preview.text and 'sf-layout-grid' in preview.text and 'pro-header' in preview.text + version=factory.post(f'/api/sites/{sid}/builds',json={}).json()['version'] + published=factory.get(f'/preview/{sid}/{version}/').text + assert '品牌故事' in published and 'sf-layout-grid' in published and 'pro-header' in published + css=factory.get(f'/preview/{sid}/{version}/assets/blocks.css').text + assert '@media(max-width:540px)' in css + + +def test_layout_rejects_duplicate_and_nested_excess(factory): + a=blueprints.new_block('richtext');b=blueprints.new_block('richtext');b['id']=a['id'] + bad=blueprints.new_block('layout',{'columns':[{'width':1,'blocks':[a,b]}]}) + try:blueprints.validate_blocks([bad]);assert False,'duplicate should be rejected' + except ValueError:pass + nested=blueprints.new_block('layout');child=blueprints.new_block('layout');grand=blueprints.new_block('layout') + child['columns'][0]['blocks']=[grand];nested['columns'][0]['blocks']=[child] + try:blueprints.validate_blocks([nested]);assert False,'deep nesting should be rejected' + except ValueError:pass + + +def test_wordpress_elementor_media_taxonomy_and_woo_gallery(factory): + sid=site(factory) + widgets=json.dumps([{'widgetType':'heading','settings':{'title':'核心价值','header_size':'h2'}}, + {'widgetType':'image','settings':{'image':{'url':'https://img.example/media.jpg'}}}, + {'widgetType':'unknown_plugin_widget','settings':{'value':'secret'}}],ensure_ascii=False) + wxr=''' + 图片attachment12https://img.example/cover.jpg + 产品Ahttps://wp.example/product/test-a/productpublish + 运动型多用途车 + _thumbnail_id12 + _product_image_gallery12 + _elementor_data + _price1200 + ''' + response=factory.post(f'/api/sites/{sid}/import',files={'file':('export.xml',wxr.encode(),'application/xml')}) + assert response.status_code==200,response.text + item=next(x for x in factory.get(f'/api/sites/{sid}/entries').json() if x['title']=='产品A') + assert item['cover']=='https://img.example/cover.jpg' + assert item['meta']['fields']['category']=='运动型多用途车' + assert item['meta']['fields']['price']=='1200' + assert '核心价值' in item['body'] + assert item['meta']['migration_warnings'] + assert item['meta']['fields']['gallery']==['https://img.example/cover.jpg'] + + +def test_excel_mapping_relations_preview_and_repeat(factory): + sid=site(factory) + w=Workbook();ws=w.active;ws.append(['车型名称','品牌','分类','动力形式','图库','原网址']) + ws.append(['SUV Alpha','Hesu','SUV','PHEV','https://img.example/1.jpg;https://img.example/2.jpg','/cars/alpha.html']) + b=io.BytesIO();w.save(b) + r=factory.post(f'/api/factory/sites/{sid}/mapping/parse-file',files={'file':('cars.xlsx',b.getvalue())}) + assert r.status_code==200,r.text + parsed=r.json();assert parsed['count']==1 + assert parsed['suggested']['title']=='车型名称' + report=factory.post(f'/api/factory/sites/{sid}/mapping/run',json={'rows':parsed['rows'], + 'rules':{**parsed['suggested'],'gallery':'图库'},'content_type':'vehicle','dry_run':True}).json() + assert report['mapped']==1 and report['sample'][0]['route']=='/cars/alpha.html' + committed=factory.post(f'/api/factory/sites/{sid}/mapping/run',json={'rows':parsed['rows'], + 'rules':{**parsed['suggested'],'gallery':'图库'},'content_type':'vehicle','dry_run':False}) + assert committed.status_code==200 + x=factory.get(f'/api/factory/sites/{sid}/relationships').json() + assert any(i['brand']=='Hesu' for i in x['brand_pages_missing']) + y=factory.post(f'/api/factory/sites/{sid}/relationships/assemble').json() + assert y['count']==1 + assert factory.post(f'/api/factory/sites/{sid}/relationships/assemble').json()['count']==0 + entries=factory.get(f'/api/sites/{sid}/entries').json() + assert any(e['type']=='brand' and e['title']=='Hesu' for e in entries) + assert next(e for e in entries if e['title']=='SUV Alpha')['meta']['fields']['gallery']==['https://img.example/1.jpg','https://img.example/2.jpg'] + for theme in ('industrial','corporate','minimal'): + assert factory.put(f'/api/factory/sites/{sid}/theme',json={'theme':theme}).status_code==200 + version=factory.post(f'/api/sites/{sid}/builds',json={}).json()['version'] + assert 'SUV Alpha' in factory.get(f'/preview/{sid}/{version}/cars/alpha.html').text + + +def test_migration_manifest_and_built_qa_blocks_missing(factory): + sid=site(factory,mode='migration') + baseline='url,seo_title,description,canonical,status\nhttps://old.example/,Title from old,Old description,,200\nhttps://old.example/legacy-only.html,Missing,,,200\n' + r=factory.post(f'/api/audit/sites/{sid}/import-baseline',files={'file':('urls.csv',baseline.encode())}) + assert r.status_code==200,r.text + assert r.json()['imported']==2 + version=factory.post(f'/api/sites/{sid}/builds',json={}).json()['version'] + report=factory.get(f'/api/audit/sites/{sid}/builds/{version}/report').json() + assert not report['passed'] and report['errors']>=1 + assert any(i['route']=='/legacy-only.html' and i['field']=='url' for i in report['issues']) + assert any(i['route']=='/' and i['severity']=='warning' for i in report['issues']) + readiness=factory.get(f'/api/factory/sites/{sid}/migration/readiness?build_id={version}').json() + assert not readiness['can_publish'] + csv=factory.get(f'/api/audit/sites/{sid}/builds/{version}/report.csv') + assert csv.status_code==200 and 'legacy-only.html' in csv.text + + +def test_sitemap_import_and_invalid_baseline_atomic(factory): + sid=site(factory,mode='migration') + xml='''https://old.example/about/''' + assert factory.post(f'/api/audit/sites/{sid}/import-baseline',files={'file':('sitemap.xml',xml.encode())}).json()['imported']==1 + bad='url\nhttps://old.example/about/\nhttps://old.example/about/?page=5\n' + response=factory.post(f'/api/audit/sites/{sid}/import-baseline',files={'file':('bad.csv',bad.encode())}) + assert response.status_code==200 and response.json()['errors'] + baseline=factory.get(f'/api/enterprise/sites/{sid}/baseline/report').json() + assert baseline['baseline_count']==1 + + +def test_theme_gallery_and_compatibility(factory): + sid=site(factory) + for theme in ('automotive','industrial','corporate','minimal'): + r=factory.get(f'/api/factory/themes/{theme}/compatibility') + assert r.status_code==200 and r.json()['valid'],r.text + product=factory.post(f'/api/sites/{sid}/entries',json={'title':'Unique Product','type':'product','route':'/product/unique/', + 'cover':'https://imgs.example/cover.jpg','meta':{'fields':{'gallery':['https://imgs.example/cover.jpg','https://imgs.example/second.jpg']}}}) + assert product.status_code in (200,201),product.text + ver=factory.post(f'/api/sites/{sid}/builds',json={}).json()['version'] + html=factory.get(f'/preview/{sid}/{ver}/product/unique/').text + assert 'pro-gallery-thumbs' in html and 'data-gallery-image' in html + assert 'pro.js' in html + + +def test_css_style_breakout_rejected(factory): + z=io.BytesIO() + with zipfile.ZipFile(z,'w') as f: + data={'id':'test-css-escape','version':'1.0.0','name':'Safe Test','layout_family':'corporate'} + f.writestr('manifest.json',json.dumps(data));f.writestr('page-presets.json',json.dumps({'home':[]})) + f.writestr('theme.css','body{color:red}') + result=factory.post('/api/factory/themes/install',files={'file':('unsafe.zip',z.getvalue())}) + assert result.status_code==400 + +def test_migration_warning_requires_explicit_review(factory): + sid=site(factory,mode='migration') + source='url,seo_title\nhttps://old.example/,Old SEO Title\n' + factory.post(f'/api/audit/sites/{sid}/import-baseline',files={'file':('old.csv',source.encode())}) + build=factory.post(f'/api/sites/{sid}/builds',json={}).json()['version'] + current=factory.get(f'/api/audit/sites/{sid}/builds/{build}/report').json() + warnings=[i for i in current['issues'] if i['severity']=='warning' and i['field']=='title'] + assert warnings and current['unreviewed_warnings']>=1 + readiness=factory.get(f'/api/factory/sites/{sid}/migration/readiness?build_id={build}').json() + assert not readiness['can_publish'] + assert any('未经审核' in x['message'] for x in readiness['gates']) + denied=factory.post(f'/api/audit/sites/{sid}/builds/{build}/review-warning',json={ + 'issue_id':warnings[0]['issue_id'],'reason':'短'}); + assert denied.status_code==400 + accepted=factory.post(f'/api/audit/sites/{sid}/builds/{build}/review-warning',json={ + 'issue_id':warnings[0]['issue_id'],'reason':'企业管理人员已经核查该 SEO 标题调整,并批准本构建中的改动。'}) + assert accepted.status_code==200,accepted.text + after=factory.get(f'/api/audit/sites/{sid}/builds/{build}/report').json() + assert after['unreviewed_warnings']==0 + next_gate=factory.get(f'/api/factory/sites/{sid}/migration/readiness?build_id={build}').json() + assert not any('未经审核' in x['message'] for x in next_gate['gates']) + # Unrelated broken links may still correctly block publication. + + +def test_site_scoped_reusable_block_library(factory): + first=site(factory); second=site(factory) + block=blueprints.new_block('layout',{'columns':[{'width':1,'blocks':[blueprints.new_block('richtext',{'title':'跨页面共用模块'})]}]}) + created=factory.post(f'/api/sites/{first}/components',json={'name':'品牌单列布局','block':block}) + assert created.status_code==200,created.text + cid=created.json()['id'] + rows=factory.get(f'/api/sites/{first}/components').json() + assert len(rows)==1 and rows[0]['block']['columns'][0]['blocks'][0]['title']=='跨页面共用模块' + assert not factory.get(f'/api/sites/{second}/components').json() + assert factory.put(f'/api/sites/{first}/components/{cid}',json={'name':'新版', 'expected_version':0,'block':block}).status_code==409 + assert factory.put(f'/api/sites/{first}/components/{cid}',json={'name':'新版', 'expected_version':1,'block':block}).json()['version']==2 + assert factory.delete(f'/api/sites/{first}/components/{cid}').json()['deleted'] + + +def test_contact_form_requires_configured_https_endpoint(factory): + sid=site(factory) + assert factory.put(f'/api/sites/{sid}/contact-settings',json={'email':'hello@example.org', 'form_endpoint':'http://unsecure.example/forms'}).status_code==400 + config=factory.put(f'/api/sites/{sid}/contact-settings',json={ + 'email':'hello@example.org','form_endpoint':'https://forms.example.org/receive','footer_text':'Official inquiry channel'}) + assert config.status_code==200,config.text + factory.post(f'/api/factory/sites/{sid}/bootstrap') + build=factory.post(f'/api/sites/{sid}/builds',json={}).json()['version'] + contact=factory.get(f'/preview/{sid}/{build}/contact/').text + assert '=2 + history=root.get(f'/api/enterprise/sites/{sid}/releases').json() + assert history and history[0]['build_id']==rebuilt + rollback=root.post(f'/api/enterprise/sites/{sid}/releases/{rebuilt}/rollback') + assert rollback.status_code==200,rollback.text + + +def test_revision_restore_and_design_tokens(root): + sid=site(root,'Design') + x=root.post(f'/api/sites/{sid}/entries',json={'type':'page','title':'Draft v1','route':'/old-page/'}) + entry=x.json()['id'] + assert root.patch(f'/api/sites/{sid}/entries/{entry}',json={'title':'Draft v2'}).status_code==200 + revs=root.get(f'/api/enterprise/sites/{sid}/entries/{entry}/revisions').json() + assert len(revs)>=1 + old=root.get(f'/api/enterprise/sites/{sid}/revisions/{revs[0]["id"]}').json() + assert old['entry']['title']=='Draft v1' + assert root.post(f'/api/enterprise/sites/{sid}/revisions/{revs[0]["id"]}/restore').status_code==200 + refreshed=root.get(f'/api/sites/{sid}/entries').json() + assert any(i['title']=='Draft v1' for i in refreshed) + invalid=root.put(f'/api/enterprise/sites/{sid}/design-tokens',json={'font':'system','radius':999}) + assert invalid.status_code==400 + payload={'font':'serif','heading_scale':1.2,'radius':18,'section_spacing':80,'container_width':1360,'button_style':'pill','header_style':'compact'} + assert root.put(f'/api/enterprise/sites/{sid}/design-tokens',json=payload).status_code==200 + assert root.get(f'/api/enterprise/sites/{sid}/design-tokens').json()==payload + build=root.post(f'/api/sites/{sid}/builds',json={}).json()['version'] + import pathlib + css=(db.DATA/'builds'/sid/build/'dist'/'assets'/'custom.css').read_text(encoding='utf-8') + assert 'Georgia' in css and '--sf-width:1360px' in css + + +def test_background_worker_and_audit(root): + sid=site(root,'Jobs') + res=root.post(f'/api/enterprise/sites/{sid}/jobs',json={'kind':'build'}) + assert res.status_code==202,res.text + assert run_once()==res.json()['id'] + jobs=root.get(f'/api/enterprise/sites/{sid}/jobs').json() + assert jobs[0]['state']=='done' + audits=root.get('/api/enterprise/audit').json() + assert any(x['operation']=='POST' for x in audits) + assert root.get('/ui/enterprise-ui.js').status_code==200 + + +def test_background_worker_retries_transient_failure(root,monkeypatch): + sid=site(root,'Retry Jobs') + job=root.post(f'/api/enterprise/sites/{sid}/jobs',json={'kind':'build'}).json()['id'] + monkeypatch.setenv('SITEFORGE_JOB_MAX_ATTEMPTS','3') + monkeypatch.setattr(worker.builder,'build',lambda *_: (_ for _ in ()).throw(RuntimeError('temporary failure'))) + assert worker.run_once()==job + queued=db.one('SELECT state,attempts,error,retry_at FROM build_jobs WHERE id=?',(job,)) + assert queued['state']=='queued' and queued['attempts']==1 + assert queued['retry_at'] and 'temporary failure' in queued['error'] + +def test_viewer_cannot_modify_or_see_other_site(root): + own=site(root,'Site One'); foreign=site(root,'Site Two') + user=root.post('/api/enterprise/team/users',json={'username':'viewer22','display_name':'Viewer','password':'long-viewer-password-2026'}).json()['id'] + assert root.put(f'/api/enterprise/sites/{own}/members',json={'user_id':user,'role':'viewer'}).status_code==200 + root.post('/api/auth/logout') + root.post('/api/auth/login',json={'username':'viewer22','password':'long-viewer-password-2026'}) + root.headers['X-SiteForge-CSRF']=root.get('/api/auth/me').json()['csrf'] + assert root.get(f'/api/sites/{own}/entries').status_code==200 + assert root.get(f'/preview/{foreign}/any/').status_code==403 + assert root.post(f'/api/sites/{own}/builds',json={}).status_code==403 + assert root.put(f'/api/enterprise/sites/{own}/design-tokens',json={}).status_code==403 + assert root.get(f'/api/enterprise/sites/{own}/jobs').status_code==200 + assert root.get(f'/api/enterprise/sites/{foreign}/jobs').status_code==403 diff --git a/tests/test_factory.py b/tests/test_factory.py new file mode 100644 index 0000000..d71cd6e --- /dev/null +++ b/tests/test_factory.py @@ -0,0 +1,269 @@ +import io +import json +import zipfile +from pathlib import Path +import pytest +from fastapi.testclient import TestClient +from app import db +from app.main import app +from app.core import route_path,ensure_public_url + +@pytest.fixture +def client(tmp_path,monkeypatch): + monkeypatch.setattr(db,'DATA',tmp_path) + monkeypatch.setattr(db,'DB_FILE',tmp_path/'test.sqlite3') + monkeypatch.setenv('SITEFORGE_ADMIN_PASSWORD','strong-supervisor-password-2026') + monkeypatch.setenv('SITEFORGE_SESSION_SECRET','large-random-key-for-test-only-2026-the-test-environment') + with TestClient(app) as c: + assert c.get('/api/sites').status_code == 401 + assert c.post('/api/auth/login',json={'password':'strong-supervisor-password-2026'}).status_code == 200 + auth=c.get('/api/auth/me').json() + c.headers.update({'X-SiteForge-CSRF': auth['csrf']}) + yield c + +def create(c,theme='automotive'): + r=c.post('/api/sites',json={'name':'Example Test','theme':theme,'domain':'https://example.test','mode':'migration'}) + assert r.status_code==201,r.text + return r.json()['id'] + +def test_site_and_theme_switch_preserve_content(client): + a=create(client) + b=create(client,'industrial') + payload={'type':'product','title':'Product A','route':'/products/legacy.html','seo_title':'Original SEO Title','seo_description':'Original SEO Description','body':'

Hello world

'} + x=client.post(f'/api/sites/{a}/entries',json=payload) + assert x.status_code==201,x.text + assert len(client.get(f'/api/sites/{b}/entries').json())==1 + for theme in ('automotive','industrial','corporate','minimal'): + assert client.patch(f'/api/sites/{a}',json={'theme':theme}).status_code==200 + info=client.post(f'/api/sites/{a}/builds',json={'mirror_images':False}) + assert info.status_code==200,info.text + bid=info.json()['version'] + resp=client.get(f'/preview/{a}/{bid}/products/legacy.html') + assert resp.status_code==200,resp.text[:150] + assert 'Original SEO Title' in resp.text + assert '' in resp.text + assert 'Original SEO Description' in resp.text + assert 'Hello' in resp.text + dl=client.get(f'/api/sites/{a}/builds/{bid}/download') + with zipfile.ZipFile(io.BytesIO(dl.content)) as z: + assert 'products/legacy.html' in z.namelist() + assert 'assets/theme.css' in z.namelist() + assert 'sitemap.xml' in z.namelist() + assert len(client.get(f'/api/sites/{a}/entries').json())==2 + +def test_home_blueprint_and_static_links(client): + sid=create(client) + pages=client.get(f'/api/sites/{sid}/entries').json() + home=pages[0] + assert home['route']=='/' + meta={'blocks':[{'type':'features','title':'优势清单','items':[{'title':'服务能力','text':'广泛覆盖'}]}, + {'type':'cta','title':'马上合作','href':'/contact/'}]} + assert client.patch(f'/api/sites/{sid}/entries/{home["id"]}',json={'meta':meta}).status_code==200 + r=client.post(f'/api/sites/{sid}/builds',json={}) + bid=r.json()['version'] + txt=client.get(f'/preview/{sid}/{bid}/').text + assert '优势清单' in txt and '马上合作' in txt + +def test_duplicate_routes_and_unsafe_html(client): + sid=create(client) + x=client.post(f'/api/sites/{sid}/entries',json={'type':'page','title':'X','route':'/test/','body':'x'}) + assert x.status_code==201 + y=client.post(f'/api/sites/{sid}/entries',json={'type':'page','title':'Y','route':'/test/'}) + assert y.status_code==400 + z=client.get(f'/api/sites/{sid}/entries').json() + it=next(x for x in z if x['route']=='/test/') + assert '','visible':False}] + res=client.post(f'/api/sites/{sid}/pages/{homepage["id"]}/editor-preview',json={'blocks':blocks,'page_template':'home'}) + assert res.status_code==200,res.text + assert 'onerror=alert' in res.text # text escaped, never a literal img tag + assert '<img src=x onerror' in res.text + assert 'href="/"' in res.text or 'href="./"' in res.text + assert 'should be hidden' not in res.text + assert client.put(f'/api/sites/{sid}/pages/{homepage["id"]}/blueprint',json={'blocks':[{'id':'bad-id','type':'script','title':'Bad'}],'page_template':'home'}).status_code==400 + +def test_bulk_theme_assembly_preserves_legacy_url_and_seo(client): + sid=create(client,'industrial') + resp=client.post(f'/api/sites/{sid}/entries',json={'type':'product','title':'HDPE Pipe','route':'/old-product/hdpe-pipe.html','body':'

Legacy Technical Details

','seo_title':'HDPE pipe SEO forever','seo_description':'Exact old metadata'}) + assert resp.status_code==201 + entry=resp.json() + assert not entry['meta'].get('blocks') + done=client.post(f'/api/sites/{sid}/apply-theme',json={'mode':'missing'}) + assert done.status_code==200,done.text + assert done.json()['updated']>=1 + updated=client.get(f'/api/sites/{sid}/entries').json() + final=next(x for x in updated if x['id']==entry['id']) + assert final['route']=='/old-product/hdpe-pipe.html' + assert final['seo_title']=='HDPE pipe SEO forever' + assert len(final['meta']['blocks'])>=2 + built=client.post(f'/api/sites/{sid}/builds',json={}) + assert built.status_code==200,built.text + html=client.get(f'/preview/{sid}/{built.json()["version"]}/old-product/hdpe-pipe.html').text + assert 'Legacy Technical Details' in html + assert 'HDPE pipe SEO forever' in html + +def test_scaffold_standard_pages_safe_idempotent(client): + sid=create(client) + first=client.post(f'/api/sites/{sid}/scaffold') + assert first.status_code==200 + assert '/about/' in first.json()['created'] + second=client.post(f'/api/sites/{sid}/scaffold') + assert second.status_code==200 and not second.json()['created'] + lst=client.get(f'/api/sites/{sid}/entries').json() + assert len([x for x in lst if x['route']=='/about/'])==1 + build=client.post(f'/api/sites/{sid}/builds',json={}) + assert build.status_code==200,build.text + +def test_attributes_bind_imported_and_typed_fields(client): + sid=create(client,'automotive') + product=client.post(f'/api/sites/{sid}/entries',json={'type':'product','title':'BYD SUV','route':'/product/byd-suv/','meta':{'fields':{'brand':'BYD','model':'SUV 1','specs':'Electric'}}}) + assert product.status_code==201 + eid=product.json()['id'] + blocks=[{'id':'attributes-1','type':'attributes','title':'Technical Parameters'}] + save=client.put(f'/api/sites/{sid}/pages/{eid}/blueprint',json={'blocks':blocks,'page_template':'product'}) + assert save.status_code==200,save.text + b=client.post(f'/api/sites/{sid}/builds',json={}) + assert b.status_code==200,b.text + page=client.get(f'/preview/{sid}/{b.json()["version"]}/product/byd-suv/').text + assert 'Technical Parameters' in page and '>BYD<' in page and 'SUV 1' in page diff --git a/tests/test_factory_v2.py b/tests/test_factory_v2.py new file mode 100644 index 0000000..5b738e6 --- /dev/null +++ b/tests/test_factory_v2.py @@ -0,0 +1,160 @@ +import io,json,zipfile +import pytest +from fastapi.testclient import TestClient +from app import db,theme_store +from app.main import app + +@pytest.fixture +def factory(tmp_path,monkeypatch): + monkeypatch.setattr(db,'DATA',tmp_path) + monkeypatch.setattr(db,'DB_FILE',tmp_path/'site.sqlite3') + monkeypatch.setattr(theme_store,'THEME_ROOT',tmp_path/'theme_packages') + monkeypatch.setenv('SITEFORGE_ADMIN_PASSWORD','strong-admin-2026-secret') + monkeypatch.setenv('SITEFORGE_SESSION_SECRET','longer-test-session-secret-2026-not-for-production') + with TestClient(app) as c: + r=c.post('/api/auth/login',json={'username':'admin','password':'strong-admin-2026-secret'}) + assert r.status_code==200,r.text + c.headers.update({'X-SiteForge-CSRF':c.get('/api/auth/me').json()['csrf']}) + yield c + +def site(c,mode='new'): + r=c.post('/api/sites',json={'name':'Factory Sandbox','mode':mode,'theme':'automotive','domain':'https://test.example'}) + assert r.status_code==201,r.text + return r.json()['id'] + +def zip_theme(id='brand-x',version='1.0.0',bad=False): + from pathlib import Path + root=Path(__file__).resolve().parent.parent/'themes'/'corporate' + arr=io.BytesIO() + with zipfile.ZipFile(arr,'w') as z: + data=json.loads((root/'manifest.json').read_text(encoding='utf-8'));data.update({'id':id,'version':version,'name':'V2 Test Brand'}) + z.writestr('manifest.json',json.dumps(data)) + z.writestr('theme.css','body{background:#f5f5f5}') + z.writestr('page-presets.json',(root/'page-presets.json').read_bytes()) + if bad:z.writestr('../hack.py','print(1)') + return arr.getvalue() + +def test_install_theme_and_pinned_build(factory): + sid=site(factory) + r=factory.post('/api/factory/themes/install',files={'file':('theme.zip',zip_theme(),'application/zip')}) + assert r.status_code==200,r.text + assert r.json()['id']=='brand-x' + assert 'brand-x' in [x.get('id',x.get('theme_id')) for x in factory.get('/api/factory/themes').json()] + update=factory.put(f'/api/factory/sites/{sid}/theme',json={'theme':'brand-x','version':'1.0.0'}) + assert update.status_code==200,update.text + assert factory.post(f'/api/factory/sites/{sid}/bootstrap').status_code==200 + b=factory.post(f'/api/sites/{sid}/builds',json={}) + assert b.status_code==200,b.text + info=factory.get(f'/api/sites/{sid}/builds/{b.json()["version"]}/report').json() + assert info['theme']=='brand-x' and info['theme_version']=='1.0.0' + file=factory.get(f'/preview/{sid}/{b.json()["version"]}/search/').text + assert '站内搜索' in file + assert factory.get(f'/api/factory/sites/{sid}/theme').json()['version']=='1.0.0' + +def test_theme_rejects_zip_traversal(factory): + r=factory.post('/api/factory/themes/install',files={'file':('evil.zip',zip_theme(bad=True),'application/zip')}) + assert r.status_code==400 + +def test_complete_bootstrap_repeated_safe(factory): + sid=site(factory) + r=factory.post(f'/api/factory/sites/{sid}/bootstrap').json() + assert len(r['created'])>=7 and '/about/' in r['created'] + r2=factory.post(f'/api/factory/sites/{sid}/bootstrap').json() + assert not r2['created'] + records=factory.get(f'/api/sites/{sid}/entries').json() + assert all(r['route'] for r in records) + build=factory.post(f'/api/sites/{sid}/builds',json={}).json() + html=factory.get(f'/preview/{sid}/{build["version"]}/').text + assert 'sf-site-head' in html and 'factory.css' in html + assert factory.get(f'/preview/{sid}/{build["version"]}/assets/site-runtime.js').status_code==200 + +def test_mapping_preview_commit_and_cross_site(factory): + a=site(factory);b=site(factory) + rows=[{'车型名称':'A SUV','品牌':'Alpha','原网址':'/legacy/suv-a.html','续航':'400km'}, + {'车型名称':'B SUV','品牌':'Beta','原网址':'/legacy/suv-b.html','续航':'500km'}] + rules={'title':'车型名称','brand':'品牌','route':'原网址','range':'续航'} + mapping=factory.post(f'/api/factory/sites/{a}/mapping/run',json={'rows':rows,'rules':rules,'content_type':'vehicle','dry_run':True}) + assert mapping.status_code==200,mapping.text + assert mapping.json()['mapped']==2 + assert factory.get(f'/api/sites/{b}/entries').json()[0]['route']=='/' + saved=factory.post(f'/api/factory/sites/{a}/mapping/run',json={'rows':rows,'rules':rules,'content_type':'vehicle','dry_run':False}) + assert saved.status_code==200,saved.text + assert saved.json()['created']==2 + imported=factory.get(f'/api/sites/{a}/entries').json() + suv=next(r for r in imported if r['title']=='A SUV') + assert suv['route']=='/legacy/suv-a.html' and suv['meta']['fields']['brand']=='Alpha' + assert all(r['title']!='A SUV' for r in factory.get(f'/api/sites/{b}/entries').json()) + again=factory.post(f'/api/factory/sites/{a}/mapping/run',json={'rows':rows,'rules':rules,'content_type':'vehicle','dry_run':False}) + assert again.json()['skipped']==2 + +def test_migration_readiness_wont_greenwash(factory): + sid=site(factory,mode='migration') + r=factory.get(f'/api/factory/sites/{sid}/migration/readiness').json() + assert not r['can_publish'] + assert any('基线' in x['message'] for x in r['gates']) + assert any('构建' in x['message'] for x in r['gates']) + assert factory.post(f'/api/factory/sites/{sid}/mapping/run',json={'rows':[{'name':''}],'rules':{'title':'name'},'dry_run':True}).json()['errors'] + +def test_editor_supports_responsive_settings(factory): + sid=site(factory) + homepage=next(x for x in factory.get(f'/api/sites/{sid}/entries').json() if x['route']=='/') + blocks=homepage['meta']['blocks'];blocks[0]['style']={'mobile_padding':19,'tablet_padding':34,'hide_mobile':True} + r=factory.put(f'/api/sites/{sid}/pages/{homepage["id"]}/blueprint',json={'blocks':blocks,'page_template':'home'}) + assert r.status_code==200,r.text + preview=factory.post(f'/api/sites/{sid}/pages/{homepage["id"]}/editor-preview',json={'blocks':blocks,'page_template':'home'}) + assert preview.status_code==200 + assert 'siteforge-inline-editor' in preview.text and '--sf-pad:19px' in preview.text + build=factory.post(f'/api/sites/{sid}/builds',json={}).json() + css=factory.get(f'/preview/{sid}/{build["version"]}/assets/custom.css').text + assert '--sf-pad:19px' in css and 'display:none' in css + +def test_release_requires_separate_approval(factory,monkeypatch): + sid=site(factory) + factory.post(f'/api/factory/sites/{sid}/bootstrap') + build=factory.post(f'/api/sites/{sid}/builds',json={}).json() + bid=build['version'] + monkeypatch.setenv('SITEFORGE_REQUIRE_RELEASE_APPROVAL','true') + monkeypatch.setenv('SITEFORGE_PUBLISH_ROOT',str(db.DATA/'published')) + fail=factory.post(f'/api/sites/{sid}/builds/{bid}/publish-local') + assert fail.status_code==409,fail.text + req=factory.post(f'/api/factory/sites/{sid}/release-requests',json={'build_id':bid}) + # If QA reports a real error we must not silently approve the release. + if req.status_code==409: + from app import quality + issues=quality.report(sid,bid) + assert issues['errors']>0,issues + return + assert req.status_code==200,req.text + rid=req.json()['id'] + same_user=factory.post(f'/api/factory/sites/{sid}/release-requests/{rid}/review',json={'state':'approved'}) + assert same_user.status_code==403 + user=factory.post('/api/enterprise/team/users',json={'username':'review_v2','password':'very-long-review-password-2026','display_name':'Reviewer'}).json() + factory.put(f'/api/enterprise/sites/{sid}/members',json={'user_id':user['id'],'role':'reviewer'}) + factory.post('/api/auth/logout') + assert factory.post('/api/auth/login',json={'username':'review_v2','password':'very-long-review-password-2026'}).status_code==200 + factory.headers.update({'X-SiteForge-CSRF':factory.get('/api/auth/me').json()['csrf']}) + approved=factory.post(f'/api/factory/sites/{sid}/release-requests/{rid}/review',json={'state':'approved'}) + assert approved.status_code==200,approved.text + # Reviewer cannot run owner's publish action. + assert factory.post(f'/api/sites/{sid}/builds/{bid}/publish-local').status_code==403 + factory.post('/api/auth/logout') + factory.post('/api/auth/login',json={'password':'strong-admin-2026-secret'}) + factory.headers.update({'X-SiteForge-CSRF':factory.get('/api/auth/me').json()['csrf']}) + assert factory.post(f'/api/sites/{sid}/builds/{bid}/publish-local').status_code==200 + +def test_theme_previews_are_rendered_by_trusted_templates(factory): + for name in ('automotive','industrial','corporate','minimal'): + result=factory.get(f'/api/factory/themes/{name}/preview') + assert result.status_code==200,result.text[:500] + assert 'sf-site-head' in result.text + assert 'assets/theme.css' not in result.text + assert 'sf-canvas' in result.text + assert factory.get(f'/api/factory/themes/{name}/preview.css').status_code==200 + +def test_theme_page_type_previews(factory): + for theme in ('automotive','industrial','corporate','minimal'): + for page_type in ('home','about','product','article','brand','listing','contact','standard','notfound'): + p=factory.get(f'/api/factory/themes/{theme}/preview',params={'page_type':page_type}) + assert p.status_code==200,(theme,page_type,p.text[:300]) + assert 'text/html' in p.headers['content-type'] + assert factory.get('/api/factory/themes/automotive/preview?page_type=wrong').status_code==400 diff --git a/tests/test_theme_pro_v3.py b/tests/test_theme_pro_v3.py new file mode 100644 index 0000000..ee711ed --- /dev/null +++ b/tests/test_theme_pro_v3.py @@ -0,0 +1,122 @@ +"""Professional theme system integration tests use the real FastAPI and static builder.""" +import io +import json +import re +import zipfile +from pathlib import Path +from bs4 import BeautifulSoup +from test_factory_v2 import factory, site, zip_theme + +THEMES=('automotive','industrial','corporate','minimal') + +def test_four_complete_theme_families_have_distinct_markup(factory): + main=[] + for theme in THEMES: + for page_type in ('home','about','contact','listing','product','article','brand','standard','notfound'): + r=factory.get(f'/api/factory/themes/{theme}/preview',params={'page_type':page_type}) + assert r.status_code==200,(theme,page_type,r.text[:400]) + soup=BeautifulSoup(r.text,'html.parser') + assert soup.find('header',class_='pro-header') + assert soup.find('footer',class_='pro-footer') + assert len(soup.find_all('h1'))==1,(theme,page_type,len(soup.find_all('h1'))) + assert 'pro-base.css' in r.text and 'pro.js' in r.text + if page_type=='product':assert soup.select_one('.pro-product-layout .pro-specs') + if page_type=='listing':assert soup.select_one('[data-pro-filter-search]') + if page_type=='article':assert soup.select_one('.pro-article-hero') + main.append(factory.get(f'/api/factory/themes/{theme}/preview?page_type=home').text) + assert len(set(re.findall(r'class="pro-home-hero [^"]+"',s)[0] for s in main))==4 + + +def test_theme_build_derived_category_brand_search_and_seo(factory): + sid=site(factory) + assert factory.post(f'/api/factory/sites/{sid}/bootstrap').status_code==200 + r=factory.post(f'/api/sites/{sid}/entries',json={ + 'type':'product','title':'Electric A','route':'/old/product-a.html', + 'summary':'Long-range sample','seo_title':'Keep Me SEO','seo_description':'Original description', + 'meta':{'fields':{'brand':'Quanta Motors','category':'SUV','range':'500 km'}}}) + assert r.status_code==201,r.text + for theme in THEMES: + selected=factory.put(f'/api/factory/sites/{sid}/theme',json={'theme':theme,'version':'1.0.0'}) + assert selected.status_code==200,selected.text + result=factory.post(f'/api/sites/{sid}/builds',json={}) + assert result.status_code==200,result.text + bid=result.json()['version'] + for url in ['/','/about/','/contact/','/products/','/brands/','/news/','/cases/','/categories/suv/','/brands/quanta-motors/','/search/','/old/product-a.html']: + page=factory.get(f'/preview/{sid}/{bid}{url}') + assert page.status_code==200,(theme,url,page.text[:200]) + soup=BeautifulSoup(page.text,'html.parser') + assert len(soup.find_all('h1'))==1,(theme,url) + assert soup.select_one('.pro-header') and soup.select_one('.pro-footer') + product=factory.get(f'/preview/{sid}/{bid}/old/product-a.html').text + assert 'Keep Me SEO' in product + assert 'range' in product.lower() and '500 km' in product + assert 'https://test.example/old/product-a.html' in product + listing=factory.get(f'/preview/{sid}/{bid}/categories/suv/').text + assert 'Electric A' in listing + theme_css=factory.get(f'/preview/{sid}/{bid}/assets/pro.css') + assert theme_css.status_code==200 and len(theme_css.content)>2000 + report=factory.get(f'/api/sites/{sid}/builds/{bid}/report').json() + assert not report['errors'] and not report['broken_links'] and not report['missing_local_images'] + + +def test_builtin_version_pin_is_real(factory): + sid=site(factory) + versions=factory.get('/api/factory/themes/automotive/versions').json() + assert {v['version'] for v in versions}=={'1.1.0','1.0.0','0.3.0'} + assert factory.put(f'/api/factory/sites/{sid}/theme',json={'theme':'automotive','version':'0.3.0'}).status_code==200 + b=factory.post(f'/api/sites/{sid}/builds',json={}).json() + old=factory.get(f'/preview/{sid}/{b["version"]}/').text + assert 'pro-auto-hero' not in old + assert factory.put(f'/api/factory/sites/{sid}/theme',json={'theme':'automotive','version':'1.0.0'}).status_code==200 + b=factory.post(f'/api/sites/{sid}/builds',json={}).json() + new=factory.get(f'/preview/{sid}/{b["version"]}/').text + assert 'pro-auto-hero' in new + + +def test_custom_theme_package_uses_trusted_layout_and_css(factory): + sid=site(factory) + raw=io.BytesIO(zip_theme('custom-v3','2.0.0')) + data=io.BytesIO() + with zipfile.ZipFile(raw) as old,zipfile.ZipFile(data,'w') as new: + for fn in old.namelist(): + content=old.read(fn) + if fn=='manifest.json': + obj=json.loads(content);obj['layout_family']='industrial' + content=json.dumps(obj).encode() + if fn=='theme.css':content=b':root{--pro-accent:#dd5500} .pro-card{border-width:2px}' + new.writestr(fn,content) + install=factory.post('/api/factory/themes/install',files={'file':('theme.zip',data.getvalue(),'application/zip')}) + assert install.status_code==200,install.text + assert factory.put(f'/api/factory/sites/{sid}/theme',json={'theme':'custom-v3','version':'2.0.0'}).status_code==200 + b=factory.post(f'/api/sites/{sid}/builds',json={}).json() + assert 'pro-industrial-hero' in factory.get(f'/preview/{sid}/{b["version"]}/').text + css=factory.get(f'/preview/{sid}/{b["version"]}/assets/theme-overrides.css').text + assert '--pro-accent:#dd5500' in css + pkg=factory.get('/api/factory/themes/custom-v3/package?version=2.0.0') + assert pkg.status_code==200 + with zipfile.ZipFile(io.BytesIO(pkg.content)) as z: + assert {'manifest.json','theme.css','page-presets.json'}<=set(z.namelist()) + + +def test_static_responsive_and_search_facilities(factory): + sid=site(factory) + b=factory.post(f'/api/sites/{sid}/builds',json={}).json() + bid=b['version'] + css=factory.get(f'/preview/{sid}/{bid}/assets/pro-base.css').text + js=factory.get(f'/preview/{sid}/{bid}/assets/pro.js').text + assert '@media(max-width:560px)' in css + assert 'aria-expanded' in js and 'data-pro-filter-brand' in js + assert 'search-index.json' in factory.get(f'/preview/{sid}/{bid}/assets/site-runtime.js').text + assert 'noindex' in factory.get(f'/preview/{sid}/{bid}/search/').text + + +def test_all_fifty_six_page_templates_and_live_gallery(factory): + base=Path(__file__).resolve().parent.parent/'themes' + expected={'home','about','contact','listing','category','product','brand','article','case','service','faq','standard','search','notfound'} + for theme in THEMES: + assert {p.stem for p in (base/theme/'pages').glob('*.html')}==expected + for page_type in ('case','service','faq','category'): + r=factory.get(f'/api/factory/themes/{theme}/preview?page_type={page_type}') + assert r.status_code==200,(theme,page_type,r.text[:300]) + page=factory.get('/theme-gallery') + assert page.status_code==200 and 'iframe' in page.text and 'page_type' in page.text diff --git a/themes/_professional_components.html b/themes/_professional_components.html new file mode 100644 index 0000000..8762c8c --- /dev/null +++ b/themes/_professional_components.html @@ -0,0 +1,16 @@ +{% macro cover(item, kind='product') %} + {% if item.cover %}{{ item.title }} + {% else %}
{{ (item.title or 'SF')[:2] }}{{ kind|upper }}
{% endif %} +{% endmacro %} +{% macro card(item, kind='product', position=0) %} + +
{{ cover(item,kind) }}
+
{{ fields_of(item).get('brand') or fields_of(item).get('category') or labels.get(item.type,'PROJECT') }}{{ '%02d'|format(position+1) }}
+

{{ item.title }}

{{ (item.summary or '')[:128] }}

查看详情
+
+{% endmacro %} +{% macro grid(items,kind='product') %}
{% for item in items %}{{ card(item,kind,loop.index0) }}{% else %}
该栏目还没有发布内容。可在网站工厂的内容中心添加内容并重新构建。
{% endfor %}
{% endmacro %} +{% macro section_title(eyebrow,heading,href='') %}
{{ eyebrow }}

{{ heading }}

{% if href %}浏览全部 ↗{% endif %}
{% endmacro %} +{% macro filters() %}
{% endmacro %} +{% macro footer_links(nav) %}{% endmacro %} +{% macro body_copy(content,page) %}{% if content %}
{{ content }}
{% elif page.summary %}

{{ page.summary }}

{% else %}

可在管理后台完善本页的内容。

{% endif %}{% endmacro %} diff --git a/themes/automotive/manifest.json b/themes/automotive/manifest.json new file mode 100644 index 0000000..43496e8 --- /dev/null +++ b/themes/automotive/manifest.json @@ -0,0 +1,54 @@ +{ + "id": "automotive", + "name": "汽车与出行", + "description": "商业主题体验升级:独立视觉素材、响应式排版、产品列表筛选、多级导航、可访问性与交付检查(需真实内容验收)", + "version": "1.1.0", + "style": "luxury", + "entry": "pro.html", + "styles": [ + "theme.css", + "blocks.css" + ], + "page_templates": [ + "home", + "about", + "contact", + "listing", + "category", + "product", + "article", + "brand", + "case", + "service", + "faq", + "standard", + "notfound" + ], + "blocks": [ + "hero", + "richtext", + "features", + "stats", + "collection", + "image_text", + "gallery", + "faq", + "cta", + "attributes", + "spacer" + ], + "responsive": true, + "auto_binding": true, + "professional_layout": true, + "commercial_ui": true, + "commercial_features": [ + "responsive-design", + "product-detail", + "brand-collections", + "gallery", + "content-facets", + "mega-navigation", + "contact-integration", + "readiness-audit" + ] +} \ No newline at end of file diff --git a/themes/automotive/page-presets.json b/themes/automotive/page-presets.json new file mode 100644 index 0000000..315b9c2 --- /dev/null +++ b/themes/automotive/page-presets.json @@ -0,0 +1,285 @@ +{ + "home": [ + { + "id": "ff2755fabe", + "type": "hero", + "visible": true, + "title": "探索全球汽车新可能", + "subtitle": "THE WORLD OF MOBILITY", + "description": "{{site.tagline}}", + "button": "浏览车型", + "href": "/product/", + "align": "left" + }, + { + "id": "f997b5975c", + "type": "collection", + "visible": true, + "title": "精选车型", + "subtitle": "EXPLORE OUR COLLECTION", + "source": "product", + "limit": 6, + "columns": 3, + "button": "查看全部", + "href": "/product/" + }, + { + "id": "b79e9bdeee", + "type": "stats", + "visible": true, + "title": "值得信赖的服务", + "items": [ + { + "value": "50,000+", + "label": "服务案例" + }, + { + "value": "70+", + "label": "市场覆盖" + }, + { + "value": "30+", + "label": "合作伙伴" + } + ] + }, + { + "id": "fd3475d4a9", + "type": "image_text", + "visible": true, + "title": "专业采购与全球交付", + "subtitle": "GLOBAL DELIVERY", + "description": "为海外市场提供适合的汽车采购与服务解决方案。", + "image": "", + "image_side": "right", + "button": "了解更多", + "href": "/about/" + }, + { + "id": "5f159c1114", + "type": "cta", + "visible": true, + "title": "开启下一次合作", + "subtitle": "告诉我们您的需求,我们将提供适合的方案。", + "button": "联系我们", + "href": "/contact/" + } + ], + "about": [ + { + "id": "3b57a5c80a", + "type": "image_text", + "visible": true, + "title": "认识我们", + "subtitle": "OUR APPROACH", + "description": "展示业务流程和服务理念。", + "image": "", + "image_side": "right", + "button": "了解更多", + "href": "/about/" + }, + { + "id": "c7aaebcbc8", + "type": "stats", + "visible": true, + "title": "我们的实力", + "items": [ + { + "value": "50,000+", + "label": "服务案例" + }, + { + "value": "70+", + "label": "市场覆盖" + }, + { + "value": "30+", + "label": "合作伙伴" + } + ] + }, + { + "id": "f0d35431bc", + "type": "features", + "visible": true, + "title": "我们的优势", + "items": [ + { + "title": "专业服务", + "text": "从咨询到交付" + }, + { + "title": "品质保障", + "text": "严格的质量流程" + }, + { + "title": "全球网络", + "text": "连接不同市场" + } + ] + } + ], + "contact": [ + { + "id": "a21100b5fd", + "type": "richtext", + "visible": true, + "title": "联系我们", + "body": "

请填写企业联系方式与地址。

" + }, + { + "id": "360304f1f1", + "type": "cta", + "visible": true, + "title": "欢迎联系", + "subtitle": "告诉我们您的需求,我们将提供适合的方案。", + "button": "联系我们", + "href": "/contact/" + } + ], + "listing": [ + { + "id": "e8c990ef8b", + "type": "collection", + "visible": true, + "title": "所有在售车型", + "subtitle": "为客户提供适合的解决方案", + "source": "product", + "limit": 24, + "columns": 3, + "button": "查看全部", + "href": "/product/" + } + ], + "product": [ + { + "id": "7dfe1376b0", + "type": "image_text", + "visible": true, + "title": "产品介绍", + "subtitle": "OUR APPROACH", + "description": "{{page.summary}}", + "image": "{{page.cover}}", + "image_side": "right", + "button": "了解更多", + "href": "/about/" + }, + { + "id": "c3a67474f4", + "type": "attributes", + "visible": true, + "title": "产品规格与参数" + }, + { + "id": "29b39f194a", + "type": "richtext", + "visible": true, + "title": "产品详情", + "body": "{{page.body}}" + }, + { + "id": "70d24b5293", + "type": "cta", + "visible": true, + "title": "开启新的合作", + "subtitle": "告诉我们您的需求,我们将提供适合的方案。", + "button": "联系我们", + "href": "/contact/" + } + ], + "article": [ + { + "id": "f283231732", + "type": "richtext", + "visible": true, + "title": "{{page.title}}", + "body": "{{page.body}}" + } + ], + "brand": [ + { + "id": "e08f6ad68f", + "type": "image_text", + "visible": true, + "title": "{{page.title}}", + "subtitle": "OUR APPROACH", + "description": "{{page.summary}}", + "image": "{{page.cover}}", + "image_side": "right", + "button": "了解更多", + "href": "/about/" + }, + { + "id": "0f3689e9f0", + "type": "collection", + "visible": true, + "title": "品牌产品", + "subtitle": "为客户提供适合的解决方案", + "source": "product", + "limit": 6, + "columns": 3, + "button": "查看全部", + "href": "/product/" + } + ], + "standard": [ + { + "id": "d47d90fe91", + "type": "richtext", + "visible": true, + "title": "{{page.title}}", + "body": "{{page.body}}" + } + ], + "notfound": [ + { + "id": "8b39fc46a4", + "type": "cta", + "visible": true, + "title": "页面暂时无法访问", + "subtitle": "告诉我们您的需求,我们将提供适合的方案。", + "button": "返回首页", + "href": "/" + } + ], + "case": [ + { + "type": "image_text", + "title": "{{page.title}}", + "image": "{{page.cover}}", + "description": "{{page.summary}}" + }, + { + "type": "richtext", + "title": "项目概述", + "body": "{{page.body}}" + } + ], + "service": [ + { + "type": "richtext", + "title": "{{page.title}}", + "body": "{{page.body}}" + }, + { + "type": "cta", + "title": "了解更多", + "href": "/contact/" + } + ], + "faq": [ + { + "type": "faq", + "title": "常见问题", + "items": [] + } + ], + "category": [ + { + "type": "collection", + "title": "分类产品", + "source": "product", + "limit": 24 + } + ] +} \ No newline at end of file diff --git a/themes/automotive/pages/about.html b/themes/automotive/pages/about.html new file mode 100644 index 0000000..945be44 --- /dev/null +++ b/themes/automotive/pages/about.html @@ -0,0 +1,2 @@ +
{{ 'GET IN TOUCH' if kind=='contact' else ('OUR STORY' if kind=='about' else 'EXPLORE') }}

{{page.title}}

{{page.summary}}

+
{{'CONNECT / CONTACT' if kind=='contact' else 'ABOUT / DISCOVER'}}

{{'让我们开始沟通。' if kind=='contact' else page.title}}

{% if kind=='contact' and footer_email %}{{footer_email}} ↗{% elif kind=='contact' %}

请先在网站管理后台填写正式联系邮箱。

{% endif %}
{{body_copy(content,page)}}{% if page.cover %}{{page.title}}{% endif %}
diff --git a/themes/automotive/pages/article.html b/themes/automotive/pages/article.html new file mode 100644 index 0000000..85626e4 --- /dev/null +++ b/themes/automotive/pages/article.html @@ -0,0 +1 @@ +
LATEST NEWS / INSIGHTS

{{page.title}}

{{page.summary}}

{% if fields.get('publish_date') %}{% endif %}
diff --git a/themes/automotive/pages/brand.html b/themes/automotive/pages/brand.html new file mode 100644 index 0000000..a710280 --- /dev/null +++ b/themes/automotive/pages/brand.html @@ -0,0 +1 @@ +
BRAND PROFILE / PARTNERS

{{page.title}}

{{page.summary}}

{{cover(page,'brand')}}
{{body_copy(content,page)}}{% if related %}{{section_title('PRODUCT LINE','相关产品','/products/')}}{{grid(related,'product')}}{% endif %}
diff --git a/themes/automotive/pages/case.html b/themes/automotive/pages/case.html new file mode 100644 index 0000000..79e4d72 --- /dev/null +++ b/themes/automotive/pages/case.html @@ -0,0 +1 @@ +
SELECTED WORK / PROJECT

{{page.title}}

{{page.summary}}

{% if page.cover %}{{page.title}}{% endif %}
{% if fields.get('location') %}项目地点
{{fields.get('location')}}
{% endif %}{% if fields.get('industry') %}所属行业
{{fields.get('industry')}}
{% endif %}
{{body_copy(content,page)}}浏览更多案例 →
diff --git a/themes/automotive/pages/category.html b/themes/automotive/pages/category.html new file mode 100644 index 0000000..3918172 --- /dev/null +++ b/themes/automotive/pages/category.html @@ -0,0 +1,2 @@ +
DISCOVER / {{'CATEGORY' if kind=='category' else 'COLLECTION'}}

{{page.title}}

{{page.summary or '查看此栏目的全部已发布内容。'}}

首页/{{page.title}}
+
{{ filters() }}
{{grid(listing,'catalog')}}
diff --git a/themes/automotive/pages/contact.html b/themes/automotive/pages/contact.html new file mode 100644 index 0000000..3a52a60 --- /dev/null +++ b/themes/automotive/pages/contact.html @@ -0,0 +1,7 @@ +
{{ 'GET IN TOUCH' if kind=='contact' else ('OUR STORY' if kind=='about' else 'EXPLORE') }}

{{page.title}}

{{page.summary}}

+
{{'CONNECT / CONTACT' if kind=='contact' else 'ABOUT / DISCOVER'}}

{{'让我们开始沟通。' if kind=='contact' else page.title}}

{% if kind=='contact' and footer_email %}{{footer_email}} ↗{% elif kind=='contact' %}

请先在网站管理后台填写正式联系邮箱。

{% endif %}
{{body_copy(content,page)}}{% if page.cover %}{{page.title}}{% endif %}
+ +{% if form_endpoint %} +
CONTACT FORM

提交业务咨询

提交内容将直接发送到网站管理员配置的外部表单服务。

+

表单提交由外部服务处理,请确认其隐私政策及垃圾信息过滤设置。

+{% endif %} diff --git a/themes/automotive/pages/faq.html b/themes/automotive/pages/faq.html new file mode 100644 index 0000000..945be44 --- /dev/null +++ b/themes/automotive/pages/faq.html @@ -0,0 +1,2 @@ +
{{ 'GET IN TOUCH' if kind=='contact' else ('OUR STORY' if kind=='about' else 'EXPLORE') }}

{{page.title}}

{{page.summary}}

+
{{'CONNECT / CONTACT' if kind=='contact' else 'ABOUT / DISCOVER'}}

{{'让我们开始沟通。' if kind=='contact' else page.title}}

{% if kind=='contact' and footer_email %}{{footer_email}} ↗{% elif kind=='contact' %}

请先在网站管理后台填写正式联系邮箱。

{% endif %}
{{body_copy(content,page)}}{% if page.cover %}{{page.title}}{% endif %}
diff --git a/themes/automotive/pages/home.html b/themes/automotive/pages/home.html new file mode 100644 index 0000000..b69d52e --- /dev/null +++ b/themes/automotive/pages/home.html @@ -0,0 +1,4 @@ +
01 / THE WORLD IN MOTION

{{page.title if page.title not in (site.name,'首页','Home') else site.name}} MOVE BEYOND.

{{page.summary or site.tagline or '探索企业产品与解决方案。'}}

{% if page.cover %}{{page.title}}{% else %}
THE NEXT CHAPTER
{% endif %}DRIVE / 01
DESIGN / TRUST / DELIVERYSCROLL TO DISCOVER ↓
+{% if showcase %}{% endif %} +
THE COMPANY

把产品带向更广阔的市场。

{{site.tagline or '在这里展示企业独特的产品与服务能力。'}}

认识我们 ↗
+{% if brands %}
{{section_title('03 / BRANDS','品牌与合作伙伴','/brands/')}}
{% for b in brands[:12] %}{{b.name}} ↗{% endfor %}
{% endif %} diff --git a/themes/automotive/pages/listing.html b/themes/automotive/pages/listing.html new file mode 100644 index 0000000..3918172 --- /dev/null +++ b/themes/automotive/pages/listing.html @@ -0,0 +1,2 @@ +
DISCOVER / {{'CATEGORY' if kind=='category' else 'COLLECTION'}}

{{page.title}}

{{page.summary or '查看此栏目的全部已发布内容。'}}

首页/{{page.title}}
+
{{ filters() }}
{{grid(listing,'catalog')}}
diff --git a/themes/automotive/pages/notfound.html b/themes/automotive/pages/notfound.html new file mode 100644 index 0000000..37e3f05 --- /dev/null +++ b/themes/automotive/pages/notfound.html @@ -0,0 +1 @@ +
ERROR / 404

404

抱歉,你访问的页面不存在,或者已被移动。

返回首页 ↗
diff --git a/themes/automotive/pages/product.html b/themes/automotive/pages/product.html new file mode 100644 index 0000000..ba1fe4f --- /dev/null +++ b/themes/automotive/pages/product.html @@ -0,0 +1,3 @@ +
首页/产品中心/{{page.title}}
+
PRODUCT DETAILS / {{fields.get('brand') or 'COLLECTION'}}

{{page.title}}

{{page.summary or '在此查看产品详细资料。'}}

{% if fields.get('brand') %}

品牌 · {{fields.get('brand')}}

{% endif %}咨询该产品 ↗
+
OVERVIEW

产品介绍

{{body_copy(content,page)}}
SPECIFICATIONS

规格参数

{% for row in specs %}
{{row.label}}{{row.value}}
{% else %}

尚未填写详细参数。

{% endfor %}
diff --git a/themes/automotive/pages/search.html b/themes/automotive/pages/search.html new file mode 100644 index 0000000..182a878 --- /dev/null +++ b/themes/automotive/pages/search.html @@ -0,0 +1,2 @@ +
SEARCH / CONTENT

站内搜索

搜索本网站的产品、品牌、新闻和案例。

+
diff --git a/themes/automotive/pages/service.html b/themes/automotive/pages/service.html new file mode 100644 index 0000000..945be44 --- /dev/null +++ b/themes/automotive/pages/service.html @@ -0,0 +1,2 @@ +
{{ 'GET IN TOUCH' if kind=='contact' else ('OUR STORY' if kind=='about' else 'EXPLORE') }}

{{page.title}}

{{page.summary}}

+
{{'CONNECT / CONTACT' if kind=='contact' else 'ABOUT / DISCOVER'}}

{{'让我们开始沟通。' if kind=='contact' else page.title}}

{% if kind=='contact' and footer_email %}{{footer_email}} ↗{% elif kind=='contact' %}

请先在网站管理后台填写正式联系邮箱。

{% endif %}
{{body_copy(content,page)}}{% if page.cover %}{{page.title}}{% endif %}
diff --git a/themes/automotive/pages/standard.html b/themes/automotive/pages/standard.html new file mode 100644 index 0000000..945be44 --- /dev/null +++ b/themes/automotive/pages/standard.html @@ -0,0 +1,2 @@ +
{{ 'GET IN TOUCH' if kind=='contact' else ('OUR STORY' if kind=='about' else 'EXPLORE') }}

{{page.title}}

{{page.summary}}

+
{{'CONNECT / CONTACT' if kind=='contact' else 'ABOUT / DISCOVER'}}

{{'让我们开始沟通。' if kind=='contact' else page.title}}

{% if kind=='contact' and footer_email %}{{footer_email}} ↗{% elif kind=='contact' %}

请先在网站管理后台填写正式联系邮箱。

{% endif %}
{{body_copy(content,page)}}{% if page.cover %}{{page.title}}{% endif %}
diff --git a/themes/automotive/pro.css b/themes/automotive/pro.css new file mode 100644 index 0000000..9d84384 --- /dev/null +++ b/themes/automotive/pro.css @@ -0,0 +1,8 @@ +:root{--pro-text:#1c2a3b;--pro-muted:#718094;--pro-border:#e2e8ef;--pro-background:#fff;--pro-surface:#f2f5f9;--pro-accent:#bb8d50;--pro-hero:#0c1b2e;--pro-radius:5px;--pro-headings:'Arial Narrow',Arial,sans-serif;--pro-wide:1320px} +.pro-header{background:#0e1e30;color:#fff;border-color:#26374b}.pro-logo-symbol{color:#e0ae6c}.pro-logo-symbol span{color:#fff}.pro-logo-name small{color:#b3bfcf}.pro-header-cta{background:#c59b63;color:#0f1a26!important}.pro-navigation a:hover{color:#dfb97d}.pro-mega{background:#13263b;border-color:#2a4058;color:#c2d0de}.pro-mega>span{color:#d5aa72}.pro-mobile-toggle{color:#fff} +.pro-auto-hero{background:radial-gradient(circle at 68% 50%,#243b54 0%,#112539 34%,#081728 100%);color:#fff}.pro-auto-hero .pro-eyebrow{color:#d5a366}.pro-auto-hero .pro-hero-copy h1 em{color:#d2a16b}.pro-auto-hero .pro-hero-copy p{color:#b9c7d6}.pro-auto-hero .pro-button{color:#0d1b2d!important;background:#d8ad72}.pro-link-light{color:#d8e4ec;font-size:13px;font-weight:750}.pro-auto-hero .pro-hero-art{background:linear-gradient(160deg,#243e59,#0f2033);border:1px solid #ffffff25;box-shadow:0 50px 80px #0005;border-radius:3px;overflow:visible}.pro-abstract-car{position:absolute;inset:0;overflow:hidden;background:radial-gradient(ellipse at 50% 80%,#45648599,transparent 63%)}.pro-abstract-car:before{content:'';position:absolute;left:10%;right:9%;bottom:29%;height:29%;border:3px solid #d3dfefad;border-radius:53% 49% 10% 12%;transform:skewX(-9deg);box-shadow:0 23px 15px #05121f88,0 0 0 3px #182c41 inset}.pro-abstract-car:after{content:'';position:absolute;left:23%;right:23%;bottom:56%;height:17%;border:3px solid #d3dfef72;border-radius:85% 85% 0 0;transform:skewX(-20deg)}.pro-abstract-car>span{position:absolute;top:26px;right:22px;font-size:10px;letter-spacing:.2em;color:#ccd9e5}.auto-wheel{position:absolute;bottom:18%;width:16%;aspect-ratio:1;border-radius:50%;border:11px solid #6f8499;background:#111e2c;box-shadow:0 0 0 4px #111a25}.auto-wheel.a{left:19%}.auto-wheel.b{right:17%}.pro-hero-art>b{position:absolute;bottom:-31px;right:0;font-size:11px;letter-spacing:.2em;color:#bdcde0}.pro-featured{background:#f4f6f8}.pro-featured .pro-card{border:0;box-shadow:0 7px 30px #0f2e4710}.pro-featured .pro-card-cover{background:#d9e2eb}.pro-featured .pro-art{background:linear-gradient(140deg,#dce5ed,#a9baca)}.pro-featured .pro-art-ring{border-color:#eef3f8bb}.pro-statement{background:#12243b;color:#fff}.pro-statement .pro-eyebrow{color:#d3a975}.pro-statement p{color:#aebfd3}.pro-statement h2{font-size:clamp(42px,5vw,70px);max-width:700px}.pro-statement .pro-text-link{color:#dfba83}.pro-brand-section{background:white}.pro-brand-pills a{min-width:150px;text-align:center;background:#f5f7fa}.pro-inner-hero{background:linear-gradient(120deg,#101e31,#273b51);color:#fff}.pro-inner-hero .pro-eyebrow{color:#d0a46b}.pro-inner-hero p,.pro-inner-hero .pro-breadcrumb{color:#c9d1df}.pro-inner-hero .pro-breadcrumb strong{color:#fff}.pro-article-hero{background:#14243a;color:#fff}.pro-article-hero p{color:#bed0df}.pro-footer{background:#071421}.pro-footer-bottom{color:#98aebf}.pro-end-cta{background:#1a324b}.pro-end-cta a{background:#d4a56d;color:#132339!important}.pro-404 h1{color:#b8905c} + +/* SiteForge V5 commercial signature */ +.pro-auto-hero .pro-hero-art{background-image:url("./visual/hero.svg")!important;background-position:center;background-size:cover;} + +.pro-auto-hero{background:linear-gradient(117deg,#081729 0%,#102a43 100%)}.pro-auto-hero .pro-hero-art{border:1px solid #52667a55;border-radius:3px;box-shadow:0 30px 90px #0004;transform:translateX(0)}.pro-auto-hero .pro-hero-art .pro-abstract-car{opacity:0}.pro-hero-bottom{border-top:1px solid #ffffff1c}.pro-brand-pills a{transition:background .2s}.pro-brand-pills a:hover{background:#d6a974;color:#10233a}.pro-card{border-color:#e0e6ee;border-radius:5px}.pro-card-cover{background:linear-gradient(135deg,#edf1f6,#dbe4eb)}.pro-art{background:linear-gradient(130deg,#e0e8ee,#c4d0dc);color:#37536b}.pro-featured{background:#f8fafc}.pro-statement{background:#0d2134;color:white}.pro-statement p{color:#c6d0dd}.pro-statement .pro-text-link{color:#f0b870}.pro-section-head h2{font-weight:800}.pro-footer{background:#08182a;color:#e5e9ed} diff --git a/themes/automotive/pro.html b/themes/automotive/pro.html new file mode 100644 index 0000000..7aaec49 --- /dev/null +++ b/themes/automotive/pro.html @@ -0,0 +1,10 @@ +{{ seo_title }}{% if canonical %}{% endif %}{% if canonical and not noindex %}{% endif %}{% if theme_overrides %}{% endif %}跳转至正文 +{% from '_professional_components.html' import card,cover,grid,section_title,filters,footer_links,body_copy with context %} +
GLOBAL MOBILITY / CURATED SUPPLYDRIVEN BY WHAT'S NEXT ✦
+
取得联系 ↗
{% if brands or categories %}{% endif %}
+{% if not designer_replace %}{% include 'automotive/pages/' ~ kind ~ '.html' %}{% endif %} + +{% if has_custom and kind!='notfound' %}
{{custom_blocks}}
{% else %}
{% endif %} +{% if kind not in ('notfound','search') %}
LET'S CONNECT

从这里开始下一步。

{{site.tagline or '了解更多产品、服务与合作信息。'}}

联系我们 ↗
{% endif %} +
+ \ No newline at end of file diff --git a/themes/automotive/site.html b/themes/automotive/site.html new file mode 100644 index 0000000..a3ceef5 --- /dev/null +++ b/themes/automotive/site.html @@ -0,0 +1,13 @@ +{{seo_title}}{% if canonical %}{% endif %} +
GLOBAL SOURCING · GLOBAL DELIVERYSiteForge Automotive Collection
+
S.{{site.name}}AUTO & MOBILITYGET IN TOUCH ↗
+{% if blueprint_mode %}{% elif mode=='home' %}
GLOBAL AUTOMOTIVE NETWORK

{{page.title if page.title!=site.name else 'MOVE BEYOND'}}

{{page.summary or site.tagline or 'Discover global mobility and automotive supply solutions.'}}

EXPLORE OUR FLEET ↗Built for the road ahead.
{% if page.cover %}{{page.title}}{% else %}
{% endif %}
01 / EXPLORE
DISCOVER MOREQUALITY · TRUST · WORLDWIDE↓
+{% elif mode=='listing' %}
THE COLLECTION

{{page.title}}

{{page.summary}}

+{% elif mode=='notfound' %}

404

We couldn't find that page.

+{% else %}
{{type_names.get(page.type,'PAGE')|upper}} / THE DETAILS

{{page.title}}

{{page.summary}}

{% endif %} +{% if blueprint_mode %}
{{blocks}}
{% else %}
{% if mode in ['detail','home'] and (content or blocks) %}
{{blocks}}{{content}}
{% endif %} +{% if mode=='detail' and page.cover %}
{{page.title}}
{% endif %} +{% if mode=='listing' %}
THE SELECTION

Explore the collection

{% endif %} +{% if mode=='home' %}{% set showcase=entries|selectattr('type','equalto','product')|list %}{% if showcase %}
CURATED MOBILITY

Featured Vehicles

VIEW COLLECTION ↗
{% endif %}{% endif %} +
+{% endif %}

{{site.tagline}}

diff --git a/themes/automotive/theme.css b/themes/automotive/theme.css new file mode 100644 index 0000000..2786b24 --- /dev/null +++ b/themes/automotive/theme.css @@ -0,0 +1 @@ +:root{--accent:#ecb75c;--ink:#f6f7fa;--body:#101318}*{box-sizing:border-box}body{margin:0;background:#f7f7f7;color:#1b232c;font:16px/1.65 Inter,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}a{text-decoration:none;color:inherit}img{max-width:100%;object-fit:cover}.utility{height:32px;display:flex;justify-content:space-between;align-items:center;background:#090d13;color:#919da8;padding:0 5%;font-size:10px;letter-spacing:2px}.header{height:90px;background:#121922;color:#fff;display:flex;align-items:center;justify-content:space-between;padding:0 5%;gap:24px}.brand{font-size:22px;font-weight:780;display:flex;align-items:center;gap:14px;line-height:1.15}.brand small{display:block;font-size:9px;color:#9ba4b4;letter-spacing:2.5px;margin-top:5px}.mark{background:var(--accent);border-radius:3px;color:#111;padding:10px;font-size:19px}.header nav{display:flex;flex-wrap:wrap;gap:26px;font-size:12px;letter-spacing:1px}.header nav a:hover{color:var(--accent)}.nav-cta{border:1px solid #505a62;padding:12px 17px;color:#e6c893;font-size:11px;letter-spacing:1px}.hero{background:radial-gradient(circle at 80% 55%,#2d4359 0%,#141e2c 34%,#0b121b 75%);color:white;min-height:580px}.hero-inner{max-width:1450px;margin:auto;padding:70px 5% 50px;display:grid;grid-template-columns:51% 49%;align-items:center;min-height:520px}.eyebrow{font-size:11px;letter-spacing:3px;font-weight:700;color:#e9b962}.glow{display:inline-block;width:8px;height:8px;background:#efbf6e;border-radius:100%;margin-right:12px;box-shadow:0 0 16px #edc36d}.hero h1{font-size:clamp(54px,7.2vw,116px);font-weight:850;letter-spacing:-6px;line-height:.95;margin:29px 0}.hero p{font-size:17px;color:#b6c4d1;max-width:470px}.hero-actions{display:flex;align-items:center;gap:30px;margin-top:34px}.btn{display:inline-flex;align-items:center;justify-content:space-between;gap:32px;padding:17px 23px;background:#d5a962;color:#12181d;font-size:12px;font-weight:850;letter-spacing:.8px}.hero-hint{color:#89929b;font-size:12px}.hero-visual{height:330px;position:relative;display:flex;align-items:center;justify-content:center}.hero-visual img{max-height:360px;width:100%;object-fit:contain;filter:drop-shadow(0 16px 23px #0008)}.hero-no{position:absolute;right:0;bottom:-70px;color:#fff6;font-size:10px;letter-spacing:3px}.car-shape{height:220px;width:100%;position:relative;filter:drop-shadow(0 28px 16px #0007)}.car-body{position:absolute;inset:45px 0 24px;border-radius:90px 130px 36px 35px;background:linear-gradient(170deg,#c7d0d9,#60788a 37%,#142433 48%,#202d3d 70%);clip-path:polygon(0 70%,13% 47%,31% 38%,47% 6%,75% 6%,88% 39%,100% 56%,98% 95%,3% 95%)}.wheel{position:absolute;width:80px;height:80px;bottom:0;border-radius:100%;background:radial-gradient(circle,#71808b 0 25%,#101722 28% 50%,#354355 54% 61%,#0a0d13 64%)}.wheel-one{left:19%}.wheel-two{right:13%}.hero-bottom{border-top:1px solid #344052;padding:18px 5%;display:flex;justify-content:space-between;color:#9fa9b5;font-size:10px;letter-spacing:2px}.inner-hero{background:linear-gradient(110deg,#0d1925,#2a3f50);color:white;padding:100px 6% 90px}.inner-hero h1{font-size:clamp(40px,6vw,84px);line-height:1.1;letter-spacing:-2px;margin:16px 0}.inner-hero p{color:#c0ccd7}.container{max-width:1220px;margin:auto;padding:0 24px}.main{padding:75px 0 110px}.section-title{display:flex;align-items:end;gap:20px;justify-content:space-between;margin:30px 0 28px}.section-title span{font-size:11px;letter-spacing:2px;color:#9f7844}.section-title h2{font-size:37px;letter-spacing:-1.5px;flex:1;margin:0}.section-title a{font-size:12px;font-weight:700}.card-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:24px}.model-card{background:#fff;transition:transform .2s,box-shadow .2s;min-width:0}.model-card:hover{transform:translateY(-4px);box-shadow:0 14px 35px #15273822}.card-visual{height:210px;background:linear-gradient(120deg,#dae2e8,#abbcc7);display:flex;align-items:center;justify-content:center;overflow:hidden}.card-visual img{height:100%;width:100%;object-fit:contain}.card-monogram{font-size:75px;font-weight:900;letter-spacing:-8px;color:#ffffff77}.card-copy{padding:24px}.card-copy span{color:#ae854c;font-size:10px;letter-spacing:2px}.card-copy h3{font-size:24px;margin:8px 0}.card-copy p{font-size:13px;color:#73808b}.card-copy b{font-size:11px;letter-spacing:1px;color:#a6793f}.detail-media{margin:40px auto;text-align:center;max-height:600px}.detail-media img{max-height:600px}.rich-content{margin-bottom:55px}.prose{max-width:860px}.rich-content p{color:#596574}.section{padding:45px 0}.section-heading h2{font-size:38px}.block-hero{padding:60px 0}.block-hero h1{font-size:52px}.feature-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:20px}.feature{padding:26px;background:white}.feature-mark{color:#b08756;font-size:12px}.stats{display:grid;grid-template-columns:repeat(4,1fr);gap:20px}.stat strong{display:block;font-size:35px}.stat span{color:#71808c}.cta{background:#19283a;color:white;padding:55px}.cta p{color:#b8c8d6}footer{background:#0d151f;color:white;padding:70px 5% 25px}.footer-brand{font-size:25px;font-weight:750}.footer-brand span{font-size:11px;font-weight:400;color:#aeb8c2}footer p{color:#8593a2}.footer-bottom{border-top:1px solid #30404c;padding-top:25px;margin-top:65px;display:flex;justify-content:space-between;font-size:10px;letter-spacing:1px;color:#8995a3}@media(max-width:860px){.utility{font-size:8px}.header{height:auto;padding:18px 5%;align-items:flex-start;flex-wrap:wrap}.header nav{order:3;width:100%;gap:18px}.nav-cta{display:none}.hero-inner{grid-template-columns:1fr;padding-top:55px}.hero h1{letter-spacing:-3px}.hero-visual{height:210px}.hero-no{bottom:0}.card-grid,.feature-grid{grid-template-columns:repeat(2,1fr)}}@media(max-width:560px){.card-grid,.feature-grid,.stats{grid-template-columns:1fr}.hero h1{font-size:56px}.hero-actions{flex-wrap:wrap}.section-title{align-items:start;flex-direction:column}.inner-hero{padding:75px 6%}.hero-bottom{font-size:8px}} diff --git a/themes/automotive/versions/0.3.0/manifest.json b/themes/automotive/versions/0.3.0/manifest.json new file mode 100644 index 0000000..569776d --- /dev/null +++ b/themes/automotive/versions/0.3.0/manifest.json @@ -0,0 +1,38 @@ +{ + "id": "automotive", + "name": "汽车与出行", + "description": "深蓝色与暖金点缀,大图汽车、车型展示", + "version": "0.3.0", + "style": "luxury", + "entry": "site.html", + "styles": [ + "theme.css", + "blocks.css" + ], + "page_templates": [ + "home", + "about", + "contact", + "listing", + "product", + "article", + "brand", + "standard", + "notfound" + ], + "blocks": [ + "hero", + "richtext", + "features", + "stats", + "collection", + "image_text", + "gallery", + "faq", + "cta", + "attributes", + "spacer" + ], + "responsive": true, + "auto_binding": true +} \ No newline at end of file diff --git a/themes/automotive/versions/0.3.0/page-presets.json b/themes/automotive/versions/0.3.0/page-presets.json new file mode 100644 index 0000000..637b6ae --- /dev/null +++ b/themes/automotive/versions/0.3.0/page-presets.json @@ -0,0 +1,245 @@ +{ + "home": [ + { + "id": "ff2755fabe", + "type": "hero", + "visible": true, + "title": "探索全球汽车新可能", + "subtitle": "THE WORLD OF MOBILITY", + "description": "{{site.tagline}}", + "button": "浏览车型", + "href": "/product/", + "align": "left" + }, + { + "id": "f997b5975c", + "type": "collection", + "visible": true, + "title": "精选车型", + "subtitle": "EXPLORE OUR COLLECTION", + "source": "product", + "limit": 6, + "columns": 3, + "button": "查看全部", + "href": "/product/" + }, + { + "id": "b79e9bdeee", + "type": "stats", + "visible": true, + "title": "值得信赖的服务", + "items": [ + { + "value": "50,000+", + "label": "服务案例" + }, + { + "value": "70+", + "label": "市场覆盖" + }, + { + "value": "30+", + "label": "合作伙伴" + } + ] + }, + { + "id": "fd3475d4a9", + "type": "image_text", + "visible": true, + "title": "专业采购与全球交付", + "subtitle": "GLOBAL DELIVERY", + "description": "为海外市场提供适合的汽车采购与服务解决方案。", + "image": "", + "image_side": "right", + "button": "了解更多", + "href": "/about/" + }, + { + "id": "5f159c1114", + "type": "cta", + "visible": true, + "title": "开启下一次合作", + "subtitle": "告诉我们您的需求,我们将提供适合的方案。", + "button": "联系我们", + "href": "/contact/" + } + ], + "about": [ + { + "id": "3b57a5c80a", + "type": "image_text", + "visible": true, + "title": "认识我们", + "subtitle": "OUR APPROACH", + "description": "展示业务流程和服务理念。", + "image": "", + "image_side": "right", + "button": "了解更多", + "href": "/about/" + }, + { + "id": "c7aaebcbc8", + "type": "stats", + "visible": true, + "title": "我们的实力", + "items": [ + { + "value": "50,000+", + "label": "服务案例" + }, + { + "value": "70+", + "label": "市场覆盖" + }, + { + "value": "30+", + "label": "合作伙伴" + } + ] + }, + { + "id": "f0d35431bc", + "type": "features", + "visible": true, + "title": "我们的优势", + "items": [ + { + "title": "专业服务", + "text": "从咨询到交付" + }, + { + "title": "品质保障", + "text": "严格的质量流程" + }, + { + "title": "全球网络", + "text": "连接不同市场" + } + ] + } + ], + "contact": [ + { + "id": "a21100b5fd", + "type": "richtext", + "visible": true, + "title": "联系我们", + "body": "

请填写企业联系方式与地址。

" + }, + { + "id": "360304f1f1", + "type": "cta", + "visible": true, + "title": "欢迎联系", + "subtitle": "告诉我们您的需求,我们将提供适合的方案。", + "button": "联系我们", + "href": "/contact/" + } + ], + "listing": [ + { + "id": "e8c990ef8b", + "type": "collection", + "visible": true, + "title": "所有在售车型", + "subtitle": "为客户提供适合的解决方案", + "source": "product", + "limit": 24, + "columns": 3, + "button": "查看全部", + "href": "/product/" + } + ], + "product": [ + { + "id": "7dfe1376b0", + "type": "image_text", + "visible": true, + "title": "产品介绍", + "subtitle": "OUR APPROACH", + "description": "{{page.summary}}", + "image": "{{page.cover}}", + "image_side": "right", + "button": "了解更多", + "href": "/about/" + }, + { + "id": "c3a67474f4", + "type": "attributes", + "visible": true, + "title": "产品规格与参数" + }, + { + "id": "29b39f194a", + "type": "richtext", + "visible": true, + "title": "产品详情", + "body": "{{page.body}}" + }, + { + "id": "70d24b5293", + "type": "cta", + "visible": true, + "title": "开启新的合作", + "subtitle": "告诉我们您的需求,我们将提供适合的方案。", + "button": "联系我们", + "href": "/contact/" + } + ], + "article": [ + { + "id": "f283231732", + "type": "richtext", + "visible": true, + "title": "{{page.title}}", + "body": "{{page.body}}" + } + ], + "brand": [ + { + "id": "e08f6ad68f", + "type": "image_text", + "visible": true, + "title": "{{page.title}}", + "subtitle": "OUR APPROACH", + "description": "{{page.summary}}", + "image": "{{page.cover}}", + "image_side": "right", + "button": "了解更多", + "href": "/about/" + }, + { + "id": "0f3689e9f0", + "type": "collection", + "visible": true, + "title": "品牌产品", + "subtitle": "为客户提供适合的解决方案", + "source": "product", + "limit": 6, + "columns": 3, + "button": "查看全部", + "href": "/product/" + } + ], + "standard": [ + { + "id": "d47d90fe91", + "type": "richtext", + "visible": true, + "title": "{{page.title}}", + "body": "{{page.body}}" + } + ], + "notfound": [ + { + "id": "8b39fc46a4", + "type": "cta", + "visible": true, + "title": "页面暂时无法访问", + "subtitle": "告诉我们您的需求,我们将提供适合的方案。", + "button": "返回首页", + "href": "/" + } + ] +} \ No newline at end of file diff --git a/themes/automotive/versions/0.3.0/site.html b/themes/automotive/versions/0.3.0/site.html new file mode 100644 index 0000000..a3ceef5 --- /dev/null +++ b/themes/automotive/versions/0.3.0/site.html @@ -0,0 +1,13 @@ +{{seo_title}}{% if canonical %}{% endif %} +
GLOBAL SOURCING · GLOBAL DELIVERYSiteForge Automotive Collection
+
S.{{site.name}}AUTO & MOBILITYGET IN TOUCH ↗
+{% if blueprint_mode %}{% elif mode=='home' %}
GLOBAL AUTOMOTIVE NETWORK

{{page.title if page.title!=site.name else 'MOVE BEYOND'}}

{{page.summary or site.tagline or 'Discover global mobility and automotive supply solutions.'}}

EXPLORE OUR FLEET ↗Built for the road ahead.
{% if page.cover %}{{page.title}}{% else %}
{% endif %}
01 / EXPLORE
DISCOVER MOREQUALITY · TRUST · WORLDWIDE↓
+{% elif mode=='listing' %}
THE COLLECTION

{{page.title}}

{{page.summary}}

+{% elif mode=='notfound' %}

404

We couldn't find that page.

+{% else %}
{{type_names.get(page.type,'PAGE')|upper}} / THE DETAILS

{{page.title}}

{{page.summary}}

{% endif %} +{% if blueprint_mode %}
{{blocks}}
{% else %}
{% if mode in ['detail','home'] and (content or blocks) %}
{{blocks}}{{content}}
{% endif %} +{% if mode=='detail' and page.cover %}
{{page.title}}
{% endif %} +{% if mode=='listing' %}
THE SELECTION

Explore the collection

{% endif %} +{% if mode=='home' %}{% set showcase=entries|selectattr('type','equalto','product')|list %}{% if showcase %}
CURATED MOBILITY

Featured Vehicles

VIEW COLLECTION ↗
{% endif %}{% endif %} +
+{% endif %}

{{site.tagline}}

diff --git a/themes/automotive/versions/0.3.0/theme.css b/themes/automotive/versions/0.3.0/theme.css new file mode 100644 index 0000000..2786b24 --- /dev/null +++ b/themes/automotive/versions/0.3.0/theme.css @@ -0,0 +1 @@ +:root{--accent:#ecb75c;--ink:#f6f7fa;--body:#101318}*{box-sizing:border-box}body{margin:0;background:#f7f7f7;color:#1b232c;font:16px/1.65 Inter,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}a{text-decoration:none;color:inherit}img{max-width:100%;object-fit:cover}.utility{height:32px;display:flex;justify-content:space-between;align-items:center;background:#090d13;color:#919da8;padding:0 5%;font-size:10px;letter-spacing:2px}.header{height:90px;background:#121922;color:#fff;display:flex;align-items:center;justify-content:space-between;padding:0 5%;gap:24px}.brand{font-size:22px;font-weight:780;display:flex;align-items:center;gap:14px;line-height:1.15}.brand small{display:block;font-size:9px;color:#9ba4b4;letter-spacing:2.5px;margin-top:5px}.mark{background:var(--accent);border-radius:3px;color:#111;padding:10px;font-size:19px}.header nav{display:flex;flex-wrap:wrap;gap:26px;font-size:12px;letter-spacing:1px}.header nav a:hover{color:var(--accent)}.nav-cta{border:1px solid #505a62;padding:12px 17px;color:#e6c893;font-size:11px;letter-spacing:1px}.hero{background:radial-gradient(circle at 80% 55%,#2d4359 0%,#141e2c 34%,#0b121b 75%);color:white;min-height:580px}.hero-inner{max-width:1450px;margin:auto;padding:70px 5% 50px;display:grid;grid-template-columns:51% 49%;align-items:center;min-height:520px}.eyebrow{font-size:11px;letter-spacing:3px;font-weight:700;color:#e9b962}.glow{display:inline-block;width:8px;height:8px;background:#efbf6e;border-radius:100%;margin-right:12px;box-shadow:0 0 16px #edc36d}.hero h1{font-size:clamp(54px,7.2vw,116px);font-weight:850;letter-spacing:-6px;line-height:.95;margin:29px 0}.hero p{font-size:17px;color:#b6c4d1;max-width:470px}.hero-actions{display:flex;align-items:center;gap:30px;margin-top:34px}.btn{display:inline-flex;align-items:center;justify-content:space-between;gap:32px;padding:17px 23px;background:#d5a962;color:#12181d;font-size:12px;font-weight:850;letter-spacing:.8px}.hero-hint{color:#89929b;font-size:12px}.hero-visual{height:330px;position:relative;display:flex;align-items:center;justify-content:center}.hero-visual img{max-height:360px;width:100%;object-fit:contain;filter:drop-shadow(0 16px 23px #0008)}.hero-no{position:absolute;right:0;bottom:-70px;color:#fff6;font-size:10px;letter-spacing:3px}.car-shape{height:220px;width:100%;position:relative;filter:drop-shadow(0 28px 16px #0007)}.car-body{position:absolute;inset:45px 0 24px;border-radius:90px 130px 36px 35px;background:linear-gradient(170deg,#c7d0d9,#60788a 37%,#142433 48%,#202d3d 70%);clip-path:polygon(0 70%,13% 47%,31% 38%,47% 6%,75% 6%,88% 39%,100% 56%,98% 95%,3% 95%)}.wheel{position:absolute;width:80px;height:80px;bottom:0;border-radius:100%;background:radial-gradient(circle,#71808b 0 25%,#101722 28% 50%,#354355 54% 61%,#0a0d13 64%)}.wheel-one{left:19%}.wheel-two{right:13%}.hero-bottom{border-top:1px solid #344052;padding:18px 5%;display:flex;justify-content:space-between;color:#9fa9b5;font-size:10px;letter-spacing:2px}.inner-hero{background:linear-gradient(110deg,#0d1925,#2a3f50);color:white;padding:100px 6% 90px}.inner-hero h1{font-size:clamp(40px,6vw,84px);line-height:1.1;letter-spacing:-2px;margin:16px 0}.inner-hero p{color:#c0ccd7}.container{max-width:1220px;margin:auto;padding:0 24px}.main{padding:75px 0 110px}.section-title{display:flex;align-items:end;gap:20px;justify-content:space-between;margin:30px 0 28px}.section-title span{font-size:11px;letter-spacing:2px;color:#9f7844}.section-title h2{font-size:37px;letter-spacing:-1.5px;flex:1;margin:0}.section-title a{font-size:12px;font-weight:700}.card-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:24px}.model-card{background:#fff;transition:transform .2s,box-shadow .2s;min-width:0}.model-card:hover{transform:translateY(-4px);box-shadow:0 14px 35px #15273822}.card-visual{height:210px;background:linear-gradient(120deg,#dae2e8,#abbcc7);display:flex;align-items:center;justify-content:center;overflow:hidden}.card-visual img{height:100%;width:100%;object-fit:contain}.card-monogram{font-size:75px;font-weight:900;letter-spacing:-8px;color:#ffffff77}.card-copy{padding:24px}.card-copy span{color:#ae854c;font-size:10px;letter-spacing:2px}.card-copy h3{font-size:24px;margin:8px 0}.card-copy p{font-size:13px;color:#73808b}.card-copy b{font-size:11px;letter-spacing:1px;color:#a6793f}.detail-media{margin:40px auto;text-align:center;max-height:600px}.detail-media img{max-height:600px}.rich-content{margin-bottom:55px}.prose{max-width:860px}.rich-content p{color:#596574}.section{padding:45px 0}.section-heading h2{font-size:38px}.block-hero{padding:60px 0}.block-hero h1{font-size:52px}.feature-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:20px}.feature{padding:26px;background:white}.feature-mark{color:#b08756;font-size:12px}.stats{display:grid;grid-template-columns:repeat(4,1fr);gap:20px}.stat strong{display:block;font-size:35px}.stat span{color:#71808c}.cta{background:#19283a;color:white;padding:55px}.cta p{color:#b8c8d6}footer{background:#0d151f;color:white;padding:70px 5% 25px}.footer-brand{font-size:25px;font-weight:750}.footer-brand span{font-size:11px;font-weight:400;color:#aeb8c2}footer p{color:#8593a2}.footer-bottom{border-top:1px solid #30404c;padding-top:25px;margin-top:65px;display:flex;justify-content:space-between;font-size:10px;letter-spacing:1px;color:#8995a3}@media(max-width:860px){.utility{font-size:8px}.header{height:auto;padding:18px 5%;align-items:flex-start;flex-wrap:wrap}.header nav{order:3;width:100%;gap:18px}.nav-cta{display:none}.hero-inner{grid-template-columns:1fr;padding-top:55px}.hero h1{letter-spacing:-3px}.hero-visual{height:210px}.hero-no{bottom:0}.card-grid,.feature-grid{grid-template-columns:repeat(2,1fr)}}@media(max-width:560px){.card-grid,.feature-grid,.stats{grid-template-columns:1fr}.hero h1{font-size:56px}.hero-actions{flex-wrap:wrap}.section-title{align-items:start;flex-direction:column}.inner-hero{padding:75px 6%}.hero-bottom{font-size:8px}} diff --git a/themes/automotive/versions/1.0.0/manifest.json b/themes/automotive/versions/1.0.0/manifest.json new file mode 100644 index 0000000..e1f5d5b --- /dev/null +++ b/themes/automotive/versions/1.0.0/manifest.json @@ -0,0 +1,43 @@ +{ + "id": "automotive", + "name": "汽车与出行", + "description": "深蓝色与暖金点缀,大图汽车、车型展示;V3 专业全页模板、分类/详情/导航和搜索", + "version": "1.0.0", + "style": "luxury", + "entry": "pro.html", + "styles": [ + "theme.css", + "blocks.css" + ], + "page_templates": [ + "home", + "about", + "contact", + "listing", + "category", + "product", + "article", + "brand", + "case", + "service", + "faq", + "standard", + "notfound" + ], + "blocks": [ + "hero", + "richtext", + "features", + "stats", + "collection", + "image_text", + "gallery", + "faq", + "cta", + "attributes", + "spacer" + ], + "responsive": true, + "auto_binding": true, + "professional_layout": true +} \ No newline at end of file diff --git a/themes/automotive/versions/1.0.0/page-presets.json b/themes/automotive/versions/1.0.0/page-presets.json new file mode 100644 index 0000000..315b9c2 --- /dev/null +++ b/themes/automotive/versions/1.0.0/page-presets.json @@ -0,0 +1,285 @@ +{ + "home": [ + { + "id": "ff2755fabe", + "type": "hero", + "visible": true, + "title": "探索全球汽车新可能", + "subtitle": "THE WORLD OF MOBILITY", + "description": "{{site.tagline}}", + "button": "浏览车型", + "href": "/product/", + "align": "left" + }, + { + "id": "f997b5975c", + "type": "collection", + "visible": true, + "title": "精选车型", + "subtitle": "EXPLORE OUR COLLECTION", + "source": "product", + "limit": 6, + "columns": 3, + "button": "查看全部", + "href": "/product/" + }, + { + "id": "b79e9bdeee", + "type": "stats", + "visible": true, + "title": "值得信赖的服务", + "items": [ + { + "value": "50,000+", + "label": "服务案例" + }, + { + "value": "70+", + "label": "市场覆盖" + }, + { + "value": "30+", + "label": "合作伙伴" + } + ] + }, + { + "id": "fd3475d4a9", + "type": "image_text", + "visible": true, + "title": "专业采购与全球交付", + "subtitle": "GLOBAL DELIVERY", + "description": "为海外市场提供适合的汽车采购与服务解决方案。", + "image": "", + "image_side": "right", + "button": "了解更多", + "href": "/about/" + }, + { + "id": "5f159c1114", + "type": "cta", + "visible": true, + "title": "开启下一次合作", + "subtitle": "告诉我们您的需求,我们将提供适合的方案。", + "button": "联系我们", + "href": "/contact/" + } + ], + "about": [ + { + "id": "3b57a5c80a", + "type": "image_text", + "visible": true, + "title": "认识我们", + "subtitle": "OUR APPROACH", + "description": "展示业务流程和服务理念。", + "image": "", + "image_side": "right", + "button": "了解更多", + "href": "/about/" + }, + { + "id": "c7aaebcbc8", + "type": "stats", + "visible": true, + "title": "我们的实力", + "items": [ + { + "value": "50,000+", + "label": "服务案例" + }, + { + "value": "70+", + "label": "市场覆盖" + }, + { + "value": "30+", + "label": "合作伙伴" + } + ] + }, + { + "id": "f0d35431bc", + "type": "features", + "visible": true, + "title": "我们的优势", + "items": [ + { + "title": "专业服务", + "text": "从咨询到交付" + }, + { + "title": "品质保障", + "text": "严格的质量流程" + }, + { + "title": "全球网络", + "text": "连接不同市场" + } + ] + } + ], + "contact": [ + { + "id": "a21100b5fd", + "type": "richtext", + "visible": true, + "title": "联系我们", + "body": "

请填写企业联系方式与地址。

" + }, + { + "id": "360304f1f1", + "type": "cta", + "visible": true, + "title": "欢迎联系", + "subtitle": "告诉我们您的需求,我们将提供适合的方案。", + "button": "联系我们", + "href": "/contact/" + } + ], + "listing": [ + { + "id": "e8c990ef8b", + "type": "collection", + "visible": true, + "title": "所有在售车型", + "subtitle": "为客户提供适合的解决方案", + "source": "product", + "limit": 24, + "columns": 3, + "button": "查看全部", + "href": "/product/" + } + ], + "product": [ + { + "id": "7dfe1376b0", + "type": "image_text", + "visible": true, + "title": "产品介绍", + "subtitle": "OUR APPROACH", + "description": "{{page.summary}}", + "image": "{{page.cover}}", + "image_side": "right", + "button": "了解更多", + "href": "/about/" + }, + { + "id": "c3a67474f4", + "type": "attributes", + "visible": true, + "title": "产品规格与参数" + }, + { + "id": "29b39f194a", + "type": "richtext", + "visible": true, + "title": "产品详情", + "body": "{{page.body}}" + }, + { + "id": "70d24b5293", + "type": "cta", + "visible": true, + "title": "开启新的合作", + "subtitle": "告诉我们您的需求,我们将提供适合的方案。", + "button": "联系我们", + "href": "/contact/" + } + ], + "article": [ + { + "id": "f283231732", + "type": "richtext", + "visible": true, + "title": "{{page.title}}", + "body": "{{page.body}}" + } + ], + "brand": [ + { + "id": "e08f6ad68f", + "type": "image_text", + "visible": true, + "title": "{{page.title}}", + "subtitle": "OUR APPROACH", + "description": "{{page.summary}}", + "image": "{{page.cover}}", + "image_side": "right", + "button": "了解更多", + "href": "/about/" + }, + { + "id": "0f3689e9f0", + "type": "collection", + "visible": true, + "title": "品牌产品", + "subtitle": "为客户提供适合的解决方案", + "source": "product", + "limit": 6, + "columns": 3, + "button": "查看全部", + "href": "/product/" + } + ], + "standard": [ + { + "id": "d47d90fe91", + "type": "richtext", + "visible": true, + "title": "{{page.title}}", + "body": "{{page.body}}" + } + ], + "notfound": [ + { + "id": "8b39fc46a4", + "type": "cta", + "visible": true, + "title": "页面暂时无法访问", + "subtitle": "告诉我们您的需求,我们将提供适合的方案。", + "button": "返回首页", + "href": "/" + } + ], + "case": [ + { + "type": "image_text", + "title": "{{page.title}}", + "image": "{{page.cover}}", + "description": "{{page.summary}}" + }, + { + "type": "richtext", + "title": "项目概述", + "body": "{{page.body}}" + } + ], + "service": [ + { + "type": "richtext", + "title": "{{page.title}}", + "body": "{{page.body}}" + }, + { + "type": "cta", + "title": "了解更多", + "href": "/contact/" + } + ], + "faq": [ + { + "type": "faq", + "title": "常见问题", + "items": [] + } + ], + "category": [ + { + "type": "collection", + "title": "分类产品", + "source": "product", + "limit": 24 + } + ] +} \ No newline at end of file diff --git a/themes/automotive/versions/1.0.0/pages/about.html b/themes/automotive/versions/1.0.0/pages/about.html new file mode 100644 index 0000000..945be44 --- /dev/null +++ b/themes/automotive/versions/1.0.0/pages/about.html @@ -0,0 +1,2 @@ +
{{ 'GET IN TOUCH' if kind=='contact' else ('OUR STORY' if kind=='about' else 'EXPLORE') }}

{{page.title}}

{{page.summary}}

+
{{'CONNECT / CONTACT' if kind=='contact' else 'ABOUT / DISCOVER'}}

{{'让我们开始沟通。' if kind=='contact' else page.title}}

{% if kind=='contact' and footer_email %}{{footer_email}} ↗{% elif kind=='contact' %}

请先在网站管理后台填写正式联系邮箱。

{% endif %}
{{body_copy(content,page)}}{% if page.cover %}{{page.title}}{% endif %}
diff --git a/themes/automotive/versions/1.0.0/pages/article.html b/themes/automotive/versions/1.0.0/pages/article.html new file mode 100644 index 0000000..85626e4 --- /dev/null +++ b/themes/automotive/versions/1.0.0/pages/article.html @@ -0,0 +1 @@ +
LATEST NEWS / INSIGHTS

{{page.title}}

{{page.summary}}

{% if fields.get('publish_date') %}{% endif %}
diff --git a/themes/automotive/versions/1.0.0/pages/brand.html b/themes/automotive/versions/1.0.0/pages/brand.html new file mode 100644 index 0000000..a710280 --- /dev/null +++ b/themes/automotive/versions/1.0.0/pages/brand.html @@ -0,0 +1 @@ +
BRAND PROFILE / PARTNERS

{{page.title}}

{{page.summary}}

{{cover(page,'brand')}}
{{body_copy(content,page)}}{% if related %}{{section_title('PRODUCT LINE','相关产品','/products/')}}{{grid(related,'product')}}{% endif %}
diff --git a/themes/automotive/versions/1.0.0/pages/case.html b/themes/automotive/versions/1.0.0/pages/case.html new file mode 100644 index 0000000..79e4d72 --- /dev/null +++ b/themes/automotive/versions/1.0.0/pages/case.html @@ -0,0 +1 @@ +
SELECTED WORK / PROJECT

{{page.title}}

{{page.summary}}

{% if page.cover %}{{page.title}}{% endif %}
{% if fields.get('location') %}项目地点
{{fields.get('location')}}
{% endif %}{% if fields.get('industry') %}所属行业
{{fields.get('industry')}}
{% endif %}
{{body_copy(content,page)}}浏览更多案例 →
diff --git a/themes/automotive/versions/1.0.0/pages/category.html b/themes/automotive/versions/1.0.0/pages/category.html new file mode 100644 index 0000000..3918172 --- /dev/null +++ b/themes/automotive/versions/1.0.0/pages/category.html @@ -0,0 +1,2 @@ +
DISCOVER / {{'CATEGORY' if kind=='category' else 'COLLECTION'}}

{{page.title}}

{{page.summary or '查看此栏目的全部已发布内容。'}}

首页/{{page.title}}
+
{{ filters() }}
{{grid(listing,'catalog')}}
diff --git a/themes/automotive/versions/1.0.0/pages/contact.html b/themes/automotive/versions/1.0.0/pages/contact.html new file mode 100644 index 0000000..3a52a60 --- /dev/null +++ b/themes/automotive/versions/1.0.0/pages/contact.html @@ -0,0 +1,7 @@ +
{{ 'GET IN TOUCH' if kind=='contact' else ('OUR STORY' if kind=='about' else 'EXPLORE') }}

{{page.title}}

{{page.summary}}

+
{{'CONNECT / CONTACT' if kind=='contact' else 'ABOUT / DISCOVER'}}

{{'让我们开始沟通。' if kind=='contact' else page.title}}

{% if kind=='contact' and footer_email %}{{footer_email}} ↗{% elif kind=='contact' %}

请先在网站管理后台填写正式联系邮箱。

{% endif %}
{{body_copy(content,page)}}{% if page.cover %}{{page.title}}{% endif %}
+ +{% if form_endpoint %} +
CONTACT FORM

提交业务咨询

提交内容将直接发送到网站管理员配置的外部表单服务。

+

表单提交由外部服务处理,请确认其隐私政策及垃圾信息过滤设置。

+{% endif %} diff --git a/themes/automotive/versions/1.0.0/pages/faq.html b/themes/automotive/versions/1.0.0/pages/faq.html new file mode 100644 index 0000000..945be44 --- /dev/null +++ b/themes/automotive/versions/1.0.0/pages/faq.html @@ -0,0 +1,2 @@ +
{{ 'GET IN TOUCH' if kind=='contact' else ('OUR STORY' if kind=='about' else 'EXPLORE') }}

{{page.title}}

{{page.summary}}

+
{{'CONNECT / CONTACT' if kind=='contact' else 'ABOUT / DISCOVER'}}

{{'让我们开始沟通。' if kind=='contact' else page.title}}

{% if kind=='contact' and footer_email %}{{footer_email}} ↗{% elif kind=='contact' %}

请先在网站管理后台填写正式联系邮箱。

{% endif %}
{{body_copy(content,page)}}{% if page.cover %}{{page.title}}{% endif %}
diff --git a/themes/automotive/versions/1.0.0/pages/home.html b/themes/automotive/versions/1.0.0/pages/home.html new file mode 100644 index 0000000..b69d52e --- /dev/null +++ b/themes/automotive/versions/1.0.0/pages/home.html @@ -0,0 +1,4 @@ +
01 / THE WORLD IN MOTION

{{page.title if page.title not in (site.name,'首页','Home') else site.name}} MOVE BEYOND.

{{page.summary or site.tagline or '探索企业产品与解决方案。'}}

{% if page.cover %}{{page.title}}{% else %}
THE NEXT CHAPTER
{% endif %}DRIVE / 01
DESIGN / TRUST / DELIVERYSCROLL TO DISCOVER ↓
+{% if showcase %}{% endif %} +
THE COMPANY

把产品带向更广阔的市场。

{{site.tagline or '在这里展示企业独特的产品与服务能力。'}}

认识我们 ↗
+{% if brands %}
{{section_title('03 / BRANDS','品牌与合作伙伴','/brands/')}}
{% for b in brands[:12] %}{{b.name}} ↗{% endfor %}
{% endif %} diff --git a/themes/automotive/versions/1.0.0/pages/listing.html b/themes/automotive/versions/1.0.0/pages/listing.html new file mode 100644 index 0000000..3918172 --- /dev/null +++ b/themes/automotive/versions/1.0.0/pages/listing.html @@ -0,0 +1,2 @@ +
DISCOVER / {{'CATEGORY' if kind=='category' else 'COLLECTION'}}

{{page.title}}

{{page.summary or '查看此栏目的全部已发布内容。'}}

首页/{{page.title}}
+
{{ filters() }}
{{grid(listing,'catalog')}}
diff --git a/themes/automotive/versions/1.0.0/pages/notfound.html b/themes/automotive/versions/1.0.0/pages/notfound.html new file mode 100644 index 0000000..37e3f05 --- /dev/null +++ b/themes/automotive/versions/1.0.0/pages/notfound.html @@ -0,0 +1 @@ +
ERROR / 404

404

抱歉,你访问的页面不存在,或者已被移动。

返回首页 ↗
diff --git a/themes/automotive/versions/1.0.0/pages/product.html b/themes/automotive/versions/1.0.0/pages/product.html new file mode 100644 index 0000000..ba1fe4f --- /dev/null +++ b/themes/automotive/versions/1.0.0/pages/product.html @@ -0,0 +1,3 @@ +
首页/产品中心/{{page.title}}
+
PRODUCT DETAILS / {{fields.get('brand') or 'COLLECTION'}}

{{page.title}}

{{page.summary or '在此查看产品详细资料。'}}

{% if fields.get('brand') %}

品牌 · {{fields.get('brand')}}

{% endif %}咨询该产品 ↗
+
OVERVIEW

产品介绍

{{body_copy(content,page)}}
SPECIFICATIONS

规格参数

{% for row in specs %}
{{row.label}}{{row.value}}
{% else %}

尚未填写详细参数。

{% endfor %}
diff --git a/themes/automotive/versions/1.0.0/pages/search.html b/themes/automotive/versions/1.0.0/pages/search.html new file mode 100644 index 0000000..182a878 --- /dev/null +++ b/themes/automotive/versions/1.0.0/pages/search.html @@ -0,0 +1,2 @@ +
SEARCH / CONTENT

站内搜索

搜索本网站的产品、品牌、新闻和案例。

+
diff --git a/themes/automotive/versions/1.0.0/pages/service.html b/themes/automotive/versions/1.0.0/pages/service.html new file mode 100644 index 0000000..945be44 --- /dev/null +++ b/themes/automotive/versions/1.0.0/pages/service.html @@ -0,0 +1,2 @@ +
{{ 'GET IN TOUCH' if kind=='contact' else ('OUR STORY' if kind=='about' else 'EXPLORE') }}

{{page.title}}

{{page.summary}}

+
{{'CONNECT / CONTACT' if kind=='contact' else 'ABOUT / DISCOVER'}}

{{'让我们开始沟通。' if kind=='contact' else page.title}}

{% if kind=='contact' and footer_email %}{{footer_email}} ↗{% elif kind=='contact' %}

请先在网站管理后台填写正式联系邮箱。

{% endif %}
{{body_copy(content,page)}}{% if page.cover %}{{page.title}}{% endif %}
diff --git a/themes/automotive/versions/1.0.0/pages/standard.html b/themes/automotive/versions/1.0.0/pages/standard.html new file mode 100644 index 0000000..945be44 --- /dev/null +++ b/themes/automotive/versions/1.0.0/pages/standard.html @@ -0,0 +1,2 @@ +
{{ 'GET IN TOUCH' if kind=='contact' else ('OUR STORY' if kind=='about' else 'EXPLORE') }}

{{page.title}}

{{page.summary}}

+
{{'CONNECT / CONTACT' if kind=='contact' else 'ABOUT / DISCOVER'}}

{{'让我们开始沟通。' if kind=='contact' else page.title}}

{% if kind=='contact' and footer_email %}{{footer_email}} ↗{% elif kind=='contact' %}

请先在网站管理后台填写正式联系邮箱。

{% endif %}
{{body_copy(content,page)}}{% if page.cover %}{{page.title}}{% endif %}
diff --git a/themes/automotive/versions/1.0.0/pro.css b/themes/automotive/versions/1.0.0/pro.css new file mode 100644 index 0000000..35247c3 --- /dev/null +++ b/themes/automotive/versions/1.0.0/pro.css @@ -0,0 +1,3 @@ +:root{--pro-text:#1c2a3b;--pro-muted:#718094;--pro-border:#e2e8ef;--pro-background:#fff;--pro-surface:#f2f5f9;--pro-accent:#bb8d50;--pro-hero:#0c1b2e;--pro-radius:5px;--pro-headings:'Arial Narrow',Arial,sans-serif;--pro-wide:1320px} +.pro-header{background:#0e1e30;color:#fff;border-color:#26374b}.pro-logo-symbol{color:#e0ae6c}.pro-logo-symbol span{color:#fff}.pro-logo-name small{color:#b3bfcf}.pro-header-cta{background:#c59b63;color:#0f1a26!important}.pro-navigation a:hover{color:#dfb97d}.pro-mega{background:#13263b;border-color:#2a4058;color:#c2d0de}.pro-mega>span{color:#d5aa72}.pro-mobile-toggle{color:#fff} +.pro-auto-hero{background:radial-gradient(circle at 68% 50%,#243b54 0%,#112539 34%,#081728 100%);color:#fff}.pro-auto-hero .pro-eyebrow{color:#d5a366}.pro-auto-hero .pro-hero-copy h1 em{color:#d2a16b}.pro-auto-hero .pro-hero-copy p{color:#b9c7d6}.pro-auto-hero .pro-button{color:#0d1b2d!important;background:#d8ad72}.pro-link-light{color:#d8e4ec;font-size:13px;font-weight:750}.pro-auto-hero .pro-hero-art{background:linear-gradient(160deg,#243e59,#0f2033);border:1px solid #ffffff25;box-shadow:0 50px 80px #0005;border-radius:3px;overflow:visible}.pro-abstract-car{position:absolute;inset:0;overflow:hidden;background:radial-gradient(ellipse at 50% 80%,#45648599,transparent 63%)}.pro-abstract-car:before{content:'';position:absolute;left:10%;right:9%;bottom:29%;height:29%;border:3px solid #d3dfefad;border-radius:53% 49% 10% 12%;transform:skewX(-9deg);box-shadow:0 23px 15px #05121f88,0 0 0 3px #182c41 inset}.pro-abstract-car:after{content:'';position:absolute;left:23%;right:23%;bottom:56%;height:17%;border:3px solid #d3dfef72;border-radius:85% 85% 0 0;transform:skewX(-20deg)}.pro-abstract-car>span{position:absolute;top:26px;right:22px;font-size:10px;letter-spacing:.2em;color:#ccd9e5}.auto-wheel{position:absolute;bottom:18%;width:16%;aspect-ratio:1;border-radius:50%;border:11px solid #6f8499;background:#111e2c;box-shadow:0 0 0 4px #111a25}.auto-wheel.a{left:19%}.auto-wheel.b{right:17%}.pro-hero-art>b{position:absolute;bottom:-31px;right:0;font-size:11px;letter-spacing:.2em;color:#bdcde0}.pro-featured{background:#f4f6f8}.pro-featured .pro-card{border:0;box-shadow:0 7px 30px #0f2e4710}.pro-featured .pro-card-cover{background:#d9e2eb}.pro-featured .pro-art{background:linear-gradient(140deg,#dce5ed,#a9baca)}.pro-featured .pro-art-ring{border-color:#eef3f8bb}.pro-statement{background:#12243b;color:#fff}.pro-statement .pro-eyebrow{color:#d3a975}.pro-statement p{color:#aebfd3}.pro-statement h2{font-size:clamp(42px,5vw,70px);max-width:700px}.pro-statement .pro-text-link{color:#dfba83}.pro-brand-section{background:white}.pro-brand-pills a{min-width:150px;text-align:center;background:#f5f7fa}.pro-inner-hero{background:linear-gradient(120deg,#101e31,#273b51);color:#fff}.pro-inner-hero .pro-eyebrow{color:#d0a46b}.pro-inner-hero p,.pro-inner-hero .pro-breadcrumb{color:#c9d1df}.pro-inner-hero .pro-breadcrumb strong{color:#fff}.pro-article-hero{background:#14243a;color:#fff}.pro-article-hero p{color:#bed0df}.pro-footer{background:#071421}.pro-footer-bottom{color:#98aebf}.pro-end-cta{background:#1a324b}.pro-end-cta a{background:#d4a56d;color:#132339!important}.pro-404 h1{color:#b8905c} diff --git a/themes/automotive/versions/1.0.0/pro.html b/themes/automotive/versions/1.0.0/pro.html new file mode 100644 index 0000000..43673a0 --- /dev/null +++ b/themes/automotive/versions/1.0.0/pro.html @@ -0,0 +1,10 @@ +{{ seo_title }}{% if canonical and not noindex %}{% endif %}{% if theme_overrides %}{% endif %}跳转至正文 +{% from '_professional_components.html' import card,cover,grid,section_title,filters,footer_links,body_copy with context %} +
GLOBAL MOBILITY / CURATED SUPPLYDRIVEN BY WHAT'S NEXT ✦
+
取得联系 ↗
{% if brands or categories %}{% endif %}
+{% if not designer_replace %}{% include 'automotive/pages/' ~ kind ~ '.html' %}{% endif %} + +{% if has_custom and kind!='notfound' %}
{{custom_blocks}}
{% else %}
{% endif %} +{% if kind not in ('notfound','search') %}
LET'S CONNECT

从这里开始下一步。

{{site.tagline or '了解更多产品、服务与合作信息。'}}

联系我们 ↗
{% endif %} +
+ \ No newline at end of file diff --git a/themes/automotive/versions/1.0.0/site.html b/themes/automotive/versions/1.0.0/site.html new file mode 100644 index 0000000..a3ceef5 --- /dev/null +++ b/themes/automotive/versions/1.0.0/site.html @@ -0,0 +1,13 @@ +{{seo_title}}{% if canonical %}{% endif %} +
GLOBAL SOURCING · GLOBAL DELIVERYSiteForge Automotive Collection
+
S.{{site.name}}AUTO & MOBILITYGET IN TOUCH ↗
+{% if blueprint_mode %}{% elif mode=='home' %}
GLOBAL AUTOMOTIVE NETWORK

{{page.title if page.title!=site.name else 'MOVE BEYOND'}}

{{page.summary or site.tagline or 'Discover global mobility and automotive supply solutions.'}}

EXPLORE OUR FLEET ↗Built for the road ahead.
{% if page.cover %}{{page.title}}{% else %}
{% endif %}
01 / EXPLORE
DISCOVER MOREQUALITY · TRUST · WORLDWIDE↓
+{% elif mode=='listing' %}
THE COLLECTION

{{page.title}}

{{page.summary}}

+{% elif mode=='notfound' %}

404

We couldn't find that page.

+{% else %}
{{type_names.get(page.type,'PAGE')|upper}} / THE DETAILS

{{page.title}}

{{page.summary}}

{% endif %} +{% if blueprint_mode %}
{{blocks}}
{% else %}
{% if mode in ['detail','home'] and (content or blocks) %}
{{blocks}}{{content}}
{% endif %} +{% if mode=='detail' and page.cover %}
{{page.title}}
{% endif %} +{% if mode=='listing' %}
THE SELECTION

Explore the collection

{% endif %} +{% if mode=='home' %}{% set showcase=entries|selectattr('type','equalto','product')|list %}{% if showcase %}
CURATED MOBILITY

Featured Vehicles

VIEW COLLECTION ↗
{% endif %}{% endif %} +
+{% endif %}

{{site.tagline}}

diff --git a/themes/automotive/versions/1.0.0/theme.css b/themes/automotive/versions/1.0.0/theme.css new file mode 100644 index 0000000..2786b24 --- /dev/null +++ b/themes/automotive/versions/1.0.0/theme.css @@ -0,0 +1 @@ +:root{--accent:#ecb75c;--ink:#f6f7fa;--body:#101318}*{box-sizing:border-box}body{margin:0;background:#f7f7f7;color:#1b232c;font:16px/1.65 Inter,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}a{text-decoration:none;color:inherit}img{max-width:100%;object-fit:cover}.utility{height:32px;display:flex;justify-content:space-between;align-items:center;background:#090d13;color:#919da8;padding:0 5%;font-size:10px;letter-spacing:2px}.header{height:90px;background:#121922;color:#fff;display:flex;align-items:center;justify-content:space-between;padding:0 5%;gap:24px}.brand{font-size:22px;font-weight:780;display:flex;align-items:center;gap:14px;line-height:1.15}.brand small{display:block;font-size:9px;color:#9ba4b4;letter-spacing:2.5px;margin-top:5px}.mark{background:var(--accent);border-radius:3px;color:#111;padding:10px;font-size:19px}.header nav{display:flex;flex-wrap:wrap;gap:26px;font-size:12px;letter-spacing:1px}.header nav a:hover{color:var(--accent)}.nav-cta{border:1px solid #505a62;padding:12px 17px;color:#e6c893;font-size:11px;letter-spacing:1px}.hero{background:radial-gradient(circle at 80% 55%,#2d4359 0%,#141e2c 34%,#0b121b 75%);color:white;min-height:580px}.hero-inner{max-width:1450px;margin:auto;padding:70px 5% 50px;display:grid;grid-template-columns:51% 49%;align-items:center;min-height:520px}.eyebrow{font-size:11px;letter-spacing:3px;font-weight:700;color:#e9b962}.glow{display:inline-block;width:8px;height:8px;background:#efbf6e;border-radius:100%;margin-right:12px;box-shadow:0 0 16px #edc36d}.hero h1{font-size:clamp(54px,7.2vw,116px);font-weight:850;letter-spacing:-6px;line-height:.95;margin:29px 0}.hero p{font-size:17px;color:#b6c4d1;max-width:470px}.hero-actions{display:flex;align-items:center;gap:30px;margin-top:34px}.btn{display:inline-flex;align-items:center;justify-content:space-between;gap:32px;padding:17px 23px;background:#d5a962;color:#12181d;font-size:12px;font-weight:850;letter-spacing:.8px}.hero-hint{color:#89929b;font-size:12px}.hero-visual{height:330px;position:relative;display:flex;align-items:center;justify-content:center}.hero-visual img{max-height:360px;width:100%;object-fit:contain;filter:drop-shadow(0 16px 23px #0008)}.hero-no{position:absolute;right:0;bottom:-70px;color:#fff6;font-size:10px;letter-spacing:3px}.car-shape{height:220px;width:100%;position:relative;filter:drop-shadow(0 28px 16px #0007)}.car-body{position:absolute;inset:45px 0 24px;border-radius:90px 130px 36px 35px;background:linear-gradient(170deg,#c7d0d9,#60788a 37%,#142433 48%,#202d3d 70%);clip-path:polygon(0 70%,13% 47%,31% 38%,47% 6%,75% 6%,88% 39%,100% 56%,98% 95%,3% 95%)}.wheel{position:absolute;width:80px;height:80px;bottom:0;border-radius:100%;background:radial-gradient(circle,#71808b 0 25%,#101722 28% 50%,#354355 54% 61%,#0a0d13 64%)}.wheel-one{left:19%}.wheel-two{right:13%}.hero-bottom{border-top:1px solid #344052;padding:18px 5%;display:flex;justify-content:space-between;color:#9fa9b5;font-size:10px;letter-spacing:2px}.inner-hero{background:linear-gradient(110deg,#0d1925,#2a3f50);color:white;padding:100px 6% 90px}.inner-hero h1{font-size:clamp(40px,6vw,84px);line-height:1.1;letter-spacing:-2px;margin:16px 0}.inner-hero p{color:#c0ccd7}.container{max-width:1220px;margin:auto;padding:0 24px}.main{padding:75px 0 110px}.section-title{display:flex;align-items:end;gap:20px;justify-content:space-between;margin:30px 0 28px}.section-title span{font-size:11px;letter-spacing:2px;color:#9f7844}.section-title h2{font-size:37px;letter-spacing:-1.5px;flex:1;margin:0}.section-title a{font-size:12px;font-weight:700}.card-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:24px}.model-card{background:#fff;transition:transform .2s,box-shadow .2s;min-width:0}.model-card:hover{transform:translateY(-4px);box-shadow:0 14px 35px #15273822}.card-visual{height:210px;background:linear-gradient(120deg,#dae2e8,#abbcc7);display:flex;align-items:center;justify-content:center;overflow:hidden}.card-visual img{height:100%;width:100%;object-fit:contain}.card-monogram{font-size:75px;font-weight:900;letter-spacing:-8px;color:#ffffff77}.card-copy{padding:24px}.card-copy span{color:#ae854c;font-size:10px;letter-spacing:2px}.card-copy h3{font-size:24px;margin:8px 0}.card-copy p{font-size:13px;color:#73808b}.card-copy b{font-size:11px;letter-spacing:1px;color:#a6793f}.detail-media{margin:40px auto;text-align:center;max-height:600px}.detail-media img{max-height:600px}.rich-content{margin-bottom:55px}.prose{max-width:860px}.rich-content p{color:#596574}.section{padding:45px 0}.section-heading h2{font-size:38px}.block-hero{padding:60px 0}.block-hero h1{font-size:52px}.feature-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:20px}.feature{padding:26px;background:white}.feature-mark{color:#b08756;font-size:12px}.stats{display:grid;grid-template-columns:repeat(4,1fr);gap:20px}.stat strong{display:block;font-size:35px}.stat span{color:#71808c}.cta{background:#19283a;color:white;padding:55px}.cta p{color:#b8c8d6}footer{background:#0d151f;color:white;padding:70px 5% 25px}.footer-brand{font-size:25px;font-weight:750}.footer-brand span{font-size:11px;font-weight:400;color:#aeb8c2}footer p{color:#8593a2}.footer-bottom{border-top:1px solid #30404c;padding-top:25px;margin-top:65px;display:flex;justify-content:space-between;font-size:10px;letter-spacing:1px;color:#8995a3}@media(max-width:860px){.utility{font-size:8px}.header{height:auto;padding:18px 5%;align-items:flex-start;flex-wrap:wrap}.header nav{order:3;width:100%;gap:18px}.nav-cta{display:none}.hero-inner{grid-template-columns:1fr;padding-top:55px}.hero h1{letter-spacing:-3px}.hero-visual{height:210px}.hero-no{bottom:0}.card-grid,.feature-grid{grid-template-columns:repeat(2,1fr)}}@media(max-width:560px){.card-grid,.feature-grid,.stats{grid-template-columns:1fr}.hero h1{font-size:56px}.hero-actions{flex-wrap:wrap}.section-title{align-items:start;flex-direction:column}.inner-hero{padding:75px 6%}.hero-bottom{font-size:8px}} diff --git a/themes/automotive/visual/hero.svg b/themes/automotive/visual/hero.svg new file mode 100644 index 0000000..4ba145a --- /dev/null +++ b/themes/automotive/visual/hero.svg @@ -0,0 +1 @@ +CONCEPT / MOBILITY diff --git a/themes/corporate/manifest.json b/themes/corporate/manifest.json new file mode 100644 index 0000000..827c65a --- /dev/null +++ b/themes/corporate/manifest.json @@ -0,0 +1,54 @@ +{ + "id": "corporate", + "name": "企业品牌", + "description": "商业主题体验升级:独立视觉素材、响应式排版、产品列表筛选、多级导航、可访问性与交付检查(需真实内容验收)", + "version": "1.1.0", + "style": "corporate", + "entry": "pro.html", + "styles": [ + "theme.css", + "blocks.css" + ], + "page_templates": [ + "home", + "about", + "contact", + "listing", + "category", + "product", + "article", + "brand", + "case", + "service", + "faq", + "standard", + "notfound" + ], + "blocks": [ + "hero", + "richtext", + "features", + "stats", + "collection", + "image_text", + "gallery", + "faq", + "cta", + "attributes", + "spacer" + ], + "responsive": true, + "auto_binding": true, + "professional_layout": true, + "commercial_ui": true, + "commercial_features": [ + "responsive-design", + "product-detail", + "brand-collections", + "gallery", + "content-facets", + "mega-navigation", + "contact-integration", + "readiness-audit" + ] +} \ No newline at end of file diff --git a/themes/corporate/page-presets.json b/themes/corporate/page-presets.json new file mode 100644 index 0000000..b0ec473 --- /dev/null +++ b/themes/corporate/page-presets.json @@ -0,0 +1,285 @@ +{ + "home": [ + { + "id": "c7aac2a0c9", + "type": "hero", + "visible": true, + "title": "连接资源,创造价值", + "subtitle": "BUILDING WHAT IS NEXT", + "description": "{{site.tagline}}", + "button": "认识我们", + "href": "/about/", + "align": "left" + }, + { + "id": "e26e908d07", + "type": "image_text", + "visible": true, + "title": "值得信赖的全球合作伙伴", + "subtitle": "OUR MISSION", + "description": "展示业务流程和服务理念。", + "image": "", + "image_side": "right", + "button": "了解更多", + "href": "/about/" + }, + { + "id": "df60bf30f1", + "type": "stats", + "visible": true, + "title": "持续发展的组织", + "items": [ + { + "value": "50,000+", + "label": "服务案例" + }, + { + "value": "70+", + "label": "市场覆盖" + }, + { + "value": "30+", + "label": "合作伙伴" + } + ] + }, + { + "id": "13cbd2547e", + "type": "collection", + "visible": true, + "title": "产品与解决方案", + "subtitle": "WHAT WE DO", + "source": "service", + "limit": 6, + "columns": 3, + "button": "查看全部", + "href": "/product/" + }, + { + "id": "452422710f", + "type": "cta", + "visible": true, + "title": "共同探索更多可能", + "subtitle": "告诉我们您的需求,我们将提供适合的方案。", + "button": "联系我们", + "href": "/contact/" + } + ], + "about": [ + { + "id": "bfc7b47ec0", + "type": "image_text", + "visible": true, + "title": "认识我们", + "subtitle": "OUR APPROACH", + "description": "展示业务流程和服务理念。", + "image": "", + "image_side": "right", + "button": "了解更多", + "href": "/about/" + }, + { + "id": "ce5473bd68", + "type": "stats", + "visible": true, + "title": "我们的实力", + "items": [ + { + "value": "50,000+", + "label": "服务案例" + }, + { + "value": "70+", + "label": "市场覆盖" + }, + { + "value": "30+", + "label": "合作伙伴" + } + ] + }, + { + "id": "a617ad837e", + "type": "features", + "visible": true, + "title": "我们的优势", + "items": [ + { + "title": "专业服务", + "text": "从咨询到交付" + }, + { + "title": "品质保障", + "text": "严格的质量流程" + }, + { + "title": "全球网络", + "text": "连接不同市场" + } + ] + } + ], + "contact": [ + { + "id": "136e741f39", + "type": "richtext", + "visible": true, + "title": "联系我们", + "body": "

请填写企业联系方式与地址。

" + }, + { + "id": "999ad7d7e2", + "type": "cta", + "visible": true, + "title": "欢迎联系", + "subtitle": "告诉我们您的需求,我们将提供适合的方案。", + "button": "联系我们", + "href": "/contact/" + } + ], + "listing": [ + { + "id": "5ef3038c4a", + "type": "collection", + "visible": true, + "title": "所有产品", + "subtitle": "为客户提供适合的解决方案", + "source": "product", + "limit": 24, + "columns": 3, + "button": "查看全部", + "href": "/product/" + } + ], + "product": [ + { + "id": "34abe3c838", + "type": "image_text", + "visible": true, + "title": "产品介绍", + "subtitle": "OUR APPROACH", + "description": "{{page.summary}}", + "image": "{{page.cover}}", + "image_side": "right", + "button": "了解更多", + "href": "/about/" + }, + { + "id": "159ea63c47", + "type": "attributes", + "visible": true, + "title": "产品规格与参数" + }, + { + "id": "2b117d9f64", + "type": "richtext", + "visible": true, + "title": "产品详情", + "body": "{{page.body}}" + }, + { + "id": "5fc378724c", + "type": "cta", + "visible": true, + "title": "开启新的合作", + "subtitle": "告诉我们您的需求,我们将提供适合的方案。", + "button": "联系我们", + "href": "/contact/" + } + ], + "article": [ + { + "id": "6f2c9d6c13", + "type": "richtext", + "visible": true, + "title": "{{page.title}}", + "body": "{{page.body}}" + } + ], + "brand": [ + { + "id": "b838559db3", + "type": "image_text", + "visible": true, + "title": "{{page.title}}", + "subtitle": "OUR APPROACH", + "description": "{{page.summary}}", + "image": "{{page.cover}}", + "image_side": "right", + "button": "了解更多", + "href": "/about/" + }, + { + "id": "44408e8e94", + "type": "collection", + "visible": true, + "title": "品牌产品", + "subtitle": "为客户提供适合的解决方案", + "source": "product", + "limit": 6, + "columns": 3, + "button": "查看全部", + "href": "/product/" + } + ], + "standard": [ + { + "id": "a0b2329d83", + "type": "richtext", + "visible": true, + "title": "{{page.title}}", + "body": "{{page.body}}" + } + ], + "notfound": [ + { + "id": "8dda5dc286", + "type": "cta", + "visible": true, + "title": "页面暂时无法访问", + "subtitle": "告诉我们您的需求,我们将提供适合的方案。", + "button": "返回首页", + "href": "/" + } + ], + "case": [ + { + "type": "image_text", + "title": "{{page.title}}", + "image": "{{page.cover}}", + "description": "{{page.summary}}" + }, + { + "type": "richtext", + "title": "项目概述", + "body": "{{page.body}}" + } + ], + "service": [ + { + "type": "richtext", + "title": "{{page.title}}", + "body": "{{page.body}}" + }, + { + "type": "cta", + "title": "了解更多", + "href": "/contact/" + } + ], + "faq": [ + { + "type": "faq", + "title": "常见问题", + "items": [] + } + ], + "category": [ + { + "type": "collection", + "title": "分类产品", + "source": "product", + "limit": 24 + } + ] +} \ No newline at end of file diff --git a/themes/corporate/pages/about.html b/themes/corporate/pages/about.html new file mode 100644 index 0000000..945be44 --- /dev/null +++ b/themes/corporate/pages/about.html @@ -0,0 +1,2 @@ +
{{ 'GET IN TOUCH' if kind=='contact' else ('OUR STORY' if kind=='about' else 'EXPLORE') }}

{{page.title}}

{{page.summary}}

+
{{'CONNECT / CONTACT' if kind=='contact' else 'ABOUT / DISCOVER'}}

{{'让我们开始沟通。' if kind=='contact' else page.title}}

{% if kind=='contact' and footer_email %}{{footer_email}} ↗{% elif kind=='contact' %}

请先在网站管理后台填写正式联系邮箱。

{% endif %}
{{body_copy(content,page)}}{% if page.cover %}{{page.title}}{% endif %}
diff --git a/themes/corporate/pages/article.html b/themes/corporate/pages/article.html new file mode 100644 index 0000000..85626e4 --- /dev/null +++ b/themes/corporate/pages/article.html @@ -0,0 +1 @@ +
LATEST NEWS / INSIGHTS

{{page.title}}

{{page.summary}}

{% if fields.get('publish_date') %}{% endif %}
diff --git a/themes/corporate/pages/brand.html b/themes/corporate/pages/brand.html new file mode 100644 index 0000000..a710280 --- /dev/null +++ b/themes/corporate/pages/brand.html @@ -0,0 +1 @@ +
BRAND PROFILE / PARTNERS

{{page.title}}

{{page.summary}}

{{cover(page,'brand')}}
{{body_copy(content,page)}}{% if related %}{{section_title('PRODUCT LINE','相关产品','/products/')}}{{grid(related,'product')}}{% endif %}
diff --git a/themes/corporate/pages/case.html b/themes/corporate/pages/case.html new file mode 100644 index 0000000..79e4d72 --- /dev/null +++ b/themes/corporate/pages/case.html @@ -0,0 +1 @@ +
SELECTED WORK / PROJECT

{{page.title}}

{{page.summary}}

{% if page.cover %}{{page.title}}{% endif %}
{% if fields.get('location') %}项目地点
{{fields.get('location')}}
{% endif %}{% if fields.get('industry') %}所属行业
{{fields.get('industry')}}
{% endif %}
{{body_copy(content,page)}}浏览更多案例 →
diff --git a/themes/corporate/pages/category.html b/themes/corporate/pages/category.html new file mode 100644 index 0000000..3918172 --- /dev/null +++ b/themes/corporate/pages/category.html @@ -0,0 +1,2 @@ +
DISCOVER / {{'CATEGORY' if kind=='category' else 'COLLECTION'}}

{{page.title}}

{{page.summary or '查看此栏目的全部已发布内容。'}}

首页/{{page.title}}
+
{{ filters() }}
{{grid(listing,'catalog')}}
diff --git a/themes/corporate/pages/contact.html b/themes/corporate/pages/contact.html new file mode 100644 index 0000000..3a52a60 --- /dev/null +++ b/themes/corporate/pages/contact.html @@ -0,0 +1,7 @@ +
{{ 'GET IN TOUCH' if kind=='contact' else ('OUR STORY' if kind=='about' else 'EXPLORE') }}

{{page.title}}

{{page.summary}}

+
{{'CONNECT / CONTACT' if kind=='contact' else 'ABOUT / DISCOVER'}}

{{'让我们开始沟通。' if kind=='contact' else page.title}}

{% if kind=='contact' and footer_email %}{{footer_email}} ↗{% elif kind=='contact' %}

请先在网站管理后台填写正式联系邮箱。

{% endif %}
{{body_copy(content,page)}}{% if page.cover %}{{page.title}}{% endif %}
+ +{% if form_endpoint %} +
CONTACT FORM

提交业务咨询

提交内容将直接发送到网站管理员配置的外部表单服务。

+

表单提交由外部服务处理,请确认其隐私政策及垃圾信息过滤设置。

+{% endif %} diff --git a/themes/corporate/pages/faq.html b/themes/corporate/pages/faq.html new file mode 100644 index 0000000..945be44 --- /dev/null +++ b/themes/corporate/pages/faq.html @@ -0,0 +1,2 @@ +
{{ 'GET IN TOUCH' if kind=='contact' else ('OUR STORY' if kind=='about' else 'EXPLORE') }}

{{page.title}}

{{page.summary}}

+
{{'CONNECT / CONTACT' if kind=='contact' else 'ABOUT / DISCOVER'}}

{{'让我们开始沟通。' if kind=='contact' else page.title}}

{% if kind=='contact' and footer_email %}{{footer_email}} ↗{% elif kind=='contact' %}

请先在网站管理后台填写正式联系邮箱。

{% endif %}
{{body_copy(content,page)}}{% if page.cover %}{{page.title}}{% endif %}
diff --git a/themes/corporate/pages/home.html b/themes/corporate/pages/home.html new file mode 100644 index 0000000..88a9fa3 --- /dev/null +++ b/themes/corporate/pages/home.html @@ -0,0 +1,4 @@ +
STRATEGY · PARTNERSHIP · DELIVERY

{{page.title if page.title not in (site.name,'首页','Home') else site.name}} IDEAS INTO IMPACT.

{{page.summary or site.tagline or '以清晰的战略、专业的执行,创造长期价值。'}}

{% if page.cover %}{{page.title}}{% else %}
BRINGING
IDEAS TO LIFE
{% endif %}
+{% if features %}{% endif %} +
ABOUT THE COMPANY

以专业连接价值,以合作创造可能。

{{site.tagline or '在这里展示企业愿景、使命与长期能力。'}}

了解我们 ↗
+{% if cases %}
{{section_title('SELECTED WORK','代表项目','/cases/')}}{{grid(cases,'case')}}
{% endif %} diff --git a/themes/corporate/pages/listing.html b/themes/corporate/pages/listing.html new file mode 100644 index 0000000..3918172 --- /dev/null +++ b/themes/corporate/pages/listing.html @@ -0,0 +1,2 @@ +
DISCOVER / {{'CATEGORY' if kind=='category' else 'COLLECTION'}}

{{page.title}}

{{page.summary or '查看此栏目的全部已发布内容。'}}

首页/{{page.title}}
+
{{ filters() }}
{{grid(listing,'catalog')}}
diff --git a/themes/corporate/pages/notfound.html b/themes/corporate/pages/notfound.html new file mode 100644 index 0000000..37e3f05 --- /dev/null +++ b/themes/corporate/pages/notfound.html @@ -0,0 +1 @@ +
ERROR / 404

404

抱歉,你访问的页面不存在,或者已被移动。

返回首页 ↗
diff --git a/themes/corporate/pages/product.html b/themes/corporate/pages/product.html new file mode 100644 index 0000000..ba1fe4f --- /dev/null +++ b/themes/corporate/pages/product.html @@ -0,0 +1,3 @@ +
首页/产品中心/{{page.title}}
+
PRODUCT DETAILS / {{fields.get('brand') or 'COLLECTION'}}

{{page.title}}

{{page.summary or '在此查看产品详细资料。'}}

{% if fields.get('brand') %}

品牌 · {{fields.get('brand')}}

{% endif %}咨询该产品 ↗
+
OVERVIEW

产品介绍

{{body_copy(content,page)}}
SPECIFICATIONS

规格参数

{% for row in specs %}
{{row.label}}{{row.value}}
{% else %}

尚未填写详细参数。

{% endfor %}
diff --git a/themes/corporate/pages/search.html b/themes/corporate/pages/search.html new file mode 100644 index 0000000..182a878 --- /dev/null +++ b/themes/corporate/pages/search.html @@ -0,0 +1,2 @@ +
SEARCH / CONTENT

站内搜索

搜索本网站的产品、品牌、新闻和案例。

+
diff --git a/themes/corporate/pages/service.html b/themes/corporate/pages/service.html new file mode 100644 index 0000000..945be44 --- /dev/null +++ b/themes/corporate/pages/service.html @@ -0,0 +1,2 @@ +
{{ 'GET IN TOUCH' if kind=='contact' else ('OUR STORY' if kind=='about' else 'EXPLORE') }}

{{page.title}}

{{page.summary}}

+
{{'CONNECT / CONTACT' if kind=='contact' else 'ABOUT / DISCOVER'}}

{{'让我们开始沟通。' if kind=='contact' else page.title}}

{% if kind=='contact' and footer_email %}{{footer_email}} ↗{% elif kind=='contact' %}

请先在网站管理后台填写正式联系邮箱。

{% endif %}
{{body_copy(content,page)}}{% if page.cover %}{{page.title}}{% endif %}
diff --git a/themes/corporate/pages/standard.html b/themes/corporate/pages/standard.html new file mode 100644 index 0000000..945be44 --- /dev/null +++ b/themes/corporate/pages/standard.html @@ -0,0 +1,2 @@ +
{{ 'GET IN TOUCH' if kind=='contact' else ('OUR STORY' if kind=='about' else 'EXPLORE') }}

{{page.title}}

{{page.summary}}

+
{{'CONNECT / CONTACT' if kind=='contact' else 'ABOUT / DISCOVER'}}

{{'让我们开始沟通。' if kind=='contact' else page.title}}

{% if kind=='contact' and footer_email %}{{footer_email}} ↗{% elif kind=='contact' %}

请先在网站管理后台填写正式联系邮箱。

{% endif %}
{{body_copy(content,page)}}{% if page.cover %}{{page.title}}{% endif %}
diff --git a/themes/corporate/pro.css b/themes/corporate/pro.css new file mode 100644 index 0000000..acb5e27 --- /dev/null +++ b/themes/corporate/pro.css @@ -0,0 +1,7 @@ +:root{--pro-text:#292d50;--pro-muted:#737793;--pro-border:#e1e2ed;--pro-background:#fff;--pro-surface:#f3f3f9;--pro-accent:#6054c5;--pro-hero:#272b61;--pro-radius:18px;--pro-headings:system-ui,Arial,sans-serif;--pro-wide:1250px} +.pro-utility{background:#343771;color:#f6f6ff}.pro-header{background:#fff}.pro-logo-symbol{color:#655dc0;font-size:35px}.pro-logo-name{letter-spacing:-.07em}.pro-navigation a{font-weight:600}.pro-header-cta{background:#4e4aa4;border-radius:999px}.pro-corporate-hero{background:linear-gradient(135deg,#fafafe 0%,#f0f0fc 55%,#e9e8fa 100%)}.pro-corporate-hero .pro-home-grid{min-height:630px}.pro-corporate-hero .pro-hero-copy h1{font-weight:810}.pro-corporate-hero .pro-hero-copy h1 em{color:#6763c3}.pro-corporate-hero .pro-hero-art{border-radius:25px 160px 25px 160px;background:#dad9f7;box-shadow:25px 35px 0 #c3bde655}.pro-corporate-shape{position:absolute;inset:0;display:grid;place-items:center;background:radial-gradient(circle at 70% 40%,#8079dc 0%,#4d4aab 55%,#333576 100%)}.pro-corporate-shape:before{content:'';position:absolute;width:62%;aspect-ratio:1;border:65px solid #a19dfb77;border-radius:50%;left:-15%;top:-27%}.pro-corporate-shape:after{content:'';position:absolute;width:53%;aspect-ratio:1;border:50px solid #9994e955;border-radius:50%;right:-16%;bottom:-33%}.pro-corporate-shape span{z-index:1;color:#fff;font-size:clamp(26px,3vw,42px);font-weight:850;text-align:center;line-height:1.15;letter-spacing:-.06em}.pro-featured{background:#fff}.pro-featured .pro-card{border-color:#e6e5f0;box-shadow:0 18px 35px #30327a0e}.pro-card-cover{background:#e9eaf7}.pro-art-service{background:linear-gradient(120deg,#d5d2f8,#e9e8fa)}.pro-art-center{color:#716eb69c}.pro-statement{background:#f3f2fa;color:#2a2d50}.pro-statement h2{max-width:820px}.pro-statement .pro-text-link{color:#5e56bc}.pro-case-section{background:#f9f9fc}.pro-inner-hero{background:linear-gradient(140deg,#f3f3fc,#dfdff5)}.pro-inner-hero:after{content:'';position:absolute;right:-5%;bottom:-55%;width:400px;aspect-ratio:1;border:80px solid #8780e41a;border-radius:50%}.pro-article-hero{background:#eeedfa}.pro-footer{background:#242750}.pro-end-cta{background:#34366e}.pro-end-cta a{background:#fff;color:#383776!important}.pro-404 h1{color:#8a84d9} + +/* SiteForge V5 commercial signature */ +.pro-corporate-hero .pro-hero-art{background-image:url("./visual/hero.svg")!important;background-position:center;background-size:cover;} + +.pro-corporate-hero .pro-hero-art{background-color:#f5f4ff;border-radius:38px 120px 40px 120px;overflow:hidden;box-shadow:30px 35px 70px #565baa21}.pro-corporate-hero .pro-hero-art .pro-corporate-shape{opacity:0}.pro-card{border:1px solid #e5e2f2;border-radius:20px;box-shadow:0 10px 40px #3d3b8110}.pro-card-cover{border-radius:20px 20px 0 0;background:#edeafa}.pro-art{background:radial-gradient(circle at 75% 32%,#dad5f7,#eceafa 74%);color:#7772c1}.pro-featured{background:#fff}.pro-statement{background:linear-gradient(120deg,#f2f1fc,#e9e7fb)}.pro-brand-section{background:#f6f5fb}.pro-footer{background:#232658;color:#fff}.pro-header-cta{border-radius:100px}.pro-section-head h2{font-weight:760} diff --git a/themes/corporate/pro.html b/themes/corporate/pro.html new file mode 100644 index 0000000..c14c57d --- /dev/null +++ b/themes/corporate/pro.html @@ -0,0 +1,10 @@ +{{ seo_title }}{% if canonical %}{% endif %}{% if canonical and not noindex %}{% endif %}{% if theme_overrides %}{% endif %}跳转至正文 +{% from '_professional_components.html' import card,cover,grid,section_title,filters,footer_links,body_copy with context %} +
IDEAS INTO IMPACTBUSINESS / PEOPLE / PROGRESS
+
开始合作 ↗
{% if features %}{% endif %}
+{% if not designer_replace %}{% include 'corporate/pages/' ~ kind ~ '.html' %}{% endif %} + +{% if has_custom and kind!='notfound' %}
{{custom_blocks}}
{% else %}
{% endif %} +{% if kind not in ('notfound','search') %}
LET'S CONNECT

从这里开始下一步。

{{site.tagline or '了解更多产品、服务与合作信息。'}}

联系我们 ↗
{% endif %} +
+ \ No newline at end of file diff --git a/themes/corporate/site.html b/themes/corporate/site.html new file mode 100644 index 0000000..0c30b60 --- /dev/null +++ b/themes/corporate/site.html @@ -0,0 +1,12 @@ +{{seo_title}}{% if canonical %}{% endif %} +
A PARTNER FOR WHAT'S NEXT → {{site.tagline or 'Think big. Go further.'}}
+
Let's Talk ↗
+{% if blueprint_mode %}{% elif mode=='home' %}
✦ WELCOME TO {{site.name|upper}}

{{page.title if page.title!=site.name else 'Ideas become impact.'}}

{{page.summary or site.tagline or 'A forward-looking partner creating meaningful opportunities for tomorrow.'}}

STRATEGY • INNOVATION • CONNECTIONS
{% if page.cover %}{{page.title}}{% else %}
THE FUTURE
IS BUILT
TOGETHER.
{% endif %}
+{% else %}
OUR {{type_names.get(page.type,'WORLD')|upper}}

{{page.title}}

{{page.summary}}

{% endif %} +{% if blueprint_mode %}
{{blocks}}
{% else %}
✦ PURPOSE-LED. OUTCOME-FOCUSED.
+{% if mode in ['detail','home'] and (content or blocks) %}
{{blocks}}{{content}}
{% endif %} +{% if mode=='detail' and page.cover %}
{{page.title}}
{% endif %} +{% set chosen=listing_items if mode=='listing' else entries|selectattr('type','equalto','product')|list if mode=='home' else [] %} +{% if chosen %}
DISCOVER MORE

{{'What we offer' if mode=='home' else page.title}}

Distinct solutions. Shared ambition.
{% endif %} +{% if mode=='home' %}
READY TO CONNECT?

Let's build something
meaningful together.

Get in touch ↗
{% endif %}
+{% endif %}
diff --git a/themes/corporate/theme.css b/themes/corporate/theme.css new file mode 100644 index 0000000..a081c46 --- /dev/null +++ b/themes/corporate/theme.css @@ -0,0 +1 @@ +:root{--ink:#152040;--blue:#283d8c;--purple:#6558b9;--light:#f6f7fb}*{box-sizing:border-box}body{margin:0;background:white;color:#172347;font-family:Inter,"Segoe UI",Arial,sans-serif;line-height:1.65}a{color:inherit;text-decoration:none}img{max-width:100%}.shell{width:min(1200px,89%);margin:auto}.announcement{height:35px;background:#262d70;color:white;display:flex;justify-content:center;align-items:center;gap:12px;font-size:10px;letter-spacing:1.3px}.announcement span{color:#b8d6fd}.header{height:94px;display:flex;align-items:center;justify-content:space-between;gap:20px}.logo{font-size:23px;font-weight:850;letter-spacing:-1px;display:flex;align-items:center;gap:12px}.logo-box{display:grid;place-items:center;width:43px;height:43px;color:#fff;font-size:25px;background:linear-gradient(135deg,#5b6ce0,#7c4dc3);border-radius:13px}.header nav{display:flex;gap:28px;font-size:13px;font-weight:600;flex-wrap:wrap}.header nav a:hover{color:#585cca}.nav-btn{background:#243475;color:#fff;padding:13px 20px;border-radius:8px;font-size:12px;font-weight:700}.nav-btn span{margin-left:18px}.hero{background:linear-gradient(120deg,#f5f7ff,#ebeeff 45%,#f8f6fc);overflow:hidden}.hero-grid{min-height:580px;display:grid;grid-template-columns:53% 47%;align-items:center}.hero-text{padding:70px 0}.pill{font-size:10px;letter-spacing:1.5px;color:#5549ac;background:#e5e5fa;border-radius:50px;display:inline-block;padding:8px 15px;font-weight:750}.hero h1{font-size:clamp(52px,6.2vw,87px);line-height:1.06;letter-spacing:-4px;margin:32px 0 20px;max-width:620px;color:#19265e}.hero p{font-size:18px;line-height:1.8;color:#69718f;max-width:520px}.actions{display:flex;gap:14px;flex-wrap:wrap;margin-top:34px}.primary,.secondary{padding:14px 24px;border-radius:8px;font-size:13px;font-weight:750}.primary{background:#38489e;color:#fff}.secondary{background:white;border:1px solid #d2d7ed}.trusted{font-size:10px;color:#9b9db6;letter-spacing:2px;margin-top:77px}.trusted span{padding:0 9px}.abstract-art{position:relative;height:100%;min-height:440px;display:grid;place-items:center}.abstract-art img{max-height:510px;object-fit:contain;filter:drop-shadow(0 25px 30px #33376620)}.orb{position:absolute;border-radius:50%;filter:drop-shadow(18px 15px 25px #5b69b125)}.orb-main{width:350px;height:350px;background:radial-gradient(circle at 30% 25%,#d9ddff,#8493e4 40%,#3d539e 70%,#1c3079);transform:rotate(-20deg)}.orb-inner{width:250px;height:250px;background:radial-gradient(circle at 30% 35%,#fff9,#a1aff366 38%,#5c68aa9c 65%);border:1px solid #ffffffc4;transform:translate(80px,60px)}.orb-small{width:93px;height:93px;background:linear-gradient(125deg,#c59fff,#6851a6);transform:translate(-155px,-155px)}.art-caption{position:relative;z-index:3;color:#fff;font-size:15px;line-height:1.4;letter-spacing:3px;text-align:center;font-weight:800;transform:translate(5px,20px)}.inner-hero{padding:85px 0;background:#f0f2ff}.inner-hero h1{font-size:clamp(40px,6vw,70px);letter-spacing:-2px;line-height:1.1;margin:20px 0}.inner-hero p{color:#69748c}.content{padding:60px 0 100px}.nudge{color:#6b6c95;font-size:10px;letter-spacing:2px;font-weight:750}.nudge span{color:#7b58d0;margin-right:7px}.section-header{margin:62px 0 25px;display:flex;justify-content:space-between;align-items:end}.section-header h2{font-size:40px;letter-spacing:-1.5px;margin:7px 0}.section-header>span{font-size:12px;color:#858eaa}.kicker{font-size:11px;color:#685dba;letter-spacing:2px}.tiles{display:grid;grid-template-columns:repeat(3,1fr);gap:22px}.tile{border:1px solid #eceef8;border-radius:15px;overflow:hidden;transition:box-shadow .2s,transform .2s}.tile:hover{box-shadow:0 18px 32px #323c7c18;transform:translateY(-4px)}.tile-image{height:195px;background:linear-gradient(130deg,#cbd3f5,#929de0);display:grid;place-items:center}.tile-image img{width:100%;height:100%;object-fit:contain}.tile-image>span{font-size:85px;color:#ffffffa8}.tile-info{padding:25px}.tile-info>span{font-size:10px;color:#7d6eba;letter-spacing:1.5px}.tile-info h3{font-size:23px;margin:9px 0}.tile-info p{font-size:13px;color:#7c859b;min-height:40px}.tile-info b{font-size:12px;color:#4e51a3}.rich-content{margin:30px 0}.section{padding:40px 0}.feature-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:15px}.feature{padding:27px;background:#f5f6fd;border-radius:10px}.feature-mark{color:#6e68c7}.stat strong{display:block;font-size:38px}.stats{display:grid;grid-template-columns:repeat(4,1fr);gap:20px}.cta{padding:50px;background:#252d73;color:white;border-radius:18px}.cta p{color:#c2c6ec}.big-cta{background:#293788;color:white;border-radius:20px;padding:60px;margin-top:90px;position:relative;overflow:hidden}.big-cta:after{content:'✦';position:absolute;right:-15px;top:-110px;font-size:410px;opacity:.10}.big-cta span{font-size:10px;letter-spacing:2px}.big-cta h2{font-size:45px;line-height:1.2;letter-spacing:-2px}.big-cta a{display:inline-block;background:white;color:#313e9a;padding:14px 20px;border-radius:9px;font-size:12px;font-weight:800}.detail-image{text-align:center;margin:30px 0}.detail-image img{max-height:520px}footer{background:#151c3c;color:white;padding:70px 0 30px}.footer-upper{display:flex;align-items:center;justify-content:space-between}.footer-upper h2{font-size:30px}.footer-upper p{color:#a6acce}.footer-line{border-top:1px solid #424866;padding-top:28px;margin-top:60px;display:flex;justify-content:space-between;color:#a7aed4;font-size:10px;letter-spacing:1.4px}@media(max-width:900px){.header{height:auto;padding:20px 0;flex-wrap:wrap}.header nav{order:3;width:100%}.hero-grid{grid-template-columns:1fr}.abstract-art{min-height:340px}.trusted{margin-top:40px}.tiles{grid-template-columns:repeat(2,1fr)}}@media(max-width:560px){.tiles,.feature-grid,.stats{grid-template-columns:1fr}.hero h1{font-size:54px}.footer-upper,.footer-line,.section-header{align-items:start;flex-direction:column}.announcement{padding:0 8px;font-size:8px}.big-cta h2{font-size:34px}} diff --git a/themes/corporate/versions/0.3.0/manifest.json b/themes/corporate/versions/0.3.0/manifest.json new file mode 100644 index 0000000..955bd8f --- /dev/null +++ b/themes/corporate/versions/0.3.0/manifest.json @@ -0,0 +1,38 @@ +{ + "id": "corporate", + "name": "企业品牌", + "description": "明亮的蓝紫色,集团介绍与服务展示", + "version": "0.3.0", + "style": "corporate", + "entry": "site.html", + "styles": [ + "theme.css", + "blocks.css" + ], + "page_templates": [ + "home", + "about", + "contact", + "listing", + "product", + "article", + "brand", + "standard", + "notfound" + ], + "blocks": [ + "hero", + "richtext", + "features", + "stats", + "collection", + "image_text", + "gallery", + "faq", + "cta", + "attributes", + "spacer" + ], + "responsive": true, + "auto_binding": true +} \ No newline at end of file diff --git a/themes/corporate/versions/0.3.0/page-presets.json b/themes/corporate/versions/0.3.0/page-presets.json new file mode 100644 index 0000000..e45da9d --- /dev/null +++ b/themes/corporate/versions/0.3.0/page-presets.json @@ -0,0 +1,245 @@ +{ + "home": [ + { + "id": "c7aac2a0c9", + "type": "hero", + "visible": true, + "title": "连接资源,创造价值", + "subtitle": "BUILDING WHAT IS NEXT", + "description": "{{site.tagline}}", + "button": "认识我们", + "href": "/about/", + "align": "left" + }, + { + "id": "e26e908d07", + "type": "image_text", + "visible": true, + "title": "值得信赖的全球合作伙伴", + "subtitle": "OUR MISSION", + "description": "展示业务流程和服务理念。", + "image": "", + "image_side": "right", + "button": "了解更多", + "href": "/about/" + }, + { + "id": "df60bf30f1", + "type": "stats", + "visible": true, + "title": "持续发展的组织", + "items": [ + { + "value": "50,000+", + "label": "服务案例" + }, + { + "value": "70+", + "label": "市场覆盖" + }, + { + "value": "30+", + "label": "合作伙伴" + } + ] + }, + { + "id": "13cbd2547e", + "type": "collection", + "visible": true, + "title": "产品与解决方案", + "subtitle": "WHAT WE DO", + "source": "service", + "limit": 6, + "columns": 3, + "button": "查看全部", + "href": "/product/" + }, + { + "id": "452422710f", + "type": "cta", + "visible": true, + "title": "共同探索更多可能", + "subtitle": "告诉我们您的需求,我们将提供适合的方案。", + "button": "联系我们", + "href": "/contact/" + } + ], + "about": [ + { + "id": "bfc7b47ec0", + "type": "image_text", + "visible": true, + "title": "认识我们", + "subtitle": "OUR APPROACH", + "description": "展示业务流程和服务理念。", + "image": "", + "image_side": "right", + "button": "了解更多", + "href": "/about/" + }, + { + "id": "ce5473bd68", + "type": "stats", + "visible": true, + "title": "我们的实力", + "items": [ + { + "value": "50,000+", + "label": "服务案例" + }, + { + "value": "70+", + "label": "市场覆盖" + }, + { + "value": "30+", + "label": "合作伙伴" + } + ] + }, + { + "id": "a617ad837e", + "type": "features", + "visible": true, + "title": "我们的优势", + "items": [ + { + "title": "专业服务", + "text": "从咨询到交付" + }, + { + "title": "品质保障", + "text": "严格的质量流程" + }, + { + "title": "全球网络", + "text": "连接不同市场" + } + ] + } + ], + "contact": [ + { + "id": "136e741f39", + "type": "richtext", + "visible": true, + "title": "联系我们", + "body": "

请填写企业联系方式与地址。

" + }, + { + "id": "999ad7d7e2", + "type": "cta", + "visible": true, + "title": "欢迎联系", + "subtitle": "告诉我们您的需求,我们将提供适合的方案。", + "button": "联系我们", + "href": "/contact/" + } + ], + "listing": [ + { + "id": "5ef3038c4a", + "type": "collection", + "visible": true, + "title": "所有产品", + "subtitle": "为客户提供适合的解决方案", + "source": "product", + "limit": 24, + "columns": 3, + "button": "查看全部", + "href": "/product/" + } + ], + "product": [ + { + "id": "34abe3c838", + "type": "image_text", + "visible": true, + "title": "产品介绍", + "subtitle": "OUR APPROACH", + "description": "{{page.summary}}", + "image": "{{page.cover}}", + "image_side": "right", + "button": "了解更多", + "href": "/about/" + }, + { + "id": "159ea63c47", + "type": "attributes", + "visible": true, + "title": "产品规格与参数" + }, + { + "id": "2b117d9f64", + "type": "richtext", + "visible": true, + "title": "产品详情", + "body": "{{page.body}}" + }, + { + "id": "5fc378724c", + "type": "cta", + "visible": true, + "title": "开启新的合作", + "subtitle": "告诉我们您的需求,我们将提供适合的方案。", + "button": "联系我们", + "href": "/contact/" + } + ], + "article": [ + { + "id": "6f2c9d6c13", + "type": "richtext", + "visible": true, + "title": "{{page.title}}", + "body": "{{page.body}}" + } + ], + "brand": [ + { + "id": "b838559db3", + "type": "image_text", + "visible": true, + "title": "{{page.title}}", + "subtitle": "OUR APPROACH", + "description": "{{page.summary}}", + "image": "{{page.cover}}", + "image_side": "right", + "button": "了解更多", + "href": "/about/" + }, + { + "id": "44408e8e94", + "type": "collection", + "visible": true, + "title": "品牌产品", + "subtitle": "为客户提供适合的解决方案", + "source": "product", + "limit": 6, + "columns": 3, + "button": "查看全部", + "href": "/product/" + } + ], + "standard": [ + { + "id": "a0b2329d83", + "type": "richtext", + "visible": true, + "title": "{{page.title}}", + "body": "{{page.body}}" + } + ], + "notfound": [ + { + "id": "8dda5dc286", + "type": "cta", + "visible": true, + "title": "页面暂时无法访问", + "subtitle": "告诉我们您的需求,我们将提供适合的方案。", + "button": "返回首页", + "href": "/" + } + ] +} \ No newline at end of file diff --git a/themes/corporate/versions/0.3.0/site.html b/themes/corporate/versions/0.3.0/site.html new file mode 100644 index 0000000..0c30b60 --- /dev/null +++ b/themes/corporate/versions/0.3.0/site.html @@ -0,0 +1,12 @@ +{{seo_title}}{% if canonical %}{% endif %} +
A PARTNER FOR WHAT'S NEXT → {{site.tagline or 'Think big. Go further.'}}
+
Let's Talk ↗
+{% if blueprint_mode %}{% elif mode=='home' %}
✦ WELCOME TO {{site.name|upper}}

{{page.title if page.title!=site.name else 'Ideas become impact.'}}

{{page.summary or site.tagline or 'A forward-looking partner creating meaningful opportunities for tomorrow.'}}

STRATEGY • INNOVATION • CONNECTIONS
{% if page.cover %}{{page.title}}{% else %}
THE FUTURE
IS BUILT
TOGETHER.
{% endif %}
+{% else %}
OUR {{type_names.get(page.type,'WORLD')|upper}}

{{page.title}}

{{page.summary}}

{% endif %} +{% if blueprint_mode %}
{{blocks}}
{% else %}
✦ PURPOSE-LED. OUTCOME-FOCUSED.
+{% if mode in ['detail','home'] and (content or blocks) %}
{{blocks}}{{content}}
{% endif %} +{% if mode=='detail' and page.cover %}
{{page.title}}
{% endif %} +{% set chosen=listing_items if mode=='listing' else entries|selectattr('type','equalto','product')|list if mode=='home' else [] %} +{% if chosen %}
DISCOVER MORE

{{'What we offer' if mode=='home' else page.title}}

Distinct solutions. Shared ambition.
{% endif %} +{% if mode=='home' %}
READY TO CONNECT?

Let's build something
meaningful together.

Get in touch ↗
{% endif %}
+{% endif %}
diff --git a/themes/corporate/versions/0.3.0/theme.css b/themes/corporate/versions/0.3.0/theme.css new file mode 100644 index 0000000..a081c46 --- /dev/null +++ b/themes/corporate/versions/0.3.0/theme.css @@ -0,0 +1 @@ +:root{--ink:#152040;--blue:#283d8c;--purple:#6558b9;--light:#f6f7fb}*{box-sizing:border-box}body{margin:0;background:white;color:#172347;font-family:Inter,"Segoe UI",Arial,sans-serif;line-height:1.65}a{color:inherit;text-decoration:none}img{max-width:100%}.shell{width:min(1200px,89%);margin:auto}.announcement{height:35px;background:#262d70;color:white;display:flex;justify-content:center;align-items:center;gap:12px;font-size:10px;letter-spacing:1.3px}.announcement span{color:#b8d6fd}.header{height:94px;display:flex;align-items:center;justify-content:space-between;gap:20px}.logo{font-size:23px;font-weight:850;letter-spacing:-1px;display:flex;align-items:center;gap:12px}.logo-box{display:grid;place-items:center;width:43px;height:43px;color:#fff;font-size:25px;background:linear-gradient(135deg,#5b6ce0,#7c4dc3);border-radius:13px}.header nav{display:flex;gap:28px;font-size:13px;font-weight:600;flex-wrap:wrap}.header nav a:hover{color:#585cca}.nav-btn{background:#243475;color:#fff;padding:13px 20px;border-radius:8px;font-size:12px;font-weight:700}.nav-btn span{margin-left:18px}.hero{background:linear-gradient(120deg,#f5f7ff,#ebeeff 45%,#f8f6fc);overflow:hidden}.hero-grid{min-height:580px;display:grid;grid-template-columns:53% 47%;align-items:center}.hero-text{padding:70px 0}.pill{font-size:10px;letter-spacing:1.5px;color:#5549ac;background:#e5e5fa;border-radius:50px;display:inline-block;padding:8px 15px;font-weight:750}.hero h1{font-size:clamp(52px,6.2vw,87px);line-height:1.06;letter-spacing:-4px;margin:32px 0 20px;max-width:620px;color:#19265e}.hero p{font-size:18px;line-height:1.8;color:#69718f;max-width:520px}.actions{display:flex;gap:14px;flex-wrap:wrap;margin-top:34px}.primary,.secondary{padding:14px 24px;border-radius:8px;font-size:13px;font-weight:750}.primary{background:#38489e;color:#fff}.secondary{background:white;border:1px solid #d2d7ed}.trusted{font-size:10px;color:#9b9db6;letter-spacing:2px;margin-top:77px}.trusted span{padding:0 9px}.abstract-art{position:relative;height:100%;min-height:440px;display:grid;place-items:center}.abstract-art img{max-height:510px;object-fit:contain;filter:drop-shadow(0 25px 30px #33376620)}.orb{position:absolute;border-radius:50%;filter:drop-shadow(18px 15px 25px #5b69b125)}.orb-main{width:350px;height:350px;background:radial-gradient(circle at 30% 25%,#d9ddff,#8493e4 40%,#3d539e 70%,#1c3079);transform:rotate(-20deg)}.orb-inner{width:250px;height:250px;background:radial-gradient(circle at 30% 35%,#fff9,#a1aff366 38%,#5c68aa9c 65%);border:1px solid #ffffffc4;transform:translate(80px,60px)}.orb-small{width:93px;height:93px;background:linear-gradient(125deg,#c59fff,#6851a6);transform:translate(-155px,-155px)}.art-caption{position:relative;z-index:3;color:#fff;font-size:15px;line-height:1.4;letter-spacing:3px;text-align:center;font-weight:800;transform:translate(5px,20px)}.inner-hero{padding:85px 0;background:#f0f2ff}.inner-hero h1{font-size:clamp(40px,6vw,70px);letter-spacing:-2px;line-height:1.1;margin:20px 0}.inner-hero p{color:#69748c}.content{padding:60px 0 100px}.nudge{color:#6b6c95;font-size:10px;letter-spacing:2px;font-weight:750}.nudge span{color:#7b58d0;margin-right:7px}.section-header{margin:62px 0 25px;display:flex;justify-content:space-between;align-items:end}.section-header h2{font-size:40px;letter-spacing:-1.5px;margin:7px 0}.section-header>span{font-size:12px;color:#858eaa}.kicker{font-size:11px;color:#685dba;letter-spacing:2px}.tiles{display:grid;grid-template-columns:repeat(3,1fr);gap:22px}.tile{border:1px solid #eceef8;border-radius:15px;overflow:hidden;transition:box-shadow .2s,transform .2s}.tile:hover{box-shadow:0 18px 32px #323c7c18;transform:translateY(-4px)}.tile-image{height:195px;background:linear-gradient(130deg,#cbd3f5,#929de0);display:grid;place-items:center}.tile-image img{width:100%;height:100%;object-fit:contain}.tile-image>span{font-size:85px;color:#ffffffa8}.tile-info{padding:25px}.tile-info>span{font-size:10px;color:#7d6eba;letter-spacing:1.5px}.tile-info h3{font-size:23px;margin:9px 0}.tile-info p{font-size:13px;color:#7c859b;min-height:40px}.tile-info b{font-size:12px;color:#4e51a3}.rich-content{margin:30px 0}.section{padding:40px 0}.feature-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:15px}.feature{padding:27px;background:#f5f6fd;border-radius:10px}.feature-mark{color:#6e68c7}.stat strong{display:block;font-size:38px}.stats{display:grid;grid-template-columns:repeat(4,1fr);gap:20px}.cta{padding:50px;background:#252d73;color:white;border-radius:18px}.cta p{color:#c2c6ec}.big-cta{background:#293788;color:white;border-radius:20px;padding:60px;margin-top:90px;position:relative;overflow:hidden}.big-cta:after{content:'✦';position:absolute;right:-15px;top:-110px;font-size:410px;opacity:.10}.big-cta span{font-size:10px;letter-spacing:2px}.big-cta h2{font-size:45px;line-height:1.2;letter-spacing:-2px}.big-cta a{display:inline-block;background:white;color:#313e9a;padding:14px 20px;border-radius:9px;font-size:12px;font-weight:800}.detail-image{text-align:center;margin:30px 0}.detail-image img{max-height:520px}footer{background:#151c3c;color:white;padding:70px 0 30px}.footer-upper{display:flex;align-items:center;justify-content:space-between}.footer-upper h2{font-size:30px}.footer-upper p{color:#a6acce}.footer-line{border-top:1px solid #424866;padding-top:28px;margin-top:60px;display:flex;justify-content:space-between;color:#a7aed4;font-size:10px;letter-spacing:1.4px}@media(max-width:900px){.header{height:auto;padding:20px 0;flex-wrap:wrap}.header nav{order:3;width:100%}.hero-grid{grid-template-columns:1fr}.abstract-art{min-height:340px}.trusted{margin-top:40px}.tiles{grid-template-columns:repeat(2,1fr)}}@media(max-width:560px){.tiles,.feature-grid,.stats{grid-template-columns:1fr}.hero h1{font-size:54px}.footer-upper,.footer-line,.section-header{align-items:start;flex-direction:column}.announcement{padding:0 8px;font-size:8px}.big-cta h2{font-size:34px}} diff --git a/themes/corporate/versions/1.0.0/manifest.json b/themes/corporate/versions/1.0.0/manifest.json new file mode 100644 index 0000000..62f36ee --- /dev/null +++ b/themes/corporate/versions/1.0.0/manifest.json @@ -0,0 +1,43 @@ +{ + "id": "corporate", + "name": "企业品牌", + "description": "明亮的蓝紫色,集团介绍与服务展示;V3 专业全页模板、分类/详情/导航和搜索", + "version": "1.0.0", + "style": "corporate", + "entry": "pro.html", + "styles": [ + "theme.css", + "blocks.css" + ], + "page_templates": [ + "home", + "about", + "contact", + "listing", + "category", + "product", + "article", + "brand", + "case", + "service", + "faq", + "standard", + "notfound" + ], + "blocks": [ + "hero", + "richtext", + "features", + "stats", + "collection", + "image_text", + "gallery", + "faq", + "cta", + "attributes", + "spacer" + ], + "responsive": true, + "auto_binding": true, + "professional_layout": true +} \ No newline at end of file diff --git a/themes/corporate/versions/1.0.0/page-presets.json b/themes/corporate/versions/1.0.0/page-presets.json new file mode 100644 index 0000000..b0ec473 --- /dev/null +++ b/themes/corporate/versions/1.0.0/page-presets.json @@ -0,0 +1,285 @@ +{ + "home": [ + { + "id": "c7aac2a0c9", + "type": "hero", + "visible": true, + "title": "连接资源,创造价值", + "subtitle": "BUILDING WHAT IS NEXT", + "description": "{{site.tagline}}", + "button": "认识我们", + "href": "/about/", + "align": "left" + }, + { + "id": "e26e908d07", + "type": "image_text", + "visible": true, + "title": "值得信赖的全球合作伙伴", + "subtitle": "OUR MISSION", + "description": "展示业务流程和服务理念。", + "image": "", + "image_side": "right", + "button": "了解更多", + "href": "/about/" + }, + { + "id": "df60bf30f1", + "type": "stats", + "visible": true, + "title": "持续发展的组织", + "items": [ + { + "value": "50,000+", + "label": "服务案例" + }, + { + "value": "70+", + "label": "市场覆盖" + }, + { + "value": "30+", + "label": "合作伙伴" + } + ] + }, + { + "id": "13cbd2547e", + "type": "collection", + "visible": true, + "title": "产品与解决方案", + "subtitle": "WHAT WE DO", + "source": "service", + "limit": 6, + "columns": 3, + "button": "查看全部", + "href": "/product/" + }, + { + "id": "452422710f", + "type": "cta", + "visible": true, + "title": "共同探索更多可能", + "subtitle": "告诉我们您的需求,我们将提供适合的方案。", + "button": "联系我们", + "href": "/contact/" + } + ], + "about": [ + { + "id": "bfc7b47ec0", + "type": "image_text", + "visible": true, + "title": "认识我们", + "subtitle": "OUR APPROACH", + "description": "展示业务流程和服务理念。", + "image": "", + "image_side": "right", + "button": "了解更多", + "href": "/about/" + }, + { + "id": "ce5473bd68", + "type": "stats", + "visible": true, + "title": "我们的实力", + "items": [ + { + "value": "50,000+", + "label": "服务案例" + }, + { + "value": "70+", + "label": "市场覆盖" + }, + { + "value": "30+", + "label": "合作伙伴" + } + ] + }, + { + "id": "a617ad837e", + "type": "features", + "visible": true, + "title": "我们的优势", + "items": [ + { + "title": "专业服务", + "text": "从咨询到交付" + }, + { + "title": "品质保障", + "text": "严格的质量流程" + }, + { + "title": "全球网络", + "text": "连接不同市场" + } + ] + } + ], + "contact": [ + { + "id": "136e741f39", + "type": "richtext", + "visible": true, + "title": "联系我们", + "body": "

请填写企业联系方式与地址。

" + }, + { + "id": "999ad7d7e2", + "type": "cta", + "visible": true, + "title": "欢迎联系", + "subtitle": "告诉我们您的需求,我们将提供适合的方案。", + "button": "联系我们", + "href": "/contact/" + } + ], + "listing": [ + { + "id": "5ef3038c4a", + "type": "collection", + "visible": true, + "title": "所有产品", + "subtitle": "为客户提供适合的解决方案", + "source": "product", + "limit": 24, + "columns": 3, + "button": "查看全部", + "href": "/product/" + } + ], + "product": [ + { + "id": "34abe3c838", + "type": "image_text", + "visible": true, + "title": "产品介绍", + "subtitle": "OUR APPROACH", + "description": "{{page.summary}}", + "image": "{{page.cover}}", + "image_side": "right", + "button": "了解更多", + "href": "/about/" + }, + { + "id": "159ea63c47", + "type": "attributes", + "visible": true, + "title": "产品规格与参数" + }, + { + "id": "2b117d9f64", + "type": "richtext", + "visible": true, + "title": "产品详情", + "body": "{{page.body}}" + }, + { + "id": "5fc378724c", + "type": "cta", + "visible": true, + "title": "开启新的合作", + "subtitle": "告诉我们您的需求,我们将提供适合的方案。", + "button": "联系我们", + "href": "/contact/" + } + ], + "article": [ + { + "id": "6f2c9d6c13", + "type": "richtext", + "visible": true, + "title": "{{page.title}}", + "body": "{{page.body}}" + } + ], + "brand": [ + { + "id": "b838559db3", + "type": "image_text", + "visible": true, + "title": "{{page.title}}", + "subtitle": "OUR APPROACH", + "description": "{{page.summary}}", + "image": "{{page.cover}}", + "image_side": "right", + "button": "了解更多", + "href": "/about/" + }, + { + "id": "44408e8e94", + "type": "collection", + "visible": true, + "title": "品牌产品", + "subtitle": "为客户提供适合的解决方案", + "source": "product", + "limit": 6, + "columns": 3, + "button": "查看全部", + "href": "/product/" + } + ], + "standard": [ + { + "id": "a0b2329d83", + "type": "richtext", + "visible": true, + "title": "{{page.title}}", + "body": "{{page.body}}" + } + ], + "notfound": [ + { + "id": "8dda5dc286", + "type": "cta", + "visible": true, + "title": "页面暂时无法访问", + "subtitle": "告诉我们您的需求,我们将提供适合的方案。", + "button": "返回首页", + "href": "/" + } + ], + "case": [ + { + "type": "image_text", + "title": "{{page.title}}", + "image": "{{page.cover}}", + "description": "{{page.summary}}" + }, + { + "type": "richtext", + "title": "项目概述", + "body": "{{page.body}}" + } + ], + "service": [ + { + "type": "richtext", + "title": "{{page.title}}", + "body": "{{page.body}}" + }, + { + "type": "cta", + "title": "了解更多", + "href": "/contact/" + } + ], + "faq": [ + { + "type": "faq", + "title": "常见问题", + "items": [] + } + ], + "category": [ + { + "type": "collection", + "title": "分类产品", + "source": "product", + "limit": 24 + } + ] +} \ No newline at end of file diff --git a/themes/corporate/versions/1.0.0/pages/about.html b/themes/corporate/versions/1.0.0/pages/about.html new file mode 100644 index 0000000..945be44 --- /dev/null +++ b/themes/corporate/versions/1.0.0/pages/about.html @@ -0,0 +1,2 @@ +
{{ 'GET IN TOUCH' if kind=='contact' else ('OUR STORY' if kind=='about' else 'EXPLORE') }}

{{page.title}}

{{page.summary}}

+
{{'CONNECT / CONTACT' if kind=='contact' else 'ABOUT / DISCOVER'}}

{{'让我们开始沟通。' if kind=='contact' else page.title}}

{% if kind=='contact' and footer_email %}{{footer_email}} ↗{% elif kind=='contact' %}

请先在网站管理后台填写正式联系邮箱。

{% endif %}
{{body_copy(content,page)}}{% if page.cover %}{{page.title}}{% endif %}
diff --git a/themes/corporate/versions/1.0.0/pages/article.html b/themes/corporate/versions/1.0.0/pages/article.html new file mode 100644 index 0000000..85626e4 --- /dev/null +++ b/themes/corporate/versions/1.0.0/pages/article.html @@ -0,0 +1 @@ +
LATEST NEWS / INSIGHTS

{{page.title}}

{{page.summary}}

{% if fields.get('publish_date') %}{% endif %}
diff --git a/themes/corporate/versions/1.0.0/pages/brand.html b/themes/corporate/versions/1.0.0/pages/brand.html new file mode 100644 index 0000000..a710280 --- /dev/null +++ b/themes/corporate/versions/1.0.0/pages/brand.html @@ -0,0 +1 @@ +
BRAND PROFILE / PARTNERS

{{page.title}}

{{page.summary}}

{{cover(page,'brand')}}
{{body_copy(content,page)}}{% if related %}{{section_title('PRODUCT LINE','相关产品','/products/')}}{{grid(related,'product')}}{% endif %}
diff --git a/themes/corporate/versions/1.0.0/pages/case.html b/themes/corporate/versions/1.0.0/pages/case.html new file mode 100644 index 0000000..79e4d72 --- /dev/null +++ b/themes/corporate/versions/1.0.0/pages/case.html @@ -0,0 +1 @@ +
SELECTED WORK / PROJECT

{{page.title}}

{{page.summary}}

{% if page.cover %}{{page.title}}{% endif %}
{% if fields.get('location') %}项目地点
{{fields.get('location')}}
{% endif %}{% if fields.get('industry') %}所属行业
{{fields.get('industry')}}
{% endif %}
{{body_copy(content,page)}}浏览更多案例 →
diff --git a/themes/corporate/versions/1.0.0/pages/category.html b/themes/corporate/versions/1.0.0/pages/category.html new file mode 100644 index 0000000..3918172 --- /dev/null +++ b/themes/corporate/versions/1.0.0/pages/category.html @@ -0,0 +1,2 @@ +
DISCOVER / {{'CATEGORY' if kind=='category' else 'COLLECTION'}}

{{page.title}}

{{page.summary or '查看此栏目的全部已发布内容。'}}

首页/{{page.title}}
+
{{ filters() }}
{{grid(listing,'catalog')}}
diff --git a/themes/corporate/versions/1.0.0/pages/contact.html b/themes/corporate/versions/1.0.0/pages/contact.html new file mode 100644 index 0000000..3a52a60 --- /dev/null +++ b/themes/corporate/versions/1.0.0/pages/contact.html @@ -0,0 +1,7 @@ +
{{ 'GET IN TOUCH' if kind=='contact' else ('OUR STORY' if kind=='about' else 'EXPLORE') }}

{{page.title}}

{{page.summary}}

+
{{'CONNECT / CONTACT' if kind=='contact' else 'ABOUT / DISCOVER'}}

{{'让我们开始沟通。' if kind=='contact' else page.title}}

{% if kind=='contact' and footer_email %}{{footer_email}} ↗{% elif kind=='contact' %}

请先在网站管理后台填写正式联系邮箱。

{% endif %}
{{body_copy(content,page)}}{% if page.cover %}{{page.title}}{% endif %}
+ +{% if form_endpoint %} +
CONTACT FORM

提交业务咨询

提交内容将直接发送到网站管理员配置的外部表单服务。

+

表单提交由外部服务处理,请确认其隐私政策及垃圾信息过滤设置。

+{% endif %} diff --git a/themes/corporate/versions/1.0.0/pages/faq.html b/themes/corporate/versions/1.0.0/pages/faq.html new file mode 100644 index 0000000..945be44 --- /dev/null +++ b/themes/corporate/versions/1.0.0/pages/faq.html @@ -0,0 +1,2 @@ +
{{ 'GET IN TOUCH' if kind=='contact' else ('OUR STORY' if kind=='about' else 'EXPLORE') }}

{{page.title}}

{{page.summary}}

+
{{'CONNECT / CONTACT' if kind=='contact' else 'ABOUT / DISCOVER'}}

{{'让我们开始沟通。' if kind=='contact' else page.title}}

{% if kind=='contact' and footer_email %}{{footer_email}} ↗{% elif kind=='contact' %}

请先在网站管理后台填写正式联系邮箱。

{% endif %}
{{body_copy(content,page)}}{% if page.cover %}{{page.title}}{% endif %}
diff --git a/themes/corporate/versions/1.0.0/pages/home.html b/themes/corporate/versions/1.0.0/pages/home.html new file mode 100644 index 0000000..88a9fa3 --- /dev/null +++ b/themes/corporate/versions/1.0.0/pages/home.html @@ -0,0 +1,4 @@ +
STRATEGY · PARTNERSHIP · DELIVERY

{{page.title if page.title not in (site.name,'首页','Home') else site.name}} IDEAS INTO IMPACT.

{{page.summary or site.tagline or '以清晰的战略、专业的执行,创造长期价值。'}}

{% if page.cover %}{{page.title}}{% else %}
BRINGING
IDEAS TO LIFE
{% endif %}
+{% if features %}{% endif %} +
ABOUT THE COMPANY

以专业连接价值,以合作创造可能。

{{site.tagline or '在这里展示企业愿景、使命与长期能力。'}}

了解我们 ↗
+{% if cases %}
{{section_title('SELECTED WORK','代表项目','/cases/')}}{{grid(cases,'case')}}
{% endif %} diff --git a/themes/corporate/versions/1.0.0/pages/listing.html b/themes/corporate/versions/1.0.0/pages/listing.html new file mode 100644 index 0000000..3918172 --- /dev/null +++ b/themes/corporate/versions/1.0.0/pages/listing.html @@ -0,0 +1,2 @@ +
DISCOVER / {{'CATEGORY' if kind=='category' else 'COLLECTION'}}

{{page.title}}

{{page.summary or '查看此栏目的全部已发布内容。'}}

首页/{{page.title}}
+
{{ filters() }}
{{grid(listing,'catalog')}}
diff --git a/themes/corporate/versions/1.0.0/pages/notfound.html b/themes/corporate/versions/1.0.0/pages/notfound.html new file mode 100644 index 0000000..37e3f05 --- /dev/null +++ b/themes/corporate/versions/1.0.0/pages/notfound.html @@ -0,0 +1 @@ +
ERROR / 404

404

抱歉,你访问的页面不存在,或者已被移动。

返回首页 ↗
diff --git a/themes/corporate/versions/1.0.0/pages/product.html b/themes/corporate/versions/1.0.0/pages/product.html new file mode 100644 index 0000000..ba1fe4f --- /dev/null +++ b/themes/corporate/versions/1.0.0/pages/product.html @@ -0,0 +1,3 @@ +
首页/产品中心/{{page.title}}
+
PRODUCT DETAILS / {{fields.get('brand') or 'COLLECTION'}}

{{page.title}}

{{page.summary or '在此查看产品详细资料。'}}

{% if fields.get('brand') %}

品牌 · {{fields.get('brand')}}

{% endif %}咨询该产品 ↗
+
OVERVIEW

产品介绍

{{body_copy(content,page)}}
SPECIFICATIONS

规格参数

{% for row in specs %}
{{row.label}}{{row.value}}
{% else %}

尚未填写详细参数。

{% endfor %}
diff --git a/themes/corporate/versions/1.0.0/pages/search.html b/themes/corporate/versions/1.0.0/pages/search.html new file mode 100644 index 0000000..182a878 --- /dev/null +++ b/themes/corporate/versions/1.0.0/pages/search.html @@ -0,0 +1,2 @@ +
SEARCH / CONTENT

站内搜索

搜索本网站的产品、品牌、新闻和案例。

+
diff --git a/themes/corporate/versions/1.0.0/pages/service.html b/themes/corporate/versions/1.0.0/pages/service.html new file mode 100644 index 0000000..945be44 --- /dev/null +++ b/themes/corporate/versions/1.0.0/pages/service.html @@ -0,0 +1,2 @@ +
{{ 'GET IN TOUCH' if kind=='contact' else ('OUR STORY' if kind=='about' else 'EXPLORE') }}

{{page.title}}

{{page.summary}}

+
{{'CONNECT / CONTACT' if kind=='contact' else 'ABOUT / DISCOVER'}}

{{'让我们开始沟通。' if kind=='contact' else page.title}}

{% if kind=='contact' and footer_email %}{{footer_email}} ↗{% elif kind=='contact' %}

请先在网站管理后台填写正式联系邮箱。

{% endif %}
{{body_copy(content,page)}}{% if page.cover %}{{page.title}}{% endif %}
diff --git a/themes/corporate/versions/1.0.0/pages/standard.html b/themes/corporate/versions/1.0.0/pages/standard.html new file mode 100644 index 0000000..945be44 --- /dev/null +++ b/themes/corporate/versions/1.0.0/pages/standard.html @@ -0,0 +1,2 @@ +
{{ 'GET IN TOUCH' if kind=='contact' else ('OUR STORY' if kind=='about' else 'EXPLORE') }}

{{page.title}}

{{page.summary}}

+
{{'CONNECT / CONTACT' if kind=='contact' else 'ABOUT / DISCOVER'}}

{{'让我们开始沟通。' if kind=='contact' else page.title}}

{% if kind=='contact' and footer_email %}{{footer_email}} ↗{% elif kind=='contact' %}

请先在网站管理后台填写正式联系邮箱。

{% endif %}
{{body_copy(content,page)}}{% if page.cover %}{{page.title}}{% endif %}
diff --git a/themes/corporate/versions/1.0.0/pro.css b/themes/corporate/versions/1.0.0/pro.css new file mode 100644 index 0000000..5736d7a --- /dev/null +++ b/themes/corporate/versions/1.0.0/pro.css @@ -0,0 +1,2 @@ +:root{--pro-text:#292d50;--pro-muted:#737793;--pro-border:#e1e2ed;--pro-background:#fff;--pro-surface:#f3f3f9;--pro-accent:#6054c5;--pro-hero:#272b61;--pro-radius:18px;--pro-headings:system-ui,Arial,sans-serif;--pro-wide:1250px} +.pro-utility{background:#343771;color:#f6f6ff}.pro-header{background:#fff}.pro-logo-symbol{color:#655dc0;font-size:35px}.pro-logo-name{letter-spacing:-.07em}.pro-navigation a{font-weight:600}.pro-header-cta{background:#4e4aa4;border-radius:999px}.pro-corporate-hero{background:linear-gradient(135deg,#fafafe 0%,#f0f0fc 55%,#e9e8fa 100%)}.pro-corporate-hero .pro-home-grid{min-height:630px}.pro-corporate-hero .pro-hero-copy h1{font-weight:810}.pro-corporate-hero .pro-hero-copy h1 em{color:#6763c3}.pro-corporate-hero .pro-hero-art{border-radius:25px 160px 25px 160px;background:#dad9f7;box-shadow:25px 35px 0 #c3bde655}.pro-corporate-shape{position:absolute;inset:0;display:grid;place-items:center;background:radial-gradient(circle at 70% 40%,#8079dc 0%,#4d4aab 55%,#333576 100%)}.pro-corporate-shape:before{content:'';position:absolute;width:62%;aspect-ratio:1;border:65px solid #a19dfb77;border-radius:50%;left:-15%;top:-27%}.pro-corporate-shape:after{content:'';position:absolute;width:53%;aspect-ratio:1;border:50px solid #9994e955;border-radius:50%;right:-16%;bottom:-33%}.pro-corporate-shape span{z-index:1;color:#fff;font-size:clamp(26px,3vw,42px);font-weight:850;text-align:center;line-height:1.15;letter-spacing:-.06em}.pro-featured{background:#fff}.pro-featured .pro-card{border-color:#e6e5f0;box-shadow:0 18px 35px #30327a0e}.pro-card-cover{background:#e9eaf7}.pro-art-service{background:linear-gradient(120deg,#d5d2f8,#e9e8fa)}.pro-art-center{color:#716eb69c}.pro-statement{background:#f3f2fa;color:#2a2d50}.pro-statement h2{max-width:820px}.pro-statement .pro-text-link{color:#5e56bc}.pro-case-section{background:#f9f9fc}.pro-inner-hero{background:linear-gradient(140deg,#f3f3fc,#dfdff5)}.pro-inner-hero:after{content:'';position:absolute;right:-5%;bottom:-55%;width:400px;aspect-ratio:1;border:80px solid #8780e41a;border-radius:50%}.pro-article-hero{background:#eeedfa}.pro-footer{background:#242750}.pro-end-cta{background:#34366e}.pro-end-cta a{background:#fff;color:#383776!important}.pro-404 h1{color:#8a84d9} diff --git a/themes/corporate/versions/1.0.0/pro.html b/themes/corporate/versions/1.0.0/pro.html new file mode 100644 index 0000000..a0c878e --- /dev/null +++ b/themes/corporate/versions/1.0.0/pro.html @@ -0,0 +1,10 @@ +{{ seo_title }}{% if canonical and not noindex %}{% endif %}{% if theme_overrides %}{% endif %}跳转至正文 +{% from '_professional_components.html' import card,cover,grid,section_title,filters,footer_links,body_copy with context %} +
IDEAS INTO IMPACTBUSINESS / PEOPLE / PROGRESS
+
开始合作 ↗
{% if features %}{% endif %}
+{% if not designer_replace %}{% include 'corporate/pages/' ~ kind ~ '.html' %}{% endif %} + +{% if has_custom and kind!='notfound' %}
{{custom_blocks}}
{% else %}
{% endif %} +{% if kind not in ('notfound','search') %}
LET'S CONNECT

从这里开始下一步。

{{site.tagline or '了解更多产品、服务与合作信息。'}}

联系我们 ↗
{% endif %} +
+ \ No newline at end of file diff --git a/themes/corporate/versions/1.0.0/site.html b/themes/corporate/versions/1.0.0/site.html new file mode 100644 index 0000000..0c30b60 --- /dev/null +++ b/themes/corporate/versions/1.0.0/site.html @@ -0,0 +1,12 @@ +{{seo_title}}{% if canonical %}{% endif %} +
A PARTNER FOR WHAT'S NEXT → {{site.tagline or 'Think big. Go further.'}}
+
Let's Talk ↗
+{% if blueprint_mode %}{% elif mode=='home' %}
✦ WELCOME TO {{site.name|upper}}

{{page.title if page.title!=site.name else 'Ideas become impact.'}}

{{page.summary or site.tagline or 'A forward-looking partner creating meaningful opportunities for tomorrow.'}}

STRATEGY • INNOVATION • CONNECTIONS
{% if page.cover %}{{page.title}}{% else %}
THE FUTURE
IS BUILT
TOGETHER.
{% endif %}
+{% else %}
OUR {{type_names.get(page.type,'WORLD')|upper}}

{{page.title}}

{{page.summary}}

{% endif %} +{% if blueprint_mode %}
{{blocks}}
{% else %}
✦ PURPOSE-LED. OUTCOME-FOCUSED.
+{% if mode in ['detail','home'] and (content or blocks) %}
{{blocks}}{{content}}
{% endif %} +{% if mode=='detail' and page.cover %}
{{page.title}}
{% endif %} +{% set chosen=listing_items if mode=='listing' else entries|selectattr('type','equalto','product')|list if mode=='home' else [] %} +{% if chosen %}
DISCOVER MORE

{{'What we offer' if mode=='home' else page.title}}

Distinct solutions. Shared ambition.
{% endif %} +{% if mode=='home' %}
READY TO CONNECT?

Let's build something
meaningful together.

Get in touch ↗
{% endif %}
+{% endif %}
diff --git a/themes/corporate/versions/1.0.0/theme.css b/themes/corporate/versions/1.0.0/theme.css new file mode 100644 index 0000000..a081c46 --- /dev/null +++ b/themes/corporate/versions/1.0.0/theme.css @@ -0,0 +1 @@ +:root{--ink:#152040;--blue:#283d8c;--purple:#6558b9;--light:#f6f7fb}*{box-sizing:border-box}body{margin:0;background:white;color:#172347;font-family:Inter,"Segoe UI",Arial,sans-serif;line-height:1.65}a{color:inherit;text-decoration:none}img{max-width:100%}.shell{width:min(1200px,89%);margin:auto}.announcement{height:35px;background:#262d70;color:white;display:flex;justify-content:center;align-items:center;gap:12px;font-size:10px;letter-spacing:1.3px}.announcement span{color:#b8d6fd}.header{height:94px;display:flex;align-items:center;justify-content:space-between;gap:20px}.logo{font-size:23px;font-weight:850;letter-spacing:-1px;display:flex;align-items:center;gap:12px}.logo-box{display:grid;place-items:center;width:43px;height:43px;color:#fff;font-size:25px;background:linear-gradient(135deg,#5b6ce0,#7c4dc3);border-radius:13px}.header nav{display:flex;gap:28px;font-size:13px;font-weight:600;flex-wrap:wrap}.header nav a:hover{color:#585cca}.nav-btn{background:#243475;color:#fff;padding:13px 20px;border-radius:8px;font-size:12px;font-weight:700}.nav-btn span{margin-left:18px}.hero{background:linear-gradient(120deg,#f5f7ff,#ebeeff 45%,#f8f6fc);overflow:hidden}.hero-grid{min-height:580px;display:grid;grid-template-columns:53% 47%;align-items:center}.hero-text{padding:70px 0}.pill{font-size:10px;letter-spacing:1.5px;color:#5549ac;background:#e5e5fa;border-radius:50px;display:inline-block;padding:8px 15px;font-weight:750}.hero h1{font-size:clamp(52px,6.2vw,87px);line-height:1.06;letter-spacing:-4px;margin:32px 0 20px;max-width:620px;color:#19265e}.hero p{font-size:18px;line-height:1.8;color:#69718f;max-width:520px}.actions{display:flex;gap:14px;flex-wrap:wrap;margin-top:34px}.primary,.secondary{padding:14px 24px;border-radius:8px;font-size:13px;font-weight:750}.primary{background:#38489e;color:#fff}.secondary{background:white;border:1px solid #d2d7ed}.trusted{font-size:10px;color:#9b9db6;letter-spacing:2px;margin-top:77px}.trusted span{padding:0 9px}.abstract-art{position:relative;height:100%;min-height:440px;display:grid;place-items:center}.abstract-art img{max-height:510px;object-fit:contain;filter:drop-shadow(0 25px 30px #33376620)}.orb{position:absolute;border-radius:50%;filter:drop-shadow(18px 15px 25px #5b69b125)}.orb-main{width:350px;height:350px;background:radial-gradient(circle at 30% 25%,#d9ddff,#8493e4 40%,#3d539e 70%,#1c3079);transform:rotate(-20deg)}.orb-inner{width:250px;height:250px;background:radial-gradient(circle at 30% 35%,#fff9,#a1aff366 38%,#5c68aa9c 65%);border:1px solid #ffffffc4;transform:translate(80px,60px)}.orb-small{width:93px;height:93px;background:linear-gradient(125deg,#c59fff,#6851a6);transform:translate(-155px,-155px)}.art-caption{position:relative;z-index:3;color:#fff;font-size:15px;line-height:1.4;letter-spacing:3px;text-align:center;font-weight:800;transform:translate(5px,20px)}.inner-hero{padding:85px 0;background:#f0f2ff}.inner-hero h1{font-size:clamp(40px,6vw,70px);letter-spacing:-2px;line-height:1.1;margin:20px 0}.inner-hero p{color:#69748c}.content{padding:60px 0 100px}.nudge{color:#6b6c95;font-size:10px;letter-spacing:2px;font-weight:750}.nudge span{color:#7b58d0;margin-right:7px}.section-header{margin:62px 0 25px;display:flex;justify-content:space-between;align-items:end}.section-header h2{font-size:40px;letter-spacing:-1.5px;margin:7px 0}.section-header>span{font-size:12px;color:#858eaa}.kicker{font-size:11px;color:#685dba;letter-spacing:2px}.tiles{display:grid;grid-template-columns:repeat(3,1fr);gap:22px}.tile{border:1px solid #eceef8;border-radius:15px;overflow:hidden;transition:box-shadow .2s,transform .2s}.tile:hover{box-shadow:0 18px 32px #323c7c18;transform:translateY(-4px)}.tile-image{height:195px;background:linear-gradient(130deg,#cbd3f5,#929de0);display:grid;place-items:center}.tile-image img{width:100%;height:100%;object-fit:contain}.tile-image>span{font-size:85px;color:#ffffffa8}.tile-info{padding:25px}.tile-info>span{font-size:10px;color:#7d6eba;letter-spacing:1.5px}.tile-info h3{font-size:23px;margin:9px 0}.tile-info p{font-size:13px;color:#7c859b;min-height:40px}.tile-info b{font-size:12px;color:#4e51a3}.rich-content{margin:30px 0}.section{padding:40px 0}.feature-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:15px}.feature{padding:27px;background:#f5f6fd;border-radius:10px}.feature-mark{color:#6e68c7}.stat strong{display:block;font-size:38px}.stats{display:grid;grid-template-columns:repeat(4,1fr);gap:20px}.cta{padding:50px;background:#252d73;color:white;border-radius:18px}.cta p{color:#c2c6ec}.big-cta{background:#293788;color:white;border-radius:20px;padding:60px;margin-top:90px;position:relative;overflow:hidden}.big-cta:after{content:'✦';position:absolute;right:-15px;top:-110px;font-size:410px;opacity:.10}.big-cta span{font-size:10px;letter-spacing:2px}.big-cta h2{font-size:45px;line-height:1.2;letter-spacing:-2px}.big-cta a{display:inline-block;background:white;color:#313e9a;padding:14px 20px;border-radius:9px;font-size:12px;font-weight:800}.detail-image{text-align:center;margin:30px 0}.detail-image img{max-height:520px}footer{background:#151c3c;color:white;padding:70px 0 30px}.footer-upper{display:flex;align-items:center;justify-content:space-between}.footer-upper h2{font-size:30px}.footer-upper p{color:#a6acce}.footer-line{border-top:1px solid #424866;padding-top:28px;margin-top:60px;display:flex;justify-content:space-between;color:#a7aed4;font-size:10px;letter-spacing:1.4px}@media(max-width:900px){.header{height:auto;padding:20px 0;flex-wrap:wrap}.header nav{order:3;width:100%}.hero-grid{grid-template-columns:1fr}.abstract-art{min-height:340px}.trusted{margin-top:40px}.tiles{grid-template-columns:repeat(2,1fr)}}@media(max-width:560px){.tiles,.feature-grid,.stats{grid-template-columns:1fr}.hero h1{font-size:54px}.footer-upper,.footer-line,.section-header{align-items:start;flex-direction:column}.announcement{padding:0 8px;font-size:8px}.big-cta h2{font-size:34px}} diff --git a/themes/corporate/visual/hero.svg b/themes/corporate/visual/hero.svg new file mode 100644 index 0000000..f89e148 --- /dev/null +++ b/themes/corporate/visual/hero.svg @@ -0,0 +1 @@ +IDEAS / PROGRESS diff --git a/themes/industrial/manifest.json b/themes/industrial/manifest.json new file mode 100644 index 0000000..ed1c14e --- /dev/null +++ b/themes/industrial/manifest.json @@ -0,0 +1,54 @@ +{ + "id": "industrial", + "name": "工业制造", + "description": "商业主题体验升级:独立视觉素材、响应式排版、产品列表筛选、多级导航、可访问性与交付检查(需真实内容验收)", + "version": "1.1.0", + "style": "technical", + "entry": "pro.html", + "styles": [ + "theme.css", + "blocks.css" + ], + "page_templates": [ + "home", + "about", + "contact", + "listing", + "category", + "product", + "article", + "brand", + "case", + "service", + "faq", + "standard", + "notfound" + ], + "blocks": [ + "hero", + "richtext", + "features", + "stats", + "collection", + "image_text", + "gallery", + "faq", + "cta", + "attributes", + "spacer" + ], + "responsive": true, + "auto_binding": true, + "professional_layout": true, + "commercial_ui": true, + "commercial_features": [ + "responsive-design", + "product-detail", + "brand-collections", + "gallery", + "content-facets", + "mega-navigation", + "contact-integration", + "readiness-audit" + ] +} \ No newline at end of file diff --git a/themes/industrial/page-presets.json b/themes/industrial/page-presets.json new file mode 100644 index 0000000..8ae1aa6 --- /dev/null +++ b/themes/industrial/page-presets.json @@ -0,0 +1,285 @@ +{ + "home": [ + { + "id": "0cdf18bf7f", + "type": "hero", + "visible": true, + "title": "工程质量,值得信赖", + "subtitle": "INDUSTRIAL ENGINEERING", + "description": "{{site.tagline}}", + "button": "查看产品", + "href": "/product/", + "align": "left" + }, + { + "id": "f846a2ff6b", + "type": "features", + "visible": true, + "title": "制造与服务能力", + "items": [ + { + "title": "专业服务", + "text": "从咨询到交付" + }, + { + "title": "品质保障", + "text": "严格的质量流程" + }, + { + "title": "全球网络", + "text": "连接不同市场" + } + ] + }, + { + "id": "ff4655d684", + "type": "collection", + "visible": true, + "title": "工业产品系列", + "subtitle": "PRODUCT SOLUTIONS", + "source": "product", + "limit": 6, + "columns": 3, + "button": "查看全部", + "href": "/product/" + }, + { + "id": "4355daaba0", + "type": "image_text", + "visible": true, + "title": "工程标准与服务流程", + "subtitle": "QUALITY FIRST", + "description": "展示业务流程和服务理念。", + "image": "", + "image_side": "right", + "button": "了解更多", + "href": "/about/" + }, + { + "id": "9e5056370d", + "type": "cta", + "visible": true, + "title": "获取专业产品方案", + "subtitle": "告诉我们您的需求,我们将提供适合的方案。", + "button": "联系我们", + "href": "/contact/" + } + ], + "about": [ + { + "id": "8ced1e0493", + "type": "image_text", + "visible": true, + "title": "认识我们", + "subtitle": "OUR APPROACH", + "description": "展示业务流程和服务理念。", + "image": "", + "image_side": "right", + "button": "了解更多", + "href": "/about/" + }, + { + "id": "52b6064084", + "type": "stats", + "visible": true, + "title": "我们的实力", + "items": [ + { + "value": "50,000+", + "label": "服务案例" + }, + { + "value": "70+", + "label": "市场覆盖" + }, + { + "value": "30+", + "label": "合作伙伴" + } + ] + }, + { + "id": "cf39bcb75d", + "type": "features", + "visible": true, + "title": "我们的优势", + "items": [ + { + "title": "专业服务", + "text": "从咨询到交付" + }, + { + "title": "品质保障", + "text": "严格的质量流程" + }, + { + "title": "全球网络", + "text": "连接不同市场" + } + ] + } + ], + "contact": [ + { + "id": "597f6ffb08", + "type": "richtext", + "visible": true, + "title": "联系我们", + "body": "

请填写企业联系方式与地址。

" + }, + { + "id": "8c02c61415", + "type": "cta", + "visible": true, + "title": "欢迎联系", + "subtitle": "告诉我们您的需求,我们将提供适合的方案。", + "button": "联系我们", + "href": "/contact/" + } + ], + "listing": [ + { + "id": "2c3106baf6", + "type": "collection", + "visible": true, + "title": "工业产品目录", + "subtitle": "为客户提供适合的解决方案", + "source": "product", + "limit": 24, + "columns": 3, + "button": "查看全部", + "href": "/product/" + } + ], + "product": [ + { + "id": "42925dc8e3", + "type": "image_text", + "visible": true, + "title": "产品介绍", + "subtitle": "OUR APPROACH", + "description": "{{page.summary}}", + "image": "{{page.cover}}", + "image_side": "right", + "button": "了解更多", + "href": "/about/" + }, + { + "id": "503266af35", + "type": "attributes", + "visible": true, + "title": "产品规格与参数" + }, + { + "id": "b2b1f5293c", + "type": "richtext", + "visible": true, + "title": "产品详情", + "body": "{{page.body}}" + }, + { + "id": "3837f66074", + "type": "cta", + "visible": true, + "title": "开启新的合作", + "subtitle": "告诉我们您的需求,我们将提供适合的方案。", + "button": "联系我们", + "href": "/contact/" + } + ], + "article": [ + { + "id": "c2573cb88d", + "type": "richtext", + "visible": true, + "title": "{{page.title}}", + "body": "{{page.body}}" + } + ], + "brand": [ + { + "id": "f43b705317", + "type": "image_text", + "visible": true, + "title": "{{page.title}}", + "subtitle": "OUR APPROACH", + "description": "{{page.summary}}", + "image": "{{page.cover}}", + "image_side": "right", + "button": "了解更多", + "href": "/about/" + }, + { + "id": "b3faf08170", + "type": "collection", + "visible": true, + "title": "品牌产品", + "subtitle": "为客户提供适合的解决方案", + "source": "product", + "limit": 6, + "columns": 3, + "button": "查看全部", + "href": "/product/" + } + ], + "standard": [ + { + "id": "7937550319", + "type": "richtext", + "visible": true, + "title": "{{page.title}}", + "body": "{{page.body}}" + } + ], + "notfound": [ + { + "id": "59383d762e", + "type": "cta", + "visible": true, + "title": "页面暂时无法访问", + "subtitle": "告诉我们您的需求,我们将提供适合的方案。", + "button": "返回首页", + "href": "/" + } + ], + "case": [ + { + "type": "image_text", + "title": "{{page.title}}", + "image": "{{page.cover}}", + "description": "{{page.summary}}" + }, + { + "type": "richtext", + "title": "项目概述", + "body": "{{page.body}}" + } + ], + "service": [ + { + "type": "richtext", + "title": "{{page.title}}", + "body": "{{page.body}}" + }, + { + "type": "cta", + "title": "了解更多", + "href": "/contact/" + } + ], + "faq": [ + { + "type": "faq", + "title": "常见问题", + "items": [] + } + ], + "category": [ + { + "type": "collection", + "title": "分类产品", + "source": "product", + "limit": 24 + } + ] +} \ No newline at end of file diff --git a/themes/industrial/pages/about.html b/themes/industrial/pages/about.html new file mode 100644 index 0000000..945be44 --- /dev/null +++ b/themes/industrial/pages/about.html @@ -0,0 +1,2 @@ +
{{ 'GET IN TOUCH' if kind=='contact' else ('OUR STORY' if kind=='about' else 'EXPLORE') }}

{{page.title}}

{{page.summary}}

+
{{'CONNECT / CONTACT' if kind=='contact' else 'ABOUT / DISCOVER'}}

{{'让我们开始沟通。' if kind=='contact' else page.title}}

{% if kind=='contact' and footer_email %}{{footer_email}} ↗{% elif kind=='contact' %}

请先在网站管理后台填写正式联系邮箱。

{% endif %}
{{body_copy(content,page)}}{% if page.cover %}{{page.title}}{% endif %}
diff --git a/themes/industrial/pages/article.html b/themes/industrial/pages/article.html new file mode 100644 index 0000000..85626e4 --- /dev/null +++ b/themes/industrial/pages/article.html @@ -0,0 +1 @@ +
LATEST NEWS / INSIGHTS

{{page.title}}

{{page.summary}}

{% if fields.get('publish_date') %}{% endif %}
diff --git a/themes/industrial/pages/brand.html b/themes/industrial/pages/brand.html new file mode 100644 index 0000000..a710280 --- /dev/null +++ b/themes/industrial/pages/brand.html @@ -0,0 +1 @@ +
BRAND PROFILE / PARTNERS

{{page.title}}

{{page.summary}}

{{cover(page,'brand')}}
{{body_copy(content,page)}}{% if related %}{{section_title('PRODUCT LINE','相关产品','/products/')}}{{grid(related,'product')}}{% endif %}
diff --git a/themes/industrial/pages/case.html b/themes/industrial/pages/case.html new file mode 100644 index 0000000..79e4d72 --- /dev/null +++ b/themes/industrial/pages/case.html @@ -0,0 +1 @@ +
SELECTED WORK / PROJECT

{{page.title}}

{{page.summary}}

{% if page.cover %}{{page.title}}{% endif %}
{% if fields.get('location') %}项目地点
{{fields.get('location')}}
{% endif %}{% if fields.get('industry') %}所属行业
{{fields.get('industry')}}
{% endif %}
{{body_copy(content,page)}}浏览更多案例 →
diff --git a/themes/industrial/pages/category.html b/themes/industrial/pages/category.html new file mode 100644 index 0000000..3918172 --- /dev/null +++ b/themes/industrial/pages/category.html @@ -0,0 +1,2 @@ +
DISCOVER / {{'CATEGORY' if kind=='category' else 'COLLECTION'}}

{{page.title}}

{{page.summary or '查看此栏目的全部已发布内容。'}}

首页/{{page.title}}
+
{{ filters() }}
{{grid(listing,'catalog')}}
diff --git a/themes/industrial/pages/contact.html b/themes/industrial/pages/contact.html new file mode 100644 index 0000000..3a52a60 --- /dev/null +++ b/themes/industrial/pages/contact.html @@ -0,0 +1,7 @@ +
{{ 'GET IN TOUCH' if kind=='contact' else ('OUR STORY' if kind=='about' else 'EXPLORE') }}

{{page.title}}

{{page.summary}}

+
{{'CONNECT / CONTACT' if kind=='contact' else 'ABOUT / DISCOVER'}}

{{'让我们开始沟通。' if kind=='contact' else page.title}}

{% if kind=='contact' and footer_email %}{{footer_email}} ↗{% elif kind=='contact' %}

请先在网站管理后台填写正式联系邮箱。

{% endif %}
{{body_copy(content,page)}}{% if page.cover %}{{page.title}}{% endif %}
+ +{% if form_endpoint %} +
CONTACT FORM

提交业务咨询

提交内容将直接发送到网站管理员配置的外部表单服务。

+

表单提交由外部服务处理,请确认其隐私政策及垃圾信息过滤设置。

+{% endif %} diff --git a/themes/industrial/pages/faq.html b/themes/industrial/pages/faq.html new file mode 100644 index 0000000..945be44 --- /dev/null +++ b/themes/industrial/pages/faq.html @@ -0,0 +1,2 @@ +
{{ 'GET IN TOUCH' if kind=='contact' else ('OUR STORY' if kind=='about' else 'EXPLORE') }}

{{page.title}}

{{page.summary}}

+
{{'CONNECT / CONTACT' if kind=='contact' else 'ABOUT / DISCOVER'}}

{{'让我们开始沟通。' if kind=='contact' else page.title}}

{% if kind=='contact' and footer_email %}{{footer_email}} ↗{% elif kind=='contact' %}

请先在网站管理后台填写正式联系邮箱。

{% endif %}
{{body_copy(content,page)}}{% if page.cover %}{{page.title}}{% endif %}
diff --git a/themes/industrial/pages/home.html b/themes/industrial/pages/home.html new file mode 100644 index 0000000..145f1ab --- /dev/null +++ b/themes/industrial/pages/home.html @@ -0,0 +1,4 @@ +
ENGINEERED FOR WHAT'S NEXT / 001

{{page.title if page.title not in (site.name,'首页','Home') else site.name}} BUILT TO PERFORM.

{{page.summary or site.tagline or '从材料到解决方案,一站式展示企业实力。'}}

{% if page.cover %}{{page.title}}{% else %}
FORM / FUNCTION
{% endif %}
+{% if categories %}
{{section_title('PRODUCT SYSTEMS','从类别开始探索','/products/')}}
{% for cat in categories[:6] %}0{{loop.index}}{{cat.name}}↗{% endfor %}
{% endif %} + +
APPLICATIONS & SUPPORT

专注可靠交付,服务多元项目。

{{site.tagline or '企业技术、材料和服务能力展示。'}}

服务与解决方案 ↗
diff --git a/themes/industrial/pages/listing.html b/themes/industrial/pages/listing.html new file mode 100644 index 0000000..3918172 --- /dev/null +++ b/themes/industrial/pages/listing.html @@ -0,0 +1,2 @@ +
DISCOVER / {{'CATEGORY' if kind=='category' else 'COLLECTION'}}

{{page.title}}

{{page.summary or '查看此栏目的全部已发布内容。'}}

首页/{{page.title}}
+
{{ filters() }}
{{grid(listing,'catalog')}}
diff --git a/themes/industrial/pages/notfound.html b/themes/industrial/pages/notfound.html new file mode 100644 index 0000000..37e3f05 --- /dev/null +++ b/themes/industrial/pages/notfound.html @@ -0,0 +1 @@ +
ERROR / 404

404

抱歉,你访问的页面不存在,或者已被移动。

返回首页 ↗
diff --git a/themes/industrial/pages/product.html b/themes/industrial/pages/product.html new file mode 100644 index 0000000..ba1fe4f --- /dev/null +++ b/themes/industrial/pages/product.html @@ -0,0 +1,3 @@ +
首页/产品中心/{{page.title}}
+
PRODUCT DETAILS / {{fields.get('brand') or 'COLLECTION'}}

{{page.title}}

{{page.summary or '在此查看产品详细资料。'}}

{% if fields.get('brand') %}

品牌 · {{fields.get('brand')}}

{% endif %}咨询该产品 ↗
+
OVERVIEW

产品介绍

{{body_copy(content,page)}}
SPECIFICATIONS

规格参数

{% for row in specs %}
{{row.label}}{{row.value}}
{% else %}

尚未填写详细参数。

{% endfor %}
diff --git a/themes/industrial/pages/search.html b/themes/industrial/pages/search.html new file mode 100644 index 0000000..182a878 --- /dev/null +++ b/themes/industrial/pages/search.html @@ -0,0 +1,2 @@ +
SEARCH / CONTENT

站内搜索

搜索本网站的产品、品牌、新闻和案例。

+
diff --git a/themes/industrial/pages/service.html b/themes/industrial/pages/service.html new file mode 100644 index 0000000..945be44 --- /dev/null +++ b/themes/industrial/pages/service.html @@ -0,0 +1,2 @@ +
{{ 'GET IN TOUCH' if kind=='contact' else ('OUR STORY' if kind=='about' else 'EXPLORE') }}

{{page.title}}

{{page.summary}}

+
{{'CONNECT / CONTACT' if kind=='contact' else 'ABOUT / DISCOVER'}}

{{'让我们开始沟通。' if kind=='contact' else page.title}}

{% if kind=='contact' and footer_email %}{{footer_email}} ↗{% elif kind=='contact' %}

请先在网站管理后台填写正式联系邮箱。

{% endif %}
{{body_copy(content,page)}}{% if page.cover %}{{page.title}}{% endif %}
diff --git a/themes/industrial/pages/standard.html b/themes/industrial/pages/standard.html new file mode 100644 index 0000000..945be44 --- /dev/null +++ b/themes/industrial/pages/standard.html @@ -0,0 +1,2 @@ +
{{ 'GET IN TOUCH' if kind=='contact' else ('OUR STORY' if kind=='about' else 'EXPLORE') }}

{{page.title}}

{{page.summary}}

+
{{'CONNECT / CONTACT' if kind=='contact' else 'ABOUT / DISCOVER'}}

{{'让我们开始沟通。' if kind=='contact' else page.title}}

{% if kind=='contact' and footer_email %}{{footer_email}} ↗{% elif kind=='contact' %}

请先在网站管理后台填写正式联系邮箱。

{% endif %}
{{body_copy(content,page)}}{% if page.cover %}{{page.title}}{% endif %}
diff --git a/themes/industrial/pro.css b/themes/industrial/pro.css new file mode 100644 index 0000000..a3b4f8f --- /dev/null +++ b/themes/industrial/pro.css @@ -0,0 +1,7 @@ +:root{--pro-text:#173a57;--pro-muted:#6b8191;--pro-border:#d8e3eb;--pro-background:#fff;--pro-surface:#edf3f7;--pro-accent:#16629a;--pro-hero:#0e3959;--pro-radius:2px;--pro-headings:Arial,sans-serif;--pro-wide:1290px} +.pro-header{background:#fff}.pro-logo-symbol{color:#14649b;font-size:38px}.pro-utility{background:#0d3a5a;color:#c6e2f1}.pro-header-cta{border-radius:2px;background:#0c5b94}.pro-industrial-hero{background:linear-gradient(90deg,#eff4f8 0%,#e6eef5 60%,#d7e5f0 100%);color:#123b59}.pro-industrial-hero .pro-hero-copy{padding-right:6%}.pro-industrial-hero .pro-hero-copy h1{font-size:clamp(45px,5.3vw,80px)}.pro-industrial-hero .pro-hero-copy h1 em{color:#186fa4}.pro-industrial-hero .pro-hero-art{border-radius:0;background:#0c466d}.pro-technical{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;gap:3%;overflow:hidden;background:repeating-linear-gradient(0deg,transparent,transparent 39px,#ffffff0b 40px),repeating-linear-gradient(90deg,transparent,transparent 39px,#ffffff0b 40px)}.pro-technical>div{width:21%;height:55%;border-radius:60px;border:27px solid #6ba6d4;transform:skewY(-13deg) rotate(-5deg);box-shadow:18px 17px 0 #14324a,0 0 0 9px #bde2fb inset,12px 22px 50px #081c30}.pro-technical>div:nth-child(2){height:72%;width:27%;border-width:34px}.pro-technical strong{position:absolute;bottom:24px;left:28px;color:#aed1e8;font-size:13px;letter-spacing:.17em}.pro-outline-button{color:#0f5988}.pro-category-band{background:#fff}.pro-category-tiles a{background:#eaf2f7;border-top:4px solid #2176aa;min-height:185px;transition:background .2s}.pro-category-tiles a:hover{background:#dce9f2}.pro-category-tiles strong{font-size:21px}.pro-featured{background:#edf3f7}.pro-card{background:#fff;border-radius:2px}.pro-card-cover{background:#d8e6ef}.pro-art-industrial,.pro-art-product{background:linear-gradient(135deg,#aecadf,#e1f1fb)}.pro-art-center{color:#45789b99}.pro-statement{background:#103e5e;color:#fff}.pro-statement .pro-eyebrow{color:#8dc8eb}.pro-statement p{color:#d3e1e9}.pro-statement .pro-text-link{color:#9cdaff}.pro-inner-hero{background:linear-gradient(115deg,#edf4f9 0%,#d9e9f4 100%)}.pro-inner-hero:after{content:'+';position:absolute;right:11%;top:6%;font-size:350px;font-weight:100;color:#15699b10;line-height:1}.pro-article-hero{background:#dceaf4}.pro-article-hero p{color:#537088}.pro-footer{background:#092c45}.pro-end-cta{background:#0a4772}.pro-end-cta a{background:#fff;color:#144b75!important}.pro-404 h1{color:#156d9d} + +/* SiteForge V5 commercial signature */ +.pro-industrial-hero .pro-hero-art{background-image:url("./visual/hero.svg")!important;background-position:center;background-size:cover;} + +.pro-industrial-hero .pro-hero-art{border-radius:0;box-shadow:25px 25px 0 #ffffff9c}.pro-industrial-hero .pro-hero-art .pro-technical{opacity:0}.pro-category-tiles a{transition:transform .2s,background .2s}.pro-category-tiles a:hover{transform:translateY(-4px);background:#ddeef9}.pro-card{border-radius:0;border:1px solid #cddfe9}.pro-card-cover{background:linear-gradient(145deg,#dceaf0,#c3d9e7)}.pro-art{background:repeating-linear-gradient(0deg,#e2edf4,#e2edf4 24px,#d6e7ef 25px);color:#20577e}.pro-featured{background:#f4f8fb}.pro-statement{background:#103b57;color:#fff}.pro-statement p{color:#d4e5ed}.pro-statement .pro-text-link{color:#a8d8ee}.pro-footer{background:#0e3046;color:#eff5f9} diff --git a/themes/industrial/pro.html b/themes/industrial/pro.html new file mode 100644 index 0000000..355acb0 --- /dev/null +++ b/themes/industrial/pro.html @@ -0,0 +1,10 @@ +{{ seo_title }}{% if canonical %}{% endif %}{% if canonical and not noindex %}{% endif %}{% if theme_overrides %}{% endif %}跳转至正文 +{% from '_professional_components.html' import card,cover,grid,section_title,filters,footer_links,body_copy with context %} +
ENGINEERING · MATERIALS · SUPPLYINDUSTRIAL SOLUTIONS
+
询价咨询 ↗
{% if categories %}{% endif %}
+{% if not designer_replace %}{% include 'industrial/pages/' ~ kind ~ '.html' %}{% endif %} + +{% if has_custom and kind!='notfound' %}
{{custom_blocks}}
{% else %}
{% endif %} +{% if kind not in ('notfound','search') %}
LET'S CONNECT

从这里开始下一步。

{{site.tagline or '了解更多产品、服务与合作信息。'}}

联系我们 ↗
{% endif %} +
+ \ No newline at end of file diff --git a/themes/industrial/site.html b/themes/industrial/site.html new file mode 100644 index 0000000..2333121 --- /dev/null +++ b/themes/industrial/site.html @@ -0,0 +1,12 @@ +{{seo_title}}{% if canonical %}{% endif %} +
ENGINEERING · MANUFACTURING · SUPPLYGLOBAL INDUSTRIAL SOLUTIONS
+
▰{{site.name}}INDUSTRIAL SOLUTIONSREQUEST A QUOTE ↗
+{% if blueprint_mode %}{% elif mode=='home' %}
STRONGER CONNECTIONS. SMARTER SOLUTIONS.

{{page.title if page.title!=site.name else 'BUILT TO LAST.'}}

{{page.summary or site.tagline or 'Industrial excellence, quality products and dependable worldwide delivery.'}}

EXPLORE PRODUCTS ↗
01 / MATERIALS02 / ENGINEERING03 / SERVICE
{% if page.cover %}{{page.title}}{% else %}
INDUSTRY
IN MOTION
{% endif %}
+{% elif mode=='notfound' %}
ERROR

404 — Page not found

+{% else %}
{{type_names.get(page.type,'COMPANY')|upper}} / {{'PRODUCT CATALOG' if mode=='listing' else 'OUR EXPERTISE'}}

{{page.title}}

{{page.summary}}

{% endif %} +{% if blueprint_mode %}
{{blocks}}
{% else %}
{% if mode=='home' %}
01PRODUCT ENGINEERING
02GLOBAL SOURCING
03QUALITY ASSURANCE
{% endif %} +{% if mode in ['home','detail'] and (content or blocks) %}
{{blocks}}{{content}}
{% endif %} +{% if mode=='detail' and page.cover %}
{{page.title}}
{% endif %} +{% set cards=listing_items if mode=='listing' else entries|selectattr('type','equalto','product')|list if mode=='home' else [] %} +{% if cards %}
OUR PORTFOLIO

{{'Product Catalogue' if mode=='listing' else 'Explore Our Products'}}

{% if mode=='home' %}ALL PRODUCTS ↗{% endif %}
{% endif %}
+{% endif %}
© {{site.name}}DESIGNED TO DELIVER. BUILT WITH SITEFORGE.
diff --git a/themes/industrial/theme.css b/themes/industrial/theme.css new file mode 100644 index 0000000..7bd34c1 --- /dev/null +++ b/themes/industrial/theme.css @@ -0,0 +1 @@ +:root{--blue:#0a3d5c;--sky:#daeaf2;--orange:#ef8f35;--text:#1d3441}*{box-sizing:border-box}body{margin:0;color:var(--text);background:#f8fafb;font-family:Inter,"Segoe UI",Arial,sans-serif;line-height:1.6}a{color:inherit;text-decoration:none}img{max-width:100%;object-fit:contain}.wrap{width:min(1300px,90%);margin:auto}.top{background:#153d56;color:#a7c5d5;font-size:10px;letter-spacing:2px}.top .wrap{display:flex;justify-content:space-between;padding:9px 0}header{background:white;border-bottom:1px solid #e0e8ea}.header-inner{min-height:94px;display:flex;justify-content:space-between;align-items:center;gap:20px}.brand{display:flex;align-items:center;gap:12px;font-size:21px;font-weight:800;letter-spacing:-.5px;line-height:1}.brand-icon{display:grid;place-items:center;width:43px;height:43px;background:var(--blue);color:white;font-size:26px}.brand small{display:block;font-size:10px;color:#718793;letter-spacing:2px;margin-top:9px;font-weight:500}nav{display:flex;gap:25px;font-size:13px;font-weight:650;flex-wrap:wrap}nav a:hover{color:#eb8725}.request{background:#e88c36;color:white;padding:15px 20px;font-size:11px;font-weight:800;white-space:nowrap;letter-spacing:.5px}.hero{background:linear-gradient(107deg,#073653 0%,#0e5270 51%,#14748c 100%);color:white;overflow:hidden}.hero-grid{display:grid;grid-template-columns:55% 45%;min-height:530px}.hero-text{padding:76px 25px 60px 0;position:relative;z-index:1}.kicker{font-size:11px;letter-spacing:2px;font-weight:750;color:#ce882e}.hero .kicker{color:#f5ac57}.kicker i{display:inline-block;width:22px;height:2px;background:#f5ac57;vertical-align:middle;margin-right:10px}.hero h1{font-size:clamp(48px,6vw,86px);letter-spacing:-3px;line-height:1.02;max-width:700px;margin:35px 0 26px}.hero p{max-width:470px;color:#c6e1e9;font-size:17px}.button{display:inline-flex;gap:55px;align-items:center;background:#ef9136;padding:18px 22px;font-size:12px;letter-spacing:1px;font-weight:800;margin-top:26px}.button b{font-size:18px}.hero-tags{display:flex;gap:22px;margin-top:60px;font-size:10px;color:#85bfcb;letter-spacing:1px}.hero-art{position:relative;display:grid;place-items:center;overflow:hidden;background:radial-gradient(circle at center,#2c94a6 2%,#11617b 45%,transparent 70%)}.hero-art:before,.hero-art:after{content:"";position:absolute;width:540px;height:540px;border:1px solid #ffffff25;transform:rotate(45deg)}.hero-art:after{width:380px;height:380px}.hero-art img{position:relative;z-index:3;width:100%;max-height:480px;filter:drop-shadow(0 20px 20px #002a3b55)}.hero-art>span{position:relative;z-index:3;color:#c6eaf0;font-size:25px;letter-spacing:8px;text-align:center;font-weight:300}.ring{position:absolute;width:430px;height:430px;border:28px solid #ffffff13;border-radius:50%;transform:rotateX(55deg) rotate(45deg)}.ring-2{width:290px;height:290px;border-color:#ffffff1d}.ring-3{width:150px;height:150px;border-color:#fba74b55}.subhero{padding:80px 0;background:linear-gradient(110deg,#073e5d,#0d6981);color:white}.subhero h1{font-size:clamp(40px,5.3vw,68px);line-height:1.1;margin:20px 0}.subhero p{color:#b8d6df}.main{padding:58px 0 100px}.metrics{display:grid;grid-template-columns:repeat(3,1fr);background:white;margin-top:-90px;position:relative;box-shadow:0 15px 45px #0b4f7515}.metrics>div{padding:25px 34px;display:flex;align-items:center;gap:18px;border-right:1px solid #eef3f5}.metrics strong{color:#dc7e2d;font-size:30px}.metrics span{letter-spacing:1px;font-size:11px;font-weight:800}.titlebar{margin:65px 0 28px;display:flex;justify-content:space-between;align-items:end}.titlebar h2{font-size:37px;margin:12px 0 0}.titlebar>a{color:#ba6d2f;font-size:12px;font-weight:800}.product-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:23px}.product{border:1px solid #e0e8ed;background:white}.product:hover{box-shadow:0 15px 35px #194b6518}.product-img{height:225px;display:flex;align-items:center;justify-content:center;background:linear-gradient(125deg,#e8f0f4,#cadce6);overflow:hidden}.product-img img{height:100%;width:100%;object-fit:contain}.technical-graphic{width:110px;height:110px;border:18px solid #96b8c8;border-radius:50%;position:relative;box-shadow:0 20px 0 #244d6120}.technical-graphic span{position:absolute;color:#e9fbff}.technical-graphic span:first-child{top:-56px;left:-50px}.technical-graphic span:nth-child(2){top:-56px;right:-50px}.technical-graphic span:nth-child(3){bottom:-55px;left:-45px}.technical-graphic span:nth-child(4){bottom:-55px;right:-45px}.product-info{padding:25px}.product-info>span{font-size:9px;color:#c17a38;letter-spacing:1.5px;font-weight:700}.product-info h3{font-size:21px;margin:8px 0}.product-info p{color:#6d8792;font-size:13px}.arrow{border-top:1px solid #e6eef1;margin-top:25px;padding-top:18px;display:flex;justify-content:space-between;font-size:11px;letter-spacing:1px;font-weight:800;color:#0c6788}.rich-content{margin:30px 0}.rich-content p{color:#617787}.section{padding:40px 0}.feature-grid,.stats{display:grid;grid-template-columns:repeat(3,1fr);gap:20px}.feature{background:white;padding:25px;border-top:4px solid #f09c52}.stat strong{display:block;font-size:40px}.stat span{color:#78909c}.cta{padding:50px;background:#0e5675;color:white}.detail-image img{max-height:560px}footer{background:#0f334a;color:white;padding:65px 0 25px}.footer-cols{display:flex;justify-content:space-between;gap:40px}.footer-cols h2{font-size:26px}.footer-cols p{color:#96b1bf}.bottom{border-top:1px solid #36596d;margin-top:65px;padding-top:22px;display:flex;justify-content:space-between;letter-spacing:1px;font-size:10px;color:#9cb9c7}@media(max-width:940px){.header-inner{flex-wrap:wrap;padding:16px 0}nav{order:3;width:100%}.request{display:none}.hero-grid{grid-template-columns:1fr}.hero-text{padding-right:0}.hero-art{min-height:300px}.metrics{margin-top:0}}@media(max-width:660px){.product-grid,.metrics,.feature-grid,.stats{grid-template-columns:1fr}.hero h1{font-size:51px}.hero-tags{flex-wrap:wrap}.footer-cols,.bottom{flex-direction:column}.top span:last-child{display:none}.subhero{padding:60px 0}} diff --git a/themes/industrial/versions/0.3.0/manifest.json b/themes/industrial/versions/0.3.0/manifest.json new file mode 100644 index 0000000..809d267 --- /dev/null +++ b/themes/industrial/versions/0.3.0/manifest.json @@ -0,0 +1,38 @@ +{ + "id": "industrial", + "name": "工业制造", + "description": "工业蓝与工程网格,产品分类及技术资料", + "version": "0.3.0", + "style": "technical", + "entry": "site.html", + "styles": [ + "theme.css", + "blocks.css" + ], + "page_templates": [ + "home", + "about", + "contact", + "listing", + "product", + "article", + "brand", + "standard", + "notfound" + ], + "blocks": [ + "hero", + "richtext", + "features", + "stats", + "collection", + "image_text", + "gallery", + "faq", + "cta", + "attributes", + "spacer" + ], + "responsive": true, + "auto_binding": true +} \ No newline at end of file diff --git a/themes/industrial/versions/0.3.0/page-presets.json b/themes/industrial/versions/0.3.0/page-presets.json new file mode 100644 index 0000000..3bedca8 --- /dev/null +++ b/themes/industrial/versions/0.3.0/page-presets.json @@ -0,0 +1,245 @@ +{ + "home": [ + { + "id": "0cdf18bf7f", + "type": "hero", + "visible": true, + "title": "工程质量,值得信赖", + "subtitle": "INDUSTRIAL ENGINEERING", + "description": "{{site.tagline}}", + "button": "查看产品", + "href": "/product/", + "align": "left" + }, + { + "id": "f846a2ff6b", + "type": "features", + "visible": true, + "title": "制造与服务能力", + "items": [ + { + "title": "专业服务", + "text": "从咨询到交付" + }, + { + "title": "品质保障", + "text": "严格的质量流程" + }, + { + "title": "全球网络", + "text": "连接不同市场" + } + ] + }, + { + "id": "ff4655d684", + "type": "collection", + "visible": true, + "title": "工业产品系列", + "subtitle": "PRODUCT SOLUTIONS", + "source": "product", + "limit": 6, + "columns": 3, + "button": "查看全部", + "href": "/product/" + }, + { + "id": "4355daaba0", + "type": "image_text", + "visible": true, + "title": "工程标准与服务流程", + "subtitle": "QUALITY FIRST", + "description": "展示业务流程和服务理念。", + "image": "", + "image_side": "right", + "button": "了解更多", + "href": "/about/" + }, + { + "id": "9e5056370d", + "type": "cta", + "visible": true, + "title": "获取专业产品方案", + "subtitle": "告诉我们您的需求,我们将提供适合的方案。", + "button": "联系我们", + "href": "/contact/" + } + ], + "about": [ + { + "id": "8ced1e0493", + "type": "image_text", + "visible": true, + "title": "认识我们", + "subtitle": "OUR APPROACH", + "description": "展示业务流程和服务理念。", + "image": "", + "image_side": "right", + "button": "了解更多", + "href": "/about/" + }, + { + "id": "52b6064084", + "type": "stats", + "visible": true, + "title": "我们的实力", + "items": [ + { + "value": "50,000+", + "label": "服务案例" + }, + { + "value": "70+", + "label": "市场覆盖" + }, + { + "value": "30+", + "label": "合作伙伴" + } + ] + }, + { + "id": "cf39bcb75d", + "type": "features", + "visible": true, + "title": "我们的优势", + "items": [ + { + "title": "专业服务", + "text": "从咨询到交付" + }, + { + "title": "品质保障", + "text": "严格的质量流程" + }, + { + "title": "全球网络", + "text": "连接不同市场" + } + ] + } + ], + "contact": [ + { + "id": "597f6ffb08", + "type": "richtext", + "visible": true, + "title": "联系我们", + "body": "

请填写企业联系方式与地址。

" + }, + { + "id": "8c02c61415", + "type": "cta", + "visible": true, + "title": "欢迎联系", + "subtitle": "告诉我们您的需求,我们将提供适合的方案。", + "button": "联系我们", + "href": "/contact/" + } + ], + "listing": [ + { + "id": "2c3106baf6", + "type": "collection", + "visible": true, + "title": "工业产品目录", + "subtitle": "为客户提供适合的解决方案", + "source": "product", + "limit": 24, + "columns": 3, + "button": "查看全部", + "href": "/product/" + } + ], + "product": [ + { + "id": "42925dc8e3", + "type": "image_text", + "visible": true, + "title": "产品介绍", + "subtitle": "OUR APPROACH", + "description": "{{page.summary}}", + "image": "{{page.cover}}", + "image_side": "right", + "button": "了解更多", + "href": "/about/" + }, + { + "id": "503266af35", + "type": "attributes", + "visible": true, + "title": "产品规格与参数" + }, + { + "id": "b2b1f5293c", + "type": "richtext", + "visible": true, + "title": "产品详情", + "body": "{{page.body}}" + }, + { + "id": "3837f66074", + "type": "cta", + "visible": true, + "title": "开启新的合作", + "subtitle": "告诉我们您的需求,我们将提供适合的方案。", + "button": "联系我们", + "href": "/contact/" + } + ], + "article": [ + { + "id": "c2573cb88d", + "type": "richtext", + "visible": true, + "title": "{{page.title}}", + "body": "{{page.body}}" + } + ], + "brand": [ + { + "id": "f43b705317", + "type": "image_text", + "visible": true, + "title": "{{page.title}}", + "subtitle": "OUR APPROACH", + "description": "{{page.summary}}", + "image": "{{page.cover}}", + "image_side": "right", + "button": "了解更多", + "href": "/about/" + }, + { + "id": "b3faf08170", + "type": "collection", + "visible": true, + "title": "品牌产品", + "subtitle": "为客户提供适合的解决方案", + "source": "product", + "limit": 6, + "columns": 3, + "button": "查看全部", + "href": "/product/" + } + ], + "standard": [ + { + "id": "7937550319", + "type": "richtext", + "visible": true, + "title": "{{page.title}}", + "body": "{{page.body}}" + } + ], + "notfound": [ + { + "id": "59383d762e", + "type": "cta", + "visible": true, + "title": "页面暂时无法访问", + "subtitle": "告诉我们您的需求,我们将提供适合的方案。", + "button": "返回首页", + "href": "/" + } + ] +} \ No newline at end of file diff --git a/themes/industrial/versions/0.3.0/site.html b/themes/industrial/versions/0.3.0/site.html new file mode 100644 index 0000000..2333121 --- /dev/null +++ b/themes/industrial/versions/0.3.0/site.html @@ -0,0 +1,12 @@ +{{seo_title}}{% if canonical %}{% endif %} +
ENGINEERING · MANUFACTURING · SUPPLYGLOBAL INDUSTRIAL SOLUTIONS
+
▰{{site.name}}INDUSTRIAL SOLUTIONSREQUEST A QUOTE ↗
+{% if blueprint_mode %}{% elif mode=='home' %}
STRONGER CONNECTIONS. SMARTER SOLUTIONS.

{{page.title if page.title!=site.name else 'BUILT TO LAST.'}}

{{page.summary or site.tagline or 'Industrial excellence, quality products and dependable worldwide delivery.'}}

EXPLORE PRODUCTS ↗
01 / MATERIALS02 / ENGINEERING03 / SERVICE
{% if page.cover %}{{page.title}}{% else %}
INDUSTRY
IN MOTION
{% endif %}
+{% elif mode=='notfound' %}
ERROR

404 — Page not found

+{% else %}
{{type_names.get(page.type,'COMPANY')|upper}} / {{'PRODUCT CATALOG' if mode=='listing' else 'OUR EXPERTISE'}}

{{page.title}}

{{page.summary}}

{% endif %} +{% if blueprint_mode %}
{{blocks}}
{% else %}
{% if mode=='home' %}
01PRODUCT ENGINEERING
02GLOBAL SOURCING
03QUALITY ASSURANCE
{% endif %} +{% if mode in ['home','detail'] and (content or blocks) %}
{{blocks}}{{content}}
{% endif %} +{% if mode=='detail' and page.cover %}
{{page.title}}
{% endif %} +{% set cards=listing_items if mode=='listing' else entries|selectattr('type','equalto','product')|list if mode=='home' else [] %} +{% if cards %}
OUR PORTFOLIO

{{'Product Catalogue' if mode=='listing' else 'Explore Our Products'}}

{% if mode=='home' %}ALL PRODUCTS ↗{% endif %}
{% endif %}
+{% endif %}
© {{site.name}}DESIGNED TO DELIVER. BUILT WITH SITEFORGE.
diff --git a/themes/industrial/versions/0.3.0/theme.css b/themes/industrial/versions/0.3.0/theme.css new file mode 100644 index 0000000..7bd34c1 --- /dev/null +++ b/themes/industrial/versions/0.3.0/theme.css @@ -0,0 +1 @@ +:root{--blue:#0a3d5c;--sky:#daeaf2;--orange:#ef8f35;--text:#1d3441}*{box-sizing:border-box}body{margin:0;color:var(--text);background:#f8fafb;font-family:Inter,"Segoe UI",Arial,sans-serif;line-height:1.6}a{color:inherit;text-decoration:none}img{max-width:100%;object-fit:contain}.wrap{width:min(1300px,90%);margin:auto}.top{background:#153d56;color:#a7c5d5;font-size:10px;letter-spacing:2px}.top .wrap{display:flex;justify-content:space-between;padding:9px 0}header{background:white;border-bottom:1px solid #e0e8ea}.header-inner{min-height:94px;display:flex;justify-content:space-between;align-items:center;gap:20px}.brand{display:flex;align-items:center;gap:12px;font-size:21px;font-weight:800;letter-spacing:-.5px;line-height:1}.brand-icon{display:grid;place-items:center;width:43px;height:43px;background:var(--blue);color:white;font-size:26px}.brand small{display:block;font-size:10px;color:#718793;letter-spacing:2px;margin-top:9px;font-weight:500}nav{display:flex;gap:25px;font-size:13px;font-weight:650;flex-wrap:wrap}nav a:hover{color:#eb8725}.request{background:#e88c36;color:white;padding:15px 20px;font-size:11px;font-weight:800;white-space:nowrap;letter-spacing:.5px}.hero{background:linear-gradient(107deg,#073653 0%,#0e5270 51%,#14748c 100%);color:white;overflow:hidden}.hero-grid{display:grid;grid-template-columns:55% 45%;min-height:530px}.hero-text{padding:76px 25px 60px 0;position:relative;z-index:1}.kicker{font-size:11px;letter-spacing:2px;font-weight:750;color:#ce882e}.hero .kicker{color:#f5ac57}.kicker i{display:inline-block;width:22px;height:2px;background:#f5ac57;vertical-align:middle;margin-right:10px}.hero h1{font-size:clamp(48px,6vw,86px);letter-spacing:-3px;line-height:1.02;max-width:700px;margin:35px 0 26px}.hero p{max-width:470px;color:#c6e1e9;font-size:17px}.button{display:inline-flex;gap:55px;align-items:center;background:#ef9136;padding:18px 22px;font-size:12px;letter-spacing:1px;font-weight:800;margin-top:26px}.button b{font-size:18px}.hero-tags{display:flex;gap:22px;margin-top:60px;font-size:10px;color:#85bfcb;letter-spacing:1px}.hero-art{position:relative;display:grid;place-items:center;overflow:hidden;background:radial-gradient(circle at center,#2c94a6 2%,#11617b 45%,transparent 70%)}.hero-art:before,.hero-art:after{content:"";position:absolute;width:540px;height:540px;border:1px solid #ffffff25;transform:rotate(45deg)}.hero-art:after{width:380px;height:380px}.hero-art img{position:relative;z-index:3;width:100%;max-height:480px;filter:drop-shadow(0 20px 20px #002a3b55)}.hero-art>span{position:relative;z-index:3;color:#c6eaf0;font-size:25px;letter-spacing:8px;text-align:center;font-weight:300}.ring{position:absolute;width:430px;height:430px;border:28px solid #ffffff13;border-radius:50%;transform:rotateX(55deg) rotate(45deg)}.ring-2{width:290px;height:290px;border-color:#ffffff1d}.ring-3{width:150px;height:150px;border-color:#fba74b55}.subhero{padding:80px 0;background:linear-gradient(110deg,#073e5d,#0d6981);color:white}.subhero h1{font-size:clamp(40px,5.3vw,68px);line-height:1.1;margin:20px 0}.subhero p{color:#b8d6df}.main{padding:58px 0 100px}.metrics{display:grid;grid-template-columns:repeat(3,1fr);background:white;margin-top:-90px;position:relative;box-shadow:0 15px 45px #0b4f7515}.metrics>div{padding:25px 34px;display:flex;align-items:center;gap:18px;border-right:1px solid #eef3f5}.metrics strong{color:#dc7e2d;font-size:30px}.metrics span{letter-spacing:1px;font-size:11px;font-weight:800}.titlebar{margin:65px 0 28px;display:flex;justify-content:space-between;align-items:end}.titlebar h2{font-size:37px;margin:12px 0 0}.titlebar>a{color:#ba6d2f;font-size:12px;font-weight:800}.product-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:23px}.product{border:1px solid #e0e8ed;background:white}.product:hover{box-shadow:0 15px 35px #194b6518}.product-img{height:225px;display:flex;align-items:center;justify-content:center;background:linear-gradient(125deg,#e8f0f4,#cadce6);overflow:hidden}.product-img img{height:100%;width:100%;object-fit:contain}.technical-graphic{width:110px;height:110px;border:18px solid #96b8c8;border-radius:50%;position:relative;box-shadow:0 20px 0 #244d6120}.technical-graphic span{position:absolute;color:#e9fbff}.technical-graphic span:first-child{top:-56px;left:-50px}.technical-graphic span:nth-child(2){top:-56px;right:-50px}.technical-graphic span:nth-child(3){bottom:-55px;left:-45px}.technical-graphic span:nth-child(4){bottom:-55px;right:-45px}.product-info{padding:25px}.product-info>span{font-size:9px;color:#c17a38;letter-spacing:1.5px;font-weight:700}.product-info h3{font-size:21px;margin:8px 0}.product-info p{color:#6d8792;font-size:13px}.arrow{border-top:1px solid #e6eef1;margin-top:25px;padding-top:18px;display:flex;justify-content:space-between;font-size:11px;letter-spacing:1px;font-weight:800;color:#0c6788}.rich-content{margin:30px 0}.rich-content p{color:#617787}.section{padding:40px 0}.feature-grid,.stats{display:grid;grid-template-columns:repeat(3,1fr);gap:20px}.feature{background:white;padding:25px;border-top:4px solid #f09c52}.stat strong{display:block;font-size:40px}.stat span{color:#78909c}.cta{padding:50px;background:#0e5675;color:white}.detail-image img{max-height:560px}footer{background:#0f334a;color:white;padding:65px 0 25px}.footer-cols{display:flex;justify-content:space-between;gap:40px}.footer-cols h2{font-size:26px}.footer-cols p{color:#96b1bf}.bottom{border-top:1px solid #36596d;margin-top:65px;padding-top:22px;display:flex;justify-content:space-between;letter-spacing:1px;font-size:10px;color:#9cb9c7}@media(max-width:940px){.header-inner{flex-wrap:wrap;padding:16px 0}nav{order:3;width:100%}.request{display:none}.hero-grid{grid-template-columns:1fr}.hero-text{padding-right:0}.hero-art{min-height:300px}.metrics{margin-top:0}}@media(max-width:660px){.product-grid,.metrics,.feature-grid,.stats{grid-template-columns:1fr}.hero h1{font-size:51px}.hero-tags{flex-wrap:wrap}.footer-cols,.bottom{flex-direction:column}.top span:last-child{display:none}.subhero{padding:60px 0}} diff --git a/themes/industrial/versions/1.0.0/manifest.json b/themes/industrial/versions/1.0.0/manifest.json new file mode 100644 index 0000000..6601893 --- /dev/null +++ b/themes/industrial/versions/1.0.0/manifest.json @@ -0,0 +1,43 @@ +{ + "id": "industrial", + "name": "工业制造", + "description": "工业蓝与工程网格,产品分类及技术资料;V3 专业全页模板、分类/详情/导航和搜索", + "version": "1.0.0", + "style": "technical", + "entry": "pro.html", + "styles": [ + "theme.css", + "blocks.css" + ], + "page_templates": [ + "home", + "about", + "contact", + "listing", + "category", + "product", + "article", + "brand", + "case", + "service", + "faq", + "standard", + "notfound" + ], + "blocks": [ + "hero", + "richtext", + "features", + "stats", + "collection", + "image_text", + "gallery", + "faq", + "cta", + "attributes", + "spacer" + ], + "responsive": true, + "auto_binding": true, + "professional_layout": true +} \ No newline at end of file diff --git a/themes/industrial/versions/1.0.0/page-presets.json b/themes/industrial/versions/1.0.0/page-presets.json new file mode 100644 index 0000000..8ae1aa6 --- /dev/null +++ b/themes/industrial/versions/1.0.0/page-presets.json @@ -0,0 +1,285 @@ +{ + "home": [ + { + "id": "0cdf18bf7f", + "type": "hero", + "visible": true, + "title": "工程质量,值得信赖", + "subtitle": "INDUSTRIAL ENGINEERING", + "description": "{{site.tagline}}", + "button": "查看产品", + "href": "/product/", + "align": "left" + }, + { + "id": "f846a2ff6b", + "type": "features", + "visible": true, + "title": "制造与服务能力", + "items": [ + { + "title": "专业服务", + "text": "从咨询到交付" + }, + { + "title": "品质保障", + "text": "严格的质量流程" + }, + { + "title": "全球网络", + "text": "连接不同市场" + } + ] + }, + { + "id": "ff4655d684", + "type": "collection", + "visible": true, + "title": "工业产品系列", + "subtitle": "PRODUCT SOLUTIONS", + "source": "product", + "limit": 6, + "columns": 3, + "button": "查看全部", + "href": "/product/" + }, + { + "id": "4355daaba0", + "type": "image_text", + "visible": true, + "title": "工程标准与服务流程", + "subtitle": "QUALITY FIRST", + "description": "展示业务流程和服务理念。", + "image": "", + "image_side": "right", + "button": "了解更多", + "href": "/about/" + }, + { + "id": "9e5056370d", + "type": "cta", + "visible": true, + "title": "获取专业产品方案", + "subtitle": "告诉我们您的需求,我们将提供适合的方案。", + "button": "联系我们", + "href": "/contact/" + } + ], + "about": [ + { + "id": "8ced1e0493", + "type": "image_text", + "visible": true, + "title": "认识我们", + "subtitle": "OUR APPROACH", + "description": "展示业务流程和服务理念。", + "image": "", + "image_side": "right", + "button": "了解更多", + "href": "/about/" + }, + { + "id": "52b6064084", + "type": "stats", + "visible": true, + "title": "我们的实力", + "items": [ + { + "value": "50,000+", + "label": "服务案例" + }, + { + "value": "70+", + "label": "市场覆盖" + }, + { + "value": "30+", + "label": "合作伙伴" + } + ] + }, + { + "id": "cf39bcb75d", + "type": "features", + "visible": true, + "title": "我们的优势", + "items": [ + { + "title": "专业服务", + "text": "从咨询到交付" + }, + { + "title": "品质保障", + "text": "严格的质量流程" + }, + { + "title": "全球网络", + "text": "连接不同市场" + } + ] + } + ], + "contact": [ + { + "id": "597f6ffb08", + "type": "richtext", + "visible": true, + "title": "联系我们", + "body": "

请填写企业联系方式与地址。

" + }, + { + "id": "8c02c61415", + "type": "cta", + "visible": true, + "title": "欢迎联系", + "subtitle": "告诉我们您的需求,我们将提供适合的方案。", + "button": "联系我们", + "href": "/contact/" + } + ], + "listing": [ + { + "id": "2c3106baf6", + "type": "collection", + "visible": true, + "title": "工业产品目录", + "subtitle": "为客户提供适合的解决方案", + "source": "product", + "limit": 24, + "columns": 3, + "button": "查看全部", + "href": "/product/" + } + ], + "product": [ + { + "id": "42925dc8e3", + "type": "image_text", + "visible": true, + "title": "产品介绍", + "subtitle": "OUR APPROACH", + "description": "{{page.summary}}", + "image": "{{page.cover}}", + "image_side": "right", + "button": "了解更多", + "href": "/about/" + }, + { + "id": "503266af35", + "type": "attributes", + "visible": true, + "title": "产品规格与参数" + }, + { + "id": "b2b1f5293c", + "type": "richtext", + "visible": true, + "title": "产品详情", + "body": "{{page.body}}" + }, + { + "id": "3837f66074", + "type": "cta", + "visible": true, + "title": "开启新的合作", + "subtitle": "告诉我们您的需求,我们将提供适合的方案。", + "button": "联系我们", + "href": "/contact/" + } + ], + "article": [ + { + "id": "c2573cb88d", + "type": "richtext", + "visible": true, + "title": "{{page.title}}", + "body": "{{page.body}}" + } + ], + "brand": [ + { + "id": "f43b705317", + "type": "image_text", + "visible": true, + "title": "{{page.title}}", + "subtitle": "OUR APPROACH", + "description": "{{page.summary}}", + "image": "{{page.cover}}", + "image_side": "right", + "button": "了解更多", + "href": "/about/" + }, + { + "id": "b3faf08170", + "type": "collection", + "visible": true, + "title": "品牌产品", + "subtitle": "为客户提供适合的解决方案", + "source": "product", + "limit": 6, + "columns": 3, + "button": "查看全部", + "href": "/product/" + } + ], + "standard": [ + { + "id": "7937550319", + "type": "richtext", + "visible": true, + "title": "{{page.title}}", + "body": "{{page.body}}" + } + ], + "notfound": [ + { + "id": "59383d762e", + "type": "cta", + "visible": true, + "title": "页面暂时无法访问", + "subtitle": "告诉我们您的需求,我们将提供适合的方案。", + "button": "返回首页", + "href": "/" + } + ], + "case": [ + { + "type": "image_text", + "title": "{{page.title}}", + "image": "{{page.cover}}", + "description": "{{page.summary}}" + }, + { + "type": "richtext", + "title": "项目概述", + "body": "{{page.body}}" + } + ], + "service": [ + { + "type": "richtext", + "title": "{{page.title}}", + "body": "{{page.body}}" + }, + { + "type": "cta", + "title": "了解更多", + "href": "/contact/" + } + ], + "faq": [ + { + "type": "faq", + "title": "常见问题", + "items": [] + } + ], + "category": [ + { + "type": "collection", + "title": "分类产品", + "source": "product", + "limit": 24 + } + ] +} \ No newline at end of file diff --git a/themes/industrial/versions/1.0.0/pages/about.html b/themes/industrial/versions/1.0.0/pages/about.html new file mode 100644 index 0000000..945be44 --- /dev/null +++ b/themes/industrial/versions/1.0.0/pages/about.html @@ -0,0 +1,2 @@ +
{{ 'GET IN TOUCH' if kind=='contact' else ('OUR STORY' if kind=='about' else 'EXPLORE') }}

{{page.title}}

{{page.summary}}

+
{{'CONNECT / CONTACT' if kind=='contact' else 'ABOUT / DISCOVER'}}

{{'让我们开始沟通。' if kind=='contact' else page.title}}

{% if kind=='contact' and footer_email %}{{footer_email}} ↗{% elif kind=='contact' %}

请先在网站管理后台填写正式联系邮箱。

{% endif %}
{{body_copy(content,page)}}{% if page.cover %}{{page.title}}{% endif %}
diff --git a/themes/industrial/versions/1.0.0/pages/article.html b/themes/industrial/versions/1.0.0/pages/article.html new file mode 100644 index 0000000..85626e4 --- /dev/null +++ b/themes/industrial/versions/1.0.0/pages/article.html @@ -0,0 +1 @@ +
LATEST NEWS / INSIGHTS

{{page.title}}

{{page.summary}}

{% if fields.get('publish_date') %}{% endif %}
diff --git a/themes/industrial/versions/1.0.0/pages/brand.html b/themes/industrial/versions/1.0.0/pages/brand.html new file mode 100644 index 0000000..a710280 --- /dev/null +++ b/themes/industrial/versions/1.0.0/pages/brand.html @@ -0,0 +1 @@ +
BRAND PROFILE / PARTNERS

{{page.title}}

{{page.summary}}

{{cover(page,'brand')}}
{{body_copy(content,page)}}{% if related %}{{section_title('PRODUCT LINE','相关产品','/products/')}}{{grid(related,'product')}}{% endif %}
diff --git a/themes/industrial/versions/1.0.0/pages/case.html b/themes/industrial/versions/1.0.0/pages/case.html new file mode 100644 index 0000000..79e4d72 --- /dev/null +++ b/themes/industrial/versions/1.0.0/pages/case.html @@ -0,0 +1 @@ +
SELECTED WORK / PROJECT

{{page.title}}

{{page.summary}}

{% if page.cover %}{{page.title}}{% endif %}
{% if fields.get('location') %}项目地点
{{fields.get('location')}}
{% endif %}{% if fields.get('industry') %}所属行业
{{fields.get('industry')}}
{% endif %}
{{body_copy(content,page)}}浏览更多案例 →
diff --git a/themes/industrial/versions/1.0.0/pages/category.html b/themes/industrial/versions/1.0.0/pages/category.html new file mode 100644 index 0000000..3918172 --- /dev/null +++ b/themes/industrial/versions/1.0.0/pages/category.html @@ -0,0 +1,2 @@ +
DISCOVER / {{'CATEGORY' if kind=='category' else 'COLLECTION'}}

{{page.title}}

{{page.summary or '查看此栏目的全部已发布内容。'}}

首页/{{page.title}}
+
{{ filters() }}
{{grid(listing,'catalog')}}
diff --git a/themes/industrial/versions/1.0.0/pages/contact.html b/themes/industrial/versions/1.0.0/pages/contact.html new file mode 100644 index 0000000..3a52a60 --- /dev/null +++ b/themes/industrial/versions/1.0.0/pages/contact.html @@ -0,0 +1,7 @@ +
{{ 'GET IN TOUCH' if kind=='contact' else ('OUR STORY' if kind=='about' else 'EXPLORE') }}

{{page.title}}

{{page.summary}}

+
{{'CONNECT / CONTACT' if kind=='contact' else 'ABOUT / DISCOVER'}}

{{'让我们开始沟通。' if kind=='contact' else page.title}}

{% if kind=='contact' and footer_email %}{{footer_email}} ↗{% elif kind=='contact' %}

请先在网站管理后台填写正式联系邮箱。

{% endif %}
{{body_copy(content,page)}}{% if page.cover %}{{page.title}}{% endif %}
+ +{% if form_endpoint %} +
CONTACT FORM

提交业务咨询

提交内容将直接发送到网站管理员配置的外部表单服务。

+

表单提交由外部服务处理,请确认其隐私政策及垃圾信息过滤设置。

+{% endif %} diff --git a/themes/industrial/versions/1.0.0/pages/faq.html b/themes/industrial/versions/1.0.0/pages/faq.html new file mode 100644 index 0000000..945be44 --- /dev/null +++ b/themes/industrial/versions/1.0.0/pages/faq.html @@ -0,0 +1,2 @@ +
{{ 'GET IN TOUCH' if kind=='contact' else ('OUR STORY' if kind=='about' else 'EXPLORE') }}

{{page.title}}

{{page.summary}}

+
{{'CONNECT / CONTACT' if kind=='contact' else 'ABOUT / DISCOVER'}}

{{'让我们开始沟通。' if kind=='contact' else page.title}}

{% if kind=='contact' and footer_email %}{{footer_email}} ↗{% elif kind=='contact' %}

请先在网站管理后台填写正式联系邮箱。

{% endif %}
{{body_copy(content,page)}}{% if page.cover %}{{page.title}}{% endif %}
diff --git a/themes/industrial/versions/1.0.0/pages/home.html b/themes/industrial/versions/1.0.0/pages/home.html new file mode 100644 index 0000000..145f1ab --- /dev/null +++ b/themes/industrial/versions/1.0.0/pages/home.html @@ -0,0 +1,4 @@ +
ENGINEERED FOR WHAT'S NEXT / 001

{{page.title if page.title not in (site.name,'首页','Home') else site.name}} BUILT TO PERFORM.

{{page.summary or site.tagline or '从材料到解决方案,一站式展示企业实力。'}}

{% if page.cover %}{{page.title}}{% else %}
FORM / FUNCTION
{% endif %}
+{% if categories %}
{{section_title('PRODUCT SYSTEMS','从类别开始探索','/products/')}}
{% for cat in categories[:6] %}0{{loop.index}}{{cat.name}}↗{% endfor %}
{% endif %} + +
APPLICATIONS & SUPPORT

专注可靠交付,服务多元项目。

{{site.tagline or '企业技术、材料和服务能力展示。'}}

服务与解决方案 ↗
diff --git a/themes/industrial/versions/1.0.0/pages/listing.html b/themes/industrial/versions/1.0.0/pages/listing.html new file mode 100644 index 0000000..3918172 --- /dev/null +++ b/themes/industrial/versions/1.0.0/pages/listing.html @@ -0,0 +1,2 @@ +
DISCOVER / {{'CATEGORY' if kind=='category' else 'COLLECTION'}}

{{page.title}}

{{page.summary or '查看此栏目的全部已发布内容。'}}

首页/{{page.title}}
+
{{ filters() }}
{{grid(listing,'catalog')}}
diff --git a/themes/industrial/versions/1.0.0/pages/notfound.html b/themes/industrial/versions/1.0.0/pages/notfound.html new file mode 100644 index 0000000..37e3f05 --- /dev/null +++ b/themes/industrial/versions/1.0.0/pages/notfound.html @@ -0,0 +1 @@ +
ERROR / 404

404

抱歉,你访问的页面不存在,或者已被移动。

返回首页 ↗
diff --git a/themes/industrial/versions/1.0.0/pages/product.html b/themes/industrial/versions/1.0.0/pages/product.html new file mode 100644 index 0000000..ba1fe4f --- /dev/null +++ b/themes/industrial/versions/1.0.0/pages/product.html @@ -0,0 +1,3 @@ +
首页/产品中心/{{page.title}}
+
PRODUCT DETAILS / {{fields.get('brand') or 'COLLECTION'}}

{{page.title}}

{{page.summary or '在此查看产品详细资料。'}}

{% if fields.get('brand') %}

品牌 · {{fields.get('brand')}}

{% endif %}咨询该产品 ↗
+
OVERVIEW

产品介绍

{{body_copy(content,page)}}
SPECIFICATIONS

规格参数

{% for row in specs %}
{{row.label}}{{row.value}}
{% else %}

尚未填写详细参数。

{% endfor %}
diff --git a/themes/industrial/versions/1.0.0/pages/search.html b/themes/industrial/versions/1.0.0/pages/search.html new file mode 100644 index 0000000..182a878 --- /dev/null +++ b/themes/industrial/versions/1.0.0/pages/search.html @@ -0,0 +1,2 @@ +
SEARCH / CONTENT

站内搜索

搜索本网站的产品、品牌、新闻和案例。

+
diff --git a/themes/industrial/versions/1.0.0/pages/service.html b/themes/industrial/versions/1.0.0/pages/service.html new file mode 100644 index 0000000..945be44 --- /dev/null +++ b/themes/industrial/versions/1.0.0/pages/service.html @@ -0,0 +1,2 @@ +
{{ 'GET IN TOUCH' if kind=='contact' else ('OUR STORY' if kind=='about' else 'EXPLORE') }}

{{page.title}}

{{page.summary}}

+
{{'CONNECT / CONTACT' if kind=='contact' else 'ABOUT / DISCOVER'}}

{{'让我们开始沟通。' if kind=='contact' else page.title}}

{% if kind=='contact' and footer_email %}{{footer_email}} ↗{% elif kind=='contact' %}

请先在网站管理后台填写正式联系邮箱。

{% endif %}
{{body_copy(content,page)}}{% if page.cover %}{{page.title}}{% endif %}
diff --git a/themes/industrial/versions/1.0.0/pages/standard.html b/themes/industrial/versions/1.0.0/pages/standard.html new file mode 100644 index 0000000..945be44 --- /dev/null +++ b/themes/industrial/versions/1.0.0/pages/standard.html @@ -0,0 +1,2 @@ +
{{ 'GET IN TOUCH' if kind=='contact' else ('OUR STORY' if kind=='about' else 'EXPLORE') }}

{{page.title}}

{{page.summary}}

+
{{'CONNECT / CONTACT' if kind=='contact' else 'ABOUT / DISCOVER'}}

{{'让我们开始沟通。' if kind=='contact' else page.title}}

{% if kind=='contact' and footer_email %}{{footer_email}} ↗{% elif kind=='contact' %}

请先在网站管理后台填写正式联系邮箱。

{% endif %}
{{body_copy(content,page)}}{% if page.cover %}{{page.title}}{% endif %}
diff --git a/themes/industrial/versions/1.0.0/pro.css b/themes/industrial/versions/1.0.0/pro.css new file mode 100644 index 0000000..217f02c --- /dev/null +++ b/themes/industrial/versions/1.0.0/pro.css @@ -0,0 +1,2 @@ +:root{--pro-text:#173a57;--pro-muted:#6b8191;--pro-border:#d8e3eb;--pro-background:#fff;--pro-surface:#edf3f7;--pro-accent:#16629a;--pro-hero:#0e3959;--pro-radius:2px;--pro-headings:Arial,sans-serif;--pro-wide:1290px} +.pro-header{background:#fff}.pro-logo-symbol{color:#14649b;font-size:38px}.pro-utility{background:#0d3a5a;color:#c6e2f1}.pro-header-cta{border-radius:2px;background:#0c5b94}.pro-industrial-hero{background:linear-gradient(90deg,#eff4f8 0%,#e6eef5 60%,#d7e5f0 100%);color:#123b59}.pro-industrial-hero .pro-hero-copy{padding-right:6%}.pro-industrial-hero .pro-hero-copy h1{font-size:clamp(45px,5.3vw,80px)}.pro-industrial-hero .pro-hero-copy h1 em{color:#186fa4}.pro-industrial-hero .pro-hero-art{border-radius:0;background:#0c466d}.pro-technical{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;gap:3%;overflow:hidden;background:repeating-linear-gradient(0deg,transparent,transparent 39px,#ffffff0b 40px),repeating-linear-gradient(90deg,transparent,transparent 39px,#ffffff0b 40px)}.pro-technical>div{width:21%;height:55%;border-radius:60px;border:27px solid #6ba6d4;transform:skewY(-13deg) rotate(-5deg);box-shadow:18px 17px 0 #14324a,0 0 0 9px #bde2fb inset,12px 22px 50px #081c30}.pro-technical>div:nth-child(2){height:72%;width:27%;border-width:34px}.pro-technical strong{position:absolute;bottom:24px;left:28px;color:#aed1e8;font-size:13px;letter-spacing:.17em}.pro-outline-button{color:#0f5988}.pro-category-band{background:#fff}.pro-category-tiles a{background:#eaf2f7;border-top:4px solid #2176aa;min-height:185px;transition:background .2s}.pro-category-tiles a:hover{background:#dce9f2}.pro-category-tiles strong{font-size:21px}.pro-featured{background:#edf3f7}.pro-card{background:#fff;border-radius:2px}.pro-card-cover{background:#d8e6ef}.pro-art-industrial,.pro-art-product{background:linear-gradient(135deg,#aecadf,#e1f1fb)}.pro-art-center{color:#45789b99}.pro-statement{background:#103e5e;color:#fff}.pro-statement .pro-eyebrow{color:#8dc8eb}.pro-statement p{color:#d3e1e9}.pro-statement .pro-text-link{color:#9cdaff}.pro-inner-hero{background:linear-gradient(115deg,#edf4f9 0%,#d9e9f4 100%)}.pro-inner-hero:after{content:'+';position:absolute;right:11%;top:6%;font-size:350px;font-weight:100;color:#15699b10;line-height:1}.pro-article-hero{background:#dceaf4}.pro-article-hero p{color:#537088}.pro-footer{background:#092c45}.pro-end-cta{background:#0a4772}.pro-end-cta a{background:#fff;color:#144b75!important}.pro-404 h1{color:#156d9d} diff --git a/themes/industrial/versions/1.0.0/pro.html b/themes/industrial/versions/1.0.0/pro.html new file mode 100644 index 0000000..c83ddf3 --- /dev/null +++ b/themes/industrial/versions/1.0.0/pro.html @@ -0,0 +1,10 @@ +{{ seo_title }}{% if canonical and not noindex %}{% endif %}{% if theme_overrides %}{% endif %}跳转至正文 +{% from '_professional_components.html' import card,cover,grid,section_title,filters,footer_links,body_copy with context %} +
ENGINEERING · MATERIALS · SUPPLYINDUSTRIAL SOLUTIONS
+
询价咨询 ↗
{% if categories %}{% endif %}
+{% if not designer_replace %}{% include 'industrial/pages/' ~ kind ~ '.html' %}{% endif %} + +{% if has_custom and kind!='notfound' %}
{{custom_blocks}}
{% else %}
{% endif %} +{% if kind not in ('notfound','search') %}
LET'S CONNECT

从这里开始下一步。

{{site.tagline or '了解更多产品、服务与合作信息。'}}

联系我们 ↗
{% endif %} +
+ \ No newline at end of file diff --git a/themes/industrial/versions/1.0.0/site.html b/themes/industrial/versions/1.0.0/site.html new file mode 100644 index 0000000..2333121 --- /dev/null +++ b/themes/industrial/versions/1.0.0/site.html @@ -0,0 +1,12 @@ +{{seo_title}}{% if canonical %}{% endif %} +
ENGINEERING · MANUFACTURING · SUPPLYGLOBAL INDUSTRIAL SOLUTIONS
+
▰{{site.name}}INDUSTRIAL SOLUTIONSREQUEST A QUOTE ↗
+{% if blueprint_mode %}{% elif mode=='home' %}
STRONGER CONNECTIONS. SMARTER SOLUTIONS.

{{page.title if page.title!=site.name else 'BUILT TO LAST.'}}

{{page.summary or site.tagline or 'Industrial excellence, quality products and dependable worldwide delivery.'}}

EXPLORE PRODUCTS ↗
01 / MATERIALS02 / ENGINEERING03 / SERVICE
{% if page.cover %}{{page.title}}{% else %}
INDUSTRY
IN MOTION
{% endif %}
+{% elif mode=='notfound' %}
ERROR

404 — Page not found

+{% else %}
{{type_names.get(page.type,'COMPANY')|upper}} / {{'PRODUCT CATALOG' if mode=='listing' else 'OUR EXPERTISE'}}

{{page.title}}

{{page.summary}}

{% endif %} +{% if blueprint_mode %}
{{blocks}}
{% else %}
{% if mode=='home' %}
01PRODUCT ENGINEERING
02GLOBAL SOURCING
03QUALITY ASSURANCE
{% endif %} +{% if mode in ['home','detail'] and (content or blocks) %}
{{blocks}}{{content}}
{% endif %} +{% if mode=='detail' and page.cover %}
{{page.title}}
{% endif %} +{% set cards=listing_items if mode=='listing' else entries|selectattr('type','equalto','product')|list if mode=='home' else [] %} +{% if cards %}
OUR PORTFOLIO

{{'Product Catalogue' if mode=='listing' else 'Explore Our Products'}}

{% if mode=='home' %}ALL PRODUCTS ↗{% endif %}
{% endif %}
+{% endif %}
© {{site.name}}DESIGNED TO DELIVER. BUILT WITH SITEFORGE.
diff --git a/themes/industrial/versions/1.0.0/theme.css b/themes/industrial/versions/1.0.0/theme.css new file mode 100644 index 0000000..7bd34c1 --- /dev/null +++ b/themes/industrial/versions/1.0.0/theme.css @@ -0,0 +1 @@ +:root{--blue:#0a3d5c;--sky:#daeaf2;--orange:#ef8f35;--text:#1d3441}*{box-sizing:border-box}body{margin:0;color:var(--text);background:#f8fafb;font-family:Inter,"Segoe UI",Arial,sans-serif;line-height:1.6}a{color:inherit;text-decoration:none}img{max-width:100%;object-fit:contain}.wrap{width:min(1300px,90%);margin:auto}.top{background:#153d56;color:#a7c5d5;font-size:10px;letter-spacing:2px}.top .wrap{display:flex;justify-content:space-between;padding:9px 0}header{background:white;border-bottom:1px solid #e0e8ea}.header-inner{min-height:94px;display:flex;justify-content:space-between;align-items:center;gap:20px}.brand{display:flex;align-items:center;gap:12px;font-size:21px;font-weight:800;letter-spacing:-.5px;line-height:1}.brand-icon{display:grid;place-items:center;width:43px;height:43px;background:var(--blue);color:white;font-size:26px}.brand small{display:block;font-size:10px;color:#718793;letter-spacing:2px;margin-top:9px;font-weight:500}nav{display:flex;gap:25px;font-size:13px;font-weight:650;flex-wrap:wrap}nav a:hover{color:#eb8725}.request{background:#e88c36;color:white;padding:15px 20px;font-size:11px;font-weight:800;white-space:nowrap;letter-spacing:.5px}.hero{background:linear-gradient(107deg,#073653 0%,#0e5270 51%,#14748c 100%);color:white;overflow:hidden}.hero-grid{display:grid;grid-template-columns:55% 45%;min-height:530px}.hero-text{padding:76px 25px 60px 0;position:relative;z-index:1}.kicker{font-size:11px;letter-spacing:2px;font-weight:750;color:#ce882e}.hero .kicker{color:#f5ac57}.kicker i{display:inline-block;width:22px;height:2px;background:#f5ac57;vertical-align:middle;margin-right:10px}.hero h1{font-size:clamp(48px,6vw,86px);letter-spacing:-3px;line-height:1.02;max-width:700px;margin:35px 0 26px}.hero p{max-width:470px;color:#c6e1e9;font-size:17px}.button{display:inline-flex;gap:55px;align-items:center;background:#ef9136;padding:18px 22px;font-size:12px;letter-spacing:1px;font-weight:800;margin-top:26px}.button b{font-size:18px}.hero-tags{display:flex;gap:22px;margin-top:60px;font-size:10px;color:#85bfcb;letter-spacing:1px}.hero-art{position:relative;display:grid;place-items:center;overflow:hidden;background:radial-gradient(circle at center,#2c94a6 2%,#11617b 45%,transparent 70%)}.hero-art:before,.hero-art:after{content:"";position:absolute;width:540px;height:540px;border:1px solid #ffffff25;transform:rotate(45deg)}.hero-art:after{width:380px;height:380px}.hero-art img{position:relative;z-index:3;width:100%;max-height:480px;filter:drop-shadow(0 20px 20px #002a3b55)}.hero-art>span{position:relative;z-index:3;color:#c6eaf0;font-size:25px;letter-spacing:8px;text-align:center;font-weight:300}.ring{position:absolute;width:430px;height:430px;border:28px solid #ffffff13;border-radius:50%;transform:rotateX(55deg) rotate(45deg)}.ring-2{width:290px;height:290px;border-color:#ffffff1d}.ring-3{width:150px;height:150px;border-color:#fba74b55}.subhero{padding:80px 0;background:linear-gradient(110deg,#073e5d,#0d6981);color:white}.subhero h1{font-size:clamp(40px,5.3vw,68px);line-height:1.1;margin:20px 0}.subhero p{color:#b8d6df}.main{padding:58px 0 100px}.metrics{display:grid;grid-template-columns:repeat(3,1fr);background:white;margin-top:-90px;position:relative;box-shadow:0 15px 45px #0b4f7515}.metrics>div{padding:25px 34px;display:flex;align-items:center;gap:18px;border-right:1px solid #eef3f5}.metrics strong{color:#dc7e2d;font-size:30px}.metrics span{letter-spacing:1px;font-size:11px;font-weight:800}.titlebar{margin:65px 0 28px;display:flex;justify-content:space-between;align-items:end}.titlebar h2{font-size:37px;margin:12px 0 0}.titlebar>a{color:#ba6d2f;font-size:12px;font-weight:800}.product-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:23px}.product{border:1px solid #e0e8ed;background:white}.product:hover{box-shadow:0 15px 35px #194b6518}.product-img{height:225px;display:flex;align-items:center;justify-content:center;background:linear-gradient(125deg,#e8f0f4,#cadce6);overflow:hidden}.product-img img{height:100%;width:100%;object-fit:contain}.technical-graphic{width:110px;height:110px;border:18px solid #96b8c8;border-radius:50%;position:relative;box-shadow:0 20px 0 #244d6120}.technical-graphic span{position:absolute;color:#e9fbff}.technical-graphic span:first-child{top:-56px;left:-50px}.technical-graphic span:nth-child(2){top:-56px;right:-50px}.technical-graphic span:nth-child(3){bottom:-55px;left:-45px}.technical-graphic span:nth-child(4){bottom:-55px;right:-45px}.product-info{padding:25px}.product-info>span{font-size:9px;color:#c17a38;letter-spacing:1.5px;font-weight:700}.product-info h3{font-size:21px;margin:8px 0}.product-info p{color:#6d8792;font-size:13px}.arrow{border-top:1px solid #e6eef1;margin-top:25px;padding-top:18px;display:flex;justify-content:space-between;font-size:11px;letter-spacing:1px;font-weight:800;color:#0c6788}.rich-content{margin:30px 0}.rich-content p{color:#617787}.section{padding:40px 0}.feature-grid,.stats{display:grid;grid-template-columns:repeat(3,1fr);gap:20px}.feature{background:white;padding:25px;border-top:4px solid #f09c52}.stat strong{display:block;font-size:40px}.stat span{color:#78909c}.cta{padding:50px;background:#0e5675;color:white}.detail-image img{max-height:560px}footer{background:#0f334a;color:white;padding:65px 0 25px}.footer-cols{display:flex;justify-content:space-between;gap:40px}.footer-cols h2{font-size:26px}.footer-cols p{color:#96b1bf}.bottom{border-top:1px solid #36596d;margin-top:65px;padding-top:22px;display:flex;justify-content:space-between;letter-spacing:1px;font-size:10px;color:#9cb9c7}@media(max-width:940px){.header-inner{flex-wrap:wrap;padding:16px 0}nav{order:3;width:100%}.request{display:none}.hero-grid{grid-template-columns:1fr}.hero-text{padding-right:0}.hero-art{min-height:300px}.metrics{margin-top:0}}@media(max-width:660px){.product-grid,.metrics,.feature-grid,.stats{grid-template-columns:1fr}.hero h1{font-size:51px}.hero-tags{flex-wrap:wrap}.footer-cols,.bottom{flex-direction:column}.top span:last-child{display:none}.subhero{padding:60px 0}} diff --git a/themes/industrial/visual/hero.svg b/themes/industrial/visual/hero.svg new file mode 100644 index 0000000..5c363ca --- /dev/null +++ b/themes/industrial/visual/hero.svg @@ -0,0 +1 @@ +MATERIAL SYSTEMS diff --git a/themes/minimal/manifest.json b/themes/minimal/manifest.json new file mode 100644 index 0000000..2136491 --- /dev/null +++ b/themes/minimal/manifest.json @@ -0,0 +1,54 @@ +{ + "id": "minimal", + "name": "极简创意", + "description": "商业主题体验升级:独立视觉素材、响应式排版、产品列表筛选、多级导航、可访问性与交付检查(需真实内容验收)", + "version": "1.1.0", + "style": "editorial", + "entry": "pro.html", + "styles": [ + "theme.css", + "blocks.css" + ], + "page_templates": [ + "home", + "about", + "contact", + "listing", + "category", + "product", + "article", + "brand", + "case", + "service", + "faq", + "standard", + "notfound" + ], + "blocks": [ + "hero", + "richtext", + "features", + "stats", + "collection", + "image_text", + "gallery", + "faq", + "cta", + "attributes", + "spacer" + ], + "responsive": true, + "auto_binding": true, + "professional_layout": true, + "commercial_ui": true, + "commercial_features": [ + "responsive-design", + "product-detail", + "brand-collections", + "gallery", + "content-facets", + "mega-navigation", + "contact-integration", + "readiness-audit" + ] +} \ No newline at end of file diff --git a/themes/minimal/page-presets.json b/themes/minimal/page-presets.json new file mode 100644 index 0000000..7f6af7a --- /dev/null +++ b/themes/minimal/page-presets.json @@ -0,0 +1,273 @@ +{ + "home": [ + { + "id": "612f6ea910", + "type": "hero", + "visible": true, + "title": "少即是多,体验至上", + "subtitle": "DESIGN THAT MATTERS", + "description": "{{site.tagline}}", + "button": "探索作品", + "href": "/product/", + "align": "left" + }, + { + "id": "a448a548e5", + "type": "image_text", + "visible": true, + "title": "设计不止于外观", + "subtitle": "OUR PHILOSOPHY", + "description": "展示业务流程和服务理念。", + "image": "", + "image_side": "right", + "button": "了解更多", + "href": "/about/" + }, + { + "id": "8a2f9a939b", + "type": "collection", + "visible": true, + "title": "精选项目", + "subtitle": "SELECTED WORK", + "source": "case", + "limit": 6, + "columns": 3, + "button": "查看全部", + "href": "/product/" + }, + { + "id": "e27cec0c11", + "type": "gallery", + "visible": true, + "title": "视觉记录", + "images": [], + "columns": 3 + }, + { + "id": "2bb8c1f33e", + "type": "cta", + "visible": true, + "title": "有一个新的想法?", + "subtitle": "告诉我们您的需求,我们将提供适合的方案。", + "button": "联系我们", + "href": "/contact/" + } + ], + "about": [ + { + "id": "35e7b20667", + "type": "image_text", + "visible": true, + "title": "认识我们", + "subtitle": "OUR APPROACH", + "description": "展示业务流程和服务理念。", + "image": "", + "image_side": "right", + "button": "了解更多", + "href": "/about/" + }, + { + "id": "61ff3a5f5c", + "type": "stats", + "visible": true, + "title": "我们的实力", + "items": [ + { + "value": "50,000+", + "label": "服务案例" + }, + { + "value": "70+", + "label": "市场覆盖" + }, + { + "value": "30+", + "label": "合作伙伴" + } + ] + }, + { + "id": "8bceb456e8", + "type": "features", + "visible": true, + "title": "我们的优势", + "items": [ + { + "title": "专业服务", + "text": "从咨询到交付" + }, + { + "title": "品质保障", + "text": "严格的质量流程" + }, + { + "title": "全球网络", + "text": "连接不同市场" + } + ] + } + ], + "contact": [ + { + "id": "a6a53010e6", + "type": "richtext", + "visible": true, + "title": "联系我们", + "body": "

请填写企业联系方式与地址。

" + }, + { + "id": "17485c5650", + "type": "cta", + "visible": true, + "title": "欢迎联系", + "subtitle": "告诉我们您的需求,我们将提供适合的方案。", + "button": "联系我们", + "href": "/contact/" + } + ], + "listing": [ + { + "id": "10c25d0a3c", + "type": "collection", + "visible": true, + "title": "全部作品", + "subtitle": "为客户提供适合的解决方案", + "source": "case", + "limit": 24, + "columns": 2, + "button": "查看全部", + "href": "/product/" + } + ], + "product": [ + { + "id": "3ee3cf11c4", + "type": "image_text", + "visible": true, + "title": "产品介绍", + "subtitle": "OUR APPROACH", + "description": "{{page.summary}}", + "image": "{{page.cover}}", + "image_side": "right", + "button": "了解更多", + "href": "/about/" + }, + { + "id": "04dee2c7f5", + "type": "attributes", + "visible": true, + "title": "产品规格与参数" + }, + { + "id": "8282854cb4", + "type": "richtext", + "visible": true, + "title": "产品详情", + "body": "{{page.body}}" + }, + { + "id": "7cbffd4a6b", + "type": "cta", + "visible": true, + "title": "开启新的合作", + "subtitle": "告诉我们您的需求,我们将提供适合的方案。", + "button": "联系我们", + "href": "/contact/" + } + ], + "article": [ + { + "id": "7b72e8bf3f", + "type": "richtext", + "visible": true, + "title": "{{page.title}}", + "body": "{{page.body}}" + } + ], + "brand": [ + { + "id": "6c6b62b1f7", + "type": "image_text", + "visible": true, + "title": "{{page.title}}", + "subtitle": "OUR APPROACH", + "description": "{{page.summary}}", + "image": "{{page.cover}}", + "image_side": "right", + "button": "了解更多", + "href": "/about/" + }, + { + "id": "1e8d347355", + "type": "collection", + "visible": true, + "title": "品牌产品", + "subtitle": "为客户提供适合的解决方案", + "source": "product", + "limit": 6, + "columns": 3, + "button": "查看全部", + "href": "/product/" + } + ], + "standard": [ + { + "id": "82c5bbcae8", + "type": "richtext", + "visible": true, + "title": "{{page.title}}", + "body": "{{page.body}}" + } + ], + "notfound": [ + { + "id": "d197054638", + "type": "cta", + "visible": true, + "title": "页面暂时无法访问", + "subtitle": "告诉我们您的需求,我们将提供适合的方案。", + "button": "返回首页", + "href": "/" + } + ], + "case": [ + { + "type": "image_text", + "title": "{{page.title}}", + "image": "{{page.cover}}", + "description": "{{page.summary}}" + }, + { + "type": "richtext", + "title": "项目概述", + "body": "{{page.body}}" + } + ], + "service": [ + { + "type": "richtext", + "title": "{{page.title}}", + "body": "{{page.body}}" + }, + { + "type": "cta", + "title": "了解更多", + "href": "/contact/" + } + ], + "faq": [ + { + "type": "faq", + "title": "常见问题", + "items": [] + } + ], + "category": [ + { + "type": "collection", + "title": "分类产品", + "source": "product", + "limit": 24 + } + ] +} \ No newline at end of file diff --git a/themes/minimal/pages/about.html b/themes/minimal/pages/about.html new file mode 100644 index 0000000..945be44 --- /dev/null +++ b/themes/minimal/pages/about.html @@ -0,0 +1,2 @@ +
{{ 'GET IN TOUCH' if kind=='contact' else ('OUR STORY' if kind=='about' else 'EXPLORE') }}

{{page.title}}

{{page.summary}}

+
{{'CONNECT / CONTACT' if kind=='contact' else 'ABOUT / DISCOVER'}}

{{'让我们开始沟通。' if kind=='contact' else page.title}}

{% if kind=='contact' and footer_email %}{{footer_email}} ↗{% elif kind=='contact' %}

请先在网站管理后台填写正式联系邮箱。

{% endif %}
{{body_copy(content,page)}}{% if page.cover %}{{page.title}}{% endif %}
diff --git a/themes/minimal/pages/article.html b/themes/minimal/pages/article.html new file mode 100644 index 0000000..85626e4 --- /dev/null +++ b/themes/minimal/pages/article.html @@ -0,0 +1 @@ +
LATEST NEWS / INSIGHTS

{{page.title}}

{{page.summary}}

{% if fields.get('publish_date') %}{% endif %}
diff --git a/themes/minimal/pages/brand.html b/themes/minimal/pages/brand.html new file mode 100644 index 0000000..a710280 --- /dev/null +++ b/themes/minimal/pages/brand.html @@ -0,0 +1 @@ +
BRAND PROFILE / PARTNERS

{{page.title}}

{{page.summary}}

{{cover(page,'brand')}}
{{body_copy(content,page)}}{% if related %}{{section_title('PRODUCT LINE','相关产品','/products/')}}{{grid(related,'product')}}{% endif %}
diff --git a/themes/minimal/pages/case.html b/themes/minimal/pages/case.html new file mode 100644 index 0000000..79e4d72 --- /dev/null +++ b/themes/minimal/pages/case.html @@ -0,0 +1 @@ +
SELECTED WORK / PROJECT

{{page.title}}

{{page.summary}}

{% if page.cover %}{{page.title}}{% endif %}
{% if fields.get('location') %}项目地点
{{fields.get('location')}}
{% endif %}{% if fields.get('industry') %}所属行业
{{fields.get('industry')}}
{% endif %}
{{body_copy(content,page)}}浏览更多案例 →
diff --git a/themes/minimal/pages/category.html b/themes/minimal/pages/category.html new file mode 100644 index 0000000..3918172 --- /dev/null +++ b/themes/minimal/pages/category.html @@ -0,0 +1,2 @@ +
DISCOVER / {{'CATEGORY' if kind=='category' else 'COLLECTION'}}

{{page.title}}

{{page.summary or '查看此栏目的全部已发布内容。'}}

首页/{{page.title}}
+
{{ filters() }}
{{grid(listing,'catalog')}}
diff --git a/themes/minimal/pages/contact.html b/themes/minimal/pages/contact.html new file mode 100644 index 0000000..3a52a60 --- /dev/null +++ b/themes/minimal/pages/contact.html @@ -0,0 +1,7 @@ +
{{ 'GET IN TOUCH' if kind=='contact' else ('OUR STORY' if kind=='about' else 'EXPLORE') }}

{{page.title}}

{{page.summary}}

+
{{'CONNECT / CONTACT' if kind=='contact' else 'ABOUT / DISCOVER'}}

{{'让我们开始沟通。' if kind=='contact' else page.title}}

{% if kind=='contact' and footer_email %}{{footer_email}} ↗{% elif kind=='contact' %}

请先在网站管理后台填写正式联系邮箱。

{% endif %}
{{body_copy(content,page)}}{% if page.cover %}{{page.title}}{% endif %}
+ +{% if form_endpoint %} +
CONTACT FORM

提交业务咨询

提交内容将直接发送到网站管理员配置的外部表单服务。

+

表单提交由外部服务处理,请确认其隐私政策及垃圾信息过滤设置。

+{% endif %} diff --git a/themes/minimal/pages/faq.html b/themes/minimal/pages/faq.html new file mode 100644 index 0000000..945be44 --- /dev/null +++ b/themes/minimal/pages/faq.html @@ -0,0 +1,2 @@ +
{{ 'GET IN TOUCH' if kind=='contact' else ('OUR STORY' if kind=='about' else 'EXPLORE') }}

{{page.title}}

{{page.summary}}

+
{{'CONNECT / CONTACT' if kind=='contact' else 'ABOUT / DISCOVER'}}

{{'让我们开始沟通。' if kind=='contact' else page.title}}

{% if kind=='contact' and footer_email %}{{footer_email}} ↗{% elif kind=='contact' %}

请先在网站管理后台填写正式联系邮箱。

{% endif %}
{{body_copy(content,page)}}{% if page.cover %}{{page.title}}{% endif %}
diff --git a/themes/minimal/pages/home.html b/themes/minimal/pages/home.html new file mode 100644 index 0000000..20e3da6 --- /dev/null +++ b/themes/minimal/pages/home.html @@ -0,0 +1,3 @@ +
INDEPENDENT / CREATIVE / CONSIDEREDNO. 001 — ESTABLISH A PRESENCE

{{page.title if page.title not in (site.name,'首页','Home') else site.name}}
MAKE IT MATTER.

{{page.summary or site.tagline or '简洁而清晰地呈现品牌与作品。'}}

EXPLORE WORK ↗
+ +
02 / ABOUT

少一点噪音,多一点重点。

{{site.tagline or '在这里介绍企业与品牌的核心价值。'}}

READ OUR STORY ↗
diff --git a/themes/minimal/pages/listing.html b/themes/minimal/pages/listing.html new file mode 100644 index 0000000..3918172 --- /dev/null +++ b/themes/minimal/pages/listing.html @@ -0,0 +1,2 @@ +
DISCOVER / {{'CATEGORY' if kind=='category' else 'COLLECTION'}}

{{page.title}}

{{page.summary or '查看此栏目的全部已发布内容。'}}

首页/{{page.title}}
+
{{ filters() }}
{{grid(listing,'catalog')}}
diff --git a/themes/minimal/pages/notfound.html b/themes/minimal/pages/notfound.html new file mode 100644 index 0000000..37e3f05 --- /dev/null +++ b/themes/minimal/pages/notfound.html @@ -0,0 +1 @@ +
ERROR / 404

404

抱歉,你访问的页面不存在,或者已被移动。

返回首页 ↗
diff --git a/themes/minimal/pages/product.html b/themes/minimal/pages/product.html new file mode 100644 index 0000000..ba1fe4f --- /dev/null +++ b/themes/minimal/pages/product.html @@ -0,0 +1,3 @@ +
首页/产品中心/{{page.title}}
+
PRODUCT DETAILS / {{fields.get('brand') or 'COLLECTION'}}

{{page.title}}

{{page.summary or '在此查看产品详细资料。'}}

{% if fields.get('brand') %}

品牌 · {{fields.get('brand')}}

{% endif %}咨询该产品 ↗
+
OVERVIEW

产品介绍

{{body_copy(content,page)}}
SPECIFICATIONS

规格参数

{% for row in specs %}
{{row.label}}{{row.value}}
{% else %}

尚未填写详细参数。

{% endfor %}
diff --git a/themes/minimal/pages/search.html b/themes/minimal/pages/search.html new file mode 100644 index 0000000..182a878 --- /dev/null +++ b/themes/minimal/pages/search.html @@ -0,0 +1,2 @@ +
SEARCH / CONTENT

站内搜索

搜索本网站的产品、品牌、新闻和案例。

+
diff --git a/themes/minimal/pages/service.html b/themes/minimal/pages/service.html new file mode 100644 index 0000000..945be44 --- /dev/null +++ b/themes/minimal/pages/service.html @@ -0,0 +1,2 @@ +
{{ 'GET IN TOUCH' if kind=='contact' else ('OUR STORY' if kind=='about' else 'EXPLORE') }}

{{page.title}}

{{page.summary}}

+
{{'CONNECT / CONTACT' if kind=='contact' else 'ABOUT / DISCOVER'}}

{{'让我们开始沟通。' if kind=='contact' else page.title}}

{% if kind=='contact' and footer_email %}{{footer_email}} ↗{% elif kind=='contact' %}

请先在网站管理后台填写正式联系邮箱。

{% endif %}
{{body_copy(content,page)}}{% if page.cover %}{{page.title}}{% endif %}
diff --git a/themes/minimal/pages/standard.html b/themes/minimal/pages/standard.html new file mode 100644 index 0000000..945be44 --- /dev/null +++ b/themes/minimal/pages/standard.html @@ -0,0 +1,2 @@ +
{{ 'GET IN TOUCH' if kind=='contact' else ('OUR STORY' if kind=='about' else 'EXPLORE') }}

{{page.title}}

{{page.summary}}

+
{{'CONNECT / CONTACT' if kind=='contact' else 'ABOUT / DISCOVER'}}

{{'让我们开始沟通。' if kind=='contact' else page.title}}

{% if kind=='contact' and footer_email %}{{footer_email}} ↗{% elif kind=='contact' %}

请先在网站管理后台填写正式联系邮箱。

{% endif %}
{{body_copy(content,page)}}{% if page.cover %}{{page.title}}{% endif %}
diff --git a/themes/minimal/pro.css b/themes/minimal/pro.css new file mode 100644 index 0000000..e9ec81c --- /dev/null +++ b/themes/minimal/pro.css @@ -0,0 +1,8 @@ +:root{--pro-text:#272722;--pro-muted:#797972;--pro-border:#d9d8d0;--pro-background:#fafaf7;--pro-surface:#f0f0eb;--pro-accent:#282923;--pro-hero:#272722;--pro-radius:0;--pro-headings:Georgia,Times New Roman,serif;--pro-font:Arial,sans-serif;--pro-wide:1380px} +.pro-header{background:#fafaf7;color:#292920}.pro-head-inner{min-height:96px}.pro-logo{min-width:200px}.pro-logo-symbol{font-family:Georgia,serif;color:#272722;font-size:40px;font-weight:400}.pro-logo-name{font-family:Georgia,serif;font-size:23px;font-weight:500}.pro-logo-name small{font-family:Arial,sans-serif;font-size:9px}.pro-navigation a{font-size:12px;font-weight:550}.pro-header-cta{border-radius:0;background:transparent;color:#262621!important;border-bottom:1px solid #222;padding:10px 0}.pro-minimal-hero{background:#f4f3ec;color:#252521;border-bottom:1px solid #d7d7cc}.pro-minimal-hero>.pro-wrap{padding-top:68px;padding-bottom:90px}.pro-minimal-meta{display:flex;justify-content:space-between;color:#727267;font-size:10px;letter-spacing:.16em}.pro-minimal-hero h1{font-weight:400;font-family:Georgia,serif;letter-spacing:-.072em;font-size:clamp(65px,10vw,153px);line-height:1.03;margin:100px 0 85px}.pro-minimal-hero h1 em{font-weight:400;font-style:italic;color:#8f8e82}.pro-minimal-bottom{display:flex;justify-content:space-between;align-items:end;gap:40px;border-top:1px solid #c9c8bf;padding-top:28px}.pro-minimal-bottom p{font-size:17px;max-width:450px;margin:0}.pro-minimal-bottom a{font-weight:750;font-size:11px;letter-spacing:.11em;white-space:nowrap}.pro-minimal-bottom a span{padding-left:25px}.pro-section-head h2{font-weight:400;font-size:clamp(40px,5vw,70px)}.pro-grid{gap:34px 20px}.pro-card{border:0;background:transparent;border-radius:0}.pro-card:hover{box-shadow:none}.pro-card-cover{height:310px;background:#eae9e1}.pro-card-body{padding:23px 0}.pro-card h3{font-family:Georgia,serif;font-size:27px;font-weight:400}.pro-art{background:linear-gradient(145deg,#deddd2,#ebeae5)}.pro-art-ring{border-color:#ffffff8a}.pro-art-center{color:#aba89b}.pro-card-link{border-color:#c7c6bf}.pro-statement{background:#e9e7dc}.pro-statement h2{font-family:Georgia,serif;font-weight:400;font-size:clamp(43px,6vw,83px)}.pro-statement .pro-text-link{color:#292a22}.pro-inner-hero{background:#f0efe8;border-bottom:1px solid #deddd3}.pro-inner-hero h1{font-family:Georgia,serif;font-weight:400;font-size:clamp(60px,9vw,117px)}.pro-article-hero{background:#eeede5}.pro-article-hero h1{font-family:Georgia,serif;font-weight:400}.pro-article-content{max-width:830px}.pro-product-summary h1{font-family:Georgia,serif;font-weight:400}.pro-footer{background:#242520}.pro-footer h2{font-family:Georgia,serif;font-size:40px;font-weight:400}.pro-end-cta{background:#282822}.pro-end-cta h2{font-family:Georgia,serif;font-weight:400}.pro-end-cta a{background:#fff;color:#242520!important}.pro-404 h1{font-family:Georgia,serif;font-weight:400;color:#8f8e82}.pro-dot{color:#a8a595} + +/* SiteForge V5 commercial signature */ +.pro-minimal-hero .pro-hero-art{background-image:url("./visual/hero.svg")!important;background-position:center;background-size:cover;} + +.pro-minimal-hero{background:#efeee8!important}.pro-minimal-hero .pro-wrap{background-image:url("./visual/hero.svg");background-repeat:no-repeat;background-size:48% auto;background-position:right 56%}.pro-minimal-hero h1{max-width:930px;position:relative;text-shadow:0 1px #ffffff70}.pro-card{border:0;border-bottom:1px solid #c8c7c0;background:transparent;border-radius:0}.pro-card-cover{background:#e7e5dc}.pro-art{background:linear-gradient(155deg,#e8e7e0,#c8c9bd);color:#585d55}.pro-card-body{padding-left:0;padding-right:0}.pro-featured{background:#faf9f5}.pro-section-head h2{font-weight:400}.pro-statement{border-block:1px solid #dddcd2;background:#f1efe7}.pro-footer{background:#22251e;color:#f6f4ec}.pro-button,.pro-header-cta{border-radius:0} +@media(max-width:700px){.pro-minimal-hero .pro-wrap{background-size:80% auto;background-position:center bottom;padding-bottom:250px}} diff --git a/themes/minimal/pro.html b/themes/minimal/pro.html new file mode 100644 index 0000000..8a82c8b --- /dev/null +++ b/themes/minimal/pro.html @@ -0,0 +1,9 @@ +{{ seo_title }}{% if canonical %}{% endif %}{% if canonical and not noindex %}{% endif %}{% if theme_overrides %}{% endif %}跳转至正文 +{% from '_professional_components.html' import card,cover,grid,section_title,filters,footer_links,body_copy with context %} +
LET'S TALK ↗
+{% if not designer_replace %}{% include 'minimal/pages/' ~ kind ~ '.html' %}{% endif %} + +{% if has_custom and kind!='notfound' %}
{{custom_blocks}}
{% else %}
{% endif %} +{% if kind not in ('notfound','search') %}
LET'S CONNECT

从这里开始下一步。

{{site.tagline or '了解更多产品、服务与合作信息。'}}

联系我们 ↗
{% endif %} +
+ \ No newline at end of file diff --git a/themes/minimal/site.html b/themes/minimal/site.html new file mode 100644 index 0000000..616c0c2 --- /dev/null +++ b/themes/minimal/site.html @@ -0,0 +1,8 @@ +{{seo_title}}{% if canonical %}{% endif %} +
{{site.name}}®CONTACT ↗
+{% if blueprint_mode %}{% elif mode=='home' %}
ESTABLISHED IN IDEAS.DESIGNED FOR CHANGE.

{{page.title if page.title!=site.name else 'Less, but better.'}}

{{page.summary or site.tagline or 'Thoughtfully made. Beautifully simple. Designed to make an impact.'}}

EXPLORE THE WORK ↗
{% if page.cover %}
{{page.title}}
{% else %}
SIMPLICITY
IS INTENTIONAL.
{% endif %}
+{% else %}
{{type_names.get(page.type,'THE STUDIO')|upper}} — {{'INDEX' if mode=='listing' else 'ABOUT'}}

{{page.title}}

{{page.summary}}

{% endif %} +{% if blueprint_mode %}
{{blocks}}
{% else %}
{% if mode in ['detail','home'] and (content or blocks) %}
{{blocks}}{{content}}
{% endif %}{% if mode=='detail' and page.cover %}
{{page.title}}
{% endif %} +{% set picks=listing_items if mode=='listing' else entries|selectattr('type','equalto','product')|list if mode=='home' else [] %} +{% if picks %}
01 / SELECTED WORK

{{'Our collection' if mode=='home' else page.title}}

{% endif %} +
{% endif %}
diff --git a/themes/minimal/theme.css b/themes/minimal/theme.css new file mode 100644 index 0000000..1547baa --- /dev/null +++ b/themes/minimal/theme.css @@ -0,0 +1 @@ +*{box-sizing:border-box}body{margin:0;color:#171717;background:#faf9f6;font:16px/1.6 Arial,Helvetica,sans-serif}a{color:inherit;text-decoration:none}.container{width:min(1260px,90%);margin:auto}.header{height:110px;display:flex;align-items:center;justify-content:space-between;gap:20px;border-bottom:1px solid #cccac4}.brand{font-family:Georgia,"Times New Roman",serif;font-size:30px;font-weight:bold;letter-spacing:-1px}.brand span{font-size:12px;vertical-align:top}.header nav{display:flex;gap:33px;font-size:12px;flex-wrap:wrap}.header nav a:hover{text-decoration:underline}.contact{font-size:12px;font-weight:bold;border-bottom:1px solid black;padding-bottom:5px}.hero{padding:48px 0 0}.topline{display:flex;justify-content:space-between;font-size:10px;letter-spacing:2.3px;font-weight:bold}.hero h1,.inner-hero h1{font-family:Georgia,"Times New Roman",serif;font-size:clamp(65px,9.1vw,155px);line-height:1;letter-spacing:-8px;font-weight:400;margin:85px 0 40px}.hero-bottom{display:flex;justify-content:space-between;align-items:end;margin:35px 0 64px;gap:25px}.hero-bottom p{font-size:18px;max-width:490px;margin:0}.hero-bottom a{font-size:12px;letter-spacing:1.5px;border-bottom:1px solid black;padding-bottom:8px}.hero-image{height:425px;background:#deddd4;overflow:hidden;position:relative;display:flex;align-items:center;justify-content:center}.hero-image img{height:100%;width:100%;object-fit:cover}.graphic{background:#cec8bc}.shape{position:absolute;border-radius:50%;background:linear-gradient(125deg,#ede9e1,#98918a);box-shadow:25px 30px 40px #59544c22}.shape.a{height:550px;width:550px;left:5%;top:-230px}.shape.b{width:280px;height:280px;right:15%;bottom:-120px;background:#989e97}.shape.c{width:160px;height:160px;right:40%;top:0;background:#ece9e3}.graphic>span{position:relative;font-size:25px;letter-spacing:7px;font-weight:500;line-height:1.5}.inner-hero{padding:80px 0;border-bottom:1px solid #d3d0ca}.inner-hero h1{font-size:clamp(48px,8vw,115px);margin:40px 0}.inner-hero p{font-size:18px;color:#777;max-width:660px}.content{padding:70px 0 130px}.list-head{display:grid;grid-template-columns:1fr 2fr;align-items:center;margin:30px 0 40px;border-bottom:1px solid black}.list-head span{font-size:10px;letter-spacing:2px}.list-head h2{font-family:Georgia,serif;font-weight:400;font-size:54px;letter-spacing:-2px}.list{display:grid;grid-template-columns:repeat(2,1fr);gap:80px 35px}.item{display:block}.item:hover h3{text-decoration:underline}.thumb{height:290px;background:#e3e1d8;display:grid;place-items:center}.thumb img{width:100%;height:100%;object-fit:contain}.thumb span{font-family:Georgia,serif;font-size:140px;color:#c0bcb2}.info{padding:25px 0}.info>span{font-size:10px;letter-spacing:1.5px}.info h3{font-family:Georgia,serif;font-size:36px;font-weight:400;margin:12px 0}.info p{color:#666;max-width:400px}.info b{font-size:11px;letter-spacing:1px}.rich-content{margin:25px 0}.section{padding:45px 0}.block-hero{padding:50px 0}.block-hero h1{font-family:Georgia,serif;font-size:60px;font-weight:400}.feature-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:30px}.feature{border-top:1px solid #8f8a84;padding-top:20px}.feature-mark{font-size:12px}.stats{display:grid;grid-template-columns:repeat(4,1fr)}.stat strong{font-family:Georgia,serif;display:block;font-size:47px;font-weight:400}.stat span{font-size:11px;letter-spacing:1px}.cta{padding:55px;background:#e4e0d8}.cta h2{font-family:Georgia,serif;font-weight:400;font-size:44px}.btn{display:inline-block;background:#222;color:white;padding:13px 20px;font-size:12px}.detail-img img{max-width:100%;max-height:550px}footer{border-top:1px solid #b9b5ae;padding:70px 0 25px}.footer-lead{font-family:Georgia,serif;font-size:clamp(45px,6vw,90px);line-height:1.05;letter-spacing:-4px}.footer-lead span{color:#909084}.footer-bottom{display:flex;justify-content:space-between;border-top:1px solid #d5d1c9;margin-top:90px;padding-top:25px;font-size:11px;color:#777}@media(max-width:850px){.header{height:auto;padding:26px 0;flex-wrap:wrap}.header nav{order:3;width:100%}.hero h1{letter-spacing:-4px;margin-top:60px}.hero-image{height:320px}.list{gap:35px}.list-head{grid-template-columns:1fr}.footer-lead{letter-spacing:-2px}}@media(max-width:560px){.list,.feature-grid,.stats{grid-template-columns:1fr}.hero h1{font-size:67px}.hero-bottom,.topline,.footer-bottom{flex-direction:column;align-items:start;gap:15px}.hero-image{height:240px}.list-head h2{font-size:40px}.inner-hero{padding:50px 0}} diff --git a/themes/minimal/versions/0.3.0/manifest.json b/themes/minimal/versions/0.3.0/manifest.json new file mode 100644 index 0000000..6045488 --- /dev/null +++ b/themes/minimal/versions/0.3.0/manifest.json @@ -0,0 +1,38 @@ +{ + "id": "minimal", + "name": "极简创意", + "description": "奶油白和纯黑,编辑式排版与作品展示", + "version": "0.3.0", + "style": "editorial", + "entry": "site.html", + "styles": [ + "theme.css", + "blocks.css" + ], + "page_templates": [ + "home", + "about", + "contact", + "listing", + "product", + "article", + "brand", + "standard", + "notfound" + ], + "blocks": [ + "hero", + "richtext", + "features", + "stats", + "collection", + "image_text", + "gallery", + "faq", + "cta", + "attributes", + "spacer" + ], + "responsive": true, + "auto_binding": true +} \ No newline at end of file diff --git a/themes/minimal/versions/0.3.0/page-presets.json b/themes/minimal/versions/0.3.0/page-presets.json new file mode 100644 index 0000000..5584ac5 --- /dev/null +++ b/themes/minimal/versions/0.3.0/page-presets.json @@ -0,0 +1,233 @@ +{ + "home": [ + { + "id": "612f6ea910", + "type": "hero", + "visible": true, + "title": "少即是多,体验至上", + "subtitle": "DESIGN THAT MATTERS", + "description": "{{site.tagline}}", + "button": "探索作品", + "href": "/product/", + "align": "left" + }, + { + "id": "a448a548e5", + "type": "image_text", + "visible": true, + "title": "设计不止于外观", + "subtitle": "OUR PHILOSOPHY", + "description": "展示业务流程和服务理念。", + "image": "", + "image_side": "right", + "button": "了解更多", + "href": "/about/" + }, + { + "id": "8a2f9a939b", + "type": "collection", + "visible": true, + "title": "精选项目", + "subtitle": "SELECTED WORK", + "source": "case", + "limit": 6, + "columns": 3, + "button": "查看全部", + "href": "/product/" + }, + { + "id": "e27cec0c11", + "type": "gallery", + "visible": true, + "title": "视觉记录", + "images": [], + "columns": 3 + }, + { + "id": "2bb8c1f33e", + "type": "cta", + "visible": true, + "title": "有一个新的想法?", + "subtitle": "告诉我们您的需求,我们将提供适合的方案。", + "button": "联系我们", + "href": "/contact/" + } + ], + "about": [ + { + "id": "35e7b20667", + "type": "image_text", + "visible": true, + "title": "认识我们", + "subtitle": "OUR APPROACH", + "description": "展示业务流程和服务理念。", + "image": "", + "image_side": "right", + "button": "了解更多", + "href": "/about/" + }, + { + "id": "61ff3a5f5c", + "type": "stats", + "visible": true, + "title": "我们的实力", + "items": [ + { + "value": "50,000+", + "label": "服务案例" + }, + { + "value": "70+", + "label": "市场覆盖" + }, + { + "value": "30+", + "label": "合作伙伴" + } + ] + }, + { + "id": "8bceb456e8", + "type": "features", + "visible": true, + "title": "我们的优势", + "items": [ + { + "title": "专业服务", + "text": "从咨询到交付" + }, + { + "title": "品质保障", + "text": "严格的质量流程" + }, + { + "title": "全球网络", + "text": "连接不同市场" + } + ] + } + ], + "contact": [ + { + "id": "a6a53010e6", + "type": "richtext", + "visible": true, + "title": "联系我们", + "body": "

请填写企业联系方式与地址。

" + }, + { + "id": "17485c5650", + "type": "cta", + "visible": true, + "title": "欢迎联系", + "subtitle": "告诉我们您的需求,我们将提供适合的方案。", + "button": "联系我们", + "href": "/contact/" + } + ], + "listing": [ + { + "id": "10c25d0a3c", + "type": "collection", + "visible": true, + "title": "全部作品", + "subtitle": "为客户提供适合的解决方案", + "source": "case", + "limit": 24, + "columns": 2, + "button": "查看全部", + "href": "/product/" + } + ], + "product": [ + { + "id": "3ee3cf11c4", + "type": "image_text", + "visible": true, + "title": "产品介绍", + "subtitle": "OUR APPROACH", + "description": "{{page.summary}}", + "image": "{{page.cover}}", + "image_side": "right", + "button": "了解更多", + "href": "/about/" + }, + { + "id": "04dee2c7f5", + "type": "attributes", + "visible": true, + "title": "产品规格与参数" + }, + { + "id": "8282854cb4", + "type": "richtext", + "visible": true, + "title": "产品详情", + "body": "{{page.body}}" + }, + { + "id": "7cbffd4a6b", + "type": "cta", + "visible": true, + "title": "开启新的合作", + "subtitle": "告诉我们您的需求,我们将提供适合的方案。", + "button": "联系我们", + "href": "/contact/" + } + ], + "article": [ + { + "id": "7b72e8bf3f", + "type": "richtext", + "visible": true, + "title": "{{page.title}}", + "body": "{{page.body}}" + } + ], + "brand": [ + { + "id": "6c6b62b1f7", + "type": "image_text", + "visible": true, + "title": "{{page.title}}", + "subtitle": "OUR APPROACH", + "description": "{{page.summary}}", + "image": "{{page.cover}}", + "image_side": "right", + "button": "了解更多", + "href": "/about/" + }, + { + "id": "1e8d347355", + "type": "collection", + "visible": true, + "title": "品牌产品", + "subtitle": "为客户提供适合的解决方案", + "source": "product", + "limit": 6, + "columns": 3, + "button": "查看全部", + "href": "/product/" + } + ], + "standard": [ + { + "id": "82c5bbcae8", + "type": "richtext", + "visible": true, + "title": "{{page.title}}", + "body": "{{page.body}}" + } + ], + "notfound": [ + { + "id": "d197054638", + "type": "cta", + "visible": true, + "title": "页面暂时无法访问", + "subtitle": "告诉我们您的需求,我们将提供适合的方案。", + "button": "返回首页", + "href": "/" + } + ] +} \ No newline at end of file diff --git a/themes/minimal/versions/0.3.0/site.html b/themes/minimal/versions/0.3.0/site.html new file mode 100644 index 0000000..616c0c2 --- /dev/null +++ b/themes/minimal/versions/0.3.0/site.html @@ -0,0 +1,8 @@ +{{seo_title}}{% if canonical %}{% endif %} +
{{site.name}}®CONTACT ↗
+{% if blueprint_mode %}{% elif mode=='home' %}
ESTABLISHED IN IDEAS.DESIGNED FOR CHANGE.

{{page.title if page.title!=site.name else 'Less, but better.'}}

{{page.summary or site.tagline or 'Thoughtfully made. Beautifully simple. Designed to make an impact.'}}

EXPLORE THE WORK ↗
{% if page.cover %}
{{page.title}}
{% else %}
SIMPLICITY
IS INTENTIONAL.
{% endif %}
+{% else %}
{{type_names.get(page.type,'THE STUDIO')|upper}} — {{'INDEX' if mode=='listing' else 'ABOUT'}}

{{page.title}}

{{page.summary}}

{% endif %} +{% if blueprint_mode %}
{{blocks}}
{% else %}
{% if mode in ['detail','home'] and (content or blocks) %}
{{blocks}}{{content}}
{% endif %}{% if mode=='detail' and page.cover %}
{{page.title}}
{% endif %} +{% set picks=listing_items if mode=='listing' else entries|selectattr('type','equalto','product')|list if mode=='home' else [] %} +{% if picks %}
01 / SELECTED WORK

{{'Our collection' if mode=='home' else page.title}}

{% endif %} +
{% endif %}
diff --git a/themes/minimal/versions/0.3.0/theme.css b/themes/minimal/versions/0.3.0/theme.css new file mode 100644 index 0000000..1547baa --- /dev/null +++ b/themes/minimal/versions/0.3.0/theme.css @@ -0,0 +1 @@ +*{box-sizing:border-box}body{margin:0;color:#171717;background:#faf9f6;font:16px/1.6 Arial,Helvetica,sans-serif}a{color:inherit;text-decoration:none}.container{width:min(1260px,90%);margin:auto}.header{height:110px;display:flex;align-items:center;justify-content:space-between;gap:20px;border-bottom:1px solid #cccac4}.brand{font-family:Georgia,"Times New Roman",serif;font-size:30px;font-weight:bold;letter-spacing:-1px}.brand span{font-size:12px;vertical-align:top}.header nav{display:flex;gap:33px;font-size:12px;flex-wrap:wrap}.header nav a:hover{text-decoration:underline}.contact{font-size:12px;font-weight:bold;border-bottom:1px solid black;padding-bottom:5px}.hero{padding:48px 0 0}.topline{display:flex;justify-content:space-between;font-size:10px;letter-spacing:2.3px;font-weight:bold}.hero h1,.inner-hero h1{font-family:Georgia,"Times New Roman",serif;font-size:clamp(65px,9.1vw,155px);line-height:1;letter-spacing:-8px;font-weight:400;margin:85px 0 40px}.hero-bottom{display:flex;justify-content:space-between;align-items:end;margin:35px 0 64px;gap:25px}.hero-bottom p{font-size:18px;max-width:490px;margin:0}.hero-bottom a{font-size:12px;letter-spacing:1.5px;border-bottom:1px solid black;padding-bottom:8px}.hero-image{height:425px;background:#deddd4;overflow:hidden;position:relative;display:flex;align-items:center;justify-content:center}.hero-image img{height:100%;width:100%;object-fit:cover}.graphic{background:#cec8bc}.shape{position:absolute;border-radius:50%;background:linear-gradient(125deg,#ede9e1,#98918a);box-shadow:25px 30px 40px #59544c22}.shape.a{height:550px;width:550px;left:5%;top:-230px}.shape.b{width:280px;height:280px;right:15%;bottom:-120px;background:#989e97}.shape.c{width:160px;height:160px;right:40%;top:0;background:#ece9e3}.graphic>span{position:relative;font-size:25px;letter-spacing:7px;font-weight:500;line-height:1.5}.inner-hero{padding:80px 0;border-bottom:1px solid #d3d0ca}.inner-hero h1{font-size:clamp(48px,8vw,115px);margin:40px 0}.inner-hero p{font-size:18px;color:#777;max-width:660px}.content{padding:70px 0 130px}.list-head{display:grid;grid-template-columns:1fr 2fr;align-items:center;margin:30px 0 40px;border-bottom:1px solid black}.list-head span{font-size:10px;letter-spacing:2px}.list-head h2{font-family:Georgia,serif;font-weight:400;font-size:54px;letter-spacing:-2px}.list{display:grid;grid-template-columns:repeat(2,1fr);gap:80px 35px}.item{display:block}.item:hover h3{text-decoration:underline}.thumb{height:290px;background:#e3e1d8;display:grid;place-items:center}.thumb img{width:100%;height:100%;object-fit:contain}.thumb span{font-family:Georgia,serif;font-size:140px;color:#c0bcb2}.info{padding:25px 0}.info>span{font-size:10px;letter-spacing:1.5px}.info h3{font-family:Georgia,serif;font-size:36px;font-weight:400;margin:12px 0}.info p{color:#666;max-width:400px}.info b{font-size:11px;letter-spacing:1px}.rich-content{margin:25px 0}.section{padding:45px 0}.block-hero{padding:50px 0}.block-hero h1{font-family:Georgia,serif;font-size:60px;font-weight:400}.feature-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:30px}.feature{border-top:1px solid #8f8a84;padding-top:20px}.feature-mark{font-size:12px}.stats{display:grid;grid-template-columns:repeat(4,1fr)}.stat strong{font-family:Georgia,serif;display:block;font-size:47px;font-weight:400}.stat span{font-size:11px;letter-spacing:1px}.cta{padding:55px;background:#e4e0d8}.cta h2{font-family:Georgia,serif;font-weight:400;font-size:44px}.btn{display:inline-block;background:#222;color:white;padding:13px 20px;font-size:12px}.detail-img img{max-width:100%;max-height:550px}footer{border-top:1px solid #b9b5ae;padding:70px 0 25px}.footer-lead{font-family:Georgia,serif;font-size:clamp(45px,6vw,90px);line-height:1.05;letter-spacing:-4px}.footer-lead span{color:#909084}.footer-bottom{display:flex;justify-content:space-between;border-top:1px solid #d5d1c9;margin-top:90px;padding-top:25px;font-size:11px;color:#777}@media(max-width:850px){.header{height:auto;padding:26px 0;flex-wrap:wrap}.header nav{order:3;width:100%}.hero h1{letter-spacing:-4px;margin-top:60px}.hero-image{height:320px}.list{gap:35px}.list-head{grid-template-columns:1fr}.footer-lead{letter-spacing:-2px}}@media(max-width:560px){.list,.feature-grid,.stats{grid-template-columns:1fr}.hero h1{font-size:67px}.hero-bottom,.topline,.footer-bottom{flex-direction:column;align-items:start;gap:15px}.hero-image{height:240px}.list-head h2{font-size:40px}.inner-hero{padding:50px 0}} diff --git a/themes/minimal/versions/1.0.0/manifest.json b/themes/minimal/versions/1.0.0/manifest.json new file mode 100644 index 0000000..8debf05 --- /dev/null +++ b/themes/minimal/versions/1.0.0/manifest.json @@ -0,0 +1,43 @@ +{ + "id": "minimal", + "name": "极简创意", + "description": "奶油白和纯黑,编辑式排版与作品展示;V3 专业全页模板、分类/详情/导航和搜索", + "version": "1.0.0", + "style": "editorial", + "entry": "pro.html", + "styles": [ + "theme.css", + "blocks.css" + ], + "page_templates": [ + "home", + "about", + "contact", + "listing", + "category", + "product", + "article", + "brand", + "case", + "service", + "faq", + "standard", + "notfound" + ], + "blocks": [ + "hero", + "richtext", + "features", + "stats", + "collection", + "image_text", + "gallery", + "faq", + "cta", + "attributes", + "spacer" + ], + "responsive": true, + "auto_binding": true, + "professional_layout": true +} \ No newline at end of file diff --git a/themes/minimal/versions/1.0.0/page-presets.json b/themes/minimal/versions/1.0.0/page-presets.json new file mode 100644 index 0000000..7f6af7a --- /dev/null +++ b/themes/minimal/versions/1.0.0/page-presets.json @@ -0,0 +1,273 @@ +{ + "home": [ + { + "id": "612f6ea910", + "type": "hero", + "visible": true, + "title": "少即是多,体验至上", + "subtitle": "DESIGN THAT MATTERS", + "description": "{{site.tagline}}", + "button": "探索作品", + "href": "/product/", + "align": "left" + }, + { + "id": "a448a548e5", + "type": "image_text", + "visible": true, + "title": "设计不止于外观", + "subtitle": "OUR PHILOSOPHY", + "description": "展示业务流程和服务理念。", + "image": "", + "image_side": "right", + "button": "了解更多", + "href": "/about/" + }, + { + "id": "8a2f9a939b", + "type": "collection", + "visible": true, + "title": "精选项目", + "subtitle": "SELECTED WORK", + "source": "case", + "limit": 6, + "columns": 3, + "button": "查看全部", + "href": "/product/" + }, + { + "id": "e27cec0c11", + "type": "gallery", + "visible": true, + "title": "视觉记录", + "images": [], + "columns": 3 + }, + { + "id": "2bb8c1f33e", + "type": "cta", + "visible": true, + "title": "有一个新的想法?", + "subtitle": "告诉我们您的需求,我们将提供适合的方案。", + "button": "联系我们", + "href": "/contact/" + } + ], + "about": [ + { + "id": "35e7b20667", + "type": "image_text", + "visible": true, + "title": "认识我们", + "subtitle": "OUR APPROACH", + "description": "展示业务流程和服务理念。", + "image": "", + "image_side": "right", + "button": "了解更多", + "href": "/about/" + }, + { + "id": "61ff3a5f5c", + "type": "stats", + "visible": true, + "title": "我们的实力", + "items": [ + { + "value": "50,000+", + "label": "服务案例" + }, + { + "value": "70+", + "label": "市场覆盖" + }, + { + "value": "30+", + "label": "合作伙伴" + } + ] + }, + { + "id": "8bceb456e8", + "type": "features", + "visible": true, + "title": "我们的优势", + "items": [ + { + "title": "专业服务", + "text": "从咨询到交付" + }, + { + "title": "品质保障", + "text": "严格的质量流程" + }, + { + "title": "全球网络", + "text": "连接不同市场" + } + ] + } + ], + "contact": [ + { + "id": "a6a53010e6", + "type": "richtext", + "visible": true, + "title": "联系我们", + "body": "

请填写企业联系方式与地址。

" + }, + { + "id": "17485c5650", + "type": "cta", + "visible": true, + "title": "欢迎联系", + "subtitle": "告诉我们您的需求,我们将提供适合的方案。", + "button": "联系我们", + "href": "/contact/" + } + ], + "listing": [ + { + "id": "10c25d0a3c", + "type": "collection", + "visible": true, + "title": "全部作品", + "subtitle": "为客户提供适合的解决方案", + "source": "case", + "limit": 24, + "columns": 2, + "button": "查看全部", + "href": "/product/" + } + ], + "product": [ + { + "id": "3ee3cf11c4", + "type": "image_text", + "visible": true, + "title": "产品介绍", + "subtitle": "OUR APPROACH", + "description": "{{page.summary}}", + "image": "{{page.cover}}", + "image_side": "right", + "button": "了解更多", + "href": "/about/" + }, + { + "id": "04dee2c7f5", + "type": "attributes", + "visible": true, + "title": "产品规格与参数" + }, + { + "id": "8282854cb4", + "type": "richtext", + "visible": true, + "title": "产品详情", + "body": "{{page.body}}" + }, + { + "id": "7cbffd4a6b", + "type": "cta", + "visible": true, + "title": "开启新的合作", + "subtitle": "告诉我们您的需求,我们将提供适合的方案。", + "button": "联系我们", + "href": "/contact/" + } + ], + "article": [ + { + "id": "7b72e8bf3f", + "type": "richtext", + "visible": true, + "title": "{{page.title}}", + "body": "{{page.body}}" + } + ], + "brand": [ + { + "id": "6c6b62b1f7", + "type": "image_text", + "visible": true, + "title": "{{page.title}}", + "subtitle": "OUR APPROACH", + "description": "{{page.summary}}", + "image": "{{page.cover}}", + "image_side": "right", + "button": "了解更多", + "href": "/about/" + }, + { + "id": "1e8d347355", + "type": "collection", + "visible": true, + "title": "品牌产品", + "subtitle": "为客户提供适合的解决方案", + "source": "product", + "limit": 6, + "columns": 3, + "button": "查看全部", + "href": "/product/" + } + ], + "standard": [ + { + "id": "82c5bbcae8", + "type": "richtext", + "visible": true, + "title": "{{page.title}}", + "body": "{{page.body}}" + } + ], + "notfound": [ + { + "id": "d197054638", + "type": "cta", + "visible": true, + "title": "页面暂时无法访问", + "subtitle": "告诉我们您的需求,我们将提供适合的方案。", + "button": "返回首页", + "href": "/" + } + ], + "case": [ + { + "type": "image_text", + "title": "{{page.title}}", + "image": "{{page.cover}}", + "description": "{{page.summary}}" + }, + { + "type": "richtext", + "title": "项目概述", + "body": "{{page.body}}" + } + ], + "service": [ + { + "type": "richtext", + "title": "{{page.title}}", + "body": "{{page.body}}" + }, + { + "type": "cta", + "title": "了解更多", + "href": "/contact/" + } + ], + "faq": [ + { + "type": "faq", + "title": "常见问题", + "items": [] + } + ], + "category": [ + { + "type": "collection", + "title": "分类产品", + "source": "product", + "limit": 24 + } + ] +} \ No newline at end of file diff --git a/themes/minimal/versions/1.0.0/pages/about.html b/themes/minimal/versions/1.0.0/pages/about.html new file mode 100644 index 0000000..945be44 --- /dev/null +++ b/themes/minimal/versions/1.0.0/pages/about.html @@ -0,0 +1,2 @@ +
{{ 'GET IN TOUCH' if kind=='contact' else ('OUR STORY' if kind=='about' else 'EXPLORE') }}

{{page.title}}

{{page.summary}}

+
{{'CONNECT / CONTACT' if kind=='contact' else 'ABOUT / DISCOVER'}}

{{'让我们开始沟通。' if kind=='contact' else page.title}}

{% if kind=='contact' and footer_email %}{{footer_email}} ↗{% elif kind=='contact' %}

请先在网站管理后台填写正式联系邮箱。

{% endif %}
{{body_copy(content,page)}}{% if page.cover %}{{page.title}}{% endif %}
diff --git a/themes/minimal/versions/1.0.0/pages/article.html b/themes/minimal/versions/1.0.0/pages/article.html new file mode 100644 index 0000000..85626e4 --- /dev/null +++ b/themes/minimal/versions/1.0.0/pages/article.html @@ -0,0 +1 @@ +
LATEST NEWS / INSIGHTS

{{page.title}}

{{page.summary}}

{% if fields.get('publish_date') %}{% endif %}
diff --git a/themes/minimal/versions/1.0.0/pages/brand.html b/themes/minimal/versions/1.0.0/pages/brand.html new file mode 100644 index 0000000..a710280 --- /dev/null +++ b/themes/minimal/versions/1.0.0/pages/brand.html @@ -0,0 +1 @@ +
BRAND PROFILE / PARTNERS

{{page.title}}

{{page.summary}}

{{cover(page,'brand')}}
{{body_copy(content,page)}}{% if related %}{{section_title('PRODUCT LINE','相关产品','/products/')}}{{grid(related,'product')}}{% endif %}
diff --git a/themes/minimal/versions/1.0.0/pages/case.html b/themes/minimal/versions/1.0.0/pages/case.html new file mode 100644 index 0000000..79e4d72 --- /dev/null +++ b/themes/minimal/versions/1.0.0/pages/case.html @@ -0,0 +1 @@ +
SELECTED WORK / PROJECT

{{page.title}}

{{page.summary}}

{% if page.cover %}{{page.title}}{% endif %}
{% if fields.get('location') %}项目地点
{{fields.get('location')}}
{% endif %}{% if fields.get('industry') %}所属行业
{{fields.get('industry')}}
{% endif %}
{{body_copy(content,page)}}浏览更多案例 →
diff --git a/themes/minimal/versions/1.0.0/pages/category.html b/themes/minimal/versions/1.0.0/pages/category.html new file mode 100644 index 0000000..3918172 --- /dev/null +++ b/themes/minimal/versions/1.0.0/pages/category.html @@ -0,0 +1,2 @@ +
DISCOVER / {{'CATEGORY' if kind=='category' else 'COLLECTION'}}

{{page.title}}

{{page.summary or '查看此栏目的全部已发布内容。'}}

首页/{{page.title}}
+
{{ filters() }}
{{grid(listing,'catalog')}}
diff --git a/themes/minimal/versions/1.0.0/pages/contact.html b/themes/minimal/versions/1.0.0/pages/contact.html new file mode 100644 index 0000000..3a52a60 --- /dev/null +++ b/themes/minimal/versions/1.0.0/pages/contact.html @@ -0,0 +1,7 @@ +
{{ 'GET IN TOUCH' if kind=='contact' else ('OUR STORY' if kind=='about' else 'EXPLORE') }}

{{page.title}}

{{page.summary}}

+
{{'CONNECT / CONTACT' if kind=='contact' else 'ABOUT / DISCOVER'}}

{{'让我们开始沟通。' if kind=='contact' else page.title}}

{% if kind=='contact' and footer_email %}{{footer_email}} ↗{% elif kind=='contact' %}

请先在网站管理后台填写正式联系邮箱。

{% endif %}
{{body_copy(content,page)}}{% if page.cover %}{{page.title}}{% endif %}
+ +{% if form_endpoint %} +
CONTACT FORM

提交业务咨询

提交内容将直接发送到网站管理员配置的外部表单服务。

+

表单提交由外部服务处理,请确认其隐私政策及垃圾信息过滤设置。

+{% endif %} diff --git a/themes/minimal/versions/1.0.0/pages/faq.html b/themes/minimal/versions/1.0.0/pages/faq.html new file mode 100644 index 0000000..945be44 --- /dev/null +++ b/themes/minimal/versions/1.0.0/pages/faq.html @@ -0,0 +1,2 @@ +
{{ 'GET IN TOUCH' if kind=='contact' else ('OUR STORY' if kind=='about' else 'EXPLORE') }}

{{page.title}}

{{page.summary}}

+
{{'CONNECT / CONTACT' if kind=='contact' else 'ABOUT / DISCOVER'}}

{{'让我们开始沟通。' if kind=='contact' else page.title}}

{% if kind=='contact' and footer_email %}{{footer_email}} ↗{% elif kind=='contact' %}

请先在网站管理后台填写正式联系邮箱。

{% endif %}
{{body_copy(content,page)}}{% if page.cover %}{{page.title}}{% endif %}
diff --git a/themes/minimal/versions/1.0.0/pages/home.html b/themes/minimal/versions/1.0.0/pages/home.html new file mode 100644 index 0000000..20e3da6 --- /dev/null +++ b/themes/minimal/versions/1.0.0/pages/home.html @@ -0,0 +1,3 @@ +
INDEPENDENT / CREATIVE / CONSIDEREDNO. 001 — ESTABLISH A PRESENCE

{{page.title if page.title not in (site.name,'首页','Home') else site.name}}
MAKE IT MATTER.

{{page.summary or site.tagline or '简洁而清晰地呈现品牌与作品。'}}

EXPLORE WORK ↗
+ +
02 / ABOUT

少一点噪音,多一点重点。

{{site.tagline or '在这里介绍企业与品牌的核心价值。'}}

READ OUR STORY ↗
diff --git a/themes/minimal/versions/1.0.0/pages/listing.html b/themes/minimal/versions/1.0.0/pages/listing.html new file mode 100644 index 0000000..3918172 --- /dev/null +++ b/themes/minimal/versions/1.0.0/pages/listing.html @@ -0,0 +1,2 @@ +
DISCOVER / {{'CATEGORY' if kind=='category' else 'COLLECTION'}}

{{page.title}}

{{page.summary or '查看此栏目的全部已发布内容。'}}

首页/{{page.title}}
+
{{ filters() }}
{{grid(listing,'catalog')}}
diff --git a/themes/minimal/versions/1.0.0/pages/notfound.html b/themes/minimal/versions/1.0.0/pages/notfound.html new file mode 100644 index 0000000..37e3f05 --- /dev/null +++ b/themes/minimal/versions/1.0.0/pages/notfound.html @@ -0,0 +1 @@ +
ERROR / 404

404

抱歉,你访问的页面不存在,或者已被移动。

返回首页 ↗
diff --git a/themes/minimal/versions/1.0.0/pages/product.html b/themes/minimal/versions/1.0.0/pages/product.html new file mode 100644 index 0000000..ba1fe4f --- /dev/null +++ b/themes/minimal/versions/1.0.0/pages/product.html @@ -0,0 +1,3 @@ +
首页/产品中心/{{page.title}}
+
PRODUCT DETAILS / {{fields.get('brand') or 'COLLECTION'}}

{{page.title}}

{{page.summary or '在此查看产品详细资料。'}}

{% if fields.get('brand') %}

品牌 · {{fields.get('brand')}}

{% endif %}咨询该产品 ↗
+
OVERVIEW

产品介绍

{{body_copy(content,page)}}
SPECIFICATIONS

规格参数

{% for row in specs %}
{{row.label}}{{row.value}}
{% else %}

尚未填写详细参数。

{% endfor %}
diff --git a/themes/minimal/versions/1.0.0/pages/search.html b/themes/minimal/versions/1.0.0/pages/search.html new file mode 100644 index 0000000..182a878 --- /dev/null +++ b/themes/minimal/versions/1.0.0/pages/search.html @@ -0,0 +1,2 @@ +
SEARCH / CONTENT

站内搜索

搜索本网站的产品、品牌、新闻和案例。

+
diff --git a/themes/minimal/versions/1.0.0/pages/service.html b/themes/minimal/versions/1.0.0/pages/service.html new file mode 100644 index 0000000..945be44 --- /dev/null +++ b/themes/minimal/versions/1.0.0/pages/service.html @@ -0,0 +1,2 @@ +
{{ 'GET IN TOUCH' if kind=='contact' else ('OUR STORY' if kind=='about' else 'EXPLORE') }}

{{page.title}}

{{page.summary}}

+
{{'CONNECT / CONTACT' if kind=='contact' else 'ABOUT / DISCOVER'}}

{{'让我们开始沟通。' if kind=='contact' else page.title}}

{% if kind=='contact' and footer_email %}{{footer_email}} ↗{% elif kind=='contact' %}

请先在网站管理后台填写正式联系邮箱。

{% endif %}
{{body_copy(content,page)}}{% if page.cover %}{{page.title}}{% endif %}
diff --git a/themes/minimal/versions/1.0.0/pages/standard.html b/themes/minimal/versions/1.0.0/pages/standard.html new file mode 100644 index 0000000..945be44 --- /dev/null +++ b/themes/minimal/versions/1.0.0/pages/standard.html @@ -0,0 +1,2 @@ +
{{ 'GET IN TOUCH' if kind=='contact' else ('OUR STORY' if kind=='about' else 'EXPLORE') }}

{{page.title}}

{{page.summary}}

+
{{'CONNECT / CONTACT' if kind=='contact' else 'ABOUT / DISCOVER'}}

{{'让我们开始沟通。' if kind=='contact' else page.title}}

{% if kind=='contact' and footer_email %}{{footer_email}} ↗{% elif kind=='contact' %}

请先在网站管理后台填写正式联系邮箱。

{% endif %}
{{body_copy(content,page)}}{% if page.cover %}{{page.title}}{% endif %}
diff --git a/themes/minimal/versions/1.0.0/pro.css b/themes/minimal/versions/1.0.0/pro.css new file mode 100644 index 0000000..771077b --- /dev/null +++ b/themes/minimal/versions/1.0.0/pro.css @@ -0,0 +1,2 @@ +:root{--pro-text:#272722;--pro-muted:#797972;--pro-border:#d9d8d0;--pro-background:#fafaf7;--pro-surface:#f0f0eb;--pro-accent:#282923;--pro-hero:#272722;--pro-radius:0;--pro-headings:Georgia,Times New Roman,serif;--pro-font:Arial,sans-serif;--pro-wide:1380px} +.pro-header{background:#fafaf7;color:#292920}.pro-head-inner{min-height:96px}.pro-logo{min-width:200px}.pro-logo-symbol{font-family:Georgia,serif;color:#272722;font-size:40px;font-weight:400}.pro-logo-name{font-family:Georgia,serif;font-size:23px;font-weight:500}.pro-logo-name small{font-family:Arial,sans-serif;font-size:9px}.pro-navigation a{font-size:12px;font-weight:550}.pro-header-cta{border-radius:0;background:transparent;color:#262621!important;border-bottom:1px solid #222;padding:10px 0}.pro-minimal-hero{background:#f4f3ec;color:#252521;border-bottom:1px solid #d7d7cc}.pro-minimal-hero>.pro-wrap{padding-top:68px;padding-bottom:90px}.pro-minimal-meta{display:flex;justify-content:space-between;color:#727267;font-size:10px;letter-spacing:.16em}.pro-minimal-hero h1{font-weight:400;font-family:Georgia,serif;letter-spacing:-.072em;font-size:clamp(65px,10vw,153px);line-height:1.03;margin:100px 0 85px}.pro-minimal-hero h1 em{font-weight:400;font-style:italic;color:#8f8e82}.pro-minimal-bottom{display:flex;justify-content:space-between;align-items:end;gap:40px;border-top:1px solid #c9c8bf;padding-top:28px}.pro-minimal-bottom p{font-size:17px;max-width:450px;margin:0}.pro-minimal-bottom a{font-weight:750;font-size:11px;letter-spacing:.11em;white-space:nowrap}.pro-minimal-bottom a span{padding-left:25px}.pro-section-head h2{font-weight:400;font-size:clamp(40px,5vw,70px)}.pro-grid{gap:34px 20px}.pro-card{border:0;background:transparent;border-radius:0}.pro-card:hover{box-shadow:none}.pro-card-cover{height:310px;background:#eae9e1}.pro-card-body{padding:23px 0}.pro-card h3{font-family:Georgia,serif;font-size:27px;font-weight:400}.pro-art{background:linear-gradient(145deg,#deddd2,#ebeae5)}.pro-art-ring{border-color:#ffffff8a}.pro-art-center{color:#aba89b}.pro-card-link{border-color:#c7c6bf}.pro-statement{background:#e9e7dc}.pro-statement h2{font-family:Georgia,serif;font-weight:400;font-size:clamp(43px,6vw,83px)}.pro-statement .pro-text-link{color:#292a22}.pro-inner-hero{background:#f0efe8;border-bottom:1px solid #deddd3}.pro-inner-hero h1{font-family:Georgia,serif;font-weight:400;font-size:clamp(60px,9vw,117px)}.pro-article-hero{background:#eeede5}.pro-article-hero h1{font-family:Georgia,serif;font-weight:400}.pro-article-content{max-width:830px}.pro-product-summary h1{font-family:Georgia,serif;font-weight:400}.pro-footer{background:#242520}.pro-footer h2{font-family:Georgia,serif;font-size:40px;font-weight:400}.pro-end-cta{background:#282822}.pro-end-cta h2{font-family:Georgia,serif;font-weight:400}.pro-end-cta a{background:#fff;color:#242520!important}.pro-404 h1{font-family:Georgia,serif;font-weight:400;color:#8f8e82}.pro-dot{color:#a8a595} diff --git a/themes/minimal/versions/1.0.0/pro.html b/themes/minimal/versions/1.0.0/pro.html new file mode 100644 index 0000000..5cfdc30 --- /dev/null +++ b/themes/minimal/versions/1.0.0/pro.html @@ -0,0 +1,9 @@ +{{ seo_title }}{% if canonical and not noindex %}{% endif %}{% if theme_overrides %}{% endif %}跳转至正文 +{% from '_professional_components.html' import card,cover,grid,section_title,filters,footer_links,body_copy with context %} +
LET'S TALK ↗
+{% if not designer_replace %}{% include 'minimal/pages/' ~ kind ~ '.html' %}{% endif %} + +{% if has_custom and kind!='notfound' %}
{{custom_blocks}}
{% else %}
{% endif %} +{% if kind not in ('notfound','search') %}
LET'S CONNECT

从这里开始下一步。

{{site.tagline or '了解更多产品、服务与合作信息。'}}

联系我们 ↗
{% endif %} +
+ \ No newline at end of file diff --git a/themes/minimal/versions/1.0.0/site.html b/themes/minimal/versions/1.0.0/site.html new file mode 100644 index 0000000..616c0c2 --- /dev/null +++ b/themes/minimal/versions/1.0.0/site.html @@ -0,0 +1,8 @@ +{{seo_title}}{% if canonical %}{% endif %} +
{{site.name}}®CONTACT ↗
+{% if blueprint_mode %}{% elif mode=='home' %}
ESTABLISHED IN IDEAS.DESIGNED FOR CHANGE.

{{page.title if page.title!=site.name else 'Less, but better.'}}

{{page.summary or site.tagline or 'Thoughtfully made. Beautifully simple. Designed to make an impact.'}}

EXPLORE THE WORK ↗
{% if page.cover %}
{{page.title}}
{% else %}
SIMPLICITY
IS INTENTIONAL.
{% endif %}
+{% else %}
{{type_names.get(page.type,'THE STUDIO')|upper}} — {{'INDEX' if mode=='listing' else 'ABOUT'}}

{{page.title}}

{{page.summary}}

{% endif %} +{% if blueprint_mode %}
{{blocks}}
{% else %}
{% if mode in ['detail','home'] and (content or blocks) %}
{{blocks}}{{content}}
{% endif %}{% if mode=='detail' and page.cover %}
{{page.title}}
{% endif %} +{% set picks=listing_items if mode=='listing' else entries|selectattr('type','equalto','product')|list if mode=='home' else [] %} +{% if picks %}
01 / SELECTED WORK

{{'Our collection' if mode=='home' else page.title}}

{% endif %} +
{% endif %}
diff --git a/themes/minimal/versions/1.0.0/theme.css b/themes/minimal/versions/1.0.0/theme.css new file mode 100644 index 0000000..1547baa --- /dev/null +++ b/themes/minimal/versions/1.0.0/theme.css @@ -0,0 +1 @@ +*{box-sizing:border-box}body{margin:0;color:#171717;background:#faf9f6;font:16px/1.6 Arial,Helvetica,sans-serif}a{color:inherit;text-decoration:none}.container{width:min(1260px,90%);margin:auto}.header{height:110px;display:flex;align-items:center;justify-content:space-between;gap:20px;border-bottom:1px solid #cccac4}.brand{font-family:Georgia,"Times New Roman",serif;font-size:30px;font-weight:bold;letter-spacing:-1px}.brand span{font-size:12px;vertical-align:top}.header nav{display:flex;gap:33px;font-size:12px;flex-wrap:wrap}.header nav a:hover{text-decoration:underline}.contact{font-size:12px;font-weight:bold;border-bottom:1px solid black;padding-bottom:5px}.hero{padding:48px 0 0}.topline{display:flex;justify-content:space-between;font-size:10px;letter-spacing:2.3px;font-weight:bold}.hero h1,.inner-hero h1{font-family:Georgia,"Times New Roman",serif;font-size:clamp(65px,9.1vw,155px);line-height:1;letter-spacing:-8px;font-weight:400;margin:85px 0 40px}.hero-bottom{display:flex;justify-content:space-between;align-items:end;margin:35px 0 64px;gap:25px}.hero-bottom p{font-size:18px;max-width:490px;margin:0}.hero-bottom a{font-size:12px;letter-spacing:1.5px;border-bottom:1px solid black;padding-bottom:8px}.hero-image{height:425px;background:#deddd4;overflow:hidden;position:relative;display:flex;align-items:center;justify-content:center}.hero-image img{height:100%;width:100%;object-fit:cover}.graphic{background:#cec8bc}.shape{position:absolute;border-radius:50%;background:linear-gradient(125deg,#ede9e1,#98918a);box-shadow:25px 30px 40px #59544c22}.shape.a{height:550px;width:550px;left:5%;top:-230px}.shape.b{width:280px;height:280px;right:15%;bottom:-120px;background:#989e97}.shape.c{width:160px;height:160px;right:40%;top:0;background:#ece9e3}.graphic>span{position:relative;font-size:25px;letter-spacing:7px;font-weight:500;line-height:1.5}.inner-hero{padding:80px 0;border-bottom:1px solid #d3d0ca}.inner-hero h1{font-size:clamp(48px,8vw,115px);margin:40px 0}.inner-hero p{font-size:18px;color:#777;max-width:660px}.content{padding:70px 0 130px}.list-head{display:grid;grid-template-columns:1fr 2fr;align-items:center;margin:30px 0 40px;border-bottom:1px solid black}.list-head span{font-size:10px;letter-spacing:2px}.list-head h2{font-family:Georgia,serif;font-weight:400;font-size:54px;letter-spacing:-2px}.list{display:grid;grid-template-columns:repeat(2,1fr);gap:80px 35px}.item{display:block}.item:hover h3{text-decoration:underline}.thumb{height:290px;background:#e3e1d8;display:grid;place-items:center}.thumb img{width:100%;height:100%;object-fit:contain}.thumb span{font-family:Georgia,serif;font-size:140px;color:#c0bcb2}.info{padding:25px 0}.info>span{font-size:10px;letter-spacing:1.5px}.info h3{font-family:Georgia,serif;font-size:36px;font-weight:400;margin:12px 0}.info p{color:#666;max-width:400px}.info b{font-size:11px;letter-spacing:1px}.rich-content{margin:25px 0}.section{padding:45px 0}.block-hero{padding:50px 0}.block-hero h1{font-family:Georgia,serif;font-size:60px;font-weight:400}.feature-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:30px}.feature{border-top:1px solid #8f8a84;padding-top:20px}.feature-mark{font-size:12px}.stats{display:grid;grid-template-columns:repeat(4,1fr)}.stat strong{font-family:Georgia,serif;display:block;font-size:47px;font-weight:400}.stat span{font-size:11px;letter-spacing:1px}.cta{padding:55px;background:#e4e0d8}.cta h2{font-family:Georgia,serif;font-weight:400;font-size:44px}.btn{display:inline-block;background:#222;color:white;padding:13px 20px;font-size:12px}.detail-img img{max-width:100%;max-height:550px}footer{border-top:1px solid #b9b5ae;padding:70px 0 25px}.footer-lead{font-family:Georgia,serif;font-size:clamp(45px,6vw,90px);line-height:1.05;letter-spacing:-4px}.footer-lead span{color:#909084}.footer-bottom{display:flex;justify-content:space-between;border-top:1px solid #d5d1c9;margin-top:90px;padding-top:25px;font-size:11px;color:#777}@media(max-width:850px){.header{height:auto;padding:26px 0;flex-wrap:wrap}.header nav{order:3;width:100%}.hero h1{letter-spacing:-4px;margin-top:60px}.hero-image{height:320px}.list{gap:35px}.list-head{grid-template-columns:1fr}.footer-lead{letter-spacing:-2px}}@media(max-width:560px){.list,.feature-grid,.stats{grid-template-columns:1fr}.hero h1{font-size:67px}.hero-bottom,.topline,.footer-bottom{flex-direction:column;align-items:start;gap:15px}.hero-image{height:240px}.list-head h2{font-size:40px}.inner-hero{padding:50px 0}} diff --git a/themes/minimal/visual/hero.svg b/themes/minimal/visual/hero.svg new file mode 100644 index 0000000..f43879d --- /dev/null +++ b/themes/minimal/visual/hero.svg @@ -0,0 +1 @@ +STUDIES IN FORM