# Interaction Patterns / 交互模式

> Extracted from SKILL.md v2.1.0 — breadcrumb sync, time range selector, chart linkage, single-filter pattern, detail navigation, and all interaction logic.

---

## Breadcrumb A (Tab筛选路径) — always visible, blue #409EFF

```html
<nav class="breadcrumb-a" id="breadcrumb-a" aria-label="Tab筛选路径" role="navigation">
  <span class="breadcrumb-item breadcrumb-item-a root" data-level="root">集团</span>
  <span class="breadcrumb-separator">></span>
  <span class="breadcrumb-item breadcrumb-item-a active" data-level="active">全集团</span>
</nav>
```

**CSS:**
```css
.breadcrumb-a {
  background: #F8FAFC; padding: 8px 24px; height: 32px;
  display: flex; align-items: center; overflow-x: auto; white-space: nowrap;
}
.breadcrumb-item-a { color: #409EFF; font-size: 14px; cursor: pointer; }
.breadcrumb-item-a.active { font-weight: 600; }
.breadcrumb-separator { color: #9CA3AF; margin: 0 8px; }
```

---

## Breadcrumb B (组件下钻路径) — conditional display, green #67C23A

```html
<nav class="breadcrumb-b hidden" id="breadcrumb-b" aria-label="组件下钻路径" role="navigation">
  <div class="breadcrumb-trail-b" id="breadcrumb-trail-b">
    <span class="breadcrumb-item breadcrumb-item-b root-b" data-level="root">全集团</span>
    <span class="breadcrumb-separator breadcrumb-separator-b">></span>
    <span class="breadcrumb-item breadcrumb-item-b active" data-level="active">下钻条件</span>
  </div>
</nav>
```

**CSS:**
```css
.breadcrumb-b { background: #F8FAFC; padding: 8px 24px; height: 32px; display: flex; align-items: center; overflow-x: auto; white-space: nowrap; }
.breadcrumb-b.hidden { display: none; }
.breadcrumb-item-b { color: #67C23A; font-size: 14px; cursor: pointer; }
.breadcrumb-item-b.active { font-weight: 600; }
.breadcrumb-separator-b { color: #9CA3AF; margin: 0 8px; }
```

---

## Breadcrumb Interaction Rules

| Rule | Description |
|------|-------------|
| Breadcrumb A | Tab filter selection auto-updates, items blue #409EFF |
| Breadcrumb B | Chart click shows breadcrumb, items green #67C23A |
| Overflow | overflow-x: auto horizontal scroll |
| Reset | Click root node → Breadcrumb B hides |
| Mobile | <576px only shows last 2 levels + ellipsis |

**JS function signatures (MUST follow):**
```javascript
/**
 * @param {string[]} trail - e.g. ['集团', '全集团']
 * @returns {() => void} - cleanup function
 */
function updateBreadcrumbA(trail) { /* ... */ return function cleanup() { /* ... */ }; }

/**
 * @param {string[]} trail - e.g. ['全集团', '子公司A', '评审阶段']
 * @returns {() => void} - cleanup function
 */
function showDrilldownBreadcrumb(trail) { /* ... */ return function cleanup() { /* ... */ }; }
```

**Implementation:**
```javascript
function updateBreadcrumbA(trail) {
  const breadcrumb = document.getElementById('breadcrumb-a');
  if (!breadcrumb) return function() {};
  breadcrumb.innerHTML = '';
  trail.forEach((item, index) => {
    const span = document.createElement('span');
    span.className = 'breadcrumb-item breadcrumb-item-a';
    if (index === 0) span.classList.add('root');
    if (index === trail.length - 1) span.classList.add('active');
    span.dataset.level = index === 0 ? 'root' : (index === trail.length - 1 ? 'active' : 'intermediate');
    span.textContent = item;
    breadcrumb.appendChild(span);
    if (index < trail.length - 1) {
      const sep = document.createElement('span');
      sep.className = 'breadcrumb-separator'; sep.textContent = '>';
      breadcrumb.appendChild(sep);
    }
  });
  return function cleanup() {
    breadcrumb.innerHTML = '<span class="breadcrumb-item breadcrumb-item-a root">集团</span><span class="breadcrumb-separator">></span><span class="breadcrumb-item breadcrumb-item-a active">全集团</span>';
  };
}

function showDrilldownBreadcrumb(trail) {
  const breadcrumb = document.getElementById('breadcrumb-b');
  if (!breadcrumb) return function() {};
  breadcrumb.classList.remove('hidden');
  const trailContainer = document.getElementById('breadcrumb-trail-b');
  if (!trailContainer) return function() {};
  trailContainer.innerHTML = '';
  trail.forEach((item, index) => {
    const span = document.createElement('span');
    span.className = 'breadcrumb-item breadcrumb-item-b';
    if (index === 0) span.classList.add('root-b');
    if (index === trail.length - 1) span.classList.add('active');
    span.dataset.level = index === 0 ? 'root' : (index === trail.length - 1 ? 'active' : 'intermediate');
    span.textContent = item;
    span.addEventListener('click', function() {
      if (index === 0) {
        breadcrumb.classList.add('hidden');
        refreshKPIValues();
        refreshCharts();
      }
    });
    trailContainer.appendChild(span);
    if (index < trail.length - 1) {
      const sep = document.createElement('span');
      sep.className = 'breadcrumb-separator breadcrumb-separator-b'; sep.textContent = '>';
      trailContainer.appendChild(sep);
    }
  });
  return function cleanup() {
    breadcrumb.classList.add('hidden');
    trailContainer.innerHTML = '';
  };
}
```

