feat: optimize source project pages and SEO
This commit is contained in:
@@ -114,6 +114,10 @@ swagger:
|
||||
# 请求前缀
|
||||
pathMapping: /dev-api
|
||||
|
||||
# Search engine canonical URL configuration
|
||||
seo:
|
||||
site-url: https://www.yidaima.cn
|
||||
|
||||
# 防止XSS攻击
|
||||
xss:
|
||||
# 过滤开关
|
||||
|
||||
@@ -14,6 +14,7 @@ import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.framework.web.service.SysPermissionService;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
@@ -66,7 +67,7 @@ public class CmsBlogController extends BaseController
|
||||
startPage();
|
||||
//状态为发布
|
||||
cmsBlog.setStatus("1");
|
||||
List<CmsBlog> list = cmsBlogService.selectCmsBlogList(cmsBlog);
|
||||
List<CmsBlog> list = cmsBlogService.selectCmsBlogCardList(cmsBlog);
|
||||
return getDataTable(list);
|
||||
}
|
||||
|
||||
@@ -74,7 +75,7 @@ public class CmsBlogController extends BaseController
|
||||
* 首页获取文章详细信息
|
||||
*/
|
||||
@GetMapping(value = { "/cms/detail/", "/cms/detail/{id}" })
|
||||
public AjaxResult getInfoDetail(@PathVariable(value = "id", required = false) Long id)
|
||||
public ResponseEntity<AjaxResult> getInfoDetail(@PathVariable(value = "id", required = false) Long id)
|
||||
{
|
||||
AjaxResult ajax = AjaxResult.success();
|
||||
CmsType cmsType = new CmsType();
|
||||
@@ -83,9 +84,14 @@ public class CmsBlogController extends BaseController
|
||||
ajax.put("tags", cmsTagService.selectCmsTagList(cmsTag));
|
||||
if (StringUtils.isNotNull(id))
|
||||
{
|
||||
ajax.put(AjaxResult.DATA_TAG, cmsBlogService.selectCmsBlogById(id));
|
||||
CmsBlog blog = cmsBlogService.selectPublishedCmsBlogById(id);
|
||||
if (blog == null)
|
||||
{
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
return ajax;
|
||||
ajax.put(AjaxResult.DATA_TAG, blog);
|
||||
}
|
||||
return ResponseEntity.ok(ajax);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -127,14 +133,18 @@ public class CmsBlogController extends BaseController
|
||||
* 首页增加阅读量
|
||||
*/
|
||||
@GetMapping("/cms/addBlogViews/{id}")
|
||||
public AjaxResult addBlogViews(@PathVariable(value = "id", required = false) Long id)
|
||||
public ResponseEntity<AjaxResult> addBlogViews(@PathVariable(value = "id", required = false) Long id)
|
||||
{
|
||||
CmsBlog cmsBlog = cmsBlogService.selectCmsBlogById(id);
|
||||
CmsBlog cmsBlog = cmsBlogService.selectPublishedCmsBlogById(id);
|
||||
if (cmsBlog == null)
|
||||
{
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
Long views = cmsBlog.getViews();
|
||||
views+=Long.parseLong("1");
|
||||
views = views == null ? 1L : views + 1L;
|
||||
cmsBlog.setViews(views);
|
||||
cmsBlogService.updateCmsBlog(cmsBlog);
|
||||
return AjaxResult.success(id);
|
||||
return ResponseEntity.ok(AjaxResult.success(id));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -8,6 +8,7 @@ package com.ruoyi.cms.blog.domain;
|
||||
|
||||
import org.apache.commons.lang3.builder.ToStringBuilder;
|
||||
import org.apache.commons.lang3.builder.ToStringStyle;
|
||||
import com.ruoyi.cms.tag.domain.CmsTag;
|
||||
|
||||
/**
|
||||
* @Author: WangNing
|
||||
@@ -18,6 +19,7 @@ public class CmsBlogTag {
|
||||
private Long tagId;
|
||||
private Long blogId;
|
||||
private Long[] tagIds;
|
||||
private CmsTag tag;
|
||||
|
||||
public Long getTagId() {
|
||||
return tagId;
|
||||
@@ -43,6 +45,14 @@ public class CmsBlogTag {
|
||||
this.tagIds = tagIds;
|
||||
}
|
||||
|
||||
public CmsTag getTag() {
|
||||
return tag;
|
||||
}
|
||||
|
||||
public void setTag(CmsTag tag) {
|
||||
this.tag = tag;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE)
|
||||
|
||||
@@ -8,6 +8,7 @@ package com.ruoyi.cms.blog.domain;
|
||||
|
||||
import org.apache.commons.lang3.builder.ToStringBuilder;
|
||||
import org.apache.commons.lang3.builder.ToStringStyle;
|
||||
import com.ruoyi.cms.type.domain.CmsType;
|
||||
|
||||
/**
|
||||
* @Author: WangNing
|
||||
@@ -18,6 +19,7 @@ public class CmsBlogType {
|
||||
private Long typeId;
|
||||
private Long blogId;
|
||||
private Long[] typeIds;
|
||||
private CmsType type;
|
||||
|
||||
public Long getTypeId() {
|
||||
return typeId;
|
||||
@@ -43,6 +45,14 @@ public class CmsBlogType {
|
||||
this.typeIds = typeIds;
|
||||
}
|
||||
|
||||
public CmsType getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public void setType(CmsType type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE)
|
||||
|
||||
@@ -20,6 +20,11 @@ public interface CmsBlogMapper
|
||||
*/
|
||||
public CmsBlog selectCmsBlogById(Long id);
|
||||
|
||||
/**
|
||||
* Query a public detail page. Drafts must behave like missing pages.
|
||||
*/
|
||||
public CmsBlog selectPublishedCmsBlogById(Long id);
|
||||
|
||||
/**
|
||||
* 查询文章管理列表
|
||||
*
|
||||
@@ -28,6 +33,16 @@ public interface CmsBlogMapper
|
||||
*/
|
||||
public List<CmsBlog> selectCmsBlogList(CmsBlog cmsBlog);
|
||||
|
||||
/**
|
||||
* 查询首页卡片所需的轻量字段
|
||||
*/
|
||||
public List<CmsBlog> selectCmsBlogCardList(CmsBlog cmsBlog);
|
||||
|
||||
/**
|
||||
* 批量读取缺少封面的文章正文,用于提取首图
|
||||
*/
|
||||
public List<CmsBlog> selectCmsBlogContentByIds(List<Long> ids);
|
||||
|
||||
public List<CmsBlog> selectCmsBlogListBetweenCreateTime(@Param("title") String title,@Param("type") String type,@Param("top") String top,@Param("status") String status,@Param("createTimeBegin") String createTimeBegin,@Param("createTimeEnd") String createTimeEnd,@Param("createBy") String createBy);
|
||||
|
||||
/**
|
||||
|
||||
@@ -27,6 +27,10 @@ public interface CmsBlogTagMapper {
|
||||
* 查询文件列表
|
||||
*/
|
||||
public List<CmsBlogTag> selectBlogTagList(Long blogId);
|
||||
/**
|
||||
* 批量查询博客及其标签,避免列表页逐条查询
|
||||
*/
|
||||
public List<CmsBlogTag> selectBlogTagsByBlogIds(List<Long> blogIds);
|
||||
/**
|
||||
* 通过tagId删除blog文件关联
|
||||
*/
|
||||
|
||||
@@ -27,6 +27,10 @@ public interface CmsBlogTypeMapper {
|
||||
* 查询博客列表
|
||||
*/
|
||||
public List<CmsBlogType> selectBlogTypeList(Long blogId);
|
||||
/**
|
||||
* 批量查询博客及其分类,避免列表页逐条查询
|
||||
*/
|
||||
public List<CmsBlogType> selectBlogTypesByBlogIds(List<Long> blogIds);
|
||||
/**
|
||||
* 通过typeId删除blog文件关联
|
||||
*/
|
||||
|
||||
@@ -19,6 +19,14 @@ public interface ICmsBlogService
|
||||
*/
|
||||
public CmsBlog selectCmsBlogById(Long id);
|
||||
|
||||
/**
|
||||
* Query an indexable public detail page.
|
||||
*
|
||||
* @param id content primary key
|
||||
* @return published content, or null when it must not be indexed
|
||||
*/
|
||||
public CmsBlog selectPublishedCmsBlogById(Long id);
|
||||
|
||||
/**
|
||||
* 查询文章管理列表
|
||||
*
|
||||
@@ -27,6 +35,11 @@ public interface ICmsBlogService
|
||||
*/
|
||||
public List<CmsBlog> selectCmsBlogList(CmsBlog cmsBlog);
|
||||
|
||||
/**
|
||||
* 查询首页项目卡片列表
|
||||
*/
|
||||
public List<CmsBlog> selectCmsBlogCardList(CmsBlog cmsBlog);
|
||||
|
||||
/**
|
||||
* 查询推荐文章列表
|
||||
*/
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package com.ruoyi.cms.blog.service.impl;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@@ -54,6 +56,10 @@ public class CmsBlogServiceImpl implements ICmsBlogService
|
||||
public CmsBlog selectCmsBlogById(Long id)
|
||||
{
|
||||
CmsBlog blog = cmsBlogMapper.selectCmsBlogById(id);
|
||||
if (blog == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
//查询标签列表
|
||||
List<CmsBlogTag> blogTagList = cmsBlogTagMapper.selectBlogTagList(id);
|
||||
Long[] tagIds = new Long[blogTagList.size()];
|
||||
@@ -83,6 +89,13 @@ public class CmsBlogServiceImpl implements ICmsBlogService
|
||||
return blog;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CmsBlog selectPublishedCmsBlogById(Long id)
|
||||
{
|
||||
CmsBlog blog = selectCmsBlogById(id);
|
||||
return blog != null && "1".equals(blog.getStatus()) ? blog : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询文章管理列表
|
||||
*
|
||||
@@ -103,18 +116,19 @@ public class CmsBlogServiceImpl implements ICmsBlogService
|
||||
return blogList;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CmsBlog> selectCmsBlogCardList(CmsBlog cmsBlog)
|
||||
{
|
||||
return prepareBlogCards(cmsBlogMapper.selectCmsBlogCardList(cmsBlog));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询推荐文章列表
|
||||
*/
|
||||
@Override
|
||||
public List<CmsBlog> selectCmsBlogListRecommend(CmsBlog cmsBlog) {
|
||||
List<CmsBlog> cmsBlogList = cmsBlogMapper.selectCmsBlogListRecommend(cmsBlog);
|
||||
// 为每个博客设置首页图片(如果为空,则从content中提取)
|
||||
for (CmsBlog blog : cmsBlogList) {
|
||||
setDefaultBlogPicFromContent(blog);
|
||||
}
|
||||
List<CmsBlog> blogList = BlogListAddTypeAndTag(cmsBlogList);
|
||||
return blogList;
|
||||
return prepareBlogCards(cmsBlogList);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -123,12 +137,7 @@ public class CmsBlogServiceImpl implements ICmsBlogService
|
||||
@Override
|
||||
public List<CmsBlog> selectCmsBlogListByTypeId(Long id) {
|
||||
List<CmsBlog> cmsBlogList = cmsBlogMapper.selectCmsBlogListByTypeId(id);
|
||||
// 为每个博客设置首页图片(如果为空,则从content中提取)
|
||||
for (CmsBlog blog : cmsBlogList) {
|
||||
setDefaultBlogPicFromContent(blog);
|
||||
}
|
||||
List<CmsBlog> blogList = BlogListAddTypeAndTag(cmsBlogList);
|
||||
return blogList;
|
||||
return prepareBlogCards(cmsBlogList);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -137,12 +146,7 @@ public class CmsBlogServiceImpl implements ICmsBlogService
|
||||
@Override
|
||||
public List<CmsBlog> selectCmsBlogListByTagId(Long id) {
|
||||
List<CmsBlog> cmsBlogList = cmsBlogMapper.selectCmsBlogListByTagId(id);
|
||||
// 为每个博客设置首页图片(如果为空,则从content中提取)
|
||||
for (CmsBlog blog : cmsBlogList) {
|
||||
setDefaultBlogPicFromContent(blog);
|
||||
}
|
||||
List<CmsBlog> blogList = BlogListAddTypeAndTag(cmsBlogList);
|
||||
return blogList;
|
||||
return prepareBlogCards(cmsBlogList);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -307,30 +311,71 @@ public class CmsBlogServiceImpl implements ICmsBlogService
|
||||
return null;
|
||||
}
|
||||
|
||||
private List<CmsBlog> BlogListAddTypeAndTag(List<CmsBlog> cmsBlogList){
|
||||
if (cmsBlogList==null||cmsBlogList.size()<0){
|
||||
/**
|
||||
* 列表不返回正文;仅对没有独立封面的项目批量读取正文并提取首图。
|
||||
*/
|
||||
private List<CmsBlog> prepareBlogCards(List<CmsBlog> cmsBlogList) {
|
||||
if (cmsBlogList == null || cmsBlogList.isEmpty()) {
|
||||
return cmsBlogList;
|
||||
}
|
||||
|
||||
Map<Long, CmsBlog> missingCoverBlogs = new HashMap<>();
|
||||
for (CmsBlog blog : cmsBlogList) {
|
||||
Long blogId = blog.getId();
|
||||
//查询标签列表
|
||||
List<CmsBlogTag> blogTagList = cmsBlogTagMapper.selectBlogTagList(blogId);
|
||||
List<CmsTag> cmsTagList = new ArrayList<>();
|
||||
blogTagList.forEach((CmsBlogTag cmsBlogTag)->{
|
||||
Long tagId = cmsBlogTag.getTagId();
|
||||
CmsTag cmsTag = cmsTagMapper.selectCmsTagByTagId(tagId);
|
||||
cmsTagList.add(cmsTag);
|
||||
});
|
||||
blog.setTags(cmsTagList);
|
||||
//查询分类列表
|
||||
List<CmsBlogType> blogTypeList = cmsBlogTypeMapper.selectBlogTypeList(blogId);
|
||||
List<CmsType> cmsTypeList = new ArrayList<>();
|
||||
blogTypeList.forEach((CmsBlogType cmsBlogType)->{
|
||||
Long typeId = cmsBlogType.getTypeId();
|
||||
CmsType cmsType = cmsTypeMapper.selectCmsTypeByTypeId(typeId);
|
||||
cmsTypeList.add(cmsType);
|
||||
});
|
||||
blog.setTypes(cmsTypeList);
|
||||
boolean hasCover = (blog.getBlogPic() != null && !blog.getBlogPic().trim().isEmpty())
|
||||
|| (blog.getBlogPicLink() != null && !blog.getBlogPicLink().trim().isEmpty());
|
||||
if (!hasCover) {
|
||||
missingCoverBlogs.put(blog.getId(), blog);
|
||||
}
|
||||
}
|
||||
|
||||
if (!missingCoverBlogs.isEmpty()) {
|
||||
List<Long> ids = new ArrayList<>(missingCoverBlogs.keySet());
|
||||
for (CmsBlog contentBlog : cmsBlogMapper.selectCmsBlogContentByIds(ids)) {
|
||||
CmsBlog targetBlog = missingCoverBlogs.get(contentBlog.getId());
|
||||
if (targetBlog == null || contentBlog.getContent() == null) {
|
||||
continue;
|
||||
}
|
||||
String firstImageUrl = extractFirstImageFromContent(contentBlog.getContent());
|
||||
if (firstImageUrl != null && !firstImageUrl.trim().isEmpty()) {
|
||||
targetBlog.setBlogPicType("0");
|
||||
targetBlog.setBlogPicLink(firstImageUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return BlogListAddTypeAndTag(cmsBlogList);
|
||||
}
|
||||
|
||||
private List<CmsBlog> BlogListAddTypeAndTag(List<CmsBlog> cmsBlogList){
|
||||
if (cmsBlogList == null || cmsBlogList.isEmpty()) {
|
||||
return cmsBlogList;
|
||||
}
|
||||
|
||||
List<Long> blogIds = new ArrayList<>();
|
||||
Map<Long, List<CmsTag>> tagsByBlogId = new HashMap<>();
|
||||
Map<Long, List<CmsType>> typesByBlogId = new HashMap<>();
|
||||
for (CmsBlog blog : cmsBlogList) {
|
||||
blogIds.add(blog.getId());
|
||||
tagsByBlogId.put(blog.getId(), new ArrayList<>());
|
||||
typesByBlogId.put(blog.getId(), new ArrayList<>());
|
||||
}
|
||||
|
||||
for (CmsBlogTag relation : cmsBlogTagMapper.selectBlogTagsByBlogIds(blogIds)) {
|
||||
List<CmsTag> tags = tagsByBlogId.get(relation.getBlogId());
|
||||
if (tags != null && relation.getTag() != null) {
|
||||
tags.add(relation.getTag());
|
||||
}
|
||||
}
|
||||
for (CmsBlogType relation : cmsBlogTypeMapper.selectBlogTypesByBlogIds(blogIds)) {
|
||||
List<CmsType> types = typesByBlogId.get(relation.getBlogId());
|
||||
if (types != null && relation.getType() != null) {
|
||||
types.add(relation.getType());
|
||||
}
|
||||
}
|
||||
|
||||
for (CmsBlog blog : cmsBlogList) {
|
||||
blog.setTags(tagsByBlogId.get(blog.getId()));
|
||||
blog.setTypes(typesByBlogId.get(blog.getId()));
|
||||
}
|
||||
return cmsBlogList;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.ruoyi.cms.sitemap.controller;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import com.ruoyi.cms.sitemap.service.SitemapService;
|
||||
import org.springframework.http.CacheControl;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* Public XML sitemap endpoint.
|
||||
*/
|
||||
@RestController
|
||||
public class SitemapController
|
||||
{
|
||||
private static final MediaType APPLICATION_XML_UTF8 =
|
||||
new MediaType("application", "xml", StandardCharsets.UTF_8);
|
||||
|
||||
private final SitemapService sitemapService;
|
||||
|
||||
public SitemapController(SitemapService sitemapService)
|
||||
{
|
||||
this.sitemapService = sitemapService;
|
||||
}
|
||||
|
||||
@GetMapping(value = "/sitemap.xml", produces = MediaType.APPLICATION_XML_VALUE)
|
||||
public ResponseEntity<String> sitemap()
|
||||
{
|
||||
return ResponseEntity.ok()
|
||||
.contentType(APPLICATION_XML_UTF8)
|
||||
.cacheControl(CacheControl.maxAge(10, TimeUnit.MINUTES).cachePublic())
|
||||
.body(sitemapService.buildSitemap());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.ruoyi.cms.sitemap.domain;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* A canonical, indexable URL source used to build the public sitemap.
|
||||
*/
|
||||
public class SitemapUrl
|
||||
{
|
||||
private Long id;
|
||||
|
||||
private String pageType;
|
||||
|
||||
private Date lastModified;
|
||||
|
||||
public Long getId()
|
||||
{
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id)
|
||||
{
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getPageType()
|
||||
{
|
||||
return pageType;
|
||||
}
|
||||
|
||||
public void setPageType(String pageType)
|
||||
{
|
||||
this.pageType = pageType;
|
||||
}
|
||||
|
||||
public Date getLastModified()
|
||||
{
|
||||
return lastModified;
|
||||
}
|
||||
|
||||
public void setLastModified(Date lastModified)
|
||||
{
|
||||
this.lastModified = lastModified;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.ruoyi.cms.sitemap.mapper;
|
||||
|
||||
import java.util.List;
|
||||
import com.ruoyi.cms.sitemap.domain.SitemapUrl;
|
||||
|
||||
/**
|
||||
* Queries only URLs that have a real, published public page.
|
||||
*/
|
||||
public interface SitemapMapper
|
||||
{
|
||||
public List<SitemapUrl> selectIndexableCategories();
|
||||
|
||||
public List<SitemapUrl> selectIndexableContent();
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
package com.ruoyi.cms.sitemap.service;
|
||||
|
||||
import java.io.StringWriter;
|
||||
import java.net.URI;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import javax.xml.stream.XMLOutputFactory;
|
||||
import javax.xml.stream.XMLStreamException;
|
||||
import javax.xml.stream.XMLStreamWriter;
|
||||
import com.ruoyi.cms.sitemap.domain.SitemapUrl;
|
||||
import com.ruoyi.cms.sitemap.mapper.SitemapMapper;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* Builds the UTF-8 sitemap from canonical URLs backed by published data.
|
||||
*/
|
||||
@Service
|
||||
public class SitemapService
|
||||
{
|
||||
private static final String SITEMAP_NAMESPACE = "http://www.sitemaps.org/schemas/sitemap/0.9";
|
||||
|
||||
private final SitemapMapper sitemapMapper;
|
||||
|
||||
private final String siteUrl;
|
||||
|
||||
public SitemapService(SitemapMapper sitemapMapper,
|
||||
@Value("${seo.site-url:https://www.yidaima.cn}") String siteUrl)
|
||||
{
|
||||
this.sitemapMapper = sitemapMapper;
|
||||
this.siteUrl = normalizeSiteUrl(siteUrl);
|
||||
}
|
||||
|
||||
public String buildSitemap()
|
||||
{
|
||||
Map<String, SitemapUrl> canonicalUrls = new LinkedHashMap<>();
|
||||
addUrls(canonicalUrls, sitemapMapper.selectIndexableCategories());
|
||||
addUrls(canonicalUrls, sitemapMapper.selectIndexableContent());
|
||||
|
||||
try
|
||||
{
|
||||
StringWriter output = new StringWriter();
|
||||
XMLStreamWriter xml = XMLOutputFactory.newFactory().createXMLStreamWriter(output);
|
||||
xml.writeStartDocument("UTF-8", "1.0");
|
||||
xml.writeStartElement("urlset");
|
||||
xml.writeDefaultNamespace(SITEMAP_NAMESPACE);
|
||||
|
||||
for (Map.Entry<String, SitemapUrl> entry : canonicalUrls.entrySet())
|
||||
{
|
||||
xml.writeStartElement("url");
|
||||
writeElement(xml, "loc", siteUrl + entry.getKey());
|
||||
if (entry.getValue().getLastModified() != null)
|
||||
{
|
||||
writeElement(xml, "lastmod",
|
||||
DateTimeFormatter.ISO_INSTANT.format(entry.getValue().getLastModified().toInstant()));
|
||||
}
|
||||
xml.writeEndElement();
|
||||
}
|
||||
|
||||
xml.writeEndElement();
|
||||
xml.writeEndDocument();
|
||||
xml.close();
|
||||
return output.toString();
|
||||
}
|
||||
catch (XMLStreamException exception)
|
||||
{
|
||||
throw new IllegalStateException("Unable to generate sitemap XML", exception);
|
||||
}
|
||||
}
|
||||
|
||||
private void addUrls(Map<String, SitemapUrl> canonicalUrls, List<SitemapUrl> urls)
|
||||
{
|
||||
if (urls == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (SitemapUrl url : urls)
|
||||
{
|
||||
if (url == null || url.getId() == null || !isSupportedPageType(url.getPageType()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
canonicalUrls.putIfAbsent("/" + url.getPageType() + "/" + url.getId(), url);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isSupportedPageType(String pageType)
|
||||
{
|
||||
return "category".equals(pageType) || "source".equals(pageType) || "article".equals(pageType);
|
||||
}
|
||||
|
||||
private void writeElement(XMLStreamWriter xml, String name, String value) throws XMLStreamException
|
||||
{
|
||||
xml.writeStartElement(name);
|
||||
xml.writeCharacters(value);
|
||||
xml.writeEndElement();
|
||||
}
|
||||
|
||||
private String normalizeSiteUrl(String configuredSiteUrl)
|
||||
{
|
||||
String value = configuredSiteUrl == null ? "" : configuredSiteUrl.trim();
|
||||
while (value.endsWith("/"))
|
||||
{
|
||||
value = value.substring(0, value.length() - 1);
|
||||
}
|
||||
|
||||
URI uri;
|
||||
try
|
||||
{
|
||||
uri = URI.create(value);
|
||||
}
|
||||
catch (IllegalArgumentException exception)
|
||||
{
|
||||
throw new IllegalStateException("seo.site-url must be an absolute HTTP(S) URL", exception);
|
||||
}
|
||||
|
||||
if (uri.getHost() == null
|
||||
|| (!"http".equalsIgnoreCase(uri.getScheme()) && !"https".equalsIgnoreCase(uri.getScheme()))
|
||||
|| uri.getQuery() != null || uri.getFragment() != null)
|
||||
{
|
||||
throw new IllegalStateException("seo.site-url must be an absolute HTTP(S) URL without query or fragment");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -47,12 +47,7 @@ public class CmsTagServiceImpl implements ICmsTagService
|
||||
@Override
|
||||
public List<CmsTag> selectCmsTagList(CmsTag cmsTag)
|
||||
{
|
||||
List<CmsTag> cmsTagList = cmsTagMapper.selectCmsTagList(cmsTag);
|
||||
for (CmsTag tag : cmsTagList) {
|
||||
int blogNum = cmsBlogTagMapper.countBlogByTagId(tag.getTagId());
|
||||
tag.setBlogNum(blogNum);
|
||||
}
|
||||
return cmsTagList;
|
||||
return cmsTagMapper.selectCmsTagList(cmsTag);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -51,12 +51,7 @@ public class CmsTypeServiceImpl implements ICmsTypeService
|
||||
@Override
|
||||
public List<CmsType> selectCmsTypeList(CmsType cmsType)
|
||||
{
|
||||
List<CmsType> cmsTypeList = cmsTypeMapper.selectCmsTypeList(cmsType);
|
||||
for (CmsType type : cmsTypeList) {
|
||||
int blogNum = cmsBlogTypeMapper.countBlogByTypeId(type.getTypeId());
|
||||
type.setBlogNum(blogNum);
|
||||
}
|
||||
return cmsTypeList;
|
||||
return cmsTypeMapper.selectCmsTypeList(cmsType);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -29,6 +29,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
select id, create_by, create_time, update_by, update_time, title, type, content, content_type, content_markdown, Top, views, status, blog_pic_type, blog_pic, blog_pic_link, blog_desc, blog_files from cms_blog
|
||||
</sql>
|
||||
|
||||
<sql id="selectCmsBlogCardVo">
|
||||
select id, create_by, create_time, update_time, title, type, Top, views, status,
|
||||
blog_pic_type, blog_pic, blog_pic_link, blog_desc
|
||||
from cms_blog
|
||||
</sql>
|
||||
|
||||
<select id="selectCmsBlogList" parameterType="CmsBlog" resultMap="CmsBlogResult">
|
||||
<include refid="selectCmsBlogVo"/>
|
||||
<where>
|
||||
@@ -43,24 +49,54 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
ORDER BY Top DESC, create_time DESC
|
||||
</select>
|
||||
|
||||
<select id="selectCmsBlogCardList" parameterType="CmsBlog" resultMap="CmsBlogResult">
|
||||
<include refid="selectCmsBlogCardVo"/>
|
||||
<where>
|
||||
<if test="title != null and title != ''"> and title like concat('%', #{title}, '%')</if>
|
||||
<if test="type != null and type != ''"> and type = #{type}</if>
|
||||
<if test="top != null and top != ''"> and Top = #{top}</if>
|
||||
<if test="views != null "> and views = #{views}</if>
|
||||
<if test="status != null and status != ''"> and status = #{status}</if>
|
||||
<if test="createBy != null and createBy != ''"> and create_by = #{createBy}</if>
|
||||
</where>
|
||||
ORDER BY Top DESC, create_time DESC
|
||||
</select>
|
||||
|
||||
<select id="selectCmsBlogContentByIds" resultMap="CmsBlogResult">
|
||||
select id, content
|
||||
from cms_blog
|
||||
where id in
|
||||
<foreach collection="list" item="id" open="(" separator="," close=")">
|
||||
#{id}
|
||||
</foreach>
|
||||
</select>
|
||||
|
||||
<select id="selectCmsBlogById" parameterType="Long" resultMap="CmsBlogResult">
|
||||
<include refid="selectCmsBlogVo"/>
|
||||
where id = #{id}
|
||||
</select>
|
||||
<select id="selectCmsBlogListByTypeId" parameterType="Long" resultMap="CmsBlogResult">
|
||||
<select id="selectPublishedCmsBlogById" parameterType="Long" resultMap="CmsBlogResult">
|
||||
<include refid="selectCmsBlogVo"/>
|
||||
where id = #{id} and status = '1'
|
||||
</select>
|
||||
<select id="selectCmsBlogListByTypeId" parameterType="Long" resultMap="CmsBlogResult">
|
||||
<include refid="selectCmsBlogCardVo"/>
|
||||
<where>
|
||||
<if test="id != null and id != ''"> and id in (SELECT blog_id FROM cms_blog_type WHERE type_id = #{id})</if>
|
||||
and status = '1'
|
||||
</where>
|
||||
ORDER BY Top DESC, create_time DESC
|
||||
</select>
|
||||
<select id="selectCmsBlogListByTagId" parameterType="Long" resultMap="CmsBlogResult">
|
||||
<include refid="selectCmsBlogVo"/>
|
||||
<include refid="selectCmsBlogCardVo"/>
|
||||
<where>
|
||||
<if test="id != null and id != ''"> and id in (SELECT blog_id FROM cms_blog_tag WHERE tag_id = #{id})</if>
|
||||
and status = '1'
|
||||
</where>
|
||||
ORDER BY Top DESC, create_time DESC
|
||||
</select>
|
||||
<select id="selectCmsBlogListRecommend" parameterType="CmsBlog" resultMap="CmsBlogResult">
|
||||
<include refid="selectCmsBlogVo"/>
|
||||
<include refid="selectCmsBlogCardVo"/>
|
||||
<where>
|
||||
<if test="title != null and title != ''"> and title like concat('%', #{title}, '%')</if>
|
||||
<if test="type != null and type != ''"> and type = #{type}</if>
|
||||
|
||||
@@ -5,6 +5,14 @@
|
||||
<result property="tagId" column="tag_id" />
|
||||
<result property="blogId" column="blog_id" />
|
||||
</resultMap>
|
||||
<resultMap type="cmsBlogTag" id="blogTagWithTagResult">
|
||||
<result property="tagId" column="tag_id" />
|
||||
<result property="blogId" column="blog_id" />
|
||||
<association property="tag" javaType="com.ruoyi.cms.tag.domain.CmsTag">
|
||||
<id property="tagId" column="tag_id" />
|
||||
<result property="tagName" column="tag_name" />
|
||||
</association>
|
||||
</resultMap>
|
||||
|
||||
<insert id="batchBlogTag">
|
||||
insert into cms_blog_tag(tag_id, blog_id) values
|
||||
@@ -30,6 +38,15 @@
|
||||
<select id="selectBlogTagList" parameterType="Long" resultMap="blogTagResult">
|
||||
select tag_id, blog_id from cms_blog_tag where blog_id = #{blogId}
|
||||
</select>
|
||||
<select id="selectBlogTagsByBlogIds" resultMap="blogTagWithTagResult">
|
||||
select bt.blog_id, t.tag_id, t.tag_name
|
||||
from cms_blog_tag bt
|
||||
inner join cms_tag t on t.tag_id = bt.tag_id
|
||||
where bt.blog_id in
|
||||
<foreach collection="list" item="blogId" open="(" separator="," close=")">
|
||||
#{blogId}
|
||||
</foreach>
|
||||
</select>
|
||||
<select id="countBlogByTagId" parameterType="Long" resultType="java.lang.Integer">
|
||||
select count(*) from cms_blog_tag where tag_id=#{tagId}
|
||||
</select>
|
||||
|
||||
@@ -5,6 +5,14 @@
|
||||
<result property="typeId" column="type_id" />
|
||||
<result property="blogId" column="blog_id" />
|
||||
</resultMap>
|
||||
<resultMap type="CmsBlogType" id="blogTypeWithTypeResult">
|
||||
<result property="typeId" column="type_id" />
|
||||
<result property="blogId" column="blog_id" />
|
||||
<association property="type" javaType="com.ruoyi.cms.type.domain.CmsType">
|
||||
<id property="typeId" column="type_id" />
|
||||
<result property="typeName" column="type_name" />
|
||||
</association>
|
||||
</resultMap>
|
||||
|
||||
<insert id="batchBlogType">
|
||||
insert into cms_blog_type(type_id, blog_id) values
|
||||
@@ -30,6 +38,15 @@
|
||||
<select id="selectBlogTypeList" parameterType="Long" resultMap="blogTypeResult">
|
||||
select type_id, blog_id from cms_blog_type where blog_id = #{blogId}
|
||||
</select>
|
||||
<select id="selectBlogTypesByBlogIds" resultMap="blogTypeWithTypeResult">
|
||||
select bt.blog_id, t.type_id, t.type_name
|
||||
from cms_blog_type bt
|
||||
inner join cms_type t on t.type_id = bt.type_id
|
||||
where bt.blog_id in
|
||||
<foreach collection="list" item="blogId" open="(" separator="," close=")">
|
||||
#{blogId}
|
||||
</foreach>
|
||||
</select>
|
||||
<select id="countBlogByTypeId" parameterType="Long" resultType="java.lang.Integer">
|
||||
select count(*) from cms_blog_type where type_id=#{typeId}
|
||||
</select>
|
||||
|
||||
@@ -11,6 +11,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<result property="updateBy" column="update_by" />
|
||||
<result property="updateTime" column="update_time" />
|
||||
<result property="tagName" column="tag_name" />
|
||||
<result property="blogNum" column="blog_num" />
|
||||
</resultMap>
|
||||
|
||||
<sql id="selectCmsTagVo">
|
||||
@@ -18,12 +19,16 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
</sql>
|
||||
|
||||
<select id="selectCmsTagList" parameterType="CmsTag" resultMap="CmsTagResult">
|
||||
<include refid="selectCmsTagVo"/>
|
||||
select t.tag_id, t.create_by, t.create_time, t.update_by, t.update_time, t.tag_name,
|
||||
count(bt.blog_id) as blog_num
|
||||
from cms_tag t
|
||||
left join cms_blog_tag bt on bt.tag_id = t.tag_id
|
||||
<where>
|
||||
<if test="tagName != null and tagName != ''"> and tag_name like concat('%', #{tagName}, '%')</if>
|
||||
<if test="createBy != null and createBy != ''"> and create_by = #{createBy}</if>
|
||||
<if test="tagName != null and tagName != ''"> and t.tag_name like concat('%', #{tagName}, '%')</if>
|
||||
<if test="createBy != null and createBy != ''"> and t.create_by = #{createBy}</if>
|
||||
</where>
|
||||
ORDER BY create_time DESC
|
||||
group by t.tag_id, t.create_by, t.create_time, t.update_by, t.update_time, t.tag_name
|
||||
ORDER BY t.create_time DESC
|
||||
</select>
|
||||
|
||||
<select id="selectCmsTagByTagId" parameterType="Long" resultMap="CmsTagResult">
|
||||
|
||||
@@ -14,6 +14,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<result property="typePicType" column="type_pic_type" />
|
||||
<result property="typePic" column="type_pic" />
|
||||
<result property="typePicLink" column="type_pic_link" />
|
||||
<result property="blogNum" column="blog_num" />
|
||||
</resultMap>
|
||||
|
||||
<sql id="selectCmsTypeVo">
|
||||
@@ -21,13 +22,18 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
</sql>
|
||||
|
||||
<select id="selectCmsTypeList" parameterType="CmsType" resultMap="CmsTypeResult">
|
||||
<include refid="selectCmsTypeVo"/>
|
||||
select t.type_id, t.create_by, t.create_time, t.update_by, t.update_time, t.type_name,
|
||||
t.type_pic_type, t.type_pic, t.type_pic_link, count(bt.blog_id) as blog_num
|
||||
from cms_type t
|
||||
left join cms_blog_type bt on bt.type_id = t.type_id
|
||||
<where>
|
||||
<if test="typeName != null and typeName != ''"> and type_name like concat('%', #{typeName}, '%')</if>
|
||||
<if test="typePic != null and typePic != ''"> and type_pic = #{typePic}</if>
|
||||
<if test="createBy != null and createBy != ''"> and create_by = #{createBy}</if>
|
||||
<if test="typeName != null and typeName != ''"> and t.type_name like concat('%', #{typeName}, '%')</if>
|
||||
<if test="typePic != null and typePic != ''"> and t.type_pic = #{typePic}</if>
|
||||
<if test="createBy != null and createBy != ''"> and t.create_by = #{createBy}</if>
|
||||
</where>
|
||||
ORDER BY create_time DESC
|
||||
group by t.type_id, t.create_by, t.create_time, t.update_by, t.update_time, t.type_name,
|
||||
t.type_pic_type, t.type_pic, t.type_pic_link
|
||||
ORDER BY t.create_time DESC
|
||||
</select>
|
||||
|
||||
<select id="selectCmsTypeByTypeId" parameterType="Long" resultMap="CmsTypeResult">
|
||||
|
||||
46
ruoyi-cms/src/main/resources/mapper/cms/SitemapMapper.xml
Normal file
46
ruoyi-cms/src/main/resources/mapper/cms/SitemapMapper.xml
Normal file
@@ -0,0 +1,46 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper
|
||||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.ruoyi.cms.sitemap.mapper.SitemapMapper">
|
||||
|
||||
<resultMap type="com.ruoyi.cms.sitemap.domain.SitemapUrl" id="SitemapUrlResult">
|
||||
<result property="id" column="id" />
|
||||
<result property="pageType" column="page_type" />
|
||||
<result property="lastModified" column="last_modified" />
|
||||
</resultMap>
|
||||
|
||||
<!-- Empty categories do not have an indexable category page. -->
|
||||
<select id="selectIndexableCategories" resultMap="SitemapUrlResult">
|
||||
SELECT
|
||||
t.type_id AS id,
|
||||
'category' AS page_type,
|
||||
CASE
|
||||
WHEN COALESCE(t.update_time, t.create_time) > MAX(COALESCE(b.update_time, b.create_time))
|
||||
THEN COALESCE(t.update_time, t.create_time)
|
||||
ELSE MAX(COALESCE(b.update_time, b.create_time))
|
||||
END AS last_modified
|
||||
FROM cms_type t
|
||||
INNER JOIN cms_blog_type bt ON bt.type_id = t.type_id
|
||||
INNER JOIN cms_blog b ON b.id = bt.blog_id AND b.status = '1'
|
||||
GROUP BY t.type_id, t.update_time, t.create_time
|
||||
ORDER BY t.type_id
|
||||
</select>
|
||||
|
||||
<!-- Published content has exactly one article or source canonical URL. -->
|
||||
<select id="selectIndexableContent" resultMap="SitemapUrlResult">
|
||||
SELECT
|
||||
b.id,
|
||||
CASE
|
||||
WHEN b.blog_files IS NOT NULL
|
||||
AND LOWER(REPLACE(TRIM(b.blog_files), ' ', '')) NOT IN ('', '[]', 'null')
|
||||
THEN 'source'
|
||||
ELSE 'article'
|
||||
END AS page_type,
|
||||
COALESCE(b.update_time, b.create_time) AS last_modified
|
||||
FROM cms_blog b
|
||||
WHERE b.status = '1'
|
||||
ORDER BY b.id
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
@@ -97,6 +97,8 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter
|
||||
// 过滤请求
|
||||
.authorizeRequests()
|
||||
.antMatchers("/profile/**").permitAll()
|
||||
// Public sitemap must be crawlable without a token.
|
||||
.antMatchers(HttpMethod.GET, "/sitemap.xml").permitAll()
|
||||
// 对于登录login 注册register 验证码captchaImage 允许匿名访问
|
||||
.antMatchers("/login", "/register", "/captchaImage").anonymous()
|
||||
// 首页获取文章信息 允许匿名访问
|
||||
|
||||
@@ -6,3 +6,6 @@ ENV = 'production'
|
||||
|
||||
# 若依管理系统/生产环境
|
||||
VUE_APP_BASE_API = '/prod-api'
|
||||
|
||||
# Canonical host used by public pages and the XML sitemap
|
||||
VUE_APP_SITE_URL = 'https://www.yidaima.cn'
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
<meta name="renderer" content="webkit">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||
<title><%= webpackConfig.name %></title>
|
||||
<title>南音源码库 - Java、Python、前端项目源码与开发教程</title>
|
||||
<meta name="description" content="南音源码库提供 Java、Python、Vue 等项目源码、开发教程、运行说明和技术文档,帮助开发者学习与快速搭建项目。">
|
||||
<!--[if lt IE 11]><script>window.location.href='/html/ie.html';</script><![endif]-->
|
||||
<style>
|
||||
html,
|
||||
|
||||
@@ -1,2 +1,9 @@
|
||||
User-agent: *
|
||||
Disallow: /
|
||||
Allow: /
|
||||
|
||||
Disallow: /login
|
||||
Disallow: /cmsLogin
|
||||
Disallow: /cmsRegister
|
||||
Disallow: /admin/
|
||||
|
||||
Sitemap: https://www.yidaima.cn/sitemap.xml
|
||||
|
||||
@@ -8,11 +8,20 @@
|
||||
export default {
|
||||
name: 'App',
|
||||
metaInfo() {
|
||||
const meta = []
|
||||
if (this.$route.meta.public !== true) {
|
||||
meta.push({
|
||||
vmid: 'robots',
|
||||
name: 'robots',
|
||||
content: 'noindex,nofollow'
|
||||
})
|
||||
}
|
||||
return {
|
||||
title: this.$store.state.settings.dynamicTitle && this.$store.state.settings.title,
|
||||
titleTemplate: title => {
|
||||
return title ? `${title} - ${process.env.VUE_APP_TITLE}` : process.env.VUE_APP_TITLE
|
||||
}
|
||||
},
|
||||
meta
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import request from '@/utils/request'
|
||||
import { praseStrEmpty } from "@/utils/ruoyi";
|
||||
import { praseStrEmpty } from '@/utils/ruoyi'
|
||||
|
||||
// 首页查询文章列表
|
||||
export function cmsListBlog(query) {
|
||||
@@ -27,25 +27,27 @@ export function getBlogDetail(id) {
|
||||
}
|
||||
|
||||
// 首页按分类查询文章列表
|
||||
export function cmsListByTypeId(id) {
|
||||
export function cmsListByTypeId(id, query) {
|
||||
return request({
|
||||
url: '/cms/blog/cms/cmsListByType/' + id,
|
||||
headers: {
|
||||
isToken: false
|
||||
},
|
||||
method: 'get',
|
||||
params: query,
|
||||
timeout: 40000
|
||||
})
|
||||
}
|
||||
|
||||
// 首页按标签查询文章列表
|
||||
export function cmsListByTagId(id) {
|
||||
export function cmsListByTagId(id, query) {
|
||||
return request({
|
||||
url: '/cms/blog/cms/cmsListByTag/' + id,
|
||||
headers: {
|
||||
isToken: false
|
||||
},
|
||||
method: 'get',
|
||||
params: query,
|
||||
timeout: 40000
|
||||
})
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ router.beforeEach((to, from, next) => {
|
||||
}
|
||||
} else {
|
||||
// 没有token
|
||||
if (whiteList.indexOf(to.path) !== -1) {
|
||||
if (whiteList.indexOf(to.path) !== -1 || to.meta.public === true) {
|
||||
// 在免登录白名单,直接进入
|
||||
next()
|
||||
} else {
|
||||
|
||||
@@ -36,12 +36,82 @@ export const constantRoutes = [{
|
||||
component: (resolve) => require(['@/views/redirect'], resolve)
|
||||
}]
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
component: (resolve) => require(['@/views/cms/index'], resolve),
|
||||
hidden: true,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
component: (resolve) => require(['@/views/cms/components/cmsIndex'], resolve),
|
||||
name: 'home',
|
||||
meta: {
|
||||
title: '首页',
|
||||
public: true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/cms',
|
||||
component: (resolve) => require(['@/views/cms/index'], resolve),
|
||||
hidden: true,
|
||||
redirect: '/cms/main/cmsIndex',
|
||||
redirect: '/',
|
||||
children: [
|
||||
{
|
||||
path: '/projects',
|
||||
component: (resolve) => require(['@/views/cms/components/cmsIndex'], resolve),
|
||||
name: 'projects',
|
||||
meta: {
|
||||
title: '源码项目',
|
||||
public: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/categories',
|
||||
component: (resolve) => require(['@/views/cms/components/cmsCategories'], resolve),
|
||||
name: 'categories',
|
||||
meta: {
|
||||
title: '项目分类',
|
||||
public: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/category/:typeId(\\d+)',
|
||||
component: (resolve) => require(['@/views/cms/components/cmsIndex'], resolve),
|
||||
name: 'category',
|
||||
meta: {
|
||||
title: '分类',
|
||||
public: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/tag/:tagId(\\d+)',
|
||||
component: (resolve) => require(['@/views/cms/components/cmsIndex'], resolve),
|
||||
name: 'tag',
|
||||
meta: {
|
||||
title: '标签',
|
||||
public: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/article/:id(\\d+)',
|
||||
component: (resolve) => require(['@/views/cms/components/cmsBlog'], resolve),
|
||||
name: 'article',
|
||||
meta: {
|
||||
title: '源码项目详情',
|
||||
public: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/source/:id(\\d+)',
|
||||
component: (resolve) => require(['@/views/cms/components/cmsBlog'], resolve),
|
||||
name: 'source',
|
||||
meta: {
|
||||
title: '源码项目详情',
|
||||
public: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'main',
|
||||
component: (resolve) => require(['@/views/cms/main'], resolve),
|
||||
@@ -49,11 +119,7 @@ export const constantRoutes = [{
|
||||
children: [
|
||||
{
|
||||
path: 'cmsIndex',
|
||||
component: (resolve) => require(['@/views/cms/components/cmsIndex'], resolve),
|
||||
name: 'cmsIndex',
|
||||
meta: {
|
||||
title: '首页',
|
||||
}
|
||||
redirect: '/'
|
||||
},
|
||||
{
|
||||
path: 'essay',
|
||||
@@ -77,6 +143,7 @@ export const constantRoutes = [{
|
||||
name: 'blog',
|
||||
meta: {
|
||||
title: '博客详情',
|
||||
public: true
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -125,11 +192,11 @@ export const constantRoutes = [{
|
||||
hidden: true
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
path: '/dashboard',
|
||||
component: Layout,
|
||||
redirect: '/cms',
|
||||
redirect: '/index',
|
||||
children: [{
|
||||
path: 'index',
|
||||
path: '/index',
|
||||
component: (resolve) => require(['@/views/index_v2'], resolve),
|
||||
name: 'Index',
|
||||
meta: {
|
||||
|
||||
128
ruoyi-ui/src/utils/seo.js
Normal file
128
ruoyi-ui/src/utils/seo.js
Normal file
@@ -0,0 +1,128 @@
|
||||
const configuredSiteUrl = process.env.VUE_APP_SITE_URL
|
||||
const siteName = '南音源码库'
|
||||
|
||||
export function siteUrl() {
|
||||
const fallback = typeof window !== 'undefined' ? window.location.origin : ''
|
||||
return (configuredSiteUrl || fallback).replace(/\/+$/, '')
|
||||
}
|
||||
|
||||
export function isSourceContent(blog) {
|
||||
if (!blog || blog.blogFiles == null) {
|
||||
return false
|
||||
}
|
||||
if (Array.isArray(blog.blogFiles)) {
|
||||
return blog.blogFiles.length > 0
|
||||
}
|
||||
if (typeof blog.blogFiles !== 'string') {
|
||||
return true
|
||||
}
|
||||
|
||||
const value = blog.blogFiles.trim()
|
||||
if (!value || value === '[]' || value.toLowerCase() === 'null') {
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
const files = JSON.parse(value)
|
||||
return Array.isArray(files) ? files.length > 0 : files != null
|
||||
} catch (error) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
export function contentPath(blog) {
|
||||
if (!blog || blog.id == null) {
|
||||
return '/'
|
||||
}
|
||||
return `/source/${blog.id}`
|
||||
}
|
||||
|
||||
export function canonicalUrl(path) {
|
||||
return `${siteUrl()}${path}`
|
||||
}
|
||||
|
||||
export function absoluteUrl(value) {
|
||||
if (!value) {
|
||||
return ''
|
||||
}
|
||||
if (/^https?:\/\//i.test(value)) {
|
||||
return value
|
||||
}
|
||||
const path = value.startsWith('/') ? value : `/${value}`
|
||||
return canonicalUrl(path)
|
||||
}
|
||||
|
||||
export function blogImageUrl(blog) {
|
||||
if (!blog) {
|
||||
return ''
|
||||
}
|
||||
if (blog.blogPicType === '0' && blog.blogPicLink) {
|
||||
return absoluteUrl(blog.blogPicLink)
|
||||
}
|
||||
if (!blog.blogPic) {
|
||||
return ''
|
||||
}
|
||||
if (/^https?:\/\//i.test(blog.blogPic)) {
|
||||
return blog.blogPic
|
||||
}
|
||||
const apiBase = process.env.VUE_APP_BASE_API || ''
|
||||
const imagePath = blog.blogPic.startsWith(apiBase)
|
||||
? blog.blogPic
|
||||
: `${apiBase}${blog.blogPic.startsWith('/') ? '' : '/'}${blog.blogPic}`
|
||||
return absoluteUrl(imagePath)
|
||||
}
|
||||
|
||||
export function plainText(value) {
|
||||
if (!value) {
|
||||
return ''
|
||||
}
|
||||
return String(value)
|
||||
.replace(/<script[\s\S]*?<\/script>/gi, ' ')
|
||||
.replace(/<style[\s\S]*?<\/style>/gi, ' ')
|
||||
.replace(/<[^>]+>/g, ' ')
|
||||
.replace(/ | /gi, ' ')
|
||||
.replace(/&/gi, '&')
|
||||
.replace(/</gi, '<')
|
||||
.replace(/>/gi, '>')
|
||||
.replace(/"/gi, '"')
|
||||
.replace(/'/gi, "'")
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim()
|
||||
}
|
||||
|
||||
export function seoDescription(blog, maxLength = 160) {
|
||||
const source = blog && (
|
||||
blog.blogDesc ||
|
||||
blog.contentMarkdown ||
|
||||
blog.content
|
||||
)
|
||||
const text = plainText(source)
|
||||
if (text.length <= maxLength) {
|
||||
return text
|
||||
}
|
||||
return `${text.slice(0, maxLength - 1).trim()}…`
|
||||
}
|
||||
|
||||
export function isoDate(value) {
|
||||
if (!value) {
|
||||
return undefined
|
||||
}
|
||||
const normalized = String(value).replace(' ', 'T')
|
||||
const parsed = new Date(normalized)
|
||||
return Number.isNaN(parsed.getTime()) ? normalized : parsed.toISOString()
|
||||
}
|
||||
|
||||
export function siteIdentity() {
|
||||
return {
|
||||
name: siteName,
|
||||
url: canonicalUrl('/')
|
||||
}
|
||||
}
|
||||
|
||||
export function jsonLdScript(vmid, json) {
|
||||
return {
|
||||
vmid,
|
||||
type: 'application/ld+json',
|
||||
json
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,70 @@
|
||||
<template>
|
||||
<div class="footer">
|
||||
<span style="color: #FFFFFF;line-height:40px;font-size: 12px;">Copyright © 2024-2025 www.yidaima.cn 版权所有</span>
|
||||
<span style="color: #FFFFFF;line-height:40px;font-size: 12px;">工信部备案:蒙ICP备17001571号</span>
|
||||
<footer class="site-footer">
|
||||
<div class="footer-wrap">
|
||||
<div class="footer-brand">
|
||||
<span></></span>
|
||||
<div>
|
||||
<strong>南音源码库</strong>
|
||||
<small>可运行的项目源码与配套资料</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer-meta">
|
||||
<span>Copyright © 2024-2026 yidaima.cn</span>
|
||||
<span>蒙ICP备17001571号</span>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'cmsFooter',
|
||||
name: 'cmsFooter'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.footer {
|
||||
text-align: center;
|
||||
.site-footer {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
color: rgba(255, 255, 255, .62);
|
||||
background: #132238;
|
||||
}
|
||||
|
||||
.footer-wrap {
|
||||
width: min(1180px, calc(100% - 40px));
|
||||
min-height: 76px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.footer-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.footer-brand > span {
|
||||
color: #ef6a3a;
|
||||
font: 700 12px/1 Consolas, monospace;
|
||||
}
|
||||
|
||||
.footer-brand div,
|
||||
.footer-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.footer-brand strong { color: #fff; font-size: 13px; }
|
||||
.footer-brand small,
|
||||
.footer-meta { font-size: 9px; }
|
||||
.footer-meta { align-items: flex-end; }
|
||||
|
||||
@media (max-width: 620px) {
|
||||
.footer-wrap { padding: 18px 0; align-items: flex-start; flex-direction: column; }
|
||||
.footer-meta { align-items: flex-start; }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,438 +1,433 @@
|
||||
<template>
|
||||
<el-header :style="'margin-bottom:'+ headerBottom +'px'">
|
||||
<h2 class="logo" @click="goToHome"><svg-icon icon-class="guide" /> 南音源码库</h2>
|
||||
<div class="bg-purple-light">
|
||||
<el-menu :default-active="activeIndex" router class="el-menu-demo" mode="horizontal" style="border: none;"
|
||||
background-color="rgba(0,0,0,0)" text-color="#fff" active-text-color="#ffd04b">
|
||||
<el-menu-item index="/cms/main/cmsIndex"><i class="el-icon-s-home" style="color: rgba(255, 255, 255);"></i>首页
|
||||
</el-menu-item>
|
||||
<el-menu-item :index="item.path" v-for="item in menulist" :key="item.id">
|
||||
<!-- 图标-->
|
||||
<i :class="item.icon" style="color: rgba(255, 255, 255);"></i>
|
||||
<!-- 文本-->
|
||||
{{item.authName}}
|
||||
</el-menu-item>
|
||||
</el-menu>
|
||||
</div>
|
||||
<div class="bg-purple-light el-menu-hidden" v-if="menuHiddenVisiable">
|
||||
<el-menu :default-active="activeIndex" router background-color="rgba(84,92,100,0.5)" text-color="#fff"
|
||||
active-text-color="#ffd04b">
|
||||
<el-menu-item index="/cms/main/cmsIndex" @click="menuAway"><i class="el-icon-s-home"
|
||||
style="color: rgba(255, 255, 255);"></i>首页</el-menu-item>
|
||||
<el-menu-item :index="item.path" v-for="item in menulist" :key="item.id" @click="menuAway">
|
||||
<!-- 图标-->
|
||||
<i :class="item.icon" style="color: rgba(255, 255, 255);"></i>
|
||||
<!-- 文本-->
|
||||
{{item.authName}}
|
||||
</el-menu-item>
|
||||
</el-menu>
|
||||
</div>
|
||||
<header class="site-header">
|
||||
<div class="nav-wrap">
|
||||
<router-link to="/" class="brand" aria-label="南音源码库首页">
|
||||
<span class="brand-mark"><span></></span></span>
|
||||
<span class="brand-copy">
|
||||
<strong>南音源码库</strong>
|
||||
<small>CODE · NOTES · IDEAS</small>
|
||||
</span>
|
||||
</router-link>
|
||||
|
||||
<div class="menu-expend" @click="menuExpend">
|
||||
<i class="el-icon-menu" style="color: rgba(255, 255, 255);"></i>
|
||||
</div>
|
||||
<nav class="desktop-nav" aria-label="主导航">
|
||||
<router-link exact to="/">首页</router-link>
|
||||
<router-link v-for="item in menulist" :key="item.id" :to="item.path">
|
||||
{{ item.authName }}
|
||||
</router-link>
|
||||
</nav>
|
||||
|
||||
<div v-if="searchInput" class="search_input">
|
||||
<el-input @focus="checkInput" @blur="notSearching()" class="search" placeholder="搜索博客"
|
||||
prefix-icon="el-icon-search" v-model="queryInfo.query" size="mini">
|
||||
</el-input>
|
||||
<ul v-if="searching">
|
||||
<li class="animate__animated animate__fadeInDown search-blog" v-for="blog in searchList" :key="blog.id"
|
||||
@click="getBlogInfo(blog.id)">
|
||||
<a><span v-html="blog.title"></span></a>
|
||||
<div class="nav-actions">
|
||||
<div class="search-box">
|
||||
<i class="el-icon-search"></i>
|
||||
<input
|
||||
v-model.trim="queryInfo.query"
|
||||
type="search"
|
||||
placeholder="搜索名称、编号或技术栈"
|
||||
aria-label="搜索源码项目"
|
||||
@focus="checkInput"
|
||||
@blur="notSearching"
|
||||
>
|
||||
<ul v-if="searching" class="search-results">
|
||||
<li v-for="blog in searchList" :key="blog.id">
|
||||
<router-link :to="contentPath(blog)">{{ blog.title }}</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div v-if="islogin" class="bg-purple">
|
||||
<el-dropdown class="avatar-container right-menu-item hover-effect" trigger="click">
|
||||
<div class="avatar-wrapper">
|
||||
<el-avatar class="user-avatar" :src="avatar" @error="errorHandler">
|
||||
<i class="el-icon-s-custom" />
|
||||
<el-dropdown v-if="islogin" trigger="click" class="account-menu">
|
||||
<button type="button" class="account-button">
|
||||
<el-avatar :size="30" :src="avatar" @error="errorHandler">
|
||||
<i class="el-icon-user-solid"></i>
|
||||
</el-avatar>
|
||||
<p class="avatar-Name">{{name}}</p>
|
||||
</div>
|
||||
<span>{{ name || '账户' }}</span>
|
||||
</button>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<router-link target="_blank" to="/index">
|
||||
<el-dropdown-item>管理博客</el-dropdown-item>
|
||||
</router-link>
|
||||
<el-dropdown-item divided @click.native="logout">
|
||||
<span>退出登录</span>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item divided @click.native="logout">退出登录</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
<div v-else class="bg-purple">
|
||||
<div class="avatar-wrapper">
|
||||
<!-- <el-avatar class="avatar" src="avatar" @error="errorHandler">
|
||||
<i class="el-icon-s-custom" @click="tologin"/>
|
||||
</el-avatar> -->
|
||||
<p class="avatar-Name" @click="tologin">登录 | 注册</p>
|
||||
<button v-else type="button" class="login-button" @click="tologin">登录</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="mobile-toggle"
|
||||
:aria-expanded="menuHiddenVisiable ? 'true' : 'false'"
|
||||
aria-label="展开导航"
|
||||
@click="menuExpend"
|
||||
>
|
||||
<i :class="menuHiddenVisiable ? 'el-icon-close' : 'el-icon-menu'"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</el-header>
|
||||
|
||||
<transition name="menu-slide">
|
||||
<nav v-if="menuHiddenVisiable" class="mobile-nav" aria-label="移动端导航">
|
||||
<router-link exact to="/" @click.native="menuAway">首页</router-link>
|
||||
<router-link v-for="item in menulist" :key="item.id" :to="item.path" @click.native="menuAway">
|
||||
{{ item.authName }}
|
||||
</router-link>
|
||||
</nav>
|
||||
</transition>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
mapGetters
|
||||
} from 'vuex'
|
||||
import {
|
||||
getToken
|
||||
} from '@/utils/auth'
|
||||
import 'element-ui/lib/theme-chalk/display.css'
|
||||
import {
|
||||
cmsListBlog,
|
||||
} from "@/api/cms/blog";
|
||||
import { mapGetters } from 'vuex'
|
||||
import { getToken } from '@/utils/auth'
|
||||
import { cmsListBlog } from '@/api/cms/blog'
|
||||
import { contentPath } from '@/utils/seo'
|
||||
|
||||
export default {
|
||||
name: 'cmsNavBar',
|
||||
data() {
|
||||
return {
|
||||
activeIndex: 'this.$router.path',
|
||||
islogin: false,
|
||||
searchInput: true,
|
||||
menuHiddenVisiable: false,
|
||||
headerBottom: 0,
|
||||
timer: null,
|
||||
queryInfo: {
|
||||
query: '',
|
||||
timer: null
|
||||
query: ''
|
||||
},
|
||||
searchList: [],
|
||||
searching: false,
|
||||
menulist: [{
|
||||
id: 1,
|
||||
authName: '随笔',
|
||||
path: '/cms/main/essay',
|
||||
icon: 'el-icon-edit',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
authName: '留言',
|
||||
path: '/cms/main/message',
|
||||
icon: 'el-icon-chat-dot-round',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
authName: '文档',
|
||||
path: '/cms/doucument',
|
||||
icon: 'el-icon-document',
|
||||
},
|
||||
menulist: [
|
||||
{ id: 1, authName: '源码项目', path: '/projects' },
|
||||
{ id: 3, authName: '运行教程', path: '/cms/doucument' },
|
||||
{ id: 4, authName: '开发笔记', path: '/cms/main/essay' }
|
||||
],
|
||||
// 查询参数
|
||||
queryParams: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
pageSize: 6,
|
||||
title: null,
|
||||
type: 1,
|
||||
content: null,
|
||||
top: null,
|
||||
views: null,
|
||||
status: null,
|
||||
},
|
||||
};
|
||||
status: null
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters([
|
||||
'avatar',
|
||||
'name'
|
||||
]),
|
||||
...mapGetters(['avatar', 'name'])
|
||||
},
|
||||
watch: {
|
||||
'queryInfo.query': {
|
||||
handler(value) {
|
||||
if (this.timer) {
|
||||
clearTimeout(this.timer)
|
||||
}
|
||||
this.timer = setTimeout(() => {
|
||||
this.searchBlog()
|
||||
}, 300)
|
||||
}
|
||||
'queryInfo.query'(value) {
|
||||
if (this.timer) clearTimeout(this.timer)
|
||||
this.timer = setTimeout(() => this.searchBlog(value), 300)
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.login();
|
||||
// this.ResponsiveLayout();
|
||||
this.islogin = Boolean(getToken())
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.timer) clearTimeout(this.timer)
|
||||
},
|
||||
methods: {
|
||||
menulistAdd() {
|
||||
//push()方法一般是添加到数组的最后的位置;unshift()方法是往最前面的位置添加。
|
||||
// this.menulist.push({id:"",authName:""})
|
||||
this.menulist.unshift({
|
||||
id: "",
|
||||
authName: ""
|
||||
})
|
||||
},
|
||||
//响应式布局
|
||||
ResponsiveLayout() {
|
||||
//浏览器窗口的内部高度
|
||||
var w = window.innerWidth ||
|
||||
document.body.clientWidth;
|
||||
//浏览器窗口的内部宽度
|
||||
var h = window.innerHeight ||
|
||||
document.body.clientHeight;
|
||||
console.log(w, h);
|
||||
},
|
||||
// 展开菜单栏
|
||||
contentPath,
|
||||
menuExpend() {
|
||||
this.menuHiddenVisiable = !this.menuHiddenVisiable
|
||||
if (this.menuHiddenVisiable === true) {
|
||||
this.headerBottom = (this.menulist.length + 1) * 56
|
||||
} else {
|
||||
this.headerBottom = 0
|
||||
}
|
||||
},
|
||||
//收起菜单
|
||||
menuAway() {
|
||||
this.menuHiddenVisiable = false;
|
||||
this.headerBottom = 0;
|
||||
this.menuHiddenVisiable = false
|
||||
},
|
||||
notSearching() {
|
||||
setTimeout(() => {
|
||||
this.searching = false
|
||||
}, 100)
|
||||
}, 150)
|
||||
},
|
||||
checkInput() {
|
||||
this.searching = this.queryInfo.query !== '';
|
||||
this.searching = Boolean(this.queryInfo.query && this.searchList.length)
|
||||
},
|
||||
tologin() {
|
||||
this.$router.push({
|
||||
path: "/cmsLogin"
|
||||
});
|
||||
},
|
||||
login() {
|
||||
let token = getToken();
|
||||
if (token == null || token == '') {
|
||||
this.islogin = false;
|
||||
} else {
|
||||
this.islogin = true;
|
||||
}
|
||||
this.$router.push('/cmsLogin')
|
||||
},
|
||||
errorHandler() {
|
||||
return true
|
||||
},
|
||||
async searchBlog() {
|
||||
if (this.queryInfo.query === '') {
|
||||
async searchBlog(value) {
|
||||
if (!value) {
|
||||
this.searchList = []
|
||||
this.searching = false
|
||||
return
|
||||
}
|
||||
this.queryParams.title = this.queryInfo.query;
|
||||
cmsListBlog(this.queryParams).then(response => {
|
||||
let lsitSize = response.rows.length;
|
||||
if(lsitSize>0){
|
||||
for(let i = 0;i<lsitSize;i++){
|
||||
let redTitle = this.brightenKeyword(response.rows[i].title,this.queryInfo.query);
|
||||
response.rows[i].title = redTitle;
|
||||
}
|
||||
};
|
||||
this.searchList = response.rows;
|
||||
if (this.searchList.length !== 0) {
|
||||
this.searching = true
|
||||
}
|
||||
});
|
||||
},
|
||||
//搜索关建字标红
|
||||
brightenKeyword(val, keyword) {
|
||||
const Reg = new RegExp(keyword, 'i');
|
||||
let res = '';
|
||||
if (val) {
|
||||
res = val.replace(Reg, `<span style="color: red;">${keyword}</span>`);
|
||||
return res;
|
||||
this.queryParams.title = value
|
||||
try {
|
||||
const response = await cmsListBlog(this.queryParams)
|
||||
this.searchList = response.rows || []
|
||||
this.searching = this.searchList.length > 0
|
||||
} catch (error) {
|
||||
this.searchList = []
|
||||
this.searching = false
|
||||
}
|
||||
},
|
||||
// 跳转到博客详情页
|
||||
getBlogInfo(blogId) {
|
||||
let routeUrl = this.$router.resolve({
|
||||
path: '/cms/main/blog',
|
||||
query: {
|
||||
id: blogId
|
||||
}
|
||||
});
|
||||
window.open(routeUrl.href, '_blank');
|
||||
},
|
||||
async logout() {
|
||||
logout() {
|
||||
this.$confirm('确定注销并退出系统登录吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
this.$store.dispatch('LogOut').then(() => {
|
||||
location.href = '/cms/main/cmsIndex';
|
||||
location.href = '/'
|
||||
})
|
||||
}).catch(() => {});
|
||||
},
|
||||
goToHome() {
|
||||
this.$router.push({
|
||||
path: '/cms/main/cmsIndex'
|
||||
});
|
||||
}).catch(() => {})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.el-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
align-items: center;
|
||||
transition: .2s;
|
||||
}
|
||||
|
||||
.el-header:hover {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.el-menu {
|
||||
flex-shrink: 0;
|
||||
background-color: rgba(0, 0, 0, 0) !important;
|
||||
}
|
||||
|
||||
.el-menu /deep/ .el-menu-item {
|
||||
background-color: rgba(0, 0, 0, 0) !important;
|
||||
}
|
||||
|
||||
|
||||
.el-menu /deep/ .el-menu-item i {
|
||||
color: rgba(255, 255, 255);
|
||||
}
|
||||
|
||||
.el-menu /deep/ .el-menu-item:hover i {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.el-menu /deep/ .el-menu-item:hover {
|
||||
background-color: rgba(0, 0, 0, 0.5) !important;
|
||||
}
|
||||
|
||||
.search_input {
|
||||
.site-header {
|
||||
height: 76px;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
z-index: 20;
|
||||
color: #fff;
|
||||
background: rgba(19, 34, 56, .97);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, .08);
|
||||
box-shadow: 0 12px 32px rgba(19, 34, 56, .12);
|
||||
}
|
||||
|
||||
.nav-wrap {
|
||||
width: min(1180px, calc(100% - 40px));
|
||||
height: 100%;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 40px;
|
||||
}
|
||||
|
||||
.search_input ul {
|
||||
position: absolute;
|
||||
top: 30px;
|
||||
width: 100%;
|
||||
border: 1px solid #e5e9ef;
|
||||
margin-top: 1px;
|
||||
background: #fff;
|
||||
z-index: 10000;
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 2px 4px rgba(58, 118, 142, 0.16);
|
||||
padding: 10px 0;
|
||||
.brand {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 11px;
|
||||
flex: 0 0 auto;
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.brand-mark {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border-radius: 12px;
|
||||
color: #fff;
|
||||
background: #ef6a3a;
|
||||
box-shadow: 0 8px 20px rgba(239, 106, 58, .28);
|
||||
font: 700 13px/1 Consolas, monospace;
|
||||
transform: rotate(-3deg);
|
||||
}
|
||||
|
||||
.brand-copy {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.brand-copy strong {
|
||||
font-size: 18px;
|
||||
letter-spacing: .04em;
|
||||
}
|
||||
|
||||
.brand-copy small {
|
||||
color: rgba(255, 255, 255, .5);
|
||||
font-size: 8px;
|
||||
letter-spacing: .18em;
|
||||
}
|
||||
|
||||
.desktop-nav {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 30px;
|
||||
}
|
||||
|
||||
.desktop-nav a {
|
||||
height: 100%;
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
color: rgba(255, 255, 255, .7);
|
||||
font-size: 14px;
|
||||
box-sizing: border-box;
|
||||
text-decoration: none;
|
||||
transition: color .2s ease;
|
||||
}
|
||||
|
||||
.search_input ul li {
|
||||
padding: 0 16px;
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
cursor: pointer;
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
display: block;
|
||||
color: #222;
|
||||
position: relative;
|
||||
/*transition: .2 ease;*/
|
||||
}
|
||||
|
||||
.search_input ul li:hover {
|
||||
background-color: #e8f3ff;
|
||||
}
|
||||
|
||||
|
||||
.search-blog {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
.bg-purple-light {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.bg-purple {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
float: left;
|
||||
cursor: pointer;
|
||||
border: dashed rgba($color: #ffff7f, $alpha: 0.5);
|
||||
}
|
||||
|
||||
.avatar-container {
|
||||
margin-right: 30px;
|
||||
}
|
||||
|
||||
.avatar-wrapper {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.avatar-Name {
|
||||
margin-left: 10px;
|
||||
cursor: pointer;
|
||||
float: right;
|
||||
font-size: 16px;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.logo {
|
||||
float: left;
|
||||
color: #019cfd;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.logo:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.el-menu-hidden {
|
||||
/*display: none;*/
|
||||
.desktop-nav a::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 60px;
|
||||
right: 50%;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
height: 3px;
|
||||
background: #ef6a3a;
|
||||
transition: right .2s ease, left .2s ease;
|
||||
}
|
||||
|
||||
.desktop-nav a:hover,
|
||||
.desktop-nav a.router-link-active {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.desktop-nav a:hover::after,
|
||||
.desktop-nav a.router-link-active::after {
|
||||
right: 0;
|
||||
left: 0;
|
||||
border-top: 1px solid #ccc;
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.nav-actions {
|
||||
min-width: 0;
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
width: 224px;
|
||||
height: 38px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 0 12px;
|
||||
border: 1px solid rgba(255, 255, 255, .13);
|
||||
border-radius: 11px;
|
||||
background: rgba(255, 255, 255, .07);
|
||||
}
|
||||
|
||||
.search-box i {
|
||||
color: rgba(255, 255, 255, .5);
|
||||
}
|
||||
|
||||
.search-box input {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
color: #fff;
|
||||
background: transparent;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.menu-expend {
|
||||
display: none !important;
|
||||
.search-box input::placeholder {
|
||||
color: rgba(255, 255, 255, .42);
|
||||
}
|
||||
|
||||
/* 窗口可视区域小于1000隐藏搜索框 */
|
||||
@media screen and (max-width: 1000px) {
|
||||
.search_input {
|
||||
display: none;
|
||||
}
|
||||
.search-results {
|
||||
width: 320px;
|
||||
position: absolute;
|
||||
top: 48px;
|
||||
right: 0;
|
||||
margin: 0;
|
||||
padding: 8px;
|
||||
list-style: none;
|
||||
border: 1px solid #e6e1d8;
|
||||
border-radius: 14px;
|
||||
background: #fffdf8;
|
||||
box-shadow: 0 20px 50px rgba(19, 34, 56, .18);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.el-menu /deep/ .el-menu-item {
|
||||
background-color: rgba(0, 0, 0, 0.3) !important;
|
||||
.search-results a {
|
||||
display: block;
|
||||
padding: 11px 12px;
|
||||
overflow: hidden;
|
||||
color: #26364c;
|
||||
border-radius: 9px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.el-menu-demo {
|
||||
display: none;
|
||||
.search-results a:hover {
|
||||
color: #d8562d;
|
||||
background: #fff0e8;
|
||||
}
|
||||
|
||||
.menu-expend {
|
||||
display: block !important;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.menu-expend:hover {
|
||||
color: #ffd04b;
|
||||
.account-button,
|
||||
.login-button,
|
||||
.mobile-toggle {
|
||||
border: 0;
|
||||
color: #fff;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.account-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.login-button {
|
||||
height: 38px;
|
||||
padding: 0 18px;
|
||||
border: 1px solid rgba(255, 255, 255, .2);
|
||||
border-radius: 10px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.login-button:hover {
|
||||
border-color: #ef6a3a;
|
||||
background: #ef6a3a;
|
||||
}
|
||||
|
||||
.mobile-toggle {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
display: none;
|
||||
border-radius: 10px;
|
||||
background: rgba(255, 255, 255, .08);
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.mobile-nav {
|
||||
position: absolute;
|
||||
top: 76px;
|
||||
right: 0;
|
||||
left: 0;
|
||||
display: none;
|
||||
padding: 12px 20px 18px;
|
||||
background: #132238;
|
||||
box-shadow: 0 18px 30px rgba(19, 34, 56, .18);
|
||||
}
|
||||
|
||||
.mobile-nav a {
|
||||
display: block;
|
||||
padding: 13px 12px;
|
||||
color: rgba(255, 255, 255, .78);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, .08);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.menu-slide-enter-active,
|
||||
.menu-slide-leave-active {
|
||||
transition: opacity .2s ease, transform .2s ease;
|
||||
}
|
||||
|
||||
.menu-slide-enter,
|
||||
.menu-slide-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(-8px);
|
||||
}
|
||||
|
||||
@media (max-width: 1020px) {
|
||||
.nav-wrap { gap: 24px; }
|
||||
.desktop-nav { gap: 20px; }
|
||||
.search-box { width: 190px; }
|
||||
}
|
||||
|
||||
@media (max-width: 820px) {
|
||||
.site-header { height: 68px; }
|
||||
.nav-wrap { width: min(100% - 28px, 1180px); }
|
||||
.brand-mark { width: 36px; height: 36px; }
|
||||
.brand-copy small { display: none; }
|
||||
.desktop-nav,
|
||||
.search-box,
|
||||
.account-menu span,
|
||||
.login-button { display: none; }
|
||||
.mobile-toggle,
|
||||
.mobile-nav { display: block; }
|
||||
.mobile-nav { top: 68px; }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -141,7 +141,7 @@ export default {
|
||||
Cookies.remove('rememberMe');
|
||||
}
|
||||
this.$store.dispatch("Login", this.loginForm).then(() => {
|
||||
this.$router.push({ path: "/cms/main/cmsIndex" });
|
||||
this.$router.push({ path: "/" });
|
||||
}).catch(() => {
|
||||
this.loading = false;
|
||||
if (this.captchaOnOff) {
|
||||
|
||||
@@ -365,9 +365,8 @@ export default {
|
||||
// 跳转到评论页
|
||||
getBlogInfo(mes) {
|
||||
let routeUrl = this.$router.resolve({
|
||||
path: '/cms/main/blog',
|
||||
path: `/article/${mes.blogId}`,
|
||||
query: {
|
||||
id: mes.blogId,
|
||||
commentId: mes.id
|
||||
}
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
342
ruoyi-ui/src/views/cms/components/cmsCategories.vue
Normal file
342
ruoyi-ui/src/views/cms/components/cmsCategories.vue
Normal file
@@ -0,0 +1,342 @@
|
||||
<template>
|
||||
<div class="categories-page">
|
||||
<section class="categories-hero">
|
||||
<div>
|
||||
<span class="eyebrow"><i></i> PROJECT DIRECTORY</span>
|
||||
<h1>按方向发现源码项目</h1>
|
||||
<p>从项目场景或技术栈出发,快速缩小范围。每个分类都可以继续查看完整项目、功能截图和运行说明。</p>
|
||||
</div>
|
||||
<div class="directory-stats">
|
||||
<div><strong>{{ fullTypeList.length }}</strong><span>项目类型</span></div>
|
||||
<div><strong>{{ fullTagList.length }}</strong><span>技术标签</span></div>
|
||||
<router-link to="/projects">浏览全部项目 <i class="el-icon-right"></i></router-link>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<main v-loading="loading" class="directory-content">
|
||||
<section class="type-section">
|
||||
<header class="section-heading">
|
||||
<div>
|
||||
<span>01 · PROJECT TYPES</span>
|
||||
<h2>项目类型</h2>
|
||||
</div>
|
||||
<p>按照应用形态和开发方向选择</p>
|
||||
</header>
|
||||
|
||||
<div class="type-grid">
|
||||
<router-link
|
||||
v-for="(type, index) in fullTypeList"
|
||||
:key="type.typeId"
|
||||
:to="`/category/${type.typeId}`"
|
||||
class="type-card"
|
||||
>
|
||||
<div class="type-card-top">
|
||||
<span class="type-index">{{ String(index + 1).padStart(2, '0') }}</span>
|
||||
<span class="type-icon"><i :class="typeIcon(type.typeName)"></i></span>
|
||||
</div>
|
||||
<h3>{{ type.typeName }}</h3>
|
||||
<p>{{ typeDescription(type.typeName) }}</p>
|
||||
<div class="type-card-bottom">
|
||||
<span><strong>{{ type.blogNum || 0 }}</strong> 个项目</span>
|
||||
<b>查看分类 <i class="el-icon-right"></i></b>
|
||||
</div>
|
||||
</router-link>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="stack-section">
|
||||
<header class="section-heading light-heading">
|
||||
<div>
|
||||
<span>02 · TECH STACKS</span>
|
||||
<h2>按技术栈筛选</h2>
|
||||
</div>
|
||||
<p>直接定位熟悉或准备学习的开发技术</p>
|
||||
</header>
|
||||
|
||||
<div class="stack-cloud">
|
||||
<router-link v-for="tag in fullTagList" :key="tag.tagId" :to="`/tag/${tag.tagId}`">
|
||||
<span>#</span>
|
||||
<strong>{{ tag.tagName }}</strong>
|
||||
<small>{{ tag.blogNum || 0 }} PROJECTS</small>
|
||||
</router-link>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="directory-guide">
|
||||
<div>
|
||||
<span class="guide-mark"></></span>
|
||||
<div><strong>还不确定选哪个方向?</strong><p>进入完整项目库,通过关键词、编号和技术标签组合筛选。</p></div>
|
||||
</div>
|
||||
<router-link to="/projects">进入源码项目库 <i class="el-icon-right"></i></router-link>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<el-backtop :bottom="32" :right="28">
|
||||
<div class="back-top"><i class="el-icon-top"></i></div>
|
||||
</el-backtop>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getBlogDetail } from '@/api/cms/blog'
|
||||
import { canonicalUrl } from '@/utils/seo'
|
||||
|
||||
export default {
|
||||
name: 'cmsCategories',
|
||||
data() {
|
||||
return {
|
||||
loading: true,
|
||||
fullTypeList: [],
|
||||
fullTagList: []
|
||||
}
|
||||
},
|
||||
metaInfo() {
|
||||
const title = '项目分类与技术栈 - 南音源码库'
|
||||
const description = '按项目类型与技术栈浏览 Java、Spring Boot、Vue、微信小程序等完整源码项目。'
|
||||
return {
|
||||
title,
|
||||
titleTemplate: null,
|
||||
meta: [
|
||||
{ vmid: 'description', name: 'description', content: description },
|
||||
{ vmid: 'robots', name: 'robots', content: 'index,follow' },
|
||||
{ vmid: 'og:title', property: 'og:title', content: title },
|
||||
{ vmid: 'og:description', property: 'og:description', content: description }
|
||||
],
|
||||
link: [{ vmid: 'canonical', rel: 'canonical', href: canonicalUrl('/categories') }]
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.loadDirectory()
|
||||
},
|
||||
methods: {
|
||||
async loadDirectory() {
|
||||
this.loading = true
|
||||
try {
|
||||
const response = await getBlogDetail()
|
||||
this.fullTypeList = response.types || []
|
||||
this.fullTagList = response.tags || []
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
typeIcon(name) {
|
||||
const value = String(name || '').toLowerCase()
|
||||
if (value.includes('小程序')) return 'el-icon-mobile-phone'
|
||||
if (value.includes('spring')) return 'el-icon-cpu'
|
||||
if (value.includes('java')) return 'el-icon-monitor'
|
||||
return 'el-icon-folder-opened'
|
||||
},
|
||||
typeDescription(name) {
|
||||
const value = String(name || '').toLowerCase()
|
||||
if (value.includes('小程序')) return '适合移动服务、校园工具、商城与预约类应用。'
|
||||
if (value.includes('spring')) return '主流 Java 企业级项目,前后端分离,便于二次开发。'
|
||||
if (value.includes('ssm')) return '经典 Java Web 技术组合,适合课程设计和基础学习。'
|
||||
if (value.includes('java')) return '包含管理系统、信息平台与业务应用等常见选题。'
|
||||
return '查看该方向下的完整项目源码、截图与运行说明。'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.categories-page {
|
||||
width: 100%;
|
||||
padding-bottom: 72px;
|
||||
color: #132238;
|
||||
}
|
||||
|
||||
.categories-hero {
|
||||
width: min(1180px, calc(100% - 40px));
|
||||
margin: 0 auto;
|
||||
padding: 68px 0 54px;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.2fr) minmax(340px, .8fr);
|
||||
align-items: end;
|
||||
gap: 80px;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
color: #cf562f;
|
||||
font: 700 11px/1.4 Consolas, Monaco, monospace;
|
||||
letter-spacing: .18em;
|
||||
}
|
||||
|
||||
.eyebrow i {
|
||||
width: 34px;
|
||||
height: 2px;
|
||||
margin-right: 12px;
|
||||
background: #ef6a3a;
|
||||
}
|
||||
|
||||
.categories-hero h1 {
|
||||
margin: 22px 0 16px;
|
||||
font-size: clamp(40px, 5vw, 62px);
|
||||
line-height: 1.12;
|
||||
letter-spacing: -.05em;
|
||||
}
|
||||
|
||||
.categories-hero p {
|
||||
max-width: 660px;
|
||||
margin: 0;
|
||||
color: #667286;
|
||||
font-size: 16px;
|
||||
line-height: 1.9;
|
||||
}
|
||||
|
||||
.directory-stats {
|
||||
padding: 24px;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 18px;
|
||||
border: 1px solid #ded8ce;
|
||||
border-radius: 18px;
|
||||
background: rgba(255, 253, 248, .9);
|
||||
box-shadow: 0 18px 42px rgba(19, 34, 56, .08);
|
||||
}
|
||||
|
||||
.directory-stats div { display: flex; flex-direction: column; gap: 5px; }
|
||||
.directory-stats strong { font: 700 30px/1 Consolas, monospace; }
|
||||
.directory-stats span { color: #8b929d; font-size: 11px; }
|
||||
|
||||
.directory-stats a {
|
||||
grid-column: 1 / -1;
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid #e4ded4;
|
||||
color: #d85831;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.directory-content { width: min(1180px, calc(100% - 40px)); margin: 0 auto; }
|
||||
.type-section { padding: 42px 0 64px; border-top: 1px solid #ddd8cf; }
|
||||
|
||||
.section-heading {
|
||||
margin-bottom: 25px;
|
||||
display: flex;
|
||||
align-items: end;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.section-heading span { color: #df6238; font: 700 10px/1 Consolas, monospace; letter-spacing: .16em; }
|
||||
.section-heading h2 { margin: 10px 0 0; font-size: 30px; letter-spacing: -.03em; }
|
||||
.section-heading > p { color: #9298a2; font-size: 12px; }
|
||||
|
||||
.type-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 18px; }
|
||||
|
||||
.type-card {
|
||||
min-height: 240px;
|
||||
padding: 26px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
border: 1px solid #ddd8cf;
|
||||
border-radius: 18px;
|
||||
color: #132238;
|
||||
background: rgba(255, 253, 248, .92);
|
||||
box-shadow: 0 10px 30px rgba(19, 34, 56, .04);
|
||||
text-decoration: none;
|
||||
transition: transform .2s ease, border-color .2s ease, box-shadow .2s ease;
|
||||
}
|
||||
|
||||
.type-card:hover { transform: translateY(-4px); border-color: #ef9b7d; box-shadow: 0 18px 38px rgba(19, 34, 56, .09); }
|
||||
.type-card-top { display: flex; align-items: center; justify-content: space-between; }
|
||||
.type-index { color: #dc633b; font: 700 11px/1 Consolas, monospace; letter-spacing: .12em; }
|
||||
|
||||
.type-icon {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border-radius: 12px;
|
||||
color: #28727a;
|
||||
background: #e8f2f0;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.type-card h3 { margin: 24px 0 9px; font-size: 23px; }
|
||||
.type-card > p { margin: 0; color: #7b8492; font-size: 13px; line-height: 1.75; }
|
||||
|
||||
.type-card-bottom {
|
||||
margin-top: auto;
|
||||
padding-top: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-top: 1px solid #ece6dc;
|
||||
}
|
||||
|
||||
.type-card-bottom span { color: #9198a3; font-size: 11px; }
|
||||
.type-card-bottom span strong { color: #172941; font: 700 16px/1 Consolas, monospace; }
|
||||
.type-card-bottom b { color: #d85831; font-size: 11px; }
|
||||
|
||||
.stack-section {
|
||||
padding: 42px;
|
||||
border-radius: 22px;
|
||||
color: #fff;
|
||||
background: #172941;
|
||||
box-shadow: 0 24px 50px rgba(19, 34, 56, .16);
|
||||
}
|
||||
|
||||
.light-heading h2 { color: #fff; }
|
||||
.light-heading > p { color: rgba(255, 255, 255, .42); }
|
||||
.stack-cloud { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 11px; }
|
||||
|
||||
.stack-cloud a {
|
||||
min-height: 92px;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 1px solid rgba(255, 255, 255, .09);
|
||||
border-radius: 12px;
|
||||
color: rgba(255, 255, 255, .84);
|
||||
background: rgba(255, 255, 255, .035);
|
||||
text-decoration: none;
|
||||
transition: border-color .2s ease, background .2s ease;
|
||||
}
|
||||
|
||||
.stack-cloud a:hover { border-color: rgba(239, 106, 58, .7); background: rgba(239, 106, 58, .11); }
|
||||
.stack-cloud a > span { color: #ef825d; font: 700 11px/1 Consolas, monospace; }
|
||||
.stack-cloud strong { margin: 11px 0 8px; overflow: hidden; font-size: 13px; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.stack-cloud small { margin-top: auto; color: rgba(255, 255, 255, .3); font: 700 8px/1 Consolas, monospace; letter-spacing: .08em; }
|
||||
|
||||
.directory-guide {
|
||||
margin-top: 30px;
|
||||
padding: 27px 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border: 1px solid #ddd8cf;
|
||||
border-radius: 18px;
|
||||
background: rgba(255, 253, 248, .88);
|
||||
}
|
||||
|
||||
.directory-guide > div { display: flex; align-items: center; gap: 18px; }
|
||||
.guide-mark { color: #ef6a3a; font: 700 22px/1 Consolas, monospace; }
|
||||
.directory-guide strong { font-size: 15px; }
|
||||
.directory-guide p { margin: 5px 0 0; color: #8b929d; font-size: 11px; }
|
||||
.directory-guide > a { padding: 13px 18px; border-radius: 10px; color: #fff; background: #ef6a3a; font-size: 11px; font-weight: 700; text-decoration: none; }
|
||||
|
||||
.back-top { width: 44px; height: 44px; display: grid; place-items: center; border-radius: 12px; color: #fff; background: #172941; }
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.categories-hero { grid-template-columns: 1fr; gap: 30px; }
|
||||
.directory-stats { max-width: 420px; }
|
||||
.stack-cloud { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.categories-page { padding-bottom: 48px; }
|
||||
.categories-hero, .directory-content { width: min(100% - 28px, 1180px); }
|
||||
.categories-hero { padding: 44px 0 36px; }
|
||||
.categories-hero h1 { font-size: 38px; }
|
||||
.type-grid { grid-template-columns: 1fr; }
|
||||
.section-heading { align-items: flex-start; flex-direction: column; gap: 8px; }
|
||||
.section-heading > p { margin: 0; }
|
||||
.stack-section { padding: 28px 20px; }
|
||||
.stack-cloud { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
.directory-guide { align-items: flex-start; flex-direction: column; gap: 20px; }
|
||||
}
|
||||
</style>
|
||||
@@ -1,39 +1,41 @@
|
||||
<template>
|
||||
<el-container>
|
||||
<div class="document-page">
|
||||
<iframe
|
||||
class="document-frame"
|
||||
src="/docs/index.html"
|
||||
id="qt"
|
||||
title="南音源码库项目文档"
|
||||
scrolling="auto"
|
||||
frameborder="0"
|
||||
style="position:absolute;top:64px;left: 0px;right:0px;bottom:100px;opacity:1;"
|
||||
></iframe>
|
||||
</el-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'CmsDocument',
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
mounted() {
|
||||
/**
|
||||
* iframe-宽高自适应显示
|
||||
*/
|
||||
function changeqtIframe() {
|
||||
const qt = document.getElementById('qt')
|
||||
const deviceWidth = document.body.clientWidth
|
||||
const deviceHeight = document.body.clientHeight
|
||||
qt.style.width = Number(deviceWidth) + 'px' // 数字是页面布局宽度差值
|
||||
qt.style.height = Number(deviceHeight) + 'px' // 数字是页面布局高度差
|
||||
}
|
||||
changeqtIframe()
|
||||
window.onresize = function() {
|
||||
changeqtIframe()
|
||||
}
|
||||
}
|
||||
name: 'CmsDocument'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped rel="stylesheet/scss" lang="scss">
|
||||
<style scoped>
|
||||
.document-page {
|
||||
width: 100%;
|
||||
height: calc(100vh - 76px);
|
||||
min-height: 620px;
|
||||
line-height: 0;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.document-frame {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 820px) {
|
||||
.document-page {
|
||||
height: calc(100vh - 68px);
|
||||
min-height: 520px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,56 +1,84 @@
|
||||
<template>
|
||||
<el-row :gutter="20">
|
||||
<el-col :sm="3" class="hidden-xs-only" style="opacity:0;">左侧占位</el-col>
|
||||
<el-col :xs="24" :sm="18">
|
||||
<el-container>
|
||||
<el-timeline class="animate__animated animate__fadeInLeft">
|
||||
<el-timeline-item :color="color" v-for="essay in essayList" :key="essay.id" :timestamp="essay.createTime"
|
||||
placement="top">
|
||||
<el-card
|
||||
style="letter-spacing: 1px;border: 1px solid rgba(255,190,23,0.9); background-color: rgba(255,255,255,0.9);box-shadow: 0 0 30px -10px ">
|
||||
<h2 v-if="essay.title" style="color: rgba(255,190,23,0.9);">{{essay.title}}</h2>
|
||||
<div v-if="essay.contentType ==='1'" class="typo ql-editor" v-html="essay.content"></div>
|
||||
<div v-else v-html="essay.content"></div>
|
||||
</el-card>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
</el-container>
|
||||
</el-col>
|
||||
<el-col :sm="3" class="hidden-xs-only" style="opacity:0;">右侧占位</el-col>
|
||||
<!-- 设置底部距离的 -->
|
||||
<el-backtop :bottom="60">
|
||||
<div
|
||||
style="{
|
||||
height: 50px;
|
||||
width: 50px;
|
||||
background-color: rgba(240,239,241,1);
|
||||
box-shadow: 0 0 6px rgba(0,0,0, .12);
|
||||
text-align: center;
|
||||
line-height: 40px;
|
||||
border-radius:2px;
|
||||
color: #1989fa;
|
||||
}"
|
||||
>
|
||||
<svg-icon icon-class="top" />
|
||||
<div class="notes-page">
|
||||
<section class="notes-hero" aria-labelledby="notes-title">
|
||||
<div>
|
||||
<p class="eyebrow"><span /> DEV NOTES</p>
|
||||
<h1 id="notes-title">开发笔记</h1>
|
||||
<p class="hero-copy">记录项目部署、问题排查与开发过程中的实用经验。</p>
|
||||
</div>
|
||||
<div class="hero-total" aria-label="笔记数量">
|
||||
<strong>{{ total }}</strong>
|
||||
<span>篇记录</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<main class="notes-content">
|
||||
<div v-if="loading" class="notes-state" aria-live="polite">
|
||||
<i class="el-icon-loading" />
|
||||
<span>正在加载开发笔记...</span>
|
||||
</div>
|
||||
|
||||
<div v-else-if="!essayList.length" class="notes-state notes-empty">
|
||||
<span class="empty-icon"><i class="el-icon-edit-outline" /></span>
|
||||
<h2>还没有开发笔记</h2>
|
||||
<p>新的记录会在这里按时间依次展示。</p>
|
||||
</div>
|
||||
|
||||
<ol v-else class="notes-timeline" aria-label="开发笔记时间线">
|
||||
<li v-for="(essay, index) in essayList" :key="essay.id" class="timeline-item">
|
||||
<div class="time-rail" :aria-label="essay.createTime || '未标记时间'">
|
||||
<span class="time-dot" />
|
||||
<time v-if="essay.createTime" :datetime="essay.createTime">
|
||||
<strong>{{ formatDate(essay.createTime) }}</strong>
|
||||
<span>{{ formatTime(essay.createTime) }}</span>
|
||||
</time>
|
||||
<span v-else class="undated">未标记日期</span>
|
||||
</div>
|
||||
|
||||
<article class="note-card" :class="{ 'without-header': !essay.title }">
|
||||
<header v-if="essay.title" class="note-header">
|
||||
<span class="note-number">NOTE {{ formatIndex(index) }}</span>
|
||||
<h2>{{ essay.title }}</h2>
|
||||
</header>
|
||||
<div
|
||||
class="note-body"
|
||||
:class="{ 'ql-editor': essay.contentType === '1' }"
|
||||
v-html="essay.content"
|
||||
/>
|
||||
</article>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<el-pagination
|
||||
v-if="total > queryParams.pageSize"
|
||||
class="notes-pagination"
|
||||
background
|
||||
layout="prev, pager, next"
|
||||
:current-page.sync="queryParams.pageNum"
|
||||
:page-size="queryParams.pageSize"
|
||||
:total="total"
|
||||
@current-change="handlePageChange"
|
||||
/>
|
||||
</main>
|
||||
|
||||
<el-backtop :bottom="32" :right="32" class="notes-backtop">
|
||||
<i class="el-icon-top" />
|
||||
</el-backtop>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// import 'cherry-markdown/dist/cherry-markdown.min.css'
|
||||
//使用prism.js代码高亮
|
||||
import '@/views/cms/plugins/prism.js'
|
||||
import '@/views/cms/plugins/prism.css'
|
||||
import {
|
||||
cmsEssayList,
|
||||
} from "@/api/cms/blog";
|
||||
import { cmsEssayList } from '@/api/cms/blog'
|
||||
|
||||
export default {
|
||||
name: 'essay',
|
||||
name: 'CmsEssay',
|
||||
data() {
|
||||
return {
|
||||
essayList: [],
|
||||
// 查询参数
|
||||
loading: true,
|
||||
total: 0,
|
||||
queryParams: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
@@ -59,9 +87,8 @@ import '@/views/cms/plugins/prism.css'
|
||||
content: null,
|
||||
top: null,
|
||||
views: null,
|
||||
status: null,
|
||||
},
|
||||
color: "#ffd04b",
|
||||
status: null
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
@@ -69,35 +96,461 @@ import '@/views/cms/plugins/prism.css'
|
||||
},
|
||||
methods: {
|
||||
async getEssayList() {
|
||||
cmsEssayList(this.queryParams).then(response => {
|
||||
this.essayList = response.rows;
|
||||
});
|
||||
this.loading = true
|
||||
try {
|
||||
const response = await cmsEssayList(this.queryParams)
|
||||
this.essayList = response.rows || []
|
||||
this.total = Number(response.total) || this.essayList.length
|
||||
} catch (error) {
|
||||
this.essayList = []
|
||||
this.total = 0
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
handlePageChange(page) {
|
||||
this.queryParams.pageNum = page
|
||||
this.getEssayList().then(() => {
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||
})
|
||||
},
|
||||
formatDate(value) {
|
||||
const date = String(value || '').split(/[ T]/)[0]
|
||||
return date ? date.replace(/-/g, '.') : ''
|
||||
},
|
||||
formatTime(value) {
|
||||
const time = String(value || '').split(/[ T]/)[1]
|
||||
return time ? time.slice(0, 5) : ''
|
||||
},
|
||||
formatIndex(index) {
|
||||
const position = (this.queryParams.pageNum - 1) * this.queryParams.pageSize + index + 1
|
||||
return String(position).padStart(2, '0')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.el-timeline {
|
||||
font: 16px/1.5 'Microsoft Yahei', 'PingFang SC', 'Hiragino Sans GB', sans-serif !important;
|
||||
|
||||
width: 80%;
|
||||
.notes-page {
|
||||
width: min(1180px, calc(100% - 40px));
|
||||
margin: 0 auto;
|
||||
padding: 54px 0 84px;
|
||||
color: #18283e;
|
||||
}
|
||||
|
||||
.el-card {
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 0 15px 5px white;
|
||||
.notes-hero {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
gap: 32px;
|
||||
margin-bottom: 48px;
|
||||
padding: 0 4px 30px;
|
||||
border-bottom: 1px solid rgba(24, 40, 62, .13);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.el-timeline {
|
||||
width: 98%;
|
||||
padding: 2px;
|
||||
.notes-hero::after {
|
||||
content: '';
|
||||
width: 76px;
|
||||
height: 3px;
|
||||
position: absolute;
|
||||
bottom: -2px;
|
||||
left: 4px;
|
||||
border-radius: 999px;
|
||||
background: #ef6a3a;
|
||||
}
|
||||
|
||||
.el-timeline /deep/ .el-timeline-item__wrapper {
|
||||
padding-left: 15px !important;
|
||||
.eyebrow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
margin: 0 0 13px;
|
||||
color: #d95730;
|
||||
font: 700 12px/1.2 Consolas, Monaco, monospace;
|
||||
letter-spacing: .16em;
|
||||
}
|
||||
|
||||
.eyebrow span {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
background: #ef6a3a;
|
||||
box-shadow: 0 0 0 5px rgba(239, 106, 58, .12);
|
||||
}
|
||||
|
||||
.notes-hero h1 {
|
||||
margin: 0;
|
||||
color: #132238;
|
||||
font-size: clamp(30px, 4vw, 44px);
|
||||
line-height: 1.15;
|
||||
letter-spacing: -.03em;
|
||||
}
|
||||
|
||||
.hero-copy {
|
||||
margin: 14px 0 0;
|
||||
color: #697386;
|
||||
font-size: 15px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.hero-total {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 7px;
|
||||
padding-bottom: 3px;
|
||||
color: #7b8493;
|
||||
}
|
||||
|
||||
.hero-total strong {
|
||||
color: #132238;
|
||||
font: 700 34px/1 Georgia, serif;
|
||||
}
|
||||
|
||||
.hero-total span {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.notes-content {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.notes-timeline {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.timeline-item {
|
||||
display: grid;
|
||||
grid-template-columns: 154px minmax(0, 1fr);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.timeline-item:not(:last-child) {
|
||||
padding-bottom: 34px;
|
||||
}
|
||||
|
||||
.time-rail {
|
||||
min-height: 100%;
|
||||
position: relative;
|
||||
padding: 21px 30px 0 0;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.time-rail::after {
|
||||
content: '';
|
||||
width: 1px;
|
||||
position: absolute;
|
||||
top: 31px;
|
||||
right: 0;
|
||||
bottom: -34px;
|
||||
background: linear-gradient(#cfd5dc, #e5e7e9);
|
||||
}
|
||||
|
||||
.timeline-item:last-child .time-rail::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.time-dot {
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: 25px;
|
||||
right: -6px;
|
||||
border: 3px solid #f4f1e9;
|
||||
border-radius: 50%;
|
||||
background: #ef6a3a;
|
||||
box-shadow: 0 0 0 1px rgba(239, 106, 58, .35);
|
||||
}
|
||||
|
||||
.time-rail time {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.time-rail strong {
|
||||
color: #3b485a;
|
||||
font-size: 13px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.time-rail time span,
|
||||
.undated {
|
||||
color: #969daa;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.note-card {
|
||||
min-width: 0;
|
||||
position: relative;
|
||||
margin-left: 30px;
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(24, 40, 62, .09);
|
||||
border-radius: 18px;
|
||||
background: rgba(255, 253, 248, .94);
|
||||
box-shadow: 0 14px 40px rgba(19, 34, 56, .08);
|
||||
transition: transform .25s ease, box-shadow .25s ease, border-color .25s ease;
|
||||
}
|
||||
|
||||
.note-card::before {
|
||||
content: '';
|
||||
width: 4px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
background: linear-gradient(180deg, #ef6a3a, #f2aa62);
|
||||
}
|
||||
|
||||
.note-card:hover {
|
||||
border-color: rgba(239, 106, 58, .24);
|
||||
box-shadow: 0 20px 52px rgba(19, 34, 56, .12);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.note-header {
|
||||
display: grid;
|
||||
grid-template-columns: 78px minmax(0, 1fr);
|
||||
gap: 20px;
|
||||
align-items: start;
|
||||
padding: 29px 38px 21px;
|
||||
border-bottom: 1px solid rgba(24, 40, 62, .08);
|
||||
}
|
||||
|
||||
.note-number {
|
||||
margin-top: 6px;
|
||||
color: #d75c35;
|
||||
font: 700 11px/1.2 Consolas, Monaco, monospace;
|
||||
letter-spacing: .1em;
|
||||
}
|
||||
|
||||
.note-header h2 {
|
||||
margin: 0;
|
||||
color: #17273d;
|
||||
font-size: 24px;
|
||||
line-height: 1.4;
|
||||
letter-spacing: -.01em;
|
||||
}
|
||||
|
||||
.note-body {
|
||||
padding: 27px 38px 34px 136px;
|
||||
color: #3e4958;
|
||||
font-size: 15px;
|
||||
line-height: 1.85;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.note-card.without-header .note-body {
|
||||
padding-left: 38px;
|
||||
}
|
||||
|
||||
.note-body /deep/ > :first-child {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
|
||||
.note-body /deep/ > :last-child {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.note-body /deep/ h1,
|
||||
.note-body /deep/ h2,
|
||||
.note-body /deep/ h3,
|
||||
.note-body /deep/ h4 {
|
||||
margin: 1.45em 0 .55em;
|
||||
color: #202d3f;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.note-body /deep/ h1 { font-size: 21px; }
|
||||
.note-body /deep/ h2 { font-size: 18px; }
|
||||
.note-body /deep/ h3,
|
||||
.note-body /deep/ h4 { font-size: 16px; }
|
||||
|
||||
.note-body /deep/ p {
|
||||
margin: .55em 0;
|
||||
}
|
||||
|
||||
.note-body /deep/ ul,
|
||||
.note-body /deep/ ol {
|
||||
margin: .55em 0;
|
||||
padding-left: 1.65em;
|
||||
}
|
||||
|
||||
.note-body /deep/ li + li {
|
||||
margin-top: .2em;
|
||||
}
|
||||
|
||||
.note-body /deep/ a {
|
||||
color: #d95730;
|
||||
text-decoration: none;
|
||||
border-bottom: 1px solid rgba(217, 87, 48, .3);
|
||||
}
|
||||
|
||||
.note-body /deep/ blockquote {
|
||||
margin: 1em 0;
|
||||
padding: 13px 18px;
|
||||
color: #566274;
|
||||
border-left: 3px solid #ef6a3a;
|
||||
background: #fff5ee;
|
||||
}
|
||||
|
||||
.note-body /deep/ img,
|
||||
.note-body /deep/ video {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.note-body /deep/ pre {
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.note-body /deep/ table {
|
||||
width: 100%;
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.notes-state {
|
||||
min-height: 280px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
color: #7a8493;
|
||||
}
|
||||
|
||||
.notes-state i {
|
||||
color: #ef6a3a;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.notes-empty {
|
||||
flex-direction: column;
|
||||
padding: 46px;
|
||||
border: 1px dashed rgba(24, 40, 62, .18);
|
||||
border-radius: 18px;
|
||||
background: rgba(255, 253, 248, .72);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
margin-bottom: 7px;
|
||||
border-radius: 16px;
|
||||
background: #fff0e8;
|
||||
}
|
||||
|
||||
.empty-icon i { font-size: 24px; }
|
||||
.notes-empty h2 { margin: 0; color: #26364c; font-size: 20px; }
|
||||
.notes-empty p { margin: 0; font-size: 14px; }
|
||||
|
||||
.notes-pagination {
|
||||
margin-top: 44px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.notes-pagination /deep/ .el-pager li:not(.disabled).active {
|
||||
background: #ef6a3a;
|
||||
}
|
||||
|
||||
.notes-backtop {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
color: #fff;
|
||||
border-radius: 13px;
|
||||
background: #17273d;
|
||||
box-shadow: 0 10px 26px rgba(19, 34, 56, .22);
|
||||
}
|
||||
|
||||
.notes-backtop:hover {
|
||||
background: #ef6a3a;
|
||||
}
|
||||
|
||||
@media (max-width: 820px) {
|
||||
.notes-page {
|
||||
width: min(100% - 28px, 1180px);
|
||||
padding: 38px 0 64px;
|
||||
}
|
||||
|
||||
.notes-hero {
|
||||
margin-bottom: 34px;
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
|
||||
.hero-total { display: none; }
|
||||
|
||||
.timeline-item {
|
||||
grid-template-columns: 1fr;
|
||||
padding-left: 24px;
|
||||
}
|
||||
|
||||
.timeline-item:not(:last-child) {
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
|
||||
.time-rail {
|
||||
min-height: 30px;
|
||||
padding: 0 0 10px 18px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.time-rail::after {
|
||||
top: 12px;
|
||||
right: auto;
|
||||
bottom: -24px;
|
||||
left: -24px;
|
||||
}
|
||||
|
||||
.time-dot {
|
||||
top: 3px;
|
||||
right: auto;
|
||||
left: -30px;
|
||||
}
|
||||
|
||||
.time-rail time {
|
||||
flex-direction: row;
|
||||
align-items: baseline;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.note-card { margin-left: 0; }
|
||||
|
||||
.note-header {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 8px;
|
||||
padding: 24px 24px 18px;
|
||||
}
|
||||
|
||||
.note-number { margin-top: 0; }
|
||||
.note-header h2 { font-size: 21px; }
|
||||
|
||||
.note-body,
|
||||
.note-card.without-header .note-body {
|
||||
padding: 22px 24px 27px;
|
||||
font-size: 14px;
|
||||
line-height: 1.8;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.notes-page { width: calc(100% - 22px); padding-top: 30px; }
|
||||
.notes-hero h1 { font-size: 30px; }
|
||||
.hero-copy { font-size: 14px; }
|
||||
.timeline-item { padding-left: 18px; }
|
||||
.time-rail { padding-left: 14px; }
|
||||
.time-rail::after { left: -18px; }
|
||||
.time-dot { left: -24px; }
|
||||
.note-card { border-radius: 14px; }
|
||||
.notes-backtop { right: 18px !important; bottom: 20px !important; }
|
||||
}
|
||||
</style>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -72,7 +72,7 @@
|
||||
likeNum: null,
|
||||
content: null,
|
||||
type: null,
|
||||
blogId: this.$route.query.id,
|
||||
blogId: this.$route.params.id || this.$route.query.id,
|
||||
userId: null,
|
||||
delFlag: null,
|
||||
createBy: null,
|
||||
@@ -119,7 +119,7 @@
|
||||
likeNum: null,
|
||||
content: null,
|
||||
type: null,
|
||||
blogId: this.$route.query.id,
|
||||
blogId: this.$route.params.id || this.$route.query.id,
|
||||
userId: null,
|
||||
delFlag: null,
|
||||
createBy: null,
|
||||
|
||||
@@ -1,75 +1,68 @@
|
||||
<template>
|
||||
|
||||
<el-container>
|
||||
<div class="background" :style="backgroundImage"></div>
|
||||
<cmsNavBar></cmsNavBar>
|
||||
<keep-alive>
|
||||
<cmsMain></cmsMain>
|
||||
</keep-alive>
|
||||
<el-footer height="40px">
|
||||
<cmsFooter></cmsFooter>
|
||||
</el-footer>
|
||||
</el-container>
|
||||
<div class="cms-shell">
|
||||
<div class="ambient ambient-one"></div>
|
||||
<div class="ambient ambient-two"></div>
|
||||
<cms-nav-bar />
|
||||
<cms-main />
|
||||
<cms-footer />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import cmsMain from './main.vue'
|
||||
import cmsNavBar from './NavBar.vue'
|
||||
import cmsFooter from './Footer.vue'
|
||||
// 引入背景图
|
||||
import backgroundImages from './backgroundImages.js'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
backgroundImages,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
handleSelect(key, keyPath) {
|
||||
console.log(key, keyPath);
|
||||
}
|
||||
},
|
||||
name: 'CmsLayout',
|
||||
components: {
|
||||
cmsMain,
|
||||
cmsNavBar,
|
||||
cmsFooter
|
||||
},
|
||||
computed: {
|
||||
backgroundImage() {
|
||||
// 根据背景图数组的长度随机选择索引
|
||||
const randIndex = Math.floor(Math.random() * this.backgroundImages.length)
|
||||
return {
|
||||
// 获取对应的图片资源并将其设置到`background-image`属性上
|
||||
backgroundImage: `url(${this.backgroundImages[randIndex]})`
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.el-footer {
|
||||
background-color: rgba(84, 92, 100, 0.5);
|
||||
.cms-shell {
|
||||
--ink: #132238;
|
||||
--muted: #697386;
|
||||
--paper: #fffdf8;
|
||||
--canvas: #f4f1e9;
|
||||
--accent: #ef6a3a;
|
||||
--accent-soft: #fff0e8;
|
||||
min-height: 100vh;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background:
|
||||
linear-gradient(rgba(19, 34, 56, .025) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(19, 34, 56, .025) 1px, transparent 1px),
|
||||
var(--canvas);
|
||||
background-size: 32px 32px;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.background {
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 120vh;
|
||||
.ambient {
|
||||
position: fixed;
|
||||
z-index:-1;
|
||||
z-index: 0;
|
||||
border-radius: 50%;
|
||||
pointer-events: none;
|
||||
filter: blur(2px);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.title {
|
||||
width: 100%;
|
||||
background-position-x: center;
|
||||
background-position-y: 0;
|
||||
.ambient-one {
|
||||
width: 420px;
|
||||
height: 420px;
|
||||
top: 80px;
|
||||
right: -170px;
|
||||
background: rgba(239, 106, 58, .08);
|
||||
}
|
||||
|
||||
.ambient-two {
|
||||
width: 320px;
|
||||
height: 320px;
|
||||
bottom: 80px;
|
||||
left: -180px;
|
||||
background: rgba(40, 109, 116, .08);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<el-main>
|
||||
<main class="cms-main">
|
||||
<router-view :key="key" />
|
||||
</el-main>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -16,7 +16,9 @@
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.el-main {
|
||||
min-height: 650px;
|
||||
.cms-main {
|
||||
min-height: calc(100vh - 152px);
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user