This commit is contained in:
2026-08-26 15:10:25 +08:00
parent 06debfd1fc
commit fc9613197b
202 changed files with 28919 additions and 5551 deletions
@@ -0,0 +1,366 @@
<!--#
layout("/layouts/platform.html"){
#-->
<style>
#sub-app-container-main-content { padding: 0 !important; background: #f5f5f5; }
.spread-wall { min-height: 100%; overflow-x: hidden; color: #2c3e50; background: #f5f5f5; }
.spread-hero { min-height: 450px; position: relative; padding: 18px 0 72px; display: flex; flex-direction: column; justify-content: center; overflow: visible; }
.spread-hero::before { content: ""; position: absolute; top: 0; left: 0; right: 0; height: 320px; background: url("${base!}/assets/platform/img/spread-bg.png?v=20260731") top center / cover no-repeat; z-index: 0; }
.spread-hero > * { position: relative; z-index: 1; }
.spread-brand { margin: 0 auto 24px; padding: 10px 16px; display: flex; align-items: center; gap: 12px; color: #2c3e50; border-radius: 12px; background: rgba(255,255,255,.76); box-shadow: 0 10px 26px rgba(0,0,0,.12); backdrop-filter: blur(6px); }
.spread-brand-icon { width: 42px; height: 42px; display: flex; align-items: center; justify-content: center; color: #c0141a; font-size: 24px; border-radius: 50%; background: #fff0f0; }
.spread-brand-title { font-family: "Microsoft YaHei", serif; font-size: 24px; font-weight: 700; letter-spacing: 3px; }
.carousel-bg-text { position: absolute; top: 48%; left: 50%; transform: translate(-50%, -50%); color: rgba(255,255,255,.04); font-size: 170px; font-weight: 900; letter-spacing: 24px; pointer-events: none; user-select: none; }
.hero-slider { width: calc(100% - 32px); max-width: none; min-height: 380px; height: auto; aspect-ratio: 32 / 9; margin: 0 auto; }
.hero-slider .el-carousel, .hero-slider .el-carousel__container { height: 100%; }
.hero-slider .el-carousel__arrow { width: 42px; height: 42px; color: #fff; border: 1px solid rgba(255,255,255,.25); background: rgba(44,62,80,.42); backdrop-filter: blur(4px); }
.hero-slider .el-carousel__item--card { overflow: visible; display: flex; align-items: center; justify-content: center; }
.hero-slider .el-carousel__mask { opacity: 0; background: transparent; }
.slider-item { width: 97%; height: auto; aspect-ratio: 16 / 9; margin: 0 auto; position: relative; overflow: hidden; display: flex; align-items: center; justify-content: center; box-sizing: border-box; cursor: pointer; border: 8px solid #fff; background: #fff; box-shadow: 0 12px 30px rgba(0,0,0,.18), 0 2px 5px rgba(0,0,0,.12); }
.slider-item img { width: auto; height: auto; max-width: 100%; max-height: 100%; display: block; flex: 0 0 auto; object-fit: contain !important; object-position: center; background: #fff; }
.slider-caption { min-height: 25%; padding: 10px 18px 9px; position: absolute; left: 0; right: 0; bottom: 0; display: flex; flex-direction: column; align-items: flex-start; justify-content: flex-end; box-sizing: border-box; color: #fff; background: linear-gradient(to bottom, rgba(82,82,82,.30) 0%, rgba(52,52,52,.52) 45%, rgba(24,24,24,.78) 100%); }
.slider-caption-line { width: 100%; }
.slider-caption-title { color: #fff; font-size: 15px; font-weight: 600; line-height: 1.45; text-shadow: 0 1px 3px rgba(0,0,0,.42); white-space: normal; word-break: break-all; }
.slider-caption-tag { max-width: 32%; margin: 0 8px 0 0; padding: 1px 7px; display: inline-block; overflow: hidden; box-sizing: border-box; color: rgba(255,255,255,.92); font-size: 10px; font-weight: 400; line-height: 18px; vertical-align: middle; text-align: center; text-overflow: ellipsis; white-space: nowrap; border: 1px solid rgba(255,255,255,.42); border-radius: 3px; background: rgba(255,255,255,.10); }
.slider-empty { height: 380px; display: flex; align-items: center; justify-content: center; color: #8a6f68; font-size: 22px; letter-spacing: 5px; border-radius: 12px; background: rgba(255,255,255,.86); box-shadow: 0 8px 28px rgba(0,0,0,.08); }
.spread-main { width: calc(100% - 32px); max-width: none; margin: -42px auto 40px; padding: 20px 0 38px; position: relative; z-index: 5; display: grid; grid-template-columns: 260px minmax(0, 1fr); gap: 24px; align-items: start; }
.category-panel, .content-panel { background: #fff; border-radius: 12px; box-shadow: 0 8px 28px rgba(90,53,34,.09); }
.category-panel { position: sticky; top: 20px; overflow: hidden; }
.category-header { padding: 18px 15px; color: #fff; font-family: "Microsoft YaHei", serif; font-size: 18px; font-weight: 700; text-align: center; letter-spacing: 2px; background: #c0141a; }
.category-group { border-bottom: 1px solid #f0e7e5; }
.category-parent { padding: 18px 20px; display: flex; align-items: center; gap: 10px; color: #2c3e50; cursor: pointer; transition: color .2s, background .2s; user-select: none; }
.category-parent:hover, .category-parent.open, .category-parent.active { color: #c0141a; background: #fafafa; }
.category-parent-name { flex: 1; font-size: 15px; }
.category-arrow { font-size: 13px; transition: transform .25s ease; }
.category-parent.open .category-arrow { transform: rotate(90deg); }
.category-children { display: grid; grid-template-rows: 0fr; transition: grid-template-rows .3s ease; }
.category-children.open { grid-template-rows: 1fr; }
.category-children-inner { min-height: 0; overflow: hidden; }
.category-child { padding: 14px 20px 14px 32px; display: flex; align-items: center; gap: 10px; color: #606266; cursor: pointer; border-left: 3px solid transparent; transition: color .2s, background .2s; }
.category-child::before { content: ""; width: 5px; height: 5px; flex: 0 0 5px; border-radius: 50%; background: #c0c4cc; }
.category-child:hover, .category-child.active { color: #c0141a; background: #fff5f5; border-left-color: #c0141a; }
.category-child:hover::before, .category-child.active::before { background: #c0141a; }
.content-panel { min-height: 600px; padding: 30px; }
.section-heading { margin-bottom: 22px; padding-bottom: 16px; display: flex; align-items: flex-end; justify-content: space-between; border-bottom: 1px solid #ebeef5; }
.section-heading h2 { margin: 0 0 4px; color: #2c3e50; font-family: "Microsoft YaHei", serif; font-size: 24px; }
.section-heading span { color: #909399; font-size: 14px; }
.content-card { margin-bottom: 16px; padding: 20px; display: grid; grid-template-columns: 80px minmax(0,1fr) 90px; gap: 20px; align-items: center; cursor: pointer; border: 1px solid #ebeef5; border-radius: 8px; transition: transform .2s, border-color .2s, box-shadow .2s; }
.content-card:hover { transform: translateY(-2px); border-color: #f3c8c6; box-shadow: 0 8px 20px rgba(192,20,26,.08); }
.content-date { color: #909399; text-align: center; border-right: 1px solid #ebeef5; }
.content-date strong { display: block; color: #2c3e50; font-size: 25px; }
.content-card:hover .content-date strong { color: #c0141a; }
.content-title { margin-bottom: 8px; color: #303133; font-size: 17px; font-weight: 700; }
.content-summary { overflow: hidden; color: #7c6d67; font-size: 13px; line-height: 21px; text-overflow: ellipsis; white-space: nowrap; }
.content-view { color: #aa9d96; font-size: 12px; text-align: right; }
.wall-pagination { margin-top: 25px; text-align: center; }
.detail-title { margin: 8px 0 12px; color: #672025; font-size: 28px; text-align: center; }
.detail-meta { padding-bottom: 18px; color: #9c8d86; text-align: center; border-bottom: 1px solid #eee2d9; }
.detail-cover { max-width: 100%; max-height: 520px; margin: 24px auto; display: block; object-fit: contain; border-radius: 8px; }
.detail-body { width: 100%; overflow: hidden; color: #493b36; font-size: 16px; line-height: 1.9; word-break: break-word; }
.detail-body img, .detail-body video { max-width: 100% !important; height: auto !important; }
.detail-body table { max-width: 100% !important; }
.back-link { margin-bottom: 12px; }
@media (max-width: 900px) {
.spread-hero { min-height: 360px; padding-bottom: 50px; }
.spread-hero::before { height: 260px; }
.hero-slider, .hero-slider .el-carousel, .hero-slider .el-carousel__container { height: 280px; }
.spread-main { width: calc(100% - 24px); grid-template-columns: 1fr; margin-top: -20px; }
.category-panel { position: static; }
.content-panel { padding: 20px 15px; }
.content-card { grid-template-columns: 64px minmax(0,1fr); }
.content-view { display: none; }
}
</style>
<div id="app" class="spread-wall" v-cloak>
<header class="spread-hero">
<div class="spread-brand">
<span class="spread-brand-icon"><i class="el-icon-trophy"></i></span>
<span class="spread-brand-title">风采墙</span>
</div>
<div class="carousel-bg-text">HONOR</div>
<div class="hero-slider">
<el-carousel v-if="sliderItems.length" type="card" height="100%" :interval="4000"
indicator-position="none" arrow="hover" :autoplay="sliderItems.length > 1" loop>
<el-carousel-item v-for="item in sliderItems" :key="item.slideKey">
<div class="slider-item" @click="openDetail(item)">
<img v-if="item.sliderImage" :src="item.sliderImage" :alt="item.title">
<div class="slider-caption">
<div class="slider-caption-line">
<div class="slider-caption-title" :title="item.title">
<span class="slider-caption-tag">{{item.unionName || item.categoryName || '风采展示'}}</span>{{item.title}}
</div>
</div>
</div>
</div>
</el-carousel-item>
</el-carousel>
<div v-else class="slider-empty">榜样引领 · 风采绽放</div>
</div>
</header>
<div class="spread-main">
<aside class="category-panel">
<div class="category-header">工会信息传播与文化展示</div>
<div v-for="parent in categories" :key="parent.id" class="category-group">
<div class="category-parent" :class="{open: isParentOpen(parent), active: isParentActive(parent)}" @click="toggleParent(parent)">
<i class="el-icon-tickets"></i>
<span class="category-parent-name">{{parent.name}}</span>
<i v-if="parent.children && parent.children.length" class="el-icon-arrow-right category-arrow"></i>
</div>
<div v-if="parent.children && parent.children.length"
class="category-children" :class="{open: isParentOpen(parent)}">
<div class="category-children-inner">
<div v-for="child in parent.children" :key="child.id"
class="category-child" :class="{active: currentCategory.id === child.id}"
@click="selectCategory(child)">{{child.name}}</div>
</div>
</div>
</div>
</aside>
<main class="content-panel" v-loading="loading">
<template v-if="!detail">
<div class="section-heading">
<h2>{{currentCategory.name || '风采展示'}}</h2>
<span>展示教职工的先进事迹与精彩瞬间</span>
</div>
<article v-for="item in pageData.list" :key="item.id" class="content-card" @click="openDetail(item)">
<div class="content-date">
<strong>{{datePart(item.publishedAt, 'DD')}}</strong>
{{datePart(item.publishedAt, 'YYYY.MM')}}
</div>
<div>
<div class="content-title">
<el-tag v-if="item.isTop === 1" type="danger" size="mini">置顶</el-tag>
{{item.title}}
</div>
<div class="content-summary">{{item.summary || item.subTitle || '点击查看详细内容'}}</div>
</div>
<div class="content-view"><i class="el-icon-view"></i> {{item.viewCount || 0}}</div>
</article>
<el-empty v-if="!pageData.list.length && !loading" description="暂无风采内容"></el-empty>
<el-pagination v-if="pageData.totalCount > pageData.pageSize" class="wall-pagination"
background layout="prev, pager, next" :current-page.sync="pageData.pageNumber"
:page-size="pageData.pageSize" :total="pageData.totalCount"
@current-change="loadPage"></el-pagination>
</template>
<template v-else>
<el-button class="back-link" icon="el-icon-back" plain size="small" @click="backToList">返回列表</el-button>
<h1 class="detail-title">{{detail.title}}</h1>
<div class="detail-meta">
{{formatTime(detail.publishedAt || detail.createdAt)}} 
{{detail.categoryName}}  <i class="el-icon-view"></i> {{detail.viewCount || 0}}
</div>
<img v-if="getCoverUrl(detail.thumbUrl)" class="detail-cover" :src="getCoverUrl(detail.thumbUrl)" :alt="detail.title">
<div class="detail-body" v-html="detail.contentBody"></div>
</template>
</main>
</div>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
data() {
return {
apiBase: "/platform/spread/display",
loading: false,
categories: [],
sliderItems: [],
openParents: [],
currentCategory: {},
detail: null,
pageData: {pageNumber: 1, pageSize: 8, totalCount: 0, list: []}
}
},
methods: {
// 加载可见栏目并自动选择第一个末级栏目。
loadCategories() {
return this.$axios.post(this.apiBase + "/categoryTree").then(resp => {
if (resp.code !== 0) return
this.categories = resp.data || []
const firstParent = this.categories[0]
this.openParents = firstParent ? [firstParent.id] : []
this.currentCategory = firstParent && firstParent.children && firstParent.children.length
? firstParent.children[0] : (firstParent || {})
})
},
// 判断父栏目是否处于展开状态。
isParentOpen(parent) {
return this.openParents.indexOf(parent.id) !== -1
},
// 当前栏目属于该父栏目时保持父级高亮。
isParentActive(parent) {
if (this.currentCategory.id === parent.id) return true
return (parent.children || []).some(child => child.id === this.currentCategory.id)
},
// 点击父栏目时展开或关闭子节点;无子节点时直接查询该栏目。
toggleParent(parent) {
const children = parent.children || []
if (!children.length) {
this.selectCategory(parent)
return
}
const index = this.openParents.indexOf(parent.id)
if (index === -1) {
this.openParents.push(parent.id)
} else {
this.openParents.splice(index, 1)
}
},
// 加载开启轮播栏目的最新发布内容。
loadSlider() {
return this.$axios.post(this.apiBase + "/sliderData").then(resp => {
if (resp.code === 0) {
this.sliderItems = this.buildSliderItems(resp.data || [])
}
})
},
// 根据当前栏目加载已发布内容。
loadPage() {
if (!this.currentCategory.id) {
this.pageData.list = []
return
}
this.loading = true
this.$axios.post(this.apiBase + "/pageData", {
categoryId: this.currentCategory.id,
pageNumber: this.pageData.pageNumber,
pageSize: this.pageData.pageSize
}).then(resp => {
if (resp.code === 0) this.pageData = resp.data
}).finally(() => this.loading = false)
},
// 切换栏目时回到第一页和列表状态。
selectCategory(category) {
this.currentCategory = category
this.detail = null
this.pageData.pageNumber = 1
const parent = this.categories.find(item => (item.children || []).some(child => child.id === category.id))
if (parent && !this.isParentOpen(parent)) {
this.openParents.push(parent.id)
}
this.loadPage()
},
// 根据内容所属栏目同步左侧栏目选中状态,并展开对应父栏目。
syncCategorySelection(categoryId) {
if (!categoryId) return
for (const parent of this.categories) {
if (parent.id === categoryId) {
this.currentCategory = parent
return
}
const child = (parent.children || []).find(item => item.id === categoryId)
if (child) {
this.currentCategory = child
if (!this.isParentOpen(parent)) this.openParents.push(parent.id)
return
}
}
},
// 从详情返回时,按当前回显栏目重新加载内容列表。
backToList() {
this.detail = null
this.pageData.pageNumber = 1
this.loadPage()
},
// 获取完整详情并登记一次浏览。
openDetail(item) {
this.loading = true
Promise.all([
this.$axios.post(this.apiBase + "/detailData", {id: item.id}),
this.$axios.post(this.apiBase + "/addView", {id: item.id})
]).then(values => {
if (values[0].code === 0) {
this.detail = values[0].data
this.detail.viewCount = Number(this.detail.viewCount || 0) + 1
this.syncCategorySelection(this.detail.categoryId || item.categoryId)
// 轮播区域较高,打开详情后滚动到正文区域完整展示内容。
this.$nextTick(() => {
const main = document.querySelector(".spread-main")
if (main) main.scrollIntoView({behavior: "smooth", block: "start"})
})
}
}).finally(() => this.loading = false)
},
// 将上传字段转换为完整图片地址数组。
getImageUrls(value) {
if (!value) return []
let files = value
if (typeof value === "string" && value.trim().startsWith("[")) {
try {
files = JSON.parse(value)
} catch (e) {
files = []
}
}
if (!Array.isArray(files)) {
files = String(files).split(",")
}
return files.map(file => {
if (typeof file === "string") return file.trim()
return file && (file.src || file.url || file.data || (file.response && file.response.data)) || ""
}).filter(Boolean)
},
// 从上传字段中取得第一张图片地址。
getCoverUrl(value) {
const urls = this.getImageUrls(value)
return urls.length ? urls[0] : ""
},
// 将轮播图片解析为图片地址和标题,兼容历史纯地址数组。
getSliderEntries(value) {
if (!value) return []
let items = value
if (!Array.isArray(items)) {
try {
items = JSON.parse(value)
} catch (e) {
items = String(value).split(",").filter(Boolean)
}
}
if (!Array.isArray(items)) return []
return items.map(item => {
if (typeof item === "string") return {src: item}
return {
src: item.src || item.url || item.data || (item.response && item.response.data) || "",
title: item.title || ""
}
}).filter(item => item.src)
},
// 每篇内容的多张轮播图片分别生成轮播项,图片标题独立,工会名称继承内容。
buildSliderItems(contents) {
const result = []
const source = contents || []
source.forEach(item => {
let slides = this.getSliderEntries(item.sliderImages)
if (!slides.length) {
slides = this.getImageUrls(item.thumbUrl).map(url => ({src: url}))
}
slides.forEach((slide, index) => {
result.push(Object.assign({}, item, {
title: slide.title || item.title,
sliderImage: slide.src,
slideKey: item.id + "-" + index
}))
})
})
return result
},
// 格式化列表日期的指定部分。
datePart(value, format) {
return value ? this.$moment(Number(value)).format(format) : "--"
},
// 格式化详情发布时间。
formatTime(value) {
return value ? this.$moment(Number(value)).format("YYYY-MM-DD HH:mm") : ""
}
},
created() {
Promise.all([this.loadCategories(), this.loadSlider()]).then(() => this.loadPage())
}
})
</script>
<!--#
}
#-->