
    <|jx                    h    d Z ddlmZ ddlZddlZddlmZ dZdZdZ	d	Z
d
ZdZddZddZddZddZy)u  HTML 报告生成器 - Vue 3 + shadcn-vue 风格组件 + violet 主题

生成的静态页面使用:
- Vue 3 (global build CDN)
- Tailwind CSS (Play CDN) + shadcn-vue violet CSS 变量
- Chart.js v4 (折线图)
组件样式复刻 shadcn-vue: Card / Table / Badge / Button / Dialog
移动端适配: ≤640px 表格转卡片布局、弹窗接近全屏、深色模式持久化 (localStorage)
    )annotationsN   )configz/https://unpkg.com/vue@3/dist/vue.global.prod.jszhttps://cdn.tailwindcss.comzAhttps://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.jsa  
:root {
  --background: 0 0% 100%;
  --foreground: 224 71.4% 4.1%;
  --card: 0 0% 100%;
  --card-foreground: 224 71.4% 4.1%;
  --popover: 0 0% 100%;
  --popover-foreground: 224 71.4% 4.1%;
  --primary: 262.1 83.3% 57.8%;
  --primary-foreground: 210 20% 98%;
  --secondary: 220 14.3% 95.9%;
  --secondary-foreground: 220.9 39.3% 11%;
  --muted: 220 14.3% 95.9%;
  --muted-foreground: 220 8.9% 46.1%;
  --accent: 220 14.3% 95.9%;
  --accent-foreground: 220.9 39.3% 11%;
  --destructive: 0 84.2% 60.2%;
  --destructive-foreground: 210 20% 98%;
  --border: 220 13% 91%;
  --input: 220 13% 91%;
  --ring: 262.1 83.3% 57.8%;
  --radius: 0.5rem;
}
.dark {
  --background: 224 71.4% 4.1%;
  --foreground: 210 20% 98%;
  --card: 224 71.4% 4.1%;
  --card-foreground: 210 20% 98%;
  --popover: 224 71.4% 4.1%;
  --popover-foreground: 210 20% 98%;
  --primary: 263.4 70% 50.4%;
  --primary-foreground: 210 20% 98%;
  --secondary: 215 27.9% 16.9%;
  --secondary-foreground: 210 20% 98%;
  --muted: 215 27.9% 16.9%;
  --muted-foreground: 217.9 10.6% 64.9%;
  --accent: 215 27.9% 16.9%;
  --accent-foreground: 210 20% 98%;
  --destructive: 0 62.8% 30.6%;
  --destructive-foreground: 210 20% 98%;
  --border: 215 27.9% 16.9%;
  --input: 215 27.9% 16.9%;
  --ring: 263.4 70% 50.4%;
}
a  
tailwind.config = {
  darkMode: 'class',
  theme: {
    extend: {
      colors: {
        border: 'hsl(var(--border))',
        input: 'hsl(var(--input))',
        ring: 'hsl(var(--ring))',
        background: 'hsl(var(--background))',
        foreground: 'hsl(var(--foreground))',
        primary: { DEFAULT: 'hsl(var(--primary))', foreground: 'hsl(var(--primary-foreground))' },
        secondary: { DEFAULT: 'hsl(var(--secondary))', foreground: 'hsl(var(--secondary-foreground))' },
        destructive: { DEFAULT: 'hsl(var(--destructive))', foreground: 'hsl(var(--destructive-foreground))' },
        muted: { DEFAULT: 'hsl(var(--muted))', foreground: 'hsl(var(--muted-foreground))' },
        accent: { DEFAULT: 'hsl(var(--accent))', foreground: 'hsl(var(--accent-foreground))' },
        popover: { DEFAULT: 'hsl(var(--popover))', foreground: 'hsl(var(--popover-foreground))' },
        card: { DEFAULT: 'hsl(var(--card))', foreground: 'hsl(var(--card-foreground))' },
      },
      borderRadius: {
        lg: 'var(--radius)',
        md: 'calc(var(--radius) - 2px)',
        sm: 'calc(var(--radius) - 4px)',
      },
      fontFamily: {
        sans: ['-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Roboto', 'Helvetica Neue', 'Microsoft YaHei', 'sans-serif'],
      },
    }
  }
}
u  
/* shadcn-vue 组件样式复刻 - 原生 CSS */
.card { border-radius: var(--radius); border: 1px solid hsl(var(--border)); background: hsl(var(--card)); color: hsl(var(--card-foreground)); box-shadow: 0 1px 2px 0 rgba(0,0,0,0.05); }
.card-header { display: flex; flex-direction: column; gap: 0.375rem; padding: 1.5rem; }
.card-title { font-size: 1.5rem; font-weight: 600; line-height: 1; letter-spacing: -0.025em; }
.card-description { font-size: 0.875rem; color: hsl(var(--muted-foreground)); }
.card-content { padding: 1.5rem; padding-top: 0; }
.btn { display: inline-flex; align-items: center; justify-content: center; border-radius: calc(var(--radius) - 2px); font-size: 0.875rem; font-weight: 500; transition: background-color 0.15s; outline: none; cursor: pointer; }
.btn-default { background: hsl(var(--primary)); color: hsl(var(--primary-foreground)); height: 2.5rem; padding: 0.5rem 1rem; }
.btn-default:hover { background: hsl(var(--primary) / 0.9); }
.btn-outline { border: 1px solid hsl(var(--input)); background: hsl(var(--background)); height: 2.5rem; padding: 0.5rem 1rem; }
.btn-outline:hover { background: hsl(var(--accent)); color: hsl(var(--accent-foreground)); }
.btn-ghost { height: 2.5rem; padding: 0 1rem; }
.btn-ghost:hover { background: hsl(var(--accent)); color: hsl(var(--accent-foreground)); }
.badge { display: inline-flex; align-items: center; border-radius: 9999px; border: 1px solid transparent; padding: 0.125rem 0.625rem; font-size: 0.75rem; font-weight: 600; }
.badge-default { background: hsl(var(--primary)); color: hsl(var(--primary-foreground)); }
.badge-default:hover { background: hsl(var(--primary) / 0.8); }
.badge-secondary { background: hsl(var(--secondary)); color: hsl(var(--secondary-foreground)); }
.badge-secondary:hover { background: hsl(var(--secondary) / 0.8); }
.badge-outline { color: hsl(var(--foreground)); border-color: hsl(var(--border)); }
/* Table */
.table-wrapper { width: 100%; overflow: auto; border: 1px solid hsl(var(--border)); border-radius: calc(var(--radius) - 2px); -webkit-overflow-scrolling: touch; }
.table { width: 100%; font-size: 0.875rem; border-collapse: collapse; table-layout: auto; }
/* 表头吸顶（长列表滚动时始终可见） */
.table-header { position: sticky; top: 0; z-index: 1; }
.table-header [tr], .table-header tr { border-bottom: 1px solid hsl(var(--border)); }
.table-body tr:last-child { border-bottom: 0; }
.table-row { border-bottom: 1px solid hsl(var(--border)); transition: background-color 0.15s; }
.table-row:hover { background: hsl(var(--muted) / 0.5); }
.table-head { height: 3rem; padding: 0 1rem; text-align: left; vertical-align: middle; font-weight: 500; color: hsl(var(--muted-foreground)); border: 1px solid hsl(var(--border)); background: hsl(var(--muted) / 0.3); }
.table-cell { padding: 0.75rem 1rem; vertical-align: middle; border: 1px solid hsl(var(--border)); }
/* Dialog */
.dialog-overlay { position: fixed; inset: 0; z-index: 50; background: rgba(0,0,0,0.8); }
.dialog-content { position: fixed; left: 50%; top: 50%; z-index: 50; transform: translate(-50%, -50%); width: 100%; max-width: 42rem; display: grid; gap: 1rem; border: 1px solid hsl(var(--border)); background: hsl(var(--background)); padding: 1.5rem; box-shadow: 0 10px 15px -3px rgba(0,0,0,0.1); border-radius: var(--radius); max-height: 85vh; overflow-y: auto; }
.dialog-title { font-size: 1.125rem; font-weight: 600; line-height: 1; letter-spacing: -0.025em; }
.dialog-close { position: absolute; right: 1rem; top: 1rem; border-radius: calc(var(--radius) - 4px); opacity: 0.7; transition: opacity 0.15s; cursor: pointer; background: transparent; border: none; font-size: 1.5rem; line-height: 1; }
.dialog-close:hover { opacity: 1; }
/* 链接 */
.link { color: hsl(var(--primary)); cursor: pointer; text-decoration: none; }
.link:hover { text-decoration: underline; }
/* 原文本滚动 */
.raw-text-scroll { max-height: 80px; overflow-y: auto; max-width: 480px; line-height: 1.5; font-size: 0.8125rem; color: hsl(var(--muted-foreground)); white-space: pre-wrap; word-break: break-all; padding: 4px; border-radius: 4px; background: hsl(var(--muted) / 0.3); }
/* 搜索框 */
.input-search { width: 100%; height: 2.5rem; padding: 0.5rem 0.75rem; border: 1px solid hsl(var(--input)); border-radius: calc(var(--radius) - 2px); background: hsl(var(--background)); color: hsl(var(--foreground)); font-size: 0.875rem; outline: none; transition: border-color 0.15s; }
.input-search:focus { border-color: hsl(var(--ring)); box-shadow: 0 0 0 2px hsl(var(--ring) / 0.2); }
/* 排序图标 */
.sort-icon { margin-left: 0.25rem; font-size: 0.75rem; color: hsl(var(--muted-foreground)); }
.table-head.sortable { cursor: pointer; user-select: none; }
.table-head.sortable:hover { color: hsl(var(--primary)); }
/* 工具类兼容 */
.text-muted-foreground { color: hsl(var(--muted-foreground)); }
.text-destructive { color: hsl(var(--destructive)); }
.text-primary { color: hsl(var(--primary)); }
.font-medium { font-weight: 500; }
.font-semibold { font-weight: 600; }
.text-sm { font-size: 0.875rem; }
.text-xs { font-size: 0.75rem; }
.text-center { text-align: center; }
.whitespace-nowrap { white-space: nowrap; }
.ml-1 { margin-left: 0.25rem; }
.mt-2 { margin-top: 0.5rem; }
.mt-4 { margin-top: 1rem; }
.mb-6 { margin-bottom: 1.5rem; }
.py-8 { padding-top: 2rem; padding-bottom: 2rem; }
/* ===== 移动端适配 (≤640px) ===== */
@media (max-width: 640px) {
  /* 表格转卡片布局：表头隐藏，每行一张卡片，单元格左侧显示字段名 */
  .table-wrapper { overflow: visible; border: 0; }
  .table thead { display: none; }
  .table tbody { display: block; }
  .table tr, .table td { display: block; width: 100%; }
  .table-row { margin-bottom: 0.75rem; border: 1px solid hsl(var(--border)); border-radius: calc(var(--radius) - 2px); padding: 0.5rem 0.75rem; }
  .table-row:hover { background: transparent; }
  .table-cell { display: flex; align-items: flex-start; justify-content: flex-start; gap: 1.25rem; padding: 0.5rem 0; border: 0; border-bottom: 1px dashed hsl(var(--border)); white-space: normal; text-align: left; font-size: 0.8125rem; }
  .table-cell:last-child { border-bottom: 0; }
  .table-cell::before { content: attr(data-label); flex-shrink: 0; min-width: 4.5rem; color: hsl(var(--muted-foreground)); font-size: 0.75rem; line-height: 1.5; text-align: left; }
  .table-cell > * { min-width: 0; }
  .table-cell .raw-text-scroll { max-width: 100%; }
  /* 弹窗接近全屏 */
  .dialog-content { width: calc(100% - 2rem); max-height: 80vh; }
  .dialog-close { padding: 0.375rem; }
  /* 避免 iOS 聚焦输入框自动放大 */
  .input-search { font-size: 1rem; }
  /* 卡片内边距收紧 */
  .card-header { padding: 1.25rem; }
  .card-content { padding: 1.25rem; }
}
c                    | yt        |       } | j                  dd      j                  dd      j                  dd      j                  dd	      S )
u   HTML 转义 &z&amp;<z&lt;>z&gt;"z&quot;)strreplace)ss    &/opt/daily-market/src/html_reporter.py_escr      sM    yAA99S'"**37??VLTTUXZbcc    c                  '()*+, t        j                  |d       t         j                  j                  |d      }t        j                  |d       | j	                  |      }t               }|D ]  }|d   s	|j                  |d           t        t        |      d      D ci c]  \  }}|t        |       }	}}|	j                         D ]  \  ,| j                  d      }
g }t               }g }|
D ]x  }|d   rt        j                  |d         ni }|j                         D ])  }||vs|j                  |       |j                  |       + |j                  |d	   |d
   |d       z |dd }|j                  d|i       t         j                  j                  |, d      }t!        |dd      5 }t        j"                  ||d       ddd        i }|D ]K  }|d   xs drt%        j'                               dk  r+|j)                  g       j                  |       M g }|j                         D ]   \  }d }d 'd}d}d(d)|D ]!  } ||      D ]  \  }}}||kD  s|}|(|)|} # t+        |'fd      } '|      }|xs |d   *t,        j.                  j1                  *t        *            +|	j1                  d      ,|du xs |d   |d   k(  } | s%|# ||      }!t+        d |!D        d      }"|"|k\  rd} ()*+,fd}#| r|j                   |#|             |j                   |#|             |j                   |#|             # t        j2                  |d      }$t5        ||$t%        |            }%t         j                  j                  |d      }&t!        |&dd      5 }|j7                  |%       ddd       |&S c c}}w # 1 sw Y   !xY w# 1 sw Y   |&S xY w) uh   生成当日静态页面 (Vue3 + shadcn-vue violet) + 各币种历史价格 JSON。返回页面路径。Texist_okchartsnamer      )daysprice_details
