feat: 健康体检体检年份筛选控件统一及PRD文档与截图补充

- 管理端健康体检各页体检年份筛选由原生select替换为自定义下拉控件
- 导航标签精简(体检预约/已选项目/已检员工等)
- 单位报表未检人数弹窗移除未检原因列
- 补充健康体检管理端与员工端PRD文档、截图脚本及页面截图
This commit is contained in:
liuyunqin
2026-09-14 19:20:51 +08:00
parent 39547a8bb6
commit dcacbe289b
160 changed files with 4869 additions and 398 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,155 @@
/**
* 健康体检(员工端)PRD 截图脚本
* 规则(同 health-monitor/doc/screenshot-final.mjs):
* - 标准 phone-shell 尺寸 390×844
* - 短页面(实测内容 < 844):保持 844 高度,外壳完整
* - 长页面(实测内容 > 844):phone-shell 解除高度限制,按真实内容展开
* - 结果页(booking/gastro/cta-date-selected)通过导航栏演示下拉切换成功/失败两态
*/
import { chromium } from 'playwright';
import { fileURLToPath } from 'url';
import path from 'path';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const docDir = __dirname;
const root = path.resolve(__dirname, '..');
// mode: auto(短页面最低 844 / 长页面真实高度)
const tasks = [
// ========== 模块主页 ==========
{ html: 'index.html', out: 'index' },
{ html: 'index-inactive.html', out: 'index-inactive' },
// ========== 体检预约(通用 + 常规体检流程) ==========
{ html: 'booking-type.html', out: 'booking-type' },
{ html: 'booking-hospital.html', out: 'booking-hospital' },
{ html: 'booking-hospital.html', out: 'booking-hospital-dialog', query: '?from=booking-type' },
{ html: 'booking-project.html', out: 'booking-project' },
{ html: 'booking-project.html', out: 'booking-project-required', clickText: '必检项目' },
{ html: 'booking-project-selected.html', out: 'booking-project-selected' },
{ html: 'booking-date.html', out: 'booking-date' },
{ html: 'booking-date-selected.html', out: 'booking-date-selected' },
{ html: 'booking-date-selected.html', out: 'booking-date-selected-fail', demo: 'fail' },
{ html: 'project-detail.html', out: 'project-detail', query: '?id=general' },
// ========== 胃肠镜体检流程 ==========
{ html: 'gastro-hospital.html', out: 'gastro-hospital' },
{ html: 'gastro-questionnaire.html', out: 'gastro-questionnaire' },
{ html: 'gastro-questionnaire.html', out: 'gastro-questionnaire-tangdu', query: '?hospital=tangdu' },
{ html: 'gastro-date.html', out: 'gastro-date' },
{ html: 'gastro-date-selected.html', out: 'gastro-date-selected' },
{ html: 'gastro-date-selected.html', out: 'gastro-date-selected-fail', demo: 'fail' },
{ html: 'gastro-preselect.html', out: 'gastro-preselect' },
{ html: 'gastro-preselect-done.html', out: 'gastro-preselect-done' },
// ========== 冠脉CTA体检流程 ==========
{ html: 'cta-hospital.html', out: 'cta-hospital' },
{ html: 'cta-date.html', out: 'cta-date' },
{ html: 'cta-date-selected.html', out: 'cta-date-selected' },
{ html: 'cta-date-selected.html', out: 'cta-date-selected-fail', demo: 'fail' },
// ========== 现场体检 ==========
{ html: 'my-appointments-all.html', out: 'my-appointments-all' },
{ html: 'my-appointments-all.html', out: 'my-appointments-all-gastro', query: '?type=gastro&status=pending' },
{ html: 'my-appointments-all.html', out: 'my-appointments-all-cta', query: '?type=cta&status=pending' },
{ html: 'hospital-checkin.html', out: 'hospital-checkin' },
// ========== 体检报告 ==========
{ html: 'reports.html', out: 'reports' },
{ html: 'report-query.html', out: 'report-query' },
{ html: 'report-cta.html', out: 'report-cta' },
{ html: 'report-gastro.html', out: 'report-gastro' },
{ html: 'indicator-compare.html', out: 'indicator-compare' },
{ html: 'indicator-compare-detail.html', out: 'indicator-compare-detail', query: '?item=bmi' },
// ========== 消息 / 咨询 / 操作指引 ==========
{ html: 'messages.html', out: 'messages' },
{ html: 'message-detail.html', out: 'message-detail', query: '?id=report-2026' },
{ html: 'message-detail.html', out: 'message-detail-ai', query: '?id=warn-indicator' },
{ html: 'consult.html', out: 'consult' },
{ html: 'process-guide.html', out: 'process-guide' },
{ html: 'process-guide-detail.html', out: 'process-guide-detail', query: '?type=image&id=booking' },
{ html: 'process-guide-detail.html', out: 'process-guide-detail-video', query: '?type=video&id=gastro' },
];
(async () => {
const browser = await chromium.launch({ args: ['--no-sandbox', '--disable-setuid-sandbox'] });
const context = await browser.newContext({ viewport: { width: 390, height: 4000 } });
for (const t of tasks) {
const page = await context.newPage();
const filePath = path.join(root, t.html);
const url = `file:///${filePath.replace(/\\/g, '/')}${t.query || ''}`;
await page.goto(url, { waitUntil: 'networkidle' });
await page.waitForTimeout(300);
// 演示下拉切换(预约结果页成功/失败两态)
if (t.demo) {
await page.evaluate((v) => {
const sel = document.querySelector('#demoSelect, .demo-select');
if (sel) {
sel.value = v;
sel.dispatchEvent(new Event('change', { bubbles: true }));
if (typeof setResult === 'function') setResult(v);
}
}, t.demo);
await page.waitForTimeout(250);
}
// 按文本点击(如「必检项目」Tab)
if (t.clickText) {
await page.evaluate((text) => {
const els = Array.from(document.querySelectorAll('div,span,button,li'));
const target = els.filter(el => el.textContent.trim() === text && el.offsetParent).pop();
if (target) target.click();
}, t.clickText);
await page.waitForTimeout(250);
}
// 解除 phone-shell 高度限制:短页面保底 844,长页面按内容展开
await page.evaluate(() => {
const shell = document.querySelector('.phone-shell');
if (shell) {
shell.style.height = 'auto';
shell.style.minHeight = '844px';
shell.style.maxHeight = 'none';
shell.style.borderRadius = '0';
shell.style.boxShadow = 'none';
shell.style.overflow = 'visible';
shell.style.display = 'block';
}
document.querySelectorAll('.scroll-content, .page-content, .main-content, .list-wrap, .content').forEach(c => {
c.style.overflow = 'visible';
c.style.maxHeight = 'none';
c.style.height = 'auto';
c.style.flex = 'none';
});
document.body.style.padding = '0';
document.body.style.minHeight = 'auto';
document.body.style.background = '#fff';
});
await page.waitForTimeout(300);
const measured = await page.evaluate(() => {
const shell = document.querySelector('.phone-shell');
if (!shell) return document.body.scrollHeight;
return Math.ceil(shell.getBoundingClientRect().height);
});
const h = Math.max(measured, 844);
await page.setViewportSize({ width: 390, height: h });
await page.waitForTimeout(150);
const shell = await page.$('.phone-shell');
const target = shell || page;
await target.screenshot({
path: path.join(docDir, 'screenshots', `${t.out}.jpg`),
type: 'jpeg',
quality: 85,
});
console.log(`${t.out}.jpg`);
await page.close();
}
await browser.close();
console.log(`\n完成!共截取 ${tasks.length}`);
})();
Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

