feat: optimize source project pages and SEO

This commit is contained in:
王鹏
2026-08-25 11:08:59 +08:00
parent e528aa4876
commit 695fac5e3a
42 changed files with 5038 additions and 1025 deletions

View File

@@ -114,6 +114,10 @@ swagger:
# 请求前缀 # 请求前缀
pathMapping: /dev-api pathMapping: /dev-api
# Search engine canonical URL configuration
seo:
site-url: https://www.yidaima.cn
# 防止XSS攻击 # 防止XSS攻击
xss: xss:
# 过滤开关 # 过滤开关

View File

@@ -14,6 +14,7 @@ import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.framework.web.service.SysPermissionService; import com.ruoyi.framework.web.service.SysPermissionService;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.beans.factory.annotation.Autowired; 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.GetMapping;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.PutMapping;
@@ -66,7 +67,7 @@ public class CmsBlogController extends BaseController
startPage(); startPage();
//状态为发布 //状态为发布
cmsBlog.setStatus("1"); cmsBlog.setStatus("1");
List<CmsBlog> list = cmsBlogService.selectCmsBlogList(cmsBlog); List<CmsBlog> list = cmsBlogService.selectCmsBlogCardList(cmsBlog);
return getDataTable(list); return getDataTable(list);
} }
@@ -74,7 +75,7 @@ public class CmsBlogController extends BaseController
* 首页获取文章详细信息 * 首页获取文章详细信息
*/ */
@GetMapping(value = { "/cms/detail/", "/cms/detail/{id}" }) @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(); AjaxResult ajax = AjaxResult.success();
CmsType cmsType = new CmsType(); CmsType cmsType = new CmsType();
@@ -83,9 +84,14 @@ public class CmsBlogController extends BaseController
ajax.put("tags", cmsTagService.selectCmsTagList(cmsTag)); ajax.put("tags", cmsTagService.selectCmsTagList(cmsTag));
if (StringUtils.isNotNull(id)) 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}") @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(); Long views = cmsBlog.getViews();
views+=Long.parseLong("1"); views = views == null ? 1L : views + 1L;
cmsBlog.setViews(views); cmsBlog.setViews(views);
cmsBlogService.updateCmsBlog(cmsBlog); cmsBlogService.updateCmsBlog(cmsBlog);
return AjaxResult.success(id); return ResponseEntity.ok(AjaxResult.success(id));
} }
/** /**

View File

@@ -8,6 +8,7 @@ package com.ruoyi.cms.blog.domain;
import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle; import org.apache.commons.lang3.builder.ToStringStyle;
import com.ruoyi.cms.tag.domain.CmsTag;
/** /**
* @Author: WangNing * @Author: WangNing
@@ -18,6 +19,7 @@ public class CmsBlogTag {
private Long tagId; private Long tagId;
private Long blogId; private Long blogId;
private Long[] tagIds; private Long[] tagIds;
private CmsTag tag;
public Long getTagId() { public Long getTagId() {
return tagId; return tagId;
@@ -43,6 +45,14 @@ public class CmsBlogTag {
this.tagIds = tagIds; this.tagIds = tagIds;
} }
public CmsTag getTag() {
return tag;
}
public void setTag(CmsTag tag) {
this.tag = tag;
}
@Override @Override
public String toString() { public String toString() {
return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE) return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE)

View File

@@ -8,6 +8,7 @@ package com.ruoyi.cms.blog.domain;
import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle; import org.apache.commons.lang3.builder.ToStringStyle;
import com.ruoyi.cms.type.domain.CmsType;
/** /**
* @Author: WangNing * @Author: WangNing
@@ -18,6 +19,7 @@ public class CmsBlogType {
private Long typeId; private Long typeId;
private Long blogId; private Long blogId;
private Long[] typeIds; private Long[] typeIds;
private CmsType type;
public Long getTypeId() { public Long getTypeId() {
return typeId; return typeId;
@@ -43,6 +45,14 @@ public class CmsBlogType {
this.typeIds = typeIds; this.typeIds = typeIds;
} }
public CmsType getType() {
return type;
}
public void setType(CmsType type) {
this.type = type;
}
@Override @Override
public String toString() { public String toString() {
return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE) return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE)

View File

@@ -20,6 +20,11 @@ public interface CmsBlogMapper
*/ */
public CmsBlog selectCmsBlogById(Long id); 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> 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); 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);
/** /**

View File

@@ -27,6 +27,10 @@ public interface CmsBlogTagMapper {
* 查询文件列表 * 查询文件列表
*/ */
public List<CmsBlogTag> selectBlogTagList(Long blogId); public List<CmsBlogTag> selectBlogTagList(Long blogId);
/**
* 批量查询博客及其标签,避免列表页逐条查询
*/
public List<CmsBlogTag> selectBlogTagsByBlogIds(List<Long> blogIds);
/** /**
* 通过tagId删除blog文件关联 * 通过tagId删除blog文件关联
*/ */

View File

