feat(frontend): add management portal landing page

This commit is contained in:
david
2026-07-31 15:00:44 +08:00
parent ad6a914450
commit 486fcc94e9
16 changed files with 2536 additions and 0 deletions

2
frontend/site/.npmrc Normal file
View File

@@ -0,0 +1,2 @@
fund=false
audit=false

View File

@@ -0,0 +1,4 @@
{
"d1": null,
"r2": null
}

17
frontend/site/AGENTS.md Normal file
View File

@@ -0,0 +1,17 @@
# Prototype Instructions
Run the local server yourself and open the preview in the browser available to this environment. Do not give the user server-start instructions when you can run it.
Before making substantial visual changes, use the Product Design plugin's `get-context` skill when the visual source is unclear or no longer matches the current goal. When the user gives durable prototype-specific design feedback, preferences, or decisions, record them in `AGENTS.md`.
When implementing from a selected generated mock, treat that image as the source of truth for layout, component anatomy, density, spacing, color, typography, visible content, and hierarchy.
Build app UI in `src/`. Keep `.openai/hosting.json`, `worker/index.js`, `scripts/prepare-sites-build.mjs`, and `tests/sites-worker.test.mjs` intact so the same local prototype can be handed to Sites. Before a Sites handoff, run `npm run build` and `npm run test:sites`; the build must leave `dist/client/index.html`, `dist/server/index.js`, and `dist/.openai/hosting.json`.
## 和气门户设计约束
- 本项目是品牌化管理入口门户,不承载统一登录、运营数据或业务编辑。
- 桌面端优先一屏完成,移动端允许自然纵向滚动。
- 视觉语言延续三套管理系统登录页的半写实等距插画,并使用平台蓝、气站橙、配送青绿区分入口。
- 三个入口始终在新标签页打开,分别指向 `p.heqiapp.com``z.heqiapp.com``d.heqiapp.com`
- 品牌主标语固定为“安全连接每一程”,不要扩展新闻、案例、价格或其他官网栏目。

View File

