模块移植:场地预约、理论学习()

This commit is contained in:
2026-05-22 15:01:28 +08:00
parent 9bd1e865b9
commit a9b6c782ed
127 changed files with 30528 additions and 38 deletions
@@ -0,0 +1,122 @@
-- 普惠疗休养平台电脑端菜单。
-- 使用 permission 做幂等判断,避免同一环境重复执行时插入重复菜单。
INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService)
SELECT
'b7c63f0c73a44a9a8c4f3d3bb1a10001',
'',
LPAD(IFNULL(MAX(CAST(path AS UNSIGNED)), 0) + 1, 4, '0'),
'普惠疗休养',
'Tour',
'menu',
'',
'',
'ti-map-alt',
1,
0,
'tour',
NULL,
991,
1,
'',
UNIX_TIMESTAMP(NOW()) * 1000,
'',
UNIX_TIMESTAMP(NOW()) * 1000,
0,
'PC',
NULL,
NULL,
'p',
0,
0
FROM sys_menu
WHERE (parentId = '' OR parentId IS NULL)
AND CHAR_LENGTH(path) = 4
HAVING NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'tour') t);
INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService)
SELECT 'b7c63f0c73a44a9a8c4f3d3bb1a10002', p.id, CONCAT(p.path, '0001'), '疗休养设置', 'Tour Setting', 'menu', '/platform/tour/setting', 'data-pjax', '', 1, 0, 'tour.setting', NULL, 1, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', NULL, NULL, 'l', 0, 0
FROM sys_menu p
WHERE p.permission = 'tour'
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'tour.setting') t);
INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService)
SELECT 'b7c63f0c73a44a9a8c4f3d3bb1a10003', p.id, CONCAT(p.path, '0002'), '旅行社管理', 'Travel Agency', 'menu', '/platform/tour/travelAgency', 'data-pjax', '', 1, 0, 'tour.travelAgency', NULL, 2, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', NULL, NULL, 'l', 0, 0
FROM sys_menu p
WHERE p.permission = 'tour'
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'tour.travelAgency') t);
INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService)
SELECT 'b7c63f0c73a44a9a8c4f3d3bb1a10004', p.id, CONCAT(p.path, '0003'), '线路管理', 'Route Manage', 'menu', '/platform/tour/route', 'data-pjax', '', 1, 0, 'tour.route', NULL, 3, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', NULL, NULL, 'x', 0, 0
FROM sys_menu p
WHERE p.permission = 'tour'
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'tour.route') t);
INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService)
SELECT 'b7c63f0c73a44a9a8c4f3d3bb1a10005', p.id, CONCAT(p.path, '0004'), '疗休养事项', 'Tour Matter', 'menu', '/platform/tour/matter', 'data-pjax', '', 1, 0, 'tour.matter', NULL, 4, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', NULL, NULL, 'l', 0, 0
FROM sys_menu p
WHERE p.permission = 'tour'
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'tour.matter') t);
INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService)
SELECT 'b7c63f0c73a44a9a8c4f3d3bb1a10006', p.id, CONCAT(p.path, '0005'), '疗休养报名', 'Tour Signup', 'menu', '/platform/tour/signup', 'data-pjax', '', 1, 0, 'tour.signup', NULL, 5, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', NULL, NULL, 'l', 0, 0
FROM sys_menu p
WHERE p.permission = 'tour'
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'tour.signup') t);
INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService)
SELECT 'b7c63f0c73a44a9a8c4f3d3bb1a10008', p.id, CONCAT(p.path, '0006'), '我的报名', 'My Signup', 'menu', '/platform/tour/mysignup', 'data-pjax', '', 1, 0, 'tour.mysignup', NULL, 6, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', NULL, NULL, 'w', 0, 0
FROM sys_menu p
WHERE p.permission = 'tour'
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'tour.mysignup') t);
INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService)
SELECT 'b7c63f0c73a44a9a8c4f3d3bb1a10009', p.id, CONCAT(p.path, '0007'), '分工会查询', 'Union Ledger', 'menu', '/platform/tour/unionledger', 'data-pjax', '', 1, 0, 'tour.unionledger', NULL, 7, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', NULL, NULL, 'f', 0, 0
FROM sys_menu p
WHERE p.permission = 'tour'
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'tour.unionledger') t);
INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService)
SELECT 'b7c63f0c73a44a9a8c4f3d3bb1a10011', p.id, CONCAT(p.path, '0010'), '分工会审核', 'Union Approval', 'menu', '/platform/tour/unionApproval', 'data-pjax', '', 1, 0, 'tour.unionApproval', NULL, 8, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', NULL, NULL, 'f', 0, 0
FROM sys_menu p
WHERE p.permission = 'tour'
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'tour.unionApproval') t);
INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService)
SELECT 'b7c63f0c73a44a9a8c4f3d3bb1a10012', p.id, CONCAT(p.path, '0011'), '校工会审核', 'School Union Approval', 'menu', '/platform/tour/schoolUnionApproval', 'data-pjax', '', 1, 0, 'tour.schoolUnionApproval', NULL, 9, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', NULL, NULL, 'x', 0, 0
FROM sys_menu p
WHERE p.permission = 'tour'
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'tour.schoolUnionApproval') t);
INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService)
SELECT 'b7c63f0c73a44a9a8c4f3d3bb1a10007', p.id, CONCAT(p.path, '0008'), '疗休养台账', 'Tour Ledger', 'menu', '/platform/tour/ledger', 'data-pjax', '', 1, 0, 'tour.ledger', NULL, 10, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', NULL, NULL, 'l', 0, 0
FROM sys_menu p
WHERE p.permission = 'tour'
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'tour.ledger') t);
INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService)
SELECT 'b7c63f0c73a44a9a8c4f3d3bb1a10010', p.id, CONCAT(p.path, '0009'), '线路成团', 'Tour Group', 'menu', '/platform/tour/group', 'data-pjax', '', 1, 0, 'tour.group', NULL, 11, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', NULL, NULL, 'x', 0, 0
FROM sys_menu p
WHERE p.permission = 'tour'
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'tour.group') t);
-- 给系统管理员默认授权,其他角色可在角色管理中按需分配。
INSERT INTO sys_role_menu (roleId, menuId)
SELECT r.id, m.id
FROM sys_role r
JOIN sys_menu m ON m.permission IN (
'tour',
'tour.setting',
'tour.travelAgency',
'tour.route',
'tour.matter',
'tour.signup',
'tour.group',
'tour.mysignup',
'tour.unionledger',
'tour.unionApproval',
'tour.schoolUnionApproval',
'tour.ledger'
)
LEFT JOIN sys_role_menu rm ON rm.roleId = r.id AND rm.menuId = m.id
WHERE r.code = 'SYSADMIN'
AND rm.roleId IS NULL;
@@ -0,0 +1,3 @@
ALTER TABLE `site_cug_info`
ADD COLUMN `segmented_open_hours` JSON NULL COMMENT '分段预约场次信息' AFTER `open_hours`,
ADD COLUMN `full_day_open_hour` JSON NULL COMMENT '全天候预约时间段' AFTER `segmented_open_hours`;
@@ -0,0 +1,3 @@
ALTER TABLE `site_cug_info`
ADD COLUMN `reserve_time_type` INT NULL DEFAULT 1 COMMENT '预约时间段类型(1分段预约,2全天候预约)' AFTER `not_apply_time_list`,
ADD COLUMN `open_hours` JSON NULL COMMENT '场次信息' AFTER `reserve_time_type`;
@@ -0,0 +1,2 @@
ALTER TABLE `site_cug_info`
ADD COLUMN `site_photo` VARCHAR(255) NULL COMMENT '场地照片' AFTER `introduce`;
+253
View File
@@ -0,0 +1,253 @@
-- 普惠疗休养系统字典初始化。
-- 执行后可在 /platform/sys/dict 页面看到:疗休养(Tour) -> 组织形式/疗休养类型/线路类型/床型。
INSERT INTO `sys_dict` (
`id`, `parentId`, `path`, `name`, `remark`, `code`, `disabled`, `location`, `hasChildren`,
`createdBy`, `createdAt`, `updatedBy`, `updatedAt`, `delFlag`
)
SELECT
'b7c63f0c73a44a9a8c4f3d3bdict001', '', '9500', '疗休养', '普惠疗休养平台字典', 'Tour', 0, 9500, 1,
'', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0
WHERE NOT EXISTS (SELECT 1 FROM (SELECT id FROM `sys_dict` WHERE `code` = 'Tour') t);
INSERT INTO `sys_dict` (
`id`, `parentId`, `path`, `name`, `remark`, `code`, `disabled`, `location`, `hasChildren`,
`createdBy`, `createdAt`, `updatedBy`, `updatedAt`, `delFlag`
)
SELECT
'b7c63f0c73a44a9a8c4f3d3bdict002', p.id, CONCAT(p.path, '0001'), '组织形式', '疗休养组织形式', 'organizationType', 0, 1, 1,
'', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0
FROM `sys_dict` p
WHERE p.`code` = 'Tour'
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM `sys_dict` WHERE `code` = 'organizationType' AND `parentId` = p.id) t);
INSERT INTO `sys_dict` (
`id`, `parentId`, `path`, `name`, `remark`, `code`, `disabled`, `location`, `hasChildren`,
`createdBy`, `createdAt`, `updatedBy`, `updatedAt`, `delFlag`
)
SELECT
'b7c63f0c73a44a9a8c4f3d3bdict003', p.id, CONCAT(p.path, '0001'), '校工会组织', '疗休养组织形式', 'schoolUnion', 0, 1, 0,
'', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0
FROM `sys_dict` p
WHERE p.`code` = 'organizationType'
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM `sys_dict` WHERE `code` = 'schoolUnion' AND `parentId` = p.id) t);
INSERT INTO `sys_dict` (
`id`, `parentId`, `path`, `name`, `remark`, `code`, `disabled`, `location`, `hasChildren`,
`createdBy`, `createdAt`, `updatedBy`, `updatedAt`, `delFlag`
)
SELECT
'b7c63f0c73a44a9a8c4f3d3bdict004', p.id, CONCAT(p.path, '0002'), '分工会组织', '疗休养组织形式', 'branchUnion', 0, 2, 0,
'', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0
FROM `sys_dict` p
WHERE p.`code` = 'organizationType'
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM `sys_dict` WHERE `code` = 'branchUnion' AND `parentId` = p.id) t);
INSERT INTO `sys_dict` (
`id`, `parentId`, `path`, `name`, `remark`, `code`, `disabled`, `location`, `hasChildren`,
`createdBy`, `createdAt`, `updatedBy`, `updatedAt`, `delFlag`
)
SELECT
'b7c63f0c73a44a9a8c4f3d3bdict005', p.id, CONCAT(p.path, '0003'), '个人组织', '疗休养组织形式', 'personal', 0, 3, 0,
'', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0
FROM `sys_dict` p
WHERE p.`code` = 'organizationType'
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM `sys_dict` WHERE `code` = 'personal' AND `parentId` = p.id) t);
INSERT INTO `sys_dict` (
`id`, `parentId`, `path`, `name`, `remark`, `code`, `disabled`, `location`, `hasChildren`,
`createdBy`, `createdAt`, `updatedBy`, `updatedAt`, `delFlag`
)
SELECT
'b7c63f0c73a44a9a8c4f3d3bdict006', p.id, CONCAT(p.path, '0002'), '疗休养类型', '疗休养类型', 'tourType', 0, 2, 1,
'', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0
FROM `sys_dict` p
WHERE p.`code` = 'Tour'
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM `sys_dict` WHERE `code` = 'tourType' AND `parentId` = p.id) t);
INSERT INTO `sys_dict` (
`id`, `parentId`, `path`, `name`, `remark`, `code`, `disabled`, `location`, `hasChildren`,
`createdBy`, `createdAt`, `updatedBy`, `updatedAt`, `delFlag`
)
SELECT
'b7c63f0c73a44a9a8c4f3d3bdict007', p.id, CONCAT(p.path, '0001'), '普惠性疗休养', '疗休养类型', 'inclusiveTour', 0, 1, 0,
'', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0
FROM `sys_dict` p
WHERE p.`code` = 'tourType'
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM `sys_dict` WHERE `code` = 'inclusiveTour' AND `parentId` = p.id) t);
INSERT INTO `sys_dict` (
`id`, `parentId`, `path`, `name`, `remark`, `code`, `disabled`, `location`, `hasChildren`,
`createdBy`, `createdAt`, `updatedBy`, `updatedAt`, `delFlag`
)
SELECT
'b7c63f0c73a44a9a8c4f3d3bdict008', p.id, CONCAT(p.path, '0002'), '优秀职工疗休养', '疗休养类型', 'excellentWorkerTour', 0, 2, 0,
'', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0
FROM `sys_dict` p
WHERE p.`code` = 'tourType'
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM `sys_dict` WHERE `code` = 'excellentWorkerTour' AND `parentId` = p.id) t);
INSERT INTO `sys_dict` (
`id`, `parentId`, `path`, `name`, `remark`, `code`, `disabled`, `location`, `hasChildren`,
`createdBy`, `createdAt`, `updatedBy`, `updatedAt`, `delFlag`
)
SELECT
'b7c63f0c73a44a9a8c4f3d3bdict009', p.id, CONCAT(p.path, '0003'), '线路类型', '线路类型', 'lineType', 0, 3, 1,
'', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0
FROM `sys_dict` p
WHERE p.`code` = 'Tour'
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM `sys_dict` WHERE `code` = 'lineType' AND `parentId` = p.id) t);
INSERT INTO `sys_dict` (
`id`, `parentId`, `path`, `name`, `remark`, `code`, `disabled`, `location`, `hasChildren`,
`createdBy`, `createdAt`, `updatedBy`, `updatedAt`, `delFlag`
)
SELECT
'b7c63f0c73a44a9a8c4f3d3bdict010', p.id, CONCAT(p.path, '0001'), '省内线路', '线路类型', 'inProvinceLine', 0, 1, 0,
'', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0
FROM `sys_dict` p
WHERE p.`code` = 'lineType'
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM `sys_dict` WHERE `code` = 'inProvinceLine' AND `parentId` = p.id) t);
INSERT INTO `sys_dict` (
`id`, `parentId`, `path`, `name`, `remark`, `code`, `disabled`, `location`, `hasChildren`,
`createdBy`, `createdAt`, `updatedBy`, `updatedAt`, `delFlag`
)
SELECT
'b7c63f0c73a44a9a8c4f3d3bdict011', p.id, CONCAT(p.path, '0002'), '省外线路', '线路类型', 'outProvinceLine', 0, 2, 0,
'', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0
FROM `sys_dict` p
WHERE p.`code` = 'lineType'
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM `sys_dict` WHERE `code` = 'outProvinceLine' AND `parentId` = p.id) t);
INSERT INTO `sys_dict` (
`id`, `parentId`, `path`, `name`, `remark`, `code`, `disabled`, `location`, `hasChildren`,
`createdBy`, `createdAt`, `updatedBy`, `updatedAt`, `delFlag`
)
SELECT
'b7c63f0c73a44a9a8c4f3d3bdict020', p.id, CONCAT(p.path, '0004'), '床型', '床型', 'bedType', 0, 4, 1,
'', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0
FROM `sys_dict` p
WHERE p.`code` = 'Tour'
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM `sys_dict` WHERE `code` = 'bedType' AND `parentId` = p.id) t);
INSERT INTO `sys_dict` (
`id`, `parentId`, `path`, `name`, `remark`, `code`, `disabled`, `location`, `hasChildren`,
`createdBy`, `createdAt`, `updatedBy`, `updatedAt`, `delFlag`
)
SELECT
'b7c63f0c73a44a9a8c4f3d3bdict021', p.id, CONCAT(p.path, '0001'), '双人床', '床型', 'doubleBed', 0, 1, 0,
'', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0
FROM `sys_dict` p
WHERE p.`code` = 'bedType'
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM `sys_dict` WHERE `code` = 'doubleBed' AND `parentId` = p.id) t);
INSERT INTO `sys_dict` (
`id`, `parentId`, `path`, `name`, `remark`, `code`, `disabled`, `location`, `hasChildren`,
`createdBy`, `createdAt`, `updatedBy`, `updatedAt`, `delFlag`
)
SELECT
'b7c63f0c73a44a9a8c4f3d3bdict022', p.id, CONCAT(p.path, '0002'), '单人床', '床型', 'singleBed', 0, 2, 0,
'', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0
FROM `sys_dict` p
WHERE p.`code` = 'bedType'
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM `sys_dict` WHERE `code` = 'singleBed' AND `parentId` = p.id) t);
INSERT INTO `sys_dict` (
`id`, `parentId`, `path`, `name`, `remark`, `code`, `disabled`, `location`, `hasChildren`,
`createdBy`, `createdAt`, `updatedBy`, `updatedAt`, `delFlag`
)
SELECT
'b7c63f0c73a44a9a8c4f3d3bdict023', p.id, CONCAT(p.path, '0003'), '大床', '床型', 'kingBed', 0, 3, 0,
'', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0
FROM `sys_dict` p
WHERE p.`code` = 'bedType'
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM `sys_dict` WHERE `code` = 'kingBed' AND `parentId` = p.id) t);
INSERT INTO `sys_dict` (
`id`, `parentId`, `path`, `name`, `remark`, `code`, `disabled`, `location`, `hasChildren`,
`createdBy`, `createdAt`, `updatedBy`, `updatedAt`, `delFlag`
)
SELECT
'b7c63f0c73a44a9a8c4f3d3bdict012', '', '9600', '亲属关系', '亲属关系', 'familyRelationship', 0, 9600, 1,
'', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0
WHERE NOT EXISTS (SELECT 1 FROM (SELECT id FROM `sys_dict` WHERE `code` = 'familyRelationship') t);
INSERT INTO `sys_dict` (
`id`, `parentId`, `path`, `name`, `remark`, `code`, `disabled`, `location`, `hasChildren`,
`createdBy`, `createdAt`, `updatedBy`, `updatedAt`, `delFlag`
)
SELECT
'b7c63f0c73a44a9a8c4f3d3bdict013', p.id, CONCAT(p.path, '0001'), '直系亲属', '亲属关系', 'directRelative', 0, 1, 1,
'', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0
FROM `sys_dict` p
WHERE p.`code` = 'familyRelationship'
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM `sys_dict` WHERE `code` = 'directRelative' AND `parentId` = p.id) t);
INSERT INTO `sys_dict` (
`id`, `parentId`, `path`, `name`, `remark`, `code`, `disabled`, `location`, `hasChildren`,
`createdBy`, `createdAt`, `updatedBy`, `updatedAt`, `delFlag`
)
SELECT
'b7c63f0c73a44a9a8c4f3d3bdict014', p.id, CONCAT(p.path, '0001'), '父亲', '直系亲属', 'father', 0, 1, 0,
'', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0
FROM `sys_dict` p
WHERE p.`code` = 'directRelative'
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM `sys_dict` WHERE `code` = 'father' AND `parentId` = p.id) t);
INSERT INTO `sys_dict` (
`id`, `parentId`, `path`, `name`, `remark`, `code`, `disabled`, `location`, `hasChildren`,
`createdBy`, `createdAt`, `updatedBy`, `updatedAt`, `delFlag`
)
SELECT
'b7c63f0c73a44a9a8c4f3d3bdict015', p.id, CONCAT(p.path, '0002'), '母亲', '直系亲属', 'mother', 0, 2, 0,
'', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0
FROM `sys_dict` p
WHERE p.`code` = 'directRelative'
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM `sys_dict` WHERE `code` = 'mother' AND `parentId` = p.id) t);
INSERT INTO `sys_dict` (
`id`, `parentId`, `path`, `name`, `remark`, `code`, `disabled`, `location`, `hasChildren`,
`createdBy`, `createdAt`, `updatedBy`, `updatedAt`, `delFlag`
)
SELECT
'b7c63f0c73a44a9a8c4f3d3bdict016', p.id, CONCAT(p.path, '0003'), '丈夫', '直系亲属', 'husband', 0, 3, 0,
'', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0
FROM `sys_dict` p
WHERE p.`code` = 'directRelative'
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM `sys_dict` WHERE `code` = 'husband' AND `parentId` = p.id) t);
INSERT INTO `sys_dict` (
`id`, `parentId`, `path`, `name`, `remark`, `code`, `disabled`, `location`, `hasChildren`,
`createdBy`, `createdAt`, `updatedBy`, `updatedAt`, `delFlag`
)
SELECT
'b7c63f0c73a44a9a8c4f3d3bdict017', p.id, CONCAT(p.path, '0004'), '妻子', '直系亲属', 'wife', 0, 4, 0,
'', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0
FROM `sys_dict` p
WHERE p.`code` = 'directRelative'
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM `sys_dict` WHERE `code` = 'wife' AND `parentId` = p.id) t);
INSERT INTO `sys_dict` (
`id`, `parentId`, `path`, `name`, `remark`, `code`, `disabled`, `location`, `hasChildren`,
`createdBy`, `createdAt`, `updatedBy`, `updatedAt`, `delFlag`
)
SELECT
'b7c63f0c73a44a9a8c4f3d3bdict018', p.id, CONCAT(p.path, '0005'), '儿子', '直系亲属', 'son', 0, 5, 0,
'', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0
FROM `sys_dict` p
WHERE p.`code` = 'directRelative'
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM `sys_dict` WHERE `code` = 'son' AND `parentId` = p.id) t);
INSERT INTO `sys_dict` (
`id`, `parentId`, `path`, `name`, `remark`, `code`, `disabled`, `location`, `hasChildren`,
`createdBy`, `createdAt`, `updatedBy`, `updatedAt`, `delFlag`
)
SELECT
'b7c63f0c73a44a9a8c4f3d3bdict019', p.id, CONCAT(p.path, '0006'), '女儿', '直系亲属', 'daughter', 0, 6, 0,
'', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0
FROM `sys_dict` p
WHERE p.`code` = 'directRelative'
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM `sys_dict` WHERE `code` = 'daughter' AND `parentId` = p.id) t);
UPDATE `sys_dict` SET `hasChildren` = 1 WHERE `code` IN ('Tour', 'organizationType', 'tourType', 'lineType', 'bedType', 'familyRelationship', 'directRelative');
@@ -0,0 +1,7 @@
-- Add matter reference so signup records are unique per travel period/matter.
ALTER TABLE `tour_ledger`
ADD COLUMN `matterId` varchar(32) DEFAULT NULL COMMENT '报名事项ID' AFTER `signupTime`;
ALTER TABLE `tour_ledger`
ADD KEY `idx_tour_ledger_matter` (`matterId`),
ADD KEY `idx_tour_ledger_matter_job` (`matterId`, `jobNo`);
@@ -0,0 +1,2 @@
ALTER TABLE `tour_ledger`
ADD COLUMN `overCostReimbursed` tinyint(1) DEFAULT 0 COMMENT '报销超出费用' AFTER `reimbursed`;
@@ -0,0 +1,9 @@
-- 疗休养台账查询加速索引。
ALTER TABLE `tour_ledger`
ADD KEY `idx_tour_ledger_year_signup` (`year`, `delFlag`, `signupTime`),
ADD KEY `idx_tour_ledger_year_line` (`year`, `lineId`, `delFlag`),
ADD KEY `idx_tour_ledger_year_union_type` (`year`, `unionId`, `lineType`, `delFlag`),
ADD KEY `idx_tour_ledger_year_job` (`year`, `jobNo`, `delFlag`);
ALTER TABLE `wf_process_instance`
ADD KEY `idx_wf_process_instance_business_state` (`businessNo`, `state`);
@@ -0,0 +1,21 @@
-- 直系亲属线路报名信息表,独立于普通携带亲属信息表。
CREATE TABLE IF NOT EXISTS `tour_ledger_direct_relative` (
`id` varchar(32) NOT NULL COMMENT 'ID',
`ledgerId` varchar(32) DEFAULT NULL COMMENT '台账ID',
`relativeName` varchar(100) DEFAULT NULL COMMENT '亲属姓名',
`unitName` varchar(100) DEFAULT NULL COMMENT '所在单位',
`relationshipCode` varchar(50) DEFAULT NULL COMMENT '亲属关系编码',
`relationshipName` varchar(50) DEFAULT NULL COMMENT '亲属关系',
`lineId` varchar(32) DEFAULT NULL COMMENT '线路ID',
`lineName` varchar(100) DEFAULT NULL COMMENT '线路名称',
`travelStartTime` varchar(20) DEFAULT NULL COMMENT '出行开始日期',
`travelEndTime` varchar(20) DEFAULT NULL COMMENT '出行结束日期',
`createdBy` varchar(32) DEFAULT NULL COMMENT '创建人',
`createdAt` bigint DEFAULT NULL COMMENT '创建时间',
`updatedBy` varchar(32) DEFAULT NULL COMMENT '修改人',
`updatedAt` bigint DEFAULT NULL COMMENT '修改时间',
`delFlag` tinyint(1) DEFAULT 0 COMMENT '删除标记',
PRIMARY KEY (`id`),
KEY `idx_tour_ledger_direct_relative_ledger` (`ledgerId`),
KEY `idx_tour_ledger_direct_relative_line` (`lineId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='普惠疗休养直系亲属线路报名信息';
@@ -0,0 +1,10 @@
-- 线路管理增加创建人和所在单位业务字段。
ALTER TABLE `tour_line`
ADD COLUMN `creatorUserId` varchar(32) DEFAULT NULL COMMENT '创建人ID' AFTER `travelAgencyId`,
ADD COLUMN `creatorName` varchar(100) DEFAULT NULL COMMENT '创建人' AFTER `creatorUserId`,
ADD COLUMN `unitId` varchar(32) DEFAULT NULL COMMENT '所在单位ID' AFTER `creatorName`,
ADD COLUMN `unitName` varchar(100) DEFAULT NULL COMMENT '所在单位' AFTER `unitId`;
ALTER TABLE `tour_line`
ADD KEY `idx_tour_line_creator` (`creatorUserId`),
ADD KEY `idx_tour_line_unit` (`unitId`);
@@ -0,0 +1,3 @@
-- 线路管理增加是否直系亲属线路字段,默认否。
ALTER TABLE `tour_line`
ADD COLUMN `directFamilyUnitLine` tinyint(1) DEFAULT 0 COMMENT '是否直系亲属线路' AFTER `openFlag`;
@@ -0,0 +1,3 @@
-- 线路管理增加是否对外开放字段,默认是。
ALTER TABLE `tour_line`
ADD COLUMN `openFlag` tinyint(1) DEFAULT 1 COMMENT '是否对外开放' AFTER `mobileThumb`;
@@ -0,0 +1,7 @@
-- 疗休养事项增加创建人字段。
ALTER TABLE `tour_matter`
ADD COLUMN `creatorUserId` varchar(32) DEFAULT NULL COMMENT '创建人ID' AFTER `settingId`,
ADD COLUMN `creatorName` varchar(100) DEFAULT NULL COMMENT '创建人' AFTER `creatorUserId`;
ALTER TABLE `tour_matter`
ADD KEY `idx_tour_matter_creator` (`creatorUserId`);
@@ -0,0 +1,26 @@
-- 疗休养事项批次表。
CREATE TABLE IF NOT EXISTS `tour_matter_batch` (
`id` varchar(32) NOT NULL COMMENT 'ID',
`matterId` varchar(32) DEFAULT NULL COMMENT '事项ID',
`lineId` varchar(32) DEFAULT NULL COMMENT '线路ID',
`batchName` varchar(100) DEFAULT NULL COMMENT '批次名称',
`signupStartTime` varchar(20) DEFAULT NULL COMMENT '报名开始时间',
`signupEndTime` varchar(20) DEFAULT NULL COMMENT '报名结束时间',
`travelStartTime` varchar(20) DEFAULT NULL COMMENT '出行开始时间',
`travelEndTime` varchar(20) DEFAULT NULL COMMENT '出行结束时间',
`changeDeadline` varchar(20) DEFAULT NULL COMMENT '变更截止时间',
`contactName` varchar(30) DEFAULT NULL COMMENT '联系人',
`contactPhone` varchar(30) DEFAULT NULL COMMENT '联系方式',
`minGroupPeople` int DEFAULT NULL COMMENT '最少成团人数',
`maxGroupPeople` int DEFAULT NULL COMMENT '最多成团人数',
`estimatedCost` decimal(10,2) DEFAULT NULL COMMENT '预计费用',
`enabled` tinyint(1) DEFAULT 1 COMMENT '状态',
`createdBy` varchar(32) DEFAULT NULL COMMENT '创建人',
`createdAt` bigint DEFAULT NULL COMMENT '创建时间',
`updatedBy` varchar(32) DEFAULT NULL COMMENT '修改人',
`updatedAt` bigint DEFAULT NULL COMMENT '修改时间',
`delFlag` tinyint(1) DEFAULT 0 COMMENT '删除标记',
PRIMARY KEY (`id`),
KEY `idx_tour_matter_batch_matter` (`matterId`),
KEY `idx_tour_matter_batch_line` (`lineId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='普惠疗休养事项批次';
@@ -0,0 +1,25 @@
-- 疗休养事项表。
CREATE TABLE IF NOT EXISTS `tour_matter` (
`id` varchar(32) NOT NULL COMMENT 'ID',
`year` int DEFAULT NULL COMMENT '年度',
`matterName` varchar(100) DEFAULT NULL COMMENT '事项名称',
`settingId` varchar(32) DEFAULT NULL COMMENT '疗休养配置ID',
`creatorUserId` varchar(32) DEFAULT NULL COMMENT '创建人ID',
`creatorName` varchar(100) DEFAULT NULL COMMENT '创建人',
`unionId` varchar(32) DEFAULT NULL COMMENT '所属工会ID',
`organizationType` varchar(100) DEFAULT NULL COMMENT '组织形式',
`mobileThumb` varchar(500) DEFAULT NULL COMMENT '移动端缩略图',
`enabled` tinyint(1) DEFAULT 1 COMMENT '事项状态',
`createdBy` varchar(32) DEFAULT NULL COMMENT '创建人',
`createdAt` bigint DEFAULT NULL COMMENT '创建时间',
`updatedBy` varchar(32) DEFAULT NULL COMMENT '修改人',
`updatedAt` bigint DEFAULT NULL COMMENT '修改时间',
`delFlag` tinyint(1) DEFAULT 0 COMMENT '删除标记',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_tour_matter_year_name` (`year`, `matterName`),
KEY `idx_tour_matter_year` (`year`),
KEY `idx_tour_matter_creator` (`creatorUserId`),
KEY `idx_tour_matter_setting` (`settingId`),
KEY `idx_tour_matter_union` (`unionId`),
KEY `idx_tour_matter_org_type` (`organizationType`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='普惠疗休养事项';
@@ -0,0 +1,6 @@
-- Add activity user scope reference for tour settings.
ALTER TABLE `tour_setting`
ADD COLUMN `activityGroupId` varchar(32) DEFAULT NULL COMMENT '可参加人员范围ID' AFTER `tourType`;
ALTER TABLE `tour_setting`
ADD KEY `idx_tour_setting_activity_group` (`activityGroupId`);
@@ -0,0 +1,3 @@
-- 疗休养配置增加周期允许次数字段。
ALTER TABLE `tour_setting`
ADD COLUMN `cycleAllowedTimes` int DEFAULT NULL COMMENT '周期允许次数' AFTER `cycleTotalCost`;
@@ -0,0 +1,2 @@
ALTER TABLE `tour_setting`
ADD COLUMN `cycleTotalCost` int DEFAULT NULL COMMENT '周期内总费用' AFTER `cycleEndYear`;
@@ -0,0 +1,4 @@
-- Add optional cycle year range for tour settings.
ALTER TABLE `tour_setting`
ADD COLUMN `cycleStartYear` int DEFAULT NULL COMMENT '周期开始年度' AFTER `outProvinceRatio`,
ADD COLUMN `cycleEndYear` int DEFAULT NULL COMMENT '周期结束年度' AFTER `cycleStartYear`;
@@ -0,0 +1,3 @@
-- 疗休养配置增加是否填报床位信息字段,默认开启以兼容历史配置。
ALTER TABLE `tour_setting`
ADD COLUMN `fillBedInfo` tinyint(1) DEFAULT 1 COMMENT '是否填报床位信息' AFTER `allowFamily`;
@@ -0,0 +1,3 @@
ALTER TABLE `tour_setting`
ADD COLUMN `outProvinceRatioType` varchar(50) DEFAULT '当年报名人数' COMMENT '省外人数占比类型' AFTER `outProvinceRatio`,
ADD COLUMN `outProvinceFixedPeople` int DEFAULT 0 COMMENT '省外固定人数' AFTER `outProvinceRatioType`;
@@ -0,0 +1,183 @@
-- 普惠疗休养基础配置表。
-- 若生产环境未开启 Nutz 自动建表,请先执行本脚本再使用“疗休养设置”菜单。
CREATE TABLE IF NOT EXISTS `tour_setting` (
`id` varchar(32) NOT NULL COMMENT 'ID',
`year` int DEFAULT NULL COMMENT '年度',
`configName` varchar(100) DEFAULT NULL COMMENT '疗休养配置名称',
`tourType` varchar(50) DEFAULT NULL COMMENT '疗休养类型',
`activityGroupId` varchar(32) DEFAULT NULL COMMENT '可参加人员范围ID',
`sortNo` int DEFAULT NULL COMMENT '排序编号',
`minGroupPeople` int DEFAULT NULL COMMENT '最少成团人数',
`maxGroupPeople` int DEFAULT NULL COMMENT '最多成团人数',
`outProvinceYears` int DEFAULT NULL COMMENT '省外几年去一次',
`outProvinceRatio` decimal(10,2) DEFAULT NULL COMMENT '省外人数占比',
`outProvinceRatioType` varchar(50) DEFAULT '当年报名人数' COMMENT '省外人数占比类型',
`outProvinceFixedPeople` int DEFAULT 0 COMMENT '省外固定人数',
`cycleStartYear` int DEFAULT NULL COMMENT '周期开始年度',
`cycleEndYear` int DEFAULT NULL COMMENT '周期结束年度',
`cycleTotalCost` int DEFAULT NULL COMMENT '周期内总费用',
`cycleAllowedTimes` int DEFAULT NULL COMMENT '周期允许次数',
`allowFamily` tinyint(1) DEFAULT 0 COMMENT '是否允许携带家属',
`fillBedInfo` tinyint(1) DEFAULT 1 COMMENT '是否填报床位信息',
`enabled` tinyint(1) DEFAULT 1 COMMENT '是否启用',
`serviceNotice` text COMMENT '服务须知',
`createdBy` varchar(32) DEFAULT NULL COMMENT '创建人',
`createdAt` bigint DEFAULT NULL COMMENT '创建时间',
`updatedBy` varchar(32) DEFAULT NULL COMMENT '修改人',
`updatedAt` bigint DEFAULT NULL COMMENT '修改时间',
`delFlag` tinyint(1) DEFAULT 0 COMMENT '删除标记',
PRIMARY KEY (`id`),
KEY `idx_tour_setting_year` (`year`),
KEY `idx_tour_setting_activity_group` (`activityGroupId`),
KEY `idx_tour_setting_sort` (`sortNo`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='普惠疗休养配置';
-- 疗休养配置标段表。
-- 标段先挂在配置上维护,后续线路、旅行社、报名等模块可继续通过 lotId 做业务关联。
CREATE TABLE IF NOT EXISTS `tour_setting_lot` (
`id` varchar(32) NOT NULL COMMENT 'ID',
`settingId` varchar(32) DEFAULT NULL COMMENT '所属配置ID',
`lotName` varchar(50) DEFAULT NULL COMMENT '标段名称',
`lotValue` varchar(50) DEFAULT NULL COMMENT '标段值',
`activityCost` int DEFAULT NULL COMMENT '标段费用',
`allowOverReimbursement` tinyint(1) DEFAULT 0 COMMENT '允许超出报销',
`createdBy` varchar(32) DEFAULT NULL COMMENT '创建人',
`createdAt` bigint DEFAULT NULL COMMENT '创建时间',
`updatedBy` varchar(32) DEFAULT NULL COMMENT '修改人',
`updatedAt` bigint DEFAULT NULL COMMENT '修改时间',
`delFlag` tinyint(1) DEFAULT 0 COMMENT '删除标记',
PRIMARY KEY (`id`),
KEY `idx_tour_setting_lot_setting` (`settingId`),
KEY `idx_tour_setting_lot_value` (`lotValue`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='普惠疗休养配置标段';
-- 旅行社管理表。
CREATE TABLE IF NOT EXISTS `tour_travel_agency` (
`id` varchar(32) NOT NULL COMMENT 'ID',
`year` int DEFAULT NULL COMMENT '年度',
`agencyCode` varchar(50) DEFAULT NULL COMMENT '旅行社编号',
`agencyName` varchar(100) DEFAULT NULL COMMENT '旅行社名称',
`contactName` varchar(30) DEFAULT NULL COMMENT '联系人',
`contactPhone` varchar(30) DEFAULT NULL COMMENT '联系人手机',
`email` varchar(100) DEFAULT NULL COMMENT '邮箱',
`remark` text COMMENT '备注',
`mobileThumb` varchar(500) DEFAULT NULL COMMENT '移动端缩略图',
`enabled` tinyint(1) DEFAULT 1 COMMENT '激活状态',
`createdBy` varchar(32) DEFAULT NULL COMMENT '创建人',
`createdAt` bigint DEFAULT NULL COMMENT '创建时间',
`updatedBy` varchar(32) DEFAULT NULL COMMENT '修改人',
`updatedAt` bigint DEFAULT NULL COMMENT '修改时间',
`delFlag` tinyint(1) DEFAULT 0 COMMENT '删除标记',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_tour_travel_agency_year_code` (`year`, `agencyCode`),
KEY `idx_tour_travel_agency_name` (`agencyName`),
KEY `idx_tour_travel_agency_contact` (`contactName`, `contactPhone`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='普惠疗休养旅行社';
-- 线路管理表。
CREATE TABLE IF NOT EXISTS `tour_line` (
`id` varchar(32) NOT NULL COMMENT 'ID',
`year` int DEFAULT NULL COMMENT '创建年度',
`lineCode` varchar(50) DEFAULT NULL COMMENT '线路编号',
`lineName` varchar(100) DEFAULT NULL COMMENT '线路名称',
`travelAgencyId` varchar(32) DEFAULT NULL COMMENT '旅行社ID',
`creatorUserId` varchar(32) DEFAULT NULL COMMENT '创建人ID',
`creatorName` varchar(100) DEFAULT NULL COMMENT '创建人',
`unitId` varchar(32) DEFAULT NULL COMMENT '所在单位ID',
`unitName` varchar(100) DEFAULT NULL COMMENT '所在单位',
`lineType` varchar(50) DEFAULT NULL COMMENT '线路类型',
`lotId` varchar(32) DEFAULT NULL COMMENT '时间标段ID',
`lineContent` text COMMENT '线路内容',
`mobileThumb` varchar(500) DEFAULT NULL COMMENT '移动端缩略图',
`openFlag` tinyint(1) DEFAULT 1 COMMENT '是否对外开放',
`directFamilyUnitLine` tinyint(1) DEFAULT 0 COMMENT '是否直系亲属线路',
`enabled` tinyint(1) DEFAULT 1 COMMENT '激活状态',
`createdBy` varchar(32) DEFAULT NULL COMMENT '创建人',
`createdAt` bigint DEFAULT NULL COMMENT '创建时间',
`updatedBy` varchar(32) DEFAULT NULL COMMENT '修改人',
`updatedAt` bigint DEFAULT NULL COMMENT '修改时间',
`delFlag` tinyint(1) DEFAULT 0 COMMENT '删除标记',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_tour_line_year_code` (`year`, `lineCode`),
KEY `idx_tour_line_year` (`year`),
KEY `idx_tour_line_name` (`lineName`),
KEY `idx_tour_line_agency` (`travelAgencyId`),
KEY `idx_tour_line_creator` (`creatorUserId`),
KEY `idx_tour_line_unit` (`unitId`),
KEY `idx_tour_line_lot` (`lotId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='普惠疗休养线路';
-- 疗休养事项表。
CREATE TABLE IF NOT EXISTS `tour_matter` (
`id` varchar(32) NOT NULL COMMENT 'ID',
`year` int DEFAULT NULL COMMENT '年度',
`matterName` varchar(100) DEFAULT NULL COMMENT '事项名称',
`settingId` varchar(32) DEFAULT NULL COMMENT '疗休养配置ID',
`creatorUserId` varchar(32) DEFAULT NULL COMMENT '创建人ID',
`creatorName` varchar(100) DEFAULT NULL COMMENT '创建人',
`unionId` varchar(32) DEFAULT NULL COMMENT '所属工会ID',
`organizationType` varchar(100) DEFAULT NULL COMMENT '组织形式',
`mobileThumb` varchar(500) DEFAULT NULL COMMENT '移动端缩略图',
`enabled` tinyint(1) DEFAULT 1 COMMENT '事项状态',
`createdBy` varchar(32) DEFAULT NULL COMMENT '创建人',
`createdAt` bigint DEFAULT NULL COMMENT '创建时间',
`updatedBy` varchar(32) DEFAULT NULL COMMENT '修改人',
`updatedAt` bigint DEFAULT NULL COMMENT '修改时间',
`delFlag` tinyint(1) DEFAULT 0 COMMENT '删除标记',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_tour_matter_year_name` (`year`, `matterName`),
KEY `idx_tour_matter_year` (`year`),
KEY `idx_tour_matter_creator` (`creatorUserId`),
KEY `idx_tour_matter_setting` (`settingId`),
KEY `idx_tour_matter_union` (`unionId`),
KEY `idx_tour_matter_org_type` (`organizationType`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='普惠疗休养事项';
-- 疗休养事项批次表。
CREATE TABLE IF NOT EXISTS `tour_matter_batch` (
`id` varchar(32) NOT NULL COMMENT 'ID',
`matterId` varchar(32) DEFAULT NULL COMMENT '事项ID',
`lineId` varchar(32) DEFAULT NULL COMMENT '线路ID',
`batchName` varchar(100) DEFAULT NULL COMMENT '批次名称',
`signupStartTime` varchar(20) DEFAULT NULL COMMENT '报名开始时间',
`signupEndTime` varchar(20) DEFAULT NULL COMMENT '报名结束时间',
`travelStartTime` varchar(20) DEFAULT NULL COMMENT '出行开始时间',
`travelEndTime` varchar(20) DEFAULT NULL COMMENT '出行结束时间',
`changeDeadline` varchar(20) DEFAULT NULL COMMENT '变更截止时间',
`contactName` varchar(30) DEFAULT NULL COMMENT '联系人',
`contactPhone` varchar(30) DEFAULT NULL COMMENT '联系方式',
`minGroupPeople` int DEFAULT NULL COMMENT '最少成团人数',
`maxGroupPeople` int DEFAULT NULL COMMENT '最多成团人数',
`estimatedCost` decimal(10,2) DEFAULT NULL COMMENT '预计费用',
`enabled` tinyint(1) DEFAULT 1 COMMENT '状态',
`createdBy` varchar(32) DEFAULT NULL COMMENT '创建人',
`createdAt` bigint DEFAULT NULL COMMENT '创建时间',
`updatedBy` varchar(32) DEFAULT NULL COMMENT '修改人',
`updatedAt` bigint DEFAULT NULL COMMENT '修改时间',
`delFlag` tinyint(1) DEFAULT 0 COMMENT '删除标记',
PRIMARY KEY (`id`),
KEY `idx_tour_matter_batch_matter` (`matterId`),
KEY `idx_tour_matter_batch_line` (`lineId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='普惠疗休养事项批次';
-- 直系亲属线路报名信息表。
CREATE TABLE IF NOT EXISTS `tour_ledger_direct_relative` (
`id` varchar(32) NOT NULL COMMENT 'ID',
`ledgerId` varchar(32) DEFAULT NULL COMMENT '台账ID',
`relativeName` varchar(100) DEFAULT NULL COMMENT '亲属姓名',
`unitName` varchar(100) DEFAULT NULL COMMENT '所在单位',
`relationshipCode` varchar(50) DEFAULT NULL COMMENT '亲属关系编码',
`relationshipName` varchar(50) DEFAULT NULL COMMENT '亲属关系',
`lineId` varchar(32) DEFAULT NULL COMMENT '线路ID',
`lineName` varchar(100) DEFAULT NULL COMMENT '线路名称',
`travelStartTime` varchar(20) DEFAULT NULL COMMENT '出行开始日期',
`travelEndTime` varchar(20) DEFAULT NULL COMMENT '出行结束日期',
`createdBy` varchar(32) DEFAULT NULL COMMENT '创建人',
`createdAt` bigint DEFAULT NULL COMMENT '创建时间',
`updatedBy` varchar(32) DEFAULT NULL COMMENT '修改人',
`updatedAt` bigint DEFAULT NULL COMMENT '修改时间',
`delFlag` tinyint(1) DEFAULT 0 COMMENT '删除标记',
PRIMARY KEY (`id`),
KEY `idx_tour_ledger_direct_relative_ledger` (`ledgerId`),
KEY `idx_tour_ledger_direct_relative_line` (`lineId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='普惠疗休养直系亲属线路报名信息';
@@ -0,0 +1,2 @@
ALTER TABLE `tour_setting_lot`
ADD COLUMN `allowOverReimbursement` tinyint(1) DEFAULT 0 COMMENT '允许超出报销' AFTER `activityCost`;
+1 -1
View File
@@ -32,7 +32,7 @@ var ioc={
// 单个文件最大尺寸(大约的值,单位为字节,即 209715200 为 200M)
maxFileSize : 209715200,
// 正则表达式匹配可以支持的文件名
nameFilter : '^(.+[.])(gif||svg|jpg|png|jpeg|doc|docx|xls|xlsx|rar|zip|7z|txt|pdf|pptx|ppt|mp4)$'
nameFilter : '^(.+[.])(gif||svg|jpg|png|jpeg|doc|docx|xls|xlsx|rar|zip|7z|txt|pdf|pptx|ppt|flv|swf|mkv|avi|rm|rmvb|wmv|mp4|mov|mpg|mpeg|webm|m4v|3gp|3g2|ts|mts|m2ts|ogv)$'
}
},
fileUpload : {
Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

@@ -322,29 +322,51 @@ module.exports = {
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)
const valid = this.isAcceptFile(file)
if (valid) {
return true
}
this.$message.error(`文件只能是 ${this.fileAccept} 格式!`)
this.$message.error("文件只能是 " + this.fileAccept + " 格式!")
return false
},
// 兼容 accept="video/*" 这类 MIME 通配写法,同时保留原有扩展名白名单校验。
isAcceptFile(file) {
if (!this.fileAccept || typeof this.fileAccept !== "string") {
return true
}
const acceptTypes = this.getAcceptTypes()
const fileName = file.name || ""
const dotIndex = fileName.lastIndexOf(".")
const fileType = dotIndex !== -1 ? fileName.substring(dotIndex).toUpperCase() : ""
const mimeType = (file.type || "").toUpperCase()
if (!fileType) {
this.$message.error("文件必须包含有效的扩展名!")
return false
}
return acceptTypes.some((acceptType) => {
if (acceptType.endsWith("/*")) {
return mimeType.startsWith(acceptType.substring(0, acceptType.length - 1)) || this.matchVideoWildcard(acceptType, fileType)
}
return acceptType === fileType
})
},
// 部分浏览器或系统无法识别视频 MIME,此处用视频后缀兜底保证 video/* 可覆盖常见视频格式。
matchVideoWildcard(acceptType, fileType) {
const videoExtTypes = [".FLV", ".SWF", ".MKV", ".AVI", ".RM", ".RMVB", ".WMV", ".MP4", ".MOV", ".MPG", ".MPEG", ".WEBM", ".M4V", ".3GP", ".3G2", ".TS", ".MTS", ".M2TS", ".OGV"]
return acceptType === "VIDEO/*" && videoExtTypes.includes(fileType)
},
getAcceptTypes() {
return this.fileAccept
.trim()
.split(",")
.map((type) => type.trim().toUpperCase())
},
// 预览图片
handlePreview(file) {
const suffix = file.name.substring(file.name.lastIndexOf(".") + 1).toLowerCase()
@@ -571,24 +593,13 @@ module.exports = {
}
if (this.fileAccept) {
// 去除前后空格并转换为数组
const acceptTypes = this.fileAccept
.trim()
.split(",")
.map((type) => type.trim().toUpperCase())
const validFiles = data.files.filter((file) => {
let fileType
const dotIndex = file.name.lastIndexOf(".")
if (dotIndex !== -1) {
fileType = file.name.substring(dotIndex).toUpperCase()
}
return acceptTypes.includes(fileType)
return this.isAcceptFile(file)
})
if (validFiles.length < data.files.length) {
data.files = validFiles
this.$message.error(`文件只能是 ${this.fileAccept} 格式,已自动过滤掉不符合要求文件!`)
this.$message.error("文件只能是 " + this.fileAccept + " 格式,已自动过滤掉不符合要求文件!")
}
}
@@ -0,0 +1,353 @@
<!--#
layout("/layouts/platform.html"){
#-->
<div id="app" v-cloak>
<guava ref="guava">
<el-card shadow="never">
<search :is-search-button="false">
<search-item label="年度">
<el-date-picker
v-model="pageForm.year"
type="year"
value-format="yyyy"
placeholder="请选择年度"
style="width: 100%">
</el-date-picker>
</search-item>
<search-item label="线路名称">
<el-input
v-model="pageForm.lineName"
clearable
placeholder="请输入线路名称"
style="width: 100%"
@keyup.enter.native="doSearch">
</el-input>
</search-item>
<search-item label="线路类型">
<el-select v-model="pageForm.lineType" clearable filterable placeholder="请选择线路类型" style="width: 100%">
<el-option v-for="item in lineTypeOptions" :key="item.lineType" :label="item.lineType" :value="item.lineType"></el-option>
</el-select>
</search-item>
<search-item label="所属工会">
<el-select v-model="pageForm.unionId" clearable filterable placeholder="请选择所属工会" style="width: 100%">
<el-option v-for="item in unionOptions" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select>
</search-item>
<div class="search-query" style="margin-left: auto;">
<el-button size="medium" type="primary" icon="el-icon-search" @click="doSearch">搜索</el-button>
<el-button size="medium" @click="resetSearch">重置</el-button>
</div>
</search>
</el-card>
<el-card class="mt10" shadow="never">
<table-tool :app="this" label="线路列表"></table-tool>
<el-table
v-loading="tableLoading"
:data="tableData"
:size="tableSize"
border
:default-sort="{prop: 'lineName', order: 'ascending'}"
:row-class-name="tableRowClassName"
@sort-change="pageOrder">
<el-table-column label="序号" type="index" :index="indexMethod" width="80" align="center" header-align="center"></el-table-column>
<el-table-column label="线路名称" prop="lineName" min-width="220" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="出行时段" prop="travelPeriod" width="260" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="事项名称" prop="matterName" min-width="200" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="所属工会" prop="unionName" min-width="180" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="线路类型" prop="lineType" width="140" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="最少成团人数" prop="minGroupPeople" width="140" sortable="custom" align="center" header-align="center">
<template slot-scope="{row}">
{{ row.minGroupPeople || 0 }}人
</template>
</el-table-column>
<el-table-column label="最多成团人数" prop="maxGroupPeople" width="140" sortable="custom" align="center" header-align="center">
<template slot-scope="{row}">
{{ row.maxGroupPeople || 0 }}人
</template>
</el-table-column>
<el-table-column label="报名人数" prop="signupCount" width="120" sortable="custom" align="center" header-align="center">
<template slot-scope="{row}">
<el-tag size="mini" :type="Number(row.signupCount || 0) > 0 ? 'success' : 'info'">{{ row.signupCount || 0 }}人</el-tag>
</template>
</el-table-column>
<el-table-column label="成团状态" prop="groupStatusName" width="120" sortable="custom" align="center" header-align="center">
<template slot-scope="{row}">
<el-tag size="mini" :type="groupStatusType(row.groupStatus)">{{ row.groupStatusName || '未成团' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="160" align="center" header-align="center" fixed="right">
<template slot-scope="{row}">
<el-button size="mini" type="primary" @click="openView(row)">查看</el-button>
<el-button size="mini" type="primary" @click="exportLine(row)">导出</el-button>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
<el-drawer
:title="viewTitle"
:visible.sync="viewVisible"
direction="rtl"
size="72%"
custom-class="tour-group-view-drawer"
:close-on-click-modal="false">
<div class="tour-group-view">
<el-card shadow="never">
<search :is-search-button="false">
<search-item label="姓名/工号">
<el-input
v-model="viewForm.keyword"
clearable
placeholder="请输入姓名或工号"
style="width: 100%"
@keyup.enter.native="viewSearch">
</el-input>
</search-item>
<search-item label="所属工会">
<el-select v-model="viewForm.unionId" clearable filterable placeholder="请选择所属工会" style="width: 100%">
<el-option v-for="item in unionOptions" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select>
</search-item>
<div class="search-query" style="margin-left: auto;">
<el-button size="medium" type="primary" icon="el-icon-search" @click="viewSearch">搜索</el-button>
<el-button size="medium" @click="viewReset">重置</el-button>
</div>
</search>
</el-card>
<el-card class="mt10" shadow="never">
<div class="tour-group-view-title">报名人员列表</div>
<el-table
v-loading="viewLoading"
:data="viewTableData"
:size="tableSize"
border
@sort-change="viewPageOrder">
<el-table-column label="序号" type="index" :index="viewIndexMethod" width="80" align="center" header-align="center"></el-table-column>
<el-table-column label="工号" prop="jobNo" width="130" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="姓名" prop="userName" width="120" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="手机号码" prop="mobile" width="140" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="身份证号" prop="idCard" min-width="190" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="所属工会" prop="unionName" min-width="180" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
</el-table>
<el-row class="el-pagination-container tour-group-view-pagination">
<el-pagination
background
:current-page="viewForm.pageNumber"
:page-size="viewForm.pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="viewForm.totalCount"
layout="total, sizes, prev, pager, next, jumper"
@size-change="viewSizeChange"
@current-change="viewCurrentChange">
</el-pagination>
</el-row>
</el-card>
</div>
</el-drawer>
</guava>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
mixins: [initTableMixins],
data() {
const currentYear = moment().format("YYYY")
return {
unionOptions: [],
lineTypeOptions: [],
viewVisible: false,
viewLoading: false,
viewRequestSeq: 0,
viewTitle: "",
viewRow: {},
viewTableData: [],
pageForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
pageOrderName: "lineName",
pageOrderBy: "ascending",
year: currentYear,
lineName: "",
lineType: "",
unionId: ""
},
viewForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
pageOrderName: "signupTime",
pageOrderBy: "descending",
keyword: "",
unionId: ""
}
}
},
methods: {
resetSearch() {
this.pageForm.year = moment().format("YYYY")
this.pageForm.lineName = ""
this.pageForm.lineType = ""
this.pageForm.unionId = ""
this.loadLineTypeOptions()
this.doSearch()
},
groupStatusType(status) {
if (status === "formed") return "success"
if (status === "over") return "danger"
return "info"
},
tableRowClassName({ row }) {
return this.isDirectFamilyLine(row) ? "direct-family-row" : ""
},
isDirectFamilyLine(row) {
if (!row) return false
return row.directFamilyUnitLine === true
|| row.directFamilyUnitLine === 1
|| row.directFamilyUnitLine === "1"
},
openView(row) {
this.viewRow = row || {}
this.viewTitle = this.viewRow.lineName || "报名人员"
this.viewVisible = true
this.viewForm.pageNumber = 1
this.viewForm.pageSize = 10
this.viewForm.totalCount = 0
this.viewForm.pageOrderName = "signupTime"
this.viewForm.pageOrderBy = "descending"
this.viewForm.keyword = ""
this.viewForm.unionId = ""
this.viewTableData = []
this.loadViewData()
},
viewSearch() {
this.viewForm.pageNumber = 1
this.loadViewData()
},
viewReset() {
this.viewForm.keyword = ""
this.viewForm.unionId = ""
this.viewForm.pageNumber = 1
this.loadViewData()
},
viewIndexMethod(index) {
return (this.viewForm.pageNumber - 1) * this.viewForm.pageSize + index + 1
},
viewSizeChange(size) {
this.viewForm.pageSize = size
this.viewForm.pageNumber = 1
this.loadViewData()
},
viewCurrentChange(page) {
this.viewForm.pageNumber = page
this.loadViewData()
},
viewPageOrder({ prop, order }) {
this.viewForm.pageOrderName = prop || "signupTime"
this.viewForm.pageOrderBy = order || "descending"
this.viewForm.pageNumber = 1
this.loadViewData()
},
loadViewData() {
if (!this.viewRow || !this.viewRow.matterId) {
this.viewTableData = []
this.viewForm.totalCount = 0
return
}
this.viewLoading = true
const requestSeq = ++this.viewRequestSeq
this.$axios.post(loc() + "/signupPageData", {
matterId: this.viewRow.matterId,
keyword: this.viewForm.keyword,
unionId: this.viewForm.unionId,
pageNumber: this.viewForm.pageNumber,
pageSize: this.viewForm.pageSize,
pageOrderName: this.viewForm.pageOrderName,
pageOrderBy: this.viewForm.pageOrderBy
}).then((res) => {
if (requestSeq !== this.viewRequestSeq) return
if (res.code === 0) {
const data = res.data || {}
this.viewTableData = data.list || []
this.viewForm.totalCount = data.totalCount || 0
} else {
this.$message.warning(res.msg || "查询失败")
}
}).finally(() => {
if (requestSeq === this.viewRequestSeq) {
this.viewLoading = false
}
})
},
exportLine(row) {
if (!row || !row.matterId) {
this.$message.warning("线路信息不完整")
return
}
window.location.href = loc() + "/exportParticipants?matterId=" + encodeURIComponent(row.matterId)
},
loadUnionOptions() {
this.$axios.post(loc() + "/unionOptions").then((res) => {
if (res.code === 0) {
this.unionOptions = res.data || []
}
})
},
loadLineTypeOptions() {
this.$axios.post(loc() + "/lineTypeOptions", {
year: this.pageForm.year
}).then((res) => {
if (res.code === 0) {
this.lineTypeOptions = (res.data || []).filter(item => item.lineType)
}
})
}
},
mounted() {
this.loadUnionOptions()
this.loadLineTypeOptions()
this.pageData()
},
watch: {
"pageForm.year"() {
this.pageForm.lineType = ""
this.loadLineTypeOptions()
}
}
})
</script>
<style>
.tour-group-view-drawer .el-drawer__body {
background: #f5f7fa;
padding: 12px;
overflow: auto;
}
.tour-group-view-pagination {
margin-top: 12px;
text-align: right;
}
.tour-group-view-title {
border-left: 4px solid #0079c2;
color: #0079c2;
font-size: 14px;
font-weight: 600;
line-height: 16px;
margin-bottom: 12px;
padding-left: 10px;
}
.el-table .direct-family-row > td,
.el-table .direct-family-row > td .cell {
color: #f56c6c;
}
</style>
<!--#
}
#-->
@@ -0,0 +1,653 @@
<!--#
layout("/layouts/platform.html"){
#-->
<div id="app" v-cloak>
<guava ref="guava">
<el-card shadow="never">
<search :is-search-button="false">
<search-item label="开始年度">
<el-date-picker
v-model="pageForm.startYear"
type="year"
value-format="yyyy"
placeholder="请选择开始年度"
style="width: 100%">
</el-date-picker>
</search-item>
<search-item label="结束年度">
<el-date-picker
v-model="pageForm.endYear"
type="year"
value-format="yyyy"
placeholder="请选择结束年度"
style="width: 100%">
</el-date-picker>
</search-item>
<search-item label="姓名/工号">
<el-input
v-model="pageForm.keyword"
clearable
placeholder="请输入姓名或工号"
style="width: 100%"
@keyup.enter.native="doSearch">
</el-input>
</search-item>
<search-item label="所属工会">
<el-select v-model="pageForm.unionId" clearable filterable placeholder="请选择所属工会" style="width: 100%">
<el-option v-for="item in unionOptions" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select>
</search-item>
<search-item label="线路">
<el-select v-model="pageForm.lineId" clearable filterable placeholder="请选择线路" style="width: 100%">
<el-option v-for="item in lineOptions" :key="item.lineId" :label="item.lineName" :value="item.lineId"></el-option>
</el-select>
</search-item>
<search-item label="出行时段">
<el-select v-model="pageForm.travelPeriod" clearable filterable placeholder="请选择出行时段" style="width: 100%">
<el-option v-for="item in travelPeriodOptions" :key="item.travelPeriod" :label="item.travelPeriod" :value="item.travelPeriod"></el-option>
</el-select>
</search-item>
<search-item label="线路类型">
<dict-select
v-model="pageForm.lineType"
code="lineType"
option_value="name"
placeholder="请选择线路类型">
</dict-select>
</search-item>
<div class="search-query" style="margin-left: auto;">
<el-button size="medium" type="primary" icon="el-icon-search" @click="doSearch">搜索</el-button>
<el-button size="medium" @click="resetSearch">重置</el-button>
</div>
</search>
</el-card>
<el-card class="mt10" shadow="never">
<div class="tour-ledger-toolbar">
<div class="tour-ledger-toolbar-title">
<table-tool :app="this" label="台账列表"></table-tool>
</div>
<div class="tour-ledger-actions">
<el-button size="medium" type="primary" icon="el-icon-upload2" @click="showImportDialog = true">参加人员导入</el-button>
<el-button size="medium" type="primary" icon="el-icon-check" @click="setParticipants">设置参加人员</el-button>
<el-button size="medium" type="primary" icon="el-icon-download" @click="exportUnionSignupZip">导出分工会报名压缩包</el-button>
</div>
<div class="tour-ledger-scope">
<el-button
class="tour-scope-btn"
size="medium"
:class="{'is-active': pageForm.directFamilyOnly}"
@click="setDirectFamilyOnly">
参加直系亲属单位线路人员
</el-button>
<el-button
class="tour-scope-btn"
size="medium"
:class="{'is-active': pageForm.overCostOnly}"
@click="setOverCostOnly">
申请超出费用由单位承担人员
</el-button>
</div>
</div>
<el-table
ref="tableRef"
v-loading="tableLoading"
:data="tableData"
:size="tableSize"
border
row-key="id"
:row-class-name="tableRowClassName"
@selection-change="handleSelectionChange"
@sort-change="pageOrder">
<el-table-column type="selection" width="55" align="center" header-align="center"></el-table-column>
<el-table-column label="序号" type="index" :index="indexMethod" width="80" align="center" header-align="center"></el-table-column>
<el-table-column label="工号" prop="jobNo" width="130" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="姓名" prop="userName" width="120" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="所在单位" prop="unitName" min-width="180" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="报名线路" prop="lineName" min-width="220" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="出行时段" prop="travelPeriod" min-width="260" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="线路类型" prop="lineType" width="130" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="报名时间" prop="signupTime" width="170" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="携带家属" prop="familyCount" width="120" sortable="custom" align="center" header-align="center">
<template slot-scope="{row}">
<el-tag size="mini" :type="Number(row.familyCount || 0) > 0 ? 'success' : 'info'">{{ row.familyCount || 0 }}人</el-tag>
</template>
</el-table-column>
<el-table-column label="是否参加" prop="joined" width="120" sortable="custom" align="center" header-align="center">
<template slot-scope="{row}">
<el-tag size="mini" :type="row.joined ? 'success' : 'info'">{{ row.joined ? '是' : '否' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="报销超出费用" prop="overCostReimbursed" width="140" align="center" header-align="center">
<template slot-scope="{row}">
<el-tag size="mini" :type="row.overCostReimbursed ? 'success' : 'info'">{{ row.overCostReimbursed ? '是' : '否' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="160" align="center" header-align="center" fixed="right">
<template slot-scope="{row}">
<el-button size="mini" type="primary" @click="openView(row)">查看</el-button>
<el-button size="mini" type="danger" @click="doDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
<div class="tour-ledger-tip">温馨提醒:走审核流程的报名,审核通过后才在台账中显示</div>
</el-card>
</guava>
<excel-import
ref="excelImportRef"
url="/platform/tour/ledger/importParticipants"
template_url="/platform/tour/ledger/downloadTemplate"
:visible.sync="showImportDialog"
title="参加人员导入"
width="700px"
@import-success="afterImport"
:extra_params="{}">
</excel-import>
<el-dialog
title="台账详情"
:visible.sync="detailVisible"
:close-on-click-modal="false"
width="72%">
<div class="tour-ledger-section">
<div class="tour-ledger-title">教职工信息</div>
<el-descriptions :column="3" border size="medium">
<el-descriptions-item label="工号">{{ detail.jobNo || '' }}</el-descriptions-item>
<el-descriptions-item label="姓名">{{ detail.userName || '' }}</el-descriptions-item>
<el-descriptions-item label="性别">{{ detail.gender || '' }}</el-descriptions-item>
<el-descriptions-item label="身份证号">{{ detail.idCard || '' }}</el-descriptions-item>
<el-descriptions-item label="所在单位">{{ detail.unitName || '' }}</el-descriptions-item>
<el-descriptions-item label="所在工会">{{ detail.unionName || '' }}</el-descriptions-item>
</el-descriptions>
</div>
<div class="tour-ledger-section mt10">
<div class="tour-ledger-title">
报名信息
<el-link v-if="detailHasWorkflow()" type="primary" @click="openChart">点击查看流程图</el-link>
</div>
<el-descriptions :column="3" border size="medium">
<el-descriptions-item label="报名时间">{{ detail.signupTime || '' }}</el-descriptions-item>
<el-descriptions-item label="报名线路">{{ detail.lineName || '' }}</el-descriptions-item>
<el-descriptions-item label="线路类型">{{ detail.lineType || '' }}</el-descriptions-item>
<el-descriptions-item label="报名酒店">{{ detail.hotelName || '' }}</el-descriptions-item>
<el-descriptions-item label="报名旅行社">{{ detail.travelAgencyName || '' }}</el-descriptions-item>
<el-descriptions-item label="出行时间">{{ detail.travelPeriod || '' }}</el-descriptions-item>
<el-descriptions-item v-if="fillBedInfo" label="床型">{{ detail.bedType || '' }}</el-descriptions-item>
<el-descriptions-item v-if="fillBedInfo" label="床位信息">{{ detail.bedInfo || '' }}</el-descriptions-item>
<el-descriptions-item v-if="fillBedInfo" label="意向拼床人">{{ detail.intendedRoommate || '' }}</el-descriptions-item>
<el-descriptions-item label="是否携带家属">{{ familyText() }}</el-descriptions-item>
<el-descriptions-item label="是否参加">{{ detail.joined ? '是' : '否' }}</el-descriptions-item>
<el-descriptions-item label="是否报销">{{ detail.reimbursed ? '是' : '否' }}</el-descriptions-item>
<el-descriptions-item label="报销超出费用">{{ detail.overCostReimbursed ? '是' : '否' }}</el-descriptions-item>
</el-descriptions>
</div>
<div v-if="isDirectFamilyLine()" class="tour-ledger-section mt10">
<div class="tour-ledger-title">直系亲属线路</div>
<el-descriptions :column="3" border size="medium">
<el-descriptions-item label="亲属姓名">{{ directRelative.relativeName || '' }}</el-descriptions-item>
<el-descriptions-item label="所在单位">{{ directRelative.unitName || '' }}</el-descriptions-item>
<el-descriptions-item label="亲属关系">{{ directRelative.relationshipName || '' }}</el-descriptions-item>
<el-descriptions-item label="线路名称">{{ directRelative.lineName || '' }}</el-descriptions-item>
<el-descriptions-item label="出行开始日期">{{ directRelative.travelStartTime || '' }}</el-descriptions-item>
<el-descriptions-item label="出行结束日期">{{ directRelative.travelEndTime || '' }}</el-descriptions-item>
</el-descriptions>
</div>
<div v-if="showFamilySection()" class="tour-ledger-section mt10">
<div class="tour-ledger-title">家属信息</div>
<el-table :data="familyData" border :size="tableSize" empty-text="暂无家属信息">
<el-table-column label="序号" type="index" width="70" align="center" header-align="center"></el-table-column>
<el-table-column label="家属姓名" prop="familyName" min-width="120" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="性别" prop="gender" width="90" align="center" header-align="center"></el-table-column>
<el-table-column label="身份证号码" prop="idCard" min-width="180" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="年龄" prop="age" width="90" align="center" header-align="center"></el-table-column>
<el-table-column label="关系" prop="relationship" width="110" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column v-if="fillBedInfo" label="床型" prop="bedType" width="110" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column v-if="fillBedInfo" label="床位" prop="bedInfo" min-width="120" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column v-if="fillBedInfo" label="意向拼床人" prop="intendedRoommate" min-width="130" align="center" header-align="center" show-overflow-tooltip></el-table-column>
</el-table>
</div>
<template v-if="detailHasWorkflow()" v-for="task in doneTasks">
<div class="tour-ledger-section mt10" :key="task.id">
<div class="tour-ledger-title">{{ task.displayName }}</div>
<el-descriptions border :column="3" v-if="task.ext && task.ext.isFirstTaskNode">
<el-descriptions-item label="申请用户">
{{ task.ext.initiatorName }}({{ task.ext.initiatorAccount }})
</el-descriptions-item>
<el-descriptions-item label="申请时间">{{ task.finishTime }}</el-descriptions-item>
<el-descriptions-item label="办理结果">
<dict-tag :options="dict.type.PROCESS_TASK_SUBMIT_TYPE" :value="task.ext.submitType"></dict-tag>
</el-descriptions-item>
</el-descriptions>
<el-descriptions border :column="3" v-else>
<el-descriptions-item label="办理用户">
{{ (task.taskFormData && task.taskFormData.userName) || '' }}({{ (task.taskFormData && task.taskFormData.loginName) || '' }})
</el-descriptions-item>
<el-descriptions-item label="办理时间">{{ task.finishTime }}</el-descriptions-item>
<el-descriptions-item label="办理结果">
<dict-tag :options="dict.type.PROCESS_TASK_SUBMIT_TYPE" :value="task.ext && task.ext.submitType"></dict-tag>
</el-descriptions-item>
<el-descriptions-item label="办理意见" :span="3">
{{ (task.taskFormData && (task.taskFormData.opinion || task.taskFormData.tf_opinion)) || '' }}
</el-descriptions-item>
</el-descriptions>
</div>
</template>
<snaker-chart ref="snakerChartRef"></snaker-chart>
<span slot="footer" class="dialog-footer">
<el-button type="primary" @click="detailVisible = false">确定</el-button>
</span>
</el-dialog>
</div>
<style>
.tour-ledger-section {
padding: 0 2px;
}
.tour-ledger-title {
border-left: 4px solid #0079c2;
color: #0079c2;
font-size: 14px;
font-weight: 600;
line-height: 16px;
margin-bottom: 14px;
padding-left: 10px;
}
.tour-ledger-toolbar {
display: flex;
align-items: center;
flex-wrap: nowrap;
gap: 24px;
margin-bottom: 12px;
white-space: nowrap;
}
.tour-ledger-toolbar-title {
flex: 0 0 auto;
}
.tour-ledger-actions {
display: flex;
align-items: center;
flex: 0 0 auto;
gap: 10px;
white-space: nowrap;
}
.tour-ledger-actions .el-button {
margin: 0;
}
.tour-ledger-scope {
display: flex;
align-items: center;
flex: 0 0 auto;
flex-wrap: nowrap;
gap: 12px;
margin: 0;
white-space: nowrap;
}
.tour-ledger-scope .el-button {
margin: 0;
}
.tour-ledger-scope .el-button + .el-button {
margin-left: 0;
}
.tour-ledger-scope .tour-scope-btn {
background: #ecf5ff;
border-color: #b3d8ff;
border-radius: 4px;
color: #0079c2;
font-weight: 600;
height: 34px;
line-height: 1;
padding: 8px 18px;
}
.tour-ledger-scope .tour-scope-btn:hover,
.tour-ledger-scope .tour-scope-btn:focus {
background: #d9ecff;
border-color: #66b1ff;
color: #006bb0;
}
.tour-ledger-scope .tour-scope-btn.is-active {
background: #0079c2;
border-color: #0079c2;
box-shadow: 0 2px 6px rgba(0, 121, 194, 0.24);
color: #fff;
}
.tour-ledger-scope .tour-scope-btn.is-active:hover,
.tour-ledger-scope .tour-scope-btn.is-active:focus {
background: #006bb0;
border-color: #006bb0;
color: #fff;
}
.tour-ledger-tip {
color: #f56c6c;
font-family: SimSun, "宋体", serif;
font-size: 9pt;
line-height: 1.6;
margin-top: -4px;
text-align: center;
}
.el-table .direct-family-row > td,
.el-table .direct-family-row > td .cell {
color: #f56c6c;
}
</style>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
mixins: [initTableMixins],
dicts: ["PROCESS_TASK_SUBMIT_TYPE"],
data() {
const currentYear = moment().format("YYYY")
return {
detailVisible: false,
unionOptions: [],
lineOptions: [],
travelPeriodOptions: [],
detail: {},
familyData: [],
directRelative: {},
directFamilyUnitLine: false,
fillBedInfo: true,
detailRow: {},
doneTasks: [],
showImportDialog: false,
multipleSelection: [],
filterOptionsTimer: null,
pageForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
pageOrderName: "signupTime",
pageOrderBy: "descending",
startYear: currentYear,
endYear: currentYear,
keyword: "",
unionId: "",
lineId: "",
travelPeriod: "",
lineType: "",
directFamilyOnly: false,
overCostOnly: false
}
}
},
methods: {
toBoolean(value) {
return value === true || value === 1 || value === "1" || value === "true" || value === "TRUE"
},
tableRowClassName({ row }) {
return this.isDirectFamilyRow(row) ? "direct-family-row" : ""
},
isDirectFamilyRow(row) {
return this.toBoolean(row && row.directFamilyUnitLine) || !!(row && row.directRelativeId)
},
handleSelectionChange(val) {
this.multipleSelection = val || []
},
clearTableSelection() {
this.multipleSelection = []
if (this.$refs.tableRef) {
this.$refs.tableRef.clearSelection()
}
},
setParticipants() {
if (!this.multipleSelection.length) {
this.$message.warning("请选择要设置的参加人员")
return
}
this.$confirm("确定将选中的" + this.multipleSelection.length + "条台账设置为已参加吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
this.$axios.post(loc() + "/setParticipants", {
ids: JSON.stringify(this.multipleSelection.map(item => item.id))
}).then((res) => {
if (res.code === 0) {
this.$message.success("设置成功")
this.clearTableSelection()
this.pageData()
} else {
this.$message.warning(res.msg || "设置失败")
}
})
})
},
exportUnionSignupZip() {
const params = new URLSearchParams()
;["startYear", "endYear", "keyword", "unionId", "lineId", "travelPeriod", "lineType"].forEach(key => {
const value = this.pageForm[key]
if (value !== undefined && value !== null && value !== "") {
params.append(key, value)
}
})
if (this.pageForm.directFamilyOnly) {
params.append("directFamilyOnly", "true")
}
if (this.pageForm.overCostOnly) {
params.append("overCostOnly", "true")
}
window.location.href = loc() + "/exportUnionSignupZip?" + params.toString()
},
afterImport() {
this.showImportDialog = false
this.clearTableSelection()
this.pageData()
this.loadFilterOptions()
},
isDirectFamilyLine() {
return this.directFamilyUnitLine
|| this.toBoolean(this.detail && this.detail.directFamilyUnitLine)
|| !!(this.directRelative && this.directRelative.id)
},
detailHasWorkflow() {
return !!(this.detailRow && this.detailRow.instanceId)
},
hasFamily() {
return this.toBoolean(this.detail && this.detail.hasFamily) || this.familyData.length > 0
},
familyText() {
if (this.isDirectFamilyLine()) {
return "否"
}
if (!this.hasFamily()) {
return "否"
}
const count = Number(this.familyData.length || 0)
return count > 0 ? count + "人" : "是"
},
showFamilySection() {
return this.hasFamily() && !this.isDirectFamilyLine()
},
loadDoneTasks() {
this.$axios.post("/flow/common/doneTasks", { bizId: this.detailRow.id }).then((res) => {
if (res.code === 0) {
this.doneTasks = res.data || []
}
})
},
openChart() {
if (!this.detailRow.instanceProcessDefineId || !this.detailRow.instanceId) {
this.$message.warning("暂无流程图信息")
return
}
this.$refs.snakerChartRef.onOpenFull(this.detailRow.instanceProcessDefineId, this.detailRow.instanceId)
},
resetSearch() {
this.pageForm.startYear = moment().format("YYYY")
this.pageForm.endYear = moment().format("YYYY")
this.pageForm.keyword = ""
this.pageForm.unionId = ""
this.pageForm.lineId = ""
this.pageForm.travelPeriod = ""
this.pageForm.lineType = ""
this.pageForm.directFamilyOnly = false
this.pageForm.overCostOnly = false
this.doSearch()
},
openView(row) {
this.detailRow = row || {}
this.detail = {}
this.familyData = []
this.directRelative = {}
this.directFamilyUnitLine = false
this.fillBedInfo = true
this.doneTasks = []
this.$axios.post(loc() + "/detail", { id: row.id }).then((res) => {
if (res.code === 0) {
const data = res.data || {}
this.detail = data.ledger || {}
this.detail.travelPeriod = data.travelPeriod || ""
this.familyData = data.families || []
this.directRelative = data.directRelative || {}
this.directFamilyUnitLine = this.toBoolean(data.directFamilyUnitLine)
this.fillBedInfo = data.fillBedInfo === undefined || data.fillBedInfo === null || this.toBoolean(data.fillBedInfo)
this.detailVisible = true
if (this.detailHasWorkflow()) {
this.loadDoneTasks()
}
} else {
this.$message.warning(res.msg || "查询失败")
}
})
},
doDelete(row) {
this.$confirm("确定删除【" + row.userName + "】的报名台账吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
this.$axios.post(loc() + "/doDelete", { id: row.id }).then((res) => {
if (res.code === 0) {
this.$message.success("删除成功")
this.pageData()
this.loadFilterOptions()
} else {
this.$message.warning(res.msg || "删除失败")
}
})
})
},
loadUnionOptions() {
this.$axios.post(loc() + "/unionOptions").then((res) => {
if (res.code === 0) {
this.unionOptions = res.data || []
}
})
},
loadFilterOptions() {
this.loadLineOptions()
this.loadTravelPeriodOptions()
},
scheduleFilterOptions(delay) {
if (this.filterOptionsTimer) {
clearTimeout(this.filterOptionsTimer)
}
this.filterOptionsTimer = setTimeout(() => {
this.loadFilterOptions()
this.filterOptionsTimer = null
}, delay === undefined ? 80 : delay)
},
loadLineOptions() {
this.$axios.post(loc() + "/lineOptions", {
startYear: this.pageForm.startYear,
endYear: this.pageForm.endYear,
travelPeriod: this.pageForm.travelPeriod,
lineType: this.pageForm.lineType,
unionId: this.pageForm.unionId,
keyword: this.pageForm.keyword,
directFamilyOnly: this.pageForm.directFamilyOnly,
overCostOnly: this.pageForm.overCostOnly
}).then((res) => {
if (res.code === 0) {
this.lineOptions = res.data || []
if (this.pageForm.lineId && !this.lineOptions.some(item => item.lineId === this.pageForm.lineId)) {
this.pageForm.lineId = ""
}
}
})
},
loadTravelPeriodOptions() {
this.$axios.post(loc() + "/travelPeriodOptions", {
startYear: this.pageForm.startYear,
endYear: this.pageForm.endYear,
lineId: this.pageForm.lineId,
lineType: this.pageForm.lineType,
unionId: this.pageForm.unionId,
keyword: this.pageForm.keyword,
directFamilyOnly: this.pageForm.directFamilyOnly,
overCostOnly: this.pageForm.overCostOnly
}).then((res) => {
if (res.code === 0) {
this.travelPeriodOptions = res.data || []
if (this.pageForm.travelPeriod && !this.travelPeriodOptions.some(item => item.travelPeriod === this.pageForm.travelPeriod)) {
this.pageForm.travelPeriod = ""
}
}
})
},
doSearch() {
this.tableKey = new Date().getTime()
this.pageForm.pageNumber = 1
this.clearTableSelection()
this.pageData()
this.scheduleFilterOptions(0)
},
setDirectFamilyOnly() {
this.pageForm.directFamilyOnly = !this.pageForm.directFamilyOnly
this.pageForm.pageNumber = 1
this.clearTableSelection()
this.pageData()
this.scheduleFilterOptions(0)
},
setOverCostOnly() {
this.pageForm.overCostOnly = !this.pageForm.overCostOnly
this.pageForm.pageNumber = 1
this.clearTableSelection()
this.pageData()
this.scheduleFilterOptions(0)
}
},
mounted() {
this.loadUnionOptions()
this.pageData()
this.scheduleFilterOptions(150)
},
watch: {
"pageForm.startYear"() {
this.pageForm.lineId = ""
this.pageForm.travelPeriod = ""
this.scheduleFilterOptions()
},
"pageForm.endYear"() {
this.pageForm.lineId = ""
this.pageForm.travelPeriod = ""
this.scheduleFilterOptions()
},
"pageForm.lineId"() {
this.scheduleFilterOptions()
},
"pageForm.travelPeriod"() {
this.scheduleFilterOptions()
},
"pageForm.lineType"() {
this.scheduleFilterOptions()
},
"pageForm.unionId"() {
this.scheduleFilterOptions()
}
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,712 @@
<!--#
layout("/layouts/platform.html"){
#-->
<div id="app" v-cloak>
<guava ref="guava">
<el-card shadow="never">
<search :is-search-button="false">
<search-item label="年度">
<el-date-picker
v-model="pageForm.year"
type="year"
value-format="yyyy"
placeholder="请选择年度"
style="width: 100%">
</el-date-picker>
</search-item>
<search-item label="所属工会">
<el-select v-model="pageForm.unionId" clearable filterable placeholder="请选择所属工会" style="width: 100%">
<el-option v-for="item in unionOptions" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select>
</search-item>
<search-item label="事项名称">
<el-input
v-model="pageForm.matterName"
clearable
placeholder="请输入事项名称"
style="width: 100%"
@keyup.enter.native="doSearch">
</el-input>
</search-item>
<search-item label="组织形式">
<el-select v-model="pageForm.organizationType" clearable filterable placeholder="请选择组织形式" style="width: 100%">
<el-option v-for="item in organizationTypeOptions" :key="item.code" :label="item.name" :value="item.code"></el-option>
</el-select>
</search-item>
<div class="search-query" style="margin-left: auto;">
<el-button size="medium" type="primary" icon="el-icon-search" @click="doSearch">搜索</el-button>
<el-button size="medium" @click="resetSearch">重置</el-button>
</div>
</search>
</el-card>
<el-card class="mt10" shadow="never">
<table-tool :app="this" label="事项列表">
<el-button @click="openAdd" size="medium" type="primary">
<i class="el-icon-plus"></i>
新增
</el-button>
</table-tool>
<el-table
v-loading="tableLoading"
:data="tableData"
:size="tableSize"
border
:row-class-name="tableRowClassName"
@sort-change="pageOrder">
<el-table-column label="序号" type="index" :index="indexMethod" width="80" align="center" header-align="center"></el-table-column>
<el-table-column label="年度" prop="year" width="110" sortable="custom" align="center" header-align="center"></el-table-column>
<el-table-column label="事项名称" prop="matterName" min-width="200" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="所属工会" prop="unionName" min-width="180" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="线路名称" prop="lineName" min-width="220" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="组织形式" prop="organizationTypeName" width="150" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="联系人" prop="contactName" width="130" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="联系方式" prop="contactPhone" width="150" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="事项状态" prop="enabled" width="160" align="center" header-align="center">
<template slot-scope="{row}">
<el-switch
v-model="row.enabled"
active-text="启用"
inactive-text="禁用"
@change="toggleEnabled(row)">
</el-switch>
</template>
</el-table-column>
<el-table-column label="操作" width="260" align="center" header-align="center">
<template slot-scope="{row}">
<el-button size="mini" type="primary" @click="openBatchForm(row)">选择线路</el-button>
<el-button size="mini" type="primary" @click="openEdit(row)">编辑</el-button>
<el-button size="mini" type="danger" :loading="row.deleteLoading" @click="doDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
</guava>
<el-dialog
:title="title"
:visible.sync="dialogVisible"
:close-on-click-modal="false"
width="54%"
@closed="destroyForm">
<el-form :model="formData" :rules="formRules" label-width="110px" ref="form" :disabled="viewMode">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="年度" prop="year">
<el-date-picker
v-model="formData.year"
type="year"
value-format="yyyy"
placeholder="请选择年度"
style="width: 100%"
@change="yearChange">
</el-date-picker>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="事项状态" prop="enabled">
<el-radio-group v-model="formData.enabled">
<el-radio :label="true">启用</el-radio>
<el-radio :label="false">禁用</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="事项名称" prop="matterName">
<el-input v-model="formData.matterName" maxlength="30" show-word-limit placeholder="请输入事项名称"></el-input>
</el-form-item>
<el-form-item label="创建人">
<el-input v-model="formData.creatorName" readonly placeholder="当前登录人"></el-input>
</el-form-item>
<el-form-item label="疗休养配置" prop="settingId">
<el-select v-model="formData.settingId" clearable filterable placeholder="请选择配置" style="width: 100%">
<el-option v-for="item in settingOptions" :key="item.id" :label="item.configName" :value="item.id"></el-option>
</el-select>
</el-form-item>
<el-form-item label="组织形式" prop="organizationType">
<el-select v-model="formData.organizationType" clearable filterable placeholder="请选择组织形式" style="width: 100%" @change="organizationTypeChange">
<el-option v-for="item in organizationTypeOptions" :key="item.code" :label="item.name" :value="item.code"></el-option>
</el-select>
</el-form-item>
<el-form-item label="所属工会" prop="unionId">
<el-select v-model="formData.unionId" clearable filterable placeholder="请选择所属工会" style="width: 100%" :disabled="isUnionDisabled()">
<el-option v-for="item in unionOptions" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button v-if="!viewMode" type="primary" :loading="subDis" @click="doSubmit">保存</el-button>
</span>
</el-dialog>
<el-dialog
:title="batchTitle"
:visible.sync="batchFormVisible"
:close-on-click-modal="false"
width="74%">
<el-form :model="batchForm" :rules="batchRules" label-width="110px" ref="batchFormRef">
<el-form-item label="线路" prop="lineId">
<el-select v-model="batchForm.lineId" clearable filterable placeholder="请选择线路" style="width: 100%" @change="lineChange">
<el-option v-for="item in lineOptions" :key="item.id" :label="lineOptionLabel(item)" :value="item.id"></el-option>
</el-select>
</el-form-item>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="报名开始时间" prop="signupStartTime">
<el-date-picker v-model="batchForm.signupStartTime" type="datetime" value-format="yyyy-MM-dd HH:mm:ss" placeholder="选择时间" style="width: 100%"></el-date-picker>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="报名结束时间" prop="signupEndTime">
<el-date-picker v-model="batchForm.signupEndTime" type="datetime" value-format="yyyy-MM-dd HH:mm:ss" placeholder="选择时间" style="width: 100%"></el-date-picker>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="出行开始时间" prop="travelStartTime">
<el-date-picker v-model="batchForm.travelStartTime" type="date" value-format="yyyy-MM-dd" placeholder="选择日期" style="width: 100%" @change="travelStartChange"></el-date-picker>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="出行结束时间" prop="travelEndTime">
<el-date-picker v-model="batchForm.travelEndTime" type="date" value-format="yyyy-MM-dd" placeholder="选择日期" style="width: 100%" @change="travelEndChange"></el-date-picker>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="联系人" prop="contactName">
<el-input v-model="batchForm.contactName" maxlength="10" placeholder="请输入联系人"></el-input>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="联系方式" prop="contactPhone">
<el-input v-model="batchForm.contactPhone" maxlength="11" placeholder="请输入联系方式"></el-input>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="预计费用" prop="estimatedCost">
<el-input v-model="batchForm.estimatedCost" maxlength="10" placeholder="请输入预计费用"></el-input>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="最少成团人数" prop="minGroupPeople">
<el-input-number v-model="batchForm.minGroupPeople" :min="1" :precision="0" controls-position="right" style="width: 100%" @change="peopleChange"></el-input-number>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="最多成团人数" prop="maxGroupPeople">
<el-input-number v-model="batchForm.maxGroupPeople" :min="1" :precision="0" controls-position="right" style="width: 100%" @change="peopleChange"></el-input-number>
</el-form-item>
</el-col>
</el-row>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="batchFormVisible = false">取消</el-button>
<el-button type="primary" :loading="batchSubDis" @click="submitBatch">确定</el-button>
</span>
</el-dialog>
</div>
<style>
.el-table .direct-family-row > td,
.el-table .direct-family-row > td .cell {
color: #f56c6c;
}
</style>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
store,
mixins: [initTableMixins],
data() {
const currentYear = moment().format("YYYY")
const validateUnion = (rule, value, callback) => {
if (this.formData.organizationType === "schoolUnion" || value) {
callback()
} else {
callback(new Error("必填"))
}
}
const validateMobile = (rule, value, callback) => {
const mobileReg = /^1[3-9]\d{9}$/
if (!value) {
callback(new Error("必填"))
} else if (!mobileReg.test(value)) {
callback(new Error("手机号格式不正确"))
} else {
callback()
}
}
const validateMoney = (rule, value, callback) => {
const moneyReg = /^(0|[1-9]\d*)(\.\d{1,2})?$/
if (value === "" || value === null || value === undefined) {
callback(new Error("必填"))
} else if (!moneyReg.test(String(value))) {
callback(new Error("请输入非负金额,最多两位小数"))
} else {
callback()
}
}
const validatePeople = (rule, value, callback) => {
if (!value || value <= 0) {
callback(new Error("人数必须大于0"))
} else if (this.batchForm.minGroupPeople && this.batchForm.maxGroupPeople
&& this.batchForm.minGroupPeople > this.batchForm.maxGroupPeople) {
callback(new Error("最少人数不能大于最多人数"))
} else {
callback()
}
}
const validateBatchTime = (rule, value, callback) => {
if (!value) {
callback(new Error("必填"))
return
}
const form = this.batchForm
if (form.signupStartTime && form.signupEndTime && form.signupStartTime >= form.signupEndTime) {
callback(new Error("报名开始时间必须小于报名结束时间"))
return
}
if (form.travelStartTime && form.travelEndTime && form.travelStartTime > form.travelEndTime) {
callback(new Error("出行开始时间不能晚于出行结束时间"))
return
}
if (form.signupEndTime && form.travelStartTime && form.signupEndTime >= form.travelStartTime) {
callback(new Error("报名结束时间必须小于出行开始时间"))
return
}
callback()
}
return {
title: "",
dialogVisible: false,
viewMode: false,
subDis: false,
batchFormVisible: false,
batchSubDis: false,
batchTitle: "",
currentMatter: {},
batchForm: {},
lineOptions: [],
settingOptions: [],
unionOptions: [],
organizationTypeOptions: [],
pageForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
year: currentYear,
matterName: "",
unionId: "",
organizationType: ""
},
formData: {},
formRules: {
year: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
matterName: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
settingId: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
unionId: [{ validator: validateUnion, trigger: ["blur", "change"] }],
organizationType: [{ required: true, message: "必填", trigger: ["blur", "change"] }]
},
batchRules: {
lineId: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
signupStartTime: [{ validator: validateBatchTime, trigger: ["blur", "change"] }],
signupEndTime: [{ validator: validateBatchTime, trigger: ["blur", "change"] }],
travelStartTime: [{ validator: validateBatchTime, trigger: ["blur", "change"] }],
travelEndTime: [{ validator: validateBatchTime, trigger: ["blur", "change"] }],
contactName: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
contactPhone: [{ validator: validateMobile, trigger: ["blur", "change"] }],
minGroupPeople: [{ validator: validatePeople, trigger: ["blur", "change"] }],
maxGroupPeople: [{ validator: validatePeople, trigger: ["blur", "change"] }],
estimatedCost: [{ validator: validateMoney, trigger: ["blur", "change"] }]
}
}
},
watch: {
"pageForm.year"(year) {
this.loadSettings(year)
},
"batchForm.lineId"() {
if (this.batchFormVisible) {
this.applySelectedLineDefaults(true)
}
},
"batchForm.travelStartTime"() {
if (this.batchFormVisible) {
this.fillTravelEndByLot()
}
},
"batchForm.travelEndTime"() {
if (this.batchFormVisible) {
this.validateTravelFields()
}
}
},
methods: {
resetSearch() {
this.pageForm.year = moment().format("YYYY")
this.pageForm.matterName = ""
this.pageForm.unionId = ""
this.pageForm.organizationType = ""
this.loadSettings(this.pageForm.year)
this.doSearch()
},
tableRowClassName({ row }) {
return this.isDirectFamilyLine(row) ? "direct-family-row" : ""
},
isDirectFamilyLine(row) {
if (!row) return false
return row.directFamilyUnitLine === true
|| row.directFamilyUnitLine === 1
|| row.directFamilyUnitLine === "1"
},
emptyForm() {
const user = this.currentUser()
return {
year: moment().format("YYYY"),
matterName: "",
settingId: "",
creatorUserId: user.id || "",
creatorName: user.username || "",
unionId: "",
organizationType: "",
enabled: true
}
},
openAdd() {
this.title = "新增事项信息"
this.viewMode = false
this.formData = this.emptyForm()
this.loadSettings(this.formData.year, true)
this.dialogVisible = true
this.$nextTick(() => this.$refs.form && this.$refs.form.clearValidate())
},
openEdit(row) {
this.title = "编辑事项信息"
this.viewMode = false
this.loadDetail(row.id)
},
openView(row) {
this.title = "查看事项信息"
this.viewMode = true
this.loadDetail(row.id)
},
loadDetail(id) {
this.$axios.post(loc() + "/detail", { id }).then((res) => {
if (res.code === 0) {
this.formData = Object.assign(this.emptyForm(), res.data || {})
this.formData.year = this.formData.year ? String(this.formData.year) : ""
this.ensureSelectedUnionOption()
this.loadSettings(this.formData.year)
this.dialogVisible = true
this.$nextTick(() => this.$refs.form && this.$refs.form.clearValidate())
} else {
this.$message.warning(res.msg || "查询失败")
}
})
},
currentUser() {
return (this.$store && this.$store.state && this.$store.state.user) || {}
},
currentUnion() {
return this.currentUser().union || {}
},
organizationTypeChange() {
this.applyOrganizationTypeRule()
this.$nextTick(() => this.$refs.form && this.$refs.form.validateField("unionId"))
},
applyOrganizationTypeRule() {
if (this.formData.organizationType === "schoolUnion") {
this.formData.unionId = ""
return
}
if (this.formData.organizationType === "branchUnion" || this.formData.organizationType === "personal") {
const union = this.currentUnion()
this.formData.unionId = union.id || ""
this.ensureCurrentUnionOption(union)
}
},
ensureCurrentUnionOption(union) {
if (!union || !union.id) return
const exists = this.unionOptions.some(item => item.id === union.id)
if (!exists) {
this.unionOptions.push({ id: union.id, name: union.name || "" })
}
},
ensureSelectedUnionOption() {
if (!this.formData.unionId) return
const exists = this.unionOptions.some(item => item.id === this.formData.unionId)
if (!exists) {
this.unionOptions.push({ id: this.formData.unionId, name: this.formData.unionName || "" })
}
},
isUnionDisabled() {
return ["schoolUnion", "branchUnion", "personal"].includes(this.formData.organizationType)
},
yearChange(year) {
this.formData.settingId = ""
this.loadSettings(year, true)
},
loadSettings(year, autoSelectLatest) {
this.$axios.post(loc() + "/settingOptions", { year }).then((res) => {
if (res.code === 0) {
this.settingOptions = res.data || []
if (autoSelectLatest && !this.formData.settingId && this.settingOptions.length > 0) {
this.formData.settingId = this.settingOptions[0].id
}
}
})
},
loadUnions() {
this.$axios.post(loc() + "/unionOptions").then((res) => {
if (res.code === 0) {
this.unionOptions = res.data || []
}
})
},
loadOrganizationTypes() {
this.$axios.post(loc() + "/organizationTypeOptions").then((res) => {
if (res.code === 0) {
this.organizationTypeOptions = res.data || []
}
})
},
emptyBatchForm() {
return {
id: this.currentMatter.id || "",
lineId: "",
signupStartTime: "",
signupEndTime: "",
travelStartTime: "",
travelEndTime: "",
contactName: "",
contactPhone: "",
minGroupPeople: null,
maxGroupPeople: null,
estimatedCost: ""
}
},
openBatchForm(row) {
this.currentMatter = row
this.loadLines(row.year)
this.batchTitle = "选择线路"
this.batchForm = this.emptyBatchForm()
this.$axios.post(loc() + "/lineConfig", { matterId: row.id }).then((res) => {
if (res.code === 0) {
this.batchForm = Object.assign(this.emptyBatchForm(), res.data || {})
this.batchForm.travelStartTime = this.dateOnly(this.batchForm.travelStartTime)
this.batchForm.travelEndTime = this.dateOnly(this.batchForm.travelEndTime)
this.applySelectedLineDefaults(false)
if (!this.batchForm.minGroupPeople || !this.batchForm.maxGroupPeople) {
this.loadSettingPeople((data) => {
this.batchForm.minGroupPeople = data.minGroupPeople || null
this.batchForm.maxGroupPeople = data.maxGroupPeople || null
})
}
this.batchFormVisible = true
this.$nextTick(() => {
this.applySelectedLineDefaults(true)
this.$refs.batchFormRef && this.$refs.batchFormRef.clearValidate()
})
} else {
this.$message.warning(res.msg || "查询失败")
}
})
},
loadLines(year) {
this.$axios.post(loc() + "/lineOptions", { year }).then((res) => {
if (res.code === 0) {
this.lineOptions = res.data || []
this.applySelectedLineDefaults(true)
}
})
},
lineOptionLabel(item) {
if (!item) return ""
const lineName = this.lineField(item, "lineName") || ""
const lineType = this.lineField(item, "lineType") || ""
return lineType ? lineName + "(" + lineType + ")" : lineName
},
lineChange() {
this.applySelectedLineDefaults(true)
},
travelStartChange() {
this.fillTravelEndByLot()
},
travelEndChange() {
this.validateTravelFields()
},
selectedLine() {
return (this.lineOptions || []).find((item) => this.lineField(item, "id") === this.batchForm.lineId) || null
},
applySelectedLineDefaults(recalculateEnd) {
const line = this.selectedLine()
if (!line) return
const activityCost = this.lineField(line, "activityCost")
if (activityCost !== null && activityCost !== undefined && activityCost !== "") {
this.batchForm.estimatedCost = activityCost
}
if (recalculateEnd) {
this.fillTravelEndByLot()
}
},
fillTravelEndByLot() {
const line = this.selectedLine()
const days = this.lineLotDays(line)
if (!this.batchForm.travelStartTime || !days) return
this.$set(this.batchForm, "travelEndTime", moment(this.batchForm.travelStartTime).add(days - 1, "days").format("YYYY-MM-DD"))
this.validateTravelFields()
},
lineLotDays(line) {
if (!line) return null
const lotDays = this.lineField(line, "lotDays") || this.lineField(line, "lotValue")
if (!/^\d+$/.test(String(lotDays || ""))) return null
const days = parseInt(lotDays, 10)
return days > 0 ? days : null
},
lineField(line, field) {
if (!line) return null
if (line[field] !== undefined) return line[field]
const lowerField = field.toLowerCase()
const matchedKey = Object.keys(line).find((key) => key.toLowerCase() === lowerField)
return matchedKey ? line[matchedKey] : null
},
validateTravelFields() {
this.$nextTick(() => {
if (this.$refs.batchFormRef) {
this.$refs.batchFormRef.validateField(["travelStartTime", "travelEndTime"])
}
})
},
dateOnly(value) {
if (!value) return ""
return String(value).substring(0, 10)
},
loadSettingPeople(callback) {
this.$axios.post(loc() + "/settingPeople", { matterId: this.currentMatter.id }).then((res) => {
if (res.code === 0) {
const data = res.data || {}
callback && callback(data)
}
})
},
peopleChange() {
this.$nextTick(() => {
if (this.$refs.batchFormRef) {
this.$refs.batchFormRef.validateField(["minGroupPeople", "maxGroupPeople"])
}
})
},
submitBatch() {
this.$refs.batchFormRef.validate((valid) => {
if (!valid) return
this.batchSubDis = true
this.$axios.post(loc() + "/lineConfigDoSubmit", this.batchForm).then((res) => {
this.batchSubDis = false
if (res.code === 0) {
this.$message.success("保存成功")
this.batchFormVisible = false
this.pageData()
} else {
this.$message.warning(res.msg || "保存失败")
}
}).catch(() => {
this.batchSubDis = false
})
})
},
doSubmit() {
this.$refs.form.validate((valid) => {
if (!valid) return
this.subDis = true
this.$axios.post(loc() + "/doSubmit", this.formData).then((res) => {
this.subDis = false
if (res.code === 0) {
this.$message.success("保存成功")
this.dialogVisible = false
this.pageData()
} else {
this.$message.warning(res.msg || "保存失败")
}
}).catch(() => {
this.subDis = false
})
})
},
toggleEnabled(row) {
this.$axios.post(loc() + "/doSubmit", row).then((res) => {
if (res.code === 0) {
this.$message.success("状态已更新")
} else {
row.enabled = !row.enabled
this.$message.warning(res.msg || "状态更新失败")
}
}).catch(() => {
row.enabled = !row.enabled
})
},
doDelete(row) {
this.$set(row, "deleteLoading", true)
this.$axios.post(loc() + "/deleteInfo", { id: row.id }).then((res) => {
this.$set(row, "deleteLoading", false)
if (res.code !== 0) {
this.$message.warning(res.msg || "删除检查失败")
return
}
const data = res.data || {}
const signupCount = Number(data.signupCount || 0)
const matterName = row.matterName || ""
if (!data.canDelete) {
this.$alert("事项【" + matterName + "】已有 " + signupCount + " 人报名,不能删除。", "删除提醒", {
confirmButtonText: "知道了",
type: "warning"
})
return
}
this.$confirm("事项【" + matterName + "】暂无人员报名,确认删除吗?", "删除提醒", {
confirmButtonText: "确认删除",
cancelButtonText: "取消",
type: "warning",
confirmButtonClass: "el-button--danger"
}).then(() => {
this.$set(row, "deleteLoading", true)
this.$axios.post(loc() + "/doDelete", { id: row.id }).then((deleteRes) => {
this.$set(row, "deleteLoading", false)
if (deleteRes.code === 0) {
this.$message.success("删除成功")
this.pageData()
} else {
this.$message.warning(deleteRes.msg || "删除失败")
}
}).catch(() => {
this.$set(row, "deleteLoading", false)
})
}).catch(() => {})
}).catch(() => {
this.$set(row, "deleteLoading", false)
})
},
destroyForm() {
this.formData = {}
this.viewMode = false
}
},
mounted() {
this.loadSettings(this.pageForm.year)
this.loadUnions()
this.loadOrganizationTypes()
this.pageData()
}
})
</script>
<!--#
}
#-->
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,23 @@
<!--#
layout("/layouts/platform.html"){
#-->
<div id="app" v-cloak>
<el-card shadow="never">
<el-empty description="${title!'功能建设中'}">
<template slot="description">
<span>${title!'功能建设中'}正在分阶段建设中</span>
</template>
</el-empty>
</el-card>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app"
})
</script>
<!--#
}
#-->
@@ -0,0 +1,432 @@
<!--#
layout("/layouts/platform.html"){
#-->
<div id="app" v-cloak>
<guava ref="guava">
<el-card shadow="never">
<search :is-search-button="false">
<search-item label="创建年度">
<el-date-picker
v-model="pageForm.year"
type="year"
value-format="yyyy"
placeholder="请选择创建年度"
style="width: 100%">
</el-date-picker>
</search-item>
<search-item label="线路名称">
<el-input
v-model="pageForm.lineName"
clearable
placeholder="请输入线路名称"
style="width: 100%"
@keyup.enter.native="doSearch">
</el-input>
</search-item>
<search-item label="时间标段">
<el-select v-model="pageForm.lotId" clearable filterable placeholder="请选择时间标段" style="width: 100%">
<el-option v-for="item in lotOptions" :key="item.id" :label="item.lotName" :value="item.id"></el-option>
</el-select>
</search-item>
<div class="search-query" style="margin-left: auto;">
<el-button size="medium" type="primary" icon="el-icon-search" @click="doSearch">搜索</el-button>
<el-button size="medium" @click="resetSearch">重置</el-button>
</div>
</search>
</el-card>
<el-card class="mt10" shadow="never">
<table-tool :app="this" label="线路列表">
<el-button @click="openAdd" size="medium" type="primary">
<i class="el-icon-plus"></i>
新增
</el-button>
</table-tool>
<el-table
v-loading="tableLoading"
:data="tableData"
:size="tableSize"
border
:default-sort="{prop: 'enabled', order: 'descending'}"
:row-class-name="tableRowClassName"
@sort-change="pageOrder">
<el-table-column label="序号" type="index" :index="indexMethod" width="80" align="center" header-align="center"></el-table-column>
<el-table-column label="创建年度" prop="year" width="120" sortable="custom" align="center" header-align="center"></el-table-column>
<el-table-column label="线路编号" prop="lineCode" width="140" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="线路名称" prop="lineName" min-width="220" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="时间标段" prop="lotName" width="140" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="线路类型" prop="lineType" width="140" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="创建人" prop="creatorName" width="140" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="是否对外开放" prop="openFlag" width="160" align="center" header-align="center">
<template slot-scope="{row}">
<el-switch
v-model="row.openFlag"
active-text="是"
inactive-text="否"
@change="toggleOpenFlag(row)">
</el-switch>
</template>
</el-table-column>
<el-table-column label="激活状态" prop="enabled" width="160" sortable="custom" align="center" header-align="center">
<template slot-scope="{row}">
<el-switch
v-model="row.enabled"
active-text="启用"
inactive-text="禁用"
@change="toggleEnabled(row)">
</el-switch>
</template>
</el-table-column>
<el-table-column label="操作" width="260" align="center" header-align="center">
<template slot-scope="{row}">
<el-button size="mini" type="primary" @click="openView(row)">查看</el-button>
<el-button size="mini" type="primary" @click="openEdit(row)">编辑</el-button>
<el-button size="mini" type="danger" @click="doDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
</guava>
<el-dialog
:title="title"
:visible.sync="dialogVisible"
:close-on-click-modal="false"
width="72%"
@closed="destroyEditor">
<el-form :model="formData" :rules="formRules" label-width="110px" ref="form" :disabled="viewMode">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="创建年度" prop="year">
<el-date-picker
v-model="formData.year"
type="year"
value-format="yyyy"
placeholder="请选择创建年度"
style="width: 100%"
@change="yearChange">
</el-date-picker>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="旅行社名称" prop="travelAgencyId">
<el-select v-model="formData.travelAgencyId" clearable filterable placeholder="请选择旅行社" style="width: 100%">
<el-option v-for="item in travelAgencyOptions" :key="item.id" :label="item.agencyName" :value="item.id"></el-option>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="线路名称" prop="lineName">
<el-input v-model="formData.lineName" maxlength="30" show-word-limit placeholder="请输入线路名称"></el-input>
</el-form-item>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="创建人">
<el-input v-model="formData.creatorName" readonly placeholder="当前登录人"></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="所在单位">
<el-input v-model="formData.unitName" readonly placeholder="当前登录人所在单位"></el-input>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="线路编号" prop="lineCode">
<el-input v-model="formData.lineCode" maxlength="50" placeholder="建议年度加序号,如202601"></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="激活状态" prop="enabled">
<el-radio-group v-model="formData.enabled">
<el-radio :label="true">启用</el-radio>
<el-radio :label="false">禁用</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="是否对外开放" prop="openFlag">
<el-radio-group v-model="formData.openFlag">
<el-radio :label="true">是</el-radio>
<el-radio :label="false">否</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="直系亲属线路" prop="directFamilyUnitLine">
<el-radio-group v-model="formData.directFamilyUnitLine">
<el-radio :label="true">是</el-radio>
<el-radio :label="false">否</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="线路类型" prop="lineType">
<el-select v-model="formData.lineType" clearable placeholder="请选择线路类型" style="width: 100%">
<el-option v-for="item in lineTypeOptions" :key="item.code" :label="item.name" :value="item.name"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="时间标段" prop="lotId">
<el-select v-model="formData.lotId" clearable filterable placeholder="请选择时间标段" style="width: 100%">
<el-option v-for="item in lotOptions" :key="item.id" :label="item.lotName" :value="item.id"></el-option>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="线路内容" prop="lineContent">
<text-editor v-model="formData.lineContent"></text-editor>
</el-form-item>
<el-form-item label="移动端缩略图" prop="mobileThumb">
<!-- 与旅行社管理保持一致:移动端缩略图使用单图上传并保存URL。 -->
<file-upload
style="--upload-width: 200px;--upload-height:108px"
:upload_number="1"
:upload_size="20971520"
:value.sync="formData.mobileThumb"
accept=".jpg,.jpeg,.png"
complete_result
upload_mode="image"
upload_result_category="interval">
</file-upload>
<div class="el-upload__tip">支持jpg、jpeg、png格式,大小不超过20MB。</div>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button v-if="!viewMode" type="primary" :loading="submitLoading" @click="doSubmit">保存</el-button>
</span>
</el-dialog>
</div>
<style>
.el-table .direct-family-row > td,
.el-table .direct-family-row > td .cell {
color: #f56c6c;
}
</style>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
store,
mixins: [initTableMixins],
data() {
const currentYear = moment().format("YYYY")
return {
title: "",
dialogVisible: false,
viewMode: false,
submitLoading: false,
travelAgencyOptions: [],
lotOptions: [],
lineTypeOptions: [],
pageForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
pageOrderName: "enabled",
pageOrderBy: "descending",
year: currentYear,
lineName: "",
lotId: ""
},
formData: {},
formRules: {
year: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
travelAgencyId: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
lineName: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
lineCode: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
lineType: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
lotId: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
mobileThumb: [{ required: true, message: "请上传移动端缩略图", trigger: ["blur", "change"] }]
}
}
},
methods: {
resetSearch() {
this.pageForm.year = moment().format("YYYY")
this.pageForm.lineName = ""
this.pageForm.lotId = ""
this.loadOptions(this.pageForm.year)
this.doSearch()
},
tableRowClassName({ row }) {
return this.isDirectFamilyLine(row) ? "direct-family-row" : ""
},
isDirectFamilyLine(row) {
if (!row) return false
return row.directFamilyUnitLine === true
|| row.directFamilyUnitLine === 1
|| row.directFamilyUnitLine === "1"
},
emptyForm() {
const user = (this.$store && this.$store.state && this.$store.state.user) || {}
return {
year: moment().format("YYYY"),
travelAgencyId: "",
creatorUserId: user.id || "",
creatorName: user.username || "",
unitId: user.unit?.id || "",
unitName: user.unit?.name || "",
lineName: "",
lineCode: "",
enabled: true,
openFlag: true,
directFamilyUnitLine: false,
lineType: "",
lotId: "",
lineContent: "",
mobileThumb: ""
}
},
openAdd() {
this.title = "新增线路信息"
this.viewMode = false
this.formData = this.emptyForm()
this.loadOptions(this.formData.year)
this.dialogVisible = true
this.$nextTick(() => this.$refs.form && this.$refs.form.clearValidate())
},
openEdit(row) {
this.title = "编辑线路信息"
this.viewMode = false
this.loadDetail(row.id)
},
openView(row) {
this.title = "查看线路信息"
this.viewMode = true
this.loadDetail(row.id)
},
loadDetail(id) {
this.$axios.post(loc() + "/detail", { id }).then((res) => {
if (res.code === 0) {
this.formData = Object.assign(this.emptyForm(), res.data || {})
this.formData.year = this.formData.year ? String(this.formData.year) : ""
this.loadOptions(this.formData.year)
this.dialogVisible = true
this.$nextTick(() => this.$refs.form && this.$refs.form.clearValidate())
} else {
this.$message.warning(res.msg || "查询失败")
}
})
},
yearChange(year) {
this.formData.travelAgencyId = ""
this.formData.lotId = ""
this.loadOptions(year)
},
loadOptions(year) {
this.loadTravelAgencies(year)
this.loadLots(year)
this.loadLineTypes()
},
loadTravelAgencies(year) {
this.$axios.post(loc() + "/travelAgencyOptions", { year }).then((res) => {
if (res.code === 0) {
this.travelAgencyOptions = res.data || []
}
})
},
loadLots(year) {
this.$axios.post(loc() + "/lotOptions", { year }).then((res) => {
if (res.code === 0) {
this.lotOptions = res.data || []
}
})
},
loadLineTypes() {
this.$axios.post(loc() + "/lineTypeOptions").then((res) => {
if (res.code === 0) {
this.lineTypeOptions = res.data || []
}
})
},
doSubmit() {
this.$refs.form.validate((valid) => {
if (!valid) return
this.submitLoading = true
this.$axios.post(loc() + "/doSubmit", this.formData).then((res) => {
this.submitLoading = false
if (res.code === 0) {
this.$message.success("保存成功")
this.dialogVisible = false
this.pageData()
} else {
this.$message.warning(res.msg || "保存失败")
}
}).catch(() => {
this.submitLoading = false
})
})
},
toggleEnabled(row) {
this.$axios.post(loc() + "/doSubmit", row).then((res) => {
if (res.code === 0) {
this.$message.success("状态已更新")
} else {
row.enabled = !row.enabled
this.$message.warning(res.msg || "状态更新失败")
}
}).catch(() => {
row.enabled = !row.enabled
})
},
toggleOpenFlag(row) {
this.$axios.post(loc() + "/doSubmit", row).then((res) => {
if (res.code === 0) {
this.$message.success("对外开放状态已更新")
} else {
row.openFlag = !row.openFlag
this.$message.warning(res.msg || "对外开放状态更新失败")
}
}).catch(() => {
row.openFlag = !row.openFlag
})
},
doDelete(row) {
this.$confirm("确定删除【" + row.lineName + "】吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
this.$axios.post(loc() + "/doDelete", { id: row.id }).then((res) => {
if (res.code === 0) {
this.$message.success("删除成功")
this.pageData()
} else {
this.$message.warning(res.msg || "删除失败")
}
})
})
},
destroyEditor() {
this.formData = {}
this.viewMode = false
}
},
mounted() {
this.loadOptions(this.pageForm.year)
this.pageData()
},
watch: {
"pageForm.year"(year) {
this.pageForm.lotId = ""
this.loadOptions(year)
}
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,547 @@
<!--#
layout("/layouts/platform.html"){
#-->
<div id="app" v-cloak>
<guava ref="guava">
<el-card shadow="never">
<search :is-search-button="false">
<search-item label="开始年度">
<el-date-picker
v-model="pageForm.startYear"
type="year"
value-format="yyyy"
placeholder="请选择开始年度"
style="width: 100%">
</el-date-picker>
</search-item>
<search-item label="结束年度">
<el-date-picker
v-model="pageForm.endYear"
type="year"
value-format="yyyy"
placeholder="请选择结束年度"
style="width: 100%">
</el-date-picker>
</search-item>
<search-item label="姓名/工号">
<el-input
v-model="pageForm.keyword"
clearable
placeholder="请输入姓名或工号"
style="width: 100%"
@keyup.enter.native="doSearch">
</el-input>
</search-item>
<search-item label="所属工会">
<el-select v-model="pageForm.unionId" clearable filterable placeholder="请选择所属工会" style="width: 100%">
<el-option v-for="item in unionOptions" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select>
</search-item>
<search-item label="线路">
<el-select v-model="pageForm.lineId" clearable filterable placeholder="请选择线路" style="width: 100%">
<el-option v-for="item in lineOptions" :key="item.lineId" :label="item.lineName" :value="item.lineId"></el-option>
</el-select>
</search-item>
<search-item label="出行时段">
<el-select v-model="pageForm.travelPeriod" clearable filterable placeholder="请选择出行时段" style="width: 100%">
<el-option v-for="item in travelPeriodOptions" :key="item.travelPeriod" :label="item.travelPeriod" :value="item.travelPeriod"></el-option>
</el-select>
</search-item>
<search-item label="线路类型">
<el-select v-model="pageForm.lineType" clearable filterable placeholder="请选择线路类型" style="width: 100%">
<el-option v-for="item in lineTypeOptions" :key="item.lineType" :label="item.lineType" :value="item.lineType"></el-option>
</el-select>
</search-item>
<div class="search-query" style="margin-left: auto;">
<el-button size="medium" type="primary" icon="el-icon-search" @click="doSearch">搜索</el-button>
<el-button size="medium" @click="resetSearch">重置</el-button>
</div>
</search>
</el-card>
<el-card class="mt10" shadow="never">
<table-tool :app="this" label="校工会审核">
<el-radio-group class="mr5" size="small" v-model="pageForm.audit" @change="changeAudit">
<el-radio-button :label="true">已审核</el-radio-button>
<el-radio-button :label="false">未审核</el-radio-button>
</el-radio-group>
</table-tool>
<el-table
v-loading="tableLoading"
:data="tableData"
:size="tableSize"
border
@sort-change="pageOrder">
<el-table-column label="序号" type="index" :index="indexMethod" width="80" align="center" header-align="center"></el-table-column>
<el-table-column label="工号" prop="jobNo" width="130" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="姓名" prop="userName" width="120" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="所在单位" prop="unitName" min-width="180" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="报名线路" prop="lineName" min-width="220" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="出行时段" prop="travelPeriod" min-width="220" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="线路类型" prop="lineType" width="130" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="报名时间" prop="signupTime" width="170" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="携带家属" prop="familyCount" width="120" sortable="custom" align="center" header-align="center">
<template slot-scope="{row}">
<el-tag size="mini" :type="Number(row.familyCount || 0) > 0 ? 'success' : 'info'">{{ row.familyCount || 0 }}人</el-tag>
</template>
</el-table-column>
<el-table-column label="报销超出费用" prop="overCostReimbursed" width="140" align="center" header-align="center">
<template slot-scope="{row}">
<el-tag size="mini" :type="row.overCostReimbursed ? 'success' : 'info'">{{ row.overCostReimbursed ? '是' : '否' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="当前节点" prop="curTaskName" width="140" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="流程状态" prop="instanceState" width="120" sortable="custom" align="center" header-align="center">
<template slot-scope="{row}">
<enum-tag :value="row.instanceState" name="ProcessInstanceStateEnum" label_key="message" size="small"></enum-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="160" align="center" header-align="center" fixed="right">
<template slot-scope="{row}">
<el-button size="mini" type="primary" @click="openView(row)">查看</el-button>
<el-button v-if="row.taskState === 10" size="mini" type="primary" @click="openApproval(row)">审核</el-button>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
<template #edit>
<tour-approval-info ref="infoRef">
<div v-if="showApprovalForm">
<div class="process-title">
{{ formData.taskName }}
</div>
<el-form
:model="formData"
ref="formRef"
label-width="0"
label-suffix=":"
class="flow-task-form">
<el-form-item
label="审批意见"
prop="tf_opinion"
:rules="[{required:true,message:'必填',trigger:['change','blur']}]">
<user-opinion-textarea v-model="formData.tf_opinion"></user-opinion-textarea>
</el-form-item>
</el-form>
<el-row type="flex" justify="end">
<el-button @click="$refs.guava.index()" size="small">取消</el-button>
<el-button @click="handleTaskAction(6)" size="small" type="info">退回到发起人</el-button>
<el-button @click="handleTaskAction(2)" size="small" type="danger">拒绝申请</el-button>
<el-button @click="handleTaskAction(1)" size="small" type="primary">同意申请</el-button>
</el-row>
</div>
</tour-approval-info>
</template>
</guava>
</div>
<style>
#app .search .search-item {
width: calc((100% - 150px) / 4);
}
#app .search .search-query {
margin-left: auto;
}
@media screen and (max-width: 1350px) {
#app .search .search-item {
width: calc((100% - 100px) / 3);
}
}
@media screen and (max-width: 1200px) {
#app .search .search-item {
width: calc((100% - 50px) / 2);
}
}
@media screen and (max-width: 992px) {
#app .search .search-item {
width: 100%;
}
}
.tour-approval-section {
margin-bottom: 14px;
}
.tour-approval-table {
width: 100%;
}
.tour-approval-empty {
color: #909399;
}
</style>
<script nonce="${cspNonce!}">
const TOUR_APPROVAL_INFO = {
template: /*language=HTML*/ `
<div>
<div class="process-title">
报名信息
<el-link type="primary" @click="openChart">点击查看流程图</el-link>
</div>
<el-descriptions :column="3" border class="tour-approval-section">
<el-descriptions-item label="工号">{{ detail.jobNo || '' }}</el-descriptions-item>
<el-descriptions-item label="姓名">{{ detail.userName || '' }}</el-descriptions-item>
<el-descriptions-item label="性别">{{ detail.gender || '' }}</el-descriptions-item>
<el-descriptions-item label="身份证号">{{ detail.idCard || '' }}</el-descriptions-item>
<el-descriptions-item label="所在单位">{{ detail.unitName || '' }}</el-descriptions-item>
<el-descriptions-item label="所在工会">{{ detail.unionName || '' }}</el-descriptions-item>
<el-descriptions-item label="报名时间">{{ detail.signupTime || '' }}</el-descriptions-item>
<el-descriptions-item label="报名线路">{{ detail.lineName || '' }}</el-descriptions-item>
<el-descriptions-item label="出行时段">{{ detail.travelPeriod || '' }}</el-descriptions-item>
<el-descriptions-item label="线路类型">{{ detail.lineType || '' }}</el-descriptions-item>
<el-descriptions-item label="报名酒店">{{ detail.hotelName || '' }}</el-descriptions-item>
<el-descriptions-item label="旅行社">{{ detail.travelAgencyName || '' }}</el-descriptions-item>
<el-descriptions-item label="是否携带家属">
{{ familyText() }}
</el-descriptions-item>
<el-descriptions-item label="报销超出费用">{{ detail.overCostReimbursed ? '是' : '否' }}</el-descriptions-item>
<el-descriptions-item v-if="fillBedInfo" label="意向拼床人">{{ detail.intendedRoommate || '' }}</el-descriptions-item>
<el-descriptions-item v-if="fillBedInfo" label="床型">{{ detail.bedType || '' }}</el-descriptions-item>
<el-descriptions-item v-if="fillBedInfo" label="床位信息">{{ detail.bedInfo || '' }}</el-descriptions-item>
</el-descriptions>
<div v-if="isDirectFamilyLine()" class="tour-approval-section">
<div class="process-title">直系亲属线路</div>
<el-descriptions :column="3" border>
<el-descriptions-item label="亲属姓名">{{ directRelative.relativeName || '' }}</el-descriptions-item>
<el-descriptions-item label="所在单位">{{ directRelative.unitName || '' }}</el-descriptions-item>
<el-descriptions-item label="亲属关系">{{ directRelative.relationshipName || '' }}</el-descriptions-item>
<el-descriptions-item label="线路名称">{{ directRelative.lineName || '' }}</el-descriptions-item>
<el-descriptions-item label="出行开始日期">{{ directRelative.travelStartTime || '' }}</el-descriptions-item>
<el-descriptions-item label="出行结束日期">{{ directRelative.travelEndTime || '' }}</el-descriptions-item>
</el-descriptions>
</div>
<div v-if="showFamilySection()" class="tour-approval-section">
<div class="process-title">亲属信息</div>
<el-table :data="familyData" border size="mini" empty-text="暂无亲属信息" class="tour-approval-table">
<el-table-column label="序号" type="index" width="70" align="center" header-align="center"></el-table-column>
<el-table-column label="家属姓名" prop="familyName" min-width="120" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="性别" prop="gender" width="90" align="center" header-align="center"></el-table-column>
<el-table-column label="身份证号码" prop="idCard" min-width="180" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="年龄" prop="age" width="90" align="center" header-align="center"></el-table-column>
<el-table-column label="关系" prop="relationship" width="110" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column v-if="fillBedInfo" label="床型" prop="bedType" width="110" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column v-if="fillBedInfo" label="床位" prop="bedInfo" min-width="120" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column v-if="fillBedInfo" label="意向拼床人" prop="intendedRoommate" min-width="130" align="center" header-align="center" show-overflow-tooltip></el-table-column>
</el-table>
</div>
<template v-for="task in doneTasks">
<div class="mt10">
<div class="process-title">{{ task.displayName }}</div>
<el-descriptions border class="flow-task-form" :column="3" :key="task.id" v-if="task.ext.isFirstTaskNode">
<el-descriptions-item label="申请用户">
{{ task.ext.initiatorName }}({{ task.ext.initiatorAccount }})
</el-descriptions-item>
<el-descriptions-item label="申请时间">{{ task.finishTime }}</el-descriptions-item>
<el-descriptions-item label="办理结果">
<dict-tag :options="dict.type.PROCESS_TASK_SUBMIT_TYPE" :value="task.ext.submitType"></dict-tag>
</el-descriptions-item>
</el-descriptions>
<el-descriptions border class="flow-task-form" :column="3" :key="task.id" v-else>
<el-descriptions-item label="办理用户">
{{ task.taskFormData.userName }}({{ task.taskFormData.loginName }})
</el-descriptions-item>
<el-descriptions-item label="办理时间">{{ task.finishTime }}</el-descriptions-item>
<el-descriptions-item label="办理结果">
<dict-tag :options="dict.type.PROCESS_TASK_SUBMIT_TYPE" :value="task.ext.submitType"></dict-tag>
</el-descriptions-item>
<el-descriptions-item label="办理意见" :span="3">
{{ task.taskFormData.opinion || task.taskFormData.tf_opinion || '' }}
</el-descriptions-item>
</el-descriptions>
</div>
</template>
<slot></slot>
<snaker-chart ref="snakerChartRef"></snaker-chart>
</div>
`,
dicts: ["PROCESS_TASK_SUBMIT_TYPE"],
data() {
return {
row: {},
detail: {},
familyData: [],
directRelative: {},
allowFamily: false,
fillBedInfo: true,
directFamilyUnitLine: false,
doneTasks: []
}
},
methods: {
onOpen(row) {
this.row = row || {}
this.detail = {}
this.familyData = []
this.directRelative = {}
this.allowFamily = false
this.fillBedInfo = true
this.directFamilyUnitLine = false
this.doneTasks = []
this.info()
this.getDoneTasks()
},
info() {
this.$axios.post(loc() + "/detail", { id: this.row.id }).then((res) => {
if (res.code === 0) {
const data = res.data || {}
this.detail = data.ledger || {}
this.detail.travelPeriod = data.travelPeriod || this.row.travelPeriod || ""
this.familyData = data.families || []
this.directRelative = data.directRelative || {}
this.allowFamily = this.toBoolean(data.allowFamily)
this.fillBedInfo = data.fillBedInfo === undefined || data.fillBedInfo === null || this.toBoolean(data.fillBedInfo)
this.directFamilyUnitLine = this.toBoolean(data.directFamilyUnitLine)
} else {
this.$message.warning(res.msg || "查询失败")
}
})
},
toBoolean(value) {
return value === true || value === 1 || value === "1"
},
isDirectFamilyLine() {
return this.directFamilyUnitLine
|| this.toBoolean(this.detail.directFamilyUnitLine)
|| (this.directRelative && this.directRelative.id)
},
hasFamily() {
return this.toBoolean(this.detail.hasFamily) || this.familyData.length > 0
},
showFamilySection() {
return this.allowFamily && this.hasFamily() && !this.isDirectFamilyLine()
},
familyText() {
if (this.isDirectFamilyLine()) {
return "否"
}
if (!this.allowFamily) {
return "否"
}
return this.hasFamily() ? "是" : "否"
},
getDoneTasks() {
this.$axios.post("/flow/common/doneTasks", { bizId: this.row.id }).then((res) => {
if (res.code === 0) {
this.doneTasks = res.data || []
}
})
},
openChart() {
if (!this.row.instanceProcessDefineId || !this.row.instanceId) {
this.$message.warning("暂无流程图信息")
return
}
this.$refs.snakerChartRef.onOpenFull(this.row.instanceProcessDefineId, this.row.instanceId)
}
}
}
new Vue({
el: "#app",
mixins: [initTableMixins],
data() {
const currentYear = moment().format("YYYY")
return {
unionOptions: [],
lineOptions: [],
travelPeriodOptions: [],
lineTypeOptions: [],
showApprovalForm: false,
formData: {
tf_opinion: ""
},
pageForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
pageOrderName: "signupTime",
pageOrderBy: "descending",
audit: false,
startYear: currentYear,
endYear: currentYear,
keyword: "",
unionId: "",
lineId: "",
travelPeriod: "",
lineType: ""
}
}
},
methods: {
resetSearch() {
this.pageForm.startYear = moment().format("YYYY")
this.pageForm.endYear = moment().format("YYYY")
this.pageForm.keyword = ""
this.pageForm.unionId = ""
this.pageForm.lineId = ""
this.pageForm.travelPeriod = ""
this.pageForm.lineType = ""
this.loadOptions()
this.doSearch()
},
changeAudit() {
this.pageForm.pageNumber = 1
this.clearCascadeFilters()
this.loadOptions()
this.pageData()
},
clearCascadeFilters() {
this.pageForm.unionId = ""
this.pageForm.lineId = ""
this.pageForm.travelPeriod = ""
this.pageForm.lineType = ""
},
openView(row) {
this.$refs.guava.edit(() => {
this.showApprovalForm = false
this.formData = {
tf_opinion: ""
}
this.$refs.infoRef.onOpen(row)
})
},
openApproval(row) {
this.$refs.guava.edit(() => {
this.showApprovalForm = true
this.formData = {
processTaskId: row.taskId,
taskName: row.curTaskName || row.taskName,
tf_opinion: ""
}
this.$refs.infoRef.onOpen(row)
})
},
handleTaskAction(val) {
this.$refs.formRef.validate((valid) => {
if (!valid) return
this.$confirm("您确定要提交吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
const loading = createLoading()
this.$axios.post("/flow/common/executeTask", {
data: JSON.stringify({
...this.formData,
submitType: val
})
}).then((res) => {
if (res.code === 0) {
this.$refs.guava.index()
this.$message.success(res.msg || "操作成功")
this.doSearch()
} else {
this.$message.warning(res.msg || "操作失败")
}
}).finally(() => {
loading.close()
})
}).catch(() => {})
})
},
loadOptions() {
this.loadUnionOptions()
this.loadLineOptions()
this.loadTravelPeriodOptions()
this.loadLineTypeOptions()
},
baseOptionParams() {
return {
audit: this.pageForm.audit,
startYear: this.pageForm.startYear,
endYear: this.pageForm.endYear,
keyword: this.pageForm.keyword,
unionId: this.pageForm.unionId,
lineId: this.pageForm.lineId,
travelPeriod: this.pageForm.travelPeriod,
lineType: this.pageForm.lineType
}
},
loadUnionOptions() {
this.$axios.post(loc() + "/unionOptions", this.baseOptionParams()).then((res) => {
if (res.code === 0) {
this.unionOptions = res.data || []
if (this.pageForm.unionId && !this.unionOptions.some(item => item.id === this.pageForm.unionId)) {
this.pageForm.unionId = ""
}
}
})
},
loadLineOptions() {
this.$axios.post(loc() + "/lineOptions", this.baseOptionParams()).then((res) => {
if (res.code === 0) {
this.lineOptions = res.data || []
if (this.pageForm.lineId && !this.lineOptions.some(item => item.lineId === this.pageForm.lineId)) {
this.pageForm.lineId = ""
}
}
})
},
loadTravelPeriodOptions() {
this.$axios.post(loc() + "/travelPeriodOptions", this.baseOptionParams()).then((res) => {
if (res.code === 0) {
this.travelPeriodOptions = res.data || []
if (this.pageForm.travelPeriod && !this.travelPeriodOptions.some(item => item.travelPeriod === this.pageForm.travelPeriod)) {
this.pageForm.travelPeriod = ""
}
}
})
},
loadLineTypeOptions() {
this.$axios.post(loc() + "/lineTypeOptions", this.baseOptionParams()).then((res) => {
if (res.code === 0) {
this.lineTypeOptions = res.data || []
if (this.pageForm.lineType && !this.lineTypeOptions.some(item => item.lineType === this.pageForm.lineType)) {
this.pageForm.lineType = ""
}
}
})
}
},
mounted() {
this.loadOptions()
this.pageData()
},
components: {
"tour-approval-info": TOUR_APPROVAL_INFO
},
watch: {
"pageForm.startYear"() {
this.clearCascadeFilters()
this.loadOptions()
},
"pageForm.endYear"() {
this.clearCascadeFilters()
this.loadOptions()
},
"pageForm.lineId"() {
this.loadTravelPeriodOptions()
this.loadLineTypeOptions()
},
"pageForm.travelPeriod"() {
this.loadLineOptions()
this.loadLineTypeOptions()
},
"pageForm.lineType"() {
this.loadLineOptions()
this.loadTravelPeriodOptions()
},
"pageForm.unionId"() {
this.loadLineOptions()
this.loadTravelPeriodOptions()
this.loadLineTypeOptions()
}
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,636 @@
<!--#
layout("/layouts/platform.html"){
#-->
<div id="app" v-cloak>
<guava ref="guava">
<el-card shadow="never">
<search :is-search-button="false">
<search-item label="年度">
<el-date-picker
v-model="pageForm.year"
type="year"
value-format="yyyy"
placeholder="请选择年度"
style="width: 100%">
</el-date-picker>
</search-item>
<search-item label="配置名称">
<el-input
v-model="pageForm.configName"
clearable
placeholder="请输入疗休养配置名称"
style="width: 100%"
@keyup.enter.native="doSearch">
</el-input>
</search-item>
<div class="search-query">
<el-button size="medium" type="primary" icon="el-icon-search" @click="doSearch">搜索</el-button>
<el-button size="medium" @click="resetSearch">重置</el-button>
</div>
</search>
</el-card>
<el-card class="mt10" shadow="never">
<table-tool :app="this" label="疗休养配置">
<el-button type="primary" size="medium" @click="openAdd">
<i class="el-icon-plus"></i>
新建
</el-button>
</table-tool>
<el-table
:data="tableData"
:size="tableSize"
border
class="vi-table"
ref="table"
row-key="id"
v-loading="tableLoading"
@sort-change="pageOrder"
>
<el-table-column label="序号" type="index" :index="indexMethod" width="70" align="center" header-align="center"></el-table-column>
<el-table-column label="年度" prop="year" width="100" align="center" header-align="center"></el-table-column>
<el-table-column label="疗休养配置名称" prop="configName" min-width="260" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="周期开始年度" prop="cycleStartYear" width="130" align="center" header-align="center"></el-table-column>
<el-table-column label="最少成团人数" prop="minGroupPeople" width="120" align="center" header-align="center"></el-table-column>
<el-table-column label="最多成团人数" prop="maxGroupPeople" width="120" align="center" header-align="center"></el-table-column>
<el-table-column label="是否携带家属" prop="allowFamily" width="120" align="center" header-align="center">
<template slot-scope="{row}">
<el-tag size="mini" type="success" v-if="row.allowFamily">允许</el-tag>
<el-tag size="mini" type="info" v-else>不允许</el-tag>
</template>
</el-table-column>
<el-table-column label="是否启用" prop="enabled" width="100" align="center" header-align="center">
<template slot-scope="{row}">
<el-tag size="mini" type="success" v-if="row.enabled">启用</el-tag>
<el-tag size="mini" type="info" v-else>停用</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="150" align="center" header-align="center">
<template slot-scope="{row}">
<el-button size="mini" type="primary" @click="openEdit(row)">编辑</el-button>
<el-button size="mini" type="danger" @click="doDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
</guava>
<el-dialog
:title="title"
:visible.sync="dialogVisible"
:close-on-click-modal="false"
width="64.8%"
@closed="destroyEditor">
<el-form :model="formData" :rules="formRules" label-width="120px" ref="form">
<div v-if="title === '新建疗休养配置'" class="tour-setting-inherit">
<el-button type="primary" plain size="medium" icon="el-icon-copy-document" :loading="inheritLoading" @click="inheritPreviousYearInfo">延用上一年信息</el-button>
</div>
<el-tabs v-model="activeTab" type="card">
<el-tab-pane label="基本信息" name="basic">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="年度" prop="year">
<el-date-picker
v-model="formData.year"
type="year"
value-format="yyyy"
placeholder="请选择年度"
style="width: 100%"
></el-date-picker>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="配置名称" prop="configName">
<el-input v-model="formData.configName" maxlength="100" placeholder="请输入配置名称"></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="疗休养类型" prop="tourType">
<el-select v-model="formData.tourType" clearable allow-create filterable placeholder="请选择或输入类型" style="width: 100%">
<el-option label="省内疗休养" value="省内疗休养"></el-option>
<el-option label="省外疗休养" value="省外疗休养"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="可参加人员" prop="activityGroupId">
<div class="tour-user-scope">
<el-select
v-model="formData.activityGroupId"
placeholder="请选择可参加人员"
filterable
clearable
style="width: 100%">
<el-option
v-for="item in activityGroupList"
:key="item.groupId"
:label="item.groupName"
:value="item.groupId">
</el-option>
</el-select>
<el-button type="primary" @click="openUserScope">设置</el-button>
</div>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="最少成团人数" prop="minGroupPeople">
<el-input-number v-model="formData.minGroupPeople" :controls="false" :min="0" :precision="0" placeholder="请输入最少成团人数" style="width: 100%"></el-input-number>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="最多成团人数" prop="maxGroupPeople">
<el-input-number v-model="formData.maxGroupPeople" :controls="false" :min="0" :precision="0" placeholder="请输入最多成团人数" style="width: 100%"></el-input-number>
</el-form-item>
</el-col>
<el-col :span="24">
<div class="tour-setting-basic-divider"></div>
</el-col>
<el-col :span="12">
<el-form-item label="省外几年去一次" prop="outProvinceYears">
<el-input-number v-model="formData.outProvinceYears" :controls="false" :min="0" :precision="0" placeholder="请输入省外间隔年限" style="width: 100%"></el-input-number>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="省外人数占比" prop="outProvinceRatio">
<el-input-number v-model="formData.outProvinceRatio" :controls="false" :min="0" :max="100" :precision="2" placeholder="请输入省外人数占比" style="width: 100%"></el-input-number>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="省外占比类型" prop="outProvinceRatioType">
<el-select v-model="formData.outProvinceRatioType" placeholder="请选择省外占比类型" style="width: 100%" @change="outProvinceRatioTypeChange">
<el-option label="当年报名人数" value="当年报名人数"></el-option>
<el-option label="可参加教职工人数" value="可参加教职工人数"></el-option>
<el-option label="固定人数" value="固定人数"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col v-if="formData.outProvinceRatioType === '固定人数'" :span="12">
<el-form-item label="固定人数" prop="outProvinceFixedPeople">
<el-input-number v-model="formData.outProvinceFixedPeople" :controls="false" :min="0" :precision="0" placeholder="请输入固定人数" style="width: 100%"></el-input-number>
</el-form-item>
</el-col>
<el-col :span="24">
<div class="tour-setting-basic-divider"></div>
</el-col>
<el-col :span="12">
<el-form-item label="周期开始年度" prop="cycleStartYear">
<el-date-picker
v-model="formData.cycleStartYear"
type="year"
value-format="yyyy"
clearable
placeholder="请选择周期开始年度"
style="width: 100%">
</el-date-picker>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="周期结束年度" prop="cycleEndYear">
<el-date-picker
v-model="formData.cycleEndYear"
type="year"
value-format="yyyy"
clearable
placeholder="请选择周期结束年度"
style="width: 100%">
</el-date-picker>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="周期内总费用" prop="cycleTotalCost">
<el-input-number v-model="formData.cycleTotalCost" :controls="false" :min="0" :precision="0" placeholder="请输入周期内总费用" style="width: 100%"></el-input-number>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="周期允许次数" prop="cycleAllowedTimes">
<el-input-number v-model="formData.cycleAllowedTimes" :controls="false" :min="0" :precision="0" placeholder="请输入周期允许次数" style="width: 100%"></el-input-number>
</el-form-item>
</el-col>
<el-col :span="24">
<div class="tour-setting-basic-divider"></div>
</el-col>
<el-col :span="12">
<el-form-item label="允许携带家属" prop="allowFamily">
<el-switch v-model="formData.allowFamily" active-text="允许" inactive-text="不允许"></el-switch>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="填报床位信息" prop="fillBedInfo">
<el-switch v-model="formData.fillBedInfo" active-text="是" inactive-text="否"></el-switch>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="是否启用" prop="enabled">
<el-switch v-model="formData.enabled" active-text="启用" inactive-text="停用"></el-switch>
</el-form-item>
</el-col>
</el-row>
</el-tab-pane>
<el-tab-pane label="标段管理" name="lots">
<div style="text-align: right; margin-bottom: 10px;">
<el-button type="primary" size="medium" icon="el-icon-plus" @click="addLot">增加一条</el-button>
</div>
<el-table
:data="formData.lots"
border
class="vi-table"
empty-text="暂无标段"
style="width: 100%">
<el-table-column label="序号" type="index" width="70" align="center" header-align="center"></el-table-column>
<el-table-column label="标段名称" min-width="180" align="center" header-align="center">
<template slot-scope="{row}">
<el-input v-model="row.lotName" maxlength="50" placeholder="请输入标段名称"></el-input>
</template>
</el-table-column>
<el-table-column label="标段值" min-width="160" align="center" header-align="center">
<template slot-scope="{row}">
<el-input v-model="row.lotValue" maxlength="10" placeholder="请输入整数" @input="integerInput(row, 'lotValue', $event)"></el-input>
</template>
</el-table-column>
<el-table-column label="标段费用" min-width="150" align="center" header-align="center">
<template slot-scope="{row}">
<el-input v-model="row.activityCost" maxlength="10" placeholder="请输入整数" @input="integerInput(row, 'activityCost', $event)"></el-input>
</template>
</el-table-column>
<el-table-column label="允许超出报销" width="140" align="center" header-align="center">
<template slot-scope="{row}">
<el-checkbox v-model="row.allowOverReimbursement"></el-checkbox>
</template>
</el-table-column>
<el-table-column label="操作" width="90" align="center" header-align="center">
<template slot-scope="scope">
<el-button type="danger" size="mini" icon="el-icon-delete" @click="deleteLot(scope.$index, scope.row)"></el-button>
</template>
</el-table-column>
</el-table>
</el-tab-pane>
<el-tab-pane label="服务须知" name="notice">
<el-form-item prop="serviceNotice" label-width="0">
<text-editor v-model="formData.serviceNotice"></text-editor>
</el-form-item>
</el-tab-pane>
</el-tabs>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" :loading="submitLoading" @click="doSubmit">确认</el-button>
</span>
</el-dialog>
<drawer-user-scope
ref="drawerUserScope"
:group_id.sync="formData.activityGroupId"
@group_change="handleActivityGroupChange">
</drawer-user-scope>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
mixins: [initTableMixins],
data() {
// 前端先做成团人数区间校验,后端也会再兜底校验一次。
const checkGroupPeople = (rule, value, callback) => {
if (this.formData.minGroupPeople !== null && this.formData.maxGroupPeople !== null
&& this.formData.minGroupPeople > this.formData.maxGroupPeople) {
callback(new Error("最少成团人数不能大于最多成团人数"))
} else {
callback()
}
}
const checkCycleYear = (rule, value, callback) => {
const startYear = parseInt(this.formData.cycleStartYear, 10)
const endYear = parseInt(this.formData.cycleEndYear, 10)
if (!isNaN(startYear) && !isNaN(endYear) && startYear > endYear) {
callback(new Error("周期开始年度不能大于周期结束年度"))
} else {
callback()
}
}
const checkOutProvinceFixedPeople = (rule, value, callback) => {
if (this.formData.outProvinceRatioType === "固定人数"
&& (value === null || value === undefined || value === "" || value < 0)) {
callback(new Error("固定人数必须大于等于0"))
} else {
callback()
}
}
return {
title: "",
dialogVisible: false,
activeTab: "basic",
pageForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
year: "",
configName: ""
},
activityGroupList: [],
formData: {},
lotDeleteList: [],
inheritLoading: false,
formRules: {
year: [{required: true, message: "必填", trigger: ["blur", "change"]}],
configName: [{required: true, message: "必填", trigger: ["blur", "change"]}],
minGroupPeople: [{validator: checkGroupPeople, trigger: ["blur", "change"]}],
maxGroupPeople: [{validator: checkGroupPeople, trigger: ["blur", "change"]}],
outProvinceRatioType: [{required: true, message: "必填", trigger: ["blur", "change"]}],
outProvinceFixedPeople: [{validator: checkOutProvinceFixedPeople, trigger: ["blur", "change"]}],
cycleStartYear: [{validator: checkCycleYear, trigger: ["blur", "change"]}],
cycleEndYear: [{validator: checkCycleYear, trigger: ["blur", "change"]}]
}
}
},
components: {
"drawer-user-scope": httpVueLoader("/components/module/activity/DrawerUserScope.vue")
},
methods: {
getActivityGroup() {
return this.$axios.post("/platform/activity/basic/scope/getActivityUserScopeGroup").then((res) => {
const data = res.data
this.activityGroupList = (data || []).map((item) => {
return Object.assign({}, item, {
groupId: item.groupId === null || item.groupId === undefined ? "" : String(item.groupId)
})
})
})
},
openUserScope() {
if (this.$refs.drawerUserScope) {
this.$refs.drawerUserScope.groupId = this.formData.activityGroupId || ""
this.$refs.drawerUserScope.userScopeDialog = true
}
},
async handleActivityGroupChange() {
if (this.formData.activityGroupId !== null && this.formData.activityGroupId !== undefined) {
this.formData.activityGroupId = String(this.formData.activityGroupId)
}
await this.getActivityGroup()
},
resetSearch() {
this.pageForm.year = ""
this.pageForm.configName = ""
this.doSearch()
},
defaultFormData() {
return {
year: moment().format("YYYY"),
configName: "",
tourType: "",
activityGroupId: "",
sortNo: 0,
minGroupPeople: 0,
maxGroupPeople: 0,
outProvinceYears: 0,
outProvinceRatio: 0,
outProvinceRatioType: "当年报名人数",
outProvinceFixedPeople: 0,
cycleStartYear: "",
cycleEndYear: "",
cycleTotalCost: null,
cycleAllowedTimes: null,
allowFamily: false,
fillBedInfo: true,
enabled: true,
lots: [],
serviceNotice: ""
}
},
openAdd() {
this.title = "新建疗休养配置"
this.lotDeleteList = []
this.activeTab = "basic"
// 新建时给出默认年度和开关值,减少校工会管理员录入成本。
this.formData = this.defaultFormData()
this.dialogVisible = true
this.$nextTick(() => this.$refs.form && this.$refs.form.clearValidate())
},
inheritPreviousYearInfo() {
const currentYear = parseInt(this.formData.year, 10)
if (isNaN(currentYear)) {
this.$message.warning("请先选择年度")
return
}
this.inheritLoading = true
this.$axios.post(loc() + "/previousYearInfo", {year: currentYear}).then((res) => {
this.inheritLoading = false
if (res.code !== 0) {
this.$message.warning(res.msg || "未找到上一年度配置")
return
}
const previous = res.data || {}
const inheritedLots = (previous.lots || []).map(item => {
return {
lotName: item.lotName || "",
lotValue: item.lotValue || "",
activityCost: item.activityCost,
allowOverReimbursement: !!item.allowOverReimbursement
}
})
this.formData = Object.assign(this.defaultFormData(), previous, {
year: String(currentYear),
cycleStartYear: previous.cycleStartYear ? String(previous.cycleStartYear) : "",
cycleEndYear: previous.cycleEndYear ? String(previous.cycleEndYear) : "",
lots: inheritedLots
})
delete this.formData.id
delete this.formData.createdAt
delete this.formData.createdBy
delete this.formData.updatedAt
delete this.formData.updatedBy
delete this.formData.delFlag
if (!this.formData.outProvinceRatioType) {
this.$set(this.formData, "outProvinceRatioType", "当年报名人数")
}
if (this.formData.outProvinceFixedPeople === null || this.formData.outProvinceFixedPeople === undefined) {
this.$set(this.formData, "outProvinceFixedPeople", 0)
}
if (this.formData.fillBedInfo === null || this.formData.fillBedInfo === undefined) {
this.$set(this.formData, "fillBedInfo", true)
}
this.lotDeleteList = []
this.$message.success("已延用上一年配置信息")
this.$nextTick(() => this.$refs.form && this.$refs.form.clearValidate())
}).catch(() => {
this.inheritLoading = false
})
},
openEdit(row) {
this.title = "编辑疗休养配置"
this.lotDeleteList = []
this.activeTab = "basic"
this.$axios.post(loc() + "/detail", {id: row.id}).then((res) => {
if (res.code === 0) {
this.formData = Object.assign({
activityGroupId: "",
outProvinceRatioType: "当年报名人数",
outProvinceFixedPeople: 0,
cycleStartYear: "",
cycleEndYear: "",
cycleTotalCost: null,
cycleAllowedTimes: null,
allowFamily: false,
fillBedInfo: true,
enabled: true,
lots: [],
serviceNotice: ""
}, res.data || {})
this.formData.year = this.formData.year ? String(this.formData.year) : ""
if (!this.formData.outProvinceRatioType) {
this.$set(this.formData, "outProvinceRatioType", "当年报名人数")
}
if (this.formData.outProvinceFixedPeople === null || this.formData.outProvinceFixedPeople === undefined) {
this.$set(this.formData, "outProvinceFixedPeople", 0)
}
if (this.formData.fillBedInfo === null || this.formData.fillBedInfo === undefined) {
this.$set(this.formData, "fillBedInfo", true)
}
this.formData.cycleStartYear = this.formData.cycleStartYear ? String(this.formData.cycleStartYear) : ""
this.formData.cycleEndYear = this.formData.cycleEndYear ? String(this.formData.cycleEndYear) : ""
this.formData.lots = (this.formData.lots || []).map(item => Object.assign({}, item, {
allowOverReimbursement: !!item.allowOverReimbursement
}))
this.dialogVisible = true
this.$nextTick(() => this.$refs.form && this.$refs.form.clearValidate())
} else {
this.$message.warning(res.msg || "查询失败")
}
})
},
doSubmit() {
this.$refs.form.validate((valid) => {
if (!valid) return
if (!this.validateLots()) return
this.submitLoading = true
// Nutz 对子表集合按字符串化 JSON 绑定更稳定,和体检项目维护的提交方式保持一致。
const submitData = JSON.parse(JSON.stringify(this.formData))
if (submitData.outProvinceRatioType !== "固定人数") {
submitData.outProvinceFixedPeople = 0
}
submitData.lots = JSON.stringify(this.formData.lots || [])
submitData.lotDeleteList = JSON.stringify(this.lotDeleteList)
this.$axios.post(loc() + "/doSubmit", submitData).then((res) => {
this.submitLoading = false
if (res.code === 0) {
this.$message.success("保存成功")
this.dialogVisible = false
this.pageData()
} else {
this.$message.warning(res.msg || "保存失败")
}
}).catch(() => {
this.submitLoading = false
})
})
},
addLot() {
if (!this.formData.lots) {
this.$set(this.formData, "lots", [])
}
this.formData.lots.push({
lotName: "",
lotValue: "",
activityCost: null,
allowOverReimbursement: false
})
},
integerInput(row, field, value) {
const nextValue = String(value || "").replace(/[^\d]/g, "")
this.$set(row, field, nextValue)
},
outProvinceRatioTypeChange(value) {
if (value !== "固定人数") {
this.$set(this.formData, "outProvinceFixedPeople", 0)
}
this.$nextTick(() => this.$refs.form && this.$refs.form.validateField("outProvinceFixedPeople"))
},
validateLots() {
const lots = this.formData.lots || []
for (let i = 0; i < lots.length; i++) {
const row = lots[i]
const hasContent = row && (row.lotName || row.lotValue || row.activityCost !== null && row.activityCost !== undefined && row.activityCost !== "")
if (!hasContent) continue
if (!/^\d+$/.test(String(row.lotValue || ""))) {
this.$message.warning("第" + (i + 1) + "行标段值必须为整数")
return false
}
if (!/^\d+$/.test(String(row.activityCost === null || row.activityCost === undefined ? "" : row.activityCost))) {
this.$message.warning("第" + (i + 1) + "行标段费用必须为整数")
return false
}
}
return true
},
deleteLot(index, row) {
this.$confirm("确定删除该标段吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
if (row && row.id) {
this.lotDeleteList.push(row.id)
}
this.formData.lots.splice(index, 1)
})
},
doDelete(row) {
this.$confirm("确定删除【" + row.configName + "】吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
this.$axios.post(loc() + "/doDelete", {id: row.id}).then((res) => {
if (res.code === 0) {
this.$message.success("删除成功")
this.pageData()
} else {
this.$message.warning(res.msg || "删除失败")
}
})
})
},
destroyEditor() {
this.formData = {}
this.lotDeleteList = []
this.inheritLoading = false
this.activeTab = "basic"
}
},
mounted() {
this.getActivityGroup()
this.pageData()
}
})
</script>
<style>
.tour-user-scope {
display: flex;
align-items: center;
gap: 10px;
}
.tour-user-scope .el-select {
flex: 1;
}
.tour-setting-inherit {
margin-bottom: -40px;
padding-right: 96px;
position: relative;
text-align: right;
z-index: 1;
}
.tour-setting-basic-divider {
border-top: 1px dashed #dcdfe6;
margin: 2px 0 18px;
}
</style>
<!--#
}
#-->
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,338 @@
<!--#
layout("/layouts/platform.html"){
#-->
<div id="app" v-cloak>
<guava ref="guava">
<el-card shadow="never">
<search :is-search-button="false">
<search-item label="年度">
<el-date-picker
v-model="pageForm.year"
type="year"
value-format="yyyy"
placeholder="请选择年度"
style="width: 100%">
</el-date-picker>
</search-item>
<search-item label="旅行社名称">
<el-input
v-model="pageForm.agencyName"
clearable
placeholder="请输入旅行社名称"
style="width: 100%"
@keyup.enter.native="doSearch">
</el-input>
</search-item>
<search-item label="联系人">
<el-input
v-model="pageForm.contactName"
clearable
placeholder="请输入联系人"
style="width: 100%"
@keyup.enter.native="doSearch">
</el-input>
</search-item>
<search-item label="联系电话">
<el-input
v-model="pageForm.contactPhone"
clearable
placeholder="请输入联系电话"
style="width: 100%"
@keyup.enter.native="doSearch">
</el-input>
</search-item>
<div class="search-query" style="margin-left: auto;">
<el-button size="medium" type="primary" icon="el-icon-search" @click="doSearch">搜索</el-button>
<el-button size="medium" @click="resetSearch">重置</el-button>
</div>
</search>
</el-card>
<el-card class="mt10" shadow="never">
<table-tool :app="this" label="旅行社列表">
<el-button @click="openAdd" size="medium" type="primary">
<i class="el-icon-plus"></i>
新增
</el-button>
</table-tool>
<el-table
v-loading="tableLoading"
:data="tableData"
:size="tableSize"
border
@sort-change="pageOrder">
<el-table-column label="序号" type="index" :index="indexMethod" width="80" align="center" header-align="center"></el-table-column>
<el-table-column label="年度" prop="year" width="110" sortable="custom" align="center" header-align="center"></el-table-column>
<el-table-column label="旅行社编号" prop="agencyCode" width="120" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="旅行社名称" prop="agencyName" min-width="220" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="联系人" prop="contactName" width="130" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="联系电话" prop="contactPhone" width="150" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="激活状态" prop="enabled" width="160" align="center" header-align="center">
<template slot-scope="{row}">
<el-switch
v-model="row.enabled"
active-text="启用"
inactive-text="禁用"
@change="toggleEnabled(row)">
</el-switch>
</template>
</el-table-column>
<el-table-column label="操作" width="260" align="center" header-align="center">
<template slot-scope="{row}">
<el-button size="mini" type="primary" @click="openView(row)">查看</el-button>
<el-button size="mini" type="primary" @click="openEdit(row)">编辑</el-button>
<el-button size="mini" type="danger" @click="doDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
</guava>
<el-dialog
:title="title"
:visible.sync="dialogVisible"
:close-on-click-modal="false"
width="72%"
@closed="destroyForm">
<el-form :model="formData" :rules="formRules" label-width="110px" ref="form" :disabled="viewMode">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="年度" prop="year">
<el-date-picker
v-model="formData.year"
type="year"
value-format="yyyy"
placeholder="请选择年度"
style="width: 100%">
</el-date-picker>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="旅行社名称" prop="agencyName">
<el-input v-model="formData.agencyName" maxlength="30" show-word-limit placeholder="请输入旅行社名称"></el-input>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="旅行社编号" prop="agencyCode">
<el-input v-model="formData.agencyCode" maxlength="50" placeholder="请输入旅行社编号"></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="激活状态" prop="enabled">
<el-radio-group v-model="formData.enabled">
<el-radio :label="true">启用</el-radio>
<el-radio :label="false">禁用</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="联系人" prop="contactName">
<el-input v-model="formData.contactName" maxlength="10" show-word-limit placeholder="请输入联系人"></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="联系人手机" prop="contactPhone">
<el-input v-model="formData.contactPhone" maxlength="30" placeholder="请输入联系人手机"></el-input>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="邮箱" prop="email">
<el-input v-model="formData.email" maxlength="100" placeholder="请输入邮箱"></el-input>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="备注" prop="remark">
<el-input v-model="formData.remark" type="textarea" maxlength="100" :rows="3" show-word-limit placeholder="请输入备注"></el-input>
</el-form-item>
<el-form-item label="移动端缩略图" prop="mobileThumb">
<!-- 复用课程管理的封面上传逻辑:单图上传,保存URL,供移动端列表展示使用。 -->
<file-upload
style="--upload-width: 200px;--upload-height:108px"
:upload_number="1"
:upload_size="20971520"
:value.sync="formData.mobileThumb"
accept=".jpg,.jpeg,.png"
complete_result
upload_mode="image"
upload_result_category="interval">
</file-upload>
<div class="el-upload__tip">支持jpg、jpeg、png格式,大小不超过20MB。</div>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button v-if="!viewMode" type="primary" :loading="subDis" @click="doSubmit">保存</el-button>
</span>
</el-dialog>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
mixins: [initTableMixins],
data() {
const currentYear = moment().format("YYYY")
const validateMobile = (rule, value, callback) => {
const mobileReg = /^1[3-9]\d{9}$/
if (!value) {
callback(new Error("必填"))
} else if (!mobileReg.test(value)) {
callback(new Error("手机号格式不正确"))
} else {
callback()
}
}
const validateEmail = (rule, value, callback) => {
const emailReg = /^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+$/
if (!value) {
callback(new Error("必填"))
} else if (!emailReg.test(value)) {
callback(new Error("邮箱格式不正确"))
} else {
callback()
}
}
return {
title: "",
dialogVisible: false,
viewMode: false,
subDis: false,
pageForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
year: currentYear,
agencyName: "",
contactName: "",
contactPhone: ""
},
formData: {},
formRules: {
year: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
agencyName: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
agencyCode: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
contactName: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
contactPhone: [{ validator: validateMobile, trigger: ["blur", "change"] }],
email: [{ validator: validateEmail, trigger: ["blur", "change"] }]
}
}
},
methods: {
resetSearch() {
this.pageForm.year = moment().format("YYYY")
this.pageForm.agencyName = ""
this.pageForm.contactName = ""
this.pageForm.contactPhone = ""
this.doSearch()
},
emptyForm() {
return {
year: moment().format("YYYY"),
agencyName: "",
agencyCode: "",
contactName: "",
contactPhone: "",
email: "",
remark: "",
mobileThumb: "",
enabled: true
}
},
openAdd() {
this.title = "新增旅行社信息"
this.viewMode = false
this.formData = this.emptyForm()
this.dialogVisible = true
this.$nextTick(() => this.$refs.form && this.$refs.form.clearValidate())
},
openEdit(row) {
this.title = "编辑旅行社信息"
this.viewMode = false
this.loadDetail(row.id)
},
openView(row) {
this.title = "查看旅行社信息"
this.viewMode = true
this.loadDetail(row.id)
},
loadDetail(id) {
this.$axios.post(loc() + "/detail", { id }).then((res) => {
if (res.code === 0) {
this.formData = Object.assign(this.emptyForm(), res.data || {})
this.formData.year = this.formData.year ? String(this.formData.year) : ""
this.dialogVisible = true
this.$nextTick(() => this.$refs.form && this.$refs.form.clearValidate())
} else {
this.$message.warning(res.msg || "查询失败")
}
})
},
doSubmit() {
this.$refs.form.validate((valid) => {
if (!valid) return
this.subDis = true
this.$axios.post(loc() + "/doSubmit", this.formData).then((res) => {
this.subDis = false
if (res.code === 0) {
this.$message.success("保存成功")
this.dialogVisible = false
this.pageData()
} else {
this.$message.warning(res.msg || "保存失败")
}
}).catch(() => {
this.subDis = false
})
})
},
toggleEnabled(row) {
this.$axios.post(loc() + "/doSubmit", row).then((res) => {
if (res.code === 0) {
this.$message.success("状态已更新")
} else {
row.enabled = !row.enabled
this.$message.warning(res.msg || "状态更新失败")
}
}).catch(() => {
row.enabled = !row.enabled
})
},
doDelete(row) {
this.$confirm("确定删除【" + row.agencyName + "】吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
this.$axios.post(loc() + "/doDelete", { id: row.id }).then((res) => {
if (res.code === 0) {
this.$message.success("删除成功")
this.pageData()
} else {
this.$message.warning(res.msg || "删除失败")
}
})
})
},
destroyForm() {
this.formData = {}
this.viewMode = false
}
},
mounted() {
this.pageData()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,547 @@
<!--#
layout("/layouts/platform.html"){
#-->
<div id="app" v-cloak>
<guava ref="guava">
<el-card shadow="never">
<search :is-search-button="false">
<search-item label="开始年度">
<el-date-picker
v-model="pageForm.startYear"
type="year"
value-format="yyyy"
placeholder="请选择开始年度"
style="width: 100%">
</el-date-picker>
</search-item>
<search-item label="结束年度">
<el-date-picker
v-model="pageForm.endYear"
type="year"
value-format="yyyy"
placeholder="请选择结束年度"
style="width: 100%">
</el-date-picker>
</search-item>
<search-item label="姓名/工号">
<el-input
v-model="pageForm.keyword"
clearable
placeholder="请输入姓名或工号"
style="width: 100%"
@keyup.enter.native="doSearch">
</el-input>
</search-item>
<search-item label="所属工会">
<el-select v-model="pageForm.unionId" clearable filterable placeholder="请选择所属工会" style="width: 100%">
<el-option v-for="item in unionOptions" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select>
</search-item>
<search-item label="线路">
<el-select v-model="pageForm.lineId" clearable filterable placeholder="请选择线路" style="width: 100%">
<el-option v-for="item in lineOptions" :key="item.lineId" :label="item.lineName" :value="item.lineId"></el-option>
</el-select>
</search-item>
<search-item label="出行时段">
<el-select v-model="pageForm.travelPeriod" clearable filterable placeholder="请选择出行时段" style="width: 100%">
<el-option v-for="item in travelPeriodOptions" :key="item.travelPeriod" :label="item.travelPeriod" :value="item.travelPeriod"></el-option>
</el-select>
</search-item>
<search-item label="线路类型">
<el-select v-model="pageForm.lineType" clearable filterable placeholder="请选择线路类型" style="width: 100%">
<el-option v-for="item in lineTypeOptions" :key="item.lineType" :label="item.lineType" :value="item.lineType"></el-option>
</el-select>
</search-item>
<div class="search-query" style="margin-left: auto;">
<el-button size="medium" type="primary" icon="el-icon-search" @click="doSearch">搜索</el-button>
<el-button size="medium" @click="resetSearch">重置</el-button>
</div>
</search>
</el-card>
<el-card class="mt10" shadow="never">
<table-tool :app="this" label="分工会审核">
<el-radio-group class="mr5" size="small" v-model="pageForm.audit" @change="changeAudit">
<el-radio-button :label="true">已审核</el-radio-button>
<el-radio-button :label="false">未审核</el-radio-button>
</el-radio-group>
</table-tool>
<el-table
v-loading="tableLoading"
:data="tableData"
:size="tableSize"
border
@sort-change="pageOrder">
<el-table-column label="序号" type="index" :index="indexMethod" width="80" align="center" header-align="center"></el-table-column>
<el-table-column label="工号" prop="jobNo" width="130" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="姓名" prop="userName" width="120" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="所在单位" prop="unitName" min-width="180" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="报名线路" prop="lineName" min-width="220" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="出行时段" prop="travelPeriod" min-width="220" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="线路类型" prop="lineType" width="130" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="报名时间" prop="signupTime" width="170" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="携带家属" prop="familyCount" width="120" sortable="custom" align="center" header-align="center">
<template slot-scope="{row}">
<el-tag size="mini" :type="Number(row.familyCount || 0) > 0 ? 'success' : 'info'">{{ row.familyCount || 0 }}人</el-tag>
</template>
</el-table-column>
<el-table-column label="报销超出费用" prop="overCostReimbursed" width="140" align="center" header-align="center">
<template slot-scope="{row}">
<el-tag size="mini" :type="row.overCostReimbursed ? 'success' : 'info'">{{ row.overCostReimbursed ? '是' : '否' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="当前节点" prop="curTaskName" width="140" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="流程状态" prop="instanceState" width="120" sortable="custom" align="center" header-align="center">
<template slot-scope="{row}">
<enum-tag :value="row.instanceState" name="ProcessInstanceStateEnum" label_key="message" size="small"></enum-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="160" align="center" header-align="center" fixed="right">
<template slot-scope="{row}">
<el-button size="mini" type="primary" @click="openView(row)">查看</el-button>
<el-button v-if="row.taskState === 10" size="mini" type="primary" @click="openApproval(row)">审核</el-button>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
<template #edit>
<tour-approval-info ref="infoRef">
<div v-if="showApprovalForm">
<div class="process-title">
{{ formData.taskName }}
</div>
<el-form
:model="formData"
ref="formRef"
label-width="0"
label-suffix=":"
class="flow-task-form">
<el-form-item
label="审批意见"
prop="tf_opinion"
:rules="[{required:true,message:'必填',trigger:['change','blur']}]">
<user-opinion-textarea v-model="formData.tf_opinion"></user-opinion-textarea>
</el-form-item>
</el-form>
<el-row type="flex" justify="end">
<el-button @click="$refs.guava.index()" size="small">取消</el-button>
<el-button @click="handleTaskAction(6)" size="small" type="info">退回到发起人</el-button>
<el-button @click="handleTaskAction(2)" size="small" type="danger">拒绝申请</el-button>
<el-button @click="handleTaskAction(1)" size="small" type="primary">同意申请</el-button>
</el-row>
</div>
</tour-approval-info>
</template>
</guava>
</div>
<style>
#app .search .search-item {
width: calc((100% - 150px) / 4);
}
#app .search .search-query {
margin-left: auto;
}
@media screen and (max-width: 1350px) {
#app .search .search-item {
width: calc((100% - 100px) / 3);
}
}
@media screen and (max-width: 1200px) {
#app .search .search-item {
width: calc((100% - 50px) / 2);
}
}
@media screen and (max-width: 992px) {
#app .search .search-item {
width: 100%;
}
}
.tour-approval-section {
margin-bottom: 14px;
}
.tour-approval-table {
width: 100%;
}
.tour-approval-empty {
color: #909399;
}
</style>
<script nonce="${cspNonce!}">
const TOUR_APPROVAL_INFO = {
template: /*language=HTML*/ `
<div>
<div class="process-title">
报名信息
<el-link type="primary" @click="openChart">点击查看流程图</el-link>
</div>
<el-descriptions :column="3" border class="tour-approval-section">
<el-descriptions-item label="工号">{{ detail.jobNo || '' }}</el-descriptions-item>
<el-descriptions-item label="姓名">{{ detail.userName || '' }}</el-descriptions-item>
<el-descriptions-item label="性别">{{ detail.gender || '' }}</el-descriptions-item>
<el-descriptions-item label="身份证号">{{ detail.idCard || '' }}</el-descriptions-item>
<el-descriptions-item label="所在单位">{{ detail.unitName || '' }}</el-descriptions-item>
<el-descriptions-item label="所在工会">{{ detail.unionName || '' }}</el-descriptions-item>
<el-descriptions-item label="报名时间">{{ detail.signupTime || '' }}</el-descriptions-item>
<el-descriptions-item label="报名线路">{{ detail.lineName || '' }}</el-descriptions-item>
<el-descriptions-item label="出行时段">{{ detail.travelPeriod || '' }}</el-descriptions-item>
<el-descriptions-item label="线路类型">{{ detail.lineType || '' }}</el-descriptions-item>
<el-descriptions-item label="报名酒店">{{ detail.hotelName || '' }}</el-descriptions-item>
<el-descriptions-item label="旅行社">{{ detail.travelAgencyName || '' }}</el-descriptions-item>
<el-descriptions-item label="是否携带家属">
{{ familyText() }}
</el-descriptions-item>
<el-descriptions-item label="报销超出费用">{{ detail.overCostReimbursed ? '是' : '否' }}</el-descriptions-item>
<el-descriptions-item v-if="fillBedInfo" label="意向拼床人">{{ detail.intendedRoommate || '' }}</el-descriptions-item>
<el-descriptions-item v-if="fillBedInfo" label="床型">{{ detail.bedType || '' }}</el-descriptions-item>
<el-descriptions-item v-if="fillBedInfo" label="床位信息">{{ detail.bedInfo || '' }}</el-descriptions-item>
</el-descriptions>
<div v-if="isDirectFamilyLine()" class="tour-approval-section">
<div class="process-title">直系亲属线路</div>
<el-descriptions :column="3" border>
<el-descriptions-item label="亲属姓名">{{ directRelative.relativeName || '' }}</el-descriptions-item>
<el-descriptions-item label="所在单位">{{ directRelative.unitName || '' }}</el-descriptions-item>
<el-descriptions-item label="亲属关系">{{ directRelative.relationshipName || '' }}</el-descriptions-item>
<el-descriptions-item label="线路名称">{{ directRelative.lineName || '' }}</el-descriptions-item>
<el-descriptions-item label="出行开始日期">{{ directRelative.travelStartTime || '' }}</el-descriptions-item>
<el-descriptions-item label="出行结束日期">{{ directRelative.travelEndTime || '' }}</el-descriptions-item>
</el-descriptions>
</div>
<div v-if="showFamilySection()" class="tour-approval-section">
<div class="process-title">亲属信息</div>
<el-table :data="familyData" border size="mini" empty-text="暂无亲属信息" class="tour-approval-table">
<el-table-column label="序号" type="index" width="70" align="center" header-align="center"></el-table-column>
<el-table-column label="家属姓名" prop="familyName" min-width="120" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="性别" prop="gender" width="90" align="center" header-align="center"></el-table-column>
<el-table-column label="身份证号码" prop="idCard" min-width="180" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="年龄" prop="age" width="90" align="center" header-align="center"></el-table-column>
<el-table-column label="关系" prop="relationship" width="110" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column v-if="fillBedInfo" label="床型" prop="bedType" width="110" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column v-if="fillBedInfo" label="床位" prop="bedInfo" min-width="120" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column v-if="fillBedInfo" label="意向拼床人" prop="intendedRoommate" min-width="130" align="center" header-align="center" show-overflow-tooltip></el-table-column>
</el-table>
</div>
<template v-for="task in doneTasks">
<div class="mt10">
<div class="process-title">{{ task.displayName }}</div>
<el-descriptions border class="flow-task-form" :column="3" :key="task.id" v-if="task.ext.isFirstTaskNode">
<el-descriptions-item label="申请用户">
{{ task.ext.initiatorName }}({{ task.ext.initiatorAccount }})
</el-descriptions-item>
<el-descriptions-item label="申请时间">{{ task.finishTime }}</el-descriptions-item>
<el-descriptions-item label="办理结果">
<dict-tag :options="dict.type.PROCESS_TASK_SUBMIT_TYPE" :value="task.ext.submitType"></dict-tag>
</el-descriptions-item>
</el-descriptions>
<el-descriptions border class="flow-task-form" :column="3" :key="task.id" v-else>
<el-descriptions-item label="办理用户">
{{ task.taskFormData.userName }}({{ task.taskFormData.loginName }})
</el-descriptions-item>
<el-descriptions-item label="办理时间">{{ task.finishTime }}</el-descriptions-item>
<el-descriptions-item label="办理结果">
<dict-tag :options="dict.type.PROCESS_TASK_SUBMIT_TYPE" :value="task.ext.submitType"></dict-tag>
</el-descriptions-item>
<el-descriptions-item label="办理意见" :span="3">
{{ task.taskFormData.opinion || task.taskFormData.tf_opinion || '' }}
</el-descriptions-item>
</el-descriptions>
</div>
</template>
<slot></slot>
<snaker-chart ref="snakerChartRef"></snaker-chart>
</div>
`,
dicts: ["PROCESS_TASK_SUBMIT_TYPE"],
data() {
return {
row: {},
detail: {},
familyData: [],
directRelative: {},
allowFamily: false,
fillBedInfo: true,
directFamilyUnitLine: false,
doneTasks: []
}
},
methods: {
onOpen(row) {
this.row = row || {}
this.detail = {}
this.familyData = []
this.directRelative = {}
this.allowFamily = false
this.fillBedInfo = true
this.directFamilyUnitLine = false
this.doneTasks = []
this.info()
this.getDoneTasks()
},
info() {
this.$axios.post(loc() + "/detail", { id: this.row.id }).then((res) => {
if (res.code === 0) {
const data = res.data || {}
this.detail = data.ledger || {}
this.detail.travelPeriod = data.travelPeriod || this.row.travelPeriod || ""
this.familyData = data.families || []
this.directRelative = data.directRelative || {}
this.allowFamily = this.toBoolean(data.allowFamily)
this.fillBedInfo = data.fillBedInfo === undefined || data.fillBedInfo === null || this.toBoolean(data.fillBedInfo)
this.directFamilyUnitLine = this.toBoolean(data.directFamilyUnitLine)
} else {
this.$message.warning(res.msg || "查询失败")
}
})
},
toBoolean(value) {
return value === true || value === 1 || value === "1"
},
isDirectFamilyLine() {
return this.directFamilyUnitLine
|| this.toBoolean(this.detail.directFamilyUnitLine)
|| (this.directRelative && this.directRelative.id)
},
hasFamily() {
return this.toBoolean(this.detail.hasFamily) || this.familyData.length > 0
},
showFamilySection() {
return this.allowFamily && this.hasFamily() && !this.isDirectFamilyLine()
},
familyText() {
if (this.isDirectFamilyLine()) {
return "否"
}
if (!this.allowFamily) {
return "否"
}
return this.hasFamily() ? "是" : "否"
},
getDoneTasks() {
this.$axios.post("/flow/common/doneTasks", { bizId: this.row.id }).then((res) => {
if (res.code === 0) {
this.doneTasks = res.data || []
}
})
},
openChart() {
if (!this.row.instanceProcessDefineId || !this.row.instanceId) {
this.$message.warning("暂无流程图信息")
return
}
this.$refs.snakerChartRef.onOpenFull(this.row.instanceProcessDefineId, this.row.instanceId)
}
}
}
new Vue({
el: "#app",
mixins: [initTableMixins],
data() {
const currentYear = moment().format("YYYY")
return {
unionOptions: [],
lineOptions: [],
travelPeriodOptions: [],
lineTypeOptions: [],
showApprovalForm: false,
formData: {
tf_opinion: ""
},
pageForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
pageOrderName: "signupTime",
pageOrderBy: "descending",
audit: false,
startYear: currentYear,
endYear: currentYear,
keyword: "",
unionId: "",
lineId: "",
travelPeriod: "",
lineType: ""
}
}
},
methods: {
resetSearch() {
this.pageForm.startYear = moment().format("YYYY")
this.pageForm.endYear = moment().format("YYYY")
this.pageForm.keyword = ""
this.pageForm.unionId = ""
this.pageForm.lineId = ""
this.pageForm.travelPeriod = ""
this.pageForm.lineType = ""
this.loadOptions()
this.doSearch()
},
changeAudit() {
this.pageForm.pageNumber = 1
this.clearCascadeFilters()
this.loadOptions()
this.pageData()
},
clearCascadeFilters() {
this.pageForm.unionId = ""
this.pageForm.lineId = ""
this.pageForm.travelPeriod = ""
this.pageForm.lineType = ""
},
openView(row) {
this.$refs.guava.edit(() => {
this.showApprovalForm = false
this.formData = {
tf_opinion: ""
}
this.$refs.infoRef.onOpen(row)
})
},
openApproval(row) {
this.$refs.guava.edit(() => {
this.showApprovalForm = true
this.formData = {
processTaskId: row.taskId,
taskName: row.curTaskName || row.taskName,
tf_opinion: ""
}
this.$refs.infoRef.onOpen(row)
})
},
handleTaskAction(val) {
this.$refs.formRef.validate((valid) => {
if (!valid) return
this.$confirm("您确定要提交吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
const loading = createLoading()
this.$axios.post("/flow/common/executeTask", {
data: JSON.stringify({
...this.formData,
submitType: val
})
}).then((res) => {
if (res.code === 0) {
this.$refs.guava.index()
this.$message.success(res.msg || "操作成功")
this.doSearch()
} else {
this.$message.warning(res.msg || "操作失败")
}
}).finally(() => {
loading.close()
})
}).catch(() => {})
})
},
loadOptions() {
this.loadUnionOptions()
this.loadLineOptions()
this.loadTravelPeriodOptions()
this.loadLineTypeOptions()
},
baseOptionParams() {
return {
audit: this.pageForm.audit,
startYear: this.pageForm.startYear,
endYear: this.pageForm.endYear,
keyword: this.pageForm.keyword,
unionId: this.pageForm.unionId,
lineId: this.pageForm.lineId,
travelPeriod: this.pageForm.travelPeriod,
lineType: this.pageForm.lineType
}
},
loadUnionOptions() {
this.$axios.post(loc() + "/unionOptions", this.baseOptionParams()).then((res) => {
if (res.code === 0) {
this.unionOptions = res.data || []
if (this.pageForm.unionId && !this.unionOptions.some(item => item.id === this.pageForm.unionId)) {
this.pageForm.unionId = ""
}
}
})
},
loadLineOptions() {
this.$axios.post(loc() + "/lineOptions", this.baseOptionParams()).then((res) => {
if (res.code === 0) {
this.lineOptions = res.data || []
if (this.pageForm.lineId && !this.lineOptions.some(item => item.lineId === this.pageForm.lineId)) {
this.pageForm.lineId = ""
}
}
})
},
loadTravelPeriodOptions() {
this.$axios.post(loc() + "/travelPeriodOptions", this.baseOptionParams()).then((res) => {
if (res.code === 0) {
this.travelPeriodOptions = res.data || []
if (this.pageForm.travelPeriod && !this.travelPeriodOptions.some(item => item.travelPeriod === this.pageForm.travelPeriod)) {
this.pageForm.travelPeriod = ""
}
}
})
},
loadLineTypeOptions() {
this.$axios.post(loc() + "/lineTypeOptions", this.baseOptionParams()).then((res) => {
if (res.code === 0) {
this.lineTypeOptions = res.data || []
if (this.pageForm.lineType && !this.lineTypeOptions.some(item => item.lineType === this.pageForm.lineType)) {
this.pageForm.lineType = ""
}
}
})
}
},
mounted() {
this.loadOptions()
this.pageData()
},
components: {
"tour-approval-info": TOUR_APPROVAL_INFO
},
watch: {
"pageForm.startYear"() {
this.clearCascadeFilters()
this.loadOptions()
},
"pageForm.endYear"() {
this.clearCascadeFilters()
this.loadOptions()
},
"pageForm.lineId"() {
this.loadTravelPeriodOptions()
this.loadLineTypeOptions()
},
"pageForm.travelPeriod"() {
this.loadLineOptions()
this.loadLineTypeOptions()
},
"pageForm.lineType"() {
this.loadLineOptions()
this.loadTravelPeriodOptions()
},
"pageForm.unionId"() {
this.loadLineOptions()
this.loadTravelPeriodOptions()
this.loadLineTypeOptions()
}
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,609 @@
<!--#
layout("/layouts/platform.html"){
#-->
<div id="app" v-cloak>
<guava ref="guava">
<el-card shadow="never">
<search :is-search-button="false">
<search-item label="开始年度">
<el-date-picker
v-model="pageForm.startYear"
type="year"
value-format="yyyy"
placeholder="请选择开始年度"
style="width: 100%">
</el-date-picker>
</search-item>
<search-item label="结束年度">
<el-date-picker
v-model="pageForm.endYear"
type="year"
value-format="yyyy"
placeholder="请选择结束年度"
style="width: 100%">
</el-date-picker>
</search-item>
<search-item label="姓名/工号">
<el-input
v-model="pageForm.keyword"
clearable
placeholder="请输入姓名或工号"
style="width: 100%"
@keyup.enter.native="doSearch">
</el-input>
</search-item>
<search-item label="所属工会">
<el-select v-model="pageForm.unionId" clearable filterable placeholder="请选择所属工会" style="width: 100%">
<el-option v-for="item in unionOptions" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select>
</search-item>
<search-item label="线路">
<el-select v-model="pageForm.lineId" clearable filterable placeholder="请选择线路" style="width: 100%">
<el-option v-for="item in lineOptions" :key="item.lineId" :label="item.lineName" :value="item.lineId"></el-option>
</el-select>
</search-item>
<search-item label="出行时段">
<el-select v-model="pageForm.travelPeriod" clearable filterable placeholder="请选择出行时段" style="width: 100%">
<el-option v-for="item in travelPeriodOptions" :key="item.travelPeriod" :label="item.travelPeriod" :value="item.travelPeriod"></el-option>
</el-select>
</search-item>
<search-item label="线路类型">
<el-select v-model="pageForm.lineType" clearable filterable placeholder="请选择线路类型" style="width: 100%">
<el-option v-for="item in lineTypeOptions" :key="item.lineType" :label="item.lineType" :value="item.lineType"></el-option>
</el-select>
</search-item>
<div class="search-query" style="margin-left: auto;">
<el-button size="medium" type="primary" icon="el-icon-search" @click="doSearch">搜索</el-button>
<el-button size="medium" @click="resetSearch">重置</el-button>
</div>
</search>
</el-card>
<el-card class="mt10" shadow="never">
<div class="tour-ledger-toolbar">
<div class="tour-ledger-toolbar-title">
<table-tool :app="this" label="分工会台账"></table-tool>
</div>
<div class="tour-ledger-scope">
<el-button
class="tour-scope-btn"
size="medium"
:class="{'is-active': pageForm.scopeType === 'ownUnionJoined'}"
@click="setScopeType('ownUnionJoined')">
本工会参加疗休养教职工({{ summaryStats.ownUnionJoinedStaffCount || 0 }})
</el-button>
<el-button
class="tour-scope-btn"
size="medium"
:class="{'is-active': pageForm.scopeType === 'organizedLineJoined'}"
@click="setScopeType('organizedLineJoined')">
参加本工会组织线路人员(含其他工会及家属)({{ summaryStats.organizedLineJoinedTotalCount || 0 }})
</el-button>
<el-button
class="tour-scope-btn"
size="medium"
:class="{'is-active': pageForm.overCostOnly}"
@click="setOverCostOnly">
超出费用由单位承担
</el-button>
<el-button
v-if="pageForm.overCostOnly"
type="primary"
size="medium"
icon="el-icon-download"
@click="downloadOverCostSummary">
导出
</el-button>
</div>
</div>
<el-table
v-loading="tableLoading"
:data="tableData"
:size="tableSize"
border
@sort-change="pageOrder">
<el-table-column label="序号" type="index" :index="indexMethod" width="80" align="center" header-align="center"></el-table-column>
<el-table-column label="工号" prop="jobNo" width="130" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="姓名" prop="userName" width="120" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="所在单位" prop="unitName" min-width="180" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="报名线路" prop="lineName" min-width="220" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="出行时段" prop="travelPeriod" min-width="260" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="线路类型" prop="lineType" width="130" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="报名时间" prop="signupTime" width="170" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="携带家属" prop="familyCount" width="120" sortable="custom" align="center" header-align="center">
<template slot-scope="{row}">
<el-tag size="mini" :type="Number(row.familyCount || 0) > 0 ? 'success' : 'info'">{{ row.familyCount || 0 }}人</el-tag>
</template>
</el-table-column>
<el-table-column label="是否参加" prop="joined" width="120" sortable="custom" align="center" header-align="center">
<template slot-scope="{row}">
<el-tag size="mini" :type="row.joined ? 'success' : 'info'">{{ row.joined ? '是' : '否' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="报销超出费用" prop="overCostReimbursed" width="140" align="center" header-align="center">
<template slot-scope="{row}">
<el-tag size="mini" :type="row.overCostReimbursed ? 'success' : 'info'">{{ row.overCostReimbursed ? '是' : '否' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="160" align="center" header-align="center" fixed="right">
<template slot-scope="{row}">
<el-button size="mini" type="primary" @click="openView(row)">查看</el-button>
<el-button size="mini" type="danger" @click="doDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
</guava>
<el-dialog
title="台账详情"
:visible.sync="detailVisible"
:close-on-click-modal="false"
width="72%">
<div class="tour-ledger-section">
<div class="tour-ledger-title">教职工信息</div>
<el-descriptions :column="3" border size="medium">
<el-descriptions-item label="工号">{{ detail.jobNo || '' }}</el-descriptions-item>
<el-descriptions-item label="姓名">{{ detail.userName || '' }}</el-descriptions-item>
<el-descriptions-item label="性别">{{ detail.gender || '' }}</el-descriptions-item>
<el-descriptions-item label="身份证号">{{ detail.idCard || '' }}</el-descriptions-item>
<el-descriptions-item label="所在单位">{{ detail.unitName || '' }}</el-descriptions-item>
<el-descriptions-item label="所在工会">{{ detail.unionName || '' }}</el-descriptions-item>
</el-descriptions>
</div>
<div class="tour-ledger-section mt10">
<div class="tour-ledger-title">
报名信息
<el-link v-if="detailHasWorkflow()" type="primary" @click="openChart">点击查看流程图</el-link>
</div>
<el-descriptions :column="3" border size="medium">
<el-descriptions-item label="报名时间">{{ detail.signupTime || '' }}</el-descriptions-item>
<el-descriptions-item label="报名线路">{{ detail.lineName || '' }}</el-descriptions-item>
<el-descriptions-item label="线路类型">{{ detail.lineType || '' }}</el-descriptions-item>
<el-descriptions-item label="报名酒店">{{ detail.hotelName || '' }}</el-descriptions-item>
<el-descriptions-item label="报名旅行社">{{ detail.travelAgencyName || '' }}</el-descriptions-item>
<el-descriptions-item label="出行时间">{{ detail.travelPeriod || '' }}</el-descriptions-item>
<el-descriptions-item v-if="fillBedInfo" label="床型">{{ detail.bedType || '' }}</el-descriptions-item>
<el-descriptions-item v-if="fillBedInfo" label="床位信息">{{ detail.bedInfo || '' }}</el-descriptions-item>
<el-descriptions-item v-if="fillBedInfo" label="意向拼床人">{{ detail.intendedRoommate || '' }}</el-descriptions-item>
<el-descriptions-item label="是否携带家属">{{ familyText() }}</el-descriptions-item>
<el-descriptions-item label="是否参加">{{ detail.joined ? '是' : '否' }}</el-descriptions-item>
<el-descriptions-item label="是否报销">{{ detail.reimbursed ? '是' : '否' }}</el-descriptions-item>
<el-descriptions-item label="报销超出费用">{{ detail.overCostReimbursed ? '是' : '否' }}</el-descriptions-item>
</el-descriptions>
</div>
<div v-if="isDirectFamilyLine()" class="tour-ledger-section mt10">
<div class="tour-ledger-title">直系亲属线路</div>
<el-descriptions :column="3" border size="medium">
<el-descriptions-item label="亲属姓名">{{ directRelative.relativeName || '' }}</el-descriptions-item>
<el-descriptions-item label="所在单位">{{ directRelative.unitName || '' }}</el-descriptions-item>
<el-descriptions-item label="亲属关系">{{ directRelative.relationshipName || '' }}</el-descriptions-item>
<el-descriptions-item label="线路名称">{{ directRelative.lineName || '' }}</el-descriptions-item>
<el-descriptions-item label="出行开始日期">{{ directRelative.travelStartTime || '' }}</el-descriptions-item>
<el-descriptions-item label="出行结束日期">{{ directRelative.travelEndTime || '' }}</el-descriptions-item>
</el-descriptions>
</div>
<div v-if="showFamilySection()" class="tour-ledger-section mt10">
<div class="tour-ledger-title">家属信息</div>
<el-table :data="familyData" border :size="tableSize" empty-text="暂无家属信息">
<el-table-column label="序号" type="index" width="70" align="center" header-align="center"></el-table-column>
<el-table-column label="家属姓名" prop="familyName" min-width="120" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="性别" prop="gender" width="90" align="center" header-align="center"></el-table-column>
<el-table-column label="身份证号码" prop="idCard" min-width="180" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="年龄" prop="age" width="90" align="center" header-align="center"></el-table-column>
<el-table-column label="关系" prop="relationship" width="110" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column v-if="fillBedInfo" label="床型" prop="bedType" width="110" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column v-if="fillBedInfo" label="床位" prop="bedInfo" min-width="120" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column v-if="fillBedInfo" label="意向拼床人" prop="intendedRoommate" min-width="130" align="center" header-align="center" show-overflow-tooltip></el-table-column>
</el-table>
</div>
<template v-if="detailHasWorkflow()" v-for="task in doneTasks">
<div class="tour-ledger-section mt10" :key="task.id">
<div class="tour-ledger-title">{{ task.displayName }}</div>
<el-descriptions border :column="3" v-if="task.ext && task.ext.isFirstTaskNode">
<el-descriptions-item label="申请用户">
{{ task.ext.initiatorName }}({{ task.ext.initiatorAccount }})
</el-descriptions-item>
<el-descriptions-item label="申请时间">{{ task.finishTime }}</el-descriptions-item>
<el-descriptions-item label="办理结果">
<dict-tag :options="dict.type.PROCESS_TASK_SUBMIT_TYPE" :value="task.ext.submitType"></dict-tag>
</el-descriptions-item>
</el-descriptions>
<el-descriptions border :column="3" v-else>
<el-descriptions-item label="办理用户">
{{ (task.taskFormData && task.taskFormData.userName) || '' }}({{ (task.taskFormData && task.taskFormData.loginName) || '' }})
</el-descriptions-item>
<el-descriptions-item label="办理时间">{{ task.finishTime }}</el-descriptions-item>
<el-descriptions-item label="办理结果">
<dict-tag :options="dict.type.PROCESS_TASK_SUBMIT_TYPE" :value="task.ext && task.ext.submitType"></dict-tag>
</el-descriptions-item>
<el-descriptions-item label="办理意见" :span="3">
{{ (task.taskFormData && (task.taskFormData.opinion || task.taskFormData.tf_opinion)) || '' }}
</el-descriptions-item>
</el-descriptions>
</div>
</template>
<snaker-chart ref="snakerChartRef"></snaker-chart>
<span slot="footer" class="dialog-footer">
<el-button type="primary" @click="detailVisible = false">确定</el-button>
</span>
</el-dialog>
</div>
<style>
.tour-ledger-section {
padding: 0 2px;
}
.tour-ledger-title {
border-left: 4px solid #0079c2;
color: #0079c2;
font-size: 14px;
font-weight: 600;
line-height: 16px;
margin-bottom: 14px;
padding-left: 10px;
}
.tour-ledger-toolbar {
display: flex;
align-items: center;
flex-wrap: nowrap;
gap: 24px;
margin-bottom: 12px;
white-space: nowrap;
}
.tour-ledger-toolbar-title {
flex: 0 0 auto;
}
.tour-ledger-scope {
display: flex;
align-items: center;
flex: 0 0 auto;
flex-wrap: nowrap;
gap: 12px;
margin: 0;
white-space: nowrap;
}
.tour-ledger-scope .el-button {
margin: 0;
}
.tour-ledger-scope .el-button + .el-button {
margin-left: 0;
}
.tour-ledger-scope .tour-scope-btn {
background: #ecf5ff;
border-color: #b3d8ff;
border-radius: 4px;
color: #0079c2;
font-weight: 600;
height: 34px;
line-height: 1;
padding: 8px 18px;
}
.tour-ledger-scope .tour-scope-btn:hover,
.tour-ledger-scope .tour-scope-btn:focus {
background: #d9ecff;
border-color: #66b1ff;
color: #006bb0;
}
.tour-ledger-scope .tour-scope-btn.is-active {
background: #0079c2;
border-color: #0079c2;
box-shadow: 0 2px 6px rgba(0, 121, 194, 0.24);
color: #fff;
}
.tour-ledger-scope .tour-scope-btn.is-active:hover,
.tour-ledger-scope .tour-scope-btn.is-active:focus {
background: #006bb0;
border-color: #006bb0;
color: #fff;
}
</style>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
mixins: [initTableMixins],
dicts: ["PROCESS_TASK_SUBMIT_TYPE"],
data() {
const currentYear = moment().format("YYYY")
return {
detailVisible: false,
unionOptions: [],
lineOptions: [],
travelPeriodOptions: [],
lineTypeOptions: [],
summaryStats: {},
detail: {},
familyData: [],
directRelative: {},
directFamilyUnitLine: false,
fillBedInfo: true,
detailRow: {},
doneTasks: [],
pageForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
pageOrderName: "signupTime",
pageOrderBy: "descending",
startYear: currentYear,
endYear: currentYear,
keyword: "",
unionId: "",
lineId: "",
travelPeriod: "",
lineType: "",
scopeType: "",
overCostOnly: false
}
}
},
methods: {
toBoolean(value) {
return value === true || value === 1 || value === "1" || value === "true"
},
isDirectFamilyLine() {
return this.directFamilyUnitLine
|| this.toBoolean(this.detail && this.detail.directFamilyUnitLine)
|| !!(this.directRelative && this.directRelative.id)
},
detailHasWorkflow() {
return !!(this.detailRow && this.detailRow.instanceId)
},
hasFamily() {
return this.toBoolean(this.detail && this.detail.hasFamily) || this.familyData.length > 0
},
familyText() {
if (this.isDirectFamilyLine()) {
return "否"
}
if (!this.hasFamily()) {
return "否"
}
const count = Number(this.familyData.length || 0)
return count > 0 ? count + "人" : "是"
},
showFamilySection() {
return this.hasFamily() && !this.isDirectFamilyLine()
},
loadDoneTasks() {
this.$axios.post("/flow/common/doneTasks", { bizId: this.detailRow.id }).then((res) => {
if (res.code === 0) {
this.doneTasks = res.data || []
}
})
},
openChart() {
if (!this.detailRow.instanceProcessDefineId || !this.detailRow.instanceId) {
this.$message.warning("暂无流程图信息")
return
}
this.$refs.snakerChartRef.onOpenFull(this.detailRow.instanceProcessDefineId, this.detailRow.instanceId)
},
resetSearch() {
this.pageForm.startYear = moment().format("YYYY")
this.pageForm.endYear = moment().format("YYYY")
this.pageForm.keyword = ""
this.pageForm.unionId = ""
this.pageForm.lineId = ""
this.pageForm.travelPeriod = ""
this.pageForm.lineType = ""
this.pageForm.scopeType = ""
this.pageForm.overCostOnly = false
this.loadUnionOptions()
this.loadLineOptions()
this.loadTravelPeriodOptions()
this.loadLineTypeOptions()
this.loadSummaryStats()
this.doSearch()
},
openView(row) {
this.detailRow = row || {}
this.detail = {}
this.familyData = []
this.directRelative = {}
this.directFamilyUnitLine = false
this.fillBedInfo = true
this.doneTasks = []
this.$axios.post(loc() + "/detail", { id: row.id }).then((res) => {
if (res.code === 0) {
const data = res.data || {}
this.detail = data.ledger || {}
this.detail.travelPeriod = data.travelPeriod || ""
this.familyData = data.families || []
this.directRelative = data.directRelative || {}
this.directFamilyUnitLine = this.toBoolean(data.directFamilyUnitLine)
this.fillBedInfo = data.fillBedInfo === undefined || data.fillBedInfo === null || this.toBoolean(data.fillBedInfo)
this.detailVisible = true
if (this.detailHasWorkflow()) {
this.loadDoneTasks()
}
} else {
this.$message.warning(res.msg || "查询失败")
}
})
},
doDelete(row) {
this.$confirm("确定删除【" + row.userName + "】的报名台账吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
this.$axios.post(loc() + "/doDelete", { id: row.id }).then((res) => {
if (res.code === 0) {
this.$message.success("删除成功")
this.pageData()
this.loadSummaryStats()
this.loadLineOptions()
this.loadTravelPeriodOptions()
this.loadLineTypeOptions()
} else {
this.$message.warning(res.msg || "删除失败")
}
})
})
},
loadUnionOptions() {
this.$axios.post(loc() + "/unionOptions", {
startYear: this.pageForm.startYear,
endYear: this.pageForm.endYear
}).then((res) => {
if (res.code === 0) {
this.unionOptions = res.data || []
}
})
},
loadLineOptions() {
this.$axios.post(loc() + "/lineOptions", {
startYear: this.pageForm.startYear,
endYear: this.pageForm.endYear,
travelPeriod: this.pageForm.travelPeriod,
lineType: this.pageForm.lineType,
unionId: this.pageForm.unionId,
keyword: this.pageForm.keyword,
scopeType: this.pageForm.scopeType
}).then((res) => {
if (res.code === 0) {
this.lineOptions = res.data || []
if (this.pageForm.lineId && !this.lineOptions.some(item => item.lineId === this.pageForm.lineId)) {
this.pageForm.lineId = ""
}
}
})
},
loadTravelPeriodOptions() {
this.$axios.post(loc() + "/travelPeriodOptions", {
startYear: this.pageForm.startYear,
endYear: this.pageForm.endYear,
lineId: this.pageForm.lineId,
lineType: this.pageForm.lineType,
unionId: this.pageForm.unionId,
keyword: this.pageForm.keyword,
scopeType: this.pageForm.scopeType
}).then((res) => {
if (res.code === 0) {
this.travelPeriodOptions = res.data || []
if (this.pageForm.travelPeriod && !this.travelPeriodOptions.some(item => item.travelPeriod === this.pageForm.travelPeriod)) {
this.pageForm.travelPeriod = ""
}
}
})
},
loadLineTypeOptions() {
this.$axios.post(loc() + "/lineTypeOptions", {
startYear: this.pageForm.startYear,
endYear: this.pageForm.endYear,
lineId: this.pageForm.lineId,
travelPeriod: this.pageForm.travelPeriod,
unionId: this.pageForm.unionId,
keyword: this.pageForm.keyword,
scopeType: this.pageForm.scopeType
}).then((res) => {
if (res.code === 0) {
this.lineTypeOptions = res.data || []
if (this.pageForm.lineType && !this.lineTypeOptions.some(item => item.lineType === this.pageForm.lineType)) {
this.pageForm.lineType = ""
}
}
})
},
loadSummaryStats() {
this.$axios.post(loc() + "/summaryStats", {
startYear: this.pageForm.startYear,
endYear: this.pageForm.endYear,
unionId: this.pageForm.unionId,
lineId: this.pageForm.lineId,
travelPeriod: this.pageForm.travelPeriod,
lineType: this.pageForm.lineType
}).then((res) => {
if (res.code === 0) {
this.summaryStats = res.data || {}
}
})
},
setScopeType(scopeType) {
this.pageForm.scopeType = this.pageForm.scopeType === scopeType ? "" : scopeType
this.pageForm.pageNumber = 1
this.loadLineOptions()
this.loadTravelPeriodOptions()
this.loadLineTypeOptions()
this.pageData()
},
setOverCostOnly() {
this.pageForm.overCostOnly = !this.pageForm.overCostOnly
this.pageForm.pageNumber = 1
this.pageData()
},
downloadOverCostSummary() {
this.$downLoad(loc() + "/exportOverCostSummary", this.pageForm)
}
},
mounted() {
this.loadUnionOptions()
this.loadLineOptions()
this.loadTravelPeriodOptions()
this.loadLineTypeOptions()
this.loadSummaryStats()
this.pageData()
},
watch: {
"pageForm.startYear"() {
this.pageForm.lineId = ""
this.pageForm.travelPeriod = ""
this.pageForm.lineType = ""
this.pageForm.unionId = ""
this.pageForm.scopeType = ""
this.loadUnionOptions()
this.loadLineOptions()
this.loadTravelPeriodOptions()
this.loadLineTypeOptions()
this.loadSummaryStats()
},
"pageForm.endYear"() {
this.pageForm.lineId = ""
this.pageForm.travelPeriod = ""
this.pageForm.lineType = ""
this.pageForm.unionId = ""
this.pageForm.scopeType = ""
this.loadUnionOptions()
this.loadLineOptions()
this.loadTravelPeriodOptions()
this.loadLineTypeOptions()
this.loadSummaryStats()
},
"pageForm.lineId"() {
this.loadTravelPeriodOptions()
this.loadLineTypeOptions()
this.loadSummaryStats()
},
"pageForm.travelPeriod"() {
this.loadLineOptions()
this.loadLineTypeOptions()
this.loadSummaryStats()
},
"pageForm.lineType"() {
this.loadLineOptions()
this.loadTravelPeriodOptions()
this.loadSummaryStats()
},
"pageForm.unionId"() {
this.loadLineOptions()
this.loadTravelPeriodOptions()
this.loadLineTypeOptions()
this.loadSummaryStats()
}
}
})
</script>
<!--#
}
#-->
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,530 @@
<!--#
layout("/layouts/platform.html"){
#-->
<div id="app" v-cloak>
<guava ref="guava">
<template>
<el-card shadow="never">
<search @search="doSearch">
<search-item label="名称/地址">
<el-input placeholder="请输入名称或地址" clearable v-model="pageForm.searchKeyword"
@keyup.enter.native="doSearch">
</el-input>
</search-item>
<search-item label="场地类型">
<el-select v-model="pageForm.type" clearable filterable placeholder="请选择场地类型" @change="doSearch">
<el-option
v-for="item in typeOptions"
:label="item.name"
:value="item.id"
:key="item.id"
></el-option>
</el-select>
</search-item>
</search>
</el-card>
<el-card shadow="never" class="mt20">
<table-tool label="场地列表"></table-tool>
<div v-if="tableData.length > 0" class="site-card-grid">
<div v-for="row in tableData" :key="row.id" class="site-card">
<div class="site-card__media">
<el-image
v-if="row.sitePhoto"
:preview-src-list="[row.sitePhoto]"
:src="row.sitePhoto"
fit="cover"
class="site-card__image">
</el-image>
<div v-else class="site-card__image site-card__image--placeholder">
<i class="el-icon-picture-outline"></i>
<span>暂无场地照片</span>
</div>
</div>
<div class="site-card__content">
<div class="site-card__header">
<div class="site-card__title-wrap">
<div class="site-card__title" :title="row.name">{{ row.name || '--' }}</div>
<div class="site-card__meta">
<i class="el-icon-location-outline"></i>
<span :title="row.address">{{ row.address || '暂无场地地址' }}</span>
</div>
</div>
<div class="site-card__actions">
<el-tag size="mini" effect="dark" type="success">{{ row.reserveTimeTypeName || '分段预约' }}</el-tag>
<el-button size="mini" plain @click="onViewSite(row)">场地详情</el-button>
<el-button size="mini" type="primary" @click="onApply(row)">预约</el-button>
</div>
</div>
<div class="site-card__summary">
<span>场地类型:{{ row.typeName || '--' }}</span>
<span>容纳人数:{{ row.maxNum || 0 }}人</span>
<span>联系人:{{ row.contactName || '--' }}</span>
</div>
<div class="site-card__timeline">
<div class="site-card__timeline-title">今日预约情况</div>
<div class="timeline-bar">
<span
v-for="(segment, index) in row.timelineSegments || []"
:key="row.id + '-segment-' + index"
:class="['timeline-bar__segment', 'timeline-bar__segment--' + (segment.status || 'closed')]">
</span>
</div>
<div class="timeline-scale">
<span v-for="hour in hourMarks" :key="row.id + '-hour-' + hour">{{ hour }}</span>
</div>
<div class="timeline-legend">
<span class="timeline-legend__item">
<i class="timeline-legend__dot timeline-legend__dot--available"></i>未预约
</span>
<span class="timeline-legend__item">
<i class="timeline-legend__dot timeline-legend__dot--reserved"></i>已预约
</span>
<span class="timeline-legend__item">
<i class="timeline-legend__dot timeline-legend__dot--closed"></i>未开放
</span>
</div>
</div>
</div>
</div>
</div>
<el-empty v-else description="暂无可预约场地"></el-empty>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
</template>
<template #edit>
<site-apply ref="applyRef" @refresh="refresh"></site-apply>
</template>
<template #view>
<info ref="infoRef"></info>
</template>
</guava>
</div>
<script nonce="${cspNonce!}">
<!--#include('../manage/info.js'){}#-->
<!--#include('apply.js'){}#-->
const vue = new Vue({
el: "#app",
mixins: [initTableMixins],
components: {
"info": siteInfo,
"site-apply": apply,
},
data() {
return {
typeOptions: [],
hourMarks: ['0', '2', '4', '6', '8', '10', '12', '14', '16', '18', '20', '22']
}
},
methods: {
refresh() {
this.doSearch()
this.$refs.guava.index()
},
onApply(row) {
this.$refs.guava.edit(() => {
this.$refs.applyRef.onOpen(row)
})
},
onViewSite(row) {
this.$refs.guava.view(() => {
this.$refs.infoRef.onOpen(row)
})
},
querySiteType() {
this.$axios.post('/platform/siteCug/function/type/queryFunctionType').then((res) => {
this.typeOptions = res.data
})
},
pageData() {
this.$axios.post(loc() + '/pageData', this.pageForm).then((res) => {
if (res.code === 0) {
this.tableData = res.data.list
this.pageForm.totalCount = res.data.totalCount
}
})
},
},
async created() {
this.querySiteType()
this.pageData()
}
})
</script>
<style>
.site-card-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 20px;
}
.site-card {
display: flex;
min-height: 240px;
padding: 18px;
border: 1px solid #e8edf5;
border-radius: 16px;
background: linear-gradient(180deg, #ffffff 0%, #f8fbff 100%);
box-shadow: 0 10px 30px rgba(21, 66, 120, 0.06);
}
.site-card__media {
flex: 0 0 180px;
margin-right: 18px;
}
.site-card__image {
width: 180px;
height: 180px;
border-radius: 14px;
overflow: hidden;
}
.site-card__image--placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #8c9bb1;
background: linear-gradient(135deg, #eef4fb 0%, #dde8f6 100%);
font-size: 14px;
gap: 10px;
}
.site-card__image--placeholder i {
font-size: 34px;
}
.site-card__content {
display: flex;
flex: 1;
flex-direction: column;
min-width: 0;
}
.site-card__header {
display: flex;
justify-content: space-between;
gap: 16px;
}
.site-card__title-wrap {
min-width: 0;
}
.site-card__title {
overflow: hidden;
color: #1f2d3d;
font-size: 22px;
font-weight: 700;
line-height: 1.3;
text-overflow: ellipsis;
white-space: nowrap;
}
.site-card__meta {
display: flex;
align-items: center;
margin-top: 10px;
color: #7f8ea3;
font-size: 14px;
gap: 6px;
}
.site-card__meta span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.site-card__actions {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
align-items: center;
gap: 8px;
}
.site-card__summary {
display: flex;
flex-wrap: wrap;
margin-top: 16px;
color: #4f6277;
font-size: 13px;
gap: 16px;
}
.site-card__timeline {
margin-top: auto;
padding-top: 20px;
}
.site-card__timeline-title {
margin-bottom: 10px;
color: #25364d;
font-size: 14px;
font-weight: 600;
}
.timeline-bar {
display: grid;
grid-template-columns: repeat(48, minmax(0, 1fr));
gap: 2px;
align-items: center;
height: 14px;
}
.timeline-bar__segment {
height: 8px;
border-radius: 999px;
background: #e5ebf3;
}
.timeline-bar__segment--available {
background: #32b56c;
}
.timeline-bar__segment--reserved {
background: #9aa6b2;
}
.timeline-bar__segment--closed {
background: #e9edf3;
}
.timeline-scale {
display: grid;
grid-template-columns: repeat(12, minmax(0, 1fr));
margin-top: 10px;
color: #607389;
font-size: 12px;
}
.timeline-scale span {
text-align: left;
}
.timeline-scale span:last-child {
text-align: right;
}
.timeline-legend {
display: flex;
flex-wrap: wrap;
margin-top: 12px;
color: #607389;
font-size: 13px;
gap: 14px;
}
.timeline-legend__item {
display: inline-flex;
align-items: center;
gap: 6px;
}
.timeline-legend__dot {
width: 10px;
height: 10px;
border-radius: 50%;
}
.timeline-legend__dot--available {
background: #32b56c;
}
.timeline-legend__dot--reserved {
background: #9aa6b2;
}
.timeline-legend__dot--closed {
background: #e9edf3;
border: 1px solid #d6dde8;
}
.apply-visual-panel {
margin-bottom: 18px;
padding: 18px;
border: 1px solid #e8edf5;
border-radius: 14px;
background: linear-gradient(180deg, #ffffff 0%, #f6faff 100%);
}
.apply-visual-panel__toolbar {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 16px;
}
.apply-visual-panel__title {
color: #1f2d3d;
font-size: 16px;
font-weight: 700;
}
.apply-visual-panel__subtitle {
margin-top: 6px;
color: #708399;
font-size: 13px;
line-height: 1.6;
}
.apply-visual-panel__actions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 10px;
}
.apply-visual-panel__legend {
display: flex;
flex-wrap: wrap;
margin-top: 14px;
gap: 14px;
}
.apply-legend__item {
display: inline-flex;
align-items: center;
color: #607389;
font-size: 13px;
gap: 6px;
}
.apply-legend__dot {
width: 10px;
height: 10px;
border-radius: 50%;
}
.apply-legend__dot--available {
background: #32b56c;
}
.apply-legend__dot--reserved {
background: #9aa6b2;
}
.apply-legend__dot--selected {
background: #2f7df6;
}
.apply-legend__dot--closed {
background: #e9edf3;
border: 1px solid #d6dde8;
}
.apply-block-group {
margin-top: 16px;
}
.apply-block-group__title {
margin-bottom: 10px;
color: #1f2d3d;
font-size: 14px;
font-weight: 600;
}
.apply-block-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 10px;
}
.apply-block {
min-height: 40px;
padding: 8px 10px;
border: 1px solid transparent;
border-radius: 10px;
font-size: 13px;
cursor: pointer;
transition: all 0.2s ease;
}
.apply-block--available {
color: #0f5132;
background: #d9f6e5;
border-color: #87d7a7;
}
.apply-block--reserved {
color: #4b5663;
background: #e1e6ec;
border-color: #c5ced8;
cursor: not-allowed;
}
.apply-block--closed {
color: #8b97a6;
background: #f3f5f8;
border-color: #e2e8f0;
cursor: not-allowed;
}
.apply-block--selected {
color: #ffffff;
background: #2f7df6;
border-color: #2f7df6;
box-shadow: 0 6px 16px rgba(47, 125, 246, 0.22);
}
.apply-block:not(.apply-block--reserved):not(.apply-block--closed):hover {
transform: translateY(-1px);
box-shadow: 0 8px 16px rgba(30, 72, 124, 0.12);
}
.apply-visual-panel__summary {
margin-top: 16px;
padding: 12px 14px;
color: #1f4e8c;
font-size: 13px;
background: #eef5ff;
border-radius: 10px;
}
@media screen and (max-width: 1400px) {
.site-card-grid {
grid-template-columns: 1fr;
}
}
@media screen and (max-width: 768px) {
.site-card {
flex-direction: column;
}
.site-card__media {
flex: none;
margin-right: 0;
margin-bottom: 16px;
}
.site-card__image {
width: 100%;
height: 220px;
}
.site-card__header {
flex-direction: column;
}
.site-card__actions {
justify-content: flex-start;
}
.apply-visual-panel__toolbar {
flex-direction: column;
}
}
</style>
<!--#
}
#-->
@@ -0,0 +1,80 @@
const siteCugApplyInfo = {
template: /*language=HTML*/ `
<div>
<div class="process-title">
申请信息
<el-link type="primary" @click="openChart">点击查看流程图</el-link>
</div>
<el-descriptions :column="2" border>
<el-descriptions-item label="预约场地">{{ viewData.siteName }}</el-descriptions-item>
<el-descriptions-item label="预约人">{{ viewData.applyUserName }}</el-descriptions-item>
<el-descriptions-item label="预约人工号">{{ viewData.applyLoginName }}</el-descriptions-item>
<el-descriptions-item label="所属单位">{{ viewData.applyUnitName }}</el-descriptions-item>
<el-descriptions-item label="预约类型">{{ reserveTypeLabel }}</el-descriptions-item>
<el-descriptions-item label="预约主体">{{ reserveTargetName }}</el-descriptions-item>
<el-descriptions-item label="开始时间">{{ viewData.reserveStartTime }}</el-descriptions-item>
<el-descriptions-item label="结束时间">{{ viewData.reserveEndTime }}</el-descriptions-item>
<el-descriptions-item label="联系电话">{{ viewData.applyMobile }}</el-descriptions-item>
<el-descriptions-item label="预约人数">{{ viewData.joinCount }}</el-descriptions-item>
<el-descriptions-item label="预约事由" :span="2">{{ viewData.applyCause }}</el-descriptions-item>
</el-descriptions>
<template v-for="task in doneTasks">
<div class="mt10">
<div class="process-title">{{ task.displayName }}</div>
<el-descriptions border class="flow-task-form" :column="3" :key="task.id" v-if="task.ext.isFirstTaskNode">
<el-descriptions-item label="申请用户">{{ task.ext.initiatorName }}({{task.ext.initiatorAccount}})</el-descriptions-item>
<el-descriptions-item label="申请时间">{{ task.finishTime }}</el-descriptions-item>
<el-descriptions-item label="办理结果">
<dict-tag :options="dict.type.PROCESS_TASK_SUBMIT_TYPE" :value="task.ext.submitType"></dict-tag>
</el-descriptions-item>
</el-descriptions>
<el-descriptions border class="flow-task-form" :column="3" :key="task.id" v-else>
<el-descriptions-item label="办理用户">{{ task.taskFormData.userName }}({{task.taskFormData.loginName}})</el-descriptions-item>
<el-descriptions-item label="办理时间">{{ task.finishTime }}</el-descriptions-item>
<el-descriptions-item label="办理结果">
<dict-tag :options="dict.type.PROCESS_TASK_SUBMIT_TYPE" :value="task.ext.submitType"></dict-tag>
</el-descriptions-item>
<el-descriptions-item label="办理意见">{{ task.taskFormData.opinion }}</el-descriptions-item>
</el-descriptions>
</div>
</template>
<slot></slot>
<snaker-chart ref="snakerChartRef"></snaker-chart>
</div>
`,
store,
dicts: ["PROCESS_TASK_SUBMIT_TYPE"],
data() {
return {
viewData: {},
doneTasks: [],
row: null
}
},
computed: {
reserveTypeLabel() {
return this.viewData.reserveType === 'club' ? '协会预约' : '分工会预约'
},
reserveTargetName() {
return this.viewData.reserveType === 'club' ? (this.viewData.clubName || '-') : (this.viewData.applyUnionName || '-')
},
},
methods: {
onOpen(row) {
this.row = row
this.viewData = row
this.getDoneTasks()
},
getDoneTasks() {
this.$axios.post('/flow/common/doneTasks', {bizId: this.row.id}).then((res) => {
if (res.code === 0) {
this.doneTasks = res.data
}
})
},
openChart() {
this.$refs.snakerChartRef.onOpenFull(this.row.instanceProcessDefineId, this.row.instanceId)
}
},
}
@@ -0,0 +1,740 @@
const basicForm = {
template: /*language=HTML*/ `
<div>
<el-form :model="formData" ref="formRef" :rules="formRules" label-width="110px" label-position="left">
<el-tabs v-model="activeTab">
<el-tab-pane label="场地基本信息" name="basicInfo">
<el-row :gutter="20" type="flex">
<el-col :span="12">
<el-form-item label="创建人" prop="createUserName">
<el-input disabled type="text" v-model="formData.createUserName" maxlength="20" placeholder="请输入创建人"></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item prop="sortNum" label="排序编号">
<el-input v-model="formData.sortNum" placeholder="请输入排序编号" type="number"></el-input>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20" type="flex">
<el-col :span="12">
<el-form-item label="场地名称" prop="name">
<el-input type="text" v-model="formData.name" maxlength="50" placeholder="请输入场地名称"></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="场地地址" prop="address">
<el-input type="text" v-model="formData.address" maxlength="100" placeholder="请输入场地地址"></el-input>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20" type="flex">
<el-col :span="12">
<el-form-item label="联系人" prop="contactName">
<el-input type="text" v-model="formData.contactName" maxlength="20" placeholder="请输入联系人"></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="联系电话" prop="contactPhone">
<el-input type="text" v-model="formData.contactPhone" maxlength="20" placeholder="请输入联系电话"></el-input>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20" type="flex">
<el-col :span="12">
<el-form-item label="容纳人数" prop="maxNum">
<el-input v-model="formData.maxNum" placeholder="请输入容纳人数" type="number"></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="场地类型" prop="typeId">
<el-select v-model="formData.typeId" clearable filterable placeholder="请选择场地类型" style="width: 100%">
<el-option
v-for="item in typeList"
:label="item.name"
:value="item.id"
:key="item.id"
></el-option>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="场地介绍" prop="introduce">
<text-editor v-model="formData.introduce"></text-editor>
</el-form-item>
<el-form-item label="场地照片" prop="sitePhoto">
<file-upload
:value.sync="formData.sitePhoto"
:upload_number="1"
:upload_size="1024 * 1024 * 10"
accept=".jpg,.jpeg,.png"
upload_result_category="interval"
complete_result>
</file-upload>
</el-form-item>
</el-tab-pane>
<el-tab-pane label="设置场地" name="siteSetting">
<el-row :gutter="20" type="flex">
<el-col :span="12">
<el-form-item prop="state" label="开启状态">
<el-radio-group v-model="formData.state" size="medium">
<el-radio-button :label="true">开启</el-radio-button>
<el-radio-button :label="false">禁用</el-radio-button>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item prop="sexLimit" label="性别限制">
<el-radio-group v-model="formData.sexLimit" size="medium">
<el-radio-button :label="0">不限制</el-radio-button>
<el-radio-button :label="1">男</el-radio-button>
<el-radio-button :label="2">女</el-radio-button>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20" type="flex">
<el-col :span="12">
<el-form-item prop="reserveTimeType" label="预约时间段类型">
<el-radio-group v-model="formData.reserveTimeType" @change="handleReserveTimeTypeChange" size="medium">
<el-radio-button :label="1">分段预约</el-radio-button>
<el-radio-button :label="2">全天候预约</el-radio-button>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item prop="filterHolidays" label="排除节假日">
<el-radio-group v-model="formData.filterHolidays" size="medium">
<el-radio-button :label="true">是</el-radio-button>
<el-radio-button :label="false">否</el-radio-button>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="禁用时间">
<el-button type="primary" @click="openSetUpTime" size="small">点击设置禁用时间</el-button>
<span style="color: #c64120">您已设置{{ formData.notApplyTimeList ? formData.notApplyTimeList.length : 0 }}个禁用时间</span>
</el-form-item>
</el-col>
</el-row>
<el-divider content-position="left">场次信息</el-divider>
<div v-if="formData.reserveTimeType === 1">
<div class="left-span-label">分段预约:可设置一个或多个场次,预约人按拆分后的场次进行预约。</div>
<el-table :data="formData.openHours" border size="mini">
<el-table-column type="index" label="序号" align="center" header-align="center" width="80"></el-table-column>
<el-table-column prop="startTime" label="开始时间" align="center" header-align="center">
<template v-slot="{ row }">
<el-time-select
style="width: 100%"
placeholder="开始时间"
v-model="row.startTime"
@change="checkSegmentedOpenHoursOverlap"
:picker-options="{ start: '00:00', step: '00:30', end: '23:59' }">
</el-time-select>
</template>
</el-table-column>
<el-table-column prop="endTime" label="结束时间" align="center" header-align="center">
<template v-slot="{ row }">
<el-time-select
style="width: 100%"
placeholder="结束时间"
v-model="row.endTime"
@change="checkSegmentedOpenHoursOverlap"
:picker-options="{ start: '00:00', step: '00:30', end: '23:59' }">
</el-time-select>
</template>
</el-table-column>
<el-table-column prop="timeUnit" label="预约时间单位(分钟)" align="center" header-align="center">
<template v-slot="{ row }">
<el-input-number style="width: 100%" v-model="row.timeUnit" :min="1" :step="1" :precision="0" placeholder="请输入预约时间单位"></el-input-number>
</template>
</el-table-column>
<el-table-column label="操作" align="center" header-align="center" width="130">
<template slot="header">
<el-button size="mini" type="primary" icon="el-icon-plus" @click="addOpenHour">添加场次</el-button>
</template>
<template v-slot="scope">
<el-button
type="danger"
icon="el-icon-delete"
size="mini"
:disabled="formData.openHours.length <= 1"
@click="formData.openHours.splice(scope.$index, 1)">
</el-button>
</template>
</el-table-column>
</el-table>
</div>
<div v-else>
<div class="left-span-label">全天候预约:设置可预约的开始时间、结束时间和预约时间单位(分钟),预约人按设定单位选择连续时段。</div>
<el-row :gutter="20" type="flex">
<el-col :span="12">
<el-form-item label="开始时间">
<el-time-select
style="width: 100%"
placeholder="开始时间"
v-model="fullDayOpenHour.startTime"
:picker-options="{ start: '00:00', step: '00:30', end: '23:59' }">
</el-time-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="结束时间">
<el-time-select
style="width: 100%"
placeholder="结束时间"
v-model="fullDayOpenHour.endTime"
:picker-options="{ start: '00:00', step: '00:30', end: '23:59' }">
</el-time-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="预约时间单位(分钟)">
<el-input-number
style="width: 100%"
v-model="fullDayOpenHour.timeUnit"
:min="1"
:step="1"
:precision="0"
placeholder="请输入预约时间单位">
</el-input-number>
</el-form-item>
</el-col>
</el-row>
</div>
</el-tab-pane>
</el-tabs>
</el-form>
<div class="mt10" style="color: #909399; font-size: 13px;">
暂存:保存当前填写内容并留在本页继续编辑。正式提交:保存后返回列表页。
</div>
<el-row class="mt10" justify="end" type="flex">
<el-button @click="$emit('refresh')">取消</el-button>
<el-button @click="onTempSave" plain type="warning">暂存并继续编辑</el-button>
<el-button @click="onSubmit" type="primary">正式提交</el-button>
</el-row>
<el-dialog append-to-body :close-on-click-modal="false" :visible.sync="setUpTimeDialog" title="设置时间">
<div class="left-span-label">选择日期</div>
<el-date-picker
@change="setUpDateChange"
placeholder="请选择一个或多个日期"
style="width: 100%"
type="dates"
v-model="formData.setUpDate"
value-format="yyyy-MM-dd">
</el-date-picker>
<div class="left-span-label mt20">设置禁用时间</div>
<el-row>
<el-time-select
:picker-options="{
start: '00:00',
step: '00:05',
end: '23:55'
}"
placeholder="开始时间"
v-model="timeOneKeySet.startTime">
</el-time-select>
<el-time-select
:picker-options="{
start: '00:00',
step: '00:05',
end: '23:55'
}"
placeholder="结束时间"
v-model="timeOneKeySet.endTime">
</el-time-select>
<el-button @click="oneKeySetStartEndTime" type="primary">一键设置开始/结束时间</el-button>
</el-row>
<el-table :data="formData.notApplyTimeList" class="mt10" max-height="520px" size="mini" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55"></el-table-column>
<el-table-column label="日期" width="200">
<template v-slot="{row}">
<i class="el-icon-time"></i>
{{$moment(row.date).format('YYYY-MM-DD')}}
</template>
</el-table-column>
<el-table-column label="开始时间">
<template v-slot="{row}">
<el-time-select
size="mini"
:picker-options="{
start: '00:00',
step: '00:05',
end: '23:55'
}"
v-model="row.startTime">
</el-time-select>
</template>
</el-table-column>
<el-table-column label="结束时间">
<template v-slot="{row}">
<el-time-select
size="mini"
:picker-options="{
start: '00:00',
step: '00:05',
end: '23:55'
}"
v-model="row.endTime">
</el-time-select>
</template>
</el-table-column>
<el-table-column label="操作" width="200">
<template v-slot="{row, $index}">
<el-button
size="mini"
@click="formData.notApplyTimeList.splice($index,0,{date:row.date,startTime:'',endTime:''})"
type="primary">
新增同天时段
</el-button>
<el-button size="mini" @click="removeSetUpTableRow(row,$index)" type="danger">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-row class="mt20" justify="end" type="flex">
<el-button @click="setUpTimeDialog = false">取消</el-button>
<el-button @click="doConfirmSetUpCourse" type="primary">确定</el-button>
</el-row>
</el-dialog>
</div>
`,
props: {
typeList: {
type: Array,
required: false,
default: [],
}
},
store,
data() {
return {
activeTab: 'basicInfo',
segmentedOpenHoursCache: [{ startTime: '', endTime: '', timeUnit: 30 }],
fullDayOpenHourCache: { startTime: '', endTime: '', timeUnit: 30 },
formData: {
state: true,
sexLimit: 0,
filterHolidays: false,
reserveTimeType: 1,
openHours: [{ startTime: '', endTime: '', timeUnit: 30 }],
segmentedOpenHours: [{ startTime: '', endTime: '', timeUnit: 30 }],
fullDayOpenHour: { startTime: '', endTime: '', timeUnit: 30 },
notApplyTimeList: [],
createUserName: this.$store.state.user.username,
createUserId: this.$store.state.user.id,
},
formRules: {
createUserName: [{required: true, message: '必填', trigger: ['blur', 'change']}],
sortNum: [{required: true, message: '必填', trigger: ['blur', 'change']}],
name: [{required: true, message: '必填', trigger: ['blur', 'change']}],
address: [{required: true, message: '必填', trigger: ['blur', 'change']}],
contactName: [{required: true, message: '必填', trigger: ['blur', 'change']}],
contactPhone: [{required: true, message: '必填', trigger: ['blur', 'change']}],
maxNum: [{required: true, message: '必填', trigger: ['blur', 'change']}],
typeId: [{required: true, message: '必填', trigger: ['blur', 'change']}],
sexLimit: [{required: true, message: '必填', trigger: ['blur', 'change']}],
filterHolidays: [{required: true, message: '必填', trigger: ['blur', 'change']}],
state: [{required: true, message: '必填', trigger: ['blur', 'change']}],
reserveTimeType: [{required: true, message: '必填', trigger: ['blur', 'change']}],
},
setUpTimeDialog: false,
timeOneKeySet: {
startTime: "",
endTime: ""
},
multipleSelection: [],
}
},
computed: {
fullDayOpenHour() {
if (!Array.isArray(this.formData.openHours)) {
this.$set(this.formData, 'openHours', [])
}
if (this.formData.openHours.length === 0) {
this.formData.openHours.push({ startTime: '', endTime: '', timeUnit: 30 })
}
if (!this.formData.openHours[0].timeUnit || this.formData.openHours[0].timeUnit <= 0) {
this.$set(this.formData.openHours[0], 'timeUnit', 30)
}
return this.formData.openHours[0]
}
},
methods: {
normalizeTimeUnit(value, defaultValue = 30) {
const num = Number(value)
if (!Number.isFinite(num) || num <= 0) {
return defaultValue
}
return Math.round(num)
},
syncOpenHoursCache() {
if (this.formData.reserveTimeType === 1) {
this.segmentedOpenHoursCache = Array.isArray(this.formData.openHours) && this.formData.openHours.length > 0
? clone(this.formData.openHours).map(item => ({
startTime: item.startTime || '',
endTime: item.endTime || '',
timeUnit: this.normalizeTimeUnit(item.timeUnit),
}))
: [{ startTime: '', endTime: '', timeUnit: 30 }]
this.formData.segmentedOpenHours = clone(this.segmentedOpenHoursCache)
} else {
const first = (this.formData.openHours && this.formData.openHours[0]) || {}
this.fullDayOpenHourCache = {
startTime: first.startTime || '',
endTime: first.endTime || '',
timeUnit: this.normalizeTimeUnit(first.timeUnit),
}
this.formData.fullDayOpenHour = clone(this.fullDayOpenHourCache)
}
},
buildSubmitFormData() {
this.syncOpenHoursCache()
const submitData = clone(this.formData)
submitData.segmentedOpenHours = clone(this.segmentedOpenHoursCache || [])
submitData.fullDayOpenHour = clone(this.fullDayOpenHourCache || { startTime: '', endTime: '', timeUnit: 30 })
if (submitData.reserveTimeType === 1) {
submitData.openHours = clone(this.segmentedOpenHoursCache || [])
} else {
submitData.openHours = [clone(this.fullDayOpenHourCache || { startTime: '', endTime: '', timeUnit: 60 })]
}
return submitData
},
initOpenHoursCache() {
const segmentedOpenHours = Array.isArray(this.formData.segmentedOpenHours) && this.formData.segmentedOpenHours.length > 0
? clone(this.formData.segmentedOpenHours)
: (Array.isArray(this.formData.openHours) ? clone(this.formData.openHours) : [])
const fullDayOpenHour = this.formData.fullDayOpenHour && Object.keys(this.formData.fullDayOpenHour).length > 0
? clone(this.formData.fullDayOpenHour)
: ((Array.isArray(this.formData.openHours) && this.formData.openHours.length > 0) ? clone(this.formData.openHours[0]) : {})
if (this.formData.reserveTimeType === 2) {
this.fullDayOpenHourCache = {
startTime: fullDayOpenHour.startTime || '',
endTime: fullDayOpenHour.endTime || '',
timeUnit: this.normalizeTimeUnit(fullDayOpenHour.timeUnit),
}
this.segmentedOpenHoursCache = segmentedOpenHours && segmentedOpenHours.length > 0
? segmentedOpenHours.map(item => ({
startTime: item.startTime || '',
endTime: item.endTime || '',
timeUnit: this.normalizeTimeUnit(item.timeUnit),
}))
: [{ startTime: '', endTime: '', timeUnit: 30 }]
this.formData.openHours = [clone(this.fullDayOpenHourCache)]
} else {
this.segmentedOpenHoursCache = segmentedOpenHours.length > 0
? segmentedOpenHours.map(item => ({
startTime: item.startTime || '',
endTime: item.endTime || '',
timeUnit: this.normalizeTimeUnit(item.timeUnit),
}))
: [{ startTime: '', endTime: '', timeUnit: 30 }]
const first = this.segmentedOpenHoursCache[0] || {}
this.fullDayOpenHourCache = {
startTime: fullDayOpenHour.startTime || first.startTime || '',
endTime: fullDayOpenHour.endTime || first.endTime || '',
timeUnit: this.normalizeTimeUnit(fullDayOpenHour.timeUnit || first.timeUnit),
}
this.formData.openHours = clone(this.segmentedOpenHoursCache)
}
this.formData.segmentedOpenHours = clone(this.segmentedOpenHoursCache)
this.formData.fullDayOpenHour = clone(this.fullDayOpenHourCache)
},
addOpenHour() {
if (!Array.isArray(this.formData.openHours)) {
this.$set(this.formData, 'openHours', [])
}
this.formData.openHours.push({ startTime: '', endTime: '', timeUnit: 30 })
this.segmentedOpenHoursCache = clone(this.formData.openHours)
},
hasSegmentedOpenHoursOverlap(openHours) {
const validHours = (openHours || [])
.map((item, index) => ({ ...item, index }))
.filter(item => item.startTime && item.endTime && item.startTime < item.endTime)
.sort((a, b) => a.startTime.localeCompare(b.startTime))
for (let i = 1; i < validHours.length; i++) {
const prev = validHours[i - 1]
const current = validHours[i]
if (current.startTime < prev.endTime) {
return {
overlap: true,
prevIndex: prev.index,
currentIndex: current.index,
}
}
}
return { overlap: false }
},
checkSegmentedOpenHoursOverlap() {
if (this.formData.reserveTimeType !== 1) {
return false
}
const result = this.hasSegmentedOpenHoursOverlap(this.formData.openHours)
if (result.overlap) {
this.$message.warning('第' + (result.prevIndex + 1) + '条和第' + (result.currentIndex + 1) + '条场次时间有重叠,请调整后再保存')
return true
}
return false
},
handleReserveTimeTypeChange(val) {
if (!Array.isArray(this.formData.openHours)) {
this.$set(this.formData, 'openHours', [])
}
if (val === 1) {
const currentFullDay = this.formData.openHours[0] || this.fullDayOpenHourCache || {}
this.fullDayOpenHourCache = {
startTime: currentFullDay.startTime || '',
endTime: currentFullDay.endTime || '',
timeUnit: this.normalizeTimeUnit(currentFullDay.timeUnit),
}
if (!Array.isArray(this.segmentedOpenHoursCache) || this.segmentedOpenHoursCache.length === 0) {
this.segmentedOpenHoursCache = [{
startTime: this.fullDayOpenHourCache.startTime || '',
endTime: this.fullDayOpenHourCache.endTime || '',
timeUnit: this.normalizeTimeUnit(this.fullDayOpenHourCache.timeUnit),
}]
}
this.formData.openHours = clone(this.segmentedOpenHoursCache)
} else if (val === 2) {
this.segmentedOpenHoursCache = Array.isArray(this.formData.openHours) && this.formData.openHours.length > 0
? clone(this.formData.openHours)
: this.segmentedOpenHoursCache
const first = this.fullDayOpenHourCache && (this.fullDayOpenHourCache.startTime || this.fullDayOpenHourCache.endTime)
? this.fullDayOpenHourCache
: (this.segmentedOpenHoursCache[0] || {})
this.fullDayOpenHourCache = {
startTime: first.startTime || '',
endTime: first.endTime || '',
timeUnit: this.normalizeTimeUnit(first.timeUnit || this.fullDayOpenHourCache.timeUnit),
}
this.formData.openHours = [clone(this.fullDayOpenHourCache)]
}
},
validateOpenHours() {
const openHours = this.formData.openHours || []
if (this.formData.reserveTimeType === 1) {
if (openHours.length === 0) {
this.$message.warning('请至少添加一条场次信息')
return false
}
for (let i = 0; i < openHours.length; i++) {
const item = openHours[i] || {}
if (!item.startTime) {
this.$message.warning('第' + (i + 1) + '条场次信息中,开始时间必填')
return false
}
if (!item.endTime) {
this.$message.warning('第' + (i + 1) + '条场次信息中,结束时间必填')
return false
}
if (item.startTime >= item.endTime) {
this.$message.warning('第' + (i + 1) + '条场次信息中,开始时间必须早于结束时间')
return false
}
if (!item.timeUnit || item.timeUnit <= 0) {
this.$message.warning('第' + (i + 1) + '条场次信息中,预约时间单位必须大于0分钟')
return false
}
}
const overlapResult = this.hasSegmentedOpenHoursOverlap(openHours)
if (overlapResult.overlap) {
this.$message.warning('第' + (overlapResult.prevIndex + 1) + '条和第' + (overlapResult.currentIndex + 1) + '条场次时间有重叠,请调整后再保存')
return false
}
} else {
const item = openHours[0] || {}
if (!item.startTime || !item.endTime) {
this.$message.warning('请设置全天候预约的开始时间和结束时间')
return false
}
if (item.startTime >= item.endTime) {
this.$message.warning('全天候预约的开始时间必须早于结束时间')
return false
}
if (!item.timeUnit || item.timeUnit <= 0) {
this.$message.warning('全天候预约的预约时间单位必须大于0分钟')
return false
}
}
return true
},
doConfirmSetUpCourse() {
const timeList = this.formData.notApplyTimeList
if (timeList && timeList.length > 0) {
const valid = timeList.every(v => v.date && v.startTime && v.endTime && (v.startTime < v.endTime))
if (!valid) {
this.$message.warning("时间不完整或者有误")
return
}
}
this.setUpTimeDialog = false
},
removeSetUpTableRow(row, index) {
this.formData.notApplyTimeList.splice(index, 1)
const dateArray = this.formData.notApplyTimeList.map(v => this.$moment(v.date).format("YYYY-MM-DD"))
const setUpDate = this.formData.setUpDate || []
this.formData.setUpDate = setUpDate.filter(v => {
return dateArray.includes(this.$moment(v).format("YYYY-MM-DD"))
})
},
handleSelectionChange(val) {
this.multipleSelection = val
},
oneKeySetStartEndTime() {
if (this.multipleSelection.length === 0) {
this.$message.warning('请在下面表格多选框中选择需要一键设置的时间')
return
}
const { startTime, endTime } = this.timeOneKeySet
this.formData.notApplyTimeList.forEach(v => {
const selected = this.multipleSelection.find(o => o.date === v.date && o.startTime === v.startTime && o.endTime === v.endTime)
if (selected) {
this.$set(v, "startTime", startTime)
this.$set(v, "endTime", endTime)
}
})
this.$forceUpdate()
},
setUpDateChange(val) {
if (!val) {
this.formData.notApplyTimeList = []
return
}
if (this.formData.notApplyTimeList === undefined) {
this.$set(this.formData, "notApplyTimeList", [])
}
const dateSet = new Set(this.formData.notApplyTimeList.map(v => this.$moment(v.date).format("YYYY-MM-DD")))
val.forEach(v => {
if (!dateSet.has(v)) {
this.formData.notApplyTimeList.push({
date: v, startTime: "", endTime: ""
})
}
})
this.formData.notApplyTimeList = this.formData.notApplyTimeList.filter(v => {
return val.includes(this.$moment(v.date).format("YYYY-MM-DD"))
})
this.formData.notApplyTimeList.sort((a, b) => {
return Date.parse(a.date) - Date.parse(b.date)
})
},
openSetUpTime() {
if (this.formData.notApplyTimeList) {
this.$set(this.formData, 'setUpDate', this.formData.notApplyTimeList.map(o => o.date))
}
this.activeTab = 'siteSetting'
this.setUpTimeDialog = true
},
onOpen(row) {
this.activeTab = 'basicInfo'
if (row && row.id) {
this.formData = clone(row)
if (!Array.isArray(this.formData.notApplyTimeList)) {
this.$set(this.formData, 'notApplyTimeList', [])
}
if (!this.formData.reserveTimeType) {
this.$set(this.formData, 'reserveTimeType', 1)
}
if (!Array.isArray(this.formData.openHours) || this.formData.openHours.length === 0) {
this.$set(this.formData, 'openHours', this.formData.reserveTimeType === 2 ? [{ startTime: '', endTime: '', timeUnit: 30 }] : [{ startTime: '', endTime: '', timeUnit: 30 }])
}
if (!Array.isArray(this.formData.segmentedOpenHours) || this.formData.segmentedOpenHours.length === 0) {
this.$set(this.formData, 'segmentedOpenHours', [{ startTime: '', endTime: '', timeUnit: 30 }])
}
if (!this.formData.fullDayOpenHour) {
this.$set(this.formData, 'fullDayOpenHour', { startTime: '', endTime: '', timeUnit: 30 })
}
this.initOpenHoursCache()
} else {
this.formData = {
state: true,
sexLimit: 0,
filterHolidays: false,
reserveTimeType: 1,
openHours: [{ startTime: '', endTime: '', timeUnit: 30 }],
segmentedOpenHours: [{ startTime: '', endTime: '', timeUnit: 30 }],
fullDayOpenHour: { startTime: '', endTime: '', timeUnit: 30 },
notApplyTimeList: [],
createUserName: this.$store.state.user.username,
createUserId: this.$store.state.user.id,
}
this.segmentedOpenHoursCache = [{ startTime: '', endTime: '', timeUnit: 30 }]
this.fullDayOpenHourCache = { startTime: '', endTime: '', timeUnit: 30 }
}
},
async doSave(stayOnPage) {
if (!this.validateOpenHours()) {
this.activeTab = 'siteSetting'
return
}
const submitData = this.buildSubmitFormData()
const resp = await this.$axios.post("/platform/siteCug/manage/submit", {data: JSON.stringify(submitData)})
if (resp.code === 0) {
this.$message.success(stayOnPage ? '暂存成功,您可以继续编辑当前内容' : '正式提交成功,已返回列表页')
if (resp.data) {
this.formData = clone(resp.data)
if (!Array.isArray(this.formData.notApplyTimeList)) {
this.$set(this.formData, 'notApplyTimeList', [])
}
if (!this.formData.reserveTimeType) {
this.$set(this.formData, 'reserveTimeType', 1)
}
if (!Array.isArray(this.formData.openHours) || this.formData.openHours.length === 0) {
this.$set(this.formData, 'openHours', this.formData.reserveTimeType === 2 ? [{ startTime: '', endTime: '', timeUnit: 30 }] : [{ startTime: '', endTime: '', timeUnit: 30 }])
}
if (!Array.isArray(this.formData.segmentedOpenHours) || this.formData.segmentedOpenHours.length === 0) {
this.$set(this.formData, 'segmentedOpenHours', [{ startTime: '', endTime: '', timeUnit: 30 }])
}
if (!this.formData.fullDayOpenHour) {
this.$set(this.formData, 'fullDayOpenHour', { startTime: '', endTime: '', timeUnit: 30 })
}
this.initOpenHoursCache()
}
if (!stayOnPage) {
this.$emit('refresh')
}
} else {
this.$message.warning(resp.msg)
}
},
onTempSave() {
this.$refs.formRef.validate(async (valid) => {
if (valid) {
this.$confirm("确认暂存当前内容,并继续留在本页编辑吗?", "暂存确认", {
confirmButtonText: "确认暂存",
cancelButtonText: "取消",
type: "warning"
}).then(async () => {
await this.doSave(true)
}).catch(() => {})
}
})
},
onSubmit() {
this.$refs.formRef.validate((valid) => {
if (valid) {
this.$confirm("确认正式提交当前内容吗?提交成功后将返回列表页。", "正式提交确认", {
confirmButtonText: "确认提交",
cancelButtonText: "取消",
type: "primary"
}).then(async () => {
await this.doSave(false)
})
}
})
},
},
style: /*language=CSS*/ `
`
};
@@ -0,0 +1,184 @@
<!--#
layout("/layouts/platform.html"){
#-->
<style>
</style>
<div id="app" v-cloak>
<guava ref="guava">
<template>
<el-card shadow="never">
<search @search="doSearch">
<search-item label="名称/地址">
<el-input placeholder="请输入名称或地址" clearable v-model="pageForm.searchKeyword"
@keyup.enter.native="doSearch">
</el-input>
</search-item>
<search-item label="场地类型">
<el-select v-model="pageForm.type" clearable filterable placeholder="请选择场地类型" @change="doSearch">
<el-option
v-for="item in typeOptions"
:label="item.name"
:value="item.id"
:key="item.id"
></el-option>
</el-select>
</search-item>
</search>
</el-card>
<el-card shadow="never" class="mt20">
<table-tool label="场地列表">
<el-button type="primary" size="small" @click="onAdd">
<i class="ti-plus"></i>
新增场地
</el-button>
</table-tool>
<el-table :data="tableData" @sort-change="pageOrder" :size="tableSize">
<el-table-column label="序号" type="index" :index="indexMethod" width="60"></el-table-column>
<el-table-column
v-for="column in tableColumns"
:key="column.prop"
:label="column.label"
:prop="column.prop"
:width="column.width"
:sortable="column.sortable"
align="center"
header-align="center"
show-overflow-tooltip
>
<template v-slot="{ row }" v-if="column.prop === 'state'">
<el-switch
@change="switchChange(row)"
v-model="row.state"
active-color="#13ce66"
inactive-color="#ff4949">
</el-switch>
</template>
<template v-slot="{ row }" v-else-if="column.prop === 'reserveTimeType'">
<span v-if="row.reserveTimeType === 2">全天候预约</span>
<span v-else>分段预约</span>
</template>
<template v-slot="{ row }" v-else-if="column.prop === 'sexLimit'">
<span v-if="row.sexLimit === 0">不限制</span>
<span v-else-if="row.sexLimit === 1">男</span>
<span v-else-if="row.sexLimit === 2">女</span>
<span v-else>--</span>
</template>
</el-table-column>
<el-table-column label="操作" width="230">
<template v-slot="{ row }">
<el-button @click="onView(row)" size="mini" type="primary">查看</el-button>
<el-button @click="onEdit(row)" size="mini" type="primary">编辑</el-button>
<el-button @click="onDelete(row)" size="mini" type="danger">删除</el-button>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
</template>
<template #edit>
<basic-form :type-list="typeOptions" ref="basicFormRef" @refresh="refresh"></basic-form>
</template>
<template #view>
<info ref="infoRef"></info>
</template>
</guava>
</div>
<script nonce="${cspNonce!}">
<!--#include('basicForm.js'){}#-->
<!--#include('info.js'){}#-->
const vue = new Vue({
el: "#app",
mixins: [initTableMixins],
components: {
"basic-form": basicForm,
"info": siteInfo,
},
data() {
return {
typeOptions: [],
tableColumns: [
{prop: 'createUserName', label: '创建人', sortable: 'custom'},
{prop: 'name', label: '场地名称', sortable: 'custom'},
{prop: 'address', label: '场地地址', sortable: 'custom'},
{prop: 'contactName', label: '联系人', sortable: 'custom'},
{prop: 'contactPhone', label: '联系电话', sortable: 'custom'},
{prop: 'maxNum', label: '容纳人数', sortable: 'custom'},
{prop: 'typeName', label: '场地类型'},
{prop: 'reserveTimeType', label: '预约时间段类型', sortable: 'custom'},
{prop: 'sexLimit', label: '性别限制', sortable: 'custom'},
{prop: 'state', label: '开启状态', sortable: 'custom'},
],
}
},
methods: {
refresh() {
this.doSearch()
this.$refs.guava.index()
},
onAdd() {
this.$refs.guava.edit(() => {
this.$refs.basicFormRef.onOpen()
})
},
onEdit(row) {
this.$refs.guava.edit(() => {
this.$refs.basicFormRef.onOpen(row)
})
},
onView(row) {
this.$refs.guava.view(() => {
this.$refs.infoRef.onOpen(row)
})
},
onDelete(row) {
this.$confirm("您确定要删除吗? 是否继续?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
})
.then(() => {
this.$axios.post("/platform/siteCug/manage/delete", { id: row.id }).then((res) => {
if (res.code === 0) {
this.$message.success(res.msg)
this.pageData()
}
})
})
.catch(() => {})
},
switchChange(row) {
this.$axios.post("/platform/siteCug/manage/submit", row).then((res) => {
if (res.code === 0) {
this.$message.success(res.msg)
this.pageData()
}
})
},
querySiteType() {
this.$axios.post("/platform/siteCug/function/type/queryFunctionType").then((res) => {
this.typeOptions = res.data
})
},
pageData() {
this.$axios.post(loc() + "/pageData", this.pageForm).then((res) => {
if (res.code === 0) {
this.tableData = res.data.list
this.pageForm.totalCount = res.data.totalCount
}
})
},
},
async created() {
this.querySiteType()
this.pageData()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,107 @@
const siteInfo = {
template: /*language=HTML*/ `
<div>
<el-tabs v-model="activeTab">
<el-tab-pane label="场地基本信息" name="basicInfo">
<el-descriptions :column="2" border>
<el-descriptions-item label="创建人">{{ viewData.createUserName }}</el-descriptions-item>
<el-descriptions-item label="排序编号">{{ viewData.sortNum }}</el-descriptions-item>
<el-descriptions-item label="场地名称">{{ viewData.name }}</el-descriptions-item>
<el-descriptions-item label="场地地址">{{ viewData.address }}</el-descriptions-item>
<el-descriptions-item label="联系人">{{ viewData.contactName }}</el-descriptions-item>
<el-descriptions-item label="联系电话">{{ viewData.contactPhone }}</el-descriptions-item>
<el-descriptions-item label="容纳人数">{{ viewData.maxNum }}</el-descriptions-item>
<el-descriptions-item label="场地类型">{{ viewData.typeName }}</el-descriptions-item>
<el-descriptions-item label="场地介绍" :span="2">
<div v-if="viewData.introduce" v-html="viewData.introduce"></div>
<div v-else>暂无场地介绍</div>
</el-descriptions-item>
<el-descriptions-item label="场地照片" :span="2">
<el-image
v-if="viewData.sitePhoto"
:preview-src-list="[viewData.sitePhoto]"
:src="viewData.sitePhoto"
fit="cover"
style="width: 160px; height: 160px; border-radius: 4px;">
</el-image>
<div v-else>暂无场地照片</div>
</el-descriptions-item>
</el-descriptions>
</el-tab-pane>
<el-tab-pane label="设置场地" name="siteSetting">
<el-descriptions :column="2" border>
<el-descriptions-item label="开启状态">
<span v-if="viewData.state">开启</span>
<span v-else>禁用</span>
</el-descriptions-item>
<el-descriptions-item label="性别限制">
<span v-if="viewData.sexLimit === 0">不限制</span>
<span v-else-if="viewData.sexLimit === 1">男</span>
<span v-else-if="viewData.sexLimit === 2">女</span>
<span v-else>--</span>
</el-descriptions-item>
<el-descriptions-item label="排除节假日">
<span v-if="viewData.filterHolidays">是</span>
<span v-else>否</span>
</el-descriptions-item>
<el-descriptions-item label="预约时间段类型">
<span v-if="viewData.reserveTimeType === 1">分段预约</span>
<span v-else-if="viewData.reserveTimeType === 2">全天候预约</span>
<span v-else>--</span>
</el-descriptions-item>
<el-descriptions-item label="禁用时间" :span="2">
<el-table v-if="viewData.notApplyTimeList && viewData.notApplyTimeList.length > 0"
:data="viewData.notApplyTimeList" max-height="300" size="mini">
<el-table-column prop="date" label="日期"></el-table-column>
<el-table-column prop="startTime" label="开始时间"></el-table-column>
<el-table-column prop="endTime" label="结束时间"></el-table-column>
</el-table>
<span v-else>暂无禁用时间</span>
</el-descriptions-item>
<el-descriptions-item label="场次信息" :span="2">
<div v-if="viewData.reserveTimeType === 1">
<el-table v-if="viewData.openHours && viewData.openHours.length > 0"
:data="viewData.openHours" max-height="300" size="mini">
<el-table-column type="index" label="序号" width="80"></el-table-column>
<el-table-column prop="startTime" label="开始时间"></el-table-column>
<el-table-column prop="endTime" label="结束时间"></el-table-column>
<el-table-column prop="timeUnit" label="预约时间单位(分钟)"></el-table-column>
</el-table>
<span v-else>暂无场次信息</span>
</div>
<div v-else-if="viewData.reserveTimeType === 2">
<div v-if="viewData.openHours && viewData.openHours.length > 0">
<div><span>开始时间:</span><span>{{ viewData.openHours[0].startTime || '--' }}</span></div>
<div><span>结束时间:</span><span>{{ viewData.openHours[0].endTime || '--' }}</span></div>
<div><span>预约时间单位(分钟):</span><span>{{ viewData.openHours[0].timeUnit || '--' }}</span></div>
</div>
<span v-else>暂无场次信息</span>
</div>
<span v-else>暂无场次信息</span>
</el-descriptions-item>
</el-descriptions>
</el-tab-pane>
</el-tabs>
</div>
`,
data() {
return {
activeTab: 'basicInfo',
viewData: {},
}
},
methods: {
onOpen(row) {
this.activeTab = 'basicInfo'
this.viewData = row
},
},
style: /*language=CSS*/ `
.el-descriptions-item__label {
width: 200px;
min-width: 200px;
max-width: 200px;
}
`
};
@@ -0,0 +1,250 @@
<!--#
layout("/layouts/platform.html"){
#-->
<style>
.mine-batch-toggle {
padding: 0;
color: #409EFF;
font-size: 16px;
}
.mine-batch-empty {
display: inline-block;
width: 16px;
height: 16px;
}
.mine-batch-child-row {
background: #fafcff;
}
.mine-batch-child-label {
display: inline-flex;
align-items: center;
gap: 8px;
color: #7a8a9a;
}
.mine-batch-child-label::before {
content: "";
width: 16px;
height: 1px;
background: #c8d3df;
}
</style>
<div id="app" v-cloak>
<guava ref="guava">
<template>
<el-card shadow="never">
<search @search="doSearch">
<search-item label="姓名/工号">
<el-input placeholder="请输入姓名或工号" clearable v-model="pageForm.searchKeyword"
@keyup.enter.native="doSearch">
</el-input>
</search-item>
<search-item label="活动场地">
<el-select v-model="pageForm.siteId" @change="doSearch" style="width: 100%;" placeholder="请选择活动场地" filterable clearable>
<el-option v-for="item in siteOptions" :value="item.id" :key="item.id" :label="item.name"></el-option>
</el-select>
</search-item>
<search-item label="场地类型">
<el-select v-model="pageForm.siteType" @change="doSearch" style="width: 100%;" placeholder="请选择场地类型" filterable clearable>
<el-option v-for="item in typeOptions" :value="item.id" :key="item.id" :label="item.name"></el-option>
</el-select>
</search-item>
</search>
</el-card>
<el-card shadow="never">
<table-tool label="申请列表"></table-tool>
<el-table :data="tableData" @sort-change="pageOrder" :row-class-name="tableRowClassName" style="width: 100%;">
<el-table-column label="" width="54" align="center" header-align="center">
<template v-slot="{ row }">
<el-button
v-if="row._hasFoldChildren"
class="mine-batch-toggle"
type="text"
@click="toggleBatchGroup(row)">
<i :class="row._expanded ? 'el-icon-caret-bottom' : 'el-icon-caret-right'"></i>
</el-button>
<span v-else class="mine-batch-empty"></span>
</template>
</el-table-column>
<el-table-column type="index" width="50" label="序号" :index="indexMethod"></el-table-column>
<el-table-column
v-for="column in tableColumns"
:key="column.prop"
:label="column.label"
:prop="column.prop"
:width="column.width"
:sortable="column.sortable"
align="center"
header-align="center"
show-overflow-tooltip
>
<template v-slot="{ row }" v-if="column.prop === 'siteName'">
<span v-if="row._isBatchChild" class="mine-batch-child-label">{{ row.siteName }}</span>
<span v-else>{{ row.siteName }}</span>
</template>
<template v-slot="{ row }" v-else-if="column.prop === 'instanceState'">
<enum-tag :value="row.instanceState" name="ProcessInstanceStateEnum" label_key="message" size="small"></enum-tag>
</template>
<template v-slot="{ row }" v-else-if="column.prop === 'yearlyBatch'">
<span>{{ normalizeBatchFlag(row) ? '是' : '否' }}</span>
</template>
</el-table-column>
<el-table-column label="操作" fixed="right" width="180">
<template v-slot="{ row }">
<el-button @click="onView(row)" size="mini" type="primary">查看</el-button>
<el-button v-if="row.canCancel && !row._isBatchChild" @click="onDelete(row)" size="mini" type="danger">删除</el-button>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
</template>
<template #edit>
<info ref="infoRef"></info>
</template>
</guava>
</div>
<script nonce="${cspNonce!}">
<!--#include('../common/applyInfo.js'){}#-->
new Vue({
el: '#app',
store,
mixins: [initTableMixins],
components: {
'info': siteCugApplyInfo,
},
data() {
return {
typeOptions: [],
siteOptions: [],
rawTableData: [],
expandedBatchKeys: {},
tableColumns: [
{prop: 'siteName', label: '场地名称', sortable: 'custom'},
{prop: 'applyUserName', label: '预约人', sortable: 'custom'},
{prop: 'applyLoginName', label: '工号', sortable: 'custom'},
{prop: 'yearlyBatch', label: '是否批量预约', sortable: 'custom', width: 130},
{prop: 'reserveTargetName', label: '预约单位', sortable: 'custom'},
{prop: 'reserveStartTime', label: '开始时间', sortable: 'custom'},
{prop: 'reserveEndTime', label: '结束时间', sortable: 'custom'},
{prop: 'applyMobile', label: '联系方式', sortable: 'custom'},
{prop: 'taskName', label: '当前节点', sortable: 'custom'},
{prop: 'instanceState', label: '流程状态', sortable: 'custom', width: 120},
],
}
},
methods: {
normalizeBatchFlag(row) {
return row && (row.yearlyBatch === 1 || row.yearlyBatch === '1' || row.yearlyBatch === true)
},
getBatchGroupKey(row) {
if (!this.normalizeBatchFlag(row)) {
return 'single_' + row.id
}
const batchNo = row.backOption || row.id
return 'batch_' + batchNo + '_' + (row.applyUserName || '') + '_' + (row.siteName || '')
},
buildTableData(rows) {
const groups = new Map()
;(rows || []).forEach(row => {
const key = this.getBatchGroupKey(row)
if (!groups.has(key)) {
groups.set(key, [])
}
groups.get(key).push({...row})
})
const displayRows = []
groups.forEach((groupRows, key) => {
const sortedRows = groupRows.slice().sort((a, b) => {
return (b.reserveStartTime || '').localeCompare(a.reserveStartTime || '')
})
const parent = {...sortedRows[0]}
const children = sortedRows.slice(1).map(item => ({
...item,
_isBatchChild: true,
_groupKey: key,
_hasFoldChildren: false,
}))
parent._groupKey = key
parent._isBatchChild = false
parent._hasFoldChildren = children.length > 0
parent._expanded = !!this.expandedBatchKeys[key]
parent._batchCount = sortedRows.length
parent._foldedRecords = children
displayRows.push(parent)
if (parent._expanded) {
displayRows.push(...children)
}
})
return displayRows
},
tableRowClassName({ row }) {
return row && row._isBatchChild ? 'mine-batch-child-row' : ''
},
toggleBatchGroup(row) {
if (!row || !row._groupKey || !row._hasFoldChildren) {
return
}
this.$set(this.expandedBatchKeys, row._groupKey, !this.expandedBatchKeys[row._groupKey])
this.tableData = this.buildTableData(this.rawTableData)
},
onView(row) {
this.$refs.guava.edit(() => {
this.$refs.infoRef.onOpen(row)
})
},
onDelete(row) {
const message = this.normalizeBatchFlag(row)
? ('该记录属于“预约本年”批量预约,确认后将一并删除同批次的 ' + (row.batchDeleteCount || row._batchCount || 0) + ' 条预约记录,是否继续?')
: '您确定要删除吗?'
this.$confirm(message, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.$axios.post('/platform/siteCug/mine/delete', { id: row.id }).then((res) => {
if (res.code === 0) {
this.$message.success(res.msg)
this.pageData()
}
})
})
},
querySiteType() {
this.$axios.post('/platform/siteCug/function/type/queryFunctionType').then((res) => {
this.typeOptions = res.data
})
},
querySites() {
this.$axios.post('/platform/siteCug/manage/querySites').then((res) => {
this.siteOptions = res.data
})
},
pageData() {
this.$axios.post(loc() + '/pageData', this.pageForm).then((res) => {
if (res.code === 0) {
this.rawTableData = Array.isArray(res.data.list) ? res.data.list : []
this.tableData = this.buildTableData(this.rawTableData)
this.pageForm.totalCount = res.data.totalCount
}
})
},
},
async created() {
this.querySiteType()
this.querySites()
this.pageData()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,317 @@
<!--#
layout("/layouts/platform.html"){
#-->
<style>
.record-batch-toggle {
padding: 0;
color: #409EFF;
font-size: 16px;
}
.record-batch-empty {
display: inline-block;
width: 16px;
height: 16px;
}
.record-batch-child-row {
background: #fafcff;
}
.record-batch-child-label {
display: inline-flex;
align-items: center;
gap: 8px;
color: #7a8a9a;
}
.record-batch-child-label::before {
content: "";
width: 16px;
height: 1px;
background: #c8d3df;
}
</style>
<div id="app" v-cloak>
<guava ref="guava">
<template>
<el-card shadow="never">
<search @search="doSearch">
<search-item label="姓名/工号">
<el-input
v-model="pageForm.searchKeyword"
placeholder="请输入姓名或工号"
clearable
@keyup.enter.native="doSearch">
</el-input>
</search-item>
<search-item label="活动场地">
<el-select
v-model="pageForm.siteId"
@change="doSearch"
style="width: 100%"
placeholder="请选择活动场地"
filterable
clearable>
<el-option
v-for="item in siteOptions"
:key="item.id"
:label="item.name"
:value="item.id">
</el-option>
</el-select>
</search-item>
<search-item label="场地类型">
<el-select
v-model="pageForm.siteType"
@change="doSearch"
style="width: 100%"
placeholder="请选择场地类型"
filterable
clearable>
<el-option
v-for="item in typeOptions"
:key="item.id"
:label="item.name"
:value="item.id">
</el-option>
</el-select>
</search-item>
<search-item label="预约单位">
<el-input
v-model="pageForm.reserveTargetKeyword"
placeholder="请输入分工会或协会名称"
clearable
@keyup.enter.native="doSearch">
</el-input>
</search-item>
<search-item label="预约开始时间">
<el-date-picker
v-model="pageForm.reserveTimeStart"
type="datetime"
value-format="yyyy-MM-dd HH:mm:ss"
placeholder="请选择预约开始时间"
style="width: 100%"
clearable
@change="doSearch">
</el-date-picker>
</search-item>
<search-item label="预约结束时间">
<el-date-picker
v-model="pageForm.reserveTimeEnd"
type="datetime"
value-format="yyyy-MM-dd HH:mm:ss"
placeholder="请选择预约结束时间"
style="width: 100%"
clearable
@change="doSearch">
</el-date-picker>
</search-item>
</search>
</el-card>
<el-card shadow="never">
<table-tool label="申请列表">
<el-button type="primary" size="small" @click="doExport">导出表格</el-button>
</table-tool>
<el-table
:data="tableData"
:row-class-name="tableRowClassName"
@sort-change="pageOrder"
style="width: 100%">
<el-table-column label="" width="54" align="center" header-align="center">
<template v-slot="{ row }">
<el-button
v-if="row._hasFoldChildren"
class="record-batch-toggle"
type="text"
@click="toggleBatchGroup(row)">
<i :class="row._expanded ? 'el-icon-caret-bottom' : 'el-icon-caret-right'"></i>
</el-button>
<span v-else class="record-batch-empty"></span>
</template>
</el-table-column>
<el-table-column type="index" width="50" label="序号" :index="indexMethod"></el-table-column>
<el-table-column
v-for="column in tableColumns"
:key="column.prop"
:label="column.label"
:prop="column.prop"
:width="column.width"
:sortable="column.sortable"
align="center"
header-align="center"
show-overflow-tooltip>
<template v-slot="{ row }" v-if="column.prop === 'siteName'">
<span v-if="row._isBatchChild" class="record-batch-child-label">{{ row.siteName }}</span>
<span v-else>{{ row.siteName }}</span>
</template>
<template v-slot="{ row }" v-else-if="column.prop === 'reserveTargetName'">
<span>{{ row.reserveType === 'club' ? (row.clubName || '-') : (row.applyUnionName || '-') }}</span>
</template>
<template v-slot="{ row }" v-else-if="column.prop === 'yearlyBatch'">
<span>{{ normalizeBatchFlag(row) ? '是' : '否' }}</span>
</template>
</el-table-column>
<el-table-column label="操作" fixed="right" width="180">
<template v-slot="{ row }">
<el-button @click="onView(row)" size="mini" type="primary">查看</el-button>
<el-button @click="onDelete(row.id)" size="mini" type="danger">删除</el-button>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
</template>
<template #edit>
<info ref="infoRef"></info>
</template>
</guava>
</div>
<script nonce="${cspNonce!}">
<!--#include('../common/applyInfo.js'){}#-->
new Vue({
el: '#app',
store,
mixins: [initTableMixins],
components: {
info: siteCugApplyInfo,
},
data() {
return {
pageForm: {
audit: false,
siteType: '',
siteId: '',
reserveType: '',
reserveTargetKeyword: '',
reserveTimeStart: '',
reserveTimeEnd: '',
},
typeOptions: [],
siteOptions: [],
rawTableData: [],
expandedBatchKeys: {},
tableColumns: [
{ prop: 'siteName', label: '场地名称', sortable: 'custom' },
{ prop: 'applyUserName', label: '预约人', sortable: 'custom' },
{ prop: 'applyLoginName', label: '工号', sortable: 'custom' },
{ prop: 'yearlyBatch', label: '是否批量预约', sortable: 'custom', width: 130 },
{ prop: 'reserveTargetName', label: '预约单位', sortable: 'custom' },
{ prop: 'reserveStartTime', label: '开始时间', sortable: 'custom' },
{ prop: 'reserveEndTime', label: '结束时间', sortable: 'custom' },
{ prop: 'applyMobile', label: '联系方式', sortable: 'custom' },
],
}
},
methods: {
normalizeBatchFlag(row) {
return row && (row.yearlyBatch === 1 || row.yearlyBatch === '1' || row.yearlyBatch === true)
},
getBatchGroupKey(row) {
if (!this.normalizeBatchFlag(row)) {
return 'single_' + row.id
}
const batchNo = row.backOption || row.id
return 'batch_' + batchNo + '_' + (row.applyUserName || '') + '_' + (row.siteName || '')
},
buildTableData(rows) {
const groups = new Map()
;(rows || []).forEach(row => {
const key = this.getBatchGroupKey(row)
if (!groups.has(key)) {
groups.set(key, [])
}
groups.get(key).push({ ...row })
})
const displayRows = []
groups.forEach((groupRows, key) => {
const sortedRows = groupRows.slice().sort((a, b) => {
return (b.reserveStartTime || '').localeCompare(a.reserveStartTime || '')
})
const parent = { ...sortedRows[0] }
const children = sortedRows.slice(1).map(item => ({
...item,
_isBatchChild: true,
_groupKey: key,
_hasFoldChildren: false,
}))
parent._groupKey = key
parent._isBatchChild = false
parent._hasFoldChildren = children.length > 0
parent._expanded = !!this.expandedBatchKeys[key]
parent._batchCount = sortedRows.length
parent._foldedRecords = children
displayRows.push(parent)
if (parent._expanded) {
displayRows.push(...children)
}
})
return displayRows
},
tableRowClassName({ row }) {
return row && row._isBatchChild ? 'record-batch-child-row' : ''
},
toggleBatchGroup(row) {
if (!row || !row._groupKey || !row._hasFoldChildren) {
return
}
this.$set(this.expandedBatchKeys, row._groupKey, !this.expandedBatchKeys[row._groupKey])
this.tableData = this.buildTableData(this.rawTableData)
},
onView(row) {
this.$refs.guava.edit(() => {
this.$refs.infoRef.onOpen(row)
})
},
onDelete(id) {
this.$confirm('您确定要删除吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(() => {
this.$axios.post('/platform/siteCug/record/delete', { id }).then((res) => {
if (res.code === 0) {
this.$message.success(res.msg)
this.pageData()
}
})
})
},
doExport() {
this.$downLoad('/platform/siteCug/record/doExport', this.pageForm)
},
querySiteType() {
this.$axios.post('/platform/siteCug/function/type/queryFunctionType').then((res) => {
this.typeOptions = res.data
})
},
querySites() {
this.$axios.post('/platform/siteCug/manage/querySites').then((res) => {
this.siteOptions = res.data
})
},
pageData() {
this.$axios.post(loc() + '/pageData', this.pageForm).then((res) => {
if (res.code === 0) {
this.rawTableData = Array.isArray(res.data.list) ? res.data.list : []
this.tableData = this.buildTableData(this.rawTableData)
this.pageForm.totalCount = res.data.totalCount
}
})
},
},
created() {
this.querySiteType()
this.querySites()
this.pageData()
},
})
</script>
<!--#
}
#-->
@@ -0,0 +1,368 @@
<!--#
layout("/layouts/platform.html"){
#-->
<style>
.audit-batch-toggle {
padding: 0;
color: #409EFF;
font-size: 16px;
}
.audit-batch-empty {
display: inline-block;
width: 16px;
height: 16px;
}
.audit-batch-child-row {
background: #fafcff;
}
.audit-batch-child-label {
display: inline-flex;
align-items: center;
gap: 8px;
color: #7a8a9a;
}
.audit-batch-child-label::before {
content: "";
width: 16px;
height: 1px;
background: #c8d3df;
}
</style>
<div id="app">
<guava ref="guava">
<el-card shadow="never">
<search @search="doSearch">
<search-item label="姓名/工号">
<el-input
v-model="pageForm.searchKeyword"
placeholder="请输入姓名或工号"
clearable
@keyup.enter.native="doSearch">
</el-input>
</search-item>
<search-item label="活动场地">
<el-select
v-model="pageForm.siteId"
@change="doSearch"
style="width: 100%"
placeholder="请选择活动场地"
filterable
clearable>
<el-option
v-for="item in siteOptions"
:key="item.id"
:label="item.name"
:value="item.id">
</el-option>
</el-select>
</search-item>
<search-item label="场地类型">
<el-select
v-model="pageForm.siteType"
@change="doSearch"
style="width: 100%"
placeholder="请选择场地类型"
filterable
clearable>
<el-option
v-for="item in typeOptions"
:key="item.id"
:label="item.name"
:value="item.id">
</el-option>
</el-select>
</search-item>
</search>
</el-card>
<el-card shadow="never">
<table-tool label="申请列表">
<el-radio-group v-model="pageForm.approval" size="small" @change="doSearch">
<el-radio-button :label="true">已审核</el-radio-button>
<el-radio-button :label="false">未审核</el-radio-button>
</el-radio-group>
</table-tool>
<el-table
:data="tableData"
:row-class-name="tableRowClassName"
@sort-change="pageOrder"
style="width: 100%">
<el-table-column label="" width="54" align="center" header-align="center">
<template v-slot="{ row }">
<el-button
v-if="row._hasFoldChildren"
class="audit-batch-toggle"
type="text"
@click="toggleBatchGroup(row)">
<i :class="row._expanded ? 'el-icon-caret-bottom' : 'el-icon-caret-right'"></i>
</el-button>
<span v-else class="audit-batch-empty"></span>
</template>
</el-table-column>
<el-table-column type="index" width="50" label="序号" :index="indexMethod"></el-table-column>
<el-table-column
v-for="column in tableColumns"
:key="column.prop"
:label="column.label"
:prop="column.prop"
:width="column.width"
:sortable="column.sortable"
align="center"
header-align="center"
show-overflow-tooltip>
<template v-slot="{ row }" v-if="column.prop === 'siteName'">
<span v-if="row._isBatchChild" class="audit-batch-child-label">{{ row.siteName }}</span>
<span v-else>{{ row.siteName }}</span>
</template>
<template v-slot="{ row }" v-else-if="column.prop === 'reserveTargetName'">
<span>{{ row.reserveType === 'club' ? (row.clubName || '-') : (row.applyUnionName || '-') }}</span>
</template>
<template v-slot="{ row }" v-else-if="column.prop === 'yearlyBatch'">
<span>{{ normalizeBatchFlag(row) ? '是' : '否' }}</span>
</template>
<template v-slot="{ row }" v-else-if="column.prop === 'instanceState'">
<enum-tag :value="row.instanceState" name="ProcessInstanceStateEnum" label_key="message" size="small"></enum-tag>
</template>
</el-table-column>
<el-table-column label="操作" fixed="right" width="220">
<template v-slot="{ row }">
<el-button @click="onView(row)" size="mini" type="primary">查看</el-button>
<el-button v-if="!row._isBatchChild && row.taskState === 10" @click="onAudit(row)" size="mini" type="primary">审核</el-button>
<el-button v-if="!row._isBatchChild && canRevoke(row)" :loading="revokeLoading" :disabled="revokeLoading" @click="onRevoke(row)" size="mini" type="danger">撤回</el-button>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
<template #edit>
<info ref="infoRef">
<div v-if="showApprovalForm">
<div class="process-title">
{{ formData.taskName }}
</div>
<el-form :model="formData" ref="formRef" :rules="formRules" label-width="0" class="flow-task-form">
<el-form-item
label="审批意见"
prop="tf_opinion"
:rules="[{ required: true, message: '必填', trigger: ['change', 'blur'] }]">
<user-opinion-textarea v-model="formData.tf_opinion"></user-opinion-textarea>
</el-form-item>
</el-form>
<el-row type="flex" justify="end">
<el-button @click="$refs.guava.index()" size="small">取消</el-button>
<el-button @click="handleTaskAction(6)" :loading="auditLoading" :disabled="auditLoading" size="small" type="info">退回到发起人</el-button>
<el-button @click="handleTaskAction(2)" :loading="auditLoading" :disabled="auditLoading" size="small" type="danger">拒绝申请</el-button>
<el-button @click="handleTaskAction(1)" :loading="auditLoading" :disabled="auditLoading" size="small" type="primary">同意申请</el-button>
</el-row>
</div>
</info>
</template>
</guava>
</div>
<script nonce="${cspNonce!}">
<!--#include('../common/applyInfo.js'){}#-->
new Vue({
el: '#app',
store,
mixins: [initTableMixins],
components: {
info: siteCugApplyInfo,
},
data() {
return {
pageForm: {
approval: false,
},
rawTableData: [],
expandedBatchKeys: {},
tableColumns: [
{ prop: 'siteName', label: '场地名称', sortable: 'custom' },
{ prop: 'applyUserName', label: '预约人', sortable: 'custom' },
{ prop: 'applyLoginName', label: '工号', sortable: 'custom' },
{ prop: 'yearlyBatch', label: '是否批量预约', sortable: 'custom', width: 130 },
{ prop: 'reserveTargetName', label: '预约单位', sortable: 'custom' },
{ prop: 'reserveStartTime', label: '开始时间', sortable: 'custom' },
{ prop: 'reserveEndTime', label: '结束时间', sortable: 'custom' },
{ prop: 'applyMobile', label: '联系方式', sortable: 'custom' },
{ prop: 'curTaskName', label: '当前节点', sortable: 'custom' },
{ prop: 'instanceState', label: '流程状态', sortable: 'custom', width: 120 },
],
typeOptions: [],
siteOptions: [],
formData: {},
formRules: {},
showApprovalForm: false,
auditLoading: false,
revokeLoading: false,
}
},
methods: {
normalizeBatchFlag(row) {
return row && (row.yearlyBatch === 1 || row.yearlyBatch === '1' || row.yearlyBatch === true)
},
getBatchGroupKey(row) {
if (!this.normalizeBatchFlag(row)) {
return 'single_' + row.id
}
const batchNo = row.backOption || row.id
return 'batch_' + batchNo + '_' + (row.applyUserName || '') + '_' + (row.siteName || '')
},
buildTableData(rows) {
const groups = new Map()
;(rows || []).forEach(row => {
const key = this.getBatchGroupKey(row)
if (!groups.has(key)) {
groups.set(key, [])
}
groups.get(key).push({ ...row })
})
const displayRows = []
groups.forEach((groupRows, key) => {
const sortedRows = groupRows.slice().sort((a, b) => {
return (b.reserveStartTime || '').localeCompare(a.reserveStartTime || '')
})
const parent = { ...sortedRows[0] }
const children = sortedRows.slice(1).map(item => ({
...item,
_isBatchChild: true,
_groupKey: key,
_hasFoldChildren: false,
}))
parent._groupKey = key
parent._isBatchChild = false
parent._hasFoldChildren = children.length > 0
parent._expanded = !!this.expandedBatchKeys[key]
parent._batchCount = sortedRows.length
parent._foldedRecords = children
displayRows.push(parent)
if (parent._expanded) {
displayRows.push(...children)
}
})
return displayRows
},
tableRowClassName({ row }) {
return row && row._isBatchChild ? 'audit-batch-child-row' : ''
},
toggleBatchGroup(row) {
if (!row || !row._groupKey || !row._hasFoldChildren) {
return
}
this.$set(this.expandedBatchKeys, row._groupKey, !this.expandedBatchKeys[row._groupKey])
this.tableData = this.buildTableData(this.rawTableData)
},
canRevoke(row) {
return Number(row.instanceState) === 20
},
onView(row) {
this.$refs.guava.edit(() => {
this.showApprovalForm = false
this.$refs.infoRef.onOpen(row)
})
},
onAudit(row) {
this.$refs.guava.edit(() => {
this.showApprovalForm = true
this.formData = {
processTaskId: row.taskId,
applyId: row.id,
taskName: row.curTaskName,
tf_opinion: '',
yearlyBatch: row.yearlyBatch,
batchAuditCount: row.batchAuditCount || row._batchCount,
}
this.$refs.infoRef.onOpen(row)
})
},
handleTaskAction(val) {
if (this.auditLoading) {
return
}
const message = this.formData.yearlyBatch
? ('该记录属于“预约本年”批量预约,提交后将一并审核同批次的 ' + (this.formData.batchAuditCount || 0) + ' 条预约记录,是否继续?')
: '您确定要提交吗?'
this.$confirm(message, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(() => {
this.auditLoading = true
this.$axios.post('/platform/siteCug/schoolUnionAudit/executeTask', {
data: JSON.stringify({
...this.formData,
submitType: val,
}),
}).then((res) => {
if (res.code === 0) {
this.$refs.guava.index()
this.$message.success(res.msg)
this.doSearch()
}
}).finally(() => {
this.auditLoading = false
})
}).catch(() => {})
},
onRevoke(row) {
if (!this.canRevoke(row) || this.revokeLoading) {
return
}
const message = this.normalizeBatchFlag(row)
? ('该记录属于“预约本年”批量预约,撤回后将一并撤回同批次的 ' + (row.batchAuditCount || row._batchCount || 0) + ' 条审核任务,是否继续?')
: '您确定要撤回吗?'
this.$confirm(message, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'info',
}).then(() => {
this.revokeLoading = true
this.$axios.post('/platform/siteCug/schoolUnionAudit/revokeTask', { taskId: row.taskId, applyId: row.id }).then((res) => {
if (res.code === 0) {
this.$message.success(res.msg)
this.doSearch()
}
}).finally(() => {
this.revokeLoading = false
})
}).catch(() => {})
},
querySiteType() {
this.$axios.post('/platform/siteCug/function/type/queryFunctionType').then((res) => {
this.typeOptions = res.data
})
},
querySites() {
this.$axios.post('/platform/siteCug/manage/querySites').then((res) => {
this.siteOptions = res.data
})
},
pageData() {
this.$axios.post(loc() + '/pageData', this.pageForm).then((res) => {
if (res.code === 0) {
this.rawTableData = Array.isArray(res.data.list) ? res.data.list : []
this.tableData = this.buildTableData(this.rawTableData)
this.pageForm.totalCount = res.data.totalCount
}
})
},
},
created() {
this.querySiteType()
this.querySites()
this.pageData()
},
})
</script>
<!--#
}
#-->
@@ -0,0 +1,68 @@
const basicForm = {
template: /*language=HTML*/ `
<div>
<el-form :model="formData" ref="formRef" :rules="formRules" label-width="110px" label-position="left">
<el-form-item label="类型编码" prop="code">
<el-input type="text" v-model="formData.code" maxlength="50"
placeholder="请输入类型编码"></el-input>
</el-form-item>
<el-form-item label="类型名称" prop="name">
<el-input type="text" v-model="formData.name" maxlength="50"
placeholder="请输入类型名称"></el-input>
</el-form-item>
<el-form-item label="是否启用" prop="enable">
<el-switch
v-model="formData.enable"
active-color="#13ce66"
inactive-color="#ff4949">
</el-switch>
</el-form-item>
</el-form>
<el-row class="mt10" justify="end" type="flex">
<el-button @click="$emit('refresh')">取消</el-button>
<el-button @click="onSubmit" type="primary">提交</el-button>
</el-row>
</div>
`,
data() {
return {
formData: {
enable: true
},
formRules: {
name: [{required: true, message: '必填', trigger: ['blur', 'change']}],
code: [{required: true, message: '必填', trigger: ['blur', 'change']}],
enable: [{required: true, message: '必填', trigger: ['blur', 'change']}],
},
}
},
methods: {
onOpen(row) {
if (row && row.id) {
this.formData = clone(row)
}
},
onSubmit() {
this.$refs.formRef.validate((valid) => {
if (valid) {
this.$confirm("您确定要提交吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(async () => {
const resp = await this.$axios.post("/platform/siteCug/function/type/submit", this.formData)
if (resp.code === 0) {
this.$message.success(resp.msg)
this.$emit('refresh')
} else {
this.$message.warning(resp.msg)
}
})
}
})
},
},
style: /*language=CSS*/ `
`
}
@@ -0,0 +1,139 @@
<!--#
layout("/layouts/platform.html"){
#-->
<style>
</style>
<div id="app" v-cloak>
<guava ref="guava">
<template>
<el-card shadow="never">
<search @search="doSearch">
<search-item label="名称/编码">
<el-input placeholder="请输入名称或编码" clearable v-model="pageForm.searchKeyword"
@keyup.enter.native="doSearch">
</el-input>
</search-item>
</search>
</el-card>
<el-card shadow="never" class="mt20">
<table-tool label="功能类型列表">
<el-button type="primary" size="small" @click="onAdd">
<i class="ti-plus"></i>
新增类型
</el-button>
</table-tool>
<el-table :data="tableData" @sort-change="pageOrder" :size="tableSize">
<el-table-column label="序号" type="index" :index="indexMethod" width="60"></el-table-column>
<el-table-column
:label="column.label"
:prop="column.prop"
:key="column.prop"
:width="column.width"
:sortable="column.sortable"
align="center"
header-align="center"
show-overflow-tooltip
v-for="column in tableColumns"
>
<template v-slot="{ row }" v-if="column.prop === 'enable'">
<el-switch
@change="switchChange(row)"
v-model="row.enable"
active-color="#13ce66"
inactive-color="#ff4949">
</el-switch>
</template>
</el-table-column>
<el-table-column label="操作" width="230">
<template v-slot="{ row }">
<el-button @click="onEdit(row)" size="mini" type="primary">编辑</el-button>
<el-button @click="onDelete(row)" size="mini" type="danger">删除</el-button>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
</template>
<template #edit>
<basic-form ref="basicFormRef" @refresh="refresh"></basic-form>
</template>
</guava>
</div>
<script nonce="${cspNonce!}">
<!--#include('basicForm.js'){}#-->
const vue = new Vue({
el: "#app",
mixins: [initTableMixins],
components: {
"basic-form": basicForm,
},
data() {
return {
tableColumns: [
{prop: 'code', label: '类型编码'},
{prop: 'name', label: '类型名称'},
{prop: 'enable', label: '是否启用'},
],
}
},
methods: {
refresh() {
this.doSearch()
this.$refs.guava.index()
},
onAdd() {
this.$refs.guava.edit(() => {
this.$refs.basicFormRef.onOpen()
})
},
onEdit(row) {
this.$refs.guava.edit(() => {
this.$refs.basicFormRef.onOpen(row)
})
},
onDelete(row) {
this.$confirm("您确定要删除吗, 是否继续?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
})
.then(() => {
this.$axios.post("/platform/siteCug/function/type/delete", { id: row.id }).then((res) => {
if (res.code === 0) {
this.$message.success(res.msg)
this.pageData()
}
})
})
.catch(() => {})
},
switchChange(row) {
this.$axios.post("/platform/siteCug/function/type/submit", row).then((res) => {
if (res.code === 0) {
this.$message.success(res.msg)
this.pageData()
}
})
},
pageData() {
this.$axios.post(loc() + "/pageData", this.pageForm).then((res) => {
if (res.code === 0) {
this.tableData = res.data.list
this.pageForm.totalCount = res.data.totalCount
}
})
},
},
async created() {
this.pageData()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,979 @@
new Vue({
el: '#app',
store,
data() {
return {
noticeCollapsed: true,
siteId: GetQueryString('siteId'),
row: {},
siteLoaded: false,
clubOptions: [],
yearlyReserve: false,
scheduleDate: '',
availabilityLoading: false,
activeTimeGroupKey: '',
selectedBlockKeys: [],
timeLimitConfig: {
filterHolidays: false,
holidayList: [],
notApplyTimeList: [],
},
availabilityData: {
date: '',
reserveTimeType: 1,
reserveTimeTypeName: '分段预约',
blocks: [],
},
formData: {
siteId: '',
applyUserId: '',
applyUserName: '',
applySex: '',
applyLoginName: '',
applyUnitId: '',
applyUnitName: '',
reserveType: '',
applyUnionId: '',
applyUnionName: '',
clubId: '',
clubName: '',
applyMobile: '',
reserveStartTime: '',
reserveEndTime: '',
yearlyReserveEndDate: '',
joinCount: 1,
applyCause: '',
},
showReserveTypePicker: false,
showClubPicker: false,
showScheduleDatePicker: false,
showAvailabilityPopup: false,
showYearlyEndDatePicker: false,
schedulePickerDate: new Date(),
yearlyReservePickerDate: new Date(),
}
},
computed: {
canApplyUnion() {
return this.$auth.hasPermission('siteCug.apply.union')
},
canApplyClub() {
return this.$auth.hasPermission('siteCug.apply.club')
},
availableReserveTypes() {
const result = []
if (this.canApplyUnion) {
result.push('union')
}
if (this.canApplyClub) {
result.push('club')
}
return result
},
availableReserveTypeOptions() {
const result = []
if (this.canApplyUnion) {
result.push({ value: 'union', label: '分工会预约' })
}
if (this.canApplyClub) {
result.push({ value: 'club', label: '协会预约' })
}
return result
},
reserveTypeColumns() {
return this.availableReserveTypeOptions.map(item => item.label)
},
reserveTypeText() {
const current = this.availableReserveTypeOptions.find(item => item.value === this.formData.reserveType)
return current ? current.label : ''
},
clubColumns() {
return this.clubOptions.map(item => item.clubName)
},
availabilityBlocks() {
return Array.isArray(this.availabilityData.blocks) ? this.availabilityData.blocks : []
},
isSegmentedMode() {
return Number(this.availabilityData.reserveTimeType || this.row.reserveTimeType || 1) === 1
},
reserveModeText() {
return this.availabilityData.reserveTimeTypeName || (this.isSegmentedMode ? '分段预约' : '全天候预约')
},
timeGroupTabs() {
const result = []
const used = {}
this.availabilityBlocks.forEach((block) => {
const key = this.getBlockGroupKey(block)
if (!key || used[key]) {
return
}
used[key] = true
result.push({
key: key,
label: this.getBlockGroupLabel(block),
})
})
return result
},
displayedAvailabilityBlocks() {
if (!this.activeTimeGroupKey) {
return this.availabilityBlocks
}
return this.availabilityBlocks.filter(block => this.getBlockGroupKey(block) === this.activeTimeGroupKey)
},
selectionSummary() {
if (!this.formData.reserveStartTime || !this.formData.reserveEndTime) {
return ''
}
return this.formData.reserveStartTime + ' 至 ' + this.formData.reserveEndTime
},
scheduleMinDate() {
return this.$moment().startOf('day').toDate()
},
scheduleMaxDate() {
return this.$moment().add(365, 'day').endOf('day').toDate()
},
yearlyReserveMinDate() {
const base = this.formData.reserveStartTime
? this.$moment(this.formData.reserveStartTime)
: (this.scheduleDate ? this.$moment(this.scheduleDate, 'YYYY-MM-DD') : this.$moment())
return base.startOf('day').toDate()
},
yearlyReserveMaxDate() {
return this.$moment().add(2, 'year').endOf('year').toDate()
},
yearlyReserveDates() {
if (!this.yearlyReserve || !this.formData.reserveStartTime || !this.formData.reserveEndTime || !this.formData.yearlyReserveEndDate) {
return []
}
const start = this.$moment(this.formData.reserveStartTime)
const end = this.$moment(this.formData.reserveEndTime)
const reserveEndDate = this.$moment(this.formData.yearlyReserveEndDate, 'YYYY-MM-DD')
if (!start.isValid() || !end.isValid() || !end.isAfter(start) || !reserveEndDate.isValid() || reserveEndDate.isBefore(start, 'day')) {
return []
}
const result = []
const current = start.clone().startOf('day')
const reserveEndDay = reserveEndDate.clone().startOf('day')
const targetWeekDay = start.day()
const startClock = start.format('HH:mm:ss')
const endClock = end.format('HH:mm:ss')
while (current.isSameOrBefore(reserveEndDay, 'day')) {
if (current.day() === targetWeekDay) {
const day = current.format('YYYY-MM-DD')
const startTime = day + ' ' + startClock
const endTime = day + ' ' + endClock
if (this.validateTimeLimit(false, false, startTime, endTime)) {
result.push(day)
}
}
current.add(1, 'day')
}
return result
},
batchReserveSummary() {
if (!this.yearlyReserve) {
return ''
}
if (!this.formData.yearlyReserveEndDate) {
return '请选择批量预约截止日期'
}
if (!this.formData.reserveStartTime || !this.formData.reserveEndTime) {
return '请先选择预约时间段,再设置批量预约截止日期'
}
if (!this.yearlyReserveDates.length) {
return '当前截止日期内没有可批量预约的同星期时段'
}
return '将从 ' + this.yearlyReserveDates[0] + ' 开始,自动预约到 ' + this.formData.yearlyReserveEndDate
},
},
watch: {
yearlyReserve(val) {
if (val) {
this.syncYearlyReserveEndDate(true)
}
},
},
methods: {
historyBack,
getDefaultReserveType() {
return this.availableReserveTypes[0] || ''
},
buildDefaultFormData() {
const user = this.$store.state.user || {}
const reserveType = this.getDefaultReserveType()
return {
siteId: this.row.id || '',
applyUserId: user.id || '',
applyUserName: user.username || '',
applySex: user.sex || '',
applyLoginName: user.loginname || '',
applyUnitId: user.unit ? user.unit.id : '',
applyUnitName: user.unit ? user.unit.name : '',
reserveType: reserveType,
applyUnionId: reserveType === 'union' && user.union ? user.union.id : '',
applyUnionName: reserveType === 'union' && user.union ? user.union.name : '',
clubId: '',
clubName: '',
applyMobile: user.mobile || '',
reserveStartTime: '',
reserveEndTime: '',
yearlyReserveEndDate: '',
joinCount: 1,
applyCause: '',
}
},
isBlockedDay(momentValue) {
return this.isDisabledDate(momentValue.toDate())
},
getInitialScheduleDate() {
let current = this.$moment().startOf('day')
for (let i = 0; i < 90; i++) {
if (!this.isBlockedDay(current)) {
return current.format('YYYY-MM-DD')
}
current = current.add(1, 'day')
}
return this.$moment().add(1, 'day').format('YYYY-MM-DD')
},
getDefaultYearlyReserveEndDate() {
const base = this.formData.reserveStartTime
? this.$moment(this.formData.reserveStartTime)
: (this.scheduleDate ? this.$moment(this.scheduleDate, 'YYYY-MM-DD') : this.$moment())
if (!base.isValid()) {
return this.$moment().endOf('month').format('YYYY-MM-DD')
}
return base.clone().endOf('month').format('YYYY-MM-DD')
},
syncYearlyReserveEndDate(force = false) {
const defaultDate = this.getDefaultYearlyReserveEndDate()
if (force || !this.formData.yearlyReserveEndDate) {
this.$set(this.formData, 'yearlyReserveEndDate', defaultDate)
return
}
if (this.formData.reserveStartTime && this.formData.yearlyReserveEndDate < this.formData.reserveStartTime.slice(0, 10)) {
this.$set(this.formData, 'yearlyReserveEndDate', defaultDate)
}
},
openReserveTypePicker() {
if (!this.reserveTypeColumns.length) {
this.$toast('当前没有可用的预约类型')
return
}
this.showReserveTypePicker = true
},
async onReserveTypeConfirm(value, index) {
this.showReserveTypePicker = false
const option = this.availableReserveTypeOptions[index]
if (!option) {
return
}
this.$set(this.formData, 'reserveType', option.value)
await this.reserveTypeChange(option.value)
},
async reserveTypeChange(value) {
const user = this.$store.state.user || {}
if (!this.availableReserveTypes.includes(value)) {
this.formData.reserveType = this.getDefaultReserveType()
return
}
if (value === 'union') {
this.formData.applyUnionId = user.union ? user.union.id : ''
this.formData.applyUnionName = user.union ? user.union.name : ''
this.formData.clubId = ''
this.formData.clubName = ''
return
}
if (value === 'club') {
this.formData.applyUnionId = ''
this.formData.applyUnionName = ''
this.formData.clubId = ''
this.formData.clubName = ''
await this.loadManagedClubs()
}
},
async loadManagedClubs() {
try {
const res = await this.$axios.post('/platform/club/examine/apply/getClubsByRole')
this.clubOptions = Array.isArray(res.data) ? res.data : []
this.syncManagedClubSelection()
if (!this.clubOptions.length) {
this.$toast('您当前没有可预约的协会管理权限')
}
} catch (e) {
this.clubOptions = []
this.formData.clubId = ''
this.formData.clubName = ''
this.$toast('协会列表加载失败,请稍后重试')
}
},
syncManagedClubSelection() {
if (!this.clubOptions.length) {
this.formData.clubId = ''
this.formData.clubName = ''
return
}
const currentClub = this.clubOptions.find(item => item.id === this.formData.clubId)
if (currentClub) {
this.formData.clubName = currentClub.clubName || ''
return
}
if (this.clubOptions.length === 1) {
this.formData.clubId = this.clubOptions[0].id
this.formData.clubName = this.clubOptions[0].clubName || ''
return
}
this.formData.clubId = ''
this.formData.clubName = ''
},
async openClubPicker() {
if (this.formData.reserveType !== 'club') {
this.$toast('请先选择协会预约')
return
}
if (!this.clubOptions.length) {
await this.loadManagedClubs()
}
if (!this.clubOptions.length) {
return
}
if (this.clubOptions.length === 1 && this.formData.clubId) {
return
}
this.showClubPicker = true
},
onClubConfirm(value, index) {
this.showClubPicker = false
const club = this.clubOptions[index]
this.formData.clubId = club ? club.id : ''
this.formData.clubName = club ? club.clubName : value
},
openScheduleDatePicker() {
const target = this.scheduleDate
? this.$moment(this.scheduleDate, 'YYYY-MM-DD')
: this.$moment(this.getInitialScheduleDate(), 'YYYY-MM-DD')
this.schedulePickerDate = target.isValid() ? target.toDate() : new Date()
this.showScheduleDatePicker = true
},
async onScheduleDateConfirm(value) {
this.showScheduleDatePicker = false
const target = this.$moment(value)
if (!target.isValid()) {
this.$toast('请选择正确的预约日期')
return
}
if (this.isBlockedDay(target)) {
this.$toast('该日期不可预约,请重新选择')
return
}
const day = target.format('YYYY-MM-DD')
this.schedulePickerDate = target.toDate()
this.scheduleDate = day
this.clearSelection(true)
await this.queryAvailability(day)
this.showAvailabilityPopup = true
},
async openAvailabilityPopup() {
if (!this.scheduleDate) {
this.$toast('请先选择预约日期')
this.openScheduleDatePicker()
return
}
if (!this.availabilityData.date || this.availabilityData.date !== this.scheduleDate) {
await this.queryAvailability(this.scheduleDate)
}
this.showAvailabilityPopup = true
},
async queryAvailability(date = this.scheduleDate) {
const targetDate = date || this.scheduleDate
if (!targetDate || !this.row.id) {
this.availabilityData = {
date: targetDate || '',
reserveTimeType: Number(this.row.reserveTimeType || 1),
reserveTimeTypeName: Number(this.row.reserveTimeType || 1) === 2 ? '全天候预约' : '分段预约',
blocks: [],
}
this.resetActiveTimeGroup()
return
}
this.availabilityLoading = true
try {
const res = await this.$axios.post('/platform/siteCug/apply/availability', {
siteId: this.row.id,
date: targetDate,
})
if (res.code === 0 && res.data) {
this.availabilityData = {
date: res.data.date || targetDate,
reserveTimeType: Number(res.data.reserveTimeType || this.row.reserveTimeType || 1),
reserveTimeTypeName: res.data.reserveTimeTypeName || (Number(res.data.reserveTimeType || this.row.reserveTimeType || 1) === 2 ? '全天候预约' : '分段预约'),
blocks: Array.isArray(res.data.blocks) ? res.data.blocks : [],
}
} else {
this.availabilityData = {
date: targetDate,
reserveTimeType: Number(this.row.reserveTimeType || 1),
reserveTimeTypeName: Number(this.row.reserveTimeType || 1) === 2 ? '全天候预约' : '分段预约',
blocks: [],
}
this.$toast((res && res.msg) || '预约时段加载失败')
}
} catch (e) {
this.availabilityData = {
date: targetDate,
reserveTimeType: Number(this.row.reserveTimeType || 1),
reserveTimeTypeName: Number(this.row.reserveTimeType || 1) === 2 ? '全天候预约' : '分段预约',
blocks: [],
}
this.$toast('预约时段加载失败,请稍后重试')
} finally {
this.availabilityLoading = false
this.resetActiveTimeGroup()
this.syncSelectionFromFields()
}
},
resetActiveTimeGroup() {
const selectedBlock = this.availabilityBlocks.find(block => this.selectedBlockKeys.includes(block.key))
if (selectedBlock) {
this.activeTimeGroupKey = this.getBlockGroupKey(selectedBlock)
return
}
this.activeTimeGroupKey = this.timeGroupTabs.length ? this.timeGroupTabs[0].key : ''
},
getBlockGroupKey(block) {
if (!block) {
return ''
}
if (this.isSegmentedMode) {
const groupIndex = block.groupIndex === undefined || block.groupIndex === null ? 0 : block.groupIndex
return 'group_' + groupIndex
}
const hour = this.$moment(block.startDateTime).hour()
return hour < 12 ? 'am' : 'pm'
},
getBlockGroupLabel(block) {
if (!block) {
return ''
}
if (this.isSegmentedMode) {
const groupLabel = block.groupLabel || '场次'
return groupLabel.split(' / ')[0]
}
const hour = this.$moment(block.startDateTime).hour()
return hour < 12 ? '上午' : '下午'
},
getSelectedIndices() {
return this.availabilityBlocks
.map((block, index) => this.selectedBlockKeys.includes(block.key) ? index : -1)
.filter(index => index > -1)
},
getBlockClass(block) {
const result = ['slot-block']
if (this.selectedBlockKeys.includes(block.key)) {
result.push('slot-block--selected')
return result
}
if (block.status === 'available') {
result.push('slot-block--available')
return result
}
if (block.status === 'reserved') {
result.push('slot-block--reserved')
return result
}
result.push('slot-block--closed')
return result
},
onAvailabilityBlockClick(block) {
if (!block || !block.key) {
return
}
if (block.status !== 'available' && !this.selectedBlockKeys.includes(block.key)) {
this.$toast(block.status === 'reserved' ? '该时段已被预约,请选择其他时段' : '该时段当前不可预约')
return
}
const index = this.availabilityBlocks.findIndex(item => item.key === block.key)
if (index < 0) {
return
}
const selectedIndices = this.getSelectedIndices()
if (selectedIndices.length) {
const currentGroupKey = this.getBlockGroupKey(this.availabilityBlocks[selectedIndices[0]])
const targetGroupKey = this.getBlockGroupKey(block)
if (currentGroupKey && targetGroupKey && currentGroupKey !== targetGroupKey) {
this.applySelectionRange(index, index)
return
}
}
this.toggleContinuousSelection(index)
},
toggleContinuousSelection(index) {
const selectedIndices = this.getSelectedIndices()
if (!selectedIndices.length) {
this.applySelectionRange(index, index)
return
}
const selectedStart = selectedIndices[0]
const selectedEnd = selectedIndices[selectedIndices.length - 1]
if (selectedIndices.includes(index)) {
if (selectedIndices.length === 1) {
this.clearSelection(true)
return
}
if (index === selectedStart) {
this.applySelectionRange(selectedStart + 1, selectedEnd)
return
}
if (index === selectedEnd) {
this.applySelectionRange(selectedStart, selectedEnd - 1)
return
}
this.applySelectionRange(index, index)
return
}
const nextStart = Math.min(index, selectedStart)
const nextEnd = Math.max(index, selectedEnd)
if (!this.canSelectRange(nextStart, nextEnd)) {
this.$toast(this.isSegmentedMode
? '分段预约只能连续选择一个或多个时段,不能跨间隔选择'
: '全天候预约只能连续选择一个或多个时段,不能跨间隔选择')
return
}
this.applySelectionRange(nextStart, nextEnd)
},
canSelectRange(startIndex, endIndex) {
if (startIndex < 0 || endIndex >= this.availabilityBlocks.length || startIndex > endIndex) {
return false
}
const firstBlock = this.availabilityBlocks[startIndex]
const groupKey = this.getBlockGroupKey(firstBlock)
for (let i = startIndex; i <= endIndex; i++) {
const block = this.availabilityBlocks[i]
if (!block || block.status !== 'available') {
return false
}
if (this.getBlockGroupKey(block) !== groupKey) {
return false
}
if (i > startIndex) {
const prev = this.availabilityBlocks[i - 1]
if (!prev || prev.endDateTime !== block.startDateTime) {
return false
}
}
}
return true
},
applySelectionRange(startIndex, endIndex) {
const selected = this.availabilityBlocks.slice(startIndex, endIndex + 1)
this.selectedBlockKeys = selected.map(item => item.key)
if (!selected.length) {
return
}
this.formData.reserveStartTime = selected[0].startDateTime
this.formData.reserveEndTime = selected[selected.length - 1].endDateTime
this.scheduleDate = selected[0].startDateTime.slice(0, 10)
this.schedulePickerDate = this.$moment(this.scheduleDate, 'YYYY-MM-DD').toDate()
this.activeTimeGroupKey = this.getBlockGroupKey(selected[0])
this.syncYearlyReserveEndDate()
},
clearSelection(clearTimeFields = false) {
this.selectedBlockKeys = []
if (clearTimeFields) {
this.formData.reserveStartTime = ''
this.formData.reserveEndTime = ''
}
},
confirmAvailabilitySelection() {
if (!this.formData.reserveStartTime || !this.formData.reserveEndTime || !this.selectedBlockKeys.length) {
this.$toast('请选择一个或多个连续时段')
return
}
if (!this.validateGraphSelection()) {
return
}
this.showAvailabilityPopup = false
},
findRangeIndicesByFields(startTime, endTime) {
if (!startTime || !endTime) {
return null
}
for (let i = 0; i < this.availabilityBlocks.length; i++) {
const block = this.availabilityBlocks[i]
if (block.status !== 'available' || block.startDateTime !== startTime) {
continue
}
let currentEnd = block.endDateTime
if (currentEnd === endTime) {
return [i, i]
}
for (let j = i + 1; j < this.availabilityBlocks.length; j++) {
const prev = this.availabilityBlocks[j - 1]
const next = this.availabilityBlocks[j]
if (next.status !== 'available' || prev.endDateTime !== next.startDateTime) {
break
}
if (this.getBlockGroupKey(prev) !== this.getBlockGroupKey(next)) {
break
}
currentEnd = next.endDateTime
if (currentEnd === endTime) {
return [i, j]
}
}
}
return null
},
syncSelectionFromFields() {
const startTime = this.formData.reserveStartTime
const endTime = this.formData.reserveEndTime
if (!startTime || !endTime) {
this.selectedBlockKeys = []
this.resetActiveTimeGroup()
return
}
if (startTime.slice(0, 10) !== this.scheduleDate || endTime.slice(0, 10) !== this.scheduleDate) {
this.selectedBlockKeys = []
this.resetActiveTimeGroup()
return
}
const range = this.findRangeIndicesByFields(startTime, endTime)
if (!range) {
this.selectedBlockKeys = []
this.resetActiveTimeGroup()
return
}
const selected = this.availabilityBlocks.slice(range[0], range[1] + 1)
this.selectedBlockKeys = selected.map(item => item.key)
if (selected.length) {
this.activeTimeGroupKey = this.getBlockGroupKey(selected[0])
}
},
validateGraphSelection(showMessage = true, startTime = this.formData.reserveStartTime, endTime = this.formData.reserveEndTime, targetDay = this.scheduleDate) {
if (!startTime || !endTime) {
return true
}
if (!targetDay || startTime.slice(0, 10) !== targetDay || endTime.slice(0, 10) !== targetDay) {
if (showMessage) {
this.$toast('预约时间必须与当前选择的预约日期一致')
}
return false
}
const range = this.findRangeIndicesByFields(startTime, endTime)
if (range) {
return true
}
if (showMessage) {
this.$toast(this.isSegmentedMode
? '分段预约请按场次标签选择一个或多个连续时段'
: '全天候预约请按上午或下午标签选择一个或多个连续时段')
}
return false
},
async normalizeReserveTypeData() {
const user = this.$store.state.user || {}
if (!this.availableReserveTypes.includes(this.formData.reserveType)) {
this.formData.reserveType = this.getDefaultReserveType()
}
if (this.formData.reserveType === 'union') {
this.formData.applyUnionId = user.union ? user.union.id : ''
this.formData.applyUnionName = user.union ? user.union.name : ''
this.formData.clubId = ''
this.formData.clubName = ''
return true
}
if (this.formData.reserveType === 'club') {
if (!this.clubOptions.length) {
await this.loadManagedClubs()
}
const club = this.clubOptions.find(item => item.id === this.formData.clubId)
this.formData.clubName = club ? club.clubName : ''
this.formData.applyUnionId = ''
this.formData.applyUnionName = ''
return true
}
return false
},
normalizeTime(timeStr) {
if (!timeStr) {
return '00:00:00'
}
return timeStr.length === 5 ? timeStr + ':00' : timeStr
},
isDisabledDate(time) {
const day = this.$moment(time).format('YYYY-MM-DD')
const today = this.$moment().startOf('day')
const currentDay = this.$moment(day)
if (currentDay.isBefore(today)) {
return true
}
if (currentDay.day() === 0 || currentDay.day() === 6) {
return true
}
return this.timeLimitConfig.filterHolidays && this.timeLimitConfig.holidayList.includes(day)
},
getDisabledRangesByDay(dayStr) {
const list = Array.isArray(this.timeLimitConfig.notApplyTimeList) ? this.timeLimitConfig.notApplyTimeList : []
return list
.filter(item => item && item.date && this.$moment(item.date).format('YYYY-MM-DD') === dayStr)
.sort((a, b) => this.normalizeTime(a.startTime).localeCompare(this.normalizeTime(b.startTime)))
},
isDateTimeBlocked(dateTimeStr) {
if (!dateTimeStr) {
return false
}
const target = this.$moment(dateTimeStr)
const dayStr = target.format('YYYY-MM-DD')
if (target.day() === 0 || target.day() === 6) {
return true
}
if (this.timeLimitConfig.filterHolidays && this.timeLimitConfig.holidayList.includes(dayStr)) {
return true
}
const disabledRanges = this.getDisabledRangesByDay(dayStr)
return disabledRanges.some(item => {
const start = this.$moment(dayStr + ' ' + this.normalizeTime(item.startTime))
const end = this.$moment(dayStr + ' ' + this.normalizeTime(item.endTime))
return target.isSameOrAfter(start) && target.isBefore(end)
})
},
hasHolidayInRange(start, end) {
const current = start.clone().startOf('day')
const endDay = end.clone().startOf('day')
while (current.isSameOrBefore(endDay)) {
if (current.day() === 0 || current.day() === 6) {
return true
}
if (this.timeLimitConfig.filterHolidays && this.timeLimitConfig.holidayList.includes(current.format('YYYY-MM-DD'))) {
return true
}
current.add(1, 'day')
}
return false
},
overlapsDisabledRange(start, end) {
const list = Array.isArray(this.timeLimitConfig.notApplyTimeList) ? this.timeLimitConfig.notApplyTimeList : []
return list.some(item => {
if (!item || !item.date || !item.startTime || !item.endTime) {
return false
}
const rangeStart = this.$moment(this.$moment(item.date).format('YYYY-MM-DD') + ' ' + this.normalizeTime(item.startTime))
const rangeEnd = this.$moment(this.$moment(item.date).format('YYYY-MM-DD') + ' ' + this.normalizeTime(item.endTime))
return start.isBefore(rangeEnd) && end.isAfter(rangeStart)
})
},
showSubmitError(message) {
return this.$dialog.alert({
title: '提示',
message: message || '提交失败',
}).catch(() => {})
},
validateTimeLimit(showMessage = true, useDialog = false, reserveStartTime = this.formData.reserveStartTime, reserveEndTime = this.formData.reserveEndTime) {
if (!reserveStartTime || !reserveEndTime) {
return true
}
const start = this.$moment(reserveStartTime)
const end = this.$moment(reserveEndTime)
const showError = (message) => {
if (!showMessage) {
return
}
if (useDialog) {
this.showSubmitError(message)
return
}
this.$toast(message)
}
if (!start.isValid() || !end.isValid()) {
showError('请选择正确的预约时间')
return false
}
if (!end.isAfter(start)) {
showError('预约结束时间必须晚于开始时间')
return false
}
if (start.format('YYYY-MM-DD') !== end.format('YYYY-MM-DD')) {
showError('预约时间必须在同一天内')
return false
}
if (this.isDateTimeBlocked(reserveStartTime) || this.isDateTimeBlocked(reserveEndTime)) {
showError('预约时间不能选择周末、节假日或禁用时间')
return false
}
if (this.hasHolidayInRange(start, end)) {
showError('预约时间范围内包含周末或节假日,请重新选择')
return false
}
if (this.overlapsDisabledRange(start, end)) {
showError('预约时间范围与禁用时间冲突,请重新选择')
return false
}
return true
},
openYearlyEndDatePicker() {
if (!this.yearlyReserve) {
return
}
this.syncYearlyReserveEndDate()
const current = this.formData.yearlyReserveEndDate
? this.$moment(this.formData.yearlyReserveEndDate, 'YYYY-MM-DD')
: this.$moment(this.getDefaultYearlyReserveEndDate(), 'YYYY-MM-DD')
this.yearlyReservePickerDate = current.isValid() ? current.toDate() : new Date()
this.showYearlyEndDatePicker = true
},
onYearlyReserveEndDateConfirm(value) {
this.showYearlyEndDatePicker = false
this.yearlyReservePickerDate = value
this.$set(this.formData, 'yearlyReserveEndDate', this.$moment(value).format('YYYY-MM-DD'))
},
validateYearlyReserve() {
if (!this.yearlyReserve) {
return true
}
if (!this.formData.yearlyReserveEndDate) {
this.$toast('请选择批量预约截止日期')
return false
}
if (this.formData.reserveStartTime && this.formData.yearlyReserveEndDate < this.formData.reserveStartTime.slice(0, 10)) {
this.$toast('批量预约截止日期不能早于开始日期')
return false
}
if (!this.yearlyReserveDates.length) {
this.$toast('当前截止日期内没有可批量预约的同星期时段')
return false
}
return true
},
buildSubmitApi() {
return this.yearlyReserve ? '/platform/siteCug/apply/submitYearly' : '/platform/siteCug/apply/submit'
},
buildSubmitConfirmMessage() {
const baseText = '您已选择 ' + this.formData.reserveStartTime + ' 至 ' + this.formData.reserveEndTime
if (!this.yearlyReserve) {
return baseText + ',是否确定选择并提交预约?'
}
return baseText + '。系统将继续预约到 ' + this.formData.yearlyReserveEndDate + ',是否确定选择并提交预约?'
},
async onSubmit() {
this.$refs.formRef.validate().then(async () => {
if (!this.formData.reserveType) {
await this.showSubmitError('请选择预约类型')
return
}
await this.normalizeReserveTypeData()
if (this.formData.reserveType === 'union' && !this.formData.applyUnionId) {
await this.showSubmitError('当前用户未关联分工会,不能发起分工会预约')
return
}
if (this.formData.reserveType === 'club' && !this.formData.clubId) {
await this.showSubmitError('请选择您管理的协会')
return
}
if (!this.formData.reserveStartTime || !this.formData.reserveEndTime) {
await this.showSubmitError('请选择预约时间段')
return
}
const start = this.$moment(this.formData.reserveStartTime)
const end = this.$moment(this.formData.reserveEndTime)
if (!start.isAfter(this.$moment())) {
await this.showSubmitError('预约开始时间必须晚于当前时间')
return
}
if (!end.isAfter(start)) {
await this.showSubmitError('预约结束时间必须晚于开始时间')
return
}
if (!this.validateGraphSelection()) {
return
}
if (!this.validateTimeLimit(true, true)) {
return
}
if (!this.validateYearlyReserve()) {
return
}
this.$dialog.confirm({
title: '提示',
message: this.buildSubmitConfirmMessage(),
}).then(() => {
const loading = this.$toast.loading({
message: '提交中...',
forbidClick: true,
overlay: true,
duration: 0,
})
this.$axios.post(this.buildSubmitApi(), {
data: JSON.stringify(this.formData),
}).then((res) => {
if (res.code === 0) {
this.$toast.success(res.msg || '提交成功')
this.$pjaxReplace('/platform/siteCug/apply/h5')
} else {
this.showSubmitError(res.msg || '提交失败')
}
}).catch((err) => {
this.showSubmitError((err && err.msg) || '提交失败')
}).finally(() => {
loading.close()
})
}).catch(() => {})
}).catch(() => {})
},
async queryTimeLimitConfig() {
const fallback = {
filterHolidays: !!this.row.filterHolidays,
holidayList: [],
notApplyTimeList: Array.isArray(this.row.notApplyTimeList) ? this.row.notApplyTimeList : [],
}
try {
const res = await this.$axios.post('/platform/siteCug/apply/timeLimitConfig', {
siteId: this.row.id,
})
if (res.code === 0 && res.data) {
this.timeLimitConfig = {
filterHolidays: !!res.data.filterHolidays,
holidayList: Array.isArray(res.data.holidayList) ? res.data.holidayList : [],
notApplyTimeList: Array.isArray(res.data.notApplyTimeList) ? res.data.notApplyTimeList : [],
}
return
}
} catch (e) {
}
this.timeLimitConfig = fallback
},
async init() {
if (!this.siteId) {
this.$toast('未获取到场地信息')
this.historyBack()
return
}
let siteRes
try {
siteRes = await this.$axios.post('/platform/siteCug/manage/info', { id: this.siteId })
} catch (e) {
this.$toast('场地信息加载失败')
this.historyBack()
return
}
if (siteRes.code !== 0 || !siteRes.data) {
this.$toast(siteRes.msg || '场地信息加载失败')
this.historyBack()
return
}
this.row = siteRes.data
this.formData = this.buildDefaultFormData()
this.syncYearlyReserveEndDate(true)
await this.queryTimeLimitConfig()
if (this.formData.reserveType) {
await this.reserveTypeChange(this.formData.reserveType)
}
this.scheduleDate = this.getInitialScheduleDate()
this.schedulePickerDate = this.$moment(this.scheduleDate, 'YYYY-MM-DD').toDate()
await this.queryAvailability(this.scheduleDate)
this.siteLoaded = true
},
},
created() {
this.init()
}
})
@@ -0,0 +1,583 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style scoped>
.page-container {
padding: 12px 12px 96px;
background: #f5f7fb;
min-height: calc(100vh - 46px);
box-sizing: border-box;
}
.panel-card {
margin-bottom: 12px;
overflow: hidden;
border-radius: 22px;
background: #ffffff;
box-shadow:
0 2px 6px rgba(15, 23, 42, 0.04),
0 12px 28px rgba(15, 23, 42, 0.08),
0 0 0 1px rgba(226, 232, 240, 0.95);
}
.notice-card__header {
width: 100%;
padding: 16px;
border: none;
background: #ffffff;
display: flex;
align-items: center;
justify-content: space-between;
color: #0f172a;
font-size: 16px;
font-weight: 700;
line-height: 1.4;
box-sizing: border-box;
}
.notice-card__icon {
color: #64748b;
font-size: 16px;
transition: transform 0.2s ease;
}
.notice-card__icon--expanded {
transform: rotate(180deg);
}
.notice-card__body {
padding: 0 16px 14px;
border-top: 1px solid #eef2f7;
}
.notice-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.notice-item {
padding: 10px 12px;
border-radius: 14px;
background: #f8fafc;
}
.notice-item__label {
color: #334155;
font-size: 13px;
font-weight: 600;
line-height: 1.5;
}
.notice-item__value {
margin-top: 4px;
color: #64748b;
font-size: 12px;
line-height: 1.7;
}
.disabled-time-list {
display: flex;
flex-direction: column;
gap: 6px;
}
.disabled-time-item {
padding: 8px 10px;
border-radius: 10px;
background: #ffffff;
color: #475569;
line-height: 1.7;
}
.form-container {
margin: 0;
}
.section-tip {
padding: 0 16px 14px;
color: #94a3b8;
font-size: 12px;
line-height: 1.6;
}
.selection-summary {
margin: 0 16px 14px;
padding: 10px 12px;
border-radius: 12px;
background: rgba(37, 99, 235, 0.08);
color: #2563eb;
font-size: 12px;
line-height: 1.7;
}
.batch-panel {
padding: 0 16px 14px;
}
.batch-summary {
margin-top: 10px;
padding: 10px 12px;
border-radius: 12px;
background: rgba(37, 99, 235, 0.08);
color: #2563eb;
font-size: 12px;
line-height: 1.7;
}
.availability-popup {
height: 82vh;
display: flex;
flex-direction: column;
background: #f8fafc;
}
.availability-popup__header {
display: flex;
align-items: flex-start;
justify-content: space-between;
padding: 16px;
background: #ffffff;
border-bottom: 1px solid #eef2f7;
}
.availability-popup__title {
color: #0f172a;
font-size: 17px;
font-weight: 700;
line-height: 1.4;
}
.availability-popup__subtitle {
margin-top: 4px;
color: #64748b;
font-size: 12px;
line-height: 1.6;
}
.availability-popup__close {
color: #94a3b8;
font-size: 18px;
line-height: 1;
}
.availability-popup__tabs {
display: flex;
gap: 8px;
padding: 12px 16px 0;
overflow-x: auto;
background: #f8fafc;
}
.availability-tab {
min-width: 72px;
height: 34px;
padding: 0 14px;
border: 1px solid #cbd5e1;
border-radius: 999px;
background: #ffffff;
color: #475569;
font-size: 13px;
font-weight: 500;
box-sizing: border-box;
}
.availability-tab--active {
border-color: #2563eb;
background: #2563eb;
color: #ffffff;
box-shadow: 0 8px 16px rgba(37, 99, 235, 0.22);
}
.availability-popup__legend {
display: flex;
flex-wrap: wrap;
gap: 12px;
padding: 12px 16px 0;
color: #64748b;
font-size: 12px;
line-height: 1.5;
}
.availability-legend__item {
display: inline-flex;
align-items: center;
gap: 6px;
}
.availability-legend__dot {
width: 10px;
height: 10px;
border-radius: 999px;
display: inline-block;
}
.availability-legend__dot--available {
background: #22c55e;
}
.availability-legend__dot--reserved {
background: #9ca3af;
}
.availability-legend__dot--selected {
background: #2563eb;
}
.availability-legend__dot--closed {
background: #e5e7eb;
}
.availability-popup__body {
flex: 1;
overflow-y: auto;
padding: 16px;
box-sizing: border-box;
}
.slot-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
}
.slot-block {
min-height: 48px;
padding: 0 12px;
border: 1px solid #dbe4ee;
border-radius: 14px;
background: #ffffff;
color: #475569;
display: inline-flex;
align-items: center;
justify-content: center;
text-align: center;
font-size: 13px;
font-weight: 500;
line-height: 1.4;
box-sizing: border-box;
}
.slot-block--available {
border-color: #86efac;
background: rgba(34, 197, 94, 0.1);
color: #15803d;
}
.slot-block--reserved {
background: #e5e7eb;
color: #6b7280;
}
.slot-block--closed {
background: #f8fafc;
color: #94a3b8;
}
.slot-block--selected {
border-color: #2563eb;
background: #2563eb;
color: #ffffff;
box-shadow: 0 10px 20px rgba(37, 99, 235, 0.24);
}
.availability-popup__footer {
padding: 12px 16px calc(env(safe-area-inset-bottom) + 12px);
background: #ffffff;
border-top: 1px solid #eef2f7;
}
.availability-popup__summary {
margin-bottom: 12px;
color: #475569;
font-size: 12px;
line-height: 1.7;
}
.availability-popup__actions {
display: flex;
gap: 10px;
}
.availability-popup__actions .van-button {
flex: 1;
}
.footer-actions {
position: fixed;
left: 0;
right: 0;
bottom: 0;
display: flex;
gap: 12px;
padding: 10px 12px calc(env(safe-area-inset-bottom) + 10px);
background: rgba(255, 255, 255, 0.96);
backdrop-filter: blur(10px);
box-shadow: 0 -8px 24px rgba(15, 23, 42, 0.08);
}
.footer-actions .van-button {
flex: 1;
height: 44px;
}
/deep/ .panel-card .van-cell-group {
background: transparent;
}
/deep/ .panel-card .van-cell-group__title {
padding: 16px 16px 8px;
margin: 0;
color: #0f172a;
font-size: 16px;
font-weight: 700;
line-height: 1.4;
background: #ffffff;
}
/deep/ .panel-card .van-cell {
padding-top: 12px;
padding-bottom: 12px;
}
/deep/ .panel-card .van-field__label,
/deep/ .panel-card .van-cell__title {
color: #334155;
font-weight: 600;
}
/deep/ .direction-column-cell .van-cell__value {
white-space: normal;
text-align: left;
}
</style>
<div id="app" v-cloak>
<van-nav-bar title="场馆申请" left-text="返回" left-arrow @click-left="historyBack" fixed placeholder></van-nav-bar>
<div class="page-container" v-if="siteLoaded">
<div class="panel-card">
<button type="button" class="notice-card__header" @click="noticeCollapsed = !noticeCollapsed">
<span>预约须知</span>
<van-icon :class="['notice-card__icon', noticeCollapsed ? '' : 'notice-card__icon--expanded']" name="arrow-down"></van-icon>
</button>
<div v-if="!noticeCollapsed" class="notice-card__body">
<div class="notice-list">
<div class="notice-item">
<div class="notice-item__label">可预约日期</div>
<div class="notice-item__value">仅限工作日</div>
</div>
<div class="notice-item">
<div class="notice-item__label">节假日限制</div>
<div class="notice-item__value">{{ timeLimitConfig.filterHolidays ? '节假日不可预约' : '不排除节假日' }}</div>
</div>
<div class="notice-item">
<div class="notice-item__label">禁用时段</div>
<div class="notice-item__value">
<div v-if="timeLimitConfig.notApplyTimeList.length" class="disabled-time-list">
<div
class="disabled-time-item"
v-for="(item, index) in timeLimitConfig.notApplyTimeList"
:key="item.date + item.startTime + item.endTime + index">
{{ item.date }} {{ item.startTime }} - {{ item.endTime }}
</div>
</div>
<div v-else>暂无禁用时段</div>
</div>
</div>
</div>
</div>
</div>
<van-form ref="formRef" class="form-container" :show-error-message="false">
<div class="panel-card">
<van-cell-group title="申请信息">
<van-field
:value="reserveTypeText"
label="预约类型"
name="reserveType"
readonly
clickable
is-link
required
placeholder="请选择预约类型"
@click="openReserveTypePicker"
:rules="[{ required: true, message: '请选择预约类型' }]">
</van-field>
<van-field v-model="formData.applyUserName" label="预约人" name="applyUserName" readonly
:rules="[{ required: true, message: '请确认预约人' }]"></van-field>
<van-field v-model="formData.applyLoginName" label="工号" name="applyLoginName" readonly
:rules="[{ required: true, message: '请确认工号' }]"></van-field>
<van-field v-model="formData.applyUnitName" label="所属单位" name="applyUnitName" readonly
:rules="[{ required: true, message: '请确认所属单位' }]"></van-field>
<van-field
v-if="formData.reserveType === 'union'"
v-model="formData.applyUnionName"
label="分工会"
name="applyUnionName"
readonly
placeholder="自动读取当前登录人的分工会">
</van-field>
<van-field
v-if="formData.reserveType === 'club'"
v-model="formData.clubName"
label="协会"
name="clubName"
readonly
clickable
is-link
required
placeholder="请选择您管理的协会"
@click="openClubPicker"
:rules="[{ required: true, message: '请选择您管理的协会' }]">
</van-field>
<van-field v-model="formData.applyMobile" label="联系电话" name="applyMobile" type="tel" maxlength="11"
required placeholder="请输入联系电话"
:rules="[{ required: true, message: '请输入联系电话' }]"></van-field>
<van-field
:value="scheduleDate"
label="预约日期"
name="scheduleDate"
readonly
clickable
is-link
required
placeholder="请选择预约日期"
@click="openScheduleDatePicker">
</van-field>
<van-field
:value="selectionSummary || ''"
label="预约时段"
name="timeSelection"
readonly
clickable
is-link
required
:placeholder="scheduleDate ? '请选择预约时段' : '请先选择预约日期'"
@click="openAvailabilityPopup">
</van-field>
<div v-if="selectionSummary" class="selection-summary">当前已选:{{ selectionSummary }}</div>
<van-cell title="批量预约">
<template #default>
<van-switch v-model="yearlyReserve" size="22px"></van-switch>
</template>
</van-cell>
<div v-if="yearlyReserve" class="batch-panel">
<div class="section-tip">将自动预约截止日期内每周同一时段,默认到本月底。</div>
<van-field
:value="formData.yearlyReserveEndDate"
label="截至日期"
name="yearlyReserveEndDate"
readonly
clickable
is-link
required
placeholder="请选择批量预约截止日期"
@click="openYearlyEndDatePicker">
</van-field>
<div v-if="batchReserveSummary" class="batch-summary">{{ batchReserveSummary }}</div>
</div>
<van-field v-model="formData.applyCause" label="预约事由" name="applyCause" required rows="4" autosize
type="textarea" maxlength="1000" show-word-limit placeholder="请输入预约事由"
:rules="[{ required: true, message: '请输入预约事由' }]"></van-field>
</van-cell-group>
</div>
</van-form>
</div>
<div class="footer-actions" v-if="siteLoaded">
<van-button plain round type="info" @click="historyBack">取消</van-button>
<van-button round type="primary" color="#246fb4" @click="onSubmit">提交预约</van-button>
</div>
<van-popup v-model="showReserveTypePicker" position="bottom" round>
<van-picker show-toolbar :columns="reserveTypeColumns" @confirm="onReserveTypeConfirm"
@cancel="showReserveTypePicker = false"></van-picker>
</van-popup>
<van-popup v-model="showClubPicker" position="bottom" round>
<van-picker show-toolbar :columns="clubColumns" @confirm="onClubConfirm"
@cancel="showClubPicker = false"></van-picker>
</van-popup>
<van-popup v-model="showScheduleDatePicker" position="bottom" round>
<van-datetime-picker
v-model="schedulePickerDate"
type="date"
title="选择预约日期"
:min-date="scheduleMinDate"
:max-date="scheduleMaxDate"
@confirm="onScheduleDateConfirm"
@cancel="showScheduleDatePicker = false">
</van-datetime-picker>
</van-popup>
<van-popup v-model="showAvailabilityPopup" position="bottom" round class="availability-popup">
<div class="availability-popup__header">
<div>
<div class="availability-popup__title">选择预约时段</div>
<div class="availability-popup__subtitle">{{ scheduleDate || '请选择预约日期' }} {{ reserveModeText }}</div>
</div>
<van-icon class="availability-popup__close" name="cross" @click="showAvailabilityPopup = false"></van-icon>
</div>
<div v-if="timeGroupTabs.length" class="availability-popup__tabs">
<button
v-for="tab in timeGroupTabs"
:key="tab.key"
type="button"
:class="['availability-tab', activeTimeGroupKey === tab.key ? 'availability-tab--active' : '']"
@click="activeTimeGroupKey = tab.key">
{{ tab.label }}
</button>
</div>
<div class="availability-popup__legend">
<span class="availability-legend__item"><i class="availability-legend__dot availability-legend__dot--available"></i>可预约</span>
<span class="availability-legend__item"><i class="availability-legend__dot availability-legend__dot--reserved"></i>已预约</span>
<span class="availability-legend__item"><i class="availability-legend__dot availability-legend__dot--selected"></i>已选择</span>
<span class="availability-legend__item"><i class="availability-legend__dot availability-legend__dot--closed"></i>不可预约</span>
</div>
<div class="availability-popup__body">
<van-loading v-if="availabilityLoading" vertical>时段加载中</van-loading>
<van-empty v-else-if="!availabilityBlocks.length" description="当前日期暂无可选时段"></van-empty>
<van-empty v-else-if="!displayedAvailabilityBlocks.length" description="当前标签暂无可选时段"></van-empty>
<div v-else class="slot-grid">
<button
v-for="block in displayedAvailabilityBlocks"
:key="block.key"
type="button"
:class="getBlockClass(block)"
@click="onAvailabilityBlockClick(block)">
<span>{{ block.label }}</span>
</button>
</div>
</div>
<div class="availability-popup__footer">
<div class="availability-popup__summary">{{ selectionSummary || '请选择一个或多个连续时段,不可跨越间隔选择。' }}</div>
<div class="availability-popup__actions">
<van-button plain round type="default" @click="clearSelection(true)">清空</van-button>
<van-button round type="primary" color="#246fb4" @click="confirmAvailabilitySelection">确定</van-button>
</div>
</div>
</van-popup>
<van-popup v-model="showYearlyEndDatePicker" position="bottom" round>
<van-datetime-picker
v-model="yearlyReservePickerDate"
type="date"
title="选择批量预约截止日期"
:min-date="yearlyReserveMinDate"
:max-date="yearlyReserveMaxDate"
@confirm="onYearlyReserveEndDateConfirm"
@cancel="showYearlyEndDatePicker = false">
</van-datetime-picker>
</van-popup>
</div>
<script nonce="${cspNonce!}">
<!--#include('apply.js'){}#-->
</script>
<!--#
}
#-->
@@ -0,0 +1,474 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style scoped>
.site-page {
background: #f5f7fb;
min-height: 100vh;
}
.toolbar-wrap {
padding: 10px 10px 8px;
background: #f5f7fb;
box-shadow: 0 8px 18px rgba(15, 23, 42, 0.04);
}
.search-toolbar {
display: flex;
align-items: center;
gap: 8px;
}
.search-panel {
flex: 1;
min-width: 0;
border-radius: 16px;
overflow: hidden;
background: #ffffff;
box-shadow: 0 4px 14px rgba(15, 23, 42, 0.06);
}
.search-panel /deep/ .van-search {
padding: 0;
background: transparent;
}
.search-panel /deep/ .van-search__content {
height: 42px;
background: #ffffff;
border-radius: 16px;
}
.toolbar-btn,
.toolbar-icon-btn {
height: 42px;
border: none;
border-radius: 14px;
background: #ffffff;
box-shadow: 0 4px 14px rgba(15, 23, 42, 0.08);
display: inline-flex;
align-items: center;
justify-content: center;
color: #2563eb;
box-sizing: border-box;
}
.toolbar-btn {
min-width: 62px;
padding: 0 14px;
font-size: 14px;
font-weight: 600;
}
.toolbar-icon-btn {
width: 42px;
font-size: 18px;
}
.selected-type-bar {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 8px;
padding: 8px 12px;
border-radius: 14px;
background: rgba(37, 99, 235, 0.08);
color: #2563eb;
font-size: 13px;
line-height: 1.4;
}
.selected-type-text {
min-width: 0;
flex: 1;
padding-right: 12px;
font-weight: 500;
}
.selected-type-clear {
border: none;
padding: 0;
background: transparent;
color: #2563eb;
font-size: 13px;
font-weight: 600;
}
.type-popup {
width: calc(100vw - 48px);
max-width: 320px;
padding: 16px 16px 14px;
box-sizing: border-box;
}
.type-popup__header {
color: #111827;
font-size: 16px;
font-weight: 700;
line-height: 1.4;
}
.type-popup__list {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 14px;
}
.type-popup__item {
min-width: calc(50% - 5px);
min-height: 38px;
padding: 0 12px;
border: 1px solid #dbe4f0;
border-radius: 12px;
background: #ffffff;
color: #475569;
display: inline-flex;
align-items: center;
justify-content: space-between;
gap: 8px;
font-size: 13px;
line-height: 1.2;
box-sizing: border-box;
}
.type-popup__item--active {
border-color: #2563eb;
background: rgba(37, 99, 235, 0.08);
color: #2563eb;
font-weight: 600;
}
.site-card-header {
display: flex;
gap: 12px;
align-items: flex-start;
margin-bottom: 10px;
}
.site-thumb {
width: 92px;
height: 92px;
flex-shrink: 0;
overflow: hidden;
border-radius: 14px;
background: linear-gradient(135deg, #e5eefb 0%, #f5f9ff 100%);
box-shadow: inset 0 0 0 1px rgba(37, 99, 235, 0.06);
}
.site-thumb img,
.site-thumb .van-image {
width: 100%;
height: 100%;
display: block;
}
.site-thumb-empty {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
color: #94a3b8;
font-size: 12px;
text-align: center;
line-height: 1.4;
padding: 8px;
box-sizing: border-box;
}
.site-head-main {
min-width: 0;
flex: 1;
padding-top: 2px;
}
.site-name {
color: #111827;
font-size: 18px;
font-weight: 700;
line-height: 1.35;
word-break: break-all;
}
.site-address {
display: flex;
align-items: center;
gap: 4px;
margin-top: 8px;
color: #6b7280;
font-size: 13px;
line-height: 1.4;
}
.site-address i {
color: #9ca3af;
font-size: 12px;
}
.site-tags {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 10px;
}
.site-tag {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 24px;
padding: 0 8px;
border-radius: 8px;
border: 1px solid #93c5fd;
color: #2563eb;
font-size: 12px;
line-height: 1;
background: #ffffff;
box-sizing: border-box;
}
.site-tag--solid {
border-color: #2563eb;
background: #2563eb;
color: #ffffff;
}
.site-tag--muted {
border-color: #cbd5e1;
color: #64748b;
}
.site-timeline {
margin-top: 4px;
}
.timeline-bar {
display: grid;
grid-template-columns: repeat(48, minmax(0, 1fr));
gap: 1px;
align-items: center;
padding: 0 2px;
}
.timeline-bar__segment {
height: 4px;
border-radius: 999px;
background: #e5e7eb;
}
.timeline-bar__segment--available {
background: #22c55e;
}
.timeline-bar__segment--reserved {
background: #9ca3af;
}
.timeline-scale {
display: flex;
justify-content: space-between;
margin-top: 8px;
padding: 0 2px;
color: #64748b;
font-size: 11px;
line-height: 1;
}
/deep/ .table-list-container {
margin-top: 8px;
padding: 0 8px 12px;
}
/deep/ .table-list-container .table-list-item {
margin-bottom: 12px;
padding: 14px;
border: none;
overflow: hidden;
border-radius: 22px;
background: #ffffff;
box-shadow:
0 2px 6px rgba(15, 23, 42, 0.05),
0 10px 24px rgba(15, 23, 42, 0.08),
0 0 0 1px rgba(226, 232, 240, 0.9);
}
/deep/ .table-list-container .table-list-item .item-actions {
margin-top: 12px;
padding-top: 10px;
border-top: 1px solid #eef2f7;
}
</style>
<div id="app" class="site-page">
<van-nav-bar title="场馆预约" left-text="返回" left-arrow @click-left="historyBack" fixed placeholder></van-nav-bar>
<van-sticky offset-top="46px">
<div class="toolbar-wrap">
<div class="search-toolbar">
<div class="search-panel">
<van-search
v-model="pageForm.searchKeyword"
:show-action="false"
:reverse-color="false"
input-align="left"
placeholder="请输入名称或地址搜索"
@search="doSearch"
></van-search>
</div>
<button type="button" class="toolbar-btn" @click="doSearch">搜索</button>
<button type="button" class="toolbar-icon-btn" @click="typePopupVisible = true">
<van-icon name="apps-o"></van-icon>
</button>
</div>
<div v-if="selectedTypeText" class="selected-type-bar">
<div class="selected-type-text">已选场地类型:{{ selectedTypeText }}</div>
<button type="button" class="selected-type-clear" @click="selectType(null)">清除</button>
</div>
</div>
</van-sticky>
<table-list api="/platform/siteCug/apply/pageData" :page_form.sync="pageForm" ref="tableListRef" @ready="onReady">
<template #header="{ row }">
<div class="site-card-header">
<div class="site-thumb" @click="previewSitePhoto(row)">
<van-image v-if="row.sitePhoto" :src="row.sitePhoto" fit="cover"></van-image>
<div v-else class="site-thumb-empty">暂无场地照片</div>
</div>
<div class="site-head-main">
<div class="site-name">{{ row.name || '-' }}</div>
<div class="site-address">
<i class="fa fa-map-marker"></i>
<span>{{ row.address || '-' }}</span>
</div>
<div class="site-tags">
<span class="site-tag site-tag--solid">{{ row.typeName || '活动场地' }}</span>
<span class="site-tag">容{{ row.maxNum || '-' }}</span>
<span :class="['site-tag', row.state ? '' : 'site-tag--muted']">{{ row.state ? '已开启' : '未开启' }}</span>
</div>
</div>
</div>
</template>
<template v-slot="{ row }">
<div class="site-timeline">
<div class="timeline-bar">
<span
v-for="(segment, index) in row.timelineSegments || []"
:key="row.id + '-segment-' + index"
:class="['timeline-bar__segment', 'timeline-bar__segment--' + (segment.status || 'closed')]">
</span>
</div>
<div class="timeline-scale">
<span v-for="mark in timeMarks" :key="row.id + '-mark-' + mark">{{ mark }}</span>
</div>
</div>
</template>
<template #actions="{ row }">
<div class="action-btn" @click="onView(row)">
<i class="fa fa-eye"></i>
<span>查看</span>
</div>
<div class="action-btn" @click="onApply(row)">
<i class="fa fa-edit"></i>
<span>预约</span>
</div>
</template>
</table-list>
<van-popup v-model="typePopupVisible" round class="type-popup">
<div class="type-popup__header">选择场地类型</div>
<div class="type-popup__list">
<button
v-for="item in typeFilterOptions"
:key="String(item.value)"
type="button"
:class="['type-popup__item', pageForm.type === item.value ? 'type-popup__item--active' : '']"
@click="selectType(item.value)">
<span>{{ item.text }}</span>
<van-icon v-if="pageForm.type === item.value" name="success"></van-icon>
</button>
</div>
</van-popup>
<info ref="infoRef"></info>
</div>
<script nonce="${cspNonce!}">
<!--#include('../common/siteInfo.js'){}#-->
new Vue({
el: '#app',
store,
components: {
info: siteInfo,
},
data() {
return {
pageForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
searchKeyword: '',
type: null,
},
typeFilterOptions: [],
typePopupVisible: false,
timeMarks: ['0', '2', '4', '6', '8', '10', '12', '14', '16', '18', '20', '22'],
}
},
computed: {
selectedTypeText() {
const current = this.typeFilterOptions.find((item) => item.value === this.pageForm.type)
return current && current.value !== null ? current.text : ''
},
},
methods: {
historyBack,
previewSitePhoto(row) {
if (!row || !row.sitePhoto) {
return
}
this.vant.ImagePreview([row.sitePhoto])
},
onView(row) {
this.$refs.infoRef.onOpen(row)
},
onApply(row) {
this.$pjaxReplace('/platform/siteCug/apply/form/h5?siteId=' + row.id)
},
async onReady() {
const typeList = await this.querySiteType()
this.typeFilterOptions = [
{
text: '全部类型',
value: null,
}
].concat(typeList.map((item) => ({ text: item.name, value: item.id })))
if (this.typeFilterOptions.length > 0) {
this.pageForm.type = this.typeFilterOptions[0].value
this.doSearch()
}
},
async querySiteType() {
const res = await this.$axios.post('/platform/siteCug/function/type/queryFunctionType')
return Array.isArray(res.data) ? res.data : []
},
selectType(value) {
this.pageForm.type = value
this.typePopupVisible = false
this.doSearch()
},
doSearch() {
this.$nextTick(() => {
this.pageForm.pageNumber = 1
this.pageForm.totalCount = 0
this.$refs.tableListRef.doSearch()
})
},
},
})
</script>
<!--#
}
#-->
@@ -0,0 +1,80 @@
const siteCugApplyInfoH5 = {
template:
/*language=HTML*/
`
<van-action-sheet v-model="visible" :title="sheetTitle">
<div class="detail-container">
<van-cell-group title="基本信息">
<van-cell title="预约场地">{{ viewData.siteName }}</van-cell>
<van-cell title="预约人">{{ viewData.applyUserName }}</van-cell>
<van-cell title="预约人工号">{{ viewData.applyLoginName }}</van-cell>
<van-cell title="所属单位">{{ viewData.applyUnitName }}</van-cell>
<van-cell title="预约类型">{{ reserveTypeLabel }}</van-cell>
<van-cell title="预约主体">{{ reserveTargetName }}</van-cell>
<van-cell title="开始时间">{{ viewData.reserveStartTime }}</van-cell>
<van-cell title="结束时间">{{ viewData.reserveEndTime }}</van-cell>
<van-cell title="联系电话">{{ viewData.applyMobile }}</van-cell>
<van-cell title="预约事由" class="direction-column-cell">{{ viewData.applyCause }}</van-cell>
</van-cell-group>
<template v-for="task in doneTasks">
<van-cell-group :title="task.displayName" v-if="task.ext.isFirstTaskNode" :key="task.id + '-first'">
<van-cell title="申请人">{{ task.ext.initiatorName }}({{task.ext.initiatorAccount}})</van-cell>
<van-cell title="申请时间">{{ task.finishTime }}</van-cell>
<van-cell title="办理结果">
<dict-tag :options="dict.type.PROCESS_TASK_SUBMIT_TYPE" :value="task.ext.submitType"></dict-tag>
</van-cell>
</van-cell-group>
<van-cell-group :title="task.displayName" v-else :key="task.id + '-done'">
<van-cell title="办理人">{{ task.taskFormData.userName }}({{task.taskFormData.loginName}})</van-cell>
<van-cell title="办理时间">{{ task.finishTime }}</van-cell>
<van-cell title="办理结果">
<dict-tag :options="dict.type.PROCESS_TASK_SUBMIT_TYPE" :value="task.ext.submitType"></dict-tag>
</van-cell>
<van-cell title="办理意见" class="direction-column-cell">
<div v-html="task.taskFormData.opinion || task.taskFormData.tf_opinion || '-' "></div>
</van-cell>
</van-cell-group>
</template>
</div>
<slot></slot>
</van-action-sheet>
`,
dicts: ['PROCESS_TASK_SUBMIT_TYPE'],
data() {
return {
visible: false,
viewData: {},
doneTasks: [],
row: null,
}
},
computed: {
reserveTypeLabel() {
return this.viewData.reserveType === 'club' ? '协会预约' : '分工会预约'
},
reserveTargetName() {
return this.viewData.reserveType === 'club' ? (this.viewData.clubName || '-') : (this.viewData.applyUnionName || '-')
},
sheetTitle() {
return this.visible && this.$slots.default ? '审核预约' : '预约详情'
},
},
methods: {
onOpen(row) {
this.row = row
this.visible = true
this.viewData = row
this.getDoneTasks()
},
onClose() {
this.visible = false
},
getDoneTasks() {
this.$axios.post('/flow/common/doneTasks', { bizId: this.row.id }).then((res) => {
if (res.code === 0) {
this.doneTasks = res.data
}
})
},
},
};
@@ -0,0 +1,142 @@
const siteInfo = {
template: /*language=HTML*/ `
<div>
<van-action-sheet v-model="visible" title="场馆信息">
<div class="detail-container">
<van-cell-group title="基本信息">
<van-cell title="创建人" :value="viewData.createUserName || '-' "></van-cell>
<van-cell title="场馆名称" :value="viewData.name || '-' "></van-cell>
<van-cell title="场地地址" class="direction-column-cell">
<template #default>
{{ viewData.address || '-' }}
</template>
</van-cell>
<van-cell title="联系人" :value="viewData.contactName || '-' "></van-cell>
<van-cell title="联系电话" :value="viewData.contactPhone || '-' "></van-cell>
<van-cell title="排序编号" :value="viewData.sortNum || '-' "></van-cell>
<van-cell title="容纳人数" :value="viewData.maxNum || '-' "></van-cell>
<van-cell title="场地类型" :value="viewData.typeName || '-' "></van-cell>
<van-cell title="性别限制">
<template #default>
<span v-if="viewData.sexLimit === 1">男</span>
<span v-else-if="viewData.sexLimit === 2">女</span>
<span v-else>不限制</span>
</template>
</van-cell>
<van-cell title="开启状态">
<template #default>
<span v-if="viewData.state">开启</span>
<span v-else>禁用</span>
</template>
</van-cell>
<van-cell title="排除节假日">
<template #default>
<span v-if="viewData.filterHolidays">是</span>
<span v-else>否</span>
</template>
</van-cell>
<van-cell title="场地介绍" class="direction-column-cell">
<template #default>
<div v-if="viewData.introduce" v-html="viewData.introduce"></div>
<div v-else>暂无场地介绍</div>
</template>
</van-cell>
</van-cell-group>
<van-cell-group title="禁用时间" v-if="viewData.notApplyTimeList && viewData.notApplyTimeList.length > 0">
<table class="table-class">
<thead>
<tr>
<th>日期</th>
<th>开始时间</th>
<th>结束时间</th>
</tr>
</thead>
<tbody>
<tr v-for="(item, index) in viewData.notApplyTimeList" :key="item.date + item.startTime + item.endTime + index">
<td>{{ item.date }}</td>
<td>{{ item.startTime }}</td>
<td>{{ item.endTime }}</td>
</tr>
</tbody>
</table>
</van-cell-group>
</div>
</van-action-sheet>
</div>
`,
data() {
return {
viewData: {},
visible: false,
}
},
methods: {
onOpen(row) {
this.viewData = row
this.visible = true
},
},
style: /*language=CSS*/ `
/deep/ .van-action-sheet {
border-top-left-radius: 24px;
border-top-right-radius: 24px;
overflow: hidden;
background: #f8fafc;
}
/deep/ .van-action-sheet__header {
font-size: 17px;
font-weight: 700;
color: #0f172a;
background: #ffffff;
}
/deep/ .detail-container {
padding: 10px 12px 18px;
background: #f8fafc;
}
/deep/ .detail-container .van-cell-group {
margin-bottom: 12px;
overflow: hidden;
border-radius: 18px;
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.06);
}
/deep/ .detail-container .van-cell-group__title {
padding: 12px 16px 8px;
color: #334155;
font-size: 14px;
font-weight: 700;
}
/deep/ .detail-container .van-cell {
min-height: 52px;
}
/deep/ .direction-column-cell .van-cell__value {
white-space: normal;
text-align: left;
}
/deep/ .table-class {
width: 100%;
border-radius: 10px;
overflow: hidden;
line-height: 1.8rem;
font-size: 13px;
table-layout: fixed;
border-collapse: collapse;
background: #ffffff;
}
/deep/ .table-class th {
padding: 10px 8px;
background-color: #f1f5f9;
border: 1px solid #dbe4ee;
color: #334155;
font-weight: 600;
}
/deep/ .table-class tr {
text-align: center;
border-bottom: 1px solid #dbe4ee;
}
/deep/ .table-class td {
padding: 10px 8px;
border: 1px solid #dbe4ee;
color: #475569;
}
`
}
@@ -0,0 +1,148 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style scoped>
</style>
<div id="app" v-cloak>
<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">
<van-dropdown-item v-model="pageForm.siteType" :options="typeOptions" :multiple="false" @change="doSearch"></van-dropdown-item>
<van-dropdown-item v-model="pageForm.siteId" :options="siteOptions" :multiple="false" @change="doSearch"></van-dropdown-item>
</van-dropdown-menu>
</van-sticky>
<table-list api="/platform/siteCug/mine/pageData" :page_form.sync="pageForm" ref="tableListRef" title="siteName" @ready="onReady">
<template v-slot="{index,row}">
<table-column label="预约人">{{row.applyUserName || '-'}}</table-column>
<table-column label="预约类型">{{row.reserveType === 'club' ? '协会预约' : '分工会预约'}}</table-column>
<table-column label="开始时间">{{row.reserveStartTime}}</table-column>
<table-column label="结束时间">{{row.reserveEndTime}}</table-column>
<table-column label="当前节点">{{row.taskName || '-'}}</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 delete" @click="onDelete(row)" v-if="row.canCancel">
<i class="fa fa-trash"></i>
<span>取消预约</span>
</div>
</template>
</table-list>
<info ref="infoRef"></info>
</div>
<script nonce="${cspNonce!}">
<!--#include('../common/applyInfo.js'){}#-->
new Vue({
el: '#app',
store,
components: {
info: siteCugApplyInfoH5,
},
data() {
return {
pageForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
siteType: null,
siteId: null,
},
typeOptions: [
{
text: '全部类型',
value: null,
}
],
siteOptions: [
{
text: '全部场地',
value: null,
}
],
}
},
methods: {
async onReady() {
await this.querySiteType()
await this.querySites()
this.doSearch()
},
onView(row) {
this.$refs.infoRef.onOpen(row)
},
onDelete(row) {
this.$dialog.confirm({
title: '提示',
message: row.yearlyBatch
? ('该记录属于“预约本年”批量预约,确认后将一并删除同批次的 ' + (row.batchDeleteCount || 0) + ' 条预约记录,是否继续?')
: '您确定要取消该预约吗?',
}).then(() => {
this.$axios.post('/platform/siteCug/mine/delete', { id: row.id }).then((res) => {
if (res.code === 0) {
this.$toast.success(res.msg || '取消成功')
this.doSearch()
} else {
this.$dialog.alert({
title: '提示',
message: res.msg || '取消失败',
}).catch(() => {})
}
}).catch((err) => {
this.$dialog.alert({
title: '提示',
message: (err && err.msg) || '取消失败',
}).catch(() => {})
})
}).catch(() => {})
},
querySiteType() {
return this.$axios.post('/platform/siteCug/function/type/queryFunctionType').then((res) => {
const list = Array.isArray(res.data) ? res.data : []
this.typeOptions = [
{
text: '全部类型',
value: null,
}
].concat(list.map((item) => ({
text: item.name,
value: item.id,
})))
})
},
querySites() {
return this.$axios.post('/platform/siteCug/manage/querySites').then((res) => {
const list = Array.isArray(res.data) ? res.data : []
this.siteOptions = [
{
text: '全部场地',
value: null,
}
].concat(list.map((item) => ({
text: item.name,
value: item.id,
})))
})
},
doSearch() {
this.$nextTick(() => {
this.pageForm.pageNumber = 1
this.pageForm.totalCount = 0
this.$refs.tableListRef.doSearch()
})
},
},
})
</script>
<!--#
}
#-->
@@ -0,0 +1,335 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style scoped>
.site-cug-audit-page {
height: 100vh;
overflow: hidden;
display: flex;
flex-direction: column;
background: #f4f6f8;
box-sizing: border-box;
}
.site-cug-audit-sticky {
flex-shrink: 0;
background: #f4f6f8;
}
.site-cug-audit-list-scroll {
min-height: 0;
flex: 1;
overflow-y: auto;
padding-bottom: 18px;
-webkit-overflow-scrolling: touch;
box-sizing: border-box;
}
.site-cug-audit-search /deep/ .van-search__content,
.site-cug-audit-search .van-search__content {
border-radius: 8px;
background: #ffffff;
}
.site-cug-card-tags {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-bottom: 8px;
}
.site-cug-tag {
display: inline-flex;
align-items: center;
min-height: 22px;
padding: 0 8px;
border-radius: 4px;
background: #eef6ff;
color: #0b75bd;
font-size: 12px;
}
.site-cug-tag.gray {
background: #f1f5f9;
color: #64748b;
}
.site-cug-tag.warn {
background: #fff3e5;
color: #d97706;
}
.action-btn.disabled {
opacity: 0.45;
pointer-events: none;
}
.action-btn.loading {
opacity: 0.9;
}
.action-btn .van-loading {
margin-right: 4px;
}
</style>
<div id="app" class="site-cug-audit-page" v-cloak>
<van-nav-bar title="校工会审核" left-text="返回" left-arrow @click-left="historyBack" fixed placeholder></van-nav-bar>
<van-sticky offset-top="46px" class="site-cug-audit-sticky">
<van-search
class="site-cug-audit-search"
v-model="pageForm.searchKeyword"
show-action
clearable
input-align="left"
placeholder="请输入姓名/工号/场地搜索"
@search="doSearch"
@clear="doSearch">
<template #action>
<div @click="doSearch">搜索</div>
</template>
</van-search>
<van-dropdown-menu :close-on-click-outside="false" :close-on-click-overlay="false">
<van-dropdown-item v-model="pageForm.siteType" :options="typeOptions" :multiple="false" @change="doSearch"></van-dropdown-item>
<van-dropdown-item v-model="pageForm.siteId" :options="siteOptions" :multiple="false" @change="doSearch"></van-dropdown-item>
</van-dropdown-menu>
<van-tabs v-model="pageForm.approvalText" @change="onApprovalTabChange">
<van-tab title="已审核" name="1"></van-tab>
<van-tab title="未审核" name="0"></van-tab>
</van-tabs>
</van-sticky>
<div class="site-cug-audit-list-scroll">
<table-list api="/platform/siteCug/schoolUnionAudit/pageData" :page_form.sync="pageForm" ref="tableListRef" title="applyUserName" @ready="onReady">
<template v-slot="{index,row}">
<div class="site-cug-card-tags">
<span v-if="row.applyLoginName" class="site-cug-tag">{{ row.applyLoginName }}</span>
<span class="site-cug-tag gray">{{ reserveTypeText(row) }}</span>
<span v-if="row.yearlyBatch" class="site-cug-tag warn">预约本年</span>
</div>
<table-column label="场地名称">{{row.siteName}}</table-column>
<table-column label="预约主体">{{row.reserveType === 'club' ? (row.clubName || '-') : (row.applyUnionName || '-')}}</table-column>
<table-column label="所属单位">{{row.applyUnitName}}</table-column>
<table-column label="开始时间">{{row.reserveStartTime}}</table-column>
<table-column label="结束时间">{{row.reserveEndTime}}</table-column>
<table-column label="当前节点">{{row.curTaskName || '-'}}</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" v-if="row.taskState === 10" @click="onAudit(row)">
<i class="fa fa-edit"></i>
<span>审核</span>
</div>
<div class="action-btn delete" v-if="canRevoke(row)" :class="{ loading: revokeLoading }" @click="onRevoke(row)">
<van-loading v-if="revokeLoading" size="14px" color="#fff"></van-loading>
<i v-else class="fa fa-reply"></i>
<span>撤回</span>
</div>
</template>
</table-list>
</div>
<info ref="infoRef">
<div v-if="showApprovalForm">
<div class="process-title">{{formData.taskName}}</div>
<van-form ref="formRef">
<van-field
v-model="formData.tf_opinion"
name="tf_opinion"
label="审批意见"
placeholder="请输入审批意见"
:rules="[{ required: true, message: '请填写审批意见' }]"
required
maxlength="100"
show-word-limit
></van-field>
</van-form>
<div style="display:flex;justify-content:space-between;column-gap:10px;padding:10px;">
<van-button type="info" block :loading="auditLoading" :disabled="auditLoading" @click="handleTaskAction(6)">退回</van-button>
<van-button type="danger" block :loading="auditLoading" :disabled="auditLoading" @click="handleTaskAction(2)">不同意</van-button>
<van-button type="primary" block :loading="auditLoading" :disabled="auditLoading" @click="handleTaskAction(1)">同意</van-button>
</div>
</div>
</info>
</div>
<script nonce="${cspNonce!}">
<!--#include('../common/applyInfo.js'){}#-->
new Vue({
el: '#app',
store,
components: {
info: siteCugApplyInfoH5,
},
data() {
return {
pageForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
searchKeyword: '',
approvalText: '0',
approval: false,
siteType: null,
siteId: null,
},
typeOptions: [
{
text: '全部类型',
value: null,
}
],
siteOptions: [
{
text: '全部场地',
value: null,
}
],
formData: {},
showApprovalForm: false,
auditLoading: false,
revokeLoading: false,
}
},
methods: {
historyBack,
async onReady() {
await this.querySiteType()
await this.querySites()
this.doSearch()
},
onApprovalTabChange(val) {
this.pageForm.approval = val === '1'
this.doSearch()
},
canRevoke(row) {
return Number(row.instanceState) === 20
},
reserveTypeText(row) {
return row && row.reserveType === 'club' ? '协会预约' : '分工会预约'
},
onView(row) {
this.showApprovalForm = false
this.$refs.infoRef.onOpen(row)
},
onAudit(row) {
this.showApprovalForm = true
this.formData = {
processTaskId: row.taskId,
applyId: row.id,
taskName: row.curTaskName,
tf_opinion: '',
yearlyBatch: row.yearlyBatch,
batchAuditCount: row.batchAuditCount,
}
this.$refs.infoRef.onOpen(row)
},
async handleTaskAction(submitType) {
if (this.auditLoading) {
return
}
try {
await this.$refs.formRef.validate()
this.$dialog.confirm({
title: '提示',
message: this.formData.yearlyBatch
? ('该记录属于“预约本年”批量预约,提交后将一并审核同批次的 ' + (this.formData.batchAuditCount || 0) + ' 条预约记录,是否继续?')
: '您确定要提交吗?',
}).then(() => {
this.auditLoading = true
this.$axios.post('/platform/siteCug/schoolUnionAudit/executeTask', {
data: JSON.stringify({
...this.formData,
submitType: submitType,
})
}).then((res) => {
if (res.code === 0) {
this.$refs.infoRef.onClose()
this.$toast.success(res.msg || '提交成功')
this.doSearch()
} else {
this.$dialog.alert({
title: '提示',
message: res.msg || '提交失败',
}).catch(() => {})
}
}).catch((err) => {
this.$dialog.alert({
title: '提示',
message: (err && err.msg) || '提交失败',
}).catch(() => {})
}).finally(() => {
this.auditLoading = false
})
}).catch(() => {})
} catch (e) {
}
},
onRevoke(row) {
if (!this.canRevoke(row) || this.revokeLoading) {
return
}
this.$dialog.confirm({
title: '提示',
message: row.yearlyBatch
? ('该记录属于“预约本年”批量预约,撤回后将一并撤回同批次的 ' + (row.batchAuditCount || 0) + ' 条审核任务,是否继续?')
: '您确定要撤回吗?',
}).then(() => {
this.revokeLoading = true
this.$axios.post('/platform/siteCug/schoolUnionAudit/revokeTask', { taskId: row.taskId, applyId: row.id }).then((res) => {
if (res.code === 0) {
this.$toast.success(res.msg || '撤回成功')
this.doSearch()
} else {
this.$dialog.alert({
title: '提示',
message: res.msg || '撤回失败',
}).catch(() => {})
}
}).catch((err) => {
this.$dialog.alert({
title: '提示',
message: (err && err.msg) || '撤回失败',
}).catch(() => {})
}).finally(() => {
this.revokeLoading = false
})
}).catch(() => {})
},
querySiteType() {
return this.$axios.post('/platform/siteCug/function/type/queryFunctionType').then((res) => {
const list = Array.isArray(res.data) ? res.data : []
this.typeOptions = [
{
text: '全部类型',
value: null,
}
].concat(list.map((item) => ({
text: item.name,
value: item.id,
})))
})
},
querySites() {
return this.$axios.post('/platform/siteCug/manage/querySites').then((res) => {
const list = Array.isArray(res.data) ? res.data : []
this.siteOptions = [
{
text: '全部场地',
value: null,
}
].concat(list.map((item) => ({
text: item.name,
value: item.id,
})))
})
},
doSearch() {
this.$nextTick(() => {
this.pageForm.pageNumber = 1
this.pageForm.totalCount = 0
this.$refs.tableListRef.doSearch()
})
},
},
})
</script>
<!--#
}
#-->
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,478 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style scoped>
.tour-approval-page {
height: 100vh;
overflow: hidden;
display: flex;
flex-direction: column;
background: #f4f6f8;
box-sizing: border-box;
}
.tour-approval-sticky {
flex-shrink: 0;
background: #f4f6f8;
}
.tour-approval-list-scroll {
min-height: 0;
flex: 1;
overflow-y: auto;
padding-bottom: 18px;
-webkit-overflow-scrolling: touch;
box-sizing: border-box;
}
.tour-approval-search /deep/ .van-search__content,
.tour-approval-search .van-search__content {
border-radius: 8px;
background: #ffffff;
}
.tour-card-tags {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-bottom: 8px;
}
.tour-tag {
display: inline-flex;
align-items: center;
min-height: 22px;
padding: 0 8px;
border-radius: 4px;
background: #eef6ff;
color: #0b75bd;
font-size: 12px;
}
.tour-tag.gray {
background: #f1f5f9;
color: #64748b;
}
.tour-tag.warn {
background: #fff3e5;
color: #d97706;
}
.tour-approval-sheet {
max-height: 86vh;
overflow-y: auto;
padding-bottom: env(safe-area-inset-bottom);
background: #f4f6f8;
}
.tour-section {
margin: 10px 12px;
overflow: hidden;
border-radius: 8px;
background: #ffffff;
}
.tour-section-title {
position: relative;
padding: 14px 16px;
border-bottom: 1px solid #edf0f4;
color: #0b75bd;
font-size: 16px;
font-weight: 700;
line-height: 1.2;
}
.tour-section-title::before {
content: "";
position: absolute;
left: 16px;
top: 0;
width: 30px;
height: 4px;
border-radius: 0 0 3px 3px;
background: #0b75bd;
}
.tour-approval-actions {
display: flex;
gap: 8px;
padding: 10px 12px 14px;
background: #ffffff;
}
.tour-process-opinion {
white-space: pre-wrap;
word-break: break-word;
}
</style>
<div id="app" class="tour-approval-page" v-cloak>
<van-nav-bar title="校工会审核" left-text="返回" left-arrow @click-left="historyBack" fixed placeholder></van-nav-bar>
<van-sticky offset-top="46px" class="tour-approval-sticky">
<van-search
class="tour-approval-search"
v-model="pageForm.keyword"
placeholder="请输入工号/姓名/线路搜索"
show-action
clearable
input-align="left"
@search="doSearch"
@clear="doSearch">
<template #action>
<div @click="doSearch">搜索</div>
</template>
</van-search>
<van-dropdown-menu :close-on-click-outside="false" :close-on-click-overlay="false">
<van-dropdown-item :options="auditOptions" @change="doSearch" v-model="pageForm.audit"></van-dropdown-item>
<van-dropdown-item :options="unionOptions" @change="doSearch" v-model="pageForm.unionId"></van-dropdown-item>
<van-dropdown-item :options="lineTypeOptions" @change="doSearch" v-model="pageForm.lineType"></van-dropdown-item>
</van-dropdown-menu>
</van-sticky>
<div class="tour-approval-list-scroll">
<table-list api="/platform/tour/schoolUnionApproval/pageData" :page_form.sync="pageForm" ref="tableListRef" title="userName" @ready="onReady">
<template v-slot="{index,row}">
<div class="tour-card-tags">
<span v-if="row.jobNo" class="tour-tag">{{ row.jobNo }}</span>
<span v-if="row.lineType" class="tour-tag gray">{{ row.lineType }}</span>
<span v-if="row.overCostReimbursed" class="tour-tag warn">报销超出费用</span>
</div>
<table-column label="线路名称">{{ row.lineName || '' }}</table-column>
<table-column label="出行时段">{{ row.travelPeriod || '' }}</table-column>
<table-column label="报销超出费用">{{ row.overCostReimbursed ? '是' : '否' }}</table-column>
<table-column label="当前节点">{{ row.curTaskName || '' }}</table-column>
<table-column label="流程状态">
<enum-tag
v-if="row.instanceState !== null && row.instanceState !== undefined && row.instanceState !== ''"
:value="row.instanceState"
name="ProcessInstanceStateEnum"
label_key="message"
size="small">
</enum-tag>
</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" v-if="row.taskState === 10" @click="onApproval(row)">
<i class="fa fa-check"></i>
<span>审核</span>
</div>
</template>
</table-list>
</div>
<tour-info ref="infoRef">
<div v-if="showApprovalForm" class="tour-section">
<div class="tour-section-title">{{ formData.taskName || '审核' }}</div>
<van-form ref="formRef">
<van-field
v-model="formData.tf_opinion"
name="tf_opinion"
label="审批意见"
placeholder="请输入审批意见"
:rules="[{ required: true, message: '请填写审批意见' }]"
required
type="textarea"
rows="3"
maxlength="200"
show-word-limit>
</van-field>
</van-form>
<div class="tour-approval-actions">
<van-button type="danger" block @click="handleTaskAction(6)">退回</van-button>
<van-button type="danger" block @click="handleTaskAction(2)">拒绝</van-button>
<van-button type="primary" block @click="handleTaskAction(1)">同意</van-button>
</div>
</div>
</tour-info>
</div>
<script nonce="${cspNonce!}">
const TOUR_INFO = {
template: /*language=HTML*/ `
<van-action-sheet v-model="visible" title="报名详情">
<div class="tour-approval-sheet">
<div class="tour-section">
<div class="tour-section-title">教职工信息</div>
<van-cell title="工号" :value="detail.jobNo || ''"></van-cell>
<van-cell title="姓名" :value="detail.userName || ''"></van-cell>
<van-cell title="性别" :value="detail.gender || ''"></van-cell>
<van-cell title="身份证号" :value="detail.idCard || ''"></van-cell>
<van-cell title="所在单位" :value="detail.unitName || ''"></van-cell>
<van-cell title="所在工会" :value="detail.unionName || ''"></van-cell>
</div>
<div class="tour-section">
<div class="tour-section-title">报名信息</div>
<van-cell title="报名线路" :value="detail.lineName || ''"></van-cell>
<van-cell title="线路类型" :value="detail.lineType || ''"></van-cell>
<van-cell title="出行时段" :value="detail.travelPeriod || ''"></van-cell>
<van-cell title="报名时间" :value="detail.signupTime || ''"></van-cell>
<van-cell title="报名酒店" :value="detail.hotelName || ''"></van-cell>
<van-cell title="旅行社" :value="detail.travelAgencyName || ''"></van-cell>
<van-cell v-if="fillBedInfo" title="床型" :value="detail.bedType || ''"></van-cell>
<van-cell v-if="fillBedInfo" title="床位信息" :value="detail.bedInfo || ''"></van-cell>
<van-cell v-if="fillBedInfo" title="意向拼床人" :value="detail.intendedRoommate || ''"></van-cell>
<van-cell title="携带家属" :value="familyText()"></van-cell>
<van-cell title="报销超出费用" :value="detail.overCostReimbursed ? '是' : '否'"></van-cell>
</div>
<div v-if="isDirectFamilyLine()" class="tour-section">
<div class="tour-section-title">直系亲属线路</div>
<van-cell title="亲属姓名" :value="directRelative.relativeName || ''"></van-cell>
<van-cell title="所在单位" :value="directRelative.unitName || ''"></van-cell>
<van-cell title="亲属关系" :value="directRelative.relationshipName || ''"></van-cell>
<van-cell title="线路名称" :value="directRelative.lineName || ''"></van-cell>
<van-cell title="出行开始" :value="directRelative.travelStartTime || ''"></van-cell>
<van-cell title="出行结束" :value="directRelative.travelEndTime || ''"></van-cell>
</div>
<div v-if="showFamilySection()" class="tour-section">
<div class="tour-section-title">家属信息</div>
<template v-for="(item,index) in familyData">
<van-cell :key="'title_' + index" :title="'家属' + (index + 1)"></van-cell>
<van-cell :key="'name_' + index" title="姓名" :value="item.familyName || ''"></van-cell>
<van-cell :key="'gender_' + index" title="性别" :value="item.gender || ''"></van-cell>
<van-cell :key="'id_' + index" title="身份证号" :value="item.idCard || ''"></van-cell>
<van-cell :key="'rel_' + index" title="关系" :value="item.relationship || ''"></van-cell>
<van-cell v-if="fillBedInfo" :key="'bed_' + index" title="床型" :value="item.bedType || ''"></van-cell>
</template>
</div>
<div v-if="doneTasks.length > 0" class="tour-section">
<div class="tour-section-title">流程记录</div>
<template v-for="task in doneTasks">
<van-cell-group :key="task.id" :title="task.displayName || task.taskName">
<van-cell title="办理时间" :value="task.finishTime || ''"></van-cell>
<van-cell title="办理结果">
<dict-tag :options="dict.type.PROCESS_TASK_SUBMIT_TYPE" :value="task.ext && task.ext.submitType"></dict-tag>
</van-cell>
<van-cell title="办理意见" v-if="task.taskFormData" class="direction-column-cell">
<div class="tour-process-opinion">{{ task.taskFormData.opinion || task.taskFormData.tf_opinion || '' }}</div>
</van-cell>
</van-cell-group>
</template>
</div>
<slot></slot>
</div>
</van-action-sheet>
`,
dicts: ["PROCESS_TASK_SUBMIT_TYPE"],
data() {
return {
visible: false,
row: {},
detail: {},
familyData: [],
directRelative: {},
allowFamily: false,
fillBedInfo: true,
directFamilyUnitLine: false,
doneTasks: []
}
},
methods: {
onOpen(row) {
this.row = row || {}
this.visible = true
this.detail = {}
this.familyData = []
this.directRelative = {}
this.allowFamily = false
this.fillBedInfo = true
this.directFamilyUnitLine = false
this.doneTasks = []
this.info()
this.getDoneTasks()
},
onClose() {
this.visible = false
},
info() {
this.$axios.post("/platform/tour/schoolUnionApproval/detail", { id: this.row.id }).then((res) => {
if (res.code === 0) {
const data = res.data || {}
this.detail = data.ledger || {}
this.detail.travelPeriod = data.travelPeriod || this.row.travelPeriod || ""
this.familyData = data.families || []
this.directRelative = data.directRelative || {}
this.allowFamily = this.toBoolean(data.allowFamily)
this.fillBedInfo = data.fillBedInfo === undefined || data.fillBedInfo === null || this.toBoolean(data.fillBedInfo)
this.directFamilyUnitLine = this.toBoolean(data.directFamilyUnitLine)
} else {
vant.Toast(res.msg || "查询失败")
}
})
},
getDoneTasks() {
this.$axios.post("/flow/common/doneTasks", { bizId: this.row.id }).then((res) => {
if (res.code === 0) {
this.doneTasks = res.data || []
}
})
},
toBoolean(value) {
return value === true || value === 1 || value === "1" || value === "true" || value === "TRUE"
},
isDirectFamilyLine() {
return this.directFamilyUnitLine || this.toBoolean(this.detail.directFamilyUnitLine) || !!(this.directRelative && this.directRelative.id)
},
hasFamily() {
return this.toBoolean(this.detail.hasFamily) || this.familyData.length > 0
},
showFamilySection() {
return this.allowFamily && this.hasFamily() && !this.isDirectFamilyLine()
},
familyText() {
if (this.isDirectFamilyLine() || !this.allowFamily) {
return "否"
}
return this.hasFamily() ? "是" : "否"
}
}
}
new Vue({
el: "#app",
store,
components: {
"tour-info": TOUR_INFO
},
data() {
return {
pageForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
pageOrderName: "signupTime",
pageOrderBy: "descending",
audit: false,
keyword: "",
unionId: "",
lineType: ""
},
auditOptions: [
{ text: "未审核", value: false },
{ text: "已审核", value: true }
],
unionOptions: [
{ text: "全部工会", value: "" }
],
lineTypeOptions: [
{ text: "全部线路类型", value: "" }
],
formData: {},
showApprovalForm: false
}
},
methods: {
historyBack,
onReady() {
this.doSearch()
},
doSearch() {
this.$nextTick(async () => {
await Promise.all([this.loadUnionOptions(), this.loadLineTypeOptions()])
this.pageForm.pageNumber = 1
this.pageForm.totalCount = 0
this.$refs.tableListRef.doSearch()
})
},
loadUnionOptions() {
return this.$axios.post("/platform/tour/schoolUnionApproval/unionOptions", {
audit: this.pageForm.audit,
keyword: this.pageForm.keyword,
lineType: this.pageForm.lineType
}).then((res) => {
if (res.code === 0) {
const options = (res.data || []).map(item => ({
text: item.name,
value: item.id
})).filter(item => item.value)
this.unionOptions = [{ text: "全部工会", value: "" }].concat(options)
if (this.pageForm.unionId && !this.unionOptions.some(item => item.value === this.pageForm.unionId)) {
this.pageForm.unionId = ""
}
}
})
},
loadLineTypeOptions() {
return this.$axios.post("/platform/tour/schoolUnionApproval/lineTypeOptions", {
audit: this.pageForm.audit,
keyword: this.pageForm.keyword,
unionId: this.pageForm.unionId
}).then((res) => {
if (res.code === 0) {
const options = (res.data || []).map(item => ({
text: item.lineType,
value: item.lineType
})).filter(item => item.value)
this.lineTypeOptions = [{ text: "全部线路类型", value: "" }].concat(options)
if (this.pageForm.lineType && !this.lineTypeOptions.some(item => item.value === this.pageForm.lineType)) {
this.pageForm.lineType = ""
}
}
})
},
onView(row) {
this.showApprovalForm = false
this.$refs.infoRef.onOpen(row)
},
onApproval(row) {
this.showApprovalForm = true
this.formData = {
processTaskId: row.taskId,
taskName: row.curTaskName || row.taskName,
tf_opinion: ""
}
this.$refs.infoRef.onOpen(row)
},
async handleTaskAction(val) {
try {
await this.$refs.formRef.validate()
} catch (e) {
return
}
this.$dialog.confirm({
title: "提示",
message: "您确定要提交吗?"
}).then(() => {
const loading = this.$toast.loading({
message: "加载中...",
forbidClick: true,
overlay: true,
duration: 0
})
this.$axios.post("/flow/common/executeTask", {
data: JSON.stringify({
...this.formData,
submitType: val
})
}).then((res) => {
if (res.code === 0) {
this.$refs.infoRef.onClose()
this.$toast.success(res.msg || "操作成功")
this.doSearch()
} else {
this.$toast(res.msg || "操作失败")
}
}).finally(() => {
loading.close()
})
}).catch(() => {})
}
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,556 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style scoped>
.tour-apply-page {
min-height: 100vh;
padding: 10px 14px 78px;
background: #f4f6f8;
box-sizing: border-box;
}
.tour-apply-card {
margin-bottom: 14px;
overflow: hidden;
border-radius: 6px;
background: #ffffff;
box-shadow: 0 2px 10px rgba(15, 23, 42, 0.05);
}
.tour-apply-title {
position: relative;
padding: 14px 16px;
border-bottom: 1px solid #edf0f4;
color: #0b75bd;
font-size: 16px;
font-weight: 700;
line-height: 1.2;
}
.tour-apply-title::before {
content: "";
position: absolute;
left: 16px;
top: 0;
width: 30px;
height: 4px;
border-radius: 0 0 3px 3px;
background: #0b75bd;
}
.tour-card-title-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
}
.tour-card-actions {
display: flex;
gap: 8px;
flex-shrink: 0;
}
.tour-family-tab {
margin: 10px 12px 0;
height: 30px;
border-radius: 5px;
background: #1684d2;
color: #ffffff;
font-size: 14px;
font-weight: 700;
line-height: 30px;
text-align: center;
}
.tour-field-tip {
padding: 0 16px 12px;
color: #ee2f2f;
font-size: 12px;
line-height: 1.5;
}
.tour-over-cost /deep/ .van-field__label,
.tour-over-cost /deep/ .van-field__label span,
.tour-over-cost /deep/ .van-radio__label {
color: #ee2f2f;
font-weight: 700;
}
.tour-submit-bar {
position: fixed;
left: 0;
right: 0;
bottom: 0;
z-index: 20;
padding: 10px 14px calc(10px + env(safe-area-inset-bottom));
border-top: 1px solid #edf0f4;
background: #ffffff;
box-sizing: border-box;
}
.tour-apply-empty {
padding: 24px 0;
color: #94a3b8;
font-size: 13px;
text-align: center;
}
</style>
<div id="app">
<van-nav-bar title="疗休养报名" left-text="返回" left-arrow @click-left="historyBack" fixed placeholder></van-nav-bar>
<div class="tour-apply-page">
<van-loading v-if="pageLoading" size="24px" vertical>加载中...</van-loading>
<template v-else>
<div class="tour-apply-card">
<div class="tour-apply-title">教职工信息</div>
<van-field label="姓名" readonly v-model="signupForm.userName"></van-field>
<van-field label="工号" readonly v-model="signupForm.jobNo"></van-field>
<van-field label="身份证号码" v-model="signupForm.idCard" maxlength="30" placeholder="请填写身份证号码"></van-field>
<van-field label="手机号" v-model="signupForm.mobile" type="tel" maxlength="30" placeholder="请填写手机号"></van-field>
<van-field label="所属工会" readonly v-model="signupForm.unionName"></van-field>
</div>
<div class="tour-apply-card">
<div class="tour-apply-title">报名信息</div>
<van-field label="报名线路" readonly v-model="signupForm.lineName"></van-field>
<van-field label="线路类型" readonly v-model="signupForm.lineType"></van-field>
<van-field v-if="signupForm.travelPeriod" label="出行时间" readonly v-model="signupForm.travelPeriod"></van-field>
<van-field label="报名酒店" v-model="signupForm.hotelName" maxlength="100" placeholder="请输入报名酒店"></van-field>
<van-field
v-if="fillBedInfo"
label="床型"
readonly
clickable
is-link
placeholder="请选择房型"
v-model="signupForm.bedType"
@click="openBedPicker('self')">
</van-field>
<van-field v-if="fillBedInfo" label="床位信息" v-model="signupForm.bedInfo" maxlength="100" placeholder="请输入床位数量"></van-field>
<van-field v-if="fillBedInfo" label="意向拼床人" v-model="signupForm.intendedRoommate" maxlength="100" placeholder="请输入意向拼床人"></van-field>
<van-field v-if="canApplyOverReimbursement(signupForm)" class="tour-over-cost" label="报销超出费用">
<template #input>
<van-radio-group v-model="signupForm.overCostReimbursed" direction="horizontal">
<van-radio :name="true">是</van-radio>
<van-radio :name="false">否</van-radio>
</van-radio-group>
</template>
</van-field>
<div v-if="canApplyOverReimbursement(signupForm) && signupForm.overCostReimbursed" class="tour-field-tip">
提醒:您选择了报销超出费用,会影响到下一年度的疗休养,请认真思考。
</div>
</div>
<div v-if="isDirectFamilyLine(signupForm)" class="tour-apply-card">
<div class="tour-apply-title">直系亲属线路</div>
<van-field label="亲属姓名" v-model="directRelativeForm.relativeName" maxlength="100" placeholder="请输入亲属姓名"></van-field>
<van-field label="所在单位" v-model="directRelativeForm.unitName" maxlength="100" placeholder="请输入所在单位"></van-field>
<van-field
label="亲属关系"
readonly
clickable
is-link
placeholder="请选择亲属关系"
v-model="directRelativeForm.relationshipName"
@click="openDirectRelativePicker">
</van-field>
<van-field label="线路名称" v-model="directRelativeForm.lineName" maxlength="100" placeholder="请输入线路名称"></van-field>
<van-field label="出行开始" v-model="directRelativeForm.travelStartTime" type="date" placeholder="请选择出行开始日期"></van-field>
<van-field label="出行结束" v-model="directRelativeForm.travelEndTime" type="date" placeholder="请选择出行结束日期"></van-field>
</div>
<div class="tour-apply-card">
<div class="tour-apply-title">
<div class="tour-card-title-row">
<span>亲属信息</span>
<div class="tour-card-actions">
<van-button size="small" type="default" :disabled="familyData.length === 0" @click="removeFamily">删除亲属</van-button>
<van-button size="small" type="info" :disabled="!allowFamily || isDirectFamilyLine(signupForm)" @click="addFamily">添加亲属</van-button>
</div>
</div>
</div>
<div v-if="!allowFamily || isDirectFamilyLine(signupForm)" class="tour-apply-empty">
当前线路不支持填写亲属
</div>
<template v-else-if="familyData.length > 0">
<div class="tour-family-tab">亲属{{ familyActive + 1 }}</div>
<van-field label="姓名" v-model="currentFamily.familyName" maxlength="100" placeholder="请填写姓名"></van-field>
<van-field label="年龄" v-model.number="currentFamily.age" type="digit" placeholder="请填写年龄"></van-field>
<van-field label="性别">
<template #input>
<van-radio-group v-model="currentFamily.gender" direction="horizontal">
<van-radio name="男">男</van-radio>
<van-radio name="女">女</van-radio>
</van-radio-group>
</template>
</van-field>
<van-field label="身份证号码" v-model="currentFamily.idCard" maxlength="18" placeholder="请填写身份证号码" @blur="normalizeFamilyIdCard(currentFamily)"></van-field>
<van-field label="手机号" v-model="currentFamily.mobile" maxlength="30" placeholder="请填写手机号"></van-field>
<van-field
label="与本人关系"
readonly
clickable
is-link
placeholder="请选择关系"
v-model="currentFamily.relationship"
@click="openRelationshipPicker">
</van-field>
<van-field
v-if="fillBedInfo"
label="床型"
readonly
clickable
is-link
placeholder="请选择房型"
v-model="currentFamily.bedType"
@click="openBedPicker('family')">
</van-field>
<van-field v-if="fillBedInfo" label="床位" v-model="currentFamily.bedInfo" maxlength="100" placeholder="请输入床位数量"></van-field>
<van-field v-if="fillBedInfo" label="意向拼床人" v-model="currentFamily.intendedRoommate" maxlength="100" placeholder="请输入意向拼床人"></van-field>
<div v-if="fillBedInfo" class="tour-field-tip">提醒:如果跟报名人员同一房间,无须选择!</div>
</template>
<div v-else class="tour-apply-empty">暂无亲属,请按需添加</div>
</div>
</template>
</div>
<div class="tour-submit-bar">
<van-button type="info" block round :loading="submitLoading" @click="submitSignup">提交报名</van-button>
</div>
<van-popup v-model="bedPickerVisible" position="bottom">
<van-picker show-toolbar :columns="bedTypeColumns" @confirm="confirmBedType" @cancel="bedPickerVisible=false"></van-picker>
</van-popup>
<van-popup v-model="relationshipPickerVisible" position="bottom">
<van-picker show-toolbar :columns="relationshipColumns" @confirm="confirmRelationship" @cancel="relationshipPickerVisible=false"></van-picker>
</van-popup>
<van-popup v-model="directRelativePickerVisible" position="bottom">
<van-picker show-toolbar :columns="directRelativeColumns" @confirm="confirmDirectRelative" @cancel="directRelativePickerVisible=false"></van-picker>
</van-popup>
</div>
<script nonce="${cspNonce!}">
const vue = new Vue({
el: "#app",
store,
data() {
return {
pageLoading: true,
submitLoading: false,
allowFamily: false,
fillBedInfo: true,
signupForm: {},
directRelativeForm: {},
familyData: [],
familyActive: 0,
bedTypeOptions: [],
familyRelationshipOptions: [],
directRelativeOptions: [],
bedPickerVisible: false,
relationshipPickerVisible: false,
directRelativePickerVisible: false,
bedPickerTarget: "self",
matterId: GetQueryString("matterId") || ""
}
},
computed: {
currentFamily() {
return this.familyData[this.familyActive] || {}
},
bedTypeColumns() {
return this.bedTypeOptions.map(item => item.name || item.code).filter(Boolean)
},
relationshipColumns() {
return this.familyRelationshipOptions.map(item => item.name || item.code).filter(Boolean)
},
directRelativeColumns() {
return this.directRelativeOptions.map(item => ({ text: item.name || item.code, item }))
}
},
methods: {
init() {
if (!this.matterId) {
vant.Dialog.alert({ title: "提示", message: "报名事项参数缺失", confirmButtonColor: "#1867b0" })
this.pageLoading = false
return
}
Promise.all([this.loadOptions(), this.loadSignupDetail()]).finally(() => {
this.pageLoading = false
})
},
loadOptions() {
return Promise.all([
this.$axios.post("/platform/tour/signup/bedTypeOptions").then(res => {
if (res.code === 0) this.bedTypeOptions = res.data || []
}),
this.$axios.post("/platform/tour/signup/familyRelationshipOptions").then(res => {
if (res.code === 0) this.familyRelationshipOptions = res.data || []
}),
this.$axios.post("/platform/tour/signup/directRelativeOptions").then(res => {
if (res.code === 0) this.directRelativeOptions = res.data || []
})
])
},
loadSignupDetail() {
return this.$axios.post("/platform/tour/signup/signupDetail", { matterId: this.matterId }).then((res) => {
if (res.code !== 0) {
vant.Dialog.alert({
title: "提示",
message: res.msg || "报名信息加载失败",
confirmButtonColor: "#1867b0"
})
return
}
const data = res.data || {}
const matter = data.matter || {}
const staff = data.staff || {}
const ledger = data.ledger || {}
const directRelative = data.directRelative || {}
this.allowFamily = matter.allowFamily === true || matter.allowFamily === 1 || matter.allowFamily === "1"
this.fillBedInfo = matter.fillBedInfo === undefined || matter.fillBedInfo === null || matter.fillBedInfo === true || matter.fillBedInfo === 1 || matter.fillBedInfo === "1"
this.signupForm = Object.assign({
id: "",
year: matter.year,
jobNo: "",
userName: "",
gender: "",
idCard: "",
mobile: "",
unitId: "",
unitName: "",
unionId: "",
unionName: "",
signupTime: this.$moment ? this.$moment().format("YYYY-MM-DD HH:mm:ss") : "",
matterId: matter.matterId || "",
lineId: matter.lineId || "",
lineName: matter.lineName || "",
lineType: matter.lineType || "",
directFamilyUnitLine: matter.directFamilyUnitLine,
hotelName: "",
travelAgencyId: matter.travelAgencyId || "",
travelAgencyName: matter.travelAgencyName || "",
travelPeriod: matter.travelPeriod || "",
travelStartTime: matter.travelStartTime || "",
travelEndTime: matter.travelEndTime || "",
allowOverReimbursement: matter.allowOverReimbursement,
overCostReimbursed: false,
hasFamily: false,
intendedRoommate: "",
bedType: "",
bedInfo: ""
}, staff, ledger, {
year: matter.year,
matterId: matter.matterId || ledger.matterId || "",
lineId: matter.lineId || ledger.lineId || "",
lineName: matter.lineName || ledger.lineName || "",
lineType: matter.lineType || ledger.lineType || "",
directFamilyUnitLine: matter.directFamilyUnitLine,
travelAgencyId: matter.travelAgencyId || ledger.travelAgencyId || "",
travelAgencyName: matter.travelAgencyName || ledger.travelAgencyName || "",
travelPeriod: matter.travelPeriod || "",
travelStartTime: matter.travelStartTime || "",
travelEndTime: matter.travelEndTime || "",
allowOverReimbursement: matter.allowOverReimbursement,
overCostReimbursed: ledger.overCostReimbursed === true || ledger.overCostReimbursed === 1 || ledger.overCostReimbursed === "1",
hasFamily: ledger.hasFamily === true || ledger.hasFamily === 1 || ledger.hasFamily === "1"
})
this.directRelativeForm = Object.assign(this.emptyDirectRelative(), {
lineId: matter.lineId || "",
lineName: matter.lineName || "",
travelStartTime: matter.travelStartTime || "",
travelEndTime: matter.travelEndTime || ""
}, directRelative)
this.familyData = (data.families || []).map(item => Object.assign(this.emptyFamily(), item))
if (this.familyData.length > 0) {
this.signupForm.hasFamily = true
}
if (!this.allowFamily || this.isDirectFamilyLine(this.signupForm)) {
this.familyData = []
this.signupForm.hasFamily = false
}
})
},
emptyFamily() {
return {
familyName: "",
gender: "",
idCard: "",
mobile: "",
age: null,
bedType: "",
bedInfo: "",
intendedRoommate: "",
staffJobNo: this.signupForm.jobNo || "",
staffName: this.signupForm.userName || "",
relationship: ""
}
},
emptyDirectRelative() {
return {
relativeName: "",
unitName: "",
relationshipCode: "",
relationshipName: "",
lineId: this.signupForm.lineId || "",
lineName: this.signupForm.lineName || "",
travelStartTime: this.signupForm.travelStartTime || "",
travelEndTime: this.signupForm.travelEndTime || ""
}
},
isDirectFamilyLine(row) {
return row && (row.directFamilyUnitLine === true || row.directFamilyUnitLine === 1 || row.directFamilyUnitLine === "1")
},
canApplyOverReimbursement(row) {
return row && (row.allowOverReimbursement === true || row.allowOverReimbursement === 1 || row.allowOverReimbursement === "1")
},
addFamily() {
if (!this.allowFamily || this.isDirectFamilyLine(this.signupForm)) return
this.familyData.push(this.emptyFamily())
this.familyActive = this.familyData.length - 1
this.signupForm.hasFamily = true
},
removeFamily() {
if (this.familyData.length === 0) return
this.familyData.splice(this.familyActive, 1)
this.familyActive = Math.max(0, this.familyActive - 1)
this.signupForm.hasFamily = this.familyData.length > 0
},
openBedPicker(target) {
if (this.bedTypeColumns.length === 0) {
vant.Toast("暂无床型选项")
return
}
this.bedPickerTarget = target
this.bedPickerVisible = true
},
confirmBedType(value) {
if (this.bedPickerTarget === "family") {
this.currentFamily.bedType = value
} else {
this.signupForm.bedType = value
}
this.bedPickerVisible = false
},
openRelationshipPicker() {
if (this.relationshipColumns.length === 0) {
vant.Toast("暂无关系选项")
return
}
this.relationshipPickerVisible = true
},
confirmRelationship(value) {
this.currentFamily.relationship = value
this.relationshipPickerVisible = false
},
openDirectRelativePicker() {
if (this.directRelativeColumns.length === 0) {
vant.Toast("暂无亲属关系选项")
return
}
this.directRelativePickerVisible = true
},
confirmDirectRelative(value) {
const item = value && value.item ? value.item : {}
this.directRelativeForm.relationshipCode = item.code || ""
this.directRelativeForm.relationshipName = item.name || value.text || ""
this.directRelativePickerVisible = false
},
normalizeFamilyIdCard(row) {
if (!row || !row.idCard) return
row.idCard = String(row.idCard).trim().toUpperCase()
},
isValidIdCard(value) {
if (!value) return false
const idCard = String(value).trim().toUpperCase()
return /^[1-9]\d{5}(18|19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dX]$/.test(idCard)
},
validateFamilies() {
if (!this.allowFamily || !this.signupForm.hasFamily) return true
if (this.familyData.length === 0) {
vant.Toast("请添加亲属信息")
return false
}
const invalid = this.familyData.some(item => !item.familyName || !item.gender || !item.idCard || !item.relationship)
if (invalid) {
vant.Toast("请完善亲属姓名、性别、身份证号码和关系")
return false
}
const invalidIdCard = this.familyData.some(item => !this.isValidIdCard(item.idCard))
if (invalidIdCard) {
vant.Toast("请输入正确的亲属身份证号码")
return false
}
return true
},
validateDirectRelative() {
if (!this.isDirectFamilyLine(this.signupForm)) return true
if (!this.directRelativeForm.relativeName) {
vant.Toast("请输入亲属姓名")
return false
}
if (!this.directRelativeForm.unitName) {
vant.Toast("请输入亲属所在单位")
return false
}
if (!this.directRelativeForm.relationshipCode) {
vant.Toast("请选择亲属关系")
return false
}
if (!this.directRelativeForm.lineName) {
vant.Toast("请输入直系亲属线路名称")
return false
}
if (!this.directRelativeForm.travelStartTime || !this.directRelativeForm.travelEndTime) {
vant.Toast("请选择直系亲属出行时间")
return false
}
return true
},
submitSignup() {
if (this.pageLoading || this.submitLoading) return
if (!this.validateFamilies()) return
if (!this.validateDirectRelative()) return
const families = this.allowFamily && this.signupForm.hasFamily ? this.familyData : []
const form = Object.assign({}, this.signupForm, {
families: JSON.stringify(families),
directRelative: this.isDirectFamilyLine(this.signupForm) ? JSON.stringify(this.directRelativeForm) : ""
})
this.submitLoading = true
this.$axios.post("/platform/tour/signup/doSignup", form).then((res) => {
this.submitLoading = false
if (res.code === 0) {
vant.Dialog.alert({
title: "提示",
message: res.msg || "报名成功",
confirmButtonColor: "#1867b0"
}).then(() => {
window.location.href = "/platform/tour/signup/h5/signup"
})
} else {
vant.Dialog.alert({
title: "提示",
message: res.msg || "报名失败",
confirmButtonColor: "#1867b0"
})
}
}).catch(() => {
this.submitLoading = false
vant.Toast("报名提交失败")
})
}
},
created() {
this.init()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,210 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style scoped>
.tour-signup-h5 {
min-height: 100vh;
background: #f5f7fb;
padding-bottom: 80px;
box-sizing: border-box;
}
.tour-signup-banner {
position: relative;
height: 193px;
overflow: hidden;
background: #0f74bc;
color: #ffffff;
}
.tour-signup-swipe {
width: 100%;
height: 100%;
}
.tour-signup-swipe img {
display: block;
width: 100%;
height: 193px;
object-fit: cover;
}
.tour-signup-banner::after {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(90deg, rgba(5, 83, 143, 0.72) 0%, rgba(5, 83, 143, 0.32) 48%, rgba(5, 83, 143, 0.08) 100%);
pointer-events: none;
}
.tour-signup-banner__text {
position: absolute;
left: 16px;
right: 16px;
bottom: 20px;
z-index: 1;
}
.tour-signup-title {
font-size: 22px;
font-weight: 700;
line-height: 1.35;
}
.tour-signup-subtitle {
margin-top: 8px;
color: rgba(255, 255, 255, 0.86);
font-size: 13px;
line-height: 1.5;
}
.tour-signup-card {
margin: 10px 12px 0;
padding: 14px 14px 16px;
border-radius: 8px;
background: #ffffff;
box-shadow: 0 8px 20px rgba(15, 23, 42, 0.08);
}
.tour-signup-card__header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
padding-bottom: 12px;
border-bottom: 1px solid #eef2f7;
}
.tour-signup-card__title {
color: #111827;
font-size: 17px;
font-weight: 700;
line-height: 1.4;
}
.tour-signup-card__year {
flex-shrink: 0;
color: #0f74bc;
font-size: 13px;
font-weight: 600;
}
.tour-signup-notice {
margin-top: 14px;
color: #334155;
font-size: 15px;
line-height: 1.8;
word-break: break-word;
}
.tour-signup-notice /deep/ img,
.tour-signup-notice /deep/ video {
max-width: 100%;
height: auto;
}
.tour-signup-notice /deep/ #pdf-container {
min-height: 560px;
}
.tour-signup-empty {
padding: 34px 0 26px;
}
.tour-signup-footer {
position: fixed;
right: 0;
bottom: 0;
left: 0;
padding: 10px 12px calc(10px + env(safe-area-inset-bottom));
background: rgba(255, 255, 255, 0.96);
box-shadow: 0 -6px 18px rgba(15, 23, 42, 0.08);
box-sizing: border-box;
}
</style>
<div id="app" class="tour-signup-h5">
<van-nav-bar title="疗休养报名" left-text="返回" left-arrow @click-left="historyBack" fixed placeholder></van-nav-bar>
<div class="tour-signup-banner">
<van-swipe class="tour-signup-swipe" :autoplay="3500" indicator-color="white">
<van-swipe-item v-for="item in bannerList" :key="item">
<img :src="item" alt="疗休养报名">
</van-swipe-item>
</van-swipe>
<div class="tour-signup-banner__text">
<div class="tour-signup-title">疗休养报名</div>
</div>
</div>
<div class="tour-signup-card">
<div class="tour-signup-card__header">
<div class="tour-signup-card__title">服务须知</div>
<div class="tour-signup-card__year">{{ setting.year || currentYear }}年度</div>
</div>
<van-skeleton title :row="8" :loading="loading">
<div v-if="setting.serviceNotice" class="tour-signup-notice">
<pdf-preview :key="serviceNoticeKey" :content="setting.serviceNotice"></pdf-preview>
</div>
<van-empty v-else class="tour-signup-empty" description="暂无服务须知"></van-empty>
</van-skeleton>
</div>
<div class="tour-signup-footer">
<van-button block type="info" color="#0f74bc" round @click="confirmRead">我已阅读</van-button>
</div>
</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 {
loading: false,
currentYear: new Date().getFullYear(),
serviceNoticeKey: "",
bannerList: [
"/assets/platform/images/tour/tour-h5-banner-1.jpg",
"/assets/platform/images/tour/tour-h5-banner-2.jpg"
],
setting: {
year: "",
configName: "",
serviceNotice: ""
}
}
},
methods: {
loadServiceNotice() {
this.loading = true
this.$axios.post("/platform/tour/signup/h5/serviceNotice").then((res) => {
this.loading = false
if (res.code === 0) {
this.setting = Object.assign({}, this.setting, res.data || {})
this.serviceNoticeKey = (this.setting.year || this.currentYear) + "_" + new Date().getTime()
} else {
vant.Toast(res.msg || "服务须知加载失败")
}
}).catch(() => {
this.loading = false
})
},
confirmRead() {
window.location.href = "/platform/tour/signup/h5/signup"
}
},
created() {
this.loadServiceNotice()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,559 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style scoped>
#app {
height: 100vh;
overflow: hidden;
}
.tour-detail-page {
height: calc(100vh - 46px);
overflow: hidden;
display: flex;
flex-direction: column;
padding: 10px 10px 70px;
background: #f4f6f8;
box-sizing: border-box;
}
.tour-detail-header {
padding: 14px 12px;
border-radius: 8px;
background: #ffffff;
box-shadow: 0 2px 10px rgba(15, 23, 42, 0.05);
}
.tour-detail-title {
color: #0f172a;
font-size: 18px;
font-weight: 700;
line-height: 1.35;
word-break: break-word;
}
.tour-detail-tags {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 8px;
}
.tour-line-tag {
display: inline-flex;
align-items: center;
height: 20px;
padding: 0 7px;
border-radius: 4px;
background: #0b75bd;
color: #ffffff;
font-size: 12px;
font-weight: 600;
line-height: 20px;
}
.tour-detail-meta {
margin-top: 10px;
color: #334155;
font-size: 13px;
line-height: 1.7;
word-break: break-word;
}
.tour-detail-meta-row {
display: flex;
align-items: flex-start;
min-width: 0;
}
.tour-detail-meta-label {
width: 72px;
flex-shrink: 0;
display: flex;
justify-content: space-between;
color: #64748b;
white-space: nowrap;
}
.tour-detail-meta-value {
min-width: 0;
flex: 1;
}
.tour-detail-section {
margin-top: 10px;
min-height: 0;
flex: 1;
display: flex;
flex-direction: column;
padding: 12px;
border-radius: 8px;
background: #ffffff;
box-shadow: 0 2px 10px rgba(15, 23, 42, 0.05);
box-sizing: border-box;
}
.tour-detail-section-title {
flex-shrink: 0;
margin-bottom: 8px;
padding-left: 8px;
border-left: 3px solid #0b75bd;
color: #0f172a;
font-size: 15px;
font-weight: 700;
line-height: 1.2;
}
.tour-detail-content /deep/ img {
max-width: 100%;
height: auto;
}
.tour-detail-section-scroll {
min-height: 0;
flex: 1;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
.tour-pdf-preview {
min-height: 220px;
}
.tour-pdf-status {
padding: 16px 0;
color: #64748b;
font-size: 13px;
text-align: center;
}
.tour-pdf-toolbar {
margin-bottom: 8px;
color: #64748b;
font-size: 12px;
line-height: 1.5;
}
.tour-pdf-pages {
min-height: 180px;
}
.tour-pdf-page {
margin-bottom: 10px;
border-radius: 6px;
overflow: hidden;
background: #f8fafc;
box-shadow: 0 1px 4px rgba(15, 23, 42, 0.08);
}
.tour-pdf-page canvas {
width: 100%;
display: block;
}
.tour-detail-empty {
padding: 18px 0;
color: #94a3b8;
font-size: 13px;
text-align: center;
}
.tour-detail-footer {
position: fixed;
left: 0;
right: 0;
bottom: 0;
z-index: 10;
padding: 9px 12px calc(9px + env(safe-area-inset-bottom));
border-top: 1px solid #edf0f4;
background: #ffffff;
box-sizing: border-box;
}
</style>
<div id="app">
<van-nav-bar title="线路详情" left-text="返回" left-arrow @click-left="historyBack" fixed placeholder></van-nav-bar>
<div class="tour-detail-page">
<van-loading v-if="detailLoading" size="24px" vertical>加载中...</van-loading>
<template v-else>
<div class="tour-detail-header">
<div class="tour-detail-title">{{ lineDetail.lineName || signupDetail.lineName || '线路详情' }}</div>
<div class="tour-detail-tags">
<span v-if="lineDetail.lotName || signupDetail.lotName" class="tour-line-tag">{{ lineDetail.lotName || signupDetail.lotName }}</span>
<span v-if="lineDetail.lineType || signupDetail.lineType" class="tour-line-tag">{{ lineDetail.lineType || signupDetail.lineType }}</span>
<span v-if="signupDetail.unionName" class="tour-line-tag">{{ signupDetail.unionName }}</span>
</div>
<div class="tour-detail-meta">
<div v-if="travelPeriod(signupDetail)" class="tour-detail-meta-row">
<span class="tour-detail-meta-label"><span>出行时间</span><span>:</span></span>
<span class="tour-detail-meta-value">{{ travelPeriod(signupDetail) }}</span>
</div>
<div class="tour-detail-meta-row">
<span class="tour-detail-meta-label"><span>已报人数</span><span>:</span></span>
<span class="tour-detail-meta-value">{{ signupDetail.signupCount || 0 }}(家属{{ signupDetail.familyCount || 0 }}人)</span>
</div>
<div class="tour-detail-meta-row">
<span class="tour-detail-meta-label"><span>联系人</span><span>:</span></span>
<span class="tour-detail-meta-value">{{ signupDetail.contactName || '暂无' }}</span>
</div>
<div class="tour-detail-meta-row">
<span class="tour-detail-meta-label"><span>联系方式</span><span>:</span></span>
<span class="tour-detail-meta-value">{{ lineDetail.contactPhone || signupDetail.contactPhone || '暂无' }}</span>
</div>
</div>
</div>
<div class="tour-detail-section">
<div class="tour-detail-section-title">线路介绍</div>
<div class="tour-detail-section-scroll">
<div v-if="lineDetail.lineContent && lineContentHref" class="tour-pdf-preview">
<div v-if="pdfPageCount" class="tour-pdf-toolbar">
已加载 {{ pdfRenderedPages }} / {{ pdfPageCount }} 页
</div>
<div ref="pdfPreviewContainer" class="tour-pdf-pages"></div>
<div v-if="pdfLoading && pdfRenderedPages === 0" class="tour-pdf-status">
<van-loading size="22px" vertical>正在加载第一页...</van-loading>
</div>
<div v-else-if="pdfLoading" class="tour-pdf-status">
正在继续加载后续页面...
</div>
<div v-if="pdfError" class="tour-pdf-status">{{ pdfError }}</div>
</div>
<div v-else-if="lineDetail.lineContent" class="tour-detail-content" v-html="lineDetail.lineContent">
</div>
<div v-else class="tour-detail-empty">暂无线路介绍</div>
</div>
</div>
</template>
</div>
<div class="tour-detail-footer">
<van-button :type="detailActionType()" block round :disabled="detailActionDisabled()" @click="handleDetailAction">
{{ detailActionText() }}
</van-button>
</div>
</div>
<script nonce="${cspNonce!}">
const vue = new Vue({
el: "#app",
store,
data() {
return {
detailLoading: true,
lineDetail: {},
signupDetail: {},
lineContentHref: "",
pdfDoc: null,
pdfLoadingTask: null,
pdfLoading: false,
pdfError: "",
pdfPageCount: 0,
pdfRenderedPages: 0,
pdfRenderToken: 0,
matterId: GetQueryString("matterId") || "",
lineId: GetQueryString("lineId") || ""
}
},
methods: {
travelPeriod(row) {
if (!row) {
return ""
}
if (row.travelStartTime && row.travelEndTime) {
return row.travelStartTime + " 至 " + row.travelEndTime
}
return row.travelPeriod || row.travelStartTime || row.travelEndTime || ""
},
toBoolean(value) {
return value === true || value === 1 || value === "1" || value === "true" || value === "TRUE"
},
isSigned() {
return !!(this.signupDetail && this.signupDetail.signed)
},
isApprovalSignup() {
return this.isSigned() && !!this.signupDetail.instanceId
},
canRevokeSignup() {
return this.isApprovalSignup() && this.toBoolean(this.signupDetail.canRevoke) && !!this.signupDetail.startTaskId
},
isFinishedSignup() {
return this.isApprovalSignup() && Number(this.signupDetail.instanceState) === 20
},
isRejectedSignup() {
return this.isApprovalSignup() && Number(this.signupDetail.instanceState) === 45
},
canModifySignup() {
if (!this.isSigned()) {
return true
}
if (!this.isApprovalSignup()) {
return true
}
return !this.isFinishedSignup() && !this.isRejectedSignup() && this.signupDetail.taskKey === "startTask"
},
detailActionText() {
if (this.isSigned()) {
if (this.canRevokeSignup()) {
return "撤回"
}
if (this.isFinishedSignup()) {
return "已完成"
}
if (this.isRejectedSignup()) {
return "已拒绝"
}
return this.canModifySignup() ? "修改报名" : "审核中"
}
return "我要报名"
},
detailActionType() {
return this.canRevokeSignup() ? "danger" : "info"
},
detailActionDisabled() {
return this.isSigned() && this.isApprovalSignup() && (this.isFinishedSignup() || this.isRejectedSignup() || (!this.canRevokeSignup() && !this.canModifySignup()))
},
handleDetailAction() {
if (this.detailActionDisabled()) {
return
}
if (this.canRevokeSignup()) {
this.revokeSignup()
return
}
this.onApply()
},
parseLineContentHref(content) {
if (!content) {
return ""
}
try {
const parser = new DOMParser()
const doc = parser.parseFromString(content, "text/html")
const link = doc.querySelector("a[href]")
return link ? link.getAttribute("href") : ""
} catch (e) {
return ""
}
},
getPdfLib() {
return window.pdfjsLib || window["pdfjs-dist/build/pdf"]
},
clearPdfPreview() {
this.pdfRenderToken += 1
this.pdfDoc = null
this.pdfLoading = false
this.pdfError = ""
this.pdfPageCount = 0
this.pdfRenderedPages = 0
if (this.pdfLoadingTask && this.pdfLoadingTask.destroy) {
try {
this.pdfLoadingTask.destroy()
} catch (e) {
}
}
this.pdfLoadingTask = null
if (this.$refs.pdfPreviewContainer) {
this.$refs.pdfPreviewContainer.innerHTML = ""
}
},
renderLinePdf() {
this.clearPdfPreview()
if (!this.lineContentHref) {
return
}
const pdfLib = this.getPdfLib()
if (!pdfLib || !pdfLib.getDocument) {
this.pdfError = "PDF预览组件加载失败"
return
}
if (pdfLib.GlobalWorkerOptions) {
pdfLib.GlobalWorkerOptions.workerSrc = "/assets/platform/plugins/pdfJs/h5/pdf.worker.js"
}
const token = this.pdfRenderToken
this.pdfLoading = true
this.pdfLoadingTask = pdfLib.getDocument({
url: this.lineContentHref,
disableAutoFetch: false,
disableStream: false,
disableRange: false
})
this.pdfLoadingTask.promise.then((pdf) => {
if (token !== this.pdfRenderToken) {
return
}
this.pdfDoc = pdf
this.pdfPageCount = pdf.numPages || 0
return this.renderPdfPage(1, token)
}).then(() => {
if (token !== this.pdfRenderToken) {
return
}
this.renderRestPdfPages(2, token)
}).catch(() => {
if (token !== this.pdfRenderToken) {
return
}
this.pdfLoading = false
this.pdfError = "PDF加载失败,请稍后重试"
})
},
renderPdfPage(pageNumber, token) {
if (!this.pdfDoc || token !== this.pdfRenderToken || pageNumber > this.pdfPageCount) {
return Promise.resolve()
}
return this.pdfDoc.getPage(pageNumber).then((page) => {
if (token !== this.pdfRenderToken || !this.$refs.pdfPreviewContainer) {
return
}
const container = this.$refs.pdfPreviewContainer
const baseViewport = page.getViewport({scale: 1})
const containerWidth = Math.max(container.clientWidth || 0, window.innerWidth - 44)
const cssScale = containerWidth / baseViewport.width
const outputScale = Math.min(window.devicePixelRatio || 1, 2)
const viewport = page.getViewport({scale: cssScale})
const pageEl = document.createElement("div")
pageEl.className = "tour-pdf-page"
const canvas = document.createElement("canvas")
const context = canvas.getContext("2d")
canvas.width = Math.floor(viewport.width * outputScale)
canvas.height = Math.floor(viewport.height * outputScale)
canvas.style.width = Math.floor(viewport.width) + "px"
canvas.style.height = Math.floor(viewport.height) + "px"
pageEl.appendChild(canvas)
container.appendChild(pageEl)
const renderContext = {
canvasContext: context,
viewport: viewport,
transform: outputScale !== 1 ? [outputScale, 0, 0, outputScale, 0, 0] : null
}
return page.render(renderContext).promise.then(() => {
if (token === this.pdfRenderToken) {
this.pdfRenderedPages = Math.max(this.pdfRenderedPages, pageNumber)
}
})
})
},
renderRestPdfPages(pageNumber, token) {
if (token !== this.pdfRenderToken || !this.pdfDoc || pageNumber > this.pdfPageCount) {
this.pdfLoading = false
return
}
setTimeout(() => {
this.renderPdfPage(pageNumber, token).then(() => {
this.renderRestPdfPages(pageNumber + 1, token)
}).catch(() => {
if (token === this.pdfRenderToken) {
this.pdfLoading = false
this.pdfError = "部分页面加载失败"
}
})
}, 120)
},
loadDetail() {
if (!this.matterId || !this.lineId) {
this.detailLoading = false
vant.Dialog.alert({
title: "提示",
message: "线路参数不完整",
confirmButtonColor: "#1867b0"
})
return
}
Promise.all([
this.$axios.post("/platform/tour/signup/lineDetail", { lineId: this.lineId }),
this.$axios.post("/platform/tour/signup/signupDetail", { matterId: this.matterId })
]).then(([lineRes, signupRes]) => {
this.detailLoading = false
if (lineRes.code !== 0) {
vant.Toast(lineRes.msg || "线路详情加载失败")
} else {
this.lineDetail = lineRes.data || {}
this.lineContentHref = this.parseLineContentHref(this.lineDetail.lineContent)
this.$nextTick(() => {
this.renderLinePdf()
})
}
if (signupRes.code !== 0) {
vant.Toast(signupRes.msg || "报名信息加载失败")
} else {
const data = signupRes.data || {}
const process = data.process || {}
this.signupDetail = Object.assign({}, data.matter || {}, {
signed: !!(data.ledger && data.ledger.id)
}, process)
}
}).catch(() => {
this.detailLoading = false
vant.Toast("线路详情加载失败")
})
},
onApply() {
if (!this.matterId) {
vant.Toast("报名事项信息不完整")
return
}
const goApply = () => {
window.location.href = "/platform/tour/signup/h5/apply?matterId=" + encodeURIComponent(this.matterId)
}
this.$axios.post("/platform/tour/signup/signupEligibilityNotice", { matterId: this.matterId }).then((res) => {
if (res.code !== 0) {
vant.Dialog.alert({
title: "温馨提醒",
message: res.msg || "当前不能报名",
confirmButtonColor: "#1867b0"
})
return
}
const data = res.data || {}
if (!data.noticeRequired) {
goApply()
return
}
if (data.canApply) {
vant.Dialog.confirm({
title: "温馨提醒",
message: data.message || "当前可报名,是否继续报名?",
confirmButtonText: "继续报名",
cancelButtonText: "稍后报名",
confirmButtonColor: "#1867b0"
}).then(goApply).catch(() => {})
} else {
vant.Dialog.alert({
title: "温馨提醒",
message: data.message || "当前不能报名",
confirmButtonColor: "#1867b0"
})
}
})
},
revokeSignup() {
vant.Dialog.confirm({
title: "提示",
message: "您确定要撤回吗?",
confirmButtonColor: "#ee2f2f"
}).then(() => {
this.$axios.post("/flow/common/revokeTask", { taskId: this.signupDetail.startTaskId }).then((resp) => {
if (resp.code === 0) {
vant.Toast.success(resp.msg || "撤回成功")
this.loadDetail()
} else {
vant.Dialog.alert({ title: "提示", message: resp.msg || "撤回失败", confirmButtonColor: "#1867b0" })
}
})
}).catch(() => {})
}
},
created() {
this.loadDetail()
},
beforeDestroy() {
this.clearPdfPreview()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,787 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style scoped>
.tour-line-page {
height: 100vh;
overflow: hidden;
display: flex;
flex-direction: column;
background: #f4f6f8;
box-sizing: border-box;
}
.tour-line-scroll {
min-height: 0;
flex: 1;
overflow-y: auto;
padding-bottom: 18px;
-webkit-overflow-scrolling: touch;
box-sizing: border-box;
}
.tour-line-toolbar {
padding: 10px 0 8px 16px;
background: #f4f6f8;
}
.tour-line-toolbar /deep/ .van-search {
flex: 1;
min-width: 0;
padding: 0;
background: transparent;
}
.tour-line-toolbar .van-search {
flex: 1;
min-width: 0;
padding: 0;
background: transparent;
}
.tour-line-toolbar /deep/ .van-search__content {
height: 38px;
border-radius: 8px;
background: #ffffff;
}
.tour-line-toolbar .van-search__content {
height: 38px;
border-radius: 8px;
background: #ffffff;
}
.tour-line-toolbar /deep/ .van-search__action {
padding: 0 0 0 6px;
}
.tour-line-toolbar .van-search__action {
padding: 0 0 0 6px;
}
.tour-line-search-button {
width: 42px;
height: 38px;
border: 0;
border-radius: 8px;
background: #1e88e5;
color: #ffffff;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
line-height: 1;
}
.tour-line-search-row {
width: 100%;
display: flex;
align-items: center;
justify-content: flex-end;
gap: 4px;
box-sizing: border-box;
}
.tour-line-union-entry {
width: 48px;
min-height: 42px;
flex-shrink: 0;
border: 0;
padding: 0;
background: transparent;
color: #64748b;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 2px;
font-size: 11px;
line-height: 1.2;
}
.tour-line-union-entry .van-icon {
color: #64748b;
font-size: 24px;
line-height: 1;
}
.tour-line-union-entry--active,
.tour-line-union-entry--active .van-icon {
color: #0b75bd;
}
.tour-line-union-popup {
max-height: 68vh;
display: flex;
flex-direction: column;
}
.tour-line-union-popup__title {
position: sticky;
top: 0;
z-index: 1;
padding: 14px 16px 10px;
background: #ffffff;
color: #0f172a;
font-size: 16px;
font-weight: 700;
text-align: center;
box-sizing: border-box;
}
.tour-line-union-popup__list {
overflow-y: auto;
padding-bottom: env(safe-area-inset-bottom);
}
.tour-line-union-popup__item {
min-height: 48px;
transition: background-color 0.18s ease, color 0.18s ease;
}
.tour-line-union-popup__item:hover,
.tour-line-union-popup__item:active {
background: rgba(30, 136, 229, 0.1);
}
.tour-line-union-popup__item:hover /deep/ .van-cell__title,
.tour-line-union-popup__item:active /deep/ .van-cell__title {
color: #0b75bd;
}
.tour-line-union-popup__item--active /deep/ .van-cell__title {
color: #0b75bd;
font-weight: 700;
}
.tour-line-shortcuts {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 10px;
margin-top: 10px;
}
.tour-line-shortcut {
min-height: 76px;
padding: 12px 8px;
border: 1px solid transparent;
border-radius: 8px;
background: #ffffff;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 5px;
box-shadow: 0 2px 10px rgba(15, 23, 42, 0.05);
box-sizing: border-box;
}
.tour-line-shortcut--active {
border-color: #0b75bd;
background: rgba(11, 117, 189, 0.08);
}
.tour-line-shortcut__icon {
width: 42px;
height: 42px;
flex-shrink: 0;
display: block;
border-radius: 50%;
object-fit: cover;
}
.tour-line-shortcut__text {
min-width: 0;
color: #0f172a;
font-size: 13px;
font-weight: 700;
line-height: 1.3;
text-align: center;
}
.tour-line-shortcut__hint {
margin-top: 2px;
color: #64748b;
font-size: 11px;
font-weight: 400;
}
.tour-line-list {
padding: 0 8px;
}
.tour-line-card {
position: relative;
display: flex;
gap: 12px;
min-height: 112px;
margin: 8px 0;
padding: 10px;
border-radius: 6px;
background: #ffffff;
box-shadow: 0 2px 10px rgba(15, 23, 42, 0.05);
box-sizing: border-box;
}
.tour-line-thumb {
width: 145px;
height: 98px;
flex-shrink: 0;
overflow: hidden;
border-radius: 6px;
background: #e5edf6;
}
.tour-line-thumb img,
.tour-line-thumb .van-image {
width: 100%;
height: 100%;
display: block;
}
.tour-line-thumb-empty {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
color: #8a98a8;
font-size: 12px;
}
.tour-line-main {
min-width: 0;
flex: 1;
}
.tour-line-signed-icon {
width: 20px;
height: 20px;
flex-shrink: 0;
border-radius: 50%;
background: #a78bfa;
color: #ffffff;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 13px;
line-height: 1;
box-shadow: 0 2px 5px rgba(167, 139, 250, 0.32);
}
.tour-line-title-row {
display: flex;
align-items: center;
gap: 6px;
min-width: 0;
}
.tour-line-title {
min-width: 0;
color: #0f172a;
font-size: 16px;
font-weight: 700;
line-height: 1.35;
word-break: break-word;
}
.tour-line-type-badge {
height: 18px;
flex-shrink: 0;
padding: 0 5px;
border: 1px solid #ffd6a8;
border-radius: 4px;
background: #fff3e5;
color: #d97706;
font-size: 11px;
font-weight: 700;
line-height: 17px;
box-sizing: border-box;
}
.tour-line-tags {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 5px;
}
.tour-line-tag {
display: inline-flex;
align-items: center;
height: 20px;
padding: 0 7px;
border-radius: 4px;
background: #39a9ed;
color: #ffffff;
font-size: 12px;
font-weight: 600;
line-height: 20px;
}
.tour-line-info {
margin-top: 5px;
color: #111827;
font-size: 13px;
line-height: 1.55;
word-break: break-word;
}
.tour-line-info span {
color: #64748b;
}
.tour-line-actions {
display: flex;
justify-content: flex-end;
margin-top: 8px;
}
.tour-line-action {
min-width: 70px;
height: 28px;
padding: 0 10px;
border: 1px solid #1e88e5;
border-radius: 14px;
background: #1e88e5;
color: #ffffff;
font-size: 12px;
line-height: 26px;
}
.tour-line-action--danger {
border-color: #ee2f2f;
background: #ee2f2f;
}
.tour-line-action--disabled {
border-color: #d1d5db;
background: #e5e7eb;
color: #94a3b8;
}
@media (max-width: 360px) {
.tour-line-card {
gap: 9px;
padding: 8px;
}
.tour-line-thumb {
width: 126px;
height: 92px;
}
.tour-line-title {
font-size: 15px;
}
}
</style>
<div id="app" class="tour-line-page">
<van-nav-bar title="疗休养线路" left-text="返回" left-arrow @click-left="historyBack" fixed placeholder></van-nav-bar>
<div class="tour-line-toolbar">
<div class="tour-line-search-row">
<van-search
v-model="pageForm.lineName"
show-action
clearable
placeholder="请输入线路名称搜索"
@search="doSearch"
@clear="doSearch">
<template #action>
<button type="button" class="tour-line-search-button" @click="doSearch">
<van-icon name="search"></van-icon>
</button>
</template>
</van-search>
<button
type="button"
class="tour-line-union-entry"
:class="{'tour-line-union-entry--active': !!pageForm.unionId}"
@click="openUnionPopup">
<van-icon name="friends-o"></van-icon>
<span>分工会</span>
</button>
</div>
<div class="tour-line-shortcuts">
<div
class="tour-line-shortcut"
:class="{'tour-line-shortcut--active': pageForm.lineType === '省内线路' && pageForm.directFamilyUnitLine === null}"
@click="toggleLineType('省内线路')">
<img class="tour-line-shortcut__icon" src="/assets/platform/images/tour/tour-in-province.png" alt="省内线路">
<div class="tour-line-shortcut__text">
<div>省内线路</div>
</div>
</div>
<div
class="tour-line-shortcut"
:class="{'tour-line-shortcut--active': pageForm.lineType === '省外线路' && pageForm.directFamilyUnitLine === null}"
@click="toggleLineType('省外线路')">
<img class="tour-line-shortcut__icon" src="/assets/platform/images/tour/tour-out-province.png" alt="省外线路">
<div class="tour-line-shortcut__text">
<div>省外线路</div>
</div>
</div>
<div
class="tour-line-shortcut"
:class="{'tour-line-shortcut--active': pageForm.directFamilyUnitLine === true}"
@click="toggleDirectFamilyLine">
<img class="tour-line-shortcut__icon" src="/assets/platform/images/tour/tour-direct-family.jpg" alt="直系亲属">
<div class="tour-line-shortcut__text">
<div>直系亲属</div>
</div>
</div>
</div>
</div>
<div class="tour-line-scroll">
<van-list
v-model="loading"
:finished="finished"
finished-text="没有更多了"
@load="loadData">
<div class="tour-line-list">
<div class="tour-line-card" v-for="row in list" :key="row.matterId" @click="onLine(row)">
<div class="tour-line-thumb">
<van-image v-if="thumbUrl(row)" :src="thumbUrl(row)" fit="cover"></van-image>
<div v-else class="tour-line-thumb-empty">暂无图片</div>
</div>
<div class="tour-line-main">
<div class="tour-line-title-row">
<div class="tour-line-title">{{ row.lineName || '未命名线路' }}</div>
<span v-if="lineTypeBadge(row.lineType)" class="tour-line-type-badge">{{ lineTypeBadge(row.lineType) }}</span>
<span v-if="isSigned(row)" class="tour-line-signed-icon">
<van-icon name="good-job"></van-icon>
</span>
</div>
<div class="tour-line-tags">
<span v-if="row.lotName" class="tour-line-tag">{{ row.lotName }}</span>
<span v-if="row.unionName" class="tour-line-tag">{{ row.unionName }}</span>
</div>
<div class="tour-line-info">
<div><span>出行开始时间:</span>{{ row.travelStartTime || '暂无' }}</div>
<div><span>出行结束时间:</span>{{ row.travelEndTime || '暂无' }}</div>
<!-- 临时屏蔽旅行社信息,保留字段和接口便于后续恢复。
<div><span>旅行社:</span>{{ row.travelAgencyName || '暂无' }}</div>
-->
</div>
<div class="tour-line-actions">
<button
v-if="showSignupAction(row)"
type="button"
class="tour-line-action"
:class="{
'tour-line-action--danger': canRevokeSignup(row),
'tour-line-action--disabled': signupActionDisabled(row)
}"
:disabled="signupActionDisabled(row)"
@click.stop="handleSignupAction(row)">
{{ signupActionText(row) }}
</button>
</div>
</div>
</div>
</div>
</van-list>
<van-empty v-if="!loading && list.length === 0" description="暂无线路"></van-empty>
</div>
<van-popup v-model="showUnionPopup" position="bottom" round>
<div class="tour-line-union-popup">
<div class="tour-line-union-popup__title">选择分工会</div>
<div class="tour-line-union-popup__list">
<van-cell
v-for="item in unionList"
:key="item.id || 'all'"
clickable
class="tour-line-union-popup__item"
:class="{'tour-line-union-popup__item--active': pageForm.unionId === item.id}"
:title="item.name"
@click="selectUnion(item)">
<template #right-icon>
<van-icon v-if="pageForm.unionId === item.id" name="success" color="#0b75bd"></van-icon>
</template>
</van-cell>
</div>
</div>
</van-popup>
</div>
<script nonce="${cspNonce!}">
const vue = new Vue({
el: "#app",
store,
data() {
return {
list: [],
loading: false,
finished: false,
lineChecking: false,
showUnionPopup: false,
unionList: [
{id: "", name: "全部分工会"}
],
pageForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
year: new Date().getFullYear(),
lineName: "",
lineType: "",
unionId: "",
directFamilyUnitLine: null,
pageOrderName: "lineName",
pageOrderBy: "ascending"
}
}
},
methods: {
thumbUrl(row) {
return row.lineMobileThumb || row.agencyMobileThumb || ""
},
lineTypeBadge(lineType) {
if (lineType === "省内线路" || lineType === "省内") {
return "省内"
}
if (lineType === "省外线路" || lineType === "省外") {
return "省外"
}
return ""
},
isSigned(row) {
return !!(row && (row.signed === true || row.signed === 1 || row.signed === "1" || row.ledgerId))
},
isApprovalSignup(row) {
return !!(row && this.isSigned(row) && row.instanceId)
},
canRevokeSignup(row) {
return this.isApprovalSignup(row) && this.toBoolean(row.canRevoke) && !!row.startTaskId
},
isFinishedSignup(row) {
return this.isApprovalSignup(row) && Number(row.instanceState) === 20
},
isRejectedSignup(row) {
return this.isApprovalSignup(row) && Number(row.instanceState) === 45
},
canModifySignup(row) {
if (!row || !this.isSigned(row)) {
return true
}
if (!this.isApprovalSignup(row)) {
return true
}
return !this.isFinishedSignup(row) && !this.isRejectedSignup(row) && row.taskKey === "startTask"
},
signupActionText(row) {
if (this.isSigned(row)) {
if (this.canRevokeSignup(row)) {
return "撤回"
}
if (this.isRejectedSignup(row)) {
return "已拒绝"
}
return this.canModifySignup(row) ? "修改" : "审核中"
}
return "报名"
},
showSignupAction(row) {
return this.isSigned(row) && this.canModifySignup(row)
},
signupActionDisabled(row) {
return !!(this.isSigned(row) && this.isApprovalSignup(row) && (this.isRejectedSignup(row) || (!this.canRevokeSignup(row) && !this.canModifySignup(row))))
},
toBoolean(value) {
return value === true || value === 1 || value === "1" || value === "true" || value === "TRUE"
},
handleSignupAction(row) {
if (this.signupActionDisabled(row)) {
return
}
if (this.canRevokeSignup(row)) {
this.revokeSignup(row)
return
}
this.goApply(row)
},
goApply(row) {
if (!row || !row.matterId) {
vant.Toast("报名事项信息不完整")
return
}
const go = () => {
window.location.href = "/platform/tour/signup/h5/apply?matterId=" + encodeURIComponent(row.matterId)
}
this.$axios.post("/platform/tour/signup/signupEligibilityNotice", { matterId: row.matterId }).then((res) => {
if (res.code !== 0) {
vant.Dialog.alert({
title: "温馨提醒",
message: res.msg || "当前不能报名",
confirmButtonColor: "#1867b0"
})
return
}
const data = res.data || {}
if (!data.noticeRequired) {
go()
return
}
if (data.canApply) {
vant.Dialog.confirm({
title: "温馨提醒",
message: data.message || "当前可报名,是否继续报名?",
confirmButtonText: "继续报名",
cancelButtonText: "稍后报名",
confirmButtonColor: "#1867b0"
}).then(go).catch(() => {})
} else {
vant.Dialog.alert({
title: "温馨提醒",
message: data.message || "当前不能报名",
confirmButtonColor: "#1867b0"
})
}
})
},
revokeSignup(row) {
vant.Dialog.confirm({
title: "提示",
message: "您确定要撤回吗?",
confirmButtonColor: "#ee2f2f"
}).then(() => {
this.$axios.post("/flow/common/revokeTask", { taskId: row.startTaskId }).then((resp) => {
if (resp.code === 0) {
vant.Toast.success(resp.msg || "撤回成功")
this.doSearch()
} else {
vant.Dialog.alert({ title: "提示", message: resp.msg || "撤回失败", confirmButtonColor: "#1867b0" })
}
})
}).catch(() => {})
},
doSearch() {
this.pageForm.pageNumber = 1
this.pageForm.totalCount = 0
this.list = []
this.finished = false
this.loadData()
},
toggleLineType(lineType) {
const active = this.pageForm.lineType === lineType && this.pageForm.directFamilyUnitLine === null
this.pageForm.lineType = active ? "" : lineType
this.pageForm.directFamilyUnitLine = null
this.doSearch()
},
toggleDirectFamilyLine() {
const active = this.pageForm.directFamilyUnitLine === true
this.pageForm.directFamilyUnitLine = active ? null : true
this.pageForm.lineType = ""
this.doSearch()
},
openUnionPopup() {
this.showUnionPopup = true
if (this.unionList.length <= 1) {
this.loadUnionOptions()
}
},
selectUnion(item) {
const unionId = item && item.id ? item.id : ""
this.showUnionPopup = false
if (this.pageForm.unionId === unionId) {
return
}
this.pageForm.unionId = unionId
this.doSearch()
},
loadUnionOptions() {
this.$axios.post("/platform/tour/signup/unionOptions").then((res) => {
if (res.code !== 0) {
return
}
const rows = res.data || []
this.unionList = [{id: "", name: "全部分工会"}].concat(rows.map((item) => {
return {
id: item.id || "",
name: item.name || "未命名分工会"
}
}))
})
},
loadData() {
if (this.finished) {
this.loading = false
return
}
this.loading = true
this.$axios.post("/platform/tour/signup/h5/pageData", this.pageForm).then((res) => {
this.loading = false
if (res.code !== 0) {
vant.Toast(res.msg || "线路加载失败")
this.finished = true
return
}
const data = res.data || {}
const rows = data.list || []
this.pageForm.totalCount = data.totalCount || 0
this.list = this.pageForm.pageNumber === 1 ? rows : this.list.concat(rows)
if (this.list.length >= this.pageForm.totalCount || rows.length === 0) {
this.finished = true
} else {
this.pageForm.pageNumber += 1
}
}).catch(() => {
this.loading = false
this.finished = true
})
},
onLine(row) {
if (!row || !row.lineId || !row.matterId) {
vant.Toast("线路信息不完整")
return
}
if (this.lineChecking) {
return
}
this.lineChecking = true
axios.post("/platform/tour/signup/signupDetail", $.param({ matterId: row.matterId }), {
headers: {
"Content-Type": "application/x-www-form-urlencoded;charset=utf-8",
"x-requested-with": "XMLHttpRequest"
}
}).then((resp) => {
this.lineChecking = false
const res = resp.data || {}
if (res.code !== 0) {
vant.Dialog.alert({
title: "温馨提醒",
message: res.msg || "当前线路暂不能报名",
confirmButtonColor: "#1867b0"
})
return
}
window.location.href = "/platform/tour/signup/h5/lineInfo?matterId="
+ encodeURIComponent(row.matterId)
+ "&lineId="
+ encodeURIComponent(row.lineId)
}).catch((err) => {
this.lineChecking = false
vant.Dialog.alert({
title: "温馨提醒",
message: err && err.msg ? err.msg : "当前线路暂不能报名",
confirmButtonColor: "#1867b0"
})
})
}
},
created() {
this.loadUnionOptions()
this.loadData()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,453 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style scoped>
.tour-approval-page {
height: 100vh;
overflow: hidden;
display: flex;
flex-direction: column;
background: #f4f6f8;
box-sizing: border-box;
}
.tour-approval-sticky {
flex-shrink: 0;
background: #f4f6f8;
}
.tour-approval-list-scroll {
min-height: 0;
flex: 1;
overflow-y: auto;
padding-bottom: 18px;
-webkit-overflow-scrolling: touch;
box-sizing: border-box;
}
.tour-approval-search /deep/ .van-search__content,
.tour-approval-search .van-search__content {
border-radius: 8px;
background: #ffffff;
}
.tour-card-tags {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-bottom: 8px;
}
.tour-tag {
display: inline-flex;
align-items: center;
min-height: 22px;
padding: 0 8px;
border-radius: 4px;
background: #eef6ff;
color: #0b75bd;
font-size: 12px;
}
.tour-tag.gray {
background: #f1f5f9;
color: #64748b;
}
.tour-tag.warn {
background: #fff3e5;
color: #d97706;
}
.tour-approval-sheet {
max-height: 86vh;
overflow-y: auto;
padding-bottom: env(safe-area-inset-bottom);
background: #f4f6f8;
}
.tour-section {
margin: 10px 12px;
overflow: hidden;
border-radius: 8px;
background: #ffffff;
}
.tour-section-title {
position: relative;
padding: 14px 16px;
border-bottom: 1px solid #edf0f4;
color: #0b75bd;
font-size: 16px;
font-weight: 700;
line-height: 1.2;
}
.tour-section-title::before {
content: "";
position: absolute;
left: 16px;
top: 0;
width: 30px;
height: 4px;
border-radius: 0 0 3px 3px;
background: #0b75bd;
}
.tour-approval-actions {
display: flex;
gap: 8px;
padding: 10px 12px 14px;
background: #ffffff;
}
.tour-process-opinion {
white-space: pre-wrap;
word-break: break-word;
}
</style>
<div id="app" class="tour-approval-page" v-cloak>
<van-nav-bar title="分工会审核" left-text="返回" left-arrow @click-left="historyBack" fixed placeholder></van-nav-bar>
<van-sticky offset-top="46px" class="tour-approval-sticky">
<van-search
class="tour-approval-search"
v-model="pageForm.keyword"
placeholder="请输入工号/姓名/线路搜索"
show-action
clearable
input-align="left"
@search="doSearch"
@clear="doSearch">
<template #action>
<div @click="doSearch">搜索</div>
</template>
</van-search>
<van-dropdown-menu :close-on-click-outside="false" :close-on-click-overlay="false">
<van-dropdown-item :options="auditOptions" @change="doSearch" v-model="pageForm.audit"></van-dropdown-item>
<van-dropdown-item :options="lineTypeOptions" @change="doSearch" v-model="pageForm.lineType"></van-dropdown-item>
</van-dropdown-menu>
</van-sticky>
<div class="tour-approval-list-scroll">
<table-list api="/platform/tour/unionApproval/pageData" :page_form.sync="pageForm" ref="tableListRef" title="userName" @ready="onReady">
<template v-slot="{index,row}">
<div class="tour-card-tags">
<span v-if="row.jobNo" class="tour-tag">{{ row.jobNo }}</span>
<span v-if="row.lineType" class="tour-tag gray">{{ row.lineType }}</span>
<span v-if="row.overCostReimbursed" class="tour-tag warn">报销超出费用</span>
</div>
<table-column label="线路名称">{{ row.lineName || '' }}</table-column>
<table-column label="出行时段">{{ row.travelPeriod || '' }}</table-column>
<table-column label="报销超出费用">{{ row.overCostReimbursed ? '是' : '否' }}</table-column>
<table-column label="当前节点">{{ row.curTaskName || '' }}</table-column>
<table-column label="流程状态">
<enum-tag
v-if="row.instanceState !== null && row.instanceState !== undefined && row.instanceState !== ''"
:value="row.instanceState"
name="ProcessInstanceStateEnum"
label_key="message"
size="small">
</enum-tag>
</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" v-if="row.taskState === 10" @click="onApproval(row)">
<i class="fa fa-check"></i>
<span>审核</span>
</div>
</template>
</table-list>
</div>
<tour-info ref="infoRef">
<div v-if="showApprovalForm" class="tour-section">
<div class="tour-section-title">{{ formData.taskName || '审核' }}</div>
<van-form ref="formRef">
<van-field
v-model="formData.tf_opinion"
name="tf_opinion"
label="审批意见"
placeholder="请输入审批意见"
:rules="[{ required: true, message: '请填写审批意见' }]"
required
type="textarea"
rows="3"
maxlength="200"
show-word-limit>
</van-field>
</van-form>
<div class="tour-approval-actions">
<van-button type="danger" block @click="handleTaskAction(6)">退回</van-button>
<van-button type="danger" block @click="handleTaskAction(2)">拒绝</van-button>
<van-button type="primary" block @click="handleTaskAction(1)">同意</van-button>
</div>
</div>
</tour-info>
</div>
<script nonce="${cspNonce!}">
const TOUR_INFO = {
template: /*language=HTML*/ `
<van-action-sheet v-model="visible" title="报名详情">
<div class="tour-approval-sheet">
<div class="tour-section">
<div class="tour-section-title">教职工信息</div>
<van-cell title="工号" :value="detail.jobNo || ''"></van-cell>
<van-cell title="姓名" :value="detail.userName || ''"></van-cell>
<van-cell title="性别" :value="detail.gender || ''"></van-cell>
<van-cell title="身份证号" :value="detail.idCard || ''"></van-cell>
<van-cell title="所在单位" :value="detail.unitName || ''"></van-cell>
<van-cell title="所在工会" :value="detail.unionName || ''"></van-cell>
</div>
<div class="tour-section">
<div class="tour-section-title">报名信息</div>
<van-cell title="报名线路" :value="detail.lineName || ''"></van-cell>
<van-cell title="线路类型" :value="detail.lineType || ''"></van-cell>
<van-cell title="出行时段" :value="detail.travelPeriod || ''"></van-cell>
<van-cell title="报名时间" :value="detail.signupTime || ''"></van-cell>
<van-cell title="报名酒店" :value="detail.hotelName || ''"></van-cell>
<van-cell title="旅行社" :value="detail.travelAgencyName || ''"></van-cell>
<van-cell v-if="fillBedInfo" title="床型" :value="detail.bedType || ''"></van-cell>
<van-cell v-if="fillBedInfo" title="床位信息" :value="detail.bedInfo || ''"></van-cell>
<van-cell v-if="fillBedInfo" title="意向拼床人" :value="detail.intendedRoommate || ''"></van-cell>
<van-cell title="携带家属" :value="familyText()"></van-cell>
<van-cell title="报销超出费用" :value="detail.overCostReimbursed ? '是' : '否'"></van-cell>
</div>
<div v-if="isDirectFamilyLine()" class="tour-section">
<div class="tour-section-title">直系亲属线路</div>
<van-cell title="亲属姓名" :value="directRelative.relativeName || ''"></van-cell>
<van-cell title="所在单位" :value="directRelative.unitName || ''"></van-cell>
<van-cell title="亲属关系" :value="directRelative.relationshipName || ''"></van-cell>
<van-cell title="线路名称" :value="directRelative.lineName || ''"></van-cell>
<van-cell title="出行开始" :value="directRelative.travelStartTime || ''"></van-cell>
<van-cell title="出行结束" :value="directRelative.travelEndTime || ''"></van-cell>
</div>
<div v-if="showFamilySection()" class="tour-section">
<div class="tour-section-title">家属信息</div>
<template v-for="(item,index) in familyData">
<van-cell :key="'title_' + index" :title="'家属' + (index + 1)"></van-cell>
<van-cell :key="'name_' + index" title="姓名" :value="item.familyName || ''"></van-cell>
<van-cell :key="'gender_' + index" title="性别" :value="item.gender || ''"></van-cell>
<van-cell :key="'id_' + index" title="身份证号" :value="item.idCard || ''"></van-cell>
<van-cell :key="'rel_' + index" title="关系" :value="item.relationship || ''"></van-cell>
<van-cell v-if="fillBedInfo" :key="'bed_' + index" title="床型" :value="item.bedType || ''"></van-cell>
</template>
</div>
<div v-if="doneTasks.length > 0" class="tour-section">
<div class="tour-section-title">流程记录</div>
<template v-for="task in doneTasks">
<van-cell-group :key="task.id" :title="task.displayName || task.taskName">
<van-cell title="办理时间" :value="task.finishTime || ''"></van-cell>
<van-cell title="办理结果">
<dict-tag :options="dict.type.PROCESS_TASK_SUBMIT_TYPE" :value="task.ext && task.ext.submitType"></dict-tag>
</van-cell>
<van-cell title="办理意见" v-if="task.taskFormData" class="direction-column-cell">
<div class="tour-process-opinion">{{ task.taskFormData.opinion || task.taskFormData.tf_opinion || '' }}</div>
</van-cell>
</van-cell-group>
</template>
</div>
<slot></slot>
</div>
</van-action-sheet>
`,
dicts: ["PROCESS_TASK_SUBMIT_TYPE"],
data() {
return {
visible: false,
row: {},
detail: {},
familyData: [],
directRelative: {},
allowFamily: false,
fillBedInfo: true,
directFamilyUnitLine: false,
doneTasks: []
}
},
methods: {
onOpen(row) {
this.row = row || {}
this.visible = true
this.detail = {}
this.familyData = []
this.directRelative = {}
this.allowFamily = false
this.fillBedInfo = true
this.directFamilyUnitLine = false
this.doneTasks = []
this.info()
this.getDoneTasks()
},
onClose() {
this.visible = false
},
info() {
this.$axios.post("/platform/tour/unionApproval/detail", { id: this.row.id }).then((res) => {
if (res.code === 0) {
const data = res.data || {}
this.detail = data.ledger || {}
this.detail.travelPeriod = data.travelPeriod || this.row.travelPeriod || ""
this.familyData = data.families || []
this.directRelative = data.directRelative || {}
this.allowFamily = this.toBoolean(data.allowFamily)
this.fillBedInfo = data.fillBedInfo === undefined || data.fillBedInfo === null || this.toBoolean(data.fillBedInfo)
this.directFamilyUnitLine = this.toBoolean(data.directFamilyUnitLine)
} else {
vant.Toast(res.msg || "查询失败")
}
})
},
getDoneTasks() {
this.$axios.post("/flow/common/doneTasks", { bizId: this.row.id }).then((res) => {
if (res.code === 0) {
this.doneTasks = res.data || []
}
})
},
toBoolean(value) {
return value === true || value === 1 || value === "1" || value === "true" || value === "TRUE"
},
isDirectFamilyLine() {
return this.directFamilyUnitLine || this.toBoolean(this.detail.directFamilyUnitLine) || !!(this.directRelative && this.directRelative.id)
},
hasFamily() {
return this.toBoolean(this.detail.hasFamily) || this.familyData.length > 0
},
showFamilySection() {
return this.allowFamily && this.hasFamily() && !this.isDirectFamilyLine()
},
familyText() {
if (this.isDirectFamilyLine() || !this.allowFamily) {
return "否"
}
return this.hasFamily() ? "是" : "否"
}
}
}
new Vue({
el: "#app",
store,
components: {
"tour-info": TOUR_INFO
},
data() {
return {
pageForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
pageOrderName: "signupTime",
pageOrderBy: "descending",
audit: false,
keyword: "",
lineType: ""
},
auditOptions: [
{ text: "未审核", value: false },
{ text: "已审核", value: true }
],
lineTypeOptions: [
{ text: "全部线路类型", value: "" }
],
formData: {},
showApprovalForm: false
}
},
methods: {
historyBack,
onReady() {
this.doSearch()
},
doSearch() {
this.$nextTick(async () => {
await this.loadLineTypeOptions()
this.pageForm.pageNumber = 1
this.pageForm.totalCount = 0
this.$refs.tableListRef.doSearch()
})
},
loadLineTypeOptions() {
return this.$axios.post("/platform/tour/unionApproval/lineTypeOptions", {
audit: this.pageForm.audit,
keyword: this.pageForm.keyword
}).then((res) => {
if (res.code === 0) {
const options = (res.data || []).map(item => ({
text: item.lineType,
value: item.lineType
})).filter(item => item.value)
this.lineTypeOptions = [{ text: "全部线路类型", value: "" }].concat(options)
if (this.pageForm.lineType && !this.lineTypeOptions.some(item => item.value === this.pageForm.lineType)) {
this.pageForm.lineType = ""
}
}
})
},
onView(row) {
this.showApprovalForm = false
this.$refs.infoRef.onOpen(row)
},
onApproval(row) {
this.showApprovalForm = true
this.formData = {
processTaskId: row.taskId,
taskName: row.curTaskName || row.taskName,
tf_opinion: ""
}
this.$refs.infoRef.onOpen(row)
},
async handleTaskAction(val) {
try {
await this.$refs.formRef.validate()
} catch (e) {
return
}
this.$dialog.confirm({
title: "提示",
message: "您确定要提交吗?"
}).then(() => {
const loading = this.$toast.loading({
message: "加载中...",
forbidClick: true,
overlay: true,
duration: 0
})
this.$axios.post("/flow/common/executeTask", {
data: JSON.stringify({
...this.formData,
submitType: val
})
}).then((res) => {
if (res.code === 0) {
this.$refs.infoRef.onClose()
this.$toast.success(res.msg || "操作成功")
this.doSearch()
} else {
this.$toast(res.msg || "操作失败")
}
}).finally(() => {
loading.close()
})
}).catch(() => {})
}
}
})
</script>
<!--#
}
#-->