@@ -27,6 +27,10 @@ public interface CmsBlogTypeMapper {
* 查询博客列表 * 查询博客列表
*/ */
public List<CmsBlogType> selectBlogTypeList(Long blogId); public List<CmsBlogType> selectBlogTypeList(Long blogId);
/**
* 批量查询博客及其分类,避免列表页逐条查询
*/
public List<CmsBlogType> selectBlogTypesByBlogIds(List<Long> blogIds);
/** /**
* 通过typeId删除blog文件关联 * 通过typeId删除blog文件关联
*/ */

View File

@@ -19,6 +19,14 @@ public interface ICmsBlogService
*/ */
public CmsBlog selectCmsBlogById(Long id); 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> selectCmsBlogList(CmsBlog cmsBlog);
/**
* 查询首页项目卡片列表
*/
public List<CmsBlog> selectCmsBlogCardList(CmsBlog cmsBlog);
/** /**
* 查询推荐文章列表 * 查询推荐文章列表
*/ */

View File

@@ -1,7 +1,9 @@
package com.ruoyi.cms.blog.service.impl; package com.ruoyi.cms.blog.service.impl;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@@ -54,6 +56,10 @@ public class CmsBlogServiceImpl implements ICmsBlogService
public CmsBlog selectCmsBlogById(Long id) public CmsBlog selectCmsBlogById(Long id)
{ {
CmsBlog blog = cmsBlogMapper.selectCmsBlogById(id); CmsBlog blog = cmsBlogMapper.selectCmsBlogById(id);
if (blog == null)
{
return null;
}
//查询标签列表 //查询标签列表
List<CmsBlogTag> blogTagList = cmsBlogTagMapper.selectBlogTagList(id); List<CmsBlogTag> blogTagList = cmsBlogTagMapper.selectBlogTagList(id);
Long[] tagIds = new Long[blogTagList.size()]; Long[] tagIds = new Long[blogTagList.size()];
@@ -83,6 +89,13 @@ public class CmsBlogServiceImpl implements ICmsBlogService
return blog; 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; return blogList;
} }
@Override
public List<CmsBlog> selectCmsBlogCardList(CmsBlog cmsBlog)
{
return prepareBlogCards(cmsBlogMapper.selectCmsBlogCardList(cmsBlog));
}
/** /**
* 查询推荐文章列表 * 查询推荐文章列表
*/ */
@Override @Override
public List<CmsBlog> selectCmsBlogListRecommend(CmsBlog cmsBlog) { public List<CmsBlog> selectCmsBlogListRecommend(CmsBlog cmsBlog) {
List<CmsBlog> cmsBlogList = cmsBlogMapper.selectCmsBlogListRecommend(cmsBlog); List<CmsBlog> cmsBlogList = cmsBlogMapper.selectCmsBlogListRecommend(cmsBlog);
// 为每个博客设置首页图片(如果为空,则从content中提取) return prepareBlogCards(cmsBlogList);
for (CmsBlog blog : cmsBlogList) {
setDefaultBlogPicFromContent(blog);
}
List<CmsBlog> blogList = BlogListAddTypeAndTag(cmsBlogList);
return blogList;
} }
/** /**
@@ -123,12 +137,7 @@ public class CmsBlogServiceImpl implements ICmsBlogService
@Override @Override
public List<CmsBlog> selectCmsBlogListByTypeId(Long id) { public List<CmsBlog> selectCmsBlogListByTypeId(Long id) {
List<CmsBlog> cmsBlogList = cmsBlogMapper.selectCmsBlogListByTypeId(id); List<CmsBlog> cmsBlogList = cmsBlogMapper.selectCmsBlogListByTypeId(id);
// 为每个博客设置首页图片(如果为空,则从content中提取) return prepareBlogCards(cmsBlogList);
for (CmsBlog blog : cmsBlogList) {
setDefaultBlogPicFromContent(blog);
}
List<CmsBlog> blogList = BlogListAddTypeAndTag(cmsBlogList);
return blogList;
} }
/** /**
@@ -137,12 +146,7 @@ public class CmsBlogServiceImpl implements ICmsBlogService
@Override @Override
public List<CmsBlog> selectCmsBlogListByTagId(Long id) { public List<CmsBlog> selectCmsBlogListByTagId(Long id) {
List<CmsBlog> cmsBlogList = cmsBlogMapper.selectCmsBlogListByTagId(id); List<CmsBlog> cmsBlogList = cmsBlogMapper.selectCmsBlogListByTagId(id);
// 为每个博客设置首页图片(如果为空,则从content中提取) return prepareBlogCards(cmsBlogList);
for (CmsBlog blog : cmsBlogList) {
setDefaultBlogPicFromContent(blog);
}
List<CmsBlog> blogList = BlogListAddTypeAndTag(cmsBlogList);
return blogList;
} }
/** /**
@@ -307,30 +311,71 @@ public class CmsBlogServiceImpl implements ICmsBlogService
return null; 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; return cmsBlogList;
} }
Map<Long, CmsBlog> missingCoverBlogs = new HashMap<>();
for (CmsBlog blog : cmsBlogList) { for (CmsBlog blog : cmsBlogList) {
Long blogId = blog.getId(); boolean hasCover = (blog.getBlogPic() != null && !blog.getBlogPic().trim().isEmpty())
//查询标签列表 || (blog.getBlogPicLink() != null && !blog.getBlogPicLink().trim().isEmpty());
List<CmsBlogTag> blogTagList = cmsBlogTagMapper.selectBlogTagList(blogId); if (!hasCover) {
List<CmsTag> cmsTagList = new ArrayList<>(); missingCoverBlogs.put(blog.getId(), blog);
blogTagList.forEach((CmsBlogTag cmsBlogTag)->{ }
Long tagId = cmsBlogTag.getTagId(); }
CmsTag cmsTag = cmsTagMapper.selectCmsTagByTagId(tagId);
cmsTagList.add(cmsTag); if (!missingCoverBlogs.isEmpty()) {
}); List<Long> ids = new ArrayList<>(missingCoverBlogs.keySet());
blog.setTags(cmsTagList); for (CmsBlog contentBlog : cmsBlogMapper.selectCmsBlogContentByIds(ids)) {
//查询分类列表 CmsBlog targetBlog = missingCoverBlogs.get(contentBlog.getId());
List<CmsBlogType> blogTypeList = cmsBlogTypeMapper.selectBlogTypeList(blogId); if (targetBlog == null || contentBlog.getContent() == null) {
List<CmsType> cmsTypeList = new ArrayList<>(); continue;
blogTypeList.forEach((CmsBlogType cmsBlogType)->{ }
Long typeId = cmsBlogType.getTypeId(); String firstImageUrl = extractFirstImageFromContent(contentBlog.getContent());
CmsType cmsType = cmsTypeMapper.selectCmsTypeByTypeId(typeId); if (firstImageUrl != null && !firstImageUrl.trim().isEmpty()) {
cmsTypeList.add(cmsType); targetBlog.setBlogPicType("0");
}); targetBlog.setBlogPicLink(firstImageUrl);
blog.setTypes(cmsTypeList); }
}
}
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; return cmsBlogList;
} }

View File

@@ -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());
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}

View File

@@ -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;
}
}

View File

@@ -47,12 +47,7 @@ public class CmsTagServiceImpl implements ICmsTagService
@Override @Override
public List<CmsTag> selectCmsTagList(CmsTag cmsTag) public List<CmsTag> selectCmsTagList(CmsTag cmsTag)
{ {
List<CmsTag> cmsTagList = cmsTagMapper.selectCmsTagList(cmsTag); return cmsTagMapper.selectCmsTagList(cmsTag);
for (CmsTag tag : cmsTagList) {
int blogNum = cmsBlogTagMapper.countBlogByTagId(tag.getTagId());
tag.setBlogNum(blogNum);
}
return cmsTagList;
} }
/** /**

View File

@@ -51,12 +51,7 @@ public class CmsTypeServiceImpl implements ICmsTypeService
@Override @Override
public List<CmsType> selectCmsTypeList(CmsType cmsType) public List<CmsType> selectCmsTypeList(CmsType cmsType)
{ {
List<CmsType> cmsTypeList = cmsTypeMapper.selectCmsTypeList(cmsType); return cmsTypeMapper.selectCmsTypeList(cmsType);
for (CmsType type : cmsTypeList) {
int blogNum = cmsBlogTypeMapper.countBlogByTypeId(type.getTypeId());
type.setBlogNum(blogNum);
}
return cmsTypeList;
} }
/** /**

View File

@@ -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 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>
<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"> <select id="selectCmsBlogList" parameterType="CmsBlog" resultMap="CmsBlogResult">
<include refid="selectCmsBlogVo"/> <include refid="selectCmsBlogVo"/>
<where> <where>
@@ -43,24 +49,54 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
ORDER BY Top DESC, create_time DESC ORDER BY Top DESC, create_time DESC
</select> </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"> <select id="selectCmsBlogById" parameterType="Long" resultMap="CmsBlogResult">
<include refid="selectCmsBlogVo"/> <include refid="selectCmsBlogVo"/>
where id = #{id} where id = #{id}
</select> </select>
<select id="selectCmsBlogListByTypeId" parameterType="Long" resultMap="CmsBlogResult"> <select id="selectPublishedCmsBlogById" parameterType="Long" resultMap="CmsBlogResult">
<include refid="selectCmsBlogVo"/> <include refid="selectCmsBlogVo"/>
where id = #{id} and status = '1'
</select>
<select id="selectCmsBlogListByTypeId" parameterType="Long" resultMap="CmsBlogResult">
<include refid="selectCmsBlogCardVo"/>
<where> <where>
<if test="id != null and id != ''"> and id in (SELECT blog_id FROM cms_blog_type WHERE type_id = #{id})</if> <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> </where>
ORDER BY Top DESC, create_time DESC
</select> </select>
<select id="selectCmsBlogListByTagId" parameterType="Long" resultMap="CmsBlogResult"> <select id="selectCmsBlogListByTagId" parameterType="Long" resultMap="CmsBlogResult">
<include refid="selectCmsBlogVo"/> <include refid="selectCmsBlogCardVo"/>
<where> <where>
<if test="id != null and id != ''"> and id in (SELECT blog_id FROM cms_blog_tag WHERE tag_id = #{id})</if> <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> </where>
ORDER BY Top DESC, create_time DESC
</select> </select>
<select id="selectCmsBlogListRecommend" parameterType="CmsBlog" resultMap="CmsBlogResult"> <select id="selectCmsBlogListRecommend" parameterType="CmsBlog" resultMap="CmsBlogResult">
<include refid="selectCmsBlogVo"/> <include refid="selectCmsBlogCardVo"/>
<where> <where>
<if test="title != null and title != ''"> and title like concat('%', #{title}, '%')</if> <if test="title != null and title != ''"> and title like concat('%', #{title}, '%')</if>
<if test="type != null and type != ''"> and type = #{type}</if> <if test="type != null and type != ''"> and type = #{type}</if>

View File

@@ -5,6 +5,14 @@
<result property="tagId" column="tag_id" /> <result property="tagId" column="tag_id" />
<result property="blogId" column="blog_id" /> <result property="blogId" column="blog_id" />
</resultMap> </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 id="batchBlogTag">
insert into cms_blog_tag(tag_id, blog_id) values insert into cms_blog_tag(tag_id, blog_id) values
@@ -30,6 +38,15 @@
<select id="selectBlogTagList" parameterType="Long" resultMap="blogTagResult"> <select id="selectBlogTagList" parameterType="Long" resultMap="blogTagResult">
select tag_id, blog_id from cms_blog_tag where blog_id = #{blogId} select tag_id, blog_id from cms_blog_tag where blog_id = #{blogId}
</select> </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 id="countBlogByTagId" parameterType="Long" resultType="java.lang.Integer">
select count(*) from cms_blog_tag where tag_id=#{tagId} select count(*) from cms_blog_tag where tag_id=#{tagId}
</select> </select>

View File

@@ -5,6 +5,14 @@
<result property="typeId" column="type_id" /> <result property="typeId" column="type_id" />
<result property="blogId" column="blog_id" /> <result property="blogId" column="blog_id" />
</resultMap> </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 id="batchBlogType">
insert into cms_blog_type(type_id, blog_id) values insert into cms_blog_type(type_id, blog_id) values
@@ -30,6 +38,15 @@
<select id="selectBlogTypeList" parameterType="Long" resultMap="blogTypeResult"> <select id="selectBlogTypeList" parameterType="Long" resultMap="blogTypeResult">
select type_id, blog_id from cms_blog_type where blog_id = #{blogId} select type_id, blog_id from cms_blog_type where blog_id = #{blogId}
</select> </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 id="countBlogByTypeId" parameterType="Long" resultType="java.lang.Integer">
select count(*) from cms_blog_type where type_id=#{typeId} select count(*) from cms_blog_type where type_id=#{typeId}
</select> </select>

View File

@@ -11,6 +11,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="updateBy" column="update_by" /> <result property="updateBy" column="update_by" />
<result property="updateTime" column="update_time" /> <result property="updateTime" column="update_time" />
<result property="tagName" column="tag_name" /> <result property="tagName" column="tag_name" />
<result property="blogNum" column="blog_num" />
</resultMap> </resultMap>
<sql id="selectCmsTagVo"> <sql id="selectCmsTagVo">
@@ -18,12 +19,16 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</sql> </sql>
<select id="selectCmsTagList" parameterType="CmsTag" resultMap="CmsTagResult"> <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> <where>
<if test="tagName != null and tagName != ''"> and tag_name like concat('%', #{tagName}, '%')</if> <if test="tagName != null and tagName != ''"> and t.tag_name like concat('%', #{tagName}, '%')</if>
<if test="createBy != null and createBy != ''"> and create_by = #{createBy}</if> <if test="createBy != null and createBy != ''"> and t.create_by = #{createBy}</if>
</where> </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>
<select id="selectCmsTagByTagId" parameterType="Long" resultMap="CmsTagResult"> <select id="selectCmsTagByTagId" parameterType="Long" resultMap="CmsTagResult">

View File

@@ -14,6 +14,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="typePicType" column="type_pic_type" /> <result property="typePicType" column="type_pic_type" />
<result property="typePic" column="type_pic" /> <result property="typePic" column="type_pic" />
<result property="typePicLink" column="type_pic_link" /> <result property="typePicLink" column="type_pic_link" />
<result property="blogNum" column="blog_num" />
</resultMap> </resultMap>
<sql id="selectCmsTypeVo"> <sql id="selectCmsTypeVo">
@@ -21,13 +22,18 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</sql> </sql>
<select id="selectCmsTypeList" parameterType="CmsType" resultMap="CmsTypeResult"> <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> <where>
<if test="typeName != null and typeName != ''"> and type_name like concat('%', #{typeName}, '%')</if> <if test="typeName != null and typeName != ''"> and t.type_name like concat('%', #{typeName}, '%')</if>
<if test="typePic != null and typePic != ''"> and type_pic = #{typePic}</if> <if test="typePic != null and typePic != ''"> and t.type_pic = #{typePic}</if>
<if test="createBy != null and createBy != ''"> and create_by = #{createBy}</if> <if test="createBy != null and createBy != ''"> and t.create_by = #{createBy}</if>
</where> </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>
<select id="selectCmsTypeByTypeId" parameterType="Long" resultMap="CmsTypeResult"> <select id="selectCmsTypeByTypeId" parameterType="Long" resultMap="CmsTypeResult">

View 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>

View File

@@ -97,6 +97,8 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter
// 过滤请求 // 过滤请求
.authorizeRequests() .authorizeRequests()
.antMatchers("/profile/**").permitAll() .antMatchers("/profile/**").permitAll()
// Public sitemap must be crawlable without a token.
.antMatchers(HttpMethod.GET, "/sitemap.xml").permitAll()
// 对于登录login 注册register 验证码captchaImage 允许匿名访问 // 对于登录login 注册register 验证码captchaImage 允许匿名访问
.antMatchers("/login", "/register", "/captchaImage").anonymous() .antMatchers("/login", "/register", "/captchaImage").anonymous()
// 首页获取文章信息 允许匿名访问 // 首页获取文章信息 允许匿名访问

View File

@@ -6,3 +6,6 @@ ENV = 'production'
# 若依管理系统/生产环境 # 若依管理系统/生产环境
VUE_APP_BASE_API = '/prod-api' VUE_APP_BASE_API = '/prod-api'
# Canonical host used by public pages and the XML sitemap
VUE_APP_SITE_URL = 'https://www.yidaima.cn'

View File

@@ -6,7 +6,8 @@
<meta name="renderer" content="webkit"> <meta name="renderer" content="webkit">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<link rel="icon" href="<%= BASE_URL %>favicon.ico"> <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]--> <!--[if lt IE 11]><script>window.location.href='/html/ie.html';</script><![endif]-->
<style> <style>
html, html,

View File

@@ -1,2 +1,9 @@
User-agent: * User-agent: *
Disallow: / Allow: /
Disallow: /login
Disallow: /cmsLogin
Disallow: /cmsRegister
Disallow: /admin/
Sitemap: https://www.yidaima.cn/sitemap.xml

View File

@@ -8,11 +8,20 @@
export default { export default {
name: 'App', name: 'App',
metaInfo() { metaInfo() {
const meta = []
if (this.$route.meta.public !== true) {
meta.push({
vmid: 'robots',
name: 'robots',
content: 'noindex,nofollow'
})
}
return { return {
title: this.$store.state.settings.dynamicTitle && this.$store.state.settings.title, title: this.$store.state.settings.dynamicTitle && this.$store.state.settings.title,
titleTemplate: title => { titleTemplate: title => {
return title ? `${title} - ${process.env.VUE_APP_TITLE}` : process.env.VUE_APP_TITLE return title ? `${title} - ${process.env.VUE_APP_TITLE}` : process.env.VUE_APP_TITLE
} },
meta
} }
} }
} }

View File

@@ -1,5 +1,5 @@
import request from '@/utils/request' import request from '@/utils/request'
import { praseStrEmpty } from "@/utils/ruoyi"; import { praseStrEmpty } from '@/utils/ruoyi'
// 首页查询文章列表 // 首页查询文章列表
export function cmsListBlog(query) { export function cmsListBlog(query) {
@@ -27,25 +27,27 @@ export function getBlogDetail(id) {
} }
// 首页按分类查询文章列表 // 首页按分类查询文章列表
export function cmsListByTypeId(id) { export function cmsListByTypeId(id, query) {
return request({ return request({
url: '/cms/blog/cms/cmsListByType/' + id, url: '/cms/blog/cms/cmsListByType/' + id,
headers: { headers: {
isToken: false isToken: false
}, },
method: 'get', method: 'get',
params: query,
timeout: 40000 timeout: 40000
}) })
} }
// 首页按标签查询文章列表 // 首页按标签查询文章列表
export function cmsListByTagId(id) { export function cmsListByTagId(id, query) {
return request({ return request({
url: '/cms/blog/cms/cmsListByTag/' + id, url: '/cms/blog/cms/cmsListByTag/' + id,
headers: { headers: {
isToken: false isToken: false
}, },
method: 'get', method: 'get',
params: query,
timeout: 40000 timeout: 40000
}) })
} }

View File

@@ -38,7 +38,7 @@ router.beforeEach((to, from, next) => {
} }
} else { } else {
// 没有token // 没有token
if (whiteList.indexOf(to.path) !== -1) { if (whiteList.indexOf(to.path) !== -1 || to.meta.public === true) {
// 在免登录白名单,直接进入 // 在免登录白名单,直接进入
next() next()
} else { } else {

View File

@@ -36,12 +36,82 @@ export const constantRoutes = [{
component: (resolve) => require(['@/views/redirect'], resolve) 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', path: '/cms',
component: (resolve) => require(['@/views/cms/index'], resolve), component: (resolve) => require(['@/views/cms/index'], resolve),
hidden: true, hidden: true,
redirect: '/cms/main/cmsIndex', redirect: '/',
children: [ 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', path: 'main',
component: (resolve) => require(['@/views/cms/main'], resolve), component: (resolve) => require(['@/views/cms/main'], resolve),
@@ -49,11 +119,7 @@ export const constantRoutes = [{
children: [ children: [
{ {
path: 'cmsIndex', path: 'cmsIndex',
component: (resolve) => require(['@/views/cms/components/cmsIndex'], resolve), redirect: '/'
name: 'cmsIndex',
meta: {
title: '首页',
}
}, },
{ {
path: 'essay', path: 'essay',
@@ -77,6 +143,7 @@ export const constantRoutes = [{
name: 'blog', name: 'blog',
meta: { meta: {
title: '博客详情', title: '博客详情',
public: true
} }
} }
] ]
@@ -125,11 +192,11 @@ export const constantRoutes = [{
hidden: true hidden: true
}, },
{ {
path: '', path: '/dashboard',
component: Layout, component: Layout,
redirect: '/cms', redirect: '/index',
children: [{ children: [{
path: 'index', path: '/index',
component: (resolve) => require(['@/views/index_v2'], resolve), component: (resolve) => require(['@/views/index_v2'], resolve),
name: 'Index', name: 'Index',
meta: { meta: {

128
ruoyi-ui/src/utils/seo.js Normal file
View 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(/&nbsp;|&#160;/gi, ' ')
.replace(/&amp;/gi, '&')
.replace(/&lt;/gi, '<')
.replace(/&gt;/gi, '>')
.replace(/&quot;/gi, '"')
.replace(/&#39;/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
}
}

View File

@@ -1,18 +1,70 @@
<template> <template>
<div class="footer"> <footer class="site-footer">
<span style="color: #FFFFFF;line-height:40px;font-size: 12px;">Copyright © 2024-2025 www.yidaima.cn 版权所有</span> <div class="footer-wrap">
<span style="color: #FFFFFF;line-height:40px;font-size: 12px;">工信部备案:蒙ICP备17001571号</span> <div class="footer-brand">
<span>&lt;/&gt;</span>
<div>
<strong>南音源码库</strong>
<small>可运行的项目源码与配套资料</small>
</div> </div>
</div>
<div class="footer-meta">
<span>Copyright © 2024-2026 yidaima.cn</span>
<span>蒙ICP备17001571号</span>
</div>
</div>
</footer>
</template> </template>
<script> <script>
export default { export default {
name: 'cmsFooter', name: 'cmsFooter'
} }
</script> </script>
<style scoped> <style scoped>
.footer { .site-footer {
text-align: center; 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> </style>

View File

@@ -1,438 +1,433 @@
<template> <template>
<el-header :style="'margin-bottom:'+ headerBottom +'px'"> <header class="site-header">
<h2 class="logo" @click="goToHome"><svg-icon icon-class="guide" /> 南音源码库</h2> <div class="nav-wrap">
<div class="bg-purple-light"> <router-link to="/" class="brand" aria-label="南音源码库首页">
<el-menu :default-active="activeIndex" router class="el-menu-demo" mode="horizontal" style="border: none;" <span class="brand-mark"><span>&lt;/&gt;</span></span>
background-color="rgba(0,0,0,0)" text-color="#fff" active-text-color="#ffd04b"> <span class="brand-copy">
<el-menu-item index="/cms/main/cmsIndex"><i class="el-icon-s-home" style="color: rgba(255, 255, 255);"></i>首页 <strong>南音源码库</strong>
</el-menu-item> <small>CODE · NOTES · IDEAS</small>
<el-menu-item :index="item.path" v-for="item in menulist" :key="item.id"> </span>
<!-- 图标--> </router-link>
<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>
<div class="menu-expend" @click="menuExpend"> <nav class="desktop-nav" aria-label="主导航">
<i class="el-icon-menu" style="color: rgba(255, 255, 255);"></i> <router-link exact to="/">首页</router-link>
</div> <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"> <div class="nav-actions">
<el-input @focus="checkInput" @blur="notSearching()" class="search" placeholder="搜索博客" <div class="search-box">
prefix-icon="el-icon-search" v-model="queryInfo.query" size="mini"> <i class="el-icon-search"></i>
</el-input> <input
<ul v-if="searching"> v-model.trim="queryInfo.query"
<li class="animate__animated animate__fadeInDown search-blog" v-for="blog in searchList" :key="blog.id" type="search"
@click="getBlogInfo(blog.id)"> placeholder="搜索名称、编号或技术栈"
<a><span v-html="blog.title"></span></a> 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> </li>
</ul> </ul>
</div> </div>
<div v-if="islogin" class="bg-purple"> <el-dropdown v-if="islogin" trigger="click" class="account-menu">
<el-dropdown class="avatar-container right-menu-item hover-effect" trigger="click"> <button type="button" class="account-button">
<div class="avatar-wrapper"> <el-avatar :size="30" :src="avatar" @error="errorHandler">
<el-avatar class="user-avatar" :src="avatar" @error="errorHandler"> <i class="el-icon-user-solid"></i>
<i class="el-icon-s-custom" />
</el-avatar> </el-avatar>
<p class="avatar-Name">{{name}}</p> <span>{{ name || '账户' }}</span>
</div> </button>
<el-dropdown-menu slot="dropdown"> <el-dropdown-menu slot="dropdown">
<router-link target="_blank" to="/index"> <router-link target="_blank" to="/index">
<el-dropdown-item>管理博客</el-dropdown-item> <el-dropdown-item>管理博客</el-dropdown-item>
</router-link> </router-link>
<el-dropdown-item divided @click.native="logout"> <el-dropdown-item divided @click.native="logout">退出登录</el-dropdown-item>
<span>退出登录</span>
</el-dropdown-item>
</el-dropdown-menu> </el-dropdown-menu>
</el-dropdown> </el-dropdown>
</div> <button v-else type="button" class="login-button" @click="tologin">登录</button>
<div v-else class="bg-purple">
<div class="avatar-wrapper"> <button
<!-- <el-avatar class="avatar" src="avatar" @error="errorHandler"> type="button"
<i class="el-icon-s-custom" @click="tologin"/> class="mobile-toggle"
</el-avatar> --> :aria-expanded="menuHiddenVisiable ? 'true' : 'false'"
<p class="avatar-Name" @click="tologin">登录 | 注册</p> aria-label="展开导航"
@click="menuExpend"
>
<i :class="menuHiddenVisiable ? 'el-icon-close' : 'el-icon-menu'"></i>
</button>
</div> </div>
</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> </template>
<script> <script>
import { import { mapGetters } from 'vuex'
mapGetters import { getToken } from '@/utils/auth'
} from 'vuex' import { cmsListBlog } from '@/api/cms/blog'
import { import { contentPath } from '@/utils/seo'
getToken
} from '@/utils/auth'
import 'element-ui/lib/theme-chalk/display.css'
import {
cmsListBlog,
} from "@/api/cms/blog";
export default { export default {
name: 'cmsNavBar', name: 'cmsNavBar',
data() { data() {
return { return {
activeIndex: 'this.$router.path',
islogin: false, islogin: false,
searchInput: true,
menuHiddenVisiable: false, menuHiddenVisiable: false,
headerBottom: 0, timer: null,
queryInfo: { queryInfo: {
query: '', query: ''
timer: null
}, },
searchList: [], searchList: [],
searching: false, searching: false,
menulist: [{ menulist: [
id: 1, { id: 1, authName: '源码项目', path: '/projects' },
authName: '随笔', { id: 3, authName: '运行教程', path: '/cms/doucument' },
path: '/cms/main/essay', { id: 4, 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',
},
], ],
// 查询参数
queryParams: { queryParams: {
pageNum: 1, pageNum: 1,
pageSize: 10, pageSize: 6,
title: null, title: null,
type: 1, type: 1,
content: null, content: null,
top: null, top: null,
views: null, views: null,
status: null, status: null
}, }
}; }
}, },
computed: { computed: {
...mapGetters([ ...mapGetters(['avatar', 'name'])
'avatar',
'name'
]),
}, },
watch: { watch: {
'queryInfo.query': { 'queryInfo.query'(value) {
handler(value) { if (this.timer) clearTimeout(this.timer)
if (this.timer) { this.timer = setTimeout(() => this.searchBlog(value), 300)
clearTimeout(this.timer)
}
this.timer = setTimeout(() => {
this.searchBlog()
}, 300)
}
} }
}, },
created() { created() {
this.login(); this.islogin = Boolean(getToken())
// this.ResponsiveLayout(); },
beforeDestroy() {
if (this.timer) clearTimeout(this.timer)
}, },
methods: { methods: {
menulistAdd() { contentPath,
//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);
},
// 展开菜单栏
menuExpend() { menuExpend() {
this.menuHiddenVisiable = !this.menuHiddenVisiable this.menuHiddenVisiable = !this.menuHiddenVisiable
if (this.menuHiddenVisiable === true) {
this.headerBottom = (this.menulist.length + 1) * 56
} else {
this.headerBottom = 0
}
}, },
//收起菜单
menuAway() { menuAway() {
this.menuHiddenVisiable = false; this.menuHiddenVisiable = false
this.headerBottom = 0;
}, },
notSearching() { notSearching() {
setTimeout(() => { setTimeout(() => {
this.searching = false this.searching = false
}, 100) }, 150)
}, },
checkInput() { checkInput() {
this.searching = this.queryInfo.query !== ''; this.searching = Boolean(this.queryInfo.query && this.searchList.length)
}, },
tologin() { tologin() {
this.$router.push({ this.$router.push('/cmsLogin')
path: "/cmsLogin"
});
},
login() {
let token = getToken();
if (token == null || token == '') {
this.islogin = false;
} else {
this.islogin = true;
}
}, },
errorHandler() { errorHandler() {
return true return true
}, },
async searchBlog() { async searchBlog(value) {
if (this.queryInfo.query === '') { if (!value) {
this.searchList = []
this.searching = false this.searching = false
return return
} }
this.queryParams.title = this.queryInfo.query; this.queryParams.title = value
cmsListBlog(this.queryParams).then(response => { try {
let lsitSize = response.rows.length; const response = await cmsListBlog(this.queryParams)
if(lsitSize>0){ this.searchList = response.rows || []
for(let i = 0;i<lsitSize;i++){ this.searching = this.searchList.length > 0
let redTitle = this.brightenKeyword(response.rows[i].title,this.queryInfo.query); } catch (error) {
response.rows[i].title = redTitle; this.searchList = []
} this.searching = false
};
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;
} }
}, },
// 跳转到博客详情页 logout() {
getBlogInfo(blogId) {
let routeUrl = this.$router.resolve({
path: '/cms/main/blog',
query: {
id: blogId
}
});
window.open(routeUrl.href, '_blank');
},
async logout() {
this.$confirm('确定注销并退出系统登录吗?', '提示', { this.$confirm('确定注销并退出系统登录吗?', '提示', {
confirmButtonText: '确定', confirmButtonText: '确定',
cancelButtonText: '取消', cancelButtonText: '取消',
type: 'warning' type: 'warning'
}).then(() => { }).then(() => {
this.$store.dispatch('LogOut').then(() => { this.$store.dispatch('LogOut').then(() => {
location.href = '/cms/main/cmsIndex'; location.href = '/'
}) })
}).catch(() => {}); }).catch(() => {})
},
goToHome() {
this.$router.push({
path: '/cms/main/cmsIndex'
});
} }
} }
} }
</script> </script>
<style scoped> <style scoped>
.el-header { .site-header {
display: flex; height: 76px;
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 {
position: relative; 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 { .brand {
position: absolute; display: inline-flex;
top: 30px; align-items: center;
width: 100%; gap: 11px;
border: 1px solid #e5e9ef; flex: 0 0 auto;
margin-top: 1px; color: #fff;
background: #fff; text-decoration: none;
z-index: 10000; }
border-radius: 2px;
box-shadow: 0 2px 4px rgba(58, 118, 142, 0.16); .brand-mark {
padding: 10px 0; 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; font-size: 14px;
box-sizing: border-box; text-decoration: none;
transition: color .2s ease;
} }
.search_input ul li { .desktop-nav a::after {
padding: 0 16px; content: '';
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;*/
position: absolute; 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; 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%; width: 100%;
min-width: 0;
border: 0;
outline: 0;
color: #fff;
background: transparent;
font-size: 13px;
} }
.menu-expend { .search-box input::placeholder {
display: none !important; color: rgba(255, 255, 255, .42);
} }
/* 窗口可视区域小于1000隐藏搜索框 */ .search-results {
@media screen and (max-width: 1000px) { width: 320px;
.search_input { position: absolute;
display: none; 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) { .search-results a {
.el-menu /deep/ .el-menu-item { display: block;
background-color: rgba(0, 0, 0, 0.3) !important; padding: 11px 12px;
overflow: hidden;
color: #26364c;
border-radius: 9px;
text-overflow: ellipsis;
white-space: nowrap;
text-decoration: none;
} }
.el-menu-demo { .search-results a:hover {
display: none; color: #d8562d;
background: #fff0e8;
} }
.menu-expend { .account-button,
display: block !important; .login-button,
float: right; .mobile-toggle {
} border: 0;
color: #fff;
.menu-expend:hover { background: transparent;
color: #ffd04b;
cursor: pointer; 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> </style>

View File

@@ -141,7 +141,7 @@ export default {
Cookies.remove('rememberMe'); Cookies.remove('rememberMe');
} }
this.$store.dispatch("Login", this.loginForm).then(() => { this.$store.dispatch("Login", this.loginForm).then(() => {
this.$router.push({ path: "/cms/main/cmsIndex" }); this.$router.push({ path: "/" });
}).catch(() => { }).catch(() => {
this.loading = false; this.loading = false;
if (this.captchaOnOff) { if (this.captchaOnOff) {

View File

@@ -365,9 +365,8 @@ export default {
// 跳转到评论页 // 跳转到评论页
getBlogInfo(mes) { getBlogInfo(mes) {
let routeUrl = this.$router.resolve({ let routeUrl = this.$router.resolve({
path: '/cms/main/blog', path: `/article/${mes.blogId}`,
query: { query: {
id: mes.blogId,
commentId: mes.id commentId: mes.id
} }
}); });

File diff suppressed because it is too large Load Diff

View 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">&lt;/&gt;</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>

View File

@@ -1,39 +1,41 @@
<template> <template>
<el-container> <div class="document-page">
<iframe <iframe
class="document-frame"
src="/docs/index.html" src="/docs/index.html"
id="qt" title="南音源码库项目文档"
scrolling="auto" scrolling="auto"
frameborder="0" frameborder="0"
style="position:absolute;top:64px;left: 0px;right:0px;bottom:100px;opacity:1;"
></iframe> ></iframe>
</el-container> </div>
</template> </template>
<script> <script>
export default { export default {
name: 'CmsDocument', 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()
}
}
} }
</script> </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> </style>

View File

@@ -1,56 +1,84 @@
<template> <template>
<el-row :gutter="20"> <div class="notes-page">
<el-col :sm="3" class="hidden-xs-only" style="opacity:0;">左侧占位</el-col> <section class="notes-hero" aria-labelledby="notes-title">
<el-col :xs="24" :sm="18"> <div>
<el-container> <p class="eyebrow"><span /> DEV NOTES</p>
<el-timeline class="animate__animated animate__fadeInLeft"> <h1 id="notes-title">开发笔记</h1>
<el-timeline-item :color="color" v-for="essay in essayList" :key="essay.id" :timestamp="essay.createTime" <p class="hero-copy">记录项目部署、问题排查与开发过程中的实用经验。</p>
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> </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-backtop>
</el-row> </div>
</template> </template>
<script> <script>
// import 'cherry-markdown/dist/cherry-markdown.min.css'
//使用prism.js代码高亮
import '@/views/cms/plugins/prism.js' import '@/views/cms/plugins/prism.js'
import '@/views/cms/plugins/prism.css' import '@/views/cms/plugins/prism.css'
import { import { cmsEssayList } from '@/api/cms/blog'
cmsEssayList,
} from "@/api/cms/blog";
export default { export default {
name: 'essay', name: 'CmsEssay',
data() { data() {
return { return {
essayList: [], essayList: [],
// 查询参数 loading: true,
total: 0,
queryParams: { queryParams: {
pageNum: 1, pageNum: 1,
pageSize: 10, pageSize: 10,
@@ -59,9 +87,8 @@ import '@/views/cms/plugins/prism.css'
content: null, content: null,
top: null, top: null,
views: null, views: null,
status: null, status: null
}, }
color: "#ffd04b",
} }
}, },
created() { created() {
@@ -69,35 +96,461 @@ import '@/views/cms/plugins/prism.css'
}, },
methods: { methods: {
async getEssayList() { async getEssayList() {
cmsEssayList(this.queryParams).then(response => { this.loading = true
this.essayList = response.rows; 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> </script>
<style scoped> <style scoped>
.el-timeline { .notes-page {
font: 16px/1.5 'Microsoft Yahei', 'PingFang SC', 'Hiragino Sans GB', sans-serif !important; width: min(1180px, calc(100% - 40px));
width: 80%;
margin: 0 auto; margin: 0 auto;
padding: 54px 0 84px;
color: #18283e;
} }
.el-card { .notes-hero {
border-radius: 20px; position: relative;
box-shadow: 0 0 15px 5px white; 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) { .notes-hero::after {
.el-timeline { content: '';
width: 98%; width: 76px;
padding: 2px; height: 3px;
position: absolute;
bottom: -2px;
left: 4px;
border-radius: 999px;
background: #ef6a3a;
} }
.el-timeline /deep/ .el-timeline-item__wrapper { .eyebrow {
padding-left: 15px !important; 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> </style>

File diff suppressed because it is too large Load Diff

View File

@@ -72,7 +72,7 @@
likeNum: null, likeNum: null,
content: null, content: null,
type: null, type: null,
blogId: this.$route.query.id, blogId: this.$route.params.id || this.$route.query.id,
userId: null, userId: null,
delFlag: null, delFlag: null,
createBy: null, createBy: null,
@@ -119,7 +119,7 @@
likeNum: null, likeNum: null,
content: null, content: null,
type: null, type: null,
blogId: this.$route.query.id, blogId: this.$route.params.id || this.$route.query.id,
userId: null, userId: null,
delFlag: null, delFlag: null,
createBy: null, createBy: null,

View File

@@ -1,75 +1,68 @@
<template> <template>
<div class="cms-shell">
<el-container> <div class="ambient ambient-one"></div>
<div class="background" :style="backgroundImage"></div> <div class="ambient ambient-two"></div>
<cmsNavBar></cmsNavBar> <cms-nav-bar />
<keep-alive> <cms-main />
<cmsMain></cmsMain> <cms-footer />
</keep-alive> </div>
<el-footer height="40px">
<cmsFooter></cmsFooter>
</el-footer>
</el-container>
</template> </template>
<script> <script>
import cmsMain from './main.vue' import cmsMain from './main.vue'
import cmsNavBar from './NavBar.vue' import cmsNavBar from './NavBar.vue'
import cmsFooter from './Footer.vue' import cmsFooter from './Footer.vue'
// 引入背景图
import backgroundImages from './backgroundImages.js'
export default { export default {
data() { name: 'CmsLayout',
return {
backgroundImages,
};
},
methods: {
handleSelect(key, keyPath) {
console.log(key, keyPath);
}
},
components: { components: {
cmsMain, cmsMain,
cmsNavBar, cmsNavBar,
cmsFooter cmsFooter
},
computed: {
backgroundImage() {
// 根据背景图数组的长度随机选择索引
const randIndex = Math.floor(Math.random() * this.backgroundImages.length)
return {
// 获取对应的图片资源并将其设置到`background-image`属性上
backgroundImage: `url(${this.backgroundImages[randIndex]})`
} }
} }
},
}
</script> </script>
<style scoped> <style scoped>
.el-footer { .cms-shell {
background-color: rgba(84, 92, 100, 0.5); --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 { .ambient {
background-repeat: no-repeat;
background-size: cover;
margin: 0px;
padding: 0px;
top: 0;
width: 100%;
height: 120vh;
position: fixed; position: fixed;
z-index:-1; z-index: 0;
border-radius: 50%;
pointer-events: none;
filter: blur(2px);
} }
@media screen and (max-width: 768px) { .ambient-one {
.title { width: 420px;
width: 100%; height: 420px;
background-position-x: center; top: 80px;
background-position-y: 0; 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> </style>

View File

@@ -1,7 +1,7 @@
<template> <template>
<el-main> <main class="cms-main">
<router-view :key="key" /> <router-view :key="key" />
</el-main> </main>
</template> </template>
<script> <script>
@@ -16,7 +16,9 @@
</script> </script>
<style scoped> <style scoped>
.el-main { .cms-main {
min-height: 650px; min-height: calc(100vh - 152px);
position: relative;
z-index: 1;
} }
</style> </style>