feat: 同步地大功能至2026年10月20日
@@ -0,0 +1,212 @@
|
||||
-- 人员数据治理模块初始化脚本(MySQL 8.0,UTF-8)。
|
||||
-- 人员变更直接复用现有 member_change_record、sys_user_history 及 WF 流程,本脚本不创建替代表。
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
SET @add_person_flag = IF(
|
||||
(SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'sys_user' AND COLUMN_NAME = 'flag') = 0,
|
||||
'ALTER TABLE sys_user ADD COLUMN flag int DEFAULT 0 COMMENT ''人员治理同步保护标识:1忽略同步,0或空正常同步''',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE add_person_flag_stmt FROM @add_person_flag;
|
||||
EXECUTE add_person_flag_stmt;
|
||||
DEALLOCATE PREPARE add_person_flag_stmt;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS person_api_config (
|
||||
id varchar(32) NOT NULL,
|
||||
configName varchar(100), sourceType varchar(20), requestUrl varchar(500), requestMethod varchar(10),
|
||||
timeoutSeconds int, sourceTable varchar(100), targetTable varchar(100), syncMode varchar(20),
|
||||
identityField varchar(100), bodyType varchar(30), bodyContent text,
|
||||
tokenRequestUrl varchar(500), tokenRequestMethod varchar(10), tokenTimeoutSeconds int,
|
||||
tokenBodyType varchar(30), tokenBodyContent text, tokenValuePath varchar(200), tokenExpiresPath varchar(200),
|
||||
tokenExpiryUnit varchar(20), tokenQueryResult text, dataQueryResult text,
|
||||
responseCodePath varchar(200), responseSuccessValue varchar(100), responseMessagePath varchar(200),
|
||||
responseDataPath varchar(200), responseTotalPath varchar(200), enabled tinyint(1), taskCron varchar(100),
|
||||
matchMemberRules tinyint(1), lastSyncTime datetime, taskId varchar(32), remark varchar(500),
|
||||
createdBy varchar(32), createdAt bigint, updatedBy varchar(32), updatedAt bigint,
|
||||
delFlag tinyint(1), opBy varchar(128), opAt varchar(128),
|
||||
PRIMARY KEY (id), UNIQUE KEY UK_PERSON_API_CONFIG_NAME (configName)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS person_api_field_mapping (
|
||||
id varchar(32) NOT NULL, configId varchar(32), sourceField varchar(100), sourceComment varchar(200),
|
||||
targetField varchar(100), targetComment varchar(200), required tinyint(1), identityFlag tinyint(1), sort int,
|
||||
PRIMARY KEY (id), UNIQUE KEY UK_PERSON_API_MAPPING_TARGET (configId, targetField)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS person_api_request_param (
|
||||
id varchar(32) NOT NULL, configId varchar(32), requestScope varchar(20), paramLocation varchar(20),
|
||||
paramName varchar(100), paramType varchar(30), paramValue text, required tinyint(1),
|
||||
description varchar(300), sensitiveFlag int, sort int, PRIMARY KEY (id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS person_api_test_case (
|
||||
id varchar(32) NOT NULL, configId varchar(32), requestScope varchar(20), caseName varchar(100),
|
||||
requestSnapshot text, responseStatus int, responseTime bigint, responseBody text, success tinyint(1),
|
||||
executedAt datetime, createdBy varchar(32), createdAt bigint, updatedBy varchar(32), updatedAt bigint,
|
||||
delFlag tinyint(1), opBy varchar(128), opAt varchar(128), PRIMARY KEY (id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS person_sync_batch (
|
||||
id varchar(32) NOT NULL, configId varchar(32), batchNo varchar(50), triggerType varchar(20),
|
||||
startedAt datetime, finishedAt datetime, status varchar(20), sourceCount int, successCount int,
|
||||
newCount int, changeCount int, exceptionCount int, errorMessage text, rawFileReid varchar(32),
|
||||
createdBy varchar(32), createdAt bigint, updatedBy varchar(32), updatedAt bigint,
|
||||
delFlag tinyint(1), opBy varchar(128), opAt varchar(128),
|
||||
PRIMARY KEY (id), UNIQUE KEY UK_PERSON_SYNC_BATCH_NO (batchNo)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS person_source_relation (
|
||||
id varchar(32) NOT NULL, configId varchar(32), sourcePersonKey varchar(150), userId varchar(32),
|
||||
personNo varchar(120), lastSeenBatchId varchar(32), lastSeenTime datetime, presentFlag tinyint(1),
|
||||
sourceDataHash varchar(64), createdBy varchar(32), createdAt bigint, updatedBy varchar(32), updatedAt bigint,
|
||||
delFlag tinyint(1), opBy varchar(128), opAt varchar(128),
|
||||
PRIMARY KEY (id), UNIQUE KEY UK_PERSON_SOURCE_KEY (configId, sourcePersonKey)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS person_member_match_config (
|
||||
id varchar(32) NOT NULL, configName varchar(100), apiConfigId varchar(32), copiedFromId varchar(32),
|
||||
enabled tinyint(1), groupCount int, lastMatchCount int, lastExecuteTime datetime,
|
||||
createdBy varchar(32), createdAt bigint, updatedBy varchar(32), updatedAt bigint,
|
||||
delFlag tinyint(1), opBy varchar(128), opAt varchar(128),
|
||||
PRIMARY KEY (id), UNIQUE KEY UK_PERSON_MEMBER_API_CONFIG (apiConfigId)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS person_member_match_group (
|
||||
id varchar(32) NOT NULL, matchConfigId varchar(32), parentGroupId varchar(32),
|
||||
ruleType varchar(30) DEFAULT 'UNION_MEMBER', groupNo int, joinLogic varchar(3), sort int,
|
||||
PRIMARY KEY (id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS person_member_match_condition (
|
||||
id varchar(32) NOT NULL, groupId varchar(32), dictCode varchar(50), dictItemId varchar(32),
|
||||
dictItemCode varchar(50), dictItemName varchar(100), mappedField varchar(100), operator varchar(30),
|
||||
matchValue varchar(500), joinLogic varchar(3), sort int, PRIMARY KEY (id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS person_exception (
|
||||
id varchar(32) NOT NULL, configId varchar(32), relationId varchar(32), discoverBatchId varchar(32),
|
||||
userId varchar(32), personNo varchar(120), personName varchar(100), unitId varchar(32), unitName varchar(150),
|
||||
exceptionType varchar(30), exceptionReason varchar(500), lastSyncTime datetime, discoverTime datetime,
|
||||
status varchar(30), handleResult varchar(30), handleOpinion varchar(500), handlerId varchar(32),
|
||||
handlerName varchar(100), handleTime datetime, rawFileReid varchar(32),
|
||||
createdBy varchar(32), createdAt bigint, updatedBy varchar(32), updatedAt bigint,
|
||||
delFlag tinyint(1), opBy varchar(128), opAt varchar(128), PRIMARY KEY (id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- PC 端独立应用菜单。完整页面跳转由布局按 /platform/personGovernance/ 路径处理。
|
||||
INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled,
|
||||
permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag,
|
||||
platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService)
|
||||
SELECT '76725022b0cc11f196a000ff8d9db2e0', '', LPAD(IFNULL(MAX(CAST(path AS UNSIGNED)), 0) + 1, 4, '0'),
|
||||
'人员数据治理', 'PersonGovernance', 'menu', '', '', 'ti-id-badge', 1, 0,
|
||||
'data.person.governance', '人员数据接入、会员匹配、人员变更及异常处理', 694, 1,
|
||||
'', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0,
|
||||
'PC', NULL, NULL, 'r', 0, 0
|
||||
FROM sys_menu WHERE (parentId = '' OR parentId IS NULL) AND CHAR_LENGTH(path) = 4
|
||||
HAVING NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'data.person.governance') m);
|
||||
|
||||
INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled,
|
||||
permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag,
|
||||
platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService)
|
||||
SELECT '7673d831b0cc11f196a000ff8d9db2e0', p.id, CONCAT(p.path, '0001'), 'API配置', 'ApiConfig', 'menu',
|
||||
'/platform/personGovernance/apiConfig', '', '', 1, 0, 'data.person.governance.api', NULL, 1, 0,
|
||||
'', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', NULL, NULL, 'a', 0, 0
|
||||
FROM sys_menu p WHERE p.permission = 'data.person.governance'
|
||||
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'data.person.governance.api') m);
|
||||
|
||||
INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled,
|
||||
permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag,
|
||||
platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService)
|
||||
SELECT '76749371b0cc11f196a000ff8d9db2e0', p.id, CONCAT(p.path, '0002'), '会员匹配', 'MemberMatch', 'menu',
|
||||
'/platform/personGovernance/memberMatch', '', '', 1, 0, 'data.person.governance.member', NULL, 2, 0,
|
||||
'', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', NULL, NULL, 'h', 0, 0
|
||||
FROM sys_menu p WHERE p.permission = 'data.person.governance'
|
||||
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'data.person.governance.member') m);
|
||||
|
||||
INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled,
|
||||
permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag,
|
||||
platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService)
|
||||
SELECT '7674b630b0cc11f196a000ff8d9db2e0', p.id, CONCAT(p.path, '0003'), '人员变更', 'PersonChange', 'menu',
|
||||
'/platform/personGovernance/personChange', '', '', 1, 0, 'data.person.governance.change', NULL, 3, 0,
|
||||
'', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', NULL, NULL, 'r', 0, 0
|
||||
FROM sys_menu p WHERE p.permission = 'data.person.governance'
|
||||
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'data.person.governance.change') m);
|
||||
|
||||
INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled,
|
||||
permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag,
|
||||
platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService)
|
||||
SELECT '7674d34eb0cc11f196a000ff8d9db2e0', p.id, CONCAT(p.path, '0004'), '人员异常', 'PersonException', 'menu',
|
||||
'/platform/personGovernance/personException', '', '', 1, 0, 'data.person.governance.exception', NULL, 4, 0,
|
||||
'', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', NULL, NULL, 'r', 0, 0
|
||||
FROM sys_menu p WHERE p.permission = 'data.person.governance'
|
||||
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'data.person.governance.exception') m);
|
||||
|
||||
-- 页面按钮权限;type=data 不在左侧菜单展示。
|
||||
INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, showit, disabled, permission, location,
|
||||
hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId,
|
||||
initialPinyinName, isRecommendApp, isRecommendService)
|
||||
SELECT '7674fb04b0cc11f196a000ff8d9db2e0', p.id, CONCAT(p.path, '0001'), '保存API配置', 'Edit', 'data',
|
||||
0, 0, 'data.person.governance.api.edit', 1, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '',
|
||||
UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', NULL, 'b', 0, 0 FROM sys_menu p
|
||||
WHERE p.permission = 'data.person.governance.api' AND NOT EXISTS
|
||||
(SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'data.person.governance.api.edit') m);
|
||||
|
||||
INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, showit, disabled, permission, location,
|
||||
hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId,
|
||||
initialPinyinName, isRecommendApp, isRecommendService)
|
||||
SELECT '76750400b0cc11f196a000ff8d9db2e0', p.id, CONCAT(p.path, '0002'), '删除API配置', 'Delete', 'data',
|
||||
0, 0, 'data.person.governance.api.delete', 2, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '',
|
||||
UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', NULL, 's', 0, 0 FROM sys_menu p
|
||||
WHERE p.permission = 'data.person.governance.api' AND NOT EXISTS
|
||||
(SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'data.person.governance.api.delete') m);
|
||||
|
||||
INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, showit, disabled, permission, location,
|
||||
hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId,
|
||||
initialPinyinName, isRecommendApp, isRecommendService)
|
||||
SELECT '76750e42b0cc11f196a000ff8d9db2e0', p.id, CONCAT(p.path, '0003'), '测试及同步', 'Test', 'data',
|
||||
0, 0, 'data.person.governance.api.test', 3, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '',
|
||||
UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', NULL, 'c', 0, 0 FROM sys_menu p
|
||||
WHERE p.permission = 'data.person.governance.api' AND NOT EXISTS
|
||||
(SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'data.person.governance.api.test') m);
|
||||
|
||||
INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, showit, disabled, permission, location,
|
||||
hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId,
|
||||
initialPinyinName, isRecommendApp, isRecommendService)
|
||||
SELECT '767518e8b0cc11f196a000ff8d9db2e0', p.id, CONCAT(p.path, '0001'), '保存会员匹配', 'Edit', 'data',
|
||||
0, 0, 'data.person.governance.member.edit', 1, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '',
|
||||
UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', NULL, 'b', 0, 0 FROM sys_menu p
|
||||
WHERE p.permission = 'data.person.governance.member' AND NOT EXISTS
|
||||
(SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'data.person.governance.member.edit') m);
|
||||
|
||||
INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, showit, disabled, permission, location,
|
||||
hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId,
|
||||
initialPinyinName, isRecommendApp, isRecommendService)
|
||||
SELECT '767523deb0cc11f196a000ff8d9db2e0', p.id, CONCAT(p.path, '0002'), '删除会员匹配', 'Delete', 'data',
|
||||
0, 0, 'data.person.governance.member.delete', 2, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '',
|
||||
UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', NULL, 's', 0, 0 FROM sys_menu p
|
||||
WHERE p.permission = 'data.person.governance.member' AND NOT EXISTS
|
||||
(SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'data.person.governance.member.delete') m);
|
||||
|
||||
INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, showit, disabled, permission, location,
|
||||
hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId,
|
||||
initialPinyinName, isRecommendApp, isRecommendService)
|
||||
SELECT '76752eacb0cc11f196a000ff8d9db2e0', p.id, CONCAT(p.path, '0001'), '处理人员异常', 'Handle', 'data',
|
||||
0, 0, 'data.person.governance.exception.handle', 1, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '',
|
||||
UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', NULL, 'c', 0, 0 FROM sys_menu p
|
||||
WHERE p.permission = 'data.person.governance.exception' AND NOT EXISTS
|
||||
(SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'data.person.governance.exception.handle') m);
|
||||
|
||||
-- 给系统管理员默认授权,其他角色可在角色管理中按需分配。
|
||||
INSERT INTO sys_role_menu (roleId, menuId)
|
||||
SELECT r.id, m.id
|
||||
FROM sys_role r
|
||||
JOIN sys_menu m ON m.permission IN (
|
||||
'data.person.governance', 'data.person.governance.api', 'data.person.governance.member',
|
||||
'data.person.governance.api.edit', 'data.person.governance.api.delete', 'data.person.governance.api.test',
|
||||
'data.person.governance.member.edit', 'data.person.governance.member.delete',
|
||||
'data.person.governance.change', 'data.person.governance.exception',
|
||||
'data.person.governance.exception.handle')
|
||||
LEFT JOIN sys_role_menu rm ON rm.roleId = r.id AND rm.menuId = m.id
|
||||
WHERE r.code = 'SYSADMIN'
|
||||
AND rm.roleId IS NULL;
|
||||
@@ -0,0 +1,19 @@
|
||||
-- 社团缩略图保存系统文件下载地址;保留可空以兼容未上传图片的历史社团。
|
||||
-- 字段存在时跳过,避免重复执行迁移时报错。
|
||||
SET @club_thumbnail_column_count = (
|
||||
SELECT COUNT(*)
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'sys_club'
|
||||
AND COLUMN_NAME = 'thumbnail'
|
||||
);
|
||||
|
||||
SET @club_thumbnail_ddl = IF(
|
||||
@club_thumbnail_column_count = 0,
|
||||
'ALTER TABLE sys_club ADD COLUMN thumbnail VARCHAR(1000) NULL COMMENT ''社团缩略图'' AFTER clubName',
|
||||
'SELECT 1'
|
||||
);
|
||||
|
||||
PREPARE club_thumbnail_stmt FROM @club_thumbnail_ddl;
|
||||
EXECUTE club_thumbnail_stmt;
|
||||
DEALLOCATE PREPARE club_thumbnail_stmt;
|
||||
@@ -0,0 +1,38 @@
|
||||
-- 社团活动属于报销项目的正式字典配置,两端沿用现有字典接口读取。
|
||||
-- 仅补充缺失记录;已有记录(包括已禁用记录)不覆盖,不在查询接口中添加兜底项。
|
||||
INSERT INTO sys_dict (
|
||||
id, parentId, path, name, remark, code, disabled, location, hasChildren,
|
||||
createdBy, createdAt, updatedBy, updatedAt, delFlag
|
||||
)
|
||||
SELECT
|
||||
REPLACE(UUID(), '-', ''),
|
||||
parent.id,
|
||||
CONCAT(parent.path, LPAD(IFNULL((
|
||||
SELECT MAX(CAST(RIGHT(child.path, 4) AS UNSIGNED))
|
||||
FROM sys_dict child
|
||||
WHERE child.path LIKE CONCAT(parent.path, '____')
|
||||
), 0) + 1, 4, '0')),
|
||||
'社团活动',
|
||||
'',
|
||||
'UNION_REIMBURSE_PROJECT_5',
|
||||
0,
|
||||
IFNULL((SELECT MAX(child.location) FROM sys_dict child WHERE child.parentId = parent.id), 0) + 1,
|
||||
0,
|
||||
'', UNIX_TIMESTAMP(NOW()) * 1000,
|
||||
'', UNIX_TIMESTAMP(NOW()) * 1000,
|
||||
0
|
||||
FROM sys_dict parent
|
||||
WHERE parent.code = 'UNION_REIMBURSE_PROJECT'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM (SELECT code FROM sys_dict WHERE code = 'UNION_REIMBURSE_PROJECT_5') existing_item
|
||||
);
|
||||
|
||||
UPDATE sys_dict parent
|
||||
SET parent.hasChildren = 1
|
||||
WHERE parent.code = 'UNION_REIMBURSE_PROJECT'
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM (SELECT parentId FROM sys_dict WHERE code = 'UNION_REIMBURSE_PROJECT_5') club_activity
|
||||
WHERE club_activity.parentId = parent.id
|
||||
);
|
||||
@@ -54,86 +54,7 @@
|
||||
|
||||
|
||||
|
||||
<!--
|
||||
#################################################################################
|
||||
# #
|
||||
# debug、info、error分文件输出 start #
|
||||
# #
|
||||
#################################################################################
|
||||
-->
|
||||
|
||||
<!-- DEBUG级别日志文件配置 -->
|
||||
<!--<appender name="DEBUG_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>${LOG_HOME}/debug.log</file>
|
||||
<encoder>
|
||||
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
|
||||
</encoder>
|
||||
<filter class="ch.qos.logback.classic.filter.LevelFilter">
|
||||
<level>DEBUG</level>
|
||||
<onMatch>ACCEPT</onMatch>
|
||||
<onMismatch>DENY</onMismatch>
|
||||
</filter>
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||
<fileNamePattern>${LOG_HOME}/debug-%d{yyyy-MM-dd}.log</fileNamePattern>
|
||||
<maxHistory>300</maxHistory>
|
||||
</rollingPolicy>
|
||||
</appender>
|
||||
|
||||
<!– INFO级别日志文件配置 –>
|
||||
<appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>${LOG_HOME}/info.log</file>
|
||||
<encoder>
|
||||
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
|
||||
</encoder>
|
||||
<filter class="ch.qos.logback.classic.filter.LevelFilter">
|
||||
<level>INFO</level>
|
||||
<onMatch>ACCEPT</onMatch>
|
||||
<onMismatch>DENY</onMismatch>
|
||||
</filter>
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||
<fileNamePattern>${LOG_HOME}/info-%d{yyyy-MM-dd}.log</fileNamePattern>
|
||||
<maxHistory>300</maxHistory>
|
||||
</rollingPolicy>
|
||||
</appender>
|
||||
|
||||
<!– ERROR级别日志文件配置 –>
|
||||
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>${LOG_HOME}/error.log</file>
|
||||
<encoder>
|
||||
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
|
||||
</encoder>
|
||||
<filter class="ch.qos.logback.classic.filter.LevelFilter">
|
||||
<level>ERROR</level>
|
||||
<onMatch>ACCEPT</onMatch>
|
||||
<onMismatch>DENY</onMismatch>
|
||||
</filter>
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||
<fileNamePattern>${LOG_HOME}/error-%d{yyyy-MM-dd}.log</fileNamePattern>
|
||||
<maxHistory>300</maxHistory>
|
||||
</rollingPolicy>
|
||||
</appender>
|
||||
|
||||
<!– 框架日志级别 –>
|
||||
<!– <logger name="java" additivity="false" />–>
|
||||
<logger name="org.eclipse.jetty" level="INFO"/>
|
||||
<logger name="org.quartz" level="INFO"/>
|
||||
<logger name="org.nutz" level="DEBUG"/>
|
||||
|
||||
<!– 日志级别和appender的关联 –>
|
||||
<root level="DEBUG">
|
||||
<appender-ref ref="STDOUT"/>
|
||||
<appender-ref ref="DEBUG_FILE"/>
|
||||
<appender-ref ref="INFO_FILE"/>
|
||||
<appender-ref ref="ERROR_FILE"/>
|
||||
</root>-->
|
||||
|
||||
<!--
|
||||
#################################################################################
|
||||
# #
|
||||
# debug、info、error分文件输出 end #
|
||||
# #
|
||||
#################################################################################
|
||||
-->
|
||||
|
||||
</configuration>
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 1.8 MiB |
@@ -0,0 +1,768 @@
|
||||
/* 人员数据治理列表统一占满内容区,数据超出时只滚动表格主体。 */
|
||||
.person-governance-page-transition {
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.person-governance-page-transition.is-ready {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.person-governance-page-transition.is-leaving {
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.person-governance-list-page {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.person-governance-search-card {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.person-governance-list-card {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
/* 会员匹配列表的查询区与列表区间距缩小为原来的一半。 */
|
||||
.person-governance-compact-gap .person-governance-list-card {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.person-governance-list-card > .el-card__body {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.person-governance-table-wrap {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.person-governance-table-wrap > .el-table {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.person-governance-list-footer,
|
||||
.person-governance-list-card .el-pagination-container {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
/* 人员变更列表位于 Guava 默认插槽内,列表视图仍按查询区、表格区、分页区分配固定高度。 */
|
||||
.person-governance-change-page > .guava-main-content,
|
||||
.person-governance-change-page > .guava-main-content > .transition-item {
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.person-governance-change-page > .guava-main-content > .transition-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.person-governance-change-list-card {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.person-governance-change-list-card > .el-card__body {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.person-governance-change-table-wrap {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.person-governance-change-table-wrap > .el-table {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.person-governance-change-list-card .el-pagination-container {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
/* 配置页顶部字段根据内容区宽度自动分列,空间不足时自然换行。 */
|
||||
.person-governance-form-page {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
max-width: 100%;
|
||||
min-height: 0;
|
||||
overflow-x: hidden;
|
||||
overflow-y: hidden;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
/* PageHeader 提供统一返回入口,并与基本配置保持紧凑间距。 */
|
||||
.person-governance-page-header {
|
||||
flex: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
padding: 16px 20px;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
/* 表单页只保留返回入口,移除 PageHeader 空内容对应的分隔线和占位间距。 */
|
||||
.person-governance-page-header .el-page-header__left {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.person-governance-page-header .el-page-header__left::after,
|
||||
.person-governance-page-header .el-page-header__content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.person-governance-form-basic-card {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
/* 第三方接口按Token与数据分区管理,一级页签保持紧凑并独立于各自的子页签。 */
|
||||
.person-governance-endpoint-tabs-card {
|
||||
flex: none;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.person-governance-endpoint-tabs-card > .el-card__body {
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.person-governance-endpoint-tabs-card .el-tabs__header {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.person-governance-endpoint-tabs-card .el-tabs__content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 顶部基本配置横向铺满卡片,并压缩默认表单项产生的底部空白。 */
|
||||
.person-governance-form-basic-card > .el-card__body {
|
||||
padding: 20px 10px;
|
||||
}
|
||||
|
||||
.person-governance-form-basic-card .el-form-item {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* 两类会员规则使用独立页签,页签本身不参与内容区滚动。 */
|
||||
.person-governance-member-type-tabs {
|
||||
flex: none;
|
||||
margin-top: 10px;
|
||||
padding: 0 20px;
|
||||
background: #fff;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.person-governance-member-type-tabs .el-tabs__header {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.person-governance-member-type-tabs .el-tabs__content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 主要配置板块占满剩余高度,内容过多时仅在板块内部滚动。 */
|
||||
.person-governance-form-content-card {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.person-governance-form-content-card > .el-card__body {
|
||||
box-sizing: border-box;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.person-governance-form-content-row {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.person-governance-form-content-row > .el-col,
|
||||
.person-governance-form-content-row > .el-col > .el-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.person-governance-form-content-row > .el-col {
|
||||
float: none;
|
||||
}
|
||||
|
||||
.person-governance-form-content-row > .el-col > .el-card {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.person-governance-form-content-row > .el-col > .el-card > .el-card__body {
|
||||
box-sizing: border-box;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/* 匹配规则标题固定,条件组占用剩余高度并独立滚动。 */
|
||||
.person-governance-form-content-row > .el-col > .person-governance-member-rules > .el-card__body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.person-governance-member-rules-header {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.person-governance-member-rules-content {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
/* 会员预览固定标题和操作区,表格独占剩余高度,长列表只滚动表格内部。 */
|
||||
.person-governance-form-content-row > .el-col > .person-governance-member-preview > .el-card__body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.person-governance-member-preview h3,
|
||||
.person-governance-member-preview p,
|
||||
.person-governance-member-preview > .el-card__body > .el-button {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.person-governance-member-preview > .el-card__body > .el-button {
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.person-governance-member-preview-table {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.person-governance-member-preview-pagination {
|
||||
flex: none;
|
||||
padding-top: 10px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.person-governance-member-preview-pagination .el-pagination {
|
||||
padding: 0;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.person-governance-member-binding-hint {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.person-governance-member-condition-relation > td {
|
||||
padding: 6px 0;
|
||||
text-align: center;
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
/* 每层条件组对应一层括号语义,通过边框和缩进直观展示父子层级。 */
|
||||
.person-governance-member-group {
|
||||
margin-top: 20px;
|
||||
padding: 14px;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.person-governance-member-group.is-nested {
|
||||
margin: 0 0 0 20px;
|
||||
border-color: #b3d8ff;
|
||||
background: #f8fbff;
|
||||
}
|
||||
|
||||
.person-governance-member-group-header {
|
||||
min-height: 28px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.person-governance-member-group-actions {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.person-governance-member-group-relation {
|
||||
margin: 28px 0;
|
||||
}
|
||||
|
||||
.person-governance-member-child-relation {
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.person-governance-form-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
flex: none;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.person-governance-form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
||||
column-gap: 20px;
|
||||
align-items: start;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.person-governance-form-grid .el-form-item {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.person-governance-form-grid .el-select,
|
||||
.person-governance-form-grid .el-input-number {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* API配置顶部字段按业务宽度靠左排列,同步方式具体值单独放在页首右侧。 */
|
||||
.person-governance-sync-type {
|
||||
color: #303133;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.person-governance-api-basic-grid {
|
||||
/* 按业务宽度紧凑靠左,剩余空间留在右侧;窄屏只允许整组字段换行。 */
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 18px 16px;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.person-governance-api-basic-grid > .el-form-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 0 1 280px;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.person-governance-api-basic-grid .el-form-item__label {
|
||||
flex: none;
|
||||
float: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.person-governance-api-basic-grid .el-form-item__content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.person-governance-api-basic-grid > .person-governance-api-request-address {
|
||||
flex-basis: 620px;
|
||||
}
|
||||
|
||||
.person-governance-api-basic-grid > .person-governance-api-timeout {
|
||||
flex-basis: 270px;
|
||||
}
|
||||
|
||||
.person-governance-api-basic-grid > .person-governance-query-action {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.person-governance-request-address-control,
|
||||
.person-governance-timeout-control {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.person-governance-request-address-control {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.person-governance-request-address-control .el-select {
|
||||
flex: none;
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.person-governance-request-address-control > .el-input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.person-governance-timeout-control .el-input-number {
|
||||
flex: none;
|
||||
width: 200px;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.person-governance-timeout-unit {
|
||||
flex: none;
|
||||
margin-left: 8px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
/* API配置页签占满内容卡片,返回结果文本域随剩余高度伸展。 */
|
||||
.person-governance-api-content-card > .el-card__body {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.person-governance-api-content-card > .el-card__body > .el-tabs {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.person-governance-api-content-card > .el-card__body > .el-tabs > .el-tabs__header {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.person-governance-api-content-card > .el-card__body > .el-tabs > .el-tabs__content {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.person-governance-api-content-card > .el-card__body > .el-tabs > .el-tabs__content > .el-tab-pane {
|
||||
box-sizing: border-box;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/* 系统表配置不显示单一的字段映射页签标题,映射内容直接占满卡片。 */
|
||||
.person-governance-system-table-card > .el-card__body > .el-tabs > .el-tabs__header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.person-governance-system-table-card > .el-card__body > .el-tabs > .el-tabs__content,
|
||||
.person-governance-system-table-card .person-governance-mapping-pane {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.person-governance-system-table-card .person-governance-mapping-pane {
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
.person-governance-result-pane {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
.person-governance-result-textarea {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* 返回结果下方的映射配置固定保留,长响应只在文本域内滚动。 */
|
||||
.person-governance-response-fields {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex: none;
|
||||
gap: 10px 20px;
|
||||
padding-top: 12px;
|
||||
}
|
||||
|
||||
.person-governance-response-fields > div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1 1 280px;
|
||||
min-width: 0;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.person-governance-response-fields label,
|
||||
.person-governance-task-cron label {
|
||||
white-space: nowrap;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.person-governance-response-fields .el-input,
|
||||
.person-governance-response-fields .el-select {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.person-governance-task-cron {
|
||||
flex: 0 1 420px;
|
||||
max-width: 420px;
|
||||
}
|
||||
|
||||
.person-governance-cron-hint,
|
||||
.person-governance-json-hint {
|
||||
flex: none;
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.person-governance-parameter-pane .person-governance-body-json {
|
||||
flex: 1;
|
||||
min-height: 80px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.person-governance-result-textarea .el-textarea__inner,
|
||||
.person-governance-body-json .el-textarea__inner {
|
||||
height: 100%;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
.person-governance-result-actions {
|
||||
flex: none;
|
||||
margin-top: 10px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.person-governance-body-json {
|
||||
height: calc(100% - 52px);
|
||||
}
|
||||
|
||||
/* 字段映射与请求参数表格在可见页签内直接按完整宽高渲染。 */
|
||||
.person-governance-mapping-table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.person-governance-mapping-pane {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
.person-governance-mapping-table-wrap {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.person-governance-mapping-table-wrap > .el-table {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.person-governance-mapping-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px 24px;
|
||||
}
|
||||
|
||||
.person-governance-mapping-toolbar-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* 两种数据来源共用右侧清空入口,窄屏换行后仍靠右,不挤压周期控件。 */
|
||||
.person-governance-mapping-toolbar > .person-governance-clear-mappings {
|
||||
flex: none;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.person-governance-mapping-toolbar-item > label {
|
||||
flex: none;
|
||||
margin-right: 10px;
|
||||
color: #303133;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.person-governance-mapping-table-select {
|
||||
flex: 0 1 360px;
|
||||
max-width: 360px;
|
||||
}
|
||||
|
||||
.person-governance-mapping-table-select .el-select {
|
||||
flex: 1;
|
||||
min-width: 180px;
|
||||
}
|
||||
|
||||
.person-governance-mapping-switch {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
/* 系统表配置项按配置名称、来源表、目标表、同步开关的顺序自适应排列。 */
|
||||
.person-governance-system-table-toolbar {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.person-governance-system-table-toolbar .el-form-item {
|
||||
/* 标签与控件作为一组排列,仅允许表单项在工具栏中整体换行。 */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1 1 280px;
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.person-governance-system-table-toolbar .person-governance-system-target-table {
|
||||
flex: 0 1 300px;
|
||||
}
|
||||
|
||||
.person-governance-system-table-toolbar .person-governance-system-task-cron {
|
||||
flex: 0 1 430px;
|
||||
}
|
||||
|
||||
.person-governance-system-table-toolbar .person-governance-system-member-match {
|
||||
flex: none;
|
||||
column-gap: 12px;
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
/* 长标签按文字实际宽度展示,通过固定列间距避免文字与开关重叠或距离过大。 */
|
||||
.person-governance-system-table-toolbar .person-governance-system-member-match .el-form-item__label {
|
||||
width: auto !important;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.person-governance-system-table-toolbar .person-governance-system-member-match .el-form-item__content {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
|
||||
.person-governance-system-table-toolbar .el-form-item__label {
|
||||
flex: none;
|
||||
float: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.person-governance-system-table-toolbar .el-form-item__content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.person-governance-system-table-toolbar .el-input,
|
||||
.person-governance-system-table-toolbar .el-select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.person-governance-parameter-pane {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
.person-governance-parameter-pane > .el-tabs,
|
||||
.person-governance-parameter-pane > .el-radio-group {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.person-governance-parameter-table-wrap {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.person-governance-parameter-table-wrap > .el-table {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* 参数表头按文字与加号按钮的实际高度紧凑展示。 */
|
||||
.person-governance-parameter-table .el-table__header-wrapper th,
|
||||
.person-governance-mapping-table .el-table__header-wrapper th {
|
||||
padding: 6px 0;
|
||||
}
|
||||
|
||||
.person-governance-parameter-table .el-table__header-wrapper th .cell,
|
||||
.person-governance-mapping-table .el-table__header-wrapper th .cell {
|
||||
line-height: 20px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.person-governance-add-column .el-button {
|
||||
padding: 0;
|
||||
font-size: 16px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
/* 弹窗和抽屉沿用 Element UI 过渡,并为本模块调整为更柔和的缓动。 */
|
||||
.dialog-fade-enter-active .person-governance-dialog,
|
||||
.dialog-fade-leave-active .person-governance-dialog {
|
||||
transition:
|
||||
opacity 260ms ease,
|
||||
transform 260ms ease;
|
||||
}
|
||||
|
||||
.dialog-fade-enter .person-governance-dialog,
|
||||
.dialog-fade-leave-to .person-governance-dialog {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px) scale(0.99);
|
||||
}
|
||||
|
||||
/* 弹窗挂载body避开页面过渡层,短屏内部滚动,窄屏保留周期编辑器的完整操作区域。 */
|
||||
.person-governance-cron-dialog {
|
||||
max-width: calc(100vw - 32px);
|
||||
}
|
||||
|
||||
.person-governance-cron-dialog .el-dialog__body {
|
||||
max-height: 78vh;
|
||||
overflow: auto;
|
||||
padding: 10px 20px 20px;
|
||||
}
|
||||
|
||||
.person-governance-cron-dialog .popup-main {
|
||||
min-width: 640px;
|
||||
}
|
||||
|
||||
.person-governance-drawer {
|
||||
transition: box-shadow 260ms ease;
|
||||
}
|
||||
|
||||
.person-governance-drawer.rtl,
|
||||
.el-drawer__open .person-governance-drawer.rtl {
|
||||
animation-duration: 340ms;
|
||||
animation-timing-function: cubic-bezier(0.22, 0.61, 0.36, 1);
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.person-governance-list-page,
|
||||
.person-governance-form-page {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.person-governance-form-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.person-governance-page-header {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.person-governance-timeout-control .el-input-number {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.person-governance-mapping-table-select {
|
||||
flex-basis: 100%;
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,7 +70,8 @@ const initTableMixins = {
|
||||
pageData(data = null) {
|
||||
const address = this.pageDataUrl ? this.pageDataUrl : loc() + "/pageData"
|
||||
this.tableLoading = true
|
||||
this.$axios.post(address, data ? data : this.pageForm).then((res) => {
|
||||
// 返回请求 Promise,供页面在列表数据加载结束后再显示主体内容。
|
||||
return this.$axios.post(address, data ? data : this.pageForm).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.tableData = res.data.list
|
||||
this.pageForm.totalCount = res.data.totalCount
|
||||
|
||||
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 22 KiB |
@@ -1,12 +1,14 @@
|
||||
<template>
|
||||
<div>
|
||||
<van-action-sheet
|
||||
v-model="visible"
|
||||
<van-popup
|
||||
:value="visible"
|
||||
class="activity-schedule-calendar"
|
||||
:style="{ height: '78%' }"
|
||||
:close-on-click-overlay="true"
|
||||
title="活动日历"
|
||||
closeable>
|
||||
position="right"
|
||||
:style="{ width: '100%', height: '100%' }"
|
||||
:close-on-click-overlay="false"
|
||||
:close-on-popstate="false">
|
||||
<van-nav-bar title="课程日历" left-text="返回" left-arrow
|
||||
@click-left="closeSignPopup('calendar')"></van-nav-bar>
|
||||
<div class="activity-schedule-calendar__body">
|
||||
<div class="activity-schedule-calendar__month-bar">
|
||||
<button type="button" class="activity-schedule-calendar__month-btn" @click="changeMonth(-1)">
|
||||
@@ -30,7 +32,10 @@
|
||||
:class="{
|
||||
'is-other-month': !day.currentMonth,
|
||||
'is-selected': day.key === selectedDate,
|
||||
'is-today': day.isToday
|
||||
'is-today': day.isToday,
|
||||
'is-signed': day.status === 'signed',
|
||||
'is-available': day.status === 'available',
|
||||
'is-pending': day.status === 'pending'
|
||||
}"
|
||||
@click="selectDay(day)">
|
||||
<span>
|
||||
@@ -42,13 +47,11 @@
|
||||
</div>
|
||||
|
||||
<div class="activity-schedule-calendar__legend">
|
||||
<span><i class="is-solid"></i>已签到</span>
|
||||
<span><i class="is-outline"></i>可签到</span>
|
||||
<span><i class="is-muted"></i>未开始</span>
|
||||
<span><i class="is-signed"></i>已签到</span>
|
||||
<span><i class="is-available"></i>可签到</span>
|
||||
<span><i class="is-pending"></i>未开始</span>
|
||||
</div>
|
||||
|
||||
<div v-if="showMap" :id="mapContainerId" class="activity-schedule-calendar__map"></div>
|
||||
|
||||
<section class="activity-schedule-calendar__schedule-card">
|
||||
<div class="activity-schedule-calendar__schedule-title">
|
||||
{{ selectedDate ? selectedDate + ' 的活动时段' : '活动时段' }}
|
||||
@@ -72,6 +75,7 @@
|
||||
v-else-if="canSignItem(item)"
|
||||
type="button"
|
||||
class="activity-schedule-calendar__sign-btn"
|
||||
:disabled="formLoading"
|
||||
@click.stop="onSign(item)">签到</button>
|
||||
<span v-else class="activity-schedule-calendar__status" :class="{
|
||||
'is-ended': getScheduleStatus(item) === '已结束',
|
||||
@@ -88,16 +92,53 @@
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</van-action-sheet>
|
||||
</van-popup>
|
||||
|
||||
<van-popup
|
||||
v-model="signVisible"
|
||||
:value="positionConfirmVisible"
|
||||
class="activity-schedule-position-popup"
|
||||
position="right"
|
||||
:style="{ width: '100%', height: '100%' }"
|
||||
:close-on-click-overlay="false"
|
||||
:close-on-popstate="false"
|
||||
@opened="initMap"
|
||||
@closed="onPositionPopupClosed">
|
||||
<div class="activity-schedule-position">
|
||||
<van-nav-bar title="定位签到" left-text="返回" left-arrow
|
||||
@click-left="closeSignPopup('position-confirm')"></van-nav-bar>
|
||||
<div :id="mapContainerId" class="activity-schedule-calendar__map"></div>
|
||||
<div class="activity-schedule-position__details">
|
||||
<div class="activity-schedule-position__location-tools">
|
||||
<span class="activity-schedule-position__location-status" role="status">{{ locationMessage }}</span>
|
||||
<van-button size="small" plain type="info" icon="aim" :loading="locationLoading"
|
||||
:disabled="formLoading || !!closingPopupKey" @click="refreshLocation">重新定位</van-button>
|
||||
</div>
|
||||
<div class="activity-schedule-position__title">{{ row ? row.courseName : '' }}</div>
|
||||
<div v-if="selectCourseTime" class="activity-schedule-position__text">
|
||||
{{ dateKey(selectCourseTime) }} {{ formatTime(selectCourseTime.courseStartTime) }} - {{ formatTime(selectCourseTime.courseEndTime) }}
|
||||
</div>
|
||||
<div v-if="row && row.courseLocation" class="activity-schedule-position__text">
|
||||
<van-icon name="location-o"></van-icon>{{ row.courseLocation }}
|
||||
</div>
|
||||
<div class="activity-schedule-position__text">签到范围:{{ row ? (row.radius || 100) : 100 }} 米</div>
|
||||
</div>
|
||||
<div class="activity-schedule-position__footer">
|
||||
<van-button type="info" block round :loading="formLoading" :disabled="formLoading || locationLoading || !!closingPopupKey"
|
||||
loading-text="签到中..." @click="onConfirmPositionSign">确认签到</van-button>
|
||||
</div>
|
||||
</div>
|
||||
</van-popup>
|
||||
|
||||
<van-popup
|
||||
:value="signVisible"
|
||||
round
|
||||
closeable
|
||||
:safe-area-inset-bottom="true"
|
||||
:close-on-click-overlay="false"
|
||||
:close-on-popstate="false"
|
||||
:style="{ width: '80%', height: '66%' }"
|
||||
get-container="#app"
|
||||
@click-close-icon="closeSignPopup('scan')"
|
||||
@close="onSignClose">
|
||||
<scan-code ref="scanCodeRef"></scan-code>
|
||||
</van-popup>
|
||||
@@ -126,7 +167,17 @@ module.exports = {
|
||||
weekdays: ["日", "一", "二", "三", "四", "五", "六"],
|
||||
selectCourseTime: null,
|
||||
signVisible: false,
|
||||
signCodePreview: null,
|
||||
positionConfirmVisible: false,
|
||||
formLoading: false,
|
||||
popupHistoryToken: "",
|
||||
calendarHistoryOpen: false,
|
||||
closingPopupKey: "",
|
||||
markerLayer: null,
|
||||
userMarkerLayer: null,
|
||||
locationLoading: false,
|
||||
locationMessage: "",
|
||||
locationRequestId: 0,
|
||||
map: null,
|
||||
circle: null,
|
||||
mapContainerId: "activityScheduleMap_" + Math.random().toString(36).slice(2)
|
||||
@@ -181,9 +232,11 @@ module.exports = {
|
||||
this.currentMonth = date.month() + 1
|
||||
this.selectedDate = initialDate
|
||||
this.visible = true
|
||||
this.$nextTick(() => {
|
||||
if (this.showMap) this.initMap()
|
||||
})
|
||||
// 所有签到模式的日历均入栈;上层扫码、二维码或定位弹框单独入栈,返回时逐层关闭。
|
||||
if (!this.calendarHistoryOpen) {
|
||||
this.calendarHistoryOpen = true
|
||||
window.h5PopupHistory.open(this.popupHistoryToken, "calendar")
|
||||
}
|
||||
},
|
||||
dateKey(item) {
|
||||
const value = item && (item.courseDate || item.courseStartTime)
|
||||
@@ -230,18 +283,43 @@ module.exports = {
|
||||
}
|
||||
},
|
||||
onSign(courseTime) {
|
||||
// 确认框关闭或定位提交期间不再切换时段,避免重复打卡和签到对象错位。
|
||||
if (this.formLoading || this.positionConfirmVisible || this.signVisible || this.signCodePreview || this.closingPopupKey) return
|
||||
this.selectCourseTime = courseTime
|
||||
if (this.row.signType === 1) {
|
||||
this.signVisible = true
|
||||
this.$nextTick(() => this.$refs.scanCodeRef.init())
|
||||
window.h5PopupHistory.open(this.popupHistoryToken, "scan")
|
||||
this.$nextTick(() => {
|
||||
if (this.signVisible) this.$refs.scanCodeRef.init()
|
||||
})
|
||||
} else if (this.row.signType === 2) {
|
||||
this.makeCode()
|
||||
} else if (this.row.signType === 3 && this.moduleType === "brand") {
|
||||
this.positionSign()
|
||||
this.positionConfirmVisible = true
|
||||
window.h5PopupHistory.open(this.popupHistoryToken, "position-confirm")
|
||||
} else {
|
||||
this.$toast("此签到模式正在升级中")
|
||||
}
|
||||
},
|
||||
/**
|
||||
* popupKey 为 calendar(日历)、scan(扫码)、sign-code(二维码)或 position-confirm(定位确认);无返回值。
|
||||
* 仅请求关闭栈顶弹层,由全局历史回调同步显示状态,防止连续点击造成多次返回。
|
||||
*/
|
||||
closeSignPopup(popupKey) {
|
||||
if (this.closingPopupKey) return
|
||||
if (window.h5PopupHistory.close(this.popupHistoryToken, popupKey)) {
|
||||
this.closingPopupKey = popupKey
|
||||
}
|
||||
},
|
||||
// 预览定位不提交签到;点击确认时重新获取位置,失败时保留弹框和当前时段供重试。
|
||||
onConfirmPositionSign() {
|
||||
if (this.formLoading || this.locationLoading || this.closingPopupKey || !this.positionConfirmVisible) return
|
||||
this.positionSign()
|
||||
},
|
||||
// 退场动画完成后释放地图;快速重新打开时保留新弹框正在使用的实例。
|
||||
onPositionPopupClosed() {
|
||||
if (!this.positionConfirmVisible) this.destroyMap()
|
||||
},
|
||||
onSignClose() {
|
||||
if (this.$refs.scanCodeRef) this.$refs.scanCodeRef.closeScan()
|
||||
},
|
||||
@@ -250,90 +328,214 @@ module.exports = {
|
||||
? "/platform/family/mine/passiveScan"
|
||||
: "/platform/trainSignUp/mine/passiveScan"
|
||||
const content = jrQrcode.getQrBase64(baseUrl + "?id=" + this.selectCourseTime.id)
|
||||
vant.ImagePreview([content])
|
||||
// 二维码预览关闭时同步回退历史;历史回调先释放引用,避免再次触发返回。
|
||||
const preview = vant.ImagePreview({
|
||||
images: [content],
|
||||
closeOnPopstate: false,
|
||||
onClose: () => {
|
||||
if (this.signCodePreview === preview) this.closeSignPopup("sign-code")
|
||||
}
|
||||
})
|
||||
this.signCodePreview = preview
|
||||
window.h5PopupHistory.open(this.popupHistoryToken, "sign-code")
|
||||
},
|
||||
/**
|
||||
* 确认后提交所选时段的点位签到,无方法参数、无返回值。
|
||||
* 接口参数:timeId 为报名时段 ID,courseId 为课程 ID,lat/lng 为定位经纬度。
|
||||
* 返回对象的 code 为 0 表示成功,msg 为失败提示;定位或请求结束后释放提交状态。
|
||||
*/
|
||||
positionSign() {
|
||||
this.getLocation(coords => {
|
||||
if (!coords) return
|
||||
if (this.formLoading || this.locationLoading) return
|
||||
const courseTime = this.selectCourseTime
|
||||
const course = this.row
|
||||
const requestId = ++this.locationRequestId
|
||||
this.formLoading = true
|
||||
this.locationMessage = "正在获取当前位置..."
|
||||
this.getLocation((coords, errorMessage) => {
|
||||
// 定位失败或等待定位时已退出地图弹框,不再发起签到请求。
|
||||
if (!this.visible || !this.positionConfirmVisible || requestId !== this.locationRequestId || this.row !== course || this.selectCourseTime !== courseTime) {
|
||||
this.formLoading = false
|
||||
return
|
||||
}
|
||||
this.showCurrentLocation(coords, errorMessage)
|
||||
if (!coords) {
|
||||
this.formLoading = false
|
||||
return
|
||||
}
|
||||
// 地图展示使用转换后的坐标,签到请求仍传手机返回的原始 WGS-84 经纬度。
|
||||
this.$axios.post("/platform/trainSignUp/mine/positionSign", {
|
||||
timeId: this.selectCourseTime.id,
|
||||
courseId: this.row.id,
|
||||
timeId: courseTime.id,
|
||||
courseId: course.id,
|
||||
lat: coords.lat,
|
||||
lng: coords.lng
|
||||
}).then(res => {
|
||||
if (res.code === 0) {
|
||||
this.selectCourseTime.isAttend = true
|
||||
this.$set(courseTime, "isAttend", true)
|
||||
this.$toast("签到成功")
|
||||
// 签到完成后清理本组件的弹框历史,返回时不再出现已完成的确认框或日历。
|
||||
return window.h5PopupHistory.clear(this.popupHistoryToken)
|
||||
} else {
|
||||
this.$toast(res.msg)
|
||||
}
|
||||
}).finally(() => {
|
||||
this.formLoading = false
|
||||
})
|
||||
})
|
||||
},
|
||||
/**
|
||||
* callback 接收浏览器 WGS-84 定位对象 {lat, lng, accuracy} 和可选失败提示;无返回值。
|
||||
* lat/lng 为纬度/经度,accuracy 为精度半径(米);失败时坐标为 null。
|
||||
* 预览和确认签到均重新定位,不使用上一次定位缓存,是否提交由调用方决定。
|
||||
*/
|
||||
getLocation(callback) {
|
||||
if (!navigator.geolocation) {
|
||||
this.$toast("当前浏览器不支持定位功能")
|
||||
callback(null)
|
||||
callback(null, "当前浏览器不支持定位功能")
|
||||
return
|
||||
}
|
||||
const loading = vant.Toast.loading({ message: "获取定位中...", forbidClick: false, duration: 0 })
|
||||
navigator.geolocation.getCurrentPosition(position => {
|
||||
loading.close()
|
||||
callback({ lat: position.coords.latitude, lng: position.coords.longitude })
|
||||
callback({ lat: position.coords.latitude, lng: position.coords.longitude, accuracy: position.coords.accuracy })
|
||||
}, error => {
|
||||
loading.close()
|
||||
const messageMap = {
|
||||
1: "请允许浏览器获取位置信息",
|
||||
2: "无法获取当前位置",
|
||||
3: "定位超时,请重试"
|
||||
}
|
||||
this.$toast(messageMap[error.code] || "定位失败,请稍后重试")
|
||||
callback(null)
|
||||
}, { enableHighAccuracy: true, timeout: 10000, maximumAge: 60000 })
|
||||
callback(null, messageMap[error.code] || "定位失败,请稍后重试")
|
||||
}, { enableHighAccuracy: true, timeout: 10000, maximumAge: 0 })
|
||||
},
|
||||
// 打开地图和点击重新定位只刷新预览;请求编号隔离已关闭或已重新打开的弹框。
|
||||
refreshLocation() {
|
||||
if (!this.map || !this.positionConfirmVisible || this.locationLoading || this.formLoading || this.closingPopupKey) return
|
||||
const requestId = ++this.locationRequestId
|
||||
this.locationLoading = true
|
||||
this.locationMessage = "正在获取当前位置..."
|
||||
this.getLocation((coords, errorMessage) => {
|
||||
if (requestId !== this.locationRequestId || !this.map || !this.positionConfirmVisible) return
|
||||
this.locationLoading = false
|
||||
this.showCurrentLocation(coords, errorMessage)
|
||||
})
|
||||
},
|
||||
/**
|
||||
* coords 为手机原始定位对象,errorMessage 为失败提示;无返回值,不提交签到。
|
||||
* 国内坐标复用项目工具转换为腾讯地图使用的 GCJ-02,境外保留原坐标。
|
||||
* 定位失败时移除旧位置点,避免将过期点位误认为当前位置。
|
||||
*/
|
||||
showCurrentLocation(coords, errorMessage) {
|
||||
if (!coords) {
|
||||
if (this.userMarkerLayer) this.userMarkerLayer.setGeometries([])
|
||||
this.locationMessage = errorMessage || "暂未获取到当前位置"
|
||||
this.$toast(this.locationMessage)
|
||||
return
|
||||
}
|
||||
this.locationMessage = Number.isFinite(coords.accuracy)
|
||||
? "定位精度约 " + Math.ceil(coords.accuracy) + " 米"
|
||||
: "已显示我的位置"
|
||||
if (!this.map || !this.userMarkerLayer) return
|
||||
const converted = this.$coordinateUtil.outOfChina(coords.lng, coords.lat)
|
||||
? [coords.lat, coords.lng]
|
||||
: this.$coordinateUtil.wgs84ToGcj02(coords.lng, coords.lat)
|
||||
const userPosition = new TMap.LatLng(converted[0], converted[1])
|
||||
this.userMarkerLayer.setGeometries([{ id: "current-user", styleId: "user", position: userPosition }])
|
||||
const activityPosition = this.markerLayer.getGeometries()[0].position
|
||||
// 同时显示签到点和我的位置,并留出边距;两点重合时仍保留可辨识的地图范围。
|
||||
const latPadding = Math.max(Math.abs(activityPosition.getLat() - converted[0]) * 0.3, 0.0003)
|
||||
const lngPadding = Math.max(Math.abs(activityPosition.getLng() - converted[1]) * 0.3, 0.0003)
|
||||
const bounds = new TMap.LatLngBounds(
|
||||
new TMap.LatLng(Math.min(activityPosition.getLat(), converted[0]) - latPadding, Math.min(activityPosition.getLng(), converted[1]) - lngPadding),
|
||||
new TMap.LatLng(Math.max(activityPosition.getLat(), converted[0]) + latPadding, Math.max(activityPosition.getLng(), converted[1]) + lngPadding)
|
||||
)
|
||||
this.map.fitBounds(bounds)
|
||||
},
|
||||
// 仅在侧滑弹框完全展开后创建地图,保证容器已有可用尺寸,避免动画期间地图空白。
|
||||
initMap() {
|
||||
if (!this.row.courseLocationCoordinates || typeof TMap === "undefined") return
|
||||
if (!this.positionConfirmVisible || !this.showMap || !this.row.courseLocationCoordinates || typeof TMap === "undefined") return
|
||||
const position = JSON.parse(this.row.courseLocationCoordinates)
|
||||
const center = new TMap.LatLng(position[0], position[1])
|
||||
if (this.map) this.map.destroy()
|
||||
this.destroyMap()
|
||||
this.map = new TMap.Map(this.mapContainerId, { zoom: 16, center })
|
||||
this.markerLayer = new TMap.MultiMarker({
|
||||
map: this.map,
|
||||
geometries: [{ id: "activity", position: center }]
|
||||
})
|
||||
// 蓝点与“我的位置”标签独立于红色签到点;内嵌 SVG 不依赖外部图片请求。
|
||||
const userIcon = '<svg xmlns="http://www.w3.org/2000/svg" width="80" height="48" viewBox="0 0 80 48">'
|
||||
+ '<rect x="2" y="1" width="76" height="22" rx="11" fill="#1989fa"/>'
|
||||
+ '<text x="40" y="16" text-anchor="middle" font-family="sans-serif" font-size="12" fill="white">我的位置</text>'
|
||||
+ '<circle cx="40" cy="36" r="11" fill="#1989fa" fill-opacity="0.2"/>'
|
||||
+ '<circle cx="40" cy="36" r="6" fill="#1989fa" stroke="white" stroke-width="2"/></svg>'
|
||||
this.userMarkerLayer = new TMap.MultiMarker({
|
||||
map: this.map,
|
||||
zIndex: 2,
|
||||
styles: { user: new TMap.MarkerStyle({ width: 80, height: 48, anchor: { x: 40, y: 36 }, src: "data:image/svg+xml;charset=UTF-8," + encodeURIComponent(userIcon) }) },
|
||||
geometries: []
|
||||
})
|
||||
this.circle = new TMap.MultiCircle({
|
||||
map: this.map,
|
||||
geometries: [{ center, radius: this.row.radius || 100 }]
|
||||
})
|
||||
this.refreshLocation()
|
||||
},
|
||||
// 关闭弹框或销毁组件时释放地图及覆盖物引用,避免重复打开后残留地图实例。
|
||||
destroyMap() {
|
||||
// 使尚未返回的定位请求失效,避免旧结果写入下一次打开的地图。
|
||||
this.locationRequestId += 1
|
||||
this.locationLoading = false
|
||||
this.locationMessage = ""
|
||||
if (this.map) this.map.destroy()
|
||||
this.map = null
|
||||
this.markerLayer = null
|
||||
this.userMarkerLayer = null
|
||||
this.circle = null
|
||||
}
|
||||
},
|
||||
created() {
|
||||
// 复用布局已加载的全局历史管理器,不在组件内注册 popstate 事件。
|
||||
this.popupHistoryToken = window.h5PopupHistory.register((popupKey) => {
|
||||
this.closingPopupKey = ""
|
||||
if (popupKey === "calendar") {
|
||||
this.calendarHistoryOpen = false
|
||||
this.visible = false
|
||||
} else if (popupKey === "scan") {
|
||||
this.signVisible = false
|
||||
} else if (popupKey === "sign-code") {
|
||||
const preview = this.signCodePreview
|
||||
this.signCodePreview = null
|
||||
if (preview) preview.close()
|
||||
} else if (popupKey === "position-confirm") {
|
||||
this.positionConfirmVisible = false
|
||||
this.locationRequestId += 1
|
||||
this.locationLoading = false
|
||||
}
|
||||
})
|
||||
},
|
||||
beforeDestroy() {
|
||||
// 页面离开时停止扫码并释放地图,再清理历史并注销回调,避免摄像头继续占用。
|
||||
if (this.signVisible) this.onSignClose()
|
||||
this.positionConfirmVisible = false
|
||||
this.destroyMap()
|
||||
window.h5PopupHistory.clear(this.popupHistoryToken).then(() => {
|
||||
window.h5PopupHistory.unregister(this.popupHistoryToken)
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.activity-schedule-calendar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
border-radius: 18px 18px 0 0;
|
||||
background: #f4f7fc;
|
||||
}
|
||||
|
||||
.activity-schedule-calendar /deep/ .van-action-sheet__header {
|
||||
height: 50px;
|
||||
color: #1f2f43;
|
||||
background: #fff;
|
||||
font-size: 16px;
|
||||
line-height: 50px;
|
||||
}
|
||||
|
||||
.activity-schedule-calendar /deep/ .van-action-sheet__close {
|
||||
top: 14px;
|
||||
color: #7e899a;
|
||||
font-size: 21px;
|
||||
.activity-schedule-calendar > .van-nav-bar {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.activity-schedule-calendar__body {
|
||||
height: 100%;
|
||||
padding: 14px 14px 24px;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
padding: 18px 14px calc(24px + env(safe-area-inset-bottom));
|
||||
overflow-y: auto;
|
||||
overflow-anchor: none;
|
||||
box-sizing: border-box;
|
||||
@@ -383,8 +585,9 @@ module.exports = {
|
||||
.activity-schedule-calendar__day {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 38px;
|
||||
height: 48px;
|
||||
width: 100%;
|
||||
max-width: 44px;
|
||||
height: 52px;
|
||||
margin: 1px auto;
|
||||
padding: 0;
|
||||
align-items: center;
|
||||
@@ -400,13 +603,13 @@ module.exports = {
|
||||
|
||||
.activity-schedule-calendar__day > span {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
@@ -436,15 +639,27 @@ module.exports = {
|
||||
color: #bdc5d0;
|
||||
}
|
||||
|
||||
.activity-schedule-calendar__day.is-signed > span {
|
||||
color: #148a52;
|
||||
}
|
||||
|
||||
.activity-schedule-calendar__day.is-available > span {
|
||||
color: #1675d1;
|
||||
}
|
||||
|
||||
.activity-schedule-calendar__day.is-pending > span {
|
||||
color: #66758a;
|
||||
}
|
||||
|
||||
/* 选中蓝底与外层状态环独立:切换日期时仍能分辨已签到、可签到和未开始。 */
|
||||
.activity-schedule-calendar__day.is-selected > span {
|
||||
flex: 0 0 40px;
|
||||
width: 40px;
|
||||
min-width: 40px;
|
||||
height: 40px;
|
||||
min-height: 40px;
|
||||
color: #fff;
|
||||
background: #1989fa;
|
||||
box-shadow: 0 5px 12px rgba(25, 137, 250, .22);
|
||||
box-shadow: 0 0 0 2px #fff;
|
||||
}
|
||||
|
||||
.activity-schedule-calendar__day.is-today .activity-schedule-calendar__day-number {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.activity-schedule-calendar__day.is-selected .activity-schedule-calendar__today-label {
|
||||
@@ -454,60 +669,150 @@ module.exports = {
|
||||
|
||||
.activity-schedule-calendar__day-status {
|
||||
position: absolute;
|
||||
bottom: -3px;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
transform: translateX(-50%);
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
transform: translate(-50%, -50%);
|
||||
border-radius: 50%;
|
||||
box-sizing: border-box;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.activity-schedule-calendar__day-status.is-signed {
|
||||
background: #1989fa;
|
||||
.activity-schedule-calendar__day-status.is-signed,
|
||||
.activity-schedule-calendar__legend .is-signed {
|
||||
border: 2px solid #19a965;
|
||||
background: #eaf8f1;
|
||||
}
|
||||
|
||||
.activity-schedule-calendar__day-status.is-available {
|
||||
border: 1px solid #1989fa;
|
||||
.activity-schedule-calendar__day-status.is-available,
|
||||
.activity-schedule-calendar__legend .is-available {
|
||||
border: 2px solid #1989fa;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.activity-schedule-calendar__day-status.is-pending {
|
||||
background: #d8dde5;
|
||||
.activity-schedule-calendar__day-status.is-pending,
|
||||
.activity-schedule-calendar__legend .is-pending {
|
||||
border: 2px dashed #98a4b5;
|
||||
}
|
||||
|
||||
.activity-schedule-calendar__legend {
|
||||
display: flex;
|
||||
margin: 12px 4px 16px;
|
||||
margin: 16px 0 20px;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
color: #78869a;
|
||||
font-size: 11px;
|
||||
justify-content: space-between;
|
||||
gap: 6px;
|
||||
color: #58677b;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.activity-schedule-calendar__legend span {
|
||||
display: inline-flex;
|
||||
padding: 8px;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
gap: 6px;
|
||||
border-radius: 18px;
|
||||
background: #fff;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.activity-schedule-calendar__legend i {
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
flex-shrink: 0;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.activity-schedule-calendar__legend .is-solid { background: #1989fa; }
|
||||
.activity-schedule-calendar__legend .is-outline { border: 1px solid #1989fa; }
|
||||
.activity-schedule-calendar__legend .is-muted { background: #d8dde5; }
|
||||
/* 小屏幕每列不足 42px 时缩小圆环,保留日期间距和选中态的白色间隔。 */
|
||||
@media (max-width: 350px) {
|
||||
.activity-schedule-calendar__day-status {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
}
|
||||
|
||||
.activity-schedule-calendar__day > span {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.activity-schedule-calendar__today-label,
|
||||
.activity-schedule-calendar__day.is-selected .activity-schedule-calendar__today-label {
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.activity-schedule-position-popup {
|
||||
background: #f4f7fc;
|
||||
}
|
||||
|
||||
.activity-schedule-position {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.activity-schedule-position > .van-nav-bar {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.activity-schedule-calendar__map {
|
||||
flex: 1 0 240px;
|
||||
width: 100%;
|
||||
height: 150px;
|
||||
margin-bottom: 12px;
|
||||
min-height: 240px;
|
||||
overflow: hidden;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.activity-schedule-position__details {
|
||||
flex-shrink: 0;
|
||||
padding: 16px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.activity-schedule-position__title {
|
||||
color: #263548;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.activity-schedule-position__location-tools {
|
||||
display: flex;
|
||||
margin-bottom: 12px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.activity-schedule-position__location-tools .van-button {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.activity-schedule-position__location-status {
|
||||
color: #78869a;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.activity-schedule-position__text {
|
||||
margin-top: 6px;
|
||||
color: #78869a;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.activity-schedule-position__text .van-icon {
|
||||
margin-right: 4px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.activity-schedule-position__footer {
|
||||
flex-shrink: 0;
|
||||
padding: 12px 16px calc(12px + env(safe-area-inset-bottom));
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.activity-schedule-calendar__schedule-card {
|
||||
|
||||
@@ -8,9 +8,12 @@
|
||||
placeholder="请输入关键词查询地址"
|
||||
@select="handleSelect"
|
||||
:trigger-on-focus="false"
|
||||
:disabled="!map"
|
||||
/>
|
||||
</div>
|
||||
<div id="mapContainer"></div>
|
||||
<el-alert v-if="mapError" :title="mapError" type="error" :closable="false" show-icon></el-alert>
|
||||
<div :id="mapContainerId" class="tmap-container" v-loading="mapLoading"></div>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -33,9 +36,12 @@ module.exports = {
|
||||
data() {
|
||||
return {
|
||||
map: null,
|
||||
// Vue 实例编号区分同页多个地图,避免地图挂到其他弹窗的容器中。
|
||||
mapContainerId: 'tmap-container-' + this._uid,
|
||||
mapLoading: false,
|
||||
mapError: '',
|
||||
mapDisposed: false,
|
||||
poi: this.position,
|
||||
appMapCenterPointX: 0,
|
||||
appMapCenterPointY: 0,
|
||||
markerLayer: null,
|
||||
addressValue: '',
|
||||
circle: null,
|
||||
@@ -43,7 +49,7 @@ module.exports = {
|
||||
},
|
||||
methods: {
|
||||
handleSelect(item) {
|
||||
if (item.location) {
|
||||
if (this.map && item.location) {
|
||||
// 定位到中心点
|
||||
const center = new TMap.LatLng(item.location.lat, item.location.lng)
|
||||
this.map.setCenter(center)
|
||||
@@ -52,41 +58,84 @@ module.exports = {
|
||||
this.poi = [item.location.lat, item.location.lng]
|
||||
}
|
||||
},
|
||||
async querySearchAsync(queryString, cb) {
|
||||
// queryString 为用户输入的地址关键词;接口 data.data 是地点数组,cb 接收带 value 的候选项数组。
|
||||
querySearchAsync(queryString, cb) {
|
||||
if (!queryString) {
|
||||
cb([])
|
||||
return
|
||||
}
|
||||
const res = await this.$axios.post("/platform/TMap/suggestion", {
|
||||
return this.$axios.post("/platform/TMap/suggestion", {
|
||||
keyword: queryString,
|
||||
}).then((res) => {
|
||||
if (this.mapDisposed) return
|
||||
if (res.code === 0 && res.data.status === 0) {
|
||||
res.data.data.forEach((item) => {
|
||||
item.value = item.title + "【详细地址:" + item.address + "】"
|
||||
})
|
||||
cb(res.data.data)
|
||||
} else {
|
||||
this.$message.error((res.data && res.data.message) || res.msg || '地点搜索失败')
|
||||
cb([])
|
||||
}
|
||||
}).catch(() => {
|
||||
if (!this.mapDisposed) cb([])
|
||||
})
|
||||
if (res.code === 0 && res.data.status === 0) {
|
||||
res.data.data.forEach((item) => {
|
||||
item.value = item.title + "【详细地址:" + item.address + "】"
|
||||
})
|
||||
cb(res.data.data)
|
||||
} else {
|
||||
this.$message.error(res.msg)
|
||||
cb([])
|
||||
}
|
||||
},
|
||||
initMap() {
|
||||
const center = new TMap.LatLng(this.appMapCenterPointY, this.appMapCenterPointX)
|
||||
this.map = new TMap.Map('mapContainer', {
|
||||
resizeEnable: true,
|
||||
// 接收 [纬度, 经度],返回数值坐标或 null;拒绝空配置,防止空字符串被当成 0。
|
||||
normalizePosition(position) {
|
||||
if (!Array.isArray(position) || position.length !== 2) return null
|
||||
if (position.some(value => !['number', 'string'].includes(typeof value) || String(value).trim() === '')) return null
|
||||
const point = position.map(value => Number(value))
|
||||
if (!point.every(value => Number.isFinite(value)) || Math.abs(point[0]) > 90 || Math.abs(point[1]) > 180) return null
|
||||
return point
|
||||
},
|
||||
// 已设置的签到点直接用于回显;仅无有效点位时读取默认中心,避免配置请求阻断已有点位。
|
||||
loadMap() {
|
||||
if (this.mapDisposed || this.map || this.mapLoading) return
|
||||
this.mapLoading = true
|
||||
this.mapError = ''
|
||||
return Promise.resolve().then(() => {
|
||||
if (this.mapDisposed) return null
|
||||
if (typeof TMap === 'undefined' || !TMap.Map || !TMap.LatLng) {
|
||||
throw new Error('腾讯地图脚本未加载成功,请刷新页面后重试')
|
||||
}
|
||||
const point = this.normalizePosition(this.poi)
|
||||
if (point) return point
|
||||
return Promise.all([
|
||||
this.getConfigKey('AppMapCenterPointY'),
|
||||
this.getConfigKey('AppMapCenterPointX')
|
||||
]).then(values => {
|
||||
const center = this.normalizePosition(values)
|
||||
if (!center) throw new Error('地图默认中心未配置或无效,请检查系统配置 AppMapCenterPointX(经度)、AppMapCenterPointY(纬度)')
|
||||
return center
|
||||
})
|
||||
}).then(point => {
|
||||
// 弹窗关闭后可能仍收到配置响应,禁止在已销毁的容器中创建地图。
|
||||
if (!this.mapDisposed && point) this.initMap(point)
|
||||
}).catch(error => {
|
||||
if (this.mapDisposed) return
|
||||
this.mapError = (error && (error.message || error.msg)) || '地图初始化失败,请刷新页面后重试'
|
||||
this.destroyMap()
|
||||
}).finally(() => {
|
||||
if (!this.mapDisposed) this.mapLoading = false
|
||||
})
|
||||
},
|
||||
// point 为经过校验的 [纬度, 经度];只设置显示中心,不将默认中心自动保存为签到点。
|
||||
initMap(point) {
|
||||
const center = new TMap.LatLng(point[0], point[1])
|
||||
this.map = new TMap.Map(this.mapContainerId, {
|
||||
zoom: 16,
|
||||
center: center
|
||||
})
|
||||
// 初始化
|
||||
// 标记和签到半径共用同一个中心,点击选点后向表单回传腾讯地图坐标。
|
||||
this.markerLayer = new TMap.MultiMarker({
|
||||
map: this.map,
|
||||
geometries: []
|
||||
});
|
||||
if (this.poi && this.poi.length > 0) {
|
||||
const posi = new TMap.LatLng(this.poi[0], this.poi[1])
|
||||
this.createMarker(posi)
|
||||
this.map.setCenter(posi)
|
||||
} else {
|
||||
this.createMarker(center)
|
||||
// 默认中心仅用于浏览地图;只有已配置的有效坐标才回显签到标记和范围圈。
|
||||
const selectedPoint = this.normalizePosition(this.poi)
|
||||
if (selectedPoint) {
|
||||
this.createMarker(new TMap.LatLng(selectedPoint[0], selectedPoint[1]))
|
||||
}
|
||||
this.map.on("click", (event) => {
|
||||
if (!this.view) {
|
||||
@@ -113,17 +162,24 @@ module.exports = {
|
||||
position: position
|
||||
}
|
||||
]);
|
||||
this.circle = new TMap.MultiCircle({
|
||||
map: this.map,
|
||||
geometries: [{
|
||||
center: position,
|
||||
radius: this.radius,
|
||||
}],
|
||||
});
|
||||
const geometries = [{center: position, radius: this.radius}]
|
||||
// 重复选点复用半径图层,避免每次点击遗留一个空图层。
|
||||
if (this.circle) {
|
||||
this.circle.setGeometries(geometries)
|
||||
} else {
|
||||
this.circle = new TMap.MultiCircle({map: this.map, geometries})
|
||||
}
|
||||
},
|
||||
async getConfigKey(key) {
|
||||
const resp = await this.$axios.post("/open/common/getConfigKey", { key })
|
||||
return resp.data
|
||||
// key 传系统配置名 AppMapCenterPointX/Y;返回 Promise,值为 Result.data 中的经纬度配置字符串。
|
||||
getConfigKey(key) {
|
||||
return this.$axios.post("/open/common/getConfigKey", { key }).then(resp => resp.data)
|
||||
},
|
||||
// 弹窗关闭或初始化失败时释放地图及其图层、事件和 WebGL 资源。
|
||||
destroyMap() {
|
||||
if (this.map) this.map.destroy()
|
||||
this.map = null
|
||||
this.markerLayer = null
|
||||
this.circle = null
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@@ -134,19 +190,19 @@ module.exports = {
|
||||
this.$emit("update:position", newVal)
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
// DOM初始化完成进行地图初始化
|
||||
this.$nextTick(async () => {
|
||||
this.appMapCenterPointX = await this.getConfigKey("AppMapCenterPointX")
|
||||
this.appMapCenterPointY = await this.getConfigKey("AppMapCenterPointY")
|
||||
this.initMap()
|
||||
})
|
||||
mounted() {
|
||||
// 父弹窗显示完成后挂载组件,再等待当前容器完成渲染。
|
||||
this.$nextTick(() => this.loadMap())
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.mapDisposed = true
|
||||
this.destroyMap()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
#mapContainer {
|
||||
.tmap-container {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
|
||||
@@ -399,6 +399,14 @@ layout("/layouts/v4/baseLayout.html"){
|
||||
// 默认选中
|
||||
getMenuOwnerPath() {
|
||||
const pathname = window.location.pathname
|
||||
// API配置表单不是独立菜单,直接刷新表单地址时仍归属API配置菜单。
|
||||
if (pathname === "/platform/personGovernance/apiConfig/form") {
|
||||
return "/platform/personGovernance/apiConfig"
|
||||
}
|
||||
// 会员匹配表单不是独立菜单,直接刷新表单地址时仍归属会员匹配菜单。
|
||||
if (pathname === "/platform/personGovernance/memberMatch/form") {
|
||||
return "/platform/personGovernance/memberMatch"
|
||||
}
|
||||
if (pathname === "/platform/qsv/survey/reportPage") {
|
||||
return "/platform/qsv/survey"
|
||||
}
|
||||
@@ -409,6 +417,12 @@ layout("/layouts/v4/baseLayout.html"){
|
||||
},
|
||||
|
||||
getMenuOwnerFullPath() {
|
||||
if (window.location.pathname === "/platform/personGovernance/apiConfig/form") {
|
||||
return "/platform/personGovernance/apiConfig"
|
||||
}
|
||||
if (window.location.pathname === "/platform/personGovernance/memberMatch/form") {
|
||||
return "/platform/personGovernance/memberMatch"
|
||||
}
|
||||
if (window.location.pathname === "/platform/qsv/survey/reportPage") {
|
||||
return "/platform/qsv/survey"
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@
|
||||
src="https://webapi.amap.com/maps?v=2.0&key=57f0d098ba1b881ecc436c4cfd23bbbf&plugin=AMap.PolyEditor,AMap.Geolocation"
|
||||
></script>
|
||||
|
||||
<script src="https://map.qq.com/api/gljs?v=2.exp&key=MLLBZ-GQECI-ASXG7-5GNOZ-XW2OF-H5BVH"></script>
|
||||
<script src="https://map.qq.com/api/gljs?v=2.exp&key=NYNBZ-K62KI-DQJGU-U4NJ5-CSME7-ULFMU"></script>
|
||||
<script nonce="${cspNonce!}">
|
||||
Vue.config.devtools = true
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -25,6 +25,10 @@
|
||||
<link rel="stylesheet" href="${base!}/assets/platform/plugins/element-ui/lib/theme-chalk/index_custom.css" />
|
||||
<link rel="stylesheet" href="${base!}/assets/platform/plugins/element-ui/lib/theme-chalk/override.css" />
|
||||
|
||||
<!-- 人员数据治理的接口配置表单使用 Cron 可视化选择器。 -->
|
||||
<link rel="stylesheet" href="${base!}/assets/platform/plugins/vue-cron/vcrontab.css" />
|
||||
<script src="${base!}/assets/platform/plugins/vue-cron/vcrontab.umd.min.js"></script>
|
||||
|
||||
<link rel="stylesheet" href="${base!}/assets/platform/plugins/fullcalendar/fullcalendar.css">
|
||||
|
||||
<!-- import common css -->
|
||||
@@ -99,7 +103,7 @@
|
||||
src="https://webapi.amap.com/maps?v=2.0&key=b4fa2e2bc10a725ab007b98a3aa447cc&plugin=AMap.PolyEditor,AMap.Geolocation"
|
||||
></script>-->
|
||||
|
||||
<script src="https://map.qq.com/api/gljs?v=2.exp&key=MLLBZ-GQECI-ASXG7-5GNOZ-XW2OF-H5BVH"></script>
|
||||
<script src="https://map.qq.com/api/gljs?v=2.exp&key=NYNBZ-K62KI-DQJGU-U4NJ5-CSME7-ULFMU"></script>
|
||||
|
||||
<script nonce="${cspNonce!}">
|
||||
// 在加载 lodash 后、使用前插入
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
<!--# layout("/layouts/platform.html"){ #-->
|
||||
<link rel="stylesheet" href="${base!}/assets/platform/css/personGovernance.css?v=1.0.22" />
|
||||
<div
|
||||
id="app"
|
||||
v-cloak
|
||||
class="person-governance-list-page person-governance-page-transition"
|
||||
:class="{'is-ready':pageReady,'is-leaving':pageLeaving}"
|
||||
>
|
||||
<el-card shadow="never" class="person-governance-search-card">
|
||||
<el-form :inline="true" :model="pageForm">
|
||||
<el-form-item label="配置名称"><el-input v-model="pageForm.configName" clearable></el-input></el-form-item>
|
||||
<el-form-item label="同步方式">
|
||||
<el-select v-model="pageForm.sourceType" clearable filterable>
|
||||
<el-option label="请求第三方接口" value="HTTP_API"></el-option>
|
||||
<el-option label="读取系统表数据" value="SYSTEM_TABLE"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="启用状态">
|
||||
<el-select v-model="pageForm.enabled" clearable filterable>
|
||||
<el-option label="启用" :value="true"></el-option>
|
||||
<el-option label="停用" :value="false"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="doSearch">查询</el-button>
|
||||
<el-button @click="resetSearch">重置</el-button>
|
||||
</el-form-item>
|
||||
<el-button class="pull-right" type="primary" icon="el-icon-plus" @click="openType">新增</el-button>
|
||||
</el-form>
|
||||
</el-card>
|
||||
<el-card shadow="never" class="person-governance-list-card">
|
||||
<div class="person-governance-table-wrap">
|
||||
<el-table ref="dataTable" :data="tableData" v-loading="tableLoading" height="100%">
|
||||
<el-table-column
|
||||
type="index"
|
||||
:index="indexMethod"
|
||||
label="序号"
|
||||
width="70"
|
||||
align="center"
|
||||
></el-table-column>
|
||||
<el-table-column prop="configName" label="配置名称" min-width="160"></el-table-column>
|
||||
<el-table-column label="同步方式" width="150">
|
||||
<template v-slot="{row}">
|
||||
{{row.sourceType==='HTTP_API'?'请求第三方接口':'读取系统表数据'}}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="targetTable" label="目标系统表" min-width="150"></el-table-column>
|
||||
<el-table-column prop="lastSyncTime" label="最近同步时间" width="180"></el-table-column>
|
||||
<el-table-column label="是否启用" width="150" align="center">
|
||||
<template v-slot="{row}">
|
||||
{{row.enabled ? '是' : '否'}}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="130">
|
||||
<template v-slot="{row}">
|
||||
<el-button type="text" @click="edit(row)">编辑</el-button>
|
||||
<el-button type="text" class="text-danger" @click="remove(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<!--#include("/layouts/pagination.html"){}#-->
|
||||
</el-card>
|
||||
<el-dialog
|
||||
title="选择API配置类型"
|
||||
:visible.sync="typeVisible"
|
||||
:append-to-body="true"
|
||||
:close-on-click-modal="false"
|
||||
width="520px"
|
||||
custom-class="person-governance-dialog"
|
||||
@closed="handleTypeClosed"
|
||||
>
|
||||
<el-alert title="请选择配置的数据来源,选择后直接进入对应配置页面" type="info" :closable="false"></el-alert>
|
||||
<el-row :gutter="16" class="mt20">
|
||||
<el-col :span="12">
|
||||
<el-card shadow="hover" @click.native="create('HTTP_API')">
|
||||
<h4>请求第三方接口</h4>
|
||||
<p>通过GET、POST等方式获取第三方人员数据</p>
|
||||
<el-link type="primary">选择</el-link>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-card shadow="hover" @click.native="create('SYSTEM_TABLE')">
|
||||
<h4>读取系统表数据</h4>
|
||||
<p>读取第三方已推送到系统表中的人员数据</p>
|
||||
<el-link type="primary">选择</el-link>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<span slot="footer"><el-button @click="closeType">取消</el-button></span>
|
||||
</el-dialog>
|
||||
</div>
|
||||
<script>
|
||||
// 使用独立作用域承载页面实例,避免列表页与表单页通过 PJAX 反复切换时重复声明顶层变量。
|
||||
;(() => {
|
||||
new Vue({
|
||||
el: '#app',
|
||||
mixins: [initTableMixins],
|
||||
data() {
|
||||
return {
|
||||
pageReady: false,
|
||||
pageLeaving: false,
|
||||
typeVisible: false,
|
||||
pendingCreateType: '',
|
||||
pageForm: { configName: '', sourceType: '', enabled: '', pageNumber: 1, pageSize: 10, totalCount: 0 },
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
pageData() {
|
||||
this.$set(this, 'tableLoading', true)
|
||||
return $.post(loc() + '/pageData', this.pageForm)
|
||||
.then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.$set(this, 'tableData', res.data.list)
|
||||
this.$set(this.pageForm, 'totalCount', res.data.totalCount)
|
||||
} else {
|
||||
this.notifyWarning(res.msg)
|
||||
}
|
||||
})
|
||||
.always(() => {
|
||||
this.$set(this, 'tableLoading', false)
|
||||
})
|
||||
},
|
||||
openType() {
|
||||
if (this.pendingCreateType || this.pageLeaving) {
|
||||
return
|
||||
}
|
||||
this.$set(this, 'typeVisible', true)
|
||||
},
|
||||
closeType() {
|
||||
this.$set(this, 'pendingCreateType', '')
|
||||
this.$set(this, 'typeVisible', false)
|
||||
},
|
||||
// 等待弹窗退出动画完成后再离开列表,避免body上的弹窗和遮罩在跳转期间残留。
|
||||
handleTypeClosed() {
|
||||
const type = this.pendingCreateType
|
||||
this.$set(this, 'pendingCreateType', '')
|
||||
if (!type || this.pageLeaving) {
|
||||
return
|
||||
}
|
||||
this.leavePage('/platform/personGovernance/apiConfig/form?sourceType=' + type)
|
||||
},
|
||||
resetSearch() {
|
||||
this.$set(this.pageForm, 'configName', '')
|
||||
this.$set(this.pageForm, 'sourceType', '')
|
||||
this.$set(this.pageForm, 'enabled', '')
|
||||
this.doSearch()
|
||||
},
|
||||
showPage() {
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.dataTable && this.$refs.dataTable.doLayout) {
|
||||
this.$refs.dataTable.doLayout()
|
||||
}
|
||||
window.requestAnimationFrame(() => {
|
||||
this.$set(this, 'pageReady', true)
|
||||
})
|
||||
})
|
||||
},
|
||||
leavePage(url) {
|
||||
this.$set(this, 'pageLeaving', true)
|
||||
window.setTimeout(() => {
|
||||
// 仅替换右侧内容区域,保留左侧导航实例及已经加载的菜单数据。
|
||||
commonUtil.pjaxPush(url)
|
||||
}, 180)
|
||||
},
|
||||
// 弹窗关闭后通过 PJAX 打开表单页,避免整个后台布局重新加载。
|
||||
create(type) {
|
||||
// 关闭期间忽略重复选择;仅记录目标类型,由closed事件统一触发跳转。
|
||||
if (!this.typeVisible || this.pendingCreateType || this.pageLeaving) {
|
||||
return
|
||||
}
|
||||
this.$set(this, 'pendingCreateType', type)
|
||||
this.$set(this, 'typeVisible', false)
|
||||
},
|
||||
edit(row) {
|
||||
this.leavePage('/platform/personGovernance/apiConfig/form?id=' + row.id)
|
||||
},
|
||||
remove(row) {
|
||||
this.$confirm('删除后不可继续同步,确认删除吗?', '提示').then(() => {
|
||||
$.post(loc() + '/delete', { id: row.id })
|
||||
.then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.notifySuccess('删除成功')
|
||||
this.pageData()
|
||||
} else {
|
||||
this.notifyWarning(res.msg)
|
||||
}
|
||||
})
|
||||
.always(() => {})
|
||||
})
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.pageData().always(() => {
|
||||
this.showPage()
|
||||
})
|
||||
},
|
||||
})
|
||||
})()
|
||||
</script>
|
||||
<!--# } #-->
|
||||
|
||||
@@ -0,0 +1,643 @@
|
||||
<!--# layout("/layouts/platform.html"){ #-->
|
||||
<link rel="stylesheet" href="${base!}/assets/platform/css/personGovernance.css?v=1.0.24" />
|
||||
<div
|
||||
id="app"
|
||||
v-cloak
|
||||
class="person-governance-form-page person-governance-page-transition"
|
||||
:class="{'is-ready':pageReady,'is-leaving':pageLeaving}"
|
||||
>
|
||||
<div class="person-governance-page-header">
|
||||
<el-page-header @back="back"></el-page-header>
|
||||
</div>
|
||||
<el-card shadow="never" class="person-governance-form-basic-card">
|
||||
<el-alert
|
||||
class="person-governance-member-binding-hint"
|
||||
title="每个API配置仅可绑定一个会员匹配配置。"
|
||||
type="info"
|
||||
:closable="false"
|
||||
></el-alert>
|
||||
<el-form :model="form" label-width="110px">
|
||||
<div class="person-governance-form-grid">
|
||||
<el-form-item label="配置名称"><el-input v-model="form.configName"></el-input></el-form-item>
|
||||
<el-form-item label="选择API配置">
|
||||
<el-select
|
||||
v-model="form.apiConfigId"
|
||||
:disabled="!!form.id"
|
||||
clearable
|
||||
filterable
|
||||
style="width: 100%"
|
||||
@change="apiChanged"
|
||||
>
|
||||
<el-option
|
||||
v-for="a in apiConfigs"
|
||||
:key="a.id"
|
||||
:disabled="!a.selectable"
|
||||
:label="a.selectable?a.configName:a.configName+'(已绑定:'+a.matchConfigName+')'"
|
||||
:value="a.id"
|
||||
></el-option>
|
||||
</el-select>
|
||||
<small v-if="form.id">该API配置已与当前会员匹配配置绑定,编辑时不可修改</small>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="!form.id" label="沿用历史匹配">
|
||||
<el-select v-model="copyId" clearable filterable @change="copyHistory">
|
||||
<el-option v-for="h in histories" :key="h.id" :label="h.configName" :value="h.id"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="启用"><el-switch v-model="form.enabled"></el-switch></el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
</el-card>
|
||||
<el-tabs
|
||||
v-model="activeRuleType"
|
||||
class="person-governance-member-type-tabs"
|
||||
@tab-click="ruleTabChanged"
|
||||
>
|
||||
<el-tab-pane label="工会会员匹配规则配置" name="UNION_MEMBER"></el-tab-pane>
|
||||
<el-tab-pane label="福利会员匹配规则配置" name="WELFARE_MEMBER"></el-tab-pane>
|
||||
</el-tabs>
|
||||
<el-row :gutter="20" class="person-governance-form-content-row">
|
||||
<el-col :span="16">
|
||||
<el-card shadow="never" class="person-governance-member-rules">
|
||||
<div class="person-governance-member-rules-header">
|
||||
<h3>{{activeRuleTitle}}</h3>
|
||||
<el-alert
|
||||
title="条件及子条件组之间可切换且/或;每个条件组(含子条件组)都作为独立括号。"
|
||||
type="info"
|
||||
:closable="false"
|
||||
></el-alert>
|
||||
</div>
|
||||
<div class="person-governance-member-rules-content">
|
||||
<div v-for="(g,gi) in groups" :key="gi">
|
||||
<el-divider v-if="gi>0" class="person-governance-member-group-relation">
|
||||
<el-radio-group
|
||||
:value="g.joinLogic"
|
||||
size="mini"
|
||||
aria-label="与上一条件组的关系"
|
||||
@input="setGroupLogic(g, $event)"
|
||||
>
|
||||
<el-radio-button label="AND">且</el-radio-button>
|
||||
<el-radio-button label="OR">或</el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-divider>
|
||||
<member-condition-group
|
||||
:group="g"
|
||||
:group-path="String(gi+1)"
|
||||
:depth="0"
|
||||
:fields="fields"
|
||||
:create-condition="emptyCondition"
|
||||
@load-dict="loadDict"
|
||||
@dict-changed="dictChanged"
|
||||
@copy-self="copyGroup(gi)"
|
||||
@delete-self="removeGroup(gi)"
|
||||
></member-condition-group>
|
||||
</div>
|
||||
<el-button class="mt20" type="primary" plain style="width: 100%" @click="addGroup">
|
||||
新增条件组
|
||||
</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-card shadow="never" class="person-governance-member-preview">
|
||||
<h3>{{activePreviewTitle}}</h3>
|
||||
<p>
|
||||
预计匹配
|
||||
<strong>{{previewTotal}}</strong>
|
||||
人
|
||||
</p>
|
||||
<el-button
|
||||
type="primary"
|
||||
:disabled="!form.id || !groups.length"
|
||||
:loading="previewLoading"
|
||||
@click="calculate"
|
||||
>
|
||||
试算匹配
|
||||
</el-button>
|
||||
<div class="person-governance-member-preview-table">
|
||||
<el-table ref="previewTable" :data="previewRows" height="100%" v-loading="previewLoading">
|
||||
<el-table-column prop="loginname" label="工号" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column prop="username" label="姓名" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column prop="unitName" label="单位" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column prop="unionName" label="工会" show-overflow-tooltip></el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="person-governance-member-preview-pagination">
|
||||
<el-pagination
|
||||
@current-change="previewPageNumberChange"
|
||||
:current-page="previewPageForm.pageNumber"
|
||||
:page-size="previewPageForm.pageSize"
|
||||
layout="total, prev, pager, next"
|
||||
:total="previewPageForm.totalCount"
|
||||
></el-pagination>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<div class="text-right person-governance-form-footer">
|
||||
<el-button @click="back">取消</el-button>
|
||||
<el-button type="primary" :loading="formLoading" @click="save">保存</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<script type="text/x-template" id="member-condition-group-template">
|
||||
<div class="person-governance-member-group" :class="{'is-nested':depth>0}">
|
||||
<div class="person-governance-member-group-header">
|
||||
<strong>条件组 {{groupPath}}</strong>
|
||||
<span class="pull-right">
|
||||
<el-button type="text" @click="$emit('copy-self')">复制条件组</el-button>
|
||||
<el-button type="text" class="text-danger" @click="$emit('delete-self')">删除条件组</el-button>
|
||||
</span>
|
||||
</div>
|
||||
<el-table
|
||||
v-if="group.conditions.length"
|
||||
:data="conditionRows"
|
||||
:span-method="conditionSpan"
|
||||
:row-class-name="conditionRowClass"
|
||||
border
|
||||
>
|
||||
<el-table-column label="字典编码">
|
||||
<template v-slot="{row}">
|
||||
<el-radio-group
|
||||
v-if="row.isRelation"
|
||||
:value="row.condition.joinLogic"
|
||||
size="mini"
|
||||
aria-label="与上一条件的关系"
|
||||
@input="setConditionLogic(row.condition, $event)"
|
||||
>
|
||||
<el-radio-button label="AND">且</el-radio-button>
|
||||
<el-radio-button label="OR">或</el-radio-button>
|
||||
</el-radio-group>
|
||||
<el-input v-else v-model="row.dictCode" @change="$emit('load-dict', row)">
|
||||
<el-button slot="append" @click="$emit('load-dict', row)">加载</el-button>
|
||||
</el-input>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="字典数据">
|
||||
<template v-slot="{row}">
|
||||
<el-select
|
||||
v-model="row.dictItemId"
|
||||
clearable
|
||||
filterable
|
||||
@change="$emit('dict-changed', row)"
|
||||
>
|
||||
<el-option
|
||||
v-for="d in row.dictItems||[]"
|
||||
:key="d.id"
|
||||
:label="d.name+'('+d.code+')'"
|
||||
:value="d.id"
|
||||
></el-option>
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="映射字段">
|
||||
<template v-slot="{row}">
|
||||
<el-select v-model="row.mappedField" clearable filterable>
|
||||
<el-option
|
||||
v-for="f in fields"
|
||||
:key="f.targetField"
|
||||
:label="f.targetComment+'('+f.targetField+')'"
|
||||
:value="f.targetField"
|
||||
></el-option>
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="匹配方式">
|
||||
<template v-slot="{row}">
|
||||
<el-select v-model="row.operator" clearable filterable>
|
||||
<el-option label="等于" value="EQ"></el-option>
|
||||
<el-option label="不等于" value="NE"></el-option>
|
||||
<el-option label="包含" value="CONTAINS"></el-option>
|
||||
<el-option label="在集合中" value="IN"></el-option>
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="匹配值">
|
||||
<template v-slot="{row}"><el-input v-model="row.matchValue"></el-input></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="70">
|
||||
<template v-slot="{$index}">
|
||||
<el-button type="text" class="text-danger" @click="removeCondition($index)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="person-governance-member-group-actions">
|
||||
<el-button type="primary" plain @click="addCondition">新增条件</el-button>
|
||||
<el-button type="primary" plain @click="addChildGroup">新增子条件组</el-button>
|
||||
</div>
|
||||
<div v-for="(child,childIndex) in group.groups" :key="childIndex">
|
||||
<el-divider
|
||||
v-if="group.conditions.length||childIndex>0"
|
||||
class="person-governance-member-group-relation person-governance-member-child-relation"
|
||||
>
|
||||
<el-radio-group
|
||||
:value="child.joinLogic"
|
||||
size="mini"
|
||||
aria-label="与上一条件或条件组的关系"
|
||||
@input="setGroupLogic(child, $event)"
|
||||
>
|
||||
<el-radio-button label="AND">且</el-radio-button>
|
||||
<el-radio-button label="OR">或</el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-divider>
|
||||
<member-condition-group
|
||||
:group="child"
|
||||
:group-path="groupPath+'.'+(childIndex+1)"
|
||||
:depth="depth+1"
|
||||
:fields="fields"
|
||||
:create-condition="createCondition"
|
||||
@load-dict="$emit('load-dict', $event)"
|
||||
@dict-changed="$emit('dict-changed', $event)"
|
||||
@copy-self="copyChildGroup(childIndex)"
|
||||
@delete-self="deleteChildGroup(childIndex)"
|
||||
></member-condition-group>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
<script>
|
||||
// 使用独立作用域承载页面常量、组件和实例,避免 PJAX 多次进入时重复声明。
|
||||
;(() => {
|
||||
const apiBase = '/platform/personGovernance/apiConfig'
|
||||
const memberBase = '/platform/personGovernance/memberMatch'
|
||||
Vue.component('member-condition-group', {
|
||||
template: '#member-condition-group-template',
|
||||
props: {
|
||||
group: { type: Object, required: true },
|
||||
groupPath: { type: String, required: true },
|
||||
depth: { type: Number, required: true },
|
||||
fields: { type: Array, required: true },
|
||||
createCondition: { type: Function, required: true },
|
||||
},
|
||||
computed: {
|
||||
conditionRows() {
|
||||
const rows = []
|
||||
this.group.conditions.forEach((condition, index) => {
|
||||
if (index > 0) rows.push({ isRelation: true, condition: condition })
|
||||
rows.push(condition)
|
||||
})
|
||||
return rows
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
conditionSpan({ row, columnIndex }) {
|
||||
if (row.isRelation) return columnIndex === 0 ? [1, 6] : [0, 0]
|
||||
return [1, 1]
|
||||
},
|
||||
conditionRowClass({ row }) {
|
||||
return row.isRelation ? 'person-governance-member-condition-relation' : ''
|
||||
},
|
||||
setConditionLogic(condition, value) {
|
||||
this.$set(condition, 'joinLogic', value)
|
||||
},
|
||||
setGroupLogic(group, value) {
|
||||
this.$set(group, 'joinLogic', value)
|
||||
},
|
||||
addCondition() {
|
||||
this.group.conditions.push(this.createCondition())
|
||||
},
|
||||
// 表格包含连接行,实际条件索引为展示行索引的一半。
|
||||
removeCondition(rowIndex) {
|
||||
this.group.conditions.splice(rowIndex / 2, 1)
|
||||
},
|
||||
addChildGroup() {
|
||||
this.group.groups.push({
|
||||
joinLogic: 'OR',
|
||||
conditions: [this.createCondition()],
|
||||
groups: [],
|
||||
})
|
||||
},
|
||||
copyChildGroup(index) {
|
||||
this.group.groups.splice(index + 1, 0, JSON.parse(JSON.stringify(this.group.groups[index])))
|
||||
},
|
||||
deleteChildGroup(index) {
|
||||
this.group.groups.splice(index, 1)
|
||||
},
|
||||
},
|
||||
})
|
||||
new Vue({
|
||||
el: '#app',
|
||||
mixins: [initTableMixins],
|
||||
data() {
|
||||
return {
|
||||
pageReady: false,
|
||||
pageLeaving: false,
|
||||
form: { id: '', configName: '', apiConfigId: '', enabled: true },
|
||||
copyId: '',
|
||||
apiConfigs: [],
|
||||
histories: [],
|
||||
fields: [],
|
||||
activeRuleType: 'UNION_MEMBER',
|
||||
ruleGroups: {
|
||||
UNION_MEMBER: [],
|
||||
WELFARE_MEMBER: [],
|
||||
},
|
||||
previewByType: {
|
||||
UNION_MEMBER: {
|
||||
rows: [],
|
||||
total: 0,
|
||||
loading: false,
|
||||
pageForm: { pageNumber: 1, pageSize: 10, totalCount: 0 },
|
||||
},
|
||||
WELFARE_MEMBER: {
|
||||
rows: [],
|
||||
total: 0,
|
||||
loading: false,
|
||||
pageForm: { pageNumber: 1, pageSize: 10, totalCount: 0 },
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
groups() {
|
||||
return this.ruleGroups[this.activeRuleType]
|
||||
},
|
||||
currentPreview() {
|
||||
return this.previewByType[this.activeRuleType]
|
||||
},
|
||||
previewRows() {
|
||||
return this.currentPreview.rows
|
||||
},
|
||||
previewTotal() {
|
||||
return this.currentPreview.total
|
||||
},
|
||||
previewLoading() {
|
||||
return this.currentPreview.loading
|
||||
},
|
||||
previewPageForm() {
|
||||
return this.currentPreview.pageForm
|
||||
},
|
||||
activeRuleTitle() {
|
||||
return this.activeRuleType === 'WELFARE_MEMBER'
|
||||
? '福利会员匹配规则配置'
|
||||
: '工会会员匹配规则配置'
|
||||
},
|
||||
activePreviewTitle() {
|
||||
return this.activeRuleType === 'WELFARE_MEMBER'
|
||||
? '福利会员匹配结果预览'
|
||||
: '工会会员匹配结果预览'
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
emptyCondition() {
|
||||
return {
|
||||
dictCode: '',
|
||||
dictItemId: '',
|
||||
dictItemCode: '',
|
||||
dictItemName: '',
|
||||
dictItems: [],
|
||||
mappedField: '',
|
||||
operator: 'EQ',
|
||||
matchValue: '',
|
||||
joinLogic: 'AND',
|
||||
}
|
||||
},
|
||||
addGroup() {
|
||||
this.groups.push({ joinLogic: 'OR', conditions: [this.emptyCondition()], groups: [] })
|
||||
},
|
||||
copyGroup(i) {
|
||||
this.groups.splice(i + 1, 0, JSON.parse(JSON.stringify(this.groups[i])))
|
||||
},
|
||||
removeGroup(i) {
|
||||
this.groups.splice(i, 1)
|
||||
},
|
||||
setGroupLogic(group, value) {
|
||||
this.$set(group, 'joinLogic', value)
|
||||
},
|
||||
// 历史记录没有父子关系时保持顶层结构;新数据递归恢复所有子条件组。
|
||||
restoreGroups(rows) {
|
||||
return (rows || []).map((row) => {
|
||||
const sourceGroup = row.group || row
|
||||
return {
|
||||
joinLogic: sourceGroup.joinLogic || 'OR',
|
||||
conditions: (row.conditions || []).map((condition) => ({
|
||||
...condition,
|
||||
joinLogic: condition.joinLogic || 'AND',
|
||||
})),
|
||||
groups: this.restoreGroups(row.groups),
|
||||
}
|
||||
})
|
||||
},
|
||||
// 对工会、福利两棵规则树执行相同处理,避免只遍历顶层条件组。
|
||||
walkGroups(groups, callback) {
|
||||
(groups || []).forEach((group) => {
|
||||
callback(group)
|
||||
this.walkGroups(group.groups, callback)
|
||||
})
|
||||
},
|
||||
loadApis(current) {
|
||||
return $.post(apiBase + '/selectableConfigs', { currentMatchConfigId: current || '' })
|
||||
.then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.$set(this, 'apiConfigs', res.data)
|
||||
}
|
||||
})
|
||||
.always(() => {})
|
||||
},
|
||||
loadHistories() {
|
||||
return $.post(memberBase + '/historyConfigs')
|
||||
.then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.$set(this, 'histories', res.data)
|
||||
}
|
||||
})
|
||||
.always(() => {})
|
||||
},
|
||||
apiChanged(id) {
|
||||
if (!id) {
|
||||
this.$set(this, 'fields', [])
|
||||
Object.keys(this.ruleGroups).forEach((ruleType) => {
|
||||
this.walkGroups(this.ruleGroups[ruleType], (group) => {
|
||||
group.conditions.forEach((condition) => {
|
||||
this.$set(condition, 'mappedField', '')
|
||||
})
|
||||
})
|
||||
})
|
||||
return $.when()
|
||||
}
|
||||
return $.post(apiBase + '/detail', { id: id })
|
||||
.then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.$set(this, 'fields', res.data.mappings || [])
|
||||
}
|
||||
})
|
||||
.always(() => {})
|
||||
},
|
||||
copyHistory(id) {
|
||||
if (!id) {
|
||||
return
|
||||
}
|
||||
$.post(memberBase + '/detail', { id: id })
|
||||
.then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.$set(
|
||||
this.ruleGroups,
|
||||
'UNION_MEMBER',
|
||||
this.restoreGroups(res.data.unionGroups || res.data.groups),
|
||||
)
|
||||
this.$set(
|
||||
this.ruleGroups,
|
||||
'WELFARE_MEMBER',
|
||||
this.restoreGroups(res.data.welfareGroups),
|
||||
)
|
||||
this.loadGroupDicts()
|
||||
}
|
||||
})
|
||||
.always(() => {})
|
||||
},
|
||||
loadDict(row) {
|
||||
if (!row.dictCode) {
|
||||
return
|
||||
}
|
||||
$.post(memberBase + '/dictItems', { dictCode: row.dictCode })
|
||||
.then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.$set(row, 'dictItems', res.data)
|
||||
} else this.notifyWarning(res.msg)
|
||||
})
|
||||
.always(() => {})
|
||||
},
|
||||
dictChanged(row) {
|
||||
const d = (row.dictItems || []).find((v) => v.id === row.dictItemId)
|
||||
if (d) {
|
||||
this.$set(row, 'dictItemCode', d.code)
|
||||
this.$set(row, 'dictItemName', d.name)
|
||||
if (!row.matchValue) {
|
||||
this.$set(row, 'matchValue', d.code)
|
||||
}
|
||||
return
|
||||
}
|
||||
const previousCode = row.dictItemCode
|
||||
this.$set(row, 'dictItemCode', '')
|
||||
this.$set(row, 'dictItemName', '')
|
||||
if (row.matchValue === previousCode) {
|
||||
this.$set(row, 'matchValue', '')
|
||||
}
|
||||
},
|
||||
save() {
|
||||
if (!this.form.configName || !this.form.apiConfigId) {
|
||||
this.notifyWarning('请填写配置名称并选择API配置')
|
||||
return
|
||||
}
|
||||
this.$set(this, 'formLoading', true)
|
||||
$.post(memberBase + '/save', {
|
||||
data: JSON.stringify({
|
||||
config: this.form,
|
||||
unionGroups: this.ruleGroups.UNION_MEMBER,
|
||||
welfareGroups: this.ruleGroups.WELFARE_MEMBER,
|
||||
}),
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.notifySuccess('保存成功')
|
||||
this.$set(this.form, 'id', res.data.id)
|
||||
} else this.notifyWarning(res.msg)
|
||||
})
|
||||
.always(() => {
|
||||
this.$set(this, 'formLoading', false)
|
||||
})
|
||||
},
|
||||
calculate(resetPage) {
|
||||
if (!this.form.id) {
|
||||
return
|
||||
}
|
||||
if (resetPage) {
|
||||
this.$set(this.currentPreview.pageForm, 'pageNumber', 1)
|
||||
}
|
||||
const ruleType = this.activeRuleType
|
||||
const preview = this.previewByType[ruleType]
|
||||
this.$set(preview, 'loading', true)
|
||||
$.post(memberBase + '/calculate', {
|
||||
id: this.form.id,
|
||||
ruleType: ruleType,
|
||||
pageNumber: preview.pageForm.pageNumber,
|
||||
pageSize: preview.pageForm.pageSize,
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.$set(preview, 'rows', res.data.list)
|
||||
this.$set(preview, 'total', res.data.totalCount)
|
||||
this.$set(preview.pageForm, 'totalCount', res.data.totalCount)
|
||||
} else this.notifyWarning(res.msg)
|
||||
})
|
||||
.always(() => {
|
||||
this.$set(preview, 'loading', false)
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.previewTable) this.$refs.previewTable.doLayout()
|
||||
})
|
||||
})
|
||||
},
|
||||
// 匹配预览分页仅切换当前页,查询条件和每页条数保持不变。
|
||||
previewPageNumberChange(number) {
|
||||
this.$set(this.currentPreview.pageForm, 'pageNumber', number)
|
||||
this.calculate()
|
||||
},
|
||||
load(id) {
|
||||
return $.post(memberBase + '/detail', { id: id })
|
||||
.then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.$set(this, 'form', res.data.config)
|
||||
this.$set(
|
||||
this.ruleGroups,
|
||||
'UNION_MEMBER',
|
||||
this.restoreGroups(res.data.unionGroups || res.data.groups),
|
||||
)
|
||||
this.$set(
|
||||
this.ruleGroups,
|
||||
'WELFARE_MEMBER',
|
||||
this.restoreGroups(res.data.welfareGroups),
|
||||
)
|
||||
this.loadGroupDicts()
|
||||
return this.apiChanged(this.form.apiConfigId)
|
||||
}
|
||||
return $.when()
|
||||
})
|
||||
.always(() => {})
|
||||
},
|
||||
showPage() {
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.previewTable) this.$refs.previewTable.doLayout()
|
||||
window.requestAnimationFrame(() => {
|
||||
this.$set(this, 'pageReady', true)
|
||||
})
|
||||
})
|
||||
},
|
||||
// 两类规则分别恢复字典候选项,切换页签时不重新覆盖另一类规则的数据。
|
||||
loadGroupDicts() {
|
||||
Object.keys(this.ruleGroups).forEach((ruleType) => {
|
||||
this.walkGroups(this.ruleGroups[ruleType], (group) => {
|
||||
group.conditions.forEach((condition) => this.loadDict(condition))
|
||||
})
|
||||
})
|
||||
},
|
||||
ruleTabChanged() {
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.previewTable) this.$refs.previewTable.doLayout()
|
||||
})
|
||||
},
|
||||
leavePage(url) {
|
||||
this.$set(this, 'pageLeaving', true)
|
||||
window.setTimeout(() => {
|
||||
// 仅替换右侧内容区域,返回列表时不销毁或重新请求左侧菜单。
|
||||
commonUtil.pjaxPush(url)
|
||||
}, 180)
|
||||
},
|
||||
// 使用 PJAX 返回列表,并保留跳转前的页面淡出效果。
|
||||
back() {
|
||||
this.leavePage(memberBase)
|
||||
},
|
||||
},
|
||||
created() {
|
||||
const id = new URLSearchParams(window.location.search).get('id')
|
||||
const requests = [this.loadApis(id), this.loadHistories()]
|
||||
if (id) {
|
||||
requests.push(this.load(id))
|
||||
} else {
|
||||
this.addGroup()
|
||||
}
|
||||
$.when.apply($, requests).always(() => {
|
||||
this.showPage()
|
||||
})
|
||||
},
|
||||
})
|
||||
})()
|
||||
</script>
|
||||
<!--# } #-->
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
<!--# layout("/layouts/platform.html"){ #-->
|
||||
<link rel="stylesheet" href="${base!}/assets/platform/css/personGovernance.css?v=1.0.23" />
|
||||
<div
|
||||
id="app"
|
||||
v-cloak
|
||||
class="person-governance-list-page person-governance-compact-gap person-governance-page-transition"
|
||||
:class="{'is-ready':pageReady,'is-leaving':pageLeaving}"
|
||||
>
|
||||
<el-card shadow="never" class="person-governance-search-card">
|
||||
<el-form :inline="true" :model="pageForm">
|
||||
<el-form-item label="配置名称"><el-input v-model="pageForm.configName" clearable></el-input></el-form-item>
|
||||
<el-form-item label="API配置">
|
||||
<el-select v-model="pageForm.apiConfigId" clearable filterable>
|
||||
<el-option v-for="a in apiConfigs" :key="a.id" :label="a.configName" :value="a.id"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="启用状态">
|
||||
<el-select v-model="pageForm.enabled" clearable filterable>
|
||||
<el-option label="启用" :value="true"></el-option>
|
||||
<el-option label="停用" :value="false"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="doSearch">查询</el-button>
|
||||
<el-button @click="resetSearch">重置</el-button>
|
||||
</el-form-item>
|
||||
<el-button class="pull-right" type="primary" icon="el-icon-plus" @click="create">新增</el-button>
|
||||
</el-form>
|
||||
</el-card>
|
||||
<el-card shadow="never" class="person-governance-list-card">
|
||||
<div class="person-governance-table-wrap">
|
||||
<el-table ref="dataTable" :data="tableData" v-loading="tableLoading" height="100%">
|
||||
<el-table-column
|
||||
type="index"
|
||||
:index="indexMethod"
|
||||
label="序号"
|
||||
width="70"
|
||||
align="center"
|
||||
></el-table-column>
|
||||
<el-table-column prop="configName" label="配置名称"></el-table-column>
|
||||
<el-table-column prop="apiConfigName" label="API配置"></el-table-column>
|
||||
<el-table-column prop="lastExecuteTime" label="最近执行时间" width="180"></el-table-column>
|
||||
<el-table-column label="是否启用" width="100">
|
||||
<template v-slot="{row}">
|
||||
<el-switch v-model="row.enabled" @change="toggle(row)"></el-switch>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200">
|
||||
<template v-slot="{row}">
|
||||
<el-button type="text" @click="edit(row)">编辑</el-button>
|
||||
<!-- <el-button type="text" @click="preview(row)">试算匹配</el-button>-->
|
||||
<el-button type="text" class="text-danger" @click="remove(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<!--#include("/layouts/pagination.html"){}#-->
|
||||
</el-card>
|
||||
<el-drawer title="匹配结果预览" :visible.sync="previewVisible" size="55%" custom-class="person-governance-drawer">
|
||||
<div style="padding: 0 20px">
|
||||
<p>
|
||||
预计匹配
|
||||
<strong>{{previewTotal}}</strong>
|
||||
人
|
||||
</p>
|
||||
<el-table :data="previewRows">
|
||||
<el-table-column
|
||||
v-for="c in previewColumns"
|
||||
:key="c"
|
||||
:prop="c"
|
||||
:label="c"
|
||||
show-overflow-tooltip
|
||||
></el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</el-drawer>
|
||||
</div>
|
||||
<script>
|
||||
// 使用独立作用域承载页面实例,支持列表与表单通过 PJAX 反复切换。
|
||||
;(() => {
|
||||
new Vue({
|
||||
el: '#app',
|
||||
mixins: [initTableMixins],
|
||||
data() {
|
||||
return {
|
||||
pageReady: false,
|
||||
pageLeaving: false,
|
||||
apiConfigs: [],
|
||||
previewVisible: false,
|
||||
previewRows: [],
|
||||
previewColumns: [],
|
||||
previewTotal: 0,
|
||||
pageForm: { configName: '', apiConfigId: '', enabled: '', pageNumber: 1, pageSize: 10, totalCount: 0 },
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
pageData() {
|
||||
this.$set(this, 'tableLoading', true)
|
||||
return $.post(loc() + '/pageData', this.pageForm)
|
||||
.then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.$set(this, 'tableData', res.data.list)
|
||||
this.$set(this.pageForm, 'totalCount', res.data.totalCount)
|
||||
} else this.notifyWarning(res.msg)
|
||||
})
|
||||
.always(() => {
|
||||
this.$set(this, 'tableLoading', false)
|
||||
})
|
||||
},
|
||||
loadApis() {
|
||||
return $.post('/platform/personGovernance/apiConfig/selectableConfigs')
|
||||
.then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.$set(this, 'apiConfigs', res.data)
|
||||
}
|
||||
})
|
||||
.always(() => {})
|
||||
},
|
||||
resetSearch() {
|
||||
this.$set(this.pageForm, 'configName', '')
|
||||
this.$set(this.pageForm, 'apiConfigId', '')
|
||||
this.$set(this.pageForm, 'enabled', '')
|
||||
this.doSearch()
|
||||
},
|
||||
showPage() {
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.dataTable && this.$refs.dataTable.doLayout) {
|
||||
this.$refs.dataTable.doLayout()
|
||||
}
|
||||
window.requestAnimationFrame(() => {
|
||||
this.$set(this, 'pageReady', true)
|
||||
})
|
||||
})
|
||||
},
|
||||
leavePage(url) {
|
||||
this.$set(this, 'pageLeaving', true)
|
||||
window.setTimeout(() => {
|
||||
// 仅替换右侧内容区域,避免重新初始化平台布局和左侧菜单。
|
||||
commonUtil.pjaxPush(url)
|
||||
}, 180)
|
||||
},
|
||||
// 进入新增或编辑页时保留人员数据治理模块的左侧菜单。
|
||||
create() {
|
||||
this.leavePage('/platform/personGovernance/memberMatch/form')
|
||||
},
|
||||
edit(row) {
|
||||
this.leavePage('/platform/personGovernance/memberMatch/form?id=' + row.id)
|
||||
},
|
||||
toggle(row) {
|
||||
$.post(loc() + '/updateEnabled', { id: row.id, enabled: row.enabled })
|
||||
.then((res) => {
|
||||
if (res.code !== 0) {
|
||||
this.$set(row, 'enabled', !row.enabled)
|
||||
this.notifyWarning(res.msg)
|
||||
}
|
||||
})
|
||||
.always(() => {})
|
||||
},
|
||||
preview(row) {
|
||||
$.post(loc() + '/calculate', { id: row.id, pageNumber: 1, pageSize: 10 })
|
||||
.then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.$set(this, 'previewRows', res.data.list)
|
||||
this.$set(this, 'previewTotal', res.data.totalCount)
|
||||
this.$set(
|
||||
this,
|
||||
'previewColumns',
|
||||
res.data.list.length ? Object.keys(res.data.list[0]).slice(0, 6) : [],
|
||||
)
|
||||
this.$set(this, 'previewVisible', true)
|
||||
} else this.notifyWarning(res.msg)
|
||||
})
|
||||
.always(() => {})
|
||||
},
|
||||
remove(row) {
|
||||
this.$confirm('确认删除该会员匹配配置吗?', '提示').then(() => {
|
||||
$.post(loc() + '/delete', { id: row.id })
|
||||
.then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.notifySuccess('删除成功')
|
||||
this.pageData()
|
||||
} else this.notifyWarning(res.msg)
|
||||
})
|
||||
.always(() => {})
|
||||
})
|
||||
},
|
||||
},
|
||||
created() {
|
||||
$.when(this.loadApis(), this.pageData()).always(() => {
|
||||
this.showPage()
|
||||
})
|
||||
},
|
||||
})
|
||||
})()
|
||||
</script>
|
||||
<!--# } #-->
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
const PERSON_GOVERNANCE_MEMBER_ALL_CHANGE_INFO = {
|
||||
template: /*language=HTML*/ `
|
||||
<div>
|
||||
<el-tabs v-model="activeName">
|
||||
<el-tab-pane label="基础信息" name="basicInfo">
|
||||
<el-descriptions :column="3" border class="descriptions-form">
|
||||
<el-descriptions-item label="工号">{{ viewData.loginname }}</el-descriptions-item>
|
||||
<el-descriptions-item label="姓名">{{ viewData.username }}</el-descriptions-item>
|
||||
<el-descriptions-item label="性别">{{ viewData.sex }}</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="民族">{{ viewData.nation }}</el-descriptions-item>
|
||||
<el-descriptions-item label="出生日期">{{ viewData.birthday ? $moment(viewData.birthday).format("YYYY年MM月DD日") : "" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="政治面貌">{{ viewData.political }}</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="学历">{{ viewData.education }}</el-descriptions-item>
|
||||
<el-descriptions-item label="学位">{{ viewData.academicDegree }}</el-descriptions-item>
|
||||
<el-descriptions-item label="党政职务">{{ viewData.position }}</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="工作单位">{{ viewData.unitName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="所属工会">{{ viewData.unionName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="所属校区">{{ viewData.campusName }}</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="身份证号码">{{ viewData.idCard }}</el-descriptions-item>
|
||||
<el-descriptions-item label="联系电话">{{ viewData.mobile }}</el-descriptions-item>
|
||||
<el-descriptions-item label="电子邮箱">{{ viewData.email }}</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="在职状态">{{ viewData.userState }}</el-descriptions-item>
|
||||
<el-descriptions-item label="人员类型">{{ viewData.personType }}</el-descriptions-item>
|
||||
|
||||
<template v-if="viewData.userState == '退休'">
|
||||
<el-descriptions-item label="退休日期">{{ viewData.retireDate ? $moment(viewData.retireDate).format("YYYY-MM-DD") : '暂无' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="福利享受截止日期">{{ $moment(viewData.welfareStopDate).format("YYYY-MM-DD") }}</el-descriptions-item>
|
||||
<el-descriptions-item></el-descriptions-item>
|
||||
</template>
|
||||
|
||||
<el-descriptions-item label="会员状态">{{ viewData.member ? '会员' : '非会员' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="福利会员状态">{{ viewData.welfareMember ? '福利会员' : '非福利会员' }}</el-descriptions-item>
|
||||
<el-descriptions-item></el-descriptions-item>
|
||||
|
||||
<template v-if="viewData.loginname == $store.state.user.loginname">
|
||||
<el-descriptions-item label="家庭主要成员" :span="3">
|
||||
<el-table v-if="viewData.families&&viewData.families.length"
|
||||
:data="viewData.families" size="mini" border
|
||||
:header-cell-style="{background:'#eff3f6'}"
|
||||
style="width: 100%">
|
||||
<el-table-column prop="relation" label="与本人关系" align="center" header-align="center">
|
||||
</el-table-column>
|
||||
<el-table-column prop="name" label="姓名" align="center" header-align="center">
|
||||
</el-table-column>
|
||||
<el-table-column prop="unit" label="单位" align="center" header-align="center">
|
||||
</el-table-column>
|
||||
<el-table-column prop="remark" label="备注" align="center" header-align="center">
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-empty v-else :image-size="50" description="暂无家庭成员信息"></el-empty>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="个人简况" :span="3">
|
||||
<template slot="label">个人简况</template>
|
||||
<div v-if="viewData.vita" class="text-left" v-html="viewData.vita"></div>
|
||||
<span v-else>无数据</span>
|
||||
</el-descriptions-item>
|
||||
</template>
|
||||
</el-descriptions>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="历史变更记录" name="changeInfo" v-if="viewData.allChangeInfo">
|
||||
<el-timeline>
|
||||
<el-timeline-item placement="top" v-for="(changeData, index) in viewData.allChangeInfo"
|
||||
:key="index" :timestamp="changeData.timestamp">
|
||||
<el-card>
|
||||
<h4>{{ changeData.changeOriginName }}</h4>
|
||||
<el-table :data="changeData.changeInfos" size="mini" ref="table" row-key="id" style="width: 100%">
|
||||
<el-table-column align="center" header-align="center"
|
||||
label="序号" type="index" width="80px"></el-table-column>
|
||||
<el-table-column prop="fieldName" label="变更字段" header-align="center"></el-table-column>
|
||||
<el-table-column prop="sourceValue" label="原数据" header-align="center">
|
||||
<template slot-scope="{row}">
|
||||
<div v-html="row.sourceValue"></div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="newValue" label="新数据" header-align="center">
|
||||
<template slot-scope="{row}">
|
||||
<div v-html="row.newValue"></div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
<slot></slot>
|
||||
</div>
|
||||
`,
|
||||
store,
|
||||
data(){
|
||||
return{
|
||||
viewData: {},
|
||||
activeName: "basicInfo"
|
||||
}
|
||||
},
|
||||
methods:{
|
||||
onOpen(userId){
|
||||
this.$axios.post('/platform/personGovernance/personChange/getAllChangeMemberInfo', {userId})
|
||||
.then((resp) => {
|
||||
if (resp.code === 0) {
|
||||
this.$set(this, "viewData", resp.data)
|
||||
} else {
|
||||
this.$message.warning(resp.msg)
|
||||
}
|
||||
})
|
||||
.finally(() => {})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,599 @@
|
||||
const PERSON_GOVERNANCE_MEMBER_CHANGE = {
|
||||
template: /*language=HTML*/ `
|
||||
<el-card shadow="never">
|
||||
<snaker-start slot="header" label="会员变更" define_key="HYBG"></snaker-start>
|
||||
<el-form :model="formData" ref="formRef" :rules="formRules" label-width="0" label-suffix=":"
|
||||
class="flow-task-form">
|
||||
<el-descriptions :column="3" border>
|
||||
<el-descriptions-item label="工号">
|
||||
<el-form-item prop="loginname">
|
||||
<el-input v-model="formData.loginname" readonly size="small"></el-input>
|
||||
</el-form-item>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="姓名">
|
||||
<el-form-item prop="username">
|
||||
<el-input v-model="formData.username" readonly size="small"></el-input>
|
||||
</el-form-item>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="性别">
|
||||
<el-form-item prop="sex">
|
||||
<el-radio-group :disabled="allowFields('sex')" v-model="formData.sex" size="small">
|
||||
<el-radio border label="男">男</el-radio>
|
||||
<el-radio border label="女">女</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="民族">
|
||||
<el-form-item prop="nation">
|
||||
<dict-select style="width: 100%" placeholder="请选择民族" v-model="formData.nation"
|
||||
:disabled="allowFields('nation')" code="USER_NATION"></dict-select>
|
||||
</el-form-item>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="出生日期">
|
||||
<el-form-item prop="birthday">
|
||||
<el-date-picker v-model="formData.birthday"
|
||||
type="date"
|
||||
:disabled="allowFields('birthday')"
|
||||
placeholder="请选择出生日期"
|
||||
format="yyyy-MM-dd"
|
||||
value-format="yyyy-MM-dd"
|
||||
style="width: 100%"></el-date-picker>
|
||||
</el-form-item>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="政治面貌">
|
||||
<el-form-item prop="political">
|
||||
<dict-select style="width: 100%" placeholder="请选择政治面貌" v-model="formData.political"
|
||||
:disabled="allowFields('political')" code="USER_POLITICAL"></dict-select>
|
||||
</el-form-item>
|
||||
</el-descriptions-item>
|
||||
|
||||
|
||||
<el-descriptions-item label="学历">
|
||||
<el-form-item prop="education">
|
||||
<dict-select style="width: 100%" placeholder="请选择学历" v-model="formData.education"
|
||||
:disabled="allowFields('education')" code="USER_EDUCATION"></dict-select>
|
||||
</el-form-item>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="学位">
|
||||
<el-form-item prop="academicDegree">
|
||||
<dict-select style="width: 100%" placeholder="请选择学位" v-model="formData.academicDegree"
|
||||
:disabled="allowFields('academicDegree')"
|
||||
code="USER_ACADEMIC_DEGREE"></dict-select>
|
||||
</el-form-item>
|
||||
</el-descriptions-item>
|
||||
<!-- <el-descriptions-item label="党政职务">-->
|
||||
<!-- <el-form-item prop="position">-->
|
||||
<!-- <el-input v-model="formData.position" placeholder="请输入党政职务"-->
|
||||
<!-- :disabled="allowFields('position')" maxlength="50"></el-input>-->
|
||||
<!-- </el-form-item>-->
|
||||
<!-- </el-descriptions-item>-->
|
||||
|
||||
<!--<template v-if="$auth.hasRoleOr('SYSADMIN, SCHOOL_UNION_ADMIN, SCHOOL_UNION_MEMBER_ADMIN')">
|
||||
<el-descriptions-item label="工作单位">
|
||||
<el-form-item prop="unitId">
|
||||
<el-select clearable filterable placeholder="请选择工作单位" style="width: 100%"
|
||||
v-model="formData.unitId">
|
||||
<el-option :key="item.id" :label="item.name" :value="item.id"
|
||||
v-for="item in units"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="所属工会">
|
||||
<el-form-item prop="unionId">
|
||||
<el-select clearable filterable placeholder="请选择所属工会" style="width: 100%"
|
||||
v-model="formData.unionId">
|
||||
<el-option :key="item.id" :label="item.name" :value="item.id"
|
||||
v-for="item in unions"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-descriptions-item>
|
||||
</template>-->
|
||||
<template>
|
||||
<el-descriptions-item label="工作单位">
|
||||
<el-form-item prop="unitId">
|
||||
<el-input v-model="formData.unitName" disabled placeholder="暂无工作单位"></el-input>
|
||||
</el-form-item>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="所属工会">
|
||||
<el-form-item prop="unionName">
|
||||
<el-input v-model="formData.unionName" disabled placeholder="请输入所属工会"></el-input>
|
||||
</el-form-item>
|
||||
</el-descriptions-item>
|
||||
</template>
|
||||
<el-descriptions-item v-if="showThreeUnit" label="三级单位">
|
||||
<el-form-item prop="threeUnitId">
|
||||
<el-select v-model="formData.threeUnitId" clearable filterable
|
||||
placeholder="请选择三级单位" style="width: 100%">
|
||||
<el-option v-for="item in threeUnits" :key="item.id" :label="item.name"
|
||||
:value="item.id"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item v-else label="所属校区">
|
||||
<el-form-item prop="campus">
|
||||
<dict-select style="width: 100%" placeholder="请选择所属校区" v-model="formData.campus"
|
||||
:disabled="allowFields('campus')" code="USER_CAMPUS"></dict-select>
|
||||
</el-form-item>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="身份证号码">
|
||||
<el-form-item prop="idCard">
|
||||
<el-input v-model="formData.idCard" :disabled="allowFields('idCard')"
|
||||
placeholder="请输入身份证号码" maxlength="18"></el-input>
|
||||
</el-form-item>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="联系电话">
|
||||
<el-form-item prop="mobile">
|
||||
<el-input v-model="formData.mobile" :disabled="allowFields('mobile')"
|
||||
placeholder="请输入联系电话" maxlength="15"></el-input>
|
||||
</el-form-item>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="电子邮箱">
|
||||
<el-form-item prop="email">
|
||||
<el-input v-model="formData.email" :disabled="allowFields('email')"
|
||||
placeholder="请输入电子邮箱" maxlength="50"></el-input>
|
||||
</el-form-item>
|
||||
</el-descriptions-item>
|
||||
|
||||
|
||||
<el-descriptions-item label="在职状态">
|
||||
<el-form-item prop="userState">
|
||||
<dict-select v-model="formData.userState" code="USER_STATE"
|
||||
@change="handleRetiredStateChange"
|
||||
:disabled="allowFields('userState')" style="width: 100%"></dict-select>
|
||||
</el-form-item>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="人员类型">
|
||||
<el-form-item prop="personType">
|
||||
<dict-select v-model="formData.personType" code="USER_PERSON_TYPE"
|
||||
:disabled="allowFields('personType')" style="width: 100%"></dict-select>
|
||||
</el-form-item>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="人员属性">
|
||||
<el-form-item prop="userAttribute">
|
||||
<dict-select v-model="formData.userAttribute" code="USER_ATTRIBUTE"
|
||||
style="width: 100%"></dict-select>
|
||||
</el-form-item>
|
||||
</el-descriptions-item>
|
||||
<!-- <el-descriptions-item label="人员分类">-->
|
||||
<!-- <el-form-item prop="aidFundMemberUserType">-->
|
||||
<!-- <dict-select v-model="formData.aidFundMemberUserType" code="AIDFUND_MEMBER_USER_TYPE"-->
|
||||
<!-- style="width: 100%"></dict-select>-->
|
||||
<!-- </el-form-item>-->
|
||||
<!-- </el-descriptions-item>-->
|
||||
|
||||
<template v-if="formData.userState == '退休'">
|
||||
<el-descriptions-item label="退休日期">
|
||||
<el-form-item prop="retireDate">
|
||||
<el-date-picker v-model="formData.retireDate"
|
||||
type="date"
|
||||
placeholder="请选择退休日期"
|
||||
format="yyyy-MM-dd"
|
||||
value-format="yyyy-MM-dd"
|
||||
style="width: 100%"></el-date-picker>
|
||||
</el-form-item>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="福利享受截止日期">
|
||||
<el-form-item prop="welfareStopDate">
|
||||
<el-date-picker v-model="formData.welfareStopDate"
|
||||
type="date"
|
||||
placeholder="请选择福利享受截止日期"
|
||||
format="yyyy-MM-dd"
|
||||
value-format="yyyy-MM-dd"
|
||||
style="width: 100%"></el-date-picker>
|
||||
</el-form-item>
|
||||
</el-descriptions-item>
|
||||
<!-- <el-descriptions-item></el-descriptions-item>-->
|
||||
</template>
|
||||
|
||||
<el-descriptions-item label="会员状态" >
|
||||
<el-form-item prop="member">
|
||||
<el-radio-group v-model="formData.member" size="small">
|
||||
<el-radio border :label="true">会员</el-radio>
|
||||
<el-radio border :label="false">非会员</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="福利会员状态">
|
||||
<el-form-item prop="welfareMember">
|
||||
<el-radio-group v-model="formData.welfareMember" size="small" class="welfare-member-radio-group"
|
||||
style="display: flex; align-items: center; flex-wrap: nowrap; white-space: nowrap;">
|
||||
<el-radio border :label="true" style="flex-shrink: 0;">福利会员</el-radio>
|
||||
<el-radio border :label="false" style="flex-shrink: 0;">非福利会员</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="入校时间">
|
||||
<el-form-item>
|
||||
<el-date-picker v-model="formData.arrivalAtSchoolDate"
|
||||
type="date"
|
||||
placeholder="请选择入校时间"
|
||||
format="yyyy-MM-dd"
|
||||
value-format="yyyy-MM-dd"></el-date-picker>
|
||||
</el-form-item>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item></el-descriptions-item>
|
||||
<el-descriptions-item></el-descriptions-item>
|
||||
<template v-if="formData.loginname == $store.state.user.loginname">
|
||||
<el-descriptions-item label="家庭主要成员" :span="3">
|
||||
<el-form-item prop="families">
|
||||
<el-table :data="formData.families" border size="small">
|
||||
<el-table-column label="关系" prop="relation">
|
||||
<template slot-scope="{row}">
|
||||
<el-input v-model="row.relation" maxlength="50"
|
||||
placeholder="请输入与本人关系"></el-input>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="姓名" prop="name">
|
||||
<template slot-scope="{row}">
|
||||
<el-input v-model="row.name" maxlength="50"
|
||||
placeholder="请输入姓名"></el-input>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="工作单位" prop="unit">
|
||||
<template slot-scope="{row}">
|
||||
<el-input v-model="row.unit" maxlength="100"
|
||||
placeholder="请输入工作单位"></el-input>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="备注" prop="remark">
|
||||
<template slot-scope="{row}">
|
||||
<el-input v-model="row.remark" maxlength="100"
|
||||
placeholder="请输入备注"></el-input>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column width="100px">
|
||||
<template slot="header" slot-scope="scope">
|
||||
<el-button type="primary" size="mini" :disabled="allowFields('families')"
|
||||
@click="formData.families.push({})">添加
|
||||
</el-button>
|
||||
</template>
|
||||
<template slot-scope="scope">
|
||||
<el-button
|
||||
type="danger"
|
||||
icon="el-icon-delete"
|
||||
size="mini"
|
||||
:disabled="formData.families.length===0 || allowFields('families')"
|
||||
@click="formData.families.splice(scope.$index,1)"
|
||||
></el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-form-item>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="个人简历" :span="3">
|
||||
<el-form-item prop="personalData">
|
||||
<text-editor v-model="formData.personalData"></text-editor>
|
||||
</el-form-item>
|
||||
</el-descriptions-item>
|
||||
</template>
|
||||
</el-descriptions>
|
||||
</el-form>
|
||||
<el-row type="flex" justify="end" class="mt20">
|
||||
<el-button type="primary" plain @click="$emit('do-back')">返回</el-button>
|
||||
<el-button type="primary" @click="onSubmit">提交变更</el-button>
|
||||
</el-row>
|
||||
<!-- <el-row justify="end" type="flex" class="mt10">-->
|
||||
<!-- <el-button @click="$emit('do-back')" v-if="formData.id || !isShow" type="primary">返 回</el-button>-->
|
||||
<!-- <template v-if="isShow">-->
|
||||
<!-- <el-button @click="doSave" type="primary">保存变更</el-button>-->
|
||||
<!-- </template>-->
|
||||
<!-- <el-button @click="doSubmit" type="primary">提交变更</el-button>-->
|
||||
<!-- </el-row>-->
|
||||
</el-card>
|
||||
`,
|
||||
props: {
|
||||
id: { type: String, default: '' },
|
||||
showThreeUnit: { type: Boolean, default: false },
|
||||
},
|
||||
mixins: [initTableMixins],
|
||||
store,
|
||||
data(){
|
||||
return{
|
||||
isShow: true,
|
||||
user: {},
|
||||
units: [],
|
||||
unions: [],
|
||||
threeUnits: [],
|
||||
// 允许变更的字段
|
||||
allowChangeFields: [],
|
||||
formData: {
|
||||
families: [],
|
||||
},
|
||||
formRules: {
|
||||
username: [{required: false, message: "必填", trigger: ["change", "blur"]}],
|
||||
sex: [{required: false, message: "必填", trigger: ["change", "blur"]}],
|
||||
/*email: [
|
||||
{
|
||||
validator: (rule, value, callback) => {
|
||||
if (!value) {
|
||||
callback();
|
||||
} else if (/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(value)) {
|
||||
callback();
|
||||
} else {
|
||||
callback(new Error("邮箱格式错误"));
|
||||
}
|
||||
},
|
||||
trigger: ["change", "blur"]
|
||||
}
|
||||
],
|
||||
idCard: [
|
||||
{
|
||||
validator: (rule, value, callback) => {
|
||||
if (!value) {
|
||||
// 如果值为空,直接通过校验
|
||||
callback();
|
||||
} else if (/^[1-9]\d{5}(18|19|20)?\d{2}((0[1-9])|(10|11|12))(([0|1|2][0-9])|10|20|30|31)\d{3}(\d|[Xx])$/.test(value)) {
|
||||
// 18位身份证号码格式正确
|
||||
callback();
|
||||
} else if (/^[1-9]\d{7}((0[1-9])|(10|11|12))(([0|1|2][0-9])|10|20|30|31)\d{3}$/.test(value)) {
|
||||
// 15位身份证号码格式正确
|
||||
callback();
|
||||
} else if (/^[1-9]\d{5}(18|19|20)?\d{2}((0[1-9])|(10|11|12))(([0|1|2][0-9])|10|20|30|31)\d{3}$/.test(value)) {
|
||||
// 外国人身份证号码格式正确(18位)
|
||||
callback();
|
||||
} else if (/^[1-9]\d{7}((0[1-9])|(10|11|12))(([0|1|2][0-9])|10|20|30|31)\d{3}$/.test(value)) {
|
||||
// 外国人身份证号码格式正确(15位)
|
||||
callback();
|
||||
} else {
|
||||
// 格式错误,返回错误信息
|
||||
callback(new Error("身份证号码格式错误"));
|
||||
}
|
||||
},
|
||||
trigger: ["change", "blur"]
|
||||
}
|
||||
],
|
||||
mobile: [
|
||||
{
|
||||
validator: (rule, value, callback) => {
|
||||
if (!value) {
|
||||
// 如果值为空,直接通过校验
|
||||
callback();
|
||||
} else if (/^[1][345789][0-9]{9}$/.test(value)) {
|
||||
// 手机号格式正确
|
||||
callback();
|
||||
} else if (/^\d+-\d+$/.test(value) && value.length <= 16) {
|
||||
// 座机号格式正确
|
||||
callback();
|
||||
} else {
|
||||
// 格式错误,返回错误信息
|
||||
callback(new Error(value.includes('-') ? '座机号码格式错误' : '手机号码格式错误'));
|
||||
}
|
||||
},
|
||||
trigger: ["change", "blur"]
|
||||
}
|
||||
]*/
|
||||
},
|
||||
|
||||
welfareProjectList: []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getUnionName(val){
|
||||
const data = this.units.find(v => String(v.id) === String(val))
|
||||
this.$set(this.formData, 'unitName', data ? data.name : '')
|
||||
this.$set(this.formData, 'unionName', data ? (data.unionName || data.unionname || '') : '')
|
||||
},
|
||||
normalizeUnitField() {
|
||||
if (!this.formData || !this.formData.unitId) {
|
||||
return
|
||||
}
|
||||
const unitId = String(this.formData.unitId)
|
||||
this.$set(this.formData, "unitId", unitId)
|
||||
let unit = this.units.find(v => String(v.id) === unitId)
|
||||
if (unit) {
|
||||
this.$set(this.formData, "unitName", unit.name)
|
||||
this.$set(this.formData, "unionName", unit.unionName || unit.unionname || this.formData.unionName || "")
|
||||
}
|
||||
},
|
||||
allowFields(prop) {
|
||||
return !this.allowChangeFields.map(v => v.code).includes(prop)
|
||||
},
|
||||
/** 在职状态选择退休时,存在“离退休人员”字典项才自动设置对应人员类型编码。 */
|
||||
handleRetiredStateChange(userState) {
|
||||
if (userState !== "退休") {
|
||||
return
|
||||
}
|
||||
this.$businessTool.getDictOptions("USER_PERSON_TYPE").then((options) => {
|
||||
const retiredOption = (options || []).find((item) => item.name === "离退休人员")
|
||||
if (retiredOption) {
|
||||
this.$set(this.formData, "personType", retiredOption.code)
|
||||
}
|
||||
})
|
||||
},
|
||||
// 管理页仅查询当前被编辑人员所属二级单位的直属三级单位,避免越级选择其他单位数据。
|
||||
initThreeUnits() {
|
||||
this.$set(this, "threeUnits", [])
|
||||
if (!this.showThreeUnit) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
if (!this.formData.unitId) {
|
||||
this.$message.warning("当前人员未配置所属单位,无法查询三级单位")
|
||||
return Promise.resolve()
|
||||
}
|
||||
return this.$axios.post("/platform/sys/unit/child", {pid: this.formData.unitId})
|
||||
.then((resp) => {
|
||||
if (resp.code === 0) {
|
||||
this.$set(this, "threeUnits", resp.data || [])
|
||||
if (this.threeUnits.length === 0) {
|
||||
this.$message.warning("当前所属单位未配置三级单位")
|
||||
}
|
||||
} else {
|
||||
this.$message.error(resp.msg || "三级单位查询失败")
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
this.$message.error("三级单位查询失败,请稍后重试")
|
||||
})
|
||||
.finally(() => {})
|
||||
},
|
||||
doSave(){
|
||||
this.$confirm("保存后可在我的申请里面再次编辑,您确定要保存吗?", "提示", { type: "warning" })
|
||||
.then(() => {
|
||||
const formData = this.formData
|
||||
if (formData.families) {
|
||||
formData.families = JSON.stringify(this.formData.families)
|
||||
}
|
||||
this.$emit('do-save', formData)
|
||||
})
|
||||
.catch()
|
||||
},
|
||||
onSubmit() {
|
||||
this.$refs.formRef.validate((valid) => {
|
||||
if (valid) {
|
||||
this.$confirm("提交后将进入审核流程,无法再进行编辑,您确定要提交申请吗?", "提示", { type: "warning" })
|
||||
.then(() => {
|
||||
const formData = this.formData
|
||||
if (formData.families) {
|
||||
formData.families = JSON.stringify(this.formData.families)
|
||||
}
|
||||
this.$emit('do-submit', formData)
|
||||
})
|
||||
.catch()
|
||||
}
|
||||
})
|
||||
},
|
||||
init(userId){
|
||||
let userPromise = Promise.resolve()
|
||||
if (userId) {
|
||||
this.$set(this, "isShow", false)
|
||||
userPromise = this.getUserById(userId)
|
||||
} else {
|
||||
this.$set(this, "isShow", true)
|
||||
this.$set(this, "user", this.$store.state.user)
|
||||
}
|
||||
return userPromise
|
||||
.then(() => this.getFormDataValue(this.id))
|
||||
.then(() => {
|
||||
const requests = [
|
||||
this.$businessTool.getDictOptions("ALLOW_CHANGE_FIELDS"),
|
||||
this.$businessTool.listUnit()
|
||||
]
|
||||
if (this.$auth.hasRoleOr('SYSADMIN, SCHOOL_UNION_MEMBER_ADMIN, SCHOOL_UNION_ADMIN')) {
|
||||
requests.push(this.$businessTool.listUnion())
|
||||
}
|
||||
return Promise.all(requests)
|
||||
})
|
||||
.then((result) => {
|
||||
this.$set(this, "allowChangeFields", result[0])
|
||||
this.$set(this, "units", result[1])
|
||||
if (result.length > 2) {
|
||||
this.$set(this, "unions", result[2])
|
||||
}
|
||||
this.normalizeUnitField()
|
||||
this.initThreeUnits()
|
||||
})
|
||||
},
|
||||
getUserById(userId){
|
||||
return $.post("/platform/personGovernance/personChange/getUserByIdForMemberChange", { userId })
|
||||
.then((resp) => {
|
||||
if (resp.code === 0) {
|
||||
this.$set(this, "user", resp.data)
|
||||
}
|
||||
})
|
||||
.always(() => {})
|
||||
},
|
||||
getFormDataValue(id){
|
||||
if (id) {
|
||||
return $.post("/platform/personGovernance/personChange/findChangeById", { id }).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.$set(this, "formData", res.data)
|
||||
this.$set(this.formData, "unionid", this.user.unionid)
|
||||
if (this.formData.families) {
|
||||
this.$set(this.formData, "families", JSON.parse(this.formData.families))
|
||||
} else {
|
||||
this.$set(this.formData, "families", [])
|
||||
}
|
||||
}
|
||||
}).always(() => {})
|
||||
} else {
|
||||
const {
|
||||
id,
|
||||
username,
|
||||
loginname,
|
||||
sex,
|
||||
member,
|
||||
welfareMember,
|
||||
nation,
|
||||
birthday,
|
||||
political,
|
||||
education,
|
||||
academicDegree,
|
||||
position,
|
||||
unit,
|
||||
union,
|
||||
campus,
|
||||
threeUnitId,
|
||||
userState,
|
||||
retireDate,
|
||||
welfareStopDate,
|
||||
personType,
|
||||
preparedBy,
|
||||
userAttribute,
|
||||
aidFundMemberUserType,
|
||||
idCard,
|
||||
mobile,
|
||||
email,
|
||||
families,
|
||||
personalData,
|
||||
arrivalAtSchoolDate
|
||||
} = this.user
|
||||
|
||||
this.$set(this.formData, "userId", id)
|
||||
this.$set(this.formData, "username", username)
|
||||
this.$set(this.formData, "loginname", loginname)
|
||||
this.$set(this.formData, "birthday", birthday)
|
||||
this.$set(this.formData, "sex", sex)
|
||||
this.$set(this.formData, "member", member === 1)
|
||||
this.$set(this.formData, "welfareMember", welfareMember === 1)
|
||||
this.$set(this.formData, "idCard", idCard)
|
||||
this.$set(this.formData, "nation", nation)
|
||||
this.$set(this.formData, "political", political)
|
||||
this.$set(this.formData, "position", position)
|
||||
this.$set(this.formData, "education", education)
|
||||
this.$set(this.formData, "academicDegree", academicDegree)
|
||||
this.$set(this.formData, "campus", campus)
|
||||
this.$set(this.formData, "threeUnitId", threeUnitId)
|
||||
this.$set(this.formData, "userState", userState)
|
||||
// 日期选择器使用 yyyy-MM-dd 字符串,避免接口日期值无法在组件中正确回显。
|
||||
this.$set(this.formData, "retireDate", retireDate
|
||||
? this.$moment(retireDate).format("YYYY-MM-DD") : "")
|
||||
this.$set(this.formData, "welfareStopDate", welfareStopDate
|
||||
? this.$moment(welfareStopDate).format("YYYY-MM-DD") : "")
|
||||
this.$set(this.formData, "personType", personType)
|
||||
this.$set(this.formData, "preparedBy", preparedBy)
|
||||
this.$set(this.formData, "userAttribute", userAttribute)
|
||||
this.$set(this.formData, "aidFundMemberUserType", aidFundMemberUserType)
|
||||
this.$set(this.formData, "unitName", unit ? unit.name : null)
|
||||
this.$set(this.formData, "unitId", unit ? String(unit.id) : null)
|
||||
this.$set(this.formData, "unionName", union ? union.name : null)
|
||||
this.$set(this.formData, "unionId", union ? union.id : null)
|
||||
|
||||
this.$set(this.formData, "mobile", mobile)
|
||||
this.$set(this.formData, "email", email)
|
||||
this.$set(this.formData, "families", families ? families : [])
|
||||
this.$set(this.formData, "personalData", personalData)
|
||||
this.$set(this.formData, "arrivalAtSchoolDate", arrivalAtSchoolDate)
|
||||
}
|
||||
}
|
||||
},
|
||||
created(){
|
||||
this.init()
|
||||
},
|
||||
style: /*language=CSS*/ `
|
||||
.el-descriptions-item__label {
|
||||
width: 15% !important;
|
||||
}
|
||||
.welfare-member-radio-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
.welfare-member-radio-group .el-radio {
|
||||
margin-right: 12px;
|
||||
}
|
||||
`
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
const PERSON_GOVERNANCE_MEMBER_INFO = {
|
||||
template: /*language=HTML*/ `
|
||||
<div>
|
||||
<!-- 基本信息 -->
|
||||
<div class="process-title">基本信息</div>
|
||||
<el-descriptions :column="3" border class="descriptions-form">
|
||||
<!-- 第 1 行 -->
|
||||
<el-descriptions-item label="姓名工号">{{ viewData.username + '(' + viewData.loginname + ')' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="性别">{{ viewData.sex }}</el-descriptions-item>
|
||||
<el-descriptions-item label="出生日期">{{ viewData.birthday ? $moment(viewData.birthday).format('YYYY-MM-DD') : null }}</el-descriptions-item>
|
||||
|
||||
<!-- 第 2 行 -->
|
||||
<el-descriptions-item label="国籍">{{ viewData.nationality }}</el-descriptions-item>
|
||||
<el-descriptions-item label="民族">{{ viewData.nation }}</el-descriptions-item>
|
||||
<el-descriptions-item label="政治面貌">{{ viewData.political }}</el-descriptions-item>
|
||||
|
||||
<!-- 第 3 行 -->
|
||||
<el-descriptions-item label="证件类型">{{ viewData.idCardType }}</el-descriptions-item>
|
||||
<el-descriptions-item label="身份证号码">{{ viewData.idCard }}</el-descriptions-item>
|
||||
<el-descriptions-item label="联系电话">{{ viewData.mobile }}</el-descriptions-item>
|
||||
|
||||
<!-- 第 4 行 -->
|
||||
<el-descriptions-item label="学历">{{ viewData.education }}</el-descriptions-item>
|
||||
<el-descriptions-item label="学位">{{ viewData.academicDegree }}</el-descriptions-item>
|
||||
<el-descriptions-item label="婚姻状况">{{ viewData.marriage }}</el-descriptions-item>
|
||||
|
||||
<!-- 第 5 行 -->
|
||||
<el-descriptions-item label="会员状态">{{ viewData.member ? '会员' : '非会员' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="福利会员状态">{{ viewData.welfareMember ? '福利会员' : '非福利会员' }}</el-descriptions-item>
|
||||
<el-descriptions-item></el-descriptions-item>
|
||||
|
||||
<!-- 退休补充 -->
|
||||
<template v-if="viewData.userState == '退休'">
|
||||
<el-descriptions-item label="退休日期">{{ viewData.retireDate ? $moment(viewData.retireDate).format("YYYY-MM-DD") : '暂无' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="福利享受截止日期">{{ viewData.welfareStopDate ? $moment(viewData.welfareStopDate).format("YYYY-MM-DD") : '暂无' }}</el-descriptions-item>
|
||||
<el-descriptions-item></el-descriptions-item>
|
||||
</template>
|
||||
|
||||
<!-- 仅本人可见 -->
|
||||
<template v-if="viewData.loginname === $store.state.user.loginname">
|
||||
<el-descriptions-item label="家庭主要成员" :span="3">
|
||||
<el-table v-if="viewData.families && viewData.families.length" :data="viewData.families" size="mini" border :header-cell-style="{background:'#eff3f6'}" style="width: 100%">
|
||||
<el-table-column prop="relation" label="与本人关系" align="center"/>
|
||||
<el-table-column prop="name" label="姓名" align="center"/>
|
||||
<el-table-column prop="unit" label="单位" align="center"/>
|
||||
<el-table-column prop="remark" label="备注" align="center"/>
|
||||
</el-table>
|
||||
<el-empty v-else :image-size="50" description="暂无家庭成员信息"/>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="个人简况" :span="3">
|
||||
<div v-if="viewData.personalData" class="text-left" v-html="viewData.personalData"/>
|
||||
<span v-else>无数据</span>
|
||||
</el-descriptions-item>
|
||||
</template>
|
||||
</el-descriptions>
|
||||
|
||||
<!-- 工作信息 -->
|
||||
<div class="process-title">工作信息</div>
|
||||
<el-descriptions :column="3" border>
|
||||
<!-- 第 1 行 -->
|
||||
<el-descriptions-item label="工作单位">{{ viewData.unitName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="所属工会">{{ viewData.unionName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="所属校区">{{ viewData.campus }}</el-descriptions-item>
|
||||
|
||||
<!-- 第 2 行 -->
|
||||
<el-descriptions-item label="入校时间">{{ viewData.arrivalAtSchoolDate ? $moment(viewData.arrivalAtSchoolDate).format('YYYY-MM-DD') : null }}</el-descriptions-item>
|
||||
<el-descriptions-item label="从教年月">{{ viewData.teachingTime ? $moment(viewData.teachingTime).format('YYYY-MM-DD') : null }}</el-descriptions-item>
|
||||
<el-descriptions-item label="预/已离校时间">{{ viewData.expectedLeaveSchoolDate ? $moment(viewData.expectedLeaveSchoolDate).format('YYYY-MM-DD') : null }}</el-descriptions-item>
|
||||
|
||||
<!-- 第 3 行 -->
|
||||
<el-descriptions-item label="在职状态">{{ viewData.userState }}</el-descriptions-item>
|
||||
<el-descriptions-item label="人员类型">{{ viewData.personType }}</el-descriptions-item>
|
||||
|
||||
<!-- 第 4 行 -->
|
||||
<el-descriptions-item label="人员属性">{{ viewData.userAttribute }}</el-descriptions-item>
|
||||
<el-descriptions-item label="职称">{{ viewData.professionalTitle }}</el-descriptions-item>
|
||||
<el-descriptions-item label="职级">{{ viewData.professionalLevel }}</el-descriptions-item>
|
||||
|
||||
<!-- 第 5 行 -->
|
||||
<el-descriptions-item label="岗位系列">{{ viewData.positionSeries }}</el-descriptions-item>
|
||||
<el-descriptions-item label="行政级别(管理岗位)">{{ viewData.administrativeLevel }}</el-descriptions-item>
|
||||
<el-descriptions-item label="岗级">{{ viewData.positionLevel }}</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="行政岗级">{{ viewData.administrativePositionLevel }}</el-descriptions-item>
|
||||
<el-descriptions-item label="人员分类" v-if="false">{{ viewData.aidFundMemberUserType }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<slot></slot>
|
||||
</div>
|
||||
`,
|
||||
store,
|
||||
data() {
|
||||
return {
|
||||
viewData: {}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onOpen(id) {
|
||||
this.$axios.post("/platform/personGovernance/personChange/findOne", { id })
|
||||
.then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.$set(this, "viewData", res.data)
|
||||
}
|
||||
})
|
||||
.finally(() => {})
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,944 @@
|
||||
<!--#
|
||||
layout("/layouts/platform.html"){
|
||||
#-->
|
||||
<link rel="stylesheet" href="${base!}/assets/platform/css/personGovernance.css?v=1.0.22" />
|
||||
<div
|
||||
id="app"
|
||||
v-cloak
|
||||
class="person-governance-list-page person-governance-change-page person-governance-page-transition"
|
||||
:class="{'is-ready':pageReady}"
|
||||
>
|
||||
<guava ref="guava">
|
||||
<el-card shadow="never" class="person-governance-search-card">
|
||||
<search @search="doSearch">
|
||||
<search-item label="姓名/工号">
|
||||
<el-input placeholder="请输入姓名或工号" clearable v-model="pageForm.searchKeyword"></el-input>
|
||||
</search-item>
|
||||
<search-item label="所属工会">
|
||||
<el-select @change="flushUnits" @clear="flushUnits" clearable filterable
|
||||
placeholder="请选择所属工会" style="width: 100%;" v-model="pageForm.unionId">
|
||||
<el-option :key="item.id" :label="item.name" :value="item.id"
|
||||
v-for="item in unions"></el-option>
|
||||
</el-select>
|
||||
</search-item>
|
||||
<search-item label="所属单位">
|
||||
<el-select @change="flushThreeUnits" @clear="flushThreeUnits" clearable filterable
|
||||
placeholder="请选择所属单位" style="width: 100%" v-model="pageForm.unitId">
|
||||
<el-option :key="item.id" :label="item.name" :value="item.id"
|
||||
v-for="item in units"></el-option>
|
||||
</el-select>
|
||||
</search-item>
|
||||
<search-item label="三级单位">
|
||||
<el-select clearable filterable placeholder="请选择三级单位" style="width: 100%"
|
||||
v-model="pageForm.threeUnitId">
|
||||
<el-option :key="item.id" :label="item.name" :value="item.id"
|
||||
v-for="item in threeUnits"></el-option>
|
||||
</el-select>
|
||||
</search-item>
|
||||
<search-item label="性别">
|
||||
<dict-select v-model="pageForm.sex" placeholder="性别" @change="doSearch"
|
||||
code="USER_SEX"></dict-select>
|
||||
</search-item>
|
||||
<search-item label="在职状态">
|
||||
<dict-select v-model="pageForm.userState" placeholder="请选择在职状态" @change="doSearch"
|
||||
code="USER_STATE"></dict-select>
|
||||
</search-item>
|
||||
<search-item label="人员类型">
|
||||
<dict-select v-model="pageForm.personType" placeholder="请选择人员类型" @change="doSearch"
|
||||
code="USER_PERSON_TYPE"></dict-select>
|
||||
</search-item>
|
||||
<!-- <search-item label="编制类别">-->
|
||||
<!-- <dict-select v-model="pageForm.preparedBy" placeholder="请选择编制类别" @change="doSearch"-->
|
||||
<!-- code="USER_PREPARED_BY_TYPE"></dict-select>-->
|
||||
<!-- </search-item>-->
|
||||
<!-- 临时屏蔽“人员分类”查询字段,不删除配置,后续需要可恢复。-->
|
||||
<!-- <search-item label="人员分类">-->
|
||||
<!-- <dict-select v-model="pageForm.aidFundMemberUserType" placeholder="请选择人员分类" @change="doSearch"-->
|
||||
<!-- code="AIDFUND_MEMBER_USER_TYPE"></dict-select>-->
|
||||
<!-- </search-item>-->
|
||||
<search-item label="人员属性">
|
||||
<dict-select v-model="pageForm.userAttributes" placeholder="请选择人员属性" @change="doSearch"
|
||||
code="USER_ATTRIBUTE" multiple collapse-tags clearable></dict-select>
|
||||
</search-item>
|
||||
<search-item label="更新日期">
|
||||
<el-date-picker :picker-options="pickerOptions" @change="changeDateRangeChange"
|
||||
align="right" end-placeholder="结束日期" format="yyyy-MM-dd"
|
||||
range-separator="-" start-placeholder="开始日期" style="width: 100%"
|
||||
type="datetimerange" v-model="pageForm.changeDateRange"
|
||||
value-format="yyyy-MM-dd"></el-date-picker>
|
||||
</search-item>
|
||||
</search>
|
||||
</el-card>
|
||||
|
||||
<el-card shadow="never" class="mt10 person-governance-change-list-card">
|
||||
<table-tool label="人员列表(工会小组或分工会提交的变更还需校工会审核)">
|
||||
<el-button :disabled="!checkUsers.length" @click="openBatchEditMember" class="ml5" icon="el-icon-edit"
|
||||
size="small" type="primary">
|
||||
设置会员
|
||||
</el-button>
|
||||
|
||||
<el-button :disabled="!checkUsers.length" @click="openBatchEditAidFundType" class="ml5" icon="el-icon-edit"
|
||||
size="small" type="primary">
|
||||
设置人员分类
|
||||
</el-button>
|
||||
|
||||
<el-button :disabled="!checkUsers.length" @click="openBatchEditUserAttribute" class="ml5" icon="el-icon-edit"
|
||||
size="small" type="primary">
|
||||
设置人员属性
|
||||
</el-button>
|
||||
|
||||
<el-button :disabled="!checkUsers.length" @click="openBatchEditUserState" class="ml5" icon="el-icon-edit"
|
||||
size="small" type="primary">
|
||||
设置在职状态
|
||||
</el-button>
|
||||
|
||||
<el-button :disabled="!checkUsers.length" @click="openUnitMove" class="ml5" icon="el-icon-edit"
|
||||
size="small" type="primary">
|
||||
单位调整
|
||||
</el-button>
|
||||
|
||||
<el-radio-group @change="doSearch" class="mr5"
|
||||
size="small"
|
||||
v-model="pageForm.isMember">
|
||||
<el-radio-button :label="null">全部教工</el-radio-button>
|
||||
<el-radio-button :label="1">会员</el-radio-button>
|
||||
<el-radio-button :label="0">非会员</el-radio-button>
|
||||
</el-radio-group>
|
||||
|
||||
<span class="text-danger">人员异动:</span>
|
||||
<el-radio-group @change="doSearch" class="mr5" size="small"
|
||||
v-model="pageForm.changed">
|
||||
<el-radio-button :label="2">全部</el-radio-button>
|
||||
<el-radio-button :label="1">异动</el-radio-button>
|
||||
<el-radio-button :label="0">未异动</el-radio-button>
|
||||
</el-radio-group>
|
||||
|
||||
<!--<div v-if="pageForm.changed == '1'" class="div-enum">
|
||||
<dict-select v-model="pageForm.USER_CHANGE_TYPE" placeholder="异动类型" @change="doSearch"
|
||||
code="UserType"></dict-select>
|
||||
</div>-->
|
||||
|
||||
<!--<el-button type="primary" size="small" icon="el-icon-printer" @click="exportExcel">导出会员名单</el-button>-->
|
||||
|
||||
<el-popover
|
||||
placement="bottom"
|
||||
trigger="click"
|
||||
width="200">
|
||||
|
||||
<div style="padding:4px 0;border-bottom:1px solid #eee;">
|
||||
<el-button size="mini" @click="checkAll">全选</el-button>
|
||||
<el-button size="mini" @click="invertCheck">反选</el-button>
|
||||
</div>
|
||||
|
||||
<div style="max-height:40vh;overflow-y:auto;padding:6px 0;">
|
||||
<el-checkbox-group v-model="checkedFields">
|
||||
<el-checkbox
|
||||
v-for="f in tableColumns"
|
||||
:key="f.prop"
|
||||
:label="f.prop"
|
||||
style="display:block;margin:6px 0;">
|
||||
{{ f.label }}
|
||||
</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
</div>
|
||||
|
||||
<el-button
|
||||
slot="reference"
|
||||
type="text"
|
||||
icon="el-icon-s-operation"
|
||||
size="small"
|
||||
style="margin-left:12px;">
|
||||
列设置
|
||||
</el-button>
|
||||
</el-popover>
|
||||
|
||||
</table-tool>
|
||||
|
||||
<div class="person-governance-change-table-wrap">
|
||||
<el-table :data="tableData" :size="tableSize" @selection-change="tableHandleSelectionChange" @sort-change="pageOrder"
|
||||
class="vi-table" ref="table" row-key="id" style="width: 100%" height="100%">
|
||||
<el-table-column :reserve-selection="true" type="selection"></el-table-column>
|
||||
<el-table-column :index="indexMethod" align="center" header-align="center"
|
||||
label="序号" type="index" width="80px" fixed="left"></el-table-column>
|
||||
<el-table-column :label="column.label" :prop="column.prop"
|
||||
:sortable="column.sortable" :width="column.width"
|
||||
align="center" header-align="center" min-width="100px"
|
||||
v-if="checkedFields.includes(column.prop)" :fixed="column.fixed"
|
||||
show-overflow-tooltip v-for="column in tableColumns">
|
||||
<template scope="{row}" v-if="column.prop=='loginname'">
|
||||
<el-link @click="openView(row.id)" type="primary">{{row.loginname}}</el-link>
|
||||
</template>
|
||||
<!-- 出生年月仅格式化表格显示值,保留原始数据供其他业务使用。 -->
|
||||
<template scope="{row}" v-if="column.prop=='birthday'">
|
||||
{{row.birthday ? $moment(row.birthday).format('YYYY-MM-DD') : ''}}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column header-align="center" label="异动信息">
|
||||
<el-table-column align="center" header-align="center" label="更新时间"
|
||||
min-width="145px" prop="changeTime"
|
||||
show-overflow-tooltip sortable>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="异动类型"
|
||||
prop="changeType" show-overflow-tooltip sortable>
|
||||
<template scope="{row}">
|
||||
<span style="color: #0e78c5;cursor: pointer" @click="openUserChangeInfo(row)">
|
||||
{{getChangeTypes(row.changeTypes)}}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column align="center" header-align="center" label="会员" prop="member"
|
||||
show-overflow-tooltip sortable>
|
||||
<template scope="{row}">
|
||||
<span class="text-success " v-if="row.member==1">
|
||||
<i class="fa fa-circle ml5"></i> 是
|
||||
</span>
|
||||
<span class="text-danger" v-else>
|
||||
<i class="fa fa-circle ml5"></i> 否
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column align="center" header-align="center" label="福利会员" prop="member"
|
||||
show-overflow-tooltip sortable>
|
||||
<template scope="{row}">
|
||||
<span class="text-success " v-if="row.welfareMember==1">
|
||||
<i class="fa fa-circle ml5"></i> 是
|
||||
</span>
|
||||
<span class="text-danger" v-else>
|
||||
<i class="fa fa-circle ml5"></i> 否
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column align="center" header-align="center" label="操作"
|
||||
prop="userOnline" width="100px" fixed="right">
|
||||
<template scope="{row}">
|
||||
<el-dropdown @command="dropdownCommand">
|
||||
<el-button plain size="mini">
|
||||
<i class="ti-settings"></i>
|
||||
<span class="ti-angle-down"></span>
|
||||
</el-button>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<el-dropdown-item :command="{type:'view',data:row}">
|
||||
查看
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
:command="{type:'change',data:row}"
|
||||
v-if="$auth.hasRoleOr(['SYSADMIN','SCHOOL_UNION_ADMIN','BRANCH_UNION_CHAIRMAN','BRANCH_UNION_CHAIRMAN','BRANCH_UNION_GROUP_LEADER'])">
|
||||
变更
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<!--#include("/layouts/pagination.html"){}#-->
|
||||
</el-card>
|
||||
|
||||
<template #view>
|
||||
<member-info ref="memberInfoRef"></member-info>
|
||||
</template>
|
||||
|
||||
<template #public>
|
||||
<member-change ref="memberChangeRef" :show-three-unit="true" @do-back="$refs.guava.index()" @do-submit="doSubmit"></member-change>
|
||||
</template>
|
||||
|
||||
<template #edit>
|
||||
<member-all-change ref="memberAllChangeRef"></member-all-change>
|
||||
</template>
|
||||
|
||||
<el-drawer
|
||||
:before-close="handleClose"
|
||||
:visible.sync="batchEditUserAttributeVisible"
|
||||
size="1000px" title="批量设置人员属性">
|
||||
<el-timeline style="margin-right: 40px">
|
||||
<el-timeline-item placement="top" timestamp="">
|
||||
<el-form :model="batchFormDataUserAttribute" label-width="100px" ref="batchFormUserAttribute">
|
||||
<el-row :gutter="60">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="人员属性">
|
||||
<dict-select
|
||||
@change="checkUsers.map(v => $set(v, 'userAttribute', batchFormDataUserAttribute.userAttribute))"
|
||||
clearable code="USER_ATTRIBUTE"
|
||||
placeholder="请选择人员属性"
|
||||
style="width: 300px"
|
||||
v-model="batchFormDataUserAttribute.userAttribute"></dict-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</el-timeline-item>
|
||||
<el-timeline-item placement="top" timestamp="">
|
||||
<el-table :data="checkUsers" class="vi-table" height="70vh" row-key="id" style="width: 100%">
|
||||
<el-table-column align="center" header-align="center" label="工号" prop="loginname" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="姓名" prop="username" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="所属单位" prop="unitName" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="所属工会" prop="unionName" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="人员属性" prop="userAttribute" show-overflow-tooltip width="300px">
|
||||
<template scope="{row}">
|
||||
<dict-select clearable code="USER_ATTRIBUTE" placeholder="请选择人员属性" style="width: 200px"
|
||||
v-model="row.userAttribute"></dict-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
|
||||
<span style="position: absolute;bottom: 20px;right: 20px">
|
||||
<el-button @click="batchEditUserAttributeVisible = false;checkUsers=[];$refs.table.clearSelection()">取消</el-button>
|
||||
<el-button :loading="batchEditLoading" @click="doBatchEditUserAttribute" type="primary">确定</el-button>
|
||||
</span>
|
||||
</el-drawer>
|
||||
|
||||
<el-drawer
|
||||
:before-close="handleClose"
|
||||
:visible.sync="batchEditMemberVisible"
|
||||
size="1000px" title="批量设置会员">
|
||||
<el-timeline style="margin-right: 40px">
|
||||
<el-timeline-item placement="top" timestamp="">
|
||||
<el-form :model="batchFormDataMember" label-width="100px" ref="batchFormMember">
|
||||
<el-row :gutter="60">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="是否会员">
|
||||
<el-switch
|
||||
@change="checkUsers.map(v => $set(v, 'member', batchFormDataMember.member))"
|
||||
v-model="batchFormDataMember.member"
|
||||
active-text="是"
|
||||
inactive-text="否">
|
||||
</el-switch>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="是否福利会员">
|
||||
<el-switch
|
||||
@change="checkUsers.map(v => $set(v, 'welfareMember', batchFormDataMember.welfareMember))"
|
||||
v-model="batchFormDataMember.welfareMember"
|
||||
active-text="是"
|
||||
inactive-text="否">
|
||||
</el-switch>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</el-timeline-item>
|
||||
<el-timeline-item placement="top" timestamp="">
|
||||
<el-table :data="checkUsers" class="vi-table" height="70vh" row-key="id" style="width: 100%">
|
||||
<el-table-column align="center" header-align="center" label="工号" prop="loginname" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="姓名" prop="username" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="所属单位" prop="unitName" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="所属工会" prop="unionName" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="是否会员" prop="member" show-overflow-tooltip width="150px">
|
||||
<template scope="{row}">
|
||||
<el-switch v-model="row.member" active-text="是" inactive-text="否"></el-switch>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="是否福利会员" prop="welfareMember" show-overflow-tooltip width="150px">
|
||||
<template scope="{row}">
|
||||
<el-switch v-model="row.welfareMember" active-text="是" inactive-text="否"></el-switch>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
|
||||
<span style="position: absolute;bottom: 20px;right: 20px">
|
||||
<el-button @click="batchEditMemberVisible = false;checkUsers=[];$refs.table.clearSelection()">取消</el-button>
|
||||
<el-button :loading="batchEditLoading" @click="doBatchEditMember" type="primary">确定</el-button>
|
||||
</span>
|
||||
</el-drawer>
|
||||
|
||||
<el-drawer
|
||||
:before-close="handleClose"
|
||||
:visible.sync="batchEditAidFundTypeVisible"
|
||||
size="1000px" title="批量设置人员分类">
|
||||
<el-timeline style="margin-right: 40px">
|
||||
<el-timeline-item placement="top" timestamp="">
|
||||
<el-form :model="batchFormDataAidFundType" label-width="100px" ref="batchFormAidFundType">
|
||||
<el-row :gutter="60">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="人员分类">
|
||||
<dict-select
|
||||
@change="checkUsers.map(v => $set(v, 'aidFundMemberUserType', batchFormDataAidFundType.aidFundMemberUserType))"
|
||||
clearable code="AIDFUND_MEMBER_USER_TYPE"
|
||||
placeholder="请选择人员分类"
|
||||
style="width: 300px"
|
||||
v-model="batchFormDataAidFundType.aidFundMemberUserType"></dict-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</el-timeline-item>
|
||||
<el-timeline-item placement="top" timestamp="">
|
||||
<el-table :data="checkUsers" class="vi-table" height="70vh" row-key="id" style="width: 100%">
|
||||
<el-table-column align="center" header-align="center" label="工号" prop="loginname" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="姓名" prop="username" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="所属单位" prop="unitName" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="所属工会" prop="unionName" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="人员分类" prop="aidFundMemberUserType" show-overflow-tooltip width="300px">
|
||||
<template scope="{row}">
|
||||
<dict-select clearable code="AIDFUND_MEMBER_USER_TYPE" placeholder="请选择人员分类" style="width: 200px"
|
||||
v-model="row.aidFundMemberUserType"></dict-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
|
||||
<span style="position: absolute;bottom: 20px;right: 20px">
|
||||
<el-button @click="batchEditAidFundTypeVisible = false;checkUsers=[];$refs.table.clearSelection()">取消</el-button>
|
||||
<el-button :loading="batchEditLoading" @click="doBatchEditAidFundType" type="primary">确定</el-button>
|
||||
</span>
|
||||
</el-drawer>
|
||||
|
||||
<el-drawer
|
||||
:before-close="handleClose"
|
||||
:visible.sync="batchEditUserStateVisible"
|
||||
size="1000px" title="批量设置在职状态">
|
||||
<el-timeline style="margin-right: 40px">
|
||||
<el-timeline-item placement="top" timestamp="">
|
||||
<el-form :model="batchFormDataUserState" label-width="100px" ref="batchFormUserState">
|
||||
<el-row :gutter="60">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="在职状态">
|
||||
<dict-select
|
||||
@change="checkUsers.map(v => $set(v, 'userState', batchFormDataUserState.userState))"
|
||||
clearable code="USER_STATE"
|
||||
placeholder="请选择在职状态"
|
||||
style="width: 300px"
|
||||
v-model="batchFormDataUserState.userState"></dict-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</el-timeline-item>
|
||||
<el-timeline-item placement="top" timestamp="">
|
||||
<el-table :data="checkUsers" class="vi-table" height="70vh" row-key="id" style="width: 100%">
|
||||
<el-table-column align="center" header-align="center" label="工号" prop="loginname" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="姓名" prop="username" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="所属单位" prop="unitName" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="所属工会" prop="unionName" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="在职状态" prop="userState" show-overflow-tooltip width="300px">
|
||||
<template scope="{row}">
|
||||
<dict-select clearable code="USER_STATE" placeholder="请选择在职状态" style="width: 200px"
|
||||
v-model="row.userState"></dict-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
|
||||
<span style="position: absolute;bottom: 20px;right: 20px">
|
||||
<el-button @click="batchEditUserStateVisible = false;checkUsers=[];$refs.table.clearSelection()">取消</el-button>
|
||||
<el-button :loading="batchEditLoading" @click="doBatchEditUserState" type="primary">确定</el-button>
|
||||
</span>
|
||||
</el-drawer>
|
||||
|
||||
<el-drawer
|
||||
:before-close="handleClose"
|
||||
:visible.sync="unitMoveVisible"
|
||||
size="1000px" title="批量单位调整">
|
||||
<el-timeline style="margin-right: 40px">
|
||||
<el-timeline-item placement="top" timestamp="">
|
||||
<el-form :model="unitMoveFormData" label-width="100px" ref="unitMoveForm">
|
||||
<el-row :gutter="60">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="所属工会">
|
||||
<el-select clearable filterable placeholder="请选择所属工会" style="width: 300px"
|
||||
v-model="unitMoveFormData.unionId" @change="moveUnionsChange">
|
||||
<el-option v-for="item in unitMoveUnions" :key="item.id" :label="item.name" :value="item.id"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="所属单位">
|
||||
<el-select clearable filterable placeholder="请选择所属单位" style="width: 300px"
|
||||
v-model="unitMoveFormData.unitId" @change="moveUnitChange">
|
||||
<el-option v-for="item in unitMoveUnits" :key="item.id" :label="item.name" :value="item.id"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</el-timeline-item>
|
||||
<el-timeline-item placement="top" timestamp="">
|
||||
<el-table :data="checkUsers" class="vi-table" height="70vh" row-key="id" style="width: 100%">
|
||||
<el-table-column align="center" header-align="center" label="工号" prop="loginname" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="姓名" prop="username" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="当前单位" prop="unitName" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="当前工会" prop="unionName" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="调整后工会" show-overflow-tooltip>
|
||||
<template>{{ unitMoveFormData.unionName }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="调整后单位" show-overflow-tooltip>
|
||||
<template>{{ unitMoveFormData.unitName }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
|
||||
<span style="position: absolute;bottom: 20px;right: 20px">
|
||||
<el-button @click="unitMoveVisible = false;checkUsers=[];$refs.table.clearSelection()">取消</el-button>
|
||||
<el-button :loading="batchEditLoading" @click="doUnitMove" type="primary">确定</el-button>
|
||||
</span>
|
||||
</el-drawer>
|
||||
</guava>
|
||||
</div>
|
||||
|
||||
<script nonce="${cspNonce!}">
|
||||
|
||||
<!--#include("common/memberInfo.js"){}#-->
|
||||
<!--#include("common/memberChange.js"){}#-->
|
||||
<!--#include("common/memberAllChangeInfo.js"){}#-->
|
||||
|
||||
new Vue({
|
||||
el: "#app",
|
||||
store,
|
||||
mixins: [initTableMixins],
|
||||
data: {
|
||||
pageReady: false,
|
||||
pickerOptions: {
|
||||
shortcuts: [{
|
||||
text: '最近一周',
|
||||
onClick(picker) {
|
||||
const end = new Date();
|
||||
const start = new Date();
|
||||
start.setTime(start.getTime() - 3600 * 1000 * 24 * 7);
|
||||
picker.$emit('pick', [start, end]);
|
||||
}
|
||||
}, {
|
||||
text: '最近一个月',
|
||||
onClick(picker) {
|
||||
const end = new Date();
|
||||
const start = new Date();
|
||||
start.setTime(start.getTime() - 3600 * 1000 * 24 * 30);
|
||||
picker.$emit('pick', [start, end]);
|
||||
}
|
||||
}, {
|
||||
text: '最近三个月',
|
||||
onClick(picker) {
|
||||
const end = new Date();
|
||||
const start = new Date();
|
||||
start.setTime(start.getTime() - 3600 * 1000 * 24 * 90);
|
||||
picker.$emit('pick', [start, end]);
|
||||
}
|
||||
}]
|
||||
},
|
||||
pageForm: {
|
||||
unionId: "",
|
||||
unitId: "",
|
||||
threeUnitId: "",
|
||||
sex: "",
|
||||
searchKeyword: "",
|
||||
changed: 2,
|
||||
isMember: 1,
|
||||
changeTypes: [],
|
||||
aidFundMemberUserType: "",
|
||||
userAttributes: [],
|
||||
},
|
||||
unions: [],
|
||||
units: [],
|
||||
threeUnits: [],
|
||||
id: "",
|
||||
checkUsers: [],
|
||||
batchEditLoading: false,
|
||||
batchEditUserAttributeVisible: false,
|
||||
batchEditMemberVisible: false,
|
||||
batchEditAidFundTypeVisible: false,
|
||||
batchEditUserStateVisible: false,
|
||||
unitMoveVisible: false,
|
||||
batchFormDataUserAttribute: {},
|
||||
batchFormDataMember: {member: false, welfareMember: false},
|
||||
batchFormDataAidFundType: {},
|
||||
batchFormDataUserState: {},
|
||||
unitMoveFormData: {
|
||||
unitId: "",
|
||||
unionId: "",
|
||||
unitName: "",
|
||||
unionName: ""
|
||||
},
|
||||
unitMoveUnions: [],
|
||||
unitMoveUnits: [],
|
||||
tableColumns: [
|
||||
{ prop: "loginname", label: "工号", width: 120, fixed: "left"},
|
||||
{ prop: "username", label: "姓名", width: 120, fixed: "left" },
|
||||
{ prop: "sex", label: "性别", width: 120},
|
||||
{ prop: "birthday", label: "出生年月", width: 120, sortable: true },
|
||||
{ prop: "nationality", label: "国籍", width: 120, sortable: true, checked: 0 },
|
||||
// 临时屏蔽“民族”列表字段,不删除配置,后续需要可恢复。
|
||||
// { prop: "nation", label: "民族", width: 120, sortable: true},
|
||||
{ prop: "mobile", label: "联系电话", width: 120 },
|
||||
// 临时屏蔽“政治面貌”列表字段,不删除配置,后续需要可恢复。
|
||||
// { prop: "political", label: "政治面貌", width: 120, sortable: true},
|
||||
{ prop: "userState", label: "在职状态", width: 120, sortable: true },
|
||||
// 临时隐藏原人员类型位置,人员类型显示到原“人员分类”位置。
|
||||
// { prop: "personType", label: "人员类型", width: 120, sortable: true, checked: 0 },
|
||||
// { prop: "preparedBy", label: "编制类别", width: 120, sortable: true },
|
||||
{ prop: "postDoctoralJoinDate", label: "进站时间", sortable: true, checked: 0 },
|
||||
{ prop: "unionName", label: "所属工会", width: 120, sortable: true },
|
||||
{ prop: "unitName", label: "所属单位", width: 120, sortable: true },
|
||||
{ prop: "personType", label: "人员类型", width: 120, sortable: true },
|
||||
// 临时屏蔽“人员分类”列表字段,不删除配置,后续需要可恢复。
|
||||
// { prop: "aidFundMemberUserType", label: "人员分类", width: 120, sortable: true },
|
||||
{ prop: "userAttribute", label: "人员属性", width: 120, sortable: true },
|
||||
{ prop: "idCardType", label: "证件类型", width: 120, sortable: true, checked: 0 },
|
||||
{ prop: "idCard", label: "证件号码", width: 120, sortable: true, checked: 0 },
|
||||
{ prop: "marriage", label: "婚姻状况", width: 120, sortable: true, checked: 0 },
|
||||
{ prop: "jobCategory", label: "岗位类别", width: 120, sortable: true, checked: 0 },
|
||||
|
||||
{ prop: "professionalTitle", label: "职称", width: 120, sortable: true, checked: 0 },
|
||||
{ prop: "professionalLevel", label: "职级", width: 120, sortable: true, checked: 0 },
|
||||
{ prop: "employeeSource", label: "职工来源", width: 120, sortable: true, checked: 0 },
|
||||
{ prop: "administrativeLevel", label: "行政级别(管理岗位)", width: 120, sortable: true, checked: 0 },
|
||||
{ prop: "positionSeries", label: "岗位系列", width: 120, sortable: true, checked: 0 },
|
||||
{ prop: "positionLevel", label: "岗级", width: 120, sortable: true, checked: 0 },
|
||||
{ prop: "administrativePositionLevel", label: "行政岗级", width: 120, sortable: true, checked: 0 },
|
||||
{ prop: "arrivalAtSchoolDate", label: "来校时间", width: 120, sortable: true, checked: 0 },
|
||||
{ prop: "teachingTime", label: "从教年月", width: 120, sortable: true, checked: 0 },
|
||||
{ prop: "expectedLeaveSchoolDate", label: "预计/最后离校时间", width: 120, sortable: true, checked: 0 },
|
||||
],
|
||||
|
||||
changeTypeData: [],
|
||||
checkedFields: [],
|
||||
allowChangeFields: []
|
||||
},
|
||||
components: {
|
||||
"member-info": PERSON_GOVERNANCE_MEMBER_INFO,
|
||||
"member-change": PERSON_GOVERNANCE_MEMBER_CHANGE,
|
||||
"member-all-change": PERSON_GOVERNANCE_MEMBER_ALL_CHANGE_INFO,
|
||||
},
|
||||
methods: {
|
||||
// 分页和导出共用该参数构建方法,保证人员属性多选值始终按 JSON 数组提交。
|
||||
buildQueryPageForm(source = this.pageForm) {
|
||||
const pageForm = clone(source)
|
||||
pageForm.userAttributes = JSON.stringify(source.userAttributes || [])
|
||||
return pageForm
|
||||
},
|
||||
// 覆盖公共分页方法,使后端按人员属性集合执行查询。
|
||||
pageData(data = null) {
|
||||
return initTableMixins.methods.pageData.call(this, this.buildQueryPageForm(data || this.pageForm))
|
||||
},
|
||||
showPage() {
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.table && this.$refs.table.doLayout) {
|
||||
this.$refs.table.doLayout()
|
||||
}
|
||||
window.requestAnimationFrame(() => {
|
||||
this.$set(this, "pageReady", true)
|
||||
})
|
||||
})
|
||||
},
|
||||
dropdownCommand(command) {
|
||||
const {type, data} = command
|
||||
if (type === 'view') {
|
||||
this.openView(data.id)
|
||||
} else if (type === 'change') {
|
||||
this.openChange(data.id)
|
||||
}
|
||||
},
|
||||
openChange(id) {
|
||||
this.$refs.guava.public(() => {
|
||||
this.$refs.memberChangeRef.init(id)
|
||||
})
|
||||
},
|
||||
openView(id) {
|
||||
this.$refs.guava.view(() => {
|
||||
this.$refs.memberInfoRef.onOpen(id)
|
||||
})
|
||||
},
|
||||
|
||||
openUserChangeInfo(row) {
|
||||
this.$refs.guava.edit(() => {
|
||||
this.$refs.memberAllChangeRef.onOpen(row.id)
|
||||
})
|
||||
},
|
||||
|
||||
doSubmit(data) {
|
||||
const loading = createLoading('正在提交中')
|
||||
this.$axios.post("/platform/personGovernance/personChange/doSubmitChange", data)
|
||||
.then((resp) => {
|
||||
if (resp.code === 0) {
|
||||
this.$message.success(resp.msg)
|
||||
this.$refs.guava.index()
|
||||
this.pageData()
|
||||
} else {
|
||||
this.$message.error(resp.msg)
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
loading.close()
|
||||
})
|
||||
},
|
||||
getChangeTypes(changeTypes){
|
||||
if (!changeTypes) return null
|
||||
const changeTypesList = JSON.parse(changeTypes)
|
||||
if (changeTypesList) {
|
||||
if (!changeTypesList || changeTypesList.length === 0 || !this.changeTypeData || this.changeTypeData.length === 0) return null
|
||||
return changeTypesList
|
||||
.map((v) => {
|
||||
return this.changeTypeData.find((item) => item.code === v).changeTypeName
|
||||
})
|
||||
.join(",")
|
||||
}
|
||||
return null
|
||||
},
|
||||
changeDateRangeChange(val) {
|
||||
if (val && val.length > 0) {
|
||||
this.$set(this.pageForm, "changeDateBefore", val[0])
|
||||
this.$set(this.pageForm, "changeDateEnd", val[1])
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 导出会员名单
|
||||
*/
|
||||
exportExcel() {
|
||||
const pageForm = this.buildQueryPageForm()
|
||||
const data = this.tableColumns.filter(item => this.checkedFields.includes(item.prop)).map(item => {
|
||||
return {prop: item.prop, label: item.label}
|
||||
})
|
||||
pageForm.columns = JSON.stringify(data)
|
||||
this.$downLoad("/platform/personGovernance/personChange/doExport", pageForm)
|
||||
},
|
||||
initData() {
|
||||
const isAdmin = this.$auth.hasRoleOr(["SYSADMIN", "SCHOOL_UNION_ADMIN"])
|
||||
const unionId = isAdmin ? this.pageForm.unionId : this.$store.state.user.union.id
|
||||
return Promise.all([
|
||||
this.$businessTool.listUnion(unionId),
|
||||
this.$businessTool.getEnumOptions("MemberChangeType"),
|
||||
this.$businessTool.getDictOptions("ALLOW_CHANGE_FIELDS")
|
||||
]).then((result) => {
|
||||
this.$set(this, "unions", result[0])
|
||||
this.$set(this, "changeTypeData", result[1])
|
||||
this.$set(this, "allowChangeFields", result[2])
|
||||
})
|
||||
},
|
||||
flushUnits() {
|
||||
this.$set(this.pageForm, "unitId", null)
|
||||
this.$set(this.pageForm, "threeUnitId", null)
|
||||
this.$set(this, "units", [])
|
||||
this.$set(this, "threeUnits", [])
|
||||
if (this.pageForm.unionId) {
|
||||
this.$businessTool.listUnit(this.pageForm.unionId).then(data => {
|
||||
this.$set(this, "units", data)
|
||||
})
|
||||
}
|
||||
},
|
||||
// 所属单位变化时只加载其直属三级单位,避免跨单位筛选。
|
||||
flushThreeUnits(unitId) {
|
||||
this.$set(this.pageForm, "threeUnitId", null)
|
||||
this.$set(this, "threeUnits", [])
|
||||
if (!unitId) {
|
||||
return
|
||||
}
|
||||
this.$axios.post("/platform/sys/unit/child", {pid: unitId})
|
||||
.then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.$set(this, "threeUnits", res.data || [])
|
||||
} else {
|
||||
this.$message.error(res.msg || "三级单位查询失败")
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
this.$message.error("三级单位查询失败,请稍后重试")
|
||||
})
|
||||
.finally(() => {})
|
||||
},
|
||||
checkAll() {
|
||||
this.$set(this, "checkedFields", this.tableColumns.map(c => c.prop));
|
||||
},
|
||||
invertCheck() {
|
||||
const all = this.tableColumns.map(c => c.prop);
|
||||
this.$set(this, "checkedFields", all.filter(p => !this.checkedFields.includes(p)));
|
||||
},
|
||||
tableHandleSelectionChange(val) {
|
||||
this.$set(this, "checkUsers", JSON.parse(JSON.stringify([...val])))
|
||||
},
|
||||
handleClose(done) {
|
||||
this.$confirm("确认关闭?").then(() => {
|
||||
done()
|
||||
this.$set(this, "checkUsers", [])
|
||||
this.$refs.table.clearSelection()
|
||||
}).catch(() => {})
|
||||
},
|
||||
openBatchEditUserAttribute() {
|
||||
this.$set(this, "batchFormDataUserAttribute", {})
|
||||
this.$set(this, "batchEditUserAttributeVisible", true)
|
||||
},
|
||||
openBatchEditMember() {
|
||||
this.$set(this, "batchFormDataMember", {member: false, welfareMember: false})
|
||||
this.$set(this, "batchEditMemberVisible", true)
|
||||
},
|
||||
openBatchEditAidFundType() {
|
||||
this.$set(this, "batchFormDataAidFundType", {})
|
||||
this.$set(this, "batchEditAidFundTypeVisible", true)
|
||||
},
|
||||
openBatchEditUserState() {
|
||||
this.$set(this, "batchFormDataUserState", {})
|
||||
this.$set(this, "batchEditUserStateVisible", true)
|
||||
},
|
||||
openUnitMove() {
|
||||
this.$set(this, "unitMoveFormData", {
|
||||
unitId: "",
|
||||
unionId: "",
|
||||
unitName: "",
|
||||
unionName: ""
|
||||
})
|
||||
this.$set(this, "unitMoveUnits", [])
|
||||
this.$set(this, "unitMoveVisible", true)
|
||||
const isAdmin = this.$auth.hasRoleOr(["SYSADMIN", "SCHOOL_UNION_ADMIN"])
|
||||
const unionId = isAdmin ? this.pageForm.unionId : this.$store.state.user.union.id
|
||||
this.$businessTool.listUnion(unionId).then((data) => {
|
||||
this.$set(this, "unitMoveUnions", data)
|
||||
})
|
||||
},
|
||||
moveUnionsChange(val) {
|
||||
if (val) {
|
||||
this.$businessTool.listUnit(val).then((data) => {
|
||||
this.$set(this, "unitMoveUnits", data)
|
||||
const union = this.unitMoveUnions.find(v => v.id === val)
|
||||
this.$set(this.unitMoveFormData, "unionName", union ? union.name : "")
|
||||
this.$set(this.unitMoveFormData, "unitId", "")
|
||||
this.$set(this.unitMoveFormData, "unitName", "")
|
||||
})
|
||||
} else {
|
||||
this.$set(this.unitMoveFormData, "unitId", "")
|
||||
this.$set(this.unitMoveFormData, "unitName", "")
|
||||
this.$set(this.unitMoveFormData, "unionName", "")
|
||||
this.$set(this, "unitMoveUnits", [])
|
||||
}
|
||||
},
|
||||
moveUnitChange(val) {
|
||||
if (val) {
|
||||
const unit = this.unitMoveUnits.find(v => v.id === val)
|
||||
this.$set(this.unitMoveFormData, "unitName", unit ? unit.name : "")
|
||||
} else {
|
||||
this.$set(this.unitMoveFormData, "unitName", "")
|
||||
}
|
||||
},
|
||||
doBatchEditUserAttribute() {
|
||||
const batch = this.checkUsers.map(v => {
|
||||
return {id: v.id, userAttribute: v.userAttribute}
|
||||
})
|
||||
this.$set(this, "batchEditLoading", true)
|
||||
this.$axios.post("/platform/personGovernance/personChange/batchUpdateUserAttribute", {users: JSON.stringify(batch)})
|
||||
.then((resp) => {
|
||||
if (resp.code === 0) {
|
||||
this.$message.success(resp.msg)
|
||||
this.pageData()
|
||||
this.$refs.table.clearSelection()
|
||||
this.$set(this, "batchEditUserAttributeVisible", false)
|
||||
this.$set(this, "checkUsers", [])
|
||||
} else {
|
||||
this.$message.error(resp.msg)
|
||||
}
|
||||
}).finally(() => {
|
||||
this.$set(this, "batchEditLoading", false)
|
||||
})
|
||||
},
|
||||
doBatchEditMember() {
|
||||
const batch = this.checkUsers.map(v => {
|
||||
return {id: v.id, member: v.member, welfareMember: v.welfareMember}
|
||||
})
|
||||
this.$set(this, "batchEditLoading", true)
|
||||
this.$axios.post("/platform/personGovernance/personChange/batchUpdateMember", {users: JSON.stringify(batch)})
|
||||
.then((resp) => {
|
||||
if (resp.code === 0) {
|
||||
this.$message.success(resp.msg)
|
||||
this.pageData()
|
||||
this.$refs.table.clearSelection()
|
||||
this.$set(this, "batchEditMemberVisible", false)
|
||||
this.$set(this, "checkUsers", [])
|
||||
} else {
|
||||
this.$message.error(resp.msg)
|
||||
}
|
||||
}).finally(() => {
|
||||
this.$set(this, "batchEditLoading", false)
|
||||
})
|
||||
},
|
||||
doBatchEditAidFundType() {
|
||||
const batch = this.checkUsers.map(v => {
|
||||
return {id: v.id, aidFundMemberUserType: v.aidFundMemberUserType}
|
||||
})
|
||||
this.$set(this, "batchEditLoading", true)
|
||||
this.$axios.post("/platform/personGovernance/personChange/batchUpdateAidFundType", {users: JSON.stringify(batch)})
|
||||
.then((resp) => {
|
||||
if (resp.code === 0) {
|
||||
this.$message.success(resp.msg)
|
||||
this.pageData()
|
||||
this.$refs.table.clearSelection()
|
||||
this.$set(this, "batchEditAidFundTypeVisible", false)
|
||||
this.$set(this, "checkUsers", [])
|
||||
} else {
|
||||
this.$message.error(resp.msg)
|
||||
}
|
||||
}).finally(() => {
|
||||
this.$set(this, "batchEditLoading", false)
|
||||
})
|
||||
},
|
||||
doBatchEditUserState() {
|
||||
const batch = this.checkUsers.map(v => {
|
||||
return {id: v.id, userState: v.userState}
|
||||
})
|
||||
this.$set(this, "batchEditLoading", true)
|
||||
this.$axios.post("/platform/personGovernance/personChange/batchUpdateUserState", {users: JSON.stringify(batch)})
|
||||
.then((resp) => {
|
||||
if (resp.code === 0) {
|
||||
this.$message.success(resp.msg)
|
||||
this.pageData()
|
||||
this.$refs.table.clearSelection()
|
||||
this.$set(this, "batchEditUserStateVisible", false)
|
||||
this.$set(this, "checkUsers", [])
|
||||
} else {
|
||||
this.$message.error(resp.msg)
|
||||
}
|
||||
}).finally(() => {
|
||||
this.$set(this, "batchEditLoading", false)
|
||||
})
|
||||
},
|
||||
doUnitMove() {
|
||||
if (!this.unitMoveFormData.unitId) {
|
||||
this.$message.warning("请选择新单位!")
|
||||
return
|
||||
}
|
||||
const params = {
|
||||
unitId: this.unitMoveFormData.unitId,
|
||||
userIds: JSON.stringify(this.checkUsers.map(v => v.id))
|
||||
}
|
||||
this.$set(this, "batchEditLoading", true)
|
||||
this.$axios.post("/platform/personGovernance/personChange/unitMove", params)
|
||||
.then((resp) => {
|
||||
if (resp.code === 0) {
|
||||
this.$message.success(resp.msg)
|
||||
this.pageData()
|
||||
this.$refs.table.clearSelection()
|
||||
this.$set(this, "unitMoveVisible", false)
|
||||
this.$set(this, "checkUsers", [])
|
||||
} else {
|
||||
this.$message.error(resp.msg)
|
||||
}
|
||||
}).finally(() => {
|
||||
this.$set(this, "batchEditLoading", false)
|
||||
})
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
showColumns() {
|
||||
return this.tableColumns.filter(c => this.checkedFields.includes(c.prop));
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$set(this, "checkedFields", this.tableColumns.filter(c => c.checked !== 0).map(c => c.prop));
|
||||
},
|
||||
created() {
|
||||
// 单位、字典和列表请求全部结束后再显示页面,避免初始化过程中的布局跳动。
|
||||
$.when(this.initData(), this.pageData()).always(() => {
|
||||
this.showPage()
|
||||
})
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<!--#
|
||||
}
|
||||
#-->
|
||||
@@ -0,0 +1,414 @@
|
||||
<!--# layout("/layouts/platform.html"){ #-->
|
||||
<link rel="stylesheet" href="${base!}/assets/platform/css/personGovernance.css?v=1.0.22" />
|
||||
<div
|
||||
id="app"
|
||||
v-cloak
|
||||
class="person-governance-list-page person-governance-page-transition"
|
||||
:class="{'is-ready':pageReady}"
|
||||
>
|
||||
<el-card shadow="never" class="person-governance-search-card">
|
||||
<el-tabs v-model="activeTab" @tab-click="switchTab">
|
||||
<el-tab-pane label="待确认" name="PENDING"></el-tab-pane>
|
||||
<el-tab-pane label="已确认" name="PROCESSED"></el-tab-pane>
|
||||
</el-tabs>
|
||||
<el-alert
|
||||
v-if="activeTab==='PENDING'"
|
||||
title="以下人员在第三方来源中已不存在,但仍保留在本系统,请确认处理。"
|
||||
type="warning"
|
||||
:closable="false"
|
||||
></el-alert>
|
||||
<el-form class="mt20" :inline="true" :model="pageForm">
|
||||
<el-form-item>
|
||||
<el-input
|
||||
v-model="pageForm.searchKeyword"
|
||||
clearable
|
||||
placeholder="请输入内容"
|
||||
@keyup.enter.native="doSearch"
|
||||
>
|
||||
<el-select v-model="pageForm.searchName" slot="prepend" style="width: 100px">
|
||||
<el-option label="姓名" value="personName"></el-option>
|
||||
<el-option label="人员编号" value="personNo"></el-option>
|
||||
</el-select>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="所属部门">
|
||||
<el-select v-model="pageForm.unitId" clearable filterable>
|
||||
<el-option v-for="u in units" :key="u.id" :label="u.name" :value="u.id"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="activeTab==='PROCESSED'" label="处理结果">
|
||||
<el-select v-model="pageForm.handleResult" clearable filterable>
|
||||
<el-option label="已停用" value="DISABLE"></el-option>
|
||||
<el-option label="继续保留" value="KEEP"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="doSearch">查询</el-button>
|
||||
<el-button @click="resetSearch">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
<el-card shadow="never" class="person-governance-list-card">
|
||||
<div class="person-governance-table-wrap">
|
||||
<el-table
|
||||
:key="'person-exception-table-' + activeTab"
|
||||
ref="dataTable"
|
||||
:data="tableData"
|
||||
v-loading="tableLoading"
|
||||
height="100%"
|
||||
>
|
||||
<el-table-column v-if="activeTab==='PENDING'" type="selection" width="45"></el-table-column>
|
||||
<el-table-column prop="personNo" label="工号"></el-table-column>
|
||||
<el-table-column prop="personName" label="名称"></el-table-column>
|
||||
<el-table-column prop="unitName" label="单位"></el-table-column>
|
||||
<el-table-column label="异常类型"><template>第三方不存在</template></el-table-column>
|
||||
<el-table-column prop="discoverTime" label="发现时间" width="180"></el-table-column>
|
||||
<el-table-column label="当前状态">
|
||||
<template v-slot="{row}">{{row.status==='PENDING'?'待确认':'已确认'}}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="120">
|
||||
<template v-slot="{row}">
|
||||
<el-button
|
||||
v-if="activeTab==='PENDING'"
|
||||
type="primary"
|
||||
plain
|
||||
size="small"
|
||||
@click="view(row, $event)"
|
||||
>确认</el-button>
|
||||
<el-button v-else type="text" @click="view(row)">查看</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="person-governance-list-footer">
|
||||
<!--#include("/layouts/pagination.html"){}#-->
|
||||
</div>
|
||||
</el-card>
|
||||
<el-drawer
|
||||
:title="activeTab==='PENDING'?'异常人员确认':'异常处理详情'"
|
||||
:visible.sync="drawerVisible"
|
||||
:append-to-body="true"
|
||||
size="85%"
|
||||
custom-class="person-governance-drawer"
|
||||
>
|
||||
<div class="person-exception-handle-page" v-loading="detailLoading">
|
||||
<el-form
|
||||
v-if="detail.exception"
|
||||
:model="handleForm"
|
||||
label-width="0"
|
||||
label-suffix=":"
|
||||
class="person-exception-handle-form"
|
||||
>
|
||||
<template v-if="detail.user">
|
||||
<vi-title title="人员信息"></vi-title>
|
||||
<el-descriptions :column="3" border>
|
||||
<el-descriptions-item label="工号"><el-input :value="detail.user.loginname" readonly size="small"></el-input></el-descriptions-item>
|
||||
<el-descriptions-item label="姓名"><el-input :value="detail.user.username" readonly size="small"></el-input></el-descriptions-item>
|
||||
<el-descriptions-item label="性别"><el-input :value="detail.user.sex" readonly size="small"></el-input></el-descriptions-item>
|
||||
<el-descriptions-item label="民族"><el-input :value="detail.user.nation" readonly size="small"></el-input></el-descriptions-item>
|
||||
<el-descriptions-item label="出生日期"><el-input :value="detail.user.birthday" readonly size="small"></el-input></el-descriptions-item>
|
||||
<el-descriptions-item label="政治面貌"><el-input :value="detail.user.political" readonly size="small"></el-input></el-descriptions-item>
|
||||
<el-descriptions-item label="学历"><el-input :value="detail.user.education" readonly size="small"></el-input></el-descriptions-item>
|
||||
<el-descriptions-item label="学位"><el-input :value="detail.user.academicDegree" readonly size="small"></el-input></el-descriptions-item>
|
||||
<el-descriptions-item label="党政职务"><el-input :value="detail.user.position" readonly size="small"></el-input></el-descriptions-item>
|
||||
<el-descriptions-item label="工作单位"><el-input :value="detail.user.unitName" readonly size="small"></el-input></el-descriptions-item>
|
||||
<el-descriptions-item label="所属工会"><el-input :value="detail.user.unionName" readonly size="small"></el-input></el-descriptions-item>
|
||||
<el-descriptions-item label="本地人员状态"><el-input :value="detail.user.disabled ? '已停用' : '在用'" readonly size="small"></el-input></el-descriptions-item>
|
||||
<el-descriptions-item label="身份证号码"><el-input :value="detail.user.idcard" readonly size="small"></el-input></el-descriptions-item>
|
||||
<el-descriptions-item label="联系电话"><el-input :value="detail.user.mobile" readonly size="small"></el-input></el-descriptions-item>
|
||||
<el-descriptions-item label="电子邮箱"><el-input :value="detail.user.email" readonly size="small"></el-input></el-descriptions-item>
|
||||
<el-descriptions-item label="在职状态">
|
||||
<el-select
|
||||
v-if="activeTab==='PENDING'"
|
||||
v-model="handleForm.userState"
|
||||
clearable
|
||||
filterable
|
||||
size="small"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in userStateOptions"
|
||||
:key="item.code"
|
||||
:label="item.name"
|
||||
:value="item.code"
|
||||
></el-option>
|
||||
</el-select>
|
||||
<el-input v-else :value="detail.user.userState" readonly size="small"></el-input>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="人员类型">
|
||||
<el-select
|
||||
v-if="activeTab==='PENDING'"
|
||||
v-model="handleForm.personType"
|
||||
clearable
|
||||
filterable
|
||||
size="small"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in personTypeOptions"
|
||||
:key="item.code"
|
||||
:label="item.name"
|
||||
:value="item.code"
|
||||
></el-option>
|
||||
</el-select>
|
||||
<el-input v-else :value="detail.user.personType" readonly size="small"></el-input>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="会员状态">
|
||||
<el-select
|
||||
v-if="activeTab==='PENDING'"
|
||||
v-model="handleForm.member"
|
||||
clearable
|
||||
filterable
|
||||
size="small"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option label="会员" :value="1"></el-option>
|
||||
<el-option label="非会员" :value="0"></el-option>
|
||||
</el-select>
|
||||
<el-input v-else :value="detail.user.member == 1 ? '会员' : '非会员'" readonly size="small"></el-input>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="福利会员状态">
|
||||
<el-select
|
||||
v-if="activeTab==='PENDING'"
|
||||
v-model="handleForm.welfareMember"
|
||||
clearable
|
||||
filterable
|
||||
size="small"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option label="福利会员" :value="1"></el-option>
|
||||
<el-option label="非福利会员" :value="0"></el-option>
|
||||
</el-select>
|
||||
<el-input v-else :value="detail.user.welfareMember == 1 ? '福利会员' : '非福利会员'" readonly size="small"></el-input>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</template>
|
||||
|
||||
<vi-title :class="{'mt20': detail.user}" title="异常信息"></vi-title>
|
||||
<el-descriptions :column="3" border>
|
||||
<el-descriptions-item label="人员姓名"><el-input :value="detail.exception.personName" readonly size="small"></el-input></el-descriptions-item>
|
||||
<el-descriptions-item label="人员编号"><el-input :value="detail.exception.personNo" readonly size="small"></el-input></el-descriptions-item>
|
||||
<el-descriptions-item label="所属部门"><el-input :value="detail.exception.unitName" readonly size="small"></el-input></el-descriptions-item>
|
||||
<el-descriptions-item label="异常类型"><el-input value="第三方不存在" readonly size="small"></el-input></el-descriptions-item>
|
||||
<el-descriptions-item label="最后同步时间"><el-input :value="detail.exception.lastSyncTime" readonly size="small"></el-input></el-descriptions-item>
|
||||
<el-descriptions-item label="异常发现时间"><el-input :value="detail.exception.discoverTime" readonly size="small"></el-input></el-descriptions-item>
|
||||
<el-descriptions-item label="异常原因" :span="3"><el-input :value="detail.exception.exceptionReason" readonly size="small"></el-input></el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<template v-if="activeTab==='PENDING'">
|
||||
<vi-title class="mt20" title="处理信息"></vi-title>
|
||||
<el-descriptions :column="3" border>
|
||||
<el-descriptions-item label="处理建议" :span="1">
|
||||
<el-radio-group v-model="handleForm.result" size="small">
|
||||
<el-radio border label="DISABLE">建议停用</el-radio>
|
||||
<el-radio border label="KEEP">继续保留</el-radio>
|
||||
</el-radio-group>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="确认建议" :span="2">
|
||||
<el-input v-model="handleForm.opinion" type="textarea" :rows="4" maxlength="500" show-word-limit></el-input>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<div class="person-exception-handle-actions">
|
||||
<el-button @click="closeDrawer">暂存</el-button>
|
||||
<el-button type="primary" :loading="formLoading" @click="submitHandle">确认处理</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<vi-title class="mt20" title="处理结果"></vi-title>
|
||||
<el-descriptions :column="3" border>
|
||||
<el-descriptions-item label="处理建议"><el-input :value="detail.exception.handleResult==='DISABLE'?'建议停用':'继续保留'" readonly size="small"></el-input></el-descriptions-item>
|
||||
<el-descriptions-item label="处理人"><el-input :value="detail.exception.handlerName" readonly size="small"></el-input></el-descriptions-item>
|
||||
<el-descriptions-item label="处理时间"><el-input :value="detail.exception.handleTime" readonly size="small"></el-input></el-descriptions-item>
|
||||
<el-descriptions-item label="确认建议" :span="3"><el-input :value="detail.exception.handleOpinion" type="textarea" :rows="3" readonly></el-input></el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</template>
|
||||
</el-form>
|
||||
</div>
|
||||
</el-drawer>
|
||||
</div>
|
||||
<style>
|
||||
.person-exception-handle-page {
|
||||
box-sizing: border-box;
|
||||
min-height: 240px;
|
||||
padding: 0 24px 24px;
|
||||
}
|
||||
|
||||
.person-exception-handle-form .el-form-item {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.person-exception-handle-form .el-input__inner,
|
||||
.person-exception-handle-form .el-textarea__inner {
|
||||
background-color: #fff;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.person-exception-handle-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
const vue = new Vue({
|
||||
el: '#app',
|
||||
mixins: [initTableMixins],
|
||||
data() {
|
||||
return {
|
||||
pageReady: false,
|
||||
activeTab: 'PENDING',
|
||||
units: [],
|
||||
personTypeOptions: [],
|
||||
userStateOptions: [],
|
||||
drawerVisible: false,
|
||||
detailLoading: false,
|
||||
detail: {},
|
||||
currentIds: [],
|
||||
handleForm: {
|
||||
result: 'DISABLE',
|
||||
opinion: '',
|
||||
personType: '',
|
||||
userState: '',
|
||||
member: null,
|
||||
welfareMember: null,
|
||||
},
|
||||
pageForm: {
|
||||
status: 'PENDING',
|
||||
searchName: 'personName',
|
||||
searchKeyword: '',
|
||||
unitId: '',
|
||||
handleResult: '',
|
||||
pageNumber: 1,
|
||||
pageSize: 10,
|
||||
totalCount: 0,
|
||||
},
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
pageData() {
|
||||
this.$set(this.pageForm, 'status', this.activeTab)
|
||||
this.$set(this, 'tableLoading', true)
|
||||
return $.post(loc() + '/pageData', this.pageForm)
|
||||
.then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.$set(this, 'tableData', res.data.list)
|
||||
this.$set(this.pageForm, 'totalCount', res.data.totalCount)
|
||||
} else this.notifyWarning(res.msg)
|
||||
})
|
||||
.always(() => {
|
||||
this.$set(this, 'tableLoading', false)
|
||||
})
|
||||
},
|
||||
switchTab() {
|
||||
this.$set(this.pageForm, 'pageNumber', 1)
|
||||
this.pageData()
|
||||
},
|
||||
resetSearch() {
|
||||
this.$set(this.pageForm, 'searchName', 'personName')
|
||||
this.$set(this.pageForm, 'searchKeyword', '')
|
||||
this.$set(this.pageForm, 'unitId', '')
|
||||
this.$set(this.pageForm, 'handleResult', '')
|
||||
this.doSearch()
|
||||
},
|
||||
view(row, event) {
|
||||
// 点击确认后立即释放按钮焦点,避免 plain 按钮持续显示实心蓝色背景。
|
||||
if (event && event.currentTarget && event.currentTarget.blur) {
|
||||
event.currentTarget.blur()
|
||||
}
|
||||
this.$set(this, 'detail', {})
|
||||
this.$set(this, 'drawerVisible', true)
|
||||
this.$set(this, 'detailLoading', true)
|
||||
$.post(loc() + '/detail', { id: row.id })
|
||||
.then((res) => {
|
||||
if (res.code === 0) {
|
||||
const user = res.data.user || {}
|
||||
this.$set(this, 'detail', res.data)
|
||||
this.$set(this, 'currentIds', [row.id])
|
||||
this.$set(this, 'handleForm', {
|
||||
result: 'DISABLE',
|
||||
opinion: '',
|
||||
personType: user.personType || '',
|
||||
userState: user.userState || '',
|
||||
member: user.member === null || typeof user.member === 'undefined'
|
||||
? null
|
||||
: Number(user.member),
|
||||
welfareMember: user.welfareMember === null || typeof user.welfareMember === 'undefined'
|
||||
? null
|
||||
: Number(user.welfareMember),
|
||||
})
|
||||
} else {
|
||||
this.notifyWarning(res.msg)
|
||||
this.$set(this, 'drawerVisible', false)
|
||||
}
|
||||
})
|
||||
.always(() => {
|
||||
this.$set(this, 'detailLoading', false)
|
||||
})
|
||||
},
|
||||
closeDrawer() {
|
||||
this.$set(this, 'drawerVisible', false)
|
||||
},
|
||||
submitHandle() {
|
||||
if (!this.handleForm.opinion) {
|
||||
this.notifyWarning('请填写确认建议')
|
||||
return
|
||||
}
|
||||
if (this.handleForm.member === null || this.handleForm.member === '') {
|
||||
this.notifyWarning('请选择会员状态')
|
||||
return
|
||||
}
|
||||
if (this.handleForm.welfareMember === null || this.handleForm.welfareMember === '') {
|
||||
this.notifyWarning('请选择福利会员状态')
|
||||
return
|
||||
}
|
||||
this.$set(this, 'formLoading', true)
|
||||
$.post(loc() + '/handle', {
|
||||
ids: JSON.stringify(this.currentIds),
|
||||
handleResult: this.handleForm.result,
|
||||
opinion: this.handleForm.opinion,
|
||||
personType: this.handleForm.personType,
|
||||
userState: this.handleForm.userState,
|
||||
member: this.handleForm.member,
|
||||
welfareMember: this.handleForm.welfareMember,
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.notifySuccess('处理完成')
|
||||
this.$set(this, 'drawerVisible', false)
|
||||
this.pageData()
|
||||
} else this.notifyWarning(res.msg)
|
||||
})
|
||||
.always(() => {
|
||||
this.$set(this, 'formLoading', false)
|
||||
})
|
||||
},
|
||||
showPage() {
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.dataTable && this.$refs.dataTable.doLayout) {
|
||||
this.$refs.dataTable.doLayout()
|
||||
}
|
||||
window.requestAnimationFrame(() => {
|
||||
this.$set(this, 'pageReady', true)
|
||||
})
|
||||
})
|
||||
},
|
||||
},
|
||||
created() {
|
||||
// 当前项目通过 businessTool 获取单位和人员字典,避免依赖原项目 v.js 中不存在的全局方法。
|
||||
const unitRequest = this.$businessTool.listUnit().then((rows) => {
|
||||
this.$set(this, 'units', rows)
|
||||
})
|
||||
const personTypeRequest = this.$businessTool.getDictOptions('USER_PERSON_TYPE').then((rows) => {
|
||||
this.$set(this, 'personTypeOptions', rows)
|
||||
})
|
||||
const userStateRequest = this.$businessTool.getDictOptions('USER_STATE').then((rows) => {
|
||||
this.$set(this, 'userStateOptions', rows)
|
||||
})
|
||||
$.when(this.pageData(), unitRequest, personTypeRequest, userStateRequest).always(() => {
|
||||
this.showPage()
|
||||
})
|
||||
},
|
||||
})
|
||||
</script>
|
||||
<!--# } #-->
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -31,11 +31,17 @@
|
||||
<div class="login-form">
|
||||
<div class="my-3 text-6xl font-semibold">${AppName!}${base!}</div>
|
||||
<div class="flx-center-center">
|
||||
<div class="p-12 bg-white rounded-3xl" style="box-sizing: border-box; width: 100%">
|
||||
<el-form ref="loginForm" :model="loginForm" :rules="loginRules" auto-complete="on" label-position="left">
|
||||
<div class="login-card p-12 bg-white rounded-3xl" style="box-sizing: border-box">
|
||||
<div class="login-tabs" role="tablist" aria-label="登录方式">
|
||||
<button type="button" role="tab" :aria-selected="loginMode === 'password'" :class="{ active: loginMode === 'password' }" :disabled="formLoading || smsSending || captchaVisible" @click="switchLoginMode('password')">账号密码</button>
|
||||
<button v-if="smsLoginVisible" type="button" role="tab" :aria-selected="loginMode === 'sms'" :class="{ active: loginMode === 'sms' }" :disabled="formLoading || smsSending || captchaVisible" @click="switchLoginMode('sms')">手机验证码</button>
|
||||
</div>
|
||||
<el-form v-show="loginMode === 'password'" ref="loginForm" :model="loginForm" :rules="loginRules" auto-complete="on" label-position="left">
|
||||
<el-form-item prop="username">
|
||||
<el-input
|
||||
v-model="loginForm.username"
|
||||
:value="loginForm.username"
|
||||
@input="$set(loginForm, 'username', $event)"
|
||||
:disabled="formLoading || captchaVisible"
|
||||
type="text"
|
||||
auto-complete="off"
|
||||
placeholder="账号"
|
||||
@@ -44,7 +50,9 @@
|
||||
</el-form-item>
|
||||
<el-form-item prop="password">
|
||||
<el-input
|
||||
v-model="loginForm.password"
|
||||
:value="loginForm.password"
|
||||
@input="$set(loginForm, 'password', $event)"
|
||||
:disabled="formLoading || captchaVisible"
|
||||
type="password"
|
||||
auto-complete="off"
|
||||
placeholder="密码"
|
||||
@@ -52,37 +60,58 @@
|
||||
prefix-icon="el-icon-lock"
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="platformCaptcha">
|
||||
<el-input
|
||||
v-model="loginForm.platformCaptcha"
|
||||
auto-complete="off"
|
||||
placeholder="验证码"
|
||||
style="width: 60%"
|
||||
@keyup.enter.native="handleLogin"
|
||||
prefix-icon="el-icon-key"
|
||||
></el-input>
|
||||
<div class="login-code">
|
||||
<img :src="codeUrl" @click="getCode" />
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 100%">
|
||||
<el-button
|
||||
:loading="loading"
|
||||
:loading="formLoading"
|
||||
size="medium"
|
||||
type="primary"
|
||||
style="width: 100%; --color-primary: #1867b0"
|
||||
@click.native.prevent="handleLogin"
|
||||
>
|
||||
<span v-if="!loading">登 录</span>
|
||||
<span v-if="!formLoading">登 录</span>
|
||||
<span v-else>登 录 中...</span>
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-form v-show="smsLoginVisible && loginMode === 'sms'" ref="smsForm" :model="smsForm" :rules="smsRules" auto-complete="off" label-position="left">
|
||||
<el-form-item prop="mobile">
|
||||
<el-input :value="smsForm.mobile" @input="handleMobileChange" :disabled="formLoading || smsSending || captchaVisible" maxlength="11" inputmode="tel" placeholder="请输入系统绑定的手机号" prefix-icon="el-icon-mobile-phone"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="smsCode">
|
||||
<el-input :value="smsForm.smsCode" @input="$set(smsForm, 'smsCode', $event)" :disabled="formLoading || captchaVisible" maxlength="6" inputmode="numeric" autocomplete="one-time-code" placeholder="6位短信验证码" @keyup.enter.native="handleSmsLogin" prefix-icon="el-icon-message">
|
||||
<el-button slot="append" :loading="smsSending" :disabled="formLoading || smsCountdown > 0 || captchaVisible" @click="sendSmsCode">{{ smsCountdown > 0 ? smsCountdown + '秒后重发' : '获取验证码' }}</el-button>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 100%">
|
||||
<el-button :loading="formLoading" :disabled="smsSending || captchaVisible" size="medium" type="primary" style="width: 100%; --color-primary: #1867b0" @click.native.prevent="handleSmsLogin">
|
||||
<span v-if="!formLoading">登 录</span>
|
||||
<span v-else>登 录 中...</span>
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
<p class="login-tip">请使用系统账号绑定的手机号</p>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 两种登录方式共用滑块弹框;系统返回键只关闭当前弹框。 -->
|
||||
<el-dialog title="安全验证" custom-class="captcha-dialog" :visible="captchaVisible" :before-close="closeCaptcha" :close-on-click-modal="false" append-to-body>
|
||||
<div class="captcha-panel" v-loading="captchaLoading">
|
||||
<div class="captcha-heading">
|
||||
<span>拖动滑块,完成拼图</span>
|
||||
<el-button type="text" :disabled="captchaLoading || captchaChecking" @click="loadSlider">换一张</el-button>
|
||||
</div>
|
||||
<div class="captcha-picture">
|
||||
<template v-if="sliderData">
|
||||
<img :key="sliderData.challengeId + '-background'" class="captcha-background" :src="sliderData.background" alt="拼图背景" @load="onSliderImageLoad('background', $event)" @error="onSliderImageError" />
|
||||
<img :key="sliderData.challengeId + '-piece'" class="captcha-piece" :src="sliderData.piece" :style="pieceStyle" alt="待移动拼图" @load="onSliderImageLoad('piece', $event)" @error="onSliderImageError" />
|
||||
</template>
|
||||
</div>
|
||||
<input type="range" class="captcha-range" min="0" :max="sliderMax" step="1" :value="sliderOffset" :disabled="!sliderReady || captchaChecking || captchaLoading" aria-label="拖动滑块使拼图对齐缺口" @input="onSliderInput" @change="checkSlider" />
|
||||
<div class="captcha-hint" role="status">{{ captchaChecking ? '正在验证…' : captchaHint }}</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</body>
|
||||
<!-- import Vue before Element -->
|
||||
@@ -97,18 +126,12 @@
|
||||
<script src="${base!}/assets/platform/plugins/sm-crypto/sm2.js"></script>
|
||||
<!-- import commonUtil -->
|
||||
<script src="${base!}/assets/platform/js/util/commonUtil.js"></script>
|
||||
<script src="${base!}/assets/platform/js/util/h5PopupHistory.js"></script>
|
||||
<!-- import jsencrypt-->
|
||||
<script src="${base!}/assets/platform/plugins/jsencrypt/jsencrypt.min.js"></script>
|
||||
<script nonce="${cspNonce!}">
|
||||
// 在加载 lodash 后、使用前插入
|
||||
const originalDefaultsDeep = window._.defaultsDeep;
|
||||
window._.defaultsDeep = function(...args) {
|
||||
// 先对所有参数做原型污染清洗
|
||||
const cleanArgs = args.map(arg => sanitizeForPrototypePollution(arg));
|
||||
return originalDefaultsDeep.apply(window._, cleanArgs);
|
||||
};
|
||||
|
||||
function sanitizeForPrototypePollution(obj) {
|
||||
// 登录页未加载 lodash 时跳过补丁,避免在 Vue 初始化前中断脚本。
|
||||
const sanitizeForPrototypePollution = (obj) => {
|
||||
if (obj === null || typeof obj !== 'object') return obj;
|
||||
if (Array.isArray(obj)) return obj.map(sanitizeForPrototypePollution);
|
||||
|
||||
@@ -122,29 +145,64 @@
|
||||
}
|
||||
return clean;
|
||||
}
|
||||
if (window._ && typeof window._.defaultsDeep === "function") {
|
||||
const originalDefaultsDeep = window._.defaultsDeep;
|
||||
window._.defaultsDeep = (...args) => {
|
||||
const cleanArgs = args.map(arg => sanitizeForPrototypePollution(arg));
|
||||
return originalDefaultsDeep.apply(window._, cleanArgs);
|
||||
};
|
||||
}
|
||||
</script>
|
||||
<script nonce="${cspNonce!}">
|
||||
|
||||
|
||||
// 本页沿用原 axios 响应结构(res.data),不使用会解包响应的全局拦截器。
|
||||
Vue.prototype.$axios = axios
|
||||
|
||||
new Vue({
|
||||
el: "#app",
|
||||
data() {
|
||||
return {
|
||||
codeUrl: "",
|
||||
loginMode: "password",
|
||||
// 服务端仅输出布尔值;参数缺失时默认隐藏短信入口。
|
||||
smsLoginVisible: "${smsLoginVisible!false}" === "true",
|
||||
loginForm: {
|
||||
username: "",
|
||||
password: "",
|
||||
platformKey: "",
|
||||
platformCaptcha: "",
|
||||
grantType: "captcha"
|
||||
},
|
||||
loginRules: {
|
||||
username: [{ required: true, trigger: ["blur", "change"], message: "账号不能为空" }],
|
||||
password: [{ required: true, trigger: ["blur", "change"], message: "密码不能为空" }],
|
||||
platformCaptcha: [{ required: true, trigger: ["blur", "change"], message: "请输入验证码" }]
|
||||
},
|
||||
loading: false,
|
||||
smsForm: {
|
||||
mobile: "",
|
||||
smsCode: ""
|
||||
},
|
||||
smsRules: {
|
||||
mobile: [
|
||||
{ required: true, trigger: "blur", message: "请输入手机号" },
|
||||
{ pattern: /^1[3-9]\d{9}$/, trigger: "blur", message: "请输入正确的11位手机号" }
|
||||
],
|
||||
smsCode: [
|
||||
{ required: true, trigger: "blur", message: "请输入短信验证码" },
|
||||
{ pattern: /^\d{6}$/, trigger: "blur", message: "请输入6位数字短信验证码" }
|
||||
]
|
||||
},
|
||||
formLoading: false,
|
||||
smsSending: false,
|
||||
captchaVisible: false,
|
||||
captchaLoading: false,
|
||||
captchaChecking: false,
|
||||
captchaHint: '拖动下方滑块,将拼图移到缺口处',
|
||||
sliderData: null,
|
||||
sliderOffset: 0,
|
||||
sliderImages: { background: false, piece: false },
|
||||
sliderVersion: 0,
|
||||
sliderTimer: null,
|
||||
sliderContext: null,
|
||||
popupToken: null,
|
||||
pageShowHandler: null,
|
||||
smsCountdown: 0,
|
||||
smsTimer: null,
|
||||
rasParams: {
|
||||
publicKey: '',
|
||||
keyId: '',
|
||||
@@ -152,69 +210,251 @@
|
||||
},
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
sliderMax() {
|
||||
return this.sliderData ? this.sliderData.width - this.sliderData.pieceSize : 270
|
||||
},
|
||||
sliderReady() {
|
||||
return !!this.sliderData && this.sliderImages.background && this.sliderImages.piece
|
||||
},
|
||||
pieceStyle() {
|
||||
if (!this.sliderData) return {}
|
||||
return {
|
||||
width: this.sliderData.pieceSize / this.sliderData.width * 100 + '%',
|
||||
left: this.sliderOffset / this.sliderData.width * 100 + '%',
|
||||
top: this.sliderData.pieceY / this.sliderData.height * 100 + '%'
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getCode() {
|
||||
axios.post("/platform/login/captcha").then((resp) => {
|
||||
if (resp.data.code === 0) {
|
||||
this.codeUrl = resp.data.data.codeUrl
|
||||
this.loginForm.platformKey = resp.data.data.key
|
||||
}
|
||||
switchLoginMode(mode) {
|
||||
if (mode === 'sms' && !this.smsLoginVisible) return
|
||||
if (this.formLoading || this.smsSending || this.captchaVisible || this.loginMode === mode) return
|
||||
this.$set(this, 'loginMode', mode)
|
||||
this.$nextTick(() => {
|
||||
this.$refs.loginForm.clearValidate()
|
||||
this.$refs.smsForm.clearValidate()
|
||||
})
|
||||
},
|
||||
handleMobileChange(mobile) {
|
||||
if (mobile !== this.smsForm.mobile) {
|
||||
this.$set(this.smsForm, 'mobile', mobile)
|
||||
// 短信验证码只适用于发送时的号码;改号后清空输入,保留发送倒计时。
|
||||
this.$set(this.smsForm, 'smsCode', '')
|
||||
}
|
||||
},
|
||||
// Element 按字段回调校验结果,所有字段完成后返回是否允许提交。
|
||||
validateSmsFields(fields) {
|
||||
return new Promise((resolve) => {
|
||||
let remaining = fields.length
|
||||
let valid = true
|
||||
this.$refs.smsForm.validateField(fields, (message) => {
|
||||
valid = valid && !message
|
||||
remaining -= 1
|
||||
if (remaining === 0) resolve(valid)
|
||||
})
|
||||
})
|
||||
},
|
||||
handleLogin() {
|
||||
this.$refs.loginForm.validate(async (valid) => {
|
||||
if (valid) {
|
||||
await this.ensurePublicKey()
|
||||
const { publicKey, keyId } = this.rasParams
|
||||
if (!publicKey || !keyId) {
|
||||
this.$message({
|
||||
message: "Login key unavailable, please refresh and try again",
|
||||
type: "error"
|
||||
})
|
||||
return
|
||||
}
|
||||
// 加密密码
|
||||
const encrypt = new JSEncrypt()
|
||||
encrypt.setPublicKey(publicKey)
|
||||
const encryptedPassword = encrypt.encrypt(this.loginForm.password)
|
||||
axios
|
||||
.post("/platform/login/doLogin", null, {
|
||||
params: {
|
||||
...this.loginForm,
|
||||
//password: window.btoa(this.loginForm.password)
|
||||
password: encryptedPassword,
|
||||
keyId: keyId
|
||||
}
|
||||
})
|
||||
.then((resp) => {
|
||||
if (resp.data.code === 0) {
|
||||
this.$message.success("登录成功")
|
||||
const redirect = new URLSearchParams(window.location.search).get("redirect")
|
||||
if (redirect) {
|
||||
window.location.href = "${base!}" + redirect || ""
|
||||
} else {
|
||||
window.location.href = "${base!}/platform/home"
|
||||
}
|
||||
} else {
|
||||
this.getCode()
|
||||
this.$message({
|
||||
message: resp.data.msg,
|
||||
type: "error"
|
||||
})
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error)
|
||||
})
|
||||
} else {
|
||||
console.log("cccccc")
|
||||
}
|
||||
if (this.formLoading || this.smsSending || this.captchaVisible || this.loginMode !== 'password') return
|
||||
this.$refs.loginForm.validate((valid) => {
|
||||
if (valid) this.openSlider('password')
|
||||
})
|
||||
},
|
||||
// 获取公钥
|
||||
sendSmsCode() {
|
||||
if (this.smsSending || this.formLoading || this.captchaVisible || this.smsCountdown > 0) return
|
||||
this.validateSmsFields(['mobile']).then((valid) => {
|
||||
if (valid && !this.captchaVisible && !this.formLoading && !this.smsSending && this.smsCountdown === 0) this.openSlider('sms')
|
||||
})
|
||||
},
|
||||
// 固定本次验证的用途及输入,避免异步返回时把凭证用于另一个账号或号码。
|
||||
openSlider(scene) {
|
||||
this.$set(this, 'sliderContext', {
|
||||
scene: scene,
|
||||
subject: scene === 'password' ? this.loginForm.username : this.smsForm.mobile,
|
||||
password: scene === 'password' ? this.loginForm.password : ''
|
||||
})
|
||||
this.$set(this, 'captchaVisible', true)
|
||||
window.h5PopupHistory.open(this.popupToken, 'login-slider')
|
||||
this.loadSlider()
|
||||
},
|
||||
// scene/subject 绑定操作;返回图片、挑战编号和原图尺寸,不包含正确横坐标。
|
||||
loadSlider() {
|
||||
if (!this.captchaVisible || !this.sliderContext || this.captchaLoading || this.captchaChecking) return
|
||||
clearTimeout(this.sliderTimer)
|
||||
this.$set(this, 'sliderVersion', this.sliderVersion + 1)
|
||||
const version = this.sliderVersion
|
||||
this.$set(this, 'sliderData', null)
|
||||
this.$set(this, 'sliderOffset', 0)
|
||||
this.$set(this, 'sliderImages', { background: false, piece: false })
|
||||
this.$set(this, 'captchaLoading', true)
|
||||
this.$set(this, 'captchaHint', '图片加载中…')
|
||||
return this.$axios.post('${base!}/platform/login/sliderCaptcha', null, {
|
||||
params: { scene: this.sliderContext.scene, subject: this.sliderContext.subject }
|
||||
}).then((resp) => {
|
||||
if (version !== this.sliderVersion || !this.captchaVisible) return
|
||||
if (resp.data.code !== 0 || !resp.data.data) throw new Error(resp.data.msg || '滑块加载失败')
|
||||
this.$set(this, 'sliderData', resp.data.data)
|
||||
this.$set(this, 'captchaHint', '拖动下方滑块,将拼图移到缺口处')
|
||||
this.$set(this, 'sliderTimer', setTimeout(() => {
|
||||
if (version !== this.sliderVersion) return
|
||||
this.$set(this, 'sliderData', null)
|
||||
this.$set(this, 'captchaHint', '拼图已过期,请点击“换一张”重新验证')
|
||||
}, Math.max(1, Number(resp.data.data.expiresIn) || 180) * 1000))
|
||||
}).catch((error) => {
|
||||
if (version !== this.sliderVersion || !this.captchaVisible) return
|
||||
this.$set(this, 'captchaHint', error.message || '加载失败,请点击“换一张”重试')
|
||||
}).finally(() => {
|
||||
if (version === this.sliderVersion) this.$set(this, 'captchaLoading', false)
|
||||
})
|
||||
},
|
||||
onSliderImageLoad(kind, event) {
|
||||
// 旧图片延迟触发 load 时不能把新一轮挑战提前标记为可拖动。
|
||||
if (this.sliderData && event.target.getAttribute('src') === this.sliderData[kind]) {
|
||||
this.$set(this.sliderImages, kind, true)
|
||||
}
|
||||
},
|
||||
onSliderImageError() {
|
||||
this.$set(this, 'sliderImages', { background: false, piece: false })
|
||||
this.$set(this, 'captchaHint', '图片加载失败,请点击“换一张”重试')
|
||||
},
|
||||
onSliderInput(event) {
|
||||
if (!this.sliderReady || this.captchaChecking) return
|
||||
this.$set(this, 'sliderOffset', Math.max(0, Math.min(this.sliderMax, Number(event.target.value))))
|
||||
},
|
||||
// offset 使用原图像素;服务器返回一次性凭证,关闭弹框历史后再提交登录或发送短信。
|
||||
checkSlider() {
|
||||
if (!this.sliderReady || this.captchaChecking || !this.captchaVisible) return
|
||||
const version = this.sliderVersion
|
||||
const context = Object.assign({}, this.sliderContext)
|
||||
this.$set(this, 'captchaChecking', true)
|
||||
return this.$axios.post('${base!}/platform/login/checkSliderCaptcha', null, {
|
||||
params: { challengeId: this.sliderData.challengeId, offset: this.sliderOffset }
|
||||
}).then((resp) => {
|
||||
if (version !== this.sliderVersion || !this.captchaVisible) return
|
||||
if (resp.data.code !== 0 || !resp.data.data) throw new Error(resp.data.msg || '验证失败,请重试')
|
||||
const ticket = resp.data.data
|
||||
if (context.scene === 'password' && (context.subject !== this.loginForm.username || context.password !== this.loginForm.password)) {
|
||||
throw new Error('账号信息已变化,请重新验证')
|
||||
}
|
||||
if (context.scene === 'sms' && context.subject !== this.smsForm.mobile) throw new Error('手机号已变化,请重新验证')
|
||||
const loadingField = context.scene === 'password' ? 'formLoading' : 'smsSending'
|
||||
this.$set(this, loadingField, true)
|
||||
// clear 会同步触发关闭回调,因此使用局部快照保留本次请求参数。
|
||||
return window.h5PopupHistory.clear(this.popupToken).then(() => {
|
||||
return context.scene === 'password' ? this.submitPassword(context, ticket) : this.submitSms(context, ticket)
|
||||
}).finally(() => {
|
||||
this.$set(this, loadingField, false)
|
||||
})
|
||||
}).catch((error) => {
|
||||
if (version !== this.sliderVersion || !this.captchaVisible) return
|
||||
this.$set(this, 'sliderData', null)
|
||||
this.$set(this, 'captchaHint', (error.message || '验证失败') + ',请点击“换一张”')
|
||||
}).finally(() => {
|
||||
if (version === this.sliderVersion) this.$set(this, 'captchaChecking', false)
|
||||
})
|
||||
},
|
||||
closeCaptcha() {
|
||||
// 关闭按钮、ESC 与系统返回共用全局历史管理器,不单独注册 popstate。
|
||||
if (!window.h5PopupHistory.close(this.popupToken, 'login-slider')) this.resetSlider()
|
||||
},
|
||||
resetSlider() {
|
||||
clearTimeout(this.sliderTimer)
|
||||
this.$set(this, 'sliderVersion', this.sliderVersion + 1)
|
||||
this.$set(this, 'captchaVisible', false)
|
||||
this.$set(this, 'captchaLoading', false)
|
||||
this.$set(this, 'captchaChecking', false)
|
||||
this.$set(this, 'sliderData', null)
|
||||
this.$set(this, 'sliderContext', null)
|
||||
this.$set(this, 'sliderOffset', 0)
|
||||
},
|
||||
// 沿用 RSA 公钥加密;只提交服务器签发的滑块凭证,不提交滑块横坐标作为登录凭证。
|
||||
submitPassword(context, ticket) {
|
||||
return this.ensurePublicKey().then(() => {
|
||||
const encrypt = new JSEncrypt()
|
||||
encrypt.setPublicKey(this.rasParams.publicKey)
|
||||
const encryptedPassword = encrypt.encrypt(context.password)
|
||||
if (!encryptedPassword) throw new Error('密码加密失败,请刷新页面后重试')
|
||||
return this.$axios.post('${base!}/platform/login/doLogin', null, {
|
||||
params: { username: context.subject, password: encryptedPassword, keyId: this.rasParams.keyId,
|
||||
platformKey: ticket.platformKey, platformCaptcha: ticket.platformCaptcha, grantType: 'captcha' }
|
||||
})
|
||||
}).then((resp) => {
|
||||
this.handleLoginResult(resp.data)
|
||||
}).catch((error) => {
|
||||
this.$message.error(error.message || '登录失败,请重新验证后重试')
|
||||
})
|
||||
},
|
||||
// mobile 为绑定号码,两个 platform 参数为滑块凭证;返回重发等待时间及短信有效时间。
|
||||
submitSms(context, ticket) {
|
||||
return this.$axios.post('${base!}/platform/login/mobileCaptcha', null, {
|
||||
params: { mobile: context.subject, platformKey: ticket.platformKey, platformCaptcha: ticket.platformCaptcha }
|
||||
}).then((resp) => {
|
||||
if (resp.data.code !== 0) throw new Error(resp.data.msg || '短信验证码发送失败')
|
||||
this.$set(this.smsForm, 'smsCode', '')
|
||||
// 优先使用后端重发等待秒数,缺失时按五分钟倒计时。
|
||||
this.startSmsCountdown(Number((resp.data.data || {}).retryAfter) || 300)
|
||||
this.$message.success('验证码已发送,5分钟内有效')
|
||||
}).catch((error) => {
|
||||
this.$message.error(error.message || '短信验证码发送失败,请重新验证后重试')
|
||||
})
|
||||
},
|
||||
startSmsCountdown(seconds) {
|
||||
if (this.smsTimer) {
|
||||
clearInterval(this.smsTimer)
|
||||
}
|
||||
const retryAt = Date.now() + Math.max(1, seconds) * 1000
|
||||
this.$set(this, "smsCountdown", Math.ceil(seconds))
|
||||
this.$set(this, "smsTimer", setInterval(() => {
|
||||
this.$set(this, "smsCountdown", Math.max(0, Math.ceil((retryAt - Date.now()) / 1000)))
|
||||
if (this.smsCountdown === 0) {
|
||||
clearInterval(this.smsTimer)
|
||||
this.$set(this, "smsTimer", null)
|
||||
}
|
||||
}, 1000))
|
||||
},
|
||||
handleSmsLogin() {
|
||||
if (this.formLoading || this.smsSending || this.captchaVisible || this.loginMode !== "sms") {
|
||||
return
|
||||
}
|
||||
// mobile 为当前手机号,smsCode 为6位短信码;接口返回标准 Result,code=0 时按原路径进入系统。
|
||||
this.$set(this, "formLoading", true)
|
||||
this.validateSmsFields(["mobile", "smsCode"])
|
||||
.then((valid) => {
|
||||
if (!valid) {
|
||||
return
|
||||
}
|
||||
return this.$axios.post("${base!}/platform/login/doSmsLogin", null, {
|
||||
params: { mobile: this.smsForm.mobile, smsCode: this.smsForm.smsCode }
|
||||
}).then((resp) => {
|
||||
this.handleLoginResult(resp.data)
|
||||
})
|
||||
})
|
||||
.catch((error) => {
|
||||
this.$message.error(error.message || "登录失败,请稍后重试")
|
||||
})
|
||||
.finally(() => {
|
||||
this.$set(this, "formLoading", false)
|
||||
})
|
||||
},
|
||||
// 两种登录方式共用原有成功提示与登录后跳转,保持 redirect 参数行为。
|
||||
handleLoginResult(result) {
|
||||
if (result.code !== 0) {
|
||||
throw new Error(result.msg || "登录失败,请稍后重试")
|
||||
}
|
||||
this.$message.success("登录成功")
|
||||
const redirect = new URLSearchParams(window.location.search).get("redirect")
|
||||
window.h5PopupHistory.clear(this.popupToken).then(() => {
|
||||
window.location.href = redirect ? "${base!}" + redirect : "${base!}/platform/home"
|
||||
})
|
||||
},
|
||||
// 公钥接口沿用原 GET 方式;返回 publicKey、keyId、expireAt 供密码登录加密和过期判断。
|
||||
fetchPublicKey() {
|
||||
return axios.get("/platform/login/publicKey").then(res => {
|
||||
this.rasParams = res.data.data
|
||||
return this.$axios.get("${base!}/platform/login/publicKey").then((res) => {
|
||||
if (res.data.code !== 0 || !res.data.data || !res.data.data.publicKey || !res.data.data.keyId) {
|
||||
throw new Error(res.data.msg || "登录公钥获取失败,请稍后重试")
|
||||
}
|
||||
this.$set(this, "rasParams", res.data.data)
|
||||
})
|
||||
},
|
||||
ensurePublicKey() {
|
||||
@@ -226,8 +466,18 @@
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.fetchPublicKey()
|
||||
this.getCode()
|
||||
this.$set(this, 'popupToken', window.h5PopupHistory.register(() => this.resetSlider()))
|
||||
this.$set(this, 'pageShowHandler', (event) => {
|
||||
// 从浏览器往返缓存恢复时丢弃旧挑战,避免显示已过期凭证或遗留弹框。
|
||||
if (event.persisted) window.h5PopupHistory.clear(this.popupToken).then(() => this.resetSlider())
|
||||
})
|
||||
window.addEventListener('pageshow', this.pageShowHandler)
|
||||
},
|
||||
beforeDestroy() {
|
||||
clearInterval(this.smsTimer)
|
||||
clearTimeout(this.sliderTimer)
|
||||
window.removeEventListener('pageshow', this.pageShowHandler)
|
||||
window.h5PopupHistory.clear(this.popupToken).then(() => window.h5PopupHistory.unregister(this.popupToken))
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -260,6 +510,8 @@
|
||||
}
|
||||
|
||||
.login-container {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
display: grid;
|
||||
@@ -288,6 +540,121 @@
|
||||
width: 500px;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.login-tabs {
|
||||
display: flex;
|
||||
gap: 32px;
|
||||
justify-content: center;
|
||||
margin-bottom: 28px;
|
||||
border-bottom: 1px solid #edf0f5;
|
||||
}
|
||||
|
||||
.login-tabs button {
|
||||
padding: 0 0 14px;
|
||||
border: 0;
|
||||
border-bottom: 3px solid transparent;
|
||||
background: transparent;
|
||||
color: #7c8798;
|
||||
font: inherit;
|
||||
font-size: 17px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.login-tabs button.active {
|
||||
border-bottom-color: #1867b0;
|
||||
color: #1867b0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.login-tabs button:disabled {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.captcha-dialog {
|
||||
width: 400px;
|
||||
max-width: calc(100vw - 32px);
|
||||
border-radius: 6px;
|
||||
}
|
||||
.captcha-dialog .el-dialog__body {
|
||||
padding: 10px 20px 20px;
|
||||
}
|
||||
.captcha-panel {
|
||||
padding: 12px;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 6px;
|
||||
background: #f7faff;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.captcha-heading {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
color: #606266;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.captcha-picture {
|
||||
position: relative;
|
||||
padding-top: 50%;
|
||||
overflow: hidden;
|
||||
border-radius: 4px;
|
||||
background: #e9eef5;
|
||||
}
|
||||
.captcha-background {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.captcha-piece {
|
||||
position: absolute;
|
||||
filter: drop-shadow(0 2px 3px rgba(0, 0, 0, .5));
|
||||
}
|
||||
.captcha-picture img {
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
.captcha-range {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
margin: 12px 0 0;
|
||||
border-radius: 5px;
|
||||
background: #e4eaf2;
|
||||
cursor: ew-resize;
|
||||
touch-action: pan-y;
|
||||
}
|
||||
.captcha-range::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
width: 42px;
|
||||
height: 40px;
|
||||
border-radius: 5px;
|
||||
border: 2px solid #fff;
|
||||
background: #409eff;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, .2);
|
||||
}
|
||||
.captcha-range::-moz-range-thumb {
|
||||
width: 38px;
|
||||
height: 36px;
|
||||
border-radius: 5px;
|
||||
border: 2px solid #fff;
|
||||
background: #409eff;
|
||||
}
|
||||
.captcha-range:focus-visible { outline: 2px solid #409eff; outline-offset: 3px; }
|
||||
.captcha-range:disabled { opacity: .55; cursor: wait; }
|
||||
.captcha-hint { margin-top: 6px; color: #909399; font-size: 12px; text-align: center; }
|
||||
|
||||
.login-tip {
|
||||
margin: -4px 0 0;
|
||||
color: #8a94a4;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.font-semibold {
|
||||
font-weight: 600;
|
||||
}
|
||||
@@ -314,21 +681,6 @@
|
||||
border-radius: 1.5rem;
|
||||
}
|
||||
|
||||
.login-code {
|
||||
display: inline-block;
|
||||
float: right;
|
||||
width: 38%;
|
||||
height: 38px;
|
||||
}
|
||||
|
||||
.login-code img {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
object-fit: contain;
|
||||
vertical-align: middle;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.login-bg-img {
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
@@ -344,7 +696,32 @@
|
||||
}
|
||||
|
||||
.login-container .login-box .login-form {
|
||||
width: 100vw !important;
|
||||
width: 100% !important;
|
||||
padding: 24px 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
padding: 30px 22px;
|
||||
}
|
||||
|
||||
.text-6xl {
|
||||
font-size: 24px;
|
||||
line-height: 1.5;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.login-tabs {
|
||||
gap: 26px;
|
||||
}
|
||||
|
||||
.login-tabs button {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
|
||||
.el-input-group__append {
|
||||
padding: 0 14px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const branchUnionManage = {
|
||||
template: `
|
||||
<div>
|
||||
<el-card shadow="never" style="border: 1px solid var(--border-color-lighter); margin-top: 10px">
|
||||
<div class="branch-union-list">
|
||||
<el-card class="branch-union-search-card" shadow="never" style="border: 1px solid var(--border-color-lighter); margin-top: 10px">
|
||||
<el-row :gutter="10" type="flex">
|
||||
<el-input placeholder="请输入分工会名称" clearable size="small" style="width: 300px" v-model="pageForm.searchKeyword"></el-input>
|
||||
|
||||
@@ -10,8 +10,9 @@ const branchUnionManage = {
|
||||
<el-button @click="openAdd" icon="ti-plus" type="primary" size="small" v-if="$auth.hasPermission('sys.manager.union.add')">新建分工会</el-button>
|
||||
</el-row>
|
||||
</el-card>
|
||||
<el-card shadow="never" style="border: 1px solid var(--border-color-lighter); margin-top: 10px">
|
||||
<el-table :data="tableData" border>
|
||||
<el-card class="branch-union-table-card" shadow="never" style="border: 1px solid var(--border-color-lighter); margin-top: 10px">
|
||||
<div class="branch-union-table-wrapper">
|
||||
<el-table :data="tableData" border height="100%">
|
||||
<el-table-column label="序号" type="index" width="50" :index="indexMethod"></el-table-column>
|
||||
<el-table-column prop="name" label="工会名称" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column prop="unionCode" label="工会编码"></el-table-column>
|
||||
@@ -25,6 +26,7 @@ const branchUnionManage = {
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<el-row class="el-pagination-container mt20">
|
||||
<el-pagination
|
||||
@size-change="pageSizeChange"
|
||||
|
||||
@@ -2,10 +2,59 @@
|
||||
layout("/layouts/platform.html"){
|
||||
#-->
|
||||
|
||||
<style>
|
||||
/* 分工会列表占满可用高度,分页固定在底部,数据在表格内滚动。 */
|
||||
.union-branch-list-panel,
|
||||
.union-branch-list-panel > .el-card__body,
|
||||
.union-branch-list-panel .el-tabs,
|
||||
.union-branch-list-panel .el-tabs__content,
|
||||
.union-branch-list-panel .el-tab-pane {
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.union-branch-list-panel .el-tabs {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.union-branch-list-panel .el-tabs__header {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.union-branch-list-panel .el-tabs__content {
|
||||
flex: 1;
|
||||
}
|
||||
.union-branch-list-panel .branch-union-list {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
.union-branch-list-panel .branch-union-search-card {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.union-branch-list-panel .branch-union-table-card {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
.union-branch-list-panel .branch-union-table-card > .el-card__body {
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.union-branch-list-panel .branch-union-table-wrapper {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
.union-branch-list-panel .el-pagination-container {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="app" v-cloak>
|
||||
<guava ref="guava">
|
||||
<template>
|
||||
<el-row type="flex" :gutter="20" style="height: calc(100vh - 126px)">
|
||||
<el-row type="flex" :gutter="20" style="height: calc(100vh - 94px)">
|
||||
<el-col :span="5">
|
||||
<el-card shadow="never" style="height: 100%" body-style="{ height: '100%' }">
|
||||
<el-input placeholder="输入关键字进行查找" v-model="filterText" clearable></el-input>
|
||||
@@ -35,7 +84,7 @@ layout("/layouts/platform.html"){
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="19">
|
||||
<el-card shadow="never">
|
||||
<el-card shadow="never" :class="{'union-branch-list-panel': (!currentTreeNode || currentTreeNode.level===1) && schoolUnionTabActive==='branchUnionManage'}">
|
||||
<el-tabs v-model="schoolUnionTabActive" type="card" v-if="currentTreeNode==null || currentTreeNode.level===1">
|
||||
<el-tab-pane name="branchUnionManage">
|
||||
<span slot="label">
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<!--#include('/platform/zhgh/activity/trainSignUp/manage/courseTime.js'){}#-->
|
||||
<!--#include('/platform/zhgh/activity/trainSignUp/manage/customForm.js'){}#-->
|
||||
<!--#include('/platform/zhgh/activity/trainSignUp/manage/responsibleForm.js'){}#-->
|
||||
const basicForm = {
|
||||
template: /*language=HTML*/ `
|
||||
<div>
|
||||
<el-form :model="formData" label-width="110px" ref="form">
|
||||
<el-form :model="formData" :disabled="formLoading" label-width="110px" ref="form">
|
||||
<div v-show="step === 1">
|
||||
<el-row :gutter="20" type="flex" v-if="!formData.id">
|
||||
<el-col :span="12">
|
||||
@@ -235,13 +236,11 @@ const basicForm = {
|
||||
|
||||
<el-table-column :label="trainType + '负责人'" prop="courseInstructor">
|
||||
<template v-slot="{row}">
|
||||
<el-input size="small" v-model="row.courseInstructor" :placeholder="'请输入' + trainType + '负责人'"></el-input>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="手机号" prop="courseInstructorMobile">
|
||||
<template v-slot="{row}">
|
||||
<el-input size="small" v-model="row.courseInstructorMobile" placeholder="请输入手机号"></el-input>
|
||||
<el-button @click="openSetUpResponsible(row)" :disabled="formLoading" type="text"
|
||||
:title="getResponsibleLabel(row)"
|
||||
style="max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap">
|
||||
{{ getResponsibleLabel(row) }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
@@ -290,11 +289,11 @@ const basicForm = {
|
||||
</div>
|
||||
</el-form>
|
||||
<div style="float: right; margin: 20px 0">
|
||||
<el-button @click="$emit('back')">取消</el-button>
|
||||
<el-button v-if="step === 2" type="primary" @click="step = 1">上一步</el-button>
|
||||
<el-button v-if="step === 1" type="primary" @click="step = 2">下一步</el-button>
|
||||
<el-button type="primary" @click="onSave">{{ copyTemplateMode ? "另存为" : "保存" }}</el-button>
|
||||
<el-button type="primary" @click="onSubmit">{{ copyTemplateMode ? "新活动提交" : "提交" }}</el-button>
|
||||
<el-button :disabled="formLoading" @click="$emit('back')">取消</el-button>
|
||||
<el-button v-if="step === 2" :disabled="formLoading" type="primary" @click="step = 1">上一步</el-button>
|
||||
<el-button v-if="step === 1" :disabled="formLoading" type="primary" @click="step = 2">下一步</el-button>
|
||||
<el-button :loading="formLoading" type="primary" @click="onSave">{{ copyTemplateMode ? "另存为" : "保存" }}</el-button>
|
||||
<el-button :loading="formLoading" type="primary" @click="onSubmit">{{ copyTemplateMode ? "新活动提交" : "提交" }}</el-button>
|
||||
</div>
|
||||
|
||||
<el-dialog :close-on-click-modal="false" :visible.sync="signUpDialog" title="设置报名限制" append-to-body>
|
||||
@@ -322,6 +321,7 @@ const basicForm = {
|
||||
|
||||
<course-time ref="courseTimeRef"></course-time>
|
||||
<custom-form ref="customFormRef"></custom-form>
|
||||
<responsible-form ref="responsibleFormRef"></responsible-form>
|
||||
</div>
|
||||
`,
|
||||
dicts: ["USER_CAMPUS", "TRAIN_SIGNUP_TYPE"],
|
||||
@@ -329,12 +329,13 @@ const basicForm = {
|
||||
return {
|
||||
signUpDialog: false,
|
||||
step: 1,
|
||||
formLoading: false,
|
||||
copyTemplateMode: false,
|
||||
copyTemplateName: "",
|
||||
formData: {
|
||||
notice: false,
|
||||
courseList: [
|
||||
{ orderNum: 1, courseName: "", courseInstructorMobile: "", courseTimeList: [], isMobileSign: false, isReceiveGift: false, reserveMode: 1, courseIsLimitApply: false },
|
||||
{ orderNum: 1, courseName: "", courseInstructorMobile: "", responsibleList: [], courseTimeList: [], isMobileSign: false, isReceiveGift: false, reserveMode: 1, courseIsLimitApply: false },
|
||||
],
|
||||
conditionStructure: {
|
||||
method: "AND",
|
||||
@@ -364,6 +365,7 @@ const basicForm = {
|
||||
"drawer-user-scope": httpVueLoader("/components/module/activity/DrawerUserScope.vue"),
|
||||
"course-time": courseTime,
|
||||
"custom-form": customForm,
|
||||
"responsible-form": responsibleForm,
|
||||
},
|
||||
methods: {
|
||||
async validNumber(row, old) {
|
||||
@@ -464,11 +466,23 @@ const basicForm = {
|
||||
openSetUpCourseTime(index) {
|
||||
this.$refs.courseTimeRef.onOpen(this.formData, index)
|
||||
},
|
||||
// 只显示新方式设置的负责人姓名;没有负责人关系时显示设置入口,不读取历史手填姓名。
|
||||
getResponsibleLabel(row) {
|
||||
if (Array.isArray(row.responsibleList)) {
|
||||
const names = row.responsibleList.map(item => (item.username || '').trim()).filter(name => name)
|
||||
return names.length ? names.join('、') : '设置负责人'
|
||||
}
|
||||
return '设置负责人'
|
||||
},
|
||||
// 和设置时间一样从当前子活动行打开弹框,未保存的新增子活动也可以设置负责人。
|
||||
openSetUpResponsible(row) {
|
||||
this.$refs.responsibleFormRef.onOpen(row)
|
||||
},
|
||||
courseOrderNumChange() {
|
||||
this.formData.courseList = this.formData.courseList.sort((a, b) => a.orderNum - b.orderNum)
|
||||
},
|
||||
addCourse() {
|
||||
this.formData.courseList.push({ courseInstructorMobile: "", courseTimeList: [], isMobileSign: false, isReceiveGift: false, reserveMode: 1, courseIsLimitApply: false })
|
||||
this.formData.courseList.push({ courseInstructorMobile: "", responsibleList: [], courseTimeList: [], isMobileSign: false, isReceiveGift: false, reserveMode: 1, courseIsLimitApply: false })
|
||||
},
|
||||
async historicalActChange(val) {
|
||||
const resp = await this.$axios.post("/platform/trainSignUp/manage/findOne", {id: val})
|
||||
@@ -510,6 +524,13 @@ const basicForm = {
|
||||
course.updatedBy = null
|
||||
course.updatedAt = null
|
||||
course.delFlag = null
|
||||
// 沿用负责人选择和联系方式,关系ID及所属活动由后端在新活动中重新生成。
|
||||
if (Array.isArray(course.responsibleList)) {
|
||||
this.$set(course, 'responsibleList', course.responsibleList.map(item => ({
|
||||
userId: item.userId, username: item.username, loginname: item.loginname,
|
||||
mobile: item.mobile, orderNum: item.orderNum
|
||||
})))
|
||||
}
|
||||
if (Array.isArray(course.courseTimeList)) {
|
||||
course.courseTimeList = course.courseTimeList.map((courseTime) => {
|
||||
courseTime.id = null
|
||||
@@ -543,7 +564,7 @@ const basicForm = {
|
||||
const resp = await this.$axios.post("/platform/trainSignUp/manage/getHistoricalActList", {})
|
||||
return resp.data
|
||||
},
|
||||
async onSave() {
|
||||
onSave() {
|
||||
let valid = false
|
||||
this.$refs.form.validateField("activityName", (errMsg) => {
|
||||
valid = errMsg === ""
|
||||
@@ -555,16 +576,19 @@ const basicForm = {
|
||||
if (!this.validateCopyTemplateName()) {
|
||||
return
|
||||
}
|
||||
await this.doHandle(this.copyTemplateMode ? '另存为' : '保存')
|
||||
this.doHandle(this.copyTemplateMode ? '另存为' : '保存')
|
||||
},
|
||||
onSubmit() {
|
||||
this.$refs["form"].validate(async (valid, errMsg) => {
|
||||
this.$refs["form"].validate((valid, errMsg) => {
|
||||
if (valid) {
|
||||
if (!this.validateCopyTemplateName()) {
|
||||
return
|
||||
}
|
||||
const courseValid = this.formData.courseList.some((v, i) => {
|
||||
const basicValid = v.courseName && v.coursePeopleNumber && v.courseLocation && v.courseInstructor && v.courseType
|
||||
// 新活动通过多选关系设置负责人;未重新设置的旧活动仍允许保留历史联系人。
|
||||
const hasResponsible = Array.isArray(v.responsibleList)
|
||||
? v.responsibleList.length > 0 : !!v.courseInstructor
|
||||
const basicValid = v.courseName && v.coursePeopleNumber && v.courseLocation && hasResponsible && v.courseType
|
||||
const timeValid =
|
||||
v.courseTimeList.length > 0 &&
|
||||
v.courseTimeList.every((x) => {
|
||||
@@ -578,8 +602,10 @@ const basicForm = {
|
||||
this.$message.warning("第" + (i + 1) + "行信息填写有误,请核查")
|
||||
return true
|
||||
}
|
||||
if (v.courseInstructorMobile && !/^1[3-9]\d{9}$/.test(v.courseInstructorMobile)) {
|
||||
this.$message.warning("第" + (i + 1) + "行手机号格式填写有误,请核查")
|
||||
// 联系方式允许手机号或办公电话,逐人校验,不能只检查兼容展示的第一位。
|
||||
if (Array.isArray(v.responsibleList) && v.responsibleList.some(item =>
|
||||
!item.userId || !item.mobile || !item.mobile.trim() || item.mobile.trim().length > 20)) {
|
||||
this.$message.warning("第" + (i + 1) + "行负责人联系方式未填写或超过20个字符,请核查")
|
||||
return true
|
||||
}
|
||||
if (v.isMobileSign === true && v.signType === 3 && (v.courseLocationCoordinates === undefined || v.courseLocationCoordinates.length < 2)) {
|
||||
@@ -598,7 +624,7 @@ const basicForm = {
|
||||
return
|
||||
}
|
||||
this.formData.isDisabled = false
|
||||
await this.doHandle(this.copyTemplateMode ? '新活动提交' : '提交')
|
||||
this.doHandle(this.copyTemplateMode ? '新活动提交' : '提交')
|
||||
} else {
|
||||
if(Object.keys(errMsg).length > 0) {
|
||||
this.$message.warning(errMsg[Object.keys(errMsg)[0]][0].message)
|
||||
@@ -620,7 +646,10 @@ const basicForm = {
|
||||
}
|
||||
return true
|
||||
},
|
||||
async doHandle(type) {
|
||||
doHandle(type) {
|
||||
if (this.formLoading) {
|
||||
return
|
||||
}
|
||||
const cloneData = clone(this.formData)
|
||||
if (this.copyTemplateMode) {
|
||||
this.cleanCopyTemplateData(cloneData)
|
||||
@@ -636,21 +665,32 @@ const basicForm = {
|
||||
cloneData.courseList = JSON.stringify(cloneData.courseList)
|
||||
cloneData.hostUnits = JSON.stringify(cloneData.hostUnits)
|
||||
cloneData.helpUnits = JSON.stringify(cloneData.helpUnits)
|
||||
const confirm = await this.$confirm("您确定要" + type + "吗?", "提示", {
|
||||
// courseList 以JSON提交,每个负责人仅需 userId、mobile;姓名及关系归属由后端确认。
|
||||
// 返回 Result,code=0 表示保存成功;finally 在失败或网络异常时也解除提交加载状态。
|
||||
return this.$confirm("您确定要" + type + "吗?", "提示", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning"
|
||||
})
|
||||
if (confirm !== "confirm") return
|
||||
const resp = await this.$axios.post("/platform/trainSignUp/manage/doHandle", cloneData)
|
||||
if (resp.code === 0) {
|
||||
this.$message.success(resp.msg)
|
||||
this.step = 1
|
||||
this.$emit('refresh')
|
||||
this.$emit('back')
|
||||
} else {
|
||||
this.$message.warning(resp.msg)
|
||||
}
|
||||
}).then(() => {
|
||||
if (this.formLoading) {
|
||||
return
|
||||
}
|
||||
this.$set(this, 'formLoading', true)
|
||||
return this.$axios.post("/platform/trainSignUp/manage/doHandle", cloneData)
|
||||
.then((resp) => {
|
||||
if (resp.code === 0) {
|
||||
this.$message.success(resp.msg)
|
||||
this.$set(this, 'step', 1)
|
||||
this.$emit('refresh')
|
||||
this.$emit('back')
|
||||
} else {
|
||||
this.$message.warning(resp.msg)
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
this.$set(this, 'formLoading', false)
|
||||
})
|
||||
}, () => {})
|
||||
},
|
||||
async getAllType() {
|
||||
const resp = await this.$axios.post("/platform/trainSignUp/type/getAllType", {})
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
<!--#include('courseManageForm.js'){}#-->
|
||||
const courseManage = {
|
||||
template: /*language=HTML*/ `
|
||||
<div>
|
||||
<el-card shadow="never">
|
||||
<div class="left-span-label">子活动管理</div>
|
||||
<div class="mb10">所属活动:{{ activity.activityName }}</div>
|
||||
<div class="text-info mb10">{{ canEditBasic ? '可管理当前活动的全部子活动' : '仅显示您负责的子活动' }}</div>
|
||||
<search @search="doSearch">
|
||||
<search-item label="子活动名称">
|
||||
<el-input :value="pageForm.courseName" clearable placeholder="请输入子活动名称"
|
||||
@input="(value) => { this.$set(pageForm, 'courseName', value) }"
|
||||
@keyup.enter.native="doSearch"></el-input>
|
||||
</search-item>
|
||||
</search>
|
||||
<el-table :data="tableData" :size="tableSize" v-loading="tableLoading"
|
||||
empty-text="暂无可管理的子活动,请检查负责人设置" class="mt20">
|
||||
<el-table-column :index="indexMethod" label="序号" type="index" width="70"></el-table-column>
|
||||
<el-table-column :label="trainType + '名称'" prop="courseName" min-width="160" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="类型" prop="courseTypeName" min-width="110" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="人数" prop="coursePeopleNumber" width="80"></el-table-column>
|
||||
<el-table-column label="预留名额" prop="courseReservedNumber" width="90"></el-table-column>
|
||||
<el-table-column label="校区" prop="campus" min-width="120" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="地点" prop="courseLocation" min-width="160" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="负责人" min-width="120" show-overflow-tooltip>
|
||||
<template v-slot="{row}">{{ row.responsibleNames || row.courseInstructor || '未设置' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="分类标识" prop="assort" width="100" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="活动时间" prop="courseTimeName" min-width="200" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="操作" width="100" fixed="right">
|
||||
<template v-slot="{row}">
|
||||
<el-button size="mini" type="primary" @click="openManage(row)">管理</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<!--#include('/layouts/pagination.html'){}#-->
|
||||
</el-card>
|
||||
<course-manage-form ref="manageFormRef" @refresh="pageData"></course-manage-form>
|
||||
<slot></slot>
|
||||
</div>
|
||||
`,
|
||||
mixins: [initTableMixins],
|
||||
dicts: ['TRAIN_SIGNUP_TYPE'],
|
||||
components: {'course-manage-form': courseManageForm},
|
||||
data() {
|
||||
return {
|
||||
activity: {},
|
||||
pageDataUrl: '/platform/trainSignUp/manage/courseManagePageData',
|
||||
pageForm: {activityId: '', courseName: ''},
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
canEditBasic() {
|
||||
return this.$auth.hasRoleOr(['SYSADMIN', 'SCHOOL_UNION_ADMIN', 'SCHOOL_UNION_ACTIVITY_ADMIN'])
|
||||
},
|
||||
trainType() {
|
||||
const type = (this.dict.type.TRAIN_SIGNUP_TYPE || []).find(item => item.code === this.activity.trainType)
|
||||
return type ? type.name : '子活动'
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
// row 为主活动列表记录;仅传主活动ID和名称查询条件,负责人范围由服务端按登录人确定。
|
||||
initData(row) {
|
||||
this.$set(this, 'activity', row)
|
||||
this.$set(this.pageForm, 'activityId', row.id)
|
||||
this.$set(this.pageForm, 'courseName', '')
|
||||
this.doSearch()
|
||||
},
|
||||
// 详情另行加载并校验权限,不能将列表数据直接作为可写表单提交。
|
||||
openManage(row) {
|
||||
this.$refs.manageFormRef.initData(this.activity.id, row.id, this.trainType)
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,286 @@
|
||||
// courseTime、responsibleForm 已由首页先 include 的 basicForm.js 引入,沿用原组件,避免重复声明。
|
||||
const courseManageForm = {
|
||||
template: /*language=HTML*/ `
|
||||
<div>
|
||||
<el-drawer :title="'子活动管理 — ' + (formData.courseName || '')" :visible.sync="drawerVisible"
|
||||
size="55%" custom-class="train-course-manage-drawer" append-to-body
|
||||
:wrapper-closable="false" :before-close="beforeClose">
|
||||
<div class="train-course-manage-body" v-loading="detailLoading">
|
||||
<el-alert :title="canEditBasic ? '当前为管理员模式,可修改活动信息和签到设置' : '当前为负责人模式,可修改签到设置;活动关键信息只读'"
|
||||
type="info" :closable="false" show-icon></el-alert>
|
||||
<div class="left-span-label mt20">基本信息
|
||||
<el-tag v-if="!canEditBasic" size="mini" type="info">只读</el-tag>
|
||||
</div>
|
||||
<el-form v-if="canEditBasic" :model="formData" :disabled="formLoading || detailLoading"
|
||||
label-width="110px" ref="basicFormRef">
|
||||
<el-form-item :label="trainType + '名称'" prop="courseName" :rules="{required: true, message: '请输入名称', trigger: 'blur'}">
|
||||
<el-input :value="formData.courseName" maxlength="50" @input="(value) => { this.$set(formData, 'courseName', value) }"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="类型" prop="courseType" :rules="{required: true, message: '请选择类型', trigger: 'change'}">
|
||||
<el-select :value="formData.courseType" @change="(value) => { this.$set(formData, 'courseType', value) }" style="width: 100%">
|
||||
<el-option v-for="item in courseTypes" :key="item.id" :label="item.typeName" :value="item.id"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12"><el-form-item label="人数">
|
||||
<el-input-number :value="formData.coursePeopleNumber" :min="0" :max="9999" :precision="0" :controls="false" style="width: 100%"
|
||||
@change="(value) => { this.$set(formData, 'coursePeopleNumber', value) }"></el-input-number>
|
||||
</el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="预留名额">
|
||||
<el-input-number :value="formData.courseReservedNumber" :min="0" :max="1000" :precision="0" :controls="false" style="width: 100%"
|
||||
@change="(value) => { this.$set(formData, 'courseReservedNumber', value) }"></el-input-number>
|
||||
</el-form-item></el-col>
|
||||
</el-row>
|
||||
<el-form-item label="校区" prop="campus" :rules="{required: true, message: '请选择校区', trigger: 'change'}">
|
||||
<el-select :value="formData.campus" @change="(value) => { this.$set(formData, 'campus', value) }" style="width: 100%">
|
||||
<el-option v-for="item in dict.type.USER_CAMPUS" :key="item.code" :label="item.name" :value="item.name"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="地点" prop="courseLocation" :rules="{required: true, message: '请输入地点', trigger: 'blur'}">
|
||||
<el-input :value="formData.courseLocation" maxlength="50" @input="(value) => { this.$set(formData, 'courseLocation', value) }"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="负责人"><el-button type="text" @click="openResponsible">{{ responsibleNames || '设置负责人' }}</el-button></el-form-item>
|
||||
<el-form-item label="分类标识"><el-input :value="formData.assort" maxlength="30" @input="(value) => { this.$set(formData, 'assort', value) }"></el-input></el-form-item>
|
||||
<el-form-item label="活动时间"><el-button type="text" @click="openTimes">设置活动时间</el-button></el-form-item>
|
||||
</el-form>
|
||||
<el-descriptions v-else :column="2" border class="table_fixed">
|
||||
<el-descriptions-item :label="trainType + '名称'">{{ formData.courseName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="类型">{{ formData.courseTypeName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="人数">{{ formData.coursePeopleNumber }}</el-descriptions-item>
|
||||
<el-descriptions-item label="预留名额">{{ formData.courseReservedNumber }}</el-descriptions-item>
|
||||
<el-descriptions-item label="校区">{{ formData.campus }}</el-descriptions-item>
|
||||
<el-descriptions-item label="地点">{{ formData.courseLocation }}</el-descriptions-item>
|
||||
<el-descriptions-item label="负责人">{{ responsibleNames || formData.courseInstructor || '未设置' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="分类标识">{{ formData.assort || '—' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="活动时间" :span="2"><el-button type="text" @click="openTimes">查看时段({{ (formData.courseTimeList || []).length }})</el-button></el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<el-divider></el-divider>
|
||||
<div class="left-span-label">签到设置</div>
|
||||
<el-form :model="formData" label-width="110px" :disabled="formLoading || detailLoading">
|
||||
<el-form-item label="是否签到">
|
||||
<el-radio-group :value="formData.isMobileSign" size="small" @input="(value) => { this.$set(formData, 'isMobileSign', value) }">
|
||||
<el-radio border :label="true">签到</el-radio><el-radio border :label="false">不签到</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<template v-if="formData.isMobileSign">
|
||||
<el-form-item label="签到方式">
|
||||
<el-radio-group :value="formData.signType" size="small" @input="(value) => { this.$set(formData, 'signType', value) }">
|
||||
<el-radio border :label="1">扫描二维码</el-radio><el-radio border :label="2">被扫</el-radio><el-radio border :label="3">GPS定位签到</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="usesSignLocation" label="签到时间误差">
|
||||
<el-input :value="formData.signUnit" type="number" min="0" step="1" placeholder="请输入签到时间误差"
|
||||
@input="(value) => { this.$set(formData, 'signUnit', value) }"><template slot="append">分钟</template></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="usesSignLocation" label="签到半径">
|
||||
<el-input :value="formData.radius" type="number" min="1" step="1" placeholder="请输入签到半径"
|
||||
@input="(value) => { this.$set(formData, 'radius', value) }"><template slot="append">米</template></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="usesSignLocation" label="地点坐标">
|
||||
<el-button type="primary" size="small" @click="openMap">设置签到点</el-button>
|
||||
<span :class="hasPosition ? 'text-success' : 'text-danger'" style="margin-left: 12px">{{ hasPosition ? '已设置' : '未设置' }}</span>
|
||||
</el-form-item>
|
||||
</template>
|
||||
</el-form>
|
||||
</div>
|
||||
<div class="train-course-manage-footer">
|
||||
<el-button :disabled="formLoading" @click="closeDrawer">取消</el-button>
|
||||
<el-button type="primary" :loading="formLoading" :disabled="detailLoading || !detailReady" @click="onSave">保存</el-button>
|
||||
</div>
|
||||
</el-drawer>
|
||||
<el-dialog title="位置信息" :visible.sync="mapVisible" :close-on-click-modal="false" append-to-body
|
||||
@opened="onMapOpened" @close="onMapClosed">
|
||||
<div style="min-height: 550px">
|
||||
<map-container v-if="mapVisible && mapOpened" :position="mapPosition" :radius="Number(formData.radius) || 100"
|
||||
@update:position="(value) => { this.$set(this, 'mapPosition', value) }"></map-container>
|
||||
</div>
|
||||
<span slot="footer"><el-button @click="closeMap">取消</el-button><el-button type="primary" @click="confirmMap">确定</el-button></span>
|
||||
</el-dialog>
|
||||
<el-dialog title="活动时段" :visible.sync="timesVisible" append-to-body width="60%">
|
||||
<el-table :data="formData.courseTimeList || []">
|
||||
<el-table-column label="日期" prop="courseDate"></el-table-column>
|
||||
<el-table-column label="开始时间" prop="courseStartTime"></el-table-column>
|
||||
<el-table-column label="结束时间" prop="courseEndTime"></el-table-column>
|
||||
</el-table>
|
||||
<span slot="footer"><el-button @click="closeTimes">关闭</el-button></span>
|
||||
</el-dialog>
|
||||
<course-time v-if="canEditBasic" ref="courseTimeRef"></course-time>
|
||||
<responsible-form v-if="canEditBasic" ref="responsibleRef"></responsible-form>
|
||||
<slot></slot>
|
||||
</div>
|
||||
`,
|
||||
dicts: ['USER_CAMPUS'],
|
||||
components: {
|
||||
'course-time': courseTime,
|
||||
'responsible-form': responsibleForm,
|
||||
'map-container': httpVueLoader('/components/plugins/mapContainer/TMap.vue?v=1.0.8'),
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
drawerVisible: false, detailLoading: false, detailReady: false, formLoading: false,
|
||||
canEditBasic: false, formData: {}, courseTypes: [], activityTime: [], trainType: '子活动',
|
||||
mapVisible: false, mapOpened: false, mapPosition: [], timesVisible: false, requestVersion: 0,
|
||||
originalTimes: '',
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
// 扫码和GPS均校验时间及位置;被扫使用固定时间规则,关闭签到时也不使用这些配置。
|
||||
usesSignLocation() {
|
||||
return this.formData.isMobileSign === true && [1, 3].includes(this.formData.signType)
|
||||
},
|
||||
responsibleNames() {
|
||||
return (this.formData.responsibleList || []).map(item => item.username).filter(item => item).join('、')
|
||||
},
|
||||
hasPosition() {
|
||||
return Array.isArray(this.formData.courseLocationCoordinates) && this.formData.courseLocationCoordinates.length === 2
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
// activityId/courseId 分别为主、子活动ID;data.course 为详情,canEditBasic 为服务端确认的字段权限。
|
||||
// 每次打开清空旧数据并使用请求序号,避免连续打开不同子活动后旧响应覆盖当前表单。
|
||||
initData(activityId, courseId, trainType) {
|
||||
const version = ++this.requestVersion
|
||||
this.$set(this, 'formData', {})
|
||||
this.$set(this, 'canEditBasic', false)
|
||||
this.$set(this, 'detailReady', false)
|
||||
this.$set(this, 'trainType', trainType)
|
||||
this.$set(this, 'drawerVisible', true)
|
||||
this.$set(this, 'detailLoading', true)
|
||||
return this.$axios.post('/platform/trainSignUp/manage/courseManageDetail', {activityId, courseId})
|
||||
.then((res) => {
|
||||
if (version !== this.requestVersion) return
|
||||
if (res.code !== 0) {
|
||||
this.$message.warning(res.msg)
|
||||
return
|
||||
}
|
||||
this.$set(this, 'formData', res.data.course)
|
||||
this.$set(this, 'canEditBasic', res.data.canEditBasic === true)
|
||||
this.$set(this, 'courseTypes', res.data.courseTypes || [])
|
||||
this.$set(this, 'activityTime', res.data.activityTime || [])
|
||||
this.$set(this, 'originalTimes', this.timeSnapshot())
|
||||
this.$set(this, 'detailReady', true)
|
||||
}).finally(() => {
|
||||
if (version === this.requestVersion) this.$set(this, 'detailLoading', false)
|
||||
})
|
||||
},
|
||||
// 仅实际更改时段或按时段限额时提交同步标志;普通签到保存不重建关联签到记录。
|
||||
timeSnapshot() {
|
||||
return JSON.stringify({times: this.formData.courseTimeList || [], limit: this.formData.courseIsLimitApply})
|
||||
},
|
||||
openResponsible() {
|
||||
if (this.canEditBasic && !this.formLoading) this.$refs.responsibleRef.onOpen(this.formData)
|
||||
},
|
||||
openTimes() {
|
||||
if (this.canEditBasic) {
|
||||
if (!this.formLoading) this.$refs.courseTimeRef.onOpen({activityTime: this.activityTime, courseList: [this.formData]}, 0)
|
||||
} else {
|
||||
this.$set(this, 'timesVisible', true)
|
||||
}
|
||||
},
|
||||
closeTimes() {
|
||||
this.$set(this, 'timesVisible', false)
|
||||
},
|
||||
// 地图使用副本,取消定位不改变原配置;确认后同时更新腾讯地图坐标及签到使用的WGS-84坐标。
|
||||
openMap() {
|
||||
this.$set(this, 'mapOpened', false)
|
||||
this.$set(this, 'mapPosition', clone(this.formData.courseLocationCoordinates || []))
|
||||
this.$set(this, 'mapVisible', true)
|
||||
},
|
||||
closeMap() {
|
||||
this.$set(this, 'mapVisible', false)
|
||||
},
|
||||
// 显示动画结束后才挂载地图,避免在隐藏容器中初始化;关闭时释放组件。
|
||||
onMapOpened() {
|
||||
this.$set(this, 'mapOpened', this.mapVisible)
|
||||
},
|
||||
onMapClosed() {
|
||||
this.$set(this, 'mapOpened', false)
|
||||
},
|
||||
confirmMap() {
|
||||
if (this.mapPosition.length !== 2) {
|
||||
this.$message.warning('请在地图上选择签到点')
|
||||
return
|
||||
}
|
||||
this.$set(this.formData, 'courseLocationCoordinates', clone(this.mapPosition))
|
||||
this.$set(this.formData, 'transPosition', this.$coordinateUtil.gcj02ToWgs84(this.mapPosition[1], this.mapPosition[0]))
|
||||
this.closeMap()
|
||||
},
|
||||
onSave() {
|
||||
if (this.formLoading || this.detailLoading || !this.detailReady) return
|
||||
if (this.canEditBasic) {
|
||||
this.$refs.basicFormRef.validate((valid) => { if (valid) this.saveCourse() })
|
||||
} else {
|
||||
this.saveCourse()
|
||||
}
|
||||
},
|
||||
// 普通负责人只提交签到白名单字段;管理员额外提交基本信息和实际修改后的负责人、时段。
|
||||
// 返回 Result,code=0 才关闭抽屉并刷新当前页;finally 在网络或业务失败时均释放加载状态。
|
||||
saveCourse() {
|
||||
if (this.formLoading) return
|
||||
if (this.formData.isMobileSign && ![1, 2, 3].includes(this.formData.signType)) {
|
||||
this.$message.warning('请选择签到方式')
|
||||
return
|
||||
}
|
||||
const params = {
|
||||
id: this.formData.id, activityId: this.formData.activityId,
|
||||
isMobileSign: this.formData.isMobileSign,
|
||||
}
|
||||
if (this.formData.isMobileSign) {
|
||||
params.signType = this.formData.signType
|
||||
}
|
||||
// 隐藏字段保留在表单中供切换回来回显,但不校验、不提交,服务端同样保留数据库原值。
|
||||
if (this.usesSignLocation) {
|
||||
const signUnit = this.formData.signUnit === '' || this.formData.signUnit == null ? 0 : Number(this.formData.signUnit)
|
||||
const radius = this.formData.radius === '' || this.formData.radius == null ? 100 : Number(this.formData.radius)
|
||||
if (!Number.isInteger(signUnit) || signUnit < 0 || signUnit > 2147483647 || !Number.isInteger(radius) || radius <= 0 || radius > 2147483647) {
|
||||
this.$message.warning('签到时间误差应为非负整数,签到半径应为正整数')
|
||||
return
|
||||
}
|
||||
if (!this.hasPosition) {
|
||||
this.$message.warning('请设置签到点')
|
||||
return
|
||||
}
|
||||
params.signUnit = signUnit
|
||||
params.radius = radius
|
||||
params.courseLocationCoordinates = JSON.stringify(this.formData.courseLocationCoordinates)
|
||||
params.transPosition = JSON.stringify(this.$coordinateUtil.gcj02ToWgs84(
|
||||
this.formData.courseLocationCoordinates[1], this.formData.courseLocationCoordinates[0]))
|
||||
}
|
||||
if (this.canEditBasic) {
|
||||
const fields = ['courseName', 'courseType', 'coursePeopleNumber', 'courseReservedNumber', 'courseLocation', 'campus', 'assort']
|
||||
fields.forEach(field => { params[field] = this.formData[field] })
|
||||
if (Array.isArray(this.formData.responsibleList)) params.responsibleList = JSON.stringify(this.formData.responsibleList)
|
||||
params.updateCourseTimes = this.originalTimes !== this.timeSnapshot()
|
||||
if (params.updateCourseTimes) {
|
||||
params.courseTimeList = JSON.stringify(this.formData.courseTimeList)
|
||||
params.courseIsLimitApply = this.formData.courseIsLimitApply
|
||||
}
|
||||
}
|
||||
this.$set(this, 'formLoading', true)
|
||||
return this.$axios.post('/platform/trainSignUp/manage/saveManagedCourse', params).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.$message.success('保存成功')
|
||||
this.$set(this, 'drawerVisible', false)
|
||||
this.$emit('refresh')
|
||||
} else {
|
||||
this.$message.warning(res.msg)
|
||||
}
|
||||
}).finally(() => {
|
||||
this.$set(this, 'formLoading', false)
|
||||
})
|
||||
},
|
||||
// 提交期间禁止退出;取消关闭仅丢弃本地副本,并使仍在途中的详情响应失效。
|
||||
beforeClose(done) {
|
||||
if (this.formLoading) return
|
||||
this.requestVersion += 1
|
||||
this.$set(this, 'detailLoading', false)
|
||||
done()
|
||||
},
|
||||
closeDrawer() {
|
||||
this.beforeClose(() => { this.$set(this, 'drawerVisible', false) })
|
||||
},
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.requestVersion += 1
|
||||
},
|
||||
}
|
||||
@@ -177,10 +177,14 @@ const customForm = {
|
||||
</div>
|
||||
</el-drawer>
|
||||
|
||||
<el-dialog :close-on-click-modal="false" :visible.sync="mapDialog" title="位置信息" :append-to-body="true">
|
||||
<map-container v-if="mapDialog"
|
||||
:radius="formData.courseList[moreInfoIndex].radius ? Number(formData.courseList[moreInfoIndex].radius) : 100"
|
||||
:position.sync="formData.courseList[mapIndex].courseLocationCoordinates"></map-container>
|
||||
<el-dialog :close-on-click-modal="false" :visible.sync="mapDialog" title="位置信息" :append-to-body="true"
|
||||
@opened="onMapOpened" @close="onMapClosed">
|
||||
<div style="min-height: 550px">
|
||||
<map-container v-if="mapDialog && mapOpened"
|
||||
:radius="formData.courseList[moreInfoIndex].radius ? Number(formData.courseList[moreInfoIndex].radius) : 100"
|
||||
:position="formData.courseList[mapIndex].courseLocationCoordinates"
|
||||
@update:position="(value) => { this.$set(formData.courseList[mapIndex], 'courseLocationCoordinates', value) }"></map-container>
|
||||
</div>
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button type="primary" @click="onMap">确 定</el-button>
|
||||
</span>
|
||||
@@ -199,18 +203,27 @@ const customForm = {
|
||||
moreInfoIndex: 0,
|
||||
|
||||
mapDialog: false,
|
||||
mapOpened: false,
|
||||
mapIndex: 0,
|
||||
}
|
||||
},
|
||||
components: {
|
||||
"union-form": unionForm,
|
||||
"map-container": httpVueLoader("/components/plugins/mapContainer/TMap.vue?v=1.0.6")
|
||||
"map-container": httpVueLoader("/components/plugins/mapContainer/TMap.vue?v=1.0.8")
|
||||
},
|
||||
methods: {
|
||||
openMap(index) {
|
||||
this.mapOpened = false
|
||||
this.mapIndex = index
|
||||
this.mapDialog = true
|
||||
},
|
||||
// 等弹窗动画结束、容器可见后挂载地图;关闭时卸载,重复打开创建新的实例。
|
||||
onMapOpened() {
|
||||
this.mapOpened = this.mapDialog
|
||||
},
|
||||
onMapClosed() {
|
||||
this.mapOpened = false
|
||||
},
|
||||
openSetUpUnionLimit(index) {
|
||||
this.$refs.unionFormRef.onOpen(this.formData, index)
|
||||
},
|
||||
|
||||
@@ -45,6 +45,33 @@ layout("/layouts/platform.html"){
|
||||
#app .train-sign-up-list-card .el-pagination-container {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
/* 子活动抽屉沿用平台表单组件,仅内容区滚动,底部操作区始终可见。 */
|
||||
.train-course-manage-drawer {
|
||||
min-width: 720px;
|
||||
max-width: 100vw;
|
||||
}
|
||||
.train-course-manage-drawer .el-drawer__body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.train-course-manage-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.train-course-manage-footer {
|
||||
flex-shrink: 0;
|
||||
padding: 16px 20px;
|
||||
border-top: 1px solid #ebeef5;
|
||||
text-align: right;
|
||||
}
|
||||
@media (max-width: 720px) {
|
||||
.train-course-manage-drawer { min-width: 100vw; }
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="app" v-cloak>
|
||||
@@ -102,6 +129,7 @@ layout("/layouts/platform.html"){
|
||||
>
|
||||
<template v-slot="{ row }" v-if="column.prop === 'isDisabled'">
|
||||
<el-switch
|
||||
:disabled="!canManageActivity"
|
||||
:active-value="false"
|
||||
:inactive-value="true"
|
||||
@change="(val) => { activityStatusChange(row.notice, val,row.id) }"
|
||||
@@ -130,14 +158,16 @@ layout("/layouts/platform.html"){
|
||||
<el-dropdown-item @click.native="openActivityCode(row.id)">活动二维码</el-dropdown-item>
|
||||
<el-dropdown-item @click.native="makeCode(row)">签到二维码</el-dropdown-item>
|
||||
<el-dropdown-item @click.native="onView(row)">查看</el-dropdown-item>
|
||||
<el-dropdown-item @click.native="openEdit(row)">编辑</el-dropdown-item>
|
||||
<!-- 校工会管理角色显示编辑;其余人员须为当前活动负责人,两个入口互斥。 -->
|
||||
<el-dropdown-item v-if="!canManageActivity && row.canManageCourse === true" @click.native="openCourseManage(row)">子活动管理</el-dropdown-item>
|
||||
<el-dropdown-item v-if="canManageActivity" @click.native="openEdit(row)">编辑</el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
v-if="canManageTemplate && row.templateStatus !== 1"
|
||||
@click.native="setTemplate(row)">设为模板</el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
v-if="canManageTemplate && row.templateStatus === 1"
|
||||
@click.native="cancelTemplate(row)">取消模板</el-dropdown-item>
|
||||
<el-dropdown-item :disabled="row.templateStatus === 1" @click.native="onDelete(row)">删除</el-dropdown-item>
|
||||
<el-dropdown-item v-if="canManageActivity" :disabled="row.templateStatus === 1" @click.native="onDelete(row)">删除</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
@@ -156,6 +186,10 @@ layout("/layouts/platform.html"){
|
||||
<info ref="infoRef"></info>
|
||||
</template>
|
||||
|
||||
<template #public>
|
||||
<course-manage ref="courseManageRef"></course-manage>
|
||||
</template>
|
||||
|
||||
<make-qrcode ref="codeRef"></make-qrcode>
|
||||
|
||||
<el-dialog
|
||||
@@ -177,6 +211,7 @@ layout("/layouts/platform.html"){
|
||||
<script nonce="${cspNonce!}">
|
||||
<!--#include('info.js'){}#-->
|
||||
<!--#include('basicForm.js'){}#-->
|
||||
<!--#include('courseManage.js'){}#-->
|
||||
<!--#include('makeQrcode.js'){}#-->
|
||||
new Vue({
|
||||
el: "#app",
|
||||
@@ -187,6 +222,7 @@ layout("/layouts/platform.html"){
|
||||
"info": info,
|
||||
"basic-form": basicForm,
|
||||
"make-qrcode": makeQrcode,
|
||||
"course-manage": courseManage,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -208,6 +244,10 @@ layout("/layouts/platform.html"){
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
// 与后端关键内容编辑角色保持一致,负责人通过子活动管理维护签到。
|
||||
canManageActivity() {
|
||||
return this.$auth.hasRoleOr(["SYSADMIN", "SCHOOL_UNION_ADMIN", "SCHOOL_UNION_ACTIVITY_ADMIN"])
|
||||
},
|
||||
// 页签沿用“设为模板”按钮的角色范围,不新增权限标识。
|
||||
canManageTemplate() {
|
||||
return this.$auth.hasRoleOr(["SYSADMIN", "SCHOOL_UNION_ADMIN"])
|
||||
@@ -218,7 +258,6 @@ layout("/layouts/platform.html"){
|
||||
const address = this.pageDataUrl ? this.pageDataUrl : loc() + "/pageData"
|
||||
this.tableLoading = true
|
||||
return this.$axios.post(address, data ? data : this.pageForm).then((res) => {
|
||||
this.tableLoading = false
|
||||
if (res.code === 0) {
|
||||
this.tableData = res.data.list
|
||||
this.pageForm.totalCount = res.data.totalCount
|
||||
@@ -227,6 +266,8 @@ layout("/layouts/platform.html"){
|
||||
})
|
||||
}
|
||||
return res
|
||||
}).finally(() => {
|
||||
this.tableLoading = false
|
||||
})
|
||||
},
|
||||
// 切换页签时按活动表中的模板状态查询,并重置到第一页。
|
||||
@@ -281,10 +322,20 @@ layout("/layouts/platform.html"){
|
||||
})
|
||||
},
|
||||
openEdit(row) {
|
||||
if (!this.canManageActivity) {
|
||||
this.$message.warning('活动关键信息仅管理员可修改,请使用子活动管理')
|
||||
return
|
||||
}
|
||||
this.$refs.guava.edit(() => {
|
||||
this.$refs.formRef.initData(row)
|
||||
})
|
||||
},
|
||||
// 使用 guava 的现有公共页面插槽,返回后保留主列表的筛选条件和页码。
|
||||
openCourseManage(row) {
|
||||
this.$refs.guava.public(() => {
|
||||
this.$refs.courseManageRef.initData(row)
|
||||
})
|
||||
},
|
||||
openUrlEdit() {
|
||||
if (this.urlEditInitialized) {
|
||||
return
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
const responsibleForm = {
|
||||
template: /*language=HTML*/ `
|
||||
<div>
|
||||
<el-dialog title="设置负责人" :visible.sync="dialogVisible" width="60%"
|
||||
:close-on-click-modal="false" append-to-body>
|
||||
<el-form label-width="90px">
|
||||
<el-form-item label="负责人">
|
||||
<el-select :value="selectedIds" multiple filterable remote reserve-keyword
|
||||
:remote-method="queryUsers" :loading="tableLoading" @change="responsibleChange"
|
||||
placeholder="请输入姓名或工号,多选负责人" style="width: 100%">
|
||||
<el-option v-for="item in userOptions" :key="item.id" :value="item.id"
|
||||
:label="item.username + (item.loginname ? '(' + item.loginname + ')' : '')"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-table :data="responsibleList">
|
||||
<el-table-column label="序号" type="index" width="70"></el-table-column>
|
||||
<el-table-column label="负责人" prop="username" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="工号" prop="loginname" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="联系方式">
|
||||
<template v-slot="{row}">
|
||||
<el-input :value="row.mobile" maxlength="20" placeholder="请输入手机号或办公电话"
|
||||
@input="(value) => { this.$set(row, 'mobile', value) }"></el-input>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="mt10 text-warning">确定后回填当前表单,保存或提交活动后生效。</div>
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button @click="closeDialog">取消</el-button>
|
||||
<el-button type="primary" @click="onConfirm">确定</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
</div>
|
||||
`,
|
||||
data() {
|
||||
return {
|
||||
dialogVisible: false,
|
||||
tableLoading: false,
|
||||
course: null,
|
||||
selectedIds: [],
|
||||
responsibleList: [],
|
||||
userOptions: [],
|
||||
queryVersion: 0,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 打开当前子活动的负责人副本,关闭或取消弹框不会更改业务表单。
|
||||
onOpen(course) {
|
||||
this.queryVersion += 1
|
||||
this.$set(this, 'course', course)
|
||||
const list = clone(course.responsibleList || [])
|
||||
this.$set(this, 'responsibleList', list)
|
||||
this.$set(this, 'selectedIds', list.map(item => item.userId))
|
||||
this.$set(this, 'userOptions', list.map(item => ({
|
||||
id: item.userId, username: item.username, loginname: item.loginname, mobile: item.mobile
|
||||
})))
|
||||
this.$set(this, 'tableLoading', false)
|
||||
this.$set(this, 'dialogVisible', true)
|
||||
},
|
||||
// query 为姓名或工号;返回 Result.data 用户数组,使用 id、username、loginname、mobile。
|
||||
// 合并已选项以保持多人回显,并忽略过期请求,避免快速搜索覆盖最新结果。
|
||||
queryUsers(query) {
|
||||
const version = ++this.queryVersion
|
||||
if (!query || !query.trim()) {
|
||||
this.$set(this, 'tableLoading', false)
|
||||
return
|
||||
}
|
||||
this.$set(this, 'tableLoading', true)
|
||||
return this.$axios.post('/open/common/userOptions', {query: query.trim()})
|
||||
.then((res) => {
|
||||
if (version !== this.queryVersion || !this.dialogVisible) {
|
||||
return
|
||||
}
|
||||
if (res.code !== 0) {
|
||||
this.$message.warning(res.msg)
|
||||
return
|
||||
}
|
||||
const selected = this.responsibleList.map(item => ({
|
||||
id: item.userId, username: item.username, loginname: item.loginname, mobile: item.mobile
|
||||
}))
|
||||
const options = new Map(selected.map(item => [item.id, item]))
|
||||
const results = res.data || []
|
||||
results.forEach(item => options.set(item.id, item))
|
||||
this.$set(this, 'userOptions', Array.from(options.values()))
|
||||
})
|
||||
.finally(() => {
|
||||
if (version === this.queryVersion) {
|
||||
this.$set(this, 'tableLoading', false)
|
||||
}
|
||||
})
|
||||
},
|
||||
// 多选结果一人一行,已选用户保留手工修改的联系方式,新选用户带出系统联系电话。
|
||||
responsibleChange(ids) {
|
||||
const oldRows = new Map(this.responsibleList.map(item => [item.userId, item]))
|
||||
const options = new Map(this.userOptions.map(item => [item.id, item]))
|
||||
const rows = ids.map((id) => {
|
||||
if (oldRows.has(id)) {
|
||||
return oldRows.get(id)
|
||||
}
|
||||
const user = options.get(id)
|
||||
return {userId: id, username: user.username, loginname: user.loginname, mobile: user.mobile || ''}
|
||||
})
|
||||
this.$set(this, 'selectedIds', ids)
|
||||
this.$set(this, 'responsibleList', rows)
|
||||
},
|
||||
// 联系方式按每个人校验;使用新数组回填,弹框副本不再与业务表单共用对象。
|
||||
onConfirm() {
|
||||
const missing = this.responsibleList.find(item => !item.mobile || !item.mobile.trim())
|
||||
if (missing) {
|
||||
this.$message.warning('请填写' + missing.username + '的联系方式')
|
||||
return
|
||||
}
|
||||
if (this.responsibleList.some(item => item.mobile.trim().length > 20)) {
|
||||
this.$message.warning('负责人联系方式不能超过20个字符')
|
||||
return
|
||||
}
|
||||
const list = this.responsibleList.map((item, index) => ({
|
||||
userId: item.userId, username: item.username, loginname: item.loginname,
|
||||
mobile: item.mobile.trim(), orderNum: index + 1
|
||||
}))
|
||||
this.$set(this.course, 'responsibleList', list)
|
||||
// 旧页面保留第一位联系人展示;完整多人数据通过 responsibleList 保存。
|
||||
this.$set(this.course, 'courseInstructor', list.length ? list[0].username : '')
|
||||
this.$set(this.course, 'courseInstructorMobile', list.length ? list[0].mobile : '')
|
||||
this.closeDialog()
|
||||
},
|
||||
closeDialog() {
|
||||
this.queryVersion += 1
|
||||
this.$set(this, 'dialogVisible', false)
|
||||
this.$set(this, 'tableLoading', false)
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -8,6 +8,12 @@ const REGISTER_INFO_COMPONENT = {
|
||||
<el-link type="primary" @click="openChart">点击查看流程图</el-link>
|
||||
</div>
|
||||
<el-descriptions :column="3" border class="table_fixed">
|
||||
<el-descriptions-item label="社团缩略图" :span="3">
|
||||
<el-image v-if="viewData.thumbnail" :src="viewData.thumbnail" :preview-src-list="[viewData.thumbnail]" fit="contain" style="width: 180px; height: 120px">
|
||||
<span slot="error">暂无图片</span>
|
||||
</el-image>
|
||||
<span v-else>暂无图片</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="社团名称">
|
||||
{{viewData.clubName}}
|
||||
</el-descriptions-item>
|
||||
|
||||
@@ -2,9 +2,19 @@
|
||||
layout("/layouts/platform.html"){
|
||||
#-->
|
||||
|
||||
<div id="app" v-cloak>
|
||||
<style>
|
||||
.club-join-apply-page .custom-card > .card-header { padding: 12px 24px; }
|
||||
.club-join-apply-page .club-apply-back-bar { display: flex; align-items: center; }
|
||||
.club-join-apply-page .club-apply-back-bar .el-button { padding: 0; font-size: 16px; }
|
||||
.club-join-apply-page .club-apply-title { margin-bottom: 20px; }
|
||||
</style>
|
||||
|
||||
<div id="app" v-cloak class="club-join-apply-page">
|
||||
<custom-card>
|
||||
<snaker-start slot="header" label="文体社团会员申请" define_key="XHRH"></snaker-start>
|
||||
<div slot="header" class="club-apply-back-bar">
|
||||
<el-button type="text" icon="el-icon-back" @click="backToClubList">返回</el-button>
|
||||
</div>
|
||||
<snaker-start class="club-apply-title" label="文体社团会员申请" define_key="XHRH"></snaker-start>
|
||||
<el-form :model="formData" ref="formRef" :rules="formRules" label-width="0" label-suffix=":" class="flow-task-form">
|
||||
<el-descriptions border class="descriptions-form">
|
||||
<el-descriptions-item label="社团" :span="3">
|
||||
@@ -156,6 +166,9 @@ layout("/layouts/platform.html"){
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
backToClubList() {
|
||||
commonUtil.pjaxPush('/platform/club/join/list');
|
||||
},
|
||||
onSave() {
|
||||
this.$confirm("您确定保存吗?", "提示", {
|
||||
confirmButtonText: "确定",
|
||||
@@ -224,6 +237,16 @@ layout("/layouts/platform.html"){
|
||||
this.$axios.post("/platform/club/common/listClub").then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.clubOptions = res.data
|
||||
// 从卡片进入新申请时预选社团,编辑已有申请仍使用原记录。
|
||||
const clubId = GetQueryString("clubId")
|
||||
if (!this.bizId && clubId) {
|
||||
const club = this.clubOptions.find(item => item.id === clubId)
|
||||
if (club) {
|
||||
this.$set(this.formData, "clubId", club.id)
|
||||
this.$set(this.formData, "clubName", club.clubName)
|
||||
this.checkApplyClub(club.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
@@ -11,9 +11,36 @@ layout("/layouts/platform.html"){
|
||||
.el-tabs__header {
|
||||
margin: 0;
|
||||
}
|
||||
.club-directory .club-card-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); gap: 20px; }
|
||||
.club-directory .club-card { display: flex; flex-direction: column; min-width: 0; overflow: hidden; border: 1px solid #e4edf8; border-radius: 10px; background: #fff; box-shadow: 0 7px 20px rgba(55, 85, 126, .08); }
|
||||
.club-directory .club-card-main { flex: 1; min-width: 0; }
|
||||
.club-directory .club-card-cover { width: 100%; height: 180px; overflow: hidden; background: #edf4fc; }
|
||||
.club-directory .club-card-cover .el-image { width: 100%; height: 100%; }
|
||||
.club-directory .club-image-empty { display: flex; flex-direction: column; gap: 8px; height: 100%; align-items: center; justify-content: center; color: #8da1ba; font-size: 13px; background: #edf4fc; }
|
||||
.club-directory .club-image-empty i { font-size: 34px; }
|
||||
.club-directory .club-card-info { min-width: 0; padding: 14px 16px; }
|
||||
.club-directory .club-card-name { margin: 0; color: #303846; font-size: 16px; font-weight: 400; line-height: 24px; overflow-wrap: anywhere; }
|
||||
.club-directory .club-card-meta { margin: 5px 0 0; color: #98a2b2; font-size: 13px; line-height: 20px; overflow-wrap: anywhere; }
|
||||
.club-directory .club-card-tags { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin-top: 10px; padding-top: 10px; border-top: 1px solid #edf1f6; }
|
||||
.club-directory .club-card-tag { display: inline-flex; align-items: center; padding: 3px 8px; border-radius: 4px; background: #edf5ff; color: #397de0; font-size: 12px; line-height: 18px; }
|
||||
.club-directory .club-card-tag-label { margin-right: 4px; color: #8aa7cf; }
|
||||
.club-directory .club-card-actions { display: flex; justify-content: flex-end; gap: 8px; padding: 12px 0 14px; margin: 0 16px; border-top: 1px solid #edf1f6; }
|
||||
.club-directory .club-card-actions .el-button { height: 32px; margin: 0; padding: 0 14px; border: 0; border-radius: 16px; font-size: 13px; }
|
||||
.club-directory .club-card-actions .el-button--primary { width: 108px; color: #fff; background: #1989fa; box-shadow: 0 5px 12px rgba(25, 137, 250, .18); }
|
||||
.club-directory .club-card-actions .el-button--primary.is-plain { width: auto; color: #397de0; background: #edf5ff; box-shadow: none; }
|
||||
.club-directory .club-card-actions .el-button--danger { width: 108px; color: #fff; background: #ee0a24; box-shadow: 0 5px 12px rgba(238, 10, 36, .18); }
|
||||
.club-directory .club-card-sort { display: flex; justify-content: flex-end; margin-bottom: 16px; }
|
||||
.club-directory .club-card-sort .el-select { width: 168px; }
|
||||
.club-directory .club-detail-cover { width: 160px; max-width: 100%; height: 110px; overflow: hidden; border-radius: 8px; background: #f8fafc; }
|
||||
.club-directory .club-detail-cover .el-image { display: block; width: 100%; height: 100%; border-radius: 8px; }
|
||||
@media (max-width: 640px) {
|
||||
.club-directory .club-card-grid { grid-template-columns: minmax(0, 1fr); }
|
||||
.club-directory .club-card-cover { height: 150px; }
|
||||
.club-directory .club-detail-cover { width: 120px; height: 90px; }
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="app" v-cloak>
|
||||
<div id="app" v-cloak class="club-directory">
|
||||
<guava ref="guava">
|
||||
<template>
|
||||
<el-card shadow="never">
|
||||
@@ -38,29 +65,56 @@ layout("/layouts/platform.html"){
|
||||
<el-radio-button :label="true">{{'已加入社团' + hasJoinCount + '(退会)'}}</el-radio-button>
|
||||
</el-radio-group>
|
||||
</table-tool>
|
||||
<el-table :data="tableData" row-key="id" style="width: 100%">
|
||||
<el-table-column :index="indexMethod" label="序号" type="index" width="80px"></el-table-column>
|
||||
<el-table-column label="社团编码" sortable prop="clubCode"></el-table-column>
|
||||
<el-table-column label="社团名称" prop="clubName" show-overflow-tooltip width="200px"></el-table-column>
|
||||
<el-table-column label="成立时间" prop="foundTime"></el-table-column>
|
||||
<el-table-column label="会长" prop="clubLeader" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="秘书长" prop="clubSecretary" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column sortable label="当前人数" show-overflow-tooltip prop="currentPeopleNum"></el-table-column>
|
||||
<el-table-column label="操作" width="250px">
|
||||
<template slot-scope="scope">
|
||||
<el-button size="mini" type="primary" @click="openDetail(scope.row)">查看</el-button>
|
||||
<el-button size="mini" v-if="hasJoin === false" type="primary" @click="doSubmit(scope.row)">申请加入</el-button>
|
||||
<el-button size="mini" v-else type="danger" @click="onExit(scope.row)">申请退出</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="club-card-sort">
|
||||
<el-select v-model="clubSort" size="small" aria-label="社团排序" @change="changeClubSort">
|
||||
<el-option label="社团编码升序" value="clubCode:ascending"></el-option>
|
||||
<el-option label="社团编码降序" value="clubCode:descending"></el-option>
|
||||
<el-option label="当前人数升序" value="currentPeopleNum:ascending"></el-option>
|
||||
<el-option label="当前人数降序" value="currentPeopleNum:descending"></el-option>
|
||||
</el-select>
|
||||
</div>
|
||||
<div class="club-card-grid" v-loading="tableLoading">
|
||||
<article v-for="row in tableData" :key="row.id" class="club-card">
|
||||
<div class="club-card-main">
|
||||
<div class="club-card-cover">
|
||||
<el-image v-if="row.thumbnail" :src="row.thumbnail" :alt="row.clubName + '缩略图'" fit="cover" :preview-src-list="[row.thumbnail]">
|
||||
<div slot="error" class="club-image-empty"><i class="el-icon-picture-outline"></i><span>暂无图片</span></div>
|
||||
</el-image>
|
||||
<div v-else class="club-image-empty"><i class="el-icon-picture-outline"></i><span>暂无图片</span></div>
|
||||
</div>
|
||||
<div class="club-card-info">
|
||||
<h3 class="club-card-name">{{row.clubName}}</h3>
|
||||
<p class="club-card-meta">成立时间:{{row.foundTime || '--'}}</p>
|
||||
<div class="club-card-tags">
|
||||
<span class="club-card-tag"><span class="club-card-tag-label">会长</span>{{row.clubLeader || '--'}}</span>
|
||||
<span class="club-card-tag"><span class="club-card-tag-label">社团类型</span>{{formatClubType(row.clubType)}}</span>
|
||||
<span class="club-card-tag"><span class="club-card-tag-label">会费标准</span>{{row.due == null || row.due === '' ? '--' : row.due + ' 元/年'}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="club-card-actions">
|
||||
<el-button size="mini" type="primary" plain icon="el-icon-view" @click="openDetail(row)">查看</el-button>
|
||||
<el-button size="mini" v-if="hasJoin === false" type="primary" icon="el-icon-right" @click="doSubmit(row)">申请加入</el-button>
|
||||
<el-button size="mini" v-else type="danger" icon="el-icon-switch-button" @click="onExit(row)">申请退出</el-button>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
<el-empty v-if="!tableLoading && !tableData.length" description="暂无社团"></el-empty>
|
||||
<!--#include("/layouts/pagination.html"){}#-->
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<template #view>
|
||||
<el-descriptions class="margin-top" title="" :column="2" border>
|
||||
<el-descriptions-item span="2">
|
||||
<el-descriptions-item label="社团图片">
|
||||
<div class="club-detail-cover">
|
||||
<el-image v-if="viewData.thumbnail" :src="viewData.thumbnail" :alt="viewData.clubName + '缩略图'" fit="contain" :preview-src-list="[viewData.thumbnail]">
|
||||
<div slot="error" class="club-image-empty">暂无图片</div>
|
||||
</el-image>
|
||||
<div v-else class="club-image-empty">暂无图片</div>
|
||||
</div>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item>
|
||||
<template slot="label">社团名称</template>
|
||||
<div v-html="viewData.clubName"></div>
|
||||
</el-descriptions-item>
|
||||
@@ -103,6 +157,7 @@ layout("/layouts/platform.html"){
|
||||
new Vue({
|
||||
el: "#app",
|
||||
store,
|
||||
dicts: ['CLUB_REGISTER_TYPE'],
|
||||
mixins: [initTableMixins],
|
||||
components: {},
|
||||
data() {
|
||||
@@ -111,10 +166,21 @@ layout("/layouts/platform.html"){
|
||||
hasJoin: false,
|
||||
hasJoinCount: 0,
|
||||
noJoinCount: 0,
|
||||
clubSort: 'clubCode:ascending',
|
||||
clickRow: {}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
formatClubType(value) {
|
||||
const option = (this.dict.type.CLUB_REGISTER_TYPE || []).find((item) => String(item.code) === String(value));
|
||||
return option ? option.name : '--';
|
||||
},
|
||||
changeClubSort(value) {
|
||||
const [prop, order] = value.split(':')
|
||||
this.pageForm.pageOrderName = prop
|
||||
this.pageForm.pageOrderBy = order
|
||||
this.doSearch()
|
||||
},
|
||||
onExit(row) {
|
||||
this.$confirm("您确定要申请退出" + row.clubName + "吗?", "提示", {
|
||||
confirmButtonText: "确定",
|
||||
@@ -162,9 +228,14 @@ layout("/layouts/platform.html"){
|
||||
},
|
||||
pageData() {
|
||||
this.pageForm.hasJoin = this.hasJoin
|
||||
this.$axios.post("/platform/club/join/list/pageData", this.pageForm).then((res) => {
|
||||
this.tableData = res.data.list
|
||||
this.pageForm.totalCount = res.data.totalCount
|
||||
this.tableLoading = true
|
||||
return this.$axios.post("/platform/club/join/list/pageData", this.pageForm).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.tableData = res.data.list
|
||||
this.pageForm.totalCount = res.data.totalCount
|
||||
}
|
||||
}).finally(() => {
|
||||
this.tableLoading = false
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
@@ -58,6 +58,12 @@ layout("/layouts/platform.html"){
|
||||
<el-tabs tab-position="top" v-model="activeName">
|
||||
<el-tab-pane label="社团基础信息" name="1">
|
||||
<el-descriptions style="margin-top: 20px;" class="margin-top" title="" :column="2" border>
|
||||
<el-descriptions-item label="社团缩略图" :span="2">
|
||||
<el-image v-if="viewData.thumbnail" :src="viewData.thumbnail" :preview-src-list="[viewData.thumbnail]" fit="contain" style="width: 180px; height: 120px">
|
||||
<span slot="error">暂无图片</span>
|
||||
</el-image>
|
||||
<span v-else>暂无图片</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="社团名称">{{viewData.clubName}}</el-descriptions-item>
|
||||
<el-descriptions-item label="成立时间">{{viewData.foundTime}}</el-descriptions-item>
|
||||
<el-descriptions-item label="社团编码">{{viewData.clubCode}}</el-descriptions-item>
|
||||
|
||||
@@ -79,6 +79,20 @@ const CLUB_FORM_TEMPLATE = {
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-form-item label="社团缩略图" prop="thumbnail">
|
||||
<file-upload
|
||||
:value.sync="formData.thumbnail"
|
||||
:upload_number="1"
|
||||
:upload_size="2 * 1024 * 1024"
|
||||
upload_mode="image"
|
||||
upload_result_category="interval"
|
||||
upload_result_type="url"
|
||||
upload_dynamic_return_url_api="/platform/club/common/uploadThumbnail"
|
||||
accept=".jpg,.jpeg,.png,.gif"
|
||||
></file-upload>
|
||||
<div style="color: #909399; font-size: 12px">支持 JPG、PNG、GIF 图片,大小不超过 2MB</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="社团介绍及宗旨" prop="introduce">
|
||||
<text-editor v-model="formData.introduce"></text-editor>
|
||||
</el-form-item>
|
||||
@@ -156,7 +170,8 @@ const CLUB_FORM_TEMPLATE = {
|
||||
return {
|
||||
formData: {
|
||||
clubName: "",
|
||||
clubType: ""
|
||||
clubType: "",
|
||||
thumbnail: ""
|
||||
},
|
||||
formRules: {
|
||||
clubName: [{ required: true, message: "请填写社团名称", trigger: ["blur", "change"] }],
|
||||
|
||||
@@ -341,32 +341,42 @@ layout("/layouts/platform.html"){
|
||||
this.noSelectOptionId = null
|
||||
this.selectByAdminDialogVisible = true
|
||||
},
|
||||
async yearChange() {
|
||||
const resp = await this.$axios.post("/platform/healthCheckup/project/mange/getHealthCheckupProject", {year: this.pageForm.year})
|
||||
if (resp.code === 0) {
|
||||
this.projectOptions = resp.data
|
||||
if (resp.data && resp.data.length > 0) {
|
||||
this.$set(this.pageForm, "projectId", resp.data[0].id)
|
||||
} else {
|
||||
this.$set(this.pageForm, "projectId", null)
|
||||
yearChange() {
|
||||
// 年度切换时先清理旧项目,避免无项目年度继续显示上一年度的数据。
|
||||
this.$set(this.pageForm, "projectId", null)
|
||||
this.$set(this, "projectOptions", [])
|
||||
this.getHealthCheckupSubject()
|
||||
return this.$axios.post("/platform/healthCheckup/project/mange/getHealthCheckupProject", {year: this.pageForm.year}).then((resp) => {
|
||||
if (resp.code === 0) {
|
||||
this.$set(this, "projectOptions", resp.data || [])
|
||||
if (this.projectOptions.length > 0) {
|
||||
this.$set(this.pageForm, "projectId", this.projectOptions[0].id)
|
||||
}
|
||||
return this.getHealthCheckupSubject()
|
||||
}
|
||||
await this.getHealthCheckupSubject()
|
||||
} else {
|
||||
this.notifyError(resp.msg)
|
||||
}
|
||||
})
|
||||
},
|
||||
async getHealthCheckupSubject() {
|
||||
this.projectSubjectOptions = []
|
||||
const resp = await this.$axios.post("/platform/healthCheckup/project/mange/findOne", {id: this.pageForm.projectId})
|
||||
if (resp.code === 0) {
|
||||
if (resp.data) {
|
||||
this.projectInfo = resp.data
|
||||
this.projectSubjectOptions = resp.data.healthCheckupProjectSubjects
|
||||
}
|
||||
} else {
|
||||
this.notifyError(resp.msg)
|
||||
getHealthCheckupSubject() {
|
||||
this.$set(this, "projectInfo", {})
|
||||
this.$set(this, "projectSubjectOptions", [])
|
||||
this.$set(this, "tableData", [])
|
||||
// 未选项目时只刷新空表结构,不请求项目详情或人数统计。
|
||||
if (!this.pageForm.projectId) {
|
||||
return this.pageData()
|
||||
}
|
||||
this.doSearch()
|
||||
return this.$axios.post("/platform/healthCheckup/project/mange/findOne", {id: this.pageForm.projectId}).then((resp) => {
|
||||
if (resp.code === 0) {
|
||||
if (resp.data) {
|
||||
this.$set(this, "projectInfo", resp.data)
|
||||
this.$set(this, "projectSubjectOptions", resp.data.healthCheckupProjectSubjects || [])
|
||||
} else {
|
||||
this.$set(this.pageForm, "projectId", null)
|
||||
}
|
||||
return this.pageData()
|
||||
}
|
||||
this.notifyError(resp.msg)
|
||||
})
|
||||
},
|
||||
async queryCampus() {
|
||||
const resp = await this.$axios.post("/platform/healthCheckup/campus/queryCampus")
|
||||
@@ -377,14 +387,8 @@ layout("/layouts/platform.html"){
|
||||
}
|
||||
},
|
||||
|
||||
async pageData() {
|
||||
pageData() {
|
||||
this.tableColumns = []
|
||||
const loading = this.$loading({
|
||||
lock: true,
|
||||
text: '努力加载中。。。',
|
||||
spinner: 'el-icon-loading',
|
||||
background: 'rgba(0, 0, 0, 0.7)'
|
||||
});
|
||||
|
||||
this.tableColumns.push(
|
||||
{prop: 'unionName', label: '所属工会'},
|
||||
@@ -400,14 +404,26 @@ layout("/layouts/platform.html"){
|
||||
{prop: "unselectedTeacherSum", label: "未选人数"}
|
||||
)
|
||||
if (!this.pageForm.projectId) {
|
||||
this.tableData = []
|
||||
this.$set(this, "tableData", [])
|
||||
return
|
||||
}
|
||||
|
||||
const resp = await this.$axios.post(loc() + '/pageData', this.pageForm)
|
||||
if (resp.code === 0) {
|
||||
this.tableData = resp.data.tableList
|
||||
}
|
||||
loading.close()
|
||||
this.tableLoading = true
|
||||
const loading = this.$loading({
|
||||
lock: true,
|
||||
text: '努力加载中。。。',
|
||||
spinner: 'el-icon-loading',
|
||||
background: 'rgba(0, 0, 0, 0.7)'
|
||||
})
|
||||
return this.$axios.post(loc() + '/pageData', this.pageForm).then((resp) => {
|
||||
if (resp.code === 0) {
|
||||
this.$set(this, "tableData", resp.data.tableList)
|
||||
}
|
||||
}).finally(() => {
|
||||
// 业务错误或网络异常也必须释放遮罩,保证页面可以重新查询。
|
||||
this.tableLoading = false
|
||||
loading.close()
|
||||
})
|
||||
},
|
||||
pageNumberChange(val) {
|
||||
this.pageFormUnion.pageNumber = val;
|
||||
|
||||
@@ -57,8 +57,10 @@ layout("/layouts/platform.html"){
|
||||
min-width="50"
|
||||
>
|
||||
<template v-slot="{row}" v-if="column.prop=='surplusQuota'">
|
||||
<span v-if="!row.totalQuota">{{row.totalQuota}}元</span>
|
||||
<span v-else>{{(row.totalQuota-row.usedQuota).toFixed(2)}}</span>
|
||||
<span>{{(Number(row.totalQuota || 0) - Number(row.usedQuota || 0)).toFixed(2)}}</span>
|
||||
</template>
|
||||
<template v-slot="{row}" v-else-if="column.prop=='usedQuota'">
|
||||
<span>{{Number(row.usedQuota || 0).toFixed(2)}}</span>
|
||||
</template>
|
||||
|
||||
</el-table-column>
|
||||
|
||||
@@ -7,6 +7,11 @@ layout("/layouts/platform.html"){
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
/* 社团表单改为两组字段后,左侧标签仍保持原三组字段表单的六分之一宽度。 */
|
||||
.club-reimbursement-form .el-descriptions-item__label:first-child {
|
||||
width: calc(100% / 6);
|
||||
}
|
||||
|
||||
.invoice-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -60,17 +65,21 @@ layout("/layouts/platform.html"){
|
||||
日常活动申请</h3>
|
||||
<h3 style="color: rgb(24, 103, 176);" v-else-if="formData.reimburseProject === 'UNION_REIMBURSE_PROJECT_4'">
|
||||
专项活动申请</h3>
|
||||
<h3 style="color: rgb(24, 103, 176);" v-else-if="formData.reimburseProject === 'UNION_REIMBURSE_PROJECT_5'">
|
||||
社团活动申请</h3>
|
||||
</template>
|
||||
<el-form :model="formData" ref="formRef" :rules="formRules" label-width="0" label-suffix=":"
|
||||
class="flow-task-form">
|
||||
<el-tabs class="reimbursement-tabs" v-model="activeTabName">
|
||||
<el-tab-pane label="申请基本信息" name="basic">
|
||||
<el-descriptions :column="3" border>
|
||||
<el-descriptions-item label="经费来源" span="3">
|
||||
<!-- 来源或项目切换时重建表单项,避免动态布局复用旧字段的校验状态。 -->
|
||||
<el-descriptions :column="formData.reimburseFundSource === 'UNION_REIMBURSE_FUND_SOURCE_3' ? 2 : 3" :class="{'club-reimbursement-form': formData.reimburseFundSource === 'UNION_REIMBURSE_FUND_SOURCE_3'}" border :key="formData.reimburseFundSource + ':' + formData.reimburseProject">
|
||||
<el-descriptions-item label="经费来源" :span="formData.reimburseFundSource === 'UNION_REIMBURSE_FUND_SOURCE_3' ? 2 : 3">
|
||||
<el-form-item label="经费来源" prop="reimburseFundSource">
|
||||
|
||||
<!-- 在表单重建前同步切换报销项目,避免延迟的 change 事件丢失。 -->
|
||||
<el-radio-group v-model="formData.reimburseFundSource" size="medium"
|
||||
@change="selectFundSource">
|
||||
@input="selectFundSource">
|
||||
<el-radio v-if="$auth.hasPermission('unionReimburse.xgh')"
|
||||
label="UNION_REIMBURSE_FUND_SOURCE_1"
|
||||
border>校工会经费
|
||||
@@ -88,11 +97,11 @@ layout("/layouts/platform.html"){
|
||||
|
||||
</el-form-item>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="报销项目" :span="3">
|
||||
<el-descriptions-item label="报销项目" :span="formData.reimburseFundSource === 'UNION_REIMBURSE_FUND_SOURCE_3' ? 2 : 3">
|
||||
<el-form-item label="报销项目" prop="reimburseProject">
|
||||
<el-radio-group v-model="formData.reimburseProject" size="medium"
|
||||
@change="handleReimburseProjectChange">
|
||||
<el-radio :label="item.code" border v-for="item in reimburseProjectList">
|
||||
<el-radio :label="item.code" border v-for="item in availableReimburseProjects" :key="item.code">
|
||||
{{item.name}}
|
||||
</el-radio>
|
||||
</el-radio-group>
|
||||
@@ -104,24 +113,6 @@ layout("/layouts/platform.html"){
|
||||
<el-descriptions-item label="经办人">{{formData.userName}}({{formData.loginName}})
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="所属协会"
|
||||
v-if="formData.reimburseFundSource === 'UNION_REIMBURSE_FUND_SOURCE_3'">
|
||||
<el-form-item label="所属协会" prop="clubId">
|
||||
<el-select clearable
|
||||
placeholder="请选择协会"
|
||||
style="width: 100%;"
|
||||
v-model="formData.clubId"
|
||||
@change="clubChange">
|
||||
<el-option
|
||||
:key="item.id"
|
||||
:label="item.clubName"
|
||||
:value="item.id"
|
||||
v-for="item in clubOptions">
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-descriptions-item>
|
||||
|
||||
<!-- <el-descriptions-item label="报销类别">-->
|
||||
<!-- <el-form-item label="报销类别" prop="reimburseType">-->
|
||||
<!-- <el-radio-group v-model="formData.reimburseType" size="medium">-->
|
||||
@@ -149,7 +140,7 @@ layout("/layouts/platform.html"){
|
||||
placeholder="请输入联系方式"></el-input>
|
||||
</el-form-item>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="余额" v-if="formData.reimburseFundSource">
|
||||
<el-descriptions-item label="余额" v-if="formData.reimburseFundSource && formData.reimburseFundSource !== 'UNION_REIMBURSE_FUND_SOURCE_3'">
|
||||
<el-form-item label="余额">
|
||||
<el-input :value="canViewFundBalance ? fundBalanceText : '******'" readonly></el-input>
|
||||
</el-form-item>
|
||||
@@ -273,7 +264,34 @@ layout("/layouts/platform.html"){
|
||||
</el-date-picker>
|
||||
</el-form-item>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="" :span="2"
|
||||
<el-descriptions-item label="报销金额" v-if="isManualReimburseAmount">
|
||||
<el-form-item label="报销金额" prop="money">
|
||||
<el-input v-model="formData.money" placeholder="请输入报销金额,最多两位小数"></el-input>
|
||||
</el-form-item>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="所属协会"
|
||||
v-if="formData.reimburseFundSource === 'UNION_REIMBURSE_FUND_SOURCE_3'">
|
||||
<el-form-item label="所属协会" prop="clubId">
|
||||
<el-select clearable
|
||||
placeholder="请选择协会"
|
||||
style="width: 100%;"
|
||||
v-model="formData.clubId"
|
||||
@change="clubChange">
|
||||
<el-option
|
||||
:key="item.id"
|
||||
:label="item.clubName"
|
||||
:value="item.id"
|
||||
v-for="item in clubOptions">
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="余额" v-if="formData.reimburseFundSource === 'UNION_REIMBURSE_FUND_SOURCE_3'">
|
||||
<el-form-item label="余额">
|
||||
<el-input :value="canViewFundBalance ? fundBalanceText : '******'" readonly></el-input>
|
||||
</el-form-item>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="" :span="isManualReimburseAmount ? 1 : 2"
|
||||
v-if="formData.reimburseProject !== 'UNION_REIMBURSE_PROJECT_1' && formData.reimburseFundSource !== 'UNION_REIMBURSE_FUND_SOURCE_3'">
|
||||
|
||||
</el-descriptions-item>
|
||||
@@ -399,7 +417,7 @@ layout("/layouts/platform.html"){
|
||||
</el-form-item>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item :span="3">
|
||||
<el-descriptions-item :span="formData.reimburseFundSource === 'UNION_REIMBURSE_FUND_SOURCE_3' ? 2 : 3">
|
||||
<template slot="label">
|
||||
附件
|
||||
</template>
|
||||
@@ -438,7 +456,7 @@ layout("/layouts/platform.html"){
|
||||
<el-col :span="12">
|
||||
<el-form-item label="报销金额" prop="money">
|
||||
<div class="invoice-field-title">报销金额</div>
|
||||
<el-input v-model="formData.money" placeholder="有发票按发票汇总,无发票按支付信息汇总" readonly></el-input>
|
||||
<el-input v-model="formData.money" :placeholder="isManualReimburseAmount ? '请在申请基本信息填写报销金额' : '有发票按发票汇总,无发票按支付信息汇总'" readonly></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
@@ -587,7 +605,7 @@ layout("/layouts/platform.html"){
|
||||
<div>
|
||||
<div class="invoice-toolbar-title">发票明细列表</div>
|
||||
<div class="invoice-summary-tip">
|
||||
发票选填;无发票时按支付信息汇总报销金额。上传后须通过验真,每条明细对应一张发票图片或单页 PDF。
|
||||
{{ isManualReimburseAmount ? '发票选填;有发票时报销金额不得超过发票总额,无发票仍可提交。' : '发票选填;无发票时按支付信息汇总报销金额。' }}上传后须通过验真,每条明细对应一张发票图片或单页 PDF。
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
@@ -821,7 +839,13 @@ layout("/layouts/platform.html"){
|
||||
// invoiceNumber: [{required: true, message: "请填写发票张数", trigger: ["change", "blur"]}],
|
||||
// invoice: [{required: true, message: "请填写发票号码", trigger: ["change", "blur"]}],
|
||||
activityName: [{required: true, message: "请填写活动名称", trigger: ["change", "blur"]}],
|
||||
money: [{required: true, message: "请填写报销金额", trigger: ["change", "blur"]}],
|
||||
money: [{required: true, message: "请填写报销金额", trigger: ["change", "blur"]}, {
|
||||
validator: (rule, value, callback) => {
|
||||
const message = this.getReimburseAmountMessage()
|
||||
callback(message ? new Error(message) : undefined)
|
||||
},
|
||||
trigger: ["change", "blur"]
|
||||
}],
|
||||
condolenceRelationship: [{required: true, message: "与被慰问人关系", trigger: ["change", "blur"]}],
|
||||
marryTime: [{required: true, message: "结婚时间", trigger: ["change", "blur"]}],
|
||||
fertilityTime: [{required: true, message: "生育时间", trigger: ["change", "blur"]}],
|
||||
@@ -835,8 +859,8 @@ layout("/layouts/platform.html"){
|
||||
],
|
||||
files: [{
|
||||
validator: (rule, value, callback) => {
|
||||
// 慰问项目不展示附件上传,提交时无需校验附件。
|
||||
if (this.formData.reimburseProject === "UNION_REIMBURSE_PROJECT_1") {
|
||||
// 慰问项目无需校验附件,文体、日常和社团活动附件选填。
|
||||
if (["UNION_REIMBURSE_PROJECT_1", "UNION_REIMBURSE_PROJECT_2", "UNION_REIMBURSE_PROJECT_3", "UNION_REIMBURSE_PROJECT_5"].includes(this.formData.reimburseProject)) {
|
||||
callback()
|
||||
return
|
||||
}
|
||||
@@ -910,6 +934,16 @@ layout("/layouts/platform.html"){
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isManualReimburseAmount() {
|
||||
return ["UNION_REIMBURSE_PROJECT_2", "UNION_REIMBURSE_PROJECT_3", "UNION_REIMBURSE_PROJECT_4"]
|
||||
.includes(this.formData.reimburseProject)
|
||||
},
|
||||
// 社团经费固定对应社团活动,复用文体活动的基本信息和发票表单。
|
||||
availableReimburseProjects() {
|
||||
const clubFund = this.formData.reimburseFundSource === "UNION_REIMBURSE_FUND_SOURCE_3"
|
||||
return this.reimburseProjectList.filter(item =>
|
||||
clubFund ? item.code === "UNION_REIMBURSE_PROJECT_5" : item.code !== "UNION_REIMBURSE_PROJECT_5")
|
||||
},
|
||||
// 按选中类型的业务编码控制字段,类型列表异步加载及旧申请回显后自动重新计算。
|
||||
condolenceTypeCode() {
|
||||
if (this.formData.reimburseProject !== "UNION_REIMBURSE_PROJECT_1") return ""
|
||||
@@ -917,9 +951,11 @@ layout("/layouts/platform.html"){
|
||||
const selectedType = this.typeOptions.find(item => item.id === typeId)
|
||||
return selectedType ? selectedType.code : ""
|
||||
},
|
||||
// 仅系统管理员和校工会管理员可查看真实余额,其他账号统一显示余额遮罩。
|
||||
// 社团经费按菜单授权查看年度剩余额度,其他经费沿用原余额可见范围。
|
||||
canViewFundBalance() {
|
||||
return this.$auth.hasRoleOr(["SYSADMIN", "SCHOOL_UNION_ADMIN"])
|
||||
|| (this.formData.reimburseFundSource === "UNION_REIMBURSE_FUND_SOURCE_3"
|
||||
&& this.$auth.hasPermission("unionReimburse.club"))
|
||||
},
|
||||
showInvoiceTab() {
|
||||
return !!this.formData.reimburseProject
|
||||
@@ -1549,6 +1585,27 @@ layout("/layouts/platform.html"){
|
||||
this.rebuildInvoiceSummary()
|
||||
})
|
||||
},
|
||||
// 手填金额按分比较票面总额;未上传发票时仍允许申请。
|
||||
getReimburseAmountMessage() {
|
||||
if (!this.isManualReimburseAmount) return ""
|
||||
const value = String(this.formData.money == null ? "" : this.formData.money)
|
||||
if (!value) return "请填写报销金额"
|
||||
const cents = this.moneyToCents(value)
|
||||
if (!/^\d+(\.\d{1,2})?$/.test(value) || cents === null || cents <= 0) {
|
||||
return "报销金额必须为正数,且最多两位小数"
|
||||
}
|
||||
const invoices = this.formData.invoiceDetails || []
|
||||
let invoiceCents = 0
|
||||
for (let i = 0; i < invoices.length; i++) {
|
||||
const amount = this.moneyToCents(invoices[i] && invoices[i].invoiceAmount)
|
||||
if (amount === null || amount <= 0) return "请填写正确的发票金额"
|
||||
invoiceCents += amount
|
||||
}
|
||||
if (invoices.length && cents > invoiceCents) {
|
||||
return "报销金额不能超过发票总额(" + (invoiceCents / 100).toFixed(2) + "元)"
|
||||
}
|
||||
return ""
|
||||
},
|
||||
rebuildInvoiceSummary() {
|
||||
let totalMoney = 0
|
||||
let fileCount = 0
|
||||
@@ -1573,10 +1630,18 @@ layout("/layouts/platform.html"){
|
||||
})
|
||||
totalMoney = paymentCents / 100
|
||||
}
|
||||
this.$set(this.formData, "money", totalMoney ? totalMoney.toFixed(2) : "0.00")
|
||||
// 三类活动保留手填申请金额,发票及支付明细变化只更新张数。
|
||||
if (!this.isManualReimburseAmount) {
|
||||
this.$set(this.formData, "money", totalMoney ? totalMoney.toFixed(2) : "0.00")
|
||||
}
|
||||
this.$set(this.formData, "invoiceNumber", String(fileCount))
|
||||
},
|
||||
validateInvoiceDetailsBeforeSubmit() {
|
||||
const amountMessage = this.getReimburseAmountMessage()
|
||||
if (amountMessage) {
|
||||
this.$message.warning(amountMessage)
|
||||
return false
|
||||
}
|
||||
const invoiceDetails = this.formData.invoiceDetails || []
|
||||
// 发票可不上传;已经添加的发票仍须填写完整并沿用验真流程。
|
||||
for (let i = 0; i < invoiceDetails.length; i++) {
|
||||
@@ -1904,6 +1969,7 @@ layout("/layouts/platform.html"){
|
||||
},
|
||||
// 根据报销项目获取对应的附件说明
|
||||
getFileDescByProject(projectCode) {
|
||||
if (projectCode === "UNION_REIMBURSE_PROJECT_5") return "社团活动附件说明"
|
||||
if (!this.descOptions || !Array.isArray(this.descOptions)) {
|
||||
return '';
|
||||
}
|
||||
@@ -1918,21 +1984,20 @@ layout("/layouts/platform.html"){
|
||||
const project = this.descOptions.find(item => item.reimburseProject === projectCode);
|
||||
return project ? project.projectTip : '';
|
||||
},
|
||||
// 新建申请默认优先选择分工会经费;若当前用户没有对应权限,则按页面可见权限回退到其它经费来源。
|
||||
// 默认来源按按钮菜单权限选择,保持分工会、协会、校工会的优先顺序。
|
||||
getDefaultFundSource() {
|
||||
if (this.$auth.hasPermission("unionReimburse.fgh")) {
|
||||
return "UNION_REIMBURSE_FUND_SOURCE_2"
|
||||
}
|
||||
if (this.$auth.hasPermission("unionReimburse.xgh")) {
|
||||
return "UNION_REIMBURSE_FUND_SOURCE_1"
|
||||
}
|
||||
if (this.$auth.hasPermission("unionReimburse.club")) {
|
||||
return "UNION_REIMBURSE_FUND_SOURCE_3"
|
||||
}
|
||||
if (this.$auth.hasPermission("unionReimburse.fgh")) return "UNION_REIMBURSE_FUND_SOURCE_2"
|
||||
if (this.$auth.hasPermission("unionReimburse.club")) return "UNION_REIMBURSE_FUND_SOURCE_3"
|
||||
if (this.$auth.hasPermission("unionReimburse.xgh")) return "UNION_REIMBURSE_FUND_SOURCE_1"
|
||||
return ""
|
||||
},
|
||||
selectFundSource(code) {
|
||||
this.$set(this.formData, "reimburseFundSource", code)
|
||||
if (code === "UNION_REIMBURSE_FUND_SOURCE_3") {
|
||||
this.$set(this.formData, "reimburseProject", "UNION_REIMBURSE_PROJECT_5")
|
||||
} else if (this.formData.reimburseProject === "UNION_REIMBURSE_PROJECT_5") {
|
||||
this.$set(this.formData, "reimburseProject", "UNION_REIMBURSE_PROJECT_2")
|
||||
}
|
||||
if (code !== "UNION_REIMBURSE_FUND_SOURCE_3") {
|
||||
this.$set(this.formData, "clubId", null)
|
||||
this.$set(this.formData, "clubName", null)
|
||||
@@ -1986,9 +2051,13 @@ layout("/layouts/platform.html"){
|
||||
this.fundBalanceText = "查询中..."
|
||||
this.$axios.post("/platform/unionReimburse/apply/getBudgetBalance", params)
|
||||
.then(response => {
|
||||
// 切换社团后忽略旧请求,避免显示上一社团的余额。
|
||||
if (fundSource !== this.formData.reimburseFundSource
|
||||
|| (params.clubId && params.clubId !== this.formData.clubId)) return
|
||||
if (response.code === 0) {
|
||||
this.$set(this.formData, "fundBalance", response.data || 0)
|
||||
this.fundBalanceText = (response.data || 0).toString()
|
||||
this.fundBalanceText = fundSource === "UNION_REIMBURSE_FUND_SOURCE_3"
|
||||
? Number(response.data || 0).toFixed(2) : (response.data || 0).toString()
|
||||
} else {
|
||||
this.$set(this.formData, "fundBalance", null)
|
||||
this.fundBalanceText = response.msg || "查询失败"
|
||||
@@ -1996,6 +2065,8 @@ layout("/layouts/platform.html"){
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
if (fundSource !== this.formData.reimburseFundSource
|
||||
|| (params.clubId && params.clubId !== this.formData.clubId)) return
|
||||
console.error("经费余额查询失败:", error)
|
||||
this.$set(this.formData, "fundBalance", null)
|
||||
this.fundBalanceText = "查询失败"
|
||||
@@ -2016,7 +2087,8 @@ layout("/layouts/platform.html"){
|
||||
this.formData.hospitalizationEndTime
|
||||
]);
|
||||
}
|
||||
if (this.$auth.hasRoleOr(["SCHOOL_OUTLAY_ADMIN", "SCHOOL_UNION_ADMIN"])) {
|
||||
if (this.$auth.hasRoleOr(["SCHOOL_OUTLAY_ADMIN", "SCHOOL_UNION_ADMIN"])
|
||||
&& !this.$auth.hasRoleOr(["BRANCH_UNION_CHAIRMAN", "BRANCH_UNION_ADMIN", "CLUB_MANAGER", "CLUB_PRESIDENT"])) {
|
||||
this.budgetTypeOption.map(v => {
|
||||
if (["UNION_REIMBURSE_FUND_SOURCE_1"].includes(v.code)) {
|
||||
budgetTypeOption.push(v)
|
||||
@@ -2030,7 +2102,7 @@ layout("/layouts/platform.html"){
|
||||
}
|
||||
})
|
||||
}
|
||||
if (this.$auth.hasRoleOr(["CLUB_MANAGER", "CLUB_PRESIDENT"])) {
|
||||
if (this.$auth.hasPermission("unionReimburse.club")) {
|
||||
this.budgetTypeOption.map(v => {
|
||||
if (["UNION_REIMBURSE_FUND_SOURCE_3"].includes(v.code)) {
|
||||
budgetTypeOption.push(v)
|
||||
@@ -2039,11 +2111,23 @@ layout("/layouts/platform.html"){
|
||||
}
|
||||
this.budgetTypeOption = budgetTypeOption
|
||||
}
|
||||
// 社团经费按菜单权限过滤,默认来源与按钮可见范围保持一致。
|
||||
this.budgetTypeOption = this.budgetTypeOption.filter(item =>
|
||||
item.code !== "UNION_REIMBURSE_FUND_SOURCE_3"
|
||||
|| this.$auth.hasPermission("unionReimburse.club"))
|
||||
this.reimburseProjectList = await this.$businessTool.getDictOptions("UNION_REIMBURSE_PROJECT")
|
||||
// 与协会经费按钮的显示条件一致,不依赖经费字典列表是否包含该项。
|
||||
if (this.$auth.hasPermission("unionReimburse.club")) {
|
||||
const clubs = await this.$axios.post("/platform/unionReimburse/apply/listReimburseClubs")
|
||||
this.clubOptions = clubs.data || []
|
||||
}
|
||||
if (this.bizId) {
|
||||
this.$axios.post("/platform/unionReimburse/apply/info", {id: this.bizId}).then(async (res) => {
|
||||
if (res.code === 0) {
|
||||
this.formData = this.ensureFormData(res.data)
|
||||
if (this.formData.reimburseFundSource === "UNION_REIMBURSE_FUND_SOURCE_3") {
|
||||
this.$set(this.formData, "reimburseProject", "UNION_REIMBURSE_PROJECT_5")
|
||||
}
|
||||
this.appendCurrentPayerOption()
|
||||
this.formData.paymentDetails.forEach(row => {
|
||||
if (row.payer) {
|
||||
@@ -2066,7 +2150,8 @@ layout("/layouts/platform.html"){
|
||||
unionId: union?.id,
|
||||
unionName: union?.name,
|
||||
// reimburseType: "UNION_REIMBURSE_TYPE_1",
|
||||
reimburseProject: 'UNION_REIMBURSE_PROJECT_1',
|
||||
reimburseProject: this.getDefaultFundSource() === "UNION_REIMBURSE_FUND_SOURCE_3"
|
||||
? "UNION_REIMBURSE_PROJECT_5" : "UNION_REIMBURSE_PROJECT_1",
|
||||
reimburseFundSource: this.getDefaultFundSource(),
|
||||
paymentWay: 'UNION_REIMBURSE_PAYMENT_WAY_2',
|
||||
paymentDetails: [this.buildDefaultPaymentDetail({
|
||||
@@ -2076,7 +2161,7 @@ layout("/layouts/platform.html"){
|
||||
userId: id,
|
||||
})
|
||||
if (this.formData.reimburseFundSource) {
|
||||
this.reimburseFundSourceChange()
|
||||
this.selectFundSource(this.formData.reimburseFundSource)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2085,17 +2170,7 @@ layout("/layouts/platform.html"){
|
||||
this.init()
|
||||
this.queryFileDesc()
|
||||
this.queryCondolenceType()
|
||||
//社团查询
|
||||
this.$businessTool.listClubByRole().then((res) => {
|
||||
this.$set(this, "clubOptions", res || [])
|
||||
if (res && res.length === 1 && !this.formData.clubId) {
|
||||
this.$set(this.formData, "clubId", res[0].id)
|
||||
this.$set(this.formData, "clubName", res[0].clubName)
|
||||
}
|
||||
if (this.formData.reimburseFundSource) {
|
||||
this.reimburseFundSourceChange()
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -51,10 +51,10 @@ layout("/layouts/platform.html"){
|
||||
</table-tool>
|
||||
<el-table :data="tableData" @sort-change="pageOrder" style="width: 100%">
|
||||
<el-table-column type="index" width="50px" label="序号" :index="indexMethod"></el-table-column>
|
||||
<el-table-column prop="serialNo" label="流水号" width="120"></el-table-column>
|
||||
<el-table-column prop="loginName" label="经办人工号" sortable="custom"></el-table-column>
|
||||
<el-table-column prop="userName" label="姓名"></el-table-column>
|
||||
<el-table-column prop="unionName" label="所属工会" sortable="custom"></el-table-column>
|
||||
<el-table-column prop="unitName" label="所属单位" sortable="custom"></el-table-column>
|
||||
<el-table-column prop="createTime" label="申请时间" sortable="custom">
|
||||
<template slot-scope="{row}">
|
||||
{{ formatCreateTime(row.createTime) }}
|
||||
@@ -62,7 +62,7 @@ layout("/layouts/platform.html"){
|
||||
</el-table-column>
|
||||
<el-table-column prop="reimburseProject" label="报销项目" sortable="custom">
|
||||
<template slot-scope="{row}">
|
||||
<el-select v-if="isAdmin && row.reimburseProject !== 'UNION_REIMBURSE_PROJECT_1'"
|
||||
<el-select v-if="isAdmin && row.reimburseProject !== 'UNION_REIMBURSE_PROJECT_1' && row.reimburseFundSource !== 'UNION_REIMBURSE_FUND_SOURCE_3'"
|
||||
:value="row.reimburseProject"
|
||||
size="mini"
|
||||
style="width: 120px"
|
||||
|
||||
@@ -35,7 +35,7 @@ const unionReimburseInfo = {
|
||||
<el-descriptions-item label="联系方式">
|
||||
<span>{{ viewData.mobile }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item></el-descriptions-item>
|
||||
<el-descriptions-item v-if="viewData.reimburseFundSource !== 'UNION_REIMBURSE_FUND_SOURCE_3'"></el-descriptions-item>
|
||||
<el-descriptions-item v-if="!viewData.reimburseFundSource"></el-descriptions-item>
|
||||
<el-descriptions-item label="支付信息" :span="3">
|
||||
<el-table :data="paymentInfoRows" border empty-text="暂无支付信息" style="width: 100%">
|
||||
|
||||
@@ -63,6 +63,7 @@ layout("/layouts/platform.html"){
|
||||
<span v-else-if="row.reimburseProject === 'UNION_REIMBURSE_PROJECT_2'">文体活动</span>
|
||||
<span v-else-if="row.reimburseProject === 'UNION_REIMBURSE_PROJECT_3'">日常活动</span>
|
||||
<span v-else-if="row.reimburseProject === 'UNION_REIMBURSE_PROJECT_4'">专项活动</span>
|
||||
<span v-else-if="row.reimburseProject === 'UNION_REIMBURSE_PROJECT_5'">社团活动</span>
|
||||
<span v-else>{{ row.reimburseProject }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
@@ -58,10 +58,10 @@ layout("/layouts/platform.html"){
|
||||
<el-table-column v-if="!pageForm.approval" type="selection" width="55"
|
||||
:reserve-selection="true" :selectable="(row) => row.stateId == 2"></el-table-column>
|
||||
<el-table-column type="index" width="50px" label="序号" :index="indexMethod"></el-table-column>
|
||||
<el-table-column prop="serialNo" label="流水号" width="120"></el-table-column>
|
||||
<el-table-column prop="loginName" label="经办人工号" sortable="custom"></el-table-column>
|
||||
<el-table-column prop="userName" label="姓名"></el-table-column>
|
||||
<el-table-column prop="unionName" label="所属工会" sortable="custom"></el-table-column>
|
||||
<el-table-column prop="unitName" label="所属单位" sortable="custom"></el-table-column>
|
||||
<el-table-column prop="createTime" label="申请时间" sortable="custom">
|
||||
<template slot-scope="{row}">
|
||||
{{ formatCreateTime(row.createTime) }}
|
||||
@@ -73,6 +73,7 @@ layout("/layouts/platform.html"){
|
||||
<span v-else-if="row.reimburseProject === 'UNION_REIMBURSE_PROJECT_2'">文体活动</span>
|
||||
<span v-else-if="row.reimburseProject === 'UNION_REIMBURSE_PROJECT_3'">日常活动</span>
|
||||
<span v-else-if="row.reimburseProject === 'UNION_REIMBURSE_PROJECT_4'">专项活动</span>
|
||||
<span v-else-if="row.reimburseProject === 'UNION_REIMBURSE_PROJECT_5'">社团活动</span>
|
||||
<span v-else>{{ row.reimburseProject }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
@@ -16,17 +16,22 @@
|
||||
:value="item.id"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item prop="userId" label="代表">
|
||||
<user-select v-model="formData.userId"
|
||||
ref="userSelectRef"
|
||||
v-if="dialogFormVisible"
|
||||
api="/platform/teacherCongress/delegate/manage/adjustUserList"
|
||||
:api_params="{sessionId:formData.sessionId}"
|
||||
api_input_key_name="keyWord"
|
||||
placeholder="请选择代表"
|
||||
:option_label_func="(item)=>{return item.userName + item.loginName + '(' + item.unitName + ')'}"
|
||||
></user-select>
|
||||
<el-button icon="el-icon-plus" type="primary" @click="move">添加到待调整列表</el-button>
|
||||
<el-form-item prop="userId" label="代表" style="flex: 1; min-width: 0; margin-left: 24px;">
|
||||
<!-- 选择框占用剩余空间,添加按钮保持在右侧并与输入框垂直对齐。 -->
|
||||
<div style="display: flex; align-items: center; gap: 12px;">
|
||||
<div style="flex: 1; min-width: 0;">
|
||||
<user-select v-model="formData.userId"
|
||||
ref="userSelectRef"
|
||||
v-if="dialogFormVisible"
|
||||
api="/platform/teacherCongress/delegate/manage/adjustUserList"
|
||||
:api_params="{sessionId:formData.sessionId}"
|
||||
api_input_key_name="keyWord"
|
||||
placeholder="请选择代表"
|
||||
:option_label_func="(item)=>{return item.userName + item.loginName + '(' + item.unitName + ')'}"
|
||||
></user-select>
|
||||
</div>
|
||||
<el-button icon="el-icon-plus" type="primary" style="flex-shrink: 0;" @click="move">添加到待调整列表</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-row>
|
||||
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
<!--#
|
||||
layout("/layouts/platform.html"){
|
||||
#-->
|
||||
<div id="app" v-cloak>
|
||||
<guava ref="guava">
|
||||
<template>
|
||||
<el-card shadow="never">
|
||||
<search @search="doSearch">
|
||||
<search-item label="员工信息">
|
||||
<el-input @keyup.enter.native="doSearch" clearable placeholder="请输入工号或姓名" v-model="pageForm.searchKeyword"></el-input>
|
||||
</search-item>
|
||||
</search>
|
||||
</el-card>
|
||||
<el-card class="mt10" shadow="never">
|
||||
<table-tool :app="this" label="华住员工管理">
|
||||
<el-button :loading="formLoading" :disabled="tableLoading" @click="syncSelected" icon="el-icon-refresh" size="small" type="primary">同步选中员工</el-button>
|
||||
</table-tool>
|
||||
<el-table :data="tableData" :size="tableSize" @selection-change="selectionChange" class="vi-table" v-loading="tableLoading || formLoading">
|
||||
<el-table-column type="selection" width="55"></el-table-column>
|
||||
<el-table-column :index="indexMethod" label="序号" type="index" width="80"></el-table-column>
|
||||
<el-table-column label="工号" min-width="140" prop="loginname"></el-table-column>
|
||||
<el-table-column label="姓名" min-width="120" prop="username"></el-table-column>
|
||||
<el-table-column label="单位" min-width="180" prop="unitName" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column fixed="right" label="操作" width="130">
|
||||
<template slot-scope="{row}"><el-button :disabled="tableLoading || formLoading" @click="queryStatus(row)" size="mini" type="primary">查询状态</el-button></template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<!--#include("/layouts/pagination.html"){}#-->
|
||||
</el-card>
|
||||
<el-dialog :visible="statusDialogVisible" @update:visible="value => this.$set(this, 'statusDialogVisible', value)" title="华住同步状态" width="520px">
|
||||
<el-descriptions :column="1" border size="small" v-if="statusData">
|
||||
<el-descriptions-item label="状态码">{{statusData.statusCode || "--"}}</el-descriptions-item>
|
||||
<el-descriptions-item label="工号">{{statusData.userNumber || "--"}}</el-descriptions-item>
|
||||
<el-descriptions-item label="姓名">{{statusData.userName || "--"}}</el-descriptions-item>
|
||||
<el-descriptions-item label="部门">{{statusData.userDep || "--"}}</el-descriptions-item>
|
||||
<el-descriptions-item label="成本中心">{{statusData.costCenter || "--"}}</el-descriptions-item>
|
||||
<el-descriptions-item label="职级">{{statusData.rank || "--"}}</el-descriptions-item>
|
||||
<el-descriptions-item label="直属领导">{{statusData.leader || "--"}}</el-descriptions-item>
|
||||
<el-descriptions-item label="邮箱">{{statusData.email || "--"}}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-dialog>
|
||||
</template>
|
||||
</guava>
|
||||
</div>
|
||||
<script nonce="${cspNonce!}">
|
||||
new Vue({
|
||||
el: "#app",
|
||||
mixins: [initTableMixins],
|
||||
data() {
|
||||
// 分页、查询和 loading 复用 initTableMixins,只声明本业务的选择与状态弹框数据。
|
||||
return {selectedRows: [], statusDialogVisible: false, statusData: null}
|
||||
},
|
||||
methods: {
|
||||
selectionChange(rows) {
|
||||
this.$set(this, "selectedRows", rows)
|
||||
},
|
||||
// userIds 为本系统主键;逐人 success=true/false/null 对应成功、失败、待核实。
|
||||
syncSelected() {
|
||||
if (this.formLoading || this.tableLoading) return
|
||||
if (!this.selectedRows.length) {
|
||||
this.$message.warning("请先选择需要同步的员工")
|
||||
return
|
||||
}
|
||||
const userIds = this.selectedRows.map((row) => row.id)
|
||||
this.$confirm("确定同步选中的 " + userIds.length + " 名员工到华住吗?", "提示", {type: "warning"}).then(() => {
|
||||
if (this.formLoading) return
|
||||
this.$set(this, "formLoading", true)
|
||||
return this.$axios.post("/platform/huazhu/syncUsers", {userIds: userIds}).then((res) => {
|
||||
if (res.code !== 0) {
|
||||
this.$message.warning(res.msg || "同步失败")
|
||||
return
|
||||
}
|
||||
const results = res.data || []
|
||||
const successCount = results.filter((item) => item.success === true).length
|
||||
const failedCount = results.filter((item) => item.success === false).length
|
||||
const pendingCount = results.length - successCount - failedCount
|
||||
const summary = "成功 " + successCount + " 人,失败 " + failedCount + " 人,待核实 " + pendingCount + " 人"
|
||||
if (failedCount || pendingCount) {
|
||||
this.$message.warning(summary)
|
||||
const details = results.filter((item) => item.success !== true).map((item) => {
|
||||
return (item.userName || "员工") + "(" + (item.loginName || item.userId) + "):" + item.message
|
||||
}).join("\n")
|
||||
this.$alert(details, "同步结果明细").catch(() => {})
|
||||
} else {
|
||||
this.$message.success(summary)
|
||||
}
|
||||
}).finally(() => {
|
||||
this.$set(this, "formLoading", false)
|
||||
}).catch(() => {
|
||||
// 网络中断无法确认华住是否已处理,提示先查询状态,避免盲目重复同步。
|
||||
this.$message.warning("请求未完成,请先查询华住状态再决定是否重试")
|
||||
})
|
||||
}).catch(() => {
|
||||
// 取消确认不发送同步请求。
|
||||
})
|
||||
},
|
||||
// userId 是本系统主键;成功 data 为该员工的华住信息,statusCode 按原值展示。
|
||||
queryStatus(row) {
|
||||
if (this.tableLoading || this.formLoading) return
|
||||
this.$set(this, "tableLoading", true)
|
||||
return this.$axios.post("/platform/huazhu/queryStatus", {userId: row.id}).then((res) => {
|
||||
if (res.code === 0 && res.data) {
|
||||
this.$set(this, "statusData", res.data)
|
||||
this.$set(this, "statusDialogVisible", true)
|
||||
} else {
|
||||
this.$message.warning(res.msg || "查询失败")
|
||||
}
|
||||
}).finally(() => {
|
||||
this.$set(this, "tableLoading", false)
|
||||
}).catch(() => {
|
||||
// 公共 Axios 拦截器提示请求异常,保留页面供用户重试。
|
||||
})
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.pageData().catch(() => {})
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<!--#
|
||||
}
|
||||
#-->
|
||||
@@ -0,0 +1,187 @@
|
||||
<!--#
|
||||
layout("/layouts/platform.html"){
|
||||
#-->
|
||||
<div id="app" v-cloak>
|
||||
<guava ref="guava">
|
||||
<template>
|
||||
<el-card shadow="never">
|
||||
<search @search="doSearch">
|
||||
<search-item label="年度">
|
||||
<el-date-picker :clearable="true" placeholder="请选择年度" style="width: 100%" type="year" v-model="pageForm.year" value-format="yyyy"></el-date-picker>
|
||||
</search-item>
|
||||
<search-item label="标题">
|
||||
<el-input @keyup.enter.native="doSearch" clearable placeholder="请输入标题" v-model="pageForm.title"></el-input>
|
||||
</search-item>
|
||||
</search>
|
||||
</el-card>
|
||||
<el-card class="mt10" shadow="never">
|
||||
<table-tool :app="this">
|
||||
<el-button @click="openAdd" icon="el-icon-plus" size="small" type="primary">新增</el-button>
|
||||
</table-tool>
|
||||
<el-table :data="tableData" :size="tableSize" class="vi-table" v-loading="tableLoading">
|
||||
<el-table-column :index="indexMethod" label="序号" type="index" width="80"></el-table-column>
|
||||
<el-table-column label="标题" min-width="180" prop="title" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="标签类型" prop="tagTypeName" width="130">
|
||||
<template slot-scope="{row}"><el-tag v-if="row.tagTypeName" size="small">{{row.tagTypeName}}</el-tag><span v-else>--</span></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="摘要" min-width="180" prop="summary" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="封面图" width="110">
|
||||
<template slot-scope="{row}"><el-image v-if="row.cover" :preview-src-list="[row.cover]" :src="row.cover" fit="cover" style="height: 36px; width: 72px"></el-image><span v-else>--</span></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="创建时间" width="170">
|
||||
<template slot-scope="{row}">{{formatDate(row.createdAt)}}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="发布状态" width="100">
|
||||
<template slot-scope="{row}"><span :class="row.status === 1 ? 'text-success' : 'text-muted'">{{row.statusName}}</span></template>
|
||||
</el-table-column>
|
||||
<el-table-column fixed="right" label="操作" width="220">
|
||||
<template slot-scope="{row}">
|
||||
<el-button @click="openEdit(row)" size="mini" type="primary">编辑</el-button>
|
||||
<el-button @click="changeStatus(row, row.status === 1 ? 0 : 1)" size="mini">{{row.status === 1 ? '下架' : '发布'}}</el-button>
|
||||
<el-button @click="deleteRow(row)" size="mini" type="danger">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<!--#include("/layouts/pagination.html"){}#-->
|
||||
</el-card>
|
||||
</template>
|
||||
<template #edit>
|
||||
<el-form :model="formData" :rules="formRules" label-width="90px" ref="form" v-loading="formLoading">
|
||||
<el-form-item label="标题" prop="title"><el-input maxlength="100" placeholder="请输入标题" v-model="formData.title"></el-input></el-form-item>
|
||||
<el-form-item label="摘要" prop="summary"><el-input maxlength="500" placeholder="请输入摘要" rows="3" type="textarea" v-model="formData.summary"></el-input></el-form-item>
|
||||
<el-form-item label="标签类型" prop="tagType">
|
||||
<el-select placeholder="请先维护标签类型字典" style="width: 100%" v-model="formData.tagType"><el-option :key="item.code" :label="item.label" :value="item.code" v-for="item in dict.type.INCLUSIVE_BENEFIT_TYPE || []"></el-option></el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="内容" prop="content"><text-editor v-model="formData.content"></text-editor></el-form-item>
|
||||
<el-form-item label="性别限制" prop="sexLimit"><el-radio-group v-model="formData.sexLimit"><el-radio :label="0">全部开放</el-radio><el-radio :label="1">男性</el-radio><el-radio :label="2">女性</el-radio></el-radio-group></el-form-item>
|
||||
<el-form-item label="封面图" prop="cover">
|
||||
<file-upload :upload_number="1" :value.sync="formData.cover" upload_mode="image" upload_result_category="interval" upload_result_type="url"></file-upload>
|
||||
</el-form-item>
|
||||
<el-form-item label="链接" prop="linkUrl"><el-input maxlength="500" placeholder="请输入外链地址" v-model="formData.linkUrl"></el-input></el-form-item>
|
||||
<el-form-item><el-button @click="back">取消</el-button><el-button :loading="formLoading" @click="saveForm(0)" type="primary">保存草稿</el-button><el-button :loading="formLoading" @click="saveForm(1)" type="primary">发布</el-button></el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
</guava>
|
||||
</div>
|
||||
<script nonce="${cspNonce!}">
|
||||
new Vue({
|
||||
el: "#app",
|
||||
dicts: ["INCLUSIVE_BENEFIT_TYPE"],
|
||||
mixins: [initTableMixins],
|
||||
data() {
|
||||
return {
|
||||
// 公共分页参数、列表请求及加载状态由 initTableMixins 提供,仅补充业务筛选项。
|
||||
pageForm: {year: "", title: ""},
|
||||
formRules: {
|
||||
title: [{required: true, message: "请输入标题", trigger: "blur"}],
|
||||
summary: [{required: true, message: "请输入摘要", trigger: "blur"}],
|
||||
tagType: [{required: true, message: "请选择标签类型", trigger: "change"}],
|
||||
content: [{required: true, message: "请输入内容", trigger: "change"}],
|
||||
cover: [{required: true, message: "请上传封面图", trigger: "change"}]
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 新内容归入查询年度;未选年度时使用当前年度,性别配置沿用原模块默认值。
|
||||
emptyForm() {
|
||||
return {id: "", year: this.pageForm.year || new Date().getFullYear() + "", title: "",
|
||||
summary: "", tagType: "", content: "", sexLimit: 0, cover: "", linkUrl: ""}
|
||||
},
|
||||
formatDate(value) {
|
||||
return value ? moment(value).format("YYYY-MM-DD HH:mm") : "--"
|
||||
},
|
||||
back() {
|
||||
this.$refs.guava.index()
|
||||
},
|
||||
openAdd() {
|
||||
this.$set(this, "formData", this.emptyForm())
|
||||
this.$refs.guava.edit(() => this.$refs.form.clearValidate())
|
||||
},
|
||||
// id 为所选内容主键;成功响应 data 为详情对象,回显时将年度转换为表单字符串。
|
||||
openEdit(row) {
|
||||
if (this.formLoading) return
|
||||
this.$set(this, "formLoading", true)
|
||||
this.$axios.post("/platform/inclusive/findOne", {id: row.id}).then((res) => {
|
||||
if (res.code === 0 && res.data) {
|
||||
const formData = Object.assign(this.emptyForm(), res.data)
|
||||
this.$set(formData, "year", formData.year == null ? "" : formData.year + "")
|
||||
this.$set(this, "formData", formData)
|
||||
this.$refs.guava.edit(() => this.$refs.form.clearValidate())
|
||||
} else {
|
||||
this.$message.warning(res.msg || "未查询到数据")
|
||||
}
|
||||
}).finally(() => {
|
||||
this.$set(this, "formLoading", false)
|
||||
}).catch(() => {
|
||||
// 公共 Axios 拦截器已展示失败原因;保留当前页面供用户重试。
|
||||
})
|
||||
},
|
||||
// status=0 保存草稿,status=1 发布;code=0 表示成功,返回列表后复用 mixin 刷新查询。
|
||||
saveForm(status) {
|
||||
if (this.formLoading) return
|
||||
this.$refs.form.validate((valid) => {
|
||||
if (!valid) return
|
||||
this.$set(this, "formLoading", true)
|
||||
const params = Object.assign({}, this.formData, {status: status})
|
||||
this.$axios.post("/platform/inclusive/save", params).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.$message.success("保存成功")
|
||||
this.$refs.guava.index()
|
||||
this.doSearch()
|
||||
} else {
|
||||
this.$message.warning(res.msg || "保存失败")
|
||||
}
|
||||
}).finally(() => {
|
||||
this.$set(this, "formLoading", false)
|
||||
}).catch(() => {
|
||||
// 保存失败保留表单,错误提示由公共 Axios 拦截器负责。
|
||||
})
|
||||
})
|
||||
},
|
||||
// 发布/下架只提交 id 和目标 status;确认取消不会发送写请求。
|
||||
changeStatus(row, status) {
|
||||
if (this.formLoading) return
|
||||
this.$confirm("确定要" + (status === 1 ? "发布" : "下架") + "该幸福工荟吗?", "提示", {type: "warning"}).then(() => {
|
||||
this.$set(this, "formLoading", true)
|
||||
this.$axios.post("/platform/inclusive/updateStatus", {id: row.id, status: status}).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.$message.success("操作成功")
|
||||
this.doSearch()
|
||||
} else {
|
||||
this.$message.warning(res.msg || "操作失败")
|
||||
}
|
||||
}).finally(() => {
|
||||
this.$set(this, "formLoading", false)
|
||||
}).catch(() => {
|
||||
// 发布或下架失败时保留现有列表状态。
|
||||
})
|
||||
}).catch(() => {})
|
||||
},
|
||||
// 删除仅提交内容主键,后端执行逻辑删除;成功后刷新分页总数。
|
||||
deleteRow(row) {
|
||||
if (this.formLoading) return
|
||||
this.$confirm("确定要删除该幸福工荟吗?", "提示", {type: "warning"}).then(() => {
|
||||
this.$set(this, "formLoading", true)
|
||||
this.$axios.post("/platform/inclusive/delete", {id: row.id}).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.$message.success("删除成功")
|
||||
this.doSearch()
|
||||
} else {
|
||||
this.$message.warning(res.msg || "删除失败")
|
||||
}
|
||||
}).finally(() => {
|
||||
this.$set(this, "formLoading", false)
|
||||
}).catch(() => {
|
||||
// 删除失败时保留当前记录,公共拦截器已提示原因。
|
||||
})
|
||||
}).catch(() => {})
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.pageData()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<!--#
|
||||
}
|
||||
#-->
|
||||
@@ -54,6 +54,7 @@ layout("/layouts/platform.html"){
|
||||
|
||||
<el-card shadow="never" class="mt10">
|
||||
<table-tool label="基金会员">
|
||||
<el-button @click="exportChangeRecord" size="small" icon="el-icon-download" type="primary">导出</el-button>
|
||||
</table-tool>
|
||||
|
||||
<el-table :data="tableData" :size="tableSize" @sort-change="pageOrder"
|
||||
@@ -71,7 +72,11 @@ layout("/layouts/platform.html"){
|
||||
show-overflow-tooltip
|
||||
v-for="column in tableColumns"
|
||||
>
|
||||
<template v-slot="{row}" v-if="column.prop=='changeType'">
|
||||
<template v-slot="{row}" v-if="column.prop=='aidFundMemberUserType'">
|
||||
<dict-tag :options="dict.type.AIDFUND_MEMBER_USER_TYPE"
|
||||
:value="row.aidFundMemberUserType"></dict-tag>
|
||||
</template>
|
||||
<template v-slot="{row}" v-else-if="column.prop=='changeType'">
|
||||
<dict-tag :options="dict.type.AIDFUND_MEMBER_CHANGE_TYPE"
|
||||
:value="row.changeType"></dict-tag>
|
||||
</template>
|
||||
@@ -131,6 +136,10 @@ layout("/layouts/platform.html"){
|
||||
'info': APPLY_INFO,
|
||||
},
|
||||
methods: {
|
||||
exportChangeRecord() {
|
||||
// 传入页面筛选和排序参数,服务端按相同条件导出全部匹配记录。
|
||||
this.$downLoad(loc() + "/exportChangeRecord", this.pageForm)
|
||||
},
|
||||
onView(row) {
|
||||
this.$refs.guava.view(() => {
|
||||
this.$refs.infoRef.onOpen(row)
|
||||
|
||||
@@ -11,9 +11,14 @@ layout("/layouts/platform.html"){
|
||||
justify-content: center;
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
#app .calculate-money-card > .card-body,
|
||||
#calcSubsidyTable .el-table__body-wrapper {
|
||||
overscroll-behavior-y: contain;
|
||||
}
|
||||
</style>
|
||||
<div id="app">
|
||||
<custom-card>
|
||||
<custom-card class="calculate-money-card" ref="calculateCard" @hook:mounted="initTableScroll">
|
||||
<div id="calcSubsidyTable" v-loading="formLoading">
|
||||
<div class="process-title">1.输入金额(元)</div>
|
||||
<el-row :gutter="20">
|
||||
@@ -129,7 +134,7 @@ layout("/layouts/platform.html"){
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-table :data="applyListData" ref="table" row-key="id" style="width: 100%"
|
||||
:summary-method="getSummaries" show-summary>
|
||||
:height="tableHeight" :summary-method="getSummaries" show-summary>
|
||||
<el-table-column type="index" width="50px" label="序号"
|
||||
fixed="left"></el-table-column>
|
||||
<el-table-column
|
||||
@@ -139,6 +144,7 @@ layout("/layouts/platform.html"){
|
||||
:width="column.width"
|
||||
:fixed="column.fixed"
|
||||
:sortable="column.sortable"
|
||||
:formatter="formatSubsidyMoney"
|
||||
show-overflow-tooltip
|
||||
sortable
|
||||
v-for="column in tableColumns"
|
||||
@@ -191,6 +197,7 @@ layout("/layouts/platform.html"){
|
||||
ratio: 60,
|
||||
medicalSetting: {},
|
||||
applyListData: [],
|
||||
tableHeight: null,
|
||||
tableSummaries: [],
|
||||
tableColumns: [
|
||||
{prop: 'loginname', label: '工号', fixed: "left"},
|
||||
@@ -211,10 +218,16 @@ layout("/layouts/platform.html"){
|
||||
{prop: 'bxyMoney$', label: '非住院肾衰竭治疗、靶向药金额', width: "120"},
|
||||
{prop: 'totalMoney', label: '合计金额', width: "120"},
|
||||
{prop: 'loveSubsidyMoney', label: '爱心补助', width: "120"},
|
||||
{prop: 'forecastSubsidyMoney', label: '当前补助金额', width: "120"},
|
||||
{prop: 'subsidyMoney', label: '当前补助金额', width: "120"},
|
||||
],
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
applyListData() {
|
||||
// 空列表切换为有数据时也重排固定列和合计,避免底部合计与数据行重叠。
|
||||
this.$nextTick(() => this.updateTableHeight())
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
|
||||
//申请费用总额
|
||||
@@ -245,7 +258,72 @@ layout("/layouts/platform.html"){
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async grantOrIntoChange(key, val) {
|
||||
initTableScroll() {
|
||||
this.$nextTick(() => {
|
||||
const card = this.$refs.calculateCard
|
||||
const scrollBody = card && card.$el.querySelector('.card-body')
|
||||
if (!scrollBody || !this.$refs.table || this._calculateScrollBody === scrollBody) {
|
||||
return
|
||||
}
|
||||
// custom-card 异步挂载后再绑定,滚动仅限当前卡片;销毁时解除监听。
|
||||
this._calculateScrollBody = scrollBody
|
||||
scrollBody.addEventListener('wheel', this.handleTableWheel, {passive: false})
|
||||
window.addEventListener('resize', this.updateTableHeight)
|
||||
if (window.ResizeObserver) {
|
||||
this._calculateResizeObserver = new ResizeObserver(() => this.updateTableHeight())
|
||||
this._calculateResizeObserver.observe(scrollBody)
|
||||
}
|
||||
this.updateTableHeight()
|
||||
})
|
||||
},
|
||||
updateTableHeight() {
|
||||
const scrollBody = this._calculateScrollBody
|
||||
const table = this.$refs.table
|
||||
if (!scrollBody || !table) {
|
||||
return
|
||||
}
|
||||
// 列表占满卡片可用高度,表头、合计和底部操作按钮保留在可见区域。
|
||||
const style = window.getComputedStyle(scrollBody)
|
||||
const height = Math.max(0, Math.floor(scrollBody.clientHeight
|
||||
- parseFloat(style.paddingTop) - parseFloat(style.paddingBottom)))
|
||||
if (this.tableHeight !== height) {
|
||||
this.$set(this, 'tableHeight', height)
|
||||
}
|
||||
this.$nextTick(() => table.doLayout())
|
||||
},
|
||||
handleTableWheel(event) {
|
||||
const scrollBody = this._calculateScrollBody
|
||||
const table = this.$refs.table
|
||||
if (!scrollBody || !table || !table.bodyWrapper || event.ctrlKey
|
||||
|| Math.abs(event.deltaX) > Math.abs(event.deltaY) || !event.deltaY) {
|
||||
return
|
||||
}
|
||||
const style = window.getComputedStyle(scrollBody)
|
||||
const tableTop = scrollBody.scrollTop + table.$el.getBoundingClientRect().top
|
||||
- scrollBody.getBoundingClientRect().top - scrollBody.clientTop - parseFloat(style.paddingTop)
|
||||
const outerLimit = Math.max(0, Math.min(tableTop, scrollBody.scrollHeight - scrollBody.clientHeight))
|
||||
const listBody = table.bodyWrapper
|
||||
const innerLimit = Math.max(0, listBody.scrollHeight - listBody.clientHeight)
|
||||
const unit = event.deltaMode === 1 ? 16 : (event.deltaMode === 2 ? scrollBody.clientHeight : 1)
|
||||
let delta = event.deltaY * unit
|
||||
event.preventDefault()
|
||||
if (delta > 0) {
|
||||
// 先将列表滚到卡片顶部,再把同次滚轮的剩余距离交给列表内容。
|
||||
const outerMove = Math.min(delta, Math.max(0, outerLimit - scrollBody.scrollTop))
|
||||
scrollBody.scrollTop += outerMove
|
||||
delta -= outerMove
|
||||
listBody.scrollTop = Math.min(innerLimit, listBody.scrollTop + delta)
|
||||
} else {
|
||||
// 反向滚动先回到列表第一行,再返回上方金额和比例区域;边界不带动整页。
|
||||
if (scrollBody.scrollTop >= outerLimit - 1) {
|
||||
const innerMove = Math.min(-delta, listBody.scrollTop)
|
||||
listBody.scrollTop -= innerMove
|
||||
delta += innerMove
|
||||
}
|
||||
scrollBody.scrollTop = Math.max(0, scrollBody.scrollTop + delta)
|
||||
}
|
||||
},
|
||||
grantOrIntoChange(key, val) {
|
||||
if (val === '') {
|
||||
if (key === 'grants') {
|
||||
this.calcData.schoolGrants = 0
|
||||
@@ -253,7 +331,11 @@ layout("/layouts/platform.html"){
|
||||
this.calcData.unionInto = 0
|
||||
}
|
||||
}
|
||||
const resp = await this.$axios.post(loc() + '/setGrantsOrUnionInto', {keys: key, value: val})
|
||||
return this.$axios.post(loc() + '/setGrantsOrUnionInto', {keys: key, value: val})
|
||||
},
|
||||
// 已保存补助去掉末尾的零;历史非零小数保持真实值,需完成分配后才变为整数。
|
||||
formatSubsidyMoney(row, column, value) {
|
||||
return column.property === 'subsidyMoney' && value != null && value !== '' ? Number(value) : value
|
||||
},
|
||||
getSummaries(param) {
|
||||
const {columns, data} = param
|
||||
@@ -266,7 +348,13 @@ layout("/layouts/platform.html"){
|
||||
const values = data.map(i => Number(i[column.property]))
|
||||
if (values && values.length > 0) {
|
||||
const everyColumnSum = values.reduce((a, b) => a + b)
|
||||
sums[index] = isNaN(everyColumnSum) ? 0 : everyColumnSum.toFixed(2)
|
||||
// 测算合计显示整数;已保存合计仅去掉末尾零,保留历史金额中的真实角分。
|
||||
if (column.property === 'subsidyMoney') {
|
||||
sums[index] = isNaN(everyColumnSum) ? 0 : Number(everyColumnSum.toFixed(2))
|
||||
} else {
|
||||
const digits = column.property === 'forecastSubsidyMoney' ? 0 : 2
|
||||
sums[index] = isNaN(everyColumnSum) ? 0 : everyColumnSum.toFixed(digits)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -279,13 +367,18 @@ layout("/layouts/platform.html"){
|
||||
ratio: this.ratio})
|
||||
},
|
||||
doHandle() {
|
||||
this.$confirm('确定要按此比例来补助吗?', '提示', {
|
||||
return this.$confirm('确定要按此比例来补助吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
this.$axios.post("/platform/medicalMutualAid/medical/calculateMoney/doHandle", {
|
||||
data: JSON.stringify(this.applyListData),
|
||||
// 仅提交本次分配所需的申请标识和金额,保存及刷新结束后统一解除按钮加载状态。
|
||||
this.formLoading = true
|
||||
return this.$axios.post("/platform/medicalMutualAid/medical/calculateMoney/doHandle", {
|
||||
data: JSON.stringify(this.applyListData.map(row => ({
|
||||
id: row.id,
|
||||
forecastSubsidyMoney: row.forecastSubsidyMoney
|
||||
}))),
|
||||
ratio: this.ratio,
|
||||
aidFund: this.calcData.aidFundMoney,
|
||||
inComeSum: this.inComeSum,
|
||||
@@ -293,28 +386,28 @@ layout("/layouts/platform.html"){
|
||||
}).then(res => {
|
||||
if (res.code === 0) {
|
||||
this.$message.success("操作成功")
|
||||
this.getData()
|
||||
this.getApplyData()
|
||||
return this.getData()
|
||||
}
|
||||
}).finally(() => {
|
||||
this.formLoading = false
|
||||
})
|
||||
|
||||
})
|
||||
},
|
||||
async getMedicalSetting() {
|
||||
try {
|
||||
const res = await this.$axios.post("/platform/medicalMutualAid/medical/basicSetting/getSetting")
|
||||
getMedicalSetting() {
|
||||
return this.$axios.post("/platform/medicalMutualAid/medical/basicSetting/getSetting").then(res => {
|
||||
if (res.code === 0) {
|
||||
this.medicalSetting = res.data
|
||||
}
|
||||
} catch (e) {
|
||||
}).catch(e => {
|
||||
this.$message.error(e.message)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
},
|
||||
getData() {
|
||||
this.formLoading = true
|
||||
this.$axios.post("/platform/medicalMutualAid/medical/calculateMoney/getData").then(res => {
|
||||
return this.$axios.post("/platform/medicalMutualAid/medical/calculateMoney/getData").then(res => {
|
||||
if (res.code === 0) {
|
||||
if (res.data.ratio) {
|
||||
this.ratio = res.data.ratio
|
||||
@@ -322,14 +415,14 @@ layout("/layouts/platform.html"){
|
||||
this.ratio = this.medicalSetting.subsidyRatio
|
||||
}
|
||||
this.calcData = res.data
|
||||
this.getApplyData()
|
||||
return this.getApplyData()
|
||||
}
|
||||
}).finally(()=>{
|
||||
this.formLoading=false
|
||||
})
|
||||
},
|
||||
getApplyData() {
|
||||
this.$axios.post("/platform/medicalMutualAid/medical/calculateMoney/getApplyData", {ratio: this.ratio}).then(res => {
|
||||
return this.$axios.post("/platform/medicalMutualAid/medical/calculateMoney/getApplyData", {ratio: this.ratio}).then(res => {
|
||||
if (res.code === 0) {
|
||||
this.applyListData = res.data
|
||||
}
|
||||
@@ -337,9 +430,20 @@ layout("/layouts/platform.html"){
|
||||
|
||||
},
|
||||
},
|
||||
async created() {
|
||||
await this.getMedicalSetting()
|
||||
this.getData()
|
||||
created() {
|
||||
this.getMedicalSetting().then(() => this.getData())
|
||||
},
|
||||
mounted() {
|
||||
this.initTableScroll()
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this._calculateScrollBody) {
|
||||
this._calculateScrollBody.removeEventListener('wheel', this.handleTableWheel)
|
||||
}
|
||||
if (this._calculateResizeObserver) {
|
||||
this._calculateResizeObserver.disconnect()
|
||||
}
|
||||
window.removeEventListener('resize', this.updateTableHeight)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -2,9 +2,62 @@
|
||||
layout("/layouts/platform.html"){
|
||||
#-->
|
||||
|
||||
<style>
|
||||
/* 查询区保持可见,申请列表使用页面剩余高度。 */
|
||||
.medical-statistics-layout {
|
||||
height: calc(100vh - 84px);
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.medical-statistics-layout > .transition-item {
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.medical-statistics-layout .medical-statistics-search-card {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.medical-statistics-layout .medical-statistics-table-card {
|
||||
flex: 1 1 0;
|
||||
min-height: 0;
|
||||
margin-top: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.medical-statistics-layout .medical-statistics-table-card > .el-card__body {
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.medical-statistics-layout .medical-statistics-table-card .ele-table-tool {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.medical-statistics-layout .medical-statistics-table-container {
|
||||
flex: 1 1 0;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.medical-statistics-layout .medical-statistics-main-table .el-table__body-wrapper {
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="app">
|
||||
<guava ref="guava">
|
||||
<el-card shadow="never">
|
||||
<guava ref="guava" class="medical-statistics-layout"
|
||||
@hook:mounted="initTableLayout" @vchange="initTableLayout">
|
||||
<el-card shadow="never" class="medical-statistics-search-card">
|
||||
<search @search="doSearch">
|
||||
<search-item label="年度">
|
||||
<el-date-picker
|
||||
@@ -53,7 +106,7 @@ layout("/layouts/platform.html"){
|
||||
</search>
|
||||
</el-card>
|
||||
|
||||
<el-card shadow="never">
|
||||
<el-card shadow="never" class="medical-statistics-table-card">
|
||||
<table-tool label="申请列表">
|
||||
<el-dropdown @command="doExport">
|
||||
<el-button type="primary" size="small">导出 <i class="el-icon-arrow-down el-icon--right"></i></el-button>
|
||||
@@ -63,47 +116,51 @@ layout("/layouts/platform.html"){
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
</table-tool>
|
||||
<el-table :data="tableData" :size="tableSize" @sort-change="pageOrder"
|
||||
ref="table" row-key="id" style="width: 100%"
|
||||
v-loading="tableLoading"
|
||||
:summary-method="getSummaries" :show-summary="showSummary">
|
||||
<el-table-column type="index" width="50px" label="序号" :index="indexMethod"
|
||||
fixed="left"></el-table-column>
|
||||
<el-table-column
|
||||
:label="column.label"
|
||||
:prop="column.prop"
|
||||
:key="column.prop"
|
||||
:width="column.width"
|
||||
:fixed="column.fixed"
|
||||
:sortable="column.sortable"
|
||||
show-overflow-tooltip
|
||||
sortable
|
||||
v-for="column in tableColumns"
|
||||
>
|
||||
<template v-if="column.prop==='isMajorDiseases'" v-slot="{row}">
|
||||
<span :class="[row.isMajorDiseases?'text-danger':'text-info']">
|
||||
<i class="fa fa-circle ml5"></i>
|
||||
{{row.isMajorDiseases?'是':'否'}}({{row.applyNum}})
|
||||
</span>
|
||||
</template>
|
||||
<template v-slot="{ row }" v-else-if="column.prop === 'instanceState'">
|
||||
<enum-tag :value="row.instanceState" name="ProcessInstanceStateEnum" label_key="message"
|
||||
size="small"></enum-tag>
|
||||
</template>
|
||||
<template v-slot="{ row }" v-else-if="column.prop === 'curTaskName'">
|
||||
{{row.curTaskName?row.curTaskName:row.taskName}}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" fixed="right" width="100">
|
||||
<template slot-scope="{row}">
|
||||
<el-button @click="onView(row)" size="mini" type="primary">查看</el-button>
|
||||
<el-button v-if="row.taskKey === 'startTask' || !row.instanceId" @click="onDelete(row.id)"
|
||||
size="mini" type="danger">
|
||||
删除
|
||||
</el-button>
|
||||
<div ref="tableContainer" class="medical-statistics-table-container">
|
||||
<el-table class="medical-statistics-main-table" :height="tableHeight"
|
||||
:data="tableData" :size="tableSize" @sort-change="pageOrder"
|
||||
ref="table" row-key="id" style="width: 100%"
|
||||
v-loading="tableLoading"
|
||||
:summary-method="getSummaries" :show-summary="showSummary">
|
||||
<el-table-column type="index" width="50px" label="序号" :index="indexMethod"
|
||||
fixed="left"></el-table-column>
|
||||
<el-table-column
|
||||
:label="column.label"
|
||||
:prop="column.prop"
|
||||
:key="column.prop"
|
||||
:width="column.width"
|
||||
:fixed="column.fixed"
|
||||
:sortable="column.sortable"
|
||||
:formatter="formatSubsidyMoney"
|
||||
show-overflow-tooltip
|
||||
sortable
|
||||
v-for="column in tableColumns"
|
||||
>
|
||||
<template v-if="column.prop==='isMajorDiseases'" v-slot="{row}">
|
||||
<span :class="[row.isMajorDiseases?'text-danger':'text-info']">
|
||||
<i class="fa fa-circle ml5"></i>
|
||||
{{row.isMajorDiseases?'是':'否'}}({{row.applyNum}})
|
||||
</span>
|
||||
</template>
|
||||
<template v-slot="{ row }" v-else-if="column.prop === 'instanceState'">
|
||||
<enum-tag :value="row.instanceState" name="ProcessInstanceStateEnum" label_key="message"
|
||||
size="small"></enum-tag>
|
||||
</template>
|
||||
<template v-slot="{ row }" v-else-if="column.prop === 'curTaskName'">
|
||||
{{row.curTaskName?row.curTaskName:row.taskName}}
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-table-column label="操作" fixed="right" width="100">
|
||||
<template slot-scope="{row}">
|
||||
<el-button @click="onView(row)" size="mini" type="primary">查看</el-button>
|
||||
<el-button v-if="row.taskKey === 'startTask' || !row.instanceId" @click="onDelete(row.id)"
|
||||
size="mini" type="danger">
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<template #edit>
|
||||
@@ -276,13 +333,18 @@ layout("/layouts/platform.html"){
|
||||
pageSize: 9999,
|
||||
isMajorDiseases: null
|
||||
},
|
||||
medicalSetting: {}
|
||||
medicalSetting: {},
|
||||
tableHeight: 300
|
||||
}
|
||||
},
|
||||
components: {
|
||||
"medical-info": MEDICAL_INFO
|
||||
},
|
||||
watch: {
|
||||
// 年度切换可能使合计行从隐藏变为显示,待数据渲染后重新测量表头、合计行和固定列。
|
||||
tableData() {
|
||||
this.$nextTick(() => this.updateTableHeight())
|
||||
},
|
||||
"localPageForm": {
|
||||
handler(val) {
|
||||
this.pageForm = {...this.pageForm, ...val}
|
||||
@@ -291,6 +353,34 @@ layout("/layouts/platform.html"){
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
// guava 异步挂载后才存在列表容器;重复调用时复用监听,避免首次进入漏掉初始化。
|
||||
initTableLayout() {
|
||||
this.$nextTick(() => {
|
||||
const container = this.$refs.tableContainer
|
||||
if (!container || !this.$refs.table) return
|
||||
|
||||
if (!this._tableResizeObserver) {
|
||||
this._tableResizeObserver = new ResizeObserver(() => this.updateTableHeight())
|
||||
this._tableResizeObserver.observe(container)
|
||||
}
|
||||
this.updateTableHeight()
|
||||
})
|
||||
},
|
||||
// 独立容器占用工具栏下方的剩余空间,隐藏列表时保留高度,返回列表后再刷新布局。
|
||||
updateTableHeight() {
|
||||
const container = this.$refs.tableContainer
|
||||
const table = this.$refs.table
|
||||
if (!container || !table || container.clientHeight <= 0) return
|
||||
|
||||
const height = container.clientHeight
|
||||
if (this.tableHeight !== height) {
|
||||
this.$set(this, "tableHeight", height)
|
||||
}
|
||||
// 空数据变为有数据时高度可能不变,仍需重算合计行占用空间。
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.table) this.$refs.table.doLayout()
|
||||
})
|
||||
},
|
||||
onDelete(id) {
|
||||
this.$confirm("您确定要删除吗?", "提示", {
|
||||
confirmButtonText: "确定",
|
||||
@@ -305,6 +395,10 @@ layout("/layouts/platform.html"){
|
||||
})
|
||||
})
|
||||
},
|
||||
// 整数补助不显示 .00;尚未重新分配的历史小数保留原值,避免显示金额与数据库不符。
|
||||
formatSubsidyMoney(row, column, value) {
|
||||
return column.property === 'subsidyMoney' && value != null && value !== '' ? Number(value) : value
|
||||
},
|
||||
getSummaries(param) {
|
||||
const {columns, data} = param
|
||||
const sums = []
|
||||
@@ -314,7 +408,9 @@ layout("/layouts/platform.html"){
|
||||
const values = data.map(i => Number(i[column.property]))
|
||||
if (values && values.length > 0) {
|
||||
const everyColumnSum = values.reduce((a, b) => a + b)
|
||||
sums[index] = isNaN(everyColumnSum) ? 0 : everyColumnSum.toFixed(2)
|
||||
// 仅补助合计去掉末尾零;爱心补助和医疗费用仍按两位小数显示。
|
||||
const total = isNaN(everyColumnSum) ? 0 : everyColumnSum.toFixed(2)
|
||||
sums[index] = column.property === 'subsidyMoney' ? Number(total) : total
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -403,6 +499,14 @@ layout("/layouts/platform.html"){
|
||||
this.getDiseaseList()
|
||||
this.unionList = await this.$businessTool.listUnion()
|
||||
this.unitList = await this.$businessTool.listUnit()
|
||||
},
|
||||
mounted() {
|
||||
this.initTableLayout()
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this._tableResizeObserver) {
|
||||
this._tableResizeObserver.disconnect()
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -29,6 +29,11 @@ layout("/layouts/platform.html"){
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.pending-list-toolbar-actions .pending-import-upload {
|
||||
display: inline-block;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
/* 在列表标题旁说明待确认人员的来源及可能情况。 */
|
||||
.pending-confirm-tip {
|
||||
color: #f56c6c;
|
||||
@@ -128,6 +133,16 @@ layout("/layouts/platform.html"){
|
||||
<el-button class="pending-batch-change-button" type="primary"
|
||||
icon="el-icon-download" size="small"
|
||||
@click="exportExcel">导出</el-button>
|
||||
<el-upload class="pending-import-upload"
|
||||
v-if="pageForm.pendingConfirmStatus === 1"
|
||||
action="#"
|
||||
accept=".xls,.xlsx"
|
||||
:show-file-list="false"
|
||||
:before-upload="validateImportFile"
|
||||
:http-request="importChange">
|
||||
<el-button type="primary" icon="el-icon-upload2" size="small"
|
||||
:loading="importLoading" :disabled="importLoading">导入变更</el-button>
|
||||
</el-upload>
|
||||
<el-button class="pending-batch-change-button" type="primary"
|
||||
icon="el-icon-edit" size="small"
|
||||
v-if="pageForm.pendingConfirmStatus === 1"
|
||||
@@ -275,6 +290,7 @@ layout("/layouts/platform.html"){
|
||||
selectedPendingUsers: [],
|
||||
batchDialogVisible: false,
|
||||
batchSubmitting: false,
|
||||
importLoading: false,
|
||||
batchChangeForm: {
|
||||
personType: "",
|
||||
userState: "",
|
||||
@@ -378,6 +394,33 @@ layout("/layouts/platform.html"){
|
||||
exportExcel() {
|
||||
this.$downLoad("/platform/sourcechange/pending/exportExcel", this.buildQueryPageForm())
|
||||
},
|
||||
/** 只接收 Excel 文件,避免无效文件进入后端解析。 */
|
||||
validateImportFile(file) {
|
||||
const fileName = file && file.name ? file.name.toLowerCase() : ""
|
||||
const valid = fileName.endsWith(".xls") || fileName.endsWith(".xlsx")
|
||||
if (!valid) {
|
||||
this.$message.warning("只能上传xls或xlsx文件")
|
||||
}
|
||||
return valid
|
||||
},
|
||||
/** 上传填写了编码的列表文件,后端仅按工号和编码执行字典映射变更。 */
|
||||
importChange(uploadRequest) {
|
||||
const formData = new FormData()
|
||||
formData.append("file", uploadRequest.file, uploadRequest.file.name)
|
||||
this.importLoading = true
|
||||
this.$axios.post("/platform/sourcechange/pending/importChange", formData).then((resp) => {
|
||||
if (resp.code === 0) {
|
||||
this.$message.success(resp.msg)
|
||||
this.doSearch()
|
||||
} else {
|
||||
this.$message.error(resp.msg)
|
||||
}
|
||||
}).catch(() => {
|
||||
this.$message.error("导入变更失败,请稍后重试")
|
||||
}).finally(() => {
|
||||
this.importLoading = false
|
||||
})
|
||||
},
|
||||
/** 保存当前页勾选的待确认人员,供批量变更使用。 */
|
||||
handleSelectionChange(selection) {
|
||||
this.selectedPendingUsers = (selection || []).filter((item) => item.pendingConfirmStatus === 1)
|
||||
|
||||
@@ -29,7 +29,7 @@ layout("/layouts/platform_h5.html"){
|
||||
.club-apply-actions__agree .van-checkbox__icon { margin-top: 2px; }
|
||||
.club-apply-actions__agree .van-checkbox__label { color: #65778e; font-size: 12px; line-height: 18px; }
|
||||
.club-apply-actions__buttons { display: flex; gap: 10px; }
|
||||
.club-apply-actions .van-button { flex: 1; height: 40px; border-radius: 8px; font-size: 14px; font-weight: 500; }
|
||||
.club-apply-actions .van-button { flex: 1; height: 40px; border-radius: 20px; font-size: 14px; font-weight: 500; }
|
||||
.club-apply-actions .van-button--info { background: var(--club-primary); border-color: var(--club-primary); }
|
||||
.club-apply-actions .van-button--info.van-button--plain { color: var(--club-primary); background: #fff; }
|
||||
</style>
|
||||
@@ -41,7 +41,7 @@ layout("/layouts/platform_h5.html"){
|
||||
<section class="club-apply-section">
|
||||
<h2 class="club-apply-section__title">基本信息</h2>
|
||||
<van-cell-group :border="false">
|
||||
<van-field :rules="[{ required: true, message: '请选择社团' }]" v-model="formData.clubName" label="社团" placeholder="请选择社团" required is-link readonly name="clubName" @click="openClubPopup"></van-field>
|
||||
<van-field :rules="[{ required: true, message: '请从社团列表选择社团后进入申请' }]" v-model="formData.clubName" label="社团" placeholder="从社团列表带入" required readonly name="clubName"></van-field>
|
||||
<van-field label="姓名" :rules="[{ required: true }]" v-model="formData.userName" readonly required name="userName"></van-field>
|
||||
<van-field label="工号" :rules="[{ required: true }]" v-model="formData.loginName" readonly required name="loginName"></van-field>
|
||||
<van-field label="性别" :rules="[{ required: true }]" v-model="formData.sex" readonly required></van-field>
|
||||
@@ -98,10 +98,6 @@ layout("/layouts/platform_h5.html"){
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<van-popup v-model="showClubPopup" position="bottom" get-container="#app" @click-overlay="closeClubPopup">
|
||||
<van-picker show-toolbar :columns="clubOptions.map((item) => item.clubName)" @confirm="onClubConfirm" @cancel="closeClubPopup"></van-picker>
|
||||
</van-popup>
|
||||
|
||||
<van-dialog :value="confirmVisible" title="提示" :message="confirmMessage" show-cancel-button @confirm="confirmPendingAction" @cancel="closeConfirm"></van-dialog>
|
||||
</div>
|
||||
|
||||
@@ -118,7 +114,6 @@ layout("/layouts/platform_h5.html"){
|
||||
clubId: "", clubName: "", userName: "", loginName: "", sex: "", birthday: "", mobile: "", email: "", unionName: "", unitName: "", position: "", technicalTitle: "", education: "", academicDegree: "", sameTimeJoinOtherClubSituation: "", awardsExperience: "", avatar: "", signature: ""
|
||||
},
|
||||
clubOptions: [],
|
||||
showClubPopup: false,
|
||||
isAgree: false,
|
||||
formLoading: false,
|
||||
confirmVisible: false,
|
||||
@@ -136,7 +131,6 @@ layout("/layouts/platform_h5.html"){
|
||||
if (manager) {
|
||||
manager.ensureRegistered('club-apply');
|
||||
this.historyLayerUnsubscribe = manager.subscribe((layers) => {
|
||||
this.$set(this, 'showClubPopup', layers.includes('club-apply-club-picker'));
|
||||
this.$set(this, 'confirmVisible', layers.includes('club-apply-confirm'));
|
||||
});
|
||||
}
|
||||
@@ -161,25 +155,6 @@ layout("/layouts/platform_h5.html"){
|
||||
}
|
||||
historyBack();
|
||||
},
|
||||
openClubPopup() {
|
||||
const manager = window.h5HistoryLayerManager;
|
||||
if (manager) {
|
||||
manager.open('club-apply-club-picker');
|
||||
return;
|
||||
}
|
||||
this.$set(this, 'showClubPopup', true);
|
||||
},
|
||||
closeClubPopup(afterClose) {
|
||||
const manager = window.h5HistoryLayerManager;
|
||||
if (manager) {
|
||||
manager.close('club-apply-club-picker', afterClose);
|
||||
return;
|
||||
}
|
||||
this.$set(this, 'showClubPopup', false);
|
||||
if (afterClose) {
|
||||
afterClose();
|
||||
}
|
||||
},
|
||||
openConfirm(action) {
|
||||
this.$set(this, 'pendingAction', action);
|
||||
const manager = window.h5HistoryLayerManager;
|
||||
@@ -271,19 +246,20 @@ layout("/layouts/platform_h5.html"){
|
||||
}
|
||||
});
|
||||
},
|
||||
onClubConfirm(value, index) {
|
||||
const club = this.clubOptions[index];
|
||||
if (club) {
|
||||
this.$set(this.formData, 'clubId', club.id);
|
||||
this.$set(this.formData, 'clubName', value);
|
||||
this.checkApplyClub(club.id);
|
||||
}
|
||||
this.closeClubPopup();
|
||||
},
|
||||
listClub() {
|
||||
this.$axios.post('/platform/club/common/listClub').then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.$set(this, 'clubOptions', res.data || []);
|
||||
// 从社团卡片进入新申请时预选社团,编辑已有申请仍使用原记录。
|
||||
const clubId = GetQueryString('clubId');
|
||||
if (!this.bizId && clubId) {
|
||||
const club = this.clubOptions.find((item) => item.id === clubId);
|
||||
if (club) {
|
||||
this.$set(this.formData, 'clubId', club.id);
|
||||
this.$set(this.formData, 'clubName', club.clubName);
|
||||
this.checkApplyClub(club.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
@@ -0,0 +1,349 @@
|
||||
<!--#
|
||||
layout("/layouts/platform_h5.html"){
|
||||
#-->
|
||||
|
||||
<style>
|
||||
<!--#include('../common/clubUserJoin.css'){}#-->
|
||||
.club-directory-page { min-height: 100vh; padding-bottom: calc(24px + env(safe-area-inset-bottom)); box-sizing: border-box; background: #f3f7fd; }
|
||||
.club-directory-page .van-nav-bar { background: #fff; }
|
||||
.club-directory-page .van-nav-bar__title { color: #1f2937; font-size: 16px; }
|
||||
.club-directory-page .van-nav-bar .van-icon, .club-directory-page .van-nav-bar__text { color: #397fbd; }
|
||||
.club-directory-content { padding: 12px 12px 0; }
|
||||
.club-directory-hero { position: relative; height: 134px; overflow: hidden; border-radius: 10px; color: #fff; background: #247fe8; box-shadow: 0 8px 20px rgba(51, 117, 219, .18); }
|
||||
.club-directory-hero__image { position: absolute; top: 0; left: 0; width: 100%; height: 100%; object-fit: cover; object-position: center; }
|
||||
.club-directory-hero__copy { position: relative; z-index: 1; display: flex; width: 64%; height: 100%; padding-left: 16px; flex-direction: column; justify-content: center; box-sizing: border-box; background: linear-gradient(90deg, rgba(22, 112, 232, .98) 0%, rgba(22, 112, 232, .88) 52%, rgba(22, 112, 232, 0) 100%); }
|
||||
.club-directory-hero__title { font-size: 25px; line-height: 34px; font-weight: 700; letter-spacing: 1px; text-shadow: 0 2px 6px rgba(17, 86, 182, .18); }
|
||||
.club-directory-hero__subtitle { margin-top: 4px; font-size: 13px; line-height: 20px; letter-spacing: .5px; opacity: .95; }
|
||||
.club-directory-sticky { padding: 10px 0 1px; background: #f3f7fd; }
|
||||
.club-directory-page .van-sticky--fixed .club-directory-sticky { padding-right: 12px; padding-left: 12px; box-sizing: border-box; }
|
||||
.club-directory-search { margin: 0 0 8px; padding: 0; overflow: hidden; border-radius: 12px; background: #fff; box-shadow: 0 5px 16px rgba(43, 73, 112, .1); }
|
||||
.club-directory-search .van-search__content { height: 42px; padding-left: 12px; align-items: center; border-radius: 12px; background: #fff; }
|
||||
.club-directory-search .van-field__control { color: #263548; font-size: 14px; }
|
||||
.club-directory-search .van-field__control::placeholder { color: #a7b0bf; }
|
||||
.club-directory-tabs { margin: 0 0 10px; overflow: hidden; border-radius: 10px; background: #fff; box-shadow: 0 5px 16px rgba(43, 73, 112, .08); }
|
||||
.club-directory-tabs .van-tab { color: #66758a; font-size: 13px; }
|
||||
.club-directory-tabs .van-tab--active { color: #1989fa; }
|
||||
.club-directory-list { padding: 0; }
|
||||
.club-directory-page .table-list-container { margin-top: 0; }
|
||||
.club-directory-page .table-list-container .table-list-item { margin-bottom: 10px; padding: 0; border: 0; border-radius: 10px; background: #fff; box-shadow: 0 7px 20px rgba(55, 85, 126, .08); }
|
||||
.club-directory-card { width: 100%; }
|
||||
.club-directory-cover { width: 100%; height: 150px; overflow: hidden; background: #edf4fc; }
|
||||
.club-directory-cover .van-image, .club-directory-detail-cover .van-image { display: block; width: 100%; height: 100%; }
|
||||
.club-directory-image-empty { display: flex; width: 100%; height: 100%; flex-direction: column; row-gap: 8px; align-items: center; justify-content: center; background: #edf4fc; color: #8da1ba; font-size: 13px; }
|
||||
.club-directory-image-empty .van-icon { font-size: 34px; }
|
||||
.club-directory-info { min-width: 0; padding: 12px 14px 14px; }
|
||||
.club-directory-name { margin: 0; color: #303846; font-size: 14px; font-weight: 400; line-height: 21px; overflow-wrap: anywhere; }
|
||||
.club-directory-meta { margin: 4px 0 0; color: #98a2b2; font-size: 12px; line-height: 17px; }
|
||||
.club-directory-tags { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin-top: 9px; }
|
||||
.club-directory-info .club-directory-tags { margin-top: 10px; padding-top: 9px; border-top: 1px solid #edf1f6; }
|
||||
.club-directory-tag { display: inline-flex; padding: 3px 8px; align-items: center; border-radius: 4px; color: #397de0; background: #edf5ff; font-size: 11px; line-height: 16px; }
|
||||
.club-directory-tag-label { margin-right: 4px; color: #8aa7cf; }
|
||||
.club-directory-page .table-list-container .table-list-item .item-actions { margin: 0 14px; padding: 10px 0 14px; border-top-color: #edf1f6; }
|
||||
.club-directory-page .item-actions .van-button { height: 28px; padding: 0 12px; border-radius: 14px; font-size: 12px; }
|
||||
.club-directory-page .item-actions .club-directory-card-button { height: 32px; padding: 0 14px; border: 0; border-radius: 16px; color: #397de0; background-color: #edf5ff; font-size: 13px; }
|
||||
.club-directory-page .item-actions .club-directory-card-button i { margin-right: 5px; font-size: 13px; }
|
||||
.club-directory-page .item-actions .club-directory-card-button--join { width: 108px; color: #fff; background-color: #1989fa; box-shadow: 0 5px 12px rgba(25, 137, 250, .18); }
|
||||
.club-directory-page .item-actions .club-directory-card-button--exit { width: 108px; color: #fff; background-color: #ee0a24; box-shadow: 0 5px 12px rgba(238, 10, 36, .18); }
|
||||
.club-directory-loading { padding: 50px 0; text-align: center; }
|
||||
.club-directory-detail-cover { height: 230px; flex: none; background: #f2f7fc; }
|
||||
.club-directory-page .club-user-join-detail__content { position: relative; margin-top: -16px; padding: 0 0 18px; }
|
||||
.club-directory-detail-heading { padding: 16px 14px 12px; }
|
||||
.club-directory-detail-heading .club-directory-name { margin-bottom: 6px; font-size: 18px; font-weight: 600; }
|
||||
.club-directory-detail-meta { display: flex; align-items: center; gap: 5px; margin-top: 6px; color: #8b9bb0; font-size: 12px; line-height: 20px; }
|
||||
.club-directory-detail-meta .van-icon { color: #1989fa; font-size: 14px; }
|
||||
.club-directory-detail-stats { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); margin: 0 14px; padding: 16px 0; border-top: 1px solid #eef2f7; }
|
||||
.club-directory-detail-stat { min-width: 0; padding: 0 8px; text-align: center; }
|
||||
.club-directory-detail-stat + .club-directory-detail-stat { border-left: 1px solid #eef2f7; }
|
||||
.club-directory-detail-stat span { display: block; margin-bottom: 6px; color: #8b9bb0; font-size: 12px; line-height: 18px; }
|
||||
.club-directory-detail-stat strong { display: block; color: #40526a; font-size: 14px; font-weight: 400; line-height: 20px; overflow-wrap: anywhere; }
|
||||
.club-directory-page .club-user-join-card { border-radius: 16px; }
|
||||
.club-directory-page .club-user-join-info-card .van-cell__value { font-size: 13px; }
|
||||
.club-directory-page .club-user-join-info-long-cell img { max-width: 100%; height: auto; }
|
||||
.club-directory-material-preview { position: relative; height: 72px; overflow: hidden; }
|
||||
.club-directory-material-preview.is-expanded { height: auto; overflow: visible; }
|
||||
.club-directory-material-preview.is-truncated::after { position: absolute; right: 0; bottom: 0; padding-left: 18px; background: linear-gradient(90deg, transparent, #fff 18px); content: '…'; }
|
||||
.club-directory-material-content { display: flow-root; overflow-wrap: anywhere; }
|
||||
.club-directory-material-rich { font-size: 16px; line-height: 1.6; }
|
||||
.club-directory-material-rich, .club-directory-material-rich * { max-width: 100% !important; box-sizing: border-box; }
|
||||
.club-directory-material-rich p, .club-directory-material-rich h1, .club-directory-material-rich h2, .club-directory-material-rich h3, .club-directory-material-rich h4, .club-directory-material-rich h5, .club-directory-material-rich h6 { margin: 0; }
|
||||
.club-directory-material-rich img, .club-directory-material-rich video { height: auto !important; }
|
||||
.club-directory-material-rich table { width: 100% !important; table-layout: fixed; }
|
||||
.club-directory-material-toggle { height: 24px; margin-top: 4px; text-align: right; }
|
||||
.club-directory-material-toggle button { padding: 0; border: 0; background: transparent; color: #1989fa; font-size: 12px; line-height: 24px; cursor: pointer; }
|
||||
.club-directory-detail-actions { display: flex; flex: none; gap: 12px; padding: 10px 15px calc(10px + env(safe-area-inset-bottom)); background: #fff; border-top: 1px solid #edf2f8; }
|
||||
.club-directory-detail-actions .van-button { flex: 1; height: 40px; border-radius: 20px; }
|
||||
.club-directory-detail-actions .van-button--info { border-color: #1989fa; background-color: #1989fa; }
|
||||
.club-directory-detail-actions .van-button--info.van-button--plain { color: #1989fa; background-color: #fff; }
|
||||
.club-directory-detail-actions .van-button--danger { color: #fff; border-color: #ee0a24; background-color: #ee0a24; }
|
||||
.club-directory-detail-actions .van-button--danger i { margin-right: 5px; }
|
||||
.club-directory-page .empty-state { padding: 0; }
|
||||
.club-directory-empty { display: flex; min-height: 260px; padding: 26px 18px 32px; align-items: center; justify-content: center; flex-direction: column; box-sizing: border-box; text-align: center; }
|
||||
.club-directory-empty img { display: block; width: 82%; max-width: 280px; height: auto; border-radius: 12px; object-fit: contain; }
|
||||
.club-directory-empty__title { margin-top: 14px; color: #50627a; font-size: 15px; line-height: 22px; font-weight: 600; }
|
||||
.club-directory-empty__hint { margin-top: 5px; color: #9aa7b8; font-size: 12px; line-height: 18px; }
|
||||
.club-directory-page .van-list__finished-text, .club-directory-page .van-list__loading { color: #9aa7b8; font-size: 13px; line-height: 44px; }
|
||||
</style>
|
||||
|
||||
<div id="app" v-cloak class="club-directory-page">
|
||||
<van-nav-bar title="社团列表" left-text="返回" left-arrow fixed placeholder @click-left="onNavBack"></van-nav-bar>
|
||||
<div class="club-directory-content">
|
||||
<section class="club-directory-hero">
|
||||
<img class="club-directory-hero__image" src="/assets/mobile/img/club/club-hero-v5.png" alt="社团入会插画">
|
||||
<div class="club-directory-hero__copy">
|
||||
<div class="club-directory-hero__title">社团入会</div>
|
||||
<div class="club-directory-hero__subtitle">以兴趣相聚 · 让生活精彩</div>
|
||||
</div>
|
||||
</section>
|
||||
<van-sticky :offset-top="46">
|
||||
<div class="club-directory-sticky">
|
||||
<van-search v-model="pageForm.clubName" class="club-directory-search" placeholder="请输入社团名称搜索" shape="round" clearable @search="doSearch" @clear="doSearch"></van-search>
|
||||
<van-tabs v-model="activeTab" class="club-directory-tabs" color="#1989fa" @change="joinChange">
|
||||
<van-tab :title="'可加入社团(' + noJoinCount + ')'" name="available"></van-tab>
|
||||
<van-tab :title="'已加入社团(' + hasJoinCount + ')'" name="joined"></van-tab>
|
||||
</van-tabs>
|
||||
</div>
|
||||
</van-sticky>
|
||||
<main class="club-directory-list">
|
||||
<table-list api="/platform/club/join/list/pageData" :page_form.sync="pageForm" ref="tableListRef" :show_loading_overlay="false" @ready="doSearch" @loading-change="onLoadingChange">
|
||||
<template #header><div></div></template>
|
||||
<template v-slot="{row}">
|
||||
<div v-if="row" class="club-directory-card">
|
||||
<div class="club-directory-cover">
|
||||
<van-image v-if="row.thumbnail" :src="row.thumbnail" :alt="row.clubName + '缩略图'" fit="cover"><template #error><div class="club-directory-image-empty"><van-icon name="photo-o"></van-icon><span>暂无图片</span></div></template></van-image>
|
||||
<div v-else class="club-directory-image-empty"><van-icon name="photo-o"></van-icon><span>暂无图片</span></div>
|
||||
</div>
|
||||
<div class="club-directory-info">
|
||||
<h2 class="club-directory-name">{{ row.clubName }}</h2>
|
||||
<p class="club-directory-meta">成立时间:{{ row.foundTime || '--' }}</p>
|
||||
<div class="club-directory-tags">
|
||||
<span class="club-directory-tag"><span class="club-directory-tag-label">会长</span>{{ row.clubLeader || '--' }}</span>
|
||||
<span class="club-directory-tag"><span class="club-directory-tag-label">社团类型</span>{{ formatClubType(row.clubType) }}</span>
|
||||
<span class="club-directory-tag"><span class="club-directory-tag-label">会费标准</span>{{ row.due == null || row.due === '' ? '--' : row.due + ' 元/年' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #actions="{row}">
|
||||
<template v-if="row">
|
||||
<van-button class="club-directory-card-button" native-type="button" size="small" type="default" @click="onView(row)"><i class="fa fa-eye"></i>查看</van-button>
|
||||
<van-button v-if="!pageForm.hasJoin" class="club-directory-card-button club-directory-card-button--join" native-type="button" size="small" type="default" :loading="formLoading" @click="onJoin(row)"><i class="fa fa-sign-in"></i>申请加入</van-button>
|
||||
<van-button v-else class="club-directory-card-button club-directory-card-button--exit" native-type="button" size="small" type="danger" :disabled="formLoading" @click="onExit(row)"><i class="fa fa-sign-out"></i>申请退出</van-button>
|
||||
</template>
|
||||
</template>
|
||||
<template #empty>
|
||||
<div v-if="listLoading" class="club-directory-loading"><van-loading size="24px" vertical>加载中</van-loading></div>
|
||||
<div v-else class="club-directory-empty"><img src="/assets/mobile/img/club/club-approval-empty.png" alt="社团空状态插画"><div class="club-directory-empty__title">暂无社团</div><div class="club-directory-empty__hint">当前筛选条件下暂时没有可展示的社团</div></div>
|
||||
</template>
|
||||
</table-list>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<van-popup v-model="clubDetailVisible" position="right" :style="{ width: '100%', height: '100%' }" get-container="#app" class="club-user-join-popup" :close-on-click-overlay="false" @opened="observeDetailMaterials">
|
||||
<div class="club-user-join-detail">
|
||||
<van-nav-bar title="社团详情" left-text="返回" left-arrow fixed @click-left="closeClubDetail"></van-nav-bar>
|
||||
<div class="club-user-join-detail__scroll">
|
||||
<div class="club-directory-detail-cover">
|
||||
<van-image v-if="clubDetail.thumbnail" :src="clubDetail.thumbnail" :alt="clubDetail.clubName + '缩略图'" fit="cover"><template #error><span class="club-directory-image-empty">暂无图片</span></template></van-image>
|
||||
<div v-else class="club-directory-image-empty">暂无图片</div>
|
||||
</div>
|
||||
<main class="club-user-join-detail__content">
|
||||
<section class="club-user-join-card club-user-join-info-card">
|
||||
<div class="club-directory-detail-heading">
|
||||
<h2 class="club-directory-name">{{ clubDetail.clubName }}</h2>
|
||||
<div class="club-directory-detail-meta">成立时间:{{ clubDetail.foundTime || '--' }}</span></div>
|
||||
</div>
|
||||
<div class="club-directory-detail-stats">
|
||||
<div class="club-directory-detail-stat"><span>社团类型</span><strong>{{ formatClubType(clubDetail.clubType) }}</strong></div>
|
||||
<div class="club-directory-detail-stat"><span>当前人数</span><strong>{{ clubDetail.currentPeopleNum || 0 }} 人</strong></div>
|
||||
<div class="club-directory-detail-stat"><span>会费标准</span><strong>{{ clubDetail.due == null || clubDetail.due === '' ? '--' : clubDetail.due + ' 元/年' }}</strong></div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="club-user-join-card club-user-join-info-card">
|
||||
<h3 class="club-user-join-card__title">人员及联系信息</h3>
|
||||
<van-cell-group :border="false">
|
||||
<van-cell title="会长">{{ clubDetail.clubLeader || '--' }}</van-cell>
|
||||
<van-cell title="秘书长">{{ clubDetail.clubSecretary || '--' }}</van-cell>
|
||||
<van-cell title="发起人">{{ clubDetail.sponsorName || '--' }}</van-cell>
|
||||
<van-cell title="报名联系人">{{ clubDetail.concatPersonName || '--' }}</van-cell>
|
||||
<van-cell title="联系人电话">{{ clubDetail.concatPersonMobile || '--' }}</van-cell>
|
||||
<van-cell title="联系人邮箱">{{ clubDetail.concatPersonEmail || '--' }}</van-cell>
|
||||
</van-cell-group>
|
||||
</section>
|
||||
<section class="club-user-join-card club-user-join-info-card">
|
||||
<h3 class="club-user-join-card__title">社团信息</h3>
|
||||
<van-cell-group :border="false">
|
||||
<van-cell v-for="section in detailSections" :key="section.key" :title="section.title" class="club-user-join-info-long-cell">
|
||||
<div class="club-directory-material-preview" :class="{ 'is-expanded': expandedMaterials[section.key], 'is-truncated': overflowingMaterials[section.key] && !expandedMaterials[section.key] }">
|
||||
<div ref="detailMaterials" :data-material-key="section.key" class="club-directory-material-content">
|
||||
<template v-if="section.files">
|
||||
<file-preview v-if="clubDetail[section.key] && clubDetail[section.key].length" :files="clubDetail[section.key]" complete_result></file-preview><span v-else>暂无</span>
|
||||
</template>
|
||||
<div v-else-if="clubDetail[section.key]" class="club-directory-material-rich" v-html="clubDetail[section.key]" v-mobile-rich-text="clubDetail[section.key]"></div><span v-else>暂无</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="club-directory-material-toggle">
|
||||
<button v-if="overflowingMaterials[section.key]" type="button" :aria-expanded="!!expandedMaterials[section.key]" @click="$set(expandedMaterials, section.key, !expandedMaterials[section.key])">{{ expandedMaterials[section.key] ? '收起' : '展开' }}<van-icon :name="expandedMaterials[section.key] ? 'arrow-up' : 'arrow-down'"></van-icon></button>
|
||||
</div>
|
||||
</van-cell>
|
||||
</van-cell-group>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
<div class="club-directory-detail-actions">
|
||||
<van-button native-type="button" type="info" plain @click="closeClubDetail">返回列表</van-button>
|
||||
<van-button v-if="!pageForm.hasJoin" native-type="button" type="info" :loading="formLoading" @click="onJoin(clubDetail)">申请加入</van-button>
|
||||
<van-button v-else native-type="button" type="danger" :disabled="formLoading" @click="onExit(clubDetail)"><i class="fa fa-sign-out"></i>申请退出</van-button>
|
||||
</div>
|
||||
</div>
|
||||
</van-popup>
|
||||
<van-dialog :value="confirmVisible" title="提示" :message="confirmMessage" show-cancel-button :confirm-button-loading="formLoading" @confirm="confirmExit" @cancel="closeConfirm"></van-dialog>
|
||||
</div>
|
||||
|
||||
<script nonce="${cspNonce!}">
|
||||
// 仅适配预览字号,保留原始富文本;先读取全部字号,避免父元素缩放后影响子元素的相对字号。
|
||||
function adaptClubMobileRichText(element, binding) {
|
||||
if (element._clubMobileRichText === binding.value) return;
|
||||
element.style.removeProperty('font-size');
|
||||
element.style.removeProperty('line-height');
|
||||
const sizes = [element, ...element.querySelectorAll('*')].map((node) => {
|
||||
const style = window.getComputedStyle(node);
|
||||
const fontSize = parseFloat(style.fontSize) || 16;
|
||||
const lineHeight = parseFloat(style.lineHeight) / fontSize || 1.6;
|
||||
return {node, fontSize, lineHeight};
|
||||
});
|
||||
sizes.forEach(({node, fontSize, lineHeight}) => {
|
||||
// 正文 16px 映射为 14px,大字号适度压缩层级,限制在移动端可读的 10~32px 范围。
|
||||
const mobileSize = Math.min(32, Math.max(10, 14 * Math.pow(fontSize / 16, 0.75)));
|
||||
node.style.setProperty('font-size', mobileSize.toFixed(2) + 'px', 'important');
|
||||
node.style.setProperty('line-height', (mobileSize * Math.min(1.8, Math.max(1.3, lineHeight))).toFixed(2) + 'px', 'important');
|
||||
});
|
||||
element._clubMobileRichText = binding.value;
|
||||
}
|
||||
|
||||
new Vue({
|
||||
el: "#app",
|
||||
store,
|
||||
dicts: ['CLUB_REGISTER_TYPE'],
|
||||
directives: {
|
||||
'mobile-rich-text': {inserted: adaptClubMobileRichText, componentUpdated: adaptClubMobileRichText}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
pageForm: {pageNumber: 1, pageSize: 10, totalCount: 0, clubName: '', hasJoin: false, pageOrderName: 'clubCode', pageOrderBy: 'ascending'},
|
||||
activeTab: 'available', hasJoinCount: 0, noJoinCount: 0, listLoading: true, formLoading: false,
|
||||
detailSections: [{key: 'introduce', title: '社团介绍'}, {key: 'purpose', title: '社团宗旨'}, {key: 'rulesFile', title: '社团章程', files: true}, {key: 'yearPlanFile', title: '年度活动计划', files: true}],
|
||||
expandedMaterials: {}, overflowingMaterials: {},
|
||||
clubDetailVisible: false, clubDetail: {}, confirmVisible: false, currentRow: null, historyLayerUnsubscribe: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
confirmMessage() { return this.currentRow ? '您确定要申请退出“' + this.currentRow.clubName + '”吗?' : '您确定要申请退出该社团吗?'; }
|
||||
},
|
||||
created() {
|
||||
// 详情和退会确认分别入栈,手机返回时只关闭当前弹层。
|
||||
const manager = window.h5HistoryLayerManager;
|
||||
if (manager) {
|
||||
manager.ensureRegistered('club-directory');
|
||||
this.historyLayerUnsubscribe = manager.subscribe((layers) => {
|
||||
this.clubDetailVisible = layers.includes('club-directory-detail');
|
||||
this.confirmVisible = layers.includes('club-directory-confirm');
|
||||
});
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.detailMaterialObserver) this.detailMaterialObserver.disconnect();
|
||||
if (this.historyLayerUnsubscribe) this.historyLayerUnsubscribe();
|
||||
const manager = window.h5HistoryLayerManager;
|
||||
if (manager) manager.unregister('club-directory');
|
||||
},
|
||||
methods: {
|
||||
observeDetailMaterials() {
|
||||
if (this.detailMaterialObserver) this.detailMaterialObserver.disconnect();
|
||||
this.$nextTick(() => {
|
||||
// 富文本图片和附件异步渲染后高度会变化,按实际高度决定是否显示展开入口。
|
||||
this.detailMaterialObserver = new ResizeObserver((entries) => {
|
||||
entries.forEach((entry) => {
|
||||
this.$set(this.overflowingMaterials, entry.target.dataset.materialKey, entry.target.scrollHeight > 72);
|
||||
});
|
||||
});
|
||||
(this.$refs.detailMaterials || []).forEach((element) => this.detailMaterialObserver.observe(element));
|
||||
});
|
||||
},
|
||||
formatClubType(value) {
|
||||
const option = (this.dict.type.CLUB_REGISTER_TYPE || []).find((item) => String(item.code) === String(value));
|
||||
return option ? option.name : '--';
|
||||
},
|
||||
onNavBack() { const manager = window.h5HistoryLayerManager; if (manager && manager.stack.length) { manager.close(); return; } historyBack(); },
|
||||
onLoadingChange(state) { this.listLoading = state.loading; },
|
||||
joinChange(name) { this.$set(this.pageForm, 'hasJoin', name === 'joined'); this.doSearch(); },
|
||||
doSearch() {
|
||||
this.$nextTick(() => {
|
||||
this.$set(this.pageForm, 'pageNumber', 1);
|
||||
this.$set(this.pageForm, 'totalCount', 0);
|
||||
this.$refs.tableListRef.doSearch();
|
||||
});
|
||||
this.$axios.post('/platform/club/join/list/getCount').then((res) => {
|
||||
if (res.code === 0) { this.hasJoinCount = res.data.hasJoinCount; this.noJoinCount = res.data.noJoinCount; }
|
||||
});
|
||||
},
|
||||
onView(row) {
|
||||
this.$axios.post('/platform/club/join/list/findOne', {id: row.id}).then((res) => {
|
||||
if (res.code !== 0) { this.$toast(res.msg); return; }
|
||||
this.clubDetail = res.data;
|
||||
this.expandedMaterials = {};
|
||||
this.overflowingMaterials = {};
|
||||
const manager = window.h5HistoryLayerManager;
|
||||
if (manager) { manager.open('club-directory-detail'); return; }
|
||||
this.clubDetailVisible = true;
|
||||
});
|
||||
},
|
||||
closeClubDetail() {
|
||||
const manager = window.h5HistoryLayerManager;
|
||||
if (manager) { manager.close('club-directory-detail'); return; }
|
||||
this.clubDetailVisible = false;
|
||||
},
|
||||
onJoin(row) {
|
||||
if (this.formLoading) return;
|
||||
this.formLoading = true;
|
||||
this.$axios.post('/platform/club/join/apply/checkApplyClub', {clubId: row.id}).then((res) => {
|
||||
if (res.code === 0) this.goToPage('/platform/club/join/apply/h5?clubId=' + encodeURIComponent(row.id));
|
||||
else this.$toast(res.msg);
|
||||
}).finally(() => { this.formLoading = false; });
|
||||
},
|
||||
onExit(row) {
|
||||
if (this.formLoading) return;
|
||||
this.currentRow = row;
|
||||
const manager = window.h5HistoryLayerManager;
|
||||
if (manager) { manager.open('club-directory-confirm'); return; }
|
||||
this.confirmVisible = true;
|
||||
},
|
||||
closeConfirm(afterClose) {
|
||||
const manager = window.h5HistoryLayerManager;
|
||||
if (manager) { manager.close('club-directory-confirm', afterClose); return; }
|
||||
this.confirmVisible = false;
|
||||
if (typeof afterClose === 'function') afterClose();
|
||||
},
|
||||
confirmExit() {
|
||||
if (this.formLoading || !this.currentRow) return;
|
||||
const row = this.currentRow;
|
||||
this.formLoading = true;
|
||||
this.closeConfirm(() => {
|
||||
// 列表返回 clubId,详情返回社团 id;统一按退会接口要求传递 clubId。
|
||||
this.$axios.post('/platform/club/join/apply/submit', {data: JSON.stringify({clubId: row.clubId || row.id}), mode: false}).then((res) => {
|
||||
this.$toast(res.msg);
|
||||
if (res.code === 0) this.goToPage('/platform/club/join/mine/h5');
|
||||
}).finally(() => { this.formLoading = false; });
|
||||
});
|
||||
},
|
||||
goToPage(url) {
|
||||
const manager = window.h5HistoryLayerManager;
|
||||
if (manager) { manager.closeAllAndNavigate(url); return; }
|
||||
this.$pjaxReplace(url);
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<!--#
|
||||
}
|
||||
#-->
|
||||
@@ -70,6 +70,10 @@ layout("/layouts/platform_h5.html"){
|
||||
<section class="club-user-join-card club-user-join-info-card">
|
||||
<h3 class="club-user-join-card__title">社团基础信息</h3>
|
||||
<van-cell-group :border="false">
|
||||
<van-cell title="社团缩略图" class="club-user-join-info-long-cell">
|
||||
<van-image v-if="clubDetail.thumbnail" :src="clubDetail.thumbnail" width="100%" height="180" fit="contain"><template #error>暂无图片</template></van-image>
|
||||
<span v-else>暂无图片</span>
|
||||
</van-cell>
|
||||
<van-cell title="社团名称">{{ clubDetail.clubName || '--' }}</van-cell>
|
||||
<van-cell title="成立时间">{{ clubDetail.foundTime || '--' }}</van-cell>
|
||||
<van-cell title="社团编码">{{ clubDetail.clubCode || '--' }}</van-cell>
|
||||
|
||||
@@ -3,8 +3,52 @@ layout("/layouts/platform_h5.html"){
|
||||
#-->
|
||||
|
||||
<style>
|
||||
|
||||
|
||||
.health-checkup-cards .table-list-item .img-container {
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.health-checkup-cards .table-list-item .img-container img {
|
||||
border-radius: 10px;
|
||||
}
|
||||
/* 仅调整体检卡片,避免窄屏下时间和标签被通用图片区域高度截断。 */
|
||||
.health-checkup-cards .table-list-content {
|
||||
max-height: none !important;
|
||||
overflow: visible !important;
|
||||
}
|
||||
.health-checkup-card-title {
|
||||
margin-bottom: 10px;
|
||||
font-size: 15px;
|
||||
line-height: 22px;
|
||||
font-weight: 600;
|
||||
color: #263548;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.health-checkup-card-details {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
.health-checkup-card-time {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
margin-bottom: 6px;
|
||||
color: #56657a;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
}
|
||||
.health-checkup-card-time > span:first-child {
|
||||
flex-shrink: 0;
|
||||
color: #98a3b3;
|
||||
}
|
||||
.health-checkup-card-time > span:last-child {
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.health-checkup-card-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="app">
|
||||
@@ -26,15 +70,27 @@ layout("/layouts/platform_h5.html"){
|
||||
</van-tabs>
|
||||
</van-sticky>
|
||||
|
||||
<table-list api="/platform/healthCheckup/h5/pageData" :page_form.sync="pageForm" ref="tableListRef"
|
||||
<table-list class="health-checkup-cards" api="/platform/healthCheckup/h5/pageData" :page_form.sync="pageForm" ref="tableListRef"
|
||||
@ready="doSearch" img="cover">
|
||||
<template #header>
|
||||
<div></div>
|
||||
</template>
|
||||
<template v-slot="{index,row}">
|
||||
<table-column label="开始时间">
|
||||
{{$moment(row.choiceTimeStart).format('MM/DD HH:mm')}}
|
||||
</table-column>
|
||||
<table-column label="结束时间">
|
||||
{{$moment(row.choiceTimeEnd).format('MM/DD HH:mm')}}
|
||||
</table-column>
|
||||
<div class="health-checkup-card-details">
|
||||
<div class="health-checkup-card-title">{{row.name}}</div>
|
||||
<div class="health-checkup-card-time">
|
||||
<span>开始时间</span>
|
||||
<span>{{row.choiceTimeStart ? $moment(row.choiceTimeStart).format('YYYY-MM-DD HH:mm') : '—'}}</span>
|
||||
</div>
|
||||
<div class="health-checkup-card-time">
|
||||
<span>结束时间</span>
|
||||
<span>{{row.choiceTimeEnd ? $moment(row.choiceTimeEnd).format('YYYY-MM-DD HH:mm') : '—'}}</span>
|
||||
</div>
|
||||
<div class="health-checkup-card-tags">
|
||||
<van-tag plain type="primary">{{row.healthCheckupType === 'jzg' ? '教职工' : row.healthCheckupType === 'js' ? '家属' : (row.healthCheckupType || '未设置对象类型')}}</van-tag>
|
||||
<van-tag plain type="success">{{row.subjectCount}} 个体检套餐</van-tag>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #actions="{index,row}">
|
||||
<template v-if="$moment().isBefore($moment(row.choiceTimeEnd))">
|
||||
|
||||
@@ -2,6 +2,48 @@
|
||||
layout("/layouts/platform_h5.html"){
|
||||
#-->
|
||||
|
||||
<style>
|
||||
/* 仅调整体检卡片,避免窄屏下时间和标签被通用图片区域高度截断。 */
|
||||
.health-checkup-cards .table-list-content {
|
||||
max-height: none !important;
|
||||
overflow: visible !important;
|
||||
}
|
||||
.health-checkup-card-title {
|
||||
margin-bottom: 10px;
|
||||
font-size: 15px;
|
||||
line-height: 22px;
|
||||
font-weight: 600;
|
||||
color: #263548;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.health-checkup-card-details {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
.health-checkup-card-time {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
margin-bottom: 6px;
|
||||
color: #56657a;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
}
|
||||
.health-checkup-card-time > span:first-child {
|
||||
flex-shrink: 0;
|
||||
color: #98a3b3;
|
||||
}
|
||||
.health-checkup-card-time > span:last-child {
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.health-checkup-card-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="app">
|
||||
<van-nav-bar @click-left="historyBack" left-arrow left-text="返回" title="我的体检" placeholder fixed></van-nav-bar>
|
||||
<van-sticky offset-top="46px">
|
||||
@@ -13,14 +55,27 @@ layout("/layouts/platform_h5.html"){
|
||||
</van-sticky>
|
||||
|
||||
|
||||
<table-list api="/platform/healthCheckup/h5/mineData" :page_form.sync="pageForm" ref="tableListRef"
|
||||
@ready="doSearch" title="healthCheckupName">
|
||||
<table-list class="health-checkup-cards" api="/platform/healthCheckup/h5/mineData" :page_form.sync="pageForm" ref="tableListRef"
|
||||
@ready="doSearch" img="cover">
|
||||
<template #header>
|
||||
<div></div>
|
||||
</template>
|
||||
<template v-slot="{index,row}">
|
||||
<table-column label="项目名称">{{row.name}}</table-column>
|
||||
<table-column label="开始时间">{{row.choiceTimeStart}}</table-column>
|
||||
<table-column label="结束时间">{{row.choiceTimeEnd}}</table-column>
|
||||
<table-column label="选择医院">{{row.optionName}}</table-column>
|
||||
<table-column label="邮寄地址">{{row.receiveAddress}}</table-column>
|
||||
<div class="health-checkup-card-details">
|
||||
<div class="health-checkup-card-title">{{row.name}}</div>
|
||||
<div class="health-checkup-card-time">
|
||||
<span>开始时间</span>
|
||||
<span>{{row.choiceTimeStart ? $moment(row.choiceTimeStart).format('YYYY-MM-DD HH:mm') : '—'}}</span>
|
||||
</div>
|
||||
<div class="health-checkup-card-time">
|
||||
<span>结束时间</span>
|
||||
<span>{{row.choiceTimeEnd ? $moment(row.choiceTimeEnd).format('YYYY-MM-DD HH:mm') : '—'}}</span>
|
||||
</div>
|
||||
<div class="health-checkup-card-tags">
|
||||
<van-tag plain type="primary">{{row.healthCheckupType === 'jzg' ? '教职工' : row.healthCheckupType === 'js' ? '家属' : (row.healthCheckupType || '未设置对象类型')}}</van-tag>
|
||||
<van-tag plain type="success">{{row.subjectCount}} 个体检套餐</van-tag>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #actions="{index,row}">
|
||||
<div class="action-btn" @click="edit(row)" v-if="new Date().getTime() < new Date(row.choiceTimeEnd).getTime()">
|
||||
|
||||
@@ -30,6 +30,25 @@ layout("/layouts/platform_h5.html"){
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 年度组件已由 platform_h5.html 注册,两个荣誉分类共用所选年度。 -->
|
||||
<div class="honor-year-filter">
|
||||
<div class="honor-year-filter__label">
|
||||
<van-icon name="calendar-o"></van-icon>
|
||||
<span>荣誉年度</span>
|
||||
</div>
|
||||
<van-dropdown-menu class="honor-year-filter__menu" active-color="#bd0018">
|
||||
<year-van-dropdown-item
|
||||
ref="yearFilter"
|
||||
:num="10"
|
||||
:value="currentYear"
|
||||
@input="setCurrentYear"
|
||||
@change="listHonor"
|
||||
@open="onYearFilterOpen"
|
||||
@close="onYearFilterClose">
|
||||
</year-van-dropdown-item>
|
||||
</van-dropdown-menu>
|
||||
</div>
|
||||
|
||||
<section class="honor-intro">
|
||||
<div class="honor-intro__corner honor-intro__corner--left"></div>
|
||||
<div class="honor-intro__corner honor-intro__corner--right"></div>
|
||||
@@ -38,7 +57,7 @@ layout("/layouts/platform_h5.html"){
|
||||
<p>致敬劳模先进,礼赞奋斗精神。</p>
|
||||
</section>
|
||||
|
||||
<van-loading v-if="loading" class="honor-loading" color="#fff">加载中...</van-loading>
|
||||
<van-loading v-if="tableLoading" class="honor-loading" color="#fff">加载中...</van-loading>
|
||||
|
||||
<div v-else class="honor-list">
|
||||
<article v-for="item in honorList" :key="item.id" class="honor-card">
|
||||
@@ -57,7 +76,10 @@ layout("/layouts/platform_h5.html"){
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<van-empty v-if="honorList.length === 0" image="search" description="暂无当年荣誉"></van-empty>
|
||||
<van-empty v-if="honorList.length === 0" class="honor-empty" image="search"
|
||||
:description="'暂无' + currentYear + '年度荣誉'">
|
||||
<div class="honor-empty__hint">可切换年度查看往年荣誉</div>
|
||||
</van-empty>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -65,7 +87,7 @@ layout("/layouts/platform_h5.html"){
|
||||
<script nonce="${cspNonce!}">
|
||||
new Vue({
|
||||
el: "#app",
|
||||
data: function () {
|
||||
data() {
|
||||
return {
|
||||
activeTab: "labor",
|
||||
currentYear: new Date().getFullYear(),
|
||||
@@ -75,14 +97,32 @@ layout("/layouts/platform_h5.html"){
|
||||
studio: null
|
||||
},
|
||||
honorList: [],
|
||||
loading: false
|
||||
tableLoading: false,
|
||||
honorRequestId: 0,
|
||||
popupHistoryToken: "",
|
||||
yearFilterOpened: false,
|
||||
yearHistoryClosing: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
goBack: function () {
|
||||
// 年度下拉展开时,导航返回只消费弹层历史,不离开当前荣誉页面。
|
||||
goBack() {
|
||||
if (this.yearHistoryClosing) {
|
||||
return
|
||||
}
|
||||
if (window.h5PopupHistory.close(this.popupHistoryToken, "honor-year")) {
|
||||
this.$set(this, "yearHistoryClosing", true)
|
||||
return
|
||||
}
|
||||
this.returnHome()
|
||||
},
|
||||
returnHome: function () {
|
||||
returnHome() {
|
||||
// 页面跳转前先清理年度弹层历史,避免返回后恢复已关闭的下拉框。
|
||||
window.h5PopupHistory.clear(this.popupHistoryToken).then(() => {
|
||||
this.navigateHome()
|
||||
})
|
||||
},
|
||||
navigateHome() {
|
||||
if (window.store && typeof window.store.commit === "function") {
|
||||
window.store.commit("setActiveTarBar", "home")
|
||||
}
|
||||
@@ -90,13 +130,13 @@ layout("/layouts/platform_h5.html"){
|
||||
window.location.replace("/platform/h5/home")
|
||||
return
|
||||
}
|
||||
var fallbackTimer = window.setTimeout(function () {
|
||||
var fallbackTimer = window.setTimeout(() => {
|
||||
window.location.replace("/platform/h5/home")
|
||||
}, 1200)
|
||||
$(document).one("pjax:complete", function () {
|
||||
$(document).one("pjax:complete", () => {
|
||||
window.clearTimeout(fallbackTimer)
|
||||
})
|
||||
$(document).one("pjax:error pjax:timeout", function () {
|
||||
$(document).one("pjax:error pjax:timeout", () => {
|
||||
window.clearTimeout(fallbackTimer)
|
||||
window.location.replace("/platform/h5/home")
|
||||
})
|
||||
@@ -109,11 +149,43 @@ layout("/layouts/platform_h5.html"){
|
||||
timeout: 8000
|
||||
})
|
||||
},
|
||||
switchTab: function (tab) {
|
||||
// 两个分类共用一个年度值,切换分类时保留当前选择。
|
||||
setCurrentYear(year) {
|
||||
// 公共年度组件返回 Number;先更新年度,再由 change 事件查询对应记录。
|
||||
this.$set(this, "currentYear", year)
|
||||
},
|
||||
// 浏览器历史回退完成前不重复打开,避免快速点击残留额外历史记录。
|
||||
onYearFilterOpen() {
|
||||
if (this.yearHistoryClosing) {
|
||||
this.closeYearFilter()
|
||||
return
|
||||
}
|
||||
this.$set(this, "yearFilterOpened", true)
|
||||
window.h5PopupHistory.open(this.popupHistoryToken, "honor-year")
|
||||
},
|
||||
// 选择年份、点击遮罩或再次点击标题关闭时,同步回退对应弹层历史。
|
||||
onYearFilterClose() {
|
||||
if (!this.yearFilterOpened) {
|
||||
return
|
||||
}
|
||||
this.$set(this, "yearFilterOpened", false)
|
||||
if (!this.yearHistoryClosing) {
|
||||
this.$set(this, "yearHistoryClosing",
|
||||
window.h5PopupHistory.close(this.popupHistoryToken, "honor-year"))
|
||||
}
|
||||
},
|
||||
closeYearFilter() {
|
||||
// 公共年度组件的根组件为 van-dropdown-item,复用其 toggle 关闭能力。
|
||||
const yearFilter = this.$refs.yearFilter
|
||||
if (yearFilter && yearFilter.$children[0]) {
|
||||
yearFilter.$children[0].toggle(false)
|
||||
}
|
||||
},
|
||||
switchTab(tab) {
|
||||
if (this.activeTab === tab) {
|
||||
return
|
||||
}
|
||||
this.activeTab = tab
|
||||
this.$set(this, "activeTab", tab)
|
||||
this.listHonor()
|
||||
},
|
||||
getDisplayName: function (item) {
|
||||
@@ -143,33 +215,46 @@ layout("/layouts/platform_h5.html"){
|
||||
}
|
||||
return target || null
|
||||
},
|
||||
listHonor: function () {
|
||||
var self = this
|
||||
var type = this.activeTab === "studio" ? this.honorTypeMap.studio : this.honorTypeMap.labor
|
||||
/**
|
||||
* 按当前年度和分类请求荣誉:year 为年度数字,honorType 为分类 ID,
|
||||
* pageNumber/pageSize 固定为第一页 50 条;返回 res.data.list 为荣誉对象数组。
|
||||
* 请求序号用于避免快速切换年度、分类时旧列表或旧图片覆盖最新查询结果。
|
||||
*/
|
||||
listHonor() {
|
||||
const requestId = this.honorRequestId + 1
|
||||
this.$set(this, "honorRequestId", requestId)
|
||||
const type = this.activeTab === "studio" ? this.honorTypeMap.studio : this.honorTypeMap.labor
|
||||
this.$set(this, "honorList", [])
|
||||
if (!type) {
|
||||
this.honorList = []
|
||||
this.$set(this, "tableLoading", false)
|
||||
return
|
||||
}
|
||||
this.loading = true
|
||||
this.tableLoading = true
|
||||
this.$axios.post("/platform/honor/manage/h5Data", {
|
||||
year: this.currentYear,
|
||||
honorType: type.id,
|
||||
pageNumber: 1,
|
||||
pageSize: 50
|
||||
}).then(function (res) {
|
||||
}).then((res) => {
|
||||
if (requestId !== this.honorRequestId || this._isDestroyed) {
|
||||
return
|
||||
}
|
||||
if (res.code === 0) {
|
||||
var list = (res.data && res.data.list) || []
|
||||
self.honorList = list.map(function (item) {
|
||||
item.cover = ""
|
||||
item.photoId = self.getFirstPhotoId(item.photoFiles)
|
||||
return item
|
||||
})
|
||||
self.loadPhotoMap()
|
||||
} else {
|
||||
self.honorList = []
|
||||
const list = (res.data && res.data.list) || []
|
||||
this.$set(this, "honorList", list.map((item) => {
|
||||
return Object.assign({}, item, {
|
||||
cover: "",
|
||||
photoId: this.getFirstPhotoId(item.photoFiles)
|
||||
})
|
||||
}))
|
||||
this.loadPhotoMap(requestId)
|
||||
}
|
||||
}).catch(() => {
|
||||
// 错误由全局 axios 拦截器提示;保留当前年度空列表,不显示上一年度记录。
|
||||
}).finally(() => {
|
||||
if (requestId === this.honorRequestId && !this._isDestroyed) {
|
||||
this.tableLoading = false
|
||||
}
|
||||
}).finally(function () {
|
||||
self.loading = false
|
||||
})
|
||||
},
|
||||
getFirstPhotoId: function (photoFiles) {
|
||||
@@ -194,37 +279,57 @@ layout("/layouts/platform_h5.html"){
|
||||
data = String(data)
|
||||
return data.indexOf("=") > -1 ? data.substring(data.lastIndexOf("=") + 1) : data
|
||||
},
|
||||
loadPhotoMap: function () {
|
||||
var self = this
|
||||
var ids = this.honorList.map(function (item) {
|
||||
// ids 为照片 ID 的 JSON 数组字符串,返回 res.data 为含 id、thumbnail、downloadPath 的文件数组。
|
||||
loadPhotoMap(requestId) {
|
||||
var ids = this.honorList.map((item) => {
|
||||
return item.photoId
|
||||
}).filter(function (id) {
|
||||
}).filter((id) => {
|
||||
return !!id
|
||||
})
|
||||
if (!ids.length) {
|
||||
return
|
||||
}
|
||||
this.$axios.post("/platform/sys/file/previewFileData", { ids: JSON.stringify(ids) }).then(function (res) {
|
||||
if (res.code !== 0) {
|
||||
this.$axios.post("/platform/sys/file/previewFileData", { ids: JSON.stringify(ids) }).then((res) => {
|
||||
if (res.code !== 0 || requestId !== this.honorRequestId || this._isDestroyed) {
|
||||
return
|
||||
}
|
||||
var fileMap = {}
|
||||
;(res.data || []).forEach(function (file) {
|
||||
fileMap[file.id] = file.thumbnail || file.downloadPath || ""
|
||||
})
|
||||
self.honorList = self.honorList.map(function (item) {
|
||||
item.cover = fileMap[item.photoId] || ""
|
||||
return item
|
||||
;(res.data || []).forEach((file) => {
|
||||
// 封面优先使用原图,避免放大 100×100 缩略图;原图地址缺失时才回退缩略图。
|
||||
fileMap[file.id] = file.downloadPath || file.thumbnail || ""
|
||||
})
|
||||
this.$set(this, "honorList", this.honorList.map((item) => {
|
||||
return Object.assign({}, item, { cover: fileMap[item.photoId] || "" })
|
||||
}))
|
||||
}).catch(() => {
|
||||
// 照片加载失败沿用卡片已有占位内容,错误提示交给全局拦截器。
|
||||
})
|
||||
}
|
||||
},
|
||||
created: function () {
|
||||
var self = this
|
||||
this.getHonorTypes().then(function () {
|
||||
self.listHonor()
|
||||
created() {
|
||||
// 全局历史管理器统一处理系统返回;回调先同步状态,避免 close 事件重复回退。
|
||||
this.$set(this, "popupHistoryToken", window.h5PopupHistory.register(() => {
|
||||
this.$set(this, "yearFilterOpened", false)
|
||||
this.$set(this, "yearHistoryClosing", false)
|
||||
this.closeYearFilter()
|
||||
}))
|
||||
this.getHonorTypes().then(() => {
|
||||
if (!this._isDestroyed) {
|
||||
this.listHonor()
|
||||
}
|
||||
})
|
||||
},
|
||||
mounted() {
|
||||
// PJAX 替换页面不会自动销毁根 Vue,替换前注销年度弹层回调及页面监听。
|
||||
$(document).one("pjax:beforeReplace.honorYear", () => {
|
||||
this.$destroy()
|
||||
})
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.h5PopupHistory.unregister(this.popupHistoryToken)
|
||||
$(document).off("pjax:beforeReplace.honorYear")
|
||||
},
|
||||
// 本项目将 /deep/ 直接替换为作用域属性,须紧贴页面元素;伪元素须放在 /deep/ 后。
|
||||
style: /*language=CSS*/ `
|
||||
body {
|
||||
background: #c80019;
|
||||
@@ -281,7 +386,7 @@ layout("/layouts/platform_h5.html"){
|
||||
color: #fff6d9;
|
||||
}
|
||||
|
||||
.honor-tab.active::after {
|
||||
.honor-tab.active/deep/::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
@@ -294,6 +399,82 @@ layout("/layouts/platform_h5.html"){
|
||||
box-shadow: 0 2px 8px rgba(255, 230, 163, .38);
|
||||
}
|
||||
|
||||
.honor-year-filter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
min-height: 52px;
|
||||
margin-bottom: 12px;
|
||||
padding: 6px 10px 6px 14px;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid rgba(255, 230, 163, .14);
|
||||
border-radius: 8px;
|
||||
color: #ffe6a3;
|
||||
background: rgba(255, 255, 255, .12);
|
||||
}
|
||||
|
||||
.honor-year-filter__label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.honor-year-filter__label/deep/ .van-icon {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.honor-year-filter/deep/ .honor-year-filter__menu {
|
||||
flex: 0 0 106px;
|
||||
}
|
||||
|
||||
.honor-year-filter/deep/ .van-dropdown-menu__bar {
|
||||
height: 38px;
|
||||
border: 1px solid #ffe6a3;
|
||||
border-radius: 24px;
|
||||
background: rgba(151, 0, 17, .48);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.honor-year-filter/deep/ .van-dropdown-menu__title {
|
||||
color: #ffe6a3 !important;
|
||||
font-size: 15px;
|
||||
font-weight: 400;
|
||||
line-height: 24px;
|
||||
padding: 0 18px 0 4px;
|
||||
}
|
||||
|
||||
.honor-year-filter/deep/ .van-dropdown-menu__title::after {
|
||||
right: 4px;
|
||||
border-color: transparent transparent #ffe6a3 #ffe6a3;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.honor-year-filter/deep/ .van-dropdown-item__content {
|
||||
max-height: 55vh;
|
||||
border-radius: 0 0 12px 12px;
|
||||
}
|
||||
|
||||
.honor-list/deep/ .honor-empty .van-empty__description {
|
||||
padding: 0;
|
||||
color: #fff1dc;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.honor-list/deep/ .honor-empty .van-empty__bottom {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.honor-list/deep/ .honor-empty__hint {
|
||||
color: rgba(255, 233, 189, .68);
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.honor-intro {
|
||||
position: relative;
|
||||
margin-bottom: 18px;
|
||||
|
||||
@@ -337,7 +337,7 @@ layout("/layouts/platform_h5.html"){
|
||||
</template>
|
||||
</van-cell>
|
||||
<van-popup position="bottom" round :close-on-click-overlay="true" v-model:show="showReimburseProjectPicker" class="union-half-popup">
|
||||
<van-picker :columns="reimburseProjectColumns" @cancel="closePopup('showReimburseProjectPicker')"
|
||||
<van-picker :columns="availableReimburseProjectColumns" @cancel="closePopup('showReimburseProjectPicker')"
|
||||
@confirm="onReimburseProjectConfirm" show-toolbar></van-picker>
|
||||
</van-popup>
|
||||
<van-field
|
||||
@@ -680,6 +680,12 @@ layout("/layouts/platform_h5.html"){
|
||||
required
|
||||
is-link
|
||||
></van-field>
|
||||
<van-field v-if="isManualReimburseAmount"
|
||||
label="报销金额" name="money"
|
||||
v-model="formData.money"
|
||||
placeholder="请输入报销金额,最多两位小数"
|
||||
:rules="[{ required: true, message: '请填写报销金额' }, { validator: () => !getReimburseAmountMessage(), message: getReimburseAmountMessage }]"
|
||||
required></van-field>
|
||||
<van-popup position="bottom" round :close-on-click-overlay="true" v-model:show="showActivityTimePicker" class="union-half-popup">
|
||||
<van-datetime-picker type="date" title="选择活动时间" :min-date="minDate" :max-date="maxDate"
|
||||
@cancel="closePopup('showActivityTimePicker')"
|
||||
@@ -692,8 +698,9 @@ layout("/layouts/platform_h5.html"){
|
||||
<section class="form-section">
|
||||
<h2 class="form-section__title">附件信息</h2>
|
||||
<van-cell-group :border="false">
|
||||
<van-field class="more-text reimburse-upload-field" name="files" required
|
||||
label="" :rules="[{ required: true, message: '请上传附件' }]">
|
||||
<van-field class="more-text reimburse-upload-field" name="files"
|
||||
:required="!['UNION_REIMBURSE_PROJECT_2', 'UNION_REIMBURSE_PROJECT_3', 'UNION_REIMBURSE_PROJECT_5'].includes(formData.reimburseProject)"
|
||||
label="" :rules="['UNION_REIMBURSE_PROJECT_2', 'UNION_REIMBURSE_PROJECT_3', 'UNION_REIMBURSE_PROJECT_5'].includes(formData.reimburseProject) ? [] : [{ required: true, message: '请上传附件' }]">
|
||||
<template #input>
|
||||
<h5-file-upload
|
||||
slot="input"
|
||||
@@ -706,6 +713,11 @@ layout("/layouts/platform_h5.html"){
|
||||
></h5-file-upload>
|
||||
</template>
|
||||
</van-field>
|
||||
<van-cell v-if="formData.reimburseProject === 'UNION_REIMBURSE_PROJECT_5'">
|
||||
<template #title>
|
||||
<span style="color: #ee0a24;">社团活动附件说明</span>
|
||||
</template>
|
||||
</van-cell>
|
||||
</van-cell-group>
|
||||
</section>
|
||||
<van-cell-group class="form-section">
|
||||
@@ -762,10 +774,10 @@ layout("/layouts/platform_h5.html"){
|
||||
</van-cell-group>
|
||||
<van-cell-group class="form-section" v-if="formData.reimburseProject">
|
||||
<div class="form-section__title">电子发票(选填)</div>
|
||||
<div class="invoice-tip">无发票时按支付信息汇总报销金额;上传的发票仍须通过验真。</div>
|
||||
<div class="invoice-tip">{{ isManualReimburseAmount ? '有发票时报销金额不得超过发票总额,无发票仍可提交;上传的发票仍须通过验真。' : '无发票时按支付信息汇总报销金额;上传的发票仍须通过验真。' }}</div>
|
||||
<van-field label="报销金额"
|
||||
v-model="formData.money"
|
||||
placeholder="有发票按发票汇总,无发票按支付信息汇总"
|
||||
:placeholder="isManualReimburseAmount ? '请在报销信息填写报销金额' : '有发票按发票汇总,无发票按支付信息汇总'"
|
||||
readonly></van-field>
|
||||
<van-field label="发票张数"
|
||||
v-model="formData.invoiceNumber"
|
||||
@@ -847,6 +859,11 @@ layout("/layouts/platform_h5.html"){
|
||||
></h5-file-upload>
|
||||
</template>
|
||||
</van-field>
|
||||
<van-cell v-if="formData.reimburseProject === 'UNION_REIMBURSE_PROJECT_5'">
|
||||
<template #title>
|
||||
<span style="color: #ee0a24;">社团活动附件说明</span>
|
||||
</template>
|
||||
</van-cell>
|
||||
<van-field label="发票号码"
|
||||
v-model="detailForm.invoiceNo" readonly
|
||||
placeholder="请输入发票号码"
|
||||
@@ -1136,6 +1153,10 @@ layout("/layouts/platform_h5.html"){
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isManualReimburseAmount() {
|
||||
return ["UNION_REIMBURSE_PROJECT_2", "UNION_REIMBURSE_PROJECT_3", "UNION_REIMBURSE_PROJECT_4"]
|
||||
.includes(this.formData.reimburseProject)
|
||||
},
|
||||
// 发票或支付明细变化时同步展示超额提示。
|
||||
paymentAmountLimitMessage() {
|
||||
return this.getPaymentAmountLimitMessage(this.formData.paymentDetails || [])
|
||||
@@ -1147,9 +1168,17 @@ layout("/layouts/platform_h5.html"){
|
||||
const selectedType = this.typeOptions.find(item => item.id === typeId)
|
||||
return selectedType ? selectedType.code : ""
|
||||
},
|
||||
// 仅系统管理员和校工会管理员可查看真实余额,其他账号统一显示余额遮罩。
|
||||
// 社团经费按菜单授权查看年度剩余额度,其他经费沿用原余额可见范围。
|
||||
canViewFundBalance() {
|
||||
return this.$auth.hasRoleOr(["SYSADMIN", "SCHOOL_UNION_ADMIN"])
|
||||
|| (this.formData.reimburseFundSource === "UNION_REIMBURSE_FUND_SOURCE_3"
|
||||
&& this.$auth.hasPermission("unionReimburse.club"))
|
||||
},
|
||||
// 社团经费仅可选择社团活动,其他经费不展示社团活动。
|
||||
availableReimburseProjectColumns() {
|
||||
const clubFund = this.formData.reimburseFundSource === "UNION_REIMBURSE_FUND_SOURCE_3"
|
||||
return this.reimburseProjectColumns.filter(item =>
|
||||
clubFund ? item.value === "UNION_REIMBURSE_PROJECT_5" : item.value !== "UNION_REIMBURSE_PROJECT_5")
|
||||
},
|
||||
paymentInfoRows() {
|
||||
return this.formData.paymentDetails || []
|
||||
@@ -1233,6 +1262,9 @@ layout("/layouts/platform_h5.html"){
|
||||
},
|
||||
"formData.reimburseProject"(newVal) {
|
||||
this.handleReimburseProjectChange(newVal)
|
||||
},
|
||||
"formData.reimburseFundSource"() {
|
||||
this.syncReimburseProject()
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1852,6 +1884,27 @@ layout("/layouts/platform_h5.html"){
|
||||
this.rebuildInvoiceSummary()
|
||||
})
|
||||
},
|
||||
// 手填金额按分比较票面总额;未上传发票时仍允许申请。
|
||||
getReimburseAmountMessage() {
|
||||
if (!this.isManualReimburseAmount) return ""
|
||||
const value = String(this.formData.money == null ? "" : this.formData.money)
|
||||
if (!value) return "请填写报销金额"
|
||||
const cents = this.moneyToCents(value)
|
||||
if (!/^\d+(\.\d{1,2})?$/.test(value) || cents === null || cents <= 0) {
|
||||
return "报销金额必须为正数,且最多两位小数"
|
||||
}
|
||||
const invoices = this.formData.invoiceDetails || []
|
||||
let invoiceCents = 0
|
||||
for (let i = 0; i < invoices.length; i++) {
|
||||
const amount = this.moneyToCents(invoices[i] && invoices[i].invoiceAmount)
|
||||
if (amount === null || amount <= 0) return "请填写正确的发票金额"
|
||||
invoiceCents += amount
|
||||
}
|
||||
if (invoices.length && cents > invoiceCents) {
|
||||
return "报销金额不能超过发票总额(" + (invoiceCents / 100).toFixed(2) + "元)"
|
||||
}
|
||||
return ""
|
||||
},
|
||||
rebuildInvoiceSummary() {
|
||||
let totalMoney = 0
|
||||
let fileCount = 0
|
||||
@@ -1876,10 +1929,18 @@ layout("/layouts/platform_h5.html"){
|
||||
})
|
||||
totalMoney = paymentCents / 100
|
||||
}
|
||||
this.$set(this.formData, "money", totalMoney ? totalMoney.toFixed(2) : "0.00")
|
||||
// 三类活动保留手填申请金额,发票及支付明细变化只更新张数。
|
||||
if (!this.isManualReimburseAmount) {
|
||||
this.$set(this.formData, "money", totalMoney ? totalMoney.toFixed(2) : "0.00")
|
||||
}
|
||||
this.$set(this.formData, "invoiceNumber", String(fileCount))
|
||||
},
|
||||
validateInvoiceDetailsBeforeSubmit() {
|
||||
const amountMessage = this.getReimburseAmountMessage()
|
||||
if (amountMessage) {
|
||||
this.$toast.fail(amountMessage)
|
||||
return false
|
||||
}
|
||||
const invoiceDetails = this.formData.invoiceDetails || []
|
||||
// 发票可不上传;已经添加的发票仍须填写完整并沿用验真流程。
|
||||
for (let i = 0; i < invoiceDetails.length; i++) {
|
||||
@@ -2366,6 +2427,15 @@ layout("/layouts/platform_h5.html"){
|
||||
}
|
||||
this.closePopup("showPaymentWayPicker")
|
||||
},
|
||||
// 新建、切换来源及编辑回显都保持社团经费与社团活动一致。
|
||||
syncReimburseProject() {
|
||||
const clubFund = this.formData.reimburseFundSource === "UNION_REIMBURSE_FUND_SOURCE_3"
|
||||
if (!clubFund && this.formData.reimburseProject !== "UNION_REIMBURSE_PROJECT_5") return
|
||||
const projectCode = clubFund ? "UNION_REIMBURSE_PROJECT_5" : "UNION_REIMBURSE_PROJECT_2"
|
||||
const project = this.reimburseProjectColumns.find(item => item.value === projectCode)
|
||||
this.$set(this.formData, "reimburseProject", project ? project.value : "")
|
||||
this.$set(this.formData, "reimburseProjectName", project ? project.text : "")
|
||||
},
|
||||
onReimburseProjectConfirm(o) {
|
||||
this.$set(this.formData, "reimburseProjectName", o.text) // 显示名称
|
||||
this.$set(this.formData, "reimburseProject", o.value) // 字典值
|
||||
@@ -2441,16 +2511,10 @@ layout("/layouts/platform_h5.html"){
|
||||
},
|
||||
// 新建申请默认优先选择分工会经费;若当前用户没有对应权限,则按页面可见权限回退到其它经费来源。
|
||||
getDefaultFundSource() {
|
||||
if (this.$auth.hasPermission("unionReimburse.fgh")) {
|
||||
return "UNION_REIMBURSE_FUND_SOURCE_2"
|
||||
}
|
||||
if (this.$auth.hasPermission("unionReimburse.xgh")) {
|
||||
return "UNION_REIMBURSE_FUND_SOURCE_1"
|
||||
}
|
||||
if (this.$auth.hasPermission("unionReimburse.club")) {
|
||||
return "UNION_REIMBURSE_FUND_SOURCE_3"
|
||||
}
|
||||
return ""
|
||||
const option = this.reimburseFundSourceColumns.find(item => item.value === "UNION_REIMBURSE_FUND_SOURCE_2")
|
||||
|| this.reimburseFundSourceColumns.find(item => item.value === "UNION_REIMBURSE_FUND_SOURCE_3")
|
||||
|| this.reimburseFundSourceColumns[0]
|
||||
return option ? option.value : ""
|
||||
},
|
||||
getReimburseFundSourceName(code) {
|
||||
const matchedFundSource = this.reimburseFundSourceColumns.find(item => item.value === code)
|
||||
@@ -2485,7 +2549,7 @@ layout("/layouts/platform_h5.html"){
|
||||
this.$set(this, "reimburseProjectList", reimburseProjectData)
|
||||
return this.$businessTool.getDictOptions("UNION_REIMBURSE_FUND_SOURCE")
|
||||
}).then((reimburseFundSourceData) => {
|
||||
this.$set(this, "reimburseFundSourceColumns", reimburseFundSourceData.map(item => ({value: item.code, text: item.name})))
|
||||
this.$set(this, "reimburseFundSourceColumns", reimburseFundSourceData.map(item => ({value: item.code, text: item.code === "UNION_REIMBURSE_FUND_SOURCE_3" ? "协会经费" : item.name})))
|
||||
this.filterReimburseFundSources()
|
||||
})
|
||||
},
|
||||
@@ -2567,7 +2631,8 @@ layout("/layouts/platform_h5.html"){
|
||||
|
||||
// 查询社团
|
||||
queryClubs() {
|
||||
return this.$businessTool.listClubByRole().then((res) => {
|
||||
return this.$axios.post("/platform/unionReimburse/apply/listReimburseClubs").then((response) => {
|
||||
const res = response.data || []
|
||||
this.$set(this, "clubOptions", res)
|
||||
this.$set(this, "clubColumns", res.map(item => {
|
||||
return { text: item.clubName, value: item.id }
|
||||
@@ -2601,7 +2666,7 @@ layout("/layouts/platform_h5.html"){
|
||||
url: "/platform/unionReimburse/apply/getUnionBalance"
|
||||
},
|
||||
"UNION_REIMBURSE_FUND_SOURCE_3": {
|
||||
url: "/platform/unionReimburse/apply/getClubBudget"
|
||||
url: "/platform/unionReimburse/apply/getBudgetBalance"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2630,14 +2695,20 @@ layout("/layouts/platform_h5.html"){
|
||||
// 发送请求
|
||||
this.$axios.post(apiInfo.url, params)
|
||||
.then(response => {
|
||||
// 切换来源或社团后,旧请求不得覆盖当前余额。
|
||||
if (fundSource !== this.formData.reimburseFundSource
|
||||
|| (params.clubId && params.clubId !== this.formData.clubId)) return
|
||||
if (response.code === 0) {
|
||||
// 显示经费余额(只显示数字,不显示"元")
|
||||
this.$set(this.formData, "fundBalance", response.data || "0.00")
|
||||
this.$set(this.formData, "fundBalance", fundSource === "UNION_REIMBURSE_FUND_SOURCE_3"
|
||||
? Number(response.data || 0).toFixed(2) : (response.data || "0.00"))
|
||||
} else {
|
||||
this.$set(this.formData, "fundBalance", "查询失败")
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
if (fundSource !== this.formData.reimburseFundSource
|
||||
|| (params.clubId && params.clubId !== this.formData.clubId)) return
|
||||
console.error("经费余额查询失败:", error)
|
||||
this.$set(this.formData, "fundBalance", "查询失败")
|
||||
this.$toast.fail("经费余额查询失败")
|
||||
@@ -2807,12 +2878,11 @@ layout("/layouts/platform_h5.html"){
|
||||
this.$set(this.formData, "unionId", user.union.id)
|
||||
this.$set(this.formData, "unionName", user.union.name)
|
||||
const defaultFundSource = this.getDefaultFundSource()
|
||||
this.$set(this.formData, "reimburseFundSource", defaultFundSource)
|
||||
this.$set(this.formData, "reimburseFundSourceName", this.getReimburseFundSourceName(defaultFundSource))
|
||||
this.$set(this.formData, "paymentDetails", [])
|
||||
if (this.formData.reimburseFundSource) {
|
||||
this.queryFundBalance()
|
||||
}
|
||||
this.onReimburseFundSourceConfirm({
|
||||
value: defaultFundSource,
|
||||
text: this.getReimburseFundSourceName(defaultFundSource)
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -632,18 +632,28 @@ layout("/layouts/platform_h5.html"){
|
||||
|
||||
<!-- 保存、首次提交和重新提交继续共用原业务入口。 -->
|
||||
<div class="proposal-actions" v-if="isWriteTime">
|
||||
<van-button plain type="info" :loading="formLoading && loadingAction === 'save'" :disabled="formLoading" @click="onSave">
|
||||
<van-button plain type="info" :loading="formLoading && loadingAction === 'save'" :disabled="formLoading || !isDelegate" @click="onSave">
|
||||
保存草稿
|
||||
</van-button>
|
||||
<van-button type="primary" :loading="formLoading && loadingAction === 'submit'" :disabled="formLoading" @click="onSubmit" v-if="!taskId">
|
||||
<van-button type="primary" :loading="formLoading && loadingAction === 'submit'" :disabled="formLoading || !isDelegate" @click="onSubmit" v-if="!taskId">
|
||||
提交提案
|
||||
</van-button>
|
||||
<van-button type="primary" :loading="formLoading && loadingAction === 'submitAgain'" :disabled="formLoading" @click="onSubmitAgain" v-else>
|
||||
<van-button type="primary" :loading="formLoading && loadingAction === 'submitAgain'" :disabled="formLoading || !isDelegate" @click="onSubmitAgain" v-else>
|
||||
重新提交
|
||||
</van-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 非代表提示由统一历史栈控制;确认先清理弹层历史,再返回上一业务页面。 -->
|
||||
<van-dialog :value="delegateDeniedVisible"
|
||||
title="提示"
|
||||
message="当前用户不是代表,无法撰写提案"
|
||||
confirm-button-text="确认"
|
||||
:close-on-click-overlay="false"
|
||||
:close-on-popstate="false"
|
||||
@confirm="confirmDelegateDenied">
|
||||
</van-dialog>
|
||||
|
||||
<!-- 提案撰写须知弹窗 -->
|
||||
<van-action-sheet v-model="noticeDialogVisible"
|
||||
title="提案撰写须知"
|
||||
@@ -684,6 +694,8 @@ layout("/layouts/platform_h5.html"){
|
||||
|
||||
proposalConfig: {},
|
||||
noticeDialogVisible: false,
|
||||
delegateDeniedVisible: false,
|
||||
isDelegate: false,
|
||||
|
||||
isWriteTime: true,
|
||||
formLoading: false,
|
||||
@@ -699,6 +711,38 @@ layout("/layouts/platform_h5.html"){
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 确认非代表提示后,等待弹层历史回退完成再使用统一页面返回入口。
|
||||
confirmDelegateDenied() {
|
||||
window.h5HistoryLayerManager.closeAll(() => historyBack());
|
||||
},
|
||||
|
||||
/**
|
||||
* 按当前表单的 sessionId 查询登录用户身份,不传客户端用户ID。
|
||||
* @return {Promise<Boolean>} data 为 true 时允许继续初始化;false 时展示非代表提示。
|
||||
* 请求异常沿用全局错误提示并保持禁止提交,不将网络或权限错误当作非代表。
|
||||
*/
|
||||
checkDelegateIdentity() {
|
||||
const sessionId = this.formData.sessionId;
|
||||
this.$set(this, "isDelegate", false);
|
||||
return this.$axios.post("/platform/h5/proposal/common/isCurrentUserDelegate", {sessionId: sessionId})
|
||||
.then((res) => {
|
||||
// 页面退出或届次发生变化后,丢弃旧查询结果,避免在其他页面打开提示。
|
||||
if (this._isDestroyed || window.h5HistoryLayerManager.pageKey !== this.historyLayerPageKey ||
|
||||
this.formData.sessionId !== sessionId) {
|
||||
return false;
|
||||
}
|
||||
if (res.code !== 0 || typeof res.data !== "boolean") {
|
||||
this.$toast("代表身份查询失败,请稍后重试");
|
||||
throw new Error("代表身份查询结果异常");
|
||||
}
|
||||
this.$set(this, "isDelegate", res.data);
|
||||
if (!res.data) {
|
||||
window.h5HistoryLayerManager.open("delegate-denied");
|
||||
}
|
||||
return res.data;
|
||||
});
|
||||
},
|
||||
|
||||
handlePageBack() {
|
||||
if (window.h5HistoryLayerManager.stack.length) {
|
||||
window.h5HistoryLayerManager.close();
|
||||
@@ -722,6 +766,8 @@ layout("/layouts/platform_h5.html"){
|
||||
noticeDialogVisible: "notice"
|
||||
}[stateName];
|
||||
if (visible) {
|
||||
// 初始化或保存、提交期间禁止切换届次,避免并发请求混用不同届次的身份结果。
|
||||
if (this.formLoading) return;
|
||||
window.h5HistoryLayerManager.open(layerName);
|
||||
return;
|
||||
}
|
||||
@@ -736,18 +782,22 @@ layout("/layouts/platform_h5.html"){
|
||||
this.$set(this, "showCommitteePicker", stack.includes("committee-picker"));
|
||||
this.$set(this, "showSuggestionUnitPicker", stack.includes("suggestion-unit-picker"));
|
||||
this.$set(this, "noticeDialogVisible", stack.includes("notice"));
|
||||
this.$set(this, "delegateDeniedVisible", stack.includes("delegate-denied"));
|
||||
});
|
||||
},
|
||||
|
||||
// 选择器确认事件
|
||||
onSessionConfirm(value) {
|
||||
this.formData.sessionId = value.value;
|
||||
if (this.formLoading) return;
|
||||
this.$set(this.formData, 'sessionId', value.value);
|
||||
const selectedOption = this.sessionOptions.find(item => item.id === value.value);
|
||||
if (selectedOption) {
|
||||
this.$set(this.formData, 'sessionName', selectedOption.fullName);
|
||||
}
|
||||
this.setVisibleState("showSessionPicker", false);
|
||||
this.meetingChange(value.value);
|
||||
this.$set(this, "isDelegate", false);
|
||||
this.formLoading = true;
|
||||
// 等待选择器对应历史回退完成后再查询,防止新提示被此次返回一起关闭。
|
||||
window.h5HistoryLayerManager.close("session-picker", () => this.meetingChange(value.value));
|
||||
},
|
||||
|
||||
onSourceConfirm(value) {
|
||||
@@ -810,7 +860,7 @@ layout("/layouts/platform_h5.html"){
|
||||
|
||||
// 保存草稿:校验当前表单后保存 ProposalInfo,接口成功时返回提案数据并回到我的提案。
|
||||
onSave() {
|
||||
if (this.formLoading) {
|
||||
if (this.formLoading || !this.isDelegate) {
|
||||
return;
|
||||
}
|
||||
this.$refs.formRef.validate().then(() => {
|
||||
@@ -862,7 +912,7 @@ layout("/layouts/platform_h5.html"){
|
||||
return true;
|
||||
},
|
||||
|
||||
// 提交成功后等待提示和 PJAX 跳转完成,避免跳转期间提前解除按钮的 loading 状态。
|
||||
// 提交成功后先清理弹层历史,再等待 PJAX 跳转完成,避免残留弹层记录或提前解除 loading。
|
||||
navigateToMine(delay = 0) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const navigate = () => {
|
||||
@@ -880,16 +930,16 @@ layout("/layouts/platform_h5.html"){
|
||||
}
|
||||
};
|
||||
if (delay > 0) {
|
||||
setTimeout(navigate, delay);
|
||||
setTimeout(() => window.h5HistoryLayerManager.closeAll(navigate), delay);
|
||||
} else {
|
||||
navigate();
|
||||
window.h5HistoryLayerManager.closeAll(navigate);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 首次提交:info 为完整提案 JSON,成功后进入提案流程并返回我的提案。
|
||||
onSubmit() {
|
||||
if (this.formLoading) {
|
||||
if (this.formLoading || !this.isDelegate) {
|
||||
return;
|
||||
}
|
||||
this.$refs.formRef.validate().then(() => {
|
||||
@@ -934,7 +984,7 @@ layout("/layouts/platform_h5.html"){
|
||||
|
||||
// 重新提交:除完整提案 JSON 外携带 taskId,用于继续当前退回任务。
|
||||
onSubmitAgain() {
|
||||
if (this.formLoading) {
|
||||
if (this.formLoading || !this.isDelegate) {
|
||||
return;
|
||||
}
|
||||
this.$refs.formRef.validate().then(() => {
|
||||
@@ -1002,8 +1052,9 @@ layout("/layouts/platform_h5.html"){
|
||||
});
|
||||
},
|
||||
|
||||
// 切换教代会后清空跨届次数据,并按类型、代表团、提案方式、撰写时间的顺序重新加载。
|
||||
// 切换教代会后先校验代表身份,通过后再加载该届次的业务选项。
|
||||
meetingChange(val) {
|
||||
this.formLoading = true;
|
||||
this.$set(this.formData, "typeId", null);
|
||||
this.$set(this.formData, "typeName", "");
|
||||
this.typeOptions = [];
|
||||
@@ -1011,11 +1062,19 @@ layout("/layouts/platform_h5.html"){
|
||||
this.$set(this.formData, "committeeId", null);
|
||||
this.$set(this.formData, "delegationName", '');
|
||||
this.$set(this.formData, "committeeName", '');
|
||||
return this.listProposalType()
|
||||
.then(() => this.listDelegation())
|
||||
.then(() => this.searchMineDelegation())
|
||||
.then(() => this.listSource())
|
||||
.then(() => this.checkWriteTime());
|
||||
return this.checkDelegateIdentity().then((allowed) => {
|
||||
if (!allowed) return;
|
||||
return this.listProposalType()
|
||||
.then(() => this.listDelegation())
|
||||
.then(() => this.searchMineDelegation())
|
||||
.then(() => this.listSource())
|
||||
.then(() => this.checkWriteTime());
|
||||
}).catch(() => {
|
||||
// 统一请求拦截器已提示失败;禁止使用未完整加载的届次数据提交。
|
||||
this.$set(this, "isDelegate", false);
|
||||
}).finally(() => {
|
||||
this.formLoading = false;
|
||||
});
|
||||
},
|
||||
|
||||
// 获取代表团
|
||||
@@ -1044,20 +1103,33 @@ layout("/layouts/platform_h5.html"){
|
||||
if (!isModify && this.sessionOptions && this.sessionOptions.length > 0) {
|
||||
this.$set(this.formData, "sessionId", this.sessionOptions[0].id);
|
||||
this.$set(this.formData, "sessionName", this.sessionOptions[0].fullName);
|
||||
}
|
||||
// 无届次不能查询代表身份,也不能当作非代表提示。
|
||||
if (!this.formData.sessionId) {
|
||||
this.$toast("没有可用的教代会,暂时无法撰写提案");
|
||||
return Promise.resolve();
|
||||
}
|
||||
return this.checkDelegateIdentity().then((allowed) => {
|
||||
if (!allowed) return;
|
||||
if (isModify) {
|
||||
// 编辑以提案原届次校验身份,保留已有表单和选择器回显。
|
||||
return this.listDelegation()
|
||||
.then(() => this.listSource())
|
||||
.then(() => this.listProposalType())
|
||||
.then(() => this.echoPicker());
|
||||
}
|
||||
return this.listProposalType()
|
||||
.then(() => this.listDelegation())
|
||||
.then(() => this.listSource())
|
||||
.then(() => this.searchMineDelegation())
|
||||
.then(() => this.checkWriteTime());
|
||||
}
|
||||
// 没有开启的教代会时不继续请求依赖 sessionId 的接口。
|
||||
if (!this.formData.sessionId) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return this.listDelegation()
|
||||
.then(() => this.listSource())
|
||||
.then(() => this.listProposalType())
|
||||
.then(() => this.echoPicker());
|
||||
.then(() => this.checkWriteTime())
|
||||
.then(() => {
|
||||
// 初始化请求完成前可能已离开页面,此时不能给新页面追加撰写须知历史。
|
||||
if (!this._isDestroyed && window.h5HistoryLayerManager.pageKey === this.historyLayerPageKey) {
|
||||
window.h5HistoryLayerManager.open("notice");
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
@@ -1118,18 +1190,15 @@ layout("/layouts/platform_h5.html"){
|
||||
|
||||
init() {
|
||||
if (this.bizId) {
|
||||
this.$axios.post("/platform/proposal/write/detail", {id: this.bizId}).then((res) => {
|
||||
return this.$axios.post("/platform/proposal/write/detail", {id: this.bizId}).then((res) => {
|
||||
if (res.code === 0) {
|
||||
res.data.brief=removeHTMLTag(res.data.brief)
|
||||
res.data.measures=removeHTMLTag(res.data.measures)
|
||||
this.$set(this, "formData", res.data);
|
||||
this.listOpenSession(true);
|
||||
return this.listOpenSession(true);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.setVisibleState("noticeDialogVisible", true);
|
||||
this.listOpenSession(false);
|
||||
|
||||
// 设置用户信息
|
||||
this.$set(this.formData, "createUserId", this.$store.state.user.id);
|
||||
this.$set(this.formData, "createUserName", this.$store.state.user.username);
|
||||
@@ -1137,12 +1206,20 @@ layout("/layouts/platform_h5.html"){
|
||||
this.$set(this.formData, "createTime", this.$moment().format("YYYY-MM-DD"));
|
||||
this.$set(this.formData, "mobile", this.$store.state.user.mobile);
|
||||
this.$set(this.formData, "unitName", this.$store.state.user.unit?.name);
|
||||
return this.listOpenSession(false);
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.registerHistoryLayers();
|
||||
this.init();
|
||||
// 从确定届次到完成身份检查及表单初始化期间,保持保存、提交按钮禁用。
|
||||
this.formLoading = true;
|
||||
this.init().catch(() => {
|
||||
// 网络、权限或初始化失败由统一拦截器提示,不放行尚未完成初始化的表单。
|
||||
this.$set(this, "isDelegate", false);
|
||||
}).finally(() => {
|
||||
this.formLoading = false;
|
||||
});
|
||||
this.listSuggestUnit();
|
||||
this.getProposalConfig();
|
||||
},
|
||||
|
||||
@@ -0,0 +1,249 @@
|
||||
<!--#
|
||||
layout("/layouts/platform_h5.html"){
|
||||
#-->
|
||||
<style>
|
||||
.inclusive-page { background: #f6f7f9; min-height: 100vh; }
|
||||
.inclusive-tab-wrap { padding: 10px 12px 8px; background: #fff; box-shadow: 0 2px 8px rgba(8, 23, 118, .08); }
|
||||
.inclusive-tabs { background: transparent; border-radius: 0; padding: 0; box-shadow: none; }
|
||||
.inclusive-tabs .van-tabs__wrap { height: 32px; overflow: hidden; }
|
||||
.inclusive-tabs .van-tabs__nav { display: flex; overflow-x: auto; overflow-y: hidden; flex-wrap: nowrap; -webkit-overflow-scrolling: touch; padding-right: 10px; background: transparent; }
|
||||
.inclusive-tabs .van-tabs__nav::-webkit-scrollbar { display: none; }
|
||||
.inclusive-tabs .van-tab { flex: 0 0 auto; padding: 0 5px; }
|
||||
.inclusive-tabs .van-tab__text { padding: 7px 12px; border-radius: 9px; background: #f2f5f8; color: #303846; font-size: 12px; line-height: 18px; white-space: nowrap; font-weight: 600; }
|
||||
.inclusive-tabs .van-tab--active .van-tab__text { color: #fff; background: #081776; }
|
||||
.inclusive-tabs .van-tabs__line { display: none; }
|
||||
.inclusive-list { padding: 12px 12px calc(22px + env(safe-area-inset-bottom)); }
|
||||
.inclusive-card { overflow: hidden; margin-bottom: 14px; background: #fff; border-radius: 12px; box-shadow: 0 3px 10px rgba(31, 45, 61, .14); }
|
||||
.inclusive-cover { width: 100%; height: 150px; overflow: hidden; background: #e8eef6; }
|
||||
.inclusive-cover img { width: 100%; height: 100%; display: block; }
|
||||
.inclusive-card-main { display: flex; flex-direction: column; min-width: 0; min-height: 132px; position: relative; padding: 12px 42px 14px 14px; }
|
||||
.inclusive-tag { display: inline-block; max-width: 120px; padding: 3px 9px; border-radius: 5px; color: #0b63d8; background: #eaf3ff; font-size: 12px; line-height: 18px; }
|
||||
.inclusive-title { margin-top: 8px; color: #111827; font-size: 16px; line-height: 24px; font-weight: bold; min-height: 24px; }
|
||||
.inclusive-summary { margin-top: 5px; color: #53657f; font-size: 13px; line-height: 19px; min-height: 38px; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
|
||||
.inclusive-date { margin-top: auto; color: #6b7c96; font-size: 12px; display: flex; align-items: center; gap: 4px; }
|
||||
.inclusive-arrow { color: #8a96a8; position: absolute; right: 14px; bottom: 16px; font-size: 22px; }
|
||||
.inclusive-empty { min-height: 320px; display: flex; align-items: center; justify-content: center; flex-direction: column; color: #909399; font-size: 14px; background: transparent; }
|
||||
.inclusive-empty-icon { width: 120px; height: 120px; margin-bottom: 10px; opacity: .56; }
|
||||
.inclusive-detail-popup { width: 100%; height: 100%; background: #fff; }
|
||||
.inclusive-detail-popup .van-nav-bar { position: sticky; top: 0; z-index: 1; }
|
||||
.inclusive-detail-loading { padding: 32px; text-align: center; }
|
||||
.inclusive-content { padding: 16px; line-height: 1.8; word-break: break-word; }
|
||||
.inclusive-content img { height: auto; max-width: 100%; }
|
||||
.inclusive-footer { padding: 12px 12px calc(12px + env(safe-area-inset-bottom)); }
|
||||
</style>
|
||||
<div class="inclusive-page" id="app" v-cloak>
|
||||
<van-nav-bar @click-left="back" fixed left-arrow left-text="返回" placeholder title="幸福工荟"></van-nav-bar>
|
||||
<van-sticky :offset-top="46">
|
||||
<div class="inclusive-tab-wrap">
|
||||
<van-tabs @change="tagChange" class="inclusive-tabs" v-model="activeTag">
|
||||
<van-tab name="" title="全部"></van-tab>
|
||||
<van-tab :key="item.value" :name="item.value" :title="item.text" v-for="item in tagOptions"></van-tab>
|
||||
</van-tabs>
|
||||
</div>
|
||||
</van-sticky>
|
||||
<div class="inclusive-list">
|
||||
<van-pull-refresh @refresh="onRefresh" v-model="refreshing">
|
||||
<van-list :immediate-check="false" :error.sync="loadError" error-text="加载失败,点击重试" :finished="finished" :finished-text="tableData.length ? '没有更多了' : ''" @load="pageData" v-model="loading">
|
||||
<div :key="item.id" @click="openDetail(item)" class="inclusive-card" v-for="item in tableData">
|
||||
<div class="inclusive-cover"><img :src="previewUrl(item.cover)" v-if="item.cover"></div>
|
||||
<div class="inclusive-card-main">
|
||||
<div class="inclusive-tag van-ellipsis">{{item.tagTypeName || '幸福工荟'}}</div>
|
||||
<div class="inclusive-title van-multi-ellipsis--l2">{{item.title}}</div>
|
||||
<div class="inclusive-summary">{{item.summary}}</div>
|
||||
<div class="inclusive-date"><van-icon name="underway-o"></van-icon><span>{{formatDate(item.publishTime || item.createdAt)}}</span></div>
|
||||
<van-icon class="inclusive-arrow" name="arrow"></van-icon>
|
||||
</div>
|
||||
</div>
|
||||
</van-list>
|
||||
</van-pull-refresh>
|
||||
<div class="inclusive-empty" v-if="!loading && tableData.length === 0">
|
||||
<img class="inclusive-empty-icon" src="https://img01.yzcdn.cn/vant/empty-image-default.png">
|
||||
<div>暂无数据</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 详情在当前列表内从右侧滑入,关闭后保留筛选、分页和列表滚动位置。 -->
|
||||
<van-popup :value="detailVisible" position="right" class="inclusive-detail-popup" :close-on-click-overlay="false" :close-on-popstate="false">
|
||||
<van-nav-bar @click-left="closeDetail" left-arrow left-text="返回" title="幸福工荟详情"></van-nav-bar>
|
||||
<div class="inclusive-detail-loading" v-if="formLoading"><van-loading>加载中...</van-loading></div>
|
||||
<template v-else>
|
||||
<van-image :src="previewUrl(detail.cover)" fit="cover" height="220" v-if="detail.cover" width="100%"></van-image>
|
||||
<van-cell-group v-if="detail.id">
|
||||
<van-cell :title="detail.title"></van-cell>
|
||||
<van-cell :value="detail.tagTypeName || '幸福工荟'" title="标签"></van-cell>
|
||||
<van-cell :value="formatDate(detail.publishTime || detail.createdAt)" title="发布时间"></van-cell>
|
||||
<van-cell :label="detail.summary" title="摘要"></van-cell>
|
||||
</van-cell-group>
|
||||
<van-empty v-if="!detail.id" description="内容不存在或已下架"></van-empty>
|
||||
<div class="inclusive-content" v-html="detail.content"></div>
|
||||
<div class="inclusive-footer" v-if="hasLink"><van-button @click="openLink" block type="primary">查看活动页面</van-button></div>
|
||||
</template>
|
||||
</van-popup>
|
||||
</div>
|
||||
<script nonce="${cspNonce!}">
|
||||
new Vue({
|
||||
el: "#app",
|
||||
data() {
|
||||
return {
|
||||
activeTag: "", tagOptions: [], tableData: [], loading: false, refreshing: false,
|
||||
finished: false, loadError: false, requesting: false, loadSequence: 0,
|
||||
detailVisible: false, detailClosing: false, detail: {}, formLoading: false,
|
||||
detailSequence: 0, popupHistoryToken: "",
|
||||
pageForm: {pageNumber: 1, pageSize: 6, totalCount: 0, tagType: ""}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
hasLink() { return !!(this.detail.linkUrl && this.detail.linkUrl.trim()) }
|
||||
},
|
||||
methods: {
|
||||
previewUrl(cover) { return cover || "" },
|
||||
formatDate(value) { return value ? moment(value).format("YYYY-MM-DD") : "" },
|
||||
back() {
|
||||
if (this.detailVisible) {
|
||||
this.closeDetail()
|
||||
return
|
||||
}
|
||||
this.historyBack()
|
||||
},
|
||||
// 无参数;data 数组中的 text/value 分别对应标签名称和标签编码。
|
||||
getTagOptions() {
|
||||
this.$axios.post("/platform/h5/inclusive/tagOptions", {}).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.$set(this, "tagOptions", res.data || [])
|
||||
} else {
|
||||
this.$toast(res.msg || "标签加载失败")
|
||||
}
|
||||
}).catch(() => {})
|
||||
},
|
||||
// 参数为页码、每页数量及标签编码;data.list 是当前页,totalCount 是过滤后的总数。
|
||||
pageData() {
|
||||
if (this.requesting) return
|
||||
this.$set(this, "requesting", true)
|
||||
this.$set(this, "loading", true)
|
||||
this.$set(this, "loadError", false)
|
||||
this.$set(this, "loadSequence", this.loadSequence + 1)
|
||||
const sequence = this.loadSequence
|
||||
const params = Object.assign({}, this.pageForm, {tagType: this.activeTag})
|
||||
this.$axios.post("/platform/h5/inclusive/pageData", params).then((res) => {
|
||||
// 切换标签或刷新后,旧请求的响应不再追加到新列表。
|
||||
if (sequence !== this.loadSequence) return
|
||||
if (res.code === 0) {
|
||||
const data = res.data || {}
|
||||
const list = data.list || []
|
||||
this.$set(this, "tableData", this.tableData.concat(list))
|
||||
this.$set(this.pageForm, "totalCount", data.totalCount || 0)
|
||||
this.$set(this, "finished", list.length === 0 || this.tableData.length >= this.pageForm.totalCount)
|
||||
if (!this.finished) this.$set(this.pageForm, "pageNumber", params.pageNumber + 1)
|
||||
} else {
|
||||
this.$set(this, "loadError", true)
|
||||
this.$toast(res.msg || "查询失败")
|
||||
}
|
||||
}, () => {
|
||||
if (sequence === this.loadSequence) this.$set(this, "loadError", true)
|
||||
}).finally(() => {
|
||||
if (sequence === this.loadSequence) {
|
||||
this.$set(this, "loading", false)
|
||||
this.$set(this, "refreshing", false)
|
||||
this.$set(this, "requesting", false)
|
||||
}
|
||||
})
|
||||
},
|
||||
// 重置分页并使在途响应失效,防止不同标签的数据混在同一个列表中。
|
||||
resetList() {
|
||||
this.$set(this, "loadSequence", this.loadSequence + 1)
|
||||
this.$set(this, "requesting", false)
|
||||
this.$set(this, "tableData", [])
|
||||
this.$set(this, "finished", false)
|
||||
this.$set(this, "loadError", false)
|
||||
this.$set(this.pageForm, "pageNumber", 1)
|
||||
this.$set(this.pageForm, "totalCount", 0)
|
||||
},
|
||||
tagChange(value) {
|
||||
this.$set(this, "activeTag", value)
|
||||
this.resetList()
|
||||
this.pageData()
|
||||
},
|
||||
onRefresh() {
|
||||
this.resetList()
|
||||
this.$set(this, "refreshing", true)
|
||||
this.pageData()
|
||||
},
|
||||
// item 为点击的列表记录,id 为内容主键;打开弹框时只增加当前地址的历史记录。
|
||||
openDetail(item) {
|
||||
if (!item || !item.id || this.detailVisible) return
|
||||
this.$set(this, "detail", {})
|
||||
this.$set(this, "detailVisible", true)
|
||||
window.h5PopupHistory.open(this.popupHistoryToken, "inclusiveDetail")
|
||||
this.findOne(item.id)
|
||||
},
|
||||
// id 传内容主键;接口返回 Result,data 为已发布详情对象,含正文、封面、标签和活动链接。
|
||||
findOne(id) {
|
||||
this.$set(this, "detailSequence", this.detailSequence + 1)
|
||||
const sequence = this.detailSequence
|
||||
this.$set(this, "formLoading", true)
|
||||
this.$axios.post("/platform/h5/inclusive/findOne", {id: id}).then((res) => {
|
||||
// 关闭或重新打开详情后,旧请求不得覆盖当前弹框内容。
|
||||
if (sequence !== this.detailSequence) return
|
||||
this.$set(this, "detail", res.code === 0 && res.data ? res.data : {})
|
||||
if (res.code !== 0 || !res.data) this.$toast(res.msg || "未查询到活动详情")
|
||||
}).catch(() => {
|
||||
// 网络及权限错误由公共拦截器提示;仅清空当前请求对应的详情。
|
||||
if (sequence === this.detailSequence) this.$set(this, "detail", {})
|
||||
}).finally(() => {
|
||||
if (sequence === this.detailSequence) this.$set(this, "formLoading", false)
|
||||
})
|
||||
},
|
||||
// 返回按钮交给全局历史栈处理;防止连续点击触发多次 history.back。
|
||||
closeDetail() {
|
||||
if (!this.detailVisible || this.detailClosing) return
|
||||
this.$set(this, "detailClosing", true)
|
||||
if (!window.h5PopupHistory.close(this.popupHistoryToken, "inclusiveDetail")) {
|
||||
this.closeDetailState()
|
||||
}
|
||||
},
|
||||
// 历史回退回调只同步弹框状态,使在途请求失效,不再额外回退历史。
|
||||
closeDetailState() {
|
||||
this.$set(this, "detailVisible", false)
|
||||
this.$set(this, "detailClosing", false)
|
||||
this.$set(this, "detailSequence", this.detailSequence + 1)
|
||||
this.$set(this, "formLoading", false)
|
||||
},
|
||||
// 先保存目标地址,再清理当前页面弹框历史,避免外链返回后残留空弹框历史。
|
||||
openLink() {
|
||||
if (!this.hasLink || this.detailClosing) return
|
||||
const url = this.detail.linkUrl.trim()
|
||||
this.$set(this, "detailClosing", true)
|
||||
window.h5PopupHistory.clear(this.popupHistoryToken).then(() => {
|
||||
const navigate = () => { window.location.href = url }
|
||||
if (window.h5HistoryLayerManager) {
|
||||
window.h5HistoryLayerManager.closeAll(navigate)
|
||||
} else {
|
||||
navigate()
|
||||
}
|
||||
})
|
||||
},
|
||||
// PJAX 直接替换 DOM 不会自动销毁 Vue,主动触发生命周期以注销页面回调。
|
||||
destroyPage() {
|
||||
this.$destroy()
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.$set(this, "popupHistoryToken", window.h5PopupHistory.register((popupKey) => {
|
||||
if (popupKey === "inclusiveDetail") this.closeDetailState()
|
||||
}))
|
||||
this.getTagOptions()
|
||||
this.pageData()
|
||||
},
|
||||
mounted() {
|
||||
$(document).on("pjax:beforeReplace.inclusiveDetail", this.destroyPage)
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.$set(this, "loadSequence", this.loadSequence + 1)
|
||||
this.$set(this, "detailSequence", this.detailSequence + 1)
|
||||
window.h5PopupHistory.unregister(this.popupHistoryToken)
|
||||
$(document).off("pjax:beforeReplace.inclusiveDetail", this.destroyPage)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<!--#
|
||||
}
|
||||
#-->
|
||||
@@ -3,105 +3,316 @@ layout("/layouts/platform_h5.html"){
|
||||
#-->
|
||||
|
||||
<style>
|
||||
#app.my-study-page { min-height: 100%; height: auto; box-sizing: border-box; padding-bottom: 24px; padding-bottom: calc(24px + env(safe-area-inset-bottom, 0px)); background: #f7fbff linear-gradient(180deg, #cfe8ff 0, #e5f3ff 160px, #f1f8ff 320px, #f7fbff 480px) no-repeat; color: #26364c; }
|
||||
.my-study-hero { padding: 24px 16px 18px; background: transparent; }
|
||||
.my-study-head { display: flex; align-items: center; gap: 8px; }
|
||||
.my-study-back { display: inline-flex; align-items: center; justify-content: center; flex: 0 0 36px; width: 36px; height: 36px; padding: 0; border: 0; border-radius: 50%; background: #fff; color: #1989fa; font-size: 20px; }
|
||||
.my-study-heading { margin: 0 0 0 auto; text-align: right; font-size: 22px; line-height: 1.5; font-weight: 700; }
|
||||
.my-study-subtitle { margin: 4px 0 18px; text-align: right; color: #8192a9; font-size: 12px; }
|
||||
.my-study-overview { padding: 18px 16px 14px; border-radius: 16px; background: #fff; box-shadow: 0 6px 20px rgba(45, 111, 188, .04); }
|
||||
.my-study-time-heading { position: relative; min-height: 80px; padding-right: 100px; }
|
||||
.my-study-book { position: absolute; top: 0; right: 0; width: 92px; height: 80px; background-color: #c9e5ff; -webkit-mask: url("${base!}/assets/platform/img/svg/ant-design_read-filled.svg") center / contain no-repeat; mask: url("${base!}/assets/platform/img/svg/ant-design_read-filled.svg") center / contain no-repeat; transform: rotate(-15deg); pointer-events: none; }
|
||||
.my-study-time-heading .my-study-time { flex-wrap: wrap; }
|
||||
.my-study-time-label { display: flex; align-items: center; gap: 6px; color: #7a8ca6; font-size: 12px; }
|
||||
.my-study-time-label .van-icon { color: #1989fa; font-size: 16px; }
|
||||
.my-study-time { display: flex; align-items: baseline; gap: 6px; margin-top: 8px; color: #8192a9; font-size: 12px; }
|
||||
.my-study-time strong { color: #1989fa; font-size: 30px; font-weight: 700; line-height: 1.3; font-variant-numeric: tabular-nums; }
|
||||
.my-study-time strong:not(:first-child) { margin-left: 8px; }
|
||||
.my-study-stats { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); padding-top: 16px; margin-top: 16px; border-top: 1px solid #edf2f8; text-align: center; }
|
||||
.my-study-stat + .my-study-stat { border-left: 1px solid #edf2f8; }
|
||||
.my-study-stat-value { font-size: 20px; line-height: 1.3; font-weight: 700; font-variant-numeric: tabular-nums; }
|
||||
.my-study-stat-label { margin-top: 4px; color: #8192a9; font-size: 11px; }
|
||||
.my-study-main { padding: 0 16px; }
|
||||
.my-study-list-header { position: sticky; top: 0; z-index: 10; margin: 0 -16px; padding: 12px 16px 14px; background: #f7fbff; }
|
||||
.my-study-list-header .my-study-tabs { margin-bottom: 0; }
|
||||
.my-study-section-head { display: flex; align-items: baseline; justify-content: space-between; margin: 2px 0 12px; }
|
||||
.my-study-section-title { margin: 0; font-size: 17px; font-weight: 700; }
|
||||
.my-study-section-count { color: #8b9ab0; font-size: 12px; }
|
||||
.my-study-tabs { display: flex; gap: 8px; margin-bottom: 14px; }
|
||||
.my-study-tab { flex: 1; padding: 8px 4px; border: 0; border-radius: 9px; background: #eaf1fa; color: #7a8ca6; font-size: 12px; line-height: 1.5; }
|
||||
.my-study-tab.active { background: #1989fa; color: #fff; font-weight: 600; }
|
||||
.my-study-list-card { margin-bottom: 12px; padding: 12px; border-radius: 14px; background: #fff; }
|
||||
.my-study-list-card:last-child { margin-bottom: 0; }
|
||||
.my-study-course-top { display: flex; align-items: flex-start; gap: 12px; width: 100%; padding: 0; border: 0; background: transparent; text-align: left; color: inherit; }
|
||||
.my-study-list-cover { position: relative; flex: 0 0 112px; width: 112px; height: 84px; aspect-ratio: 4 / 3; border-radius: 8px; overflow: hidden; background: #edf5ff; }
|
||||
.my-study-list-cover img { display: block; width: 100%; height: 100%; object-fit: cover; }
|
||||
.my-study-cover-empty { display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; color: #98bde9; font-size: 30px; }
|
||||
.my-study-list-info { min-width: 0; flex: 1; }
|
||||
.my-study-list-title { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; overflow-wrap: anywhere; color: #26364c; font-size: 14px; font-weight: 600; line-height: 1.5; }
|
||||
.my-study-status { display: inline-flex; align-items: center; gap: 3px; margin-top: 6px; padding: 2px 6px; border-radius: 5px; color: #1989fa; background: #edf6ff; font-size: 10px; }
|
||||
.my-study-status.completed { color: #28b583; background: #eafaf3; }
|
||||
.my-study-progress { display: flex; align-items: center; gap: 10px; margin-top: 14px; }
|
||||
.my-study-progress .van-progress { flex: 1; }
|
||||
.my-study-progress-value { flex-shrink: 0; color: #1989fa; font-size: 12px; font-weight: 600; }
|
||||
.my-study-course-foot { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-top: 10px; }
|
||||
.my-study-list-meta { min-width: 0; color: #8b9ab0; font-size: 11px; line-height: 1.7; }
|
||||
.my-study-list-meta span { display: block; }
|
||||
.my-study-continue { display: inline-flex; align-items: center; gap: 4px; flex-shrink: 0; padding: 7px 12px; border: 0; border-radius: 16px; background: #edf6ff; color: #1989fa; font-size: 12px; line-height: 1.5; }
|
||||
.my-study-loading { padding: 40px 0; text-align: center; }
|
||||
.my-study-page button:focus-visible { outline: 2px solid #1989fa; outline-offset: 2px; }
|
||||
@media (max-width: 359px) { .my-study-main { padding: 0 12px; } .my-study-list-header { margin-right: -12px; margin-left: -12px; padding-right: 12px; padding-left: 12px; } .my-study-list-cover { flex-basis: 96px; width: 96px; height: 72px; } .my-study-course-top { gap: 9px; } }
|
||||
.my-study-page {
|
||||
min-height: 100vh;
|
||||
background: #f3f7fb;
|
||||
color: #202733;
|
||||
}
|
||||
.my-study-hero {
|
||||
position: relative;
|
||||
padding: 86px 20px 72px;
|
||||
color: #ffffff;
|
||||
background: linear-gradient(135deg, #0ca7dd 0%, #0699d6 58%, #0f8fd7 100%);
|
||||
overflow: hidden;
|
||||
}
|
||||
.my-study-hero::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
right: -74px;
|
||||
bottom: -108px;
|
||||
width: 260px;
|
||||
height: 260px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, .08);
|
||||
}
|
||||
.my-study-back {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 8px;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
border-radius: 50%;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: rgba(15, 23, 42, .22);
|
||||
color: #ffffff;
|
||||
font: inherit;
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.my-study-status {
|
||||
display: block;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
.my-study-time {
|
||||
font-size: 15px;
|
||||
color: rgba(255, 255, 255, .82);
|
||||
}
|
||||
.my-study-time strong {
|
||||
margin: 0 3px;
|
||||
color: #ffffff;
|
||||
font-size: 26px;
|
||||
font-weight: 800;
|
||||
}
|
||||
.my-study-stats {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 0;
|
||||
margin-top: 26px;
|
||||
text-align: center;
|
||||
}
|
||||
.my-study-stat-value {
|
||||
font-size: 23px;
|
||||
font-weight: 800;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.my-study-stat-label {
|
||||
margin-top: 7px;
|
||||
color: rgba(255, 255, 255, .78);
|
||||
font-size: 13px;
|
||||
line-height: 1.25;
|
||||
}
|
||||
.my-study-main {
|
||||
position: relative;
|
||||
margin-top: -36px;
|
||||
padding: 0 20px 28px;
|
||||
z-index: 2;
|
||||
}
|
||||
.my-study-assistant {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
min-height: 70px;
|
||||
padding: 13px 14px;
|
||||
border-radius: 8px;
|
||||
background: #eef6ff;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0 8px 18px rgba(17, 101, 166, .08);
|
||||
}
|
||||
.my-study-ai {
|
||||
flex-shrink: 0;
|
||||
font-size: 27px;
|
||||
font-weight: 900;
|
||||
font-style: italic;
|
||||
color: #1a75ff;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
.my-study-ai span {
|
||||
color: #6b46ff;
|
||||
}
|
||||
.my-study-assistant-text {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
color: #30445c;
|
||||
font-size: 15px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
.my-study-assistant-btn {
|
||||
flex-shrink: 0;
|
||||
padding: 7px 16px;
|
||||
border-radius: 18px;
|
||||
color: #5d8fd8;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
background: #ffffff;
|
||||
}
|
||||
.my-study-section {
|
||||
margin-top: 20px;
|
||||
padding: 16px 0;
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
}
|
||||
.my-study-section-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 14px 14px;
|
||||
}
|
||||
.my-study-section-title {
|
||||
font-size: 20px;
|
||||
font-weight: 800;
|
||||
}
|
||||
.my-study-all {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
color: #9aa3b1;
|
||||
font-size: 13px;
|
||||
}
|
||||
.my-study-course-scroll {
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
padding: 0 14px;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
.my-study-course-scroll::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
.my-study-course-card {
|
||||
flex: 0 0 154px;
|
||||
width: 154px;
|
||||
overflow: hidden;
|
||||
border-radius: 5px;
|
||||
background: #f7fbff;
|
||||
}
|
||||
.my-study-cover {
|
||||
width: 154px;
|
||||
height: 88px;
|
||||
background: linear-gradient(135deg, #8657f2, #56c4ec);
|
||||
}
|
||||
.my-study-cover img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
.my-study-cover-empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 10px;
|
||||
color: #ffffff;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.my-study-course-name {
|
||||
height: 48px;
|
||||
padding: 8px 10px 0;
|
||||
color: #293241;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
line-height: 1.35;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
.my-study-course-foot {
|
||||
padding: 8px 10px 10px;
|
||||
color: #b5bdc8;
|
||||
font-size: 13px;
|
||||
text-align: right;
|
||||
}
|
||||
.my-study-list {
|
||||
margin-top: 18px;
|
||||
}
|
||||
.my-study-list-card {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
}
|
||||
.my-study-list-cover {
|
||||
flex: 0 0 108px;
|
||||
width: 108px;
|
||||
height: 66px;
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(135deg, #8657f2, #56c4ec);
|
||||
}
|
||||
.my-study-list-cover img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
.my-study-list-info {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
.my-study-list-title {
|
||||
color: #202733;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
line-height: 1.35;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
.my-study-list-meta {
|
||||
margin-top: 10px;
|
||||
color: #8f99a8;
|
||||
font-size: 13px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="app" class="my-study-page" v-cloak>
|
||||
<div class="my-study-hero">
|
||||
<div class="my-study-head">
|
||||
<button type="button" class="my-study-back" aria-label="返回" @click="goBack"><van-icon name="arrow-left"></van-icon></button>
|
||||
<h1 class="my-study-heading">我的学习</h1>
|
||||
</div>
|
||||
<p class="my-study-subtitle">记录学习点滴,见证每一次进步</p>
|
||||
<div class="my-study-overview">
|
||||
<div class="my-study-time-heading">
|
||||
<div class="my-study-time-label"><van-icon name="clock-o"></van-icon>累计学习时长</div>
|
||||
<div class="my-study-time"><strong>{{ summary.studyHour || 0 }}</strong>小时<strong>{{ summary.studyMinute || 0 }}</strong>分钟</div>
|
||||
<span class="my-study-book" aria-hidden="true"></span>
|
||||
<button type="button" class="my-study-back" @click="historyBack()" aria-label="返回上一页">
|
||||
<van-icon name="arrow-left"></van-icon>
|
||||
</button>
|
||||
<div class="my-study-status">
|
||||
<div class="my-study-time">
|
||||
累计学习时长 <strong>{{ summary.studyHour || 0 }}</strong> 小时 <strong>{{ summary.studyMinute || 0 }}</strong> 分钟
|
||||
</div>
|
||||
<div class="my-study-stats">
|
||||
<div v-for="item in statItems" :key="item.label" class="my-study-stat">
|
||||
<div class="my-study-stat-value">{{ item.value }}</div>
|
||||
<div class="my-study-stat-label">{{ item.label }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="my-study-stats">
|
||||
<div v-for="item in statItems" :key="item.label">
|
||||
<div class="my-study-stat-value">{{ item.value }}</div>
|
||||
<div class="my-study-stat-label">{{ item.label }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="my-study-main">
|
||||
<div class="my-study-list-header">
|
||||
<div class="my-study-section-head"><h2 class="my-study-section-title">我学习的课程</h2><span class="my-study-section-count">共 {{ courses.length }} 门</span></div>
|
||||
<nav class="my-study-tabs" aria-label="学习状态">
|
||||
<button v-for="tab in statusTabs" :key="tab.value" type="button" class="my-study-tab" :class="{ active: activeStatus === tab.value }" :aria-pressed="activeStatus === tab.value" @click="activeStatus = tab.value">{{ tab.label }}</button>
|
||||
</nav>
|
||||
<div class="my-study-assistant">
|
||||
<div class="my-study-ai"><span>AI</span>学助手</div>
|
||||
<div class="my-study-assistant-text">来问问AI学习助手,获取专属学习计划</div>
|
||||
<div class="my-study-assistant-btn">查看</div>
|
||||
</div>
|
||||
<div v-if="loading" class="my-study-loading"><van-loading size="22px" color="#1989fa">加载中…</van-loading></div>
|
||||
<van-empty v-else-if="loadError" description="学习记录加载失败"><van-button round size="small" type="info" @click="loadData">重新加载</van-button></van-empty>
|
||||
<div v-else class="my-study-list">
|
||||
<article v-for="course in visibleCourses" :key="course.courseId" class="my-study-list-card">
|
||||
<button type="button" class="my-study-course-top" @click="openCourse(course)">
|
||||
<div class="my-study-list-cover">
|
||||
|
||||
<div class="my-study-section">
|
||||
<div class="my-study-section-head">
|
||||
<div class="my-study-section-title">我学习的课程</div>
|
||||
<div class="my-study-all" @click="scrollToList">全部 <van-icon name="arrow"></van-icon></div>
|
||||
</div>
|
||||
<div class="my-study-course-scroll">
|
||||
<div
|
||||
v-for="course in courses"
|
||||
:key="course.courseId"
|
||||
class="my-study-course-card"
|
||||
@click="openCourse(course)">
|
||||
<div class="my-study-cover">
|
||||
<img v-if="getCoverUrl(course.cover)" :src="getCoverUrl(course.cover)" :alt="course.courseName">
|
||||
<div v-else class="my-study-cover-empty"><van-icon name="description"></van-icon></div>
|
||||
<div v-else class="my-study-cover-empty">{{ course.courseName || '课程' }}</div>
|
||||
</div>
|
||||
<div class="my-study-list-info">
|
||||
<div class="my-study-list-title">{{ course.courseName || '未命名课程' }}</div>
|
||||
<span class="my-study-status" :class="{ completed: isCompleted(course) }"><van-icon :name="isCompleted(course) ? 'checked' : 'clock-o'"></van-icon>{{ isCompleted(course) ? '已完成' : '学习中' }}</span>
|
||||
</div>
|
||||
</button>
|
||||
<div v-if="Number(course.outlineCount || 0) > 0" class="my-study-progress"><van-progress :percentage="courseProgress(course)" :show-pivot="false" stroke-width="5" :color="isCompleted(course) ? '#2fc78c' : '#1989fa'"></van-progress><span class="my-study-progress-value">{{ courseProgress(course) }}%</span></div>
|
||||
<div class="my-study-course-foot">
|
||||
<div class="my-study-list-meta"><span>已完成 {{ course.completedOutlineCount || 0 }} 个章节 · 已学 {{ course.studyTimeText || '0秒' }}</span><span v-if="studyDate(course.latestStudyTime)">最近学习 {{ studyDate(course.latestStudyTime) }}</span></div>
|
||||
<button type="button" class="my-study-continue" @click="openCourse(course)"><van-icon name="play"></van-icon>{{ isCompleted(course) ? '再次学习' : '继续学习' }}</button>
|
||||
<div class="my-study-course-name">{{ course.courseName || '未命名课程' }}</div>
|
||||
<div class="my-study-course-foot">已学完{{ course.completedOutlineCount || 0 }}讲</div>
|
||||
</div>
|
||||
</article>
|
||||
<van-empty v-if="!visibleCourses.length" :description="activeStatus === 'completed' ? '暂无已完成课程' : activeStatus === 'studying' ? '暂无学习中的课程' : '暂无学习课程'"></van-empty>
|
||||
</div>
|
||||
<van-empty v-if="!loading && !courses.length" description="暂无学习课程"></van-empty>
|
||||
</div>
|
||||
|
||||
<div ref="courseList" class="my-study-list">
|
||||
<div
|
||||
v-for="course in courses"
|
||||
:key="'list_' + course.courseId"
|
||||
class="my-study-list-card"
|
||||
@click="openCourse(course)">
|
||||
<div class="my-study-list-cover">
|
||||
<img v-if="getCoverUrl(course.cover)" :src="getCoverUrl(course.cover)" :alt="course.courseName">
|
||||
<div v-else class="my-study-cover-empty">{{ course.courseName || '课程' }}</div>
|
||||
</div>
|
||||
<div class="my-study-list-info">
|
||||
<div class="my-study-list-title">{{ course.courseName || '未命名课程' }}</div>
|
||||
<div class="my-study-list-meta">学习进度 {{ course.progressPercent || 0 }}% · {{ course.studyTimeText || '0秒' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -114,75 +325,35 @@ layout("/layouts/platform_h5.html"){
|
||||
apiBase: "/platform/learning/study/record",
|
||||
summary: {},
|
||||
courses: [],
|
||||
loading: false,
|
||||
loadError: false,
|
||||
activeStatus: "all",
|
||||
statusTabs: [
|
||||
{ value: "all", label: "全部" },
|
||||
{ value: "studying", label: "学习中" },
|
||||
{ value: "completed", label: "已完成" }
|
||||
]
|
||||
loading: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
completedCount() {
|
||||
return this.courses.filter(course => this.isCompleted(course)).length
|
||||
},
|
||||
statItems() {
|
||||
return [
|
||||
{ label: "已学课程", value: this.courses.length },
|
||||
{ label: "学习中", value: this.courses.length - this.completedCount },
|
||||
{ label: "已完成", value: this.completedCount }
|
||||
{ label: "已学完课程", value: this.summary.completedCourseCount || 0 },
|
||||
{ label: "学习专题", value: 0 }
|
||||
]
|
||||
},
|
||||
visibleCourses() {
|
||||
if (this.activeStatus === "completed") return this.courses.filter(course => this.isCompleted(course))
|
||||
if (this.activeStatus === "studying") return this.courses.filter(course => !this.isCompleted(course))
|
||||
return this.courses
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
goBack() {
|
||||
if (window.history.length > 1) {
|
||||
window.history.back()
|
||||
return
|
||||
}
|
||||
pjaxReplace("/platform/learning/course/h5")
|
||||
},
|
||||
async loadData() {
|
||||
this.loading = true
|
||||
this.loadError = false
|
||||
try {
|
||||
const [summaryResp, courseResp] = await Promise.all([
|
||||
this.$axios.post(this.apiBase + "/h5Summary"),
|
||||
this.$axios.post(this.apiBase + "/h5Courses")
|
||||
])
|
||||
if (summaryResp.code !== 0 || courseResp.code !== 0) {
|
||||
this.loadError = true
|
||||
return
|
||||
if (summaryResp.code === 0) {
|
||||
this.summary = summaryResp.data || {}
|
||||
}
|
||||
if (courseResp.code === 0) {
|
||||
this.courses = courseResp.data || []
|
||||
}
|
||||
this.summary = summaryResp.data || {}
|
||||
this.courses = courseResp.data || []
|
||||
} catch (e) {
|
||||
this.loadError = true
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
courseProgress(course) {
|
||||
const value = Number(course.progressPercent || 0)
|
||||
return Number.isFinite(value) ? Math.max(0, Math.min(100, Math.round(value))) : 0
|
||||
},
|
||||
isCompleted(course) {
|
||||
return this.courseProgress(course) === 100 && Number(course.outlineCount || 0) > 0
|
||||
&& Number(course.completedOutlineCount || 0) >= Number(course.outlineCount)
|
||||
},
|
||||
studyDate(value) {
|
||||
if (!value) return ""
|
||||
const date = new Date(/^\d+$/.test(String(value)) ? Number(value) : String(value).replace(/-/g, "/"))
|
||||
if (Number.isNaN(date.getTime())) return ""
|
||||
return date.getFullYear() + "-" + String(date.getMonth() + 1).padStart(2, "0") + "-" + String(date.getDate()).padStart(2, "0")
|
||||
},
|
||||
getCoverUrl(cover) {
|
||||
if (!cover) return ""
|
||||
if (Array.isArray(cover)) {
|
||||
@@ -205,7 +376,10 @@ layout("/layouts/platform_h5.html"){
|
||||
},
|
||||
openCourse(course) {
|
||||
if (!course || !course.courseId) return
|
||||
pjaxReplace("/platform/learning/course/h5/study?id=" + encodeURIComponent(course.courseId))
|
||||
pjaxReplace("/platform/learning/course/h5/study?id=" + course.courseId)
|
||||
},
|
||||
scrollToList() {
|
||||
this.$refs.courseList && this.$refs.courseList.scrollIntoView({ behavior: "smooth", block: "start" })
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
|
||||
@@ -54,6 +54,54 @@ layout("/layouts/platform_h5.html"){
|
||||
.member-voluntary__paragraph + .member-voluntary__paragraph {
|
||||
margin-top: 6px;
|
||||
}
|
||||
.member-h5-page.has-fixed-actions {
|
||||
padding-bottom: calc(120px + env(safe-area-inset-bottom));
|
||||
}
|
||||
.member-form-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
.member-action-buttons,
|
||||
.member-notice-actions {
|
||||
display: flex;
|
||||
gap: 9px;
|
||||
}
|
||||
.member-notice-actions .van-button {
|
||||
height: 42px;
|
||||
border-radius: 14px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.member-notice-consent {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 5px;
|
||||
color: #596779;
|
||||
font-size: 12px;
|
||||
line-height: 22px;
|
||||
}
|
||||
.member-notice-link {
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: #1989fa;
|
||||
font-size: inherit;
|
||||
}
|
||||
.member-notice-popup {
|
||||
box-sizing: border-box;
|
||||
max-height: 80vh;
|
||||
padding: 20px 16px calc(16px + env(safe-area-inset-bottom));
|
||||
}
|
||||
.member-notice-title {
|
||||
margin: 0 0 12px;
|
||||
text-align: center;
|
||||
font-size: 18px;
|
||||
color: #263548;
|
||||
}
|
||||
.member-notice-content {
|
||||
max-height: 50vh;
|
||||
overflow-y: auto;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
</style>
|
||||
<div id="app" v-cloak>
|
||||
<div class="member-h5-page" :class="{'has-fixed-actions': showActionButtons}">
|
||||
@@ -341,29 +389,36 @@ layout("/layouts/platform_h5.html"){
|
||||
</van-cell-group>
|
||||
</section>
|
||||
|
||||
<section class="form-section">
|
||||
<h2 class="member-form-section__title">入会意愿</h2>
|
||||
<van-cell-group :border="false">
|
||||
<van-checkbox class="member-voluntary" icon-size="22px"
|
||||
v-model="formData.isVoluntary" shape="square">
|
||||
<div class="member-voluntary__paragraph">
|
||||
<span :style="'color:' + (formData.isVoluntary ? '#1989fa' : '#F56C6C') ">
|
||||
我自愿申请加入学校工会,并委托学校按规定代为扣缴工会会员会费。
|
||||
</span>
|
||||
</div>
|
||||
<div class="member-voluntary__paragraph">
|
||||
<span :style="'color:' + (formData.isVoluntary ? '#1989fa' : '#F56C6C') ">
|
||||
我将严格遵守工会章程,认真执行工会决议,积极参与工会活动,主动融入“学校健康幸福家”建设,为营造温暖、和谐、奋进的校园氛围贡献力量,以实际行动助力学校各项事业发展。
|
||||
</span>
|
||||
</div>
|
||||
</van-checkbox>
|
||||
</van-cell-group>
|
||||
</section>
|
||||
<div v-if="showActionButtons" class="member-form-actions">
|
||||
<van-button block plain class="member-save-button" :loading="formLoading && loadingAction === 'save'" :disabled="formLoading" @click.submit="onSave">保存草稿</van-button>
|
||||
<van-button v-if="!taskId" block type="primary" class="member-submit-button" :loading="formLoading && loadingAction === 'submit'" :disabled="formLoading" @click.submit="onSubmit">提交申请</van-button>
|
||||
<van-button v-else block type="primary" class="member-submit-button" :loading="formLoading && loadingAction === 'submitAgain'" :disabled="formLoading" @click.submit="onFinishTask">重新提交</van-button>
|
||||
<div class="member-notice-consent">
|
||||
<van-checkbox :value="hasReadNotice" :disabled="formLoading" shape="square" icon-size="18px"
|
||||
aria-label="同意入会意愿" @input="onNoticeCheckChange"></van-checkbox>
|
||||
<span>我已阅读并同意</span>
|
||||
<button type="button" class="member-notice-link" :disabled="formLoading" @click="openMembershipNotice">《入会意愿》</button>
|
||||
</div>
|
||||
<div class="member-action-buttons">
|
||||
<van-button block plain native-type="button" class="member-save-button" :loading="formLoading && loadingAction === 'save'" :disabled="formLoading" @click="onSave">保存草稿</van-button>
|
||||
<van-button v-if="!taskId" block type="primary" native-type="button" class="member-submit-button" :loading="formLoading && loadingAction === 'submit'" :disabled="formLoading || !hasReadNotice" @click="onSubmit">提交申请</van-button>
|
||||
<van-button v-else block type="primary" native-type="button" class="member-submit-button" :loading="formLoading && loadingAction === 'submitAgain'" :disabled="formLoading || !hasReadNotice" @click="onFinishTask">重新提交</van-button>
|
||||
</div>
|
||||
</div>
|
||||
<van-popup :value="showMembershipNotice" position="bottom" round class="member-notice-popup"
|
||||
:close-on-click-overlay="false" :close-on-popstate="false">
|
||||
<h2 class="member-notice-title">入会意愿</h2>
|
||||
<div class="member-voluntary member-notice-content">
|
||||
<div class="member-voluntary__paragraph">
|
||||
我自愿申请加入学校工会,并委托学校按规定代为扣缴工会会员会费。
|
||||
</div>
|
||||
<div class="member-voluntary__paragraph">
|
||||
我将严格遵守工会章程,认真执行工会决议,积极参与工会活动,主动融入“学校健康幸福家”建设,为营造温暖、和谐、奋进的校园氛围贡献力量,以实际行动助力学校各项事业发展。
|
||||
</div>
|
||||
</div>
|
||||
<div class="member-notice-actions">
|
||||
<van-button block native-type="button" @click="cancelMembershipNotice">取消</van-button>
|
||||
<van-button block native-type="button" type="primary" :disabled="noticeCountdown > 0"
|
||||
@click="confirmMembershipNotice">{{noticeCountdown > 0 ? '我已知晓(' + noticeCountdown + '秒)' : '我已知晓'}}</van-button>
|
||||
</div>
|
||||
</van-popup>
|
||||
</van-form>
|
||||
</main>
|
||||
</div>
|
||||
@@ -377,12 +432,18 @@ layout("/layouts/platform_h5.html"){
|
||||
return {
|
||||
id: GetQueryString("bizId"),
|
||||
taskId: GetQueryString("taskId"),
|
||||
// 当前登录人已是会员时,不显示保存和提交按钮。
|
||||
showActionButtons: true,
|
||||
// 已入会或申请处理中仍显示操作栏,保存和提交限制由申请状态单独控制。
|
||||
showActionButtons: false,
|
||||
applicationBlockMessage: "",
|
||||
pageLoading: true,
|
||||
formLoading: false,
|
||||
loadingAction: "",
|
||||
historyLayerPageKey: "member-apply-submit",
|
||||
showMembershipNotice: false,
|
||||
noticeAcknowledged: false,
|
||||
noticeCloseRequested: false,
|
||||
noticeCountdown: 5,
|
||||
noticeTimer: null,
|
||||
formData: {
|
||||
families: []
|
||||
},
|
||||
@@ -404,7 +465,51 @@ layout("/layouts/platform_h5.html"){
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
openMembershipNotice() {
|
||||
if (this.formLoading || this.showMembershipNotice) return
|
||||
window.h5HistoryLayerManager.open("membership-notice")
|
||||
},
|
||||
onNoticeCheckChange(checked) {
|
||||
if (checked) {
|
||||
this.openMembershipNotice()
|
||||
} else {
|
||||
this.noticeAcknowledged = false
|
||||
this.$set(this.formData, "isVoluntary", false)
|
||||
}
|
||||
},
|
||||
// 每次打开都重新计时,只有本次阅读确认后才能勾选,旧草稿不能跳过确认。
|
||||
startNoticeCountdown() {
|
||||
this.clearNoticeTimer()
|
||||
this.noticeAcknowledged = false
|
||||
this.$set(this.formData, "isVoluntary", false)
|
||||
this.noticeCountdown = 5
|
||||
this.noticeTimer = setInterval(() => {
|
||||
this.noticeCountdown = Math.max(0, this.noticeCountdown - 1)
|
||||
if (this.noticeCountdown === 0) this.clearNoticeTimer()
|
||||
}, 1000)
|
||||
},
|
||||
clearNoticeTimer() {
|
||||
if (this.noticeTimer !== null) {
|
||||
clearInterval(this.noticeTimer)
|
||||
this.noticeTimer = null
|
||||
}
|
||||
},
|
||||
confirmMembershipNotice() {
|
||||
if (!this.showMembershipNotice || this.noticeCountdown > 0) return
|
||||
this.noticeAcknowledged = true
|
||||
this.$set(this.formData, "isVoluntary", true)
|
||||
this.noticeCloseRequested = true
|
||||
window.h5HistoryLayerManager.close("membership-notice")
|
||||
},
|
||||
cancelMembershipNotice() {
|
||||
if (!this.showMembershipNotice) return
|
||||
this.noticeAcknowledged = false
|
||||
this.$set(this.formData, "isVoluntary", false)
|
||||
this.noticeCloseRequested = true
|
||||
window.h5HistoryLayerManager.close("membership-notice")
|
||||
},
|
||||
handlePageBack() {
|
||||
if (this.showMembershipNotice) return
|
||||
if (window.h5HistoryLayerManager.stack.length) {
|
||||
window.h5HistoryLayerManager.close()
|
||||
return
|
||||
@@ -433,7 +538,7 @@ layout("/layouts/platform_h5.html"){
|
||||
},
|
||||
|
||||
registerHistoryLayers() {
|
||||
// 六个选择器共用页面级历史栈,手机返回键只关闭当前最上层选择器。
|
||||
// 入会意愿仅允许通过底部按钮关闭;其他选择器沿用原返回栈行为。
|
||||
window.h5HistoryLayerManager.register(this.historyLayerPageKey, (stack) => {
|
||||
this.$set(this, "showNationPicker", stack.includes("nation-picker"))
|
||||
this.$set(this, "showDatePicker", stack.includes("birthday-picker"))
|
||||
@@ -441,6 +546,21 @@ layout("/layouts/platform_h5.html"){
|
||||
this.$set(this, "showEducationPicker", stack.includes("education-picker"))
|
||||
this.$set(this, "showAcademicDegreePicker", stack.includes("degree-picker"))
|
||||
this.$set(this, "showMarriagePicker", stack.includes("marriage-picker"))
|
||||
const noticeVisible = stack.includes("membership-notice")
|
||||
// 系统返回时恢复当前弹层历史,保留阅读状态及倒计时,须由底部按钮关闭。
|
||||
if (!noticeVisible && this.showMembershipNotice && !this.noticeCloseRequested) {
|
||||
window.h5HistoryLayerManager.open("membership-notice")
|
||||
return
|
||||
}
|
||||
if (noticeVisible && !this.showMembershipNotice) {
|
||||
this.noticeCloseRequested = false
|
||||
this.startNoticeCountdown()
|
||||
} else if (!noticeVisible && this.showMembershipNotice) {
|
||||
this.clearNoticeTimer()
|
||||
this.noticeCloseRequested = false
|
||||
if (!this.noticeAcknowledged) this.$set(this.formData, "isVoluntary", false)
|
||||
}
|
||||
this.$set(this, "showMembershipNotice", noticeVisible)
|
||||
})
|
||||
},
|
||||
|
||||
@@ -479,7 +599,11 @@ layout("/layouts/platform_h5.html"){
|
||||
},
|
||||
|
||||
onSave() {
|
||||
if (this.formLoading) return
|
||||
if (this.formLoading || !this.showActionButtons) return
|
||||
if (this.applicationBlockMessage) {
|
||||
this.$dialog.alert({ title: "提示", message: this.applicationBlockMessage })
|
||||
return
|
||||
}
|
||||
this.$dialog.confirm({
|
||||
title: "提示",
|
||||
message: "您确定要保存吗?",
|
||||
@@ -504,7 +628,15 @@ layout("/layouts/platform_h5.html"){
|
||||
},
|
||||
|
||||
onSubmit() {
|
||||
if (this.formLoading) return
|
||||
if (this.formLoading || !this.showActionButtons) return
|
||||
if (this.applicationBlockMessage) {
|
||||
this.$dialog.alert({ title: "提示", message: this.applicationBlockMessage })
|
||||
return
|
||||
}
|
||||
if (!this.hasReadNotice) {
|
||||
this.$toast.fail("请阅读并勾选入会意愿")
|
||||
return
|
||||
}
|
||||
this.$refs.formRef.validate().then(() => {
|
||||
if (!this.formData.personalData) {
|
||||
this.$toast.fail("请填写个人学习及工作经历")
|
||||
@@ -527,8 +659,7 @@ layout("/layouts/platform_h5.html"){
|
||||
this.$toast.success(res.msg)
|
||||
this.$pjaxReplace("/platform/member/apply/mine/h5")
|
||||
} else {
|
||||
this.$toast.fail("操作失败")
|
||||
console.log(res.msg)
|
||||
this.$dialog.alert({ title: "提示", message: res.msg || "操作失败" })
|
||||
}
|
||||
}).finally(() => {
|
||||
this.formLoading = false
|
||||
@@ -539,7 +670,15 @@ layout("/layouts/platform_h5.html"){
|
||||
},
|
||||
|
||||
onFinishTask() {
|
||||
if (this.formLoading) return
|
||||
if (this.formLoading || !this.showActionButtons) return
|
||||
if (this.applicationBlockMessage) {
|
||||
this.$dialog.alert({ title: "提示", message: this.applicationBlockMessage })
|
||||
return
|
||||
}
|
||||
if (!this.hasReadNotice) {
|
||||
this.$toast.fail("请阅读并勾选入会意愿")
|
||||
return
|
||||
}
|
||||
this.$refs.formRef.validate().then(() => {
|
||||
if (!this.formData.personalData) {
|
||||
this.$toast.fail("请填写个人学习及工作经历")
|
||||
@@ -563,8 +702,7 @@ layout("/layouts/platform_h5.html"){
|
||||
this.$toast.success(res.msg)
|
||||
this.$pjaxReplace("/platform/member/apply/mine/h5")
|
||||
} else {
|
||||
this.$toast.fail("操作失败")
|
||||
console.log(res.msg)
|
||||
this.$dialog.alert({ title: "提示", message: res.msg || "操作失败" })
|
||||
}
|
||||
}).finally(() => {
|
||||
this.formLoading = false
|
||||
@@ -580,12 +718,21 @@ layout("/layouts/platform_h5.html"){
|
||||
},
|
||||
|
||||
init() {
|
||||
// 无参数获取当前登录人资料;有 id 时改查已有申请,接口均返回对应的用户/申请对象。
|
||||
return this.$axios.post("/platform/member/apply/submit/getSelfUserInfo", {}, {h5SkeletonLoading: true})
|
||||
// 使用服务端实时状态,已有申请携带任务信息以保留合法重新提交。
|
||||
return this.$axios.post("/platform/member/apply/submit/getSelfUserInfo", {
|
||||
id: this.id || null,
|
||||
taskId: this.taskId || null
|
||||
}, {h5SkeletonLoading: true})
|
||||
.then((resp) => {
|
||||
const user = resp.code === 0 && resp.data ? resp.data : this.$store.state.user
|
||||
// 页面按钮是否展示,以当前登录人的会员状态为准。
|
||||
this.showActionButtons = !(user && user.member)
|
||||
if (resp.code !== 0 || !resp.data) {
|
||||
return this.$dialog.alert({ title: "提示", message: resp.msg || "无法获取申请状态,请稍后重试" })
|
||||
}
|
||||
const user = resp.data
|
||||
this.applicationBlockMessage = user.applicationBlockMessage || ""
|
||||
this.showActionButtons = true
|
||||
if (user.applicationBlockMessage) {
|
||||
this.$dialog.alert({ title: "提示", message: user.applicationBlockMessage })
|
||||
}
|
||||
|
||||
if (this.id) {
|
||||
return this.$axios.post("/platform/member/apply/submit/findApplyById", { id: this.id }, {h5SkeletonLoading: true})
|
||||
@@ -666,6 +813,10 @@ layout("/layouts/platform_h5.html"){
|
||||
this.$set(this.formData, "campusName", "")
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
this.showActionButtons = false
|
||||
this.$dialog.alert({ title: "提示", message: "无法获取申请状态,请稍后重试" })
|
||||
})
|
||||
},
|
||||
toChinesNum(num) {
|
||||
let changeNum = ["零", "一", "二", "三", "四", "五", "六", "七", "八", "九"]
|
||||
@@ -708,6 +859,9 @@ layout("/layouts/platform_h5.html"){
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
hasReadNotice() {
|
||||
return this.noticeAcknowledged && this.formData.isVoluntary === true
|
||||
},
|
||||
|
||||
nationColumns() {
|
||||
if (this.dict && this.dict.type && this.dict.type.USER_NATION) {
|
||||
@@ -749,6 +903,7 @@ layout("/layouts/platform_h5.html"){
|
||||
})
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.clearNoticeTimer()
|
||||
window.h5HistoryLayerManager.unregister(this.historyLayerPageKey)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -37,7 +37,7 @@ const home = {
|
||||
<div class="quick-entry-item"
|
||||
v-for="item in quickEntries"
|
||||
@click="menuClick(item)"
|
||||
:key="item.id">
|
||||
:key="item.id">
|
||||
<div class="quick-entry-icon">
|
||||
<img v-if="getQuickEntryIcon(item)" :src="getQuickEntryIcon(item)" alt="">
|
||||
<van-icon v-else name="apps-o" size="36"></van-icon>
|
||||
@@ -51,12 +51,12 @@ const home = {
|
||||
<!--娲诲姩鍒楄〃-->
|
||||
<div class="feature-panel">
|
||||
<div
|
||||
class="feature-card"
|
||||
:class="[item.className, { 'feature-card--primary': index === 0 }]"
|
||||
:style="{ backgroundImage: 'url(' + item.asset + ')' }"
|
||||
v-for="(item, index) in featuredEntries"
|
||||
:key="item.name"
|
||||
@click="featureClick(item)">
|
||||
class="feature-card"
|
||||
:class="[item.className, { 'feature-card--primary': index === 0 }]"
|
||||
:style="{ backgroundImage: 'url(' + item.asset + ')' }"
|
||||
v-for="(item, index) in featuredEntries"
|
||||
:key="item.name"
|
||||
@click="featureClick(item)">
|
||||
<div class="feature-card__text">
|
||||
<div class="feature-card__title">{{ item.name }}</div>
|
||||
<div class="feature-card__desc">{{ item.desc }}</div>
|
||||
@@ -70,12 +70,25 @@ 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 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"
|
||||
@@ -95,15 +108,19 @@ const home = {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="union-news-list" v-else-if="spreadDynamics.length">
|
||||
<div class="union-news-list" ref="spreadViewport" 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 +134,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>
|
||||
@@ -210,8 +228,8 @@ const home = {
|
||||
<van-tag class="act-item-wrapper-tag" v-if="$moment().unix() > $moment(item.endDate).unix()">
|
||||
已结束
|
||||
</van-tag>
|
||||
|
||||
<van-tag class="act-item-wrapper-tag" type="success"
|
||||
|
||||
<van-tag class="act-item-wrapper-tag" type="success"
|
||||
v-else-if="$moment().unix() > $moment(item.startDate).unix() && $moment().unix() < $moment(item.endDate).unix()">
|
||||
进行中
|
||||
</van-tag>
|
||||
@@ -219,7 +237,7 @@ const home = {
|
||||
<van-tag class="act-item-wrapper-tag" type="success" v-else-if="$moment().unix() < $moment(item.startDate).unix()">
|
||||
即将开始
|
||||
</van-tag>
|
||||
|
||||
|
||||
<div class="act-item-cover">
|
||||
<van-image v-if="item.cover" width="120" height="84" :src="item.cover"></van-image>
|
||||
</div>
|
||||
@@ -247,6 +265,10 @@ const home = {
|
||||
classroomRecommendOptions: [],
|
||||
showHomeSummary: false,
|
||||
homeScrollContainer: null,
|
||||
spreadSections: [],
|
||||
spreadMenuVisible: false,
|
||||
spreadHistoryToken: null,
|
||||
spreadActiveIndex: 0,
|
||||
spreadDynamics: [],
|
||||
spreadLoading: true,
|
||||
spreadLoadSeq: 0,
|
||||
@@ -282,7 +304,7 @@ const home = {
|
||||
action: "查看信息",
|
||||
asset: "/assets/mobile/img/home/v4/feature-inclusive-v8.png",
|
||||
className: "feature-card--inclusive",
|
||||
href: ""
|
||||
href: "/platform/h5/inclusive"
|
||||
}
|
||||
],
|
||||
appLogo: "",
|
||||
@@ -311,6 +333,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 +542,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 +572,48 @@ 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] : [])
|
||||
@@ -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,12 +1523,51 @@ 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;
|
||||
@@ -1429,42 +1578,42 @@ const home = {
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.union-news-list::-webkit-scrollbar {
|
||||
.home-page/deep/ .union-news-list::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.union-news-item--skeleton {
|
||||
height: 96px;
|
||||
min-height: 96px;
|
||||
.home-page/deep/ .union-news-item--skeleton {
|
||||
height: 100px;
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
.union-news-skeleton__cover {
|
||||
width: 80px;
|
||||
height: 48px;
|
||||
margin-right: 10px;
|
||||
.home-page/deep/ .union-news-skeleton__cover {
|
||||
width: 104px;
|
||||
height: 78px;
|
||||
margin-right: 12px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 4px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.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 {
|
||||
.home-page/deep/ .union-news-item {
|
||||
width: calc(100% - 32px);
|
||||
min-width: calc(100% - 32px);
|
||||
min-height: 96px;
|
||||
min-height: 100px;
|
||||
padding: 10px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
@@ -1475,36 +1624,45 @@ const home = {
|
||||
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 {
|
||||
width: 80px;
|
||||
height: 48px;
|
||||
margin-right: 10px;
|
||||
.home-page/deep/ .union-news-item__cover {
|
||||
width: 104px;
|
||||
height: 78px;
|
||||
margin-right: 12px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 4px;
|
||||
border-radius: 6px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.union-news-item__content {
|
||||
.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-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 +1675,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 +1690,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 +1700,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 +1710,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 +1731,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 +1739,7 @@ const home = {
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.union-news-empty__text {
|
||||
.home-page/deep/ .union-news-empty__text {
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
|
||||