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,145 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style>
body { background: #f5f7fa; }
.spread-detail-h5 { min-height: 100vh; color: #202124; background: #f5f7fa; }
.spread-h5__detail { margin: 10px 12px 24px; padding: 18px 15px 24px; border-radius: 10px; background: #fff; }
.spread-h5__detail-title { margin: 0; color: #202124; font-size: 20px; line-height: 29px; text-align: center; word-break: break-word; }
.spread-h5__detail-meta { margin-top: 10px; padding-bottom: 14px; color: #9aa2af; font-size: 11px; line-height: 17px; text-align: center; border-bottom: 1px solid #edf1f6; }
.spread-h5__detail-summary { margin: 15px 0 0; padding: 10px 12px; color: #697386; font-size: 14px; line-height: 22px; border-radius: 6px; background: #f7f9fc; }
.spread-h5__detail-cover { width: 100%; max-height: 240px; margin: 16px 0 0; display: block; border-radius: 6px; object-fit: contain; }
.spread-h5__slider { height: 220px; margin-top: 16px; overflow: hidden; border-radius: 6px; background: #eef2f7; }
.spread-h5__slide { position: relative; height: 220px; }
.spread-h5__slide img { width: 100%; height: 100%; display: block; object-fit: contain; }
.spread-h5__slide-caption { position: absolute; left: 0; right: 0; bottom: 0; padding: 24px 12px 10px; overflow: hidden; color: #fff; font-size: 12px; line-height: 18px; text-overflow: ellipsis; white-space: nowrap; background: linear-gradient(to top, rgba(15,23,42,.72), transparent); box-sizing: border-box; }
.spread-h5__detail-body { margin-top: 16px; overflow: hidden; color: #323233; font-size: 15px; line-height: 1.8; word-break: break-word; }
.spread-h5__detail-body img, .spread-h5__detail-body video { max-width: 100% !important; height: auto !important; }
.spread-h5__detail-body table { max-width: 100% !important; }
.spread-h5__detail--skeleton .van-skeleton { padding: 0; }
.spread-h5__detail-skeleton-cover { margin-top: 18px; }
.spread-h5__detail-skeleton-cover .van-skeleton__row { height: 180px; border-radius: 6px; }
.spread-h5__detail-skeleton-body { margin-top: 18px; }
.spread-h5__detail-empty { padding: 80px 20px; color: #9aa2af; font-size: 14px; text-align: center; }
</style>
<div id="app" class="spread-detail-h5" v-cloak>
<van-nav-bar title="动态详情" left-arrow left-text="返回" fixed placeholder :z-index="20" @click-left="handleBack"></van-nav-bar>
<article v-if="detailLoading" class="spread-h5__detail spread-h5__detail--skeleton">
<van-skeleton title :row="2"></van-skeleton>
<van-skeleton class="spread-h5__detail-skeleton-cover" :row="1"></van-skeleton>
<van-skeleton class="spread-h5__detail-skeleton-body" title :row="6"></van-skeleton>
</article>
<article v-else-if="detail" class="spread-h5__detail">
<h1 class="spread-h5__detail-title">{{detail.title}}</h1>
<div class="spread-h5__detail-meta">
{{formatDate(detail.publishedAt || detail.createdAt)}} {{detail.categoryName}} 
<van-icon name="eye-o"></van-icon> {{detail.viewCount || 0}}
</div>
<img v-if="getCoverUrl(detail.thumbUrl)" class="spread-h5__detail-cover" :src="getCoverUrl(detail.thumbUrl)" :alt="detail.title">
<div v-if="detail.summary" class="spread-h5__detail-summary">{{detail.summary}}</div>
<van-swipe v-if="detailSlides.length"
class="spread-h5__slider"
:autoplay="3000"
:show-indicators="detailSlides.length > 1"
indicator-color="#1989fa">
<van-swipe-item v-for="(slide, index) in detailSlides" :key="slide.src + '-' + index">
<div class="spread-h5__slide">
<img :src="slide.src" :alt="slide.title || detail.title">
<div v-if="slide.title" class="spread-h5__slide-caption">{{slide.title}}</div>
</div>
</van-swipe-item>
</van-swipe>
<div class="spread-h5__detail-body" v-html="detail.contentBody"></div>
</article>
<div v-else class="spread-h5__detail-empty">动态不存在或已下架</div>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
data() {
return {
apiBase: "/platform/h5/spread",
detailLoading: true,
detail: null
}
},
computed: {
detailSlides() {
return this.getSliderEntries(this.detail && this.detail.sliderImages)
}
},
methods: {
loadDetail() {
const id = new URLSearchParams(window.location.search).get("id")
if (!id) {
this.detailLoading = false
return
}
this.detailLoading = true
Promise.all([
this.$axios.post(this.apiBase + "/detailData", {id: id}),
this.$axios.post(this.apiBase + "/addView", {id: id})
]).then(values => {
if (values[0].code === 0) {
this.detail = values[0].data
this.detail.viewCount = Number(this.detail.viewCount || 0) + 1
}
}).finally(() => {
this.detailLoading = false
})
},
handleBack() {
const source = new URLSearchParams(window.location.search).get("from")
this.$pjaxReplace(source === "home" ? "/platform/h5/home" : "/platform/h5/spread")
},
getCoverUrl(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(",")
const first = files[0]
if (typeof first === "string") return first.trim()
return first && (first.src || first.url || first.data || (first.response && first.response.data)) || ""
},
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.trim(), title: ""}
return {
src: item.src || item.url || item.data || (item.response && item.response.data) || "",
title: item.title || item.description || ""
}
}).filter(item => item.src)
},
formatDate(value) {
return value ? this.$moment(Number(value)).format("YYYY-MM-DD") : ""
}
},
created() {
this.loadDetail()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,296 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style>
body { background: #f5f7fa; }
.spread-h5 { min-height: 100vh; color: #202124; background: #f5f7fa; }
.spread-h5__toolbar { position: sticky; top: 46px; z-index: 15; background: #fff; box-shadow: 0 3px 10px rgba(31,72,122,.06); }
.spread-h5__search { padding: 8px 12px 2px; background: #fff; }
.spread-h5__search .van-search { padding: 0; }
.spread-h5__categories { padding: 2px 12px 8px; background: #fff; }
.spread-h5__tabs-nav { display: flex; align-items: center; border-bottom: 1px solid #edf1f6; }
.spread-h5__tabs { height: 36px; padding: 0; display: flex; gap: 0; overflow-x: auto; overflow-y: hidden; border-bottom: 1px solid #edf1f6; border-radius: 0; background: transparent; box-sizing: border-box; scrollbar-width: none; -webkit-overflow-scrolling: touch; }
.spread-h5__tabs-nav .spread-h5__tabs { min-width: 0; flex: 1; border-bottom: 0; }
.spread-h5__tabs-arrow { width: 26px; height: 30px; padding: 0; flex-shrink: 0; display: flex; align-items: center; justify-content: center; color: #8d98a8; font-size: 14px; line-height: 1; border: 0; background: transparent; }
.spread-h5__tabs-arrow .van-icon { display: block; line-height: 1; }
.spread-h5__tabs-arrow:active { color: #087af4; background: #f3f8ff; }
.spread-h5__tabs::-webkit-scrollbar { display: none; }
.spread-h5__tab { position: relative; min-width: 92px; height: 30px; padding: 0 8px; flex: 1 0 calc((100% - 8px) / 3); display: flex; align-items: center; justify-content: center; color: #7d8797; font-size: 13px; white-space: nowrap; border-radius: 6px; box-sizing: border-box; }
.spread-h5__tab--active { color: #087af4; font-weight: 600; background: transparent; box-shadow: none; }
.spread-h5__tabs:not(.spread-h5__tabs--secondary) .spread-h5__tab--active::after { content: ""; position: absolute; left: 50%; bottom: -1px; width: 26px; height: 2px; border-radius: 2px; background: #087af4; transform: translateX(-50%); }
.spread-h5__filters { height: 42px; margin-top: 8px; display: flex; overflow: hidden; border-radius: 10px; box-sizing: border-box; }
.spread-h5__filter-menu { min-width: 0; flex: 1; background: transparent; }
.spread-h5__filter-menu .van-dropdown-menu__bar { height: 40px; background: transparent; box-shadow: none; }
.spread-h5__filter-menu .van-dropdown-menu__item { position: relative; min-width: 0; }
.spread-h5__filter-menu .van-dropdown-menu__title { max-width: 100%; padding: 0 16px 0 8px; color: #5f6978; font-size: 12px; line-height: 28px; box-sizing: border-box; }
.spread-h5__filter-menu .van-dropdown-menu__title::after { right: 6px; border-color: transparent transparent #8d98a8 #8d98a8; }
.spread-h5__filter-menu .van-dropdown-menu__title--active::after { border-color: transparent transparent currentColor currentColor; }
.spread-h5__list { margin: 10px 12px 20px; padding: 0 12px; border-radius: 10px; background: #fff; }
.spread-h5__item { min-height: 82px; padding: 11px 0; display: flex; align-items: center; box-sizing: border-box; }
.spread-h5__item + .spread-h5__item { border-top: 1px solid #f0f2f5; }
.spread-h5__cover { width: 96px; height: 60px; margin-right: 11px; flex-shrink: 0; border-radius: 5px; object-fit: cover; }
.spread-h5__item-content { min-width: 0; flex: 1; }
.spread-h5__item-title { overflow: hidden; color: #202124; font-size: 14px; line-height: 20px; font-weight: 600; text-overflow: ellipsis; white-space: nowrap; }
.spread-h5__item-summary { margin-top: 4px; overflow: hidden; color: #7d8797; font-size: 11px; line-height: 16px; text-overflow: ellipsis; white-space: nowrap; }
.spread-h5__item-meta { margin-top: 6px; display: flex; align-items: center; justify-content: space-between; color: #9aa2af; font-size: 10px; line-height: 14px; }
.spread-h5__views { display: inline-flex; align-items: center; }
.spread-h5__views .van-icon { margin-right: 3px; }
.spread-h5__skeleton-list { margin: 10px 12px 20px; padding: 0 12px; border-radius: 10px; background: #fff; }
.spread-h5__skeleton-item { min-height: 82px; padding: 11px 0; box-sizing: border-box; }
.spread-h5__skeleton-item + .spread-h5__skeleton-item { border-top: 1px solid #f0f2f5; }
.spread-h5__skeleton-item .van-skeleton { padding: 0; }
.spread-h5__skeleton-item .van-skeleton__avatar { width: 96px; height: 60px; margin-right: 11px; border-radius: 5px; }
.spread-h5__loading { padding: 35px 0; text-align: center; }
.spread-h5__finished { padding: 10px 0 18px; color: #b0b6c0; font-size: 11px; line-height: 16px; text-align: center; }
.spread-h5__empty { min-height: 250px; padding: 28px 18px 36px; display: flex; align-items: center; justify-content: center; flex-direction: column; box-sizing: border-box; text-align: center; }
.spread-h5__empty-image { display: block; width: 82%; max-width: 280px; height: auto; object-fit: contain; }
.spread-h5__empty-title { margin-top: 8px; color: #50627a; font-size: 15px; line-height: 22px; font-weight: 500; }
.spread-h5__empty-hint { margin-top: 5px; color: #9aa7b8; font-size: 12px; line-height: 18px; }
</style>
<div id="app" class="spread-h5" v-cloak>
<van-nav-bar title="工会动态" left-arrow left-text="返回" fixed placeholder :z-index="20" @click-left="handleBack"></van-nav-bar>
<template>
<div class="spread-h5__toolbar">
<div class="spread-h5__search">
<van-search v-model.trim="keyword"
shape="round"
placeholder="搜索动态标题"
@search="doSearch"
@clear="doSearch"></van-search>
</div>
<div class="spread-h5__categories" v-if="categories.length">
<div class="spread-h5__tabs-nav">
<button type="button" class="spread-h5__tabs-arrow" @click="scrollTabs(-1)">
<van-icon name="arrow-left"></van-icon>
</button>
<div ref="spreadTabs" class="spread-h5__tabs">
<div class="spread-h5__tab"
v-for="category in categories"
:key="category.id"
:class="{'spread-h5__tab--active': parentId === category.id}"
@click="selectParent(category)">{{category.name}}</div>
</div>
<button type="button" class="spread-h5__tabs-arrow" @click="scrollTabs(1)">
<van-icon name="arrow"></van-icon>
</button>
</div>
<div class="spread-h5__filters">
<van-dropdown-menu class="spread-h5__filter-menu" active-color="#087af4">
<van-dropdown-item v-model="categoryId"
:options="typeOptions"
@change="handleFilterChange"></van-dropdown-item>
<van-dropdown-item v-model="sortType"
:options="sortOptions"
@change="handleFilterChange"></van-dropdown-item>
<van-dropdown-item v-model="publishDays"
:options="publishTimeOptions"
@change="handleFilterChange"></van-dropdown-item>
</van-dropdown-menu>
</div>
</div>
</div>
<div v-if="listSkeletonLoading" class="spread-h5__skeleton-list">
<div class="spread-h5__skeleton-item" v-for="index in 4" :key="'spread-list-skeleton-' + index">
<van-skeleton title avatar :row="2"></van-skeleton>
</div>
</div>
<div v-if="!listSkeletonLoading && pageData.list.length" class="spread-h5__list">
<div class="spread-h5__item" v-for="item in pageData.list" :key="item.id" @click="enterDetail(item)">
<img v-if="getCoverUrl(item.thumbUrl)" class="spread-h5__cover" :src="getCoverUrl(item.thumbUrl)" :alt="item.title">
<div class="spread-h5__item-content">
<div class="spread-h5__item-title">{{item.title}}</div>
<div v-if="item.summary || item.subTitle" class="spread-h5__item-summary">{{item.summary || item.subTitle}}</div>
<div class="spread-h5__item-meta">
<span>{{formatDate(item.publishedAt || item.createdAt)}}</span>
<span class="spread-h5__views"><van-icon name="eye-o"></van-icon>{{item.viewCount || 0}}</span>
</div>
</div>
</div>
</div>
<div v-if="loading && !listSkeletonLoading" class="spread-h5__loading"><van-loading size="22px">加载中...</van-loading></div>
<div v-if="!loading && finished && pageData.list.length" class="spread-h5__finished">没有更多了</div>
<div v-if="!loading && finished && !pageData.list.length" class="spread-h5__empty">
<img class="spread-h5__empty-image"
src="/assets/mobile/img/home/v4/union-news-empty-v4.png"
alt="暂无工会动态">
<div class="spread-h5__empty-title">暂无工会动态</div>
<div class="spread-h5__empty-hint">当前筛选条件下暂无可展示的动态</div>
</div>
</template>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
data() {
return {
apiBase: "/platform/h5/spread",
categories: [],
parentId: "",
categoryId: "",
keyword: "",
sortType: "comprehensive",
publishDays: 0,
sortOptions: [
{text: "综合排序", value: "comprehensive"},
{text: "最新发布", value: "latest"},
{text: "浏览最多", value: "views"}
],
publishTimeOptions: [
{text: "发布时间", value: 0},
{text: "近7天", value: 7},
{text: "近30天", value: 30},
{text: "近半年", value: 180},
{text: "近一年", value: 365}
],
loading: false,
listSkeletonLoading: true,
finished: false,
scrollContainer: null,
requestSeq: 0,
pageData: {pageNumber: 1, pageSize: 10, totalCount: 0, list: []}
}
},
computed: {
currentChildren() {
const parent = this.categories.find(item => item.id === this.parentId)
return parent && parent.children ? parent.children : []
},
typeOptions() {
return [{text: "全部类型", value: this.parentId}].concat(
this.currentChildren.map(item => ({text: item.name, value: item.id}))
)
}
},
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]
if (!firstParent) return
this.parentId = firstParent.id
this.categoryId = firstParent.id
})
},
loadPage() {
if (!this.categoryId) {
this.listSkeletonLoading = false
return
}
if (this.loading || this.finished) {
return
}
const showSkeleton = this.pageData.pageNumber === 1 && !this.pageData.list.length
if (showSkeleton) this.listSkeletonLoading = true
this.loading = true
const requestSeq = ++this.requestSeq
this.$axios.post(this.apiBase + "/pageData", {
categoryId: this.categoryId,
title: this.keyword,
sortType: this.sortType,
publishDays: this.publishDays,
pageNumber: this.pageData.pageNumber,
pageSize: this.pageData.pageSize
}).then(resp => {
if (requestSeq !== this.requestSeq) return
if (resp.code === 0) {
const data = resp.data || {}
const rows = data.list || []
this.pageData.list = this.pageData.list.concat(rows)
this.pageData.totalCount = data.totalCount || 0
this.pageData.pageNumber += 1
this.finished = this.pageData.list.length >= this.pageData.totalCount || rows.length < this.pageData.pageSize
} else {
this.finished = true
}
}).finally(() => {
if (requestSeq === this.requestSeq) {
this.loading = false
if (showSkeleton) this.listSkeletonLoading = false
}
})
},
resetPage() {
this.requestSeq += 1
this.loading = false
this.listSkeletonLoading = true
this.finished = false
this.pageData = {pageNumber: 1, pageSize: 10, totalCount: 0, list: []}
this.$nextTick(() => this.loadPage())
},
handlePageScroll() {
if (!this.scrollContainer || this.loading || this.finished) return
const remaining = this.scrollContainer.scrollHeight
- this.scrollContainer.scrollTop
- this.scrollContainer.clientHeight
if (remaining <= 80) this.loadPage()
},
doSearch() {
this.resetPage()
},
scrollTabs(direction) {
const tabs = this.$refs.spreadTabs
if (!tabs) return
tabs.scrollTo({
left: tabs.scrollLeft + direction * tabs.clientWidth * 0.75,
behavior: "smooth"
})
},
selectParent(category) {
if (this.parentId === category.id) return
this.parentId = category.id
this.categoryId = category.id
this.resetPage()
},
handleFilterChange() {
this.resetPage()
},
enterDetail(item) {
this.$pjaxReplace(this.apiBase + "/detail?id=" + encodeURIComponent(item.id) + "&from=list")
},
handleBack() {
this.$pjaxReplace("/platform/h5/home")
},
getCoverUrl(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(",")
const first = files[0]
if (typeof first === "string") return first.trim()
return first && (first.src || first.url || first.data || (first.response && first.response.data)) || ""
},
formatDate(value) {
return value ? this.$moment(Number(value)).format("YYYY-MM-DD") : ""
}
},
created() {
this.loadCategories().then(() => this.resetPage())
},
mounted() {
this.scrollContainer = document.querySelector("body > .main-page .page-wrapper")
if (this.scrollContainer) {
this.scrollContainer.addEventListener("scroll", this.handlePageScroll, {passive: true})
}
},
beforeDestroy() {
if (this.scrollContainer) {
this.scrollContainer.removeEventListener("scroll", this.handlePageScroll)
}
}
})
</script>
<!--#
}
#-->