Compare commits
4 Commits
cda8a3e0ee
...
3c1076834b
| Author | SHA1 | Date | |
|---|---|---|---|
| 3c1076834b | |||
| aeb360bc8a | |||
| ab9749a111 | |||
| aeb0866ec1 |
@@ -88,7 +88,7 @@ public class WebSecurityConfig {
|
|||||||
auth.requestMatchers(
|
auth.requestMatchers(
|
||||||
"/css/**", "/js/**", "/images/**", "/static/**", "/favicon.ico",
|
"/css/**", "/js/**", "/images/**", "/static/**", "/favicon.ico",
|
||||||
"/login/**", "/register", "/registerDo", "/initUserData",
|
"/login/**", "/register", "/registerDo", "/initUserData",
|
||||||
"/", "/main", "/index", "/getdiscussionsbyid",
|
"/", "/main", "/index", "/getdiscussionsbyid", "/search", "/profile",
|
||||||
// SpringDoc OpenAPI UI and spec endpoint (developer tools, no sensitive data)
|
// SpringDoc OpenAPI UI and spec endpoint (developer tools, no sensitive data)
|
||||||
"/swagger-ui/**", "/swagger-ui.html", "/v3/api-docs/**"
|
"/swagger-ui/**", "/swagger-ui.html", "/v3/api-docs/**"
|
||||||
).permitAll();
|
).permitAll();
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ public class AdminSystemController extends BaseController {
|
|||||||
private final UpgradeLogRepository upgradeLogRepository;
|
private final UpgradeLogRepository upgradeLogRepository;
|
||||||
private final AuditLogRepository auditLogRepository;
|
private final AuditLogRepository auditLogRepository;
|
||||||
private final AuditLogService auditLogService;
|
private final AuditLogService auditLogService;
|
||||||
|
private final com.yaoyuan.jiscuss.service.IScoreService scoreService;
|
||||||
|
|
||||||
public AdminSystemController(
|
public AdminSystemController(
|
||||||
UsersRepository usersRepository,
|
UsersRepository usersRepository,
|
||||||
@@ -65,7 +66,8 @@ public class AdminSystemController extends BaseController {
|
|||||||
UserRoleRepository userRoleRepository,
|
UserRoleRepository userRoleRepository,
|
||||||
UpgradeLogRepository upgradeLogRepository,
|
UpgradeLogRepository upgradeLogRepository,
|
||||||
AuditLogRepository auditLogRepository,
|
AuditLogRepository auditLogRepository,
|
||||||
AuditLogService auditLogService) {
|
AuditLogService auditLogService,
|
||||||
|
com.yaoyuan.jiscuss.service.IScoreService scoreService) {
|
||||||
this.usersRepository = usersRepository;
|
this.usersRepository = usersRepository;
|
||||||
this.discussionsRepository = discussionsRepository;
|
this.discussionsRepository = discussionsRepository;
|
||||||
this.postsRepository = postsRepository;
|
this.postsRepository = postsRepository;
|
||||||
@@ -75,6 +77,7 @@ public class AdminSystemController extends BaseController {
|
|||||||
this.upgradeLogRepository = upgradeLogRepository;
|
this.upgradeLogRepository = upgradeLogRepository;
|
||||||
this.auditLogRepository = auditLogRepository;
|
this.auditLogRepository = auditLogRepository;
|
||||||
this.auditLogService = auditLogService;
|
this.auditLogService = auditLogService;
|
||||||
|
this.scoreService = scoreService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/admin/home")
|
@GetMapping("/admin/home")
|
||||||
@@ -294,6 +297,27 @@ public class AdminSystemController extends BaseController {
|
|||||||
return "admin/themes";
|
return "admin/themes";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 积分管理 — list all users sorted by score desc. */
|
||||||
|
@GetMapping("/admin/scores")
|
||||||
|
public String scores(HttpServletRequest request, ModelMap map) {
|
||||||
|
fillCommon(request, map, "scores");
|
||||||
|
List<User> users = usersRepository.findAll(Sort.by(Sort.Direction.DESC, "score"));
|
||||||
|
map.put("users", users);
|
||||||
|
return "admin/scores";
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Manually adjust a user's score (admin operation). */
|
||||||
|
@PostMapping("/admin/scores/{id}/adjust")
|
||||||
|
public String adjustScore(@PathVariable Integer id,
|
||||||
|
@RequestParam int delta,
|
||||||
|
HttpServletRequest request) {
|
||||||
|
UserInfo operator = getUserInfo(request);
|
||||||
|
scoreService.addScore(id, delta);
|
||||||
|
auditLogService.log(operator, "score_adjust", "user", id,
|
||||||
|
"score " + (delta >= 0 ? "+" : "") + delta);
|
||||||
|
return "redirect:/admin/scores";
|
||||||
|
}
|
||||||
|
|
||||||
private void fillCommon(HttpServletRequest request, ModelMap map, String active) {
|
private void fillCommon(HttpServletRequest request, ModelMap map, String active) {
|
||||||
UserInfo user = getUserInfo(request);
|
UserInfo user = getUserInfo(request);
|
||||||
map.put("adminName", user == null ? "admin" : user.getUsername());
|
map.put("adminName", user == null ? "admin" : user.getUsername());
|
||||||
@@ -327,13 +351,30 @@ public class AdminSystemController extends BaseController {
|
|||||||
result.put("java", System.getProperty("java.version"));
|
result.put("java", System.getProperty("java.version"));
|
||||||
result.put("spring", implVersion("org.springframework.core.SpringVersion"));
|
result.put("spring", implVersion("org.springframework.core.SpringVersion"));
|
||||||
result.put("hibernate", implVersion("org.hibernate.Version"));
|
result.put("hibernate", implVersion("org.hibernate.Version"));
|
||||||
result.put("flyway", implVersion("org.flywaydb.core.Flyway"));
|
result.put("flyway", mavenVersion("org.flywaydb", "flyway-core",
|
||||||
result.put("druid", implVersion("com.alibaba.druid.pool.DruidDataSource"));
|
implVersion("org.flywaydb.core.Flyway")));
|
||||||
|
result.put("druid", mavenVersion("com.alibaba", "druid-spring-boot-3-starter",
|
||||||
|
mavenVersion("com.alibaba", "druid",
|
||||||
|
implVersion("com.alibaba.druid.pool.DruidDataSource"))));
|
||||||
result.put("ehcache", implVersion("org.ehcache.CacheManager"));
|
result.put("ehcache", implVersion("org.ehcache.CacheManager"));
|
||||||
result.put("springdoc", implVersion("org.springdoc.core.configuration.SpringDocConfiguration"));
|
result.put("springdoc", implVersion("org.springdoc.core.configuration.SpringDocConfiguration"));
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Read version from META-INF/maven pom.properties (works in fat JARs). Falls back to {@code fallback}. */
|
||||||
|
private String mavenVersion(String groupId, String artifactId, String fallback) {
|
||||||
|
String path = "/META-INF/maven/" + groupId + "/" + artifactId + "/pom.properties";
|
||||||
|
try (java.io.InputStream is = getClass().getResourceAsStream(path)) {
|
||||||
|
if (is != null) {
|
||||||
|
java.util.Properties props = new java.util.Properties();
|
||||||
|
props.load(is);
|
||||||
|
String v = props.getProperty("version");
|
||||||
|
if (v != null && !v.isBlank()) return v;
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
private String implVersion(String className) {
|
private String implVersion(String className) {
|
||||||
try {
|
try {
|
||||||
Class<?> clazz = Class.forName(className);
|
Class<?> clazz = Class.forName(className);
|
||||||
|
|||||||
@@ -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.data.domain.PageRequest;
|
||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
import org.springframework.ui.ModelMap;
|
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.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestParam;
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
|
||||||
@@ -100,6 +101,54 @@ public class UserPageController extends BaseController {
|
|||||||
map.put("likedDiscussions", liked);
|
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";
|
return "user";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import com.yaoyuan.jiscuss.entity.Tag;
|
|||||||
import com.yaoyuan.jiscuss.entity.User;
|
import com.yaoyuan.jiscuss.entity.User;
|
||||||
import com.yaoyuan.jiscuss.entity.UserInfo;
|
import com.yaoyuan.jiscuss.entity.UserInfo;
|
||||||
import com.yaoyuan.jiscuss.entity.custom.DiscussionCustom;
|
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.IDiscussionsService;
|
||||||
import com.yaoyuan.jiscuss.service.IDiscussionsTagsService;
|
import com.yaoyuan.jiscuss.service.IDiscussionsTagsService;
|
||||||
import com.yaoyuan.jiscuss.service.IPostsService;
|
import com.yaoyuan.jiscuss.service.IPostsService;
|
||||||
@@ -19,6 +21,7 @@ import org.slf4j.Logger;
|
|||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.BeanUtils;
|
import org.springframework.beans.BeanUtils;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.data.domain.PageRequest;
|
||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
import org.springframework.ui.ModelMap;
|
import org.springframework.ui.ModelMap;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
@@ -59,12 +62,21 @@ public class UserPostController extends BaseController {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private IUsersService usersService;
|
private IUsersService usersService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private DiscussionsRepository discussionsRepository;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private PostsRepository postsRepository;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private IpRegionService ipRegionService;
|
private IpRegionService ipRegionService;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private com.yaoyuan.jiscuss.service.INotificationService notificationService;
|
private com.yaoyuan.jiscuss.service.INotificationService notificationService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private com.yaoyuan.jiscuss.service.IScoreService scoreService;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 首页查看主题列表(各种条件筛选,最热最新标签等)
|
* 首页查看主题列表(各种条件筛选,最热最新标签等)
|
||||||
@@ -112,6 +124,19 @@ public class UserPostController extends BaseController {
|
|||||||
map.put("discussions", newdd);
|
map.put("discussions", newdd);
|
||||||
map.put("posts", postsObj);
|
map.put("posts", postsObj);
|
||||||
map.put("sort", sort);
|
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);
|
UserInfo user = getUserInfo(request);
|
||||||
if (user != null) {
|
if (user != null) {
|
||||||
map.put("username", user.getUsername());
|
map.put("username", user.getUsername());
|
||||||
@@ -161,6 +186,10 @@ public class UserPostController extends BaseController {
|
|||||||
logger.info(">>>{}", saveDiscussion);
|
logger.info(">>>{}", saveDiscussion);
|
||||||
resultobj.put("msg", "添加主题成功");
|
resultobj.put("msg", "添加主题成功");
|
||||||
resultobj.put("flag", true);
|
resultobj.put("flag", true);
|
||||||
|
// Award points for creating a discussion
|
||||||
|
if (user != null) {
|
||||||
|
scoreService.addScore(user.getId(), 10);
|
||||||
|
}
|
||||||
return resultobj;
|
return resultobj;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,6 +249,10 @@ public class UserPostController extends BaseController {
|
|||||||
logger.info(">>>{}", savePost);
|
logger.info(">>>{}", savePost);
|
||||||
resultobj.put("msg", "添加评论成功");
|
resultobj.put("msg", "添加评论成功");
|
||||||
resultobj.put("flag", true);
|
resultobj.put("flag", true);
|
||||||
|
// Award points for posting a reply
|
||||||
|
if (user != null) {
|
||||||
|
scoreService.addScore(user.getId(), 5);
|
||||||
|
}
|
||||||
return resultobj;
|
return resultobj;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import com.yaoyuan.jiscuss.entity.User;
|
|||||||
import com.yaoyuan.jiscuss.entity.UserInfo;
|
import com.yaoyuan.jiscuss.entity.UserInfo;
|
||||||
import com.yaoyuan.jiscuss.entity.custom.DiscussionCustom;
|
import com.yaoyuan.jiscuss.entity.custom.DiscussionCustom;
|
||||||
import com.yaoyuan.jiscuss.entity.custom.TagCustom;
|
import com.yaoyuan.jiscuss.entity.custom.TagCustom;
|
||||||
|
import com.yaoyuan.jiscuss.repository.DiscussionsRepository;
|
||||||
import com.yaoyuan.jiscuss.service.IDiscussionsService;
|
import com.yaoyuan.jiscuss.service.IDiscussionsService;
|
||||||
import com.yaoyuan.jiscuss.service.ITagsService;
|
import com.yaoyuan.jiscuss.service.ITagsService;
|
||||||
import com.yaoyuan.jiscuss.service.IUsersService;
|
import com.yaoyuan.jiscuss.service.IUsersService;
|
||||||
@@ -54,6 +55,9 @@ public class UserSystemController extends BaseController {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private BCryptPasswordEncoder passwordEncoder;
|
private BCryptPasswordEncoder passwordEncoder;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private DiscussionsRepository discussionsRepository;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 首页index
|
* 首页index
|
||||||
* @param tag
|
* @param tag
|
||||||
@@ -276,6 +280,36 @@ public class UserSystemController extends BaseController {
|
|||||||
return "register";
|
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";
|
||||||
|
}
|
||||||
|
|
||||||
//获取设置信息
|
//获取设置信息
|
||||||
|
|
||||||
//获取首页统计
|
//获取首页统计
|
||||||
|
|||||||
@@ -49,7 +49,9 @@ public class RestMsgController extends BaseController {
|
|||||||
"id", user.getId(),
|
"id", user.getId(),
|
||||||
"username", user.getUsername() != null ? user.getUsername() : "",
|
"username", user.getUsername() != null ? user.getUsername() : "",
|
||||||
"discussionsCount", user.getDiscussionsCount() != null ? user.getDiscussionsCount() : 0,
|
"discussionsCount", user.getDiscussionsCount() != null ? user.getDiscussionsCount() : 0,
|
||||||
"commentsCount", user.getCommentsCount() != null ? user.getCommentsCount() : 0
|
"commentsCount", user.getCommentsCount() != null ? user.getCommentsCount() : 0,
|
||||||
|
"level", user.getLevel() != null ? user.getLevel() : 0,
|
||||||
|
"score", user.getScore() != null ? user.getScore() : 0
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -82,6 +82,6 @@ public class Discussion implements Serializable {
|
|||||||
private Date createTime;
|
private Date createTime;
|
||||||
|
|
||||||
@Column(name = "view_count")
|
@Column(name = "view_count")
|
||||||
private Integer viewCount;
|
private Integer viewCount = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -76,4 +76,7 @@ public class User implements Serializable {
|
|||||||
|
|
||||||
@Column(name = "level")
|
@Column(name = "level")
|
||||||
private Integer level;
|
private Integer level;
|
||||||
|
|
||||||
|
@Column(name = "score")
|
||||||
|
private Integer score;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ public class UserInfo implements UserDetails {
|
|||||||
private String email;
|
private String email;
|
||||||
private String gender;
|
private String gender;
|
||||||
private Integer level;
|
private Integer level;
|
||||||
|
private Integer score;
|
||||||
private Integer flag;
|
private Integer flag;
|
||||||
|
|
||||||
public UserInfo() {
|
public UserInfo() {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import org.springframework.stereotype.Repository;
|
|||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
@Repository
|
@Repository
|
||||||
public interface DiscussionsRepository extends JpaRepository<Discussion, Integer> {
|
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.repository.query.Param("userId") Integer userId,
|
||||||
org.springframework.data.domain.Pageable pageable);
|
org.springframework.data.domain.Pageable pageable);
|
||||||
long countByStartUserId(Integer startUserId);
|
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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,4 +48,17 @@ public interface UsersRepository extends JpaRepository<User, Integer> {
|
|||||||
/** Top active users by comment count (for the ranking sidebar). Shows all users if counts are 0. */
|
/** Top active users by comment count (for the ranking sidebar). Shows all users if counts are 0. */
|
||||||
@Query("FROM User u ORDER BY COALESCE(u.commentsCount, 0) DESC")
|
@Query("FROM User u ORDER BY COALESCE(u.commentsCount, 0) DESC")
|
||||||
List<User> findTop10ActiveUsers(org.springframework.data.domain.Pageable pageable);
|
List<User> findTop10ActiveUsers(org.springframework.data.domain.Pageable pageable);
|
||||||
|
|
||||||
|
@Modifying
|
||||||
|
@Transactional
|
||||||
|
@Query("UPDATE User u SET u.score = GREATEST(0, COALESCE(u.score, 0) + :delta), " +
|
||||||
|
"u.level = CASE " +
|
||||||
|
" WHEN (GREATEST(0, COALESCE(u.score, 0) + :delta)) >= 2000 THEN 5 " +
|
||||||
|
" WHEN (GREATEST(0, COALESCE(u.score, 0) + :delta)) >= 1000 THEN 4 " +
|
||||||
|
" WHEN (GREATEST(0, COALESCE(u.score, 0) + :delta)) >= 500 THEN 3 " +
|
||||||
|
" WHEN (GREATEST(0, COALESCE(u.score, 0) + :delta)) >= 200 THEN 2 " +
|
||||||
|
" WHEN (GREATEST(0, COALESCE(u.score, 0) + :delta)) >= 50 THEN 1 " +
|
||||||
|
" ELSE 0 END " +
|
||||||
|
"WHERE u.id = :id")
|
||||||
|
void addScore(@Param("id") Integer id, @Param("delta") int delta);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package com.yaoyuan.jiscuss.service;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service for awarding activity points and recomputing user levels.
|
||||||
|
*
|
||||||
|
* <p>Level thresholds (score → level):
|
||||||
|
* <ul>
|
||||||
|
* <li>0 – 49 → 0 新手</li>
|
||||||
|
* <li>50 – 199 → 1 学徒</li>
|
||||||
|
* <li>200 – 499 → 2 熟手</li>
|
||||||
|
* <li>500 – 999 → 3 达人</li>
|
||||||
|
* <li>1000 – 1999 → 4 专家</li>
|
||||||
|
* <li>2000+ → 5 大神</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
public interface IScoreService {
|
||||||
|
|
||||||
|
/** Award (or deduct) {@code delta} points to the user and refresh their level. */
|
||||||
|
void addScore(Integer userId, int delta);
|
||||||
|
|
||||||
|
/** Compute the level (0-5) corresponding to a given score. */
|
||||||
|
static int computeLevel(int score) {
|
||||||
|
if (score >= 2000) return 5;
|
||||||
|
if (score >= 1000) return 4;
|
||||||
|
if (score >= 500) return 3;
|
||||||
|
if (score >= 200) return 2;
|
||||||
|
if (score >= 50) return 1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Human-readable name for a level (0-5). */
|
||||||
|
static String levelName(int level) {
|
||||||
|
return switch (level) {
|
||||||
|
case 1 -> "学徒";
|
||||||
|
case 2 -> "熟手";
|
||||||
|
case 3 -> "达人";
|
||||||
|
case 4 -> "专家";
|
||||||
|
case 5 -> "大神";
|
||||||
|
default -> "新手";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import com.yaoyuan.jiscuss.repository.LikeCollectRepository;
|
|||||||
import com.yaoyuan.jiscuss.repository.PostsRepository;
|
import com.yaoyuan.jiscuss.repository.PostsRepository;
|
||||||
import com.yaoyuan.jiscuss.service.ILikeCollectService;
|
import com.yaoyuan.jiscuss.service.ILikeCollectService;
|
||||||
import com.yaoyuan.jiscuss.service.INotificationService;
|
import com.yaoyuan.jiscuss.service.INotificationService;
|
||||||
|
import com.yaoyuan.jiscuss.entity.Discussion;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
@@ -29,9 +30,19 @@ public class LikeCollectServiceImpl implements ILikeCollectService {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private INotificationService notificationService;
|
private INotificationService notificationService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private com.yaoyuan.jiscuss.service.IScoreService scoreService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private com.yaoyuan.jiscuss.repository.UsersRepository usersRepository;
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
@Override
|
@Override
|
||||||
public String voteDiscussion(Integer userId, Integer discussionId, String action) {
|
public String voteDiscussion(Integer userId, Integer discussionId, String action) {
|
||||||
|
// Look up discussion author once for score updates
|
||||||
|
com.yaoyuan.jiscuss.entity.Discussion disc = discussionsRepository.findById(discussionId).orElse(null);
|
||||||
|
Integer authorId = (disc != null) ? disc.getStartUserId() : null;
|
||||||
|
|
||||||
Optional<LikeCollect> existing = likeCollectRepository.findDiscussionVote(userId, discussionId);
|
Optional<LikeCollect> existing = likeCollectRepository.findDiscussionVote(userId, discussionId);
|
||||||
if (existing.isPresent()) {
|
if (existing.isPresent()) {
|
||||||
LikeCollect vote = existing.get();
|
LikeCollect vote = existing.get();
|
||||||
@@ -39,6 +50,10 @@ public class LikeCollectServiceImpl implements ILikeCollectService {
|
|||||||
// Toggle off: cancel the same action
|
// Toggle off: cancel the same action
|
||||||
likeCollectRepository.delete(vote);
|
likeCollectRepository.delete(vote);
|
||||||
adjustDiscussion(discussionId, action, -1);
|
adjustDiscussion(discussionId, action, -1);
|
||||||
|
// Reverse score for the author (only likes affect score)
|
||||||
|
if ("like".equals(action) && authorId != null && !authorId.equals(userId)) {
|
||||||
|
scoreService.addScore(authorId, -2);
|
||||||
|
}
|
||||||
return "none";
|
return "none";
|
||||||
} else {
|
} else {
|
||||||
// Switch action: undo old, apply new
|
// Switch action: undo old, apply new
|
||||||
@@ -46,6 +61,14 @@ public class LikeCollectServiceImpl implements ILikeCollectService {
|
|||||||
vote.setAction(action);
|
vote.setAction(action);
|
||||||
likeCollectRepository.save(vote);
|
likeCollectRepository.save(vote);
|
||||||
adjustDiscussion(discussionId, action, +1);
|
adjustDiscussion(discussionId, action, +1);
|
||||||
|
// Switched from like→dislike: deduct; dislike→like: award
|
||||||
|
if (authorId != null && !authorId.equals(userId)) {
|
||||||
|
if ("like".equals(action)) {
|
||||||
|
scoreService.addScore(authorId, +2);
|
||||||
|
} else {
|
||||||
|
scoreService.addScore(authorId, -2);
|
||||||
|
}
|
||||||
|
}
|
||||||
return action;
|
return action;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -59,6 +82,10 @@ public class LikeCollectServiceImpl implements ILikeCollectService {
|
|||||||
vote.setCreateTime(new Date());
|
vote.setCreateTime(new Date());
|
||||||
likeCollectRepository.save(vote);
|
likeCollectRepository.save(vote);
|
||||||
adjustDiscussion(discussionId, action, +1);
|
adjustDiscussion(discussionId, action, +1);
|
||||||
|
// Award score to discussion author on new like
|
||||||
|
if ("like".equals(action) && authorId != null && !authorId.equals(userId)) {
|
||||||
|
scoreService.addScore(authorId, +2);
|
||||||
|
}
|
||||||
return action;
|
return action;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -72,12 +99,28 @@ public class LikeCollectServiceImpl implements ILikeCollectService {
|
|||||||
if (vote.getAction().equals(action)) {
|
if (vote.getAction().equals(action)) {
|
||||||
likeCollectRepository.delete(vote);
|
likeCollectRepository.delete(vote);
|
||||||
adjustPost(postId, action, -1);
|
adjustPost(postId, action, -1);
|
||||||
|
// Reverse score on unlike
|
||||||
|
if ("like".equals(action)) {
|
||||||
|
Post post = postsRepository.findById(postId).orElse(null);
|
||||||
|
if (post != null && post.getCreateId() != null && !post.getCreateId().equals(userId)) {
|
||||||
|
scoreService.addScore(post.getCreateId(), -2);
|
||||||
|
}
|
||||||
|
}
|
||||||
return "none";
|
return "none";
|
||||||
} else {
|
} else {
|
||||||
adjustPost(postId, vote.getAction(), -1);
|
adjustPost(postId, vote.getAction(), -1);
|
||||||
vote.setAction(action);
|
vote.setAction(action);
|
||||||
likeCollectRepository.save(vote);
|
likeCollectRepository.save(vote);
|
||||||
adjustPost(postId, action, +1);
|
adjustPost(postId, action, +1);
|
||||||
|
// Switched vote direction: adjust score accordingly
|
||||||
|
Post post = postsRepository.findById(postId).orElse(null);
|
||||||
|
if (post != null && post.getCreateId() != null && !post.getCreateId().equals(userId)) {
|
||||||
|
if ("like".equals(action)) {
|
||||||
|
scoreService.addScore(post.getCreateId(), +2);
|
||||||
|
} else {
|
||||||
|
scoreService.addScore(post.getCreateId(), -2);
|
||||||
|
}
|
||||||
|
}
|
||||||
return action;
|
return action;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -90,11 +133,14 @@ public class LikeCollectServiceImpl implements ILikeCollectService {
|
|||||||
vote.setCreateTime(new Date());
|
vote.setCreateTime(new Date());
|
||||||
likeCollectRepository.save(vote);
|
likeCollectRepository.save(vote);
|
||||||
adjustPost(postId, action, +1);
|
adjustPost(postId, action, +1);
|
||||||
// Notify post author when liked (not disliked)
|
// Award score to post author on new like; also send notification
|
||||||
if ("like".equals(action)) {
|
if ("like".equals(action)) {
|
||||||
Post post = postsRepository.findById(postId).orElse(null);
|
Post post = postsRepository.findById(postId).orElse(null);
|
||||||
if (post != null && post.getCreateId() != null) {
|
if (post != null && post.getCreateId() != null) {
|
||||||
notificationService.notifyLike(post.getCreateId(), userId, postId, post.getDiscussionId());
|
notificationService.notifyLike(post.getCreateId(), userId, postId, post.getDiscussionId());
|
||||||
|
if (!post.getCreateId().equals(userId)) {
|
||||||
|
scoreService.addScore(post.getCreateId(), +2);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return action;
|
return action;
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package com.yaoyuan.jiscuss.service.impl;
|
||||||
|
|
||||||
|
import com.yaoyuan.jiscuss.repository.UsersRepository;
|
||||||
|
import com.yaoyuan.jiscuss.service.IScoreService;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.cache.annotation.CacheEvict;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class ScoreServiceImpl implements IScoreService {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private UsersRepository usersRepository;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
@CacheEvict(value = "user", key = "#userId")
|
||||||
|
public void addScore(Integer userId, int delta) {
|
||||||
|
if (userId == null || delta == 0) return;
|
||||||
|
usersRepository.addScore(userId, delta);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
-- V10: Add score column for the points/level system.
|
||||||
|
-- score: cumulative activity points (default 0).
|
||||||
|
-- level is recomputed by ScoreServiceImpl whenever score changes.
|
||||||
|
ALTER TABLE user ADD COLUMN score INTEGER NOT NULL DEFAULT 0;
|
||||||
|
|
||||||
|
-- Back-fill an initial score for existing users based on their activity counts.
|
||||||
|
-- Formula: discussions_count*10 + comments_count*5 (mirrors live award rates)
|
||||||
|
UPDATE user
|
||||||
|
SET score = COALESCE(discussions_count, 0) * 10 + COALESCE(comments_count, 0) * 5;
|
||||||
|
|
||||||
|
-- Recompute level for ALL users based on back-filled score.
|
||||||
|
-- Thresholds: 0=新手(<50), 1=学徒(50-199), 2=熟手(200-499),
|
||||||
|
-- 3=达人(500-999), 4=专家(1000-1999), 5=大神(>=2000)
|
||||||
|
-- Note: admin access is now RBAC-based; level is purely cosmetic.
|
||||||
|
UPDATE user SET level =
|
||||||
|
CASE
|
||||||
|
WHEN score >= 2000 THEN 5
|
||||||
|
WHEN score >= 1000 THEN 4
|
||||||
|
WHEN score >= 500 THEN 3
|
||||||
|
WHEN score >= 200 THEN 2
|
||||||
|
WHEN score >= 50 THEN 1
|
||||||
|
ELSE 0
|
||||||
|
END;
|
||||||
@@ -9,46 +9,146 @@
|
|||||||
<title>${title}</title>
|
<title>${title}</title>
|
||||||
<#include "admin/admin-commjs.ftl"/>
|
<#include "admin/admin-commjs.ftl"/>
|
||||||
<style>
|
<style>
|
||||||
body { background: #f6f7fb; }
|
body { background: #f6f7fb; margin: 0; font-family: 'Lato', 'Helvetica Neue', Arial, sans-serif; }
|
||||||
.admin-header { background: #1f2937; color: #fff; padding: 12px 18px; }
|
|
||||||
.admin-grid { display: grid; grid-template-columns: 230px 1fr; gap: 16px; padding: 16px; }
|
/* ── Top navigation bar (matches main site style) ── */
|
||||||
.admin-sidebar { background: #fff; border-radius: 8px; padding: 8px; box-shadow: 0 1px 3px rgba(0,0,0,.08); }
|
.admin-topbar {
|
||||||
.admin-main { background: #fff; border-radius: 8px; padding: 16px; box-shadow: 0 1px 3px rgba(0,0,0,.08); }
|
background: #1b1c1d;
|
||||||
.admin-sidebar a.item.active { background: #1d4ed8 !important; color: #fff !important; border-radius: 6px; }
|
color: #fff;
|
||||||
.metric-card { border: 1px solid #e5e7eb; border-radius: 8px; padding: 12px; }
|
padding: 0 20px;
|
||||||
.small-muted { color: #6b7280; font-size: 12px; }
|
height: 50px;
|
||||||
@media (max-width: 900px) {
|
display: flex;
|
||||||
.admin-grid { grid-template-columns: 1fr; }
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,.2);
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 100;
|
||||||
}
|
}
|
||||||
|
.admin-topbar .brand { font-size: 1.05em; font-weight: 700; display: flex; align-items: center; gap: 8px; }
|
||||||
|
.admin-topbar .brand img { height: 24px; }
|
||||||
|
.admin-topbar .nav-right { display: flex; align-items: center; gap: 18px; font-size: .88em; }
|
||||||
|
.admin-topbar .nav-right a { color: #93c5fd; text-decoration: none; }
|
||||||
|
.admin-topbar .nav-right a:hover { color: #fff; }
|
||||||
|
.admin-topbar .nav-right .admin-name { color: #d1d5db; }
|
||||||
|
|
||||||
|
/* ── Layout ── */
|
||||||
|
.admin-layout { display: grid; grid-template-columns: 220px 1fr; min-height: calc(100vh - 50px - 60px); gap: 0; }
|
||||||
|
@media (max-width: 900px) { .admin-layout { grid-template-columns: 1fr; } }
|
||||||
|
|
||||||
|
/* ── Sidebar ── */
|
||||||
|
.admin-sidebar {
|
||||||
|
background: #fff;
|
||||||
|
border-right: 1px solid #e5e7eb;
|
||||||
|
padding: 12px 8px;
|
||||||
|
}
|
||||||
|
.admin-sidebar .section-label {
|
||||||
|
font-size: .72em; font-weight: 700; color: #9ca3af;
|
||||||
|
text-transform: uppercase; letter-spacing: .06em;
|
||||||
|
padding: 8px 12px 4px;
|
||||||
|
}
|
||||||
|
.admin-sidebar a.item {
|
||||||
|
display: flex; align-items: center; gap: 8px;
|
||||||
|
padding: 9px 14px; border-radius: 7px;
|
||||||
|
color: #374151; font-size: .91em;
|
||||||
|
text-decoration: none; margin: 1px 0;
|
||||||
|
transition: background .15s, color .15s;
|
||||||
|
}
|
||||||
|
.admin-sidebar a.item:hover { background: #f3f4f6; color: #111; }
|
||||||
|
.admin-sidebar a.item.active { background: #1d4ed8; color: #fff; font-weight: 600; }
|
||||||
|
.admin-sidebar a.item.active:hover { background: #1e40af; }
|
||||||
|
.admin-sidebar .divider { border-top: 1px solid #e5e7eb; margin: 8px 0; }
|
||||||
|
|
||||||
|
/* ── Main content ── */
|
||||||
|
.admin-main { padding: 24px; }
|
||||||
|
.admin-main .page-card {
|
||||||
|
background: #fff; border-radius: 10px;
|
||||||
|
box-shadow: 0 1px 4px rgba(0,0,0,.07);
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Footer ── */
|
||||||
|
.admin-footer {
|
||||||
|
background: #fff;
|
||||||
|
border-top: 1px solid #e5e7eb;
|
||||||
|
padding: 14px 24px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: .82em;
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
.admin-footer a { color: #6b7280; text-decoration: none; }
|
||||||
|
.admin-footer a:hover { color: #2185d0; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="admin-header">
|
|
||||||
<div class="ui grid">
|
<div class="admin-topbar">
|
||||||
<div class="ten wide column">Jiscuss 后台管理</div>
|
<div class="brand">
|
||||||
<div class="six wide right aligned column">当前管理员: ${adminName} | <a style="color:#93c5fd" href="/swagger-ui.html" target="_blank">接口文档</a> | <a style="color:#93c5fd" href="/">前台首页</a></div>
|
<img src="/static/assets/images/logo.png" alt="logo">
|
||||||
|
Jiscuss 后台管理
|
||||||
|
</div>
|
||||||
|
<div class="nav-right">
|
||||||
|
<span class="admin-name"><i class="user circle icon"></i> ${adminName}</span>
|
||||||
|
<a href="/swagger-ui.html" target="_blank"><i class="book icon"></i> 接口文档</a>
|
||||||
|
<a href="/"><i class="home icon"></i> 前台首页</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="admin-grid">
|
<div class="admin-layout">
|
||||||
<div class="admin-sidebar">
|
<div class="admin-sidebar">
|
||||||
<div class="ui vertical fluid menu">
|
<div class="section-label">概览</div>
|
||||||
<a class="item <#if active=='home'>active</#if>" href="/admin/home">总览与运维</a>
|
<a class="item <#if active=='home'>active</#if>" href="/admin/home">
|
||||||
<a class="item <#if active=='users'>active</#if>" href="/admin/users">用户与角色</a>
|
<i class="tachometer alternate icon"></i>总览与运维
|
||||||
<a class="item <#if active=='discussions'>active</#if>" href="/admin/discussions">文章管理</a>
|
</a>
|
||||||
<a class="item <#if active=='posts'>active</#if>" href="/admin/posts">评论管理</a>
|
<div class="divider"></div>
|
||||||
<a class="item <#if active=='upgradeLogs'>active</#if>" href="/admin/upgrade-logs">版本升级日志</a>
|
<div class="section-label">内容管理</div>
|
||||||
<a class="item <#if active=='auditLogs'>active</#if>" href="/admin/audit-logs">操作审计日志</a>
|
<a class="item <#if active=='discussions'>active</#if>" href="/admin/discussions">
|
||||||
<div class="item small-muted">预留扩展</div>
|
<i class="list alternate outline icon"></i>文章管理
|
||||||
<a class="item <#if active=='plugins'>active</#if>" href="/admin/plugins">插件管理(预留)</a>
|
</a>
|
||||||
<a class="item <#if active=='themes'>active</#if>" href="/admin/themes">主题管理(预留)</a>
|
<a class="item <#if active=='posts'>active</#if>" href="/admin/posts">
|
||||||
</div>
|
<i class="comments outline icon"></i>评论管理
|
||||||
|
</a>
|
||||||
|
<div class="divider"></div>
|
||||||
|
<div class="section-label">用户管理</div>
|
||||||
|
<a class="item <#if active=='users'>active</#if>" href="/admin/users">
|
||||||
|
<i class="users icon"></i>用户与角色
|
||||||
|
</a>
|
||||||
|
<a class="item <#if active=='scores'>active</#if>" href="/admin/scores">
|
||||||
|
<i class="trophy icon"></i>积分管理
|
||||||
|
</a>
|
||||||
|
<div class="divider"></div>
|
||||||
|
<div class="section-label">系统</div>
|
||||||
|
<a class="item <#if active=='upgradeLogs'>active</#if>" href="/admin/upgrade-logs">
|
||||||
|
<i class="history icon"></i>升级日志
|
||||||
|
</a>
|
||||||
|
<a class="item <#if active=='auditLogs'>active</#if>" href="/admin/audit-logs">
|
||||||
|
<i class="shield alternate icon"></i>操作审计
|
||||||
|
</a>
|
||||||
|
<div class="divider"></div>
|
||||||
|
<div class="section-label">预留</div>
|
||||||
|
<a class="item <#if active=='plugins'>active</#if>" href="/admin/plugins">
|
||||||
|
<i class="plug icon"></i>插件管理
|
||||||
|
</a>
|
||||||
|
<a class="item <#if active=='themes'>active</#if>" href="/admin/themes">
|
||||||
|
<i class="paint brush icon"></i>主题管理
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="admin-main">
|
<div class="admin-main">
|
||||||
|
<div class="page-card">
|
||||||
<#nested>
|
<#nested>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="admin-footer">
|
||||||
|
<span>Jiscuss 后台管理系统 © 2019-2026</span>
|
||||||
|
<span>Powered by <strong>Jiscuss V2.0</strong> | <a href="/">返回前台</a></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
</#macro>
|
</#macro>
|
||||||
|
|
||||||
|
|||||||
@@ -1,50 +1,75 @@
|
|||||||
<#import "admin/admin-shell.ftl" as shell>
|
<#import "admin/admin-shell.ftl" as shell>
|
||||||
<@shell.page title="后台管理 - 总览" active=active adminName=adminName>
|
<@shell.page title="后台管理 - 总览" active=active adminName=adminName>
|
||||||
<h2 class="ui header">系统总览</h2>
|
<h2 class="ui header" style="margin-bottom:1.2em;"><i class="tachometer alternate icon"></i>系统总览</h2>
|
||||||
|
|
||||||
<div class="ui four stackable cards">
|
<div class="ui four stackable cards" style="margin-bottom:1.5em;">
|
||||||
<div class="card"><div class="content"><div class="header">用户总数</div><div class="description">${userCount}</div></div></div>
|
<div class="card" style="border-radius:8px; box-shadow:0 1px 4px rgba(0,0,0,.08);">
|
||||||
<div class="card"><div class="content"><div class="header">文章总数</div><div class="description">${discussionCount}</div></div></div>
|
|
||||||
<div class="card"><div class="content"><div class="header">评论总数</div><div class="description">${postCount}</div></div></div>
|
|
||||||
<div class="card"><div class="content"><div class="header">角色总数</div><div class="description">${roleCount}</div></div></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h3 class="ui dividing header">服务器与运行时</h3>
|
|
||||||
<div class="ui two stackable cards">
|
|
||||||
<div class="card">
|
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<div class="header">CPU / 内存</div>
|
<div class="header" style="font-size:1em; color:#6b7280;">用户总数</div>
|
||||||
<div class="description">
|
<div class="description" style="font-size:2em; font-weight:700; color:#2185d0;">${userCount}</div>
|
||||||
<p>CPU 负载: ${sys.cpuLoad}</p>
|
|
||||||
<p>JVM 已用内存: ${sys.usedMb} MB / ${sys.totalMb} MB</p>
|
|
||||||
<p>JVM 最大内存: ${sys.maxMb} MB</p>
|
|
||||||
<p>Heap 已用: ${sys.heapUsedMb} MB</p>
|
|
||||||
<p>应用运行时长: ${sys.uptimeMs} ms</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="card" style="border-radius:8px; box-shadow:0 1px 4px rgba(0,0,0,.08);">
|
||||||
<div class="card">
|
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<div class="header">关键依赖版本</div>
|
<div class="header" style="font-size:1em; color:#6b7280;">文章总数</div>
|
||||||
|
<div class="description" style="font-size:2em; font-weight:700; color:#21ba45;">${discussionCount}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card" style="border-radius:8px; box-shadow:0 1px 4px rgba(0,0,0,.08);">
|
||||||
|
<div class="content">
|
||||||
|
<div class="header" style="font-size:1em; color:#6b7280;">评论总数</div>
|
||||||
|
<div class="description" style="font-size:2em; font-weight:700; color:#f2711c;">${postCount}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card" style="border-radius:8px; box-shadow:0 1px 4px rgba(0,0,0,.08);">
|
||||||
|
<div class="content">
|
||||||
|
<div class="header" style="font-size:1em; color:#6b7280;">角色总数</div>
|
||||||
|
<div class="description" style="font-size:2em; font-weight:700; color:#a333c8;">${roleCount}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 class="ui dividing header"><i class="server icon"></i>服务器与运行时</h3>
|
||||||
|
<div class="ui two stackable cards" style="margin-bottom:1.5em;">
|
||||||
|
<div class="card" style="border-radius:8px; box-shadow:0 1px 4px rgba(0,0,0,.08);">
|
||||||
|
<div class="content">
|
||||||
|
<div class="header" style="font-size:1em; color:#374151; margin-bottom:.6em;"><i class="microchip icon"></i>CPU / 内存</div>
|
||||||
<div class="description">
|
<div class="description">
|
||||||
<p>Spring Boot: ${deps.springBoot}</p>
|
<p><span style="color:#888;">CPU 负载</span> <b>${sys.cpuLoad}</b></p>
|
||||||
<p>Java: ${deps.java}</p>
|
<p><span style="color:#888;">JVM 已用</span> <b>${sys.usedMb} MB</b> / ${sys.totalMb} MB</p>
|
||||||
<p>Spring: ${deps.spring}</p>
|
<p><span style="color:#888;">JVM 最大</span> <b>${sys.maxMb} MB</b></p>
|
||||||
<p>Hibernate: ${deps.hibernate}</p>
|
<p><span style="color:#888;">Heap 已用</span> <b>${sys.heapUsedMb} MB</b></p>
|
||||||
<p>Flyway: ${deps.flyway}</p>
|
<p><span style="color:#888;">运行时长</span> <b>${sys.uptimeMs} ms</b></p>
|
||||||
<p>Druid: ${deps.druid}</p>
|
</div>
|
||||||
<p>Ehcache: ${deps.ehcache}</p>
|
</div>
|
||||||
<p>SpringDoc: ${deps.springdoc}</p>
|
</div>
|
||||||
|
<div class="card" style="border-radius:8px; box-shadow:0 1px 4px rgba(0,0,0,.08);">
|
||||||
|
<div class="content">
|
||||||
|
<div class="header" style="font-size:1em; color:#374151; margin-bottom:.6em;"><i class="code branch icon"></i>关键依赖版本</div>
|
||||||
|
<div class="description">
|
||||||
|
<table style="width:100%; border-collapse:collapse; font-size:.9em;">
|
||||||
|
<tr><td style="color:#888; padding:2px 0;">Spring Boot</td><td><b>${deps.springBoot}</b></td></tr>
|
||||||
|
<tr><td style="color:#888; padding:2px 0;">Java</td><td><b>${deps.java}</b></td></tr>
|
||||||
|
<tr><td style="color:#888; padding:2px 0;">Spring</td><td><b>${deps.spring}</b></td></tr>
|
||||||
|
<tr><td style="color:#888; padding:2px 0;">Hibernate</td><td><b>${deps.hibernate}</b></td></tr>
|
||||||
|
<tr><td style="color:#888; padding:2px 0;">Flyway</td><td><b>${deps.flyway}</b></td></tr>
|
||||||
|
<tr><td style="color:#888; padding:2px 0;">Druid</td><td><b>${deps.druid}</b></td></tr>
|
||||||
|
<tr><td style="color:#888; padding:2px 0;">Ehcache</td><td><b>${deps.ehcache}</b></td></tr>
|
||||||
|
<tr><td style="color:#888; padding:2px 0;">SpringDoc</td><td><b>${deps.springdoc}</b></td></tr>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3 class="ui dividing header">快捷入口</h3>
|
<h3 class="ui dividing header"><i class="lightning icon"></i>快捷入口</h3>
|
||||||
<div class="ui buttons">
|
<div style="display:flex; flex-wrap:wrap; gap:10px;">
|
||||||
<a class="ui primary button" href="/admin/users">用户与角色</a>
|
<a class="ui primary button" href="/admin/users"><i class="users icon"></i>用户与角色</a>
|
||||||
<a class="ui button" href="/admin/discussions">文章管理</a>
|
<a class="ui teal button" href="/admin/scores"><i class="trophy icon"></i>积分管理</a>
|
||||||
<a class="ui button" href="/admin/posts">评论管理</a>
|
<a class="ui button" href="/admin/discussions"><i class="list alternate outline icon"></i>文章管理</a>
|
||||||
<a class="ui button" href="/swagger-ui.html" target="_blank">接口文档</a>
|
<a class="ui button" href="/admin/posts"><i class="comments outline icon"></i>评论管理</a>
|
||||||
|
<a class="ui button" href="/admin/audit-logs"><i class="shield alternate icon"></i>审计日志</a>
|
||||||
|
<a class="ui button" href="/swagger-ui.html" target="_blank"><i class="book icon"></i>接口文档</a>
|
||||||
</div>
|
</div>
|
||||||
</@shell.page>
|
</@shell.page>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
<#import "admin/admin-shell.ftl" as shell>
|
||||||
|
<@shell.page title="后台管理 - 积分管理" active=active adminName=adminName>
|
||||||
|
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:1.2em;">
|
||||||
|
<h2 class="ui header" style="margin:0;"><i class="trophy icon"></i>积分管理</h2>
|
||||||
|
<span style="color:#888; font-size:.9em;">共 ${users?size} 位用户</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<#assign lvlNames = ["新手","学徒","熟手","达人","专家","大神"]>
|
||||||
|
<#assign lvlColors = ["#95a5a6","#27ae60","#2980b9","#8e44ad","#e67e22","#e74c3c"]>
|
||||||
|
|
||||||
|
<div class="ui segments" style="margin-bottom:1.5em; background:#f8f9ff; border-radius:8px; padding:12px 16px;">
|
||||||
|
<div style="font-size:.9em; color:#555;">
|
||||||
|
<b>等级阈值:</b>
|
||||||
|
<#list 0..5 as i>
|
||||||
|
<span style="display:inline-block; margin-right:10px;">
|
||||||
|
<span style="display:inline-block; padding:1px 8px; border-radius:10px;
|
||||||
|
background:${lvlColors[i]}; color:#fff; font-size:.82em; font-weight:bold;">
|
||||||
|
Lv.${i} ${lvlNames[i]}
|
||||||
|
</span>
|
||||||
|
<span style="color:#888;">
|
||||||
|
<#if i==0><50<#elseif i==1>50–199<#elseif i==2>200–499
|
||||||
|
<#elseif i==3>500–999<#elseif i==4>1000–1999<#else>≥2000</#if>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</#list>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table class="ui celled striped table" style="font-size:.93em;">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style="width:50px;">ID</th>
|
||||||
|
<th>用户名</th>
|
||||||
|
<th>真实姓名</th>
|
||||||
|
<th style="width:90px; text-align:center;">等级</th>
|
||||||
|
<th style="width:80px; text-align:center;">积分</th>
|
||||||
|
<th style="width:70px; text-align:center;">发帖</th>
|
||||||
|
<th style="width:70px; text-align:center;">回复</th>
|
||||||
|
<th style="width:180px; text-align:center;">手动调整积分</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<#list users as u>
|
||||||
|
<#assign lvl = (u.level)!0>
|
||||||
|
<#assign lvlIdx = lvl?int>
|
||||||
|
<#if lvlIdx < 0><#assign lvlIdx = 0></#if>
|
||||||
|
<#if lvlIdx > 5><#assign lvlIdx = 5></#if>
|
||||||
|
<tr>
|
||||||
|
<td>${u.id}</td>
|
||||||
|
<td><a href="/profile?uid=${u.id}" target="_blank">${u.username!}</a></td>
|
||||||
|
<td style="color:#666;">${u.realname!'-'}</td>
|
||||||
|
<td style="text-align:center;">
|
||||||
|
<span style="display:inline-block; padding:2px 10px; border-radius:12px;
|
||||||
|
background:${lvlColors[lvlIdx]}; color:#fff; font-size:.82em; font-weight:bold;">
|
||||||
|
Lv.${lvl} ${lvlNames[lvlIdx]}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td style="text-align:center; font-weight:bold; color:#2185d0;">${(u.score)!0}</td>
|
||||||
|
<td style="text-align:center;">${(u.discussionsCount)!0}</td>
|
||||||
|
<td style="text-align:center;">${(u.commentsCount)!0}</td>
|
||||||
|
<td style="text-align:center;">
|
||||||
|
<form method="post" action="/admin/scores/${u.id}/adjust"
|
||||||
|
style="display:flex; align-items:center; gap:4px; justify-content:center;">
|
||||||
|
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
|
||||||
|
<input type="number" name="delta" value="0"
|
||||||
|
style="width:70px; text-align:center; border:1px solid #ddd; border-radius:4px; padding:3px 6px;"
|
||||||
|
min="-9999" max="9999">
|
||||||
|
<button type="submit" class="ui mini primary button" style="margin:0;">确定</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</#list>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</@shell.page>
|
||||||
@@ -29,20 +29,22 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="seven wide column">
|
<div class="seven wide column">
|
||||||
<h4 class="ui header">这里是页脚预留信息</h4>
|
<h4 class="ui header">关于 Jiscuss</h4>
|
||||||
<p>This is about Jiscuus Page
|
<p>一个简单、易用的 Java 开源论坛,基于 Spring Boot + Semantic UI 构建。</p>
|
||||||
.</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="ui section divider"></div>
|
<div class="ui section divider"></div>
|
||||||
<img src="static/assets/images/logo.png" class="ui centered mini image">
|
<img src="/static/assets/images/logo.png" class="ui centered mini image">
|
||||||
@ 2019-2020
|
© 2019-2026
|
||||||
<div class="ui horizontal small divided link list">
|
<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>
|
||||||
<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>
|
||||||
|
<div style="margin-top:8px; color:#aaa; font-size:.82em;">
|
||||||
|
Powered by <strong>Jiscuss V2.0</strong>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script>if(typeof NProgress !== 'undefined') NProgress.done();</script>
|
<script>if(typeof NProgress !== 'undefined') NProgress.done();</script>
|
||||||
|
|||||||
@@ -6,22 +6,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<a class=" item borderless" href="/">首页</a>
|
<a class=" item borderless" href="/">首页</a>
|
||||||
<a class="active borderless item">论坛</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="right menu">
|
||||||
<div class="item">
|
<div class="item">
|
||||||
<div class="ui transparent icon input">
|
<div class="ui transparent icon input" id="headerSearchBox">
|
||||||
<i class="search icon"></i>
|
<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>
|
</div>
|
||||||
<div class="item">
|
<div class="item">
|
||||||
@@ -44,17 +33,29 @@
|
|||||||
style="display:none; position:absolute; top:-4px; right:-8px; font-size:10px; padding:2px 4px;"></span>
|
style="display:none; position:absolute; top:-4px; right:-8px; font-size:10px; padding:2px 4px;"></span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</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>
|
</#if>
|
||||||
<div class="item" id="userlogin">
|
<div class="divider"></div>
|
||||||
<#if username??>
|
<a class="item" onclick="document.getElementById('logoutForm').submit();" style="cursor:pointer;">
|
||||||
<form class="ui large form" action="/logout" id="logoutForm" method="post" style="display: none;">
|
<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}"/>
|
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
|
||||||
</form>
|
</form>
|
||||||
${username} <a href="javascript:document.getElementById('logoutForm').submit();">注销</a>
|
|
||||||
<#else>
|
<#else>
|
||||||
<a href="/login" id="loginmodel">登录 & 注册</a>
|
<div class="item" id="userlogin">
|
||||||
</#if>
|
<a href="/login" id="loginmodel">登录 & 注册</a>
|
||||||
</div>
|
</div>
|
||||||
|
</#if>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -66,6 +67,8 @@
|
|||||||
var saved = localStorage.getItem('jiscuss-theme') || 'light';
|
var saved = localStorage.getItem('jiscuss-theme') || 'light';
|
||||||
if (saved === 'dark') { document.documentElement.setAttribute('data-theme', 'dark'); }
|
if (saved === 'dark') { document.documentElement.setAttribute('data-theme', 'dark'); }
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Initialize user dropdown
|
||||||
|
$('.ui.dropdown').dropdown();
|
||||||
var btn = document.getElementById('themeToggle');
|
var btn = document.getElementById('themeToggle');
|
||||||
var icon = document.getElementById('themeIcon');
|
var icon = document.getElementById('themeIcon');
|
||||||
function applyTheme(t) {
|
function applyTheme(t) {
|
||||||
|
|||||||
@@ -187,10 +187,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="widescreen large screen computer tablet only four wide column">
|
<div class="widescreen large screen computer tablet only four wide column">
|
||||||
<div class="ui fluid action input">
|
<form action="/search" method="get" class="ui fluid action input">
|
||||||
<input type="text" placeholder="搜索...">
|
<input type="text" name="q" placeholder="搜索主题..." id="sidebarSearch">
|
||||||
<div class="ui button">搜索</div>
|
<button type="submit" class="ui button">搜索</button>
|
||||||
</div>
|
</form>
|
||||||
|
|
||||||
<div class="ui section divider"></div>
|
<div class="ui section divider"></div>
|
||||||
|
|
||||||
@@ -199,90 +199,48 @@
|
|||||||
<div class="header">作者</div>
|
<div class="header">作者</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<h4 class="ui sub header">测试人员1</h4>
|
<h4 class="ui sub header">
|
||||||
<div class="ui small feed">
|
<a class="user-card-trigger" data-user-id="${discussions.startUserId!''}">${discussions.realname!''}</a>
|
||||||
<div class="event">
|
</h4>
|
||||||
<div class="content">
|
<div class="ui small statistics" style="margin-top:8px;">
|
||||||
<div class="summary">
|
<div class="statistic">
|
||||||
<a>Jiscuss</a> 一直, <a>Jiscuss</a> 一感谢支持!
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="event">
|
<#if authorTopDiscs?? && authorTopDiscs?size gt 0>
|
||||||
<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>
|
|
||||||
|
|
||||||
<div class="ui card">
|
<div class="ui card">
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<div class="header">作者热门主题</div>
|
<div class="header">作者其他主题</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="content">
|
<div class="content" style="padding:0;">
|
||||||
<div class="ui middle aligned divided list">
|
<div class="ui middle aligned divided list">
|
||||||
<div class="item">
|
<#list authorTopDiscs as d>
|
||||||
<img class="ui avatar image" src="/static/assets/images/logo.png">
|
<#if d.id != discussions.id>
|
||||||
|
<div class="item" style="padding:8px 12px;">
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<a class="header">its here</a>
|
<a class="header" href="/getdiscussionsbyid?id=${d.id}" style="font-size:.9em;font-weight:normal;">
|
||||||
</div>
|
${(d.title?length > 30)?then(d.title?substring(0,30) + '…', d.title)}
|
||||||
</div>
|
</a>
|
||||||
<div class="item">
|
<div class="description" style="font-size:.8em;color:#999;">
|
||||||
<img class="ui avatar image" src="/static/assets/images/logo.png">
|
<i class="heart icon"></i>${d.likeCount!0}
|
||||||
<div class="content">
|
<i class="comment icon"></i>${d.commentsCount!0}
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</#if>
|
||||||
<div class="extra content">
|
</#list>
|
||||||
<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>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</#if>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -329,11 +287,19 @@
|
|||||||
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');
|
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) {
|
function showCard(u, x, y) {
|
||||||
|
var lvlNames = ['新手','学徒','熟手','达人','专家','大神'];
|
||||||
|
var lvlColors = ['#95a5a6','#27ae60','#2980b9','#8e44ad','#e67e22','#e74c3c'];
|
||||||
|
var lvl = Math.min(5, Math.max(0, u.level || 0));
|
||||||
|
var badge = '<span style="display:inline-block;padding:1px 8px;border-radius:10px;background:' + lvlColors[lvl] + ';color:#fff;font-size:.78em;font-weight:bold;">Lv.' + lvl + ' ' + lvlNames[lvl] + '</span>';
|
||||||
$card.html(
|
$card.html(
|
||||||
'<div style="font-weight:700;font-size:1em;margin-bottom:4px;">' + (u.username || '') + '</div>' +
|
'<div style="font-weight:700;font-size:1em;margin-bottom:4px;">' + (u.username || '') + ' ' + badge + '</div>' +
|
||||||
'<div style="color:#888;font-size:.88em;">发帖: ' + (u.discussionsCount || 0) +
|
'<div style="color:#888;font-size:.88em;">发帖: ' + (u.discussionsCount || 0) +
|
||||||
' 回复: ' + (u.commentsCount || 0) + '</div>' +
|
' 回复: ' + (u.commentsCount || 0) +
|
||||||
'<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>'
|
' 积分: ' + (u.score || 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();
|
).css({ left: x + 14, top: y + 14 }).show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -333,8 +333,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="meta">
|
<div class="meta">
|
||||||
<div class=" description "><i class="edit icon"></i>
|
<div class=" description "><i class="edit icon"></i>
|
||||||
<a data-tooltip="${discussions.username}"
|
<a class="user-card-trigger" data-user-id="${discussions.startUserId!''}"><b>${discussions.username}</b></a> •
|
||||||
data-position="top center"><b>${discussions.username}</b></a> •
|
|
||||||
<i class="reply icon"></i>最后由 <a><b>${discussions.usernameLast}</b></a>
|
<i class="reply icon"></i>最后由 <a><b>${discussions.usernameLast}</b></a>
|
||||||
回复于${discussions.lastTime}.
|
回复于${discussions.lastTime}.
|
||||||
</div>
|
</div>
|
||||||
@@ -387,7 +386,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="meta">
|
<div class="meta">
|
||||||
<div class=" description "><i class="edit icon"></i>
|
<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>
|
<i class="reply icon"></i>最后由 <a><b>${discussions.usernameLast}</b></a>
|
||||||
回复于${discussions.lastTime}.
|
回复于${discussions.lastTime}.
|
||||||
</div>
|
</div>
|
||||||
@@ -439,7 +438,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="meta">
|
<div class="meta">
|
||||||
<div class=" description "><i class="edit icon"></i>
|
<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>
|
<i class="reply icon"></i>最后由 <a><b>${discussions.usernameLast}</b></a>
|
||||||
回复于${discussions.lastTime}.
|
回复于${discussions.lastTime}.
|
||||||
</div>
|
</div>
|
||||||
@@ -505,7 +504,7 @@
|
|||||||
<div class="date">${discussions.startTime}</div>
|
<div class="date">${discussions.startTime}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="meta">
|
<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="comment icon"></i>${discussions.commentsCount!0} 回复
|
||||||
<i class="eye icon"></i>${discussions.viewCount!0} 浏览
|
<i class="eye icon"></i>${discussions.viewCount!0} 浏览
|
||||||
</div>
|
</div>
|
||||||
@@ -556,6 +555,92 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script type="text/javascript" charset="UTF-8" src="/static/js/user/system.js"></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) {
|
||||||
|
var lvlNames = ['新手','学徒','熟手','达人','专家','大神'];
|
||||||
|
var lvlColors = ['#95a5a6','#27ae60','#2980b9','#8e44ad','#e67e22','#e74c3c'];
|
||||||
|
var lvl = Math.min(5, Math.max(0, u.level || 0));
|
||||||
|
var badge = '<span style="display:inline-block;padding:1px 8px;border-radius:10px;background:' + lvlColors[lvl] + ';color:#fff;font-size:.78em;font-weight:bold;">Lv.' + lvl + ' ' + lvlNames[lvl] + '</span>';
|
||||||
|
$card.html(
|
||||||
|
'<div style="font-weight:700;font-size:1em;margin-bottom:4px;">' + (u.username || '') + ' ' + badge + '</div>' +
|
||||||
|
'<div style="color:#888;font-size:.88em;">发帖: ' + (u.discussionsCount || 0) +
|
||||||
|
' 回复: ' + (u.commentsCount || 0) +
|
||||||
|
' 积分: ' + (u.score || 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>
|
</body>
|
||||||
|
|
||||||
</html>
|
</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 .number { font-size:1.6em; font-weight:bold; color:var(--color-primary,#2185d0); }
|
||||||
.profile-stat .label2 { color:#888; font-size:0.85em; }
|
.profile-stat .label2 { color:#888; font-size:0.85em; }
|
||||||
.tab-content { background:#fff; border-radius:8px; padding:1.2em; min-height:200px; }
|
.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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body style="background:#f7f8fa;">
|
<body style="background:#f7f8fa;">
|
||||||
@@ -39,6 +85,13 @@
|
|||||||
加入于 ${userEntity.joinTime?string("yyyy-MM-dd")}
|
加入于 ${userEntity.joinTime?string("yyyy-MM-dd")}
|
||||||
</div>
|
</div>
|
||||||
</#if>
|
</#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>
|
||||||
<div class="profile-card">
|
<div class="profile-card">
|
||||||
<div class="ui three column grid" style="text-align:center;">
|
<div class="ui three column grid" style="text-align:center;">
|
||||||
@@ -51,10 +104,19 @@
|
|||||||
<div class="label2">回复</div>
|
<div class="label2">回复</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="column profile-stat">
|
<div class="column profile-stat">
|
||||||
<div class="number">${(userEntity.level)!1}</div>
|
<div class="number">${(userEntity.score)!0}</div>
|
||||||
<div class="label2">等级</div>
|
<div class="label2">积分</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div style="text-align:center; margin-top:10px;">
|
||||||
|
<#assign lvl = (userEntity.level)!0>
|
||||||
|
<#assign lvlName = ["新手","学徒","熟手","达人","专家","大神"][lvl?int]>
|
||||||
|
<#assign lvlColors = ["#95a5a6","#27ae60","#2980b9","#8e44ad","#e67e22","#e74c3c"]>
|
||||||
|
<span style="display:inline-block; padding:3px 12px; border-radius:20px;
|
||||||
|
background:${lvlColors[lvl?int]}; color:#fff; font-size:.85em; font-weight:bold;">
|
||||||
|
Lv.${lvl} ${lvlName}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -148,5 +210,34 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<#include "comm/footer.ftl"/>
|
<#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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user