Initialize AdCloud console with React and TypeScript
This commit is contained in:
@@ -0,0 +1 @@
|
||||
VITE_API_BASE_URL=http://localhost:8000
|
||||
@@ -0,0 +1,6 @@
|
||||
node_modules/
|
||||
dist/
|
||||
.DS_Store
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
@@ -0,0 +1,45 @@
|
||||
# AdCloud · 开发者控制台
|
||||
|
||||
管理应用接入、广告位与流量收益。 React + TypeScript + Vite 独立前端项目。
|
||||
|
||||
## 启动
|
||||
|
||||
需要 Node.js 20.19+ 或 22.12+。
|
||||
|
||||
```sh
|
||||
npm ci
|
||||
cp .env.example .env
|
||||
npm run dev
|
||||
```
|
||||
|
||||
开发地址:`http://localhost:5174`。通过 `VITE_API_BASE_URL` 配置共享后端地址,默认 `http://localhost:8000`。
|
||||
`VITE_` 配置会进入浏览器代码,不用于保存服务器密钥。
|
||||
|
||||
```sh
|
||||
npm run typecheck
|
||||
npm run build
|
||||
npm run preview
|
||||
```
|
||||
|
||||
## 当前范围
|
||||
|
||||
提供导航、空数据概览、内存中的应用草稿预览,以及共享后端的 `/health` 状态检查。
|
||||
草稿刷新即清空;尚未实现登录、持久化、广告发布或业务报表。界面中的 `—` 表示尚无真实数据。
|
||||
|
||||
后续业务范围:应用与广告位管理、SDK / coding agent 接入文档、流量及收益报表。
|
||||
|
||||
## 项目边界
|
||||
|
||||
- `adcloud-advertiser-console`:广告商投放、预算和效果管理。
|
||||
- `adcloud-developer-console`:开发者应用、广告位、接入和收益管理。
|
||||
- `adcloud-server`:共享后端,统一负责身份认证、角色权限、埋点、广告投放和报表 API。
|
||||
- `adcloud-react-sdk`:提供给第三方应用集成的 SDK。
|
||||
|
||||
两套控制台独立构建和部署。角色权限与数据隔离需要由后端校验。
|
||||
本项目目前只调用 `/health`;后端业务接口未定义,之后可按 OpenAPI 生成共用管理 API 客户端。
|
||||
|
||||
后端默认 CORS 允许 3000、5173 端口。要同时运行两套控制台,在 `adcloud-server` 目录运行:
|
||||
|
||||
```sh
|
||||
CORS_ORIGINS=http://localhost:5173,http://localhost:5174 uv run uvicorn adcloud_server.main:app --reload
|
||||
```
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>AdCloud · 开发者控制台</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
Generated
+1197
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "@entroia/adcloud-developer-console",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"build": "tsc --noEmit && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
"typescript": "^5.0.0",
|
||||
"vite": "^7.0.0"
|
||||
}
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
import { useEffect, useState, type FormEvent } from "react";
|
||||
|
||||
const tabs = ["总览", "应用管理", "接入数据"];
|
||||
const metrics = ["预估收益", "广告请求", "展示次数", "填充率"];
|
||||
const apiBaseUrl = (import.meta.env.VITE_API_BASE_URL || "http://localhost:8000").replace(/\/+$/, "");
|
||||
|
||||
export default function App() {
|
||||
const [tab, setTab] = useState(tabs[0]);
|
||||
const [connection, setConnection] = useState("检查中");
|
||||
const [name, setName] = useState("");
|
||||
const [drafts, setDrafts] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
fetch(`${apiBaseUrl}/health`, { signal: controller.signal })
|
||||
.then(async (response) => {
|
||||
if (!response.ok) throw new Error("Backend unavailable");
|
||||
const body = await response.json();
|
||||
if (body.status !== "ok") throw new Error("Invalid health response");
|
||||
if (!controller.signal.aborted) setConnection("已连接");
|
||||
})
|
||||
.catch(() => { if (!controller.signal.aborted) setConnection("未连接"); });
|
||||
return () => controller.abort();
|
||||
}, []);
|
||||
|
||||
function saveDraft(event: FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
const trimmed = name.trim();
|
||||
if (!trimmed) return;
|
||||
setDrafts((current) => [...current, trimmed]);
|
||||
setName("");
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="layout">
|
||||
<aside>
|
||||
<a className="brand" href="/">AdCloud<span>开发者控制台</span></a>
|
||||
<nav aria-label="主导航">
|
||||
{tabs.map((item) => <button key={item} className={tab === item ? "active" : ""} aria-current={tab === item ? "page" : undefined} onClick={() => setTab(item)}>{item}</button>)}
|
||||
</nav>
|
||||
<p className="aside-note">面向开发者与 coding agent 的广告云</p>
|
||||
</aside>
|
||||
<main>
|
||||
<header><span>开发者控制台 / {tab}</span><span className="badge">开发预览 · 登录待接入</span></header>
|
||||
<section className="intro"><p className="eyebrow">ADCLOUD WORKSPACE</p><h1>{tab}</h1><p>管理应用接入、广告位与流量收益。</p></section>
|
||||
<p className="notice">当前为前端脚手架。业务 API、账号登录和真实数据尚未接入。后端健康检查:<strong role="status">{connection}</strong></p>
|
||||
{tab === tabs[0] && <>
|
||||
<section className="metrics" aria-label="数据概览">{metrics.map((metric) => <article key={metric}><p>{metric}</p><strong>—</strong><small>等待业务数据接入</small></article>)}</section>
|
||||
<section className="panel"><h2>开始使用</h2><p>在{tabs[1]}中创建本地应用草稿,预览工作流程。</p><button className="primary" onClick={() => setTab(tabs[1])}>进入{tabs[1]}</button></section>
|
||||
</>}
|
||||
{tab === tabs[1] && <section className="panel">
|
||||
<h2>应用草稿</h2><p>仅保存在当前页面内存中,刷新后清空,不会发布或提交到服务器。</p>
|
||||
<form onSubmit={saveDraft}><label htmlFor="draft-name">应用名称</label><div className="form-row"><input id="draft-name" value={name} onChange={(event) => setName(event.target.value)} maxLength={100} required placeholder="输入名称" /><button className="primary" type="submit">创建本地草稿</button></div></form>
|
||||
{drafts.length === 0 ? <p className="empty">暂无草稿</p> : <ul className="drafts">{drafts.map((draft, index) => <li key={index}><span>{draft}</span><span className="badge">本地草稿</span></li>)}</ul>}
|
||||
</section>}
|
||||
{tab === tabs[2] && <section className="panel empty"><h2>暂无数据</h2><p>连接业务数据接口后,将在这里展示应用请求、广告展示与收益数据。</p></section>}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import App from "./App";
|
||||
import "./styles.css";
|
||||
|
||||
createRoot(document.getElementById("root")!).render(<StrictMode><App /></StrictMode>);
|
||||
@@ -0,0 +1,40 @@
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; color: #17243c; background: #f5f7fb; }
|
||||
button, input { font: inherit; }
|
||||
button { cursor: pointer; }
|
||||
button:focus-visible, a:focus-visible { outline: 3px solid #739cff; outline-offset: 3px; }
|
||||
.layout { min-height: 100vh; display: grid; grid-template-columns: 240px minmax(0, 1fr); }
|
||||
aside { background: #11203b; color: #dce5f7; padding: 34px 24px; display: flex; flex-direction: column; }
|
||||
.brand { color: white; text-decoration: none; font-size: 29px; font-weight: 750; letter-spacing: -1px; }
|
||||
.brand span { display: block; font-size: 13px; font-weight: 400; letter-spacing: 1px; color: #a9bad8; margin-top: 8px; }
|
||||
nav { display: grid; gap: 10px; margin-top: 48px; }
|
||||
nav button { border: 0; text-align: left; border-radius: 8px; padding: 14px 16px; background: transparent; color: #b8c7df; }
|
||||
nav button.active, nav button:hover { background: #263b60; color: white; }
|
||||
.aside-note { margin-top: auto; padding-top: 60px; font-size: 12px; line-height: 1.8; color: #91a5c6; }
|
||||
main { padding: 0 42px 48px; max-width: 1500px; width: 100%; }
|
||||
header { min-height: 80px; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #e0e5ed; gap: 16px; color: #6e7b90; font-size: 13px; }
|
||||
.badge { background: #e9eef6; color: #62748e; font-size: 12px; padding: 7px 10px; border-radius: 6px; white-space: nowrap; }
|
||||
.intro { margin: 38px 0 24px; }
|
||||
.eyebrow { color: #3c6ce3; letter-spacing: 2px; font-size: 11px; font-weight: 700; }
|
||||
h1 { font-size: 32px; margin: 12px 0; letter-spacing: -1px; }
|
||||
p { line-height: 1.7; }
|
||||
.intro > p:last-child, .panel p { color: #738097; }
|
||||
.notice { background: #edf2ff; border: 1px solid #dce5fc; padding: 14px 18px; border-radius: 10px; font-size: 13px; color: #50638b; }
|
||||
.metrics { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 18px; margin: 26px 0; }
|
||||
.metrics article, .panel { background: white; border: 1px solid #e2e7ef; border-radius: 12px; padding: 24px; }
|
||||
.metrics p { color: #738097; margin: 0 0 20px; font-size: 13px; }
|
||||
.metrics strong { display: block; font-size: 32px; margin-bottom: 14px; }
|
||||
.metrics small { color: #96a2b4; font-size: 11px; }
|
||||
.panel { margin-top: 26px; }
|
||||
h2 { font-size: 18px; margin: 0 0 12px; }
|
||||
.primary { background: #386be9; color: white; border: 0; border-radius: 7px; padding: 12px 18px; }
|
||||
.primary:hover { background: #2856c6; }
|
||||
form { margin: 28px 0; }
|
||||
label { display: block; margin-bottom: 10px; font-size: 14px; }
|
||||
.form-row { display: flex; gap: 12px; }
|
||||
input { border: 1px solid #ccd5e4; border-radius: 7px; padding: 12px; flex: 1; min-width: 0; }
|
||||
.empty { padding: 46px 24px; text-align: center; color: #96a2b4; }
|
||||
.drafts { list-style: none; padding: 0; }
|
||||
.drafts li { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 16px 0; border-top: 1px solid #edf0f5; overflow-wrap: anywhere; }
|
||||
@media (max-width: 1000px) { .metrics { grid-template-columns: repeat(2, minmax(0, 1fr)); } main { padding: 0 24px 32px; } }
|
||||
@media (max-width: 640px) { .layout { grid-template-columns: 1fr; } aside { padding: 20px; } nav { grid-template-columns: repeat(3, 1fr); margin-top: 20px; } nav button { text-align: center; padding: 12px 6px; } .aside-note { display: none; } header { flex-wrap: wrap; padding: 16px 0; } .form-row { flex-direction: column; } }
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"lib": [
|
||||
"ES2022",
|
||||
"DOM",
|
||||
"DOM.Iterable"
|
||||
],
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"jsx": "react-jsx",
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"skipLibCheck": true,
|
||||
"isolatedModules": true,
|
||||
"types": [
|
||||
"vite/client"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src",
|
||||
"vite.config.ts"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import { defineConfig } from "vite";
|
||||
|
||||
export default defineConfig({ server: { port: 5174, strictPort: true } });
|
||||
Reference in New Issue
Block a user