Files
ruo-yi-vue-blog/ruoyi-ui/src/views/cms/components/cmsBlog.vue
2026-08-25 11:08:59 +08:00

1453 lines
39 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="detail-page">
<header class="article-hero">
<nav class="breadcrumbs" aria-label="面包屑导航">
<router-link to="/">首页</router-link>
<i class="el-icon-arrow-right"></i>
<router-link v-if="primaryCategory" :to="`/category/${primaryCategory.typeId}`">
{{ primaryCategory.typeName }}
</router-link>
<span v-else>内容详情</span>
</nav>
<div class="hero-layout">
<div class="article-heading">
<div class="hero-heading-top">
<div class="category-row">
<span class="project-code-label">{{ projectCode }}</span>
<router-link v-for="type in blog.types" :key="type.typeId" :to="`/category/${type.typeId}`">
{{ type.typeName }}
</router-link>
</div>
<span class="hero-section-label">PROJECT DETAIL</span>
</div>
<h1>{{ cleanProjectTitle || '正在加载项目…' }}</h1>
<p v-if="blog.blogDesc" class="article-lead">{{ blog.blogDesc }}</p>
<div class="hero-footer">
<div class="article-meta">
<span class="author-avatar">{{ (blog.createBy || '南').slice(0, 1) }}</span>
<div class="author-copy">
<strong>{{ blog.createBy || '南音' }}</strong>
<span>
<time :datetime="isoDate(blog.createTime)">{{ blog.createTime }}</time>
<i></i>{{ blog.views || 0 }} 次阅读
</span>
</div>
</div>
<router-link to="/projects" class="hero-catalog-link">
浏览全部源码 <i class="el-icon-right"></i>
</router-link>
</div>
</div>
</div>
</header>
<main class="article-wrap">
<div class="project-detail-grid">
<div class="project-main-column">
<article class="article-paper">
<section class="project-overview" aria-labelledby="overview-title">
<div class="content-section-title">
<span>PROJECT OVERVIEW</span>
<h2 id="overview-title">项目概览</h2>
</div>
<div class="overview-grid">
<div><span>项目编号</span><strong>{{ projectCode }}</strong></div>
<div><span>项目类型</span><strong>{{ projectTypeText }}</strong></div>
<div><span>核心技术</span><strong>{{ techStackText }}</strong></div>
<div><span>运行环境</span><strong>JDK · MySQL · Node.js</strong></div>
</div>
<div class="delivery-strip">
<span><i class="el-icon-circle-check"></i> 完整源码</span>
<span><i class="el-icon-circle-check"></i> 数据库文件</span>
<span><i class="el-icon-circle-check"></i> 项目截图</span>
<span><i class="el-icon-circle-check"></i> 运行说明</span>
</div>
</section>
<div class="content-section-title project-detail-title">
<span>PROJECT DETAILS</span>
<h2>项目功能与说明</h2>
</div>
<div v-if="blog.contentType === '1'" class="article-content typo ql-editor" v-html="blog.content"></div>
<div v-if="blog.contentType === '3'" class="article-content" v-html="blog.content"></div>
<div v-if="blog.contentType === '2'" class="article-content markdown-content">
<CherryMarkdown ref="CherryMarkdown" v-model="blog.contentMarkdown" :defaultModel="'previewOnly'" />
</div>
<section v-if="blog.blogFilesNew && blog.blogFilesNew.length" class="attachments" aria-labelledby="attachment-title">
<div class="content-section-title">
<span>FILES</span>
<h2 id="attachment-title">项目附件</h2>
</div>
<div class="attachment-list">
<div v-for="file in blog.blogFilesNew" :key="file.filePath" class="attachment-card">
<span class="file-icon"><i class="el-icon-document"></i></span>
<div class="file-copy">
<strong>{{ file.fileOriginName }}</strong>
<span>{{ file.fileSuffix || '文件' }} · {{ file.fileSize || '未知大小' }}</span>
<p v-if="file.remark">{{ file.remark }}</p>
</div>
<div class="file-actions">
<button type="button" @click="handlePreview(file)"><i class="el-icon-view"></i> 预览</button>
<button type="button" class="download" @click="handleDownload(file)"><i class="el-icon-download"></i> 下载</button>
</div>
</div>
</div>
</section>
<footer class="article-footer">
<div class="article-tags">
<router-link v-for="tag in blog.tags" :key="tag.tagId" :to="`/tag/${tag.tagId}`">
# {{ tag.tagName }}
</router-link>
</div>
<div class="author-note">
<span class="note-mark">NY</span>
<div>
<strong>由 {{ blog.createBy || '南音' }} 整理发布</strong>
<p>记录真实开发过程,分享可以运行、可以复用的代码与经验。</p>
</div>
<time v-if="blog.updateTime" :datetime="isoDate(blog.updateTime)">更新于 {{ blog.updateTime }}</time>
</div>
</footer>
</article>
</div>
<aside class="source-panel">
<div class="source-panel-head">
<span>{{ projectCode }}</span>
<small>SOURCE PACKAGE</small>
</div>
<h2>{{ cleanProjectTitle }}</h2>
<p>{{ blog.blogDesc || '完整项目源码与运行资料,适合学习、课程设计和毕业设计参考。' }}</p>
<dl>
<div><dt>技术栈</dt><dd>{{ techStackText }}</dd></div>
<div><dt>数据库</dt><dd>MySQL</dd></div>
<div><dt>更新时间</dt><dd>{{ shortDate(blog.updateTime || blog.createTime) }}</dd></div>
<div><dt>浏览量</dt><dd>{{ blog.views || 0 }}</dd></div>
</dl>
<ul>
<li><i class="el-icon-check"></i> 项目源代码</li>
<li><i class="el-icon-check"></i> 数据库脚本</li>
<li><i class="el-icon-check"></i> 配置与运行说明</li>
</ul>
<button type="button" class="get-source-button" @click="getSourcePackage">
<i class="el-icon-download"></i> 获取源码
</button>
<router-link to="/cms/doucument" class="document-link">
<i class="el-icon-document"></i> 查看运行教程
</router-link>
<small class="source-tip">获取前请先确认技术栈与运行环境</small>
</aside>
</div>
<aside v-if="relatedBlogs.length" class="related-section" aria-labelledby="related-content-title">
<div class="content-section-title">
<span>KEEP READING</span>
<h2 id="related-content-title">相似源码项目</h2>
</div>
<div class="related-grid">
<router-link v-for="(related, index) in relatedBlogs" :key="related.id" :to="contentPath(related)" class="related-card">
<span>{{ String(index + 1).padStart(2, '0') }}</span>
<h3>{{ related.title }}</h3>
<i class="el-icon-right"></i>
</router-link>
</div>
</aside>
<section class="comments-section">
<div class="content-section-title">
<span>DISCUSSION</span>
<h2>项目咨询与交流</h2>
</div>
<comment />
</section>
</main>
<el-dialog
:visible.sync="sourceQrVisible"
custom-class="source-qr-dialog"
:show-close="true"
:close-on-click-modal="true"
:modal-append-to-body="false"
aria-label="小程序源码获取二维码"
>
<div class="source-qr-content">
<span class="source-qr-kicker">MINI PROGRAM</span>
<h2>扫码进入小程序获取源码</h2>
<p>打开微信扫一扫,在小程序内搜索当前项目编号即可查看源码获取方式。</p>
<div class="source-qr-image">
<img src="https://img.yidaima.cn/qrcode.jpg" alt="南音源码库微信小程序二维码">
</div>
<div class="source-qr-project">
<span>当前项目</span>
<strong>{{ projectCode }} · {{ cleanProjectTitle }}</strong>
</div>
</div>
</el-dialog>
<el-backtop :bottom="32" :right="28">
<div class="back-top"><i class="el-icon-top"></i></div>
</el-backtop>
</div>
</template>
<script>
// import 'cherry-markdown/dist/cherry-markdown.min.css'
//使用prism.js代码高亮
import {
getBlogDetail,
addBlogViews,
cmsListByTypeId,
} from "@/api/cms/blog";
import {mapState} from 'vuex'
import {
blogImageUrl,
canonicalUrl,
contentPath,
isoDate,
jsonLdScript,
seoDescription,
siteIdentity
} from '@/utils/seo'
export default {
components: {
comment: () => import(/* webpackChunkName: "cms-comments" */ './comment/Ipcomment'),
CherryMarkdown: () => import(/* webpackChunkName: "cherry-markdown" */ '@/components/CherryMarkdown'),
},
data() {
return {
blog: {
blogFilesNew: []
},
relatedBlogs: [],
commentForm: {
content: ''
},
sourceQrVisible: false,
}
},
watch: {
blogId() {
this.getBlogInfomation()
}
},
created() {
this.getBlogInfomation()
},
computed: {
...mapState([
'userInfo',
'administrator',
]),
blogId() {
return this.$route.params.id || this.$route.query.id
},
articleDescription() {
return seoDescription(this.blog) || '南音源码库项目源码、开发教程与技术文档。'
},
articleImage() {
return blogImageUrl(this.blog)
},
articleUrl() {
const path = this.blog.id ? contentPath(this.blog) : this.$route.path
return canonicalUrl(path)
},
primaryCategory() {
return this.blog.types && this.blog.types.length ? this.blog.types[0] : null
},
projectCode() {
const title = this.blog.title ? String(this.blog.title) : ''
const match = title.match(/[【\[]\s*([A-Za-z]*\d+)\s*[】\]]/)
return match ? match[1].toUpperCase() : `SRC-${String(this.blog.id || 0).padStart(4, '0')}`
},
cleanProjectTitle() {
return String(this.blog.title || '')
.replace(/^[【\[]\s*[A-Za-z]*\d+\s*[】\]]\s*/, '')
.trim()
},
projectTypeText() {
const types = (this.blog.types || []).map(type => type.typeName).filter(Boolean)
return types.length ? types.slice(0, 2).join(' · ') : '管理系统'
},
techStack() {
const tags = (this.blog.tags || []).map(tag => tag.tagName).filter(Boolean)
if (tags.length) return tags.slice(0, 4)
const source = `${this.blog.title || ''} ${this.blog.blogDesc || ''} ${this.blog.content || ''}`.toLowerCase()
const candidates = [
['Spring Boot', /spring\s*boot|springboot/],
['SSM', /\bssm\b/],
['Vue3', /vue\s*3|vue3/],
['Vue2', /vue\s*2|vue2/],
['微信小程序', /微信小程序|小程序/],
['Java', /\bjava\b|jdk/],
['MySQL', /mysql/],
['Python', /python/]
]
const inferred = candidates.filter(item => item[1].test(source)).map(item => item[0])
return inferred.length ? inferred.slice(0, 4) : ['Java Web', 'MySQL']
},
techStackText() {
return this.techStack.join(' · ')
}
},
metaInfo() {
const identity = siteIdentity()
const fullTitle = this.blog.title ? `${this.blog.title} - ${identity.name}` : `内容详情 - ${identity.name}`
const meta = [
{
vmid: 'description',
name: 'description',
content: this.articleDescription
},
{
vmid: 'robots',
name: 'robots',
content: this.blog.id ? 'index,follow,max-image-preview:large' : 'noindex,follow'
},
{
vmid: 'og:title',
property: 'og:title',
content: fullTitle
},
{
vmid: 'og:description',
property: 'og:description',
content: this.articleDescription
},
{
vmid: 'og:type',
property: 'og:type',
content: 'article'
},
{
vmid: 'og:url',
property: 'og:url',
content: this.articleUrl
},
{
vmid: 'twitter:card',
name: 'twitter:card',
content: this.articleImage ? 'summary_large_image' : 'summary'
},
{
vmid: 'twitter:title',
name: 'twitter:title',
content: fullTitle
},
{
vmid: 'twitter:description',
name: 'twitter:description',
content: this.articleDescription
}
]
if (this.articleImage) {
meta.push(
{
vmid: 'og:image',
property: 'og:image',
content: this.articleImage
},
{
vmid: 'twitter:image',
name: 'twitter:image',
content: this.articleImage
}
)
}
if (this.blog.createTime) {
meta.push({
vmid: 'article:published_time',
property: 'article:published_time',
content: isoDate(this.blog.createTime)
})
}
if (this.blog.updateTime) {
meta.push({
vmid: 'article:modified_time',
property: 'article:modified_time',
content: isoDate(this.blog.updateTime)
})
}
const scripts = []
if (this.blog.id) {
const article = {
'@context': 'https://schema.org',
'@type': 'BlogPosting',
headline: this.blog.title,
description: this.articleDescription,
url: this.articleUrl,
mainEntityOfPage: this.articleUrl,
image: this.articleImage || undefined,
datePublished: isoDate(this.blog.createTime),
dateModified: isoDate(this.blog.updateTime || this.blog.createTime),
author: {
'@type': 'Person',
name: this.blog.createBy || identity.name
},
publisher: {
'@type': 'Organization',
name: identity.name,
url: identity.url
},
articleSection: this.primaryCategory && this.primaryCategory.typeName,
keywords: (this.blog.tags || []).map(tag => tag.tagName).join(',')
}
if (contentPath(this.blog).startsWith('/source/')) {
article.about = {
'@type': 'SoftwareSourceCode',
name: this.blog.title,
description: this.articleDescription
}
}
const breadcrumbItems = [
{
'@type': 'ListItem',
position: 1,
name: '首页',
item: identity.url
}
]
if (this.primaryCategory) {
breadcrumbItems.push({
'@type': 'ListItem',
position: 2,
name: this.primaryCategory.typeName,
item: canonicalUrl(`/category/${this.primaryCategory.typeId}`)
})
}
breadcrumbItems.push({
'@type': 'ListItem',
position: breadcrumbItems.length + 1,
name: this.blog.title,
item: this.articleUrl
})
scripts.push(
jsonLdScript('article-jsonld', article),
jsonLdScript('breadcrumb-jsonld', {
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
itemListElement: breadcrumbItems
})
)
}
return {
title: fullTitle,
titleTemplate: null,
meta: [
...meta
],
link: [
{
vmid: 'canonical',
rel: 'canonical',
href: this.articleUrl
}
],
script: scripts
}
},
methods: {
contentPath,
isoDate,
shortDate(value) {
return value ? String(value).slice(0, 10) : '持续维护'
},
getSourcePackage() {
this.sourceQrVisible = true
},
// 获取博客详情信息
async getBlogInfomation() {
if (!this.blogId) {
this.$router.replace('/404')
return
}
this.blog = {
blogFilesNew: []
}
this.relatedBlogs = []
try {
const response = await getBlogDetail(this.blogId)
if (!response.data) {
throw new Error('文章不存在')
}
this.blog = response.data
this.blog.blogFilesNew = []
if (response.data.blogFiles !== null) {
try {
this.blog.blogFilesNew = JSON.parse(response.data.blogFiles)
} catch (error) {
this.blog.blogFilesNew = []
}
}
const preferredPath = contentPath(this.blog)
if (this.$route.path !== preferredPath) {
this.$router.replace(preferredPath).catch(() => {})
}
this.loadSyntaxHighlighting()
this.loadRelatedBlogs()
addBlogViews(this.blogId).catch(() => {})
} catch (error) {
this.blog = {
blogFilesNew: []
}
this.$router.replace('/404')
}
},
async loadRelatedBlogs() {
const primaryType = this.primaryCategory
if (!primaryType) {
return
}
try {
const response = await cmsListByTypeId(primaryType.typeId, {
pageNum: 1,
pageSize: 6
})
this.relatedBlogs = response.rows
.filter(blog => String(blog.id) !== String(this.blog.id))
.slice(0, 4)
} catch (error) {
this.relatedBlogs = []
}
},
async loadSyntaxHighlighting() {
if (!['1', '3'].includes(String(this.blog.contentType))) {
return
}
try {
const modules = await Promise.all([
import(/* webpackChunkName: "prism-highlighter" */ '@/views/cms/plugins/prism.js'),
import(/* webpackChunkName: "prism-highlighter" */ '@/views/cms/plugins/prism.css')
])
const prism = modules[0].default || window.Prism
this.$nextTick(() => {
if (prism && prism.highlightAllUnder) {
prism.highlightAllUnder(this.$el)
}
})
} catch (error) {
// Optional highlighting must not prevent the article from rendering.
}
},
// 文件下载处理
handleDownload(row) {
var name = row.fileOriginName;
var url = row.filePath;
const a = document.createElement('a')
a.setAttribute('download', name)
a.setAttribute('target', '_blank')
a.setAttribute('href', process.env.VUE_APP_BASE_API + url)
a.click()
},
// 文件预览处理
async handlePreview(row) {
var name = row.fileOriginName;
var filePath = row.filePath;
var url = process.env.VUE_APP_BASE_API + filePath;
const response = await fetch(url);
if(response.status === 200){
const blob = await response.blob()
var objectURL = window.URL.createObjectURL(blob);
var openUrl = window.location.origin + '/fileViewer/index.html' + '?name=' + name + '&url=' + objectURL
window.open(openUrl);
} else {
this.$message.error("文件不存在!")
}
},
},
}
</script>
<style scoped>
.el-card {
width: 100%;
}
.el-popper /deep/ {
box-shadow: 0 2px 4px 0 rgb(34 36 38 / 12%),
}
.first-card {
border-radius: 10px 10px 10px 10px;
position: relative;
padding-bottom: 10px;
/*text-align: center;*/
font: 300 1em/1.8 PingFang SC, Lantinghei SC, Microsoft Yahei, Hiragino Sans GB, Microsoft Sans Serif, WenQuanYi Micro Hei, sans-serif;
}
hr.style-one {
width: 100%;
background-image: linear-gradient(to right, rgba(64, 158, 255, 0), rgba(64, 158, 255, 0.75), rgba(64, 158, 255, 0));
}
.appreciate {
text-align: center;
}
.tags {
display: flex;
align-items: center;
margin-left: 50px;
margin-top: 20px;
}
.tag-item {
display: flex;
justify-content: space-around;
align-items: center;
margin-left: 10px;
margin-bottom: 20px;
}
.tag {
padding-left: 10px;
padding-right: 10px;
border-radius: 5px;
background-color: #ecf5ff;
border: 1px solid #409eff;
color: #409eff;
display: flex;
text-decoration: none;
}
.type-link {
margin-left: 6px;
text-decoration: none;
}
.related-content {
margin: 24px 0;
padding: 16px 20px;
border-left: 4px solid #409eff;
background: #f7fbff;
}
.related-content h2 {
margin: 0 0 10px;
font-size: 18px;
}
.related-content ul {
margin: 0;
padding-left: 20px;
}
.related-content a {
color: #3a8ee6;
line-height: 2;
text-decoration: none;
}
.related-content a:hover {
text-decoration: underline;
}
.sjx-outer {
width: 0;
height: 0;
border-top: 7px solid transparent;
border-bottom: 7px solid transparent;
border-right: 7px solid #409eff;
position: relative;
}
.sjx-inner {
border-top: 7px solid transparent;
border-bottom: 7px solid transparent;
border-right: 7px solid #ecf5ff;
top: -7px;
left: 1px;
position: absolute;
}
.author {
text-align: left;
background-color: #fcfff5;
box-shadow: 0 0 0 1px #a3c293 inset;
color: #2c662d;
width: 100%;
position: absolute;
left: 0;
margin: 20px 0;
padding: 20px 0;
font-size: small;
font-family: PingFang SC, Lantinghei SC, Microsoft Yahei, Hiragino Sans GB, Microsoft Sans Serif, WenQuanYi Micro Hei, sans-serif;
}
.comments {
margin-top: 150px;
box-shadow: 0 1px 2px 0 rgb(34 36 38 / 15%);
border: 1px solid rgba(34, 36, 38, .15);
border-top: 2px solid #409EFF;
text-align: left;
}
.blog-title {
text-align: center;
}
.blog-info {
display: flex;
align-items: center;
color: rgba(0, 0, 0, .4);
font-size: 13px;
}
.blog-date {
margin-right: 5px;
float: right;
}
.blog-views {
margin-right: 5px;
float: right;
}
.user-info {
justify-content: space-around;
align-items: center;
margin-right: 15px;
float: left;
}
.header {
text-decoration: none;
color: #3a8ee6;
font-weight: bold;
}
@media screen and (max-width: 768px) {
.tags {
margin-left: 0;
margin-top: 20px;
}
hr {
display: none;
}
.comment-content {
font-size: 12px !important;
}
}
@media only screen and (max-width: 480px) {
h2 {
font-weight: normal;
}
code, pre {
font-size: 13px !important;
}
}
.blogFilesInfoName {
text-align: center;
padding-top: 5px;
}
/* 限制文章内容中图片的大小 */
.ql-editor img,
.typo img {
max-width: 100%;
height: auto;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
margin: 10px 0;
}
/* Markdown 内容中的图片样式 */
/deep/ .cherry-markdown img {
max-width: 100%;
height: auto;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
margin: 10px 0;
}
</style>
<style scoped>
.detail-page {
padding-bottom: 76px;
color: #17273c;
}
.article-hero {
position: relative;
padding: 28px 0 42px;
color: #fff;
background:
radial-gradient(circle at 85% 15%, rgba(239, 106, 58, .18), transparent 26%),
#172941;
}
.article-hero::after {
content: '';
height: 1px;
position: absolute;
right: 0;
bottom: 0;
left: 0;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, .15), transparent);
}
.breadcrumbs,
.hero-layout,
.article-wrap {
width: min(980px, calc(100% - 40px));
margin-right: auto;
margin-left: auto;
}
.breadcrumbs {
margin-bottom: 22px;
display: flex;
align-items: center;
gap: 9px;
color: rgba(255, 255, 255, .46);
font-size: 12px;
}
.breadcrumbs a {
color: rgba(255, 255, 255, .62);
text-decoration: none;
}
.breadcrumbs a:hover { color: #fff; }
.breadcrumbs i { font-size: 10px; }
.hero-layout {
position: relative;
display: block;
overflow: hidden;
padding: 30px 34px 26px;
border: 1px solid rgba(255, 255, 255, .1);
border-radius: 18px;
background:
linear-gradient(135deg, rgba(255, 255, 255, .055), rgba(255, 255, 255, .018)),
rgba(10, 24, 42, .22);
box-shadow: 0 22px 48px rgba(7, 17, 30, .18);
}
.hero-layout::before {
content: '';
width: 4px;
position: absolute;
top: 28px;
bottom: 28px;
left: 0;
border-radius: 0 999px 999px 0;
background: linear-gradient(180deg, #ef6a3a, #f5aa76);
}
.hero-layout::after {
content: '';
width: 260px;
height: 260px;
position: absolute;
top: -150px;
right: -80px;
border: 1px solid rgba(255, 255, 255, .04);
border-radius: 50%;
box-shadow: 0 0 0 38px rgba(255, 255, 255, .012), 0 0 0 78px rgba(255, 255, 255, .008);
pointer-events: none;
}
.article-heading {
position: relative;
z-index: 1;
}
.hero-heading-top {
display: flex;
align-items: center;
justify-content: space-between;
gap: 20px;
}
.hero-section-label {
color: rgba(255, 255, 255, .28);
font: 700 9px/1 Consolas, monospace;
letter-spacing: .18em;
}
.category-row {
min-height: 22px;
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.category-row a {
padding: 5px 9px;
border: 1px solid rgba(239, 106, 58, .4);
border-radius: 6px;
color: #f6a283;
background: rgba(239, 106, 58, .08);
font-size: 10px;
font-weight: 700;
text-decoration: none;
letter-spacing: .06em;
}
.article-heading h1 {
margin: 20px 0 18px;
overflow: hidden;
color: #fff;
font-size: clamp(30px, 3.2vw, 42px);
line-height: 1.18;
letter-spacing: -.04em;
text-overflow: ellipsis;
white-space: nowrap;
}
.article-lead {
max-width: 680px;
margin: 0 0 26px;
color: rgba(255, 255, 255, .58);
font-size: 15px;
line-height: 1.8;
}
.hero-footer {
display: flex;
align-items: center;
justify-content: space-between;
gap: 24px;
margin-top: 22px;
padding-top: 20px;
border-top: 1px solid rgba(255, 255, 255, .09);
}
.article-meta {
display: flex;
align-items: center;
gap: 11px;
}
.author-avatar {
width: 38px;
height: 38px;
display: grid;
place-items: center;
flex: 0 0 auto;
border-radius: 50%;
color: #fff;
background: #ef6a3a;
font-size: 14px;
font-weight: 700;
}
.author-copy {
display: flex;
flex-direction: column;
gap: 5px;
}
.author-copy strong { color: rgba(255, 255, 255, .9); font-size: 12px; }
.author-copy span {
display: flex;
align-items: center;
color: rgba(255, 255, 255, .4);
font-size: 10px;
}
.author-copy i {
width: 3px;
height: 3px;
margin: 0 8px;
border-radius: 50%;
background: rgba(255, 255, 255, .35);
}
.hero-catalog-link {
min-height: 36px;
display: inline-flex;
align-items: center;
gap: 9px;
padding: 0 14px;
border: 1px solid rgba(255, 255, 255, .13);
border-radius: 9px;
color: rgba(255, 255, 255, .7);
background: rgba(255, 255, 255, .045);
font-size: 11px;
font-weight: 700;
text-decoration: none;
transition: color .2s ease, border-color .2s ease, background .2s ease;
}
.hero-catalog-link:hover {
color: #fff;
border-color: rgba(239, 106, 58, .62);
background: rgba(239, 106, 58, .14);
}
.article-wrap {
margin-top: 34px;
}
.article-paper,
.comments-section {
padding: 54px 68px;
border: 1px solid #e4dfd5;
border-radius: 20px;
background: #fffdf8;
box-shadow: 0 16px 44px rgba(19, 34, 56, .07);
}
.article-content {
color: #2f3d50;
font-size: 16px;
line-height: 1.9;
}
.article-content /deep/ h1,
.article-content /deep/ h2,
.article-content /deep/ h3,
.article-content /deep/ h4 {
color: #17273c;
line-height: 1.4;
scroll-margin-top: 24px;
}
.article-content /deep/ h2 {
margin: 2.2em 0 .8em;
padding-bottom: .45em;
border-bottom: 1px solid #e9e3d9;
font-size: 25px;
}
.article-content /deep/ h3 { margin-top: 1.9em; font-size: 20px; }
.article-content /deep/ p { margin: 1em 0; }
.article-content /deep/ a { color: #d85831; text-decoration-thickness: 1px; text-underline-offset: 3px; }
.article-content /deep/ strong { color: #17273c; }
.article-content /deep/ blockquote {
margin: 24px 0;
padding: 16px 20px;
border-left: 3px solid #ef6a3a;
color: #5d6878;
background: #fff2eb;
}
.article-content /deep/ pre {
margin: 24px 0;
padding: 20px;
overflow-x: auto;
border: 0;
border-radius: 13px;
background: #172941;
box-shadow: 0 10px 24px rgba(19, 34, 56, .12);
}
.article-content /deep/ code {
padding: 2px 5px;
border-radius: 4px;
color: #c7532e;
background: #fff0e8;
font-family: Consolas, Monaco, monospace;
}
.article-content /deep/ pre code {
padding: 0;
color: inherit;
background: transparent;
}
.article-content /deep/ img,
.markdown-content /deep/ img {
max-width: 100%;
height: auto;
margin: 22px auto;
display: block;
border-radius: 13px;
box-shadow: 0 12px 30px rgba(19, 34, 56, .12);
}
.content-section-title {
margin-bottom: 20px;
}
.content-section-title span {
color: #d85831;
font: 700 10px/1.4 Consolas, monospace;
letter-spacing: .17em;
}
.content-section-title h2 {
margin: 6px 0 0;
color: #17273c;
font-size: 25px;
letter-spacing: -.02em;
}
.attachments {
margin-top: 48px;
padding-top: 38px;
border-top: 1px solid #e8e2d8;
}
.attachment-list {
display: grid;
gap: 12px;
}
.attachment-card {
padding: 15px;
display: grid;
grid-template-columns: 42px minmax(0, 1fr) auto;
align-items: center;
gap: 14px;
border: 1px solid #e5e0d7;
border-radius: 12px;
background: #faf8f2;
}
.file-icon {
width: 42px;
height: 42px;
display: grid;
place-items: center;
border-radius: 10px;
color: #28727a;
background: #e6f0ef;
font-size: 18px;
}
.file-copy {
min-width: 0;
display: flex;
flex-direction: column;
gap: 4px;
}
.file-copy strong { overflow: hidden; font-size: 13px; text-overflow: ellipsis; white-space: nowrap; }
.file-copy span { color: #969ca5; font-size: 10px; }
.file-copy p { margin: 2px 0 0; color: #737c89; font-size: 11px; }
.file-actions { display: flex; gap: 7px; }
.file-actions button {
height: 34px;
padding: 0 12px;
border: 1px solid #d8d4cb;
border-radius: 8px;
color: #596678;
background: #fff;
font-size: 11px;
cursor: pointer;
}
.file-actions .download { border-color: #ef6a3a; color: #fff; background: #ef6a3a; }
.article-footer {
margin-top: 48px;
padding-top: 30px;
border-top: 1px solid #e8e2d8;
}
.article-tags {
margin-bottom: 24px;
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.article-tags a {
padding: 7px 11px;
border-radius: 8px;
color: #28727a;
background: #eaf2f1;
font-size: 11px;
text-decoration: none;
}
.author-note {
padding: 18px;
display: grid;
grid-template-columns: 48px minmax(0, 1fr) auto;
align-items: center;
gap: 14px;
border-radius: 14px;
background: #172941;
}
.note-mark {
width: 48px;
height: 48px;
display: grid;
place-items: center;
border: 1px solid rgba(255, 255, 255, .14);
border-radius: 12px;
color: #fff;
background: rgba(255, 255, 255, .06);
font: 700 14px/1 Consolas, monospace;
}
.author-note strong { color: #fff; font-size: 12px; }
.author-note p { margin: 5px 0 0; color: rgba(255, 255, 255, .45); font-size: 10px; }
.author-note time { color: rgba(255, 255, 255, .32); font-size: 9px; }
.related-section {
margin-top: 48px;
}
.related-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
}
.related-card {
min-height: 110px;
padding: 19px;
display: grid;
grid-template-columns: 28px minmax(0, 1fr) 18px;
align-items: center;
gap: 8px;
border: 1px solid #dfdbd2;
border-radius: 14px;
color: #17273c;
background: rgba(255, 253, 248, .88);
text-decoration: none;
transition: transform .2s ease, border-color .2s ease;
}
.related-card:hover { border-color: #ef6a3a; transform: translateY(-2px); }
.related-card > span { color: #ef6a3a; font: 700 10px/1 Consolas, monospace; }
.related-card h3 { margin: 0; font-size: 14px; line-height: 1.55; }
.related-card i { color: #9ca2aa; }
.comments-section {
margin-top: 48px;
padding-top: 42px;
}
.back-top {
width: 44px;
height: 44px;
display: grid;
place-items: center;
border-radius: 12px;
color: #fff;
background: #172941;
box-shadow: 0 12px 24px rgba(19, 34, 56, .2);
}
@media (max-width: 820px) {
.article-hero { padding: 24px 0 36px; }
.breadcrumbs,
.hero-layout,
.article-wrap { width: min(100% - 28px, 980px); }
.breadcrumbs { margin-bottom: 18px; }
.hero-layout { padding: 26px 26px 23px; }
.article-heading h1 { white-space: normal; }
.article-paper,
.comments-section { padding: 38px 34px; }
}
@media (max-width: 560px) {
.hero-layout { padding: 22px 20px 20px; border-radius: 14px; }
.hero-layout::before { top: 20px; bottom: 20px; }
.hero-section-label { display: none; }
.article-heading h1 { font-size: 32px; }
.article-lead { font-size: 14px; }
.hero-footer { align-items: flex-start; flex-direction: column; gap: 16px; margin-top: 18px; padding-top: 17px; }
.hero-catalog-link { width: 100%; justify-content: center; }
.article-wrap { margin-top: 20px; }
.article-paper,
.comments-section { padding: 28px 20px; border-radius: 15px; }
.article-content { font-size: 15px; line-height: 1.8; }
.attachment-card { grid-template-columns: 38px minmax(0, 1fr); }
.file-actions { grid-column: 1 / -1; }
.file-actions button { flex: 1; }
.author-note { grid-template-columns: 42px minmax(0, 1fr); }
.author-note time { display: none; }
.related-grid { grid-template-columns: 1fr; }
}
</style>
<style scoped>
.project-code-label {
padding: 5px 9px;
border-radius: 6px;
color: #fff;
background: #ef6a3a;
font: 700 10px/1.2 Consolas, monospace;
letter-spacing: .06em;
}
.article-hero .breadcrumbs,
.article-hero .hero-layout,
.article-wrap {
width: min(1180px, calc(100% - 40px));
}
.project-detail-grid {
display: grid;
grid-template-columns: minmax(0, 1fr) 300px;
align-items: start;
gap: 28px;
}
.project-main-column { min-width: 0; }
.project-overview {
margin-bottom: 44px;
padding-bottom: 34px;
border-bottom: 1px solid #e8e2d8;
}
.overview-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
}
.overview-grid > div {
min-width: 0;
padding: 15px 17px;
display: flex;
flex-direction: column;
gap: 7px;
border: 1px solid #e5e0d7;
border-radius: 11px;
background: #faf8f2;
}
.overview-grid span { color: #9298a1; font-size: 9px; }
.overview-grid strong { overflow: hidden; color: #24364c; font-size: 12px; text-overflow: ellipsis; white-space: nowrap; }
.delivery-strip {
margin-top: 12px;
padding: 14px 16px;
display: flex;
flex-wrap: wrap;
gap: 18px;
border-radius: 10px;
color: #687485;
background: #eaf2f1;
font-size: 10px;
}
.delivery-strip i { margin-right: 4px; color: #299274; }
.project-detail-title { margin: 0 0 28px; scroll-margin-top: 18px; }
.source-panel {
position: sticky;
top: 18px;
padding: 24px;
border: 1px solid #ded8ce;
border-radius: 17px;
background: #fffdf8;
box-shadow: 0 16px 38px rgba(19, 34, 56, .1);
}
.source-panel-head { display: flex; align-items: center; justify-content: space-between; }
.source-panel-head > span { padding: 7px 9px; border-radius: 7px; color: #fff; background: #ef6a3a; font: 700 10px/1 Consolas, monospace; }
.source-panel-head small { color: #a3a7ad; font: 700 8px/1 Consolas, monospace; letter-spacing: .13em; }
.source-panel h2 { margin: 18px 0 10px; color: #17273c; font-size: 20px; line-height: 1.45; }
.source-panel > p { margin: 0 0 19px; color: #747e8c; font-size: 11px; line-height: 1.75; }
.source-panel dl { margin: 0; padding: 4px 0; border-top: 1px solid #e9e4da; border-bottom: 1px solid #e9e4da; }
.source-panel dl > div { padding: 9px 0; display: grid; grid-template-columns: 62px minmax(0, 1fr); gap: 8px; font-size: 10px; }
.source-panel dt { color: #9298a1; }
.source-panel dd { margin: 0; overflow: hidden; color: #303f52; text-align: right; text-overflow: ellipsis; white-space: nowrap; }
.source-panel ul { margin: 17px 0; padding: 0; display: grid; gap: 8px; list-style: none; color: #687485; font-size: 10px; }
.source-panel li i { width: 18px; height: 18px; margin-right: 6px; display: inline-grid; place-items: center; border-radius: 50%; color: #299274; background: #e7f3ef; font-size: 9px; }
.get-source-button,
.document-link {
width: 100%;
height: 44px;
display: flex;
align-items: center;
justify-content: center;
gap: 7px;
border-radius: 10px;
font-size: 12px;
font-weight: 700;
cursor: pointer;
text-decoration: none;
}
.get-source-button { border: 0; color: #fff; background: #ef6a3a; box-shadow: 0 10px 20px rgba(239, 106, 58, .22); }
.document-link { height: 40px; margin-top: 9px; border: 1px solid #dcd7ce; color: #536174; background: #fff; }
.source-tip { margin-top: 12px; display: block; color: #a1a5ab; text-align: center; font-size: 8px; }
.detail-page /deep/ .source-qr-dialog {
width: min(460px, calc(100% - 28px));
margin-top: 10vh !important;
overflow: hidden;
border-radius: 20px;
background: #fffdf8;
box-shadow: 0 24px 70px rgba(18, 34, 55, .24);
}
.detail-page /deep/ .source-qr-dialog .el-dialog__header { padding: 18px 20px 0; }
.detail-page /deep/ .source-qr-dialog .el-dialog__body { padding: 8px 28px 28px; }
.detail-page /deep/ .source-qr-dialog .el-dialog__headerbtn { top: 18px; right: 20px; }
.detail-page /deep/ .source-qr-dialog .el-dialog__close { color: #6e7886; font-size: 20px; }
.source-qr-content { text-align: center; }
.source-qr-kicker { color: #ef6a3a; font: 700 9px/1 Consolas, monospace; letter-spacing: .18em; }
.source-qr-content h2 { margin: 10px 0 7px; color: #17273c; font-size: 24px; line-height: 1.35; }
.source-qr-content > p { margin: 0 auto 20px; max-width: 390px; color: #788291; font-size: 12px; line-height: 1.7; }
.source-qr-image {
width: 300px;
height: 300px;
margin: 0 auto;
overflow: hidden;
border: 1px solid #e9e2d8;
border-radius: 14px;
background: #f8f5ef;
}
.source-qr-image img { width: 100%; height: 100%; display: block; object-fit: contain; }
.source-qr-project {
margin-top: 15px;
padding: 11px 13px;
display: grid;
grid-template-columns: auto minmax(0, 1fr);
align-items: center;
gap: 12px;
border-radius: 10px;
color: #25384e;
background: #edf4f2;
text-align: left;
}
.source-qr-project span { color: #74808d; font-size: 10px; }
.source-qr-project strong { overflow: hidden; font-size: 11px; text-align: right; text-overflow: ellipsis; white-space: nowrap; }
@media (max-width: 920px) {
.project-detail-grid { grid-template-columns: 1fr; }
.source-panel { position: static; }
}
@media (max-width: 820px) {
.article-hero .breadcrumbs,
.article-hero .hero-layout,
.article-wrap { width: min(100% - 28px, 1180px); }
}
@media (max-width: 560px) {
.overview-grid { grid-template-columns: 1fr; }
.delivery-strip { display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px; }
.source-panel { padding: 21px; }
.detail-page /deep/ .source-qr-dialog .el-dialog__body { padding: 8px 20px 22px; }
.source-qr-content h2 { font-size: 20px; }
.source-qr-image { width: 236px; height: 236px; margin: 0 auto; }
.source-qr-image img { width: auto; max-width: none; height: 100%; }
.source-qr-project { grid-template-columns: 1fr; gap: 5px; text-align: center; }
.source-qr-project strong { text-align: center; }
}
</style>