change
This commit is contained in:
@@ -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>
|
||||
<!--#
|
||||
}
|
||||
#-->
|
||||
Reference in New Issue
Block a user