Inbox Message & bugs
This commit is contained in:
@@ -3,9 +3,11 @@ package com.yaoyuan.jiscuss;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.cache.annotation.EnableCaching;
|
||||
import org.springframework.scheduling.annotation.EnableAsync;
|
||||
|
||||
@SpringBootApplication
|
||||
@EnableCaching
|
||||
@EnableAsync
|
||||
public class JiccussApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
|
||||
@@ -1,15 +1,115 @@
|
||||
package com.yaoyuan.jiscuss.controller;
|
||||
|
||||
import com.yaoyuan.jiscuss.dto.InboxItem;
|
||||
import com.yaoyuan.jiscuss.entity.Message;
|
||||
import com.yaoyuan.jiscuss.entity.Notification;
|
||||
import com.yaoyuan.jiscuss.entity.User;
|
||||
import com.yaoyuan.jiscuss.entity.UserInfo;
|
||||
import com.yaoyuan.jiscuss.service.IMessageService;
|
||||
import com.yaoyuan.jiscuss.service.INotificationService;
|
||||
import com.yaoyuan.jiscuss.service.IUsersService;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.ModelMap;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author Chuyaoyuan
|
||||
* 用户消息控制器
|
||||
* 站内私信与通知控制器
|
||||
*/
|
||||
@Controller
|
||||
public class UserMsgController extends BaseController {
|
||||
|
||||
/**
|
||||
* 首页最新消息
|
||||
*/
|
||||
@Autowired
|
||||
private IMessageService messageService;
|
||||
|
||||
@Autowired
|
||||
private INotificationService notificationService;
|
||||
|
||||
@Autowired
|
||||
private IUsersService usersService;
|
||||
|
||||
/** 收件箱:对话列表 */
|
||||
@GetMapping("/messages")
|
||||
public String inbox(HttpServletRequest request, ModelMap map) {
|
||||
UserInfo user = getUserInfo(request);
|
||||
if (user == null) return "redirect:/login";
|
||||
|
||||
List<Message> inboxList = messageService.getInbox(user.getId());
|
||||
|
||||
List<InboxItem> items = new ArrayList<>();
|
||||
for (Message msg : inboxList) {
|
||||
Integer otherId = msg.getSenderId().equals(user.getId()) ? msg.getReceiverId() : msg.getSenderId();
|
||||
User other = usersService.findOne(otherId);
|
||||
String rawName = (other != null) ? other.getUsername() : null;
|
||||
String otherName = (rawName != null && !rawName.isBlank()) ? rawName : "用户" + otherId;
|
||||
boolean hasUnread = !Boolean.TRUE.equals(msg.getIsRead()) && msg.getReceiverId().equals(user.getId());
|
||||
items.add(new InboxItem(otherId, otherName, msg.getContent(), msg.getCreateTime(), hasUnread));
|
||||
}
|
||||
|
||||
map.put("username", user.getUsername());
|
||||
map.put("currentUserId", user.getId());
|
||||
map.put("inboxItems", items);
|
||||
map.put("unreadMsgCount", messageService.countUnread(user.getId()));
|
||||
map.put("unreadNotifCount", notificationService.countUnread(user.getId()));
|
||||
return "messages";
|
||||
}
|
||||
|
||||
/** 对话详情页 */
|
||||
@GetMapping("/messages/{otherId}")
|
||||
public String conversation(@PathVariable Integer otherId, HttpServletRequest request, ModelMap map) {
|
||||
UserInfo user = getUserInfo(request);
|
||||
if (user == null) return "redirect:/login";
|
||||
|
||||
messageService.markConversationRead(user.getId(), otherId);
|
||||
|
||||
List<Message> messages = messageService.getConversation(user.getId(), otherId);
|
||||
User other = usersService.findOne(otherId);
|
||||
|
||||
map.put("username", user.getUsername());
|
||||
map.put("currentUserId", user.getId());
|
||||
map.put("otherId", otherId);
|
||||
map.put("otherUsername", other != null ? other.getUsername() : "用户" + otherId);
|
||||
map.put("messages", messages);
|
||||
map.put("unreadMsgCount", messageService.countUnread(user.getId()));
|
||||
map.put("unreadNotifCount", notificationService.countUnread(user.getId()));
|
||||
return "conversation";
|
||||
}
|
||||
|
||||
/** 通知列表页 */
|
||||
@GetMapping("/notifications")
|
||||
public String notifications(@RequestParam(defaultValue = "0") int page,
|
||||
HttpServletRequest request, ModelMap map) {
|
||||
UserInfo user = getUserInfo(request);
|
||||
if (user == null) return "redirect:/login";
|
||||
|
||||
notificationService.markAllRead(user.getId());
|
||||
Page<Notification> notifs = notificationService.listByUser(user.getId(), page, 20);
|
||||
|
||||
List<String> fromUsernames = new ArrayList<>();
|
||||
for (Notification n : notifs.getContent()) {
|
||||
if (n.getFromUserId() != null) {
|
||||
User from = usersService.findOne(n.getFromUserId());
|
||||
fromUsernames.add(from != null ? from.getUsername() : "用户" + n.getFromUserId());
|
||||
} else {
|
||||
fromUsernames.add("系统");
|
||||
}
|
||||
}
|
||||
|
||||
map.put("username", user.getUsername());
|
||||
map.put("notifications", notifs.getContent());
|
||||
map.put("fromUsernames", fromUsernames);
|
||||
map.put("totalPages", notifs.getTotalPages());
|
||||
map.put("currentPage", page);
|
||||
map.put("unreadMsgCount", messageService.countUnread(user.getId()));
|
||||
map.put("unreadNotifCount", 0L);
|
||||
return "notifications";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,68 +1,105 @@
|
||||
package com.yaoyuan.jiscuss.controller;
|
||||
|
||||
import com.yaoyuan.jiscuss.entity.Discussion;
|
||||
import com.yaoyuan.jiscuss.entity.Post;
|
||||
import com.yaoyuan.jiscuss.entity.User;
|
||||
import com.yaoyuan.jiscuss.entity.UserInfo;
|
||||
import com.yaoyuan.jiscuss.service.IDiscussionsService;
|
||||
import com.yaoyuan.jiscuss.service.ITagsService;
|
||||
import com.yaoyuan.jiscuss.repository.DiscussionsRepository;
|
||||
import com.yaoyuan.jiscuss.repository.LikeCollectRepository;
|
||||
import com.yaoyuan.jiscuss.repository.PostsRepository;
|
||||
import com.yaoyuan.jiscuss.service.IMessageService;
|
||||
import com.yaoyuan.jiscuss.service.INotificationService;
|
||||
import com.yaoyuan.jiscuss.service.IUsersService;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
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.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author Chuyaoyuan
|
||||
* @Title:
|
||||
* @Package com.yaoyuan.jiscuss.controller
|
||||
* @Description:
|
||||
* @date
|
||||
*/
|
||||
@Controller
|
||||
public class UserPageController extends BaseController {
|
||||
|
||||
private static Logger logger = LoggerFactory.getLogger(UserPageController.class);
|
||||
private static final Logger logger = LoggerFactory.getLogger(UserPageController.class);
|
||||
|
||||
@Autowired
|
||||
private IUsersService usersService;
|
||||
|
||||
@Autowired
|
||||
private IDiscussionsService discussionsService;
|
||||
private DiscussionsRepository discussionsRepository;
|
||||
|
||||
@Autowired
|
||||
private ITagsService tagsService;
|
||||
private PostsRepository postsRepository;
|
||||
|
||||
@Autowired
|
||||
private LikeCollectRepository likeCollectRepository;
|
||||
|
||||
@Autowired
|
||||
private IMessageService messageService;
|
||||
|
||||
@Autowired
|
||||
private INotificationService notificationService;
|
||||
|
||||
/**
|
||||
* 用户页面
|
||||
*
|
||||
* @return
|
||||
* 用户个人主页
|
||||
*/
|
||||
@RequestMapping("/user")
|
||||
public String user(@RequestParam(defaultValue = "discussion") String type, HttpServletRequest request, ModelMap map) {
|
||||
public String user(@RequestParam(defaultValue = "discussion") String type,
|
||||
HttpServletRequest request, ModelMap map) {
|
||||
|
||||
// type 判断
|
||||
if (type.equals("discussion")) {
|
||||
map.put("discussion", "active");
|
||||
UserInfo currentUser = getUserInfo(request);
|
||||
if (currentUser == null) return "redirect:/login";
|
||||
|
||||
User userEntity = usersService.findOne(currentUser.getId());
|
||||
long liveDiscCount = discussionsRepository.countByStartUserId(currentUser.getId());
|
||||
long liveReplyCount = postsRepository.countByCreateId(currentUser.getId());
|
||||
map.put("username", currentUser.getUsername());
|
||||
map.put("userEntity", userEntity);
|
||||
map.put("liveDiscCount", liveDiscCount);
|
||||
map.put("liveReplyCount", liveReplyCount);
|
||||
map.put("type", type);
|
||||
map.put("unreadMsgCount", messageService.countUnread(currentUser.getId()));
|
||||
map.put("unreadNotifCount", notificationService.countUnread(currentUser.getId()));
|
||||
|
||||
if ("discussion".equals(type)) {
|
||||
Page<Discussion> discussions = discussionsRepository.findByStartUserIdOrderByStartTimeDesc(
|
||||
currentUser.getId(), PageRequest.of(0, 20));
|
||||
map.put("discussions", discussions.getContent());
|
||||
} else if ("reply".equals(type)) {
|
||||
Page<Post> posts = postsRepository.findByCreateIdOrderByCreateTimeDesc(
|
||||
currentUser.getId(), PageRequest.of(0, 20));
|
||||
map.put("posts", posts.getContent());
|
||||
// Attach discussion titles
|
||||
List<String> discTitles = new ArrayList<>();
|
||||
for (Post p : posts.getContent()) {
|
||||
if (p.getDiscussionId() != null) {
|
||||
Discussion d = discussionsRepository.findById(p.getDiscussionId()).orElse(null);
|
||||
discTitles.add(d != null ? d.getTitle() : "");
|
||||
} else {
|
||||
discTitles.add("");
|
||||
}
|
||||
|
||||
if (type.equals("change")) {
|
||||
map.put("change", "active");
|
||||
}
|
||||
|
||||
if (type.equals("like")) {
|
||||
map.put("like", "active");
|
||||
map.put("discTitles", discTitles);
|
||||
} else if ("like".equals(type)) {
|
||||
List<Integer> likedIds = likeCollectRepository.findLikedDiscussionIdsByUser(currentUser.getId());
|
||||
List<Discussion> liked = new ArrayList<>();
|
||||
for (Integer id : likedIds) {
|
||||
Discussion d = discussionsRepository.findById(id).orElse(null);
|
||||
if (d != null) liked.add(d);
|
||||
}
|
||||
|
||||
UserInfo user = getUserInfo(request);
|
||||
if (user != null) {
|
||||
map.put("username", user.getUsername());
|
||||
map.put("data", "Jiscuss 用户:" + user.getUsername());
|
||||
map.put("likedDiscussions", liked);
|
||||
}
|
||||
|
||||
|
||||
return "user";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +62,9 @@ public class UserPostController extends BaseController {
|
||||
@Autowired
|
||||
private IpRegionService ipRegionService;
|
||||
|
||||
@Autowired
|
||||
private com.yaoyuan.jiscuss.service.INotificationService notificationService;
|
||||
|
||||
|
||||
/**
|
||||
* 首页查看主题列表(各种条件筛选,最热最新标签等)
|
||||
@@ -205,6 +208,14 @@ public class UserPostController extends BaseController {
|
||||
}
|
||||
|
||||
Post savePost = postsService.insert(post);
|
||||
// Notify the discussion starter about a reply (async, ignores self-reply)
|
||||
if (post.getDiscussionId() != null && user != null) {
|
||||
Discussion disc = discussionsService.findOne(post.getDiscussionId());
|
||||
if (disc != null && disc.getStartUserId() != null) {
|
||||
notificationService.notifyReply(disc.getStartUserId(), user.getId(),
|
||||
disc.getId(), disc.getTitle());
|
||||
}
|
||||
}
|
||||
Map<String, Object> resultobj = new HashMap<>();
|
||||
logger.info(">>>{}", savePost);
|
||||
resultobj.put("msg", "添加评论成功");
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
package com.yaoyuan.jiscuss.controller.api;
|
||||
|
||||
import com.yaoyuan.jiscuss.controller.BaseController;
|
||||
import com.yaoyuan.jiscuss.entity.Message;
|
||||
import com.yaoyuan.jiscuss.entity.Notification;
|
||||
import com.yaoyuan.jiscuss.entity.User;
|
||||
import com.yaoyuan.jiscuss.entity.UserInfo;
|
||||
import com.yaoyuan.jiscuss.response.ApiResponse;
|
||||
import com.yaoyuan.jiscuss.service.IMessageService;
|
||||
import com.yaoyuan.jiscuss.service.INotificationService;
|
||||
import com.yaoyuan.jiscuss.service.IUsersService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* REST API for private messages and notifications.
|
||||
*/
|
||||
@Tag(name = "Message API", description = "Private messages and notifications")
|
||||
@RestController
|
||||
@RequestMapping("/msg_api")
|
||||
public class RestMsgController extends BaseController {
|
||||
|
||||
@Autowired
|
||||
private IMessageService messageService;
|
||||
|
||||
@Autowired
|
||||
private INotificationService notificationService;
|
||||
|
||||
@Autowired
|
||||
private IUsersService usersService;
|
||||
|
||||
// ─── Debug (TEMP) ─────────────────────────────────────────────────────
|
||||
|
||||
/** Temp debug: returns raw message counts and inbox list for current user. */
|
||||
@GetMapping("/debug/inbox")
|
||||
public ApiResponse<Map<String, Object>> debugInbox(HttpServletRequest request) {
|
||||
UserInfo user = getUserInfo(request);
|
||||
if (user == null) return ApiResponse.fail(401, "请先登录");
|
||||
long unread = messageService.countUnread(user.getId());
|
||||
List<Message> inbox = messageService.getInbox(user.getId());
|
||||
return ApiResponse.ok(Map.of(
|
||||
"userId", user.getId(),
|
||||
"username", user.getUsername(),
|
||||
"unreadCount", unread,
|
||||
"inboxSize", inbox.size(),
|
||||
"inbox", inbox
|
||||
));
|
||||
}
|
||||
|
||||
// ─── User card (public — accessible to any authenticated user) ────────
|
||||
|
||||
/** Returns basic user info for the hover card (discussionsCount, commentsCount). */
|
||||
@Operation(summary = "Get user card info")
|
||||
@GetMapping("/user-card/{id}")
|
||||
public ApiResponse<Map<String, Object>> getUserCard(@PathVariable Integer id, HttpServletRequest request) {
|
||||
UserInfo currentUser = getUserInfo(request);
|
||||
if (currentUser == null) return ApiResponse.fail(401, "请先登录");
|
||||
User user = usersService.findOne(id);
|
||||
if (user == null) return ApiResponse.fail(404, "用户不存在");
|
||||
return ApiResponse.ok(Map.of(
|
||||
"id", user.getId(),
|
||||
"username", user.getUsername() != null ? user.getUsername() : "",
|
||||
"discussionsCount", user.getDiscussionsCount() != null ? user.getDiscussionsCount() : 0,
|
||||
"commentsCount", user.getCommentsCount() != null ? user.getCommentsCount() : 0
|
||||
));
|
||||
}
|
||||
|
||||
// ─── Notifications ────────────────────────────────────────────────────
|
||||
|
||||
@Operation(summary = "Get unread notification + message counts")
|
||||
@GetMapping("/unread-counts")
|
||||
public ApiResponse<Map<String, Long>> unreadCounts(HttpServletRequest request) {
|
||||
UserInfo user = getUserInfo(request);
|
||||
if (user == null) return ApiResponse.ok(Map.of("notifications", 0L, "messages", 0L));
|
||||
return ApiResponse.ok(Map.of(
|
||||
"notifications", notificationService.countUnread(user.getId()),
|
||||
"messages", messageService.countUnread(user.getId())
|
||||
));
|
||||
}
|
||||
|
||||
@Operation(summary = "Mark a notification as read")
|
||||
@PostMapping("/notifications/{id}/read")
|
||||
public ApiResponse<Void> markNotifRead(@PathVariable Integer id, HttpServletRequest request) {
|
||||
UserInfo user = getUserInfo(request);
|
||||
if (user == null) return ApiResponse.fail(401, "请先登录");
|
||||
notificationService.markOneRead(id, user.getId());
|
||||
return ApiResponse.ok(null);
|
||||
}
|
||||
|
||||
@Operation(summary = "Mark all notifications as read")
|
||||
@PostMapping("/notifications/read-all")
|
||||
public ApiResponse<Void> markAllNotifRead(HttpServletRequest request) {
|
||||
UserInfo user = getUserInfo(request);
|
||||
if (user == null) return ApiResponse.fail(401, "请先登录");
|
||||
notificationService.markAllRead(user.getId());
|
||||
return ApiResponse.ok(null);
|
||||
}
|
||||
|
||||
// ─── Messages ─────────────────────────────────────────────────────────
|
||||
|
||||
@Operation(summary = "Send a private message")
|
||||
@PostMapping("/messages/send")
|
||||
public ApiResponse<Message> sendMessage(
|
||||
@RequestParam Integer receiverId,
|
||||
@RequestParam String content,
|
||||
HttpServletRequest request) {
|
||||
UserInfo user = getUserInfo(request);
|
||||
if (user == null) return ApiResponse.fail(401, "请先登录");
|
||||
if (user.getId().equals(receiverId)) return ApiResponse.fail(400, "不能给自己发消息");
|
||||
if (content == null || content.isBlank()) return ApiResponse.fail(400, "消息内容不能为空");
|
||||
Message msg = messageService.send(user.getId(), receiverId, content.trim());
|
||||
return ApiResponse.ok(msg);
|
||||
}
|
||||
|
||||
@Operation(summary = "Get conversation with another user")
|
||||
@GetMapping("/messages/conversation/{otherId}")
|
||||
public ApiResponse<List<Message>> getConversation(@PathVariable Integer otherId, HttpServletRequest request) {
|
||||
UserInfo user = getUserInfo(request);
|
||||
if (user == null) return ApiResponse.fail(401, "请先登录");
|
||||
messageService.markConversationRead(user.getId(), otherId);
|
||||
return ApiResponse.ok(messageService.getConversation(user.getId(), otherId));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.yaoyuan.jiscuss.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/** View model for one inbox entry (latest message in a conversation). */
|
||||
@Data
|
||||
public class InboxItem {
|
||||
private final Integer otherId;
|
||||
private final String otherUsername;
|
||||
private final String lastContent;
|
||||
private final Date lastTime;
|
||||
private final boolean hasUnread;
|
||||
|
||||
public InboxItem(Integer otherId, String otherUsername, String lastContent, Date lastTime, boolean hasUnread) {
|
||||
this.otherId = otherId;
|
||||
this.otherUsername = otherUsername;
|
||||
this.lastContent = lastContent;
|
||||
this.lastTime = lastTime;
|
||||
this.hasUnread = hasUnread;
|
||||
}
|
||||
}
|
||||
@@ -82,6 +82,6 @@ public class Discussion implements Serializable {
|
||||
private Date createTime;
|
||||
|
||||
@Column(name = "view_count")
|
||||
private Integer viewCount = 0;
|
||||
private Integer viewCount;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.yaoyuan.jiscuss.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
@Data
|
||||
@Entity
|
||||
@Table(name = "message")
|
||||
public class Message implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Integer id;
|
||||
|
||||
@Column(name = "sender_id", nullable = false)
|
||||
private Integer senderId;
|
||||
|
||||
@Column(name = "receiver_id", nullable = false)
|
||||
private Integer receiverId;
|
||||
|
||||
@Column(name = "content", columnDefinition = "TEXT", nullable = false)
|
||||
private String content;
|
||||
|
||||
@Column(name = "is_read")
|
||||
private Boolean isRead = false;
|
||||
|
||||
@Column(name = "create_time")
|
||||
private Date createTime;
|
||||
|
||||
/**
|
||||
* Conversation identifier = "min(a,b)_max(a,b)", groups all messages
|
||||
* between two users into a single conversation thread.
|
||||
*/
|
||||
@Column(name = "conversation_id", nullable = false)
|
||||
private String conversationId;
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.yaoyuan.jiscuss.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
@Data
|
||||
@Entity
|
||||
@Table(name = "notification")
|
||||
public class Notification implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Integer id;
|
||||
|
||||
/** Receiver user ID. */
|
||||
@Column(name = "user_id", nullable = false)
|
||||
private Integer userId;
|
||||
|
||||
/** Sender user ID (null for system notifications). */
|
||||
@Column(name = "from_user_id")
|
||||
private Integer fromUserId;
|
||||
|
||||
/** Notification type: reply / like / system. */
|
||||
@Column(name = "type", nullable = false)
|
||||
private String type;
|
||||
|
||||
@Column(name = "title")
|
||||
private String title;
|
||||
|
||||
@Column(name = "content", columnDefinition = "TEXT")
|
||||
private String content;
|
||||
|
||||
/** Related entity ID (e.g., discussion or post id). */
|
||||
@Column(name = "related_id")
|
||||
private Integer relatedId;
|
||||
|
||||
/** Type of related entity: discussion / post. */
|
||||
@Column(name = "related_type")
|
||||
private String relatedType;
|
||||
|
||||
@Column(name = "is_read")
|
||||
private Boolean isRead = false;
|
||||
|
||||
@Column(name = "create_time")
|
||||
private Date createTime;
|
||||
}
|
||||
@@ -42,4 +42,11 @@ public interface DiscussionsRepository extends JpaRepository<Discussion, Integer
|
||||
@Query("UPDATE Discussion d SET d.dislikeCount = COALESCE(d.dislikeCount, 0) + :delta WHERE d.id = :id")
|
||||
void adjustDislikeCount(@org.springframework.data.repository.query.Param("id") Integer id,
|
||||
@org.springframework.data.repository.query.Param("delta") int delta);
|
||||
|
||||
/** Discussions started by a given user, most recent first. */
|
||||
@Query("SELECT d FROM Discussion d WHERE d.startUserId = :userId ORDER BY d.startTime DESC")
|
||||
org.springframework.data.domain.Page<Discussion> findByStartUserIdOrderByStartTimeDesc(
|
||||
@org.springframework.data.repository.query.Param("userId") Integer userId,
|
||||
org.springframework.data.domain.Pageable pageable);
|
||||
long countByStartUserId(Integer startUserId);
|
||||
}
|
||||
|
||||
@@ -18,4 +18,8 @@ public interface LikeCollectRepository extends JpaRepository<LikeCollect, Intege
|
||||
/** Find an existing vote record for a user on a post. */
|
||||
@Query("FROM LikeCollect lc WHERE lc.userId = :userId AND lc.postId = :postId AND lc.type = 'post'")
|
||||
Optional<LikeCollect> findPostVote(@Param("userId") Integer userId, @Param("postId") Integer postId);
|
||||
|
||||
/** Discussions liked by a user (action = 'like'). */
|
||||
@Query("SELECT lc.discussionId FROM LikeCollect lc WHERE lc.userId = :userId AND lc.type = 'discussion' AND lc.action = 'like'")
|
||||
java.util.List<Integer> findLikedDiscussionIdsByUser(@Param("userId") Integer userId);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.yaoyuan.jiscuss.repository;
|
||||
|
||||
import com.yaoyuan.jiscuss.entity.Message;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Modifying;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Repository
|
||||
public interface MessageRepository extends JpaRepository<Message, Integer> {
|
||||
|
||||
/** Latest messages in a conversation, most recent first. */
|
||||
List<Message> findByConversationIdOrderByCreateTimeDesc(String conversationId);
|
||||
|
||||
/** All messages sent by user. */
|
||||
List<Message> findBySenderIdOrderByCreateTimeDesc(Integer senderId);
|
||||
|
||||
/** All messages received by user. */
|
||||
List<Message> findByReceiverIdOrderByCreateTimeDesc(Integer receiverId);
|
||||
|
||||
long countByReceiverIdAndIsReadFalse(Integer receiverId);
|
||||
|
||||
@Modifying
|
||||
@Transactional
|
||||
@Query("UPDATE Message m SET m.isRead = true WHERE m.conversationId = :convId AND m.receiverId = :userId")
|
||||
void markConversationReadForUser(@Param("convId") String convId, @Param("userId") Integer userId);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.yaoyuan.jiscuss.repository;
|
||||
|
||||
import com.yaoyuan.jiscuss.entity.Notification;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Modifying;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Repository
|
||||
public interface NotificationRepository extends JpaRepository<Notification, Integer> {
|
||||
|
||||
Page<Notification> findByUserIdOrderByCreateTimeDesc(Integer userId, Pageable pageable);
|
||||
|
||||
long countByUserIdAndIsReadFalse(Integer userId);
|
||||
|
||||
@Modifying
|
||||
@Transactional
|
||||
@Query("UPDATE Notification n SET n.isRead = true WHERE n.userId = :userId")
|
||||
void markAllReadByUserId(@Param("userId") Integer userId);
|
||||
|
||||
@Modifying
|
||||
@Transactional
|
||||
@Query("UPDATE Notification n SET n.isRead = true WHERE n.id = :id AND n.userId = :userId")
|
||||
void markReadById(@Param("id") Integer id, @Param("userId") Integer userId);
|
||||
}
|
||||
@@ -89,4 +89,11 @@ public interface PostsRepository extends JpaRepository<Post, Integer> {
|
||||
//
|
||||
// @Query("from Post where parentId is not null and discussionId = :dId")
|
||||
// List<Post> findAllByDIdAndparentIdNotNull(@Param("dId")Integer dId);
|
||||
|
||||
/** Posts (replies) created by a given user, most recent first. */
|
||||
@Query("SELECT p FROM Post p WHERE p.createId = :userId ORDER BY p.createTime DESC")
|
||||
org.springframework.data.domain.Page<Post> findByCreateIdOrderByCreateTimeDesc(
|
||||
@Param("userId") Integer userId, org.springframework.data.domain.Pageable pageable);
|
||||
|
||||
long countByCreateId(Integer createId);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.yaoyuan.jiscuss.service;
|
||||
|
||||
import com.yaoyuan.jiscuss.entity.Message;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface IMessageService {
|
||||
|
||||
/** Send a message from sender to receiver. */
|
||||
Message send(Integer senderId, Integer receiverId, String content);
|
||||
|
||||
/** All messages in a conversation (ordered oldest→newest). */
|
||||
List<Message> getConversation(Integer userId, Integer otherId);
|
||||
|
||||
/** Inbox: latest message per conversation, sorted by latest activity (max 50). */
|
||||
List<Message> getInbox(Integer userId);
|
||||
|
||||
/** Unread message count for a user. */
|
||||
long countUnread(Integer userId);
|
||||
|
||||
/** Mark an entire conversation as read for the current user. */
|
||||
void markConversationRead(Integer userId, Integer otherId);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.yaoyuan.jiscuss.service;
|
||||
|
||||
import com.yaoyuan.jiscuss.entity.Notification;
|
||||
import org.springframework.data.domain.Page;
|
||||
|
||||
public interface INotificationService {
|
||||
|
||||
/** Create a notification for a user. */
|
||||
Notification create(Integer toUserId, Integer fromUserId, String type, String title, String content,
|
||||
Integer relatedId, String relatedType);
|
||||
|
||||
/** Convenience: create a "reply" notification. */
|
||||
void notifyReply(Integer toUserId, Integer fromUserId, Integer discussionId, String discussionTitle);
|
||||
|
||||
/** Convenience: create a "like" notification for a post. */
|
||||
void notifyLike(Integer toUserId, Integer fromUserId, Integer postId, Integer discussionId);
|
||||
|
||||
/** Unread notification count for a user. */
|
||||
long countUnread(Integer userId);
|
||||
|
||||
/** Paged notification list for a user. */
|
||||
Page<Notification> listByUser(Integer userId, int page, int size);
|
||||
|
||||
void markAllRead(Integer userId);
|
||||
|
||||
void markOneRead(Integer notificationId, Integer userId);
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
package com.yaoyuan.jiscuss.service.impl;
|
||||
|
||||
import com.yaoyuan.jiscuss.entity.LikeCollect;
|
||||
import com.yaoyuan.jiscuss.entity.Post;
|
||||
import com.yaoyuan.jiscuss.repository.DiscussionsRepository;
|
||||
import com.yaoyuan.jiscuss.repository.LikeCollectRepository;
|
||||
import com.yaoyuan.jiscuss.repository.PostsRepository;
|
||||
import com.yaoyuan.jiscuss.service.ILikeCollectService;
|
||||
import com.yaoyuan.jiscuss.service.INotificationService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
@@ -24,6 +26,9 @@ public class LikeCollectServiceImpl implements ILikeCollectService {
|
||||
@Autowired
|
||||
private PostsRepository postsRepository;
|
||||
|
||||
@Autowired
|
||||
private INotificationService notificationService;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public String voteDiscussion(Integer userId, Integer discussionId, String action) {
|
||||
@@ -85,6 +90,13 @@ public class LikeCollectServiceImpl implements ILikeCollectService {
|
||||
vote.setCreateTime(new Date());
|
||||
likeCollectRepository.save(vote);
|
||||
adjustPost(postId, action, +1);
|
||||
// Notify post author when liked (not disliked)
|
||||
if ("like".equals(action)) {
|
||||
Post post = postsRepository.findById(postId).orElse(null);
|
||||
if (post != null && post.getCreateId() != null) {
|
||||
notificationService.notifyLike(post.getCreateId(), userId, postId, post.getDiscussionId());
|
||||
}
|
||||
}
|
||||
return action;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
package com.yaoyuan.jiscuss.service.impl;
|
||||
|
||||
import com.yaoyuan.jiscuss.entity.Message;
|
||||
import com.yaoyuan.jiscuss.repository.MessageRepository;
|
||||
import com.yaoyuan.jiscuss.service.IMessageService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
public class MessageServiceImpl implements IMessageService {
|
||||
|
||||
@Autowired
|
||||
private MessageRepository messageRepository;
|
||||
|
||||
private String buildConversationId(Integer a, Integer b) {
|
||||
return Math.min(a, b) + "_" + Math.max(a, b);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public Message send(Integer senderId, Integer receiverId, String content) {
|
||||
Message msg = new Message();
|
||||
msg.setSenderId(senderId);
|
||||
msg.setReceiverId(receiverId);
|
||||
msg.setContent(content);
|
||||
msg.setIsRead(false);
|
||||
msg.setCreateTime(new Date());
|
||||
msg.setConversationId(buildConversationId(senderId, receiverId));
|
||||
return messageRepository.save(msg);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
@Override
|
||||
public List<Message> getConversation(Integer userId, Integer otherId) {
|
||||
String convId = buildConversationId(userId, otherId);
|
||||
List<Message> msgs = messageRepository.findByConversationIdOrderByCreateTimeDesc(convId);
|
||||
// reverse to get oldest-first order
|
||||
java.util.Collections.reverse(msgs);
|
||||
return msgs;
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
@Override
|
||||
public List<Message> getInbox(Integer userId) {
|
||||
// Fetch sent and received separately, merge, then deduplicate by conversationId
|
||||
List<Message> sent = messageRepository.findBySenderIdOrderByCreateTimeDesc(userId);
|
||||
List<Message> received = messageRepository.findByReceiverIdOrderByCreateTimeDesc(userId);
|
||||
|
||||
// Merge both lists, sort by createTime descending
|
||||
java.util.List<Message> all = new java.util.ArrayList<>();
|
||||
all.addAll(sent);
|
||||
all.addAll(received);
|
||||
all.sort((a, b) -> {
|
||||
if (a.getCreateTime() == null) return 1;
|
||||
if (b.getCreateTime() == null) return -1;
|
||||
return b.getCreateTime().compareTo(a.getCreateTime());
|
||||
});
|
||||
|
||||
// Keep only the latest message per conversation
|
||||
java.util.LinkedHashMap<String, Message> seen = new java.util.LinkedHashMap<>();
|
||||
for (Message m : all) {
|
||||
String cid = (m.getConversationId() != null && !m.getConversationId().isEmpty())
|
||||
? m.getConversationId()
|
||||
: Math.min(m.getSenderId(), m.getReceiverId()) + "_" + Math.max(m.getSenderId(), m.getReceiverId());
|
||||
seen.putIfAbsent(cid, m);
|
||||
}
|
||||
return new java.util.ArrayList<>(seen.values());
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
@Override
|
||||
public long countUnread(Integer userId) {
|
||||
return messageRepository.countByReceiverIdAndIsReadFalse(userId);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public void markConversationRead(Integer userId, Integer otherId) {
|
||||
messageRepository.markConversationReadForUser(buildConversationId(userId, otherId), userId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package com.yaoyuan.jiscuss.service.impl;
|
||||
|
||||
import com.yaoyuan.jiscuss.entity.Notification;
|
||||
import com.yaoyuan.jiscuss.repository.NotificationRepository;
|
||||
import com.yaoyuan.jiscuss.service.INotificationService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
@Service
|
||||
public class NotificationServiceImpl implements INotificationService {
|
||||
|
||||
@Autowired
|
||||
private NotificationRepository notificationRepository;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public Notification create(Integer toUserId, Integer fromUserId, String type, String title,
|
||||
String content, Integer relatedId, String relatedType) {
|
||||
// Do not notify yourself
|
||||
if (toUserId != null && toUserId.equals(fromUserId)) {
|
||||
return null;
|
||||
}
|
||||
Notification n = new Notification();
|
||||
n.setUserId(toUserId);
|
||||
n.setFromUserId(fromUserId);
|
||||
n.setType(type);
|
||||
n.setTitle(title);
|
||||
n.setContent(content);
|
||||
n.setRelatedId(relatedId);
|
||||
n.setRelatedType(relatedType);
|
||||
n.setIsRead(false);
|
||||
n.setCreateTime(new Date());
|
||||
return notificationRepository.save(n);
|
||||
}
|
||||
|
||||
@Async
|
||||
@Override
|
||||
public void notifyReply(Integer toUserId, Integer fromUserId, Integer discussionId, String discussionTitle) {
|
||||
create(toUserId, fromUserId, "reply",
|
||||
"有人回复了你的帖子",
|
||||
"在话题《" + discussionTitle + "》中回复了你",
|
||||
discussionId, "discussion");
|
||||
}
|
||||
|
||||
@Async
|
||||
@Override
|
||||
public void notifyLike(Integer toUserId, Integer fromUserId, Integer postId, Integer discussionId) {
|
||||
create(toUserId, fromUserId, "like",
|
||||
"有人赞了你的回复",
|
||||
null, postId, "post");
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
@Override
|
||||
public long countUnread(Integer userId) {
|
||||
return notificationRepository.countByUserIdAndIsReadFalse(userId);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
@Override
|
||||
public Page<Notification> listByUser(Integer userId, int page, int size) {
|
||||
return notificationRepository.findByUserIdOrderByCreateTimeDesc(
|
||||
userId, PageRequest.of(page, size));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public void markAllRead(Integer userId) {
|
||||
notificationRepository.markAllReadByUserId(userId);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public void markOneRead(Integer notificationId, Integer userId) {
|
||||
notificationRepository.markReadById(notificationId, userId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
-- V9: Add notification and private message support
|
||||
|
||||
-- Notification table: system/reply/like alerts to a user
|
||||
CREATE TABLE IF NOT EXISTS notification (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
user_id INT NOT NULL, -- receiver
|
||||
from_user_id INT, -- sender (NULL for system notifications)
|
||||
type VARCHAR(50) NOT NULL, -- 'reply', 'like', 'system'
|
||||
title VARCHAR(200),
|
||||
content TEXT,
|
||||
related_id INT, -- e.g. discussion_id or post_id
|
||||
related_type VARCHAR(50), -- 'discussion', 'post'
|
||||
is_read BOOLEAN DEFAULT FALSE,
|
||||
create_time DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_notification_user ON notification(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_notification_user_unread ON notification(user_id, is_read);
|
||||
|
||||
-- Message table: private messages between users
|
||||
CREATE TABLE IF NOT EXISTS message (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
sender_id INT NOT NULL,
|
||||
receiver_id INT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
is_read BOOLEAN DEFAULT FALSE,
|
||||
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
conversation_id VARCHAR(50) NOT NULL -- format: min_max of user IDs
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_message_receiver ON message(receiver_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_message_conversation ON message(conversation_id, create_time);
|
||||
CREATE INDEX IF NOT EXISTS idx_message_receiver_unread ON message(receiver_id, is_read);
|
||||
@@ -26,9 +26,25 @@
|
||||
</div>
|
||||
<div class="item">
|
||||
<a id="themeToggle" title="切换主题" style="cursor:pointer; font-size:1.2em; color:inherit;">
|
||||
<i class="moon icon" id="themeIcon"></i> 主题
|
||||
<i class="moon icon" id="themeIcon"></i>
|
||||
</a>
|
||||
</div>
|
||||
<#if username??>
|
||||
<div class="item">
|
||||
<a href="/notifications" title="通知" style="position:relative; cursor:pointer; color:inherit;">
|
||||
<i class="bell icon"></i>
|
||||
<span id="headerNotifBadge" class="ui circular mini red label"
|
||||
style="display:none; position:absolute; top:-4px; right:-8px; font-size:10px; padding:2px 4px;"></span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="item">
|
||||
<a href="/messages" title="私信" style="position:relative; cursor:pointer; color:inherit;">
|
||||
<i class="envelope icon"></i>
|
||||
<span id="headerMsgBadge" class="ui circular mini red label"
|
||||
style="display:none; position:absolute; top:-4px; right:-8px; font-size:10px; padding:2px 4px;"></span>
|
||||
</a>
|
||||
</div>
|
||||
</#if>
|
||||
<div class="item" id="userlogin">
|
||||
<#if username??>
|
||||
<form class="ui large form" action="/logout" id="logoutForm" method="post" style="display: none;">
|
||||
@@ -70,6 +86,27 @@
|
||||
applyTheme(next);
|
||||
};
|
||||
}
|
||||
// Fetch unread counts and update header badges
|
||||
function updateBadges() {
|
||||
$.get('/msg_api/unread-counts', function(resp) {
|
||||
if (!resp || resp.code !== 10000) return;
|
||||
var d = resp.data || {};
|
||||
var notifBadge = document.getElementById('headerNotifBadge');
|
||||
var msgBadge = document.getElementById('headerMsgBadge');
|
||||
if (notifBadge) {
|
||||
if (d.notifications > 0) { notifBadge.textContent = d.notifications > 99 ? '99+' : d.notifications; notifBadge.style.display = ''; }
|
||||
else { notifBadge.style.display = 'none'; }
|
||||
}
|
||||
if (msgBadge) {
|
||||
if (d.messages > 0) { msgBadge.textContent = d.messages > 99 ? '99+' : d.messages; msgBadge.style.display = ''; }
|
||||
else { msgBadge.style.display = 'none'; }
|
||||
}
|
||||
});
|
||||
}
|
||||
if (document.getElementById('headerNotifBadge')) {
|
||||
updateBadges();
|
||||
setInterval(updateBadges, 60000); // refresh every 60s
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
@@ -0,0 +1,114 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<title>与 ${otherUsername} 的对话 - Jiscuss</title>
|
||||
<head>
|
||||
<meta name="_csrf" content="${_csrf.token}"/>
|
||||
<meta name="_csrf_header" content="${_csrf.headerName}"/>
|
||||
<#include "comm/commjs.ftl"/>
|
||||
<style>
|
||||
.msg-bubble { max-width: 65%; padding: 10px 14px; border-radius: 12px; margin: 6px 0; display: inline-block; word-break: break-word; }
|
||||
.msg-mine { background: #2185d0; color: #fff; border-bottom-right-radius: 2px; }
|
||||
.msg-other { background: #fff; color: #333; border: 1px solid #e0e0e0; border-bottom-left-radius: 2px; }
|
||||
.msg-row { display: flex; margin: 4px 0; }
|
||||
.msg-row.mine { justify-content: flex-end; }
|
||||
.msg-row.other { justify-content: flex-start; }
|
||||
.msg-time { font-size: 0.75em; color: #aaa; margin: 2px 8px; align-self: flex-end; }
|
||||
#chatBox { height: 450px; overflow-y: auto; padding: 16px; background: #f7f8fa; border: 1px solid #e0e0e0; border-radius: 8px; }
|
||||
#msgInput { resize: none; border-radius: 6px; }
|
||||
</style>
|
||||
</head>
|
||||
<body style="background:#f7f8fa;">
|
||||
<#include "comm/header.ftl"/>
|
||||
|
||||
<div class="ui container" style="margin-top:2em; margin-bottom:4em;">
|
||||
<div class="ui breadcrumb" style="margin-bottom:1em;">
|
||||
<a class="section" href="/messages"><i class="envelope icon"></i> 收件箱</a>
|
||||
<div class="divider"> / </div>
|
||||
<div class="active section">${otherUsername}</div>
|
||||
</div>
|
||||
|
||||
<h3 class="ui header">${otherUsername}</h3>
|
||||
|
||||
<div id="chatBox">
|
||||
<#if messages?has_content>
|
||||
<#list messages as msg>
|
||||
<div class="msg-row <#if msg.senderId == currentUserId>mine<#else>other</#if>">
|
||||
<#if msg.senderId != currentUserId>
|
||||
<div class="msg-bubble msg-other">${msg.content}</div>
|
||||
<span class="msg-time">${msg.createTime?string("HH:mm")}</span>
|
||||
<#else>
|
||||
<span class="msg-time">${msg.createTime?string("HH:mm")}</span>
|
||||
<div class="msg-bubble msg-mine">${msg.content}</div>
|
||||
</#if>
|
||||
</div>
|
||||
</#list>
|
||||
<#else>
|
||||
<div style="text-align:center; color:#aaa; padding:60px 0;">暂无消息,发送第一条吧!</div>
|
||||
</#if>
|
||||
</div>
|
||||
|
||||
<div class="ui form" style="margin-top:1em;">
|
||||
<div class="field">
|
||||
<textarea id="msgInput" rows="3" placeholder="输入消息..." style="width:100%;"></textarea>
|
||||
</div>
|
||||
<button class="ui primary button" id="sendBtn">
|
||||
<i class="send icon"></i> 发送
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<#include "comm/footer.ftl"/>
|
||||
|
||||
<script>
|
||||
var otherId = ${otherId};
|
||||
var csrfToken = $('meta[name="_csrf"]').attr('content');
|
||||
var csrfHeader = $('meta[name="_csrf_header"]').attr('content');
|
||||
|
||||
$('#sendBtn').on('click', function() {
|
||||
var content = $('#msgInput').val().trim();
|
||||
if (!content) return;
|
||||
$(this).addClass('loading');
|
||||
var headers = {};
|
||||
headers[csrfHeader] = csrfToken;
|
||||
$.ajax({
|
||||
url: '/msg_api/messages/send',
|
||||
type: 'POST',
|
||||
headers: headers,
|
||||
data: { receiverId: otherId, content: content },
|
||||
success: function(resp) {
|
||||
$('#sendBtn').removeClass('loading');
|
||||
if (resp && resp.code === 10000) {
|
||||
$('#msgInput').val('');
|
||||
// Append new bubble
|
||||
var now = new Date();
|
||||
var timeStr = (now.getHours()<10?'0':'')+now.getHours()+':'+(now.getMinutes()<10?'0':'')+now.getMinutes();
|
||||
var html = '<div class="msg-row mine">' +
|
||||
'<span class="msg-time">'+timeStr+'</span>' +
|
||||
'<div class="msg-bubble msg-mine">'+$('<div>').text(content).html()+'</div>' +
|
||||
'</div>';
|
||||
$('#chatBox').append(html);
|
||||
$('#chatBox').scrollTop($('#chatBox')[0].scrollHeight);
|
||||
} else {
|
||||
alert(resp.message || '发送失败');
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
$('#sendBtn').removeClass('loading');
|
||||
alert('发送失败,请重试');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Allow Ctrl+Enter to send
|
||||
$('#msgInput').on('keydown', function(e) {
|
||||
if (e.ctrlKey && e.keyCode === 13) { $('#sendBtn').click(); }
|
||||
});
|
||||
|
||||
// Scroll to bottom on load
|
||||
$(document).ready(function() {
|
||||
var box = document.getElementById('chatBox');
|
||||
box.scrollTop = box.scrollHeight;
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -326,13 +326,14 @@
|
||||
|
||||
// User card — custom fixed-position tooltip (more reliable than Semantic UI popup)
|
||||
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;pointer-events: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) {
|
||||
$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>'
|
||||
' 回复: ' + (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>'
|
||||
).css({ left: x + 14, top: y + 14 }).show();
|
||||
}
|
||||
|
||||
@@ -340,7 +341,7 @@
|
||||
var userId = $(this).data('user-id');
|
||||
if (!userId) return;
|
||||
if (cardCache[userId]) { showCard(cardCache[userId], e.clientX, e.clientY); return; }
|
||||
$.getJSON('/user_api/user/' + userId, function(res) {
|
||||
$.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);
|
||||
@@ -349,8 +350,12 @@
|
||||
}).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() {
|
||||
$card.hide();
|
||||
// Delay hide so user can click links inside card
|
||||
setTimeout(function() {
|
||||
if (!$card.is(':hover')) $card.hide();
|
||||
}, 300);
|
||||
});
|
||||
$card.on('mouseleave', function() { $card.hide(); });
|
||||
});
|
||||
|
||||
function voteDiscussion(action) {
|
||||
@@ -391,6 +396,69 @@
|
||||
|
||||
</script>
|
||||
<script type="text/javascript" charset="UTF-8" src="/static/js/user/discussions.js"></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');
|
||||
|
||||
// Open modal when user clicks 发私信 in user card
|
||||
$(document).on('click', '.open-msg-modal', function() {
|
||||
_msgReceiverId = $(this).data('uid');
|
||||
var uname = $(this).data('uname');
|
||||
$('#msgModalUsername').text(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');
|
||||
massage('私信发送成功!', 'success', '');
|
||||
} else {
|
||||
$('#msgModalError').text((resp && resp.msg) || '发送失败,请重试').show();
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
$btn.removeClass('loading disabled');
|
||||
$('#msgModalError').text('请求失败,请确认已登录').show();
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
<!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" style="margin-top:2em; margin-bottom:4em;">
|
||||
<h2 class="ui header">
|
||||
<i class="envelope icon"></i>
|
||||
<div class="content">私信收件箱
|
||||
<#if unreadMsgCount?? && (unreadMsgCount > 0)>
|
||||
<span class="ui mini red label">${unreadMsgCount} 未读</span>
|
||||
</#if>
|
||||
</div>
|
||||
</h2>
|
||||
<div class="ui divider"></div>
|
||||
|
||||
<#if inboxItems?has_content>
|
||||
<div class="ui relaxed divided list">
|
||||
<#list inboxItems as item>
|
||||
<a class="item" href="/messages/${item.otherId}"
|
||||
style="padding:1em 0; display:flex; align-items:center; gap:1em; color:inherit; text-decoration:none; cursor:pointer;">
|
||||
<div class="ui circular label"
|
||||
style="min-width:40px;height:40px;line-height:40px;text-align:center;font-size:1.1em;background:#2185d0;color:#fff;">
|
||||
${(item.otherUsername?has_content)?then(item.otherUsername?substring(0,1)?upper_case, "?")}
|
||||
</div>
|
||||
<div style="flex:1; overflow:hidden;">
|
||||
<div style="font-weight:bold;">${item.otherUsername}</div>
|
||||
<div style="color:#888; font-size:0.9em; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; max-width:450px;">
|
||||
${item.lastContent}
|
||||
</div>
|
||||
</div>
|
||||
<div style="color:#aaa; font-size:0.85em; white-space:nowrap;">
|
||||
<#if item.lastTime??>${item.lastTime?string("MM-dd HH:mm")}</#if>
|
||||
</div>
|
||||
<#if item.hasUnread>
|
||||
<span class="ui mini red label">未读</span>
|
||||
</#if>
|
||||
</a>
|
||||
</#list>
|
||||
</div>
|
||||
<#else>
|
||||
<div class="ui placeholder segment">
|
||||
<div class="ui icon header">
|
||||
<i class="envelope open outline icon"></i>
|
||||
暂无私信
|
||||
</div>
|
||||
</div>
|
||||
</#if>
|
||||
</div>
|
||||
|
||||
<#include "comm/footer.ftl"/>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,77 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<title>通知 - Jiscuss</title>
|
||||
<head>
|
||||
<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" style="margin-top:2em; margin-bottom:4em;">
|
||||
<h2 class="ui header">
|
||||
<i class="bell icon"></i>
|
||||
<div class="content">通知</div>
|
||||
</h2>
|
||||
<div class="ui divider"></div>
|
||||
|
||||
<#if notifications?has_content>
|
||||
<div class="ui feed" style="background:#fff; border-radius:8px; padding:1em;">
|
||||
<#list notifications as notif>
|
||||
<div class="event" style="padding:1em 0; border-bottom:1px solid #f0f0f0;">
|
||||
<div class="label" style="float:left; margin-right:1em;">
|
||||
<#if notif.type == "reply">
|
||||
<i class="reply blue icon" style="font-size:1.5em;"></i>
|
||||
<#elseif notif.type == "like">
|
||||
<i class="heart red icon" style="font-size:1.5em;"></i>
|
||||
<#else>
|
||||
<i class="info circle grey icon" style="font-size:1.5em;"></i>
|
||||
</#if>
|
||||
</div>
|
||||
<div class="content" style="margin-left:3em;">
|
||||
<div class="summary">
|
||||
<#if fromUsernames?has_content && notif?index < fromUsernames?size>
|
||||
<strong>${fromUsernames[notif?index]}</strong>
|
||||
</#if>
|
||||
${notif.title!"通知"}
|
||||
<#if notif.relatedId?? && notif.relatedType?? && notif.relatedType == "discussion">
|
||||
<a href="/getdiscussionsbyid?id=${notif.relatedId}">查看帖子</a>
|
||||
<#elseif notif.relatedId?? && notif.relatedType?? && notif.relatedType == "post">
|
||||
<a href="#">查看回复</a>
|
||||
</#if>
|
||||
</div>
|
||||
<#if notif.content??>
|
||||
<div class="extra text" style="color:#666; margin-top:4px;">${notif.content}</div>
|
||||
</#if>
|
||||
<div class="date" style="color:#aaa; font-size:0.85em; margin-top:4px;">
|
||||
<#if notif.createTime??>${notif.createTime?string("yyyy-MM-dd HH:mm")}</#if>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</#list>
|
||||
</div>
|
||||
|
||||
<#if (totalPages > 1)>
|
||||
<div class="ui center aligned basic segment">
|
||||
<div class="ui pagination menu">
|
||||
<#list 0..(totalPages-1) as p>
|
||||
<a class="item <#if p == currentPage>active</#if>" href="/notifications?page=${p}">${p+1}</a>
|
||||
</#list>
|
||||
</div>
|
||||
</div>
|
||||
</#if>
|
||||
|
||||
<#else>
|
||||
<div class="ui placeholder segment">
|
||||
<div class="ui icon header">
|
||||
<i class="bell slash outline icon"></i>
|
||||
暂无通知
|
||||
</div>
|
||||
</div>
|
||||
</#if>
|
||||
</div>
|
||||
|
||||
<#include "comm/footer.ftl"/>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,118 +1,152 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<title>用户详情页</title>
|
||||
<title>${username!}的个人主页 - Jiscuss</title>
|
||||
<head>
|
||||
<!-- <link rel="stylesheet" type="text/css" href="static/semanticui/semantic.css"> -->
|
||||
<!-- <script src="static/jquery/jquery-3.4.1.min.js"></script> -->
|
||||
<!-- <script src="static/semanticui/semantic.min.js"></script> -->
|
||||
<meta name="_csrf" content="${_csrf.token}"/>
|
||||
<meta name="_csrf_header" content="${_csrf.headerName}"/>
|
||||
<#include "comm/commjs.ftl"/>
|
||||
<style>
|
||||
.profile-card { background:#fff; border-radius:10px; padding:24px; box-shadow:0 1px 4px rgba(0,0,0,.08); margin-bottom:1.5em; }
|
||||
.profile-stat { text-align:center; }
|
||||
.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; }
|
||||
</style>
|
||||
</head>
|
||||
<body style=" background: #f7f8fa;">
|
||||
<body style="background:#f7f8fa;">
|
||||
<#include "comm/header.ftl"/>
|
||||
|
||||
<div class="ui container" id="container">
|
||||
<h1>${username}的个人主页</h1>
|
||||
<div class="ui container" style="margin-top:2em; margin-bottom:4em;">
|
||||
<div class="ui grid stackable">
|
||||
<#-- Left: profile info -->
|
||||
<div class="four wide column">
|
||||
<div class="profile-card" style="text-align:center;">
|
||||
<div class="ui circular image" style="width:80px;height:80px;margin:0 auto 1em;">
|
||||
<#if userEntity?? && userEntity.avatar??>
|
||||
<img src="${userEntity.avatar}" style="width:80px;height:80px;object-fit:cover;border-radius:50%;">
|
||||
<#else>
|
||||
<div style="width:80px;height:80px;border-radius:50%;background:#2185d0;color:#fff;font-size:2em;line-height:80px;text-align:center;">
|
||||
${(username!"")?substring(0,1)?upper_case}
|
||||
</div>
|
||||
</#if>
|
||||
</div>
|
||||
<div style="font-size:1.2em; font-weight:bold;">${username!}</div>
|
||||
<#if userEntity?? && userEntity.realname??>
|
||||
<div style="color:#888; font-size:0.9em;">${userEntity.realname}</div>
|
||||
</#if>
|
||||
<#if userEntity?? && userEntity.joinTime??>
|
||||
<div style="color:#aaa; font-size:0.8em; margin-top:4px;">
|
||||
加入于 ${userEntity.joinTime?string("yyyy-MM-dd")}
|
||||
</div>
|
||||
</#if>
|
||||
</div>
|
||||
<div class="profile-card">
|
||||
<div class="ui three column grid" style="text-align:center;">
|
||||
<div class="column profile-stat">
|
||||
<div class="number">${liveDiscCount!0}</div>
|
||||
<div class="label2">发帖</div>
|
||||
</div>
|
||||
<div class="column profile-stat">
|
||||
<div class="number">${liveReplyCount!0}</div>
|
||||
<div class="label2">回复</div>
|
||||
</div>
|
||||
<div class="column profile-stat">
|
||||
<div class="number">${(userEntity.level)!1}</div>
|
||||
<div class="label2">等级</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 class="ui dividing header">详情</h2>
|
||||
<#-- Right: tabs -->
|
||||
<div class="twelve wide column">
|
||||
<div class="ui pointing secondary menu">
|
||||
<a class="item <#if type == 'discussion'>active</#if>" href="/user?type=discussion">
|
||||
<i class="list icon"></i>主题
|
||||
</a>
|
||||
<a class="item <#if type == 'reply'>active</#if>" href="/user?type=reply">
|
||||
<i class="comment icon"></i>回复
|
||||
</a>
|
||||
<a class="item <#if type == 'like'>active</#if>" href="/user?type=like">
|
||||
<i class="heart icon"></i>点赞
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="ui vertical stripe segment">
|
||||
<#-- <div class="ui middle aligned stackable grid container">-->
|
||||
<div class="ui internally celled grid">
|
||||
<div class="row">
|
||||
<div class="eight wide column">
|
||||
<div class="ui secondary menu">
|
||||
<a class="item ${discussion}" href="/user?type=discussion">主题</a>
|
||||
<a class="item ${change}" href="/user?type=change">动态</a>
|
||||
<a class="item ${like}" href="/user?type=like">喜欢收藏</a>
|
||||
<div class="tab-content">
|
||||
<#if type == 'discussion'>
|
||||
<#if discussions?has_content>
|
||||
<div class="ui divided list">
|
||||
<#list discussions as d>
|
||||
<div class="item" style="padding:10px 0; display:flex; justify-content:space-between; align-items:center;">
|
||||
<div>
|
||||
<a href="/getdiscussionsbyid?id=${d.id}" style="font-weight:500;">${d.title}</a>
|
||||
<div style="color:#aaa; font-size:0.8em; margin-top:2px;">
|
||||
<#if d.startTime??>${d.startTime?string("yyyy-MM-dd")}</#if>
|
||||
</div>
|
||||
</div>
|
||||
<div style="color:#aaa; font-size:0.85em; white-space:nowrap; margin-left:1em;">
|
||||
<i class="comment outline icon"></i>${(d.commentsCount)!0}
|
||||
<i class="eye icon"></i>${(d.viewCount)!0}
|
||||
</div>
|
||||
</div>
|
||||
</#list>
|
||||
</div>
|
||||
<#else>
|
||||
<div class="ui placeholder segment" style="border:none; box-shadow:none;">
|
||||
<div class="ui icon header"><i class="file outline icon"></i>暂无发帖记录</div>
|
||||
</div>
|
||||
</#if>
|
||||
|
||||
<#elseif type == 'reply'>
|
||||
<#if posts?has_content>
|
||||
<div class="ui divided list">
|
||||
<#list posts as p>
|
||||
<div class="item" style="padding:10px 0;">
|
||||
<div style="color:#888; font-size:0.85em; margin-bottom:4px;">
|
||||
<#if discTitles?has_content && p?index < discTitles?size>
|
||||
回复了 <a href="/getdiscussionsbyid?id=${p.discussionId!0}">${discTitles[p?index]}</a>
|
||||
</#if>
|
||||
·
|
||||
<#if p.createTime??>${p.createTime?string("MM-dd HH:mm")}</#if>
|
||||
</div>
|
||||
<div class="ui bottom attached tab segment ${discussion}">
|
||||
主题...
|
||||
<div style="color:#333;"><#if (p.content?length > 150)>${p.content?html?substring(0, 150)}…<#else>${p.content?html}</#if></div>
|
||||
</div>
|
||||
<div class="ui bottom attached tab segment ${change}">
|
||||
动态...
|
||||
</#list>
|
||||
</div>
|
||||
<div class="ui bottom attached tab segment ${like}">
|
||||
喜欢收藏...
|
||||
<#else>
|
||||
<div class="ui placeholder segment" style="border:none; box-shadow:none;">
|
||||
<div class="ui icon header"><i class="comment outline icon"></i>暂无回复记录</div>
|
||||
</div>
|
||||
</#if>
|
||||
|
||||
<#elseif type == 'like'>
|
||||
<#if likedDiscussions?has_content>
|
||||
<div class="ui divided list">
|
||||
<#list likedDiscussions as d>
|
||||
<div class="item" style="padding:10px 0; display:flex; justify-content:space-between; align-items:center;">
|
||||
<div>
|
||||
<a href="/getdiscussionsbyid?id=${d.id}" style="font-weight:500;">${d.title}</a>
|
||||
<div style="color:#aaa; font-size:0.8em;">
|
||||
<#if d.startTime??>${d.startTime?string("yyyy-MM-dd")}</#if>
|
||||
</div>
|
||||
</div>
|
||||
<div class="six wide right floated column">
|
||||
<div class="ui link cards">
|
||||
<div class="card">
|
||||
<div class="image">
|
||||
<img src="static/assets/images/logo.png">
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="header">Matt Giampietro</div>
|
||||
<div class="meta">
|
||||
<a>Friends</a>
|
||||
</div>
|
||||
<div class="description">
|
||||
Matthew is an interior designer living in New York.
|
||||
<div style="color:#aaa; font-size:0.85em; white-space:nowrap; margin-left:1em;">
|
||||
<i class="heart red icon"></i>${(d.likeCount)!0}
|
||||
</div>
|
||||
</div>
|
||||
<div class="extra content">
|
||||
<span class="right floated">
|
||||
Joined in 2013
|
||||
</span>
|
||||
<span>
|
||||
<i class="user icon"></i>
|
||||
75 Friends
|
||||
</span>
|
||||
</#list>
|
||||
</div>
|
||||
<#else>
|
||||
<div class="ui placeholder segment" style="border:none; box-shadow:none;">
|
||||
<div class="ui icon header"><i class="heart outline icon"></i>暂无点赞记录</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="center aligned column">
|
||||
<button class="ui basic button" id="userButton">
|
||||
<i class="icon hand peace outline"></i>
|
||||
小按钮·点下试试
|
||||
</button>
|
||||
</div>
|
||||
</#if>
|
||||
</#if>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<#include "comm/footer.ftl"/>
|
||||
|
||||
<#-- <div class="ui bottom vertical inverted sidebar labeled icon menu" id="userButtonsidebar">-->
|
||||
<div class="ui bottom item three labeled icon sidebar menu scale down" id="userButtonsidebar">
|
||||
<#-- <div class="ui bottom demo inverted nine item labeled icon sidebar menu scale down visible" style="">-->
|
||||
|
||||
<a class="item">
|
||||
<i class="home icon"></i>
|
||||
主题
|
||||
</a>
|
||||
<a class="item">
|
||||
<i class="block layout icon"></i>
|
||||
动态
|
||||
</a>
|
||||
<a class="item">
|
||||
<i class="smile icon"></i>
|
||||
喜欢收藏
|
||||
</a>
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
|
||||
$(document).ready(function () {
|
||||
<#if username??>
|
||||
setusername('${username}');
|
||||
console.log('已经登陆:' + username);
|
||||
</#if>
|
||||
<#--setDiscussionsId('${discussions.id}');-->
|
||||
|
||||
$('#userTab')
|
||||
.tab()
|
||||
;
|
||||
});
|
||||
|
||||
</script>
|
||||
<script type="text/javascript" charset="UTF-8" src="/static/js/user/user.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user