限制首页 Feed 自动加载上限,超出后支持手动加载更多历史帖子。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
freefire
2026-06-16 06:11:33 +08:00
parent 0eae3f8c47
commit 9767b54192
34 changed files with 147 additions and 61 deletions

View File

@@ -1 +1 @@
import{e as A,r as i,j as e}from"./react-vendor-1fwFMt02.js";import{i as D,S as C,B as d,a as m,n as c}from"./index-kfMSnNdB.js";import{B as N}from"./badge-C0Go598m.js";import{A as y,a as b,b as k,c as S,d as _,e as z,f as B,g as E,h as P}from"./alert-dialog-_wPjKojj.js";import"./ui-vendor-Gb6mai95.js";function G(){const j=A(),{ready:r}=D(),[o,g]=i.useState([]),[p,h]=i.useState(!0),[a,u]=i.useState(1),[l,f]=i.useState(1),n=(s=a)=>{h(!0),m.adminComments(s).then(t=>{g(t.comments??[]),u(t.page),f(t.total_pages)}).catch(t=>c.error(t.message)).finally(()=>h(!1))};i.useEffect(()=>{r&&n(1)},[r]);const v=async s=>{try{await m.adminDeleteComment(s),c.success("评论已删除"),n()}catch(t){c.error(t instanceof Error?t.message:"删除失败")}};return r?e.jsxs("div",{className:"admin-page",children:[e.jsxs("div",{className:"admin-page-head",children:[e.jsx("h1",{children:"评论管理"}),e.jsx("p",{children:"查看与删除楼层评论"})]}),e.jsx("div",{className:"admin-card",children:p?e.jsx("div",{className:"flex justify-center py-12",children:e.jsx(C,{size:"lg"})}):e.jsxs(e.Fragment,{children:[e.jsxs("table",{className:"admin-table",children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"ID"}),e.jsx("th",{children:"楼层"}),e.jsx("th",{children:"帖子"}),e.jsx("th",{children:"作者"}),e.jsx("th",{children:"内容"}),e.jsx("th",{children:"私密"}),e.jsx("th",{children:"时间"}),e.jsx("th",{children:"操作"})]})}),e.jsx("tbody",{children:o.map(s=>{var t,x;return e.jsxs("tr",{children:[e.jsx("td",{children:s.id}),e.jsxs("td",{children:["#",s.floor]}),e.jsx("td",{children:e.jsx("button",{type:"button",className:"admin-text-link",onClick:()=>j(`/post/${s.post_id}`),children:((t=s.post)==null?void 0:t.title)??`#${s.post_id}`})}),e.jsx("td",{children:((x=s.user)==null?void 0:x.nickname)||s.guest_nick||"游客"}),e.jsx("td",{className:"max-w-[200px] truncate",children:s.content}),e.jsx("td",{children:s.is_private?e.jsx(N,{variant:"secondary",children:"是"}):"—"}),e.jsx("td",{children:new Date(s.created_at).toLocaleString("zh-CN")}),e.jsx("td",{children:e.jsxs(y,{children:[e.jsx(b,{asChild:!0,children:e.jsx(d,{size:"sm",variant:"ghost",className:"text-destructive",children:"删除"})}),e.jsxs(k,{children:[e.jsxs(S,{children:[e.jsx(_,{children:"确定删除该评论?"}),e.jsx(z,{children:"此操作不可恢复。"})]}),e.jsxs(B,{children:[e.jsx(E,{children:"取消"}),e.jsx(P,{onClick:()=>v(s.id),children:"删除"})]})]})]})})]},s.id)})})]}),o.length===0&&e.jsx("div",{className:"admin-empty",children:"暂无评论"}),l>1&&e.jsxs("div",{className:"admin-pagination",children:[e.jsx(d,{size:"sm",variant:"outline",disabled:a<=1,onClick:()=>n(a-1),children:"上一页"}),e.jsxs("span",{children:["第 ",a," / ",l," 页"]}),e.jsx(d,{size:"sm",variant:"outline",disabled:a>=l,onClick:()=>n(a+1),children:"下一页"})]})]})})]}):null}export{G as default};
import{e as A,r as i,j as e}from"./react-vendor-1fwFMt02.js";import{i as D,S as C,B as d,a as m,n as c}from"./index-BCHo3nLu.js";import{B as N}from"./badge-C7JnHIoR.js";import{A as y,a as b,b as k,c as S,d as _,e as z,f as B,g as E,h as P}from"./alert-dialog-C_2cPHgh.js";import"./ui-vendor-Gb6mai95.js";function G(){const j=A(),{ready:r}=D(),[o,g]=i.useState([]),[p,h]=i.useState(!0),[a,u]=i.useState(1),[l,f]=i.useState(1),n=(s=a)=>{h(!0),m.adminComments(s).then(t=>{g(t.comments??[]),u(t.page),f(t.total_pages)}).catch(t=>c.error(t.message)).finally(()=>h(!1))};i.useEffect(()=>{r&&n(1)},[r]);const v=async s=>{try{await m.adminDeleteComment(s),c.success("评论已删除"),n()}catch(t){c.error(t instanceof Error?t.message:"删除失败")}};return r?e.jsxs("div",{className:"admin-page",children:[e.jsxs("div",{className:"admin-page-head",children:[e.jsx("h1",{children:"评论管理"}),e.jsx("p",{children:"查看与删除楼层评论"})]}),e.jsx("div",{className:"admin-card",children:p?e.jsx("div",{className:"flex justify-center py-12",children:e.jsx(C,{size:"lg"})}):e.jsxs(e.Fragment,{children:[e.jsxs("table",{className:"admin-table",children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"ID"}),e.jsx("th",{children:"楼层"}),e.jsx("th",{children:"帖子"}),e.jsx("th",{children:"作者"}),e.jsx("th",{children:"内容"}),e.jsx("th",{children:"私密"}),e.jsx("th",{children:"时间"}),e.jsx("th",{children:"操作"})]})}),e.jsx("tbody",{children:o.map(s=>{var t,x;return e.jsxs("tr",{children:[e.jsx("td",{children:s.id}),e.jsxs("td",{children:["#",s.floor]}),e.jsx("td",{children:e.jsx("button",{type:"button",className:"admin-text-link",onClick:()=>j(`/post/${s.post_id}`),children:((t=s.post)==null?void 0:t.title)??`#${s.post_id}`})}),e.jsx("td",{children:((x=s.user)==null?void 0:x.nickname)||s.guest_nick||"游客"}),e.jsx("td",{className:"max-w-[200px] truncate",children:s.content}),e.jsx("td",{children:s.is_private?e.jsx(N,{variant:"secondary",children:"是"}):"—"}),e.jsx("td",{children:new Date(s.created_at).toLocaleString("zh-CN")}),e.jsx("td",{children:e.jsxs(y,{children:[e.jsx(b,{asChild:!0,children:e.jsx(d,{size:"sm",variant:"ghost",className:"text-destructive",children:"删除"})}),e.jsxs(k,{children:[e.jsxs(S,{children:[e.jsx(_,{children:"确定删除该评论?"}),e.jsx(z,{children:"此操作不可恢复。"})]}),e.jsxs(B,{children:[e.jsx(E,{children:"取消"}),e.jsx(P,{onClick:()=>v(s.id),children:"删除"})]})]})]})})]},s.id)})})]}),o.length===0&&e.jsx("div",{className:"admin-empty",children:"暂无评论"}),l>1&&e.jsxs("div",{className:"admin-pagination",children:[e.jsx(d,{size:"sm",variant:"outline",disabled:a<=1,onClick:()=>n(a-1),children:"上一页"}),e.jsxs("span",{children:["第 ",a," / ",l," 页"]}),e.jsx(d,{size:"sm",variant:"outline",disabled:a>=l,onClick:()=>n(a+1),children:"下一页"})]})]})})]}):null}export{G as default};

View File

