Files
Axi_Omron/docs/axiomron-pcbcheck-report-draft.html

1623 lines
56 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#f4f7fb" />
<title>从初学者需求到完整项目计划 · AxiOmron PCBCheck</title>
<style>
:root {
--ink: #10233f;
--ink-soft: #40536d;
--muted: #6d7e92;
--canvas: #f4f7fb;
--surface: #ffffff;
--surface-soft: #edf4fa;
--line: #d9e3ee;
--primary: #0877bd;
--primary-dark: #075b91;
--primary-soft: #e4f3fb;
--teal: #0e8f83;
--teal-soft: #e2f5f1;
--orange: #d86b18;
--orange-soft: #fff0e3;
--danger: #c84343;
--danger-soft: #fdeaea;
--shadow: 0 18px 55px rgba(28, 58, 90, .10);
--font-sans: "Microsoft YaHei UI", "Microsoft YaHei", "Segoe UI", Arial, sans-serif;
--font-mono: "Cascadia Mono", Consolas, monospace;
}
* { box-sizing: border-box; }
html,
body {
width: 100%;
height: 100%;
margin: 0;
overflow: hidden;
background: var(--canvas);
}
body {
color: var(--ink);
font-family: var(--font-sans);
-webkit-font-smoothing: antialiased;
}
button { font: inherit; }
button:focus-visible {
outline: 3px solid rgba(8, 119, 189, .32);
outline-offset: 3px;
}
.progress-track {
position: fixed;
z-index: 50;
top: 0;
right: 0;
left: 0;
height: 5px;
background: #dfe8f1;
}
.progress-track span {
display: block;
width: 12.5%;
height: 100%;
background: linear-gradient(90deg, var(--primary), var(--teal));
transition: width .28s ease-out;
}
.brand-mark {
position: fixed;
z-index: 30;
top: 26px;
left: clamp(24px, 4vw, 64px);
display: flex;
align-items: center;
gap: 11px;
color: var(--ink-soft);
font-size: 12px;
font-weight: 700;
letter-spacing: .08em;
}
.brand-mark::before {
width: 30px;
height: 8px;
border-radius: 999px;
background: linear-gradient(90deg, var(--primary), var(--teal));
content: "";
}
.deck {
position: relative;
width: 100%;
height: 100dvh;
}
.slide {
position: absolute;
inset: 0;
display: grid;
grid-template-rows: 1fr auto;
min-height: 100%;
padding: clamp(72px, 8vh, 96px) clamp(28px, 7vw, 128px) clamp(84px, 10vh, 112px);
overflow: hidden;
opacity: 0;
pointer-events: none;
transform: translateX(34px);
transition: opacity .24s ease-out, transform .28s ease-out;
scrollbar-width: thin;
scrollbar-color: #b8c7d8 transparent;
}
.slide::before {
position: absolute;
z-index: -2;
top: -24%;
right: -10%;
width: min(50vw, 760px);
aspect-ratio: 1;
border-radius: 50%;
background: radial-gradient(circle, rgba(8, 119, 189, .10), transparent 68%);
content: "";
}
.slide::after {
position: absolute;
z-index: -2;
bottom: -30%;
left: -14%;
width: min(42vw, 650px);
aspect-ratio: 1;
border-radius: 50%;
background: radial-gradient(circle, rgba(14, 143, 131, .08), transparent 70%);
content: "";
}
.slide.is-active {
z-index: 2;
opacity: 1;
pointer-events: auto;
transform: translateX(0);
}
.slide-body {
align-self: center;
width: min(100%, 1280px);
margin: 0 auto;
}
.slide-footer {
display: flex;
align-items: center;
justify-content: space-between;
width: min(100%, 1280px);
margin: 28px auto 0;
color: var(--muted);
font-size: 12px;
font-weight: 600;
letter-spacing: .04em;
}
.slide-index {
color: var(--primary);
font-family: var(--font-mono);
}
.eyebrow {
display: flex;
align-items: center;
gap: 10px;
margin: 0 0 18px;
color: var(--primary);
font-family: var(--font-mono);
font-size: clamp(11px, .9vw, 14px);
font-weight: 700;
letter-spacing: .1em;
text-transform: uppercase;
}
.eyebrow::before {
width: 28px;
height: 2px;
background: currentColor;
content: "";
}
h1,
h2,
h3,
p { margin-top: 0; }
h1 {
max-width: 980px;
margin-bottom: 24px;
font-size: clamp(48px, 6.2vw, 92px);
line-height: 1.08;
letter-spacing: -.05em;
}
h2 {
max-width: 1050px;
margin-bottom: 28px;
font-size: clamp(36px, 4.2vw, 64px);
line-height: 1.16;
letter-spacing: -.04em;
}
h3 {
margin-bottom: 10px;
font-size: clamp(18px, 1.4vw, 22px);
line-height: 1.35;
}
.lede {
max-width: 820px;
margin-bottom: 0;
color: var(--ink-soft);
font-size: clamp(18px, 1.55vw, 24px);
line-height: 1.72;
}
.accent { color: var(--primary); }
.teal { color: var(--teal); }
.orange { color: var(--orange); }
.cover-grid {
display: grid;
grid-template-columns: minmax(0, 1.4fr) minmax(270px, .6fr);
gap: clamp(42px, 7vw, 110px);
align-items: end;
}
.cover-card {
padding: 28px;
border: 1px solid var(--line);
border-radius: 22px;
background: rgba(255, 255, 255, .92);
box-shadow: var(--shadow);
}
.cover-card .label,
.mini-label {
color: var(--muted);
font-family: var(--font-mono);
font-size: 11px;
font-weight: 700;
letter-spacing: .08em;
text-transform: uppercase;
}
.cover-card strong {
display: block;
margin: 12px 0 24px;
font-size: 27px;
line-height: 1.28;
}
.meta-row {
display: flex;
justify-content: space-between;
gap: 16px;
padding: 13px 0;
border-top: 1px solid var(--line);
color: var(--muted);
font-size: 13px;
}
.meta-row b { color: var(--ink); }
.compare-grid {
display: grid;
grid-template-columns: 1fr auto 1fr;
gap: 22px;
align-items: stretch;
margin-top: 34px;
}
.compare-card,
.project-card,
.level-card,
.debug-card,
.step-card,
.rule-card,
.takeaway-card {
border: 1px solid var(--line);
border-radius: 20px;
background: rgba(255, 255, 255, .94);
box-shadow: 0 12px 38px rgba(28, 58, 90, .07);
}
.compare-card { padding: 28px; }
.compare-card.is-now { border-color: #abd8ed; background: var(--primary-soft); }
.compare-card p {
margin: 0;
color: var(--ink-soft);
font-size: clamp(16px, 1.25vw, 20px);
line-height: 1.65;
}
.compare-arrow {
display: grid;
width: 54px;
place-items: center;
color: var(--primary);
font-size: 30px;
font-weight: 300;
}
.analogy {
display: flex;
align-items: center;
gap: 14px;
max-width: 890px;
margin-top: 24px;
padding: 16px 20px;
border-left: 4px solid var(--teal);
color: var(--ink-soft);
background: var(--teal-soft);
font-size: clamp(16px, 1.25vw, 20px);
line-height: 1.55;
}
.project-layout {
display: grid;
grid-template-columns: .78fr 1.22fr;
gap: clamp(32px, 6vw, 86px);
align-items: center;
}
.project-evidence-layout {
display: grid;
grid-template-columns: minmax(0, 1.35fr) minmax(310px, .65fr);
gap: 22px;
align-items: stretch;
}
.app-shot-card {
position: relative;
margin: 0;
padding: 10px;
overflow: hidden;
border: 1px solid var(--line);
border-radius: 20px;
background: var(--surface);
box-shadow: var(--shadow);
}
.app-shot-frame {
aspect-ratio: 16 / 8.75;
overflow: hidden;
border: 1px solid #cbd7e4;
border-radius: 13px;
background: #e9eff5;
}
.app-shot-frame img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
object-position: left top;
}
.shot-badge {
position: absolute;
top: 23px;
right: 23px;
padding: 7px 10px;
border: 1px solid rgba(255, 255, 255, .65);
border-radius: 999px;
color: #fff;
background: rgba(16, 35, 63, .82);
font-size: 11px;
font-weight: 800;
letter-spacing: .04em;
}
.app-shot-card figcaption {
display: flex;
align-items: center;
justify-content: space-between;
gap: 18px;
padding: 11px 7px 2px;
color: var(--muted);
font-size: 12px;
line-height: 1.45;
}
.app-shot-card figcaption b { color: var(--ink); }
.flow-title {
display: block;
margin-bottom: 10px;
color: var(--ink-soft);
font-size: 14px;
font-weight: 800;
}
.flow-list.compact { gap: 8px; }
.flow-list.compact .flow-item {
min-height: 54px;
padding: 8px 13px;
grid-template-columns: 38px 1fr;
}
.flow-list.compact .flow-no {
width: 32px;
height: 32px;
}
.flow-list.compact .flow-item b { font-size: 14px; }
.flow-list.compact .flow-item span:last-child { display: none; }
.project-summary {
padding: 30px;
border-left: 5px solid var(--primary);
background: var(--surface);
box-shadow: var(--shadow);
}
.project-summary strong {
display: block;
margin-bottom: 12px;
font-size: 26px;
}
.project-summary p {
margin: 0;
color: var(--ink-soft);
font-size: 17px;
line-height: 1.7;
}
.flow-list { display: grid; gap: 12px; }
.flow-item {
display: grid;
grid-template-columns: 46px 1fr auto;
gap: 16px;
align-items: center;
min-height: 68px;
padding: 13px 18px;
border: 1px solid var(--line);
border-radius: 15px;
background: var(--surface);
}
.flow-no {
display: grid;
width: 38px;
height: 38px;
place-items: center;
border-radius: 50%;
color: #fff;
background: var(--primary);
font-family: var(--font-mono);
font-size: 12px;
font-weight: 700;
}
.flow-item b { font-size: 17px; }
.flow-item span:last-child { color: var(--muted); font-size: 12px; font-weight: 700; }
.level-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 18px;
margin-top: 30px;
}
.level-card { position: relative; padding: 28px; overflow: hidden; }
.level-card::before {
position: absolute;
top: 0;
right: 0;
left: 0;
height: 5px;
background: var(--primary);
content: "";
}
.level-card:nth-child(2)::before { background: var(--teal); }
.level-card:nth-child(3)::before { background: var(--orange); }
.level-badge {
display: inline-block;
margin-bottom: 18px;
padding: 7px 11px;
border-radius: 999px;
color: var(--primary-dark);
background: var(--primary-soft);
font-size: 12px;
font-weight: 800;
}
.level-card:nth-child(2) .level-badge { color: #086a62; background: var(--teal-soft); }
.level-card:nth-child(3) .level-badge { color: #9c4a10; background: var(--orange-soft); }
.level-card p {
margin: 0;
color: var(--ink-soft);
font-size: 16px;
line-height: 1.65;
}
.level-footer {
margin-top: 22px;
color: var(--ink-soft);
font-size: clamp(16px, 1.35vw, 20px);
text-align: center;
}
.debug-layout {
display: grid;
grid-template-columns: .92fr 1.08fr;
gap: clamp(30px, 5vw, 72px);
align-items: center;
}
.bug-box {
padding: 26px;
border: 1px solid #efc7c7;
border-radius: 18px;
background: var(--danger-soft);
}
.bug-box strong {
display: block;
margin: 10px 0 16px;
color: #8b2e2e;
font-size: 24px;
}
.log-line {
margin: 7px 0 0;
color: #653d3d;
font-family: var(--font-mono);
font-size: 13px;
line-height: 1.55;
}
.mail-analogy {
margin: 18px 0 0;
padding: 18px 20px;
border-left: 4px solid var(--primary);
color: var(--ink-soft);
background: var(--primary-soft);
font-size: 17px;
line-height: 1.6;
}
.logic-compare {
display: grid;
grid-template-columns: .84fr 1.16fr;
gap: 12px;
}
.logic-panel {
min-height: 176px;
padding: 19px;
border: 1px solid var(--line);
border-radius: 16px;
background: var(--surface);
}
.logic-panel.is-wrong {
border-color: #efc7c7;
background: var(--danger-soft);
}
.logic-panel.is-right {
border-color: #b9dfd8;
background: var(--teal-soft);
}
.logic-panel pre {
margin: 14px 0 0;
white-space: pre-wrap;
color: var(--ink);
font-family: var(--font-mono);
font-size: clamp(11px, .86vw, 13px);
line-height: 1.75;
}
.debug-clue {
margin: 12px 0 0;
padding: 11px 14px;
border-left: 4px solid var(--orange);
color: #75401a;
background: var(--orange-soft);
font-family: var(--font-mono);
font-size: 12px;
line-height: 1.5;
}
.debug-card { overflow: hidden; }
.debug-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 18px 22px;
border-bottom: 1px solid var(--line);
background: #f9fbfd;
}
.status-dot {
display: inline-flex;
align-items: center;
gap: 8px;
color: var(--muted);
font-size: 12px;
font-weight: 700;
}
.status-dot::before {
width: 8px;
height: 8px;
border-radius: 50%;
background: #9cafc2;
content: "";
}
.debug-body { padding: 24px; }
.scenario-buttons {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
margin-bottom: 20px;
}
.scenario-button,
.nav-button {
min-height: 46px;
border: 1px solid var(--line);
border-radius: 12px;
color: var(--ink);
background: var(--surface);
cursor: pointer;
font-weight: 700;
transition: color .18s ease, background .18s ease, border-color .18s ease, transform .18s ease;
}
.scenario-button:hover,
.scenario-button.is-selected {
border-color: var(--primary);
color: #fff;
background: var(--primary);
}
.scenario-result {
min-height: 170px;
padding: 20px;
border: 1px solid var(--line);
border-radius: 14px;
background: #f8fafc;
}
.scenario-result h3 { margin-bottom: 12px; }
.result-line {
display: flex;
justify-content: space-between;
gap: 20px;
padding: 10px 0;
border-top: 1px solid var(--line);
color: var(--ink-soft);
font-size: 14px;
}
.result-line b { color: var(--ink); }
.method-grid {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 12px;
margin-top: 28px;
}
.step-card { padding: 22px 18px; }
.step-number {
display: block;
margin-bottom: 28px;
color: var(--primary);
font-family: var(--font-mono);
font-size: 28px;
font-weight: 800;
}
.step-card p {
margin: 0;
color: var(--ink-soft);
font-size: 14px;
line-height: 1.55;
}
.prompt-box {
margin: 0;
padding: 17px 22px;
border-left: 4px solid var(--teal);
color: #16524e;
background: var(--teal-soft);
font-size: clamp(15px, 1.15vw, 18px);
line-height: 1.6;
}
.evidence-row {
display: grid;
grid-template-columns: .9fr 1.1fr;
gap: 14px;
align-items: stretch;
margin-top: 20px;
}
.test-proof {
overflow: hidden;
border: 1px solid #cbd7e4;
border-radius: 14px;
color: #dcebf7;
background: #12243a;
box-shadow: 0 10px 28px rgba(18, 36, 58, .12);
}
.test-proof-head {
display: flex;
justify-content: space-between;
gap: 16px;
padding: 10px 14px;
border-bottom: 1px solid rgba(255, 255, 255, .12);
color: #99abc0;
font-family: var(--font-mono);
font-size: 10px;
font-weight: 700;
letter-spacing: .05em;
}
.test-proof-head b { color: #82dfc7; }
.test-row {
display: grid;
grid-template-columns: 46px 1fr;
gap: 10px;
align-items: center;
padding: 8px 14px;
border-top: 1px solid rgba(255, 255, 255, .08);
font-size: 12px;
line-height: 1.4;
}
.pass-label {
color: #82dfc7;
font-family: var(--font-mono);
font-size: 10px;
font-weight: 800;
}
.requirement-layout,
.question-layout,
.contract-layout,
.plan-layout,
.phase-layout {
display: grid;
gap: clamp(24px, 4vw, 60px);
align-items: center;
}
.requirement-layout { grid-template-columns: .72fr 1.28fr; }
.question-layout { grid-template-columns: .72fr 1.28fr; }
.contract-layout,
.plan-layout,
.phase-layout { grid-template-columns: 1fr; }
.requirement-intro,
.question-intro {
align-self: center;
}
.quote-card {
margin-top: 24px;
padding: 22px 24px;
border-left: 5px solid var(--orange);
border-radius: 0 16px 16px 0;
color: var(--ink);
background: var(--orange-soft);
box-shadow: 0 10px 28px rgba(216, 107, 24, .08);
font-size: clamp(17px, 1.25vw, 20px);
font-weight: 700;
line-height: 1.65;
}
.small-note {
margin: 18px 0 0;
color: var(--muted);
font-size: 14px;
line-height: 1.6;
}
.requirement-grid,
.question-grid,
.contract-grid,
.plan-grid,
.phase-grid,
.boundary-row {
display: grid;
gap: 12px;
}
.requirement-grid { grid-template-columns: repeat(3, 1fr); }
.question-grid,
.contract-grid,
.plan-grid { grid-template-columns: repeat(2, 1fr); }
.phase-grid { grid-template-columns: repeat(4, 1fr); }
.boundary-row { grid-template-columns: repeat(3, 1fr); margin-top: 18px; }
.requirement-card,
.question-card,
.contract-card,
.plan-card,
.phase-card,
.boundary-card {
border: 1px solid var(--line);
border-radius: 16px;
background: rgba(255, 255, 255, .94);
box-shadow: 0 10px 28px rgba(28, 58, 90, .06);
}
.requirement-card {
min-height: 92px;
padding: 16px;
}
.requirement-card strong,
.question-card strong,
.contract-card strong,
.plan-card strong,
.phase-card strong,
.boundary-card strong {
display: block;
margin-bottom: 7px;
color: var(--ink);
font-size: 15px;
line-height: 1.35;
}
.requirement-card span,
.question-card p,
.contract-card p,
.plan-card p,
.phase-card p,
.boundary-card p {
margin: 0;
color: var(--ink-soft);
font-size: 13px;
line-height: 1.5;
}
.requirement-card .card-number,
.phase-card .card-number {
display: block;
margin-bottom: 10px;
color: var(--primary);
font-family: var(--font-mono);
font-size: 11px;
font-weight: 800;
letter-spacing: .08em;
}
.question-intro .quote-card {
border-left-color: var(--primary);
color: var(--primary-dark);
background: var(--primary-soft);
}
.question-card,
.contract-card,
.plan-card {
min-height: 148px;
padding: 20px;
}
.question-card::before,
.contract-card::before,
.plan-card::before {
display: block;
width: 30px;
height: 4px;
margin-bottom: 18px;
border-radius: 999px;
background: var(--primary);
content: "";
}
.question-card:nth-child(2)::before,
.contract-card:nth-child(2)::before,
.plan-card:nth-child(2)::before { background: var(--teal); }
.question-card:nth-child(3)::before,
.contract-card:nth-child(3)::before,
.plan-card:nth-child(3)::before { background: var(--orange); }
.question-card:nth-child(4)::before,
.contract-card:nth-child(4)::before,
.plan-card:nth-child(4)::before { background: #7b61a8; }
.card-kicker {
display: block;
margin-bottom: 8px;
color: var(--muted);
font-family: var(--font-mono);
font-size: 10px;
font-weight: 800;
letter-spacing: .08em;
text-transform: uppercase;
}
.contract-flow {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
gap: 9px;
margin-top: 22px;
color: var(--primary-dark);
font-size: 13px;
font-weight: 800;
}
.contract-flow span {
padding: 9px 12px;
border: 1px solid #abd8ed;
border-radius: 999px;
background: var(--primary-soft);
}
.contract-flow i {
color: var(--muted);
font-style: normal;
}
.plan-card {
min-height: 168px;
}
.plan-card strong { font-size: 20px; }
.plan-card p { font-size: 15px; line-height: 1.6; }
.plan-foot,
.phase-foot {
margin: 20px auto 0;
color: var(--ink-soft);
font-size: clamp(15px, 1.2vw, 18px);
line-height: 1.6;
text-align: center;
}
.phase-card {
min-height: 134px;
padding: 17px;
}
.phase-card strong { font-size: 15px; }
.phase-card p { font-size: 12px; }
.phase-card:nth-child(2) { background: #f1faf8; }
.phase-card:nth-child(3) { background: #fff8f1; }
.phase-card:nth-child(4) { background: #f6f2fb; }
.phase-card:nth-child(5) { background: #f3f8fc; }
.phase-card:nth-child(6) { background: #f5faf8; }
.phase-card:nth-child(7) { background: #fff8f1; }
.phase-foot {
padding: 13px 18px;
border-left: 4px solid var(--teal);
background: var(--teal-soft);
}
.boundary-card {
padding: 17px 18px;
}
.boundary-card strong { font-size: 16px; }
.boundary-card p { font-size: 13px; }
.boundary-card:nth-child(2) { border-top: 4px solid var(--teal); }
.boundary-card:nth-child(3) { border-top: 4px solid var(--orange); }
.boundary-note {
margin-top: 20px;
padding: 15px 18px;
border: 1px solid #abd8ed;
border-radius: 14px;
color: var(--primary-dark);
background: var(--primary-soft);
font-size: 15px;
line-height: 1.6;
}
.rule-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
margin-top: 28px;
}
.rule-card {
display: grid;
grid-template-columns: 54px 1fr;
gap: 18px;
align-items: start;
padding: 22px;
}
.rule-no {
display: grid;
width: 48px;
height: 48px;
place-items: center;
border-radius: 14px;
color: var(--primary-dark);
background: var(--primary-soft);
font-family: var(--font-mono);
font-weight: 800;
}
.rule-card:nth-child(4) .rule-no { color: #973e0c; background: var(--orange-soft); }
.rule-card p {
margin: 0;
color: var(--ink-soft);
font-size: 15px;
line-height: 1.6;
}
.finish-layout {
display: grid;
grid-template-columns: 1.08fr .92fr;
gap: clamp(40px, 7vw, 108px);
align-items: center;
}
.finish-statement {
max-width: 760px;
color: var(--ink-soft);
font-size: clamp(20px, 1.7vw, 27px);
line-height: 1.65;
}
.takeaway-list { display: grid; gap: 14px; }
.takeaway-card {
padding: 22px 24px;
border-left: 5px solid var(--primary);
}
.takeaway-card:nth-child(2) { border-left-color: var(--teal); }
.takeaway-card:nth-child(3) { border-left-color: var(--orange); }
.takeaway-card b { display: block; margin-bottom: 6px; font-size: 18px; }
.takeaway-card span { color: var(--muted); font-size: 14px; line-height: 1.5; }
.deck-controls {
position: fixed;
z-index: 40;
right: clamp(20px, 3vw, 50px);
bottom: 24px;
display: flex;
align-items: center;
gap: 10px;
}
.nav-button {
min-width: 86px;
padding: 0 18px;
box-shadow: 0 8px 24px rgba(28, 58, 90, .08);
}
.nav-button.primary {
border-color: var(--primary);
color: #fff;
background: var(--primary);
}
.nav-button:hover { transform: translateY(-1px); }
.nav-button:disabled { opacity: .42; cursor: default; transform: none; }
.dots {
position: fixed;
z-index: 40;
bottom: 38px;
left: clamp(20px, 4vw, 64px);
display: flex;
gap: 8px;
}
.dot {
width: 9px;
height: 9px;
padding: 0;
border: 0;
border-radius: 50%;
background: #b9c8d7;
cursor: pointer;
transition: width .18s ease, border-radius .18s ease, background .18s ease;
}
.dot.is-active { width: 26px; border-radius: 999px; background: var(--primary); }
@media (max-width: 920px) {
html,
body { overflow: auto; }
.deck { min-height: 100dvh; }
.slide { position: relative; display: none; min-height: 100dvh; padding: 76px 24px 104px; overflow-x: hidden; overflow-y: auto; }
.slide.is-active { display: grid; }
.cover-grid,
.project-layout,
.project-evidence-layout,
.debug-layout,
.finish-layout,
.requirement-layout,
.question-layout { grid-template-columns: 1fr; }
.compare-grid { grid-template-columns: 1fr; }
.compare-arrow { width: auto; height: 30px; transform: rotate(90deg); }
.level-grid { grid-template-columns: 1fr; }
.method-grid { grid-template-columns: repeat(2, 1fr); }
.evidence-row { grid-template-columns: 1fr; }
.rule-grid { grid-template-columns: 1fr; }
.requirement-grid,
.question-grid,
.contract-grid,
.plan-grid,
.phase-grid { grid-template-columns: repeat(2, 1fr); }
.brand-mark { top: 22px; }
.slide-footer span:first-child { display: none; }
.slide-footer { justify-content: flex-end; }
}
@media (max-width: 560px) {
h1 { font-size: clamp(42px, 13vw, 64px); }
h2 { font-size: clamp(32px, 9vw, 46px); }
.method-grid,
.logic-compare,
.scenario-buttons,
.requirement-grid,
.question-grid,
.contract-grid,
.plan-grid,
.phase-grid,
.boundary-row { grid-template-columns: 1fr; }
.flow-item { grid-template-columns: 42px 1fr; }
.flow-item span:last-child { display: none; }
.slide-footer { display: none; }
.deck-controls { right: 16px; bottom: 18px; }
.dots { left: 16px; bottom: 31px; }
.nav-button { min-width: 66px; padding: 0 12px; }
}
@media (max-height: 800px) and (min-width: 921px) {
.slide { padding-top: 68px; padding-bottom: 76px; }
h1 { font-size: clamp(44px, 5.4vw, 76px); }
h2 { margin-bottom: 20px; font-size: clamp(32px, 3.7vw, 52px); }
.project-evidence-layout { grid-template-columns: minmax(0, 1.3fr) minmax(290px, .7fr); gap: 16px; }
.app-shot-frame { aspect-ratio: 16 / 7.15; }
.app-shot-card figcaption { padding-top: 7px; font-size: 11px; }
.flow-list.compact { gap: 6px; }
.flow-list.compact .flow-item { min-height: 46px; padding-top: 6px; padding-bottom: 6px; }
.level-card { padding: 22px; }
.method-grid { margin-top: 18px; }
.step-card { padding: 16px; }
.step-number { margin-bottom: 18px; }
.evidence-row { margin-top: 12px; }
.prompt-box { padding: 13px 16px; }
.test-row { padding-top: 6px; padding-bottom: 6px; }
.rule-grid { margin-top: 20px; }
.rule-card { padding: 17px; }
.requirement-card { min-height: 78px; padding: 13px; }
.question-card,
.contract-card,
.plan-card { min-height: 126px; padding: 16px; }
.question-card::before,
.contract-card::before,
.plan-card::before { margin-bottom: 12px; }
.plan-layout { gap: 14px; }
.plan-layout h2 { margin-bottom: 8px; }
.plan-card { min-height: 112px; padding: 14px; }
.plan-card::before { margin-bottom: 9px; }
.plan-card strong { font-size: 17px; }
.plan-card p { font-size: 13px; line-height: 1.45; }
.plan-foot { margin-top: 8px; font-size: 14px; line-height: 1.35; }
.phase-card { min-height: 108px; padding: 13px; }
.phase-foot { margin-top: 14px; padding: 10px 13px; font-size: 14px; }
.slide-footer { margin-top: 16px; }
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
scroll-behavior: auto !important;
transition-duration: .01ms !important;
animation-duration: .01ms !important;
animation-iteration-count: 1 !important;
}
}
</style>
</head>
<body>
<div class="progress-track" aria-hidden="true"><span id="progressBar"></span></div>
<div class="brand-mark">AI PROGRAMMING · FIELD CASE</div>
<main class="deck" id="deck" aria-live="polite">
<section class="slide is-active" data-slide="0" data-title="封面" aria-label="第 1 页:封面">
<div class="slide-body cover-grid">
<div>
<p class="eyebrow">3 MONTHS · ONE REAL PROJECT</p>
<h1>从写 <span class="orange">if-else</span><br>到 <span class="accent">Vibe Coding</span></h1>
<p class="lede">我如何用 AI 搭建一套 PCB 目检放行上位机,以及普通人怎样上手、调试和避坑。</p>
</div>
<aside class="cover-card" aria-label="汇报信息">
<span class="label">CASE STUDY</span>
<strong>AxiOmron<br>PCBCheck</strong>
<div class="meta-row"><span>项目</span><b>工业上位机</b></div>
<div class="meta-row"><span>重点</span><b>上手 · 调试 · 边界</b></div>
<div class="meta-row"><span>时长</span><b>10–12 MIN</b></div>
</aside>
</div>
<footer class="slide-footer"><span>一段真实的 AI 编程实践</span><span class="slide-index">01 / 12</span></footer>
</section>
<section class="slide" data-slide="1" data-title="初始需求" aria-label="第 2 页:初始需求">
<div class="slide-body requirement-layout">
<div class="requirement-intro">
<p class="eyebrow">01 / THE FIRST REQUEST</p>
<h2>最开始,我只想让它<br><span class="orange">先跑起来</span>。</h2>
<div class="quote-card">“我需要写一个程序:PCB 到位后扫码,根据条码检查文件是否存在,再把结果返回给 PLC。”</div>
<p class="small-note">这就是《开发流程.txt》里的最初版本:目标很清楚,但工程边界还没有被说出来。</p>
</div>
<div class="requirement-grid" aria-label="初始需求清单">
<article class="requirement-card"><span class="card-number">01</span><strong>PCB 到位后开始处理</strong><span>收到触发,进入本轮任务。</span></article>
<article class="requirement-card"><span class="card-number">02</span><strong>读取一个条码</strong><span>拿到 PCB 身份信息。</span></article>
<article class="requirement-card"><span class="card-number">03</span><strong>检查对应文件</strong><span>根据条码查找指定文件。</span></article>
<article class="requirement-card"><span class="card-number">04</span><strong>文件存在显示正常</strong><span>给出 OK 结果。</span></article>
<article class="requirement-card"><span class="card-number">05</span><strong>文件不存在显示异常</strong><span>给出 NG 结果。</span></article>
<article class="requirement-card"><span class="card-number">06</span><strong>页面显示当前状态</strong><span>看到条码、结果和日志。</span></article>
<article class="requirement-card"><span class="card-number">07</span><strong>保存最近处理记录</strong><span>之后可以回看过程。</span></article>
<article class="requirement-card"><span class="card-number">08</span><strong>先使用模拟数据</strong><span>不一开始连接真实设备。</span></article>
<article class="requirement-card"><span class="card-number">09</span><strong>模拟正常、异常、故障</strong><span>页面提供可控演示入口。</span></article>
</div>
</div>
<footer class="slide-footer"><span>初版需求:先把业务主线讲清楚</span><span class="slide-index">02 / 12</span></footer>
</section>
<section class="slide" data-slide="2" data-title="第一次对话" aria-label="第 3 页:第一次对话">
<div class="slide-body question-layout">
<div class="question-intro">
<p class="eyebrow">02 / ASK BEFORE BUILDING</p>
<h2>真正开始写代码前,<br>我先问:<span class="accent">还缺什么?</span></h2>
<div class="quote-card">“还需要哪些信息才能开始写程序?”</div>
<p class="small-note">这一步把“能不能写”变成“哪些事实还没有确认”。</p>
</div>
<div class="question-grid" aria-label="第一次对话中需要补齐的信息">
<article class="question-card"><span class="card-kicker">01 · PLC</span><strong>触发和握手怎么定义?</strong><p>型号、协议、点位、保持/脉冲、复位、应答、超时和异常恢复。</p></article>
<article class="question-card"><span class="card-kicker">02 · SCANNERS</span><strong>两把扫码枪怎么返回?</strong><p>IP、端口、通信角色、结束符、编码、超时、双枪关系和断线行为。</p></article>
<article class="question-card"><span class="card-kicker">03 · FILE RULES</span><strong>什么样的文件算通过?</strong><p>条码格式、文件名规则、精确/模糊匹配、目录类型、空文件和正在写入的处理。</p></article>
<article class="question-card"><span class="card-kicker">04 · APP & RECORDS</span><strong>程序要怎样被使用和追溯?</strong><p>界面、配置、日志、记录、权限、模拟入口、部署方式和是否需要数据库。</p></article>
</div>
</div>
<footer class="slide-footer"><span>好的协作不是马上给答案,而是先把问题问完整</span><span class="slide-index">03 / 12</span></footer>
</section>
<section class="slide" data-slide="3" data-title="需求逐步完善" aria-label="第 4 页:需求逐步完善">
<div class="slide-body contract-layout">
<div>
<p class="eyebrow">03 / TURN QUESTIONS INTO CONTRACTS</p>
<h2>需求不再只是“做什么”,<br>还要说清楚<span class="teal">“边界在哪里”</span>。</h2>
</div>
<div class="contract-grid" aria-label="需求完善后的四类工程契约">
<article class="contract-card"><span class="card-kicker">PLC CONTRACT</span><strong>到位、复位、结果、应答</strong><p>定义一轮任务什么时候开始、什么时候结束,以及长保持信号如何避免重复触发。</p></article>
<article class="contract-card"><span class="card-kicker">SCANNER CONTRACT</span><strong>双路并发、超时、报文边界</strong><p>不把一次 TCP Receive 想当然地当成完整条码,明确无结束符和迟到结果的处理。</p></article>
<article class="contract-card"><span class="card-kicker">FILE CONTRACT</span><strong>路径安全、结果分类</strong><p>把条码清洗、文件存在、目录不可访问和路径越界区分成可追踪的结果。</p></article>
<article class="contract-card"><span class="card-kicker">APP CONTRACT</span><strong>状态可见、过程可回看</strong><p>把设备状态、当前任务、日志、统计和模拟场景放进可观察的界面闭环。</p></article>
</div>
<div class="contract-flow" aria-label="需求演进链路">
<span>功能描述</span><i>→</i><span>条件边界</span><i>→</i><span>失败策略</span><i>→</i><span>验收证据</span>
</div>
</div>
<footer class="slide-footer"><span>每补充一个边界,现场就少一个靠猜的地方</span><span class="slide-index">04 / 12</span></footer>
</section>
<section class="slide" data-slide="4" data-title="完整项目计划" aria-label="第 5 页:完整项目计划">
<div class="slide-body plan-layout">
<div>
<p class="eyebrow">04 / THE COMPLETE PROJECT PLAN</p>
<h2>对话的终点,<br>是一份<span class="accent">可以执行的计划</span>。</h2>
</div>
<div class="plan-grid" aria-label="完整项目计划的四个层面">
<article class="plan-card"><span class="card-kicker">01 · GOAL</span><strong>目标</strong><p>让 PCB 从到位、扫码、文件校验到 PLC 结果反馈,形成一条可解释、可追溯的业务闭环。</p></article>
<article class="plan-card"><span class="card-kicker">02 · WORKFLOW</span><strong>流程</strong><p>到位触发 → 双枪扫描 → 条码校验 → 文件判断 → 结果保持 → 应答/复位 → 记录完成。</p></article>
<article class="plan-card"><span class="card-kicker">03 · ARCHITECTURE</span><strong>架构</strong><p>当前项目落地为 .NET 8 + WPF;设备通信、流程编排、界面状态和配置日志保持清晰边界。</p></article>
<article class="plan-card"><span class="card-kicker">04 · ACCEPTANCE</span><strong>测试与验收</strong><p>单元测试和 Fake 设备先验证逻辑,再用真实 PLC、扫描枪和文件服务完成现场联调。</p></article>
</div>
<p class="plan-foot"><strong>计划的价值:</strong>把“我想要一个程序”翻译成目标、状态、接口、阶段、测试和交付门槛。</p>
</div>
<footer class="slide-footer"><span>完整计划不是凭空出现的,而是对话逐项补齐后的结果</span><span class="slide-index">05 / 12</span></footer>
</section>
<section class="slide" data-slide="5" data-title="开发阶段路线图" aria-label="第 6 页:开发阶段路线图">
<div class="slide-body phase-layout">
<div>
<p class="eyebrow">05 / BUILD IN RISK ORDER</p>
<h2>计划被拆成七个小闭环,<br>每一步都有<span class="teal">可以验证的产物</span>。</h2>
</div>
<div class="phase-grid" aria-label="七个开发阶段">
<article class="phase-card"><span class="card-number">01</span><strong>需求与协议确认</strong><p>点位、握手、报文、结果码和文件规则。</p></article>
<article class="phase-card"><span class="card-number">02</span><strong>项目骨架</strong><p>WPF、配置、日志、依赖注入和基础状态。</p></article>
<article class="phase-card"><span class="card-number">03</span><strong>PLC 通信</strong><p>读写、超时、断线、重连和模拟接口。</p></article>
<article class="phase-card"><span class="card-number">04</span><strong>双扫描枪</strong><p>并发触发、无结束符、首个有效结果。</p></article>
<article class="phase-card"><span class="card-number">05</span><strong>流程编排</strong><p>任务去重、文件安全、结果保持和恢复。</p></article>
<article class="phase-card"><span class="card-number">06</span><strong>状态界面</strong><p>设备、任务、统计、日志和操作权限。</p></article>
<article class="phase-card"><span class="card-number">07</span><strong>测试与交付</strong><p>单测、Fake 流程、异常测试和现场验收。</p></article>
</div>
<p class="phase-foot">计划不是一次写死:事实确认后可以调整实现,但每次调整都要回到边界、证据和验收标准。</p>
</div>
<footer class="slide-footer"><span>AI 加快每个小闭环,我负责决定闭环是否成立</span><span class="slide-index">06 / 12</span></footer>
</section>
<section class="slide" data-slide="6" data-title="Vibe Coding 上手难度" aria-label="第 7 页:Vibe Coding 上手难度">
<div class="slide-body">
<p class="eyebrow">06 / HOW HARD IS IT</p>
<h2>上手不难,<br>难的是从“能跑”走到<span class="orange">“可靠”</span>。</h2>
<div class="level-grid">
<article class="level-card">
<span class="level-badge">门槛低</span>
<h3>做出原型</h3>
<p>会描述需求、会运行项目,就能很快看到第一个结果。</p>
</article>
<article class="level-card">
<span class="level-badge">需要方法</span>
<h3>做到可用</h3>
<p>要会拆小任务、补充上下文,并检查 AI 实际改了什么。</p>
</article>
<article class="level-card">
<span class="level-badge">仍需专业判断</span>
<h3>做到可靠</h3>
<p>需要理解业务风险,并验证断网、超时和错误输入等异常场景。</p>
</article>
</div>
<p class="level-footer"><strong>它降低的是写代码的门槛,</strong>没有消除软件工程本身的难度。</p>
</div>
<footer class="slide-footer"><span>有代码基础,上手会更快;没有代码基础,也可以从小工具开始</span><span class="slide-index">07 / 12</span></footer>
</section>
<section class="slide" data-slide="7" data-title="实际项目运行" aria-label="第 8 页:实际项目运行">
<div class="slide-body">
<p class="eyebrow">07 / THE PROJECT RUNS</p>
<h2>这不是概念图,<br><span class="teal">程序真的运行起来了</span>。</h2>
<div class="project-evidence-layout">
<figure class="app-shot-card">
<div class="app-shot-frame">
<img src="assets/axiomron-dashboard-report.png" width="2575" height="1407" alt="AxiOmron PCBCheck WPF 主界面,展示 PLC、扫码枪、文件校验、安灯、流程状态与运行日志" />
</div>
<span class="shot-badge">真实 WPF 运行界面</span>
<figcaption><b>设备状态、当前条码、流程和日志集中展示</b><span>本地展示环境 · 未连接真实设备</span></figcaption>
</figure>
<div>
<span class="flow-title">一块 PCB 的 5 步旅程</span>
<div class="flow-list compact" aria-label="PCB 放行流程">
<div class="flow-item"><span class="flow-no">01</span><b>PLC 通知 PCB 到位</b><span>START</span></div>
<div class="flow-item"><span class="flow-no">02</span><b>两把扫码枪读取条码</b><span>SCAN</span></div>
<div class="flow-item"><span class="flow-no">03</span><b>根据条码检查产品文件</b><span>CHECK</span></div>
<div class="flow-item"><span class="flow-no">04</span><b>发送结果并等待确认</b><span>ACK</span></div>
<div class="flow-item"><span class="flow-no">05</span><b>保存记录并回到空闲</b><span>DONE</span></div>
</div>
</div>
</div>
</div>
<footer class="slide-footer"><span>运行界面是本地证据,不等同于真实 PLC / SFTP 现场验收</span><span class="slide-index">08 / 12</span></footer>
</section>
<section class="slide" data-slide="8" data-title="真实调试案例" aria-label="第 9 页:真实调试案例">
<div class="slide-body debug-layout">
<div>
<p class="eyebrow">08 / A REAL DEBUGGING CASE</p>
<h2>信号写成功了,<br>为什么<span class="orange">文件不能删</span>?</h2>
<div class="logic-compare" aria-label="修改前后的代码逻辑对比">
<article class="logic-panel is-wrong">
<span class="mini-label">最初想法 · 有风险</span>
<pre><code>await WriteReleaseAsync();
await DeleteFileAsync();</code></pre>
</article>
<article class="logic-panel is-right">
<span class="mini-label">调试后 · 等待业务确认</span>
<pre><code>await WriteReleaseAsync();
if (ackReceived)
await DeleteFileAsync();</code></pre>
</article>
</div>
<p class="debug-clue">LOG 放行信号写入成功 → 等待 PLC 确认超时</p>
<p class="mail-analogy">像邮件显示“发送成功”,不代表对方已经阅读并执行。</p>
</div>
<section class="debug-card" aria-label="调试情景演示">
<div class="debug-head"><b>调试情景演示</b><span class="status-dot" id="scenarioStatus">等待选择</span></div>
<div class="debug-body">
<div class="scenario-buttons">
<button class="scenario-button" id="ackButton" type="button">收到 PLC 确认</button>
<button class="scenario-button" id="timeoutButton" type="button">等待确认超时</button>
</div>
<div class="scenario-result" id="scenarioResult">
<span class="mini-label">点击上方按钮</span>
<h3>比较两种结果</h3>
<div class="result-line"><span>PLC 业务结果</span><b>待确认</b></div>
<div class="result-line"><span>产品文件</span><b>保持不动</b></div>
</div>
</div>
</section>
</div>
<footer class="slide-footer"><span>调试结论:协议成功 ≠ 业务完成</span><span class="slide-index">09 / 12</span></footer>
</section>
<section class="slide" data-slide="9" data-title="五步调试法" aria-label="第 10 页:五步调试法">
<div class="slide-body">
<p class="eyebrow">09 / DEBUG WITH AI</p>
<h2>程序出错时,<br>我用这套<span class="accent">五步调试法</span>。</h2>
<div class="method-grid">
<article class="step-card"><span class="step-number">01</span><h3>先复现</h3><p>说明做了什么、期望什么、实际发生什么。</p></article>
<article class="step-card"><span class="step-number">02</span><h3>给证据</h3><p>提供报错、日志、配置和最近的修改。</p></article>
<article class="step-card"><span class="step-number">03</span><h3>讲流程</h3><p>先让 AI 解释调用过程,不急着修改。</p></article>
<article class="step-card"><span class="step-number">04</span><h3>小步改</h3><p>一次只处理一个原因,修改后立即运行。</p></article>
<article class="step-card"><span class="step-number">05</span><h3>正反测</h3><p>既测试成功,也测试超时、断网和错误输入。</p></article>
</div>
<div class="evidence-row">
<div class="prompt-box"><strong>我的常用提示:</strong>“先不要改代码。请根据日志找到调用流程,列出最可能的三个原因,以及如何逐个验证。”</div>
<section class="test-proof" aria-label="定向测试结果">
<div class="test-proof-head"><span>dotnet test · targeted</span><b>3 PASSED · 666 ms</b></div>
<div class="test-row"><span class="pass-label">PASS</span><span>收到 PLC 确认且开关开启 → 删除文件</span></div>
<div class="test-row"><span class="pass-label">PASS</span><span>等待 PLC 确认超时 → 保留文件</span></div>
<div class="test-row"><span class="pass-label">PASS</span><span>删除功能开关关闭 → 保留文件</span></div>
</section>
</div>
</div>
<footer class="slide-footer"><span>证据越完整,AI 越不容易靠猜</span><span class="slide-index">10 / 12</span></footer>
</section>
<section class="slide" data-slide="10" data-title="工程边界与验收" aria-label="第 11 页:工程边界与验收">
<div class="slide-body">
<p class="eyebrow">10 / BOUNDARIES & ACCEPTANCE</p>
<h2>Vibe Coding 很快,<br>但这些<span class="orange">不能省</span>。</h2>
<div class="rule-grid">
<article class="rule-card"><span class="rule-no">01</span><div><h3>任务要拆小</h3><p>每一步都应该能运行、能观察、能撤回。</p></div></article>
<article class="rule-card"><span class="rule-no">02</span><div><h3>修改要看懂</h3><p>至少确认改了哪些文件、为什么改、影响到哪里。</p></div></article>
<article class="rule-card"><span class="rule-no">03</span><div><h3>模拟不等于现场</h3><p>Fake 设备只能先验证逻辑,不能替代真实 PLC、扫描枪和文件服务。</p></div></article>
<article class="rule-card"><span class="rule-no">04</span><div><h3>高风险动作默认关闭</h3><p>删除、付款、放行等动作必须有明确条件和可控开关。</p></div></article>
</div>
<div class="boundary-row" aria-label="交付边界">
<article class="boundary-card"><strong>异常可恢复</strong><p>断线、超时、目录不可访问,都要有状态、日志和恢复策略。</p></article>
<article class="boundary-card"><strong>过程可追溯</strong><p>每一轮任务都能回看输入、判断、结果和失败原因。</p></article>
<article class="boundary-card"><strong>现场才算验收</strong><p>真实点位、通信边界、权限和连续生产稳定性必须现场确认。</p></article>
</div>
<div class="boundary-note"><strong>交付门槛:</strong>本地构建与测试通过只是软件证据;真实设备和现场权限通过,才是生产验收。</div>
</div>
<footer class="slide-footer"><span>AI 可以给建议,但业务责任和最终判断仍然属于人</span><span class="slide-index">11 / 12</span></footer>
</section>
<section class="slide" data-slide="11" data-title="总结" aria-label="第 12 页:总结">
<div class="slide-body finish-layout">
<div>
<p class="eyebrow">11 / TAKEAWAY</p>
<h2>从一句话需求,<br>到<span class="accent">可验证的完整计划</span>。</h2>
<p class="finish-statement">AI 没有替我完成工程判断,而是帮我更快地提问、拆解、实现和验证,最后把一个模糊需求变成能运行、能追溯、知道边界的项目。</p>
</div>
<div class="takeaway-list">
<article class="takeaway-card"><b>会提问</b><span>把需求里的空白和风险问出来</span></article>
<article class="takeaway-card"><b>会拆解</b><span>把自然语言变成流程、阶段和验收标准</span></article>
<article class="takeaway-card"><b>会验证</b><span>把本地证据与真实现场验收分开</span></article>
</div>
</div>
<footer class="slide-footer"><span>谢谢 · AxiOmron.PcbCheck</span><span class="slide-index">12 / 12</span></footer>
</section>
</main>
<nav class="dots" id="dots" aria-label="汇报页面导航"></nav>
<div class="deck-controls" aria-label="翻页控制">
<button class="nav-button" id="previousButton" type="button">上一页</button>
<button class="nav-button primary" id="nextButton" type="button">下一页</button>
</div>
<script>
const slides = Array.from(document.querySelectorAll('.slide'));
const progressBar = document.getElementById('progressBar');
const dotsContainer = document.getElementById('dots');
const previousButton = document.getElementById('previousButton');
const nextButton = document.getElementById('nextButton');
const ackButton = document.getElementById('ackButton');
const timeoutButton = document.getElementById('timeoutButton');
const scenarioStatus = document.getElementById('scenarioStatus');
const scenarioResult = document.getElementById('scenarioResult');
let activeSlideIndex = 0;
/**
* 根据页面数量统一更新页脚编号和无障碍页面标题。
* @returns {void} 只同步每个页面的展示元数据,不返回数据。
*/
function syncSlideMetadata() {
const totalLabel = String(slides.length).padStart(2, '0');
slides.forEach((slide, index) => {
const pageNumber = String(index + 1).padStart(2, '0');
const title = slide.dataset.title || `第 ${index + 1} 页`;
const indexLabel = slide.querySelector('.slide-index');
if (indexLabel) indexLabel.textContent = `${pageNumber} / ${totalLabel}`;
slide.setAttribute('aria-label', `第 ${index + 1} 页:${title}`);
});
}
/**
* 创建汇报页面圆点导航,并为每个圆点绑定跳转行为。
* @returns {void} 只更新页面导航结构,不返回数据。
*/
function createDots() {
slides.forEach((_, index) => {
const dot = document.createElement('button');
dot.type = 'button';
dot.className = 'dot';
dot.setAttribute('aria-label', `跳转到第 ${index + 1} 页`);
dot.addEventListener('click', () => setSlide(index));
dotsContainer.appendChild(dot);
});
}
/**
* 切换到指定汇报页面,并同步进度条、圆点状态与翻页按钮状态。
* @param {number} index 目标页面的零基索引,超出范围时会自动限制到有效区间。
* @returns {void} 只更新当前页面状态,不返回数据。
*/
function setSlide(index) {
activeSlideIndex = Math.max(0, Math.min(slides.length - 1, index));
slides.forEach((slide, slideIndex) => {
const isActive = slideIndex === activeSlideIndex;
slide.classList.toggle('is-active', isActive);
slide.setAttribute('aria-hidden', String(!isActive));
});
Array.from(dotsContainer.children).forEach((dot, dotIndex) => {
const isActive = dotIndex === activeSlideIndex;
dot.classList.toggle('is-active', isActive);
dot.setAttribute('aria-current', isActive ? 'page' : 'false');
});
progressBar.style.width = `${((activeSlideIndex + 1) / slides.length) * 100}%`;
previousButton.disabled = activeSlideIndex === 0;
nextButton.disabled = activeSlideIndex === slides.length - 1;
}
/**
* 按指定方向移动汇报页面。
* @param {number} offset 页面偏移量,正数向后,负数向前。
* @returns {void} 通过调用页面切换函数更新界面。
*/
function moveSlide(offset) {
setSlide(activeSlideIndex + offset);
}
/**
* 展示“收到 PLC 确认”的调试情景,说明确认后才允许执行文件删除。
* @returns {void} 只更新情景卡片的可视内容和按钮状态。
*/
function showAcknowledgedScenario() {
ackButton.classList.add('is-selected');
timeoutButton.classList.remove('is-selected');
scenarioStatus.textContent = '业务已确认';
scenarioResult.innerHTML = `
<span class="mini-label">情景 A · 收到确认</span>
<h3 class="teal">允许执行后续动作</h3>
<div class="result-line"><span>PLC 业务结果</span><b>已确认放行</b></div>
<div class="result-line"><span>产品文件</span><b>按配置安全删除</b></div>`;
}
/**
* 展示“PLC 确认超时”的调试情景,说明协议写入成功时仍需保留产品文件。
* @returns {void} 只更新情景卡片的可视内容和按钮状态。
*/
function showTimeoutScenario() {
timeoutButton.classList.add('is-selected');
ackButton.classList.remove('is-selected');
scenarioStatus.textContent = '确认超时';
scenarioResult.innerHTML = `
<span class="mini-label">情景 B · 确认超时</span>
<h3 class="orange">停止危险的后续动作</h3>
<div class="result-line"><span>PLC 业务结果</span><b>未得到确认</b></div>
<div class="result-line"><span>产品文件</span><b>保留,不删除</b></div>`;
}
previousButton.addEventListener('click', () => moveSlide(-1));
nextButton.addEventListener('click', () => moveSlide(1));
ackButton.addEventListener('click', showAcknowledgedScenario);
timeoutButton.addEventListener('click', showTimeoutScenario);
document.addEventListener('keydown', event => {
if (event.target instanceof HTMLButtonElement) return;
if (event.key === 'ArrowRight' || event.key === 'PageDown' || event.key === ' ') moveSlide(1);
if (event.key === 'ArrowLeft' || event.key === 'PageUp') moveSlide(-1);
if (event.key === 'Home') setSlide(0);
if (event.key === 'End') setSlide(slides.length - 1);
});
syncSlideMetadata();
createDots();
setSlide(0);
</script>
</body>
</html>