Files
guoju0808/proto/html/components/new-invitation.html
T
郭庆泰 5a0a892574 feat: 邀请函邮件发送 + 首次设密/密码为空 + 姓名单一可信源
邀请发送 (菜单暂隐藏, 待确认参数见 发送邀请-待确认参数.md):
- 后端: BizInvite / BizInviteRecipient + Controller/Service/Mapper/XML
- 邮件: InviteMailSender (spring-boot-starter-mail SMTP) + application*.yml 邮件配置
- 上传进度: UploadProgressRegistry + UploadProgressController
- 前端: InviteList / InviteNew / InviteDetail / InviteView + api/business/invite.js
- 原型: proto/html/components/invite-detail / new-invitation / send-invitation

登录/账号:
- 首次设密: /getInfo 返回 isPasswordEmpty, SysProfileController 密码为空时跳过旧密码校验, ForcePasswordDialog 强制弹窗
- 姓名单一可信源 resolveDisplayName: doctor→biz_expert.name, sponsor/executor→biz_person.name, 其余回退 nick_name
- OA compliance 门禁改为按手机号查 ecology 视图 (不再限定 manager/leader)

其它:
- OSS zip 在线查看 (列清单+取单文件, 公开只读) + SecurityConfig permitAll
- doctor 项目详情 ProjectDetail.vue
- 数据库/测试/设计文档 (md) 入库
2026-09-10 20:40:49 +08:00