@@ -1 +1 @@
import{e as h,r as n,j as s}from"./react-vendor-1fwFMt02.js";import{i as o,a as x,S as j}from"./index-kfMSnNdB.js";import{B as u}from"./badge-C0Go598m.js";import"./ui-vendor-Gb6mai95.js";function f(){const i=h(),{ready:t}=o(),[e,d]=n.useState(null),[r,c]=n.useState(!0);if(n.useEffect(()=>{t&&x.adminDashboard().then(d).finally(()=>c(!1))},[t]),!t||r)return s.jsx("div",{className:"flex justify-center py-16",children:s.jsx(j,{size:"lg"})});if(!e)return null;const m=[{label:"注册用户",value:e.users,cls:"admin-stat-users"},{label:"帖子总数",value:e.posts,cls:"admin-stat-posts"},{label:"板块数量",value:e.boards,cls:"admin-stat-boards"},{label:"评论总数",value:e.comments,cls:"admin-stat-comments"},{label:"当前在线",value:e.online,cls:"admin-stat-online"}];return s.jsxs("div",{className:"admin-page",children:[s.jsxs("div",{className:"admin-page-head",children:[s.jsx("h1",{children:"仪表盘"}),s.jsx("p",{children:"论坛运行概览与最新帖子"})]}),s.jsx("div",{className:"admin-stat-grid",children:m.map(a=>s.jsxs("div",{className:`admin-stat-card ${a.cls}`,children:[s.jsx("div",{className:"admin-stat-value",children:a.value}),s.jsx("div",{className:"admin-stat-label",children:a.label})]},a.label))}),s.jsxs("div",{className:"admin-card",children:[s.jsxs("div",{className:"admin-card-head",children:[s.jsx("span",{children:"最新帖子"}),s.jsx("button",{type:"button",className:"admin-text-link",onClick:()=>i("/admin/posts"),children:"查看全部 →"})]}),s.jsxs("table",{className:"admin-table",children:[s.jsx("thead",{children:s.jsxs("tr",{children:[s.jsx("th",{children:"ID"}),s.jsx("th",{children:"标题"}),s.jsx("th",{children:"作者"}),s.jsx("th",{children:"置顶"}),s.jsx("th",{children:"时间"})]})}),s.jsx("tbody",{children:e.recent_posts.map(a=>{var l;return s.jsxs("tr",{children:[s.jsx("td",{children:a.id}),s.jsx("td",{children:s.jsx("button",{type:"button",className:"admin-text-link",onClick:()=>i(`/post/${a.id}`),children:a.title})}),s.jsx("td",{children:((l=a.user)==null?void 0:l.nickname)??"—"}),s.jsx("td",{children:a.pinned?s.jsx(u,{variant:"orange",children:"是"}):"—"}),s.jsx("td",{children:new Date(a.created_at).toLocaleString("zh-CN")})]},a.id)})})]}),e.recent_posts.length===0&&s.jsx("div",{className:"admin-empty",children:"暂无帖子"})]})]})}export{f as default};
import{e as h,r as n,j as s}from"./react-vendor-1fwFMt02.js";import{i as o,a as x,S as j}from"./index-BCHo3nLu.js";import{B as u}from"./badge-C7JnHIoR.js";import"./ui-vendor-Gb6mai95.js";function f(){const i=h(),{ready:t}=o(),[e,d]=n.useState(null),[r,c]=n.useState(!0);if(n.useEffect(()=>{t&&x.adminDashboard().then(d).finally(()=>c(!1))},[t]),!t||r)return s.jsx("div",{className:"flex justify-center py-16",children:s.jsx(j,{size:"lg"})});if(!e)return null;const m=[{label:"注册用户",value:e.users,cls:"admin-stat-users"},{label:"帖子总数",value:e.posts,cls:"admin-stat-posts"},{label:"板块数量",value:e.boards,cls:"admin-stat-boards"},{label:"评论总数",value:e.comments,cls:"admin-stat-comments"},{label:"当前在线",value:e.online,cls:"admin-stat-online"}];return s.jsxs("div",{className:"admin-page",children:[s.jsxs("div",{className:"admin-page-head",children:[s.jsx("h1",{children:"仪表盘"}),s.jsx("p",{children:"论坛运行概览与最新帖子"})]}),s.jsx("div",{className:"admin-stat-grid",children:m.map(a=>s.jsxs("div",{className:`admin-stat-card ${a.cls}`,children:[s.jsx("div",{className:"admin-stat-value",children:a.value}),s.jsx("div",{className:"admin-stat-label",children:a.label})]},a.label))}),s.jsxs("div",{className:"admin-card",children:[s.jsxs("div",{className:"admin-card-head",children:[s.jsx("span",{children:"最新帖子"}),s.jsx("button",{type:"button",className:"admin-text-link",onClick:()=>i("/admin/posts"),children:"查看全部 →"})]}),s.jsxs("table",{className:"admin-table",children:[s.jsx("thead",{children:s.jsxs("tr",{children:[s.jsx("th",{children:"ID"}),s.jsx("th",{children:"标题"}),s.jsx("th",{children:"作者"}),s.jsx("th",{children:"置顶"}),s.jsx("th",{children:"时间"})]})}),s.jsx("tbody",{children:e.recent_posts.map(a=>{var l;return s.jsxs("tr",{children:[s.jsx("td",{children:a.id}),s.jsx("td",{children:s.jsx("button",{type:"button",className:"admin-text-link",onClick:()=>i(`/post/${a.id}`),children:a.title})}),s.jsx("td",{children:((l=a.user)==null?void 0:l.nickname)??"—"}),s.jsx("td",{children:a.pinned?s.jsx(u,{variant:"orange",children:"是"}):"—"}),s.jsx("td",{children:new Date(a.created_at).toLocaleString("zh-CN")})]},a.id)})})]}),e.recent_posts.length===0&&s.jsx("div",{className:"admin-empty",children:"暂无帖子"})]})]})}export{f as default};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
import{r as i,j as e}from"./react-vendor-1fwFMt02.js";import{i as v,S as y,B as l,a as j,n as r}from"./index-kfMSnNdB.js";import{B as h}from"./badge-C0Go598m.js";import"./ui-vendor-Gb6mai95.js";function z(){const{ready:d}=v(),[o,m]=i.useState([]),[g,x]=i.useState(!0),[n,p]=i.useState(1),[c,f]=i.useState(1),t=(s=n)=>{x(!0),j.adminUsers(s).then(a=>{m(a.users??[]),p(a.page),f(a.total_pages)}).catch(a=>r.error(a.message)).finally(()=>x(!1))};i.useEffect(()=>{d&&t(1)},[d]);const u=async s=>{if(s.role==="admin"){r.warning("不能禁言管理员");return}try{const a=await j.adminBanUser(s.id,!s.banned);r.success(a.message),t()}catch(a){r.error(a instanceof Error?a.message:"操作失败")}};return d?e.jsxs("div",{className:"admin-page",children:[e.jsxs("div",{className:"admin-page-head",children:[e.jsx("h1",{children:"用户管理"}),e.jsx("p",{children:"查看注册用户,禁言或解除禁言"})]}),e.jsx("div",{className:"admin-card",children:g?e.jsx("div",{className:"flex justify-center py-12",children:e.jsx(y,{size:"lg"})}):e.jsxs(e.Fragment,{children:[e.jsxs("table",{className:"admin-table",children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"ID"}),e.jsx("th",{children:"用户名"}),e.jsx("th",{children:"昵称"}),e.jsx("th",{children:"角色"}),e.jsx("th",{children:"状态"}),e.jsx("th",{children:"注册时间"}),e.jsx("th",{children:"操作"})]})}),e.jsx("tbody",{children:o.map(s=>e.jsxs("tr",{children:[e.jsx("td",{children:s.id}),e.jsx("td",{children:s.username}),e.jsx("td",{children:s.nickname}),e.jsx("td",{children:s.role==="admin"?e.jsx(h,{variant:"orange",children:"管理员"}):e.jsx(h,{variant:"secondary",children:"用户"})}),e.jsx("td",{children:s.banned?e.jsx(h,{variant:"destructive",children:"已禁言"}):"正常"}),e.jsx("td",{children:s.created_at?new Date(s.created_at).toLocaleString("zh-CN"):"—"}),e.jsx("td",{children:s.role!=="admin"&&e.jsx(l,{size:"sm",variant:"outline",onClick:()=>u(s),children:s.banned?"解除禁言":"禁言"})})]},s.id))})]}),o.length===0&&e.jsx("div",{className:"admin-empty",children:"暂无用户"}),c>1&&e.jsxs("div",{className:"admin-pagination",children:[e.jsx(l,{size:"sm",variant:"outline",disabled:n<=1,onClick:()=>t(n-1),children:"上一页"}),e.jsxs("span",{children:["第 ",n," / ",c," 页"]}),e.jsx(l,{size:"sm",variant:"outline",disabled:n>=c,onClick:()=>t(n+1),children:"下一页"})]})]})})]}):null}export{z as default};
import{r as i,j as e}from"./react-vendor-1fwFMt02.js";import{i as v,S as y,B as l,a as j,n as r}from"./index-BCHo3nLu.js";import{B as h}from"./badge-C7JnHIoR.js";import"./ui-vendor-Gb6mai95.js";function z(){const{ready:d}=v(),[o,m]=i.useState([]),[g,x]=i.useState(!0),[n,p]=i.useState(1),[c,f]=i.useState(1),t=(s=n)=>{x(!0),j.adminUsers(s).then(a=>{m(a.users??[]),p(a.page),f(a.total_pages)}).catch(a=>r.error(a.message)).finally(()=>x(!1))};i.useEffect(()=>{d&&t(1)},[d]);const u=async s=>{if(s.role==="admin"){r.warning("不能禁言管理员");return}try{const a=await j.adminBanUser(s.id,!s.banned);r.success(a.message),t()}catch(a){r.error(a instanceof Error?a.message:"操作失败")}};return d?e.jsxs("div",{className:"admin-page",children:[e.jsxs("div",{className:"admin-page-head",children:[e.jsx("h1",{children:"用户管理"}),e.jsx("p",{children:"查看注册用户,禁言或解除禁言"})]}),e.jsx("div",{className:"admin-card",children:g?e.jsx("div",{className:"flex justify-center py-12",children:e.jsx(y,{size:"lg"})}):e.jsxs(e.Fragment,{children:[e.jsxs("table",{className:"admin-table",children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"ID"}),e.jsx("th",{children:"用户名"}),e.jsx("th",{children:"昵称"}),e.jsx("th",{children:"角色"}),e.jsx("th",{children:"状态"}),e.jsx("th",{children:"注册时间"}),e.jsx("th",{children:"操作"})]})}),e.jsx("tbody",{children:o.map(s=>e.jsxs("tr",{children:[e.jsx("td",{children:s.id}),e.jsx("td",{children:s.username}),e.jsx("td",{children:s.nickname}),e.jsx("td",{children:s.role==="admin"?e.jsx(h,{variant:"orange",children:"管理员"}):e.jsx(h,{variant:"secondary",children:"用户"})}),e.jsx("td",{children:s.banned?e.jsx(h,{variant:"destructive",children:"已禁言"}):"正常"}),e.jsx("td",{children:s.created_at?new Date(s.created_at).toLocaleString("zh-CN"):"—"}),e.jsx("td",{children:s.role!=="admin"&&e.jsx(l,{size:"sm",variant:"outline",onClick:()=>u(s),children:s.banned?"解除禁言":"禁言"})})]},s.id))})]}),o.length===0&&e.jsx("div",{className:"admin-empty",children:"暂无用户"}),c>1&&e.jsxs("div",{className:"admin-pagination",children:[e.jsx(l,{size:"sm",variant:"outline",disabled:n<=1,onClick:()=>t(n-1),children:"上一页"}),e.jsxs("span",{children:["第 ",n," / ",c," 页"]}),e.jsx(l,{size:"sm",variant:"outline",disabled:n>=c,onClick:()=>t(n+1),children:"下一页"})]})]})})]}):null}export{z as default};