trade_date	ref_price)dater   detailsNdetail_keysz.jsonwutf-8encodingFensure_asciir   c                   | d   rt        j                  | d         ni }g }|j                         D ]@  \  }}|s		 t        |      }d|cxk  rdk  s!n $|j                  ||t        |      f       B |S # t        t
        f$ r Y Vw xY w)Nr   r   i )jsonloadsitemsfloat
ValueError	TypeErrorappendr   )recr   outkvfvs         r   
_prices_ofz'generate_daily_site.<locals>._prices_of   s    :=o:Ndjj_!56TVGC 01qB r#V#JJ2s1v/0 J	 #I. s   A77B	B	c                V    | d   xs d}	 t        |      S # t        t        f$ r Y yw xY w)Nquantity0        )r)   r*   r+   )r-   qs     r   _qty_ofz$generate_daily_site.<locals>._qty_of  s5    J&3AQx	* s   
 ((r6   c                     |       S N )rr8   s    r   <lambda>z%generate_daily_site.<locals>.<lambda>   s    wqz r   )keyboard_ididc              3  (   K   | ]
  \  }}}|  y wr:   r;   ).0_r1   s      r   	<genexpr>z&generate_daily_site.<locals>.<genexpr>-  s      ?2q ?s   )defaultc                f    | d   xs d| d   xs d| d   xs d| d   xs d| d   xs ddS )Nr4   r   quantity_unitraw_text	topic_urltopic_title)r   	max_pricemax_price_labelr4   rG   r?   board_labelrH   rI   rJ   chart_idr;   )r-   best_price_labelbest_price_strr?   rM   rN   r   s    r   	_make_rowz&generate_daily_site.<locals>._make_row1  s^    +#3
