Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
876a0a6a6e | ||
|
|
aae28a6b4b | ||
|
|
17fc9c7c83 | ||
|
|
22a0e8f383 | ||
|
|
f58a3b2caf | ||
|
|
fb5525bc86 |
@@ -24,3 +24,6 @@ replay_pid*
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
Desktop.ini
|
||||
|
||||
src/main/resources/application.yaml
|
||||
src/main/resources/application-dev.yaml
|
||||
|
||||
@@ -0,0 +1,277 @@
|
||||
-- v3 教职工社团数据迁移至 v4。
|
||||
-- 仅迁移可明确映射的数据;不伪造审核人、审核意见或没有来源的业务字段。
|
||||
-- 执行要求:指定 zhgh_swufe 为默认库,且 mysql 客户端禁止使用 --force。
|
||||
|
||||
USE zhgh_swufe;
|
||||
|
||||
SET @migration_time = UNIX_TIMESTAMP() * 1000;
|
||||
SET @backup_suffix = '20260813_v3_club';
|
||||
SET @create_backup = (
|
||||
SELECT CASE WHEN COUNT(*) = 0 THEN 1 ELSE 0 END
|
||||
FROM information_schema.TABLES
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'bak_sys_club_20260813_v3_club'
|
||||
);
|
||||
|
||||
-- 本批次专属备份:保留清理前的业务表、社团角色和对应流程全链路。
|
||||
CREATE TABLE IF NOT EXISTS bak_sys_club_20260813_v3_club AS SELECT * FROM sys_club WHERE 1 = 0;
|
||||
INSERT INTO bak_sys_club_20260813_v3_club SELECT * FROM sys_club WHERE @create_backup = 1;
|
||||
CREATE TABLE IF NOT EXISTS bak_club_user_20260813_v3_club AS SELECT * FROM club_user WHERE 1 = 0;
|
||||
INSERT INTO bak_club_user_20260813_v3_club SELECT * FROM club_user WHERE @create_backup = 1;
|
||||
CREATE TABLE IF NOT EXISTS bak_club_user_apply_20260813_v3_club AS SELECT * FROM club_user_apply WHERE 1 = 0;
|
||||
INSERT INTO bak_club_user_apply_20260813_v3_club SELECT * FROM club_user_apply WHERE @create_backup = 1;
|
||||
CREATE TABLE IF NOT EXISTS bak_club_pay_record_20260813_v3_club AS SELECT * FROM club_pay_record WHERE 1 = 0;
|
||||
INSERT INTO bak_club_pay_record_20260813_v3_club SELECT * FROM club_pay_record WHERE @create_backup = 1;
|
||||
CREATE TABLE IF NOT EXISTS bak_sys_club_sponsor_20260813_v3_club AS SELECT * FROM sys_club_sponsor WHERE 1 = 0;
|
||||
INSERT INTO bak_sys_club_sponsor_20260813_v3_club SELECT * FROM sys_club_sponsor WHERE @create_backup = 1;
|
||||
CREATE TABLE IF NOT EXISTS bak_sys_club_evaluate_20260813_v3_club AS SELECT * FROM sys_club_evaluate WHERE 1 = 0;
|
||||
INSERT INTO bak_sys_club_evaluate_20260813_v3_club SELECT * FROM sys_club_evaluate WHERE @create_backup = 1;
|
||||
CREATE TABLE IF NOT EXISTS bak_sys_club_examine_register_20260813_v3_club AS SELECT * FROM sys_club_examine_register WHERE 1 = 0;
|
||||
INSERT INTO bak_sys_club_examine_register_20260813_v3_club SELECT * FROM sys_club_examine_register WHERE @create_backup = 1;
|
||||
CREATE TABLE IF NOT EXISTS bak_sys_club_examine_register_detailed_20260813_v3_club AS SELECT * FROM sys_club_examine_register_detailed WHERE 1 = 0;
|
||||
INSERT INTO bak_sys_club_examine_register_detailed_20260813_v3_club SELECT * FROM sys_club_examine_register_detailed WHERE @create_backup = 1;
|
||||
CREATE TABLE IF NOT EXISTS bak_sys_club_manager_20260813_v3_club AS SELECT * FROM sys_club_manager WHERE 1 = 0;
|
||||
INSERT INTO bak_sys_club_manager_20260813_v3_club SELECT * FROM sys_club_manager WHERE @create_backup = 1;
|
||||
CREATE TABLE IF NOT EXISTS bak_sys_club_refresh_20260813_v3_club AS SELECT * FROM sys_club_refresh WHERE 1 = 0;
|
||||
INSERT INTO bak_sys_club_refresh_20260813_v3_club SELECT * FROM sys_club_refresh WHERE @create_backup = 1;
|
||||
CREATE TABLE IF NOT EXISTS bak_sys_club_rule_20260813_v3_club AS SELECT * FROM sys_club_rule WHERE 1 = 0;
|
||||
INSERT INTO bak_sys_club_rule_20260813_v3_club SELECT * FROM sys_club_rule WHERE @create_backup = 1;
|
||||
CREATE TABLE IF NOT EXISTS bak_sys_club_user_20260813_v3_club AS SELECT * FROM sys_club_user WHERE 1 = 0;
|
||||
INSERT INTO bak_sys_club_user_20260813_v3_club SELECT * FROM sys_club_user WHERE @create_backup = 1;
|
||||
CREATE TABLE IF NOT EXISTS bak_sys_club_user_back_history_20260813_v3_club AS SELECT * FROM sys_club_user_back_history WHERE 1 = 0;
|
||||
INSERT INTO bak_sys_club_user_back_history_20260813_v3_club SELECT * FROM sys_club_user_back_history WHERE @create_backup = 1;
|
||||
CREATE TABLE IF NOT EXISTS bak_sys_club_files_20260813_v3_club AS SELECT * FROM sys_club_files WHERE 1 = 0;
|
||||
INSERT INTO bak_sys_club_files_20260813_v3_club SELECT * FROM sys_club_files WHERE @create_backup = 1;
|
||||
CREATE TABLE IF NOT EXISTS bak_sys_user_role_club_20260813_v3_club AS SELECT * FROM sys_user_role WHERE 1 = 0;
|
||||
INSERT INTO bak_sys_user_role_club_20260813_v3_club SELECT * FROM sys_user_role WHERE NULLIF(TRIM(clubId), '') IS NOT NULL AND @create_backup = 1;
|
||||
CREATE TABLE IF NOT EXISTS bak_wf_process_instance_club_20260813_v3_club AS SELECT i.* FROM wf_process_instance i WHERE 1 = 0;
|
||||
INSERT INTO bak_wf_process_instance_club_20260813_v3_club
|
||||
SELECT i.* FROM wf_process_instance i JOIN wf_process_define d ON d.id = i.processDefineId
|
||||
WHERE d.name IN ('XHZC','XHRH','XHTH','XHPY','XHNS','XHBGLS','XHHJBG','XHZCXD') AND @create_backup = 1;
|
||||
CREATE TABLE IF NOT EXISTS bak_wf_process_task_club_20260813_v3_club AS SELECT * FROM wf_process_task WHERE 1 = 0;
|
||||
INSERT INTO bak_wf_process_task_club_20260813_v3_club
|
||||
SELECT t.* FROM wf_process_task t JOIN bak_wf_process_instance_club_20260813_v3_club i ON i.id = t.processInstanceId WHERE @create_backup = 1;
|
||||
CREATE TABLE IF NOT EXISTS bak_wf_process_task_actor_club_20260813_v3_club AS SELECT * FROM wf_process_task_actor WHERE 1 = 0;
|
||||
INSERT INTO bak_wf_process_task_actor_club_20260813_v3_club
|
||||
SELECT a.* FROM wf_process_task_actor a JOIN bak_wf_process_task_club_20260813_v3_club t ON t.id = a.processTaskId WHERE @create_backup = 1;
|
||||
CREATE TABLE IF NOT EXISTS bak_wf_process_cc_instance_club_20260813_v3_club AS SELECT * FROM wf_process_cc_instance WHERE 1 = 0;
|
||||
INSERT INTO bak_wf_process_cc_instance_club_20260813_v3_club
|
||||
SELECT c.* FROM wf_process_cc_instance c JOIN bak_wf_process_instance_club_20260813_v3_club i ON i.id = c.processInstanceId WHERE @create_backup = 1;
|
||||
CREATE TABLE IF NOT EXISTS bak_outlay_manage_club_refs_20260813_v3_club AS
|
||||
SELECT o.* FROM outlay_manage_club o JOIN sys_club c ON c.id = o.clubId WHERE @create_backup = 1;
|
||||
CREATE TABLE IF NOT EXISTS bak_article_club_refs_20260813_v3_club AS
|
||||
SELECT a.* FROM article a JOIN sys_club c ON c.id = a.clubId WHERE @create_backup = 1;
|
||||
|
||||
DROP TEMPORARY TABLE IF EXISTS tmp_club_guard;
|
||||
CREATE TEMPORARY TABLE tmp_club_guard (name VARCHAR(64), actual BIGINT, expected BIGINT, bad INT AS (actual <> expected) STORED) ENGINE=InnoDB;
|
||||
INSERT INTO tmp_club_guard(name, actual, expected) SELECT 'source_club_count', COUNT(*), 12 FROM union_swufe.sys_club;
|
||||
INSERT INTO tmp_club_guard(name, actual, expected) SELECT 'source_valid_member_count', COUNT(*), 14 FROM union_swufe.sys_club_user WHERE clubid IS NOT NULL AND status = 5;
|
||||
INSERT INTO tmp_club_guard(name, actual, expected) SELECT 'source_sponsor_count', COUNT(*), 2 FROM union_swufe.sys_club_sponsor;
|
||||
INSERT INTO tmp_club_guard(name, actual, expected)
|
||||
SELECT 'source_users_missing_v4', COUNT(*), 0
|
||||
FROM union_swufe.sys_club_user s LEFT JOIN sys_user u
|
||||
ON CONVERT(u.id USING utf8mb4) = CONVERT(s.userid USING utf8mb4) COLLATE utf8mb4_0900_ai_ci
|
||||
WHERE s.clubid IS NOT NULL AND s.status = 5 AND u.id IS NULL;
|
||||
INSERT INTO tmp_club_guard(name, actual, expected)
|
||||
SELECT 'source_club_contact_missing_v4', COUNT(*), 0
|
||||
FROM union_swufe.sys_club s LEFT JOIN sys_user u
|
||||
ON CONVERT(u.id USING utf8mb4) = CONVERT(s.concatPerson USING utf8mb4) COLLATE utf8mb4_0900_ai_ci
|
||||
WHERE NULLIF(TRIM(s.concatPerson), '') IS NOT NULL AND u.id IS NULL;
|
||||
|
||||
DROP TEMPORARY TABLE IF EXISTS tmp_club_guard_fail;
|
||||
CREATE TEMPORARY TABLE tmp_club_guard_fail(id INT NOT NULL) ENGINE=InnoDB;
|
||||
INSERT INTO tmp_club_guard_fail SELECT NULL FROM tmp_club_guard WHERE bad = 1 LIMIT 1;
|
||||
|
||||
START TRANSACTION;
|
||||
|
||||
-- 先删除流程子表,再删除社团业务表,避免残留无主任务和角色关系。
|
||||
DELETE a FROM wf_process_task_actor a
|
||||
JOIN wf_process_task t ON t.id = a.processTaskId
|
||||
JOIN wf_process_instance i ON i.id = t.processInstanceId
|
||||
JOIN wf_process_define d ON d.id = i.processDefineId
|
||||
WHERE d.name IN ('XHZC','XHRH','XHTH','XHPY','XHNS','XHBGLS','XHHJBG','XHZCXD');
|
||||
DELETE c FROM wf_process_cc_instance c
|
||||
JOIN wf_process_instance i ON i.id = c.processInstanceId
|
||||
JOIN wf_process_define d ON d.id = i.processDefineId
|
||||
WHERE d.name IN ('XHZC','XHRH','XHTH','XHPY','XHNS','XHBGLS','XHHJBG','XHZCXD');
|
||||
DELETE t FROM wf_process_task t
|
||||
JOIN wf_process_instance i ON i.id = t.processInstanceId
|
||||
JOIN wf_process_define d ON d.id = i.processDefineId
|
||||
WHERE d.name IN ('XHZC','XHRH','XHTH','XHPY','XHNS','XHBGLS','XHHJBG','XHZCXD');
|
||||
DELETE i FROM wf_process_instance i
|
||||
JOIN wf_process_define d ON d.id = i.processDefineId
|
||||
WHERE d.name IN ('XHZC','XHRH','XHTH','XHPY','XHNS','XHBGLS','XHHJBG','XHZCXD');
|
||||
|
||||
DELETE FROM sys_user_role WHERE NULLIF(TRIM(clubId), '') IS NOT NULL;
|
||||
DELETE FROM club_pay_record;
|
||||
DELETE FROM club_user_apply;
|
||||
DELETE FROM club_user;
|
||||
DELETE FROM sys_club_evaluate;
|
||||
DELETE FROM sys_club_examine_register_detailed;
|
||||
DELETE FROM sys_club_examine_register;
|
||||
DELETE FROM sys_club_manager;
|
||||
DELETE FROM sys_club_refresh;
|
||||
DELETE FROM sys_club_rule;
|
||||
DELETE FROM sys_club_sponsor;
|
||||
DELETE FROM sys_club_user_back_history;
|
||||
DELETE FROM sys_club_user;
|
||||
DELETE FROM sys_club_files;
|
||||
DELETE FROM sys_club;
|
||||
|
||||
-- v3 社团主数据:字段不对应的值保持为空,不做兜底拼装。
|
||||
INSERT INTO sys_club(
|
||||
id, clubName, clubCode, clubType, concatPerson, concatPersonMobile, concatPersonEmail,
|
||||
introduce, purpose, createTime, foundTime, dismiss, due, files, establishReport,
|
||||
rulesFile, manageFile, yearPlanFile, userId, QQGroupCode, wechatGroupCode,
|
||||
replaceReport, createdBy, createdAt, updatedBy, updatedAt, delFlag,
|
||||
afterRulesFile, schoolReplyFile, state, ruleState, reportState
|
||||
)
|
||||
SELECT
|
||||
s.id, s.name, s.code,
|
||||
CASE s.type WHEN 'tyjj' THEN '体育竞技' WHEN 'whys' THEN '文化艺术' ELSE NULL END,
|
||||
s.concatPerson, s.concatPersonMobile, s.concatPersonEmail,
|
||||
s.stjs, s.stzc, s.create_time, s.foundTime, COALESCE(s.isjs, 0), s.dues_standard,
|
||||
NULL, NULL, NULL, NULL, NULL, s.userid,
|
||||
CAST(s.QQGroupCode AS CHAR), CAST(s.wechatGroupCode AS CHAR), NULL,
|
||||
s.userid,
|
||||
CASE WHEN STR_TO_DATE(s.create_time, '%Y-%m-%d %H:%i:%s') IS NULL THEN NULL ELSE UNIX_TIMESTAMP(STR_TO_DATE(s.create_time, '%Y-%m-%d %H:%i:%s')) * 1000 END,
|
||||
s.userid,
|
||||
CASE WHEN STR_TO_DATE(s.create_time, '%Y-%m-%d %H:%i:%s') IS NULL THEN NULL ELSE UNIX_TIMESTAMP(STR_TO_DATE(s.create_time, '%Y-%m-%d %H:%i:%s')) * 1000 END,
|
||||
0, NULL, NULL, CAST(s.state AS CHAR), s.ruleState, NULL
|
||||
FROM union_swufe.sys_club s;
|
||||
|
||||
INSERT INTO sys_club_sponsor(id, clubId, sponsorId, createdBy, createdAt, updatedBy, updatedAt, delFlag)
|
||||
SELECT id, clubId, sponsorId, sponsorId, @migration_time, sponsorId, @migration_time, 0
|
||||
FROM union_swufe.sys_club_sponsor;
|
||||
|
||||
-- 正式成员仅迁移 clubId 明确且 status=5 的14条;2条无社团ID和1条待审核记录不强迁。
|
||||
INSERT INTO club_user(id, clubId, userId, roleCode, clubPosition, email, avatar,
|
||||
sameTimeJoinOtherClubSituation, awardsExperience,
|
||||
createdBy, createdAt, updatedBy, updatedAt, delFlag)
|
||||
SELECT s.id, s.clubid, s.userid,
|
||||
JSON_ARRAY(CASE s.sf
|
||||
WHEN 1 THEN 'CLUB_PRESIDENT'
|
||||
WHEN 2 THEN 'CLUB_MEMBER'
|
||||
WHEN 3 THEN 'CLUB_VICE_PRESIDENT'
|
||||
WHEN 4 THEN 'CLUB_SECRETARY'
|
||||
WHEN 5 THEN 'CLUB_VICE_SECRETARY'
|
||||
END),
|
||||
s.position, NULL, NULL, NULL, NULL,
|
||||
s.userid,
|
||||
CASE WHEN STR_TO_DATE(s.join_time, '%Y-%m-%d %H:%i:%s') IS NULL THEN @migration_time ELSE UNIX_TIMESTAMP(STR_TO_DATE(s.join_time, '%Y-%m-%d %H:%i:%s')) * 1000 END,
|
||||
s.userid, @migration_time, 0
|
||||
FROM union_swufe.sys_club_user s
|
||||
WHERE s.clubid IS NOT NULL AND s.status = 5 AND s.sf IN (1,2,3,4,5);
|
||||
|
||||
-- 保留旧兼容表可明确对应的字段,供现有统计和旧查询使用。
|
||||
INSERT INTO sys_club_user(
|
||||
id, clubId, userId, joinTime, roleCode, payed, giveMoney, status, exitStatus,
|
||||
state, changeRoleCode, roleCodeChangeTime, auditList, exitAuditList, isNormal,
|
||||
changeTime, position, mode, email, avatar, sameTimeJoinOtherClubSituation,
|
||||
awardsExperience, createdBy, createdAt, updatedBy, updatedAt, delFlag
|
||||
)
|
||||
SELECT s.id, s.clubid, s.userid, s.join_time,
|
||||
CASE s.sf WHEN 1 THEN 'CLUB_PRESIDENT' WHEN 2 THEN 'CLUB_MEMBER'
|
||||
WHEN 3 THEN 'CLUB_VICE_PRESIDENT' WHEN 4 THEN 'CLUB_SECRETARY'
|
||||
WHEN 5 THEN 'CLUB_VICE_SECRETARY' END,
|
||||
s.isjf, s.giveMoney, s.status, NULL, s.state,
|
||||
CASE s.changeSf WHEN 1 THEN 'CLUB_PRESIDENT' WHEN 2 THEN 'CLUB_MEMBER'
|
||||
WHEN 3 THEN 'CLUB_VICE_PRESIDENT' WHEN 4 THEN 'CLUB_SECRETARY'
|
||||
WHEN 5 THEN 'CLUB_VICE_SECRETARY' END,
|
||||
s.sfChangeTime, s.auditList, NULL, s.isNormal, s.changeTime, s.position,
|
||||
TRUE, NULL, NULL, NULL, NULL, s.userid,
|
||||
CASE WHEN STR_TO_DATE(s.join_time, '%Y-%m-%d %H:%i:%s') IS NULL THEN @migration_time ELSE UNIX_TIMESTAMP(STR_TO_DATE(s.join_time, '%Y-%m-%d %H:%i:%s')) * 1000 END,
|
||||
s.userid, @migration_time, 0
|
||||
FROM union_swufe.sys_club_user s
|
||||
WHERE s.clubid IS NOT NULL AND s.status = 5 AND s.sf IN (1,2,3,4,5);
|
||||
|
||||
-- 社团角色必须按 v4 角色编码重新取 roleId,禁止复制 v3 角色ID。
|
||||
INSERT INTO sys_user_role(roleId, userId, clubId)
|
||||
SELECT r.id, s.userid, s.clubid
|
||||
FROM union_swufe.sys_club_user s
|
||||
JOIN sys_role r ON r.code = CASE s.sf
|
||||
WHEN 1 THEN 'CLUB_PRESIDENT' WHEN 2 THEN 'CLUB_MEMBER'
|
||||
WHEN 3 THEN 'CLUB_VICE_PRESIDENT' WHEN 4 THEN 'CLUB_SECRETARY'
|
||||
WHEN 5 THEN 'CLUB_VICE_SECRETARY' END
|
||||
WHERE s.clubid IS NOT NULL AND s.status = 5 AND s.sf IN (1,2,3,4,5);
|
||||
|
||||
-- 旧系统没有 v4 流程表。12个社团均为930且审核ID为空,只创建已完成注册实例,
|
||||
-- 用 legacyMigration/directPassNote 明确标记迁移事实,不生成虚假的审核任务、审核人和意见。
|
||||
SET @xhzc_define_id = (SELECT id FROM wf_process_define WHERE name = 'XHZC' AND state = 1 AND delFlag = 0 ORDER BY version DESC, id DESC LIMIT 1);
|
||||
INSERT INTO wf_process_instance(
|
||||
parentId, processDefineId, state, parentNodeName, businessNo, operator, expireTime,
|
||||
variable, createdBy, createdAt, updatedBy, updatedAt, delFlag
|
||||
)
|
||||
SELECT NULL, @xhzc_define_id, 20, NULL, s.id, s.userid, NULL,
|
||||
JSON_OBJECT(
|
||||
'f_data', JSON_OBJECT('id', s.id, 'clubName', s.name, 'clubCode', s.code,
|
||||
'clubType', CASE s.type WHEN 'tyjj' THEN '体育竞技' WHEN 'whys' THEN '文化艺术' ELSE NULL END,
|
||||
'concatPerson', s.concatPerson, 'foundTime', s.foundTime, 'createTime', s.create_time),
|
||||
'initiator', s.userid,
|
||||
'initiatorName', COALESCE(u.username, ''),
|
||||
'initiatorAccount', COALESCE(u.loginname, ''),
|
||||
'initiatorUnitId', COALESCE(u.unitId, ''),
|
||||
'initiatorUnitName', COALESCE(unit.name, ''),
|
||||
'submitType', 0,
|
||||
'legacyMigration', TRUE,
|
||||
'legacySource', 'union_swufe',
|
||||
'legacyState', s.state,
|
||||
'legacyProcess', '社团注册',
|
||||
'directPass', TRUE,
|
||||
'directPassNote', 'v3社团状态为930且审核ID为空,仅迁移为已完成实例,未伪造审核任务和审核意见'
|
||||
),
|
||||
s.userid,
|
||||
CASE WHEN STR_TO_DATE(s.create_time, '%Y-%m-%d %H:%i:%s') IS NULL THEN @migration_time ELSE UNIX_TIMESTAMP(STR_TO_DATE(s.create_time, '%Y-%m-%d %H:%i:%s')) * 1000 END,
|
||||
s.userid, @migration_time, 0
|
||||
FROM union_swufe.sys_club s
|
||||
LEFT JOIN sys_user u ON CONVERT(u.id USING utf8mb4) = CONVERT(s.userid USING utf8mb4) COLLATE utf8mb4_0900_ai_ci
|
||||
LEFT JOIN sys_unit unit ON unit.id = u.unitId
|
||||
WHERE s.state = 930;
|
||||
|
||||
-- 原 v4 社团ID被经费/文章引用。仅同名可明确匹配的3个社团转换到v3 ID;
|
||||
-- 其余无同名目标的数据保留业务记录,但将clubId置空,避免形成错误社团关联。
|
||||
UPDATE outlay_manage_club o
|
||||
JOIN bak_sys_club_20260813_v3_club oldc ON oldc.id = o.clubId
|
||||
LEFT JOIN union_swufe.sys_club newc
|
||||
ON CONVERT(newc.name USING utf8mb4) = CONVERT(oldc.clubName USING utf8mb4) COLLATE utf8mb4_0900_ai_ci
|
||||
SET o.clubId = newc.id;
|
||||
UPDATE article a
|
||||
JOIN bak_sys_club_20260813_v3_club oldc ON oldc.id = a.clubId
|
||||
LEFT JOIN union_swufe.sys_club newc
|
||||
ON CONVERT(newc.name USING utf8mb4) = CONVERT(oldc.clubName USING utf8mb4) COLLATE utf8mb4_0900_ai_ci
|
||||
SET a.clubId = newc.id;
|
||||
-- 原库中少量clubId本来就无法关联旧社团主表;迁移后统一置空,禁止误连到新社团。
|
||||
UPDATE outlay_manage_club o
|
||||
LEFT JOIN sys_club c ON c.id = o.clubId
|
||||
SET o.clubId = NULL
|
||||
WHERE o.clubId IS NOT NULL AND c.id IS NULL;
|
||||
UPDATE article a
|
||||
LEFT JOIN sys_club c ON c.id = a.clubId
|
||||
SET a.clubId = NULL
|
||||
WHERE a.clubId IS NOT NULL AND c.id IS NULL;
|
||||
|
||||
DROP TEMPORARY TABLE IF EXISTS tmp_club_final_guard;
|
||||
CREATE TEMPORARY TABLE tmp_club_final_guard(name VARCHAR(64), actual BIGINT, expected BIGINT, bad INT AS (actual <> expected) STORED) ENGINE=InnoDB;
|
||||
INSERT INTO tmp_club_final_guard(name, actual, expected) SELECT 'target_club_count', COUNT(*), 12 FROM sys_club;
|
||||
INSERT INTO tmp_club_final_guard(name, actual, expected) SELECT 'target_member_count', COUNT(*), 14 FROM club_user;
|
||||
INSERT INTO tmp_club_final_guard(name, actual, expected) SELECT 'target_legacy_member_count', COUNT(*), 14 FROM sys_club_user;
|
||||
INSERT INTO tmp_club_final_guard(name, actual, expected) SELECT 'target_sponsor_count', COUNT(*), 2 FROM sys_club_sponsor;
|
||||
INSERT INTO tmp_club_final_guard(name, actual, expected) SELECT 'target_club_role_count', COUNT(*), 14 FROM sys_user_role WHERE NULLIF(TRIM(clubId), '') IS NOT NULL;
|
||||
INSERT INTO tmp_club_final_guard(name, actual, expected)
|
||||
SELECT 'target_register_instance_count', COUNT(*), 12
|
||||
FROM wf_process_instance i JOIN wf_process_define d ON d.id = i.processDefineId
|
||||
WHERE d.name = 'XHZC' AND i.state = 20;
|
||||
INSERT INTO tmp_club_final_guard(name, actual, expected)
|
||||
SELECT 'orphan_member_club', COUNT(*), 0 FROM club_user u LEFT JOIN sys_club c ON c.id = u.clubId WHERE c.id IS NULL;
|
||||
INSERT INTO tmp_club_final_guard(name, actual, expected)
|
||||
SELECT 'orphan_role_club', COUNT(*), 0 FROM sys_user_role r LEFT JOIN sys_club c ON c.id = r.clubId WHERE NULLIF(TRIM(r.clubId), '') IS NOT NULL AND c.id IS NULL;
|
||||
INSERT INTO tmp_club_final_guard(name, actual, expected)
|
||||
SELECT 'duplicate_member', COUNT(*), 0 FROM (SELECT clubId,userId,COUNT(*) cnt FROM club_user GROUP BY clubId,userId HAVING COUNT(*) > 1) d;
|
||||
|
||||
DROP TEMPORARY TABLE IF EXISTS tmp_club_final_guard_fail;
|
||||
CREATE TEMPORARY TABLE tmp_club_final_guard_fail(id INT NOT NULL) ENGINE=InnoDB;
|
||||
INSERT INTO tmp_club_final_guard_fail SELECT NULL FROM tmp_club_final_guard WHERE bad = 1 LIMIT 1;
|
||||
|
||||
COMMIT;
|
||||
|
||||
SELECT * FROM tmp_club_guard ORDER BY name;
|
||||
SELECT * FROM tmp_club_final_guard ORDER BY name;
|
||||
@@ -85,6 +85,9 @@ public class MainLauncher {
|
||||
init_task();
|
||||
init_auth();
|
||||
ioc.get(Globals.class);
|
||||
// Globals 会清空并恢复系统任务,必须等它完成后再恢复华住未结束批次;新批次由按钮提交时创建调度。
|
||||
log.info("[华住同步][步骤=全局任务初始化完成] 开始检查待恢复批次");
|
||||
ioc.get(com.budwk.app.zhgh.huazhu.service.impl.HuaZhuEmployeeServiceImpl.class).restorePendingSyncTask();
|
||||
ioc.get(FlowEngine.class);
|
||||
ioc.get(GlobalMessageSendService.class);
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ package com.budwk.app.base.constant;
|
||||
public class RedisConstant {
|
||||
//项目统一前缀
|
||||
public final static String PLATFORM_REDIS_PREFIX = "budwk5mini:";
|
||||
/** 与原华住认证服务一致,缓存 HuaZhuAuthTokenResultDTO 的 JSON。 */
|
||||
public static final String REDIS_KEY_HUAZHU_AUTH_TOKEN = PLATFORM_REDIS_PREFIX + "hz:token:";
|
||||
public final static String PLATFORM_REDIS_WKCACHE_PREFIX = PLATFORM_REDIS_PREFIX + "wkcache:";
|
||||
|
||||
//聊天室
|
||||
|
||||
@@ -128,6 +128,17 @@ public class SysHomeController {
|
||||
public void index() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 兼容旧手机端首页地址,无需请求参数。
|
||||
* 未登录时沿用现有登录拦截;登录后重定向到标准 H5 首页,使底部导航按首页地址正常显示。
|
||||
* 方法返回 void,无响应数据字段,由重定向视图跳转到 /platform/h5/home。
|
||||
*/
|
||||
@At(value = "/mobile/index", top = true)
|
||||
@Ok(">>:/platform/h5/home")
|
||||
@SaCheckLogin
|
||||
public void mobileIndex() {
|
||||
}
|
||||
|
||||
@At(value = "/platform/h5", top = true)
|
||||
@Ok("re")
|
||||
@SaCheckLogin
|
||||
|
||||
@@ -84,7 +84,8 @@ public class SysDataUnitPullServiceImpl extends BaseServiceImpl<Sys_unit> implem
|
||||
}
|
||||
|
||||
for (Sys_unit unit : units) {
|
||||
unit.setParentId("0");
|
||||
// 当前系统学校根单位 ID 为 1,同步的二级单位统一挂在该节点下。
|
||||
unit.setParentId("1");
|
||||
unit.setUnitLevel(2);
|
||||
unit.setUnitTypeCode(1);
|
||||
if (unitIds.contains(unit.getId())) {
|
||||
@@ -92,7 +93,7 @@ public class SysDataUnitPullServiceImpl extends BaseServiceImpl<Sys_unit> implem
|
||||
} else {
|
||||
Map<String, Object> beanMap = BeanUtil.beanToMap(unit);
|
||||
beanMap.remove("child");
|
||||
sysUnitService.save(unit, "0");
|
||||
sysUnitService.save(unit, "1");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -69,7 +69,7 @@ public class SysUserServiceImpl extends BaseServiceImpl<Sys_user> implements Sys
|
||||
* sys_user_role 仅用于确认用户具有目标角色,组织范围统一取 sys_user.unitId,
|
||||
* 避免依赖未维护的 sys_user_role.unitId 字段导致流程无人可分派。
|
||||
*
|
||||
* @param unitId 单位 ID
|
||||
* @param unitId 单位 ID
|
||||
* @param roleCode 角色编码
|
||||
* @return 对应单位下有效且具有目标角色的用户 ID
|
||||
*/
|
||||
@@ -104,7 +104,7 @@ public class SysUserServiceImpl extends BaseServiceImpl<Sys_user> implements Sys
|
||||
* 角色用户通过 sys_user.unitId 关联 sys_unit,再以 sys_unit.unionId 确定
|
||||
* 二级党组织范围,不读取未维护的 sys_user_role.unionId 字段。
|
||||
*
|
||||
* @param unionId 分工会 ID
|
||||
* @param unionId 分工会 ID
|
||||
* @param roleCode 角色编码
|
||||
* @return 对应二级党组织下有效且具有目标角色的用户 ID
|
||||
*/
|
||||
@@ -381,9 +381,9 @@ public class SysUserServiceImpl extends BaseServiceImpl<Sys_user> implements Sys
|
||||
String decodePwd = Base64Decoder.decodeStr(passowrd);
|
||||
String hashedPassword = PwdUtil.getPassword(decodePwd, user.getSalt());
|
||||
if (!Strings.sNull(hashedPassword).equalsIgnoreCase(user.getPassword())) {
|
||||
// if (Globals.sso) {
|
||||
throw new BaseException("用户名或者密码不正确");
|
||||
// }
|
||||
if (Globals.sso) {
|
||||
throw new BaseException("用户名或者密码不正确");
|
||||
}
|
||||
}
|
||||
user = this.fetchLinks(user, "unit");
|
||||
if (Lang.isNotEmpty(user.getUnit()) && StrUtil.isNotBlank(user.getUnit().getUnionId())) {
|
||||
|
||||
@@ -21,11 +21,13 @@ import com.budwk.app.zhgh.activity.trainSignUp.service.TrainSignUpActivityStatis
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||
import org.nutz.dao.Cnd;
|
||||
import org.nutz.dao.Dao;
|
||||
import org.nutz.dao.Sqls;
|
||||
import org.nutz.dao.sql.Sql;
|
||||
import org.nutz.dao.util.cri.Static;
|
||||
import org.nutz.ioc.aop.Aop;
|
||||
import org.nutz.ioc.loader.annotation.Inject;
|
||||
import org.nutz.ioc.loader.annotation.IocBean;
|
||||
import org.nutz.json.Json;
|
||||
@@ -370,11 +372,27 @@ public class TrainSignUpApplyController {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 接收本次报名信息并校验手机号必填及11位格式,PC 与 H5 共用此入口。
|
||||
*
|
||||
* @param trainSignUpUser 报名参数:activityId 为活动ID,courseId 为课程ID,mobile 为必填的11位大陆手机号;
|
||||
* 限额课程通过 activityCourseId 指定时段,mobileColumnsValue 为动态字段及其填写值列表
|
||||
* @return Result,code 为结果状态码,msg 为手机号校验或报名处理的结果说明
|
||||
*/
|
||||
@At
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
@ApiOperation("活动报名")
|
||||
@SaCheckPermission(value = {"trainSignUp.apply", "h5.trainSignUp.apply"}, mode = SaMode.OR)
|
||||
@SLog(tag = "品牌活动-活动报名", msg = "活动报名")
|
||||
public Result doSignUp(TrainSignUpUser trainSignUpUser) {
|
||||
// 在进入报名处理前拦截缺失、空串及纯空白手机号,避免绕过页面必填校验。
|
||||
if (trainSignUpUser == null || StrUtil.isBlank(trainSignUpUser.getMobile())) {
|
||||
return Result.error("请填写手机号");
|
||||
}
|
||||
// 与 PC、H5 使用同一规则,仅接受1开头、第二位为3至9的11位数字。
|
||||
if (!trainSignUpUser.getMobile().matches("^1[3-9][0-9]{9}$")) {
|
||||
return Result.error("请输入正确的11位手机号");
|
||||
}
|
||||
try {
|
||||
lock.lock();
|
||||
boolean courseByUser = trainSignUpActivityService.isSignCourseByUser(trainSignUpUser.getCourseId(), SecurityUtil.getUserId());
|
||||
|
||||
@@ -22,7 +22,6 @@ import com.budwk.app.zhgh.activity.trainSignUp.service.TrainSignUpActivityStatis
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
|
||||
import org.apache.poi.ss.usermodel.Workbook;
|
||||
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
||||
import org.nutz.dao.Chain;
|
||||
@@ -188,6 +187,12 @@ public class TrainSignUpActivityStatisticsController {
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出指定活动的报名人员,按课程分别生成工作表,包含手机号等基础信息及该课程的动态报名字段。
|
||||
*
|
||||
* @param activityId 要导出报名人员的活动ID
|
||||
* @param response 接收Excel下载内容的HTTP响应;方法无返回值,输出.xlsx格式的报名人员名单
|
||||
*/
|
||||
@At
|
||||
@Ok("void")
|
||||
@ApiOperation("导出签到名单")
|
||||
@@ -237,9 +242,10 @@ public class TrainSignUpActivityStatisticsController {
|
||||
for (TrainSignUpCourse c : courseList) {
|
||||
String k = c.getCourseName();
|
||||
List<NutMap> v = userList.stream().filter(x -> StrUtil.isNotBlank(x.getString("courseName")) && x.getString("courseName").equals(k)).collect(Collectors.toList());
|
||||
ExportParams userExportParams = new ExportParams();
|
||||
userExportParams.setSheetName(k);
|
||||
userExportParams.setType(ExcelType.HSSF);
|
||||
ExportParams exportParams = new ExportParams();
|
||||
// 工作表参数与工作簿统一使用XSSF,确保导出的.xlsx文件格式一致。
|
||||
exportParams.setType(ExcelType.XSSF);
|
||||
exportParams.setSheetName(k);
|
||||
|
||||
List<ExcelExportEntity> currentEntities = new ArrayList<>();
|
||||
currentEntities.addAll(excelCommonExportEntity);
|
||||
@@ -285,20 +291,19 @@ public class TrainSignUpActivityStatisticsController {
|
||||
|
||||
Map<String, Object> userExportMap = new HashMap<>();
|
||||
userExportMap.put("name", k);
|
||||
userExportParams.setType(ExcelType.HSSF);
|
||||
userExportMap.put("title", userExportParams);
|
||||
userExportMap.put("title", exportParams);
|
||||
userExportMap.put("entity", currentEntities);
|
||||
userExportMap.put("data", v);
|
||||
|
||||
sheetsList.add(userExportMap);
|
||||
}
|
||||
|
||||
Workbook workbook = new HSSFWorkbook();
|
||||
Workbook workbook = new XSSFWorkbook();
|
||||
for (Map<String, Object> map : sheetsList) {
|
||||
ExcelExportService service = new ExcelExportService();
|
||||
service.createSheetForMap(workbook, (ExportParams) map.get("title"), (List<ExcelExportEntity>) map.get("entity"), (Collection<?>) map.get("data"));
|
||||
}
|
||||
CommonDownloadUtil.download(activity.getActivityName() + "报名人员名单" + ".xls", workbook, response);
|
||||
CommonDownloadUtil.download(activity.getActivityName() + "报名人员名单" + ".xlsx", workbook, response);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
@@ -557,6 +557,12 @@ public class ProposalExportServiceImpl extends BaseServiceImpl<ProposalInfo> imp
|
||||
return ExcelExportUtil.exportExcel(exportParams, exportEntities, list);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按综合导出查询条件将提案逐份生成为 DOCX,再完整打包为 ZIP。
|
||||
*
|
||||
* @param pageForm 综合导出页面的筛选参数,用于筛选提案名称、编号、单位等条件
|
||||
* @param response HTTP 响应,成功时直接输出 ZIP 二进制,包内每个 DOCX 对应一条提案
|
||||
*/
|
||||
@Override
|
||||
public void exportProposalStatisticsAsZip(ProposalQueryComprehensiveParam pageForm, HttpServletResponse response) {
|
||||
Sql sql = Sqls.create("""
|
||||
@@ -574,32 +580,38 @@ public class ProposalExportServiceImpl extends BaseServiceImpl<ProposalInfo> imp
|
||||
""");
|
||||
Cnd cnd = Cnd.NEW();
|
||||
ProposalQueryComprehensiveParam.buildSearch(cnd, pageForm);
|
||||
// 同一提案人可有多届代表记录,关联查询后按提案 ID 去重,避免重复生成同一提案。
|
||||
cnd.groupBy("info.id");
|
||||
sql.setCondition(cnd);
|
||||
List<NutMap> list = listMap(sql);
|
||||
|
||||
try {
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
ZipOutputStream zipOutputStream = new ZipOutputStream(bos);
|
||||
for (NutMap proposalRow : list) {
|
||||
try (ByteArrayOutputStream docxByteArrayOutputStream = new ByteArrayOutputStream()) {
|
||||
exportProposalAsDocx(proposalRow.getString("id"), docxByteArrayOutputStream);
|
||||
ZipEntry zipEntry = new ZipEntry(proposalRow.getString("code") + "-" + sanitizeZipFileName(proposalRow.getString("name")) + ".docx");
|
||||
zipOutputStream.putNextEntry(zipEntry);
|
||||
docxByteArrayOutputStream.writeTo(zipOutputStream);
|
||||
zipOutputStream.closeEntry();
|
||||
} catch (IOException e) {
|
||||
// 处理单个文件生成失败的情况
|
||||
throw new RuntimeException("zip entry write failed, proposalId=" + proposalRow.getString("id"), e);
|
||||
try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
|
||||
LinkedHashSet<String> zipEntryNames = new LinkedHashSet<>();
|
||||
try (ZipOutputStream zipOutputStream = new ZipOutputStream(bos)) {
|
||||
for (NutMap proposalRow : list) {
|
||||
try (ByteArrayOutputStream docxByteArrayOutputStream = new ByteArrayOutputStream()) {
|
||||
exportProposalAsDocx(proposalRow.getString("id"), docxByteArrayOutputStream);
|
||||
String baseEntryName = proposalRow.getString("code") + "-" + sanitizeZipFileName(proposalRow.getString("name"));
|
||||
String entryName = baseEntryName + ".docx";
|
||||
int sameNameIndex = 2;
|
||||
// 不同提案可能同名,或清理特殊字符后重名;按最终文件名检查,保留全部提案。
|
||||
while (!zipEntryNames.add(entryName)) {
|
||||
entryName = baseEntryName + "-" + sameNameIndex++ + ".docx";
|
||||
}
|
||||
zipOutputStream.putNextEntry(new ZipEntry(entryName));
|
||||
docxByteArrayOutputStream.writeTo(zipOutputStream);
|
||||
zipOutputStream.closeEntry();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("zip entry write failed, proposalId=" + proposalRow.getString("id"), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
zipOutputStream.finish();
|
||||
zipOutputStream.close();
|
||||
// 压缩流成功关闭、ZIP 目录完整写入后再下载,任一提案失败时不输出部分压缩包。
|
||||
CommonDownloadUtil.download("提案汇总压缩包.zip", bos.toByteArray(), response);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
log.error("批量导出提案表 ZIP 失败", e);
|
||||
throw new BaseException("批量导出提案表失败,请联系管理员");
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -4,16 +4,19 @@ import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.budwk.app.base.result.Result;
|
||||
import com.budwk.app.zhgh.huazhu.param.HuaZhuEmployeePageForm;
|
||||
import com.budwk.app.zhgh.huazhu.param.HuaZhuSyncPageForm;
|
||||
import com.budwk.app.zhgh.huazhu.service.HuaZhuEmployeeService;
|
||||
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||
import org.nutz.ioc.aop.Aop;
|
||||
import org.nutz.ioc.loader.annotation.Inject;
|
||||
import org.nutz.ioc.loader.annotation.IocBean;
|
||||
import org.nutz.mvc.annotation.At;
|
||||
import org.nutz.mvc.annotation.Ok;
|
||||
import org.nutz.mvc.annotation.Param;
|
||||
import org.nutz.lang.util.NutMap;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
|
||||
/**
|
||||
* PC 端华住员工同步管理。
|
||||
@@ -35,18 +38,78 @@ public class HuaZhuManageController {
|
||||
@At
|
||||
@SaCheckPermission("huazhu.manage")
|
||||
public Result pageData(@Valid HuaZhuEmployeePageForm pageForm) {
|
||||
return Result.success(huaZhuEmployeeService.pageUsers(pageForm));
|
||||
try {
|
||||
return Result.success(huaZhuEmployeeService.pageUsers(pageForm));
|
||||
} catch (Exception e) {
|
||||
return Result.error("读取待推送员工失败,请稍后重试");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param userIds 页面勾选员工的本项目用户主键数组,至少传入一个主键
|
||||
* @return Result,data 为已创建批次,含 id、total、status、submittedAt;失败原因在 msg
|
||||
*/
|
||||
@At
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
@SaCheckPermission("huazhu.manage")
|
||||
public Result syncUsers(@Param("userIds") String[] userIds) {
|
||||
if (userIds == null || userIds.length == 0) {
|
||||
return Result.error("请先选择需要同步的员工");
|
||||
}
|
||||
// 写入异常交给统一异常处理器;不在事务内部吞掉异常,保证批次与人员一起回滚。
|
||||
return Result.success(huaZhuEmployeeService.syncUsers(Arrays.asList(userIds)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 无需请求参数;只读取规则说明,供页面确认使用,不推送人员。
|
||||
* @return Result,data.description 为包含实际单位名称的中文规则;单位缺失时返回错误提示
|
||||
*/
|
||||
@At
|
||||
@SaCheckPermission("huazhu.manage")
|
||||
public Result syncRuleDescription() {
|
||||
try {
|
||||
return Result.success(NutMap.NEW().addv("description", huaZhuEmployeeService.getSyncRuleDescription()));
|
||||
} catch (IllegalStateException e) {
|
||||
return Result.error(e.getMessage());
|
||||
} catch (Exception e) {
|
||||
return Result.error("读取同步规则失败,请稍后重试");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 无需请求参数;由服务端按固定业务规则筛选全部待同步人员。
|
||||
* @return Result,data 为已创建批次,含 id、total、status、submittedAt;失败原因在 msg;无人需要推送时返回中文错误
|
||||
*/
|
||||
@At
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
@SaCheckPermission("huazhu.manage")
|
||||
public Result syncRuleUsers() {
|
||||
return Result.success(huaZhuEmployeeService.syncRuleUsers());
|
||||
}
|
||||
|
||||
/** taskId 为可选批次主键;data 含 task、counts、tasks、running,不触发推送。 */
|
||||
@At
|
||||
@SaCheckPermission("huazhu.manage")
|
||||
public Result syncSummary(@Param("taskId") String taskId) {
|
||||
try {
|
||||
return Result.success(huaZhuEmployeeService.syncSummary(taskId));
|
||||
} catch (Exception e) {
|
||||
return Result.error("读取推送进度失败,请稍后重试");
|
||||
}
|
||||
}
|
||||
|
||||
/** pageForm 含分页及可选批次、状态、工号/姓名;data.list 为逐人回执,totalCount 为总条数。 */
|
||||
@At
|
||||
@SaCheckPermission("huazhu.manage")
|
||||
public Result recordPageData(@Valid HuaZhuSyncPageForm pageForm) {
|
||||
try {
|
||||
return Result.success(huaZhuEmployeeService.pageRecords(pageForm));
|
||||
} catch (Exception e) {
|
||||
return Result.error("读取推送记录失败,请稍后重试");
|
||||
}
|
||||
}
|
||||
|
||||
/** userId 为本项目员工主键;data 为华住实时状态,msg 为可直接显示的中文错误。 */
|
||||
@At
|
||||
@SaCheckPermission("huazhu.manage")
|
||||
public Result queryStatus(@Param("userId") String userId) {
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.budwk.app.zhgh.huazhu.dto;
|
||||
|
||||
import com.alibaba.fastjson.annotation.JSONField;
|
||||
import lombok.Data;
|
||||
|
||||
/** 沿用原系统的认证响应对象及字段映射,同时用于 Redis 缓存序列化。 */
|
||||
@Data
|
||||
public class HuaZhuAuthTokenResultDTO {
|
||||
/** 华住返回的裸 Token,调用接口时再拼接 Bearer 前缀。 */
|
||||
@JSONField(name = "access_token")
|
||||
private String accessToken;
|
||||
@JSONField(name = "token_type")
|
||||
private String tokenType;
|
||||
/** 华住返回的有效时间,单位秒;保持原 DTO 的字符串类型。 */
|
||||
@JSONField(name = "expires_in")
|
||||
private String expiresIn;
|
||||
private String scope;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.budwk.app.zhgh.huazhu.job;
|
||||
|
||||
import com.budwk.app.zhgh.huazhu.service.HuaZhuEmployeeService;
|
||||
import org.nutz.ioc.loader.annotation.Inject;
|
||||
import org.nutz.ioc.loader.annotation.IocBean;
|
||||
import org.nutz.log.Log;
|
||||
import org.nutz.log.Logs;
|
||||
import org.quartz.DisallowConcurrentExecution;
|
||||
import org.quartz.Job;
|
||||
import org.quartz.JobExecutionContext;
|
||||
import org.quartz.JobExecutionException;
|
||||
|
||||
/** 固定后台入口;同一 Job 串行运行整批人员,页面离开不会中断执行。 */
|
||||
@IocBean
|
||||
@DisallowConcurrentExecution
|
||||
public class HuaZhuSyncJob implements Job {
|
||||
private static final Log log = Logs.get();
|
||||
@Inject
|
||||
private HuaZhuEmployeeService huaZhuEmployeeService;
|
||||
|
||||
/** context 为 Quartz 触发上下文;仅调用业务服务,不在 Job 中编排人员或开启整批事务。 */
|
||||
@Override
|
||||
public void execute(JobExecutionContext context) throws JobExecutionException {
|
||||
String fireId = context == null ? "-" : context.getFireInstanceId();
|
||||
long started = System.nanoTime();
|
||||
log.infof("[华住同步][步骤=调度触发] 开始,fireInstanceId=%s", fireId);
|
||||
if (huaZhuEmployeeService == null) {
|
||||
// 工厂回退为普通实例会丢失 IOC 注入,此日志明确区别于触发器没有触发。
|
||||
log.error("[华住同步][步骤=检查服务注入] 失败,Job 未获得华住服务,请检查 Quartz 的 IOC JobFactory 配置");
|
||||
throw new JobExecutionException("华住后台服务未注入", false);
|
||||
}
|
||||
try {
|
||||
huaZhuEmployeeService.runPendingTask();
|
||||
} catch (Exception e) {
|
||||
log.errorf("[华住同步][步骤=后台调用] 异常,fireInstanceId=%s,类型=%s,栈=%s", fireId,
|
||||
e.getClass().getName(), java.util.Arrays.toString(e.getStackTrace()));
|
||||
// 不携带可能含认证信息的原异常消息,不立即重发推送。
|
||||
throw new JobExecutionException("华住后台调用异常,请查看阶段日志", false);
|
||||
} finally {
|
||||
log.infof("[华住同步][步骤=调度结束] fireInstanceId=%s,耗时=%dms", fireId, (System.nanoTime() - started) / 1_000_000L);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.budwk.app.zhgh.huazhu.models;
|
||||
|
||||
import lombok.Data;
|
||||
import org.nutz.dao.entity.annotation.*;
|
||||
|
||||
/** 每次推送的人员快照与回执;不保存认证 token、卡密码或请求正文。 */
|
||||
@Data
|
||||
@Table("huazhu_sync_record")
|
||||
@TableMeta("{'mysql-charset':'utf8mb4','mysql-engine':'InnoDB'}")
|
||||
// 同批次及同一员工均可存在多条相同状态的记录;这两个索引仅用于查询,不能限制唯一性。
|
||||
@TableIndexes({@Index(name = "IDX_HUAZHU_TASK_STATUS", fields = {"taskId", "status"}, unique = false),
|
||||
@Index(name = "IDX_HUAZHU_USER_STATUS", fields = {"userId", "status"}, unique = false)})
|
||||
public class HuaZhuSyncRecord {
|
||||
@Name
|
||||
@Column
|
||||
@ColDefine(type = ColType.VARCHAR, width = 32)
|
||||
private String id;
|
||||
@Column
|
||||
@ColDefine(type = ColType.VARCHAR, width = 32)
|
||||
private String taskId;
|
||||
@Column
|
||||
@ColDefine(type = ColType.VARCHAR, width = 32)
|
||||
private String userId;
|
||||
@Column
|
||||
private String loginName;
|
||||
@Column
|
||||
private String userName;
|
||||
/** PENDING 待推送、SENDING 发送中、SUCCESS 推送成功、EXISTS 华住已存在、FAILED 明确失败、UNKNOWN 待核实。 */
|
||||
@Column
|
||||
private String status;
|
||||
@Column
|
||||
@ColDefine(type = ColType.TEXT)
|
||||
private String message;
|
||||
@Column
|
||||
private Long submittedAt;
|
||||
@Column
|
||||
private Long finishedAt;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.budwk.app.zhgh.huazhu.models;
|
||||
|
||||
import lombok.Data;
|
||||
import org.nutz.dao.entity.annotation.*;
|
||||
|
||||
/** 华住推送批次。activeSlot 唯一约束防止管理员重复提交;后台按当前单实例 Quartz 配置串行执行。 */
|
||||
@Data
|
||||
@Table("huazhu_sync_task")
|
||||
@TableMeta("{'mysql-charset':'utf8mb4','mysql-engine':'InnoDB'}")
|
||||
@TableIndexes({@Index(name = "UK_HUAZHU_ACTIVE_SLOT", fields = {"activeSlot"}, unique = true)})
|
||||
public class HuaZhuSyncTask {
|
||||
@Name
|
||||
@Column
|
||||
@ColDefine(type = ColType.VARCHAR, width = 32)
|
||||
private String id;
|
||||
@Column
|
||||
@ColDefine(type = ColType.VARCHAR, width = 32)
|
||||
private String activeSlot;
|
||||
/** SELECTED 为勾选同步,RULE 为规则同步。 */
|
||||
@Column
|
||||
private String source;
|
||||
/** RUNNING 为已提交待后台处理或处理中,COMPLETED 为遍历结束,FAILED 为批次异常中止。 */
|
||||
@Column
|
||||
private String status;
|
||||
@Column
|
||||
private Integer total;
|
||||
@Column
|
||||
private Long submittedAt;
|
||||
@Column
|
||||
private Long finishedAt;
|
||||
/** 兼容旧表保留的租约字段;现由固定 Quartz Job 串行执行,不再按逐人租约领取。 */
|
||||
@Column
|
||||
private Long leaseUntil;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.budwk.app.zhgh.huazhu.param;
|
||||
|
||||
import com.budwk.app.base.param.PageForm;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/** 推送记录分页:taskId 为空查全部批次,status 为空查全部状态,searchKeyword 匹配工号或姓名。 */
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class HuaZhuSyncPageForm extends PageForm {
|
||||
private String taskId;
|
||||
private String status;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.budwk.app.zhgh.huazhu.service;
|
||||
|
||||
import com.budwk.app.zhgh.huazhu.dto.HuaZhuAuthTokenResultDTO;
|
||||
|
||||
/** 华住认证,保留原系统 getToken 接口。 */
|
||||
public interface HuaZhuAuthService {
|
||||
/** 无参数;优先读取缓存,否则请求认证。返回 DTO,accessToken 为裸 Token,失败抛出异常。 */
|
||||
HuaZhuAuthTokenResultDTO getToken();
|
||||
}
|
||||
@@ -3,7 +3,9 @@ package com.budwk.app.zhgh.huazhu.service;
|
||||
import com.budwk.app.base.page.Pagination;
|
||||
import com.budwk.app.zhgh.huazhu.param.HuaZhuEmployeePageForm;
|
||||
import com.budwk.app.zhgh.huazhu.vo.HuaZhuEmployeeStatus;
|
||||
import com.budwk.app.zhgh.huazhu.vo.HuaZhuSyncResult;
|
||||
import com.budwk.app.zhgh.huazhu.models.HuaZhuSyncTask;
|
||||
import com.budwk.app.zhgh.huazhu.models.HuaZhuSyncRecord;
|
||||
import com.budwk.app.zhgh.huazhu.param.HuaZhuSyncPageForm;
|
||||
import org.nutz.lang.util.NutMap;
|
||||
|
||||
import java.util.List;
|
||||
@@ -25,9 +27,47 @@ public interface HuaZhuEmployeeService {
|
||||
* 将选中的本项目员工同步到华住。
|
||||
*
|
||||
* @param userIds 本项目用户主键列表
|
||||
* @return 每位员工的同步结果
|
||||
* @return 已创建批次,含 id、total、status 和 submittedAt;实际推送在后台进行
|
||||
*/
|
||||
List<HuaZhuSyncResult> syncUsers(List<String> userIds);
|
||||
HuaZhuSyncTask syncUsers(List<String> userIds);
|
||||
|
||||
/**
|
||||
* 获取规则同步的中文说明,无需传参;单位名称从当前单位表读取,缺失时拒绝继续同步。
|
||||
*
|
||||
* @return 包含在岗条件、限定单位名称及非自聘条件的说明文字
|
||||
*/
|
||||
String getSyncRuleDescription();
|
||||
|
||||
/**
|
||||
* 同步全部符合规则且未删除的员工,无需传参,不受页面搜索、分页或勾选范围限制。
|
||||
*
|
||||
* @return 已创建批次;无人需要推送时抛出中文业务异常
|
||||
*/
|
||||
HuaZhuSyncTask syncRuleUsers();
|
||||
|
||||
/** 无参数、无返回值;后台顺序推送一个批次,明确认证失败时刷新 Token 并重试当前人员一次,仍失败则停止批次。 */
|
||||
void runPendingTask();
|
||||
|
||||
/** 无参数;返回活动批次或 null,将上次中断的 SENDING 转待核实,禁止自动重发。 */
|
||||
HuaZhuSyncTask prepareBatch();
|
||||
|
||||
/** recordId 为记录主键、taskId 为批次主键;返回是否将待推送记录成功标为发送中。 */
|
||||
boolean markSending(String recordId, String taskId);
|
||||
|
||||
/** record 为发送结果;单独事务保存,状态不匹配或保存失败时抛出异常。 */
|
||||
void finishRecord(HuaZhuSyncRecord record);
|
||||
|
||||
/** taskId 为完成批次主键;确认无待处理人员后结束批次,无返回值。 */
|
||||
void completeBatch(String taskId);
|
||||
|
||||
/** taskId 为异常批次、reason 为中文原因;保留已完成结果,结束剩余记录及批次。 */
|
||||
void failBatch(String taskId, String reason);
|
||||
|
||||
/** taskId 可为空,返回最近/指定批次、状态计数、最近批次及后台是否运行。 */
|
||||
NutMap syncSummary(String taskId);
|
||||
|
||||
/** pageForm 提供分页及可选批次、状态、关键词;返回每人的持久化推送记录。 */
|
||||
Pagination<HuaZhuSyncRecord> pageRecords(HuaZhuSyncPageForm pageForm);
|
||||
|
||||
/**
|
||||
* 查询指定本项目员工在华住的实时状态。
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
package com.budwk.app.zhgh.huazhu.service.impl;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.http.HttpRequest;
|
||||
import cn.hutool.http.HttpResponse;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.budwk.app.base.constant.RedisConstant;
|
||||
import com.budwk.app.zhgh.huazhu.constant.HuaZhuConstant;
|
||||
import com.budwk.app.zhgh.huazhu.dto.HuaZhuAuthTokenResultDTO;
|
||||
import com.budwk.app.zhgh.huazhu.service.HuaZhuAuthService;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||
import org.nutz.ioc.aop.Aop;
|
||||
import org.nutz.ioc.loader.annotation.Inject;
|
||||
import org.nutz.ioc.loader.annotation.IocBean;
|
||||
import org.nutz.integration.jedis.RedisService;
|
||||
import org.nutz.log.Log;
|
||||
import org.nutz.log.Logs;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
|
||||
/** 原认证方法仅增加阶段日志和响应关闭,认证参数、DTO 转换及缓存内容保持原写法。 */
|
||||
@IocBean
|
||||
public class HuaZhuAuthServiceImpl implements HuaZhuAuthService {
|
||||
private static final Log log = Logs.get();
|
||||
@Inject
|
||||
private RedisService redisService;
|
||||
|
||||
/** 无参数;返回原 DTO。缓存 58 分钟,认证请求 20 秒超时,日志不输出认证头或 Token。 */
|
||||
@Override
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public HuaZhuAuthTokenResultDTO getToken() {
|
||||
String step = "读取Token缓存";
|
||||
long started = System.nanoTime();
|
||||
try {
|
||||
log.info("[华住认证][步骤=读取Token缓存] 开始");
|
||||
String token = redisService.get(RedisConstant.REDIS_KEY_HUAZHU_AUTH_TOKEN);
|
||||
if (StringUtils.isNotBlank(token)) {
|
||||
step = "缓存转Token对象";
|
||||
log.info("[华住认证][步骤=缓存转Token对象] 开始,命中缓存");
|
||||
return JSONUtil.toBean(token, HuaZhuAuthTokenResultDTO.class);
|
||||
}
|
||||
step = "构建Basic认证请求";
|
||||
log.info("[华住认证][步骤=构建Basic认证请求] 开始");
|
||||
String url = String.format("%s%s", HuaZhuConstant.OAUTH2_URL, "/oauth/token?scope=ALL&grant_type=client_credentials");
|
||||
String sign = StringUtils.join("Basic ", Base64.getEncoder().encodeToString(String.format("%s:%s", HuaZhuConstant.CLIENT_ID, HuaZhuConstant.CLIENT_SECRET).getBytes(StandardCharsets.UTF_8)));
|
||||
step = "请求华住Token";
|
||||
log.info("[华住认证][步骤=请求华住Token] 开始,超时=20000ms");
|
||||
try (HttpResponse response = HttpRequest.post(url).header("Authorization", sign).timeout(20000).execute()) {
|
||||
log.infof("[华住认证][步骤=请求华住Token] 收到响应,HTTP=%d", response.getStatus());
|
||||
if (response.isOk()) {
|
||||
step = "读取并转换认证响应";
|
||||
log.info("[华住认证][步骤=读取并转换认证响应] 开始");
|
||||
String body = response.body();
|
||||
HuaZhuAuthTokenResultDTO tokenResultDTO = JSONUtil.toBean(body, HuaZhuAuthTokenResultDTO.class);
|
||||
String newToken = tokenResultDTO.getAccessToken();
|
||||
if (StrUtil.isBlank(newToken)) {
|
||||
throw new RuntimeException("请求华住认证接口失败,返回的Token为空");
|
||||
}
|
||||
step = "缓存Token对象";
|
||||
log.info("[华住认证][步骤=缓存Token对象] 开始,有效期=3480秒");
|
||||
redisService.setex(RedisConstant.REDIS_KEY_HUAZHU_AUTH_TOKEN, 60 * 58, JSONUtil.toJsonStr(tokenResultDTO));
|
||||
return tokenResultDTO;
|
||||
} else {
|
||||
throw new RuntimeException("请求华住认证接口失败,状态码:" + response.getStatus());
|
||||
}
|
||||
}
|
||||
} catch (RuntimeException e) {
|
||||
// 认证异常原文可能包含响应正文,只记录类型和位置,业务层另行返回受控中文原因。
|
||||
log.errorf("[华住认证][步骤=%s] 失败,异常类型=%s,位置=%s", step, e.getClass().getName(),
|
||||
e.getStackTrace().length == 0 ? "无" : e.getStackTrace()[0]);
|
||||
throw e;
|
||||
} finally {
|
||||
log.infof("[华住认证][步骤=%s] 调用结束,耗时=%dms", step, (System.nanoTime() - started) / 1_000_000L);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -81,10 +81,10 @@ public class CondolenceApplyController {
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存慰问申报截止日期,服务层会再次校验当前用户必须为系统管理员。
|
||||
* 保存慰问申报截止日期,服务层会再次校验当前用户必须为系统管理员或校工会管理员。
|
||||
*
|
||||
* @param endDate 截止日期,格式为 yyyy-MM-dd
|
||||
* @return 保存成功结果
|
||||
* @return Result,保存成功时返回成功状态和“设置成功”提示
|
||||
*/
|
||||
@At
|
||||
@ApiOperation("保存慰问申报截止日期")
|
||||
|
||||
@@ -22,7 +22,7 @@ public interface CondolenceService extends BaseService<Condolence> {
|
||||
String getApplyEndDate();
|
||||
|
||||
/**
|
||||
* 保存慰问申报截止日期,仅允许系统管理员操作。
|
||||
* 保存慰问申报截止日期,仅允许系统管理员或校工会管理员操作。
|
||||
*
|
||||
* @param endDate 截止日期,格式为 yyyy-MM-dd
|
||||
*/
|
||||
|
||||
@@ -90,15 +90,15 @@ public class CondolenceServiceImpl extends BaseServiceImpl<Condolence> implement
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存慰问申报截止日期。参数必须是 yyyy-MM-dd,且仅 SYSADMIN 可以修改。
|
||||
* 保存慰问申报截止日期。参数必须是 yyyy-MM-dd,仅系统管理员或校工会管理员可以修改。
|
||||
*
|
||||
* @param endDate 截止日期,格式为 yyyy-MM-dd
|
||||
*/
|
||||
@Override
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public void saveApplyEndDate(String endDate) {
|
||||
if (!AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name())) {
|
||||
throw new BaseException("仅系统管理员可以设置慰问申报时间");
|
||||
if (!AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name(), RoleConstant.SCHOOL_UNION_ADMIN.name())) {
|
||||
throw new BaseException("仅系统管理员或校工会管理员可以设置慰问申报时间");
|
||||
}
|
||||
parseApplyEndDate(endDate);
|
||||
Sys_config config = sysConfigService.fetch(APPLY_END_DATE_CONFIG_KEY);
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
package com.budwk.app.zhgh.staffbenefit.difficultSubsidy.controller.process;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import cn.dev33.satoken.annotation.SaMode;
|
||||
import com.budwk.app.base.result.Result;
|
||||
import com.budwk.app.zhgh.staffbenefit.difficultSubsidy.models.DifficultSubsidyApply;
|
||||
import com.budwk.app.zhgh.staffbenefit.difficultSubsidy.service.DifficultSubsidyService;
|
||||
import org.nutz.ioc.loader.annotation.Inject;
|
||||
import org.nutz.ioc.loader.annotation.IocBean;
|
||||
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||
import org.nutz.ioc.aop.Aop;
|
||||
import org.nutz.mvc.annotation.At;
|
||||
import org.nutz.mvc.annotation.Ok;
|
||||
import org.nutz.mvc.annotation.Param;
|
||||
@@ -36,9 +39,32 @@ public class DifficultSubsidyApplyController {
|
||||
public void form() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 无请求参数;返回 Result,data 为 yyyy-MM-dd 截止日期,空字符串表示未设置。
|
||||
* 审核修改复用本表单,因此允许申请或校工会审核权限读取。
|
||||
*/
|
||||
@At
|
||||
@SaCheckPermission(value = {"difficultSubsidy.apply", "difficultSubsidy.schoolAudit"}, mode = SaMode.OR)
|
||||
public Result getApplyEndDate() {
|
||||
return Result.success().addData(difficultSubsidyService.getApplyEndDate());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param endDate yyyy-MM-dd 格式的申报截止日期;允许设置的角色由 service 统一校验
|
||||
* @return Result 成功状态,不附加业务数据
|
||||
*/
|
||||
@At
|
||||
@SaCheckPermission("difficultSubsidy.apply")
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public Result saveApplyEndDate(@Param("endDate") String endDate) {
|
||||
difficultSubsidyService.saveApplyEndDate(endDate);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@At
|
||||
@Ok("json")
|
||||
@SaCheckPermission("difficultSubsidy.apply")
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public Result save(@Param("data") DifficultSubsidyApply apply) {
|
||||
return Result.success(difficultSubsidyService.saveDraft(apply));
|
||||
}
|
||||
@@ -46,6 +72,7 @@ public class DifficultSubsidyApplyController {
|
||||
@At
|
||||
@Ok("json")
|
||||
@SaCheckPermission("difficultSubsidy.apply")
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public Result submit(@Param("data") DifficultSubsidyApply apply) {
|
||||
return Result.success(difficultSubsidyService.submit(apply));
|
||||
}
|
||||
|
||||
@@ -15,6 +15,23 @@ import java.util.List;
|
||||
*/
|
||||
public interface DifficultSubsidyService extends BaseService<DifficultSubsidyApply> {
|
||||
|
||||
/**
|
||||
* @return 系统参数 DifficultSubsidyApplyEndDate 的 yyyy-MM-dd 日期,未配置时返回空字符串
|
||||
*/
|
||||
String getApplyEndDate();
|
||||
|
||||
/**
|
||||
* 仅系统管理员、校工会管理员可设置特困补助申报截止日期。
|
||||
* @param endDate yyyy-MM-dd 格式的截止日期,当天起停止新增申报
|
||||
*/
|
||||
void saveApplyEndDate(String endDate);
|
||||
|
||||
/**
|
||||
* 校验新申请是否在申报期内,数据库中已存在的申请可继续修改和重新提交。
|
||||
* @param id 申请记录ID;空值或数据库中不存在的ID均按新申请校验
|
||||
*/
|
||||
void checkApplyDeadline(String id);
|
||||
|
||||
DifficultSubsidyApply saveDraft(DifficultSubsidyApply apply);
|
||||
|
||||
DifficultSubsidyApply submit(DifficultSubsidyApply apply);
|
||||
|
||||
@@ -13,6 +13,7 @@ import com.budwk.app.base.utils.SysOfficeTemplateUtil;
|
||||
import com.budwk.app.base.page.Pagination;
|
||||
import com.budwk.app.base.service.impl.BaseServiceImpl;
|
||||
import com.budwk.app.base.constant.RoleConstant;
|
||||
import com.budwk.app.base.exception.BaseException;
|
||||
import com.budwk.app.flow.constant.FlowConst;
|
||||
import com.budwk.app.flow.engine.FlowEngine;
|
||||
import com.budwk.app.flow.entity.ProcessDefine;
|
||||
@@ -26,6 +27,8 @@ import com.budwk.app.flow.enums.ProcessTaskStateEnum;
|
||||
import com.budwk.app.flow.service.FlowCommonService;
|
||||
import com.budwk.app.flow.vo.ProcessTaskVO;
|
||||
import com.budwk.app.sys.services.SysFileService;
|
||||
import com.budwk.app.sys.services.SysConfigService;
|
||||
import com.budwk.app.sys.models.Sys_config;
|
||||
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
|
||||
import com.budwk.app.web.commons.auth.utils.AuthUtil;
|
||||
import com.budwk.app.zhgh.staffbenefit.difficultSubsidy.constants.DifficultSubsidyState;
|
||||
@@ -56,6 +59,8 @@ import org.nutz.lang.util.NutMap;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeParseException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
@@ -78,6 +83,12 @@ public class DifficultSubsidyServiceImpl extends BaseServiceImpl<DifficultSubsid
|
||||
private static final String PROCESS_KEY = "TKBZ";
|
||||
private static final String SCHOOL_AUDIT_TASK_NAME = "校工会审核";
|
||||
private static final String START_TASK_NAME = "startTask";
|
||||
// 沿用现有角色表中的特困补助审批员编码,供全校代申请范围判断使用。
|
||||
private static final String SCHOOL_AUDIT_ROLE_CODE = "sp04";
|
||||
private static final String APPLY_END_DATE_CONFIG_KEY = "DifficultSubsidyApplyEndDate";
|
||||
|
||||
@Inject
|
||||
private SysConfigService sysConfigService;
|
||||
|
||||
@Inject
|
||||
private FlowEngine flowEngine;
|
||||
@@ -95,9 +106,84 @@ public class DifficultSubsidyServiceImpl extends BaseServiceImpl<DifficultSubsid
|
||||
super(dao);
|
||||
}
|
||||
|
||||
/** 直接读取系统参数,保证管理员保存后下一次申请校验立即生效。 */
|
||||
@Override
|
||||
public String getApplyEndDate() {
|
||||
Sys_config config = sysConfigService.fetch(APPLY_END_DATE_CONFIG_KEY);
|
||||
return config == null ? "" : StrUtil.blankToDefault(config.getConfigValue(), "");
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存申报时间配置,页面隐藏入口之外还需在服务端校验管理角色。
|
||||
* @param endDate yyyy-MM-dd 格式的截止日期,截止当天即停止新增申报
|
||||
*/
|
||||
@Override
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public void saveApplyEndDate(String endDate) {
|
||||
if (!AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name(), RoleConstant.SCHOOL_UNION_ADMIN.name())) {
|
||||
throw new BaseException("仅系统管理员和校工会管理员可以设置特困补助申报时间");
|
||||
}
|
||||
parseApplyEndDate(endDate);
|
||||
Sys_config config = sysConfigService.fetch(APPLY_END_DATE_CONFIG_KEY);
|
||||
if (config == null) {
|
||||
config = new Sys_config();
|
||||
config.setConfigKey(APPLY_END_DATE_CONFIG_KEY);
|
||||
config.setConfigValue(endDate);
|
||||
config.setNote("特困补助申请截止日期(yyyy-MM-dd)");
|
||||
sysConfigService.insert(config);
|
||||
} else {
|
||||
config.setConfigValue(endDate);
|
||||
sysConfigService.update(config);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 截止时间仅限制新申请;已有草稿和退回记录可继续办理,不信任客户端随意传入的ID。
|
||||
* @param id 申请ID;只有数据库中真实存在的记录才免除新申请截止校验
|
||||
*/
|
||||
@Override
|
||||
public void checkApplyDeadline(String id) {
|
||||
if (Strings.isNotBlank(id) && dao().fetch(DifficultSubsidyApply.class, id) != null) {
|
||||
return;
|
||||
}
|
||||
String endDate = getApplyEndDate();
|
||||
if (Strings.isBlank(endDate)) {
|
||||
return;
|
||||
}
|
||||
// 与慰问申报保持一致:截止日期当天已关闭,不按当天 23:59:59 计算。
|
||||
if (!LocalDate.now().isBefore(parseApplyEndDate(endDate))) {
|
||||
throw new BaseException("本次申报已截止请关注下次申报");
|
||||
}
|
||||
}
|
||||
|
||||
/** 严格校验配置格式及真实日历日期,避免错误配置被当作开放申报。 */
|
||||
private LocalDate parseApplyEndDate(String endDate) {
|
||||
if (Strings.isBlank(endDate)) {
|
||||
throw new BaseException("申报截止日期不能为空");
|
||||
}
|
||||
try {
|
||||
if (!endDate.matches("\\d{4}-\\d{2}-\\d{2}")) {
|
||||
throw new DateTimeParseException("Invalid date format", endDate, 0);
|
||||
}
|
||||
return LocalDate.parse(endDate);
|
||||
} catch (DateTimeParseException e) {
|
||||
throw new BaseException("申报截止日期格式不正确,应为yyyy-MM-dd");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存本人或当前账号有权代填的特困补助申请,受助人身份和组织信息以人员资料为准。
|
||||
*
|
||||
* @param apply 页面表单;userId 为受助人用户ID,未选择时按当前登录人处理,其余为业务填写内容
|
||||
* @return 保存后的申请实体,applyUserId 为填报人,userId 为实际受助人
|
||||
*/
|
||||
@Override
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public DifficultSubsidyApply saveDraft(DifficultSubsidyApply apply) {
|
||||
if (apply == null) {
|
||||
throw new IllegalArgumentException("申请表单不能为空");
|
||||
}
|
||||
checkApplyDeadline(apply.getId());
|
||||
fillApplicantSnapshot(apply);
|
||||
normalizeFormCollections(apply);
|
||||
if (apply.getApplyDate() == null) {
|
||||
@@ -110,21 +196,18 @@ public class DifficultSubsidyServiceImpl extends BaseServiceImpl<DifficultSubsid
|
||||
return apply;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按代申请范围搜索受助人,审批员和校级管理员可搜索全校人员,分工会管理角色限本工会。
|
||||
*
|
||||
* @param keyword 姓名或工号关键字,空值时返回当前可选范围内的前十人
|
||||
* @return 最多十条人员信息,包含id、loginname、username、birthday、mobile、sex及单位、工会ID和名称
|
||||
*/
|
||||
@Override
|
||||
public List<NutMap> listRecipients(String keyword) {
|
||||
Sql sql = Sqls.create("SELECT id, loginname, username, birthday, mobile, sex, "
|
||||
+ "unitName, unionName, unionId, unitId FROM vw_user $condition");
|
||||
Cnd cnd = Cnd.NEW();
|
||||
|
||||
// 校工会管理员可选择全校人员,分工会管理员仅可选择本工会人员,普通用户仅可选择本人。
|
||||
if (!AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name(), RoleConstant.SCHOOL_UNION_ADMIN.name())) {
|
||||
if (AuthUtil.hasRoleOr(RoleConstant.BRANCH_UNION_CHAIRMAN.name(),
|
||||
RoleConstant.BRANCH_UNION_ADMIN.name(), RoleConstant.BRANCH_UNION_OPERATOR.name())) {
|
||||
cnd.and("unionId", "=", SecurityUtil.getUnionId());
|
||||
} else {
|
||||
cnd.and("id", "=", SecurityUtil.getUserId());
|
||||
}
|
||||
}
|
||||
appendRecipientScope(cnd);
|
||||
if (Strings.isNotBlank(keyword)) {
|
||||
SqlExpressionGroup keywordGroup = new SqlExpressionGroup();
|
||||
keywordGroup.orLike("username", keyword);
|
||||
@@ -725,23 +808,58 @@ public class DifficultSubsidyServiceImpl extends BaseServiceImpl<DifficultSubsid
|
||||
}
|
||||
|
||||
/**
|
||||
* 补全申请人快照。受助人的姓名、工号、单位等数据来自页面选择结果,不更新 sys_user。
|
||||
* 将同一套代申请范围用于人员搜索和保存校验,避免只限制下拉选项而允许越权提交受助人ID。
|
||||
*
|
||||
* @param cnd 人员查询条件;审批员和校级管理员不追加限制,分工会管理角色限本工会,其他账号限本人
|
||||
*/
|
||||
private void appendRecipientScope(Cnd cnd) {
|
||||
if (AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name(), RoleConstant.SCHOOL_UNION_ADMIN.name(),
|
||||
SCHOOL_AUDIT_ROLE_CODE)) {
|
||||
return;
|
||||
}
|
||||
if (AuthUtil.hasRoleOr(RoleConstant.BRANCH_UNION_CHAIRMAN.name(),
|
||||
RoleConstant.BRANCH_UNION_ADMIN.name(), RoleConstant.BRANCH_UNION_OPERATOR.name())) {
|
||||
// 未配置工会时仅允许本人,不能把空工会下的其他人员纳入代申请范围。
|
||||
String unionId = SecurityUtil.getUnionId();
|
||||
if (Strings.isNotBlank(unionId)) {
|
||||
cnd.and("unionId", "=", unionId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
cnd.and("id", "=", SecurityUtil.getUserId());
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录当前填报人,并核实受助人确实在该账号可选择的范围内;跨部门代填时保留受助人真实组织。
|
||||
*
|
||||
* @param apply 页面表单,userId为空时默认本人;姓名、工号、性别、单位和工会字段由查询结果覆盖
|
||||
*/
|
||||
private void fillApplicantSnapshot(DifficultSubsidyApply apply) {
|
||||
if (apply == null) {
|
||||
throw new IllegalArgumentException("申请表单不能为空");
|
||||
}
|
||||
String userId = Strings.isBlank(apply.getUserId()) ? SecurityUtil.getUserId() : apply.getUserId();
|
||||
Sql sql = Sqls.create("SELECT id, loginname, username, sex, unitId, unitName, unionId, unionName "
|
||||
+ "FROM vw_user $condition");
|
||||
Cnd cnd = Cnd.where("id", "=", userId);
|
||||
appendRecipientScope(cnd);
|
||||
sql.setCondition(cnd);
|
||||
List<NutMap> recipients = listMap(sql);
|
||||
if (recipients.isEmpty()) {
|
||||
throw new IllegalArgumentException("受助人不存在或不在当前账号可代申请的范围内");
|
||||
}
|
||||
NutMap recipient = recipients.get(0);
|
||||
apply.setApplyUserId(SecurityUtil.getUserId());
|
||||
apply.setApplyUserName(SecurityUtil.getUserUsername());
|
||||
apply.setApplyLoginName(SecurityUtil.getUserLoginname());
|
||||
if (Strings.isBlank(apply.getUserId())) {
|
||||
apply.setUserId(apply.getApplyUserId());
|
||||
apply.setUserName(apply.getApplyUserName());
|
||||
apply.setLoginName(apply.getApplyLoginName());
|
||||
}
|
||||
if (Strings.isBlank(apply.getUnitId())) {
|
||||
apply.setUnitId(SecurityUtil.getUnitId());
|
||||
}
|
||||
if (Strings.isBlank(apply.getUnionId())) {
|
||||
apply.setUnionId(SecurityUtil.getUnionId());
|
||||
}
|
||||
apply.setUserId(recipient.getString("id"));
|
||||
apply.setUserName(recipient.getString("username"));
|
||||
apply.setLoginName(recipient.getString("loginname"));
|
||||
apply.setSex(recipient.getString("sex"));
|
||||
apply.setUnitId(recipient.getString("unitId"));
|
||||
apply.setUnitName(recipient.getString("unitName"));
|
||||
apply.setUnionId(recipient.getString("unionId"));
|
||||
apply.setUnionName(recipient.getString("unionName"));
|
||||
}
|
||||
|
||||
private void normalizeFormCollections(DifficultSubsidyApply apply) {
|
||||
|
||||
@@ -4,3 +4,11 @@ ALTER TABLE `user_birthday_config`
|
||||
ADD COLUMN `autoSendEnabled` tinyint(1) DEFAULT 1 COMMENT '是否自动发送生日祝福',
|
||||
ADD COLUMN `sendTitle` varchar(255) DEFAULT NULL COMMENT '默认发送标题',
|
||||
ADD COLUMN `sendContent` text COMMENT '默认发送内容';
|
||||
|
||||
|
||||
INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, processKey, platform, moduleId, isQuickEntry, picIcon, initialPinyinName, isRecommendApp, isRecommendService) VALUES ('9f5c6f6bc684452997079b536ad5fa87', '93487f22face4e18887d90154b70094a', '01610004', '生日消息记录', null, 'menu', '/platform/staffManage/birthday/msgLog', null, null, true, false, 'staff.birthday.msgLog', null, 1013, false, '17a7f8ad3ee947b4a26175049a9c253d', 1790214237143, '17a7f8ad3ee947b4a26175049a9c253d', 1790214237144, false, null, 'PC', '07d940ba34bd47399cda140894f5d86b', false, '/platform/sys/file/download?id=f71c9e07faf05ac4bf8a5364ae0bfd85', 's', false, false);
|
||||
INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, processKey, platform, moduleId, isQuickEntry, picIcon, initialPinyinName, isRecommendApp, isRecommendService) VALUES ('df478143ff1846ddab704616d9fdae41', '93487f22face4e18887d90154b70094a', '01610005', '生日模板配置', null, 'menu', '/platform/staffManage/birthday/config', null, null, true, false, 'staff.birthday.config', null, 1014, false, '17a7f8ad3ee947b4a26175049a9c253d', 1790220577535, '17a7f8ad3ee947b4a26175049a9c253d', 1790220577536, false, null, 'PC', '07d940ba34bd47399cda140894f5d86b', false, '/platform/sys/file/download?id=f71c9e07faf05ac4bf8a5364ae0bfd85', 's', false, false);
|
||||
INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, processKey, platform, moduleId, isQuickEntry, picIcon, initialPinyinName, isRecommendApp, isRecommendService) VALUES ('f614e4a8117e40e487883c292d7a0950', '93487f22face4e18887d90154b70094a', '01610003', '生日祝福管理', null, 'menu', '/platform/staffManage/birthday/manage', null, null, true, false, 'staff.birthday.manage', null, 1012, false, '17a7f8ad3ee947b4a26175049a9c253d', 1790214217197, '17a7f8ad3ee947b4a26175049a9c253d', 1790215149743, false, null, 'PC', '07d940ba34bd47399cda140894f5d86b', false, '/platform/sys/file/download?id=f71c9e07faf05ac4bf8a5364ae0bfd85', 's', false, false);
|
||||
INSERT INTO sys_role_menu (roleId, menuId) VALUES ('7f4a820d5a394bb8a0a92d6699afe7c8', '9f5c6f6bc684452997079b536ad5fa87');
|
||||
INSERT INTO sys_role_menu (roleId, menuId) VALUES ('7f4a820d5a394bb8a0a92d6699afe7c8', 'df478143ff1846ddab704616d9fdae41');
|
||||
INSERT INTO sys_role_menu (roleId, menuId) VALUES ('7f4a820d5a394bb8a0a92d6699afe7c8', 'f614e4a8117e40e487883c292d7a0950');
|
||||
@@ -1,59 +1,60 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<configuration scan="false" scanPeriod="60000" debug="false">
|
||||
|
||||
<!-- 定义日志文件的存储路径 -->
|
||||
<!-- 日志存储路径 -->
|
||||
<property name="LOG_HOME" value="./logs"/>
|
||||
|
||||
<!-- 控制台输出配置 -->
|
||||
<!-- 控制台输出 -->
|
||||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<!-- INFO级别日志文件配置 -->
|
||||
<appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>${LOG_HOME}/info.log</file>
|
||||
|
||||
<!--
|
||||
#################################################################################
|
||||
# #
|
||||
# 全文输出日志(一个文件存储)start #
|
||||
# #
|
||||
#################################################################################
|
||||
-->
|
||||
|
||||
<!-- 所有级别日志写入同一个文件 -->
|
||||
<appender name="ALL_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>${LOG_HOME}/app.log</file>
|
||||
<encoder>
|
||||
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
|
||||
<pattern>%d{yyyy-MM-dd 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>30</maxHistory>
|
||||
<fileNamePattern>${LOG_HOME}/app-%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>30</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 logger:DEBUG 级别,输出到控制台和全量文件 -->
|
||||
<root level="DEBUG">
|
||||
<appender-ref ref="STDOUT"/>
|
||||
<appender-ref ref="INFO_FILE"/>
|
||||
<appender-ref ref="ERROR_FILE"/>
|
||||
<appender-ref ref="ALL_FILE"/>
|
||||
</root>
|
||||
|
||||
<!--
|
||||
#################################################################################
|
||||
# #
|
||||
# 全文输出日志(一个文件存储)end #
|
||||
# #
|
||||
#################################################################################
|
||||
-->
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</configuration>
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 985 KiB |
|
After Width: | Height: | Size: 628 KiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 2.1 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 708 KiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.6 MiB |
|
After Width: | Height: | Size: 695 KiB |
@@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 128 128" fill="none">
|
||||
<circle cx="64" cy="64" r="58" stroke="#F5222D" stroke-width="3"/>
|
||||
<path d="M21 54A44 44 0 0 1 107 54" stroke="#F5222D" stroke-width="9" stroke-linecap="round"/>
|
||||
<path d="M21 74A44 44 0 0 0 107 74" stroke="#F5222D" stroke-width="9" stroke-linecap="round"/>
|
||||
<circle cx="64" cy="64" r="27" stroke="#F5222D" stroke-width="9"/>
|
||||
<path d="M64 20V108" stroke="#F5222D" stroke-width="8" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 531 B |
|
After Width: | Height: | Size: 930 KiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 462 KiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 469 B |
|
After Width: | Height: | Size: 414 B |
|
After Width: | Height: | Size: 448 B |
|
After Width: | Height: | Size: 447 B |
|
After Width: | Height: | Size: 722 B |
|
After Width: | Height: | Size: 1021 KiB |
@@ -0,0 +1,8 @@
|
||||
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="24" cy="24" r="22" fill="#EAF3FF"/>
|
||||
<rect x="15" y="11" width="18" height="17" rx="4" fill="#2F7CF6"/>
|
||||
<path d="M12 24C10.8954 24 10 24.8954 10 26V32C10 33.1046 10.8954 34 12 34H36C37.1046 34 38 33.1046 38 32V26C38 24.8954 37.1046 24 36 24C34.8954 24 34 24.8954 34 26V29H14V26C14 24.8954 13.1046 24 12 24Z" fill="#1769E8"/>
|
||||
<rect x="14" y="28" width="20" height="7" rx="2.5" fill="#2F7CF6"/>
|
||||
<path d="M16 34H20L19 38H15L16 34Z" fill="#1769E8"/>
|
||||
<path d="M28 34H32L33 38H29L28 34Z" fill="#1769E8"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 637 B |
|
After Width: | Height: | Size: 986 KiB |
|
After Width: | Height: | Size: 528 KiB |
|
After Width: | Height: | Size: 1.6 MiB |
|
After Width: | Height: | Size: 1.6 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 257 KiB |
@@ -124,11 +124,71 @@ body {
|
||||
}
|
||||
|
||||
.custom-list-loading {
|
||||
background-color: unset;
|
||||
min-width: 132px;
|
||||
padding: 15px 12px 12px;
|
||||
border-radius: 14px;
|
||||
color: #666;
|
||||
background-color: rgba(255, 255, 255, 0.98);
|
||||
box-shadow: 0 6px 22px rgba(128, 72, 72, 0.08);
|
||||
}
|
||||
|
||||
.custom-list-loading .van-icon {
|
||||
font-size: 108px;
|
||||
position: relative;
|
||||
width: 88px;
|
||||
height: 88px;
|
||||
overflow: visible;
|
||||
font-size: 88px;
|
||||
}
|
||||
|
||||
.custom-list-loading .van-icon::before {
|
||||
position: absolute;
|
||||
inset: 6px;
|
||||
border: 1px solid rgba(255, 112, 103, 0.24);
|
||||
border-radius: 50%;
|
||||
content: "";
|
||||
}
|
||||
|
||||
.custom-list-loading .van-icon::after {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 41px;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: #ff8c83;
|
||||
box-shadow: 33px 37px 0 #ff6f68, -35px 21px 0 #ffb0a9;
|
||||
content: "";
|
||||
transform-origin: 3px 42px;
|
||||
animation: smart-union-loading-orbit 1.8s linear infinite;
|
||||
}
|
||||
|
||||
.custom-list-loading .van-icon__image {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
box-sizing: border-box;
|
||||
width: 54px;
|
||||
height: 54px;
|
||||
padding: 5px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 238, 236, 0.92);
|
||||
object-fit: contain;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
@keyframes smart-union-loading-orbit {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.custom-list-loading .van-toast__text {
|
||||
margin-top: 4px;
|
||||
color: #707070;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
line-height: 18px;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
.list-card {
|
||||
@@ -308,3 +368,11 @@ body {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
|
||||
/* 西财独有的幸福工荟与立案分派页面沿用迁移后的卡片间距和手机安全区。 */
|
||||
.swufe-mobile-page { min-height:100vh; background:#f3f7fd; padding-bottom:calc(20px + env(safe-area-inset-bottom)); }
|
||||
.swufe-mobile-page .van-search { margin:10px 12px; padding:0; border-radius:12px; overflow:hidden; }
|
||||
.swufe-mobile-page .van-search__content { background:#fff; }
|
||||
.swufe-mobile-page .van-nav-bar .van-icon, .swufe-mobile-page .van-nav-bar__text { color:#1989fa; }
|
||||
.swufe-mobile-page .van-cell-group { border-radius:12px; overflow:hidden; }
|
||||
.swufe-mobile-page img { max-width:100%; }
|
||||
|
||||
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 2.0 MiB |
@@ -8,31 +8,27 @@
|
||||
)
|
||||
})(navigator.userAgent || navigator.vendor || window.opera)
|
||||
|
||||
// jQuery 回调的 this 不是 Vue 实例,使用项目已有的 Element 消息入口。
|
||||
// 每个请求最多提示一次;保留响应正文,页面 then/always 继续执行并关闭 loading。
|
||||
$.ajaxSetup({
|
||||
contentType: "application/x-www-form-urlencoded;charset=utf-8",
|
||||
success: function (response, textStatus, jqXHR) {
|
||||
if (response.code !== 0) {
|
||||
this.$message.error(response.msg)
|
||||
} else {
|
||||
return response
|
||||
success: (response, textStatus, jqXHR) => {
|
||||
if (response && typeof response === "object" && response.code !== undefined && response.code !== 0) {
|
||||
ELEMENT.Message.error(response.msg || "请求处理失败,请稍后重试");
|
||||
jqXHR.platformErrorShown = true;
|
||||
}
|
||||
},
|
||||
error: function (v1) {
|
||||
this.$message.error(v1.status + " " + v1.statusText)
|
||||
error: (xhr) => {
|
||||
const message = xhr.responseJSON && xhr.responseJSON.msg;
|
||||
ELEMENT.Message.error(message || (xhr.status === 0 ? "网络连接失败或请求超时,请稍后重试" : "请求失败,状态码:" + xhr.status));
|
||||
xhr.platformErrorShown = true;
|
||||
},
|
||||
complete: function (XMLHttpRequest, textStatus) {
|
||||
var sessionstatus = XMLHttpRequest.getResponseHeader("loginStatus") //通过XMLHttpRequest取得响应头,sessionstatus,
|
||||
if (sessionstatus == "accessDenied") {
|
||||
this.$message({
|
||||
message: "\u767b\u5f55\u5931\u6548\uff0c\u8bf7\u5237\u65b0\u9875\u9762\u91cd\u65b0\u767b\u5f55",
|
||||
type: "error"
|
||||
})
|
||||
}
|
||||
if (sessionstatus == "unauthorized") {
|
||||
this.$message({
|
||||
message: "\u6ca1\u6709\u6743\u9650",
|
||||
type: "error"
|
||||
})
|
||||
complete: (xhr) => {
|
||||
const sessionStatus = xhr.getResponseHeader("loginStatus");
|
||||
if (!xhr.platformErrorShown && sessionStatus === "accessDenied") {
|
||||
ELEMENT.Message.error("登录失效,请刷新页面重新登录");
|
||||
} else if (!xhr.platformErrorShown && sessionStatus === "unauthorized") {
|
||||
ELEMENT.Message.error("没有权限");
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -69,13 +69,17 @@ 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) => {
|
||||
this.tableLoading = false
|
||||
this.$set(this, "tableLoading", true)
|
||||
// 业务错误会由公共拦截器 reject,同样需要 finally 解除列表加载状态。
|
||||
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
|
||||
this.$set(this, "tableData", res.data.list)
|
||||
this.$set(this.pageForm, "totalCount", res.data.totalCount)
|
||||
}
|
||||
}).catch(() => {
|
||||
// 中文错误已由统一响应拦截器提示,避免页面重复弹出。
|
||||
}).finally(() => {
|
||||
this.$set(this, "tableLoading", false)
|
||||
})
|
||||
},
|
||||
notifySuccess(msg) {
|
||||
|
||||
@@ -8,6 +8,8 @@ const commonUtil = {
|
||||
"x-requested-with": "XMLHttpRequest"
|
||||
}
|
||||
})
|
||||
// H5 布局提供加载状态适配;PC 布局未提供此方法时不改变其请求行为。
|
||||
if (window.bindH5AxiosLoading) window.bindH5AxiosLoading(axiosService)
|
||||
//axios拦截器
|
||||
axiosService.interceptors.response.use(
|
||||
(response) => {
|
||||
|
||||
@@ -1,79 +1,197 @@
|
||||
/**
|
||||
* H5 弹框浏览器历史栈管理器。
|
||||
* 页面只注册关闭回调;本文件全局统一监听 popstate,避免每个页面重复绑定返回事件。
|
||||
* H5 弹框统一历史栈。两种页面 API 和 Vant 弹框适配共用唯一的 popstate 监听。
|
||||
* 历史状态保留 PJAX 页面信息;关闭弹框只消费本页增加的记录,不触发业务页面返回。
|
||||
*/
|
||||
;((global) => {
|
||||
if (global.h5PopupHistory && global.h5PopupHistory.unified) return
|
||||
const callbacks = new Map()
|
||||
const popupStack = []
|
||||
let tokenSeed = 0
|
||||
let stateSeed = 0
|
||||
let pendingClear = null
|
||||
|
||||
const invokeClose = (entry) => {
|
||||
const entries = []
|
||||
const subscribers = new Set()
|
||||
const pageInstances = new Set()
|
||||
const sessionId = Date.now().toString(36)
|
||||
let sequence = 0
|
||||
let pending = null
|
||||
let notifying = false
|
||||
let pageKey = ""
|
||||
let pageCallback = null
|
||||
const names = () => entries.map((entry) => entry.popupKey)
|
||||
const notify = () => {
|
||||
notifying = true
|
||||
try {
|
||||
if (pageCallback) pageCallback(names())
|
||||
subscribers.forEach((callback) => callback(names()))
|
||||
} finally { notifying = false }
|
||||
}
|
||||
/** entry 为栈记录;同步业务回调与 Vant input 事件,不再创建或回退历史。 */
|
||||
const dismiss = (entry) => {
|
||||
const callback = callbacks.get(entry.token)
|
||||
if (callback) callback(entry.popupKey)
|
||||
if (entry.component && !entry.component._isDestroyed) {
|
||||
const component = entry.component
|
||||
if ((component.$options.name || "").replace(/-/g, "").toLowerCase() === "vandialog") {
|
||||
// 返回键等同取消确认,结束 Dialog.confirm 的 Promise,避免后续操作一直等待。
|
||||
component.$emit("cancel")
|
||||
component.onClose("cancel")
|
||||
} else component.$emit("input", false)
|
||||
}
|
||||
}
|
||||
/** token 为注册标识,popupKey 为弹框名称;返回栈记录,重复打开同一弹框时复用记录。 */
|
||||
const open = (token, popupKey, component = null) => {
|
||||
const existing = entries.find((entry) => entry.token === token && entry.popupKey === popupKey)
|
||||
if (existing) return existing
|
||||
if (pending) {
|
||||
pending.promise.then(() => {
|
||||
if (!component || (!component._isDestroyed && component.value)) open(token, popupKey, component)
|
||||
})
|
||||
return null
|
||||
}
|
||||
const entry = {id: ++sequence, token: token, popupKey: popupKey, component: component, claimable: !component}
|
||||
entries.push(entry)
|
||||
global.history.pushState(Object.assign({}, global.history.state || {}, {
|
||||
__h5PopupHistory: {session: sessionId, ids: entries.map((item) => item.id)}
|
||||
}), global.document.title, global.location.href)
|
||||
notify()
|
||||
// 命名弹层触发本轮 Vue 渲染后不再允许其他弹框占用其记录。
|
||||
if (global.Vue) global.Vue.nextTick(() => { entry.claimable = false })
|
||||
return entry
|
||||
}
|
||||
/** index 为需要关闭的最外层弹框位置;Promise<void> 在浏览器完成回退后才结束。 */
|
||||
const closeFrom = (index) => {
|
||||
if (index < 0 || index >= entries.length) return Promise.resolve()
|
||||
if (pending) {
|
||||
// 重复关闭只追踪本次请求时的弹框,避免异步回退结束后误关新打开的弹框。
|
||||
const closingIds = entries.slice(index).map((entry) => entry.id)
|
||||
return pending.promise.then(() => closeFrom(entries.findIndex((entry) => closingIds.includes(entry.id))))
|
||||
}
|
||||
let resolveClose
|
||||
const promise = new Promise((resolve) => { resolveClose = resolve })
|
||||
pending = {promise: promise, resolve: resolveClose}
|
||||
global.history.go(-(entries.length - index))
|
||||
return promise
|
||||
}
|
||||
|
||||
global.addEventListener("popstate", (event) => {
|
||||
if (pendingClear) {
|
||||
event.stopImmediatePropagation()
|
||||
const clear = pendingClear
|
||||
pendingClear = null
|
||||
clear.resolve()
|
||||
const marker = event.state && event.state.__h5PopupHistory
|
||||
if (!entries.length && !pending) {
|
||||
// 已关闭的弹框不因浏览器前进而重新显示,也不把旧弹框记录交给 PJAX。
|
||||
if (marker && marker.session === sessionId) event.stopImmediatePropagation()
|
||||
return
|
||||
}
|
||||
if (!popupStack.length) return
|
||||
|
||||
// 有弹框历史时消费本次返回事件,阻止 PJAX 将页面直接切回上一个业务页面。
|
||||
event.stopImmediatePropagation()
|
||||
const entry = popupStack.pop()
|
||||
invokeClose(entry)
|
||||
const targetIds = marker && marker.session === sessionId ? marker.ids : []
|
||||
const removed = []
|
||||
while (entries.length && !targetIds.includes(entries[entries.length - 1].id)) removed.push(entries.pop())
|
||||
notifying = true
|
||||
try { removed.forEach(dismiss) } finally { notifying = false }
|
||||
notify()
|
||||
const finished = pending
|
||||
pending = null
|
||||
if (finished) finished.resolve()
|
||||
}, true)
|
||||
|
||||
global.h5PopupHistory = {
|
||||
unified: true,
|
||||
register(closeCallback) {
|
||||
tokenSeed += 1
|
||||
const token = "h5-popup-page-" + tokenSeed
|
||||
const token = "h5-popup-" + (++sequence)
|
||||
callbacks.set(token, closeCallback)
|
||||
return token
|
||||
},
|
||||
unregister(token) {
|
||||
callbacks.delete(token)
|
||||
for (let index = popupStack.length - 1; index >= 0; index -= 1) {
|
||||
if (popupStack[index].token === token) popupStack.splice(index, 1)
|
||||
}
|
||||
},
|
||||
open(token, popupKey) {
|
||||
if (!callbacks.has(token)) return
|
||||
const current = popupStack[popupStack.length - 1]
|
||||
if (current && current.token === token && current.popupKey === popupKey) return
|
||||
|
||||
stateSeed += 1
|
||||
const entry = {token:token,popupKey:popupKey,stateId:stateSeed}
|
||||
popupStack.push(entry)
|
||||
const state = Object.assign({}, global.history.state || {}, {
|
||||
__h5PopupHistory: {token:token,popupKey:popupKey,stateId:stateSeed}
|
||||
})
|
||||
global.history.pushState(state, global.document.title, global.location.href)
|
||||
},
|
||||
unregister(token) { this.clear(token).then(() => callbacks.delete(token)) },
|
||||
open(token, popupKey) { if (callbacks.has(token)) open(token, popupKey) },
|
||||
close(token, popupKey) {
|
||||
const current = popupStack[popupStack.length - 1]
|
||||
if (!current || current.token !== token || current.popupKey !== popupKey) return false
|
||||
global.history.back()
|
||||
if (notifying) return false
|
||||
const index = entries.findIndex((entry) => entry.token === token && entry.popupKey === popupKey)
|
||||
if (index < 0) return false
|
||||
closeFrom(index)
|
||||
return true
|
||||
},
|
||||
clear(token) {
|
||||
const entries = popupStack.filter((entry) => entry.token === token)
|
||||
if (!entries.length) return Promise.resolve()
|
||||
|
||||
entries.forEach((entry) => invokeClose(entry))
|
||||
for (let index = popupStack.length - 1; index >= 0; index -= 1) {
|
||||
if (popupStack[index].token === token) popupStack.splice(index, 1)
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
pendingClear = {resolve:resolve}
|
||||
global.history.go(-entries.length)
|
||||
})
|
||||
clear(token) { return closeFrom(entries.findIndex((entry) => entry.token === token)) },
|
||||
clearAll() { return closeFrom(0) },
|
||||
hasOpen() { return entries.length > 0 || !!pending },
|
||||
// PJAX 替换 DOM 前销毁旧页面实例,使业务组件的 beforeDestroy 能正常注销监听。
|
||||
destroyPage() {
|
||||
pageInstances.forEach((instance) => { if (!instance._isDestroyed) instance.$destroy() })
|
||||
pageInstances.clear()
|
||||
}
|
||||
}
|
||||
// 命名弹层 API 适配到同一历史栈,不另行监听 popstate。
|
||||
global.h5HistoryLayerManager = {
|
||||
get stack() { return names() },
|
||||
get pageKey() { return pageKey },
|
||||
register(key, onChange) {
|
||||
pageKey = key
|
||||
pageCallback = typeof onChange === "function" ? onChange : null
|
||||
notify()
|
||||
},
|
||||
ensureRegistered(key) { if (!pageKey) pageKey = key; return true },
|
||||
subscribe(callback) {
|
||||
subscribers.add(callback)
|
||||
callback(names())
|
||||
return () => subscribers.delete(callback)
|
||||
},
|
||||
unregister(key) { if (!key || key === pageKey) this.reset() },
|
||||
open(name) { if (!name) return false; open("named-layer", name); return true },
|
||||
close(name, afterClose = null) {
|
||||
if (notifying) return false
|
||||
const index = name ? entries.findIndex((entry) => entry.popupKey === name) : entries.length - 1
|
||||
if (index < 0) { if (afterClose) afterClose(); return false }
|
||||
closeFrom(index).then(() => { if (afterClose) afterClose() })
|
||||
return true
|
||||
},
|
||||
closeAll(afterClose = null) {
|
||||
const hadEntries = entries.length > 0
|
||||
closeFrom(0).then(() => { if (afterClose) afterClose() })
|
||||
return hadEntries
|
||||
},
|
||||
/** url 为业务页面地址,data 为可选 PJAX 参数;返回 true 表示已接管导航。 */
|
||||
closeAllAndNavigate(url, data = null) {
|
||||
if (!url) return false
|
||||
closeFrom(0).then(() => global.pjaxReplace(url, data))
|
||||
return true
|
||||
},
|
||||
reset() {
|
||||
pageCallback = null
|
||||
pageKey = ""
|
||||
subscribers.clear()
|
||||
return closeFrom(0)
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Vant Popup/Dialog 统一适配已有 v-model 页面,包括 ActionSheet 内部的 Popup。
|
||||
* 已登记弹层复用记录;未登记弹层自动注册,组件销毁时注销 watcher 和回调。
|
||||
*/
|
||||
if (global.Vue) global.Vue.mixin({
|
||||
beforeCreate() {
|
||||
const props = this.$options.props || {}
|
||||
if (props.closeOnPopstate) {
|
||||
// 禁用 Vant 的组件级返回监听,由统一栈按后进先出顺序处理。
|
||||
this.$options.propsData = Object.assign({}, this.$options.propsData, {closeOnPopstate: false})
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (this.$el && this.$el.id === "app" && this.$root === this) pageInstances.add(this)
|
||||
const name = (this.$options.name || "").toLowerCase().replace(/-/g, "")
|
||||
if (name !== "vanpopup" && name !== "vandialog") return
|
||||
this._h5PopupToken = global.h5PopupHistory.register(() => this.$emit("input", false))
|
||||
const sync = (visible) => {
|
||||
if (notifying) return
|
||||
if (visible) {
|
||||
const top = entries[entries.length - 1]
|
||||
if (top && !top.component && top.claimable) top.component = this
|
||||
else open(this._h5PopupToken, "vant-popup-" + this._uid, this)
|
||||
} else {
|
||||
const index = entries.findIndex((entry) => entry.component === this)
|
||||
if (index >= 0) closeFrom(index)
|
||||
}
|
||||
}
|
||||
this._h5PopupUnwatch = this.$watch("value", sync, {immediate: true})
|
||||
},
|
||||
beforeDestroy() {
|
||||
pageInstances.delete(this)
|
||||
if (this._h5PopupUnwatch) this._h5PopupUnwatch()
|
||||
if (this._h5PopupToken) {
|
||||
const index = entries.findIndex((entry) => entry.component === this)
|
||||
if (index >= 0) closeFrom(index)
|
||||
global.h5PopupHistory.unregister(this._h5PopupToken)
|
||||
}
|
||||
}
|
||||
})
|
||||
})(window)
|
||||
|
||||
@@ -0,0 +1,634 @@
|
||||
<template>
|
||||
<div>
|
||||
<van-action-sheet
|
||||
v-model="visible"
|
||||
class="activity-schedule-calendar"
|
||||
:style="{ height: '78%' }"
|
||||
:close-on-click-overlay="true"
|
||||
title="活动日历"
|
||||
closeable>
|
||||
<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)">
|
||||
<van-icon name="arrow-left"></van-icon>
|
||||
</button>
|
||||
<div class="activity-schedule-calendar__month-title">{{ currentYear }}年{{ currentMonth }}月</div>
|
||||
<button type="button" class="activity-schedule-calendar__month-btn" @click="changeMonth(1)">
|
||||
<van-icon name="arrow"></van-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="activity-schedule-calendar__weekdays">
|
||||
<span v-for="item in weekdays" :key="item">{{ item }}</span>
|
||||
</div>
|
||||
<div class="activity-schedule-calendar__days">
|
||||
<button
|
||||
v-for="day in calendarDays"
|
||||
:key="day.key"
|
||||
type="button"
|
||||
class="activity-schedule-calendar__day"
|
||||
:class="{
|
||||
'is-other-month': !day.currentMonth,
|
||||
'is-selected': day.key === selectedDate,
|
||||
'is-today': day.isToday
|
||||
}"
|
||||
@click="selectDay(day)">
|
||||
<span>
|
||||
<span class="activity-schedule-calendar__day-number">{{ day.day }}</span>
|
||||
<small v-if="day.isToday" class="activity-schedule-calendar__today-label">今</small>
|
||||
</span>
|
||||
<i v-if="day.status" class="activity-schedule-calendar__day-status" :class="'is-' + day.status"></i>
|
||||
</button>
|
||||
</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>
|
||||
</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 + ' 的活动时段' : '活动时段' }}
|
||||
</div>
|
||||
<div v-if="selectedTimes.length" class="activity-schedule-calendar__schedule-list">
|
||||
<div v-for="item in selectedTimes" :key="item.id" class="activity-schedule-calendar__schedule-item">
|
||||
<div class="activity-schedule-calendar__schedule-main">
|
||||
<van-icon :name="item.isAttend === true ? 'checked' : 'clock-o'"></van-icon>
|
||||
<div class="activity-schedule-calendar__schedule-copy">
|
||||
<div class="activity-schedule-calendar__schedule-time">
|
||||
{{ formatTime(item.courseStartTime) }} - {{ formatTime(item.courseEndTime) }}
|
||||
</div>
|
||||
<div v-if="row && row.courseLocation" class="activity-schedule-calendar__schedule-location">
|
||||
<van-icon name="location-o"></van-icon>
|
||||
<span>{{ row.courseLocation }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span v-if="item.isAttend === true" class="activity-schedule-calendar__status is-success">已签到</span>
|
||||
<button
|
||||
v-else-if="canSignItem(item)"
|
||||
type="button"
|
||||
class="activity-schedule-calendar__sign-btn"
|
||||
@click.stop="onSign(item)">签到</button>
|
||||
<span v-else class="activity-schedule-calendar__status" :class="{
|
||||
'is-ended': getScheduleStatus(item) === '已结束',
|
||||
'is-progressing': getScheduleStatus(item) === '进行中'
|
||||
}">
|
||||
{{ getScheduleStatus(item) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="activity-schedule-calendar__empty">
|
||||
<img src="/assets/mobile/img/activity/schedule-empty.png?v=20260821-transparent" alt="">
|
||||
<div class="activity-schedule-calendar__empty-title">当天暂无签到时段</div>
|
||||
<div class="activity-schedule-calendar__empty-desc">当前日期没有需要签到的活动安排</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</van-action-sheet>
|
||||
|
||||
<van-popup
|
||||
v-model="signVisible"
|
||||
round
|
||||
closeable
|
||||
:safe-area-inset-bottom="true"
|
||||
:close-on-click-overlay="false"
|
||||
:style="{ width: '80%', height: '66%' }"
|
||||
get-container="#app"
|
||||
@close="onSignClose">
|
||||
<scan-code ref="scanCodeRef"></scan-code>
|
||||
</van-popup>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
module.exports = {
|
||||
name: "ActivityScheduleCalendar",
|
||||
props: {
|
||||
moduleType: {
|
||||
type: String,
|
||||
default: "brand"
|
||||
}
|
||||
},
|
||||
components: {
|
||||
"scan-code": httpVueLoader("/components/plugins/sysScanCode/index.vue?v=" + new Date().getTime())
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
visible: false,
|
||||
row: null,
|
||||
currentYear: new Date().getFullYear(),
|
||||
currentMonth: new Date().getMonth() + 1,
|
||||
selectedDate: "",
|
||||
weekdays: ["日", "一", "二", "三", "四", "五", "六"],
|
||||
selectCourseTime: null,
|
||||
signVisible: false,
|
||||
markerLayer: null,
|
||||
map: null,
|
||||
circle: null,
|
||||
mapContainerId: "activityScheduleMap_" + Math.random().toString(36).slice(2)
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
courseTimes() {
|
||||
return this.row && Array.isArray(this.row.courseTimes) ? this.row.courseTimes : []
|
||||
},
|
||||
scheduleDateMap() {
|
||||
const map = {}
|
||||
this.courseTimes.forEach(item => {
|
||||
const key = this.dateKey(item)
|
||||
if (!key) return
|
||||
if (!map[key]) map[key] = []
|
||||
map[key].push(item)
|
||||
})
|
||||
return map
|
||||
},
|
||||
selectedTimes() {
|
||||
return this.scheduleDateMap[this.selectedDate] || []
|
||||
},
|
||||
canSign() {
|
||||
return !!(this.row && this.row.isSign === true && this.row.isMobileSign === true)
|
||||
},
|
||||
showMap() {
|
||||
return !!(this.moduleType === "brand" && this.row && this.row.isMobileSign === true && this.row.signType === 3)
|
||||
},
|
||||
calendarDays() {
|
||||
const firstDay = new Date(this.currentYear, this.currentMonth - 1, 1)
|
||||
const startDate = new Date(this.currentYear, this.currentMonth - 1, 1 - firstDay.getDay())
|
||||
const todayKey = this.formatDateKey(new Date())
|
||||
return Array.from({ length: 42 }, (_, index) => {
|
||||
const date = new Date(startDate.getFullYear(), startDate.getMonth(), startDate.getDate() + index)
|
||||
const key = this.formatDateKey(date)
|
||||
return {
|
||||
key,
|
||||
day: date.getDate(),
|
||||
currentMonth: date.getFullYear() === this.currentYear && date.getMonth() + 1 === this.currentMonth,
|
||||
isToday: key === todayKey,
|
||||
status: this.getDayStatus(this.scheduleDateMap[key] || [])
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onOpen(row) {
|
||||
this.row = row
|
||||
const initialDate = this.formatDateKey(new Date())
|
||||
const date = this.$moment(initialDate)
|
||||
this.currentYear = date.year()
|
||||
this.currentMonth = date.month() + 1
|
||||
this.selectedDate = initialDate
|
||||
this.visible = true
|
||||
this.$nextTick(() => {
|
||||
if (this.showMap) this.initMap()
|
||||
})
|
||||
},
|
||||
dateKey(item) {
|
||||
const value = item && (item.courseDate || item.courseStartTime)
|
||||
return value ? this.$moment(value).format("YYYY-MM-DD") : ""
|
||||
},
|
||||
formatDateKey(date) {
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, "0")
|
||||
const day = String(date.getDate()).padStart(2, "0")
|
||||
return year + "-" + month + "-" + day
|
||||
},
|
||||
formatTime(value) {
|
||||
return value ? this.$moment(value).format("HH:mm") : "--"
|
||||
},
|
||||
canSignItem(item) {
|
||||
if (!this.canSign || !item || !item.courseStartTime || !item.courseEndTime) return false
|
||||
const now = this.$moment()
|
||||
return !now.isBefore(this.$moment(item.courseStartTime)) && !now.isAfter(this.$moment(item.courseEndTime))
|
||||
},
|
||||
getScheduleStatus(item) {
|
||||
if (!item || !item.courseStartTime || !item.courseEndTime) return "待开始"
|
||||
const now = this.$moment()
|
||||
if (now.isBefore(this.$moment(item.courseStartTime))) return "待开始"
|
||||
if (now.isAfter(this.$moment(item.courseEndTime))) return "已结束"
|
||||
return "进行中"
|
||||
},
|
||||
getDayStatus(items) {
|
||||
if (items.some(item => item.isAttend === true)) return "signed"
|
||||
if (items.some(item => this.canSignItem(item))) return "available"
|
||||
if (items.some(item => item.courseStartTime && this.$moment().isBefore(this.$moment(item.courseStartTime)))) return "pending"
|
||||
return ""
|
||||
},
|
||||
changeMonth(offset) {
|
||||
const date = new Date(this.currentYear, this.currentMonth - 1 + offset, 1)
|
||||
this.currentYear = date.getFullYear()
|
||||
this.currentMonth = date.getMonth() + 1
|
||||
},
|
||||
selectDay(day) {
|
||||
this.selectedDate = day.key
|
||||
if (!day.currentMonth) {
|
||||
const date = this.$moment(day.key)
|
||||
this.currentYear = date.year()
|
||||
this.currentMonth = date.month() + 1
|
||||
}
|
||||
},
|
||||
onSign(courseTime) {
|
||||
this.selectCourseTime = courseTime
|
||||
if (this.row.signType === 1) {
|
||||
this.signVisible = true
|
||||
this.$nextTick(() => this.$refs.scanCodeRef.init())
|
||||
} else if (this.row.signType === 2) {
|
||||
this.makeCode()
|
||||
} else if (this.row.signType === 3 && this.moduleType === "brand") {
|
||||
this.positionSign()
|
||||
} else {
|
||||
this.$toast("此签到模式正在升级中")
|
||||
}
|
||||
},
|
||||
onSignClose() {
|
||||
if (this.$refs.scanCodeRef) this.$refs.scanCodeRef.closeScan()
|
||||
},
|
||||
makeCode() {
|
||||
const baseUrl = this.moduleType === "family"
|
||||
? "/platform/family/mine/passiveScan"
|
||||
: "/platform/trainSignUp/mine/passiveScan"
|
||||
const content = jrQrcode.getQrBase64(baseUrl + "?id=" + this.selectCourseTime.id)
|
||||
vant.ImagePreview([content])
|
||||
},
|
||||
positionSign() {
|
||||
this.getLocation(coords => {
|
||||
if (!coords) return
|
||||
this.$axios.post("/platform/trainSignUp/mine/positionSign", {
|
||||
timeId: this.selectCourseTime.id,
|
||||
courseId: this.row.id,
|
||||
lat: coords.lat,
|
||||
lng: coords.lng
|
||||
}).then(res => {
|
||||
if (res.code === 0) {
|
||||
this.selectCourseTime.isAttend = true
|
||||
this.$toast("签到成功")
|
||||
} else {
|
||||
this.$toast(res.msg)
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
getLocation(callback) {
|
||||
if (!navigator.geolocation) {
|
||||
this.$toast("当前浏览器不支持定位功能")
|
||||
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 })
|
||||
}, error => {
|
||||
loading.close()
|
||||
const messageMap = {
|
||||
1: "请允许浏览器获取位置信息",
|
||||
2: "无法获取当前位置",
|
||||
3: "定位超时,请重试"
|
||||
}
|
||||
this.$toast(messageMap[error.code] || "定位失败,请稍后重试")
|
||||
callback(null)
|
||||
}, { enableHighAccuracy: true, timeout: 10000, maximumAge: 60000 })
|
||||
},
|
||||
initMap() {
|
||||
if (!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.map = new TMap.Map(this.mapContainerId, { zoom: 16, center })
|
||||
this.markerLayer = new TMap.MultiMarker({
|
||||
map: this.map,
|
||||
geometries: [{ id: "activity", position: center }]
|
||||
})
|
||||
this.circle = new TMap.MultiCircle({
|
||||
map: this.map,
|
||||
geometries: [{ center, radius: this.row.radius || 100 }]
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.activity-schedule-calendar {
|
||||
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__body {
|
||||
height: 100%;
|
||||
padding: 14px 14px 24px;
|
||||
overflow-y: auto;
|
||||
overflow-anchor: none;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.activity-schedule-calendar__month-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.activity-schedule-calendar__month-btn {
|
||||
display: flex;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
padding: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid #edf1f6;
|
||||
border-radius: 50%;
|
||||
color: #263548;
|
||||
background: #fff;
|
||||
box-shadow: 0 5px 14px rgba(55, 85, 126, .08);
|
||||
}
|
||||
|
||||
.activity-schedule-calendar__month-title {
|
||||
color: #1f2f43;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.activity-schedule-calendar__weekdays,
|
||||
.activity-schedule-calendar__days {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, minmax(0, 1fr));
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.activity-schedule-calendar__weekdays {
|
||||
margin-bottom: 5px;
|
||||
color: #58677b;
|
||||
font-size: 12px;
|
||||
line-height: 28px;
|
||||
}
|
||||
|
||||
.activity-schedule-calendar__day {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 38px;
|
||||
height: 48px;
|
||||
margin: 1px auto;
|
||||
padding: 0;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
border: 0;
|
||||
color: #263548;
|
||||
background: transparent;
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.activity-schedule-calendar__day > span {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.activity-schedule-calendar__day-number {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
line-height: 14px;
|
||||
}
|
||||
|
||||
.activity-schedule-calendar__today-label {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 0;
|
||||
left: 0;
|
||||
color: #1989fa;
|
||||
font-size: 10px;
|
||||
line-height: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.activity-schedule-calendar__day.is-other-month > span {
|
||||
color: #bdc5d0;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
.activity-schedule-calendar__day.is-selected .activity-schedule-calendar__today-label {
|
||||
top: 2px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.activity-schedule-calendar__day-status {
|
||||
position: absolute;
|
||||
bottom: -3px;
|
||||
left: 50%;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
transform: translateX(-50%);
|
||||
border-radius: 50%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.activity-schedule-calendar__day-status.is-signed {
|
||||
background: #1989fa;
|
||||
}
|
||||
|
||||
.activity-schedule-calendar__day-status.is-available {
|
||||
border: 1px solid #1989fa;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.activity-schedule-calendar__day-status.is-pending {
|
||||
background: #d8dde5;
|
||||
}
|
||||
|
||||
.activity-schedule-calendar__legend {
|
||||
display: flex;
|
||||
margin: 12px 4px 16px;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
color: #78869a;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.activity-schedule-calendar__legend span {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.activity-schedule-calendar__legend i {
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
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; }
|
||||
|
||||
.activity-schedule-calendar__map {
|
||||
width: 100%;
|
||||
height: 150px;
|
||||
margin-bottom: 12px;
|
||||
overflow: hidden;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.activity-schedule-calendar__schedule-card {
|
||||
overflow: hidden;
|
||||
border-radius: 12px;
|
||||
background: #fff;
|
||||
box-shadow: 0 7px 20px rgba(55, 85, 126, .07);
|
||||
}
|
||||
|
||||
.activity-schedule-calendar__schedule-title {
|
||||
padding: 12px 14px;
|
||||
border-bottom: 1px solid #edf1f6;
|
||||
color: #263548;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.activity-schedule-calendar__schedule-item {
|
||||
display: flex;
|
||||
min-height: 70px;
|
||||
padding: 12px 14px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid #edf1f6;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.activity-schedule-calendar__schedule-item:last-child { border-bottom: 0; }
|
||||
|
||||
.activity-schedule-calendar__schedule-main {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: flex-start;
|
||||
gap: 9px;
|
||||
}
|
||||
|
||||
.activity-schedule-calendar__schedule-main > .van-icon {
|
||||
margin-top: 2px;
|
||||
color: #1989fa;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.activity-schedule-calendar__schedule-time {
|
||||
color: #263548;
|
||||
font-size: 15px;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.activity-schedule-calendar__schedule-location {
|
||||
display: flex;
|
||||
margin-top: 5px;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
color: #7d899a;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.activity-schedule-calendar__status {
|
||||
flex: 0 0 auto;
|
||||
padding: 4px 8px;
|
||||
border-radius: 5px;
|
||||
color: #8b96a7;
|
||||
background: #f1f4f8;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.activity-schedule-calendar__status.is-success {
|
||||
color: #19a965;
|
||||
background: #eaf8f1;
|
||||
}
|
||||
|
||||
.activity-schedule-calendar__status.is-progressing {
|
||||
color: #ff8a00;
|
||||
background: #fff4e6;
|
||||
}
|
||||
|
||||
.activity-schedule-calendar__status.is-ended {
|
||||
color: #98a2b2;
|
||||
background: #eef1f5;
|
||||
}
|
||||
|
||||
.activity-schedule-calendar__sign-btn {
|
||||
display: inline-flex;
|
||||
min-width: 42px !important;
|
||||
height: 24px !important;
|
||||
padding: 0 8px !important;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid #1989fa !important;
|
||||
border-radius: 5px !important;
|
||||
color: #fff !important;
|
||||
background-color: #1989fa !important;
|
||||
box-shadow: none !important;
|
||||
font-size: 11px !important;
|
||||
}
|
||||
|
||||
.activity-schedule-calendar__empty {
|
||||
padding: 18px 14px 22px;
|
||||
color: #9aa5b5;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.activity-schedule-calendar__empty img {
|
||||
display: block;
|
||||
width: 142px;
|
||||
height: 94px;
|
||||
margin: 0 auto 4px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.activity-schedule-calendar__empty-title {
|
||||
color: #58677b;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.activity-schedule-calendar__empty-desc {
|
||||
margin-top: 2px;
|
||||
color: #a2adbc;
|
||||
font-size: 11px;
|
||||
line-height: 18px;
|
||||
}
|
||||
</style>
|
||||
@@ -37,26 +37,32 @@ module.exports = {
|
||||
<style scoped>
|
||||
.table-column {
|
||||
display: flex;
|
||||
padding: 6px 0;
|
||||
align-items: center;
|
||||
width: calc(50% - 6px);
|
||||
min-width: 0;
|
||||
padding: 4px 0;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
color: #98a3b3;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
flex-shrink: 0;
|
||||
max-width: 120px;
|
||||
max-width: 50%;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.value {
|
||||
color: #555;
|
||||
font-size: 14px;
|
||||
flex-grow: 1; /* 动态占据剩余部分 */
|
||||
white-space: nowrap; /* 防止换行 */
|
||||
overflow: hidden; /* 隐藏溢出的部分 */
|
||||
text-overflow: ellipsis; /* 省略号 */
|
||||
min-width: 0;
|
||||
color: #56657a;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
flex-grow: 1;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
<template>
|
||||
<van-pull-refresh v-model="tableRefreshing" @refresh="onRefresh">
|
||||
<van-pull-refresh class="table-list" v-model="tableRefreshing" @refresh="onRefresh">
|
||||
<div v-if="tableData.length === 0" class="empty-state">
|
||||
<van-empty description="暂无数据"></van-empty>
|
||||
<slot name="empty">
|
||||
<van-empty description="暂无数据"></van-empty>
|
||||
</slot>
|
||||
</div>
|
||||
<van-list v-if="tableData && tableData.length>0"
|
||||
v-model="tableLoading"
|
||||
@@ -9,7 +11,8 @@
|
||||
finished-text="没有更多了"
|
||||
@load="onLoad">
|
||||
<div class="table-list-container">
|
||||
<div v-for="(row, index) in tableData" :key="index" class="table-list-item">
|
||||
<div v-for="(row, index) in tableData" :key="index" class="table-list-item"
|
||||
:data-row-id="row && row.id ? row.id : ''">
|
||||
<slot name="header" :index="index" :row="row">
|
||||
<div class="item-header">
|
||||
<div class="item-title">{{ calcTitle(row) }}</div>
|
||||
@@ -18,12 +21,12 @@
|
||||
</div>
|
||||
</slot>
|
||||
|
||||
<div style="display: flex;column-gap: 10px"
|
||||
<div class="table-list-content"
|
||||
:style="{'max-height':img ? '100px':'unset', 'overflow': img ? 'hidden':'unset' }">
|
||||
<div class="img-container" v-if="img">
|
||||
<img :src="row[img]" alt="" style="object-fit: cover">
|
||||
<img :src="row[img]" alt="">
|
||||
</div>
|
||||
<div style="width: 100%">
|
||||
<div class="table-list-columns">
|
||||
<slot :index="index" :row="row"></slot>
|
||||
</div>
|
||||
</div>
|
||||
@@ -66,6 +69,10 @@ module.exports = {
|
||||
img: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
show_loading_overlay: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@@ -87,6 +94,33 @@ module.exports = {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* 获取当前列表快照。
|
||||
* 返回值包含已加载记录、分页参数和加载完成状态,供业务页面在进入详情前暂存列表现场。
|
||||
*/
|
||||
getListState() {
|
||||
return {
|
||||
tableData: JSON.parse(JSON.stringify(this.tableData)),
|
||||
localPageForm: JSON.parse(JSON.stringify(this.localPageForm)),
|
||||
tableFinished: this.tableFinished
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 恢复列表快照。
|
||||
* state 应包含 tableData、localPageForm 和 tableFinished;返回 true 表示恢复成功。
|
||||
*/
|
||||
restoreListState(state) {
|
||||
if (!state || !Array.isArray(state.tableData)) return false
|
||||
this.$set(this, "tableData", state.tableData)
|
||||
this.$set(this, "localPageForm", {...this.localPageForm, ...(state.localPageForm || {})})
|
||||
this.$set(this, "tableFinished", !!state.tableFinished)
|
||||
this.$set(this, "tableLoading", false)
|
||||
this.$set(this, "tableRefreshing", false)
|
||||
this.$emit("update:page_form", {...this.localPageForm})
|
||||
return true
|
||||
},
|
||||
|
||||
calcTitle(row) {
|
||||
if (!this.title) return ""
|
||||
// 处理key1.key2.key3 这样的形式
|
||||
@@ -111,23 +145,32 @@ module.exports = {
|
||||
|
||||
pageData() {
|
||||
this.tableLoading = true
|
||||
const isInitialLoad = this.tableData.length === 0
|
||||
this.$emit("loading-change", {loading: true, initial: isInitialLoad})
|
||||
|
||||
const loading = createListLoading()
|
||||
this.$axios.post(this.api, this.json ? ({pageForm: JSON.stringify(this.localPageForm)}) : this.localPageForm).then((res) => {
|
||||
const loading = this.show_loading_overlay ? createListLoading() : null
|
||||
return this.$axios.post(
|
||||
this.api,
|
||||
this.json ? ({pageForm: JSON.stringify(this.localPageForm)}) : this.localPageForm,
|
||||
{h5SkeletonLoading: !this.show_loading_overlay}
|
||||
).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.tableData = this.tableData.concat(res.data.list)
|
||||
// 接口异常返回 null 行时不渲染该行,避免业务插槽读取行字段产生渲染异常。
|
||||
const responseList = Array.isArray(res.data.list) ? res.data.list.filter((item) => item) : []
|
||||
this.tableData = this.tableData.concat(responseList)
|
||||
this.localPageForm.totalCount = res.data.totalCount
|
||||
if (this.tableData.length >= this.localPageForm.totalCount) {
|
||||
this.tableFinished = true
|
||||
}
|
||||
this.$emit("update:page_form", {...this.localPageForm})
|
||||
}
|
||||
}).catch(() => {
|
||||
// 公共请求拦截器展示错误;保留当前列表,允许用户再次下拉刷新。
|
||||
}).finally(() => {
|
||||
loading.close()
|
||||
console.log(this.tableLoading)
|
||||
if (loading) loading.close()
|
||||
this.tableLoading = false
|
||||
console.log(this.tableLoading)
|
||||
this.tableRefreshing = false
|
||||
this.$emit("loading-change", {loading: false, initial: isInitialLoad})
|
||||
})
|
||||
},
|
||||
doSearch() {
|
||||
@@ -152,33 +195,36 @@ module.exports = {
|
||||
<style scoped>
|
||||
.table-list-container {
|
||||
padding: 0;
|
||||
margin-top: 10px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.table-list-container .table-list-item {
|
||||
background-color: #fff;
|
||||
border-bottom: 1px solid #eaecef;
|
||||
padding: 16px;
|
||||
border: 1px solid #eef1f6;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 7px 20px rgba(55, 85, 126, .08);
|
||||
padding: 14px;
|
||||
position: relative;
|
||||
margin-bottom: 10px;
|
||||
margin-bottom: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.table-list-container .table-list-item:last-child {
|
||||
border-bottom: none;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.table-list-container .table-list-item .item-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 10px;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.table-list-container .table-list-item .item-title {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #1f2f3d;
|
||||
font-size: 15px;
|
||||
line-height: 22px;
|
||||
font-weight: 600;
|
||||
color: #263548;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
@@ -190,15 +236,29 @@ module.exports = {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.table-list-container .table-list-item .img-container img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 6px;
|
||||
object-fit: cover;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.table-list-container .table-list-item .table-list-content {
|
||||
display: flex;
|
||||
column-gap: 10px;
|
||||
}
|
||||
|
||||
.table-list-container .table-list-item .table-list-columns {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
flex-wrap: wrap;
|
||||
column-gap: 12px;
|
||||
}
|
||||
|
||||
.table-list-container .table-list-item .item-meta {
|
||||
display: flex;
|
||||
@@ -245,48 +305,57 @@ module.exports = {
|
||||
|
||||
.table-list-container .table-list-item .item-actions {
|
||||
display: flex;
|
||||
column-gap: 20px;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: end;
|
||||
margin-top: 15px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid #eaecef;
|
||||
margin-top: 10px;
|
||||
padding-top: 10px;
|
||||
border-top: 1px solid #edf1f6;
|
||||
}
|
||||
|
||||
.table-list-container .table-list-item .action-btn {
|
||||
display: flex;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: end;
|
||||
font-size: 14px;
|
||||
justify-content: center;
|
||||
min-height: 26px;
|
||||
padding: 4px 10px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 14px;
|
||||
background-color: #eaf4ff;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
color: var(--color-primary);
|
||||
padding: 4px 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.table-list-container .table-list-item .action-btn i {
|
||||
margin-right: 6px;
|
||||
font-size: 16px;
|
||||
margin-right: 5px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.table-list-container .table-list-item .action-btn.delete {
|
||||
color: #ff0000;
|
||||
color: #f04444;
|
||||
background-color: #fff0f0;
|
||||
}
|
||||
|
||||
.table-list-container .table-list-item .action-btn.review {
|
||||
color: #67c23a;
|
||||
background-color: #eef9e9;
|
||||
}
|
||||
|
||||
.table-list-container .empty-state {
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.table-list-container .empty-state i {
|
||||
.empty-state i {
|
||||
font-size: 60px;
|
||||
margin-bottom: 16px;
|
||||
color: #dcdee0;
|
||||
}
|
||||
|
||||
.table-list-container .empty-state p {
|
||||
.empty-state p {
|
||||
margin-top: 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
@@ -3,16 +3,16 @@
|
||||
<div class="file-preview-container" v-if="fileList && fileList.length > 0">
|
||||
<div class="file-item" v-for="item in fileList" :key="item.id">
|
||||
<img
|
||||
v-if="item.suffix === 'jpg' || item.suffix === 'png' || item.suffix === 'jpeg' || item.suffix === 'gif'"
|
||||
v-if="isImageFile(item)"
|
||||
:src="item.thumbnail"
|
||||
@click.stop="$commonUtil.previewFile(item)"
|
||||
@click.stop="onPreviewFile(item)"
|
||||
class="file-image"
|
||||
/>
|
||||
|
||||
<img
|
||||
v-else
|
||||
:src="$commonUtil.getFileItemShowIcon(item.suffix)"
|
||||
@click.stop="$commonUtil.previewFile(item)"
|
||||
@click.stop="onPreviewFile(item)"
|
||||
class="file-image"
|
||||
/>
|
||||
|
||||
@@ -57,7 +57,12 @@ module.exports = {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
fileList: []
|
||||
fileList: [],
|
||||
imagePreviewInstance: null,
|
||||
imagePreviewContainer: null,
|
||||
imagePreviewHistoryUnsubscribe: null,
|
||||
closingImagePreviewFromHistory: false,
|
||||
imagePreviewLayerName: "h5-file-image-preview"
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -65,22 +70,10 @@ module.exports = {
|
||||
let ids = []
|
||||
if (this.complete_result) {
|
||||
if (Array.isArray(val)) {
|
||||
ids = val.map((item) => {
|
||||
if (item.response.data.includes("=")) {
|
||||
return item?.response?.data.substring(item?.response?.data.lastIndexOf("=") + 1)
|
||||
} else {
|
||||
return item?.response?.data
|
||||
}
|
||||
})
|
||||
ids = val.map((item) => this.resolveFileId(item)).filter((id) => id)
|
||||
} else {
|
||||
try {
|
||||
ids = JSON.parse(val).map((item) => {
|
||||
if (item.response.data.includes("=")) {
|
||||
return item?.response?.data.substring(item?.response?.data.lastIndexOf("=") + 1)
|
||||
} else {
|
||||
return item?.response?.data
|
||||
}
|
||||
})
|
||||
ids = JSON.parse(val).map((item) => this.resolveFileId(item)).filter((id) => id)
|
||||
} catch (err) {
|
||||
this.fileList = []
|
||||
}
|
||||
@@ -100,12 +93,109 @@ module.exports = {
|
||||
this.requestFullFile(ids)
|
||||
},
|
||||
|
||||
// 新版上传组件保存 response.data,迁移的历史附件保留原文件主键 id。
|
||||
resolveFileId(item) {
|
||||
if (!item) {
|
||||
return ""
|
||||
}
|
||||
const fileValue = item.response && item.response.data ? item.response.data : item.id
|
||||
if (typeof fileValue !== "string" || !fileValue) {
|
||||
return ""
|
||||
}
|
||||
return fileValue.includes("=") ? fileValue.substring(fileValue.lastIndexOf("=") + 1) : fileValue
|
||||
},
|
||||
|
||||
requestFullFile(ids) {
|
||||
this.$axios.post("/platform/sys/file/previewFileData", {ids: JSON.stringify(ids)}).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.fileList = res.data
|
||||
this.$set(this, "fileList", res.data || [])
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 判断附件是否为图片;item 传完整文件对象,返回 boolean 类型。
|
||||
isImageFile(item) {
|
||||
const suffix = item && item.suffix ? String(item.suffix).toLowerCase() : ""
|
||||
return ["jpg", "jpeg", "png", "gif", "bmp", "webp"].includes(suffix)
|
||||
},
|
||||
|
||||
/**
|
||||
* 预览附件。
|
||||
* item 传 previewFileData 返回的文件对象,需包含 id、suffix、downloadPath;方法无返回值。
|
||||
* 图片会与当前附件列表中的其他图片组成轮播,非图片继续使用公共文件预览逻辑。
|
||||
*/
|
||||
onPreviewFile(item) {
|
||||
if (!this.isImageFile(item)) {
|
||||
this.$commonUtil.previewFile(item)
|
||||
return
|
||||
}
|
||||
if (this.imagePreviewInstance) return
|
||||
const imageFiles = this.fileList.filter((file) => this.isImageFile(file) && file.downloadPath)
|
||||
const startPosition = imageFiles.findIndex((file) => file.id === item.id)
|
||||
if (startPosition < 0) return
|
||||
const previewContainer = document.createElement("div")
|
||||
previewContainer.style.display = "none"
|
||||
imageFiles.forEach((file) => {
|
||||
const image = document.createElement("img")
|
||||
image.src = file.downloadPath
|
||||
image.alt = file.name || ""
|
||||
previewContainer.appendChild(image)
|
||||
})
|
||||
document.body.appendChild(previewContainer)
|
||||
this.$set(this, "closingImagePreviewFromHistory", false)
|
||||
this.$set(this, "imagePreviewContainer", previewContainer)
|
||||
this.$set(this, "imagePreviewInstance", new Viewer(previewContainer, {
|
||||
zIndex: 99999,
|
||||
title: false,
|
||||
initialViewIndex: startPosition,
|
||||
hidden: () => { this.handleImagePreviewHidden() }
|
||||
}))
|
||||
this.imagePreviewInstance.show()
|
||||
window.h5HistoryLayerManager.open(this.imagePreviewLayerName)
|
||||
},
|
||||
|
||||
// Viewer 完全关闭后销毁实例和临时图片容器,并按关闭来源同步浏览器历史。
|
||||
handleImagePreviewHidden() {
|
||||
const closedFromHistory = this.closingImagePreviewFromHistory
|
||||
this.cleanupImagePreview()
|
||||
if (closedFromHistory) {
|
||||
this.$set(this, "closingImagePreviewFromHistory", false)
|
||||
return
|
||||
}
|
||||
if (window.h5HistoryLayerManager.stack.includes(this.imagePreviewLayerName)) {
|
||||
window.h5HistoryLayerManager.close(this.imagePreviewLayerName)
|
||||
}
|
||||
},
|
||||
|
||||
// 清理 Viewer 实例及其临时 DOM;方法无参数、无返回值。
|
||||
cleanupImagePreview() {
|
||||
const previewInstance = this.imagePreviewInstance
|
||||
const previewContainer = this.imagePreviewContainer
|
||||
this.$set(this, "imagePreviewInstance", null)
|
||||
this.$set(this, "imagePreviewContainer", null)
|
||||
if (previewInstance) previewInstance.destroy()
|
||||
if (previewContainer && previewContainer.parentNode) {
|
||||
previewContainer.parentNode.removeChild(previewContainer)
|
||||
}
|
||||
},
|
||||
|
||||
// 浏览器返回导致图片预览层出栈时,仅关闭预览实例,不再重复回退历史记录。
|
||||
syncImagePreviewHistory(stack) {
|
||||
if (!this.imagePreviewInstance || stack.includes(this.imagePreviewLayerName)) return
|
||||
this.$set(this, "closingImagePreviewFromHistory", true)
|
||||
this.imagePreviewInstance.hide()
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.$set(this, "imagePreviewHistoryUnsubscribe", window.h5HistoryLayerManager.subscribe((stack) => {
|
||||
this.syncImagePreviewHistory(stack)
|
||||
}))
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.imagePreviewHistoryUnsubscribe) this.imagePreviewHistoryUnsubscribe()
|
||||
if (this.imagePreviewInstance || this.imagePreviewContainer) {
|
||||
this.$set(this, "closingImagePreviewFromHistory", true)
|
||||
this.cleanupImagePreview()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,28 @@
|
||||
<template>
|
||||
<div class="h5-signature">
|
||||
<div v-if="!showSignaturePanel" class="main-panel">
|
||||
<slot v-if="$slots.default"></slot>
|
||||
<template v-else>
|
||||
<van-button @click="openSignature" class="open-button" size="middle" plain native-type="button">打开签字板</van-button>
|
||||
<van-image :src="signatureContent"></van-image>
|
||||
</template>
|
||||
<slot v-if="$slots.default"></slot>
|
||||
<div v-else class="signature-entry">
|
||||
<!-- 未签名时展示统一空状态和入口,组件放入 van-field 时仍保持完整宽度。 -->
|
||||
<div v-if="!signatureContent" class="signature-entry-panel">
|
||||
<van-icon class="signature-placeholder-icon" name="edit"></van-icon>
|
||||
<div class="signature-placeholder-text">请完成本人手写签名</div>
|
||||
<van-button @click="openSignature" class="signature-open-button" block plain
|
||||
native-type="button">进入签字板</van-button>
|
||||
</div>
|
||||
|
||||
<!-- 已签名时直接预览结果,并在下方提供统一的重新签名入口。 -->
|
||||
<div v-else class="signature-entry-panel signature-entry-panel--completed">
|
||||
<van-image :src="signatureContent" class="signature-preview" fit="contain"></van-image>
|
||||
<div class="signature-completed-text">
|
||||
<van-icon name="passed"></van-icon>
|
||||
<span>已完成本人手写签名</span>
|
||||
</div>
|
||||
</div>
|
||||
<button v-if="signatureContent" @click="openSignature" class="signature-reset-button"
|
||||
type="button">重新签名</button>
|
||||
</div>
|
||||
<van-popup v-model="showSignaturePanel" :style="{ height: '100vh', width: '100vw' }">
|
||||
<van-popup v-model="showSignaturePanel" :close-on-click-overlay="false"
|
||||
:style="{ height: '100vh', width: '100vw' }" @close="handleSignaturePopupClose">
|
||||
<signature @save="save"></signature>
|
||||
</van-popup>
|
||||
</div>
|
||||
@@ -36,14 +51,43 @@ module.exports = {
|
||||
data() {
|
||||
return {
|
||||
signatureContent: null,
|
||||
showSignaturePanel: false
|
||||
|
||||
|
||||
showSignaturePanel: false,
|
||||
historyLayerName: "",
|
||||
historyLayerUnsubscribe: null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
||||
|
||||
/**
|
||||
* 打开签字板。
|
||||
* 无请求参数;优先向全局历史栈增加当前签字弹层,返回值为空。
|
||||
*/
|
||||
openSignature() {
|
||||
const manager = window.h5HistoryLayerManager
|
||||
if (manager && manager.open(this.historyLayerName)) return
|
||||
this.showSignaturePanel = true
|
||||
},
|
||||
|
||||
// 保存成功或业务主动关闭时同步回退签字弹层历史,管理器不可用时直接关闭 Popup。
|
||||
closeSignaturePanel() {
|
||||
const manager = window.h5HistoryLayerManager
|
||||
if (manager && manager.close(this.historyLayerName)) return
|
||||
this.showSignaturePanel = false
|
||||
},
|
||||
|
||||
// Vant 组件自行关闭时仅处理仍位于栈顶的签字层,避免返回同步触发重复 history.back。
|
||||
handleSignaturePopupClose() {
|
||||
const manager = window.h5HistoryLayerManager
|
||||
if (!manager) return
|
||||
const stack = manager.stack
|
||||
if (stack[stack.length - 1] === this.historyLayerName) {
|
||||
manager.close(this.historyLayerName)
|
||||
}
|
||||
},
|
||||
|
||||
save(signature) {
|
||||
const file = this.base64ToFile(signature, "signature.png")
|
||||
const formData = new FormData()
|
||||
@@ -59,11 +103,20 @@ module.exports = {
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.code === 0) {
|
||||
alert("保存成功")
|
||||
this.showSignaturePanel = false
|
||||
// 保存成功后使用项目统一的 Vant 反馈,避免浏览器原生弹框阻塞签字流程。
|
||||
this.$toast.success({
|
||||
message: "签名保存成功",
|
||||
duration: 1500,
|
||||
forbidClick: true
|
||||
})
|
||||
this.$emit("input", res.data)
|
||||
this.closeSignaturePanel()
|
||||
} else {
|
||||
alert("保存失败")
|
||||
// 接口返回失败时保留签字板,方便用户重试。
|
||||
this.$toast.fail({
|
||||
message: res.msg || "签名保存失败,请重试",
|
||||
duration: 2000
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
@@ -86,27 +139,114 @@ module.exports = {
|
||||
return new File([blob], filename, { type: contentType })
|
||||
}
|
||||
},
|
||||
created() {}
|
||||
mounted() {
|
||||
const manager = window.h5HistoryLayerManager
|
||||
if (!manager || typeof manager.ensureRegistered !== "function" || typeof manager.subscribe !== "function") return
|
||||
|
||||
// 每个组件实例使用唯一弹层名称,同一页面存在多个签字组件时互不影响。
|
||||
this.historyLayerName = "h5-signature-" + this._uid
|
||||
manager.ensureRegistered("h5-signature-page-" + window.location.pathname + window.location.search)
|
||||
this.historyLayerUnsubscribe = manager.subscribe((stack) => {
|
||||
this.showSignaturePanel = stack.includes(this.historyLayerName)
|
||||
})
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (typeof this.historyLayerUnsubscribe === "function") {
|
||||
this.historyLayerUnsubscribe()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.h5-signature {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.main-panel {
|
||||
position: relative;
|
||||
flex-direction: column-reverse;
|
||||
.signature-entry {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
row-gap: 10px;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.main-panel .van-image {
|
||||
.signature-entry-panel {
|
||||
width: 100%;
|
||||
min-height: 150px;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
border: 1px dashed #dfe6ef;
|
||||
border-radius: 9px;
|
||||
background: #fbfcfe;
|
||||
}
|
||||
|
||||
.main-panel .open-button {
|
||||
color: var(--color-primary);
|
||||
.signature-placeholder-icon {
|
||||
margin-bottom: 6px;
|
||||
color: #c7cfda;
|
||||
font-size: 38px;
|
||||
}
|
||||
|
||||
.signature-placeholder-text {
|
||||
margin-bottom: 12px;
|
||||
color: #9aa4b2;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.signature-open-button {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
color: var(--color-primary, #1989fa);
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
background: linear-gradient(90deg, #f0f7ff, #f5f9ff);
|
||||
border-color: #d8e9ff;
|
||||
border-radius: 7px;
|
||||
}
|
||||
|
||||
.signature-entry-panel--completed {
|
||||
min-height: 150px;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.signature-preview {
|
||||
width: 100%;
|
||||
height: 96px;
|
||||
background: #fff;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.signature-completed-text {
|
||||
margin-top: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #5f6b7a;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.signature-completed-text .van-icon {
|
||||
margin-right: 5px;
|
||||
color: #28a745;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.signature-reset-button {
|
||||
align-self: center;
|
||||
margin-top: 10px;
|
||||
padding: 3px 12px;
|
||||
color: var(--color-primary, #1989fa);
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -57,7 +57,16 @@ module.exports = {
|
||||
},
|
||||
save() {
|
||||
if (signature.isEmpty()) {
|
||||
alert("请签字后再保存")
|
||||
// H5 页面使用统一的 Vant 提示;未加载 Vant 的独立签字页保留原生提示作为兼容兜底。
|
||||
if (typeof this.$toast === "function") {
|
||||
this.$toast({
|
||||
message: "请先完成手写签名",
|
||||
icon: "edit",
|
||||
duration: 2000
|
||||
})
|
||||
} else {
|
||||
alert("请先完成手写签名")
|
||||
}
|
||||
return
|
||||
}
|
||||
// const png = signature.getPNG()
|
||||
|
||||
@@ -175,6 +175,12 @@ module.exports = {
|
||||
default: "",
|
||||
required: false
|
||||
},
|
||||
// 默认仅由文件选择器按 accept 过滤;启用后上传前再次校验扩展名,accept 需传 .pdf,.jpg 等白名单。
|
||||
validate_accept: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: false
|
||||
},
|
||||
// 是否是完整的结果(就是文件上传返回什么,该组件返回什么,uploadResultCategory必须为array)
|
||||
complete_result: {
|
||||
type: Boolean,
|
||||
@@ -316,7 +322,7 @@ module.exports = {
|
||||
}
|
||||
},
|
||||
|
||||
// 这是兜底逻辑,保准只让这个image类型上传图片
|
||||
// 保留原有大小限制;业务显式开启 validate_accept 时,额外按扩展名拦截不符合 accept 的文件。
|
||||
beforeUpload(file) {
|
||||
|
||||
// 需要兼容部分业务不限制附件大小,关闭后仅跳过大小校验,不影响数量和格式控制。
|
||||
@@ -324,35 +330,17 @@ module.exports = {
|
||||
this.$message.error("文件过大,请上传小于" + (this.upload_size / 1024 / 1024).toFixed(0) + "M的文件!")
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
if (this.validate_accept && this.fileAccept) {
|
||||
const acceptTypes = this.fileAccept.split(",").map((type) => type.trim().toLowerCase())
|
||||
const fileName = (file.name || "").toLowerCase()
|
||||
const dotIndex = fileName.lastIndexOf(".")
|
||||
// 文件名可能没有扩展名或被伪装为“申请表.pdf.docx”,始终只校验最后一个扩展名。
|
||||
if (dotIndex < 0 || !acceptTypes.includes(fileName.substring(dotIndex))) {
|
||||
this.$message.error("文件只能是 " + this.fileAccept + " 格式!")
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
|
||||
// if (!this.fileAccept || typeof this.fileAccept !== "string") {
|
||||
// return true
|
||||
// }
|
||||
//
|
||||
// // 去除前后空格并转换为数组
|
||||
// const acceptTypes = this.fileAccept
|
||||
// .trim()
|
||||
// .split(",")
|
||||
// .map((type) => type.trim().toUpperCase())
|
||||
//
|
||||
// let fileType
|
||||
// const dotIndex = file.name.lastIndexOf(".")
|
||||
// if (dotIndex !== -1) {
|
||||
// fileType = file.name.substring(dotIndex).toUpperCase()
|
||||
// } else {
|
||||
// this.$message.error("文件必须包含有效的扩展名!")
|
||||
// return false
|
||||
// }
|
||||
//
|
||||
// const valid = acceptTypes.includes(fileType)
|
||||
// if (valid) {
|
||||
// return true
|
||||
// }
|
||||
// this.$message.error(`文件只能是 ${this.fileAccept} 格式!`)
|
||||
// return false
|
||||
},
|
||||
|
||||
// 预览图片
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
<template>
|
||||
<van-dropdown-item :options="yearOptions" @change="yearChange" v-model="year"></van-dropdown-item>
|
||||
<van-dropdown-item
|
||||
:options="yearOptions"
|
||||
v-model="year"
|
||||
@change="yearChange"
|
||||
@open="$emit('open')"
|
||||
@close="$emit('close')"
|
||||
></van-dropdown-item>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -17,7 +23,7 @@ module.exports = {
|
||||
},
|
||||
watch: {
|
||||
value: {
|
||||
handler: function (val) {
|
||||
handler(val) {
|
||||
this.year = val
|
||||
},
|
||||
immediate: true
|
||||
|
||||
@@ -61,6 +61,7 @@
|
||||
<script src="${base!}/assets/platform/js/mixin/styleMixin.js"></script>
|
||||
<script src="${base!}/assets/platform/js/tool/businessTool.js"></script>
|
||||
<script src="${base!}/assets/platform/js/util/h5AutoShowError.js"></script>
|
||||
<script src="${base!}/assets/platform/js/util/h5PopupHistory.js"></script>
|
||||
|
||||
<!--富文本编辑器-->
|
||||
<link rel="stylesheet" href="${base!}/assets/platform/plugins/wangEditor4/wangEditor.css"/>
|
||||
@@ -84,7 +85,7 @@
|
||||
vant.Form.props.showErrorMessage.default = false
|
||||
vant.ActionSheet.props.lazyRender.default = false
|
||||
|
||||
function toggleShowBar() {
|
||||
const toggleShowBar = () => {
|
||||
if (["/platform/h5/home", "/platform/home"].includes(window.location.pathname)) {
|
||||
document.querySelector("body>.main-page").classList.add("page-showtabbar")
|
||||
} else {
|
||||
@@ -96,8 +97,10 @@
|
||||
vant.Toast.allowMultiple()
|
||||
|
||||
//初始化pjax
|
||||
;(function initPjax() {
|
||||
;(() => {
|
||||
if ($.support.pjax) {
|
||||
// jquery-pjax 的历史缓存读取全局默认值;必须在初始化前关闭,避免返回时恢复缺少 Vue 动态样式的旧 DOM。
|
||||
$.pjax.defaults.maxCacheLength = 0
|
||||
$(document).pjax("a[data-pjax]", "#container", {
|
||||
maxCacheLength: 0,
|
||||
push: true,
|
||||
@@ -105,39 +108,44 @@
|
||||
fragment: "#container",
|
||||
timeout: 8000
|
||||
})
|
||||
$(document).on("pjax:send", function () {
|
||||
$(document).on("pjax:send", () => {
|
||||
window.h5HistoryLayerManager.reset()
|
||||
showH5PageSkeleton()
|
||||
NProgress.start()
|
||||
window.customStyleList = document.querySelectorAll('style[id^="style-"]')
|
||||
})
|
||||
$(document).on("pjax:complete", function () {
|
||||
$(document).on("pjax:complete", () => {
|
||||
// PJAX 替换 #container 时可能已经移除旧样式节点,清理前需兼容节点脱离 DOM 的情况。
|
||||
if (window.customStyleList) {
|
||||
window.customStyleList.forEach((style) => {
|
||||
style.parentNode.removeChild(style)
|
||||
if (style && style.parentNode) {
|
||||
style.parentNode.removeChild(style)
|
||||
}
|
||||
})
|
||||
window.customStyleList = null
|
||||
}
|
||||
NProgress.done()
|
||||
hideH5PageSkeleton()
|
||||
toggleShowBar()
|
||||
})
|
||||
|
||||
// 监听浏览器的前进和后退操作
|
||||
$(window).on("popstate", function (event) {
|
||||
if (event.originalEvent.state) {
|
||||
// 如果存在历史记录状态,触发 Pjax 加载对应内容
|
||||
$.pjax({
|
||||
url: event.originalEvent.state.url,
|
||||
container: "#container",
|
||||
fragment: "#container",
|
||||
timeout: 8000,
|
||||
push: false,
|
||||
scrollTo: false
|
||||
})
|
||||
}
|
||||
$(document).on("pjax:beforeReplace", () => {
|
||||
window.h5PopupHistory.destroyPage()
|
||||
})
|
||||
|
||||
}
|
||||
})()
|
||||
|
||||
function pjaxReplace(url, data = null) {
|
||||
$.pjax({
|
||||
window.pjaxReplace = (url, data = null) => {
|
||||
// 导航必须等待弹框历史退栈,避免返回时恢复已关闭的弹框记录。
|
||||
if (window.h5PopupHistory.hasOpen()) return window.h5PopupHistory.clearAll().then(() => pjaxReplace(url, data))
|
||||
showH5PageSkeleton(url)
|
||||
// 疗休养报名页面使用独立 H5 布局,其中包含图片懒加载等专用初始化逻辑。
|
||||
// 禁止通过公共 PJAX 只替换页面片段,否则专用布局脚本不会执行,首次进入会出现图片懒加载指令未注册。
|
||||
if (url && url.indexOf("/platform/tour/signup/h5") === 0) {
|
||||
window.location.href = url
|
||||
return
|
||||
}
|
||||
return $.pjax({
|
||||
url: url,
|
||||
container: "#container",
|
||||
maxCacheLength: 0,
|
||||
@@ -149,17 +157,97 @@
|
||||
})
|
||||
}
|
||||
|
||||
function createListLoading(icon = "https://preview.qiantucdn.com/58pic/35/39/61/62K58PICb88i68HEwVnm5_PIC2018.gif!qt480") {
|
||||
return vant.Toast.loading({
|
||||
icon: icon,
|
||||
forbidClick: true,
|
||||
loadingType: "spinner",
|
||||
duration: 0,
|
||||
className: "custom-list-loading"
|
||||
window.pjaxReplace = pjaxReplace
|
||||
|
||||
// 仅在疗休养 H5 页面切换期间显示骨架,避免影响共用布局下的其他业务模块。
|
||||
const showH5PageSkeleton = (targetUrl) => {
|
||||
const pageUrl = targetUrl || window.location.pathname
|
||||
if (pageUrl.indexOf("/platform/tour/") !== 0) return
|
||||
const skeleton = document.getElementById("tour-page-transition-skeleton")
|
||||
if (skeleton) skeleton.classList.add("is-visible")
|
||||
}
|
||||
|
||||
const hideH5PageSkeleton = () => {
|
||||
const skeleton = document.getElementById("tour-page-transition-skeleton")
|
||||
if (skeleton) skeleton.classList.remove("is-visible")
|
||||
}
|
||||
|
||||
// 从疗休养完整页面返回公共 H5 页面时可能命中 BFCache,需要清除离开前保留的骨架状态。
|
||||
window.addEventListener("pageshow", () => {
|
||||
hideH5PageSkeleton()
|
||||
})
|
||||
|
||||
let h5GlobalLoading = null
|
||||
let h5PendingRequestCount = 0
|
||||
let h5ManualLoadingCount = 0
|
||||
|
||||
const showH5GlobalLoading = (icon = "/assets/mobile/img/common/smart-union-loading-red.svg") => {
|
||||
if (!h5GlobalLoading) {
|
||||
h5GlobalLoading = vant.Toast.loading({
|
||||
icon: icon,
|
||||
message: "加载中...",
|
||||
forbidClick: true,
|
||||
loadingType: "spinner",
|
||||
duration: 0,
|
||||
className: "custom-list-loading"
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const closeH5GlobalLoading = () => {
|
||||
if (h5PendingRequestCount > 0 || h5ManualLoadingCount > 0 || !h5GlobalLoading) return
|
||||
h5GlobalLoading.clear()
|
||||
h5GlobalLoading = null
|
||||
}
|
||||
|
||||
// 请求数量归零后关闭统一 Loading;使用页面骨架屏的请求不叠加遮罩。
|
||||
window.bindH5AxiosLoading = (axiosService) => {
|
||||
if (axiosService.__h5LoadingBound) return
|
||||
axiosService.__h5LoadingBound = true
|
||||
axiosService.interceptors.request.use((config) => {
|
||||
if (config.h5SkeletonLoading) return config
|
||||
h5PendingRequestCount++
|
||||
showH5GlobalLoading()
|
||||
return config
|
||||
}, (error) => {
|
||||
return Promise.reject(error)
|
||||
})
|
||||
|
||||
axiosService.interceptors.response.use((response) => {
|
||||
if (!response.config || !response.config.h5SkeletonLoading) {
|
||||
h5PendingRequestCount = Math.max(0, h5PendingRequestCount - 1)
|
||||
closeH5GlobalLoading()
|
||||
}
|
||||
return response
|
||||
}, (error) => {
|
||||
if (!error.config || !error.config.h5SkeletonLoading) {
|
||||
h5PendingRequestCount = Math.max(0, h5PendingRequestCount - 1)
|
||||
closeH5GlobalLoading()
|
||||
}
|
||||
return Promise.reject(error)
|
||||
})
|
||||
}
|
||||
|
||||
function historyBack(func) {
|
||||
const createListLoading = (icon = "/assets/mobile/img/common/smart-union-loading-red.svg") => {
|
||||
h5ManualLoadingCount++
|
||||
showH5GlobalLoading(icon)
|
||||
let cleared = false
|
||||
const closeLoading = () => {
|
||||
if (cleared) return
|
||||
cleared = true
|
||||
h5ManualLoadingCount = Math.max(0, h5ManualLoadingCount - 1)
|
||||
closeH5GlobalLoading()
|
||||
}
|
||||
|
||||
return {
|
||||
clear: closeLoading,
|
||||
close: closeLoading
|
||||
}
|
||||
}
|
||||
|
||||
const historyBack = (func) => {
|
||||
if (window.h5PopupHistory.hasOpen()) { window.h5HistoryLayerManager.close(); return }
|
||||
showH5PageSkeleton()
|
||||
if (window.history.length > 1) {
|
||||
window.history.back()
|
||||
} else {
|
||||
@@ -168,7 +256,8 @@
|
||||
func && typeof func === "function" && func()
|
||||
}
|
||||
|
||||
function returnH5Home(activeTab = "home") {
|
||||
const returnH5Home = (activeTab = "home") => {
|
||||
if (window.h5PopupHistory.hasOpen()) return window.h5PopupHistory.clearAll().then(() => returnH5Home(activeTab))
|
||||
if (window.store && typeof window.store.commit === "function") {
|
||||
window.store.commit("setActiveTarBar", activeTab || "home")
|
||||
}
|
||||
@@ -177,10 +266,10 @@
|
||||
|
||||
Vue.mixin({
|
||||
methods: {
|
||||
historyBack: function(func = ()=>{}) {
|
||||
historyBack(func = ()=>{}) {
|
||||
historyBack(func);
|
||||
},
|
||||
returnH5Home: function(activeTab = "home") {
|
||||
returnH5Home(activeTab = "home") {
|
||||
returnH5Home(activeTab)
|
||||
}
|
||||
},
|
||||
@@ -405,10 +494,12 @@
|
||||
Vue.prototype.$axios = commonUtil.axiosService()
|
||||
Vue.prototype.$downLoad = commonUtil.downLoadService.bind(commonUtil)
|
||||
Vue.prototype.$pjaxReplace = pjaxReplace
|
||||
Vue.prototype.$showH5PageSkeleton = showH5PageSkeleton
|
||||
Vue.prototype.$hideH5PageSkeleton = hideH5PageSkeleton
|
||||
</script>
|
||||
|
||||
<script nonce="${cspNonce!}">
|
||||
const h5ComponentVersion = "20260524"
|
||||
const h5ComponentVersion = "20260821_2"
|
||||
Vue.component("rich-text", httpVueLoader("/components/plugins/sysRichTextView/index.vue?v=" + h5ComponentVersion))
|
||||
Vue.component("h5-file-upload", httpVueLoader("/components/plugins/sysUpload/h5Index.vue?v=" + h5ComponentVersion))
|
||||
Vue.component("h5-signature", httpVueLoader("/components/plugins/sysSignature/h5Index.vue?v=" + h5ComponentVersion))
|
||||
@@ -450,9 +541,41 @@
|
||||
.h5-voice-menu .voice-menu-text {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tour-page-transition-skeleton {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 30000;
|
||||
display: none;
|
||||
padding: 58px 12px 20px;
|
||||
background: #f3f7fd;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.tour-page-transition-skeleton.is-visible { display: block; }
|
||||
.tour-page-transition-skeleton__nav,
|
||||
.tour-page-transition-skeleton__hero,
|
||||
.tour-page-transition-skeleton__line,
|
||||
.tour-page-transition-skeleton__card {
|
||||
background: linear-gradient(90deg, #edf2f8 25%, #f8fbff 37%, #edf2f8 63%);
|
||||
background-size: 400% 100%;
|
||||
animation: tour-page-skeleton-loading 1.35s ease infinite;
|
||||
}
|
||||
.tour-page-transition-skeleton__nav { position: absolute; top: 0; left: 0; right: 0; height: 46px; background-color: #fff; }
|
||||
.tour-page-transition-skeleton__hero { height: 154px; border-radius: 14px; }
|
||||
.tour-page-transition-skeleton__line { width: 42%; height: 18px; margin: 18px 2px 12px; border-radius: 9px; }
|
||||
.tour-page-transition-skeleton__card { height: 126px; margin-top: 12px; border-radius: 14px; }
|
||||
@keyframes tour-page-skeleton-loading { 0% { background-position: 100% 50%; } 100% { background-position: 0 50%; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="tour-page-transition-skeleton" class="tour-page-transition-skeleton" aria-hidden="true">
|
||||
<div class="tour-page-transition-skeleton__nav"></div>
|
||||
<div class="tour-page-transition-skeleton__hero"></div>
|
||||
<div class="tour-page-transition-skeleton__line"></div>
|
||||
<div class="tour-page-transition-skeleton__card"></div>
|
||||
<div class="tour-page-transition-skeleton__card"></div>
|
||||
<div class="tour-page-transition-skeleton__card"></div>
|
||||
</div>
|
||||
<div class="main-page page-showtabbar">
|
||||
<div class="page-wrapper">
|
||||
<div id="container">${layoutContent}</div>
|
||||
|
||||
@@ -11,11 +11,38 @@
|
||||
<link rel="stylesheet" href="${base!}/assets/platform/css/common.css?v=20260524"/>
|
||||
<link rel="stylesheet" href="${base!}/assets/platform/css/h5.css?v=20260524"/>
|
||||
|
||||
<style>
|
||||
.tour-page-transition-skeleton {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 30000;
|
||||
display: none;
|
||||
padding: 58px 12px 20px;
|
||||
background: #f3f7fd;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.tour-page-transition-skeleton.is-visible { display: block; }
|
||||
.tour-page-transition-skeleton__nav,
|
||||
.tour-page-transition-skeleton__hero,
|
||||
.tour-page-transition-skeleton__line,
|
||||
.tour-page-transition-skeleton__card {
|
||||
background: linear-gradient(90deg, #edf2f8 25%, #f8fbff 37%, #edf2f8 63%);
|
||||
background-size: 400% 100%;
|
||||
animation: tour-page-skeleton-loading 1.35s ease infinite;
|
||||
}
|
||||
.tour-page-transition-skeleton__nav { position: absolute; top: 0; left: 0; right: 0; height: 46px; background-color: #fff; }
|
||||
.tour-page-transition-skeleton__hero { height: 154px; border-radius: 14px; }
|
||||
.tour-page-transition-skeleton__line { width: 42%; height: 18px; margin: 18px 2px 12px; border-radius: 9px; }
|
||||
.tour-page-transition-skeleton__card { height: 126px; margin-top: 12px; border-radius: 14px; }
|
||||
@keyframes tour-page-skeleton-loading { 0% { background-position: 100% 50%; } 100% { background-position: 0 50%; } }
|
||||
</style>
|
||||
|
||||
<script src="${base!}/assets/platform/plugins/vue/vue.js?v=20260524"></script>
|
||||
<script src="${base!}/assets/platform/plugins/vant/index.js?v=20260524"></script>
|
||||
<script src="${base!}/assets/platform/plugins/axios/axios.js?v=20260524"></script>
|
||||
<script src="${base!}/assets/platform/plugins/jquery/jquery.js?v=20260524"></script>
|
||||
<script src="${base!}/assets/platform/js/util/commonUtil.js?v=20260524"></script>
|
||||
<script src="${base!}/assets/platform/js/util/h5PopupHistory.js?v=20260830"></script>
|
||||
|
||||
<script nonce="${cspNonce!}">
|
||||
Vue.config.devtools = true
|
||||
@@ -31,12 +58,12 @@
|
||||
Vue.use(vant.Lazyload)
|
||||
}
|
||||
|
||||
window.loadScriptOnce = function(src) {
|
||||
window.loadScriptOnce = (src) => {
|
||||
window.__scriptLoadingMap = window.__scriptLoadingMap || {}
|
||||
if (window.__scriptLoadingMap[src]) {
|
||||
return window.__scriptLoadingMap[src]
|
||||
}
|
||||
window.__scriptLoadingMap[src] = new Promise(function(resolve, reject) {
|
||||
window.__scriptLoadingMap[src] = new Promise((resolve, reject) => {
|
||||
const existing = document.querySelector('script[src="' + src + '"]')
|
||||
if (existing) {
|
||||
resolve()
|
||||
@@ -44,10 +71,10 @@
|
||||
}
|
||||
const script = document.createElement("script")
|
||||
script.src = src
|
||||
script.onload = function() {
|
||||
script.onload = () => {
|
||||
resolve()
|
||||
}
|
||||
script.onerror = function() {
|
||||
script.onerror = () => {
|
||||
reject(new Error("script load failed: " + src))
|
||||
}
|
||||
document.head.appendChild(script)
|
||||
@@ -55,12 +82,12 @@
|
||||
return window.__scriptLoadingMap[src]
|
||||
}
|
||||
|
||||
window.loadStyleOnce = function(href) {
|
||||
window.loadStyleOnce = (href) => {
|
||||
window.__styleLoadingMap = window.__styleLoadingMap || {}
|
||||
if (window.__styleLoadingMap[href]) {
|
||||
return window.__styleLoadingMap[href]
|
||||
}
|
||||
window.__styleLoadingMap[href] = new Promise(function(resolve, reject) {
|
||||
window.__styleLoadingMap[href] = new Promise((resolve, reject) => {
|
||||
const existing = document.querySelector('link[href="' + href + '"]')
|
||||
if (existing) {
|
||||
resolve()
|
||||
@@ -69,10 +96,10 @@
|
||||
const link = document.createElement("link")
|
||||
link.rel = "stylesheet"
|
||||
link.href = href
|
||||
link.onload = function() {
|
||||
link.onload = () => {
|
||||
resolve()
|
||||
}
|
||||
link.onerror = function() {
|
||||
link.onerror = () => {
|
||||
reject(new Error("style load failed: " + href))
|
||||
}
|
||||
document.head.appendChild(link)
|
||||
@@ -123,13 +150,38 @@
|
||||
}
|
||||
}
|
||||
|
||||
function historyBack(fallbackUrl, func) {
|
||||
// 疗休养页面跳转前统一展示结构化骨架,避免旧页面或未完成样式短暂闪现。
|
||||
const showH5PageSkeleton = () => {
|
||||
const skeleton = document.getElementById("tour-page-transition-skeleton")
|
||||
if (skeleton) skeleton.classList.add("is-visible")
|
||||
}
|
||||
|
||||
const hideH5PageSkeleton = () => {
|
||||
const skeleton = document.getElementById("tour-page-transition-skeleton")
|
||||
if (skeleton) skeleton.classList.remove("is-visible")
|
||||
}
|
||||
|
||||
// 浏览器返回或前进时可能直接恢复 BFCache 页面,需要主动关闭离开页面前显示的骨架层。
|
||||
// 此处只恢复页面展示,不刷新数据,从而保留线路列表的筛选条件、分页和滚动位置。
|
||||
window.addEventListener("pageshow", () => {
|
||||
hideH5PageSkeleton()
|
||||
})
|
||||
|
||||
const pjaxReplace = (url) => {
|
||||
if (window.h5PopupHistory.hasOpen()) return window.h5PopupHistory.clearAll().then(() => pjaxReplace(url))
|
||||
showH5PageSkeleton()
|
||||
window.location.href = url
|
||||
}
|
||||
|
||||
const historyBack = (fallbackUrl, func) => {
|
||||
if (window.h5PopupHistory.hasOpen()) { window.h5HistoryLayerManager.close(); return }
|
||||
if (typeof fallbackUrl === "function") {
|
||||
func = fallbackUrl
|
||||
fallbackUrl = "/platform/h5/home"
|
||||
} else if (typeof fallbackUrl !== "string" || !fallbackUrl) {
|
||||
fallbackUrl = "/platform/h5/home"
|
||||
}
|
||||
showH5PageSkeleton()
|
||||
if (window.history.length > 1) {
|
||||
window.history.back()
|
||||
} else {
|
||||
@@ -138,17 +190,18 @@
|
||||
func && typeof func === "function" && func()
|
||||
}
|
||||
|
||||
function returnH5Home(activeTab = "home") {
|
||||
const returnH5Home = (activeTab = "home") => {
|
||||
if (window.h5PopupHistory.hasOpen()) return window.h5PopupHistory.clearAll().then(() => returnH5Home(activeTab))
|
||||
store.commit("setActiveTarBar", activeTab || "home")
|
||||
window.location.replace("/platform/h5/home")
|
||||
}
|
||||
|
||||
Vue.mixin({
|
||||
methods: {
|
||||
historyBack: function(fallbackUrl = "/platform/h5/home", func = function(){}) {
|
||||
historyBack(fallbackUrl = "/platform/h5/home", func = ()=>{}) {
|
||||
historyBack(fallbackUrl, func)
|
||||
},
|
||||
returnH5Home: function(activeTab = "home") {
|
||||
returnH5Home(activeTab = "home") {
|
||||
returnH5Home(activeTab)
|
||||
}
|
||||
}
|
||||
@@ -158,9 +211,21 @@
|
||||
Vue.prototype.$auth = commonUtil.authService()
|
||||
Vue.prototype.$axios = commonUtil.axiosService()
|
||||
Vue.prototype.$downLoad = commonUtil.downLoadService.bind(commonUtil)
|
||||
window.pjaxReplace = pjaxReplace
|
||||
Vue.prototype.$pjaxReplace = pjaxReplace
|
||||
Vue.prototype.$showH5PageSkeleton = showH5PageSkeleton
|
||||
Vue.prototype.$hideH5PageSkeleton = hideH5PageSkeleton
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="tour-page-transition-skeleton" class="tour-page-transition-skeleton" aria-hidden="true">
|
||||
<div class="tour-page-transition-skeleton__nav"></div>
|
||||
<div class="tour-page-transition-skeleton__hero"></div>
|
||||
<div class="tour-page-transition-skeleton__line"></div>
|
||||
<div class="tour-page-transition-skeleton__card"></div>
|
||||
<div class="tour-page-transition-skeleton__card"></div>
|
||||
<div class="tour-page-transition-skeleton__card"></div>
|
||||
</div>
|
||||
<div class="main-page">
|
||||
<div class="page-wrapper">
|
||||
<div id="container">${layoutContent}</div>
|
||||
|
||||
@@ -33,11 +33,16 @@ const applyForm = {
|
||||
<el-input readonly v-model="formData.sex"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<!--<el-col :span="24">
|
||||
<el-form-item label="联系方式" prop="mobile">
|
||||
<el-input v-model="formData.mobile"></el-input>
|
||||
<el-col :span="24">
|
||||
<!-- 手机号必填,格式与后端保持一致:1开头、第二位3至9,共11位数字。 -->
|
||||
<el-form-item label="手机号" prop="mobile"
|
||||
:rules="[
|
||||
{ required: true, whitespace: true, message: '请填写手机号', trigger: 'blur' },
|
||||
{ pattern: /^1[3-9][0-9]{9}$/, message: '请输入正确的11位手机号', trigger: 'blur' }
|
||||
]">
|
||||
<el-input v-model="formData.mobile" maxlength="32" placeholder="请填写手机号"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>-->
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row v-if="courseRow.courseIsLimitApply">
|
||||
@@ -103,8 +108,8 @@ const applyForm = {
|
||||
</el-form>
|
||||
|
||||
<el-row class="mt20" justify="end" type="flex">
|
||||
<el-button @click="signDialog = false">取 消</el-button>
|
||||
<el-button @click="onSubmit" type="primary">提 交</el-button>
|
||||
<el-button @click="$set($data, 'signDialog', false)" :disabled="formLoading">取 消</el-button>
|
||||
<el-button @click="onSubmit" type="primary" :loading="formLoading" :disabled="formLoading">提 交</el-button>
|
||||
</el-row>
|
||||
</el-dialog>
|
||||
</div>
|
||||
@@ -114,60 +119,81 @@ const applyForm = {
|
||||
data() {
|
||||
return {
|
||||
signDialog: false,
|
||||
formLoading: false,
|
||||
formData: {},
|
||||
mobileColumn: null,
|
||||
courseRow: {},
|
||||
courseTimeSelectList: [],
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async getCourseTimeSelectList(o) {
|
||||
const resp = await $.post('/platform/trainSignUp/apply/getCourseTimeSelectList',{ courseId: o.id })
|
||||
if (resp.code === 0) {
|
||||
this.courseTimeSelectList = resp.data
|
||||
} else {
|
||||
this.$message.warning('获取时段信息失败,请联系管理员')
|
||||
}
|
||||
},
|
||||
async onOpen(row, course) {
|
||||
this.initData(row, course)
|
||||
if (row.courseIsLimitApply) {
|
||||
await this.getCourseTimeSelectList(row)
|
||||
}
|
||||
this.courseRow = row
|
||||
this.signDialog = true
|
||||
this.$nextTick(() => {
|
||||
this.$refs.form.clearValidate()
|
||||
/**
|
||||
* o.id 为当前课程 ID;接口返回 Result,data 为含 value(时段 ID)、text(时段描述)的选项数组。
|
||||
*/
|
||||
getCourseTimeSelectList(o) {
|
||||
this.formLoading = true
|
||||
return $.post('/platform/trainSignUp/apply/getCourseTimeSelectList', { courseId: o.id }).then((resp) => {
|
||||
if (resp.code === 0) {
|
||||
this.courseTimeSelectList = resp.data
|
||||
} else {
|
||||
this.$message.warning('获取时段信息失败,请联系管理员')
|
||||
}
|
||||
}).always(() => {
|
||||
this.formLoading = false
|
||||
})
|
||||
},
|
||||
async validateSignUp() {
|
||||
// 获取家属人数
|
||||
// row 为课程记录,course 为课程类型及其动态字段配置;限额课程返回时段加载完成后的 Promise。
|
||||
onOpen(row, course) {
|
||||
this.initData(row, course)
|
||||
const openForm = () => {
|
||||
this.courseRow = row
|
||||
this.signDialog = true
|
||||
this.$nextTick(() => {
|
||||
this.$refs.form.clearValidate()
|
||||
})
|
||||
}
|
||||
// 限额课程先取得可报名时段,再展示表单,保持原有打开顺序。
|
||||
if (row.courseIsLimitApply) return this.getCourseTimeSelectList(row).then(openForm)
|
||||
openForm()
|
||||
},
|
||||
/**
|
||||
* 传入 courseId(课程 ID)、currentFamilyNumber(动态表单填写的家属总人数),返回报名资格是否通过的 Promise<boolean>。
|
||||
*/
|
||||
validateSignUp() {
|
||||
// 家属人数继续取动态字段 xdqsrs,空值或非数值按 0 参与报名人数校验。
|
||||
let familyCount = this.formData.trainMobileSignColumnList.filter(o => o.columnCode === 'xdqsrs').reduce((sum, item) => {
|
||||
return sum + (Number(item.columnValue) || 0)
|
||||
}, 0)
|
||||
const res = await this.$axios.post("/platform/trainSignUp/apply/validateSignUp", {
|
||||
return this.$axios.post("/platform/trainSignUp/apply/validateSignUp", {
|
||||
courseId: this.formData.courseId,
|
||||
currentFamilyNumber: familyCount
|
||||
})
|
||||
return res.code === 0
|
||||
}).then((res) => res.code === 0)
|
||||
},
|
||||
async validateCourseTime() {
|
||||
const res = await this.$axios.post('/platform/trainSignUp/apply/validateSourceSignUp', {
|
||||
/**
|
||||
* 传入 activityCourseId(所选报名时段 ID)、courseId(课程 ID),返回时段名额校验是否通过的 Promise<boolean>。
|
||||
*/
|
||||
validateCourseTime() {
|
||||
return this.$axios.post('/platform/trainSignUp/apply/validateSourceSignUp', {
|
||||
activityCourseId: this.formData.activityCourseId,
|
||||
courseId: this.courseRow.id
|
||||
})
|
||||
return res.code === 0
|
||||
}).then((res) => res.code === 0)
|
||||
},
|
||||
async onSubmit() {
|
||||
if(!await this.validateSignUp()) return
|
||||
|
||||
this.$refs["form"].validate(async (valid) => {
|
||||
if (valid) {
|
||||
if(this.courseRow.courseIsLimitApply && !await this.validateCourseTime()) return
|
||||
this.$confirm("您确定要提交吗?", "提示", {
|
||||
// 返回完整校验和报名提交链路的 Promise;正在处理时直接返回,提交按钮同步显示加载状态。
|
||||
onSubmit() {
|
||||
// 锁定资格校验至提交完成的整个链路,避免重复点击产生多次报名请求。
|
||||
if (this.formLoading) return
|
||||
this.formLoading = true
|
||||
return this.validateSignUp().then((valid) => {
|
||||
if (!valid) return
|
||||
return this.$refs.form.validate().then(() => {
|
||||
return this.courseRow.courseIsLimitApply ? this.validateCourseTime() : true
|
||||
}).then((validTime) => {
|
||||
if (!validTime) return
|
||||
return this.$confirm("您确定要提交吗?", "提示", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning"
|
||||
}).then(async () => {
|
||||
}).then(() => {
|
||||
const mobileColumnsValue = this.formData.trainMobileSignColumnList.map((v) => {
|
||||
return {
|
||||
columnName: v.columnName,
|
||||
@@ -176,28 +202,51 @@ const applyForm = {
|
||||
columnFormType: v.columnFormType
|
||||
}
|
||||
})
|
||||
this.formData.mobileColumnsValue = JSON.stringify(mobileColumnsValue)
|
||||
const resp = await this.$axios.post("/platform/trainSignUp/apply/doSignUp", this.formData)
|
||||
if (resp.code === 0) {
|
||||
this.signDialog = false
|
||||
this.$message.success(resp.msg)
|
||||
this.$emit('refresh')
|
||||
} else {
|
||||
this.$message.warning(resp.msg)
|
||||
// 已配置 mobile 动态字段时同步最新手机号,兼容按动态字段读取数据的原有导出。
|
||||
if (this.mobileColumn) {
|
||||
mobileColumnsValue.push({
|
||||
columnName: this.mobileColumn.columnName,
|
||||
columnValue: this.formData.mobile,
|
||||
columnCode: this.mobileColumn.columnCode,
|
||||
columnFormType: this.mobileColumn.columnFormType
|
||||
})
|
||||
}
|
||||
this.$set(this.formData, 'mobileColumnsValue', JSON.stringify(mobileColumnsValue))
|
||||
// activityId、courseId、activityCourseId 分别标识活动、课程和所选时段;mobile 为本次报名联系方式。
|
||||
// mobileColumnsValue 为含字段名、编码、控件类型及值的 JSON 数组,其他基础信息保持原表单内容。
|
||||
// 接口返回 Result,code 为 0 表示报名成功,msg 用于结果提示。
|
||||
return this.$axios.post("/platform/trainSignUp/apply/doSignUp", this.formData).then((resp) => {
|
||||
if (resp.code === 0) {
|
||||
this.signDialog = false
|
||||
this.$message.success(resp.msg)
|
||||
this.$emit('refresh')
|
||||
} else {
|
||||
this.$message.warning(resp.msg)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
}).catch(() => {
|
||||
// 取消确认或表单校验不通过时保留当前填写内容;网络错误由公共请求拦截器提示。
|
||||
}).finally(() => {
|
||||
this.formLoading = false
|
||||
})
|
||||
},
|
||||
// row 提供活动、课程 ID,course 提供动态字段;仅准备本次报名表单及手机号元数据。
|
||||
initData(row, course) {
|
||||
this.$set(this.formData, 'username', this.$store.state.user.username)
|
||||
this.$set(this.formData, 'loginname', this.$store.state.user.loginname)
|
||||
this.$set(this.formData, 'unionName', this.$store.state.user.union.name)
|
||||
this.$set(this.formData, 'unitName', this.$store.state.user.unit.name)
|
||||
this.$set(this.formData, 'sex', this.$store.state.user.sex)
|
||||
// 默认取登录用户手机号,可编辑且只随本次报名提交,不修改个人资料。
|
||||
this.$set(this.formData, 'mobile', this.$store.state.user.mobile || '')
|
||||
this.$set(this.formData, 'activityId', row.activityId)
|
||||
this.$set(this.formData, 'courseId', row.id)
|
||||
this.$set(this.formData, 'trainMobileSignColumnList', course.trainMobileSignColumnList)
|
||||
// 复制配置以免修改课程类型原数据;手机号统一必填,不受动态 isRequired 影响,保留 mobile 元数据兼容同步提交。
|
||||
const columns = clone(course.trainMobileSignColumnList)
|
||||
this.mobileColumn = columns.find((item) => item.columnCode === 'mobile') || null
|
||||
this.$set(this.formData, 'trainMobileSignColumnList', columns.filter((item) => item.columnCode !== 'mobile'))
|
||||
|
||||
this.formData.trainMobileSignColumnList.forEach((item) => {
|
||||
item.columnValue = this.$store.state.user[item.columnCode] || ""
|
||||
|
||||
@@ -96,8 +96,8 @@ const basicForm = {
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="活动类型" prop="trainType" :rules="{required:true,message: '请选择活动类型', trigger: 'blur'}">
|
||||
<el-select @change="typeChange" filterable placeholder="请选择活动类型" style="width: 100%" v-model="formData.trainType">
|
||||
<el-option :label="item.name" :value="item.code" :key="item.code" v-for="item in dict.type.TRAIN_SIGNUP_TYPE"></el-option>
|
||||
<el-select @change="typeChange" @visible-change="(visible) => visible && loadDictOptions('TRAIN_SIGNUP_TYPE')" :loading="dictOptionsLoading.TRAIN_SIGNUP_TYPE" filterable placeholder="请选择活动类型" style="width: 100%" v-model="formData.trainType">
|
||||
<el-option :label="item.name" :value="item.code" :key="item.code" v-for="item in trainTypeList"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
@@ -221,8 +221,8 @@ const basicForm = {
|
||||
|
||||
<el-table-column label="校区" prop="campus">
|
||||
<template v-slot="{row}">
|
||||
<el-select size="small" v-model="row.campus" style="width: 100%">
|
||||
<el-option :label="item.name" :value="item.name" :key="item.code" v-for="item in dict.type.USER_CAMPUS"></el-option>
|
||||
<el-select size="small" v-model="row.campus" style="width: 100%" :loading="dictOptionsLoading.USER_CAMPUS" @visible-change="(visible) => visible && loadDictOptions('USER_CAMPUS')">
|
||||
<el-option :label="item.name" :value="item.name" :key="item.code" v-for="item in campusList"></el-option>
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -324,7 +324,6 @@ const basicForm = {
|
||||
<custom-form ref="customFormRef"></custom-form>
|
||||
</div>
|
||||
`,
|
||||
dicts: ["USER_CAMPUS", "TRAIN_SIGNUP_TYPE"],
|
||||
data() {
|
||||
return {
|
||||
signUpDialog: false,
|
||||
@@ -356,6 +355,8 @@ const basicForm = {
|
||||
}
|
||||
},
|
||||
campusList: [],
|
||||
dictOptionsLoading: { USER_CAMPUS: false, TRAIN_SIGNUP_TYPE: false },
|
||||
dictOptionsRequests: { USER_CAMPUS: null, TRAIN_SIGNUP_TYPE: null },
|
||||
courseTypeList: [],
|
||||
undertakeOptions: [],
|
||||
}
|
||||
@@ -523,8 +524,56 @@ const basicForm = {
|
||||
})
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 查询本表单的下拉字典。code 仅传 USER_CAMPUS(校区)或 TRAIN_SIGNUP_TYPE(活动类型)。
|
||||
* 接口返回 {code: 0, data: [{name, code}]};name 为显示名称,校区保存 name,活动类型保存 code。
|
||||
* 返回 Promise<Array>,失败时返回空数组;共用进行中的请求,失败或无选项时允许重新展开下拉框重试。
|
||||
*/
|
||||
loadDictOptions(code) {
|
||||
const options = {
|
||||
USER_CAMPUS: { field: "campusList", label: "校区" },
|
||||
TRAIN_SIGNUP_TYPE: { field: "trainTypeList", label: "活动类型" }
|
||||
}[code]
|
||||
if (!options) {
|
||||
return Promise.resolve([])
|
||||
}
|
||||
if (this.dictOptionsRequests[code]) {
|
||||
return this.dictOptionsRequests[code]
|
||||
}
|
||||
if (this[options.field].length > 0) {
|
||||
return Promise.resolve(this[options.field])
|
||||
}
|
||||
this.$set(this.dictOptionsLoading, code, true)
|
||||
const request = this.$axios.post("/open/common/dictOptions", { code })
|
||||
.then((res) => {
|
||||
if (!res || res.code !== 0 || !Array.isArray(res.data)) {
|
||||
this.$message.warning(options.label + "选项加载失败,请重新展开下拉框重试")
|
||||
return []
|
||||
}
|
||||
this.$set(this, options.field, res.data)
|
||||
if (res.data.length === 0) {
|
||||
this.$message.warning(options.label + "暂无可用选项,请检查字典配置")
|
||||
}
|
||||
// 重试成功后按当前表单值更新业务名称,避免早于字典返回的回显停留在默认名称。
|
||||
if (code === "TRAIN_SIGNUP_TYPE") {
|
||||
this.typeChange(this.formData.trainType)
|
||||
}
|
||||
return res.data
|
||||
})
|
||||
.catch(() => {
|
||||
// 网络及接口异常由全局 axios 拦截器提示;保留原表单值,允许下次展开时重试。
|
||||
return []
|
||||
})
|
||||
.finally(() => {
|
||||
this.$set(this.dictOptionsLoading, code, false)
|
||||
this.$set(this.dictOptionsRequests, code, null)
|
||||
})
|
||||
this.$set(this.dictOptionsRequests, code, request)
|
||||
return request
|
||||
},
|
||||
// val 为活动类型字典编码;按已查询的选项更新名称,未匹配时沿用原有默认名称。
|
||||
typeChange(val) {
|
||||
const type = this.dict.type.TRAIN_SIGNUP_TYPE.find(o => o.code === val)
|
||||
const type = this.trainTypeList.find(o => o.code === val)
|
||||
this.trainType = type?.name || "培训班"
|
||||
},
|
||||
changeActivity(val) {
|
||||
@@ -669,13 +718,25 @@ const basicForm = {
|
||||
}
|
||||
}
|
||||
},
|
||||
async initData(row, copyTemplateMode = false) {
|
||||
// row 为待编辑活动(新建时不传),copyTemplateMode 标识模板复制;返回完成字典及表单回显的 Promise。
|
||||
initData(row, copyTemplateMode = false) {
|
||||
this.copyTemplateMode = copyTemplateMode
|
||||
this.activityGroupList = await this.getActivityGroup()
|
||||
this.historicalActList = await this.getHistoricalActList()
|
||||
this.courseTypeList = await this.getAllType()
|
||||
await this.selectUnitAndClub()
|
||||
await this.init(row)
|
||||
// 两类字典查询完成后再初始化表单,避免活动类型回显与字典加载发生竞争。
|
||||
return Promise.all([this.loadDictOptions("USER_CAMPUS"), this.loadDictOptions("TRAIN_SIGNUP_TYPE")])
|
||||
.then(() => this.getActivityGroup())
|
||||
.then((data) => {
|
||||
this.activityGroupList = data
|
||||
return this.getHistoricalActList()
|
||||
})
|
||||
.then((data) => {
|
||||
this.historicalActList = data
|
||||
return this.getAllType()
|
||||
})
|
||||
.then((data) => {
|
||||
this.courseTypeList = data
|
||||
return this.selectUnitAndClub()
|
||||
})
|
||||
.then(() => this.init(row))
|
||||
},
|
||||
async selectUnitAndClub() {
|
||||
const resp = await this.$axios.post("/platform/trainSignUp/manage/selectUnitAndClub")
|
||||
|
||||
@@ -1,6 +1,79 @@
|
||||
<!--#
|
||||
layout("/layouts/platform.html"){
|
||||
#-->
|
||||
<style nonce="${cspNonce!}">
|
||||
.huazhu-sync-dialog { max-width: calc(100vw - 48px); max-height: 88vh; display: flex; flex-direction: column; border-radius: 8px; }
|
||||
.huazhu-sync-dialog .el-dialog__header { padding: 24px 24px 0; border-bottom: 0; }
|
||||
.huazhu-sync-header-copy { flex: 1; min-width: 0; }
|
||||
.huazhu-sync-dialog .el-dialog__headerbtn { align-self: flex-start; margin-left: 16px; }
|
||||
.huazhu-sync-dialog .el-dialog__body { padding: 0 24px; overflow-y: auto; min-height: 0; font-size: 14px; }
|
||||
.huazhu-sync-dialog .el-button--small { padding: 9px 16px; font-size: 13px; border-radius: 4px; }
|
||||
.huazhu-sync-dialog .el-input--small .el-input__inner { height: 36px; line-height: 36px; font-size: 14px; border-radius: 4px; }
|
||||
.huazhu-sync-dialog .el-input--small .el-input__icon { line-height: 36px; }
|
||||
.huazhu-sync-dialog .el-dialog__footer { padding: 16px 24px; border-top: 1px solid #ebeef5; }
|
||||
.huazhu-sync-title { margin: 0 0 6px; color: #182333; font-size: 20px; font-weight: 600; line-height: 28px; }
|
||||
.huazhu-sync-subtitle { margin: 0; color: #8a929f; font-size: 14px; line-height: 22px; }
|
||||
.huazhu-sync-tabs > .el-tabs__header { margin: 16px 0 22px; }
|
||||
.huazhu-sync-tabs .el-tabs__item { padding: 0 22px; font-size: 14px; }
|
||||
.huazhu-sync-tabs .el-tabs__nav-wrap::after { height: 1px; background: #ebeef5; }
|
||||
.huazhu-sync-footer { display: flex; align-items: center; justify-content: space-between; gap: 16px; text-align: left; }
|
||||
.huazhu-sync-note { color: #8a929f; font-size: 12px; line-height: 20px; }
|
||||
.huazhu-sync-note .el-icon-info { margin-right: 6px; color: #006bb8; }
|
||||
.huazhu-sync-batch { display: flex; align-items: center; gap: 14px; margin-bottom: 18px; }
|
||||
.huazhu-sync-batch-label { color: #303b4a; font-weight: 600; white-space: nowrap; }
|
||||
.huazhu-sync-batch .el-select { width: 330px; max-width: 100%; }
|
||||
.huazhu-sync-batch .el-button { margin-left: auto; }
|
||||
.huazhu-sync-batch .el-button--primary.is-plain { color: #006bb8; background: #fff; border-color: #006bb8; }
|
||||
.huazhu-sync-batch .el-button--primary.is-plain:hover { background: #f1f7fc; }
|
||||
.huazhu-sync-stats { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); padding: 18px 0; margin-bottom: 14px; background: #f6f8fc; border-radius: 4px; }
|
||||
.huazhu-sync-stat { text-align: center; }
|
||||
.huazhu-sync-stat + .huazhu-sync-stat { border-left: 1px solid #dde3ed; }
|
||||
.huazhu-sync-stat strong { display: block; color: #182333; font-size: 28px; font-weight: 600; line-height: 34px; }
|
||||
.huazhu-sync-stat span { display: block; margin-top: 5px; color: #606b7b; font-size: 13px; }
|
||||
.huazhu-sync-stat.success strong, .huazhu-result-success { color: #32953d; }
|
||||
.huazhu-sync-stat.exists strong, .huazhu-result-exists { color: #009fb0; }
|
||||
.huazhu-sync-stat.failed strong, .huazhu-result-failed { color: #e3283b; }
|
||||
.huazhu-sync-stat.unknown strong, .huazhu-result-unknown { color: #ed8a00; }
|
||||
.huazhu-sync-stat.pending strong, .huazhu-result-pending, .huazhu-result-sending { color: #006bb8; }
|
||||
.huazhu-sync-progress { display: flex; align-items: center; gap: 16px; color: #606b7b; font-size: 13px; margin-bottom: 24px; }
|
||||
.huazhu-sync-progress .el-progress { flex: 1; min-width: 100px; }
|
||||
.huazhu-sync-progress .el-progress-bar__outer { background: #e0e5ec; }
|
||||
.huazhu-sync-updated { margin-left: auto; color: #8a929f; white-space: nowrap; }
|
||||
.huazhu-sync-filter { display: flex; align-items: center; flex-wrap: wrap; gap: 12px; margin-bottom: 16px; }
|
||||
.huazhu-sync-filter > label { color: #303b4a; font-size: 13px; }
|
||||
.huazhu-sync-filter .el-input { width: 260px; }
|
||||
.huazhu-sync-filter .el-select { width: 170px; }
|
||||
.huazhu-sync-filter .el-select .el-input { width: 100%; }
|
||||
.huazhu-sync-filter .filter-status { margin-left: 12px; }
|
||||
.huazhu-sync-record-table .el-table__header th { background: #f4f6fa; color: #303b4a; font-weight: 600; }
|
||||
.huazhu-sync-record-table { font-size: 14px; }
|
||||
.huazhu-sync-record-table .el-table__cell { padding: 7px 0; }
|
||||
.huazhu-sync-record-table .el-button--text { padding: 3px 0; }
|
||||
.huazhu-sync-record-table .result-icon { margin-right: 5px; }
|
||||
.huazhu-sync-records .el-pagination-container { margin: 16px 0 14px !important; text-align: right; }
|
||||
.huazhu-sync-empty { padding: 18px 20px; margin-bottom: 18px; color: #9099a6; background: #f6f8fc; border-radius: 4px; font-size: 13px; line-height: 22px; }
|
||||
.huazhu-sync-submit { padding: 2px 0 24px; }
|
||||
.huazhu-sync-submit-note { margin-bottom: 22px; }
|
||||
.huazhu-sync-options { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 32px; padding: 12px 0 24px; }
|
||||
.huazhu-sync-option + .huazhu-sync-option { padding-left: 32px; border-left: 1px solid #ebeef5; }
|
||||
.huazhu-sync-option h3 { margin: 0 0 12px; color: #303b4a; font-size: 16px; font-weight: 600; }
|
||||
.huazhu-sync-option h3 i { margin-right: 8px; color: #006bb8; }
|
||||
.huazhu-sync-option p { margin: 0 0 16px; min-height: 46px; color: #7a8595; font-size: 13px; line-height: 23px; }
|
||||
.huazhu-sync-option .el-tag { margin: 0 6px 8px 0; }
|
||||
.huazhu-sync-selected { min-height: 42px; margin-bottom: 14px; }
|
||||
@media (max-width: 900px) {
|
||||
.huazhu-sync-dialog { max-width: calc(100vw - 24px); }
|
||||
.huazhu-sync-batch, .huazhu-sync-progress { flex-wrap: wrap; }
|
||||
.huazhu-sync-batch .el-select { flex: 1; min-width: 180px; }
|
||||
.huazhu-sync-stats { grid-template-columns: repeat(3, minmax(0, 1fr)); row-gap: 20px; }
|
||||
.huazhu-sync-stat:nth-child(4) { border-left: 0; }
|
||||
.huazhu-sync-stat strong { font-size: 24px; }
|
||||
.huazhu-sync-progress .el-progress { min-width: 130px; }
|
||||
.huazhu-sync-updated { width: 100%; text-align: right; }
|
||||
.huazhu-sync-options { grid-template-columns: 1fr; gap: 24px; }
|
||||
.huazhu-sync-option + .huazhu-sync-option { padding: 24px 0 0; border-left: 0; border-top: 1px solid #ebeef5; }
|
||||
}
|
||||
</style>
|
||||
<div id="app" v-cloak>
|
||||
<guava ref="guava">
|
||||
<template>
|
||||
@@ -12,8 +85,8 @@ layout("/layouts/platform.html"){
|
||||
</search>
|
||||
</el-card>
|
||||
<el-card class="mt10" shadow="never">
|
||||
<table-tool :app="this" label="华住员工管理">
|
||||
<el-button @click="syncSelected" icon="el-icon-refresh" size="small" type="primary">同步选中员工</el-button>
|
||||
<table-tool :app="this" label="待推送员工(已推送人员自动隐藏)">
|
||||
<el-button @click="openSyncManage" 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">
|
||||
<el-table-column type="selection" width="55"></el-table-column>
|
||||
@@ -22,12 +95,57 @@ layout("/layouts/platform.html"){
|
||||
<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 @click="queryStatus(row)" size="mini" type="primary">查询状态</el-button></template>
|
||||
<template slot-scope="{row}"><el-button :loading="queryingUserId === row.id" :disabled="!!queryingUserId" @click="queryStatus(row)" size="mini" type="primary">查询状态</el-button></template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<!--#include("/layouts/pagination.html"){}#-->
|
||||
</el-card>
|
||||
<el-dialog :visible.sync="statusDialogVisible" title="华住同步状态" width="520px">
|
||||
<el-dialog :visible.sync="syncDialogVisible" custom-class="huazhu-sync-dialog" width="1080px" top="8vh" :close-on-click-modal="false">
|
||||
<template slot="title">
|
||||
<div class="huazhu-sync-header-copy">
|
||||
<h2 class="huazhu-sync-title">华住同步管理</h2>
|
||||
<p class="huazhu-sync-subtitle">后台逐人推送,提交后可离开页面,稍后查看结果</p>
|
||||
</div>
|
||||
</template>
|
||||
<el-tabs :value="syncTab" @tab-click="changeSyncTab" class="huazhu-sync-tabs">
|
||||
<el-tab-pane label="发起推送" name="submit">
|
||||
<div class="huazhu-sync-submit">
|
||||
<el-alert class="huazhu-sync-submit-note" :closable="false" show-icon :type="summary.running ? 'warning' : 'info'"
|
||||
:title="summary.running ? '已有未结束任务,可前往进度与记录查看后台状态;任务结束后可再次提交。' : '同步过程中时间略长,请稍后再试登录华住会。已推送、已排队和待核实的人员不会重复提交。'"></el-alert>
|
||||
<div class="huazhu-sync-options">
|
||||
<section class="huazhu-sync-option">
|
||||
<h3><i class="el-icon-user"></i>同步选中员工</h3>
|
||||
<p>将主页面已勾选的 {{selectedRows.length}} 名员工加入后台任务。需要调整人员时,关闭弹框后重新勾选。</p>
|
||||
<div class="huazhu-sync-selected">
|
||||
<el-tag v-for="row in selectedRows.slice(0, 6)" :key="row.id" size="small" type="info">{{row.username || row.loginname}}</el-tag>
|
||||
<el-tag v-if="selectedRows.length > 6" size="small" type="info">等 {{selectedRows.length}} 人</el-tag>
|
||||
<span v-if="!selectedRows.length" class="huazhu-sync-note">暂未勾选员工,请先在主页面选择。</span>
|
||||
</div>
|
||||
<el-button :disabled="!selectedRows.length || summary.running || summaryLoading" :loading="syncLoading" @click="syncSelected" type="primary" size="small">推送选中员工</el-button>
|
||||
</section>
|
||||
<section class="huazhu-sync-option">
|
||||
<h3><i class="el-icon-office-building"></i>同步规则内人员</h3>
|
||||
<p v-loading="ruleDescriptionLoading">{{ruleDescription || '读取同步范围后,可将全部符合规则的待推送人员加入后台任务。'}}</p>
|
||||
<div class="huazhu-sync-selected huazhu-sync-note">本操作不受当前搜索、分页和勾选范围影响。</div>
|
||||
<el-button :disabled="summary.running || summaryLoading || ruleDescriptionLoading || !ruleDescription" :loading="syncLoading" @click="syncRuleUsers" type="primary" size="small">推送规则内人员</el-button>
|
||||
<el-button v-if="!ruleDescription && !ruleDescriptionLoading" @click="loadRuleDescription" type="text" size="small">重新读取规则</el-button>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="进度与记录" name="records">
|
||||
<hua-zhu-sync-info ref="syncInfo" :key="selectedTaskId" :summary="summary" :summary-loading="summaryLoading" :updated-at="summaryUpdatedAt"
|
||||
:querying-user-id="queryingUserId" @query-status="queryStatus" @task-change="changeSummaryTask" @refresh="refreshResults" v-if="syncDialogVisible"></hua-zhu-sync-info>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
<template slot="footer">
|
||||
<div class="huazhu-sync-footer">
|
||||
<span class="huazhu-sync-note"><i class="el-icon-info"></i>更新只查看结果,不会重复推送;已推送人员自动从待推送列表隐藏。</span>
|
||||
<el-button @click="closeSyncManage" size="small">关闭</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<el-dialog append-to-body :visible.sync="statusDialogVisible" 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>
|
||||
@@ -43,38 +161,132 @@ layout("/layouts/platform.html"){
|
||||
</guava>
|
||||
</div>
|
||||
<script nonce="${cspNonce!}">
|
||||
<!--#include("./info.js"){}#-->
|
||||
new Vue({
|
||||
el: "#app",
|
||||
mixins: [initTableMixins],
|
||||
data: function () { return {pageForm: {searchKeyword: "", pageNumber: 1, pageSize: 10, totalCount: 0}, selectedRows: [], statusDialogVisible: false, statusData: null} },
|
||||
methods: {
|
||||
selectionChange: function (rows) { this.$set(this, "selectedRows", rows) },
|
||||
syncSelected: function () {
|
||||
var self = this;
|
||||
if (!this.selectedRows.length) { this.$message.warning("请先选择需要同步的员工"); return }
|
||||
this.$confirm("确定同步选中的 " + this.selectedRows.length + " 名员工到华住吗?", "提示", {type: "warning"}).then(function () {
|
||||
self.$set(self, "tableLoading", true);
|
||||
return $.post(loc() + "/syncUsers", {userIds: self.selectedRows.map(function (row) { return row.id })})
|
||||
}).then(function (res) {
|
||||
if (res && res.code === 0) {
|
||||
var failed = (res.data || []).filter(function (item) { return !item.success });
|
||||
self.$message.success("同步完成,成功 " + ((res.data || []).length - failed.length) + " 人,失败 " + failed.length + " 人");
|
||||
if (failed.length) { self.$alert(failed.map(function (item) { return item.userName + "(" + item.loginName + "):" + item.message }).join("\n"), "失败明细") }
|
||||
} else if (res) { self.$message.warning(res.msg || "同步失败") }
|
||||
}).catch(function () {}).then(function () { self.$set(self, "tableLoading", false) })
|
||||
},
|
||||
queryStatus: function (row) {
|
||||
var self = this;
|
||||
this.$set(this, "tableLoading", true);
|
||||
$.post(loc() + "/queryStatus", {userId: row.id}).then(function (res) {
|
||||
if (res.code === 0) { self.$set(self, "statusData", res.data); self.$set(self, "statusDialogVisible", true) } else { self.$message.warning(res.msg || "查询失败") }
|
||||
}).catch(function (xhr) {
|
||||
var message = xhr && xhr.responseJSON && xhr.responseJSON.msg;
|
||||
self.$message.error(message || "查询华住状态失败,请稍后重试");
|
||||
}).then(function () { self.$set(self, "tableLoading", false) })
|
||||
components: {HuaZhuSyncInfo: huaZhuSyncInfo},
|
||||
data() {
|
||||
return {
|
||||
selectedRows: [], syncLoading: false, summaryLoading: false, summaryRequestId: 0,
|
||||
summary: {task: null, counts: {}, tasks: [], running: false}, summaryUpdatedAt: null,
|
||||
selectedTaskId: "", syncDialogVisible: false, syncTab: "records",
|
||||
ruleDescription: "", ruleDescriptionLoading: false,
|
||||
queryingUserId: "", statusDialogVisible: false, statusData: null
|
||||
}
|
||||
},
|
||||
created: function () { this.pageData() }
|
||||
methods: {
|
||||
selectionChange(rows) { this.$set(this, "selectedRows", rows) },
|
||||
// 主页面只提供一个入口;每次打开读取最新批次,关闭不会影响后台任务。
|
||||
openSyncManage() {
|
||||
this.$set(this, "syncDialogVisible", true);
|
||||
this.$set(this, "syncTab", "records");
|
||||
this.$nextTick(() => { this.loadSummary("", true) });
|
||||
},
|
||||
closeSyncManage() { this.$set(this, "syncDialogVisible", false) },
|
||||
changeSyncTab(tab) {
|
||||
this.$set(this, "syncTab", tab.name);
|
||||
if (tab.name === "submit" && !this.ruleDescription && !this.ruleDescriptionLoading) { this.loadRuleDescription() }
|
||||
},
|
||||
// 只读取并展示实际单位名称和业务规则,不创建任务;提交时服务端会再次校验。
|
||||
loadRuleDescription() {
|
||||
this.$set(this, "ruleDescriptionLoading", true);
|
||||
return $.post(loc() + "/syncRuleDescription", {}).then((res) => {
|
||||
if (res.code === 0 && res.data) { this.$set(this, "ruleDescription", res.data.description || "") }
|
||||
}, () => { /* 中文错误由全局回调显示。 */ }).always(() => {
|
||||
this.$set(this, "ruleDescriptionLoading", false);
|
||||
});
|
||||
},
|
||||
// 固定主页面勾选范围,确认后只创建批次,实际推送由后台逐人执行。
|
||||
syncSelected() {
|
||||
if (this.syncLoading || this.summary.running || this.summaryLoading) { return }
|
||||
if (!this.selectedRows.length) { this.$message.warning("请先在主页面选择需要同步的员工"); return }
|
||||
const userIds = this.selectedRows.map((row) => row.id);
|
||||
this.$set(this, "syncLoading", true);
|
||||
this.confirmSync("确定将选中的 " + userIds.length + " 名员工加入后台推送任务吗?", "/syncUsers", {userIds: userIds});
|
||||
},
|
||||
syncRuleUsers() {
|
||||
if (this.syncLoading || this.summary.running || this.summaryLoading || !this.ruleDescription) { return }
|
||||
this.$set(this, "syncLoading", true);
|
||||
this.confirmSync("确定将符合以下规则的待推送人员加入后台任务吗? " + this.ruleDescription
|
||||
+ " 不受当前搜索、分页及勾选影响,已推送及待核实人员不会重复提交。", "/syncRuleUsers", {});
|
||||
},
|
||||
// path/params 为创建任务的接口和参数;用户取消时不提交。
|
||||
confirmSync(message, path, params) {
|
||||
this.$confirm(message, "同步确认", {
|
||||
type: "warning", confirmButtonText: "确定同步", cancelButtonText: "取消"
|
||||
}).then(() => this.submitSync(path, params)).catch((reason) => {
|
||||
// 旧版 jQuery 的网络失败仍会传到原生 Promise;jqXHR 的中文提示已由全局回调显示。
|
||||
if (reason && typeof reason.status === "number") { return }
|
||||
if (reason !== "cancel" && reason !== "close") { this.$message.error("提交过程异常,请更新结果确认任务是否已创建") }
|
||||
}).finally(() => { this.$set(this, "syncLoading", false) });
|
||||
},
|
||||
// data 为批次 id/total 等元数据;提交成功切到该批次记录,不等待后台处理完成。
|
||||
submitSync(path, params) {
|
||||
return $.post(loc() + path, params).then((res) => {
|
||||
if (!res || res.code !== 0) { return }
|
||||
this.$message.success("已创建 " + res.data.total + " 人的后台推送任务,可离开页面,稍后查看结果");
|
||||
this.$set(this, "selectedRows", []);
|
||||
this.$set(this, "syncTab", "records");
|
||||
this.$nextTick(() => { this.loadSummary(res.data.id, true) });
|
||||
this.doSearch();
|
||||
}, (xhr) => {
|
||||
// 旧版 jQuery 保留拒绝状态;传回 jqXHR,供确认 Promise 识别已由全局提示的网络错误。
|
||||
return xhr;
|
||||
}).always(() => {
|
||||
this.$set(this, "syncLoading", false);
|
||||
});
|
||||
},
|
||||
// taskId 为空读取最近批次;initializeRecords 只在打开弹框或新任务提交后重置记录范围。
|
||||
// 请求序号防止快速切换批次时,较慢的旧响应覆盖当前批次统计。
|
||||
loadSummary(taskId, initializeRecords = false) {
|
||||
const requestId = this.summaryRequestId + 1;
|
||||
this.$set(this, "summaryRequestId", requestId);
|
||||
this.$set(this, "summaryLoading", true);
|
||||
return $.post(loc() + "/syncSummary", {taskId: taskId || ""}).then((res) => {
|
||||
if (requestId !== this.summaryRequestId || !res || res.code !== 0) { return }
|
||||
this.$set(this, "summary", res.data);
|
||||
this.$set(this, "summaryUpdatedAt", Date.now());
|
||||
if (initializeRecords) {
|
||||
const selectedId = res.data.task ? res.data.task.id : "";
|
||||
this.$set(this, "selectedTaskId", selectedId);
|
||||
this.$nextTick(() => {
|
||||
if (requestId === this.summaryRequestId && this.$refs.syncInfo) { this.$refs.syncInfo.onOpen(selectedId) }
|
||||
});
|
||||
}
|
||||
}, () => { /* 全局回调已显示中文错误。 */ }).always(() => {
|
||||
if (requestId === this.summaryRequestId) { this.$set(this, "summaryLoading", false) }
|
||||
});
|
||||
},
|
||||
changeSummaryTask(taskId) {
|
||||
// 每个批次使用独立记录组件,旧批次迟到的分页响应只会更新已销毁的实例。
|
||||
this.$set(this, "selectedTaskId", taskId);
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.syncInfo) { this.$refs.syncInfo.onOpen(taskId) }
|
||||
});
|
||||
this.loadSummary(taskId);
|
||||
},
|
||||
// 仅刷新数据库统计、回执和主页面待推送名单,绝不调用任何推送接口。
|
||||
refreshResults() {
|
||||
this.loadSummary(this.selectedTaskId);
|
||||
if (this.$refs.syncInfo) { this.$refs.syncInfo.pageData() }
|
||||
this.doSearch();
|
||||
},
|
||||
// row.id 为本项目用户主键;本行独立显示进度,不阻塞列表或后台任务。
|
||||
queryStatus(row) {
|
||||
if (this.queryingUserId) { return }
|
||||
this.$set(this, "queryingUserId", row.id);
|
||||
$.post(loc() + "/queryStatus", {userId: row.id}).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.$set(this, "statusData", res.data);
|
||||
this.$set(this, "statusDialogVisible", true);
|
||||
}
|
||||
}, () => { /* 中文错误由全局回调显示一次。 */ }).always(() => {
|
||||
this.$set(this, "queryingUserId", "");
|
||||
});
|
||||
}
|
||||
},
|
||||
created() { this.pageData(); this.loadSummary("") }
|
||||
})
|
||||
</script>
|
||||
<!--#
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
const huaZhuSyncInfo = {
|
||||
template: /*language=HTML*/ `
|
||||
<div class="huazhu-sync-records">
|
||||
<div class="huazhu-sync-batch">
|
||||
<span class="huazhu-sync-batch-label">推送批次</span>
|
||||
<el-select :value="pageForm.taskId" @change="changeTask" placeholder="选择批次" size="small" :disabled="summaryLoading || tableLoading" aria-label="推送批次">
|
||||
<el-option label="全部历史批次" value=""></el-option>
|
||||
<el-option v-for="task in summary.tasks" :key="task.id" :value="task.id" :label="batchLabel(task)"></el-option>
|
||||
</el-select>
|
||||
<el-tag v-if="activeTask" :type="activeTask.status === 'FAILED' ? 'danger' : activeTask.status === 'RUNNING' ? '' : 'info'" size="small" effect="light">
|
||||
<i :class="activeTask.status === 'FAILED' ? 'el-icon-warning' : activeTask.status === 'RUNNING' ? 'el-icon-time' : 'el-icon-finished'"></i>
|
||||
{{taskStatusText}}
|
||||
</el-tag>
|
||||
<el-button :loading="summaryLoading || tableLoading" @click="refreshResults" icon="el-icon-refresh" size="small" plain type="primary">更新结果</el-button>
|
||||
</div>
|
||||
<el-alert v-if="summary.background && summary.background.message" class="huazhu-sync-submit-note" :title="summary.background.message" type="warning" show-icon :closable="false"></el-alert>
|
||||
<el-alert v-if="activeTask && activeTask.status === 'FAILED'" class="huazhu-sync-submit-note" title="本批次异常中止,已完成的结果已保留。请查看人员结果说明;未发送人员可重新提交,待核实人员请先查询华住状态。" type="error" show-icon :closable="false"></el-alert>
|
||||
<p v-if="summary.background" class="huazhu-sync-note">{{summary.background.lastTriggeredAt ? '本次应用启动后最近后台触发:' + $moment(summary.background.lastTriggeredAt).format('YYYY-MM-DD HH:mm:ss') : '本次应用启动后尚未收到后台执行回报'}}</p>
|
||||
<template v-if="activeTask">
|
||||
<div class="huazhu-sync-stats" v-loading="summaryLoading">
|
||||
<div v-for="item in statistics" :key="item.label" :class="['huazhu-sync-stat', item.type]">
|
||||
<strong>{{formatCount(item.value)}}</strong><span>{{item.label}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="huazhu-sync-progress">
|
||||
<span>已处理 {{formatCount(processedCount)}} / {{formatCount(activeTask.total)}} 人</span>
|
||||
<el-progress :percentage="progressPercentage" :show-text="false" :stroke-width="8" color="#006bb8"></el-progress>
|
||||
<span>{{progressPercentage}}%</span>
|
||||
<span class="huazhu-sync-updated">{{updatedAt ? '更新于 ' + $moment(updatedAt).format('HH:mm:ss') : ''}}</span>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else class="huazhu-sync-empty" v-loading="summaryLoading">
|
||||
{{summary.tasks.length ? '当前查看全部历史记录。选择具体批次,可查看本批次统计和进度。' : '暂无推送任务。可切换到发起推送,选择勾选员工或规则内人员进行同步。'}}
|
||||
</div>
|
||||
<div class="huazhu-sync-filter">
|
||||
<label>员工信息</label>
|
||||
<el-input v-model="pageForm.searchKeyword" clearable placeholder="输入工号或姓名" @keyup.enter.native="doSearch" size="small" suffix-icon="el-icon-search" aria-label="员工信息"></el-input>
|
||||
<label class="filter-status">处理结果</label>
|
||||
<el-select :value="pageForm.status" @change="changeStatus" :disabled="tableLoading" size="small" aria-label="处理结果">
|
||||
<el-option label="全部状态" value=""></el-option>
|
||||
<el-option v-for="(label, value) in statusLabels" :key="value" :label="label" :value="value"></el-option>
|
||||
</el-select>
|
||||
<el-button @click="doSearch" :disabled="tableLoading" size="small" type="primary">查询</el-button>
|
||||
</div>
|
||||
<el-table :data="tableData" v-loading="tableLoading" :size="tableSize" :max-height="320" class="vi-table huazhu-sync-record-table" empty-text="暂无符合条件的推送记录">
|
||||
<el-table-column label="工号" prop="loginName" min-width="110"></el-table-column>
|
||||
<el-table-column label="姓名" prop="userName" min-width="90"></el-table-column>
|
||||
<el-table-column label="处理结果" min-width="125">
|
||||
<template slot-scope="{row}">
|
||||
<span :class="'huazhu-result-' + (row.status || '').toLowerCase()"><i :class="['result-icon', statusIcons[row.status]]"></i>{{statusLabels[row.status] || row.status}}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="处理时间" min-width="165">
|
||||
<template slot-scope="{row}">{{row.finishedAt ? $moment(row.finishedAt).format('YYYY-MM-DD HH:mm:ss') : '--'}}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="结果说明" min-width="270" show-overflow-tooltip>
|
||||
<template slot-scope="{row}">{{resultMessage(row)}}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="100" fixed="right">
|
||||
<template slot-scope="{row}"><el-button size="mini" type="text" :loading="queryingUserId === row.userId" :disabled="!!queryingUserId" @click="queryStatus(row)">查询状态</el-button></template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<!--#include("/layouts/pagination.html"){}#-->
|
||||
<slot></slot>
|
||||
</div>
|
||||
`,
|
||||
mixins: [initTableMixins],
|
||||
props: {
|
||||
summary: {type: Object, required: true},
|
||||
summaryLoading: {type: Boolean, default: false},
|
||||
updatedAt: {type: Number, default: null},
|
||||
queryingUserId: {type: String, default: ""}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
pageDataUrl: loc() + "/recordPageData",
|
||||
statusLabels: {PENDING: "等待推送", SENDING: "正在推送", SUCCESS: "推送成功", EXISTS: "华住已存在", FAILED: "推送失败", UNKNOWN: "待核实"},
|
||||
statusIcons: {PENDING: "el-icon-time", SENDING: "el-icon-loading", SUCCESS: "el-icon-success", EXISTS: "el-icon-circle-check", FAILED: "el-icon-error", UNKNOWN: "el-icon-warning"}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
// 数据库 RUNNING 只表示批次未结束,实际进入后台循环才显示正在处理。
|
||||
taskStatusText() {
|
||||
if (!this.activeTask) { return "" }
|
||||
if (this.activeTask.status === "FAILED") { return "异常中止" }
|
||||
if (this.activeTask.status !== "RUNNING") { return "处理结束" }
|
||||
return this.summary.background && this.summary.background.executingTaskId === this.activeTask.id ? "后台处理中" : "等待后台执行";
|
||||
},
|
||||
// 全部历史记录不展示单批次统计;批次切换期间不把上一个批次的计数展示为当前结果。
|
||||
activeTask() {
|
||||
return this.summary.task && this.summary.task.id === this.pageForm.taskId ? this.summary.task : null;
|
||||
},
|
||||
statistics() {
|
||||
const counts = this.summary.counts || {};
|
||||
return [
|
||||
{label: "总人数", value: this.activeTask ? this.activeTask.total : 0, type: "total"},
|
||||
{label: "推送成功", value: counts.SUCCESS || 0, type: "success"},
|
||||
{label: "华住已存在", value: counts.EXISTS || 0, type: "exists"},
|
||||
{label: "失败", value: counts.FAILED || 0, type: "failed"},
|
||||
{label: "待核实", value: counts.UNKNOWN || 0, type: "unknown"},
|
||||
{label: "待处理", value: (counts.PENDING || 0) + (counts.SENDING || 0), type: "pending"}
|
||||
];
|
||||
},
|
||||
processedCount() {
|
||||
if (!this.activeTask) { return 0 }
|
||||
const counts = this.summary.counts || {};
|
||||
return Math.max(0, Math.min(this.activeTask.total, (counts.SUCCESS || 0) + (counts.EXISTS || 0) + (counts.FAILED || 0) + (counts.UNKNOWN || 0)));
|
||||
},
|
||||
progressPercentage() {
|
||||
return this.activeTask && this.activeTask.total > 0 ? Math.round(this.processedCount * 100 / this.activeTask.total) : 0;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// taskId 为打开时的批次主键,空值查全部;查询、分页、加载状态均复用 initTableMixins。
|
||||
onOpen(taskId) {
|
||||
this.$set(this.pageForm, "taskId", taskId || "");
|
||||
this.$set(this.pageForm, "status", "");
|
||||
this.doSearch();
|
||||
},
|
||||
changeTask(value) {
|
||||
this.$set(this.pageForm, "taskId", value);
|
||||
// 父组件按批次重建记录实例,再调用 onOpen 复用分页查询,隔离旧批次的未完成请求。
|
||||
this.$emit("task-change", value);
|
||||
},
|
||||
changeStatus(value) { this.$set(this.pageForm, "status", value); this.doSearch() },
|
||||
refreshResults() { this.$emit("refresh") },
|
||||
queryStatus(row) { this.$emit("query-status", {id: row.userId}) },
|
||||
batchLabel(task) {
|
||||
return this.$moment(task.submittedAt).format("YYYY-MM-DD HH:mm:ss") + " · " + (task.source === "RULE" ? "规则内人员" : "选中员工");
|
||||
},
|
||||
formatCount(value) { return Number(value || 0).toLocaleString("zh-CN") },
|
||||
// 服务端保存脱敏原始回执;表格优先显示中文 message,未知文本保留用于核实。
|
||||
resultMessage(row) {
|
||||
try {
|
||||
const receipt = JSON.parse(row.message);
|
||||
return receipt.message || receipt.msg || row.message || "--";
|
||||
} catch (ignored) {
|
||||
return row.message || "--";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,8 @@ layout("/layouts/platform.html"){
|
||||
<custom-card>
|
||||
<snaker-start slot="header" label="补贴申请" define_key="ZGWW">
|
||||
<template slot="header-right-label">
|
||||
<el-button v-if="$auth.hasRole('SYSADMIN')" type="text" class="mr10"
|
||||
<!-- 系统管理员和校工会管理员均可维护全校慰问申报截止日期。 -->
|
||||
<el-button v-if="$auth.hasRoleOr(['SYSADMIN', 'SCHOOL_UNION_ADMIN'])" type="text" class="mr10"
|
||||
@click="openApplyPeriodDialog">申报时间设置</el-button>
|
||||
</template>
|
||||
</snaker-start>
|
||||
|
||||
@@ -138,6 +138,8 @@ layout("/layouts/platform.html"){
|
||||
{prop: 'applyUnitName', label: '所属单位'},
|
||||
{prop: 'typeName', label: '补贴类型'},
|
||||
{prop: 'taskName', label: '当前节点'},
|
||||
// 展示业务申请时间,与详情页及月份筛选使用的 createTime 一致。
|
||||
{prop: 'createTime', label: '申请时间', width: 180},
|
||||
{prop: 'instanceState', label: '流程状态'},
|
||||
],
|
||||
unionList: [],
|
||||
|
||||
@@ -9,8 +9,13 @@ layout("/layouts/platform.html"){
|
||||
.difficult-apply .nowrap-select .el-select__tags { flex-wrap: nowrap; overflow: hidden; }
|
||||
</style>
|
||||
<div id="app" class="difficult-apply" v-cloak>
|
||||
<el-card shadow="never">
|
||||
<snaker-start slot="header" label="特困补助申请" define_key="TKBZ"></snaker-start>
|
||||
<custom-card>
|
||||
<snaker-start slot="header" label="特困补助申请" define_key="TKBZ">
|
||||
<template slot="header-right-label">
|
||||
<el-button v-if="$auth.hasRoleOr(['SYSADMIN', 'SCHOOL_UNION_ADMIN'])"
|
||||
type="text" class="mr10" @click="openApplyPeriodDialog">申报时间设置</el-button>
|
||||
</template>
|
||||
</snaker-start>
|
||||
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="0"
|
||||
label-suffix=":" class="flow-task-form">
|
||||
<el-descriptions :column="3" border>
|
||||
@@ -98,9 +103,9 @@ layout("/layouts/platform.html"){
|
||||
<template slot-scope="scope">
|
||||
<el-form-item :prop="'hospitalRecords.' + scope.$index + '.selfFundedAmount'"
|
||||
:rules="conditionalRule(isAccidentSubsidy(scope.row))">
|
||||
<el-input-number v-model="scope.row.selfFundedAmount" :min="0" :precision="2"
|
||||
<el-input v-model="scope.row.selfFundedAmount" type="number"
|
||||
:disabled="!isAccidentSubsidy(scope.row)"
|
||||
style="width:100%"></el-input-number>
|
||||
style="width:100%"></el-input>
|
||||
</el-form-item>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -158,6 +163,7 @@ layout("/layouts/platform.html"){
|
||||
<el-form-item prop="applyFile" :rules="requiredRule('请上传纸质申请表')">
|
||||
<file-upload :value.sync="formData.applyFile" upload_mode="drag"
|
||||
:upload_number="1" upload_result_category="array"
|
||||
accept=".pdf,.jpg,.jpeg,.png,.gif,.bmp,.webp,.tif,.tiff" :validate_accept="true"
|
||||
complete_result></file-upload>
|
||||
</el-form-item>
|
||||
</el-descriptions-item>
|
||||
@@ -188,11 +194,27 @@ layout("/layouts/platform.html"){
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-form>
|
||||
<el-row type="flex" justify="end" class="mt20">
|
||||
<el-button v-if="!isSchoolAuditEdit" type="primary" plain @click="onSave">保存</el-button>
|
||||
<el-button type="primary" @click="onSubmit">提交</el-button>
|
||||
</el-row>
|
||||
</el-card>
|
||||
<template slot="footer">
|
||||
<template v-if="bizId || isSchoolAuditEdit || (applyPeriodLoaded && !applyClosed)">
|
||||
<el-button v-if="!isSchoolAuditEdit" type="primary" plain :loading="formLoading" @click="onSave">保存</el-button>
|
||||
<el-button type="primary" :loading="formLoading" @click="onSubmit">提交</el-button>
|
||||
</template>
|
||||
</template>
|
||||
</custom-card>
|
||||
<el-dialog title="申报时间设置" :visible="applyPeriodDialogVisible" width="460px"
|
||||
:close-on-click-modal="false" @close="closeApplyPeriodDialog">
|
||||
<el-form ref="applyPeriodFormRef" :model="applyPeriodForm" :rules="applyPeriodRules" label-width="120px">
|
||||
<el-form-item label="申报截止日期" prop="endDate">
|
||||
<el-date-picker v-model="applyPeriodForm.endDate" type="date" value-format="yyyy-MM-dd"
|
||||
placeholder="请选择截止日期" style="width:100%"></el-date-picker>
|
||||
</el-form-item>
|
||||
<el-alert title="截止日期当天起停止新增申报,已有申请可继续办理。" type="info" :closable="false"></el-alert>
|
||||
</el-form>
|
||||
<template slot="footer">
|
||||
<el-button @click="closeApplyPeriodDialog">取消</el-button>
|
||||
<el-button type="primary" :loading="applyPeriodSaving" @click="saveApplyEndDate">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
<script nonce="${cspNonce!}">
|
||||
new Vue({
|
||||
@@ -224,10 +246,26 @@ layout("/layouts/platform.html"){
|
||||
projectList: [],
|
||||
diseaseList: [],
|
||||
userOptions: [],
|
||||
userLoading: false
|
||||
userLoading: false,
|
||||
applyEndDate: "",
|
||||
applyPeriodLoaded: false,
|
||||
applyPeriodDialogVisible: false,
|
||||
applyPeriodSaving: false,
|
||||
applyPeriodForm: {endDate: ""},
|
||||
applyPeriodRules: {
|
||||
endDate: [{required: true, message: "请选择申报截止日期", trigger: "change"}]
|
||||
}
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
// 空配置不限制申请;错误配置关闭新增入口,最终仍由后端校验日期。
|
||||
applyClosed() {
|
||||
if (!this.applyEndDate) {
|
||||
return false;
|
||||
}
|
||||
var deadline = this.$moment(this.applyEndDate, "YYYY-MM-DD", true);
|
||||
return !deadline.isValid() || !this.$moment().startOf("day").isBefore(deadline);
|
||||
},
|
||||
selectedProjects: function () {
|
||||
var selected = this.formData.subsidyType || [];
|
||||
return this.projectList.filter(function (item) {
|
||||
@@ -236,6 +274,78 @@ layout("/layouts/platform.html"){
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 读取 Result.data 中的 yyyy-MM-dd 日期,读取成功前不开放新申请按钮。
|
||||
queryApplyEndDate() {
|
||||
return this.$axios.post("/platform/difficultSubsidy/apply/getApplyEndDate").then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.$set(this, "applyEndDate", res.data || "");
|
||||
this.$set(this, "applyPeriodLoaded", true);
|
||||
}
|
||||
});
|
||||
},
|
||||
// 与慰问申请一致:截止时显示“温馨提示”,关闭弹框后留在当前页,便于管理员设置申报时间。
|
||||
showApplyClosedAlert() {
|
||||
this.$alert("本次申报已截止请关注下次申报", "温馨提示", {
|
||||
confirmButtonText: "确定",
|
||||
callback: () => {}
|
||||
});
|
||||
},
|
||||
openApplyPeriodDialog() {
|
||||
this.queryApplyEndDate().then(() => {
|
||||
if (!this.applyPeriodLoaded) {
|
||||
return;
|
||||
}
|
||||
this.$set(this.applyPeriodForm, "endDate", this.applyEndDate);
|
||||
this.$set(this, "applyPeriodDialogVisible", true);
|
||||
this.$nextTick(() => {
|
||||
this.$refs.applyPeriodFormRef.clearValidate();
|
||||
});
|
||||
});
|
||||
},
|
||||
closeApplyPeriodDialog() {
|
||||
this.$set(this, "applyPeriodDialogVisible", false);
|
||||
},
|
||||
// endDate 为日期字符串,后端统一校验管理角色及格式;成功后立即刷新本页截止状态。
|
||||
saveApplyEndDate() {
|
||||
if (this.applyPeriodSaving) {
|
||||
return;
|
||||
}
|
||||
this.$refs.applyPeriodFormRef.validate((valid) => {
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
var endDate = this.applyPeriodForm.endDate;
|
||||
this.$set(this, "applyPeriodSaving", true);
|
||||
this.$axios.post("/platform/difficultSubsidy/apply/saveApplyEndDate", {endDate: endDate})
|
||||
.then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.$set(this, "applyEndDate", endDate);
|
||||
this.closeApplyPeriodDialog();
|
||||
this.$message.success("申报时间设置成功");
|
||||
}
|
||||
}).finally(() => {
|
||||
this.$set(this, "applyPeriodSaving", false);
|
||||
});
|
||||
});
|
||||
},
|
||||
// 已有申请和审核修改不受新申请截止限制,后端会核实申请ID确实存在。
|
||||
canSaveApplication() {
|
||||
if (this.formLoading) {
|
||||
return false;
|
||||
}
|
||||
if (this.bizId || this.isSchoolAuditEdit) {
|
||||
return true;
|
||||
}
|
||||
if (!this.applyPeriodLoaded) {
|
||||
this.$message.warning("申报时间尚未加载,请刷新页面后重试");
|
||||
return false;
|
||||
}
|
||||
if (this.applyClosed) {
|
||||
this.showApplyClosedAlert();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
requiredRule: function (message) {
|
||||
return [{required: true, message: message, trigger: ["change", "blur"]}];
|
||||
},
|
||||
@@ -299,23 +409,21 @@ layout("/layouts/platform.html"){
|
||||
this.$set(data, "files", []);
|
||||
}
|
||||
},
|
||||
queryRecipients: function (keyword, selectFirst) {
|
||||
var self = this;
|
||||
queryRecipients(keyword, selectFirst) {
|
||||
if (!keyword) {
|
||||
this.$set(this, "userOptions", []);
|
||||
return;
|
||||
}
|
||||
this.$set(this, "userLoading", true);
|
||||
this.$axios.post("/platform/difficultSubsidy/apply/queryRecipients", {key: keyword}).then(function (res) {
|
||||
this.$axios.post("/platform/difficultSubsidy/apply/queryRecipients", {key: keyword}).then((res) => {
|
||||
var users = res.code === 0 ? (res.data || []) : [];
|
||||
self.$set(self, "userOptions", users);
|
||||
self.$set(self, "userLoading", false);
|
||||
this.$set(this, "userOptions", users);
|
||||
if (selectFirst && users.length > 0) {
|
||||
self.$set(self.formData, "userId", users[0].id);
|
||||
self.setSelectedUser(users[0]);
|
||||
this.$set(this.formData, "userId", users[0].id);
|
||||
this.setSelectedUser(users[0]);
|
||||
}
|
||||
}).catch(function () {
|
||||
self.$set(self, "userLoading", false);
|
||||
}).finally(() => {
|
||||
this.$set(this, "userLoading", false);
|
||||
});
|
||||
},
|
||||
userChange: function (userId) {
|
||||
@@ -389,62 +497,71 @@ layout("/layouts/platform.html"){
|
||||
removeHospitalRecord: function (index) {
|
||||
this.formData.hospitalRecords.splice(index, 1);
|
||||
},
|
||||
onSave: function () {
|
||||
var self = this;
|
||||
onSave() {
|
||||
if (!this.canSaveApplication()) {
|
||||
return;
|
||||
}
|
||||
this.$confirm("您确定保存吗?", "提示", {
|
||||
confirmButtonText: "确定", cancelButtonText: "取消", type: "warning"
|
||||
}).then(function () {
|
||||
self.$axios.post("/platform/difficultSubsidy/apply/save", {
|
||||
data: JSON.stringify(self.formData)
|
||||
}).then(function (res) {
|
||||
if (res.code === 0) {
|
||||
self.$message.success("保存成功");
|
||||
commonUtil.pjaxPush("/platform/difficultSubsidy/mine");
|
||||
}
|
||||
});
|
||||
}).then(() => {
|
||||
this.sendApplication("/platform/difficultSubsidy/apply/save", {
|
||||
data: JSON.stringify(this.formData)
|
||||
}, "保存成功", "/platform/difficultSubsidy/mine");
|
||||
});
|
||||
},
|
||||
onSubmit: function () {
|
||||
var self = this;
|
||||
this.$refs.formRef.validate(function (valid) {
|
||||
onSubmit() {
|
||||
if (!this.canSaveApplication()) {
|
||||
return;
|
||||
}
|
||||
this.$refs.formRef.validate((valid) => {
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
if (!self.formData.hospitalRecords || self.formData.hospitalRecords.length === 0) {
|
||||
self.$message.warning("请填写住院记录");
|
||||
if (!this.formData.hospitalRecords || this.formData.hospitalRecords.length === 0) {
|
||||
this.$message.warning("请填写住院记录");
|
||||
return;
|
||||
}
|
||||
self.$confirm("您确定要提交吗?", "提示", {
|
||||
this.$confirm("您确定要提交吗?", "提示", {
|
||||
confirmButtonText: "确定", cancelButtonText: "取消", type: "warning"
|
||||
}).then(function () {
|
||||
if (self.isSchoolAuditEdit) {
|
||||
self.$axios.post("/platform/difficultSubsidy/schoolAudit/modifyApply", {
|
||||
processTaskId: self.taskId,
|
||||
data: JSON.stringify(self.formData)
|
||||
}).then(function (res) {
|
||||
if (res.code === 0) {
|
||||
self.$message.success("修改成功");
|
||||
commonUtil.pjaxPush("/platform/difficultSubsidy/schoolAudit");
|
||||
}
|
||||
});
|
||||
}).then(() => {
|
||||
if (this.isSchoolAuditEdit) {
|
||||
this.sendApplication("/platform/difficultSubsidy/schoolAudit/modifyApply", {
|
||||
processTaskId: this.taskId,
|
||||
data: JSON.stringify(this.formData)
|
||||
}, "修改成功", "/platform/difficultSubsidy/schoolAudit");
|
||||
return;
|
||||
}
|
||||
self.$axios.post("/platform/difficultSubsidy/apply/submit", {
|
||||
data: JSON.stringify(self.formData)
|
||||
}).then(function (res) {
|
||||
if (res.code === 0) {
|
||||
self.$message.success("提交成功");
|
||||
commonUtil.pjaxPush("/platform/difficultSubsidy/mine");
|
||||
}
|
||||
});
|
||||
this.sendApplication("/platform/difficultSubsidy/apply/submit", {
|
||||
data: JSON.stringify(this.formData)
|
||||
}, "提交成功", "/platform/difficultSubsidy/mine");
|
||||
});
|
||||
});
|
||||
},
|
||||
// url/params 对应草稿、提交或审核修改接口;Result.code 为0时提示并跳转到对应列表。
|
||||
sendApplication(url, params, successMessage, redirectUrl) {
|
||||
if (!this.canSaveApplication()) {
|
||||
return;
|
||||
}
|
||||
this.$set(this, "formLoading", true);
|
||||
this.$axios.post(url, params).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.$message.success(successMessage);
|
||||
commonUtil.pjaxPush(redirectUrl);
|
||||
}
|
||||
}).finally(() => {
|
||||
this.$set(this, "formLoading", false);
|
||||
});
|
||||
}
|
||||
},
|
||||
created: function () {
|
||||
var self = this;
|
||||
this.loadProject().then(function () {
|
||||
self.loadDetail();
|
||||
created() {
|
||||
// 仅新申请在截止日期读取成功后弹出提示,已有申请和校工会审核修改仍可继续办理。
|
||||
this.queryApplyEndDate().then(() => {
|
||||
if (this.applyPeriodLoaded && !this.bizId && !this.isSchoolAuditEdit && this.applyClosed) {
|
||||
this.showApplyClosedAlert();
|
||||
}
|
||||
});
|
||||
this.loadProject().then(() => {
|
||||
this.loadDetail();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -3,79 +3,448 @@ layout("/layouts/platform_h5.html"){
|
||||
#-->
|
||||
|
||||
<style>
|
||||
.list-card {
|
||||
border-radius: 0;
|
||||
body {
|
||||
background-color: #f3f7fd;
|
||||
}
|
||||
|
||||
.list-card-body {
|
||||
/* width: calc(100% - 140px);
|
||||
display: flex;*/
|
||||
width: calc(100% - 140px);
|
||||
.culture-activity-page {
|
||||
min-height: 100vh;
|
||||
padding-bottom: calc(24px + env(safe-area-inset-bottom));
|
||||
box-sizing: border-box;
|
||||
background-color: #f3f7fd;
|
||||
}
|
||||
|
||||
.culture-activity-page .van-nav-bar {
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.culture-activity-page .van-nav-bar__title {
|
||||
color: #1f2937;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.culture-activity-page .van-nav-bar .van-icon,
|
||||
.culture-activity-page .van-nav-bar__text {
|
||||
color: #397fbd;
|
||||
}
|
||||
|
||||
.culture-activity-content {
|
||||
padding: 12px 12px 0;
|
||||
}
|
||||
|
||||
.culture-activity-hero {
|
||||
position: relative;
|
||||
height: 134px;
|
||||
overflow: hidden;
|
||||
border-radius: 10px;
|
||||
color: #fff;
|
||||
background-color: #247fe8;
|
||||
box-shadow: 0 8px 20px rgba(51, 117, 219, .18);
|
||||
}
|
||||
|
||||
.culture-activity-hero__image {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
}
|
||||
|
||||
.culture-activity-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%);
|
||||
}
|
||||
|
||||
.culture-activity-hero__title {
|
||||
font-size: 25px;
|
||||
line-height: 34px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 1px;
|
||||
text-shadow: 0 2px 6px rgba(17, 86, 182, .18);
|
||||
}
|
||||
|
||||
.culture-activity-hero__subtitle {
|
||||
margin-top: 4px;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
letter-spacing: .5px;
|
||||
opacity: .95;
|
||||
}
|
||||
|
||||
.culture-activity-sticky {
|
||||
padding: 10px 0 1px;
|
||||
background-color: #f3f7fd;
|
||||
}
|
||||
|
||||
.van-sticky--fixed .culture-activity-sticky {
|
||||
padding-right: 12px;
|
||||
padding-left: 12px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.culture-activity-search {
|
||||
margin: 0 0 8px;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
border-radius: 12px;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 5px 16px rgba(43, 73, 112, .1);
|
||||
}
|
||||
|
||||
.culture-activity-search .van-search__content {
|
||||
height: 42px;
|
||||
padding-left: 12px;
|
||||
align-items: center;
|
||||
border-radius: 12px;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.culture-activity-search .van-field__control {
|
||||
color: #263548;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.culture-activity-search .van-field__control::placeholder {
|
||||
color: #a7b0bf;
|
||||
}
|
||||
|
||||
.culture-activity-filter {
|
||||
margin: 0 0 10px;
|
||||
overflow: hidden;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 5px 16px rgba(43, 73, 112, .08);
|
||||
transition: border-radius .18s ease;
|
||||
}
|
||||
|
||||
.culture-activity-filter.is-open {
|
||||
border-radius: 10px 10px 0 0;
|
||||
}
|
||||
|
||||
.culture-activity-filter .van-dropdown-menu__bar {
|
||||
height: 44px;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.culture-activity-filter .van-dropdown-menu__item {
|
||||
min-width: 0;
|
||||
flex: 1 1 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.culture-activity-filter .van-dropdown-menu__item:last-child {
|
||||
flex-grow: 1.35;
|
||||
}
|
||||
|
||||
.culture-activity-filter .van-dropdown-menu__title {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
padding: 0 15px 0 4px;
|
||||
overflow: hidden;
|
||||
color: #66758a;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.culture-activity-filter .van-dropdown-menu__title::after {
|
||||
right: 5px;
|
||||
}
|
||||
|
||||
.culture-activity-filter .van-dropdown-menu__title--active,
|
||||
.culture-activity-filter .van-dropdown-item__option--active,
|
||||
.culture-activity-filter .van-dropdown-item__option--active .van-dropdown-item__icon {
|
||||
color: #1989fa;
|
||||
}
|
||||
|
||||
.culture-activity-filter .van-dropdown-menu__title--down::after {
|
||||
border-color: transparent transparent #1989fa #1989fa;
|
||||
}
|
||||
|
||||
.culture-activity-filter .van-dropdown-item__content {
|
||||
width: calc(100% - 24px);
|
||||
margin: 0 12px;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
border-radius: 0 0 12px 12px;
|
||||
box-shadow: 0 10px 24px rgba(31, 65, 108, .14);
|
||||
}
|
||||
|
||||
.culture-activity-filter .van-dropdown-item__option {
|
||||
min-height: 48px;
|
||||
padding: 0 16px;
|
||||
color: #46566c;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.culture-activity-filter .van-cell::after {
|
||||
right: 16px;
|
||||
left: 16px;
|
||||
}
|
||||
|
||||
.culture-activity-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.list-card-body .title {
|
||||
.culture-activity-card {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: 10px;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 7px 20px rgba(55, 85, 126, .08);
|
||||
}
|
||||
|
||||
.culture-activity-card__cover,
|
||||
.culture-activity-card__cover-state {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 150px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
background-color: #edf4fc;
|
||||
}
|
||||
|
||||
.culture-activity-card__cover {
|
||||
display: block;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.culture-activity-card__cover-state {
|
||||
flex-direction: column;
|
||||
row-gap: 8px;
|
||||
color: #8da1ba;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.culture-activity-card__cover-state .van-icon {
|
||||
font-size: 34px;
|
||||
}
|
||||
|
||||
.culture-activity-card__cover-state.is-error {
|
||||
color: #c27b75;
|
||||
background-color: #fff3f1;
|
||||
}
|
||||
|
||||
.culture-activity-card__body {
|
||||
padding: 12px 38px 14px 14px;
|
||||
}
|
||||
|
||||
.culture-activity-card__title {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
color: #303846;
|
||||
font-size: 14px;
|
||||
line-height: 21px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.culture-activity-card__time {
|
||||
margin-top: 4px;
|
||||
color: #98a2b2;
|
||||
font-size: 12px;
|
||||
line-height: 17px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.culture-activity-card__meta {
|
||||
margin-top: 10px;
|
||||
padding-top: 9px;
|
||||
border-top: 1px solid #edf1f6;
|
||||
}
|
||||
|
||||
.culture-activity-card__address {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
color: #69788d;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.culture-activity-card__address .van-icon {
|
||||
margin: 2px 5px 0 0;
|
||||
flex: none;
|
||||
color: #6c9ff8;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.culture-activity-card__address span {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 1.1em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.list-card-body .time {
|
||||
color: #9a9696;
|
||||
line-height: 20px;
|
||||
.culture-activity-card__tags {
|
||||
display: flex;
|
||||
margin-top: 8px;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.list-card-body .footer {
|
||||
text-align: right;
|
||||
.culture-activity-card__tag {
|
||||
display: inline-flex;
|
||||
padding: 3px 8px;
|
||||
align-items: center;
|
||||
border-radius: 4px;
|
||||
color: #397de0;
|
||||
background-color: #edf5ff;
|
||||
font-size: 11px;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.list-card-body .footer .tag {
|
||||
border: 1px solid transparent;
|
||||
padding: 5px;
|
||||
border-radius: 5px;
|
||||
.culture-activity-card__tag-label {
|
||||
margin-right: 4px;
|
||||
color: #8aa7cf;
|
||||
}
|
||||
.list-card-img img{
|
||||
width: 100%;
|
||||
|
||||
.culture-activity-card__arrow {
|
||||
position: absolute;
|
||||
top: 164px;
|
||||
right: 14px;
|
||||
color: #b4bdca;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.culture-activity-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;
|
||||
}
|
||||
|
||||
.culture-activity-empty img {
|
||||
display: block;
|
||||
width: 82%;
|
||||
max-width: 280px;
|
||||
height: auto;
|
||||
border-radius: 12px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.culture-activity-empty__title {
|
||||
margin-top: 14px;
|
||||
color: #50627a;
|
||||
font-size: 15px;
|
||||
line-height: 22px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.culture-activity-empty__hint {
|
||||
margin-top: 5px;
|
||||
color: #9aa7b8;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.culture-activity-page .van-list__finished-text,
|
||||
.culture-activity-page .van-list__loading {
|
||||
color: #9aa7b8;
|
||||
font-size: 13px;
|
||||
line-height: 44px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="app" v-cloak>
|
||||
<van-nav-bar title="活动列表" @click-left="historyBack" left-arrow left-text="返回" placeholder fixed></van-nav-bar>
|
||||
<van-sticky offset-top="46px">
|
||||
<van-dropdown-menu>
|
||||
<year-van-dropdown-item :num="5" @change="doSearch" v-model="pageForm.year"></year-van-dropdown-item>
|
||||
<van-dropdown-item :options="stateList" @change="doSearch" v-model="pageForm.state"></van-dropdown-item>
|
||||
<van-dropdown-item :options="isEnrolledOptions" @change="doSearch" v-model="pageForm.isEnrolled"></van-dropdown-item>
|
||||
<van-dropdown-item :options="activityTypeOptions" @change="doSearch" v-model="pageForm.activity_type"></van-dropdown-item>
|
||||
</van-dropdown-menu>
|
||||
</van-sticky>
|
||||
<table-list api="/platform/activity/culture/applyUser/pageData"
|
||||
:page_form.sync="pageForm"
|
||||
ref="tableListRef"
|
||||
title="name"
|
||||
img="cover"
|
||||
@ready="onReady"
|
||||
>
|
||||
<template v-slot="{index,row}">
|
||||
<table-column label="报名开始时间">
|
||||
{{$moment(row.startTime).format('MM/DD HH:mm')}}
|
||||
</table-column>
|
||||
<table-column label="报名结束时间">
|
||||
{{$moment(row.endTime).format('MM/DD HH:mm')}}
|
||||
</table-column>
|
||||
</template>
|
||||
<template #actions="{index,row}">
|
||||
<template v-if="$moment().isBefore($moment(row.endTime))">
|
||||
<div class="action-btn" @click="openDetail(row)">
|
||||
<i class="fa fa-edit"></i>
|
||||
<span>去报名</span>
|
||||
<div class="culture-activity-page">
|
||||
<van-nav-bar title="文化活动" @click-left="historyBack" left-arrow left-text="返回" placeholder fixed></van-nav-bar>
|
||||
|
||||
<div class="culture-activity-content">
|
||||
<section class="culture-activity-hero">
|
||||
<img class="culture-activity-hero__image" src="/assets/mobile/img/activity/hero/culture-hero-v3.png" alt="文化活动插画">
|
||||
<div class="culture-activity-hero__copy">
|
||||
<div class="culture-activity-hero__title">文化活动</div>
|
||||
<div class="culture-activity-hero__subtitle">丰富生活 · 乐享校园</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</table-list>
|
||||
</section>
|
||||
|
||||
<van-sticky :offset-top="46">
|
||||
<div class="culture-activity-sticky">
|
||||
<van-search
|
||||
v-model="searchKeyword"
|
||||
class="culture-activity-search"
|
||||
placeholder="搜索文化活动"
|
||||
shape="round"
|
||||
clearable
|
||||
@search="doSearch"
|
||||
@clear="doSearch"
|
||||
></van-search>
|
||||
<van-dropdown-menu class="culture-activity-filter" :class="{'is-open': filterOpened}" active-color="#1989fa">
|
||||
<year-van-dropdown-item :num="5" @change="doSearch" @open="$set($data, 'filterOpened', true)" @close="$set($data, 'filterOpened', false)" v-model="pageForm.year"></year-van-dropdown-item>
|
||||
<van-dropdown-item :options="stateList" @change="doSearch" @open="$set($data, 'filterOpened', true)" @close="$set($data, 'filterOpened', false)" v-model="pageForm.state"></van-dropdown-item>
|
||||
<van-dropdown-item :options="isEnrolledOptions" @change="doSearch" @open="$set($data, 'filterOpened', true)" @close="$set($data, 'filterOpened', false)" v-model="pageForm.isEnrolled"></van-dropdown-item>
|
||||
<van-dropdown-item :options="activityTypeOptions" @change="doSearch" @open="$set($data, 'filterOpened', true)" @close="$set($data, 'filterOpened', false)" v-model="pageForm.activity_type"></van-dropdown-item>
|
||||
</van-dropdown-menu>
|
||||
</div>
|
||||
</van-sticky>
|
||||
|
||||
<van-pull-refresh v-model="refreshing" @refresh="onRefresh">
|
||||
<van-list v-model="loading" :finished="finished" @load="onLoad">
|
||||
<div class="culture-activity-list">
|
||||
<div v-for="row in tableData" :key="row.id" class="culture-activity-card" @click="openDetail(row)">
|
||||
<img
|
||||
v-if="hasCover(row) && !imageLoadErrors[row.id]"
|
||||
class="culture-activity-card__cover"
|
||||
:src="row.cover"
|
||||
:alt="row.name"
|
||||
@error="handleImageError(row)"
|
||||
>
|
||||
<div v-else class="culture-activity-card__cover-state" :class="{'is-error': hasCover(row)}">
|
||||
<van-icon name="photo-o"></van-icon>
|
||||
<span>{{ hasCover(row) ? '图片加载失败' : '暂无图片' }}</span>
|
||||
</div>
|
||||
<div class="culture-activity-card__body">
|
||||
<div class="culture-activity-card__title">{{ row.name }}</div>
|
||||
<div class="culture-activity-card__time" v-if="row.applyStartTime && row.applyEndTime">
|
||||
报名时间:{{ $moment(row.applyStartTime).format('MM/DD HH:mm') }}~{{ $moment(row.applyEndTime).format('MM/DD HH:mm') }}
|
||||
</div>
|
||||
<div class="culture-activity-card__time" v-if="row.startTime && row.endTime">
|
||||
活动时间:{{ $moment(row.startTime).format('MM/DD HH:mm') }}~{{ $moment(row.endTime).format('MM/DD HH:mm') }}
|
||||
</div>
|
||||
<div class="culture-activity-card__meta" v-if="row.address || row.projectTypeName || formatSignUpMethod(row.signUpMethod)">
|
||||
<div class="culture-activity-card__address" v-if="row.address">
|
||||
<van-icon name="location-o"></van-icon>
|
||||
<span>{{ row.address }}</span>
|
||||
</div>
|
||||
<div class="culture-activity-card__tags" v-if="row.projectTypeName || formatSignUpMethod(row.signUpMethod)">
|
||||
<span class="culture-activity-card__tag" v-if="row.projectTypeName">
|
||||
<span class="culture-activity-card__tag-label">活动类型</span>{{ row.projectTypeName }}
|
||||
</span>
|
||||
<span class="culture-activity-card__tag" v-if="formatSignUpMethod(row.signUpMethod)">
|
||||
<span class="culture-activity-card__tag-label">报名方式</span>{{ formatSignUpMethod(row.signUpMethod) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<van-icon class="culture-activity-card__arrow" name="arrow"></van-icon>
|
||||
</div>
|
||||
</div>
|
||||
</van-list>
|
||||
<div v-if="!loading && tableData.length === 0" class="culture-activity-empty">
|
||||
<img src="/assets/mobile/img/activity/culture-empty.png" alt="文化活动空状态插画">
|
||||
<div class="culture-activity-empty__title">暂无文化活动</div>
|
||||
<div class="culture-activity-empty__hint">当前筛选条件下暂时没有可展示的活动</div>
|
||||
</div>
|
||||
</van-pull-refresh>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<van-popup :style="{ height: '100%',width: '100%'}" position="right" v-model="viewShow">
|
||||
<mobile-culture-apply-user :id="viewData.id" @back="back" @submit_back="submit_back" ref="viewInfo"></mobile-culture-apply-user>
|
||||
@@ -92,10 +461,14 @@ layout("/layouts/platform_h5.html"){
|
||||
finished: false,
|
||||
loading: false,
|
||||
refreshing: false,
|
||||
searchKeyword: "",
|
||||
imageLoadErrors: {},
|
||||
filterOpened: false,
|
||||
pageForm: {
|
||||
state: 2,
|
||||
isEnrolled: 0,
|
||||
year: new Date().getFullYear(),
|
||||
name: "",
|
||||
pageNumber: 1,
|
||||
pageSize: 5,
|
||||
totalCount: 0,
|
||||
@@ -128,6 +501,47 @@ layout("/layouts/platform_h5.html"){
|
||||
onReady() {
|
||||
this.doSearch()
|
||||
},
|
||||
hasCover(row) {
|
||||
return !!(row && row.cover && String(row.cover).trim())
|
||||
},
|
||||
handleImageError(row) {
|
||||
this.$set(this.imageLoadErrors, row.id, true)
|
||||
},
|
||||
formatSignUpMethod(value) {
|
||||
return {
|
||||
1: "个人报名",
|
||||
2: "组队报名",
|
||||
3: "分工会报名"
|
||||
}[Number(value)] || ""
|
||||
},
|
||||
onLoad() {
|
||||
if (!this.finished) this.loadActivities(false)
|
||||
},
|
||||
onRefresh() {
|
||||
this.loadActivities(true)
|
||||
},
|
||||
loadActivities(reset) {
|
||||
if (reset) {
|
||||
this.pageForm.pageNumber = 1
|
||||
this.pageForm.totalCount = 0
|
||||
this.tableData = []
|
||||
this.finished = false
|
||||
this.imageLoadErrors = {}
|
||||
}
|
||||
this.loading = true
|
||||
this.$axios.post("/platform/activity/culture/applyUser/pageData", this.pageForm).then((res) => {
|
||||
if (res.code === 0) {
|
||||
const list = res.data && res.data.list ? res.data.list : []
|
||||
this.tableData = this.pageForm.pageNumber === 1 ? list : this.tableData.concat(list)
|
||||
this.pageForm.totalCount = res.data && res.data.totalCount ? res.data.totalCount : 0
|
||||
this.finished = this.tableData.length >= this.pageForm.totalCount
|
||||
if (!this.finished) this.pageForm.pageNumber++
|
||||
}
|
||||
}).finally(() => {
|
||||
this.loading = false
|
||||
this.refreshing = false
|
||||
})
|
||||
},
|
||||
|
||||
openDetail(row) {
|
||||
this.$axios.post('/platform/activity/basic/scope/getScopeUser',{activityGroupId:row.groupId}).then(res=>{
|
||||
@@ -161,66 +575,61 @@ layout("/layouts/platform_h5.html"){
|
||||
this.$refs.viewInfo.getActivityInfo()
|
||||
}, 500)
|
||||
},
|
||||
async doAdd(row) {
|
||||
this.$dialog
|
||||
.confirm({
|
||||
title: "提示",
|
||||
message: "确认要报名吗?"
|
||||
})
|
||||
.then(() => {
|
||||
this.$axios.post("/platform/activity/culture/applyUser/singleSignUp", { activityId: row.id }).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.$toast.loading({
|
||||
duration: 0,
|
||||
forbidClick: true,
|
||||
message: "加载中...."
|
||||
})
|
||||
setTimeout(() => {
|
||||
this.$toast.clear()
|
||||
this.$toast.success(res.msg)
|
||||
this.doSearch()
|
||||
this.subLoading = false
|
||||
}, 1000)
|
||||
} else {
|
||||
this.subLoading = false
|
||||
}
|
||||
})
|
||||
})
|
||||
.catch(() => {})
|
||||
doAdd(row) {
|
||||
this.$dialog.confirm({
|
||||
title: "提示",
|
||||
message: "确认要报名吗?"
|
||||
}).then(() => {
|
||||
this.$axios.post("/platform/activity/culture/applyUser/singleSignUp", {
|
||||
activityId: row.id
|
||||
}).then(res => {
|
||||
if (res.code === 0) {
|
||||
this.$toast.loading({
|
||||
duration: 0,
|
||||
forbidClick: true,
|
||||
message: "加载中...."
|
||||
});
|
||||
setTimeout(() => {
|
||||
this.$toast.clear();
|
||||
this.$toast.success(res.msg);
|
||||
this.doSearch();
|
||||
this.subLoading = false;
|
||||
}, 1000);
|
||||
} else {
|
||||
this.subLoading = false;
|
||||
}
|
||||
});
|
||||
}).catch(() => {});
|
||||
},
|
||||
async doDelete(row) {
|
||||
this.$dialog
|
||||
.confirm({
|
||||
title: "提示",
|
||||
message: "确认要取消报名吗?"
|
||||
})
|
||||
.then(() => {
|
||||
this.$axios.post("/platform/activity/culture/applyUser/cancelSignUp", { activityId: row.id }).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.$toast.loading({
|
||||
duration: 0,
|
||||
forbidClick: true,
|
||||
message: "加载中...."
|
||||
})
|
||||
setTimeout(() => {
|
||||
this.$toast.clear()
|
||||
this.$toast.success(res.msg)
|
||||
this.doSearch()
|
||||
this.subLoading = false
|
||||
}, 1000)
|
||||
} else {
|
||||
this.$toast.fail(res.msg)
|
||||
}
|
||||
})
|
||||
})
|
||||
.catch(() => {})
|
||||
doDelete(row) {
|
||||
this.$dialog.confirm({
|
||||
title: "提示",
|
||||
message: "确认要取消报名吗?"
|
||||
}).then(() => {
|
||||
this.$axios.post("/platform/activity/culture/applyUser/cancelSignUp", {
|
||||
activityId: row.id
|
||||
}).then(res => {
|
||||
if (res.code === 0) {
|
||||
this.$toast.loading({
|
||||
duration: 0,
|
||||
forbidClick: true,
|
||||
message: "加载中...."
|
||||
});
|
||||
setTimeout(() => {
|
||||
this.$toast.clear();
|
||||
this.$toast.success(res.msg);
|
||||
this.doSearch();
|
||||
this.subLoading = false;
|
||||
}, 1000);
|
||||
} else {
|
||||
this.$toast.fail(res.msg);
|
||||
}
|
||||
});
|
||||
}).catch(() => {});
|
||||
},
|
||||
doSearch() {
|
||||
this.$nextTick(() => {
|
||||
this.pageForm.pageNumber = 1
|
||||
this.pageForm.totalCount = 0
|
||||
this.$refs.tableListRef.doSearch()
|
||||
})
|
||||
this.pageForm.name = this.searchKeyword.trim()
|
||||
this.loadActivities(true)
|
||||
}
|
||||
},
|
||||
created() {
|
||||
|
||||
@@ -2,112 +2,572 @@
|
||||
layout("/layouts/platform_h5.html"){
|
||||
#-->
|
||||
|
||||
<style scoped>
|
||||
.info-title {
|
||||
font-weight: bold;
|
||||
background-color: white;
|
||||
padding: 13px;
|
||||
box-shadow: 0 8px 12px #ebedf0;
|
||||
text-align: center !important;
|
||||
<style>
|
||||
body {
|
||||
background-color: #f3f7fd;
|
||||
}
|
||||
.info-container {
|
||||
margin-bottom: 60px;
|
||||
|
||||
.family-activity-page {
|
||||
min-height: 100vh;
|
||||
padding-bottom: calc(24px + env(safe-area-inset-bottom));
|
||||
box-sizing: border-box;
|
||||
background-color: #f3f7fd;
|
||||
}
|
||||
.button {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
|
||||
.family-activity-page .van-nav-bar {
|
||||
background-color: #fff;
|
||||
}
|
||||
.info-img {
|
||||
display: block;
|
||||
|
||||
.family-activity-page .van-nav-bar__title {
|
||||
color: #1f2937;
|
||||
font-size: 16px;
|
||||
}
|
||||
.van-count-down {
|
||||
|
||||
.family-activity-page .van-nav-bar .van-icon,
|
||||
.family-activity-page .van-nav-bar__text {
|
||||
color: #397fbd;
|
||||
}
|
||||
|
||||
.family-activity-content {
|
||||
padding: 12px 12px 0;
|
||||
}
|
||||
|
||||
.family-activity-hero {
|
||||
position: relative;
|
||||
height: 134px;
|
||||
overflow: hidden;
|
||||
border-radius: 10px;
|
||||
color: #fff;
|
||||
background-color: #247fe8;
|
||||
box-shadow: 0 8px 20px rgba(51, 117, 219, .18);
|
||||
}
|
||||
.item-header .van-tag {
|
||||
|
||||
.family-activity-hero__image {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
object-position: left center;
|
||||
}
|
||||
|
||||
.family-activity-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%);
|
||||
}
|
||||
|
||||
.family-activity-hero__title {
|
||||
font-size: 25px;
|
||||
line-height: 34px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 1px;
|
||||
text-shadow: 0 2px 6px rgba(17, 86, 182, .18);
|
||||
}
|
||||
|
||||
.family-activity-hero__subtitle {
|
||||
margin-top: 4px;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
letter-spacing: .5px;
|
||||
opacity: .95;
|
||||
}
|
||||
|
||||
.family-activity-sticky {
|
||||
padding: 10px 0 1px;
|
||||
background-color: #f3f7fd;
|
||||
}
|
||||
|
||||
.van-sticky--fixed .family-activity-sticky {
|
||||
padding-right: 12px;
|
||||
padding-left: 12px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.family-activity-search {
|
||||
margin: 0 0 8px;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
border-radius: 12px;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 5px 16px rgba(43, 73, 112, .1);
|
||||
}
|
||||
|
||||
.family-activity-search .van-search__content {
|
||||
height: 42px;
|
||||
padding-left: 12px;
|
||||
align-items: center;
|
||||
border-radius: 12px;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.family-activity-filter {
|
||||
margin-bottom: 10px;
|
||||
overflow: hidden;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 5px 16px rgba(43, 73, 112, .08);
|
||||
transition: border-radius .18s ease;
|
||||
}
|
||||
|
||||
.family-activity-filter.is-open {
|
||||
border-radius: 10px 10px 0 0;
|
||||
}
|
||||
|
||||
.family-activity-filter .van-dropdown-menu__bar {
|
||||
height: 44px;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.family-activity-filter .van-dropdown-menu__title {
|
||||
color: #66758a;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.family-activity-filter .van-dropdown-menu__title--active,
|
||||
.family-activity-filter .van-dropdown-item__option--active,
|
||||
.family-activity-filter .van-dropdown-item__option--active .van-dropdown-item__icon {
|
||||
color: #1989fa;
|
||||
}
|
||||
|
||||
.family-activity-filter .van-dropdown-item__content {
|
||||
width: calc(100% - 24px);
|
||||
margin: 0 12px;
|
||||
overflow: hidden;
|
||||
border-radius: 0 0 12px 12px;
|
||||
box-shadow: 0 10px 24px rgba(31, 65, 108, .14);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.family-activity-list .table-list-container {
|
||||
display: flex;
|
||||
margin-top: 0 !important;
|
||||
padding: 0 !important;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.family-activity-list .table-list-item {
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
overflow: hidden;
|
||||
border: 0 !important;
|
||||
border-radius: 10px;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 7px 20px rgba(55, 85, 126, .08);
|
||||
}
|
||||
|
||||
.family-activity-list .table-list-item .item-header {
|
||||
display: block !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.family-activity-list .table-list-item .item-actions:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.family-activity-card__cover,
|
||||
.family-activity-card__cover-state {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 150px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
background-color: #edf4fc;
|
||||
}
|
||||
|
||||
.family-activity-card__cover {
|
||||
display: block;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.family-activity-card__cover-state {
|
||||
flex-direction: column;
|
||||
row-gap: 8px;
|
||||
color: #8da1ba;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.family-activity-card__cover-state .van-icon {
|
||||
font-size: 34px;
|
||||
}
|
||||
|
||||
.family-activity-card__cover-state.is-error {
|
||||
color: #c27b75;
|
||||
background-color: #fff3f1;
|
||||
}
|
||||
|
||||
.family-activity-card__body {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
padding: 12px 38px 14px 14px;
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.family-activity-card__title {
|
||||
overflow: hidden;
|
||||
color: #303846;
|
||||
font-size: 14px;
|
||||
line-height: 21px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.family-activity-card__time {
|
||||
margin-top: 4px;
|
||||
overflow: hidden;
|
||||
color: #98a2b2;
|
||||
font-size: 12px;
|
||||
padding: 3px 6px;
|
||||
border-radius: 6px;
|
||||
line-height: 17px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.family-activity-card__meta {
|
||||
display: flex;
|
||||
margin-top: 10px;
|
||||
padding-top: 9px;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
border-top: 1px solid #edf1f6;
|
||||
}
|
||||
|
||||
.family-activity-card__tag {
|
||||
display: inline-flex;
|
||||
padding: 3px 8px;
|
||||
align-items: center;
|
||||
border-radius: 4px;
|
||||
color: #397de0;
|
||||
background-color: #edf5ff;
|
||||
font-size: 11px;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.family-activity-card__tag-label {
|
||||
margin-right: 4px;
|
||||
color: #8aa7cf;
|
||||
}
|
||||
|
||||
.family-activity-card__arrow {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
right: 14px;
|
||||
color: #b4bdca;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.family-activity-empty {
|
||||
display: flex;
|
||||
min-height: 260px;
|
||||
padding: 26px 18px 32px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.family-activity-empty img {
|
||||
width: 82%;
|
||||
max-width: 280px;
|
||||
height: auto;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.family-activity-empty__title {
|
||||
margin-top: 14px;
|
||||
color: #50627a;
|
||||
font-size: 15px;
|
||||
line-height: 22px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.family-activity-empty__hint {
|
||||
margin-top: 5px;
|
||||
color: #9aa7b8;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.family-intro-popup,
|
||||
.family-intro-page {
|
||||
background-color: #f3f7fd;
|
||||
}
|
||||
|
||||
.family-intro-page {
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.family-intro-scroll {
|
||||
height: calc(100vh - 46px);
|
||||
padding: 12px 12px calc(92px + env(safe-area-inset-bottom));
|
||||
overflow-y: auto;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.family-intro-hero {
|
||||
display: flex;
|
||||
min-height: 82px;
|
||||
padding: 16px;
|
||||
align-items: center;
|
||||
border-radius: 12px;
|
||||
color: #fff;
|
||||
background: linear-gradient(120deg, #2d7eea 0%, #5ba6f7 100%);
|
||||
box-shadow: 0 8px 20px rgba(45, 126, 234, .18);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.family-intro-hero__icon {
|
||||
display: flex;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
margin-right: 12px;
|
||||
flex: 0 0 44px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 11px;
|
||||
color: #2d7eea;
|
||||
background-color: rgba(255, 255, 255, .92);
|
||||
font-size: 25px;
|
||||
}
|
||||
|
||||
.family-intro-hero__title {
|
||||
font-size: 17px;
|
||||
line-height: 24px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.family-intro-hero__subtitle {
|
||||
margin-top: 3px;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
opacity: .9;
|
||||
}
|
||||
|
||||
.family-intro-card {
|
||||
margin-top: 12px;
|
||||
padding: 16px;
|
||||
overflow: hidden;
|
||||
border-radius: 12px;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 7px 20px rgba(55, 85, 126, .08);
|
||||
}
|
||||
|
||||
.family-intro-card .content {
|
||||
padding: 0 !important;
|
||||
color: #3f4c5e !important;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.family-intro-card .content div,
|
||||
.family-intro-card .content p,
|
||||
.family-intro-card .content span,
|
||||
.family-intro-card .content li {
|
||||
color: #3f4c5e !important;
|
||||
font-size: 14px !important;
|
||||
line-height: 1.8 !important;
|
||||
white-space: normal !important;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.family-intro-card .content h1,
|
||||
.family-intro-card .content h2,
|
||||
.family-intro-card .content h3,
|
||||
.family-intro-card .content h4 {
|
||||
margin: 16px 0 9px !important;
|
||||
color: #1f2f43 !important;
|
||||
font-size: 18px !important;
|
||||
line-height: 1.5 !important;
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
|
||||
.family-intro-card .content > :first-child {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
|
||||
.family-intro-card .content p {
|
||||
margin: 0 0 8px !important;
|
||||
}
|
||||
|
||||
.family-intro-card .content ul,
|
||||
.family-intro-card .content ol {
|
||||
margin: 8px 0 !important;
|
||||
padding-left: 22px !important;
|
||||
}
|
||||
|
||||
.family-intro-card .content img {
|
||||
max-width: 100% !important;
|
||||
height: auto !important;
|
||||
margin: 8px auto !important;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.family-intro-card .content table {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.family-intro-empty {
|
||||
display: flex;
|
||||
min-height: 180px;
|
||||
margin-top: 12px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
row-gap: 10px;
|
||||
border-radius: 12px;
|
||||
color: #9aa7b8;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 7px 20px rgba(55, 85, 126, .08);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.family-intro-empty .van-icon {
|
||||
color: #83b5f4;
|
||||
font-size: 36px;
|
||||
}
|
||||
|
||||
.family-intro-footer {
|
||||
position: fixed;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 5;
|
||||
padding: 10px 12px calc(10px + env(safe-area-inset-bottom));
|
||||
border-top: 1px solid #edf1f6;
|
||||
background-color: rgba(255, 255, 255, .96);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.family-intro-footer .van-button {
|
||||
height: 42px;
|
||||
border: 0;
|
||||
border-radius: 21px;
|
||||
font-size: 15px;
|
||||
background-color: #1989fa;
|
||||
box-shadow: 0 6px 16px rgba(25, 137, 250, .22);
|
||||
}
|
||||
|
||||
.family-intro-footer .van-button--disabled {
|
||||
color: #8b98aa !important;
|
||||
background-color: #eef2f7 !important;
|
||||
opacity: 1;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.family-intro-footer .van-count-down {
|
||||
display: inline-block;
|
||||
margin-left: 4px;
|
||||
color: #fff;
|
||||
font-size: 13px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="app">
|
||||
<van-nav-bar title="亲子活动" left-text="返回" left-arrow @click-left="historyBack" fixed placeholder></van-nav-bar>
|
||||
<div id="app" v-cloak>
|
||||
<div class="family-activity-page">
|
||||
<van-nav-bar title="亲子活动" left-text="返回" left-arrow @click-left="historyBack" fixed placeholder></van-nav-bar>
|
||||
|
||||
<van-sticky offset-top="46px">
|
||||
<van-dropdown-menu :close-on-click-outside="false" :close-on-click-overlay="false">
|
||||
<year-van-dropdown-item :num="5" @change="doSearch" v-model="pageForm.year"></year-van-dropdown-item>
|
||||
<van-dropdown-item v-model="pageForm.activityType" :options="typeOptions" :multiple="false"
|
||||
@change="doSearch"></van-dropdown-item>
|
||||
</van-dropdown-menu>
|
||||
</van-sticky>
|
||||
|
||||
<table-list api="/platform/family/apply/activityPageData"
|
||||
:page_form.sync="pageForm"
|
||||
ref="tableListRef"
|
||||
img="cover"
|
||||
@ready="onReady"
|
||||
>
|
||||
<template #header="{index,row}">
|
||||
<div class="item-header">
|
||||
<div class="item-title">{{ row.activityName }}</div>
|
||||
<div>
|
||||
<van-tag type="primary" v-if="$moment().isBefore($moment(row.activitySignUpStartTime))">
|
||||
即将开始
|
||||
</van-tag>
|
||||
<van-tag type="success" v-if="$moment().isAfter($moment(row.activitySignUpStartTime)) && $moment().isBefore($moment(row.activitySignUpEndTime))">
|
||||
进行中
|
||||
</van-tag>
|
||||
<van-tag class="grey" v-if="$moment().isAfter($moment(row.activitySignUpEndTime))">
|
||||
已结束
|
||||
</van-tag>
|
||||
<div class="family-activity-content">
|
||||
<section class="family-activity-hero">
|
||||
<img class="family-activity-hero__image" src="/assets/mobile/img/activity/hero/family-hero-v3.png" alt="亲子活动插画">
|
||||
<div class="family-activity-hero__copy">
|
||||
<div class="family-activity-hero__title">亲子活动</div>
|
||||
<div class="family-activity-hero__subtitle">温馨陪伴 · 快乐成长</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<van-sticky :offset-top="46">
|
||||
<div class="family-activity-sticky">
|
||||
<van-search v-model="pageForm.searchKeyword" class="family-activity-search" placeholder="搜索亲子活动"
|
||||
shape="round" clearable @search="doSearch" @clear="doSearch"></van-search>
|
||||
<van-dropdown-menu class="family-activity-filter" :class="{'is-open': filterOpened}" active-color="#1989fa">
|
||||
<year-van-dropdown-item :num="5" @change="doSearch" @open="$set($data, 'filterOpened', true)" @close="$set($data, 'filterOpened', false)" v-model="pageForm.year"></year-van-dropdown-item>
|
||||
<van-dropdown-item v-model="pageForm.activityType" :options="typeOptions" :multiple="false"
|
||||
@change="doSearch" @open="$set($data, 'filterOpened', true)" @close="$set($data, 'filterOpened', false)"></van-dropdown-item>
|
||||
</van-dropdown-menu>
|
||||
</div>
|
||||
</van-sticky>
|
||||
|
||||
<table-list class="family-activity-list" api="/platform/family/apply/activityPageData"
|
||||
:page_form.sync="pageForm" ref="tableListRef" @ready="onReady">
|
||||
<template #empty>
|
||||
<div class="family-activity-empty">
|
||||
<img src="/assets/mobile/img/activity/family-empty.png" alt="亲子活动空状态插画">
|
||||
<div class="family-activity-empty__title">暂无亲子活动</div>
|
||||
<div class="family-activity-empty__hint">当前筛选条件下暂时没有可展示的活动</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #header="{index,row}">
|
||||
<img v-if="hasCover(row) && !imageLoadErrors[row.id]" class="family-activity-card__cover"
|
||||
:src="row.cover" :alt="row.activityName" @error="handleImageError(row)" @click="onView(row)">
|
||||
<div v-else class="family-activity-card__cover-state" :class="{'is-error': hasCover(row)}" @click="onView(row)">
|
||||
<van-icon name="photo-o"></van-icon>
|
||||
<span>{{ hasCover(row) ? '图片加载失败' : '暂无图片' }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot="{index,row}">
|
||||
<table-column label="面向对象">{{row.activityGroupName}}</table-column>
|
||||
<table-column label="报名时间">
|
||||
{{$moment(row.activitySignUpStartTime).format('MM/DD HH:mm') + '~' + $moment(row.activitySignUpEndTime).format('MM/DD HH:mm')}}
|
||||
</table-column>
|
||||
<table-column label="活动时间">
|
||||
{{$moment(row.activityStartTime).format('MM/DD HH:mm') + '~' + $moment(row.activityEndTime).format('MM/DD HH:mm')}}
|
||||
</table-column>
|
||||
</template>
|
||||
<template #actions="{index,row}">
|
||||
<template v-if="$moment().isBefore($moment(row.activitySignUpEndTime))">
|
||||
<div class="action-btn" @click="onView(row)">
|
||||
<i class="fa fa-eye"></i>
|
||||
<span>查看介绍</span>
|
||||
<div class="family-activity-card__body" @click="onView(row)">
|
||||
<div class="family-activity-card__title">{{ row.activityName }}</div>
|
||||
<div class="family-activity-card__time" v-if="row.activitySignUpStartTime && row.activitySignUpEndTime">
|
||||
报名时间:{{ $moment(row.activitySignUpStartTime).format('MM/DD HH:mm') }}~{{ $moment(row.activitySignUpEndTime).format('MM/DD HH:mm') }}
|
||||
</div>
|
||||
<div class="action-btn" @click="onApply(row)">
|
||||
<i class="fa fa-edit"></i>
|
||||
<span>去报名</span>
|
||||
<div class="family-activity-card__time" v-if="row.activityStartTime && row.activityEndTime">
|
||||
活动时间:{{ $moment(row.activityStartTime).format('MM/DD HH:mm') }}~{{ $moment(row.activityEndTime).format('MM/DD HH:mm') }}
|
||||
</div>
|
||||
</template>
|
||||
<div class="family-activity-card__meta" v-if="row.activityGroupName || row.trainType">
|
||||
<span class="family-activity-card__tag" v-if="row.activityGroupName"><span class="family-activity-card__tag-label">面向对象</span>{{row.activityGroupName}}</span>
|
||||
<span class="family-activity-card__tag" v-if="row.trainType"><span class="family-activity-card__tag-label">活动类型</span>{{row.trainType}}</span>
|
||||
</div>
|
||||
<van-icon class="family-activity-card__arrow" name="arrow"></van-icon>
|
||||
</div>
|
||||
</template>
|
||||
</table-list>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<van-action-sheet :close-on-click-overlay="false" title="详细信息" v-model="infoVisible">
|
||||
|
||||
<div class="info-container">
|
||||
<div>
|
||||
<van-image class="info-img" height="186" width="100%" :src="infoRow.cover"></van-image>
|
||||
<div class="info-title">{{infoRow.activityName}}</div>
|
||||
<pdf-preview :content="infoRow.introduce"></pdf-preview>
|
||||
<van-popup class="family-intro-popup" :style="{height: '100%', width: '100%'}" position="right" safe-area-inset-bottom v-model="infoVisible">
|
||||
<div class="family-intro-page">
|
||||
<van-nav-bar title="活动介绍" left-text="返回" left-arrow @click-left="historyBack"></van-nav-bar>
|
||||
<div class="family-intro-scroll">
|
||||
<div class="family-intro-hero">
|
||||
<div class="family-intro-hero__icon"><van-icon name="description"></van-icon></div>
|
||||
<div>
|
||||
<div class="family-intro-hero__title">亲子活动报名须知</div>
|
||||
<div class="family-intro-hero__subtitle">请仔细阅读活动安排与报名要求</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="family-intro-card" v-if="infoRow.introduce">
|
||||
<pdf-preview :content="infoRow.introduce"></pdf-preview>
|
||||
</div>
|
||||
<div v-else class="family-intro-empty">
|
||||
<van-icon name="description"></van-icon>
|
||||
<span>暂无活动介绍</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="button">
|
||||
<van-button @click="onApply(infoRow)" type="primary" block>
|
||||
<span v-if="time >= 0">
|
||||
去报名
|
||||
</span>
|
||||
<template v-else>
|
||||
距离开始
|
||||
<van-count-down :time="Math.abs(time)" class="count-down" format="DD 天 HH 时 mm 分 ss 秒" @finish="time = 0"></van-count-down>
|
||||
</template>
|
||||
<div class="family-intro-footer">
|
||||
<van-button v-if="canApply(infoRow)" @click="onApply(infoRow)" type="primary" block round>进入报名</van-button>
|
||||
<van-button v-else-if="time < 0" @click="onApply(infoRow)" type="primary" block round>
|
||||
距离开始
|
||||
<van-count-down :time="Math.abs(time)" format="DD 天 HH 时 mm 分 ss 秒" @finish="$set($data, 'time', 0)"></van-count-down>
|
||||
</van-button>
|
||||
<van-button v-else disabled block round>{{ registrationStatusText(infoRow) }}</van-button>
|
||||
</div>
|
||||
</div>
|
||||
</van-action-sheet>
|
||||
</van-popup>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -136,17 +596,48 @@ layout("/layouts/platform_h5.html"){
|
||||
],
|
||||
infoVisible: false,
|
||||
infoRow: {},
|
||||
imageLoadErrors: {},
|
||||
filterOpened: false,
|
||||
|
||||
id: GetQueryString('id')
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
hasCover(row) {
|
||||
return !!(row && row.cover && String(row.cover).trim())
|
||||
},
|
||||
handleImageError(row) {
|
||||
this.$set(this.imageLoadErrors, row.id, true)
|
||||
},
|
||||
isRegistrationClosed(row) {
|
||||
return !!(row && row.activitySignUpEndTime && !this.$moment().isBefore(this.$moment(row.activitySignUpEndTime)))
|
||||
},
|
||||
canApply(row) {
|
||||
if(!row || !row.activitySignUpStartTime || !row.activitySignUpEndTime) {
|
||||
return false
|
||||
}
|
||||
const now = Date.now()
|
||||
return now >= this.$moment(row.activitySignUpStartTime).valueOf()
|
||||
&& now <= this.$moment(row.activitySignUpEndTime).valueOf()
|
||||
},
|
||||
registrationStatusText(row) {
|
||||
if(!row || !row.activitySignUpStartTime || !row.activitySignUpEndTime) {
|
||||
return '当前不可报名'
|
||||
}
|
||||
if(Date.now() < this.$moment(row.activitySignUpStartTime).valueOf()) {
|
||||
return '报名未开始'
|
||||
}
|
||||
return '已过报名时间'
|
||||
},
|
||||
onView(row) {
|
||||
this.infoRow = row
|
||||
this.time = this.$moment().diff(this.$moment(row.activitySignUpStartTime), 'milliseconds')
|
||||
this.infoVisible = true
|
||||
},
|
||||
onApply(row) {
|
||||
if(this.isRegistrationClosed(row)) {
|
||||
return
|
||||
}
|
||||
if(this.$moment().isBefore(this.$moment(row.activitySignUpStartTime))) {
|
||||
this.onView(row)
|
||||
return
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
const times = {
|
||||
template:
|
||||
/*language=HTML*/
|
||||
`
|
||||
template: /*language=HTML*/ `
|
||||
<div>
|
||||
<van-action-sheet v-model="visible" title="时间段" cancel-text="取消">
|
||||
<button v-for="(item,index) in row?.courseTimes" type="button" class="van-action-sheet__item">
|
||||
<van-action-sheet class="family-course-times-sheet" :style="{height: '78%'}" :close-on-click-overlay="true" v-model="visible" title="时间段" cancel-text="取消">
|
||||
<div class="family-course-times-list">
|
||||
<button v-for="(item,index) in row?.courseTimes" :key="index" type="button" class="van-action-sheet__item family-course-time-item">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center">
|
||||
<div>
|
||||
<div class="van-action-sheet__name">
|
||||
@@ -21,6 +20,7 @@ const times = {
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</van-action-sheet>
|
||||
|
||||
<van-popup round :safe-area-inset-bottom="true"
|
||||
@@ -84,6 +84,84 @@ const times = {
|
||||
},
|
||||
},
|
||||
style: /*language=CSS*/ `
|
||||
.family-course-times-sheet {
|
||||
overflow: hidden;
|
||||
border-radius: 16px 16px 0 0;
|
||||
background-color: #f3f7fd;
|
||||
}
|
||||
|
||||
.family-course-times-sheet /deep/ .van-action-sheet__header {
|
||||
height: 48px;
|
||||
color: #1f2f43;
|
||||
background-color: #fff;
|
||||
font-size: 16px;
|
||||
line-height: 48px;
|
||||
}
|
||||
|
||||
.family-course-times-sheet /deep/ .van-action-sheet__close {
|
||||
top: 13px;
|
||||
color: #b5bdc9;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.family-course-times-list {
|
||||
height: calc(100% - 98px);
|
||||
padding: 10px 12px 0;
|
||||
overflow-y: auto;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.family-course-times-sheet /deep/ .family-course-time-item {
|
||||
width: 100%;
|
||||
margin-bottom: 10px;
|
||||
padding: 14px;
|
||||
border-radius: 10px;
|
||||
color: #263548;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 6px 18px rgba(55, 85, 126, .07);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.family-course-times-sheet /deep/ .van-action-sheet__name {
|
||||
color: #263548;
|
||||
font-size: 14px;
|
||||
line-height: 21px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.family-course-times-sheet /deep/ .van-action-sheet__subname {
|
||||
margin-top: 5px;
|
||||
color: #98a2b2;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.family-course-times-sheet /deep/ .sign_button .van-button {
|
||||
min-width: 48px;
|
||||
height: 28px;
|
||||
padding: 0 12px;
|
||||
border: 0;
|
||||
border-radius: 14px;
|
||||
color: #fff;
|
||||
background-color: #1989fa;
|
||||
font-size: 12px;
|
||||
line-height: 28px;
|
||||
box-shadow: 0 4px 10px rgba(25, 137, 250, .18);
|
||||
}
|
||||
|
||||
.family-course-times-sheet /deep/ .sign_button .van-button--disabled {
|
||||
color: #8b98aa;
|
||||
background-color: #eef2f7;
|
||||
opacity: 1;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.family-course-times-sheet /deep/ .van-action-sheet__cancel {
|
||||
height: 50px;
|
||||
color: #66758a;
|
||||
background-color: #fff;
|
||||
font-size: 14px;
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
const applyForm = {
|
||||
template:
|
||||
/*language=HTML*/
|
||||
`
|
||||
template: /*language=HTML*/ `
|
||||
<div>
|
||||
<van-action-sheet :close-on-click-overlay="false" title="报名信息" v-model="visible">
|
||||
<van-action-sheet class="family-course-form-sheet" :style="{height: '78%'}" :close-on-click-overlay="true" title="报名信息" v-model="visible">
|
||||
<van-form ref="formRef" class="form-container">
|
||||
<van-cell-group title="活动信息" class="form-section">
|
||||
<van-field label="活动名称" readonly v-model="row.courseName"></van-field>
|
||||
@@ -21,7 +19,7 @@ const applyForm = {
|
||||
required
|
||||
:rules="[{ required: true, message: '请选择报名时段' }]"
|
||||
readonly
|
||||
@click="showCoursePicker = true"
|
||||
@click="$set($data, 'showCoursePicker', true)"
|
||||
placeholder="请选择报名时段"
|
||||
name="courseTimeName"
|
||||
v-model="formData.courseTimeName">
|
||||
@@ -31,7 +29,7 @@ const applyForm = {
|
||||
show-toolbar
|
||||
:columns="courseTimeSelectList"
|
||||
@confirm="onCourseConfirm"
|
||||
@cancel="showCoursePicker=false"
|
||||
@cancel="$set($data, 'showCoursePicker', false)"
|
||||
></van-picker>
|
||||
</van-popup>
|
||||
</template>
|
||||
@@ -39,16 +37,20 @@ const applyForm = {
|
||||
|
||||
<van-cell-group class="form-section">
|
||||
<template #title>
|
||||
<div style="display: flex; align-items: center; justify-content: space-between">
|
||||
<div class="family-member-section-title">
|
||||
<div>{{activity.keyWord}}信息</div>
|
||||
<div>
|
||||
<van-tag @click="delFamily" size="large" type="primary" color="#ff3b30">删除{{activity.keyWord}}</van-tag>
|
||||
<van-tag @click="addFamily" size="large" type="primary" color="#1867b0">添加{{activity.keyWord}}</van-tag>
|
||||
<div class="family-member-actions">
|
||||
<button type="button" class="family-member-action family-member-action--delete" :aria-label="'删除' + activity.keyWord" @click="delFamily">
|
||||
<van-icon name="delete-o"></van-icon>
|
||||
</button>
|
||||
<button type="button" class="family-member-action family-member-action--add" :aria-label="'添加' + activity.keyWord" @click="addFamily">
|
||||
<van-icon name="plus"></van-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div v-if="formData.mobileColumnsValue && formData.mobileColumnsValue.length > 0" class="mt10">
|
||||
<van-tabs v-model="familyActive" type="card" color="#0e78c5" animated>
|
||||
<van-tabs v-model="familyActive" color="#1989fa" title-active-color="#1989fa" animated>
|
||||
<van-tab v-for="item, index in formData.mobileColumnsValue"
|
||||
:name="index + ''"
|
||||
:title="activity.keyWord + (index + 1)"
|
||||
@@ -65,7 +67,7 @@ const applyForm = {
|
||||
</van-cell-group>
|
||||
|
||||
<div class="button">
|
||||
<van-button @click="onSubmit" round type="info" block>提交</van-button>
|
||||
<van-button :loading="formLoading" :disabled="formLoading" @click="onSubmit" round type="info" block>提交</van-button>
|
||||
</div>
|
||||
</van-form>
|
||||
</van-action-sheet>
|
||||
@@ -77,6 +79,7 @@ const applyForm = {
|
||||
row: {},
|
||||
activity: {},
|
||||
visible: false,
|
||||
formLoading: false,
|
||||
formData: {},
|
||||
showCoursePicker: false,
|
||||
courseTimeSelectList: [],
|
||||
@@ -100,16 +103,18 @@ const applyForm = {
|
||||
const ac = (Number(this.familyActive) - 1)
|
||||
this.familyActive = '' + (ac >= 0 ? ac : 0)
|
||||
},
|
||||
async onOpen(row, courseType, activity) {
|
||||
this.row = row
|
||||
this.courseType = courseType
|
||||
this.activity = activity
|
||||
|
||||
this.init(row, courseType)
|
||||
if(row.courseIsLimitApply) {
|
||||
await this.getCourseTimeSelectList(row)
|
||||
}
|
||||
this.visible = true
|
||||
onOpen(row, courseType, activity) {
|
||||
this.row = row;
|
||||
this.courseType = courseType;
|
||||
this.activity = activity;
|
||||
this.init(row, courseType);
|
||||
if (row.courseIsLimitApply) {
|
||||
return this.getCourseTimeSelectList(row).then(() => {
|
||||
this.visible = true;
|
||||
});
|
||||
} else {
|
||||
this.visible = true;
|
||||
}
|
||||
},
|
||||
init(row, courseType) {
|
||||
this.$set(this.formData, 'username', this.$store.state.user.username)
|
||||
@@ -127,13 +132,16 @@ const applyForm = {
|
||||
this.$set(this.formData, "courseTimeName", val.text.substring(0, 12))
|
||||
this.showCoursePicker = false
|
||||
},
|
||||
async getCourseTimeSelectList(o) {
|
||||
const resp = await this.$axios.post('/platform/family/apply/getCourseTimeSelectList',{courseId: o.id})
|
||||
getCourseTimeSelectList(o) {
|
||||
return this.$axios.post('/platform/family/apply/getCourseTimeSelectList', {
|
||||
courseId: o.id
|
||||
}).then(resp => {
|
||||
if (resp.code === 0) {
|
||||
this.courseTimeSelectList = resp.data
|
||||
this.courseTimeSelectList = resp.data;
|
||||
} else {
|
||||
this.$toast.fail("获取时段信息失败,请联系管理员")
|
||||
this.$toast.fail("获取时段信息失败,请联系管理员");
|
||||
}
|
||||
});
|
||||
},
|
||||
validateIdCard(idCard) {
|
||||
if (!idCard) {
|
||||
@@ -266,78 +274,181 @@ const applyForm = {
|
||||
|
||||
return { age, sex, };
|
||||
},
|
||||
async validateSignUp() {
|
||||
// 获取家属人数
|
||||
const res = await this.$axios.post("/platform/family/apply/validateSignUp", {
|
||||
courseId: this.formData.courseId,
|
||||
currentFamilyNumber: this.formData.mobileColumnsValue.length || 0
|
||||
})
|
||||
if(res.code !== 0) {
|
||||
this.$dialog.alert({title: '温馨提示', message: res.msg})
|
||||
validateSignUp() {
|
||||
return this.$axios.post("/platform/family/apply/validateSignUp", {
|
||||
courseId: this.formData.courseId,
|
||||
currentFamilyNumber: this.formData.mobileColumnsValue.length || 0
|
||||
}).then(res => {
|
||||
if (res.code !== 0) {
|
||||
this.$dialog.alert({
|
||||
title: '温馨提示',
|
||||
message: res.msg
|
||||
});
|
||||
}
|
||||
return res.code === 0
|
||||
return res.code === 0;
|
||||
});
|
||||
},
|
||||
async validateCourseTime() {
|
||||
const res = await this.$axios.post('/platform/family/apply/validateSourceSignUp', {
|
||||
activityCourseId: this.formData.activityCourseId,
|
||||
courseId: this.row.id
|
||||
})
|
||||
if(res.code !== 0) {
|
||||
this.$dialog.alert({title: '温馨提示', message: res.msg})
|
||||
validateCourseTime() {
|
||||
return this.$axios.post('/platform/family/apply/validateSourceSignUp', {
|
||||
activityCourseId: this.formData.activityCourseId,
|
||||
courseId: this.row.id
|
||||
}).then(res => {
|
||||
if (res.code !== 0) {
|
||||
this.$dialog.alert({
|
||||
title: '温馨提示',
|
||||
message: res.msg
|
||||
});
|
||||
}
|
||||
return res.code === 0
|
||||
return res.code === 0;
|
||||
});
|
||||
},
|
||||
async onSubmit() {
|
||||
onSubmit() {
|
||||
// 保留原有报名资格、动态表单和限额时段校验;确认后才发送报名数据。
|
||||
if (this.formLoading) return
|
||||
if (!this.validFamilyForm()) return
|
||||
if (!await this.validateSignUp()) return
|
||||
|
||||
this.$refs.formRef.validate().then(() => {
|
||||
this.$dialog.confirm({
|
||||
title: "提示",
|
||||
message: "您确定要提交吗?"
|
||||
}).then(async () => {
|
||||
if (this.row.courseIsLimitApply) {
|
||||
if(!await this.validateCourseTime()) return
|
||||
}
|
||||
|
||||
return this.validateSignUp().then((valid) => {
|
||||
if (!valid) return
|
||||
return this.$refs.formRef.validate().then(() => {
|
||||
return this.$dialog.confirm({title: "提示", message: "您确定要提交吗?"})
|
||||
}).then(() => {
|
||||
return this.row.courseIsLimitApply ? this.validateCourseTime() : true
|
||||
}).then((validTime) => {
|
||||
if (!validTime) return
|
||||
this.$set(this, "formLoading", true)
|
||||
const formData = clone(this.formData)
|
||||
let array = []
|
||||
formData.mobileColumnsValue.forEach(item => {
|
||||
const o = item.map((v) => {
|
||||
return {
|
||||
columnName: v.columnName,
|
||||
columnValue: v.columnValue,
|
||||
columnCode: v.columnCode,
|
||||
columnFormType: v.columnFormType
|
||||
formData.mobileColumnsValue = JSON.stringify(formData.mobileColumnsValue.map((items) => items.map((item) => ({
|
||||
columnName: item.columnName, columnValue: item.columnValue,
|
||||
columnCode: item.columnCode, columnFormType: item.columnFormType
|
||||
}))))
|
||||
return this.$axios.post("/platform/family/apply/doSignUp", formData).then((res) => {
|
||||
// 保留原有报名结果确认,阅读提示后再关闭报名表单。
|
||||
return this.$dialog.alert({title: "温馨提示", message: res.msg}).then(() => {
|
||||
if (res.code === 0) {
|
||||
this.$set(this, "visible", false)
|
||||
this.$emit("refresh")
|
||||
}
|
||||
})
|
||||
array.push(o)
|
||||
})
|
||||
formData.mobileColumnsValue = JSON.stringify(clone(array))
|
||||
this.$axios.post("/platform/family/apply/doSignUp", formData).then(res => {
|
||||
this.$dialog.alert({title: '温馨提示', message: res.msg})
|
||||
.then(() => {
|
||||
if (res.code === 0) {
|
||||
this.visible = false
|
||||
this.$emit('refresh')
|
||||
}
|
||||
})
|
||||
}).finally(() => {
|
||||
this.$set(this, "formLoading", false)
|
||||
})
|
||||
}).catch(() => {
|
||||
// 取消确认或校验未通过时保留表单,网络错误由公共请求拦截器提示。
|
||||
})
|
||||
})
|
||||
},
|
||||
},
|
||||
style: /*language=CSS*/ `
|
||||
.family-course-form-sheet {
|
||||
overflow: hidden;
|
||||
border-radius: 16px 16px 0 0;
|
||||
background-color: #f3f7fd;
|
||||
}
|
||||
|
||||
.family-course-form-sheet /deep/ .van-action-sheet__header {
|
||||
height: 48px;
|
||||
color: #1f2f43;
|
||||
background-color: #fff;
|
||||
font-size: 16px;
|
||||
line-height: 48px;
|
||||
}
|
||||
|
||||
.family-course-form-sheet /deep/ .van-action-sheet__close {
|
||||
top: 13px;
|
||||
color: #b5bdc9;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.family-course-form-sheet /deep/ .form-container {
|
||||
height: calc(100% - 48px);
|
||||
padding: 10px 12px 72px;
|
||||
overflow-y: auto;
|
||||
background-color: #f3f7fd;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.family-course-form-sheet /deep/ .form-section {
|
||||
margin-bottom: 10px;
|
||||
overflow: hidden;
|
||||
border-radius: 10px;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 6px 18px rgba(55, 85, 126, .07);
|
||||
}
|
||||
|
||||
.family-course-form-sheet /deep/ .van-cell-group__title {
|
||||
padding: 12px 14px 8px;
|
||||
color: #263548;
|
||||
background-color: #fff;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.family-course-form-sheet /deep/ .van-cell-group__title::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.family-course-form-sheet /deep/ .van-cell {
|
||||
padding: 11px 14px;
|
||||
color: #263548;
|
||||
background-color: #fff;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.family-course-form-sheet /deep/ .van-field__label {
|
||||
color: #66758a;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.family-course-form-sheet /deep/ .van-field__control {
|
||||
color: #263548;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.family-course-form-sheet /deep/ .van-tabs__nav--card {
|
||||
overflow: hidden;
|
||||
border: 0;
|
||||
border-radius: 16px;
|
||||
background-color: #eef3f9;
|
||||
}
|
||||
|
||||
.family-course-form-sheet /deep/ .van-tabs__nav--card .van-tab {
|
||||
border-right: 0;
|
||||
color: #66758a;
|
||||
}
|
||||
|
||||
.family-course-form-sheet /deep/ .van-tabs__nav--card .van-tab--active {
|
||||
color: #1989fa;
|
||||
background-color: #e7f2ff;
|
||||
}
|
||||
|
||||
/deep/ .companionList_empty_text {
|
||||
text-align: center;
|
||||
padding: 10px 0;
|
||||
font-size: 14px;
|
||||
color: grey;
|
||||
}
|
||||
/deep/ .button {
|
||||
position: fixed;
|
||||
|
||||
.family-course-form-sheet /deep/ .button {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
left: 0;
|
||||
z-index: 2;
|
||||
width: auto;
|
||||
padding: 10px 12px calc(10px + env(safe-area-inset-bottom));
|
||||
border-top: 1px solid #edf1f6;
|
||||
background-color: rgba(255, 255, 255, .96);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.family-course-form-sheet /deep/ .button .van-button {
|
||||
height: 42px;
|
||||
border: 0;
|
||||
border-radius: 21px;
|
||||
color: #fff;
|
||||
background-color: #1989fa;
|
||||
font-size: 15px;
|
||||
box-shadow: 0 6px 16px rgba(25, 137, 250, .22);
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
@@ -2,248 +2,462 @@
|
||||
layout("/layouts/platform_h5.html"){
|
||||
#-->
|
||||
|
||||
<style scoped>
|
||||
.info-title {
|
||||
font-weight: bold;
|
||||
background-color: white;
|
||||
padding: 13px;
|
||||
box-shadow: 0 8px 12px #ebedf0;
|
||||
text-align: center !important;
|
||||
}
|
||||
.info-container {
|
||||
|
||||
}
|
||||
.button {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
}
|
||||
.info-img {
|
||||
display: block;
|
||||
}
|
||||
.van-count-down {
|
||||
color: #fff;
|
||||
}
|
||||
.table-list-container {
|
||||
padding: 0;
|
||||
margin-top: 10px;
|
||||
<style>
|
||||
body {
|
||||
background-color: #f3f7fd;
|
||||
}
|
||||
|
||||
.table-list-container .table-list-item {
|
||||
.family-mine-page {
|
||||
min-height: 100vh;
|
||||
padding-bottom: calc(24px + env(safe-area-inset-bottom));
|
||||
box-sizing: border-box;
|
||||
background-color: #f3f7fd;
|
||||
}
|
||||
|
||||
.family-mine-page .van-nav-bar {
|
||||
background-color: #fff;
|
||||
border-bottom: 1px solid #eaecef;
|
||||
padding: 16px;
|
||||
position: relative;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.table-list-container .table-list-item:last-child {
|
||||
border-bottom: none;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.table-list-container .table-list-item .item-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.table-list-container .table-list-item .item-title {
|
||||
.family-mine-page .van-nav-bar__title {
|
||||
color: #1f2937;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #1f2f3d;
|
||||
}
|
||||
|
||||
.family-mine-page .van-nav-bar .van-icon,
|
||||
.family-mine-page .van-nav-bar__text {
|
||||
color: #397fbd;
|
||||
}
|
||||
|
||||
.family-mine-content {
|
||||
padding: 12px 12px 0;
|
||||
}
|
||||
|
||||
.family-mine-hero {
|
||||
position: relative;
|
||||
height: 134px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: 1;
|
||||
padding-right: 10px;
|
||||
border-radius: 10px;
|
||||
color: #fff;
|
||||
background: linear-gradient(115deg, #347eea 0%, #5b9cf4 58%, #cfe3ff 100%);
|
||||
box-shadow: 0 8px 20px rgba(51, 117, 219, .18);
|
||||
}
|
||||
|
||||
.table-list-container .table-list-item .img-container {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.table-list-container .table-list-item .img-container img {
|
||||
width: 100%;
|
||||
.family-mine-hero__image {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 67%;
|
||||
height: 100%;
|
||||
border-radius: 6px;
|
||||
object-fit: cover;
|
||||
object-position: right center;
|
||||
-webkit-mask-image: linear-gradient(90deg, transparent 0%, #000 26%);
|
||||
mask-image: linear-gradient(90deg, transparent 0%, #000 26%);
|
||||
}
|
||||
|
||||
|
||||
.table-list-container .table-list-item .item-meta {
|
||||
.family-mine-hero__copy {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
color: #606266;
|
||||
font-size: 14px;
|
||||
margin-bottom: 5px;
|
||||
padding: 4px 0;
|
||||
height: 100%;
|
||||
padding-left: 16px;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.table-list-container .table-list-item .meta-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
.family-mine-hero__title {
|
||||
font-size: 25px;
|
||||
line-height: 34px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.table-list-container .table-list-item .meta-item i {
|
||||
font-size: 14px;
|
||||
color: #909399;
|
||||
width: 16px;
|
||||
text-align: center;
|
||||
.family-mine-hero__subtitle {
|
||||
margin-top: 4px;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
letter-spacing: .5px;
|
||||
opacity: .95;
|
||||
}
|
||||
|
||||
.table-list-container .table-list-item .item-content {
|
||||
color: #606266;
|
||||
font-size: 14px;
|
||||
.family-mine-sticky {
|
||||
padding: 10px 0 1px;
|
||||
background-color: #f3f7fd;
|
||||
}
|
||||
|
||||
.van-sticky--fixed .family-mine-sticky {
|
||||
padding-right: 12px;
|
||||
padding-left: 12px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.family-mine-search {
|
||||
margin-bottom: 8px;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
line-height: 1.5;
|
||||
border-radius: 12px;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 5px 16px rgba(43, 73, 112, .1);
|
||||
}
|
||||
|
||||
.table-list-container .table-list-item .item-footer {
|
||||
.family-mine-search .van-search__content {
|
||||
height: 42px;
|
||||
padding-left: 12px;
|
||||
align-items: center;
|
||||
border-radius: 12px;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.family-mine-search .van-field__control {
|
||||
color: #263548;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.family-mine-filter {
|
||||
margin-bottom: 10px;
|
||||
overflow: hidden;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 5px 16px rgba(43, 73, 112, .08);
|
||||
transition: border-radius .18s ease;
|
||||
}
|
||||
|
||||
.family-mine-filter.is-open {
|
||||
border-radius: 10px 10px 0 0;
|
||||
}
|
||||
|
||||
.family-mine-filter .van-dropdown-menu__bar {
|
||||
height: 44px;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.family-mine-filter .van-dropdown-menu__title {
|
||||
width: auto;
|
||||
max-width: calc(100% - 24px);
|
||||
padding: 0 12px 0 0;
|
||||
color: #66758a;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.family-mine-filter .van-dropdown-menu__title::after {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.family-mine-filter .van-dropdown-item__content {
|
||||
width: calc(100% - 24px);
|
||||
margin: 0 12px;
|
||||
overflow: hidden;
|
||||
border-radius: 0 0 12px 12px;
|
||||
box-shadow: 0 10px 24px rgba(31, 65, 108, .14);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.family-mine-list {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 12px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px dashed #eaecef;
|
||||
color: #909399;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.family-mine-card {
|
||||
overflow: hidden;
|
||||
border-radius: 10px;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 7px 20px rgba(55, 85, 126, .08);
|
||||
}
|
||||
|
||||
.family-mine-card__cover,
|
||||
.family-mine-card__cover-state {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 150px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #edf4fc;
|
||||
}
|
||||
|
||||
.family-mine-card__cover {
|
||||
display: block;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.family-mine-card__cover-state {
|
||||
flex-direction: column;
|
||||
row-gap: 8px;
|
||||
color: #8da1ba;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.table-list-container .table-list-item .item-actions {
|
||||
display: flex;
|
||||
column-gap: 20px;
|
||||
justify-content: end;
|
||||
margin-top: 15px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid #eaecef;
|
||||
.family-mine-card__cover-state .van-icon {
|
||||
font-size: 34px;
|
||||
}
|
||||
|
||||
.table-list-container .table-list-item .action-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: end;
|
||||
font-size: 14px;
|
||||
color: var(--color-primary);
|
||||
padding: 4px 0;
|
||||
.family-mine-card__cover-state.is-error {
|
||||
color: #c27b75;
|
||||
background-color: #fff3f1;
|
||||
}
|
||||
|
||||
.table-list-container .table-list-item .action-btn i {
|
||||
margin-right: 6px;
|
||||
font-size: 16px;
|
||||
.family-mine-card__body {
|
||||
padding: 12px 14px 14px;
|
||||
}
|
||||
|
||||
.table-list-container .table-list-item .action-btn.delete {
|
||||
color: #ff0000;
|
||||
}
|
||||
|
||||
.table-list-container .table-list-item .action-btn.review {
|
||||
color: #67c23a;
|
||||
}
|
||||
|
||||
.table-list-container .empty-state {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.table-list-container .empty-state i {
|
||||
font-size: 60px;
|
||||
margin-bottom: 16px;
|
||||
color: #dcdee0;
|
||||
}
|
||||
|
||||
.table-list-container .empty-state p {
|
||||
margin-top: 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.table-column {
|
||||
display: flex;
|
||||
padding: 6px 0;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
flex-shrink: 0;
|
||||
max-width: 120px;
|
||||
white-space: nowrap;
|
||||
.family-mine-card__title {
|
||||
overflow: hidden;
|
||||
color: #303846;
|
||||
font-size: 14px;
|
||||
line-height: 21px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.value {
|
||||
color: #555;
|
||||
.family-mine-card__time {
|
||||
margin-top: 4px;
|
||||
overflow: hidden;
|
||||
color: #98a2b2;
|
||||
font-size: 12px;
|
||||
line-height: 17px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.family-mine-card__meta {
|
||||
display: flex;
|
||||
margin-top: 10px;
|
||||
padding-top: 9px;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
border-top: 1px solid #edf1f6;
|
||||
}
|
||||
|
||||
.family-mine-card__tag {
|
||||
display: inline-flex;
|
||||
padding: 3px 8px;
|
||||
border-radius: 4px;
|
||||
color: #397de0;
|
||||
background-color: #edf5ff;
|
||||
font-size: 11px;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.family-mine-card__tag-label {
|
||||
margin-right: 4px;
|
||||
color: #8aa7cf;
|
||||
}
|
||||
|
||||
.family-mine-card__actions {
|
||||
display: flex;
|
||||
padding: 12px 14px;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
border-top: 1px solid #edf1f6;
|
||||
background-color: #f8fbff;
|
||||
}
|
||||
|
||||
.family-mine-card__action {
|
||||
display: inline-flex;
|
||||
min-width: 82px;
|
||||
height: 32px;
|
||||
padding: 0 14px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 0;
|
||||
border-radius: 16px;
|
||||
color: #397de0;
|
||||
background-color: #edf5ff;
|
||||
font-size: 13px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.family-mine-card__action .van-icon {
|
||||
margin-right: 6px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.family-mine-card__action.is-primary {
|
||||
color: #fff;
|
||||
background-color: #1989fa;
|
||||
box-shadow: 0 5px 12px rgba(25, 137, 250, .18);
|
||||
}
|
||||
|
||||
.family-mine-empty {
|
||||
display: flex;
|
||||
min-height: 260px;
|
||||
padding: 26px 18px 32px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.family-mine-empty img {
|
||||
width: 82%;
|
||||
max-width: 280px;
|
||||
height: auto;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.family-mine-empty__title {
|
||||
margin-top: 14px;
|
||||
color: #50627a;
|
||||
font-size: 15px;
|
||||
line-height: 22px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.family-mine-empty__hint {
|
||||
margin-top: 5px;
|
||||
color: #9aa7b8;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.family-mine-intro-page {
|
||||
min-height: 100vh;
|
||||
background-color: #f3f7fd;
|
||||
}
|
||||
|
||||
.family-mine-intro-scroll {
|
||||
height: calc(100vh - 46px);
|
||||
padding: 12px;
|
||||
overflow-y: auto;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.family-mine-intro-hero {
|
||||
display: flex;
|
||||
min-height: 82px;
|
||||
padding: 16px;
|
||||
align-items: center;
|
||||
border-radius: 12px;
|
||||
color: #fff;
|
||||
background: linear-gradient(120deg, #2d7eea 0%, #5ba6f7 100%);
|
||||
box-shadow: 0 8px 20px rgba(45, 126, 234, .18);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.family-mine-intro-hero__icon {
|
||||
display: flex;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
margin-right: 12px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 0 0 44px;
|
||||
border-radius: 11px;
|
||||
color: #2d7eea;
|
||||
background-color: rgba(255, 255, 255, .92);
|
||||
font-size: 25px;
|
||||
}
|
||||
|
||||
.family-mine-intro-hero__title {
|
||||
font-size: 17px;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.family-mine-intro-hero__subtitle {
|
||||
margin-top: 3px;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
opacity: .9;
|
||||
}
|
||||
|
||||
.family-mine-intro-card {
|
||||
margin-top: 12px;
|
||||
padding: 18px 16px;
|
||||
overflow: hidden;
|
||||
border-radius: 12px;
|
||||
color: #3f4c5e;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 7px 20px rgba(55, 85, 126, .08);
|
||||
font-size: 14px;
|
||||
flex-grow: 1; /* 动态占据剩余部分 */
|
||||
white-space: nowrap; /* 防止换行 */
|
||||
overflow: hidden; /* 隐藏溢出的部分 */
|
||||
text-overflow: ellipsis; /* 省略号 */
|
||||
line-height: 1.8;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.family-mine-intro-card img {
|
||||
max-width: 100% !important;
|
||||
height: auto !important;
|
||||
border-radius: 8px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="app">
|
||||
<van-nav-bar title="亲子活动-我的报名" left-text="返回" left-arrow @click-left="historyBack" fixed placeholder></van-nav-bar>
|
||||
<div id="app" v-cloak>
|
||||
<div class="family-mine-page">
|
||||
<van-nav-bar title="亲子活动-我的报名" left-text="返回" left-arrow @click-left="historyBack" fixed placeholder></van-nav-bar>
|
||||
|
||||
<van-sticky offset-top="46px">
|
||||
<van-dropdown-menu :close-on-click-outside="false" :close-on-click-overlay="false">
|
||||
<year-van-dropdown-item :num="5" @change="doSearch" v-model="pageForm.year"></year-van-dropdown-item>
|
||||
<van-dropdown-item v-model="pageForm.activityType" :options="typeOptions" :multiple="false"
|
||||
@change="doSearch"></van-dropdown-item>
|
||||
</van-dropdown-menu>
|
||||
</van-sticky>
|
||||
<div class="family-mine-content">
|
||||
<section class="family-mine-hero">
|
||||
<img class="family-mine-hero__image" src="/assets/mobile/img/activity/hero/family-hero-v3.png" alt="亲子活动插画">
|
||||
<div class="family-mine-hero__copy">
|
||||
<div class="family-mine-hero__title">我的报名</div>
|
||||
<div class="family-mine-hero__subtitle">亲子活动 · 报名记录</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<table-list api="/platform/family/apply/activityPageData"
|
||||
:page_form.sync="pageForm"
|
||||
ref="tableListRef"
|
||||
title="activityName"
|
||||
img="cover"
|
||||
@ready="onReady"
|
||||
>
|
||||
<template v-slot="{index,row}">
|
||||
<table-column label="面向对象">{{row.activityGroupName}}</table-column>
|
||||
<table-column label="报名时间">
|
||||
{{$moment(row.activitySignUpStartTime).format('MM/DD HH:mm') + '~' + $moment(row.activitySignUpEndTime).format('MM/DD HH:mm')}}
|
||||
</table-column>
|
||||
<table-column label="活动时间">
|
||||
{{$moment(row.activityStartTime).format('MM/DD HH:mm') + '~' + $moment(row.activityEndTime).format('MM/DD HH:mm')}}
|
||||
</table-column>
|
||||
</template>
|
||||
<template #actions="{index,row}">
|
||||
<div class="action-btn" @click="onView(row)">
|
||||
<i class="fa fa-eye"></i>
|
||||
<span>活动介绍</span>
|
||||
</div>
|
||||
<div class="action-btn" @click="onInfo(row)">
|
||||
<i class="fa fa-eye"></i>
|
||||
<span>{{row.keyWord}}信息</span>
|
||||
</div>
|
||||
<div class="action-btn" @click="onApply(row)">
|
||||
<i class="fa fa-edit"></i>
|
||||
<span>操作</span>
|
||||
</div>
|
||||
</template>
|
||||
</table-list>
|
||||
<van-sticky :offset-top="46">
|
||||
<div class="family-mine-sticky">
|
||||
<van-search v-model="pageForm.searchKeyword" class="family-mine-search" placeholder="搜索已报名活动" shape="round" clearable @search="doSearch" @clear="doSearch"></van-search>
|
||||
<van-dropdown-menu class="family-mine-filter" :class="{'is-open': filterOpened}" active-color="#1989fa">
|
||||
<year-van-dropdown-item :num="5" @change="doSearch" @open="$set($data, 'filterOpened', true)" @close="$set($data, 'filterOpened', false)" v-model="pageForm.year"></year-van-dropdown-item>
|
||||
<van-dropdown-item v-model="pageForm.activityType" :options="typeOptions" :multiple="false" @change="doSearch" @open="$set($data, 'filterOpened', true)" @close="$set($data, 'filterOpened', false)"></van-dropdown-item>
|
||||
</van-dropdown-menu>
|
||||
</div>
|
||||
</van-sticky>
|
||||
|
||||
<van-action-sheet :close-on-click-overlay="false" title="活动详细信息" v-model="infoVisible" cancel-text="取消">
|
||||
<van-pull-refresh v-model="refreshing" @refresh="onRefresh">
|
||||
<van-list v-model="loading" :finished="finished" :immediate-check="false" @load="onLoad">
|
||||
<div class="family-mine-list">
|
||||
<div v-for="row in tableData" :key="row.id" class="family-mine-card">
|
||||
<img v-if="hasCover(row) && !imageLoadErrors[row.id]" class="family-mine-card__cover" :src="row.cover" :alt="row.activityName" @error="handleImageError(row)">
|
||||
<div v-else class="family-mine-card__cover-state" :class="{'is-error': hasCover(row)}">
|
||||
<van-icon name="photo-o"></van-icon>
|
||||
<span>{{ hasCover(row) ? '图片加载失败' : '暂无图片' }}</span>
|
||||
</div>
|
||||
<div class="family-mine-card__body">
|
||||
<div class="family-mine-card__title">{{ row.activityName }}</div>
|
||||
<div class="family-mine-card__time" v-if="row.activitySignUpStartTime && row.activitySignUpEndTime">
|
||||
报名时间:{{ $moment(row.activitySignUpStartTime).format('MM/DD HH:mm') }}~{{ $moment(row.activitySignUpEndTime).format('MM/DD HH:mm') }}
|
||||
</div>
|
||||
<div class="family-mine-card__time" v-if="row.activityStartTime && row.activityEndTime">
|
||||
活动时间:{{ $moment(row.activityStartTime).format('MM/DD HH:mm') }}~{{ $moment(row.activityEndTime).format('MM/DD HH:mm') }}
|
||||
</div>
|
||||
<div class="family-mine-card__meta" v-if="row.activityGroupName || row.trainType">
|
||||
<span class="family-mine-card__tag" v-if="row.activityGroupName"><span class="family-mine-card__tag-label">面向对象</span>{{ row.activityGroupName }}</span>
|
||||
<span class="family-mine-card__tag" v-if="row.trainType"><span class="family-mine-card__tag-label">活动类型</span>{{ row.trainType }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="family-mine-card__actions">
|
||||
<button type="button" class="family-mine-card__action" @click="onView(row)"><van-icon name="eye-o"></van-icon>查看介绍</button>
|
||||
<button type="button" class="family-mine-card__action" @click="onInfo(row)"><van-icon name="calendar-o"></van-icon>我的课程</button>
|
||||
<button type="button" class="family-mine-card__action is-primary" @click="onApply(row)"><van-icon name="edit"></van-icon>查看报名</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</van-list>
|
||||
<div v-if="!loading && tableData.length === 0" class="family-mine-empty">
|
||||
<img src="/assets/mobile/img/activity/family-empty.png" alt="亲子活动报名空状态插画">
|
||||
<div class="family-mine-empty__title">暂无报名记录</div>
|
||||
<div class="family-mine-empty__hint">当前筛选条件下暂时没有已报名活动</div>
|
||||
</div>
|
||||
</van-pull-refresh>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-container">
|
||||
<div>
|
||||
<van-image class="info-img" height="186" width="100%" :src="infoRow.cover"></van-image>
|
||||
<div class="info-title">{{infoRow.activityName}}</div>
|
||||
<pdf-preview :content="infoRow.introduce"></pdf-preview>
|
||||
<van-popup :style="{height: '100%', width: '100%'}" position="right" v-model="infoVisible">
|
||||
<div class="family-mine-intro-page">
|
||||
<van-nav-bar title="活动介绍" left-text="返回" left-arrow @click-left="historyBack"></van-nav-bar>
|
||||
<div class="family-mine-intro-scroll">
|
||||
<div class="family-mine-intro-hero">
|
||||
<div class="family-mine-intro-hero__icon"><van-icon name="description"></van-icon></div>
|
||||
<div>
|
||||
<div class="family-mine-intro-hero__title">亲子活动报名须知</div>
|
||||
<div class="family-mine-intro-hero__subtitle">请仔细阅读活动安排与报名要求</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="infoRow.introduce" class="family-mine-intro-card" v-html="infoRow.introduce"></div>
|
||||
<div v-else class="family-mine-intro-card">暂无活动介绍</div>
|
||||
</div>
|
||||
</div>
|
||||
</van-action-sheet>
|
||||
|
||||
</van-popup>
|
||||
<van-action-sheet :close-on-click-overlay="false" title="报名信息" v-model="courseVisible" cancel-text="取消">
|
||||
<div class="table-list-container">
|
||||
<div v-for="(row, index) in mineCourses" :key="index" class="table-list-item">
|
||||
@@ -280,18 +494,20 @@ layout("/layouts/platform_h5.html"){
|
||||
<div class="item-actions"></div>
|
||||
</div>
|
||||
</van-action-sheet>
|
||||
|
||||
</div>
|
||||
|
||||
<script nonce="${cspNonce!}">
|
||||
const vue = new Vue({
|
||||
el: "#app",
|
||||
store,
|
||||
components: {
|
||||
"pdf-preview": httpVueLoader("/components/plugins/sysFilePreview/PdfIndex.vue?v=" + new Date().getTime())
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
tableData: [],
|
||||
loading: false,
|
||||
finished: false,
|
||||
refreshing: false,
|
||||
imageLoadErrors: {},
|
||||
filterOpened: false,
|
||||
pageForm: {
|
||||
pageNumber: 1,
|
||||
pageSize: 10,
|
||||
@@ -307,20 +523,12 @@ layout("/layouts/platform_h5.html"){
|
||||
{text: '已结束', value: 3},
|
||||
],
|
||||
infoVisible: false,
|
||||
infoRow: {},
|
||||
|
||||
mineCourses: [],
|
||||
courseVisible: false,
|
||||
mineCourses: [],
|
||||
infoRow: {},
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onView(row) {
|
||||
this.infoRow = row
|
||||
this.infoVisible = true
|
||||
},
|
||||
onApply(row) {
|
||||
this.$pjaxReplace('/platform/family/apply/list/h5?id=' + row.id + '&dataType=mine')
|
||||
},
|
||||
onInfo(row) {
|
||||
this.$axios.post('/platform/family/mine/queryMineCourse', {activityId: row.id})
|
||||
.then((res) => {
|
||||
@@ -328,17 +536,58 @@ layout("/layouts/platform_h5.html"){
|
||||
this.courseVisible = true
|
||||
})
|
||||
},
|
||||
onReady() {
|
||||
this.doSearch()
|
||||
hasCover(row) {
|
||||
return !!(row && row.cover && String(row.cover).trim())
|
||||
},
|
||||
doSearch() {
|
||||
this.$nextTick(() => {
|
||||
handleImageError(row) {
|
||||
this.$set(this.imageLoadErrors, row.id, true)
|
||||
},
|
||||
onView(row) {
|
||||
this.infoRow = row
|
||||
this.infoVisible = true
|
||||
},
|
||||
onApply(row) {
|
||||
this.$pjaxReplace('/platform/family/apply/list/h5?id=' + row.id + '&dataType=mine')
|
||||
},
|
||||
onLoad() {
|
||||
if(!this.finished) {
|
||||
this.loadActivities(false)
|
||||
}
|
||||
},
|
||||
onRefresh() {
|
||||
this.loadActivities(true)
|
||||
},
|
||||
loadActivities(reset) {
|
||||
if(reset) {
|
||||
this.pageForm.pageNumber = 1
|
||||
this.pageForm.totalCount = 0
|
||||
this.$refs.tableListRef.doSearch()
|
||||
this.tableData = []
|
||||
this.finished = false
|
||||
this.imageLoadErrors = {}
|
||||
}
|
||||
this.loading = true
|
||||
this.$axios.post('/platform/family/apply/activityPageData', this.pageForm).then((res) => {
|
||||
if(res.code === 0) {
|
||||
const list = res.data && res.data.list ? res.data.list : []
|
||||
this.tableData = this.pageForm.pageNumber === 1 ? list : this.tableData.concat(list)
|
||||
this.pageForm.totalCount = res.data && res.data.totalCount ? res.data.totalCount : 0
|
||||
this.finished = this.tableData.length >= this.pageForm.totalCount
|
||||
if(!this.finished) {
|
||||
this.pageForm.pageNumber++
|
||||
}
|
||||
}
|
||||
}).finally(() => {
|
||||
this.loading = false
|
||||
this.refreshing = false
|
||||
})
|
||||
},
|
||||
doSearch() {
|
||||
this.loadActivities(true)
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.loadActivities(true)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -2,53 +2,682 @@
|
||||
layout("/layouts/platform_h5.html"){
|
||||
#-->
|
||||
<style>
|
||||
.van-tag--default {
|
||||
background-color: #f1f1f1 !important;
|
||||
body {
|
||||
background-color: #f3f7fd;
|
||||
}
|
||||
|
||||
.sports-activity-page {
|
||||
min-height: 100vh;
|
||||
padding-bottom: calc(24px + env(safe-area-inset-bottom));
|
||||
box-sizing: border-box;
|
||||
background-color: #f3f7fd;
|
||||
}
|
||||
|
||||
.sports-activity-page .van-nav-bar {
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.sports-activity-page .van-nav-bar__title {
|
||||
color: #1f2937;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.sports-activity-page .van-nav-bar .van-icon,
|
||||
.sports-activity-page .van-nav-bar__text {
|
||||
color: #397fbd;
|
||||
}
|
||||
|
||||
.sports-activity-content {
|
||||
padding: 12px 12px 0;
|
||||
}
|
||||
|
||||
.sports-activity-hero {
|
||||
position: relative;
|
||||
height: 134px;
|
||||
overflow: hidden;
|
||||
border-radius: 10px;
|
||||
color: #fff;
|
||||
background-color: #247fe8;
|
||||
box-shadow: 0 8px 20px rgba(51, 117, 219, .18);
|
||||
}
|
||||
|
||||
.sports-activity-hero__image {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
}
|
||||
|
||||
.sports-activity-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%);
|
||||
}
|
||||
|
||||
.sports-activity-hero__title {
|
||||
font-size: 25px;
|
||||
line-height: 34px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 1px;
|
||||
text-shadow: 0 2px 6px rgba(17, 86, 182, .18);
|
||||
}
|
||||
|
||||
.sports-activity-hero__subtitle {
|
||||
margin-top: 4px;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
letter-spacing: .5px;
|
||||
opacity: .95;
|
||||
}
|
||||
|
||||
.sports-activity-sticky {
|
||||
padding: 10px 0 1px;
|
||||
background-color: #f3f7fd;
|
||||
}
|
||||
|
||||
.van-sticky--fixed .sports-activity-sticky {
|
||||
padding-right: 12px;
|
||||
padding-left: 12px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.sports-activity-search {
|
||||
margin: 0 0 8px;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
border-radius: 12px;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 5px 16px rgba(43, 73, 112, .1);
|
||||
}
|
||||
|
||||
.sports-activity-search .van-search__content {
|
||||
height: 42px;
|
||||
padding-left: 12px;
|
||||
align-items: center;
|
||||
border-radius: 12px;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.sports-activity-search .van-field__control {
|
||||
color: #263548;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.sports-activity-search .van-field__control::placeholder {
|
||||
color: #a7b0bf;
|
||||
}
|
||||
|
||||
.sports-activity-filter {
|
||||
margin: 0 0 10px;
|
||||
overflow: hidden;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 5px 16px rgba(43, 73, 112, .08);
|
||||
transition: border-radius .18s ease;
|
||||
}
|
||||
|
||||
.sports-activity-filter.is-open {
|
||||
border-radius: 10px 10px 0 0;
|
||||
}
|
||||
|
||||
.sports-activity-filter .van-dropdown-menu__bar {
|
||||
height: 44px;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.sports-activity-filter .van-dropdown-menu__title {
|
||||
color: #66758a;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.sports-activity-filter .van-dropdown-menu__title--active,
|
||||
.sports-activity-filter .van-dropdown-item__option--active,
|
||||
.sports-activity-filter .van-dropdown-item__option--active .van-dropdown-item__icon {
|
||||
color: #1989fa;
|
||||
}
|
||||
|
||||
.sports-activity-filter .van-dropdown-menu__title--down::after {
|
||||
border-color: transparent transparent #1989fa #1989fa;
|
||||
}
|
||||
|
||||
.sports-activity-filter .van-dropdown-item__content {
|
||||
width: calc(100% - 24px);
|
||||
margin: 0 12px;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
border-radius: 0 0 12px 12px;
|
||||
box-shadow: 0 10px 24px rgba(31, 65, 108, .14);
|
||||
}
|
||||
|
||||
.sports-activity-filter .van-dropdown-item__option {
|
||||
min-height: 48px;
|
||||
padding: 0 16px;
|
||||
color: #46566c;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.sports-activity-filter .van-cell::after {
|
||||
right: 16px;
|
||||
left: 16px;
|
||||
}
|
||||
|
||||
.sports-activity-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.sports-activity-card {
|
||||
display: flex;
|
||||
min-height: 74px;
|
||||
padding: 14px;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
border-radius: 10px;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 7px 20px rgba(55, 85, 126, .08);
|
||||
}
|
||||
|
||||
.sports-activity-card {
|
||||
position: relative;
|
||||
min-height: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.sports-activity-card__icon {
|
||||
display: flex;
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
margin-right: 12px;
|
||||
flex: 0 0 38px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 9px;
|
||||
color: #4d91ef;
|
||||
background-color: #edf4ff;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.sports-activity-card__body {
|
||||
min-width: 0;
|
||||
padding: 12px 38px 14px 14px;
|
||||
box-sizing: border-box;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.sports-activity-card__cover {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 150px;
|
||||
margin: 0;
|
||||
flex: 0 0 150px;
|
||||
object-fit: cover;
|
||||
border-radius: 0;
|
||||
background-color: #edf4ff;
|
||||
}
|
||||
|
||||
.sports-activity-card__cover-state {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 150px;
|
||||
flex: 0 0 150px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
row-gap: 8px;
|
||||
color: #8da1ba;
|
||||
background-color: #edf4fc;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.sports-activity-card__cover-state .van-icon {
|
||||
font-size: 34px;
|
||||
}
|
||||
|
||||
.sports-activity-card__cover-state.is-error {
|
||||
color: #c27b75;
|
||||
background-color: #fff3f1;
|
||||
}
|
||||
|
||||
.sports-activity-card > .sports-activity-card__icon {
|
||||
width: 100%;
|
||||
height: 150px;
|
||||
margin: 0;
|
||||
flex: 0 0 150px;
|
||||
border-radius: 0;
|
||||
font-size: 40px;
|
||||
}
|
||||
|
||||
.sports-activity-card__title {
|
||||
color: #303846;
|
||||
font-size: 14px;
|
||||
line-height: 21px;
|
||||
}
|
||||
|
||||
.sports-activity-card__title {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.sports-activity-card__time {
|
||||
margin-top: 4px;
|
||||
color: #98a2b2;
|
||||
font-size: 12px;
|
||||
line-height: 17px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.sports-activity-card__meta {
|
||||
margin-top: 10px;
|
||||
padding-top: 9px;
|
||||
border-top: 1px solid #edf1f6;
|
||||
}
|
||||
|
||||
.sports-activity-card__address {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
color: #69788d;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.sports-activity-card__address .van-icon {
|
||||
margin: 2px 5px 0 0;
|
||||
flex: none;
|
||||
color: #6c9ff8;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.sports-activity-card__address span {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.sports-activity-card__tags {
|
||||
display: flex;
|
||||
margin-top: 8px;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.sports-activity-card__tag {
|
||||
display: inline-flex;
|
||||
padding: 3px 8px;
|
||||
align-items: center;
|
||||
border-radius: 4px;
|
||||
color: #397de0;
|
||||
background-color: #edf5ff;
|
||||
font-size: 11px;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.sports-activity-card__tag-label {
|
||||
margin-right: 4px;
|
||||
color: #8aa7cf;
|
||||
}
|
||||
|
||||
.sports-activity-card__arrow {
|
||||
position: absolute;
|
||||
top: 164px;
|
||||
right: 14px;
|
||||
color: #b4bdca;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.sports-activity-empty-illustration {
|
||||
display: flex;
|
||||
min-height: 250px;
|
||||
padding: 24px 18px 32px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.sports-activity-empty-illustration img {
|
||||
display: block;
|
||||
width: 82%;
|
||||
max-width: 280px;
|
||||
height: auto;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.sports-activity-empty-illustration__title {
|
||||
margin-top: -8px;
|
||||
color: #50627a;
|
||||
font-size: 15px;
|
||||
line-height: 22px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.sports-activity-empty-illustration__hint {
|
||||
margin-top: 6px;
|
||||
color: #9aa7b8;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.activity-notification-popup,
|
||||
.activity-notification-page {
|
||||
background-color: #f3f7fd;
|
||||
}
|
||||
|
||||
.activity-notification-page {
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.activity-notification-page .van-nav-bar {
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.activity-notification-page .van-nav-bar__title {
|
||||
color: #1f2937;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.activity-notification-page .van-nav-bar .van-icon,
|
||||
.activity-notification-page .van-nav-bar__text {
|
||||
color: #397fbd;
|
||||
}
|
||||
|
||||
.activity-notification-scroll {
|
||||
height: calc(100vh - 46px);
|
||||
padding: 12px 12px calc(92px + env(safe-area-inset-bottom));
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.activity-notification-hero {
|
||||
display: flex;
|
||||
min-height: 82px;
|
||||
padding: 16px;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
border-radius: 12px;
|
||||
color: #fff;
|
||||
background: linear-gradient(120deg, #2d7eea 0%, #5ba6f7 100%);
|
||||
box-shadow: 0 8px 20px rgba(45, 126, 234, .18);
|
||||
}
|
||||
|
||||
.activity-notification-hero__icon {
|
||||
display: flex;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
margin-right: 12px;
|
||||
flex: 0 0 44px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 11px;
|
||||
color: #2d7eea;
|
||||
background-color: rgba(255, 255, 255, .92);
|
||||
font-size: 25px;
|
||||
}
|
||||
|
||||
.activity-notification-hero__title {
|
||||
font-size: 17px;
|
||||
line-height: 24px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.activity-notification-hero__subtitle {
|
||||
margin-top: 3px;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
opacity: .9;
|
||||
}
|
||||
|
||||
.activity-notification-card {
|
||||
margin-top: 12px;
|
||||
padding: 18px 16px;
|
||||
overflow: hidden;
|
||||
border-radius: 12px;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 7px 20px rgba(55, 85, 126, .08);
|
||||
}
|
||||
|
||||
.activity-notification-content,
|
||||
.activity-notification-content div,
|
||||
.activity-notification-content p,
|
||||
.activity-notification-content span,
|
||||
.activity-notification-content li {
|
||||
color: #3f4c5e !important;
|
||||
font-size: 14px !important;
|
||||
line-height: 1.8 !important;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.activity-notification-content h1,
|
||||
.activity-notification-content h2,
|
||||
.activity-notification-content h3 {
|
||||
margin: 16px 0 10px !important;
|
||||
color: #1f2f43 !important;
|
||||
font-size: 19px !important;
|
||||
line-height: 1.5 !important;
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
|
||||
.activity-notification-content > :first-child {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
|
||||
.activity-notification-content p {
|
||||
margin-top: 0 !important;
|
||||
margin-bottom: 8px !important;
|
||||
}
|
||||
|
||||
.activity-notification-content img {
|
||||
max-width: 100% !important;
|
||||
height: auto !important;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.activity-notification-content table {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.activity-notification-empty {
|
||||
display: flex;
|
||||
min-height: 180px;
|
||||
margin-top: 12px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
row-gap: 10px;
|
||||
border-radius: 12px;
|
||||
color: #9aa7b8;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 7px 20px rgba(55, 85, 126, .08);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.activity-notification-empty .van-icon {
|
||||
color: #83b5f4;
|
||||
font-size: 36px;
|
||||
}
|
||||
|
||||
.activity-notification-footer {
|
||||
position: fixed;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 5;
|
||||
padding: 10px 12px calc(10px + env(safe-area-inset-bottom));
|
||||
border-top: 1px solid #edf1f6;
|
||||
background-color: rgba(255, 255, 255, .96);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.activity-notification-footer .van-button {
|
||||
height: 42px;
|
||||
border: 0;
|
||||
font-size: 15px;
|
||||
background-color: #1989fa;
|
||||
box-shadow: 0 6px 16px rgba(25, 137, 250, .22);
|
||||
}
|
||||
|
||||
.activity-notification-footer .van-button.is-disabled,
|
||||
.activity-notification-footer .van-button--disabled {
|
||||
color: #8b98aa !important;
|
||||
background-color: #eef2f7 !important;
|
||||
opacity: 1;
|
||||
box-shadow: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="app" v-cloak>
|
||||
<van-nav-bar title="活动列表" @click-left="()=>this.$pjaxReplace('/platform/home')" left-arrow left-text="返回" placeholder fixed></van-nav-bar>
|
||||
<van-sticky offset-top="46px">
|
||||
<van-dropdown-menu>
|
||||
<year-van-dropdown-item :num="5" @change="doSearch" v-model="pageForm.year"></year-van-dropdown-item>
|
||||
<van-dropdown-item :options="stateList" @change="doSearch"
|
||||
v-model="pageForm.isActivity"></van-dropdown-item>
|
||||
<van-dropdown-item :options="applyStatusOptions" @change="doSearch"
|
||||
v-model="pageForm.applyStatus"></van-dropdown-item>
|
||||
</van-dropdown-menu>
|
||||
</van-sticky>
|
||||
<div class="sports-activity-page">
|
||||
<van-nav-bar
|
||||
title="体育活动"
|
||||
left-arrow
|
||||
left-text="返回"
|
||||
fixed
|
||||
placeholder
|
||||
@click-left="goBack"
|
||||
></van-nav-bar>
|
||||
|
||||
<table-list api="/platform/activity/apply/h5/activityData"
|
||||
:page_form.sync="pageForm"
|
||||
ref="tableListRef"
|
||||
title="NAME"
|
||||
img="image"
|
||||
@ready="onReady"
|
||||
>
|
||||
<template v-slot="{index,row}">
|
||||
<table-column label="报名时间">
|
||||
{{$moment(row.applyStartTime).format('MM/DD HH:mm') + '~' + $moment(row.applyEndTime).format('MM/DD HH:mm')}}
|
||||
</table-column>
|
||||
<table-column label="活动时间">
|
||||
{{$moment(row.startTime).format('MM/DD HH:mm') + '~' + $moment(row.endTime).format('MM/DD HH:mm')}}
|
||||
</table-column>
|
||||
</template>
|
||||
<template #actions="{index,row}">
|
||||
<template v-if="$moment().isBefore($moment(row.applyEndTime))">
|
||||
<div class="action-btn" @click="openView(row)">
|
||||
<i class="fa fa-edit"></i>
|
||||
<span>去报名</span>
|
||||
<div class="sports-activity-content">
|
||||
<section class="sports-activity-hero">
|
||||
<img
|
||||
class="sports-activity-hero__image"
|
||||
src="/assets/mobile/img/activity/hero/sports-hero-v2.png"
|
||||
alt="体育活动插画"
|
||||
>
|
||||
<div class="sports-activity-hero__copy">
|
||||
<div class="sports-activity-hero__title">体育活动</div>
|
||||
<div class="sports-activity-hero__subtitle">强健体魄 · 运动同行</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</table-list>
|
||||
</section>
|
||||
|
||||
<activity_event_notification
|
||||
:event_notification="viewData.eventNotification"
|
||||
:id="viewData.id"
|
||||
ref="eventNotification"
|
||||
></activity_event_notification>
|
||||
<van-sticky :offset-top="46">
|
||||
<div class="sports-activity-sticky">
|
||||
<van-search
|
||||
v-model="searchKeyword"
|
||||
class="sports-activity-search"
|
||||
placeholder="搜索体育活动"
|
||||
shape="round"
|
||||
clearable
|
||||
@search="doSearch"
|
||||
@clear="doSearch"
|
||||
></van-search>
|
||||
|
||||
<van-dropdown-menu
|
||||
class="sports-activity-filter"
|
||||
:class="{'is-open': filterOpened}"
|
||||
active-color="#1989fa"
|
||||
>
|
||||
<year-van-dropdown-item
|
||||
:num="5"
|
||||
v-model="pageForm.year"
|
||||
@change="doSearch"
|
||||
@open="$set($data, 'filterOpened', true)"
|
||||
@close="$set($data, 'filterOpened', false)"
|
||||
></year-van-dropdown-item>
|
||||
<van-dropdown-item
|
||||
v-model="pageForm.isActivity"
|
||||
:options="stateList"
|
||||
@change="doSearch"
|
||||
@open="$set($data, 'filterOpened', true)"
|
||||
@close="$set($data, 'filterOpened', false)"
|
||||
></van-dropdown-item>
|
||||
<van-dropdown-item
|
||||
v-model="pageForm.applyStatus"
|
||||
:options="applyStatusOptions"
|
||||
@change="doSearch"
|
||||
@open="$set($data, 'filterOpened', true)"
|
||||
@close="$set($data, 'filterOpened', false)"
|
||||
></van-dropdown-item>
|
||||
</van-dropdown-menu>
|
||||
</div>
|
||||
</van-sticky>
|
||||
|
||||
<div class="sports-activity-list">
|
||||
<div
|
||||
v-for="row in activityList"
|
||||
:key="row.id"
|
||||
class="sports-activity-card"
|
||||
@click="openView(row)"
|
||||
>
|
||||
<img
|
||||
v-if="hasActivityImage(row) && !imageLoadErrors[row.id]"
|
||||
class="sports-activity-card__cover"
|
||||
:src="row.image"
|
||||
:alt="row.NAME || row.name"
|
||||
@error="handleImageError(row)"
|
||||
>
|
||||
<div
|
||||
v-else
|
||||
class="sports-activity-card__cover-state"
|
||||
:class="{'is-error': hasActivityImage(row)}"
|
||||
>
|
||||
<van-icon name="photo-o"></van-icon>
|
||||
<span>{{ hasActivityImage(row) ? '图片加载失败' : '暂无图片' }}</span>
|
||||
</div>
|
||||
<div class="sports-activity-card__body">
|
||||
<div class="sports-activity-card__title">{{ row.NAME || row.name }}</div>
|
||||
<div class="sports-activity-card__time" v-if="row.applyStartTime && row.applyEndTime">
|
||||
报名时间:{{ $moment(row.applyStartTime).format('MM/DD HH:mm') }}~{{ $moment(row.applyEndTime).format('MM/DD HH:mm') }}
|
||||
</div>
|
||||
<div class="sports-activity-card__time" v-if="row.startTime && row.endTime">
|
||||
活动时间:{{ $moment(row.startTime).format('MM/DD HH:mm') }}~{{ $moment(row.endTime).format('MM/DD HH:mm') }}
|
||||
</div>
|
||||
<div class="sports-activity-card__meta" v-if="row.address || formatGameStyle(row.gameStyle) || formatApplyType(row.applyType) || formatApplyWay(row.applyWay)">
|
||||
<div class="sports-activity-card__address" v-if="row.address">
|
||||
<van-icon name="location-o"></van-icon>
|
||||
<span>{{ row.address }}</span>
|
||||
</div>
|
||||
<div class="sports-activity-card__tags" v-if="formatGameStyle(row.gameStyle) || formatApplyType(row.applyType) || formatApplyWay(row.applyWay)">
|
||||
<span class="sports-activity-card__tag" v-if="formatGameStyle(row.gameStyle)">
|
||||
<span class="sports-activity-card__tag-label">竞赛方式</span>{{ formatGameStyle(row.gameStyle) }}
|
||||
</span>
|
||||
<span class="sports-activity-card__tag" v-if="formatApplyType(row.applyType)">
|
||||
<span class="sports-activity-card__tag-label">活动类型</span>{{ formatApplyType(row.applyType) }}
|
||||
</span>
|
||||
<span class="sports-activity-card__tag" v-if="formatApplyWay(row.applyWay)">
|
||||
<span class="sports-activity-card__tag-label">报名方式</span>{{ formatApplyWay(row.applyWay) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<van-icon class="sports-activity-card__arrow" name="arrow"></van-icon>
|
||||
</div>
|
||||
|
||||
<div v-if="activityList.length === 0" class="sports-activity-empty-illustration">
|
||||
<img src="/assets/mobile/img/activity/sports-empty.png" alt="体育活动空状态插画">
|
||||
<div class="sports-activity-empty-illustration__title">{{ emptyText }}</div>
|
||||
<div class="sports-activity-empty-illustration__hint">{{ emptyHint }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<activity_event_notification
|
||||
:event_notification="viewData.eventNotification"
|
||||
:id="viewData.id"
|
||||
:apply_start_time="viewData.applyStartTime"
|
||||
:apply_end_time="viewData.applyEndTime"
|
||||
ref="eventNotification"
|
||||
></activity_event_notification>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script nonce="${cspNonce!}">
|
||||
@@ -57,52 +686,106 @@ layout("/layouts/platform_h5.html"){
|
||||
el: "#app",
|
||||
data() {
|
||||
return {
|
||||
tableData: [],
|
||||
finished: false,
|
||||
loading: false,
|
||||
activityList: [],
|
||||
searchKeyword: "",
|
||||
imageLoadErrors: {},
|
||||
filterOpened: false,
|
||||
pageForm: {
|
||||
isActivity: 2,
|
||||
applyStatus: 1,
|
||||
year: new Date().getFullYear(),
|
||||
pageNumber: 1,
|
||||
pageSize: 5,
|
||||
totalCount: 0
|
||||
year: new Date().getFullYear()
|
||||
},
|
||||
yearList: [],
|
||||
stateList: [
|
||||
{value: 1, text: "全部活动"},
|
||||
{value: 2, text: "报名中"},
|
||||
{value: 3, text: "报名已结束"}
|
||||
],
|
||||
applyStatusOptions: [
|
||||
{value: 1, text: "未报名"},
|
||||
{value: 2, text: "已报名"}
|
||||
],
|
||||
stateList: [
|
||||
{value: 1, text: "全部"},
|
||||
{value: 2, text: "报名中"},
|
||||
{value: 3, text: "报名已结束"}
|
||||
],
|
||||
subLoading: false,
|
||||
viewShow: false,
|
||||
viewData: {}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
emptyText() {
|
||||
if (this.searchKeyword.trim()) return "未找到相关体育活动"
|
||||
if (this.pageForm.isActivity === 3) return "暂无已结束的体育活动"
|
||||
if (this.pageForm.isActivity === 2) return "暂无报名中的体育活动"
|
||||
return "暂无体育活动"
|
||||
},
|
||||
emptyHint() {
|
||||
return this.searchKeyword.trim() ? "请尝试更换搜索关键词" : "当前筛选条件下暂时没有活动"
|
||||
}
|
||||
},
|
||||
components: {
|
||||
activity_event_notification: ACTIVITY_EVENT_NOTIFICATION
|
||||
},
|
||||
methods: {
|
||||
goBack() {
|
||||
this.$pjaxReplace("/platform/home")
|
||||
},
|
||||
openView(row) {
|
||||
this.viewData = row
|
||||
this.$refs.eventNotification.viewShow = true
|
||||
},
|
||||
doSearch() {
|
||||
this.$nextTick(() => {
|
||||
this.pageForm.pageNumber = 1
|
||||
this.pageForm.totalCount = 0
|
||||
this.$refs.tableListRef.doSearch()
|
||||
this.$refs.eventNotification.viewShow = true
|
||||
})
|
||||
},
|
||||
onReady() {
|
||||
this.doSearch()
|
||||
doSearch() {
|
||||
this.loadActivities()
|
||||
},
|
||||
hasActivityImage(row) {
|
||||
return !!(row && row.image && String(row.image).trim())
|
||||
},
|
||||
handleImageError(row) {
|
||||
this.$set(this.imageLoadErrors, row.id, true)
|
||||
},
|
||||
formatGameStyle(value) {
|
||||
return {
|
||||
1: "竞赛类",
|
||||
2: "非竞赛类"
|
||||
}[Number(value)] || ""
|
||||
},
|
||||
formatApplyType(value) {
|
||||
return {
|
||||
1: "单一团体赛",
|
||||
2: "综合赛",
|
||||
3: "个人赛"
|
||||
}[Number(value)] || ""
|
||||
},
|
||||
formatApplyWay(value) {
|
||||
if (value === null || value === undefined || value === "") return ""
|
||||
let values = value
|
||||
if (typeof values === "string") {
|
||||
try {
|
||||
values = JSON.parse(values)
|
||||
} catch (e) {
|
||||
values = values.split(",")
|
||||
}
|
||||
}
|
||||
if (!Array.isArray(values)) values = [values]
|
||||
const labels = {
|
||||
1: "个人报名",
|
||||
2: "分工会报名",
|
||||
3: "小程序报名"
|
||||
}
|
||||
return values.map(item => labels[Number(item)]).filter(Boolean).join("、")
|
||||
},
|
||||
loadActivities() {
|
||||
this.activityList = []
|
||||
this.imageLoadErrors = {}
|
||||
const queryForm = Object.assign({}, this.pageForm, {
|
||||
searchKeyword: this.searchKeyword.trim()
|
||||
})
|
||||
this.$axios.post("/platform/activity/apply/h5/activityData", queryForm).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.activityList = res.data && res.data.list ? res.data.list : []
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.loadActivities()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1,8 +1,580 @@
|
||||
<!--#
|
||||
layout("/layouts/platform_h5.html"){
|
||||
#-->
|
||||
<style>
|
||||
body {
|
||||
background-color: #f3f7fd;
|
||||
}
|
||||
|
||||
.sports-event-page {
|
||||
min-height: 100vh;
|
||||
padding-bottom: calc(24px + env(safe-area-inset-bottom));
|
||||
box-sizing: border-box;
|
||||
background-color: #f3f7fd;
|
||||
}
|
||||
|
||||
.sports-event-page .van-nav-bar {
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.sports-event-page .van-nav-bar__title {
|
||||
color: #1f2937;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.sports-event-page .van-nav-bar .van-icon,
|
||||
.sports-event-page .van-nav-bar__text {
|
||||
color: #397fbd;
|
||||
}
|
||||
|
||||
.sports-event-summary {
|
||||
display: flex;
|
||||
min-height: 82px;
|
||||
margin: 12px 12px 0;
|
||||
padding: 16px;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
border-radius: 12px;
|
||||
color: #fff;
|
||||
background: linear-gradient(120deg, #2d7eea 0%, #5ba6f7 100%);
|
||||
box-shadow: 0 8px 20px rgba(45, 126, 234, .18);
|
||||
}
|
||||
|
||||
.sports-event-summary__icon {
|
||||
display: flex;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
margin-right: 12px;
|
||||
overflow: hidden;
|
||||
flex: 0 0 44px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 11px;
|
||||
color: #2d7eea;
|
||||
background-color: rgba(255, 255, 255, .92);
|
||||
font-size: 25px;
|
||||
}
|
||||
|
||||
.sports-event-summary__body {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.sports-event-summary__title {
|
||||
overflow: hidden;
|
||||
color: #fff;
|
||||
font-size: 17px;
|
||||
line-height: 24px;
|
||||
font-weight: 600;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.sports-event-summary__subtitle {
|
||||
margin-top: 3px;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
opacity: .9;
|
||||
}
|
||||
|
||||
.sports-event-sticky {
|
||||
padding: 10px 0 8px;
|
||||
background-color: #f3f7fd;
|
||||
}
|
||||
|
||||
.sports-event-search {
|
||||
margin: 0 12px;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.sports-event-search .van-search__content {
|
||||
height: 42px;
|
||||
padding-left: 13px;
|
||||
align-items: center;
|
||||
border-radius: 12px;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 5px 16px rgba(55, 85, 126, .07);
|
||||
}
|
||||
|
||||
.sports-event-search .van-field__control {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.sports-event-list-head {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
margin: 16px 0 0;
|
||||
padding: 0 12px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.sports-event-list-head__title {
|
||||
color: #263548;
|
||||
font-size: 16px;
|
||||
line-height: 22px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.sports-event-list-head__count {
|
||||
color: #8d9aad;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.sports-event-list-head__tools {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.sports-event-view-toggle {
|
||||
display: inline-flex;
|
||||
height: 28px;
|
||||
padding: 0 10px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 0;
|
||||
border-radius: 14px;
|
||||
color: #397de0;
|
||||
background-color: #eaf3ff;
|
||||
font-size: 12px;
|
||||
line-height: 28px;
|
||||
}
|
||||
|
||||
.sports-event-view-toggle i {
|
||||
margin-right: 5px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.sports-event-page .sports-event-card-list {
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.sports-event-page .table-card {
|
||||
margin-bottom: 12px;
|
||||
overflow: hidden;
|
||||
border-radius: 12px;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 7px 20px rgba(55, 85, 126, .08);
|
||||
}
|
||||
|
||||
.sports-event-page .table-card > .van-image,
|
||||
.sports-event-page .table-card > .van-image img {
|
||||
display: block;
|
||||
border-radius: 12px 12px 0 0;
|
||||
}
|
||||
|
||||
.sports-event-page .table-card .van-cell {
|
||||
padding: 9px 16px;
|
||||
color: #566579;
|
||||
font-size: 13px;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.sports-event-page .table-card .van-cell:first-of-type {
|
||||
padding-top: 15px;
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
||||
.sports-event-page .table-card .van-cell::after {
|
||||
right: 16px;
|
||||
left: 16px;
|
||||
border-color: #edf1f6;
|
||||
}
|
||||
|
||||
.sports-event-page .table-card .van-cell__title {
|
||||
color: #66758a;
|
||||
}
|
||||
|
||||
.sports-event-page .table-card .van-cell__value,
|
||||
.sports-event-page .table-card .van-cell__right-icon {
|
||||
color: #263548;
|
||||
}
|
||||
|
||||
.sports-event-page .table-card .font-size3 {
|
||||
color: #23344a;
|
||||
font-size: 16px;
|
||||
line-height: 23px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.sports-event-page .table-card .van-tag {
|
||||
padding: 3px 7px;
|
||||
justify-content: center;
|
||||
border: 0;
|
||||
border-radius: 4px;
|
||||
box-sizing: border-box;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.sports-event-page .table-card .sports-event-status-tag.is-pending {
|
||||
color: #397de0;
|
||||
background-color: #edf5ff;
|
||||
}
|
||||
|
||||
.sports-event-page .table-card .sports-event-status-tag.is-success {
|
||||
color: #35a86b;
|
||||
background-color: #eaf8f1;
|
||||
}
|
||||
|
||||
.sports-event-page .table-card .sports-event-status-tag.is-rejected {
|
||||
color: #df5b59;
|
||||
background-color: #fff1f0;
|
||||
}
|
||||
|
||||
.sports-event-card__body {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.sports-event-card__title-row {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.sports-event-card__title {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
color: #23344a;
|
||||
font-size: 16px;
|
||||
line-height: 23px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.sports-event-card__status {
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.sports-event-card__activity {
|
||||
display: flex;
|
||||
margin-top: 5px;
|
||||
align-items: center;
|
||||
color: #78879a;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.sports-event-card__activity .van-icon {
|
||||
margin-right: 5px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.sports-event-card__tags,
|
||||
.sports-event-card__config {
|
||||
display: flex;
|
||||
margin-top: 10px;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.sports-event-card__tag,
|
||||
.sports-event-card__config-tag {
|
||||
display: inline-flex;
|
||||
min-height: 24px;
|
||||
padding: 3px 8px;
|
||||
align-items: center;
|
||||
border-radius: 5px;
|
||||
color: #397de0;
|
||||
background-color: #edf5ff;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.sports-event-card__tag-label {
|
||||
margin-right: 4px;
|
||||
color: #7f9fc9;
|
||||
}
|
||||
|
||||
.sports-event-card__stats {
|
||||
display: grid;
|
||||
margin-top: 12px;
|
||||
padding: 12px 0;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
border-top: 1px solid #edf1f6;
|
||||
border-bottom: 1px solid #edf1f6;
|
||||
}
|
||||
|
||||
.sports-event-card__stat {
|
||||
min-width: 0;
|
||||
padding: 0 10px;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.sports-event-card__stat:first-child {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.sports-event-card__stat:last-child {
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.sports-event-card__stat + .sports-event-card__stat {
|
||||
border-left: 1px solid #edf1f6;
|
||||
}
|
||||
|
||||
.sports-event-card__stat-label {
|
||||
color: #7b899b;
|
||||
font-size: 11px;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.sports-event-card__stat-value {
|
||||
margin-top: 3px;
|
||||
overflow: hidden;
|
||||
color: #33445a;
|
||||
font-size: 13px;
|
||||
line-height: 18px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.sports-event-card__config-tag {
|
||||
color: #66758a;
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
.sports-event-card__extra {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.sports-event-card__extra-row {
|
||||
display: flex;
|
||||
padding: 4px 0;
|
||||
align-items: flex-start;
|
||||
color: #66758a;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.sports-event-card__extra-row .van-icon {
|
||||
margin: 2px 6px 0 0;
|
||||
flex: 0 0 auto;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.sports-event-card__extra-label {
|
||||
margin-right: 8px;
|
||||
flex: 0 0 auto;
|
||||
color: #7b899b;
|
||||
}
|
||||
|
||||
.sports-event-card__extra-value {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
color: #33445a;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.sports-event-page .table-card .table-card-footer {
|
||||
min-height: 56px;
|
||||
margin-top: 4px;
|
||||
padding: 12px 16px;
|
||||
align-items: center;
|
||||
border-top: 1px solid #edf1f6;
|
||||
background-color: #f8fbff;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.sports-event-page .table-card .table-card-footer::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sports-event-card__footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.sports-event-card__footer > div {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.sports-event-union-action-hint {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
color: #ed6a0c;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.sports-event-union-action-hint.is-authorized {
|
||||
color: #1989fa;
|
||||
}
|
||||
|
||||
.sports-event-page .table-card .table-card-footer .van-cell__value {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.sports-event-page .table-card-footer .van-button {
|
||||
min-width: 78px;
|
||||
height: 32px;
|
||||
padding: 0 14px;
|
||||
border: 0;
|
||||
border-radius: 16px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.sports-event-page .table-card-footer .van-button i {
|
||||
margin-right: 6px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.sports-event-page .table-card-footer .sports-event-apply-button {
|
||||
width: 108px;
|
||||
min-width: 108px;
|
||||
height: 32px;
|
||||
color: #fff !important;
|
||||
border-color: #1989fa !important;
|
||||
background-color: #1989fa !important;
|
||||
box-shadow: 0 5px 12px rgba(25, 137, 250, .18);
|
||||
}
|
||||
|
||||
.sports-event-page .table-card-footer .sports-event-cancel-button {
|
||||
width: 108px;
|
||||
min-width: 108px;
|
||||
height: 32px;
|
||||
color: #fff !important;
|
||||
border-color: #e84d4f !important;
|
||||
background-color: #e84d4f !important;
|
||||
box-shadow: 0 5px 12px rgba(232, 77, 79, .16);
|
||||
}
|
||||
|
||||
.sports-event-page .table-card-footer .van-button__content {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.sports-event-compact-list {
|
||||
margin: 0 12px;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
border-radius: 12px;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 7px 20px rgba(55, 85, 126, .08);
|
||||
}
|
||||
|
||||
.sports-event-compact-item {
|
||||
display: flex;
|
||||
min-height: 68px;
|
||||
padding: 12px 14px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
box-sizing: border-box;
|
||||
border-bottom: 1px solid #edf1f6;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.sports-event-compact-item:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.sports-event-compact-item__name {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
color: #23344a;
|
||||
font-size: 15px;
|
||||
line-height: 22px;
|
||||
font-weight: 600;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.sports-event-compact-item__action {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.sports-event-page .sports-event-compact-list .sports-event-compact-button.van-button {
|
||||
min-width: 0;
|
||||
height: 28px !important;
|
||||
padding: 0 10px !important;
|
||||
border: 0 !important;
|
||||
border-radius: 14px;
|
||||
font-size: 12px;
|
||||
line-height: 28px;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.sports-event-compact-button i {
|
||||
margin-right: 4px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.sports-event-page .sports-event-compact-list .sports-event-compact-button--apply.van-button {
|
||||
color: #397de0 !important;
|
||||
background: #edf5ff !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.sports-event-page .sports-event-compact-list .sports-event-compact-button--cancel.van-button {
|
||||
color: #df5b59 !important;
|
||||
background: #fff1f0 !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.sports-event-compact-button .van-button__content {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.sports-event-page .van-list__finished-text,
|
||||
.sports-event-page .van-list__loading {
|
||||
color: #9aa7b8;
|
||||
font-size: 13px;
|
||||
line-height: 44px;
|
||||
}
|
||||
|
||||
.sports-event-refresh {
|
||||
min-height: calc(100vh - 226px);
|
||||
}
|
||||
|
||||
.sports-event-empty {
|
||||
display: flex;
|
||||
min-height: 300px;
|
||||
padding: 28px 18px;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.sports-event-empty img {
|
||||
display: block;
|
||||
width: 82%;
|
||||
max-width: 300px;
|
||||
height: auto;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.sports-event-empty__title {
|
||||
margin-top: -8px;
|
||||
color: #50627a;
|
||||
font-size: 15px;
|
||||
line-height: 22px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.sports-event-empty__hint {
|
||||
margin-top: 6px;
|
||||
color: #9aa7b8;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="app" v-cloak>
|
||||
<div class="sports-event-page">
|
||||
<van-sticky>
|
||||
<van-nav-bar
|
||||
@click-left="historyBack"
|
||||
@@ -13,99 +585,188 @@ layout("/layouts/platform_h5.html"){
|
||||
></van-nav-bar>
|
||||
</van-sticky>
|
||||
|
||||
<div class="sports-event-summary">
|
||||
<div class="sports-event-summary__icon">
|
||||
<van-icon name="award-o"></van-icon>
|
||||
</div>
|
||||
<div class="sports-event-summary__body">
|
||||
<div class="sports-event-summary__title">体育活动项目</div>
|
||||
<div class="sports-event-summary__subtitle">选择适合的项目,参与健康运动</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<van-sticky :offset-top="46">
|
||||
<div class="sports-event-sticky">
|
||||
<van-search
|
||||
v-model="pageForm.searchKeyword"
|
||||
class="sports-event-search"
|
||||
placeholder="搜索项目名称"
|
||||
shape="round"
|
||||
clearable
|
||||
@search="doSearch"
|
||||
@clear="doSearch"
|
||||
></van-search>
|
||||
|
||||
<div class="sports-event-list-head">
|
||||
<button type="button" class="sports-event-view-toggle" @click="toggleViewMode">
|
||||
<i :class="viewMode === 'card' ? 'fa fa-list' : 'fa fa-th-large'"></i>
|
||||
<span>{{ viewMode === 'card' ? '列表' : '卡片' }}</span>
|
||||
</button>
|
||||
<div class="sports-event-list-head__count" v-if="pageForm.totalCount">共 {{ pageForm.totalCount }} 项</div>
|
||||
</div>
|
||||
</div>
|
||||
</van-sticky>
|
||||
|
||||
<van-pull-refresh class="sports-event-refresh" v-model="refreshing" @refresh="onRefresh">
|
||||
<div>
|
||||
<van-list :finished="finished" @load="pageData" finished-text="没有更多了" v-if="tableData.length>0"
|
||||
v-model="loading">
|
||||
<div class="table-list">
|
||||
<div class="sports-event-card-list" v-if="viewMode === 'card'">
|
||||
<div :key="row.id" class="table-card" v-for="row in tableData">
|
||||
<van-image :src="row.image" fit="cover" height="200" v-if="row.image" width="100%"></van-image>
|
||||
<van-cell>
|
||||
<template slot="title">
|
||||
<div class="flex-align">
|
||||
<span class="font-size3 bold">{{row.allName}}</span>
|
||||
<div class="sports-event-card__body">
|
||||
<div class="sports-event-card__title-row">
|
||||
<div class="sports-event-card__title">{{row.allName}}</div>
|
||||
<div class="sports-event-card__status">
|
||||
<template v-if="row.status&&row.applyWay.length===1&&row.applyWay.includes(1)">
|
||||
<van-tag class="sports-event-status-tag is-pending" v-if="row.status===1">待审核</van-tag>
|
||||
<van-tag class="sports-event-status-tag is-success" v-if="row.status===2">已成功报名</van-tag>
|
||||
<van-tag class="sports-event-status-tag is-rejected" v-if="row.status===3">审核不通过</van-tag>
|
||||
</template>
|
||||
<template v-else>
|
||||
<van-tag class="sports-event-status-tag is-pending" v-if="row.status2===0">待审核</van-tag>
|
||||
<van-tag class="sports-event-status-tag is-success" v-if="row.status2===2">已成功报名</van-tag>
|
||||
<van-tag class="sports-event-status-tag is-rejected" v-if="row.status2===3">审核不通过</van-tag>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<template #right-icon>
|
||||
<div style="display: flex; justify-content: flex-end"
|
||||
v-if="row.status&&row.applyWay.length===1&&row.applyWay.includes(1)">
|
||||
<van-tag type="primary" v-if="row.status===1">待审核</van-tag>
|
||||
<van-tag type="success" v-if="row.status===2">已成功报名</van-tag>
|
||||
<van-tag type="danger" v-if="row.status===3">审核不通过</van-tag>
|
||||
</div>
|
||||
<div class="sports-event-card__activity" v-if="row.name">
|
||||
<van-icon name="flag-o"></van-icon>
|
||||
<span>{{row.name}}</span>
|
||||
</div>
|
||||
<div class="sports-event-card__tags">
|
||||
<span class="sports-event-card__tag" v-if="row.eveProjectType"><span class="sports-event-card__tag-label">项目</span>{{row.eveProjectType == 2 ? '团体' : '单项'}}</span>
|
||||
<span class="sports-event-card__tag"><span class="sports-event-card__tag-label">性别</span>{{row.isMenWomen == 1 ? '男子' : row.isMenWomen == 2 ? '女子' : '不限'}}</span>
|
||||
<span class="sports-event-card__tag" v-if="row.isInterest">趣味项目</span>
|
||||
<span class="sports-event-card__tag" v-if="row.registrationWays"><span class="sports-event-card__tag-label">报名</span>{{row.registrationWays == 2 ? '双人' : '单人'}}</span>
|
||||
<span class="sports-event-card__tag" v-if="row.applyType"><span class="sports-event-card__tag-label">活动</span>{{row.applyType == 2 ? '综合类' : '单一类'}}</span>
|
||||
<span class="sports-event-card__tag" v-if="row.applyWay && row.applyWay.includes(1)">个人报名</span>
|
||||
<span class="sports-event-card__tag" v-if="row.applyWay && row.applyWay.includes(2)">分工会报名</span>
|
||||
<span class="sports-event-card__tag" v-if="row.applyWay && row.applyWay.includes(3)">小程序报名</span>
|
||||
</div>
|
||||
<div class="sports-event-card__stats">
|
||||
<div class="sports-event-card__stat">
|
||||
<div class="sports-event-card__stat-label">可报队数</div>
|
||||
<div class="sports-event-card__stat-value">{{row.eveProjectType == 2 && row.restrictTotalTeam != null ? row.restrictTotalTeam + '队' : '不适用'}}</div>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: flex-end" v-else>
|
||||
<van-tag type="primary" v-if="row.status2===0">待审核</van-tag>
|
||||
<van-tag type="success" v-if="row.status2===2">已成功报名</van-tag>
|
||||
<van-tag type="danger" v-if="row.status2===3">审核不通过</van-tag>
|
||||
<div class="sports-event-card__stat">
|
||||
<div class="sports-event-card__stat-label">运动员人数</div>
|
||||
<div class="sports-event-card__stat-value" v-if="row.athletesMinNum != null && row.athletesMaxNum != null">{{row.athletesMinNum}}-{{row.athletesMaxNum}}人</div>
|
||||
<div class="sports-event-card__stat-value" v-else-if="row.athletesMaxNum != null">最多{{row.athletesMaxNum}}人</div>
|
||||
<div class="sports-event-card__stat-value" v-else>暂无限制</div>
|
||||
</div>
|
||||
</template>
|
||||
</van-cell>
|
||||
<van-cell title="可报队数" v-if="row.eveProjectType==='2'">
|
||||
<template #right-icon>{{ row.restrictTotalTeam }}队</template>
|
||||
</van-cell>
|
||||
<van-cell title="每队可报人数" v-if="row.athletesMaxNum<=99">
|
||||
<template #right-icon>
|
||||
<span v-if="row.athletesMaxNum">{{ row.athletesMaxNum }}人</span>
|
||||
<span v-else-if="row.applyType===1">{{ row.athletesMaxNum * row.restrictTotalTeam}}人</span>
|
||||
<span v-else-if="row.applyType===2&&row.eveProjectType==='1'">{{row.athletesMaxNum}}人</span>
|
||||
<span v-else-if="row.applyType===2&&row.eveProjectType==='2'">
|
||||
{{(row.restrictGirlNum ? row.restrictGirlNum : 0) + (row.restrictBoyNum ? row.restrictBoyNum : 0)}}人
|
||||
</span>
|
||||
<span v-else>暂无</span>
|
||||
</template>
|
||||
</van-cell>
|
||||
<!-- <van-cell title="已报名总人数">-->
|
||||
<!-- <template #right-icon>-->
|
||||
<!-- <span v-if="row.successUserApply>0&&row.applyType!==3">{{ row.successUserApply }}人</span>-->
|
||||
<!-- <span v-else-if="JSON.parse(row.applyWay).length===2&&row.applyWay.includes(1)">{{row.totalApplyNum}}人</span>-->
|
||||
<!-- <span v-else-if="row.totalApplyNum>0&&row.applyType===3">{{ row.totalApplyNum }}人</span>-->
|
||||
<!-- <span v-else>0人</span>-->
|
||||
<!-- </template>-->
|
||||
<!-- </van-cell>-->
|
||||
<van-cell title="暂保存人数">
|
||||
<template #right-icon>
|
||||
<span v-if="row.apply_num_bc>0">已保存({{row.apply_num_bc}})人</span>
|
||||
<span v-else>暂无已保存人数</span>
|
||||
</template>
|
||||
</van-cell>
|
||||
<van-cell title="已报名人员" v-if="JSON.parse(row.applyWay).length===1">
|
||||
<template #right-icon>
|
||||
<span @click="openApplyUser(row)" v-if="row.userApplyNames">{{ row.userApplyNames}}</span>
|
||||
<span v-else>暂无</span>
|
||||
</template>
|
||||
</van-cell>
|
||||
<van-cell class="table-card-footer">
|
||||
<div class="sports-event-card__stat">
|
||||
<div class="sports-event-card__stat-label">每人限报</div>
|
||||
<div class="sports-event-card__stat-value">{{row.restrictRegNumber != null ? row.restrictRegNumber + '项' : '暂无限制'}}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sports-event-card__config" v-if="row.restrictBoyNum != null || row.restrictGirlNum != null || row.coachNum != null || row.leanderNum != null || row.substituteNum != null || row.isLeader || row.isManGirlNum">
|
||||
<span class="sports-event-card__config-tag" v-if="row.restrictBoyNum != null">男子 {{row.restrictBoyNum}}人</span>
|
||||
<span class="sports-event-card__config-tag" v-if="row.restrictGirlNum != null">女子 {{row.restrictGirlNum}}人</span>
|
||||
<span class="sports-event-card__config-tag" v-if="row.coachNum != null">教练 {{row.coachNum}}人</span>
|
||||
<span class="sports-event-card__config-tag" v-if="row.leanderNum != null">领队 {{row.leanderNum}}人</span>
|
||||
<span class="sports-event-card__config-tag" v-if="row.substituteNum != null">替补 {{row.substituteNum}}人</span>
|
||||
<span class="sports-event-card__config-tag" v-if="row.isLeader">仅报领队</span>
|
||||
<span class="sports-event-card__config-tag" v-if="row.isManGirlNum">男女不限</span>
|
||||
</div>
|
||||
<div class="sports-event-card__extra">
|
||||
<div class="sports-event-card__extra-row" v-if="row.startAgeDate || row.endAgeDate"><van-icon name="calendar-o"></van-icon><span class="sports-event-card__extra-label">出生日期</span><span class="sports-event-card__extra-value">{{row.startAgeDate || '不限'}} 至 {{row.endAgeDate || '不限'}}</span></div>
|
||||
<div class="sports-event-card__extra-row" v-if="row.activityTeamName"><van-icon name="friends-o"></van-icon><span class="sports-event-card__extra-label">当前小队</span><span class="sports-event-card__extra-value">{{row.activityTeamName}}</span></div>
|
||||
<div class="sports-event-card__extra-row" v-if="row.userApplyNames"><van-icon name="contact-o"></van-icon><span class="sports-event-card__extra-label">已报名人员</span><span class="sports-event-card__extra-value" @click="openApplyUser(row)">{{row.userApplyNames}}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-card-footer sports-event-card__footer">
|
||||
<div v-if="row.applyWay.length===1&&row.applyWay.includes(1)">
|
||||
<van-button :loading="subLoading" @click="signUpUser(row)" round
|
||||
<van-button class="sports-event-apply-button" :loading="subLoading" @click="signUpUser(row)" round
|
||||
size="small"
|
||||
type="primary" v-if="activityData.applyWay.includes(1)&&row.userApply===0">
|
||||
报 名
|
||||
<i class="fa fa-sign-in"></i>
|
||||
我要报名
|
||||
</van-button>
|
||||
<van-button :loading="subLoading" @click="cancelApply(row)" round
|
||||
<van-button class="sports-event-cancel-button" :loading="subLoading" @click="cancelApply(row)" round
|
||||
size="small"
|
||||
color="#dd6363"
|
||||
type="primary" v-if="activityData.applyWay.includes(1)&&row.userApply>0">
|
||||
<i class="fa fa-trash"></i>
|
||||
取消报名
|
||||
</van-button>
|
||||
</div>
|
||||
<div v-else>
|
||||
<van-button :loading="subLoading" @click="signUpUser(row)" round
|
||||
<van-button class="sports-event-apply-button" :loading="subLoading" @click="signUpUser(row)" round
|
||||
size="small"
|
||||
type="primary" v-if="activityData.applyWay.includes(1)&&row.userApply2===0">
|
||||
报 名
|
||||
<i class="fa fa-sign-in"></i>
|
||||
我要报名
|
||||
</van-button>
|
||||
<van-button :loading="subLoading" @click="cancelApply(row)" round
|
||||
<van-button class="sports-event-cancel-button" :loading="subLoading" @click="cancelApply(row)" round
|
||||
size="small"
|
||||
color="#dd6363"
|
||||
type="primary" v-if="activityData.applyWay.includes(1)&&row.userApply2>0">
|
||||
<i class="fa fa-trash"></i>
|
||||
取消报名
|
||||
</van-button>
|
||||
</div>
|
||||
</van-cell>
|
||||
<span
|
||||
v-if="row.applyWay.includes(2) && !row.applyWay.includes(1)"
|
||||
class="sports-event-union-action-hint"
|
||||
:class="{'is-authorized': canUnionApplyOnPc}">
|
||||
{{ canUnionApplyOnPc ? '分工会报名请在PC端进行' : '该活动为分工会报名,暂无权限' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sports-event-compact-list" v-else>
|
||||
<div :key="row.id" class="sports-event-compact-item" v-for="row in tableData">
|
||||
<div class="sports-event-compact-item__name">{{ row.allName }}</div>
|
||||
<div class="sports-event-compact-item__action">
|
||||
<div v-if="row.applyWay.length===1&&row.applyWay.includes(1)">
|
||||
<van-button class="sports-event-compact-button sports-event-compact-button--apply" :loading="subLoading" @click="signUpUser(row)" round
|
||||
size="small" type="default"
|
||||
v-if="activityData.applyWay.includes(1)&&row.userApply===0">
|
||||
<i class="fa fa-sign-in"></i>我要报名
|
||||
</van-button>
|
||||
<van-button class="sports-event-compact-button sports-event-compact-button--cancel" :loading="subLoading" @click="cancelApply(row)" round
|
||||
size="small" type="default"
|
||||
v-if="activityData.applyWay.includes(1)&&row.userApply>0">
|
||||
<i class="fa fa-trash"></i>取消报名
|
||||
</van-button>
|
||||
</div>
|
||||
<div v-else>
|
||||
<van-button class="sports-event-compact-button sports-event-compact-button--apply" :loading="subLoading" @click="signUpUser(row)" round
|
||||
size="small" type="default"
|
||||
v-if="activityData.applyWay.includes(1)&&row.userApply2===0">
|
||||
<i class="fa fa-sign-in"></i>我要报名
|
||||
</van-button>
|
||||
<van-button class="sports-event-compact-button sports-event-compact-button--cancel" :loading="subLoading" @click="cancelApply(row)" round
|
||||
size="small" type="default"
|
||||
v-if="activityData.applyWay.includes(1)&&row.userApply2>0">
|
||||
<i class="fa fa-trash"></i>取消报名
|
||||
</van-button>
|
||||
</div>
|
||||
<span
|
||||
v-if="row.applyWay.includes(2) && !row.applyWay.includes(1)"
|
||||
class="sports-event-union-action-hint"
|
||||
:class="{'is-authorized': canUnionApplyOnPc}">
|
||||
{{ canUnionApplyOnPc ? '分工会报名请在PC端进行' : '该活动为分工会报名,暂无权限' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</van-list>
|
||||
<van-empty image="/assets/platform/plugins/vant-green/images/nodata/nodata.png" description="暂无数据"
|
||||
v-else></van-empty>
|
||||
<div class="sports-event-empty" v-else>
|
||||
<img src="/assets/mobile/img/activity/sports-empty.png" alt="体育活动项目空状态插画">
|
||||
<div class="sports-event-empty__title">暂无可报名项目</div>
|
||||
<div class="sports-event-empty__hint">当前活动暂未发布可报名项目</div>
|
||||
</div>
|
||||
</div>
|
||||
</van-pull-refresh>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -118,18 +779,34 @@ layout("/layouts/platform_h5.html"){
|
||||
tableData: [],
|
||||
finished: false,
|
||||
loading: false,
|
||||
refreshing: false,
|
||||
pageForm: {
|
||||
activityId: GetQueryString("activityId"),
|
||||
searchKeyword: "",
|
||||
pageNumber: 1,
|
||||
pageSize: 5,
|
||||
totalCount: 0
|
||||
},
|
||||
subLoading: false,
|
||||
activityData: {}
|
||||
activityData: {},
|
||||
viewMode: "card"
|
||||
}
|
||||
},
|
||||
components: {},
|
||||
computed: {
|
||||
canUnionApplyOnPc() {
|
||||
return this.$auth.hasRoleOr([
|
||||
"SYSADMIN",
|
||||
"SCHOOL_UNION_ADMIN",
|
||||
"BRANCH_UNION_CHAIRMAN",
|
||||
"BRANCH_UNION_WENTI_SPORTS"
|
||||
]) && this.$auth.hasPermission("activity.apply")
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toggleViewMode() {
|
||||
this.viewMode = this.viewMode === "card" ? "list" : "card"
|
||||
},
|
||||
openApplyUser() {
|
||||
},
|
||||
cancelApply(row) {
|
||||
@@ -165,45 +842,51 @@ layout("/layouts/platform_h5.html"){
|
||||
this.$dialog.confirm({
|
||||
title: "提示",
|
||||
message: "确认要报名吗?"
|
||||
}).then(async () => {
|
||||
const teamList = await this.listTeamList(row)
|
||||
}).then(() => {
|
||||
return this.listTeamList(row).then(teamList => {
|
||||
if (teamList.length > 0) {
|
||||
row.teamId = teamList[0].id
|
||||
row.teamId = teamList[0].id;
|
||||
}
|
||||
this.$axios.post("/platform/activity/apply/h5/signUpUser", {
|
||||
activityId: row.activityId,
|
||||
teamId: row.teamId,
|
||||
eventId: row.eventId,
|
||||
schoolEventId: row.id
|
||||
activityId: row.activityId,
|
||||
teamId: row.teamId,
|
||||
eventId: row.eventId,
|
||||
schoolEventId: row.id
|
||||
}).then(resp => {
|
||||
if (resp.code === 0) {
|
||||
this.doSearch()
|
||||
this.$dialog.alert({
|
||||
title: '温馨提示',
|
||||
message: resp.msg,
|
||||
}).then(() => {
|
||||
// on close
|
||||
});
|
||||
}
|
||||
})
|
||||
if (resp.code === 0) {
|
||||
this.doSearch();
|
||||
this.$dialog.alert({
|
||||
title: '温馨提示',
|
||||
message: resp.msg
|
||||
}).then(() => {
|
||||
// on close
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
})
|
||||
},
|
||||
async listTeamList(row) {
|
||||
const resp = await this.$axios.post("/platform/activity/apply/h5/listTeamList", {
|
||||
activityId: row.activityId,
|
||||
eventId: row.eventId,
|
||||
})
|
||||
listTeamList(row) {
|
||||
return this.$axios.post("/platform/activity/apply/h5/listTeamList", {
|
||||
activityId: row.activityId,
|
||||
eventId: row.eventId
|
||||
}).then(resp => {
|
||||
if (resp.code === 0) {
|
||||
return resp.data
|
||||
return resp.data;
|
||||
}
|
||||
});
|
||||
},
|
||||
doSearch() {
|
||||
this.tableKey = new Date().getTime()
|
||||
this.pageForm.pageNumber = 1
|
||||
this.pageForm.totalCount = 0
|
||||
this.finished = false
|
||||
this.tableData = []
|
||||
this.pageData()
|
||||
},
|
||||
onRefresh() {
|
||||
this.doSearch()
|
||||
},
|
||||
pageData() {
|
||||
this.loading = true
|
||||
this.$axios.post("/platform/activity/apply/h5/pageData", {data: JSON.stringify(this.pageForm)}).then((res) => {
|
||||
@@ -214,7 +897,9 @@ layout("/layouts/platform_h5.html"){
|
||||
this.pageForm.pageNumber++
|
||||
}
|
||||
this.pageForm.totalCount = res.data.totalCount
|
||||
}).finally(() => {
|
||||
this.loading = false
|
||||
this.refreshing = false
|
||||
})
|
||||
},
|
||||
|
||||
|
||||