diff --git a/docs/rebuild-spec/02-features.md b/docs/rebuild-spec/02-features.md index d0bc546..a1e71d1 100644 --- a/docs/rebuild-spec/02-features.md +++ b/docs/rebuild-spec/02-features.md @@ -20,7 +20,7 @@ - [ ] 列表样式可配:`title` | `excerpt` | `thumbnail` - [x] 搜索:关键词、标签、作者、仅标题(`title_only`)— SSR Feed 面板(`/` / `/board/:id`) - [x] 右栏:热门帖、标签云、最新评论、最新用户、友链(可开关排序)— SSR 热门固定 + `aside_widgets`;Admin 仅友链侧栏开关 -- [ ] 登录用户右栏/侧边:签到与抽奖入口(入口暂在 `/profile` 钱包区) +- [x] 登录用户右栏/侧边:签到与抽奖入口 — SSR 右栏条(钱包详情仍在 `/profile#wallet`) - [ ] 下拉刷新(移动端) - [ ] 可选伪静态:`/post/123.html` 等形式(后缀后台可配) - [x] 404 页 diff --git a/docs/rebuild-spec/06-pages-ux.md b/docs/rebuild-spec/06-pages-ux.md index 2d1d72f..fa2a17a 100644 --- a/docs/rebuild-spec/06-pages-ux.md +++ b/docs/rebuild-spec/06-pages-ux.md @@ -86,7 +86,7 @@ ### 2.3 右栏 RightPanel -- 登录用户:`AsideCheckInStrip`(签到 + 抽奖 + 积分入口)— **未迁**(入口仍在 `/profile`) +- 登录用户:`AsideCheckInStrip`(签到 + 抽奖 + 积分入口)— **已迁** SSR 右栏;流水/详情仍在 `/profile#wallet` - 热门帖 — **已迁** SSR(固定块) - 可配置 widgets:`tag_cloud` / `recent_comments` / `recent_users` / `friend_links`(顺序与开关来自 `aside_widgets`)— **已迁** SSR;Admin 完整排序编辑器未迁,友链侧栏开关在 `/admin/friend-links` - 桌面三列;≤900px 隐藏右栏 diff --git a/docs/rebuild-spec/08-gitea-ssr-architecture.md b/docs/rebuild-spec/08-gitea-ssr-architecture.md index 77c22e3..678136e 100644 --- a/docs/rebuild-spec/08-gitea-ssr-architecture.md +++ b/docs/rebuild-spec/08-gitea-ssr-architecture.md @@ -92,7 +92,7 @@ web_src/ → public/assets/ Feed 搜索:`/` / `/board/:id` 面板(`keyword` / `tag` / `author` / `title_only`);排序与分页 URL 保参;顶栏链到 `/#search`。 -右栏:首页与帖详情三栏;固定热门帖 + `aside_widgets`(标签云 / 最新评论 / 最新用户 / 友链);Admin 友链页可开关侧栏友链。 +右栏:首页与帖详情三栏;登录用户签到/抽奖条(PRG 可回跳当前 Feed/帖页);固定热门帖 + `aside_widgets`(标签云 / 最新评论 / 最新用户 / 友链);Admin 友链页可开关侧栏友链。 友链:`/links`(列表、登录申请/取消、Logo 上传);Admin `/admin/friend-links`(品牌增删、审核、nav/footer/aside 入口开关)。 diff --git a/public/assets/site.css b/public/assets/site.css index 9c2c423..bcf4f0e 100644 --- a/public/assets/site.css +++ b/public/assets/site.css @@ -167,6 +167,38 @@ body.j13-body { } .j13-widget__more a { color: var(--j13-accent); } +.j13-checkin { + padding: 0.75rem 0.8rem; + border: 1px solid var(--j13-border); + border-radius: var(--j13-radius); + background: var(--j13-surface); +} +.j13-checkin__points { + margin: 0 0 0.5rem; + font-size: 0.95rem; + font-weight: 600; +} +.j13-checkin__points a { + color: inherit; + text-decoration: none; +} +.j13-checkin__points a:hover { color: var(--j13-accent); } +.j13-checkin__actions { + display: flex; + flex-wrap: wrap; + gap: 0.4rem; + align-items: center; +} +.j13-checkin__done { + font-size: 0.8rem; + color: var(--j13-muted); +} +.j13-checkin__more { + margin: 0.5rem 0 0; + font-size: 0.8rem; +} +.j13-checkin__more a { color: var(--j13-accent); } + .j13-main { background: var(--j13-surface); border: 1px solid var(--j13-border); @@ -544,6 +576,10 @@ body.j13-body { font-size: 0.9rem; cursor: pointer; } +.j13-btn--sm { + padding: 0.25rem 0.55rem; + font-size: 0.8rem; +} .j13-btn:disabled { opacity: 0.55; cursor: not-allowed; diff --git a/routers/web/chrome.go b/routers/web/chrome.go index 37de8ee..cf8a0fb 100644 --- a/routers/web/chrome.go +++ b/routers/web/chrome.go @@ -76,10 +76,17 @@ type RightAsideWidget struct { FriendLinks []RightAsideFriendLink } -// RightAsideData 右栏:固定热门 + 可配置 widgets +// RightAsideData 右栏:签到条 + 固定热门 + 可配置 widgets type RightAsideData struct { - HotPosts []RightAsideHotPost - Widgets []RightAsideWidget + ShowCheckIn bool + CheckedIn bool + CheckInStreak int + CheckInPoints int // 已签实得或预计可得 + LotteryDrawn bool + LotteryPoints int + LotteryCost int + HotPosts []RightAsideHotPost + Widgets []RightAsideWidget } // PageChrome 布局公共字段 @@ -160,7 +167,7 @@ func (d Deps) chrome(ctx *webctx.Context, title, desc, inner string) PageChrome ViewerPoints: viewerPoints, ShowFriendLinksNav: d.Settings.NavShowFriendLinks(), ShowFriendLinksFooter: d.Settings.FooterShowFriendLinks(), - RightAside: d.loadRightAside(brand), + RightAside: d.loadRightAside(ctx, brand), } } @@ -171,11 +178,24 @@ const ( rightAsideUserLimit = 8 ) -func (d Deps) loadRightAside(brand services.SiteBranding) RightAsideData { +func (d Deps) loadRightAside(ctx *webctx.Context, brand services.SiteBranding) RightAsideData { out := RightAsideData{ HotPosts: []RightAsideHotPost{}, Widgets: []RightAsideWidget{}, } + if ctx.IsSigned() && d.Points != nil { + out.ShowCheckIn = true + if st, err := d.Points.GetCheckInStatus(ctx.UserID()); err == nil { + out.CheckedIn = st.CheckedIn + out.CheckInStreak = st.Streak + out.CheckInPoints = st.TodayPoints + } + if st, err := d.Points.GetLotteryStatus(ctx.UserID()); err == nil { + out.LotteryDrawn = st.Drawn + out.LotteryPoints = st.Points + out.LotteryCost = st.Cost + } + } if d.Post != nil { if items, err := d.Post.HotPosts(rightAsideHotLimit); err == nil { for _, it := range items { diff --git a/routers/web/profile.go b/routers/web/profile.go index 53bf195..c575915 100644 --- a/routers/web/profile.go +++ b/routers/web/profile.go @@ -226,11 +226,22 @@ func (d Deps) ProfileAvatarPost(c *gin.Context) { // ProfileCheckInPost 每日签到(PRG) func (d Deps) ProfileCheckInPost(c *gin.Context) { ctx := d.ctx(c) + redir := safePointsRedirect(c.PostForm("redirect")) if !ctx.CheckCSRF() { + if redir != "/profile" { + ctx.SetFlash("无效请求,请重试") + ctx.Redirect(redir) + return + } d.renderProfile(ctx, "无效请求,请重试") return } if d.Points == nil { + if redir != "/profile" { + ctx.SetFlash("积分服务未就绪") + ctx.Redirect(redir) + return + } d.renderProfile(ctx, "积分服务未就绪") return } @@ -238,24 +249,40 @@ func (d Deps) ProfileCheckInPost(c *gin.Context) { if err != nil { if errors.Is(err, services.ErrAlreadyCheckedIn) { ctx.SetFlash("今日已签到") - ctx.Redirect("/profile") + ctx.Redirect(redir) + return + } + if redir != "/profile" { + ctx.SetFlash(err.Error()) + ctx.Redirect(redir) return } d.renderProfile(ctx, err.Error()) return } ctx.SetFlash(fmt.Sprintf("签到成功:连续 %d 天,获得 %d 积分", st.Streak, st.TodayPoints)) - ctx.Redirect("/profile") + ctx.Redirect(redir) } // ProfileLotteryPost 每日抽奖(PRG) func (d Deps) ProfileLotteryPost(c *gin.Context) { ctx := d.ctx(c) + redir := safePointsRedirect(c.PostForm("redirect")) if !ctx.CheckCSRF() { + if redir != "/profile" { + ctx.SetFlash("无效请求,请重试") + ctx.Redirect(redir) + return + } d.renderProfile(ctx, "无效请求,请重试") return } if d.Points == nil { + if redir != "/profile" { + ctx.SetFlash("积分服务未就绪") + ctx.Redirect(redir) + return + } d.renderProfile(ctx, "积分服务未就绪") return } @@ -263,7 +290,12 @@ func (d Deps) ProfileLotteryPost(c *gin.Context) { if err != nil { if errors.Is(err, services.ErrAlreadyLottery) { ctx.SetFlash("今日已抽奖") - ctx.Redirect("/profile") + ctx.Redirect(redir) + return + } + if redir != "/profile" { + ctx.SetFlash(err.Error()) + ctx.Redirect(redir) return } d.renderProfile(ctx, err.Error()) @@ -274,5 +306,23 @@ func (d Deps) ProfileLotteryPost(c *gin.Context) { } else { ctx.SetFlash("抽奖结果:未中奖,明天再来") } - ctx.Redirect("/profile") + ctx.Redirect(redir) +} + +// safePointsRedirect 签到/抽奖 PRG 回跳:仅允许 /、/board/…、/post/…;空或非法则 /profile +func safePointsRedirect(raw string) string { + raw = strings.TrimSpace(raw) + if raw == "" || strings.Contains(raw, "://") || strings.HasPrefix(raw, "//") || strings.Contains(raw, "..") { + return "/profile" + } + if raw == "/" { + return raw + } + if strings.HasPrefix(raw, "/board/") || strings.HasPrefix(raw, "/post/") { + if strings.ContainsAny(raw, "?#") { + return "/profile" + } + return raw + } + return "/profile" } diff --git a/templates/shared/right_aside.tmpl b/templates/shared/right_aside.tmpl index dee4ddf..8a4b83d 100644 --- a/templates/shared/right_aside.tmpl +++ b/templates/shared/right_aside.tmpl @@ -1,5 +1,33 @@ {{define "shared/right_aside"}}