feat(header): 顶栏时钟可切时区 + 汇率计算器;工作台移除运行日期

1) 工作台
   - 删掉欢迎区右侧的「运行日期」卡片,含 luxury.css 里对应样式与窄屏残留

2) 顶栏时钟(仿小满 CRM)
   - 新增 TimeZoneClock:时钟图标 +「东八区:北京,上海 ⌄」+ 大号时间(20px 等宽,
     tabular-nums 保证秒位不跳动),下方小字带日期
   - 点整块展开时区列表,23 个时区由西向东排列,每行右侧实时显示该地时间与 UTC 偏移
   - 时区用 IANA 名(Europe/London 等)而非写死偏移,夏令时自动正确;
     打开列表会自动滚到当前选中项;选择记 localStorage('happy.clockZone')
   - 新增 src/timezones.js 集中时区表与格式化工具

3) 顶栏汇率计算器
   - 新增 FxCalculator:计算器图标点开浮层,默认 USD→CNY
   - 左右两侧各带可搜索的币种下拉(币种来自接口返回的全部币种,常用币种排前),
     中间一键互换;任一框输入另一框实时反算;显示主汇率与倒数汇率
   - 换币种时锁住所改那一侧的数字(与 Google 换算一致)
   - 金额按数量级自动取精度(USD→VND 与 CNY→USD 都不出现一堆无意义的零)
   - 新增 src/currencies.js(币种中文名 + 排序 + 交叉汇率与格式化)

4) 后端汇率接口
   - 新增 /api/fx/rates:以 USD 为基准返回全量汇率,前端据此算任意币种交叉汇率
   - 主源 open.er-api.com(166 币种),备源 frankfurter.app(欧洲央行);
     内存 + 落盘缓存 1 小时,并发请求只发一次网络
   - 网络不可用时依次回退「上次成功的数据」→「内置离线汇率表」,
     并在响应里用 source/warning 标注,前端明确提示数据来源

5) 顶栏排布
   - 时钟变宽后中等屏会溢出,新增 ≤1440px 断点收紧留白/品牌区/搜索框;
     搜索框允许收缩;1280/1100/980/700 各档实测无横向溢出
   - 移动端(≤680px)汇率浮层改用固定定位铺满两侧,避免左侧被裁

6) .gitignore
   - backend/data/ 整体忽略(trade.db、fx-cache.json 等全是运行时产物)
