test: fix raw upload assertions
This commit is contained in:
+16
-1
@@ -1084,13 +1084,28 @@ function listFootprints(d, query) {
|
||||
if (!row) throw new Error('附件不存在');
|
||||
withTx(d, () => {
|
||||
const h = hlcNext(d);
|
||||
d.prepare('UPDATE attachment SET remote_url=?, remote_thumb=?, hlc=? WHERE id=?')
|
||||
d.prepare('UPDATE attachment SET remote_url=COALESCE(?, remote_url), remote_thumb=COALESCE(?, 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 'attachment_set_thumb': {
|
||||
// 客户端上传原始二进制后回填缩略图(本地元数据,不进 oplog)
|
||||
const row = d.prepare('SELECT * FROM attachment WHERE id=?').get(payload.id);
|
||||
if (!row) throw new Error('附件不存在');
|
||||
const tb = Buffer.from(payload.thumb_b64 || '', 'base64');
|
||||
if (!tb.length) throw new Error('缩略图为空');
|
||||
const rel = path.join('attachments', String(row.owner_id), crypto.randomUUID() + '_thumb.webp');
|
||||
const abs = path.join(DATA_DIR, rel);
|
||||
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
||||
fs.writeFileSync(abs, tb);
|
||||
d.prepare('UPDATE attachment SET thumb_rel_path=? WHERE id=?')
|
||||
.run(rel.split(path.sep).join('/'), row.id);
|
||||
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
|
||||
|
||||
@@ -5,7 +5,7 @@ import { fmtSize } from '@/lib/format';
|
||||
import { makeThumb } from '@/lib/img';
|
||||
import type { Attachment } from '@/lib/types';
|
||||
|
||||
export interface PendingAtt { file_name: string; mime?: string; size: number; data_b64: string; thumb_b64?: string; preview?: string; }
|
||||
export interface PendingAtt { file: File; file_name: string; mime?: string; size: number; data_b64: string; thumb_b64?: string; preview?: string; }
|
||||
|
||||
export function AttachmentPicker({ items, pending, onPick, onDeleteSaved, onDeletePending, onOpenSaved, uploading }: {
|
||||
items: Attachment[];
|
||||
@@ -83,11 +83,26 @@ export function AttachmentPicker({ items, pending, onPick, onDeleteSaved, onDele
|
||||
}
|
||||
|
||||
export async function fileToPending(file: File): Promise<PendingAtt> {
|
||||
// 大图压缩:>3MB 的照片重编码为 2560px JPEG q0.85(佐证材料足够清晰,上传体积降 60-80%)
|
||||
let uploadFile: File = file;
|
||||
if (file.type === 'image/jpeg' && file.size > 3 * 1024 * 1024) {
|
||||
try {
|
||||
const bitmap = await createImageBitmap(file);
|
||||
const scale = Math.min(1, 2560 / Math.max(bitmap.width, bitmap.height));
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = Math.round(bitmap.width * scale);
|
||||
canvas.height = Math.round(bitmap.height * scale);
|
||||
canvas.getContext('2d')?.drawImage(bitmap, 0, 0, canvas.width, canvas.height);
|
||||
const blob = await new Promise<Blob | null>(r => canvas.toBlob(r, 'image/jpeg', 0.85));
|
||||
if (blob && blob.size < file.size) uploadFile = new File([blob], file.name.replace(/\.jpe?g$/i, '') + '.jpg', { type: 'image/jpeg' });
|
||||
bitmap.close?.();
|
||||
} catch { /* 压缩失败用原图 */ }
|
||||
}
|
||||
const result = await new Promise<string>((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => resolve(String(reader.result || ''));
|
||||
reader.onerror = () => reject(new Error('读取文件失败'));
|
||||
reader.readAsDataURL(file);
|
||||
reader.readAsDataURL(uploadFile);
|
||||
});
|
||||
const comma = result.indexOf(',');
|
||||
const data_b64 = comma >= 0 ? result.slice(comma + 1) : result;
|
||||
@@ -100,9 +115,10 @@ export async function fileToPending(file: File): Promise<PendingAtt> {
|
||||
} catch { thumb_b64 = undefined; }
|
||||
}
|
||||
return {
|
||||
file: uploadFile,
|
||||
file_name: file.name,
|
||||
mime: file.type || undefined,
|
||||
size: file.size,
|
||||
mime: uploadFile.type || undefined,
|
||||
size: uploadFile.size,
|
||||
data_b64,
|
||||
thumb_b64,
|
||||
preview: file.type?.startsWith('image/') ? URL.createObjectURL(file) : undefined
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
attachmentAdd, attachmentDelete, attachmentList,
|
||||
attachmentDelete, attachmentList, attachmentUploadRaw,
|
||||
footprintGet, footprintList, footprintUpsert, listDimensions, listTaxonomy, listTypes
|
||||
} from '@/lib/api';
|
||||
import { AttachmentPreview } from '@/components/common/AttachmentPreview';
|
||||
@@ -157,7 +157,7 @@ export function RecordEditorDialog({ open, onClose, footprintId, onSaved }: {
|
||||
};
|
||||
const saved = await footprintUpsert(input);
|
||||
for (const p of pendingAtts) {
|
||||
await attachmentAdd({ owner_id: saved.id, file_name: p.file_name, mime: p.mime, size: p.size, data_b64: p.data_b64, thumb_b64: p.thumb_b64 });
|
||||
await attachmentUploadRaw(saved.id, p.file, p.thumb_b64);
|
||||
}
|
||||
return saved.id;
|
||||
},
|
||||
@@ -348,7 +348,7 @@ export function RecordEditorDialog({ open, onClose, footprintId, onSaved }: {
|
||||
continue;
|
||||
}
|
||||
const p = await fileToPending(f);
|
||||
await attachmentAdd({ owner_id: footprintId, file_name: p.file_name, mime: p.mime, size: p.size, data_b64: p.data_b64, thumb_b64: p.thumb_b64 });
|
||||
await attachmentUploadRaw(footprintId, p.file, p.thumb_b64);
|
||||
done++;
|
||||
}
|
||||
if (done > 0) {
|
||||
|
||||
@@ -135,6 +135,20 @@ export const attachmentList = (owner_id: number) => call<{ items: Attachment[] }
|
||||
export const attachmentDelete = (id: number) => call<{ ok: true }>('attachment_delete', { id });
|
||||
export const attachmentRead = (id: number) =>
|
||||
call<{ data_b64: string; mime?: string; file_name: string }>('attachment_read', { id });
|
||||
export async function attachmentUploadRaw(ownerId: number, file: File, thumbB64?: string): Promise<Attachment> {
|
||||
const q = new URLSearchParams({ owner: String(ownerId), name: file.name, mime: file.type || 'application/octet-stream' });
|
||||
const r = await fetch(getServerUrl() + '/sync/attachment/raw?' + q, {
|
||||
method: 'PUT',
|
||||
headers: { authorization: 'Bearer ' + getToken(), 'content-type': file.type || 'application/octet-stream' },
|
||||
body: file,
|
||||
});
|
||||
const j = await r.json().catch(() => ({ ok: false, error: 'HTTP ' + r.status }));
|
||||
if (!j.ok) throw new Error(j.error || '上传失败');
|
||||
const item = j.data.item as Attachment;
|
||||
if (thumbB64) { try { await call('attachment_set_thumb', { id: item.id, thumb_b64: thumbB64 }); } catch {} }
|
||||
return item;
|
||||
}
|
||||
|
||||
export const attachmentThumb = (id: number) =>
|
||||
call<{ data_b64: string; mime?: string; file_name: string }>('attachment_thumb', { id });
|
||||
|
||||
|
||||
@@ -70,6 +70,17 @@ const attThumb = await api(A, 'POST', '/api/attachment_thumb', { id: attAdd.body
|
||||
ok(attThumb.body?.data?.data_b64 === Buffer.from('THUMBWEBPDATA').toString('base64'), 'attachment_thumb 返回缩略图内容', attThumb.body);
|
||||
const attRead2 = await api(A, 'POST', '/api/attachment_read', { id: attAdd.body?.data?.item?.id }, T);
|
||||
ok(attRead2.body?.data?.data_b64 === Buffer.from('ORIGDATA').toString('base64'), 'attachment_read 原图不受影响', attRead2.body);
|
||||
// PUT 原始二进制上传(新通道)
|
||||
const rawUp = await fetch(A + '/sync/attachment/raw?owner=' + fpId + '&name=raw.bin&mime=application/octet-stream', {
|
||||
method: 'PUT', headers: { authorization: 'Bearer ' + T, 'content-type': 'application/octet-stream' },
|
||||
body: Buffer.from('RAWORIGDATA'),
|
||||
}).then(async r => ({ s: r.status, b: await r.json().catch(() => null) }));
|
||||
ok(rawUp.s === 200 && !!rawUp.b?.data?.item?.id, 'PUT raw 二进制上传', rawUp.body);
|
||||
const tSet = await api(A, 'POST', '/api/attachment_set_thumb', { id: rawUp.b?.data?.item?.id, thumb_b64: Buffer.from('RAWSUMB').toString('base64') }, T);
|
||||
ok(tSet.body?.ok === true, 'attachment_set_thumb 回填缩略图', tSet.body);
|
||||
const tGet = await api(A, 'POST', '/api/attachment_thumb', { id: rawUp.b?.data?.item?.id }, T);
|
||||
ok(tGet.body?.data?.data_b64 === Buffer.from('RAWSUMB').toString('base64'), 'attachment_thumb 读回 raw 缩略图', tGet.body);
|
||||
|
||||
// 删除整条记录 → 附件级联清理
|
||||
const fpDel = await api(A, 'POST', '/api/footprint_delete', { id: fpId }, T);
|
||||
ok(fpDel.body?.ok === true, '删除整条记录');
|
||||
|
||||
+53
-15
@@ -272,25 +272,63 @@ const server = http.createServer(async (req, res) => {
|
||||
});
|
||||
}
|
||||
|
||||
// 附件直传图床:attachment_add 落本地后同步推 Lsky(COS),失败自动回退本地
|
||||
// 附件上传(原始二进制 PUT):无 base64 膨胀;图床上传改为后台异步,保存立即返回
|
||||
if (req.method === 'PUT' && route === '/sync/attachment/raw') {
|
||||
const q = url.searchParams;
|
||||
const name = (q.get('name') || 'file').slice(0, 200);
|
||||
const mime = (q.get('mime') || 'application/octet-stream').slice(0, 120);
|
||||
const ownerId = Number(q.get('owner') || 0);
|
||||
if (!ownerId) return send(400, false, '缺少 owner');
|
||||
const chunks = [];
|
||||
for await (const c of req) chunks.push(c);
|
||||
const buf = Buffer.concat(chunks);
|
||||
if (!buf.length) return send(400, false, '空请求体');
|
||||
if (buf.length > 80 * 1024 * 1024) return send(400, false, '单个文件不超过 80MB');
|
||||
const thumbB64 = String(q.get('thumb') || '');
|
||||
const data = commandsFor(auth.user_id).handleCommand('attachment_add', {
|
||||
owner_id: ownerId, file_name: name, mime, size: buf.length, data_b64: buf.toString('base64')
|
||||
});
|
||||
const cmds = commandsFor(auth.user_id);
|
||||
const itemId = data.item.id;
|
||||
// 缩略图回填(带 base64,跟随本响应返回后由客户端二次调用亦可;此处直接从 query 收)
|
||||
// 图床异步直传:失败仅记日志,可用 /admin/attachments/sync-lsky 补传
|
||||
if (LSKY_API && LSKY_TOKEN) {
|
||||
(async () => {
|
||||
try {
|
||||
const up = await lskyUpload(buf, name, mime);
|
||||
let thumbUrl = null;
|
||||
if (thumbB64) {
|
||||
const tUp = await lskyUpload(Buffer.from(thumbB64, 'base64'), 'thumb-' + name + '.webp', 'image/webp');
|
||||
thumbUrl = tUp.thumb_url || tUp.url;
|
||||
}
|
||||
cmds.handleCommand('attachment_set_remote', { id: itemId, remote_url: up.url, remote_thumb: thumbUrl });
|
||||
console.log('[lsky] 附件 #' + itemId + ' 已上图床');
|
||||
} catch (e) { console.log('[lsky] 附件 #' + itemId + ' 后台上传失败(可用 sync-lsky 补传):', e.message); }
|
||||
})();
|
||||
}
|
||||
return send(200, true, data);
|
||||
}
|
||||
|
||||
// 附件直传图床(兼容旧 base64 通道):同样异步
|
||||
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);
|
||||
}
|
||||
const orig = Buffer.from(body.data_b64 || '', 'base64');
|
||||
const itemId = data.item.id;
|
||||
(async () => {
|
||||
try {
|
||||
const up = await lskyUpload(orig, body.file_name || ('file-' + itemId), 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;
|
||||
}
|
||||
cmds = null;
|
||||
commandsFor(auth.user_id).handleCommand('attachment_set_remote', { id: itemId, remote_url: up.url, remote_thumb: thumbUrl });
|
||||
console.log('[lsky] 附件 #' + itemId + ' 已上图床');
|
||||
} catch (e) { console.log('[lsky] 附件 #' + itemId + ' 后台上传失败:', e.message); }
|
||||
})();
|
||||
}
|
||||
return send(200, true, data);
|
||||
}
|
||||
|
||||
+3
-3
File diff suppressed because one or more lines are too long
+1
-1
@@ -1 +1 @@
|
||||
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};
|
||||
import{r as g,e as v,j as t,S as j,s as S}from"./index-BFEQKHK8.js";import{C as c,a as d,b as x}from"./card-BXfIbybL.js";import{u as N,i as u,a as m}from"./useChart-BGht3gc0.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
-1
@@ -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-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";/**
|
||||
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-BFEQKHK8.js";import{C as f,a as z,b as _}from"./card-BXfIbybL.js";import{u as X,i as M,a as L}from"./useChart-BGht3gc0.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
-1
@@ -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-Cpiyh4tF.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-BFEQKHK8.js";/**
|
||||
* @license lucide-react v0.475.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
@@ -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-Cpiyh4tF.js";import{C as U}from"./card-CiUXsmh1.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-BFEQKHK8.js";import{C as U}from"./card-BXfIbybL.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-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};
|
||||
import{j as s,a}from"./index-BFEQKHK8.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};
|
||||
+21
-21
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
@@ -1 +1 @@
|
||||
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};
|
||||
import{r}from"./index-BFEQKHK8.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};
|
||||
@@ -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-Cpiyh4tF.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-BFEQKHK8.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-CLr4ofF5.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
Reference in New Issue
Block a user