View File

@@ -1,4 +1,4 @@
var ap=Object.defineProperty;var cp=(n,e,t)=>e in n?ap(n,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):n[e]=t;var V=(n,e,t)=>cp(n,typeof e!="symbol"?e+"":e,t);import{r as N,k as Ft,j as b,a as up,m as dp,e as hp,w as fp,i as pp}from"./react-vendor-1fwFMt02.js";import{B as gi,f as Yl,a as en,n as He,u as mp,h as gp,S as Xl}from"./index-kfMSnNdB.js";import{P as lu,r as yp,i as kp}from"./postContent-ClrHK1rL.js";import{p as au}from"./purify-vendor-Cx4rtWv4.js";import{O as to,Q as bp,V as Ql,W as Zl,Y as ea,Z as ta,_ as na,$ as ra,a0 as sa,a1 as ia,a2 as oa,a3 as la,a4 as aa,a5 as xp,a6 as Sp,B as wp,D as vp,A as Ep,x as Cp,a7 as Tp}from"./ui-vendor-Gb6mai95.js";import{D as Mp,a as Ap,b as Np,c as Op,d as Rp,e as Ip}from"./dialog-DJohwkVv.js";import{I as Dp}from"./input-BjHCK3cg.js";var cu={exports:{}},uu={};/**
var ap=Object.defineProperty;var cp=(n,e,t)=>e in n?ap(n,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):n[e]=t;var V=(n,e,t)=>cp(n,typeof e!="symbol"?e+"":e,t);import{r as N,k as Ft,j as b,a as up,m as dp,e as hp,w as fp,i as pp}from"./react-vendor-1fwFMt02.js";import{B as gi,f as Yl,a as en,n as He,h as mp,u as gp,S as Xl}from"./index-BCHo3nLu.js";import{P as lu,r as yp,i as kp}from"./postContent-ClrHK1rL.js";import{p as au}from"./purify-vendor-Cx4rtWv4.js";import{O as to,Q as bp,V as Ql,W as Zl,Y as ea,Z as ta,_ as na,$ as ra,a0 as sa,a1 as ia,a2 as oa,a3 as la,a4 as aa,a5 as xp,a6 as Sp,B as wp,D as vp,A as Ep,x as Cp,a7 as Tp}from"./ui-vendor-Gb6mai95.js";import{D as Mp,a as Ap,b as Np,c as Op,d as Rp,e as Ip}from"./dialog-CXDeMxB9.js";import{I as Dp}from"./input-Dsk6No-h.js";var cu={exports:{}},uu={};/**
* @license React
* use-sync-external-store-shim.production.js
*

View File

@@ -1 +1 @@
import{e as j,r as n,j as s}from"./react-vendor-1fwFMt02.js";import{u as f,a as g,n as N,S as v,B as m}from"./index-kfMSnNdB.js";import{f as y}from"./content-BrFHUpp8.js";import{A as k}from"./ui-vendor-Gb6mai95.js";function S(){const a=j(),{user:t,loading:i}=f(),[r,h]=n.useState([]),[u,x]=n.useState(!0);return n.useEffect(()=>{if(!i){if(!t){a("/login");return}g.favorites().then(e=>h(Array.isArray(e.favorites)?e.favorites:[])).catch(e=>N.error(e.message)).finally(()=>x(!1))}},[t,i,a]),i||u?s.jsx("div",{className:"flex justify-center py-16",children:s.jsx(v,{size:"lg"})}):t?s.jsx("div",{className:"page-wrap",children:s.jsxs("div",{className:"page-inner-wide",children:[s.jsxs(m,{variant:"ghost",className:"mb-3",onClick:()=>a("/"),children:[s.jsx(k,{}),"返回"]}),s.jsx("h1",{className:"page-title",children:"我的收藏"}),s.jsxs("p",{className:"page-desc",children:["共 ",r.length," 篇收藏帖子"]}),r.length===0?s.jsxs("div",{className:"empty-state",children:[s.jsx("p",{children:"还没有收藏任何帖子"}),s.jsx(m,{onClick:()=>a("/"),children:"去逛逛"})]}):s.jsx("div",{className:"content-surface",children:r.map(e=>{var o,l,c,d,p;return s.jsx("div",{className:"post-row",onClick:()=>a(`/post/${e.post_id}`),children:s.jsxs("div",{className:"post-body",children:[s.jsx("div",{className:"post-title",children:((o=e.post)==null?void 0:o.title)||"帖子已删除"}),s.jsxs("div",{className:"post-meta",children:[((c=(l=e.post)==null?void 0:l.board)==null?void 0:c.name)&&s.jsx("span",{children:e.post.board.name}),((p=(d=e.post)==null?void 0:d.user)==null?void 0:p.nickname)&&s.jsx("span",{children:e.post.user.nickname}),s.jsxs("span",{children:["收藏于 ",y(e.created_at)]})]})]})},e.id)})})]})}):null}export{S as default};
import{e as j,r as n,j as s}from"./react-vendor-1fwFMt02.js";import{h as f,a as g,n as N,S as v,B as m}from"./index-BCHo3nLu.js";import{f as y}from"./content-BrFHUpp8.js";import{A as k}from"./ui-vendor-Gb6mai95.js";function S(){const a=j(),{user:t,loading:i}=f(),[r,h]=n.useState([]),[x,u]=n.useState(!0);return n.useEffect(()=>{if(!i){if(!t){a("/login");return}g.favorites().then(e=>h(Array.isArray(e.favorites)?e.favorites:[])).catch(e=>N.error(e.message)).finally(()=>u(!1))}},[t,i,a]),i||x?s.jsx("div",{className:"flex justify-center py-16",children:s.jsx(v,{size:"lg"})}):t?s.jsx("div",{className:"page-wrap",children:s.jsxs("div",{className:"page-inner-wide",children:[s.jsxs(m,{variant:"ghost",className:"mb-3",onClick:()=>a("/"),children:[s.jsx(k,{}),"返回"]}),s.jsx("h1",{className:"page-title",children:"我的收藏"}),s.jsxs("p",{className:"page-desc",children:["共 ",r.length," 篇收藏帖子"]}),r.length===0?s.jsxs("div",{className:"empty-state",children:[s.jsx("p",{children:"还没有收藏任何帖子"}),s.jsx(m,{onClick:()=>a("/"),children:"去逛逛"})]}):s.jsx("div",{className:"content-surface",children:r.map(e=>{var o,l,c,d,p;return s.jsx("div",{className:"post-row",onClick:()=>a(`/post/${e.post_id}`),children:s.jsxs("div",{className:"post-body",children:[s.jsx("div",{className:"post-title",children:((o=e.post)==null?void 0:o.title)||"帖子已删除"}),s.jsxs("div",{className:"post-meta",children:[((c=(l=e.post)==null?void 0:l.board)==null?void 0:c.name)&&s.jsx("span",{children:e.post.board.name}),((p=(d=e.post)==null?void 0:d.user)==null?void 0:p.nickname)&&s.jsx("span",{children:e.post.user.nickname}),s.jsxs("span",{children:["收藏于 ",y(e.created_at)]})]})]})},e.id)})})]})}):null}export{S as default};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
import{e as f,r as g,j as s,L as b}from"./react-vendor-1fwFMt02.js";import{u as w,a as F,F as y,b as t,c as n,d as l,e as c,f as i,o as N,s as m}from"./form-PYX6hP9B.js";import{u as v,B as L,a as S,n as d}from"./index-kfMSnNdB.js";import{I as u}from"./input-BjHCK3cg.js";import"./ui-vendor-Gb6mai95.js";const C=N({username:m().min(1,"请输入用户名"),password:m().min(1,"请输入密码")});function z(){const x=f(),{refresh:p}=v(),[h,a]=g.useState(!1),r=w({resolver:F(C),defaultValues:{username:"",password:""}}),j=async e=>{a(!0);try{await S.login(e.username,e.password),await p(),d.success("登录成功"),x("/",{replace:!0})}catch(o){d.error(o instanceof Error?o.message:"登录失败")}finally{a(!1)}};return s.jsx("div",{className:"auth-page",children:s.jsxs("div",{className:"auth-box",children:[s.jsx("div",{className:"logo-mark",children:"姜"}),s.jsx("h1",{children:"登录姜十三论坛"}),s.jsx("p",{className:"subtitle",children:"拾三一隅,自在交流"}),s.jsx(y,{...r,children:s.jsxs("form",{onSubmit:r.handleSubmit(j),className:"space-y-4",children:[s.jsx(t,{control:r.control,name:"username",render:({field:e})=>s.jsxs(n,{children:[s.jsx(l,{children:"用户名"}),s.jsx(c,{children:s.jsx(u,{placeholder:"用户名",autoComplete:"username",...e})}),s.jsx(i,{})]})}),s.jsx(t,{control:r.control,name:"password",render:({field:e})=>s.jsxs(n,{children:[s.jsx(l,{children:"密码"}),s.jsx(c,{children:s.jsx(u,{type:"password",placeholder:"密码",autoComplete:"current-password",...e})}),s.jsx(i,{})]})}),s.jsx(L,{type:"submit",className:"w-full",loading:h,children:"登录"})]})}),s.jsxs("p",{style:{textAlign:"center",marginTop:16,fontSize:13,color:"var(--color-text-3)"},children:["没有账号?",s.jsx(b,{to:"/register",children:"注册"})]})]})})}export{z as default};
import{e as f,r as g,j as s,L as b}from"./react-vendor-1fwFMt02.js";import{u as w,a as F,F as y,b as t,c as n,d as l,e as c,f as i,o as N,s as m}from"./form-CB9KBlPG.js";import{h as v,B as L,a as S,n as d}from"./index-BCHo3nLu.js";import{I as u}from"./input-Dsk6No-h.js";import"./ui-vendor-Gb6mai95.js";const C=N({username:m().min(1,"请输入用户名"),password:m().min(1,"请输入密码")});function z(){const x=f(),{refresh:p}=v(),[h,a]=g.useState(!1),r=w({resolver:F(C),defaultValues:{username:"",password:""}}),j=async e=>{a(!0);try{await S.login(e.username,e.password),await p(),d.success("登录成功"),x("/",{replace:!0})}catch(o){d.error(o instanceof Error?o.message:"登录失败")}finally{a(!1)}};return s.jsx("div",{className:"auth-page",children:s.jsxs("div",{className:"auth-box",children:[s.jsx("div",{className:"logo-mark",children:"姜"}),s.jsx("h1",{children:"登录姜十三论坛"}),s.jsx("p",{className:"subtitle",children:"拾三一隅,自在交流"}),s.jsx(y,{...r,children:s.jsxs("form",{onSubmit:r.handleSubmit(j),className:"space-y-4",children:[s.jsx(t,{control:r.control,name:"username",render:({field:e})=>s.jsxs(n,{children:[s.jsx(l,{children:"用户名"}),s.jsx(c,{children:s.jsx(u,{placeholder:"用户名",autoComplete:"username",...e})}),s.jsx(i,{})]})}),s.jsx(t,{control:r.control,name:"password",render:({field:e})=>s.jsxs(n,{children:[s.jsx(l,{children:"密码"}),s.jsx(c,{children:s.jsx(u,{type:"password",placeholder:"密码",autoComplete:"current-password",...e})}),s.jsx(i,{})]})}),s.jsx(L,{type:"submit",className:"w-full",loading:h,children:"登录"})]})}),s.jsxs("p",{style:{textAlign:"center",marginTop:16,fontSize:13,color:"var(--color-text-3)"},children:["没有账号?",s.jsx(b,{to:"/register",children:"注册"})]})]})})}export{z as default};

View File

@@ -1 +1 @@
import{j as n}from"./react-vendor-1fwFMt02.js";import{f as i}from"./index-kfMSnNdB.js";import{N as s}from"./ui-vendor-Gb6mai95.js";function m({className:o,size:r=16}){return n.jsx(s,{className:i("post-pinned-icon",o),size:r,fill:"currentColor","aria-label":"置顶",role:"img"})}export{m as P};
import{j as n}from"./react-vendor-1fwFMt02.js";import{f as i}from"./index-BCHo3nLu.js";import{N as s}from"./ui-vendor-Gb6mai95.js";function m({className:o,size:r=16}){return n.jsx(s,{className:i("post-pinned-icon",o),size:r,fill:"currentColor","aria-label":"置顶",role:"img"})}export{m as P};

View File

@@ -1,4 +1,4 @@
import{x as Gt,r as u,j as s,e as Ht}from"./react-vendor-1fwFMt02.js";import{u as ut,a as ft,F as mt,c as L,d as F,e as T,b as Z,f as G,o as Pt,s as X}from"./form-PYX6hP9B.js";import{S as et,B as P,n as z,u as Bt,h as Xt,a as H}from"./index-kfMSnNdB.js";import{I as W}from"./input-BjHCK3cg.js";import{B as Yt}from"./badge-C0Go598m.js";import{D as Vt,a as Kt,b as qt,c as Jt,d as Qt,e as $t}from"./dialog-DJohwkVv.js";import{A as te,a8 as ee,a9 as ie,X as oe,t as re,k as se}from"./ui-vendor-Gb6mai95.js";var gt=!1,E,it,ot,Y,V,Dt,K,rt,st,nt,Rt,at,ht,zt,Et;function C(){if(!gt){gt=!0;var i=navigator.userAgent,e=/(?:MSIE.(\d+\.\d+))|(?:(?:Firefox|GranParadiso|Iceweasel).(\d+\.\d+))|(?:Opera(?:.+Version.|.)(\d+\.\d+))|(?:AppleWebKit.(\d+(?:\.\d+)?))|(?:Trident\/\d+\.\d+.*rv:(\d+\.\d+))/.exec(i),t=/(Mac OS X)|(Windows)|(Linux)/.exec(i);if(at=/\b(iPhone|iP[ao]d)/.exec(i),ht=/\b(iP[ao]d)/.exec(i),nt=/Android/i.exec(i),zt=/FBAN\/\w+;/i.exec(i),Et=/Mobile/i.exec(i),Rt=!!/Win64/.exec(i),e){E=e[1]?parseFloat(e[1]):e[5]?parseFloat(e[5]):NaN,E&&document&&document.documentMode&&(E=document.documentMode);var o=/(?:Trident\/(\d+.\d+))/.exec(i);Dt=o?parseFloat(o[1])+4:E,it=e[2]?parseFloat(e[2]):NaN,ot=e[3]?parseFloat(e[3]):NaN,Y=e[4]?parseFloat(e[4]):NaN,Y?(e=/(?:Chrome\/(\d+\.\d+))/.exec(i),V=e&&e[1]?parseFloat(e[1]):NaN):V=NaN}else E=it=ot=V=Y=NaN;if(t){if(t[1]){var r=/(?:Mac OS X (\d+(?:[._]\d+)?))/.exec(i);K=r?parseFloat(r[1].replace("_",".")):!0}else K=!1;rt=!!t[2],st=!!t[3]}else K=rt=st=!1}}var ct={ie:function(){return C()||E},ieCompatibilityMode:function(){return C()||Dt>E},ie64:function(){return ct.ie()&&Rt},firefox:function(){return C()||it},opera:function(){return C()||ot},webkit:function(){return C()||Y},safari:function(){return ct.webkit()},chrome:function(){return C()||V},windows:function(){return C()||rt},osx:function(){return C()||K},linux:function(){return C()||st},iphone:function(){return C()||at},mobile:function(){return C()||at||ht||nt||Et},nativeApp:function(){return C()||zt},android:function(){return C()||nt},ipad:function(){return C()||ht}},ne=ct,ae=!!(typeof window<"u"&&window.document&&window.document.createElement),he={canUseDOM:ae},ce=he,At=ce,Mt;At.canUseDOM&&(Mt=document.implementation&&document.implementation.hasFeature&&document.implementation.hasFeature("","")!==!0);/**
import{x as Gt,r as u,j as s,e as Ht}from"./react-vendor-1fwFMt02.js";import{u as ut,a as ft,F as mt,c as L,d as F,e as T,b as Z,f as G,o as Pt,s as X}from"./form-CB9KBlPG.js";import{S as et,B as P,n as z,h as Bt,u as Xt,a as H}from"./index-BCHo3nLu.js";import{I as W}from"./input-Dsk6No-h.js";import{B as Yt}from"./badge-C7JnHIoR.js";import{D as Vt,a as Kt,b as qt,c as Jt,d as Qt,e as $t}from"./dialog-CXDeMxB9.js";import{A as te,a8 as ee,a9 as ie,X as oe,t as re,k as se}from"./ui-vendor-Gb6mai95.js";var gt=!1,E,it,ot,Y,V,Dt,K,rt,st,nt,Rt,at,ht,zt,Et;function C(){if(!gt){gt=!0;var i=navigator.userAgent,e=/(?:MSIE.(\d+\.\d+))|(?:(?:Firefox|GranParadiso|Iceweasel).(\d+\.\d+))|(?:Opera(?:.+Version.|.)(\d+\.\d+))|(?:AppleWebKit.(\d+(?:\.\d+)?))|(?:Trident\/\d+\.\d+.*rv:(\d+\.\d+))/.exec(i),t=/(Mac OS X)|(Windows)|(Linux)/.exec(i);if(at=/\b(iPhone|iP[ao]d)/.exec(i),ht=/\b(iP[ao]d)/.exec(i),nt=/Android/i.exec(i),zt=/FBAN\/\w+;/i.exec(i),Et=/Mobile/i.exec(i),Rt=!!/Win64/.exec(i),e){E=e[1]?parseFloat(e[1]):e[5]?parseFloat(e[5]):NaN,E&&document&&document.documentMode&&(E=document.documentMode);var o=/(?:Trident\/(\d+.\d+))/.exec(i);Dt=o?parseFloat(o[1])+4:E,it=e[2]?parseFloat(e[2]):NaN,ot=e[3]?parseFloat(e[3]):NaN,Y=e[4]?parseFloat(e[4]):NaN,Y?(e=/(?:Chrome\/(\d+\.\d+))/.exec(i),V=e&&e[1]?parseFloat(e[1]):NaN):V=NaN}else E=it=ot=V=Y=NaN;if(t){if(t[1]){var r=/(?:Mac OS X (\d+(?:[._]\d+)?))/.exec(i);K=r?parseFloat(r[1].replace("_",".")):!0}else K=!1;rt=!!t[2],st=!!t[3]}else K=rt=st=!1}}var ct={ie:function(){return C()||E},ieCompatibilityMode:function(){return C()||Dt>E},ie64:function(){return ct.ie()&&Rt},firefox:function(){return C()||it},opera:function(){return C()||ot},webkit:function(){return C()||Y},safari:function(){return ct.webkit()},chrome:function(){return C()||V},windows:function(){return C()||rt},osx:function(){return C()||K},linux:function(){return C()||st},iphone:function(){return C()||at},mobile:function(){return C()||at||ht||nt||Et},nativeApp:function(){return C()||zt},android:function(){return C()||nt},ipad:function(){return C()||ht}},ne=ct,ae=!!(typeof window<"u"&&window.document&&window.document.createElement),he={canUseDOM:ae},ce=he,At=ce,Mt;At.canUseDOM&&(Mt=document.implementation&&document.implementation.hasFeature&&document.implementation.hasFeature("","")!==!0);/**
* Checks if an event is supported in the current execution environment.
*
* NOTE: This will not work correctly for non-generic events such as `change`,

View File

@@ -1 +1 @@
import{e as g,r as w,j as s,L as F}from"./react-vendor-1fwFMt02.js";import{u as b,a as y,F as k,b as n,c as o,d as t,e as i,f as l,o as N,s as c}from"./form-PYX6hP9B.js";import{h as S,u as v,B as C,a as L,n as x}from"./index-kfMSnNdB.js";import{I as m}from"./input-BjHCK3cg.js";import"./ui-vendor-Gb6mai95.js";const _=r=>N({username:c().min(1,"请输入用户名"),nickname:c().optional(),password:c().min(r,`密码至少 ${r}`)});function $(){const{limits:r}=S(),p=g(),{refresh:h}=v(),[j,d]=w.useState(!1),a=b({resolver:y(_(r.password_min_len)),defaultValues:{username:"",nickname:"",password:""}}),f=async e=>{d(!0);try{await L.register(e.username,e.password,e.nickname||e.username),await h(),x.success("注册成功"),p("/",{replace:!0})}catch(u){x.error(u instanceof Error?u.message:"注册失败")}finally{d(!1)}};return s.jsx("div",{className:"auth-page",children:s.jsxs("div",{className:"auth-box",children:[s.jsx("div",{className:"logo-mark",children:"姜"}),s.jsx("h1",{children:"注册账号"}),s.jsx("p",{className:"subtitle",children:"首个注册用户自动成为管理员"}),s.jsx(k,{...a,children:s.jsxs("form",{onSubmit:a.handleSubmit(f),className:"space-y-4",children:[s.jsx(n,{control:a.control,name:"username",render:({field:e})=>s.jsxs(o,{children:[s.jsx(t,{children:"用户名"}),s.jsx(i,{children:s.jsx(m,{placeholder:"3-32 位字母数字下划线",autoComplete:"username",...e})}),s.jsx(l,{})]})}),s.jsx(n,{control:a.control,name:"nickname",render:({field:e})=>s.jsxs(o,{children:[s.jsx(t,{children:"昵称"}),s.jsx(i,{children:s.jsx(m,{placeholder:"显示名称(可选)",autoComplete:"nickname",...e})}),s.jsx(l,{})]})}),s.jsx(n,{control:a.control,name:"password",render:({field:e})=>s.jsxs(o,{children:[s.jsx(t,{children:"密码"}),s.jsx(i,{children:s.jsx(m,{type:"password",placeholder:`至少 ${r.password_min_len}`,autoComplete:"new-password",...e})}),s.jsx(l,{})]})}),s.jsx(C,{type:"submit",className:"w-full",loading:j,children:"注册"})]})}),s.jsxs("p",{style:{textAlign:"center",marginTop:16,fontSize:13,color:"var(--color-text-3)"},children:["已有账号?",s.jsx(F,{to:"/login",children:"登录"})]})]})})}export{$ as default};
import{e as g,r as w,j as s,L as F}from"./react-vendor-1fwFMt02.js";import{u as b,a as y,F as k,b as n,c as o,d as t,e as i,f as l,o as N,s as c}from"./form-CB9KBlPG.js";import{u as S,h as v,B as C,a as L,n as x}from"./index-BCHo3nLu.js";import{I as m}from"./input-Dsk6No-h.js";import"./ui-vendor-Gb6mai95.js";const _=r=>N({username:c().min(1,"请输入用户名"),nickname:c().optional(),password:c().min(r,`密码至少 ${r}`)});function $(){const{limits:r}=S(),p=g(),{refresh:h}=v(),[j,d]=w.useState(!1),a=b({resolver:y(_(r.password_min_len)),defaultValues:{username:"",nickname:"",password:""}}),f=async e=>{d(!0);try{await L.register(e.username,e.password,e.nickname||e.username),await h(),x.success("注册成功"),p("/",{replace:!0})}catch(u){x.error(u instanceof Error?u.message:"注册失败")}finally{d(!1)}};return s.jsx("div",{className:"auth-page",children:s.jsxs("div",{className:"auth-box",children:[s.jsx("div",{className:"logo-mark",children:"姜"}),s.jsx("h1",{children:"注册账号"}),s.jsx("p",{className:"subtitle",children:"首个注册用户自动成为管理员"}),s.jsx(k,{...a,children:s.jsxs("form",{onSubmit:a.handleSubmit(f),className:"space-y-4",children:[s.jsx(n,{control:a.control,name:"username",render:({field:e})=>s.jsxs(o,{children:[s.jsx(t,{children:"用户名"}),s.jsx(i,{children:s.jsx(m,{placeholder:"3-32 位字母数字下划线",autoComplete:"username",...e})}),s.jsx(l,{})]})}),s.jsx(n,{control:a.control,name:"nickname",render:({field:e})=>s.jsxs(o,{children:[s.jsx(t,{children:"昵称"}),s.jsx(i,{children:s.jsx(m,{placeholder:"显示名称(可选)",autoComplete:"nickname",...e})}),s.jsx(l,{})]})}),s.jsx(n,{control:a.control,name:"password",render:({field:e})=>s.jsxs(o,{children:[s.jsx(t,{children:"密码"}),s.jsx(i,{children:s.jsx(m,{type:"password",placeholder:`至少 ${r.password_min_len}`,autoComplete:"new-password",...e})}),s.jsx(l,{})]})}),s.jsx(C,{type:"submit",className:"w-full",loading:j,children:"注册"})]})}),s.jsxs("p",{style:{textAlign:"center",marginTop:16,fontSize:13,color:"var(--color-text-3)"},children:["已有账号?",s.jsx(F,{to:"/login",children:"登录"})]})]})})}export{$ as default};

View File

@@ -1 +1 @@
import{r as l,j as s}from"./react-vendor-1fwFMt02.js";import{aj as x,ak as g,al as N,am as r,an as i,ao as d,ap as n,aq as m,ar as c}from"./ui-vendor-Gb6mai95.js";import{f as o,k as f}from"./index-kfMSnNdB.js";const C=x,T=g,y=N,p=l.forwardRef(({className:a,...e},t)=>s.jsx(c,{className:o("fixed inset-0 z-[110] bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",a),...e,ref:t}));p.displayName=c.displayName;const u=l.forwardRef(({className:a,...e},t)=>s.jsxs(y,{children:[s.jsx(p,{}),s.jsx(r,{ref:t,className:o("fixed left-[50%] top-[50%] z-[110] grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",a),...e})]}));u.displayName=r.displayName;const A=({className:a,...e})=>s.jsx("div",{className:o("flex flex-col space-y-2 text-center sm:text-left",a),...e});A.displayName="AlertDialogHeader";const D=({className:a,...e})=>s.jsx("div",{className:o("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",a),...e});D.displayName="AlertDialogFooter";const j=l.forwardRef(({className:a,...e},t)=>s.jsx(i,{ref:t,className:o("text-lg font-semibold",a),...e}));j.displayName=i.displayName;const w=l.forwardRef(({className:a,...e},t)=>s.jsx(d,{ref:t,className:o("text-sm text-muted-foreground",a),...e}));w.displayName=d.displayName;const b=l.forwardRef(({className:a,...e},t)=>s.jsx(m,{ref:t,className:o(f(),a),...e}));b.displayName=m.displayName;const R=l.forwardRef(({className:a,...e},t)=>s.jsx(n,{ref:t,className:o(f({variant:"outline"}),"mt-2 sm:mt-0",a),...e}));R.displayName=n.displayName;export{C as A,T as a,u as b,A as c,j as d,w as e,D as f,R as g,b as h};
import{r as l,j as s}from"./react-vendor-1fwFMt02.js";import{aj as x,ak as g,al as N,am as r,an as i,ao as d,ap as n,aq as m,ar as c}from"./ui-vendor-Gb6mai95.js";import{f as o,k as f}from"./index-BCHo3nLu.js";const C=x,T=g,y=N,p=l.forwardRef(({className:a,...e},t)=>s.jsx(c,{className:o("fixed inset-0 z-[110] bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",a),...e,ref:t}));p.displayName=c.displayName;const u=l.forwardRef(({className:a,...e},t)=>s.jsxs(y,{children:[s.jsx(p,{}),s.jsx(r,{ref:t,className:o("fixed left-[50%] top-[50%] z-[110] grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",a),...e})]}));u.displayName=r.displayName;const A=({className:a,...e})=>s.jsx("div",{className:o("flex flex-col space-y-2 text-center sm:text-left",a),...e});A.displayName="AlertDialogHeader";const D=({className:a,...e})=>s.jsx("div",{className:o("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",a),...e});D.displayName="AlertDialogFooter";const j=l.forwardRef(({className:a,...e},t)=>s.jsx(i,{ref:t,className:o("text-lg font-semibold",a),...e}));j.displayName=i.displayName;const w=l.forwardRef(({className:a,...e},t)=>s.jsx(d,{ref:t,className:o("text-sm text-muted-foreground",a),...e}));w.displayName=d.displayName;const b=l.forwardRef(({className:a,...e},t)=>s.jsx(m,{ref:t,className:o(f(),a),...e}));b.displayName=m.displayName;const R=l.forwardRef(({className:a,...e},t)=>s.jsx(n,{ref:t,className:o(f({variant:"outline"}),"mt-2 sm:mt-0",a),...e}));R.displayName=n.displayName;export{C as A,T as a,u as b,A as c,j as d,w as e,D as f,R as g,b as h};

View File

@@ -1 +1 @@
import{j as n}from"./react-vendor-1fwFMt02.js";import{f as a,j as o}from"./index-kfMSnNdB.js";const s=o("inline-flex items-center rounded-md border px-2 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",{variants:{variant:{default:"border-transparent bg-primary text-primary-foreground shadow",secondary:"border-transparent bg-secondary text-secondary-foreground",destructive:"border-transparent bg-destructive text-destructive-foreground shadow",outline:"text-foreground",green:"border-transparent bg-[var(--j13-green-bg)] text-[var(--j13-green)]",orange:"border-transparent bg-orange-100 text-orange-700 dark:bg-orange-950 dark:text-orange-300"}},defaultVariants:{variant:"default"}});function g({className:r,variant:e,...t}){return n.jsx("div",{className:a(s({variant:e}),r),...t})}export{g as B};
import{j as n}from"./react-vendor-1fwFMt02.js";import{f as a,j as o}from"./index-BCHo3nLu.js";const s=o("inline-flex items-center rounded-md border px-2 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",{variants:{variant:{default:"border-transparent bg-primary text-primary-foreground shadow",secondary:"border-transparent bg-secondary text-secondary-foreground",destructive:"border-transparent bg-destructive text-destructive-foreground shadow",outline:"text-foreground",green:"border-transparent bg-[var(--j13-green-bg)] text-[var(--j13-green)]",orange:"border-transparent bg-orange-100 text-orange-700 dark:bg-orange-950 dark:text-orange-300"}},defaultVariants:{variant:"default"}});function g({className:r,variant:e,...t}){return n.jsx("div",{className:a(s({variant:e}),r),...t})}export{g as B};

View File

@@ -1 +1 @@
import{r as l,j as t}from"./react-vendor-1fwFMt02.js";import{ab as f,ac as p,ad as d,ae as g,X as x,af as i,ag as n,ah as r}from"./ui-vendor-Gb6mai95.js";import{f as o}from"./index-kfMSnNdB.js";const R=f,u=p,c=l.forwardRef(({className:a,...e},s)=>t.jsx(r,{ref:s,className:o("fixed inset-0 z-[110] bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",a),...e}));c.displayName=r.displayName;const y=l.forwardRef(({className:a,children:e,...s},m)=>t.jsxs(u,{children:[t.jsx(c,{}),t.jsxs(d,{ref:m,className:o("fixed left-[50%] top-[50%] z-[110] grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",a),...s,children:[e,t.jsxs(g,{className:"absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground",children:[t.jsx(x,{className:"h-4 w-4"}),t.jsx("span",{className:"sr-only",children:"关闭"})]})]})]}));y.displayName=d.displayName;const N=({className:a,...e})=>t.jsx("div",{className:o("flex flex-col space-y-1.5 text-center sm:text-left",a),...e});N.displayName="DialogHeader";const j=({className:a,...e})=>t.jsx("div",{className:o("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",a),...e});j.displayName="DialogFooter";const D=l.forwardRef(({className:a,...e},s)=>t.jsx(i,{ref:s,className:o("text-lg font-semibold leading-none tracking-tight",a),...e}));D.displayName=i.displayName;const b=l.forwardRef(({className:a,...e},s)=>t.jsx(n,{ref:s,className:o("text-sm text-muted-foreground",a),...e}));b.displayName=n.displayName;export{R as D,y as a,N as b,D as c,b as d,j as e};
import{r as l,j as t}from"./react-vendor-1fwFMt02.js";import{ab as f,ac as p,ad as d,ae as g,X as x,af as i,ag as n,ah as r}from"./ui-vendor-Gb6mai95.js";import{f as o}from"./index-BCHo3nLu.js";const R=f,u=p,c=l.forwardRef(({className:a,...e},s)=>t.jsx(r,{ref:s,className:o("fixed inset-0 z-[110] bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",a),...e}));c.displayName=r.displayName;const y=l.forwardRef(({className:a,children:e,...s},m)=>t.jsxs(u,{children:[t.jsx(c,{}),t.jsxs(d,{ref:m,className:o("fixed left-[50%] top-[50%] z-[110] grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",a),...s,children:[e,t.jsxs(g,{className:"absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground",children:[t.jsx(x,{className:"h-4 w-4"}),t.jsx("span",{className:"sr-only",children:"关闭"})]})]})]}));y.displayName=d.displayName;const N=({className:a,...e})=>t.jsx("div",{className:o("flex flex-col space-y-1.5 text-center sm:text-left",a),...e});N.displayName="DialogHeader";const j=({className:a,...e})=>t.jsx("div",{className:o("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",a),...e});j.displayName="DialogFooter";const D=l.forwardRef(({className:a,...e},s)=>t.jsx(i,{ref:s,className:o("text-lg font-semibold leading-none tracking-tight",a),...e}));D.displayName=i.displayName;const b=l.forwardRef(({className:a,...e},s)=>t.jsx(n,{ref:s,className:o("text-sm text-muted-foreground",a),...e}));b.displayName=n.displayName;export{R as D,y as a,N as b,D as c,b as d,j as e};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
import{r as t,j as i}from"./react-vendor-1fwFMt02.js";import{f as n}from"./index-kfMSnNdB.js";const a=t.forwardRef(({className:e,type:o,...r},s)=>i.jsx("input",{type:o,className:n("flex h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",e),ref:s,...r}));a.displayName="Input";export{a as I};
import{r as t,j as i}from"./react-vendor-1fwFMt02.js";import{f as n}from"./index-BCHo3nLu.js";const a=t.forwardRef(({className:e,type:o,...r},s)=>i.jsx("input",{type:o,className:n("flex h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",e),ref:s,...r}));a.displayName="Input";export{a as I};

View File

@@ -1 +1 @@
import{r as s,j as a}from"./react-vendor-1fwFMt02.js";import{f as t}from"./index-kfMSnNdB.js";const i=s.forwardRef(({className:e,...r},o)=>a.jsx("textarea",{className:t("flex min-h-[60px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",e),ref:o,...r}));i.displayName="Textarea";export{i as T};
import{r as s,j as a}from"./react-vendor-1fwFMt02.js";import{f as t}from"./index-BCHo3nLu.js";const i=s.forwardRef(({className:e,...r},o)=>a.jsx("textarea",{className:t("flex min-h-[60px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",e),ref:o,...r}));i.displayName="Textarea";export{i as T};

View File

@@ -27,10 +27,10 @@
document.documentElement.style.colorScheme = theme;
})();
</script>
<script type="module" crossorigin src="/assets/index-kfMSnNdB.js"></script>
<script type="module" crossorigin src="/assets/index-BCHo3nLu.js"></script>
<link rel="modulepreload" crossorigin href="/assets/react-vendor-1fwFMt02.js">
<link rel="modulepreload" crossorigin href="/assets/ui-vendor-Gb6mai95.js">
<link rel="stylesheet" crossorigin href="/assets/index-BsZMEGDY.css">
<link rel="stylesheet" crossorigin href="/assets/index-BH_9j3kA.css">
</head>
<body>
<div id="root"></div>

View File

@@ -105,6 +105,8 @@ export interface ForumLimits {
search_keyword_max: number;
page_size_default: number;
page_size_max: number;
feed_max_pages: number;
feed_max_items: number;
password_min_len: number;
avatar_max_mb: number;
}
@@ -116,6 +118,9 @@ export interface ForumLimitsPublic {
comment_max: number;
search_keyword_min: number;
search_keyword_max: number;
page_size_default: number;
feed_max_pages: number;
feed_max_items: number;
password_min_len: number;
avatar_max_mb: number;
}

View File

@@ -1,6 +1,7 @@
import { useRef, useEffect, useLayoutEffect } from 'react';
import { useVirtualizer } from '@tanstack/react-virtual';
import { Spinner } from '@/components/ui/spinner';
import { Button } from '@/components/ui/button';
import PostListItem from './PostListItem';
import type { PostItem } from '../api/types';
import type { FeedSort } from './FeedSortBar';
@@ -10,6 +11,9 @@ interface Props {
sort?: FeedSort;
loading: boolean;
hasMore: boolean;
/** 是否允许滚动触底自动加载(达到上限后为 false */
canAutoLoad: boolean;
postTotal: number;
onLoadMore: () => void;
onSelect: (id: number) => void;
/** 返回列表时恢复的滚动位置 */
@@ -25,6 +29,8 @@ export default function VirtualPostList({
sort = 'latest',
loading,
hasMore,
canAutoLoad,
postTotal,
onLoadMore,
onSelect,
restoreScrollTop,
@@ -42,6 +48,9 @@ export default function VirtualPostList({
overscan: 8,
});
const showHistoryPrompt = hasMore && !canAutoLoad && !loading;
const showEnd = !hasMore && posts.length > 0 && !loading;
useLayoutEffect(() => {
if (resetScrollKey <= 0) return;
const el = parentRef.current;
@@ -69,13 +78,13 @@ export default function VirtualPostList({
if (!el) return;
const onScroll = () => {
onScrollTopChange?.(el.scrollTop);
if (el.scrollTop + el.clientHeight >= el.scrollHeight - 120 && hasMore && !loading) {
if (el.scrollTop + el.clientHeight >= el.scrollHeight - 120 && canAutoLoad && hasMore && !loading) {
onLoadMore();
}
};
el.addEventListener('scroll', onScroll);
return () => el.removeEventListener('scroll', onScroll);
}, [hasMore, loading, onLoadMore, onScrollTopChange]);
}, [canAutoLoad, hasMore, loading, onLoadMore, onScrollTopChange]);
return (
<div className="post-list-scroll" ref={parentRef}>
@@ -99,12 +108,22 @@ export default function VirtualPostList({
})}
</div>
{loading && (
<div className="flex justify-center py-4">
<div className="feed-list-footer">
<Spinner />
</div>
)}
{!loading && !hasMore && posts.length > 0 && (
<div style={{ textAlign: 'center', padding: 6, fontSize: 12, color: 'var(--color-text-3)' }}> </div>
{showHistoryPrompt && (
<div className="feed-list-footer feed-list-footer--history">
<p className="feed-list-footer__hint">
{posts.length} / {postTotal}
</p>
<Button type="button" variant="outline" size="sm" onClick={onLoadMore}>
</Button>
</div>
)}
{showEnd && (
<div className="feed-list-footer feed-list-footer--end"> </div>
)}
</div>
);

View File

@@ -9,6 +9,9 @@ const DEFAULT_LIMITS: ForumLimitsPublic = {
comment_max: 5000,
search_keyword_min: 1,
search_keyword_max: 50,
page_size_default: 30,
feed_max_pages: 10,
feed_max_items: 300,
password_min_len: 6,
avatar_max_mb: 2,
};

View File

@@ -1,4 +1,4 @@
import { useState, useEffect, useCallback, useRef } from 'react';
import { useState, useEffect, useCallback, useRef, useMemo } from 'react';
import { useNavigate, useOutletContext, useSearchParams, useLocation } from 'react-router-dom';
import { notify } from '@/lib/notify';
import { api } from '../api/client';
@@ -7,6 +7,7 @@ import type { LayoutCtx } from '../layouts/MainLayout';
import VirtualPostList from '../components/VirtualPostList';
import FeedHeader from '../components/FeedHeader';
import FeedSortBar, { parseFeedSort, buildHomeUrl, type FeedSort } from '../components/FeedSortBar';
import { useForumLimits } from '../hooks/useForumLimits';
import {
getFeedCache,
setFeedCache,
@@ -21,6 +22,11 @@ export default function HomePage() {
const location = useLocation();
const [params] = useSearchParams();
const ctx = useOutletContext<LayoutCtx>();
const { limits } = useForumLimits();
const pageSize = limits.page_size_default;
const feedMaxPages = limits.feed_max_pages;
const feedMaxItems = limits.feed_max_items;
const boardId = Number(params.get('board')) || ctx?.boardId || 0;
const keyword = params.get('keyword') || '';
const sort = parseFeedSort(params.get('sort'));
@@ -38,6 +44,11 @@ export default function HomePage() {
/** 主动刷新时不把旧列表/滚动位置写回 cache */
const skipCacheSaveRef = useRef(false);
const canAutoLoad = useMemo(
() => hasMore && page < feedMaxPages && posts.length < feedMaxItems,
[hasMore, page, feedMaxPages, posts.length, feedMaxItems],
);
const resetFeedView = useCallback(() => {
setRestoreScrollTop(null);
scrollTopRef.current = 0;
@@ -56,7 +67,7 @@ export default function HomePage() {
try {
const data = await api.posts({
page: p,
size: 30,
size: pageSize,
board_id: boardId || '',
keyword,
sort: sort === 'latest' ? '' : sort,
@@ -72,36 +83,32 @@ export default function HomePage() {
} finally {
setLoading(false);
}
}, [boardId, keyword, sort]);
}, [boardId, keyword, sort, pageSize]);
/** 有缓存时静默拉取已加载页,更新置顶等状态同时保留滚动位置 */
const revalidate = useCallback(async (maxPage: number) => {
/** 有缓存时静默刷新第 1 页,合并置顶等变化同时保留已加载的历史 */
const revalidate = useCallback(async () => {
try {
const all: PostItem[] = [];
let total = 0;
let hasMore = true;
for (let p = 1; p <= maxPage; p++) {
const data = await api.posts({
page: p,
size: 30,
board_id: boardId || '',
keyword,
sort: sort === 'latest' ? '' : sort,
});
const batch = Array.isArray(data.posts) ? data.posts : [];
all.push(...batch);
total = data.total ?? 0;
hasMore = !!data.has_more;
if (!data.has_more) break;
}
setPosts(all);
setPostTotal(total);
setHasMore(hasMore);
setPage(maxPage);
const data = await api.posts({
page: 1,
size: pageSize,
board_id: boardId || '',
keyword,
sort: sort === 'latest' ? '' : sort,
});
const fresh = Array.isArray(data.posts) ? data.posts : [];
const freshIds = new Set(fresh.map(p => p.id));
setPosts(prev => [...fresh, ...prev.filter(p => !freshIds.has(p.id))]);
setPostTotal(data.total ?? 0);
setHasMore(!!data.has_more);
} catch {
// 静默失败,保留缓存数据
}
}, [boardId, keyword, sort]);
}, [boardId, keyword, sort, pageSize]);
const loadNextPage = useCallback(() => {
if (loading || !hasMore) return;
load(page + 1);
}, [loading, hasMore, page, load]);
useEffect(() => {
const forceRefresh = (location.state as FeedNavState | null)?.refreshFeed;
@@ -119,7 +126,7 @@ export default function HomePage() {
setRestoreScrollTop(cached.scrollTop);
scrollTopRef.current = cached.scrollTop;
setLoading(false);
revalidate(cached.page);
revalidate();
return;
}
setRestoreScrollTop(null);
@@ -193,7 +200,9 @@ export default function HomePage() {
sort={sort}
loading={loading}
hasMore={hasMore}
onLoadMore={() => !loading && hasMore && load(page + 1)}
canAutoLoad={canAutoLoad}
postTotal={postTotal}
onLoadMore={loadNextPage}
onSelect={(id) => nav(`/post/${id}`)}
restoreScrollTop={restoreScrollTop}
resetScrollKey={listResetKey}

View File

@@ -68,6 +68,8 @@ const SETTING_SECTIONS: SettingSection[] = [
{ key: 'search_keyword_max', label: '关键词最长', unit: '字', min: 1 },
{ key: 'page_size_default', label: '默认每页', unit: '条', min: 1 },
{ key: 'page_size_max', label: '最大每页', unit: '条', min: 1 },
{ key: 'feed_max_pages', label: '首页自动加载页数', unit: '页', hint: '超出后需手动加载', min: 1 },
{ key: 'feed_max_items', label: '首页自动加载条数', unit: '条', hint: '与页数取先到上限', min: 1 },
],
},
{

View File

@@ -553,6 +553,31 @@ a:hover { text-decoration: underline; }
.post-list-scroll::-webkit-scrollbar {
display: none;
}
.feed-list-footer {
display: flex;
justify-content: center;
align-items: center;
padding: 16px 12px;
}
.feed-list-footer--end {
font-size: 12px;
color: var(--color-text-3);
padding: 10px 12px;
}
.feed-list-footer--history {
flex-direction: column;
gap: 10px;
}
.feed-list-footer__hint {
margin: 0;
font-size: 12px;
color: var(--color-text-3);
}
.page-inner { padding: 20px 24px; max-width: 720px; }
.page-inner-wide { padding: 20px 24px; }
.page-title { font-size: 20px; font-weight: 600; margin: 0 0 4px; }

View File

@@ -29,6 +29,9 @@ const (
SettingPageSizeDefault = "page_size_default"
SettingPageSizeMax = "page_size_max"
SettingFeedMaxPages = "feed_max_pages"
SettingFeedMaxItems = "feed_max_items"
SettingPasswordMinLen = "password_min_len"
SettingAvatarMaxMB = "avatar_max_mb"
)
@@ -55,6 +58,9 @@ type ForumLimits struct {
PageSizeDefault int `json:"page_size_default"`
PageSizeMax int `json:"page_size_max"`
FeedMaxPages int `json:"feed_max_pages"`
FeedMaxItems int `json:"feed_max_items"`
PasswordMinLen int `json:"password_min_len"`
AvatarMaxMB int `json:"avatar_max_mb"`
}
@@ -67,6 +73,9 @@ type ForumLimitsPublic struct {
CommentMax int `json:"comment_max"`
SearchKeywordMin int `json:"search_keyword_min"`
SearchKeywordMax int `json:"search_keyword_max"`
PageSizeDefault int `json:"page_size_default"`
FeedMaxPages int `json:"feed_max_pages"`
FeedMaxItems int `json:"feed_max_items"`
PasswordMinLen int `json:"password_min_len"`
AvatarMaxMB int `json:"avatar_max_mb"`
}
@@ -99,6 +108,9 @@ var forumSettingDefs = []settingDef{
{SettingPageSizeDefault, "30", 1, 200},
{SettingPageSizeMax, "50", 1, 200},
{SettingFeedMaxPages, "10", 1, 100},
{SettingFeedMaxItems, "300", 1, 5000},
{SettingPasswordMinLen, "6", 4, 128},
{SettingAvatarMaxMB, "2", 1, 20},
}
@@ -176,6 +188,9 @@ func (s *ForumSettingsService) Limits() ForumLimits {
PageSizeDefault: s.PageSizeDefault(),
PageSizeMax: s.PageSizeMax(),
FeedMaxPages: s.FeedMaxPages(),
FeedMaxItems: s.FeedMaxItems(),
PasswordMinLen: s.PasswordMinLen(),
AvatarMaxMB: s.AvatarMaxMB(),
}
@@ -190,6 +205,9 @@ func (s *ForumSettingsService) PublicLimits() ForumLimitsPublic {
CommentMax: limits.CommentMax,
SearchKeywordMin: limits.SearchKeywordMin,
SearchKeywordMax: limits.SearchKeywordMax,
PageSizeDefault: limits.PageSizeDefault,
FeedMaxPages: limits.FeedMaxPages,
FeedMaxItems: limits.FeedMaxItems,
PasswordMinLen: limits.PasswordMinLen,
AvatarMaxMB: limits.AvatarMaxMB,
}
@@ -211,6 +229,8 @@ func (s *ForumSettingsService) UpdateLimits(in ForumLimits) error {
SettingSearchKeywordMax: in.SearchKeywordMax,
SettingPageSizeDefault: in.PageSizeDefault,
SettingPageSizeMax: in.PageSizeMax,
SettingFeedMaxPages: in.FeedMaxPages,
SettingFeedMaxItems: in.FeedMaxItems,
SettingPasswordMinLen: in.PasswordMinLen,
SettingAvatarMaxMB: in.AvatarMaxMB,
}
@@ -262,6 +282,9 @@ func (s *ForumSettingsService) SearchKeywordMax() int { return s.getInt(SettingS
func (s *ForumSettingsService) PageSizeDefault() int { return s.getInt(SettingPageSizeDefault, 30) }
func (s *ForumSettingsService) PageSizeMax() int { return s.getInt(SettingPageSizeMax, 50) }
func (s *ForumSettingsService) FeedMaxPages() int { return s.getInt(SettingFeedMaxPages, 10) }
func (s *ForumSettingsService) FeedMaxItems() int { return s.getInt(SettingFeedMaxItems, 300) }
func (s *ForumSettingsService) PasswordMinLen() int { return s.getInt(SettingPasswordMinLen, 6) }
func (s *ForumSettingsService) AvatarMaxMB() int { return s.getInt(SettingAvatarMaxMB, 2) }