This commit is contained in:
Drsakura
2026-09-10 17:44:36 +08:00
parent 1edf5ac995
commit 46a963e23d
12 changed files with 938 additions and 17 deletions
+72 -8
View File
@@ -228,7 +228,7 @@ input:focus-visible, select:focus-visible, textarea:focus-visible { outline: 2px
.header-status-dot { width: 9px; height: 9px; flex: 0 0 9px; border-radius: 50%; background: var(--success); box-shadow: 0 0 10px var(--success); }
.global-search { display: flex; align-items: center; gap: 9px; flex: 0 0 auto; width: min(360px, 30vw); min-height: 38px; padding: 0 11px; color: var(--text-tertiary); background: var(--surface-deep); border: 1px solid var(--border-light); border-radius: 11px; transition: border-color 150ms, box-shadow 150ms; }
.global-search:focus-within { border-color: var(--primary-border); box-shadow: 0 0 0 3px var(--accent-glow); }
.global-search input { min-height: 0; padding: 0; border: 0; background: transparent; box-shadow: none; color: var(--text-primary); }
.global-search input { min-width: 0; min-height: 0; padding: 0; border: 0; background: transparent; box-shadow: none; color: var(--text-primary); }
.global-search input:focus { box-shadow: none; }
.global-search kbd { margin-left: auto; padding: 2px 5px; color: var(--text-tertiary); background: var(--surface); border: 1px solid var(--border-light); border-radius: 5px; font: 10px var(--font-mono); }
/* ---- 已打开页面标签:搜索框后的快速跳转(参考 GoodJob 的顶部标签条) ---- */
@@ -245,8 +245,27 @@ input:focus-visible, select:focus-visible, textarea:focus-visible { outline: 2px
.page-tab.active .page-tab-close { color: var(--text-secondary); }
.page-tab.active .page-tab-close:hover { color: var(--danger); }
.global-header-actions { display: flex; align-items: center; gap: 9px; flex: 0 0 auto; margin-left: auto; }
.header-clock { display: flex; align-items: center; gap: 8px; margin-right: 8px; color: var(--text-tertiary); font: 11px var(--font-mono); white-space: nowrap; }
.header-clock b { color: var(--text-primary); font-size: 13px; font-weight: 500; }
.header-clock { position: relative; flex: 0 0 auto; }
.clock-trigger { display: flex; align-items: center; gap: 8px; padding: 5px 10px; border: 1px solid transparent; border-radius: 11px; background: transparent; color: var(--text-tertiary); cursor: pointer; white-space: nowrap; transition: background 150ms, border-color 150ms; }
.clock-trigger:hover, .clock-trigger.is-open { background: var(--surface-hover); border-color: var(--border-light); }
.clock-trigger > svg:first-child { flex: 0 0 auto; }
.clock-zone { color: var(--text-secondary); font-size: 11.5px; letter-spacing: .01em; }
.clock-caret { flex: 0 0 auto; color: var(--text-tertiary); transition: transform 180ms; }
.clock-trigger.is-open .clock-caret { transform: rotate(180deg); }
.clock-readout { display: grid; justify-items: end; gap: 2px; }
.clock-time { color: var(--text-primary); font: 600 20px/1 var(--font-mono); font-variant-numeric: tabular-nums; letter-spacing: .01em; }
.clock-date { color: var(--text-tertiary); font-size: 10px; font-style: normal; line-height: 1; }
.clock-pop { position: absolute; top: calc(100% + 9px); right: 0; z-index: 60; width: 318px; padding: 6px; border: 1px solid var(--border); border-radius: 14px; background: var(--surface); box-shadow: 0 20px 52px rgba(0, 0, 0, .25), inset 0 1px 0 var(--panel-highlight); }
.clock-pop-head { padding: 8px 10px 7px; color: var(--text-tertiary); font-size: 10px; letter-spacing: .1em; }
.clock-pop-list { max-height: 344px; overflow-y: auto; }
.clock-zone-item { display: flex; align-items: center; justify-content: space-between; gap: 10px; width: 100%; padding: 7px 10px; border: 0; border-radius: 8px; background: transparent; color: var(--text-secondary); cursor: pointer; font-size: 12px; text-align: left; transition: background 120ms, color 120ms; }
.clock-zone-item:hover { background: var(--surface-hover); color: var(--text-primary); }
.clock-zone-item.active { background: var(--primary-bg); color: var(--text-primary); }
.clock-zone-name { white-space: nowrap; }
.clock-zone-right { display: flex; flex: 0 0 auto; align-items: baseline; gap: 7px; }
.clock-zone-right b { color: var(--text-primary); font: 500 12px var(--font-mono); font-variant-numeric: tabular-nums; }
.clock-zone-item.active .clock-zone-right b { color: var(--primary-light); }
.clock-zone-right em { color: var(--text-tertiary); font-size: 9.5px; font-style: normal; }
.quick-create-btn, .ai-entry-btn, .mobile-search-btn { display: inline-flex; align-items: center; gap: 7px; flex: 0 0 auto; min-height: 36px; padding: 8px 11px; border: 1px solid transparent; border-radius: 10px; background: transparent; color: var(--text-secondary); cursor: pointer; font-size: 12px; white-space: nowrap; transition: background 150ms, border-color 150ms, color 150ms; }
.quick-create-btn:hover, .mobile-search-btn:hover { color: var(--text-primary); background: var(--surface-hover); border-color: var(--border-light); }
.ai-entry-btn { color: var(--primary-light); background: var(--primary-bg); border-color: var(--primary-border); box-shadow: 0 0 18px var(--accent-glow); }
@@ -259,9 +278,6 @@ input:focus-visible, select:focus-visible, textarea:focus-visible { outline: 2px
.dashboard-welcome { display: flex; align-items: flex-end; justify-content: space-between; gap: 18px; margin-bottom: 22px; }
.dashboard-welcome .page-title { margin: 0; }
.dashboard-welcome p { margin: 7px 0 0; color: var(--text-secondary); font-size: 13px; }
.dashboard-date-chip { display: grid; gap: 5px; min-width: 150px; padding: 12px 14px; border-radius: 12px; background: var(--surface); box-shadow: var(--panel-shadow); text-align: right; }
.dashboard-date-chip span { color: var(--text-tertiary); font: 10px var(--font-mono); letter-spacing: .08em; }
.dashboard-date-chip strong { color: var(--text-primary); font-size: 13px; font-weight: 600; }
.dashboard-panel { margin-bottom: 18px; padding: 20px; border-radius: 16px; background: var(--surface); box-shadow: var(--panel-shadow); }
.dashboard-panel-header { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-bottom: 16px; }
.dashboard-panel-header h2 { display: flex; align-items: center; gap: 8px; margin: 0; color: var(--text-primary); font-size: 15px; }
@@ -329,9 +345,57 @@ input:focus-visible, select:focus-visible, textarea:focus-visible { outline: 2px
.ai-message-list { display: grid; gap: 13px; margin-top: 14px; }.ai-message { display: flex; align-items: flex-start; gap: 8px; }.ai-message.user { flex-direction: row-reverse; }.ai-message-avatar { display: grid; place-items: center; width: 25px; height: 25px; flex: 0 0 25px; border-radius: 9px; background: var(--surface-deep); color: var(--primary-light); font-size: 10px; font-weight: 800; }.ai-message.user .ai-message-avatar { color: var(--surface-deep); background: var(--primary); }.ai-message-content { max-width: 82%; padding: 9px 11px; border-radius: 4px 13px 13px 13px; background: var(--surface-deep); }.ai-message.user .ai-message-content { border-radius: 13px 4px 13px 13px; background: var(--primary-bg); }.ai-message-content p { margin: 0; color: var(--text-secondary); font-size: 12px; line-height: 1.7; }.ai-message-content small { display: block; margin-top: 5px; color: var(--text-tertiary); font: 9px var(--font-mono); }.ai-thinking { display: flex; gap: 4px; align-items: center; min-height: 34px; }.ai-thinking span { width: 5px; height: 5px; border-radius: 50%; background: var(--primary); animation: ai-bounce 1s infinite ease-in-out; }.ai-thinking span:nth-child(2) { animation-delay: .15s; }.ai-thinking span:nth-child(3) { animation-delay: .3s; }@keyframes ai-bounce { 0%,80%,100% { transform: translateY(0); opacity:.45; } 40% { transform: translateY(-4px); opacity:1; } }
.ai-suggestion-row { display: flex; gap: 7px; padding: 8px 20px; overflow-x: auto; }.ai-suggestion-row button { flex: 0 0 auto; padding: 6px 9px; border: 1px solid var(--border-light); border-radius: 99px; background: var(--surface-deep); color: var(--text-secondary); cursor: pointer; font-size: 10px; }.ai-suggestion-row button:hover { color: var(--primary-light); border-color: var(--primary-border); }
.ai-input-row { display: flex; gap: 8px; padding: 10px 16px; border-top: 1px solid var(--border-light); }.ai-input-row input { min-height: 40px; background: var(--surface-deep); }.ai-input-row button { display: grid; place-items: center; width: 40px; min-width: 40px; border: 0; border-radius: 11px; background: var(--primary); color: var(--surface-deep); cursor: pointer; }.ai-input-row button:disabled { cursor: not-allowed; opacity: .4; }.ai-panel-footer { padding: 8px 20px 13px; color: var(--text-tertiary); font-size: 10px; text-align: center; }
/* ---------- 汇率计算器(顶栏) ---------- */
.fx-calc { position: relative; flex: 0 0 auto; }
.header-tool-btn { display: grid; place-items: center; width: 36px; height: 36px; padding: 0; border: 1px solid transparent; border-radius: 10px; background: transparent; color: var(--text-secondary); cursor: pointer; transition: background 150ms, border-color 150ms, color 150ms; }
.header-tool-btn:hover { color: var(--text-primary); background: var(--surface-hover); border-color: var(--border-light); }
.header-tool-btn.is-open { color: var(--primary-light); background: var(--primary-bg); border-color: var(--primary-border); }
.fx-panel { position: absolute; top: calc(100% + 9px); right: 0; z-index: 60; width: min(396px, calc(100vw - 24px)); padding: 14px; border: 1px solid var(--border); border-radius: 16px; background: var(--surface); box-shadow: 0 20px 52px rgba(0, 0, 0, .25), inset 0 1px 0 var(--panel-highlight); }
.fx-panel-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; }
.fx-panel-title { display: inline-flex; align-items: center; gap: 7px; color: var(--text-primary); font-size: 12.5px; font-weight: 600; }
.fx-refresh { display: grid; place-items: center; width: 26px; height: 26px; padding: 0; border: 1px solid var(--border-light); border-radius: 8px; background: transparent; color: var(--text-tertiary); cursor: pointer; transition: color 150ms, border-color 150ms; }
.fx-refresh:hover { color: var(--primary-light); border-color: var(--primary-border); }
.fx-refresh:disabled { opacity: .45; cursor: default; }
.fx-panel-body { display: flex; align-items: center; gap: 10px; }
.fx-side { position: relative; flex: 1 1 0; min-width: 0; display: grid; gap: 6px; }
.fx-amount { width: 100%; height: 44px; padding: 0 12px; border: 1px solid var(--border-light); border-radius: 11px; background: var(--surface-deep); color: var(--text-primary); font: 600 17px var(--font-mono); font-variant-numeric: tabular-nums; text-align: right; }
.fx-amount:focus { outline: none; border-color: var(--primary-border); box-shadow: 0 0 0 3px var(--primary-bg); }
.fx-swap { display: grid; place-items: center; flex: 0 0 auto; width: 34px; height: 34px; padding: 0; border: 1px solid var(--border-light); border-radius: 50%; background: var(--surface-deep); color: var(--text-secondary); cursor: pointer; transition: color 150ms, border-color 150ms, transform 200ms; }
.fx-swap:hover { color: var(--primary-light); border-color: var(--primary-border); transform: rotate(180deg); }
.fx-picker { position: relative; }
.fx-picker-btn { display: flex; align-items: center; gap: 6px; width: 100%; height: 34px; padding: 0 9px; border: 1px solid var(--border-light); border-radius: 10px; background: var(--surface-deep); color: var(--text-secondary); cursor: pointer; transition: border-color 150ms, color 150ms; }
.fx-picker-btn:hover, .fx-picker-btn.is-open { border-color: var(--primary-border); color: var(--text-primary); }
.fx-picker-btn b { flex: 0 0 auto; color: var(--text-primary); font-size: 12.5px; font-weight: 600; letter-spacing: .02em; }
.fx-picker-btn span { flex: 1 1 auto; overflow: hidden; color: var(--text-tertiary); font-size: 11px; text-align: left; text-overflow: ellipsis; white-space: nowrap; }
.fx-picker-btn svg { flex: 0 0 auto; color: var(--text-tertiary); }
.fx-picker-pop { position: absolute; top: calc(100% + 5px); z-index: 5; width: 232px; padding: 6px; border: 1px solid var(--border); border-radius: 12px; background: var(--surface); box-shadow: 0 20px 52px rgba(0, 0, 0, .25), inset 0 1px 0 var(--panel-highlight); }
.fx-picker.left .fx-picker-pop { left: 0; }
.fx-picker.right .fx-picker-pop { right: 0; }
.fx-picker-search { width: 100%; height: 30px; margin-bottom: 5px; padding: 0 9px; border: 1px solid var(--border-light); border-radius: 8px; background: var(--surface-deep); color: var(--text-primary); font-size: 11.5px; }
.fx-picker-search:focus { outline: none; border-color: var(--primary-border); }
.fx-picker-list { max-height: 220px; overflow-y: auto; }
.fx-picker-item { display: flex; align-items: baseline; gap: 7px; width: 100%; padding: 6px 9px; border: 0; border-radius: 7px; background: transparent; color: var(--text-secondary); cursor: pointer; text-align: left; }
.fx-picker-item:hover { background: var(--surface-hover); color: var(--text-primary); }
.fx-picker-item.active { background: var(--primary-bg); }
.fx-picker-item b { flex: 0 0 auto; color: var(--text-primary); font-size: 12px; font-weight: 600; }
.fx-picker-item.active b { color: var(--primary-light); }
.fx-picker-item span { overflow: hidden; font-size: 11px; text-overflow: ellipsis; white-space: nowrap; }
.fx-picker-empty { padding: 12px 9px; color: var(--text-tertiary); font-size: 11px; text-align: center; }
.fx-rate-line { margin-top: 12px; padding-top: 11px; border-top: 1px solid var(--border-light); color: var(--text-secondary); font-size: 12px; }
.fx-rate-line b { color: var(--primary-light); font-family: var(--font-mono); font-size: 13px; }
.fx-rate-line em { margin-left: 7px; color: var(--text-tertiary); font-size: 10.5px; font-style: normal; }
.fx-error, .fx-warn { color: var(--warning); }
.fx-panel-foot { margin-top: 7px; color: var(--text-tertiary); font-size: 10px; }
/* 时钟与汇率入口加进来后顶栏变宽,中等屏先收紧留白与品牌区,避免把右侧按钮挤出去 */
@media (max-width: 1440px) {
.global-header { gap: 16px; padding: 0 clamp(14px, 2.2vw, 34px); }
.global-header-brand { min-width: 196px; }
.global-search { flex: 0 1 auto; width: min(340px, 26vw); min-width: 148px; }
.global-header-actions { gap: 7px; }
}
@media (max-width: 1280px) { .page-tabs { display: none; } }
@media (max-width: 980px) { .global-header-brand { min-width: auto; }.global-header-brand div { display: none; }.global-search { width: min(360px, 40vw); }.header-clock span { display: none; }.dashboard-content-grid { grid-template-columns: 1fr; }.dashboard-secondary-column { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 18px; }.dashboard-secondary-column .dashboard-panel { margin-bottom: 0; } }
@media (max-width: 680px) { :root { --header-height: 62px; }.global-header { gap: 8px; padding: 0 12px; }.global-search { display: none; }.global-search.is-open { position: absolute; left: 12px; right: 12px; top: 65px; display: flex; width: auto; background: var(--surface); }.mobile-search-btn { display: inline-flex; }.quick-create-btn span, .ai-entry-btn span, .header-clock { display: none; }.ai-entry-btn { padding: 8px; }.route-content { min-height: calc(100vh - var(--header-height)); padding: 22px 12px 42px; }.dashboard-welcome { align-items: flex-start; flex-direction: column; }.dashboard-date-chip { text-align: left; }.calendar-day { min-height: 68px; padding: 5px 4px; }.calendar-week-row > div { padding: 8px 4px; }.calendar-event { font-size: 8px; }.calendar-day-number { font-size: 10px; }.dashboard-secondary-column { display: block; }.dashboard-secondary-column .dashboard-panel { margin-bottom: 18px; }.ai-assistant-panel { top: 8px; right: 8px; bottom: 8px; width: calc(100vw - 16px); } }
@media (max-width: 980px) { .global-header-brand { min-width: auto; }.global-header-brand div { display: none; }.global-search { width: min(360px, 40vw); }.clock-zone, .clock-caret { display: none; }.dashboard-content-grid { grid-template-columns: 1fr; }.dashboard-secondary-column { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 18px; }.dashboard-secondary-column .dashboard-panel { margin-bottom: 0; } }
@media (max-width: 680px) { :root { --header-height: 62px; }.global-header { gap: 8px; padding: 0 12px; }.global-search { display: none; }.global-search.is-open { position: absolute; left: 12px; right: 12px; top: 65px; display: flex; width: auto; background: var(--surface); }.mobile-search-btn { display: inline-flex; }.quick-create-btn span, .ai-entry-btn span, .header-clock { display: none; }.ai-entry-btn { padding: 8px; }.fx-panel { position: fixed; top: calc(var(--header-height) + 8px); left: 12px; right: 12px; width: auto; max-height: calc(100vh - var(--header-height) - 24px); overflow-y: auto; }.route-content { min-height: calc(100vh - var(--header-height)); padding: 22px 12px 42px; }.dashboard-welcome { align-items: flex-start; flex-direction: column; }.calendar-day { min-height: 68px; padding: 5px 4px; }.calendar-week-row > div { padding: 8px 4px; }.calendar-event { font-size: 8px; }.calendar-day-number { font-size: 10px; }.dashboard-secondary-column { display: block; }.dashboard-secondary-column .dashboard-panel { margin-bottom: 18px; }.ai-assistant-panel { top: 8px; right: 8px; bottom: 8px; width: calc(100vw - 16px); } }
/* Requested compact navigation controls. */
.collapse-btn { display: grid; place-items: center; width: 32px; height: 32px; padding: 0; border: 1px solid var(--sb-border); border-radius: 8px; background: transparent; color: var(--sb-text); box-shadow: none; }