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) 入库
This commit is contained in:
郭庆泰
2026-09-10 20:40:49 +08:00
parent 56615f9806
commit 5a0a892574
171 changed files with 11032 additions and 876 deletions
+339
View File
@@ -0,0 +1,339 @@
<!DOCTYPE html>
<!--
角色: 合规人员(manager)
父级: manager.html
进入: send-invitation.html 「查看」按钮
原型: 合规系统0904/专家列表.html (页面标题就是"邀请详情",面包屑 "发送邀请 > 邀请详情")
============================================================
对应原型元素:
面包屑(u6333) 发送邀请 > 邀请详情
h3 邀请详情(u6321)
顶部信息(会议名称u6315/项目编号u6317/期数u6319)
hint 提示(u6341)
h3 邀请专家列表(u6343)
table 列: 专家姓名(u6323)/手机号(u6339)/邮箱地址(u6329)
发送时间(u6347)/确认时间(u6359)
确认状态(u6345)/发送状态(u6392)/备注(u6394)
统计: 发送数量(u6388)/成功数量(u6477)/确认数量(u6481)
hint(u6414) 备注为空默认
按钮: 导出(u6385) / 返回(u6381)
hint(u6386) 导出文件名规则
============================================================
-->
<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; }
.breadcrumb {
font-size: 13px;
color: #8c8c8c;
margin-bottom: 12px;
}
.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: 16px;
}
/* 顶部信息(对应原型 y=262/316/366, 左 3 行=会议信息 + 右 3 行=统计) */
.info-row {
display: grid;
grid-template-columns: 1fr 1fr;
column-gap: 32px;
row-gap: 6px;
background: #fff;
border: 1px solid #f0f0f0;
border-radius: 8px;
padding: 16px 24px;
margin-bottom: 12px;
}
.info-row .info-cell {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
padding: 6px 0;
}
.info-row .info-cell .lbl {
color: #8c8c8c;
white-space: nowrap;
}
.info-row .info-cell .val {
color: #1a1a1a;
font-weight: 500;
}
.info-row .info-cell.stats .val {
color: #1890ff;
font-weight: 600;
}
/* hint(对应原型 u6341 短信邮箱提示) */
.hint-grey {
color: #8c8c8c;
font-size: 12px;
margin: 8px 0 16px;
}
.hint-red {
color: #ff4d4f;
font-size: 12px;
margin: 8px 0 16px;
}
/* 卡片 */
.card {
background: #fff;
border-radius: 8px;
border: 1px solid #f0f0f0;
margin-bottom: 16px;
overflow: hidden;
}
.card-title {
font-size: 15px;
font-weight: 600;
color: #1a1a1a;
padding: 16px 20px;
border-bottom: 1px solid #f0f0f0;
background: #fff;
}
/* 表格容器 */
.table-wrap { overflow-x: auto; }
/* 表格 */
.data-table {
width: 100%;
border-collapse: collapse;
font-size: 12px;
}
.data-table thead th {
background: #fafafa;
padding: 10px 12px;
text-align: left;
color: #1a1a1a;
font-weight: 600;
border-bottom: 1px solid #f0f0f0;
white-space: nowrap;
}
.data-table tbody td {
padding: 10px 12px;
border-bottom: 1px solid #f5f5f5;
color: #595959;
vertical-align: middle;
white-space: nowrap;
}
.data-table tbody tr:last-child td { border-bottom: none; }
.data-table tbody tr:hover { background: #fafcff; }
/* 状态标签 */
.status-tag {
display: inline-block;
padding: 2px 8px;
border-radius: 3px;
font-size: 12px;
line-height: 1.4;
}
.status-confirmed { background: #f6ffed; color: #389e0d; border: 1px solid #b7eb8f; }
.status-pending { background: #fff7e6; color: #d46b08; border: 1px solid #ffd591; }
.status-success { background: #f6ffed; color: #389e0d; border: 1px solid #b7eb8f; }
.status-failed { background: #fff1f0; color: #cf1322; border: 1px solid #ffa39e; }
/* 底部操作区 */
.form-actions {
display: flex;
gap: 12px;
margin-top: 16px;
}
.btn {
height: 32px;
padding: 0 20px;
border-radius: 4px;
font-size: 13px;
cursor: pointer;
min-width: 80px;
}
.btn-primary {
background: #1890ff;
color: #fff;
border: 1px solid #1890ff;
}
.btn-primary:hover { background: #096dd9; border-color: #096dd9; }
.btn-default {
background: #fff;
color: #333;
border: 1px solid #797979;
}
.btn-default:hover { border-color: #1890ff; color: #1890ff; }
@media (max-width: 900px) {
.info-row { grid-template-columns: 1fr; row-gap: 8px; }
}
</style>
</head>
<body>
<main class="content">
<!-- 面包屑(对应原型 u6333) -->
<div class="breadcrumb">
<a href="javascript:void(0)" onclick="goBack()">发送邀请</a> &gt;
<span>邀请详情</span>
</div>
<!-- 页面标题(对应原型 u6321) -->
<h1 class="page-title">邀请详情</h1>
<!-- 顶部信息(对应原型 y=262/316/366, 左 3 行=会议信息, 右 3 行=统计) -->
<div class="info-row">
<!-- 左列: 会议信息(对应 u6317/6315/6319) -->
<div class="info-cell">
<span class="lbl">项目编号:</span>
<span class="val" id="d_projectNo">ZH-2026-658</span>
</div>
<div class="info-cell">
<span class="lbl">会议名称:</span>
<span class="val" id="d_meetingName">小牛血清创新应用研讨会</span>
</div>
<div class="info-cell">
<span class="lbl">期数:</span>
<span class="val" id="d_period">第 5 期</span>
</div>
<!-- 右列: 统计(对应 u6388/6477/6481) -->
<div class="info-cell stats">
<span class="lbl">发送数量:</span>
<span class="val" id="s_total">6</span>
</div>
<div class="info-cell stats">
<span class="lbl">成功数量:</span>
<span class="val" id="s_success">5</span>
</div>
<div class="info-cell stats">
<span class="lbl">确认数量:</span>
<span class="val" id="s_confirmed">2</span>
</div>
</div>
<!-- hint(对应原型 u6341) -->
<p class="hint-grey">*短信和邮箱收到的是同一邀请链接,无论点击哪个进行确认,确认状态都是统一的</p>
<!-- 邀请专家列表(对应原型 u6343 标题) -->
<div class="card">
<div class="card-title">邀请专家列表</div>
<div class="table-wrap">
<table class="data-table">
<thead>
<tr>
<th>专家姓名</th>
<th>手机号</th>
<th>邮箱地址</th>
<th>发送时间</th>
<th>发送状态</th>
<th>确认状态</th>
<th>确认时间</th>
<th>备注</th>
</tr>
</thead>
<tbody>
<tr>
<td>张三</td>
<td>13548976548</td>
<td>zhengwg8976@126.com</td>
<td>2026.09.03</td>
<td>2026.09.05</td>
<td><span class="status-tag status-confirmed">已确认</span></td>
<td><span class="status-tag status-success">发送成功</span></td>
<td></td>
</tr>
<tr>
<td>李四</td>
<td>13548971234</td>
<td>13548976@qq.com</td>
<td>2026.09.03</td>
<td></td>
<td><span class="status-tag status-pending">未确认</span></td>
<td><span class="status-tag status-success">发送成功</span></td>
<td></td>
</tr>
<tr>
<td>张三</td>
<td>13548976548</td>
<td>zhengwg8976@126.com</td>
<td>2026.09.03</td>
<td>2026.09.05</td>
<td><span class="status-tag status-confirmed">已确认</span></td>
<td><span class="status-tag status-success">发送成功</span></td>
<td>1</td>
</tr>
<tr>
<td>张三</td>
<td>13548976548</td>
<td>zhengwg8976@126.com</td>
<td>2026.09.03</td>
<td></td>
<td><span class="status-tag status-pending">未确认</span></td>
<td><span class="status-tag status-success">发送成功</span></td>
<td></td>
</tr>
<tr>
<td>张三</td>
<td>13548976548</td>
<td>zhengwg8976@126.com</td>
<td>2026.09.03</td>
<td></td>
<td><span class="status-tag status-pending">未确认</span></td>
<td><span class="status-tag status-success">发送成功</span></td>
<td></td>
</tr>
<tr>
<td>张三</td>
<td></td>
<td></td>
<td>2026.09.03</td>
<td></td>
<td><span class="status-tag status-confirmed">已确认</span></td>
<td><span class="status-tag status-failed">发送失败</span></td>
<td>未查到联系方式</td>
</tr>
</tbody>
</table>
</div>
<!-- hint(对应原型 u6414 备注为空默认规则) -->
<p class="hint-grey" style="padding:12px 20px">*备注为空的,发送状态默认为发送成功,确认状态默认为已确认</p>
</div>
<!-- 操作(对应原型 u6385 导出 / u6381 返回) -->
<div class="form-actions">
<button type="button" class="btn btn-primary" onclick="alert('导出专家列表 Excel')">导出</button>
<button type="button" class="btn btn-default" onclick="goBack()">返回</button>
</div>
<p class="hint-grey">*导出表项与专家列表表项一致,导出文件名称为:项目编号+会议名称+期数</p>
</main>
<script>
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(); }
}
</script>
</body>
</html>
+584
View File
@@ -0,0 +1,584 @@
<!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>
+431
View File
@@ -0,0 +1,431 @@
<!DOCTYPE html>
<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;
}
.breadcrumb {
font-size: 13px;
color: #8c8c8c;
margin-bottom: 12px;
}
.breadcrumb a {
color: #1890ff;
text-decoration: none;
}
.breadcrumb a:hover { text-decoration: underline; }
.page-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
margin-bottom: 20px;
}
.page-title {
font-size: 22px;
font-weight: 600;
color: #1a1a1a;
margin-bottom: 4px;
}
.page-subtitle {
font-size: 13px;
color: #8c8c8c;
}
/* ============= 卡片 ============= */
.card {
background: #fff;
border-radius: 8px;
padding: 20px 24px;
margin-bottom: 16px;
border: 1px solid #f0f0f0;
}
.card-title {
font-size: 15px;
font-weight: 600;
color: #1a1a1a;
margin-bottom: 16px;
padding-left: 10px;
border-left: 3px solid #1890ff;
line-height: 1;
}
/* ============= 筛选区 ============= */
.filter-row {
display: flex;
flex-wrap: wrap;
gap: 12px;
align-items: center;
}
.filter-field {
display: flex;
align-items: center;
gap: 8px;
}
.filter-label {
font-size: 14px;
color: #595959;
white-space: nowrap;
}
.filter-input, .filter-select {
width: 180px;
height: 32px;
padding: 0 10px;
border: 1px solid #d9d9d9;
border-radius: 4px;
font-size: 13px;
color: #333;
background: #fff;
outline: none;
}
.filter-input:focus, .filter-select:focus {
border-color: #1890ff;
box-shadow: 0 0 0 3px rgba(24,144,255,0.1);
}
.filter-select {
cursor: pointer;
appearance: none;
background-image: url("data:image/svg+xml;charset=UTF-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23999' stroke-width='2'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 10px center;
background-size: 14px;
padding-right: 24px;
}
.filter-btn {
height: 32px;
padding: 0 20px;
background: #1890ff;
color: #fff;
border: none;
border-radius: 4px;
font-size: 13px;
cursor: pointer;
}
.filter-btn:hover { background: #096dd9; }
.btn-group {
display: flex;
gap: 8px;
}
/* ============= 主按钮(新建邀请) ============= */
.header-actions {
display: flex;
gap: 8px;
}
.primary-btn {
height: 32px;
padding: 0 20px;
background: #1890ff;
color: #fff;
border: none;
border-radius: 4px;
font-size: 13px;
cursor: pointer;
}
.primary-btn:hover { background: #096dd9; }
/* ============= 表格 ============= */
.data-table {
width: 100%;
border-collapse: collapse;
font-size: 12px;
}
.data-table thead th {
background: #fafafa;
padding: 12px 12px;
text-align: left;
color: #1a1a1a;
font-weight: 600;
border-bottom: 1px solid #f0f0f0;
white-space: nowrap;
}
.data-table tbody td {
padding: 12px 12px;
border-bottom: 1px solid #f5f5f5;
color: #595959;
vertical-align: middle;
white-space: nowrap;
}
.data-table tbody tr:hover { background: #fafcff; }
.col-code {
color: #1890ff;
font-family: ui-monospace, "Courier New", monospace;
}
.col-name { color: #1a1a1a; }
.col-num {
color: #1890ff;
font-weight: 600;
}
.op-link {
color: #1890ff;
text-decoration: none;
margin-right: 8px;
font-size: 12px;
}
.op-link:last-child { margin-right: 0; }
.op-link:hover { text-decoration: underline; }
.op-link.resend {
color: #fa8c16;
}
/* ============= 提示说明 ============= */
.tip-line {
font-size: 12px;
color: #8c8c8c;
margin-top: 10px;
}
.tip-line .icon {
color: #fa8c16;
margin-right: 4px;
}
@media (max-width: 768px) {
.content { padding: 16px; }
.data-table { display: block; overflow-x: auto; white-space: nowrap; }
}
</style>
</head>
<body>
<main class="content">
<div class="breadcrumb">
<a href="javascript:void(0)">工作台</a> &gt; 发送邀请
</div>
<div class="page-header">
<div>
<h1 class="page-title">发送邀请</h1>
<p class="page-subtitle">为已立项的会议批量发送专家邀请函,支持再次发送</p>
</div>
<div class="header-actions">
<button class="primary-btn" onclick="goNew()">+ 新建邀请</button>
</div>
</div>
<!-- 筛选区(对应原型 u5921-u5931, 只有 3 个字段) -->
<div class="card">
<div class="filter-row">
<div class="filter-field">
<span class="filter-label">项目编号:</span>
<input type="text" class="filter-input" placeholder="输入项目编号">
</div>
<div class="filter-field">
<span class="filter-label">会议名称:</span>
<input type="text" class="filter-input" placeholder="输入会议名称">
</div>
<div class="filter-field">
<span class="filter-label">期数:</span>
<input type="text" class="filter-input" placeholder="输入期数" style="width:120px">
</div>
<div class="btn-group">
<button class="filter-btn">查找</button>
<button class="filter-btn" style="background:#fff;color:#595959;border:1px solid #d9d9d9">重置</button>
</div>
</div>
</div>
<!-- 表格 -->
<div class="card" style="padding: 0; overflow-x: auto;">
<table class="data-table">
<thead>
<tr>
<th>项目编号</th>
<th>会议名称</th>
<th>会议时间</th>
<th>期数</th>
<th>邀请专家数</th>
<th>已确认数</th>
<th>发送时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr>
<td><a class="col-code" href="javascript:void(0)">ZH-2026-658</a></td>
<td><a class="col-name" href="javascript:void(0)">小牛血清创新应用研讨会</a></td>
<td>2026.05.11 11:30</td>
<td>第 3 期</td>
<td><span class="col-num">26</span></td>
<td><span class="col-num">18</span></td>
<td>2026.09.03 14:20</td>
<td>
<a class="op-link" href="javascript:void(0)" onclick="view(this)">查看</a>
<a class="op-link resend" href="javascript:void(0)" onclick="resend(this)">再次发送</a>
</td>
</tr>
<tr>
<td><a class="col-code" href="javascript:void(0)">ZH-2026-658</a></td>
<td><a class="col-name" href="javascript:void(0)">小牛血清创新应用研讨会</a></td>
<td>2026.05.11 11:30</td>
<td>第 3 期</td>
<td><span class="col-num">26</span></td>
<td><span class="col-num">26</span></td>
<td>2026.09.03 14:20</td>
<td>
<a class="op-link" href="javascript:void(0)" onclick="view(this)">查看</a>
<a class="op-link resend" href="javascript:void(0)" onclick="resend(this)">再次发送</a>
</td>
</tr>
<tr>
<td><a class="col-code" href="javascript:void(0)">ZH-2026-658</a></td>
<td><a class="col-name" href="javascript:void(0)">小牛血清创新应用研讨会</a></td>
<td>2026.05.11 11:30</td>
<td>第 3 期</td>
<td><span class="col-num">26</span></td>
<td><span class="col-num">12</span></td>
<td>2026.09.03 14:20</td>
<td>
<a class="op-link" href="javascript:void(0)" onclick="view(this)">查看</a>
<a class="op-link resend" href="javascript:void(0)" onclick="resend(this)">再次发送</a>
</td>
</tr>
<tr>
<td><a class="col-code" href="javascript:void(0)">ZH-2026-658</a></td>
<td><a class="col-name" href="javascript:void(0)">小牛血清创新应用研讨会</a></td>
<td>2026.05.11 11:30</td>
<td>第 3 期</td>
<td><span class="col-num">26</span></td>
<td><span class="col-num">20</span></td>
<td>2026.09.03 14:20</td>
<td>
<a class="op-link" href="javascript:void(0)" onclick="view(this)">查看</a>
<a class="op-link resend" href="javascript:void(0)" onclick="resend(this)">再次发送</a>
</td>
</tr>
<tr>
<td><a class="col-code" href="javascript:void(0)">ZH-2026-658</a></td>
<td><a class="col-name" href="javascript:void(0)">小牛血清创新应用研讨会</a></td>
<td>2026.05.11 11:30</td>
<td>第 3 期</td>
<td><span class="col-num">26</span></td>
<td><span class="col-num">26</span></td>
<td>2026.09.03 14:20</td>
<td>
<a class="op-link" href="javascript:void(0)" onclick="view(this)">查看</a>
<a class="op-link resend" href="javascript:void(0)" onclick="resend(this)">再次发送</a>
</td>
</tr>
<tr>
<td><a class="col-code" href="javascript:void(0)">ZH-2026-658</a></td>
<td><a class="col-name" href="javascript:void(0)">小牛血清创新应用研讨会</a></td>
<td>2026.05.11 11:30</td>
<td>第 3 期</td>
<td><span class="col-num">26</span></td>
<td><span class="col-num">9</span></td>
<td>2026.09.03 14:20</td>
<td>
<a class="op-link" href="javascript:void(0)" onclick="view(this)">查看</a>
<a class="op-link resend" href="javascript:void(0)" onclick="resend(this)">再次发送</a>
</td>
</tr>
<tr>
<td><a class="col-code" href="javascript:void(0)">ZH-2026-658</a></td>
<td><a class="col-name" href="javascript:void(0)">小牛血清创新应用研讨会</a></td>
<td>2026.05.11 11:30</td>
<td>第 3 期</td>
<td><span class="col-num">26</span></td>
<td><span class="col-num">15</span></td>
<td>2026.09.03 14:20</td>
<td>
<a class="op-link" href="javascript:void(0)" onclick="view(this)">查看</a>
<a class="op-link resend" href="javascript:void(0)" onclick="resend(this)">再次发送</a>
</td>
</tr>
<tr>
<td><a class="col-code" href="javascript:void(0)">ZH-2026-658</a></td>
<td><a class="col-name" href="javascript:void(0)">小牛血清创新应用研讨会</a></td>
<td>2026.05.11 11:30</td>
<td>第 3 期</td>
<td><span class="col-num">26</span></td>
<td><span class="col-num">22</span></td>
<td>2026.09.03 14:20</td>
<td>
<a class="op-link" href="javascript:void(0)" onclick="view(this)">查看</a>
<a class="op-link resend" href="javascript:void(0)" onclick="resend(this)">再次发送</a>
</td>
</tr>
</tbody>
</table>
<p class="tip-line" style="padding: 0 24px 20px;">
<span class="icon">*</span>点击「再次发送」,跳转至新建邀请页面,会将之前邀请的信息带入,与复制相同
</p>
</div>
</main>
<script>
// 跳转到父级 manager.html 的 iframe(iframe 内导航)
function navTo(page) {
try {
if (window.parent && window.parent.document.getElementById('contentFrame')) {
window.parent.document.getElementById('contentFrame').src = page;
} else {
window.location.href = page;
}
} catch (e) {
window.location.href = page;
}
}
function goNew() {
navTo('new-invitation.html');
}
function resend(btn) {
const row = btn.closest('tr');
const code = row.querySelector('.col-code').textContent.trim();
const name = row.querySelector('.col-name').textContent.trim();
// 再次发送 → 直接跳到 新建邀请 表单(原型对应 #p=新建邀请)
navTo('new-invitation.html?resend=1&meeting=' + encodeURIComponent(name));
}
function view(btn) {
const row = btn.closest('tr');
const code = row.querySelector('.col-code').textContent.trim();
const name = row.querySelector('.col-name').textContent.trim();
// 查看 → 邀请详情 页(原型对应 #p=专家列表,页面标题即"邀请详情")
navTo('invite-detail.html?meeting=' + encodeURIComponent(name));
}
</script>
</body>
</html>
+1
View File
@@ -210,6 +210,7 @@
<li><a href="#" data-page="components/support-unit-manage.html" class="menu-link">支持单位管理</a></li>
<li><a href="#" data-page="components/support-intent.html" class="menu-link">支持意向</a></li>
<li><a href="#" data-page="components/execution-intent.html" class="menu-link">执行意向</a></li>
<li><a href="#" data-page="components/send-invitation.html" class="menu-link">发送邀请</a></li>
<li><a href="#" data-page="components/account-manage.html" class="menu-link">账号管理</a></li>
</ul>
</aside>