This commit is contained in:
oneao committed 2026-01-05 17:31:38 +08:00
1 parent f98c280ac4
commit d4b25b13ac
73 files changed
+13112 -1003

No files matched your search

+20
View File
@@ -136,4 +136,24 @@
</snapshots>
</pluginRepository>
</pluginRepositories>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.5.15</version>
<configuration>
<fork>true</fork> <!-- 如果没有该配置,devtools不会生效 -->
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
@@ -1,5 +1,6 @@
package cn.link56.auth.identity.controller;
import cn.link56.auth.identity.model.dto.AuthForgetDTO;
import cn.link56.auth.identity.model.dto.AuthLoginEmailCodeDTO;
import cn.link56.auth.identity.model.dto.AuthLoginPasswordDTO;
import cn.link56.auth.identity.model.dto.AuthRegisterDTO;
@@ -36,6 +37,15 @@ public class AuthIdentityController {
@PostMapping("/login/emailCode")
public Result<AuthLoginVO> loginByEmailCode(@RequestBody @Validated AuthLoginEmailCodeDTO loginEmailCodeDTO){
AuthLoginVO authLoginVO = authIdentityService.loginByEmailCode(loginEmailCodeDTO);
return Result.success(authLoginVO);
}
@PostMapping("/forget")
public Result<Void> loginByEmailCode(@RequestBody @Validated AuthForgetDTO dto){
authIdentityService.forgetPassword(dto.getEmail());
return Result.success();
}
}
@@ -0,0 +1,17 @@
package cn.link56.auth.identity.model.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class AuthForgetDTO {
@NotBlank(message = "邮箱不能为空")
@Email(message = "邮箱格式有误")
private String email;
}
@@ -9,6 +9,7 @@ import cn.link56.auth.identity.model.vo.AuthLoginVO;
import cn.link56.auth.token.model.AuthToken;
import cn.link56.auth.token.model.TokenPayload;
import cn.link56.auth.token.service.AuthTokenService;
import cn.link56.common.utils.EmailUtils;
import cn.link56.company.model.po.Company;
import cn.link56.company.model.po.CompanyUser;
import cn.link56.auth.identity.model.dto.AuthRegisterDTO;
@@ -16,11 +17,16 @@ import cn.link56.company.service.CompanyService;
import cn.link56.company.service.CompanyUserService;
import cn.link56.common.exception.BusinessException;
import cn.link56.common.utils.snowflake.idgen.IdGenerator;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import java.util.HashMap;
import java.util.Map;
@Service
public class AuthIdentityService {
@@ -32,6 +38,8 @@ public class AuthIdentityService {
private AuthTokenService tokenService;
@Resource
private AuthVerificationCodeService authVerificationCodeService;
@Resource
private EmailUtils emailUtils;
@Transactional
public void register(AuthRegisterDTO registerDTO) {
@@ -145,4 +153,18 @@ public class AuthIdentityService {
}
public void forgetPassword(String email) {
CompanyUser companyUser = companyUserService.getByEmail(email);
if (ObjectUtils.isEmpty(companyUser)) {
throw new BusinessException("该邮箱地址未注册,请检查后再试");
}
Map<String,Object> data = new HashMap<String,Object>();
data.put("Password",companyUser.getPassword());
boolean flag = emailUtils.sendSync("ForgetPassword.html", "找回密码", data, email);
System.out.println(flag);
}
}
@@ -1,41 +1,40 @@
package cn.link56.common.config;
import cn.link56.common.interceptor.AuthInterceptor;
import cn.link56.common.interceptor.TokenParsingInterceptor;
import cn.link56.common.interceptor.TokenValidationInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
private final AuthInterceptor authInterceptor;
private final TokenParsingInterceptor tokenParsingInterceptor;
private final TokenValidationInterceptor tokenValidationInterceptor;
public WebMvcConfig(AuthInterceptor authInterceptor) {
this.authInterceptor = authInterceptor;
public WebMvcConfig(TokenParsingInterceptor tokenParsingInterceptor,
TokenValidationInterceptor tokenValidationInterceptor) {
this.tokenParsingInterceptor = tokenParsingInterceptor;
this.tokenValidationInterceptor = tokenValidationInterceptor;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authInterceptor)
.addPathPatterns("/**")
.excludePathPatterns(
"/auth/**",
"/company/query",
"/company/detail/**",
"/company/recommend",
"/company/recommend/hot",
registry.addInterceptor(tokenParsingInterceptor)
.addPathPatterns("/**");
"/post/tag",
registry.addInterceptor(tokenValidationInterceptor)
.addPathPatterns(
"/im/**",
"/forum/post/page",
"/forum/post/detail",
"/forum/post/getRecommendList",
"/forum/post/getHotList",
"/forum/post/comment/list",
"/company/update",
"/company/user/add",
"/company/user/delete",
"/company/user/update",
"/company/user/query",
"/base/**",
"/error/**",
"/favicon.ico"
"/post/editor/**",
"/post/comment/create",
"/post/comment/delete"
);
}
}
@@ -2,7 +2,6 @@ package cn.link56.common.context;
/**
* 用户信息上下文(ThreadLocal)
* 独立,不依赖 JwtUtils 或拦截器
*/
public class UserContext {
@@ -37,4 +36,8 @@ public class UserContext {
removeUserId();
removeCompanyId();
}
public static boolean isValid() {
return USER_ID.get() != null && COMPANY_ID.get() != null;
}
}
@@ -1,93 +0,0 @@
package cn.link56.common.interceptor;
import cn.link56.auth.token.model.TokenPayload;
import cn.link56.auth.token.service.AuthTokenService;
import cn.link56.common.Result;
import cn.link56.common.constants.ResponseStatusConstants;
import cn.link56.common.context.UserContext;
import cn.link56.common.utils.JsonUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
@Component
public class AuthInterceptor implements HandlerInterceptor {
private final AuthTokenService authTokenService;
public AuthInterceptor(AuthTokenService authTokenService) {
this.authTokenService = authTokenService;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 预检请求放行
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
return true;
}
try {
String token = request.getHeader("Authorization");
if (StringUtils.isBlank(token)) {
return writeUnauthorized(response);
}
if (token.startsWith("Bearer ")) {
token = token.substring(7);
}else{
return writeUnauthorized(response);
}
boolean flag = authTokenService.validateToken(token);
if (!flag){
return writeUnauthorized(response);
}
// 解析 token
TokenPayload tokenPayload = authTokenService.parsePayload(token);
// 对比信息
String companyIdStr = request.getHeader("X-Company-Id");
String userIdStr = request.getHeader("X-User-Id");
if ((StringUtils.isNotBlank(companyIdStr) && !companyIdStr.equals(tokenPayload.getCompanyId().toString()))
|| (StringUtils.isNotBlank(userIdStr) && !userIdStr.equals(tokenPayload.getUserId().toString()))) {
return writeUnauthorized(response);
}
// 注入到 UserContext
UserContext.setUserId(tokenPayload.getUserId());
UserContext.setCompanyId(tokenPayload.getCompanyId());
return true;
} catch (Exception e) {
return writeUnauthorized(response);
}
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
UserContext.clear(); // 清理 ThreadLocal
}
private boolean writeUnauthorized(HttpServletResponse response) throws Exception {
Result<Void> result = Result.error(ResponseStatusConstants.UNAUTHORIZED, "登录过期或未授权");
String jsonString = JsonUtils.toJson(result);
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json;charset=utf-8");
try (PrintWriter out = response.getWriter()) {
out.println(jsonString);
out.flush();
}
return false;
}
}
@@ -0,0 +1,58 @@
package cn.link56.common.interceptor;
import cn.link56.auth.token.model.TokenPayload;
import cn.link56.auth.token.service.AuthTokenService;
import cn.link56.common.context.UserContext;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Component
public class TokenParsingInterceptor implements HandlerInterceptor {
private final AuthTokenService authTokenService;
public TokenParsingInterceptor(AuthTokenService authTokenService) {
this.authTokenService = authTokenService;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 预检请求放行
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
return true;
}
String token = request.getHeader("Authorization");
if (StringUtils.isBlank(token)) {
return true; // 如果没有 token,不做处理,交给下游处理
}
try {
if (token.startsWith("Bearer ")) {
token = token.substring(7); // 去掉 "Bearer " 前缀
} else {
return true; // 如果不是 Bearer token,交给下游处理
}
// 解析 token
TokenPayload tokenPayload = authTokenService.parsePayload(token);
// 将解析后的用户信息存入 UserContext(或其他类似上下文)
UserContext.setUserId(tokenPayload.getUserId());
UserContext.setCompanyId(tokenPayload.getCompanyId());
return true;
} catch (Exception e) {
return true;
}
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
UserContext.clear();
}
}
@@ -0,0 +1,51 @@
package cn.link56.common.interceptor;
import cn.link56.auth.token.model.TokenPayload;
import cn.link56.auth.token.service.AuthTokenService;
import cn.link56.common.Result;
import cn.link56.common.constants.ResponseStatusConstants;
import cn.link56.common.context.UserContext;
import cn.link56.common.utils.JsonUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
@Component
public class TokenValidationInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
boolean valid = UserContext.isValid(); // 校验是否有效(是否登录)
if (!valid) {
// 用户未登录或信息无效,返回 401 Unauthorized
return writeUnauthorized(response);
}
// 如果有效,允许请求继续
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
private boolean writeUnauthorized(HttpServletResponse response) throws Exception {
Result<Void> result = Result.error(ResponseStatusConstants.UNAUTHORIZED, "登录过期或未授权");
String jsonString = JsonUtils.toJson(result);
response.setStatus(HttpServletResponse.SC_OK);
response.setContentType("application/json;charset=utf-8");
try (PrintWriter out = response.getWriter()) {
out.println(jsonString);
out.flush();
}
return false; // 返回 false,阻止请求继续处理
}
}
@@ -20,6 +20,18 @@ public class CompanyController {
@Resource
private CompanyService companyService;
// 地区
@GetMapping("/area")
public Result<List<AreaVO>> listAreas(){
return Result.success(companyService.listAreas());
}
// 行业
@GetMapping("/industry")
public Result<List<IndustryVO>> listIndustries(){
return Result.success(companyService.listIndustries());
}
// 公司分页
@PostMapping("/query")
public Result<BasePageVO<QueryCompanyVO>> query(@RequestBody QueryCompanyDTO queryCompanyDTO){
@@ -58,13 +70,4 @@ public class CompanyController {
return Result.success();
}
@GetMapping("/area")
public Result<List<AreaVO>> listAreas(){
return Result.success(companyService.listAreas());
}
@GetMapping("/industry")
public Result<List<IndustryVO>> listIndustries(){
return Result.success(companyService.listIndustries());
}
}
@@ -19,6 +19,8 @@ import cn.link56.im.mapper.ImFriendMapper;
import cn.link56.im.mapper.ImGroupMapper;
import cn.link56.im.mapper.ImGroupUserMapper;
import cn.link56.im.storage.SessionManager;
import cn.link56.post.mapper.PostMapper;
import cn.link56.post.service.PostService;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
@@ -44,6 +46,9 @@ public class CompanyUserService extends ServiceImpl<CompanyUserMapper, CompanyUs
@Resource
private CompanyMapper companyMapper;
@Resource
private PostMapper postMapper;
/**
* 判断邮箱是否重复
* @param email 用户邮箱
@@ -74,6 +79,13 @@ public class CompanyUserService extends ServiceImpl<CompanyUserMapper, CompanyUs
}
public CompanyUser getByEmail(String email){
LambdaQueryWrapper<CompanyUser> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(CompanyUser::getEmail,email);
return companyUserMapper.selectOne(queryWrapper);
}
/**
* 校验当前用户是否有权限操作指定的公司
* @param companyId 当前公司ID
@@ -309,6 +321,8 @@ public class CompanyUserService extends ServiceImpl<CompanyUserMapper, CompanyUs
// 执行删除操作
companyUserMapper.delete(updateWrapper);
// 删除该用户的帖子
postMapper.deleteUserPosts(ids,UserContext.getUserId());
}
@@ -44,6 +44,6 @@ public interface ImFriendMapper extends BaseMapper<ImFriend> {
);
@Select("SELECT SUM(b_unread_count) FROM b_im_friend WHERE b_user_id = #{userId}")
@Select("SELECT ISNULL(SUM(b_unread_count), 0) FROM b_im_friend WHERE b_user_id = #{userId}")
Long selectAllUnreadCount(@Param("userId") Long userId);
}
@@ -17,6 +17,6 @@ public interface ImGroupUserMapper extends BaseMapper<ImGroupUser> {
);
@Select("SELECT SUM(b_unread_count) FROM b_im_group_user WHERE b_user_id = #{userId}")
@Select("SELECT ISNULL(SUM(b_unread_count), 0) FROM b_im_group_user WHERE b_user_id = #{userId}")
Long selectAllUnreadCount(@Param("userId") Long userId);
}
@@ -11,9 +11,10 @@ import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.validation.constraints.NotNull;
@RestController
@RequestMapping("/editor/post")
@RequestMapping("/post/editor")
public class EditorPostController {
@Resource
private PostService postService;
@@ -43,9 +44,9 @@ public class EditorPostController {
return Result.success();
}
@PostMapping("/delete")
public Result<Void> deletePost(@RequestBody @Validated DeletePostDTO deletePostDTO){
postService.deletePost(deletePostDTO);
@PostMapping("/delete/{postId}")
public Result<Void> deletePost(@PathVariable @NotNull(message = "帖子Id不能为空") Long postId) {
postService.deletePost(postId);
return Result.success();
}
@@ -55,10 +56,4 @@ public class EditorPostController {
return Result.success(data);
}
@GetMapping("/dashboard")
public Result<EditorDashboardVO> getEditorDashboard(){
EditorDashboardVO editorDashboardVO = postService.getEditorDashboard();
return Result.success(editorDashboardVO);
}
}
@@ -40,11 +40,11 @@ public class PostController {
return Result.success(publicPostVO);
}
@GetMapping("/company/{id}")
public Result<PostCompanyVO> getForumCompany(@PathVariable("id") @NotNull(message = "公司ID不能为空") Long companyId){
PostCompanyVO postCompanyVO = postService.getPostCompanyById(companyId);
@GetMapping("/author/{id}")
public Result<PostAuthorProfileVO> getAuthorProfile(@PathVariable("id") @NotNull(message = "文章ID不能为空") Long id){
PostAuthorProfileVO postAuthorProfileVO = postService.getAuthorProfileByPostId(id);
return Result.success(postCompanyVO);
return Result.success(postAuthorProfileVO);
}
}
@@ -1,9 +0,0 @@
package cn.link56.post.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/post/view")
public class PostViewController {
}
@@ -6,6 +6,7 @@ import cn.link56.post.model.vo.PublicPostVO;
import cn.link56.post.model.vo.QueryEditorPostVO;
import cn.link56.post.model.vo.QueryPublicPostVO;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Update;
@@ -32,10 +33,6 @@ public interface PostMapper extends BaseMapper<Post> {
@Param("month") String month
);
EditorDashboardVO.Count selectDashboardOverview(@Param("userId") Long userId);
List<EditorDashboardVO.Chart> selectDashboardViewStats(@Param("userId") Long userId);
List<EditorDashboardVO.Chart> selectDashboardNumberStats(@Param("userId") Long userId);
List<QueryPublicPostVO> selectPublicPostPage(
@Param("offset") int offset,
@@ -51,4 +48,6 @@ public interface PostMapper extends BaseMapper<Post> {
@Update("UPDATE b_post SET b_active_time = GETDATE() WHERE b_id = #{postId}")
int updateActiveTime(@Param("postId") Long postId);
void deleteUserPosts(@Param("userIds") List<Long> userIds,Long updateBy);
}
@@ -0,0 +1,17 @@
package cn.link56.post.model.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.NotNull;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CreatePostViewDTO {
@NotNull(message = "帖子ID不能为空")
private Long postId;
private Long userId;
}
@@ -1,9 +1,6 @@
package cn.link56.post.model.po;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@@ -64,4 +61,11 @@ public class Post {
value = "b_active_time"
)
private Date activeTime;
@TableLogic
@TableField(
value = "b_deleted",
fill = FieldFill.INSERT
)
private Integer deleted;
}
@@ -8,6 +8,8 @@ import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
@Data
@AllArgsConstructor
@NoArgsConstructor
@@ -26,5 +28,5 @@ public class PostStat {
value = "b_update_time",
fill = FieldFill.UPDATE
)
private Long createTime;
private Date createTime;
}
@@ -8,6 +8,8 @@ import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
@Data
@AllArgsConstructor
@NoArgsConstructor
@@ -26,5 +28,5 @@ public class PostView {
value = "b_create_time",
fill = FieldFill.INSERT
)
private Long createTime;
private Date createTime;
}
@@ -0,0 +1,40 @@
package cn.link56.post.model.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
import java.util.List;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PostAuthorProfileVO {
private Company company;
private User user;
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class User{
private Long id;
private String avatar;
private String name;
private String position;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class Company {
private Long id;
private String name;
private String logo;
private String profile;
private Date establishmentDate;
private List<String> areas;
private List<String> industries;
}
}
@@ -1,21 +0,0 @@
package cn.link56.post.model.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
import java.util.List;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PostCompanyVO {
private Long id;
private String name;
private String logo;
private String profile;
private Date establishmentDate;
private List<String> areas;
private List<String> industries;
}
@@ -17,8 +17,8 @@ public class PublicPostVO {
private List<String> tags;
private Date activeTime;
private Long viewCount;
private Long commentCount;
private Integer viewCount;
private Integer commentCount;
private Long companyId;
}
@@ -22,7 +22,8 @@ public class QueryEditorPostVO {
private Integer status;
private Integer viewCount;
private Integer likeCount;
private Integer favoriteCount;
private Integer commentCount;
private Long editorId;
private String editorName;
}
@@ -4,13 +4,16 @@ import cn.link56.common.BasePageVO;
import cn.link56.common.context.UserContext;
import cn.link56.common.exception.BusinessException;
import cn.link56.common.utils.snowflake.idgen.IdGenerator;
import cn.link56.company.mapper.CompanyMapper;
import cn.link56.company.mapper.CompanyUserMapper;
import cn.link56.company.model.po.Company;
import cn.link56.company.model.po.CompanyUser;
import cn.link56.company.service.CompanyService;
import cn.link56.company.service.CompanyUserService;
import cn.link56.post.mapper.PostMapper;
import cn.link56.post.model.dto.*;
import cn.link56.post.model.enums.PostStatusEnum;
import cn.link56.post.model.po.Post;
import cn.link56.post.model.po.PostComment;
import cn.link56.post.model.vo.*;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
@@ -34,11 +37,11 @@ public class PostService extends ServiceImpl<PostMapper, Post> {
@Resource
private PostViewService postViewService;
@Resource
private PostCommentService postCommentService;
@Resource
private CompanyService companyService;
@Resource
private PostStatService postStatService;
@Resource
private CompanyMapper companyMapper;
@Resource
private CompanyUserMapper companyUserMapper;
@Transactional
public EditorPostVO createPost(CreatePostDTO dto) {
@@ -145,17 +148,9 @@ public class PostService extends ServiceImpl<PostMapper, Post> {
}
@Transactional
public void deletePost(DeletePostDTO deletePostDTO) {
Long postId = deletePostDTO.getId();
public void deletePost(Long postId) {
// 删除帖子
postMapper.deleteById(postId);
// 删除帖子统计
postStatService.removeById(postId);
// 删除帖子浏览
postViewService.deleteByPostId(postId);
// 删除帖子评论
postCommentService.deleteByPostId(postId);
}
@Transactional
@@ -172,27 +167,6 @@ public class PostService extends ServiceImpl<PostMapper, Post> {
postMapper.updateById(post);
}
public EditorDashboardVO getEditorDashboard() {
Long userId = UserContext.getUserId();
EditorDashboardVO editorDashboardVO = new EditorDashboardVO();
EditorDashboardVO.Count count = postMapper.selectDashboardOverview(userId);
// 原始数据
List<EditorDashboardVO.Chart> viewCharts = postMapper.selectDashboardViewStats(userId);
List<EditorDashboardVO.Chart> numberCharts = postMapper.selectDashboardNumberStats(userId);
List<EditorDashboardVO.Chart> typeCharts = getDashboardTypeStats(userId);
// 统一处理最近 7 天,补齐缺失值
editorDashboardVO.setViewChart(fillLast7Days(viewCharts));
editorDashboardVO.setNumberChart(fillLast7Days(numberCharts));
editorDashboardVO.setTypeChart(typeCharts);
editorDashboardVO.setAllCount(count);
return editorDashboardVO;
}
/**
* 填充最近7天的 chart 数据,如果 map 里没有,则补 0
*/
@@ -275,27 +249,46 @@ public class PostService extends ServiceImpl<PostMapper, Post> {
BeanUtils.copyProperties(post,postVO);
postVO.setTags(Arrays.stream(StringUtils.split(post.getTag(),",")).collect(Collectors.toList()));
Long viewTotal = postViewService.getTotalByPostId(id);
Long commentTotal = postCommentService.getTotalByPostId(id);
Integer viewTotal = postStatService.getPostViewCount(id);
Integer commentTotal = postStatService.getPostCommentCount(id);
postVO.setViewCount(viewTotal);
postVO.setCommentCount(commentTotal);
// 新增view数量
postViewService.create(id,UserContext.getUserId());
return postVO;
}
public PostCompanyVO getPostCompanyById(Long id) {
Company company = companyService.getById(id);
public PostAuthorProfileVO getAuthorProfileByPostId(Long id) {
Post post = postMapper.selectById(id);
Company company = companyMapper.selectById(post.getCompanyId());
if (company == null){
throw new BusinessException("该公司不存在或已注销");
}
PostCompanyVO postCompanyVO = new PostCompanyVO();
BeanUtils.copyProperties(company,postCompanyVO);
// company
PostAuthorProfileVO.Company companyVO = new PostAuthorProfileVO.Company();
postCompanyVO.setAreas(Arrays.stream(StringUtils.split(company.getArea(),",")).collect(Collectors.toList()));
postCompanyVO.setIndustries(Arrays.stream(StringUtils.split(company.getIndustry(),",")).collect(Collectors.toList()));
BeanUtils.copyProperties(company, companyVO);
return postCompanyVO;
companyVO.setAreas(Arrays.stream(StringUtils.split(company.getArea(),",")).collect(Collectors.toList()));
companyVO.setIndustries(Arrays.stream(StringUtils.split(company.getIndustry(),",")).collect(Collectors.toList()));
// user
CompanyUser companyUser = companyUserMapper.selectById(post.getCreateBy());
PostAuthorProfileVO.User userVO =new PostAuthorProfileVO.User();
BeanUtils.copyProperties(companyUser,userVO);
PostAuthorProfileVO postAuthorProfileVO = new PostAuthorProfileVO();
postAuthorProfileVO.setCompany(companyVO);
postAuthorProfileVO.setUser(userVO);
return postAuthorProfileVO;
}
}
@@ -2,9 +2,8 @@ package cn.link56.post.service;
import cn.link56.post.mapper.PostStatMapper;
import cn.link56.post.model.po.PostStat;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.apache.commons.lang3.ObjectUtils;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@@ -39,4 +38,24 @@ public class PostStatService extends ServiceImpl<PostStatMapper, PostStat> {
this.save(stat);
}
public Integer getPostViewCount(Long postId){
PostStat postStat = postStatMapper.selectById(postId);
if (ObjectUtils.isEmpty(postStat)){
return 0;
}
return postStat.getViewCount();
}
public Integer getPostCommentCount(Long postId){
PostStat postStat = postStatMapper.selectById(postId);
if (ObjectUtils.isEmpty(postStat)){
return 0;
}
return postStat.getCommentCount();
}
}
@@ -1,9 +1,13 @@
package cn.link56.post.service;
import cn.link56.common.utils.snowflake.idgen.IdGenerator;
import cn.link56.post.mapper.PostViewMapper;
import cn.link56.post.model.dto.CreatePostViewDTO;
import cn.link56.post.model.po.PostView;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.apache.commons.lang3.ObjectUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
@@ -11,7 +15,8 @@ import javax.annotation.Resource;
public class PostViewService extends ServiceImpl<PostViewMapper, PostView> {
@Resource
private PostViewMapper postViewMapper;
@Resource
private PostStatService postStatService;
/**
* 根据文章 id 获取浏览总数
@@ -30,4 +35,17 @@ public class PostViewService extends ServiceImpl<PostViewMapper, PostView> {
.eq(PostView::getPostId, postId)
.remove();
}
@Transactional
public void create(Long postId,Long userId) {
if (ObjectUtils.isNotEmpty(userId)){
PostView postView = new PostView();
postView.setId(IdGenerator.nextId());
postView.setPostId(postId);
postView.setUserId(userId);
postViewMapper.insert(postView);
}
postStatService.increaseViewCount(postId);
}
}
@@ -0,0 +1,58 @@
server:
port: 8090
netty:
port: 8080
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: net.sourceforge.jtds.jdbc.Driver
url: jdbc:jtds:sqlserver://127.0.0.1:1433;databaseName=G3-Link56
username: sa
password: "123456@aaa"
# druid 连接池管理
druid:
# 初始化时建立物理连接的个数
initial-size: 5
# 连接池的最小空闲数量
min-idle: 5
# 连接池最大连接数量
max-active: 20
# 获取连接时最大等待时间,单位毫秒
max-wait: 60000
# 申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。
test-while-idle: true
# 既作为检测的间隔时间又作为testWhileIdel执行的依据
time-between-eviction-runs-millis: 60000
# 销毁线程时检测当前连接的最后活动时间和当前时间差大于该值时,关闭当前连接(配置连接在池中的最小生存时间)
min-evictable-idle-time-millis: 30000
# 用来检测数据库连接是否有效的sql 必须是一个查询语句(oracle中为 select 1 from dual) validation-query: select 'x'
# 申请连接时会执行validationQuery检测连接是否有效,开启会降低性能,默认为true
test-on-borrow: false
# 归还连接时会执行validationQuery检测连接是否有效,开启会降低性能,默认为true
test-on-return: false
# 是否缓存preparedStatement, 也就是PSCache,PSCache对支持游标的数据库性能提升巨大,比如说oracle,在mysql下建议关闭。
pool-prepared-statements: false
# 置监控统计拦截的filters,去掉后监控界面sql无法统计,stat: 监控统计、Slf4j:日志记录、waLL: 防御sqL注入
filters: stat,wall,slf4j
# 要启用PSCache,必须配置大于0,当大于0时,poolPreparedStatements自动触发修改为true。在Druid中,不会存在Oracle下PSCache占用内存过多的问题,可以把这个数值配置大一些,比如说100
max-pool-prepared-statement-per-connection-size: -1
# 合并多个DruidDataSource的监控数据
use-global-data-source-stat: true
# 通过connectProperties属性来打开mergeSql功能;慢SQL记录
connect-properties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
stat-view-servlet:
# 是否启用StatViewServlet默认值true
enabled: true
# 访问路径为/druid时,跳转到StatViewServlet
url-pattern: /druid/*
# 是否能够重置数据
reset-enable: false
# 需要账号密码才能访问控制台,默认为root
login-username: root
login-password: root
# IP白名单
allow: 127.0.0.1
# IP黑名单(共同存在时,deny优先于allow)
deny:
@@ -15,21 +15,19 @@ logging:
spring:
profiles:
active: dev
active: prod
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
mail:
host: smtp.163.com #邮箱服务器地址
username: hi_itao@163.com #邮箱账号
password: GVdEzKAee9MByXdy #邮箱授权码
password: RSchPDWrKwVcyMG7 #邮箱授权码
default-encoding: utf-8 #默认编码
mybatis-plus:
# 对应的 XML 文件位置
mapperLocations: classpath*:mapper/**/*Mapper.xml
# 实体扫描,多个package用逗号或者分号分隔
typeAliasesPackage: cn.link56.domain
# configLocation: classpath:mybatis/mybatis-config.xml
configuration:
cache-enabled: true
@@ -72,7 +72,7 @@
</if>
<if test="title != null and title != ''">
AND p.b_title LIKE CONCAT('%', #{title}, '%')
AND p.b_title LIKE '%' + #{title} + '%'
</if>
<if test="year != null and year != ''">
@@ -82,6 +82,8 @@
<if test="month != null and month != ''">
AND MONTH(p.b_update_time) = #{month}
</if>
AND p.b_deleted = 0
</where>
) t
WHERE t.rn BETWEEN #{offset} AND #{limit}
@@ -111,47 +113,10 @@
<if test="month != null and month != ''">
AND MONTH(p.b_update_time) = #{month}
</if>
AND p.b_deleted = 0
</where>
</select>
<select id="selectDashboardOverview"
resultType="cn.link56.post.model.vo.EditorDashboardVO$Count">
SELECT
COUNT(p.b_id) AS postCount,
ISNULL(SUM(ps.b_view_count), 0) AS viewCount,
ISNULL(SUM(ps.b_like_count), 0) AS likeCount
FROM b_post p
LEFT JOIN b_post_stat ps ON ps.b_post_id = p.b_id
WHERE p.b_create_by = #{userId}
</select>
<select id="selectDashboardViewStats" resultType="cn.link56.post.model.vo.EditorDashboardVO$Chart">
SELECT
CONVERT(VARCHAR(10), v.b_create_time, 120) AS label,
COUNT(*) AS value
FROM b_post_view v
JOIN b_post p ON v.b_post_id = p.b_id
WHERE p.b_create_by = #{userId}
AND v.b_create_time &gt;= CONVERT(DATE, DATEADD(DAY, -6, GETDATE()))
AND v.b_create_time &lt; CONVERT(DATE, DATEADD(DAY, 1, GETDATE()))
GROUP BY CONVERT(VARCHAR(10), v.b_create_time, 120)
ORDER BY label
</select>
<select id="selectDashboardNumberStats" resultType="cn.link56.post.model.vo.EditorDashboardVO$Chart">
SELECT
CONVERT(VARCHAR(10), p.b_create_time, 120) AS label,
COUNT(*) AS value
FROM b_post p
WHERE p.b_create_by = #{userId}
AND p.b_create_time &gt;= CONVERT(DATE, DATEADD(DAY, -6, GETDATE()))
AND p.b_create_time &lt; CONVERT(DATE, DATEADD(DAY, 1, GETDATE()))
GROUP BY CONVERT(VARCHAR(10), p.b_create_time, 120)
ORDER BY label
</select>
<select id="selectPublicPostPage" resultMap="QueryPublicPostVOMap">
SELECT
t.id,
@@ -191,6 +156,7 @@
<if test="tag != null and tag != ''">
AND p.b_tag LIKE '%' + #{tag} + '%'
</if>
AND p.b_deleted = 0
</where>
) t
WHERE t.rn BETWEEN #{offset} AND #{limit}
@@ -209,7 +175,22 @@
<if test="tag != null and tag != ''">
AND p.b_tag LIKE '%' + #{tag} + '%'
</if>
AND p.b_deleted = 0
</where>
</select>
<!-- 批量软删除用户的帖子 -->
<delete id="deleteUserPosts">
UPDATE b_post
SET b_deleted = 1,
update_by = #{updateBy},
update_time = GETDATE()
WHERE b_create_by IN
<foreach item="userId" collection="userIds" open="(" separator="," close=")">
#{userId}
</foreach>
AND b_deleted = 0
</delete>
</mapper>
@@ -11,7 +11,6 @@
<title></title>
<style type="text/css" rel="stylesheet" media="all">
/* Base ------------------------------ */
@import url("https://fonts.googleapis.com/css?family=Nunito+Sans:400,700&display=swap");
body {
@@ -381,6 +380,7 @@
width: 100%;
margin: 30px auto;
padding: 0;
font-size: 42px;
-premailer-width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
@@ -464,7 +464,7 @@
<table class="email-content" width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td class="email-masthead">
<a href="https://example.com" class="f-fallback email-masthead_name">
<a href="http://link56.net/" class="f-fallback email-masthead_name">
link56
</a>
</td>
@@ -478,29 +478,17 @@
<tr>
<td class="content-cell">
<div class="f-fallback">
<h1>您好 [[${Name}]],</h1>
<p>您最近请求重置 link56 账户的密码。请点击下面的按钮进行密码重置。<strong>该重置链接仅在 10 分钟内有效。</strong></p>
<h1>您好</h1>
<p>
您正在 <strong>link56</strong> 平台进行<strong>找回密码</strong>操作,
密码如下,请注意保存。
</p>
<!-- Action -->
<table class="body-action" align="center" width="100%" cellpadding="0" cellspacing="0"
role="presentation">
<tr>
<td align="center">
<!-- Border based button
https://litmus.com/blog/a-guide-to-bulletproof-buttons-in-email-design -->
<table width="100%" border="0" cellspacing="0" cellpadding="0" role="presentation">
<tr>
<td align="center">
<a href="[[${ResetUrl}]]" class="f-fallback button button--green"
target="_blank">重置您的密码</a>
</td>
</tr>
</table>
</td>
</tr>
</table>
<p class="body-action">
[[${Password}]]
</p>
<p>如果不是您本人操作,请忽略此邮件,或如有疑问,请 <a
href="[[${SupportUrl}]]">联系客服</a>。</p>
href="{{SupportUrl}}">联系客服</a>。</p>
<p>
感谢,<br>
@@ -0,0 +1,524 @@
<!DOCTYPE html
PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="x-apple-disable-message-reformatting" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="color-scheme" content="light dark" />
<meta name="supported-color-schemes" content="light dark" />
<title></title>
<style type="text/css" rel="stylesheet" media="all">
/* Base ------------------------------ */
@import url("https://fonts.googleapis.com/css?family=Nunito+Sans:400,700&display=swap");
body {
width: 100% !important;
height: 100%;
margin: 0;
-webkit-text-size-adjust: none;
}
a {
color: #3869D4;
}
a img {
border: none;
}
td {
word-break: break-word;
}
.preheader {
display: none !important;
visibility: hidden;
mso-hide: all;
font-size: 1px;
line-height: 1px;
max-height: 0;
max-width: 0;
opacity: 0;
overflow: hidden;
}
/* Type ------------------------------ */
body,
td,
th {
font-family: "Nunito Sans", Helvetica, Arial, sans-serif;
}
h1 {
margin-top: 0;
color: #333333;
font-size: 22px;
font-weight: bold;
text-align: left;
}
h2 {
margin-top: 0;
color: #333333;
font-size: 16px;
font-weight: bold;
text-align: left;
}
h3 {
margin-top: 0;
color: #333333;
font-size: 14px;
font-weight: bold;
text-align: left;
}
td,
th {
font-size: 16px;
}
p,
ul,
ol,
blockquote {
margin: .4em 0 1.1875em;
font-size: 16px;
line-height: 1.625;
}
p.sub {
font-size: 13px;
}
/* Utilities ------------------------------ */
.align-right {
text-align: right;
}
.align-left {
text-align: left;
}
.align-center {
text-align: center;
}
.u-margin-bottom-none {
margin-bottom: 0;
}
/* Buttons ------------------------------ */
.button {
background-color: #3869D4;
border-top: 10px solid #3869D4;
border-right: 18px solid #3869D4;
border-bottom: 10px solid #3869D4;
border-left: 18px solid #3869D4;
display: inline-block;
color: #FFF;
text-decoration: none;
border-radius: 3px;
box-shadow: 0 2px 3px rgba(0, 0, 0, 0.16);
-webkit-text-size-adjust: none;
box-sizing: border-box;
}
.button--green {
background-color: #22BC66;
border-top: 10px solid #22BC66;
border-right: 18px solid #22BC66;
border-bottom: 10px solid #22BC66;
border-left: 18px solid #22BC66;
}
.button--red {
background-color: #FF6136;
border-top: 10px solid #FF6136;
border-right: 18px solid #FF6136;
border-bottom: 10px solid #FF6136;
border-left: 18px solid #FF6136;
}
@media only screen and (max-width: 500px) {
.button {
width: 100% !important;
text-align: center !important;
}
}
/* Attribute list ------------------------------ */
.attributes {
margin: 0 0 21px;
}
.attributes_content {
background-color: #F4F4F7;
padding: 16px;
}
.attributes_item {
padding: 0;
}
/* Related Items ------------------------------ */
.related {
width: 100%;
margin: 0;
padding: 25px 0 0 0;
-premailer-width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
}
.related_item {
padding: 10px 0;
color: #CBCCCF;
font-size: 15px;
line-height: 18px;
}
.related_item-title {
display: block;
margin: .5em 0 0;
}
.related_item-thumb {
display: block;
padding-bottom: 10px;
}
.related_heading {
border-top: 1px solid #CBCCCF;
text-align: center;
padding: 25px 0 10px;
}
/* Discount Code ------------------------------ */
.discount {
width: 100%;
margin: 0;
padding: 24px;
-premailer-width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
background-color: #F4F4F7;
border: 2px dashed #CBCCCF;
}
.discount_heading {
text-align: center;
}
.discount_body {
text-align: center;
font-size: 15px;
}
/* Social Icons ------------------------------ */
.social {
width: auto;
}
.social td {
padding: 0;
width: auto;
}
.social_icon {
height: 20px;
margin: 0 8px 10px 8px;
padding: 0;
}
/* Data table ------------------------------ */
.purchase {
width: 100%;
margin: 0;
padding: 35px 0;
-premailer-width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
}
.purchase_content {
width: 100%;
margin: 0;
padding: 25px 0 0 0;
-premailer-width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
}
.purchase_item {
padding: 10px 0;
color: #51545E;
font-size: 15px;
line-height: 18px;
}
.purchase_heading {
padding-bottom: 8px;
border-bottom: 1px solid #EAEAEC;
}
.purchase_heading p {
margin: 0;
color: #85878E;
font-size: 12px;
}
.purchase_footer {
padding-top: 15px;
border-top: 1px solid #EAEAEC;
}
.purchase_total {
margin: 0;
text-align: right;
font-weight: bold;
color: #333333;
}
.purchase_total--label {
padding: 0 15px 0 0;
}
body {
background-color: #F2F4F6;
color: #51545E;
}
p {
color: #51545E;
}
.email-wrapper {
width: 100%;
margin: 0;
padding: 0;
-premailer-width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
background-color: #F2F4F6;
}
.email-content {
width: 100%;
margin: 0;
padding: 0;
-premailer-width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
}
/* Masthead ----------------------- */
.email-masthead {
padding: 25px 0;
text-align: center;
}
.email-masthead_logo {
width: 94px;
}
.email-masthead_name {
font-size: 16px;
font-weight: bold;
color: #A8AAAF;
text-decoration: none;
text-shadow: 0 1px 0 white;
}
/* Body ------------------------------ */
.email-body {
width: 100%;
margin: 0;
padding: 0;
-premailer-width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
}
.email-body_inner {
width: 570px;
margin: 0 auto;
padding: 0;
-premailer-width: 570px;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
background-color: #FFFFFF;
}
.email-footer {
width: 570px;
margin: 0 auto;
padding: 0;
-premailer-width: 570px;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
text-align: center;
}
.email-footer p {
color: #A8AAAF;
}
.body-action {
width: 100%;
margin: 30px auto;
padding: 0;
font-size: 42px;
-premailer-width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
text-align: center;
}
.body-sub {
margin-top: 25px;
padding-top: 25px;
border-top: 1px solid #EAEAEC;
}
.content-cell {
padding: 45px;
}
/*Media Queries ------------------------------ */
@media only screen and (max-width: 600px) {
.email-body_inner,
.email-footer {
width: 100% !important;
}
}
@media (prefers-color-scheme: dark) {
body,
.email-body,
.email-body_inner,
.email-content,
.email-wrapper,
.email-masthead,
.email-footer {
background-color: #333333 !important;
color: #FFF !important;
}
p,
ul,
ol,
blockquote,
h1,
h2,
h3,
span,
.purchase_item {
color: #FFF !important;
}
.attributes_content,
.discount {
background-color: #222 !important;
}
.email-masthead_name {
text-shadow: none !important;
}
}
:root {
color-scheme: light dark;
supported-color-schemes: light dark;
}
</style>
<!--[if mso]>
<style type="text/css">
.f-fallback {
font-family: Arial, sans-serif;
}
</style>
<![endif]-->
</head>
<body>
<span class="preheader">Use this link to reset your password. The link is only valid for 24 hours.</span>
<table class="email-wrapper" width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td align="center">
<table class="email-content" width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td class="email-masthead">
<a href="http://link56.net" class="f-fallback email-masthead_name">
link56
</a>
</td>
</tr>
<!-- Email Body -->
<tr>
<td class="email-body" width="570" cellpadding="0" cellspacing="0">
<table class="email-body_inner" align="center" width="570" cellpadding="0" cellspacing="0"
role="presentation">
<!-- Body content -->
<tr>
<td class="content-cell">
<div class="f-fallback">
<h1>您好</h1>
<p>
您正在 <strong>link56</strong> 平台进行<strong>找回密码</strong>操作,
密码如下,请注意保存。
</p>
<p class="body-action">
[[${Password}]]
</p>
<p>如果不是您本人操作,请忽略此邮件,或如有疑问,请 <a
href="{{SupportUrl}}">联系客服</a>。</p>
<p>
感谢,<br>
link56 团队
</p>
</div>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td>
<table class="email-footer" align="center" width="570" cellpadding="0" cellspacing="0"
role="presentation">
<tr>
<td class="content-cell" align="center">
<p class="f-fallback sub align-center">
宁波江东远通计算机有限公司
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
@@ -11,7 +11,6 @@
<title></title>
<style type="text/css" rel="stylesheet" media="all">
/* Base ------------------------------ */
@import url("https://fonts.googleapis.com/css?family=Nunito+Sans:400,700&display=swap");
body {
@@ -465,7 +464,7 @@
<table class="email-content" width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td class="email-masthead">
<a href="https://example.com" class="f-fallback email-masthead_name">
<a href="http://link56.net/" class="f-fallback email-masthead_name">
link56
</a>
</td>
@@ -465,7 +465,7 @@
<table class="email-content" width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td class="email-masthead">
<a href="https://example.com" class="f-fallback email-masthead_name">
<a href="http://link56.net/" class="f-fallback email-masthead_name">
link56
</a>
</td>
@@ -465,7 +465,7 @@
<table class="email-content" width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td class="email-masthead">
<a href="https://example.com" class="f-fallback email-masthead_name">
<a href="http://link56.net/" class="f-fallback email-masthead_name">
link56
</a>
</td>
+1 -1
View File
@@ -1,5 +1,5 @@
VITE_SERVER_BASEURL = 'http://link56.net:80'
VITE_WEBSOCKET_BASEURL = 'ws://link56.net:8080/websocket'
VITE_WEBSOCKET_BASEURL = 'ws://link56.net:8080/ws'
# VITE_SERVER_BASEURL = 'http://121.237.182.108:80'
# VITE_WEBSOCKET_BASEURL = 'ws://121.237.182.108:8080/websocket'
+2 -2
View File
@@ -2,9 +2,9 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<link rel="icon" href="/logo.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>link-frontend-new2</title>
<title>link56</title>
</head>
<body>
<div id="app"></div>
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

@@ -52,8 +52,9 @@ function onSearch() {
border: 0;
color: #000;
box-shadow:
0 1px 2px rgba(0, 0, 0, 0.06),
0 4px 12px rgba(0, 0, 0, 0.08);
0 1px 1px rgba(0, 0, 0, 0.04),
0 2px 4px rgba(0, 0, 0, 0.06);
.query-item {
flex: 1;
// border-right: 1px solid #ddd;
@@ -26,7 +26,7 @@ defineProps<{
</el-sub-menu>
<!-- 单菜单 -->
<el-menu-item v-else :index="item.index">
<el-menu-item v-else :index="item.index" :disabled="item.disabled">
<el-icon v-if="item.icon">
<SvgIcon :icon="item.icon" />
</el-icon>
@@ -1,6 +1,7 @@
export interface NavMenuItem {
export interface NavMenuItem {
label: string
index: string
icon?: string
children?: NavMenuItem []
disabled?: boolean
}
@@ -20,7 +20,7 @@ const menuList = [
{ name: '首页', path: '/' },
{ name: '企业名录', path: '/company' },
{ name: '论坛中心', path: '/post' },
{ name: '关于我们', path: '/aboutus' },
// { name: '关于我们', path: '/aboutus' },
// { name: '联系我们', path: '/forum' },
]
@@ -64,7 +64,7 @@ function isActive(path: string) {
<template>
<header class="header">
<nav class="container">
<img src="/logo.png" class="logo">
<img src="/logo.png" class="logo" @click="navigateTo('/')">
<div class="menu">
<span
v-for="item in menuList"
@@ -150,6 +150,7 @@ function isActive(path: string) {
height: 50px;
flex-shrink: 0;
min-width: 0;
cursor: pointer;
}
.menu {
@@ -1,4 +1,4 @@
import type { LoginApiResult, LoginEmailCodeApiParams, LoginPasswordApiParams, RegisterApiParams } from '@/types/auth/index'
import type { ForgetApiParams, LoginApiResult, LoginEmailCodeApiParams, LoginPasswordApiParams, RegisterApiParams } from '@/types/auth/index'
import { httpPostJson } from '@/request/utils'
export function registerApi(params: RegisterApiParams) {
@@ -12,3 +12,7 @@ export function loginByPasswordApi(params: LoginPasswordApiParams) {
export function loginByEmailCodeApi(params: LoginEmailCodeApiParams) {
return httpPostJson<LoginApiResult>('/auth/identity/login/emailCode', params)
}
export function forgetApi(params: ForgetApiParams) {
return httpPostJson<void>('/auth/identity/forget', params)
}
@@ -1,7 +1,6 @@
import type { DashboardData, PostTag } from '@/types/post'
import type { DashboardData } from '@/types/post'
import type {
CreatePostParams,
DeletePostParams,
Post,
PostEditorForm,
QueryPostParams,
@@ -12,29 +11,29 @@ import { httpGet, httpPostJson } from '@/request/utils'
// ===== 编辑端 API =====
export function createPostApi(params: CreatePostParams) {
return httpPostJson<PostEditorForm>('/editor/post/create', params)
return httpPostJson<PostEditorForm>('/post/editor/create', params)
}
export function updatePostApi(params: UpdatePostParams) {
return httpPostJson<PostEditorForm>('/editor/post/update', params)
return httpPostJson<PostEditorForm>('/post/editor/update', params)
}
export function updatePostStatusApi(params: UpdatePostStatusParams) {
return httpPostJson<PostEditorForm>('/editor/post/update/status', params)
return httpPostJson<PostEditorForm>('/post/editor/update/status', params)
}
export function queryEditorPostsApi(params: QueryPostParams) {
return httpPostJson<BasePageResult<Post>>('/editor/post/query', params)
return httpPostJson<BasePageResult<Post>>('/post/editor/query', params)
}
export function getEditorPostApi(id: number) {
return httpGet<PostEditorForm>(`/editor/post/${id}`)
return httpGet<PostEditorForm>(`/post/editor/${id}`)
}
export function deletePostApi(params: DeletePostParams) {
return httpPostJson<void>('/editor/post/delete', params)
export function deletePostApi(postId: number) {
return httpPostJson<void>(`/post/editor/delete/${postId}`)
}
export function getEditorDashboardApi() {
return httpGet<DashboardData>(`/editor/post/dashboard`)
return httpGet<DashboardData>(`/post/editor/dashboard`)
}
@@ -1,4 +1,4 @@
import type { CreatePostCommentParams, CreatePostCommentResult, PostComment, PostCompany, PostTag, PublicPost, PublicPostDetail, QueryPublicPostParams } from '@/types/post'
import type { CreatePostCommentParams, CreatePostCommentResult, PostAuthorProfile, PostComment, PostTag, PublicPost, PublicPostDetail, QueryPublicPostParams } from '@/types/post'
import { httpGet, httpPostJson } from '@/request/utils'
// ===== 公共接口 =====
@@ -14,8 +14,8 @@ export function getPublicPostDetailApi(id: number) {
return httpGet<PublicPostDetail>(`/post/${id}`)
}
export function getPostCompanyById(id: number) {
return httpGet<PostCompany>(`/post/company/${id}`)
export function getAuthorProfileByPostId(id: number) {
return httpGet<PostAuthorProfile>(`/post/author/${id}`)
}
export function createPostCommentApi(params: CreatePostCommentParams) {
@@ -1,5 +1,6 @@
import type { AxiosError, AxiosInstance, AxiosResponse, InternalAxiosRequestConfig } from 'axios'
import axios from 'axios'
import { ElMessage } from 'element-plus'
import { router } from '@/router/index'
import { useUserStore } from '@/store/user'
@@ -84,14 +85,17 @@ axiosInstance.interceptors.response.use(
if (resData.code === 401) {
router.push('/auth/login')
return Promise.reject(response.data)
return Promise.resolve(response.data)
}
else if (resData.code === 404) {
router.push('/error/404')
return Promise.reject(response.data)
}
else if (resData.data === 500) {
ElMessage.error('系统异常,请稍后再试')
return Promise.resolve(response.data)
}
else {
// 500的话需要做特殊处理
resData.isSuccess = resData.code === 200
return Promise.resolve(response.data)
}
@@ -18,6 +18,10 @@ const router = createRouter({
path: '/company',
component: () => import('@/views/company/index.vue'),
},
{
path: '/company/detail/:id',
component: () => import('@/views/company/detail.vue'),
},
{
path: '/post',
children: [
@@ -35,10 +39,7 @@ const router = createRouter({
path: '/aboutus',
component: () => import('@/views/aboutus/index.vue'),
},
{
path: '/company/detail',
component: () => import('@/views/company/detail.vue'),
},
{
path: '/im/:type?/:id?',
component: () => import('@/views/im/index.vue'),
@@ -217,6 +217,8 @@ function createWebSocket(options?: Partial<UseWebSocketOptions>) {
function logout() {
cachedToken = null
authenticated.value = false
sendRaw(MessageType.LOGOUT)
}
/* ================== 消息处理(核心修改点) ================== */
@@ -31,6 +31,7 @@ export interface RegisterCompanyForm {
phone: string
email: string
profile: string
emailCode: string
}
export interface RegisterAdminForm {
@@ -62,3 +63,7 @@ export interface RegisterApiParams {
position: string
}
}
export interface ForgetApiParams {
email: string
}
@@ -14,66 +14,21 @@ declare module 'vue' {
AnchorMenu: typeof import('./../components/anchor-menu/index.vue')['default']
AreaSelect: typeof import('./../components/AreaSelect.vue')['default']
ChatCompanyUserCard: typeof import('./../components/ChatCompanyUserCard.vue')['default']
ChatCompanyUserSelect: typeof i../components/ChatCompanyUserCard.vuet.vue')['default']
ChatUserSelect: typeof import('./../components/ChatUserSelect.vue')['default']
CompanyUserCard: typeof import('../components/CompanyUserProfileDialog.vue')['default']
CompanyUserPopover: typeof import('./../components/CompanyUserPopover.vue')['default']
CompanyUserProfileDialog: typeof import('./../components/CompanyUserProfileDialog.vue')['default']
CustomEmpty: typeof import('./../components/CustomEmpty.vue')['default']
CustomLoading: typeof import('./../components/CustomLoading.vue')['default']
Directory: typeof import('./../components/wang-editor/modules/directory.vue')['default']
EmailCodeInput: typeof import('./../components/EmailCodeInput.vue')['default']
EmojiPopover: typeof import('./../components/EmojiPopover.vue')['default']
HelloWorld: typeof import('./../components/HelloWorld.vue')['default']
ICarbonAccessibility: typeof import('~icons/carbon/accessibility')['default']
'IFa7Solid:userShield': typeof import('~icons/fa7-solid/user-shield')['default']
'IHugeicons:building02': typeof import('~icons/hugeicons/building02')['default']
'IHugeicons:edit01': typeof import('~icons/hugeicons/edit01')['default']
'IHugeicons:edit04': typeof import('~icons/hugeicons/edit04')['default']
'IHugeicons:home03': typeof import('~icons/hugeicons/home03')['default']
'IHugeicons:materialAndTexture': typeof import('~icons/hugeicons/material-and-texture')['default']
'IHugeicons:setting07': typeof import('~icons/hugeicons/setting07')['default']
'IHugeicons:upload01': typeof import('~icons/hugeicons/upload01')['default']
'IHugeicons:userSettings01': typeof import('~icons/hugeicons/user-settings01')['default']
'IIc:outlineSettings': typeof import('~icons/ic/outline-settings')['default']
'IIc:twotoneHeadphones': typeof import('~icons/ic/twotone-headphones')['default']
'IIconoir:infoCircleSolid': typeof import('~icons/iconoir/info-circle-solid')['default']
'IIon:iosMore': typeof import('~icons/ion/ios-more')['default']
ImageUploadCard: typeof import('./../components/ImageUploadCard.vue')['default']
'IMajesticons:settingsCogLine': typeof import('~icons/majesticons/settings-cog-line')['default']
'IMaterialSymbols:call': typeof import('~icons/material-symbols/call')['default']
'IMaterialSymbols:chatBubbleOutline': typeof import('~icons/material-symbols/chat-bubble-outline')['default']
'IMaterialSymbols:databaseOutline': typeof import('~icons/material-symbols/database-outline')['default']
'IMaterialSymbols:folderCopyOutline': typeof import('~icons/material-symbols/folder-copy-outline')['default']
'IMaterialSymbols:fullscreen': typeof import('~icons/material-symbols/fullscreen')['default']
'IMaterialSymbols:fullscreenExit': typeof import('~icons/material-symbols/fullscreen-exit')['default']
'IMaterialSymbols:locationOn': typeof import('~icons/material-symbols/location-on')['default']
'IMaterialSymbols:locationOnOutlineRounded': typeof import('~icons/material-symbols/location-on-outline-rounded')['default']
'IMaterialSymbols:lockOpenRightSharp': typeof import('~icons/material-symbols/lock-open-right-sharp')['default']
'IMaterialSymbols:mail': typeof import('~icons/material-symbols/mail')['default']
IMdiAccountBox: typeof import('~icons/mdi/account-box')['default']
'IMingcute:chat1Fill': typeof import('~icons/mingcute/chat1-fill')['default']
'IMingcute:eye2Line': typeof import('~icons/mingcute/eye2-line')['default']
Industry: typeof import('../components/IndustrySelect.vue')['default']
IndustrySelect: typeof import('./../components/IndustrySelect.vue')['default']
'INrk:show': typeof import('~icons/nrk/show')['default']
'IRi:buildingFill': typeof import('~icons/ri/building-fill')['default']
'IRi:buildingLine': typeof import('~icons/ri/building-line')['default']
'IRi:information2Line': typeof import('~icons/ri/information2-line')['default']
'IRi:informationFill': typeof import('~icons/ri/information-fill')['default']
'IRi:message3Fill': typeof import('~icons/ri/message3-fill')['default']
'ISolar:buildings2Broken': typeof import('~icons/solar/buildings2-broken')['default']
'ISolar:chatRoundBold': typeof import('~icons/solar/chat-round-bold')['default']
'ISolar:chatRoundCallBroken': typeof import('~icons/solar/chat-round-call-broken')['default']
'ISolar:databaseBoldDuotone': typeof import('~icons/solar/database-bold-duotone')['default']
'ISolar:eyeBroken': typeof import('~icons/solar/eye-broken')['default']
'ISolar:eyeClosedBroken': typeof import('~icons/solar/eye-closed-broken')['default']
'IStreamlineUltimate:informationCircleBold': typeof import('~icons/streamline-ultimate/information-circle-bold')['default']
'ITdesign:notification': typeof import('~icons/tdesign/notification')['default']
'ITdesign:search': typeof import('~icons/tdesign/search')['default']
'ITdesign:setting1': typeof import('~icons/tdesign/setting1')['default']
Menu: typeof import('../components/nav-menu/index.vue')['default']
MenuItem: typeof import('../components/nav-menu/index.vue')['default']
NavMenu: typeof import('./../components/nav-menu/index.vue')['default']
Preview: typeof import('./../components/wang-editor/modules/preview.vue')['default']
QueryCompany: typeof import('./../components/QueryCompany.vue')['default']
@@ -59,5 +59,5 @@ export interface Post {
viewCount: number
commentCount: number
activeTime: string
updateTime: string
}
@@ -63,14 +63,22 @@ export interface PublicPostDetail {
companyId: number
}
export interface PostCompany {
id: number
name: string
logo: string
profile: string
establishmentDate: string
areas: string[]
industries: string[]
export interface PostAuthorProfile {
user: {
id: number
name: string
avatar: string
position: string
}
company: {
id: number
name: string
logo: string
profile: string
establishmentDate: string
areas: string[]
industries: string[]
}
}
export interface CreatePostCommentParams {
@@ -0,0 +1,103 @@
<script lang="ts" setup>
const carouselList = [
{
title: '值得信赖',
company: '恒星科技有限公司',
content: '我们已经连续三年使用该平台,数据处理稳定高效,团队协作体验非常顺畅。',
},
{
title: '安全 · 高效',
company: '博越信息技术',
content: '平台的权限管理做得很好,操作简单清晰,极大提升了内部管理效率。',
},
{
title: '稳定可靠',
company: '远航供应链集团',
content: '系统运行非常稳定,几乎没有出现过宕机情况,客服响应速度也很快。',
},
{
title: '智能化体验',
company: '蓝海数据服务中心',
content: '智能报表和自动化功能非常实用,减少了大量人工整理数据的时间。',
},
{
title: '灵活易用',
company: '中联创投有限公司',
content: 'UI设计简洁友好,新员工上手很快,各项业务流程也能灵活配置。',
},
]
</script>
<template>
<div class="auth-wrapper">
<div class="auth-bg">
<img src="/auth-bg.jpg">
<div class="bg-content">
<el-carousel
height="150px"
indicator-position="none"
>
<el-carousel-item v-for="(item, index) in carouselList" :key="index">
<div class="carousel-item">
<h2 class="title">
{{ item.title }}
</h2>
<span class="content">{{ item.content }}</span>
<span class="company"> - {{ item.company }}</span>
</div>
</el-carousel-item>
</el-carousel>
</div>
</div>
<slot />
</div>
</template>
<style lang="scss" scoped>
.auth-wrapper {
display: flex;
height: 100vh;
overflow: hidden;
background-color: #fff;
.auth-bg {
width: 70%;
height: 100%;
position: relative;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
.bg-content {
position: absolute;
left: 50%;
bottom: 10%;
transform: translateX(-50%);
text-align: center;
color: #fff;
width: 600px;
.carousel-item {
display: flex;
flex-direction: column;
.title {
font-size: 28px;
line-height: 28px;
margin-bottom: 16px;
}
.content {
margin-bottom: 8px;
}
.company {
font-size: 14px;
}
}
}
}
}
</style>
@@ -1,8 +1,11 @@
<script lang="ts" setup>
import type { FormInstance } from 'element-plus'
import { ElMessage } from 'element-plus'
import { reactive, ref } from 'vue'
import { useRouter } from 'vue-router'
import { forgetApi } from '@/request/api/auth'
import AuthWrapper from './components/AuthWrapper.vue'
const router = useRouter()
@@ -10,34 +13,6 @@ function navigateTo(path: string) {
router.push(path)
}
const carouselList = [
{
title: '值得信赖',
company: '恒星科技有限公司',
content: '我们已经连续三年使用该平台,数据处理稳定高效,团队协作体验非常顺畅。',
},
{
title: '安全 · 高效',
company: '博越信息技术',
content: '平台的权限管理做得很好,操作简单清晰,极大提升了内部管理效率。',
},
{
title: '稳定可靠',
company: '远航供应链集团',
content: '系统运行非常稳定,几乎没有出现过宕机情况,客服响应速度也很快。',
},
{
title: '智能化体验',
company: '蓝海数据服务中心',
content: '智能报表和自动化功能非常实用,减少了大量人工整理数据的时间。',
},
{
title: '灵活易用',
company: '中联创投有限公司',
content: 'UI设计简洁友好,新员工上手很快,各项业务流程也能灵活配置。',
},
]
const form = reactive<{
email: string
}>({
@@ -58,39 +33,29 @@ async function submitForm(formEl: FormInstance | undefined) {
await formEl.validate((valid) => {
if (valid) {
submitLoading.value = true
forgetApi({
email: form.email,
}).then((res) => {
if (res.isSuccess) {
ElMessage.success('密码已成功发送至您的邮箱,请注意查收!')
}
else {
ElMessage.error(res.message || '发送失败,请检查邮箱是否正确')
}
}).catch((err) => {
console.log(err)
ElMessage.error('发送失败,请稍后再试')
}).finally(() => {
submitLoading.value = false
})
}
})
}
function onSubmit() {
console.log('submit!')
}
</script>
<template>
<div class="auth-wrapper">
<div class="auth-bg">
<img src="/auth-bg.jpg">
<div class="bg-content">
<el-carousel
height="150px"
indicator-position="none"
>
<el-carousel-item v-for="(item, index) in carouselList" :key="index">
<div class="carousel-item">
<h2 class="title">
{{ item.title }}
</h2>
<span class="content">{{ item.content }}</span>
<span class="company"> - {{ item.company }}</span>
</div>
</el-carousel-item>
</el-carousel>
</div>
</div>
<AuthWrapper>
<div class="login-form">
<img src="/logo.png" class="logo" @click="navigateTo('/')">
@@ -126,121 +91,76 @@ function onSubmit() {
<span class="back" @click="navigateTo('/auth/login')">登录</span>
</div>
</div>
</div>
</AuthWrapper>
</template>
<style lang="scss" scoped>
.auth-wrapper {
.form-label-flex {
display: flex;
height: 100vh;
overflow: hidden;
background-color: #fff;
justify-content: space-between;
width: 100%;
align-items: center;
.auth-bg {
width: 70%;
height: 100%;
position: relative;
.forgot {
color: #0f6fec;
cursor: pointer;
}
}
img {
.login-form {
flex: 1;
display: flex;
flex-direction: column;
padding: 36px;
.logo {
height: 70px;
width: 160px;
cursor: pointer;
}
.main {
flex: 1;
display: flex;
justify-content: center;
flex-direction: column;
:deep(.el-form-item__label) {
font-size: 16px;
width: 100%;
height: 100%;
object-fit: cover;
margin-bottom: 12px;
}
.bg-content {
position: absolute;
left: 50%;
bottom: 10%;
transform: translateX(-50%);
text-align: center;
color: #fff;
width: 600px;
.title {
font-size: 28px;
font-weight: 600;
margin-bottom: 10px;
}
.carousel-item {
display: flex;
flex-direction: column;
.title {
font-size: 28px;
line-height: 28px;
margin-bottom: 16px;
}
.content {
margin-bottom: 8px;
}
.company {
font-size: 14px;
}
}
.title-extra {
font-size: 16px;
color: #666;
margin-bottom: 36px;
}
.form-btn {
width: 100%;
height: 36px;
font-size: 16px;
margin-top: 16px;
}
}
.form-label-flex {
display: flex;
justify-content: space-between;
width: 100%;
align-items: center;
.footer {
font-size: 18px;
color: #999;
height: 48px;
text-align: center;
line-height: 48px;
.forgot {
.back {
color: #0f6fec;
cursor: pointer;
}
}
.login-form {
flex: 1;
display: flex;
flex-direction: column;
padding: 36px;
.logo {
height: 70px;
width: 160px;
cursor: pointer;
}
.main {
flex: 1;
display: flex;
justify-content: center;
flex-direction: column;
:deep(.el-form-item__label) {
font-size: 16px;
width: 100%;
margin-bottom: 12px;
}
.title {
font-size: 28px;
font-weight: 600;
margin-bottom: 10px;
}
.title-extra {
font-size: 16px;
color: #666;
margin-bottom: 36px;
}
.form-btn {
width: 100%;
height: 36px;
font-size: 16px;
margin-top: 16px;
}
}
.footer {
font-size: 18px;
color: #999;
height: 48px;
text-align: center;
line-height: 48px;
.back {
color: #0f6fec;
cursor: pointer;
}
}
}
}
</style>
@@ -1,13 +1,14 @@
<script lang="ts" setup>
import type { FormInstance } from 'element-plus'
import { ElMessage } from 'element-plus'
import { computed, reactive, ref } from 'vue'
import { reactive, ref } from 'vue'
import { useRouter } from 'vue-router'
import EmailCodeInput from '@/components/EmailCodeInput.vue'
import { loginByEmailCodeApi, loginByPasswordApi } from '@/request/api/auth'
import { useUserStore } from '@/store/user'
import { useWebSocket } from '@/store/websocket'
import { AuthCodeChannel, AuthCodePurpose } from '@/types/auth/code'
import AuthWrapper from './components/AuthWrapper.vue'
const websocket = useWebSocket()
const userStore = useUserStore()
@@ -25,34 +26,6 @@ const loginOptions = [
]
const loginMode = ref('0')
const carouselList = [
{
title: '值得信赖',
company: '恒星科技有限公司',
content: '我们已经连续三年使用该平台,数据处理稳定高效,团队协作体验非常顺畅。',
},
{
title: '安全 · 高效',
company: '博越信息技术',
content: '平台的权限管理做得很好,操作简单清晰,极大提升了内部管理效率。',
},
{
title: '稳定可靠',
company: '远航供应链集团',
content: '系统运行非常稳定,几乎没有出现过宕机情况,客服响应速度也很快。',
},
{
title: '智能化体验',
company: '蓝海数据服务中心',
content: '智能报表和自动化功能非常实用,减少了大量人工整理数据的时间。',
},
{
title: '灵活易用',
company: '中联创投有限公司',
content: 'UI设计简洁友好,新员工上手很快,各项业务流程也能灵活配置。',
},
]
const form = reactive<{
email: string
password: string
@@ -157,29 +130,7 @@ function navigateTo(path: string) {
</script>
<template>
<div class="auth-wrapper">
<div class="auth-bg">
<img src="/auth-bg.jpg">
<div class="bg-content">
<el-carousel
height="150px"
indicator-position="none"
>
<el-carousel-item v-for="(item, index) in carouselList" :key="index">
<div class="carousel-item">
<h2 class="title">
{{ item.title }}
</h2>
<span class="content">{{ item.content }}</span>
<span class="company"> - {{ item.company }}</span>
</div>
</el-carousel-item>
</el-carousel>
</div>
</div>
<AuthWrapper>
<div class="login-form">
<img src="/logo.png" class="logo" @click="navigateTo('/')">
@@ -268,142 +219,99 @@ function navigateTo(path: string) {
<span class="back" @click="navigateTo('/auth/register')">注册</span>
</div>
</div>
</div>
</AuthWrapper>
</template>
<style lang="scss" scoped>
.auth-wrapper {
.form-label-flex {
display: flex;
height: 100vh;
overflow: hidden;
background-color: #fff;
.auth-bg {
width: 70%;
height: 100%;
position: relative;
justify-content: space-between;
width: 100%;
align-items: center;
img {
.forgot {
color: #0f6fec;
cursor: pointer;
}
}
.login-form {
flex: 1;
display: flex;
flex-direction: column;
padding: 36px;
.logo {
height: 70px;
width: 160px;
cursor: pointer;
}
.main {
flex: 1;
display: flex;
justify-content: center;
flex-direction: column;
:deep(.el-form-item__label) {
font-size: 16px;
width: 100%;
height: 100%;
object-fit: cover;
margin-bottom: 12px;
}
.email-code {
:deep(.el-input-group__append) {
background-color: #409eff;
color: #fff;
cursor: pointer;
}
.bg-content {
position: absolute;
left: 50%;
bottom: 10%;
transform: translateX(-50%);
text-align: center;
color: #fff;
width: 600px;
.carousel-item {
display: flex;
flex-direction: column;
.title {
font-size: 28px;
line-height: 28px;
margin-bottom: 16px;
}
.content {
margin-bottom: 8px;
}
.company {
font-size: 14px;
&.disabled {
:deep(.el-input-group__append) {
background-color: #f5f7fa;
color: #c0c4cc;
}
}
}
}
.form-label-flex {
display: flex;
justify-content: space-between;
width: 100%;
align-items: center;
.forgot {
.remeber {
:deep(.el-checkbox__inner) {
width: 16px;
height: 16px;
}
:deep(.el-checkbox__label) {
font-size: 16px;
}
}
.title {
font-size: 28px;
font-weight: 600;
margin-bottom: 10px;
}
.title-extra {
font-size: 16px;
color: #666;
margin-bottom: 36px;
}
.form-btn {
width: 100%;
height: 36px;
font-size: 16px;
}
}
.footer {
font-size: 18px;
color: #999;
height: 48px;
text-align: center;
line-height: 48px;
.back {
color: #0f6fec;
cursor: pointer;
}
}
.login-form {
flex: 1;
display: flex;
flex-direction: column;
padding: 36px;
.logo {
height: 70px;
width: 160px;
cursor: pointer;
}
.main {
flex: 1;
display: flex;
justify-content: center;
flex-direction: column;
:deep(.el-form-item__label) {
font-size: 16px;
width: 100%;
margin-bottom: 12px;
}
.email-code {
:deep(.el-input-group__append) {
background-color: #409eff;
color: #fff;
cursor: pointer;
}
&.disabled {
:deep(.el-input-group__append) {
background-color: #f5f7fa;
color: #c0c4cc;
}
}
}
.remeber {
:deep(.el-checkbox__inner) {
width: 16px;
height: 16px;
}
:deep(.el-checkbox__label) {
font-size: 16px;
}
}
.title {
font-size: 28px;
font-weight: 600;
margin-bottom: 10px;
}
.title-extra {
font-size: 16px;
color: #666;
margin-bottom: 36px;
}
.form-btn {
width: 100%;
height: 36px;
font-size: 16px;
}
}
.footer {
font-size: 18px;
color: #999;
height: 48px;
text-align: center;
line-height: 48px;
.back {
color: #0f6fec;
cursor: pointer;
}
}
}
}
</style>
@@ -4,7 +4,6 @@ import type { RegisterAdminForm } from '@/types/auth'
import { ElMessage } from 'element-plus'
import { reactive, ref } from 'vue'
import EmailCodeInput from '@/components/EmailCodeInput.vue'
import { authCodeVerifyApi } from '@/request/api/auth/code'
import { AuthCodeChannel, AuthCodePurpose } from '@/types/auth/code'
import { validatePhone } from '@/utils/validate'
@@ -1,15 +1,24 @@
<script setup lang="ts">
import type { FormInstance } from 'element-plus'
import type { RegisterCompanyForm } from '@/types/auth'
import { reactive, ref } from 'vue'
import { ElMessage } from 'element-plus'
import { computed, reactive, ref } from 'vue'
import AreaSelect from '@/components/AreaSelect.vue'
import EmailCodeInput from '@/components/EmailCodeInput.vue'
import IndustrySelect from '@/components/IndustrySelect.vue'
import { authCodeVerifyApi } from '@/request/api/auth/code'
import { AuthCodeChannel, AuthCodePurpose } from '@/types/auth/code'
import { validateEmail, validatePhone } from '@/utils/validate'
const emit = defineEmits<{
(e: 'submit', value: RegisterCompanyForm): void
}>()
const submitLoading = ref(false)
const isEmailCodeValid = ref(true)
const originEmail = ref('')
const formRef = ref<FormInstance>()
const formRules = {
name: [
@@ -23,6 +32,23 @@ const formRules = {
],
email: [{ validator: emailValidator, trigger: 'blur' }],
phone: [{ validator: phoneValidator, trigger: 'blur' }],
emailCode: [
{
validator: (rule: any, value: any, callback: any) => {
if (!isEmailChanged.value) {
callback()
return
}
if (!value) {
callback(new Error('邮箱验证码不能为空'))
return
}
callback()
},
trigger: ['blur', 'change'],
},
],
}
function emailValidator(rule: any, value: any, callback: any) {
@@ -39,6 +65,7 @@ function emailValidator(rule: any, value: any, callback: any) {
callback()
}
}
function phoneValidator(rule: any, value: any, callback: any) {
// 未填写:直接通过
if (!value) {
@@ -53,6 +80,7 @@ function phoneValidator(rule: any, value: any, callback: any) {
callback()
}
}
const form = reactive<RegisterCompanyForm>({
name: '',
industries: [],
@@ -60,6 +88,11 @@ const form = reactive<RegisterCompanyForm>({
phone: '',
email: '',
profile: '',
emailCode: '',
})
const isEmailChanged = computed(() => {
return form.email !== originEmail.value
})
async function handleSubmit(formEl: FormInstance | undefined) {
@@ -68,7 +101,34 @@ async function handleSubmit(formEl: FormInstance | undefined) {
await formEl.validate((valid) => {
if (valid) {
emit('submit', form)
if (isEmailCodeValid.value) {
emit('submit', form)
return
}
submitLoading.value = true
authCodeVerifyApi({
channel: AuthCodeChannel.EMAIL,
purpose: AuthCodePurpose.REGISTER,
target: form.email,
code: form.emailCode,
}).then((res) => {
if (res.isSuccess) {
emit('submit', form)
isEmailCodeValid.value = true
}
else {
ElMessage.error(res.message || '验证码无效或已过期,请重新获取')
}
}).catch((err) => {
console.log(err)
ElMessage.error('邮箱验证码校验失败,请稍后再试')
isEmailCodeValid.value = false
}).finally(() => {
submitLoading.value = false
})
}
})
}
@@ -117,6 +177,21 @@ async function handleSubmit(formEl: FormInstance | undefined) {
<el-input v-model="form.email" placeholder="请输入邮箱" />
</el-form-item>
</el-col>
<el-col
v-if="isEmailChanged"
:span="24"
style="margin-bottom: 8px;"
>
<el-form-item label="邮箱验证码" prop="emailCode">
<EmailCodeInput
v-model="form.emailCode"
:email="form.email"
:channel="AuthCodeChannel.EMAIL"
:purpose="AuthCodePurpose.REGISTER"
placeholder="请输入邮箱验证码"
/>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="公司简介">
<el-input v-model="form.profile" type="textarea" placeholder="请输入简介" />
@@ -128,7 +203,7 @@ async function handleSubmit(formEl: FormInstance | undefined) {
<div class="button-group">
<div />
<el-button type="primary" @click="handleSubmit(formRef)">
<el-button type="primary" :loading="submitLoading" @click="handleSubmit(formRef)">
下一步
</el-button>
</div>
@@ -2,6 +2,7 @@
import type { RegisterAdminForm, RegisterCompanyForm } from '@/types/auth'
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import AuthWrapper from '../components/AuthWrapper.vue'
import AdminForm from './components/AdminForm.vue'
import CompanyForm from './components/CompanyForm.vue'
import ConfirmForm from './components/ConfirmForm.vue'
@@ -9,34 +10,6 @@ import ResultForm from './components/ResultForm.vue'
const router = useRouter()
const carouselList = [
{
title: '值得信赖',
company: '恒星科技有限公司',
content: '我们已经连续三年使用该平台,数据处理稳定高效,团队协作体验非常顺畅。',
},
{
title: '安全 · 高效',
company: '博越信息技术',
content: '平台的权限管理做得很好,操作简单清晰,极大提升了内部管理效率。',
},
{
title: '稳定可靠',
company: '远航供应链集团',
content: '系统运行非常稳定,几乎没有出现过宕机情况,客服响应速度也很快。',
},
{
title: '智能化体验',
company: '蓝海数据服务中心',
content: '智能报表和自动化功能非常实用,减少了大量人工整理数据的时间。',
},
{
title: '灵活易用',
company: '中联创投有限公司',
content: 'UI设计简洁友好,新员工上手很快,各项业务流程也能灵活配置。',
},
]
const activeStep = ref(0)
const adminForm = ref<RegisterAdminForm>({
@@ -49,6 +22,7 @@ const adminForm = ref<RegisterAdminForm>({
confirmPassword: '',
position: '',
})
const companyForm = ref<RegisterCompanyForm>({
name: '',
industries: [],
@@ -56,6 +30,7 @@ const companyForm = ref<RegisterCompanyForm>({
phone: '',
email: '',
profile: '',
emailCode: '',
})
function handleNextStep() {
@@ -83,29 +58,7 @@ function handleAdminSubmit(data: RegisterAdminForm) {
</script>
<template>
<div class="auth-wrapper">
<div class="auth-bg">
<img src="/auth-bg.jpg">
<div class="bg-content">
<el-carousel
height="150px"
indicator-position="none"
>
<el-carousel-item v-for="(item, index) in carouselList" :key="index">
<div class="carousel-item">
<h2 class="title">
{{ item.title }}
</h2>
<span class="content">{{ item.content }}</span>
<span class="company"> - {{ item.company }}</span>
</div>
</el-carousel-item>
</el-carousel>
</div>
</div>
<AuthWrapper>
<div class="auth-container">
<div class="form-container">
<div>
@@ -190,167 +143,120 @@ function handleAdminSubmit(data: RegisterAdminForm) {
</ul>
</div>
</div>
</div>
</AuthWrapper>
</template>
<style lang="scss" scoped>
.auth-wrapper {
.auth-container {
border: 1px solid #ddd;
background-color: #fff;
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
height: 100%;
width: 100%;
overflow: hidden;
.auth-bg {
width: 40%;
height: 100%;
position: relative;
.form-container {
display: flex;
flex-direction: column;
flex: 1;
padding: 36px;
border-right: 1px solid #e4e4e7;
img {
width: 100%;
height: 100%;
object-fit: cover;
.logo {
height: 70px;
width: 160px;
cursor: pointer;
margin-bottom: 16px;
}
.section-area {
margin-top: 16px;
overflow-y: auto;
overflow-x: hidden;
.section-title {
font-size: 17px;
font-weight: 600;
margin-bottom: 10px;
padding-bottom: 10px;
border-bottom: 1px solid #e4e4e7;
}
.section-subtitle {
color: #71717a;
font-size: 13px;
margin-bottom: 10px;
}
.bg-content {
position: absolute;
left: 50%;
bottom: 10%;
transform: translateX(-50%);
text-align: center;
color: #fff;
width: 600px;
.carousel-item {
.button-group {
display: flex;
flex-direction: column;
.title {
font-size: 28px;
line-height: 28px;
margin-bottom: 16px;
}
.content {
margin-bottom: 8px;
}
.company {
font-size: 14px;
}
justify-content: space-between;
margin-top: 29px;
}
}
}
.auth-container {
border: 1px solid #ddd;
background-color: #fff;
.info-sidebar {
width: 350px;
padding: 28px;
display: flex;
height: 100%;
width: 60%;
flex-direction: column;
background-color: #fafafa;
.form-container {
display: flex;
flex-direction: column;
flex: 1;
padding: 36px;
border-right: 1px solid #e4e4e7;
.logo {
height: 70px;
width: 160px;
cursor: pointer;
margin-bottom: 16px;
}
.section-area {
margin-top: 16px;
overflow-y: auto;
overflow-x: hidden;
.section-title {
font-size: 17px;
font-weight: 600;
margin-bottom: 10px;
padding-bottom: 10px;
border-bottom: 1px solid #e4e4e7;
}
.section-subtitle {
color: #71717a;
font-size: 13px;
margin-bottom: 10px;
}
.button-group {
display: flex;
justify-content: space-between;
margin-top: 29px;
}
}
.info-header {
margin-bottom: 24px;
}
.info-sidebar {
width: 350px;
padding: 28px;
display: flex;
flex-direction: column;
background-color: #fafafa;
.page-title {
font-size: 26px;
font-weight: 700;
letter-spacing: -0.022em;
margin-bottom: 6px;
color: #09090b;
}
.page-description {
font-size: 14px;
line-height: 1.55;
color: #71717a;
}
.info-header {
margin-bottom: 24px;
.info-list {
list-style: none;
flex-grow: 1;
.info-item {
padding: 16px 0;
border-bottom: 1px solid #e4e4e7;
}
.info-item:last-child {
border-bottom: none;
}
.page-title {
font-size: 26px;
font-weight: 700;
letter-spacing: -0.022em;
margin-bottom: 6px;
.info-item-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 9px;
}
.info-title {
font-size: 14px;
font-weight: 600;
color: #09090b;
}
.page-description {
font-size: 14px;
line-height: 1.55;
.info-content {
color: #71717a;
}
.info-list {
list-style: none;
flex-grow: 1;
.info-item {
padding: 16px 0;
border-bottom: 1px solid #e4e4e7;
}
.info-item:last-child {
border-bottom: none;
}
.info-item-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 9px;
}
.info-title {
font-size: 14px;
font-weight: 600;
color: #09090b;
}
.info-content {
color: #71717a;
font-size: 13px;
line-height: 1.5;
}
font-size: 13px;
line-height: 1.5;
}
}
}
.footer {
font-size: 18px;
color: #999;
text-align: center;
.footer {
font-size: 18px;
color: #999;
text-align: center;
.back {
color: #0f6fec;
cursor: pointer;
}
.back {
color: #0f6fec;
cursor: pointer;
}
}
}
@@ -55,10 +55,9 @@ const anchorMenu = ref<AnchorMenuItem[]>([
children: [],
},
])
const activeCollapseNames = ref(['1'])
onMounted(() => {
companyId.value = Number(route.query.id) || 0
companyId.value = Number(route.params.id) || 0
if (!companyId.value) {
router.push('/')
@@ -52,12 +52,7 @@ function getCompanyList() {
}
function toDetail(data: Company) {
router.push({
path: '/company/detail',
query: {
id: data.id,
},
})
router.push(`/company/detail/${data.id}`)
}
</script>
@@ -93,32 +88,30 @@ function toDetail(data: Company) {
</div>
<p class="company-profile">
{{ item.profile || '-' }}
{{ item.profile }}
</p>
<div class="company-footer">
<div class="company-details">
<div class="detail-item">
<SvgIcon icon="material-symbols:location-on" />
<span>{{ item.areas.join('/') || '___' }}</span>
<span>{{ item.areas.join('/') }}</span>
</div>
<div class="detail-item">
<div v-if="item.email" class="detail-item">
<SvgIcon icon="material-symbols:mail" />
<span>{{ item.email || '___' }}</span>
<span>{{ item.email }}</span>
</div>
<div class="detail-item">
<div v-if="item.phone" class="detail-item">
<SvgIcon icon="material-symbols:call" />
<span>{{ item.phone || '___' }}</span>
<span>{{ item.phone }}</span>
</div>
</div>
<div class="company-actions">
<ChatCompanyUserCard :company-id="item.id">
<el-button
type="primary"
size="small"
>
<i-ri:message-3-fill />
<span class="button-text">
发起聊天
</span>
</el-button>
</ChatCompanyUserCard>
</div>
@@ -182,7 +175,7 @@ function toDetail(data: Company) {
.company-item {
background-color: #fff;
border-radius: 4px;
padding: 13px 26px;
padding: 12px 26px;
display: flex;
align-items: center;
border-bottom: 1px solid #e0e0e0;
@@ -213,11 +206,11 @@ function toDetail(data: Company) {
font-weight: 600;
color: #1e293b;
cursor: pointer;
line-height: 1;
}
}
.company-tags {
margin-bottom: 6px;
> *:not(:last-child) {
margin-right: 16px;
}
@@ -226,7 +219,6 @@ function toDetail(data: Company) {
.company-profile {
color: #64748b;
line-height: 1.6;
margin-bottom: 8px;
font-size: 14px;
}
@@ -234,6 +226,7 @@ function toDetail(data: Company) {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 12px;
.company-details {
display: flex;
@@ -252,6 +245,7 @@ function toDetail(data: Company) {
.company-actions {
display: flex;
align-items: center;
> * {
margin-left: 12px;
}
@@ -1,11 +1,13 @@
<script lang="ts" setup>
import type { RecommendCompany, RecommendHotCompany } from '@/types/company'
import { onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import QueryCompany from '@/components/QueryCompany.vue'
import { getRecommendCompanyApi, getRecommendHotCompanyApi} from '@/request/api/company'
import { getRecommendCompanyApi, getRecommendHotCompanyApi } from '@/request/api/company'
import { useCommonStore } from '@/store/common'
import Recommend from './components/recommend.vue'
const router = useRouter()
const commonStore = useCommonStore()
const carouselList = [
@@ -29,6 +31,10 @@ onMounted(() => {
}
})
})
function handleSearch() {
router.push('/company')
}
</script>
<template>
@@ -47,7 +53,7 @@ onMounted(() => {
</div>
<div class="query">
<QueryCompany v-model="commonStore.companyQuery" :industries="commonStore.industryOptions" :areas="commonStore.areaOptions" />
<QueryCompany v-model="commonStore.companyQuery" :industries="commonStore.industryOptions" :areas="commonStore.areaOptions" @search="handleSearch"/>
</div>
</div>
@@ -1,7 +1,7 @@
<script lang="ts" setup>
import type { ImFriend, ImGroup, ImGroupUser } from '@/types/im'
import { Search } from '@element-plus/icons-vue'
import { computed, nextTick, onMounted, ref } from 'vue'
import { computed, onMounted, ref } from 'vue'
import { useRoute } from 'vue-router'
import CompanyUserProfileDialog from '@/components/CompanyUserProfileDialog.vue'
import { getFriendProfileApi, getGroupProfileApi, queryFriendApi, queryGroupApi, updateGroupUnreadApi, updatePrivateUnreadApi } from '@/request/api/im'
@@ -627,12 +627,12 @@ async function handleStartChat() {
/>
</div>
<div class="im__list-item">
<div class="im__list-header">
<!-- <div class="im__list-header">
<SvgIcon icon="tabler:message-circle" :size="16" />
<span class="im__list-header-title">
单聊
</span>
</div>
</div> -->
<div class="im__list-body" @scroll="onFriendListScroll">
<div
v-for="item in friendState.list" :key="item.friendId" class="im__chat-item"
@@ -673,7 +673,7 @@ async function handleStartChat() {
</div>
</div>
</div>
<div class="im__list-item">
<!-- <div class="im__list-item">
<div class="im__list-header">
<SvgIcon icon="bi:people" :size="16" />
<span class="im__list-header-title">
@@ -715,7 +715,7 @@ async function handleStartChat() {
</div>
</div>
</div>
</div>
</div> -->
</div>
<div v-if="selectedChat.id && selectedChat.type" class="im__window">
@@ -167,7 +167,7 @@ function handleValiteVcodeSuccess() {
})
}
function handleToLogin() {
router.push('/login')
router.push('/auth/login')
}
</script>
@@ -1,13 +1,15 @@
<script setup lang="ts">
import type { AnchorMenuItem } from '@/components/anchor-menu/type'
import type { PostComment, PostCompany, PublicPostDetail } from '@/types/post'
import type { PostAuthorProfile, PostComment, PublicPostDetail } from '@/types/post'
import { ElMessage } from 'element-plus'
import { computed, onMounted, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import AnchorMenu from '@/components/anchor-menu/index.vue'
import CustomEmpty from '@/components/CustomEmpty.vue'
import SafeImage from '@/components/SafeImage.vue'
import WandEditor from '@/components/wang-editor/index.vue'
import { createPostCommentApi, getPostCompanyById, getPublicPostDetailApi, listPostCommentApi } from '@/request/api/post'
import { createFriendApi } from '@/request/api/im'
import { createPostCommentApi, getAuthorProfileByPostId, getPublicPostDetailApi, listPostCommentApi } from '@/request/api/post'
import { useUserStore } from '@/store/user'
import { formatDate, formatPostTime } from '@/utils/time'
import CommentInput from './components/comment-input.vue'
@@ -20,14 +22,22 @@ const userStore = useUserStore()
const id = Number(route.params.id)
const loading = ref<boolean>(true)
const postCompany = ref<PostCompany>({
id: 0,
name: '',
logo: '',
profile: '',
establishmentDate: '',
areas: [],
industries: [],
const authorProfile = ref<PostAuthorProfile>({
user: {
id: 0,
name: '',
avatar: '',
position: '',
},
company: {
id: 0,
name: '',
logo: '',
profile: '',
establishmentDate: '',
areas: [],
industries: [],
},
})
const postDetail = ref<PublicPostDetail>({
@@ -74,27 +84,29 @@ onMounted(async () => {
loading.value = true
try {
// 获取帖子详情
const postRes = await getPublicPostDetailApi(id)
// 使用 Promise.all 来并行请求帖子详情、作者信息和评论信息
const [postRes, authorRes, commentRes] = await Promise.all([
getPublicPostDetailApi(id), // 获取帖子详情
getAuthorProfileByPostId(id), // 获取作者信息
listPostCommentApi(id), // 获取评论信息
])
// 处理帖子详情
if (!postRes.isSuccess) {
throw new Error(postRes.message || '加载帖子失败,请稍后再试')
}
postDetail.value = postRes.data
// 获取公司信息
const companyRes = await getPostCompanyById(postDetail.value.companyId)
if (!companyRes.isSuccess) {
throw new Error(companyRes.message || '加载公司信息失败,请稍后再试')
// 处理作者信息
if (!authorRes.isSuccess) {
throw new Error(authorRes.message || '加载作者信息失败,请稍后再试')
}
authorProfile.value = authorRes.data
postCompany.value = companyRes.data
// 获取评论信息
const commentRes = await listPostCommentApi(postDetail.value.id)
// 处理评论信息
if (!commentRes.isSuccess) {
throw new Error(commentRes.message || '加载评论信息失败,请稍后再试')
}
postComments.value = commentRes.data
}
catch (err: any) {
@@ -162,6 +174,33 @@ function getTotalCommentCount(list: PostComment[]): number {
return count
}
function handleToCompanyDetail(companyId: number) {
router.push({
path: `/company/detail/${companyId}`,
})
}
function handleToPrivateChat() {
if (userStore.user.id === authorProfile.value.user.id) {
ElMessage.warning('无法与自己发起聊天')
return
}
createFriendApi({
friendId: authorProfile.value.user.id,
}).then((res) => {
if (res.isSuccess) {
router.push(`/im/private/${authorProfile.value.user.id}`)
}
else {
ElMessage.warning(res.message || '发起聊天失败,请稍后再试')
}
}).catch((err) => {
console.log(err)
ElMessage.warning('发起聊天失败,请稍后再试')
})
}
</script>
<template>
@@ -202,7 +241,7 @@ function getTotalCommentCount(list: PostComment[]): number {
<WandEditor v-model="postDetail.content" mode="view" @update:directory="handleUpdateEditorDirectory" />
</div>
<div class="post-comment" id="comment">
<div id="comment" class="post-comment">
<div class="divider" />
<div class="post-comment-title">
@@ -226,26 +265,46 @@ function getTotalCommentCount(list: PostComment[]): number {
<div class="post-right">
<el-affix :offset="70">
<div class="post-company">
<el-avatar :src="postCompany.logo || '/no-avatar.png'" :size="70" class="company-logo" />
<div class="company-info">
<h3 class="company-name">
{{ postCompany.name }}
</h3>
<div v-if="postCompany.areas.length > 0" class="company-meta">
<template v-if="postCompany.establishmentDate">
成立于 {{ formatDate(postCompany.establishmentDate, 'YYYY') }} 年 ·
</template>
{{ postCompany.areas.join('/') }}
<div class="post-author">
<div class="post-user">
<SafeImage :size="64" :url="authorProfile.user.avatar" round />
<div class="user-info">
<div class="user-main">
<span>
{{ authorProfile.user.name }}
</span>
<el-button size="small" type="primary" @click="handleToPrivateChat">
发起聊天
</el-button>
</div>
<div class="user-meta">
{{ authorProfile.user.position }}
</div>
</div>
</div>
<div class="company-desc">
{{ postCompany.profile }}
</div>
<el-divider style="margin: 16px 0px;" />
<div class="company-tags">
<span v-for="industry in postCompany.industries" :key="industry" class="tag">{{ industry }}</span>
<div class="post-company">
<div class="company-info">
<h3 class="company-name" @click="handleToCompanyDetail(authorProfile.company.id)">
{{ authorProfile.company.name }}
</h3>
<div v-if="authorProfile.company.areas.length > 0" class="company-meta">
<template v-if="authorProfile.company.establishmentDate">
成立于 {{ formatDate(authorProfile.company.establishmentDate, 'YYYY') }} 年 ·
</template>
{{ authorProfile.company.areas.join('/') }}
</div>
<div class="company-desc">
{{ authorProfile.company.profile }}
</div>
<div class="company-tags">
<span v-for="industry in authorProfile.company.industries" :key="industry" class="tag">{{ industry }}</span>
</div>
</div>
</div>
</div>
@@ -365,18 +424,51 @@ function getTotalCommentCount(list: PostComment[]): number {
width: 360px;
flex-shrink: 0;
.post-company {
.post-author {
background-color: #fff;
padding: 16px;
border-radius: 6px;
}
.post-user {
display: flex;
align-items: flex-start;
margin: 0 auto;
.user-info {
flex: 1;
margin-left: 12px;
}
.user-main {
display: flex;
align-items: center;
justify-content: space-between;
span {
width: 180px;
margin: 0;
font-size: 18px;
font-weight: 600;
color: #222;
cursor: pointer;
word-break: break-all;
margin-right: 4px;
}
}
.user-meta {
margin: 6px 0 10px;
font-size: 13px;
color: #888;
}
}
.post-company {
display: flex;
align-items: flex-start;
padding: 16px;
margin: 0 auto;
border-radius: 6px;
.company-info {
flex: 1;
margin-left: 12px;
}
.company-name {
@@ -384,12 +476,15 @@ function getTotalCommentCount(list: PostComment[]): number {
font-size: 18px;
font-weight: 600;
color: #222;
cursor: pointer;
word-break: break-all;
}
.company-meta {
margin: 6px 0 10px;
font-size: 13px;
color: #888;
word-break: break-all;
}
.company-desc {
@@ -397,6 +492,8 @@ function getTotalCommentCount(list: PostComment[]): number {
line-height: 1.6;
color: #555;
margin-bottom: 12px;
word-break: break-all;
}
.company-tags {
@@ -3,7 +3,7 @@ import type { PostTag, PublicPost, QueryPublicPostParams } from '@/types/post'
import { Search } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import { onMounted, reactive, ref, watch } from 'vue'
import { onMounted, reactive, ref } from 'vue'
import { useRouter } from 'vue-router'
import CustomEmpty from '@/components/CustomEmpty.vue'
import CustomLoading from '@/components/CustomLoading.vue'
@@ -88,6 +88,12 @@ function toDetail(data: PublicPost) {
path: `/post/detail/${data.id}`,
})
}
function handleToCompanyDetail(data: PublicPost) {
router.push({
path: `/company/detail/${data.companyId}`,
})
}
</script>
<template>
@@ -146,7 +152,7 @@ function toDetail(data: PublicPost) {
<div class="post-meta">
<div class="post-meta-actions">
<div class="post-meta-item">
<div class="post-meta-company">
<div class="post-meta-company" @click="handleToCompanyDetail(item)">
{{ item.companyName }}
</div>
</div>
@@ -188,14 +194,15 @@ function toDetail(data: PublicPost) {
height: 100%;
display: flex;
padding: 0px 240px;
background-color: #fff;
// background-color: #fff;
.post-sidebar {
background-color: #fff;
height: calc(100vh - var(--header-height));
font-size: 16px;
flex-shrink: 0;
border-right: 1px solid rgb(232.9, 232.9, 232.9);
margin-right: 36px;
// border-right: 1px solid rgb(232.9, 232.9, 232.9);
position: sticky;
top: var(--header-height);
align-self: flex-start;
@@ -248,6 +255,8 @@ function toDetail(data: PublicPost) {
flex: 1;
width: 100%;
flex-shrink: 0;
min-width: 600px;
.post-main-header {
position: sticky;
top: calc(var(--header-height));
@@ -263,6 +272,8 @@ function toDetail(data: PublicPost) {
.post-content {
flex: 1;
padding-bottom: 36px;
background-color: #fff;
flex-shrink: 0;
}
.post-list {
@@ -279,7 +290,7 @@ function toDetail(data: PublicPost) {
.post-title {
font-weight: 500;
font-size: 16px;
font-size: 18px;
line-height: 24px;
color: #252933;
width: 100%;
@@ -319,6 +330,7 @@ function toDetail(data: PublicPost) {
.post-meta-company {
color: #0053a1;
cursor: pointer;
}
}
@@ -24,7 +24,7 @@ const loading = reactive({
confirm: false,
})
const showPushDrawer = ref<boolean>(false)
const showPushDialog = ref<boolean>(false)
const imageUpload = ref<UploadImageResponse | null>(null)
const statusOptions = [
@@ -45,7 +45,7 @@ const oldForm = ref<PostEditorForm>({
tags: [],
publishTime: '',
updateTime: '',
status: 0,
status: PostStatusEnum.PUBLISHED,
})
const form = ref<PostEditorForm>({
@@ -55,7 +55,7 @@ const form = ref<PostEditorForm>({
tags: [],
publishTime: '',
updateTime: '',
status: 0,
status: PostStatusEnum.PUBLISHED,
})
const formRules = {
@@ -98,9 +98,9 @@ onMounted(() => {
})
})
async function handleShowPushDrawer() {
async function handleShowPushDialog() {
loading.init = true
showPushDrawer.value = true
showPushDialog.value = true
loading.init = false
}
@@ -144,7 +144,7 @@ async function handleSave(formEl?: FormInstance) {
ElMessage.success(isUpdate.value ? '更新成功' : '保存成功')
form.value = res.data
showPushDrawer.value = false
showPushDialog.value = false
}
catch (err) {
console.error(err)
@@ -155,6 +155,10 @@ async function handleSave(formEl?: FormInstance) {
}
})
}
function handleClosePushDialog() {
showPushDialog.value = false
}
</script>
<template>
@@ -169,7 +173,7 @@ async function handleSave(formEl?: FormInstance) {
<div v-if="form.updateTime" class="time">
更新于:{{ form.updateTime }}
</div>
<el-button type="primary" @click="handleShowPushDrawer()">
<el-button type="primary" @click="handleShowPushDialog()">
发布
</el-button>
</div>
@@ -177,7 +181,7 @@ async function handleSave(formEl?: FormInstance) {
<WangEditor ref="wangEditorRef" v-model="form.content" :show-direcotry="true" />
</div>
<el-drawer v-model="showPushDrawer" title="发布文章">
<el-dialog v-model="showPushDialog" title="发布文章" width="800">
<CustomLoading v-if="loading.init" :in-container="true" />
<el-form
@@ -243,7 +247,7 @@ async function handleSave(formEl?: FormInstance) {
<template #footer>
<div style="flex: auto">
<el-button>
<el-button @click="handleClosePushDialog">
取消
</el-button>
<el-button type="primary" :loading="loading.confirm" @click="handleSave(formRef)">
@@ -251,7 +255,7 @@ async function handleSave(formEl?: FormInstance) {
</el-button>
</div>
</template>
</el-drawer>
</el-dialog>
</template>
<style lang="scss" scoped>
@@ -143,7 +143,7 @@ function handleDeleteRow(row: Post) {
try {
instance.confirmButtonLoading = true
const res = await deletePostApi({ id: row.id })
const res = await deletePostApi(row.id)
if (!res.isSuccess) {
ElMessage.error(res.message || '删除失败,请稍后再试')
@@ -216,12 +216,15 @@ function handleUpdateToDraft(row: Post) {
function handleToEdit(row?: Post) {
const query = row ? { id: row.id } : undefined
router.push({
path: '/settings/creator/editor',
query,
})
}
function handleToDetail(row: Post) {
router.push(`/post/detail/${row.id}`)
}
</script>
<template>
@@ -313,7 +316,7 @@ function handleToEdit(row?: Post) {
<el-dropdown-item @click="handleToEdit(item)">
编辑
</el-dropdown-item>
<el-dropdown-item v-if="item.status !== PostStatusEnum.DRAFT" @click="handleToEdit(item)">
<el-dropdown-item v-if="item.status !== PostStatusEnum.DRAFT" @click="handleToDetail(item)">
详细
</el-dropdown-item>
<el-dropdown-item v-if="item.status !== PostStatusEnum.DRAFT" @click="handleUpdateToDraft(item)">
@@ -482,7 +485,7 @@ function handleToEdit(row?: Post) {
word-break: break-word;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2; // 限制为两行
-webkit-line-clamp: 2;
overflow: hidden;
}
@@ -3,13 +3,16 @@ import type { NavMenuItem } from '@/components/nav-menu/type'
import { computed, nextTick, ref, watch } from 'vue'
import { useRoute } from 'vue-router'
import NavMenu from '@/components/nav-menu/index.vue'
import { useUserStore } from '@/store/user'
const userStore = useUserStore()
const searchKey = ref('')
const route = useRoute()
const menuRef = ref()
const menuList: NavMenuItem [] = [
const menuList = ref<NavMenuItem[]>([
{
label: '资料设置',
index: '/settings/info',
@@ -19,11 +22,13 @@ const menuList: NavMenuItem [] = [
label: '公司信息设置',
index: '/settings/info/company',
icon: 'hugeicons:building-02',
disabled: userStore.user.role === 1,
},
{
label: '成员设置',
index: '/settings/info/user',
icon: 'hugeicons:user-settings-01',
disabled: userStore.user.role === 1,
},
],
},
@@ -49,7 +54,7 @@ const menuList: NavMenuItem [] = [
},
],
},
]
])
/**
* 递归过滤菜单
@@ -81,7 +86,7 @@ function filterMenu(
}
const filteredMenu = computed(() =>
filterMenu(menuList, searchKey.value.trim()),
filterMenu(menuList.value, searchKey.value.trim()),
)
function collectOpeneds(menu: NavMenuItem[], result: string[] = []) {
for (const item of menu) {
@@ -149,13 +154,12 @@ watch(searchKey, async (val) => {
top: var(--header-height);
align-self: flex-start;
width: 240px;
padding: 0px 16px;
// padding: 0px 16px;
:deep(.el-menu) {
border-right: none;
.el-menu-item {
border-radius: 8px;
// border-radius: 8px;
margin: 4px 0px;
font-size: 16px;
&:hover {
@@ -164,16 +168,17 @@ watch(searchKey, async (val) => {
}
.el-sub-menu {
margin: 4px 0px;
// margin: 4px 0px;
&.is-active {
.el-sub-menu__title {
background-color: #f1f5f9;
font-weight: bold;
}
}
}
.el-sub-menu__title {
border-radius: 8px;
// border-radius: 8px;
font-size: 16px;
&:hover {
@@ -189,7 +194,7 @@ watch(searchKey, async (val) => {
}
.right-view {
padding:0px 120px;
padding: 0px 120px;
flex: 1;
flex-shrink: 0;
border-radius: 0px;
@@ -254,7 +254,7 @@ async function handleSubmit(formEl: FormInstance | undefined) {
<el-form-item label="头像" prop="avatar">
<ImageUploadCard
v-model="form.avatar"
@change="(val) => {
@change="(val:any) => {
imageUpload = val
form.avatar = val?.url || ''