@@ -0,0 +1,64 @@
# 和气管理入口门户设计 QA
## 对照目标
- Source visual truth: `/Users/david/.codex/generated_images/019fb64b-4fca-7e12-a4cd-c9b59661af31/call_ioQ3ZBsql0CDatVAv3EXSRmE.png`
- Generated hero asset: `/Users/david/Documents/work/platforms/frontend/site/public/assets/heqi-ecosystem-hero.jpg`
- Desktop implementation: `/tmp/heqi-site-qa-desktop-final.jpg`
- Mobile implementation: `/tmp/heqi-site-qa-mobile-revised.jpg`
- Final side-by-side comparison: `/tmp/heqi-site-source-vs-implementation-final.jpg`
## 归一化条件
- Source pixels: `1536 x 1024`.
- Source normalized pixels: `1440 x 1024`(先缩放至 `1440 x 960`,再以页面底色补齐上下各 `32px`,不拉伸主视觉)。
- Desktop implementation pixels and CSS viewport: `1440 x 1024`, density `1:1`.
- Mobile implementation CSS viewport: `390 x 844`; full-page screenshot包含自然纵向内容。
- State: 首页初始态,三个系统入口未悬停、未点击。
## Full-view comparison evidence
最终并排对照显示实现保留了选定稿的核心构图:顶部品牌与可信说明、居中的主标语与说明、横向三段系统入口,以及下方由蓝、橙、青绿路线连接的半写实等距生态场景。桌面端在 `1440 x 1024` 内完整呈现,没有页面滚动或横向溢出。
实现使用单独生成的无文字生态插画作为真实图片资产,所有品牌文案、系统名称和入口按钮均为可访问的 HTML。插画主体与选定稿保持同一业务顺序和品牌语言但建筑细节属于同风格的重新生成不要求逐对象像素一致。
## Focused region comparison evidence
- 标题区Noto Sans SC Variable 的字重、行高和居中层级与选定稿接近,标题与说明均无截断。
- 入口区:三组编号、分隔线、标题、说明和色彩按钮保持同一解剖结构;按钮使用统一箭头图标,未使用字符或 CSS 绘图替代。
- 移动端:三个入口改为纵向可点击行,按钮收敛为 `46px` 圆形触点;品牌说明被主动分成两行,避免孤字。
## Required fidelity surfaces
- Fonts and typography: 使用 Noto Sans SC Variable标题、入口标题、说明与辅助文案建立清晰层级桌面与移动端均无异常换行或截断。
- Spacing and layout rhythm: 桌面端保持一屏布局,入口与对应插画停靠点对齐;移动端使用 `18px` 安全边距和 `12px` 入口间距。
- Colors and visual tokens: 平台蓝 `#1265e8`、气站橙 `#dc7100`、配送青绿 `#068f8b` 与选定稿一致映射到编号、按钮和插画路线。
- Image quality and asset fidelity: 主视觉为独立 ImageGen 生成并压缩的 JPEGLogo 复用仓库原始 SVG没有占位图、CSS 插画或手绘 SVG。
- Copy and content: 品牌、标语、三套系统名称、定位文案及跳转地址均与确认内容一致。
## Interaction and browser checks
- 三个入口均为语义化链接,分别指向 `http://p.heqiapp.com``http://z.heqiapp.com``http://d.heqiapp.com`
- 三个入口均配置 `target="_blank"``rel="noreferrer"` 和明确的无障碍名称。
- 链接具有 hover 与 `:focus-visible` 状态,并在减少动态效果偏好下关闭位移动画。
- 浏览器在 `1440 x 1024``390 x 844` 完成渲染检查;移动端无横向溢出。
- 浏览器控制台未发现 warning 或 error。
## Comparison history
1. 初次桌面对照发现主标语整体偏低,入口文字与插画建筑重叠,属于 P2 级层级偏差。
2. 将标题区上移,并把主视觉场景向下调整,重新捕获相同 `1440 x 1024` 视口;入口与插画停靠点分离。
3. 初次移动端检查发现说明文字末尾单字成行,属于 P2 级排版问题。
4. 将说明拆为语义连续的两行,重新捕获 `390 x 844`;标题、说明与三个入口均无孤字、截断或溢出。
5. 最终并排对照未发现剩余 P0、P1 或 P2 问题。
## Findings
- 无 P0、P1 或 P2 问题。
- P3: 实现中的生态插画为针对响应式页面重新生成的同风格资产,具体建筑和车辆位置与视觉稿存在轻微差异,但信息顺序、路线颜色和整体构图一致。
## Follow-up polish
- 若后续确定正式官网域名,可补充 canonical 与 Open Graph 元信息。
final result: passed

18
frontend/site/index.html Normal file
View File

@@ -0,0 +1,18 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta
name="description"
content="和气智能瓶阀、气站运营与配送履约一体化管理入口"
/>
<meta name="theme-color" content="#f7faff" />
<link rel="icon" type="image/svg+xml" href="/assets/heqi-logo.svg" />
<title>和气 · 管理系统入口</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

