feat: filename-aware attachment download endpoint (Content-Disposition), preview download uses it (no popup flash)

This commit is contained in:
root
2026-10-17 11:07:00 +08:00
parent 6d686c1d69
commit a0aa77dfda
15 changed files with 120 additions and 99 deletions
+10 -22
View File
@@ -2,6 +2,7 @@ import { useEffect, useState } from 'react';
import { Download, Loader2 } from 'lucide-react'; import { Download, Loader2 } from 'lucide-react';
import { Button, Dialog, useToast } from '@/components/ui'; import { Button, Dialog, useToast } from '@/components/ui';
import { readFull } from '@/lib/att-cache'; import { readFull } from '@/lib/att-cache';
import { attachmentDownload } from '@/lib/api';
import type { Attachment } from '@/lib/types'; import type { Attachment } from '@/lib/types';
export type AttKind = 'image' | 'pdf' | 'text' | 'audio' | 'video' | 'docx' | 'xlsx' | 'other'; export type AttKind = 'image' | 'pdf' | 'text' | 'audio' | 'video' | 'docx' | 'xlsx' | 'other';
@@ -23,14 +24,6 @@ function b64ToUint8(dataB64: string): Uint8Array<ArrayBuffer> {
return Uint8Array.from(atob(dataB64), c => c.charCodeAt(0)) as Uint8Array<ArrayBuffer>; return Uint8Array.from(atob(dataB64), c => c.charCodeAt(0)) as Uint8Array<ArrayBuffer>;
} }
function forceDownload(fileName: string, mime: string | undefined | null, dataB64: string) {
const url = URL.createObjectURL(new Blob([b64ToUint8(dataB64)], { type: mime || 'application/octet-stream' }));
const a = document.createElement('a');
a.href = url;
a.download = fileName;
a.click();
setTimeout(() => URL.revokeObjectURL(url), 60_000);
}
const KIND_LABEL: Record<AttKind, string> = { const KIND_LABEL: Record<AttKind, string> = {
image: '图片', pdf: 'PDF 文档', text: '文本文件', audio: '音频', video: '视频', image: '图片', pdf: 'PDF 文档', text: '文本文件', audio: '音频', video: '视频',
@@ -109,22 +102,17 @@ export function AttachmentPreview({ att, onClose }: { att: Attachment | null; on
footer={att ? ( footer={att ? (
<> <>
<Button variant="secondary" className="mr-auto" onClick={() => { <Button variant="secondary" className="mr-auto" onClick={() => {
// 远程附件:同步新标签打开(用户手势内触发,不会被拦截;右键即可另存) // 统一走服务器下载端点:Content-Disposition 带原始文件名,浏览器直接落盘,不闪窗
if (att.remote_url) {
const a = document.createElement('a');
a.href = att.remote_url;
a.target = '_blank';
a.rel = 'noopener';
a.click();
toast('success', '已在新的标签页打开,右键图片可「另存为」');
return;
}
// 本地附件:base64 下载,失败给出提示
void (async () => { void (async () => {
try { try {
toast('success', '已开始下载'); const { blob, file_name } = await attachmentDownload(att.id);
const r = await readFull(att); const url = URL.createObjectURL(blob);
forceDownload(att.file_name, att.mime ?? undefined, r.data_b64); const a = document.createElement('a');
a.href = url;
a.download = file_name;
a.click();
setTimeout(() => URL.revokeObjectURL(url), 60_000);
toast('success', '已下载:' + file_name);
} catch (e) { } catch (e) {
toast('error', '下载失败:' + (e as Error).message); toast('error', '下载失败:' + (e as Error).message);
} }
+15
View File
@@ -147,6 +147,21 @@ export async function attachmentUploadRaw(ownerId: number, file: File, thumbB64?
return item; return item;
} }
export async function attachmentDownload(id: number): Promise<{ blob: Blob; file_name: string; mime?: string }> {
const r = await fetch(getServerUrl() + '/api/attachment_download', {
method: 'POST',
headers: { 'content-type': 'application/json', authorization: 'Bearer ' + getToken() },
body: JSON.stringify({ id }),
});
if (!r.ok) {
const j = await r.json().catch(() => null);
throw new Error(j?.error || '下载失败');
}
const file_name = decodeURIComponent(r.headers.get('x-file-name') || (r.headers.get('content-disposition') || '').match(/UTF-8''([^;]+)/)?.[1] || 'file');
const mime = r.headers.get('content-type') || undefined;
return { blob: await r.blob(), file_name, mime };
}
export const attachmentThumb = (id: number) => export const attachmentThumb = (id: number) =>
call<{ data_b64: string; mime?: string; file_name: string }>('attachment_thumb', { id }); call<{ data_b64: string; mime?: string; file_name: string }>('attachment_thumb', { id });
+5
View File
@@ -70,6 +70,11 @@ 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); 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); 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); ok(attRead2.body?.data?.data_b64 === Buffer.from('ORIGDATA').toString('base64'), 'attachment_read 原图不受影响', attRead2.body);
// 下载端点(带原始文件名)
const dlAtt = await api(A, 'POST', '/api/attachment_download', { id: attAdd.body?.data?.item?.id }, T);
const dlText = await (dlAdd0 => dlAdd0)(fetch(A + '/api/attachment_download', { method: 'POST', headers: { 'content-type': 'application/json', authorization: 'Bearer ' + T }, body: JSON.stringify({ id: attAdd.body?.data?.item?.id }) }).then(async r => ({ s: r.status, ct: r.headers.get('content-type'), cd: r.headers.get('content-disposition'), t: await r.text() })));
ok(dlText.s === 200 && dlText.t === 'ORIGDATA' && /photo\.jpg/.test(dlText.cd), 'attachment_download 端点(文件名+内容)', { s: dlText.s, cd: dlText.cd, t: dlText.t.slice(0, 40) });
// PUT 原始二进制上传(新通道) // PUT 原始二进制上传(新通道)
const rawUp = await fetch(A + '/sync/attachment/raw?owner=' + fpId + '&name=raw.bin&mime=application/octet-stream', { 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' }, method: 'PUT', headers: { authorization: 'Bearer ' + T, 'content-type': 'application/octet-stream' },
+13
View File
@@ -357,6 +357,19 @@ const server = http.createServer(async (req, res) => {
return send(200, true, data); return send(200, true, data);
} }
// 附件下载(带原始文件名的 Content-Disposition,浏览器直接落盘)
if (req.method === 'POST' && route === '/api/attachment_download') {
const body = await readJson(req);
const data = commandsFor(auth.user_id).handleCommand('attachment_read', { id: Number(body.id) });
const buf = Buffer.from(data.data_b64, 'base64');
res.writeHead(200, {
'Content-Type': data.mime || 'application/octet-stream',
'Content-Length': buf.length,
'Content-Disposition': "attachment; filename*=UTF-8''" + encodeURIComponent(data.file_name || 'file')
});
return res.end(buf);
}
// 附件删除:本地清理 + 图床异步清理 // 附件删除:本地清理 + 图床异步清理
if (req.method === 'POST' && route === '/api/attachment_delete') { if (req.method === 'POST' && route === '/api/attachment_delete') {
const body = await readJson(req); const body = await readJson(req);
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
import{r as g,f as v,j as t,S as j,t as S}from"./index-GKqw9oy7.js";import{C as c,a as d,b as x}from"./card-C3qAWMF6.js";import{u as N,i as u,a as m}from"./useChart-CZ5C9jzK.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,f as v,j as t,S as j,t as S}from"./index-B7s6g2Bv.js";import{C as c,a as d,b as x}from"./card-abtg_WiG.js";import{u as N,i as u,a as m}from"./useChart-oVrBsOjL.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 A,f as I,j as e,E as O,w as $,u as k,d as S,r as u,g as y,x as H,y as W,B as g,P as q,C as Q,z as J,A as T,D as U,m as N,l as G,T as E,I as j,F as Y,H as C,J as B,K as V}from"./index-GKqw9oy7.js";import{C as f,a as z,b as _}from"./card-C3qAWMF6.js";import{u as X,i as M,a as L}from"./useChart-CZ5C9jzK.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 A,f as I,j as e,E as O,w as $,u as k,d as S,r as u,g as y,x as H,y as W,B as g,P as q,C as Q,z as J,A as T,D as U,m as N,l as G,T as E,I as j,F as Y,H as C,J as B,K as V}from"./index-B7s6g2Bv.js";import{C as f,a as z,b as _}from"./card-abtg_WiG.js";import{u as X,i as M,a as L}from"./useChart-oVrBsOjL.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 * @license lucide-react v0.475.0 - ISC
* *
* This source code is licensed under the ISC license. * This source code is licensed under the ISC license.
@@ -1,4 +1,4 @@
import{c as pe,j as e,a as b,r as s,b as fe,u as he,d as ve,e as ge,f as L,g as je,I as ye,S as N,B as k,P as Q,h as ee,L as te,G as se,R as be,i as ae,k as Ne,l as ke,T as we,E as ne,m as Ce,n as Ee,o as Se,C as _e,p as ze,q as qe,s as Le,t as Re,v as De}from"./index-GKqw9oy7.js";/** import{c as pe,j as e,a as b,r as s,b as fe,u as he,d as ve,e as ge,f as L,g as je,I as ye,S as N,B as k,P as Q,h as ee,L as te,G as se,R as be,i as ae,k as Ne,l as ke,T as we,E as ne,m as Ce,n as Ee,o as Se,C as _e,p as ze,q as qe,s as Le,t as Re,v as De}from"./index-B7s6g2Bv.js";/**
* @license lucide-react v0.475.0 - ISC * @license lucide-react v0.475.0 - ISC
* *
* This source code is licensed under the ISC license. * 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 D,u as K,d as F,f as L,r,g as I,j as e,B as p,P as q,M as O,m as _,l as H,T as P,C as R,N as G,F as U,H as i,I as j,O as W,S as z,Q as J,U as V,q as X}from"./index-GKqw9oy7.js";import{C as Y}from"./card-C3qAWMF6.js";/** import{c as D,u as K,d as F,f as L,r,g as I,j as e,B as p,P as q,M as O,m as _,l as H,T as P,C as R,N as G,F as U,H as i,I as j,O as W,S as z,Q as J,U as V,q as X}from"./index-B7s6g2Bv.js";import{C as Y}from"./card-abtg_WiG.js";/**
* @license lucide-react v0.475.0 - ISC * @license lucide-react v0.475.0 - ISC
* *
* This source code is licensed under the ISC license. * This source code is licensed under the ISC license.
@@ -1 +1 @@
import{j as s,a}from"./index-GKqw9oy7.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-B7s6g2Bv.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-GKqw9oy7.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-B7s6g2Bv.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="icon" href="/icons/pwa-192.png" />
<link rel="apple-touch-icon" href="/icons/pwa-192.png" /> <link rel="apple-touch-icon" href="/icons/pwa-192.png" />
<title>足迹 Footprints</title> <title>足迹 Footprints</title>
<script type="module" crossorigin src="/assets/index-GKqw9oy7.js"></script> <script type="module" crossorigin src="/assets/index-B7s6g2Bv.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-D6K8zE2F.css"> <link rel="stylesheet" crossorigin href="/assets/index-D6K8zE2F.css">
</head> </head>
<body> <body>