O1r!$_!5!;$*
O1r -3"=17R$ r   
index.html)osmakedirspathjoinfetch_prices_by_datesetadd	enumeratesortedr   r(   fetch_coin_historyr&   r'   keysr,   opendumplenstrip
setdefaultmaxr   TARGET_BOARDSgetdumps_build_daily_htmlwrite)-dbr   
output_dir
charts_dir	rows_data
coin_namesr<   ir   
name_to_idhistoryr   seenchart_pointshr   r/   combined	json_pathfgroups
table_datarecsr2   best_price_recbest_price_valr-   labelr1   svbest_qty_recbest_qty_valis_same
qty_pricesqty_max_pricerQ   page_data_jsonhtml	page_pathr8   rO   rP   r?   rM   rN   s-           `                              @@@@@@r   generate_daily_siter      s&   KK
T*j(3JKK
T*''
3I J &V9NN1V9%& /8z8JA.NO71d$A,OJO %**, 7h''2'6!# 
	A89/8Jdjj?!34PRG\\^ *D=HHQK&&q)* ,{^"! 
	  ?45GGLLz-?@	)S73 	7qIIh6	7 	7'7. !F .yBs4::<(A-$#**1-	.  Jlln K7
d		  	)C!+C )r2&%'N',$%'N%(N)	) 4%9:|,"2lJ?**..xXG>>$+ "T) A$T*l4.@@ 	 >5#L1J ?J ?MM.	 	 i56 i78i56WK7Z ZZ
?NZZIDZ6I	iw	/ 1	k P,	7 	7zs   *O$O*O7*O4	7Pc                t    d|  dt          dt         dt         dt         d|  d| dt         d	t
         d
| dS )u   构建当日 Vue 3 页面 HTMLu   <!DOCTYPE html>
<html lang="zh-CN" class="">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="theme-color" content="#ffffff">
<title>每日行情 - z</title>
<script src=""></script>
<script></script>
<style>
u_  
body { background-color: hsl(var(--background)); color: hsl(var(--foreground)); }
[v-cloak] { display: none; }
</style>
</head>
<body>
<div id="app" v-cloak>
  <div class="container mx-auto max-w-[1400px] px-4 py-8">
    <!-- 返回链接 -->
    <a href="../../site/index.html" class="btn btn-ghost mb-4">&larr; 返回首页</a>

    <!-- Card: 标题区 -->
    <div class="card mb-6">
      <div class="card-header">
        <div class="flex flex-wrap items-center justify-between gap-3">
          <div>
            <h1 class="card-title">每日行情</h1>
            <p class="card-description mt-1">u    · 共 u   条价格记录</p>
          </div>
          <button class="btn btn-outline" @click="toggleDark">
            {{ isDark ? '☀️ 浅色' : '🌙 深色' }}
          </button>
        </div>
      </div>
    </div>

    <!-- Card: 价格表格 -->
    <div class="card">
      <div class="card-header">
        <h2 class="text-xl font-semibold">价格列表</h2>
        <p class="text-sm text-muted-foreground">点击品种名称查看近期行情折线图 · 点击表头排序 · 每品种保留最高价与最大需求量</p>
      </div>
      <div class="card-content">
        <div class="mb-4">
          <input v-model="searchQuery" placeholder="搜索品种名称..." class="input-search">
          <p v-if="searchQuery" class="text-xs text-muted-foreground mt-2">匹配 {{ filteredRows.length }} / {{ rows.length }} 条</p>
        </div>
        <div class="table-wrapper">
          <table class="table">
            <thead class="table-header">
              <tr class="table-row">
                <th class="table-head sortable" @click="toggleSort('name')">品种/钞种 <span class="sort-icon">{{ sortIcon('name') }}</span></th>
                <th class="table-head sortable" @click="toggleSort('max_price')">最高价 <span class="sort-icon">{{ sortIcon('max_price') }}</span></th>
                <th class="table-head sortable" @click="toggleSort('quantity')">需求量 <span class="sort-icon">{{ sortIcon('quantity') }}</span></th>
                <th class="table-head">原文本</th>
                <th class="table-head">板块</th>
                <th class="table-head">来源</th>
              </tr>
            </thead>
            <tbody class="table-body">
              <tr v-for="(row, idx) in sortedRows" :key="idx" class="table-row">
                <td class="table-cell whitespace-nowrap" data-label="品种">
                  <span class="link" @click="showChart(row.name, row.chart_id)">{{ row.name }}</span>
                </td>
                <td class="table-cell whitespace-nowrap font-medium" data-label="最高价">
                  <span v-if="row.max_price">{{ row.max_price_label }} {{ row.max_price }}</span>
                  <span v-else class="text-muted-foreground">-</span>
                </td>
                <td class="table-cell whitespace-nowrap" data-label="需求量">
                  <span v-if="row.quantity">{{ row.quantity }}{{ row.quantity_unit }}</span>
                  <span v-else class="text-muted-foreground">-</span>
                </td>
                <td class="table-cell" data-label="原文本">
                  <div class="raw-text-scroll">{{ row.raw_text || '-' }}</div>
                </td>
                <td class="table-cell" data-label="板块">
                  <span :class="['badge', row.board_id === 10 ? 'badge-default' : 'badge-secondary']">
                    {{ row.board_label }}
                  </span>
                </td>
                <td class="table-cell text-center" data-label="来源">
                  <a v-if="row.topic_url" :href="row.topic_url" target="_blank" class="link" :title="row.topic_title || '查看原帖'">&nearr;</a>
                  <span v-else class="text-muted-foreground">-</span>
                </td>
              </tr>
              <tr v-if="sortedRows.length === 0">
                <td colspan="6" class="table-cell text-center text-muted-foreground py-8">暂无数据</td>
              </tr>
            </tbody>
          </table>
        </div>
      </div>
    </div>
  </div>

  <!-- Dialog: 折线图 -->
  <div v-if="chartVisible" class="dialog-overlay" @click.self="closeChart">
    <div class="dialog-content" role="dialog" aria-modal="true" :aria-label="chartTitle">
      <span class="dialog-close" aria-label="关闭" @click="closeChart">&times;</span>
      <h3 class="dialog-title">{{ chartTitle }}</h3>
      <div class="mt-4">
        <canvas ref="chartCanvas" style="max-height:400px;"></canvas>
        <p v-if="chartLoading" class="text-center text-muted-foreground py-8">加载中...</p>
        <p v-if="chartError" class="text-center text-destructive py-8">暂无 {{ chartTitle }} 的历史数据</p>
        <p v-if="chartInfo" class="text-center text-muted-foreground text-sm mt-2">{{ chartInfo }}</p>
      </div>
    </div>
  </div>
</div>

<script src="z"></script>
<script src="zw"></script>
<script>
const { createApp, ref, computed, nextTick } = Vue;

createApp({
  setup() {
    const rows = ref(u$  );
    // 深色模式：优先取 localStorage，其次跟随系统偏好
    const savedTheme = (() => { try { return localStorage.getItem('daily-market-theme'); } catch (e) { return null; } })();
    const isDark = ref(savedTheme === 'dark' || (savedTheme === null && window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches));
    const themeMeta = document.querySelector('meta[name="theme-color"]');
    if (isDark.value) document.documentElement.classList.add('dark');
    if (themeMeta) themeMeta.setAttribute('content', isDark.value ? '#0b0b12' : '#ffffff');
    const chartVisible = ref(false);
    const chartTitle = ref('');
    const chartLoading = ref(false);
    const chartError = ref(false);
    const chartInfo = ref('');
    const chartCanvas = ref(null);
    let priceChart = null;
    // 小屏图例放底部，避免横向挤压
    const legendPosition = window.innerWidth < 640 ? 'bottom' : 'top';

    // 搜索与排序
    const searchQuery = ref('');
    const sortKey = ref('');
    const sortOrder = ref('asc');

    const toggleSort = (key) => {
      if (sortKey.value === key) {
        sortOrder.value = sortOrder.value === 'asc' ? 'desc' : 'asc';
      } else {
        sortKey.value = key;
        sortOrder.value = 'asc';
      }
    };

    const sortIcon = (key) => {
      if (sortKey.value !== key) return '⇅';
      return sortOrder.value === 'asc' ? '▲' : '▼';
    };

    const filteredRows = computed(() => {
      const q = searchQuery.value.trim();
      if (!q) return rows.value;
      return rows.value.filter(r => (r.name || '').includes(q));
    });

    const sortedRows = computed(() => {
      if (!sortKey.value) return filteredRows.value;
      const key = sortKey.value;
      const dir = sortOrder.value === 'asc' ? 1 : -1;
      return [...filteredRows.value].sort((a, b) => {
        if (key === 'name') return (a.name || '').localeCompare(b.name || '') * dir;
        const va = parseFloat(a[key]) || 0;
        const vb = parseFloat(b[key]) || 0;
        return (va - vb) * dir;
      });
    });

    const toggleDark = () => {
      isDark.value = !isDark.value;
      document.documentElement.classList.toggle('dark', isDark.value);
      try { localStorage.setItem('daily-market-theme', isDark.value ? 'dark' : 'light'); } catch (e) {}
      if (themeMeta) themeMeta.setAttribute('content', isDark.value ? '#0b0b12' : '#ffffff');
    };

    const showChart = async (coinName, chartId) => {
      if (!chartId) {
        chartTitle.value = coinName;
        chartVisible.value = true;
        chartError.value = true;
        chartInfo.value = '';
        return;
      }
      chartTitle.value = coinName + ' 近期行情';
      chartVisible.value = true;
      chartLoading.value = true;
      chartError.value = false;
      chartInfo.value = '';
      await nextTick();
      try {
        const res = await fetch('charts/' + chartId + '.json');
        if (!res.ok) throw new Error('not found');
        const data = await res.json();
        chartLoading.value = false;
        const detailKeys = data[data.length - 1]?.detail_keys || [];
        const points = data.slice(0, -1);
        if (points.length === 0) {
          chartError.value = true;
          return;
        }
        if (points.length === 1) {
          chartInfo.value = '仅有当日数据，暂无历史趋势（需多日积累）';
        }
        const pointRadius = points.length <= 1 ? 6 : 3;
        const datasets = [{
          label: '参考价',
          data: points.map(d => d.ref_price ? parseFloat(d.ref_price) : null),
          borderColor: 'hsl(262.1, 83.3%, 57.8%)',
          backgroundColor: 'hsl(262.1, 83.3%, 57.8% / 0.1)',
          fill: true, tension: 0.3, spanGaps: true, borderWidth: 2,
          pointRadius: pointRadius, pointHoverRadius: pointRadius + 2
        }];
        const colors = ['hsl(142, 71%, 45%)', 'hsl(25, 95%, 53%)', 'hsl(330, 81%, 60%)', 'hsl(265, 89%, 78%)', 'hsl(187, 85%, 43%)'];
        detailKeys.forEach((key, i) => {
          datasets.push({
            label: key,
            data: points.map(d => (d.details && d.details[key]) ? parseFloat(d.details[key]) : null),
            borderColor: colors[i % colors.length],
            fill: false, tension: 0.3, spanGaps: true, borderWidth: 1.5, hidden: true,
            pointRadius: pointRadius, pointHoverRadius: pointRadius + 2
          });
        });
        if (priceChart) priceChart.destroy();
        priceChart = new Chart(chartCanvas.value.getContext('2d'), {
          type: 'line',
          data: { labels: points.map(d => d.date), datasets },
          options: {
            responsive: true,
            interaction: { mode: 'index', intersect: false },
            plugins: {
              legend: { position: legendPosition, labels: { usePointStyle: true } },
              tooltip: { mode: 'index', intersect: false }
            },
            scales: { y: { beginAtZero: false } }
          }
        });
      } catch (err) {
        chartLoading.value = false;
        chartError.value = true;
      }
    };

    const closeChart = () => {
      chartVisible.value = false;
      chartInfo.value = '';
      if (priceChart) { priceChart.destroy(); priceChart = null; }
    };

    document.addEventListener('keydown', e => { if (e.key === 'Escape') closeChart(); });

    return { rows, isDark, chartVisible, chartTitle, chartLoading, chartError, chartInfo, chartCanvas,
             searchQuery, sortKey, sortOrder, toggleSort, sortIcon, filteredRows, sortedRows,
             toggleDark, showChart, closeChart };
  }
}).mount('#app');
</script>
</body>
</html>)TAILWIND_CDNTAILWIND_CONFIGVIOLET_THEME_CSSCOMPONENT_STYLESVUE_CDNCHART_JS_CDN)r   r   counts      r   rg   rg   Q  s     "l #^ 		    ." /9\% SIf Y ^  %% O&kD Dr   c                   t        j                  |d       | j                         }g }|D ]0  }| j                  |      }|j	                  |t        |      d       2 t        j                  |d      }dt         dt         dt         d	t         d
t         d| d}t         j                  j                  |d      }t        |dd      5 }	|	j!                  |       ddd       |S # 1 sw Y   |S xY w)u?   生成站点首页：日期列表 (Vue3 + shadcn-vue violet)。Tr   )r   r   Fr#   u   <!DOCTYPE html>
<html lang="zh-CN" class="">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="theme-color" content="#ffffff">
<title>每日行情 - 日期索引</title>
<script src="r   r   r   ug  
body { background-color: hsl(var(--background)); color: hsl(var(--foreground)); }
[v-cloak] { display: none; }
</style>
</head>
<body>
<div id="app" v-cloak>
  <div class="container mx-auto max-w-3xl px-4 py-8 md:py-12">
    <div class="card">
      <div class="card-header">
        <div class="flex flex-wrap items-center justify-between gap-3">
          <div>
            <h1 class="card-title">每日行情</h1>
            <p class="card-description">PM001 论坛纪念币/钞收购信息汇总</p>
          </div>
          <button class="btn btn-outline" @click="toggleDark">
            {{ isDark ? '☀️ 浅色' : '🌙 深色' }}
          </button>
        </div>
      </div>
      <div class="card-content">
        <div v-if="dates.length === 0" class="text-center text-muted-foreground py-8">
          暂无数据，请先运行抓取
        </div>
        <div v-else class="space-y-2">
          <a v-for="item in dates" :key="item.date"
             :href="'../data/daily/' + item.date + '/index.html'"
             class="flex items-center justify-between rounded-lg border border-border p-4 transition-colors hover:bg-accent">
            <span class="text-lg font-medium">{{ item.date }}</span>
            <span class="badge badge-secondary">{{ item.count }} 条</span>
          </a>
        </div>
      </div>
    </div>
  </div>
</div>
<script src="zc"></script>
<script>
const { createApp, ref } = Vue;
createApp({
  setup() {
    const dates = ref(u  );
    // 深色模式：优先取 localStorage，其次跟随系统偏好
    const savedTheme = (() => { try { return localStorage.getItem('daily-market-theme'); } catch (e) { return null; } })();
    const isDark = ref(savedTheme === 'dark' || (savedTheme === null && window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches));
    const themeMeta = document.querySelector('meta[name="theme-color"]');
    if (isDark.value) document.documentElement.classList.add('dark');
    if (themeMeta) themeMeta.setAttribute('content', isDark.value ? '#0b0b12' : '#ffffff');
    const toggleDark = () => {
      isDark.value = !isDark.value;
      document.documentElement.classList.toggle('dark', isDark.value);
      try { localStorage.setItem('daily-market-theme', isDark.value ? 'dark' : 'light'); } catch (e) {}
      if (themeMeta) themeMeta.setAttribute('content', isDark.value ? '#0b0b12' : '#ffffff');
    };
    return { dates, isDark, toggleDark };
  }
}).mount('#app');
</script>
</body>
</html>rR   r   r    r!   N)rS   rT   list_trade_datesrW   r,   r`   r&   rf   r   r   r   r   r   rU   rV   r^   rh   )
ri   site_dirtrade_datesr(   drows
items_jsonr   rU   rv   s
             r   generate_site_indexr   Z  s   KK4(%%'KE 6&&q)a#d)456 E6J ^ 		    $H Y 
 "l #gEDN 77<<,/D	dC'	* a	KKs   C))C3)returnr   )r   r   rj   r   r   r   )r   r   r   r   r   intr   r   )r   r   r   r   )__doc__
__future__r   r&   rS   r   r   r   r   r   r   r   r   r   r   rg   r   r;   r   r   <module>r      s^    #  	  <,R, ^BW tdBJFRVr   