1691
frontend/site/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,20 @@
{
"name": "site",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build && node scripts/prepare-sites-build.mjs",
"preview": "vite preview",
"test:sites": "node --test tests/sites-worker.test.mjs"
},
"dependencies": {
"@fontsource-variable/noto-sans-sc": "^5.3.0",
"@phosphor-icons/react": "^2.1.10",
"@vitejs/plugin-react": "5.0.4",
"react": "19.2.0",
"react-dom": "19.2.0",
"vite": "6.4.2"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 428 KiB

View File

@@ -0,0 +1,12 @@
<svg width="33" height="33" viewBox="0 0 33 33" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.37754 16.9795L12.7498 9.43027C14.7163 7.41663 17.9428 7.37837 19.9564 9.34482C19.9852 9.37297 20.0137 9.40145 20.0418 9.43027L20.1221 9.51243C22.1049 11.5429 22.1049 14.7847 20.1221 16.8152L12.7498 24.3644C10.7834 26.378 7.55686 26.4163 5.54322 24.4498C5.5144 24.4217 5.48592 24.3932 5.45777 24.3644L5.37754 24.2822C3.39468 22.2518 3.39468 19.0099 5.37754 16.9795Z" fill="#12D2AC"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.0479 9.43034L27.3399 16.8974C29.3674 18.9735 29.3674 22.2883 27.3399 24.3644C25.3735 26.3781 22.147 26.4163 20.1333 24.4499C20.1045 24.4217 20.076 24.3933 20.0479 24.3644L12.7558 16.8974C10.7284 14.8213 10.7284 11.5065 12.7558 9.43034C14.7223 7.4167 17.9488 7.37844 19.9624 9.34489C19.9912 9.37304 20.0197 9.40152 20.0479 9.43034Z" fill="#307AF2"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.1321 9.52163L23.6851 13.1599L16.3931 20.627L9.10103 13.1599L12.6541 9.52163C14.6707 7.45664 17.9794 7.4174 20.0444 9.434C20.074 9.46286 20.1032 9.49207 20.1321 9.52163Z" fill="#0057FE"/>
</g>
<defs>
<clipPath id="clip0">
<rect width="26" height="19" fill="white" transform="translate(3.5 7)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,21 @@
#!/usr/bin/env node
import { copyFileSync, existsSync, mkdirSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const dist = path.join(root, "dist");
const index = path.join(dist, "client", "index.html");
const worker = path.join(root, "worker", "index.js");
const hosting = path.join(root, ".openai", "hosting.json");
for (const file of [index, worker, hosting]) {
if (!existsSync(file)) throw new Error("Missing Sites build input: " + file);
}
mkdirSync(path.join(dist, "server"), { recursive: true });
mkdirSync(path.join(dist, ".openai"), { recursive: true });
copyFileSync(worker, path.join(dist, "server", "index.js"));
copyFileSync(hosting, path.join(dist, ".openai", "hosting.json"));
console.log("Prepared Sites build: dist/server/index.js and dist/.openai/hosting.json");

100
frontend/site/src/App.jsx Normal file
View File

@@ -0,0 +1,100 @@
import {
ArrowRight,
ArrowSquareOut,
ShieldCheck,
} from "@phosphor-icons/react";
const portals = [
{
number: "01",
title: "平台总后台",
description: "全局治理 · 安全运营 · 数据协同",
href: "http://p.heqiapp.com",
tone: "platform",
},
{
number: "02",
title: "气站管理后台",
description: "站点经营 · 履约管理 · 安全服务",
href: "http://z.heqiapp.com",
tone: "station",
},
{
number: "03",
title: "配送点管理后台",
description: "智能调度 · 高效配送 · 全程可追溯",
href: "http://d.heqiapp.com",
tone: "delivery",
},
];
function PortalLink({ portal }) {
return (
<a
className={`portal portal--${portal.tone}`}
href={portal.href}
target="_blank"
rel="noreferrer"
aria-label={`进入${portal.title},将在新标签页打开`}
>
<div className="portal__heading">
<span className="portal__number" aria-hidden="true">
{portal.number}
</span>
<span className="portal__divider" aria-hidden="true" />
<div>
<h2>{portal.title}</h2>
<p>{portal.description}</p>
</div>
</div>
<span className="portal__action">
进入系统
<ArrowRight size={22} weight="bold" aria-hidden="true" />
</span>
</a>
);
}
export function App() {
return (
<main className="site-shell">
<img
className="ecosystem-scene"
src="/assets/heqi-ecosystem-hero.jpg"
alt=""
aria-hidden="true"
/>
<header className="site-header">
<a className="brand" href="/" aria-label="和气首页">
<img src="/assets/heqi-logo.svg" alt="" />
<span>和气</span>
</a>
<div className="trust-line">
<ShieldCheck size={20} weight="regular" aria-hidden="true" />
<span>安全可靠 · 稳定高效 · 智能互联</span>
</div>
</header>
<section className="hero" aria-labelledby="hero-title">
<p className="hero__eyebrow">HEQI SMART GAS PLATFORM</p>
<h1 id="hero-title">安全连接每一程</h1>
<p className="hero__description">
<span>智能瓶阀气站运营与配送履约</span>
<span>的一体化管理入口</span>
</p>
</section>
<nav className="portal-grid" aria-label="管理系统入口">
{portals.map((portal) => (
<PortalLink key={portal.href} portal={portal} />
))}
</nav>
<p className="external-note">
<ArrowSquareOut size={17} weight="regular" aria-hidden="true" />
系统将在新标签页中打开
</p>
</main>
);
}

View File

@@ -0,0 +1,10 @@
import React from "react";
import { createRoot } from "react-dom/client";
import { App } from "./App.jsx";
import "./styles.css";
createRoot(document.getElementById("root")).render(
<React.StrictMode>
<App />
</React.StrictMode>,
);

View File

@@ -0,0 +1,475 @@
@import "@fontsource-variable/noto-sans-sc";
:root {
font-family:
"Noto Sans SC Variable", "PingFang SC", "Microsoft YaHei", sans-serif;
color: #202936;
background: #f7faff;
font-synthesis: none;
text-rendering: optimizeLegibility;
--platform: #1265e8;
--platform-deep: #0c4fbd;
--station: #dc7100;
--station-deep: #b85a00;
--delivery: #068f8b;
--delivery-deep: #08746f;
}
* {
box-sizing: border-box;
}
html {
min-width: 320px;
background: #f7faff;
}
body {
margin: 0;
min-width: 320px;
min-height: 100vh;
color: #202936;
background: #f7faff;
-webkit-font-smoothing: antialiased;
}
#root {
min-height: 100vh;
}
a {
color: inherit;
}
.site-shell {
position: relative;
isolation: isolate;
min-height: 100vh;
min-height: 100dvh;
padding: 38px clamp(28px, 4.6vw, 72px) 30px;
overflow: hidden;
background: #f7faff;
}
.ecosystem-scene {
position: absolute;
inset: 108px 0 0;
z-index: -1;
width: 100%;
height: calc(100% - 108px);
object-fit: cover;
object-position: center bottom;
user-select: none;
pointer-events: none;
}
.site-header {
display: flex;
align-items: center;
justify-content: space-between;
width: min(100%, 1320px);
margin: 0 auto;
}
.brand {
display: inline-flex;
gap: 12px;
align-items: center;
color: #17212d;
font-weight: 650;
font-size: 27px;
line-height: 1;
letter-spacing: 0.04em;
text-decoration: none;
}
.brand img {
width: 43px;
height: 43px;
}
.trust-line {
display: inline-flex;
gap: 9px;
align-items: center;
color: #687586;
font-size: 14px;
line-height: 22px;
letter-spacing: 0.02em;
}
.hero {
position: relative;
z-index: 1;
width: min(100%, 900px);
margin: clamp(34px, 4.5vh, 46px) auto 0;
text-align: center;
}
.hero__eyebrow {
margin: 0 0 12px;
color: #6d7f95;
font-weight: 620;
font-size: 12px;
line-height: 20px;
letter-spacing: 0.2em;
}
.hero h1 {
margin: 0;
color: #202936;
font-weight: 680;
font-size: clamp(48px, 4.2vw, 66px);
line-height: 1.16;
letter-spacing: 0.035em;
}
.hero__description {
margin: 18px auto 0;
color: #637083;
font-size: clamp(16px, 1.35vw, 19px);
line-height: 30px;
letter-spacing: 0.025em;
}
.hero__description span {
display: inline;
}
.portal-grid {
position: relative;
z-index: 2;
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: clamp(30px, 4vw, 64px);
width: min(100%, 1280px);
margin: clamp(48px, 6.5vh, 66px) auto 0;
}
.portal {
--accent: var(--platform);
--accent-deep: var(--platform-deep);
display: flex;
min-width: 0;
flex-direction: column;
gap: 24px;
padding: 8px 10px 12px;
border-radius: 14px;
text-decoration: none;
outline: none;
transition:
background-color 180ms ease,
box-shadow 180ms ease,
transform 180ms ease;
}
.portal--station {
--accent: var(--station);
--accent-deep: var(--station-deep);
}
.portal--delivery {
--accent: var(--delivery);
--accent-deep: var(--delivery-deep);
}
.portal:hover {
background: rgba(255, 255, 255, 0.82);
box-shadow: 0 18px 46px rgba(37, 58, 84, 0.12);
transform: translateY(-4px);
}
.portal:focus-visible {
background: rgba(255, 255, 255, 0.9);
box-shadow:
0 0 0 4px rgba(18, 101, 232, 0.18),
0 18px 46px rgba(37, 58, 84, 0.12);
}
.portal__heading {
display: grid;
grid-template-columns: auto 1px minmax(0, 1fr);
gap: 20px;
align-items: center;
min-height: 68px;
}
.portal__number {
color: var(--accent);
font-weight: 660;
font-size: clamp(46px, 4vw, 60px);
line-height: 1;
letter-spacing: -0.035em;
}
.portal__divider {
width: 1px;
height: 54px;
background: color-mix(in srgb, var(--accent) 35%, transparent);
}
.portal h2 {
margin: 0;
color: #202936;
font-weight: 650;
font-size: clamp(20px, 1.7vw, 25px);
line-height: 1.35;
}
.portal p {
margin: 7px 0 0;
color: #6b7787;
font-size: 13px;
line-height: 21px;
white-space: nowrap;
}
.portal__action {
display: inline-flex;
width: min(100%, 194px);
min-height: 50px;
gap: 18px;
align-items: center;
justify-content: center;
margin-left: 78px;
color: #fff;
background: var(--accent);
border-radius: 9px;
font-weight: 600;
font-size: 15px;
line-height: 22px;
box-shadow: 0 9px 22px color-mix(in srgb, var(--accent) 24%, transparent);
transition: background-color 180ms ease;
}
.portal:hover .portal__action,
.portal:focus-visible .portal__action {
background: var(--accent-deep);
}
.external-note {
position: absolute;
right: clamp(28px, 4.6vw, 72px);
bottom: 24px;
z-index: 2;
display: inline-flex;
gap: 7px;
align-items: center;
margin: 0;
color: rgba(60, 76, 94, 0.72);
font-size: 12px;
line-height: 20px;
}
@media (max-width: 1020px) {
.site-shell {
padding-inline: 28px;
}
.portal-grid {
gap: 20px;
}
.portal {
padding-inline: 4px;
}
.portal__heading {
gap: 13px;
}
.portal__number {
font-size: 44px;
}
.portal h2 {
font-size: 19px;
}
.portal p {
font-size: 12px;
}
.portal__action {
margin-left: 58px;
}
}
@media (max-width: 760px) {
.site-shell {
display: flex;
min-height: 100dvh;
flex-direction: column;
padding: 24px 18px 0;
overflow: hidden;
background: #f7faff;
}
.ecosystem-scene {
position: relative;
inset: auto;
order: 5;
width: calc(100% + 36px);
height: auto;
margin: 34px -18px 0;
object-fit: contain;
}
.brand {
gap: 9px;
font-size: 22px;
}
.brand img {
width: 36px;
height: 36px;
}
.trust-line {
display: none;
}
.hero {
margin-top: 48px;
}
.hero__eyebrow {
margin-bottom: 9px;
font-size: 10px;
letter-spacing: 0.15em;
}
.hero h1 {
font-size: clamp(37px, 11vw, 48px);
line-height: 1.18;
}
.hero__description {
max-width: 330px;
margin-top: 14px;
font-size: 15px;
line-height: 25px;
}
.hero__description span {
display: block;
}
.portal-grid {
grid-template-columns: 1fr;
gap: 12px;
margin-top: 38px;
}
.portal {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 16px;
align-items: center;
padding: 15px;
background: rgba(255, 255, 255, 0.84);
border: 1px solid rgba(104, 132, 164, 0.14);
border-radius: 12px;
box-shadow: 0 10px 28px rgba(35, 60, 88, 0.08);
}
.portal:hover {
transform: none;
}
.portal__heading {
grid-template-columns: auto 1px minmax(0, 1fr);
gap: 12px;
min-height: auto;
}
.portal__number {
font-size: 32px;
}
.portal__divider {
height: 44px;
}
.portal h2 {
font-size: 17px;
}
.portal p {
margin-top: 4px;
overflow: hidden;
font-size: 11px;
line-height: 18px;
text-overflow: ellipsis;
white-space: nowrap;
}
.portal__action {
width: 46px;
min-height: 46px;
margin: 0;
border-radius: 50%;
font-size: 0;
}
.portal__action svg {
width: 20px;
height: 20px;
}
.external-note {
position: static;
order: 4;
justify-content: center;
margin: 18px 0 0;
font-size: 11px;
}
}
@media (max-width: 380px) {
.site-shell {
padding-inline: 14px;
}
.ecosystem-scene {
width: calc(100% + 28px);
margin-inline: -14px;
}
.portal {
padding-inline: 12px;
}
.portal__heading {
gap: 9px;
}
.portal__number {
font-size: 29px;
}
.portal h2 {
font-size: 16px;
}
}
@media (max-height: 790px) and (min-width: 761px) {
.site-shell {
padding-top: 26px;
}
.hero {
margin-top: 38px;
}
.portal-grid {
margin-top: 40px;
}
}
@media (prefers-reduced-motion: reduce) {
.portal,
.portal__action {
transition: none;
}
}

View File

@@ -0,0 +1,68 @@
import assert from "node:assert/strict";
import { access } from "node:fs/promises";
import test from "node:test";
import worker from "../worker/index.js";
test("serves existing static assets without a fallback", async () => {
const calls = [];
const response = await worker.fetch(new Request("https://example.test/assets/app.js"), {
ASSETS: {
fetch: async (request) => {
calls.push(new URL(request.url).pathname);
return new Response("asset", { status: 200 });
},
},
});
assert.equal(response.status, 200);
assert.deepEqual(calls, ["/assets/app.js"]);
});
test("falls back to index.html for an unknown app route", async () => {
const calls = [];
const response = await worker.fetch(
new Request("https://example.test/flow/step-two?source=share", {
headers: { accept: "text/html" },
}),
{
ASSETS: {
fetch: async (request) => {
const url = new URL(request.url);
calls.push(url.pathname + url.search);
return new Response(url.pathname === "/index.html" ? "app" : "missing", {
status: url.pathname === "/index.html" ? 200 : 404,
});
},
},
},
);
assert.equal(response.status, 200);
assert.deepEqual(calls, ["/flow/step-two?source=share", "/index.html"]);
});
test("does not turn missing API or write requests into the app shell", async () => {
for (const request of [
new Request("https://example.test/api/missing", { headers: { accept: "application/json" } }),
new Request("https://example.test/flow", { method: "POST", headers: { accept: "text/html" } }),
]) {
let calls = 0;
const response = await worker.fetch(request, {
ASSETS: {
fetch: async () => {
calls += 1;
return new Response("missing", { status: 404 });
},
},
});
assert.equal(response.status, 404);
assert.equal(calls, 1);
}
});
test("emits the files required by Sites packaging", async () => {
await access(new URL("../dist/client/index.html", import.meta.url));
await access(new URL("../dist/server/index.js", import.meta.url));
await access(new URL("../dist/.openai/hosting.json", import.meta.url));
});

View File

@@ -0,0 +1,19 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
export default defineConfig({
build: {
outDir: "dist/client",
},
optimizeDeps: {
include: ["react", "react-dom/client"],
},
server: {
host: "0.0.0.0",
allowedHosts: ["terminal.local"],
warmup: {
clientFiles: ["./src/main.jsx"],
},
},
plugins: [react()],
});

View File

@@ -0,0 +1,15 @@
export default {
async fetch(request, env) {
const response = await env.ASSETS.fetch(request);
const acceptsHtml = request.headers.get("accept")?.includes("text/html");
if (response.status !== 404 || !acceptsHtml || !["GET", "HEAD"].includes(request.method)) {
return response;
}
const indexUrl = new URL(request.url);
indexUrl.pathname = "/index.html";
indexUrl.search = "";
return env.ASSETS.fetch(new Request(indexUrl, request));
},
};