企业微信消息和oa待办

This commit is contained in:
2026-10-27 19:44:55 +08:00
parent e87014fa72
commit f17f6153b5
11 changed files with 238 additions and 54 deletions
@@ -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']}]
},
// 替他人申请相关
@@ -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">
@@ -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()
@@ -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
@@ -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