---

## Interaction Rules (Rule 5)

| Interaction | Trigger Method | Function |
|-------------|---------------|----------|
| Company filter change | Global linkage | `updateBreadcrumbA()` + `refreshKPIValues()` + `refreshCharts()` |
| Ring chart sector click (ECharts) | In-page component linkage | `filterByStage()` → `showDrilldownBreadcrumb()` + `filterBarChart()` + `filterTable()` + `refreshKPIValues()` |
| Bar chart bar click (ECharts) | In-page component linkage | `filterByCompany()` → `updateBreadcrumbA()` + `showDrilldownBreadcrumb()` + `filterDoughnutChart()` + `filterTable()` + `refreshKPIValues()` |
| Line chart point click (ECharts) | In-page component linkage | `showDrilldownBreadcrumb()` + refresh |
| KPI card "详情→" | Standalone detail page (`<a>` tag) | `<a href="detail.html?indicator=IDX-xxx&company=...&year=...">` |
| Table row "详情→" | Standalone detail page (`<a>` tag) | `<a href="detail.html?project=N">` |
| Breadcrumb B root click | In-page reset | `resetAllFilters()` → hide breadcrumb B + restore ECharts |
| Data change flash | CSS transition | KPI and chart opacity transition (0.5s) |

---

## Component Linkage Matrix (Rule 5a)

| Source | Target | Effect |
|--------|--------|--------|
| Ring chart sector click (stage filter) | Bar chart + Table + Breadcrumb B | Bar chart filtered by stage; table filtered to that stage; breadcrumb shows drill path |
| Bar chart bar click (company filter) | Ring chart + Table + Breadcrumb A/B | Ring chart filtered by company; table filtered to that company; breadcrumb A updated |
| Breadcrumb B root click | All charts + KPI + Table | Reset all to default, hide breadcrumb B |

**Core implementation pattern:**
```javascript
var ORIGINAL_DATA = { doughnut: [], bar: [], table: [] };

function snapshotOriginalData() {
  var charts = window.__cockpitCharts;
  if (charts && charts.doughnut) ORIGINAL_DATA.doughnut = charts.doughnut.getOption().series[0].data.slice();
  if (charts && charts.bar) ORIGINAL_DATA.bar = charts.bar.getOption().series[0].data.slice();
  ORIGINAL_DATA.table = getAllTableRows();
}

function filterByStage(stageName) {
  var charts = window.__cockpitCharts;
  if (!charts) return;
  if (charts.bar) charts.bar.setOption({ series: [{ data: getBarDataByStage(stageName) }] });
  filterTableByStage(stageName);
  showDrilldownBreadcrumb(['全集团', stageName + '阶段']);
  refreshKPIValues();
}

function resetAllFilters() {
  var charts = window.__cockpitCharts;
  if (!charts) return;
  if (charts.doughnut && ORIGINAL_DATA.doughnut.length > 0)
    charts.doughnut.setOption({ series: [{ data: ORIGINAL_DATA.doughnut }] });
  if (charts.bar && ORIGINAL_DATA.bar.length > 0)
    charts.bar.setOption({ series: [{ data: ORIGINAL_DATA.bar }] });
  if (charts.line)
    charts.line.setOption({ series: [{ data: [8.2,9.5,7.8,10.1,8.9,11.2,9.6,10.8,8.5,11.5,9.2,12.3] }, { data: [6.5,7.2,6.8,8.5,7.3,9.1,8.0,8.9,7.1,9.5,7.8,10.1] }] });
  restoreTableRows(ORIGINAL_DATA.table);
  updateBreadcrumbA(['集团', '全集团']);
  document.getElementById('breadcrumb-b').classList.add('hidden');
  refreshKPIValues();
}

// Table filter helpers
function filterTableByStage(stageName) {
  document.querySelectorAll('.data-table tbody tr').forEach(function(row) {
    var cell = row.querySelector('td:nth-child(5)');
    if (cell) row.style.display = (cell.textContent.trim() === stageName) ? '' : 'none';
  });
}
```

**Reset link:**
```html
<a class="reset-overview-link" href="javascript:void(0)" onclick="resetAllFilters()">← 返回概览</a>
```
```css
.reset-overview-link { color: var(--breadcrumb-b-color); font-size: 14px; text-decoration: none; cursor: pointer; padding: 4px 12px; }
.reset-overview-link:hover { text-decoration: underline; }
```

---

## Detail Link Navigation Pattern

All "详情→" links MUST use standard `<a>` tags (**NOT button + onclick**), navigating to standalone `detail.html` via URL query string.

### URL Parameters

