test2123213
1062 行
29 KiB
HTML
1062 行
29 KiB
HTML
<!doctype html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<meta name="description" content="武汉 OPC 社区活动征集与 AI 共创助手">
|
||
<title>武汉 OPC|和 AI 共创一场活动</title>
|
||
<style>
|
||
:root {
|
||
color-scheme: light;
|
||
--river: #0d5c73;
|
||
--river-deep: #12323d;
|
||
--lake: #dff3f1;
|
||
--paper: #f7fbfa;
|
||
--lotus: #d94c71;
|
||
--sun: #f4b860;
|
||
--ink: #152a32;
|
||
--muted: #62767d;
|
||
--line: #bad9d7;
|
||
--white: #fff;
|
||
font-family: "Noto Sans SC", "Microsoft YaHei", "PingFang SC", sans-serif;
|
||
}
|
||
|
||
* {
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
html {
|
||
scroll-behavior: smooth;
|
||
}
|
||
|
||
body {
|
||
min-width: 320px;
|
||
margin: 0;
|
||
background:
|
||
linear-gradient(90deg, rgb(13 92 115 / 4%) 1px, transparent 1px) 0 0 / 48px 48px,
|
||
linear-gradient(rgb(13 92 115 / 4%) 1px, transparent 1px) 0 0 / 48px 48px,
|
||
var(--paper);
|
||
color: var(--ink);
|
||
}
|
||
|
||
button,
|
||
input,
|
||
select,
|
||
textarea {
|
||
font: inherit;
|
||
}
|
||
|
||
button,
|
||
a {
|
||
-webkit-tap-highlight-color: transparent;
|
||
}
|
||
|
||
a {
|
||
color: inherit;
|
||
}
|
||
|
||
.page {
|
||
width: min(1180px, calc(100% - 32px));
|
||
margin: 0 auto;
|
||
}
|
||
|
||
.masthead {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
min-height: 76px;
|
||
border-bottom: 1px solid var(--line);
|
||
}
|
||
|
||
.brand {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
text-decoration: none;
|
||
}
|
||
|
||
.brand-mark {
|
||
display: grid;
|
||
width: 42px;
|
||
height: 42px;
|
||
place-items: center;
|
||
border-radius: 50%;
|
||
background: var(--river);
|
||
color: white;
|
||
font-family: ui-monospace, "SFMono-Regular", Consolas, monospace;
|
||
font-size: 0.75rem;
|
||
font-weight: 800;
|
||
letter-spacing: -0.06em;
|
||
}
|
||
|
||
.brand strong {
|
||
display: block;
|
||
font-size: 1rem;
|
||
}
|
||
|
||
.brand span {
|
||
color: var(--muted);
|
||
font-size: 0.75rem;
|
||
}
|
||
|
||
nav {
|
||
display: flex;
|
||
gap: 26px;
|
||
}
|
||
|
||
nav a {
|
||
color: var(--muted);
|
||
font-size: 0.88rem;
|
||
font-weight: 700;
|
||
text-decoration: none;
|
||
}
|
||
|
||
nav a:hover {
|
||
color: var(--river);
|
||
}
|
||
|
||
.hero {
|
||
position: relative;
|
||
display: grid;
|
||
grid-template-columns: minmax(0, 1.3fr) minmax(240px, 0.7fr);
|
||
gap: 56px;
|
||
padding: 82px 0 70px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.hero::after {
|
||
position: absolute;
|
||
right: 2%;
|
||
bottom: 18%;
|
||
width: 360px;
|
||
height: 86px;
|
||
border-top: 12px solid rgb(13 92 115 / 12%);
|
||
border-bottom: 12px solid rgb(13 92 115 / 12%);
|
||
border-radius: 50%;
|
||
content: "";
|
||
transform: rotate(-9deg);
|
||
pointer-events: none;
|
||
}
|
||
|
||
.eyebrow {
|
||
display: inline-flex;
|
||
gap: 10px;
|
||
align-items: center;
|
||
margin: 0 0 20px;
|
||
color: var(--river);
|
||
font-family: ui-monospace, "SFMono-Regular", Consolas, monospace;
|
||
font-size: 0.76rem;
|
||
font-weight: 800;
|
||
letter-spacing: 0.14em;
|
||
text-transform: uppercase;
|
||
}
|
||
|
||
.eyebrow::before {
|
||
width: 34px;
|
||
height: 3px;
|
||
background: var(--lotus);
|
||
content: "";
|
||
}
|
||
|
||
h1,
|
||
h2,
|
||
h3,
|
||
p {
|
||
margin-top: 0;
|
||
}
|
||
|
||
h1 {
|
||
max-width: 790px;
|
||
margin-bottom: 28px;
|
||
font-family: "STKaiti", "KaiTi", "FangSong", serif;
|
||
font-size: clamp(3.4rem, 8.5vw, 7.8rem);
|
||
font-weight: 700;
|
||
line-height: 0.92;
|
||
letter-spacing: -0.09em;
|
||
}
|
||
|
||
h1 em {
|
||
color: var(--lotus);
|
||
font-style: normal;
|
||
}
|
||
|
||
.hero-copy {
|
||
max-width: 670px;
|
||
color: var(--muted);
|
||
font-size: clamp(1rem, 2vw, 1.18rem);
|
||
line-height: 1.8;
|
||
}
|
||
|
||
.hero-ticket {
|
||
position: relative;
|
||
z-index: 1;
|
||
align-self: end;
|
||
border: 1px solid var(--river);
|
||
border-radius: 2px;
|
||
padding: 26px;
|
||
background: var(--lake);
|
||
box-shadow: 14px 14px 0 var(--sun);
|
||
transform: rotate(2deg);
|
||
}
|
||
|
||
.ticket-label {
|
||
margin-bottom: 36px;
|
||
font-family: ui-monospace, "SFMono-Regular", Consolas, monospace;
|
||
font-size: 0.72rem;
|
||
letter-spacing: 0.12em;
|
||
text-transform: uppercase;
|
||
}
|
||
|
||
.hero-ticket strong {
|
||
display: block;
|
||
margin-bottom: 12px;
|
||
font-family: "STKaiti", "KaiTi", serif;
|
||
font-size: 2.2rem;
|
||
}
|
||
|
||
.hero-ticket p {
|
||
margin-bottom: 0;
|
||
color: var(--river);
|
||
line-height: 1.7;
|
||
}
|
||
|
||
.call-strip {
|
||
display: flex;
|
||
gap: 14px;
|
||
align-items: center;
|
||
margin-bottom: 72px;
|
||
border-block: 1px solid var(--line);
|
||
padding: 16px 0;
|
||
overflow-x: auto;
|
||
color: var(--river);
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.call-strip strong {
|
||
color: var(--lotus);
|
||
}
|
||
|
||
.call-strip span::after {
|
||
margin-left: 14px;
|
||
color: var(--line);
|
||
content: "◆";
|
||
}
|
||
|
||
.section-heading {
|
||
display: flex;
|
||
gap: 24px;
|
||
align-items: end;
|
||
justify-content: space-between;
|
||
margin-bottom: 28px;
|
||
}
|
||
|
||
.section-heading h2 {
|
||
margin-bottom: 0;
|
||
font-family: "STKaiti", "KaiTi", serif;
|
||
font-size: clamp(2.4rem, 5vw, 4.7rem);
|
||
line-height: 1;
|
||
letter-spacing: -0.06em;
|
||
}
|
||
|
||
.section-heading p {
|
||
max-width: 420px;
|
||
margin-bottom: 4px;
|
||
color: var(--muted);
|
||
line-height: 1.65;
|
||
}
|
||
|
||
.collection-grid {
|
||
display: grid;
|
||
grid-template-columns: minmax(320px, 0.82fr) minmax(0, 1.18fr);
|
||
gap: 28px;
|
||
align-items: start;
|
||
margin-bottom: 92px;
|
||
}
|
||
|
||
.proposal-form {
|
||
border: 1px solid var(--line);
|
||
border-radius: 24px;
|
||
padding: 28px;
|
||
background: rgb(255 255 255 / 86%);
|
||
box-shadow: 0 24px 70px rgb(21 42 50 / 9%);
|
||
}
|
||
|
||
.cocreate {
|
||
position: relative;
|
||
margin: -10px -10px 26px;
|
||
border-radius: 18px;
|
||
padding: 24px;
|
||
overflow: hidden;
|
||
background: var(--river-deep);
|
||
color: white;
|
||
}
|
||
|
||
.cocreate::after {
|
||
position: absolute;
|
||
top: -68px;
|
||
right: -54px;
|
||
width: 180px;
|
||
height: 180px;
|
||
border: 38px solid rgb(223 243 241 / 7%);
|
||
border-radius: 50%;
|
||
content: "";
|
||
pointer-events: none;
|
||
}
|
||
|
||
.flow-label {
|
||
position: relative;
|
||
z-index: 1;
|
||
margin-bottom: 8px;
|
||
color: var(--sun);
|
||
font-family: ui-monospace, "SFMono-Regular", Consolas, monospace;
|
||
font-size: 0.7rem;
|
||
font-weight: 800;
|
||
letter-spacing: 0.12em;
|
||
text-transform: uppercase;
|
||
}
|
||
|
||
.cocreate h3 {
|
||
position: relative;
|
||
z-index: 1;
|
||
margin-bottom: 8px;
|
||
font-family: "STKaiti", "KaiTi", serif;
|
||
font-size: 2rem;
|
||
}
|
||
|
||
.cocreate-intro {
|
||
position: relative;
|
||
z-index: 1;
|
||
margin-bottom: 18px;
|
||
color: rgb(255 255 255 / 66%);
|
||
font-size: 0.88rem;
|
||
line-height: 1.65;
|
||
}
|
||
|
||
.idea-label {
|
||
position: relative;
|
||
z-index: 1;
|
||
color: white;
|
||
}
|
||
|
||
.cocreate textarea {
|
||
min-height: 104px;
|
||
border-color: rgb(255 255 255 / 18%);
|
||
background: rgb(255 255 255 / 8%);
|
||
color: white;
|
||
}
|
||
|
||
.cocreate textarea::placeholder {
|
||
color: rgb(255 255 255 / 42%);
|
||
}
|
||
|
||
.idea-examples {
|
||
position: relative;
|
||
z-index: 1;
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 7px;
|
||
margin: -4px 0 16px;
|
||
}
|
||
|
||
.idea-chip {
|
||
border: 1px solid rgb(255 255 255 / 22%);
|
||
border-radius: 999px;
|
||
padding: 6px 10px;
|
||
background: transparent;
|
||
color: rgb(255 255 255 / 78%);
|
||
cursor: pointer;
|
||
font-size: 0.74rem;
|
||
}
|
||
|
||
.idea-chip:hover {
|
||
border-color: var(--sun);
|
||
color: var(--sun);
|
||
}
|
||
|
||
.cocreate-actions {
|
||
position: relative;
|
||
z-index: 1;
|
||
display: flex;
|
||
gap: 12px;
|
||
align-items: center;
|
||
}
|
||
|
||
.draft-button {
|
||
border: 0;
|
||
border-radius: 10px;
|
||
padding: 11px 18px;
|
||
background: var(--sun);
|
||
color: var(--river-deep);
|
||
cursor: pointer;
|
||
font-weight: 900;
|
||
}
|
||
|
||
.draft-status {
|
||
color: rgb(255 255 255 / 60%);
|
||
font-size: 0.8rem;
|
||
}
|
||
|
||
.draft-preview {
|
||
position: relative;
|
||
z-index: 1;
|
||
margin-top: 20px;
|
||
border: 1px dashed rgb(255 255 255 / 28%);
|
||
border-radius: 12px;
|
||
padding: 18px;
|
||
background: rgb(255 255 255 / 8%);
|
||
}
|
||
|
||
.draft-preview[hidden] {
|
||
display: none;
|
||
}
|
||
|
||
.draft-kicker {
|
||
margin-bottom: 9px;
|
||
color: var(--sun);
|
||
font-family: ui-monospace, "SFMono-Regular", Consolas, monospace;
|
||
font-size: 0.68rem;
|
||
font-weight: 800;
|
||
letter-spacing: 0.1em;
|
||
text-transform: uppercase;
|
||
}
|
||
|
||
.draft-preview h4 {
|
||
margin: 0 0 10px;
|
||
font-size: 1.16rem;
|
||
}
|
||
|
||
.draft-meta {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 7px;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.draft-meta span {
|
||
border-radius: 999px;
|
||
padding: 4px 8px;
|
||
background: rgb(223 243 241 / 14%);
|
||
color: var(--lake);
|
||
font-size: 0.72rem;
|
||
font-weight: 800;
|
||
}
|
||
|
||
.draft-copy {
|
||
margin-bottom: 14px;
|
||
color: rgb(255 255 255 / 74%);
|
||
font-size: 0.84rem;
|
||
line-height: 1.65;
|
||
white-space: pre-wrap;
|
||
}
|
||
|
||
.adopt-button {
|
||
border: 1px solid var(--sun);
|
||
border-radius: 9px;
|
||
padding: 9px 13px;
|
||
background: transparent;
|
||
color: var(--sun);
|
||
cursor: pointer;
|
||
font-weight: 800;
|
||
}
|
||
|
||
.manual-step {
|
||
display: flex;
|
||
gap: 12px;
|
||
align-items: center;
|
||
margin: 0 0 18px;
|
||
color: var(--river);
|
||
font-size: 0.78rem;
|
||
font-weight: 900;
|
||
letter-spacing: 0.04em;
|
||
}
|
||
|
||
.manual-step::after {
|
||
height: 1px;
|
||
flex: 1;
|
||
background: var(--line);
|
||
content: "";
|
||
}
|
||
|
||
.form-grid {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 0 14px;
|
||
}
|
||
|
||
label {
|
||
display: grid;
|
||
gap: 8px;
|
||
margin-bottom: 16px;
|
||
color: var(--river-deep);
|
||
font-size: 0.84rem;
|
||
font-weight: 800;
|
||
}
|
||
|
||
label.full {
|
||
grid-column: 1 / -1;
|
||
}
|
||
|
||
input,
|
||
select,
|
||
textarea {
|
||
width: 100%;
|
||
min-width: 0;
|
||
border: 1px solid var(--line);
|
||
border-radius: 12px;
|
||
padding: 12px 13px;
|
||
outline: none;
|
||
background: white;
|
||
color: var(--ink);
|
||
transition: border-color 160ms ease, box-shadow 160ms ease;
|
||
}
|
||
|
||
textarea {
|
||
min-height: 130px;
|
||
resize: vertical;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
input:focus,
|
||
select:focus,
|
||
textarea:focus,
|
||
button:focus-visible {
|
||
border-color: var(--lotus);
|
||
box-shadow: 0 0 0 4px rgb(217 76 113 / 14%);
|
||
}
|
||
|
||
.primary-button {
|
||
width: 100%;
|
||
border: 0;
|
||
border-radius: 12px;
|
||
padding: 14px 20px;
|
||
background: var(--lotus);
|
||
color: white;
|
||
cursor: pointer;
|
||
font-weight: 800;
|
||
}
|
||
|
||
.primary-button:hover {
|
||
background: #bd365b;
|
||
}
|
||
|
||
button:disabled {
|
||
cursor: wait;
|
||
opacity: 0.62;
|
||
}
|
||
|
||
.form-status {
|
||
min-height: 22px;
|
||
margin: 12px 0 0;
|
||
color: var(--river);
|
||
font-size: 0.88rem;
|
||
}
|
||
|
||
.event-board {
|
||
display: grid;
|
||
gap: 14px;
|
||
}
|
||
|
||
.event-card {
|
||
display: grid;
|
||
grid-template-columns: 92px minmax(0, 1fr);
|
||
overflow: hidden;
|
||
border: 1px solid var(--line);
|
||
border-radius: 18px;
|
||
background: white;
|
||
}
|
||
|
||
.event-date {
|
||
display: grid;
|
||
align-content: center;
|
||
padding: 18px 12px;
|
||
background: var(--river);
|
||
color: white;
|
||
text-align: center;
|
||
}
|
||
|
||
.event-date strong {
|
||
font-family: "STKaiti", "KaiTi", serif;
|
||
font-size: 2rem;
|
||
line-height: 1;
|
||
}
|
||
|
||
.event-date span {
|
||
margin-top: 6px;
|
||
font-family: ui-monospace, "SFMono-Regular", Consolas, monospace;
|
||
font-size: 0.72rem;
|
||
letter-spacing: 0.06em;
|
||
}
|
||
|
||
.event-content {
|
||
padding: 18px 20px;
|
||
}
|
||
|
||
.event-meta {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.tag {
|
||
border-radius: 999px;
|
||
padding: 4px 9px;
|
||
background: var(--lake);
|
||
color: var(--river);
|
||
font-size: 0.72rem;
|
||
font-weight: 800;
|
||
}
|
||
|
||
.tag.status {
|
||
background: #fff0d8;
|
||
color: #855300;
|
||
}
|
||
|
||
.event-content h3 {
|
||
margin-bottom: 8px;
|
||
font-size: 1.12rem;
|
||
}
|
||
|
||
.event-content p {
|
||
display: -webkit-box;
|
||
margin-bottom: 10px;
|
||
overflow: hidden;
|
||
color: var(--muted);
|
||
line-height: 1.6;
|
||
-webkit-box-orient: vertical;
|
||
-webkit-line-clamp: 2;
|
||
}
|
||
|
||
.event-place {
|
||
color: var(--river);
|
||
font-size: 0.82rem;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.empty-state {
|
||
border: 1px dashed var(--line);
|
||
border-radius: 18px;
|
||
padding: 54px 24px;
|
||
color: var(--muted);
|
||
text-align: center;
|
||
}
|
||
|
||
footer {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
border-top: 1px solid var(--line);
|
||
padding: 26px 0 40px;
|
||
color: var(--muted);
|
||
font-size: 0.82rem;
|
||
}
|
||
|
||
@media (max-width: 860px) {
|
||
.hero,
|
||
.collection-grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.hero {
|
||
gap: 30px;
|
||
padding-top: 56px;
|
||
}
|
||
|
||
.hero-ticket {
|
||
max-width: 420px;
|
||
}
|
||
|
||
.section-heading {
|
||
display: block;
|
||
}
|
||
|
||
.section-heading p {
|
||
margin-top: 14px;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 620px) {
|
||
.page {
|
||
width: min(100% - 22px, 1180px);
|
||
}
|
||
|
||
nav {
|
||
display: none;
|
||
}
|
||
|
||
h1 {
|
||
font-size: clamp(3.2rem, 19vw, 5.3rem);
|
||
}
|
||
|
||
.form-grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
label.full {
|
||
grid-column: auto;
|
||
}
|
||
|
||
.proposal-form {
|
||
padding: 20px;
|
||
}
|
||
|
||
.event-card {
|
||
grid-template-columns: 74px minmax(0, 1fr);
|
||
}
|
||
|
||
.cocreate-actions {
|
||
display: grid;
|
||
}
|
||
|
||
.draft-button {
|
||
width: 100%;
|
||
}
|
||
|
||
footer {
|
||
display: grid;
|
||
gap: 8px;
|
||
}
|
||
}
|
||
|
||
@media (prefers-reduced-motion: reduce) {
|
||
html {
|
||
scroll-behavior: auto;
|
||
}
|
||
|
||
* {
|
||
transition: none !important;
|
||
}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<header class="page masthead">
|
||
<a class="brand" href="#">
|
||
<span class="brand-mark">OPC<br>WH</span>
|
||
<span>
|
||
<strong>武汉 OPC 社区</strong>
|
||
<span>一人公司 · 独立创造者</span>
|
||
</span>
|
||
</a>
|
||
<nav aria-label="页面导航">
|
||
<a href="#collection">AI 共创提案</a>
|
||
<a href="#events">最新提案</a>
|
||
</nav>
|
||
</header>
|
||
|
||
<main class="page">
|
||
<section class="hero">
|
||
<div>
|
||
<p class="eyebrow">Wuhan open call · 2026</p>
|
||
<h1>一句想法,长成<em>江城</em>活动。</h1>
|
||
<p class="hero-copy">
|
||
无论你是独立开发者、产品人、设计师,还是正在经营一人公司的行动者,
|
||
先告诉 AI 你想聚集怎样的人、一起做什么。它会整理成活动草案,再由你确认并发起。
|
||
</p>
|
||
</div>
|
||
<aside class="hero-ticket" aria-label="征集说明">
|
||
<div class="ticket-label">30.5928° N / 114.3055° E</div>
|
||
<strong>活动接力票</strong>
|
||
<p>想法由你点亮,草案由 AI 整理,最终安排由你决定。社区会接住每一份认真发起。</p>
|
||
</aside>
|
||
</section>
|
||
|
||
<div class="call-strip" aria-label="征集类型">
|
||
<strong>正在征集</strong>
|
||
<span>技术共学</span>
|
||
<span>产品交流</span>
|
||
<span>独立开发</span>
|
||
<span>城市漫游</span>
|
||
<span>创意市集</span>
|
||
<span>更多自发活动</span>
|
||
</div>
|
||
|
||
<section id="collection">
|
||
<div class="section-heading">
|
||
<h2>和 AI 共创活动提案</h2>
|
||
<p>从一句不完整的念头开始,生成草案、人工确认,再补充真实日期和联系方式。</p>
|
||
</div>
|
||
|
||
<div class="collection-grid">
|
||
<form class="proposal-form" id="event-form">
|
||
<section class="cocreate" aria-labelledby="cocreate-title">
|
||
<div class="flow-label">Step 1 · 先说一个想法</div>
|
||
<h3 id="cocreate-title">江城活动共创台</h3>
|
||
<p class="cocreate-intro">
|
||
不用先写完整方案。说清楚想和谁、做什么,已选择的类型、区域和地点也会一起交给 AI 参考。
|
||
</p>
|
||
<label class="idea-label" for="event-idea">
|
||
你的活动念头
|
||
<textarea id="event-idea" minlength="8" maxlength="800" placeholder="例如:想找 20 位独立开发者,在周末互相体验还没上线的产品,并给出真诚反馈"></textarea>
|
||
</label>
|
||
<div class="idea-examples" aria-label="活动想法示例">
|
||
<button class="idea-chip" type="button">做一场独立产品互诊</button>
|
||
<button class="idea-chip" type="button">沿东湖边走边聊一人公司</button>
|
||
<button class="idea-chip" type="button">让设计师和开发者交换工作流</button>
|
||
</div>
|
||
<div class="cocreate-actions">
|
||
<button class="draft-button" id="draft-submit" type="button">生成活动草案</button>
|
||
<span class="draft-status" id="draft-status" aria-live="polite">AI 只负责整理,提交前由你确认。</span>
|
||
</div>
|
||
<section class="draft-preview" id="draft-preview" aria-live="polite" hidden>
|
||
<div class="draft-kicker">AI draft · 待确认</div>
|
||
<h4 id="draft-title"></h4>
|
||
<div class="draft-meta">
|
||
<span id="draft-category"></span>
|
||
<span id="draft-district"></span>
|
||
<span id="draft-venue"></span>
|
||
</div>
|
||
<p class="draft-copy" id="draft-description"></p>
|
||
<button class="adopt-button" id="adopt-draft" type="button">采用这份草案并继续</button>
|
||
</section>
|
||
</section>
|
||
|
||
<div class="manual-step">Step 2 · 确认草案并补充真实信息</div>
|
||
<div class="form-grid">
|
||
<label class="full">
|
||
活动名称
|
||
<input name="title" maxlength="100" required placeholder="例如:独立开发者周末产品诊所">
|
||
</label>
|
||
<label>
|
||
发起人 / 团队
|
||
<input name="organizer" maxlength="60" required placeholder="你的称呼">
|
||
</label>
|
||
<label>
|
||
活动类型
|
||
<select name="category" required>
|
||
<option value="">请选择</option>
|
||
<option>技术共学</option>
|
||
<option>产品交流</option>
|
||
<option>独立开发</option>
|
||
<option>城市漫游</option>
|
||
<option>创意市集</option>
|
||
<option>其他</option>
|
||
</select>
|
||
</label>
|
||
<label>
|
||
活动区域
|
||
<select name="district" required>
|
||
<option value="">请选择</option>
|
||
<option>江岸区</option>
|
||
<option>江汉区</option>
|
||
<option>硚口区</option>
|
||
<option>汉阳区</option>
|
||
<option>武昌区</option>
|
||
<option>青山区</option>
|
||
<option>洪山区</option>
|
||
<option>东西湖区</option>
|
||
<option>线上</option>
|
||
</select>
|
||
</label>
|
||
<label>
|
||
计划日期
|
||
<input name="event_date" type="date" required>
|
||
</label>
|
||
<label class="full">
|
||
场地 / 集合点
|
||
<input name="venue" maxlength="120" required placeholder="可以先写意向区域或线上">
|
||
</label>
|
||
<label class="full">
|
||
活动说明
|
||
<textarea name="description" minlength="20" maxlength="1200" required placeholder="介绍活动主题、适合谁参加、你希望现场发生什么(至少 20 字)"></textarea>
|
||
</label>
|
||
<label class="full">
|
||
联系方式(仅社区运营可见)
|
||
<input name="contact" maxlength="160" required placeholder="微信、邮箱或其他联系方式">
|
||
</label>
|
||
</div>
|
||
<button class="primary-button" id="event-submit" type="submit">提交活动提案</button>
|
||
<p class="form-status" id="event-status" aria-live="polite"></p>
|
||
</form>
|
||
|
||
<div id="events">
|
||
<div class="event-board" id="event-board">
|
||
<div class="empty-state">正在读取社区提案...</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
</main>
|
||
|
||
<footer class="page">
|
||
<span>武汉 OPC 社区 AI 活动共创 Demo</span>
|
||
<span>想法由人发起,AI 帮它变得可行动</span>
|
||
</footer>
|
||
|
||
<script>
|
||
const apiBase = (() => {
|
||
const envID = location.pathname.split("/").filter(Boolean)[0];
|
||
return envID ? `/${envID}/api/cloud` : "api/cloud";
|
||
})();
|
||
|
||
const eventForm = document.querySelector("#event-form");
|
||
const eventSubmit = document.querySelector("#event-submit");
|
||
const eventStatus = document.querySelector("#event-status");
|
||
const eventBoard = document.querySelector("#event-board");
|
||
const ideaInput = document.querySelector("#event-idea");
|
||
const draftSubmit = document.querySelector("#draft-submit");
|
||
const draftStatus = document.querySelector("#draft-status");
|
||
const draftPreview = document.querySelector("#draft-preview");
|
||
const adoptDraft = document.querySelector("#adopt-draft");
|
||
let currentDraft = null;
|
||
|
||
function escapeHTML(value) {
|
||
return String(value ?? "").replace(/[&<>"']/g, (character) => ({
|
||
"&": "&",
|
||
"<": "<",
|
||
">": ">",
|
||
"\"": """,
|
||
"'": "'"
|
||
}[character]));
|
||
}
|
||
|
||
async function requestJSON(path, init) {
|
||
const response = await fetch(`${apiBase}${path}`, init);
|
||
let payload;
|
||
try {
|
||
payload = await response.json();
|
||
} catch (error) {
|
||
throw new Error(`服务返回了无法读取的内容(${response.status})`);
|
||
}
|
||
if (!response.ok || payload.ok === false) {
|
||
throw new Error(payload.error || `请求失败(${response.status})`);
|
||
}
|
||
return payload;
|
||
}
|
||
|
||
function renderDate(value) {
|
||
const date = new Date(`${value}T00:00:00`);
|
||
if (Number.isNaN(date.getTime())) {
|
||
return {day: "--", month: "待定"};
|
||
}
|
||
return {
|
||
day: String(date.getDate()).padStart(2, "0"),
|
||
month: `${date.getMonth() + 1}月`
|
||
};
|
||
}
|
||
|
||
function renderEvent(event) {
|
||
const date = renderDate(event.event_date);
|
||
return `
|
||
<article class="event-card">
|
||
<div class="event-date">
|
||
<strong>${escapeHTML(date.day)}</strong>
|
||
<span>${escapeHTML(date.month)}</span>
|
||
</div>
|
||
<div class="event-content">
|
||
<div class="event-meta">
|
||
<span class="tag">${escapeHTML(event.category)}</span>
|
||
<span class="tag">${escapeHTML(event.district)}</span>
|
||
<span class="tag status">${escapeHTML(event.status)}</span>
|
||
</div>
|
||
<h3>${escapeHTML(event.title)}</h3>
|
||
<p>${escapeHTML(event.description)}</p>
|
||
<div class="event-place">📍 ${escapeHTML(event.venue)} · 发起人 ${escapeHTML(event.organizer)}</div>
|
||
</div>
|
||
</article>
|
||
`;
|
||
}
|
||
|
||
async function loadEvents() {
|
||
try {
|
||
const payload = await requestJSON("/events");
|
||
const events = Array.isArray(payload.events) ? payload.events : [];
|
||
eventBoard.innerHTML = events.length
|
||
? events.map(renderEvent).join("")
|
||
: '<div class="empty-state">还没有活动提案。提交第一场活动,让接力从你开始。</div>';
|
||
} catch (error) {
|
||
eventBoard.innerHTML = `<div class="empty-state">${escapeHTML(error.message)}</div>`;
|
||
}
|
||
}
|
||
|
||
eventForm.addEventListener("submit", async (event) => {
|
||
event.preventDefault();
|
||
eventSubmit.disabled = true;
|
||
eventStatus.textContent = "正在提交...";
|
||
const data = Object.fromEntries(new FormData(eventForm).entries());
|
||
try {
|
||
await requestJSON("/events", {
|
||
method: "POST",
|
||
headers: {"content-type": "application/json"},
|
||
body: JSON.stringify(data)
|
||
});
|
||
eventForm.reset();
|
||
currentDraft = null;
|
||
draftPreview.hidden = true;
|
||
draftStatus.textContent = "AI 只负责整理,提交前由你确认。";
|
||
document.querySelector('[name="event_date"]').min = new Date().toISOString().slice(0, 10);
|
||
eventStatus.textContent = "提案已收到,期待在武汉见面。";
|
||
await loadEvents();
|
||
} catch (error) {
|
||
eventStatus.textContent = error.message;
|
||
} finally {
|
||
eventSubmit.disabled = false;
|
||
}
|
||
});
|
||
|
||
document.querySelectorAll(".idea-chip").forEach((button) => {
|
||
button.addEventListener("click", () => {
|
||
ideaInput.value = button.textContent.trim();
|
||
ideaInput.focus();
|
||
});
|
||
});
|
||
|
||
draftSubmit.addEventListener("click", async () => {
|
||
const idea = ideaInput.value.trim();
|
||
if (idea.length < 8) {
|
||
draftStatus.textContent = "请用至少 8 个字说说想办什么";
|
||
ideaInput.focus();
|
||
return;
|
||
}
|
||
const formData = Object.fromEntries(new FormData(eventForm).entries());
|
||
const context = {
|
||
idea,
|
||
category: formData.category,
|
||
district: formData.district,
|
||
venue: formData.venue,
|
||
event_date: formData.event_date
|
||
};
|
||
draftSubmit.disabled = true;
|
||
draftStatus.textContent = "正在把想法整理成活动草案...";
|
||
try {
|
||
const payload = await requestJSON("/event-draft", {
|
||
method: "POST",
|
||
headers: {"content-type": "application/json"},
|
||
body: JSON.stringify(context)
|
||
});
|
||
currentDraft = payload.draft;
|
||
document.querySelector("#draft-title").textContent = currentDraft.title;
|
||
document.querySelector("#draft-category").textContent = currentDraft.category;
|
||
document.querySelector("#draft-district").textContent = currentDraft.district;
|
||
document.querySelector("#draft-venue").textContent = `场地:${currentDraft.venue}`;
|
||
document.querySelector("#draft-description").textContent = currentDraft.description;
|
||
draftPreview.hidden = false;
|
||
draftStatus.textContent = "草案已生成,请确认后采用";
|
||
} catch (error) {
|
||
draftStatus.textContent = error.message;
|
||
} finally {
|
||
draftSubmit.disabled = false;
|
||
}
|
||
});
|
||
|
||
adoptDraft.addEventListener("click", () => {
|
||
if (!currentDraft) {
|
||
return;
|
||
}
|
||
Object.entries(currentDraft).forEach(([name, value]) => {
|
||
const control = eventForm.elements.namedItem(name);
|
||
if (control) {
|
||
control.value = value;
|
||
}
|
||
});
|
||
draftStatus.textContent = "草案已回填,请补充日期、发起人和联系方式";
|
||
eventForm.elements.namedItem("organizer").focus();
|
||
document.querySelector(".manual-step").scrollIntoView({behavior: "smooth", block: "start"});
|
||
});
|
||
|
||
document.querySelector('[name="event_date"]').min = new Date().toISOString().slice(0, 10);
|
||
loadEvents();
|
||
</script>
|
||
</body>
|
||
</html>
|