Files
郭庆泰 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

340 lines
13 KiB
HTML

<!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>