好动作 希望CL可以加入官方适配
GPT老师,改自Scriptable脚本。

<!doctype html><html lang="zh-CN"><head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no" /> <title>燕幕 · 美元汇率</title> <style> :root{ --bg0:#081018; --bg1:#0c1421; --bg2:#111b2c; --line:rgba(185,220,255,.16); --line2:rgba(133,184,255,.12); --text:#e8f2ff; --muted:rgba(232,242,255,.66); --soft:rgba(255,255,255,.04); --soft2:rgba(255,255,255,.06); --accent:#ffb1df; --orange:#ffbe6b; --red:#ff6f7f; --blue:#7db5ff; --green:#7fe3bb; --shadow:0 18px 40px rgba(0,0,0,.34); --radius:18px; }
*{ box-sizing:border-box; } html,body{ margin:0; width:100%; height:100%; overflow:hidden; background:transparent; font-family:"Microsoft YaHei",sans-serif; color:var(--text); -webkit-font-smoothing:antialiased; text-rendering:optimizeLegibility; }
.card{ position:relative; width:100%; height:100%; overflow:hidden; border-radius:var(--radius); border:1px solid var(--line); box-sizing:border-box; background: radial-gradient(120% 120% at 0% 0%, rgba(117,163,255,.16) 0%, rgba(117,163,255,0) 45%), radial-gradient(90% 90% at 100% 0%, rgba(255,160,214,.12) 0%, rgba(255,160,214,0) 52%), linear-gradient(180deg, #101a2a 0%, #0d1523 42%, #0a111b 100%); box-shadow: inset 0 1px 0 rgba(255,255,255,.08), inset 0 -1px 0 rgba(255,255,255,.03), 0 0 0 1px rgba(255,255,255,.02), var(--shadow); }
.card::before{ content:""; position:absolute; inset:1px; border-radius:calc(var(--radius) - 1px); pointer-events:none; background: linear-gradient(180deg, rgba(255,255,255,.09), rgba(255,255,255,0) 12%); mask:linear-gradient(#000,#000); opacity:.55; }
.card::after{ content:""; position:absolute; inset:-40% -20% auto auto; width:170px; height:170px; pointer-events:none; background:radial-gradient(circle, rgba(150,200,255,.11), rgba(150,200,255,0) 70%); filter:blur(2px); opacity:.9; }
.wrap{ position:relative; z-index:1; width:100%; height:100%; display:flex; flex-direction:column; padding:12px 12px 10px; gap:10px; min-width:0; min-height:0; }
.topbar{ display:flex; align-items:flex-start; justify-content:space-between; gap:10px; min-width:0; }
.titleBox{ display:flex; flex-direction:column; gap:4px; min-width:0; flex:1; }
.titleRow{ display:flex; align-items:center; gap:8px; min-width:0; }
.badge{ flex:0 0 auto; width:8px; height:8px; border-radius:999px; background:linear-gradient(180deg, #ffd2ee, #ff8cc7); box-shadow:0 0 0 4px rgba(255,143,199,.12); margin-top:2px; }
.title{ font-size:16px; font-weight:700; letter-spacing:.2px; color:var(--accent); line-height:1.1; user-select:none; cursor:grab; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.subtitle{ font-size:11px; color:var(--muted); line-height:1.35; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.actions{ display:flex; align-items:center; gap:8px; flex:0 0 auto; }
.btn{ appearance:none; border:1px solid var(--line2); background:linear-gradient(180deg, rgba(255,255,255,.08), rgba(255,255,255,.03)); color:var(--text); border-radius:10px; height:30px; padding:0 10px; display:inline-flex; align-items:center; justify-content:center; gap:6px; font-size:12px; line-height:1; cursor:pointer; user-select:none; transition:transform .12s ease, background .12s ease, border-color .12s ease, box-shadow .12s ease, opacity .12s ease; box-shadow:inset 0 1px 0 rgba(255,255,255,.06); } .btn:hover{ background:linear-gradient(180deg, rgba(255,255,255,.12), rgba(255,255,255,.05)); border-color:rgba(150,200,255,.26); box-shadow:0 8px 18px rgba(0,0,0,.16), inset 0 1px 0 rgba(255,255,255,.08); } .btn:active{ transform:translateY(1px) scale(.99); background:linear-gradient(180deg, rgba(255,255,255,.06), rgba(255,255,255,.02)); border-color:rgba(150,200,255,.18); } .btn.primary{ border-color:rgba(125,181,255,.34); background:linear-gradient(180deg, rgba(125,181,255,.20), rgba(125,181,255,.08)); }
.searchRow{ display:flex; gap:8px; align-items:center; min-width:0; }
.input{ flex:1; min-width:0; height:32px; border-radius:11px; border:1px solid rgba(150,200,255,.16); background:rgba(255,255,255,.035); color:var(--text); padding:0 11px; outline:none; font-size:12px; box-shadow:inset 0 1px 0 rgba(255,255,255,.04); transition:border-color .12s ease, background .12s ease, box-shadow .12s ease; } .input::placeholder{ color:rgba(232,242,255,.38); } .input:hover{ border-color:rgba(150,200,255,.24); background:rgba(255,255,255,.05); } .input:focus{ border-color:rgba(125,181,255,.44); background:rgba(255,255,255,.07); box-shadow:0 0 0 3px rgba(125,181,255,.10), inset 0 1px 0 rgba(255,255,255,.05); }
.meta{ display:flex; align-items:center; justify-content:space-between; gap:8px; min-width:0; font-size:11px; color:var(--muted); }
.meta .status{ display:flex; align-items:center; gap:6px; min-width:0; } .dot{ width:7px; height:7px; border-radius:50%; background:var(--green); box-shadow:0 0 0 4px rgba(127,227,187,.12); flex:0 0 auto; } .statusText{ overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.list{ flex:1; min-height:0; overflow:auto; padding-right:2px; scrollbar-width:thin; scrollbar-color:rgba(154,188,232,.34) rgba(255,255,255,.04); } .list::-webkit-scrollbar{ width:8px; } .list::-webkit-scrollbar-track{ background:rgba(255,255,255,.04); border-radius:999px; } .list::-webkit-scrollbar-thumb{ background:rgba(154,188,232,.28); border-radius:999px; border:2px solid rgba(255,255,255,.03); } .list::-webkit-scrollbar-thumb:hover{ background:rgba(154,188,232,.42); }
.row{ display:grid; grid-template-columns:auto 1fr auto; gap:8px; align-items:center; min-width:0; padding:9px 10px; margin-bottom:7px; border-radius:13px; border:1px solid rgba(255,255,255,.06); background:linear-gradient(180deg, rgba(255,255,255,.05), rgba(255,255,255,.025)); box-shadow:inset 0 1px 0 rgba(255,255,255,.03); } .row:last-child{ margin-bottom:0; }
.flag{ font-size:16px; line-height:1; flex:0 0 auto; width:22px; text-align:center; filter:saturate(1.05); }
.currency{ min-width:0; display:flex; flex-direction:column; gap:2px; } .currency .name{ color:var(--orange); font-size:13px; font-weight:600; line-height:1.1; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; } .currency .code{ color:rgba(232,242,255,.46); font-size:10px; line-height:1.1; letter-spacing:.3px; }
.rate{ color:var(--red); font-size:14px; font-weight:700; letter-spacing:.2px; font-variant-numeric:tabular-nums; white-space:nowrap; }
.empty{ display:none; padding:16px 10px; text-align:center; color:rgba(232,242,255,.55); font-size:12px; border:1px dashed rgba(150,200,255,.16); border-radius:13px; background:rgba(255,255,255,.03); } .empty.show{ display:block; }
.footer{ display:flex; align-items:center; justify-content:space-between; gap:10px; min-width:0; font-size:11px; color:var(--muted); padding-top:2px; }
.footer .left, .footer .right{ min-width:0; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.footer .left b{ color:var(--blue); font-weight:600; }
.hint{ color:rgba(232,242,255,.42); }
.resize{ position:absolute; right:7px; bottom:7px; width:16px; height:16px; border-radius:5px; background: linear-gradient(135deg, transparent 0 42%, rgba(125,181,255,.22) 42% 55%, transparent 55% 100%), linear-gradient(135deg, transparent 0 62%, rgba(255,255,255,.12) 62% 72%, transparent 72% 100%); opacity:.9; cursor:nwse-resize; user-select:none; }
.dragArea{ display:inline-flex; align-items:center; gap:8px; min-width:0; }
.dragIcon{ flex:0 0 auto; width:11px; height:11px; border-radius:3px; border:1px solid rgba(255,177,223,.40); background:linear-gradient(180deg, rgba(255,177,223,.22), rgba(255,177,223,.06)); box-shadow:inset 0 1px 0 rgba(255,255,255,.08); }
.pulse{ display:inline-block; width:6px; height:6px; border-radius:50%; background:var(--green); margin-right:6px; vertical-align:middle; box-shadow:0 0 0 4px rgba(127,227,187,.10); }
.noSelect{ -webkit-user-select:none; user-select:none; }
@media (max-width: 280px){ .actions{ gap:6px; } .btn{ padding:0 8px; } .row{ grid-template-columns:auto 1fr; } .rate{ grid-column:2; justify-self:end; } } </style></head><body> <div class="card"> <div class="wrap"> <div class="topbar"> <div class="titleBox"> <div class="titleRow"> <span class="badge" aria-hidden="true"></span> <div class="dragArea"> <span class="title yanm-drag" data-yanm-drag="true">美元汇率</span> </div> </div> <div class="subtitle" id="subtitle">USD → CNY / EUR / JPY / GBP / AUD / SGD</div> </div> <div class="actions"> <button class="btn" id="btnReset" type="button">重置</button> <button class="btn primary" id="btnRefresh" type="button">刷新</button> </div> </div>
<div class="searchRow"> <input class="input" id="filter" type="text" inputmode="text" placeholder="搜索货币名称或代码…" /> <button class="btn" id="btnClear" type="button">清除</button> </div>
<div class="meta"> <div class="status"><span class="dot"></span><span class="statusText" id="statusText">准备就绪</span></div> <div class="hint" id="countText">6 项</div> </div>
<div class="list" id="list"></div> <div class="empty" id="empty">没有匹配的货币。</div>
<div class="footer"> <div class="left"><b>更新于</b> <span id="updatedAt">--</span></div> <div class="right" id="sourceText">来源:本地缓存 / 在线 API</div> </div> </div>
<div class="resize yanm-resize" data-yanm-resize="true" aria-hidden="true"></div> </div>
<script> (() => { const API_URL = "https://api.exchangerate-api.com/v4/latest/USD"; const STORAGE_KEY = "yanmu_fx_widget_state_v1"; const HOST_KEY = "yanmu_fx_widget_state_v1";
const CURRENCIES = { CNY: { name: "人民币", flag: "🇨🇳" }, EUR: { name: "欧元", flag: "🇪🇺" }, JPY: { name: "日元", flag: "🇯🇵" }, GBP: { name: "英镑", flag: "🇬🇧" }, AUD: { name: "澳元", flag: "🇦🇺" }, SGD: { name: "新元", flag: "🇸🇬" } };
const COLOR = { title: "#ffb1df", currency: "#ffbe6b", rate: "#ff6f7f", footer: "#7db5ff" };
const $ = (s) => document.querySelector(s); const listEl = $("#list"); const emptyEl = $("#empty"); const subtitleEl = $("#subtitle"); const updatedEl = $("#updatedAt"); const statusEl = $("#statusText"); const countEl = $("#countText"); const sourceEl = $("#sourceText"); const filterEl = $("#filter"); const btnRefresh = $("#btnRefresh"); const btnReset = $("#btnReset"); const btnClear = $("#btnClear");
const nowISO = () => new Date().toISOString(); const fmtTime = (iso) => { if (!iso) return "--"; const d = new Date(iso); if (Number.isNaN(d.getTime())) return "--"; const pad = (n) => String(n).padStart(2, "0"); return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`; };
const defaultRates = { CNY: 7.20, EUR: 0.92, JPY: 156.50, GBP: 0.79, AUD: 1.52, SGD: 1.34 };
const defaultState = { title: "美元汇率", query: "", rates: defaultRates, updatedAt: "", source: "本地缓存 / 在线 API", loading: false };
let state = loadState(); let renderTimer = 0;
function loadState() { try { const raw = localStorage.getItem(STORAGE_KEY); if (raw) { const parsed = JSON.parse(raw); return { ...defaultState, ...parsed, rates: { ...defaultRates, ...(parsed.rates || {}) } }; } } catch (_) {} return structuredClone(defaultState); }
function saveState() { const payload = JSON.stringify({ title: state.title, query: state.query, rates: state.rates, updatedAt: state.updatedAt, source: state.source });
try { localStorage.setItem(STORAGE_KEY, payload); } catch (_) {}
try { if (window.yanm && typeof window.yanm.invoke === "function") { window.yanm.invoke("state.set", { key: HOST_KEY, value: payload }); } } catch (_) {} }
function setState(patch, persist = true) { state = { ...state, ...patch }; render(); if (persist) saveState(); }
function setLoading(loading, text) { state.loading = loading; if (text) statusEl.textContent = text; btnRefresh.textContent = loading ? "刷新中…" : "刷新"; btnRefresh.disabled = loading; btnReset.disabled = loading; btnClear.disabled = loading; filterEl.disabled = loading; }
function normalize(v) { return String(v || "").trim().toLowerCase(); }
function getDisplayRows() { const q = normalize(state.query); const rows = Object.entries(CURRENCIES).map(([code, details]) => ({ code, ...details, rate: state.rates?.[code] }));
if (!q) return rows; return rows.filter(item => { return normalize(item.code).includes(q) || normalize(item.name).includes(q); }); }
function render() { if (renderTimer) cancelAnimationFrame(renderTimer); renderTimer = requestAnimationFrame(() => { filterEl.value = state.query || ""; updatedEl.textContent = fmtTime(state.updatedAt); sourceEl.textContent = state.source || "本地缓存 / 在线 API"; subtitleEl.textContent = state.title === "美元汇率" ? "USD → CNY / EUR / JPY / GBP / AUD / SGD" : state.title;
const rows = getDisplayRows(); countEl.textContent = `${rows.length} 项`; emptyEl.classList.toggle("show", rows.length === 0); listEl.innerHTML = "";
rows.forEach((item) => { const row = document.createElement("div"); row.className = "row";
const flag = document.createElement("div"); flag.className = "flag"; flag.textContent = item.flag || "🏳️";
const currency = document.createElement("div"); currency.className = "currency";
const name = document.createElement("div"); name.className = "name"; name.style.color = COLOR.currency; name.textContent = `${item.name} (${item.code})`;
const code = document.createElement("div"); code.className = "code"; code.textContent = `USD 兑 ${item.code}`;
currency.appendChild(name); currency.appendChild(code);
const rate = document.createElement("div"); rate.className = "rate"; rate.style.color = COLOR.rate; rate.textContent = typeof item.rate === "number" ? item.rate.toFixed(2) : "--";
row.appendChild(flag); row.appendChild(currency); row.appendChild(rate);
listEl.appendChild(row); });
if (rows.length === 0) { listEl.innerHTML = ""; } }); }
function seededFallbackRates() { const base = { CNY: 7.18, EUR: 0.92, JPY: 156.80, GBP: 0.79, AUD: 1.51, SGD: 1.34 }; const t = Math.floor(Date.now() / 3600000); const jitter = (seed, spread) => { const x = Math.sin(seed + t * 1.37) * 10000; return (x - Math.floor(x) - 0.5) * spread; }; return { CNY: +(base.CNY + jitter(1, 0.08)).toFixed(2), EUR: +(base.EUR + jitter(2, 0.02)).toFixed(2), JPY: +(base.JPY + jitter(3, 1.2)).toFixed(2), GBP: +(base.GBP + jitter(4, 0.02)).toFixed(2), AUD: +(base.AUD + jitter(5, 0.03)).toFixed(2), SGD: +(base.SGD + jitter(6, 0.02)).toFixed(2) }; }
async function refreshRates() { setLoading(true, "正在获取汇率…"); let ok = false;
try { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), 7000);
const res = await fetch(API_URL, { method: "GET", cache: "no-store", signal: controller.signal });
clearTimeout(timer);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json(); const rates = {}; for (const code of Object.keys(CURRENCIES)) { const value = Number(data?.rates?.[code]); rates[code] = Number.isFinite(value) ? +value.toFixed(2) : defaultRates[code]; }
ok = true; setState({ rates, updatedAt: data?.date ? `${data.date} ${new Date().toLocaleTimeString("zh-CN", { hour12:false })}` : nowISO(), source: "在线 API", }, true); statusEl.textContent = "已更新"; } catch (_) { const rates = seededFallbackRates(); setState({ rates, updatedAt: nowISO(), source: "离线兜底" }, true); statusEl.textContent = "网络不可用,已切换离线兜底"; } finally { setLoading(false, ok ? "已获取最新汇率" : "已使用离线兜底"); } }
function bind() { filterEl.value = state.query || ""; filterEl.addEventListener("input", () => { setState({ query: filterEl.value }, true); });
btnClear.addEventListener("click", () => { setState({ query: "" }, true); filterEl.focus(); });
btnReset.addEventListener("click", () => { setState({ ...structuredClone(defaultState), rates: seededFallbackRates(), updatedAt: nowISO(), source: "重置完成" }, true); statusEl.textContent = "已重置"; });
btnRefresh.addEventListener("click", refreshRates);
document.addEventListener("keydown", (e) => { if (e.key === "Escape") { setState({ query: "" }, true); } if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "r") { e.preventDefault(); refreshRates(); } }); }
function init() { if (!state.rates) state.rates = seededFallbackRates(); if (!state.updatedAt) state.updatedAt = nowISO(); if (!state.source) state.source = "本地缓存 / 在线 API"; render(); bind(); saveState(); refreshRates(); }
init(); })(); </script></body></html>