会员管理整改

This commit is contained in:
2026-10-14 09:16:43 +08:00
parent 122f8bebb2
commit 79ce184be3
11 changed files with 652 additions and 178 deletions
@@ -3,6 +3,7 @@ package com.budwk.app.sys.controller;
import cn.dev33.satoken.annotation.SaCheckLogin;
import cn.dev33.satoken.annotation.SaCheckPermission;
import com.budwk.app.base.annotation.SLog;
import com.budwk.app.base.exception.BaseException;
import com.budwk.app.base.result.Result;
import com.budwk.app.sys.models.Sys_dict;
import com.budwk.app.sys.services.SysDictService;
@@ -119,6 +120,8 @@ public class SysDictController {
sysDictService.save(dict, parentId);
sysDictService.clearCache();
return Result.success("system.success");
} catch (BaseException e) {
return Result.error(e.getMessage());
} catch (Exception e) {
return Result.error("system.error");
}
@@ -142,9 +145,11 @@ public class SysDictController {
public Object editDo(@Param("..") Sys_dict dict, @Param("parentId") String parentId, HttpServletRequest req) {
try {
dict.setUpdatedBy(SecurityUtil.getUserId());
sysDictService.updateIgnoreNull(dict);
sysDictService.updateDict(dict);
sysDictService.clearCache();
return Result.success();
} catch (BaseException e) {
return Result.error(e.getMessage());
} catch (Exception e) {
return Result.error();
}
@@ -11,26 +11,21 @@ import com.budwk.app.base.result.Result;
import com.budwk.app.sys.models.Sys_home_activity;
import com.budwk.app.sys.models.Sys_home_template;
import com.budwk.app.sys.models.Sys_menu;
import com.budwk.app.sys.services.SysHomeNewsService;
import com.budwk.app.sys.services.SysMenuService;
import com.budwk.app.sys.services.SysUserService;
import com.budwk.app.web.commons.auth.utils.AuthUtil;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.activity.basic.service.ActivityBasicScopeService;
import io.swagger.annotations.ApiOperation;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.dao.FieldFilter;
import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql;
import org.nutz.dao.util.Daos;
import org.nutz.integration.jedis.RedisService;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.json.Json;
import org.nutz.lang.Lang;
import org.nutz.lang.Strings;
import org.nutz.lang.util.NutMap;
@@ -42,7 +37,6 @@ import org.nutz.mvc.annotation.Param;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.util.*;
import java.util.stream.Collectors;
@@ -58,9 +52,9 @@ public class SysHomeController {
@Inject
private SysUserService sysUserService;
@Inject
private RedisService redisService;
@Inject
private ActivityBasicScopeService activityBasicScopeService;
@Inject
private SysHomeNewsService sysHomeNewsService;
@At("")
@Ok("re")
@@ -293,34 +287,17 @@ public class SysHomeController {
@SaCheckLogin
@ApiOperation("获取新闻")
@Ok("json")
// @CacheDefaults(cacheName = RedisConstant.PLATFORM_REDIS_WKCACHE_PREFIX + "web_news", isHash = true)
// @CacheResult(cacheKey = "news", ignoreNull = true, cacheLiveTime = 60 * 60 * 24)
public Result getNews() {
try {
String url = "https://xgh.cug.edu.cn/";
Document document = Jsoup.connect(url).get();
Elements homeElements = document.select(".gh,.jc,.fc,.jg");
return Result.success(sysHomeNewsService.getWebsiteNews());
}
List<Map<String, Object>> news = homeElements.stream().map(homeElement -> {
//获取板块名称
String name = homeElement.selectFirst(".title").text();
Elements liElements = homeElement.select("li");
List<NutMap> ele = liElements.stream().map(liElement -> {
NutMap row = NutMap.NEW();
String href = liElement.selectFirst("a").attr("href");
String time = liElement.selectFirst("span").text();
String title = liElement.selectFirst("a").attr("title");
row.put("href", href);
row.put("time", time);
row.put("text", title);
return row;
}).toList();
return Map.of("label", name, "value", ele);
}).collect(Collectors.toList());
redisService.setex("gonghui_news", 60 * 60 * 24, Json.toJson(news));
return Result.success(news);
} catch (IOException e) {
throw new RuntimeException(e);
}
/**
* href 传首页新闻项原始链接;成功结果 data 为图片地址字符串,空字符串表示没有可用图片。
*/
@At
@SaCheckLogin
@Ok("json")
public Result getNewsImage(@Param("href") String href) {
return Result.success(sysHomeNewsService.getNewsImage(href));
}
}
@@ -52,6 +52,16 @@ public class Sys_dict extends BaseModel implements Serializable {
@ColDefine(type = ColType.VARCHAR, width = 100)
private String code;
@Column
@Comment("关联人员类型编码")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String personType;
@Column
@Comment("人员属性业务编码")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String userAttributeCode;
@Column
@Comment("启用状态")
@ColDefine(type = ColType.BOOLEAN)
@@ -79,6 +79,13 @@ public interface SysDictService extends BaseService<Sys_dict> {
*/
void save(Sys_dict dict, String pid);
/**
* 更新数据字典,并对人员属性直属子项的人员类型和业务编码进行校验。
*
* @param dict 待更新字典;id 为必填项,personType 与 userAttributeCode 可为空
*/
void updateDict(Sys_dict dict);
/**
* 保存数据字典
*
@@ -0,0 +1,24 @@
package com.budwk.app.sys.services;
import org.nutz.lang.util.NutMap;
import java.util.List;
/**
* 首页工会网站新闻服务。
*/
public interface SysHomeNewsService {
/**
* 获取学校工会网站首页新闻。
*
* @return 新闻栏目集合;每个栏目包含 label 栏目名称和 value 新闻列表,
* 新闻项包含 href 跳转地址、time 发布时间、text 新闻标题和 thumbUrl 缩略图绝对地址(无图时为空)
*/
List<NutMap> getWebsiteNews();
/**
* href 必须是工会首页返回的新闻链接;返回正文首图的绝对地址,无图或不可访问时为空字符串。
*/
String getNewsImage(String href);
}
@@ -27,6 +27,9 @@ import java.util.Map;
@IocBean(args = {"refer:dao"})
@CacheDefaults(cacheName = RedisConstant.PLATFORM_REDIS_WKCACHE_PREFIX + "sys_dict", isHash = true)
public class SysDictServiceImpl extends BaseServiceImpl<Sys_dict> implements SysDictService {
private static final String USER_ATTRIBUTE_DICT_CODE = "USER_ATTRIBUTE";
private static final String USER_PERSON_TYPE_DICT_CODE = "USER_PERSON_TYPE";
public SysDictServiceImpl(Dao dao) {
super(dao);
}
@@ -132,18 +135,76 @@ public class SysDictServiceImpl extends BaseServiceImpl<Sys_dict> implements Sys
@Aop(TransAop.READ_COMMITTED)
public void save(Sys_dict dict, String pid) {
String path = "";
Sys_dict parentDict = null;
if (!Strings.isEmpty(pid)) {
Sys_dict pp = this.fetch(pid);
path = pp.getPath();
parentDict = this.fetch(pid);
path = parentDict.getPath();
}
dict.setPath(getSubPath("sys_dict", "path", path));
dict.setParentId(pid);
validateUserAttribute(dict, parentDict);
dao().insert(dict);
if (!Strings.isEmpty(pid)) {
this.update(Chain.make("hasChildren", true), Cnd.where("id", "=", pid));
}
}
/**
* 更新字典记录。人员属性关联字段允许为空;填写后校验人员类型有效性和业务编码唯一性。
*
* @param dict 待更新字典,id 用于定位原记录并在唯一性校验中排除自身
*/
@Override
@Aop(TransAop.READ_COMMITTED)
public void updateDict(Sys_dict dict) {
Sys_dict current = this.fetch(dict.getId());
if (current == null) {
throw new BaseException("字典数据不存在");
}
Sys_dict parentDict = Strings.isBlank(current.getParentId()) ? null : this.fetch(current.getParentId());
dict.setParentId(current.getParentId());
validateUserAttribute(dict, parentDict);
this.updateIgnoreNull(dict);
}
/**
* 校验 USER_ATTRIBUTE 直属子项的扩展关系。
* personType 为空时允许保存;有值时必须属于 USER_PERSON_TYPE 的启用子项。
* userAttributeCode 为空时允许保存;有值时在 USER_ATTRIBUTE 直属子项中必须唯一。
*/
private void validateUserAttribute(Sys_dict dict, Sys_dict parentDict) {
if (parentDict == null || !USER_ATTRIBUTE_DICT_CODE.equals(parentDict.getCode())) {
return;
}
String personType = Strings.sNull(dict.getPersonType()).trim();
String userAttributeCode = Strings.sNull(dict.getUserAttributeCode()).trim();
dict.setPersonType(personType);
dict.setUserAttributeCode(userAttributeCode);
if (!Strings.isBlank(personType)) {
Sys_dict personTypeRoot = this.fetch(Cnd.where("code", "=", USER_PERSON_TYPE_DICT_CODE));
boolean personTypeValid = personTypeRoot != null && this.count(
Cnd.where("parentId", "=", personTypeRoot.getId())
.and("code", "=", personType)
.and("disabled", "=", false)
) > 0;
if (!personTypeValid) {
throw new BaseException("人员类型不存在或已禁用");
}
}
if (!Strings.isBlank(userAttributeCode)) {
Cnd codeCondition = Cnd.where("parentId", "=", parentDict.getId())
.and("userAttributeCode", "=", userAttributeCode);
if (!Strings.isBlank(dict.getId())) {
codeCondition.and("id", "!=", dict.getId());
}
if (this.count(codeCondition) > 0) {
throw new BaseException("人员属性编码已存在");
}
}
}
@Aop(TransAop.READ_COMMITTED)
public void saveByParentCode(Sys_dict dict, String parentCode) {
Sys_dict parentDict = fetch(Cnd.where("code", "=", parentCode));
@@ -0,0 +1,159 @@
package com.budwk.app.sys.services.impl;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.sys.services.SysHomeNewsService;
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.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 RedisService redisService;
/**
* 抓取学校工会网站首页的工会要闻、基层动态等栏目。
* 外部网站暂时不可用时优先返回最近一次缓存,避免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<NutMap> sections = new ArrayList<>();
for (Element homeElement : homeElements) {
Element titleElement = homeElement.selectFirst(".title");
if (titleElement == null || StrUtil.isBlank(titleElement.text())) {
continue;
}
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());
rows.add(NutMap.NEW()
.addv("href", linkElement.attr("href"))
.addv("time", timeElement == null ? "" : timeElement.text())
.addv("text", title)
.addv("thumbUrl", thumbUrl));
}
sections.add(NutMap.NEW().addv("label", titleElement.text()).addv("value", rows));
}
return sections;
}
/**
* 读取最近一次成功抓取的数据;缓存为空或内容异常时返回空集合。
*/
private List<NutMap> getCachedNews() {
String cached = redisService.get(NEWS_CACHE_KEY);
if (StrUtil.isBlank(cached)) {
return Collections.emptyList();
}
try {
return Json.fromJsonAsList(NutMap.class, cached);
} catch (RuntimeException e) {
return Collections.emptyList();
}
}
/**
* 仅补取缓存新闻列表中已存在的公开文章,禁止任意地址抓取和自动重定向。
* 正文首图独立缓存;无图短暂缓存,防止每次栏目切换重复访问外站。
*/
@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 (!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 "";
}
}
}
@@ -51,6 +51,8 @@ public class WkTomcatConfiguration implements NbAppEventListener {
// NutzBoot 使用编程式 Context,必须显式运行官方 WebSocket 初始化器。
StandardContext context = (StandardContext) tomcat.getHost().findChild(starter.getContextPath());
// 为静态 SVG 补齐响应类型,避免浏览器收到文件后仍无法按图片解码。
context.addMimeMapping("svg", "image/svg+xml");
context.setUseHttpOnly(conf.getBoolean("tomcat.session.cookie.httponly", true));
context.addServletContainerInitializer(new WsSci(), Set.of(WkWebSocket.class));
// Tomcat 的错误页再次 forward 时仍保持 ERROR 派发;让 MVC 渲染原业务错误页。
@@ -323,7 +323,8 @@
let h5GlobalLoading = null
let h5PendingRequestCount = 0
function showH5GlobalLoading(icon = "/assets/mobile/img/common/smart-union-loading-red.svg") {
// 更新资源版本,避免继续命中缺少 SVG 响应类型的旧缓存。
function showH5GlobalLoading(icon = "/assets/mobile/img/common/smart-union-loading-red.svg?v=20261014") {
if (!h5GlobalLoading) {
h5GlobalLoading = vant.Toast.loading({
icon: icon,
@@ -375,7 +376,7 @@
})
}
function createListLoading(icon = "/assets/mobile/img/common/smart-union-loading-red.svg") {
function createListLoading(icon = "/assets/mobile/img/common/smart-union-loading-red.svg?v=20261014") {
showH5GlobalLoading(icon)
const closeLoading = function() {
@@ -35,6 +35,14 @@ layout("/layouts/platform.html"){
<el-table-column label="字典代码" header-align="center" prop="code" :show-overflow-tooltip="true"></el-table-column>
<el-table-column label="人员类型" header-align="center" prop="personType" :show-overflow-tooltip="true">
<template slot-scope="scope">
{{personTypeNameMap[scope.row.personType]||scope.row.personType}}
</template>
</el-table-column>
<el-table-column label="编码" header-align="center" prop="userAttributeCode" :show-overflow-tooltip="true"></el-table-column>
<el-table-column label="启用状态" header-align="center" prop="disabled" align="center" :show-overflow-tooltip="true">
<template slot-scope="scope">
<i v-if="scope.row.disabled" class="fa fa-circle text-danger ml5"></i>
@@ -50,12 +58,12 @@ layout("/layouts/platform.html"){
<span class="ti-angle-down"></span>
</el-button>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item :command="{type:'add',id:scope.row.id,name:scope.row.name}">添加子级</el-dropdown-item>
<el-dropdown-item :command="{type:'add',id:scope.row.id,name:scope.row.name,row:scope.row}">添加子级</el-dropdown-item>
<el-dropdown-item divided :command="{type:'enable',id:scope.row.id,name:scope.row.name,row:scope.row}">
启用
</el-dropdown-item>
<el-dropdown-item :command="{type:'disable',id:scope.row.id,name:scope.row.name,row:scope.row}">禁用</el-dropdown-item>
<el-dropdown-item divided :command="{type:'edit',id:scope.row.id,name:scope.row.name}">修改</el-dropdown-item>
<el-dropdown-item divided :command="{type:'edit',id:scope.row.id,name:scope.row.name,row:scope.row}">修改</el-dropdown-item>
<el-dropdown-item :command="{type:'delete',id:scope.row.id,name:scope.row.name}">删除</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
@@ -77,12 +85,20 @@ layout("/layouts/platform.html"){
></el-cascader>
<el-input v-if="isAddFromSub" type="text" v-model="formData.parentName" :disabled="true"></el-input>
</el-form-item>
<el-form-item v-if="showUserAttributeFields" prop="personType" label="人员类型">
<el-select v-model="formData.personType" clearable filterable placeholder="请选择人员类型" style="width: 100%">
<el-option v-for="item in personTypeOptions" :key="item.code" :label="item.name" :value="item.code"></el-option>
</el-select>
</el-form-item>
<el-form-item prop="name" label="字典名称">
<el-input maxlength="100" placeholder="字典名称" v-model="formData.name" auto-complete="off" tabindex="3" type="text"></el-input>
</el-form-item>
<el-form-item prop="code" label="字典代码">
<el-input maxlength="100" placeholder="字典代码" v-model="formData.code" auto-complete="off" tabindex="5" type="text"></el-input>
</el-form-item>
<el-form-item v-if="showUserAttributeFields" prop="userAttributeCode" label="编码">
<el-input maxlength="100" placeholder="请输入人员属性编码" v-model="formData.userAttributeCode" auto-complete="off" type="text"></el-input>
</el-form-item>
<el-form-item prop="remark" label="字典备注">
<el-input maxlength="100" placeholder="字典备注" v-model="formData.remark" auto-complete="off" tabindex="2" type="text"></el-input>
</el-form-item>
@@ -91,18 +107,26 @@ layout("/layouts/platform.html"){
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="addDialogVisible = false">取 消</el-button>
<el-button type="primary" @click="doAdd">确 定</el-button>
<el-button @click="closeAddDialog">取 消</el-button>
<el-button type="primary" :loading="formLoading" @click="doAdd">确 定</el-button>
</span>
</el-dialog>
<el-dialog title="修改字典" :visible.sync="editDialogVisible" :close-on-click-modal="false" width="50%">
<el-form :model="formData" ref="editForm" :rules="formRules" size="small" label-width="80px">
<el-form-item v-if="showUserAttributeFields" prop="personType" label="人员类型">
<el-select v-model="formData.personType" clearable filterable placeholder="请选择人员类型" style="width: 100%">
<el-option v-for="item in personTypeOptions" :key="item.code" :label="item.name" :value="item.code"></el-option>
</el-select>
</el-form-item>
<el-form-item prop="name" label="字典名称">
<el-input maxlength="100" placeholder="字典名称" v-model="formData.name" auto-complete="off" tabindex="1" type="text"></el-input>
</el-form-item>
<el-form-item prop="code" label="字典代码">
<el-input maxlength="100" placeholder="字典代码" v-model="formData.code" auto-complete="off" tabindex="2" type="text"></el-input>
</el-form-item>
<el-form-item v-if="showUserAttributeFields" prop="userAttributeCode" label="编码">
<el-input maxlength="100" placeholder="请输入人员属性编码" v-model="formData.userAttributeCode" auto-complete="off" type="text"></el-input>
</el-form-item>
<el-form-item prop="remark" label="字典备注">
<el-input maxlength="100" placeholder="字典备注" v-model="formData.remark" auto-complete="off" tabindex="2" type="text"></el-input>
</el-form-item>
@@ -111,8 +135,8 @@ layout("/layouts/platform.html"){
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="editDialogVisible = false">取 消</el-button>
<el-button type="primary" @click="doEdit">确 定</el-button>
<el-button @click="closeEditDialog">取 消</el-button>
<el-button type="primary" :loading="formLoading" @click="doEdit">确 定</el-button>
</span>
</el-dialog>
<el-dialog title="字典排序" :visible.sync="sortDialogVisible" width="50%">
@@ -136,12 +160,19 @@ layout("/layouts/platform.html"){
editDialogVisible: false,
sortDialogVisible: false,
isAddFromSub: false,
showUserAttributeFields: false,
formLoading: false,
userAttributeDictId: "",
personTypeOptions: [],
personTypeNameMap: {},
tableData: [], //后台取出的数据
tableKey: "",
formData: {
id: "",
parentId: "",
parentName: "",
personType: "",
userAttributeCode: "",
type: "menu",
target: "data-pjax",
children: "false"
@@ -244,83 +275,86 @@ layout("/layouts/platform.html"){
},
openAdd: function () {
var self = this
self.isAddFromSub = false
self.addDialogVisible = true
self.parentMenu = []
self.menuOptions = []
self.$set(self, "isAddFromSub", false)
self.$set(self, "showUserAttributeFields", false)
self.$set(self, "addDialogVisible", true)
self.$set(self, "parentMenu", [])
self.$set(self, "menuOptions", [])
//表单还原为初始值
self.formData = {
self.$set(self, "formData", {
id: "",
parentId: "",
parentName: "",
personType: "",
userAttributeCode: "",
type: "menu",
target: "data-pjax",
children: "false"
}
})
if (this.$refs["addForm"]) this.$refs["addForm"].resetFields()
},
closeAddDialog() {
this.$set(this, "addDialogVisible", false)
},
closeEditDialog() {
this.$set(this, "editDialogVisible", false)
},
// 标记 USER_ATTRIBUTE 的直属子项,供编辑弹窗判断是否展示人员类型和业务编码。
markUserAttributeChildren(rows, parentCode) {
const children = rows || []
children.forEach((item) => {
this.$set(item, "isUserAttributeChild", parentCode === "USER_ATTRIBUTE")
})
return children
},
doAdd: function () {
var self = this
var url = base + "/platform/sys/dict/addDo"
self.$refs["addForm"].validate(function (valid) {
if (valid) {
//表单验证通过
if (!self.isAddFromSub) {
//如果不是添加子菜单,则从级联选择框取父节点ID
self.formData.parentId = self.parentMenu[self.parentMenu.length - 1] || ""
}
$.post(
url,
self.formData,
function (data) {
if (data.code == 0) {
self.$message({
message: data.msg,
type: "success"
})
self.addDialogVisible = false
self.loadChildByExpandedKeys()
// self.initTreeTable();
} else {
self.$message({
message: data.msg,
type: "error"
})
}
},
"json"
)
self.$refs["addForm"].validate((valid) => {
if (!valid) {
return
}
// 顶级或级联新增时,从级联选择结果获取父节点;子级快捷新增沿用当前父节点。
if (!self.isAddFromSub) {
self.$set(self.formData, "parentId", self.parentMenu[self.parentMenu.length - 1] || "")
}
self.$set(self, "formLoading", true)
$.post(url, self.formData)
.then((data) => {
if (data.code === 0) {
self.$message({ message: data.msg, type: "success" })
self.$set(self, "addDialogVisible", false)
self.loadChildByExpandedKeys()
} else {
self.$message({ message: data.msg, type: "error" })
}
})
.always(() => {
self.$set(self, "formLoading", false)
})
})
},
doEdit: function () {
var self = this
var url = base + "/platform/sys/dict/editDo"
self.$refs["editForm"].validate(function (valid) {
if (valid) {
//表单验证通过
$.post(
url,
self.formData,
function (data) {
if (data.code == 0) {
self.$message({
message: data.msg,
type: "success"
})
self.editDialogVisible = false
self.loadChildByExpandedKeys()
// self.initTreeTable()
} else {
self.$message({
message: data.msg,
type: "error"
})
}
},
"json"
)
self.$refs["editForm"].validate((valid) => {
if (!valid) {
return
}
self.$set(self, "formLoading", true)
$.post(url, self.formData)
.then((data) => {
if (data.code === 0) {
self.$message({ message: data.msg, type: "success" })
self.$set(self, "editDialogVisible", false)
self.loadChildByExpandedKeys()
} else {
self.$message({ message: data.msg, type: "error" })
}
})
.always(() => {
self.$set(self, "formLoading", false)
})
})
},
initTreeTable: function () {
@@ -331,18 +365,22 @@ layout("/layouts/platform.html"){
{ pid: "" },
function (data) {
if (data.code == 0) {
self.tableData = data.data
self.tableKey = +new Date()
self.$set(self, "tableData", data.data)
self.$set(self, "tableKey", +new Date())
const userAttributeRoot = (data.data || []).find((item) => item.code === "USER_ATTRIBUTE")
self.$set(self, "userAttributeDictId", userAttributeRoot ? userAttributeRoot.id : "")
}
},
"json"
)
},
loadChild: function (tree, treeNode, resolve) {
var self = this
// 在之前声明的全局变量中,增加一个key为 本条数据的id,id可替换为你数据中的任意唯一值
this.tableTreeRefreshTool[tree.id] = {}
// 重要!保存resolve方法,以便后续使用
this.tableTreeRefreshTool[tree.id].resolve = resolve
this.tableTreeRefreshTool[tree.id].parentCode = tree.code
// 记录展开次数,具体作用后续介绍
this.tableTreeRefreshTool[tree.id].expandCount = 0
var url = base + "/platform/sys/dict/child"
@@ -351,7 +389,8 @@ layout("/layouts/platform.html"){
{ pid: tree.id },
function (data) {
if (data.code == 0) {
resolve(data.data)
// 直接根据当前展开的父字典识别直属子项,避免依赖顶级字典异步初始化结果。
resolve(self.markUserAttributeChildren(data.data, tree.code))
}
},
"json"
@@ -362,9 +401,10 @@ layout("/layouts/platform.html"){
var self = this
if ("add" == command.type) {
self.openAdd()
self.formData.parentId = command.id
self.formData.parentName = command.name
self.isAddFromSub = true
self.$set(self.formData, "parentId", command.id)
self.$set(self.formData, "parentName", command.name)
self.$set(self, "isAddFromSub", true)
self.$set(self, "showUserAttributeFields", command.row && command.row.code === "USER_ATTRIBUTE")
}
if ("edit" == command.type) {
$.post(
@@ -372,8 +412,9 @@ layout("/layouts/platform.html"){
{},
function (data) {
if (data.code == 0) {
self.formData = data.data //加载后台表单数据
self.editDialogVisible = true //打开编辑窗口
self.$set(self, "formData", data.data) //加载后台表单数据
self.$set(self, "showUserAttributeFields", Boolean(command.row && command.row.isUserAttributeChild))
self.$set(self, "editDialogVisible", true) //打开编辑窗口
} else {
self.$message({
message: data.msg,
@@ -460,15 +501,28 @@ layout("/layouts/platform.html"){
const curr = this.tableTreeRefreshTool[v]
// api请求
$.get("/platform/sys/dict/child", { pid: v }).then((res) => {
curr.resolve(res.data)
curr.resolve(this.markUserAttributeChildren(res.data, curr.parentCode))
})
})
} else {
this.initTreeTable()
}
},
// 加载启用的人员类型,同时生成列表展示使用的编码名称映射。
loadPersonTypes() {
return this.$businessTool.getDictOptions("USER_PERSON_TYPE").then((options) => {
const rows = options || []
const nameMap = {}
rows.forEach((item) => {
nameMap[item.code] = item.name
})
this.$set(this, "personTypeOptions", rows)
this.$set(this, "personTypeNameMap", nameMap)
})
}
},
created: function () {
created() {
this.loadPersonTypes()
this.initTreeTable()
}
})
@@ -70,12 +70,26 @@ const home = {
<div class="union-news-panel">
<div class="union-news-panel__head">
<div class="union-news-panel__title">工会动态</div>
<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__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 class="union-news-viewport" ref="spreadViewport">
<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"
@@ -98,12 +112,16 @@ const home = {
<div class="union-news-list" v-else-if="spreadDynamics.length">
<div class="union-news-item"
v-for="item in spreadDynamics"
:key="item.id"
:key="item.href || item.title"
@click="enterSpreadDetail(item)">
<img v-if="getSpreadCover(item.thumbUrl)"
<img v-if="item.thumbUrl && !item.imageFailed"
class="union-news-item__cover"
:src="getSpreadCover(item.thumbUrl)"
:src="item.thumbUrl" referrerpolicy="no-referrer"
@error="$set(item, 'imageFailed', true)"
: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"
@@ -117,9 +135,10 @@ const home = {
</div>
<div class="union-news-item__meta">
<span>{{ formatSpreadDate(item.publishedAt || item.createdAt) }}</span>
<span class="union-news-item__views">
<span v-if="item.viewCount !== null && item.viewCount !== undefined"
class="union-news-item__views">
<van-icon name="eye-o"></van-icon>
{{ item.viewCount || 0 }}
{{ item.viewCount }}
</span>
</div>
</div>
@@ -131,6 +150,7 @@ const home = {
alt="暂无工会动态">
<span class="union-news-empty__text">暂无工会动态</span>
</div>
</div>
</div>
<div class="classroom-section">
@@ -247,6 +267,10 @@ const home = {
classroomRecommendOptions: [],
showHomeSummary: false,
homeScrollContainer: null,
spreadSections: [],
spreadMenuVisible: false,
spreadHistoryToken: null,
spreadActiveIndex: 0,
spreadDynamics: [],
spreadLoading: true,
spreadLoadSeq: 0,
@@ -311,6 +335,10 @@ const home = {
}
},
computed: {
spreadHasImages() {
// 按当前栏目统一保留图片列;异步补图及加载失败都会重新计算,全部无图时文字占满行宽。
return this.spreadDynamics.some(item => item.thumbUrl && !item.imageFailed)
},
currentBanner() {
return this.bannerList[this.activeBannerIndex] || this.bannerList[0] || {}
}
@@ -516,18 +544,29 @@ const home = {
listSpreadHomeContent() {
const loadSeq = ++this.spreadLoadSeq
this.spreadLoading = true
this.$axios.post("/platform/h5/spread/pageData", {
sortType: "latest",
pageNumber: 1,
pageSize: 10
}).then((res) => {
this.$axios.post("/platform/home/getNews").then((res) => {
if (loadSeq === this.spreadLoadSeq && res.code === 0) {
const data = res.data || {}
this.spreadDynamics = data.list || []
// 接口按栏目返回 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)
}
}).catch(() => {
if (loadSeq === this.spreadLoadSeq) {
this.spreadDynamics = []
this.$set(this, "spreadSections", [])
this.$set(this, "spreadDynamics", [])
}
}).finally(() => {
if (loadSeq === this.spreadLoadSeq) {
@@ -535,6 +574,46 @@ const home = {
}
})
},
selectSpreadSection(index) {
// 切换栏目时复位列表滚动位置;固定视口避免不同数据量引发页面跳动。
// 新闻样式锚定 home-page 根节点作用域,动态行无需等待 styleMixin 异步补属性。
const section = this.spreadSections[index] || {}
this.$set(this, "spreadActiveIndex", index)
this.$set(this, "spreadDynamics", section.value || [])
if (this.$refs.spreadViewport) this.$refs.spreadViewport.scrollTop = 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] : [])
@@ -561,13 +640,29 @@ const home = {
return first && (first.src || first.url || first.data || (first.response && first.response.data)) || ""
},
formatSpreadDate(value) {
return value ? this.$moment(Number(value)).format("YYYY-MM-DD") : ""
if (!value) {
return ""
}
const numberValue = Number(value)
return Number.isNaN(numberValue) ? String(value) : this.$moment(numberValue).format("YYYY-MM-DD")
},
enterSpreadDetail(item) {
this.$pjaxReplace("/platform/h5/spread/detail?id=" + encodeURIComponent(item.id) + "&from=home")
const url = this.resolveUnionNewsUrl(item.href)
if (url) {
window.location.href = url
}
},
enterSpreadList() {
this.$pjaxReplace("/platform/h5/spread")
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
},
menuClick(item) {
this.$pjaxReplace(item.href)
@@ -606,6 +701,10 @@ const home = {
}
},
created() {
// 注册弹层返回回调,关闭状态由历史回退同步,避免留下多余历史记录。
this.spreadHistoryToken = window.h5PopupHistory.register(() => {
this.$set(this, "spreadMenuVisible", false)
})
this.listHomeBrand()
this.listRecommendApp()
this.listClassroomCourses()
@@ -623,6 +722,8 @@ 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)
}
@@ -1383,7 +1484,8 @@ const home = {
font-size: 14px;
}
.union-news-panel {
.home-page/deep/ .union-news-panel {
position: relative;
margin: 15px 12px 7px;
padding: 13px 12px 8px;
border-radius: 10px;
@@ -1391,21 +1493,29 @@ const home = {
box-sizing: border-box;
}
.union-news-panel__head {
.home-page/deep/ .union-news-panel__head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
}
.union-news-panel__title {
.home-page/deep/ .union-news-panel__title {
position: relative;
z-index: 42;
padding: 0;
border: 0;
background: transparent;
display: inline-flex;
align-items: center;
gap: 6px;
color: #202124;
font-size: 15px;
line-height: 21px;
font-weight: 600;
}
.union-news-panel__more {
.home-page/deep/ .union-news-panel__more {
display: flex;
align-items: center;
color: #969799;
@@ -1413,32 +1523,68 @@ const home = {
line-height: 18px;
}
.union-news-panel__more .van-icon {
.home-page/deep/ .union-news-panel__more .van-icon {
margin-left: 4px;
font-size: 13px;
}
.union-news-list {
.home-page/deep/ .union-news-menu-dismiss {
position: fixed;
inset: 0;
z-index: 40;
background: transparent;
}
.home-page/deep/ .union-news-menu {
position: absolute;
top: 40px;
left: 12px;
width: 124px;
padding: 5px;
z-index: 43;
border: 1px solid #e8edf3;
border-radius: 8px;
background: #fff;
box-shadow: 0 5px 16px rgba(35, 55, 80, .12);
}
.home-page/deep/ .union-news-menu button {
width: 100%;
height: 34px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 9px;
border: 0;
border-radius: 4px;
background: #fff;
color: #444;
font-size: 13px;
}
.home-page/deep/ .union-news-menu button.is-selected {
color: #1989fa;
background: #f0f7ff;
}
.home-page/deep/ .union-news-list {
margin-top: 7px;
padding-bottom: 3px;
display: flex;
gap: 10px;
overflow-x: auto;
overflow-y: hidden;
display: block;
scrollbar-width: none;
-webkit-overflow-scrolling: touch;
}
.union-news-list::-webkit-scrollbar {
.home-page/deep/ .union-news-list::-webkit-scrollbar {
display: none;
}
.union-news-item--skeleton {
.home-page/deep/ .union-news-item--skeleton {
height: 96px;
min-height: 96px;
}
.union-news-skeleton__cover {
.home-page/deep/ .union-news-skeleton__cover {
width: 80px;
height: 48px;
margin-right: 10px;
@@ -1446,65 +1592,93 @@ const home = {
border-radius: 4px;
}
.union-news-skeleton__content {
.home-page/deep/ .union-news-skeleton__content {
min-width: 0;
flex: 1;
}
.union-news-skeleton__tags { height: 15px; margin-bottom: 4px; display: flex; gap: 4px; }
.union-news-skeleton__tag { width: 48px; height: 15px; border-radius: 999px; }
.union-news-skeleton__tag--short { width: 38px; }
.union-news-skeleton__line { height: 10px; border-radius: 3px; }
.union-news-skeleton__line--title { width: 88%; }
.union-news-skeleton__line--summary { width: 72%; margin-top: 7px; }
.union-news-skeleton__meta { margin-top: 8px; display: flex; align-items: center; justify-content: space-between; }
.union-news-skeleton__meta span { width: 55px; height: 8px; border-radius: 3px; }
.union-news-skeleton__meta span:last-child { width: 28px; }
.home-page/deep/ .union-news-skeleton__tags { height: 15px; margin-bottom: 4px; display: flex; gap: 4px; }
.home-page/deep/ .union-news-skeleton__tag { width: 48px; height: 15px; border-radius: 999px; }
.home-page/deep/ .union-news-skeleton__tag--short { width: 38px; }
.home-page/deep/ .union-news-skeleton__line { height: 10px; border-radius: 3px; }
.home-page/deep/ .union-news-skeleton__line--title { width: 88%; }
.home-page/deep/ .union-news-skeleton__line--summary { width: 72%; margin-top: 7px; }
.home-page/deep/ .union-news-skeleton__meta { margin-top: 8px; display: flex; align-items: center; justify-content: space-between; }
.home-page/deep/ .union-news-skeleton__meta span { width: 55px; height: 8px; border-radius: 3px; }
.home-page/deep/ .union-news-skeleton__meta span:last-child { width: 28px; }
.union-news-item {
width: calc(100% - 32px);
min-width: calc(100% - 32px);
min-height: 96px;
padding: 10px;
.home-page/deep/ .union-news-item {
width: 100%;
min-width: 0;
height: 96px;
padding: 12px 0;
flex-shrink: 0;
display: flex;
align-items: center;
border: 1px solid #edf1f6;
border-radius: 9px;
border: 0;
border-radius: 0;
background-color: #fff;
box-sizing: border-box;
}
.union-news-item + .union-news-item {
.home-page/deep/ .union-news-item + .union-news-item {
border-top: 1px solid #edf1f6;
}
.union-news-item:active {
.home-page/deep/ .union-news-item:active {
background-color: #f7faff;
}
.union-news-item__cover {
.home-page/deep/ .union-news-item__cover {
width: 80px;
height: 48px;
height: 60px;
margin-right: 10px;
flex-shrink: 0;
border-radius: 4px;
object-fit: cover;
}
.union-news-item__content {
.home-page/deep/ .union-news-viewport {
height: 288px;
overflow-y: auto;
overflow-x: hidden;
overscroll-behavior: contain;
overflow-anchor: none;
}
.home-page/deep/ .union-news-item__placeholder {
display: flex;
align-items: center;
justify-content: center;
background: #f0f4f8;
color: #9aaabe;
font-size: 26px;
}
.home-page/deep/ .union-news-empty {
height: 100%;
box-sizing: border-box;
justify-content: center;
}
.home-page/deep/ .union-news-empty__image {
max-height: 200px;
object-fit: contain;
}
.home-page/deep/ .union-news-item__content {
min-width: 0;
flex: 1;
}
.union-news-item__tags {
.home-page/deep/ .union-news-item__tags {
margin-bottom: 4px;
display: flex;
gap: 4px;
overflow: hidden;
}
.union-news-item__tag {
.home-page/deep/ .union-news-item__tag {
max-width: 46%;
padding: 1px 6px;
overflow: hidden;
@@ -1517,12 +1691,12 @@ const home = {
background-color: #eaf4ff;
}
.union-news-item__tag--secondary {
.home-page/deep/ .union-news-item__tag--secondary {
color: #627ea4;
background-color: #f0f4f9;
}
.union-news-item__title {
.home-page/deep/ .union-news-item__title {
overflow: hidden;
color: #202124;
font-size: 13px;
@@ -1532,7 +1706,7 @@ const home = {
white-space: nowrap;
}
.union-news-item__summary {
.home-page/deep/ .union-news-item__summary {
margin-top: 2px;
overflow: hidden;
color: #7d8797;
@@ -1542,7 +1716,7 @@ const home = {
white-space: nowrap;
}
.union-news-item__meta {
.home-page/deep/ .union-news-item__meta {
margin-top: 3px;
display: flex;
align-items: center;
@@ -1552,17 +1726,17 @@ const home = {
line-height: 14px;
}
.union-news-item__views {
.home-page/deep/ .union-news-item__views {
display: inline-flex;
align-items: center;
}
.union-news-item__views .van-icon {
.home-page/deep/ .union-news-item__views .van-icon {
margin-right: 3px;
font-size: 11px;
}
.union-news-empty {
.home-page/deep/ .union-news-empty {
display: flex;
padding: 12px 0 16px;
flex-direction: column;
@@ -1573,7 +1747,7 @@ const home = {
text-align: center;
}
.union-news-empty__image {
.home-page/deep/ .union-news-empty__image {
display: block;
width: 82%;
max-width: 280px;
@@ -1581,7 +1755,7 @@ const home = {
object-fit: contain;
}
.union-news-empty__text {
.home-page/deep/ .union-news-empty__text {
margin-top: 2px;
}