企业微信消息和oa待办
This commit is contained in:
@@ -30,6 +30,7 @@ import java.util.Base64;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
@@ -138,8 +139,10 @@ public class SchoolOaTodoServiceImpl implements SchoolOaTodoService {
|
||||
JSONObject result = execute(HttpRequest.get(endpoint).header("Authorization", "Bearer " + getAccessToken()), "待办同步");
|
||||
Integer errorCode = result.getInt("errcode");
|
||||
if (errorCode == null || errorCode != 0) {
|
||||
// 远端消息可能回显密钥或请求地址,不把原始响应直接输出到日志和页面。
|
||||
throw new BaseException("通达OA待办同步失败:errcode={},taskId={}", errorCode, task.getId());
|
||||
String detail = responseError(result);
|
||||
log.error("通达OA待办业务失败,taskId={},instanceId={},status={},errcode={},detail={}",
|
||||
task.getId(), task.getProcessInstanceId(), status, errorCode, detail);
|
||||
throw new BaseException("通达OA待办同步失败:errcode={},taskId={},{}", errorCode, task.getId(), detail);
|
||||
}
|
||||
log.info("通达OA接受待办同步请求,taskId={},status={}", task.getId(), status);
|
||||
}
|
||||
@@ -188,44 +191,129 @@ public class SchoolOaTodoServiceImpl implements SchoolOaTodoService {
|
||||
String password = conf.get(PREFIX + "token-password", "password");
|
||||
String currentConfig = JSONUtil.toJsonStr(List.of(base, clientId, secret, username, password));
|
||||
long now = System.currentTimeMillis();
|
||||
if (currentConfig.equals(tokenConfig) && StrUtil.isNotBlank(accessToken) && now < tokenExpiresAt) return accessToken;
|
||||
if (currentConfig.equals(tokenConfig) && StrUtil.isNotBlank(accessToken) && now < tokenExpiresAt) {
|
||||
// 只记录缓存剩余有效期,便于区分复用与重新获取,不输出凭据。
|
||||
log.info("通达OA复用token缓存,remainingSeconds={}", (tokenExpiresAt - now) / 1000L);
|
||||
return accessToken;
|
||||
}
|
||||
Map<String, Object> form = new LinkedHashMap<>();
|
||||
form.put("grant_type", "password");
|
||||
form.put("client_id", clientId);
|
||||
form.put("client_secret", secret);
|
||||
form.put("username", username);
|
||||
form.put("password", password);
|
||||
long startedAt = System.nanoTime();
|
||||
JSONObject result = execute(HttpRequest.post(base + "/oauth2/token")
|
||||
.header("Content-Type", "application/x-www-form-urlencoded")
|
||||
.body(encodeParams(form)), "获取凭据");
|
||||
String token = result.getStr("access_token");
|
||||
Long expires = result.getLong("expires_in");
|
||||
if (StrUtil.isBlank(token) || expires == null || expires <= 0 || expires > Integer.MAX_VALUE) {
|
||||
throw new BaseException("通达OA获取凭据失败:缺少有效的 access_token 或 expires_in");
|
||||
String detail = responseError(result);
|
||||
log.error("通达OA获取凭据业务失败,缺少有效的 access_token 或 expires_in,detail={}", detail);
|
||||
throw new BaseException("通达OA获取凭据失败:缺少有效的 access_token 或 expires_in,{}", detail);
|
||||
}
|
||||
accessToken = token;
|
||||
tokenConfig = currentConfig;
|
||||
tokenExpiresAt = now + (expires - Math.min(60, expires / 10)) * 1000L;
|
||||
log.info("通达OA获取token成功,elapsedMs={},expiresInSeconds={}",
|
||||
(System.nanoTime() - startedAt) / 1_000_000, expires);
|
||||
return token;
|
||||
}
|
||||
|
||||
/** HTTP 成功不代表业务成功;本方法只验证状态码和 JSON,业务码由调用处校验。 */
|
||||
private JSONObject execute(HttpRequest request, String action) {
|
||||
try (HttpResponse response = request.timeout(conf.getInt(PREFIX + "timeout-ms", 10000)).execute()) {
|
||||
long startedAt = System.nanoTime();
|
||||
int timeout = conf.getInt(PREFIX + "timeout-ms", 10000);
|
||||
// 只记录接口地址,不记录 GET 查询参数、请求头或 token 请求正文。
|
||||
String endpoint = request.getUrl().split("[?#]", 2)[0];
|
||||
try (HttpResponse response = request.timeout(timeout).execute()) {
|
||||
String body = response.body();
|
||||
boolean jsonObject = JSONUtil.isTypeJSONObject(body);
|
||||
// 非 JSON 错误常为 OA 或网关的 HTML 页面;仅在日志保留脱敏摘要,不把页面返回给前端。
|
||||
if (!jsonObject) {
|
||||
log.error("通达OA非JSON响应,action={},method={},endpoint={},httpStatus={},contentType={},bodySummary={}",
|
||||
action, request.getMethod(), endpoint, response.getStatus(),
|
||||
redact(response.header("Content-Type")), responseBodySummary(body));
|
||||
}
|
||||
if (!response.isOk()) {
|
||||
if (response.getStatus() == 401) invalidateToken();
|
||||
throw new BaseException("通达OA{}失败:HTTP {}", action, response.getStatus());
|
||||
String detail = jsonObject
|
||||
? responseError(JSONUtil.parseObj(body)) : "响应不是JSON对象,脱敏响应摘要请查看服务端日志";
|
||||
throw new BaseException("通达OA{}失败:HTTP {},{}", action, response.getStatus(), detail);
|
||||
}
|
||||
if (!JSONUtil.isTypeJSONObject(response.body())) throw new BaseException("通达OA{}失败:响应不是JSON对象", action);
|
||||
return JSONUtil.parseObj(response.body());
|
||||
} catch (BaseException e) {
|
||||
throw e;
|
||||
if (!jsonObject) throw new BaseException("通达OA{}失败:响应不是JSON对象,脱敏响应摘要请查看服务端日志", action);
|
||||
return JSONUtil.parseObj(body);
|
||||
} catch (Exception e) {
|
||||
long elapsed = (System.nanoTime() - startedAt) / 1_000_000;
|
||||
Throwable root = e;
|
||||
Map<Throwable, Boolean> visited = new IdentityHashMap<>();
|
||||
while (root.getCause() != null && visited.put(root, Boolean.TRUE) == null
|
||||
&& !visited.containsKey(root.getCause())) root = root.getCause();
|
||||
String detail = root.getClass().getName() + ": " + redact(root.getMessage());
|
||||
// 保留各层原因、原始栈帧及 suppressed 异常;先复制脱敏,避免全局异常处理器泄露凭证。
|
||||
Throwable safeCause = diagnosticCause(e, new IdentityHashMap<>());
|
||||
log.error("通达OA请求失败,action={},method={},endpoint={},elapsedMs={},timeoutMs={},rootCause={}",
|
||||
action, request.getMethod(), endpoint, elapsed, timeout, detail, safeCause);
|
||||
// 不自动重放写请求,超时可能发生在远端已成功之后;避免重复待办。
|
||||
throw new BaseException("通达OA{}异常({}),请核对远端结果后再重试", action, e.getClass().getSimpleName());
|
||||
BaseException failure = new BaseException("通达OA{}失败,耗时{}ms:{};请核对远端结果后再重试", action, elapsed, detail);
|
||||
failure.initCause(safeCause);
|
||||
throw failure;
|
||||
}
|
||||
}
|
||||
|
||||
/** body 为远端非 JSON 正文;先脱敏再截取最多 2000 字符,避免截断凭据后无法匹配脱敏。 */
|
||||
private String responseBodySummary(String body) {
|
||||
if (StrUtil.isBlank(body)) return "[空响应正文]";
|
||||
String safe = redact(body);
|
||||
return safe.length() > 2000 ? safe.substring(0, 2000) + "[已截断]" : safe;
|
||||
}
|
||||
|
||||
/** 只提取接口错误字段,不输出含 access_token、refresh_token 等内容的整个响应。 */
|
||||
private String responseError(JSONObject response) {
|
||||
Map<String, Object> errors = new LinkedHashMap<>();
|
||||
for (String key : List.of("error", "error_description", "errcode", "errmsg", "message")) {
|
||||
if (response.containsKey(key)) errors.put(key, redact(String.valueOf(response.get(key))));
|
||||
}
|
||||
return errors.isEmpty() ? "OA未返回错误说明" : JSONUtil.toJsonStr(errors);
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制异常诊断信息,message 标注原异常类型,保留原始栈帧和原因链。
|
||||
* visited 防止第三方异常存在循环引用;原异常不修改,也不直接写入日志。
|
||||
*/
|
||||
private Throwable diagnosticCause(Throwable source, Map<Throwable, Throwable> visited) {
|
||||
if (visited.containsKey(source)) return visited.get(source);
|
||||
RuntimeException copy = new RuntimeException(source.getClass().getName() + ": " + redact(source.getMessage()));
|
||||
visited.put(source, copy);
|
||||
copy.setStackTrace(source.getStackTrace());
|
||||
if (source.getCause() != null && !visited.containsKey(source.getCause())) {
|
||||
copy.initCause(diagnosticCause(source.getCause(), visited));
|
||||
}
|
||||
for (Throwable suppressed : source.getSuppressed()) {
|
||||
if (!visited.containsKey(suppressed)) copy.addSuppressed(diagnosticCause(suppressed, visited));
|
||||
}
|
||||
return copy;
|
||||
}
|
||||
|
||||
/** 清除已知凭证及可能被远端回显的认证字段、URL查询参数,并移除日志换行。 */
|
||||
private String redact(String message) {
|
||||
if (message == null) return "无异常说明";
|
||||
String text = message;
|
||||
for (String key : List.of("client-secret-encrypted", "token-password", "client-id")) {
|
||||
String value = conf.get(PREFIX + key);
|
||||
if (StrUtil.isNotBlank(value)) {
|
||||
text = text.replace(value, "[REDACTED]")
|
||||
.replace(URLEncoder.encode(value, StandardCharsets.UTF_8), "[REDACTED]");
|
||||
}
|
||||
}
|
||||
if (StrUtil.isNotBlank(accessToken)) text = text.replace(accessToken, "[REDACTED]");
|
||||
text = text.replaceAll("(?i)Bearer\\s+[^\\s,;\\\"']+", "Bearer [REDACTED]");
|
||||
text = text.replaceAll("(?i)((?:client_secret|access_token|refresh_token|password|authorization)[\\\"']?\\s*[:=]\\s*)[\\\"']?[^\\s&,;}\\\"']+[\\\"']?", "$1[REDACTED]");
|
||||
text = text.replaceAll("(https?://[^\\s?#]+)\\?[^\\s]+", "$1?[REDACTED]");
|
||||
return text.replace('\r', ' ').replace('\n', ' ');
|
||||
}
|
||||
|
||||
private synchronized void invalidateToken() {
|
||||
accessToken = null;
|
||||
tokenExpiresAt = 0;
|
||||
|
||||
@@ -189,25 +189,16 @@ public class WkFailProcessor extends ViewProcessor {
|
||||
if (Globals.sso) {
|
||||
//1.判断是否为企业微信浏览器并且对接了企业微信
|
||||
if (browserPlatform == BrowserPlatform.QI_YE_WEI_XIN && propertiesProxy.getBoolean("wxwork.enable", false)) {
|
||||
//进行企业微信授权
|
||||
// String token = getWxWorkToken();
|
||||
// if (StrUtil.isNotBlank(token)) {
|
||||
// //跳转到企业微信授权地址
|
||||
// new ServerRedirectView(buildWxWorkOauth2Url()).render(request, response, null);
|
||||
// } else {
|
||||
// //跳转到错误页 别问我为啥错了 我也不知道 首先去检查配置文件里的参数有没有写对
|
||||
//// new ServerRedirectView(Globals.AppDomain + errorUri).render(request, response, null);
|
||||
// //跳转到cas登录页
|
||||
// String callBackUrl = propertiesProxy.get("cas.client-host-url") + propertiesProxy.get("cas.client-call-back-url") + "?redirect=" + redirect;
|
||||
// String authUrl = propertiesProxy.get("cas.qi-wx-server-login-url") + "?service=" + URLEncoder.encode(callBackUrl, StandardCharsets.UTF_8);
|
||||
// new ServerRedirectView(authUrl).render(request, response, null);
|
||||
// }
|
||||
|
||||
// 武汉城市职业学院 跳转到企业微信cas登录页
|
||||
String callBackUrl = propertiesProxy.get("wecom.wecom-client-host-url") + propertiesProxy.get("wecom.wecom-client-call-back-url") + "?redirect=" + redirect;
|
||||
String authUrl = propertiesProxy.get("wecom.wecom-server-login-url") + "?service=" + URLEncoder.encode(callBackUrl, StandardCharsets.UTF_8);
|
||||
log.info("auth:"+authUrl);
|
||||
new ServerRedirectView(authUrl).render(request, response, null);
|
||||
// 专用企业微信认证配置缺失时整组回退到已接通的 CAS,保留卡片原始页面。
|
||||
try {
|
||||
String authUrl = buildEnterpriseCasUrl(redirect);
|
||||
log.info("auth:" + authUrl);
|
||||
new ServerRedirectView(authUrl).render(request, response, null);
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.error("企业微信登录跳转配置无效:" + e.getMessage());
|
||||
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
|
||||
"统一身份认证配置不完整或无效,请联系管理员检查认证地址及回调地址");
|
||||
}
|
||||
} else if (browserPlatform == BrowserPlatform.WEI_XIN && propertiesProxy.getBoolean("wx.enable", false)) {
|
||||
//进行微信公众号授权
|
||||
//跳转到微信授权页
|
||||
@@ -225,6 +216,53 @@ public class WkFailProcessor extends ViewProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造企业微信内的 CAS 登录地址。redirect 为已 URL 编码的原始站内路径及查询参数;
|
||||
* 返回携带 service 参数的完整认证 URL,认证回调通过 redirect 返回卡片页面。
|
||||
* 三个 wecom 配置必须同时存在,否则整体使用 cas 配置,避免混用两套认证平台。
|
||||
*/
|
||||
private String buildEnterpriseCasUrl(String redirect) {
|
||||
String loginUrl = propertiesProxy.get("wecom.wecom-server-login-url");
|
||||
String clientHost = propertiesProxy.get("wecom.wecom-client-host-url");
|
||||
String callbackPath = propertiesProxy.get("wecom.wecom-client-call-back-url");
|
||||
if (missingAuthConfig(loginUrl) || missingAuthConfig(clientHost) || missingAuthConfig(callbackPath)) {
|
||||
loginUrl = propertiesProxy.get("cas.server-login-url");
|
||||
clientHost = propertiesProxy.get("cas.client-host-url");
|
||||
callbackPath = propertiesProxy.get("cas.client-call-back-url");
|
||||
}
|
||||
if (missingAuthConfig(loginUrl) || missingAuthConfig(clientHost) || missingAuthConfig(callbackPath)) {
|
||||
throw new IllegalArgumentException("CAS 登录地址、系统域名或回调路径为空");
|
||||
}
|
||||
loginUrl = loginUrl.trim();
|
||||
clientHost = clientHost.trim();
|
||||
callbackPath = callbackPath.trim();
|
||||
validateAuthUrl(loginUrl);
|
||||
validateAuthUrl(clientHost);
|
||||
// 回调必须是本系统路径,不能被绝对 URL 或双斜线路径覆盖到其他主机。
|
||||
if (!callbackPath.startsWith("/") || callbackPath.startsWith("//") || callbackPath.contains("#")) {
|
||||
throw new IllegalArgumentException("认证回调必须是以单斜线开头的站内路径");
|
||||
}
|
||||
String callback = clientHost.replaceAll("/+$", "") + callbackPath
|
||||
+ (callbackPath.contains("?") ? "&" : "?") + "redirect=" + redirect;
|
||||
// 保留部署配置中的 HTTP/HTTPS 协议;工会 HTTPS 回调不会被改为 HTTP。
|
||||
return loginUrl + (loginUrl.contains("?") ? "&" : "?") + "service="
|
||||
+ URLEncoder.encode(callback, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
/** 空值和字面量 null 都视为未配置,防止拼接出浏览器相对地址 null?service=...。 */
|
||||
private boolean missingAuthConfig(String value) {
|
||||
return StrUtil.isBlank(value) || "null".equalsIgnoreCase(value.trim());
|
||||
}
|
||||
|
||||
/** 认证地址和回调域名必须为完整 HTTP(S) URL,拒绝无主机名、片段或用户信息的地址。 */
|
||||
private void validateAuthUrl(String value) {
|
||||
java.net.URI uri = java.net.URI.create(value);
|
||||
if (!("https".equalsIgnoreCase(uri.getScheme()) || "http".equalsIgnoreCase(uri.getScheme()))
|
||||
|| uri.getHost() == null || uri.getFragment() != null || uri.getUserInfo() != null) {
|
||||
throw new IllegalArgumentException("认证配置不是有效的 HTTP(S) 地址");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断浏览器平台
|
||||
*
|
||||
|
||||
@@ -482,7 +482,8 @@
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
height: 100%;
|
||||
max-width: 310px;
|
||||
/* 用户区按完整姓名、账号及操作按钮的实际宽度布局,避免退出按钮溢出后被顶栏裁切。 */
|
||||
width: max-content;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@@ -618,6 +619,26 @@
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* 常见笔记本宽度下压缩导航留白,为完整用户信息与退出按钮腾出空间。 */
|
||||
@media (min-width: 1025px) and (max-width: 1440px) {
|
||||
.v4-nav {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.v4-nav-item {
|
||||
padding: 0 8px;
|
||||
column-gap: 6px;
|
||||
}
|
||||
|
||||
.v4-user-section {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.v4-user-info {
|
||||
padding: 0 6px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.v4-nav {
|
||||
gap: 24px;
|
||||
|
||||
@@ -342,7 +342,6 @@ layout("/layouts/platform.html"){
|
||||
officialCapacity: [{required: true, message: '请填写职称', trigger: ['blur', 'change']}],
|
||||
subsidyStandards: [{required: true, message: '请选择困难类型', trigger: ['blur', 'change']}],
|
||||
reason: [{required: true, message: '请填写申请原因', trigger: ['blur', 'change']}],
|
||||
sign: [{required: true, message: '请签字', trigger: ['blur', 'change']}]
|
||||
},
|
||||
|
||||
// 替他人申请相关
|
||||
|
||||
+1
-1
@@ -55,7 +55,7 @@ layout("/layouts/platform_h5.html"){
|
||||
|
||||
<div id="app">
|
||||
<div class="condolence-list-page">
|
||||
<van-nav-bar title="分工会主席审批" left-text="返回" left-arrow @click-left="historyBack" fixed placeholder></van-nav-bar>
|
||||
<van-nav-bar title="校工会主席审批" left-text="返回" left-arrow @click-left="historyBack" fixed placeholder></van-nav-bar>
|
||||
|
||||
<van-sticky offset-top="46px">
|
||||
<div class="condolence-list-sticky">
|
||||
|
||||
+26
-9
@@ -9,7 +9,7 @@ layout("/layouts/platform_h5.html"){
|
||||
</style>
|
||||
<div id="app">
|
||||
<div class="difficult-help-approval-page">
|
||||
<van-nav-bar title="帮扶分工会审核" left-text="返回" left-arrow @click-left="historyBack" placeholder fixed></van-nav-bar>
|
||||
<van-nav-bar title="校工会办公室主任审批" left-text="返回" left-arrow @click-left="historyBack" placeholder fixed></van-nav-bar>
|
||||
<van-sticky offset-top="46px">
|
||||
<div class="difficult-help-approval-sticky">
|
||||
<van-search
|
||||
@@ -29,7 +29,7 @@ layout("/layouts/platform_h5.html"){
|
||||
:close-on-click-overlay="false"
|
||||
>
|
||||
<van-dropdown-item
|
||||
v-if="$auth.hasRoleOr('SYSADMIN,SCHOOL_UNION_ADMIN,BRANCH_UNION_CHAIRMAN')"
|
||||
v-if="filterUserReady && $auth.hasRoleOr('SYSADMIN,SCHOOL_UNION_ADMIN,BRANCH_UNION_CHAIRMAN')"
|
||||
:options="unionList"
|
||||
@change="unionChange"
|
||||
@open="onFilterOpen"
|
||||
@@ -37,7 +37,7 @@ layout("/layouts/platform_h5.html"){
|
||||
v-model="pageForm.unionId">
|
||||
</van-dropdown-item>
|
||||
<van-dropdown-item
|
||||
v-if="$auth.hasRoleOr('SYSADMIN,SCHOOL_UNION_ADMIN,BRANCH_UNION_CHAIRMAN')"
|
||||
v-if="filterUserReady && $auth.hasRoleOr('SYSADMIN,SCHOOL_UNION_ADMIN,BRANCH_UNION_CHAIRMAN')"
|
||||
:options="unitList"
|
||||
@change="doSearch"
|
||||
@open="onFilterOpen"
|
||||
@@ -228,6 +228,14 @@ layout("/layouts/platform_h5.html"){
|
||||
components: {
|
||||
"info": INFO
|
||||
},
|
||||
computed: {
|
||||
// 首次进入时用户接口可能尚未返回;非管理员还需所属工会,避免空对象报错或请求全部工会。
|
||||
filterUserReady() {
|
||||
const user = this.$store.state.user
|
||||
if (!user || !Array.isArray(user.roles)) return false
|
||||
return this.$auth.hasRoleOr(["SYSADMIN", "SCHOOL_UNION_ADMIN"]) || !!(user.union && user.union.id)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
historyBack,
|
||||
|
||||
@@ -419,13 +427,22 @@ layout("/layouts/platform_h5.html"){
|
||||
},
|
||||
},
|
||||
created() {
|
||||
// 先订阅弹框状态,工会加载异常也不能阻断等级选择器与确认框的打开、返回。
|
||||
if (window.h5HistoryLayerManager) {
|
||||
this.$set(this, "historyLayerUnsubscribe", window.h5HistoryLayerManager.subscribe((stack) => {
|
||||
this.$set(this, "confirmVisible", stack.includes("difficult-help-action-confirm"))
|
||||
this.$set(this, "showLevelPicker", stack.includes("difficult-help-level-picker"))
|
||||
}))
|
||||
}
|
||||
this.initLevelDict()
|
||||
this.initUnion().then(() => this.flushUnits())
|
||||
if (!window.h5HistoryLayerManager) return
|
||||
this.$set(this, "historyLayerUnsubscribe", window.h5HistoryLayerManager.subscribe((stack) => {
|
||||
this.$set(this, "confirmVisible", stack.includes("difficult-help-action-confirm"))
|
||||
this.$set(this, "showLevelPicker", stack.includes("difficult-help-level-picker"))
|
||||
}))
|
||||
// 缓存已就绪时立即加载,否则等待用户接口回填;只初始化一次,避免重置已选筛选条件。
|
||||
// $watch 由当前 Vue 实例管理,页面销毁时自动注销。
|
||||
let filtersInitialized = false
|
||||
this.$watch("filterUserReady", (ready) => {
|
||||
if (!ready || filtersInitialized) return
|
||||
filtersInitialized = true
|
||||
this.initUnion().then(() => this.flushUnits())
|
||||
}, {immediate: true})
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (typeof this.historyLayerUnsubscribe === "function") this.historyLayerUnsubscribe()
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ layout("/layouts/platform_h5.html"){
|
||||
</style>
|
||||
<div id="app">
|
||||
<div class="difficult-help-approval-page">
|
||||
<van-nav-bar title="帮扶校工会审核" left-text="返回" left-arrow @click-left="historyBack" placeholder fixed></van-nav-bar>
|
||||
<van-nav-bar title="分管校领导审批" left-text="返回" left-arrow @click-left="historyBack" placeholder fixed></van-nav-bar>
|
||||
<van-sticky offset-top="46px">
|
||||
<div class="difficult-help-approval-sticky">
|
||||
<van-search
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ layout("/layouts/platform_h5.html"){
|
||||
</style>
|
||||
<div id="app">
|
||||
<div class="difficult-help-approval-page">
|
||||
<van-nav-bar title="帮扶二级党组织审核" left-text="返回" left-arrow @click-left="historyBack" placeholder fixed></van-nav-bar>
|
||||
<van-nav-bar title="校工会主席审批" left-text="返回" left-arrow @click-left="historyBack" placeholder fixed></van-nav-bar>
|
||||
<van-sticky offset-top="46px">
|
||||
<div class="difficult-help-approval-sticky">
|
||||
<van-search
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
const apps = {
|
||||
// 从“我的工具”进入时指定分类名称;空值表示恢复应用中心原有浏览状态。
|
||||
props: {
|
||||
initialCategoryName: {type: String, default: ""}
|
||||
},
|
||||
template: /*language=HTML*/ `
|
||||
<div class="apps-container">
|
||||
<van-nav-bar title="应用中心" placeholder fixed></van-nav-bar>
|
||||
@@ -201,7 +205,8 @@ const apps = {
|
||||
},
|
||||
mounted() {
|
||||
// 动态分类加载完成后再恢复分类,避免把缓存的动态分类误判为“全部应用”
|
||||
this.restoreAppsState()
|
||||
// 指定入口不恢复旧搜索词、滚动位置及子菜单弹框,避免目标分类被旧状态遮挡。
|
||||
if (!this.initialCategoryName) this.restoreAppsState()
|
||||
this.loadCategories().finally(() => {
|
||||
this.restoreActiveCategory()
|
||||
this.loadApps()
|
||||
@@ -249,6 +254,13 @@ const apps = {
|
||||
},
|
||||
|
||||
restoreActiveCategory() {
|
||||
// 动态分类返回后按名称定位;未配置或无可用分类时明确提示,并展示全部应用。
|
||||
if (this.initialCategoryName) {
|
||||
const targetIndex = this.allCategories.findIndex(item => item.name === this.initialCategoryName)
|
||||
this.$set(this, "activeTab", targetIndex >= 0 ? targetIndex : 0)
|
||||
if (targetIndex < 0) this.$toast("未找到“" + this.initialCategoryName + "”分类")
|
||||
return
|
||||
}
|
||||
const index = this.allCategories.findIndex(item => String(item.id) === String(this.cachedCategoryId))
|
||||
this.activeTab = index >= 0 ? index : 0
|
||||
},
|
||||
|
||||
@@ -73,7 +73,7 @@ layout("/layouts/platform_h5.html"){
|
||||
<todo v-if="homeTabbarActive === 'todo'" @todo-count-change="updateTodoCount"></todo>
|
||||
<work v-if="homeTabbarActive === 'work'"></work>
|
||||
<mine v-if="homeTabbarActive === 'mine'" @switch-tab="switchHomeTab"></mine>
|
||||
<apps v-if="homeTabbarActive === 'apps'"></apps>
|
||||
<apps v-if="homeTabbarActive === 'apps'" :initial-category-name="appsCategoryName"></apps>
|
||||
<msg v-if="homeTabbarActive === 'msg'" @unread-count-change="updateUnreadMessageCount"></msg>
|
||||
</div>
|
||||
|
||||
@@ -111,19 +111,25 @@ layout("/layouts/platform_h5.html"){
|
||||
data() {
|
||||
return {
|
||||
homeTabbarActive: "home",
|
||||
appsCategoryName: "",
|
||||
todoCount: 0,
|
||||
unreadMessageCount: 0
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
homeTabbarChange(val) {
|
||||
// 普通页签切换沿用应用页缓存,不保留工具入口指定的分类。
|
||||
this.$set(this, "appsCategoryName", "")
|
||||
this.$store.commit("setActiveTarBar", val)
|
||||
},
|
||||
switchHomeTab(val) {
|
||||
this.homeTabbarActive = val
|
||||
// categoryName 来自“我的工具”,先传分类再挂载应用页,避免读取上一次的分类缓存。
|
||||
switchHomeTab(val, categoryName = "") {
|
||||
this.$set(this, "appsCategoryName", val === "apps" ? categoryName : "")
|
||||
this.$set(this, "homeTabbarActive", val)
|
||||
this.$store.commit("setActiveTarBar", val)
|
||||
},
|
||||
moduleClick(val) {
|
||||
this.$set(this, "appsCategoryName", "")
|
||||
this.homeTabbarActive = "apps"
|
||||
this.$store.commit("setActiveTarBar", "apps")
|
||||
},
|
||||
|
||||
@@ -74,22 +74,23 @@ const mine = {
|
||||
<van-icon name="edit"></van-icon>
|
||||
<span>我的签名</span>
|
||||
</button>
|
||||
<button type="button" class="mine-grid-item" @click="switchTab('apps')">
|
||||
<button type="button" class="mine-grid-item" @click="switchTab('apps', '我的收藏')">
|
||||
<van-icon name="star-o"></van-icon>
|
||||
<span>我的收藏</span>
|
||||
</button>
|
||||
<button type="button" class="mine-grid-item" @click="switchTab('apps')">
|
||||
<button type="button" class="mine-grid-item" @click="switchTab('apps', '职工福利')">
|
||||
<van-icon name="gift-o"></van-icon>
|
||||
<span>福利中心</span>
|
||||
</button>
|
||||
<button type="button" class="mine-grid-item" @click="switchTab('apps')">
|
||||
<button type="button" class="mine-grid-item" @click="switchTab('apps', '活动管理')">
|
||||
<van-icon name="calendar-o"></van-icon>
|
||||
<span>活动报名</span>
|
||||
</button>
|
||||
<button type="button" class="mine-grid-item" @click="switchTab('apps')">
|
||||
<button type="button" class="mine-grid-item" @click="$pjaxReplace('/platform/h5/qsv')">
|
||||
<van-icon name="completed-o"></van-icon>
|
||||
<span>问卷调查</span>
|
||||
</button>
|
||||
<!-- 暂不开放设置中心和帮助中心,保留入口模板便于恢复。
|
||||
<button type="button" class="mine-grid-item" @click="switchTab('apps')">
|
||||
<van-icon name="setting-o"></van-icon>
|
||||
<span>设置中心</span>
|
||||
@@ -98,6 +99,7 @@ const mine = {
|
||||
<van-icon name="question-o"></van-icon>
|
||||
<span>帮助中心</span>
|
||||
</button>
|
||||
-->
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -150,9 +152,10 @@ const mine = {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
switchTab(tab) {
|
||||
// tab 为底部页签,categoryName 为可选应用分类名称;通过事件交给首页切换并定位分类。
|
||||
switchTab(tab, categoryName = "") {
|
||||
this.$store.commit("setActiveTarBar", tab)
|
||||
this.$emit("switch-tab", tab)
|
||||
this.$emit("switch-tab", tab, categoryName)
|
||||
},
|
||||
logout() {
|
||||
this.$dialog
|
||||
|
||||
Reference in New Issue
Block a user