如何修改PDF格式的设计图纸上面的内容|PDF编辑 | 万兴PDF
html <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>赛程前瞻编辑器 — PDF 体育赛程</title> <link rel="preconnect" href="https://fonts.googleapis.com"> <link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;0,600;0,700;1,400&family=Noto+Sans+SC:wght@300;400;500;700;900&family=DM+Mono:wght@400;500&family=Bebas+Neue&display=swap" rel="stylesheet"> <style> :root { --bg: #0a0b0f; --surface: #12131a; --surface-2: #1a1b25; --surface-3: #22232f; --accent: #d4a843; --accent-dim: #b8922e; --accent-glow: rgba(212, 168, 67, 0.15); --danger: #e04545; --success: #3dd68c; --info: #4a9eff; --text-primary: #f0ece4; --text-secondary: #a8a4a0; --text-muted: #5c5955; --border: rgba(255,255,255,0.06); --radius: 6px; --radius-lg: 12px; --shadow: 0 4px 24px rgba(0,0,0,0.4); --transition: 0.25s cubic-bezier(0.4, 0, 0.2, 1); } * { margin: 0; padding: 0; box-sizing: border-box; } html { font-size: 15px; } body { font-family: 'Noto Sans SC', sans-serif; background: var(--bg); color: var(--text-primary); min-height: 100vh; overflow-x: hidden; } body::before { content: ''; position: fixed; inset: 0; background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='1'/%3E%3C/svg%3E"); opacity: 0.025; pointer-events: none; z-index: 0; } ::-webkit-scrollbar { width: 6px; } ::-webkit-scrollbar-track { background: transparent; } ::-webkit-scrollbar-thumb { background: var(--text-muted); border-radius: 3px; } /* ========== TOP BAR ========== */ .topbar { position: sticky; top: 0; z-index: 100; background: rgba(10, 11, 15, 0.85); backdrop-filter: blur(20px); border-bottom: 1px solid var(--border); padding: 0 2rem; height: 56px; display: flex; align-items: center; justify-content: space-between; } .topbar-brand { display: flex; align-items: center; gap: 12px; } .topbar-logo { width: 32px; height: 32px; background: var(--accent); border-radius: 4px; display: flex; align-items: center; justify-content: center; font-family: 'Bebas Neue', sans-serif; font-size: 18px; color: var(--bg); letter-spacing: 1px; } .topbar-title { font-family: 'Noto Sans SC', sans-serif; font-weight: 700; font-size: 0.95rem; letter-spacing: 0.5px; } .topbar-title span { color: var(--accent); } .topbar-actions { display: flex; gap: 8px; align-items: center; } .btn { font-family: 'Noto Sans SC', sans-serif; font-size: 0.8rem; font-weight: 500; padding: 7px 16px; border: none; border-radius: var(--radius); cursor: pointer; transition: all var(--transition); display: inline-flex; align-items: center; gap: 6px; letter-spacing: 0.3px; } .btn-ghost { background: transparent; color: var(--text-secondary); border: 1px solid var(--border); } .btn-ghost:hover { background: var(--surface-2); color: var(--text-primary); } .btn-accent { background: var(--accent); color: var(--bg); font-weight: 700; } .btn-accent:hover { background: var(--accent-dim); transform: translateY(-1px); box-shadow: 0 4px 16px rgba(212,168,67,0.3); } .btn-danger { background: rgba(224,69,69,0.12); color: var(--danger); border: 1px solid rgba(224,69,69,0.2); } .btn-danger:hover { background: rgba(224,69,69,0.2); } /* ========== MAIN LAYOUT ========== */ .app { display: grid; grid-template-columns: 340px 1fr; min-height: calc(100vh - 56px); position: relative; z-index: 1; } /* ========== SIDEBAR ========== */ .sidebar { background: var(--surface); border-right: 1px solid var(--border); padding: 1.5rem; overflow-y: auto; max-height: calc(100vh - 56px); position: sticky; top: 56px; } .section-label { font-family: 'DM Mono', monospace; font-size: 0.65rem; text-transform: uppercase; letter-spacing: 2px; color: var(--text-muted); margin-bottom: 10px; display: flex; align-items: center; gap: 8px; } .section-label::after { content: ''; flex: 1; height: 1px; background: var(--border); } .form-group { margin-bottom: 16px; } .form-group label { display: block; font-size: 0.78rem; font-weight: 500; color: var(--text-secondary); margin-bottom: 5px; } .form-input, .form-textarea, .form-select { width: 100%; background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); padding: 9px 12px; color: var(--text-primary); font-family: 'Noto Sans SC', sans-serif; font-size: 0.82rem; transition: all var(--transition); outline: none; } .form-input:focus, .form-textarea:focus, .form-select:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-glow); } .form-textarea { resize: vertical; min-height: 80px; line-height: 1.6; } .form-select { cursor: pointer; } .form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; } .sidebar-divider { height: 1px; background: var(--border); margin: 20px 0; } .tag-group { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 6px; } .tag { font-size: 0.7rem; padding: 4px 10px; border-radius: 20px; cursor: pointer; border: 1px solid var(--border); background: transparent; color: var(--text-secondary); transition: all var(--transition); font-family: 'Noto Sans SC', sans-serif; } .tag:hover, .tag.active { border-color: var(--accent); color: var(--accent); background: var(--accent-glow); } .match-entry { background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); padding: 12px; margin-bottom: 10px; transition: all var(--transition); cursor: default; animation: fadeSlide 0.3s ease forwards; } .match-entry:hover { border-color: rgba(255,255,255,0.1); } .match-entry-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; } .match-entry-num { font-family: 'Bebas Neue', sans-serif; font-size: 1rem; color: var(--accent); letter-spacing: 1px; } .match-entry-remove { width: 22px; height: 22px; background: rgba(224,69,69,0.1); border: none; border-radius: 50%; color: var(--danger); cursor: pointer; font-size: 12px; display: flex; align-items: center; justify-content: center; transition: all var(--transition); } .match-entry-remove:hover { background: rgba(224,69,69,0.25); } .match-entry .form-row { gap: 6px; } .match-entry .form-input { padding: 6px 8px; font-size: 0.78rem; } .match-entry .mini-label { font-size: 0.65rem; color: var(--text-muted); margin-bottom: 3px; font-family: 'DM Mono', monospace; text-transform: uppercase; letter-spacing: 1px; } .add-match-btn { width: 100%; padding: 10px; border: 1px dashed var(--border); border-radius: var(--radius); background: transparent; color: var(--text-muted); font-size: 0.78rem; cursor: pointer; transition: all var(--transition); font-family: 'Noto Sans SC', sans-serif; } .add-match-btn:hover { border-color: var(--accent); color: var(--accent); background: var(--accent-glow); } /* ========== PREVIEW AREA ========== */ .preview-area { padding: 2.5rem 3rem; overflow-y: auto; max-height: calc(100vh - 56px); } /* PDF PREVIEW CARD */ .pdf-card { max-width: 860px; margin: 0 auto; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; box-shadow: var(--shadow); animation: fadeUp 0.5s ease forwards; } /* HERO SECTION */ .pdf-hero { position: relative; padding: 3rem 3rem 2.5rem; background: linear-gradient(135deg, #0f1019 0%, #1a1b28 50%, #0f1019 100%); overflow: hidden; } .pdf-hero::before { content: ''; position: absolute; top: -50%; right: -20%; width: 500px; height: 500px; background: radial-gradient(circle, var(--accent-glow) 0%, transparent 70%); pointer-events: none; } .pdf-hero::after { content: ''; position: absolute; bottom: 0; left: 0; right: 0; height: 1px; background: linear-gradient(90deg, transparent, var(--accent-dim), transparent); } .pdf-meta-bar { display: flex; gap: 20px; align-items: center; margin-bottom: 1.2rem; flex-wrap: wrap; } .pdf-meta-chip { font-family: 'DM Mono', monospace; font-size: 0.68rem; letter-spacing: 1.5px; text-transform: uppercase; color: var(--accent); background: var(--accent-glow); padding: 4px 12px; border-radius: 3px; border: 1px solid rgba(212,168,67,0.2); } .pdf-meta-text { font-family: 'DM Mono', monospace; font-size: 0.68rem; letter-spacing: 1px; color: var(--text-muted); text-transform: uppercase; } .pdf-hero-title { font-family: 'Cormorant Garamond', serif; font-size: 3rem; font-weight: 700; line-height: 1.15; letter-spacing: -0.5px; margin-bottom: 0.8rem; position: relative; } .pdf-hero-subtitle { font-size: 1.05rem; font-weight: 300; color: var(--text-secondary); line-height: 1.7; max-width: 600px; position: relative; } .pdf-hero-subtitle em { color: var(--accent); font-style: italic; font-weight: 400; } /* DRAMA TICKER */ .drama-ticker { display: flex; gap: 12px; margin-top: 1.5rem; flex-wrap: wrap; position: relative; } .drama-item { display: flex; align-items: center; gap: 6px; font-size: 0.72rem; font-weight: 500; padding: 5px 12px; border-radius: 3px; border: 1px solid; } .drama-item.hot { color: var(--danger); border-color: rgba(224,69,69,0.3); background: rgba(224,69,69,0.08); } .drama-item.gold { color: var(--accent); border-color: rgba(212,168,67,0.3); background: rgba(212,168,67,0.08); } .drama-item.blue { color: var(--info); border-color: rgba(74,158,255,0.3); background: rgba(74,158,255,0.08); } .drama-dot { width: 6px; height: 6px; border-radius: 50%; animation: pulse 2s ease infinite; } .drama-item.hot .drama-dot { background: var(--danger); } .drama-item.gold .drama-dot { background: var(--accent); } .drama-item.blue .drama-dot { background: var(--info); } /* MATCH CARDS */ .matches-section { padding: 2rem 3rem; } .matches-grid { display: flex; flex-direction: column; gap: 14px; } .match-card { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); padding: 20px 24px; transition: all var(--transition); opacity: 0; animation: fadeUp 0.4s ease forwards; } .match-card:nth-child(1) { animation-delay: 0.1s; } .match-card:nth-child(2) { animation-delay: 0.15s; } .match-card:nth-child(3) { animation-delay: 0.2s; } .match-card:nth-child(4) { animation-delay: 0.25s; } .match-card:nth-child(5) { animation-delay: 0.3s; } .match-card:nth-child(6) { animation-delay: 0.35s; } .match-card:nth-child(7) { animation-delay: 0.4s; } .match-card:nth-child(8) { animation-delay: 0.45s; } .match-card:nth-child(9) { animation-delay: 0.5s; } .match-card:nth-child(10) { animation-delay: 0.55s; } .match-card:hover { border-color: rgba(255,255,255,0.1); transform: translateY(-1px); box-shadow: 0 6px 20px rgba(0,0,0,0.3); } .match-card.featured { border-color: rgba(212,168,67,0.3); background: linear-gradient(135deg, rgba(212,168,67,0.04) 0%, var(--surface-2) 100%); } .match-card.featured::before { content: '★ 焦点战'; position: absolute; top: -1px; right: 16px; font-family: 'DM Mono', monospace; font-size: 0.6rem; letter-spacing: 2px; text-transform: uppercase; color: var(--bg); background: var(--accent); padding: 2px 10px; border-radius: 0 0 4px 4px; } .match-card { position: relative; } .team-side { display: flex; align-items: center; gap: 12px; } .team-side.right { flex-direction: row-reverse; text-align: right; } .team-badge { width: 40px; height: 40px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-family: 'Bebas Neue', sans-serif; font-size: 0.85rem; letter-spacing: 1px; flex-shrink: 0; border: 2px solid rgba(255,255,255,0.08); } .team-info {} .team-name { font-weight: 700; font-size: 0.92rem; margin-bottom: 2px; } .team-stat { font-family: 'DM Mono', monospace; font-size: 0.65rem; color: var(--text-muted); letter-spacing: 0.5px; } .match-center { text-align: center; padding: 0 20px; min-width: 100px; } .match-time { font-family: 'Bebas Neue', sans-serif; font-size: 1.3rem; color: var(--text-primary); letter-spacing: 2px; } .match-date { font-family: 'DM Mono', monospace; font-size: 0.62rem; color: var(--text-muted); letter-spacing: 1px; margin-top: 2px; } .match-vs { font-family: 'Cormorant Garamond', serif; font-size: 0.75rem; color: var(--text-muted); font-style: italic; margin-top: 2px; } .match-tag { font-family: 'DM Mono', monospace; font-size: 0.58rem; letter-spacing: 1px; text-transform: uppercase; padding: 2px 8px; border-radius: 3px; margin-top: 6px; display: inline-block; } .match-tag.derby { color: var(--danger); background: rgba(224,69,69,0.1); } .match-tag.relegation { color: #ff9f43; background: rgba(255,159,67,0.1); } .match-tag.title { color: var(--accent); background: var(--accent-glow); } .match-tag.normal { color: var(--text-muted); background: rgba(255,255,255,0.04); } /* NARRATIVE SECTION */ .narrative-section { padding: 0 3rem 2.5rem; } .narrative-divider { display: flex; align-items: center; gap: 16px; margin-bottom: 1.5rem; } .narrative-divider-line { flex: 1; height: 1px; background: var(--border); } .narrative-divider-text { font-family: 'DM Mono', monospace; font-size: 0.65rem; letter-spacing: 3px; text-transform: uppercase; color: var(--text-muted); } .narrative-box { background: linear-gradient(135deg, rgba(212,168,67,0.03) 0%, rgba(212,168,67,0.01) 100%); border: 1px solid rgba(212,168,67,0.1); border-radius: var(--radius); padding: 24px 28px; position: relative; } .narrative-box::before { content: '"'; font-family: 'Cormorant Garamond', serif; font-size: 4rem; color: var(--accent); opacity: 0.3; position: absolute; top: 8px; left: 16px; line-height: 1; } .narrative-text { font-size: 0.92rem; line-height: 1.85; color: var(--text-secondary); font-weight: 300; text-indent: 2em; } .narrative-text strong { color: var(--text-primary); font-weight: 500; } .narrative-text .hl { color: var(--accent); font-weight: 400; } .narrative-text .hl-red { color: var(--danger); font-weight: 400; } /* STATS STRIP */ .stats-strip { display: grid; grid-template-columns: repeat(4, 1fr); gap: 1px; background: var(--border); border-radius: var(--radius); overflow: hidden; margin: 0 3rem 2.5rem; } .stat-cell { background: var(--surface-2); padding: 18px 20px; text-align: center; } .stat-number { font-family: 'Bebas Neue', sans-serif; font-size: 2rem; color: var(--accent); letter-spacing: 2px; line-height: 1; } .stat-label { font-family: 'DM Mono', monospace; font-size: 0.6rem; color: var(--text-muted); letter-spacing: 1.5px; text-transform: uppercase; margin-top: 4px; } /* FOOTER */ .pdf-footer { padding: 1.5rem 3rem; border-top: 1px solid var(--border); display: flex; justify-content: space-between; align-items: center; } .footer-left { font-family: 'DM Mono', monospace; font-size: 0.62rem; color: var(--text-muted); letter-spacing: 1px; } .footer-right { display: flex; gap: 16px; } .footer-chip { font-family: 'DM Mono', monospace; font-size: 0.58rem; letter-spacing: 1px; text-transform: uppercase; color: var(--text-muted); padding: 3px 8px; border: 1px solid var(--border); border-radius: 3px; } /* ========== EMPTY STATE ========== */ .empty-state { text-align: center; padding: 80px 40px; } .empty-icon { width: 64px; height: 64px; margin: 0 auto 20px; border: 2px dashed var(--border); border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 1.5rem; color: var(--text-muted); } .empty-title { font-family: 'Cormorant Garamond', serif; font-size: 1.4rem; color: var(--text-secondary); margin-bottom: 8px; } .empty-desc { font-size: 0.82rem; color: var(--text-muted); } /* ========== ANIMATIONS ========== */ @keyframes fadeUp { from { opacity: 0; transform: translateY(16px); } to { opacity: 1; transform: translateY(0); } } @keyframes fadeSlide { from { opacity: 0; transform: translateX(-8px); } to { opacity: 1; transform: translateX(0); } } @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } } /* ========== PRINT / PDF ========== */ @media print { body { background: #fff; color: #111; } body::before { display: none; } .topbar, .sidebar { display: none !important; } .app { display: block; } .preview-area { padding: 0; max-height: none; overflow: visible; } .pdf-card { box-shadow: none; border: none; background: #fff; max-width: none; } .pdf-hero { background: #f7f5f0 !important; } .pdf-hero::before { display: none; } .pdf-hero-title { color: #111; } .pdf-hero-subtitle { color: #444; } .pdf-hero-subtitle em { color: #8b6914; } .pdf-meta-chip { color: #8b6914; background: #f7f0d8; border-color: #e8d9a8; } .pdf-meta-text { color: #888; } .match-card { background: #fafafa; border-color: #e0e0e0; } .match-card.featured { background: #f7f5f0; border-color: #e8d9a8; } .match-card.featured::before { color: #fff; } .team-name { color: #111; } .team-stat { color: #888; } .match-time { color: #111; } .match-date, .match-vs { color: #888; } .drama-item.hot { color: #c0392b; border-color: #e8c4c0; background: #fdf0ef; } .drama-item.gold { color: #8b6914; border-color: #e8d9a8; background: #f7f5f0; } .drama-item.blue { color: #2980b9; border-color: #b8d4e8; background: #eef5fa; } .narrative-box { background: #f7f5f0; border-color: #e8d9a8; } .narrative-text { color: #333; } .narrative-text strong { color: #111; } .narrative-text .hl { color: #8b6914; } .narrative-text .hl-red { color: #c0392b; } .stat-cell { background: #fafafa; } .stat-number { color: #8b6914; } .stat-label { color: #888; } .stats-strip { background: #e0e0e0; } .pdf-footer { border-color: #e0e0e0; } .footer-left, .footer-chip { color: #888; border-color: #e0e0e0; } .narrative-divider-line { background: #e0e0e0; } .narrative-divider-text { color: #888; } } /* ========== TOAST ========== */ .toast { position: fixed; bottom: 24px; right: 24px; background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); padding: 12px 20px; font-size: 0.8rem; color: var(--text-primary); box-shadow: var(--shadow); z-index: 200; transform: translateY(80px); opacity: 0; transition: all 0.3s ease; } .toast.show { transform: translateY(0); opacity: 1; } /* responsive */ @media (max-width: 1024px) { .app { grid-template-columns: 1fr; } .sidebar { position: static; max-height: none; border-right: none; border-bottom: 1px solid var(--border); } .preview-area { max-height: none; } } </style> </head> <body> <!-- TOP BAR --> <header class="topbar"> <div class="topbar-brand"> <div class="topbar-logo">SP</div> <div class="topbar-title">赛程前瞻<span>编辑器</span></div> </div> <div class="topbar-actions"> <button class="btn btn-ghost" onclick="resetAll()">清空</button> <button class="btn btn-ghost" onclick="loadDemo()">加载示例</button> <button class="btn btn-accent" onclick="exportPDF()">导出 PDF</button> </div> </header> <!-- MAIN --> <div class="app"> <!-- SIDEBAR --> <aside class="sidebar" id="sidebar"> <div class="section-label">赛事信息</div> <div class="form-group"> <label>联赛 / 杯赛名称</label> <input class="form-input" id="leagueName" value="英超联赛 2025/26" oninput="renderPreview()"> </div> <div class="form-row"> <div class="form-group"> <label>轮次</label> <input class="form-input" id="roundNum" value="第 22 轮" oninput="renderPreview()"> </div> <div class="form-group"> <label>日期范围</label> <input class="form-input" id="dateRange" value="2026.01.17 — 01.19" oninput="renderPreview()"> </div> </div> <div class="sidebar-divider"></div> <div class="section-label">核心叙事</div> <div class="form-group"> <label>本轮主题(标题)</label> <input class="form-input" id="heroTitle" value="北方风暴来袭" oninput="renderPreview()"> </div> <div class="form-group"> <label>戏剧冲突类型</label> <div class="tag-group" id="dramaTags"> <button class="tag active" data-type="hot" onclick="toggleDramaTag(this)">保级生死战</button> <button class="tag active" data-type="gold" onclick="toggleDramaTag(this)">冠军争夺</button> <button class="tag" data-type="blue" onclick="toggleDramaTag(this)">纪录冲刺</button> <button class="tag active" data-type="hot" onclick="toggleDramaTag(this)">豪门德比</button> <button class="tag" data-type="gold" onclick="toggleDramaTag(this)">告别之夜</button> <button class="tag" data-type="blue" onclick="toggleDramaTag(this)">新星首秀</button> <button class="tag" data-type="hot" onclick="toggleDramaTag(this)">复仇之战</button> <button class="tag" data-type="gold" onclick="toggleDramaTag(this)">历史节点</button> </div> </div> <div class="form-group"> <label>叙事正文</label> <textarea class="form-textarea" id="narrativeText" oninput="renderPreview()" rows="6">当利物浦踏上安菲尔德的草皮,他们面对的不仅是来访的曼城——更是一段长达15年的宿命对决,瓜迪奥拉的球队在近5次做客默西赛德郡仅取1胜,而阿尔特塔治下的红军正在追逐自2020年以来的首个联赛冠军,在积分榜的另一端,诺丁汉森林与伯恩利的保级六分之战,将决定谁能继续留在这个价值数十亿英镑的顶级舞台。</textarea> </div> <div class="sidebar-divider"></div> <div class="section-label">比赛列表</div> <div id="matchesList"></div> <button class="add-match-btn" onclick="addMatch()">+ 添加比赛</button> </aside> <!-- PREVIEW --> <main class="preview-area" id="previewArea"> <div class="pdf-card" id="pdfCard"> <!-- rendered dynamically --> </div> </main> </div> <div class="toast" id="toast"></div> <script> // ====== DATA MODEL ====== let matches = []; let matchIdCounter = 0; const teamColors = [ '#c0392b','#2980b9','#27ae60','#f39c12','#8e44ad', '#1abc9c','#d35400','#2c3e50','#e74c3c','#3498db', '#16a085','#c0872a','#7f1d1d','#1e3a5f','#14532d' ]; const tagTypes = { '保级生死战': 'relegation', '冠军争夺': 'title', '纪录冲刺': 'normal', '豪门德比': 'derby', '告别之夜': 'title', '新星首秀': 'normal', '复仇之战': 'derby', '历史节点': 'title' }; // ====== INIT ====== function init() { loadDemo(); } function loadDemo() { document.getElementById('leagueName').value = '英超联赛 2025/26'; document.getElementById('roundNum').value = '第 22 轮'; document.getElementById('dateRange').value = '2026.01.17 — 01.19'; document.getElementById('heroTitle').value = '北方风暴来袭'; document.getElementById('narrativeText').value = '当利物浦踏上安菲尔德的草皮,他们面对的不仅是来访的曼城——更是一段长达15年的宿命对决,瓜迪奥拉的球队在近5次做客默西赛德郡仅取1胜,而阿尔特塔治下的红军正在追逐自2020年以来的首个联赛冠军,在积分榜的另一端,诺丁汉森林与伯恩利的保级六分之战,将决定谁还能继续留在这个价值数十亿英镑的顶级舞台。'; // reset drama tags const tags = document.querySelectorAll('#dramaTags .tag'); tags.forEach(t => { const txt = t.textContent; t.classList.toggle('active', ['保级生死战','冠军争夺','豪门德比'].includes(txt)); }); matches = []; matchIdCounter = 0; const demo = [ { home: '利物浦', away: '曼城', homeStat: '联赛第二 · 近5场4胜1平', awayStat: '联赛第四 · 近5场3胜1平1负', time: '17:30', date: '周六 01.17', tag: 'derby', featured: true }, { home: '阿森纳', away: '切尔西', homeStat: '联赛第一 · 近5场5胜', awayStat: '联赛第五 · 近5场2胜2平1负', time: '20:00', date: '周六 01.17', tag: 'derby', featured: false }, { home: '曼联', away: '热刺', homeStat: '联赛第七 · 近5场2胜1平2负', awayStat: '联赛第六 · 近5场3胜2负', time: '22:00', date: '周六 01.17', tag: 'title', featured: false }, { home: '诺丁汉森林', away: '伯恩利', homeStat: '联赛第十八 · 近5场1胜1平3负', awayStat: '联赛第十九 · 近5场0胜2平3负', time: '15:00', date: '周日 01.18', tag: 'relegation', featured: false }, { home: '纽卡斯尔', away: '布莱顿', homeStat: '联赛第三 · 近5场4胜1负', awayStat: '联赛第八 · 近5场2胜2平1负', time: '17:30', date: '周日 01.18', tag: 'normal', featured: false }, { home: '阿斯顿维拉', away: '西汉姆', homeStat: '联赛第九 · 近5场2胜2平1负', awayStat: '联赛第十二 · 近5场1胜1平3负', time: '20:00', date: '周日 01.18', tag: 'normal', featured: false }, { home: '富勒姆', away: '水晶宫', homeStat: '联赛第十 · 近5场2胜1平2负', awayStat: '联赛第十四 · 近5场1胜2平2负', time: '22:00', date: '周日 01.18', tag: 'normal', featured: false }, { home: '狼队', away: '伯恩茅斯', homeStat: '联赛第十一 · 近5场2胜3负', awayStat: '联赛第十三 · 近5场1胜2平2负', time: '15:00', date: '周一 01.19', tag: 'normal', featured: false }, { home: '布伦特福德', away: '卢顿', homeStat: '联赛第十五 · 近5场1胜2平2负', awayStat: '联赛第二十 · 近5场0胜1平4负', time: '17:30', date: '周一 01.19', tag: 'relegation', featured: false }, { home: '谢菲尔德联', away: '埃弗顿', homeStat: '联赛第十七 · 近5场1胜1平3负', awayStat: '联赛第十六 · 近5场1胜1平3负', time: '20:00', date: '周一 01.19', tag: 'relegation', featured: false }, ]; demo.forEach(m => { matches.push({ id: matchIdCounter++, ...m }); }); renderMatchesList(); renderPreview(); } // ====== MATCH MANAGEMENT ====== function addMatch() { matches.push({ id: matchIdCounter++, home: '主队', away: '客队', homeStat: '', awayStat: '', time: '20:00', date: '', tag: 'normal', featured: false }); renderMatchesList(); renderPreview(); } function removeMatch(id) { matches = matches.filter(m => m.id !== id); renderMatchesList(); renderPreview(); } function updateMatch(id, field, value) { const m = matches.find(m => m.id === id); if (m) m[field] = value; renderPreview(); } function renderMatchesList() { const container = document.getElementById('matchesList'); container.innerHTML = matches.map((m, i) => ` <div class="match-entry" style="animation-delay:${i*0.03}s"> <div class="match-entry-header"> <span class="match-entry-num">MATCH ${String(i+1).padStart(2,'0')}</span> <button class="match-entry-remove" onclick="removeMatch(${m.id})">✕</button> </div> <div class="form-row" style="margin-bottom:6px"> <div> <div class="mini-label">主队</div> <input class="form-input" value="${esc(m.home)}" oninput="updateMatch(${m.id},'home',this.value)"> </div> <div> <div class="mini-label">客队</div> <input class="form-input" value="${esc(m.away)}" oninput="updateMatch(${m.id},'away',this.value)"> </div> </div> <div class="form-row" style="margin-bottom:6px"> <div> <div class="mini-label">开球时间</div> <input class="form-input" value="${esc(m.time)}" oninput="updateMatch(${m.id},'time',this.value)"> </div> <div> <div class="mini-label">日期</div> <input class="form-input" value="${esc(m.date)}" oninput="updateMatch(${m.id},'date',this.value)"> </div> </div> <div class="form-row" style="margin-bottom:6px"> <div> <div class="mini-label">主队数据</div> <input class="form-input" value="${esc(m.homeStat)}" oninput="updateMatch(${m.id},'homeStat',this.value)"> </div> <div> <div class="mini-label">客队数据</div> <input class="form-input" value="${esc(m.awayStat)}" oninput="updateMatch(${m.id},'awayStat',this.value)"> </div> </div> <div class="form-row"> <div> <div class="mini-label">比赛标签</div> <select class="form-select" onchange="updateMatch(${m.id},'tag',this.value)"> <option value="normal" ${m.tag==='normal'?'selected':''}>普通</option> <option value="derby" ${m.tag==='derby'?'selected':''}>德比</option> <option value="title" ${m.tag==='title'?'selected':''}>争冠</option> <option value="relegation" ${m.tag==='relegation'?'selected':''}>保级</option> </select> </div> <div style="display:flex;align-items:flex-end"> <label style="display:flex;align-items:center;gap:6px;font-size:0.75rem;color:var(--text-secondary);cursor:pointer"> <input type="checkbox" ${m.featured?'checked':''} onchange="updateMatch(${m.id},'featured',this.checked)"> 设为焦点战 </label> </div> </div> </div> `).join(''); } // ====== DRAMA TAGS ====== function toggleDramaTag(el) { el.classList.toggle('active'); renderPreview(); } function getActiveDramas() { const tags = document.querySelectorAll('#dramaTags .tag.active'); return Array.from(tags).map(t => ({ text: t.textContent, type: t.dataset.type || 'gold' })); } // ====== RENDER PREVIEW ====== function renderPreview() { const league = val('leagueName'); const round = val('roundNum'); const dateRange = val('dateRange'); const heroTitle = val('heroTitle'); const narrative = val('narrativeText'); const dramas = getActiveDramas(); // stats const totalMatches = matches.length; const featuredCount = matches.filter(m => m.featured).length; const derbyCount = matches.filter(m => m.tag === 'derby').length; const relegationCount = matches.filter(m => m.tag === 'relegation').length; // format narrative const narrativeHtml = narrative .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>') .replace(/\*(.*?)\*/g, '<em>$1</em>') .replace(/\n/g, '<br>'); const html = ` <!-- HERO --> <div class="pdf-hero"> <div class="pdf-meta-bar"> <span class="pdf-meta-chip">${esc(league)}</span> <span class="pdf-meta-chip">${esc(round)}</span> <span class="pdf-meta-text">${esc(dateRange)}</span> </div> <h1 class="pdf-hero-title">${esc(heroTitle)}</h1> <p class="pdf-hero-subtitle">${narrativeHtml.length > 200 ? narrativeHtml.substring(0, 200) + '...' : narrativeHtml}</p> <div class="drama-ticker"> ${dramas.map(d => ` <div class="drama-item ${d.type}"> <span class="drama-dot"></span> ${esc(d.text)} </div> `).join('')} </div> </div> <!-- STATS STRIP --> <div class="stats-strip"> <div class="stat-cell"> <div class="stat-number">${totalMatches}</div> <div class="stat-label">总场次</div> </div> <div class="stat-cell"> <div class="stat-number">${featuredCount}</div> <div class="stat-label">焦点大战</div> </div> <div class="stat-cell"> <div class="stat-number">${derbyCount}</div> <div class="stat-label">德比 / 宿敌</div> </div> <div class="stat-cell"> <div class="stat-number">${relegationCount}</div> <div class="stat-label">保级生死战</div> </div> </div> <!-- MATCHES --> <div class="matches-section"> <div class="matches-grid"> ${matches.map(m => { const homeBadge = getBadge(m.home); const awayBadge = getBadge(m.away); const tagLabel = { derby:'德比', title:'争冠', relegation:'保级', normal:'' }[m.tag] || ''; return ` <div class="match-card ${m.featured ? 'featured' : ''}"> <div class="team-side"> <div class="team-badge" style="background:${homeBadge.color};color:#fff">${homeBadge.abbr}</div> <div class="team-info"> <div class="team-name">${esc(m.home)}</div> ${m.homeStat ? `<div class="team-stat">${esc(m.homeStat)}</div>` : ''} </div> </div> <div class="match-center"> <div class="match-time">${esc(m.time)}</div> <div class="match-date">${esc(m.date)}</div> <div class="match-vs">vs</div> ${tagLabel ? `<div class="match-tag ${m.tag}">${tagLabel}</div>` : ''} </div> <div class="team-side right"> <div class="team-badge" style="background:${awayBadge.color};color:#fff">${awayBadge.abbr}</div> <div class="team-info"> <div class="team-name">${esc(m.away)}</div> ${m.awayStat ? `<div class="team-stat">${esc(m.awayStat)}</div>` : ''} </div> </div> </div> `; }).join('')} </div> </div> ${narrative.length > 200 ? ` <!-- NARRATIVE --> <div class="narrative-section"> <div class="narrative-divider"> <div class="narrative-divider-line"></div> <div class="narrative-divider-text">赛程前瞻</div> <div class="narrative-divider-line"></div> </div> <div class="narrative-box"> <div class="narrative-text">${narrativeHtml}</div> </div> </div> ` : ''} <!-- FOOTER --> <div class="pdf-footer"> <div class="footer-left">赛程前瞻 · ${esc(league)} · ${esc(round)} · ${esc(dateRange)}</div> <div class="footer-right"> <span class="footer-chip">${totalMatches} MATCHES</span> <span class="footer-chip">${new Date().toLocaleDateString('zh-CN')}</span> </div> </div> `; document.getElementById('pdfCard').innerHTML = html; } // ====== HELPERS ====== function val(id) { return document.getElementById(id).value; } function esc(s) { const d = document.createElement('div'); d.textContent = s; return d.innerHTML; } function getBadge(name) { const shortNames = { '利物浦':'LIV','曼城':'MCI','阿森纳':'ARS','切尔西':'CHE','曼联':'MUN', '热刺':'TOT','纽卡斯尔':'NEW','布莱顿':'BHA','阿斯顿维拉':'AVL','西汉姆':'WHU', '富勒姆':'FUL','水晶宫':'CRY','狼队':'WOL','伯恩茅斯':'BOU','布伦特福德':'BRE', '卢顿':'LUT','诺丁汉森林':'NFO','伯恩利':'BUR','谢菲尔德联':'SHU','埃弗顿':'EVE' }; const abbr = shortNames[name] || name.substring(0, 3).toUpperCase(); // deterministic color from name let hash = 0; for (let i = 0; i < name.length; i++) hash = name.charCodeAt(i) + ((hash << 5) - hash); const color = teamColors[Math.abs(hash) % teamColors.length]; return { abbr, color }; } // ====== ACTIONS ====== function exportPDF() { showToast('正在生成 PDF,请在弹出的打印对话框中选择"另存为 PDF"...'); setTimeout(() => window.print(), 600); } function resetAll() { if (!confirm('确定要清空所有内容吗?')) return; document.getElementById('leagueName').value = ''; document.getElementById('roundNum').value = ''; document.getElementById('dateRange').value = ''; document.getElementById('heroTitle').value = ''; document.getElementById('narrativeText').value = ''; document.querySelectorAll('#dramaTags .tag').forEach(t => t.classList.remove('active')); matches = []; matchIdCounter = 0; renderMatchesList(); renderPreview(); } function showToast(msg) { const t = document.getElementById('toast'); t.textContent = msg; t.classList.add('show'); setTimeout(() => t.classList.remove('show'), 3500); } // ====== BOOT ====== init(); </script> </body> </html> 这是一个完整的 PDF 体育赛程前瞻编辑器,核心设计理念是让每一轮赛程都成为"戏剧冲突 + 专业数据 + 球迷共鸣"的结合体。 --- ### 核心功能 左侧编辑面板: - 赛事基础信息(联赛、轮次、日期) - 核心叙事区——标题 + 戏剧冲突标签(保级生死战 / 豪门德比 / 冠军争夺 / 纪录冲刺等,可多选) - 详细叙事正文,支持 **加粗** 和 *斜体* 标记 - 每场比赛独立卡片:主客队、开球时间、数据摘要、比赛类型标签、焦点战标记 - 可动态增删比赛 右侧实时预览: - 杂志级排版的 PDF 输出卡片 - 英雄区:联赛标识 + 轮次 + 标题 + 前几行叙事 + 脉冲闪烁的戏剧标签 - 数据条:总场次 / 焦点大战 / 德比数量 / 保级战数量,一目了然 - 比赛卡片:队徽色块 + 对阵信息 + 数据 + 德比/争冠/保级标签,焦点战高亮 - 深度叙事区:完整前瞻分析正文 - 页脚信息栏 导出: 点击「导出 PDF」调用浏览器打印,打印样式已专门优化(白底、深色文字、去除编辑界面) 设计亮点: - 深色编辑模式 / 打印时自动切换浅色模式 - Bebas Neue + Cormorant Garamond + DM Mono 三字体搭配 - 噪点纹理背景 + 渐变光晕 + 焦点战金色边框 - 入场动画级联延迟效果