Update Search footer header index user
This commit is contained in:
@@ -88,7 +88,7 @@ public class WebSecurityConfig {
|
||||
auth.requestMatchers(
|
||||
"/css/**", "/js/**", "/images/**", "/static/**", "/favicon.ico",
|
||||
"/login/**", "/register", "/registerDo", "/initUserData",
|
||||
"/", "/main", "/index", "/getdiscussionsbyid",
|
||||
"/", "/main", "/index", "/getdiscussionsbyid", "/search", "/profile",
|
||||
// SpringDoc OpenAPI UI and spec endpoint (developer tools, no sensitive data)
|
||||
"/swagger-ui/**", "/swagger-ui.html", "/v3/api-docs/**"
|
||||
).permitAll();
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.yaoyuan.jiscuss.controller;
|
||||
|
||||
import com.yaoyuan.jiscuss.entity.UserInfo;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.context.SecurityContext;
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
|
||||
/**
|
||||
* Injects common model attributes (e.g. isAdmin) into every rendered view.
|
||||
*/
|
||||
@ControllerAdvice
|
||||
public class GlobalModelAdvice {
|
||||
|
||||
@ModelAttribute("isAdmin")
|
||||
public boolean isAdmin(HttpServletRequest request) {
|
||||
HttpSession session = request.getSession(false);
|
||||
if (session == null) return false;
|
||||
Object ctx = session.getAttribute("SPRING_SECURITY_CONTEXT");
|
||||
if (!(ctx instanceof SecurityContext sc)) return false;
|
||||
Authentication auth = sc.getAuthentication();
|
||||
if (auth == null || !(auth.getPrincipal() instanceof UserInfo)) return false;
|
||||
return auth.getAuthorities().stream()
|
||||
.map(GrantedAuthority::getAuthority)
|
||||
.anyMatch(a -> a.equals("ROLE_ADMIN"));
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.ModelMap;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
@@ -100,6 +101,54 @@ public class UserPageController extends BaseController {
|
||||
map.put("likedDiscussions", liked);
|
||||
}
|
||||
|
||||
map.put("isOwn", true);
|
||||
return "user";
|
||||
}
|
||||
|
||||
/**
|
||||
* 公开用户主页 /profile?uid=xxx (无需登录)
|
||||
*/
|
||||
@GetMapping("/profile")
|
||||
public String profile(@RequestParam Integer uid,
|
||||
@RequestParam(defaultValue = "discussion") String type,
|
||||
HttpServletRequest request, ModelMap map) {
|
||||
User userEntity = usersService.findOne(uid);
|
||||
if (userEntity == null) return "redirect:/";
|
||||
|
||||
long liveDiscCount = discussionsRepository.countByStartUserId(uid);
|
||||
long liveReplyCount = postsRepository.countByCreateId(uid);
|
||||
map.put("username", userEntity.getUsername());
|
||||
map.put("userEntity", userEntity);
|
||||
map.put("liveDiscCount", liveDiscCount);
|
||||
map.put("liveReplyCount", liveReplyCount);
|
||||
map.put("type", type);
|
||||
map.put("isOwn", false);
|
||||
|
||||
if ("discussion".equals(type)) {
|
||||
Page<Discussion> discussions = discussionsRepository.findByStartUserIdOrderByStartTimeDesc(
|
||||
uid, PageRequest.of(0, 20));
|
||||
map.put("discussions", discussions.getContent());
|
||||
} else if ("reply".equals(type)) {
|
||||
Page<Post> posts = postsRepository.findByCreateIdOrderByCreateTimeDesc(
|
||||
uid, PageRequest.of(0, 20));
|
||||
map.put("posts", posts.getContent());
|
||||
List<String> discTitles = new ArrayList<>();
|
||||
for (Post p : posts.getContent()) {
|
||||
Discussion d = p.getDiscussionId() != null
|
||||
? discussionsRepository.findById(p.getDiscussionId()).orElse(null) : null;
|
||||
discTitles.add(d != null ? d.getTitle() : "");
|
||||
}
|
||||
map.put("discTitles", discTitles);
|
||||
} else if ("like".equals(type)) {
|
||||
List<Integer> likedIds = likeCollectRepository.findLikedDiscussionIdsByUser(uid);
|
||||
List<Discussion> liked = new ArrayList<>();
|
||||
for (Integer id : likedIds) {
|
||||
Discussion d = discussionsRepository.findById(id).orElse(null);
|
||||
if (d != null) liked.add(d);
|
||||
}
|
||||
map.put("likedDiscussions", liked);
|
||||
}
|
||||
|
||||
return "user";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ import com.yaoyuan.jiscuss.entity.Tag;
|
||||
import com.yaoyuan.jiscuss.entity.User;
|
||||
import com.yaoyuan.jiscuss.entity.UserInfo;
|
||||
import com.yaoyuan.jiscuss.entity.custom.DiscussionCustom;
|
||||
import com.yaoyuan.jiscuss.repository.DiscussionsRepository;
|
||||
import com.yaoyuan.jiscuss.repository.PostsRepository;
|
||||
import com.yaoyuan.jiscuss.service.IDiscussionsService;
|
||||
import com.yaoyuan.jiscuss.service.IDiscussionsTagsService;
|
||||
import com.yaoyuan.jiscuss.service.IPostsService;
|
||||
@@ -19,6 +21,7 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.ModelMap;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
@@ -59,6 +62,12 @@ public class UserPostController extends BaseController {
|
||||
@Autowired
|
||||
private IUsersService usersService;
|
||||
|
||||
@Autowired
|
||||
private DiscussionsRepository discussionsRepository;
|
||||
|
||||
@Autowired
|
||||
private PostsRepository postsRepository;
|
||||
|
||||
@Autowired
|
||||
private IpRegionService ipRegionService;
|
||||
|
||||
@@ -112,6 +121,19 @@ public class UserPostController extends BaseController {
|
||||
map.put("discussions", newdd);
|
||||
map.put("posts", postsObj);
|
||||
map.put("sort", sort);
|
||||
|
||||
// Sidebar: author stats and top discussions
|
||||
if (newdd.getStartUserId() != null) {
|
||||
Integer authorId = newdd.getStartUserId();
|
||||
long authorDiscCount = discussionsRepository.countByStartUserId(authorId);
|
||||
long authorReplyCount = postsRepository.countByCreateId(authorId);
|
||||
java.util.List<Discussion> authorTopDiscs = discussionsRepository
|
||||
.findTopByStartUserIdOrderByLikes(authorId, PageRequest.of(0, 5));
|
||||
map.put("authorDiscCount", authorDiscCount);
|
||||
map.put("authorReplyCount", authorReplyCount);
|
||||
map.put("authorTopDiscs", authorTopDiscs);
|
||||
}
|
||||
|
||||
UserInfo user = getUserInfo(request);
|
||||
if (user != null) {
|
||||
map.put("username", user.getUsername());
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.yaoyuan.jiscuss.entity.User;
|
||||
import com.yaoyuan.jiscuss.entity.UserInfo;
|
||||
import com.yaoyuan.jiscuss.entity.custom.DiscussionCustom;
|
||||
import com.yaoyuan.jiscuss.entity.custom.TagCustom;
|
||||
import com.yaoyuan.jiscuss.repository.DiscussionsRepository;
|
||||
import com.yaoyuan.jiscuss.service.IDiscussionsService;
|
||||
import com.yaoyuan.jiscuss.service.ITagsService;
|
||||
import com.yaoyuan.jiscuss.service.IUsersService;
|
||||
@@ -54,6 +55,9 @@ public class UserSystemController extends BaseController {
|
||||
@Autowired
|
||||
private BCryptPasswordEncoder passwordEncoder;
|
||||
|
||||
@Autowired
|
||||
private DiscussionsRepository discussionsRepository;
|
||||
|
||||
/**
|
||||
* 首页index
|
||||
* @param tag
|
||||
@@ -276,6 +280,36 @@ public class UserSystemController extends BaseController {
|
||||
return "register";
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索页
|
||||
*/
|
||||
@GetMapping("/search")
|
||||
public String search(@RequestParam(defaultValue = "") String q,
|
||||
@RequestParam(defaultValue = "1") Integer pageNum,
|
||||
HttpServletRequest request, ModelMap map) {
|
||||
String keyword = q.trim();
|
||||
int pageSize = 10;
|
||||
int page = Math.max(pageNum - 1, 0);
|
||||
Page<Discussion> resultPage = keyword.isEmpty()
|
||||
? Page.empty()
|
||||
: discussionsRepository.searchByKeyword(keyword,
|
||||
org.springframework.data.domain.PageRequest.of(page, pageSize));
|
||||
|
||||
List<DiscussionCustom> results = new ArrayList<>();
|
||||
if (!keyword.isEmpty()) {
|
||||
setTagAndUserList(results, resultPage.getContent());
|
||||
}
|
||||
|
||||
map.put("q", keyword);
|
||||
map.put("results", results);
|
||||
map.put("totalPages", resultPage.getTotalPages());
|
||||
map.put("totalElements", resultPage.getTotalElements());
|
||||
map.put("pageNum", pageNum);
|
||||
UserInfo user = getUserInfo(request);
|
||||
if (user != null) map.put("username", user.getUsername());
|
||||
return "search";
|
||||
}
|
||||
|
||||
//获取设置信息
|
||||
|
||||
//获取首页统计
|
||||
|
||||
@@ -10,6 +10,7 @@ import org.springframework.stereotype.Repository;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
@Repository
|
||||
public interface DiscussionsRepository extends JpaRepository<Discussion, Integer> {
|
||||
@@ -49,4 +50,14 @@ public interface DiscussionsRepository extends JpaRepository<Discussion, Integer
|
||||
@org.springframework.data.repository.query.Param("userId") Integer userId,
|
||||
org.springframework.data.domain.Pageable pageable);
|
||||
long countByStartUserId(Integer startUserId);
|
||||
|
||||
/** Author's top discussions ordered by likeCount (for sidebar). */
|
||||
@Query("SELECT d FROM Discussion d WHERE d.startUserId = :userId ORDER BY COALESCE(d.likeCount, 0) DESC, d.startTime DESC")
|
||||
List<Discussion> findTopByStartUserIdOrderByLikes(
|
||||
@org.springframework.data.repository.query.Param("userId") Integer userId,
|
||||
Pageable pageable);
|
||||
|
||||
/** Full-text search by title or content. */
|
||||
@Query("SELECT d FROM Discussion d WHERE d.title LIKE %:q% OR d.content LIKE %:q% ORDER BY d.startTime DESC")
|
||||
Page<Discussion> searchByKeyword(@org.springframework.data.repository.query.Param("q") String q, Pageable pageable);
|
||||
}
|
||||
|
||||
@@ -29,20 +29,22 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="seven wide column">
|
||||
<h4 class="ui header">这里是页脚预留信息</h4>
|
||||
<p>This is about Jiscuus Page
|
||||
.</p>
|
||||
<h4 class="ui header">关于 Jiscuss</h4>
|
||||
<p>一个简单、易用的 Java 开源论坛,基于 Spring Boot + Semantic UI 构建。</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui section divider"></div>
|
||||
<img src="static/assets/images/logo.png" class="ui centered mini image">
|
||||
@ 2019-2020
|
||||
<img src="/static/assets/images/logo.png" class="ui centered mini image">
|
||||
© 2019-2026
|
||||
<div class="ui horizontal small divided link list">
|
||||
<a class="item" href="fixed.php#">站点地图</a>
|
||||
<a class="item" href="fixed.php#">关于这里</a>
|
||||
<a class="item" href="fixed.php#">团队介绍</a>
|
||||
<a class="item" href="fixed.php#">隐私政策</a>
|
||||
</div>
|
||||
<div style="margin-top:8px; color:#aaa; font-size:.82em;">
|
||||
Powered by <strong>Jiscuss V2.0</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>if(typeof NProgress !== 'undefined') NProgress.done();</script>
|
||||
|
||||
@@ -6,22 +6,11 @@
|
||||
</div>
|
||||
<a class=" item borderless" href="/">首页</a>
|
||||
<a class="active borderless item">论坛</a>
|
||||
<div class="ui dropdown item borderless" tabindex="0">
|
||||
更多
|
||||
<i class="dropdown icon"></i>
|
||||
<div class="menu transition hidden" tabindex="-1">
|
||||
<a class=" item " href="/user">用户页</a>
|
||||
<div class="divider"></div>
|
||||
<div class="item">分割链接</div>
|
||||
<div class="divider"></div>
|
||||
<a class="item" href="/admin/home">后台管理</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right menu">
|
||||
<div class="item">
|
||||
<div class="ui transparent icon input">
|
||||
<div class="ui transparent icon input" id="headerSearchBox">
|
||||
<i class="search icon"></i>
|
||||
<input type="text" placeholder="搜索">
|
||||
<input type="text" id="headerSearchInput" placeholder="搜索" onkeydown="if(event.key==='Enter'&&this.value.trim()){window.location='/search?q='+encodeURIComponent(this.value.trim());}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
@@ -44,17 +33,29 @@
|
||||
style="display:none; position:absolute; top:-4px; right:-8px; font-size:10px; padding:2px 4px;"></span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="ui dropdown item borderless" tabindex="0">
|
||||
<i class="user circle icon"></i>${username}
|
||||
<i class="dropdown icon"></i>
|
||||
<div class="menu transition hidden" tabindex="-1">
|
||||
<a class="item" href="/user"><i class="id card outline icon"></i> 个人主页</a>
|
||||
<#if isAdmin?? && isAdmin>
|
||||
<div class="divider"></div>
|
||||
<a class="item" href="/admin/home"><i class="settings icon"></i> 后台管理</a>
|
||||
</#if>
|
||||
<div class="item" id="userlogin">
|
||||
<#if username??>
|
||||
<form class="ui large form" action="/logout" id="logoutForm" method="post" style="display: none;">
|
||||
<div class="divider"></div>
|
||||
<a class="item" onclick="document.getElementById('logoutForm').submit();" style="cursor:pointer;">
|
||||
<i class="sign out alternate icon"></i> 注销
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<form action="/logout" id="logoutForm" method="post" style="display:none;">
|
||||
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
|
||||
</form>
|
||||
${username} <a href="javascript:document.getElementById('logoutForm').submit();">注销</a>
|
||||
<#else>
|
||||
<a href="/login" id="loginmodel">登录 & 注册</a>
|
||||
</#if>
|
||||
<div class="item" id="userlogin">
|
||||
<a href="/login" id="loginmodel">登录 & 注册</a>
|
||||
</div>
|
||||
</#if>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -66,6 +67,8 @@
|
||||
var saved = localStorage.getItem('jiscuss-theme') || 'light';
|
||||
if (saved === 'dark') { document.documentElement.setAttribute('data-theme', 'dark'); }
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Initialize user dropdown
|
||||
$('.ui.dropdown').dropdown();
|
||||
var btn = document.getElementById('themeToggle');
|
||||
var icon = document.getElementById('themeIcon');
|
||||
function applyTheme(t) {
|
||||
|
||||
@@ -187,10 +187,10 @@
|
||||
</div>
|
||||
|
||||
<div class="widescreen large screen computer tablet only four wide column">
|
||||
<div class="ui fluid action input">
|
||||
<input type="text" placeholder="搜索...">
|
||||
<div class="ui button">搜索</div>
|
||||
</div>
|
||||
<form action="/search" method="get" class="ui fluid action input">
|
||||
<input type="text" name="q" placeholder="搜索主题..." id="sidebarSearch">
|
||||
<button type="submit" class="ui button">搜索</button>
|
||||
</form>
|
||||
|
||||
<div class="ui section divider"></div>
|
||||
|
||||
@@ -199,90 +199,48 @@
|
||||
<div class="header">作者</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<h4 class="ui sub header">测试人员1</h4>
|
||||
<div class="ui small feed">
|
||||
<div class="event">
|
||||
<div class="content">
|
||||
<div class="summary">
|
||||
<a>Jiscuss</a> 一直, <a>Jiscuss</a> 一感谢支持!
|
||||
<h4 class="ui sub header">
|
||||
<a class="user-card-trigger" data-user-id="${discussions.startUserId!''}">${discussions.realname!''}</a>
|
||||
</h4>
|
||||
<div class="ui small statistics" style="margin-top:8px;">
|
||||
<div class="statistic">
|
||||
<div class="value">${authorDiscCount!0}</div>
|
||||
<div class="label">发帖</div>
|
||||
</div>
|
||||
<div class="statistic">
|
||||
<div class="value">${authorReplyCount!0}</div>
|
||||
<div class="label">回复</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="event">
|
||||
<div class="content">
|
||||
<div class="summary">
|
||||
<a>2019</a> 测试内容
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="extra content">
|
||||
<button class="ui button">预留按钮</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<#if authorTopDiscs?? && authorTopDiscs?size gt 0>
|
||||
<div class="ui card">
|
||||
<div class="content">
|
||||
<div class="header">作者热门主题</div>
|
||||
<div class="header">作者其他主题</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="content" style="padding:0;">
|
||||
<div class="ui middle aligned divided list">
|
||||
<div class="item">
|
||||
<img class="ui avatar image" src="/static/assets/images/logo.png">
|
||||
<#list authorTopDiscs as d>
|
||||
<#if d.id != discussions.id>
|
||||
<div class="item" style="padding:8px 12px;">
|
||||
<div class="content">
|
||||
<a class="header">its here</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<img class="ui avatar image" src="/static/assets/images/logo.png">
|
||||
<div class="content">
|
||||
<a class="header">他的测试主题2</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<img class="ui avatar image" src="/static/assets/images/logo.png">
|
||||
<div class="content">
|
||||
<a class="header">他的测试主题3</a>
|
||||
<a class="header" href="/getdiscussionsbyid?id=${d.id}" style="font-size:.9em;font-weight:normal;">
|
||||
${(d.title?length > 30)?then(d.title?substring(0,30) + '…', d.title)}
|
||||
</a>
|
||||
<div class="description" style="font-size:.8em;color:#999;">
|
||||
<i class="heart icon"></i>${d.likeCount!0}
|
||||
<i class="comment icon"></i>${d.commentsCount!0}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="extra content">
|
||||
<p>预留信息</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="ui card">
|
||||
<div class="content">
|
||||
<img class="right floated mini ui image"
|
||||
src="static/assets/images/logo.png">
|
||||
<div class="header">Jiscuss Demo</div>
|
||||
<div class="meta">简单、易用、开源</div>
|
||||
<div class="description">一款基于JAVA的开源论坛 框架采用:Spring Boot + H2
|
||||
+ Semantic UI
|
||||
</div>
|
||||
</div>
|
||||
<div class="extra content">
|
||||
<div class="ui two buttons">
|
||||
<div class="ui basic red button">
|
||||
<div class="ui mini statistic">
|
||||
<div class="value">1</div>
|
||||
<div class="label">用户数</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui basic green button">
|
||||
<div class="ui mini statistic">
|
||||
<div class="value">10</div>
|
||||
<div class="label">发帖数</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</#if>
|
||||
</#list>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</#if>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -333,7 +291,10 @@
|
||||
'<div style="font-weight:700;font-size:1em;margin-bottom:4px;">' + (u.username || '') + '</div>' +
|
||||
'<div style="color:#888;font-size:.88em;">发帖: ' + (u.discussionsCount || 0) +
|
||||
' 回复: ' + (u.commentsCount || 0) + '</div>' +
|
||||
'<div style="margin-top:8px;"><a class="open-msg-modal" data-uid="' + u.id + '" data-uname="' + (u.username||'') + '" style="font-size:.85em;color:#2185d0;cursor:pointer;"><i class="envelope icon"></i>发私信</a></div>'
|
||||
'<div style="margin-top:8px;display:flex;gap:12px;">' +
|
||||
'<a href="/profile?uid=' + u.id + '" style="font-size:.85em;color:#2185d0;"><i class="user icon"></i>主页</a>' +
|
||||
'<a class="open-msg-modal" data-uid="' + u.id + '" data-uname="' + (u.username||'') + '" style="font-size:.85em;color:#2185d0;cursor:pointer;"><i class="envelope icon"></i>发私信</a>' +
|
||||
'</div>'
|
||||
).css({ left: x + 14, top: y + 14 }).show();
|
||||
}
|
||||
|
||||
|
||||
@@ -333,7 +333,8 @@
|
||||
</div>
|
||||
<div class="meta">
|
||||
<div class=" description "><i class="edit icon"></i>
|
||||
<a data-tooltip="${discussions.username}"
|
||||
<a class="user-card-trigger" data-user-id="${discussions.startUserId!''}"
|
||||
data-tooltip="${discussions.username}"
|
||||
data-position="top center"><b>${discussions.username}</b></a> •
|
||||
<i class="reply icon"></i>最后由 <a><b>${discussions.usernameLast}</b></a>
|
||||
回复于${discussions.lastTime}.
|
||||
@@ -387,7 +388,7 @@
|
||||
</div>
|
||||
<div class="meta">
|
||||
<div class=" description "><i class="edit icon"></i>
|
||||
<a><b>${discussions.username}</b></a> •
|
||||
<a class="user-card-trigger" data-user-id="${discussions.startUserId!''}"><b>${discussions.username}</b></a> •
|
||||
<i class="reply icon"></i>最后由 <a><b>${discussions.usernameLast}</b></a>
|
||||
回复于${discussions.lastTime}.
|
||||
</div>
|
||||
@@ -439,7 +440,7 @@
|
||||
</div>
|
||||
<div class="meta">
|
||||
<div class=" description "><i class="edit icon"></i>
|
||||
<a><b>${discussions.username}</b></a> •
|
||||
<a class="user-card-trigger" data-user-id="${discussions.startUserId!''}"><b>${discussions.username}</b></a> •
|
||||
<i class="reply icon"></i>最后由 <a><b>${discussions.usernameLast}</b></a>
|
||||
回复于${discussions.lastTime}.
|
||||
</div>
|
||||
@@ -505,7 +506,7 @@
|
||||
<div class="date">${discussions.startTime}</div>
|
||||
</div>
|
||||
<div class="meta">
|
||||
<a><b>${discussions.username}</b></a> •
|
||||
<a class="user-card-trigger" data-user-id="${discussions.startUserId!''}"><b>${discussions.username}</b></a> •
|
||||
<i class="comment icon"></i>${discussions.commentsCount!0} 回复
|
||||
<i class="eye icon"></i>${discussions.viewCount!0} 浏览
|
||||
</div>
|
||||
@@ -556,6 +557,87 @@
|
||||
</script>
|
||||
|
||||
<script type="text/javascript" charset="UTF-8" src="/static/js/user/system.js"></script>
|
||||
|
||||
<#-- 用户悬浮卡片 -->
|
||||
<script>
|
||||
(function() {
|
||||
var cardCache = {};
|
||||
var $card = $('<div id="jUserCard" style="position:fixed;background:#fff;border:1px solid #ddd;border-radius:6px;padding:12px 16px;box-shadow:0 4px 16px rgba(0,0,0,.15);z-index:9999;min-width:180px;display:none;"></div>').appendTo('body');
|
||||
function showCard(u, x, y) {
|
||||
$card.html(
|
||||
'<div style="font-weight:700;font-size:1em;margin-bottom:4px;">' + (u.username || '') + '</div>' +
|
||||
'<div style="color:#888;font-size:.88em;">发帖: ' + (u.discussionsCount || 0) +
|
||||
' 回复: ' + (u.commentsCount || 0) + '</div>' +
|
||||
'<div style="margin-top:8px;display:flex;gap:12px;">' +
|
||||
'<a href="/profile?uid=' + u.id + '" style="font-size:.85em;color:#2185d0;"><i class="user icon"></i>主页</a>' +
|
||||
'<a class="open-msg-modal" data-uid="' + u.id + '" data-uname="' + (u.username||'') + '" style="font-size:.85em;color:#2185d0;cursor:pointer;"><i class="envelope icon"></i>发私信</a>' +
|
||||
'</div>'
|
||||
).css({ left: x + 14, top: y + 14 }).show();
|
||||
}
|
||||
$(document).on('mouseenter', '.user-card-trigger', function(e) {
|
||||
var userId = $(this).data('user-id');
|
||||
if (!userId) return;
|
||||
if (cardCache[userId]) { showCard(cardCache[userId], e.clientX, e.clientY); return; }
|
||||
$.getJSON('/msg_api/user-card/' + userId, function(res) {
|
||||
if (res && res.code === 10000 && res.data) {
|
||||
cardCache[userId] = res.data;
|
||||
showCard(res.data, e.clientX, e.clientY);
|
||||
}
|
||||
});
|
||||
}).on('mousemove', '.user-card-trigger', function(e) {
|
||||
if ($card.is(':visible')) $card.css({ left: e.clientX + 14, top: e.clientY + 14 });
|
||||
}).on('mouseleave', '.user-card-trigger', function() {
|
||||
setTimeout(function() { if (!$card.is(':hover')) $card.hide(); }, 300);
|
||||
});
|
||||
$card.on('mouseleave', function() { $card.hide(); });
|
||||
})();
|
||||
</script>
|
||||
|
||||
<#-- 私信弹窗 Modal -->
|
||||
<div class="ui small modal" id="sendMsgModal">
|
||||
<div class="header"><i class="envelope icon"></i> 发送私信给 <span id="msgModalUsername"></span></div>
|
||||
<div class="content">
|
||||
<div class="ui form">
|
||||
<div class="field">
|
||||
<textarea id="msgModalContent" rows="4" placeholder="输入消息内容..." style="resize:none;"></textarea>
|
||||
</div>
|
||||
<div id="msgModalError" class="ui red message" style="display:none;"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<div class="ui cancel button">取消</div>
|
||||
<div class="ui primary button" id="msgModalSendBtn"><i class="send icon"></i>发送</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
var _msgReceiverId = null;
|
||||
var _msgCsrfToken = $('meta[name="_csrf"]').attr('content');
|
||||
var _msgCsrfHeader = $('meta[name="_csrf_header"]').attr('content');
|
||||
$(document).on('click', '.open-msg-modal', function() {
|
||||
_msgReceiverId = $(this).data('uid');
|
||||
$('#msgModalUsername').text($(this).data('uname'));
|
||||
$('#msgModalContent').val('');
|
||||
$('#msgModalError').hide();
|
||||
$('#jUserCard').hide();
|
||||
$('#sendMsgModal').modal('show');
|
||||
});
|
||||
$('#msgModalSendBtn').on('click', function() {
|
||||
var content = $('#msgModalContent').val().trim();
|
||||
if (!content) { $('#msgModalError').text('消息内容不能为空').show(); return; }
|
||||
var $btn = $(this).addClass('loading disabled');
|
||||
var headers = {}; headers[_msgCsrfHeader] = _msgCsrfToken;
|
||||
$.ajax({
|
||||
url: '/msg_api/messages/send', type: 'POST', headers: headers,
|
||||
data: { receiverId: _msgReceiverId, content: content },
|
||||
success: function(resp) {
|
||||
$btn.removeClass('loading disabled');
|
||||
if (resp && resp.code === 10000) { $('#sendMsgModal').modal('hide'); }
|
||||
else { $('#msgModalError').text((resp && resp.msg) || '发送失败').show(); }
|
||||
},
|
||||
error: function() { $btn.removeClass('loading disabled'); $('#msgModalError').text('请先登录').show(); }
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>搜索 - Jiscuss</title>
|
||||
<meta name="_csrf" content="${_csrf.token}"/>
|
||||
<meta name="_csrf_header" content="${_csrf.headerName}"/>
|
||||
<#include "comm/commjs.ftl"/>
|
||||
</head>
|
||||
<body style="background: #f7f8fa;">
|
||||
<#include "comm/header.ftl"/>
|
||||
|
||||
<div class="ui container" id="container" style="margin-top: 20px;">
|
||||
|
||||
<div class="ui segment">
|
||||
<form action="/search" method="get" class="ui form">
|
||||
<div class="ui large fluid action input">
|
||||
<input type="text" name="q" value="${q!''}" placeholder="搜索主题..." autofocus>
|
||||
<button type="submit" class="ui primary button"><i class="search icon"></i>搜索</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<#if q?? && q != "">
|
||||
<div class="ui secondary segment" style="margin-top:0; border-top:none;">
|
||||
<#if totalElements?? && totalElements gt 0>
|
||||
<span style="color:#555;">找到 <strong>${totalElements}</strong> 个关于 "<strong>${q}</strong>" 的结果</span>
|
||||
<#else>
|
||||
<span style="color:#888;">没有找到关于 "<strong>${q}</strong>" 的相关结果</span>
|
||||
</#if>
|
||||
</div>
|
||||
</#if>
|
||||
|
||||
<#if results?? && results?size gt 0>
|
||||
<div class="ui relaxed divided items" style="margin-top:10px;">
|
||||
<#list results as disc>
|
||||
<div class="item" style="padding: 14px 0; border-bottom: 1px solid #eee;">
|
||||
<div class="content">
|
||||
<a class="header" href="/getdiscussionsbyid?id=${disc.id}" style="font-size:1.1em; color:#2185d0;">
|
||||
${disc.title}
|
||||
</a>
|
||||
<div class="description" style="margin-top:6px; color:#555; font-size:.9em; overflow:hidden; white-space:nowrap; text-overflow:ellipsis; max-width:700px;">
|
||||
${disc.content!''}
|
||||
</div>
|
||||
<div class="meta" style="margin-top:6px; color:#999; font-size:.85em;">
|
||||
<i class="user icon"></i> <b>${disc.username!''}</b>
|
||||
•
|
||||
<i class="clock outline icon"></i> ${disc.startTime!''}
|
||||
|
||||
<i class="like icon"></i> ${disc.likeCount!0}
|
||||
|
||||
<i class="comment icon"></i> ${disc.commentsCount!0} 回复
|
||||
|
||||
<i class="eye icon"></i> ${disc.viewCount!0} 浏览
|
||||
<#if disc.tagList?? && disc.tagList?size gt 0>
|
||||
|
||||
<#list disc.tagList as tag>
|
||||
<a class="ui mini ${tag.color!''} label">${tag.name}</a>
|
||||
</#list>
|
||||
</#if>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</#list>
|
||||
</div>
|
||||
|
||||
<#if totalPages?? && totalPages gt 1>
|
||||
<div class="ui pagination menu" style="margin-top:20px;">
|
||||
<#if pageNum gt 1>
|
||||
<a class="item" href="/search?q=${q?url}&pageNum=${pageNum - 1}"><i class="left chevron icon"></i>上一页</a>
|
||||
</#if>
|
||||
<#list 1..totalPages as p>
|
||||
<a class="item <#if p == pageNum>active</#if>" href="/search?q=${q?url}&pageNum=${p}">${p}</a>
|
||||
</#list>
|
||||
<#if pageNum lt totalPages>
|
||||
<a class="item" href="/search?q=${q?url}&pageNum=${pageNum + 1}">下一页<i class="right chevron icon"></i></a>
|
||||
</#if>
|
||||
</div>
|
||||
</#if>
|
||||
|
||||
<#elseif q?? && q != "">
|
||||
<div class="ui placeholder segment" style="margin-top:10px;">
|
||||
<div class="ui icon header">
|
||||
<i class="search icon"></i>
|
||||
暂无搜索结果
|
||||
</div>
|
||||
<a class="ui primary button" href="/">返回首页</a>
|
||||
</div>
|
||||
</#if>
|
||||
|
||||
</div>
|
||||
|
||||
<#include "comm/footer.ftl"/>
|
||||
</body>
|
||||
</html>
|
||||
@@ -11,6 +11,52 @@
|
||||
.profile-stat .number { font-size:1.6em; font-weight:bold; color:var(--color-primary,#2185d0); }
|
||||
.profile-stat .label2 { color:#888; font-size:0.85em; }
|
||||
.tab-content { background:#fff; border-radius:8px; padding:1.2em; min-height:200px; }
|
||||
|
||||
/* 彩蛋按钮特效 */
|
||||
@keyframes rainbowBg {
|
||||
0% { background-position: 0% 50%; }
|
||||
50% { background-position: 100% 50%; }
|
||||
100% { background-position: 0% 50%; }
|
||||
}
|
||||
@keyframes btnGlow {
|
||||
0%, 100% { box-shadow: 0 0 8px 2px rgba(41,128,185,.6); }
|
||||
33% { box-shadow: 0 0 10px 3px rgba(192,57,43,.6); }
|
||||
66% { box-shadow: 0 0 10px 3px rgba(39,174,96,.6); }
|
||||
}
|
||||
@keyframes btnWiggle {
|
||||
0%,100% { transform: rotate(0deg) scale(1); }
|
||||
20% { transform: rotate(-4deg) scale(1.08); }
|
||||
40% { transform: rotate(4deg) scale(1.08); }
|
||||
60% { transform: rotate(-3deg) scale(1.05); }
|
||||
80% { transform: rotate(3deg) scale(1.05); }
|
||||
}
|
||||
@keyframes ripple {
|
||||
to { transform: scale(3); opacity: 0; }
|
||||
}
|
||||
#userButton {
|
||||
position: relative; overflow: hidden;
|
||||
border: 2px solid transparent !important;
|
||||
background: linear-gradient(#fff,#fff) padding-box,
|
||||
linear-gradient(270deg,#e74c3c,#f39c12,#27ae60,#2980b9,#8e44ad,#e74c3c) border-box;
|
||||
background-size: auto, 400% 400%;
|
||||
animation: rainbowBg 4s ease infinite, btnGlow 3s ease-in-out infinite;
|
||||
color: #333 !important;
|
||||
font-weight: bold;
|
||||
letter-spacing: .5px;
|
||||
transition: transform .15s;
|
||||
}
|
||||
#userButton:hover {
|
||||
animation: btnWiggle .5s ease, rainbowBg 4s ease infinite, btnGlow 3s ease-in-out infinite;
|
||||
cursor: pointer;
|
||||
}
|
||||
#userButton .ripple-circle {
|
||||
position: absolute; border-radius: 50%;
|
||||
background: rgba(100,100,255,.25);
|
||||
width: 60px; height: 60px;
|
||||
margin-top: -30px; margin-left: -30px;
|
||||
animation: ripple .6s linear;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body style="background:#f7f8fa;">
|
||||
@@ -39,6 +85,13 @@
|
||||
加入于 ${userEntity.joinTime?string("yyyy-MM-dd")}
|
||||
</div>
|
||||
</#if>
|
||||
<div style="margin-top:14px;">
|
||||
<#if isOwn!true>
|
||||
<button class="ui basic button" id="userButton">
|
||||
<i class="hand peace outline icon"></i>小按钮·点下试试
|
||||
</button>
|
||||
</#if>
|
||||
</div>
|
||||
</div>
|
||||
<div class="profile-card">
|
||||
<div class="ui three column grid" style="text-align:center;">
|
||||
@@ -148,5 +201,34 @@
|
||||
</div>
|
||||
|
||||
<#include "comm/footer.ftl"/>
|
||||
|
||||
<#if isOwn!true>
|
||||
<div class="ui bottom three item labeled icon sidebar menu scale down" id="userButtonsidebar">
|
||||
<a class="item" href="/user?type=discussion">
|
||||
<i class="list icon"></i>主题
|
||||
</a>
|
||||
<a class="item" href="/user?type=reply">
|
||||
<i class="comment icon"></i>回复
|
||||
</a>
|
||||
<a class="item" href="/user?type=like">
|
||||
<i class="heart icon"></i>点赞
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript" charset="UTF-8" src="/static/js/user/user.js"></script>
|
||||
<script>
|
||||
// Ripple effect on button click
|
||||
document.getElementById('userButton') && document.getElementById('userButton').addEventListener('click', function(e) {
|
||||
var btn = this;
|
||||
var circle = document.createElement('span');
|
||||
circle.classList.add('ripple-circle');
|
||||
var rect = btn.getBoundingClientRect();
|
||||
circle.style.left = (e.clientX - rect.left) + 'px';
|
||||
circle.style.top = (e.clientY - rect.top) + 'px';
|
||||
btn.appendChild(circle);
|
||||
setTimeout(function(){ circle.remove(); }, 650);
|
||||
});
|
||||
</script>
|
||||
</#if>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user