+1 -1
View File
@@ -675,7 +675,7 @@ stateDiagram-v2
- 周历(含月历弹窗):日期用彩色圆环标记来源(自记/蓝牙称/体重秤/体脂仪),圆环下方显示来源文字标签(自记/蓝牙称/体重秤/体脂仪)
- 当日最新记录区:体重(大字)+ 较上次变化 + BMI(含偏瘦/正常/超重/肥胖标签)+ 体脂率/腰围 + 时间 + 来源标签
- 数据来源卡片:四张来源小卡(体重秤/蓝牙称/体脂仪/自记,各显示该来源最近一次体重值,无记录显示"尚未记录"),点击体重秤/蓝牙称/体脂仪卡片查看当日该来源体重记录,点击自记卡片打开记录体重弹窗
- 变化趋势卡片:时间范围(周/月/季/年/活动期间)+ 三 Tab(体重变化/体脂变化/腰围变化),各含统计行(平均/最大/最小/变化)+ 折线图(初始线/目标线/推荐范围/实际趋势 + 数据点悬浮提示)
- 变化趋势卡片:时间范围(周/月/季/年/活动期间)+ 三 T························································································································································································································································································································ ab(体重变化/体脂变化/腰围变化),各含统计行(平均/最大/最小/变化)+ 折线图(初始线/目标线/推荐范围/实际趋势 + 数据点悬浮提示)
- 记录明细列表:日期 + 时间 + 体重 + 体脂/腰围 + 来源标签;非自记来源带箭头可点击
- 记录体重弹窗:记录体重(必填)+ 记录体脂率(开关)+ 记录腰围(开关),滑动尺子调整数值 + 保存
+4 -4
View File
@@ -237,9 +237,9 @@ const SITE_MAP = [
{ file:'pre-cta-plan-mgmt.html', label:'冠脉CTA单位体检计划管理' },
{ file:'pre-special-cta.html', label:'冠脉CTA体检人员' },
{ file:'pre-special-hospital.html', label:'指定员工体检医院' },
{ file:'mid-appointment-track.html', label:'员工预约跟踪' },
{ file:'mid-selected-items.html', label:'已选项目员工信息' },
{ file:'mid-participation.html', label:'员工参检情况' },
{ file:'mid-appointment-track.html', label:'体检预约' },
{ file:'mid-selected-items.html', label:'已选项目' },
{ file:'mid-participation.html', label:'已检员工' },
{ file:'mid-unit-appointment.html', label:'单位预约人数统计' },
{ file:'mid-hospital-realtime.html', label:'医院实时预约人数' },
{ file:'mid-doctor-realtime.html', label:'医生实时检查情况' },
@@ -659,7 +659,7 @@ const SITE_MAP = [
{ name:'应用主页', dir:'employee-app/home', entry:'index.html', pages:[
{n:'应用主页(收起态)',f:'index.html'},
] },
{ name:'健康体检', dir:'employee-app/health-checkup', entry:'index.html', pages:[
{ name:'健康体检', dir:'employee-app/health-checkup', entry:'index.html', prd:'employee-app/health-checkup/doc/PRD.md', pages:[
{ file:'index.html', label:'健康体检主页' },
{ file:'index-inactive.html', label:'健康体检-未开展' },
{ file:'booking-type.html', label:'选择体检类型' },
+38 -4
View File
@@ -119,6 +119,15 @@
.report-sub__title{font-size:var(--font-sm);font-weight:600;color:var(--text-primary);margin:14px 0 8px;}
.report-sub:first-of-type .report-sub__title{margin-top:0;}
.result-abnormal{color:var(--danger);font-weight:600;}
/* 体检年份选择控件 */
.year-picker{position:relative;}
.year-picker .filter-input{padding-right:30px;cursor:pointer;}
.year-picker>svg{position:absolute;right:9px;top:50%;transform:translateY(-50%);pointer-events:none;}
.year-picker__dropdown{display:none;position:absolute;top:40px;left:0;width:120px;background:#fff;border:1px solid var(--border);border-radius:var(--radius-sm);box-shadow:var(--shadow-md);z-index:50;max-height:180px;overflow-y:auto;}
.year-picker__dropdown.open{display:block;}
.year-picker__dropdown div{padding:8px 14px;font-size:var(--font-sm);cursor:pointer;}
.year-picker__dropdown div:hover{background:var(--primary-light);}
.year-picker__dropdown div.selected{color:var(--primary);font-weight:600;}
</style>
</head>
<body>
@@ -159,10 +168,16 @@
<!-- 筛选栏 -->
<div class="filter-bar">
<div class="filter-item">
<label>体检年份:</label>
<select class="filter-select" style="min-width:90px;">
<option>2026</option><option>2025</option><option>2024</option>
</select>
<label>体检年份:</label>
<div class="year-picker">
<input class="filter-input" type="text" readonly value="2026" style="min-width:90px;">
<svg width="14" height="14" viewBox="0 0 14 14" fill="none"><rect x="1.5" y="2.5" width="11" height="10" rx="1.5" stroke="#999" stroke-width="1.2"/><path d="M1.5 5.5h11M4.5 1v3M9.5 1v3" stroke="#999" stroke-width="1.2" stroke-linecap="round"/></svg>
<div class="year-picker__dropdown">
<div class="selected" onclick="pickYear(this)">2026</div>
<div onclick="pickYear(this)">2025</div>
<div onclick="pickYear(this)">2024</div>
</div>
</div>
</div>
<div class="filter-item">
<label>单位部门:</label>
@@ -573,6 +588,25 @@ function resetFilter(){
showToast('已重置筛选条件');
}
renderBigTable();
/* 体检年份选择控件 */
function pickYear(el){
var dp=el.parentElement,input=dp.parentElement.querySelector('input');
input.value=el.textContent;
dp.querySelectorAll('div').forEach(function(d){d.classList.toggle('selected',d===el);});
dp.classList.remove('open');
}
document.querySelectorAll('.year-picker>input').forEach(function(inp){
inp.addEventListener('click',function(e){
e.stopPropagation();
var dp=inp.parentElement.querySelector('.year-picker__dropdown');
var wasOpen=dp.classList.contains('open');
document.querySelectorAll('.year-picker__dropdown.open').forEach(function(d){d.classList.remove('open');});
if(!wasOpen)dp.classList.add('open');
});
});
document.addEventListener('click',function(){
document.querySelectorAll('.year-picker__dropdown.open').forEach(function(d){d.classList.remove('open');});
});
</script>
</body>
</html>
+47 -9
View File
@@ -110,6 +110,15 @@
.modal__footer{display:flex;align-items:center;justify-content:space-between;padding:12px 24px;border-top:1px solid var(--border);flex-shrink:0;}
.pager{display:flex;align-items:center;gap:6px;}
.pager .page-btn{width:28px;height:28px;}
/* 体检年份选择控件 */
.year-picker{position:relative;}
.year-picker .filter-input{padding-right:30px;cursor:pointer;}
.year-picker>svg{position:absolute;right:9px;top:50%;transform:translateY(-50%);pointer-events:none;}
.year-picker__dropdown{display:none;position:absolute;top:40px;left:0;width:120px;background:#fff;border:1px solid var(--border);border-radius:var(--radius-sm);box-shadow:var(--shadow-md);z-index:50;max-height:180px;overflow-y:auto;}
.year-picker__dropdown.open{display:block;}
.year-picker__dropdown div{padding:8px 14px;font-size:var(--font-sm);cursor:pointer;}
.year-picker__dropdown div:hover{background:var(--primary-light);}
.year-picker__dropdown div.selected{color:var(--primary);font-weight:600;}
</style>
</head>
<body>
@@ -156,10 +165,16 @@
<!-- 筛选栏 -->
<div class="filter-bar">
<div class="filter-item">
<label>体检年份</label>
<select class="filter-select" style="min-width:90px;">
<option>2025</option><option>2024</option><option>2023</option>
</select>
<label>体检年份</label>
<div class="year-picker">
<input class="filter-input" type="text" readonly value="2025" style="min-width:90px;">
<svg width="14" height="14" viewBox="0 0 14 14" fill="none"><rect x="1.5" y="2.5" width="11" height="10" rx="1.5" stroke="#999" stroke-width="1.2"/><path d="M1.5 5.5h11M4.5 1v3M9.5 1v3" stroke="#999" stroke-width="1.2" stroke-linecap="round"/></svg>
<div class="year-picker__dropdown">
<div onclick="pickYear(this)">2026</div>
<div class="selected" onclick="pickYear(this)">2025</div>
<div onclick="pickYear(this)">2024</div>
</div>
</div>
</div>
<div class="filter-item">
<label>单位</label>
@@ -261,7 +276,6 @@ var PERSON_EXTRA={
9:['已选项目数',function(row,i){return 15+i%6;}],
10:['预约日期',function(row,i){return personDate(row,i+5);}],
11:['体检状态',function(){return '<span class="tag tag-green">已检</span>';}],
12:['未检原因',function(row,i){return ['未预约','已预约未体检','弃检'][i%3];}],
16:['异常指标',function(row,i){return ['血压 158/96 mmHg↑','尿酸 428 μmol/L↑','甘油三酯 1.80 mmol/L↑','总胆固醇 5.96 mmol/L↑','空腹血糖 8.2 mmol/L↑','BMI 28.4↑','低密度胆固醇 3.96 mmol/L↑','高密度胆固醇 0.89 mmol/L↓'][i%8];}],
17:['CTA结论',function(row,i){return ['LAD 中段狭窄 25%CAD-RADS 2','钙化积分 Agatston 156(中度)','RCA 近段非钙化斑块、轻度狭窄','LCX 远段钙化斑块、CAD-RADS 2'][i%4];}],
18:['筛查异常项',function(row,i){return ['Septin9 基因甲基化 阳性','CA724 ↑(38.6 U/mL','便潜血 阳性','CEA ↑(6.2 ng/mL'][i%4];}]
@@ -297,17 +311,21 @@ function personRowData(i){
var sex=(seed+i)%2?'女':'男';
var name=SURNAMES[(seed*7+i)%10]+(sex==='男'?GIVEN_M[(seed+i)%8]:GIVEN_F[(seed+i)%8]);
var code='CQ'+String(100000+PERSON_LIST.col*1000+i*13+Math.floor(i/10));
var cells=[''+(i+1),name,code,sex,''+(28+(seed*7+i*11)%30),DEPTS[(seed+i*3)%8],row.hospital,personDate(row,i+1)];
var extra=PERSON_EXTRA[PERSON_LIST.col];
return [''+(i+1),name,code,sex,''+(28+(seed*7+i*11)%30),DEPTS[(seed+i*3)%8],row.hospital,personDate(row,i+1),String(extra[1](row,i)).replace(/<[^>]+>/g,'')];
if(extra)cells.push(String(extra[1](row,i)).replace(/<[^>]+>/g,''));
return cells;
}
function renderPersonPage(){
var extra=PERSON_EXTRA[PERSON_LIST.col];
document.getElementById('person-modal-head').innerHTML='<tr><th>序号</th><th>姓名</th><th>员工编号</th><th>性别</th><th>年龄</th><th>部门</th><th>体检医院</th><th>体检日期</th><th>'+extra[0]+'</th></tr>';
var head='<tr><th>序号</th><th>姓名</th><th>员工编号</th><th>性别</th><th>年龄</th><th>部门</th><th>体检医院</th><th>体检日期</th>';
if(extra)head+='<th>'+extra[0]+'</th>';
document.getElementById('person-modal-head').innerHTML=head+'</tr>';
var start=PERSON_LIST.page*10,html='';
for(var i=start;i<Math.min(start+10,PERSON_LIST.total);i++){
var cells=personRowData(i);
/* 体检状态列回挂绿色标签样式 */
if(PERSON_LIST.col===11)cells[8]='<span class="tag tag-green">'+cells[8]+'</span>';
if(PERSON_LIST.col===11)cells[cells.length-1]='<span class="tag tag-green">'+cells[cells.length-1]+'</span>';
html+='<tr><td>'+cells.join('</td><td>')+'</td></tr>';
}
document.getElementById('person-modal-body').innerHTML=html;
@@ -321,7 +339,8 @@ function hidePersonList(){document.getElementById('person-modal').style.display=
/* 导出当前口径全量人员名单(CSV,Excel 兼容) */
function exportPersonList(){
var extra=PERSON_EXTRA[PERSON_LIST.col];
var head=['序号','姓名','员工编号','性别','年龄','部门','体检医院','体检日期',extra[0]];
var head=['序号','姓名','员工编号','性别','年龄','部门','体检医院','体检日期'];
if(extra)head.push(extra[0]);
var rows=[head];
for(var i=0;i<PERSON_LIST.total;i++)rows.push(personRowData(i));
var csv=''+rows.map(function(r){return r.map(function(c){return '"'+String(c).replace(/"/g,'""')+'"';}).join(',');}).join('\r\n');
@@ -362,6 +381,25 @@ function toggleSubgroup(header){
var sub=active.closest('.sidebar-subgroup');
if(sub)sub.classList.add('open');
})();
/* 体检年份选择控件 */
function pickYear(el){
var dp=el.parentElement,input=dp.parentElement.querySelector('input');
input.value=el.textContent;
dp.querySelectorAll('div').forEach(function(d){d.classList.toggle('selected',d===el);});
dp.classList.remove('open');
}
document.querySelectorAll('.year-picker>input').forEach(function(inp){
inp.addEventListener('click',function(e){
e.stopPropagation();
var dp=inp.parentElement.querySelector('.year-picker__dropdown');
var wasOpen=dp.classList.contains('open');
document.querySelectorAll('.year-picker__dropdown.open').forEach(function(d){d.classList.remove('open');});
if(!wasOpen)dp.classList.add('open');
});
});
document.addEventListener('click',function(){
document.querySelectorAll('.year-picker__dropdown.open').forEach(function(d){d.classList.remove('open');});
});
</script>
</body>
</html>
@@ -99,6 +99,18 @@
.form-input:focus{border-color:var(--primary);box-shadow:0 0 0 2px rgba(24,144,255,0.2);}
.form-textarea{width:100%;min-height:84px;border:1px solid var(--border);border-radius:var(--radius-sm);padding:10px 12px;font-size:var(--font-sm);color:var(--text-primary);outline:none;background:#fff;resize:vertical;font-family:inherit;line-height:1.6;}
.form-textarea:focus{border-color:var(--primary);box-shadow:0 0 0 2px rgba(24,144,255,0.2);}
.form-item__hint{font-size:var(--font-xs);color:#999;margin-top:6px;}
/* 富文本编辑器 */
.rich-editor{border:1px solid var(--border);border-radius:var(--radius-sm);overflow:hidden;background:#fff;}
.rich-editor:focus-within{border-color:var(--primary);}
.rich-editor__toolbar{display:flex;align-items:center;gap:2px;padding:8px;border-bottom:1px solid var(--border);background:#FAFAFA;flex-wrap:wrap;}
.rich-tool{width:28px;height:28px;border:none;background:transparent;border-radius:var(--radius-sm);cursor:pointer;display:flex;align-items:center;justify-content:center;color:var(--text-secondary);font-size:14px;line-height:1;transition:all .15s;}
.rich-tool:hover{background:var(--primary-light);color:var(--primary);}
.rich-tool--wide{width:34px;font-size:12px;font-weight:600;}
.rich-editor__divider{width:1px;height:16px;background:var(--border);margin:0 4px;flex-shrink:0;}
.rich-editor__body{min-height:120px;padding:12px;font-size:var(--font-sm);color:var(--text-primary);line-height:1.8;outline:none;overflow-y:auto;max-height:260px;}
.rich-editor__body:empty:before{content:attr(data-placeholder);color:#999;pointer-events:none;}
.rich-editor__body ol,.rich-editor__body ul{padding-left:20px;margin:6px 0;}
.form-select{width:100%;height:40px;border:1px solid var(--border);border-radius:var(--radius-sm);padding:0 28px 0 12px;font-size:var(--font-sm);color:var(--text-primary);outline:none;background:#fff url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%23999'/%3E%3C/svg%3E") no-repeat right 12px center;appearance:none;cursor:pointer;}
.form-select:focus{border-color:var(--primary);box-shadow:0 0 0 2px rgba(24,144,255,0.2);}
/* 单选按钮 */
@@ -200,6 +212,7 @@
<div class="type-tabs">
<div class="type-tab active" onclick="switchType('normal')">常规体检</div>
<div class="type-tab" onclick="switchType('gastro')">胃肠镜体检</div>
<div class="type-tab" onclick="switchType('cta')">冠脉CTA体检</div>
</div>
<!-- 常规体检 -->
@@ -224,12 +237,12 @@
<table>
<thead>
<tr>
<th>序号</th><th>医院名称</th><th>医院类型</th><th>医院地址</th><th>医院图片</th><th>医院简介</th><th>开启职业体检</th><th>开启冠脉CTA体检</th><th>排序序号</th><th>操作</th>
<th>序号</th><th>医院名称</th><th>医院类型</th><th>医院地址</th><th>医院图片</th><th>医院简介</th><th>开启职业体检</th><th>排序序号</th><th>操作</th>
</tr>
</thead>
<tbody id="normal-tbody">
<tr><td>1</td><td>泾河医院</td><td>普通医院</td><td>西安市高陵区西安泾河开发区CQ西路20号</td><td><img class="hospital-img" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='72' height='48' viewBox='0 0 72 48'%3E%3Crect width='72' height='48' fill='%231890ff'/%3E%3Crect x='18' y='14' width='36' height='26' rx='2' fill='%23ffffff'/%3E%3Cpath d='M32 20h8v6h6v8h-6v6h-8v-6h-6v-8h6z' fill='%231890ff'/%3E%3C/svg%3E" alt="医院图片"></td><td>西安宝石花CQ医院(高陵)</td><td></td><td></td><td>1</td><td><span class="link" onclick="openEditDrawer(this)">编辑</span> <span class="link" style="margin-left:8px;" onclick="deleteRow(this)">删除</span></td></tr>
<tr><td>2</td><td>庆阳医院</td><td>指定医院</td><td>甘肃省庆阳市西峰区岐黄大道18号</td><td><img class="hospital-img" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='72' height='48' viewBox='0 0 72 48'%3E%3Crect width='72' height='48' fill='%231890ff'/%3E%3Crect x='18' y='14' width='36' height='26' rx='2' fill='%23ffffff'/%3E%3Cpath d='M32 20h8v6h6v8h-6v6h-8v-6h-6v-8h6z' fill='%231890ff'/%3E%3C/svg%3E" alt="医院图片"></td><td>庆阳市人民医院体检中心</td><td></td><td></td><td>2</td><td><span class="link" onclick="openEditDrawer(this)">编辑</span> <span class="link" style="margin-left:8px;" onclick="deleteRow(this)">删除</span></td></tr>
<tr><td>1</td><td>泾河医院</td><td>普通医院</td><td>西安市高陵区西安泾河开发区CQ西路20号</td><td><img class="hospital-img" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='72' height='48' viewBox='0 0 72 48'%3E%3Crect width='72' height='48' fill='%231890ff'/%3E%3Crect x='18' y='14' width='36' height='26' rx='2' fill='%23ffffff'/%3E%3Cpath d='M32 20h8v6h6v8h-6v6h-8v-6h-6v-8h6z' fill='%231890ff'/%3E%3C/svg%3E" alt="医院图片"></td><td>西安宝石花CQ医院(高陵)</td><td></td><td>1</td><td><span class="link" onclick="openEditDrawer(this)">编辑</span> <span class="link" style="margin-left:8px;" onclick="deleteRow(this)">删除</span></td></tr>
<tr><td>2</td><td>庆阳医院</td><td>指定医院</td><td>甘肃省庆阳市西峰区岐黄大道18号</td><td><img class="hospital-img" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='72' height='48' viewBox='0 0 72 48'%3E%3Crect width='72' height='48' fill='%231890ff'/%3E%3Crect x='18' y='14' width='36' height='26' rx='2' fill='%23ffffff'/%3E%3Cpath d='M32 20h8v6h6v8h-6v6h-8v-6h-6v-8h6z' fill='%231890ff'/%3E%3C/svg%3E" alt="医院图片"></td><td>庆阳市人民医院体检中心</td><td></td><td>2</td><td><span class="link" onclick="openEditDrawer(this)">编辑</span> <span class="link" style="margin-left:8px;" onclick="deleteRow(this)">删除</span></td></tr>
</tbody>
</table>
</div>
@@ -280,6 +293,46 @@
</div>
</div>
</div>
<!-- 冠脉CTA体检 -->
<div class="tab-pane" id="pane-cta">
<div class="filter-bar">
<div class="filter-item">
<label>医院名称</label>
<input class="filter-input" type="text" placeholder="请输入" style="width:180px;">
</div>
<button class="btn btn-primary">查询</button>
<button class="btn btn-default">重置</button>
</div>
<div style="display:flex;gap:12px;margin-bottom:16px;">
<button class="btn btn-primary" onclick="openDrawer()">+ 新增</button>
<button class="btn btn-default">导出列表数据</button>
</div>
<div class="table-card">
<div class="table-card__header">
<div class="table-card__title">冠脉CTA体检医院信息</div>
</div>
<div class="table-card__content">
<table>
<thead>
<tr>
<th>序号</th><th>医院名称</th><th>医院地址</th><th>医院图片</th><th>医院简介</th><th>排序序号</th><th>操作</th>
</tr>
</thead>
<tbody id="cta-tbody">
<tr><td>1</td><td>泾河医院</td><td>西安市高陵区西安泾河开发区CQ西路20号</td><td><img class="hospital-img" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='72' height='48' viewBox='0 0 72 48'%3E%3Crect width='72' height='48' fill='%231890ff'/%3E%3Crect x='18' y='14' width='36' height='26' rx='2' fill='%23ffffff'/%3E%3Cpath d='M32 20h8v6h6v8h-6v6h-8v-6h-6v-8h6z' fill='%231890ff'/%3E%3C/svg%3E" alt="医院图片"></td><td>西安宝石花CQ医院(高陵)</td><td>1</td><td><span class="link" onclick="openEditDrawer(this)">编辑</span> <span class="link" style="margin-left:8px;" onclick="deleteRow(this)">删除</span></td></tr>
<tr><td>2</td><td>庆阳医院</td><td>甘肃省庆阳市西峰区岐黄大道18号</td><td><img class="hospital-img" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='72' height='48' viewBox='0 0 72 48'%3E%3Crect width='72' height='48' fill='%231890ff'/%3E%3Crect x='18' y='14' width='36' height='26' rx='2' fill='%23ffffff'/%3E%3Cpath d='M32 20h8v6h6v8h-6v6h-8v-6h-6v-8h6z' fill='%231890ff'/%3E%3C/svg%3E" alt="医院图片"></td><td>庆阳市人民医院体检中心</td><td>2</td><td><span class="link" onclick="openEditDrawer(this)">编辑</span> <span class="link" style="margin-left:8px;" onclick="deleteRow(this)">删除</span></td></tr>
</tbody>
</table>
</div>
<div class="pagination">
<span>显示 10 条,共 56 条</span>
<button class="page-btn active">1</button>
<span>每页</span>
<select class="filter-select" style="min-width:70px;height:32px;"><option>10条</option><option>20条</option><option>50条</option></select>
</div>
</div>
</div>
</main>
<!-- 新增医院抽屉 -->
@@ -321,13 +374,6 @@
<span class="switch-label">是否开启职业体检</span>
</div>
</div>
<div class="form-item">
<div class="form-label">开启冠脉CTA体检</div>
<div class="switch-row">
<div class="switch" id="switch-cta" onclick="this.classList.toggle('on')"></div>
<span class="switch-label">是否开启冠脉CTA体检</span>
</div>
</div>
<div class="form-item">
<div class="form-label">排序序号</div>
<input class="form-input" id="form-sort-normal" type="number" placeholder="数字越小越靠前">
@@ -349,13 +395,34 @@
</div>
<div class="form-item" id="card-reg-item" style="display:none;">
<div class="form-label">就诊卡号注册说明</div>
<textarea class="form-textarea" id="form-card-reg" placeholder="请输入就诊卡号注册说明"></textarea>
<div class="rich-editor">
<div class="rich-editor__toolbar">
<button type="button" class="rich-tool" onmousedown="event.preventDefault()" onclick="richCmd('bold')" title="加粗" style="font-weight:700;">B</button>
<button type="button" class="rich-tool" onmousedown="event.preventDefault()" onclick="richCmd('italic')" title="斜体" style="font-style:italic;">I</button>
<button type="button" class="rich-tool" onmousedown="event.preventDefault()" onclick="richCmd('underline')" title="下划线" style="text-decoration:underline;">U</button>
<span class="rich-editor__divider"></span>
<button type="button" class="rich-tool rich-tool--wide" onmousedown="event.preventDefault()" onclick="richCmd('insertOrderedList')" title="编号列表">1.</button>
<button type="button" class="rich-tool rich-tool--wide" onmousedown="event.preventDefault()" onclick="richCmd('insertUnorderedList')" title="项目符号"></button>
<span class="rich-editor__divider"></span>
<button type="button" class="rich-tool rich-tool--wide" onmousedown="event.preventDefault()" onclick="richCmd('removeFormat')" title="清除格式">Tx</button>
</div>
<div class="rich-editor__body" id="form-card-reg" contenteditable="true" data-placeholder="请输入就诊卡号注册说明"></div>
</div>
<div class="form-item__hint">支持加粗、列表等富文本格式,员工端将按此展示</div>
</div>
<div class="form-item">
<div class="form-label">排序序号</div>
<input class="form-input" id="form-sort-gastro" type="number" placeholder="数字越小越靠前">
</div>
</div>
<!-- 冠脉CTA体检字段 -->
<div id="cta-fields" style="display:none;">
<div class="form-item">
<div class="form-label">排序序号</div>
<input class="form-input" id="form-sort-cta" type="number" placeholder="数字越小越靠前">
</div>
</div>
</div>
<div class="drawer__footer">
<button class="btn btn-default" onclick="closeDrawer()">取消</button>
@@ -420,21 +487,23 @@ function resetDrawerFields(){
document.getElementById('hospital-info').classList.remove('show');
document.getElementById('form-sort-normal').value = '';
document.getElementById('form-sort-gastro').value = '';
document.getElementById('form-sort-cta').value = '';
document.querySelectorAll('input[name="hos-type"]').forEach(function(r){ r.checked = false; });
document.getElementById('switch-oc').classList.remove('on');
document.getElementById('switch-cta').classList.remove('on');
document.getElementById('switch-card').classList.remove('on');
document.getElementById('card-reg-item').style.display = 'none';
document.getElementById('form-gastro-notice').value = '';
document.getElementById('form-card-reg').value = '';
document.getElementById('form-card-reg').innerHTML = '';
}
function openDrawer(){
editingRow = null;
var isNormal = currentType === 'normal';
document.getElementById('drawer-title').textContent = isNormal ? '新增常规体检医院' : '新增胃肠镜体检医院';
var isCta = currentType === 'cta';
document.getElementById('drawer-title').textContent = isNormal ? '新增常规体检医院' : (isCta ? '新增冠脉CTA体检医院' : '新增胃肠镜体检医院');
document.getElementById('normal-fields').style.display = isNormal ? '' : 'none';
document.getElementById('gastro-fields').style.display = isNormal ? 'none' : '';
document.getElementById('gastro-fields').style.display = currentType === 'gastro' ? '' : 'none';
document.getElementById('cta-fields').style.display = isCta ? '' : 'none';
resetDrawerFields();
setHospitalLocked(false);
document.getElementById('hospital-drawer').classList.add('open');
@@ -451,6 +520,7 @@ function readTd(tr, i){
function openEditDrawer(btn){
editingRow = btn.closest('tr');
var isNormal = currentType === 'normal';
var isCta = currentType === 'cta';
resetDrawerFields();
if(isNormal){
document.getElementById('drawer-title').textContent = '编辑常规体检医院';
@@ -459,8 +529,14 @@ function openEditDrawer(btn){
setHospitalLocked(true, readTd(editingRow, 1), readTd(editingRow, 3));
document.querySelectorAll('input[name="hos-type"]').forEach(function(r){ r.checked = (r.value === readTd(editingRow, 2)); });
document.getElementById('switch-oc').classList.toggle('on', readTd(editingRow, 6) === '是');
document.getElementById('switch-cta').classList.toggle('on', readTd(editingRow, 7) === '是');
document.getElementById('form-sort-normal').value = readTd(editingRow, 8);
document.getElementById('form-sort-normal').value = readTd(editingRow, 7);
} else if(isCta){
document.getElementById('drawer-title').textContent = '编辑冠脉CTA体检医院';
document.getElementById('normal-fields').style.display = 'none';
document.getElementById('gastro-fields').style.display = 'none';
document.getElementById('cta-fields').style.display = '';
setHospitalLocked(true, readTd(editingRow, 1), readTd(editingRow, 2));
document.getElementById('form-sort-cta').value = readTd(editingRow, 5);
} else {
document.getElementById('drawer-title').textContent = '编辑胃肠镜体检医院';
document.getElementById('normal-fields').style.display = 'none';
@@ -471,7 +547,7 @@ function openEditDrawer(btn){
document.getElementById('card-reg-item').style.display = needCard ? '' : 'none';
document.getElementById('form-sort-gastro').value = readTd(editingRow, 6);
document.getElementById('form-gastro-notice').value = editingRow.dataset.notice || '';
document.getElementById('form-card-reg').value = editingRow.dataset.cardreg || '';
document.getElementById('form-card-reg').innerHTML = editingRow.dataset.cardreg || '';
}
document.getElementById('hospital-drawer').classList.add('open');
document.getElementById('drawer-overlay').classList.add('open');
@@ -482,6 +558,12 @@ function closeDrawer(){
document.getElementById('drawer-overlay').classList.remove('open');
}
/* 富文本编辑器命令 */
function richCmd(cmd){
document.execCommand(cmd, false, null);
document.getElementById('form-card-reg').focus();
}
function toggleCardSwitch(el){
el.classList.toggle('on');
var on = el.classList.contains('on');
@@ -563,7 +645,7 @@ function refreshSeq(tbody){
}
/* 生成常规体检行 HTML */
function buildNormalRow(data, hosType, oc, cta, sort){
function buildNormalRow(data, hosType, oc, sort){
return '<td></td>'
+ '<td>' + data.name + '</td>'
+ '<td>' + hosType + '</td>'
@@ -571,7 +653,17 @@ function buildNormalRow(data, hosType, oc, cta, sort){
+ '<td><img class="hospital-img" src="' + HOSPITAL_IMG + '" alt="医院图片"></td>'
+ '<td>' + data.intro + '</td>'
+ '<td>' + (oc ? '是' : '否') + '</td>'
+ '<td>' + (cta ? '是' : '否') + '</td>'
+ '<td>' + sort + '</td>'
+ ROW_ACTIONS;
}
/* 生成冠脉CTA体检行 HTML */
function buildCtaRow(data, sort){
return '<td></td>'
+ '<td>' + data.name + '</td>'
+ '<td>' + data.address + '</td>'
+ '<td><img class="hospital-img" src="' + HOSPITAL_IMG + '" alt="医院图片"></td>'
+ '<td>' + data.intro + '</td>'
+ '<td>' + sort + '</td>'
+ ROW_ACTIONS;
}
@@ -591,27 +683,29 @@ function buildGastroRow(data, needCard, sort){
/* 保存(新增或编辑) */
function saveHospital(){
var isNormal = currentType === 'normal';
var tbody = document.getElementById(isNormal ? 'normal-tbody' : 'gastro-tbody');
var isCta = currentType === 'cta';
var tbody = document.getElementById(isNormal ? 'normal-tbody' : (isCta ? 'cta-tbody' : 'gastro-tbody'));
if(editingRow){
/* 编辑:医院不变,更新可变列 */
if(isNormal){
var hosTypeEl = document.querySelector('input[name="hos-type"]:checked');
if(!hosTypeEl){ alert('请选择医院类型'); return; }
var sort = document.getElementById('form-sort-normal').value.trim() || editingRow.children[8].textContent.trim();
var sort = document.getElementById('form-sort-normal').value.trim() || editingRow.children[7].textContent.trim();
var oc = document.getElementById('switch-oc').classList.contains('on');
var cta = document.getElementById('switch-cta').classList.contains('on');
editingRow.children[2].textContent = hosTypeEl.value;
editingRow.children[6].textContent = oc ? '是' : '否';
editingRow.children[7].textContent = cta ? '是' : '否';
editingRow.children[8].textContent = sort;
editingRow.children[7].textContent = sort;
} else if(isCta){
var sortC = document.getElementById('form-sort-cta').value.trim() || editingRow.children[5].textContent.trim();
editingRow.children[5].textContent = sortC;
} else {
var needCard = document.getElementById('switch-card').classList.contains('on');
var sortG = document.getElementById('form-sort-gastro').value.trim() || editingRow.children[6].textContent.trim();
editingRow.children[5].textContent = needCard ? '是' : '否';
editingRow.children[6].textContent = sortG;
editingRow.dataset.notice = document.getElementById('form-gastro-notice').value;
editingRow.dataset.cardreg = document.getElementById('form-card-reg').value;
editingRow.dataset.cardreg = document.getElementById('form-card-reg').innerHTML;
}
closeDrawer();
return;
@@ -627,13 +721,16 @@ function saveHospital(){
var hosTypeEl = document.querySelector('input[name="hos-type"]:checked');
if(!hosTypeEl){ alert('请选择医院类型'); return; }
var sort = document.getElementById('form-sort-normal').value.trim() || String(seq);
tr.innerHTML = buildNormalRow(data, hosTypeEl.value, document.getElementById('switch-oc').classList.contains('on'), document.getElementById('switch-cta').classList.contains('on'), sort);
tr.innerHTML = buildNormalRow(data, hosTypeEl.value, document.getElementById('switch-oc').classList.contains('on'), sort);
} else if(isCta){
var sortC = document.getElementById('form-sort-cta').value.trim() || String(seq);
tr.innerHTML = buildCtaRow(data, sortC);
} else {
var needCard = document.getElementById('switch-card').classList.contains('on');
var sortG = document.getElementById('form-sort-gastro').value.trim() || String(seq);
tr.innerHTML = buildGastroRow(data, needCard, sortG);
tr.dataset.notice = document.getElementById('form-gastro-notice').value;
tr.dataset.cardreg = document.getElementById('form-card-reg').value;
tr.dataset.cardreg = document.getElementById('form-card-reg').innerHTML;
}
tbody.appendChild(tr);
tr.children[0].textContent = seq;
@@ -296,11 +296,11 @@ function toggleSubgroup(header){
})();
/* ===== 组合配置交互 ===== */
/* 各医院体检项目 */
/* 各医院体检项目(实际项目名称) */
var comboHospitals = {
'泾河医院': ['一般检查','血常规','尿常规','肝功能','肾功能','血脂四项','空腹血糖','心电图','胸部X光','腹部B超','甲状腺彩超','肿瘤标志物','胃镜检查','肠镜检查'],
'庆阳医院': ['一般检查','血常规','尿常规','肝功','肾功能','血脂四项','空腹血糖','心电图','胸部X光','腹部B超','甲状腺彩超'],
'长安胃肠镜医院': ['胃镜检查','镜检查','幽门螺杆菌检测','无痛胃镜','无痛肠镜','胶囊内镜']
'泾河医院': ['乙肝六项','肝功十一项','胸部CT平扫','小心肌酶四项','甲状腺彩超','颈部血管彩超(颈动脉、椎动脉、锁骨下动脉)','心电图检查','甲胎蛋白','癌胚抗原','糖类抗原199'],
'庆阳医院': ['乙肝六项','肝功十一项','小心肌酶四项','甲状腺彩超','心电图检查','甲胎蛋白','癌胚抗原','糖类抗原199'],
'长安胃肠镜医院': ['普通胃镜检查','无痛胃镜检查','普通肠镜检查','无痛肠镜检查','胶囊内镜检查','幽门螺杆菌碳13呼气试验']
};
var editingComboRow = null; // 新增/编辑组合对应的行
@@ -364,6 +364,10 @@ function openItemsDrawer(btn){
configuringRow = btn.closest('tr');
currentHospital = configuringRow.dataset.hospital || '';
selectedItems = configuringRow.dataset.items ? configuringRow.dataset.items.split('|') : [];
/* 未配置过时默认勾选前3项 */
if(!selectedItems.length){
selectedItems = (comboHospitals[currentHospital] || []).slice(0, 3);
}
document.getElementById('items-drawer-title').textContent = '配置组合项 - ' + (configuringRow.dataset.name || '');
document.getElementById('items-hospital-label').textContent = currentHospital;
document.getElementById('items-search').value = '';
@@ -234,28 +234,28 @@
<table id="guide-table">
<thead>
<tr>
<th>排序</th><th>标题</th><th>讲解类型</th><th>简介</th><th>操作</th>
<th>标题</th><th>讲解类型</th><th>排序</th><th>简介</th><th>操作</th>
</tr>
</thead>
<tbody>
<tr data-title="体检预约流程" data-type="image" data-desc="选择医院、项目、日期,完成体检预约" data-sort="1" data-content="<ol><li>打开「健康体检」,点击「体检预约」进入预约页面</li><li>选择体检医院与体检项目(常规体检 / 胃肠镜体检)</li><li>选择体检日期与时间段</li><li>确认预约信息,预约成功后可在「我的预约」中查看</li></ol>">
<td>1</td><td>体检预约流程</td><td><span class="tag tag-blue">图文讲解</span></td><td>选择医院、项目、日期,完成体检预约</td>
<td>体检预约流程</td><td><span class="tag tag-blue">图文讲解</span></td><td>1</td><td>选择医院、项目、日期,完成体检预约</td>
<td><span class="link" onclick="previewGuide(this)">预览</span> <span class="link" onclick="editRow(this)" style="margin-left:8px;">编辑</span> <span class="link" onclick="deleteRow(this)" style="margin-left:8px;">删除</span></td>
</tr>
<tr data-title="到院体检流程" data-type="image" data-desc="前台登记、逐项检查、领取报告" data-sort="2" data-content="<ol><li>前台登记:携带身份证到体检中心前台登记</li><li>缴费确认:核对单位信息与体检项目</li><li>逐项检查:按指引单到各科室完成检查</li><li>交回指引单:检查完成后将指引单交回前台</li><li>领取报告:报告生成后在线查看或到院领取</li></ol>">
<td>2</td><td>到院体检流程</td><td><span class="tag tag-blue">图文讲解</span></td><td>前台登记、逐项检查、领取报告</td>
<td>到院体检流程</td><td><span class="tag tag-blue">图文讲解</span></td><td>2</td><td>前台登记、逐项检查、领取报告</td>
<td><span class="link" onclick="previewGuide(this)">预览</span> <span class="link" onclick="editRow(this)" style="margin-left:8px;">编辑</span> <span class="link" onclick="deleteRow(this)" style="margin-left:8px;">删除</span></td>
</tr>
<tr data-title="报告查询流程" data-type="image" data-desc="查看报告列表与指标解读详情" data-sort="3" data-content="<ol><li>进入「体检报告」页面</li><li>查看报告列表与最新报告</li><li>点击查看报告详情与指标解读</li></ol>">
<td>3</td><td>报告查询流程</td><td><span class="tag tag-blue">图文讲解</span></td><td>查看报告列表与指标解读详情</td>
<td>报告查询流程</td><td><span class="tag tag-blue">图文讲解</span></td><td>3</td><td>查看报告列表与指标解读详情</td>
<td><span class="link" onclick="previewGuide(this)">预览</span> <span class="link" onclick="editRow(this)" style="margin-left:8px;">编辑</span> <span class="link" onclick="deleteRow(this)" style="margin-left:8px;">删除</span></td>
</tr>
<tr data-title="胃肠镜检查流程" data-type="video" data-desc="检查前准备与检查过程讲解" data-sort="4" data-cover="">
<td>4</td><td>胃肠镜检查流程</td><td><span class="tag tag-green">视频讲解</span></td><td>检查前准备与检查过程讲解</td>
<td>胃肠镜检查流程</td><td><span class="tag tag-green">视频讲解</span></td><td>4</td><td>检查前准备与检查过程讲解</td>
<td><span class="link" onclick="previewGuide(this)">预览</span> <span class="link" onclick="editRow(this)" style="margin-left:8px;">编辑</span> <span class="link" onclick="deleteRow(this)" style="margin-left:8px;">删除</span></td>
</tr>
<tr data-title="体检注意事项" data-type="video" data-desc="体检前饮食、作息等准备事项讲解" data-sort="5" data-cover="">
<td>5</td><td>体检注意事项</td><td><span class="tag tag-green">视频讲解</span></td><td>体检前饮食、作息等准备事项讲解</td>
<td>体检注意事项</td><td><span class="tag tag-green">视频讲解</span></td><td>5</td><td>体检前饮食、作息等准备事项讲解</td>
<td><span class="link" onclick="previewGuide(this)">预览</span> <span class="link" onclick="editRow(this)" style="margin-left:8px;">编辑</span> <span class="link" onclick="deleteRow(this)" style="margin-left:8px;">删除</span></td>
</tr>
</tbody>
@@ -546,9 +546,9 @@ function saveGuide(){
tr.dataset.cover = data.cover || '';
delete tr.dataset.content;
}
tr.innerHTML = '<td>'+data.sort+'</td>'
+ '<td>'+data.title+'</td>'
tr.innerHTML = '<td>'+data.title+'</td>'
+ '<td>'+renderTypeTag(data.type)+'</td>'
+ '<td>'+data.sort+'</td>'
+ '<td>'+data.desc+'</td>'
+ '<td><span class="link" onclick="previewGuide(this)">预览</span> <span class="link" onclick="editRow(this)" style="margin-left:8px;">编辑</span> <span class="link" onclick="deleteRow(this)" style="margin-left:8px;">删除</span></td>';
closeModal();
@@ -260,7 +260,7 @@
<table>
<thead>
<tr>
<th>触发时间</th><th>同步人</th><th>状态</th>
<th>触发时间</th><th>同步人</th><th>状态</th><th>同步数据信息</th>
</tr>
</thead>
<tbody id="sync-task-tbody"></tbody>
@@ -272,6 +272,30 @@
</div>
</div>
<!-- 同步数据信息弹窗 -->
<div class="modal-overlay" id="sync-detail-modal" onclick="if(event.target===this)closeSyncDetailModal()">
<div class="modal modal--wide">
<div class="modal__header">
<div class="modal__title">同步数据信息</div>
<button class="modal__close" onclick="closeSyncDetailModal()"></button>
</div>
<div class="modal__body">
<div id="sync-detail-meta" style="font-size:var(--font-sm);color:var(--text-secondary);margin-bottom:12px;"></div>
<table>
<thead>
<tr>
<th>序号</th><th>医院</th><th>体检项目编码</th><th>体检项目名称</th><th>关联体检基本项</th><th>关联医院系统项目</th><th>同步结果</th>
</tr>
</thead>
<tbody id="sync-detail-tbody"></tbody>
</table>
</div>
<div class="modal__footer">
<button class="btn btn-default" onclick="closeSyncDetailModal()">关闭</button>
</div>
</div>
</div>
<script>
/* 侧边栏展开/折叠交互 */
function toggleSubgroup(header){
@@ -360,10 +384,29 @@ function todayStr(){
/* ===== 同步医院体检项目 & 同步任务记录 ===== */
var syncTasks = [
{ time: '2026-08-28 09:12:30', user: '系统管理员', status: 'success' },
{ time: '2026-08-20 14:05:12', user: '系统管理员', status: 'success' }
{ time: '2026-08-28 09:12:30', user: '系统管理员', status: 'success', items: [
{ hospital: '泾河医院', code: 'JH-XM012', name: '胃镜检查', basic: 'XM012(胃镜)', sys: '胃镜检查套餐', action: '新增' },
{ hospital: '泾河医院', code: 'JH-XM013', name: '肠镜检查', basic: 'XM013(肠镜)', sys: '肠镜检查套餐', action: '新增' },
{ hospital: '泾河医院', code: 'JH-XM002', name: '体重测量', basic: 'XM002(体重)', sys: '身高体重检测, 基础体检套餐', action: '更新' }
]},
{ time: '2026-08-20 14:05:12', user: '系统管理员', status: 'success', items: [
{ hospital: '泾河医院', code: 'JH-XM010', name: '甲状腺彩超', basic: 'XM010(甲状腺)', sys: '甲状腺检查套餐', action: '新增' },
{ hospital: '庆阳医院', code: 'QY-XM003', name: '血压检测', basic: 'XM004(血压)', sys: '内科常规, 深度体检套餐', action: '更新' }
]}
];
/* 模拟一次同步拉取到的增量项目 */
function makeSyncItems(){
var pool = [
{ hospital: '泾河医院', code: 'JH-XM015', name: '糖类抗原199', basic: 'XM015(糖类抗原)', sys: '肿瘤标志物套餐', action: '新增' },
{ hospital: '泾河医院', code: 'JH-XM016', name: '甲胎蛋白', basic: 'XM016(甲胎蛋白)', sys: '肿瘤标志物套餐', action: '新增' },
{ hospital: '泾河医院', code: 'JH-XM001', name: '身高测量', basic: 'XM001(身高)', sys: '身高体重检测, 基础体检套餐', action: '更新' },
{ hospital: '庆阳医院', code: 'QY-XM008', name: '癌胚抗原', basic: 'XM008(癌胚抗原)', sys: '肿瘤标志物套餐', action: '新增' }
];
var n = 2 + Math.floor(Math.random() * 3);
return pool.slice(0, n);
}
function nowTimeStr(){
var d = new Date();
var p = function(n){ return String(n).padStart(2, '0'); };
@@ -377,21 +420,45 @@ function syncStatusTag(status){
return '<span class="tag tag-green">成功</span>';
}
function syncActionTag(action){
if(action === '更新') return '<span class="tag tag-green">更新</span>';
if(action === '失败') return '<span class="tag tag-red">失败</span>';
return '<span class="tag tag-blue">新增</span>';
}
function renderSyncTasks(){
var tbody = document.getElementById('sync-task-tbody');
if(!tbody) return;
if(!syncTasks.length){
tbody.innerHTML = '<tr><td colspan="3" style="text-align:center;color:var(--text-placeholder);">暂无同步任务</td></tr>';
tbody.innerHTML = '<tr><td colspan="4" style="text-align:center;color:var(--text-placeholder);">暂无同步任务</td></tr>';
return;
}
tbody.innerHTML = syncTasks.map(function(t){
return '<tr><td>' + t.time + '</td><td>' + t.user + '</td><td>' + syncStatusTag(t.status) + '</td></tr>';
tbody.innerHTML = syncTasks.map(function(t, i){
return '<tr><td>' + t.time + '</td><td>' + t.user + '</td><td>' + syncStatusTag(t.status) + '</td>'
+ '<td><span class="link" onclick="openSyncDetail(' + i + ')">查看详情</span></td></tr>';
}).join('');
}
/* 查看某次同步的具体项目信息 */
function openSyncDetail(idx){
var t = syncTasks[idx];
if(!t) return;
var items = t.items || [];
document.getElementById('sync-detail-meta').textContent = '触发时间:' + t.time + '  同步人:' + t.user + '  共同步 ' + items.length + ' 条';
document.getElementById('sync-detail-tbody').innerHTML = items.length
? items.map(function(it, i){
return '<tr><td>' + (i + 1) + '</td><td>' + it.hospital + '</td><td>' + it.code + '</td><td>' + it.name + '</td><td>' + it.basic + '</td><td>' + it.sys + '</td><td>' + syncActionTag(it.action) + '</td></tr>';
}).join('')
: '<tr><td colspan="7" style="text-align:center;color:var(--text-placeholder);">暂无同步数据</td></tr>';
document.getElementById('sync-detail-modal').classList.add('open');
}
function closeSyncDetailModal(){
document.getElementById('sync-detail-modal').classList.remove('open');
}
function syncHospitalItems(){
if(!confirm('是否从医院同步增量体检项目?')) return;
var task = { time: nowTimeStr(), user: '系统管理员', status: 'running' };
var task = { time: nowTimeStr(), user: '系统管理员', status: 'running', items: makeSyncItems() };
syncTasks.unshift(task);
renderSyncTasks();
/* 模拟异步同步:2 秒后完成 */
@@ -1,5 +1,10 @@
# 健康体检模块 CHANGELOG
## [Unreleased] 2026-09-14
### Changed
- `archive-unit.html`(档案/单位报表数据):未检人数人员弹窗移除"未检原因"附加列——PERSON_EXTRA 删除 12 号口径映射,弹窗表头与行数据不再包含该列(未检人数口径仅展示序号/姓名/员工编号/性别/年龄/部门/体检医院/体检日期 8 列基础信息);personRowData/renderPersonPage/exportPersonList 改为按 PERSON_EXTRA 是否存在附加列动态拼装表头、行与导出 CSV(已检人数的"已检"绿标签改挂末列,不受列数变化影响),其余 7 个可点击口径弹窗行为不变;mock DOM 回归(未检 8 列、已检 9 列含绿标签、导出 CSV 表头/数据行均 8 字段且无"未检原因")与内联脚本语法校验通过
## [Unreleased] 2026-09-08
### Changed
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,139 @@
/**
* 健康体检管理端PRD 截图生成脚本
*
* 用法 web-admin/health-checkup 目录下执行 node doc/screenshot-prd.js
* 依赖playwrightnpm i playwright && npx playwright install chromium
* 输出doc/screenshots/*.jpg doc/PRD.md ![页面截图](screenshots/xxx.jpg) 引用一一对应
*
* 说明仅做页面级整屏截图1440×900 视口不模拟任何点击交互
* 页面为纯静态原型file:// 直接加载即可完整渲染。
*/
const path = require('path');
const fs = require('fs');
const { chromium } = require('playwright');
const ROOT = path.resolve(__dirname, '..');
const OUT_DIR = path.join(__dirname, 'screenshots');
const TASKS = [
// 一、档案
{ page: 'archive-personal.html', output: 'archive-personal.jpg' },
{ page: 'archive-unit.html', output: 'archive-unit.jpg' },
// 二、基础配置
{ page: 'config-material-notice.html', output: 'config-material-notice.jpg' },
{ page: 'config-material-video.html', output: 'config-material-video.jpg' },
{ page: 'config-hospital-basic.html', output: 'config-hospital-basic.jpg' },
{ page: 'config-hospital-combo.html', output: 'config-hospital-combo.jpg' },
{ page: 'config-report-template.html', output: 'config-report-template.jpg' },
{ page: 'config-standard-category.html', output: 'config-standard-category.jpg' },
{ page: 'config-standard-basic.html', output: 'config-standard-basic.jpg' },
{ page: 'config-standard-hospital.html', output: 'config-standard-hospital.jpg' },
// 三、检前 · 常规体检
{ page: 'pre-hospital-scope.html', output: 'pre-hospital-scope.jpg' },
{ page: 'pre-plan.html', output: 'pre-plan.jpg' },
{ page: 'pre-quota-group.html', output: 'pre-quota-group.jpg' },
{ page: 'pre-quota-personal.html', output: 'pre-quota-personal.jpg' },
{ page: 'pre-item-hospital-fee.html', output: 'pre-item-hospital-fee.jpg' },
{ page: 'pre-item-unit-fee.html', output: 'pre-item-unit-fee.jpg' },
{ page: 'pre-item-combo-template.html', output: 'pre-item-combo-template.jpg' },
{ page: 'pre-item-exception.html', output: 'pre-item-exception.jpg' },
{ page: 'pre-item-required.html', output: 'pre-item-required.jpg' },
{ page: 'pre-special-cancer.html', output: 'pre-special-cancer.jpg' },
{ page: 'pre-special-occupation.html', output: 'pre-special-occupation.jpg' },
{ page: 'pre-special-hospital.html', output: 'pre-special-hospital.jpg' },
// 三、检前 · 胃肠镜体检
{ page: 'pre-gastro-plan.html', output: 'pre-gastro-plan.jpg' },
{ page: 'pre-gastro-employee.html', output: 'pre-gastro-employee.jpg' },
{ page: 'pre-gastro-stat.html', output: 'pre-gastro-stat.jpg' },
{ page: 'pre-gastro-hospital-scope.html', output: 'pre-gastro-hospital-scope.jpg' },
{ page: 'pre-gastro-plan-mgmt.html', output: 'pre-gastro-plan-mgmt.jpg' },
{ page: 'pre-special-gastro.html', output: 'pre-special-gastro.jpg' },
// 三、检前 · 冠脉CTA体检
{ page: 'pre-cta-hospital-scope.html', output: 'pre-cta-hospital-scope.jpg' },
{ page: 'pre-cta-plan-mgmt.html', output: 'pre-cta-plan-mgmt.jpg' },
{ page: 'pre-special-cta.html', output: 'pre-special-cta.jpg' },
// 四、检中 · 常规体检
{ page: 'mid-appointment-track.html', output: 'mid-appointment-track.jpg' },
{ page: 'mid-selected-items.html', output: 'mid-selected-items.jpg' },
{ page: 'mid-participation.html', output: 'mid-participation.jpg' },
{ page: 'mid-unit-appointment.html', output: 'mid-unit-appointment.jpg' },
{ page: 'mid-hospital-realtime.html', output: 'mid-hospital-realtime.jpg' },
{ page: 'mid-doctor-realtime.html', output: 'mid-doctor-realtime.jpg' },
{ page: 'mid-hospital-appointment.html', output: 'mid-hospital-appointment.jpg' },
{ page: 'mid-unit-expected.html', output: 'mid-unit-expected.jpg' },
// 四、检中 · 胃肠镜体检
{ page: 'mid-gastro-appointment-track.html', output: 'mid-gastro-appointment-track.jpg' },
{ page: 'mid-gastro-unit-appointment.html', output: 'mid-gastro-unit-appointment.jpg' },
{ page: 'mid-gastro-questionnaire.html', output: 'mid-gastro-questionnaire.jpg' },
// 四、检中 · 冠脉CTA体检
{ page: 'mid-cta-appointment-track.html', output: 'mid-cta-appointment-track.jpg' },
{ page: 'mid-cta-unit-appointment.html', output: 'mid-cta-unit-appointment.jpg' },
// 五、检后 · 常规体检
{ page: 'post-report-employee.html', output: 'post-report-employee.jpg' },
{ page: 'post-employee-master.html', output: 'post-employee-master.jpg' },
{ page: 'post-illness-personnel.html', output: 'post-illness-personnel.jpg' },
{ page: 'post-indicator-notify.html', output: 'post-indicator-notify.jpg' },
{ page: 'post-report-unmatched.html', output: 'post-report-unmatched.jpg' },
// 五、检后 · 体检统计汇总
{ page: 'post-unit-count.html', output: 'post-unit-count.jpg' },
{ page: 'post-unit-cost-stat.html', output: 'post-unit-cost-stat.jpg' },
{ page: 'post-hospital-count.html', output: 'post-hospital-count.jpg' },
{ page: 'post-doctor-count.html', output: 'post-doctor-count.jpg' },
{ page: 'post-item-stat.html', output: 'post-item-stat.jpg' },
{ page: 'post-cancer-screening.html', output: 'post-cancer-screening.jpg' },
{ page: 'post-standard-stat.html', output: 'post-standard-stat.jpg' },
{ page: 'post-contracted-hospital.html', output: 'post-contracted-hospital.jpg' },
{ page: 'post-key-indicator-stat.html', output: 'post-key-indicator-stat.jpg' },
// 五、检后 · 病种信息管理
{ page: 'post-disease-info.html', output: 'post-disease-info.jpg' },
{ page: 'post-conclusion-disease.html', output: 'post-conclusion-disease.jpg' },
{ page: 'post-auto-select-disease.html', output: 'post-auto-select-disease.jpg' },
// 五、检后 · 胃肠镜体检
{ page: 'post-report-gastro.html', output: 'post-report-gastro.jpg' },
{ page: 'post-gastro-stat.html', output: 'post-gastro-stat.jpg' },
// 五、检后 · 冠脉CTA体检
{ page: 'post-cta-report.html', output: 'post-cta-report.jpg' },
{ page: 'post-cta-stat.html', output: 'post-cta-stat.jpg' },
// 六、异常事件
{ page: 'exception-dirty.html', output: 'exception-dirty.jpg' },
{ page: 'exception-result.html', output: 'exception-result.jpg' },
];
(async () => {
fs.mkdirSync(OUT_DIR, { recursive: true });
const browser = await chromium.launch();
const context = await browser.newContext({
viewport: { width: 1440, height: 900 },
deviceScaleFactor: 1,
});
const page = await context.newPage();
let failed = 0;
for (const task of TASKS) {
const file = path.join(ROOT, task.page);
if (!fs.existsSync(file)) {
console.error('✗ 缺少文件:', task.page);
failed++;
continue;
}
const url = 'file:///' + file.replace(/\\/g, '/');
try {
await page.goto(url, { waitUntil: 'load' });
await page.waitForTimeout(300); // 等待字体/布局稳定
await page.screenshot({
path: path.join(OUT_DIR, task.output),
type: 'jpeg',
quality: 85,
fullPage: true,
});
console.log('✓', task.output);
} catch (err) {
console.error('✗ 截图失败:', task.page, err.message);
failed++;
}
}
await browser.close();
console.log(`\n完成:成功 ${TASKS.length - failed} / ${TASKS.length} 张 → ${OUT_DIR}`);
if (failed > 0) process.exit(1);
})();
Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Some files were not shown because too many files have changed in this diff Show More