feat: Lsky/COS gateway - attachments upload direct to object storage (remote_url/remote_thumb), preview loads from COS, graceful local fallback

This commit is contained in:
root
2026-10-17 01:16:59 +08:00
parent a94c1f6e25
commit 2bba3be08e
23 changed files with 174 additions and 51 deletions
+26
View File
@@ -0,0 +1,26 @@
{
"SystemRoot": "C:\\Windows",
"windir": "C:\\Windows",
"SystemDrive": "C:",
"ProgramData": "C:\\ProgramData",
"ProgramFiles": "C:\\Program Files",
"ProgramFiles(x86)": "C:\\Program Files (x86)",
"ProgramW6432": "C:\\Program Files",
"COMMONPROGRAMFILES": "C:\\Program Files\\Common Files",
"APPDATA": "C:\\Users\\Administrator\\AppData\\Roaming",
"LOCALAPPDATA": "C:\\Users\\Administrator\\AppData\\Local",
"USERPROFILE": "C:\\Users\\Administrator",
"HOMEDRIVE": "C:",
"HOMEPATH": "\\Users\\Administrator",
"USERNAME": "Administrator",
"COMPUTERNAME": "LINXIONG",
"OS": "Windows_NT",
"PROCESSOR_ARCHITECTURE": "AMD64",
"NUMBER_OF_PROCESSORS": "8",
"TEMP": "C:\\Users\\ADMINI~1\\AppData\\Local\\Temp",
"TMP": "C:\\Users\\ADMINI~1\\AppData\\Local\\Temp",
"JAVA_HOME": "C:\\Program Files\\Java\\jdk-17",
"ANDROID_HOME": "C:\\Android\\Sdk",
"NDK_HOME": "C:\\Android\\Sdk\\ndk\\27.0.12077973",
"PATH": "C:\\Windows\\System32;C:\\Windows;C:\\Windows\\System32\\Wbem;C:\\Windows\\System32\\WindowsPowerShell\\v1.0;D:\\APP\\DSH-desktop;C:\\Program Files\\Java\\jdk-17\\bin;C:\\Users\\Administrator\\.cargo\\bin;C:\\Android\\Sdk\\platform-tools"
}
+2
View File
@@ -90,6 +90,8 @@ CREATE TABLE IF NOT EXISTS attachment (
rel_path TEXT NOT NULL,
sha256 TEXT,
thumb_rel_path TEXT,
remote_url TEXT,
remote_thumb TEXT,
taken_at TEXT,
deleted_at TEXT,
origin TEXT,
+14 -1
View File
@@ -125,7 +125,7 @@ function preMigrateColumns(d) {
}
};
for (const t of SYNC_TABLES) addCols(t, SYNC_COLS);
addCols('attachment', [['sha256', 'TEXT'], ['thumb_rel_path', 'TEXT']]);
addCols('attachment', [['sha256', 'TEXT'], ['thumb_rel_path', 'TEXT'], ['remote_url', 'TEXT'], ['remote_thumb', 'TEXT']]);
}
export function migrateSync(d) {
@@ -1078,6 +1078,19 @@ function listFootprints(d, query) {
return { data_b64: data.toString('base64'), mime: row.thumb_rel_path ? 'image/webp' : row.mime, file_name: row.file_name };
}
case 'attachment_set_remote': {
// 图床(Lsky/COS)直传成功后回填远程地址;同步到所有端
const row = d.prepare('SELECT * FROM attachment WHERE id=?').get(payload.id);
if (!row) throw new Error('附件不存在');
withTx(d, () => {
const h = hlcNext(d);
d.prepare('UPDATE attachment SET remote_url=?, remote_thumb=?, hlc=? WHERE id=?')
.run(payload.remote_url ?? null, payload.remote_thumb ?? null, h, row.id);
touchOplog(d, 'attachment', row.uuid, 'upsert', h);
});
return { ok: true };
}
case 'view_list':
return { items: d.prepare('SELECT * FROM saved_view WHERE deleted_at IS NULL ORDER BY id').all().map(v => ({
...v, filter: JSON.parse(v.filter || '{}'), sort: JSON.parse(v.sort || '{}'), is_default: !!v.is_default
+28 -14
View File
@@ -1,7 +1,7 @@
import { useEffect, useState } from 'react';
import { Download, Loader2 } from 'lucide-react';
import { Button, Dialog } from '@/components/ui';
import { readFull, b64ToObjectUrl } from '@/lib/att-cache';
import { readFull } from '@/lib/att-cache';
import type { Attachment } from '@/lib/types';
export type AttKind = 'image' | 'pdf' | 'text' | 'audio' | 'video' | 'docx' | 'xlsx' | 'other';
@@ -50,29 +50,43 @@ export function AttachmentPreview({ att, onClose }: { att: Attachment | null; on
(async () => {
try {
const kind = attKind(att);
const { data_b64, mime } = await readFull(att);
const remote = att.remote_url || null;
// 远程直连(COS/图床 URL):图片/PDF/音视频直接作 src,零 base64 往返
if (remote && (kind === 'image' || kind === 'pdf' || kind === 'audio' || kind === 'video')) {
setContent({ kind, objectUrl: remote });
setLoading(false);
return;
}
let mime: string | undefined = att.mime ?? undefined;
let bytes: Uint8Array<ArrayBuffer>;
if (remote) {
bytes = new Uint8Array(await (await fetch(remote)).arrayBuffer());
} else {
const r = await readFull(att);
mime = r.mime;
bytes = b64ToUint8(r.data_b64);
}
if (cancelled) return;
if (kind === 'image' || kind === 'pdf' || kind === 'audio' || kind === 'video') {
const url = b64ToObjectUrl(data_b64, mime);
if (cancelled) { URL.revokeObjectURL(url); return; }
created.push(url);
setContent({ kind, objectUrl: url });
} else if (kind === 'text') {
const text = new TextDecoder().decode(b64ToUint8(data_b64));
if (kind === 'text') {
const text = new TextDecoder().decode(bytes);
setContent({ kind, text: text.length > 400_000 ? text.slice(0, 400_000) + '\n…(已截断)' : text });
} else if (kind === 'docx') {
const mammoth = await import('mammoth/mammoth.browser') as any;
const html = (await mammoth.convertToHtml({ arrayBuffer: b64ToUint8(data_b64).buffer })).value as string;
const html = (await mammoth.convertToHtml({ arrayBuffer: bytes.buffer })).value as string;
if (cancelled) return;
setContent({ kind, html });
} else if (kind === 'xlsx') {
const XLSX = await import('xlsx');
const wb = XLSX.read(b64ToUint8(data_b64).buffer, { type: 'array' });
const wb = XLSX.read(bytes, { type: 'array' });
const html = XLSX.utils.sheet_to_html(wb.Sheets[wb.SheetNames[0]]);
if (cancelled) return;
setContent({ kind, html });
} else {
setContent({ kind: 'other' });
// 本地回退的图片/PDF/音视频
const url = URL.createObjectURL(new Blob([bytes], { type: mime || 'application/octet-stream' }));
if (cancelled) { URL.revokeObjectURL(url); return; }
created.push(url);
setContent({ kind, objectUrl: url });
}
} catch (e) {
if (!cancelled) setError((e as Error).message || '预览加载失败');
@@ -81,7 +95,7 @@ export function AttachmentPreview({ att, onClose }: { att: Attachment | null; on
}
})();
return () => { cancelled = true; created.forEach(u => URL.revokeObjectURL(u)); };
}, [att?.id, att?.file_name]);
}, [att?.id, att?.file_name, att?.remote_url]);
const kind = att ? attKind(att) : 'other';
@@ -93,7 +107,7 @@ export function AttachmentPreview({ att, onClose }: { att: Attachment | null; on
title={(att ? '附件预览 · ' + att.file_name : '附件预览') + (att ? '(' + KIND_LABEL[kind] + ')' : '')}
footer={att ? (
<>
<Button variant="secondary" className="mr-auto" onClick={() => { void readFull(att).then(r => forceDownload(att.file_name, att.mime, r.data_b64)); }}>
<Button variant="secondary" className="mr-auto" onClick={() => { void readFull(att).then(r => forceDownload(att.file_name, att.mime ?? undefined, r.data_b64)); }}>
<Download size={13} /> 下载
</Button>
<Button variant="primary" onClick={onClose}>关闭</Button>
@@ -64,6 +64,7 @@ export function RecordDetailDialog({ open, onClose, footprintId, onEdit }: {
for (const a of list) {
if (cancelled) return;
if (thumbCache.has(a.id)) { setImgMap(m => ({ ...m, [a.id]: thumbCache.get(a.id)! })); continue; }
if (a.remote_thumb) { thumbCache.set(a.id, a.remote_thumb); setImgMap(m => ({ ...m, [a.id]: a.remote_thumb! })); continue; }
try {
if (a.thumb_rel_path) {
// 新上传:服务端已存 480px WebP 缩略图,20KB 级,秒开
+2
View File
@@ -51,6 +51,8 @@ export interface Attachment {
size: number;
rel_path: string;
thumb_rel_path?: string | null;
remote_url?: string | null;
remote_thumb?: string | null;
created_at: string;
}
+2
View File
@@ -19,6 +19,8 @@ services:
- ../app/server:/app/app-server:ro # 命令层整目录挂载(避免单文件嵌套遮蔽的不可靠行为)
environment:
- REGISTER_ENABLED=true
# - LSKY_API=https://img.linyux.cn/api/v1 # 兰空图床(对接 COS):填入后附件自动直传对象存储
# - LSKY_TOKEN= # 兰空 个人中心 → API 令牌
- INVITE_CODE=please-change-me # 多用户:注册须携带此邀请码(留空则不校验)
- MAX_USERS=20 # 用户数上限,0=不限
restart: unless-stopped
+63
View File
@@ -2,6 +2,23 @@
import http from 'node:http';
import fs from 'node:fs';
import zlib from 'node:zlib';
// 图床网关:Lsky Pro(已对接 COS),附件直传对象存储,应用服务器不做搬运工
const LSKY_API = (process.env.LSKY_API || '').replace(/\/$/, '');
const LSKY_TOKEN = process.env.LSKY_TOKEN || '';
async function lskyUpload(buf, fileName, mime) {
const fd = new FormData();
fd.append('file', new Blob([buf], { type: mime || 'application/octet-stream' }), fileName);
const r = await fetch(LSKY_API + '/upload', {
method: 'POST',
headers: { authorization: 'Bearer ' + LSKY_TOKEN, accept: 'application/json' },
body: fd,
signal: AbortSignal.timeout(60_000),
});
const j = await r.json().catch(() => null);
if (!j || j.status !== true || !j.data?.url) throw new Error('LSKY ' + r.status + ': ' + (j?.message || 'no url'));
return j.data;
}
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { getDb } from './db.mjs';
@@ -166,6 +183,29 @@ const server = http.createServer(async (req, res) => {
if (!r.changes) return send(404, false, '邀请码不存在');
return send(200, true, { deleted: r.changes });
}
if (req.method === 'POST' && route === '/admin/attachments/sync-lsky') {
// 存量迁移:本用户所有未上图的附件推送到 Lsky(逐个,失败跳过)
if (!LSKY_API || !LSKY_TOKEN) return send(400, false, '未配置 LSKY_API/LSKY_TOKEN');
const cmds = commandsFor(auth.user_id);
const rows = cmds.getDb().prepare("SELECT id, file_name, mime FROM attachment WHERE owner_type='footprint' AND deleted_at IS NULL AND (remote_url IS NULL OR remote_url='')").all();
let done = 0, failed = 0;
for (const row of rows) {
try {
const orig = cmds.handleCommand('attachment_read', { id: row.id });
const up = await lskyUpload(Buffer.from(orig.data_b64, 'base64'), row.file_name, row.mime);
let thumbUrl = null;
if (row.mime?.startsWith('image/')) {
const td = cmds.handleCommand('attachment_thumb', { id: row.id });
const tUp = await lskyUpload(Buffer.from(td.data_b64, 'base64'), 'thumb-' + row.file_name + '.webp', td.mime || 'image/webp');
thumbUrl = tUp.thumb_url || tUp.url;
}
cmds.handleCommand('attachment_set_remote', { id: row.id, remote_url: up.url, remote_thumb: thumbUrl });
done++;
} catch (e) { failed++; console.log('[lsky-migrate]', row.id, e.message); }
}
return send(200, true, { total: rows.length, done, failed });
}
if (req.method === 'POST' && route === '/admin/demo-seed') {
const demoRow = db.prepare('SELECT id FROM users WHERE username=?').get('demo');
if (!demoRow) return send(404, false, 'demo 账号不存在');
@@ -224,6 +264,29 @@ const server = http.createServer(async (req, res) => {
});
}
// 附件直传图床:attachment_add 落本地后同步推 Lsky(COS),失败自动回退本地
if (req.method === 'POST' && route === '/api/attachment_add') {
const body = await readJson(req);
const data = commandsFor(auth.user_id).handleCommand('attachment_add', body);
if (LSKY_API && LSKY_TOKEN) {
try {
const orig = Buffer.from(body.data_b64 || '', 'base64');
const up = await lskyUpload(orig, body.file_name || ('file-' + data.item.id), body.mime);
let thumbUrl = null;
if (body.thumb_b64) {
const tUp = await lskyUpload(Buffer.from(body.thumb_b64, 'base64'), 'thumb-' + (body.file_name || 'img') + '.webp', 'image/webp');
thumbUrl = tUp.thumb_url || tUp.url;
}
commandsFor(auth.user_id).handleCommand('attachment_set_remote', { id: data.item.id, remote_url: up.url, remote_thumb: thumbUrl });
data.item.remote_url = up.url;
data.item.remote_thumb = thumbUrl;
} catch (e) {
console.log('[lsky] 直传失败,保留本地存储:', e.message);
}
}
return send(200, true, data);
}
// Web 应用命令层:/api/<command>(与桌面端同一契约,按用户隔离的档案库)
if (req.method === 'POST' && route.startsWith('/api/')) {
const cmd = route.slice(5);
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
import{r as g,e as v,j as t,S as j,s as S}from"./index-BCzrIT2G.js";import{C as c,a as d,b as x}from"./card-BnbpsHNY.js";import{u as N,i as u,a as m}from"./useChart-DQHpg1hj.js";import{u as k,i as w,a as C,b as L,c as R,d as $,e as z,f as T}from"./echarts-BIWefrIc.js";k([w,C,L,R,$,z,T]);function q(){const[l,n]=g.useState(""),i=N(),e=v({queryKey:["stats",l||void 0],queryFn:()=>S(l?Number(l):void 0)}).data,p=u("2"),r=u("3"),h=m(()=>({grid:{left:34,right:12,top:14,bottom:24},tooltip:{trigger:"axis"},xAxis:{type:"category",data:(e==null?void 0:e.monthly.map(a=>a.ym.slice(2)))??[],axisLine:{lineStyle:{color:r}},axisTick:{show:!1},axisLabel:{color:r,fontSize:10}},yAxis:{type:"value",splitLine:{lineStyle:{color:"rgba(128,138,155,0.15)"}},axisLabel:{color:r,fontSize:10}},series:[{type:"bar",data:(e==null?void 0:e.monthly.map(a=>a.count))??[],barMaxWidth:18,itemStyle:{color:"#3056d3",borderRadius:[3,3,0,0]}}]}),[e,i]),b=m(()=>{const a=((e==null?void 0:e.by_type)??[]).filter(s=>s.count>0);return{tooltip:{trigger:"item"},legend:{orient:"vertical",right:4,top:"middle",textStyle:{color:p,fontSize:11},itemWidth:10,itemHeight:10},series:[{type:"pie",radius:["48%","72%"],center:["34%","50%"],label:{show:!1},data:a.map(s=>({name:s.label,value:s.count,itemStyle:{color:s.color||"#64748b"}}))}]}},[e,i]),y=m(()=>{const a=((e==null?void 0:e.by_level)??[]).filter(s=>s.count>0).slice().reverse();return{grid:{left:64,right:24,top:8,bottom:22},tooltip:{trigger:"axis"},xAxis:{type:"value",splitLine:{lineStyle:{color:"rgba(128,138,155,0.15)"}},axisLabel:{color:r,fontSize:10}},yAxis:{type:"category",data:a.map(s=>s.label),axisLine:{show:!1},axisTick:{show:!1},axisLabel:{color:p,fontSize:11}},series:[{type:"bar",data:a.map(s=>({value:s.count,itemStyle:{color:s.color||"#3056d3",borderRadius:[0,3,3,0]}})),barMaxWidth:14}]}},[e,i]),f=((e==null?void 0:e.metrics)??[]).slice(0,3);return t.jsxs("div",{className:"mx-auto max-w-[1080px] px-3 py-4 sm:px-6 sm:py-5",children:[t.jsxs("div",{className:"mb-4 flex flex-wrap items-center justify-between gap-3",children:[t.jsxs("div",{className:"flex flex-wrap gap-3",children:[t.jsx(o,{label:"总记录",value:(e==null?void 0:e.total)??0}),t.jsx(o,{label:l?l+" 年新增":"本年新增",value:(e==null?void 0:e.total_this_year)??0}),t.jsx(o,{label:"附件总数",value:(e==null?void 0:e.attachment_total)??0}),f.map(a=>t.jsx(o,{label:a.label,value:a.value,unit:a.unit??""},a.key))]}),t.jsx(j,{className:"w-[120px]",value:l,onChange:a=>n(a.target.value),options:[{value:"",label:"全部年份"},...((e==null?void 0:e.years)??[]).map(a=>({value:String(a),label:a+" 年"}))]})]}),!(e!=null&&e.metrics.length)&&t.jsx("div",{className:"mb-4 rounded-md border border-dashed border-line px-4 py-2 text-[12px] text-ink-3",children:"提示:在「统计」配置中注册指标(如志愿时长、读书本数),带单位的数值字段即可在此量化展示。"}),t.jsxs("div",{className:"grid grid-cols-1 lg:grid-cols-2 gap-4",children:[t.jsxs(c,{children:[t.jsx(d,{title:"近 12 个月活跃度"}),t.jsx(x,{children:t.jsx("div",{ref:h,className:"h-[220px]"})})]}),t.jsxs(c,{children:[t.jsx(d,{title:"类型分布"}),t.jsx(x,{children:t.jsx("div",{ref:b,className:"h-[220px]"})})]}),t.jsxs(c,{className:"col-span-2",children:[t.jsx(d,{title:"级别分布"}),t.jsx(x,{children:t.jsx("div",{ref:y,className:"h-[200px]"})})]})]})]})}function o({label:l,value:n,unit:i}){return t.jsxs("div",{className:"min-w-[120px] rounded-lg border border-line bg-surface px-4 py-3 shadow-card",children:[t.jsx("div",{className:"text-[12px] text-ink-3",children:l}),t.jsxs("div",{className:"num mt-0.5 text-[22px] font-semibold leading-tight text-ink-1",children:[n,i&&t.jsx("span",{className:"ml-1 text-[12px] font-normal text-ink-3",children:i})]})]})}export{q as DashboardPage};
import{r as g,e as v,j as t,S as j,s as S}from"./index-Cpiyh4tF.js";import{C as c,a as d,b as x}from"./card-CiUXsmh1.js";import{u as N,i as u,a as m}from"./useChart-AwFHZWNy.js";import{u as k,i as w,a as C,b as L,c as R,d as $,e as z,f as T}from"./echarts-BIWefrIc.js";k([w,C,L,R,$,z,T]);function q(){const[l,n]=g.useState(""),i=N(),e=v({queryKey:["stats",l||void 0],queryFn:()=>S(l?Number(l):void 0)}).data,p=u("2"),r=u("3"),h=m(()=>({grid:{left:34,right:12,top:14,bottom:24},tooltip:{trigger:"axis"},xAxis:{type:"category",data:(e==null?void 0:e.monthly.map(a=>a.ym.slice(2)))??[],axisLine:{lineStyle:{color:r}},axisTick:{show:!1},axisLabel:{color:r,fontSize:10}},yAxis:{type:"value",splitLine:{lineStyle:{color:"rgba(128,138,155,0.15)"}},axisLabel:{color:r,fontSize:10}},series:[{type:"bar",data:(e==null?void 0:e.monthly.map(a=>a.count))??[],barMaxWidth:18,itemStyle:{color:"#3056d3",borderRadius:[3,3,0,0]}}]}),[e,i]),b=m(()=>{const a=((e==null?void 0:e.by_type)??[]).filter(s=>s.count>0);return{tooltip:{trigger:"item"},legend:{orient:"vertical",right:4,top:"middle",textStyle:{color:p,fontSize:11},itemWidth:10,itemHeight:10},series:[{type:"pie",radius:["48%","72%"],center:["34%","50%"],label:{show:!1},data:a.map(s=>({name:s.label,value:s.count,itemStyle:{color:s.color||"#64748b"}}))}]}},[e,i]),y=m(()=>{const a=((e==null?void 0:e.by_level)??[]).filter(s=>s.count>0).slice().reverse();return{grid:{left:64,right:24,top:8,bottom:22},tooltip:{trigger:"axis"},xAxis:{type:"value",splitLine:{lineStyle:{color:"rgba(128,138,155,0.15)"}},axisLabel:{color:r,fontSize:10}},yAxis:{type:"category",data:a.map(s=>s.label),axisLine:{show:!1},axisTick:{show:!1},axisLabel:{color:p,fontSize:11}},series:[{type:"bar",data:a.map(s=>({value:s.count,itemStyle:{color:s.color||"#3056d3",borderRadius:[0,3,3,0]}})),barMaxWidth:14}]}},[e,i]),f=((e==null?void 0:e.metrics)??[]).slice(0,3);return t.jsxs("div",{className:"mx-auto max-w-[1080px] px-3 py-4 sm:px-6 sm:py-5",children:[t.jsxs("div",{className:"mb-4 flex flex-wrap items-center justify-between gap-3",children:[t.jsxs("div",{className:"flex flex-wrap gap-3",children:[t.jsx(o,{label:"总记录",value:(e==null?void 0:e.total)??0}),t.jsx(o,{label:l?l+" 年新增":"本年新增",value:(e==null?void 0:e.total_this_year)??0}),t.jsx(o,{label:"附件总数",value:(e==null?void 0:e.attachment_total)??0}),f.map(a=>t.jsx(o,{label:a.label,value:a.value,unit:a.unit??""},a.key))]}),t.jsx(j,{className:"w-[120px]",value:l,onChange:a=>n(a.target.value),options:[{value:"",label:"全部年份"},...((e==null?void 0:e.years)??[]).map(a=>({value:String(a),label:a+" 年"}))]})]}),!(e!=null&&e.metrics.length)&&t.jsx("div",{className:"mb-4 rounded-md border border-dashed border-line px-4 py-2 text-[12px] text-ink-3",children:"提示:在「统计」配置中注册指标(如志愿时长、读书本数),带单位的数值字段即可在此量化展示。"}),t.jsxs("div",{className:"grid grid-cols-1 lg:grid-cols-2 gap-4",children:[t.jsxs(c,{children:[t.jsx(d,{title:"近 12 个月活跃度"}),t.jsx(x,{children:t.jsx("div",{ref:h,className:"h-[220px]"})})]}),t.jsxs(c,{children:[t.jsx(d,{title:"类型分布"}),t.jsx(x,{children:t.jsx("div",{ref:b,className:"h-[220px]"})})]}),t.jsxs(c,{className:"col-span-2",children:[t.jsx(d,{title:"级别分布"}),t.jsx(x,{children:t.jsx("div",{ref:y,className:"h-[200px]"})})]})]})]})}function o({label:l,value:n,unit:i}){return t.jsxs("div",{className:"min-w-[120px] rounded-lg border border-line bg-surface px-4 py-3 shadow-card",children:[t.jsx("div",{className:"text-[12px] text-ink-3",children:l}),t.jsxs("div",{className:"num mt-0.5 text-[22px] font-semibold leading-tight text-ink-1",children:[n,i&&t.jsx("span",{className:"ml-1 text-[12px] font-normal text-ink-3",children:i})]})]})}export{q as DashboardPage};
@@ -1,4 +1,4 @@
import{c as K,e as I,j as e,E as O,v as $,u as k,b as S,r as u,f as y,w as H,x as W,B as g,P as q,C as Q,y as J,z as T,A as U,l as N,k as G,T as E,I as j,D as Y,F as C,H as B,J as V}from"./index-BCzrIT2G.js";import{C as f,a as z,b as _}from"./card-BnbpsHNY.js";import{u as X,i as M,a as L}from"./useChart-DQHpg1hj.js";import{u as Z,i as ee,b as se,a as te,c as ae,d as re,e as ne,f as le}from"./echarts-BIWefrIc.js";/**
import{c as K,e as I,j as e,E as O,v as $,u as k,b as S,r as u,f as y,w as H,x as W,B as g,P as q,C as Q,y as J,z as T,A as U,l as N,k as G,T as E,I as j,D as Y,F as C,H as B,J as V}from"./index-Cpiyh4tF.js";import{C as f,a as z,b as _}from"./card-CiUXsmh1.js";import{u as X,i as M,a as L}from"./useChart-AwFHZWNy.js";import{u as Z,i as ee,b as se,a as te,c as ae,d as re,e as ne,f as le}from"./echarts-BIWefrIc.js";/**
* @license lucide-react v0.475.0 - ISC
*
* This source code is licensed under the ISC license.
@@ -1,4 +1,4 @@
import{c as pe,j as e,a as x,r as t,u as he,b as ve,d as je,e as w,f as fe,I as ge,S as h,B as v,P as L,g as U,L as V,G as X,R as ye,h as $,i as be,k as Ne,T as ke,E as ee,l as _e,m as Ce,n as we,C as Se,o as Ee,p as qe,q as ze,s as Ie,t as De}from"./index-BCzrIT2G.js";/**
import{c as pe,j as e,a as x,r as t,u as he,b as ve,d as je,e as w,f as fe,I as ge,S as h,B as v,P as L,g as U,L as V,G as X,R as ye,h as $,i as be,k as Ne,T as ke,E as ee,l as _e,m as Ce,n as we,C as Se,o as Ee,p as qe,q as ze,s as Ie,t as De}from"./index-Cpiyh4tF.js";/**
* @license lucide-react v0.475.0 - ISC
*
* This source code is licensed under the ISC license.
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
import{c as A,u as Q,b as z,e as B,r as i,f as T,j as e,B as p,P as F,K as q,l as E,k as D,T as I,C as K,M as L,D as G,F as r,I as j,N as H,S as R,O as W,p as J}from"./index-BCzrIT2G.js";import{C as U}from"./card-BnbpsHNY.js";/**
import{c as A,u as Q,b as z,e as B,r as i,f as T,j as e,B as p,P as F,K as q,l as E,k as D,T as I,C as K,M as L,D as G,F as r,I as j,N as H,S as R,O as W,p as J}from"./index-Cpiyh4tF.js";import{C as U}from"./card-CiUXsmh1.js";/**
* @license lucide-react v0.475.0 - ISC
*
* This source code is licensed under the ISC license.
@@ -1 +1 @@
import{j as s,a}from"./index-BCzrIT2G.js";function i({className:r,...e}){return s.jsx("div",{className:a("rounded-lg border border-line bg-surface shadow-card",r),...e})}function t({title:r,extra:e,className:n}){return s.jsxs("div",{className:a("flex min-h-11 flex-wrap items-center justify-between gap-2 border-b border-line px-4 py-1.5",n),children:[s.jsx("h3",{className:"text-[13px] font-semibold text-ink-1",children:r}),e&&s.jsx("div",{className:"flex flex-wrap items-center gap-2",children:e})]})}function c({className:r,...e}){return s.jsx("div",{className:a("p-4",r),...e})}export{i as C,t as a,c as b};
import{j as s,a}from"./index-Cpiyh4tF.js";function i({className:r,...e}){return s.jsx("div",{className:a("rounded-lg border border-line bg-surface shadow-card",r),...e})}function t({title:r,extra:e,className:n}){return s.jsxs("div",{className:a("flex min-h-11 flex-wrap items-center justify-between gap-2 border-b border-line px-4 py-1.5",n),children:[s.jsx("h3",{className:"text-[13px] font-semibold text-ink-1",children:r}),e&&s.jsx("div",{className:"flex flex-wrap items-center gap-2",children:e})]})}function c({className:r,...e}){return s.jsx("div",{className:a("p-4",r),...e})}export{i as C,t as a,c as b};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
import{r}from"./index-BCzrIT2G.js";import{g as f}from"./echarts-BIWefrIc.js";function p(){const[n,o]=r.useState(0);return r.useEffect(()=>{const s=()=>o(t=>t+1);return window.addEventListener("fp-theme-change",s),()=>window.removeEventListener("fp-theme-change",s)},[]),n}function d(n){return getComputedStyle(document.documentElement).getPropertyValue("--text-"+n).trim()||"#454e5c"}function h(n,o){const s=r.useRef(null),[t,i]=r.useState(null),u=r.useRef(n);return u.current=n,r.useEffect(()=>{if(!t)return;const e=f(t);s.current=e,e.setOption(u.current(),{notMerge:!0});const c=new ResizeObserver(()=>e.resize());return c.observe(t),()=>{c.disconnect(),e.dispose(),s.current=null}},[t]),r.useEffect(()=>{var e;(e=s.current)==null||e.setOption(u.current(),{notMerge:!0})},[t,...o]),e=>i(e)}export{h as a,d as i,p as u};
import{r}from"./index-Cpiyh4tF.js";import{g as f}from"./echarts-BIWefrIc.js";function p(){const[n,o]=r.useState(0);return r.useEffect(()=>{const s=()=>o(t=>t+1);return window.addEventListener("fp-theme-change",s),()=>window.removeEventListener("fp-theme-change",s)},[]),n}function d(n){return getComputedStyle(document.documentElement).getPropertyValue("--text-"+n).trim()||"#454e5c"}function h(n,o){const s=r.useRef(null),[t,i]=r.useState(null),u=r.useRef(n);return u.current=n,r.useEffect(()=>{if(!t)return;const e=f(t);s.current=e,e.setOption(u.current(),{notMerge:!0});const c=new ResizeObserver(()=>e.resize());return c.observe(t),()=>{c.disconnect(),e.dispose(),s.current=null}},[t]),r.useEffect(()=>{var e;(e=s.current)==null||e.setOption(u.current(),{notMerge:!0})},[t,...o]),e=>i(e)}export{h as a,d as i,p as u};
+1 -1
View File
@@ -8,7 +8,7 @@
<link rel="icon" href="/icons/pwa-192.png" />
<link rel="apple-touch-icon" href="/icons/pwa-192.png" />
<title>足迹 Footprints</title>
<script type="module" crossorigin src="/assets/index-BCzrIT2G.js"></script>
<script type="module" crossorigin src="/assets/index-Cpiyh4tF.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CLr4ofF5.css">
</head>
<body>