feat: 同步长江大学的风采墙优化

This commit is contained in:
2026-10-21 15:23:13 +08:00
parent 88d624172c
commit b19996f57e
15 changed files with 2383 additions and 756 deletions
@@ -5,20 +5,20 @@ import org.nutz.lang.util.NutMap;
import java.util.List;
/**
* 首页工会网站新闻服务。
* 首页风采墙新闻服务。
*/
public interface SysHomeNewsService {
/**
* 获取学校工会网站首页新闻。
* 获取风采墙可见栏目下的已发布新闻。
*
* @return 新闻栏目集合;每个栏目包含 label 栏目名称和 value 新闻列表,
* 新闻项包含 href 跳转地址、time 发布时间、text 新闻标题和 thumbUrl 缩略图绝对地址(无图时为空)
* 新闻项包含 href 跳转地址、time 发布时间、text 新闻标题和 thumbUrl 缩略图地址(无图时为空)
*/
List<NutMap> getWebsiteNews();
/**
* href 必须是工会首页返回的新闻链接;返回正文首图的绝对地址,无图或不可访问时为空字符串。
* href 必须是首页返回的风采墙新闻链接;返回封面或正文首图地址,无图时为空字符串。
*/
String getNewsImage(String href);
}
@@ -1,159 +1,108 @@
package com.budwk.app.sys.services.impl;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.date.DateUtil;
import com.budwk.app.sys.services.SysHomeNewsService;
import com.budwk.app.zhgh.spread.models.SpreadCategory;
import com.budwk.app.zhgh.spread.models.SpreadContent;
import com.budwk.app.zhgh.spread.service.SpreadCategoryService;
import com.budwk.app.zhgh.spread.service.SpreadContentService;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import org.nutz.integration.jedis.RedisService;
import org.nutz.dao.Cnd;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.json.Json;
import org.nutz.lang.util.NutMap;
import java.io.IOException;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.UUID;
import java.util.Map;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* 首页工会网站新闻服务实现,统一为PC端和移动端提供同一份新闻数据。
* 首页风采墙新闻服务实现,沿用首页新闻数据结构。
*/
@IocBean
public class SysHomeNewsServiceImpl implements SysHomeNewsService {
private static final String WEBSITE_URL = "https://xgh.cug.edu.cn/";
private static final String NEWS_CACHE_KEY = "gonghui_news";
private static final int NEWS_CACHE_SECONDS = 60 * 60 * 24;
@Inject
private SpreadCategoryService categoryService;
@Inject
private RedisService redisService;
private SpreadContentService contentService;
/**
* 抓取学校工会网站首页的工会要闻、基层动态等栏目。
* 外部网站暂时不可用时优先返回最近一次缓存,避免PC端和移动端同时显示为空。
*
* @return 新闻栏目集合;栏目 value 中的新闻项包含 href、time、text
* 按风采墙顶级栏目返回已发布内容,保留首页现有新闻数据结构。
*/
@Override
public List<NutMap> getWebsiteNews() {
try {
Document document = Jsoup.connect(WEBSITE_URL).timeout(10000).get();
List<NutMap> news = parseWebsiteNews(document.select(".gh,.jc,.fc,.jg"));
if (!news.isEmpty()) {
redisService.setex(NEWS_CACHE_KEY, NEWS_CACHE_SECONDS, Json.toJson(news));
return news;
}
return getCachedNews();
} catch (IOException e) {
List<NutMap> cachedNews = getCachedNews();
if (!cachedNews.isEmpty()) {
return cachedNews;
}
throw new RuntimeException("学校工会网站新闻获取失败", e);
}
}
/**
* 将外部网站栏目节点转换为前端统一使用的栏目和新闻结构。
*/
private List<NutMap> parseWebsiteNews(Elements homeElements) {
List<SpreadCategory> categories = categoryService.listCategories(true);
List<NutMap> sections = new ArrayList<>();
for (Element homeElement : homeElements) {
Element titleElement = homeElement.selectFirst(".title");
if (titleElement == null || StrUtil.isBlank(titleElement.text())) {
continue;
for (SpreadCategory category : categories) {
if (!"0".equals(category.getParentId())) continue;
List<String> categoryIds = new ArrayList<>();
categoryIds.add(category.getId());
for (SpreadCategory child : categories) {
if (category.getId().equals(child.getParentId())) categoryIds.add(child.getId());
}
List<SpreadContent> contents = contentService.query(Cnd.where("delFlag", "=", false)
.and("isPublished", "=", 1).and("categoryId", "in", categoryIds)
.desc("isTop").desc("publishedAt").asc("sortOrder").desc("createdAt"));
List<NutMap> rows = new ArrayList<>();
for (Element itemElement : homeElement.select("li")) {
Element linkElement = itemElement.selectFirst("a");
if (linkElement == null || StrUtil.isBlank(linkElement.attr("href"))) {
continue;
}
Element timeElement = itemElement.selectFirst("span");
// 仅使用当前新闻条目的缩略图,按网页基址补全相对路径。
Element imageElement = itemElement.selectFirst("img");
String thumbUrl = imageElement == null ? "" : imageElement.absUrl("src");
if (!thumbUrl.startsWith("https://") && !thumbUrl.startsWith("http://")) {
thumbUrl = "";
}
String title = StrUtil.blankToDefault(linkElement.attr("title"), linkElement.text());
for (SpreadContent content : contents) {
Long publishedAt = content.getPublishedAt();
rows.add(NutMap.NEW()
.addv("href", linkElement.attr("href"))
.addv("time", timeElement == null ? "" : timeElement.text())
.addv("text", title)
.addv("thumbUrl", thumbUrl));
.addv("href", "/platform/spread/display?id=" + content.getId())
.addv("time", publishedAt == null ? "" : DateUtil.format(new java.util.Date(publishedAt), "yyyy-MM-dd"))
.addv("text", content.getTitle())
.addv("thumbUrl", getContentImage(content)));
}
sections.add(NutMap.NEW().addv("label", titleElement.text()).addv("value", rows));
sections.add(NutMap.NEW().addv("label", category.getName()).addv("value", rows));
}
return sections;
}
/**
* 读取最近一次成功抓取的数据;缓存为空或内容异常时返回空集合。
*/
private List<NutMap> getCachedNews() {
String cached = redisService.get(NEWS_CACHE_KEY);
if (StrUtil.isBlank(cached)) {
return Collections.emptyList();
private String getContentImage(SpreadContent content) {
String image = "";
if (StrUtil.isNotBlank(content.getThumbUrl())) {
Object value = content.getThumbUrl().trim();
if (value.toString().startsWith("[") || value.toString().startsWith("{")) {
try {
value = Json.fromJson(value.toString());
} catch (RuntimeException e) {
value = "";
}
}
if (value instanceof List) {
List<?> files = (List<?>) value;
value = files.isEmpty() ? "" : files.get(0);
}
if (value instanceof Map) {
Map<?, ?> file = (Map<?, ?>) value;
value = file.get("src") != null ? file.get("src") : file.get("url");
}
image = value == null ? "" : value.toString().split(",", 2)[0].trim();
}
try {
return Json.fromJsonAsList(NutMap.class, cached);
} catch (RuntimeException e) {
return Collections.emptyList();
if (StrUtil.isBlank(image) && StrUtil.isNotBlank(content.getContentBody())) {
Element first = Jsoup.parse(content.getContentBody()).selectFirst("img[src]");
if (first != null) image = first.attr("src");
}
return image.startsWith("/") && !image.startsWith("//")
|| image.startsWith("https://") || image.startsWith("http://") ? image : "";
}
/**
* 仅补取缓存新闻列表中已存在的公开文章,禁止任意地址抓取和自动重定向。
* 正文首图独立缓存;无图短暂缓存,防止每次栏目切换重复访问外站。
* 仅返回首页已发布风采内容的图片,不再请求外部新闻网站。
*/
@Override
public String getNewsImage(String href) {
if (StrUtil.isBlank(href) || href.length() > 2048) return "";
boolean listed = false;
for (NutMap section : getCachedNews()) {
Object values = section.get("value");
if (!(values instanceof List)) continue;
for (Object value : (List<?>) values) {
if (value instanceof Map && href.equals(((Map<?, ?>) value).get("href"))) {
listed = true;
break;
}
if (StrUtil.isBlank(href)) return "";
for (NutMap section : getWebsiteNews()) {
for (Object value : (List<?>) section.get("value")) {
NutMap news = (NutMap) value;
if (href.equals(news.getString("href"))) return news.getString("thumbUrl");
}
}
if (!listed) return "";
try {
URI uri = URI.create(WEBSITE_URL).resolve(href);
String host = uri.getHost();
// 来源限制为学校公开网站和微信公众号,端口及凭据均不接受自定义。
if (host == null || !(host.equals("cug.edu.cn") || host.endsWith(".cug.edu.cn")
|| host.equals("mp.weixin.qq.com")) || uri.getUserInfo() != null
|| uri.getPort() != -1 || !("https".equals(uri.getScheme()) || "http".equals(uri.getScheme()))) return "";
String cacheKey = "gonghui_news_image:" + UUID.nameUUIDFromBytes(uri.toString().getBytes(StandardCharsets.UTF_8));
String cached = redisService.get(cacheKey);
if (cached != null) return "-".equals(cached) ? "" : cached;
String image = "";
try {
Document article = Jsoup.connect(uri.toString()).timeout(5000).maxBodySize(2 * 1024 * 1024)
.followRedirects(false).get();
Element first = article.selectFirst(".v_news_content img, #js_content img");
if (first != null) {
image = first.hasAttr("data-src") ? first.absUrl("data-src") : first.absUrl("src");
}
if (!image.startsWith("https://") && !image.startsWith("http://")) image = "";
} catch (IOException ignored) {
// 外站失败不影响首页新闻文字和栏目切换,五分钟后允许再次补取。
}
redisService.setex(cacheKey, image.isEmpty() ? 300 : NEWS_CACHE_SECONDS, image.isEmpty() ? "-" : image);
return image;
} catch (IllegalArgumentException e) {
return "";
}
return "";
}
}
@@ -44,6 +44,15 @@ public class SpreadDisplayController {
return Result.success(contentService.pagePublished(pageForm, categoryId));
}
@At
@ApiOperation("热门内容分页")
@SaCheckPermission("spread.display")
public Result hotData(PageForm pageForm) {
pageForm.setPageNumber(pageForm.getPageNumber() == null ? 1 : Math.max(1, pageForm.getPageNumber()));
pageForm.setPageSize(10);
return Result.success(contentService.pagePublished(pageForm, null, null, "views", null, null));
}
@At
@ApiOperation("内容详情")
@SaCheckPermission("spread.display")
@@ -97,6 +97,19 @@ public class SpreadContentServiceImpl extends BaseServiceImpl<SpreadContent> imp
query.setTitle(title);
query.setIsPublished(1);
Cnd cnd = buildCondition(query);
List<SpreadCategory> visibleCategories = dao().query(SpreadCategory.class,
Cnd.where("delFlag", "=", false).and("visible", "=", 1));
List<String> visibleIds = new ArrayList<>();
for (SpreadCategory category : visibleCategories) {
if ("0".equals(category.getParentId()) || visibleCategories.stream()
.anyMatch(parent -> parent.getId().equals(category.getParentId()) && "0".equals(parent.getParentId()))) {
visibleIds.add(category.getId());
}
}
if (visibleIds.isEmpty()) {
return new Pagination<>(pageForm.getPageNumber(), pageForm.getPageSize(), 0, Collections.emptyList());
}
cnd.and("c.categoryId", "in", visibleIds);
if (publishDays != null && publishDays > 0) {
cnd.and("c.publishedAt", ">=", System.currentTimeMillis() - publishDays * 24L * 60L * 60L * 1000L);
}
@@ -139,7 +152,7 @@ public class SpreadContentServiceImpl extends BaseServiceImpl<SpreadContent> imp
return "c.publishedAt DESC, c.createdAt DESC";
}
if ("views".equals(sortType)) {
return "c.viewCount DESC, c.publishedAt DESC, c.createdAt DESC";
return "c.viewCount DESC, c.publishedAt DESC, c.id DESC";
}
return "c.isTop DESC, c.publishedAt DESC, c.sortOrder ASC, c.createdAt DESC";
}
@@ -222,6 +235,8 @@ public class SpreadContentServiceImpl extends BaseServiceImpl<SpreadContent> imp
LEFT JOIN spread_category parent ON parent.id = child.parentId
WHERE c.delFlag = 0
AND c.isPublished = 1
AND c.thumbUrl IS NOT NULL
AND TRIM(c.thumbUrl) <> ''
AND child.delFlag = 0
AND child.visible = 1
AND child.slider = 1
@@ -273,7 +288,7 @@ public class SpreadContentServiceImpl extends BaseServiceImpl<SpreadContent> imp
}
/**
* 校验并规范每张轮播图的标题,轮播图统一继承内容所属工会。
* 校验每张轮播图的标题和所属工会,兼容未单独设置工会的历史图片。
*/
private void validateSliderImages(SpreadContent content) {
if (StrUtil.isBlank(content.getSliderImages())) {
@@ -301,6 +316,10 @@ public class SpreadContentServiceImpl extends BaseServiceImpl<SpreadContent> imp
JSONObject normalizedItem = new JSONObject();
normalizedItem.set("src", item.getStr("src"));
normalizedItem.set("title", item.getStr("title").trim());
Sys_union imageUnion = requireSelectableUnion(StrUtil.isBlank(item.getStr("unionId"))
? content.getUnionId() : item.getStr("unionId"));
normalizedItem.set("unionId", imageUnion.getId());
normalizedItem.set("unionName", imageUnion.getName());
normalizedArray.add(normalizedItem);
}
content.setSliderImages(JSONUtil.toJsonStr(normalizedArray));
+3 -2
View File
@@ -9,10 +9,11 @@
contextPath: ""
staticPathLocal: ''
redis:
host: 127.0.0.1
host: 175.27.165.74
port: 6379
database: 1
database: 4
mode: normal
password: ddss@2025
jdbc:
url: jdbc:mysql://127.0.0.1:10086/zhgh_zjnu?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
username: root
Binary file not shown.

After

Width:  |  Height:  |  Size: 714 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 249 KiB

@@ -0,0 +1,152 @@
Vue.component("spread-back-button", {
template: `<div
class="float-btn"
:class="{ 'is-dragging': dragging }"
:style="{ left: x + 'px', top: y + 'px' }"
@mousedown="startDrag"
@touchstart="startDrag"
@click="handleClick"
>
<!-- 外发光 -->
<div class="glow-ring"></div>
<!-- 点击涟漪 -->
<span
v-for="r in ripples"
:key="r.id"
class="ripple"
:style="{ left: r.x + 'px', top: r.y + 'px' }"
></span>
<!-- 图标 -->
<div class="icon-wrap">
<van-icon name="share-o" size="28" class="flip-icon" ></van-icon>
</div>
</div>`,
props: {
path: {
type: String,
default: '/platform/h5/home'
}
},
data() {
return {
x: 20,
y: 200,
dragging: false,
offsetX: 0,
offsetY: 0,
startX: 0,
startY: 0,
moved: false,
ripples: [],
rippleId: 0
}
},
mounted() {
const pos = localStorage.getItem("float-btn-pos")
if (pos) {
const { x, y } = JSON.parse(pos)
this.x = x
this.y = y
}
window.addEventListener("mousemove", this.onDrag)
window.addEventListener("mouseup", this.endDrag)
window.addEventListener("touchmove", this.onDrag, { passive: false })
window.addEventListener("touchend", this.endDrag)
},
beforeDestroy() {
window.removeEventListener("mousemove", this.onDrag)
window.removeEventListener("mouseup", this.endDrag)
window.removeEventListener("touchmove", this.onDrag)
window.removeEventListener("touchend", this.endDrag)
},
methods: {
startDrag(e) {
const point = e.touches ? e.touches[0] : e
this.dragging = true
this.moved = false
this.startX = point.clientX
this.startY = point.clientY
this.offsetX = point.clientX - this.x
this.offsetY = point.clientY - this.y
},
onDrag(e) {
if (!this.dragging) return
if (e.cancelable) e.preventDefault()
const point = e.touches ? e.touches[0] : e
let newX = point.clientX - this.offsetX
let newY = point.clientY - this.offsetY
const btnSize = 54
const maxX = window.innerWidth - btnSize
const maxY = window.innerHeight - btnSize
this.x = Math.max(0, Math.min(newX, maxX))
this.y = Math.max(0, Math.min(newY, maxY))
const dx = Math.abs(point.clientX - this.startX)
const dy = Math.abs(point.clientY - this.startY)
if (dx > 5 || dy > 5) {
this.moved = true
}
},
endDrag() {
if (!this.dragging) return
this.dragging = false
localStorage.setItem(
"float-btn-pos",
JSON.stringify({
x: this.x,
y: this.y
})
)
},
handleClick(e) {
if (this.moved) return
const point = e.changedTouches ? e.changedTouches[0] : e
const rect = this.$el.getBoundingClientRect()
const id = this.rippleId++
this.ripples.push({
id,
x: point.clientX - rect.left,
y: point.clientY - rect.top
})
setTimeout(() => {
this.ripples = this.ripples.filter(r => r.id !== id)
}, 600)
if (this.path === '-1') {
window.history.back()
} else {
this.$pjaxReplace(this.path)
}
}
}
})
@@ -60,7 +60,7 @@ const jcdt = {
this.pageNumber = 1
},
onLink(item) {
const url = this.isHttpOrHttps(item.href) ? item.href : "https://xgh.cug.edu.cn/" + item.href
const url = item.href
window.open(url)
},
isHttpOrHttps(url) {
@@ -80,7 +80,7 @@ layout("/layouts/platform.html"){
upload_mode="image" upload_result_category="interval"></file-upload>
<div class="form-tip">建议使用横版图片,单张不超过 10MB。</div>
</el-form-item>
<el-form-item v-if="sliderEnabled" label="轮播图片">
<el-form-item label="详情轮播图">
<div class="slider-upload-head">
<span class="slider-upload-title">点击下方“+”新增轮播图片</span>
<span class="slider-upload-count">已添加 {{sliderMeta.length}} / 10 张</span>
@@ -88,7 +88,7 @@ layout("/layouts/platform.html"){
<file-upload :upload_number="10" :upload_size="10 * 1024 * 1024"
:value.sync="sliderFiles" accept=".jpg,.jpeg,.png,.webp"
upload_mode="image" upload_result_category="array"></file-upload>
<div class="form-tip">仅栏目开启“参与轮播”后生效,最多上传 10 张;每张图片只需填写标题,工会名称沿用内容所属工会。</div>
<div class="form-tip">用于内容详情页轮播,最多上传 10 张;请填写每张图片的标题和所属工会。首页轮播使用内容封面图。</div>
<div v-if="sliderMeta.length" class="slider-meta-list">
<div class="slider-meta-toolbar">
<span class="form-tip">可调整顺序,轮播将按当前顺序展示。</span>
@@ -105,6 +105,10 @@ layout("/layouts/platform.html"){
<span class="slider-field-label"><i class="color-danger">*</i> 图片标题</span>
<el-input v-model.trim="img.title" maxlength="100" show-word-limit
:placeholder="'请输入第' + (index + 1) + '张图片标题'"></el-input>
<span class="slider-field-label" style="margin-top: 10px"><i class="color-danger">*</i> 所属工会</span>
<el-select v-model="img.unionId" filterable placeholder="请选择所属工会" style="width: 100%">
<el-option v-for="item in unionOptions" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select>
</div>
<div class="slider-item-actions">
@@ -160,23 +164,6 @@ layout("/layouts/platform.html"){
}
}
},
computed: {
// 仅所选末级栏目及其上级栏目都开启轮播时,显示轮播图片配置。
sliderEnabled() {
if (!this.formData.categoryId) return false
const findPath = (nodes, categoryId, parents) => {
for (const node of nodes || []) {
const path = parents.concat(node)
if (node.id === categoryId) return path
const childPath = findPath(node.children, categoryId, path)
if (childPath) return childPath
}
return null
}
const categoryPath = findPath(this.categoryOptions, this.formData.categoryId, [])
return !!categoryPath && categoryPath.every(item => Number(item.slider) === 1)
}
},
methods: {
// 生成新增内容时使用的默认表单。
defaultForm() {
@@ -214,6 +201,9 @@ layout("/layouts/platform.html"){
contentUnionChanged(unionId) {
const union = this.unionOptions.find(item => item.id === unionId)
this.formData.unionName = union ? union.name : ""
this.sliderMeta.forEach(item => {
if (!item.unionId) item.unionId = unionId
})
},
// 分工会人员只有一个可选工会时自动带入,校工会管理员仍需主动选择。
@@ -240,7 +230,7 @@ layout("/layouts/platform.html"){
}
}).finally(() => this.loading = false)
},
// 兼容历史图片地址数组,并转换为只包含图片地址和标题的轮播对象。
// 兼容历史图片地址数组,未设置工会的图片默认使用内容所属工会。
parseSliderImages(value) {
if (!value) return []
let items = value
@@ -254,11 +244,12 @@ layout("/layouts/platform.html"){
if (!Array.isArray(items)) return []
return items.map(item => {
if (typeof item === "string") {
return {src: item, title: ""}
return {src: item, title: "", unionId: this.formData.unionId || ""}
}
return {
src: item.src || item.url || item.data || (item.response && item.response.data) || "",
title: item.title || ""
title: item.title || "",
unionId: item.unionId || this.formData.unionId || ""
}
}).filter(item => item.src)
},
@@ -272,7 +263,7 @@ layout("/layouts/platform.html"){
const oldItems = {}
this.sliderMeta.forEach(item => oldItems[item.src] = item)
this.sliderMeta = (this.sliderFiles || []).map(item => this.getSliderFileUrl(item)).filter(Boolean).map(src => {
return oldItems[src] || {src: src, title: ""}
return oldItems[src] || {src: src, title: "", unionId: this.formData.unionId || ""}
})
this.applyUnionDefaults()
},
@@ -301,20 +292,21 @@ layout("/layouts/platform.html"){
this.$message.success("轮播图片已清空")
}).catch(() => {})
},
// 将轮播图片地址和标题转换为可持久化 JSON。
// 将轮播图片地址、标题和所属工会转换为可持久化 JSON。
buildSliderImages() {
this.syncSliderMeta()
return JSON.stringify(this.sliderMeta.map(item => ({
src: item.src,
title: item.title
title: item.title,
unionId: item.unionId
})))
},
// 校验每张轮播图的标题。
// 校验每张轮播图的标题和所属工会。
validateSliderMeta() {
this.syncSliderMeta()
const invalidIndex = this.sliderMeta.findIndex(item => !item.title)
const invalidIndex = this.sliderMeta.findIndex(item => !item.title || !item.unionId)
if (invalidIndex < 0) return true
this.$message.warning("请输入第" + (invalidIndex + 1) + "张轮播图标题")
this.$message.warning("请完善第" + (invalidIndex + 1) + "张轮播图的标题和所属工会")
return false
},
// 重置新增表单;编辑状态下恢复数据库内容。
@@ -334,14 +326,9 @@ layout("/layouts/platform.html"){
submit() {
this.$refs.contentForm.validate(valid => {
if (!valid) return
if (this.sliderEnabled && !this.validateSliderMeta()) return
if (!this.validateSliderMeta()) return
const data = Object.assign({}, this.formData)
if (this.sliderEnabled) {
data.sliderImages = this.buildSliderImages()
} else {
// 未开启轮播的栏目不提交隐藏字段,编辑时保留已有数据,避免切换栏目造成误删。
delete data.sliderImages
}
data.sliderImages = this.buildSliderImages()
// 推荐和头条没有对应业务逻辑,提交时不再携带这两个字段。
delete data.isRecommend
delete data.isHeadline
File diff suppressed because it is too large Load Diff
@@ -2,60 +2,252 @@
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; }
#app.spread-mobile { height: auto; min-height: 100%; }
.spread-mobile .dynamic-detail-page {
min-height: 100%;
box-sizing: border-box;
background: #f4f7fb;
}
.spread-mobile .page-loading {
padding-top: 140px;
}
.spread-mobile .article-card {
min-height: calc(100vh - 46px);
padding: 22px 18px 30px;
box-sizing: border-box;
border-radius: 12px;
background: #fff;
box-shadow: 0 8px 24px rgba(29, 75, 129, 0.08);
}
.spread-mobile .article-title {
margin: 0;
color: #17233d;
font-size: 22px;
line-height: 32px;
font-weight: 700;
}
.spread-mobile .article-meta {
display: flex;
flex-wrap: wrap;
gap: 8px 16px;
margin-top: 12px;
color: #8a96a8;
font-size: 12px;
line-height: 18px;
}
.spread-mobile .article-thumbnail,
.spread-mobile .article-carousel {
width: 100%;
border-radius: 8px;
overflow: hidden;
background: #eef3f8;
}
.spread-mobile .article-thumbnail {
height: 220px;
margin-top: 18px;
}
.spread-mobile .article-carousel {
width: 100%;
height: 220px;
margin: 12px 0 0;
border-radius: 8px;
background: transparent;
}
.spread-mobile .article-carousel .van-swipe__track,
.spread-mobile .article-carousel .van-swipe-item {
height: 100%;
}
.spread-mobile .carousel-slide {
position: relative;
height: 100%;
display: flex;
flex-direction: column;
}
.spread-mobile .article-cover {
display: block;
width: 100%;
height: 220px;
flex: none;
background: transparent;
}
.spread-mobile .carousel-title {
position: absolute;
right: 0;
bottom: 0;
left: 0;
height: 38px;
padding: 0 12px;
display: flex;
align-items: center;
justify-content: space-between;
box-sizing: border-box;
color: #fff;
font-size: 13px;
line-height: 20px;
background: rgba(0, 0, 0, 0.5);
}
.spread-mobile .carousel-title > span:first-child {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.spread-mobile .carousel-count {
margin-left: 12px;
color: #fff;
flex: none;
}
.spread-mobile .image-empty {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
color: #9aa8b8;
background: #eef3f8;
font-size: 13px;
}
.spread-mobile .article-summary {
margin-top: 18px;
padding: 12px 14px;
border-left: 3px solid #1677ff;
color: #607086;
font-size: 14px;
line-height: 22px;
background: #f4f8ff;
}
.spread-mobile .article-content {
margin-top: 20px;
overflow: hidden;
color: #39475a;
font-size: 15px;
line-height: 1.8;
word-break: break-word;
}
.spread-mobile .article-content img,
.spread-mobile .article-content video {
max-width: 100%;
height: auto;
}
.spread-mobile .article-content table {
display: block;
max-width: 100%;
overflow-x: auto;
border-collapse: collapse;
}
.spread-mobile .article-content p {
margin: 10px 0;
}
.spread-mobile .skeleton-block {
display: block;
background: linear-gradient(90deg, #edf1f5 25%, #f7f9fb 37%, #edf1f5 63%);
background-size: 400% 100%;
animation: detailSkeleton 1.4s ease infinite;
}
.spread-mobile .detail-skeleton-title {
width: 78%;
height: 28px;
border-radius: 5px;
}
.spread-mobile .detail-skeleton-meta {
display: flex;
gap: 16px;
margin-top: 14px;
}
.spread-mobile .detail-skeleton-meta .skeleton-block {
width: 72px;
height: 13px;
border-radius: 4px;
}
.spread-mobile .detail-skeleton-image {
width: 100%;
height: 220px;
margin-top: 20px;
border-radius: 8px;
}
.spread-mobile .detail-skeleton-summary {
width: 100%;
height: 58px;
margin-top: 18px;
border-radius: 5px;
}
.spread-mobile .detail-skeleton-content {
margin-top: 22px;
}
.spread-mobile .detail-skeleton-content .skeleton-block {
width: 100%;
height: 14px;
margin-top: 12px;
border-radius: 4px;
}
.spread-mobile .detail-skeleton-content .skeleton-block:nth-child(2n) {
width: 88%;
}
.spread-mobile .detail-skeleton-content .skeleton-block:last-child {
width: 62%;
}
@keyframes detailSkeleton {
0% {
background-position: 100% 50%;
}
100% {
background-position: 0 50%;
}
}
</style>
<div id="app" class="spread-detail-h5" v-cloak>
<div id="app" class="spread-mobile" 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>
<div class="dynamic-detail-page">
<div v-if="detailLoading" class="article-card detail-skeleton">
<span class="skeleton-block detail-skeleton-title"></span>
<div class="detail-skeleton-meta">
<span class="skeleton-block"></span>
<span class="skeleton-block"></span>
<span class="skeleton-block"></span>
</div>
<span class="skeleton-block detail-skeleton-summary"></span>
<span class="skeleton-block detail-skeleton-image"></span>
<div class="detail-skeleton-content">
<span v-for="index in 5" :key="'detail-skeleton-line-' + index" class="skeleton-block"></span>
</div>
</div>
<article v-else-if="detail && detail.id" class="article-card">
<h1 class="article-title">{{ detail.title }}</h1>
<div class="article-meta">
<span>{{ detail.categoryName || '工会动态' }}</span>
<span>{{ formatDate(detail.publishedAt || detail.createdAt) }}</span>
<span><van-icon name="eye-o" ></van-icon> {{ detail.viewCount || 0 }}</span>
</div>
<div v-if="detail.summary" class="article-summary">{{ detail.summary }}</div>
<van-swipe
v-if="detailSlides.length"
class="article-carousel"
:autoplay="detailSlides.length > 1 ? 3500 : 0"
:show-indicators="false"
>
<van-swipe-item v-for="(image, index) in detailSlides" :key="image.src + '-' + index">
<div class="carousel-slide">
<van-image class="article-cover" fit="cover" :src="image.src">
<template #loading>
<van-loading type="spinner" size="24" ></van-loading>
</template>
<template #error>
<div class="image-empty">暂无图片</div>
</template>
</van-image>
<div class="carousel-title">
<span>{{ image.title }}</span>
<span class="carousel-count">{{ index + 1 }}/{{ detailSlides.length }}</span>
</div>
</div>
</van-swipe-item>
</van-swipe>
<div v-if="detail.contentBody" class="article-content" v-html="detail.contentBody"></div>
<van-empty v-else description="暂无正文内容" ></van-empty>
</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>
<van-empty v-else description="动态信息不存在或未发布" ></van-empty>
</div>
</div>
<script nonce="${cspNonce!}">
@@ -94,8 +286,8 @@ layout("/layouts/platform_h5.html"){
})
},
handleBack() {
const source = new URLSearchParams(window.location.search).get("from")
this.$pjaxReplace(source === "home" ? "/platform/h5/home" : "/platform/h5/spread")
const categoryId = this.detail && this.detail.categoryId
this.$pjaxReplace("/platform/h5/spread" + (categoryId ? "?categoryId=" + encodeURIComponent(categoryId) : ""))
},
getCoverUrl(value) {
if (!value) return ""
@@ -127,12 +319,13 @@ layout("/layouts/platform_h5.html"){
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 || ""
title: item.title || item.description || "",
unionName: item.unionName || ""
}
}).filter(item => item.src)
},
formatDate(value) {
return value ? this.$moment(Number(value)).format("YYYY-MM-DD") : ""
return value ? this.$moment(Number(value)).format("YYYY-MM-DD HH:mm") : ""
}
},
created() {
@@ -2,125 +2,458 @@
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; }
#app.spread-mobile { height: 100%; min-height: 0; overflow: hidden; display: flex; flex-direction: column; }
.spread-mobile .van-nav-bar__placeholder { flex-shrink: 0; }
.spread-mobile .dynamic-page {
flex: 1;
min-height: 0;
padding: 0 16px;
box-sizing: border-box;
display: flex;
flex-direction: column;
overflow: hidden;
color: #17233d;
background: #f4f7fc;
}
.spread-mobile .spread-news-header {
position: relative;
isolation: isolate;
flex: none;
margin: 0 -16px;
background: #f4f7fc;
}
.spread-mobile .spread-news-header::before {
position: absolute;
top: 0;
left: 0;
z-index: -1;
width: 100%;
padding-top: 46.258%;
background: url('/assets/mobile/img/home/v4/spread-news-header.png') center top / 100% auto no-repeat;
-webkit-mask-image: linear-gradient(to bottom, #000 65%, transparent 100%);
mask-image: linear-gradient(to bottom, #000 65%, transparent 100%);
pointer-events: none;
content: '';
}
.spread-mobile .page-heading {
min-height: 104px;
padding: 24px 20px 14px;
box-sizing: border-box;
}
.spread-mobile .spread-news-title {
font-size: 28px;
line-height: 38px;
font-weight: 700;
}
.spread-mobile .page-subtitle {
margin-top: 10px;
color: #8390a3;
font-size: 13px;
line-height: 20px;
}
.spread-mobile .page-loading {
padding-top: 120px;
}
.spread-mobile .dynamic-layout {
display: flex;
flex-direction: column;
align-items: stretch;
flex: 1;
min-height: 0;
margin-left: -16px;
margin-right: -16px;
gap: 0;
}
.spread-mobile .category-sidebar,
.spread-mobile .category-skeleton {
width: 100%;
max-height: 35vh;
flex: none;
overflow-x: hidden;
overflow-y: auto;
border-radius: 0;
background: transparent;
}
.spread-mobile .category-row {
display: flex;
gap: 8px;
padding: 6px 16px;
overflow-x: auto;
scrollbar-width: none;
}
.spread-mobile .category-row::-webkit-scrollbar {
display: none;
}
.spread-mobile .category-item {
position: relative;
display: flex;
align-items: center;
flex: none;
max-width: 200px;
min-height: 32px;
padding: 6px 12px;
border: 0;
border-radius: 16px;
color: #657289;
background: rgba(255, 255, 255, 0.8);
font-size: 13px;
line-height: 18px;
text-align: left;
}
.spread-mobile .category-name {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.spread-mobile .category-item.active {
color: #1677ff;
background: #e1edff;
font-weight: 600;
}
.spread-mobile .category-row-root .category-item {
padding: 8px 4px 12px;
margin-right: 12px;
border-radius: 0;
background: transparent;
font-size: 16px;
line-height: 22px;
}
.spread-mobile .category-row-root .category-item.active::after {
position: absolute;
bottom: 2px;
left: calc(50% - 10px);
width: 20px;
height: 3px;
border-radius: 3px;
background: #1677ff;
content: '';
}
.spread-mobile .category-skeleton {
display: flex;
gap: 12px;
padding: 8px 16px;
box-sizing: border-box;
}
.spread-mobile .category-skeleton-item {
width: 64px;
flex: none;
height: 32px;
margin: 7px 0;
border-radius: 5px;
}
.spread-mobile .dynamic-content {
flex: 1;
min-width: 0;
min-height: 0;
padding: 0 0 28px;
box-sizing: border-box;
overflow-y: auto;
overscroll-behavior: contain;
-webkit-overflow-scrolling: touch;
background: #f4f7fc;
}
.spread-mobile .dynamic-empty-state {
padding: 32px 0 20px;
text-align: center;
}
.spread-mobile .dynamic-empty-page {
flex: 1;
padding-top: 52px;
}
.spread-mobile .dynamic-empty-illustration {
display: block;
width: 220px;
max-width: 100%;
height: 150px;
margin: 0 auto;
}
.spread-mobile .dynamic-empty-title {
margin-top: 8px;
color: #17233d;
font-size: 16px;
line-height: 24px;
font-weight: 600;
}
.spread-mobile .dynamic-empty-desc {
margin-top: 3px;
color: #8a96a8;
font-size: 13px;
line-height: 20px;
}
.spread-mobile .dynamic-group + .dynamic-group {
margin-top: 14px;
}
.spread-mobile .group-title {
margin-bottom: 8px;
padding-left: 9px;
border-left: 3px solid #1677ff;
color: #53637a;
font-size: 14px;
line-height: 20px;
font-weight: 600;
}
.spread-mobile .dynamic-list {
overflow: hidden;
padding: 12px 12px 0;
border-radius: 0;
background: transparent;
}
.spread-mobile .dynamic-item {
display: flex;
flex-direction: row-reverse;
gap: 12px;
align-items: center;
padding: 16px 14px;
margin-bottom: 12px;
border-radius: 12px;
background: #fff;
}
.spread-mobile .dynamic-item:last-child {
border-bottom: 0;
}
.spread-mobile .dynamic-cover {
width: 112px;
height: 80px;
border-radius: 8px;
overflow: hidden;
flex: none;
}
.spread-mobile .image-empty {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
color: #9aa8b8;
background: #eef3f8;
font-size: 12px;
}
.spread-mobile .dynamic-info {
flex: 1;
min-width: 0;
}
.spread-mobile .dynamic-title {
display: -webkit-box;
overflow: hidden;
color: #17233d;
font-size: 16px;
line-height: 24px;
font-weight: 600;
text-overflow: ellipsis;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
word-break: break-word;
}
.spread-mobile .dynamic-summary {
display: -webkit-box;
overflow: hidden;
margin-top: 5px;
color: #7e8897;
font-size: 12px;
line-height: 18px;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.spread-mobile .dynamic-meta {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 4px 16px;
margin-top: 10px;
color: #9aa4b2;
font-size: 12px;
line-height: 18px;
}
.spread-mobile .dynamic-meta > span {
display: inline-flex;
align-items: center;
gap: 5px;
}
.spread-mobile .dynamic-meta > span + span {
padding-left: 12px;
border-left: 1px solid #e5eaf2;
}
.spread-mobile .dynamic-skeleton-group + .dynamic-skeleton-group {
margin-top: 14px;
}
.spread-mobile .skeleton-block {
display: block;
background: linear-gradient(90deg, #edf1f5 25%, #f7f9fb 37%, #edf1f5 63%);
background-size: 400% 100%;
animation: dynamicSkeleton 1.4s ease infinite;
}
.spread-mobile .dynamic-skeleton-category {
width: 80px;
height: 18px;
margin-bottom: 8px;
border-radius: 4px;
}
.spread-mobile .dynamic-skeleton-item {
display: flex;
flex-direction: row-reverse;
gap: 12px;
align-items: center;
padding: 16px 14px;
margin-bottom: 12px;
border-radius: 12px;
background: #fff;
}
.spread-mobile .dynamic-skeleton-cover {
width: 112px;
height: 80px;
border-radius: 8px;
flex: none;
}
.spread-mobile .dynamic-skeleton-info {
flex: 1;
min-width: 0;
}
.spread-mobile .dynamic-skeleton-title {
width: 86%;
height: 16px;
border-radius: 4px;
}
.spread-mobile .dynamic-skeleton-summary {
width: 68%;
height: 12px;
margin-top: 10px;
border-radius: 4px;
}
.spread-mobile .dynamic-skeleton-meta {
width: 48%;
height: 12px;
margin-top: 12px;
border-radius: 4px;
}
.spread-mobile .pagination-skeleton {
margin-top: 10px;
}
@keyframes dynamicSkeleton {
0% {
background-position: 100% 50%;
}
100% {
background-position: 0 50%;
}
}
</style>
<div id="app" class="spread-h5" v-cloak>
<div id="app" class="spread-mobile" v-cloak>
<van-nav-bar title="工会动态" left-arrow left-text="返回" fixed placeholder :z-index="20" @click-left="handleBack"></van-nav-bar>
<div class="dynamic-page">
<header class="spread-news-header">
<div class="page-heading">
<div class="spread-news-title">工会动态</div>
<div class="page-subtitle">记录工会风采,传递职工声音</div>
</div>
<div v-if="categoryLoading" class="category-skeleton">
<span v-for="index in 5" :key="'category-skeleton-' + index" class="skeleton-block category-skeleton-item"></span>
</div>
<nav v-else-if="categories.length" class="category-sidebar" aria-label="新闻栏目">
<div v-for="(row, depth) in categoryRows" :key="depth" class="category-row" :class="{ 'category-row-root': depth === 0 }">
<button
v-for="category in row.categories"
:key="category.id"
class="category-item"
:class="{ active: category.id === row.selectedId }"
:aria-pressed="category.id === row.selectedId"
type="button"
@click="handleCategoryClick(category)"
>
<span class="category-name">{{ category.name || '未命名栏目' }}</span>
</button>
</div>
</nav>
</header>
<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 v-if="categoryLoading" class="dynamic-layout">
<div class="dynamic-content">
<div class="dynamic-list">
<div v-for="index in 5" :key="'dynamic-item-skeleton-' + index" class="dynamic-skeleton-item">
<span class="skeleton-block dynamic-skeleton-cover"></span>
<div class="dynamic-skeleton-info">
<span class="skeleton-block dynamic-skeleton-title"></span>
<span class="skeleton-block dynamic-skeleton-meta"></span>
</div>
</div>
</div>
</div>
</div>
<div v-else-if="categories.length" class="dynamic-layout">
<section class="dynamic-content">
<van-list
:key="activeCategoryId"
v-model="loading"
:finished="finished"
:finished-text="dynamicList.length ? '没有更多了' : ''"
:error.sync="error"
error-text="请求失败,点击重新加载"
:immediate-check="false"
@load="loadDynamicList"
>
<div v-if="dynamicList.length" class="dynamic-list">
<article
v-for="item in dynamicList"
:key="item.id"
class="dynamic-item"
@click="openDetail(item)"
>
<van-image v-if="imageUrl(item.thumbUrl)" class="dynamic-cover" fit="cover" :src="imageUrl(item.thumbUrl)">
<template #loading>
<van-loading type="spinner" size="20" ></van-loading>
</template>
<template #error>
<div class="image-empty">暂无图片</div>
</template>
</van-image>
<div class="dynamic-info">
<div class="dynamic-title">{{ item.title }}</div>
<div class="dynamic-meta">
<span><van-icon name="calendar-o" ></van-icon> {{ formatDate(item.publishedAt || item.createdAt) }}</span>
<span><van-icon name="eye-o" ></van-icon> {{ item.viewCount || 0 }}</span>
</div>
</div>
</article>
</div>
<template #loading>
<div class="dynamic-list pagination-skeleton">
<div v-for="index in 2" :key="'pagination-skeleton-' + index" class="dynamic-skeleton-item">
<span class="skeleton-block dynamic-skeleton-cover"></span>
<div class="dynamic-skeleton-info">
<span class="skeleton-block dynamic-skeleton-title"></span>
<span class="skeleton-block dynamic-skeleton-meta"></span>
</div>
</div>
</div>
</template>
</van-list>
<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 v-if="!loading && finished && !dynamicList.length" class="dynamic-empty-state">
<van-image
class="dynamic-empty-illustration"
fit="contain"
:src="'/assets/mobile/img/home/v4/spread-news-empty.png'"
></van-image>
<div class="dynamic-empty-title">暂无工会动态</div>
<div class="dynamic-empty-desc">当前没有工会动态,敬请期待~</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>
</section>
</div>
<div v-else-if="categoryError" class="dynamic-empty-state dynamic-empty-page"><van-empty description="栏目加载失败"><van-button type="primary" size="small" @click="loadCategoryList">重新加载</van-button></van-empty></div>
<div v-else class="dynamic-empty-state dynamic-empty-page">
<van-image
class="dynamic-empty-illustration"
fit="contain"
:src="'/assets/mobile/img/home/v4/spread-news-empty.png'"
></van-image>
<div class="dynamic-empty-title">暂无工会动态</div>
<div class="dynamic-empty-desc">当前没有工会动态,敬请期待~</div>
</div>
</div>
</div>
<script nonce="${cspNonce!}">
@@ -128,166 +461,129 @@ layout("/layouts/platform_h5.html"){
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: []}
categories: [], categoryLoading: true, categoryError: false, activeCategoryId: "",
dynamicList: [], loading: false, finished: false, error: false,
currentPage: 1, pageSize: 10, requestVersion: 0
}
},
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}))
)
categoryRows() {
const findPath = nodes => {
for (const category of nodes || []) {
if (category.id === this.activeCategoryId) return [category]
const children = findPath(category.children)
if (children.length) return [category].concat(children)
}
return []
}
const rows = []
let categories = this.categories
findPath(this.categories).forEach(category => {
rows.push({categories: categories, selectedId: category.id})
categories = category.children || []
})
return rows.length ? rows : [{categories: this.categories, selectedId: ""}]
}
},
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) {
findFirstLeafCategory(categories) {
for (const category of categories || []) {
if (!category.children || !category.children.length) return category
const child = this.findFirstLeafCategory(category.children)
if (child) return child
}
return null
},
loadCategoryList() {
this.categoryLoading = true
this.categoryError = false
this.$axios.post("/platform/h5/spread/categoryTree").then(resp => {
if (resp.code !== 0) throw new Error(resp.msg || "栏目加载失败")
this.categories = resp.data || []
const requestedId = new URLSearchParams(window.location.search).get("categoryId")
const findCategory = nodes => {
for (const node of nodes || []) {
if (node.id === requestedId) return node
const child = findCategory(node.children)
if (child) return child
}
return null
}
const requested = requestedId && findCategory(this.categories)
const category = this.findFirstLeafCategory(requested ? [requested] : this.categories)
if (category) {
this.activeCategoryId = category.id
this.resetDynamicList()
this.loadDynamicList()
}
}).catch(() => {
this.categories = []
this.categoryError = true
}).finally(() => this.categoryLoading = false)
},
handleCategoryClick(category) {
const selected = this.findFirstLeafCategory([category])
if (!selected || selected.id === this.activeCategoryId) return
this.activeCategoryId = selected.id
this.resetDynamicList()
this.loadDynamicList()
},
resetDynamicList() {
this.requestVersion += 1
this.dynamicList = []
this.currentPage = 1
this.finished = false
this.error = false
this.loading = false
},
loadDynamicList() {
if (this.finished || !this.activeCategoryId) return
const requestVersion = this.requestVersion
this.loading = true
this.error = false
this.$axios.post("/platform/h5/spread/pageData", {
pageNumber: this.currentPage, pageSize: this.pageSize, categoryId: this.activeCategoryId
}).then(resp => {
if (requestVersion !== this.requestVersion) return
if (resp.code !== 0) throw new Error(resp.msg || "加载失败")
const data = resp.data || {}
const rows = data.list || []
this.dynamicList = this.dynamicList.concat(rows)
const total = Number(data.totalCount || 0)
this.finished = total > 0 ? this.dynamicList.length >= total : rows.length < this.pageSize
if (!this.finished) this.currentPage += 1
}).catch(() => {
if (requestVersion === this.requestVersion) this.error = true
}).finally(() => {
if (requestVersion === this.requestVersion) this.loading = false
})
},
imageUrl(value) {
if (!value) return ""
let files = value
if (typeof value === "string" && value.trim().startsWith("[")) {
try {
files = JSON.parse(value)
} catch (e) {
files = []
}
if (typeof value === "string" && /^[\[{]/.test(value.trim())) {
try { files = JSON.parse(value) } catch (e) { return "" }
}
if (!Array.isArray(files)) files = String(files).split(",")
if (!Array.isArray(files)) files = typeof files === "string" ? files.split(",") : [files]
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") : ""
},
openDetail(item) {
this.$pjaxReplace("/platform/h5/spread/detail?id=" + encodeURIComponent(item.id) + "&from=list")
}
},
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})
}
this.loadCategoryList()
},
beforeDestroy() {
if (this.scrollContainer) {
this.scrollContainer.removeEventListener("scroll", this.handlePageScroll)
}
this.requestVersion += 1
}
})
</script>
@@ -3,8 +3,8 @@ const home = {
<div class="home-page">
<div class="home-summary-bar" :class="{'home-summary-bar--visible': showHomeSummary}">
<img class="home-summary-bar__logo"
src="/assets/mobile/img/home/v4/中国地质大学-logo.png"
alt="中国地质大学">
:src="appLogo"
:alt="appName">
<span class="home-summary-bar__divider"></span>
<span class="home-summary-bar__title">智慧工会</span>
</div>
@@ -14,8 +14,8 @@ const home = {
<div class="home-hero__content">
<div class="home-hero__topbar">
<img class="home-hero__brand-logo"
src="/assets/mobile/img/home/v4/中国地质大学-logo.png"
alt="中国地质大学">
:src="appLogo"
:alt="appName">
</div>
<div class="home-hero__title">智慧工会</div>
<div class="home-hero__subtitle">情系职工 · 真诚服务</div>
@@ -70,25 +70,12 @@ const home = {
<div class="union-news-panel">
<div class="union-news-panel__head">
<button type="button" class="union-news-panel__title" @click="openSpreadMenu"
:disabled="spreadSections.length < 2" :aria-expanded="spreadMenuVisible">
{{ (spreadSections[spreadActiveIndex] || {}).label || '工会动态' }}
<van-icon v-if="spreadSections.length > 1" name="arrow-down"></van-icon>
</button>
<div class="union-news-panel__title">工会动态</div>
<div class="union-news-panel__more" @click="enterSpreadList">
<span>全部动态</span>
<van-icon name="arrow"></van-icon>
</div>
</div>
<div v-show="spreadMenuVisible" class="union-news-menu-dismiss" @click="closeSpreadMenu"></div>
<div v-show="spreadMenuVisible" class="union-news-menu" role="menu" @keydown.esc="closeSpreadMenu">
<button type="button" role="menuitem" v-for="(section, index) in spreadSections"
:key="section.label + '-' + index" @click="chooseSpreadSection(index)"
:class="{'is-selected': spreadActiveIndex === index}" :aria-current="spreadActiveIndex === index ? 'true' : null">
<span>{{ section.label }}</span>
<van-icon v-if="spreadActiveIndex === index" name="success" color="#1989fa"></van-icon>
</button>
</div>
<div v-if="spreadLoading && !spreadDynamics.length" class="union-news-list union-news-list--skeleton">
<div class="union-news-item union-news-item--skeleton"
v-for="index in 2"
@@ -108,19 +95,15 @@ const home = {
</div>
</div>
</div>
<div class="union-news-list" ref="spreadViewport" v-else-if="spreadDynamics.length">
<div class="union-news-list" v-else-if="spreadDynamics.length">
<div class="union-news-item"
v-for="item in spreadDynamics"
:key="item.href || item.title"
:key="item.id"
@click="enterSpreadDetail(item)">
<img v-if="item.thumbUrl && !item.imageFailed"
<img v-if="getSpreadCover(item.thumbUrl)"
class="union-news-item__cover"
:src="item.thumbUrl" referrerpolicy="no-referrer"
@error="$set(item, 'imageFailed', true)"
:src="getSpreadCover(item.thumbUrl)"
:alt="item.title">
<div v-else-if="spreadHasImages" class="union-news-item__cover union-news-item__placeholder">
<van-icon name="photo-o"></van-icon>
</div>
<div class="union-news-item__content">
<div class="union-news-item__tags">
<span class="union-news-item__tag"
@@ -134,10 +117,9 @@ const home = {
</div>
<div class="union-news-item__meta">
<span>{{ formatSpreadDate(item.publishedAt || item.createdAt) }}</span>
<span v-if="item.viewCount !== null && item.viewCount !== undefined"
class="union-news-item__views">
<span class="union-news-item__views">
<van-icon name="eye-o"></van-icon>
{{ item.viewCount }}
{{ item.viewCount || 0 }}
</span>
</div>
</div>
@@ -265,10 +247,6 @@ const home = {
classroomRecommendOptions: [],
showHomeSummary: false,
homeScrollContainer: null,
spreadSections: [],
spreadMenuVisible: false,
spreadHistoryToken: null,
spreadActiveIndex: 0,
spreadDynamics: [],
spreadLoading: true,
spreadLoadSeq: 0,
@@ -298,14 +276,14 @@ const home = {
className: "feature-card--model",
href: "/platform/honor/manage/h5"
},
{
/* {
name: "普惠信息",
desc: "普惠服务信息",
action: "查看信息",
asset: "/assets/mobile/img/home/v4/feature-inclusive-v8.png",
className: "feature-card--inclusive",
href: "/platform/h5/inclusive"
}
} */
],
appLogo: "",
appName: "",
@@ -333,10 +311,6 @@ const home = {
}
},
computed: {
spreadHasImages() {
// 按当前栏目统一保留图片列;异步补图及加载失败都会重新计算,全部无图时文字占满行宽。
return this.spreadDynamics.some(item => item.thumbUrl && !item.imageFailed)
},
currentBanner() {
return this.bannerList[this.activeBannerIndex] || this.bannerList[0] || {}
}
@@ -542,29 +516,18 @@ const home = {
listSpreadHomeContent() {
const loadSeq = ++this.spreadLoadSeq
this.spreadLoading = true
this.$axios.post("/platform/home/getNews").then((res) => {
this.$axios.post("/platform/h5/spread/pageData", {
sortType: "latest",
pageNumber: 1,
pageSize: 10
}).then((res) => {
if (loadSeq === this.spreadLoadSeq && res.code === 0) {
// 接口按栏目返回 href/time/text,移动端保存全部栏目供切换,并映射为现有动态卡片字段。
const sections = (Array.isArray(res.data) ? res.data : []).map(section => ({
label: section.label || "工会动态",
value: (Array.isArray(section.value) ? section.value : []).slice(0, 10).map(item => ({
href: item.href || "",
thumbUrl: /^https?:\/\//i.test(item.thumbUrl || "") ? item.thumbUrl : "",
imageFailed: false,
imageRequested: false,
title: item.text || "",
publishedAt: item.time || "",
categoryName: section.label || "工会动态"
})).filter(item => item.title)
}))
const firstAvailableIndex = sections.findIndex(section => section.value.length)
this.$set(this, "spreadSections", sections)
this.selectSpreadSection(firstAvailableIndex >= 0 ? firstAvailableIndex : 0)
const data = res.data || {}
this.spreadDynamics = data.list || []
}
}).catch(() => {
if (loadSeq === this.spreadLoadSeq) {
this.$set(this, "spreadSections", [])
this.$set(this, "spreadDynamics", [])
this.spreadDynamics = []
}
}).finally(() => {
if (loadSeq === this.spreadLoadSeq) {
@@ -572,48 +535,6 @@ const home = {
}
})
},
selectSpreadSection(index) {
// 切换栏目后回到横向卡片列表开头,等待新栏目列表渲染完成。
// 新闻样式锚定 home-page 根节点作用域,动态行无需等待 styleMixin 异步补属性。
const section = this.spreadSections[index] || {}
this.$set(this, "spreadActiveIndex", index)
this.$set(this, "spreadDynamics", section.value || [])
this.$nextTick(() => {
if (this.$refs.spreadViewport) this.$refs.spreadViewport.scrollLeft = 0
})
this.loadSpreadImages()
},
loadSpreadImages() {
// href 传原始新闻链接;data 为正文首图地址。每条只补取一次,回填原对象避免快速切换串图。
const missing = this.spreadDynamics.filter(item => !item.thumbUrl && !item.imageRequested)
missing.forEach(item => this.$set(item, "imageRequested", true))
// 串行补图限制外站并发;切换后仍可缓存到对应栏目,不阻塞新闻展示。
missing.reduce((pending, item) => pending.then(() => {
return this.$axios.post("/platform/home/getNewsImage", {href: item.href}).then(res => {
if (res.code === 0 && /^https?:\/\//i.test(res.data || "")) {
this.$set(item, "thumbUrl", res.data)
this.$set(item, "imageFailed", false)
}
}).catch(() => {})
}), Promise.resolve())
},
openSpreadMenu() {
// 栏目选择弹层交由全局历史栈管理,系统返回键仅关闭当前弹层。
if (this.spreadMenuVisible) {
this.closeSpreadMenu()
return
}
if (this.spreadSections.length < 2) return
window.h5PopupHistory.open(this.spreadHistoryToken, "news-section")
this.$set(this, "spreadMenuVisible", true)
},
closeSpreadMenu() {
if (this.spreadMenuVisible) window.h5PopupHistory.close(this.spreadHistoryToken, "news-section")
},
chooseSpreadSection(index) {
this.selectSpreadSection(index)
this.closeSpreadMenu()
},
getSpreadCategoryLabels(item) {
const labels = [item.parentCategoryName, item.childCategoryName].filter(Boolean)
return labels.length ? labels : (item.categoryName ? [item.categoryName] : [])
@@ -647,22 +568,10 @@ const home = {
return Number.isNaN(numberValue) ? String(value) : this.$moment(numberValue).format("YYYY-MM-DD")
},
enterSpreadDetail(item) {
const url = this.resolveUnionNewsUrl(item.href)
if (url) {
window.location.href = url
}
this.$pjaxReplace("/platform/h5/spread/detail?id=" + encodeURIComponent(item.id) + "&from=home")
},
enterSpreadList() {
window.location.href = "https://xgh.cug.edu.cn/"
},
resolveUnionNewsUrl(href) {
if (!href) {
return ""
}
if (/^https?:\/\//i.test(href)) {
return href
}
return new URL(href, "https://xgh.cug.edu.cn/").href
this.$pjaxReplace("/platform/h5/spread")
},
menuClick(item) {
this.$pjaxReplace(item.href)
@@ -701,10 +610,6 @@ const home = {
}
},
created() {
// 注册弹层返回回调,关闭状态由历史回退同步,避免留下多余历史记录。
this.spreadHistoryToken = window.h5PopupHistory.register(() => {
this.$set(this, "spreadMenuVisible", false)
})
this.listHomeBrand()
this.listRecommendApp()
this.listClassroomCourses()
@@ -722,8 +627,6 @@ const home = {
}
},
beforeDestroy() {
const token = this.spreadHistoryToken
window.h5PopupHistory.clear(token).then(() => window.h5PopupHistory.unregister(token))
if (this.homeScrollContainer) {
this.homeScrollContainer.removeEventListener("scroll", this.handleHomeScroll)
}
@@ -997,7 +900,8 @@ const home = {
margin: 0 12px 7px;
display: grid;
grid-template-columns: 1.08fr 1fr;
grid-template-rows: 86px 86px 92px;
grid-template-rows: 86px 86px;
grid-auto-rows: 92px;
gap: 8px;
}