| Param | Type | Description | Source | Example |
|-------|------|-------------|--------|---------|
| `indicator` | string | Indicator ID (IDX-xxx) | KPI card `data-indicator` | `IDX-001` |
| `company` | string | Company filter (default `all`) | `company-filter` selected value | `all`, `子公司A` |
| `year` | string | Year (default current) | `time-filter` selected value | `2026` |
| `project` | string | Project ID (number) | Table row `data-row` | `1`, `2` |

### Link Format
```
KPI card: detail.html?indicator=IDX-001&company=all&year=2026
Table row: detail.html?project=1
```

### Detail Page Parameter Reading
```javascript
(function() {
  var params = new URLSearchParams(window.location.search);
  var indicator = params.get('indicator');
  var company = params.get('company');
  var year = params.get('year');
  var project = params.get('project');
  if (indicator) renderIndicatorDetail(indicator, company, year);
  else if (project) renderProjectDetail(project);
})();
```

### Key Rules
- **MUST use `<a>` tags**: href attribute constructs link directly
- **Parameters dynamically read**: company/year from current page filter values
- **Static-page compatible**: no routing framework needed

---

## Target Detail Page Layout

Indicator detail is NOT an inline panel — it navigates to standalone `detail.html` with these sections:

1. **KPI Overview**: current indicator value, change rate, trend arrow
2. **Trend Analysis**: 12-month SVG line chart
3. **Dimension Breakdown**: ring/bar chart by stage/industry/company
4. **Related Projects**: filtered project list table

---

## V12 Pattern 4: Breadcrumb Accumulation Pattern

APPEND segments via `drillPath.slice()` instead of replacing. Company selection resets the path.

```javascript
var drillPath = ['全集团'];
var currentCompany = 'all';

function drillTo(segment) {
  drillPath = drillPath.slice();
  drillPath.push(segment);
  syncAllBreadcrumbs();
}

document.getElementById('company-filter').addEventListener('change', function(e) {
  currentCompany = e.target.value;
  drillPath = [currentCompany === 'all' ? '全集团' : currentCompany];
  syncAllBreadcrumbs();
  resetAllCharts();
});

function syncAllBreadcrumbs() {
  updateBreadcrumbA(drillPath);
  var panelBreadcrumb = document.getElementById('panel-breadcrumb');
  if (panelBreadcrumb) renderPanelBreadcrumb(panelBreadcrumb, drillPath);
}
```

---

## V12 Pattern 6: Time Range Selector

Dual year-month dropdowns with "至今" (to present) default for end date.

```html
<div class="time-range-selector" id="time-range">
  <select id="start-year" aria-label="起始年份">
    <option value="2024" selected>2024</option><!-- ... -->
  </select>
  <select id="start-month" aria-label="起始月份">
    <option value="1">1月</option><!-- ... -->
  </select>
  <span class="time-separator">至</span>
  <select id="end-year" aria-label="截止年份">
    <option value="2026" selected>2026</option>
    <option value="present">至今</option>
  </select>
  <select id="end-month" aria-label="截止月份">
    <option value="6" selected>6月</option><!-- ... -->
  </select>
</div>
```

```javascript
function buildTimeRangeText() {
  var sy = document.getElementById('start-year').value;
  var sm = document.getElementById('start-month').value;
  var ey = document.getElementById('end-year').value;
  var em = document.getElementById('end-month').value;
  return sy + '-' + sm + ' 至 ' + (ey === 'present' ? '至今' : (ey + '-' + em));
}

function updateDataDateDisplay() {
  var el = document.querySelector('.data-date');
  if (el) el.textContent = '数据截止: ' + buildTimeRangeText();
}

['start-year','start-month','end-year','end-month'].forEach(function(id) {
  var el = document.getElementById(id);
  if (el) el.addEventListener('change', function() { updateDataDateDisplay(); refreshAllData(); });
});
```

---

## ECharts Global Init & Event Binding

```javascript
// Company filter change
document.getElementById('company-filter').addEventListener('change', function(e) {
  var trail = e.target.value === 'all' ? ['集团', '全集团'] : ['集团', e.target.value];
  updateBreadcrumbA(trail);
  refreshKPIValues();
  refreshCharts();
});

function initAllCharts() {
  window.__cockpitCharts = {
    doughnut: initDoughnutChart(),
    bar: initBarChart(),
    line: initLineChart()
  };
}

window.addEventListener('resize', function() {
  var charts = window.__cockpitCharts;
  if (!charts) return;
  if (charts.doughnut) charts.doughnut.resize();
  if (charts.bar) charts.bar.resize();
  if (charts.line) charts.line.resize();
});

document.addEventListener('DOMContentLoaded', initAllCharts);

// Flash effects
function refreshKPIValues() {
  var kpi = document.querySelector('.kpi-section');
  kpi.style.opacity = '0.5';
  setTimeout(function() { kpi.style.opacity = '1'; }, 500);
}
function refreshCharts() {
  var charts = document.querySelector('.chart-section');
  charts.style.opacity = '0.5';
  setTimeout(function() { charts.style.opacity = '1'; }, 500);
}
```
