@huicai/cli 2026.814.0 → 2026.1021.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +391 -32
- package/README.md +139 -8
- package/bin/huicai.js +155 -74
- package/bin/update.js +97 -0
- package/package.json +27 -8
- package/skills/ab-testing/trigger-cases.json +30 -0
- package/skills/academic-writing/trigger-cases.json +30 -0
- package/skills/accounting-ops/trigger-cases.json +30 -0
- package/skills/ai-agent/trigger-cases.json +30 -0
- package/skills/ai-aiops/trigger-cases.json +30 -0
- package/skills/ai-coding/SKILL.md +2 -0
- package/skills/ai-coding/trigger-cases.json +30 -0
- package/skills/ai-compliance/trigger-cases.json +30 -0
- package/skills/ai-eval/trigger-cases.json +30 -0
- package/skills/ai-llm-app/trigger-cases.json +30 -0
- package/skills/ai-mlops/trigger-cases.json +30 -0
- package/skills/ai-multimodal/trigger-cases.json +30 -0
- package/skills/ai-prompt/trigger-cases.json +30 -0
- package/skills/ai-rag/trigger-cases.json +30 -0
- package/skills/anomaly-detection/trigger-cases.json +30 -0
- package/skills/api-design/trigger-cases.json +31 -0
- package/skills/api-integration/trigger-cases.json +31 -0
- package/skills/arch-design/trigger-cases.json +12 -0
- package/skills/arch-diagram/SKILL.md +118 -0
- package/skills/arch-diagram/trigger-cases.json +30 -0
- package/skills/arch-distributed/trigger-cases.json +30 -0
- package/skills/arch-multitenant/trigger-cases.json +30 -0
- package/skills/audio-brief/SKILL.md +65 -0
- package/skills/audio-brief/trigger-cases.json +30 -0
- package/skills/audit-assist/trigger-cases.json +30 -0
- package/skills/audit-chain/trigger-cases.json +31 -0
- package/skills/banking-credit/trigger-cases.json +30 -0
- package/skills/brand-design/trigger-cases.json +30 -0
- package/skills/business-plan/trigger-cases.json +30 -0
- package/skills/capital-ops/trigger-cases.json +30 -0
- package/skills/cicd-pipeline/trigger-cases.json +31 -0
- package/skills/code-archaeology/SKILL.md +55 -0
- package/skills/code-archaeology/trigger-cases.json +9 -0
- package/skills/code-quality/SKILL.md +138 -0
- package/skills/code-quality/references/languages.md +107 -0
- package/skills/code-quality/scripts/__pycache__/code_battery.cpython-312.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_check.cpython-312.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_check.cpython-313.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_csharp.cpython-312.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_csharp.cpython-313.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_fix.cpython-312.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_fix.cpython-313.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_java.cpython-312.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_java.cpython-313.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_jsts.cpython-312.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_jsts.cpython-313.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_lens.cpython-312.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_lens.cpython-313.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_python.cpython-312.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_python.cpython-313.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_testplan.cpython-312.pyc +0 -0
- package/skills/code-quality/scripts/code_battery.py +1072 -0
- package/skills/code-quality/scripts/code_check.py +1269 -0
- package/skills/code-quality/scripts/code_csharp.py +371 -0
- package/skills/code-quality/scripts/code_fix.py +201 -0
- package/skills/code-quality/scripts/code_java.py +695 -0
- package/skills/code-quality/scripts/code_jsts.py +313 -0
- package/skills/code-quality/scripts/code_lens.py +1079 -0
- package/skills/code-quality/scripts/code_python.py +536 -0
- package/skills/code-quality/scripts/code_testplan.py +239 -0
- package/skills/code-quality/trigger-cases.json +45 -0
- package/skills/code-review/SKILL.md +24 -0
- package/skills/code-review/trigger-cases.json +35 -0
- package/skills/commit-and-pr/trigger-cases.json +31 -0
- package/skills/content-creation/trigger-cases.json +30 -0
- package/skills/corporate-governance/trigger-cases.json +30 -0
- package/skills/crawl-chain/trigger-cases.json +31 -0
- package/skills/customer-service/trigger-cases.json +30 -0
- package/skills/data-analysis/trigger-cases.json +30 -0
- package/skills/data-chain/SKILL.md +1 -1
- package/skills/data-chain/trigger-cases.json +31 -0
- package/skills/data-governance/trigger-cases.json +30 -0
- package/skills/data-report-pro/trigger-cases.json +30 -0
- package/skills/data-streaming/trigger-cases.json +30 -0
- package/skills/data-wrangling/SKILL.md +1 -1
- package/skills/data-wrangling/trigger-cases.json +30 -0
- package/skills/db-es/trigger-cases.json +30 -0
- package/skills/db-migration/trigger-cases.json +30 -0
- package/skills/db-mysql/trigger-cases.json +30 -0
- package/skills/db-postgres/trigger-cases.json +30 -0
- package/skills/db-redis/trigger-cases.json +30 -0
- package/skills/debug-root-cause/trigger-cases.json +30 -0
- package/skills/dependency-audit/SKILL.md +56 -0
- package/skills/dependency-audit/trigger-cases.json +7 -0
- package/skills/dependency-upgrade/trigger-cases.json +31 -0
- package/skills/dev-cpp/trigger-cases.json +30 -0
- package/skills/dev-cross-platform/trigger-cases.json +30 -0
- package/skills/dev-csharp/SKILL.md +23 -0
- package/skills/dev-csharp/trigger-cases.json +30 -0
- package/skills/dev-frontend/SKILL.md +19 -0
- package/skills/dev-frontend/trigger-cases.json +30 -0
- package/skills/dev-go/trigger-cases.json +30 -0
- package/skills/dev-harmony/trigger-cases.json +30 -0
- package/skills/dev-java/SKILL.md +20 -0
- package/skills/dev-java/trigger-cases.json +30 -0
- package/skills/dev-mobile/trigger-cases.json +30 -0
- package/skills/dev-node/trigger-cases.json +30 -0
- package/skills/dev-php/trigger-cases.json +30 -0
- package/skills/dev-python/SKILL.md +21 -0
- package/skills/dev-python/trigger-cases.json +30 -0
- package/skills/dev-rust/trigger-cases.json +30 -0
- package/skills/dev-toolchain/trigger-cases.json +31 -0
- package/skills/doc-chain/trigger-cases.json +31 -0
- package/skills/doc-compare/trigger-cases.json +30 -0
- package/skills/docx-author/SKILL.md +127 -25
- package/skills/docx-author/scripts/__pycache__/docx_battery.cpython-312.pyc +0 -0
- package/skills/docx-author/scripts/__pycache__/docx_build.cpython-312.pyc +0 -0
- package/skills/docx-author/scripts/__pycache__/docx_check.cpython-312.pyc +0 -0
- package/skills/docx-author/scripts/__pycache__/docx_layout.cpython-312.pyc +0 -0
- package/skills/docx-author/scripts/__pycache__/zh_prose.cpython-312.pyc +0 -0
- package/skills/docx-author/scripts/docx_battery.py +586 -0
- package/skills/docx-author/scripts/docx_build.py +995 -0
- package/skills/docx-author/scripts/docx_check.py +1306 -0
- package/skills/docx-author/scripts/docx_doc.py +14 -154
- package/skills/docx-author/scripts/docx_layout.py +490 -0
- package/skills/docx-author/scripts/zh_prose.py +639 -0
- package/skills/docx-author/trigger-cases.json +58 -0
- package/skills/echo-brief/SKILL.md +84 -0
- package/skills/echo-brief/references/echo-format.md +68 -0
- package/skills/echo-brief/trigger-cases.json +40 -0
- package/skills/ecommerce-ops/trigger-cases.json +30 -0
- package/skills/education-training/trigger-cases.json +30 -0
- package/skills/ehs-safety/trigger-cases.json +30 -0
- package/skills/email-writing/trigger-cases.json +30 -0
- package/skills/eng-ai/trigger-cases.json +30 -0
- package/skills/eng-architecture/trigger-cases.json +31 -0
- package/skills/eng-automation/trigger-cases.json +31 -0
- package/skills/eng-collaboration/trigger-cases.json +30 -0
- package/skills/eng-complex-env/trigger-cases.json +30 -0
- package/skills/eng-data-platform/trigger-cases.json +31 -0
- package/skills/eng-database/trigger-cases.json +31 -0
- package/skills/eng-develop/SKILL.md +9 -1
- package/skills/eng-develop/references/dod-checklist.md +3 -0
- package/skills/eng-develop/references/engineering-workflow.md +37 -0
- package/skills/eng-develop/trigger-cases.json +30 -0
- package/skills/eng-docs/SKILL.md +24 -0
- package/skills/eng-docs/trigger-cases.json +30 -0
- package/skills/eng-efficiency/trigger-cases.json +30 -0
- package/skills/eng-infra/trigger-cases.json +30 -0
- package/skills/eng-integration/trigger-cases.json +31 -0
- package/skills/eng-legacy/trigger-cases.json +31 -0
- package/skills/eng-observability/trigger-cases.json +31 -0
- package/skills/eng-ops/trigger-cases.json +31 -0
- package/skills/eng-performance/trigger-cases.json +31 -0
- package/skills/eng-planning/trigger-cases.json +31 -0
- package/skills/eng-quality/SKILL.md +8 -0
- package/skills/eng-quality/trigger-cases.json +31 -0
- package/skills/eng-release/trigger-cases.json +31 -0
- package/skills/eng-reliability/trigger-cases.json +31 -0
- package/skills/eng-requirements/trigger-cases.json +12 -0
- package/skills/eng-research/trigger-cases.json +30 -0
- package/skills/eng-security/trigger-cases.json +31 -0
- package/skills/eng-ux/trigger-cases.json +30 -0
- package/skills/engineering-chain/trigger-cases.json +30 -0
- package/skills/env-offline/trigger-cases.json +31 -0
- package/skills/env-private-deploy/trigger-cases.json +31 -0
- package/skills/env-xinchuang/trigger-cases.json +31 -0
- package/skills/equipment-tpm/trigger-cases.json +30 -0
- package/skills/esg-report/trigger-cases.json +30 -0
- package/skills/event-management/trigger-cases.json +30 -0
- package/skills/excel-analysis-chain/SKILL.md +17 -4
- package/skills/excel-analysis-chain/trigger-cases.json +31 -0
- package/skills/explain-codebase/trigger-cases.json +11 -0
- package/skills/file-ingest/trigger-cases.json +30 -0
- package/skills/finance-analysis/trigger-cases.json +31 -0
- package/skills/finance-chain/trigger-cases.json +31 -0
- package/skills/financial-modeling/trigger-cases.json +31 -0
- package/skills/find-simplifications/SKILL.md +43 -0
- package/skills/find-simplifications/trigger-cases.json +23 -0
- package/skills/foreign-trade/trigger-cases.json +30 -0
- package/skills/funnel-cohort/trigger-cases.json +30 -0
- package/skills/gov-document/trigger-cases.json +30 -0
- package/skills/gui-change-evidence/SKILL.md +47 -0
- package/skills/gui-change-evidence/trigger-cases.json +23 -0
- package/skills/hr-chain/trigger-cases.json +31 -0
- package/skills/hr-operations/trigger-cases.json +30 -0
- package/skills/info-disclosure/trigger-cases.json +31 -0
- package/skills/infra-gateway/trigger-cases.json +31 -0
- package/skills/infra-k8s/trigger-cases.json +31 -0
- package/skills/infra-mq/trigger-cases.json +30 -0
- package/skills/insurance-advisory/trigger-cases.json +30 -0
- package/skills/internal-audit/trigger-cases.json +30 -0
- package/skills/ip-management/trigger-cases.json +31 -0
- package/skills/ipo-listing/trigger-cases.json +31 -0
- package/skills/it-audit/trigger-cases.json +31 -0
- package/skills/itsm-ops/trigger-cases.json +31 -0
- package/skills/knowledge-sop/trigger-cases.json +31 -0
- package/skills/legal-chain/trigger-cases.json +31 -0
- package/skills/legal-contract/trigger-cases.json +30 -0
- package/skills/litigation-support/trigger-cases.json +31 -0
- package/skills/logistics-warehouse/trigger-cases.json +30 -0
- package/skills/long-doc-analysis/trigger-cases.json +31 -0
- package/skills/management-accounting/trigger-cases.json +30 -0
- package/skills/manufacturing-chain/trigger-cases.json +31 -0
- package/skills/manufacturing-quality/trigger-cases.json +30 -0
- package/skills/marketing-analytics/trigger-cases.json +31 -0
- package/skills/marketing-chain/trigger-cases.json +31 -0
- package/skills/meeting-minutes/trigger-cases.json +30 -0
- package/skills/metrics-system/trigger-cases.json +31 -0
- package/skills/negotiation-prep/trigger-cases.json +30 -0
- package/skills/obs-prometheus/trigger-cases.json +31 -0
- package/skills/office-automation/trigger-cases.json +31 -0
- package/skills/okr-review/trigger-cases.json +30 -0
- package/skills/ops-fleet/trigger-cases.json +31 -0
- package/skills/ops-linux/trigger-cases.json +31 -0
- package/skills/optimization-solver/trigger-cases.json +30 -0
- package/skills/patent-disclosure/SKILL.md +105 -0
- package/skills/patent-disclosure/references/figures.md +110 -0
- package/skills/patent-disclosure/references/outline.md +162 -0
- package/skills/patent-disclosure/scripts/patent_docx.py +230 -0
- package/skills/patent-disclosure/trigger-cases.json +31 -0
- package/skills/patent-drafting/SKILL.md +108 -0
- package/skills/patent-drafting/trigger-cases.json +31 -0
- package/skills/patent-portfolio/SKILL.md +81 -0
- package/skills/patent-portfolio/trigger-cases.json +31 -0
- package/skills/pdf-toolkit/trigger-cases.json +31 -0
- package/skills/perf-optimize/trigger-cases.json +31 -0
- package/skills/pivot-crosstab/trigger-cases.json +30 -0
- package/skills/policy-analysis/trigger-cases.json +31 -0
- package/skills/portfolio-analysis/trigger-cases.json +31 -0
- package/skills/ppt-chain/SKILL.md +14 -6
- package/skills/ppt-chain/trigger-cases.json +31 -0
- package/skills/pptx-author/SKILL.md +86 -33
- package/skills/pptx-author/scripts/__pycache__/pptx_check.cpython-312.pyc +0 -0
- package/skills/pptx-author/scripts/__pycache__/pptx_deck.cpython-312.pyc +0 -0
- package/skills/pptx-author/scripts/__pycache__/pptx_layout.cpython-312.pyc +0 -0
- package/skills/pptx-author/scripts/pptx_battery.py +178 -0
- package/skills/pptx-author/scripts/pptx_build.py +119 -0
- package/skills/pptx-author/scripts/pptx_check.py +1182 -0
- package/skills/pptx-author/scripts/pptx_deck.py +736 -145
- package/skills/pptx-author/scripts/pptx_layout.py +384 -0
- package/skills/pptx-author/trigger-cases.json +15 -0
- package/skills/pr-comms/trigger-cases.json +30 -0
- package/skills/procurement-supply/trigger-cases.json +31 -0
- package/skills/product-design/trigger-cases.json +31 -0
- package/skills/production-planning/trigger-cases.json +31 -0
- package/skills/project-management/trigger-cases.json +31 -0
- package/skills/project-proposal/trigger-cases.json +31 -0
- package/skills/prose-standard/SKILL.md +53 -0
- package/skills/prose-standard/trigger-cases.json +23 -0
- package/skills/qa-api-automation/trigger-cases.json +31 -0
- package/skills/qa-mobile-automation/trigger-cases.json +31 -0
- package/skills/qa-ui-automation/trigger-cases.json +31 -0
- package/skills/quant-analysis/trigger-cases.json +31 -0
- package/skills/rag-triage/SKILL.md +64 -0
- package/skills/rag-triage/evals.json +57 -0
- package/skills/rag-triage/references/triage-playbook.md +75 -0
- package/skills/rag-triage/trigger-cases.json +31 -0
- package/skills/real-estate-property/trigger-cases.json +30 -0
- package/skills/refactor-safely/SKILL.md +1 -1
- package/skills/refactor-safely/trigger-cases.json +30 -0
- package/skills/regression-forecast/trigger-cases.json +31 -0
- package/skills/release-checklist/trigger-cases.json +31 -0
- package/skills/report-craft/SKILL.md +76 -0
- package/skills/report-craft/references/feasibility.md +118 -0
- package/skills/report-craft/references/review-checklist.md +51 -0
- package/skills/report-craft/references/visuals.md +114 -0
- package/skills/report-craft/references/writing.md +80 -0
- package/skills/report-craft/scripts/__pycache__/chart_render.cpython-312.pyc +0 -0
- package/skills/report-craft/scripts/__pycache__/fin_model.cpython-312.pyc +0 -0
- package/skills/report-craft/scripts/__pycache__/project_facts.cpython-312.pyc +0 -0
- package/skills/report-craft/scripts/chart_render.py +1222 -0
- package/skills/report-craft/scripts/fin_model.py +602 -0
- package/skills/report-craft/scripts/project_facts.py +580 -0
- package/skills/report-craft/scripts/report_battery.py +349 -0
- package/skills/report-craft/trigger-cases.json +15 -0
- package/skills/retail-store-ops/trigger-cases.json +30 -0
- package/skills/risk-compliance/trigger-cases.json +31 -0
- package/skills/scope-creep/SKILL.md +45 -0
- package/skills/scope-creep/trigger-cases.json +7 -0
- package/skills/sec-auth/trigger-cases.json +31 -0
- package/skills/sec-compliance/trigger-cases.json +31 -0
- package/skills/securities-chain/trigger-cases.json +31 -0
- package/skills/securities-research/trigger-cases.json +31 -0
- package/skills/security-audit/trigger-cases.json +31 -0
- package/skills/slides-html/SKILL.md +25 -10
- package/skills/slides-html/trigger-cases.json +11 -0
- package/skills/sql-analysis/SKILL.md +110 -0
- package/skills/sql-analysis/trigger-cases.json +34 -0
- package/skills/sql-tuning/trigger-cases.json +31 -0
- package/skills/statistical-testing/trigger-cases.json +31 -0
- package/skills/strategy-planning/trigger-cases.json +30 -0
- package/skills/supply-chain/trigger-cases.json +31 -0
- package/skills/tax-compliance/trigger-cases.json +30 -0
- package/skills/tech-doc/SKILL.md +262 -0
- package/skills/tech-doc/scripts/__pycache__/archive_battery.cpython-312.pyc +0 -0
- package/skills/tech-doc/scripts/__pycache__/archive_build.cpython-312.pyc +0 -0
- package/skills/tech-doc/scripts/__pycache__/archive_check.cpython-312.pyc +0 -0
- package/skills/tech-doc/scripts/__pycache__/archive_spec.cpython-312.pyc +0 -0
- package/skills/tech-doc/scripts/__pycache__/doc_templates.cpython-312.pyc +0 -0
- package/skills/tech-doc/scripts/archive_battery.py +860 -0
- package/skills/tech-doc/scripts/archive_build.py +599 -0
- package/skills/tech-doc/scripts/archive_check.py +1307 -0
- package/skills/tech-doc/scripts/archive_spec.py +499 -0
- package/skills/tech-doc/scripts/doc_templates.py +624 -0
- package/skills/tech-doc/trigger-cases.json +57 -0
- package/skills/tech-research/trigger-cases.json +31 -0
- package/skills/tender-bidding/trigger-cases.json +31 -0
- package/skills/translation-pro/trigger-cases.json +30 -0
- package/skills/treasury-mgmt/trigger-cases.json +31 -0
- package/skills/trim-cot-leakage/SKILL.md +58 -0
- package/skills/trim-cot-leakage/trigger-cases.json +23 -0
- package/skills/web-craft/SKILL.md +103 -0
- package/skills/web-craft/references/craft-checklist.md +75 -0
- package/skills/web-craft/references/design-direction.md +122 -0
- package/skills/web-craft/references/h5-mobile.md +100 -0
- package/skills/web-craft/references/motion.md +136 -0
- package/skills/web-craft/references/recipes.md +74 -0
- package/skills/web-craft/templates/wedding-invitation.html +1180 -0
- package/skills/web-craft/trigger-cases.json +14 -0
- package/skills/web-scraping/trigger-cases.json +34 -0
- package/skills/web-search/SKILL.md +11 -10
- package/skills/web-search/trigger-cases.json +31 -0
- package/skills/weekly-report/SKILL.md +134 -0
- package/skills/weekly-report/trigger-cases.json +11 -0
- package/skills/write-tests/trigger-cases.json +31 -0
- package/skills/xlsx-analysis/SKILL.md +143 -77
- package/skills/xlsx-analysis/scripts/__pycache__/data_profile.cpython-312.pyc +0 -0
- package/skills/xlsx-analysis/scripts/__pycache__/xlsx_build.cpython-312.pyc +0 -0
- package/skills/xlsx-analysis/scripts/__pycache__/xlsx_check.cpython-312.pyc +0 -0
- package/skills/xlsx-analysis/scripts/__pycache__/xlsx_layout.cpython-312.pyc +0 -0
- package/skills/xlsx-analysis/scripts/__pycache__/xlsx_toolkit.cpython-312.pyc +0 -0
- package/skills/xlsx-analysis/scripts/data_profile.py +391 -0
- package/skills/xlsx-analysis/scripts/xlsx_battery.py +389 -0
- package/skills/xlsx-analysis/scripts/xlsx_build.py +522 -0
- package/skills/xlsx-analysis/scripts/xlsx_check.py +924 -0
- package/skills/xlsx-analysis/scripts/xlsx_layout.py +235 -0
- package/skills/xlsx-analysis/scripts/xlsx_style.py +28 -66
- package/skills/xlsx-analysis/scripts/xlsx_toolkit.py +61 -5
- package/skills/xlsx-analysis/trigger-cases.json +59 -0
- package/squads/hc-defense.md +47 -0
- package/squads/hc-rdarchive.md +110 -0
- package/squads/hc-redteam.md +50 -0
- package/squads/hc-weekly.md +125 -0
- package/src/account.js +1 -1
- package/src/acp.js +1 -0
- package/src/advisor.js +1 -1
- package/src/agent.js +1 -1
- package/src/api.js +1 -1
- package/src/archaeology.js +1 -0
- package/src/ask-ui.js +1 -0
- package/src/assets-import.js +1 -1
- package/src/autofix.js +1 -1
- package/src/automation-tools.js +1 -1
- package/src/automation.js +1 -1
- package/src/autorun.js +1 -1
- package/src/banner.js +1 -1
- package/src/capability-probe.js +1 -0
- package/src/checkpoints.js +1 -1
- package/src/code-tools.js +1 -0
- package/src/codeindex.js +1 -1
- package/src/codemode.js +1 -0
- package/src/command-bridge.js +1 -0
- package/src/command-ui.js +1 -0
- package/src/commands.js +1 -1
- package/src/compaction.js +1 -1
- package/src/computer.js +1 -1
- package/src/config.js +1 -1
- package/src/consult.js +1 -1
- package/src/context-report.js +1 -1
- package/src/context-window.js +1 -0
- package/src/context.js +1 -1
- package/src/crew.js +1 -0
- package/src/crush.js +1 -0
- package/src/daemon.js +1 -1
- package/src/decisions.js +1 -1
- package/src/depaudit.js +1 -0
- package/src/device.js +1 -1
- package/src/diagram-draw.js +1 -0
- package/src/diagram-layouts.js +1 -0
- package/src/diagram-raster.js +1 -0
- package/src/diagram.js +1 -0
- package/src/dictation.js +1 -0
- package/src/diffview.js +1 -1
- package/src/doctor.js +1 -1
- package/src/docx-tools.js +1 -0
- package/src/engineering.js +1 -0
- package/src/errkind.js +1 -1
- package/src/evidence.js +1 -0
- package/src/executors.js +1 -1
- package/src/expert-catalog.js +1 -1
- package/src/experts-store.js +1 -1
- package/src/experts.js +1 -1
- package/src/external-agents.js +1 -0
- package/src/forks.js +1 -1
- package/src/fs-observe.js +1 -0
- package/src/gitflow.js +1 -1
- package/src/goalround.js +1 -0
- package/src/hooks.js +1 -1
- package/src/ide.js +1 -1
- package/src/input.js +1 -1
- package/src/insights.js +1 -1
- package/src/jobs.js +1 -0
- package/src/kbsync.js +1 -1
- package/src/lsp.js +1 -0
- package/src/mcp-apps.js +1 -0
- package/src/mcp.js +1 -1
- package/src/memory.js +1 -1
- package/src/notify.js +1 -1
- package/src/permissions.js +1 -1
- package/src/plan-limits.js +1 -0
- package/src/policy.js +1 -0
- package/src/pptx-tools.js +1 -0
- package/src/presence.js +1 -1
- package/src/privatedir.js +1 -0
- package/src/pyruntime.js +1 -1
- package/src/radar.js +1 -0
- package/src/render.js +1 -1
- package/src/repl.js +1 -1
- package/src/report-tools.js +1 -0
- package/src/routing.js +1 -0
- package/src/sandbox-probe.js +1 -0
- package/src/sandbox-win-launcher.mjs +156 -0
- package/src/sandbox.js +1 -0
- package/src/scan.js +1 -1
- package/src/schedule.js +1 -1
- package/src/scopecreep.js +1 -0
- package/src/sdk.js +1 -0
- package/src/selection.js +1 -0
- package/src/session-events.js +1 -0
- package/src/session.js +1 -1
- package/src/sessionindex.js +1 -0
- package/src/sessionlog.js +1 -0
- package/src/skill-catalog.js +1 -0
- package/src/skill-lint.js +1 -0
- package/src/skill-optimize.js +1 -0
- package/src/skill-scan.js +1 -0
- package/src/skills.js +1 -1
- package/src/spill.js +1 -0
- package/src/squad.js +1 -1
- package/src/subagent.js +1 -1
- package/src/surface-state.js +1 -0
- package/src/tapd.js +1 -0
- package/src/terminal-select.js +1 -0
- package/src/terminal.js +1 -0
- package/src/test-runner.js +1 -0
- package/src/todo.js +1 -1
- package/src/toolpipe.js +1 -0
- package/src/tools.js +1 -1
- package/src/toon.js +1 -0
- package/src/transcript.js +1 -1
- package/src/tui/app.js +1 -1
- package/src/tui/command-menu.js +1 -0
- package/src/tui/progress.js +1 -0
- package/src/tui/scroll.js +1 -0
- package/src/tui/welcome.js +1 -0
- package/src/tui/workbench.js +1 -0
- package/src/ui-spec.js +1 -0
- package/src/uitree.js +1 -0
- package/src/uninstall.js +1 -1
- package/src/vision.js +1 -1
- package/src/visual.js +1 -1
- package/src/voice-form.js +1 -0
- package/src/voice.js +1 -0
- package/src/watch.js +1 -1
- package/src/web-audit.js +1 -0
- package/src/web-cdp.js +1 -0
- package/src/web-inspect.js +1 -0
- package/src/websearch.js +1 -0
- package/src/weekly.js +1 -0
- package/src/workflow.js +1 -0
- package/src/worklog.js +1 -0
- package/src/xlsx-tools.js +1 -0
- package/ui/command-picker.js +79 -0
- package/skills/ab-testing/scripts/__pycache__/ab_test.cpython-312.pyc +0 -0
- package/skills/accounting-ops/scripts/__pycache__/acct_tools.cpython-312.pyc +0 -0
- package/skills/anomaly-detection/scripts/__pycache__/anomaly.cpython-312.pyc +0 -0
- package/skills/audit-assist/scripts/__pycache__/audit_sampling.cpython-312.pyc +0 -0
- package/skills/banking-credit/scripts/__pycache__/credit_metrics.cpython-312.pyc +0 -0
- package/skills/data-report-pro/scripts/__pycache__/bigdata.cpython-312.pyc +0 -0
- package/skills/data-wrangling/scripts/__pycache__/join_tools.cpython-312.pyc +0 -0
- package/skills/ecommerce-ops/scripts/__pycache__/gmv_decompose.cpython-312.pyc +0 -0
- package/skills/funnel-cohort/scripts/__pycache__/funnel.cpython-312.pyc +0 -0
- package/skills/management-accounting/scripts/__pycache__/costing.cpython-312.pyc +0 -0
- package/skills/manufacturing-quality/scripts/__pycache__/spc.cpython-312.pyc +0 -0
- package/skills/marketing-analytics/scripts/__pycache__/rfm.cpython-312.pyc +0 -0
- package/skills/optimization-solver/scripts/__pycache__/solver.cpython-312.pyc +0 -0
- package/skills/pivot-crosstab/scripts/__pycache__/pivot_tools.cpython-312.pyc +0 -0
- package/skills/portfolio-analysis/scripts/__pycache__/portfolio.cpython-312.pyc +0 -0
- package/skills/procurement-supply/scripts/__pycache__/supply.cpython-312.pyc +0 -0
- package/skills/production-planning/scripts/__pycache__/mrp_capacity.cpython-312.pyc +0 -0
- package/skills/project-management/scripts/__pycache__/cpm.cpython-312.pyc +0 -0
- package/skills/quant-analysis/scripts/__pycache__/quant.cpython-312.pyc +0 -0
- package/skills/real-estate-property/scripts/__pycache__/property_calc.cpython-312.pyc +0 -0
- package/skills/regression-forecast/scripts/__pycache__/regress.cpython-312.pyc +0 -0
- package/skills/securities-research/scripts/__pycache__/valuation.cpython-312.pyc +0 -0
- package/skills/statistical-testing/scripts/__pycache__/stat_tests.cpython-312.pyc +0 -0
- package/skills/treasury-mgmt/scripts/__pycache__/cashflow_forecast.cpython-312.pyc +0 -0
|
@@ -0,0 +1,1180 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="zh-CN" data-theme="cinnabar">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
|
6
|
+
<meta name="format-detection" content="telephone=no">
|
|
7
|
+
<meta name="theme-color" content="#B3302A">
|
|
8
|
+
<title>顾北辰 & 沈知意 · 婚礼请柬</title>
|
|
9
|
+
<meta name="description" content="谨定于 2026 年 6 月 6 日(星期六)中午 11:18,在苏州云栖酒店揽月厅举行婚礼,恭候光临。">
|
|
10
|
+
<meta property="og:title" content="顾北辰 & 沈知意 · 婚礼请柬">
|
|
11
|
+
<meta property="og:description" content="2026.06.06 星期六 · 苏州云栖酒店 揽月厅">
|
|
12
|
+
<script>
|
|
13
|
+
/* ════════
|
|
14
|
+
* 请柬内容 —— 只改这一段即可(页面上的文字全部由它生成)。
|
|
15
|
+
* 同步改一下上面 <title> 与 description:微信/QQ 抓分享卡片时脚本还没跑。
|
|
16
|
+
* ════════ */
|
|
17
|
+
window.INVITE = {
|
|
18
|
+
// 示例数据标记:换成新人的真实信息后改为 false。为 true 时页面顶部会挂一条 TODO 提醒,
|
|
19
|
+
// web_inspect 会按「开发说明露给访客」报必修——专防复制了模板却忘了换内容
|
|
20
|
+
sample: true,
|
|
21
|
+
groom: '顾北辰',
|
|
22
|
+
bride: '沈知意',
|
|
23
|
+
// 婚礼开始时间:必须带时区(+08:00),外地亲友手机时区不同也不会算错
|
|
24
|
+
date: '2026-06-06T11:18:00+08:00',
|
|
25
|
+
durationHours: 3, // 加入日历时的时长
|
|
26
|
+
lunar: '', // 留空 = 按中国农历自动计算(如「丙午年四月廿一」)
|
|
27
|
+
tagline: '六月六日 · 六六大顺', // 首屏一句话:日子有讲究就点出来(用代码算过的);没有就写「我们结婚啦」
|
|
28
|
+
theme: 'cinnabar', // cinnabar 朱砂宣纸 | champagne 法式香槟 | sage 森系 | ink 水墨
|
|
29
|
+
venue: {
|
|
30
|
+
name: '云栖酒店',
|
|
31
|
+
hall: '揽月厅',
|
|
32
|
+
city: '苏州',
|
|
33
|
+
address: '苏州市姑苏区示例路 88 号',
|
|
34
|
+
lnglat: '', // 可选:高德坐标「经度,纬度」;留空则按名称检索导航(不会因坐标写错把人导错地方)
|
|
35
|
+
tips: '园区内可停车,婚宴当日请听从现场指引。'
|
|
36
|
+
},
|
|
37
|
+
// 流程时间请与新人核对后再发出
|
|
38
|
+
schedule: [
|
|
39
|
+
{ time: '10:30', title: '迎宾签到', desc: '宴会厅前庭 · 茶点与合影' },
|
|
40
|
+
{ time: '11:18', title: '婚礼仪式', desc: '见证我们交换誓言', key: true },
|
|
41
|
+
{ time: '12:00', title: '喜宴开席', desc: '薄酒佳肴 · 与君同乐' },
|
|
42
|
+
{ time: '13:00', title: '新人敬酒', desc: '逐桌致谢 · 感念相伴' }
|
|
43
|
+
],
|
|
44
|
+
rsvp: {
|
|
45
|
+
deadline: '', // 留空 = 婚礼前 10 天;也可写「10月8日」
|
|
46
|
+
endpoint: '', // 可选:收集回执的接口(POST JSON);留空则保存在宾客本机并生成回执文本
|
|
47
|
+
maxGuests: 6
|
|
48
|
+
},
|
|
49
|
+
photos: [], // 可选:[{ src: 'photos/1.jpg', alt: '湖边的合照' }];为空则不显示相册
|
|
50
|
+
contacts: [], // 可选:[{ role: '新郎', name: '顾北辰', phone: '13800000000' }]
|
|
51
|
+
music: { src: '' } // 可选:背景音乐 mp3 路径;留空用内置八音盒《卡农》(无需任何文件)
|
|
52
|
+
};
|
|
53
|
+
document.documentElement.classList.add('js');
|
|
54
|
+
document.documentElement.setAttribute('data-theme', window.INVITE.theme || 'cinnabar');
|
|
55
|
+
</script>
|
|
56
|
+
<style>
|
|
57
|
+
/* ── 设计令牌:换风格只动这里 */
|
|
58
|
+
:root {
|
|
59
|
+
--paper: #F7F1E6; --paper-2: #EFE5D3; --card: #FBF7EF;
|
|
60
|
+
--ink: #2B2420; --ink-2: #554840; --ink-3: #6F6053;
|
|
61
|
+
--accent: #B3302A; --accent-deep: #8C1F18; --accent-text: #B3302A; --on-accent: #FBEFD9;
|
|
62
|
+
--gold: #9A7438; --gold-text: #7D5E2B; --gold-soft: #D8BF8C; --line: rgba(154, 116, 56, .38);
|
|
63
|
+
--hill: #E6D8BF; --water: #D9C7A6; --petal: 196, 72, 64;
|
|
64
|
+
--font-display: "STKaiti", "Kaiti SC", "KaiTi", "BiauKai", "Songti SC", "STSong", "Noto Serif SC", "Source Han Serif SC", "Noto Serif CJK SC", serif;
|
|
65
|
+
--font-serif: "Songti SC", "STSong", "Noto Serif SC", "Source Han Serif SC", "Noto Serif CJK SC", "SimSun", serif;
|
|
66
|
+
--font-sans: -apple-system, BlinkMacSystemFont, "PingFang SC", "HarmonyOS Sans SC", "MiSans", "Microsoft YaHei", "Noto Sans SC", sans-serif;
|
|
67
|
+
--font-num: "Didot", "Bodoni 72", "Bodoni MT", "Times New Roman", var(--font-serif);
|
|
68
|
+
--step--1: 13px; --step-0: 16px; --step-1: clamp(18px, 4.8vw, 20px);
|
|
69
|
+
--step-2: clamp(22px, 6vw, 26px); --step-3: clamp(30px, 8.6vw, 36px); --step-4: clamp(40px, 12vw, 54px);
|
|
70
|
+
--radius: 14px; --shadow: 0 14px 36px -18px rgba(70, 36, 16, .35);
|
|
71
|
+
--ease: cubic-bezier(.2, .7, .2, 1); --maxw: 440px;
|
|
72
|
+
--meander: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='44' height='44' viewBox='0 0 44 44'%3E%3Cpath d='M6 6h14v14H10V10h6v6M24 24h14v14H28V28h6v6' fill='none' stroke='%23fff' stroke-opacity='.09' stroke-width='1.2'/%3E%3C/svg%3E"); /* 回纹底纹:白色低透明,叠在任何主色上都成立 */
|
|
73
|
+
}
|
|
74
|
+
[data-theme="champagne"] {
|
|
75
|
+
--paper: #FBF7F2; --paper-2: #F3EBE1; --card: #FFFDFA;
|
|
76
|
+
--ink: #2E2A28; --ink-2: #57504B; --ink-3: #6E6560;
|
|
77
|
+
--accent: #9A5A60; --accent-deep: #7E4349; --accent-text: #7E4349; --on-accent: #FFF8F2;
|
|
78
|
+
--gold: #8F7348; --gold-text: #77603B; --gold-soft: #D9C6A3; --line: rgba(143, 115, 72, .36);
|
|
79
|
+
--hill: #EFE3D6; --water: #E2D2BF; --petal: 214, 150, 150;
|
|
80
|
+
}
|
|
81
|
+
[data-theme="sage"] {
|
|
82
|
+
--paper: #F5F4EE; --paper-2: #E9EBE0; --card: #FBFBF7;
|
|
83
|
+
--ink: #232923; --ink-2: #4A5248; --ink-3: #606A5E;
|
|
84
|
+
--accent: #5E7461; --accent-deep: #3F5243; --accent-text: #3F5243; --on-accent: #F6F5EE;
|
|
85
|
+
--gold: #8A7A4E; --gold-text: #6E6139; --gold-soft: #CFC6A2; --line: rgba(94, 116, 97, .34);
|
|
86
|
+
--hill: #DDE3D2; --water: #CBD5C0; --petal: 236, 214, 196;
|
|
87
|
+
}
|
|
88
|
+
[data-theme="ink"] {
|
|
89
|
+
--paper: #F6F4EF; --paper-2: #ECE9E2; --card: #FBFAF7;
|
|
90
|
+
--ink: #1F1E1C; --ink-2: #46443F; --ink-3: #5F5C56;
|
|
91
|
+
--accent: #A3312B; --accent-deep: #7D211C; --accent-text: #A3312B; --on-accent: #FAF6EE;
|
|
92
|
+
--gold: #7F6B45; --gold-text: #6E5C3A; --gold-soft: #CDBF9F; --line: rgba(31, 30, 28, .22);
|
|
93
|
+
--hill: #E3E0D8; --water: #D6D2C8; --petal: 163, 49, 43;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/* ── 基础 */
|
|
97
|
+
*, *::before, *::after { box-sizing: border-box; }
|
|
98
|
+
html { -webkit-text-size-adjust: 100%; text-size-adjust: 100%; }
|
|
99
|
+
body {
|
|
100
|
+
margin: 0; background: var(--paper); color: var(--ink);
|
|
101
|
+
font: 400 var(--step-0)/1.8 var(--font-sans); letter-spacing: .02em;
|
|
102
|
+
-webkit-font-smoothing: antialiased; -webkit-tap-highlight-color: transparent;
|
|
103
|
+
}
|
|
104
|
+
[hidden] { display: none !important; }
|
|
105
|
+
html.js body.is-locked { overflow: hidden; }
|
|
106
|
+
img, svg { display: block; max-width: 100%; }
|
|
107
|
+
button, input, textarea, select { font: inherit; color: inherit; }
|
|
108
|
+
button { cursor: pointer; }
|
|
109
|
+
h1, h2, h3, p, figure, ol, ul { margin: 0; }
|
|
110
|
+
ol, ul { padding: 0; list-style: none; }
|
|
111
|
+
:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 6px; }
|
|
112
|
+
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
|
|
113
|
+
|
|
114
|
+
.app { position: relative; max-width: var(--maxw); margin: 0 auto; background: var(--paper); overflow: clip; }
|
|
115
|
+
/* 宣纸纤维:极淡的噪点,只为让大面积纯色「有质感」 */
|
|
116
|
+
.app::before {
|
|
117
|
+
content: ""; position: absolute; inset: 0; pointer-events: none; opacity: .5; mix-blend-mode: multiply;
|
|
118
|
+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 .5 0 0 0 0 .42 0 0 0 0 .3 0 0 0 .07 0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/* ── 按钮 */
|
|
122
|
+
.btn {
|
|
123
|
+
display: inline-flex; align-items: center; justify-content: center; gap: 8px;
|
|
124
|
+
min-height: 46px; padding: 0 20px; border-radius: 999px;
|
|
125
|
+
border: 1px solid var(--accent); background: transparent; color: var(--accent-text);
|
|
126
|
+
font-size: 15px; letter-spacing: .08em; text-decoration: none;
|
|
127
|
+
transition: transform .15s var(--ease), background-color .2s, color .2s;
|
|
128
|
+
}
|
|
129
|
+
.btn:active { transform: scale(.97); }
|
|
130
|
+
.btn--solid { background: var(--accent); color: var(--on-accent); }
|
|
131
|
+
.btn--block { width: 100%; min-height: 52px; font-size: 16px; }
|
|
132
|
+
.btn[disabled] { opacity: .55; cursor: default; }
|
|
133
|
+
.btn svg { width: 18px; height: 18px; flex: none; }
|
|
134
|
+
|
|
135
|
+
/* ── 封面(点开请柬) */
|
|
136
|
+
.gate { position: fixed; inset: 0; z-index: 50; display: grid; place-items: center; perspective: 1400px; touch-action: none; }
|
|
137
|
+
html:not(.js) .gate { display: none; }
|
|
138
|
+
.gate[hidden] { display: none; }
|
|
139
|
+
.gate__door {
|
|
140
|
+
position: absolute; top: 0; bottom: 0; width: 50.2%;
|
|
141
|
+
background:
|
|
142
|
+
radial-gradient(140% 70% at 50% 50%, rgba(255, 255, 255, .07), transparent 60%),
|
|
143
|
+
var(--meander),
|
|
144
|
+
var(--accent);
|
|
145
|
+
transition: transform 1.25s var(--ease) .3s;
|
|
146
|
+
backface-visibility: hidden;
|
|
147
|
+
}
|
|
148
|
+
.gate__door::before { content: ""; position: absolute; top: 16px; bottom: 16px; border: 1px solid rgba(255, 255, 255, .26); }
|
|
149
|
+
.gate__door--l { left: 0; transform-origin: left center; box-shadow: inset -1px 0 0 rgba(0, 0, 0, .18); }
|
|
150
|
+
.gate__door--l::before { left: 16px; right: 0; border-right: 0; }
|
|
151
|
+
.gate__door--r { right: 0; transform-origin: right center; box-shadow: inset 1px 0 0 rgba(255, 255, 255, .12); }
|
|
152
|
+
.gate__door--r::before { right: 16px; left: 0; border-left: 0; }
|
|
153
|
+
.gate__content {
|
|
154
|
+
position: relative; z-index: 1; display: flex; flex-direction: column; align-items: center;
|
|
155
|
+
padding: 24px 28px calc(24px + env(safe-area-inset-bottom)); color: var(--on-accent); text-align: center;
|
|
156
|
+
transition: opacity .45s var(--ease), transform .45s var(--ease);
|
|
157
|
+
}
|
|
158
|
+
.gate__kicker { font-size: 12px; letter-spacing: .42em; margin-right: -.42em; }
|
|
159
|
+
.gate__medal {
|
|
160
|
+
display: grid; place-items: center; width: 104px; height: 104px; margin: 30px 0 26px; border-radius: 50%;
|
|
161
|
+
background: var(--gold-soft); color: var(--accent-deep);
|
|
162
|
+
box-shadow: 0 0 0 6px rgba(255, 255, 255, .1), 0 0 0 7px rgba(216, 191, 140, .55), 0 18px 40px -12px rgba(0, 0, 0, .45);
|
|
163
|
+
font: 56px/1 var(--font-display);
|
|
164
|
+
}
|
|
165
|
+
.gate__names { font: 400 34px/1.3 var(--font-display); color: var(--gold-soft); letter-spacing: .12em; }
|
|
166
|
+
.gate__names i { font-style: normal; margin: 0 .35em; opacity: .8; }
|
|
167
|
+
.gate__to { margin-top: 12px; font-size: 15px; letter-spacing: .12em; }
|
|
168
|
+
.gate__date { margin-top: 6px; font: 18px/1.6 var(--font-num); letter-spacing: .24em; }
|
|
169
|
+
.gate__btn {
|
|
170
|
+
margin-top: 40px; min-height: 50px; padding: 0 34px; border-radius: 999px;
|
|
171
|
+
border: 1px solid var(--gold-soft); background: rgba(0, 0, 0, .12); color: var(--on-accent);
|
|
172
|
+
font-size: 16px; letter-spacing: .3em; text-indent: .3em;
|
|
173
|
+
animation: breathe 2.6s ease-in-out infinite;
|
|
174
|
+
}
|
|
175
|
+
@keyframes breathe { 50% { box-shadow: 0 0 0 8px rgba(216, 191, 140, .14); } }
|
|
176
|
+
.gate.is-opening .gate__content { opacity: 0; transform: scale(.96); }
|
|
177
|
+
.gate.is-opening .gate__door--l { transform: rotateY(-108deg); }
|
|
178
|
+
.gate.is-opening .gate__door--r { transform: rotateY(108deg); }
|
|
179
|
+
.gate.is-gone { opacity: 0; transition: opacity .4s ease; pointer-events: none; }
|
|
180
|
+
|
|
181
|
+
/* ── 音乐按钮 */
|
|
182
|
+
.music {
|
|
183
|
+
position: fixed; z-index: 40; top: calc(12px + env(safe-area-inset-top)); right: max(12px, calc(50vw - var(--maxw) / 2 + 12px));
|
|
184
|
+
width: 44px; height: 44px; padding: 0; border-radius: 50%; display: grid; place-items: center;
|
|
185
|
+
border: 1px solid var(--line); background: rgba(255, 255, 255, .72); color: var(--accent-text);
|
|
186
|
+
-webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px); box-shadow: var(--shadow);
|
|
187
|
+
}
|
|
188
|
+
.music svg { width: 22px; height: 22px; }
|
|
189
|
+
.music.is-playing svg { animation: spin 5s linear infinite; }
|
|
190
|
+
.music::after { content: ""; position: absolute; width: 24px; height: 1.5px; background: currentColor; transform: rotate(-45deg); transition: opacity .2s; }
|
|
191
|
+
.music.is-playing::after { opacity: 0; }
|
|
192
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
193
|
+
.petals { position: fixed; inset: 0; z-index: 30; pointer-events: none; }
|
|
194
|
+
|
|
195
|
+
/* ── 首屏 */
|
|
196
|
+
.hero {
|
|
197
|
+
position: relative; min-height: 100vh; min-height: 100dvh;
|
|
198
|
+
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
|
199
|
+
padding: calc(72px + env(safe-area-inset-top)) 28px calc(96px + env(safe-area-inset-bottom)); text-align: center;
|
|
200
|
+
background: radial-gradient(120% 70% at 50% 38%, var(--card), var(--paper) 70%);
|
|
201
|
+
}
|
|
202
|
+
.corner { position: absolute; width: 58px; height: 58px; color: var(--gold); opacity: .8; }
|
|
203
|
+
.corner--tl { top: calc(16px + env(safe-area-inset-top)); left: 16px; }
|
|
204
|
+
.corner--tr { top: calc(16px + env(safe-area-inset-top)); right: 16px; transform: scaleX(-1); }
|
|
205
|
+
.corner--bl { bottom: 16px; left: 16px; transform: scaleY(-1); }
|
|
206
|
+
.corner--br { bottom: 16px; right: 16px; transform: scale(-1); }
|
|
207
|
+
.branch { position: absolute; top: calc(30px + env(safe-area-inset-top)); right: -8px; width: 58%; max-width: 250px; pointer-events: none; }
|
|
208
|
+
.hero__kicker { font-size: 12px; letter-spacing: .42em; margin-right: -.42em; color: var(--ink-3); }
|
|
209
|
+
.hero__seal {
|
|
210
|
+
width: 62px; height: 62px; margin: 26px auto 22px; display: grid; place-items: center; border-radius: 8px;
|
|
211
|
+
background: var(--accent); color: var(--on-accent); font: 38px/1 var(--font-display);
|
|
212
|
+
box-shadow: inset 0 0 0 3px var(--accent), inset 0 0 0 4.5px rgba(255, 255, 255, .55); transform: rotate(-4deg);
|
|
213
|
+
}
|
|
214
|
+
html.js body:not(.is-open) .hero__seal { opacity: 0; }
|
|
215
|
+
body.is-open .hero__seal { animation: stamp .75s var(--ease) .25s both; }
|
|
216
|
+
@keyframes stamp { from { opacity: 0; transform: scale(1.9) rotate(-14deg); } to { opacity: 1; transform: scale(1) rotate(-4deg); } }
|
|
217
|
+
.hero__names { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: 0 .22em; font: 400 var(--step-4)/1.2 var(--font-display); letter-spacing: .08em; }
|
|
218
|
+
.hero__names.is-long { font-size: var(--step-3); }
|
|
219
|
+
.hero__amp { font: italic 400 .52em/1 var(--font-num); color: var(--gold-text); padding-top: .2em; }
|
|
220
|
+
.hero__tagline { margin-top: 14px; font: 400 var(--step-1)/1.6 var(--font-serif); color: var(--ink-2); letter-spacing: .24em; margin-right: -.24em; }
|
|
221
|
+
.hero__date { display: flex; align-items: center; gap: 14px; margin-top: 34px; font: 400 var(--step-3)/1 var(--font-num); letter-spacing: .06em; }
|
|
222
|
+
.hero__date::before, .hero__date::after { content: ""; width: 34px; height: 1px; background: var(--line); }
|
|
223
|
+
.hero__meta { margin-top: 14px; font-size: 15px; color: var(--ink-2); letter-spacing: .06em; text-wrap: balance; }
|
|
224
|
+
.hero__place { margin-top: 6px; font: 400 16px/1.7 var(--font-serif); color: var(--accent-text); letter-spacing: .16em; }
|
|
225
|
+
.scroll-hint {
|
|
226
|
+
position: absolute; left: 50%; bottom: calc(22px + env(safe-area-inset-bottom)); transform: translateX(-50%);
|
|
227
|
+
display: flex; flex-direction: column; align-items: center; gap: 2px; min-width: 44px; min-height: 44px; padding: 4px 8px;
|
|
228
|
+
font-size: 12px; letter-spacing: .3em; color: var(--ink-3); text-decoration: none;
|
|
229
|
+
}
|
|
230
|
+
.scroll-hint svg { width: 18px; height: 18px; animation: nudge 1.8s ease-in-out infinite; }
|
|
231
|
+
@keyframes nudge { 50% { transform: translateY(5px); } }
|
|
232
|
+
|
|
233
|
+
/* ── 通用区块 */
|
|
234
|
+
.section { position: relative; padding: 84px 26px; }
|
|
235
|
+
.section--tint { background: var(--paper-2); }
|
|
236
|
+
.head { text-align: center; margin-bottom: 38px; }
|
|
237
|
+
.head__en { display: block; font-size: 12px; letter-spacing: .4em; margin-right: -.4em; color: var(--ink-3); }
|
|
238
|
+
.head__cn { margin-top: 8px; font: 400 var(--step-2)/1.4 var(--font-display); letter-spacing: .3em; margin-right: -.3em; }
|
|
239
|
+
.head__rule { display: flex; align-items: center; justify-content: center; gap: 10px; margin-top: 14px; color: var(--gold); }
|
|
240
|
+
.head__rule::before, .head__rule::after { content: ""; width: 42px; height: 1px; background: var(--line); }
|
|
241
|
+
.head__rule i { width: 7px; height: 7px; background: currentColor; transform: rotate(45deg); }
|
|
242
|
+
|
|
243
|
+
/* 入场:默认可见;脚本启用后才先藏起来(脚本出错时内容不会永远看不见) */
|
|
244
|
+
html.js .reveal { opacity: 0; transform: translateY(18px); transition: opacity .9s var(--ease), transform .9s var(--ease); transition-delay: calc(var(--d, 0) * 110ms); }
|
|
245
|
+
html.js .reveal.is-in { opacity: 1; transform: none; }
|
|
246
|
+
|
|
247
|
+
/* ── 请柬正文 */
|
|
248
|
+
.letter {
|
|
249
|
+
position: relative; padding: 40px 24px 34px; background: var(--card); text-align: center;
|
|
250
|
+
border: 1px solid var(--line); border-radius: 4px; box-shadow: var(--shadow);
|
|
251
|
+
}
|
|
252
|
+
.letter::before { content: ""; position: absolute; inset: 7px; border: 1px solid var(--line); border-radius: 2px; pointer-events: none; }
|
|
253
|
+
.letter__to { font: 400 var(--step-1)/1.6 var(--font-display); color: var(--accent-text); margin-bottom: 18px; letter-spacing: .12em; }
|
|
254
|
+
.letter__body { font: 400 16px/2.15 var(--font-serif); color: var(--ink); letter-spacing: .08em; }
|
|
255
|
+
.letter__body b { font-weight: 600; color: var(--accent-text); }
|
|
256
|
+
.letter__body .big { display: block; margin: 6px 0; font: 400 var(--step-2)/1.7 var(--font-display); letter-spacing: .2em; color: var(--ink); }
|
|
257
|
+
.letter__sign { margin-top: 26px; display: flex; justify-content: center; align-items: baseline; gap: 10px; font: 400 var(--step-1)/1.4 var(--font-display); }
|
|
258
|
+
.letter__sign small { font: 13px/1 var(--font-sans); color: var(--ink-3); letter-spacing: .2em; }
|
|
259
|
+
|
|
260
|
+
/* ── 倒计时 */
|
|
261
|
+
.count { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px; }
|
|
262
|
+
.count__cell { padding: 16px 0 12px; text-align: center; background: var(--card); border: 1px solid var(--line); border-radius: var(--radius); }
|
|
263
|
+
.count__num { display: block; font: 400 30px/1.1 var(--font-num); font-variant-numeric: tabular-nums; color: var(--ink); }
|
|
264
|
+
.count__num.tick { animation: tick .45s var(--ease); }
|
|
265
|
+
@keyframes tick { from { opacity: .2; transform: translateY(-6px); } }
|
|
266
|
+
.count__unit { display: block; margin-top: 6px; font-size: 13px; color: var(--ink-3); letter-spacing: .2em; margin-right: -.2em; }
|
|
267
|
+
.count__note { margin-top: 20px; text-align: center; font: 400 16px/1.8 var(--font-serif); color: var(--ink-2); letter-spacing: .1em; }
|
|
268
|
+
.count__actions { display: flex; justify-content: center; margin-top: 22px; }
|
|
269
|
+
|
|
270
|
+
/* ── 流程 */
|
|
271
|
+
.flow { position: relative; margin-left: 6px; }
|
|
272
|
+
.flow::before { content: ""; position: absolute; left: 76px; top: 10px; bottom: 10px; width: 1px; background: var(--line); }
|
|
273
|
+
.flow__item { position: relative; display: grid; grid-template-columns: 62px minmax(0, 1fr); gap: 30px; padding: 12px 0; }
|
|
274
|
+
.flow__time { font: 400 19px/1.5 var(--font-num); text-align: right; color: var(--ink-2); font-variant-numeric: tabular-nums; }
|
|
275
|
+
.flow__item::after { content: ""; position: absolute; left: 72px; top: 21px; width: 9px; height: 9px; border-radius: 50%; background: var(--paper-2); border: 1px solid var(--gold); }
|
|
276
|
+
.flow__item.is-key::after { background: var(--accent); border-color: var(--accent); box-shadow: 0 0 0 4px rgba(var(--petal), .16); }
|
|
277
|
+
.flow__item.is-key .flow__time { color: var(--accent-text); }
|
|
278
|
+
.flow__title { font: 400 18px/1.5 var(--font-serif); letter-spacing: .1em; }
|
|
279
|
+
.flow__desc { font-size: 14px; color: var(--ink-2); line-height: 1.7; }
|
|
280
|
+
|
|
281
|
+
/* ── 地点 */
|
|
282
|
+
.venue { overflow: hidden; background: var(--card); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow); }
|
|
283
|
+
.venue__art { aspect-ratio: 32 / 15; width: 100%; height: auto; }
|
|
284
|
+
.venue__body { padding: 22px 22px 24px; }
|
|
285
|
+
.venue__name { font: 400 var(--step-2)/1.4 var(--font-display); letter-spacing: .12em; }
|
|
286
|
+
.venue__hall { margin-left: .4em; font: 400 16px var(--font-serif); color: var(--accent-text); letter-spacing: .1em; }
|
|
287
|
+
.venue__addr { margin-top: 6px; font-size: 15px; color: var(--ink-2); }
|
|
288
|
+
.venue__tips { margin-top: 10px; font-size: 14px; line-height: 1.7; color: var(--ink-3); }
|
|
289
|
+
.venue__nav { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; margin-top: 20px; }
|
|
290
|
+
.venue__nav .btn { padding: 0 10px; font-size: 14px; letter-spacing: .04em; }
|
|
291
|
+
.pin-ring { transform-box: fill-box; transform-origin: center; animation: ring 2.4s ease-out infinite; }
|
|
292
|
+
@keyframes ring { from { transform: scale(.4); opacity: .7; } to { transform: scale(1.6); opacity: 0; } }
|
|
293
|
+
|
|
294
|
+
/* ── 诗 */
|
|
295
|
+
.poem { position: relative; padding: 76px 26px 64px; background: var(--accent); color: var(--on-accent); text-align: center; overflow: hidden; }
|
|
296
|
+
.poem::before {
|
|
297
|
+
content: ""; position: absolute; inset: 0; opacity: .9;
|
|
298
|
+
background: var(--meander);
|
|
299
|
+
}
|
|
300
|
+
.poem__cols {
|
|
301
|
+
position: relative; display: inline-block; height: 5.6em; text-align: start;
|
|
302
|
+
writing-mode: vertical-rl; font: 400 24px/1.5 var(--font-display); letter-spacing: .34em;
|
|
303
|
+
}
|
|
304
|
+
.poem__cols p { margin-block-end: .9em; }
|
|
305
|
+
.poem__cols p:last-child { margin-block-end: 0; }
|
|
306
|
+
.poem__src { position: relative; margin-top: 26px; font-size: 13px; letter-spacing: .3em; margin-right: -.3em; }
|
|
307
|
+
.poem__seal {
|
|
308
|
+
position: relative; display: inline-block; height: 3.1em; margin-top: 18px; padding: 6px 5px;
|
|
309
|
+
writing-mode: vertical-rl; font: 17px/1.15 var(--font-display); letter-spacing: 0;
|
|
310
|
+
color: var(--accent-deep); background: var(--gold-soft); border-radius: 3px;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/* ── 相册(有照片才出现) */
|
|
314
|
+
.album { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
|
|
315
|
+
.album__item { position: relative; overflow: hidden; border-radius: 10px; background: var(--paper-2); aspect-ratio: 3 / 4; padding: 0; border: 0; }
|
|
316
|
+
.album__item:first-child { grid-column: span 2; aspect-ratio: 4 / 3; }
|
|
317
|
+
.album__item img { width: 100%; height: 100%; object-fit: cover; transition: transform .6s var(--ease); }
|
|
318
|
+
.album__item:active img { transform: scale(1.03); }
|
|
319
|
+
.lightbox { position: fixed; inset: 0; z-index: 60; display: grid; place-items: center; background: rgba(20, 14, 10, .92); padding: 24px; }
|
|
320
|
+
.lightbox[hidden] { display: none; }
|
|
321
|
+
.lightbox img { max-height: 82vh; border-radius: 6px; object-fit: contain; }
|
|
322
|
+
.lightbox__close { position: absolute; top: calc(12px + env(safe-area-inset-top)); right: 12px; width: 44px; height: 44px; border-radius: 50%; border: 0; background: rgba(255, 255, 255, .14); color: #fff; font-size: 22px; }
|
|
323
|
+
|
|
324
|
+
/* ── 回执 */
|
|
325
|
+
.rsvp__lead { margin: -14px 0 26px; text-align: center; font-size: 15px; color: var(--ink-2); }
|
|
326
|
+
.form { display: grid; gap: 18px; padding: 26px 22px; background: var(--card); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow); }
|
|
327
|
+
.field { display: grid; gap: 8px; margin: 0; padding: 0; border: 0; min-width: 0; }
|
|
328
|
+
.field > label, .field > legend { padding: 0; font-size: 15px; color: var(--ink-2); letter-spacing: .06em; }
|
|
329
|
+
.field em { font-style: normal; color: var(--accent-text); }
|
|
330
|
+
.input {
|
|
331
|
+
width: 100%; min-height: 48px; padding: 10px 14px; border-radius: 12px;
|
|
332
|
+
border: 1px solid rgba(43, 36, 32, .2); background: #fff; font-size: 16px; line-height: 1.5;
|
|
333
|
+
transition: border-color .2s, box-shadow .2s; -webkit-appearance: none; appearance: none;
|
|
334
|
+
}
|
|
335
|
+
.input::placeholder { color: #776A5F; }
|
|
336
|
+
.input:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px rgba(var(--petal), .16); }
|
|
337
|
+
.input[aria-invalid="true"] { border-color: var(--accent-deep); }
|
|
338
|
+
textarea.input { min-height: 96px; resize: vertical; }
|
|
339
|
+
.seg { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
|
|
340
|
+
.seg__item { position: relative; }
|
|
341
|
+
.seg__item input { position: absolute; inset: 0; opacity: 0; margin: 0; cursor: pointer; }
|
|
342
|
+
.seg__item span {
|
|
343
|
+
display: grid; place-items: center; min-height: 48px; border-radius: 12px; border: 1px solid rgba(43, 36, 32, .2);
|
|
344
|
+
background: #fff; font-size: 15px; letter-spacing: .12em; transition: all .2s;
|
|
345
|
+
}
|
|
346
|
+
.seg__item input:checked + span { border-color: var(--accent); background: var(--accent); color: var(--on-accent); }
|
|
347
|
+
.seg__item input:focus-visible + span { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
348
|
+
.stepper { display: grid; grid-template-columns: 48px minmax(0, 1fr) 48px; gap: 8px; }
|
|
349
|
+
.stepper .input { text-align: center; font: 400 18px var(--font-num); -moz-appearance: textfield; }
|
|
350
|
+
.stepper .input::-webkit-outer-spin-button, .stepper .input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
|
|
351
|
+
.stepper__btn { min-height: 48px; border-radius: 12px; border: 1px solid rgba(43, 36, 32, .2); background: #fff; font-size: 22px; line-height: 1; }
|
|
352
|
+
.field[aria-disabled="true"] { opacity: .45; }
|
|
353
|
+
.field__err { min-height: 0; font-size: 13px; color: var(--accent-deep); }
|
|
354
|
+
.field__err:empty { display: none; }
|
|
355
|
+
.field__count { justify-self: end; font-size: 13px; color: var(--ink-3); }
|
|
356
|
+
.done { padding: 30px 24px; text-align: center; background: var(--card); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow); }
|
|
357
|
+
.done[hidden] { display: none; }
|
|
358
|
+
.done__mark { width: 60px; height: 60px; margin: 0 auto 14px; display: grid; place-items: center; border-radius: 50%; background: var(--accent); color: var(--on-accent); font: 30px/1 var(--font-display); }
|
|
359
|
+
.done__title { font: 400 var(--step-2)/1.5 var(--font-display); letter-spacing: .12em; }
|
|
360
|
+
.done__name { margin-top: 2px; font: 400 var(--step-1)/1.5 var(--font-display); color: var(--accent-text); letter-spacing: .1em; overflow-wrap: anywhere; }
|
|
361
|
+
.done__text { margin-top: 10px; font-size: 15px; color: var(--ink-2); white-space: pre-line; }
|
|
362
|
+
.done__actions { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 22px; }
|
|
363
|
+
|
|
364
|
+
/* ── 联系与结尾 */
|
|
365
|
+
.contacts { display: grid; gap: 10px; margin-top: 26px; }
|
|
366
|
+
.footer { padding: 76px 26px calc(56px + env(safe-area-inset-bottom)); text-align: center; background: var(--paper-2); }
|
|
367
|
+
.footer__thanks { font: 400 var(--step-1)/1.8 var(--font-serif); color: var(--ink-2); letter-spacing: .2em; margin-right: -.2em; }
|
|
368
|
+
.footer__names { margin-top: 10px; font: 400 var(--step-3)/1.3 var(--font-display); letter-spacing: .12em; }
|
|
369
|
+
.footer__date { margin-top: 6px; font: 400 16px/1.6 var(--font-num); letter-spacing: .3em; color: var(--ink-3); }
|
|
370
|
+
.footer__actions { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 30px; }
|
|
371
|
+
|
|
372
|
+
/* ── 轻提示 / 分享引导 */
|
|
373
|
+
.toast {
|
|
374
|
+
position: fixed; left: 50%; bottom: calc(28px + env(safe-area-inset-bottom)); z-index: 70; max-width: min(86vw, 360px);
|
|
375
|
+
padding: 11px 20px; border-radius: 999px; background: rgba(43, 36, 32, .92); color: var(--paper);
|
|
376
|
+
font-size: 14px; text-align: center; transform: translate(-50%, 16px); opacity: 0; visibility: hidden;
|
|
377
|
+
transition: opacity .25s, transform .25s var(--ease), visibility .25s;
|
|
378
|
+
}
|
|
379
|
+
.toast.is-on { opacity: 1; visibility: visible; transform: translate(-50%, 0); }
|
|
380
|
+
.guide { position: fixed; inset: 0; z-index: 80; background: rgba(20, 14, 10, .82); color: #fff; }
|
|
381
|
+
.guide[hidden] { display: none; }
|
|
382
|
+
.guide svg { position: absolute; top: calc(8px + env(safe-area-inset-top)); right: 26px; width: 92px; height: 120px; }
|
|
383
|
+
.guide p { position: absolute; top: calc(140px + env(safe-area-inset-top)); left: 0; right: 0; text-align: center; font: 400 20px/1.9 var(--font-serif); letter-spacing: .12em; }
|
|
384
|
+
|
|
385
|
+
/* ── 桌面:请柬居中成一张「手机卡片」 */
|
|
386
|
+
@media (min-width: 720px) {
|
|
387
|
+
body {
|
|
388
|
+
background:
|
|
389
|
+
var(--meander),
|
|
390
|
+
var(--accent-deep);
|
|
391
|
+
}
|
|
392
|
+
.app { margin: 48px auto; border-radius: 28px; box-shadow: 0 40px 90px -30px rgba(0, 0, 0, .55); }
|
|
393
|
+
.music { top: 60px; }
|
|
394
|
+
.gate__kicker { font-size: 14px; }
|
|
395
|
+
.gate__medal { width: 132px; height: 132px; font-size: 72px; }
|
|
396
|
+
.gate__names { font-size: 46px; }
|
|
397
|
+
.gate__to { font-size: 18px; }
|
|
398
|
+
.gate__date { font-size: 22px; }
|
|
399
|
+
.hero { min-height: 780px; }
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/* ── 减弱动效:晕动症用户、系统开了「减少动态效果」 */
|
|
403
|
+
@media (prefers-reduced-motion: reduce) {
|
|
404
|
+
*, *::before, *::after { animation-duration: .01ms !important; animation-iteration-count: 1 !important; transition-duration: .01ms !important; transition-delay: 0s !important; scroll-behavior: auto !important; }
|
|
405
|
+
html.js .reveal { opacity: 1; transform: none; }
|
|
406
|
+
}
|
|
407
|
+
</style>
|
|
408
|
+
</head>
|
|
409
|
+
<body class="is-locked">
|
|
410
|
+
<svg width="0" height="0" style="position:absolute" aria-hidden="true">
|
|
411
|
+
<symbol id="corner" viewBox="0 0 58 58"><path d="M2 56V14Q2 2 14 2h42M8 56V20Q8 8 20 8h36M14 14h8v8" fill="none" stroke="currentColor" stroke-width="1.1"/></symbol>
|
|
412
|
+
<defs><g id="bloom" fill="var(--accent)"><circle r="6" cx="0" cy="-6"/><circle r="6" cx="5.7" cy="-1.9"/><circle r="6" cx="3.5" cy="4.9"/><circle r="6" cx="-3.5" cy="4.9"/><circle r="6" cx="-5.7" cy="-1.9"/><circle r="2.6" fill="var(--gold-soft)"/></g></defs>
|
|
413
|
+
</svg>
|
|
414
|
+
|
|
415
|
+
<!-- 封面:点开请柬(首次点击同时作为播放音乐的用户手势) -->
|
|
416
|
+
<div class="gate" id="gate" role="dialog" aria-modal="true" aria-labelledby="gateNames">
|
|
417
|
+
<div class="gate__door gate__door--l" aria-hidden="true"></div>
|
|
418
|
+
<div class="gate__door gate__door--r" aria-hidden="true"></div>
|
|
419
|
+
<div class="gate__content">
|
|
420
|
+
<p class="gate__kicker">WEDDING INVITATION</p>
|
|
421
|
+
<div class="gate__medal" aria-hidden="true">囍</div>
|
|
422
|
+
<p class="gate__names" id="gateNames"><span data-bind="groom"></span><i>·</i><span data-bind="bride"></span></p>
|
|
423
|
+
<p class="gate__to" data-bind="gateTo"></p>
|
|
424
|
+
<p class="gate__date" data-bind="dateDots"></p>
|
|
425
|
+
<button class="gate__btn" id="gateOpen" type="button">轻触开启</button>
|
|
426
|
+
</div>
|
|
427
|
+
</div>
|
|
428
|
+
|
|
429
|
+
<button class="music" id="music" type="button" aria-label="播放背景音乐" aria-pressed="false" hidden>
|
|
430
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true"><circle cx="12" cy="12" r="9.2"/><circle cx="12" cy="12" r="2.6"/><path d="M12 2.8a9.2 9.2 0 0 1 8.4 5.4" stroke-linecap="round"/></svg>
|
|
431
|
+
</button>
|
|
432
|
+
<canvas class="petals" id="petals" aria-hidden="true"></canvas>
|
|
433
|
+
|
|
434
|
+
<main class="app" id="app">
|
|
435
|
+
<!-- 首屏 -->
|
|
436
|
+
<header class="hero" id="top">
|
|
437
|
+
<svg class="corner corner--tl" viewBox="0 0 58 58" aria-hidden="true"><use href="#corner"/></svg>
|
|
438
|
+
<svg class="corner corner--tr" viewBox="0 0 58 58" aria-hidden="true"><use href="#corner"/></svg>
|
|
439
|
+
<svg class="corner corner--bl" viewBox="0 0 58 58" aria-hidden="true"><use href="#corner"/></svg>
|
|
440
|
+
<svg class="corner corner--br" viewBox="0 0 58 58" aria-hidden="true"><use href="#corner"/></svg>
|
|
441
|
+
<!-- 梅枝:线条用墨色、花用主色、花蕊用金,换主题自动跟着变 -->
|
|
442
|
+
<svg class="branch" viewBox="0 0 240 170" aria-hidden="true">
|
|
443
|
+
<g fill="none" stroke="var(--ink-2)" stroke-linecap="round" opacity=".78">
|
|
444
|
+
<path d="M242 8C196 18 162 34 138 60 118 82 100 94 72 102 52 108 34 120 18 142" stroke-width="3"/>
|
|
445
|
+
<path d="M166 38c-4 16 0 30 12 42" stroke-width="2"/>
|
|
446
|
+
<path d="M118 80c-12-10-26-14-42-11" stroke-width="1.7"/>
|
|
447
|
+
<path d="M92 97c2 14-2 26-12 36" stroke-width="1.5"/>
|
|
448
|
+
</g>
|
|
449
|
+
<g fill="var(--accent)">
|
|
450
|
+
<use href="#bloom" transform="translate(178 80)"/>
|
|
451
|
+
<use href="#bloom" transform="translate(76 69) scale(.8)"/>
|
|
452
|
+
<use href="#bloom" transform="translate(138 60) scale(.62)" opacity=".9"/>
|
|
453
|
+
<circle cx="80" cy="133" r="3.6"/><circle cx="208" cy="22" r="3.2"/><circle cx="36" cy="124" r="2.8" opacity=".8"/><circle cx="160" cy="44" r="2.6" opacity=".8"/>
|
|
454
|
+
</g>
|
|
455
|
+
</svg>
|
|
456
|
+
|
|
457
|
+
<p class="hero__kicker reveal">WEDDING INVITATION</p>
|
|
458
|
+
<div class="hero__seal" aria-hidden="true">囍</div>
|
|
459
|
+
<h1 class="hero__names reveal" style="--d:1">
|
|
460
|
+
<span data-bind="groom"></span><span class="hero__amp" aria-label="与">&</span><span data-bind="bride"></span>
|
|
461
|
+
</h1>
|
|
462
|
+
<p class="hero__tagline reveal" style="--d:2" data-bind="tagline"></p>
|
|
463
|
+
<p class="hero__date reveal" style="--d:3" data-bind="dateDots"></p>
|
|
464
|
+
<p class="hero__meta reveal" style="--d:4" data-bind="dateMeta"></p>
|
|
465
|
+
<p class="hero__place reveal" style="--d:5" data-bind="placeLine"></p>
|
|
466
|
+
<a class="scroll-hint" href="#letter" aria-label="向下浏览请柬">向下
|
|
467
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true"><path d="M6 9l6 6 6-6" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
468
|
+
</a>
|
|
469
|
+
</header>
|
|
470
|
+
|
|
471
|
+
<!-- 请柬正文 -->
|
|
472
|
+
<section class="section" id="letter" aria-labelledby="letterTitle">
|
|
473
|
+
<div class="head reveal">
|
|
474
|
+
<span class="head__en">INVITATION</span>
|
|
475
|
+
<h2 class="head__cn" id="letterTitle">敬 请 光 临</h2>
|
|
476
|
+
<div class="head__rule" aria-hidden="true"><i></i></div>
|
|
477
|
+
</div>
|
|
478
|
+
<article class="letter reveal" style="--d:1">
|
|
479
|
+
<p class="letter__to" data-bind="letterTo" hidden></p>
|
|
480
|
+
<p class="letter__body" id="letterBody"></p>
|
|
481
|
+
<p class="letter__sign"><span data-bind="groom"></span><span data-bind="bride"></span><small>敬邀</small></p>
|
|
482
|
+
</article>
|
|
483
|
+
</section>
|
|
484
|
+
|
|
485
|
+
<!-- 倒计时 -->
|
|
486
|
+
<section class="section section--tint" id="countdown" aria-labelledby="countTitle">
|
|
487
|
+
<div class="head reveal">
|
|
488
|
+
<span class="head__en">COUNTDOWN</span>
|
|
489
|
+
<h2 class="head__cn" id="countTitle" data-bind="countTitle">婚 礼 倒 计 时</h2>
|
|
490
|
+
<div class="head__rule" aria-hidden="true"><i></i></div>
|
|
491
|
+
</div>
|
|
492
|
+
<div class="count reveal" style="--d:1" id="count" role="timer" aria-live="off">
|
|
493
|
+
<div class="count__cell"><b class="count__num" id="cDay">--</b><span class="count__unit">天</span></div>
|
|
494
|
+
<div class="count__cell"><b class="count__num" id="cHour">--</b><span class="count__unit">时</span></div>
|
|
495
|
+
<div class="count__cell"><b class="count__num" id="cMin">--</b><span class="count__unit">分</span></div>
|
|
496
|
+
<div class="count__cell"><b class="count__num" id="cSec">--</b><span class="count__unit">秒</span></div>
|
|
497
|
+
</div>
|
|
498
|
+
<p class="count__note reveal" style="--d:2" id="countNote"></p>
|
|
499
|
+
<div class="count__actions reveal" style="--d:3">
|
|
500
|
+
<button class="btn" id="calBtn" type="button">
|
|
501
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true"><rect x="3.5" y="5" width="17" height="15.5" rx="2.5"/><path d="M3.5 10h17M8 3v4M16 3v4M12 13v5M9.5 15.5h5" stroke-linecap="round"/></svg>
|
|
502
|
+
添加到日历提醒
|
|
503
|
+
</button>
|
|
504
|
+
</div>
|
|
505
|
+
</section>
|
|
506
|
+
|
|
507
|
+
<!-- 流程 -->
|
|
508
|
+
<section class="section" id="schedule" aria-labelledby="flowTitle">
|
|
509
|
+
<div class="head reveal">
|
|
510
|
+
<span class="head__en">SCHEDULE</span>
|
|
511
|
+
<h2 class="head__cn" id="flowTitle">婚 礼 流 程</h2>
|
|
512
|
+
<div class="head__rule" aria-hidden="true"><i></i></div>
|
|
513
|
+
</div>
|
|
514
|
+
<ol class="flow" id="flow"></ol>
|
|
515
|
+
</section>
|
|
516
|
+
|
|
517
|
+
<!-- 地点 -->
|
|
518
|
+
<section class="section section--tint" id="venue" aria-labelledby="venueTitle">
|
|
519
|
+
<div class="head reveal">
|
|
520
|
+
<span class="head__en">LOCATION</span>
|
|
521
|
+
<h2 class="head__cn" id="venueTitle">婚 宴 地 点</h2>
|
|
522
|
+
<div class="head__rule" aria-hidden="true"><i></i></div>
|
|
523
|
+
</div>
|
|
524
|
+
<div class="venue reveal" style="--d:1">
|
|
525
|
+
<!-- 示意插画而不是假地图:画错的地图比没有地图更糟 -->
|
|
526
|
+
<svg class="venue__art" viewBox="0 0 320 150" role="img" aria-label="婚宴地点示意插画">
|
|
527
|
+
<rect width="320" height="150" fill="var(--paper-2)"/>
|
|
528
|
+
<path d="M0 92C34 66 66 64 100 82c34-30 82-30 118-6 28-16 64-14 102 2v72H0z" fill="var(--hill)"/>
|
|
529
|
+
<path d="M0 112c40-8 86-6 124 0s86 8 124 0 58-8 72-6v44H0z" fill="var(--water)" opacity=".7"/>
|
|
530
|
+
<g stroke="var(--card)" stroke-width="2" stroke-linecap="round" opacity=".9"><path d="M24 126h46M104 134h60M200 124h52M60 142h44M232 140h56"/></g>
|
|
531
|
+
<g fill="none" stroke="var(--ink-2)" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" opacity=".7">
|
|
532
|
+
<path d="M44 96q16-12 32 0M48 96v12M72 96v12M40 108h40"/>
|
|
533
|
+
<path d="M226 82q22-16 44 0M232 82v16M264 82v16M222 98h52M236 72q12-10 24 0"/>
|
|
534
|
+
</g>
|
|
535
|
+
<circle class="pin-ring" cx="160" cy="70" r="16" fill="none" stroke="var(--accent)" stroke-width="1.5"/>
|
|
536
|
+
<path d="M160 88s-15-14-15-25a15 15 0 0 1 30 0c0 11-15 25-15 25z" fill="var(--accent)"/>
|
|
537
|
+
<circle cx="160" cy="63" r="5.4" fill="var(--card)"/>
|
|
538
|
+
</svg>
|
|
539
|
+
<div class="venue__body">
|
|
540
|
+
<h3><span class="venue__name" data-bind="venueName"></span><span class="venue__hall" data-bind="venueHall"></span></h3>
|
|
541
|
+
<p class="venue__addr" data-bind="venueAddr"></p>
|
|
542
|
+
<p class="venue__tips" data-bind="venueTips"></p>
|
|
543
|
+
<div class="venue__nav">
|
|
544
|
+
<a class="btn btn--solid" id="navAmap" href="#" target="_blank" rel="noopener">高德导航</a>
|
|
545
|
+
<a class="btn" id="navBaidu" href="#" target="_blank" rel="noopener">百度导航</a>
|
|
546
|
+
<a class="btn" id="navApple" href="#" target="_blank" rel="noopener">Apple 地图</a>
|
|
547
|
+
<button class="btn" id="copyAddr" type="button">复制地址</button>
|
|
548
|
+
</div>
|
|
549
|
+
</div>
|
|
550
|
+
</div>
|
|
551
|
+
</section>
|
|
552
|
+
|
|
553
|
+
<!-- 诗:用经典代替编造的「恋爱故事」(私人经历不该由我们杜撰) -->
|
|
554
|
+
<section class="poem" aria-labelledby="poemSrc">
|
|
555
|
+
<div class="poem__cols reveal" lang="zh-CN"><p>桃之夭夭</p><p>灼灼其华</p><p>之子于归</p><p>宜其室家</p></div>
|
|
556
|
+
<p class="poem__src reveal" style="--d:2" id="poemSrc">《诗经 · 周南 · 桃夭》</p>
|
|
557
|
+
<div><p class="poem__seal reveal" style="--d:3" aria-hidden="true">宜室宜家</p></div>
|
|
558
|
+
</section>
|
|
559
|
+
|
|
560
|
+
<!-- 相册:INVITE.photos 为空时整块不出现(不给宾客看空框) -->
|
|
561
|
+
<section class="section" id="album" aria-labelledby="albumTitle" hidden>
|
|
562
|
+
<div class="head reveal">
|
|
563
|
+
<span class="head__en">MOMENTS</span>
|
|
564
|
+
<h2 class="head__cn" id="albumTitle">我 们 的 时 光</h2>
|
|
565
|
+
<div class="head__rule" aria-hidden="true"><i></i></div>
|
|
566
|
+
</div>
|
|
567
|
+
<div class="album" id="albumGrid"></div>
|
|
568
|
+
</section>
|
|
569
|
+
|
|
570
|
+
<!-- 回执 -->
|
|
571
|
+
<section class="section" id="rsvp" aria-labelledby="rsvpTitle">
|
|
572
|
+
<div class="head reveal">
|
|
573
|
+
<span class="head__en">R.S.V.P.</span>
|
|
574
|
+
<h2 class="head__cn" id="rsvpTitle">出 席 回 执</h2>
|
|
575
|
+
<div class="head__rule" aria-hidden="true"><i></i></div>
|
|
576
|
+
</div>
|
|
577
|
+
<p class="rsvp__lead reveal" data-bind="rsvpLead"></p>
|
|
578
|
+
<form class="form reveal" style="--d:1" id="rsvpForm" novalidate>
|
|
579
|
+
<div class="field">
|
|
580
|
+
<label for="fName">您的称呼 <em>*</em></label>
|
|
581
|
+
<input class="input" id="fName" name="name" type="text" autocomplete="name" maxlength="20" placeholder="如:张三 / 张三一家" aria-describedby="eName" required>
|
|
582
|
+
<p class="field__err" id="eName" role="alert"></p>
|
|
583
|
+
</div>
|
|
584
|
+
<fieldset class="field">
|
|
585
|
+
<legend>能否出席 <em>*</em></legend>
|
|
586
|
+
<div class="seg">
|
|
587
|
+
<label class="seg__item"><input type="radio" name="attend" value="yes" aria-describedby="eAttend"><span>欣然赴约</span></label>
|
|
588
|
+
<label class="seg__item"><input type="radio" name="attend" value="no" aria-describedby="eAttend"><span>遗憾缺席</span></label>
|
|
589
|
+
</div>
|
|
590
|
+
<p class="field__err" id="eAttend" role="alert"></p>
|
|
591
|
+
</fieldset>
|
|
592
|
+
<div class="field" id="guestsField">
|
|
593
|
+
<label for="fGuests">出席人数(含本人)</label>
|
|
594
|
+
<div class="stepper">
|
|
595
|
+
<button class="stepper__btn" type="button" data-step="-1" aria-label="减少一位">−</button>
|
|
596
|
+
<input class="input" id="fGuests" name="guests" type="number" inputmode="numeric" min="1" max="6" value="1">
|
|
597
|
+
<button class="stepper__btn" type="button" data-step="1" aria-label="增加一位">+</button>
|
|
598
|
+
</div>
|
|
599
|
+
</div>
|
|
600
|
+
<div class="field">
|
|
601
|
+
<label for="fPhone">联系电话(选填)</label>
|
|
602
|
+
<input class="input" id="fPhone" name="phone" type="tel" inputmode="numeric" autocomplete="tel" maxlength="11" placeholder="方便新人与您联系" aria-describedby="ePhone">
|
|
603
|
+
<p class="field__err" id="ePhone" role="alert"></p>
|
|
604
|
+
</div>
|
|
605
|
+
<div class="field">
|
|
606
|
+
<label for="fWish">祝福留言(选填)</label>
|
|
607
|
+
<textarea class="input" id="fWish" name="wish" rows="3" maxlength="200" placeholder="写下对新人的祝福"></textarea>
|
|
608
|
+
<p class="field__count" aria-live="off"><span id="wishCount">0</span>/200</p>
|
|
609
|
+
</div>
|
|
610
|
+
<button class="btn btn--solid btn--block" id="rsvpSubmit" type="submit">提交回执</button>
|
|
611
|
+
</form>
|
|
612
|
+
<div class="done" id="rsvpDone" hidden>
|
|
613
|
+
<div class="done__mark" aria-hidden="true">囍</div>
|
|
614
|
+
<p class="done__title" id="doneTitle"></p>
|
|
615
|
+
<p class="done__name" id="doneName"></p>
|
|
616
|
+
<p class="done__text" id="doneText"></p>
|
|
617
|
+
<div class="done__actions">
|
|
618
|
+
<button class="btn btn--solid" id="copyReceipt" type="button">复制回执</button>
|
|
619
|
+
<button class="btn" id="editReceipt" type="button">修改回复</button>
|
|
620
|
+
</div>
|
|
621
|
+
</div>
|
|
622
|
+
<div class="contacts" id="contacts" hidden></div>
|
|
623
|
+
</section>
|
|
624
|
+
|
|
625
|
+
<footer class="footer">
|
|
626
|
+
<p class="footer__thanks reveal">感谢您的祝福与陪伴</p>
|
|
627
|
+
<p class="footer__names reveal" style="--d:1" data-bind="namesLine"></p>
|
|
628
|
+
<p class="footer__date reveal" style="--d:2" data-bind="dateDots"></p>
|
|
629
|
+
<div class="footer__actions reveal" style="--d:3">
|
|
630
|
+
<button class="btn btn--solid" id="shareBtn" type="button">分享请柬</button>
|
|
631
|
+
<a class="btn" href="#top">回到顶部</a>
|
|
632
|
+
</div>
|
|
633
|
+
</footer>
|
|
634
|
+
</main>
|
|
635
|
+
|
|
636
|
+
<div class="lightbox" id="lightbox" hidden>
|
|
637
|
+
<img id="lightboxImg" alt="">
|
|
638
|
+
<button class="lightbox__close" id="lightboxClose" type="button" aria-label="关闭大图">×</button>
|
|
639
|
+
</div>
|
|
640
|
+
<div class="toast" id="toast" role="status" aria-live="polite"></div>
|
|
641
|
+
<div class="guide" id="guide" hidden>
|
|
642
|
+
<svg viewBox="0 0 92 120" fill="none" stroke="#fff" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 112C24 70 44 38 78 14M78 14l-22 4M78 14l-4 22"/></svg>
|
|
643
|
+
<p>点击右上角 ···<br>选择「发送给朋友」</p>
|
|
644
|
+
</div>
|
|
645
|
+
<noscript><p style="padding:24px;text-align:center">请在微信或浏览器中打开,以查看完整的电子请柬。</p></noscript>
|
|
646
|
+
|
|
647
|
+
<script>
|
|
648
|
+
(() => {
|
|
649
|
+
'use strict';
|
|
650
|
+
const C = window.INVITE;
|
|
651
|
+
const $ = (s, r = document) => r.querySelector(s);
|
|
652
|
+
const $$ = (s, r = document) => Array.from(r.querySelectorAll(s));
|
|
653
|
+
const UA = navigator.userAgent;
|
|
654
|
+
const isWeChat = /MicroMessenger/i.test(UA);
|
|
655
|
+
const isIOS = /iP(hone|ad|od)/.test(UA) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
|
|
656
|
+
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
657
|
+
const store = { // 隐私模式下 localStorage 会抛错:读写都要兜住
|
|
658
|
+
get(k) { try { return JSON.parse(localStorage.getItem(k)); } catch (e) { return null; } },
|
|
659
|
+
set(k, v) { try { localStorage.setItem(k, JSON.stringify(v)); return true; } catch (e) { return false; } },
|
|
660
|
+
};
|
|
661
|
+
|
|
662
|
+
// ── 日期:一律按北京时间算,星期与农历都由代码推出来,不靠手填
|
|
663
|
+
const when = new Date(C.date);
|
|
664
|
+
const TZ = 'Asia/Shanghai';
|
|
665
|
+
const partsOf = (d) => {
|
|
666
|
+
const o = {};
|
|
667
|
+
new Intl.DateTimeFormat('zh-CN', { timeZone: TZ, year: 'numeric', month: 'numeric', day: 'numeric', hour: 'numeric', minute: '2-digit', hourCycle: 'h23', weekday: 'long' })
|
|
668
|
+
.formatToParts(d).forEach((p) => { o[p.type] = p.value; });
|
|
669
|
+
return { y: +o.year, m: +o.month, d: +o.day, h: +o.hour, min: o.minute, week: o.weekday };
|
|
670
|
+
};
|
|
671
|
+
const W = partsOf(when);
|
|
672
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
673
|
+
const DIGIT = '〇一二三四五六七八九';
|
|
674
|
+
const cnYear = (y) => String(y).split('').map((c) => DIGIT[+c]).join('');
|
|
675
|
+
const cnNum = (n) => (n <= 10 ? (n === 10 ? '十' : DIGIT[n]) : n < 20 ? '十' + (n % 10 ? DIGIT[n % 10] : '') : DIGIT[Math.floor(n / 10)] + '十' + (n % 10 ? DIGIT[n % 10] : ''));
|
|
676
|
+
const lunarDay = (n) => (n <= 10 ? '初' + cnNum(n) : n < 20 ? '十' + DIGIT[n - 10] : n === 20 ? '二十' : n < 30 ? '廿' + DIGIT[n - 20] : '三十');
|
|
677
|
+
function lunarText(d) {
|
|
678
|
+
if (C.lunar) return C.lunar;
|
|
679
|
+
try {
|
|
680
|
+
const o = {};
|
|
681
|
+
new Intl.DateTimeFormat('zh-CN-u-ca-chinese', { timeZone: TZ, year: 'numeric', month: 'long', day: 'numeric' })
|
|
682
|
+
.formatToParts(d).forEach((p) => { o[p.type] = p.value; });
|
|
683
|
+
if (!o.month || !o.day) return '';
|
|
684
|
+
const month = o.month === '十一月' ? '冬月' : o.month;
|
|
685
|
+
return (o.yearName ? o.yearName + '年' : '') + month + lunarDay(+o.day);
|
|
686
|
+
} catch (e) { return ''; } // 老内核没有中国历:不显示,也不瞎猜
|
|
687
|
+
}
|
|
688
|
+
const lunar = lunarText(when);
|
|
689
|
+
const period = W.h < 11 ? '上午' : W.h < 13 ? '中午' : W.h < 18 ? '下午' : '晚上';
|
|
690
|
+
const clock = `${period} ${W.h > 12 ? W.h - 12 : W.h}:${W.min}`;
|
|
691
|
+
const guest = (new URLSearchParams(location.search).get('to') || '').replace(/[<>]/g, '').trim().slice(0, 20);
|
|
692
|
+
|
|
693
|
+
// ── 文案绑定:全部用 textContent(宾客名来自链接参数,绝不能拼进 innerHTML)──
|
|
694
|
+
const deadline = C.rsvp.deadline || (() => { const d = partsOf(new Date(when.getTime() - 10 * 864e5)); return `${d.m}月${d.d}日`; })();
|
|
695
|
+
const TEXT = {
|
|
696
|
+
groom: C.groom, bride: C.bride, tagline: C.tagline || '我们结婚啦',
|
|
697
|
+
dateDots: `${W.y} · ${pad(W.m)} · ${pad(W.d)}`,
|
|
698
|
+
dateMeta: [W.week, lunar && '农历' + lunar.replace(/^.{2}年/, ''), clock].filter(Boolean).join(' · '),
|
|
699
|
+
placeLine: `${C.venue.city} · ${C.venue.name}${C.venue.hall ? ' ' + C.venue.hall : ''}`,
|
|
700
|
+
namesLine: `${C.groom} & ${C.bride}`,
|
|
701
|
+
gateTo: guest ? `诚邀 ${guest} 见证我们的婚礼` : '诚邀您见证我们的婚礼',
|
|
702
|
+
letterTo: guest ? `敬呈 ${guest}` : '',
|
|
703
|
+
venueName: C.venue.name, venueHall: C.venue.hall, venueAddr: C.venue.address, venueTips: C.venue.tips || '',
|
|
704
|
+
rsvpLead: `为便于安排席位,请于 ${deadline} 前回复`,
|
|
705
|
+
countTitle: '婚 礼 倒 计 时',
|
|
706
|
+
};
|
|
707
|
+
$$('[data-bind]').forEach((el) => {
|
|
708
|
+
const v = TEXT[el.dataset.bind];
|
|
709
|
+
if (v == null) return;
|
|
710
|
+
el.textContent = v;
|
|
711
|
+
if (el.hasAttribute('hidden') && v) el.hidden = false;
|
|
712
|
+
});
|
|
713
|
+
document.title = `${C.groom} & ${C.bride} · 婚礼请柬`;
|
|
714
|
+
if (C.sample) { // 示例数据绊线:见 INVITE.sample 的注释
|
|
715
|
+
const bar = document.createElement('p');
|
|
716
|
+
bar.textContent = 'TODO:这是模板示例数据——把 window.INVITE 改成新人的真实信息,并把 sample 设为 false';
|
|
717
|
+
bar.style.cssText = 'position:fixed;top:0;left:0;right:0;z-index:99;margin:0;padding:10px 14px;background:#fff3cd;color:#5c4400;font:14px/1.5 sans-serif;text-align:center';
|
|
718
|
+
document.body.appendChild(bar);
|
|
719
|
+
}
|
|
720
|
+
if (Math.max(C.groom.length, C.bride.length) >= 3) $('.hero__names').classList.add('is-long'); // 复姓/三字名:首屏名字降一档,免得挤出屏
|
|
721
|
+
|
|
722
|
+
// 请柬正文:旧式请柬的写法,日期用中文数字
|
|
723
|
+
(function letter() {
|
|
724
|
+
const box = $('#letterBody');
|
|
725
|
+
const line = (parts) => {
|
|
726
|
+
const p = document.createElement('span');
|
|
727
|
+
p.style.display = 'block';
|
|
728
|
+
parts.forEach((x) => {
|
|
729
|
+
if (typeof x === 'string') p.appendChild(document.createTextNode(x));
|
|
730
|
+
else { const el = document.createElement(x.tag || 'b'); el.textContent = x.text; if (x.cls) el.className = x.cls; p.appendChild(el); }
|
|
731
|
+
});
|
|
732
|
+
box.appendChild(p);
|
|
733
|
+
};
|
|
734
|
+
line(['谨定于公历', { text: `${cnYear(W.y)}年${cnNum(W.m)}月${cnNum(W.d)}日` }]);
|
|
735
|
+
line([lunar ? `农历${lunar} ${W.week}` : W.week]);
|
|
736
|
+
line([`${period}${cnNum(W.h > 12 ? W.h - 12 : W.h)}时${+W.min ? cnNum(+W.min) + '分' : '整'}`]);
|
|
737
|
+
line(['在 ', { text: `${C.venue.name}${C.venue.hall ? ' · ' + C.venue.hall : ''}` }]);
|
|
738
|
+
line(['举行结婚典礼']);
|
|
739
|
+
line([{ tag: 'span', cls: 'big', text: '敬备喜宴 恭候光临' }]);
|
|
740
|
+
})();
|
|
741
|
+
|
|
742
|
+
// ── 流程
|
|
743
|
+
(function flow() {
|
|
744
|
+
const list = $('#flow');
|
|
745
|
+
(C.schedule || []).forEach((it, i) => {
|
|
746
|
+
const li = document.createElement('li');
|
|
747
|
+
li.className = 'flow__item reveal' + (it.key ? ' is-key' : '');
|
|
748
|
+
li.style.setProperty('--d', i);
|
|
749
|
+
const t = document.createElement('span'); t.className = 'flow__time'; t.textContent = it.time;
|
|
750
|
+
const body = document.createElement('div');
|
|
751
|
+
const h = document.createElement('h3'); h.className = 'flow__title'; h.textContent = it.title;
|
|
752
|
+
body.appendChild(h);
|
|
753
|
+
if (it.desc) { const p = document.createElement('p'); p.className = 'flow__desc'; p.textContent = it.desc; body.appendChild(p); }
|
|
754
|
+
li.append(t, body);
|
|
755
|
+
list.appendChild(li);
|
|
756
|
+
});
|
|
757
|
+
if (!list.children.length) $('#schedule').hidden = true;
|
|
758
|
+
})();
|
|
759
|
+
|
|
760
|
+
// ── 导航:默认按名称检索(不需要地图 key,也不会因坐标写错把人导错地方)──
|
|
761
|
+
(function nav() {
|
|
762
|
+
const v = C.venue;
|
|
763
|
+
const kw = encodeURIComponent(v.name);
|
|
764
|
+
const city = encodeURIComponent(v.city || '');
|
|
765
|
+
const ll = (v.lnglat || '').split(',').map(Number);
|
|
766
|
+
const hasLL = ll.length === 2 && ll.every((n) => Number.isFinite(n));
|
|
767
|
+
$('#navAmap').href = hasLL
|
|
768
|
+
? `https://uri.amap.com/marker?position=${ll[0]},${ll[1]}&name=${kw}&coordinate=gaode&callnative=1`
|
|
769
|
+
: `https://uri.amap.com/search?keyword=${kw}&city=${city}&callnative=1`;
|
|
770
|
+
$('#navBaidu').href = hasLL
|
|
771
|
+
? `https://api.map.baidu.com/marker?location=${ll[1]},${ll[0]}&title=${kw}&content=${encodeURIComponent(v.address || '')}&coord_type=gcj02&output=html&src=webapp.invite.h5`
|
|
772
|
+
: `https://api.map.baidu.com/place/search?query=${kw}®ion=${city}&output=html&src=webapp.invite.h5`;
|
|
773
|
+
$('#navApple').href = `https://maps.apple.com/?q=${encodeURIComponent(v.city + v.name)}${hasLL ? `&ll=${ll[1]},${ll[0]}` : ''}`;
|
|
774
|
+
$('#copyAddr').addEventListener('click', async () => {
|
|
775
|
+
toast(await copyText(`${v.name}${v.hall ? ' ' + v.hall : ''}\n${v.address}`) ? '地址已复制' : '复制失败,请长按地址手动复制');
|
|
776
|
+
});
|
|
777
|
+
})();
|
|
778
|
+
|
|
779
|
+
// ── 轻提示 / 复制
|
|
780
|
+
let toastTimer = 0;
|
|
781
|
+
function toast(msg) {
|
|
782
|
+
const el = $('#toast');
|
|
783
|
+
el.textContent = msg;
|
|
784
|
+
el.classList.add('is-on');
|
|
785
|
+
clearTimeout(toastTimer);
|
|
786
|
+
toastTimer = setTimeout(() => el.classList.remove('is-on'), 2200);
|
|
787
|
+
}
|
|
788
|
+
async function copyText(text) {
|
|
789
|
+
try {
|
|
790
|
+
if (navigator.clipboard && window.isSecureContext) { await navigator.clipboard.writeText(text); return true; }
|
|
791
|
+
} catch (e) { /* 老版微信:走下面的兜底 */ }
|
|
792
|
+
const ta = document.createElement('textarea');
|
|
793
|
+
ta.value = text; ta.setAttribute('readonly', ''); ta.style.cssText = 'position:fixed;top:-200px;opacity:0';
|
|
794
|
+
document.body.appendChild(ta); ta.select(); ta.setSelectionRange(0, text.length);
|
|
795
|
+
let ok = false;
|
|
796
|
+
try { ok = document.execCommand('copy'); } catch (e) { ok = false; }
|
|
797
|
+
ta.remove();
|
|
798
|
+
return ok;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// ── 倒计时:按时间戳差值算,跨时区也准;过了婚期自动换成「已相守 N 天」──
|
|
802
|
+
(function countdown() {
|
|
803
|
+
const el = { d: $('#cDay'), h: $('#cHour'), m: $('#cMin'), s: $('#cSec') };
|
|
804
|
+
const note = $('#countNote');
|
|
805
|
+
const set = (node, val) => {
|
|
806
|
+
if (node.textContent === val) return;
|
|
807
|
+
node.textContent = val;
|
|
808
|
+
if (!reduceMotion) { node.classList.remove('tick'); void node.offsetWidth; node.classList.add('tick'); }
|
|
809
|
+
};
|
|
810
|
+
function render() {
|
|
811
|
+
const diff = when.getTime() - Date.now();
|
|
812
|
+
if (diff > 0) {
|
|
813
|
+
set(el.d, String(Math.floor(diff / 864e5)));
|
|
814
|
+
set(el.h, pad(Math.floor(diff % 864e5 / 36e5)));
|
|
815
|
+
set(el.m, pad(Math.floor(diff % 36e5 / 6e4)));
|
|
816
|
+
set(el.s, pad(Math.floor(diff % 6e4 / 1e3)));
|
|
817
|
+
note.textContent = diff < 864e5 ? '就是今天,期待与您相见' : `静候 ${W.m} 月 ${W.d} 日,与您共赴良辰`;
|
|
818
|
+
return true;
|
|
819
|
+
}
|
|
820
|
+
const days = Math.floor(-diff / 864e5);
|
|
821
|
+
$('#count').hidden = true;
|
|
822
|
+
$('[data-bind="countTitle"]').textContent = days < 1 ? '礼 成' : '执 手 同 行';
|
|
823
|
+
note.textContent = days < 1 ? '我们今天结婚了,谢谢您的到来' : `我们已携手走过 ${days} 天,感谢一路有您`;
|
|
824
|
+
return false;
|
|
825
|
+
}
|
|
826
|
+
if (render()) { const t = setInterval(() => { if (!render()) clearInterval(t); }, 1000); }
|
|
827
|
+
})();
|
|
828
|
+
|
|
829
|
+
// ── 加入日历(.ics)──
|
|
830
|
+
$('#calBtn').addEventListener('click', async () => {
|
|
831
|
+
const v = C.venue;
|
|
832
|
+
const end = new Date(when.getTime() + (C.durationHours || 3) * 36e5);
|
|
833
|
+
const fmt = (d) => d.toISOString().replace(/[-:]/g, '').replace(/\.\d{3}/, '');
|
|
834
|
+
const esc = (s) => String(s).replace(/[\\;,]/g, (m) => '\\' + m).replace(/\n/g, '\\n');
|
|
835
|
+
const title = `${C.groom} & ${C.bride} 的婚礼`;
|
|
836
|
+
const where = `${v.name}${v.hall ? ' ' + v.hall : ''}(${v.address})`;
|
|
837
|
+
if (isWeChat) { // 微信内置浏览器不支持下载文件
|
|
838
|
+
await copyText(`${title}\n${W.y}年${W.m}月${W.d}日 ${W.week} ${clock}\n${where}`);
|
|
839
|
+
toast('微信内无法直接添加日历,已复制时间地点;可点右上角「在浏览器打开」后添加');
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
const ics = [
|
|
843
|
+
'BEGIN:VCALENDAR', 'VERSION:2.0', 'PRODID:-//invite//wedding//CN', 'CALSCALE:GREGORIAN', 'BEGIN:VEVENT',
|
|
844
|
+
`UID:${fmt(when)}-${encodeURIComponent(C.groom + C.bride)}@invite`, `DTSTAMP:${fmt(new Date())}`,
|
|
845
|
+
`DTSTART:${fmt(when)}`, `DTEND:${fmt(end)}`, `SUMMARY:${esc(title)}`, `LOCATION:${esc(where)}`,
|
|
846
|
+
`DESCRIPTION:${esc('期待与您相见。' + (lunar ? '农历' + lunar : ''))}`,
|
|
847
|
+
'BEGIN:VALARM', 'TRIGGER:-P1D', 'ACTION:DISPLAY', `DESCRIPTION:${esc('明天是' + title)}`, 'END:VALARM',
|
|
848
|
+
'END:VEVENT', 'END:VCALENDAR',
|
|
849
|
+
].join('\r\n');
|
|
850
|
+
if (isIOS) { location.href = 'data:text/calendar;charset=utf-8,' + encodeURIComponent(ics); return; }
|
|
851
|
+
const url = URL.createObjectURL(new Blob([ics], { type: 'text/calendar;charset=utf-8' }));
|
|
852
|
+
const a = Object.assign(document.createElement('a'), { href: url, download: 'wedding.ics' });
|
|
853
|
+
document.body.appendChild(a); a.click(); a.remove();
|
|
854
|
+
setTimeout(() => URL.revokeObjectURL(url), 5000);
|
|
855
|
+
toast('已生成日历文件,打开即可添加提醒');
|
|
856
|
+
});
|
|
857
|
+
|
|
858
|
+
// ── 相册(可选)──
|
|
859
|
+
(function album() {
|
|
860
|
+
const photos = (C.photos || []).filter((p) => p && p.src);
|
|
861
|
+
if (!photos.length) return;
|
|
862
|
+
const grid = $('#albumGrid');
|
|
863
|
+
photos.forEach((p, i) => {
|
|
864
|
+
const btn = document.createElement('button');
|
|
865
|
+
btn.type = 'button'; btn.className = 'album__item reveal'; btn.style.setProperty('--d', i % 3);
|
|
866
|
+
btn.setAttribute('aria-label', '查看大图:' + (p.alt || '照片 ' + (i + 1)));
|
|
867
|
+
const img = new Image();
|
|
868
|
+
img.src = p.src; img.alt = p.alt || ''; img.loading = i ? 'lazy' : 'eager'; img.decoding = 'async';
|
|
869
|
+
img.addEventListener('error', () => btn.remove()); // 某张图丢了:这一格直接不显示,别留破图
|
|
870
|
+
btn.appendChild(img);
|
|
871
|
+
btn.addEventListener('click', () => { $('#lightboxImg').src = p.src; $('#lightboxImg').alt = p.alt || ''; $('#lightbox').hidden = false; });
|
|
872
|
+
grid.appendChild(btn);
|
|
873
|
+
});
|
|
874
|
+
$('#album').hidden = false;
|
|
875
|
+
const close = () => { $('#lightbox').hidden = true; };
|
|
876
|
+
$('#lightboxClose').addEventListener('click', close);
|
|
877
|
+
$('#lightbox').addEventListener('click', (e) => { if (e.target.id === 'lightbox') close(); });
|
|
878
|
+
document.addEventListener('keydown', (e) => { if (e.key === 'Escape') close(); });
|
|
879
|
+
})();
|
|
880
|
+
|
|
881
|
+
// ── 联系新人(可选)──
|
|
882
|
+
(function contacts() {
|
|
883
|
+
const list = (C.contacts || []).filter((c) => c && c.phone);
|
|
884
|
+
if (!list.length) return;
|
|
885
|
+
const box = $('#contacts');
|
|
886
|
+
list.forEach((c) => {
|
|
887
|
+
const a = document.createElement('a');
|
|
888
|
+
a.className = 'btn'; a.href = 'tel:' + String(c.phone).replace(/[^\d+]/g, '');
|
|
889
|
+
a.textContent = `致电${c.role || ''} ${c.name || ''}`.trim();
|
|
890
|
+
box.appendChild(a);
|
|
891
|
+
});
|
|
892
|
+
box.hidden = false;
|
|
893
|
+
})();
|
|
894
|
+
|
|
895
|
+
// ── 回执
|
|
896
|
+
(function rsvp() {
|
|
897
|
+
const form = $('#rsvpForm');
|
|
898
|
+
const done = $('#rsvpDone');
|
|
899
|
+
const key = 'invite-rsvp:' + C.groom + C.bride + C.date;
|
|
900
|
+
const max = Math.max(1, C.rsvp.maxGuests || 6);
|
|
901
|
+
const guests = $('#fGuests');
|
|
902
|
+
guests.max = String(max);
|
|
903
|
+
const setErr = (input, id, msg) => {
|
|
904
|
+
$('#' + id).textContent = msg;
|
|
905
|
+
if (input) input.setAttribute('aria-invalid', msg ? 'true' : 'false');
|
|
906
|
+
};
|
|
907
|
+
const attendValue = () => (form.querySelector('input[name="attend"]:checked') || {}).value || '';
|
|
908
|
+
const syncGuests = () => {
|
|
909
|
+
const no = attendValue() === 'no';
|
|
910
|
+
$('#guestsField').setAttribute('aria-disabled', no ? 'true' : 'false');
|
|
911
|
+
guests.disabled = no;
|
|
912
|
+
$$('.stepper__btn', form).forEach((b) => { b.disabled = no; });
|
|
913
|
+
};
|
|
914
|
+
$$('input[name="attend"]', form).forEach((r) => r.addEventListener('change', () => { setErr(null, 'eAttend', ''); syncGuests(); }));
|
|
915
|
+
$$('.stepper__btn', form).forEach((b) => b.addEventListener('click', () => {
|
|
916
|
+
const n = Math.min(max, Math.max(1, (parseInt(guests.value, 10) || 1) + Number(b.dataset.step)));
|
|
917
|
+
guests.value = String(n);
|
|
918
|
+
}));
|
|
919
|
+
guests.addEventListener('change', () => { guests.value = String(Math.min(max, Math.max(1, parseInt(guests.value, 10) || 1))); });
|
|
920
|
+
$('#fWish').addEventListener('input', (e) => { $('#wishCount').textContent = String(e.target.value.length); });
|
|
921
|
+
$('#fName').addEventListener('input', () => setErr($('#fName'), 'eName', ''));
|
|
922
|
+
$('#fPhone').addEventListener('input', (e) => { e.target.value = e.target.value.replace(/\D/g, '').slice(0, 11); setErr(e.target, 'ePhone', ''); });
|
|
923
|
+
|
|
924
|
+
const receipt = (r) => [
|
|
925
|
+
`【婚礼回执】${C.groom} & ${C.bride} · ${W.y}.${pad(W.m)}.${pad(W.d)}`,
|
|
926
|
+
`称呼:${r.name}`,
|
|
927
|
+
r.attend === 'yes' ? `出席:欣然赴约(${r.guests} 位)` : '出席:遗憾缺席',
|
|
928
|
+
r.phone ? `电话:${r.phone}` : '',
|
|
929
|
+
r.wish ? `祝福:${r.wish}` : '',
|
|
930
|
+
].filter(Boolean).join('\n');
|
|
931
|
+
const showDone = (r, note) => {
|
|
932
|
+
$('#doneTitle').textContent = r.attend === 'yes' ? '期待与您相见' : '感谢您的祝福';
|
|
933
|
+
$('#doneName').textContent = r.name;
|
|
934
|
+
$('#doneText').textContent = (r.attend === 'yes' ? `已为您预留 ${r.guests} 个座位` : '虽不能同席,心意已收到') + (note ? '\n' + note : '');
|
|
935
|
+
form.hidden = true;
|
|
936
|
+
done.hidden = false;
|
|
937
|
+
};
|
|
938
|
+
const saved = store.get(key);
|
|
939
|
+
if (saved && saved.name) showDone(saved, '');
|
|
940
|
+
|
|
941
|
+
form.addEventListener('submit', async (e) => {
|
|
942
|
+
e.preventDefault();
|
|
943
|
+
const r = {
|
|
944
|
+
name: $('#fName').value.trim(), attend: attendValue(),
|
|
945
|
+
guests: attendValue() === 'yes' ? Math.min(max, Math.max(1, parseInt(guests.value, 10) || 1)) : 0,
|
|
946
|
+
phone: $('#fPhone').value.trim(), wish: $('#fWish').value.trim(), at: new Date().toISOString(),
|
|
947
|
+
};
|
|
948
|
+
let first = null;
|
|
949
|
+
if (!r.name) { setErr($('#fName'), 'eName', '请留下您的称呼'); first = first || $('#fName'); }
|
|
950
|
+
if (!r.attend) { setErr(null, 'eAttend', '请选择能否出席'); first = first || form.querySelector('input[name="attend"]'); }
|
|
951
|
+
if (r.phone && !/^1[3-9]\d{9}$/.test(r.phone)) { setErr($('#fPhone'), 'ePhone', '手机号格式不对(11 位,以 1 开头)'); first = first || $('#fPhone'); }
|
|
952
|
+
if (first) { first.focus(); return; }
|
|
953
|
+
const btn = $('#rsvpSubmit');
|
|
954
|
+
btn.disabled = true; btn.textContent = '提交中…';
|
|
955
|
+
let note = '';
|
|
956
|
+
if (C.rsvp.endpoint) {
|
|
957
|
+
try {
|
|
958
|
+
const ctl = new AbortController();
|
|
959
|
+
const timer = setTimeout(() => ctl.abort(), 8000);
|
|
960
|
+
const res = await fetch(C.rsvp.endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ...r, couple: `${C.groom}&${C.bride}` }), signal: ctl.signal });
|
|
961
|
+
clearTimeout(timer);
|
|
962
|
+
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
963
|
+
} catch (err) {
|
|
964
|
+
note = '网络不太顺畅,回执已保存在本机;可复制发给新人';
|
|
965
|
+
}
|
|
966
|
+
} else {
|
|
967
|
+
note = '可点「复制回执」发给新人';
|
|
968
|
+
}
|
|
969
|
+
store.set(key, r);
|
|
970
|
+
btn.disabled = false; btn.textContent = '提交回执';
|
|
971
|
+
showDone(r, note);
|
|
972
|
+
toast('回执已提交');
|
|
973
|
+
});
|
|
974
|
+
$('#copyReceipt').addEventListener('click', async () => {
|
|
975
|
+
const r = store.get(key);
|
|
976
|
+
if (!r) return;
|
|
977
|
+
toast(await copyText(receipt(r)) ? '回执已复制,可粘贴发给新人' : '复制失败,请截图发给新人');
|
|
978
|
+
});
|
|
979
|
+
$('#editReceipt').addEventListener('click', () => {
|
|
980
|
+
const r = store.get(key) || {};
|
|
981
|
+
$('#fName').value = r.name || '';
|
|
982
|
+
$('#fPhone').value = r.phone || '';
|
|
983
|
+
$('#fWish').value = r.wish || '';
|
|
984
|
+
$('#wishCount').textContent = String(($('#fWish').value || '').length);
|
|
985
|
+
guests.value = String(r.guests || 1);
|
|
986
|
+
$$('input[name="attend"]', form).forEach((x) => { x.checked = x.value === r.attend; });
|
|
987
|
+
syncGuests();
|
|
988
|
+
done.hidden = true;
|
|
989
|
+
form.hidden = false;
|
|
990
|
+
$('#fName').focus();
|
|
991
|
+
});
|
|
992
|
+
})();
|
|
993
|
+
|
|
994
|
+
// ── 分享:有系统分享用系统分享;微信里给「点右上角」引导;其余复制链接
|
|
995
|
+
$('#shareBtn').addEventListener('click', async () => {
|
|
996
|
+
const data = { title: document.title, text: `${C.groom} & ${C.bride} 诚邀您见证我们的婚礼`, url: location.href };
|
|
997
|
+
if (isWeChat) { $('#guide').hidden = false; return; }
|
|
998
|
+
if (navigator.share) { try { await navigator.share(data); return; } catch (e) { if (e && e.name === 'AbortError') return; } }
|
|
999
|
+
toast(await copyText(location.href) ? '链接已复制,发给亲友即可' : '请复制浏览器地址栏的链接分享');
|
|
1000
|
+
});
|
|
1001
|
+
$('#guide').addEventListener('click', () => { $('#guide').hidden = true; });
|
|
1002
|
+
|
|
1003
|
+
// ── 音乐:给了 mp3 就放 mp3;没给就用 Web Audio 合成八音盒《卡农》——不依赖任何文件
|
|
1004
|
+
const btnEl = $('#music');
|
|
1005
|
+
const music = (function createMusic() {
|
|
1006
|
+
let playing = false;
|
|
1007
|
+
if (C.music && C.music.src) {
|
|
1008
|
+
const a = new Audio(C.music.src);
|
|
1009
|
+
a.loop = true; a.preload = 'auto';
|
|
1010
|
+
a.addEventListener('error', () => { btnEl.hidden = true; });
|
|
1011
|
+
document.addEventListener('WeixinJSBridgeReady', () => { if (playing) a.play().catch(() => {}); });
|
|
1012
|
+
return {
|
|
1013
|
+
start() { playing = true; return a.play().then(() => true, () => { playing = false; return false; }); },
|
|
1014
|
+
stop() { playing = false; a.pause(); },
|
|
1015
|
+
get on() { return playing; },
|
|
1016
|
+
};
|
|
1017
|
+
}
|
|
1018
|
+
let ac = null;
|
|
1019
|
+
let out = null;
|
|
1020
|
+
let timer = 0;
|
|
1021
|
+
let next = 0;
|
|
1022
|
+
let beat = 0;
|
|
1023
|
+
const SPB = 60 / 66; // 每拍秒数(66 BPM)
|
|
1024
|
+
const hz = (n) => 440 * Math.pow(2, (n - 69) / 12);
|
|
1025
|
+
// 帕赫贝尔《卡农》(公有领域)C 大调:每个和弦两拍,八个和弦一轮;两段旋律交替
|
|
1026
|
+
const CHORDS = [[60, 64, 67, 72], [55, 59, 62, 67], [57, 60, 64, 69], [52, 55, 59, 64], [53, 57, 60, 65], [48, 52, 55, 60], [53, 57, 60, 65], [55, 59, 62, 67]];
|
|
1027
|
+
const MELODY = [[76, 74, 72, 71, 69, 67, 69, 71], [72, 71, 69, 67, 65, 64, 65, 67]];
|
|
1028
|
+
const bell = (f, t, dur, vol) => {
|
|
1029
|
+
const g = ac.createGain();
|
|
1030
|
+
const o1 = ac.createOscillator();
|
|
1031
|
+
const o2 = ac.createOscillator();
|
|
1032
|
+
const g2 = ac.createGain();
|
|
1033
|
+
o1.frequency.value = f; o2.frequency.value = f * 2.01; g2.gain.value = 0.25; // 略失谐的二次泛音 = 八音盒的金属感
|
|
1034
|
+
o1.connect(g); o2.connect(g2); g2.connect(g); g.connect(out);
|
|
1035
|
+
g.gain.setValueAtTime(0.0001, t);
|
|
1036
|
+
g.gain.exponentialRampToValueAtTime(vol, t + 0.012);
|
|
1037
|
+
g.gain.exponentialRampToValueAtTime(0.0001, t + dur);
|
|
1038
|
+
o1.start(t); o2.start(t); o1.stop(t + dur + 0.05); o2.stop(t + dur + 0.05);
|
|
1039
|
+
};
|
|
1040
|
+
const tick = () => {
|
|
1041
|
+
while (next < ac.currentTime + 0.3) {
|
|
1042
|
+
const bar = Math.floor(beat / 2) % 8;
|
|
1043
|
+
const round = Math.floor(beat / 16) % 2;
|
|
1044
|
+
const half = beat % 2;
|
|
1045
|
+
const chord = CHORDS[bar];
|
|
1046
|
+
bell(hz(chord[(half * 2) % 4]), next, 1.5, 0.045);
|
|
1047
|
+
bell(hz(chord[(half * 2 + 1) % 4]), next + SPB / 2, 1.5, 0.04);
|
|
1048
|
+
if (!half) bell(hz(MELODY[round][bar]), next, 3.4, 0.1);
|
|
1049
|
+
next += SPB;
|
|
1050
|
+
beat++;
|
|
1051
|
+
}
|
|
1052
|
+
};
|
|
1053
|
+
return {
|
|
1054
|
+
start() {
|
|
1055
|
+
const AC = window.AudioContext || window.webkitAudioContext;
|
|
1056
|
+
if (!AC) return Promise.resolve(false);
|
|
1057
|
+
if (!ac) {
|
|
1058
|
+
ac = new AC();
|
|
1059
|
+
out = ac.createGain();
|
|
1060
|
+
out.gain.value = 0.9;
|
|
1061
|
+
const lp = ac.createBiquadFilter();
|
|
1062
|
+
lp.type = 'lowpass'; lp.frequency.value = 3400;
|
|
1063
|
+
out.connect(lp); lp.connect(ac.destination);
|
|
1064
|
+
next = ac.currentTime + 0.12;
|
|
1065
|
+
}
|
|
1066
|
+
playing = true;
|
|
1067
|
+
clearInterval(timer);
|
|
1068
|
+
timer = setInterval(tick, 80);
|
|
1069
|
+
tick();
|
|
1070
|
+
return ac.resume().then(() => true, () => false);
|
|
1071
|
+
},
|
|
1072
|
+
stop() { playing = false; clearInterval(timer); timer = 0; if (ac) ac.suspend().catch(() => {}); },
|
|
1073
|
+
get on() { return playing; },
|
|
1074
|
+
};
|
|
1075
|
+
})();
|
|
1076
|
+
const paintMusic = () => {
|
|
1077
|
+
btnEl.classList.toggle('is-playing', music.on);
|
|
1078
|
+
btnEl.setAttribute('aria-pressed', String(music.on));
|
|
1079
|
+
btnEl.setAttribute('aria-label', music.on ? '暂停背景音乐' : '播放背景音乐');
|
|
1080
|
+
};
|
|
1081
|
+
btnEl.addEventListener('click', () => { if (music.on) music.stop(); else music.start().then(paintMusic); paintMusic(); });
|
|
1082
|
+
let resumeOnShow = false;
|
|
1083
|
+
document.addEventListener('visibilitychange', () => { // 切到后台就停,回来再续:别在别人不看时吵
|
|
1084
|
+
if (document.hidden) { resumeOnShow = music.on; if (music.on) { music.stop(); paintMusic(); } }
|
|
1085
|
+
else if (resumeOnShow) { music.start().then(paintMusic); resumeOnShow = false; }
|
|
1086
|
+
});
|
|
1087
|
+
|
|
1088
|
+
// ── 花瓣:轻量画布,页面不可见/减弱动效时不跑
|
|
1089
|
+
const petals = (function createPetals() {
|
|
1090
|
+
const cv = $('#petals');
|
|
1091
|
+
const ctx = cv.getContext && cv.getContext('2d');
|
|
1092
|
+
if (!ctx || reduceMotion) return { start() {} };
|
|
1093
|
+
const rgb = getComputedStyle(document.documentElement).getPropertyValue('--petal').trim() || '196,72,64';
|
|
1094
|
+
let W0 = 0; let H0 = 0; let raf = 0; let last = 0; let list = [];
|
|
1095
|
+
const resize = () => {
|
|
1096
|
+
const dpr = Math.min(window.devicePixelRatio || 1, 2);
|
|
1097
|
+
W0 = window.innerWidth; H0 = window.innerHeight;
|
|
1098
|
+
cv.width = Math.round(W0 * dpr); cv.height = Math.round(H0 * dpr);
|
|
1099
|
+
cv.style.width = W0 + 'px'; cv.style.height = H0 + 'px';
|
|
1100
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
1101
|
+
};
|
|
1102
|
+
const spawn = (anywhere) => ({
|
|
1103
|
+
x: Math.random() * W0, y: anywhere ? Math.random() * H0 : -20, r: 5 + Math.random() * 6,
|
|
1104
|
+
vy: 18 + Math.random() * 26, sway: 14 + Math.random() * 24, ph: Math.random() * 6.28,
|
|
1105
|
+
rot: Math.random() * 6.28, vr: (Math.random() - 0.5) * 1.6, a: 0.35 + Math.random() * 0.4,
|
|
1106
|
+
});
|
|
1107
|
+
const frame = (t) => {
|
|
1108
|
+
const dt = Math.min(0.05, (t - (last || t)) / 1000);
|
|
1109
|
+
last = t;
|
|
1110
|
+
ctx.clearRect(0, 0, W0, H0);
|
|
1111
|
+
for (const p of list) {
|
|
1112
|
+
p.y += p.vy * dt; p.ph += dt * 1.2; p.rot += p.vr * dt;
|
|
1113
|
+
const x = p.x + Math.sin(p.ph) * p.sway;
|
|
1114
|
+
if (p.y > H0 + 20) Object.assign(p, spawn(false));
|
|
1115
|
+
ctx.save(); ctx.translate(x, p.y); ctx.rotate(p.rot); ctx.scale(1, 0.55 + 0.45 * Math.abs(Math.cos(p.ph)));
|
|
1116
|
+
ctx.beginPath(); ctx.moveTo(0, -p.r);
|
|
1117
|
+
ctx.bezierCurveTo(p.r * 0.95, -p.r * 0.55, p.r * 0.75, p.r * 0.7, 0, p.r);
|
|
1118
|
+
ctx.bezierCurveTo(-p.r * 0.75, p.r * 0.7, -p.r * 0.95, -p.r * 0.55, 0, -p.r);
|
|
1119
|
+
ctx.fillStyle = `rgba(${rgb},${p.a})`; ctx.fill(); ctx.restore();
|
|
1120
|
+
}
|
|
1121
|
+
raf = requestAnimationFrame(frame);
|
|
1122
|
+
};
|
|
1123
|
+
return {
|
|
1124
|
+
start() {
|
|
1125
|
+
resize();
|
|
1126
|
+
list = Array.from({ length: W0 > 600 ? 22 : 14 }, () => spawn(true));
|
|
1127
|
+
window.addEventListener('resize', resize);
|
|
1128
|
+
document.addEventListener('visibilitychange', () => {
|
|
1129
|
+
cancelAnimationFrame(raf);
|
|
1130
|
+
if (!document.hidden) { last = 0; raf = requestAnimationFrame(frame); }
|
|
1131
|
+
});
|
|
1132
|
+
raf = requestAnimationFrame(frame);
|
|
1133
|
+
},
|
|
1134
|
+
};
|
|
1135
|
+
})();
|
|
1136
|
+
|
|
1137
|
+
// ── 入场动画:等封面打开后再开始观察,宾客才看得到首屏的入场
|
|
1138
|
+
function startReveal() {
|
|
1139
|
+
const els = $$('.reveal');
|
|
1140
|
+
if (reduceMotion || !('IntersectionObserver' in window)) { els.forEach((e) => e.classList.add('is-in')); return; }
|
|
1141
|
+
// threshold 取 0:比视口还高的元素永远到不了 0.5,用 0 + 底部内缩才不会「滚过去了还是看不见」
|
|
1142
|
+
const io = new IntersectionObserver((entries) => {
|
|
1143
|
+
entries.forEach((en) => { if (en.isIntersecting) { en.target.classList.add('is-in'); io.unobserve(en.target); } });
|
|
1144
|
+
}, { threshold: 0, rootMargin: '0px 0px -8% 0px' });
|
|
1145
|
+
els.forEach((e) => io.observe(e));
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
// ── 封面
|
|
1149
|
+
const gate = $('#gate');
|
|
1150
|
+
const app = $('#app');
|
|
1151
|
+
let opened = false;
|
|
1152
|
+
function afterOpen() {
|
|
1153
|
+
gate.hidden = true;
|
|
1154
|
+
document.body.classList.remove('is-locked');
|
|
1155
|
+
document.body.classList.add('is-open');
|
|
1156
|
+
app.removeAttribute('inert');
|
|
1157
|
+
btnEl.hidden = false;
|
|
1158
|
+
paintMusic();
|
|
1159
|
+
startReveal();
|
|
1160
|
+
petals.start();
|
|
1161
|
+
}
|
|
1162
|
+
function openGate(withMusic) {
|
|
1163
|
+
if (opened) return;
|
|
1164
|
+
opened = true;
|
|
1165
|
+
try { sessionStorage.setItem('invite-opened', '1'); } catch (e) { /* 隐私模式 */ }
|
|
1166
|
+
if (withMusic) music.start().then(paintMusic); // 必须在这次点击的同步调用栈里发起:它就是播放要的用户手势
|
|
1167
|
+
if (reduceMotion) { afterOpen(); return; }
|
|
1168
|
+
gate.classList.add('is-opening');
|
|
1169
|
+
setTimeout(() => gate.classList.add('is-gone'), 1250);
|
|
1170
|
+
setTimeout(afterOpen, 1650);
|
|
1171
|
+
}
|
|
1172
|
+
app.setAttribute('inert', '');
|
|
1173
|
+
$('#gateOpen').addEventListener('click', () => openGate(true));
|
|
1174
|
+
let skip = false;
|
|
1175
|
+
try { skip = sessionStorage.getItem('invite-opened') === '1'; } catch (e) { skip = false; }
|
|
1176
|
+
if (skip || (location.hash && location.hash.length > 1)) { opened = true; afterOpen(); } // 同一会话里再打开 / 从锚点链接进来:不重复拦封面
|
|
1177
|
+
})();
|
|
1178
|
+
</script>
|
|
1179
|
+
</body>
|
|
1180
|
+
</html>
|