584 lines
23 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<!--
角色: 合规人员(manager)
父级: manager.html
进入:
- send-invitation.html 的「+ 新建邀请」按钮 (无参数)
- send-invitation.html / invite-detail.html 的「再次发送」按钮 (resend=1)
原型: 合规系统0904/新建邀请.html
============================================================
对应原型元素:
面包屑(u6156) → 发送邀请 > 新建邀请
h1 标题(u6146) → 新建邀请
字段(u6137-6161,6166,6203,6208,6248,6250):
会议名称(*) / 项目编号(*) / 期数(*) / 会议时间(*) / 发送时间(*) / 专家(*)
+ 选择专家按钮(u6168)
+ 邀请内容(*)(u6169)
红头文件(u6184) + 选择文件(u6186)
邀请形式(u6193-6198): 短信 / 邮件 checkbox
提示文案(u6180,6182,6199,6201,6208,6248,6250)
按钮: 确定发送(u6170) / 返回(u6172) / 邀请预览(u6174)
弹窗:
- 选择专家(u6252)
- 邀请函预览(u6285)
- 发送成功(u6308)
============================================================
-->
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>新建邀请 - 项目管理系统</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif;
color: #333;
background: #fafafa;
min-height: 100vh;
}
.content { padding: 24px 32px; max-width: 1100px; }
.breadcrumb {
font-size: 13px;
color: #8c8c8c;
margin-bottom: 16px;
}
.breadcrumb a { color: #1890ff; text-decoration: none; }
.breadcrumb a:hover { text-decoration: underline; }
.page-title {
font-size: 22px;
font-weight: 600;
color: #1a1a1a;
margin-bottom: 20px;
}
.resend-hint { display: none; }
.card {
background: #fff;
border-radius: 8px;
padding: 24px 28px;
margin-bottom: 16px;
border: 1px solid #f0f0f0;
}
.card-title {
font-size: 15px;
font-weight: 600;
color: #1a1a1a;
margin-bottom: 18px;
padding-left: 10px;
border-left: 3px solid #1890ff;
line-height: 1;
}
.form-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 18px 32px;
}
.form-row {
display: flex;
align-items: center;
}
.form-row.full { grid-column: 1 / -1; }
.form-label {
flex: 0 0 110px;
font-size: 14px;
color: #595959;
text-align: right;
padding-right: 16px;
white-space: nowrap;
}
.form-label .req {
color: #ff4d4f;
margin-right: 4px;
font-weight: 700;
}
.form-input, .form-select, .form-textarea {
flex: 1;
min-width: 0;
padding: 6px 12px;
border: 1px solid #d9d9d9;
border-radius: 4px;
font-size: 13px;
color: #333;
background: #fff;
outline: none;
transition: all 0.2s;
}
.form-input { height: 32px; }
.form-textarea { min-height: 120px; resize: vertical; font-family: inherit; }
.form-input:focus, .form-select:focus, .form-textarea:focus {
border-color: #1890ff;
box-shadow: 0 0 0 3px rgba(24,144,255,0.1);
}
.inline-btn {
flex: 0 0 auto;
margin-left: 8px;
padding: 6px 16px;
background: #fff;
border: 1px solid #d9d9d9;
border-radius: 4px;
font-size: 13px;
color: #333;
cursor: pointer;
}
.inline-btn:hover { border-color: #1890ff; color: #1890ff; }
.checkbox-group {
display: flex;
gap: 24px;
flex: 1;
align-items: center;
}
.checkbox-group label {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: #333;
cursor: pointer;
}
/* 红字提示(对应原型 u6248/6250 等红字) */
.hint-red {
color: #ff4d4f;
font-size: 12px;
margin-top: 6px;
grid-column: 1 / -1;
}
.hint-grey {
color: #8c8c8c;
font-size: 12px;
margin-top: 6px;
grid-column: 1 / -1;
}
/* 操作区(对应原型 u6170/6172/6174) */
.form-actions {
display: flex;
gap: 12px;
margin-top: 8px;
}
.btn {
height: 40px;
padding: 0 32px;
border-radius: 5px;
font-size: 14px;
cursor: pointer;
min-width: 120px;
transition: all 0.2s;
}
.btn-primary {
background: #169bd5;
color: #fff;
border: 1px solid #169bd5;
}
.btn-primary:hover { background: #0f8db8; border-color: #0f8db8; }
.btn-default {
background: #fff;
color: #333;
border: 1px solid #797979;
}
.btn-default:hover { border-color: #1890ff; color: #1890ff; }
/* 短信/邮件内容预览(对应原型 u6180/6182/6187/6189/6191) */
.preview-box {
background: #fafafa;
border: 1px dashed #d9d9d9;
border-radius: 4px;
padding: 14px 18px;
font-size: 13px;
color: #595959;
line-height: 1.8;
flex: 1;
min-width: 0;
}
.preview-box .label { color: #8c8c8c; font-size: 12px; margin-bottom: 4px; }
.preview-box .link {
color: #1890ff;
word-break: break-all;
}
/* 弹窗(对应原型 u6252/6285/6308 动态面板) */
.modal-mask {
display: none;
position: fixed;
inset: 0;
background: rgba(0,0,0,0.45);
z-index: 1000;
align-items: center;
justify-content: center;
}
.modal-mask.show { display: flex; }
.modal {
background: #fff;
border-radius: 8px;
width: 560px;
max-width: calc(100vw - 32px);
max-height: calc(100vh - 64px);
display: flex;
flex-direction: column;
overflow: hidden;
}
.modal-header {
padding: 16px 24px;
border-bottom: 1px solid #f0f0f0;
display: flex;
align-items: center;
justify-content: space-between;
}
.modal-title {
font-size: 16px;
font-weight: 600;
color: #1a1a1a;
}
.modal-close {
cursor: pointer;
color: #8c8c8c;
font-size: 20px;
line-height: 1;
background: none;
border: none;
}
.modal-body {
padding: 20px 24px;
overflow-y: auto;
flex: 1;
}
.modal-footer {
padding: 12px 24px;
border-top: 1px solid #f0f0f0;
display: flex;
justify-content: flex-end;
gap: 8px;
}
.search-bar {
margin-bottom: 12px;
}
.search-bar input {
width: 100%;
height: 32px;
padding: 0 12px;
border: 1px solid #d9d9d9;
border-radius: 4px;
font-size: 13px;
outline: none;
}
.search-bar input:focus {
border-color: #1890ff;
}
/* 选择专家 弹窗列表 */
.expert-list {
border: 1px solid #f0f0f0;
border-radius: 4px;
max-height: 200px;
overflow-y: auto;
}
.expert-item {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 14px;
border-bottom: 1px solid #f5f5f5;
font-size: 13px;
}
.expert-item:last-child { border-bottom: none; }
.expert-item input { cursor: pointer; }
.import-row {
display: flex;
align-items: center;
gap: 12px;
margin: 12px 0;
padding: 10px 12px;
background: #fafafa;
border-radius: 4px;
}
.import-row .link {
color: #1890ff;
font-size: 13px;
cursor: pointer;
text-decoration: none;
}
/* 邀请函预览 */
.letter-preview {
background: #fff;
border: 1px solid #e8e8e8;
padding: 24px 28px;
font-size: 14px;
line-height: 2;
color: #1a1a1a;
border-radius: 4px;
}
.letter-preview .meta {
color: #8c8c8c;
font-size: 12px;
margin-bottom: 12px;
}
.letter-preview .confirm-btn {
display: inline-block;
margin-top: 12px;
padding: 8px 24px;
background: #169bd5;
color: #fff;
border-radius: 4px;
font-size: 13px;
}
.file-link {
color: #1890ff;
margin-right: 16px;
font-size: 13px;
cursor: pointer;
text-decoration: none;
}
@media (max-width: 768px) {
.content { padding: 16px; }
.form-grid { grid-template-columns: 1fr; }
.modal { width: 100%; }
}
</style>
</head>
<body>
<main class="content">
<!-- 面包屑(对应原型 u6156) -->
<div class="breadcrumb">
<a href="javascript:void(0)" onclick="goBack()">发送邀请</a> &gt;
<span>新建邀请</span>
</div>
<!-- h1 标题(对应原型 u6146),再次发送时也保持 "新建邀请" 不变 -->
<h1 class="page-title" id="pageTitle">新建邀请</h1>
<!-- 主表单(对应原型 u6137-6169,6203-6248) -->
<div class="card">
<div class="form-grid">
<!-- 会议名称(*) -->
<div class="form-row">
<span class="form-label"><span class="req">*</span>会议名称:</span>
<input type="text" class="form-input" id="f_meetingName" placeholder="请输入会议名称">
</div>
<!-- 项目编号(*) -->
<div class="form-row">
<span class="form-label"><span class="req">*</span>项目编号:</span>
<input type="text" class="form-input" id="f_projectNo" placeholder="请输入项目编号">
</div>
<!-- 期数(*) -->
<div class="form-row">
<span class="form-label"><span class="req">*</span>期数:</span>
<input type="text" class="form-input" id="f_period" placeholder="请输入期数">
</div>
<!-- 会议时间(*) -->
<div class="form-row">
<span class="form-label"><span class="req">*</span>会议时间:</span>
<input type="text" class="form-input" id="f_meetingTime" placeholder="选择会议时间(到年月日即可)">
</div>
<!-- 发送时间(*) -->
<div class="form-row">
<span class="form-label"><span class="req">*</span>发送时间:</span>
<input type="text" class="form-input" id="f_sendTime" placeholder="选择发送时间(默认当前时间)">
</div>
<!-- 专家(*) + 选择专家按钮(u6168) -->
<div class="form-row">
<span class="form-label"><span class="req">*</span>专家:</span>
<input type="text" class="form-input" id="f_expert" placeholder="选择或导入专家" readonly>
<button type="button" class="inline-btn" onclick="openExpertModal()">选择专家</button>
</div>
<!-- 邀请内容(*) -->
<div class="form-row full">
<span class="form-label"><span class="req">*</span>邀请内容:</span>
<textarea class="form-textarea" id="f_content" placeholder="请输入邀请函内容"></textarea>
</div>
<!-- 红字:发送时间规则(对应原型 u6248) -->
<div class="hint-red">*发送时间不能晚于会议时间,如果设置的发送时间晚于会议时间,则弹出报错提示</div>
<div class="hint-grey">*会议时间和发送时间到年月日即可</div>
<div class="hint-grey">*发送时间默认为当前时间,支持修改,该时间只显示在详情列表里,不做定时发送</div>
<!-- 红头文件(u6184) + 选择文件(u6186) -->
<div class="form-row full">
<span class="form-label">红头文件:</span>
<input type="text" class="form-input" placeholder="未选择任何文件" readonly>
<button type="button" class="inline-btn" onclick="alert('打开文件选择对话框')">选择文件</button>
</div>
<!-- 邀请形式(u6193/u6195/u6197) -->
<div class="form-row full">
<span class="form-label">邀请形式:</span>
<div class="checkbox-group">
<label><input type="checkbox" id="chkSms" checked> 短信</label>
<label><input type="checkbox" id="chkEmail" checked> 邮件</label>
</div>
</div>
<div class="hint-red">*提示:如邀请形式全部勾选,则两种方式均会发送,发送前请确认专家是否有该联系方式</div>
</div>
<!-- 按钮(对应原型 u6170/6172/6174) -->
<div class="form-actions" style="margin-top:24px">
<button type="button" class="btn btn-primary" onclick="send()">确定发送</button>
<button type="button" class="btn btn-default" onclick="openPreview()">邀请预览</button>
<button type="button" class="btn btn-default" onclick="goBack()">返回</button>
</div>
</div>
</main>
<!-- 弹窗1:选择专家(对应原型 u6252 动态面板) -->
<div class="modal-mask" id="modalExpert">
<div class="modal">
<div class="modal-header">
<span class="modal-title">选择专家</span>
<button class="modal-close" onclick="closeModal('modalExpert')">×</button>
</div>
<div class="modal-body">
<div class="search-bar">
<input type="text" placeholder="搜索专家姓名/手机号/邮箱">
</div>
<div class="expert-list">
<label class="expert-item"><input type="checkbox" value="张三"> 张三</label>
<label class="expert-item"><input type="checkbox" value="李四"> 李四</label>
<label class="expert-item"><input type="checkbox" value="王五"> 王五</label>
<label class="expert-item"><input type="checkbox" value="张三"> 张三</label>
</div>
<div class="import-row">
<button type="button" class="inline-btn" onclick="alert('打开导入对话框')">导入专家</button>
<a class="link" href="javascript:void(0)" onclick="alert('下载导入模板')">下载导入模板</a>
</div>
<div class="hint-grey">*专家可以从已有的专家库中进行选择,也支持批量导入</div>
<div class="hint-grey" style="margin-top:4px">*专家库中选择的专家为必发短信或邮件,批量导入的专家,备注写 1 的为发,备注为空的为不发</div>
<div class="hint-grey" style="margin-top:4px">*模板字段为:姓名,手机号,邮箱,备注</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" style="min-width:80px;height:32px;padding:0 16px" onclick="closeModal('modalExpert')">取消</button>
<button type="button" class="btn btn-primary" style="min-width:80px;height:32px;padding:0 16px" onclick="confirmExpert()">确定</button>
</div>
</div>
</div>
<!-- 弹窗2:邀请函预览(对应原型 u6285 动态面板) -->
<div class="modal-mask" id="modalPreview">
<div class="modal" style="width:680px">
<div class="modal-header">
<span class="modal-title">邀请函</span>
<button class="modal-close" onclick="closeModal('modalPreview')">×</button>
</div>
<div class="modal-body">
<div class="letter-preview">
<div class="meta">会议日程(略) · 会议日期:2026.05.11 · 项目名称:小牛血清创新应用研讨会</div>
<p>尊敬的 XX 教授:</p>
<p>您好!由衷感谢您长期以来对北京整合医学学会工作的鼎力支持。您报名参与的 <strong>【项目名称】</strong>,现定于【会议日期】正式举办。我们诚挚邀请担任【专家角色】。</p>
<p>再次感谢您的支持与参与,期待与您相会!</p>
<p style="margin-top:16px">北京整合医学学会</p>
<p>XXXX 年 XX 月 XX 日</p>
<div style="text-align:center;margin-top:20px">
<span class="confirm-btn">确认参加</span>
</div>
</div>
<div style="margin-top:14px;font-size:13px">
<a class="file-link" href="javascript:void(0)" onclick="alert('预览红头文件')">查看邀请文件</a>
<a class="file-link" href="javascript:void(0)" onclick="alert('下载红头文件')">下载邀请文件</a>
</div>
<div class="hint-grey" style="margin-top:8px">*点击查看邀请文件,可预览上传的红头文件</div>
<div class="hint-grey" style="margin-top:4px">*发送邀请为链接,点击链接后,打开页面</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" style="min-width:80px;height:32px;padding:0 16px" onclick="closeModal('modalPreview')">关闭预览</button>
</div>
</div>
</div>
<!-- 弹窗3:发送成功(对应原型 u6308 动态面板) -->
<div class="modal-mask" id="modalSent">
<div class="modal" style="width:420px">
<div class="modal-body" style="text-align:center;padding:32px 24px">
<p style="font-size:16px;color:#1a1a1a;margin-bottom:8px">邀请已发送,请点击专家数查看发送详情</p>
</div>
<div class="modal-footer" style="justify-content:center">
<button type="button" class="btn btn-primary" style="min-width:80px;height:32px;padding:0 16px" onclick="closeModal('modalSent');goBack()">确定</button>
</div>
</div>
</div>
<script>
// 再次发送 入口(resend=1)——按原型说法"跳转至新建邀请页面,会将之前邀请的信息带入"
// 页面与新建邀请完全一致,仅把上次内容预填到表单里
(function() {
var params = new URLSearchParams(location.search);
if (params.get('resend') !== '1') return;
var meeting = params.get('meeting') || '小牛血清创新应用研讨会';
document.getElementById('f_meetingName').value = meeting;
document.getElementById('f_projectNo').value = 'ZH-2026-658';
document.getElementById('f_period').value = '3';
document.getElementById('f_meetingTime').value = '2026.05.11';
document.getElementById('f_sendTime').value = new Date().toISOString().slice(0,10);
document.getElementById('f_content').value = '北京整合医学学会诚挚邀请您参加【' + meeting + '】,请您点击确认。';
})();
// 弹窗控制
function openExpertModal() { openModal('modalExpert'); }
function openPreview() { openModal('modalPreview'); }
function openModal(id) { document.getElementById(id).classList.add('show'); }
function closeModal(id) { document.getElementById(id).classList.remove('show'); }
function confirmExpert() {
var cbs = document.querySelectorAll('#modalExpert input[type="checkbox"]:checked');
var names = Array.from(cbs).map(function(c){return c.value;});
document.getElementById('f_expert').value = names.join(', ') || '';
closeModal('modalExpert');
}
function send() {
var required = ['f_meetingName','f_projectNo','f_period','f_meetingTime','f_sendTime','f_expert','f_content'];
for (var i = 0; i < required.length; i++) {
var el = document.getElementById(required[i]);
if (!el.value.trim()) { alert('请填写完整必填项:' + el.previousElementSibling.textContent); el.focus(); return; }
}
// 发送时间不能晚于会议时间
var mt = document.getElementById('f_meetingTime').value;
var st = document.getElementById('f_sendTime').value;
if (st && mt && st > mt) {
alert('发送时间不能晚于会议时间');
return;
}
openModal('modalSent');
}
function goBack() {
try {
if (window.parent && window.parent.document.getElementById('contentFrame')) {
window.parent.document.getElementById('contentFrame').src = 'send-invitation.html';
} else {
history.back();
}
} catch (e) { history.back(); }
}
// 点击遮罩关闭
document.querySelectorAll('.modal-mask').forEach(function(m){
m.addEventListener('click', function(e){
if (e.target === m) m.classList.remove('show');
});
});
</script>
</body>
</html>