| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681 |
- package handlers
- import "net/http"
- type OpsWorkbenchHandler struct{}
- func NewOpsWorkbenchHandler() *OpsWorkbenchHandler {
- return &OpsWorkbenchHandler{}
- }
- func (h *OpsWorkbenchHandler) Get(w http.ResponseWriter, r *http.Request) {
- w.Header().Set("Content-Type", "text/html; charset=utf-8")
- _, _ = w.Write([]byte(opsWorkbenchHTML))
- }
- const opsWorkbenchHTML = `<!doctype html>
- <html lang="zh-CN">
- <head>
- <meta charset="utf-8">
- <meta name="viewport" content="width=device-width, initial-scale=1">
- <title>CMR 运维后台</title>
- <style>
- :root {
- --bg: #071218;
- --panel: #12212c;
- --panel-2: #172a36;
- --line: #284355;
- --text: #ecf6ff;
- --muted: #92acbf;
- --brand: #52d6b3;
- --warn: #ffd36a;
- --danger: #ff7b7b;
- --accent: #4fa3ff;
- }
- * { box-sizing: border-box; }
- html { scroll-behavior: smooth; }
- body {
- margin: 0;
- color: var(--text);
- font-family: "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
- background:
- radial-gradient(circle at top left, rgba(82,214,179,.12), transparent 28%),
- radial-gradient(circle at top right, rgba(79,163,255,.12), transparent 24%),
- var(--bg);
- }
- .shell {
- width: 100%;
- max-width: none;
- margin: 0;
- padding: 24px 28px 32px;
- display: grid;
- grid-template-columns: 280px minmax(0,1fr) 400px;
- gap: 20px;
- align-items: start;
- }
- .sidebar, .aside, .panel {
- background: rgba(18,33,44,.92);
- border: 1px solid var(--line);
- border-radius: 24px;
- box-shadow: 0 16px 60px rgba(0,0,0,.22);
- }
- .sidebar, .aside {
- position: sticky;
- top: 20px;
- padding: 18px;
- display: grid;
- gap: 18px;
- align-content: start;
- }
- .sidebar { background: linear-gradient(180deg, rgba(13,27,36,.96), rgba(18,33,44,.94)); }
- .aside { background: linear-gradient(180deg, rgba(18,33,44,.94), rgba(13,23,31,.96)); }
- .content { display: grid; gap: 20px; min-width: 0; }
- .panel { padding: 20px; display: grid; gap: 16px; align-content: start; }
- .hero {
- background: linear-gradient(135deg, rgba(82,214,179,.15), rgba(79,163,255,.10));
- padding: 22px 24px;
- }
- .brand h1, .hero h2, .panel h3 { margin: 0; line-height: 1.2; }
- .brand h1 { font-size: 26px; }
- .hero h2 { font-size: 30px; }
- .panel h3 { font-size: 24px; }
- .brand p, .hero p, .panel p, .hint { margin: 0; color: var(--muted); line-height: 1.7; }
- .tag, .eyebrow {
- display: inline-flex;
- width: fit-content;
- padding: 4px 10px;
- border-radius: 999px;
- font-size: 12px;
- font-weight: 700;
- }
- .tag { background: rgba(82,214,179,.18); color: var(--brand); }
- .eyebrow { background: rgba(79,163,255,.16); color: var(--accent); }
- .nav-group { display: grid; gap: 10px; }
- .nav-title {
- color: var(--muted);
- font-size: 12px;
- letter-spacing: .08em;
- text-transform: uppercase;
- padding: 0 4px;
- }
- .nav-link {
- display: grid;
- gap: 4px;
- width: 100%;
- text-align: left;
- color: var(--text);
- padding: 13px 14px;
- border-radius: 18px;
- border: 1px solid var(--line);
- background: rgba(255,255,255,.02);
- cursor: pointer;
- }
- .nav-link.active {
- border-color: rgba(82,214,179,.45);
- background: rgba(82,214,179,.10);
- box-shadow: inset 0 0 0 1px rgba(82,214,179,.16);
- }
- .nav-link span { color: var(--muted); font-size: 13px; line-height: 1.45; }
- .hero-points, .metrics, .grid-2, .grid-3, .split { display: grid; gap: 14px; }
- .toolbar { display: flex; gap: 12px; flex-wrap: wrap; align-items: end; justify-content: space-between; }
- .toolbar .actions { justify-content: flex-end; }
- .hero-points, .metrics { grid-template-columns: repeat(3, minmax(0,1fr)); }
- .metrics { grid-template-columns: repeat(4, minmax(0,1fr)); }
- .grid-2 { grid-template-columns: repeat(2, minmax(0,1fr)); }
- .grid-3 { grid-template-columns: repeat(3, minmax(0,1fr)); }
- .split { grid-template-columns: 1.05fr .95fr; align-items: start; }
- .hero-card, .metric, .item, .token-box, .statusbox, .logbox, .list {
- border-radius: 18px;
- border: 1px solid var(--line);
- background: rgba(255,255,255,.03);
- }
- .hero-card, .metric, .item { padding: 14px; display: grid; gap: 6px; }
- .item.selectable { cursor: pointer; transition: border-color .18s ease, background .18s ease, transform .18s ease; }
- .item.selectable:hover { border-color: rgba(82,214,179,.35); background: rgba(82,214,179,.08); transform: translateY(-1px); }
- .item.active { border-color: rgba(82,214,179,.45); background: rgba(82,214,179,.10); box-shadow: inset 0 0 0 1px rgba(82,214,179,.14); }
- .metric strong { font-size: 28px; line-height: 1; }
- .metric span, .item .meta { color: var(--muted); font-size: 13px; line-height: 1.5; }
- .field { display: grid; gap: 8px; min-width: 0; }
- .field label { color: var(--muted); font-size: 14px; }
- input, textarea, select {
- width: 100%;
- border-radius: 16px;
- border: 1px solid var(--line);
- background: var(--panel-2);
- color: var(--text);
- padding: 13px 14px;
- font: inherit;
- }
- textarea { min-height: 110px; resize: vertical; line-height: 1.55; }
- .tall textarea { min-height: 190px; }
- .actions { display: flex; gap: 12px; flex-wrap: wrap; }
- button {
- border: 0;
- border-radius: 16px;
- padding: 13px 18px;
- font: inherit;
- font-weight: 700;
- cursor: pointer;
- background: var(--brand);
- color: #03231d;
- }
- button.nav-link {
- background: rgba(255,255,255,.02);
- color: var(--text);
- border: 1px solid var(--line);
- padding: 13px 14px;
- }
- button.nav-link.active {
- border-color: rgba(82,214,179,.45);
- background: rgba(82,214,179,.10);
- color: var(--text);
- }
- button.secondary { background: var(--warn); color: #3f2d00; }
- button.ghost { background: transparent; border: 1px solid var(--line); color: var(--text); }
- .token-box, .statusbox, .logbox {
- padding: 14px;
- white-space: pre-wrap;
- word-break: break-word;
- font-family: "Consolas", "SFMono-Regular", monospace;
- }
- .statusbox { min-height: 84px; }
- .logbox { min-height: 220px; max-height: 420px; overflow: auto; }
- .list { padding: 10px; display: grid; gap: 10px; max-height: 320px; overflow: auto; }
- .ops-view { display: none; }
- .ops-view.active { display: grid; }
- [hidden] { display: none !important; }
- .modal-backdrop {
- position: fixed;
- inset: 0;
- background: rgba(3, 10, 14, .68);
- backdrop-filter: blur(4px);
- display: grid;
- place-items: center;
- padding: 24px;
- z-index: 30;
- }
- .modal-card {
- width: min(1180px, calc(100vw - 48px));
- max-height: calc(100vh - 48px);
- overflow: auto;
- background: linear-gradient(180deg, rgba(18,33,44,.98), rgba(12,23,31,.98));
- border: 1px solid var(--line);
- border-radius: 24px;
- box-shadow: 0 20px 80px rgba(0,0,0,.35);
- padding: 22px;
- display: grid;
- gap: 16px;
- }
- .status-ok { color: var(--brand); }
- .status-error { color: var(--danger); }
- @media (max-width: 1560px) {
- .shell { grid-template-columns: 250px minmax(0,1fr) 360px; padding: 22px 24px 28px; }
- }
- @media (max-width: 1360px) {
- .shell { grid-template-columns: 220px minmax(0,1fr); }
- .aside { grid-column: 2; position: static; }
- }
- @media (max-width: 1120px) {
- .shell { grid-template-columns: 1fr; }
- .sidebar, .aside { position: static; }
- .hero-points, .metrics, .grid-2, .grid-3, .split { grid-template-columns: 1fr; }
- }
- </style>
- </head>
- <body>
- <div class="shell">
- <aside class="sidebar">
- <div class="brand">
- <span class="tag">运维后台第一期</span>
- <h1>资源录入与发布中心</h1>
- <p>这里不做联调回归,只做资源纳管、活动绑定和发布回滚。调试问题继续去 <code>/dev/workbench</code>。</p>
- </div>
- <div class="nav-group">
- <div class="nav-title">主要流程</div>
- <button class="nav-link active" type="button" data-view="overview"><strong>资源总览</strong><span>先看已有资源、活动和当前发布状态。</span></button>
- <button class="nav-link" type="button" data-view="maps"><strong>地图 / 地点管理</strong><span>先管地图列表、地点、当前瓦片版本和地图预览。</span></button>
- <button class="nav-link" type="button" data-view="courses"><strong>路线资源管理</strong><span>围绕地图管理 KML、路线组、默认路线和预览。</span></button>
- <button class="nav-link" type="button" data-view="events"><strong>活动管理</strong><span>活动列表、基础信息、状态和当前发布概况。</span></button>
- <button class="nav-link" type="button" data-view="compose"><strong>活动编排</strong><span>绑定 runtime、展示定义、内容包,准备发布。</span></button>
- <button class="nav-link" type="button" data-view="publish"><strong>发布中心</strong><span>查看 pipeline、build、publish、rollback。</span></button>
- </div>
- <div class="nav-group">
- <div class="nav-title">辅助入口</div>
- <button class="nav-link" type="button" data-view="ingest"><strong>资源录入</strong><span>上传文件或登记正式链接,统一纳管资源对象。</span></button>
- </div>
- <div class="nav-group">
- <div class="nav-title">调试工具</div>
- <a class="nav-link" href="/dev/workbench"><strong>返回调试工作台</strong><span>去一键回归、配置摘要、前端日志面板。</span></a>
- </div>
- </aside>
- <main class="content">
- <section class="panel hero">
- <h2>先录资源,再绑活动,最后发布</h2>
- <p>运维平台第一版的目标很单一:把正式资源稳定录入系统,沉淀成资源对象,再通过统一发布链形成可追溯的 release。这里不混调试按钮,也不直接暴露玩家链路。</p>
- <div class="hero-points">
- <div class="hero-card"><strong>资源录入</strong><span>文件上传和外链登记统一收口,不再依赖手工改代码或散脚本。</span></div>
- <div class="hero-card"><strong>活动绑定</strong><span>活动侧只绑定默认 runtime、presentation、content bundle 三元组。</span></div>
- <div class="hero-card"><strong>发布中心</strong><span>继续走统一 build / publish / rollback,不开第二套发布逻辑。</span></div>
- </div>
- </section>
- <section class="panel ops-view active" id="overview">
- <div class="eyebrow">资源总览</div>
- <h3>先看关键统计,再看当前运行时信息</h3>
- <p>资源总览先回答两个问题:系统里现在有多少正式对象;你当前选中的活动此刻绑定了哪套 release / runtime / 展示定义 / 内容包。</p>
- <input id="managed-place-id" type="hidden" value="">
- <div class="split">
- <div class="panel" style="padding:18px;">
- <div class="eyebrow">资源与路线统计</div>
- <div class="metrics">
- <div class="metric"><strong id="metric-places">0</strong><span>地点</span></div>
- <div class="metric"><strong id="metric-map-assets">0</strong><span>地图</span></div>
- <div class="metric"><strong id="metric-tile-releases">0</strong><span>瓦片版本</span></div>
- <div class="metric"><strong id="metric-assets">0</strong><span>受管资源</span></div>
- <div class="metric"><strong id="metric-course-sets">0</strong><span>路线组</span></div>
- <div class="metric"><strong id="metric-course-variants">0</strong><span>路线变体</span></div>
- <div class="metric"><strong id="metric-runtime-bindings">0</strong><span>运行绑定</span></div>
- <div class="metric"><strong id="metric-config-sources">0</strong><span>配置源</span></div>
- </div>
- </div>
- <div class="panel" style="padding:18px;">
- <div class="eyebrow">活动与发布统计</div>
- <div class="metrics">
- <div class="metric"><strong id="metric-events">0</strong><span>活动数</span></div>
- <div class="metric"><strong id="metric-default-events">0</strong><span>默认体验活动</span></div>
- <div class="metric"><strong id="metric-published-events">0</strong><span>已发布活动</span></div>
- <div class="metric"><strong id="metric-pipeline-releases">0</strong><span>发布版本</span></div>
- <div class="metric"><strong id="metric-presentations">0</strong><span>展示定义</span></div>
- <div class="metric"><strong id="metric-content-bundles">0</strong><span>内容包</span></div>
- <div class="metric"><strong id="metric-ops-users">0</strong><span>运维账号</span></div>
- </div>
- </div>
- </div>
- <div class="split">
- <div class="panel" style="padding:18px;">
- <div class="eyebrow">当前运行时信息</div>
- <div class="field"><label>当前活动</label><div class="token-box" id="overview-current-event">-</div></div>
- <div class="field"><label>当前发布版本</label><div class="token-box" id="overview-current-release">-</div></div>
- <div class="field"><label>当前 runtime</label><div class="token-box" id="overview-current-runtime">-</div></div>
- <div class="field"><label>当前展示定义</label><div class="token-box" id="overview-current-presentation">-</div></div>
- <div class="field"><label>当前内容包</label><div class="token-box" id="overview-current-content-bundle">-</div></div>
- </div>
- <div class="panel" style="padding:18px;">
- <div class="eyebrow">操作提示</div>
- <div class="item"><strong>1. 先看资源总览</strong><div class="meta">确认地点、地图、路线组、活动、已发布数量是否符合预期。</div></div>
- <div class="item"><strong>2. 再选主流程</strong><div class="meta">地图 / 地点管理 -> 路线资源管理 -> 活动管理 / 活动编排 -> 发布中心。</div></div>
- <div class="item"><strong>3. 关联活动只看摘要</strong><div class="meta">地图页只看数量和跳转,活动详情统一放到“活动管理”。</div></div>
- </div>
- </div>
- <div class="grid-3">
- <div class="field"><label>运维手机号</label><input id="ops-mobile" value="13800138000"></div>
- <div class="field"><label>短信验证码</label><input id="ops-code" value=""></div>
- <div class="field"><label>运维显示名</label><input id="ops-display-name" value="开发运维"></div>
- <div class="field"><label>国家区号</label><input id="ops-country-code" value="86"></div>
- <div class="field"><label>设备标识</label><input id="ops-device-key" value="ops-console-001"></div>
- <div class="field"><label>当前角色</label><input id="ops-role-code" value="开发态自动放行" readonly></div>
- <div class="field"><label>活动 ID(总览 / pipeline)</label><input id="event-id" value="evt_demo_variant_manual_001"></div>
- <div class="field"><label>当前发布版本 ID</label><input id="release-id" value=""></div>
- <div class="field"><label>当前构建 ID</label><input id="build-id" value=""></div>
- </div>
- <div class="actions">
- <button id="btn-send-ops-code">发送验证码</button>
- <button id="btn-register-ops" class="secondary">注册运维账号</button>
- <button id="btn-login-ops" class="secondary">手机号登录</button>
- <button class="secondary" id="btn-refresh-overview">刷新总览</button>
- <button class="ghost" id="btn-clear-token">清空令牌</button>
- </div>
- <div class="hint">开发环境默认免登录放行,生产环境请使用手机号验证码注册/登录的独立运维账号。运维账号和前端玩家账号完全分离。</div>
- </section>
- <section class="panel ops-view" id="maps">
- <div class="eyebrow">地图 / 地点管理</div>
- <h3>先进入地图列表,再做新增、编辑和预览</h3>
- <p>地点是地图的归属容器,不是主入口。一个地点可挂多张地图,一张地图只属于一个地点。关联活动在这里先只看数量和摘要,详情统一去“活动管理”。</p>
- <div class="toolbar">
- <div class="grid-2" style="min-width:min(100%,720px);">
- <div class="field"><label>地图关键字</label><input id="map-search" value="" placeholder="按地图名称、编码、地点筛选"></div>
- <div class="field"><label>地点关键字</label><input id="place-search" value="" placeholder="按地点名称、省市、编码筛选"></div>
- </div>
- <div class="actions">
- <button id="btn-open-create-map">添加地图</button>
- <button class="ghost" id="btn-open-create-place">添加地点</button>
- <button class="secondary" id="btn-refresh-map-area">刷新地图区</button>
- </div>
- </div>
- <div class="panel" style="padding:18px;">
- <div class="eyebrow">地图列表</div>
- <div class="hint">默认只看地图列表。点一张地图,再进入地图详情;新增地图和新增地点都走独立弹出层。</div>
- <div class="list" id="map-library-list"><div class="item"><strong>暂无地图</strong><div class="meta">打开页面后会自动拉取地图列表,也可以手动点“刷新地图区”。</div></div></div>
- </div>
- <div hidden>
- <div id="place-list"></div>
- <div id="map-list"></div>
- </div>
- <div class="modal-backdrop" id="map-detail-modal" hidden>
- <div class="modal-card">
- <div class="eyebrow">地图详情</div>
- <h3 style="margin:0;">当前地图详情 / 预览</h3>
- <div class="split">
- <div class="panel" style="padding:18px;">
- <div class="field"><label>当前地点</label><div class="token-box" id="map-preview-place">-</div></div>
- <div class="field"><label>当前地图</label><div class="token-box" id="map-preview-map">-</div></div>
- <div class="field"><label>当前瓦片版本</label><div class="token-box" id="map-preview-tile-version">-</div></div>
- <div class="field"><label>Tile Base URL</label><div class="token-box" id="map-preview-tile-base">-</div></div>
- <div class="field"><label>Meta URL</label><div class="token-box" id="map-preview-meta">-</div></div>
- </div>
- <div class="panel" style="padding:18px;">
- <div class="field"><label>默认活动数量</label><div class="token-box" id="map-preview-default-count">0</div></div>
- <div class="field"><label>默认活动摘要</label><div class="token-box" id="map-preview-default-events">-</div></div>
- <div class="field"><label>关联活动数量</label><div class="token-box" id="map-preview-linked-count">0</div></div>
- <div class="field"><label>关联活动摘要</label><div class="token-box" id="map-preview-linked-summary">当前未关联活动</div></div>
- </div>
- </div>
- <div class="actions">
- <button id="btn-open-map-editor-from-detail">编辑地图</button>
- <button class="secondary" id="btn-open-map-tile-from-detail">导入瓦片版本</button>
- <button class="ghost" id="btn-open-events-view">前往活动管理</button>
- <button class="ghost" id="btn-close-map-detail">关闭详情</button>
- </div>
- </div>
- </div>
- <div class="modal-backdrop" id="map-editor-panel" hidden>
- <div class="modal-card">
- <div class="eyebrow">添加 / 编辑地图</div>
- <div class="grid-2">
- <div class="field"><label>管理地图 ID</label><input id="managed-map-id" value=""></div>
- <div class="field"><label>所属地点</label><select id="map-place-select"><option value="">请先录入地点</option></select></div>
- <div class="field"><label>地图编码</label><input id="map-code" value="lxcb-map-001"></div>
- <div class="field"><label>地图名称</label><input id="map-name" value="领秀城公园底图"></div>
- <div class="field"><label>地图类型</label><input id="map-type" value="raster"></div>
- <div class="field"><label>地图封面 URL</label><input id="map-cover-url" value=""></div>
- <div class="field"><label>地图摘要</label><input id="map-summary" value="领秀城公园地图资源,包含默认体验活动与当前瓦片版本。"></div>
- </div>
- <div class="panel" style="padding:18px;">
- <div class="eyebrow">上传 / 导入首个瓦片版本</div>
- <div class="grid-3">
- <div class="field"><label>瓦片版本号</label><input id="tile-version" value="2026-04-07"></div>
- <div class="field"><label>瓦片根地址</label><input id="tile-base-url" value="https://oss-mbh5.colormaprun.com/gotomars/map/lxcb-001/tiles/"></div>
- <div class="field"><label>元数据地址</label><input id="tile-meta-url" value="https://oss-mbh5.colormaprun.com/gotomars/map/lxcb-001/tiles/meta.json"></div>
- </div>
- </div>
- <div class="actions">
- <button id="btn-create-map-asset">新增地图</button>
- <button class="secondary" id="btn-update-map-asset">保存地图修改</button>
- <button class="secondary" id="btn-import-tile">导入瓦片版本</button>
- <button class="ghost" id="btn-get-map-asset">重新读取地图详情</button>
- <button class="ghost" id="btn-close-map-editor">关闭</button>
- </div>
- </div>
- </div>
- <div class="modal-backdrop" id="place-editor-panel" hidden>
- <div class="modal-card">
- <div class="eyebrow">添加 / 编辑地点</div>
- <div class="grid-2">
- <div class="field"><label>管理地点 ID</label><input id="place-manage-id" value=""></div>
- <div class="field"><label>地点编码</label><input id="place-code" value="lxcb-001"></div>
- <div class="field"><label>地点名称</label><input id="place-name" value="领秀城公园"></div>
- <div class="field"><label>省份</label><select id="place-province"><option value="">加载中...</option></select></div>
- <div class="field"><label>城市</label><select id="place-city"><option value="">请先选择省份</option></select></div>
- <div class="field"><label>地点区域</label><input id="place-region" value="" readonly></div>
- </div>
- <div class="actions">
- <button id="btn-create-place">保存地点</button>
- <button class="ghost" id="btn-get-place">重新读取地点详情</button>
- <button class="ghost" id="btn-close-place-editor">关闭</button>
- </div>
- </div>
- </div>
- </section>
- <section class="panel ops-view" id="ingest">
- <div class="eyebrow">资源录入</div>
- <h3>统一纳管资源</h3>
- <p>运维只需要关心“录一个资源”,不需要关心它最终是 OSS 上传还是已有外链。录完之后都落成统一资源对象。</p>
- <div class="split">
- <div class="panel" style="padding:18px;">
- <div class="eyebrow">上传文件</div>
- <p>适合 KML、schema、manifest、本地静态包。backend 负责上传 OSS 并纳管。</p>
- <div class="grid-2">
- <div class="field"><label>资源类型</label><select id="asset-type"><option value="kml">kml</option><option value="tiles">tiles</option><option value="presentation">presentation</option><option value="content_bundle">content_bundle</option><option value="static_bundle">static_bundle</option></select></div>
- <div class="field"><label>资源编码</label><input id="asset-code" value="lxcb-route-pack-2026-04-07"></div>
- <div class="field"><label>版本</label><input id="asset-version" value="2026-04-07"></div>
- <div class="field"><label>标题</label><input id="asset-title" value="领秀城多赛道 KML 包"></div>
- <div class="field"><label>状态</label><select id="asset-status"><option value="active">active</option><option value="draft">draft</option></select></div>
- <div class="field"><label>对象目录(可选)</label><input id="asset-object-dir" value="gotomars/kml/lxcb-001/2026-04-07"></div>
- <div class="field"><label>内容类型(可选)</label><input id="asset-content-type" value="application/vnd.google-earth.kml+xml"></div>
- <div class="field"><label>上传文件</label><input id="asset-file" type="file"></div>
- </div>
- <div class="field"><label>Metadata JSON(可选)</label><textarea id="asset-metadata">{}</textarea></div>
- <div class="actions"><button id="btn-upload-asset">上传并纳管资源</button></div>
- </div>
- <div class="panel" style="padding:18px;">
- <div class="eyebrow">登记外链</div>
- <p>适合正式 OSS / CDN 上已经存在的资源。登记后,活动侧和发布链统一只认受管资源对象。</p>
- <div class="field"><label>外链 URL</label><input id="asset-public-url" value="https://oss-mbh5.colormaprun.com/gotomars/kml/lxcb-001/2026-04-07/route01.kml"></div>
- <div class="actions">
- <button class="secondary" id="btn-register-link">登记外链资源</button>
- <button class="ghost" id="btn-list-assets">查看受管资源</button>
- </div>
- <div class="hint">建议优先纳管:地图瓦片目录、KML、presentation schema、content bundle manifest。</div>
- </div>
- </div>
- </section>
- <section class="panel ops-view" id="courses">
- <div class="eyebrow">KML / 赛道管理</div>
- <h3>围绕当前地图管理 KML、赛道集和默认路线</h3>
- <p>KML 不是独立漂在外面的资源。运维上应该先选地图,再导入一批 KML,形成赛道集,然后查看默认路线和预览数据。</p>
- <div class="split">
- <div class="panel" style="padding:18px;">
- <div class="eyebrow">导入瓦片版本</div>
- <div class="grid-2">
- <div class="field"><label>当前地点编码</label><input value="复用上方“地图资源管理”的地点编码" readonly></div>
- <div class="field"><label>当前地图编码</label><input value="复用上方“地图资源管理”的地图编码" readonly></div>
- <div class="field"><label>瓦片版本号</label><input id="tile-version" value="2026-04-07"></div>
- <div class="field"><label>瓦片根地址</label><input id="tile-base-url" value="https://oss-mbh5.colormaprun.com/gotomars/map/lxcb-001/tiles/"></div>
- <div class="field"><label>元数据地址</label><input id="tile-meta-url" value="https://oss-mbh5.colormaprun.com/gotomars/map/lxcb-001/tiles/meta.json"></div>
- </div>
- <div class="actions"><button id="btn-import-tile">导入瓦片版本</button></div>
- </div>
- <div class="panel tall" style="padding:18px;">
- <div class="eyebrow">批量导入 KML</div>
- <div class="grid-2">
- <div class="field"><label>Course Set Code</label><input id="course-set-code" value="lxcb-manual-2026-04-07"></div>
- <div class="field"><label>Course Set Name</label><input id="course-set-name" value="领秀城公园多赛道 2026-04-07"></div>
- <div class="field"><label>Mode</label><input id="course-mode" value="classic-sequential"></div>
- <div class="field"><label>Default Route Code</label><input id="default-route-code" value="route-variant-d"></div>
- </div>
- <div class="field">
- <label>KML Batch JSON</label>
- <textarea id="routes-json">[
- {"name":"路线 01","routeCode":"route-variant-a","fileUrl":"https://oss-mbh5.colormaprun.com/gotomars/kml/lxcb-001/2026-04-07/route01.kml","sourceType":"kml","controlCount":10,"status":"active"},
- {"name":"路线 02","routeCode":"route-variant-b","fileUrl":"https://oss-mbh5.colormaprun.com/gotomars/kml/lxcb-001/2026-04-07/route02.kml","sourceType":"kml","controlCount":10,"status":"active"},
- {"name":"路线 03","routeCode":"route-variant-c","fileUrl":"https://oss-mbh5.colormaprun.com/gotomars/kml/lxcb-001/2026-04-07/route03.kml","sourceType":"kml","controlCount":10,"status":"active"},
- {"name":"路线 04","routeCode":"route-variant-d","fileUrl":"https://oss-mbh5.colormaprun.com/gotomars/kml/lxcb-001/2026-04-07/route04.kml","sourceType":"kml","controlCount":10,"status":"active"}
- ]</textarea>
- </div>
- <div class="actions"><button id="btn-import-kml-batch">批量导入 KML</button></div>
- </div>
- </div>
- <div class="split">
- <div class="panel" style="padding:18px;">
- <div class="eyebrow">当前地图下赛道集</div>
- <div class="list" id="course-set-list"><div class="item"><strong>暂无赛道集</strong><div class="meta">读取地图详情或完成一轮 KML 导入后,这里会显示当前地图的赛道集。</div></div></div>
- </div>
- <div class="panel" style="padding:18px;">
- <div class="eyebrow">KML / 变体预览</div>
- <div class="field"><label>当前赛道集</label><div class="token-box" id="course-preview-course-set">-</div></div>
- <div class="field"><label>默认路线</label><div class="token-box" id="course-preview-default-route">-</div></div>
- <div class="field"><label>路线数量</label><div class="token-box" id="course-preview-variant-count">0</div></div>
- <div class="field"><label>路线摘要</label><div class="token-box" id="course-preview-variants">-</div></div>
- </div>
- </div>
- </section>
- <section class="panel ops-view" id="events">
- <div class="eyebrow">活动管理</div>
- <h3>先看活动列表和基础信息</h3>
- <p>活动管理先处理业务壳:名称、状态、是否默认体验、是否出现在活动列表,以及当前发布概况。资源绑定与发布准备统一去“活动编排”。</p>
- <div class="grid-3">
- <div class="field"><label>租户编码</label><input id="event-tenant-code" value="tenant_demo"></div>
- <div class="field"><label>活动标识 Slug</label><input id="event-slug" value="city-park-manual-variant"></div>
- <div class="field"><label>活动名称</label><input id="event-display-name" value="领秀城公园多赛道挑战"></div>
- <div class="field"><label>活动摘要</label><input id="event-summary" value="多赛道联调体验活动"></div>
- <div class="field"><label>活动状态</label><input id="event-status" value="active"></div>
- <div class="field"><label>活动 ID</label><input id="binding-event-id" value="evt_demo_variant_manual_001"></div>
- </div>
- <div class="actions">
- <button class="secondary" id="btn-list-events">读取活动列表</button>
- <button class="secondary" id="btn-create-event">新建活动</button>
- <button class="secondary" id="btn-update-event">更新活动</button>
- <button class="secondary" id="btn-get-event">读取活动</button>
- <button class="ghost" id="btn-open-compose-view">前往活动编排</button>
- </div>
- <div class="split">
- <div class="panel" style="padding:18px;">
- <div class="eyebrow">活动列表</div>
- <div class="list" id="event-list-main"><div class="item"><strong>暂无活动</strong><div class="meta">先点“读取活动列表”。</div></div></div>
- </div>
- <div class="panel" style="padding:18px;">
- <div class="eyebrow">当前活动概况</div>
- <div class="field"><label>当前发布版本</label><div class="token-box" id="event-preview-release">-</div></div>
- <div class="field"><label>当前 runtime</label><div class="token-box" id="event-preview-runtime">-</div></div>
- <div class="field"><label>当前展示定义</label><div class="token-box" id="event-preview-presentation">-</div></div>
- <div class="field"><label>当前内容包</label><div class="token-box" id="event-preview-content-bundle">-</div></div>
- </div>
- </div>
- </section>
- <section class="panel ops-view" id="compose">
- <div class="eyebrow">活动编排</div>
- <h3>绑定运行对象、展示定义和内容包</h3>
- <p>这里才进入发布前准备:给当前活动绑定 runtime、presentation、content bundle,确认默认 active 三元组,然后交给发布中心 build / publish。</p>
- <div class="grid-3">
- <div class="field"><label>Presentation ID</label><input id="presentation-id" value=""></div>
- <div class="field"><label>Content Bundle ID</label><input id="content-bundle-id" value=""></div>
- <div class="field"><label>Runtime Binding ID</label><input id="runtime-binding-id" value=""></div>
- <div class="field"><label>Presentation Title</label><input id="presentation-title" value="多赛道详情展示模板"></div>
- <div class="field"><label>Presentation Template Key</label><input id="presentation-template-key" value="event.detail.multi-variant"></div>
- <div class="field"><label>Presentation Schema URL</label><input id="presentation-schema-url" value="https://oss-mbh5.colormaprun.com/gotomars/presentations/event-detail-standard/v2026-04-07/schema.json"></div>
- <div class="field"><label>Presentation Version</label><input id="presentation-version" value="v2026-04-07"></div>
- <div class="field"><label>Bundle Title</label><input id="bundle-title" value="多赛道结果内容包"></div>
- <div class="field"><label>Bundle Type</label><input id="bundle-type" value="result_media"></div>
- <div class="field"><label>Bundle Manifest URL</label><input id="bundle-manifest-url" value="https://oss-mbh5.colormaprun.com/gotomars/content-bundles/result-media-manual/v2026-04-07/manifest.json"></div>
- <div class="field"><label>Bundle Version</label><input id="bundle-version" value="v2026-04-07"></div>
- </div>
- <div class="actions">
- <button id="btn-import-presentation">导入展示定义</button>
- <button id="btn-import-bundle">导入内容包</button>
- <button class="ghost" id="btn-save-defaults">保存活动默认绑定</button>
- <button class="ghost" id="btn-open-publish-view">前往发布中心</button>
- </div>
- </section>
- <section class="panel ops-view" id="publish">
- <div class="eyebrow">发布中心</div>
- <h3>统一 build / publish / rollback</h3>
- <p>运维后台不造第二条发布链,仍然复用现在这套 source、build、release 流程。</p>
- <div class="grid-3">
- <div class="field"><label>Pipeline Event ID</label><input id="pipeline-event-id" value="evt_demo_variant_manual_001"></div>
- <div class="field"><label>配置源 ID</label><input id="source-id" value=""></div>
- <div class="field"><label>回滚发布版本 ID</label><input id="rollback-release-id" value=""></div>
- </div>
- <div class="actions">
- <button class="secondary" id="btn-get-pipeline">读取发布链</button>
- <button id="btn-build-source">构建配置源</button>
- <button id="btn-publish-build">发布构建</button>
- <button class="ghost" id="btn-get-release">读取发布版本</button>
- <button class="ghost" id="btn-rollback-release">回滚发布</button>
- </div>
- </section>
- </main>
- <aside class="aside">
- <div class="field">
- <label>当前 Bearer Token</label>
- <div class="token-box" id="token-box">当前无令牌</div>
- </div>
- <section class="panel" style="padding:18px;">
- <div class="eyebrow">当前状态</div>
- <p>当前动作结果会写到这里,方便你快速判断到底卡在资源录入、活动绑定还是发布流程。</p>
- <div class="statusbox" id="status-box">待执行</div>
- </section>
- <section class="panel" style="padding:18px;">
- <div class="eyebrow">响应日志</div>
- <p>保留最后一次响应。运维排查时先看这里,不用再回调试工作台翻日志。</p>
- <div class="logbox" id="log-box">等待操作...</div>
- </section>
- <section class="panel" style="padding:18px;">
- <div class="eyebrow">最近受管资源</div>
- <div class="list" id="asset-list"><div class="item"><strong>暂无数据</strong><div class="meta">先执行刷新总览或查看受管资源</div></div></div>
- </section>
- <section class="panel" style="padding:18px;">
- <div class="eyebrow">最近活动</div>
- <div class="list" id="event-list"><div class="item"><strong>暂无数据</strong><div class="meta">先执行刷新总览</div></div></div>
- </section>
- </aside>
- </div>
- <script>
- const STORAGE_KEY = 'cmr-ops-workbench-v5';
- const state = {
- accessToken: '',
- activeView: 'overview',
- placeItems: [],
- mapItems: [],
- placeMapItems: [],
- regionOptions: [],
- mapEditorMode: 'none'
- };
- function $(id) { return document.getElementById(id); }
- function setActiveView(view) {
- state.activeView = view || 'overview';
- document.querySelectorAll('.ops-view').forEach(section => {
- section.classList.toggle('active', section.id === state.activeView);
- });
- document.querySelectorAll('.nav-link[data-view]').forEach(button => {
- button.classList.toggle('active', button.dataset.view === state.activeView);
- });
- }
- function setStatus(text, isError) {
- const el = $('status-box');
- el.textContent = text;
- el.className = 'statusbox ' + (isError ? 'status-error' : 'status-ok');
- }
- function writeLog(title, payload) {
- $('log-box').textContent = '[' + new Date().toLocaleString() + '] ' + title + '\n' + JSON.stringify(payload, null, 2);
- }
- function syncToken() {
- $('token-box').textContent = state.accessToken
- ? ('Bearer ' + state.accessToken.slice(0, 36) + '...')
- : '开发环境默认免登录,可直接操作;如需验证运维账号链路,再执行手机号登录。';
- }
- function setMapEditorMode(mode) {
- state.mapEditorMode = mode || 'none';
- $('map-editor-panel').hidden = state.mapEditorMode !== 'map';
- $('place-editor-panel').hidden = state.mapEditorMode !== 'place';
- persist();
- }
- function getFieldMap() {
- return {
- opsMobile: $('ops-mobile'),
- opsCode: $('ops-code'),
- opsDisplayName: $('ops-display-name'),
- opsCountryCode: $('ops-country-code'),
- opsDeviceKey: $('ops-device-key'),
- opsRoleCode: $('ops-role-code'),
- eventId: $('event-id'),
- releaseId: $('release-id'),
- buildId: $('build-id'),
- managedPlaceId: $('managed-place-id'),
- managedMapId: $('managed-map-id'),
- mapPlaceSelect: $('map-place-select'),
- placeManageId: $('place-manage-id'),
- assetType: $('asset-type'),
- assetCode: $('asset-code'),
- assetVersion: $('asset-version'),
- assetTitle: $('asset-title'),
- assetStatus: $('asset-status'),
- assetObjectDir: $('asset-object-dir'),
- assetPublicUrl: $('asset-public-url'),
- assetContentType: $('asset-content-type'),
- assetMetadata: $('asset-metadata'),
- placeCode: $('place-code'),
- placeName: $('place-name'),
- placeProvince: $('place-province'),
- placeCity: $('place-city'),
- placeRegion: $('place-region'),
- mapCode: $('map-code'),
- mapName: $('map-name'),
- mapType: $('map-type'),
- mapCoverUrl: $('map-cover-url'),
- mapSummary: $('map-summary'),
- tileVersion: $('tile-version'),
- tileBaseUrl: $('tile-base-url'),
- tileMetaUrl: $('tile-meta-url'),
- courseSetCode: $('course-set-code'),
- courseSetName: $('course-set-name'),
- courseMode: $('course-mode'),
- defaultRouteCode: $('default-route-code'),
- routesJson: $('routes-json'),
- eventTenantCode: $('event-tenant-code'),
- eventSlug: $('event-slug'),
- eventDisplayName: $('event-display-name'),
- eventSummary: $('event-summary'),
- eventStatus: $('event-status'),
- bindingEventId: $('binding-event-id'),
- presentationId: $('presentation-id'),
- contentBundleId: $('content-bundle-id'),
- runtimeBindingId: $('runtime-binding-id'),
- presentationTitle: $('presentation-title'),
- presentationTemplateKey: $('presentation-template-key'),
- presentationSchemaUrl: $('presentation-schema-url'),
- presentationVersion: $('presentation-version'),
- bundleTitle: $('bundle-title'),
- bundleType: $('bundle-type'),
- bundleManifestUrl: $('bundle-manifest-url'),
- bundleVersion: $('bundle-version'),
- pipelineEventId: $('pipeline-event-id'),
- sourceId: $('source-id'),
- rollbackReleaseId: $('rollback-release-id')
- };
- }
- function persist() {
- const payload = { accessToken: state.accessToken, activeView: state.activeView };
- Object.entries(getFieldMap()).forEach(([key, node]) => payload[key] = node.value);
- localStorage.setItem(STORAGE_KEY, JSON.stringify(payload));
- }
- function restore() {
- const raw = localStorage.getItem(STORAGE_KEY);
- if (!raw) {
- syncToken();
- return;
- }
- try {
- const p = JSON.parse(raw);
- state.accessToken = p.accessToken || '';
- state.activeView = p.activeView || 'overview';
- Object.entries(getFieldMap()).forEach(([key, node]) => {
- if (p[key] !== undefined && p[key] !== null && p[key] !== '') node.value = p[key];
- });
- } catch (_) {}
- syncToken();
- setActiveView(state.activeView);
- }
- function syncPlaceRegion() {
- const provinceText = $('place-province').selectedOptions[0] ? $('place-province').selectedOptions[0].textContent : '';
- const cityText = $('place-city').selectedOptions[0] ? $('place-city').selectedOptions[0].textContent : '';
- $('place-region').value = [provinceText, cityText].filter(Boolean).join(' / ');
- persist();
- }
- function renderCityOptions(provinceCode, preferredCityCode) {
- const province = state.regionOptions.find(item => item.code === provinceCode);
- const cities = province ? province.cities || [] : [];
- $('place-city').innerHTML = cities.length
- ? cities.map(item => '<option value="' + item.code + '">' + item.name + '</option>').join('')
- : '<option value="">暂无城市</option>';
- if (preferredCityCode && cities.some(item => item.code === preferredCityCode)) {
- $('place-city').value = preferredCityCode;
- } else if (cities[0]) {
- $('place-city').value = cities[0].code;
- } else {
- $('place-city').value = '';
- }
- syncPlaceRegion();
- }
- function renderProvinceOptions(preferredProvinceCode, preferredCityCode) {
- $('place-province').innerHTML = state.regionOptions.length
- ? state.regionOptions.map(item => '<option value="' + item.code + '">' + item.name + '</option>').join('')
- : '<option value="">暂无省份</option>';
- if (preferredProvinceCode && state.regionOptions.some(item => item.code === preferredProvinceCode)) {
- $('place-province').value = preferredProvinceCode;
- } else if (state.regionOptions[0]) {
- $('place-province').value = state.regionOptions[0].code;
- } else {
- $('place-province').value = '';
- }
- renderCityOptions($('place-province').value, preferredCityCode);
- }
- async function loadRegionOptions() {
- const result = await request('GET', '/ops/admin/region-options');
- state.regionOptions = result.data || [];
- renderProvinceOptions($('place-province').value, $('place-city').value);
- return result;
- }
- function applyRegionText(regionText) {
- if (!regionText || !state.regionOptions.length) {
- return;
- }
- const [provinceText, cityText] = String(regionText).split('/').map(item => item.trim()).filter(Boolean);
- const province = state.regionOptions.find(item => item.name === provinceText);
- if (!province) {
- $('place-region').value = regionText;
- return;
- }
- $('place-province').value = province.code;
- renderCityOptions(province.code);
- const city = (province.cities || []).find(item => item.name === cityText);
- if (city) {
- $('place-city').value = city.code;
- }
- syncPlaceRegion();
- }
- async function request(method, url, body, isJSON = true) {
- const headers = {};
- let payload = body;
- if (state.accessToken) headers['Authorization'] = 'Bearer ' + state.accessToken;
- if (isJSON && body !== undefined) {
- headers['Content-Type'] = 'application/json';
- payload = JSON.stringify(body);
- }
- const resp = await fetch(url, { method, headers, body: payload });
- const text = await resp.text();
- let data = {};
- try { data = text ? JSON.parse(text) : {}; } catch (_) { data = { raw: text }; }
- if (!resp.ok) throw { status: resp.status, body: data, method, url };
- return data;
- }
- function renderItems(rootId, items, render) {
- const root = $(rootId);
- if (!Array.isArray(items) || !items.length) {
- root.innerHTML = '<div class="item"><strong>暂无数据</strong><div class="meta">当前还没有可展示对象</div></div>';
- return;
- }
- root.innerHTML = items.map(render).join('');
- }
- function renderAssets(items) {
- renderItems('asset-list', items, item =>
- '<div class="item"><strong>' + (item.title || item.assetCode || item.id || '-') + '</strong>' +
- '<div class="meta">' + (item.assetType || '-') + ' / ' + (item.assetCode || '-') + ' / ' + (item.version || '-') + '</div>' +
- '<div class="meta">' + (item.publicUrl || '-') + '</div></div>'
- );
- }
- function renderEvents(items) {
- renderItems('event-list', items, item =>
- '<div class="item"><strong>' + (item.displayName || item.id || '-') + '</strong>' +
- '<div class="meta">' + (item.id || '-') + ' / ' + (item.status || '-') + '</div>' +
- '<div class="meta">' + (item.summary || '暂无摘要') + '</div></div>'
- );
- }
- function renderEventListMain(items) {
- renderItems('event-list-main', items, item =>
- '<div class="item"><strong>' + (item.displayName || item.id || '-') + '</strong>' +
- '<div class="meta">' + (item.id || '-') + ' / ' + (item.status || '-') + '</div>' +
- '<div class="meta">' + ((item.currentRelease && item.currentRelease.id) || '当前未发布') + '</div></div>'
- );
- }
- function renderPlaces(items) {
- const keyword = ($('place-search') && $('place-search').value || '').trim().toLowerCase();
- const filtered = !keyword ? items : (items || []).filter(item =>
- [item.name, item.code, item.region].filter(Boolean).join(' ').toLowerCase().includes(keyword)
- );
- renderItems('place-list', items, item =>
- '<div class="item selectable' + ((item.id || '') === $('managed-place-id').value ? ' active' : '') + '" data-place-id="' + (item.id || '') + '"><strong>' + (item.name || item.code || item.id || '-') + '</strong>' +
- '<div class="meta">' + (item.id || '-') + ' / ' + (item.code || '-') + '</div>' +
- '<div class="meta">' + (item.region || '区域待补充') + '</div></div>'
- );
- renderItems('place-list', filtered, item =>
- '<div class="item selectable' + ((item.id || '') === $('managed-place-id').value ? ' active' : '') + '" data-place-id="' + (item.id || '') + '"><strong>' + (item.name || item.code || item.id || '-') + '</strong>' +
- '<div class="meta">' + (item.id || '-') + ' / ' + (item.code || '-') + '</div>' +
- '<div class="meta">' + (item.region || '区域待补充') + '</div></div>'
- );
- }
- function renderMapAssets(items) {
- const keyword = ($('map-search') && $('map-search').value || '').trim().toLowerCase();
- const filtered = !keyword ? items : (items || []).filter(item =>
- [item.name, item.code, item.placeName].filter(Boolean).join(' ').toLowerCase().includes(keyword)
- );
- renderItems('map-list', filtered, item =>
- '<div class="item selectable' + ((item.id || '') === $('managed-map-id').value ? ' active' : '') + '" data-map-id="' + (item.id || '') + '"><strong>' + (item.name || item.code || item.id || '-') + '</strong>' +
- '<div class="meta">' + (item.id || '-') + ' / ' + (item.code || '-') + ' / ' + (item.mapType || '-') + '</div>' +
- '<div class="meta">' + ((item.currentTileRelease && item.currentTileRelease.versionCode) || '当前无瓦片版本') + '</div></div>'
- );
- }
- function renderMapLibrary(items) {
- const keyword = ($('map-search') && $('map-search').value || '').trim().toLowerCase();
- const filtered = !keyword ? items : (items || []).filter(item =>
- [item.name, item.code, item.placeName].filter(Boolean).join(' ').toLowerCase().includes(keyword)
- );
- renderItems('map-library-list', filtered, item =>
- '<div class="item selectable' + ((item.id || '') === $('managed-map-id').value ? ' active' : '') + '" data-map-id="' + (item.id || '') + '"><strong>' + (item.name || item.code || item.id || '-') + '</strong>' +
- '<div class="meta">' + (item.id || '-') + ' / ' + (item.placeName || '未关联地点') + '</div>' +
- '<div class="meta">' + ((item.currentTileRelease && item.currentTileRelease.versionCode) || '当前无瓦片版本') + '</div></div>'
- );
- }
- function renderMapPlaceSelect() {
- const items = Array.isArray(state.placeItems) ? state.placeItems : [];
- $('map-place-select').innerHTML = items.length
- ? items.map(item => '<option value="' + item.id + '">' + (item.name || item.code || item.id) + ' / ' + (item.region || '未设置区域') + '</option>').join('')
- : '<option value="">请先录入地点</option>';
- if ($('managed-place-id').value && items.some(item => item.id === $('managed-place-id').value)) {
- $('map-place-select').value = $('managed-place-id').value;
- } else if (items[0]) {
- $('map-place-select').value = items[0].id;
- $('managed-place-id').value = items[0].id;
- } else {
- $('map-place-select').value = '';
- }
- persist();
- }
- function renderLinkedEvents(items) {
- renderItems('linked-event-list', items, item =>
- '<div class="item"><strong>' + (item.title || item.eventId || '-') + '</strong>' +
- '<div class="meta">' + (item.eventId || '-') + ' / ' + (item.status || '-') + (item.isDefaultExperience ? ' / 默认体验' : '') + '</div>' +
- '<div class="meta">' + (item.currentReleaseId || '当前未发布') + ' / ' + (item.currentPresentation || '无展示定义') + ' / ' + (item.currentContentBundle || '无内容包') + '</div></div>'
- );
- }
- function renderCourseSets(items) {
- renderItems('course-set-list', items, item =>
- '<div class="item"><strong>' + (item.name || item.code || item.id || '-') + '</strong>' +
- '<div class="meta">' + (item.id || '-') + ' / ' + (item.mode || '-') + ' / ' + (item.status || '-') + '</div>' +
- '<div class="meta">' + ((item.currentVariant && (item.currentVariant.name || item.currentVariant.routeCode)) || '当前无默认路线') + '</div></div>'
- );
- }
- async function refreshOverviewAndLog(reason) {
- try {
- const result = await refreshOverview();
- writeLog(reason || 'refresh-overview', result);
- setStatus('已刷新:资源总览', false);
- } catch (error) {
- writeLog(reason || 'refresh-overview', { error });
- setStatus('失败:资源总览自动刷新', true);
- }
- }
- function setPreviewValue(id, value) {
- $(id).textContent = (value !== undefined && value !== null && value !== '') ? String(value) : '-';
- }
- function applyPipelineSummary(pipeline) {
- const sources = Array.isArray(pipeline.sources) ? pipeline.sources : [];
- const releases = Array.isArray(pipeline.releases) ? pipeline.releases : [];
- if (pipeline.currentRelease && pipeline.currentRelease.id) $('release-id').value = pipeline.currentRelease.id;
- if (sources[0] && sources[0].id) $('source-id').value = sources[0].id;
- if (releases[0] && releases[0].id) $('rollback-release-id').value = releases[0].id;
- setPreviewValue('overview-current-release', pipeline.currentRelease ? (pipeline.currentRelease.id || '-') : '当前未发布');
- setPreviewValue('overview-current-runtime', pipeline.currentRelease && pipeline.currentRelease.runtime ? ((pipeline.currentRelease.runtime.name || pipeline.currentRelease.runtime.runtimeBindingId || pipeline.currentRelease.runtime.id || '-') + ' / ' + (pipeline.currentRelease.runtime.runtimeBindingId || pipeline.currentRelease.runtime.id || '-')) : '当前未绑定');
- setPreviewValue('overview-current-presentation', pipeline.currentRelease && pipeline.currentRelease.presentation ? ((pipeline.currentRelease.presentation.name || pipeline.currentRelease.presentation.templateKey || '-') + ' / ' + (pipeline.currentRelease.presentation.presentationId || pipeline.currentRelease.presentation.id || '-')) : '当前未绑定');
- setPreviewValue('overview-current-content-bundle', pipeline.currentRelease && pipeline.currentRelease.contentBundle ? ((pipeline.currentRelease.contentBundle.name || pipeline.currentRelease.contentBundle.bundleType || '-') + ' / ' + (pipeline.currentRelease.contentBundle.contentBundleId || pipeline.currentRelease.contentBundle.id || '-')) : '当前未绑定');
- persist();
- }
- function applyEventDetail(detail) {
- if (!detail) return;
- if (detail.event && detail.event.id) {
- $('event-id').value = detail.event.id;
- $('binding-event-id').value = detail.event.id;
- $('pipeline-event-id').value = detail.event.id;
- setPreviewValue('overview-current-event', (detail.event.displayName || detail.event.id || '-') + ' / ' + (detail.event.id || '-'));
- }
- if (detail.event && detail.event.tenantCode) $('event-tenant-code').value = detail.event.tenantCode;
- if (detail.event && detail.event.slug) $('event-slug').value = detail.event.slug;
- if (detail.event && detail.event.displayName) $('event-display-name').value = detail.event.displayName;
- if (detail.event && detail.event.summary) $('event-summary').value = detail.event.summary;
- if (detail.event && detail.event.status) $('event-status').value = detail.event.status;
- if (detail.event && detail.event.currentRelease && detail.event.currentRelease.id) $('release-id').value = detail.event.currentRelease.id;
- if (detail.latestSource && detail.latestSource.id) $('source-id').value = detail.latestSource.id;
- if (detail.currentPresentation && detail.currentPresentation.id) $('presentation-id').value = detail.currentPresentation.id;
- if (detail.currentContentBundle && detail.currentContentBundle.id) $('content-bundle-id').value = detail.currentContentBundle.id;
- if (detail.currentRuntime && detail.currentRuntime.id) $('runtime-binding-id').value = detail.currentRuntime.id;
- setPreviewValue('event-preview-runtime', detail.currentRuntime ? (detail.currentRuntime.runtimeBindingId || detail.currentRuntime.id || '-') : '当前未绑定');
- setPreviewValue('event-preview-presentation', detail.currentPresentation ? ((detail.currentPresentation.name || '-') + ' / ' + (detail.currentPresentation.presentationId || detail.currentPresentation.id || '-')) : '当前未绑定');
- setPreviewValue('event-preview-content-bundle', detail.currentContentBundle ? ((detail.currentContentBundle.name || '-') + ' / ' + (detail.currentContentBundle.contentBundleId || detail.currentContentBundle.id || '-')) : '当前未绑定');
- setPreviewValue('event-preview-release', detail.event && detail.event.currentRelease ? (detail.event.currentRelease.id || '-') : '当前未发布');
- persist();
- }
- function applyPlaceDetail(detail) {
- if (!detail || !detail.place) return;
- $('managed-place-id').value = detail.place.id || '';
- $('place-manage-id').value = detail.place.id || '';
- $('place-code').value = detail.place.code || $('place-code').value;
- $('place-name').value = detail.place.name || $('place-name').value;
- $('place-region').value = detail.place.region || $('place-region').value;
- applyRegionText(detail.place.region || '');
- state.placeMapItems = detail.mapAssets || [];
- renderMapAssets(state.placeMapItems);
- if (Array.isArray(detail.mapAssets) && detail.mapAssets[0] && detail.mapAssets[0].id) {
- $('managed-map-id').value = detail.mapAssets[0].id;
- }
- setPreviewValue('map-preview-place', (detail.place.name || '-') + ' / ' + (detail.place.code || '-'));
- renderMapPlaceSelect();
- persist();
- }
- function applyMapAssetDetail(detail) {
- if (!detail || !detail.mapAsset) return;
- $('managed-map-id').value = detail.mapAsset.id || '';
- $('map-code').value = detail.mapAsset.code || $('map-code').value;
- $('map-name').value = detail.mapAsset.name || $('map-name').value;
- $('map-type').value = detail.mapAsset.mapType || $('map-type').value;
- $('map-cover-url').value = detail.mapAsset.coverUrl || $('map-cover-url').value;
- $('map-summary').value = detail.mapAsset.description || $('map-summary').value;
- const currentTile = detail.mapAsset.currentTileRelease || (Array.isArray(detail.tileReleases) ? detail.tileReleases[0] : null);
- if (currentTile && currentTile.id) {
- $('tile-version').value = currentTile.versionCode || $('tile-version').value;
- $('tile-base-url').value = currentTile.tileBaseUrl || $('tile-base-url').value;
- $('tile-meta-url').value = currentTile.metaUrl || $('tile-meta-url').value;
- }
- setPreviewValue('map-preview-map', (detail.mapAsset.name || '-') + ' / ' + (detail.mapAsset.code || '-'));
- setPreviewValue('map-preview-tile-version', currentTile ? (currentTile.versionCode || '-') : '-');
- setPreviewValue('map-preview-tile-base', currentTile ? (currentTile.tileBaseUrl || '-') : '-');
- setPreviewValue('map-preview-meta', currentTile ? (currentTile.metaUrl || '-') : '-');
- renderCourseSets(detail.courseSets || []);
- const linked = Array.isArray(detail.linkedEvents) ? detail.linkedEvents : [];
- const defaultCount = linked.filter(item => !!item.isDefaultExperience).length;
- setPreviewValue('map-preview-linked-count', linked.length);
- setPreviewValue('map-preview-linked-summary', linked.length ? linked.slice(0, 4).map(item => (item.title || item.eventId || '-') + (item.isDefaultExperience ? ' / 默认体验' : '')).join('\n') : '当前未关联活动');
- setPreviewValue('map-preview-default-count', defaultCount || $('map-preview-default-count').textContent || 0);
- persist();
- }
- function applyMapExperienceDetail(detail) {
- if (!detail) return;
- setPreviewValue('map-preview-default-count', detail.defaultExperienceCount || 0);
- const items = Array.isArray(detail.defaultExperiences) ? detail.defaultExperiences : [];
- setPreviewValue(
- 'map-preview-default-events',
- items.length
- ? items.map(item => (item.title || item.eventId || '-') + ' / ' + (item.status || item.statusCode || '-')).join('\n')
- : '当前无默认活动'
- );
- }
- function applyCourseImportDetail(result) {
- if (!result) return;
- if (result.courseSet && result.courseSet.id) {
- setPreviewValue('course-preview-course-set', (result.courseSet.name || '-') + ' / ' + (result.courseSet.id || '-'));
- setPreviewValue('course-preview-default-route', (result.courseSet.currentVariant && (result.courseSet.currentVariant.routeCode || result.courseSet.currentVariant.name)) || $('default-route-code').value || '-');
- }
- const variants = Array.isArray(result.variants) ? result.variants : [];
- setPreviewValue('course-preview-variant-count', variants.length);
- setPreviewValue('course-preview-variants', variants.length ? variants.map(item => (item.name || '-') + ' / ' + (item.routeCode || '-') + ' / ' + (item.status || '-')).join('\n') : '暂无路线');
- }
- async function run(title, fn) {
- setStatus('执行中:' + title, false);
- try {
- const result = await fn();
- writeLog(title, result);
- setStatus('完成:' + title, false);
- persist();
- return result;
- } catch (error) {
- const serializedError = (error && typeof error === 'object')
- ? {
- name: error.name || 'Error',
- message: error.message || '',
- stack: error.stack || '',
- status: error.status,
- body: error.body,
- method: error.method,
- url: error.url
- }
- : { message: String(error) };
- writeLog(title, { error: serializedError });
- setStatus('失败:' + title, true);
- return null;
- }
- }
- async function refreshOverview() {
- const [summaryResp, assetsResp, eventsResp, pipelineResp] = await Promise.all([
- request('GET', '/ops/admin/summary'),
- request('GET', '/ops/admin/assets'),
- request('GET', '/ops/admin/events?limit=12'),
- request('GET', '/ops/admin/events/' + encodeURIComponent($('event-id').value) + '/pipeline')
- ]);
- const summary = summaryResp.data || {};
- $('metric-assets').textContent = String(summary.managedAssets || 0);
- $('metric-places').textContent = String(summary.places || 0);
- $('metric-map-assets').textContent = String(summary.mapAssets || 0);
- $('metric-tile-releases').textContent = String(summary.tileReleases || 0);
- $('metric-course-sets').textContent = String(summary.courseSets || 0);
- $('metric-course-variants').textContent = String(summary.courseVariants || 0);
- $('metric-events').textContent = String(summary.events || 0);
- $('metric-default-events').textContent = String(summary.defaultEvents || 0);
- $('metric-published-events').textContent = String(summary.publishedEvents || 0);
- $('metric-config-sources').textContent = String(summary.configSources || 0);
- $('metric-runtime-bindings').textContent = String(summary.runtimeBindings || 0);
- $('metric-pipeline-releases').textContent = String(summary.releases || 0);
- $('metric-presentations').textContent = String(summary.presentations || 0);
- $('metric-content-bundles').textContent = String(summary.contentBundles || 0);
- $('metric-ops-users').textContent = String(summary.opsUsers || 0);
- renderAssets(assetsResp.data || []);
- renderEvents(eventsResp.data || []);
- applyPipelineSummary(pipelineResp.data || {});
- return {
- summary,
- assets: (assetsResp.data || []).length,
- events: (eventsResp.data || []).length,
- pipeline: pipelineResp.data || {}
- };
- }
- async function loadPlaces() {
- const result = await request('GET', '/ops/admin/places?limit=20');
- state.placeItems = result.data || [];
- renderPlaces(state.placeItems);
- renderMapPlaceSelect();
- if (Array.isArray(state.placeItems) && state.placeItems[0] && !($('managed-place-id').value)) {
- $('managed-place-id').value = state.placeItems[0].id;
- }
- persist();
- return result;
- }
- async function loadMapAssets() {
- const result = await request('GET', '/ops/admin/map-assets?limit=30');
- state.mapItems = result.data || [];
- renderMapLibrary(state.mapItems);
- if (Array.isArray(state.mapItems) && state.mapItems[0] && !($('managed-map-id').value)) {
- $('managed-map-id').value = state.mapItems[0].id;
- }
- persist();
- return result;
- }
- async function loadPlaceDetail(placeID) {
- const id = placeID || $('place-manage-id').value || $('managed-place-id').value;
- if (!id) throw new Error('请先提供管理地点 ID');
- $('place-manage-id').value = id;
- $('managed-place-id').value = id;
- const result = await request('GET', '/ops/admin/places/' + encodeURIComponent(id));
- applyPlaceDetail(result.data || {});
- renderPlaces(state.placeItems);
- renderMapAssets(state.placeMapItems);
- persist();
- return result;
- }
- async function loadMapAssetDetail(mapID) {
- const id = mapID || $('managed-map-id').value;
- if (!id) throw new Error('请先提供管理地图 ID');
- $('managed-map-id').value = id;
- setMapEditorMode('none');
- const [mapResult, experienceResult] = await Promise.all([
- request('GET', '/ops/admin/map-assets/' + encodeURIComponent(id)),
- request('GET', '/experience-maps/' + encodeURIComponent(id))
- ]);
- applyMapAssetDetail(mapResult.data || {});
- applyMapExperienceDetail(experienceResult.data || {});
- renderMapLibrary(state.mapItems);
- renderMapAssets(state.placeMapItems);
- $('map-detail-modal').hidden = false;
- persist();
- return { map: mapResult.data || {}, experience: experienceResult.data || {} };
- }
- $('btn-open-create-map').onclick = () => {
- $('map-detail-modal').hidden = true;
- $('managed-map-id').value = '';
- $('map-code').value = 'map-' + Date.now();
- if ($('map-place-select').value) {
- $('managed-place-id').value = $('map-place-select').value;
- }
- setMapEditorMode('map');
- persist();
- setStatus('已打开:添加地图', false);
- };
- $('btn-open-create-place').onclick = () => {
- $('map-detail-modal').hidden = true;
- $('place-manage-id').value = '';
- $('managed-place-id').value = '';
- setMapEditorMode('place');
- persist();
- setStatus('已打开:添加地点', false);
- };
- $('btn-close-map-editor').onclick = () => {
- setMapEditorMode('none');
- setStatus('已收起:地图编辑区', false);
- };
- $('btn-close-place-editor').onclick = () => {
- setMapEditorMode('none');
- setStatus('已收起:地点编辑区', false);
- };
- $('btn-close-map-detail').onclick = () => {
- $('map-detail-modal').hidden = true;
- setStatus('已关闭:地图详情', false);
- };
- $('btn-open-map-editor-from-detail').onclick = () => {
- $('map-detail-modal').hidden = true;
- setMapEditorMode('map');
- setStatus('已打开:地图编辑', false);
- };
- $('btn-open-map-tile-from-detail').onclick = () => {
- $('map-detail-modal').hidden = true;
- setMapEditorMode('map');
- setStatus('已打开:瓦片版本导入', false);
- };
- $('btn-create-place').onclick = () => run('create-place', async () => {
- const result = await request('POST', '/ops/admin/places', {
- code: $('place-code').value,
- name: $('place-name').value,
- region: $('place-region').value || undefined,
- status: 'active'
- });
- if (result && result.data && result.data.id) {
- $('managed-place-id').value = result.data.id;
- $('place-manage-id').value = result.data.id;
- }
- await loadPlaces();
- await refreshOverview();
- setMapEditorMode('none');
- persist();
- return result;
- });
- $('btn-get-place').onclick = () => run('get-place', () => loadPlaceDetail());
- $('btn-create-map-asset').onclick = () => run('create-map-asset', async () => {
- const placeId = $('map-place-select').value || $('managed-place-id').value;
- if (!placeId) throw new Error('请先选择地点');
- $('managed-place-id').value = placeId;
- const result = await request('POST', '/ops/admin/places/' + encodeURIComponent(placeId) + '/map-assets', {
- code: $('map-code').value,
- name: $('map-name').value,
- mapType: $('map-type').value,
- coverUrl: $('map-cover-url').value || undefined,
- description: $('map-summary').value || undefined,
- status: 'active'
- });
- if (result && result.data && result.data.id) {
- $('managed-map-id').value = result.data.id;
- }
- await loadMapAssets();
- if ($('managed-map-id').value) {
- await loadMapAssetDetail($('managed-map-id').value);
- }
- setMapEditorMode('none');
- persist();
- return result;
- });
- $('btn-update-map-asset').onclick = () => run('update-map-asset', async () => {
- if (!$('managed-map-id').value) throw new Error('请先读取地图详情,拿到管理地图 ID');
- const result = await request('PUT', '/ops/admin/map-assets/' + encodeURIComponent($('managed-map-id').value), {
- code: $('map-code').value,
- name: $('map-name').value,
- mapType: $('map-type').value,
- coverUrl: $('map-cover-url').value || undefined,
- description: $('map-summary').value || undefined,
- status: 'active'
- });
- await refreshOverview();
- await loadMapAssetDetail($('managed-map-id').value);
- setMapEditorMode('none');
- return result;
- });
- $('btn-get-map-asset').onclick = () => run('get-map-asset', () => loadMapAssetDetail());
- $('btn-refresh-map-area').onclick = () => run('refresh-map-area', async () => {
- const [regionsResult, placesResult, mapsResult] = await Promise.all([loadRegionOptions(), loadPlaces(), loadMapAssets()]);
- $('map-detail-modal').hidden = true;
- if ($('place-manage-id').value || $('managed-place-id').value) {
- await loadPlaceDetail($('managed-place-id').value);
- }
- if ($('managed-map-id').value) {
- await loadMapAssetDetail($('managed-map-id').value);
- }
- return {
- regions: ((regionsResult && regionsResult.data) || []).length,
- places: ((placesResult && placesResult.data) || []).length,
- maps: ((mapsResult && mapsResult.data) || []).length,
- };
- });
- $('btn-send-ops-code').onclick = () => run('ops-send-sms-code', async () => {
- return await request('POST', '/ops/auth/sms/send', {
- countryCode: $('ops-country-code').value,
- mobile: $('ops-mobile').value,
- deviceKey: $('ops-device-key').value,
- scene: 'ops_login'
- });
- });
- $('btn-register-ops').onclick = () => run('ops-register', async () => {
- const result = await request('POST', '/ops/auth/register', {
- countryCode: $('ops-country-code').value,
- mobile: $('ops-mobile').value,
- code: $('ops-code').value,
- deviceKey: $('ops-device-key').value,
- displayName: $('ops-display-name').value
- });
- state.accessToken = result.data.tokens.accessToken;
- $('ops-role-code').value = result.data.user.roleCode || '';
- syncToken();
- return result;
- });
- $('btn-open-events-view').onclick = () => {
- setActiveView('events');
- persist();
- setStatus('已切到:活动管理', false);
- };
- $('btn-open-compose-view').onclick = () => {
- setActiveView('compose');
- persist();
- setStatus('已切到:活动编排', false);
- };
- $('btn-open-publish-view').onclick = () => {
- setActiveView('publish');
- persist();
- setStatus('已切到:发布中心', false);
- };
- $('btn-login-ops').onclick = () => run('ops-login-sms', async () => {
- const result = await request('POST', '/ops/auth/login/sms', {
- countryCode: $('ops-country-code').value,
- mobile: $('ops-mobile').value,
- code: $('ops-code').value,
- deviceKey: $('ops-device-key').value
- });
- state.accessToken = result.data.tokens.accessToken;
- $('ops-role-code').value = result.data.user.roleCode || '';
- syncToken();
- return result;
- });
- $('btn-clear-token').onclick = () => {
- state.accessToken = '';
- syncToken();
- persist();
- setStatus('已清空 token', false);
- };
- $('btn-refresh-overview').onclick = () => run('refresh-overview', refreshOverview);
- $('btn-upload-asset').onclick = () => run('upload-asset', async () => {
- const file = $('asset-file').files[0];
- if (!file) throw new Error('请选择文件');
- const form = new FormData();
- form.append('assetType', $('asset-type').value);
- form.append('assetCode', $('asset-code').value);
- form.append('version', $('asset-version').value);
- form.append('title', $('asset-title').value);
- form.append('objectDir', $('asset-object-dir').value);
- form.append('status', $('asset-status').value);
- form.append('metadataJson', $('asset-metadata').value || '{}');
- form.append('file', file);
- const result = await request('POST', '/ops/admin/assets/upload', form, false);
- await refreshOverview();
- return result;
- });
- $('btn-register-link').onclick = () => run('register-link', async () => {
- const result = await request('POST', '/ops/admin/assets/register-link', {
- assetType: $('asset-type').value,
- assetCode: $('asset-code').value,
- version: $('asset-version').value,
- title: $('asset-title').value,
- publicUrl: $('asset-public-url').value,
- contentType: $('asset-content-type').value || undefined,
- status: $('asset-status').value,
- metadata: JSON.parse($('asset-metadata').value || '{}')
- });
- await refreshOverview();
- return result;
- });
- $('btn-list-assets').onclick = () => run('list-assets', async () => {
- const result = await request('GET', '/ops/admin/assets');
- renderAssets(result.data || []);
- return result;
- });
- $('btn-import-tile').onclick = () => run('import-tile-release', async () => {
- const result = await request('POST', '/ops/admin/ops/tile-releases/import', {
- placeCode: $('place-code').value,
- placeName: $('place-name').value,
- mapAssetCode: $('map-code').value,
- mapAssetName: $('map-name').value,
- mapType: $('map-type').value,
- versionCode: $('tile-version').value,
- status: 'active',
- tileBaseUrl: $('tile-base-url').value,
- metaUrl: $('tile-meta-url').value,
- setAsCurrent: true
- });
- if (result && result.data) {
- if (result.data.place && result.data.place.id) $('managed-place-id').value = result.data.place.id;
- if (result.data.mapAsset && result.data.mapAsset.id) $('managed-map-id').value = result.data.mapAsset.id;
- applyMapAssetDetail({
- mapAsset: result.data.mapAsset,
- tileReleases: result.data.tileRelease ? [result.data.tileRelease] : []
- });
- }
- $('map-detail-modal').hidden = false;
- setMapEditorMode('none');
- persist();
- return result;
- });
- $('btn-import-kml-batch').onclick = () => run('import-kml-batch', async () => {
- const result = await request('POST', '/ops/admin/ops/course-sets/import-kml-batch', {
- placeCode: $('place-code').value,
- placeName: $('place-name').value,
- mapAssetCode: $('map-code').value,
- mapAssetName: $('map-name').value,
- mapType: $('map-type').value,
- courseSetCode: $('course-set-code').value,
- courseSetName: $('course-set-name').value,
- mode: $('course-mode').value,
- status: 'active',
- defaultRouteCode: $('default-route-code').value,
- routes: JSON.parse($('routes-json').value || '[]')
- });
- applyCourseImportDetail(result.data || {});
- return result;
- });
- $('btn-list-events').onclick = () => run('list-events', async () => {
- const result = await request('GET', '/ops/admin/events?limit=30');
- renderEvents(result.data || []);
- renderEventListMain(result.data || []);
- if (Array.isArray(result.data) && result.data[0] && result.data[0].id) {
- $('binding-event-id').value = result.data[0].id;
- $('pipeline-event-id').value = result.data[0].id;
- }
- persist();
- return result;
- });
- $('btn-create-event').onclick = () => run('create-event', async () => {
- const result = await request('POST', '/ops/admin/events', {
- tenantCode: $('event-tenant-code').value || null,
- slug: $('event-slug').value,
- displayName: $('event-display-name').value,
- summary: $('event-summary').value || null,
- status: $('event-status').value
- });
- if (result && result.data) {
- applyEventDetail({ event: result.data });
- }
- persist();
- await refreshOverview();
- return result;
- });
- $('btn-update-event').onclick = () => run('update-event', async () => {
- const eventId = $('binding-event-id').value.trim();
- if (!eventId) throw new Error('请先填写活动 ID');
- const result = await request('PUT', '/ops/admin/events/' + encodeURIComponent(eventId), {
- tenantCode: $('event-tenant-code').value || null,
- slug: $('event-slug').value,
- displayName: $('event-display-name').value,
- summary: $('event-summary').value || null,
- status: $('event-status').value
- });
- if (result && result.data) {
- applyEventDetail({ event: result.data });
- }
- persist();
- await refreshOverview();
- return result;
- });
- $('btn-get-event').onclick = () => run('get-event', async () => {
- const result = await request('GET', '/ops/admin/events/' + encodeURIComponent($('binding-event-id').value));
- applyEventDetail(result.data || {});
- return result;
- });
- $('btn-import-presentation').onclick = () => run('import-presentation', async () => {
- const result = await request('POST', '/ops/admin/events/' + encodeURIComponent($('binding-event-id').value) + '/presentations/import', {
- title: $('presentation-title').value,
- templateKey: $('presentation-template-key').value,
- sourceType: 'schema',
- schemaUrl: $('presentation-schema-url').value,
- version: $('presentation-version').value,
- status: 'active',
- isDefault: true
- });
- if (result && result.data && result.data.id) $('presentation-id').value = result.data.id;
- persist();
- return result;
- });
- $('btn-import-bundle').onclick = () => run('import-content-bundle', async () => {
- const manifestUrl = $('bundle-manifest-url').value;
- const result = await request('POST', '/ops/admin/events/' + encodeURIComponent($('binding-event-id').value) + '/content-bundles/import', {
- title: $('bundle-title').value,
- bundleType: $('bundle-type').value,
- sourceType: 'manifest',
- manifestUrl: manifestUrl,
- version: $('bundle-version').value,
- status: 'active',
- isDefault: true,
- assetManifest: { manifestUrl }
- });
- if (result && result.data && result.data.id) $('content-bundle-id').value = result.data.id;
- persist();
- return result;
- });
- $('btn-save-defaults').onclick = () => run('save-event-defaults', async () => {
- return await request('POST', '/ops/admin/events/' + encodeURIComponent($('binding-event-id').value) + '/defaults', {
- presentationId: $('presentation-id').value || undefined,
- contentBundleId: $('content-bundle-id').value || undefined,
- runtimeBindingId: $('runtime-binding-id').value || undefined
- });
- });
- $('btn-get-pipeline').onclick = () => run('get-pipeline', async () => {
- const result = await request('GET', '/ops/admin/events/' + encodeURIComponent($('pipeline-event-id').value) + '/pipeline');
- applyPipelineSummary(result.data || {});
- return result;
- });
- $('btn-build-source').onclick = () => run('build-source', async () => {
- if (!$('source-id').value) throw new Error('请先提供配置源 ID');
- const result = await request('POST', '/ops/admin/sources/' + encodeURIComponent($('source-id').value) + '/build');
- if (result && result.data && result.data.id) $('build-id').value = result.data.id;
- persist();
- return result;
- });
- $('btn-publish-build').onclick = () => run('publish-build', async () => {
- if (!$('build-id').value) throw new Error('请先提供构建 ID');
- const result = await request('POST', '/ops/admin/builds/' + encodeURIComponent($('build-id').value) + '/publish', {
- runtimeBindingId: $('runtime-binding-id').value || undefined,
- presentationId: $('presentation-id').value || undefined,
- contentBundleId: $('content-bundle-id').value || undefined
- });
- if (result && result.data && result.data.id) {
- $('release-id').value = result.data.id;
- $('rollback-release-id').value = result.data.id;
- }
- persist();
- return result;
- });
- $('btn-get-release').onclick = () => run('get-release', async () => {
- if (!$('release-id').value) throw new Error('请先提供发布版本 ID');
- return await request('GET', '/ops/admin/releases/' + encodeURIComponent($('release-id').value));
- });
- $('btn-rollback-release').onclick = () => run('rollback-release', async () => {
- if (!$('rollback-release-id').value) throw new Error('请先提供待回滚的发布版本 ID');
- return await request('POST', '/ops/admin/events/' + encodeURIComponent($('pipeline-event-id').value) + '/rollback', {
- releaseId: $('rollback-release-id').value
- });
- });
- document.querySelectorAll('input, textarea, select').forEach(node => {
- node.addEventListener('change', persist);
- node.addEventListener('input', persist);
- });
- ['place-search', 'map-search'].forEach(id => {
- $(id).addEventListener('input', () => {
- renderPlaces(state.placeItems);
- renderMapLibrary(state.mapItems);
- renderMapAssets(state.placeMapItems);
- });
- });
- $('place-province').addEventListener('change', () => {
- renderCityOptions($('place-province').value);
- });
- $('place-city').addEventListener('change', syncPlaceRegion);
- $('map-place-select').addEventListener('change', () => {
- $('managed-place-id').value = $('map-place-select').value || '';
- persist();
- });
- $('place-list').addEventListener('click', event => {
- const item = event.target.closest('[data-place-id]');
- if (!item) return;
- void run('get-place', () => loadPlaceDetail(item.dataset.placeId));
- });
- ['map-list', 'map-library-list'].forEach(id => {
- $(id).addEventListener('click', event => {
- const item = event.target.closest('[data-map-id]');
- if (!item) return;
- void run('get-map-asset', () => loadMapAssetDetail(item.dataset.mapId));
- });
- });
- ['map-detail-modal', 'map-editor-panel', 'place-editor-panel'].forEach(id => {
- $(id).addEventListener('click', event => {
- if (event.target !== $(id)) return;
- if (id === 'map-detail-modal') {
- $('map-detail-modal').hidden = true;
- return;
- }
- setMapEditorMode('none');
- });
- });
- document.querySelectorAll('.nav-link[data-view]').forEach(button => {
- button.addEventListener('click', () => {
- setActiveView(button.dataset.view);
- persist();
- if (button.dataset.view === 'overview') {
- void refreshOverviewAndLog('refresh-overview');
- } else if (button.dataset.view === 'maps') {
- void run('refresh-map-area', async () => {
- const [regionsResult, placesResult, mapsResult] = await Promise.all([loadRegionOptions(), loadPlaces(), loadMapAssets()]);
- return {
- regions: ((regionsResult && regionsResult.data) || []).length,
- places: ((placesResult && placesResult.data) || []).length,
- maps: ((mapsResult && mapsResult.data) || []).length,
- };
- });
- }
- });
- });
- restore();
- setActiveView(state.activeView);
- setMapEditorMode('none');
- renderPlaces([]);
- renderMapAssets([]);
- renderMapLibrary([]);
- renderCourseSets([]);
- renderAssets([]);
- renderEvents([]);
- renderEventListMain([]);
- writeLog('ops-workbench-ready', {
- ok: true,
- hint: '开发环境默认免登录。建议顺序:先看资源总览 -> 地图/地点管理 -> 路线资源管理 -> 活动管理 / 活动编排 -> 发布中心。'
- });
- void refreshOverviewAndLog('refresh-overview');
- void loadRegionOptions();
- if (state.activeView === 'maps') {
- void run('refresh-map-area', async () => {
- const [regionsResult, placesResult, mapsResult] = await Promise.all([loadRegionOptions(), loadPlaces(), loadMapAssets()]);
- return {
- regions: ((regionsResult && regionsResult.data) || []).length,
- places: ((placesResult && placesResult.data) || []).length,
- maps: ((mapsResult && mapsResult.data) || []).length,
- };
- });
- }
- </script>
- </body>
- </html>`
|