This commit is contained in:
oneao committed 2025-12-22 22:30:20 +08:00
1 parent 9aa4a72321
commit 9bf7c40b70
40 files changed
+918 -189

No files matched your search

@@ -17,6 +17,7 @@ public class AutoMetaObjectHandler implements MetaObjectHandler {
if (ObjectUtils.isNotEmpty(userId) && userId > 0){
this.strictInsertFill(metaObject, "createBy", Long.class, userId);
this.strictUpdateFill(metaObject, "updateBy", Long.class, userId);
}
Date date = new Date();
@@ -1,14 +0,0 @@
package cn.link56.forum.service;
import cn.link56.forum.mapper.PostMapper;
import cn.link56.forum.model.po.PostPO;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@Service
public class PostService extends ServiceImpl<PostMapper, PostPO> {
@Resource
private PostMapper postMapper;
}
@@ -1,14 +0,0 @@
package cn.link56.forum.service;
import cn.link56.forum.mapper.PostTagMapper;
import cn.link56.forum.model.po.PostTagPO;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@Service
public class PostTagService extends ServiceImpl<PostTagMapper, PostTagPO> {
@Resource
private PostTagMapper postTagMapper;
}
@@ -0,0 +1,27 @@
package cn.link56.post.controller;
import cn.link56.common.Result;
import cn.link56.post.model.dto.PostAddDTO;
import cn.link56.post.model.vo.PostEditVO;
import cn.link56.post.model.vo.PostTagVO;
import cn.link56.post.service.PostService;
import cn.link56.post.service.PostTagService;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.List;
@RestController
@RequestMapping("/post")
public class PostController {
@Resource
private PostService postService;
@PostMapping("/add")
public Result<PostEditVO> addPost(@RequestBody @Validated PostAddDTO postAddDTO) {
PostEditVO postEditVO = postService.addPost(postAddDTO);
return Result.success(postEditVO);
}
}
@@ -0,0 +1,27 @@
package cn.link56.post.controller;
import cn.link56.common.Result;
import cn.link56.post.mapper.PostTagMapper;
import cn.link56.post.model.vo.PostTagVO;
import cn.link56.post.service.PostTagService;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.List;
@RestController
@RequestMapping("/post/tag")
public class PostTagController {
@Resource
private PostTagService postTagService;
@GetMapping
public Result<List<PostTagVO>> listTags(){
List<PostTagVO> list = postTagService.listTags();
return Result.success(list);
}
}
@@ -1,6 +1,6 @@
package cn.link56.forum.mapper;
package cn.link56.post.mapper;
import cn.link56.forum.model.po.PostCommentPO;
import cn.link56.post.model.po.PostCommentPO;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
@@ -1,6 +1,6 @@
package cn.link56.forum.mapper;
package cn.link56.post.mapper;
import cn.link56.forum.model.po.PostFavoritePO;
import cn.link56.post.model.po.PostFavoritePO;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
@@ -1,6 +1,6 @@
package cn.link56.forum.mapper;
package cn.link56.post.mapper;
import cn.link56.forum.model.po.PostLikePO;
import cn.link56.post.model.po.PostLikePO;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
@@ -1,6 +1,6 @@
package cn.link56.forum.mapper;
package cn.link56.post.mapper;
import cn.link56.forum.model.po.PostPO;
import cn.link56.post.model.po.PostPO;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
@@ -1,9 +1,12 @@
package cn.link56.forum.mapper;
package cn.link56.post.mapper;
import cn.link56.forum.model.po.PostTagPO;
import cn.link56.post.model.po.PostTagPO;
import cn.link56.post.model.vo.PostTagVO;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface PostTagMapper extends BaseMapper<PostTagPO> {
}
@@ -1,6 +1,6 @@
package cn.link56.forum.mapper;
package cn.link56.post.mapper;
import cn.link56.forum.model.po.PostViewPO;
import cn.link56.post.model.po.PostViewPO;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
@@ -0,0 +1,24 @@
package cn.link56.post.model.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.NotBlank;
import java.util.List;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PostAddDTO {
@NotBlank(message = "帖子标题不能为空")
private String title;
@NotBlank(message = "帖子内容不能为空")
private String content;
@NotBlank(message = "帖子摘要不能为空")
private String summary;
private String cover;
private List<String> tags;
private Integer status;
}
@@ -1,4 +1,4 @@
package cn.link56.forum.model.po;
package cn.link56.post.model.po;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
@@ -1,4 +1,4 @@
package cn.link56.forum.model.po;
package cn.link56.post.model.po;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
@@ -1,4 +1,4 @@
package cn.link56.forum.model.po;
package cn.link56.post.model.po;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
@@ -1,12 +1,16 @@
package cn.link56.forum.model.po;
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.fasterxml.jackson.annotation.JsonFormat;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import net.sf.jsqlparser.expression.DateTimeLiteralExpression;
import java.util.Date;
@Data
@AllArgsConstructor
@@ -28,33 +32,39 @@ public class PostPO {
@TableField("b_summary")
private String summary;
@TableField("b_tags")
private String tags;
@TableField("b_tag")
private String tag;
@TableField("b_cover")
private String cover;
@TableField("b_publish_time")
private String publishTime;
@TableField("b_status")
private Integer status;
@TableField(
value = "b_create_user_id",
value = "b_create_by",
fill = FieldFill.INSERT
)
private Long createUserId;
private Long createBy;
@TableField(
value = "b_create_time",
fill = FieldFill.INSERT
)
private Long createTime;
private Date createTime;
@TableField(
value = "b_update_user_id",
value = "b_update_by",
fill = FieldFill.INSERT_UPDATE
)
private Long updateUserId;
private Long updateBy;
@TableField(
value = "b_update_time",
fill = FieldFill.INSERT_UPDATE
)
private Long updateTime;
private Date updateTime;
}
@@ -1,4 +1,4 @@
package cn.link56.forum.model.po;
package cn.link56.post.model.po;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
@@ -1,4 +1,4 @@
package cn.link56.forum.model.po;
package cn.link56.post.model.po;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
@@ -0,0 +1,24 @@
package cn.link56.post.model.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
import java.util.List;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PostEditVO {
private Long id;
private String title;
private String content;
private String summary;
private String cover;
private List<String> tags;
private Integer status;
private Date publishTime;
private Date updateTime;
}
@@ -0,0 +1,13 @@
package cn.link56.post.model.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PostTagVO {
private Long id;
private String name;
}
@@ -1,7 +1,7 @@
package cn.link56.forum.service;
package cn.link56.post.service;
import cn.link56.forum.mapper.PostCommentMapper;
import cn.link56.forum.model.po.PostCommentPO;
import cn.link56.post.mapper.PostCommentMapper;
import cn.link56.post.model.po.PostCommentPO;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
@@ -1,7 +1,7 @@
package cn.link56.forum.service;
package cn.link56.post.service;
import cn.link56.forum.mapper.PostFavoriteMapper;
import cn.link56.forum.model.po.PostFavoritePO;
import cn.link56.post.mapper.PostFavoriteMapper;
import cn.link56.post.model.po.PostFavoritePO;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
@@ -1,7 +1,7 @@
package cn.link56.forum.service;
package cn.link56.post.service;
import cn.link56.forum.mapper.PostLikeMapper;
import cn.link56.forum.model.po.PostLikePO;
import cn.link56.post.mapper.PostLikeMapper;
import cn.link56.post.model.po.PostLikePO;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
@@ -0,0 +1,39 @@
package cn.link56.post.service;
import cn.link56.common.context.UserContext;
import cn.link56.post.mapper.PostMapper;
import cn.link56.post.model.dto.PostAddDTO;
import cn.link56.post.model.po.PostPO;
import cn.link56.post.model.vo.PostEditVO;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
@Service
public class PostService extends ServiceImpl<PostMapper, PostPO> {
@Resource
private PostMapper postMapper;
@Transactional
public PostEditVO addPost(PostAddDTO postAddDTO) {
PostPO postPO = new PostPO();
BeanUtils.copyProperties(postAddDTO, postPO);
postPO.setTag(StringUtils.join(postAddDTO.getTags(), ","));
postPO.setCompanyId(UserContext.getCompanyId());
postMapper.insert(postPO);
PostEditVO postEditVO = new PostEditVO();
BeanUtils.copyProperties(postPO, postEditVO);
postEditVO.setTags(postAddDTO.getTags());
return postEditVO;
}
}
@@ -0,0 +1,33 @@
package cn.link56.post.service;
import cn.link56.post.mapper.PostTagMapper;
import cn.link56.post.model.po.PostTagPO;
import cn.link56.post.model.vo.PostTagVO;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.List;
import java.util.stream.Collectors;
@Service
public class PostTagService extends ServiceImpl<PostTagMapper, PostTagPO> {
@Resource
private PostTagMapper postTagMapper;
public List<PostTagVO> listTags() {
LambdaQueryWrapper<PostTagPO> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(PostTagPO::getCanuse,"1");
List<PostTagPO> postTagPOS = postTagMapper.selectList(wrapper);
return postTagPOS.stream().map(postTagPO -> {
PostTagVO postTagVO = new PostTagVO();
BeanUtils.copyProperties(postTagPO, postTagVO);
return postTagVO;
}).collect(Collectors.toList());
}
}
@@ -1,7 +1,7 @@
package cn.link56.forum.service;
package cn.link56.post.service;
import cn.link56.forum.mapper.PostViewMapper;
import cn.link56.forum.model.po.PostViewPO;
import cn.link56.post.mapper.PostViewMapper;
import cn.link56.post.model.po.PostViewPO;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
@@ -16,6 +16,9 @@ logging:
spring:
profiles:
active: dev
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
mail:
host: smtp.163.com #邮箱服务器地址
username: hi_itao@163.com #邮箱账号
@@ -12,10 +12,12 @@
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.2",
"@vueuse/core": "^14.1.0",
"@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "^5.1.12",
"axios": "^1.13.2",
"dayjs": "^1.11.19",
"echarts": "^6.0.0",
"element-plus": "^2.12.0",
"pinia": "^3.0.4",
"pinia-plugin-persistedstate": "^4.7.1",
+57 -17
View File
@@ -11,6 +11,9 @@ importers:
'@element-plus/icons-vue':
specifier: ^2.3.2
version: 2.3.2(vue@3.5.25(typescript@5.9.3))
'@vueuse/core':
specifier: ^14.1.0
version: 14.1.0(vue@3.5.25(typescript@5.9.3))
'@wangeditor/editor':
specifier: ^5.1.23
version: 5.1.23
@@ -23,6 +26,9 @@ importers:
dayjs:
specifier: ^1.11.19
version: 1.11.19
echarts:
specifier: ^6.0.0
version: 6.0.0
element-plus:
specifier: ^2.12.0
version: 2.12.0(vue@3.5.25(typescript@5.9.3))
@@ -496,42 +502,36 @@ packages:
engines: {node: '>= 10.0.0'}
cpu: [arm]
os: [linux]
libc: [glibc]
'@parcel/watcher-linux-arm-musl@2.5.1':
resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==}
engines: {node: '>= 10.0.0'}
cpu: [arm]
os: [linux]
libc: [musl]
'@parcel/watcher-linux-arm64-glibc@2.5.1':
resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==}
engines: {node: '>= 10.0.0'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@parcel/watcher-linux-arm64-musl@2.5.1':
resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==}
engines: {node: '>= 10.0.0'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@parcel/watcher-linux-x64-glibc@2.5.1':
resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==}
engines: {node: '>= 10.0.0'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@parcel/watcher-linux-x64-musl@2.5.1':
resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==}
engines: {node: '>= 10.0.0'}
cpu: [x64]
os: [linux]
libc: [musl]
'@parcel/watcher-win32-arm64@2.5.1':
resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==}
@@ -596,67 +596,56 @@ packages:
resolution: {integrity: sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==}
cpu: [arm]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm-musleabihf@4.53.3':
resolution: {integrity: sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==}
cpu: [arm]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-arm64-gnu@4.53.3':
resolution: {integrity: sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm64-musl@4.53.3':
resolution: {integrity: sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-loong64-gnu@4.53.3':
resolution: {integrity: sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==}
cpu: [loong64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-ppc64-gnu@4.53.3':
resolution: {integrity: sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-gnu@4.53.3':
resolution: {integrity: sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-musl@4.53.3':
resolution: {integrity: sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==}
cpu: [riscv64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-s390x-gnu@4.53.3':
resolution: {integrity: sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-gnu@4.53.3':
resolution: {integrity: sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-musl@4.53.3':
resolution: {integrity: sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==}
cpu: [x64]
os: [linux]
libc: [musl]
'@rollup/rollup-openharmony-arm64@4.53.3':
resolution: {integrity: sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==}
@@ -732,6 +721,9 @@ packages:
'@types/web-bluetooth@0.0.16':
resolution: {integrity: sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==}
'@types/web-bluetooth@0.0.21':
resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==}
'@typescript-eslint/eslint-plugin@8.49.0':
resolution: {integrity: sha512-JXij0vzIaTtCwu6SxTh8qBc66kmf1xs7pI4UOiMDFVct6q86G0Zs7KRcEoJgY3Cav3x5Tq0MF5jwgpgLqgKG3A==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -897,12 +889,25 @@ packages:
vue:
optional: true
'@vueuse/core@14.1.0':
resolution: {integrity: sha512-rgBinKs07hAYyPF834mDTigH7BtPqvZ3Pryuzt1SD/lg5wEcWqvwzXXYGEDb2/cP0Sj5zSvHl3WkmMELr5kfWw==}
peerDependencies:
vue: ^3.5.0
'@vueuse/core@9.13.0':
resolution: {integrity: sha512-pujnclbeHWxxPRqXWmdkKV5OX4Wk4YeK7wusHqRwU0Q7EFusHoqNA/aPhB6KCh9hEqJkLAJo7bb0Lh9b+OIVzw==}
'@vueuse/metadata@14.1.0':
resolution: {integrity: sha512-7hK4g015rWn2PhKcZ99NyT+ZD9sbwm7SGvp7k+k+rKGWnLjS/oQozoIZzWfCewSUeBmnJkIb+CNr7Zc/EyRnnA==}
'@vueuse/metadata@9.13.0':
resolution: {integrity: sha512-gdU7TKNAUVlXXLbaF+ZCfte8BjRJQWPCa2J55+7/h+yDtzw3vOoGQDRXzI6pyKyo6bXFT5/QoPE4hAknExjRLQ==}
'@vueuse/shared@14.1.0':
resolution: {integrity: sha512-EcKxtYvn6gx1F8z9J5/rsg3+lTQnvOruQd8fUecW99DCK04BkWD7z5KQ/wTAx+DazyoEE9dJt/zV8OIEQbM6kw==}
peerDependencies:
vue: ^3.5.0
'@vueuse/shared@9.13.0':
resolution: {integrity: sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw==}
@@ -1203,6 +1208,9 @@ packages:
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
engines: {node: '>= 0.4'}
echarts@6.0.0:
resolution: {integrity: sha512-Tte/grDQRiETQP4xz3iZWSvoHrkCQtwqd6hs+mifXcjrCuo2iKWbajFObuLJVBlDIJlOzgQPd1hsaKt/3+OMkQ==}
electron-to-chromium@1.5.267:
resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==}
@@ -2277,6 +2285,9 @@ packages:
peerDependencies:
typescript: '>=4.0.0'
tslib@2.3.0:
resolution: {integrity: sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==}
type-check@0.4.0:
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
engines: {node: '>= 0.8.0'}
@@ -2474,6 +2485,9 @@ packages:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'}
zrender@6.0.0:
resolution: {integrity: sha512-41dFXEEXuJpNecuUQq6JlbybmnHaqqpGlbH1yxnA5V9MMP4SbohSVZsJIwz+zdjQXSSlR1Vc34EgH1zxyTDvhg==}
zwitch@2.0.4:
resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
@@ -2974,6 +2988,8 @@ snapshots:
'@types/web-bluetooth@0.0.16': {}
'@types/web-bluetooth@0.0.21': {}
'@typescript-eslint/eslint-plugin@8.49.0(@typescript-eslint/parser@8.49.0(eslint@9.39.1)(typescript@5.9.3))(eslint@9.39.1)(typescript@5.9.3)':
dependencies:
'@eslint-community/regexpp': 4.12.2
@@ -3213,6 +3229,13 @@ snapshots:
typescript: 5.9.3
vue: 3.5.25(typescript@5.9.3)
'@vueuse/core@14.1.0(vue@3.5.25(typescript@5.9.3))':
dependencies:
'@types/web-bluetooth': 0.0.21
'@vueuse/metadata': 14.1.0
'@vueuse/shared': 14.1.0(vue@3.5.25(typescript@5.9.3))
vue: 3.5.25(typescript@5.9.3)
'@vueuse/core@9.13.0(vue@3.5.25(typescript@5.9.3))':
dependencies:
'@types/web-bluetooth': 0.0.16
@@ -3223,8 +3246,14 @@ snapshots:
- '@vue/composition-api'
- vue
'@vueuse/metadata@14.1.0': {}
'@vueuse/metadata@9.13.0': {}
'@vueuse/shared@14.1.0(vue@3.5.25(typescript@5.9.3))':
dependencies:
vue: 3.5.25(typescript@5.9.3)
'@vueuse/shared@9.13.0(vue@3.5.25(typescript@5.9.3))':
dependencies:
vue-demi: 0.14.10(vue@3.5.25(typescript@5.9.3))
@@ -3521,6 +3550,11 @@ snapshots:
es-errors: 1.3.0
gopd: 1.2.0
echarts@6.0.0:
dependencies:
tslib: 2.3.0
zrender: 6.0.0
electron-to-chromium@1.5.267: {}
element-plus@2.12.0(vue@3.5.25(typescript@5.9.3)):
@@ -4818,6 +4852,8 @@ snapshots:
picomatch: 4.0.3
typescript: 5.9.3
tslib@2.3.0: {}
type-check@0.4.0:
dependencies:
prelude-ls: 1.2.1
@@ -4975,4 +5011,8 @@ snapshots:
yocto-queue@0.1.0: {}
zrender@6.0.0:
dependencies:
tslib: 2.3.0
zwitch@2.0.4: {}
@@ -0,0 +1,147 @@
<script setup lang="ts">
import type { UploadImageResponse } from '@/utils/file'
import { Delete } from '@element-plus/icons-vue'
import { ref } from 'vue'
import { uploadImage, uploadToAliyun } from '@/utils/file'
import SvgIcon from './SvgIcon.vue'
const props = defineProps({
modelValue: {
type: String,
required: true,
},
autoUploadAliyun: {
type: Boolean,
required: false,
default: false,
},
})
const emit = defineEmits<{
(e: 'update:modelValue', val: string): void
(e: 'change', val: UploadImageResponse | null): void
}>()
const loading = ref(false)
async function handleClickUploadImage() {
if (loading.value)
return
try {
const res = await uploadImage()
loading.value = true
if (props.autoUploadAliyun) {
const aliyunRes = await uploadToAliyun(res.blob, res.name, res.size)
emit('update:modelValue', aliyunRes.url)
}
else {
emit('update:modelValue', res.url)
}
emit('change', res)
}
finally {
loading.value = false
}
}
function handleRemove(e: MouseEvent) {
e.stopPropagation()
emit('update:modelValue', '')
emit('change', null)
}
</script>
<template>
<div class="upload-card" :class="{ loading }" @click="handleClickUploadImage">
<div
v-if="props.modelValue && !loading"
class="remove-icon"
@click="handleRemove"
>
<el-icon><Delete /></el-icon>
</div>
<div v-if="!props.modelValue && !loading" class="upload-inner">
<SvgIcon icon="material-symbols:arrow-upload-ready-rounded" class="upload-icon" />
<div class="upload-text">
点击上传
</div>
</div>
<div v-else-if="loading" class="upload-inner">
<SvgIcon icon="line-md:loading-twotone-loop" class="upload-icon" />
<div class="upload-text">
上传中...
</div>
</div>
<img v-else :src="props.modelValue" alt="已上传图片" class="upload-image">
</div>
</template>
<style lang="scss" scoped>
.upload-card {
position: relative;
width: 120px;
height: 120px;
border: 1px dashed #d9d9d9;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
background-color: #fafafa;
cursor: pointer;
transition: border-color 0.3s ease;
padding: 4px;
&:hover {
border-color: #409eff;
}
&.loading {
pointer-events: none;
opacity: 0.6;
}
.remove-icon {
position: absolute;
top: 4px;
right: 4px;
width: 25px;
height: 25px;
border-radius: 50%;
background: rgba(0, 0, 0, 0.6);
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
opacity: 1;
cursor: pointer;
z-index: 2;
&:hover {
background-color: red;
}
}
.upload-inner {
color: #999;
display: flex;
flex-direction: column;
align-items: center;
}
.upload-image {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 4px;
}
.upload-icon {
font-size: 24px;
}
}
</style>
@@ -67,11 +67,11 @@ onMounted(() => {
})
const toolbarClass = computed(() => {
return props.mode === 'view' ? 'toolbar-view' : 'toolbar-edit'
return props.mode === 'view' ? 'wang-toolbar-view' : 'wang-toolbar-edit'
})
const editorClass = computed(() => {
return props.mode === 'view' ? 'editor-view' : 'editor-edit'
return props.mode === 'view' ? 'wang-editor-view' : 'wang-editor-edit'
})
// 编辑器实例,必须用 shallowRef
@@ -210,13 +210,13 @@ function doGenerateDirectory() {
</script>
<template>
<div class="editor-container">
<div class="wang-editor-container">
<div class="main">
<Toolbar v-if="props.mode === 'edit'" :class="toolbarClass" class="toolbar" :editor="editorRef" :default-config="toolbarConfig" />
<Editor key="main" v-model="valueHtml" :class="editorClass" class="editor" :default-config="editorConfig" @on-change="handleChange" @on-created="handleCreated" />
</div>
<div v-if="showDirecotry" class="directory">
<div v-if="showDirecotry" class="wang-editor-directory">
<el-collapse v-model="activeCollapseNames">
<el-collapse-item title="目录" name="1">
<el-anchor :offset="70">
@@ -232,9 +232,9 @@ function doGenerateDirectory() {
<style lang="scss" scoped>
.toolbar {
}
.toolbar-view {
.wang-toolbar-view {
}
.toolbar-edit {
.wang-toolbar-edit {
border: 1px solid #ccc;
}
.editor {
@@ -243,7 +243,7 @@ function doGenerateDirectory() {
width: 100% !important;
}
}
.editor-view {
.wang-editor-view {
height: auto;
:deep(.w-e-text-container) {
background-color: transparent;
@@ -253,18 +253,18 @@ function doGenerateDirectory() {
padding: 0 0px !important;
}
}
.editor-edit {
height: 500px !important;
.wang-editor-edit {
height: 600px !important;
border: 1px solid #ccc;
border-top: transparent;
}
.editor-container {
.wang-editor-container {
display: flex;
width: 100%;
height: 100%;
.directory {
.wang-editor-directory {
width: 200px;
height: 100%;
margin-left: 16px;
@@ -1,12 +1,12 @@
import { effectScope, nextTick, onScopeDispose, ref, watch } from "vue";
import { useElementSize } from "@vueuse/core";
import * as echarts from "echarts/core";
import { BarChart, GaugeChart, LineChart, PictorialBarChart, PieChart, RadarChart, ScatterChart } from "echarts/charts";
import type { BarSeriesOption, GaugeSeriesOption, LineSeriesOption, PictorialBarSeriesOption, PieSeriesOption, RadarSeriesOption, ScatterSeriesOption } from "echarts/charts";
import { DatasetComponent, GridComponent, LegendComponent, TitleComponent, ToolboxComponent, TooltipComponent, TransformComponent } from "echarts/components";
import type { DatasetComponentOption, GridComponentOption, LegendComponentOption, TitleComponentOption, ToolboxComponentOption, TooltipComponentOption } from "echarts/components";
import { LabelLayout, UniversalTransition } from "echarts/features";
import { CanvasRenderer } from "echarts/renderers";
import type { BarSeriesOption, GaugeSeriesOption, LineSeriesOption, PictorialBarSeriesOption, PieSeriesOption, RadarSeriesOption, ScatterSeriesOption } from 'echarts/charts'
import type { DatasetComponentOption, GridComponentOption, LegendComponentOption, TitleComponentOption, ToolboxComponentOption, TooltipComponentOption } from 'echarts/components'
import { useElementSize } from '@vueuse/core'
import { BarChart, GaugeChart, LineChart, PictorialBarChart, PieChart, RadarChart, ScatterChart } from 'echarts/charts'
import { DatasetComponent, GridComponent, LegendComponent, TitleComponent, ToolboxComponent, TooltipComponent, TransformComponent } from 'echarts/components'
import * as echarts from 'echarts/core'
import { LabelLayout, UniversalTransition } from 'echarts/features'
import { CanvasRenderer } from 'echarts/renderers'
import { effectScope, nextTick, onScopeDispose, ref, watch } from 'vue'
export type ECOption = echarts.ComposeOption<
| BarSeriesOption
@@ -22,7 +22,7 @@ export type ECOption = echarts.ComposeOption<
| GridComponentOption
| ToolboxComponentOption
| DatasetComponentOption
>;
>
echarts.use([
TitleComponent,
@@ -41,13 +41,13 @@ echarts.use([
GaugeChart,
LabelLayout,
UniversalTransition,
CanvasRenderer
]);
CanvasRenderer,
])
interface ChartHooks {
onRender?: (chart: echarts.ECharts) => void | Promise<void>;
onUpdated?: (chart: echarts.ECharts) => void | Promise<void>;
onDestroy?: (chart: echarts.ECharts) => void | Promise<void>;
onRender?: (chart: echarts.ECharts) => void | Promise<void>
onUpdated?: (chart: echarts.ECharts) => void | Promise<void>
onDestroy?: (chart: echarts.ECharts) => void | Promise<void>
}
/**
@@ -57,33 +57,33 @@ interface ChartHooks {
* @param darkMode dark mode
*/
export function useEcharts<T extends ECOption>(optionsFactory: () => T, hooks: ChartHooks = {}) {
const scope = effectScope();
const scope = effectScope()
const darkMode = ref(false);
const darkMode = ref(false)
const domRef = ref<HTMLElement | null>(null);
const initialSize = { width: 0, height: 0 };
const { width, height } = useElementSize(domRef, initialSize);
const domRef = ref<HTMLElement | null>(null)
const initialSize = { width: 0, height: 0 }
const { width, height } = useElementSize(domRef, initialSize)
let chart: echarts.ECharts | null = null;
const chartOptions: T = optionsFactory();
let chart: echarts.ECharts | null = null
const chartOptions: T = optionsFactory()
const {
onRender = (instance) => {
const textColor = darkMode.value ? "rgb(224, 224, 224)" : "rgb(31, 31, 31)";
const maskColor = darkMode.value ? "rgba(0, 0, 0, 0.4)" : "rgba(255, 255, 255, 0.8)";
const textColor = darkMode.value ? 'rgb(224, 224, 224)' : 'rgb(31, 31, 31)'
const maskColor = darkMode.value ? 'rgba(0, 0, 0, 0.4)' : 'rgba(255, 255, 255, 0.8)'
instance.showLoading({
textColor,
fontSize: 14,
maskColor
});
maskColor,
})
},
onUpdated = (instance) => {
instance.hideLoading();
instance.hideLoading()
},
onDestroy
} = hooks;
onDestroy,
} = hooks
/**
* whether can render chart
@@ -91,12 +91,12 @@ export function useEcharts<T extends ECOption>(optionsFactory: () => T, hooks: C
* when domRef is ready and initialSize is valid
*/
function canRender() {
return domRef.value && initialSize.width > 0 && initialSize.height > 0;
return domRef.value && initialSize.width > 0 && initialSize.height > 0
}
/** is chart rendered */
function isRendered() {
return Boolean(domRef.value && chart);
return Boolean(domRef.value && chart)
}
/**
@@ -105,59 +105,61 @@ export function useEcharts<T extends ECOption>(optionsFactory: () => T, hooks: C
* @param callback callback function
*/
async function updateOptions(callback: (opts: T, optsFactory: () => T) => ECOption = () => chartOptions) {
if (!isRendered()) return;
if (!isRendered())
return
const updatedOpts = callback(chartOptions, optionsFactory);
const updatedOpts = callback(chartOptions, optionsFactory)
Object.assign(chartOptions, updatedOpts);
Object.assign(chartOptions, updatedOpts)
if (isRendered()) {
chart?.clear();
chart?.clear()
}
chart?.setOption({ ...updatedOpts, backgroundColor: "transparent" });
chart?.setOption({ ...updatedOpts, backgroundColor: 'transparent' })
await onUpdated?.(chart!);
await onUpdated?.(chart!)
}
function setOptions(options: T) {
chart?.setOption(options);
chart?.setOption(options)
}
/** render chart */
async function render() {
if (!isRendered()) {
const chartTheme = darkMode.value ? "dark" : "light";
const chartTheme = darkMode.value ? 'dark' : 'light'
await nextTick();
await nextTick()
chart = echarts.init(domRef.value, chartTheme);
chart = echarts.init(domRef.value, chartTheme)
chart.setOption({ ...chartOptions, backgroundColor: "transparent" });
chart.setOption({ ...chartOptions, backgroundColor: 'transparent' })
await onRender?.(chart);
await onRender?.(chart)
}
}
/** resize chart */
function resize() {
chart?.resize();
chart?.resize()
}
/** destroy chart */
async function destroy() {
if (!chart) return;
if (!chart)
return
await onDestroy?.(chart);
chart?.dispose();
chart = null;
await onDestroy?.(chart)
chart?.dispose()
chart = null
}
/** change chart theme */
async function changeTheme() {
await destroy();
await render();
await onUpdated?.(chart!);
await destroy()
await render()
await onUpdated?.(chart!)
}
/**
@@ -167,47 +169,47 @@ export function useEcharts<T extends ECOption>(optionsFactory: () => T, hooks: C
* @param h height
*/
async function renderChartBySize(w: number, h: number) {
initialSize.width = w;
initialSize.height = h;
initialSize.width = w
initialSize.height = h
// size is abnormal, destroy chart
if (!canRender()) {
await destroy();
await destroy()
return;
return
}
// resize chart
if (isRendered()) {
resize();
resize()
}
// render chart
await render();
await render()
if (chart) {
await onUpdated?.(chart);
await onUpdated?.(chart)
}
}
scope.run(() => {
watch([width, height], ([newWidth, newHeight]) => {
renderChartBySize(newWidth, newHeight);
});
renderChartBySize(newWidth, newHeight)
})
watch(darkMode, () => {
changeTheme();
});
});
changeTheme()
})
})
onScopeDispose(() => {
destroy();
scope.stop();
});
destroy()
scope.stop()
})
return {
domRef,
updateOptions,
setOptions
};
setOptions,
}
}
@@ -0,0 +1,6 @@
import type { AddPostParams, PostEditorForm } from '@/types/post'
import { httpGet, httpPostJson } from '@/request/utils'
export function addPostApi(params: AddPostParams) {
return httpPostJson<PostEditorForm>('/post/add', params)
}
@@ -0,0 +1,6 @@
import type { PostTag } from '@/types/post/tag'
import { httpGet, httpPostJson } from '@/request/utils'
export function getPostTagApi() {
return httpGet<PostTag[]>('/post/tag')
}
@@ -31,6 +31,7 @@ declare module 'vue' {
'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']
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']
@@ -0,0 +1,20 @@
export interface PostEditorForm {
id: number
title: string
content: string
summary: string
cover: string
tags: string[]
status: number
publishTime: string
updateTime: string
}
export interface AddPostParams {
title: string
content: string
summary: string
cover: string
status: number
tags: string[]
}
@@ -0,0 +1,4 @@
export interface PostTag {
id: number
name: string
}
@@ -149,10 +149,12 @@ export interface UploadToAliyunResponse {
fileSize: string
}
export function uploadToAliyun(blob: Blob, fileName: string, fileSize: string, onProgress?: (percent: number) => void): Promise<UploadToAliyunResponse> {
export function uploadToAliyun(blob: Blob, fileName: string, fileSize: string, showProgress: boolean = true, onProgress?: (percent: number) => void): Promise<UploadToAliyunResponse> {
return new Promise((resolve, reject) => {
// 在开始上传时显示上传进度
showUploadProgress('uploading')
if (showProgress) {
showUploadProgress('uploading')
}
getAliyunKeyApi()
.then(async (res) => {
@@ -177,7 +179,9 @@ export function uploadToAliyun(blob: Blob, fileName: string, fileSize: string, o
if (event.lengthComputable && onProgress) {
const percent = (event.loaded / event.total) * 100
onProgress(Number.parseFloat(percent.toFixed(2))) // 保留两位小数
updateUploadProgress(percent) // 更新进度条
if (showProgress) {
updateUploadProgress(percent) // 更新进度条
}
}
}
@@ -191,12 +195,13 @@ export function uploadToAliyun(blob: Blob, fileName: string, fileSize: string, o
fileMapName: allFilePath,
})
updateUploadProgress(100) // 上传完毕
ElMessage.success('上传成功')
setTimeout(() => {
closeUploadProgress() // 关闭上传进度条
}, 500)
if (showProgress) {
updateUploadProgress(100) // 上传完毕
ElMessage.success('上传成功')
setTimeout(() => {
closeUploadProgress() // 关闭上传进度条
}, 500)
}
resolve({
url: allFilePath,
@@ -205,36 +210,42 @@ export function uploadToAliyun(blob: Blob, fileName: string, fileSize: string, o
})
}
else {
updateUploadProgress(0, 'failed') // 上传失败
ElMessage.error('上传失败,请稍后再试')
if (showProgress) {
updateUploadProgress(0, 'failed') // 上传失败
ElMessage.error('上传失败,请稍后再试')
setTimeout(() => {
closeUploadProgress() // 关闭上传进度条
}, 500)
setTimeout(() => {
closeUploadProgress() // 关闭上传进度条
}, 500)
}
reject(new Error(`OSS上传失败: ${xhr.responseText}`))
}
}
xhr.onerror = () => {
updateUploadProgress(0, 'failed') // 上传失败
ElMessage.error('上传失败,请稍后再试')
if (showProgress) {
updateUploadProgress(0, 'failed') // 上传失败
ElMessage.error('上传失败,请稍后再试')
setTimeout(() => {
closeUploadProgress() // 关闭上传进度条
}, 500)
setTimeout(() => {
closeUploadProgress() // 关闭上传进度条
}, 500)
}
reject(new Error(`OSS上传异常: ${xhr.responseText}`))
}
xhr.send(formData)
}
catch (error) {
updateUploadProgress(0, 'failed') // 上传失败
ElMessage.error('上传失败,请稍后再试')
if (showProgress) {
updateUploadProgress(0, 'failed') // 上传失败
ElMessage.error('上传失败,请稍后再试')
setTimeout(() => {
closeUploadProgress() // 关闭上传进度条
}, 500)
setTimeout(() => {
closeUploadProgress() // 关闭上传进度条
}, 500)
}
reject(error)
}
})
@@ -1,10 +1,324 @@
<script lang="ts" setup>
import type { FormInstance } from 'element-plus'
import type { PostEditorForm } from '@/types/post'
import type { PostTag } from '@/types/post/tag'
import type { UploadImageResponse } from '@/utils/file'
import { ElMessage } from 'element-plus'
import { computed, onMounted, reactive, ref, watch } from 'vue'
import CustomLoading from '@/components/CustomLoading.vue'
import ImageUploadCard from '@/components/ImageUploadCard.vue'
import WangEditor from '@/components/wang-editor/index.vue'
import { addPostApi } from '@/request/api/post'
import { getPostTagApi } from '@/request/api/post/tag'
import { uploadImage, uploadToAliyun } from '@/utils/file'
const wangEditorRef = ref<any>(null)
const formRef = ref<FormInstance>()
const loading = reactive({
init: true,
confirm: false,
})
const showPushDrawer = ref<boolean>(false)
const imageUpload = ref<UploadImageResponse | null>(null)
const tagOptions = ref<PostTag[]>([])
const statusOptions = [
{
label: '草稿',
value: 0,
},
{
label: '发布',
value: 1,
},
]
const oldForm = ref<PostEditorForm>({
id: 0,
title: '',
content: '',
summary: '',
cover: '',
tags: [],
publishTime: '',
updateTime: '',
status: 0,
})
const form = ref<PostEditorForm>({
id: 0,
title: '',
content: '',
summary: '',
cover: '',
tags: [],
publishTime: '',
updateTime: '',
status: 0,
})
const formRules = {
title: [
{ required: true, message: '文章标题不能为空', trigger: 'blur' },
],
summary: [
{ required: true, message: '摘要不能为空', trigger: 'blur' },
],
status: [
{ required: true, message: '状态不能为空', trigger: 'blur' },
],
}
const isUpdate = computed(() => form.value.id !== 0)
onMounted(() => {
loading.init = false
oldForm.value = JSON.parse(JSON.stringify(form.value))
})
async function handleShowPushDrawer() {
loading.init = true
showPushDrawer.value = true
if (!isUpdate.value) {
form.value.summary = (wangEditorRef.value.getText() || '').slice(0, 100)
}
await getPostTagApi().then((res) => {
if (res.isSuccess) {
tagOptions.value = res.data
}
else {
ElMessage.error(res.message || '获取帖子标签失败,请稍后再试')
}
}).catch((err) => {
console.log(err)
ElMessage.error('获取帖子标签失败,请稍后再试')
})
loading.init = false
}
async function handleSave(formEl: FormInstance | undefined) {
if (!formEl)
return
await formEl.validate(async (valid) => {
if (!valid)
return
if (form.value.content === '') {
ElMessage.warning('文章内容不能为空')
return
}
loading.confirm = true
if (form.value.cover !== oldForm.value.cover) {
if (form.value.cover && imageUpload.value !== null) {
const aliyunRes = await uploadToAliyun(imageUpload.value.blob, imageUpload.value.name, imageUpload.value.size, false)
form.value.cover = aliyunRes.url
}
}
addPostApi({
title: form.value.title,
content: form.value.content,
summary: form.value.summary,
status: form.value.status,
cover: form.value.cover,
tags: form.value.tags,
}).then((res) => {
if (res.isSuccess) {
console.log(res)
}
else {
ElMessage.error(res.message || '保存失败,请稍后再试')
}
}).catch((err) => {
console.log(err)
ElMessage.error('保存失败,请稍后再试')
}).finally(() => {
loading.confirm = false
showPushDrawer.value = false
})
})
}
</script>
<template>
<div class="" />
<div class="editor-container">
<div class="toolbar">
<input v-model="form.title" placeholder="输入标题" class="editor-title-input" maxlength="100">
<div class="toolbar-extra">
<div v-if="form.publishTime" class="time">
更新于:{{ form.publishTime }}
</div>
<el-button type="primary" @click="handleShowPushDrawer()">
发布
</el-button>
</div>
</div>
<WangEditor ref="wangEditorRef" v-model="form.content" :show-direcotry="true" />
</div>
<el-drawer v-model="showPushDrawer" title="发布文章">
<CustomLoading v-if="loading.init" :in-container="true" />
<el-form
v-else
ref="formRef"
:model="form"
:rules="formRules"
label-width="auto"
>
<el-form-item label="文章标题" prop="title">
<el-input
v-model="form.title"
placeholder="请输入文字标题"
size="large"
show-word-limit
maxlength="100"
type="textarea"
/>
</el-form-item>
<el-form-item label="标签" prop="tags">
<el-select v-model="form.tags" placeholder="请选择标签">
<el-option
v-for="item in tagOptions"
:key="item.id"
:label="item.name"
:value="item.name"
/>
</el-select>
</el-form-item>
<el-form-item label="文章封面 " prop="cover">
<ImageUploadCard
v-model="form.cover"
@change="(data) => {
imageUpload = data
}"
/>
</el-form-item>
<el-form-item label="文章摘要" prop="summary">
<el-input
v-model="form.summary"
placeholder="请输入文章摘要"
size="large"
show-word-limit
maxlength="100"
type="textarea"
/>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="form.status" placeholder="请选择状态">
<el-option
v-for="item in statusOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-form>
<template #footer>
<div style="flex: auto">
<el-button>
取消
</el-button>
<el-button type="primary" :loading="loading.confirm" @click="handleSave(formRef)">
确认并发布
</el-button>
</div>
</template>
</el-drawer>
</template>
<style lang="scss" scoped>
//
.editor-container {
padding: 16px 32px;
.toolbar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
.editor-title-input {
width: 1000px;
border: none;
font-size: 24px;
padding: 8px 0px;
box-sizing: border-box;
outline: none;
resize: none;
line-height: 1.5;
}
.toolbar-extra {
.time {
font-size: 14px;
color: #666;
}
}
}
}
.upload-card {
width: 120px;
height: 120px;
border: 1px dashed #d9d9d9;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
background-color: #fafafa;
cursor: pointer;
transition: border-color 0.3s ease;
padding: 4px;
&:hover {
border-color: #409eff;
}
&.loading {
pointer-events: none;
opacity: 0.6;
}
.upload-inner {
font-size: 14px;
color: #999;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8px;
}
.upload-icon {
font-size: 18px;
}
.upload-loading-icon {
font-size: 18px;
}
.upload-image {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 4px;
}
}
.form-cover {
height: 192px;
width: 128px;
}
</style>
@@ -13,7 +13,7 @@
v-model:visible="showPushDrawer"
:width="450"
unmountOnClose
ok-text="确认并发布"
ok-text=" "
@before-open="handleBeforeOpenPushDrawer"
:ok-loading="loading.init || loading.confirm"
:on-before-ok="handleConfirmPush">